From 449c36fd5d48b447bee9d65847c55d0e05b442b4 Mon Sep 17 00:00:00 2001 From: william Date: Mon, 31 Aug 2026 21:49:05 +0200 Subject: [PATCH] =?UTF-8?q?Biblioth=C3=A8que=20de=20sprites=20animaux=20Cr?= =?UTF-8?q?aftPix=20(1/3)=20:=20catalogue,=20galerie,=20acc=C3=A8s=20admin?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Premier commit d'une fonctionnalité découpée en plusieurs lots (voir le plan "Bibliothèque de sprites animaux CraftPix") : intègre 14 familles d'animaux (15 variantes de couleur chacune) comme personnages Forge sélectionnables, à côté des 6 Kenney existants — réservé au rôle admin, licence CraftPix oblige (interdiction contractuelle de rendre ces sprites utilisables par un compte "user" via l'application). - screens/labels/animal_sprite_library.py (nouveau) : charge un manifest JSON généré une fois (voir scripts/generate_animal_sprite_manifest.py, commit suivant) et construit ADMIN_SPRITE_LIBRARY, dans le même format que l'existant PUBLIC_SPRITE_LIBRARY (screens/labels/sprite_library.py, ex-SPRITE_LIBRARY, renommé pour distinguer les deux). screens.SPRITE_LIBRARY reste le catalogue FUSIONNÉ (utilisé par resolve_personnage_animations pour la résolution runtime, sans filtrage par rôle — voir le constat d'exploration : le payload de jeu et /jouer/ ne vérifient déjà aucun rôle nulle part). - screens/labels/sprite_gallery.py (nouveau) : sprite_gallery_families() groupe la galerie par famille — un animal n'apparaît qu'une fois (sa variante "de base"), ses 15 couleurs se choisissent depuis le panneau de propriétés (render_variant_gallery, templates/screen_edit.html), répondant à la suggestion de l'utilisateur plutôt que d'encombrer la galerie d'ajout de 210 tuiles quasi identiques. - routes/screens/screen_edit.py, routes/scenes/scene_edit_view.py : la galerie passée au template est filtrée par rôle (PUBLIC_SPRITE_LIBRARY pour un compte "user", SPRITE_LIBRARY complet pour un admin) — même idiome que core/auth_guard.py. - core/sprite_gate.py (nouveau) + 4 routes d'écriture (element_add, element_set_personnage_data, scene_object_add, scene_object_personnage_data) : ferme la brèche d'un POST direct qui contournerait la galerie filtrée (403 si un compte non-admin tente d'assigner un personnage animal). - tests/conftest.py : nouvelles fixtures user_client/user_game (compte "user" non-admin avec un projet assigné) pour tester le filtrage par rôle de bout en bout. Co-Authored-By: Claude Sonnet 5 --- core/sprite_gate.py | 16 ++ routes/elements/element_add.py | 2 + .../elements/element_set_personnage_data.py | 3 + routes/scenes/scene_edit_view.py | 11 +- routes/scenes/scene_object_add.py | 3 + routes/scenes/scene_object_personnage_data.py | 3 + routes/screens/screen_edit.py | 13 +- screens/__init__.py | 14 +- screens/labels/animal_sprite_library.py | 63 +++++++ screens/labels/sprite_gallery.py | 21 +++ screens/labels/sprite_library.py | 7 +- screens/rendering/personnage_data.py | 12 +- screens/scenes/add_scene_object.py | 5 +- templates/scene_edit.html | 26 ++- templates/screen_edit.html | 39 ++++- tests/conftest.py | 40 +++++ tests/test_animal_sprite_library.py | 160 ++++++++++++++++++ tests/test_personnage_widget.py | 17 +- 18 files changed, 430 insertions(+), 25 deletions(-) create mode 100644 core/sprite_gate.py create mode 100644 screens/labels/animal_sprite_library.py create mode 100644 screens/labels/sprite_gallery.py create mode 100644 tests/test_animal_sprite_library.py diff --git a/core/sprite_gate.py b/core/sprite_gate.py new file mode 100644 index 00000000..280e58e6 --- /dev/null +++ b/core/sprite_gate.py @@ -0,0 +1,16 @@ +"""Garde d'accès aux sprites animaux CraftPix (screens.ADMIN_SPRITE_LIBRARY) +— licence CraftPix, réservés au rôle admin (voir le plan "Bibliothèque de +sprites animaux CraftPix"). La galerie de l'éditeur (routes/screens/ +screen_edit.py, routes/scenes/scene_edit_view.py) ne les propose déjà pas +à un compte "user", mais un POST direct vers une route d'écriture +(element_add, scene_object_add, element_set_personnage_data, +scene_object_personnage_data) pourrait contourner ce filtrage d'UI — ce +garde-fou ferme cette brèche au moment de l'écriture.""" +from flask import abort, g + +import screens + + +def forbid_admin_only_character(forge_character): + if forge_character in screens.ADMIN_ONLY_CHARACTER_SLUGS and g.current_user["role"] != "admin": + abort(403) diff --git a/routes/elements/element_add.py b/routes/elements/element_add.py index 5348eacd..45cb24d0 100644 --- a/routes/elements/element_add.py +++ b/routes/elements/element_add.py @@ -3,6 +3,7 @@ from flask import request, redirect, url_for import screens from core.flask_app import app +from core.sprite_gate import forbid_admin_only_character @app.route("/game//screens//elements/add", methods=["POST"]) @@ -31,5 +32,6 @@ def element_add(slug, screen_id): # réellement cliqué. forge_character = request.form.get("forge_character") if forge_character and forge_character in screens.SPRITE_LIBRARY: + forbid_admin_only_character(forge_character) screens.set_personnage_data(slug, new_id, {"source": "forge", "forge_character": forge_character}) return redirect(url_for("screen_edit", slug=slug, screen_id=screen_id, selected=new_id)) diff --git a/routes/elements/element_set_personnage_data.py b/routes/elements/element_set_personnage_data.py index 226670fc..881907d3 100644 --- a/routes/elements/element_set_personnage_data.py +++ b/routes/elements/element_set_personnage_data.py @@ -3,6 +3,7 @@ from flask import request, jsonify import screens from core.flask_app import app +from core.sprite_gate import forbid_admin_only_character @app.route("/game//elements//personnage", methods=["POST"]) @@ -16,6 +17,8 @@ def element_set_personnage_data(slug, element_id): el = screens.get_element(slug, element_id) if not el: return jsonify({"error": "élément introuvable"}), 404 + if body.get("source") == "forge" and body.get("forge_character"): + forbid_admin_only_character(body["forge_character"]) screens.set_personnage_data(slug, element_id, body) el = screens.get_element(slug, element_id) return jsonify({"ok": True, "animations": screens.resolve_personnage_animations(el)}) diff --git a/routes/scenes/scene_edit_view.py b/routes/scenes/scene_edit_view.py index de60c1dc..43d7c93b 100644 --- a/routes/scenes/scene_edit_view.py +++ b/routes/scenes/scene_edit_view.py @@ -1,6 +1,6 @@ import json -from flask import render_template, request, abort +from flask import render_template, request, abort, g import db import screens @@ -46,11 +46,18 @@ def render_scene_edit(slug, screen_id, game): screens_map = {s["id"]: s["name"] for s in all_screens} blocks_view = screens.blocks_view(flow_blocks, flow_nodes, object_labels) + # Sprites animaux CraftPix (screens.ADMIN_SPRITE_LIBRARY) : réservés au + # rôle admin (licence CraftPix, voir core/sprite_gate.py et le plan + # "Bibliothèque de sprites animaux CraftPix"). + sprite_library = screens.SPRITE_LIBRARY if g.current_user["role"] == "admin" else screens.PUBLIC_SPRITE_LIBRARY + sprite_library_families = screens.sprite_gallery_families(sprite_library) + return render_template( "scene_edit.html", game=game, screen=screen, all_screens=all_screens, objects=objects, selected_id=selected_id, personnage_data=personnage_data, - sprite_library=screens.SPRITE_LIBRARY, sprite_library_json=json.dumps(screens.SPRITE_LIBRARY), + sprite_library=sprite_library, sprite_library_families=sprite_library_families, + sprite_library_json=json.dumps(sprite_library), element_animations_map_json=json.dumps(element_animations_map), element_orientation_values=screens.ELEMENT_ORIENTATION_VALUES, element_orientation_labels_json=json.dumps(screens.ELEMENT_ORIENTATION_LABELS), diff --git a/routes/scenes/scene_object_add.py b/routes/scenes/scene_object_add.py index fe56c9ab..4786ea93 100644 --- a/routes/scenes/scene_object_add.py +++ b/routes/scenes/scene_object_add.py @@ -3,6 +3,7 @@ from flask import request, redirect, url_for import screens from core.flask_app import app +from core.sprite_gate import forbid_admin_only_character @app.route("/game//screens//scene-objects/add", methods=["POST"]) @@ -12,6 +13,8 @@ def scene_object_add(slug, screen_id): add_scene_object.py).""" kind = request.form.get("kind", "personnage") forge_character = request.form.get("forge_character") + if forge_character: + forbid_admin_only_character(forge_character) new_id = screens.add_scene_object(slug, screen_id, kind=kind, forge_character=forge_character) # Même route que l'éditeur document (/game//screens//edit, # endpoint "screen_edit") — c'est CE dispatcher qui bascule vers la diff --git a/routes/scenes/scene_object_personnage_data.py b/routes/scenes/scene_object_personnage_data.py index 52d7ed74..8af44d7e 100644 --- a/routes/scenes/scene_object_personnage_data.py +++ b/routes/scenes/scene_object_personnage_data.py @@ -3,6 +3,7 @@ from flask import request, jsonify import screens from core.flask_app import app +from core.sprite_gate import forbid_admin_only_character @app.route("/game//scene-objects//personnage", methods=["POST"]) @@ -17,6 +18,8 @@ def scene_object_personnage_data(slug, object_id): obj = screens.get_scene_object(slug, object_id) if not obj: return jsonify({"error": "objet introuvable"}), 404 + if body.get("source") == "forge" and body.get("forge_character"): + forbid_admin_only_character(body["forge_character"]) screens.set_scene_object_personnage_data(slug, object_id, body) obj = screens.get_scene_object(slug, object_id) return jsonify({"ok": True, "animations": screens.resolve_personnage_animations(obj)}) diff --git a/routes/screens/screen_edit.py b/routes/screens/screen_edit.py index 71abac20..0701db0d 100644 --- a/routes/screens/screen_edit.py +++ b/routes/screens/screen_edit.py @@ -1,6 +1,6 @@ import json -from flask import render_template, request, abort +from flask import render_template, request, abort, g import db import screens @@ -78,6 +78,13 @@ def screen_edit(slug, screen_id): blocks_view = screens.blocks_view(flow_blocks, flow_nodes, elements_labels) + # Sprites animaux CraftPix (screens.ADMIN_SPRITE_LIBRARY) : réservés au + # rôle admin (licence CraftPix, voir core/sprite_gate.py et le plan + # "Bibliothèque de sprites animaux CraftPix") — un compte "user" ne + # doit même pas les voir dans la galerie. + sprite_library = screens.SPRITE_LIBRARY if g.current_user["role"] == "admin" else screens.PUBLIC_SPRITE_LIBRARY + sprite_library_families = screens.sprite_gallery_families(sprite_library) + return render_template( "screen_edit.html", game=game, screen=screen, elements=elements, top_level_elements=top_level_elements, @@ -93,8 +100,8 @@ def screen_edit(slug, screen_id): element_surbrillance_values=screens.ELEMENT_SURBRILLANCE_VALUES, element_desactive_values=screens.ELEMENT_DESACTIVE_VALUES, element_orientation_values=screens.ELEMENT_ORIENTATION_VALUES, - sprite_library=screens.SPRITE_LIBRARY, - sprite_library_json=json.dumps(screens.SPRITE_LIBRARY), + sprite_library=sprite_library, sprite_library_families=sprite_library_families, + sprite_library_json=json.dumps(sprite_library), personnage_data=personnage_data, element_animations_map_json=json.dumps(element_animations_map), data_operations=screens.DATA_OPERATIONS, diff --git a/screens/__init__.py b/screens/__init__.py index 93a75a2c..d39e440f 100644 --- a/screens/__init__.py +++ b/screens/__init__.py @@ -55,7 +55,19 @@ from .labels.element_action_properties import ( ) from .labels.element_visibility import ELEMENT_VISIBILITY_VALUES, ELEMENT_VISIBILITY_LABELS from .labels.data_operations import DATA_OPERATIONS, DATA_OPERATION_LABELS -from .labels.sprite_library import SPRITE_LIBRARY +from .labels.sprite_library import PUBLIC_SPRITE_LIBRARY +from .labels.animal_sprite_library import ADMIN_SPRITE_LIBRARY + +# SPRITE_LIBRARY (fusionné) : utilisé par resolve_personnage_animations +# pour la résolution RUNTIME (jeu jouable, tous rôles confondus — voir le +# plan "Bibliothèque de sprites animaux CraftPix" : aucun filtrage par +# rôle au runtime, seulement à la galerie/l'écriture). Les routes qui +# alimentent la galerie de l'éditeur doivent utiliser PUBLIC_SPRITE_LIBRARY +# ou ADMIN_SPRITE_LIBRARY selon le rôle, jamais SPRITE_LIBRARY directement. +SPRITE_LIBRARY = {**PUBLIC_SPRITE_LIBRARY, **ADMIN_SPRITE_LIBRARY} +ADMIN_ONLY_CHARACTER_SLUGS = set(ADMIN_SPRITE_LIBRARY) +from .labels.sprite_gallery import sprite_gallery_families +from .rendering.list_used_forge_characters import list_used_forge_characters from .flow.constants import ( TRIGGER_EVENTS, CONDITION_OPERATORS, CONDITION_OPERATOR_LABELS, CLICKED_ROW_ID, LAST_INSERTED_ROW_ID, diff --git a/screens/labels/animal_sprite_library.py b/screens/labels/animal_sprite_library.py new file mode 100644 index 00000000..b90bdf81 --- /dev/null +++ b/screens/labels/animal_sprite_library.py @@ -0,0 +1,63 @@ +import json +import os + +# Bibliothèque de sprites animaux CraftPix — RÉSERVÉE au rôle admin (voir +# routes/screens/screen_edit.py, routes/scenes/scene_edit_view.py, et les +# routes d'écriture screens/scene-objects "personnage"/"add"). Licence +# CraftPix : interdiction de rendre ces sprites utilisables par un compte +# "user" via l'application (voir le plan "Bibliothèque de sprites animaux +# CraftPix"). Générée une fois par scripts/generate_animal_sprite_manifest.py +# depuis assets/characters/animal_sprite/ (pack source, non versionné) vers +# static/characters/animals///.png, avec un manifest +# JSON décrivant le compte RÉEL de frames par pose/variante (variable selon +# les packs — pas de table figée comme _SEQUENCE_ANIMATIONS pour Kenney). +_MANIFEST_PATH = os.path.join( + os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))), + "static", "characters", "animals", "manifest.json", +) + + +def _load_manifest(): + try: + with open(_MANIFEST_PATH, encoding="utf-8") as f: + return json.load(f) + except FileNotFoundError: + # Poste de dev sans les assets animaux copiés (script pas encore + # lancé), ou paquet exporté pour un jeu qui n'utilise aucun + # personnage animal (voir publish/build_package.py, copie + # sélective) : aucun personnage animal disponible, pas une erreur. + return {} + + +def _slug(animal, nn): + return f"animal-{animal}-{nn}" + + +def _label(family_label, nn): + n = int(nn) + return f"{family_label} (variante {n})" if n > 1 else f"{family_label} (de base)" + + +def _animations_for(animal, nn, poses): + return { + pose: [f"/static/characters/animals/{animal}/{nn}/{pose}{i}.png" for i in range(count)] + for pose, count in poses.items() + } + + +def _build_library(): + manifest = _load_manifest() + library = {} + for animal, pack in manifest.items(): + family_label = pack["label"] + for nn, poses in pack["variants"].items(): + library[_slug(animal, nn)] = { + "label": _label(family_label, nn), + "animations": _animations_for(animal, nn, poses), + "family": animal, + "family_label": family_label, + } + return library + + +ADMIN_SPRITE_LIBRARY = _build_library() diff --git a/screens/labels/sprite_gallery.py b/screens/labels/sprite_gallery.py new file mode 100644 index 00000000..ec26bfa8 --- /dev/null +++ b/screens/labels/sprite_gallery.py @@ -0,0 +1,21 @@ +def sprite_gallery_families(sprite_library): + """Un tile par FAMILLE pour la galerie d'ajout/de changement de + personnage — les personnages Kenney (pas de clé "family", voir + screens/labels/sprite_library.py) sont chacun leur propre famille ; + les animaux CraftPix (screens/labels/animal_sprite_library.py) sont + représentés par leur variante "de base" (dict Python : ordre + d'insertion garanti, "01" toujours vu en premier — voir + _build_library()). Les autres variantes se choisissent depuis le + panneau de propriétés (voir render_variant_gallery, templates/ + screen_edit.html) plutôt que d'encombrer cette galerie de 15 tiles + quasi identiques par animal.""" + seen_families = set() + families = [] + for slug, character in sprite_library.items(): + family = character.get("family") + if family: + if family in seen_families: + continue + seen_families.add(family) + families.append((slug, character)) + return families diff --git a/screens/labels/sprite_library.py b/screens/labels/sprite_library.py index 5afd29c8..2bb9f8f8 100644 --- a/screens/labels/sprite_library.py +++ b/screens/labels/sprite_library.py @@ -48,7 +48,12 @@ def _animations_for(slug): return animations -SPRITE_LIBRARY = { +# Bibliothèque publique — visible par tous les comptes (CC0, voir +# LICENSE-kenney.txt). Distincte de screens.ADMIN_SPRITE_LIBRARY (sprites +# animaux CraftPix, réservés au rôle admin — voir +# screens/labels/animal_sprite_library.py et le plan "Bibliothèque de +# sprites animaux CraftPix"). +PUBLIC_SPRITE_LIBRARY = { slug: {"label": label, "animations": _animations_for(slug)} for slug, label in CHARACTERS.items() } diff --git a/screens/rendering/personnage_data.py b/screens/rendering/personnage_data.py index e613c2b1..bbd37b5a 100644 --- a/screens/rendering/personnage_data.py +++ b/screens/rendering/personnage_data.py @@ -1,6 +1,14 @@ import json -from ..labels.sprite_library import SPRITE_LIBRARY +from ..labels.sprite_library import PUBLIC_SPRITE_LIBRARY +from ..labels.animal_sprite_library import ADMIN_SPRITE_LIBRARY + +# Résolution RUNTIME (jeu jouable) : le catalogue COMPLET, quel que soit le +# rôle du créateur — voir le plan "Bibliothèque de sprites animaux +# CraftPix" (aucun filtrage par rôle au runtime, seulement à la galerie de +# l'éditeur/l'écriture, voir routes/screens/screen_edit.py et +# routes/elements/element_add.py). +_SPRITE_LIBRARY = {**PUBLIC_SPRITE_LIBRARY, **ADMIN_SPRITE_LIBRARY} _DEFAULT_PERSONNAGE_DATA = {"source": "forge", "forge_character": "male-adventurer"} @@ -26,7 +34,7 @@ def resolve_personnage_animations(el): data = resolve_personnage_data(el) if data.get("source") == "custom": return data.get("animations") or {} - character = SPRITE_LIBRARY.get(data.get("forge_character")) + character = _SPRITE_LIBRARY.get(data.get("forge_character")) return character["animations"] if character else {} diff --git a/screens/scenes/add_scene_object.py b/screens/scenes/add_scene_object.py index 18077cb1..a80dae60 100644 --- a/screens/scenes/add_scene_object.py +++ b/screens/scenes/add_scene_object.py @@ -22,8 +22,9 @@ def add_scene_object(slug, scene_id, kind="personnage", forge_character=None): if kind == "personnage": data = dict(_DEFAULT_PERSONNAGE_DATA) if forge_character: - from ..labels.sprite_library import SPRITE_LIBRARY - if forge_character in SPRITE_LIBRARY: + from ..labels.sprite_library import PUBLIC_SPRITE_LIBRARY + from ..labels.animal_sprite_library import ADMIN_SPRITE_LIBRARY + if forge_character in PUBLIC_SPRITE_LIBRARY or forge_character in ADMIN_SPRITE_LIBRARY: data["forge_character"] = forge_character attributes["_personnage_data"] = json.dumps(data) conn = db.connect(slug) diff --git a/templates/scene_edit.html b/templates/scene_edit.html index b1265792..80aadd56 100644 --- a/templates/scene_edit.html +++ b/templates/scene_edit.html @@ -67,14 +67,14 @@
🎭 Personnages
-
- {% for slug_c, character in sprite_library.items() %} +
+ {% for slug_c, character in sprite_library_families %} {% endfor %}
-

Personnage Kenney prêt à l'emploi (licence libre CC0), avec toutes ses animations.

+

Personnage Kenney (licence libre CC0) ou animal Forge, avec toutes ses animations — les variantes de couleur d'un animal se choisissent depuis le panneau de propriétés.

@@ -629,14 +629,26 @@ 🎭 Personnage

Personnage Forge — clique un autre pour en changer.

-
- {% for slug_c, character in sprite_library.items() %} +
+ {% for slug_c, character in sprite_library_families %} {% endfor %}
+ {% set current_character = sprite_library.get(personnage_data.forge_character) %} + {% if current_character and current_character.get('family') %} +

Variantes de {{ current_character.family_label }} :

+
+ {% for slug_c, character in sprite_library.items() if character.get('family') == current_character.family %} + + {% endfor %} +
+ {% endif %}
{% endif %} diff --git a/templates/screen_edit.html b/templates/screen_edit.html index c0173cfa..411b414e 100644 --- a/templates/screen_edit.html +++ b/templates/screen_edit.html @@ -43,14 +43,40 @@ + personnage Forge d'un élément déjà posé (panneau droit, propriétés). + Un tile par FAMILLE (sprite_library_families, voir + screens/labels/sprite_gallery.py) — un animal CraftPix (screens/ + labels/animal_sprite_library.py) n'apparaît ici qu'une fois, dans sa + variante "de base" ; ses 15 variantes se choisissent depuis + render_variant_gallery ci-dessous, dans le panneau de propriétés. + Miniature = 1ère frame de l'animation "idle" du personnage (jamais un + chemin en dur : le rangement sur disque diffère entre Kenney et les + animaux, voir screens/labels/animal_sprite_library.py). --> {% macro render_personnage_gallery(mode, gallery_id, selected_forge_character=None) %} -