Refonte : vrai widget "Personnage" visuel (remplace l'UI de la Phase 7)
Build and deploy / test-python (push) Successful in 1m26s
Build and deploy / test-js (push) Successful in 6s
Build and deploy / build-and-push (push) Skipped
Build and deploy / deploy (push) Skipped

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:
william
2026-08-30 20:26:39 +02:00
co-authored by Claude Sonnet 5
parent d2c2f20b65
commit 9e076f8cf9
21 changed files with 660 additions and 159 deletions
+99 -46
View File
@@ -40,6 +40,27 @@
</div>
{% endmacro %}
<!-- Phase 8 — galerie visuelle de personnages Forge (VRAIES miniatures,
pas un emoji/icône), réutilisée deux fois : "add" pour poser un
nouveau personnage (panneau gauche), "swap" pour changer le
personnage Forge d'un élément déjà posé (panneau droit, propriétés). -->
{% macro render_personnage_gallery(mode, gallery_id, selected_forge_character=None) %}
<div class="iconGallery" id="{{ gallery_id }}">
{% for char_slug, character in sprite_library.items() %}
<button type="button" class="iconGalleryTile {{ 'selected' if char_slug == selected_forge_character else '' }}"
title="{{ character.label }}"
onclick="{{ 'addPersonnageElement' if mode == 'add' else 'swapPersonnageCharacter' }}('{{ char_slug }}')">
<img src="/static/characters/{{ char_slug }}/idle.png" alt="{{ character.label }}" class="personnageThumb">
</button>
{% endfor %}
{% if mode == 'add' %}
<button type="button" class="iconGalleryTile" title="Sprite personnalisé" onclick="addPersonnageElement('__custom__')">
<span style="font-size:28px;">➕</span>
</button>
{% endif %}
</div>
{% endmacro %}
<div class="builder3" id="builder3">
{% set el = elements|selectattr("id", "equalto", selected_id)|first if selected_id else none %}
<!-- Panneau gauche : ajouter un élément (icônes, sans jargon technique) —
@@ -102,7 +123,7 @@
<summary>Ajouter un élément sur l'écran</summary>
<div class="propGroupBody">
<div class="widgetGrid">
{% for wkey, w in widgets.items() if wkey != 'icone' %}
{% for wkey, w in widgets.items() if wkey != 'icone' and wkey != 'personnage' %}
<form method="post" action="{{ url_for('element_add', slug=game.slug, screen_id=screen.id) }}" class="widgetBtnForm">
<input type="hidden" name="widget" value="{{ wkey }}">
<button type="submit" class="widgetBtn" title="{{ w.label }}">
@@ -136,6 +157,21 @@
</div>
</details>
<!-- Phase 8 — galerie de personnages Forge (widget "personnage" dédié,
voir screens/widgets/registry.py) : cliquer une tuile pose un
personnage DÉJÀ configuré (Kenney CC0), animé immédiatement dans
le canevas (voir personnage-preview.js) — pas un élément générique
à configurer ensuite dans la logique. "➕ Sprite personnalisé" pose
un personnage vide dont les animations s'importent dans ses
propriétés (panneau droit). -->
<details class="propGroup">
<summary>🎭 Personnages</summary>
<div class="propGroupBody">
{{ render_personnage_gallery('add', 'personnageGalleryAdd') }}
<p class="help">Personnage prêt à l'emploi (Kenney, licence libre CC0) ou "➕" pour un sprite personnalisé — importe ensuite tes propres images d'animation dans ses propriétés.</p>
</div>
</details>
<!-- Menu rétractable dédié aux éléments de jeu personnalisés (Personnage,
Outil, IA...) : chacun a désormais SA PROPRE tuile, avec son nom,
plutôt qu'une unique tuile générique "Élément de jeu" qui ajoutait
@@ -505,36 +541,26 @@
</div>
</div>
<!-- Phase 7 — personnage animé par sprites : joue une séquence de
PNG (cycle en boucle type marche, ou une fois type saut) sur
l'élément visé, depuis la bibliothèque Forge ou des sprites
propres au créateur (même bouton d'upload que le son ci-dessus,
mais ajoute une ligne plutôt que de remplacer le champ). -->
<!-- Phase 8 — personnage animé par sprites : joue une des
animations du PERSONNAGE CIBLÉ (miniatures réelles, jamais un
menu déroulant ni une zone d'URLs en texte) — seuls les
éléments widget "personnage" sont proposés ici, leurs
animations viennent de leurs propres propriétés (galerie
Forge ou sprites importés, voir personnage-panel.js). -->
<div id="nf-action-sprite-fields" style="display:none;">
<label>Élément à animer
<select id="nf-action-sprite-target-element">
{% for e2 in elements %}
<label>Personnage à animer
<select id="nf-action-sprite-target-element" onchange="onSpriteTargetElementChange()">
<option value="">— Choisir —</option>
{% for e2 in elements if e2.widget == 'personnage' %}
<option value="{{ e2.id }}">{{ e2|elabel }} (elt-{{ e2.id }})</option>
{% endfor %}
</select>
</label>
<label style="margin-top:6px; display:block;">Personnage Forge (optionnel)
<select id="nf-action-sprite-library-character" onchange="onSpriteLibraryCharacterChange()">
<option value="">— Choisir —</option>
{% for slug, character in sprite_library.items() %}
<option value="{{ slug }}">{{ character.label }}</option>
{% endfor %}
</select>
</label>
<select id="nf-action-sprite-library-animation" style="margin-top:6px; display:none;" onchange="applySpriteLibraryAnimation()"></select>
<label style="margin-top:10px; display:block;">Images (une URL par ligne)
<textarea id="nf-action-sprite-frames" rows="4" style="width:100%;" placeholder="https://.../frame1.png"></textarea>
</label>
<div style="margin-top:6px;">
<label for="nf-action-sprite-upload" style="font-size:12px;">Ajouter un fichier :</label>
<input type="file" id="nf-action-sprite-upload" accept="image/*" style="font-size:12px;">
<span id="nf-action-sprite-upload-status" class="uploadStatus" style="font-size:12px; color:var(--text-dim);"></span>
</div>
{% if not elements|selectattr('widget', 'equalto', 'personnage')|list %}
<p class="hint">Aucun personnage sur cet écran — ajoute-en un depuis la galerie "🎭 Personnages" du panneau de gauche.</p>
{% endif %}
<div id="nf-action-sprite-animation-grid" class="iconGallery" style="margin-top:6px;"></div>
<input type="hidden" id="nf-action-sprite-animation-name" value="">
<div class="fieldRow" style="margin-top:6px;">
<label style="flex:1;">Images/seconde
<input type="number" id="nf-action-sprite-fps" min="1" step="1" value="8">
@@ -638,27 +664,15 @@
<button type="button" class="smallBtn button is-small" onclick="addAnimKeyframeRow()">+ Ajouter une image-clé</button>
</div>
<!-- Phase 7 — même champs que le nœud de flow "Jouer une animation
de sprites" (voir plus haut #nf-action-sprite-fields), formulaire
indépendant : bibliothèque Forge ou sprites propres au créateur. -->
<!-- Phase 8 — même principe que le nœud de flow "Jouer une
animation de sprites" (voir plus haut #nf-action-sprite-fields) :
miniatures réelles des SEULES animations du personnage propriétaire
de ce clip (clip.element_id, fixé à la création du clip — voir
addAnimTrack()), jamais un menu déroulant ni une zone d'URLs. -->
<div id="anim-clip-sprite-fields" style="display:none;">
<label>Personnage Forge (optionnel)
<select id="anim-clip-sprite-library-character" onchange="onAnimClipSpriteLibraryCharacterChange()">
<option value="">— Choisir —</option>
{% for slug, character in sprite_library.items() %}
<option value="{{ slug }}">{{ character.label }}</option>
{% endfor %}
</select>
</label>
<select id="anim-clip-sprite-library-animation" style="margin-top:6px; display:none;" onchange="applyAnimClipSpriteLibraryAnimation()"></select>
<label style="margin-top:10px; display:block;">Images (une URL par ligne)
<textarea id="anim-clip-sprite-frames" rows="4" style="width:100%;" placeholder="https://.../frame1.png"></textarea>
</label>
<div style="margin-top:6px;">
<label for="anim-clip-sprite-upload" style="font-size:12px;">Ajouter un fichier :</label>
<input type="file" id="anim-clip-sprite-upload" accept="image/*" style="font-size:12px;">
<span id="anim-clip-sprite-upload-status" class="uploadStatus" style="font-size:12px; color:var(--text-dim);"></span>
</div>
<p class="hint" id="anim-clip-sprite-no-animations" style="display:none;">Cet élément n'est pas un personnage (widget "Personnage"), ou n'a encore aucune animation.</p>
<div id="anim-clip-sprite-animation-grid" class="iconGallery"></div>
<input type="hidden" id="anim-clip-sprite-animation-name" value="">
<label style="margin-top:6px; display:block;">Images/seconde
<input type="number" id="anim-clip-sprite-fps" min="1" step="1" value="8">
</label>
@@ -883,6 +897,42 @@
</details>
{% endif %}
{% if el.widget == 'personnage' %}
<!-- Phase 8 — panneau sur-mesure (custom_panel, voir registry.py) :
pas de controls génériques, juste la source (Forge/personnalisée)
et son contenu. Sauvegarde en AJAX (personnage-panel.js), jamais
via #propsForm (générique, utilisé par tous les autres widgets). -->
<details class="propGroup" open>
<summary>🎭 Personnage</summary>
<div class="propGroupBody" id="personnagePanelBody" data-element-id="{{ el.id }}" data-source="{{ personnage_data.source }}">
{% if personnage_data.source == 'custom' %}
<p class="help">Sprite personnalisé — importe les images de chaque animation ("idle" recommandée, affichée par défaut).</p>
<div id="personnageAnimationsList">
{% for anim_name, frames in (personnage_data.animations or {}).items() %}
<div class="personnageAnimRow box" data-anim-name="{{ anim_name }}" style="margin-bottom:8px; padding:8px;">
<div class="fieldRow" style="align-items:center; gap:6px;">
<input type="text" class="input is-small personnageAnimName" value="{{ anim_name }}" placeholder="ex. idle, marche, saut" style="flex:1;">
<button type="button" class="button is-small is-danger" onclick="removePersonnageAnimation(this)">🗑</button>
</div>
<div class="personnageAnimFrames" style="display:flex; flex-wrap:wrap; gap:4px; margin-top:6px;">
{% for url in frames %}
<img src="{{ url }}" alt="" class="personnageFrameThumb" style="width:32px; height:32px; object-fit:contain; background:var(--panel2); border-radius:4px;">
{% endfor %}
</div>
<input type="file" class="personnageFrameUpload input is-small" accept="image/*" multiple style="margin-top:6px;">
<span class="uploadStatus" style="font-size:12px; color:var(--text-dim);"></span>
</div>
{% endfor %}
</div>
<button type="button" class="button is-small is-fullwidth" onclick="addPersonnageAnimation()">+ Ajouter une animation</button>
{% else %}
<p class="help">Personnage Forge — clique un autre pour en changer.</p>
{{ render_personnage_gallery('swap', 'personnageGallerySwap', personnage_data.forge_character) }}
{% endif %}
</div>
</details>
{% endif %}
{% set size_ctrl_keys = ['child-width', 'child-height'] %}
{% set size_ctrls = {} %}
{% for c in selected_controls if c.key in size_ctrl_keys %}{% set _ = size_ctrls.update({c.key: c}) %}{% endfor %}
@@ -1218,6 +1268,7 @@
var ELEMENT_DESACTIVE_LABELS_MAP = {{ element_desactive_labels_json|safe }};
var ELEMENT_ORIENTATION_LABELS_MAP = {{ element_orientation_labels_json|safe }};
var SPRITE_LIBRARY = {{ sprite_library_json|safe }};
var ELEMENT_ANIMATIONS_MAP = {{ element_animations_map_json|safe }};
var DATA_OPERATION_LABELS_MAP = {{ data_operation_labels_json|safe }};
var CONDITION_OPERATOR_LABELS_MAP = {{ condition_operator_labels_json|safe }};
var TRIGGER_LABELS_MAP = {{ trigger_labels_json|safe }};
@@ -1227,9 +1278,11 @@
</script>
<script src="{{ url_for('static', filename='js/screen_edit/tree-panels.js') }}"></script>
<script src="{{ url_for('static', filename='js/screen_edit/panel-init.js') }}"></script>
<script src="{{ url_for('static', filename='js/screen_edit/personnage-panel.js') }}"></script>
<script src="{{ url_for('static', filename='js/screen_edit/flow-editor.js') }}"></script>
<script src="{{ url_for('static', filename='js/screen_edit/tabs-and-blocks.js') }}"></script>
<script src="{{ url_for('static', filename='js/screen_edit/animation-timeline.js') }}"></script>
<script src="{{ url_for('static', filename='js/screen_edit/personnage-preview.js') }}"></script>
<script>
// Onglet initial : ?tab=... (utilisé par les routes de CRUD des
// événements/blocs, pour revenir directement sur le bon onglet après