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>
120 lines
5.5 KiB
JavaScript
120 lines
5.5 KiB
JavaScript
// ---------- Personnage jouable — déplacement/animation automatiques ----------
|
|
// N'importe quel objet de scène kind="personnage" se déplace et s'anime
|
|
// TOUT SEUL avec ses touches (ZQSD + E par défaut — voir "Commandes" du
|
|
// panneau de propriétés, templates/scene_edit.html) dès qu'il est posé sur
|
|
// la scène, sans le moindre nœud de flow à poser — contrairement au reste
|
|
// du moteur, entièrement piloté par le graphe de logique. Réutilise
|
|
// heldKeys (déjà tenu à jour par bindHeldKeyTriggers, triggers.js) pour
|
|
// savoir quelles touches sont enfoncées, et
|
|
// applyObjectProperty/clampSceneObjectPosition/runSpriteAnimation/
|
|
// resolveSpriteFrames (actions.js) pour le déplacement/l'animation — même
|
|
// mécanique que "Modifier un objet de scène"/"Jouer une animation",
|
|
// jamais dupliquée.
|
|
|
|
var activePersonnageControllerInterval = null;
|
|
var activePersonnageInteractListener = null;
|
|
|
|
function forgePersonnageAnimExists(elementId, animName) {
|
|
var byAnim = (gameData.personnage_animations || {})[elementId];
|
|
return !!(byAnim && byAnim[animName] && byAnim[animName].length);
|
|
}
|
|
|
|
// "walk" est la SEULE animation de déplacement du catalogue Forge/CraftPix
|
|
// (voir screens/labels/sprite_library.py — aucune variante directionnelle
|
|
// walk_haut/walk_bas...) : le sens se voit via l'inversion miroir
|
|
// (orientation, déjà existante — Phase 7), pas une frame différente.
|
|
// Absente (personnage sans cette pose) : le déplacement continue sans
|
|
// animation plutôt que de planter, comme le reste du moteur.
|
|
function forgePersonnageMovementAnim(elementId) {
|
|
return forgePersonnageAnimExists(elementId, 'walk') ? 'walk' : null;
|
|
}
|
|
|
|
function forgePersonnageIdleAnim(elementId) {
|
|
return forgePersonnageAnimExists(elementId, 'idle') ? 'idle' : null;
|
|
}
|
|
|
|
function forgeStopPersonnageControllers() {
|
|
if (activePersonnageControllerInterval) {
|
|
clearInterval(activePersonnageControllerInterval);
|
|
activePersonnageControllerInterval = null;
|
|
}
|
|
if (activePersonnageInteractListener) {
|
|
window.removeEventListener('keydown', activePersonnageInteractListener);
|
|
activePersonnageInteractListener = null;
|
|
}
|
|
}
|
|
|
|
// 50ms (20 fois/seconde) — même cadence que runScreenHeldKeyTriggers
|
|
// (triggers.js), pour un déplacement perçu comme continu.
|
|
function forgeRunPersonnageControllerTick(screenData) {
|
|
(screenData.elements || []).forEach(function (obj) {
|
|
if (obj.kind !== 'personnage' || !obj.personnage_commandes) return;
|
|
var cmd = obj.personnage_commandes;
|
|
var targetEl = document.querySelector('[data-object-id="' + obj.id + '"]');
|
|
if (!targetEl) return;
|
|
|
|
var dx = 0, dy = 0;
|
|
if (cmd.axe !== 'vertical') {
|
|
if (heldKeys.has(cmd.gauche)) dx -= cmd.vitesse;
|
|
if (heldKeys.has(cmd.droite)) dx += cmd.vitesse;
|
|
}
|
|
if (cmd.axe !== 'horizontal') {
|
|
if (heldKeys.has(cmd.haut)) dy -= cmd.vitesse;
|
|
if (heldKeys.has(cmd.bas)) dy += cmd.vitesse;
|
|
}
|
|
|
|
if (dx === 0 && dy === 0) {
|
|
var idleAnim = forgePersonnageIdleAnim(obj.id);
|
|
if (idleAnim) runSpriteAnimation(targetEl, resolveSpriteFrames(obj.id, { animation: idleAnim, fps: 4, loop: true }));
|
|
return;
|
|
}
|
|
if (dx !== 0) {
|
|
applyObjectProperty(targetEl, 'orientation', dx < 0 ? 'gauche' : 'droite');
|
|
applyObjectProperty(targetEl, 'pos_x_relatif', dx);
|
|
}
|
|
if (dy !== 0) applyObjectProperty(targetEl, 'pos_y_relatif', dy);
|
|
var moveAnim = forgePersonnageMovementAnim(obj.id);
|
|
if (moveAnim) runSpriteAnimation(targetEl, resolveSpriteFrames(obj.id, { animation: moveAnim, fps: 8, loop: true }));
|
|
});
|
|
}
|
|
|
|
// Ponctuel (keydown, pas heldKeys) — contrairement au déplacement,
|
|
// "interagir" est une action instantanée (ouvrir un coffre, parler à un
|
|
// PNJ...), 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 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 }));
|
|
});
|
|
};
|
|
window.addEventListener('keydown', activePersonnageInteractListener);
|
|
}
|
|
|
|
// Même patron PAR ÉCRAN que runScreenHeldKeyTriggers/runScreenTimerTriggers
|
|
// (triggers.js/screens.js) : n'existe que tant que l'écran est affiché,
|
|
// réinitialisé à chaque changement d'écran (voir showScreen()).
|
|
function forgeStartPersonnageControllers(screenData) {
|
|
forgeStopPersonnageControllers();
|
|
if (!screenData || screenData.kind !== 'jeu_2d') return;
|
|
var hasPersonnage = (screenData.elements || []).some(function (o) { return o.kind === 'personnage'; });
|
|
if (!hasPersonnage) return;
|
|
activePersonnageControllerInterval = setInterval(function () { forgeRunPersonnageControllerTick(screenData); }, 50);
|
|
forgeStartPersonnageInteractListener(screenData);
|
|
}
|
|
|
|
// static/js/play/__tests__/ (node:test) — pas de risque en navigateur :
|
|
// `module` n'existe pas là-bas, cette branche ne s'exécute jamais côté
|
|
// client.
|
|
if (typeof module !== 'undefined' && module.exports) {
|
|
module.exports = {
|
|
forgePersonnageAnimExists, forgePersonnageMovementAnim, forgePersonnageIdleAnim,
|
|
forgeRunPersonnageControllerTick, forgeStartPersonnageControllers, forgeStopPersonnageControllers,
|
|
forgeStartPersonnageInteractListener,
|
|
};
|
|
}
|