BIG EGG · Habillage visuel · Décisions prises

Des formes aux images

Comment lier une image, une animation ou un effet à une entité dans un état donné — éditable dans l'outil, consommé par Godot, et sans jamais toucher au jeu qui est dessous.

La bonne nouvelle

Le mécanisme existe déjà

Un nœud d'upgrade porte un image_path, séparé de son theme réutilisable, et upgrade_tree_node.gd en fait exactement ce qu'il faut :

# scripts/progression/upgrade_tree_node.gd
var image_path := str(data.get("image_path", ""))
if not image_path.is_empty() and ResourceLoader.exists(image_path):
    icon = load(image_path) as Texture2D

Chemin déclaré dans la donnée, existence vérifiée, repli silencieux sur le dessin procédural si le fichier manque. C'est précisément le comportement voulu partout ailleurs. Il n'y a donc rien à inventer sur le principe — seulement à le généraliser, et à lui donner un vocabulaire.

Réponse courte à ta question

Oui, ça se fait à la base, et non, ça ne demande pas de scènes coquilles pour le cas simple. Mais les deux se valent selon le contenu : une marche à quatre images tient dans une feuille de sprites, un feu avec particules et lumière demande une scène. La bonne réponse est de laisser la donnée choisir — un visuel déclare sa nature, et le jeu sait charger les deux.

La contrainte à connaître avant tout

Godot n'exécute que ce qu'il a importé

Déposer un PNG dans assets/ ne suffit pas. Godot génère un fichier .import à côté de chaque ressource, et seul ce qui a été importé par l'éditeur existe à l'exécution. Le projet n'en compte que deux aujourd'hui (aim_cursor.svg et l'icône), donc la règle ne s'est jamais vue — elle se verra au premier lot d'art.

C'est la même contrainte que « un nouveau script avec class_name doit passer par l'éditeur avant tout lancement headless », déjà dans les directives. Elle devient : après avoir ajouté des images, ouvrir Godot une fois.

Ce que l'outil peut en faire

C'est vérifiable, et c'est un vrai service : un fichier sans .import voisin n'est pas encore utilisable par le jeu. Le sélecteur de l'outil peut le marquer « pas encore importé » au lieu de laisser publier une référence morte. Ça coûte un file_exists() par entrée.

L'alternative — charger depuis user:// avec Image.load_from_file() — évite l'import mais perd la compression, les réglages de filtrage, et ne marche pas pour une scène ni une SpriteFrames. À écarter, sauf pour un mode « atelier » qui n'est pas le sujet.

La règle qui protège le jeu

Le visuel ne décide jamais de la collision

Aujourd'hui ShapeFactory sert deux maîtres : il produit les polygones dessinés et les formes physiques. C'était le bon choix tant que le dessin était la forme. Avec des images, ça devient un piège : la moindre retouche d'art déplacerait une hitbox.

Décision de fond

La collision et la portée restent procédurales et gardent leur propriétaire actuel — ShapeFactory.create_collision(), Enemy.body_radius(), Player.BODY_RADIUS. Un visuel est une décoration posée par-dessus : il a une échelle et un décalage, jamais une influence. Sans cette séparation, chaque livraison d'art devient une session d'équilibrage.

Corollaire pratique : un visuel déclare anchor et scale pour se caler sur un corps dont il ne décide pas. Un monstre agrandi ×2 garde son rayon de 36 px et son image suit.

Le modèle

Une collection visuals, deux natures

Nouvelle collection au même rang que employees ou room_elements. Chaque entrée est un jeu visuel réutilisable : dix monstres peuvent partager la même marche générique, et la retoucher une fois les met tous à jour.

ChampRôle
kindframes · scene — décide de ce qui est chargé.
pathFeuille de sprites, ou .tscn.
columns · rows · countDécoupe de la feuille. Ignorés pour une scène.
fps · loopLecture. loop à faux pour une mort ou une destruction.
offset · scale · flip_follows_facingCalage sur un corps dont le visuel ne décide pas.
shader · shader_paramsMatériau optionnel. Voir plus bas.
preview_imageVignette pour l'outil quand kind = scene, qui ne peut pas rendre une scène Godot.

Pourquoi les deux natures, et pas une

frames — la feuille de sprites Le cas courant

Un PNG, une grille, une cadence. Godot construit une SpriteFrames à la volée et l'assigne à un AnimatedSprite2D. Aucun fichier intermédiaire, aucune scène à maintenir. C'est ce qui couvrira la marche, le travail, l'attaque, l'idle — l'écrasante majorité.

scene — la coquille Quand il faut plus

Un .tscn contenant ce qu'on veut : particules, PointLight2D, AnimationPlayer avec pistes multiples, matériau. Le jeu l'instancie et l'attache. Indispensable pour le feu, le nuage de poison, un boss à plusieurs couches.

Le point important : le lien est le même dans les deux cas. Le code qui pose un visuel n'a pas à savoir lequel il pose. Une entité qui commence en feuille de sprites peut passer en scène plus tard sans qu'une ligne de logique bouge.

Le vocabulaire

Les états, par collection

Chaque genre d'élément a ses besoins, comme tu le décris. Un constant déclaré des trois côtés, sur le modèle de ROOM_BEHAVIOURS et EMPLOYEE_SKILLS :

CollectionÉtatsRequis
enemiesidle walk attack hurt death specialidle
employeesidle walk workidle
customer_profilesidle walk eat angryidle
player_configsidle walk attack dash hurtidle
room_elementsidle triggered destroyedidle
ingredients · recipesicon · plated—
biomes · dimensionsbackground foreground—
roomsbackground—

La chaîne de repli

C'est la règle du projet appliquée aux images : zéro ou vide veut dire « demande au-dessus ».

death  →  hurt  →  idle  →  forme procédurale actuelle
walk   →  idle  →  forme procédurale actuelle
attack →  idle  →  forme procédurale actuelle

Conséquence directe : le jeu tourne pendant toute la migration. Un monstre qui n'a qu'un idle est jouable ; un monstre sans aucun visuel est jouable aussi, il reste un polygone coloré. Aucun moment où il faut « tout avoir fait » pour lancer la partie.

Et l'écriture reste légère : la fiche d'une entité gagne une seule carte visuals: {état → identifiant}, exactement la forme des tables de poids que l'outil sait déjà éditer.

Côté outil

Scanner ce qui existe

Un point d'entrée action=scan_assets qui parcourt le assets/ du projet actif et rend l'arborescence filtrée aux extensions connues. PHP fait ça nativement, et l'outil est servi par le même serveur que les fichiers — donc un <img src="../../assets/…"> affiche une vraie prévisualisation dans le navigateur, sans copier quoi que ce soit.

Ce que le scan rendCe que l'outil en fait
Chemin relatifLa valeur écrite dans la donnée.
Présence du .importMarque « pas encore importé » — la référence serait morte en jeu.
Dimensions du PNGPropose une découpe plausible, et détecte une grille incohérente.
Date de modificationRepère l'art retouché depuis la dernière publication.

Ce que l'outil ne pourra pas faire

Les effets

Réutiliser les crochets plutôt qu'inventer un déclencheur

Un flash au moment d'un coup, un nuage de poison, une gerbe à la mort : ce ne sont pas des états, ce sont des événements. Et le projet a déjà un vocabulaire d'événements — les crochets.

enemies:       on_kill  on_spawn  on_damage_player
recipes:       on_cooked  on_served
room_elements: on_destroyed  on_triggered
employees:     on_hired
rooms:         on_enter  on_cleared  on_blocked
La proposition

Une collection vfx — même forme qu'un visuel de nature scene, mais éphémère et détachée : elle se joue à un point, elle se libère toute seule. Les entités gagnent une carte vfx: {crochet → identifiant}, clés prises dans leur vocabulaire de crochets existant. Aucun concept neuf, et l'outil rend déjà des sections de crochets : le formulaire est presque gratuit.

Le flash rouge à l'impact existe déjà en dur (modulate sur un tween, dans enemy.gd et player.gd). Il devient le défaut quand aucune donnée ne dit mieux — même logique de repli que pour les formes.

Les effets temporaires ont déjà leur place

DEFAULT_EFFECTS porte déjà une color par effet — burning est orange, paralyzed violet. La collection effects existe dans le schéma et n'a jamais été remplie dans le fichier maître : elle tourne encore sur trois valeurs de secours codées en dur. Lui ajouter un champ vfx revient à dire « une brûlure, ça ressemble à ça », et c'est le chemin le plus court vers du feu et du poison visibles.

Les shaders

Un matériau, et des paramètres que l'outil peut découvrir

Un shader n'est pas une entité de plus : c'est un matériau posé sur un visuel. D'où shader et shader_params sur l'entrée visuelle, jamais sur l'entité.

Le point intéressant : l'outil peut découvrir les paramètres tout seul. Un .gdshader déclare ses entrées en clair —

shader_type canvas_item;
uniform vec4 tint : source_color = vec4(1.0);
uniform float intensity : hint_range(0.0, 4.0) = 1.0;

— et une expression régulière PHP en sort les noms, les types et les bornes. Le formulaire se construit donc à partir du shader lui-même : ajouter un uniform dans le fichier fait apparaître un champ dans l'outil, sans toucher au schéma. C'est le même esprit que le catalogue de textes dérivé des données.

Contrepartie honnête : les types utiles restent float, vec4, bool et sampler2D. Une matrice ou un tableau ne s'éditent pas raisonnablement dans un formulaire — l'outil les ignorera et les laissera à leur valeur par défaut.

La lumière n'est pas un effet d'entité

Une ambiance se règle par lieu, pas par créature : CanvasModulate pour la teinte générale, PointLight2D pour les sources. Le biome porte déjà trois couleurs et n'est plus qu'une palette — c'est là que l'ambiance appartient, avec la dimension pour les cas plus larges. Une torche posée dans une salle reste un room_element avec un visuel de nature scene qui contient sa lumière.

Faisabilité

Ce que ça coûte, honnêtement

PointDifficultéPourquoi
Charger une feuille en SpriteFramesFacileAtlasTexture par case, boucle sur la grille. Une trentaine de lignes.
Instancier une scène et l'attacherFacileLe projet le fait déjà pour les assiettes, les clients, les employés.
Scanner les ressources en PHPFacileRecursiveDirectoryIterator. Le vrai travail est le sélecteur, pas le scan.
Chaîne de repli et validationFacileMême forme que « zéro veut dire demande au-dessus », déjà éprouvée.
Découper les 14 _draw existantsMoyenChacun doit accepter un visuel par-dessus et garder son dessin en repli. Répétitif, pas subtil.
Paramètres de shader découvertsMoyenL'analyse est simple, le rendu générique du formulaire l'est moins.
Aperçu animé dans l'outilMoyenCSS steps() suffit, mais il faut que la découpe saisie soit juste.
Le volume d'artC'est là qu'est le coûtCinq monstres × six états, cinq employés × trois, six éléments × trois… La technique ne sera jamais le facteur limitant.

Les quatre risques que je surveillerais

  1. L'oubli d'import. Une référence publiée vers un fichier jamais importé passe toutes les validations de données et ne s'affiche pas en jeu. C'est exactement la classe de bug qui a coûté cher avec les salles qui ne se publiaient pas. Le marqueur dans le sélecteur n'est pas un confort, c'est le garde-fou.
  2. La tentation de faire décider le visuel. Le jour où une hitbox suit une image, l'équilibrage devient impossible à tenir. La séparation doit être écrite dans les directives, pas seulement dans les têtes.
  3. Le coût mémoire des scènes. Charger paresseusement et garder un cache par identifiant. À quelques centaines de ressources ce n'est pas un sujet ; à quelques milliers, si.
  4. Le test. Un visuel ne se vérifie pas à l'œil dans un smoke test headless. Ce qui se teste : la résolution d'un état vers un identifiant, la chaîne de repli, l'existence du fichier, et le fait que la collision n'a pas bougé. C'est déjà l'essentiel.

La route

Six phases

  1. Le résolveur et le repli

    Collection visuals, vocabulaire d'états, chaîne de repli, un VisualCatalog qui rend « quelle ressource pour cette entité dans cet état ». Aucune image livrée : à la fin de la phase, tout retombe sur les formes actuelles et rien n'a changé à l'écran. C'est le socle, et il est testable seul.

  2. Le porteur de visuel

    Un nœud réutilisable qui reçoit un identifiant et se charge du reste : feuille ou scène, cadence, décalage, miroir selon l'orientation. Branché d'abord sur les monstres seuls, avec deux ou trois états, pour éprouver le modèle sur un cas réel avant de le répandre.

  3. Le sélecteur dans l'outil

    Scan des ressources, marqueur d'import, aperçu image et aperçu animé, écriture de la carte visuals. C'est la phase qui rend le reste utilisable par quelqu'un qui ne lit pas le JSON.

  4. Les autres collections

    Joueur, employés, clients, éléments de salle, ingrédients, plats. Répétitif et sans surprise une fois les trois premières phases posées — c'est le moment où les 14 _draw se convertissent un par un.

  5. Les effets

    Collection vfx, carte vfx: {crochet → identifiant} sur les entités, remplissage de la collection effects restée vide. Le flash à l'impact devient une donnée, feu et poison deviennent possibles.

  6. Shaders et ambiance

    Matériau sur un visuel, paramètres découverts depuis le .gdshader, lumière et teinte portées par le biome et la dimension. Dernière parce que c'est la seule qui ne débloque rien d'autre.

L'ordre a une logique : les trois premières phases ne changent rien à l'écran et peuvent donc être livrées sans le moindre asset. Le jour où le premier lot d'art arrive, il n'y a plus qu'à le poser.

Tranché

Les décisions, et ce qu'elles fixent

QuestionDécisionCe que ça fixe
Nature du visuelCollection partagéeUne entrée visuals réutilisable, référencée par identifiant. L'entité ne porte qu'une carte état → identifiant.
RangementPar entitéassets/<collection>/<id>/<état>.png, plus assets/shared/ pour ce qui sert à plusieurs.
FormatFeuille + grilleUn fichier par animation, découpe déclarée (columns, rows, count, fps).
DécorImage de fondUne image par biome, dimension ou salle. Pas de TileMap: rien ne le réclame aujourd'hui.
Les deux premières se combinent mieux que prévu

Je les avais présentées comme indépendantes, et l'une compense l'autre. Un visuel partagé mais rangé par entité donne une convention que l'outil peut exploiter tout seul : trouvant assets/enemies/slime/walk.png, il propose en un clic « créer le visuel slime_walk et le lier à l'état walk du slime ». La collection partagée cesse d'être une corvée de saisie et redevient ce qu'elle doit être : un moyen de réutiliser, pas une étape obligatoire.

La convention de rangement

assets/
  enemies/
    slime/
      idle.png          4×1
      walk.png          4×1
      death.png         6×1, loop = false
  employees/
    pablo/  idle.png  walk.png  work.png
  room_elements/
    crate/  idle.png  destroyed.png
  biomes/
    mossy_cellar/  background.png
  shared/
    generic_walk.png    ce qui sert à plusieurs entités
  vfx/
    hit_flash.tscn
    hit_flash.png       vignette, la scène n'étant pas rendable dans un navigateur

Supprimer une créature redevient un dossier à jeter — et la suppression en cascade de l'outil peut le signaler, comme elle signale déjà les textes orphelins.

La forme écrite

// la collection: un jeu visuel réutilisable
"visuals": {
  "slime_walk": {
    "display_name": "Slime — marche",
    "kind": "frames",
    "path": "res://assets/enemies/slime/walk.png",
    "columns": 4, "rows": 1, "count": 4,
    "fps": 8, "loop": true,
    "offset": {"x": 0, "y": -4}, "scale": 1.0,
    "flip_follows_facing": true,
    "shader": "", "shader_params": {},
    "preview_image": ""
  }
}

// l'entité: rien d'autre qu'une carte état → identifiant
"slime": { …, "visuals": {"idle": "slime_idle", "walk": "slime_walk"} }

C'est exactement la forme des tables de poids que l'outil édite déjà — weightGrid devient un sélecteur de visuels sans rien réinventer.

La suite

Ce que la première phase livre

Avec les quatre décisions prises, la phase 1 est entièrement définie et ne change rien à l'écran. Elle peut donc être construite et validée sans un seul fichier d'art.

Rien de tout ça n'est visible en jeu. C'est voulu : le socle se pose, s'éprouve, et attend l'art sans bloquer quoi que ce soit. La phase 2 branche le porteur de visuel sur les monstres seuls, la phase 3 donne le sélecteur à l'outil — et c'est seulement à ce moment qu'un lot d'images devient utile.

Le seul vrai prérequis

Avant que quoi que ce soit s'affiche, il faudra ouvrir Godot une fois après chaque ajout de fichiers. C'est la contrainte d'import, et aucune quantité de code ne la contourne. Autant l'inscrire dans la routine dès maintenant.