Bibliothèque de sprites animaux CraftPix (1/3) : catalogue, galerie, accès admin
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/<slug> 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 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
43abfc9b93
commit
449c36fd5d
@@ -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)
|
||||||
@@ -3,6 +3,7 @@ from flask import request, redirect, url_for
|
|||||||
import screens
|
import screens
|
||||||
|
|
||||||
from core.flask_app import app
|
from core.flask_app import app
|
||||||
|
from core.sprite_gate import forbid_admin_only_character
|
||||||
|
|
||||||
|
|
||||||
@app.route("/game/<slug>/screens/<int:screen_id>/elements/add", methods=["POST"])
|
@app.route("/game/<slug>/screens/<int:screen_id>/elements/add", methods=["POST"])
|
||||||
@@ -31,5 +32,6 @@ def element_add(slug, screen_id):
|
|||||||
# réellement cliqué.
|
# réellement cliqué.
|
||||||
forge_character = request.form.get("forge_character")
|
forge_character = request.form.get("forge_character")
|
||||||
if forge_character and forge_character in screens.SPRITE_LIBRARY:
|
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})
|
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))
|
return redirect(url_for("screen_edit", slug=slug, screen_id=screen_id, selected=new_id))
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ from flask import request, jsonify
|
|||||||
import screens
|
import screens
|
||||||
|
|
||||||
from core.flask_app import app
|
from core.flask_app import app
|
||||||
|
from core.sprite_gate import forbid_admin_only_character
|
||||||
|
|
||||||
|
|
||||||
@app.route("/game/<slug>/elements/<int:element_id>/personnage", methods=["POST"])
|
@app.route("/game/<slug>/elements/<int:element_id>/personnage", methods=["POST"])
|
||||||
@@ -16,6 +17,8 @@ def element_set_personnage_data(slug, element_id):
|
|||||||
el = screens.get_element(slug, element_id)
|
el = screens.get_element(slug, element_id)
|
||||||
if not el:
|
if not el:
|
||||||
return jsonify({"error": "élément introuvable"}), 404
|
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)
|
screens.set_personnage_data(slug, element_id, body)
|
||||||
el = screens.get_element(slug, element_id)
|
el = screens.get_element(slug, element_id)
|
||||||
return jsonify({"ok": True, "animations": screens.resolve_personnage_animations(el)})
|
return jsonify({"ok": True, "animations": screens.resolve_personnage_animations(el)})
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import json
|
import json
|
||||||
|
|
||||||
from flask import render_template, request, abort
|
from flask import render_template, request, abort, g
|
||||||
|
|
||||||
import db
|
import db
|
||||||
import screens
|
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}
|
screens_map = {s["id"]: s["name"] for s in all_screens}
|
||||||
blocks_view = screens.blocks_view(flow_blocks, flow_nodes, object_labels)
|
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(
|
return render_template(
|
||||||
"scene_edit.html",
|
"scene_edit.html",
|
||||||
game=game, screen=screen, all_screens=all_screens,
|
game=game, screen=screen, all_screens=all_screens,
|
||||||
objects=objects, selected_id=selected_id, personnage_data=personnage_data,
|
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_animations_map_json=json.dumps(element_animations_map),
|
||||||
element_orientation_values=screens.ELEMENT_ORIENTATION_VALUES,
|
element_orientation_values=screens.ELEMENT_ORIENTATION_VALUES,
|
||||||
element_orientation_labels_json=json.dumps(screens.ELEMENT_ORIENTATION_LABELS),
|
element_orientation_labels_json=json.dumps(screens.ELEMENT_ORIENTATION_LABELS),
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ from flask import request, redirect, url_for
|
|||||||
import screens
|
import screens
|
||||||
|
|
||||||
from core.flask_app import app
|
from core.flask_app import app
|
||||||
|
from core.sprite_gate import forbid_admin_only_character
|
||||||
|
|
||||||
|
|
||||||
@app.route("/game/<slug>/screens/<int:screen_id>/scene-objects/add", methods=["POST"])
|
@app.route("/game/<slug>/screens/<int:screen_id>/scene-objects/add", methods=["POST"])
|
||||||
@@ -12,6 +13,8 @@ def scene_object_add(slug, screen_id):
|
|||||||
add_scene_object.py)."""
|
add_scene_object.py)."""
|
||||||
kind = request.form.get("kind", "personnage")
|
kind = request.form.get("kind", "personnage")
|
||||||
forge_character = request.form.get("forge_character")
|
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)
|
new_id = screens.add_scene_object(slug, screen_id, kind=kind, forge_character=forge_character)
|
||||||
# Même route que l'éditeur document (/game/<slug>/screens/<id>/edit,
|
# Même route que l'éditeur document (/game/<slug>/screens/<id>/edit,
|
||||||
# endpoint "screen_edit") — c'est CE dispatcher qui bascule vers la
|
# endpoint "screen_edit") — c'est CE dispatcher qui bascule vers la
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ from flask import request, jsonify
|
|||||||
import screens
|
import screens
|
||||||
|
|
||||||
from core.flask_app import app
|
from core.flask_app import app
|
||||||
|
from core.sprite_gate import forbid_admin_only_character
|
||||||
|
|
||||||
|
|
||||||
@app.route("/game/<slug>/scene-objects/<int:object_id>/personnage", methods=["POST"])
|
@app.route("/game/<slug>/scene-objects/<int:object_id>/personnage", methods=["POST"])
|
||||||
@@ -17,6 +18,8 @@ def scene_object_personnage_data(slug, object_id):
|
|||||||
obj = screens.get_scene_object(slug, object_id)
|
obj = screens.get_scene_object(slug, object_id)
|
||||||
if not obj:
|
if not obj:
|
||||||
return jsonify({"error": "objet introuvable"}), 404
|
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)
|
screens.set_scene_object_personnage_data(slug, object_id, body)
|
||||||
obj = screens.get_scene_object(slug, object_id)
|
obj = screens.get_scene_object(slug, object_id)
|
||||||
return jsonify({"ok": True, "animations": screens.resolve_personnage_animations(obj)})
|
return jsonify({"ok": True, "animations": screens.resolve_personnage_animations(obj)})
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import json
|
import json
|
||||||
|
|
||||||
from flask import render_template, request, abort
|
from flask import render_template, request, abort, g
|
||||||
|
|
||||||
import db
|
import db
|
||||||
import screens
|
import screens
|
||||||
@@ -78,6 +78,13 @@ def screen_edit(slug, screen_id):
|
|||||||
|
|
||||||
blocks_view = screens.blocks_view(flow_blocks, flow_nodes, elements_labels)
|
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(
|
return render_template(
|
||||||
"screen_edit.html",
|
"screen_edit.html",
|
||||||
game=game, screen=screen, elements=elements, top_level_elements=top_level_elements,
|
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_surbrillance_values=screens.ELEMENT_SURBRILLANCE_VALUES,
|
||||||
element_desactive_values=screens.ELEMENT_DESACTIVE_VALUES,
|
element_desactive_values=screens.ELEMENT_DESACTIVE_VALUES,
|
||||||
element_orientation_values=screens.ELEMENT_ORIENTATION_VALUES,
|
element_orientation_values=screens.ELEMENT_ORIENTATION_VALUES,
|
||||||
sprite_library=screens.SPRITE_LIBRARY,
|
sprite_library=sprite_library, sprite_library_families=sprite_library_families,
|
||||||
sprite_library_json=json.dumps(screens.SPRITE_LIBRARY),
|
sprite_library_json=json.dumps(sprite_library),
|
||||||
personnage_data=personnage_data,
|
personnage_data=personnage_data,
|
||||||
element_animations_map_json=json.dumps(element_animations_map),
|
element_animations_map_json=json.dumps(element_animations_map),
|
||||||
data_operations=screens.DATA_OPERATIONS,
|
data_operations=screens.DATA_OPERATIONS,
|
||||||
|
|||||||
+13
-1
@@ -55,7 +55,19 @@ from .labels.element_action_properties import (
|
|||||||
)
|
)
|
||||||
from .labels.element_visibility import ELEMENT_VISIBILITY_VALUES, ELEMENT_VISIBILITY_LABELS
|
from .labels.element_visibility import ELEMENT_VISIBILITY_VALUES, ELEMENT_VISIBILITY_LABELS
|
||||||
from .labels.data_operations import DATA_OPERATIONS, DATA_OPERATION_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 (
|
from .flow.constants import (
|
||||||
TRIGGER_EVENTS, CONDITION_OPERATORS, CONDITION_OPERATOR_LABELS, CLICKED_ROW_ID, LAST_INSERTED_ROW_ID,
|
TRIGGER_EVENTS, CONDITION_OPERATORS, CONDITION_OPERATOR_LABELS, CLICKED_ROW_ID, LAST_INSERTED_ROW_ID,
|
||||||
|
|||||||
@@ -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/<animal>/<NN>/<pose><i>.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()
|
||||||
@@ -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
|
||||||
@@ -48,7 +48,12 @@ def _animations_for(slug):
|
|||||||
return animations
|
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)}
|
slug: {"label": label, "animations": _animations_for(slug)}
|
||||||
for slug, label in CHARACTERS.items()
|
for slug, label in CHARACTERS.items()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,14 @@
|
|||||||
import json
|
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"}
|
_DEFAULT_PERSONNAGE_DATA = {"source": "forge", "forge_character": "male-adventurer"}
|
||||||
|
|
||||||
@@ -26,7 +34,7 @@ def resolve_personnage_animations(el):
|
|||||||
data = resolve_personnage_data(el)
|
data = resolve_personnage_data(el)
|
||||||
if data.get("source") == "custom":
|
if data.get("source") == "custom":
|
||||||
return data.get("animations") or {}
|
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 {}
|
return character["animations"] if character else {}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -22,8 +22,9 @@ def add_scene_object(slug, scene_id, kind="personnage", forge_character=None):
|
|||||||
if kind == "personnage":
|
if kind == "personnage":
|
||||||
data = dict(_DEFAULT_PERSONNAGE_DATA)
|
data = dict(_DEFAULT_PERSONNAGE_DATA)
|
||||||
if forge_character:
|
if forge_character:
|
||||||
from ..labels.sprite_library import SPRITE_LIBRARY
|
from ..labels.sprite_library import PUBLIC_SPRITE_LIBRARY
|
||||||
if forge_character in 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
|
data["forge_character"] = forge_character
|
||||||
attributes["_personnage_data"] = json.dumps(data)
|
attributes["_personnage_data"] = json.dumps(data)
|
||||||
conn = db.connect(slug)
|
conn = db.connect(slug)
|
||||||
|
|||||||
@@ -67,14 +67,14 @@
|
|||||||
<details class="propGroup" open>
|
<details class="propGroup" open>
|
||||||
<summary>🎭 Personnages</summary>
|
<summary>🎭 Personnages</summary>
|
||||||
<div class="propGroupBody">
|
<div class="propGroupBody">
|
||||||
<div class="iconGallery">
|
<div class="iconGallery personnageGallery">
|
||||||
{% for slug_c, character in sprite_library.items() %}
|
{% for slug_c, character in sprite_library_families %}
|
||||||
<button type="button" class="iconGalleryTile" title="{{ character.label }}" onclick="addSceneObject('personnage', '{{ slug_c }}')">
|
<button type="button" class="iconGalleryTile" title="{{ character.label }}" onclick="addSceneObject('personnage', '{{ slug_c }}')">
|
||||||
<img src="/static/characters/{{ slug_c }}/idle.png" alt="{{ character.label }}" class="personnageThumb">
|
<img src="{{ character.animations.idle[0] }}" alt="{{ character.label }}" class="personnageThumb">
|
||||||
</button>
|
</button>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
<p class="help">Personnage Kenney prêt à l'emploi (licence libre CC0), avec toutes ses animations.</p>
|
<p class="help">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.</p>
|
||||||
</div>
|
</div>
|
||||||
</details>
|
</details>
|
||||||
<button type="button" class="button is-small is-fullwidth" onclick="addSceneObject('decor')">🖼️ + Ajouter un décor</button>
|
<button type="button" class="button is-small is-fullwidth" onclick="addSceneObject('decor')">🖼️ + Ajouter un décor</button>
|
||||||
@@ -629,14 +629,26 @@
|
|||||||
<summary>🎭 Personnage</summary>
|
<summary>🎭 Personnage</summary>
|
||||||
<div class="propGroupBody" id="personnagePanelBody" data-element-id="{{ obj.id }}">
|
<div class="propGroupBody" id="personnagePanelBody" data-element-id="{{ obj.id }}">
|
||||||
<p class="help">Personnage Forge — clique un autre pour en changer.</p>
|
<p class="help">Personnage Forge — clique un autre pour en changer.</p>
|
||||||
<div class="iconGallery" id="personnageGallerySwap">
|
<div class="iconGallery personnageGallery" id="personnageGallerySwap">
|
||||||
{% for slug_c, character in sprite_library.items() %}
|
{% for slug_c, character in sprite_library_families %}
|
||||||
<button type="button" class="iconGalleryTile {{ 'selected' if slug_c == personnage_data.forge_character else '' }}"
|
<button type="button" class="iconGalleryTile {{ 'selected' if slug_c == personnage_data.forge_character else '' }}"
|
||||||
title="{{ character.label }}" onclick="swapSceneObjectCharacter('{{ slug_c }}')">
|
title="{{ character.label }}" onclick="swapSceneObjectCharacter('{{ slug_c }}')">
|
||||||
<img src="/static/characters/{{ slug_c }}/idle.png" alt="{{ character.label }}" class="personnageThumb">
|
<img src="{{ character.animations.idle[0] }}" alt="{{ character.label }}" class="personnageThumb">
|
||||||
</button>
|
</button>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
|
{% set current_character = sprite_library.get(personnage_data.forge_character) %}
|
||||||
|
{% if current_character and current_character.get('family') %}
|
||||||
|
<p class="help">Variantes de {{ current_character.family_label }} :</p>
|
||||||
|
<div class="iconGallery personnageGallery" id="personnageGalleryVariants">
|
||||||
|
{% for slug_c, character in sprite_library.items() if character.get('family') == current_character.family %}
|
||||||
|
<button type="button" class="iconGalleryTile {{ 'selected' if slug_c == personnage_data.forge_character else '' }}"
|
||||||
|
title="{{ character.label }}" onclick="swapSceneObjectCharacter('{{ slug_c }}')">
|
||||||
|
<img src="{{ character.animations.idle[0] }}" alt="{{ character.label }}" class="personnageThumb">
|
||||||
|
</button>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</details>
|
</details>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|||||||
@@ -43,14 +43,40 @@
|
|||||||
<!-- Phase 8 — galerie visuelle de personnages Forge (VRAIES miniatures,
|
<!-- Phase 8 — galerie visuelle de personnages Forge (VRAIES miniatures,
|
||||||
pas un emoji/icône), réutilisée deux fois : "add" pour poser un
|
pas un emoji/icône), réutilisée deux fois : "add" pour poser un
|
||||||
nouveau personnage (panneau gauche), "swap" pour changer le
|
nouveau personnage (panneau gauche), "swap" pour changer le
|
||||||
personnage Forge d'un élément déjà posé (panneau droit, propriétés). -->
|
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) %}
|
{% macro render_personnage_gallery(mode, gallery_id, selected_forge_character=None) %}
|
||||||
<div class="iconGallery" id="{{ gallery_id }}">
|
<div class="iconGallery personnageGallery" id="{{ gallery_id }}">
|
||||||
{% for char_slug, character in sprite_library.items() %}
|
{% for char_slug, character in sprite_library_families %}
|
||||||
<button type="button" class="iconGalleryTile {{ 'selected' if char_slug == selected_forge_character else '' }}"
|
<button type="button" class="iconGalleryTile {{ 'selected' if char_slug == selected_forge_character else '' }}"
|
||||||
title="{{ character.label }}"
|
title="{{ character.label }}"
|
||||||
onclick="{{ 'addPersonnageElement' if mode == 'add' else 'swapPersonnageCharacter' }}('{{ char_slug }}')">
|
onclick="{{ 'addPersonnageElement' if mode == 'add' else 'swapPersonnageCharacter' }}('{{ char_slug }}')">
|
||||||
<img src="/static/characters/{{ char_slug }}/idle.png" alt="{{ character.label }}" class="personnageThumb">
|
<img src="{{ character.animations.idle[0] }}" alt="{{ character.label }}" class="personnageThumb">
|
||||||
|
</button>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
{% endmacro %}
|
||||||
|
|
||||||
|
<!-- Variantes de couleur/skin d'UNE famille d'animal (les 15 personnages
|
||||||
|
CraftPix qui partagent le même "family", voir screens/labels/
|
||||||
|
animal_sprite_library.py) — affichée uniquement dans le panneau de
|
||||||
|
propriétés, sous la galerie groupée ci-dessus, quand le personnage
|
||||||
|
sélectionné appartient à une famille à variantes (jamais pour un
|
||||||
|
Kenney, qui n'a pas de "family"). -->
|
||||||
|
{% macro render_variant_gallery(family, gallery_id, selected_forge_character=None) %}
|
||||||
|
<div class="iconGallery personnageGallery" id="{{ gallery_id }}">
|
||||||
|
{% for char_slug, character in sprite_library.items() if character.get('family') == family %}
|
||||||
|
<button type="button" class="iconGalleryTile {{ 'selected' if char_slug == selected_forge_character else '' }}"
|
||||||
|
title="{{ character.label }}"
|
||||||
|
onclick="swapPersonnageCharacter('{{ char_slug }}')">
|
||||||
|
<img src="{{ character.animations.idle[0] }}" alt="{{ character.label }}" class="personnageThumb">
|
||||||
</button>
|
</button>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
@@ -902,6 +928,11 @@
|
|||||||
<div class="propGroupBody" id="personnagePanelBody" data-element-id="{{ el.id }}">
|
<div class="propGroupBody" id="personnagePanelBody" data-element-id="{{ el.id }}">
|
||||||
<p class="help">Personnage Forge — clique un autre pour en changer.</p>
|
<p class="help">Personnage Forge — clique un autre pour en changer.</p>
|
||||||
{{ render_personnage_gallery('swap', 'personnageGallerySwap', personnage_data.forge_character) }}
|
{{ render_personnage_gallery('swap', 'personnageGallerySwap', personnage_data.forge_character) }}
|
||||||
|
{% set current_character = sprite_library.get(personnage_data.forge_character) %}
|
||||||
|
{% if current_character and current_character.get('family') %}
|
||||||
|
<p class="help">Variantes de {{ current_character.family_label }} :</p>
|
||||||
|
{{ render_variant_gallery(current_character.family, 'personnageGalleryVariants', personnage_data.forge_character) }}
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</details>
|
</details>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|||||||
@@ -74,6 +74,46 @@ def client():
|
|||||||
yield c
|
yield c
|
||||||
|
|
||||||
|
|
||||||
|
_TEST_USER_COUNTER = [0]
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def user_client():
|
||||||
|
"""Un compte "user" (jamais le premier de la session de tests, donc
|
||||||
|
jamais admin — voir auth/create_user.py), connecté mais SANS projet
|
||||||
|
assigné pour l'instant (voir la fixture `user_game` ci-dessous) —
|
||||||
|
pour les tests qui vérifient qu'un rôle non-admin voit/peut moins de
|
||||||
|
choses qu'un admin (ex. galerie de sprites animaux CraftPix, voir
|
||||||
|
core/sprite_gate.py)."""
|
||||||
|
_TEST_USER_COUNTER[0] += 1
|
||||||
|
email = f"pytest-user-{_TEST_USER_COUNTER[0]}@pytest.local"
|
||||||
|
user_id = auth.create_user(email, "Test1234!", "Test", "User")
|
||||||
|
auth.confirm_totp(user_id)
|
||||||
|
flask_app.config["TESTING"] = True
|
||||||
|
with flask_app.test_client() as c:
|
||||||
|
with c.session_transaction() as sess:
|
||||||
|
sess["user_id"] = user_id
|
||||||
|
yield c
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def user_game(client, user_client):
|
||||||
|
"""Un jeu appartenant au compte `user_client` (project_slug assigné,
|
||||||
|
voir auth.set_project_slug — même mécanisme que l'inscription réelle,
|
||||||
|
routes/auth/register_2fa.py). Créé via le client ADMIN (`client`,
|
||||||
|
illimité) puis rattaché, pour ne pas dépendre du parcours
|
||||||
|
d'inscription complet dans les tests qui n'en ont pas besoin."""
|
||||||
|
resp = client.post("/games/new", data={"name": "pytest_user_game"}, follow_redirects=False)
|
||||||
|
assert resp.status_code == 302
|
||||||
|
slug = resp.headers["Location"].rstrip("/").split("/")[-1]
|
||||||
|
with user_client.session_transaction() as sess:
|
||||||
|
auth.set_project_slug(sess["user_id"], slug)
|
||||||
|
yield slug
|
||||||
|
game_dir = db.game_dir(slug)
|
||||||
|
if os.path.isdir(game_dir):
|
||||||
|
shutil.rmtree(game_dir)
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def game(client):
|
def game(client):
|
||||||
"""Crée un jeu de test frais et le supprime après le test, quel que
|
"""Crée un jeu de test frais et le supprime après le test, quel que
|
||||||
|
|||||||
@@ -0,0 +1,160 @@
|
|||||||
|
"""Bibliothèque de sprites animaux CraftPix (screens.ADMIN_SPRITE_LIBRARY,
|
||||||
|
screens/labels/animal_sprite_library.py) — réservée 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 ni la voir dans la
|
||||||
|
galerie de l'éditeur, ni pouvoir l'assigner par un POST direct."""
|
||||||
|
import json
|
||||||
|
|
||||||
|
import screens
|
||||||
|
|
||||||
|
|
||||||
|
def test_admin_sprite_library_loaded_from_manifest():
|
||||||
|
assert len(screens.ADMIN_SPRITE_LIBRARY) > 0
|
||||||
|
bunny = screens.ADMIN_SPRITE_LIBRARY["animal-bunny-01"]
|
||||||
|
assert bunny["family"] == "bunny"
|
||||||
|
assert bunny["animations"]["idle"][0] == "/static/characters/animals/bunny/01/idle0.png"
|
||||||
|
assert len(bunny["animations"]["idle"]) == 20
|
||||||
|
|
||||||
|
|
||||||
|
def test_sprite_library_is_public_plus_admin_merged():
|
||||||
|
assert set(screens.SPRITE_LIBRARY) == set(screens.PUBLIC_SPRITE_LIBRARY) | set(screens.ADMIN_SPRITE_LIBRARY)
|
||||||
|
assert screens.ADMIN_ONLY_CHARACTER_SLUGS == set(screens.ADMIN_SPRITE_LIBRARY)
|
||||||
|
assert "male-adventurer" not in screens.ADMIN_ONLY_CHARACTER_SLUGS
|
||||||
|
|
||||||
|
|
||||||
|
def test_sprite_gallery_families_shows_one_tile_per_animal():
|
||||||
|
families = screens.sprite_gallery_families(screens.SPRITE_LIBRARY)
|
||||||
|
slugs = [slug for slug, _ in families]
|
||||||
|
assert len(slugs) == len(screens.PUBLIC_SPRITE_LIBRARY) + 14 # 6 Kenney + 14 familles animales
|
||||||
|
assert "animal-bunny-01" in slugs
|
||||||
|
assert "animal-bunny-02" not in slugs
|
||||||
|
|
||||||
|
|
||||||
|
def test_document_editor_hides_animal_characters_from_a_user_account(user_game, user_client):
|
||||||
|
resp = user_client.post(f"/game/{user_game}/screens/new", data={"name": "Accueil"}, follow_redirects=False)
|
||||||
|
screen_id = resp.headers["Location"].rstrip("/").split("/")[-2]
|
||||||
|
html = user_client.get(f"/game/{user_game}/screens/{screen_id}/edit").get_data(as_text=True)
|
||||||
|
assert "animal-bunny-01" not in html
|
||||||
|
assert "male-adventurer" in html
|
||||||
|
|
||||||
|
|
||||||
|
def test_document_editor_shows_animal_characters_to_admin(game, client):
|
||||||
|
resp = client.post(f"/game/{game}/screens/new", data={"name": "Accueil"}, follow_redirects=False)
|
||||||
|
screen_id = resp.headers["Location"].rstrip("/").split("/")[-2]
|
||||||
|
html = client.get(f"/game/{game}/screens/{screen_id}/edit").get_data(as_text=True)
|
||||||
|
assert "animal-bunny-01" in html
|
||||||
|
|
||||||
|
|
||||||
|
def _create_jeu2d_game_as(post_client, name, slug_owner_client=None):
|
||||||
|
resp = post_client.post("/games/new", data={"name": name, "game_type": "jeu_2d"}, follow_redirects=False)
|
||||||
|
assert resp.status_code == 302
|
||||||
|
return resp.headers["Location"].rstrip("/").split("/")[-1]
|
||||||
|
|
||||||
|
|
||||||
|
def test_scene_editor_hides_animal_characters_from_a_user_account(client, user_client, tmp_game_slug_cleanup):
|
||||||
|
slug = tmp_game_slug_cleanup(_create_jeu2d_game_as(client, "pytest_animal_scene_user"))
|
||||||
|
with user_client.session_transaction() as sess:
|
||||||
|
import auth
|
||||||
|
auth.set_project_slug(sess["user_id"], slug)
|
||||||
|
screen_id = screens.create_screen(slug, "Scène 1")
|
||||||
|
html = user_client.get(f"/game/{slug}/screens/{screen_id}/edit").get_data(as_text=True)
|
||||||
|
assert "animal-bunny-01" not in html
|
||||||
|
|
||||||
|
|
||||||
|
def test_scene_object_add_rejects_animal_character_for_a_user_account(client, user_client, tmp_game_slug_cleanup):
|
||||||
|
slug = tmp_game_slug_cleanup(_create_jeu2d_game_as(client, "pytest_animal_scene_gate"))
|
||||||
|
with user_client.session_transaction() as sess:
|
||||||
|
import auth
|
||||||
|
auth.set_project_slug(sess["user_id"], slug)
|
||||||
|
screen_id = screens.create_screen(slug, "Scène 1")
|
||||||
|
resp = user_client.post(
|
||||||
|
f"/game/{slug}/screens/{screen_id}/scene-objects/add",
|
||||||
|
data={"kind": "personnage", "forge_character": "animal-bunny-01"},
|
||||||
|
)
|
||||||
|
assert resp.status_code == 403
|
||||||
|
|
||||||
|
|
||||||
|
def test_scene_object_add_allows_animal_character_for_admin(client, tmp_game_slug_cleanup):
|
||||||
|
slug = tmp_game_slug_cleanup(_create_jeu2d_game_as(client, "pytest_animal_scene_admin"))
|
||||||
|
screen_id = screens.create_screen(slug, "Scène 1")
|
||||||
|
resp = client.post(
|
||||||
|
f"/game/{slug}/screens/{screen_id}/scene-objects/add",
|
||||||
|
data={"kind": "personnage", "forge_character": "animal-bunny-01"},
|
||||||
|
follow_redirects=False,
|
||||||
|
)
|
||||||
|
assert resp.status_code == 302
|
||||||
|
obj_id = int(resp.headers["Location"].rsplit("selected=", 1)[1])
|
||||||
|
obj = screens.get_scene_object(slug, obj_id)
|
||||||
|
assert screens.resolve_personnage_data(obj)["forge_character"] == "animal-bunny-01"
|
||||||
|
|
||||||
|
|
||||||
|
def test_scene_object_personnage_swap_rejects_animal_character_for_a_user_account(client, user_client, tmp_game_slug_cleanup):
|
||||||
|
slug = tmp_game_slug_cleanup(_create_jeu2d_game_as(client, "pytest_animal_swap_gate"))
|
||||||
|
with user_client.session_transaction() as sess:
|
||||||
|
import auth
|
||||||
|
auth.set_project_slug(sess["user_id"], slug)
|
||||||
|
screen_id = screens.create_screen(slug, "Scène 1")
|
||||||
|
obj_id = screens.add_scene_object(slug, screen_id)
|
||||||
|
resp = user_client.post(
|
||||||
|
f"/game/{slug}/scene-objects/{obj_id}/personnage",
|
||||||
|
data=json.dumps({"source": "forge", "forge_character": "animal-bunny-01"}),
|
||||||
|
content_type="application/json",
|
||||||
|
)
|
||||||
|
assert resp.status_code == 403
|
||||||
|
|
||||||
|
|
||||||
|
def test_element_add_rejects_animal_character_for_a_user_account(user_game, user_client):
|
||||||
|
resp = user_client.post(f"/game/{user_game}/screens/new", data={"name": "Accueil"}, follow_redirects=False)
|
||||||
|
screen_id = resp.headers["Location"].rstrip("/").split("/")[-2]
|
||||||
|
resp = user_client.post(
|
||||||
|
f"/game/{user_game}/screens/{screen_id}/elements/add",
|
||||||
|
data={"widget": "personnage", "forge_character": "animal-bunny-01"},
|
||||||
|
)
|
||||||
|
assert resp.status_code == 403
|
||||||
|
|
||||||
|
|
||||||
|
def test_element_set_personnage_data_rejects_animal_character_for_a_user_account(user_game, user_client):
|
||||||
|
resp = user_client.post(f"/game/{user_game}/screens/new", data={"name": "Accueil"}, follow_redirects=False)
|
||||||
|
screen_id = resp.headers["Location"].rstrip("/").split("/")[-2]
|
||||||
|
resp = user_client.post(
|
||||||
|
f"/game/{user_game}/screens/{screen_id}/elements/add", data={"widget": "personnage"}, follow_redirects=False
|
||||||
|
)
|
||||||
|
element_id = int(resp.headers["Location"].rsplit("selected=", 1)[1])
|
||||||
|
resp = user_client.post(
|
||||||
|
f"/game/{user_game}/elements/{element_id}/personnage",
|
||||||
|
data=json.dumps({"source": "forge", "forge_character": "animal-bunny-01"}),
|
||||||
|
content_type="application/json",
|
||||||
|
)
|
||||||
|
assert resp.status_code == 403
|
||||||
|
|
||||||
|
|
||||||
|
def test_list_used_forge_characters_scans_elements_and_scene_objects(client, tmp_game_slug_cleanup):
|
||||||
|
slug = tmp_game_slug_cleanup(_create_jeu2d_game_as(client, "pytest_used_characters"))
|
||||||
|
screen_id = screens.create_screen(slug, "Scène 1")
|
||||||
|
screens.add_scene_object(slug, screen_id, forge_character="animal-bunny-01")
|
||||||
|
screens.add_scene_object(slug, screen_id, kind="decor")
|
||||||
|
used = screens.list_used_forge_characters(slug)
|
||||||
|
assert used == {"animal-bunny-01"}
|
||||||
|
|
||||||
|
|
||||||
|
def test_build_package_only_copies_used_animal_character_folders(client, tmp_game_slug_cleanup, tmp_path):
|
||||||
|
slug = tmp_game_slug_cleanup(_create_jeu2d_game_as(client, "pytest_publish_animals"))
|
||||||
|
screen_id = screens.create_screen(slug, "Scène 1")
|
||||||
|
screens.add_scene_object(slug, screen_id, forge_character="animal-bunny-01")
|
||||||
|
|
||||||
|
from publish import build_package
|
||||||
|
zip_path, _name = build_package.build_game_zip(slug)
|
||||||
|
try:
|
||||||
|
import zipfile
|
||||||
|
with zipfile.ZipFile(zip_path) as zf:
|
||||||
|
names = zf.namelist()
|
||||||
|
animal_paths = {n for n in names if "static/characters/animals/" in n and not n.endswith("manifest.json")}
|
||||||
|
animals_used = {n.split("static/characters/animals/")[1].split("/")[0] for n in animal_paths}
|
||||||
|
assert animals_used == {"bunny"}
|
||||||
|
assert any("static/characters/animals/bunny/01/" in n for n in animal_paths)
|
||||||
|
assert not any("static/characters/animals/bear/" in n for n in animal_paths)
|
||||||
|
assert any(n.endswith("static/characters/animals/manifest.json") for n in names)
|
||||||
|
assert any("static/characters/male-adventurer/" in n for n in names)
|
||||||
|
finally:
|
||||||
|
import os
|
||||||
|
os.remove(zip_path)
|
||||||
@@ -117,10 +117,16 @@ def test_resolve_personnage_idle_frame_falls_back_to_first_animation(client, gam
|
|||||||
|
|
||||||
|
|
||||||
def test_screen_edit_exposes_personnage_gallery_and_scripts(client, game):
|
def test_screen_edit_exposes_personnage_gallery_and_scripts(client, game):
|
||||||
|
"""Les 6 Kenney (screens.PUBLIC_SPRITE_LIBRARY) ont chacun leur tile —
|
||||||
|
les sprites animaux CraftPix (screens.ADMIN_SPRITE_LIBRARY), eux, sont
|
||||||
|
groupés par famille dans cette même galerie (un seul tile par animal,
|
||||||
|
voir screens.sprite_gallery_families et le plan "Bibliothèque de
|
||||||
|
sprites animaux CraftPix"), pas testés ici un par un."""
|
||||||
screen_id = _create_screen(client, game)
|
screen_id = _create_screen(client, game)
|
||||||
html = client.get(f"/game/{game}/screens/{screen_id}/edit").get_data(as_text=True)
|
html = client.get(f"/game/{game}/screens/{screen_id}/edit").get_data(as_text=True)
|
||||||
for slug in screens.SPRITE_LIBRARY:
|
for slug in screens.PUBLIC_SPRITE_LIBRARY:
|
||||||
assert f"/static/characters/{slug}/idle.png" in html
|
assert f"/static/characters/{slug}/idle.png" in html
|
||||||
|
assert "/static/characters/animals/bunny/01/idle0.png" in html
|
||||||
assert "personnageGallery" in html
|
assert "personnageGallery" in html
|
||||||
# L'import de sprites personnalisés est retiré pour le moment.
|
# L'import de sprites personnalisés est retiré pour le moment.
|
||||||
assert "Sprite personnalisé" not in html
|
assert "Sprite personnalisé" not in html
|
||||||
@@ -130,11 +136,16 @@ def test_screen_edit_exposes_personnage_gallery_and_scripts(client, game):
|
|||||||
|
|
||||||
|
|
||||||
def test_sprite_library_has_all_six_characters_with_all_animations(client, game):
|
def test_sprite_library_has_all_six_characters_with_all_animations(client, game):
|
||||||
assert set(screens.SPRITE_LIBRARY) == {
|
"""screens.PUBLIC_SPRITE_LIBRARY (les 6 Kenney, CC0) — distinct de
|
||||||
|
screens.SPRITE_LIBRARY, qui inclut aussi les sprites animaux CraftPix
|
||||||
|
(screens.ADMIN_SPRITE_LIBRARY, réservés au rôle admin, voir le plan
|
||||||
|
"Bibliothèque de sprites animaux CraftPix" et
|
||||||
|
test_admin_sprite_library_loaded_from_manifest)."""
|
||||||
|
assert set(screens.PUBLIC_SPRITE_LIBRARY) == {
|
||||||
"male-adventurer", "female-adventurer", "male-person",
|
"male-adventurer", "female-adventurer", "male-person",
|
||||||
"female-person", "robot", "zombie",
|
"female-person", "robot", "zombie",
|
||||||
}
|
}
|
||||||
for character in screens.SPRITE_LIBRARY.values():
|
for character in screens.PUBLIC_SPRITE_LIBRARY.values():
|
||||||
names = set(character["animations"])
|
names = set(character["animations"])
|
||||||
assert {"idle", "walk", "run", "jump", "attack"} <= names
|
assert {"idle", "walk", "run", "jump", "attack"} <= names
|
||||||
assert len(character["animations"]["walk"]) == 8
|
assert len(character["animations"]["walk"]) == 8
|
||||||
|
|||||||
Reference in New Issue
Block a user