Commandes personnage : détection de touche au clavier + touches d'animation supplémentaires
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:
co-authored by
Claude Sonnet 5
parent
74f7ca396f
commit
5891c82c62
@@ -32,6 +32,9 @@ def scene_object_commands(slug, object_id):
|
|||||||
except (TypeError, ValueError):
|
except (TypeError, ValueError):
|
||||||
vitesse = defaults["vitesse"]
|
vitesse = defaults["vitesse"]
|
||||||
commands["vitesse"] = max(1, min(20, vitesse))
|
commands["vitesse"] = max(1, min(20, vitesse))
|
||||||
|
# Touches supplémentaires (sauter, attaquer...) — voir "Animations
|
||||||
|
# supplémentaires" dans le panneau Commandes, templates/scene_edit.html.
|
||||||
|
commands["extra"] = screens.sanitize_extra_commands(body.get("extra"))
|
||||||
|
|
||||||
screens.set_scene_object_commands(slug, object_id, commands)
|
screens.set_scene_object_commands(slug, object_id, commands)
|
||||||
return jsonify({"ok": True, "commands": commands})
|
return jsonify({"ok": True, "commands": commands})
|
||||||
|
|||||||
+1
-1
@@ -76,7 +76,7 @@ from .flow.constants import (
|
|||||||
from .rendering.render_element_html import render_element_html
|
from .rendering.render_element_html import render_element_html
|
||||||
from .rendering.trigger_for import _trigger_for
|
from .rendering.trigger_for import _trigger_for
|
||||||
from .rendering.personnage_data import resolve_personnage_data, resolve_personnage_animations, resolve_personnage_idle_frame
|
from .rendering.personnage_data import resolve_personnage_data, resolve_personnage_animations, resolve_personnage_idle_frame
|
||||||
from .rendering.personnage_commands import resolve_personnage_commands, DEFAULT_PERSONNAGE_COMMANDS
|
from .rendering.personnage_commands import resolve_personnage_commands, DEFAULT_PERSONNAGE_COMMANDS, sanitize_extra_commands
|
||||||
|
|
||||||
from .screens_repo.ensure_schema import ensure_schema
|
from .screens_repo.ensure_schema import ensure_schema
|
||||||
from .screens_repo.list_screens import list_screens
|
from .screens_repo.list_screens import list_screens
|
||||||
|
|||||||
@@ -8,11 +8,33 @@ import json
|
|||||||
# axe de déplacement (ex. un personnage qui ne se déplace que de gauche à
|
# axe de déplacement (ex. un personnage qui ne se déplace que de gauche à
|
||||||
# droite). "vitesse" est en pixels par tick (50ms, voir
|
# droite). "vitesse" est en pixels par tick (50ms, voir
|
||||||
# personnage-controller.js — même cadence que touche_maintenue).
|
# personnage-controller.js — même cadence que touche_maintenue).
|
||||||
|
# "extra" : touches SUPPLÉMENTAIRES, chacune liée à une pose du personnage
|
||||||
|
# au choix (ex. "sauter" -> "jump") — au-delà des 4 touches de déplacement
|
||||||
|
# et de "interagir", qui restent les deux seules touches à usage FIXE.
|
||||||
DEFAULT_PERSONNAGE_COMMANDS = {
|
DEFAULT_PERSONNAGE_COMMANDS = {
|
||||||
"haut": "z", "bas": "s", "gauche": "q", "droite": "d",
|
"haut": "z", "bas": "s", "gauche": "q", "droite": "d",
|
||||||
"interagir": "e", "axe": "libre", "vitesse": 4,
|
"interagir": "e", "axe": "libre", "vitesse": 4, "extra": [],
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Nombre maximum de touches supplémentaires — large marge pour un vrai
|
||||||
|
# jeu (sauter, attaquer, courir...) sans laisser un panneau grossir sans
|
||||||
|
# fin par erreur de manipulation.
|
||||||
|
_MAX_EXTRA_COMMANDS = 20
|
||||||
|
|
||||||
|
|
||||||
|
def sanitize_extra_commands(raw_extra):
|
||||||
|
if not isinstance(raw_extra, list):
|
||||||
|
return []
|
||||||
|
cleaned = []
|
||||||
|
for item in raw_extra[:_MAX_EXTRA_COMMANDS]:
|
||||||
|
if not isinstance(item, dict):
|
||||||
|
continue
|
||||||
|
touche = item.get("touche")
|
||||||
|
animation = item.get("animation")
|
||||||
|
if isinstance(touche, str) and touche and isinstance(animation, str) and animation:
|
||||||
|
cleaned.append({"touche": touche, "animation": animation})
|
||||||
|
return cleaned
|
||||||
|
|
||||||
|
|
||||||
def resolve_personnage_commands(el):
|
def resolve_personnage_commands(el):
|
||||||
"""Décode attributes._personnage_commandes (voir
|
"""Décode attributes._personnage_commandes (voir
|
||||||
@@ -22,11 +44,15 @@ def resolve_personnage_commands(el):
|
|||||||
l'ajout de ce réglage)."""
|
l'ajout de ce réglage)."""
|
||||||
raw = (el.get("attributes") or {}).get("_personnage_commandes")
|
raw = (el.get("attributes") or {}).get("_personnage_commandes")
|
||||||
commands = dict(DEFAULT_PERSONNAGE_COMMANDS)
|
commands = dict(DEFAULT_PERSONNAGE_COMMANDS)
|
||||||
|
commands["extra"] = [] # jamais l'alias de la liste DEFAULT (mutable partagée)
|
||||||
if raw:
|
if raw:
|
||||||
try:
|
try:
|
||||||
parsed = json.loads(raw)
|
parsed = json.loads(raw)
|
||||||
except (TypeError, ValueError):
|
except (TypeError, ValueError):
|
||||||
parsed = None
|
parsed = None
|
||||||
if isinstance(parsed, dict):
|
if isinstance(parsed, dict):
|
||||||
commands.update({k: v for k, v in parsed.items() if k in DEFAULT_PERSONNAGE_COMMANDS})
|
for key in ("haut", "bas", "gauche", "droite", "interagir", "axe", "vitesse"):
|
||||||
|
if key in parsed:
|
||||||
|
commands[key] = parsed[key]
|
||||||
|
commands["extra"] = sanitize_extra_commands(parsed.get("extra"))
|
||||||
return commands
|
return commands
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ const actionsMod = require('../actions.js');
|
|||||||
Object.assign(global, actionsMod);
|
Object.assign(global, actionsMod);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
forgePersonnageAnimExists, forgePersonnageMovementAnim, forgePersonnageIdleAnim,
|
forgePersonnageAnimExists, forgePersonnageMovementAnim, forgePersonnageIdleAnim, forgePersonnageActionAnim,
|
||||||
forgeRunPersonnageControllerTick,
|
forgeRunPersonnageControllerTick,
|
||||||
} = require('../personnage-controller.js');
|
} = 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] }));
|
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);
|
||||||
|
});
|
||||||
|
|||||||
@@ -78,18 +78,29 @@ function forgeRunPersonnageControllerTick(screenData) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ponctuel (keydown, pas heldKeys) — contrairement au déplacement,
|
// Quelle pose jouer pour une touche d'ACTION (pas de déplacement) donnée —
|
||||||
// "interagir" est une action instantanée (ouvrir un coffre, parler à un
|
// "interagir" (usage fixe, pose "interact") d'abord, sinon la première
|
||||||
// PNJ...), jamais un mouvement continu.
|
// 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) {
|
function forgeStartPersonnageInteractListener(screenData) {
|
||||||
activePersonnageInteractListener = function (ev) {
|
activePersonnageInteractListener = function (ev) {
|
||||||
(screenData.elements || []).forEach(function (obj) {
|
(screenData.elements || []).forEach(function (obj) {
|
||||||
if (obj.kind !== 'personnage' || !obj.personnage_commandes) return;
|
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 + '"]');
|
var targetEl = document.querySelector('[data-object-id="' + obj.id + '"]');
|
||||||
if (!targetEl) return;
|
if (!targetEl) return;
|
||||||
var interactAnim = forgePersonnageAnimExists(obj.id, 'interact') ? 'interact' : null;
|
runSpriteAnimation(targetEl, resolveSpriteFrames(obj.id, { animation: animName, fps: 8, loop: false }));
|
||||||
if (interactAnim) runSpriteAnimation(targetEl, resolveSpriteFrames(obj.id, { animation: interactAnim, fps: 8, loop: false }));
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
window.addEventListener('keydown', activePersonnageInteractListener);
|
window.addEventListener('keydown', activePersonnageInteractListener);
|
||||||
@@ -112,7 +123,7 @@ function forgeStartPersonnageControllers(screenData) {
|
|||||||
// client.
|
// client.
|
||||||
if (typeof module !== 'undefined' && module.exports) {
|
if (typeof module !== 'undefined' && module.exports) {
|
||||||
module.exports = {
|
module.exports = {
|
||||||
forgePersonnageAnimExists, forgePersonnageMovementAnim, forgePersonnageIdleAnim,
|
forgePersonnageAnimExists, forgePersonnageMovementAnim, forgePersonnageIdleAnim, forgePersonnageActionAnim,
|
||||||
forgeRunPersonnageControllerTick, forgeStartPersonnageControllers, forgeStopPersonnageControllers,
|
forgeRunPersonnageControllerTick, forgeStartPersonnageControllers, forgeStopPersonnageControllers,
|
||||||
forgeStartPersonnageInteractListener,
|
forgeStartPersonnageInteractListener,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -145,14 +145,23 @@ function swapSceneObjectCharacter(forgeCharacter) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// "Commandes" (voir static/js/play/personnage-controller.js) : touches de
|
// "Commandes" (voir static/js/play/personnage-controller.js) : touches de
|
||||||
// déplacement/interaction + blocage d'axe d'un personnage — un seul
|
// déplacement/interaction + blocage d'axe + touches supplémentaires d'un
|
||||||
// formulaire, enregistré en AJAX à chaque changement de champ, même
|
// personnage — un seul formulaire, enregistré en AJAX à chaque
|
||||||
// patron que swapSceneObjectCharacter() ci-dessus (POST JSON vers
|
// changement de champ, même patron que swapSceneObjectCharacter()
|
||||||
// routes/scenes/scene_object_commands.py, aucun rechargement de page).
|
// ci-dessus (POST JSON vers routes/scenes/scene_object_commands.py,
|
||||||
|
// aucun rechargement de page).
|
||||||
function saveSceneObjectCommands() {
|
function saveSceneObjectCommands() {
|
||||||
var body = document.getElementById('personnageCommandsBody');
|
var body = document.getElementById('personnageCommandsBody');
|
||||||
if (!body) return;
|
if (!body) return;
|
||||||
var objectId = parseInt(body.dataset.elementId, 10);
|
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', {
|
fetch('/game/' + GAME_SLUG + '/scene-objects/' + objectId + '/commandes', {
|
||||||
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
@@ -163,15 +172,83 @@ function saveSceneObjectCommands() {
|
|||||||
interagir: document.getElementById('cmd-interagir').value,
|
interagir: document.getElementById('cmd-interagir').value,
|
||||||
axe: document.getElementById('cmd-axe').value,
|
axe: document.getElementById('cmd-axe').value,
|
||||||
vitesse: document.getElementById('cmd-vitesse').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, '"') + '"></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 () {
|
(function () {
|
||||||
var body = document.getElementById('personnageCommandsBody');
|
var body = document.getElementById('personnageCommandsBody');
|
||||||
if (!body) return;
|
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) {
|
['cmd-haut', 'cmd-bas', 'cmd-gauche', 'cmd-droite', 'cmd-interagir', 'cmd-axe', 'cmd-vitesse'].forEach(function (id) {
|
||||||
var el = document.getElementById(id);
|
var el = document.getElementById(id);
|
||||||
if (el) el.addEventListener('change', saveSceneObjectCommands);
|
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));
|
||||||
|
});
|
||||||
})();
|
})();
|
||||||
|
|||||||
@@ -680,21 +680,21 @@
|
|||||||
<details class="propGroup">
|
<details class="propGroup">
|
||||||
<summary>🕹️ Commandes</summary>
|
<summary>🕹️ Commandes</summary>
|
||||||
<div class="propGroupBody" id="personnageCommandsBody" data-element-id="{{ obj.id }}">
|
<div class="propGroupBody" id="personnageCommandsBody" data-element-id="{{ obj.id }}">
|
||||||
<p class="help">Ce personnage se déplace et s'anime automatiquement avec ces touches, sans rien à poser dans la logique.</p>
|
<p class="help">Ce personnage se déplace et s'anime automatiquement avec ces touches, sans rien à poser dans la logique. Clique un champ puis appuie sur la touche voulue.</p>
|
||||||
<div class="field is-grouped">
|
<div class="field is-grouped">
|
||||||
<div class="control"><label class="label is-small">Haut</label>
|
<div class="control"><label class="label is-small">Haut</label>
|
||||||
<input class="input is-small" id="cmd-haut" maxlength="12" value="{{ personnage_commands.haut }}"></div>
|
<input class="input is-small personnageKeyCapture" id="cmd-haut" readonly value="{{ personnage_commands.haut }}"></div>
|
||||||
<div class="control"><label class="label is-small">Bas</label>
|
<div class="control"><label class="label is-small">Bas</label>
|
||||||
<input class="input is-small" id="cmd-bas" maxlength="12" value="{{ personnage_commands.bas }}"></div>
|
<input class="input is-small personnageKeyCapture" id="cmd-bas" readonly value="{{ personnage_commands.bas }}"></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="field is-grouped">
|
<div class="field is-grouped">
|
||||||
<div class="control"><label class="label is-small">Gauche</label>
|
<div class="control"><label class="label is-small">Gauche</label>
|
||||||
<input class="input is-small" id="cmd-gauche" maxlength="12" value="{{ personnage_commands.gauche }}"></div>
|
<input class="input is-small personnageKeyCapture" id="cmd-gauche" readonly value="{{ personnage_commands.gauche }}"></div>
|
||||||
<div class="control"><label class="label is-small">Droite</label>
|
<div class="control"><label class="label is-small">Droite</label>
|
||||||
<input class="input is-small" id="cmd-droite" maxlength="12" value="{{ personnage_commands.droite }}"></div>
|
<input class="input is-small personnageKeyCapture" id="cmd-droite" readonly value="{{ personnage_commands.droite }}"></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="field"><label class="label is-small">Interagir</label>
|
<div class="field"><label class="label is-small">Interagir</label>
|
||||||
<div class="control"><input class="input is-small" id="cmd-interagir" maxlength="12" value="{{ personnage_commands.interagir }}"></div>
|
<div class="control"><input class="input is-small personnageKeyCapture" id="cmd-interagir" readonly value="{{ personnage_commands.interagir }}"></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="field"><label class="label is-small">Axe de déplacement</label>
|
<div class="field"><label class="label is-small">Axe de déplacement</label>
|
||||||
<div class="control">
|
<div class="control">
|
||||||
@@ -710,6 +710,17 @@
|
|||||||
<div class="field"><label class="label is-small">Vitesse (px/tick)</label>
|
<div class="field"><label class="label is-small">Vitesse (px/tick)</label>
|
||||||
<div class="control"><input class="input is-small" type="number" id="cmd-vitesse" min="1" max="20" value="{{ personnage_commands.vitesse }}"></div>
|
<div class="control"><input class="input is-small" type="number" id="cmd-vitesse" min="1" max="20" value="{{ personnage_commands.vitesse }}"></div>
|
||||||
</div>
|
</div>
|
||||||
|
<!-- Touches supplémentaires (sauter, attaquer...) : au-delà des 4
|
||||||
|
touches de déplacement et de "Interagir" ci-dessus, chacune
|
||||||
|
liée à UNE pose au choix du personnage (voir
|
||||||
|
ELEMENT_ANIMATIONS_MAP/PERSONNAGE_COMMANDS_EXTRA, déjà
|
||||||
|
connues de scene-editor.js) — construites en JS, jamais en
|
||||||
|
Jinja : le nombre de lignes change dynamiquement (+ Ajouter
|
||||||
|
/ ×). -->
|
||||||
|
<div class="field"><label class="label is-small">Animations supplémentaires (sauter, attaquer...)</label>
|
||||||
|
<div id="personnageCommandsExtra"></div>
|
||||||
|
<button type="button" class="button is-small" onclick="addPersonnageCommandExtraRow()">+ Ajouter une touche</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</details>
|
</details>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@@ -746,6 +757,7 @@
|
|||||||
var ANIMATE_CSS_LABELS = {{ animate_css_labels_json|safe }};
|
var ANIMATE_CSS_LABELS = {{ animate_css_labels_json|safe }};
|
||||||
var SPRITE_LIBRARY = {{ sprite_library_json|safe }};
|
var SPRITE_LIBRARY = {{ sprite_library_json|safe }};
|
||||||
var ELEMENT_ANIMATIONS_MAP = {{ element_animations_map_json|safe }};
|
var ELEMENT_ANIMATIONS_MAP = {{ element_animations_map_json|safe }};
|
||||||
|
var PERSONNAGE_COMMANDS_EXTRA = {{ (personnage_commands.extra if personnage_commands else [])|tojson }};
|
||||||
</script>
|
</script>
|
||||||
<script src="{{ url_for('static', filename='js/screen_edit/tree-panels.js') }}"></script>
|
<script src="{{ url_for('static', filename='js/screen_edit/tree-panels.js') }}"></script>
|
||||||
<script src="{{ url_for('static', filename='js/screen_edit/panel-init.js') }}"></script>
|
<script src="{{ url_for('static', filename='js/screen_edit/panel-init.js') }}"></script>
|
||||||
|
|||||||
@@ -51,10 +51,52 @@ def test_commands_route_persists_remapped_keys_and_axis_lock(client, tmp_game_sl
|
|||||||
commands = screens.resolve_personnage_commands(obj)
|
commands = screens.resolve_personnage_commands(obj)
|
||||||
assert commands == {
|
assert commands == {
|
||||||
"haut": "ArrowUp", "bas": "ArrowDown", "gauche": "ArrowLeft", "droite": "ArrowRight",
|
"haut": "ArrowUp", "bas": "ArrowDown", "gauche": "ArrowLeft", "droite": "ArrowRight",
|
||||||
"interagir": " ", "axe": "horizontal", "vitesse": 8,
|
"interagir": " ", "axe": "horizontal", "vitesse": 8, "extra": [],
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def test_commands_route_persists_extra_animation_bound_keys(client, tmp_game_slug_cleanup):
|
||||||
|
"""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."""
|
||||||
|
slug, screen_id = _create_jeu2d_game(client, tmp_game_slug_cleanup)
|
||||||
|
obj_id = _add_personnage(client, slug, screen_id)
|
||||||
|
|
||||||
|
resp = client.post(
|
||||||
|
f"/game/{slug}/scene-objects/{obj_id}/commandes",
|
||||||
|
data='{"extra": [{"touche": "j", "animation": "jump"}, {"touche": "a", "animation": "attack"}]}',
|
||||||
|
content_type="application/json",
|
||||||
|
)
|
||||||
|
commands = resp.get_json()["commands"]
|
||||||
|
assert commands["extra"] == [{"touche": "j", "animation": "jump"}, {"touche": "a", "animation": "attack"}]
|
||||||
|
|
||||||
|
obj = screens.get_scene_object(slug, obj_id)
|
||||||
|
assert screens.resolve_personnage_commands(obj)["extra"] == commands["extra"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_commands_route_drops_malformed_extra_entries(client, tmp_game_slug_cleanup):
|
||||||
|
"""Une entrée sans touche/animation, un doublon de type, ou une liste
|
||||||
|
qui n'en est pas une : ignorée plutôt que de planter ou de polluer le
|
||||||
|
panneau avec une ligne inutilisable côté client."""
|
||||||
|
slug, screen_id = _create_jeu2d_game(client, tmp_game_slug_cleanup)
|
||||||
|
obj_id = _add_personnage(client, slug, screen_id)
|
||||||
|
|
||||||
|
resp = client.post(
|
||||||
|
f"/game/{slug}/scene-objects/{obj_id}/commandes",
|
||||||
|
data='{"extra": [{"touche": "j", "animation": "jump"}, {"touche": ""}, {"animation": "attack"}, "pas un objet", 42]}',
|
||||||
|
content_type="application/json",
|
||||||
|
)
|
||||||
|
assert resp.get_json()["commands"]["extra"] == [{"touche": "j", "animation": "jump"}]
|
||||||
|
|
||||||
|
resp = client.post(
|
||||||
|
f"/game/{slug}/scene-objects/{obj_id}/commandes",
|
||||||
|
data='{"extra": "pas une liste"}',
|
||||||
|
content_type="application/json",
|
||||||
|
)
|
||||||
|
assert resp.get_json()["commands"]["extra"] == []
|
||||||
|
|
||||||
|
|
||||||
def test_commands_route_rejects_invalid_axis_and_clamps_speed(client, tmp_game_slug_cleanup):
|
def test_commands_route_rejects_invalid_axis_and_clamps_speed(client, tmp_game_slug_cleanup):
|
||||||
"""Un axe inconnu retombe sur "libre", une vitesse hors bornes est
|
"""Un axe inconnu retombe sur "libre", une vitesse hors bornes est
|
||||||
ramenée dans [1, 20] — jamais une valeur qui planterait le calcul
|
ramenée dans [1, 20] — jamais une valeur qui planterait le calcul
|
||||||
|
|||||||
Reference in New Issue
Block a user