Commandes personnage : détection de touche au clavier + touches d'animation supplémentaires
Build and deploy / test-python (push) Successful in 6m17s
Build and deploy / test-js (push) Successful in 53s
Build and deploy / build-and-push (push) Skipped
Build and deploy / deploy (push) Skipped

Deux retours utilisateur sur le panneau "Commandes" (déplacement/
animation automatiques, voir précédent commit) :
- Les champs de touche (haut/bas/gauche/droite/interagir) capturent
  maintenant la touche au clavier (clic puis appui — event.key, même
  valeur que heldKeys/triggers.js) au lieu d'être tapés à la main,
  source d'erreurs ("Espace" vs " ", "flèche haut" vs "ArrowUp"...).
- Nouvelle section "Animations supplémentaires" : un nombre illimité de
  touches, chacune liée à UNE pose au choix parmi celles réellement
  disponibles pour ce personnage (sauter, attaquer, courir...) — pas
  seulement les 4 touches de déplacement + interagir.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
william
2026-09-02 11:50:29 +02:00
co-authored by Claude Sonnet 5
parent 74f7ca396f
commit 5891c82c62
8 changed files with 209 additions and 22 deletions
@@ -15,7 +15,7 @@ const actionsMod = require('../actions.js');
Object.assign(global, actionsMod);
const {
forgePersonnageAnimExists, forgePersonnageMovementAnim, forgePersonnageIdleAnim,
forgePersonnageAnimExists, forgePersonnageMovementAnim, forgePersonnageIdleAnim, forgePersonnageActionAnim,
forgeRunPersonnageControllerTick,
} = require('../personnage-controller.js');
@@ -121,3 +121,19 @@ test('tick — ignore tout objet qui n\'est pas kind="personnage" ou sans comman
assert.doesNotThrow(() => forgeRunPersonnageControllerTick({ elements: [decor] }));
});
});
// Touches supplémentaires (sauter, attaquer...) — bug remonté par
// l'utilisateur : au-delà des 4 touches de déplacement et de "interagir",
// il manquait un moyen de lier une touche à N'IMPORTE QUELLE autre pose du
// personnage (voir "Animations supplémentaires", templates/scene_edit.html).
test('forgePersonnageActionAnim — "interagir" prioritaire, sinon la touche supplémentaire correspondante', () => {
const cmd = { interagir: 'e', extra: [{ touche: 'j', animation: 'jump' }, { touche: 'a', animation: 'attack' }] };
assert.equal(forgePersonnageActionAnim(cmd, 'e'), 'interact');
assert.equal(forgePersonnageActionAnim(cmd, 'j'), 'jump');
assert.equal(forgePersonnageActionAnim(cmd, 'a'), 'attack');
assert.equal(forgePersonnageActionAnim(cmd, 'x'), null); // touche non liée
});
test('forgePersonnageActionAnim — commandes sans "extra" (personnage posé avant l\'ajout du réglage)', () => {
assert.equal(forgePersonnageActionAnim({ interagir: 'e' }, 'j'), null);
});
+18 -7
View File
@@ -78,18 +78,29 @@ function forgeRunPersonnageControllerTick(screenData) {
});
}
// Ponctuel (keydown, pas heldKeys) — contrairement au déplacement,
// "interagir" est une action instantanée (ouvrir un coffre, parler à un
// PNJ...), jamais un mouvement continu.
// Quelle pose jouer pour une touche d'ACTION (pas de déplacement) donnée —
// "interagir" (usage fixe, pose "interact") d'abord, sinon la première
// touche supplémentaire (cmd.extra, voir "Animations supplémentaires" du
// panneau Commandes) qui correspond, pour un nombre de poses illimité
// (sauter, attaquer, courir...) sans ajouter un champ fixe par pose.
function forgePersonnageActionAnim(cmd, key) {
if (cmd.interagir === key) return 'interact';
var extraMatch = (cmd.extra || []).find(function (e) { return e.touche === key; });
return extraMatch ? extraMatch.animation : null;
}
// Ponctuel (keydown, pas heldKeys) — contrairement au déplacement, une
// touche d'action est une action instantanée (interagir, sauter,
// attaquer...), jamais un mouvement continu.
function forgeStartPersonnageInteractListener(screenData) {
activePersonnageInteractListener = function (ev) {
(screenData.elements || []).forEach(function (obj) {
if (obj.kind !== 'personnage' || !obj.personnage_commandes) return;
if (obj.personnage_commandes.interagir !== ev.key) return;
var animName = forgePersonnageActionAnim(obj.personnage_commandes, ev.key);
if (!animName || !forgePersonnageAnimExists(obj.id, animName)) return;
var targetEl = document.querySelector('[data-object-id="' + obj.id + '"]');
if (!targetEl) return;
var interactAnim = forgePersonnageAnimExists(obj.id, 'interact') ? 'interact' : null;
if (interactAnim) runSpriteAnimation(targetEl, resolveSpriteFrames(obj.id, { animation: interactAnim, fps: 8, loop: false }));
runSpriteAnimation(targetEl, resolveSpriteFrames(obj.id, { animation: animName, fps: 8, loop: false }));
});
};
window.addEventListener('keydown', activePersonnageInteractListener);
@@ -112,7 +123,7 @@ function forgeStartPersonnageControllers(screenData) {
// client.
if (typeof module !== 'undefined' && module.exports) {
module.exports = {
forgePersonnageAnimExists, forgePersonnageMovementAnim, forgePersonnageIdleAnim,
forgePersonnageAnimExists, forgePersonnageMovementAnim, forgePersonnageIdleAnim, forgePersonnageActionAnim,
forgeRunPersonnageControllerTick, forgeStartPersonnageControllers, forgeStopPersonnageControllers,
forgeStartPersonnageInteractListener,
};
+81 -4
View File
@@ -145,14 +145,23 @@ function swapSceneObjectCharacter(forgeCharacter) {
}
// "Commandes" (voir static/js/play/personnage-controller.js) : touches de
// déplacement/interaction + blocage d'axe d'un personnage — un seul
// formulaire, enregistré en AJAX à chaque changement de champ, même
// patron que swapSceneObjectCharacter() ci-dessus (POST JSON vers
// routes/scenes/scene_object_commands.py, aucun rechargement de page).
// déplacement/interaction + blocage d'axe + touches supplémentaires d'un
// personnage — un seul formulaire, enregistré en AJAX à chaque
// changement de champ, même patron que swapSceneObjectCharacter()
// ci-dessus (POST JSON vers routes/scenes/scene_object_commands.py,
// aucun rechargement de page).
function saveSceneObjectCommands() {
var body = document.getElementById('personnageCommandsBody');
if (!body) return;
var objectId = parseInt(body.dataset.elementId, 10);
var extra = [];
document.querySelectorAll('#personnageCommandsExtra .personnageCommandExtraRow').forEach(function (row) {
var toucheEl = row.querySelector('.personnageKeyCapture');
var animEl = row.querySelector('.cmdExtraAnim');
if (toucheEl && toucheEl.value && animEl && animEl.value) {
extra.push({ touche: toucheEl.value, animation: animEl.value });
}
});
fetch('/game/' + GAME_SLUG + '/scene-objects/' + objectId + '/commandes', {
method: 'POST', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
@@ -163,15 +172,83 @@ function saveSceneObjectCommands() {
interagir: document.getElementById('cmd-interagir').value,
axe: document.getElementById('cmd-axe').value,
vitesse: document.getElementById('cmd-vitesse').value,
extra: extra,
}),
});
}
// Détection de touche (bug remonté par l'utilisateur : taper le nom d'une
// touche à la main est source d'erreurs — "Espace" vs " ", "flèche haut"
// vs "ArrowUp"...) : au focus, le champ (readonly, jamais tapé) affiche un
// indice puis se remplit avec la PROCHAINE touche pressée (event.key,
// même valeur que heldKeys/bindKeyboardTriggers, triggers.js — cohérent
// avec ce que le moteur compare réellement au jeu). capture:true sur
// window pour intercepter la touche avant tout autre raccourci de
// l'éditeur (ex. Suppr qui efface l'objet sélectionné).
function bindPersonnageKeyCapture(el) {
if (!el || el.dataset.keyCaptureBound) return;
el.dataset.keyCaptureBound = '1';
el.addEventListener('focus', function () {
var previous = el.value;
el.value = 'Appuie sur une touche...';
el.classList.add('is-warning');
function onKey(ev) {
ev.preventDefault();
ev.stopPropagation();
window.removeEventListener('keydown', onKey, true);
el.classList.remove('is-warning');
if (ev.key === 'Escape') { el.value = previous; el.blur(); return; }
el.value = ev.key;
el.blur();
el.dispatchEvent(new Event('change'));
}
window.addEventListener('keydown', onKey, true);
});
}
// Une ligne = une touche supplémentaire liée à UNE pose au choix parmi
// celles RÉELLEMENT disponibles pour CE personnage (ELEMENT_ANIMATIONS_MAP,
// déjà connu de personnage-panel.js/personnage-preview.js) — jamais un nom
// tapé à la main, qui pourrait ne correspondre à aucune pose existante.
function personnageCommandExtraRowEl(objectId, touche, animation) {
var row = document.createElement('div');
row.className = 'field is-grouped personnageCommandExtraRow';
var animNames = Object.keys(ELEMENT_ANIMATIONS_MAP[objectId] || {});
var options = animNames.map(function (name) {
return '<option value="' + name + '"' + (name === animation ? ' selected' : '') + '>' + name + '</option>';
}).join('');
row.innerHTML =
'<div class="control"><input class="input is-small personnageKeyCapture" readonly placeholder="Touche..." value="' + (touche || '').replace(/"/g, '&quot;') + '"></div>' +
'<div class="control"><div class="select is-small"><select class="cmdExtraAnim">' + options + '</select></div></div>' +
'<div class="control"><button type="button" class="button is-small" onclick="this.closest(\'.personnageCommandExtraRow\').remove(); saveSceneObjectCommands();">×</button></div>';
var keyInput = row.querySelector('.personnageKeyCapture');
bindPersonnageKeyCapture(keyInput);
keyInput.addEventListener('change', saveSceneObjectCommands);
row.querySelector('.cmdExtraAnim').addEventListener('change', saveSceneObjectCommands);
return row;
}
function addPersonnageCommandExtraRow() {
var body = document.getElementById('personnageCommandsBody');
var container = document.getElementById('personnageCommandsExtra');
if (!body || !container) return;
var objectId = parseInt(body.dataset.elementId, 10);
container.appendChild(personnageCommandExtraRowEl(objectId, '', ''));
}
(function () {
var body = document.getElementById('personnageCommandsBody');
if (!body) return;
var objectId = parseInt(body.dataset.elementId, 10);
['cmd-haut', 'cmd-bas', 'cmd-gauche', 'cmd-droite', 'cmd-interagir'].forEach(function (id) {
bindPersonnageKeyCapture(document.getElementById(id));
});
['cmd-haut', 'cmd-bas', 'cmd-gauche', 'cmd-droite', 'cmd-interagir', 'cmd-axe', 'cmd-vitesse'].forEach(function (id) {
var el = document.getElementById(id);
if (el) el.addEventListener('change', saveSceneObjectCommands);
});
var container = document.getElementById('personnageCommandsExtra');
(window.PERSONNAGE_COMMANDS_EXTRA || []).forEach(function (e) {
container.appendChild(personnageCommandExtraRowEl(objectId, e.touche, e.animation));
});
})();