Refonte : vrai widget "Personnage" visuel (remplace l'UI de la Phase 7)
L'utilisateur a testé la Phase 7 (sprites pilotés via un menu déroulant + zone de texte dans la logique de flow) et l'a rejetée à raison : ce n'est pas comme ça qu'un moteur de jeu (Phaser, Unity) gère un personnage. Cette refonte remplace tout le flux d'AUTEURING par un vrai widget visuel — le moteur d'exécution de la Phase 7 (runSpriteAnimation, activeSpriteAnimations, orientation, kind="sprite" de la Timeline) reste inchangé. Nouveau widget "personnage" (screens/widgets/registry.py) : - Rendu serveur dédié (special_render, screens/rendering/render_personnage.py) qui affiche la pose "idle" dès le HTML généré — jamais une image cassée à configurer après coup. - Toutes ses données (source Forge ou sprites propres au créateur, quel personnage/quelles animations) vivent dans une seule clé JSON _personnage_data (screens/rendering/personnage_data.py), sans aucune migration de schéma (même patron que c_clause_list). - Nouvelle échappatoire "custom_panel", symétrique à "special_render" mais pour le panneau de propriétés : ce widget affiche une galerie/un import de sprites sur-mesure plutôt que les contrôles génériques. Galerie visuelle de personnages Forge (VRAIES miniatures, pas un emoji) : - Panneau gauche "🎭 Personnages" : pose un personnage déjà configuré, animé immédiatement. - Panneau droit (propriétés) : change le personnage Forge de l'élément sélectionné, ou importe les animations d'un sprite personnalisé. - static/js/screen_edit/personnage-preview.js : lance l'aperçu animé de CHAQUE personnage du canevas dès le chargement de la page et après toute sauvegarde — le cœur de la demande ("je dois voir ça bouger"). Sélecteur d'animation à miniatures (nœud de flow "Jouer une animation" et clip de Timeline "sprite") : remplace le menu déroulant/la zone de texte par une grille de vraies miniatures, scopée aux SEULES animations du personnage réellement ciblé (ELEMENT_ANIMATIONS_MAP, résolu côté serveur à partir des propriétés de cet élément précis) — jamais un catalogue global. Le format stocké sur le nœud/le clip ({frames, fps, loop}) est inchangé, seule l'UI d'édition change. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
d2c2f20b65
commit
9e076f8cf9
@@ -19,6 +19,7 @@ function animUrl(path) { return "/game/" + GAME_SLUG + path; }
|
||||
|
||||
function animClipLabel(clip) {
|
||||
if (clip.kind === 'custom') return '🎨 Personnalisée';
|
||||
if (clip.kind === 'sprite') return '🏃 Sprite';
|
||||
return '🎞️ ' + (ANIMATE_CSS_LABELS[clip.animate_name] || clip.animate_name);
|
||||
}
|
||||
|
||||
@@ -171,31 +172,39 @@ function toggleAnimClipKind() {
|
||||
document.getElementById('anim-clip-animate-css-fields').style.display = kind === 'animate_css' ? 'block' : 'none';
|
||||
document.getElementById('anim-clip-custom-fields').style.display = kind === 'custom' ? 'block' : 'none';
|
||||
document.getElementById('anim-clip-sprite-fields').style.display = kind === 'sprite' ? 'block' : 'none';
|
||||
if (kind === 'sprite') populateAnimClipSpriteGrid();
|
||||
}
|
||||
|
||||
// Phase 7 — même mécanisme que onSpriteLibraryCharacterChange()/
|
||||
// applySpriteLibraryAnimation() dans flow-editor.js, pour le formulaire de
|
||||
// clip de Timeline (DOM distinct, pas d'état partagé entre les deux formulaires).
|
||||
function onAnimClipSpriteLibraryCharacterChange() {
|
||||
var charSel = document.getElementById('anim-clip-sprite-library-character');
|
||||
var animSel = document.getElementById('anim-clip-sprite-library-animation');
|
||||
var slug = charSel.value;
|
||||
animSel.innerHTML = '';
|
||||
if (!slug || !SPRITE_LIBRARY[slug]) { animSel.style.display = 'none'; return; }
|
||||
Object.keys(SPRITE_LIBRARY[slug].animations).forEach(function(name) {
|
||||
var opt = document.createElement('option');
|
||||
opt.value = name; opt.textContent = name;
|
||||
animSel.appendChild(opt);
|
||||
// Phase 8 — mirror de onSpriteTargetElementChange() (flow-editor.js), mais
|
||||
// scopé au personnage PROPRIÉTAIRE de ce clip (clip.element_id, fixé à la
|
||||
// création — voir addAnimTrack()) : pas de sélecteur d'élément séparé ici,
|
||||
// contrairement au nœud de flow (une action peut viser n'importe quel
|
||||
// personnage, un clip appartient à un seul élément dès sa création).
|
||||
function populateAnimClipSpriteGrid(preselectedName) {
|
||||
var grid = document.getElementById('anim-clip-sprite-animation-grid');
|
||||
var nameField = document.getElementById('anim-clip-sprite-animation-name');
|
||||
var noAnimHint = document.getElementById('anim-clip-sprite-no-animations');
|
||||
grid.innerHTML = '';
|
||||
var clip = ANIM_CLIPS.find(function(c) { return c.id === animCurrentClipId; });
|
||||
var animations = clip ? (ELEMENT_ANIMATIONS_MAP[clip.element_id] || {}) : {};
|
||||
var hasAnimations = Object.keys(animations).length > 0;
|
||||
noAnimHint.style.display = hasAnimations ? 'none' : 'block';
|
||||
Object.keys(animations).forEach(function(animName) {
|
||||
var frames = animations[animName];
|
||||
if (!frames.length) return;
|
||||
var tile = document.createElement('button');
|
||||
tile.type = 'button';
|
||||
tile.className = 'iconGalleryTile' + (animName === preselectedName ? ' selected' : '');
|
||||
tile.title = animName;
|
||||
tile.innerHTML = '<img src="' + frames[0] + '" alt="" class="personnageThumb">';
|
||||
tile.onclick = function() {
|
||||
grid.querySelectorAll('.iconGalleryTile').forEach(function(t) { t.classList.remove('selected'); });
|
||||
tile.classList.add('selected');
|
||||
nameField.value = animName;
|
||||
};
|
||||
grid.appendChild(tile);
|
||||
});
|
||||
animSel.style.display = 'block';
|
||||
applyAnimClipSpriteLibraryAnimation();
|
||||
}
|
||||
|
||||
function applyAnimClipSpriteLibraryAnimation() {
|
||||
var slug = document.getElementById('anim-clip-sprite-library-character').value;
|
||||
var name = document.getElementById('anim-clip-sprite-library-animation').value;
|
||||
if (!slug || !name || !SPRITE_LIBRARY[slug]) return;
|
||||
document.getElementById('anim-clip-sprite-frames').value = SPRITE_LIBRARY[slug].animations[name].join('\n');
|
||||
if (preselectedName && animations[preselectedName]) nameField.value = preselectedName;
|
||||
}
|
||||
|
||||
function addAnimKeyframeRow(percent, cssText) {
|
||||
@@ -260,15 +269,25 @@ function openAnimClipForm(clip) {
|
||||
var cssText = Object.keys(k.props || {}).map(function(p) { return p + ': ' + k.props[p] + ';'; }).join(' ');
|
||||
addAnimKeyframeRow(k.percent, cssText);
|
||||
});
|
||||
// Phase 7 — clip.custom_keyframes stocke {frames, fps} en JSON pour un
|
||||
// clip kind="sprite" (pas une liste d'images-clés CSS, voir ci-dessus).
|
||||
// Phase 8 — clip.custom_keyframes stocke {frames, fps} en JSON pour un
|
||||
// clip kind="sprite" (pas une liste d'images-clés CSS, voir ci-dessus) ;
|
||||
// le NOM de l'animation, lui, n'est pas stocké (seules ses frames le
|
||||
// sont) — retrouvé ici par comparaison avec les animations actuelles du
|
||||
// personnage propriétaire, pour présélectionner la bonne miniature.
|
||||
var spriteData = {};
|
||||
if (clip.kind === 'sprite' && clip.custom_keyframes) {
|
||||
try { spriteData = JSON.parse(clip.custom_keyframes); } catch (e) { /* ignore */ }
|
||||
}
|
||||
document.getElementById('anim-clip-sprite-frames').value = (spriteData.frames || []).join('\n');
|
||||
document.getElementById('anim-clip-sprite-fps').value = spriteData.fps || 8;
|
||||
toggleAnimClipKind();
|
||||
if (clip.kind === 'sprite') {
|
||||
var ownerAnimations = ELEMENT_ANIMATIONS_MAP[clip.element_id] || {};
|
||||
var matchedName = Object.keys(ownerAnimations).find(function(name) {
|
||||
var f = ownerAnimations[name];
|
||||
return spriteData.frames && f.length === spriteData.frames.length && f.every(function(u, i) { return u === spriteData.frames[i]; });
|
||||
}) || null;
|
||||
populateAnimClipSpriteGrid(matchedName);
|
||||
}
|
||||
document.querySelectorAll('.animClip').forEach(function(b) {
|
||||
b.classList.toggle('selected', parseInt(b.dataset.clipId, 10) === clip.id);
|
||||
});
|
||||
@@ -294,14 +313,16 @@ function saveAnimClipForm() {
|
||||
};
|
||||
if (kind === 'custom') fields.custom_keyframes = collectAnimKeyframes();
|
||||
if (kind === 'sprite') {
|
||||
// Phase 7 — {frames, fps} en JSON dans custom_keyframes (colonne
|
||||
// générique déjà réutilisée par le kind "custom" ci-dessus pour ses
|
||||
// images-clés) ; la boucle est déjà portée par iteration_count
|
||||
// (1/2/3/infinite, ci-dessus) — aucun champ dédié nécessaire.
|
||||
var frames = document.getElementById('anim-clip-sprite-frames').value
|
||||
.split('\n').map(function(s) { return s.trim(); }).filter(Boolean);
|
||||
// Phase 8 — l'animation choisie (miniature cliquée, voir
|
||||
// populateAnimClipSpriteGrid()) est résolue en liste de frames ICI ;
|
||||
// {frames, fps} stocké dans custom_keyframes reste inchangé depuis la
|
||||
// Phase 7 (moteur d'exécution intact, seule l'UI a changé) — la boucle
|
||||
// reste portée par iteration_count (1/2/3/infinite, ci-dessus).
|
||||
var clipForFields = ANIM_CLIPS.find(function(c) { return c.id === animCurrentClipId; });
|
||||
var ownerAnimations = clipForFields ? (ELEMENT_ANIMATIONS_MAP[clipForFields.element_id] || {}) : {};
|
||||
var animName = document.getElementById('anim-clip-sprite-animation-name').value;
|
||||
fields.custom_keyframes = {
|
||||
frames: frames,
|
||||
frames: ownerAnimations[animName] || [],
|
||||
fps: parseInt(document.getElementById('anim-clip-sprite-fps').value, 10) || 8,
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user