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) %} -