Déplacement/animation automatiques d'un personnage (scène 2D)
Build and deploy / test-python (push) Successful in 6m25s
Build and deploy / test-js (push) Successful in 55s
Build and deploy / build-and-push (push) Skipped
Build and deploy / deploy (push) Skipped

Simplification demandée pour l'éditeur de jeu RPG : un personnage posé
sur une scène 2D se déplace et s'anime TOUT SEUL avec ZQSD (+ E pour
interagir) dès qu'il est posé — plus besoin de poser le moindre nœud de
flow pour un mouvement de base. Le panneau "🕹️ Commandes" du personnage
permet de remapper les 4 touches de déplacement et la touche
d'interaction, et de bloquer un axe (horizontal/vertical seulement).

Entièrement client (static/js/play/personnage-controller.js), réutilise
heldKeys (triggers.js) et applyObjectProperty/clampSceneObjectPosition/
runSpriteAnimation (actions.js) — aucune logique dupliquée, et ça
fonctionne aussi bien en ligne que dans l'export Web/SCORM (aucune
requête serveur). "walk"/"idle"/"interact" (poses déjà présentes pour
tout personnage Forge/CraftPix) sont utilisées telles quelles ; une pose
absente dégrade silencieusement (déplacement sans animation) plutôt que
de planter.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
william
2026-09-02 11:26:37 +02:00
co-authored by Claude Sonnet 5
parent e82eb7ce88
commit 74f7ca396f
14 changed files with 539 additions and 0 deletions
@@ -0,0 +1,123 @@
// Déplacement/animation automatiques d'un personnage (voir
// static/js/play/personnage-controller.js) : ZQSD + E par défaut, sans
// le moindre nœud de flow à poser — réutilise applyObjectProperty/
// clampSceneObjectPosition/runSpriteAnimation/resolveSpriteFrames
// (actions.js), heldKeys (triggers.js).
const test = require('node:test');
const assert = require('node:assert/strict');
global.window = global.window || {};
global.gameData = global.gameData || {};
global.screensData = [];
global.heldKeys = new Set();
const actionsMod = require('../actions.js');
Object.assign(global, actionsMod);
const {
forgePersonnageAnimExists, forgePersonnageMovementAnim, forgePersonnageIdleAnim,
forgeRunPersonnageControllerTick,
} = require('../personnage-controller.js');
function fakeSceneEl() {
const classes = new Set();
return {
style: { left: '100px', top: '100px', width: '20px', height: '20px' },
classList: {
add: c => classes.add(c), remove: c => classes.delete(c),
toggle: c => (classes.has(c) ? classes.delete(c) : classes.add(c)),
contains: c => classes.has(c),
},
};
}
function withPersonnageObject(obj, elByObjectId, fn) {
const previousDoc = global.document;
global.document = { querySelector: sel => elByObjectId[sel.match(/"(\d+)"/)[1]] };
global.screensData = [{ id: 1, scene_width: 500, scene_height: 500 }];
global.window.currentScreenId = 1;
global.gameData = { personnage_animations: { [obj.id]: { walk: ['/w0.png'], idle: ['/i0.png'] } } };
try { fn(); } finally { global.document = previousDoc; stopAllSpriteAnimations(); }
}
const { stopAllSpriteAnimations } = actionsMod;
test('forgePersonnageMovementAnim/forgePersonnageIdleAnim — repli silencieux si la pose est absente', () => {
global.gameData = { personnage_animations: { 5: { idle: ['/i0.png'] } } };
assert.equal(forgePersonnageMovementAnim(5), null); // pas de "walk" pour ce personnage
assert.equal(forgePersonnageIdleAnim(5), 'idle');
assert.equal(forgePersonnageAnimExists(5, 'interact'), false);
});
test('tick — touche "droite" enfoncée : déplace vers la droite et retire le miroir', () => {
const obj = { id: 10, kind: 'personnage', personnage_commandes: { haut: 'z', bas: 's', gauche: 'q', droite: 'd', interagir: 'e', axe: 'libre', vitesse: 4 } };
const el = fakeSceneEl();
withPersonnageObject(obj, { 10: el }, () => {
global.heldKeys = new Set(['d']);
forgeRunPersonnageControllerTick({ elements: [obj] });
assert.equal(el.style.left, '104px');
assert.equal(el.classList.contains('forgeFlipped'), false);
});
});
test('tick — touche "gauche" enfoncée : déplace vers la gauche et applique le miroir', () => {
const obj = { id: 11, kind: 'personnage', personnage_commandes: { haut: 'z', bas: 's', gauche: 'q', droite: 'd', interagir: 'e', axe: 'libre', vitesse: 4 } };
const el = fakeSceneEl();
withPersonnageObject(obj, { 11: el }, () => {
global.heldKeys = new Set(['q']);
forgeRunPersonnageControllerTick({ elements: [obj] });
assert.equal(el.style.left, '96px');
assert.equal(el.classList.contains('forgeFlipped'), true);
});
});
test('tick — axe "horizontal" : ignore haut/bas même si la touche est enfoncée', () => {
const obj = { id: 12, kind: 'personnage', personnage_commandes: { haut: 'z', bas: 's', gauche: 'q', droite: 'd', interagir: 'e', axe: 'horizontal', vitesse: 4 } };
const el = fakeSceneEl();
withPersonnageObject(obj, { 12: el }, () => {
global.heldKeys = new Set(['z']); // "haut", bloqué par l'axe
forgeRunPersonnageControllerTick({ elements: [obj] });
assert.equal(el.style.top, '100px'); // inchangé
});
});
test('tick — axe "vertical" : ignore gauche/droite même si la touche est enfoncée', () => {
const obj = { id: 13, kind: 'personnage', personnage_commandes: { haut: 'z', bas: 's', gauche: 'q', droite: 'd', interagir: 'e', axe: 'vertical', vitesse: 4 } };
const el = fakeSceneEl();
withPersonnageObject(obj, { 13: el }, () => {
global.heldKeys = new Set(['d']); // "droite", bloqué par l'axe
forgeRunPersonnageControllerTick({ elements: [obj] });
assert.equal(el.style.left, '100px'); // inchangé
});
});
test('tick — bornage aux limites de la scène (ne sort jamais du cadre)', () => {
const obj = { id: 14, kind: 'personnage', personnage_commandes: { haut: 'z', bas: 's', gauche: 'q', droite: 'd', interagir: 'e', axe: 'libre', vitesse: 50 } };
const el = fakeSceneEl();
el.style.left = '490px'; // 500 (scène) - 20 (largeur) = 480 max
withPersonnageObject(obj, { 14: el }, () => {
global.heldKeys = new Set(['d']);
forgeRunPersonnageControllerTick({ elements: [obj] });
assert.equal(el.style.left, '480px');
});
});
test('tick — aucune touche enfoncée : ne bouge pas (l\'appel reste sans effet)', () => {
const obj = { id: 15, kind: 'personnage', personnage_commandes: { haut: 'z', bas: 's', gauche: 'q', droite: 'd', interagir: 'e', axe: 'libre', vitesse: 4 } };
const el = fakeSceneEl();
withPersonnageObject(obj, { 15: el }, () => {
global.heldKeys = new Set();
forgeRunPersonnageControllerTick({ elements: [obj] });
assert.equal(el.style.left, '100px');
assert.equal(el.style.top, '100px');
});
});
test('tick — ignore tout objet qui n\'est pas kind="personnage" ou sans commandes résolues', () => {
const decor = { id: 16, kind: 'decor' };
withPersonnageObject(decor, {}, () => {
global.heldKeys = new Set(['d']);
// Ne doit pas lever (aucun élément DOM trouvé pour cet objet, mais on
// ne devrait même pas tenter de le chercher vu qu'il n'est pas un personnage).
assert.doesNotThrow(() => forgeRunPersonnageControllerTick({ elements: [decor] }));
});
});