diff --git a/src/common/menu.rs b/src/common/menu.rs index 9bfd1cd..5a90327 100644 --- a/src/common/menu.rs +++ b/src/common/menu.rs @@ -96,7 +96,7 @@ pub unsafe fn set_menu_from_json(message: &str) { skyline::error::show_error( 0x70, "Could not parse the menu response!\nPlease send a screenshot of the details page to the developers.\n\0", - &*format!("{:#?}\0", web_response.err().unwrap()) + &*format!("{:#?}\0", message) ); }; if MENU.quick_menu == OnOff::Off { diff --git a/src/lib.rs b/src/lib.rs index 74fbed9..5a95d43 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -54,6 +54,27 @@ macro_rules! c_str { #[skyline::main(name = "training_modpack")] pub fn main() { + std::panic::set_hook(Box::new(|info| { + let location = info.location().unwrap(); + + let msg = match info.payload().downcast_ref::<&'static str>() { + Some(s) => *s, + None => { + match info.payload().downcast_ref::() { + Some(s) => &s[..], + None => "Box", + } + }, + }; + + let err_msg = format!("thread has panicked at '{}', {}", msg, location); + skyline::error::show_error( + 69, + "Skyline plugin has panicked! Please open the details and send a screenshot to the developer, then close the game.\n", + err_msg.as_str(), + ); + })); + macro_rules! log { ($($arg:tt)*) => { println!("{}{}", "[Training Modpack] ".green(), format!($($arg)*)); diff --git a/src/static/css/nouislider.min.css b/src/static/css/nouislider.min.css new file mode 100644 index 0000000..60f217c --- /dev/null +++ b/src/static/css/nouislider.min.css @@ -0,0 +1 @@ +.noUi-target,.noUi-target *{-webkit-touch-callout:none;-webkit-tap-highlight-color:transparent;-webkit-user-select:none;-ms-touch-action:none;touch-action:none;-ms-user-select:none;-moz-user-select:none;user-select:none;-moz-box-sizing:border-box;box-sizing:border-box}.noUi-target{position:relative}.noUi-base,.noUi-connects{width:100%;height:100%;position:relative;z-index:1}.noUi-connects{overflow:hidden;z-index:0}.noUi-connect,.noUi-origin{will-change:transform;position:absolute;z-index:1;top:0;right:0;height:100%;width:100%;-ms-transform-origin:0 0;-webkit-transform-origin:0 0;-webkit-transform-style:preserve-3d;transform-origin:0 0;transform-style:flat}.noUi-txt-dir-rtl.noUi-horizontal .noUi-origin{left:0;right:auto}.noUi-vertical .noUi-origin{top:-100%;width:0}.noUi-horizontal .noUi-origin{height:0}.noUi-handle{-webkit-backface-visibility:hidden;backface-visibility:hidden;position:absolute}.noUi-touch-area{height:100%;width:100%}.noUi-state-tap .noUi-connect,.noUi-state-tap .noUi-origin{-webkit-transition:transform .3s;transition:transform .3s}.noUi-state-drag *{cursor:inherit!important}.noUi-horizontal{height:18px}.noUi-horizontal .noUi-handle{width:34px;height:28px;right:-17px;top:-6px}.noUi-vertical{width:18px}.noUi-vertical .noUi-handle{width:28px;height:34px;right:-6px;bottom:-17px}.noUi-txt-dir-rtl.noUi-horizontal .noUi-handle{left:-17px;right:auto}.noUi-target{background:#FAFAFA;border-radius:4px;border:1px solid #D3D3D3;box-shadow:inset 0 1px 1px #F0F0F0,0 3px 6px -5px #BBB}.noUi-connects{border-radius:3px}.noUi-connect{background:#3FB8AF}.noUi-draggable{cursor:ew-resize}.noUi-vertical .noUi-draggable{cursor:ns-resize}.noUi-handle{border:1px solid #D9D9D9;border-radius:3px;background:#FFF;cursor:default;box-shadow:inset 0 0 1px #FFF,inset 0 1px 7px #EBEBEB,0 3px 6px -3px #BBB}.noUi-active{box-shadow:inset 0 0 1px #FFF,inset 0 1px 7px #DDD,0 3px 6px -3px #BBB}.noUi-handle:after,.noUi-handle:before{content:"";display:block;position:absolute;height:14px;width:1px;background:#E8E7E6;left:14px;top:6px}.noUi-handle:after{left:17px}.noUi-vertical .noUi-handle:after,.noUi-vertical .noUi-handle:before{width:14px;height:1px;left:6px;top:14px}.noUi-vertical .noUi-handle:after{top:17px}[disabled] .noUi-connect{background:#B8B8B8}[disabled] .noUi-handle,[disabled].noUi-handle,[disabled].noUi-target{cursor:not-allowed}.noUi-pips,.noUi-pips *{-moz-box-sizing:border-box;box-sizing:border-box}.noUi-pips{position:absolute;color:#999}.noUi-value{position:absolute;white-space:nowrap;text-align:center}.noUi-value-sub{color:#ccc;font-size:10px}.noUi-marker{position:absolute;background:#CCC}.noUi-marker-sub{background:#AAA}.noUi-marker-large{background:#AAA}.noUi-pips-horizontal{padding:10px 0;height:80px;top:100%;left:0;width:100%}.noUi-value-horizontal{-webkit-transform:translate(-50%,50%);transform:translate(-50%,50%)}.noUi-rtl .noUi-value-horizontal{-webkit-transform:translate(50%,50%);transform:translate(50%,50%)}.noUi-marker-horizontal.noUi-marker{margin-left:-1px;width:2px;height:5px}.noUi-marker-horizontal.noUi-marker-sub{height:10px}.noUi-marker-horizontal.noUi-marker-large{height:15px}.noUi-pips-vertical{padding:0 10px;height:100%;top:0;left:100%}.noUi-value-vertical{-webkit-transform:translate(0,-50%);transform:translate(0,-50%);padding-left:25px}.noUi-rtl .noUi-value-vertical{-webkit-transform:translate(0,50%);transform:translate(0,50%)}.noUi-marker-vertical.noUi-marker{width:5px;height:2px;margin-top:-1px}.noUi-marker-vertical.noUi-marker-sub{width:10px}.noUi-marker-vertical.noUi-marker-large{width:15px}.noUi-tooltip{display:block;position:absolute;border:1px solid #D9D9D9;border-radius:3px;background:#fff;color:#000;padding:5px;text-align:center;white-space:nowrap}.noUi-horizontal .noUi-tooltip{-webkit-transform:translate(-50%,0);transform:translate(-50%,0);left:50%;bottom:120%}.noUi-vertical .noUi-tooltip{-webkit-transform:translate(0,-50%);transform:translate(0,-50%);top:50%;right:120%}.noUi-horizontal .noUi-origin>.noUi-tooltip{-webkit-transform:translate(50%,0);transform:translate(50%,0);left:auto;bottom:10px}.noUi-vertical .noUi-origin>.noUi-tooltip{-webkit-transform:translate(0,-18px);transform:translate(0,-18px);top:auto;right:28px} \ No newline at end of file diff --git a/src/static/css/training_modpack.css b/src/static/css/training_modpack.css index dacc919..6d1f404 100644 --- a/src/static/css/training_modpack.css +++ b/src/static/css/training_modpack.css @@ -185,7 +185,10 @@ body { visibility: hidden; } -:focus { +:focus:not(.noUi-handle), +.handleSelected, +.noUi-connect +{ background: rgb(255, 70, 2); background: linear-gradient( 45deg, @@ -302,3 +305,38 @@ body { text-align: center; font-size: large; } + +.modal-slider { + width: 70%; + margin-bottom: 170px; +} + +.modal-slider-label { + width: 100%; + display: flex; + justify-content: center; +} + +.modal-slider-label > p { + font-size: 26px; + padding: 0.5rem; + background: #ccc; + border-color: black; + border-radius: 0.5rem; + border-style: solid; + border-width: 0.25rem; +} + +.noUi-value, .noUi-pips { + margin-top: 12px; + color: #000; +} + +.noUi-marker, .noUi-marker-large { + background: #000; +} + +.noUi-tooltip { + padding: 12px; + font-size: 1.5rem; +} diff --git a/src/static/img/save_damage.svg b/src/static/img/save_damage_cpu.svg similarity index 100% rename from src/static/img/save_damage.svg rename to src/static/img/save_damage_cpu.svg diff --git a/src/static/img/save_damage_limits_cpu.svg b/src/static/img/save_damage_limits_cpu.svg new file mode 100644 index 0000000..8273d98 --- /dev/null +++ b/src/static/img/save_damage_limits_cpu.svg @@ -0,0 +1,83 @@ + + + + + + + + + + image/svg+xml + + + + + + + + + + + + diff --git a/src/static/img/save_damage_limits_player.svg b/src/static/img/save_damage_limits_player.svg new file mode 100644 index 0000000..8273d98 --- /dev/null +++ b/src/static/img/save_damage_limits_player.svg @@ -0,0 +1,83 @@ + + + + + + + + + + image/svg+xml + + + + + + + + + + + + diff --git a/src/static/img/save_damage_player.svg b/src/static/img/save_damage_player.svg new file mode 100644 index 0000000..a42ff93 --- /dev/null +++ b/src/static/img/save_damage_player.svg @@ -0,0 +1,75 @@ + + + + + + + + + + image/svg+xml + + + + + + + + + + + diff --git a/src/static/js/nouislider.min.js b/src/static/js/nouislider.min.js new file mode 100644 index 0000000..ca2b37b --- /dev/null +++ b/src/static/js/nouislider.min.js @@ -0,0 +1 @@ +!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?e(exports):"function"==typeof define&&define.amd?define(["exports"],e):e((t="undefined"!=typeof globalThis?globalThis:t||self).noUiSlider={})}(this,function(ot){"use strict";function n(t){return"object"==typeof t&&"function"==typeof t.to}function st(t){t.parentElement.removeChild(t)}function at(t){return null!=t}function lt(t){t.preventDefault()}function i(t){return"number"==typeof t&&!isNaN(t)&&isFinite(t)}function ut(t,e,r){0=e[r];)r+=1;return r}function r(t,e,r){if(r>=t.slice(-1)[0])return 100;var n=l(r,t),i=t[n-1],o=t[n],t=e[n-1],n=e[n];return t+(r=r,a(o=[i,o],o[0]<0?r+Math.abs(o[0]):r-o[0],0)/s(t,n))}function o(t,e,r,n){if(100===n)return n;var i=l(n,t),o=t[i-1],s=t[i];return r?(s-o)/2this.xPct[n+1];)n++;else t===this.xPct[this.xPct.length-1]&&(n=this.xPct.length-2);r||t!==this.xPct[n+1]||n++;for(var i,o=1,s=(e=null===e?[]:e)[n],a=0,l=0,u=0,c=r?(t-this.xPct[n])/(this.xPct[n+1]-this.xPct[n]):(this.xPct[n+1]-t)/(this.xPct[n+1]-this.xPct[n]);0= 2) required for mode 'count'.");for(var e=t.values-1,r=100/e,n=[];e--;)n[e]=e*r;return n.push(100),U(n,t.stepped)}(d),m={},t=S.xVal[0],e=S.xVal[S.xVal.length-1],g=!1,v=!1,b=0;return(h=h.slice().sort(function(t,e){return t-e}).filter(function(t){return!this[t]&&(this[t]=!0)},{}))[0]!==t&&(h.unshift(t),g=!0),h[h.length-1]!==e&&(h.push(e),v=!0),h.forEach(function(t,e){var r,n,i,o,s,a,l,u,t=t,c=h[e+1],p=d.mode===ot.PipsMode.Steps,f=(f=p?S.xNumSteps[e]:f)||c-t;for(void 0===c&&(c=t),f=Math.max(f,1e-7),r=t;r<=c;r=Number((r+f).toFixed(7))){for(a=(o=(i=S.toStepping(r))-b)/(d.density||1),u=o/(l=Math.round(a)),n=1;n<=l;n+=1)m[(s=b+n*u).toFixed(5)]=[S.fromStepping(s),0];a=-1ot.PipsType.NoValue&&((t=P(a,!1)).className=p(n,f.cssClasses.value),t.setAttribute("data-value",String(r)),t.style[f.style]=e+"%",t.innerHTML=String(s.to(r))))}),a}function L(){n&&(st(n),n=null)}function T(t){L();var e=D(t),r=t.filter,t=t.format||{to:function(t){return String(Math.round(t))}};return n=d.appendChild(O(e,r,t))}function j(){var t=i.getBoundingClientRect(),e="offset"+["Width","Height"][f.ort];return 0===f.ort?t.width||i[e]:t.height||i[e]}function z(n,i,o,s){function e(t){var e,r=function(e,t,r){var n=0===e.type.indexOf("touch"),i=0===e.type.indexOf("mouse"),o=0===e.type.indexOf("pointer"),s=0,a=0;0===e.type.indexOf("MSPointer")&&(o=!0);if("mousedown"===e.type&&!e.buttons&&!e.touches)return!1;if(n){var l=function(t){t=t.target;return t===r||r.contains(t)||e.composed&&e.composedPath().shift()===r};if("touchstart"===e.type){n=Array.prototype.filter.call(e.touches,l);if(1r.stepAfter.startValue&&(i=r.stepAfter.startValue-n),t=n>r.thisStep.startValue?r.thisStep.step:!1!==r.stepBefore.step&&n-r.stepBefore.highestStep,100===e?i=null:0===e&&(t=null);e=S.countStepDecimals();return null!==i&&!1!==i&&(i=Number(i.toFixed(e))),[t=null!==t&&!1!==t?Number(t.toFixed(e)):t,i]}ft(t=d,f.cssClasses.target),0===f.dir?ft(t,f.cssClasses.ltr):ft(t,f.cssClasses.rtl),0===f.ort?ft(t,f.cssClasses.horizontal):ft(t,f.cssClasses.vertical),ft(t,"rtl"===getComputedStyle(t).direction?f.cssClasses.textDirectionRtl:f.cssClasses.textDirectionLtr),i=P(t,f.cssClasses.base),function(t,e){var r=P(e,f.cssClasses.connects);l=[],(a=[]).push(N(r,t[0]));for(var n=0;n { + switch (event.keyCode) { + case 37: // Control stick left + decreaseSelectedHandle(); + break; + case 39: // Control stick right + increaseSelectedHandle(); + break; + } + }) } else { - document.addEventListener('keypress', (event) => { + document.addEventListener('keydown', (event) => { switch (event.key) { + case 'a': + console.log('a'); + select(document.activeElement); + break; case 'b': console.log('b'); closeOrExit(); @@ -58,6 +72,14 @@ if (isNx) { console.log('o'); cyclePrevTab(); break; + case 'ArrowLeft': + console.log('ArrowLeft'); + decreaseSelectedHandle(); + break; + case 'ArrowRight': + console.log('ArrowRight'); + increaseSelectedHandle(); + break; } }); } @@ -65,6 +87,13 @@ if (isNx) { const onLoad = () => { // Activate the first tab openTab(document.querySelector('button.tab-button')); + initializeAllSliders(); + if (isNx) { + window.nx.sendMessage("loaded"); + } else { + settings = {}; + setSettingsFromJSON("{\"menu\":{\"aerial_delay\":0,\"air_dodge_dir\":0,\"attack_angle\":0,\"buff_state\":0,\"character_item\":0,\"clatter_strength\":0,\"crouch\":0,\"di_state\":0,\"falling_aerials\":0,\"fast_fall_delay\":0,\"fast_fall\":0,\"follow_up\":0,\"frame_advantage\":0,\"full_hop\":0,\"hitbox_vis\":1,\"input_delay\":1,\"ledge_delay\":0,\"ledge_state\":31,\"mash_state\":0,\"mash_triggers\":131,\"miss_tech_state\":15,\"oos_offset\":0,\"pummel_delay\":0,\"quick_menu\":0,\"reaction_time\":0,\"save_damage\":4,\"save_damage_limits\":[63,106],\"save_state_autoload\":1,\"save_state_enable\":1,\"save_state_mirroring\":1,\"sdi_state\":0,\"sdi_strength\":0,\"shield_state\":0,\"shield_tilt\":0,\"stage_hazards\":0,\"tech_state\":15,\"throw_delay\":0,\"throw_state\":1},\"defaults_menu\":{\"aerial_delay\":0,\"air_dodge_dir\":0,\"attack_angle\":0,\"buff_state\":0,\"character_item\":0,\"clatter_strength\":0,\"crouch\":0,\"di_state\":0,\"falling_aerials\":0,\"fast_fall_delay\":0,\"fast_fall\":0,\"follow_up\":0,\"frame_advantage\":0,\"full_hop\":0,\"hitbox_vis\":1,\"input_delay\":1,\"ledge_delay\":0,\"ledge_state\":31,\"mash_state\":0,\"mash_triggers\":131,\"miss_tech_state\":15,\"oos_offset\":0,\"pummel_delay\":0,\"quick_menu\":0,\"reaction_time\":0,\"save_damage\":4,\"save_damage_limits\":[41,118],\"save_state_autoload\":1,\"save_state_enable\":1,\"save_state_mirroring\":1,\"sdi_state\":0,\"sdi_strength\":0,\"shield_state\":0,\"shield_tilt\":0,\"stage_hazards\":0,\"tech_state\":15,\"throw_delay\":0,\"throw_state\":1}}"); + } }; window.onload = onLoad; @@ -73,6 +102,8 @@ var settings; var defaultSettings; var lastFocusedItem = document.querySelector('.menu-item > button'); +var selectedSliderHandle = -1; + const currentTabContent = () => { const currentActiveTab = document.querySelector('.tab-button.active'); @@ -114,7 +145,11 @@ const openMenuItem = (eventTarget) => { currentTabContent().classList.toggle('hide'); modal.classList.toggle('hide'); - modal.querySelector('button').focus(); + elem = modal.querySelector('button'); + if (!elem) { + elem = modal.querySelector('.noUi-handle-lower') + } + elem.focus(); lastFocusedItem = eventTarget; }; @@ -195,6 +230,19 @@ const exit = () => { }; function closeOrExit() { + // Deselect any sliders + handlesWereSelected = deselectSliderHandles(); + if (handlesWereSelected) {return} + + selectedHandles = document.querySelectorAll(".handleSelected"); + if (selectedHandles.length) { + console.log("Found selected handles"); + selectedHandles.forEach((handle) => { + handle.classList.remove("handleSelected"); + }); + return; + } + // Close any open menus if (document.querySelector('.modal:not(.hide)')) { console.log('Closing Items'); @@ -210,52 +258,12 @@ function closeOrExit() { function setSettingsFromJSON(msg) { // Receive a menu message and set settings - var msg_json = JSON.parse(msg.data); + var msg_json = JSON.parse(msg); settings = msg_json["menu"]; defaultSettings = msg_json["defaults_menu"]; populateMenuFromSettings(); } -function setSettingsFromURL() { - var { search } = window.location; - // Actual settings - const settingsFromSearch = search - .replace('?', '') - .split('&') - .reduce((accumulator, currentValue) => { - var [key, value] = currentValue.split('='); - if (!key.startsWith('__')) { - accumulator[key] = parseInt(value); - } - return accumulator; - }, {}); - settings = settingsFromSearch; - - // Default settings - const defaultSettingsFromSearch = search - .replace('?', '') - .split('&') - .reduce((accumulator, currentValue) => { - var [key, value] = currentValue.split('='); - if (key.startsWith('__')) { - accumulator[key.replace('__','')] = parseInt(value); - } - return accumulator; - }, {}); - defaultSettings = defaultSettingsFromSearch; - populateMenuFromSettings() -} - -function buildURLFromSettings() { - const url = 'http://localhost/?'; - - const urlParams = Object.entries(settings).map((setting) => { - return `${setting[0]}=${setting[1]}`; - }); - - return url + urlParams.join('&'); -} - function selectSingleOption(eventTarget) { // Deselect all options in the submenu const parent = eventTarget.parentElement; @@ -277,19 +285,26 @@ const isValueInBitmask = (value, mask) => (mask & value) != 0; const setOptionsForMenu = (menuId) => { const modal = document.querySelector(`.modal[data-id="${menuId}"]`); - modal.querySelectorAll('.menu-icon').forEach(function (toggle) { - if (isValueInBitmask(toggle.dataset.val, settings[menuId])) { - toggle.classList.remove('hidden'); - } else { - toggle.classList.add('hidden'); - } - }); + if (modal.querySelector('.modal-button')) { + // Toggle menu + modal.querySelectorAll('.menu-icon').forEach(function (toggle) { + if (isValueInBitmask(toggle.dataset.val, settings[menuId])) { + toggle.classList.remove('hidden'); + } else { + toggle.classList.add('hidden'); + } + }); - if (modal.classList.contains('single-option')) { - // If no option is selected default to the first option - if (modal.querySelectorAll('.menu-icon:not(.hidden)').length === 0) { - selectSingleOption(modal.querySelector('button')); + if (modal.classList.contains('single-option')) { + // If no option is selected default to the first option + if (modal.querySelectorAll('.menu-icon:not(.hidden)').length === 0) { + selectSingleOption(modal.querySelector('button')); + } } + } else { + // Slider menu + slider = modal.querySelector('.modal-slider'); + setSliderVals(slider, settings[menuId]); } }; @@ -297,17 +312,25 @@ function populateMenuFromSettings() { document.querySelectorAll('.menu-item').forEach((item) => setOptionsForMenu(item.id)); } -function getMaskFromMenuID(id) { - var value = 0; +function getSettingsValFromMenuID(id) { const modal = document.querySelector(`.modal[data-id='${id}']`); - const options = modal.querySelectorAll('img:not(.hidden)'); + if (modal.querySelector('.modal-button')) { + // Toggle menu + // Return value is a bitmask + var value = 0; + const options = modal.querySelectorAll('img:not(.hidden)'); - options.forEach(function (toggle) { - value += parseInt(toggle.dataset.val); - }); - - return value; + options.forEach(function (toggle) { + value += parseInt(toggle.dataset.val); + }); + return value; + } else { + // Slider menu + // Return value is a [lower,upper] array + slider = modal.querySelector('.modal-slider'); + return getSliderVals(slider); + } } function resetCurrentMenu() { @@ -315,10 +338,11 @@ function resetCurrentMenu() { const menu = document.querySelector('.modal:not(.hide)'); const menuId = menu.dataset.id; - const defaultSectionMask = defaultSettings[menuId]; + const defaultSubmenuSetting = defaultSettings[menuId]; - settings[menuId] = defaultSectionMask; + settings[menuId] = defaultSubmenuSetting; + deselectSliderHandles(); populateMenuFromSettings(); } @@ -327,10 +351,11 @@ function resetAllMenus() { if (confirm('Are you sure that you want to reset all menu settings to the default?')) { document.querySelectorAll('.menu-item').forEach(function (item) { const defaultMenuId = item.id; - const defaultMask = defaultSettings[defaultMenuId]; + const defaultSubmenuSetting = defaultSettings[defaultMenuId]; - settings[item.id] = defaultMask; + settings[item.id] = defaultSubmenuSetting; + deselectSliderHandles(); populateMenuFromSettings(); }); } @@ -344,13 +369,13 @@ function saveDefaults() { if (confirm('Are you sure that you want to change the default menu settings to the current selections?')) { document.querySelectorAll('.menu-item').forEach((item) => { const menu = item.id; - - defaultSettings[menu] = getMaskFromMenuID(item.id); + defaultSettings[menu] = getSettingsValFromMenuID(item.id); }); } } function cycleNextTab() { + deselectSliderHandles(); // Cycle to the next tab const activeTab = document.querySelector('.tab-button.active'); var nextTab = activeTab.nextElementSibling; @@ -362,6 +387,7 @@ function cycleNextTab() { } function cyclePrevTab() { + deselectSliderHandles(); // Cycle to the previous tab const activeTab = document.querySelector('.tab-button.active'); var previousTab = activeTab.previousElementSibling; @@ -372,3 +398,124 @@ function cyclePrevTab() { } openTab(previousTab); } + +function getSliderVals(slider) { + var arr = slider.noUiSlider.get(); + return [parseFloat(arr[0]), parseFloat(arr[1])] +} + +function setSliderVals(slider, vals) { + slider.noUiSlider.set(vals); +} + +function setSettingsFromSlider(slider) { + menuId = closestClass(slider, "modal").dataset.id; + settings[menuId] = getSliderVals(slider) +} + +function initializeSlider(slider) { + noUiSlider.create( + slider, + { + start: [ + parseFloat(slider.dataset.selectedMin), + parseFloat(slider.dataset.selectedMax), + ], + connect: true, + range: { + 'min': parseFloat(slider.dataset.absMin), + 'max': parseFloat(slider.dataset.absMax), + }, + step: 1, + tooltips: [ + { to: function (value) { return value.toFixed(0) + '%'; } }, + { to: function (value) { return value.toFixed(0) + '%'; } }, + ], + pips: { + mode: 'range', + density: 10, + }, + keyboardMultiplier: 0, // Prevents doublestepping with custom implementation + } + ); + slider.noUiSlider.on('set', function () { setSettingsFromSlider(slider) }); +} + +function initializeAllSliders() { + document.querySelectorAll(".modal-slider").forEach((item) => { + initializeSlider(item); + }); +} + +function select(element) { + if (element.classList.contains("noUi-handle")) { + element.classList.toggle("handleSelected"); + } + element.click(); +} + +function increaseSelectedHandle() { + // Increments the selected slider handle, if one is selected + // Won't go past the slider limit + handle = document.querySelector(".noUi-handle.handleSelected"); + if (handle) { + slider = closestClass(handle, "modal-slider"); + isLowerHandle = handle.classList.contains("noUi-handle-lower"); + step = slider.noUiSlider.options.step; + currentVals = getSliderVals(slider); + if (isLowerHandle) { + setSliderVals( + slider, + [currentVals[0] + step, null] + ); + } else { + setSliderVals( + slider, + [null, currentVals[1] + step] + ); + } + // Refocus the handle, since the native navigation might focus the other handle + // TODO: Is there a more elegant way to do this? + setTimeout( function() {handle.focus() }, 15); + } +} + +function decreaseSelectedHandle() { + // Decrements the selected slider handle, if one is selected + // Won't go past the slider limit + handle = document.querySelector(".noUi-handle.handleSelected"); + if (handle) { + slider = closestClass(handle, "modal-slider"); + isLowerHandle = handle.classList.contains("noUi-handle-lower"); + step = slider.noUiSlider.options.step; + currentVals = getSliderVals(slider); + if (isLowerHandle) { + setSliderVals( + slider, + [currentVals[0] - step, null] + ); + } else { + setSliderVals( + slider, + [null, currentVals[1] - step] + ); + } + // Refocus the handle, since the native navigation might focus the other handle + // TODO: Is there a more elegant way to do this? + setTimeout( function() {handle.focus() }, 15); + } +} + +function deselectSliderHandles() { + /// Returns true if any slider handles were changed from selected -> deselected + /// Returns false if there were no selected slider handles to begin with + selectedHandles = document.querySelectorAll(".handleSelected"); + if (selectedHandles.length) { + selectedHandles.forEach((handle) => { + handle.classList.remove("handleSelected"); + }); + return true; + } else { + return false; + } +} diff --git a/src/templates/menu.html b/src/templates/menu.html index 7df51ea..b4bb091 100644 --- a/src/templates/menu.html +++ b/src/templates/menu.html @@ -5,10 +5,12 @@ Modpack Menu + + {{/toggles}} + {{#slider}} + + + {{/slider}} {{/tab_submenus}}
diff --git a/src/training/save_states.rs b/src/training/save_states.rs index aa4e8bd..e30f9c2 100644 --- a/src/training/save_states.rs +++ b/src/training/save_states.rs @@ -1,4 +1,5 @@ use crate::common::button_config; +use crate::common::consts::get_random_float; use crate::common::consts::get_random_int; use crate::common::consts::FighterId; use crate::common::consts::OnOff; @@ -15,7 +16,7 @@ use smash::app::{self, lua_bind::*, Item}; use smash::hash40; use smash::lib::lua_const::*; use smash::phx::{Hash40, Vector3f}; -use training_mod_consts::CharacterItem; +use training_mod_consts::{CharacterItem, SaveDamage}; #[derive(PartialEq)] enum SaveState { @@ -149,16 +150,6 @@ pub unsafe fn get_param_int( } fn set_damage(module_accessor: &mut app::BattleObjectModuleAccessor, damage: f32) { - let overwrite_damage; - - unsafe { - overwrite_damage = MENU.save_damage == OnOff::On; - } - - if !overwrite_damage { - return; - } - unsafe { DamageModule::heal( module_accessor, @@ -365,7 +356,41 @@ pub unsafe fn save_states(module_accessor: &mut app::BattleObjectModuleAccessor) // If we're done moving, reset percent, handle charges, and apply buffs if save_state.state == NoAction { - set_damage(module_accessor, save_state.percent); + // Set damage of the save state + if !is_cpu { + match MENU.save_damage_player { + SaveDamage::SAVED => { + set_damage(module_accessor, save_state.percent); + } + SaveDamage::RANDOM => { + // Gen random value + let pct: f32 = get_random_float( + MENU.save_damage_limits_player.0 as f32, + MENU.save_damage_limits_player.1 as f32, + ); + set_damage(module_accessor, pct); + } + SaveDamage::DEFAULT => {} + _ => {} + } + } else { + match MENU.save_damage_cpu { + SaveDamage::SAVED => { + set_damage(module_accessor, save_state.percent); + } + SaveDamage::RANDOM => { + // Gen random value + let pct: f32 = get_random_float( + MENU.save_damage_limits_cpu.0 as f32, + MENU.save_damage_limits_cpu.1 as f32, + ); + set_damage(module_accessor, pct); + } + SaveDamage::DEFAULT => {} + _ => {} + } + } + // Set to held item if !is_cpu && !fighter_is_nana && MENU.character_item != CharacterItem::None { apply_item(MENU.character_item); diff --git a/training_mod_consts/src/lib.rs b/training_mod_consts/src/lib.rs index 13a7135..c6cb08c 100644 --- a/training_mod_consts/src/lib.rs +++ b/training_mod_consts/src/lib.rs @@ -1,1552 +1,1637 @@ -#![feature(const_option)] -#[macro_use] -extern crate bitflags; - -#[macro_use] -extern crate bitflags_serde_shim; - -#[macro_use] -extern crate num_derive; - -use core::f64::consts::PI; -use ramhorns::Content; -use serde::{Deserialize, Serialize}; -use serde_repr::{Deserialize_repr, Serialize_repr}; -#[cfg(feature = "smash")] -use smash::lib::lua_const::*; -use std::collections::HashMap; -use strum::IntoEnumIterator; -use strum_macros::EnumIter; - -pub trait ToggleTrait { - fn to_toggle_strs() -> Vec<&'static str>; - fn to_toggle_vals() -> Vec; -} - -// bitflag helper function macro -macro_rules! extra_bitflag_impls { - ($e:ty) => { - impl core::fmt::Display for $e { - fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { - core::fmt::Debug::fmt(self, f) - } - } - - impl $e { - pub fn to_vec(&self) -> Vec::<$e> { - let mut vec = Vec::<$e>::new(); - let mut field = <$e>::from_bits_truncate(self.bits); - while !field.is_empty() { - let flag = <$e>::from_bits(1u32 << field.bits.trailing_zeros()).unwrap(); - field -= flag; - vec.push(flag); - } - return vec; - } - - pub fn to_index(&self) -> u32 { - if self.bits == 0 { - 0 - } else { - self.bits.trailing_zeros() - } - } - - pub fn get_random(&self) -> $e { - let options = self.to_vec(); - match options.len() { - 0 => { - return <$e>::empty(); - } - 1 => { - return options[0]; - } - _ => { - return *random_option(&options); - } - } - } - } - 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() - } - - fn to_toggle_vals() -> Vec { - let all_options = <$e>::all().to_vec(); - all_options.iter().map(|i| i.bits() as usize).collect() - } - } - } -} - -pub fn get_random_int(_max: i32) -> i32 { - #[cfg(feature = "smash")] - unsafe { - smash::app::sv_math::rand(smash::hash40("fighter"), _max) - } - - #[cfg(not(feature = "smash"))] - 0 -} - -pub fn random_option(arg: &[T]) -> &T { - &arg[get_random_int(arg.len() as i32) as usize] -} - -// DI -/* - 0, 0.785398, 1.570796, 2.356194, -3.14159, -2.356194, -1.570796, -0.785398 - 0, pi/4, pi/2, 3pi/4, pi, 5pi/4, 3pi/2, 7pi/4 -*/ - -// DI / Left stick -bitflags! { - pub struct Direction : u32 { - const OUT = 0x1; - const UP_OUT = 0x2; - const UP = 0x4; - const UP_IN = 0x8; - const IN = 0x10; - const DOWN_IN = 0x20; - const DOWN = 0x40; - const DOWN_OUT = 0x80; - const NEUTRAL = 0x100; - const LEFT = 0x200; - const RIGHT = 0x400; - } -} - -impl Direction { - pub fn into_angle(self) -> Option { - let index = self.into_index(); - - if index == 0 { - None - } else { - Some((index as i32 - 1) as f64 * PI / 4.0) - } - } - fn into_index(self) -> i32 { - match self { - Direction::OUT => 1, - Direction::UP_OUT => 2, - Direction::UP => 3, - Direction::UP_IN => 4, - Direction::IN => 5, - Direction::DOWN_IN => 6, - Direction::DOWN => 7, - Direction::DOWN_OUT => 8, - Direction::NEUTRAL => 0, - Direction::LEFT => 5, - Direction::RIGHT => 1, - _ => 0, - } - } - - fn as_str(self) -> Option<&'static str> { - Some(match self { - Direction::OUT => "Away", - Direction::UP_OUT => "Up and Away", - Direction::UP => "Up", - Direction::UP_IN => "Up and In", - Direction::IN => "In", - Direction::DOWN_IN => "Down and In", - Direction::DOWN => "Down", - Direction::DOWN_OUT => "Down and Away", - Direction::NEUTRAL => "Neutral", - Direction::LEFT => "Left", - Direction::RIGHT => "Right", - _ => return None, - }) - } -} - -extra_bitflag_impls! {Direction} -impl_serde_for_bitflags!(Direction); - -// Ledge Option -bitflags! { - pub struct LedgeOption : u32 - { - const NEUTRAL = 0x1; - const ROLL = 0x2; - const JUMP = 0x4; - const ATTACK = 0x8; - const WAIT = 0x10; - } -} - -impl LedgeOption { - pub fn into_status(self) -> Option { - #[cfg(feature = "smash")] - { - Some(match self { - LedgeOption::NEUTRAL => *FIGHTER_STATUS_KIND_CLIFF_CLIMB, - LedgeOption::ROLL => *FIGHTER_STATUS_KIND_CLIFF_ESCAPE, - LedgeOption::JUMP => *FIGHTER_STATUS_KIND_CLIFF_JUMP1, - LedgeOption::ATTACK => *FIGHTER_STATUS_KIND_CLIFF_ATTACK, - LedgeOption::WAIT => *FIGHTER_STATUS_KIND_CLIFF_WAIT, - _ => return None, - }) - } - - #[cfg(not(feature = "smash"))] - None - } - - fn as_str(self) -> Option<&'static str> { - Some(match self { - LedgeOption::NEUTRAL => "Neutral Getup", - LedgeOption::ROLL => "Roll", - LedgeOption::JUMP => "Jump", - LedgeOption::ATTACK => "Getup Attack", - LedgeOption::WAIT => "Wait", - _ => return None, - }) - } -} - -extra_bitflag_impls! {LedgeOption} -impl_serde_for_bitflags!(LedgeOption); - -// Tech options -bitflags! { - pub struct TechFlags : u32 { - const NO_TECH = 0x1; - const ROLL_F = 0x2; - const ROLL_B = 0x4; - const IN_PLACE = 0x8; - } -} - -impl TechFlags { - fn as_str(self) -> Option<&'static str> { - Some(match self { - TechFlags::NO_TECH => "No Tech", - TechFlags::ROLL_F => "Roll Forwards", - TechFlags::ROLL_B => "Roll Backwards", - TechFlags::IN_PLACE => "Tech In Place", - _ => return None, - }) - } -} - -extra_bitflag_impls! {TechFlags} -impl_serde_for_bitflags!(TechFlags); - -// Missed Tech Options -bitflags! { - pub struct MissTechFlags : u32 { - const GETUP = 0x1; - const ATTACK = 0x2; - const ROLL_F = 0x4; - const ROLL_B = 0x8; - } -} - -impl MissTechFlags { - fn as_str(self) -> Option<&'static str> { - Some(match self { - MissTechFlags::GETUP => "Neutral Getup", - MissTechFlags::ATTACK => "Getup Attack", - MissTechFlags::ROLL_F => "Roll Forwards", - MissTechFlags::ROLL_B => "Roll Backwards", - _ => return None, - }) - } -} - -extra_bitflag_impls! {MissTechFlags} -impl_serde_for_bitflags!(MissTechFlags); - -/// Shield States -#[repr(i32)] -#[derive( - Debug, Clone, Copy, PartialEq, FromPrimitive, EnumIter, Serialize_repr, Deserialize_repr, -)] -pub enum Shield { - None = 0x0, - Infinite = 0x1, - Hold = 0x2, - Constant = 0x4, -} - -impl Shield { - pub fn as_str(self) -> Option<&'static str> { - Some(match self { - Shield::None => "None", - Shield::Infinite => "Infinite", - Shield::Hold => "Hold", - Shield::Constant => "Constant", - }) - } -} - -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 { - Shield::iter().map(|i| i as usize).collect() - } -} - -// Save State Mirroring -#[repr(i32)] -#[derive( - Debug, Clone, Copy, PartialEq, FromPrimitive, EnumIter, Serialize_repr, Deserialize_repr, -)] -pub enum SaveStateMirroring { - None = 0x0, - Alternate = 0x1, - Random = 0x2, -} - -impl SaveStateMirroring { - pub fn as_str(self) -> Option<&'static str> { - Some(match self { - SaveStateMirroring::None => "None", - SaveStateMirroring::Alternate => "Alternate", - SaveStateMirroring::Random => "Random", - }) - } -} - -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 { - SaveStateMirroring::iter().map(|i| i as usize).collect() - } -} - -#[repr(i32)] -#[derive(Debug, Clone, Copy, PartialEq, Serialize_repr, Deserialize_repr)] -pub enum OnOff { - Off = 0, - On = 1, -} - -impl OnOff { - pub fn from_val(val: u32) -> Option { - match val { - 1 => Some(OnOff::On), - 0 => Some(OnOff::Off), - _ => None, - } - } - - pub fn as_str(self) -> Option<&'static str> { - Some(match self { - OnOff::Off => "Off", - OnOff::On => "On", - }) - } -} - -impl ToggleTrait for OnOff { - fn to_toggle_strs() -> Vec<&'static str> { - vec!["Off", "On"] - } - fn to_toggle_vals() -> Vec { - vec![0, 1] - } -} - -bitflags! { - pub struct Action : u32 { - const AIR_DODGE = 0x1; - const JUMP = 0x2; - const SHIELD = 0x4; - const SPOT_DODGE = 0x8; - const ROLL_F = 0x10; - const ROLL_B = 0x20; - const NAIR = 0x40; - const FAIR = 0x80; - const BAIR = 0x100; - const UAIR = 0x200; - const DAIR = 0x400; - const NEUTRAL_B = 0x800; - const SIDE_B = 0x1000; - const UP_B = 0x2000; - const DOWN_B = 0x4000; - const F_SMASH = 0x8000; - const U_SMASH = 0x10000; - const D_SMASH = 0x20000; - const JAB = 0x40000; - const F_TILT = 0x80000; - const U_TILT = 0x0010_0000; - const D_TILT = 0x0020_0000; - const GRAB = 0x0040_0000; - // TODO: Make work - const DASH = 0x0080_0000; - const DASH_ATTACK = 0x0100_0000; - } -} - -impl Action { - pub fn into_attack_air_kind(self) -> Option { - #[cfg(feature = "smash")] - { - Some(match self { - Action::NAIR => *FIGHTER_COMMAND_ATTACK_AIR_KIND_N, - Action::FAIR => *FIGHTER_COMMAND_ATTACK_AIR_KIND_F, - Action::BAIR => *FIGHTER_COMMAND_ATTACK_AIR_KIND_B, - Action::DAIR => *FIGHTER_COMMAND_ATTACK_AIR_KIND_LW, - Action::UAIR => *FIGHTER_COMMAND_ATTACK_AIR_KIND_HI, - _ => return None, - }) - } - - #[cfg(not(feature = "smash"))] - None - } - - pub fn as_str(self) -> Option<&'static str> { - Some(match self { - Action::AIR_DODGE => "Airdodge", - Action::JUMP => "Jump", - Action::SHIELD => "Shield", - Action::SPOT_DODGE => "Spotdodge", - Action::ROLL_F => "Roll Forwards", - Action::ROLL_B => "Roll Backwards", - Action::NAIR => "Neutral Aerial", - Action::FAIR => "Forward Aerial", - Action::BAIR => "Backward Aerial", - Action::UAIR => "Up Aerial", - Action::DAIR => "Down Aerial", - Action::NEUTRAL_B => "Neutral Special", - Action::SIDE_B => "Side Special", - Action::UP_B => "Up Special", - Action::DOWN_B => "Down Special", - Action::F_SMASH => "Forward Smash", - Action::U_SMASH => "Up Smash", - Action::D_SMASH => "Down Smash", - Action::JAB => "Jab", - Action::F_TILT => "Forward Tilt", - Action::U_TILT => "Up Tilt", - Action::D_TILT => "Down Tilt", - Action::GRAB => "Grab", - Action::DASH => "Dash", - Action::DASH_ATTACK => "Dash Attack", - _ => return None, - }) - } -} - -extra_bitflag_impls! {Action} -impl_serde_for_bitflags!(Action); - -bitflags! { - pub struct AttackAngle : u32 { - const NEUTRAL = 0x1; - const UP = 0x2; - const DOWN = 0x4; - } -} - -impl AttackAngle { - pub fn as_str(self) -> Option<&'static str> { - Some(match self { - AttackAngle::NEUTRAL => "Neutral", - AttackAngle::UP => "Up", - AttackAngle::DOWN => "Down", - _ => return None, - }) - } -} - -extra_bitflag_impls! {AttackAngle} -impl_serde_for_bitflags!(AttackAngle); - -bitflags! { - pub struct Delay : u32 { - const D0 = 0x1; - const D1 = 0x2; - const D2 = 0x4; - const D3 = 0x8; - const D4 = 0x10; - const D5 = 0x20; - const D6 = 0x40; - const D7 = 0x80; - const D8 = 0x100; - const D9 = 0x200; - const D10 = 0x400; - const D11 = 0x800; - const D12 = 0x1000; - const D13 = 0x2000; - const D14 = 0x4000; - const D15 = 0x8000; - const D16 = 0x10000; - const D17 = 0x20000; - const D18 = 0x40000; - const D19 = 0x80000; - const D20 = 0x0010_0000; - const D21 = 0x0020_0000; - const D22 = 0x0040_0000; - const D23 = 0x0080_0000; - const D24 = 0x0100_0000; - const D25 = 0x0200_0000; - const D26 = 0x0400_0000; - const D27 = 0x0800_0000; - const D28 = 0x1000_0000; - const D29 = 0x2000_0000; - const D30 = 0x4000_0000; - } -} - -// Throw Option -bitflags! { - pub struct ThrowOption : u32 - { - const NONE = 0x1; - const FORWARD = 0x2; - const BACKWARD = 0x4; - const UP = 0x8; - const DOWN = 0x10; - } -} - -impl ThrowOption { - pub fn into_cmd(self) -> Option { - #[cfg(feature = "smash")] - { - Some(match self { - ThrowOption::NONE => 0, - ThrowOption::FORWARD => *FIGHTER_PAD_CMD_CAT2_FLAG_THROW_F, - ThrowOption::BACKWARD => *FIGHTER_PAD_CMD_CAT2_FLAG_THROW_B, - ThrowOption::UP => *FIGHTER_PAD_CMD_CAT2_FLAG_THROW_HI, - ThrowOption::DOWN => *FIGHTER_PAD_CMD_CAT2_FLAG_THROW_LW, - _ => return None, - }) - } - - #[cfg(not(feature = "smash"))] - None - } - - pub fn as_str(self) -> Option<&'static str> { - Some(match self { - ThrowOption::NONE => "None", - ThrowOption::FORWARD => "Forward Throw", - ThrowOption::BACKWARD => "Back Throw", - ThrowOption::UP => "Up Throw", - ThrowOption::DOWN => "Down Throw", - _ => return None, - }) - } -} - -extra_bitflag_impls! {ThrowOption} -impl_serde_for_bitflags!(ThrowOption); - -// Buff Option -bitflags! { - pub struct BuffOption : u32 - { - const ACCELERATLE = 0x1; - const OOMPH = 0x2; - const PSYCHE = 0x4; - const BOUNCE = 0x8; - const ARSENE = 0x10; - const BREATHING = 0x20; - const LIMIT = 0x40; - const KO = 0x80; - const WING = 0x100; - } -} - -impl BuffOption { - pub fn into_int(self) -> Option { - #[cfg(feature = "smash")] - { - Some(match self { - BuffOption::ACCELERATLE => *FIGHTER_BRAVE_SPECIAL_LW_COMMAND11_SPEED_UP, - BuffOption::OOMPH => *FIGHTER_BRAVE_SPECIAL_LW_COMMAND12_ATTACK_UP, - BuffOption::PSYCHE => *FIGHTER_BRAVE_SPECIAL_LW_COMMAND21_CHARGE, - BuffOption::BOUNCE => *FIGHTER_BRAVE_SPECIAL_LW_COMMAND13_REFLECT, - BuffOption::BREATHING => 1, - BuffOption::ARSENE => 1, - BuffOption::LIMIT => 1, - BuffOption::KO => 1, - BuffOption::WING => 1, - _ => return None, - }) - } - - #[cfg(not(feature = "smash"))] - None - } - - fn as_str(self) -> Option<&'static str> { - Some(match self { - BuffOption::ACCELERATLE => "Acceleratle", - BuffOption::OOMPH => "Oomph", - BuffOption::BOUNCE => "Bounce", - BuffOption::PSYCHE => "Psyche Up", - BuffOption::BREATHING => "Deep Breathing", - BuffOption::ARSENE => "Arsene", - BuffOption::LIMIT => "Limit Break", - BuffOption::KO => "KO Punch", - BuffOption::WING => "1-Winged Angel", - _ => return None, - }) - } -} - -extra_bitflag_impls! {BuffOption} -impl_serde_for_bitflags!(BuffOption); - -impl Delay { - pub fn as_str(self) -> Option<&'static str> { - Some(match self { - Delay::D0 => "0", - Delay::D1 => "1", - Delay::D2 => "2", - Delay::D3 => "3", - Delay::D4 => "4", - Delay::D5 => "5", - Delay::D6 => "6", - Delay::D7 => "7", - Delay::D8 => "8", - Delay::D9 => "9", - Delay::D10 => "10", - Delay::D11 => "11", - Delay::D12 => "12", - Delay::D13 => "13", - Delay::D14 => "14", - Delay::D15 => "15", - Delay::D16 => "16", - Delay::D17 => "17", - Delay::D18 => "18", - Delay::D19 => "19", - Delay::D20 => "20", - Delay::D21 => "21", - Delay::D22 => "22", - Delay::D23 => "23", - Delay::D24 => "24", - Delay::D25 => "25", - Delay::D26 => "26", - Delay::D27 => "27", - Delay::D28 => "28", - Delay::D29 => "29", - Delay::D30 => "30", - _ => return None, - }) - } - - pub fn into_delay(&self) -> u32 { - self.to_index() - } -} - -extra_bitflag_impls! {Delay} -impl_serde_for_bitflags!(Delay); - -bitflags! { - pub struct MedDelay : u32 { - const D0 = 0x1; - const D5 = 0x2; - const D10 = 0x4; - const D15 = 0x8; - const D20 = 0x10; - const D25 = 0x20; - const D30 = 0x40; - const D35 = 0x80; - const D40 = 0x100; - const D45 = 0x200; - const D50 = 0x400; - const D55 = 0x800; - const D60 = 0x1000; - const D65 = 0x2000; - const D70 = 0x4000; - const D75 = 0x8000; - const D80 = 0x10000; - const D85 = 0x20000; - const D90 = 0x40000; - const D95 = 0x80000; - const D100 = 0x0010_0000; - const D105 = 0x0020_0000; - const D110 = 0x0040_0000; - const D115 = 0x0080_0000; - const D120 = 0x0100_0000; - const D125 = 0x0200_0000; - const D130 = 0x0400_0000; - const D135 = 0x0800_0000; - const D140 = 0x1000_0000; - const D145 = 0x2000_0000; - const D150 = 0x4000_0000; - } -} - -impl MedDelay { - pub fn as_str(self) -> Option<&'static str> { - Some(match self { - MedDelay::D0 => "0", - MedDelay::D5 => "5", - MedDelay::D10 => "10", - MedDelay::D15 => "15", - MedDelay::D20 => "20", - MedDelay::D25 => "25", - MedDelay::D30 => "30", - MedDelay::D35 => "35", - MedDelay::D40 => "40", - MedDelay::D45 => "45", - MedDelay::D50 => "50", - MedDelay::D55 => "55", - MedDelay::D60 => "60", - MedDelay::D65 => "65", - MedDelay::D70 => "70", - MedDelay::D75 => "75", - MedDelay::D80 => "80", - MedDelay::D85 => "85", - MedDelay::D90 => "90", - MedDelay::D95 => "95", - MedDelay::D100 => "100", - MedDelay::D105 => "105", - MedDelay::D110 => "110", - MedDelay::D115 => "115", - MedDelay::D120 => "120", - MedDelay::D125 => "125", - MedDelay::D130 => "130", - MedDelay::D135 => "135", - MedDelay::D140 => "140", - MedDelay::D145 => "145", - MedDelay::D150 => "150", - _ => return None, - }) - } - - pub fn into_meddelay(&self) -> u32 { - self.to_index() * 5 - } -} - -extra_bitflag_impls! {MedDelay} -impl_serde_for_bitflags!(MedDelay); - -bitflags! { - pub struct LongDelay : u32 { - const D0 = 0x1; - const D10 = 0x2; - const D20 = 0x4; - const D30 = 0x8; - const D40 = 0x10; - const D50 = 0x20; - const D60 = 0x40; - const D70 = 0x80; - const D80 = 0x100; - const D90 = 0x200; - const D100 = 0x400; - const D110 = 0x800; - const D120 = 0x1000; - const D130 = 0x2000; - const D140 = 0x4000; - const D150 = 0x8000; - const D160 = 0x10000; - const D170 = 0x20000; - const D180 = 0x40000; - const D190 = 0x80000; - const D200 = 0x0010_0000; - const D210 = 0x0020_0000; - const D220 = 0x0040_0000; - const D230 = 0x0080_0000; - const D240 = 0x0100_0000; - const D250 = 0x0200_0000; - const D260 = 0x0400_0000; - const D270 = 0x0800_0000; - const D280 = 0x1000_0000; - const D290 = 0x2000_0000; - const D300 = 0x4000_0000; - } -} - -impl LongDelay { - pub fn as_str(self) -> Option<&'static str> { - Some(match self { - LongDelay::D0 => "0", - LongDelay::D10 => "10", - LongDelay::D20 => "20", - LongDelay::D30 => "30", - LongDelay::D40 => "40", - LongDelay::D50 => "50", - LongDelay::D60 => "60", - LongDelay::D70 => "70", - LongDelay::D80 => "80", - LongDelay::D90 => "90", - LongDelay::D100 => "100", - LongDelay::D110 => "110", - LongDelay::D120 => "120", - LongDelay::D130 => "130", - LongDelay::D140 => "140", - LongDelay::D150 => "150", - LongDelay::D160 => "160", - LongDelay::D170 => "170", - LongDelay::D180 => "180", - LongDelay::D190 => "190", - LongDelay::D200 => "200", - LongDelay::D210 => "210", - LongDelay::D220 => "220", - LongDelay::D230 => "230", - LongDelay::D240 => "240", - LongDelay::D250 => "250", - LongDelay::D260 => "260", - LongDelay::D270 => "270", - LongDelay::D280 => "280", - LongDelay::D290 => "290", - LongDelay::D300 => "300", - _ => return None, - }) - } - - pub fn into_longdelay(&self) -> u32 { - self.to_index() * 10 - } -} - -extra_bitflag_impls! {LongDelay} -impl_serde_for_bitflags!(LongDelay); - -bitflags! { - pub struct BoolFlag : u32 { - const TRUE = 0x1; - const FALSE = 0x2; - } -} - -extra_bitflag_impls! {BoolFlag} -impl_serde_for_bitflags!(BoolFlag); - -impl BoolFlag { - pub fn into_bool(self) -> bool { - matches!(self, BoolFlag::TRUE) - } - - pub fn as_str(self) -> Option<&'static str> { - Some(match self { - BoolFlag::TRUE => "True", - _ => "False", - }) - } -} - -#[repr(u32)] -#[derive( - Debug, Clone, Copy, PartialEq, Eq, FromPrimitive, EnumIter, Serialize_repr, Deserialize_repr, -)] -pub enum InputFrequency { - None = 0, - Normal = 1, - Medium = 2, - High = 4, -} - -impl InputFrequency { - pub fn into_u32(self) -> u32 { - match self { - InputFrequency::None => u32::MAX, - InputFrequency::Normal => 8, - InputFrequency::Medium => 6, - InputFrequency::High => 4, - } - } - - pub fn as_str(self) -> Option<&'static str> { - Some(match self { - InputFrequency::None => "None", - InputFrequency::Normal => "Normal", - InputFrequency::Medium => "Medium", - InputFrequency::High => "High", - }) - } -} - -impl ToggleTrait for InputFrequency { - fn to_toggle_strs() -> Vec<&'static str> { - InputFrequency::iter() - .map(|i| i.as_str().unwrap_or("")) - .collect() - } - - fn to_toggle_vals() -> Vec { - InputFrequency::iter().map(|i| i as usize).collect() - } -} - -/// Item Selections -#[repr(i32)] -#[derive( - Debug, Clone, Copy, PartialEq, FromPrimitive, EnumIter, Serialize_repr, Deserialize_repr, -)] -pub enum CharacterItem { - None = 0, - PlayerVariation1 = 0x1, - PlayerVariation2 = 0x2, - PlayerVariation3 = 0x4, - PlayerVariation4 = 0x8, - PlayerVariation5 = 0x10, - PlayerVariation6 = 0x20, - PlayerVariation7 = 0x40, - PlayerVariation8 = 0x80, - CpuVariation1 = 0x100, - CpuVariation2 = 0x200, - CpuVariation3 = 0x400, - CpuVariation4 = 0x800, - CpuVariation5 = 0x1000, - CpuVariation6 = 0x2000, - CpuVariation7 = 0x4000, - CpuVariation8 = 0x8000, -} - -impl CharacterItem { - pub fn as_idx(self) -> u32 { - log_2(self as i32 as u32) - } - - pub fn as_str(self) -> Option<&'static str> { - Some(match self { - CharacterItem::PlayerVariation1 => "Player 1st Var.", - CharacterItem::PlayerVariation2 => "Player 2nd Var.", - CharacterItem::PlayerVariation3 => "Player 3rd Var.", - CharacterItem::PlayerVariation4 => "Player 4th Var.", - CharacterItem::PlayerVariation5 => "Player 5th Var.", - CharacterItem::PlayerVariation6 => "Player 6th Var.", - CharacterItem::PlayerVariation7 => "Player 7th Var.", - CharacterItem::PlayerVariation8 => "Player 8th Var.", - CharacterItem::CpuVariation1 => "CPU 1st Var.", - CharacterItem::CpuVariation2 => "CPU 2nd Var.", - CharacterItem::CpuVariation3 => "CPU 3rd Var.", - CharacterItem::CpuVariation4 => "CPU 4th Var.", - CharacterItem::CpuVariation5 => "CPU 5th Var.", - CharacterItem::CpuVariation6 => "CPU 6th Var.", - CharacterItem::CpuVariation7 => "CPU 7th Var.", - CharacterItem::CpuVariation8 => "CPU 8th Var.", - _ => "None", - }) - } -} - -impl ToggleTrait for CharacterItem { - fn to_toggle_strs() -> Vec<&'static str> { - CharacterItem::iter() - .map(|i| i.as_str().unwrap_or("")) - .collect() - } - - fn to_toggle_vals() -> Vec { - CharacterItem::iter().map(|i| i as usize).collect() - } -} - -bitflags! { - pub struct MashTrigger : u32 { - const HIT = 0b0000_0000_0000_0000_0001; - const BLOCK = 0b0000_0000_0000_0000_0010; - const PARRY = 0b0000_0000_0000_0000_0100; - const TUMBLE = 0b0000_0000_0000_0000_1000; - const LANDING = 0b0000_0000_0000_0001_0000; - const TRUMP = 0b0000_0000_0000_0010_0000; - const FOOTSTOOL = 0b0000_0000_0000_0100_0000; - const CLATTER = 0b0000_0000_0000_1000_0000; - const LEDGE = 0b0000_0000_0001_0000_0000; - const TECH = 0b0000_0000_0010_0000_0000; - const MISTECH = 0b0000_0000_0100_0000_0000; - const GROUNDED = 0b0000_0000_1000_0000_0000; - const AIRBORNE = 0b0000_0001_0000_0000_0000; - const DISTANCE_CLOSE = 0b0000_0010_0000_0000_0000; - const DISTANCE_MID = 0b0000_0100_0000_0000_0000; - const DISTANCE_FAR = 0b0000_1000_0000_0000_0000; - const ALWAYS = 0b0001_0000_0000_0000_0000; - } -} - -impl MashTrigger { - pub fn as_str(self) -> Option<&'static str> { - Some(match self { - MashTrigger::HIT => "Hitstun", - MashTrigger::BLOCK => "Shieldstun", - MashTrigger::PARRY => "Parry", - MashTrigger::TUMBLE => "Tumble", - MashTrigger::LANDING => "Landing", - MashTrigger::TRUMP => "Ledge Trump", - MashTrigger::FOOTSTOOL => "Footstool", - MashTrigger::CLATTER => "Clatter", - MashTrigger::LEDGE => "Ledge Option", - MashTrigger::TECH => "Tech Option", - MashTrigger::MISTECH => "Mistech Option", - MashTrigger::GROUNDED => "Grounded", - MashTrigger::AIRBORNE => "Airborne", - MashTrigger::DISTANCE_CLOSE => "Distance: Close", - MashTrigger::DISTANCE_MID => "Distance: Mid", - MashTrigger::DISTANCE_FAR => "Distance: Far", - MashTrigger::ALWAYS => "Always", - _ => return None, - }) - } - - const fn default() -> MashTrigger { - // Hit, block, clatter - MashTrigger::HIT.union(MashTrigger::BLOCK).union(MashTrigger::CLATTER) - } -} - -extra_bitflag_impls! {MashTrigger} -impl_serde_for_bitflags!(MashTrigger); - -#[repr(C)] -#[derive(Clone, Copy, Serialize, Deserialize, Debug)] -pub struct TrainingModpackMenu { - // Mash Tab - pub aerial_delay: Delay, - pub air_dodge_dir: Direction, - pub attack_angle: AttackAngle, - pub buff_state: BuffOption, - pub character_item: CharacterItem, - pub clatter_strength: InputFrequency, - pub crouch: OnOff, - pub di_state: Direction, - pub falling_aerials: BoolFlag, - pub fast_fall_delay: Delay, - pub fast_fall: BoolFlag, - pub follow_up: Action, - pub frame_advantage: OnOff, - pub full_hop: BoolFlag, - pub hitbox_vis: OnOff, - pub input_delay: Delay, - pub ledge_delay: LongDelay, - pub ledge_state: LedgeOption, - pub mash_state: Action, - pub mash_triggers: MashTrigger, - pub miss_tech_state: MissTechFlags, - pub oos_offset: Delay, - pub pummel_delay: MedDelay, - pub quick_menu: OnOff, - pub reaction_time: Delay, - pub save_damage: OnOff, - pub save_state_autoload: OnOff, - pub save_state_enable: OnOff, - pub save_state_mirroring: SaveStateMirroring, - pub sdi_state: Direction, - pub sdi_strength: InputFrequency, - pub shield_state: Shield, - pub shield_tilt: Direction, - pub stage_hazards: OnOff, - pub tech_state: TechFlags, - pub throw_delay: MedDelay, - pub throw_state: ThrowOption, -} - -macro_rules! set_by_str { - ($obj:ident, $s:ident, $($field:ident = $rhs:expr,)*) => { - $( - if $s == stringify!($field) { - $obj.$field = $rhs.unwrap(); - } - )* - } -} - -const fn num_bits() -> usize { - std::mem::size_of::() * 8 -} - -fn log_2(x: u32) -> u32 { - if x == 0 { - 0 - } else { - num_bits::() as u32 - x.leading_zeros() - 1 - } -} - -impl TrainingModpackMenu { - pub fn set(&mut self, s: &str, val: u32) { - set_by_str!( - self, - s, - aerial_delay = Delay::from_bits(val), - air_dodge_dir = Direction::from_bits(val), - attack_angle = AttackAngle::from_bits(val), - clatter_strength = num::FromPrimitive::from_u32(val), - crouch = OnOff::from_val(val), - di_state = Direction::from_bits(val), - falling_aerials = BoolFlag::from_bits(val), - fast_fall_delay = Delay::from_bits(val), - fast_fall = BoolFlag::from_bits(val), - follow_up = Action::from_bits(val), - full_hop = BoolFlag::from_bits(val), - hitbox_vis = OnOff::from_val(val), - input_delay = Delay::from_bits(val), - ledge_delay = LongDelay::from_bits(val), - ledge_state = LedgeOption::from_bits(val), - mash_state = Action::from_bits(val), - mash_triggers = MashTrigger::from_bits(val), - miss_tech_state = MissTechFlags::from_bits(val), - oos_offset = Delay::from_bits(val), - reaction_time = Delay::from_bits(val), - sdi_state = Direction::from_bits(val), - sdi_strength = num::FromPrimitive::from_u32(val), - shield_state = num::FromPrimitive::from_u32(val), - shield_tilt = Direction::from_bits(val), - stage_hazards = OnOff::from_val(val), - tech_state = TechFlags::from_bits(val), - save_damage = OnOff::from_val(val), - frame_advantage = OnOff::from_val(val), - save_state_mirroring = num::FromPrimitive::from_u32(val), - save_state_enable = OnOff::from_val(val), - save_state_autoload = OnOff::from_val(val), - throw_state = ThrowOption::from_bits(val), - throw_delay = MedDelay::from_bits(val), - pummel_delay = MedDelay::from_bits(val), - buff_state = BuffOption::from_bits(val), - character_item = num::FromPrimitive::from_u32(val), - quick_menu = OnOff::from_val(val), - ); - } -} - -#[repr(C)] -#[derive(Debug, Serialize, Deserialize)] -pub struct MenuJsonStruct { - pub menu: TrainingModpackMenu, - pub defaults_menu: TrainingModpackMenu, - // pub last_focused_submenu: &str -} - -// Fighter Ids -#[repr(i32)] -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum FighterId { - Player = 0, - CPU = 1, -} - -#[derive(Clone)] -pub enum SubMenuType { - TOGGLE, - SLIDER, -} - -impl SubMenuType { - pub fn from_str(s: &str) -> SubMenuType { - match s { - "toggle" => SubMenuType::TOGGLE, - "slider" => SubMenuType::SLIDER, - _ => panic!("Unexpected SubMenuType!"), - } - } -} - -pub static DEFAULTS_MENU: TrainingModpackMenu = TrainingModpackMenu { - aerial_delay: Delay::empty(), - air_dodge_dir: Direction::empty(), - attack_angle: AttackAngle::empty(), - buff_state: BuffOption::empty(), - character_item: CharacterItem::None, - clatter_strength: InputFrequency::None, - crouch: OnOff::Off, - di_state: Direction::empty(), - falling_aerials: BoolFlag::empty(), - fast_fall_delay: Delay::empty(), - fast_fall: BoolFlag::empty(), - follow_up: Action::empty(), - frame_advantage: OnOff::Off, - full_hop: BoolFlag::empty(), - hitbox_vis: OnOff::On, - input_delay: Delay::D0, - ledge_delay: LongDelay::empty(), - ledge_state: LedgeOption::all(), - mash_state: Action::empty(), - mash_triggers: MashTrigger::default(), - miss_tech_state: MissTechFlags::all(), - oos_offset: Delay::empty(), - pummel_delay: MedDelay::empty(), - quick_menu: OnOff::Off, - reaction_time: Delay::empty(), - save_damage: OnOff::On, - save_state_autoload: OnOff::Off, - save_state_enable: OnOff::On, - save_state_mirroring: SaveStateMirroring::None, - sdi_state: Direction::empty(), - sdi_strength: InputFrequency::None, - shield_state: Shield::None, - shield_tilt: Direction::empty(), - stage_hazards: OnOff::Off, - tech_state: TechFlags::all(), - throw_delay: MedDelay::empty(), - throw_state: ThrowOption::NONE, -}; - -pub static mut MENU: TrainingModpackMenu = DEFAULTS_MENU; - -#[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 toggle_value: usize, - pub toggle_title: &'a str, - pub checked: bool, -} - -#[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>, - pub _type: &'a str, -} - -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, - }); - } - pub fn new_with_toggles( - 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 - } -} - -#[derive(Content)] -pub struct Tab<'a> { - pub tab_id: &'a str, - pub tab_title: &'a str, - pub tab_submenus: Vec>, -} - -impl<'a> Tab<'a> { - pub fn add_submenu_with_toggles( - &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::( - submenu_title, - submenu_id, - help_text, - is_single_option, - )); - } -} - -#[derive(Content)] -pub struct UiMenu<'a> { - pub tabs: Vec>, -} - -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::( - "Mash Toggles", - "mash_state", - "Mash Toggles: Actions to be performed as soon as possible", - false, - ); - mash_tab.add_submenu_with_toggles::( - "Followup Toggles", - "follow_up", - "Followup Toggles: Actions to be performed after the Mash option", - false, - ); - mash_tab.add_submenu_with_toggles::( - "Mash Triggers", - "mash_triggers", - "Mash triggers: When the Mash Option will be performed", - false, - ); - mash_tab.add_submenu_with_toggles::( - "Attack Angle", - "attack_angle", - "Attack Angle: For attacks that can be angled, such as some forward tilts", - false, - ); - mash_tab.add_submenu_with_toggles::( - "Throw Options", - "throw_state", - "Throw Options: Throw to be performed when a grab is landed", - false, - ); - mash_tab.add_submenu_with_toggles::( - "Throw Delay", - "throw_delay", - "Throw Delay: How many frames to delay the throw option", - false, - ); - mash_tab.add_submenu_with_toggles::( - "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::( - "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::( - "Full Hop", - "full_hop", - "Full Hop: Should the CPU perform a full hop or a short hop", - false, - ); - mash_tab.add_submenu_with_toggles::( - "Aerial Delay", - "aerial_delay", - "Aerial Delay: How long to delay a Mash aerial attack", - false, - ); - mash_tab.add_submenu_with_toggles::( - "Fast Fall", - "fast_fall", - "Fast Fall: Should the CPU fastfall during a jump", - false, - ); - mash_tab.add_submenu_with_toggles::( - "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::( - "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::( - "Reaction Time", - "reaction_time", - "Reaction Time: How many frames to delay before performing a mash option", - false, - ); - 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::( - "Airdodge Direction", - "air_dodge_dir", - "Airdodge Direction: Direction to angle airdodges", - false, - ); - defensive_tab.add_submenu_with_toggles::( - "DI Direction", - "di_state", - "DI Direction: Direction to angle the directional influence during hitlag", - false, - ); - defensive_tab.add_submenu_with_toggles::( - "SDI Direction", - "sdi_state", - "SDI Direction: Direction to angle the smash directional influence during hitlag", - false, - ); - defensive_tab.add_submenu_with_toggles::( - "SDI Strength", - "sdi_strength", - "SDI Strength: Relative strength of the smash directional influence inputs", - true, - ); - defensive_tab.add_submenu_with_toggles::( - "Clatter Strength", - "clatter_strength", - "Clatter Strength: Relative strength of the mashing out of grabs, buries, etc.", - true, - ); - defensive_tab.add_submenu_with_toggles::( - "Ledge Options", - "ledge_state", - "Ledge Options: Actions to be taken when on the ledge", - false, - ); - defensive_tab.add_submenu_with_toggles::( - "Ledge Delay", - "ledge_delay", - "Ledge Delay: How many frames to delay the ledge option", - false, - ); - defensive_tab.add_submenu_with_toggles::( - "Tech Options", - "tech_state", - "Tech Options: Actions to take when slammed into a hard surface", - false, - ); - defensive_tab.add_submenu_with_toggles::( - "Mistech Options", - "miss_tech_state", - "Mistech Options: Actions to take after missing a tech", - false, - ); - defensive_tab.add_submenu_with_toggles::( - "Shield Toggles", - "shield_state", - "Shield Toggles: CPU Shield Behavior", - true, - ); - defensive_tab.add_submenu_with_toggles::( - "Shield Tilt", - "shield_tilt", - "Shield Tilt: Direction to tilt the shield", - false, // TODO: Should this be true? - ); - defensive_tab.add_submenu_with_toggles::( - "Buff Options", - "buff_state", - "Buff Options: Buff(s) to be applied to respective character when loading save states", - false, - ); - defensive_tab.add_submenu_with_toggles::( - "Character Item", - "character_item", - "Character Item: CPU/Player item to hold when loading a save state", - true, - ); - defensive_tab.add_submenu_with_toggles::( - "Crouch", - "crouch", - "Crouch: Should the CPU crouch when on the ground", - true, - ); - 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::( - "Mirroring", - "save_state_mirroring", - "Mirroring: Flips save states in the left-right direction across the stage center", - true, - ); - misc_tab.add_submenu_with_toggles::( - "Save Damage", - "save_damage", - "Save Damage: Should save states retain player/CPU damage", - true, - ); - misc_tab.add_submenu_with_toggles::( - "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::( - "Save States Autoload", - "save_state_autoload", - "Save States Autoload: Load save state when any fighter dies", - true, - ); - misc_tab.add_submenu_with_toggles::( - "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::( - "Hitbox Visualization", - "hitbox_vis", - "Hitbox Visualization: Should hitboxes be displayed, hiding other visual effects", - true, - ); - misc_tab.add_submenu_with_toggles::( - "Input Delay", - "input_delay", - "Input Delay: Frames to delay player inputs by", - true, - ); - misc_tab.add_submenu_with_toggles::( - "Stage Hazards", - "stage_hazards", - "Stage Hazards: Should stage hazards be present", - true, - ); - misc_tab.add_submenu_with_toggles::( - "Quick Menu", - "quick_menu", - "Quick Menu: Should use quick or web menu", - true, - ); - overall_menu.tabs.push(misc_tab); - - let non_ui_menu = serde_json::to_string(&MENU) - .unwrap() - .replace("\"", "") - .replace("{", "") - .replace("}", ""); - let toggle_values_all = non_ui_menu.split(',').collect::>(); - let mut sub_menu_id_to_vals: HashMap<&str, u32> = HashMap::new(); - for toggle_values in toggle_values_all { - let toggle_value_split = toggle_values.split(':').collect::>(); - let sub_menu_id = toggle_value_split[0]; - if sub_menu_id.is_empty() { - continue; - } - - let full_bits: u32 = toggle_value_split[1].parse().unwrap_or(0); - sub_menu_id_to_vals.insert(&sub_menu_id, full_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] & (toggle.toggle_value as u32) != 0) - { - toggle.checked = true - } - }) - }) - }); - - overall_menu -} +#[macro_use] +extern crate bitflags; + +#[macro_use] +extern crate bitflags_serde_shim; + +#[macro_use] +extern crate num_derive; + +use core::f64::consts::PI; +use ramhorns::Content; +use serde::{Deserialize, Serialize}; +use serde_repr::{Deserialize_repr, Serialize_repr}; +#[cfg(feature = "smash")] +use smash::lib::lua_const::*; +use strum::IntoEnumIterator; +use strum_macros::EnumIter; + +pub trait ToggleTrait { + fn to_toggle_strs() -> Vec<&'static str>; + fn to_toggle_vals() -> Vec; +} + +pub trait SliderTrait { + fn get_limits() -> (u32, u32); +} + +// bitflag helper function macro +macro_rules! extra_bitflag_impls { + ($e:ty) => { + impl core::fmt::Display for $e { + fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { + core::fmt::Debug::fmt(self, f) + } + } + + impl $e { + pub fn to_vec(&self) -> Vec::<$e> { + let mut vec = Vec::<$e>::new(); + let mut field = <$e>::from_bits_truncate(self.bits); + while !field.is_empty() { + let flag = <$e>::from_bits(1u32 << field.bits.trailing_zeros()).unwrap(); + field -= flag; + vec.push(flag); + } + return vec; + } + + pub fn to_index(&self) -> u32 { + if self.bits == 0 { + 0 + } else { + self.bits.trailing_zeros() + } + } + + pub fn get_random(&self) -> $e { + let options = self.to_vec(); + match options.len() { + 0 => { + return <$e>::empty(); + } + 1 => { + return options[0]; + } + _ => { + return *random_option(&options); + } + } + } + } + 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() + } + + fn to_toggle_vals() -> Vec { + let all_options = <$e>::all().to_vec(); + all_options.iter().map(|i| i.bits() as u32).collect() + } + } + } +} + +pub fn get_random_int(_max: i32) -> i32 { + #[cfg(feature = "smash")] + unsafe { + smash::app::sv_math::rand(smash::hash40("fighter"), _max) + } + + #[cfg(not(feature = "smash"))] + 0 +} + +/// Generate a random float between _min and _max. +/// Note that (_min <= _max) is not enforced. +pub fn get_random_float(_min: f32, _max: f32) -> f32 { + #[cfg(feature = "smash")] + unsafe { + _min + smash::app::sv_math::randf(smash::hash40("fighter"), _max - _min) + } + + #[cfg(not(feature = "smash"))] + _min +} + +pub fn random_option(arg: &[T]) -> &T { + &arg[get_random_int(arg.len() as i32) as usize] +} + +// DI +/* + 0, 0.785398, 1.570796, 2.356194, -3.14159, -2.356194, -1.570796, -0.785398 + 0, pi/4, pi/2, 3pi/4, pi, 5pi/4, 3pi/2, 7pi/4 +*/ + +// DI / Left stick +bitflags! { + pub struct Direction : u32 { + const OUT = 0x1; + const UP_OUT = 0x2; + const UP = 0x4; + const UP_IN = 0x8; + const IN = 0x10; + const DOWN_IN = 0x20; + const DOWN = 0x40; + const DOWN_OUT = 0x80; + const NEUTRAL = 0x100; + const LEFT = 0x200; + const RIGHT = 0x400; + } +} + +impl Direction { + pub fn into_angle(self) -> Option { + let index = self.into_index(); + + if index == 0 { + None + } else { + Some((index as i32 - 1) as f64 * PI / 4.0) + } + } + fn into_index(self) -> i32 { + match self { + Direction::OUT => 1, + Direction::UP_OUT => 2, + Direction::UP => 3, + Direction::UP_IN => 4, + Direction::IN => 5, + Direction::DOWN_IN => 6, + Direction::DOWN => 7, + Direction::DOWN_OUT => 8, + Direction::NEUTRAL => 0, + Direction::LEFT => 5, + Direction::RIGHT => 1, + _ => 0, + } + } + + fn as_str(self) -> Option<&'static str> { + Some(match self { + Direction::OUT => "Away", + Direction::UP_OUT => "Up and Away", + Direction::UP => "Up", + Direction::UP_IN => "Up and In", + Direction::IN => "In", + Direction::DOWN_IN => "Down and In", + Direction::DOWN => "Down", + Direction::DOWN_OUT => "Down and Away", + Direction::NEUTRAL => "Neutral", + Direction::LEFT => "Left", + Direction::RIGHT => "Right", + _ => return None, + }) + } +} + +extra_bitflag_impls! {Direction} +impl_serde_for_bitflags!(Direction); + +// Ledge Option +bitflags! { + pub struct LedgeOption : u32 + { + const NEUTRAL = 0x1; + const ROLL = 0x2; + const JUMP = 0x4; + const ATTACK = 0x8; + const WAIT = 0x10; + } +} + +impl LedgeOption { + pub fn into_status(self) -> Option { + #[cfg(feature = "smash")] + { + Some(match self { + LedgeOption::NEUTRAL => *FIGHTER_STATUS_KIND_CLIFF_CLIMB, + LedgeOption::ROLL => *FIGHTER_STATUS_KIND_CLIFF_ESCAPE, + LedgeOption::JUMP => *FIGHTER_STATUS_KIND_CLIFF_JUMP1, + LedgeOption::ATTACK => *FIGHTER_STATUS_KIND_CLIFF_ATTACK, + LedgeOption::WAIT => *FIGHTER_STATUS_KIND_CLIFF_WAIT, + _ => return None, + }) + } + + #[cfg(not(feature = "smash"))] + None + } + + fn as_str(self) -> Option<&'static str> { + Some(match self { + LedgeOption::NEUTRAL => "Neutral Getup", + LedgeOption::ROLL => "Roll", + LedgeOption::JUMP => "Jump", + LedgeOption::ATTACK => "Getup Attack", + LedgeOption::WAIT => "Wait", + _ => return None, + }) + } +} + +extra_bitflag_impls! {LedgeOption} +impl_serde_for_bitflags!(LedgeOption); + +// Tech options +bitflags! { + pub struct TechFlags : u32 { + const NO_TECH = 0x1; + const ROLL_F = 0x2; + const ROLL_B = 0x4; + const IN_PLACE = 0x8; + } +} + +impl TechFlags { + fn as_str(self) -> Option<&'static str> { + Some(match self { + TechFlags::NO_TECH => "No Tech", + TechFlags::ROLL_F => "Roll Forwards", + TechFlags::ROLL_B => "Roll Backwards", + TechFlags::IN_PLACE => "Tech In Place", + _ => return None, + }) + } +} + +extra_bitflag_impls! {TechFlags} +impl_serde_for_bitflags!(TechFlags); + +// Missed Tech Options +bitflags! { + pub struct MissTechFlags : u32 { + const GETUP = 0x1; + const ATTACK = 0x2; + const ROLL_F = 0x4; + const ROLL_B = 0x8; + } +} + +impl MissTechFlags { + fn as_str(self) -> Option<&'static str> { + Some(match self { + MissTechFlags::GETUP => "Neutral Getup", + MissTechFlags::ATTACK => "Getup Attack", + MissTechFlags::ROLL_F => "Roll Forwards", + MissTechFlags::ROLL_B => "Roll Backwards", + _ => return None, + }) + } +} + +extra_bitflag_impls! {MissTechFlags} +impl_serde_for_bitflags!(MissTechFlags); + +/// Shield States +#[repr(i32)] +#[derive( + Debug, Clone, Copy, PartialEq, FromPrimitive, EnumIter, Serialize_repr, Deserialize_repr, +)] +pub enum Shield { + None = 0x0, + Infinite = 0x1, + Hold = 0x2, + Constant = 0x4, +} + +impl Shield { + pub fn as_str(self) -> Option<&'static str> { + Some(match self { + Shield::None => "None", + Shield::Infinite => "Infinite", + Shield::Hold => "Hold", + Shield::Constant => "Constant", + }) + } +} + +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 { + Shield::iter().map(|i| i as u32).collect() + } +} + +// Save State Mirroring +#[repr(i32)] +#[derive( + Debug, Clone, Copy, PartialEq, FromPrimitive, EnumIter, Serialize_repr, Deserialize_repr, +)] +pub enum SaveStateMirroring { + None = 0x0, + Alternate = 0x1, + Random = 0x2, +} + +impl SaveStateMirroring { + pub fn as_str(self) -> Option<&'static str> { + Some(match self { + SaveStateMirroring::None => "None", + SaveStateMirroring::Alternate => "Alternate", + SaveStateMirroring::Random => "Random", + }) + } +} + +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 { + SaveStateMirroring::iter().map(|i| i as u32).collect() + } +} + +#[repr(i32)] +#[derive(Debug, Clone, Copy, PartialEq, Serialize_repr, Deserialize_repr)] +pub enum OnOff { + Off = 0, + On = 1, +} + +impl OnOff { + pub fn from_val(val: u32) -> Option { + match val { + 1 => Some(OnOff::On), + 0 => Some(OnOff::Off), + _ => None, + } + } + + pub fn as_str(self) -> Option<&'static str> { + Some(match self { + OnOff::Off => "Off", + OnOff::On => "On", + }) + } +} + +impl ToggleTrait for OnOff { + fn to_toggle_strs() -> Vec<&'static str> { + vec!["Off", "On"] + } + fn to_toggle_vals() -> Vec { + vec![0, 1] + } +} + +bitflags! { + pub struct Action : u32 { + const AIR_DODGE = 0x1; + const JUMP = 0x2; + const SHIELD = 0x4; + const SPOT_DODGE = 0x8; + const ROLL_F = 0x10; + const ROLL_B = 0x20; + const NAIR = 0x40; + const FAIR = 0x80; + const BAIR = 0x100; + const UAIR = 0x200; + const DAIR = 0x400; + const NEUTRAL_B = 0x800; + const SIDE_B = 0x1000; + const UP_B = 0x2000; + const DOWN_B = 0x4000; + const F_SMASH = 0x8000; + const U_SMASH = 0x10000; + const D_SMASH = 0x20000; + const JAB = 0x40000; + const F_TILT = 0x80000; + const U_TILT = 0x0010_0000; + const D_TILT = 0x0020_0000; + const GRAB = 0x0040_0000; + // TODO: Make work + const DASH = 0x0080_0000; + const DASH_ATTACK = 0x0100_0000; + } +} + +impl Action { + pub fn into_attack_air_kind(self) -> Option { + #[cfg(feature = "smash")] + { + Some(match self { + Action::NAIR => *FIGHTER_COMMAND_ATTACK_AIR_KIND_N, + Action::FAIR => *FIGHTER_COMMAND_ATTACK_AIR_KIND_F, + Action::BAIR => *FIGHTER_COMMAND_ATTACK_AIR_KIND_B, + Action::DAIR => *FIGHTER_COMMAND_ATTACK_AIR_KIND_LW, + Action::UAIR => *FIGHTER_COMMAND_ATTACK_AIR_KIND_HI, + _ => return None, + }) + } + + #[cfg(not(feature = "smash"))] + None + } + + pub fn as_str(self) -> Option<&'static str> { + Some(match self { + Action::AIR_DODGE => "Airdodge", + Action::JUMP => "Jump", + Action::SHIELD => "Shield", + Action::SPOT_DODGE => "Spotdodge", + Action::ROLL_F => "Roll Forwards", + Action::ROLL_B => "Roll Backwards", + Action::NAIR => "Neutral Aerial", + Action::FAIR => "Forward Aerial", + Action::BAIR => "Backward Aerial", + Action::UAIR => "Up Aerial", + Action::DAIR => "Down Aerial", + Action::NEUTRAL_B => "Neutral Special", + Action::SIDE_B => "Side Special", + Action::UP_B => "Up Special", + Action::DOWN_B => "Down Special", + Action::F_SMASH => "Forward Smash", + Action::U_SMASH => "Up Smash", + Action::D_SMASH => "Down Smash", + Action::JAB => "Jab", + Action::F_TILT => "Forward Tilt", + Action::U_TILT => "Up Tilt", + Action::D_TILT => "Down Tilt", + Action::GRAB => "Grab", + Action::DASH => "Dash", + Action::DASH_ATTACK => "Dash Attack", + _ => return None, + }) + } +} + +extra_bitflag_impls! {Action} +impl_serde_for_bitflags!(Action); + +bitflags! { + pub struct AttackAngle : u32 { + const NEUTRAL = 0x1; + const UP = 0x2; + const DOWN = 0x4; + } +} + +impl AttackAngle { + pub fn as_str(self) -> Option<&'static str> { + Some(match self { + AttackAngle::NEUTRAL => "Neutral", + AttackAngle::UP => "Up", + AttackAngle::DOWN => "Down", + _ => return None, + }) + } +} + +extra_bitflag_impls! {AttackAngle} +impl_serde_for_bitflags!(AttackAngle); + +bitflags! { + pub struct Delay : u32 { + const D0 = 0x1; + const D1 = 0x2; + const D2 = 0x4; + const D3 = 0x8; + const D4 = 0x10; + const D5 = 0x20; + const D6 = 0x40; + const D7 = 0x80; + const D8 = 0x100; + const D9 = 0x200; + const D10 = 0x400; + const D11 = 0x800; + const D12 = 0x1000; + const D13 = 0x2000; + const D14 = 0x4000; + const D15 = 0x8000; + const D16 = 0x10000; + const D17 = 0x20000; + const D18 = 0x40000; + const D19 = 0x80000; + const D20 = 0x0010_0000; + const D21 = 0x0020_0000; + const D22 = 0x0040_0000; + const D23 = 0x0080_0000; + const D24 = 0x0100_0000; + const D25 = 0x0200_0000; + const D26 = 0x0400_0000; + const D27 = 0x0800_0000; + const D28 = 0x1000_0000; + const D29 = 0x2000_0000; + const D30 = 0x4000_0000; + } +} + +// Throw Option +bitflags! { + pub struct ThrowOption : u32 + { + const NONE = 0x1; + const FORWARD = 0x2; + const BACKWARD = 0x4; + const UP = 0x8; + const DOWN = 0x10; + } +} + +impl ThrowOption { + pub fn into_cmd(self) -> Option { + #[cfg(feature = "smash")] + { + Some(match self { + ThrowOption::NONE => 0, + ThrowOption::FORWARD => *FIGHTER_PAD_CMD_CAT2_FLAG_THROW_F, + ThrowOption::BACKWARD => *FIGHTER_PAD_CMD_CAT2_FLAG_THROW_B, + ThrowOption::UP => *FIGHTER_PAD_CMD_CAT2_FLAG_THROW_HI, + ThrowOption::DOWN => *FIGHTER_PAD_CMD_CAT2_FLAG_THROW_LW, + _ => return None, + }) + } + + #[cfg(not(feature = "smash"))] + None + } + + pub fn as_str(self) -> Option<&'static str> { + Some(match self { + ThrowOption::NONE => "None", + ThrowOption::FORWARD => "Forward Throw", + ThrowOption::BACKWARD => "Back Throw", + ThrowOption::UP => "Up Throw", + ThrowOption::DOWN => "Down Throw", + _ => return None, + }) + } +} + +extra_bitflag_impls! {ThrowOption} +impl_serde_for_bitflags!(ThrowOption); + +// Buff Option +bitflags! { + pub struct BuffOption : u32 + { + const ACCELERATLE = 0x1; + const OOMPH = 0x2; + const PSYCHE = 0x4; + const BOUNCE = 0x8; + const ARSENE = 0x10; + const BREATHING = 0x20; + const LIMIT = 0x40; + const KO = 0x80; + const WING = 0x100; + } +} + +impl BuffOption { + pub fn into_int(self) -> Option { + #[cfg(feature = "smash")] + { + Some(match self { + BuffOption::ACCELERATLE => *FIGHTER_BRAVE_SPECIAL_LW_COMMAND11_SPEED_UP, + BuffOption::OOMPH => *FIGHTER_BRAVE_SPECIAL_LW_COMMAND12_ATTACK_UP, + BuffOption::PSYCHE => *FIGHTER_BRAVE_SPECIAL_LW_COMMAND21_CHARGE, + BuffOption::BOUNCE => *FIGHTER_BRAVE_SPECIAL_LW_COMMAND13_REFLECT, + BuffOption::BREATHING => 1, + BuffOption::ARSENE => 1, + BuffOption::LIMIT => 1, + BuffOption::KO => 1, + BuffOption::WING => 1, + _ => return None, + }) + } + + #[cfg(not(feature = "smash"))] + None + } + + fn as_str(self) -> Option<&'static str> { + Some(match self { + BuffOption::ACCELERATLE => "Acceleratle", + BuffOption::OOMPH => "Oomph", + BuffOption::BOUNCE => "Bounce", + BuffOption::PSYCHE => "Psyche Up", + BuffOption::BREATHING => "Deep Breathing", + BuffOption::ARSENE => "Arsene", + BuffOption::LIMIT => "Limit Break", + BuffOption::KO => "KO Punch", + BuffOption::WING => "1-Winged Angel", + _ => return None, + }) + } +} + +extra_bitflag_impls! {BuffOption} +impl_serde_for_bitflags!(BuffOption); + +impl Delay { + pub fn as_str(self) -> Option<&'static str> { + Some(match self { + Delay::D0 => "0", + Delay::D1 => "1", + Delay::D2 => "2", + Delay::D3 => "3", + Delay::D4 => "4", + Delay::D5 => "5", + Delay::D6 => "6", + Delay::D7 => "7", + Delay::D8 => "8", + Delay::D9 => "9", + Delay::D10 => "10", + Delay::D11 => "11", + Delay::D12 => "12", + Delay::D13 => "13", + Delay::D14 => "14", + Delay::D15 => "15", + Delay::D16 => "16", + Delay::D17 => "17", + Delay::D18 => "18", + Delay::D19 => "19", + Delay::D20 => "20", + Delay::D21 => "21", + Delay::D22 => "22", + Delay::D23 => "23", + Delay::D24 => "24", + Delay::D25 => "25", + Delay::D26 => "26", + Delay::D27 => "27", + Delay::D28 => "28", + Delay::D29 => "29", + Delay::D30 => "30", + _ => return None, + }) + } + + pub fn into_delay(&self) -> u32 { + self.to_index() + } +} + +extra_bitflag_impls! {Delay} +impl_serde_for_bitflags!(Delay); + +bitflags! { + pub struct MedDelay : u32 { + const D0 = 0x1; + const D5 = 0x2; + const D10 = 0x4; + const D15 = 0x8; + const D20 = 0x10; + const D25 = 0x20; + const D30 = 0x40; + const D35 = 0x80; + const D40 = 0x100; + const D45 = 0x200; + const D50 = 0x400; + const D55 = 0x800; + const D60 = 0x1000; + const D65 = 0x2000; + const D70 = 0x4000; + const D75 = 0x8000; + const D80 = 0x10000; + const D85 = 0x20000; + const D90 = 0x40000; + const D95 = 0x80000; + const D100 = 0x0010_0000; + const D105 = 0x0020_0000; + const D110 = 0x0040_0000; + const D115 = 0x0080_0000; + const D120 = 0x0100_0000; + const D125 = 0x0200_0000; + const D130 = 0x0400_0000; + const D135 = 0x0800_0000; + const D140 = 0x1000_0000; + const D145 = 0x2000_0000; + const D150 = 0x4000_0000; + } +} + +impl MedDelay { + pub fn as_str(self) -> Option<&'static str> { + Some(match self { + MedDelay::D0 => "0", + MedDelay::D5 => "5", + MedDelay::D10 => "10", + MedDelay::D15 => "15", + MedDelay::D20 => "20", + MedDelay::D25 => "25", + MedDelay::D30 => "30", + MedDelay::D35 => "35", + MedDelay::D40 => "40", + MedDelay::D45 => "45", + MedDelay::D50 => "50", + MedDelay::D55 => "55", + MedDelay::D60 => "60", + MedDelay::D65 => "65", + MedDelay::D70 => "70", + MedDelay::D75 => "75", + MedDelay::D80 => "80", + MedDelay::D85 => "85", + MedDelay::D90 => "90", + MedDelay::D95 => "95", + MedDelay::D100 => "100", + MedDelay::D105 => "105", + MedDelay::D110 => "110", + MedDelay::D115 => "115", + MedDelay::D120 => "120", + MedDelay::D125 => "125", + MedDelay::D130 => "130", + MedDelay::D135 => "135", + MedDelay::D140 => "140", + MedDelay::D145 => "145", + MedDelay::D150 => "150", + _ => return None, + }) + } + + pub fn into_meddelay(&self) -> u32 { + self.to_index() * 5 + } +} + +extra_bitflag_impls! {MedDelay} +impl_serde_for_bitflags!(MedDelay); + +bitflags! { + pub struct LongDelay : u32 { + const D0 = 0x1; + const D10 = 0x2; + const D20 = 0x4; + const D30 = 0x8; + const D40 = 0x10; + const D50 = 0x20; + const D60 = 0x40; + const D70 = 0x80; + const D80 = 0x100; + const D90 = 0x200; + const D100 = 0x400; + const D110 = 0x800; + const D120 = 0x1000; + const D130 = 0x2000; + const D140 = 0x4000; + const D150 = 0x8000; + const D160 = 0x10000; + const D170 = 0x20000; + const D180 = 0x40000; + const D190 = 0x80000; + const D200 = 0x0010_0000; + const D210 = 0x0020_0000; + const D220 = 0x0040_0000; + const D230 = 0x0080_0000; + const D240 = 0x0100_0000; + const D250 = 0x0200_0000; + const D260 = 0x0400_0000; + const D270 = 0x0800_0000; + const D280 = 0x1000_0000; + const D290 = 0x2000_0000; + const D300 = 0x4000_0000; + } +} + +impl LongDelay { + pub fn as_str(self) -> Option<&'static str> { + Some(match self { + LongDelay::D0 => "0", + LongDelay::D10 => "10", + LongDelay::D20 => "20", + LongDelay::D30 => "30", + LongDelay::D40 => "40", + LongDelay::D50 => "50", + LongDelay::D60 => "60", + LongDelay::D70 => "70", + LongDelay::D80 => "80", + LongDelay::D90 => "90", + LongDelay::D100 => "100", + LongDelay::D110 => "110", + LongDelay::D120 => "120", + LongDelay::D130 => "130", + LongDelay::D140 => "140", + LongDelay::D150 => "150", + LongDelay::D160 => "160", + LongDelay::D170 => "170", + LongDelay::D180 => "180", + LongDelay::D190 => "190", + LongDelay::D200 => "200", + LongDelay::D210 => "210", + LongDelay::D220 => "220", + LongDelay::D230 => "230", + LongDelay::D240 => "240", + LongDelay::D250 => "250", + LongDelay::D260 => "260", + LongDelay::D270 => "270", + LongDelay::D280 => "280", + LongDelay::D290 => "290", + LongDelay::D300 => "300", + _ => return None, + }) + } + + pub fn into_longdelay(&self) -> u32 { + self.to_index() * 10 + } +} + +extra_bitflag_impls! {LongDelay} +impl_serde_for_bitflags!(LongDelay); + +bitflags! { + pub struct BoolFlag : u32 { + const TRUE = 0x1; + const FALSE = 0x2; + } +} + +extra_bitflag_impls! {BoolFlag} +impl_serde_for_bitflags!(BoolFlag); + +impl BoolFlag { + pub fn into_bool(self) -> bool { + matches!(self, BoolFlag::TRUE) + } + + pub fn as_str(self) -> Option<&'static str> { + Some(match self { + BoolFlag::TRUE => "True", + _ => "False", + }) + } +} + +#[repr(u32)] +#[derive( + Debug, Clone, Copy, PartialEq, Eq, FromPrimitive, EnumIter, Serialize_repr, Deserialize_repr, +)] +pub enum InputFrequency { + None = 0, + Normal = 1, + Medium = 2, + High = 4, +} + +impl InputFrequency { + pub fn into_u32(self) -> u32 { + match self { + InputFrequency::None => u32::MAX, + InputFrequency::Normal => 8, + InputFrequency::Medium => 6, + InputFrequency::High => 4, + } + } + + pub fn as_str(self) -> Option<&'static str> { + Some(match self { + InputFrequency::None => "None", + InputFrequency::Normal => "Normal", + InputFrequency::Medium => "Medium", + InputFrequency::High => "High", + }) + } +} + +impl ToggleTrait for InputFrequency { + fn to_toggle_strs() -> Vec<&'static str> { + InputFrequency::iter() + .map(|i| i.as_str().unwrap_or("")) + .collect() + } + + fn to_toggle_vals() -> Vec { + InputFrequency::iter().map(|i| i as u32).collect() + } +} + +/// Item Selections +#[repr(i32)] +#[derive( + Debug, Clone, Copy, PartialEq, FromPrimitive, EnumIter, Serialize_repr, Deserialize_repr, +)] +pub enum CharacterItem { + None = 0, + PlayerVariation1 = 0x1, + PlayerVariation2 = 0x2, + PlayerVariation3 = 0x4, + PlayerVariation4 = 0x8, + PlayerVariation5 = 0x10, + PlayerVariation6 = 0x20, + PlayerVariation7 = 0x40, + PlayerVariation8 = 0x80, + CpuVariation1 = 0x100, + CpuVariation2 = 0x200, + CpuVariation3 = 0x400, + CpuVariation4 = 0x800, + CpuVariation5 = 0x1000, + CpuVariation6 = 0x2000, + CpuVariation7 = 0x4000, + CpuVariation8 = 0x8000, +} + +impl CharacterItem { + pub fn as_idx(self) -> u32 { + log_2(self as i32 as u32) + } + + pub fn as_str(self) -> Option<&'static str> { + Some(match self { + CharacterItem::PlayerVariation1 => "Player 1st Var.", + CharacterItem::PlayerVariation2 => "Player 2nd Var.", + CharacterItem::PlayerVariation3 => "Player 3rd Var.", + CharacterItem::PlayerVariation4 => "Player 4th Var.", + CharacterItem::PlayerVariation5 => "Player 5th Var.", + CharacterItem::PlayerVariation6 => "Player 6th Var.", + CharacterItem::PlayerVariation7 => "Player 7th Var.", + CharacterItem::PlayerVariation8 => "Player 8th Var.", + CharacterItem::CpuVariation1 => "CPU 1st Var.", + CharacterItem::CpuVariation2 => "CPU 2nd Var.", + CharacterItem::CpuVariation3 => "CPU 3rd Var.", + CharacterItem::CpuVariation4 => "CPU 4th Var.", + CharacterItem::CpuVariation5 => "CPU 5th Var.", + CharacterItem::CpuVariation6 => "CPU 6th Var.", + CharacterItem::CpuVariation7 => "CPU 7th Var.", + CharacterItem::CpuVariation8 => "CPU 8th Var.", + _ => "None", + }) + } +} + +impl ToggleTrait for CharacterItem { + fn to_toggle_strs() -> Vec<&'static str> { + CharacterItem::iter() + .map(|i| i.as_str().unwrap_or("")) + .collect() + } + + fn to_toggle_vals() -> Vec { + CharacterItem::iter().map(|i| i as u32).collect() + } +} + +bitflags! { + pub struct MashTrigger : u32 { + const HIT = 0b0000_0000_0000_0000_0001; + const BLOCK = 0b0000_0000_0000_0000_0010; + const PARRY = 0b0000_0000_0000_0000_0100; + const TUMBLE = 0b0000_0000_0000_0000_1000; + const LANDING = 0b0000_0000_0000_0001_0000; + const TRUMP = 0b0000_0000_0000_0010_0000; + const FOOTSTOOL = 0b0000_0000_0000_0100_0000; + const CLATTER = 0b0000_0000_0000_1000_0000; + const LEDGE = 0b0000_0000_0001_0000_0000; + const TECH = 0b0000_0000_0010_0000_0000; + const MISTECH = 0b0000_0000_0100_0000_0000; + const GROUNDED = 0b0000_0000_1000_0000_0000; + const AIRBORNE = 0b0000_0001_0000_0000_0000; + const DISTANCE_CLOSE = 0b0000_0010_0000_0000_0000; + const DISTANCE_MID = 0b0000_0100_0000_0000_0000; + const DISTANCE_FAR = 0b0000_1000_0000_0000_0000; + const ALWAYS = 0b0001_0000_0000_0000_0000; + } +} + +impl MashTrigger { + pub fn as_str(self) -> Option<&'static str> { + Some(match self { + MashTrigger::HIT => "Hitstun", + MashTrigger::BLOCK => "Shieldstun", + MashTrigger::PARRY => "Parry", + MashTrigger::TUMBLE => "Tumble", + MashTrigger::LANDING => "Landing", + MashTrigger::TRUMP => "Ledge Trump", + MashTrigger::FOOTSTOOL => "Footstool", + MashTrigger::CLATTER => "Clatter", + MashTrigger::LEDGE => "Ledge Option", + MashTrigger::TECH => "Tech Option", + MashTrigger::MISTECH => "Mistech Option", + MashTrigger::GROUNDED => "Grounded", + MashTrigger::AIRBORNE => "Airborne", + MashTrigger::DISTANCE_CLOSE => "Distance: Close", + MashTrigger::DISTANCE_MID => "Distance: Mid", + MashTrigger::DISTANCE_FAR => "Distance: Far", + MashTrigger::ALWAYS => "Always", + _ => return None, + }) + } + + const fn default() -> MashTrigger { + // Hit, block, clatter + MashTrigger::HIT + .union(MashTrigger::BLOCK) + .union(MashTrigger::CLATTER) + } +} + +extra_bitflag_impls! {MashTrigger} +impl_serde_for_bitflags!(MashTrigger); + +#[derive(Clone, Copy, Serialize, Deserialize, Debug)] +pub struct DamagePercent(pub u32, pub u32); + +impl SliderTrait for DamagePercent { + fn get_limits() -> (u32, u32) { + (0, 150) + } +} + +impl DamagePercent { + const fn default() -> DamagePercent { + DamagePercent(0, 150) + } +} + +bitflags! { + pub struct SaveDamage : u32 + { + const DEFAULT = 0b001; + const SAVED = 0b010; + const RANDOM = 0b100; + } +} + +impl SaveDamage { + fn as_str(self) -> Option<&'static str> { + Some(match self { + SaveDamage::DEFAULT => "Default", + SaveDamage::SAVED => "Save State", + SaveDamage::RANDOM => "Random Value", + _ => return None, + }) + } +} + +extra_bitflag_impls! {SaveDamage} +impl_serde_for_bitflags!(SaveDamage); + +#[repr(C)] +#[derive(Clone, Copy, Serialize, Deserialize, Debug)] +pub struct TrainingModpackMenu { + pub aerial_delay: Delay, + pub air_dodge_dir: Direction, + pub attack_angle: AttackAngle, + pub buff_state: BuffOption, + pub character_item: CharacterItem, + pub clatter_strength: InputFrequency, + pub crouch: OnOff, + pub di_state: Direction, + pub falling_aerials: BoolFlag, + pub fast_fall_delay: Delay, + pub fast_fall: BoolFlag, + pub follow_up: Action, + pub frame_advantage: OnOff, + pub full_hop: BoolFlag, + pub hitbox_vis: OnOff, + pub input_delay: Delay, + pub ledge_delay: LongDelay, + pub ledge_state: LedgeOption, + pub mash_state: Action, + pub mash_triggers: MashTrigger, + pub miss_tech_state: MissTechFlags, + pub oos_offset: Delay, + pub pummel_delay: MedDelay, + pub quick_menu: OnOff, + pub reaction_time: Delay, + pub save_damage_cpu: SaveDamage, + pub save_damage_limits_cpu: DamagePercent, + pub save_damage_player: SaveDamage, + pub save_damage_limits_player: DamagePercent, + pub save_state_autoload: OnOff, + pub save_state_enable: OnOff, + pub save_state_mirroring: SaveStateMirroring, + pub sdi_state: Direction, + pub sdi_strength: InputFrequency, + pub shield_state: Shield, + pub shield_tilt: Direction, + pub stage_hazards: OnOff, + pub tech_state: TechFlags, + pub throw_delay: MedDelay, + pub throw_state: ThrowOption, +} + +const fn num_bits() -> u32 { + (std::mem::size_of::() * 8) as u32 +} + +fn log_2(x: u32) -> u32 { + if x == 0 { + 0 + } else { + num_bits::() - x.leading_zeros() - 1 + } +} + +#[repr(C)] +#[derive(Debug, Serialize, Deserialize)] +pub struct MenuJsonStruct { + pub menu: TrainingModpackMenu, + pub defaults_menu: TrainingModpackMenu, + // pub last_focused_submenu: &str +} + +// Fighter Ids +#[repr(i32)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum FighterId { + Player = 0, + CPU = 1, +} + +#[derive(Clone)] +pub enum SubMenuType { + TOGGLE, + SLIDER, +} + +impl SubMenuType { + pub fn from_str(s: &str) -> SubMenuType { + match s { + "toggle" => SubMenuType::TOGGLE, + "slider" => SubMenuType::SLIDER, + _ => panic!("Unexpected SubMenuType!"), + } + } +} + +pub static DEFAULTS_MENU: TrainingModpackMenu = TrainingModpackMenu { + aerial_delay: Delay::empty(), + air_dodge_dir: Direction::empty(), + attack_angle: AttackAngle::empty(), + buff_state: BuffOption::empty(), + character_item: CharacterItem::None, + clatter_strength: InputFrequency::None, + crouch: OnOff::Off, + di_state: Direction::empty(), + falling_aerials: BoolFlag::empty(), + fast_fall_delay: Delay::empty(), + fast_fall: BoolFlag::empty(), + follow_up: Action::empty(), + frame_advantage: OnOff::Off, + full_hop: BoolFlag::empty(), + hitbox_vis: OnOff::On, + input_delay: Delay::D0, + ledge_delay: LongDelay::empty(), + ledge_state: LedgeOption::all(), + mash_state: Action::empty(), + mash_triggers: MashTrigger::default(), + miss_tech_state: MissTechFlags::all(), + oos_offset: Delay::empty(), + pummel_delay: MedDelay::empty(), + quick_menu: OnOff::Off, + reaction_time: Delay::empty(), + save_damage_cpu: SaveDamage::DEFAULT, + save_damage_limits_cpu: DamagePercent::default(), + save_damage_player: SaveDamage::DEFAULT, + save_damage_limits_player: DamagePercent::default(), + save_state_autoload: OnOff::Off, + save_state_enable: OnOff::On, + save_state_mirroring: SaveStateMirroring::None, + sdi_state: Direction::empty(), + sdi_strength: InputFrequency::None, + shield_state: Shield::None, + shield_tilt: Direction::empty(), + stage_hazards: OnOff::Off, + tech_state: TechFlags::all(), + throw_delay: MedDelay::empty(), + throw_state: ThrowOption::NONE, +}; + +pub static mut MENU: TrainingModpackMenu = DEFAULTS_MENU; + +#[derive(Content, Clone, Serialize)] +pub struct Slider { + pub selected_min: u32, + pub selected_max: u32, + pub abs_min: u32, + pub abs_max: u32, +} + +#[derive(Content, Clone, Serialize)] +pub struct Toggle<'a> { + pub toggle_value: u32, + pub toggle_title: &'a str, + pub checked: bool, +} + +#[derive(Content, Clone, Serialize)] +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>, + pub slider: Option, + pub _type: &'a str, +} + +impl<'a> SubMenu<'a> { + pub fn add_toggle(&mut self, toggle_value: u32, toggle_title: &'a str, checked: bool) { + self.toggles.push(Toggle { + toggle_value, + toggle_title, + checked, + }); + } + pub fn new_with_toggles( + submenu_title: &'a str, + submenu_id: &'a str, + help_text: &'a str, + is_single_option: bool, + initial_value: &u32 + ) -> 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(), + slider: None, + _type: "toggle", + }; + + let values = T::to_toggle_vals(); + let titles = T::to_toggle_strs(); + for i in 0..values.len() { + let checked: bool = (values[i] & initial_value) > 0 + || (!values[i] == 0 && initial_value == &0); + instance.add_toggle(values[i], titles[i], checked); + } + instance + } + pub fn new_with_slider( + submenu_title: &'a str, + submenu_id: &'a str, + help_text: &'a str, + initial_lower_value: &u32, + initial_upper_value: &u32, + ) -> SubMenu<'a> { + let min_max = S::get_limits(); + SubMenu { + submenu_title: submenu_title, + submenu_id: submenu_id, + help_text: help_text, + is_single_option: false, + toggles: Vec::new(), + slider: Some(Slider { + selected_min: *initial_lower_value, + selected_max: *initial_upper_value, + abs_min: min_max.0, + abs_max: min_max.1, + }), + _type: "slider", + } + } +} + +#[derive(Content, Serialize)] +pub struct Tab<'a> { + pub tab_id: &'a str, + pub tab_title: &'a str, + pub tab_submenus: Vec>, +} + +impl<'a> Tab<'a> { + pub fn add_submenu_with_toggles( + &mut self, + submenu_title: &'a str, + submenu_id: &'a str, + help_text: &'a str, + is_single_option: bool, + initial_value: &u32, + ) { + self.tab_submenus.push(SubMenu::new_with_toggles::( + submenu_title, + submenu_id, + help_text, + is_single_option, + initial_value, + )); + } + + pub fn add_submenu_with_slider( + &mut self, + submenu_title: &'a str, + submenu_id: &'a str, + help_text: &'a str, + initial_lower_value: &u32, + initial_upper_value: &u32, + ) { + self.tab_submenus.push(SubMenu::new_with_slider::( + submenu_title, + submenu_id, + help_text, + initial_lower_value, + initial_upper_value, + )) + } +} + +#[derive(Content, Serialize)] +pub struct UiMenu<'a> { + pub tabs: Vec>, +} + +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::( + "Mash Toggles", + "mash_state", + "Mash Toggles: Actions to be performed as soon as possible", + false, + &(MENU.mash_state.bits as u32), + ); + mash_tab.add_submenu_with_toggles::( + "Followup Toggles", + "follow_up", + "Followup Toggles: Actions to be performed after the Mash option", + false, + &(MENU.follow_up.bits as u32), + ); + mash_tab.add_submenu_with_toggles::( + "Mash Triggers", + "mash_triggers", + "Mash triggers: When the Mash Option will be performed", + false, + &(MENU.mash_triggers.bits as u32), + ); + mash_tab.add_submenu_with_toggles::( + "Attack Angle", + "attack_angle", + "Attack Angle: For attacks that can be angled, such as some forward tilts", + false, + &(MENU.attack_angle.bits as u32), + ); + mash_tab.add_submenu_with_toggles::( + "Throw Options", + "throw_state", + "Throw Options: Throw to be performed when a grab is landed", + false, + &(MENU.throw_state.bits as u32), + ); + mash_tab.add_submenu_with_toggles::( + "Throw Delay", + "throw_delay", + "Throw Delay: How many frames to delay the throw option", + false, + &(MENU.throw_delay.bits as u32), + ); + mash_tab.add_submenu_with_toggles::( + "Pummel Delay", + "pummel_delay", + "Pummel Delay: How many frames after a grab to wait before starting to pummel", + false, + &(MENU.pummel_delay.bits as u32), + ); + mash_tab.add_submenu_with_toggles::( + "Falling Aerials", + "falling_aerials", + "Falling Aerials: Should aerials be performed when rising or when falling", + false, + &(MENU.falling_aerials.bits as u32), + ); + mash_tab.add_submenu_with_toggles::( + "Full Hop", + "full_hop", + "Full Hop: Should the CPU perform a full hop or a short hop", + false, + &(MENU.full_hop.bits as u32), + ); + mash_tab.add_submenu_with_toggles::( + "Aerial Delay", + "aerial_delay", + "Aerial Delay: How long to delay a Mash aerial attack", + false, + &(MENU.aerial_delay.bits as u32), + ); + mash_tab.add_submenu_with_toggles::( + "Fast Fall", + "fast_fall", + "Fast Fall: Should the CPU fastfall during a jump", + false, + &(MENU.fast_fall.bits as u32), + ); + mash_tab.add_submenu_with_toggles::( + "Fast Fall Delay", + "fast_fall_delay", + "Fast Fall Delay: How many frames the CPU should delay their fastfall", + false, + &(MENU.fast_fall_delay.bits as u32), + ); + mash_tab.add_submenu_with_toggles::( + "OoS Offset", + "oos_offset", + "OoS Offset: How many times the CPU shield can be hit before performing a Mash option", + false, + &(MENU.oos_offset.bits as u32), + ); + mash_tab.add_submenu_with_toggles::( + "Reaction Time", + "reaction_time", + "Reaction Time: How many frames to delay before performing a mash option", + false, + &(MENU.reaction_time.bits as u32), + ); + 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::( + "Airdodge Direction", + "air_dodge_dir", + "Airdodge Direction: Direction to angle airdodges", + false, + &(MENU.air_dodge_dir.bits as u32), + ); + defensive_tab.add_submenu_with_toggles::( + "DI Direction", + "di_state", + "DI Direction: Direction to angle the directional influence during hitlag", + false, + &(MENU.di_state.bits as u32), + ); + defensive_tab.add_submenu_with_toggles::( + "SDI Direction", + "sdi_state", + "SDI Direction: Direction to angle the smash directional influence during hitlag", + false, + &(MENU.sdi_state.bits as u32), + ); + defensive_tab.add_submenu_with_toggles::( + "SDI Strength", + "sdi_strength", + "SDI Strength: Relative strength of the smash directional influence inputs", + true, + &(MENU.sdi_strength as u32), + ); + defensive_tab.add_submenu_with_toggles::( + "Clatter Strength", + "clatter_strength", + "Clatter Strength: Relative strength of the mashing out of grabs, buries, etc.", + true, + &(MENU.clatter_strength as u32), + ); + defensive_tab.add_submenu_with_toggles::( + "Ledge Options", + "ledge_state", + "Ledge Options: Actions to be taken when on the ledge", + false, + &(MENU.ledge_state.bits as u32), + ); + defensive_tab.add_submenu_with_toggles::( + "Ledge Delay", + "ledge_delay", + "Ledge Delay: How many frames to delay the ledge option", + false, + &(MENU.ledge_delay.bits as u32), + ); + defensive_tab.add_submenu_with_toggles::( + "Tech Options", + "tech_state", + "Tech Options: Actions to take when slammed into a hard surface", + false, + &(MENU.tech_state.bits as u32), + ); + defensive_tab.add_submenu_with_toggles::( + "Mistech Options", + "miss_tech_state", + "Mistech Options: Actions to take after missing a tech", + false, + &(MENU.miss_tech_state.bits as u32), + ); + defensive_tab.add_submenu_with_toggles::( + "Shield Toggles", + "shield_state", + "Shield Toggles: CPU Shield Behavior", + true, + &(MENU.shield_state as u32), + ); + defensive_tab.add_submenu_with_toggles::( + "Shield Tilt", + "shield_tilt", + "Shield Tilt: Direction to tilt the shield", + false, // TODO: Should this be true? + &(MENU.shield_tilt.bits as u32), + ); + + defensive_tab.add_submenu_with_toggles::( + "Crouch", + "crouch", + "Crouch: Should the CPU crouch when on the ground", + true, + &(MENU.crouch as u32), + ); + overall_menu.tabs.push(defensive_tab); + + let mut save_state_tab = Tab { + tab_id: "save_state", + tab_title: "Save States", + tab_submenus: Vec::new(), + }; + save_state_tab.add_submenu_with_toggles::( + "Mirroring", + "save_state_mirroring", + "Mirroring: Flips save states in the left-right direction across the stage center", + true, + &(MENU.save_state_mirroring as u32), + ); + save_state_tab.add_submenu_with_toggles::( + "Auto Save States", + "save_state_autoload", + "Auto Save States: Load save state when any fighter dies", + true, + &(MENU.save_state_autoload as u32), + ); + save_state_tab.add_submenu_with_toggles::( + "Save Dmg (CPU)", + "save_damage_cpu", + "Save Damage: Should save states retain CPU damage", + true, + &(MENU.save_damage_cpu.bits as u32), + ); + save_state_tab.add_submenu_with_slider::( + "Dmg Range (CPU)", + "save_damage_limits_cpu", + "Limits on random damage to apply to the CPU when loading a save state", + &(MENU.save_damage_limits_cpu.0 as u32), + &(MENU.save_damage_limits_cpu.1 as u32), + ); + save_state_tab.add_submenu_with_toggles::( + "Save Dmg (Player)", + "save_damage_player", + "Save Damage: Should save states retain player damage", + true, + &(MENU.save_damage_player.bits as u32), + ); + save_state_tab.add_submenu_with_slider::( + "Dmg Range (Player)", + "save_damage_limits_player", + "Limits on random damage to apply to the player when loading a save state", + &(MENU.save_damage_limits_player.0 as u32), + &(MENU.save_damage_limits_player.1 as u32), + ); + save_state_tab.add_submenu_with_toggles::( + "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, + &(MENU.save_state_enable as u32), + ); + save_state_tab.add_submenu_with_toggles::( + "Character Item", + "character_item", + "Character Item: CPU/Player item to hold when loading a save state", + true, + &(MENU.character_item as u32), + ); + save_state_tab.add_submenu_with_toggles::( + "Buff Options", + "buff_state", + "Buff Options: Buff(s) to be applied to respective character when loading save states", + false, + &(MENU.buff_state.bits as u32), + ); + overall_menu.tabs.push(save_state_tab); + + let mut misc_tab = Tab { + tab_id: "misc", + tab_title: "Misc Settings", + tab_submenus: Vec::new(), + }; + misc_tab.add_submenu_with_toggles::( + "Frame Advantage", + "frame_advantage", + "Frame Advantage: Display the time difference between when the player is actionable and the CPU is actionable", + true, + &(MENU.frame_advantage as u32), + ); + misc_tab.add_submenu_with_toggles::( + "Hitbox Visualization", + "hitbox_vis", + "Hitbox Visualization: Should hitboxes be displayed, hiding other visual effects", + true, + &(MENU.hitbox_vis as u32), + ); + misc_tab.add_submenu_with_toggles::( + "Input Delay", + "input_delay", + "Input Delay: Frames to delay player inputs by", + true, + &(MENU.input_delay.bits as u32), + ); + misc_tab.add_submenu_with_toggles::( + "Stage Hazards", + "stage_hazards", + "Stage Hazards: Should stage hazards be present", + true, + &(MENU.stage_hazards as u32), + ); + misc_tab.add_submenu_with_toggles::( + "Quick Menu", + "quick_menu", + "Quick Menu: Should use quick or web menu", + true, + &(MENU.quick_menu as u32), + ); + overall_menu.tabs.push(misc_tab); + + overall_menu +} diff --git a/training_mod_tui/src/gauge.rs b/training_mod_tui/src/gauge.rs new file mode 100644 index 0000000..cb9f462 --- /dev/null +++ b/training_mod_tui/src/gauge.rs @@ -0,0 +1,27 @@ +pub enum GaugeState { + MinHover, + MaxHover, + MinSelected, + MaxSelected, + None, +} + +pub struct DoubleEndedGauge { + pub state: GaugeState, + pub selected_min: u32, + pub selected_max: u32, + pub abs_min: u32, + pub abs_max: u32, +} + +impl DoubleEndedGauge { + pub fn new() -> DoubleEndedGauge { + DoubleEndedGauge { + state: GaugeState::None, + selected_min: 0, + selected_max: 150, + abs_min: 0, + abs_max: 150, + } + } +} diff --git a/training_mod_tui/src/lib.rs b/training_mod_tui/src/lib.rs index 3fee42e..0ba36b9 100644 --- a/training_mod_tui/src/lib.rs +++ b/training_mod_tui/src/lib.rs @@ -1,367 +1,808 @@ -use training_mod_consts::{Slider, SubMenu, SubMenuType, Toggle, UiMenu}; -use tui::{ - backend::{Backend}, - layout::{Constraint, Corner, Direction, Layout}, - style::{Modifier, Style}, - text::{Span, Spans}, - widgets::{Tabs, Paragraph, Block, List, ListItem, ListState}, - Frame, -}; - -pub use tui::{backend::TestBackend, Terminal, style::Color}; -use std::collections::HashMap; - -mod list; - -use crate::list::{StatefulList, MultiStatefulList}; - -/// We should hold a list of SubMenus. -/// The currently selected SubMenu should also have an associated list with necessary information. -/// We can convert the option types (Toggle, OnOff, Slider) to lists -pub struct App<'a> { - pub tabs: StatefulList<&'a str>, - pub menu_items: HashMap<&'a str, MultiStatefulList>>, - pub selected_sub_menu_toggles: MultiStatefulList>, - pub selected_sub_menu_sliders: MultiStatefulList, - pub outer_list: bool -} - -impl<'a> App<'a> { - pub fn new(menu: UiMenu<'a>) -> App<'a> { - let num_lists = 3; - - let mut menu_items_stateful = HashMap::new(); - menu.tabs.iter().for_each(|tab| { - menu_items_stateful.insert( - tab.tab_title, - MultiStatefulList::with_items(tab.tab_submenus.clone(), num_lists) - ); - }); - - let mut app = App { - 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_sliders: MultiStatefulList::with_items(vec![], 0), - outer_list: true - }; - app.set_sub_menu_items(); - app - } - - pub fn set_sub_menu_items(&mut self) { - 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); - 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(); - match SubMenuType::from_str(self.sub_menu_selected()._type) { - SubMenuType::TOGGLE => { - self.selected_sub_menu_toggles = MultiStatefulList::with_items( - toggles, - 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()} ) - }, - }; - } - - fn tab_selected(&self) -> &str { - self.tabs.items.get(self.tabs.state.selected().unwrap()).unwrap() - } - - 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() - } - - 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(), - } - } - - pub fn sub_menu_next_list(&mut self) { - 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(), - } - } - - pub fn sub_menu_previous(&mut self) { - 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(), - } - } - - pub fn sub_menu_previous_list(&mut self) { - 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(), - } - } - - 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.toggle_title) - ).collect(), toggle_list.state.clone()) - }).collect() - }, - SubMenuType::SLIDER => { - vec![(vec![], ListState::default())] - }, - }) - } - - pub fn on_a(&mut self) { - if self.outer_list { - self.outer_list = false; - } else { - let tab_selected = self.tabs.items.get(self.tabs.state.selected().unwrap()).unwrap(); - let (list_section, list_idx) = self.menu_items.get(tab_selected) - .unwrap() - .idx_to_list_idx(self.menu_items.get(tab_selected).unwrap().state); - let selected_sub_menu = self.menu_items.get_mut(tab_selected) - .unwrap() - .lists[list_section] - .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; - let state = self.selected_sub_menu_toggles.state; - self.selected_sub_menu_toggles.lists.iter_mut() - .map(|list| (list.state.selected(), &mut list.items)) - .for_each(|(state, toggle_list)| toggle_list.iter_mut() - .enumerate() - .for_each(|(i, o)| - if state.is_some() && i == state.unwrap() { - if !o.checked { - o.checked = true; - } else { - o.checked = false; - } - } else if is_single_option { - o.checked = false; - } - )); - selected_sub_menu.toggles.iter_mut() - .enumerate() - .for_each(|(i, o)| { - if i == state { - if !o.checked { - o.checked = true; - } else { - o.checked = false; - } - } else if is_single_option { - o.checked = false; - } - }); - }, - SubMenuType::SLIDER => { - // self.selected_sub_menu_sliders.selected_list_item().checked = true; - } - } - } - } - - pub fn on_b(&mut self) { - self.outer_list = true; - } - - pub fn on_l(&mut self) { - if self.outer_list { - self.tabs.previous(); - self.set_sub_menu_items(); - } - } - - pub fn on_r(&mut self) { - if self.outer_list { - self.tabs.next(); - self.set_sub_menu_items(); - } - } - - pub fn on_up(&mut self) { - if self.outer_list { - self.menu_items.get_mut(self.tabs.items.get(self.tabs.state.selected().unwrap()).unwrap()).unwrap().previous(); - self.set_sub_menu_items(); - } else { - self.sub_menu_previous(); - } - } - - pub fn on_down(&mut self) { - if self.outer_list { - self.menu_items.get_mut(self.tabs.items.get(self.tabs.state.selected().unwrap()).unwrap()).unwrap().next(); - self.set_sub_menu_items(); - } else { - self.sub_menu_next(); - } - } - - pub fn on_left(&mut self) { - if self.outer_list { - self.menu_items.get_mut(self.tabs.items.get(self.tabs.state.selected().unwrap()).unwrap()).unwrap().previous_list(); - self.set_sub_menu_items(); - } else { - self.sub_menu_previous_list(); - } - } - - pub fn on_right(&mut self) { - if self.outer_list { - self.menu_items.get_mut(self.tabs.items.get(self.tabs.state.selected().unwrap()).unwrap()).unwrap().next_list(); - self.set_sub_menu_items(); - } else { - self.sub_menu_next_list(); - } - } -} - -pub fn ui(f: &mut Frame, app: &mut App) -> String { - let app_tabs = &app.tabs; - let tab_selected = app_tabs.state.selected().unwrap(); - let titles = app_tabs.items.iter().cloned().enumerate().map(|(idx, tab)|{ - if idx == tab_selected { - Spans::from(">> ".to_owned() + tab) - } else { - Spans::from(" ".to_owned() + tab) - } - }).collect(); - - let tabs = Tabs::new(titles) - .block(Block::default() - .title( - Spans::from( - Span::styled("Ultimate Training Modpack Menu", - Style::default().fg(Color::LightRed))))) - .style(Style::default().fg(Color::White)) - .highlight_style(Style::default().fg(Color::Yellow)) - .divider("|") - .select(tab_selected); - - let vertical_chunks = Layout::default() - .direction(Direction::Vertical) - .constraints([ - Constraint::Length(2), - Constraint::Max(10), - Constraint::Length(2)].as_ref()) - .split(f.size()); - - let list_chunks = Layout::default() - .direction(Direction::Horizontal) - .constraints([Constraint::Percentage(33), Constraint::Percentage(32), Constraint::Percentage(33)].as_ref()) - .split(vertical_chunks[1]); - - f.render_widget(tabs, vertical_chunks[0]); - - if app.outer_list { - let tab_selected = app.tab_selected(); - let mut item_help = None; - for (list_section, stateful_list) in app.menu_items.get(tab_selected).unwrap().lists.iter().enumerate() { - let items: Vec = stateful_list - .items - .iter() - .map(|i| { - let lines = vec![Spans::from( - if stateful_list.state.selected().is_some() { - i.submenu_title.to_owned() - } else { - " ".to_owned() + i.submenu_title - })]; - ListItem::new(lines).style(Style::default().fg(Color::White)) - }) - .collect(); - - let list = List::new(items) - .block(Block::default() - .title(if list_section == 0 { "Options" } else { "" }) - .style(Style::default().fg(Color::LightRed))) - .highlight_style( - Style::default() - .fg(Color::Green) - .add_modifier(Modifier::BOLD), - ) - .highlight_symbol(">> "); - - let mut state = stateful_list.state.clone(); - if state.selected().is_some() { - item_help = Some(stateful_list.items[state.selected().unwrap()].help_text); - } - - f.render_stateful_widget(list, list_chunks[list_section], &mut state); - } - - // TODO: Add Save Defaults - let help_paragraph = Paragraph::new( - 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]); - } else { - let (title, help_text, mut sub_menu_str_lists) = app.sub_menu_strs_and_states(); - for list_section in 0..sub_menu_str_lists.len() { - let sub_menu_str = sub_menu_str_lists[list_section].0.clone(); - let sub_menu_state = &mut sub_menu_str_lists[list_section].1; - let values_items: Vec = sub_menu_str.iter().map(|s| { - ListItem::new( - vec![ - Spans::from((if s.0 { "X " } else { " " }).to_owned() + s.1) - ] - ) - }).collect(); - - let values_list = List::new(values_items) - .block(Block::default().title(if list_section == 0 { title } else { "" })) - .start_corner(Corner::TopLeft) - .highlight_style( - Style::default() - .fg(Color::LightGreen) - .add_modifier(Modifier::BOLD), - ) - .highlight_symbol(">> "); - f.render_stateful_widget(values_list, list_chunks[list_section], sub_menu_state); - } - - let help_paragraph = Paragraph::new( - help_text.replace('\"', "") + - "\nA: Select toggle | B: Exit submenu" - ).style(Style::default().fg(Color::Cyan)); - f.render_widget(help_paragraph, vertical_chunks[2]); - } - - let mut settings = HashMap::new(); - - // Collect settings for toggles - for key in app.menu_items.keys() { - for list in &app.menu_items.get(key).unwrap().lists { - for sub_menu in &list.items { - let val : usize = sub_menu.toggles.iter() - .filter(|t| t.checked) - .map(|t| t.toggle_value) - .sum(); - - settings.insert(sub_menu.submenu_id, val); - } - } - } - serde_json::to_string(&settings).unwrap() - - // TODO: Add saveDefaults - // if (document.getElementById("saveDefaults").checked) { - // url += "save_defaults=1"; - // } else { - // url = url.slice(0, -1); - // } -} \ No newline at end of file +use training_mod_consts::{Slider, SubMenu, SubMenuType, Toggle, UiMenu}; +use tui::{ + backend::Backend, + layout::{Constraint, Corner, Direction, Layout, Rect}, + style::{Modifier, Style}, + text::{Span, Spans}, + widgets::{Block, LineGauge, List, ListItem, ListState, Paragraph, Tabs}, + Frame, +}; + +use std::collections::HashMap; +use serde_json::{Map, json}; +pub use tui::{backend::TestBackend, style::Color, Terminal}; + +mod gauge; +mod list; + +use crate::gauge::{DoubleEndedGauge, GaugeState}; +use crate::list::{MultiStatefulList, StatefulList}; + +static NX_TUI_WIDTH: u16 = 66; + +/// We should hold a list of SubMenus. +/// The currently selected SubMenu should also have an associated list with necessary information. +/// We can convert the option types (Toggle, OnOff, Slider) to lists +pub struct App<'a> { + pub tabs: StatefulList<&'a str>, + pub menu_items: HashMap<&'a str, MultiStatefulList>>, + pub selected_sub_menu_toggles: MultiStatefulList>, + pub selected_sub_menu_slider: DoubleEndedGauge, + pub outer_list: bool, +} + +impl<'a> App<'a> { + pub fn new(menu: UiMenu<'a>) -> App<'a> { + let num_lists = 3; + + let mut menu_items_stateful = HashMap::new(); + menu.tabs.iter().for_each(|tab| { + menu_items_stateful.insert( + tab.tab_title, + MultiStatefulList::with_items(tab.tab_submenus.clone(), num_lists), + ); + }); + + let mut app = App { + 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_slider: DoubleEndedGauge::new(), + outer_list: true, + }; + app.set_sub_menu_items(); + app + } + + /// Takes the currently selected tab/submenu and clones the options into + /// self.selected_sub_menu_toggles and self.selected_sub_menu_slider + pub fn set_sub_menu_items(&mut self) { + 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); + 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 slider = selected_sub_menu.slider.clone(); + match SubMenuType::from_str(self.sub_menu_selected()._type) { + SubMenuType::TOGGLE => { + self.selected_sub_menu_toggles = MultiStatefulList::with_items( + toggles, + if selected_sub_menu.toggles.len() >= 3 { + 3 + } else { + selected_sub_menu.toggles.len() + }, + ) + } + SubMenuType::SLIDER => { + let slider = slider.unwrap(); + self.selected_sub_menu_slider = DoubleEndedGauge { + state: GaugeState::None, + selected_min: slider.selected_min, + selected_max: slider.selected_max, + abs_min: slider.abs_min, + abs_max: slider.abs_max, + } + } + }; + } + + /// Returns the id of the currently selected tab + fn tab_selected(&self) -> &str { + self.tabs + .items + .get(self.tabs.state.selected().unwrap()) + .unwrap() + } + + /// Returns the currently selected SubMenu struct + /// + /// { + /// submenu_title: &'a str, + /// submenu_id: &'a str, + /// help_text: &'a str, + /// is_single_option: bool, + /// toggles: Vec>, + /// slider: Option, + /// _type: &'a str, + /// } + 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() + } + + /// A "next()" function which differs per submenu type + /// Toggles: calls next() + /// Slider: Swaps between MinHover and MaxHover + 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 => match self.selected_sub_menu_slider.state { + GaugeState::MinHover => self.selected_sub_menu_slider.state = GaugeState::MaxHover, + GaugeState::MaxHover => self.selected_sub_menu_slider.state = GaugeState::MinHover, + _ => {} + }, + } + } + + /// A "next_list()" function which differs per submenu type + /// Toggles: Calls next_list() + /// Slider: + /// * Swaps between MinHover and MaxHover + /// * Increments the selected_min/max if possible + pub fn sub_menu_next_list(&mut self) { + match SubMenuType::from_str(self.sub_menu_selected()._type) { + SubMenuType::TOGGLE => self.selected_sub_menu_toggles.next_list(), + SubMenuType::SLIDER => match self.selected_sub_menu_slider.state { + GaugeState::MinHover => self.selected_sub_menu_slider.state = GaugeState::MaxHover, + GaugeState::MaxHover => self.selected_sub_menu_slider.state = GaugeState::MinHover, + GaugeState::MinSelected => { + if self.selected_sub_menu_slider.selected_min + < self.selected_sub_menu_slider.selected_max + { + self.selected_sub_menu_slider.selected_min += 1; + } + } + GaugeState::MaxSelected => { + if self.selected_sub_menu_slider.selected_max + < self.selected_sub_menu_slider.abs_max + { + self.selected_sub_menu_slider.selected_max += 1; + } + } + GaugeState::None => {} + }, + } + } + + /// A "previous()" function which differs per submenu type + /// Toggles: calls previous() + /// Slider: Swaps between MinHover and MaxHover + pub fn sub_menu_previous(&mut self) { + match SubMenuType::from_str(self.sub_menu_selected()._type) { + SubMenuType::TOGGLE => self.selected_sub_menu_toggles.previous(), + SubMenuType::SLIDER => match self.selected_sub_menu_slider.state { + GaugeState::MinHover => self.selected_sub_menu_slider.state = GaugeState::MaxHover, + GaugeState::MaxHover => self.selected_sub_menu_slider.state = GaugeState::MinHover, + _ => {} + }, + } + } + + /// A "previous_list()" function which differs per submenu type + /// Toggles: Calls previous_list() + /// Slider: + /// * Swaps between MinHover and MaxHover + /// * Decrements the selected_min/max if possible + pub fn sub_menu_previous_list(&mut self) { + match SubMenuType::from_str(self.sub_menu_selected()._type) { + SubMenuType::TOGGLE => self.selected_sub_menu_toggles.previous_list(), + SubMenuType::SLIDER => match self.selected_sub_menu_slider.state { + GaugeState::MinHover => self.selected_sub_menu_slider.state = GaugeState::MaxHover, + GaugeState::MaxHover => self.selected_sub_menu_slider.state = GaugeState::MinHover, + GaugeState::MinSelected => { + if self.selected_sub_menu_slider.selected_min + > self.selected_sub_menu_slider.abs_min + { + self.selected_sub_menu_slider.selected_min -= 1; + } + } + GaugeState::MaxSelected => { + if self.selected_sub_menu_slider.selected_max + > self.selected_sub_menu_slider.selected_min + { + self.selected_sub_menu_slider.selected_max -= 1; + } + } + GaugeState::None => {} + }, + } + } + + /// Returns information about the currently selected submenu + /// + /// 0: Submenu Title + /// 1: Submenu Help Text + /// 2: Vec(toggle checked, title) for toggles, Vec(nothing) for slider + /// 3: ListState for toggles, ListState::new() for slider + /// TODO: Refactor return type into a nice struct + 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.toggle_title)) + .collect(), + toggle_list.state.clone(), + ) + }) + .collect(), + SubMenuType::SLIDER => { + vec![(vec![], ListState::default())] + } + }, + ) + } + + /// Returns information about the currently selected slider + /// 0: Title + /// 1: Help text + /// 2: Reference to self.selected_sub_menu_slider + /// TODO: Refactor return type into a nice struct + pub fn sub_menu_strs_for_slider(&mut self) -> (&str, &str, &DoubleEndedGauge) { + let slider = match SubMenuType::from_str(self.sub_menu_selected()._type) { + SubMenuType::SLIDER => &self.selected_sub_menu_slider, + _ => { + panic!("Slider not selected!"); + } + }; + ( + self.sub_menu_selected().submenu_title, + self.sub_menu_selected().help_text, + slider, + ) + } + + /// Different behavior depending on the current menu location + /// Outer list: Sets self.outer_list to false + /// Toggle submenu: Toggles the selected submenu toggle in self.selected_sub_menu_toggles and in the actual SubMenu struct + /// Slider submenu: Swaps hover/selected state. Updates the actual SubMenu struct if going from Selected -> Hover + pub fn on_a(&mut self) { + let tab_selected = self + .tabs + .items + .get(self.tabs.state.selected().unwrap()) + .unwrap(); + let (list_section, list_idx) = self + .menu_items + .get(tab_selected) + .unwrap() + .idx_to_list_idx(self.menu_items.get(tab_selected).unwrap().state); + let selected_sub_menu = self.menu_items.get_mut(tab_selected).unwrap().lists + [list_section] + .items + .get_mut(list_idx) + .unwrap(); + if self.outer_list { + self.outer_list = false; + match SubMenuType::from_str(selected_sub_menu._type) { + // Need to change the slider state to MinHover so the slider shows up initially + SubMenuType::SLIDER => { + self.selected_sub_menu_slider.state = GaugeState::MinHover; + } + _ => {} + } + } else { + match SubMenuType::from_str(selected_sub_menu._type) { + SubMenuType::TOGGLE => { + let is_single_option = selected_sub_menu.is_single_option; + let state = self.selected_sub_menu_toggles.state; + // Change the toggles in self.selected_sub_menu_toggles (for display) + self.selected_sub_menu_toggles + .lists + .iter_mut() + .map(|list| (list.state.selected(), &mut list.items)) + .for_each(|(state, toggle_list)| { + toggle_list.iter_mut().enumerate().for_each(|(i, o)| { + if state.is_some() && i == state.unwrap() { + if !o.checked { + o.checked = true; + } else { + o.checked = false; + } + } else if is_single_option { + o.checked = false; + } + }) + }); + // Actually change the toggle values in the SubMenu struct + selected_sub_menu + .toggles + .iter_mut() + .enumerate() + .for_each(|(i, o)| { + if i == state { + if !o.checked { + o.checked = true; + } else { + o.checked = false; + } + } else if is_single_option { + o.checked = false; + } + }); + } + SubMenuType::SLIDER => match self.selected_sub_menu_slider.state { + GaugeState::MinHover => { + self.selected_sub_menu_slider.state = GaugeState::MinSelected; + } + GaugeState::MaxHover => { + self.selected_sub_menu_slider.state = GaugeState::MaxSelected; + } + GaugeState::MinSelected => { + self.selected_sub_menu_slider.state = GaugeState::MinHover; + selected_sub_menu.slider = Some(Slider{ + selected_min: self.selected_sub_menu_slider.selected_min, + selected_max: self.selected_sub_menu_slider.selected_max, + abs_min: self.selected_sub_menu_slider.abs_min, + abs_max: self.selected_sub_menu_slider.abs_max, + }); + } + GaugeState::MaxSelected => { + self.selected_sub_menu_slider.state = GaugeState::MaxHover; + selected_sub_menu.slider = Some(Slider{ + selected_min: self.selected_sub_menu_slider.selected_min, + selected_max: self.selected_sub_menu_slider.selected_max, + abs_min: self.selected_sub_menu_slider.abs_min, + abs_max: self.selected_sub_menu_slider.abs_max, + }); + } + GaugeState::None => { + self.selected_sub_menu_slider.state = GaugeState::MinHover; + } + }, + } + } + } + + /// Different behavior depending on the current menu location + /// Outer list: None + /// Toggle submenu: Sets self.outer_list to true + /// Slider submenu: If in a selected state, then commit changes and change to hover. Else set self.outer_list to true + pub fn on_b(&mut self) { + let tab_selected = self + .tabs + .items + .get(self.tabs.state.selected().unwrap()) + .unwrap(); + let (list_section, list_idx) = self + .menu_items + .get(tab_selected) + .unwrap() + .idx_to_list_idx(self.menu_items.get(tab_selected).unwrap().state); + let selected_sub_menu = self.menu_items.get_mut(tab_selected).unwrap().lists[list_section] + .items + .get_mut(list_idx) + .unwrap(); + match SubMenuType::from_str(selected_sub_menu._type) { + SubMenuType::SLIDER => match self.selected_sub_menu_slider.state { + GaugeState::MinSelected => { + self.selected_sub_menu_slider.state = GaugeState::MinHover; + selected_sub_menu.slider = Some(Slider{ + selected_min: self.selected_sub_menu_slider.selected_min, + selected_max: self.selected_sub_menu_slider.selected_max, + abs_min: self.selected_sub_menu_slider.abs_min, + abs_max: self.selected_sub_menu_slider.abs_max, + }); + // Don't go back to the outer list + return; + } + GaugeState::MaxSelected => { + self.selected_sub_menu_slider.state = GaugeState::MaxHover; + selected_sub_menu.slider = Some(Slider{ + selected_min: self.selected_sub_menu_slider.selected_min, + selected_max: self.selected_sub_menu_slider.selected_max, + abs_min: self.selected_sub_menu_slider.abs_min, + abs_max: self.selected_sub_menu_slider.abs_max, + }); + // Don't go back to the outer list + return; + } + _ => {} + }, + _ => {} + } + self.outer_list = true; + self.set_sub_menu_items(); + } + + pub fn on_l(&mut self) { + if self.outer_list { + self.tabs.previous(); + self.set_sub_menu_items(); + } + } + + pub fn on_r(&mut self) { + if self.outer_list { + self.tabs.next(); + self.set_sub_menu_items(); + } + } + + pub fn on_up(&mut self) { + if self.outer_list { + self.menu_items + .get_mut( + self.tabs + .items + .get(self.tabs.state.selected().unwrap()) + .unwrap(), + ) + .unwrap() + .previous(); + self.set_sub_menu_items(); + } else { + self.sub_menu_previous(); + } + } + + pub fn on_down(&mut self) { + if self.outer_list { + self.menu_items + .get_mut( + self.tabs + .items + .get(self.tabs.state.selected().unwrap()) + .unwrap(), + ) + .unwrap() + .next(); + self.set_sub_menu_items(); + } else { + self.sub_menu_next(); + } + } + + pub fn on_left(&mut self) { + if self.outer_list { + self.menu_items + .get_mut( + self.tabs + .items + .get(self.tabs.state.selected().unwrap()) + .unwrap(), + ) + .unwrap() + .previous_list(); + self.set_sub_menu_items(); + } else { + self.sub_menu_previous_list(); + } + } + + pub fn on_right(&mut self) { + if self.outer_list { + self.menu_items + .get_mut( + self.tabs + .items + .get(self.tabs.state.selected().unwrap()) + .unwrap(), + ) + .unwrap() + .next_list(); + self.set_sub_menu_items(); + } else { + self.sub_menu_next_list(); + } + } +} + +pub fn ui(f: &mut Frame, app: &mut App) -> String { + let app_tabs = &app.tabs; + let tab_selected = app_tabs.state.selected().unwrap(); + let mut span_selected = Spans::default(); + + let titles: Vec = app_tabs + .items + .iter() + .cloned() + .enumerate() + .map(|(idx, tab)| { + if idx == tab_selected { + span_selected = Spans::from("> ".to_owned() + tab); + Spans::from("> ".to_owned() + tab) + } else { + Spans::from(" ".to_owned() + tab) + } + }) + .collect(); + // There is only enough room to display 3 tabs of text + // So lets replace tabs not near the selected with "..." + let all_windows: Vec<&[Spans]> = titles + .windows(3) + .filter(|w| w.contains(&titles[tab_selected])) + .collect(); + let first_window = all_windows[0]; + let mut titles: Vec = titles + .iter() + .cloned() + .map( + // Converts all tabs not in the window to "..." + |t| { + if first_window.contains(&t) { + t + } else { + Spans::from("...".to_owned()) + } + }, + ) + .collect(); + // Don't keep consecutive "..." tabs + titles.dedup(); + // Now that the size of the titles vector has changed, need to re-locate the selected tab + let tab_selected_deduped: usize = titles + .iter() + .cloned() + .position(|span| span == span_selected) + .unwrap_or(0); + + let tabs = Tabs::new(titles) + .block(Block::default().title(Spans::from(Span::styled( + "Ultimate Training Modpack Menu", + Style::default().fg(Color::LightRed), + )))) + .style(Style::default().fg(Color::White)) + .highlight_style(Style::default().fg(Color::Yellow)) + .divider("|") + .select(tab_selected_deduped); + + let vertical_chunks = Layout::default() + .direction(Direction::Vertical) + .constraints( + [ + Constraint::Length(2), + Constraint::Max(10), + Constraint::Length(2), + ] + .as_ref(), + ) + .split(f.size()); + + // Prevent overflow by adding a length constraint of NX_TUI_WIDTH + // Need to add a second constraint since the .expand_to_fill() method + // is not publicly exposed, and the attribute defaults to true. + // https://github.com/fdehau/tui-rs/blob/v0.19.0/src/layout.rs#L121 + let vertical_chunks: Vec = vertical_chunks + .iter() + .map(|chunk| { + Layout::default() + .direction(Direction::Horizontal) + .constraints( + [ + Constraint::Length(NX_TUI_WIDTH), // Width of the TUI terminal + Constraint::Min(0), // Fill the remainder margin + ] + .as_ref(), + ) + .split(*chunk)[0] + } + ) + .collect(); + + + let list_chunks = Layout::default() + .direction(Direction::Horizontal) + .constraints( + [ + Constraint::Percentage(33), + Constraint::Percentage(33), + Constraint::Percentage(33), + ] + .as_ref(), + ) + .split(vertical_chunks[1]); + + f.render_widget(tabs, vertical_chunks[0]); + + if app.outer_list { + let tab_selected = app.tab_selected(); + let mut item_help = None; + for (list_section, stateful_list) in app + .menu_items + .get(tab_selected) + .unwrap() + .lists + .iter() + .enumerate() + { + let items: Vec = stateful_list + .items + .iter() + .map(|i| { + let lines = vec![Spans::from(if stateful_list.state.selected().is_some() { + i.submenu_title.to_owned() + } else { + " ".to_owned() + i.submenu_title + })]; + ListItem::new(lines).style(Style::default().fg(Color::White)) + }) + .collect(); + + let list = List::new(items) + .block( + Block::default() + .title(if list_section == 0 { "Options" } else { "" }) + .style(Style::default().fg(Color::LightRed)), + ) + .highlight_style( + Style::default() + .fg(Color::Green) + .add_modifier(Modifier::BOLD), + ) + .highlight_symbol(">> "); + + let mut state = stateful_list.state.clone(); + if state.selected().is_some() { + item_help = Some(stateful_list.items[state.selected().unwrap()].help_text); + } + + f.render_stateful_widget(list, list_chunks[list_section], &mut state); + } + + // TODO: Add Save Defaults + let help_paragraph = Paragraph::new( + 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]); + } else { + if matches!(app.selected_sub_menu_slider.state, GaugeState::None) { + let (title, help_text, mut sub_menu_str_lists) = app.sub_menu_strs_and_states(); + for list_section in 0..sub_menu_str_lists.len() { + let sub_menu_str = sub_menu_str_lists[list_section].0.clone(); + let sub_menu_state = &mut sub_menu_str_lists[list_section].1; + let values_items: Vec = sub_menu_str + .iter() + .map(|s| { + ListItem::new(vec![Spans::from( + (if s.0 { "X " } else { " " }).to_owned() + s.1, + )]) + }) + .collect(); + + let values_list = List::new(values_items) + .block(Block::default().title(if list_section == 0 { title } else { "" })) + .start_corner(Corner::TopLeft) + .highlight_style( + Style::default() + .fg(Color::LightGreen) + .add_modifier(Modifier::BOLD), + ) + .highlight_symbol(">> "); + f.render_stateful_widget(values_list, list_chunks[list_section], sub_menu_state); + } + let help_paragraph = Paragraph::new( + help_text.replace('\"', "") + "\nA: Select toggle | B: Exit submenu", + ) + .style(Style::default().fg(Color::Cyan)); + f.render_widget(help_paragraph, vertical_chunks[2]); + } else { + let (_title, help_text, gauge_vals) = app.sub_menu_strs_for_slider(); + let abs_min = gauge_vals.abs_min; + let abs_max = gauge_vals.abs_max; + let selected_min = gauge_vals.selected_min; + let selected_max = gauge_vals.selected_max; + let lbl_ratio = 0.95; // Needed so that the upper limit label is visible + let constraints = [ + Constraint::Ratio((lbl_ratio * (selected_min-abs_min) as f32) as u32, abs_max-abs_min), + Constraint::Ratio((lbl_ratio * (selected_max-selected_min) as f32) as u32, abs_max-abs_min), + Constraint::Ratio((lbl_ratio * (abs_max-selected_max) as f32) as u32, abs_max-abs_min), + Constraint::Min(3), // For upper limit label + ]; + let gauge_chunks = Layout::default() + .direction(Direction::Horizontal) + .constraints(constraints) + .split(vertical_chunks[1]); + + let slider_lbls = [ + abs_min, + selected_min, + selected_max, + abs_max, + ]; + for (idx, lbl) in slider_lbls.iter().enumerate() { + let mut line_set = tui::symbols::line::NORMAL; + line_set.horizontal = "-"; + let mut gauge = LineGauge::default() + .ratio(1.0) + .label(format!("{}", lbl)) + .style(Style::default().fg(Color::White)) + .line_set(line_set) + .gauge_style(Style::default().fg(Color::White).bg(Color::Black)); + if idx == 1 { + // Slider between selected_min and selected_max + match gauge_vals.state { + GaugeState::MinHover => { + gauge = gauge.style(Style::default().fg(Color::Red)) + } + GaugeState::MinSelected => { + gauge = gauge.style(Style::default().fg(Color::Green)) + } + _ => {} + } + gauge = gauge.gauge_style(Style::default().fg(Color::Yellow).bg(Color::Black)); + } else if idx == 2 { + // Slider between selected_max and abs_max + match gauge_vals.state { + GaugeState::MaxHover => { + gauge = gauge.style(Style::default().fg(Color::Red)) + } + GaugeState::MaxSelected => { + gauge = gauge.style(Style::default().fg(Color::Green)) + } + _ => {} + } + } else if idx == 3 { + // Slider for abs_max + // We only want the label to show, so set the line character to " " + let mut line_set = tui::symbols::line::NORMAL; + line_set.horizontal = " "; + gauge = gauge.line_set(line_set); + + // For some reason, the selected_max slider displays on top + // So we need to change the abs_max slider styling to match + // If the selected_max is close enough to the abs_max + if (selected_max as f32 / abs_max as f32) > 0.95 { + gauge = gauge.style(match gauge_vals.state { + GaugeState::MaxHover => Style::default().fg(Color::Red), + GaugeState::MaxSelected => Style::default().fg(Color::Green), + _ => Style::default(), + }) + } + } + f.render_widget(gauge, gauge_chunks[idx]); + } + + let help_paragraph = Paragraph::new( + help_text.replace('\"', "") + "\nA: Select toggle | B: Exit submenu", + ) + .style(Style::default().fg(Color::Cyan)); + f.render_widget(help_paragraph, vertical_chunks[2]); + } + } + + // Collect settings + let mut settings = Map::new(); + for key in app.menu_items.keys() { + for list in &app.menu_items.get(key).unwrap().lists { + for sub_menu in &list.items { + if !sub_menu.toggles.is_empty() { + let val: u32 = sub_menu + .toggles + .iter() + .filter(|t| t.checked) + .map(|t| t.toggle_value) + .sum(); + settings.insert(sub_menu.submenu_id.to_string(), json!(val)); + } else if sub_menu.slider.is_some() { + let s: &Slider = sub_menu.slider.as_ref().unwrap(); + let val: Vec = vec![s.selected_min, s.selected_max]; + settings.insert(sub_menu.submenu_id.to_string(), json!(val)); + } else { + panic!("Could not collect settings for {:?}", sub_menu.submenu_id); + } + } + } + } + serde_json::to_string(&settings).unwrap() + + // TODO: Add saveDefaults +} diff --git a/training_mod_tui/src/list.rs b/training_mod_tui/src/list.rs index 2e0d337..46a0026 100644 --- a/training_mod_tui/src/list.rs +++ b/training_mod_tui/src/list.rs @@ -21,7 +21,6 @@ impl MultiStatefulList { (self.total_len as f32 / self.lists.len() as f32).ceil() as usize * (list_section + 1), self.total_len); if (list_section_min_idx..list_section_max_idx).contains(&idx) { - // println!("\n{}: ({}, {})", idx, list_section_min_idx, list_section_max_idx); return (list_section, idx - list_section_min_idx) } } diff --git a/training_mod_tui/src/main.rs b/training_mod_tui/src/main.rs index febec79..4cf9f31 100644 --- a/training_mod_tui/src/main.rs +++ b/training_mod_tui/src/main.rs @@ -54,6 +54,8 @@ fn ensure_menu_retains_selections() -> Result<(), Box> { } fn main() -> Result<(), Box> { + let args: Vec = std::env::args().collect(); + let inputs = args.get(1); let menu; unsafe { menu = get_menu(); @@ -61,6 +63,21 @@ fn main() -> Result<(), Box> { #[cfg(not(feature = "has_terminal"))] { let (mut terminal, mut app) = test_backend_setup(menu)?; + if inputs.is_some() { + inputs.unwrap().split(",").for_each(|input| { + match input.to_uppercase().as_str() { + "L" => app.on_l(), + "R" => app.on_r(), + "A" => app.on_a(), + "B" => app.on_b(), + "UP" => app.on_up(), + "DOWN" => app.on_down(), + "LEFT" => app.on_left(), + "RIGHT" => app.on_right(), + _ => {} + } + }) + } let mut json_response = String::new(); let frame_res = terminal.draw(|f| json_response = training_mod_tui::ui(f, &mut app))?;