// ---------- 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, }; }