Image de fond de scène + caméra qui suit le personnage, fps d'animation cohérent
Build and deploy / test-python (push) Successful in 6m26s
Build and deploy / test-js (push) Successful in 57s
Build and deploy / build-and-push (push) Skipped
Build and deploy / deploy (push) Skipped

Trois retours utilisateur :
- fps d'idle/interagir/touches supplémentaires alignés sur celui de la
  marche (8 i/s partout, était 4 pour idle — perçu comme "les autres
  animations sont lentes à côté de la marche").
- Nouveau menu "🏞️ Images de fond" dans "Objets de cette scène" : pose
  un objet kind="fond" à la taille RÉELLE de l'image choisie (pack
  CraftPix intégré en galerie, admin seulement — licence, voir
  core/sprite_gate.py), derrière tout le reste, insensible au clic.
- Si ce fond dépasse la scène, elle devient le "monde" : .playScreen.
  playScene est désormais le viewport (overflow:hidden, taille fixe),
  .sceneWorld le monde à l'intérieur — la caméra centre le premier
  personnage trouvé, bornée pour ne jamais montrer au-delà des bords
  (voir personnage-controller.js::forgeUpdateSceneCamera). Le personnage
  peut désormais se déplacer sur tout le monde, pas seulement le petit
  cadre visible (clampSceneObjectPosition, actions.js). Un jeu sans fond
  XXL garde un comportement strictement identique à avant (monde ==
  scène, transform vide).

Bibliothèque générée une fois par scripts/generate_background_manifest.py
(assets/background/, non versionné, licence CraftPix) vers
static/backgrounds/ (committé), même patron que
generate_animal_sprite_manifest.py.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
william
2026-09-02 12:24:10 +02:00
co-authored by Claude Sonnet 5
parent 5891c82c62
commit 9199897784
24 changed files with 581 additions and 33 deletions
+71 -5
View File
@@ -14,6 +14,12 @@
var activePersonnageControllerInterval = null;
var activePersonnageInteractListener = null;
// Un seul rythme pour TOUTES les poses (marche, idle, interagir, touches
// supplémentaires) — les désynchroniser (ex. idle plus lent que walk)
// n'a aucune justification et se voit immédiatement au jeu (bug signalé
// par l'utilisateur).
var FORGE_PERSONNAGE_FPS = 8;
function forgePersonnageAnimExists(elementId, animName) {
var byAnim = (gameData.personnage_animations || {})[elementId];
return !!(byAnim && byAnim[animName] && byAnim[animName].length);
@@ -33,6 +39,53 @@ function forgePersonnageIdleAnim(elementId) {
return forgePersonnageAnimExists(elementId, 'idle') ? 'idle' : null;
}
// Taille du "monde" (voir static/style.css .sceneWorld) : la scène elle-
// même par défaut, agrandie si un objet kind="fond" (voir
// add_scene_object.py) dépasse ses bords — un jeu sans fond XXL garde
// donc EXACTEMENT son comportement d'avant (monde == scène). Mémorisée
// directement sur screenData (déjà l'objet partagé avec screensData,
// voir static/js/play/actions.js) pour que clampSceneObjectPosition
// borne un déplacement au monde entier, pas seulement au petit cadre
// visible de la scène.
function forgeUpdateSceneWorldSize(screenData) {
var width = screenData.scene_width, height = screenData.scene_height;
(screenData.elements || []).forEach(function (o) {
if (o.kind !== 'fond') return;
width = Math.max(width, (o.x || 0) + (o.width || 0));
height = Math.max(height, (o.y || 0) + (o.height || 0));
});
screenData.world_width = width;
screenData.world_height = height;
return { width: width, height: height };
}
// Caméra qui suit le personnage (le premier objet kind="personnage"
// trouvé — aucune notion de "joueur" distincte n'existe ailleurs dans le
// moteur 2D, voir personnage-controller.js dans son ensemble) : centre le
// monde sur lui, borné pour ne jamais montrer au-delà de ses bords (même
// principe que clampSceneObjectPosition, actions.js, mais pour la caméra
// plutôt qu'un objet). Sans effet (transform vidé) tant que le monde ne
// dépasse pas la scène — c'est ce qui garde le comportement identique à
// avant pour tout jeu sans fond plus grand que sa scène.
function forgeUpdateSceneCamera(screenData) {
var worldEl = document.querySelector('.playScreen[data-screen-id="' + screenData.id + '"] .sceneWorld');
if (!worldEl) return;
var world = forgeUpdateSceneWorldSize(screenData);
worldEl.style.width = world.width + 'px';
worldEl.style.height = world.height + 'px';
if (world.width <= screenData.scene_width && world.height <= screenData.scene_height) {
worldEl.style.transform = '';
return;
}
var player = (screenData.elements || []).find(function (o) { return o.kind === 'personnage'; });
var playerEl = player && document.querySelector('[data-object-id="' + player.id + '"]');
var px = playerEl ? (parseFloat(playerEl.style.left) || 0) + (parseFloat(playerEl.style.width) || 0) / 2 : world.width / 2;
var py = playerEl ? (parseFloat(playerEl.style.top) || 0) + (parseFloat(playerEl.style.height) || 0) / 2 : world.height / 2;
var offsetX = Math.min(Math.max(px - screenData.scene_width / 2, 0), Math.max(0, world.width - screenData.scene_width));
var offsetY = Math.min(Math.max(py - screenData.scene_height / 2, 0), Math.max(0, world.height - screenData.scene_height));
worldEl.style.transform = 'translate(' + (-offsetX) + 'px, ' + (-offsetY) + 'px)';
}
function forgeStopPersonnageControllers() {
if (activePersonnageControllerInterval) {
clearInterval(activePersonnageControllerInterval);
@@ -47,6 +100,7 @@ function forgeStopPersonnageControllers() {
// 50ms (20 fois/seconde) — même cadence que runScreenHeldKeyTriggers
// (triggers.js), pour un déplacement perçu comme continu.
function forgeRunPersonnageControllerTick(screenData) {
forgeUpdateSceneCamera(screenData);
(screenData.elements || []).forEach(function (obj) {
if (obj.kind !== 'personnage' || !obj.personnage_commandes) return;
var cmd = obj.personnage_commandes;
@@ -63,9 +117,13 @@ function forgeRunPersonnageControllerTick(screenData) {
if (heldKeys.has(cmd.bas)) dy += cmd.vitesse;
}
// Même fps pour idle et walk (bug signalé par l'utilisateur : idle
// tournait à 4 i/s contre 8 pour walk, perçue comme "les autres
// animations sont lentes à côté de la marche" — aucune raison réelle
// de les désynchroniser).
if (dx === 0 && dy === 0) {
var idleAnim = forgePersonnageIdleAnim(obj.id);
if (idleAnim) runSpriteAnimation(targetEl, resolveSpriteFrames(obj.id, { animation: idleAnim, fps: 4, loop: true }));
if (idleAnim) runSpriteAnimation(targetEl, resolveSpriteFrames(obj.id, { animation: idleAnim, fps: FORGE_PERSONNAGE_FPS, loop: true }));
return;
}
if (dx !== 0) {
@@ -74,7 +132,7 @@ function forgeRunPersonnageControllerTick(screenData) {
}
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 }));
if (moveAnim) runSpriteAnimation(targetEl, resolveSpriteFrames(obj.id, { animation: moveAnim, fps: FORGE_PERSONNAGE_FPS, loop: true }));
});
}
@@ -100,7 +158,7 @@ function forgeStartPersonnageInteractListener(screenData) {
if (!animName || !forgePersonnageAnimExists(obj.id, animName)) return;
var targetEl = document.querySelector('[data-object-id="' + obj.id + '"]');
if (!targetEl) return;
runSpriteAnimation(targetEl, resolveSpriteFrames(obj.id, { animation: animName, fps: 8, loop: false }));
runSpriteAnimation(targetEl, resolveSpriteFrames(obj.id, { animation: animName, fps: FORGE_PERSONNAGE_FPS, loop: false }));
});
};
window.addEventListener('keydown', activePersonnageInteractListener);
@@ -112,7 +170,15 @@ function forgeStartPersonnageInteractListener(screenData) {
function forgeStartPersonnageControllers(screenData) {
forgeStopPersonnageControllers();
if (!screenData || screenData.kind !== 'jeu_2d') return;
var hasPersonnage = (screenData.elements || []).some(function (o) { return o.kind === 'personnage'; });
var elements = screenData.elements || [];
var hasPersonnage = elements.some(function (o) { return o.kind === 'personnage'; });
var hasFond = elements.some(function (o) { return o.kind === 'fond'; });
// Appliqué tout de suite (avant le premier tick de l'intervalle, 50ms
// plus tard) pour éviter un flash d'une frame à la taille/caméra par
// défaut. Un fond seul (sans personnage) n'a besoin que de CET appel
// ponctuel — rien à suivre en continu ; un personnage, lui, a besoin du
// tick régulier pour le déplacement continu au clavier.
if (hasFond) forgeUpdateSceneCamera(screenData);
if (!hasPersonnage) return;
activePersonnageControllerInterval = setInterval(function () { forgeRunPersonnageControllerTick(screenData); }, 50);
forgeStartPersonnageInteractListener(screenData);
@@ -125,6 +191,6 @@ if (typeof module !== 'undefined' && module.exports) {
module.exports = {
forgePersonnageAnimExists, forgePersonnageMovementAnim, forgePersonnageIdleAnim, forgePersonnageActionAnim,
forgeRunPersonnageControllerTick, forgeStartPersonnageControllers, forgeStopPersonnageControllers,
forgeStartPersonnageInteractListener,
forgeStartPersonnageInteractListener, forgeUpdateSceneWorldSize, forgeUpdateSceneCamera,
};
}