Add hold and special inputs to combos
All checks were successful
Deploy Sokko G / deploy (push) Successful in 6s

This commit is contained in:
Shinuwa 2026-08-03 12:53:34 +02:00
parent 9b72f4934c
commit 1a35dcae7b
15 changed files with 575 additions and 26 deletions

View file

@ -348,6 +348,8 @@
"simultaneousModeTitle": "Ajouter dans l'étape active",
"successiveModeLabel": "A B",
"simultaneousModeLabel": "A + B",
"holdLabel": "Maintien (s)",
"holdToggleLabel": "Hold",
"removeStepTitle": "Supprimer l'étape",
"removeLastInputTitle": "Retirer la dernière touche",
"addComboButton": "Ajouter le combo",

View file

@ -0,0 +1,2 @@
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
<svg width="800px" height="800px" viewBox="0 0 24 24" id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg"><defs><style>.cls-1{fill:none;stroke:#020202;stroke-miterlimit:10;stroke-width:1.91px;}</style></defs><path class="cls-1" d="M6.17,10.86h0A8.16,8.16,0,0,1,14.33,19h0"/><rect class="cls-1" x="17.73" y="2.48" width="4.77" height="7.64" rx="2.39"/><rect class="cls-1" x="12.95" y="1.52" width="4.77" height="8.59" rx="2.39"/><path class="cls-1" d="M11.56,9.89A2.37,2.37,0,0,0,13,7.73V3.91a2.39,2.39,0,1,0-4.77,0V6.19"/><path class="cls-1" d="M3.41,7.79V3.91a2.39,2.39,0,1,1,4.77,0V6.19"/><path class="cls-1" d="M6.27,23.48V21.57l-.75-.64c-2.57-2.58-4-5.12-4-8.77h0A6,6,0,0,1,3.25,7.94h0A6,6,0,0,1,7.47,6.19h2.2A2.33,2.33,0,0,1,12,8.52h0a2.33,2.33,0,0,1-2.33,2.34H6.17"/><path class="cls-1" d="M17.73,23.48V21.57l.75-.64c2.57-2.58,4-5.12,4-8.77h0V7.36"/></svg>

After

Width:  |  Height:  |  Size: 997 B

View file

@ -0,0 +1,36 @@
<?xml version="1.0" encoding="iso-8859-1"?>
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
<svg fill="#000000" height="800px" width="800px" version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
viewBox="0 0 511.997 511.997" xml:space="preserve">
<g>
<g>
<path d="M317.276,116.749c-60.041-37.086-139.059-18.406-176.145,41.643L13.344,365.291
c-25.958,42.035-12.885,97.348,29.15,123.307c42.035,25.958,97.348,12.885,123.307-29.15l33.63-54.443
c15.693-25.412,39.62-34.799,67.319-45.67c31.642-12.425,67.507-26.496,92.169-66.424
C395.996,232.854,377.317,153.835,317.276,116.749z M337.133,279.446c-40.354,65.34-119.142,46.763-159.488,112.102
c-13.449,21.777-17.357,28.109-33.63,54.443c-18.569,30.071-58.001,39.39-88.073,20.821
c-30.071-18.569-39.39-58.001-20.821-88.073C46.087,360.982,149.349,193.788,162.9,171.841
c29.713-48.111,92.8-63.027,140.919-33.314C351.939,168.24,366.846,231.335,337.133,279.446z"/>
</g>
</g>
<g>
<g>
<path d="M484.683,132.895c-8.841-5.461-18.594-7.979-28.305-8.294c3.703-17.92-3.703-37.052-20.156-47.206
c-9.233-5.7-19.729-7.441-29.722-6.025c-1.434-11.076-7.492-21.47-17.715-27.785c-10.214-6.306-22.212-7.074-32.759-3.405
c-3.029-6.784-8.047-12.766-14.839-16.964c-13.577-8.38-30.37-7.253-42.59,1.519c-2.697-3.43-5.922-6.536-9.847-8.96
c-18.014-11.128-41.719-5.521-52.847,12.493c-11.128,18.014-5.521,41.719,12.493,52.847c13.577,8.38,30.37,7.245,42.59-1.519
c2.705,3.43,5.922,6.536,9.847,8.96c10.214,6.306,22.212,7.074,32.76,3.405c3.029,6.784,8.047,12.766,14.839,16.964
c6.135,3.789,12.928,5.495,19.661,5.572c-0.324,15.352,7.066,30.49,21.052,39.125c4.062,2.509,8.397,4.07,12.8,5.154
c-11.392,25.865-2.5,56.858,22.212,72.124c27.025,16.691,62.575,8.286,79.266-18.739
C520.105,185.144,511.7,149.586,484.683,132.895z M279.465,55.164c-3.712,6.007-11.597,7.885-17.613,4.164
c-6.016-3.712-7.876-11.605-4.164-17.613c3.712-6.016,11.605-7.876,17.613-4.164C281.317,41.264,283.177,49.157,279.465,55.164z
M331.902,62.605c-3.712,6.016-11.597,7.885-17.613,4.164c-6.016-3.712-7.876-11.605-4.164-17.613
c3.712-6.016,11.605-7.876,17.613-4.164C333.754,48.705,335.614,56.598,331.902,62.605z M379.501,82.975
c-3.712,6.016-11.605,7.885-17.613,4.164c-6.016-3.712-7.876-11.605-4.164-17.613c3.712-6.016,11.605-7.876,17.613-4.164
C381.344,69.074,383.213,76.967,379.501,82.975z M402.601,131.836c-9.02-5.564-11.819-17.399-6.246-26.419
c5.572-9.02,17.408-11.819,26.419-6.246c9.02,5.572,11.819,17.399,6.246,26.419C423.448,134.61,411.621,137.409,402.601,131.836z
M481.645,198.712c-9.284,15.036-29.005,19.695-44.041,10.411c-15.027-9.284-19.695-29.005-10.411-44.041
s29.005-19.695,44.041-10.411C486.27,163.956,490.929,183.676,481.645,198.712z"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.8 KiB

View file

@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<path fill="#000" d="M7.4 3.1c.9 0 1.6.7 1.6 1.6v5.1h.6V3.6c0-.9.7-1.6 1.6-1.6s1.6.7 1.6 1.6v6.2h.6V4.2c0-.9.7-1.6 1.6-1.6s1.6.7 1.6 1.6v6h.6V6.1c0-.9.7-1.6 1.6-1.6s1.6.7 1.6 1.6v7.2c0 5.2-2.7 8.7-7.1 8.7h-2.6c-2.5 0-4.4-1.2-5.7-3.3L2.9 15c-.6-.8-.4-1.8.4-2.4.8-.6 1.8-.4 2.4.3l1.1 1.3V4.7c0-.9.7-1.6 1.6-1.6z"/>
</svg>

After

Width:  |  Height:  |  Size: 383 B

View file

@ -0,0 +1,57 @@
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
<svg version="1.1" id="designs" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
width="800px" height="800px" viewBox="0 0 32 32" xml:space="preserve">
<style type="text/css">
.sketchy_een{fill:#111918;}
</style>
<path class="sketchy_een" d="M29.919,11.654c0.009-0.303,0.028-0.605,0.033-0.908c0.004-0.308-0.007-0.62-0.009-0.928
c-0.007-0.655,0.019-1.309,0.056-1.964c0.022-0.427-0.375-0.783-0.783-0.783c-0.034,0-0.063,0.015-0.096,0.019
c-0.152-0.157-0.362-0.257-0.594-0.26c-0.376-0.004-0.751-0.011-1.127-0.016c0.004-0.165,0.007-0.331,0.01-0.497
c0.005-0.276-0.153-0.51-0.371-0.652c-0.154-0.182-0.375-0.305-0.63-0.311c-0.871-0.018-1.741-0.018-2.612-0.043
c-0.819-0.024-1.637-0.058-2.458-0.072c-1.662-0.028-3.323-0.056-4.987-0.063c-0.728-0.002-1.454,0-2.181,0
c-0.895,0.002-1.79,0.004-2.685-0.004C10.689,5.166,9.89,5.123,9.092,5.11c-0.273-0.004-0.547-0.005-0.82-0.005
c-0.526,0-1.052,0.005-1.579,0.009C6.129,5.12,5.563,5.121,4.998,5.125C4.755,5.126,4.545,5.248,4.401,5.426
C4.165,5.51,3.967,5.696,3.908,5.95C3.741,6.676,3.559,7.4,3.418,8.132c-0.136,0.706-0.301,1.402-0.472,2.1
c-0.173,0.706-0.29,1.426-0.473,2.13c-0.162,0.616-0.317,1.231-0.453,1.851c-0.058,0.264,0.015,0.524,0.175,0.716
c0.041,0.42,0.384,0.723,0.803,0.742c0.269,0.013,0.539,0.01,0.809,0.014c-0.154,0.853-0.305,1.706-0.478,2.554
c-0.163,0.798-0.316,1.6-0.447,2.404c-0.132,0.798-0.286,1.593-0.425,2.39c-0.137,0.795-0.265,1.591-0.382,2.387
c-0.02,0.132-0.025,0.253-0.009,0.372c-0.01,0.056-0.033,0.109-0.033,0.166c0,0.483,0.427,0.973,0.936,0.936
c0.769-0.056,1.537-0.115,2.308-0.15c0.653-0.028,1.311-0.033,1.964-0.03c0.681,0.002,1.361,0.002,2.042,0.022
c0.654,0.018,1.303,0.08,1.957,0.044c0.369-0.021,0.674-0.265,0.759-0.614c0.084-0.095,0.151-0.206,0.171-0.322
c0.132-0.776,0.342-1.537,0.509-2.306c0.163-0.756,0.254-1.524,0.405-2.284c0.041-0.208,0.085-0.416,0.127-0.624
c0.616-0.059,1.226-0.116,1.83-0.263c0.291-0.072,0.568-0.201,0.837-0.33c0.252-0.121,0.498-0.252,0.737-0.397
c0.152-0.093,0.292-0.21,0.427-0.321c0.13-0.108,0.225-0.252,0.327-0.384c0.147-0.191,0.236-0.433,0.336-0.65
c0.271-0.594,0.516-1.203,0.748-1.814c0.026-0.07,0.02-0.15,0.018-0.229c0.709-0.005,1.418-0.012,2.125-0.031
c0.819-0.022,1.637-0.054,2.456-0.054c0.904,0,1.808,0.017,2.714,0.02c0.258,0.001,0.484-0.128,0.639-0.317
c0.024,0.003,0.046,0.015,0.071,0.015c0.21,0,0.414-0.084,0.564-0.232c0.158-0.158,0.217-0.347,0.232-0.564
c0.05-0.667,0.049-1.338,0.08-2.006c0.598,0.023,1.19,0.073,1.784,0.128c0.21,0.021,0.447-0.1,0.59-0.245
c0.156-0.156,0.245-0.37,0.245-0.59c0-0.145-0.05-0.281-0.121-0.405C29.891,11.865,29.916,11.762,29.919,11.654z M16.363,17.328
c-0.117,0.274-0.236,0.555-0.4,0.802c-0.042,0.045-0.087,0.086-0.134,0.127c-0.269,0.184-0.561,0.328-0.859,0.46
c-0.316,0.113-0.642,0.186-0.972,0.244c-0.15,0.016-0.301,0.01-0.452,0.013c0.016-0.077,0.031-0.154,0.047-0.23
c0.175-0.825,0.373-1.646,0.565-2.469c0.866-0.012,1.733-0.02,2.599-0.023C16.628,16.612,16.506,16.974,16.363,17.328z
M20.663,14.53c-1.652,0.028-3.305,0.074-4.957,0.083c-0.78,0.004-1.56-0.004-2.341-0.013c-0.459-0.005-0.841,0.388-0.841,0.843
c0,0.095,0.025,0.186,0.057,0.273c-0.381,1.672-0.717,3.353-1.067,5.033c-0.16,0.769-0.28,1.547-0.446,2.313
c-0.152,0.691-0.336,1.377-0.476,2.07c-1.091-0.101-2.181-0.184-3.278-0.208c-0.35-0.007-0.7-0.011-1.05-0.011
c-0.795,0-1.589,0.025-2.383,0.063c0.011-0.082,0.024-0.164,0.036-0.245c0.061-0.405,0.13-0.81,0.199-1.212
c0.136-0.784,0.278-1.565,0.394-2.354c0.117-0.811,0.24-1.625,0.401-2.428c0.178-0.889,0.36-1.777,0.525-2.67
c0.025-0.134,0.023-0.261,0.001-0.385c0.219-0.004,0.439-0.007,0.658-0.01c0.436-0.008,0.873-0.017,1.309-0.021
c0.388-0.003,0.782-0.051,1.17-0.026c0.21,0.037,0.407,0.025,0.599-0.087c0.182-0.108,0.317-0.284,0.373-0.487
c0.058-0.21,0.028-0.44-0.082-0.628c-0.152-0.258-0.412-0.386-0.702-0.403c-0.124-0.008-0.249-0.01-0.374-0.01
c-0.284,0-0.57,0.015-0.85,0.025c-0.176,0.005-0.351,0.026-0.525,0.037c-0.267,0.017-0.531,0.02-0.798,0.026
c-0.507,0.013-1.016,0.017-1.522,0.008c-0.341-0.006-0.679-0.028-1.018-0.046c0.093-0.417,0.186-0.834,0.283-1.251
c0.165-0.722,0.282-1.456,0.451-2.176c0.16-0.68,0.31-1.361,0.455-2.044C4.999,7.951,5.177,7.32,5.34,6.686
c1.464,0.018,2.928,0.019,4.393,0.045c0.789,0.013,1.576,0.045,2.365,0.043c0.834-0.002,1.667,0,2.501,0
c3.273-0.004,6.546,0.05,9.818,0.178c0.469,0.018,0.938,0.031,1.407,0.045c0.004,0.578,0.011,1.156,0.013,1.736
c0.004,0.737-0.02,1.474-0.032,2.211c-0.002,0.121-0.011,0.241-0.015,0.362c-0.272-0.001-0.544-0.003-0.816-0.003
c-0.36,0.002-0.722,0.004-1.084-0.002c-0.728-0.009-1.457-0.032-2.185-0.032c-0.778,0-1.558,0.013-2.336,0.017
c-0.743,0.006-1.485-0.009-2.226-0.017c-1.539-0.011-3.076-0.015-4.615-0.002c-0.743,0.008-1.487,0.008-2.232,0
c-0.694-0.005-1.387-0.031-2.081-0.035c-0.403-0.004-0.806-0.007-1.21-0.009c-0.001,0-0.002,0-0.003,0
c-0.409,0-0.749,0.345-0.749,0.754c0,0.401,0.342,0.765,0.752,0.752c0.631-0.018,1.264-0.009,1.894,0.002
c0.711,0.011,1.42,0.03,2.129,0.041c1.513,0.024,3.028,0.03,4.541,0.035c0.789,0.002,1.576,0.021,2.365,0.039
c0.744,0.019,1.489,0.021,2.233,0.021c0.759,0,1.521-0.009,2.28-0.013c0.739-0.004,1.482-0.015,2.222,0.004
c0.217,0.005,0.433,0.015,0.65,0.023c0.116,0.084,0.25,0.142,0.401,0.155c-0.025,0.485-0.03,0.97-0.036,1.455
C24.011,14.495,22.335,14.5,20.663,14.53z M27.384,11.476c0.001-0.178,0.01-0.355,0.011-0.533c0-0.737-0.035-1.474-0.032-2.211
c0.001-0.071,0.003-0.142,0.004-0.213c0.353-0.007,0.707-0.015,1.06-0.026c0,0.432,0.004,0.864-0.004,1.295
c-0.004,0.31-0.002,0.618-0.015,0.928c-0.011,0.262-0.027,0.524-0.036,0.787c-0.287-0.01-0.574-0.029-0.862-0.029
C27.468,11.474,27.426,11.476,27.384,11.476z"/>
</svg>

After

Width:  |  Height:  |  Size: 5.7 KiB

View file

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
<svg width="800px" height="800px" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M11.302 21.6149C11.5234 21.744 11.6341 21.8086 11.7903 21.8421C11.9116 21.8681 12.0884 21.8681 12.2097 21.8421C12.3659 21.8086 12.4766 21.744 12.698 21.6149C14.646 20.4784 20 16.9084 20 12V6.6C20 6.04207 20 5.7631 19.8926 5.55048C19.7974 5.36198 19.6487 5.21152 19.4613 5.11409C19.25 5.00419 18.9663 5.00084 18.3988 4.99413C15.4272 4.95899 13.7136 4.71361 12 3C10.2864 4.71361 8.57279 4.95899 5.6012 4.99413C5.03373 5.00084 4.74999 5.00419 4.53865 5.11409C4.35129 5.21152 4.20259 5.36198 4.10739 5.55048C4 5.7631 4 6.04207 4 6.6V12C4 16.9084 9.35396 20.4784 11.302 21.6149Z" stroke="#000000" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 897 B

View file

@ -16,26 +16,32 @@ const DEVICE_OPTIONS = [
{ value: "keyboardMouse", label: "Clavier/Souris" }
];
const SPECIAL_INPUT_GROUP = { title: "Spéciale", column: "actions", inputs: [["button", "fist"], ["button", "foot"], ["button", "grab"], ["button", "protect"], ["button", "shoot"]] };
const DEVICE_GROUPS = {
playstation: [
{ title: "Directions", kind: "directions", column: "movement", inputs: [["direction", "up-left"], ["direction", "up"], ["direction", "up-right"], ["direction", "left"], null, ["direction", "right"], ["direction", "down-left"], ["direction", "down"], ["direction", "down-right"]] },
{ title: "Actions", column: "actions", inputs: [["button", "triangle"], ["button", "circle"], ["button", "cross"], ["button", "square"], ["button", "l1"], ["button", "r1"], ["button", "l2"], ["button", "r2"]] },
SPECIAL_INPUT_GROUP,
{ title: "Système", inputs: [["button", "options"], ["button", "share"], ["button", "l3"], ["button", "r3"]] }
],
xbox: [
{ title: "Directions", kind: "directions", column: "movement", inputs: [["direction", "up-left"], ["direction", "up"], ["direction", "up-right"], ["direction", "left"], null, ["direction", "right"], ["direction", "down-left"], ["direction", "down"], ["direction", "down-right"]] },
{ title: "Actions", column: "actions", inputs: [["button", "a"], ["button", "b"], ["button", "x"], ["button", "y"], ["button", "lb"], ["button", "rb"], ["button", "lt"], ["button", "rt"]] },
SPECIAL_INPUT_GROUP,
{ title: "Système", inputs: [["button", "menu"], ["button", "view"], ["button", "ls"], ["button", "rs"]] }
],
switch: [
{ title: "Directions", kind: "directions", column: "movement", inputs: [["direction", "up-left"], ["direction", "up"], ["direction", "up-right"], ["direction", "left"], null, ["direction", "right"], ["direction", "down-left"], ["direction", "down"], ["direction", "down-right"]] },
{ title: "Actions", column: "actions", inputs: [["button", "a"], ["button", "b"], ["button", "x"], ["button", "y"], ["button", "l"], ["button", "r"], ["button", "zl"], ["button", "zr"]] },
SPECIAL_INPUT_GROUP,
{ title: "Système", inputs: [["button", "plus"], ["button", "minus"], ["button", "ls"], ["button", "rs"]] }
],
n64: [
{ title: "Stick", kind: "directions", column: "movement", inputs: [["direction", "stick-up-left"], ["direction", "stick-up"], ["direction", "stick-up-right"], ["direction", "stick-left"], null, ["direction", "stick-right"], ["direction", "stick-down-left"], ["direction", "stick-down"], ["direction", "stick-down-right"]] },
{ title: "D-Pad", kind: "directions", column: "movement", inputs: [["direction", "up-left"], ["direction", "up"], ["direction", "up-right"], ["direction", "left"], null, ["direction", "right"], ["direction", "down-left"], ["direction", "down"], ["direction", "down-right"]] },
{ title: "Actions", column: "actions", inputs: [["button", "a"], ["button", "b"], ["button", "z"], ["button", "start"], ["button", "l"], ["button", "r"]] },
SPECIAL_INPUT_GROUP,
{ title: "C-buttons", kind: "directions", inputs: [["button", "c-up"], ["button", "c-down"], ["button", "c-left"], ["button", "c-right"]] }
]
};
@ -45,12 +51,14 @@ const KEYBOARD_GROUPS = {
{ title: "ZQSD", kind: "directions", column: "movement", inputs: [null, ["key", "z"], null, ["key", "q"], ["key", "s"], ["key", "d"], null, null, null] },
{ title: "Flèches", kind: "directions", column: "movement", inputs: [null, ["direction", "up"], null, ["direction", "left"], ["direction", "down"], ["direction", "right"], null, null, null] },
{ title: "Touches", column: "actions", inputs: [["key", "ctrl"], ["key", "alt"], ["key", "shift"], ["key", "space"], ["key", "enter"], ["key", "tab"], ["key", "e"], ["key", "a"], ["key", "r"], ["key", "f"]] },
SPECIAL_INPUT_GROUP,
{ title: "Souris", inputs: [["mouse", "left"], ["mouse", "right"], ["mouse", "middle"], ["mouse", "wheel-up"], ["mouse", "wheel-down"]] }
],
qwerty: [
{ title: "WASD", kind: "directions", column: "movement", inputs: [null, ["key", "w"], null, ["key", "a"], ["key", "s"], ["key", "d"], null, null, null] },
{ title: "Flèches", kind: "directions", column: "movement", inputs: [null, ["direction", "up"], null, ["direction", "left"], ["direction", "down"], ["direction", "right"], null, null, null] },
{ title: "Touches", column: "actions", inputs: [["key", "ctrl"], ["key", "alt"], ["key", "shift"], ["key", "space"], ["key", "enter"], ["key", "tab"], ["key", "e"], ["key", "q"], ["key", "r"], ["key", "f"]] },
SPECIAL_INPUT_GROUP,
{ title: "Souris", inputs: [["mouse", "left"], ["mouse", "right"], ["mouse", "middle"], ["mouse", "wheel-up"], ["mouse", "wheel-down"]] }
]
};
@ -84,9 +92,22 @@ const INPUT_LABELS = {
"c-down": "C↓",
"c-left": "C←",
"c-right": "C→",
fist: "Poing",
foot: "Pied",
grab: "Grab",
protect: "Protect",
shoot: "Shoot",
space: "Espace"
};
const INPUT_ICONS = {
fist: "fist",
foot: "foot",
grab: "grab",
protect: "shield",
shoot: "gun"
};
const MOUSE_LABELS = {
left: "Clic G",
right: "Clic D",
@ -103,11 +124,33 @@ function getInputLabel(input) {
}
function inputKey(input) {
return `${input.kind}:${input.value}`;
return `${input.kind}:${input.value}:${input.hold ? "hold" : ""}:${input.holdMs || 0}`;
}
function createInput(kind, value) {
return { kind, value };
function createInput(kind, value, holdMs = 0) {
const input = { kind, value };
if (holdMs > 0) input.holdMs = holdMs;
return input;
}
function getHoldSeconds(input) {
return Math.round((Number(input?.holdMs) || 0) / 1000);
}
function formatHoldLabel(input) {
const seconds = getHoldSeconds(input);
if (!seconds) return input?.hold ? "hold" : "";
return `${seconds}s`;
}
function parseHoldSeconds(value) {
const seconds = Number.parseInt(String(value || "").replace(/[^\d]/g, ""), 10);
if (!Number.isInteger(seconds) || seconds <= 0) return 0;
return Math.min(99, seconds);
}
function isDirectionInput(input) {
return input?.kind === "direction";
}
function createCombo(context, id, name, category, inputs, fallbackName, device) {
@ -153,9 +196,13 @@ function setComboItemCategory(combo, category) {
function ComboInputToken({ input, device, palette = false, onClick, onDragStart }) {
const label = getInputLabel(input);
const iconName = INPUT_ICONS[input.value];
const holdLabel = !palette ? formatHoldLabel(input) : "";
const classNames = [
"combo-input-token",
`combo-input-${input.kind}`,
iconName ? "combo-input-icon" : "",
holdLabel ? "has-hold" : "",
input.kind === "key" && label.length === 1 ? "combo-input-short" : "",
`combo-device-${device}`,
`combo-value-${String(input.value || "").replace(/[^a-z0-9]+/gi, "-").toLowerCase()}`
@ -164,19 +211,20 @@ function ComboInputToken({ input, device, palette = false, onClick, onDragStart
if (palette) {
return (
<button className={classNames} type="button" draggable onClick={onClick} onDragStart={onDragStart} title={label} aria-label={label}>
<span>{label}</span>
<span>{iconName ? <Icon name={iconName} /> : label}</span>
</button>
);
}
return (
<span className={classNames} draggable={Boolean(onDragStart)} onDragStart={onDragStart} title={label}>
<span>{label}</span>
<span className={classNames} draggable={Boolean(onDragStart)} onClick={onClick} onDragStart={onDragStart} title={holdLabel ? `${label} · ${holdLabel}` : label}>
<span>{iconName ? <Icon name={iconName} /> : label}</span>
{holdLabel && <em className="combo-input-hold">{holdLabel}</em>}
</span>
);
}
function ComboStep({ step, stepIndex, device, active = false, onClick, onDropInput, onDragInput }) {
function ComboStep({ step, stepIndex, device, active = false, selectedInputIndex = -1, onClick, onSelectInput, onDropInput, onDragInput }) {
const className = [
"combo-step-inputs",
step.length > 1 ? "is-simultaneous" : "",
@ -184,9 +232,17 @@ function ComboStep({ step, stepIndex, device, active = false, onClick, onDropInp
active ? "is-active" : ""
].filter(Boolean).join(" ");
const content = step.length ? step.map((input, inputIndex) => (
<span className="combo-step-input" key={`${inputKey(input)}-${inputIndex}`}>
<span className={`combo-step-input ${selectedInputIndex === inputIndex ? "is-selected" : ""}`} key={`${inputKey(input)}-${inputIndex}`}>
{inputIndex > 0 && <span className="combo-plus" aria-hidden="true">+</span>}
<ComboInputToken input={input} device={device} onDragStart={onDragInput ? (event) => onDragInput(event, stepIndex, inputIndex, input) : null} />
<ComboInputToken
input={input}
device={device}
onClick={onSelectInput ? (event) => {
event.stopPropagation();
onSelectInput(stepIndex, inputIndex);
} : null}
onDragStart={onDragInput ? (event) => onDragInput(event, stepIndex, inputIndex, input) : null}
/>
</span>
)) : <span className="combo-empty-step">Étape vide</span>;
@ -214,7 +270,7 @@ function ComboStep({ step, stepIndex, device, active = false, onClick, onDropInp
return <span className={className}>{content}</span>;
}
function ComboSequence({ inputs, device, emptyLabel, activeStepIndex = -1, onSelectStep, onDropInput, onDropNewStep, onDragInput }) {
function ComboSequence({ inputs, device, emptyLabel, activeStepIndex = -1, selectedInput, onSelectStep, onSelectInput, onDropInput, onDropNewStep, onDragInput }) {
const visibleInputs = inputs.map((step, index) => ({ step, index })).filter(({ step }) => step.length || onSelectStep);
if (!visibleInputs.length) return <span className="combo-empty-sequence">{emptyLabel}</span>;
return (
@ -230,12 +286,26 @@ function ComboSequence({ inputs, device, emptyLabel, activeStepIndex = -1, onSel
onDropNewStep(event);
}}
>
{visibleInputs.map(({ step, index: stepIndex }, visibleIndex) => (
{visibleInputs.map(({ step, index: stepIndex }, visibleIndex) => {
const nextStep = visibleInputs[visibleIndex + 1]?.step || [];
const hideSeparator = step.every(isDirectionInput) && nextStep.every(isDirectionInput);
return (
<span className="combo-step" key={`${stepIndex}-${step.map(inputKey).join("+")}`}>
<ComboStep step={step} stepIndex={stepIndex} device={device} active={activeStepIndex === stepIndex} onClick={onSelectStep ? () => onSelectStep(stepIndex) : null} onDropInput={onDropInput ? (event) => onDropInput(event, stepIndex) : null} onDragInput={onDragInput} />
{visibleIndex < visibleInputs.length - 1 && <span className="combo-step-separator" aria-hidden="true"></span>}
<ComboStep
step={step}
stepIndex={stepIndex}
device={device}
active={activeStepIndex === stepIndex}
selectedInputIndex={selectedInput?.stepIndex === stepIndex ? selectedInput.inputIndex : -1}
onClick={onSelectStep ? () => onSelectStep(stepIndex) : null}
onSelectInput={onSelectInput}
onDropInput={onDropInput ? (event) => onDropInput(event, stepIndex) : null}
onDragInput={onDragInput}
/>
{visibleIndex < visibleInputs.length - 1 && !hideSeparator && <span className="combo-step-separator" aria-hidden="true"></span>}
</span>
))}
);
})}
</div>
);
}
@ -265,6 +335,7 @@ export function CombosModule({ toolboxId, moduleId, context, editing }) {
const [comboName, setComboName] = useState("");
const [draftInputs, setDraftInputs] = useState([[]]);
const [activeStepIndex, setActiveStepIndex] = useState(0);
const [selectedInput, setSelectedInput] = useState(null);
const [simultaneousMode, setSimultaneousMode] = useState(false);
const [keyboardLayout, setKeyboardLayout] = useState("azerty");
const [customKey, setCustomKey] = useState("");
@ -272,6 +343,9 @@ export function CombosModule({ toolboxId, moduleId, context, editing }) {
const paletteGroups = useMemo(() => getPaletteGroups(data.device, keyboardLayout), [data.device, keyboardLayout]);
const paletteColumns = useMemo(() => getPaletteColumns(paletteGroups), [paletteGroups]);
const cleanDraftInputs = draftInputs.map((step) => step.filter(Boolean)).filter((step) => step.length);
const selectedDraftInput = Number.isInteger(selectedInput?.stepIndex) && Number.isInteger(selectedInput?.inputIndex)
? draftInputs[selectedInput.stepIndex]?.[selectedInput.inputIndex] || null
: null;
const entries = getGroupedEntries(data.combos, {
groupOrder: data.categoryOrder,
getItemGroup: getComboCategory,
@ -344,6 +418,7 @@ export function CombosModule({ toolboxId, moduleId, context, editing }) {
const nextSteps = steps.length ? steps.map((step) => [...step]) : [[]];
const targetIndex = Math.min(Math.max(0, stepIndex), nextSteps.length - 1);
nextSteps[targetIndex] = [...nextSteps[targetIndex], input];
setSelectedInput({ stepIndex: targetIndex, inputIndex: nextSteps[targetIndex].length - 1 });
return nextSteps;
});
}
@ -359,6 +434,7 @@ export function CombosModule({ toolboxId, moduleId, context, editing }) {
const filledSteps = steps.filter((step) => step.length);
const nextSteps = [...filledSteps, [input]];
setActiveStepIndex(nextSteps.length - 1);
setSelectedInput({ stepIndex: nextSteps.length - 1, inputIndex: 0 });
return nextSteps;
});
}
@ -417,6 +493,7 @@ export function CombosModule({ toolboxId, moduleId, context, editing }) {
const [input] = nextSteps[sourceStepIndex]?.splice(sourceInputIndex, 1) || [];
if (!input || !nextSteps[targetStepIndex]) return steps;
nextSteps[targetStepIndex].push(input);
setSelectedInput({ stepIndex: targetStepIndex, inputIndex: nextSteps[targetStepIndex].length - 1 });
return nextSteps.filter((step, index) => step.length || index === targetStepIndex);
});
}
@ -428,6 +505,7 @@ export function CombosModule({ toolboxId, moduleId, context, editing }) {
if (!input) return steps;
const safeSteps = nextSteps.filter((step) => step.length);
setActiveStepIndex(safeSteps.length);
setSelectedInput({ stepIndex: safeSteps.length, inputIndex: 0 });
return [...safeSteps, [input]];
});
}
@ -437,6 +515,7 @@ export function CombosModule({ toolboxId, moduleId, context, editing }) {
const nextSteps = steps.filter((_, index) => index !== activeStepIndex);
const safeSteps = nextSteps.length ? nextSteps : [[]];
setActiveStepIndex(Math.min(activeStepIndex, safeSteps.length - 1));
setSelectedInput(null);
return safeSteps;
});
}
@ -455,10 +534,46 @@ export function CombosModule({ toolboxId, moduleId, context, editing }) {
nextSteps[targetIndex].pop();
const safeSteps = nextSteps.filter((step) => step.length);
setActiveStepIndex(Math.max(0, safeSteps.length - 1));
setSelectedInput(null);
return safeSteps.length ? safeSteps : [[]];
});
}
function updateSelectedInputHold(value) {
if (!selectedDraftInput) return;
const holdMs = Math.round(parseHoldSeconds(value) * 1000);
setDraftInputs((steps) => steps.map((step, stepIndex) => step.map((input, inputIndex) => {
if (stepIndex !== selectedInput.stepIndex || inputIndex !== selectedInput.inputIndex) return input;
const nextInput = { ...input };
if (holdMs > 0) {
nextInput.hold = true;
nextInput.holdMs = holdMs;
}
else delete nextInput.holdMs;
return nextInput;
})));
}
function toggleSelectedInputHold() {
if (!selectedDraftInput) return;
setDraftInputs((steps) => steps.map((step, stepIndex) => step.map((input, inputIndex) => {
if (stepIndex !== selectedInput.stepIndex || inputIndex !== selectedInput.inputIndex) return input;
const nextInput = { ...input };
if (nextInput.hold || nextInput.holdMs) {
delete nextInput.hold;
delete nextInput.holdMs;
} else {
nextInput.hold = true;
}
return nextInput;
})));
}
function selectDraftStep(stepIndex) {
setActiveStepIndex(stepIndex);
setSelectedInput(null);
}
function addCustomKey(event) {
event.preventDefault();
const value = normalizeCustomKey(customKey);
@ -485,6 +600,7 @@ export function CombosModule({ toolboxId, moduleId, context, editing }) {
setComboName(combo.name);
setDraftInputs(combo.inputs.length ? combo.inputs.map((step) => step.map((input) => ({ ...input }))) : [[]]);
setActiveStepIndex(0);
setSelectedInput(null);
}
function cancelComboEdit() {
@ -493,6 +609,7 @@ export function CombosModule({ toolboxId, moduleId, context, editing }) {
setComboCategory("");
setDraftInputs([[]]);
setActiveStepIndex(0);
setSelectedInput(null);
setSimultaneousMode(false);
}
@ -708,8 +825,47 @@ export function CombosModule({ toolboxId, moduleId, context, editing }) {
)}
<div className="combo-draft-panel">
<ComboSequence inputs={draftInputs} device={data.device} emptyLabel={textContent.emptyDraft || "Aucune touche sélectionnée."} activeStepIndex={activeStepIndex} onSelectStep={setActiveStepIndex} onDropInput={dropInputOnStep} onDropNewStep={dropInputAsNewStep} onDragInput={dragDraftInput} />
<ComboSequence
inputs={draftInputs}
device={data.device}
emptyLabel={textContent.emptyDraft || "Aucune touche sélectionnée."}
activeStepIndex={activeStepIndex}
selectedInput={selectedInput}
onSelectStep={selectDraftStep}
onSelectInput={(stepIndex, inputIndex) => {
setActiveStepIndex(stepIndex);
setSelectedInput({ stepIndex, inputIndex });
}}
onDropInput={dropInputOnStep}
onDropNewStep={dropInputAsNewStep}
onDragInput={dragDraftInput}
/>
<div className="combo-draft-actions">
<label className="combo-hold-field">
<span className="combo-hold-controls">
<button
type="button"
className={selectedDraftInput?.hold || selectedDraftInput?.holdMs ? "active" : ""}
onClick={toggleSelectedInputHold}
disabled={!selectedDraftInput}
aria-pressed={Boolean(selectedDraftInput?.hold || selectedDraftInput?.holdMs)}
>
{textContent.holdToggleLabel || "Hold"}
</button>
<input
type="number"
min="0"
max="99"
step="1"
inputMode="numeric"
value={selectedDraftInput ? getHoldSeconds(selectedDraftInput) || "" : ""}
onChange={(event) => updateSelectedInputHold(event.target.value)}
disabled={!selectedDraftInput}
placeholder="0"
aria-label={textContent.holdLabel || "Maintien en secondes"}
/>
</span>
</label>
<button
type="button"
className={`combos-simultaneous-toggle ${simultaneousMode ? "active" : ""}`}

View file

@ -362,7 +362,15 @@ export function normalizeCountersData(data) {
function normalizeComboInput(input) {
const kind = COMBO_INPUT_KINDS.has(input?.kind) ? input.kind : "";
const value = String(input?.value || "").trim().slice(0, 24);
return kind && value ? { kind, value } : null;
if (!kind || !value) return null;
const holdMs = Number(input?.holdMs);
const normalizedHoldMs = Number.isFinite(holdMs) && holdMs > 0
? Math.min(99000, Math.round(holdMs / 1000) * 1000)
: 0;
const normalized = { kind, value };
if (input?.hold === true || normalizedHoldMs > 0) normalized.hold = true;
if (normalizedHoldMs > 0) normalized.holdMs = normalizedHoldMs;
return normalized;
}
function normalizeComboSteps(inputs) {

View file

@ -252,6 +252,31 @@
-webkit-mask-image: url("/static/icons/back.svg");
}
.ui-icon-fist {
mask-image: url("/static/icons/fist.svg");
-webkit-mask-image: url("/static/icons/fist.svg");
}
.ui-icon-foot {
mask-image: url("/static/icons/foot.svg");
-webkit-mask-image: url("/static/icons/foot.svg");
}
.ui-icon-grab {
mask-image: url("/static/icons/grab.svg");
-webkit-mask-image: url("/static/icons/grab.svg");
}
.ui-icon-shield {
mask-image: url("/static/icons/shield.svg");
-webkit-mask-image: url("/static/icons/shield.svg");
}
.ui-icon-gun {
mask-image: url("/static/icons/gun.svg");
-webkit-mask-image: url("/static/icons/gun.svg");
}
.ui-icon-trash {
mask-image: url("/static/icons/trashcan.svg");
-webkit-mask-image: url("/static/icons/trashcan.svg");

View file

@ -2732,6 +2732,20 @@ textarea:focus {
min-width: 0;
}
.combo-step-input.is-selected .combo-input-token {
box-shadow:
var(--combo-token-shadow),
0 0 0 2px rgba(246, 196, 83, 0.22);
}
.combo-step-input.is-selected .combo-input-token.has-hold {
box-shadow:
var(--combo-token-shadow),
0 0 0 2px rgba(246, 196, 83, 0.22),
7px 0 0 -1px var(--combo-hold-shadow),
9px 0 6px -3px var(--combo-hold-shadow);
}
.combo-step-inputs {
min-height: 42px;
padding: 3px;
@ -2775,10 +2789,18 @@ button.combo-step-inputs {
.combo-step-inputs.is-single.is-active .combo-input-token {
box-shadow:
inset 0 -2px 0 rgba(0, 0, 0, 0.2),
var(--combo-token-shadow),
0 0 0 2px rgba(139, 92, 246, 0.18);
}
.combo-step-inputs.is-single.is-active .combo-input-token.has-hold {
box-shadow:
var(--combo-token-shadow),
0 0 0 2px rgba(139, 92, 246, 0.18),
7px 0 0 -1px var(--combo-hold-shadow),
9px 0 6px -3px var(--combo-hold-shadow);
}
.combo-step-separator,
.combo-plus {
color: var(--color-text-muted);
@ -2807,6 +2829,8 @@ button.combo-step-inputs {
--combo-token-bg: rgba(21, 26, 48, 0.82);
--combo-token-border: rgba(165, 180, 252, 0.22);
--combo-token-color: var(--color-text-primary);
--combo-token-shadow: inset 0 -2px 0 rgba(0, 0, 0, 0.2);
--combo-hold-shadow: rgba(125, 135, 159, 0.34);
display: inline-flex;
box-sizing: border-box;
width: 34px;
@ -2825,7 +2849,7 @@ button.combo-step-inputs {
font-size: 12px;
font-weight: 950;
line-height: 1;
box-shadow: inset 0 -2px 0 rgba(0, 0, 0, 0.2);
box-shadow: var(--combo-token-shadow);
}
button.combo-input-token {
@ -2852,6 +2876,36 @@ button.combo-input-token:hover {
white-space: nowrap;
}
.combo-input-token .ui-icon {
width: 17px;
height: 17px;
}
.combo-input-token.has-hold {
position: relative;
box-shadow:
var(--combo-token-shadow),
7px 0 0 -1px var(--combo-hold-shadow),
9px 0 6px -3px var(--combo-hold-shadow);
}
.combo-input-hold {
position: absolute;
right: -7px;
bottom: -6px;
min-width: 20px;
padding: 1px 4px;
border: 1px solid rgba(246, 196, 83, 0.34);
border-radius: var(--radius-pill);
background: rgba(5, 7, 17, 0.92);
color: var(--color-accent-gold);
font-size: 9px;
font-style: normal;
font-weight: 950;
line-height: 1.2;
text-align: center;
}
.combo-input-key,
.combo-input-mouse,
.combo-value-options,
@ -2967,9 +3021,86 @@ button.combo-input-token.combo-input-mouse {
}
.combo-input-direction {
--combo-token-bg: rgba(34, 211, 238, 0.12);
--combo-token-border: rgba(34, 211, 238, 0.3);
--combo-token-color: var(--color-accent-cyan);
--combo-token-bg:
radial-gradient(circle at 50% 42%, rgba(34, 211, 238, 0.26), transparent 64%),
rgba(34, 211, 238, 0.12);
--combo-token-border: rgba(34, 211, 238, 0.52);
--combo-token-color: #67e8f9;
--combo-token-shadow:
inset 0 -2px 0 rgba(0, 0, 0, 0.24),
inset 0 0 0 1px rgba(34, 211, 238, 0.16);
font-size: 15px;
}
button.combo-input-token.combo-input-direction:hover {
border-color: rgba(34, 211, 238, 0.72);
background:
linear-gradient(180deg, rgba(255, 255, 255, 0.12), transparent),
radial-gradient(circle at 50% 42%, rgba(34, 211, 238, 0.34), transparent 64%),
rgba(18, 52, 70, 0.76);
}
.combo-device-n64.combo-value-stick-up-left,
.combo-device-n64.combo-value-stick-up,
.combo-device-n64.combo-value-stick-up-right,
.combo-device-n64.combo-value-stick-left,
.combo-device-n64.combo-value-stick-right,
.combo-device-n64.combo-value-stick-down-left,
.combo-device-n64.combo-value-stick-down,
.combo-device-n64.combo-value-stick-down-right {
--combo-token-bg:
radial-gradient(circle at 38% 30%, rgba(245, 247, 255, 0.2), transparent 28%),
radial-gradient(circle at 50% 54%, rgba(34, 211, 238, 0.3), transparent 66%),
rgba(14, 75, 94, 0.68);
--combo-token-border: rgba(34, 211, 238, 0.62);
--combo-token-color: #a5f3fc;
--combo-token-shadow:
inset 0 -3px 0 rgba(0, 0, 0, 0.24),
inset 0 0 0 1px rgba(34, 211, 238, 0.18);
}
.combo-device-n64.combo-value-up-left,
.combo-device-n64.combo-value-up,
.combo-device-n64.combo-value-up-right,
.combo-device-n64.combo-value-left,
.combo-device-n64.combo-value-right,
.combo-device-n64.combo-value-down-left,
.combo-device-n64.combo-value-down,
.combo-device-n64.combo-value-down-right {
--combo-token-bg:
linear-gradient(180deg, rgba(246, 196, 83, 0.12), transparent),
rgba(31, 34, 54, 0.92);
--combo-token-border: rgba(246, 196, 83, 0.48);
--combo-token-color: #f8d982;
--combo-token-shadow:
inset 0 -3px 0 rgba(0, 0, 0, 0.28),
inset 0 0 0 1px rgba(246, 196, 83, 0.14);
border-radius: 10px;
}
button.combo-device-n64.combo-value-stick-up-left:hover,
button.combo-device-n64.combo-value-stick-up:hover,
button.combo-device-n64.combo-value-stick-up-right:hover,
button.combo-device-n64.combo-value-stick-left:hover,
button.combo-device-n64.combo-value-stick-right:hover,
button.combo-device-n64.combo-value-stick-down-left:hover,
button.combo-device-n64.combo-value-stick-down:hover,
button.combo-device-n64.combo-value-stick-down-right:hover {
border-color: rgba(34, 211, 238, 0.78);
}
button.combo-device-n64.combo-value-up-left:hover,
button.combo-device-n64.combo-value-up:hover,
button.combo-device-n64.combo-value-up-right:hover,
button.combo-device-n64.combo-value-left:hover,
button.combo-device-n64.combo-value-right:hover,
button.combo-device-n64.combo-value-down-left:hover,
button.combo-device-n64.combo-value-down:hover,
button.combo-device-n64.combo-value-down-right:hover {
border-color: rgba(246, 196, 83, 0.7);
background:
linear-gradient(180deg, rgba(246, 196, 83, 0.16), transparent),
rgba(38, 40, 62, 0.96);
}
.combo-device-playstation.combo-value-triangle {
@ -3066,6 +3197,104 @@ button.combo-input-token.combo-input-mouse {
--combo-token-color: #fca5a5;
}
.combo-value-fist,
.combo-value-foot,
.combo-value-grab,
.combo-value-protect,
.combo-value-shoot {
--combo-token-bg: rgba(217, 70, 239, 0.14);
--combo-token-border: rgba(217, 70, 239, 0.34);
--combo-token-color: var(--color-accent-pink);
}
.combo-value-grab .ui-icon {
width: 21px;
height: 21px;
}
button.combo-input-token.combo-value-grab .ui-icon {
width: 23px;
height: 23px;
}
.combo-hold-field {
display: inline-flex;
align-items: center;
margin: 0;
}
.combo-hold-controls {
display: inline-flex;
width: 92px;
min-width: 92px;
min-height: 34px;
align-items: center;
justify-content: stretch;
gap: 3px;
padding: 3px;
border: 1px solid rgba(165, 180, 252, 0.12);
border-radius: var(--radius-md);
background: rgba(5, 7, 17, 0.26);
}
.combo-hold-controls button {
min-width: 50px;
min-height: 26px;
padding: 0;
border: 1px solid transparent;
border-radius: var(--radius-sm);
background: transparent;
color: var(--color-text-secondary);
font-size: var(--font-size-xs);
font-weight: 900;
}
.combo-hold-controls button.active {
border-color: rgba(246, 196, 83, 0.34);
background: rgba(246, 196, 83, 0.12);
color: var(--color-text-primary);
}
.combo-hold-controls button:disabled {
cursor: not-allowed;
opacity: 0.48;
}
.combo-hold-field input {
box-sizing: border-box;
width: 26px;
min-width: 26px;
height: 26px;
min-height: 26px;
margin: 0;
padding: 0 3px;
border: 1px solid transparent;
border-color: transparent;
border-radius: var(--radius-sm);
background: transparent;
color: var(--color-text-secondary);
font-size: var(--font-size-xs);
font-weight: 900;
line-height: 1;
text-align: center;
}
.combo-hold-field input:focus {
border-color: rgba(246, 196, 83, 0.34);
}
.combo-hold-field input {
appearance: textfield;
-moz-appearance: textfield;
}
.combo-hold-field input::-webkit-outer-spin-button,
.combo-hold-field input::-webkit-inner-spin-button {
margin: 0;
appearance: none;
-webkit-appearance: none;
}
.counters-add-form {
grid-template-columns: minmax(0, 1fr) auto;
}