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
+19
View File
@@ -113,6 +113,25 @@ test('clampSceneObjectPosition — borne au bord gauche/haut (jamais négatif)',
});
});
// world_width/world_height (voir personnage-controller.js::
// forgeUpdateSceneWorldSize) : demandé par l'utilisateur — un fond plus
// grand que la scène doit laisser le personnage se déplacer sur TOUT le
// fond, pas seulement le petit cadre visible de la scène d'origine.
test('clampSceneObjectPosition — borne au MONDE (world_width/height) quand un fond XXL l\'a agrandi', () => {
withScreensData({ id: 1, scene_width: 200, scene_height: 100, world_width: 2000, world_height: 1000 }, () => {
const el = { style: { width: '40px', height: '40px' } };
assert.equal(clampSceneObjectPosition(el, 'left', 500), 500); // pas encore au bord du MONDE
assert.equal(clampSceneObjectPosition(el, 'left', 5000), 1960); // 2000 - 40
});
});
test('clampSceneObjectPosition — sans world_width/height (aucun fond), retombe sur scene_width/height', () => {
withScreensData({ id: 1, scene_width: 200, scene_height: 100 }, () => {
const el = { style: { width: '40px', height: '40px' } };
assert.equal(clampSceneObjectPosition(el, 'left', 500), 160); // identique au tout premier test ci-dessus
});
});
test('applyObjectProperty — pos_x_relatif reste dans les limites de la scène après plusieurs pas', () => {
withScreensData({ id: 1, scene_width: 100, scene_height: 100 }, () => {
const el = { style: { left: '90px', top: '0px', width: '20px', height: '20px' } };
@@ -16,7 +16,7 @@ Object.assign(global, actionsMod);
const {
forgePersonnageAnimExists, forgePersonnageMovementAnim, forgePersonnageIdleAnim, forgePersonnageActionAnim,
forgeRunPersonnageControllerTick,
forgeRunPersonnageControllerTick, forgeUpdateSceneWorldSize, forgeUpdateSceneCamera,
} = require('../personnage-controller.js');
function fakeSceneEl() {
@@ -33,7 +33,11 @@ function fakeSceneEl() {
function withPersonnageObject(obj, elByObjectId, fn) {
const previousDoc = global.document;
global.document = { querySelector: sel => elByObjectId[sel.match(/"(\d+)"/)[1]] };
// forgeRunPersonnageControllerTick appelle aussi forgeUpdateSceneCamera
// (cherche ".sceneWorld", jamais présente dans elByObjectId ici) — un
// sélecteur qui ne matche aucun id numérique renvoie undefined plutôt
// que de lever, comme un vrai document.querySelector sans résultat.
global.document = { querySelector: sel => { const m = sel.match(/"(\d+)"/); return m ? elByObjectId[m[1]] : undefined; } };
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'] } } };
@@ -137,3 +141,84 @@ test('forgePersonnageActionAnim — "interagir" prioritaire, sinon la touche sup
test('forgePersonnageActionAnim — commandes sans "extra" (personnage posé avant l\'ajout du réglage)', () => {
assert.equal(forgePersonnageActionAnim({ interagir: 'e' }, 'j'), null);
});
// Caméra qui suit le personnage derrière un fond plus grand que la scène
// (voir screens/scenes/add_scene_object.py, kind="fond") — demandé par
// l'utilisateur : "si l'image de fond est plus grande que la scène, la
// caméra devra défiler en suivant le joueur".
function fakeWorldEl() {
return { style: {} };
}
test('forgeUpdateSceneWorldSize — le monde reste la scène tant qu\'aucun fond ne dépasse (comportement inchangé)', () => {
const screenData = { id: 1, scene_width: 500, scene_height: 500, elements: [{ kind: 'decor', x: 10, y: 10, width: 50, height: 50 }] };
const world = forgeUpdateSceneWorldSize(screenData);
assert.deepEqual(world, { width: 500, height: 500 });
assert.equal(screenData.world_width, 500);
assert.equal(screenData.world_height, 500);
});
test('forgeUpdateSceneWorldSize — un fond plus grand agrandit le monde jusqu\'à ses propres bords', () => {
const screenData = { id: 1, scene_width: 500, scene_height: 500, elements: [{ kind: 'fond', x: 0, y: 0, width: 3840, height: 2160 }] };
const world = forgeUpdateSceneWorldSize(screenData);
assert.deepEqual(world, { width: 3840, height: 2160 });
});
test('forgeUpdateSceneCamera — sans fond XXL, transform vidé (aucun effet, comportement d\'avant)', () => {
const worldEl = fakeWorldEl();
const previousDoc = global.document;
global.document = { querySelector: () => worldEl };
const screenData = { id: 1, scene_width: 500, scene_height: 500, elements: [] };
try {
forgeUpdateSceneCamera(screenData);
assert.equal(worldEl.style.width, '500px');
assert.equal(worldEl.style.height, '500px');
assert.equal(worldEl.style.transform, '');
} finally { global.document = previousDoc; }
});
test('forgeUpdateSceneCamera — centre le monde sur le personnage, borné aux bords du monde', () => {
const worldEl = fakeWorldEl();
const playerEl = { style: { left: '1900px', top: '1000px', width: '40px', height: '60px' } };
const previousDoc = global.document;
global.document = {
querySelector: sel => (sel.indexOf('sceneWorld') !== -1 ? worldEl : playerEl),
};
const screenData = {
id: 1, scene_width: 500, scene_height: 500,
elements: [
{ kind: 'fond', x: 0, y: 0, width: 3840, height: 2160 },
{ id: 42, kind: 'personnage' },
],
};
try {
forgeUpdateSceneCamera(screenData);
assert.equal(worldEl.style.width, '3840px');
assert.equal(worldEl.style.height, '2160px');
// Centre du joueur : 1900+20=1920, 1000+30=1030 -> décalage caméra
// 1920-250=1670, 1030-250=780 (aucune des deux bornes atteinte ici).
assert.equal(worldEl.style.transform, 'translate(-1670px, -780px)');
} finally { global.document = previousDoc; }
});
test('forgeUpdateSceneCamera — ne montre jamais au-delà des bords du monde (bornage)', () => {
const worldEl = fakeWorldEl();
const playerEl = { style: { left: '3820px', top: '0px', width: '20px', height: '20px' } }; // tout au bord droit
const previousDoc = global.document;
global.document = {
querySelector: sel => (sel.indexOf('sceneWorld') !== -1 ? worldEl : playerEl),
};
const screenData = {
id: 1, scene_width: 500, scene_height: 500,
elements: [
{ kind: 'fond', x: 0, y: 0, width: 3840, height: 2160 },
{ id: 42, kind: 'personnage' },
],
};
try {
forgeUpdateSceneCamera(screenData);
// max offsetX = 3840 - 500 = 3340 ; jamais plus, même si le joueur est
// encore plus près du bord (pas de bande vide visible au-delà du monde).
assert.equal(worldEl.style.transform, 'translate(-3340px, 0px)');
} finally { global.document = previousDoc; }
});