mirror of
https://github.com/jugeeya/UltimateTrainingModpack.git
synced 2024-11-24 02:44:17 +00:00
Randomized percent on load (#394)
* Create save states tab * Initial work - crashes on boot * Change usizes to u32's. Refactor toggle "checked" logic. Add blujay's panic tracker. * Rename tui mins/maxes * Fix misc. TUI bugs * Fix panic caused by prematurely setting the submenu state to GaugeState::None Set submenu state to GaugeState::MinHover when opening a slider menu so that the slider is immediately loaded When changing from GaugeState::Min/MaxSelected to Min/MaxHover, commit changes from App.current_sub_menu_slider to SubMenu.slider so that it can be exported to JSON * Merge save_damage and save_state_pct_rand_enable settings * Add comments to training_mod_tui::lib.rs * Add icon * Initial work on web slider Todo: Styling polish Bugfix for initial settings load Handle dragging using gamepad * Style and fix web slider * Add separate settings for player random damage * TUI styling fixes * Paginate TUI tabs * Address CR comments
This commit is contained in:
parent
aea5011a89
commit
fa451986e0
17 changed files with 3063 additions and 2004 deletions
|
@ -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 {
|
||||
|
|
21
src/lib.rs
21
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::<String>() {
|
||||
Some(s) => &s[..],
|
||||
None => "Box<Any>",
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
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)*));
|
||||
|
|
1
src/static/css/nouislider.min.css
vendored
Normal file
1
src/static/css/nouislider.min.css
vendored
Normal file
|
@ -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}
|
|
@ -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;
|
||||
}
|
||||
|
|
Before Width: | Height: | Size: 2.4 KiB After Width: | Height: | Size: 2.4 KiB |
83
src/static/img/save_damage_limits_cpu.svg
Normal file
83
src/static/img/save_damage_limits_cpu.svg
Normal file
|
@ -0,0 +1,83 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
width="80.0px"
|
||||
height="80.0px"
|
||||
viewBox="0 0 80.0 80.0"
|
||||
version="1.1"
|
||||
id="SVGRoot"
|
||||
sodipodi:docname="save_damage_limits.svg"
|
||||
inkscape:version="1.1 (c68e22c387, 2021-05-23)"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/">
|
||||
<defs
|
||||
id="defs5493" />
|
||||
<sodipodi:namedview
|
||||
id="base"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1.0"
|
||||
inkscape:pageopacity="0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:zoom="7.9195959"
|
||||
inkscape:cx="43.373425"
|
||||
inkscape:cy="38.322662"
|
||||
inkscape:document-units="px"
|
||||
inkscape:current-layer="layer1"
|
||||
inkscape:document-rotation="0"
|
||||
showgrid="true"
|
||||
inkscape:window-width="1904"
|
||||
inkscape:window-height="1001"
|
||||
inkscape:window-x="192"
|
||||
inkscape:window-y="0"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:pagecheckerboard="1">
|
||||
<inkscape:grid
|
||||
type="xygrid"
|
||||
id="grid6063" />
|
||||
</sodipodi:namedview>
|
||||
<metadata
|
||||
id="metadata5496">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<g
|
||||
inkscape:label="Layer 1"
|
||||
inkscape:groupmode="layer"
|
||||
id="layer1">
|
||||
<path
|
||||
style="fill:#000000;stroke:#000000;stroke-width:6;stroke-linecap:round;stroke-linejoin:miter;stroke-opacity:1;stroke-miterlimit:4;stroke-dasharray:none"
|
||||
d="M 15,65 65,15"
|
||||
id="path6070" />
|
||||
<circle
|
||||
style="opacity:1;fill:none;stroke:#000000;stroke-width:6;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;paint-order:fill markers stroke"
|
||||
id="path6072"
|
||||
cx="22.5"
|
||||
cy="22.5"
|
||||
r="7.5" />
|
||||
<circle
|
||||
style="opacity:1;fill:none;stroke:#000000;stroke-width:6;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;paint-order:fill markers stroke"
|
||||
id="path6072-8"
|
||||
cx="60.051399"
|
||||
cy="55.43021"
|
||||
r="7.5" />
|
||||
<path
|
||||
style="fill:none;stroke:#000000;stroke-width:3;stroke-linecap:round;stroke-linejoin:miter;stroke-opacity:1;stroke-miterlimit:4;stroke-dasharray:none"
|
||||
d="M 10,5 H 70"
|
||||
id="path993" />
|
||||
<path
|
||||
style="fill:none;stroke:#000000;stroke-width:3;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="M 10,75 H 70"
|
||||
id="path993-8" />
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 2.8 KiB |
83
src/static/img/save_damage_limits_player.svg
Normal file
83
src/static/img/save_damage_limits_player.svg
Normal file
|
@ -0,0 +1,83 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
width="80.0px"
|
||||
height="80.0px"
|
||||
viewBox="0 0 80.0 80.0"
|
||||
version="1.1"
|
||||
id="SVGRoot"
|
||||
sodipodi:docname="save_damage_limits.svg"
|
||||
inkscape:version="1.1 (c68e22c387, 2021-05-23)"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/">
|
||||
<defs
|
||||
id="defs5493" />
|
||||
<sodipodi:namedview
|
||||
id="base"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1.0"
|
||||
inkscape:pageopacity="0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:zoom="7.9195959"
|
||||
inkscape:cx="43.373425"
|
||||
inkscape:cy="38.322662"
|
||||
inkscape:document-units="px"
|
||||
inkscape:current-layer="layer1"
|
||||
inkscape:document-rotation="0"
|
||||
showgrid="true"
|
||||
inkscape:window-width="1904"
|
||||
inkscape:window-height="1001"
|
||||
inkscape:window-x="192"
|
||||
inkscape:window-y="0"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:pagecheckerboard="1">
|
||||
<inkscape:grid
|
||||
type="xygrid"
|
||||
id="grid6063" />
|
||||
</sodipodi:namedview>
|
||||
<metadata
|
||||
id="metadata5496">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<g
|
||||
inkscape:label="Layer 1"
|
||||
inkscape:groupmode="layer"
|
||||
id="layer1">
|
||||
<path
|
||||
style="fill:#000000;stroke:#000000;stroke-width:6;stroke-linecap:round;stroke-linejoin:miter;stroke-opacity:1;stroke-miterlimit:4;stroke-dasharray:none"
|
||||
d="M 15,65 65,15"
|
||||
id="path6070" />
|
||||
<circle
|
||||
style="opacity:1;fill:none;stroke:#000000;stroke-width:6;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;paint-order:fill markers stroke"
|
||||
id="path6072"
|
||||
cx="22.5"
|
||||
cy="22.5"
|
||||
r="7.5" />
|
||||
<circle
|
||||
style="opacity:1;fill:none;stroke:#000000;stroke-width:6;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;paint-order:fill markers stroke"
|
||||
id="path6072-8"
|
||||
cx="60.051399"
|
||||
cy="55.43021"
|
||||
r="7.5" />
|
||||
<path
|
||||
style="fill:none;stroke:#000000;stroke-width:3;stroke-linecap:round;stroke-linejoin:miter;stroke-opacity:1;stroke-miterlimit:4;stroke-dasharray:none"
|
||||
d="M 10,5 H 70"
|
||||
id="path993" />
|
||||
<path
|
||||
style="fill:none;stroke:#000000;stroke-width:3;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="M 10,75 H 70"
|
||||
id="path993-8" />
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 2.8 KiB |
75
src/static/img/save_damage_player.svg
Normal file
75
src/static/img/save_damage_player.svg
Normal file
|
@ -0,0 +1,75 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
width="80.0px"
|
||||
height="80.0px"
|
||||
viewBox="0 0 80.0 80.0"
|
||||
version="1.1"
|
||||
id="SVGRoot"
|
||||
sodipodi:docname="save_damage.svg"
|
||||
inkscape:version="1.0.1 (3bc2e813f5, 2020-09-07)">
|
||||
<defs
|
||||
id="defs5493" />
|
||||
<sodipodi:namedview
|
||||
id="base"
|
||||
pagecolor="#000000"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1.0"
|
||||
inkscape:pageopacity="0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:zoom="7.9195959"
|
||||
inkscape:cx="43.461799"
|
||||
inkscape:cy="28.245216"
|
||||
inkscape:document-units="px"
|
||||
inkscape:current-layer="layer1"
|
||||
inkscape:document-rotation="0"
|
||||
showgrid="true"
|
||||
inkscape:window-width="1431"
|
||||
inkscape:window-height="1150"
|
||||
inkscape:window-x="192"
|
||||
inkscape:window-y="0"
|
||||
inkscape:window-maximized="0">
|
||||
<inkscape:grid
|
||||
type="xygrid"
|
||||
id="grid6063" />
|
||||
</sodipodi:namedview>
|
||||
<metadata
|
||||
id="metadata5496">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
<dc:title></dc:title>
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<g
|
||||
inkscape:label="Layer 1"
|
||||
inkscape:groupmode="layer"
|
||||
id="layer1">
|
||||
<path
|
||||
style="fill:#000000;stroke:#000000;stroke-width:6;stroke-linecap:round;stroke-linejoin:miter;stroke-opacity:1;stroke-miterlimit:4;stroke-dasharray:none"
|
||||
d="M 15,65 65,15"
|
||||
id="path6070" />
|
||||
<circle
|
||||
style="opacity:1;fill:none;stroke:#000000;stroke-width:6;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;paint-order:fill markers stroke"
|
||||
id="path6072"
|
||||
cx="22.5"
|
||||
cy="22.5"
|
||||
r="7.5" />
|
||||
<circle
|
||||
style="opacity:1;fill:none;stroke:#000000;stroke-width:6;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;paint-order:fill markers stroke"
|
||||
id="path6072-8"
|
||||
cx="60.051399"
|
||||
cy="55.43021"
|
||||
r="7.5" />
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 2.4 KiB |
1
src/static/js/nouislider.min.js
vendored
Normal file
1
src/static/js/nouislider.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
|
@ -23,17 +23,31 @@ var DEFAULTS_PREFIX = '__';
|
|||
|
||||
// Set input handlers
|
||||
if (isNx) {
|
||||
window.nx.footer.setAssign('A', '', function () { select(document.activeElement) }, { se: '' });
|
||||
window.nx.footer.setAssign('B', '', closeOrExit, { se: '' });
|
||||
window.nx.footer.setAssign('X', '', resetCurrentMenu, { se: '' });
|
||||
window.nx.footer.setAssign('L', '', resetAllMenus, { se: '' });
|
||||
window.nx.footer.setAssign('R', '', saveDefaults, { se: '' });
|
||||
window.nx.footer.setAssign('ZR', '', cycleNextTab, { se: '' });
|
||||
window.nx.footer.setAssign('ZL', '', cyclePrevTab, { se: '' });
|
||||
window.nx.addEventListener("message", function(msg) { setSettingsFromJSON(msg)});
|
||||
window.nx.sendMessage("loaded");
|
||||
window.nx.addEventListener("message", function (msg) { setSettingsFromJSON(msg.data) });
|
||||
document.addEventListener('keydown', (event) => {
|
||||
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,6 +285,8 @@ const isValueInBitmask = (value, mask) => (mask & value) != 0;
|
|||
const setOptionsForMenu = (menuId) => {
|
||||
const modal = document.querySelector(`.modal[data-id="${menuId}"]`);
|
||||
|
||||
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');
|
||||
|
@ -291,23 +301,36 @@ const setOptionsForMenu = (menuId) => {
|
|||
selectSingleOption(modal.querySelector('button'));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Slider menu
|
||||
slider = modal.querySelector('.modal-slider');
|
||||
setSliderVals(slider, settings[menuId]);
|
||||
}
|
||||
};
|
||||
|
||||
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}']`);
|
||||
|
||||
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;
|
||||
} 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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,10 +5,12 @@
|
|||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
|
||||
<title>Modpack Menu</title>
|
||||
<link rel="stylesheet" href="./css/nouislider.min.css">
|
||||
<link rel="stylesheet" href="./css/training_modpack.css" />
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<script defer src="./js/nouislider.min.js"></script>
|
||||
<script defer src="./js/training_modpack.js"></script>
|
||||
<div class="header">
|
||||
<a id="ret-button" tabindex="-1" class="return-icon-container" onclick="closeOrExit()">
|
||||
|
@ -42,6 +44,20 @@
|
|||
</div>
|
||||
</button>
|
||||
{{/toggles}}
|
||||
{{#slider}}
|
||||
<div class="modal-slider-label">
|
||||
<p>{{submenu_title}}</p>
|
||||
</div>
|
||||
<div
|
||||
id="{{submenu_id}}-slider"
|
||||
data-selected-min="{{selected_min}}"
|
||||
data-selected-max="{{selected_max}}"
|
||||
data-abs-min="{{abs_min}}"
|
||||
data-abs-max="{{abs_max}}"
|
||||
class="modal-slider"
|
||||
>
|
||||
</div>
|
||||
{{/slider}}
|
||||
</div>
|
||||
{{/tab_submenus}}
|
||||
<div id="{{tab_id}}_tab" class="tab-content hide">
|
||||
|
|
|
@ -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 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);
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
#![feature(const_option)]
|
||||
#[macro_use]
|
||||
extern crate bitflags;
|
||||
|
||||
|
@ -14,13 +13,16 @@ 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<usize>;
|
||||
fn to_toggle_vals() -> Vec<u32>;
|
||||
}
|
||||
|
||||
pub trait SliderTrait {
|
||||
fn get_limits() -> (u32, u32);
|
||||
}
|
||||
|
||||
// bitflag helper function macro
|
||||
|
@ -73,9 +75,9 @@ macro_rules! extra_bitflag_impls {
|
|||
all_options.iter().map(|i| i.as_str().unwrap_or("")).collect()
|
||||
}
|
||||
|
||||
fn to_toggle_vals() -> Vec<usize> {
|
||||
fn to_toggle_vals() -> Vec<u32> {
|
||||
let all_options = <$e>::all().to_vec();
|
||||
all_options.iter().map(|i| i.bits() as usize).collect()
|
||||
all_options.iter().map(|i| i.bits() as u32).collect()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -91,6 +93,18 @@ pub fn get_random_int(_max: i32) -> i32 {
|
|||
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<T>(arg: &[T]) -> &T {
|
||||
&arg[get_random_int(arg.len() as i32) as usize]
|
||||
}
|
||||
|
@ -289,8 +303,8 @@ impl ToggleTrait for Shield {
|
|||
Shield::iter().map(|i| i.as_str().unwrap_or("")).collect()
|
||||
}
|
||||
|
||||
fn to_toggle_vals() -> Vec<usize> {
|
||||
Shield::iter().map(|i| i as usize).collect()
|
||||
fn to_toggle_vals() -> Vec<u32> {
|
||||
Shield::iter().map(|i| i as u32).collect()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -322,8 +336,8 @@ impl ToggleTrait for SaveStateMirroring {
|
|||
.collect()
|
||||
}
|
||||
|
||||
fn to_toggle_vals() -> Vec<usize> {
|
||||
SaveStateMirroring::iter().map(|i| i as usize).collect()
|
||||
fn to_toggle_vals() -> Vec<u32> {
|
||||
SaveStateMirroring::iter().map(|i| i as u32).collect()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -355,7 +369,7 @@ impl ToggleTrait for OnOff {
|
|||
fn to_toggle_strs() -> Vec<&'static str> {
|
||||
vec!["Off", "On"]
|
||||
}
|
||||
fn to_toggle_vals() -> Vec<usize> {
|
||||
fn to_toggle_vals() -> Vec<u32> {
|
||||
vec![0, 1]
|
||||
}
|
||||
}
|
||||
|
@ -875,8 +889,8 @@ impl ToggleTrait for InputFrequency {
|
|||
.collect()
|
||||
}
|
||||
|
||||
fn to_toggle_vals() -> Vec<usize> {
|
||||
InputFrequency::iter().map(|i| i as usize).collect()
|
||||
fn to_toggle_vals() -> Vec<u32> {
|
||||
InputFrequency::iter().map(|i| i as u32).collect()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -940,8 +954,8 @@ impl ToggleTrait for CharacterItem {
|
|||
.collect()
|
||||
}
|
||||
|
||||
fn to_toggle_vals() -> Vec<usize> {
|
||||
CharacterItem::iter().map(|i| i as usize).collect()
|
||||
fn to_toggle_vals() -> Vec<u32> {
|
||||
CharacterItem::iter().map(|i| i as u32).collect()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -993,17 +1007,56 @@ impl MashTrigger {
|
|||
|
||||
const fn default() -> MashTrigger {
|
||||
// Hit, block, clatter
|
||||
MashTrigger::HIT.union(MashTrigger::BLOCK).union(MashTrigger::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 {
|
||||
// Mash Tab
|
||||
pub aerial_delay: Delay,
|
||||
pub air_dodge_dir: Direction,
|
||||
pub attack_angle: AttackAngle,
|
||||
|
@ -1029,7 +1082,10 @@ pub struct TrainingModpackMenu {
|
|||
pub pummel_delay: MedDelay,
|
||||
pub quick_menu: OnOff,
|
||||
pub reaction_time: Delay,
|
||||
pub save_damage: OnOff,
|
||||
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,
|
||||
|
@ -1043,71 +1099,15 @@ pub struct TrainingModpackMenu {
|
|||
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<T>() -> usize {
|
||||
std::mem::size_of::<T>() * 8
|
||||
const fn num_bits<T>() -> u32 {
|
||||
(std::mem::size_of::<T>() * 8) as u32
|
||||
}
|
||||
|
||||
fn log_2(x: u32) -> u32 {
|
||||
if x == 0 {
|
||||
0
|
||||
} else {
|
||||
num_bits::<u32>() 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),
|
||||
);
|
||||
num_bits::<u32>() - x.leading_zeros() - 1
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1169,7 +1169,10 @@ pub static DEFAULTS_MENU: TrainingModpackMenu = TrainingModpackMenu {
|
|||
pummel_delay: MedDelay::empty(),
|
||||
quick_menu: OnOff::Off,
|
||||
reaction_time: Delay::empty(),
|
||||
save_damage: OnOff::On,
|
||||
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,
|
||||
|
@ -1185,37 +1188,38 @@ pub static DEFAULTS_MENU: TrainingModpackMenu = TrainingModpackMenu {
|
|||
|
||||
pub static mut MENU: TrainingModpackMenu = DEFAULTS_MENU;
|
||||
|
||||
#[derive(Content, Clone)]
|
||||
#[derive(Content, Clone, Serialize)]
|
||||
pub struct Slider {
|
||||
pub min: usize,
|
||||
pub max: usize,
|
||||
pub index: usize,
|
||||
pub value: usize,
|
||||
pub selected_min: u32,
|
||||
pub selected_max: u32,
|
||||
pub abs_min: u32,
|
||||
pub abs_max: u32,
|
||||
}
|
||||
|
||||
#[derive(Content, Clone)]
|
||||
#[derive(Content, Clone, Serialize)]
|
||||
pub struct Toggle<'a> {
|
||||
pub toggle_value: usize,
|
||||
pub toggle_value: u32,
|
||||
pub toggle_title: &'a str,
|
||||
pub checked: bool,
|
||||
}
|
||||
|
||||
#[derive(Content, Clone)]
|
||||
#[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<Toggle<'a>>,
|
||||
pub slider: Option<Slider>,
|
||||
pub _type: &'a str,
|
||||
}
|
||||
|
||||
impl<'a> SubMenu<'a> {
|
||||
pub fn add_toggle(&mut self, toggle_value: usize, toggle_title: &'a str) {
|
||||
pub fn add_toggle(&mut self, toggle_value: u32, toggle_title: &'a str, checked: bool) {
|
||||
self.toggles.push(Toggle {
|
||||
toggle_value: toggle_value,
|
||||
toggle_title: toggle_title,
|
||||
checked: false,
|
||||
toggle_value,
|
||||
toggle_title,
|
||||
checked,
|
||||
});
|
||||
}
|
||||
pub fn new_with_toggles<T: ToggleTrait>(
|
||||
|
@ -1223,6 +1227,7 @@ impl<'a> SubMenu<'a> {
|
|||
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,
|
||||
|
@ -1230,19 +1235,45 @@ impl<'a> SubMenu<'a> {
|
|||
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() {
|
||||
instance.add_toggle(values[i], titles[i]);
|
||||
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<S: SliderTrait>(
|
||||
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)]
|
||||
#[derive(Content, Serialize)]
|
||||
pub struct Tab<'a> {
|
||||
pub tab_id: &'a str,
|
||||
pub tab_title: &'a str,
|
||||
|
@ -1256,17 +1287,36 @@ impl<'a> Tab<'a> {
|
|||
submenu_id: &'a str,
|
||||
help_text: &'a str,
|
||||
is_single_option: bool,
|
||||
initial_value: &u32,
|
||||
) {
|
||||
self.tab_submenus.push(SubMenu::new_with_toggles::<T>(
|
||||
submenu_title,
|
||||
submenu_id,
|
||||
help_text,
|
||||
is_single_option,
|
||||
initial_value,
|
||||
));
|
||||
}
|
||||
|
||||
pub fn add_submenu_with_slider<S: SliderTrait>(
|
||||
&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::<S>(
|
||||
submenu_title,
|
||||
submenu_id,
|
||||
help_text,
|
||||
initial_lower_value,
|
||||
initial_upper_value,
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Content)]
|
||||
#[derive(Content, Serialize)]
|
||||
pub struct UiMenu<'a> {
|
||||
pub tabs: Vec<Tab<'a>>,
|
||||
}
|
||||
|
@ -1284,84 +1334,98 @@ pub unsafe fn get_menu() -> UiMenu<'static> {
|
|||
"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::<Action>(
|
||||
"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::<MashTrigger>(
|
||||
"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::<AttackAngle>(
|
||||
"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::<ThrowOption>(
|
||||
"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::<MedDelay>(
|
||||
"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::<MedDelay>(
|
||||
"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::<BoolFlag>(
|
||||
"Falling Aerials",
|
||||
"falling_aerials",
|
||||
"Falling Aerials: Should aerials be performed when rising or when falling",
|
||||
false, // TODO: Should this be a single option submenu?
|
||||
false,
|
||||
&(MENU.falling_aerials.bits as u32),
|
||||
);
|
||||
mash_tab.add_submenu_with_toggles::<BoolFlag>(
|
||||
"Full Hop",
|
||||
"full_hop",
|
||||
"Full Hop: Should the CPU perform a full hop or a short hop",
|
||||
false,
|
||||
&(MENU.full_hop.bits as u32),
|
||||
);
|
||||
mash_tab.add_submenu_with_toggles::<Delay>(
|
||||
"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::<BoolFlag>(
|
||||
"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::<Delay>(
|
||||
"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::<Delay>(
|
||||
"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::<Delay>(
|
||||
"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);
|
||||
|
||||
|
@ -1375,178 +1439,199 @@ pub unsafe fn get_menu() -> UiMenu<'static> {
|
|||
"air_dodge_dir",
|
||||
"Airdodge Direction: Direction to angle airdodges",
|
||||
false,
|
||||
&(MENU.air_dodge_dir.bits as u32),
|
||||
);
|
||||
defensive_tab.add_submenu_with_toggles::<Direction>(
|
||||
"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::<Direction>(
|
||||
"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::<InputFrequency>(
|
||||
"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::<InputFrequency>(
|
||||
"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::<LedgeOption>(
|
||||
"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::<LongDelay>(
|
||||
"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::<TechFlags>(
|
||||
"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::<MissTechFlags>(
|
||||
"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>(
|
||||
"Shield Toggles",
|
||||
"shield_state",
|
||||
"Shield Toggles: CPU Shield Behavior",
|
||||
true,
|
||||
&(MENU.shield_state as u32),
|
||||
);
|
||||
defensive_tab.add_submenu_with_toggles::<Direction>(
|
||||
"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::<BuffOption>(
|
||||
"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::<CharacterItem>(
|
||||
"Character Item",
|
||||
"character_item",
|
||||
"Character Item: CPU/Player item to hold when loading a save state",
|
||||
true,
|
||||
);
|
||||
|
||||
defensive_tab.add_submenu_with_toggles::<OnOff>(
|
||||
"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::<SaveStateMirroring>(
|
||||
"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::<OnOff>(
|
||||
"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::<SaveDamage>(
|
||||
"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::<DamagePercent>(
|
||||
"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::<SaveDamage>(
|
||||
"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::<DamagePercent>(
|
||||
"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::<OnOff>(
|
||||
"Enable Save States",
|
||||
"save_state_enable",
|
||||
"Save States: Enable save states! Save a state with Grab+Down Taunt, load it with Grab+Up Taunt.",
|
||||
true,
|
||||
&(MENU.save_state_enable as u32),
|
||||
);
|
||||
save_state_tab.add_submenu_with_toggles::<CharacterItem>(
|
||||
"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::<BuffOption>(
|
||||
"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::<SaveStateMirroring>(
|
||||
"Mirroring",
|
||||
"save_state_mirroring",
|
||||
"Mirroring: Flips save states in the left-right direction across the stage center",
|
||||
true,
|
||||
);
|
||||
misc_tab.add_submenu_with_toggles::<OnOff>(
|
||||
"Save Damage",
|
||||
"save_damage",
|
||||
"Save Damage: Should save states retain player/CPU damage",
|
||||
true,
|
||||
);
|
||||
misc_tab.add_submenu_with_toggles::<OnOff>(
|
||||
"Enable Save States",
|
||||
"save_state_enable",
|
||||
"Save States: Enable save states! Save a state with Grab+Down Taunt, load it with Grab+Up Taunt.",
|
||||
true,
|
||||
);
|
||||
misc_tab.add_submenu_with_toggles::<OnOff>(
|
||||
"Save States Autoload",
|
||||
"save_state_autoload",
|
||||
"Save States Autoload: Load save state when any fighter dies",
|
||||
true,
|
||||
);
|
||||
misc_tab.add_submenu_with_toggles::<OnOff>(
|
||||
"Frame Advantage",
|
||||
"frame_advantage",
|
||||
"Frame Advantage: Display the time difference between when the player is actionable and the CPU is actionable",
|
||||
true,
|
||||
&(MENU.frame_advantage as u32),
|
||||
);
|
||||
misc_tab.add_submenu_with_toggles::<OnOff>(
|
||||
"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::<Delay>(
|
||||
"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::<OnOff>(
|
||||
"Stage Hazards",
|
||||
"stage_hazards",
|
||||
"Stage Hazards: Should stage hazards be present",
|
||||
true,
|
||||
&(MENU.stage_hazards as u32),
|
||||
);
|
||||
misc_tab.add_submenu_with_toggles::<OnOff>(
|
||||
"Quick Menu",
|
||||
"quick_menu",
|
||||
"Quick Menu: Should use quick or web menu",
|
||||
true,
|
||||
&(MENU.quick_menu as u32),
|
||||
);
|
||||
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::<Vec<&str>>();
|
||||
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::<Vec<&str>>();
|
||||
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
|
||||
}
|
||||
|
|
27
training_mod_tui/src/gauge.rs
Normal file
27
training_mod_tui/src/gauge.rs
Normal file
|
@ -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,
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,19 +1,24 @@
|
|||
use training_mod_consts::{Slider, SubMenu, SubMenuType, Toggle, UiMenu};
|
||||
use tui::{
|
||||
backend::{Backend},
|
||||
layout::{Constraint, Corner, Direction, Layout},
|
||||
backend::Backend,
|
||||
layout::{Constraint, Corner, Direction, Layout, Rect},
|
||||
style::{Modifier, Style},
|
||||
text::{Span, Spans},
|
||||
widgets::{Tabs, Paragraph, Block, List, ListItem, ListState},
|
||||
widgets::{Block, LineGauge, List, ListItem, ListState, Paragraph, Tabs},
|
||||
Frame,
|
||||
};
|
||||
|
||||
pub use tui::{backend::TestBackend, Terminal, style::Color};
|
||||
use std::collections::HashMap;
|
||||
use serde_json::{Map, json};
|
||||
pub use tui::{backend::TestBackend, style::Color, Terminal};
|
||||
|
||||
mod gauge;
|
||||
mod list;
|
||||
|
||||
use crate::list::{StatefulList, MultiStatefulList};
|
||||
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.
|
||||
|
@ -22,8 +27,8 @@ pub struct App<'a> {
|
|||
pub tabs: StatefulList<&'a str>,
|
||||
pub menu_items: HashMap<&'a str, MultiStatefulList<SubMenu<'a>>>,
|
||||
pub selected_sub_menu_toggles: MultiStatefulList<Toggle<'a>>,
|
||||
pub selected_sub_menu_sliders: MultiStatefulList<Slider>,
|
||||
pub outer_list: bool
|
||||
pub selected_sub_menu_slider: DoubleEndedGauge,
|
||||
pub outer_list: bool,
|
||||
}
|
||||
|
||||
impl<'a> App<'a> {
|
||||
|
@ -34,7 +39,7 @@ impl<'a> App<'a> {
|
|||
menu.tabs.iter().for_each(|tab| {
|
||||
menu_items_stateful.insert(
|
||||
tab.tab_title,
|
||||
MultiStatefulList::with_items(tab.tab_submenus.clone(), num_lists)
|
||||
MultiStatefulList::with_items(tab.tab_submenus.clone(), num_lists),
|
||||
);
|
||||
});
|
||||
|
||||
|
@ -42,107 +47,268 @@ impl<'a> App<'a> {
|
|||
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
|
||||
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 (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();
|
||||
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()} )
|
||||
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()} )
|
||||
},
|
||||
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()
|
||||
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<Toggle<'a>>,
|
||||
/// slider: Option<Slider>,
|
||||
/// _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()
|
||||
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 => self.selected_sub_menu_sliders.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 => self.selected_sub_menu_sliders.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 => self.selected_sub_menu_sliders.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 => self.selected_sub_menu_sliders.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 => {}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
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,
|
||||
/// 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::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) {
|
||||
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)
|
||||
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();
|
||||
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;
|
||||
self.selected_sub_menu_toggles.lists.iter_mut()
|
||||
// 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)|
|
||||
.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;
|
||||
|
@ -152,8 +318,12 @@ impl<'a> App<'a> {
|
|||
} else if is_single_option {
|
||||
o.checked = false;
|
||||
}
|
||||
));
|
||||
selected_sub_menu.toggles.iter_mut()
|
||||
})
|
||||
});
|
||||
// Actually change the toggle values in the SubMenu struct
|
||||
selected_sub_menu
|
||||
.toggles
|
||||
.iter_mut()
|
||||
.enumerate()
|
||||
.for_each(|(i, o)| {
|
||||
if i == state {
|
||||
|
@ -166,16 +336,89 @@ impl<'a> App<'a> {
|
|||
o.checked = false;
|
||||
}
|
||||
});
|
||||
},
|
||||
SubMenuType::SLIDER => {
|
||||
// self.selected_sub_menu_sliders.selected_list_item().checked = true;
|
||||
}
|
||||
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) {
|
||||
|
@ -194,7 +437,15 @@ impl<'a> App<'a> {
|
|||
|
||||
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.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();
|
||||
|
@ -203,7 +454,15 @@ impl<'a> App<'a> {
|
|||
|
||||
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.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();
|
||||
|
@ -212,7 +471,15 @@ impl<'a> App<'a> {
|
|||
|
||||
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.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();
|
||||
|
@ -221,7 +488,15 @@ impl<'a> App<'a> {
|
|||
|
||||
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.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();
|
||||
|
@ -232,36 +507,106 @@ impl<'a> App<'a> {
|
|||
pub fn ui<B: Backend>(f: &mut Frame<B>, 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)|{
|
||||
let mut span_selected = Spans::default();
|
||||
|
||||
let titles: Vec<Spans> = app_tabs
|
||||
.items
|
||||
.iter()
|
||||
.cloned()
|
||||
.enumerate()
|
||||
.map(|(idx, tab)| {
|
||||
if idx == tab_selected {
|
||||
Spans::from(">> ".to_owned() + tab)
|
||||
span_selected = Spans::from("> ".to_owned() + tab);
|
||||
Spans::from("> ".to_owned() + tab)
|
||||
} else {
|
||||
Spans::from(" ".to_owned() + tab)
|
||||
}
|
||||
}).collect();
|
||||
})
|
||||
.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<Spans> = 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)))))
|
||||
.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);
|
||||
.select(tab_selected_deduped);
|
||||
|
||||
let vertical_chunks = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.constraints([
|
||||
.constraints(
|
||||
[
|
||||
Constraint::Length(2),
|
||||
Constraint::Max(10),
|
||||
Constraint::Length(2)].as_ref())
|
||||
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<Rect> = 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(32), Constraint::Percentage(33)].as_ref())
|
||||
.constraints(
|
||||
[
|
||||
Constraint::Percentage(33),
|
||||
Constraint::Percentage(33),
|
||||
Constraint::Percentage(33),
|
||||
]
|
||||
.as_ref(),
|
||||
)
|
||||
.split(vertical_chunks[1]);
|
||||
|
||||
f.render_widget(tabs, vertical_chunks[0]);
|
||||
|
@ -269,13 +614,19 @@ pub fn ui<B: Backend>(f: &mut Frame<B>, app: &mut App) -> String {
|
|||
if app.outer_list {
|
||||
let tab_selected = app.tab_selected();
|
||||
let mut item_help = None;
|
||||
for (list_section, stateful_list) in app.menu_items.get(tab_selected).unwrap().lists.iter().enumerate() {
|
||||
for (list_section, stateful_list) in app
|
||||
.menu_items
|
||||
.get(tab_selected)
|
||||
.unwrap()
|
||||
.lists
|
||||
.iter()
|
||||
.enumerate()
|
||||
{
|
||||
let items: Vec<ListItem> = stateful_list
|
||||
.items
|
||||
.iter()
|
||||
.map(|i| {
|
||||
let lines = vec![Spans::from(
|
||||
if stateful_list.state.selected().is_some() {
|
||||
let lines = vec![Spans::from(if stateful_list.state.selected().is_some() {
|
||||
i.submenu_title.to_owned()
|
||||
} else {
|
||||
" ".to_owned() + i.submenu_title
|
||||
|
@ -285,9 +636,11 @@ pub fn ui<B: Backend>(f: &mut Frame<B>, app: &mut App) -> String {
|
|||
.collect();
|
||||
|
||||
let list = List::new(items)
|
||||
.block(Block::default()
|
||||
.block(
|
||||
Block::default()
|
||||
.title(if list_section == 0 { "Options" } else { "" })
|
||||
.style(Style::default().fg(Color::LightRed)))
|
||||
.style(Style::default().fg(Color::LightRed)),
|
||||
)
|
||||
.highlight_style(
|
||||
Style::default()
|
||||
.fg(Color::Green)
|
||||
|
@ -305,22 +658,25 @@ pub fn ui<B: Backend>(f: &mut Frame<B>, app: &mut App) -> String {
|
|||
|
||||
// TODO: Add Save Defaults
|
||||
let help_paragraph = Paragraph::new(
|
||||
item_help.unwrap_or("").replace('\"', "") +
|
||||
"\nA: Enter sub-menu | B: Exit menu | ZL/ZR: Next tab"
|
||||
).style(Style::default().fg(Color::Cyan));
|
||||
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<ListItem> = sub_menu_str.iter().map(|s| {
|
||||
ListItem::new(
|
||||
vec![
|
||||
Spans::from((if s.0 { "X " } else { " " }).to_owned() + s.1)
|
||||
]
|
||||
)
|
||||
}).collect();
|
||||
let values_items: Vec<ListItem> = 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 { "" }))
|
||||
|
@ -333,35 +689,120 @@ pub fn ui<B: Backend>(f: &mut Frame<B>, app: &mut App) -> String {
|
|||
.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));
|
||||
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 mut settings = HashMap::new();
|
||||
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 for toggles
|
||||
// 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 {
|
||||
let val : usize = sub_menu.toggles.iter()
|
||||
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, val);
|
||||
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<u32> = 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
|
||||
// if (document.getElementById("saveDefaults").checked) {
|
||||
// url += "save_defaults=1";
|
||||
// } else {
|
||||
// url = url.slice(0, -1);
|
||||
// }
|
||||
}
|
|
@ -21,7 +21,6 @@ impl<T: Clone> MultiStatefulList<T> {
|
|||
(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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -54,6 +54,8 @@ fn ensure_menu_retains_selections() -> Result<(), Box<dyn Error>> {
|
|||
}
|
||||
|
||||
fn main() -> Result<(), Box<dyn Error>> {
|
||||
let args: Vec<String> = std::env::args().collect();
|
||||
let inputs = args.get(1);
|
||||
let menu;
|
||||
unsafe {
|
||||
menu = get_menu();
|
||||
|
@ -61,6 +63,21 @@ fn main() -> Result<(), Box<dyn Error>> {
|
|||
|
||||
#[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))?;
|
||||
|
||||
|
|
Loading…
Reference in a new issue