Phase 3 : hardening qualite de code - typage strict, securite, dead code, a11y
Config strictement stricte partout (ruff, mypy --strict, bandit, vulture, import-linter, eslint, stylelint), aucune regle desactivee "pour ne pas casser le build" - l'existant a ete corrige pour la satisfaire plutot que l'inverse. Hooks pre-commit locaux (language: system) bloquants. - Typage mypy --strict propage a tout le moteur (db, screens, auth, core, ai, routes, puis publish/scripts/tests/app.py/build_css.py). - Securite : fuite de handle fichier Windows corrigee dans l'export SCORM (routes/publish/export_scorm.py), CSRF/RNG non-crypto/xAPI documentes (# nosec, # NOSONAR justifies), nouveau db.json_for_script() (echappe "</script>" dans le JSON embarque en <script>, 25 sites). - Architecture : imports circulaires/F811 nettoyes, contrats import-linter respectes, code mort retire (vulture). - Accessibilite : 69 champs de formulaire sans label correctement associe corriges (for/id ou aria-label) sur 11 templates. - ESLint/Stylelint : lot mecanique JS/CSS, regles ajustees puis appliquees (aucune desactivee sans verification individuelle). - Tests : isolation du compte admin partage (nettoyage ponctuel + fixture de teardown automatique en filet de securite), suite complete verte (591 tests Python, 241 tests JS). - SonarQube Community Build self-heberge (Docker + PostgreSQL) : rapport complet analyse point par point, faux positifs documentes. - .gitattributes ajoute (LF force) : core.autocrlf=true sur cette machine faisait echouer ESLint (linebreak-style) via un bug connu de git (checkout "en place" qui ignore l'eol force sur un fichier deja present sur disque - contourne en supprimant puis recreant chaque fichier suivi). djLint (H021, styles inline) volontairement saute pour ce commit - backlog assume, deja documente, traite dans un lot separe. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
7db4803b93
commit
c57420c8c9
+289
-118
@@ -18,16 +18,150 @@ utilisée par app.py et les templates, afin que `import screens` continue
|
||||
de fonctionner à l'identique.
|
||||
"""
|
||||
|
||||
from .animations.add_animation_clip import add_animation_clip
|
||||
from .animations.animate_css_catalog import ANIMATE_CSS_CATALOG, ANIMATE_CSS_LABELS
|
||||
from .animations.delete_animation_clip import delete_animation_clip
|
||||
from .animations.ensure_animation_schema import ensure_animation_schema
|
||||
from .animations.get_animation_clip import get_animation_clip
|
||||
from .animations.list_animation_clips import list_animation_clips
|
||||
from .animations.update_animation_clip import update_animation_clip
|
||||
from .custom_events.delete_custom_event import delete_custom_event
|
||||
from .custom_events.list_custom_event_usages import list_custom_event_usages
|
||||
from .data_actions.apply_add_row_action import apply_add_row_action
|
||||
from .data_actions.apply_data_action import apply_data_action
|
||||
from .data_actions.apply_score_action import apply_score_action
|
||||
from .data_actions.apply_status_action import apply_status_action
|
||||
from .data_actions.apply_variable_action import apply_variable_action
|
||||
from .data_definitions.data_definition_options import data_definition_options
|
||||
|
||||
from .labels.action_type_labels import ACTION_TYPE_LABELS
|
||||
from .labels.element_action_properties import (
|
||||
ELEMENT_ORIENTATION_VALUES, ELEMENT_ORIENTATION_LABELS,
|
||||
from .flow.add_flow_edge import add_flow_edge
|
||||
from .flow.add_flow_node import add_flow_node
|
||||
from .flow.blocks.blocks_view import blocks_view
|
||||
from .flow.blocks.create_flow_block import create_flow_block
|
||||
from .flow.blocks.delete_flow_block import delete_flow_block
|
||||
from .flow.blocks.get_flow_block import get_flow_block
|
||||
from .flow.blocks.list_flow_blocks import list_flow_blocks
|
||||
from .flow.blocks.update_flow_block import update_flow_block
|
||||
from .flow.constants import (
|
||||
CLICKED_ROW_ID,
|
||||
CONDITION_OPERATOR_LABELS,
|
||||
CONDITION_OPERATORS,
|
||||
LAST_INSERTED_ROW_ID,
|
||||
TRIGGER_EVENTS,
|
||||
)
|
||||
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 PUBLIC_SPRITE_LIBRARY
|
||||
from .flow.delete_flow_edge import delete_flow_edge
|
||||
from .flow.delete_flow_node import delete_flow_node
|
||||
from .flow.ensure_flow_schema import ensure_flow_schema
|
||||
from .flow.get_flow_node import get_flow_node
|
||||
from .flow.list_flow_edges import list_flow_edges
|
||||
from .flow.list_flow_nodes import list_flow_nodes
|
||||
from .flow.move_flow_node import move_flow_node
|
||||
from .ia.add_ia_chat_message import add_ia_chat_message
|
||||
from .ia.create_ia_conversation import create_ia_conversation
|
||||
from .ia.delete_ia_conversation import delete_ia_conversation
|
||||
from .ia.ensure_ia_chat_schema import ensure_ia_chat_schema
|
||||
from .ia.get_ia_conversation import get_ia_conversation
|
||||
from .ia.list_ia_chat_messages import list_ia_chat_messages
|
||||
from .ia.list_ia_conversations import list_ia_conversations
|
||||
from .labels.action_type_labels import ACTION_TYPE_LABELS
|
||||
from .labels.animal_sprite_library import ADMIN_SPRITE_LIBRARY
|
||||
from .labels.background_library import BACKGROUND_LIBRARY
|
||||
from .labels.data_operations import DATA_OPERATION_LABELS, DATA_OPERATIONS
|
||||
from .labels.element_action_properties import (
|
||||
ELEMENT_ORIENTATION_LABELS,
|
||||
ELEMENT_ORIENTATION_VALUES,
|
||||
)
|
||||
from .labels.element_visibility import ELEMENT_VISIBILITY_LABELS, ELEMENT_VISIBILITY_VALUES
|
||||
from .labels.sprite_gallery import sprite_gallery_families
|
||||
from .labels.sprite_library import PUBLIC_SPRITE_LIBRARY
|
||||
from .labels.surbrillance_values import SURBRILLANCE_LABELS, SURBRILLANCE_VALUES
|
||||
from .labels.video_modes import VIDEO_MODE_LABELS, VIDEO_MODES
|
||||
from .payload.full_game_payload import full_game_payload
|
||||
from .rendering.collision_rules import (
|
||||
backfill_missing_leaf_ids,
|
||||
move_action_in_rules,
|
||||
remove_action_from_rules,
|
||||
resolve_collision_rules,
|
||||
sanitize_collision_rules,
|
||||
set_dialogue_lines_in_rules,
|
||||
set_then_action_in_rules,
|
||||
)
|
||||
from .rendering.collision_settings import COLLISION_SHAPES, DEFAULT_COLLISION_SETTINGS, resolve_collision_settings
|
||||
from .rendering.dialogue_box_style import (
|
||||
DEFAULT_DIALOGUE_BOX_STYLE,
|
||||
FONT_WEIGHT_CHOICES,
|
||||
resolve_dialogue_box_style,
|
||||
sanitize_dialogue_box_style,
|
||||
)
|
||||
from .rendering.list_used_forge_characters import list_used_forge_characters
|
||||
from .rendering.personnage_commands import (
|
||||
DEFAULT_PERSONNAGE_COMMANDS,
|
||||
resolve_personnage_commands,
|
||||
sanitize_extra_commands,
|
||||
)
|
||||
from .rendering.personnage_data import (
|
||||
resolve_personnage_animations,
|
||||
resolve_personnage_data,
|
||||
resolve_personnage_idle_frame,
|
||||
)
|
||||
from .rendering.personnage_role import (
|
||||
DEFAULT_PERSONNAGE_ROLE,
|
||||
PERSONNAGE_ROLE_LABELS,
|
||||
PERSONNAGE_ROLES,
|
||||
resolve_personnage_role,
|
||||
)
|
||||
from .rendering.quiz_box_config import (
|
||||
DEFAULT_QUIZ_BOX_CONFIG,
|
||||
QUIZ_BOX_DIALOG_TEMPLATE_LABELS,
|
||||
QUIZ_BOX_DIALOG_TEMPLATES,
|
||||
QUIZ_BOX_PAGE_TEMPLATE_LABELS,
|
||||
QUIZ_BOX_PAGE_TEMPLATES,
|
||||
TIMER_MODES,
|
||||
resolve_quiz_box_config,
|
||||
sanitize_quiz_box_config,
|
||||
)
|
||||
from .rendering.scene_object_names import list_named_scene_object_names
|
||||
from .rendering.screen_triggers import TRIGGER_TYPES_SCREEN, resolve_screen_triggers, sanitize_screen_triggers
|
||||
from .rendering.trigger_graph import (
|
||||
collect_all_dialogue_lines,
|
||||
list_all_collision_objects,
|
||||
list_all_triggers,
|
||||
list_completable_dialogue_ids,
|
||||
)
|
||||
from .scenes.add_scene_object import add_scene_object
|
||||
from .scenes.append_action_to_trigger import append_action_to_trigger
|
||||
from .scenes.build_collision_rule_objects import build_collision_rule_objects
|
||||
from .scenes.delete_scene_object import delete_scene_object
|
||||
from .scenes.ensure_scene_schema import ensure_scene_schema
|
||||
from .scenes.flow_palette import ACTION_TYPE_LABELS_2D, OBJECT_ACTION_PROPERTIES, TRIGGER_EVENTS_2D
|
||||
from .scenes.get_scene_object import get_scene_object
|
||||
from .scenes.list_scene_objects import list_scene_objects
|
||||
from .scenes.move_action_in_trigger import move_action_in_trigger
|
||||
from .scenes.remove_action_from_trigger import remove_action_from_trigger
|
||||
from .scenes.render_scene_object import render_scene_object
|
||||
from .scenes.resolve_scene_world_size import resolve_scene_world_size
|
||||
from .scenes.set_scene_object_collision import set_scene_object_collision
|
||||
from .scenes.set_scene_object_collision_rules import set_scene_object_collision_rules
|
||||
from .scenes.set_scene_object_commands import set_scene_object_commands
|
||||
from .scenes.set_scene_object_dialogue_box_style import set_scene_object_dialogue_box_style
|
||||
from .scenes.set_scene_object_name import set_scene_object_name
|
||||
from .scenes.set_scene_object_personnage_data import set_scene_object_personnage_data
|
||||
from .scenes.set_scene_object_quiz_config import set_scene_object_quiz_config
|
||||
from .scenes.set_scene_object_role import set_scene_object_role
|
||||
from .scenes.update_scene_object_geometry import update_scene_object_geometry
|
||||
from .screens_repo.append_action_to_screen_trigger import append_action_to_screen_trigger
|
||||
from .screens_repo.create_screen import create_screen
|
||||
from .screens_repo.delete_screen import delete_screen
|
||||
from .screens_repo.ensure_schema import ensure_schema
|
||||
from .screens_repo.get_screen import get_screen
|
||||
from .screens_repo.list_screens import list_screens
|
||||
from .screens_repo.move_action_in_screen_trigger import move_action_in_screen_trigger
|
||||
from .screens_repo.move_screen import move_screen
|
||||
from .screens_repo.remove_action_from_screen_trigger import remove_action_from_screen_trigger
|
||||
from .screens_repo.rename_screen import rename_screen
|
||||
from .screens_repo.screens_referencing import screens_referencing
|
||||
from .screens_repo.set_scene_size import set_scene_size
|
||||
from .screens_repo.set_screen_background_music import set_screen_background_music
|
||||
from .screens_repo.set_screen_triggers import set_screen_triggers
|
||||
|
||||
# SPRITE_LIBRARY (fusionné) : utilisé par resolve_personnage_animations
|
||||
# pour la résolution RUNTIME (jeu jouable, tous rôles confondus — voir le
|
||||
@@ -37,119 +171,156 @@ from .labels.animal_sprite_library import ADMIN_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 .labels.background_library import BACKGROUND_LIBRARY
|
||||
# Un seul pack aujourd'hui, licence CraftPix — tout BACKGROUND_LIBRARY est
|
||||
# admin-only (voir core/sprite_gate.py::forbid_admin_only_background),
|
||||
# jamais de version "publique" comme pour les personnages Kenney.
|
||||
ADMIN_ONLY_BACKGROUND_SLUGS = set(BACKGROUND_LIBRARY)
|
||||
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,
|
||||
)
|
||||
|
||||
from .rendering.trigger_for import _trigger_for
|
||||
from .rendering.personnage_data import resolve_personnage_data, resolve_personnage_animations, resolve_personnage_idle_frame
|
||||
from .rendering.personnage_role import resolve_personnage_role, PERSONNAGE_ROLES, PERSONNAGE_ROLE_LABELS, DEFAULT_PERSONNAGE_ROLE
|
||||
from .rendering.collision_settings import resolve_collision_settings, DEFAULT_COLLISION_SETTINGS, COLLISION_SHAPES
|
||||
from .rendering.collision_rules import (
|
||||
resolve_collision_rules, sanitize_collision_rules, set_dialogue_lines_in_rules, set_then_action_in_rules,
|
||||
remove_action_from_rules, move_action_in_rules, backfill_missing_leaf_ids,
|
||||
)
|
||||
from .rendering.screen_triggers import resolve_screen_triggers, sanitize_screen_triggers, TRIGGER_TYPES_SCREEN
|
||||
from .labels.surbrillance_values import SURBRILLANCE_VALUES, SURBRILLANCE_LABELS
|
||||
from .labels.video_modes import VIDEO_MODES, VIDEO_MODE_LABELS
|
||||
from .rendering.personnage_commands import resolve_personnage_commands, DEFAULT_PERSONNAGE_COMMANDS, sanitize_extra_commands
|
||||
from .rendering.scene_object_names import list_named_scene_object_names
|
||||
from .rendering.dialogue_box_style import resolve_dialogue_box_style, sanitize_dialogue_box_style, DEFAULT_DIALOGUE_BOX_STYLE, FONT_WEIGHT_CHOICES
|
||||
from .rendering.quiz_box_config import (
|
||||
resolve_quiz_box_config, sanitize_quiz_box_config, DEFAULT_QUIZ_BOX_CONFIG,
|
||||
TIMER_MODES, QUIZ_BOX_DIALOG_TEMPLATES, QUIZ_BOX_DIALOG_TEMPLATE_LABELS,
|
||||
QUIZ_BOX_PAGE_TEMPLATES, QUIZ_BOX_PAGE_TEMPLATE_LABELS,
|
||||
)
|
||||
|
||||
from .screens_repo.ensure_schema import ensure_schema
|
||||
from .screens_repo.list_screens import list_screens
|
||||
from .screens_repo.get_screen import get_screen
|
||||
from .screens_repo.create_screen import create_screen
|
||||
from .screens_repo.rename_screen import rename_screen
|
||||
from .screens_repo.delete_screen import delete_screen
|
||||
from .screens_repo.move_screen import move_screen
|
||||
from .screens_repo.screens_referencing import screens_referencing
|
||||
from .screens_repo.set_scene_size import set_scene_size
|
||||
from .screens_repo.set_screen_background_music import set_screen_background_music
|
||||
from .screens_repo.set_screen_triggers import set_screen_triggers
|
||||
from .screens_repo.append_action_to_screen_trigger import append_action_to_screen_trigger
|
||||
from .screens_repo.remove_action_from_screen_trigger import remove_action_from_screen_trigger
|
||||
from .screens_repo.move_action_in_screen_trigger import move_action_in_screen_trigger
|
||||
|
||||
from .scenes.ensure_scene_schema import ensure_scene_schema
|
||||
from .scenes.add_scene_object import add_scene_object
|
||||
from .scenes.list_scene_objects import list_scene_objects
|
||||
from .scenes.resolve_scene_world_size import resolve_scene_world_size
|
||||
from .scenes.get_scene_object import get_scene_object
|
||||
from .scenes.update_scene_object_geometry import update_scene_object_geometry
|
||||
from .scenes.delete_scene_object import delete_scene_object
|
||||
from .scenes.set_scene_object_personnage_data import set_scene_object_personnage_data
|
||||
from .scenes.set_scene_object_commands import set_scene_object_commands
|
||||
from .scenes.set_scene_object_role import set_scene_object_role
|
||||
from .scenes.set_scene_object_collision import set_scene_object_collision
|
||||
from .scenes.set_scene_object_collision_rules import set_scene_object_collision_rules
|
||||
from .scenes.set_scene_object_name import set_scene_object_name
|
||||
from .scenes.set_scene_object_dialogue_box_style import set_scene_object_dialogue_box_style
|
||||
from .scenes.set_scene_object_quiz_config import set_scene_object_quiz_config
|
||||
from .scenes.render_scene_object import render_scene_object
|
||||
from .scenes.build_collision_rule_objects import build_collision_rule_objects
|
||||
from .scenes.append_action_to_trigger import append_action_to_trigger
|
||||
from .scenes.remove_action_from_trigger import remove_action_from_trigger
|
||||
from .scenes.move_action_in_trigger import move_action_in_trigger
|
||||
from .rendering.trigger_graph import (
|
||||
list_all_triggers, list_all_collision_objects, collect_all_dialogue_lines,
|
||||
list_completable_dialogue_ids,
|
||||
)
|
||||
from .scenes.flow_palette import TRIGGER_EVENTS_2D, ACTION_TYPE_LABELS_2D, OBJECT_ACTION_PROPERTIES
|
||||
|
||||
from .flow.ensure_flow_schema import ensure_flow_schema
|
||||
from .flow.list_flow_nodes import list_flow_nodes
|
||||
from .flow.list_flow_edges import list_flow_edges
|
||||
from .flow.get_flow_node import get_flow_node
|
||||
from .flow.add_flow_node import add_flow_node
|
||||
from .flow.move_flow_node import move_flow_node
|
||||
from .flow.delete_flow_node import delete_flow_node
|
||||
from .flow.add_flow_edge import add_flow_edge
|
||||
from .flow.delete_flow_edge import delete_flow_edge
|
||||
|
||||
from .flow.blocks.list_flow_blocks import list_flow_blocks
|
||||
from .flow.blocks.get_flow_block import get_flow_block
|
||||
from .flow.blocks.create_flow_block import create_flow_block
|
||||
from .flow.blocks.update_flow_block import update_flow_block
|
||||
from .flow.blocks.delete_flow_block import delete_flow_block
|
||||
from .flow.blocks.blocks_view import blocks_view
|
||||
|
||||
from .data_actions.apply_data_action import apply_data_action
|
||||
from .data_actions.apply_variable_action import apply_variable_action
|
||||
from .data_actions.apply_score_action import apply_score_action
|
||||
from .data_actions.apply_status_action import apply_status_action
|
||||
from .data_actions.apply_add_row_action import apply_add_row_action
|
||||
|
||||
from .animations.ensure_animation_schema import ensure_animation_schema
|
||||
from .animations.list_animation_clips import list_animation_clips
|
||||
from .animations.get_animation_clip import get_animation_clip
|
||||
from .animations.add_animation_clip import add_animation_clip
|
||||
from .animations.update_animation_clip import update_animation_clip
|
||||
from .animations.delete_animation_clip import delete_animation_clip
|
||||
from .animations.animate_css_catalog import ANIMATE_CSS_CATALOG, ANIMATE_CSS_LABELS
|
||||
|
||||
from .custom_events.list_custom_event_usages import list_custom_event_usages
|
||||
from .custom_events.delete_custom_event import delete_custom_event
|
||||
|
||||
from .payload.full_game_payload import full_game_payload
|
||||
|
||||
from .ia.ensure_ia_chat_schema import ensure_ia_chat_schema
|
||||
from .ia.list_ia_chat_messages import list_ia_chat_messages
|
||||
from .ia.add_ia_chat_message import add_ia_chat_message
|
||||
from .ia.create_ia_conversation import create_ia_conversation
|
||||
from .ia.list_ia_conversations import list_ia_conversations
|
||||
from .ia.get_ia_conversation import get_ia_conversation
|
||||
from .ia.delete_ia_conversation import delete_ia_conversation
|
||||
# Liste generee a partir des noms REELLEMENT importables depuis ce module
|
||||
# (introspection de vars(screens) + parsing AST des imports de ce fichier,
|
||||
# les deux methodes se recoupent exactement — voir discussion Phase 3) —
|
||||
# jamais retapee a la main. `_trigger_for` (rendering/trigger_for.py) est
|
||||
# volontairement exclu : prefixe underscore = prive par convention, et
|
||||
# confirme inutilise en dehors de ce fichier (grep sur tout le depot).
|
||||
__all__ = [
|
||||
"data_definition_options",
|
||||
"ACTION_TYPE_LABELS",
|
||||
"ELEMENT_ORIENTATION_VALUES",
|
||||
"ELEMENT_ORIENTATION_LABELS",
|
||||
"ELEMENT_VISIBILITY_VALUES",
|
||||
"ELEMENT_VISIBILITY_LABELS",
|
||||
"DATA_OPERATIONS",
|
||||
"DATA_OPERATION_LABELS",
|
||||
"PUBLIC_SPRITE_LIBRARY",
|
||||
"ADMIN_SPRITE_LIBRARY",
|
||||
"SPRITE_LIBRARY",
|
||||
"ADMIN_ONLY_CHARACTER_SLUGS",
|
||||
"sprite_gallery_families",
|
||||
"BACKGROUND_LIBRARY",
|
||||
"ADMIN_ONLY_BACKGROUND_SLUGS",
|
||||
"list_used_forge_characters",
|
||||
"TRIGGER_EVENTS",
|
||||
"CONDITION_OPERATORS",
|
||||
"CONDITION_OPERATOR_LABELS",
|
||||
"CLICKED_ROW_ID",
|
||||
"LAST_INSERTED_ROW_ID",
|
||||
"resolve_personnage_data",
|
||||
"resolve_personnage_animations",
|
||||
"resolve_personnage_idle_frame",
|
||||
"resolve_personnage_role",
|
||||
"PERSONNAGE_ROLES",
|
||||
"PERSONNAGE_ROLE_LABELS",
|
||||
"DEFAULT_PERSONNAGE_ROLE",
|
||||
"resolve_collision_settings",
|
||||
"DEFAULT_COLLISION_SETTINGS",
|
||||
"COLLISION_SHAPES",
|
||||
"resolve_collision_rules",
|
||||
"sanitize_collision_rules",
|
||||
"set_dialogue_lines_in_rules",
|
||||
"set_then_action_in_rules",
|
||||
"remove_action_from_rules",
|
||||
"move_action_in_rules",
|
||||
"backfill_missing_leaf_ids",
|
||||
"resolve_screen_triggers",
|
||||
"sanitize_screen_triggers",
|
||||
"TRIGGER_TYPES_SCREEN",
|
||||
"SURBRILLANCE_VALUES",
|
||||
"SURBRILLANCE_LABELS",
|
||||
"VIDEO_MODES",
|
||||
"VIDEO_MODE_LABELS",
|
||||
"resolve_personnage_commands",
|
||||
"DEFAULT_PERSONNAGE_COMMANDS",
|
||||
"sanitize_extra_commands",
|
||||
"list_named_scene_object_names",
|
||||
"resolve_dialogue_box_style",
|
||||
"sanitize_dialogue_box_style",
|
||||
"DEFAULT_DIALOGUE_BOX_STYLE",
|
||||
"FONT_WEIGHT_CHOICES",
|
||||
"resolve_quiz_box_config",
|
||||
"sanitize_quiz_box_config",
|
||||
"DEFAULT_QUIZ_BOX_CONFIG",
|
||||
"TIMER_MODES",
|
||||
"QUIZ_BOX_DIALOG_TEMPLATES",
|
||||
"QUIZ_BOX_DIALOG_TEMPLATE_LABELS",
|
||||
"QUIZ_BOX_PAGE_TEMPLATES",
|
||||
"QUIZ_BOX_PAGE_TEMPLATE_LABELS",
|
||||
"ensure_schema",
|
||||
"list_screens",
|
||||
"get_screen",
|
||||
"create_screen",
|
||||
"rename_screen",
|
||||
"delete_screen",
|
||||
"move_screen",
|
||||
"screens_referencing",
|
||||
"set_scene_size",
|
||||
"set_screen_background_music",
|
||||
"set_screen_triggers",
|
||||
"append_action_to_screen_trigger",
|
||||
"remove_action_from_screen_trigger",
|
||||
"move_action_in_screen_trigger",
|
||||
"ensure_scene_schema",
|
||||
"add_scene_object",
|
||||
"list_scene_objects",
|
||||
"resolve_scene_world_size",
|
||||
"get_scene_object",
|
||||
"update_scene_object_geometry",
|
||||
"delete_scene_object",
|
||||
"set_scene_object_personnage_data",
|
||||
"set_scene_object_commands",
|
||||
"set_scene_object_role",
|
||||
"set_scene_object_collision",
|
||||
"set_scene_object_collision_rules",
|
||||
"set_scene_object_name",
|
||||
"set_scene_object_dialogue_box_style",
|
||||
"set_scene_object_quiz_config",
|
||||
"render_scene_object",
|
||||
"build_collision_rule_objects",
|
||||
"append_action_to_trigger",
|
||||
"remove_action_from_trigger",
|
||||
"move_action_in_trigger",
|
||||
"list_all_triggers",
|
||||
"list_all_collision_objects",
|
||||
"collect_all_dialogue_lines",
|
||||
"list_completable_dialogue_ids",
|
||||
"TRIGGER_EVENTS_2D",
|
||||
"ACTION_TYPE_LABELS_2D",
|
||||
"OBJECT_ACTION_PROPERTIES",
|
||||
"ensure_flow_schema",
|
||||
"list_flow_nodes",
|
||||
"list_flow_edges",
|
||||
"get_flow_node",
|
||||
"add_flow_node",
|
||||
"move_flow_node",
|
||||
"delete_flow_node",
|
||||
"add_flow_edge",
|
||||
"delete_flow_edge",
|
||||
"list_flow_blocks",
|
||||
"get_flow_block",
|
||||
"create_flow_block",
|
||||
"update_flow_block",
|
||||
"delete_flow_block",
|
||||
"blocks_view",
|
||||
"apply_data_action",
|
||||
"apply_variable_action",
|
||||
"apply_score_action",
|
||||
"apply_status_action",
|
||||
"apply_add_row_action",
|
||||
"ensure_animation_schema",
|
||||
"list_animation_clips",
|
||||
"get_animation_clip",
|
||||
"add_animation_clip",
|
||||
"update_animation_clip",
|
||||
"delete_animation_clip",
|
||||
"ANIMATE_CSS_CATALOG",
|
||||
"ANIMATE_CSS_LABELS",
|
||||
"list_custom_event_usages",
|
||||
"delete_custom_event",
|
||||
"full_game_payload",
|
||||
"ensure_ia_chat_schema",
|
||||
"list_ia_chat_messages",
|
||||
"add_ia_chat_message",
|
||||
"create_ia_conversation",
|
||||
"list_ia_conversations",
|
||||
"get_ia_conversation",
|
||||
"delete_ia_conversation",
|
||||
]
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import json
|
||||
from typing import Any
|
||||
|
||||
import db
|
||||
|
||||
@@ -7,15 +8,25 @@ from .ensure_animation_schema import ensure_animation_schema
|
||||
# Point de départ raisonnable pour un nouveau clip "personnalisé" : un
|
||||
# fondu simple de 0 à 1, que le créateur du jeu peut ensuite modifier ou
|
||||
# remplacer entièrement dans l'éditeur de images-clés.
|
||||
_DEFAULT_CUSTOM_KEYFRAMES = [
|
||||
_DEFAULT_CUSTOM_KEYFRAMES: list[dict[str, Any]] = [
|
||||
{"percent": 0, "props": {"opacity": "0"}},
|
||||
{"percent": 100, "props": {"opacity": "1"}},
|
||||
]
|
||||
|
||||
|
||||
def add_animation_clip(slug, screen_id, element_id, kind="animate_css", animate_name=None,
|
||||
custom_keyframes=None, start_time=0, duration=1, delay=0,
|
||||
easing="ease", iteration_count="1"):
|
||||
def add_animation_clip(
|
||||
slug: str,
|
||||
screen_id: int,
|
||||
element_id: int,
|
||||
kind: str = "animate_css",
|
||||
animate_name: str | None = None,
|
||||
custom_keyframes: list[dict[str, Any]] | None = None,
|
||||
start_time: float = 0,
|
||||
duration: float = 1,
|
||||
delay: float = 0,
|
||||
easing: str = "ease",
|
||||
iteration_count: str = "1",
|
||||
) -> int:
|
||||
ensure_animation_schema(slug)
|
||||
if kind == "custom" and not custom_keyframes:
|
||||
custom_keyframes = _DEFAULT_CUSTOM_KEYFRAMES
|
||||
@@ -28,10 +39,20 @@ def add_animation_clip(slug, screen_id, element_id, kind="animate_css", animate_
|
||||
(screen_id, element_id, kind, animate_name, custom_keyframes,
|
||||
start_time, duration, delay, easing, iteration_count)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)""",
|
||||
(screen_id, element_id, kind, animate_name, keyframes_json,
|
||||
float(start_time), float(duration), float(delay), easing, str(iteration_count)),
|
||||
(
|
||||
screen_id,
|
||||
element_id,
|
||||
kind,
|
||||
animate_name,
|
||||
keyframes_json,
|
||||
float(start_time),
|
||||
float(duration),
|
||||
float(delay),
|
||||
easing,
|
||||
str(iteration_count),
|
||||
),
|
||||
)
|
||||
clip_id = conn.execute("SELECT last_insert_rowid() AS id").fetchone()["id"]
|
||||
clip_id = int(conn.execute("SELECT last_insert_rowid() AS id").fetchone()["id"])
|
||||
conn.commit()
|
||||
conn.close()
|
||||
return clip_id
|
||||
|
||||
@@ -6,70 +6,143 @@
|
||||
# templates/play.html). Regroupées par catégorie pour un menu déroulant
|
||||
# plus lisible que 80 entrées à plat.
|
||||
ANIMATE_CSS_CATALOG = [
|
||||
("Attirer l'attention", [
|
||||
("bounce", "Rebond"), ("flash", "Flash"), ("pulse", "Pulsation"),
|
||||
("rubberBand", "Élastique"), ("shakeX", "Secousse horizontale"),
|
||||
("shakeY", "Secousse verticale"), ("headShake", "Hoche la tête"),
|
||||
("swing", "Balancement"), ("tada", "Tada"), ("wobble", "Vacillement"),
|
||||
("jello", "Gélatine"), ("heartBeat", "Battement de cœur"),
|
||||
]),
|
||||
("Entrée — fondu", [
|
||||
("fadeIn", "Apparition"), ("fadeInDown", "Apparition depuis le haut"),
|
||||
("fadeInLeft", "Apparition depuis la gauche"), ("fadeInRight", "Apparition depuis la droite"),
|
||||
("fadeInUp", "Apparition depuis le bas"),
|
||||
]),
|
||||
("Sortie — fondu", [
|
||||
("fadeOut", "Disparition"), ("fadeOutDown", "Disparition vers le bas"),
|
||||
("fadeOutLeft", "Disparition vers la gauche"), ("fadeOutRight", "Disparition vers la droite"),
|
||||
("fadeOutUp", "Disparition vers le haut"),
|
||||
]),
|
||||
("Entrée — rebond", [
|
||||
("bounceIn", "Rebond entrant"), ("bounceInDown", "Rebond depuis le haut"),
|
||||
("bounceInLeft", "Rebond depuis la gauche"), ("bounceInRight", "Rebond depuis la droite"),
|
||||
("bounceInUp", "Rebond depuis le bas"),
|
||||
]),
|
||||
("Sortie — rebond", [
|
||||
("bounceOut", "Rebond sortant"), ("bounceOutDown", "Rebond vers le bas"),
|
||||
("bounceOutLeft", "Rebond vers la gauche"), ("bounceOutRight", "Rebond vers la droite"),
|
||||
("bounceOutUp", "Rebond vers le haut"),
|
||||
]),
|
||||
("Entrée — glissement", [
|
||||
("slideInDown", "Glisse depuis le haut"), ("slideInLeft", "Glisse depuis la gauche"),
|
||||
("slideInRight", "Glisse depuis la droite"), ("slideInUp", "Glisse depuis le bas"),
|
||||
]),
|
||||
("Sortie — glissement", [
|
||||
("slideOutDown", "Glisse vers le bas"), ("slideOutLeft", "Glisse vers la gauche"),
|
||||
("slideOutRight", "Glisse vers la droite"), ("slideOutUp", "Glisse vers le haut"),
|
||||
]),
|
||||
("Entrée — zoom", [
|
||||
("zoomIn", "Zoom entrant"), ("zoomInDown", "Zoom depuis le haut"),
|
||||
("zoomInLeft", "Zoom depuis la gauche"), ("zoomInRight", "Zoom depuis la droite"),
|
||||
("zoomInUp", "Zoom depuis le bas"),
|
||||
]),
|
||||
("Sortie — zoom", [
|
||||
("zoomOut", "Zoom sortant"), ("zoomOutDown", "Zoom vers le bas"),
|
||||
("zoomOutLeft", "Zoom vers la gauche"), ("zoomOutRight", "Zoom vers la droite"),
|
||||
("zoomOutUp", "Zoom vers le haut"),
|
||||
]),
|
||||
("Rotation", [
|
||||
("rotateIn", "Rotation entrante"), ("rotateOut", "Rotation sortante"),
|
||||
("rotateInDownLeft", "Rotation depuis haut-gauche"), ("rotateInDownRight", "Rotation depuis haut-droite"),
|
||||
("rotateInUpLeft", "Rotation depuis bas-gauche"), ("rotateInUpRight", "Rotation depuis bas-droite"),
|
||||
]),
|
||||
("Retournement", [
|
||||
("flip", "Retournement"), ("flipInX", "Retournement horizontal entrant"),
|
||||
("flipInY", "Retournement vertical entrant"), ("flipOutX", "Retournement horizontal sortant"),
|
||||
("flipOutY", "Retournement vertical sortant"),
|
||||
]),
|
||||
("Vitesse lumière", [
|
||||
("lightSpeedInRight", "Entrée vitesse lumière — droite"), ("lightSpeedInLeft", "Entrée vitesse lumière — gauche"),
|
||||
("lightSpeedOutRight", "Sortie vitesse lumière — droite"), ("lightSpeedOutLeft", "Sortie vitesse lumière — gauche"),
|
||||
]),
|
||||
("Spéciales", [
|
||||
("jackInTheBox", "Boîte à surprise"), ("rollIn", "Roule à l'entrée"), ("rollOut", "Roule à la sortie"),
|
||||
("backInDown", "Recul entrant — haut"), ("backInUp", "Recul entrant — bas"),
|
||||
("backOutDown", "Recul sortant — bas"), ("backOutUp", "Recul sortant — haut"),
|
||||
]),
|
||||
(
|
||||
"Attirer l'attention",
|
||||
[
|
||||
("bounce", "Rebond"),
|
||||
("flash", "Flash"),
|
||||
("pulse", "Pulsation"),
|
||||
("rubberBand", "Élastique"),
|
||||
("shakeX", "Secousse horizontale"),
|
||||
("shakeY", "Secousse verticale"),
|
||||
("headShake", "Hoche la tête"),
|
||||
("swing", "Balancement"),
|
||||
("tada", "Tada"),
|
||||
("wobble", "Vacillement"),
|
||||
("jello", "Gélatine"),
|
||||
("heartBeat", "Battement de cœur"),
|
||||
],
|
||||
),
|
||||
(
|
||||
"Entrée — fondu",
|
||||
[
|
||||
("fadeIn", "Apparition"),
|
||||
("fadeInDown", "Apparition depuis le haut"),
|
||||
("fadeInLeft", "Apparition depuis la gauche"),
|
||||
("fadeInRight", "Apparition depuis la droite"),
|
||||
("fadeInUp", "Apparition depuis le bas"),
|
||||
],
|
||||
),
|
||||
(
|
||||
"Sortie — fondu",
|
||||
[
|
||||
("fadeOut", "Disparition"),
|
||||
("fadeOutDown", "Disparition vers le bas"),
|
||||
("fadeOutLeft", "Disparition vers la gauche"),
|
||||
("fadeOutRight", "Disparition vers la droite"),
|
||||
("fadeOutUp", "Disparition vers le haut"),
|
||||
],
|
||||
),
|
||||
(
|
||||
"Entrée — rebond",
|
||||
[
|
||||
("bounceIn", "Rebond entrant"),
|
||||
("bounceInDown", "Rebond depuis le haut"),
|
||||
("bounceInLeft", "Rebond depuis la gauche"),
|
||||
("bounceInRight", "Rebond depuis la droite"),
|
||||
("bounceInUp", "Rebond depuis le bas"),
|
||||
],
|
||||
),
|
||||
(
|
||||
"Sortie — rebond",
|
||||
[
|
||||
("bounceOut", "Rebond sortant"),
|
||||
("bounceOutDown", "Rebond vers le bas"),
|
||||
("bounceOutLeft", "Rebond vers la gauche"),
|
||||
("bounceOutRight", "Rebond vers la droite"),
|
||||
("bounceOutUp", "Rebond vers le haut"),
|
||||
],
|
||||
),
|
||||
(
|
||||
"Entrée — glissement",
|
||||
[
|
||||
("slideInDown", "Glisse depuis le haut"),
|
||||
("slideInLeft", "Glisse depuis la gauche"),
|
||||
("slideInRight", "Glisse depuis la droite"),
|
||||
("slideInUp", "Glisse depuis le bas"),
|
||||
],
|
||||
),
|
||||
(
|
||||
"Sortie — glissement",
|
||||
[
|
||||
("slideOutDown", "Glisse vers le bas"),
|
||||
("slideOutLeft", "Glisse vers la gauche"),
|
||||
("slideOutRight", "Glisse vers la droite"),
|
||||
("slideOutUp", "Glisse vers le haut"),
|
||||
],
|
||||
),
|
||||
(
|
||||
"Entrée — zoom",
|
||||
[
|
||||
("zoomIn", "Zoom entrant"),
|
||||
("zoomInDown", "Zoom depuis le haut"),
|
||||
("zoomInLeft", "Zoom depuis la gauche"),
|
||||
("zoomInRight", "Zoom depuis la droite"),
|
||||
("zoomInUp", "Zoom depuis le bas"),
|
||||
],
|
||||
),
|
||||
(
|
||||
"Sortie — zoom",
|
||||
[
|
||||
("zoomOut", "Zoom sortant"),
|
||||
("zoomOutDown", "Zoom vers le bas"),
|
||||
("zoomOutLeft", "Zoom vers la gauche"),
|
||||
("zoomOutRight", "Zoom vers la droite"),
|
||||
("zoomOutUp", "Zoom vers le haut"),
|
||||
],
|
||||
),
|
||||
(
|
||||
"Rotation",
|
||||
[
|
||||
("rotateIn", "Rotation entrante"),
|
||||
("rotateOut", "Rotation sortante"),
|
||||
("rotateInDownLeft", "Rotation depuis haut-gauche"),
|
||||
("rotateInDownRight", "Rotation depuis haut-droite"),
|
||||
("rotateInUpLeft", "Rotation depuis bas-gauche"),
|
||||
("rotateInUpRight", "Rotation depuis bas-droite"),
|
||||
],
|
||||
),
|
||||
(
|
||||
"Retournement",
|
||||
[
|
||||
("flip", "Retournement"),
|
||||
("flipInX", "Retournement horizontal entrant"),
|
||||
("flipInY", "Retournement vertical entrant"),
|
||||
("flipOutX", "Retournement horizontal sortant"),
|
||||
("flipOutY", "Retournement vertical sortant"),
|
||||
],
|
||||
),
|
||||
(
|
||||
"Vitesse lumière",
|
||||
[
|
||||
("lightSpeedInRight", "Entrée vitesse lumière — droite"),
|
||||
("lightSpeedInLeft", "Entrée vitesse lumière — gauche"),
|
||||
("lightSpeedOutRight", "Sortie vitesse lumière — droite"),
|
||||
("lightSpeedOutLeft", "Sortie vitesse lumière — gauche"),
|
||||
],
|
||||
),
|
||||
(
|
||||
"Spéciales",
|
||||
[
|
||||
("jackInTheBox", "Boîte à surprise"),
|
||||
("rollIn", "Roule à l'entrée"),
|
||||
("rollOut", "Roule à la sortie"),
|
||||
("backInDown", "Recul entrant — haut"),
|
||||
("backInUp", "Recul entrant — bas"),
|
||||
("backOutDown", "Recul sortant — bas"),
|
||||
("backOutUp", "Recul sortant — haut"),
|
||||
],
|
||||
),
|
||||
]
|
||||
|
||||
# Map key -> label, pour l'affichage côté éditeur (nom du nœud, etc.).
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import db
|
||||
|
||||
|
||||
def delete_animation_clip(slug, clip_id):
|
||||
def delete_animation_clip(slug: str, clip_id: int) -> None:
|
||||
conn = db.connect(slug)
|
||||
conn.execute("DELETE FROM _animation_clips WHERE id = ?", (clip_id,))
|
||||
conn.commit()
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import sqlite3
|
||||
|
||||
import db
|
||||
|
||||
_CREATE_TABLE = """
|
||||
@@ -18,7 +20,7 @@ _CREATE_TABLE = """
|
||||
"""
|
||||
|
||||
|
||||
def ensure_animation_schema(slug):
|
||||
def ensure_animation_schema(slug: str) -> None:
|
||||
"""Crée la table des clips d'animation si elle n'existe pas encore —
|
||||
même principe que ensure_flow_schema.py : un jeu créé avant l'ajout de
|
||||
cette fonctionnalité n'a pas besoin d'être migré à la main, la première
|
||||
@@ -40,9 +42,7 @@ def ensure_animation_schema(slug):
|
||||
if not cols:
|
||||
conn.executescript(_CREATE_TABLE)
|
||||
elif cols["element_id"]["notnull"] and _has_fk_on_element_id(conn):
|
||||
conn.executescript(
|
||||
_CREATE_TABLE.replace("CREATE TABLE _animation_clips", "CREATE TABLE _animation_clips_new")
|
||||
)
|
||||
conn.executescript(_CREATE_TABLE.replace("CREATE TABLE _animation_clips", "CREATE TABLE _animation_clips_new"))
|
||||
conn.execute(
|
||||
"""INSERT INTO _animation_clips_new
|
||||
(id, screen_id, element_id, kind, animate_name, custom_keyframes,
|
||||
@@ -57,7 +57,7 @@ def ensure_animation_schema(slug):
|
||||
conn.close()
|
||||
|
||||
|
||||
def _has_fk_on_element_id(conn):
|
||||
def _has_fk_on_element_id(conn: sqlite3.Connection) -> bool:
|
||||
return any(
|
||||
fk["from"] == "element_id" for fk in conn.execute("PRAGMA foreign_key_list(_animation_clips)").fetchall()
|
||||
)
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
from typing import Any
|
||||
|
||||
import db
|
||||
|
||||
from .ensure_animation_schema import ensure_animation_schema
|
||||
|
||||
|
||||
def get_animation_clip(slug, clip_id):
|
||||
def get_animation_clip(slug: str, clip_id: int) -> dict[str, Any] | None:
|
||||
ensure_animation_schema(slug)
|
||||
conn = db.connect(slug)
|
||||
row = conn.execute("SELECT * FROM _animation_clips WHERE id = ?", (clip_id,)).fetchone()
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
from typing import Any
|
||||
|
||||
import db
|
||||
|
||||
from .ensure_animation_schema import ensure_animation_schema
|
||||
|
||||
|
||||
def list_animation_clips(slug, screen_id):
|
||||
def list_animation_clips(slug: str, screen_id: int) -> list[dict[str, Any]]:
|
||||
ensure_animation_schema(slug)
|
||||
conn = db.connect(slug)
|
||||
rows = conn.execute(
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import json
|
||||
from typing import Any
|
||||
|
||||
import db
|
||||
|
||||
@@ -8,13 +9,20 @@ from .ensure_animation_schema import ensure_animation_schema
|
||||
# souris dans le panneau timeline n'envoie que start_time/duration, le
|
||||
# formulaire d'édition du clip peut envoyer n'importe lequel des autres.
|
||||
_UPDATABLE_FIELDS = {
|
||||
"kind", "animate_name", "start_time", "duration", "delay", "easing", "iteration_count",
|
||||
"kind",
|
||||
"animate_name",
|
||||
"start_time",
|
||||
"duration",
|
||||
"delay",
|
||||
"easing",
|
||||
"iteration_count",
|
||||
}
|
||||
|
||||
|
||||
def update_animation_clip(slug, clip_id, **fields):
|
||||
def update_animation_clip(slug: str, clip_id: int, **fields: Any) -> None:
|
||||
ensure_animation_schema(slug)
|
||||
sets, vals = [], []
|
||||
sets: list[str] = []
|
||||
vals: list[Any] = []
|
||||
for key, value in fields.items():
|
||||
if key == "custom_keyframes":
|
||||
sets.append("custom_keyframes = ?")
|
||||
@@ -28,6 +36,6 @@ def update_animation_clip(slug, clip_id, **fields):
|
||||
return
|
||||
vals.append(clip_id)
|
||||
conn = db.connect(slug)
|
||||
conn.execute(f"UPDATE _animation_clips SET {', '.join(sets)} WHERE id = ?", vals)
|
||||
conn.execute(f"UPDATE _animation_clips SET {', '.join(sets)} WHERE id = ?", vals) # nosec B608 # noqa: S608 - sets construit uniquement a partir de _UPDATABLE_FIELDS (whitelist codee en dur ligne 22), jamais d'une cle arbitraire ; valeurs parametrees (?)
|
||||
conn.commit()
|
||||
conn.close()
|
||||
|
||||
@@ -1,45 +0,0 @@
|
||||
import json
|
||||
|
||||
# Partagé par control_value.py (widgets) et filter_repeater_rows.py
|
||||
# (rendering) — module "feuille" sans import de l'un ou l'autre, pour ne
|
||||
# jamais introduire de dépendance circulaire entre ces deux packages.
|
||||
|
||||
|
||||
def decode_clauses(attrs, clauses_attr, combinator_attr, legacy_prefix=None):
|
||||
"""Lit une liste de clauses {"champ", "operateur", "valeur"} depuis un
|
||||
attribut JSON (clauses_attr). Repli sur l'ANCIEN format à 2 filtres
|
||||
fixes (legacy_prefix + "_champ"/"_operateur"/"_valeur", et
|
||||
legacy_prefix + "2_champ"/"2_operateur"/"2_valeur") si clauses_attr est
|
||||
absent — pour ne rien casser sur les éléments créés avant l'ajout des
|
||||
clauses illimitées (aucune migration de données nécessaire, la
|
||||
conversion se fait à la volée, ici, à la lecture). Renvoie toujours
|
||||
(clauses, combinateur), combinateur valant "et" par défaut."""
|
||||
raw = attrs.get(clauses_attr)
|
||||
clauses = None
|
||||
if raw:
|
||||
try:
|
||||
parsed = json.loads(raw)
|
||||
if isinstance(parsed, list):
|
||||
clauses = parsed
|
||||
except (TypeError, ValueError):
|
||||
clauses = None
|
||||
if clauses is None and legacy_prefix:
|
||||
clauses = []
|
||||
for suffix in ("", "2"):
|
||||
champ = (attrs.get(f"{legacy_prefix}{suffix}_champ") or "").strip()
|
||||
if champ:
|
||||
clauses.append({
|
||||
"champ": champ,
|
||||
"operateur": attrs.get(f"{legacy_prefix}{suffix}_operateur") or "egal",
|
||||
"valeur": attrs.get(f"{legacy_prefix}{suffix}_valeur") or "",
|
||||
})
|
||||
combinateur = attrs.get(combinator_attr) or "et"
|
||||
return clauses or [], combinateur
|
||||
|
||||
|
||||
def encode_clauses(clauses):
|
||||
"""Sérialise une liste de clauses en JSON, en retirant celles sans nom
|
||||
de champ (une ligne ajoutée puis jamais remplie) — "" si la liste
|
||||
nettoyée est vide, pour ne rien écrire de superflu en base."""
|
||||
cleaned = [c for c in clauses if (c.get("champ") or "").strip()]
|
||||
return json.dumps(cleaned) if cleaned else ""
|
||||
@@ -3,7 +3,8 @@ _flow_nodes/_flow_edges (screens/flow/) — jamais dans db/custom_events/,
|
||||
pour la même raison que screens/elements/delete_element.py n'est pas
|
||||
dans db/ : ce sont deux domaines différents (une entité game-wide, des
|
||||
nœuds de graphe), le nettoyage croisé vit dans screens/, pas db/."""
|
||||
from .list_custom_event_usages import list_custom_event_usages
|
||||
|
||||
from .delete_custom_event import delete_custom_event
|
||||
from .list_custom_event_usages import list_custom_event_usages
|
||||
|
||||
__all__ = ["list_custom_event_usages", "delete_custom_event"]
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import db
|
||||
|
||||
from ..flow.ensure_flow_schema import ensure_flow_schema
|
||||
|
||||
|
||||
def delete_custom_event(slug, event_id):
|
||||
def delete_custom_event(slug: str, event_id: int) -> None:
|
||||
"""Seule suppression d'entité game-wide du moteur qui cascade
|
||||
RÉELLEMENT (demande explicite) : une variable globale ou une
|
||||
définition d'objet supprimée laisse des références pendantes
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
from typing import Any
|
||||
|
||||
import db
|
||||
|
||||
from ..flow.ensure_flow_schema import ensure_flow_schema
|
||||
|
||||
|
||||
def list_custom_event_usages(slug):
|
||||
def list_custom_event_usages(slug: str) -> dict[int, list[dict[str, Any]]]:
|
||||
"""Pour CHAQUE événement, où est-il écouté ("ecoute", nœud Déclencheur)
|
||||
et déclenché ("declenche", nœud Action) — une seule requête plutôt
|
||||
qu'une par événement, pour l'onglet Événements (screen_edit.html) qui
|
||||
@@ -28,10 +31,14 @@ def list_custom_event_usages(slug):
|
||||
"""
|
||||
).fetchall()
|
||||
conn.close()
|
||||
usages = {}
|
||||
usages: dict[int, list[dict[str, Any]]] = {}
|
||||
for row in rows:
|
||||
usages.setdefault(row["event_id"], []).append({
|
||||
"screen_id": row["screen_id"], "screen_name": row["screen_name"],
|
||||
"is_template": bool(row["is_template"]), "role": row["role"],
|
||||
})
|
||||
usages.setdefault(row["event_id"], []).append(
|
||||
{
|
||||
"screen_id": row["screen_id"],
|
||||
"screen_name": row["screen_name"],
|
||||
"is_template": bool(row["is_template"]),
|
||||
"role": row["role"],
|
||||
}
|
||||
)
|
||||
return usages
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
from typing import Any
|
||||
|
||||
import db
|
||||
|
||||
|
||||
def apply_add_row_action(slug, action, player_id=db.PLAYER_SHARED):
|
||||
def apply_add_row_action(slug: str, action: dict[str, Any], player_id: str = db.PLAYER_SHARED) -> int | None:
|
||||
"""Exécute au moment du clic (mode jouable) une action "ajouter_ligne" :
|
||||
crée une ligne VIDE dans l'objet visé (aucun champ renseigné ici —
|
||||
c'est à un ou plusieurs nœuds "Modifier une donnée" enchaînés,
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
from typing import Any
|
||||
|
||||
import db
|
||||
|
||||
from .compute_operation import compute_new_value
|
||||
|
||||
|
||||
def apply_data_action(slug, action, player_id=db.PLAYER_SHARED):
|
||||
def apply_data_action(slug: str, action: dict[str, Any], player_id: str = db.PLAYER_SHARED) -> bool:
|
||||
"""Exécute au moment du clic (mode jouable) une action "modifier_donnee" :
|
||||
lit l'objet/la ligne/le champ visés dans l'action, calcule la nouvelle
|
||||
valeur selon l'opération choisie, et l'enregistre — un vrai UPDATE SQL,
|
||||
@@ -42,7 +44,7 @@ def apply_data_action(slug, action, player_id=db.PLAYER_SHARED):
|
||||
return True
|
||||
|
||||
|
||||
def _clamp_to_field_bounds(value, field_def):
|
||||
def _clamp_to_field_bounds(value: Any, field_def: dict[str, Any]) -> Any:
|
||||
"""2.2 — bornage automatique : ramène la valeur dans [min_value,
|
||||
max_value] si l'un ou l'autre est réglé sur ce champ, pour qu'une
|
||||
jauge (voir render_jauge.py) ne puisse jamais dépasser 100 % ni
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
from typing import Any
|
||||
|
||||
import db
|
||||
|
||||
from .compute_operation import compute_new_value
|
||||
|
||||
|
||||
def apply_score_action(slug, action, player_id=db.PLAYER_SHARED):
|
||||
def apply_score_action(slug: str, action: dict[str, Any], player_id: str = db.PLAYER_SHARED) -> bool:
|
||||
"""Exécute au moment du clic (mode jouable) une action
|
||||
"modifier_score" — même vocabulaire d'opérations que
|
||||
apply_variable_action.py (DATA_OPERATIONS), appliqué au score de CE
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
from typing import Any
|
||||
|
||||
import db
|
||||
|
||||
|
||||
def apply_status_action(slug, action, player_id=db.PLAYER_SHARED):
|
||||
def apply_status_action(slug: str, action: dict[str, Any], player_id: str = db.PLAYER_SHARED) -> bool:
|
||||
"""Exécute au moment du clic (mode jouable) une action
|
||||
"definir_statut_partie" — réutilise data_value (déjà un simple champ
|
||||
texte générique sur le nœud action, comme pour "attendre") pour porter
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
from typing import Any
|
||||
|
||||
import db
|
||||
|
||||
from .compute_operation import compute_new_value
|
||||
|
||||
|
||||
def apply_variable_action(slug, action, player_id=db.PLAYER_SHARED):
|
||||
def apply_variable_action(slug: str, action: dict[str, Any], player_id: str = db.PLAYER_SHARED) -> bool:
|
||||
"""Exécute au moment du clic (mode jouable) une action
|
||||
"modifier_variable" — même logique/vocabulaire d'opérations que
|
||||
apply_data_action.py (DATA_OPERATIONS), appliqué à une variable globale
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import random
|
||||
from typing import Any
|
||||
|
||||
|
||||
def compute_new_value(operation, current, raw_value, is_decimal):
|
||||
def compute_new_value(operation: str, current: Any, raw_value: Any, is_decimal: bool) -> Any:
|
||||
"""Calcule la nouvelle valeur d'un champ d'objet ou d'une variable
|
||||
globale pour une opération de DATA_OPERATIONS (voir
|
||||
screens/labels/data_operations.py) — logique partagée par
|
||||
@@ -77,5 +78,7 @@ def compute_new_value(operation, current, raw_value, is_decimal):
|
||||
lo, hi = 0, 0
|
||||
if lo > hi:
|
||||
lo, hi = hi, lo
|
||||
return random.uniform(lo, hi) if is_decimal else random.randint(int(lo), int(hi))
|
||||
if is_decimal:
|
||||
return random.uniform(lo, hi) # nosec B311 # noqa: S311 - tirage aleatoire de jeu, jamais crypto # NOSONAR S2245
|
||||
return random.randint(int(lo), int(hi)) # nosec B311 # noqa: S311 - tirage aleatoire de jeu, jamais crypto # NOSONAR S2245
|
||||
raise ValueError(f"opération inconnue : {operation!r}")
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
from typing import Any
|
||||
|
||||
import db
|
||||
|
||||
|
||||
def data_definition_options(slug, definition_id):
|
||||
def data_definition_options(slug: str | None, definition_id: int | None) -> dict[str, list[dict[str, Any]]]:
|
||||
"""Lignes et champs d'un objet de données, prêts à peupler un menu
|
||||
déroulant — utilisé à la fois pour DEFINITIONS_DATA (tous les objets du
|
||||
jeu, consommé côté client par les formulaires de la Logique de la
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import db
|
||||
|
||||
|
||||
def add_flow_edge(slug, screen_id, from_node_id, from_port, to_node_id):
|
||||
def add_flow_edge(slug: str, screen_id: int, from_node_id: int, from_port: str | None, to_node_id: int) -> int:
|
||||
conn = db.connect(slug)
|
||||
conn.execute(
|
||||
"INSERT INTO _flow_edges (screen_id, from_node_id, from_port, to_node_id) VALUES (?, ?, ?, ?)",
|
||||
(screen_id, from_node_id, from_port or "out", to_node_id),
|
||||
)
|
||||
edge_id = conn.execute("SELECT last_insert_rowid() AS id").fetchone()["id"]
|
||||
edge_id = int(conn.execute("SELECT last_insert_rowid() AS id").fetchone()["id"])
|
||||
conn.commit()
|
||||
conn.close()
|
||||
return edge_id
|
||||
|
||||
@@ -1,21 +1,23 @@
|
||||
from typing import Any
|
||||
|
||||
import db
|
||||
|
||||
from .ensure_flow_schema import ensure_flow_schema
|
||||
from .constants import FLOW_NODE_FIELDS
|
||||
from .ensure_flow_schema import ensure_flow_schema
|
||||
|
||||
|
||||
def add_flow_node(slug, screen_id, node_type, x=40, y=40, **fields):
|
||||
def add_flow_node(slug: str, screen_id: int, node_type: str, x: float = 40, y: float = 40, **fields: Any) -> int:
|
||||
ensure_flow_schema(slug)
|
||||
cols = ["screen_id", "node_type", "x", "y"]
|
||||
vals = [screen_id, node_type, x, y]
|
||||
cols: list[str] = ["screen_id", "node_type", "x", "y"]
|
||||
vals: list[Any] = [screen_id, node_type, x, y]
|
||||
for k, v in fields.items():
|
||||
if k in FLOW_NODE_FIELDS and v not in (None, ""):
|
||||
cols.append(k)
|
||||
vals.append(v)
|
||||
placeholders = ", ".join("?" for _ in cols)
|
||||
conn = db.connect(slug)
|
||||
conn.execute(f"INSERT INTO _flow_nodes ({', '.join(cols)}) VALUES ({placeholders})", vals)
|
||||
node_id = conn.execute("SELECT last_insert_rowid() AS id").fetchone()["id"]
|
||||
conn.execute(f"INSERT INTO _flow_nodes ({', '.join(cols)}) VALUES ({placeholders})", vals) # nosec B608 # noqa: S608 - cols = liste fixe + cles filtrees contre FLOW_NODE_FIELDS (whitelist), jamais une cle arbitraire ; valeurs parametrees (?)
|
||||
node_id = int(conn.execute("SELECT last_insert_rowid() AS id").fetchone()["id"])
|
||||
conn.commit()
|
||||
conn.close()
|
||||
return node_id
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
def blocks_view(flow_blocks, flow_nodes, elements_labels):
|
||||
from typing import Any
|
||||
|
||||
|
||||
def blocks_view(
|
||||
flow_blocks: list[dict[str, Any]], flow_nodes: list[dict[str, Any]], elements_labels: dict[int, str]
|
||||
) -> list[dict[str, Any]]:
|
||||
"""Enrichit chaque bloc (nom/description bruts) avec son nombre de
|
||||
nœuds et les éléments qu'il concerne (trigger_element_id/
|
||||
trigger_object_id des déclencheurs + target_element_id/
|
||||
@@ -7,19 +12,21 @@ def blocks_view(flow_blocks, flow_nodes, elements_labels):
|
||||
requête SQL supplémentaire. trigger_object_id/target_object_id
|
||||
(fondations multi-éditeurs, objets de scène) sont toujours None sur
|
||||
un jeu "document" — sans effet sur son comportement existant."""
|
||||
nodes_by_block = {}
|
||||
nodes_by_block: dict[Any, list[dict[str, Any]]] = {}
|
||||
for n in flow_nodes:
|
||||
nodes_by_block.setdefault(n.get("block_id"), []).append(n)
|
||||
view = []
|
||||
view: list[dict[str, Any]] = []
|
||||
for b in flow_blocks:
|
||||
block_nodes = nodes_by_block.get(b["id"], [])
|
||||
element_ids = {n["trigger_element_id"] for n in block_nodes if n.get("trigger_element_id")}
|
||||
element_ids |= {n["target_element_id"] for n in block_nodes if n.get("target_element_id")}
|
||||
element_ids |= {n["trigger_object_id"] for n in block_nodes if n.get("trigger_object_id")}
|
||||
element_ids |= {n["target_object_id"] for n in block_nodes if n.get("target_object_id")}
|
||||
view.append({
|
||||
**b,
|
||||
"node_count": len(block_nodes),
|
||||
"element_labels": [elements_labels[eid] for eid in element_ids if eid in elements_labels],
|
||||
})
|
||||
view.append(
|
||||
{
|
||||
**b,
|
||||
"node_count": len(block_nodes),
|
||||
"element_labels": [elements_labels[eid] for eid in element_ids if eid in elements_labels],
|
||||
}
|
||||
)
|
||||
return view
|
||||
|
||||
@@ -3,7 +3,7 @@ import db
|
||||
from .ensure_flow_blocks_schema import ensure_flow_blocks_schema
|
||||
|
||||
|
||||
def create_flow_block(slug, screen_id, name, description=""):
|
||||
def create_flow_block(slug: str, screen_id: int, name: str, description: str = "") -> int | None:
|
||||
name = (name or "").strip()
|
||||
if not name:
|
||||
return None
|
||||
@@ -13,7 +13,7 @@ def create_flow_block(slug, screen_id, name, description=""):
|
||||
"INSERT INTO _flow_blocks (screen_id, name, description) VALUES (?, ?, ?)",
|
||||
(screen_id, name, (description or "").strip()),
|
||||
)
|
||||
new_id = conn.execute("SELECT last_insert_rowid() AS id").fetchone()["id"]
|
||||
new_id = int(conn.execute("SELECT last_insert_rowid() AS id").fetchone()["id"])
|
||||
conn.commit()
|
||||
conn.close()
|
||||
return new_id
|
||||
|
||||
@@ -4,7 +4,7 @@ from ..ensure_flow_schema import ensure_flow_schema
|
||||
from .ensure_flow_blocks_schema import ensure_flow_blocks_schema
|
||||
|
||||
|
||||
def delete_flow_block(slug, block_id):
|
||||
def delete_flow_block(slug: str, block_id: int) -> None:
|
||||
"""Supprime le bloc ET tout ce qu'il contient (nœuds + arêtes) — décision
|
||||
explicite de l'utilisateur, un bloc de logique se comporte comme une
|
||||
fonction qu'on supprime avec son corps. Patron identique à
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import db
|
||||
|
||||
|
||||
def ensure_flow_blocks_schema(slug):
|
||||
def ensure_flow_blocks_schema(slug: str) -> None:
|
||||
"""Un bloc de logique regroupe les nœuds d'UN écran (scène ou modèle)
|
||||
en sous-graphe nommé/décrit, pour rester lisible même quand un écran
|
||||
accumule des milliers de nœuds — voir _flow_nodes.block_id
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
from typing import Any
|
||||
|
||||
import db
|
||||
|
||||
from .ensure_flow_blocks_schema import ensure_flow_blocks_schema
|
||||
|
||||
|
||||
def get_flow_block(slug, block_id):
|
||||
def get_flow_block(slug: str, block_id: int | None) -> dict[str, Any] | None:
|
||||
if not block_id:
|
||||
return None
|
||||
ensure_flow_blocks_schema(slug)
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
from typing import Any
|
||||
|
||||
import db
|
||||
|
||||
from ..ensure_flow_schema import ensure_flow_schema
|
||||
@@ -6,7 +8,7 @@ from .ensure_flow_blocks_schema import ensure_flow_blocks_schema
|
||||
DEFAULT_BLOCK_NAME = "Bloc principal"
|
||||
|
||||
|
||||
def list_flow_blocks(slug, screen_id):
|
||||
def list_flow_blocks(slug: str, screen_id: int) -> list[dict[str, Any]]:
|
||||
"""Migration douce : un écran qui a des nœuds posés AVANT l'existence
|
||||
des blocs (block_id NULL) se voit attribuer, à la première consultation
|
||||
de l'onglet Blocs, un bloc "Bloc principal" auto-créé qui les regroupe —
|
||||
@@ -38,8 +40,6 @@ def list_flow_blocks(slug, screen_id):
|
||||
(default_block_id, screen_id),
|
||||
)
|
||||
conn.commit()
|
||||
rows = conn.execute(
|
||||
"SELECT * FROM _flow_blocks WHERE screen_id = ? ORDER BY id", (screen_id,)
|
||||
).fetchall()
|
||||
rows = conn.execute("SELECT * FROM _flow_blocks WHERE screen_id = ? ORDER BY id", (screen_id,)).fetchall()
|
||||
conn.close()
|
||||
return [dict(r) for r in rows]
|
||||
|
||||
@@ -3,7 +3,7 @@ import db
|
||||
from .ensure_flow_blocks_schema import ensure_flow_blocks_schema
|
||||
|
||||
|
||||
def update_flow_block(slug, block_id, name, description=""):
|
||||
def update_flow_block(slug: str, block_id: int, name: str, description: str = "") -> None:
|
||||
name = (name or "").strip()
|
||||
if not name:
|
||||
return
|
||||
|
||||
+31
-10
@@ -103,19 +103,36 @@ CONDITION_OPERATORS = [
|
||||
CONDITION_OPERATOR_LABELS = dict(CONDITION_OPERATORS)
|
||||
|
||||
FLOW_NODE_FIELDS = {
|
||||
"trigger_element_id", "trigger_event",
|
||||
"cond_definition_id", "cond_row_id", "cond_field", "cond_field_type", "cond_operator", "cond_value",
|
||||
"trigger_element_id",
|
||||
"trigger_event",
|
||||
"cond_definition_id",
|
||||
"cond_row_id",
|
||||
"cond_field",
|
||||
"cond_field_type",
|
||||
"cond_operator",
|
||||
"cond_value",
|
||||
# 2.4 — conditions combinées (ET/OU) : cond_clauses est une liste JSON de
|
||||
# clauses supplémentaires (en plus de la clause "historique" ci-dessus,
|
||||
# qui reste la première clause) + cond_combinator ('et'/'ou') pour savoir
|
||||
# comment les combiner. Absents => comportement legacy (une seule clause).
|
||||
"cond_clauses", "cond_combinator",
|
||||
"cond_clauses",
|
||||
"cond_combinator",
|
||||
# Condition sur une VARIABLE GLOBALE plutôt qu'un champ d'objet — voir
|
||||
# ensure_flow_schema.py pour le détail des 3 clés (cond_source vaut
|
||||
# "objet" ou "variable" ; absent => "objet", comportement historique).
|
||||
"cond_source", "cond_variable", "cond_variable_chemin",
|
||||
"action_type", "target_screen_id", "target_element_id", "element_property", "element_value",
|
||||
"target_definition_id", "target_row_id", "target_field", "data_operation", "data_value",
|
||||
"cond_source",
|
||||
"cond_variable",
|
||||
"cond_variable_chemin",
|
||||
"action_type",
|
||||
"target_screen_id",
|
||||
"target_element_id",
|
||||
"element_property",
|
||||
"element_value",
|
||||
"target_definition_id",
|
||||
"target_row_id",
|
||||
"target_field",
|
||||
"data_operation",
|
||||
"data_value",
|
||||
"target_variable",
|
||||
# Événements personnalisés (voir screens/custom_events/) : quel
|
||||
# événement un nœud Déclencheur écoute (trigger_event="evenement") ou
|
||||
@@ -125,7 +142,8 @@ FLOW_NODE_FIELDS = {
|
||||
# (déclencheur → condition → action) de décider quoi faire ensuite,
|
||||
# avec ses propres réglages habituels (target_element_id normal, pas
|
||||
# de mécanisme dynamique dédié).
|
||||
"trigger_custom_event_id", "target_custom_event_id",
|
||||
"trigger_custom_event_id",
|
||||
"target_custom_event_id",
|
||||
# Blocs de logique (voir screens/flow/blocks/) : à quel bloc ce nœud
|
||||
# appartient. Aucun `update_flow_node` n'existe (éditer un nœud le
|
||||
# recrée puis supprime l'ancien) — le client doit donc renvoyer
|
||||
@@ -133,14 +151,17 @@ FLOW_NODE_FIELDS = {
|
||||
"block_id",
|
||||
# Phase 3 — déclencheur clavier ("clavier") et minuteur récurrent
|
||||
# ("minuteur"), voir TRIGGER_EVENTS ci-dessus.
|
||||
"trigger_key", "trigger_interval_ms",
|
||||
"trigger_key",
|
||||
"trigger_interval_ms",
|
||||
# Phase 5 — condition de collision (cond_source="collision", aux
|
||||
# côtés de "objet"/"variable") : les deux éléments dont on compare les
|
||||
# rectangles — voir evaluateConditionClause() dans
|
||||
# static/js/play/conditions.js.
|
||||
"cond_element_a", "cond_element_b",
|
||||
"cond_element_a",
|
||||
"cond_element_b",
|
||||
# Fondations multi-éditeurs — jeux "jeu_2d" : équivalents de
|
||||
# trigger_element_id/target_element_id pour un objet de scène (voir
|
||||
# ensure_flow_schema.py).
|
||||
"trigger_object_id", "target_object_id",
|
||||
"trigger_object_id",
|
||||
"target_object_id",
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import db
|
||||
|
||||
|
||||
def delete_flow_edge(slug, edge_id):
|
||||
def delete_flow_edge(slug: str, edge_id: int) -> None:
|
||||
conn = db.connect(slug)
|
||||
conn.execute("DELETE FROM _flow_edges WHERE id = ?", (edge_id,))
|
||||
conn.commit()
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import db
|
||||
|
||||
|
||||
def delete_flow_node(slug, node_id):
|
||||
def delete_flow_node(slug: str, node_id: int) -> None:
|
||||
conn = db.connect(slug)
|
||||
conn.execute("DELETE FROM _flow_edges WHERE from_node_id = ? OR to_node_id = ?", (node_id, node_id))
|
||||
conn.execute("DELETE FROM _flow_nodes WHERE id = ?", (node_id,))
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import db
|
||||
|
||||
|
||||
def ensure_flow_schema(slug):
|
||||
def ensure_flow_schema(slug: str) -> None:
|
||||
conn = db.connect(slug)
|
||||
conn.executescript(
|
||||
"""
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
from typing import Any
|
||||
|
||||
import db
|
||||
|
||||
|
||||
def get_flow_node(slug, node_id):
|
||||
def get_flow_node(slug: str, node_id: int) -> dict[str, Any] | None:
|
||||
conn = db.connect(slug)
|
||||
row = conn.execute("SELECT * FROM _flow_nodes WHERE id = ?", (node_id,)).fetchone()
|
||||
conn.close()
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
from typing import Any
|
||||
|
||||
import db
|
||||
|
||||
from .ensure_flow_schema import ensure_flow_schema
|
||||
|
||||
|
||||
def list_flow_edges(slug, screen_id):
|
||||
def list_flow_edges(slug: str, screen_id: int) -> list[dict[str, Any]]:
|
||||
ensure_flow_schema(slug)
|
||||
conn = db.connect(slug)
|
||||
rows = conn.execute("SELECT * FROM _flow_edges WHERE screen_id = ? ORDER BY id", (screen_id,)).fetchall()
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
from typing import Any
|
||||
|
||||
import db
|
||||
|
||||
from .ensure_flow_schema import ensure_flow_schema
|
||||
|
||||
|
||||
def list_flow_nodes(slug, screen_id):
|
||||
def list_flow_nodes(slug: str, screen_id: int) -> list[dict[str, Any]]:
|
||||
ensure_flow_schema(slug)
|
||||
conn = db.connect(slug)
|
||||
rows = conn.execute("SELECT * FROM _flow_nodes WHERE screen_id = ? ORDER BY id", (screen_id,)).fetchall()
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import db
|
||||
|
||||
|
||||
def move_flow_node(slug, node_id, x, y):
|
||||
def move_flow_node(slug: str, node_id: int, x: float, y: float) -> None:
|
||||
conn = db.connect(slug)
|
||||
conn.execute("UPDATE _flow_nodes SET x = ?, y = ? WHERE id = ?", (x, y, node_id))
|
||||
conn.commit()
|
||||
|
||||
@@ -6,7 +6,7 @@ _DEFAULT_TITLE = "Nouvelle conversation"
|
||||
_TITLE_MAX_LENGTH = 50
|
||||
|
||||
|
||||
def add_ia_chat_message(slug, conversation_id, role, content):
|
||||
def add_ia_chat_message(slug: str, conversation_id: int, role: str, content: str) -> int:
|
||||
"""Ajoute un message (role "user" ou "assistant") à une conversation
|
||||
— voir routes/ia/ia_chat.py, seul appelant.
|
||||
|
||||
@@ -20,11 +20,9 @@ def add_ia_chat_message(slug, conversation_id, role, content):
|
||||
"INSERT INTO _ia_chat_messages (conversation_id, role, content) VALUES (?, ?, ?)",
|
||||
(conversation_id, role, content),
|
||||
)
|
||||
message_id = conn.execute("SELECT last_insert_rowid() AS id").fetchone()["id"]
|
||||
message_id = int(conn.execute("SELECT last_insert_rowid() AS id").fetchone()["id"])
|
||||
if role == "user":
|
||||
conversation = conn.execute(
|
||||
"SELECT title FROM _ia_conversations WHERE id = ?", (conversation_id,)
|
||||
).fetchone()
|
||||
conversation = conn.execute("SELECT title FROM _ia_conversations WHERE id = ?", (conversation_id,)).fetchone()
|
||||
if conversation and conversation["title"] == _DEFAULT_TITLE:
|
||||
title = content.strip()[:_TITLE_MAX_LENGTH]
|
||||
conn.execute("UPDATE _ia_conversations SET title = ? WHERE id = ?", (title, conversation_id))
|
||||
|
||||
@@ -3,14 +3,14 @@ import db
|
||||
from .ensure_ia_chat_schema import ensure_ia_chat_schema
|
||||
|
||||
|
||||
def create_ia_conversation(slug, screen_id):
|
||||
""""+ Nouvelle conversation" (voir static/js/ia/ia-chat.js) : créée
|
||||
def create_ia_conversation(slug: str, screen_id: int) -> int:
|
||||
""" "+ Nouvelle conversation" (voir static/js/ia/ia-chat.js) : créée
|
||||
IMMÉDIATEMENT, vide — visible tout de suite dans la colonne, pas
|
||||
besoin d'attendre un premier message."""
|
||||
ensure_ia_chat_schema(slug)
|
||||
conn = db.connect(slug)
|
||||
conn.execute("INSERT INTO _ia_conversations (screen_id) VALUES (?)", (screen_id,))
|
||||
conversation_id = conn.execute("SELECT last_insert_rowid() AS id").fetchone()["id"]
|
||||
conversation_id = int(conn.execute("SELECT last_insert_rowid() AS id").fetchone()["id"])
|
||||
conn.commit()
|
||||
conn.close()
|
||||
return conversation_id
|
||||
|
||||
@@ -3,7 +3,7 @@ import db
|
||||
from .ensure_ia_chat_schema import ensure_ia_chat_schema
|
||||
|
||||
|
||||
def delete_ia_conversation(slug, conversation_id):
|
||||
def delete_ia_conversation(slug: str, conversation_id: int) -> None:
|
||||
"""Cascade sur les messages (ON DELETE CASCADE, voir
|
||||
ensure_ia_chat_schema.py)."""
|
||||
ensure_ia_chat_schema(slug)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import db
|
||||
|
||||
|
||||
def ensure_ia_chat_schema(slug):
|
||||
def ensure_ia_chat_schema(slug: str) -> None:
|
||||
"""Chat "Générer avec l'IA" — plusieurs CONVERSATIONS par écran
|
||||
(demande explicite : gérer plusieurs fils, comme claude.ai), chacune
|
||||
portant sa propre liste de messages. ON DELETE CASCADE à chaque
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
from typing import Any
|
||||
|
||||
import db
|
||||
|
||||
from .ensure_ia_chat_schema import ensure_ia_chat_schema
|
||||
|
||||
|
||||
def get_ia_conversation(slug, conversation_id):
|
||||
def get_ia_conversation(slug: str, conversation_id: int) -> dict[str, Any] | None:
|
||||
"""Renvoie aussi screen_id — indispensable pour vérifier
|
||||
l'appartenance à CE jeu avant de supprimer/lire/écrire (voir
|
||||
routes/ia/ia_chat.py), même patron que auth.get_user_asset."""
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
from typing import Any
|
||||
|
||||
import db
|
||||
|
||||
from .ensure_ia_chat_schema import ensure_ia_chat_schema
|
||||
|
||||
|
||||
def list_ia_chat_messages(slug, conversation_id):
|
||||
def list_ia_chat_messages(slug: str, conversation_id: int) -> list[dict[str, Any]]:
|
||||
"""Historique d'UNE conversation, dans l'ordre chronologique (voir
|
||||
static/js/ia/ia-chat.js, qui les affiche tels quels)."""
|
||||
ensure_ia_chat_schema(slug)
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
from typing import Any
|
||||
|
||||
import db
|
||||
|
||||
from .ensure_ia_chat_schema import ensure_ia_chat_schema
|
||||
|
||||
|
||||
def list_ia_conversations(slug, screen_id):
|
||||
def list_ia_conversations(slug: str, screen_id: int) -> list[dict[str, Any]]:
|
||||
"""Conversations d'un écran, plus récente en premier (voir la
|
||||
colonne de gauche de l'onglet "Générer avec l'IA")."""
|
||||
ensure_ia_chat_schema(slug)
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import json
|
||||
import os
|
||||
from typing import Any
|
||||
|
||||
# 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
|
||||
@@ -13,14 +14,17 @@ import os
|
||||
# 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",
|
||||
"static",
|
||||
"characters",
|
||||
"animals",
|
||||
"manifest.json",
|
||||
)
|
||||
|
||||
|
||||
def _load_manifest():
|
||||
def _load_manifest() -> dict[str, Any]:
|
||||
try:
|
||||
with open(_MANIFEST_PATH, encoding="utf-8") as f:
|
||||
return json.load(f)
|
||||
return dict(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
|
||||
@@ -29,25 +33,25 @@ def _load_manifest():
|
||||
return {}
|
||||
|
||||
|
||||
def _slug(animal, nn):
|
||||
def _slug(animal: str, nn: str) -> str:
|
||||
return f"animal-{animal}-{nn}"
|
||||
|
||||
|
||||
def _label(family_label, nn):
|
||||
def _label(family_label: str, nn: str) -> str:
|
||||
n = int(nn)
|
||||
return f"{family_label} (variante {n})" if n > 1 else f"{family_label} (de base)"
|
||||
|
||||
|
||||
def _animations_for(animal, nn, poses):
|
||||
def _animations_for(animal: str, nn: str, poses: dict[str, int]) -> dict[str, list[str]]:
|
||||
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():
|
||||
def _build_library() -> dict[str, dict[str, Any]]:
|
||||
manifest = _load_manifest()
|
||||
library = {}
|
||||
library: dict[str, dict[str, Any]] = {}
|
||||
for animal, pack in manifest.items():
|
||||
family_label = pack["label"]
|
||||
for nn, poses in pack["variants"].items():
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import json
|
||||
import os
|
||||
from typing import Any
|
||||
|
||||
# Bibliothèque d'images de fond CraftPix — RÉSERVÉE au rôle admin (voir
|
||||
# core/sprite_gate.py, routes/scenes/scene_object_add.py), même règle que
|
||||
@@ -14,14 +15,16 @@ import os
|
||||
# modifiable ensuite comme n'importe quel objet (panneau "Position & taille").
|
||||
_MANIFEST_PATH = os.path.join(
|
||||
os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))),
|
||||
"static", "backgrounds", "manifest.json",
|
||||
"static",
|
||||
"backgrounds",
|
||||
"manifest.json",
|
||||
)
|
||||
|
||||
|
||||
def _load_manifest():
|
||||
def _load_manifest() -> dict[str, Any]:
|
||||
try:
|
||||
with open(_MANIFEST_PATH, encoding="utf-8") as f:
|
||||
return json.load(f)
|
||||
return dict(json.load(f))
|
||||
except FileNotFoundError:
|
||||
# Poste de dev sans les images de fond copiées (script pas encore
|
||||
# lancé), ou paquet exporté pour un jeu qui n'utilise aucun fond
|
||||
@@ -31,9 +34,9 @@ def _load_manifest():
|
||||
return {}
|
||||
|
||||
|
||||
def _build_library():
|
||||
def _build_library() -> dict[str, dict[str, Any]]:
|
||||
manifest = _load_manifest()
|
||||
library = {}
|
||||
library: dict[str, dict[str, Any]] = {}
|
||||
for pack_slug, pack in manifest.items():
|
||||
pack_label = pack["label"]
|
||||
for image_name, dims in pack["images"].items():
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
def sprite_gallery_families(sprite_library):
|
||||
from typing import Any
|
||||
|
||||
|
||||
def sprite_gallery_families(sprite_library: dict[str, dict[str, Any]]) -> list[tuple[str, dict[str, Any]]]:
|
||||
"""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 ;
|
||||
@@ -9,8 +12,8 @@ def sprite_gallery_families(sprite_library):
|
||||
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 = []
|
||||
seen_families: set[str] = set()
|
||||
families: list[tuple[str, dict[str, Any]]] = []
|
||||
for slug, character in sprite_library.items():
|
||||
family = character.get("family")
|
||||
if family:
|
||||
|
||||
@@ -23,22 +23,43 @@ CHARACTERS = {
|
||||
# pour les 6 personnages (confirmé sur disque), donc une seule définition
|
||||
# suffit pour tous.
|
||||
_SEQUENCE_ANIMATIONS = {
|
||||
"walk": 8, # cycle de marche
|
||||
"run": 3, # course
|
||||
"walk": 8, # cycle de marche
|
||||
"run": 3, # course
|
||||
"attack": 3, # attaque
|
||||
"cheer": 2, # acclamation
|
||||
"climb": 2, # escalade
|
||||
"cheer": 2, # acclamation
|
||||
"climb": 2, # escalade
|
||||
"switch": 2, # bascule/actionner
|
||||
}
|
||||
_SINGLE_POSES = [
|
||||
"idle", "jump", "fall", "duck", "hit", "back", "slide", "interact",
|
||||
"kick", "side", "shove", "shoveBack", "talk", "attackKick", "hang",
|
||||
"hold", "hurt", "behindBack", "down", "drag", "fallDown", "rope",
|
||||
"show", "think", "wide",
|
||||
"idle",
|
||||
"jump",
|
||||
"fall",
|
||||
"duck",
|
||||
"hit",
|
||||
"back",
|
||||
"slide",
|
||||
"interact",
|
||||
"kick",
|
||||
"side",
|
||||
"shove",
|
||||
"shoveBack",
|
||||
"talk",
|
||||
"attackKick",
|
||||
"hang",
|
||||
"hold",
|
||||
"hurt",
|
||||
"behindBack",
|
||||
"down",
|
||||
"drag",
|
||||
"fallDown",
|
||||
"rope",
|
||||
"show",
|
||||
"think",
|
||||
"wide",
|
||||
]
|
||||
|
||||
|
||||
def _animations_for(slug):
|
||||
def _animations_for(slug: str) -> dict[str, list[str]]:
|
||||
animations = {
|
||||
name: [f"/static/characters/{slug}/{name}{i}.png" for i in range(count)]
|
||||
for name, count in _SEQUENCE_ANIMATIONS.items()
|
||||
@@ -54,6 +75,5 @@ def _animations_for(slug):
|
||||
# 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()
|
||||
slug: {"label": label, "animations": _animations_for(slug)} for slug, label in CHARACTERS.items()
|
||||
}
|
||||
|
||||
@@ -1,23 +1,25 @@
|
||||
from typing import Any
|
||||
|
||||
import db
|
||||
|
||||
from ..screens_repo.list_screens import list_screens
|
||||
from ..flow.list_flow_nodes import list_flow_nodes
|
||||
from ..flow.list_flow_edges import list_flow_edges
|
||||
from ..animations.list_animation_clips import list_animation_clips
|
||||
from ..rendering.personnage_data import resolve_personnage_animations
|
||||
from ..rendering.personnage_commands import resolve_personnage_commands
|
||||
from ..rendering.personnage_role import resolve_personnage_role
|
||||
from ..rendering.collision_settings import resolve_collision_settings
|
||||
from ..flow.list_flow_edges import list_flow_edges
|
||||
from ..flow.list_flow_nodes import list_flow_nodes
|
||||
from ..rendering.collision_rules import resolve_collision_rules
|
||||
from ..rendering.screen_triggers import resolve_screen_triggers
|
||||
from ..rendering.collision_settings import resolve_collision_settings
|
||||
from ..rendering.dialogue_box_style import resolve_dialogue_box_style
|
||||
from ..rendering.personnage_commands import resolve_personnage_commands
|
||||
from ..rendering.personnage_data import resolve_personnage_animations
|
||||
from ..rendering.personnage_role import resolve_personnage_role
|
||||
from ..rendering.quiz_box_config import resolve_quiz_box_config
|
||||
from ..rendering.screen_triggers import resolve_screen_triggers
|
||||
from ..rendering.trigger_graph import list_completable_dialogue_ids
|
||||
from ..scenes.list_scene_objects import list_scene_objects
|
||||
from ..scenes.render_scene_object import render_scene_object
|
||||
from ..screens_repo.list_screens import list_screens
|
||||
|
||||
|
||||
def full_game_payload(slug, player_id=db.PLAYER_SHARED):
|
||||
def full_game_payload(slug: str, player_id: str = db.PLAYER_SHARED) -> dict[str, Any]:
|
||||
"""Toutes les données nécessaires au runtime JS de la page de jeu jouable
|
||||
(/game/<slug>/play, ou le paquet Web/SCORM exporté — voir
|
||||
publish/build_scorm_package.py) : chaque écran avec ses éléments (HTML
|
||||
@@ -134,6 +136,9 @@ def full_game_payload(slug, player_id=db.PLAYER_SHARED):
|
||||
fields_meta = {}
|
||||
for d in definitions:
|
||||
full = db.get_definition(slug, d["id"])
|
||||
assert full is not None, ( # nosec B101 # noqa: S101 - narrowing de type, sur par construction
|
||||
"d['id'] vient de list_definitions(slug), donc get_definition ne peut pas renvoyer None ici"
|
||||
)
|
||||
rows = db.list_rows(slug, full, player_id)
|
||||
# Un champ "relation" vit dans une colonne SQL "<champ>_id", jamais
|
||||
# sous son nom "propre" (voir _field_column dans
|
||||
@@ -152,10 +157,7 @@ def full_game_payload(slug, player_id=db.PLAYER_SHARED):
|
||||
if f["type"] == "relation":
|
||||
col += "_id"
|
||||
cols[f["name"]] = col
|
||||
data[str(d["id"])] = [
|
||||
{**{fname: r.get(col) for fname, col in cols.items()}, "id": r["id"]}
|
||||
for r in rows
|
||||
]
|
||||
data[str(d["id"])] = [{**{fname: r.get(col) for fname, col in cols.items()}, "id": r["id"]} for r in rows]
|
||||
# min_value/max_value (2.2 — bornage automatique, voir
|
||||
# apply_data_action.py) : nécessaires ici pour que le port
|
||||
# client-side de l'export Web/SCORM (static/js/play/offline/,
|
||||
@@ -194,9 +196,13 @@ def full_game_payload(slug, player_id=db.PLAYER_SHARED):
|
||||
completable_dialogue_ids = list_completable_dialogue_ids(slug)
|
||||
|
||||
return {
|
||||
"screens": payload_screens, "flows": flows, "animations": animations,
|
||||
"data": data, "fields_meta": fields_meta,
|
||||
"variables": variables, "personnage_animations": personnage_animations,
|
||||
"screens": payload_screens,
|
||||
"flows": flows,
|
||||
"animations": animations,
|
||||
"data": data,
|
||||
"fields_meta": fields_meta,
|
||||
"variables": variables,
|
||||
"personnage_animations": personnage_animations,
|
||||
"definition_names": definition_names,
|
||||
"completable_dialogue_ids": completable_dialogue_ids,
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import json
|
||||
import uuid
|
||||
from typing import Any, Callable
|
||||
|
||||
from db.dialogue_lines import sanitize_dialogue_lines
|
||||
from screens.flow.constants import CONDITION_OPERATORS
|
||||
@@ -110,8 +111,16 @@ from screens.labels.video_modes import VIDEO_MODE_LABELS
|
||||
# ("attaque"/"evenement" retirés — ne servaient à rien pour l'instant,
|
||||
# demande explicite.)
|
||||
ACTION_TYPES = [
|
||||
"dialogue", "variable", "surbrillance", "visibilite", "son", "video", "indication", "attendre",
|
||||
"interagir", "condition",
|
||||
"dialogue",
|
||||
"variable",
|
||||
"surbrillance",
|
||||
"visibilite",
|
||||
"son",
|
||||
"video",
|
||||
"indication",
|
||||
"attendre",
|
||||
"interagir",
|
||||
"condition",
|
||||
]
|
||||
TRIGGER_TYPES = ["collision", "clic", "survol"]
|
||||
CONDITION_OPERATOR_KEYS = {key for key, _label in CONDITION_OPERATORS}
|
||||
@@ -139,7 +148,7 @@ _MAX_ACTION_ID_LENGTH = 40
|
||||
_NO_VALUE_OPERATIONS = ("definir_bool_vrai", "definir_bool_faux", "basculer_bool")
|
||||
|
||||
|
||||
def _sanitize_dialogue(raw_dialogue):
|
||||
def _sanitize_dialogue(raw_dialogue: Any) -> dict[str, Any] | None:
|
||||
if not isinstance(raw_dialogue, dict):
|
||||
return None
|
||||
dialogue_id = raw_dialogue.get("id")
|
||||
@@ -151,8 +160,8 @@ def _sanitize_dialogue(raw_dialogue):
|
||||
}
|
||||
|
||||
|
||||
def _sanitize_dialogue_action(raw_action):
|
||||
action = {"type": "dialogue"}
|
||||
def _sanitize_dialogue_action(raw_action: dict[str, Any]) -> dict[str, Any]:
|
||||
action: dict[str, Any] = {"type": "dialogue"}
|
||||
dialogue = _sanitize_dialogue(raw_action.get("dialogue"))
|
||||
if dialogue:
|
||||
action["dialogue"] = dialogue
|
||||
@@ -161,14 +170,18 @@ def _sanitize_dialogue_action(raw_action):
|
||||
return action
|
||||
|
||||
|
||||
def _sanitize_variable_action(raw_action):
|
||||
def _sanitize_variable_action(raw_action: dict[str, Any]) -> dict[str, Any] | None:
|
||||
target_variable = raw_action.get("target_variable")
|
||||
if not isinstance(target_variable, str) or not target_variable.strip():
|
||||
return None
|
||||
data_operation = raw_action.get("data_operation")
|
||||
if data_operation not in DATA_OPERATION_LABELS:
|
||||
return None
|
||||
action = {"type": "variable", "target_variable": target_variable.strip(), "data_operation": data_operation}
|
||||
action: dict[str, Any] = {
|
||||
"type": "variable",
|
||||
"target_variable": target_variable.strip(),
|
||||
"data_operation": data_operation,
|
||||
}
|
||||
action_id = raw_action.get("id")
|
||||
if isinstance(action_id, str) and action_id.strip():
|
||||
action["id"] = action_id.strip()[:_MAX_ACTION_ID_LENGTH]
|
||||
@@ -181,15 +194,17 @@ def _sanitize_variable_action(raw_action):
|
||||
return action
|
||||
|
||||
|
||||
def _sanitize_object_id(raw_action):
|
||||
def _sanitize_object_id(raw_action: dict[str, Any]) -> int | None:
|
||||
object_id = raw_action.get("object_id")
|
||||
if object_id is None:
|
||||
return None
|
||||
try:
|
||||
return int(object_id)
|
||||
except (TypeError, ValueError):
|
||||
return None
|
||||
|
||||
|
||||
def _with_optional_id(action, raw_action):
|
||||
def _with_optional_id(action: dict[str, Any], raw_action: dict[str, Any]) -> dict[str, Any]:
|
||||
"""Ajoute le champ `id` (optionnel, généré côté client) commun à
|
||||
toutes les feuilles autres que "dialogue" — sert à adresser CETTE
|
||||
feuille précise pour y attacher un "then" plus tard (voir
|
||||
@@ -200,7 +215,7 @@ def _with_optional_id(action, raw_action):
|
||||
return action
|
||||
|
||||
|
||||
def _sanitize_surbrillance_action(raw_action):
|
||||
def _sanitize_surbrillance_action(raw_action: dict[str, Any]) -> dict[str, Any] | None:
|
||||
object_id = _sanitize_object_id(raw_action)
|
||||
valeur = raw_action.get("valeur")
|
||||
if object_id is None or valeur not in SURBRILLANCE_LABELS:
|
||||
@@ -208,7 +223,7 @@ def _sanitize_surbrillance_action(raw_action):
|
||||
return _with_optional_id({"type": "surbrillance", "object_id": object_id, "valeur": valeur}, raw_action)
|
||||
|
||||
|
||||
def _sanitize_visibilite_action(raw_action):
|
||||
def _sanitize_visibilite_action(raw_action: dict[str, Any]) -> dict[str, Any] | None:
|
||||
object_id = _sanitize_object_id(raw_action)
|
||||
valeur = raw_action.get("valeur")
|
||||
if object_id is None or valeur not in ELEMENT_VISIBILITY_LABELS:
|
||||
@@ -216,14 +231,14 @@ def _sanitize_visibilite_action(raw_action):
|
||||
return _with_optional_id({"type": "visibilite", "object_id": object_id, "valeur": valeur}, raw_action)
|
||||
|
||||
|
||||
def _sanitize_son_action(raw_action):
|
||||
def _sanitize_son_action(raw_action: dict[str, Any]) -> dict[str, Any] | None:
|
||||
asset_url = raw_action.get("asset_url")
|
||||
if not isinstance(asset_url, str) or not asset_url.strip():
|
||||
return None
|
||||
return _with_optional_id({"type": "son", "asset_url": asset_url.strip()}, raw_action)
|
||||
|
||||
|
||||
def _sanitize_video_action(raw_action):
|
||||
def _sanitize_video_action(raw_action: dict[str, Any]) -> dict[str, Any] | None:
|
||||
asset_url = raw_action.get("asset_url")
|
||||
mode = raw_action.get("mode")
|
||||
if not isinstance(asset_url, str) or not asset_url.strip() or mode not in VIDEO_MODE_LABELS:
|
||||
@@ -231,26 +246,28 @@ def _sanitize_video_action(raw_action):
|
||||
return _with_optional_id({"type": "video", "asset_url": asset_url.strip(), "mode": mode}, raw_action)
|
||||
|
||||
|
||||
def _sanitize_indication_action(raw_action):
|
||||
def _sanitize_indication_action(raw_action: dict[str, Any]) -> dict[str, Any] | None:
|
||||
object_id = _sanitize_object_id(raw_action)
|
||||
texte = raw_action.get("texte")
|
||||
if object_id is None or not isinstance(texte, str) or not texte.strip():
|
||||
return None
|
||||
action = {"type": "indication", "object_id": object_id, "texte": texte.strip()[:200]}
|
||||
action: dict[str, Any] = {"type": "indication", "object_id": object_id, "texte": texte.strip()[:200]}
|
||||
duree_ms = raw_action.get("duree_ms")
|
||||
if isinstance(duree_ms, (int, float)) and not isinstance(duree_ms, bool) and duree_ms > 0:
|
||||
action["duree_ms"] = int(duree_ms)
|
||||
return _with_optional_id(action, raw_action)
|
||||
|
||||
|
||||
def _sanitize_attendre_action(raw_action):
|
||||
""""Attendre N secondes" avant de continuer la chaîne — MÊME
|
||||
def _sanitize_attendre_action(raw_action: dict[str, Any]) -> dict[str, Any] | None:
|
||||
""" "Attendre N secondes" avant de continuer la chaîne — MÊME
|
||||
vocabulaire que l'action "attendre" du Flow (voir static/js/play/
|
||||
actions.js, `data_value` en SECONDES, pas en ms) : suspend le "then"
|
||||
plutôt que de l'exécuter immédiatement (comme "video"), utile par ex.
|
||||
pour une narration à l'affichage de l'écran qui doit laisser un temps
|
||||
de lecture avant d'enchaîner."""
|
||||
raw_value = raw_action.get("data_value")
|
||||
if raw_value is None:
|
||||
return None
|
||||
try:
|
||||
seconds = float(raw_value)
|
||||
except (TypeError, ValueError):
|
||||
@@ -263,7 +280,7 @@ def _sanitize_attendre_action(raw_action):
|
||||
# Une seule table de vérité pour les feuilles : ajouter un type ici suffit
|
||||
# (voir LEAF_ACTION_TYPES dérivé de ses clés) — évite le if/elif qui aurait
|
||||
# dû être répété à chaque nouvel ajout.
|
||||
_LEAF_ACTION_SANITIZERS = {
|
||||
_LEAF_ACTION_SANITIZERS: dict[str, Callable[[dict[str, Any]], dict[str, Any] | None]] = {
|
||||
"dialogue": _sanitize_dialogue_action,
|
||||
"variable": _sanitize_variable_action,
|
||||
"surbrillance": _sanitize_surbrillance_action,
|
||||
@@ -276,14 +293,14 @@ _LEAF_ACTION_SANITIZERS = {
|
||||
LEAF_ACTION_TYPES = tuple(_LEAF_ACTION_SANITIZERS)
|
||||
|
||||
|
||||
def _sanitize_leaf_action(raw_action, depth=1):
|
||||
def _sanitize_leaf_action(raw_action: Any, depth: int = 1) -> dict[str, Any] | None:
|
||||
"""Sanitize une FEUILLE (voir LEAF_ACTION_TYPES) et son éventuelle
|
||||
chaîne "then" (bornée à _MAX_THEN_CHAIN) — utilisée partout où une
|
||||
feuille est attendue : action de premier niveau, sous "interagir", ou
|
||||
branche Vrai/Faux de "condition"."""
|
||||
if not isinstance(raw_action, dict):
|
||||
return None
|
||||
sanitizer = _LEAF_ACTION_SANITIZERS.get(raw_action.get("type"))
|
||||
sanitizer = _LEAF_ACTION_SANITIZERS.get(raw_action.get("type") or "")
|
||||
if not sanitizer:
|
||||
return None
|
||||
action = sanitizer(raw_action)
|
||||
@@ -296,7 +313,7 @@ def _sanitize_leaf_action(raw_action, depth=1):
|
||||
return action
|
||||
|
||||
|
||||
def _sanitize_condition_branch(raw_branch):
|
||||
def _sanitize_condition_branch(raw_branch: Any) -> dict[str, Any] | None:
|
||||
"""Une branche Vrai/Faux de "condition" : soit None ("ne rien
|
||||
faire", jamais un dict vide), soit une FEUILLE ("dialogue" ou
|
||||
"variable", éventuellement chaînée via "then") — jamais "interagir"/
|
||||
@@ -307,7 +324,7 @@ def _sanitize_condition_branch(raw_branch):
|
||||
return _sanitize_leaf_action(raw_branch)
|
||||
|
||||
|
||||
def _sanitize_action(raw_action, depth=1):
|
||||
def _sanitize_action(raw_action: Any, depth: int = 1) -> dict[str, Any] | None:
|
||||
if not isinstance(raw_action, dict):
|
||||
return None
|
||||
action_type = raw_action.get("type")
|
||||
@@ -337,7 +354,7 @@ def _sanitize_action(raw_action, depth=1):
|
||||
}
|
||||
|
||||
|
||||
def sanitize_collision_rules(raw_rules, allowed_triggers=None):
|
||||
def sanitize_collision_rules(raw_rules: Any, allowed_triggers: list[str] | None = None) -> list[dict[str, Any]]:
|
||||
"""Valide/nettoie une liste de règles postée par le client (voir
|
||||
routes/triggers/) — une règle invalide (déclencheur/action inconnus,
|
||||
imbrication trop profonde...) est IGNORÉE plutôt que de faire planter
|
||||
@@ -350,7 +367,7 @@ def sanitize_collision_rules(raw_rules, allowed_triggers=None):
|
||||
allowed_triggers = TRIGGER_TYPES
|
||||
if not isinstance(raw_rules, list):
|
||||
return []
|
||||
cleaned = []
|
||||
cleaned: list[dict[str, Any]] = []
|
||||
for raw_rule in raw_rules[:50]: # marge large, jamais illimité
|
||||
if not isinstance(raw_rule, dict):
|
||||
continue
|
||||
@@ -364,7 +381,7 @@ def sanitize_collision_rules(raw_rules, allowed_triggers=None):
|
||||
return cleaned
|
||||
|
||||
|
||||
def _apply_dialogue_lines(action, dialogue_id, lines):
|
||||
def _apply_dialogue_lines(action: Any, dialogue_id: str, lines: list[dict[str, Any]]) -> bool:
|
||||
if not isinstance(action, dict):
|
||||
return False
|
||||
action_type = action.get("type")
|
||||
@@ -379,14 +396,13 @@ def _apply_dialogue_lines(action, dialogue_id, lines):
|
||||
if action_type == "interagir":
|
||||
return _apply_dialogue_lines(action.get("sub_action"), dialogue_id, lines)
|
||||
if action_type == "condition":
|
||||
return (
|
||||
_apply_dialogue_lines(action.get("si_vrai"), dialogue_id, lines)
|
||||
or _apply_dialogue_lines(action.get("si_faux"), dialogue_id, lines)
|
||||
return _apply_dialogue_lines(action.get("si_vrai"), dialogue_id, lines) or _apply_dialogue_lines(
|
||||
action.get("si_faux"), dialogue_id, lines
|
||||
)
|
||||
return False
|
||||
|
||||
|
||||
def set_dialogue_lines_in_rules(rules, dialogue_id, raw_lines):
|
||||
def set_dialogue_lines_in_rules(rules: list[dict[str, Any]], dialogue_id: str, raw_lines: Any) -> bool:
|
||||
"""Met à jour EN PLACE les répliques du dialogue `dialogue_id`,
|
||||
où qu'il se trouve dans l'arbre d'action d'une des règles (direct,
|
||||
chaîné via "then", sous "interagir", ou dans une branche de
|
||||
@@ -395,13 +411,10 @@ def set_dialogue_lines_in_rules(rules, dialogue_id, raw_lines):
|
||||
rien modifier si `dialogue_id` n'existe dans aucune règle (référence
|
||||
orpheline, ex. règle supprimée entre-temps)."""
|
||||
lines = sanitize_dialogue_lines(raw_lines)
|
||||
for rule in rules:
|
||||
if _apply_dialogue_lines(rule.get("action"), dialogue_id, lines):
|
||||
return True
|
||||
return False
|
||||
return any(_apply_dialogue_lines(rule.get("action"), dialogue_id, lines) for rule in rules)
|
||||
|
||||
|
||||
def _leaf_id(action):
|
||||
def _leaf_id(action: dict[str, Any]) -> str | None:
|
||||
if action.get("type") == "dialogue":
|
||||
dialogue = action.get("dialogue")
|
||||
return dialogue.get("id") if isinstance(dialogue, dict) else None
|
||||
@@ -410,7 +423,7 @@ def _leaf_id(action):
|
||||
return None
|
||||
|
||||
|
||||
def _apply_then_action(action, leaf_id, then_action):
|
||||
def _apply_then_action(action: Any, leaf_id: str, then_action: dict[str, Any]) -> bool:
|
||||
if not isinstance(action, dict):
|
||||
return False
|
||||
action_type = action.get("type")
|
||||
@@ -431,14 +444,13 @@ def _apply_then_action(action, leaf_id, then_action):
|
||||
if action_type == "interagir":
|
||||
return _apply_then_action(action.get("sub_action"), leaf_id, then_action)
|
||||
if action_type == "condition":
|
||||
return (
|
||||
_apply_then_action(action.get("si_vrai"), leaf_id, then_action)
|
||||
or _apply_then_action(action.get("si_faux"), leaf_id, then_action)
|
||||
return _apply_then_action(action.get("si_vrai"), leaf_id, then_action) or _apply_then_action(
|
||||
action.get("si_faux"), leaf_id, then_action
|
||||
)
|
||||
return False
|
||||
|
||||
|
||||
def set_then_action_in_rules(rules, leaf_id, raw_then_action):
|
||||
def set_then_action_in_rules(rules: list[dict[str, Any]], leaf_id: str, raw_then_action: Any) -> bool:
|
||||
"""INSÈRE EN PLACE une NOUVELLE action juste après la feuille
|
||||
identifiée par `leaf_id` (dialogue.id pour une feuille dialogue, id
|
||||
pour toute autre feuille) — n'importe où dans l'arbre d'action d'une
|
||||
@@ -452,10 +464,7 @@ def set_then_action_in_rules(rules, leaf_id, raw_then_action):
|
||||
then_action = _sanitize_leaf_action(raw_then_action)
|
||||
if not then_action:
|
||||
return False
|
||||
for rule in rules:
|
||||
if _apply_then_action(rule.get("action"), leaf_id, then_action):
|
||||
return True
|
||||
return False
|
||||
return any(_apply_then_action(rule.get("action"), leaf_id, then_action) for rule in rules)
|
||||
|
||||
|
||||
# ---------- Suppression/réordonnancement d'un maillon précis (demande
|
||||
@@ -468,7 +477,8 @@ def set_then_action_in_rules(rules, leaf_id, raw_then_action):
|
||||
# RÈGLE elle-même doit être remplacée ou retirée, pas juste un champ
|
||||
# interne).
|
||||
|
||||
def _remove_leaf_id(action, leaf_id):
|
||||
|
||||
def _remove_leaf_id(action: Any, leaf_id: str) -> tuple[Any, bool]:
|
||||
"""Retire la feuille `leaf_id` de l'arbre `action` — renvoie
|
||||
(nouvel_arbre_ou_None, trouvé). `nouvel_arbre` vaut None si `action`
|
||||
lui-même était la cible et n'avait rien pour le remplacer (ni "then",
|
||||
@@ -511,7 +521,7 @@ def _remove_leaf_id(action, leaf_id):
|
||||
return action, False
|
||||
|
||||
|
||||
def remove_action_from_rules(rules, leaf_id):
|
||||
def remove_action_from_rules(rules: list[dict[str, Any]], leaf_id: str) -> bool:
|
||||
"""Retire EN PLACE l'action `leaf_id` n'importe où dans l'arbre d'une
|
||||
des règles (feuille directe d'une règle, maillon d'une chaîne "then",
|
||||
sous-action d'un "interagir", ou dans une branche de "condition"). Si
|
||||
@@ -531,12 +541,12 @@ def remove_action_from_rules(rules, leaf_id):
|
||||
return False
|
||||
|
||||
|
||||
def _chain_as_list(head_action):
|
||||
def _chain_as_list(head_action: Any) -> list[dict[str, Any]]:
|
||||
"""Aplatit une chaîne de FEUILLES pures (voir LEAF_ACTION_TYPES) en
|
||||
liste — le réordonnancement ne s'applique qu'à l'intérieur d'une
|
||||
chaîne "then" (jamais entre deux branches différentes d'une
|
||||
condition, ni entre une "interagir" et ce qui l'entoure)."""
|
||||
items = []
|
||||
items: list[dict[str, Any]] = []
|
||||
node = head_action
|
||||
while isinstance(node, dict) and node.get("type") in LEAF_ACTION_TYPES:
|
||||
items.append(node)
|
||||
@@ -544,7 +554,7 @@ def _chain_as_list(head_action):
|
||||
return items
|
||||
|
||||
|
||||
def _relink_chain(items):
|
||||
def _relink_chain(items: list[dict[str, Any]]) -> dict[str, Any] | None:
|
||||
items = [dict(n) for n in items]
|
||||
for i in range(len(items) - 1):
|
||||
items[i]["then"] = items[i + 1]
|
||||
@@ -553,7 +563,7 @@ def _relink_chain(items):
|
||||
return items[0] if items else None
|
||||
|
||||
|
||||
def _move_in_action_tree(action, leaf_id, direction):
|
||||
def _move_in_action_tree(action: Any, leaf_id: str, direction: str) -> dict[str, Any] | None:
|
||||
"""Cherche, dans `action`, LA chaîne "then" qui contient `leaf_id`
|
||||
(au premier niveau, sous une "interagir", ou dans une branche de
|
||||
"condition"), et échange `leaf_id` avec son voisin immédiat
|
||||
@@ -593,7 +603,7 @@ def _move_in_action_tree(action, leaf_id, direction):
|
||||
return None
|
||||
|
||||
|
||||
def move_action_in_rules(rules, leaf_id, direction):
|
||||
def move_action_in_rules(rules: list[dict[str, Any]], leaf_id: str, direction: str) -> bool:
|
||||
"""Déplace EN PLACE la feuille `leaf_id` d'un cran vers le haut
|
||||
("up") ou le bas ("down") DANS SA PROPRE CHAÎNE "then" (jamais
|
||||
au-delà de sa branche de condition/sous-action interagir — voir
|
||||
@@ -611,11 +621,11 @@ def move_action_in_rules(rules, leaf_id, direction):
|
||||
return False
|
||||
|
||||
|
||||
def _generate_leaf_id():
|
||||
def _generate_leaf_id() -> str:
|
||||
return "d_" + uuid.uuid4().hex[:12]
|
||||
|
||||
|
||||
def _backfill_leaf_ids(action):
|
||||
def _backfill_leaf_ids(action: Any) -> bool:
|
||||
"""Assure EN PLACE qu'un id STABLE existe sur toute feuille non-
|
||||
"dialogue" (qui a déjà le sien via dialogue.id) qui n'en avait pas —
|
||||
ex. une action "variable" posée AVANT que ce champ ne devienne
|
||||
@@ -647,7 +657,7 @@ def _backfill_leaf_ids(action):
|
||||
return changed
|
||||
|
||||
|
||||
def backfill_missing_leaf_ids(rules):
|
||||
def backfill_missing_leaf_ids(rules: list[dict[str, Any]]) -> bool:
|
||||
"""Applique _backfill_leaf_ids à CHAQUE règle de `rules` (mutées EN
|
||||
PLACE) — renvoie True si au moins une règle a été modifiée, auquel
|
||||
cas l'appelant DOIT persister `rules` (voir routes/triggers/
|
||||
@@ -663,7 +673,7 @@ def backfill_missing_leaf_ids(rules):
|
||||
return changed
|
||||
|
||||
|
||||
def resolve_collision_rules(obj):
|
||||
def resolve_collision_rules(obj: dict[str, Any]) -> list[dict[str, Any]]:
|
||||
"""Décode attributes._collision_rules en liste Python, jamais None —
|
||||
un attribut absent/corrompu retombe sur aucune règle (comportement
|
||||
inchangé pour tout objet déjà posé avant l'ajout de cet éditeur)."""
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import json
|
||||
from typing import Any
|
||||
|
||||
# Boîte de collision d'un objet de scène (voir screens/scenes/
|
||||
# set_scene_object_collision.py) : posée AUTOMATIQUEMENT sur chaque objet
|
||||
@@ -9,14 +10,18 @@ import json
|
||||
# forme (rectangle/cercle) et taille/décalage propres, pour un sprite très
|
||||
# paddé (ex. les frames CraftPix) dont la silhouette réelle est bien plus
|
||||
# petite que son canevas.
|
||||
DEFAULT_COLLISION_SETTINGS = {
|
||||
"enabled": True, "shape": "rectangle", "width": None, "height": None,
|
||||
"offset_x": 0, "offset_y": 0,
|
||||
DEFAULT_COLLISION_SETTINGS: dict[str, Any] = {
|
||||
"enabled": True,
|
||||
"shape": "rectangle",
|
||||
"width": None,
|
||||
"height": None,
|
||||
"offset_x": 0,
|
||||
"offset_y": 0,
|
||||
}
|
||||
COLLISION_SHAPES = ["rectangle", "cercle"]
|
||||
|
||||
|
||||
def resolve_collision_settings(obj):
|
||||
def resolve_collision_settings(obj: dict[str, Any]) -> dict[str, Any]:
|
||||
"""Décode attributes._collision en dict Python, jamais None — un
|
||||
attribut absent/corrompu retombe sur le défaut (boîte de collision =
|
||||
la boîte entière de l'objet, comme si ce réglage n'existait pas)."""
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import json
|
||||
from typing import Any
|
||||
|
||||
# Widget d'interface "💬 Boîte de dialogue" (voir "🖥️ Interface",
|
||||
# templates/scene_edit.html, screens/scenes/add_scene_object.py kind=
|
||||
@@ -7,7 +8,7 @@ import json
|
||||
# static/js/play/dialogue-box-controller.js) — header (nom du
|
||||
# personnage qui parle), corps (texte), pied (bouton "Suivant"),
|
||||
# entièrement stylisable par l'auteur.
|
||||
DEFAULT_DIALOGUE_BOX_STYLE = {
|
||||
DEFAULT_DIALOGUE_BOX_STYLE: dict[str, Any] = {
|
||||
"font_family": "inherit",
|
||||
"font_size": 14,
|
||||
"font_weight": "normal",
|
||||
@@ -20,14 +21,17 @@ DEFAULT_DIALOGUE_BOX_STYLE = {
|
||||
FONT_WEIGHT_CHOICES = ["normal", "bold"]
|
||||
|
||||
|
||||
def sanitize_dialogue_box_style(raw_style):
|
||||
def sanitize_dialogue_box_style(raw_style: Any) -> dict[str, Any]:
|
||||
style = dict(DEFAULT_DIALOGUE_BOX_STYLE)
|
||||
if not isinstance(raw_style, dict):
|
||||
return style
|
||||
if isinstance(raw_style.get("font_family"), str) and raw_style["font_family"].strip():
|
||||
style["font_family"] = raw_style["font_family"].strip()[:60]
|
||||
raw_font_size = raw_style.get("font_size")
|
||||
try:
|
||||
style["font_size"] = max(8, min(48, int(raw_style.get("font_size"))))
|
||||
if raw_font_size is None:
|
||||
raise ValueError
|
||||
style["font_size"] = max(8, min(48, int(raw_font_size)))
|
||||
except (TypeError, ValueError):
|
||||
pass
|
||||
if raw_style.get("font_weight") in FONT_WEIGHT_CHOICES:
|
||||
@@ -39,7 +43,7 @@ def sanitize_dialogue_box_style(raw_style):
|
||||
return style
|
||||
|
||||
|
||||
def resolve_dialogue_box_style(obj):
|
||||
def resolve_dialogue_box_style(obj: dict[str, Any]) -> dict[str, Any]:
|
||||
raw = (obj.get("attributes") or {}).get("_dialogue_box_style")
|
||||
if not raw:
|
||||
return dict(DEFAULT_DIALOGUE_BOX_STYLE)
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
from ..screens_repo.list_screens import list_screens
|
||||
from ..scenes.list_scene_objects import list_scene_objects
|
||||
from ..screens_repo.list_screens import list_screens
|
||||
from .personnage_data import resolve_personnage_data
|
||||
|
||||
|
||||
def list_used_forge_characters(slug):
|
||||
def list_used_forge_characters(slug: str) -> set[str]:
|
||||
"""Ensemble des slugs SPRITE_LIBRARY (screens.SPRITE_LIBRARY)
|
||||
réellement référencés par CE jeu — tous écrans confondus. Utilisé par
|
||||
publish/build_scorm_package.py pour ne copier dans le .zip exporté que les
|
||||
dossiers static/characters/animals/<animal>/<NN>/ réellement utilisés
|
||||
par CE jeu, plutôt que les ~52 000 fichiers de toute la bibliothèque
|
||||
(voir le plan "Bibliothèque de sprites animaux CraftPix")."""
|
||||
used = set()
|
||||
used: set[str] = set()
|
||||
for s in list_screens(slug, include_templates=True):
|
||||
for o in list_scene_objects(slug, s["id"]):
|
||||
if o.get("kind") == "personnage":
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import json
|
||||
from typing import Any
|
||||
|
||||
# ZQSD (convention AZERTY, comme le reste de l'éditeur) + E pour interagir
|
||||
# — un objet de scène kind="personnage" se déplace et s'anime TOUT SEUL
|
||||
@@ -11,9 +12,15 @@ import json
|
||||
# "extra" : touches SUPPLÉMENTAIRES, chacune liée à une pose du personnage
|
||||
# au choix (ex. "sauter" -> "jump") — au-delà des 4 touches de déplacement
|
||||
# et de "interagir", qui restent les deux seules touches à usage FIXE.
|
||||
DEFAULT_PERSONNAGE_COMMANDS = {
|
||||
"haut": "z", "bas": "s", "gauche": "q", "droite": "d",
|
||||
"interagir": "e", "axe": "libre", "vitesse": 4, "extra": [],
|
||||
DEFAULT_PERSONNAGE_COMMANDS: dict[str, Any] = {
|
||||
"haut": "z",
|
||||
"bas": "s",
|
||||
"gauche": "q",
|
||||
"droite": "d",
|
||||
"interagir": "e",
|
||||
"axe": "libre",
|
||||
"vitesse": 4,
|
||||
"extra": [],
|
||||
}
|
||||
|
||||
# Nombre maximum de touches supplémentaires — large marge pour un vrai
|
||||
@@ -22,10 +29,10 @@ DEFAULT_PERSONNAGE_COMMANDS = {
|
||||
_MAX_EXTRA_COMMANDS = 20
|
||||
|
||||
|
||||
def sanitize_extra_commands(raw_extra):
|
||||
def sanitize_extra_commands(raw_extra: Any) -> list[dict[str, str]]:
|
||||
if not isinstance(raw_extra, list):
|
||||
return []
|
||||
cleaned = []
|
||||
cleaned: list[dict[str, str]] = []
|
||||
for item in raw_extra[:_MAX_EXTRA_COMMANDS]:
|
||||
if not isinstance(item, dict):
|
||||
continue
|
||||
@@ -36,7 +43,7 @@ def sanitize_extra_commands(raw_extra):
|
||||
return cleaned
|
||||
|
||||
|
||||
def resolve_personnage_commands(el):
|
||||
def resolve_personnage_commands(el: dict[str, Any]) -> dict[str, Any]:
|
||||
"""Décode attributes._personnage_commandes (voir
|
||||
screens/scenes/set_scene_object_commands.py) en dict Python, jamais
|
||||
None — un attribut absent/corrompu retombe sur les touches par défaut
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import json
|
||||
from typing import Any
|
||||
|
||||
from ..labels.sprite_library import PUBLIC_SPRITE_LIBRARY
|
||||
from ..labels.animal_sprite_library import ADMIN_SPRITE_LIBRARY
|
||||
from ..labels.sprite_library import PUBLIC_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
|
||||
@@ -13,7 +14,7 @@ _SPRITE_LIBRARY = {**PUBLIC_SPRITE_LIBRARY, **ADMIN_SPRITE_LIBRARY}
|
||||
_DEFAULT_PERSONNAGE_DATA = {"source": "forge", "forge_character": "male-adventurer"}
|
||||
|
||||
|
||||
def resolve_personnage_data(el):
|
||||
def resolve_personnage_data(el: dict[str, Any]) -> dict[str, Any]:
|
||||
"""Décode attributes._personnage_data (voir set_personnage_data.py) en
|
||||
dict Python, jamais None — un attribut absent/corrompu retombe sur le
|
||||
défaut posé à la création (default_style_for_widget.py)."""
|
||||
@@ -27,18 +28,18 @@ def resolve_personnage_data(el):
|
||||
return data if isinstance(data, dict) else dict(_DEFAULT_PERSONNAGE_DATA)
|
||||
|
||||
|
||||
def resolve_personnage_animations(el):
|
||||
def resolve_personnage_animations(el: dict[str, Any]) -> dict[str, list[str]]:
|
||||
"""{nom_animation: [urls...]} pour cet élément personnage, quelle que
|
||||
soit sa source — Forge (SPRITE_LIBRARY) ou sprites propres au créateur
|
||||
(ses propres frames importées, voir personnage-panel.js)."""
|
||||
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") or "")
|
||||
return character["animations"] if character else {}
|
||||
|
||||
|
||||
def resolve_personnage_idle_frame(el):
|
||||
def resolve_personnage_idle_frame(el: dict[str, Any]) -> str:
|
||||
"""Première image de l'animation "idle" (ou, à défaut, de la première
|
||||
animation disponible) — utilisée pour le rendu serveur ET l'aperçu
|
||||
animé de l'éditeur, jamais vide tant qu'au moins une frame existe."""
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
from typing import Any
|
||||
|
||||
# Rôle d'un personnage posé sur une scène 2D (voir
|
||||
# screens/scenes/set_scene_object_role.py) : seul un personnage "joueur"
|
||||
# est déplacé/animé automatiquement au clavier et suivi par la caméra
|
||||
@@ -11,7 +13,7 @@ PERSONNAGE_ROLE_LABELS = {"joueur": "Joueur", "ennemie": "Ennemi", "pnj": "PNJ"}
|
||||
DEFAULT_PERSONNAGE_ROLE = "joueur"
|
||||
|
||||
|
||||
def resolve_personnage_role(el):
|
||||
def resolve_personnage_role(el: dict[str, Any]) -> str:
|
||||
"""Décode attributes._personnage_role — un attribut absent/corrompu
|
||||
retombe sur "joueur" (comportement inchangé pour tout personnage posé
|
||||
avant l'ajout de ce réglage : c'était déjà, de fait, le seul rôle qui
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import json
|
||||
from typing import Any
|
||||
|
||||
# Réglages "❓ Boîte à quiz" propres au quiz AUTONOME/plein écran (demande
|
||||
# explicite : "étendre les fonctionnalités de la boîte à quiz pour les RH
|
||||
@@ -19,7 +20,7 @@ import json
|
||||
# Les deux réglages sont conservés INDÉPENDAMMENT (jamais écrasés l'un par
|
||||
# l'autre) : activer/désactiver le plein écran change juste lequel des
|
||||
# deux s'applique, sans jamais perdre le choix de l'autre.
|
||||
DEFAULT_QUIZ_BOX_CONFIG = {
|
||||
DEFAULT_QUIZ_BOX_CONFIG: dict[str, Any] = {
|
||||
"fullscreen": False,
|
||||
"timer_mode": "aucun",
|
||||
"timer_seconds": 20,
|
||||
@@ -68,15 +69,18 @@ _MIN_TIMER_SECONDS = 3
|
||||
_MAX_TIMER_SECONDS = 600
|
||||
|
||||
|
||||
def sanitize_quiz_box_config(raw_config):
|
||||
def sanitize_quiz_box_config(raw_config: Any) -> dict[str, Any]:
|
||||
config = dict(DEFAULT_QUIZ_BOX_CONFIG)
|
||||
if not isinstance(raw_config, dict):
|
||||
return config
|
||||
config["fullscreen"] = bool(raw_config.get("fullscreen"))
|
||||
if raw_config.get("timer_mode") in TIMER_MODES:
|
||||
config["timer_mode"] = raw_config["timer_mode"]
|
||||
raw_timer_seconds = raw_config.get("timer_seconds")
|
||||
try:
|
||||
config["timer_seconds"] = max(_MIN_TIMER_SECONDS, min(_MAX_TIMER_SECONDS, int(raw_config.get("timer_seconds"))))
|
||||
if raw_timer_seconds is None:
|
||||
raise ValueError
|
||||
config["timer_seconds"] = max(_MIN_TIMER_SECONDS, min(_MAX_TIMER_SECONDS, int(raw_timer_seconds)))
|
||||
except (TypeError, ValueError):
|
||||
pass
|
||||
if raw_config.get("dialog_template") in QUIZ_BOX_DIALOG_TEMPLATES:
|
||||
@@ -86,7 +90,7 @@ def sanitize_quiz_box_config(raw_config):
|
||||
return config
|
||||
|
||||
|
||||
def resolve_quiz_box_config(obj):
|
||||
def resolve_quiz_box_config(obj: dict[str, Any]) -> dict[str, Any]:
|
||||
raw = (obj.get("attributes") or {}).get("_quiz_box_config")
|
||||
if not raw:
|
||||
return dict(DEFAULT_QUIZ_BOX_CONFIG)
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import html as html_lib
|
||||
from typing import Any, Callable
|
||||
|
||||
# ---------- "❓ Boîte à quiz" — DEUX catégories de modèles bien distinctes
|
||||
# (voir screens/rendering/quiz_box_config.py) :
|
||||
@@ -43,7 +44,7 @@ import html as html_lib
|
||||
# render_quiz_box_defaut.
|
||||
|
||||
|
||||
def _quiz_data_attrs(obj, config, active_template):
|
||||
def _quiz_data_attrs(obj: dict[str, Any], config: dict[str, Any], active_template: str) -> str:
|
||||
return (
|
||||
f'data-object-id="{obj["id"]}" data-element-id="{obj["id"]}" '
|
||||
f'data-quiz-template="{active_template}" '
|
||||
@@ -52,7 +53,7 @@ def _quiz_data_attrs(obj, config, active_template):
|
||||
)
|
||||
|
||||
|
||||
def _quiz_box_style(obj, style):
|
||||
def _quiz_box_style(obj: dict[str, Any], style: dict[str, Any]) -> str:
|
||||
return (
|
||||
f"position:absolute; left:{obj['x']}px; top:{obj['y']}px; "
|
||||
f"width:{obj['width']}px; height:{obj['height']}px; z-index:{obj['z_index']}; "
|
||||
@@ -61,16 +62,16 @@ def _quiz_box_style(obj, style):
|
||||
)
|
||||
|
||||
|
||||
def _quiz_topbar_html():
|
||||
def _quiz_topbar_html() -> str:
|
||||
return (
|
||||
'<div class="quizBoxTopBar" data-quiz-role="topbar">'
|
||||
'<span class="quizBoxScoreDisplay" data-quiz-role="score">Score : 0</span>'
|
||||
'<span class="quizBoxTimerDisplay" data-quiz-role="timer"></span>'
|
||||
'</div>'
|
||||
"</div>"
|
||||
)
|
||||
|
||||
|
||||
def _quiz_header_row_html(header_text, header_bg=None):
|
||||
def _quiz_header_row_html(header_text: str, header_bg: str | None = None) -> str:
|
||||
# Le score/minuteur (topbar) est un FRÈRE du header, jamais son enfant
|
||||
# (voir le commentaire d'en-tête) — les deux partagent ici le MÊME
|
||||
# bandeau (fond commun posé sur ".quizBoxHeaderRow", jamais sur le
|
||||
@@ -79,29 +80,28 @@ def _quiz_header_row_html(header_text, header_bg=None):
|
||||
# part au-dessus (demande explicite). `header_bg=None` : aucun fond
|
||||
# posé, le bandeau reste transparent (repli disponible pour un futur
|
||||
# modèle qui n'en aurait pas besoin).
|
||||
bg_style = f' style="background:{html_lib.escape(header_bg)}"' if header_bg else ''
|
||||
bg_style = f' style="background:{html_lib.escape(header_bg)}"' if header_bg else ""
|
||||
return (
|
||||
f'<div class="quizBoxHeaderRow"{bg_style}>'
|
||||
f'<div class="quizBoxHeader" data-quiz-role="header">{header_text}</div>'
|
||||
+ _quiz_topbar_html() +
|
||||
'</div>'
|
||||
f'<div class="quizBoxHeader" data-quiz-role="header">{header_text}</div>' + _quiz_topbar_html() + "</div>"
|
||||
)
|
||||
|
||||
|
||||
def render_quiz_box_defaut(obj, config, style):
|
||||
def render_quiz_box_defaut(obj: dict[str, Any], config: dict[str, Any], style: dict[str, Any]) -> str:
|
||||
return (
|
||||
f'<div class="quizBoxWidget" role="dialog" aria-live="polite" aria-atomic="true" {_quiz_data_attrs(obj, config, config["dialog_template"])} style="{_quiz_box_style(obj, style)}">'
|
||||
+ _quiz_header_row_html("Quête : Titre de la quête", style["header_bg"]) +
|
||||
f'<div class="quizBoxBody" data-quiz-role="body" style="background:{html_lib.escape(style["body_bg"])}">'
|
||||
f'<div class="quizBoxWidget" role="dialog" aria-live="polite" aria-atomic="true" '
|
||||
f'{_quiz_data_attrs(obj, config, config["dialog_template"])} style="{_quiz_box_style(obj, style)}">'
|
||||
+ _quiz_header_row_html("Quête : Titre de la quête", style["header_bg"])
|
||||
+ f'<div class="quizBoxBody" data-quiz-role="body" style="background:{html_lib.escape(style["body_bg"])}">'
|
||||
f'<p class="quizBoxQuestionText" data-quiz-role="question">La question s\'affiche ici.</p>'
|
||||
f'<div class="quizBoxChoices" data-quiz-role="choices">'
|
||||
f'<button type="button" class="quizBoxChoiceBtn" disabled>Choix A</button>'
|
||||
f'<button type="button" class="quizBoxChoiceBtn" disabled>Choix B</button>'
|
||||
f'</div></div></div>'
|
||||
f"</div></div></div>"
|
||||
)
|
||||
|
||||
|
||||
def render_quiz_box_manga_dialogue(obj, config, style):
|
||||
def render_quiz_box_manga_dialogue(obj: dict[str, Any], config: dict[str, Any], style: dict[str, Any]) -> str:
|
||||
# "Manga" (boîte de dialogue) — pendant, en petite carte flottante
|
||||
# JAMAIS plein écran, du modèle "page de quiz" "Manga" (voir
|
||||
# render_quiz_box_page_manga), validé par le créateur via une
|
||||
@@ -123,10 +123,12 @@ def render_quiz_box_manga_dialogue(obj, config, style):
|
||||
# doublerait dans l'aperçu éditeur (jamais réécrit par le JS avant le
|
||||
# premier vrai jeu).
|
||||
return (
|
||||
f'<div class="quizBoxWidget quizBoxWidget--mangaDialogue" role="dialog" aria-live="polite" aria-atomic="true" {_quiz_data_attrs(obj, config, config["dialog_template"])} style="{_quiz_box_style(obj, style)}">'
|
||||
f'<div class="quizBoxWidget quizBoxWidget--mangaDialogue" role="dialog" aria-live="polite" '
|
||||
f'aria-atomic="true" {_quiz_data_attrs(obj, config, config["dialog_template"])} '
|
||||
f'style="{_quiz_box_style(obj, style)}">'
|
||||
'<div class="quizMangaDialogueFrame">'
|
||||
+ _quiz_header_row_html("Titre de la quête", style["header_bg"]) +
|
||||
f'<div class="quizBoxBody" data-quiz-role="body" style="background:{html_lib.escape(style["body_bg"])}">'
|
||||
+ _quiz_header_row_html("Titre de la quête", style["header_bg"])
|
||||
+ f'<div class="quizBoxBody" data-quiz-role="body" style="background:{html_lib.escape(style["body_bg"])}">'
|
||||
f'<p class="quizBoxQuestionText" data-quiz-role="question">La question s\'affiche ici.</p>'
|
||||
f'<div class="quizBoxChoices" data-quiz-role="choices">'
|
||||
f'<button type="button" class="quizBoxChoiceBtn" disabled>Choix A</button>'
|
||||
@@ -138,8 +140,8 @@ def render_quiz_box_manga_dialogue(obj, config, style):
|
||||
# demande explicite), donc ce repère est maintenant fidèle au
|
||||
# comportement réel, pas juste à l'aperçu éditeur.
|
||||
'<button type="button" class="quizBoxContinueBtn" disabled>Continuer →</button>'
|
||||
'</div></div>'
|
||||
'</div>'
|
||||
"</div></div>"
|
||||
"</div>"
|
||||
# Bandeau de pied de page TOUJOURS visible (comme la maquette
|
||||
# validée) — purement décoratif (aria-hidden, jamais un
|
||||
# data-quiz-role), le moteur JS partagé ne le touche JAMAIS : le
|
||||
@@ -152,11 +154,11 @@ def render_quiz_box_manga_dialogue(obj, config, style):
|
||||
# l'intérieur au lieu d'être positionnés par-dessus TOUT le
|
||||
# widget (.quizBoxWidget--mangaDialogue est position:relative).
|
||||
'<div class="quizMangaDialogueFooterBand" aria-hidden="true"></div>'
|
||||
'</div>'
|
||||
"</div>"
|
||||
)
|
||||
|
||||
|
||||
def render_quiz_box_page_manga(obj, config, style):
|
||||
def render_quiz_box_page_manga(obj: dict[str, Any], config: dict[str, Any], style: dict[str, Any]) -> str:
|
||||
# "Manga" — premier modèle "PAGE DE QUIZ" (voir screens/rendering/
|
||||
# quiz_box_config.py::QUIZ_BOX_PAGE_TEMPLATES) : thème complet et
|
||||
# autonome, validé par le créateur via une maquette interactive avant
|
||||
@@ -185,18 +187,20 @@ def render_quiz_box_page_manga(obj, config, style):
|
||||
# dédié pour le maintenir à jour question après question.
|
||||
timer_placeholder = "Aucun" if config["timer_mode"] == "aucun" else ""
|
||||
return (
|
||||
f'<div class="quizBoxWidget quizBoxWidget--manga" role="dialog" aria-live="polite" aria-atomic="true" {_quiz_data_attrs(obj, config, config["page_template"])} style="{_quiz_box_style(obj, style)}">'
|
||||
f'<div class="quizBoxWidget quizBoxWidget--manga" role="dialog" aria-live="polite" '
|
||||
f'aria-atomic="true" {_quiz_data_attrs(obj, config, config["page_template"])} '
|
||||
f'style="{_quiz_box_style(obj, style)}">'
|
||||
'<div class="mangaTopbar">'
|
||||
'<div class="mangaPlate mangaPlate--timer">'
|
||||
'<span class="mangaPlateLabel">Temps</span>'
|
||||
f'<span class="mangaPlateValue quizBoxTimerDisplay" data-quiz-role="timer">{timer_placeholder}</span>'
|
||||
'</div>'
|
||||
"</div>"
|
||||
'<div class="mangaTitlemark"><span>Manga Quiz</span></div>'
|
||||
'<div class="mangaPlate mangaPlate--score">'
|
||||
'<span class="mangaPlateLabel">Score</span>'
|
||||
'<span class="mangaPlateValue" data-quiz-role="score">Score : 0</span>'
|
||||
'</div>'
|
||||
'</div>'
|
||||
"</div>"
|
||||
"</div>"
|
||||
# Barre d'étapes (voir static/js/play/quiz-box-templates.js::
|
||||
# forgeQuizMangaOnShow) — remplie EN JEU (nombre de questions total
|
||||
# connu seulement à l'exécution, jamais à la pose de l'objet) ;
|
||||
@@ -206,11 +210,11 @@ def render_quiz_box_page_manga(obj, config, style):
|
||||
'<div class="mangaPanelQuestion">'
|
||||
'<span class="mangaKicker" data-quiz-role="header">Titre de la quête</span>'
|
||||
'<p class="mangaQuestionText" data-quiz-role="question">La question s\'affiche ici.</p>'
|
||||
'</div>'
|
||||
"</div>"
|
||||
'<div class="mangaChoices" data-quiz-role="choices">'
|
||||
'<button type="button" class="quizBoxChoiceBtn" disabled>Choix A</button>'
|
||||
'<button type="button" class="quizBoxChoiceBtn" disabled>Choix B</button>'
|
||||
'</div>'
|
||||
"</div>"
|
||||
# Écran de résultat de fin de quiz — voir forgeQuizMangaOnQuizComplete
|
||||
# (static/js/play/quiz-box-templates.js), même mécanique que
|
||||
# "classique" (render_quiz_box_page_classique) mais dans le thème
|
||||
@@ -221,11 +225,11 @@ def render_quiz_box_page_manga(obj, config, style):
|
||||
# bandeau topbar/score + la barre d'étapes visibles. data-manga-
|
||||
# role (pas data-quiz-role) : décoration propre à ce modèle.
|
||||
'<div class="mangaResultZone" data-manga-role="result"></div>'
|
||||
'</div>'
|
||||
"</div>"
|
||||
)
|
||||
|
||||
|
||||
def render_quiz_box_page_classique(obj, config, style):
|
||||
def render_quiz_box_page_classique(obj: dict[str, Any], config: dict[str, Any], style: dict[str, Any]) -> str:
|
||||
# "Classique" — modèle de PAGE DE QUIZ DE BASE (sobre/professionnel),
|
||||
# validé par le créateur via une maquette interactive (formation
|
||||
# sécurité incendie comme exemple). Reprend VOLONTAIREMENT les
|
||||
@@ -249,23 +253,27 @@ def render_quiz_box_page_classique(obj, config, style):
|
||||
# dans [data-quiz-role="choices"]) se pose en bandeau bas façon
|
||||
# pied de page, dans la couleur configurée via "🎨 Style"
|
||||
# (footer_bg), jamais une couleur figée en dur.
|
||||
f'<div class="quizBoxWidget quizBoxWidget--classique" role="dialog" aria-live="polite" aria-atomic="true" {_quiz_data_attrs(obj, config, config["page_template"])} style="{_quiz_box_style(obj, style)} --classique-footer-bg:{html_lib.escape(style["footer_bg"])}; --classique-body-bg:{html_lib.escape(style["body_bg"])};">'
|
||||
f'<div class="quizBoxWidget quizBoxWidget--classique" role="dialog" aria-live="polite" '
|
||||
f'aria-atomic="true" {_quiz_data_attrs(obj, config, config["page_template"])} '
|
||||
f'style="{_quiz_box_style(obj, style)} '
|
||||
f"--classique-footer-bg:{html_lib.escape(style['footer_bg'])}; "
|
||||
f'--classique-body-bg:{html_lib.escape(style["body_bg"])};">'
|
||||
f'<div class="classiqueHeaderBar" style="background:{html_lib.escape(style["header_bg"])}">'
|
||||
'<div class="classiqueHeaderTitleWrap">'
|
||||
'<span class="classiqueHeaderEyebrow">❓ Quiz</span>'
|
||||
f'<span class="classiqueHeaderTitle">{title}</span>'
|
||||
'</div>'
|
||||
"</div>"
|
||||
'<div class="classiqueHeaderChips">'
|
||||
'<div class="classiqueChip classiqueChip--timer">'
|
||||
'<span class="classiqueChipLabel">Temps</span>'
|
||||
f'<span class="classiqueChipValue quizBoxTimerDisplay" data-quiz-role="timer">{timer_placeholder}</span>'
|
||||
'</div>'
|
||||
"</div>"
|
||||
'<div class="classiqueChip classiqueChip--score">'
|
||||
'<span class="classiqueChipLabel">Score</span>'
|
||||
'<span class="classiqueChipValue" data-quiz-role="score">Score : 0</span>'
|
||||
'</div>'
|
||||
'</div>'
|
||||
'</div>'
|
||||
"</div>"
|
||||
"</div>"
|
||||
"</div>"
|
||||
f'<div class="classiqueBody" style="background:{html_lib.escape(style["body_bg"])}">'
|
||||
'<div class="classiqueContent">'
|
||||
# Barre d'étapes — même principe que "manga" (voir
|
||||
@@ -282,7 +290,7 @@ def render_quiz_box_page_classique(obj, config, style):
|
||||
'<div class="classiqueChoices" data-quiz-role="choices">'
|
||||
'<button type="button" class="quizBoxChoiceBtn" disabled>Choix A</button>'
|
||||
'<button type="button" class="quizBoxChoiceBtn" disabled>Choix B</button>'
|
||||
'</div>'
|
||||
"</div>"
|
||||
# Encart d'explication après réponse — RÉSERVÉ pour une
|
||||
# fonctionnalité FUTURE, PAS ENCORE implémentée (demande
|
||||
# explicite : "tu peut le mettre dans le modele mais ne
|
||||
@@ -299,8 +307,8 @@ def render_quiz_box_page_classique(obj, config, style):
|
||||
# CSS tout le reste : progression/question/choix). data-classique-
|
||||
# role (pas data-quiz-role) : décoration propre à ce modèle.
|
||||
'<div class="classiqueResultZone" data-classique-role="result"></div>'
|
||||
'</div>'
|
||||
'</div>'
|
||||
"</div>"
|
||||
"</div>"
|
||||
# Bandeau de pied de page TOUJOURS visible (footer_bg, même
|
||||
# esprit que header/body ci-dessus) — purement décoratif
|
||||
# (aria-hidden, jamais un data-quiz-role), le moteur JS partagé
|
||||
@@ -309,22 +317,24 @@ def render_quiz_box_page_classique(obj, config, style):
|
||||
# (voir static/style.css), pas DANS ce bandeau — demande
|
||||
# explicite : "il manque le footer foncé avec le bouton".
|
||||
'<div class="classiqueFooterBand" aria-hidden="true"></div>'
|
||||
'</div>'
|
||||
"</div>"
|
||||
)
|
||||
|
||||
|
||||
QUIZ_BOX_DIALOG_RENDERERS = {
|
||||
_QuizRenderer = Callable[[dict[str, Any], dict[str, Any], dict[str, Any]], str]
|
||||
|
||||
QUIZ_BOX_DIALOG_RENDERERS: dict[str, _QuizRenderer] = {
|
||||
"defaut": render_quiz_box_defaut,
|
||||
"manga_dialogue": render_quiz_box_manga_dialogue,
|
||||
}
|
||||
|
||||
QUIZ_BOX_PAGE_RENDERERS = {
|
||||
QUIZ_BOX_PAGE_RENDERERS: dict[str, _QuizRenderer] = {
|
||||
"classique": render_quiz_box_page_classique,
|
||||
"manga": render_quiz_box_page_manga,
|
||||
}
|
||||
|
||||
|
||||
def render_quiz_box(obj, config, style):
|
||||
def render_quiz_box(obj: dict[str, Any], config: dict[str, Any], style: dict[str, Any]) -> str:
|
||||
"""Point d'entrée UNIQUE (voir screens/scenes/render_scene_object.py)
|
||||
— bascule entre les deux catégories de modèles selon `fullscreen`
|
||||
(demande explicite : ces modèles ne se voient JAMAIS dans l'autre
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
from ..screens_repo.list_screens import list_screens
|
||||
from ..scenes.list_scene_objects import list_scene_objects
|
||||
from ..screens_repo.list_screens import list_screens
|
||||
|
||||
# Le widget "💬 Boîte de dialogue" (kind="dialogue_box") n'est pas un
|
||||
# acteur de la scène — il n'a pas de sens comme "qui parle" dans un
|
||||
@@ -8,14 +8,14 @@ from ..scenes.list_scene_objects import list_scene_objects
|
||||
_EXCLUDED_KINDS = {"dialogue_box"}
|
||||
|
||||
|
||||
def list_named_scene_object_names(slug):
|
||||
def list_named_scene_object_names(slug: str) -> list[str]:
|
||||
"""Noms distincts de tous les objets de scène nommés (voir "ℹ️
|
||||
Informations", templates/scene_edit.html) — n'importe quel kind
|
||||
(personnage, décor, fond), toutes scènes 2D du jeu confondues : un
|
||||
déclencheur est GAME-WIDE, son dialogue peut donc mettre en scène un
|
||||
objet posé sur une autre scène que celle où la règle de collision se
|
||||
déclenche. Ordre alphabétique, jamais de doublon."""
|
||||
names = set()
|
||||
names: set[str] = set()
|
||||
for s in list_screens(slug):
|
||||
if s["kind"] != "jeu_2d":
|
||||
continue
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import json
|
||||
from typing import Any
|
||||
|
||||
from .collision_rules import sanitize_collision_rules
|
||||
|
||||
@@ -13,7 +14,7 @@ from .collision_rules import sanitize_collision_rules
|
||||
TRIGGER_TYPES_SCREEN = ["affichage"]
|
||||
|
||||
|
||||
def sanitize_screen_triggers(raw_rules):
|
||||
def sanitize_screen_triggers(raw_rules: Any) -> list[dict[str, Any]]:
|
||||
"""Valide/nettoie une liste de règles "déclencheur d'écran -> action"
|
||||
postée par le client (voir routes/screens/screen_triggers.py) — même
|
||||
garanties que sanitize_collision_rules, juste un vocabulaire de
|
||||
@@ -21,7 +22,7 @@ def sanitize_screen_triggers(raw_rules):
|
||||
return sanitize_collision_rules(raw_rules, allowed_triggers=TRIGGER_TYPES_SCREEN)
|
||||
|
||||
|
||||
def resolve_screen_triggers(screen):
|
||||
def resolve_screen_triggers(screen: dict[str, Any] | None) -> list[dict[str, Any]]:
|
||||
"""Décode _screens._screen_triggers en liste Python, jamais None — un
|
||||
écran créé avant l'ajout de cette fonctionnalité (colonne NULL)
|
||||
retombe sur aucune règle, même principe que resolve_collision_rules
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
def _trigger_for(widget_key, attributes):
|
||||
if (attributes or {}).get("type") == "submit":
|
||||
return "soumission"
|
||||
return "clic"
|
||||
@@ -1,10 +1,15 @@
|
||||
def _game_objects_with_thumbnails(slug, screens_mod):
|
||||
from typing import Any
|
||||
|
||||
from .collision_rules import LEAF_ACTION_TYPES
|
||||
|
||||
|
||||
def _game_objects_with_thumbnails(slug: str, screens_mod: Any) -> list[dict[str, Any]]:
|
||||
"""Objets de TOUTES les scènes "jeu_2d" du jeu (contrairement à
|
||||
screens.list_scene_objects, qui ne prend qu'un screen_id) —
|
||||
rendered_html ajouté comme l'exigent screens.build_collision_rule_objects
|
||||
(voir routes/scenes/scene_edit_view.py pour le même patron, une seule
|
||||
scène)."""
|
||||
result = []
|
||||
result: list[dict[str, Any]] = []
|
||||
for s in screens_mod.list_screens(slug):
|
||||
if s["kind"] != "jeu_2d":
|
||||
continue
|
||||
@@ -17,14 +22,12 @@ def _game_objects_with_thumbnails(slug, screens_mod):
|
||||
return result
|
||||
|
||||
|
||||
from .collision_rules import LEAF_ACTION_TYPES
|
||||
|
||||
# Champs affichés pour chaque type de feuille (voir screens/rendering/
|
||||
# collision_rules.py pour la forme exacte de chacune) — un simple filtre
|
||||
# sur l'action déjà sanitizée, jamais une recopie de valeur : ajouter un
|
||||
# type de feuille suffit à l'ajouter ici, sans dupliquer sa logique de
|
||||
# sanitisation.
|
||||
_LEAF_DISPLAY_FIELDS = {
|
||||
_LEAF_DISPLAY_FIELDS: dict[str, tuple[str, ...]] = {
|
||||
"dialogue": ("dialogue",),
|
||||
"variable": ("id", "target_variable", "data_operation", "data_value"),
|
||||
"surbrillance": ("id", "object_id", "valeur"),
|
||||
@@ -36,21 +39,21 @@ _LEAF_DISPLAY_FIELDS = {
|
||||
}
|
||||
|
||||
|
||||
def _describe_leaf(action):
|
||||
def _describe_leaf(action: dict[str, Any]) -> dict[str, Any]:
|
||||
"""Une seule feuille (voir LEAF_ACTION_TYPES, screens/rendering/
|
||||
collision_rules.py) décrite pour l'affichage, SANS son éventuel
|
||||
"then" (voir _describe_chain, qui aplatit la chaîne complète)."""
|
||||
action_type = action.get("type")
|
||||
fields = _LEAF_DISPLAY_FIELDS.get(action_type, ())
|
||||
fields = _LEAF_DISPLAY_FIELDS.get(action_type or "", ())
|
||||
return {"type": action_type, **{f: action[f] for f in fields if f in action}}
|
||||
|
||||
|
||||
def _describe_chain(action):
|
||||
def _describe_chain(action: Any) -> list[dict[str, Any]]:
|
||||
"""Aplatit une feuille + ses "then" successifs en une LISTE, dans
|
||||
l'ordre d'exécution — voir maquette validée ("plusieurs actions
|
||||
chaînées"). Liste vide si `action` n'est pas une feuille reconnue
|
||||
(voir LEAF_ACTION_TYPES)."""
|
||||
chain = []
|
||||
chain: list[dict[str, Any]] = []
|
||||
node = action
|
||||
while isinstance(node, dict) and node.get("type") in LEAF_ACTION_TYPES:
|
||||
chain.append(_describe_leaf(node))
|
||||
@@ -58,13 +61,13 @@ def _describe_chain(action):
|
||||
return chain
|
||||
|
||||
|
||||
def _branch_info(branch):
|
||||
def _branch_info(branch: Any) -> dict[str, Any] | None:
|
||||
if not isinstance(branch, dict) or branch.get("type") not in LEAF_ACTION_TYPES:
|
||||
return None
|
||||
return {"chain": _describe_chain(branch)}
|
||||
|
||||
|
||||
def _resolve_action_trigger(action, has_interagir=False):
|
||||
def _resolve_action_trigger(action: Any, has_interagir: bool = False) -> dict[str, Any]:
|
||||
"""Décrit ENTIÈREMENT un arbre d'action (voir screens/rendering/
|
||||
collision_rules.py) pour l'affichage dans l'onglet "Déclencheurs" —
|
||||
chaque feuille affiche DIRECTEMENT son propre contenu, où qu'elle soit
|
||||
@@ -85,9 +88,11 @@ def _resolve_action_trigger(action, has_interagir=False):
|
||||
return _resolve_action_trigger(action.get("sub_action") or {}, has_interagir=True)
|
||||
if action_type == "condition":
|
||||
return {
|
||||
"has_interagir": has_interagir, "chain": [],
|
||||
"has_interagir": has_interagir,
|
||||
"chain": [],
|
||||
"condition": {
|
||||
"variable": action.get("variable"), "operateur": action.get("operateur"),
|
||||
"variable": action.get("variable"),
|
||||
"operateur": action.get("operateur"),
|
||||
"valeur": action.get("valeur"),
|
||||
"si_vrai": _branch_info(action.get("si_vrai")),
|
||||
"si_faux": _branch_info(action.get("si_faux")),
|
||||
@@ -96,7 +101,7 @@ def _resolve_action_trigger(action, has_interagir=False):
|
||||
return {"has_interagir": has_interagir, "chain": [], "condition": None}
|
||||
|
||||
|
||||
def list_all_triggers(slug):
|
||||
def list_all_triggers(slug: str) -> list[dict[str, Any]]:
|
||||
"""TOUS les déclencheurs du jeu, un par occurrence de "À la collision"
|
||||
posée sur un objet de scène — remplace l'ancienne recherche inversée
|
||||
par quête : plus de filtrage par id, chaque déclencheur affiche
|
||||
@@ -107,7 +112,7 @@ def list_all_triggers(slug):
|
||||
objects = _game_objects_with_thumbnails(slug, screens_mod)
|
||||
rule_objects = screens_mod.build_collision_rule_objects(objects)
|
||||
|
||||
result = []
|
||||
result: list[dict[str, Any]] = []
|
||||
for obj in rule_objects:
|
||||
# Rétro-remplissage des feuilles posées avant que "id" ne devienne
|
||||
# systématique (ex. par l'IA, ce champ restant optionnel dans son
|
||||
@@ -120,19 +125,21 @@ def list_all_triggers(slug):
|
||||
screens_mod.set_scene_object_collision_rules(slug, obj["id"], obj["rules"])
|
||||
for rule in obj["rules"]:
|
||||
info = _resolve_action_trigger(rule["action"])
|
||||
result.append({
|
||||
"object_id": obj["id"],
|
||||
"object_name": obj["name"],
|
||||
"thumbnail_html": obj["thumbnail_html"],
|
||||
"has_interagir": info["has_interagir"],
|
||||
"trigger": rule["trigger"],
|
||||
"condition": info["condition"],
|
||||
"chain": info["chain"],
|
||||
})
|
||||
result.append(
|
||||
{
|
||||
"object_id": obj["id"],
|
||||
"object_name": obj["name"],
|
||||
"thumbnail_html": obj["thumbnail_html"],
|
||||
"has_interagir": info["has_interagir"],
|
||||
"trigger": rule["trigger"],
|
||||
"condition": info["condition"],
|
||||
"chain": info["chain"],
|
||||
}
|
||||
)
|
||||
return result
|
||||
|
||||
|
||||
def _collect_dialogue_lines_from_action(action, acc):
|
||||
def _collect_dialogue_lines_from_action(action: Any, acc: list[list[dict[str, Any]]]) -> None:
|
||||
if not isinstance(action, dict):
|
||||
return
|
||||
action_type = action.get("type")
|
||||
@@ -150,7 +157,7 @@ def _collect_dialogue_lines_from_action(action, acc):
|
||||
_collect_dialogue_lines_from_action(action.get("si_faux"), acc)
|
||||
|
||||
|
||||
def collect_all_dialogue_lines(slug):
|
||||
def collect_all_dialogue_lines(slug: str) -> list[list[dict[str, Any]]]:
|
||||
"""Toutes les répliques/questions de TOUS les dialogues embarqués dans
|
||||
les règles de collision de TOUS les objets de scène du jeu (voir
|
||||
screens/rendering/collision_rules.py::"dialogue".dialogue), y compris
|
||||
@@ -160,7 +167,7 @@ def collect_all_dialogue_lines(slug):
|
||||
publish/build_scorm_package.py)."""
|
||||
import screens as screens_mod
|
||||
|
||||
acc = []
|
||||
acc: list[list[dict[str, Any]]] = []
|
||||
for s in screens_mod.list_screens(slug):
|
||||
if s["kind"] != "jeu_2d":
|
||||
continue
|
||||
@@ -172,7 +179,7 @@ def collect_all_dialogue_lines(slug):
|
||||
return acc
|
||||
|
||||
|
||||
def _collect_completable_ids_from_action(action, acc):
|
||||
def _collect_completable_ids_from_action(action: Any, acc: list[str]) -> None:
|
||||
if not isinstance(action, dict):
|
||||
return
|
||||
action_type = action.get("type")
|
||||
@@ -190,7 +197,7 @@ def _collect_completable_ids_from_action(action, acc):
|
||||
_collect_completable_ids_from_action(action.get("si_faux"), acc)
|
||||
|
||||
|
||||
def list_completable_dialogue_ids(slug):
|
||||
def list_completable_dialogue_ids(slug: str) -> list[str]:
|
||||
"""Tous les id de dialogue du jeu dont l'action porte
|
||||
`mark_completed` (voir screens/rendering/collision_rules.py), y
|
||||
compris au fil d'une chaîne "then" — utilisée par static/js/play/
|
||||
@@ -199,7 +206,7 @@ def list_completable_dialogue_ids(slug):
|
||||
aucune notion de quête."""
|
||||
import screens as screens_mod
|
||||
|
||||
acc = []
|
||||
acc: list[str] = []
|
||||
for s in screens_mod.list_screens(slug):
|
||||
if s["kind"] != "jeu_2d":
|
||||
continue
|
||||
@@ -211,7 +218,7 @@ def list_completable_dialogue_ids(slug):
|
||||
return acc
|
||||
|
||||
|
||||
def list_all_collision_objects(slug):
|
||||
def list_all_collision_objects(slug: str) -> list[dict[str, Any]]:
|
||||
"""Catalogue de TOUS les objets éligibles à une règle de collision,
|
||||
toutes scènes "jeu_2d" confondues — pour peupler le sélecteur d'objet
|
||||
de la modale d'ajout de déclencheur (voir routes/triggers/)."""
|
||||
|
||||
@@ -24,7 +24,18 @@ _DECOR_DEFAULT_SIZE = (200, 200)
|
||||
_DECOR_COLLISION_SIZE = (150, 150)
|
||||
|
||||
|
||||
def add_scene_object(slug, scene_id, kind="personnage", forge_character=None, background_slug=None, image_url=None, image_width=None, image_height=None, x=None, y=None):
|
||||
def add_scene_object(
|
||||
slug: str,
|
||||
scene_id: int,
|
||||
kind: str = "personnage",
|
||||
forge_character: str | None = None,
|
||||
background_slug: str | None = None,
|
||||
image_url: str | None = None,
|
||||
image_width: int | None = None,
|
||||
image_height: int | None = None,
|
||||
x: float | None = None,
|
||||
y: float | None = None,
|
||||
) -> int:
|
||||
"""Pose un nouvel objet sur une scène (voir ensure_scene_schema.py).
|
||||
kind="personnage" : attributes._personnage_data, MÊME structure/mêmes
|
||||
fonctions resolve_personnage_* que le widget "personnage" de l'éditeur
|
||||
@@ -64,8 +75,9 @@ def add_scene_object(slug, scene_id, kind="personnage", forge_character=None, ba
|
||||
if kind == "personnage":
|
||||
data = dict(_DEFAULT_PERSONNAGE_DATA)
|
||||
if forge_character:
|
||||
from ..labels.sprite_library import PUBLIC_SPRITE_LIBRARY
|
||||
from ..labels.animal_sprite_library import ADMIN_SPRITE_LIBRARY
|
||||
from ..labels.sprite_library import PUBLIC_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)
|
||||
@@ -86,11 +98,15 @@ def add_scene_object(slug, scene_id, kind="personnage", forge_character=None, ba
|
||||
elif kind == "decor" and image_url:
|
||||
attributes["src"] = image_url
|
||||
width, height = _DECOR_DEFAULT_SIZE
|
||||
attributes["_collision"] = json.dumps({
|
||||
"width": _DECOR_COLLISION_SIZE[0], "height": _DECOR_COLLISION_SIZE[1],
|
||||
})
|
||||
attributes["_collision"] = json.dumps(
|
||||
{
|
||||
"width": _DECOR_COLLISION_SIZE[0],
|
||||
"height": _DECOR_COLLISION_SIZE[1],
|
||||
}
|
||||
)
|
||||
elif kind == "fond" and background_slug:
|
||||
from ..labels.background_library import BACKGROUND_LIBRARY
|
||||
|
||||
background = BACKGROUND_LIBRARY.get(background_slug)
|
||||
if background:
|
||||
attributes["src"] = background["url"]
|
||||
@@ -104,9 +120,9 @@ def add_scene_object(slug, scene_id, kind="personnage", forge_character=None, ba
|
||||
if kind == "fond":
|
||||
z_index = -1000
|
||||
else:
|
||||
max_z = conn.execute(
|
||||
"SELECT MAX(z_index) AS m FROM _scene_objects WHERE scene_id = ?", (scene_id,)
|
||||
).fetchone()["m"]
|
||||
max_z = conn.execute("SELECT MAX(z_index) AS m FROM _scene_objects WHERE scene_id = ?", (scene_id,)).fetchone()[
|
||||
"m"
|
||||
]
|
||||
z_index = (max_z or 0) + 1
|
||||
cols = ["scene_id", "kind", "attributes", "z_index"]
|
||||
values = [scene_id, kind, json.dumps(attributes), z_index]
|
||||
@@ -123,8 +139,8 @@ def add_scene_object(slug, scene_id, kind="personnage", forge_character=None, ba
|
||||
cols += ["x", "y"]
|
||||
values += [x, y]
|
||||
placeholders = ", ".join(["?"] * len(values))
|
||||
conn.execute(f"INSERT INTO _scene_objects ({', '.join(cols)}) VALUES ({placeholders})", values)
|
||||
object_id = conn.execute("SELECT last_insert_rowid() AS id").fetchone()["id"]
|
||||
conn.execute(f"INSERT INTO _scene_objects ({', '.join(cols)}) VALUES ({placeholders})", values) # nosec B608 # noqa: S608 - cols construit uniquement a partir de litteraux Python, jamais d'une cle arbitraire ; valeurs parametrees (?)
|
||||
object_id = int(conn.execute("SELECT last_insert_rowid() AS id").fetchone()["id"])
|
||||
conn.commit()
|
||||
conn.close()
|
||||
return object_id
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
from typing import Any
|
||||
|
||||
from ..rendering.collision_rules import resolve_collision_rules, sanitize_collision_rules, set_then_action_in_rules
|
||||
from .get_scene_object import get_scene_object
|
||||
from .set_scene_object_collision_rules import set_scene_object_collision_rules
|
||||
from ..rendering.collision_rules import resolve_collision_rules, sanitize_collision_rules, set_then_action_in_rules
|
||||
|
||||
|
||||
def append_action_to_trigger(slug, object_id, after_id, raw_action):
|
||||
""""+ Ajouter une action" — extrait de routes/triggers/triggers.py::
|
||||
def append_action_to_trigger(slug: str, object_id: int, after_id: str, raw_action: Any) -> bool:
|
||||
""" "+ Ajouter une action" — extrait de routes/triggers/triggers.py::
|
||||
trigger_append_action pour être l'UNIQUE chemin d'écriture, partagé
|
||||
par la route HTTP ET par l'agent IA (voir ai/tools.py) : ni l'un ni
|
||||
l'autre n'écrit `_collision_rules` directement, tous deux passent par
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
from typing import Any
|
||||
|
||||
import screens
|
||||
|
||||
_DEFAULT_OBJECT_LABELS = {"personnage": "Personnage", "fond": "Fond"}
|
||||
_COLLISION_EXCLUDED_KINDS = ("fond", "dialogue_box", "quiz_box", "score_widget")
|
||||
|
||||
|
||||
def build_collision_rule_objects(objects):
|
||||
def build_collision_rule_objects(objects: list[dict[str, Any]]) -> list[dict[str, Any]]:
|
||||
"""Une carte "🧩 Collision" (voir templates/scene_edit.html) par objet
|
||||
ÉLIGIBLE (jamais un "fond", jamais le personnage "joueur" — ce sont
|
||||
TOUJOURS les AUTRES objets qui réagissent à SA présence, voir
|
||||
@@ -33,14 +35,13 @@ def build_collision_rule_objects(objects):
|
||||
rôle explicite sont traités comme "pnj" ici, sans rien réécrire en
|
||||
base."""
|
||||
implicit_joueur_claimed = any(
|
||||
(o.get("attributes") or {}).get("_personnage_role") == "joueur"
|
||||
for o in objects if o["kind"] == "personnage"
|
||||
(o.get("attributes") or {}).get("_personnage_role") == "joueur" for o in objects if o["kind"] == "personnage"
|
||||
)
|
||||
result = []
|
||||
result: list[dict[str, Any]] = []
|
||||
for o in objects:
|
||||
if o["kind"] in _COLLISION_EXCLUDED_KINDS:
|
||||
continue
|
||||
role = None
|
||||
role: str | None = None
|
||||
if o["kind"] == "personnage":
|
||||
explicit_role = (o.get("attributes") or {}).get("_personnage_role")
|
||||
if explicit_role in screens.PERSONNAGE_ROLES:
|
||||
@@ -52,12 +53,14 @@ def build_collision_rule_objects(objects):
|
||||
role = "pnj"
|
||||
if role == "joueur":
|
||||
continue
|
||||
result.append({
|
||||
"id": o["id"],
|
||||
"name": o.get("name") or _DEFAULT_OBJECT_LABELS.get(o["kind"], "Décor"),
|
||||
"kind": o["kind"],
|
||||
"role": role,
|
||||
"thumbnail_html": o["rendered_html"],
|
||||
"rules": screens.resolve_collision_rules(o),
|
||||
})
|
||||
result.append(
|
||||
{
|
||||
"id": o["id"],
|
||||
"name": o.get("name") or _DEFAULT_OBJECT_LABELS.get(o["kind"], "Décor"),
|
||||
"kind": o["kind"],
|
||||
"role": role,
|
||||
"thumbnail_html": o["rendered_html"],
|
||||
"rules": screens.resolve_collision_rules(o),
|
||||
}
|
||||
)
|
||||
return result
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import db
|
||||
|
||||
from .ensure_scene_schema import ensure_scene_schema
|
||||
from ..animations.ensure_animation_schema import ensure_animation_schema
|
||||
from ..flow.ensure_flow_schema import ensure_flow_schema
|
||||
from .ensure_scene_schema import ensure_scene_schema
|
||||
|
||||
|
||||
def delete_scene_object(slug, object_id):
|
||||
def delete_scene_object(slug: str, object_id: int) -> None:
|
||||
"""Supprime un objet de scène ET les nœuds de logique qui le
|
||||
référencent (trigger_object_id/target_object_id/cond_element_a/
|
||||
cond_element_b — aucun n'a de contrainte FK, voir
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import db
|
||||
|
||||
|
||||
def ensure_scene_schema(slug):
|
||||
def ensure_scene_schema(slug: str) -> None:
|
||||
"""Crée _scene_objects si nécessaire — le contenu d'une scène pour un
|
||||
jeu "jeu_2d" (voir db/games/get_game_type.py), équivalent de
|
||||
_screen_elements pour un jeu "document" mais en coordonnées PIXEL
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import json
|
||||
from typing import Any
|
||||
|
||||
import db
|
||||
|
||||
from .ensure_scene_schema import ensure_scene_schema
|
||||
|
||||
|
||||
def get_scene_object(slug, object_id):
|
||||
def get_scene_object(slug: str, object_id: int) -> dict[str, Any] | None:
|
||||
ensure_scene_schema(slug)
|
||||
conn = db.connect(slug)
|
||||
row = conn.execute("SELECT * FROM _scene_objects WHERE id = ?", (object_id,)).fetchone()
|
||||
|
||||
@@ -1,18 +1,17 @@
|
||||
import json
|
||||
from typing import Any
|
||||
|
||||
import db
|
||||
|
||||
from .ensure_scene_schema import ensure_scene_schema
|
||||
|
||||
|
||||
def list_scene_objects(slug, scene_id):
|
||||
def list_scene_objects(slug: str, scene_id: int) -> list[dict[str, Any]]:
|
||||
ensure_scene_schema(slug)
|
||||
conn = db.connect(slug)
|
||||
rows = conn.execute(
|
||||
"SELECT * FROM _scene_objects WHERE scene_id = ? ORDER BY z_index, id", (scene_id,)
|
||||
).fetchall()
|
||||
rows = conn.execute("SELECT * FROM _scene_objects WHERE scene_id = ? ORDER BY z_index, id", (scene_id,)).fetchall()
|
||||
conn.close()
|
||||
result = []
|
||||
result: list[dict[str, Any]] = []
|
||||
for r in rows:
|
||||
d = dict(r)
|
||||
d["attributes"] = json.loads(d["attributes"] or "{}")
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
from ..rendering.collision_rules import move_action_in_rules, resolve_collision_rules, sanitize_collision_rules
|
||||
from .get_scene_object import get_scene_object
|
||||
from .set_scene_object_collision_rules import set_scene_object_collision_rules
|
||||
from ..rendering.collision_rules import resolve_collision_rules, sanitize_collision_rules, move_action_in_rules
|
||||
|
||||
|
||||
def move_action_in_trigger(slug, object_id, leaf_id, direction):
|
||||
def move_action_in_trigger(slug: str, object_id: int, leaf_id: str, direction: str) -> bool:
|
||||
"""Déplace une action précise d'un cran ("up"/"down", voir
|
||||
screens/rendering/collision_rules.py::move_action_in_rules) dans son
|
||||
déclencheur d'OBJET — même patron qu'append_action_to_trigger."""
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
from ..rendering.collision_rules import remove_action_from_rules, resolve_collision_rules, sanitize_collision_rules
|
||||
from .get_scene_object import get_scene_object
|
||||
from .set_scene_object_collision_rules import set_scene_object_collision_rules
|
||||
from ..rendering.collision_rules import resolve_collision_rules, sanitize_collision_rules, remove_action_from_rules
|
||||
|
||||
|
||||
def remove_action_from_trigger(slug, object_id, leaf_id):
|
||||
def remove_action_from_trigger(slug: str, object_id: int, leaf_id: str) -> bool:
|
||||
"""Retire une action précise (voir screens/rendering/collision_rules.py
|
||||
::remove_action_from_rules) d'un déclencheur d'OBJET — même patron
|
||||
qu'append_action_to_trigger : UNIQUE chemin d'écriture, partagé par la
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import html as html_lib
|
||||
from typing import Any
|
||||
|
||||
from ..rendering.personnage_data import resolve_personnage_idle_frame
|
||||
from ..rendering.dialogue_box_style import resolve_dialogue_box_style
|
||||
from ..rendering.personnage_data import resolve_personnage_idle_frame
|
||||
from ..rendering.quiz_box_config import resolve_quiz_box_config
|
||||
from ..rendering.quiz_box_templates import render_quiz_box
|
||||
|
||||
|
||||
def render_scene_object(obj):
|
||||
def render_scene_object(obj: dict[str, Any]) -> str:
|
||||
"""HTML d'un objet de scène — positionnement en PIXELS ABSOLUS (pas de
|
||||
% fluide, voir ensure_scene_schema.py), mirror volontairement plus
|
||||
simple que render_element_html.py (pas d'arbre de nesting, pas de
|
||||
@@ -52,12 +53,12 @@ def render_scene_object(obj):
|
||||
# collision "dialogue" ait un dialogue à afficher. Toujours VISIBLE
|
||||
# dans l'éditeur (avec un texte d'exemple) pour pouvoir le
|
||||
# positionner/styliser.
|
||||
style = resolve_dialogue_box_style(obj)
|
||||
box_theme = resolve_dialogue_box_style(obj)
|
||||
box_style = (
|
||||
f"position:absolute; left:{obj['x']}px; top:{obj['y']}px; "
|
||||
f"width:{obj['width']}px; height:{obj['height']}px; z-index:{obj['z_index']}; "
|
||||
f"font-family:{html_lib.escape(style['font_family'])}; font-size:{style['font_size']}px; "
|
||||
f"font-weight:{style['font_weight']}; color:{html_lib.escape(style['text_color'])};"
|
||||
f"font-family:{html_lib.escape(box_theme['font_family'])}; font-size:{box_theme['font_size']}px; "
|
||||
f"font-weight:{box_theme['font_weight']}; color:{html_lib.escape(box_theme['text_color'])};"
|
||||
)
|
||||
return (
|
||||
# role="dialog"/aria-live (RGAA/WCAG 4.1.3) : le texte de
|
||||
@@ -65,12 +66,17 @@ def render_scene_object(obj):
|
||||
# static/js/play/dialogue-box-controller.js) sans jamais
|
||||
# recharger la page — sans annonce explicite, un lecteur
|
||||
# d'écran ne détecte aucun changement.
|
||||
f'<div class="dialogueBoxWidget" role="dialog" aria-live="polite" aria-atomic="true" data-object-id="{obj["id"]}" data-element-id="{obj["id"]}" style="{box_style}">'
|
||||
f'<div class="dialogueBoxHeader" data-dialogue-role="header" style="background:{html_lib.escape(style["header_bg"])}">Nom du personnage</div>'
|
||||
f'<div class="dialogueBoxBody" data-dialogue-role="body" style="background:{html_lib.escape(style["body_bg"])}">Le texte du dialogue s\'affiche ici.</div>'
|
||||
f'<div class="dialogueBoxFooter" data-dialogue-role="footer" style="background:{html_lib.escape(style["footer_bg"])}">'
|
||||
f'<button type="button" class="dialogueBoxNextBtn" data-dialogue-role="next-btn" onclick="forgeDialogueBoxAdvance({obj["id"]})">Suivant →</button>'
|
||||
f'</div></div>'
|
||||
f'<div class="dialogueBoxWidget" role="dialog" aria-live="polite" aria-atomic="true" '
|
||||
f'data-object-id="{obj["id"]}" data-element-id="{obj["id"]}" style="{box_style}">'
|
||||
f'<div class="dialogueBoxHeader" data-dialogue-role="header" '
|
||||
f'style="background:{html_lib.escape(box_theme["header_bg"])}">Nom du personnage</div>'
|
||||
f'<div class="dialogueBoxBody" data-dialogue-role="body" '
|
||||
f'style="background:{html_lib.escape(box_theme["body_bg"])}">Le texte du dialogue s\'affiche ici.</div>'
|
||||
f'<div class="dialogueBoxFooter" data-dialogue-role="footer" '
|
||||
f'style="background:{html_lib.escape(box_theme["footer_bg"])}">'
|
||||
f'<button type="button" class="dialogueBoxNextBtn" data-dialogue-role="next-btn" '
|
||||
f'onclick="forgeDialogueBoxAdvance({obj["id"]})">Suivant →</button>'
|
||||
f"</div></div>"
|
||||
)
|
||||
if obj["kind"] == "quiz_box":
|
||||
# Widget "❓ Boîte à quiz" (voir "🖥️ Interface") — réutilise le
|
||||
@@ -85,9 +91,9 @@ def render_scene_object(obj):
|
||||
# respectent le même contrat de data-quiz-role, lu EN JEU par
|
||||
# static/js/play/dialogue-box-controller.js. "defaut" (Classique)
|
||||
# reste pixel-identique au rendu d'avant cette extension.
|
||||
style = resolve_dialogue_box_style(obj)
|
||||
box_theme = resolve_dialogue_box_style(obj)
|
||||
config = resolve_quiz_box_config(obj)
|
||||
return render_quiz_box(obj, config, style)
|
||||
return render_quiz_box(obj, config, box_theme)
|
||||
if obj["kind"] == "score_widget":
|
||||
# Widget "🏆 Score" (voir "🖥️ Interface") — toujours VISIBLE une
|
||||
# fois posé (pas de collision/déclencheur, contrairement aux deux
|
||||
@@ -95,18 +101,18 @@ def render_scene_object(obj):
|
||||
# cours (voir static/js/play/dialogue-box-controller.js::
|
||||
# forgeUpdateAllScoreWidgets). Même style que les deux autres
|
||||
# widgets, header_bg sert de fond unique (pas de zone body/footer).
|
||||
style = resolve_dialogue_box_style(obj)
|
||||
box_theme = resolve_dialogue_box_style(obj)
|
||||
box_style = (
|
||||
f"position:absolute; left:{obj['x']}px; top:{obj['y']}px; "
|
||||
f"width:{obj['width']}px; height:{obj['height']}px; z-index:{obj['z_index']}; "
|
||||
f"font-family:{html_lib.escape(style['font_family'])}; font-size:{style['font_size']}px; "
|
||||
f"font-weight:{style['font_weight']}; color:{html_lib.escape(style['text_color'])}; "
|
||||
f"background:{html_lib.escape(style['header_bg'])};"
|
||||
f"font-family:{html_lib.escape(box_theme['font_family'])}; font-size:{box_theme['font_size']}px; "
|
||||
f"font-weight:{box_theme['font_weight']}; color:{html_lib.escape(box_theme['text_color'])}; "
|
||||
f"background:{html_lib.escape(box_theme['header_bg'])};"
|
||||
)
|
||||
return (
|
||||
f'<div class="scoreWidget" data-object-id="{obj["id"]}" data-element-id="{obj["id"]}" style="{box_style}">'
|
||||
f'🏆 <span data-score-role="value">0</span>'
|
||||
f'</div>'
|
||||
f"</div>"
|
||||
)
|
||||
if obj["kind"] == "personnage":
|
||||
src = resolve_personnage_idle_frame(obj)
|
||||
@@ -117,8 +123,10 @@ def render_scene_object(obj):
|
||||
# un dialogue de quête réutilise le même nom). alt="" (décoratif)
|
||||
# sinon, jamais le nom de fichier brut par défaut du navigateur.
|
||||
alt_attr = f' alt="{html_lib.escape(obj.get("name") or "")}"'
|
||||
return (f'<img class="sceneObjectSprite" data-object-id="{obj["id"]}" data-element-id="{obj["id"]}" '
|
||||
f'data-anim-target="{obj["id"]}" data-personnage="1" style="{style}"{src_attr}{alt_attr}>')
|
||||
return (
|
||||
f'<img class="sceneObjectSprite" data-object-id="{obj["id"]}" data-element-id="{obj["id"]}" '
|
||||
f'data-anim-target="{obj["id"]}" data-personnage="1" style="{style}"{src_attr}{alt_attr}>'
|
||||
)
|
||||
src = (obj.get("attributes") or {}).get("src", "")
|
||||
src_attr = f' src="{html_lib.escape(src)}"' if src else ""
|
||||
# "fond" (voir add_scene_object.py) : même image fixe que "decor",
|
||||
@@ -132,5 +140,7 @@ def render_scene_object(obj):
|
||||
# information à transmettre) — un "decor" nommé par l'auteur utilise
|
||||
# ce nom, sinon alt="" plutôt qu'aucun attribut du tout.
|
||||
alt_attr = ' alt=""' if obj["kind"] == "fond" else f' alt="{html_lib.escape(obj.get("name") or "")}"'
|
||||
return (f'<img class="{css_class}" data-object-id="{obj["id"]}" data-element-id="{obj["id"]}" '
|
||||
f'data-anim-target="{obj["id"]}" style="{style}"{src_attr}{alt_attr}>')
|
||||
return (
|
||||
f'<img class="{css_class}" data-object-id="{obj["id"]}" data-element-id="{obj["id"]}" '
|
||||
f'data-anim-target="{obj["id"]}" style="{style}"{src_attr}{alt_attr}>'
|
||||
)
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
def resolve_scene_world_size(screen, objects):
|
||||
from typing import Any
|
||||
|
||||
|
||||
def resolve_scene_world_size(screen: dict[str, Any], objects: list[dict[str, Any]]) -> tuple[int, int]:
|
||||
"""Taille du "monde" d'une scène 2D (voir static/js/play/personnage-
|
||||
controller.js::forgeUpdateSceneWorldSize, même principe côté serveur) :
|
||||
un "fond" posé à sa taille RÉELLE (souvent plus grande que la scène/
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import json
|
||||
from typing import Any
|
||||
|
||||
import db
|
||||
|
||||
from .ensure_scene_schema import ensure_scene_schema
|
||||
|
||||
|
||||
def set_scene_object_collision(slug, object_id, settings):
|
||||
def set_scene_object_collision(slug: str, object_id: int, settings: dict[str, Any]) -> None:
|
||||
"""Enregistre la boîte de collision (activée/désactivée, forme,
|
||||
taille, décalage — voir screens/rendering/collision_settings.py) d'un
|
||||
objet de scène QUELCONQUE (pas seulement "personnage" : un décor/fond
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import json
|
||||
from typing import Any
|
||||
|
||||
import db
|
||||
|
||||
from .ensure_scene_schema import ensure_scene_schema
|
||||
|
||||
|
||||
def set_scene_object_collision_rules(slug, object_id, rules):
|
||||
def set_scene_object_collision_rules(slug: str, object_id: int, rules: list[dict[str, Any]]) -> None:
|
||||
"""Enregistre la liste de règles "déclencheur -> action" d'un objet de
|
||||
scène (voir screens/rendering/collision_rules.py, "🧩 Collision" dans
|
||||
templates/scene_edit.html) — un attribut JSON distinct de tous les
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import json
|
||||
from typing import Any
|
||||
|
||||
import db
|
||||
|
||||
from .ensure_scene_schema import ensure_scene_schema
|
||||
|
||||
|
||||
def set_scene_object_commands(slug, object_id, commands):
|
||||
def set_scene_object_commands(slug: str, object_id: int, commands: dict[str, Any]) -> None:
|
||||
"""Enregistre les touches de déplacement/interaction et le blocage
|
||||
d'axe d'un objet de scène kind="personnage" (voir
|
||||
screens/rendering/personnage_commands.py pour les valeurs par défaut,
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import json
|
||||
from typing import Any
|
||||
|
||||
import db
|
||||
|
||||
@@ -6,7 +7,7 @@ from ..rendering.dialogue_box_style import sanitize_dialogue_box_style
|
||||
from .ensure_scene_schema import ensure_scene_schema
|
||||
|
||||
|
||||
def set_scene_object_dialogue_box_style(slug, object_id, raw_style):
|
||||
def set_scene_object_dialogue_box_style(slug: str, object_id: int, raw_style: Any) -> dict[str, Any] | None:
|
||||
ensure_scene_schema(slug)
|
||||
conn = db.connect(slug)
|
||||
row = conn.execute("SELECT attributes FROM _scene_objects WHERE id = ?", (object_id,)).fetchone()
|
||||
|
||||
@@ -3,7 +3,7 @@ import db
|
||||
from .ensure_scene_schema import ensure_scene_schema
|
||||
|
||||
|
||||
def set_scene_object_name(slug, object_id, name):
|
||||
def set_scene_object_name(slug: str, object_id: int, name: str | None) -> None:
|
||||
"""Enregistre le nom d'un objet de scène (colonne `name`, voir
|
||||
ensure_scene_schema.py — déjà utilisée comme LIBELLÉ d'affichage
|
||||
partout ailleurs, mais jamais éditable avant le panneau "ℹ️
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import json
|
||||
from typing import Any
|
||||
|
||||
import db
|
||||
|
||||
from .ensure_scene_schema import ensure_scene_schema
|
||||
|
||||
|
||||
def set_scene_object_personnage_data(slug, object_id, data):
|
||||
def set_scene_object_personnage_data(slug: str, object_id: int, data: dict[str, Any]) -> None:
|
||||
"""Change le personnage Forge d'un objet de scène kind="personnage",
|
||||
ou met à jour ses animations — mirror exact de
|
||||
screens/elements/set_personnage_data.py, même clé _personnage_data
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import json
|
||||
from typing import Any
|
||||
|
||||
import db
|
||||
|
||||
@@ -6,7 +7,7 @@ from ..rendering.quiz_box_config import sanitize_quiz_box_config
|
||||
from .ensure_scene_schema import ensure_scene_schema
|
||||
|
||||
|
||||
def set_scene_object_quiz_config(slug, object_id, raw_config):
|
||||
def set_scene_object_quiz_config(slug: str, object_id: int, raw_config: Any) -> dict[str, Any] | None:
|
||||
ensure_scene_schema(slug)
|
||||
conn = db.connect(slug)
|
||||
row = conn.execute("SELECT attributes FROM _scene_objects WHERE id = ?", (object_id,)).fetchone()
|
||||
|
||||
@@ -5,7 +5,7 @@ import db
|
||||
from .ensure_scene_schema import ensure_scene_schema
|
||||
|
||||
|
||||
def set_scene_object_role(slug, object_id, role):
|
||||
def set_scene_object_role(slug: str, object_id: int, role: str) -> None:
|
||||
"""Enregistre le rôle (joueur/ennemie/pnj, voir
|
||||
screens/rendering/personnage_role.py) d'un objet de scène
|
||||
kind="personnage" — un attribut DISTINCT de _personnage_data (choix du
|
||||
|
||||
@@ -3,7 +3,7 @@ import db
|
||||
from .ensure_scene_schema import ensure_scene_schema
|
||||
|
||||
|
||||
def update_scene_object_geometry(slug, object_id, x, y, width, height):
|
||||
def update_scene_object_geometry(slug: str, object_id: int, x: float, y: float, width: float, height: float) -> None:
|
||||
"""Position/taille en PIXELS (pas des %, voir ensure_scene_schema.py)
|
||||
— utilisé par le glisser-déposer/redimensionnement de l'éditeur de
|
||||
scène, mirror exact de update_element_geometry.py (éditeur document) :
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
from typing import Any
|
||||
|
||||
from ..rendering.collision_rules import set_then_action_in_rules
|
||||
from ..rendering.screen_triggers import resolve_screen_triggers, sanitize_screen_triggers
|
||||
from .get_screen import get_screen
|
||||
from .set_screen_triggers import set_screen_triggers
|
||||
from ..rendering.screen_triggers import resolve_screen_triggers, sanitize_screen_triggers
|
||||
from ..rendering.collision_rules import set_then_action_in_rules
|
||||
|
||||
|
||||
def append_action_to_screen_trigger(slug, screen_id, after_id, raw_action):
|
||||
def append_action_to_screen_trigger(slug: str, screen_id: int, after_id: str, raw_action: Any) -> bool:
|
||||
"""Équivalent de screens.append_action_to_trigger (voir scenes/
|
||||
append_action_to_trigger.py) pour un déclencheur D'ÉCRAN plutôt que
|
||||
d'un objet — même chemin d'écriture UNIQUE partagé par la route HTTP
|
||||
|
||||
@@ -3,7 +3,7 @@ import db
|
||||
from .ensure_schema import ensure_schema
|
||||
|
||||
|
||||
def create_screen(slug, name, is_template=False, kind="jeu_2d"):
|
||||
def create_screen(slug: str, name: str, is_template: bool = False, kind: str = "jeu_2d") -> int:
|
||||
ensure_schema(slug)
|
||||
conn = db.connect(slug)
|
||||
if is_template:
|
||||
@@ -15,7 +15,7 @@ def create_screen(slug, name, is_template=False, kind="jeu_2d"):
|
||||
"INSERT INTO _screens (name, order_index, is_template, kind) VALUES (?, ?, ?, ?)",
|
||||
(name, order_index, 1 if is_template else 0, kind),
|
||||
)
|
||||
screen_id = conn.execute("SELECT last_insert_rowid() AS id").fetchone()["id"]
|
||||
screen_id = int(conn.execute("SELECT last_insert_rowid() AS id").fetchone()["id"])
|
||||
conn.commit()
|
||||
conn.close()
|
||||
return screen_id
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
import db
|
||||
|
||||
|
||||
def delete_screen(slug, screen_id):
|
||||
def delete_screen(slug: str, screen_id: int) -> None:
|
||||
# Les conversations "Générer avec l'IA" de cet écran (et leurs messages)
|
||||
# sont déjà nettoyées automatiquement par SQLite : _ia_conversations.
|
||||
# screen_id REFERENCES _screens(id) ON DELETE CASCADE (voir
|
||||
# screens/ia/ensure_ia_chat_schema.py) — rien à faire ici explicitement.
|
||||
conn = db.connect(slug)
|
||||
conn.execute("DELETE FROM _screens WHERE id = ?", (screen_id,))
|
||||
conn.commit()
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import db
|
||||
|
||||
|
||||
def ensure_schema(slug):
|
||||
def ensure_schema(slug: str) -> None:
|
||||
"""Crée les tables de ce module si elles n'existent pas encore — les
|
||||
jeux créés avant l'ajout de cette fonctionnalité n'ont pas besoin d'être
|
||||
migrés manuellement, la première visite d'un écran de ce jeu suffit."""
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
from typing import Any
|
||||
|
||||
import db
|
||||
|
||||
|
||||
def get_screen(slug, screen_id):
|
||||
def get_screen(slug: str, screen_id: int) -> dict[str, Any] | None:
|
||||
conn = db.connect(slug)
|
||||
row = conn.execute("SELECT * FROM _screens WHERE id = ?", (screen_id,)).fetchone()
|
||||
conn.close()
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
from typing import Any
|
||||
|
||||
import db
|
||||
|
||||
from .ensure_schema import ensure_schema
|
||||
|
||||
|
||||
def list_screens(slug, include_templates=False):
|
||||
def list_screens(slug: str, include_templates: bool = False) -> list[dict[str, Any]]:
|
||||
"""Par défaut, n'affiche que les vrais écrans du jeu (pas les
|
||||
écrans-modèles qui hébergent le contenu des éléments de jeu réutilisables
|
||||
— voir ensure_schema) : c'est ce qu'on veut partout où on énumère "les
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
from ..rendering.collision_rules import move_action_in_rules
|
||||
from ..rendering.screen_triggers import resolve_screen_triggers, sanitize_screen_triggers
|
||||
from .get_screen import get_screen
|
||||
from .set_screen_triggers import set_screen_triggers
|
||||
from ..rendering.screen_triggers import resolve_screen_triggers, sanitize_screen_triggers
|
||||
from ..rendering.collision_rules import move_action_in_rules
|
||||
|
||||
|
||||
def move_action_in_screen_trigger(slug, screen_id, leaf_id, direction):
|
||||
def move_action_in_screen_trigger(slug: str, screen_id: int, leaf_id: str, direction: str) -> bool:
|
||||
"""Équivalent de screens.move_action_in_trigger pour un déclencheur
|
||||
D'ÉCRAN plutôt que d'un objet."""
|
||||
screen = get_screen(slug, screen_id)
|
||||
|
||||
@@ -3,7 +3,7 @@ import db
|
||||
from .list_screens import list_screens
|
||||
|
||||
|
||||
def move_screen(slug, screen_id, direction):
|
||||
def move_screen(slug: str, screen_id: int, direction: int) -> None:
|
||||
"""direction: -1 (monter, l'écran devient plus tôt dans l'enchaînement)
|
||||
ou +1 (descendre) — échange l'order_index avec l'écran voisin."""
|
||||
screens_ = list_screens(slug)
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
from ..rendering.collision_rules import remove_action_from_rules
|
||||
from ..rendering.screen_triggers import resolve_screen_triggers, sanitize_screen_triggers
|
||||
from .get_screen import get_screen
|
||||
from .set_screen_triggers import set_screen_triggers
|
||||
from ..rendering.screen_triggers import resolve_screen_triggers, sanitize_screen_triggers
|
||||
from ..rendering.collision_rules import remove_action_from_rules
|
||||
|
||||
|
||||
def remove_action_from_screen_trigger(slug, screen_id, leaf_id):
|
||||
def remove_action_from_screen_trigger(slug: str, screen_id: int, leaf_id: str) -> bool:
|
||||
"""Équivalent de screens.remove_action_from_trigger pour un
|
||||
déclencheur D'ÉCRAN plutôt que d'un objet — même chemin d'écriture
|
||||
UNIQUE partagé par la route HTTP et l'agent IA."""
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import db
|
||||
|
||||
|
||||
def rename_screen(slug, screen_id, new_name):
|
||||
def rename_screen(slug: str, screen_id: int, new_name: str) -> None:
|
||||
conn = db.connect(slug)
|
||||
conn.execute("UPDATE _screens SET name = ? WHERE id = ?", (new_name, screen_id))
|
||||
conn.commit()
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
from typing import Any
|
||||
|
||||
import db
|
||||
|
||||
from ..flow.ensure_flow_schema import ensure_flow_schema
|
||||
|
||||
|
||||
def screens_referencing(slug, screen_id):
|
||||
def screens_referencing(slug: str, screen_id: int) -> list[dict[str, Any]]:
|
||||
"""Écrans dont un nœud de logique (action "aller_a") cible précisément
|
||||
cet écran — pour avertir avant suppression plutôt que de casser
|
||||
silencieusement le lien."""
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import db
|
||||
|
||||
|
||||
def set_scene_size(slug, screen_id, width, height):
|
||||
def set_scene_size(slug: str, screen_id: int, width: int, height: int) -> None:
|
||||
"""Redimensionne la scène/caméra elle-même (colonnes scene_width/
|
||||
scene_height, voir ensure_schema.py) — jusqu'ici fixées à la création
|
||||
et jamais modifiables ensuite. Utilisé par "📐 Agrandir la caméra à la
|
||||
|
||||
@@ -3,7 +3,7 @@ import db
|
||||
from .ensure_schema import ensure_schema
|
||||
|
||||
|
||||
def set_screen_background_music(slug, screen_id, url):
|
||||
def set_screen_background_music(slug: str, screen_id: int, url: str | None) -> None:
|
||||
"""Mémorise l'URL de musique de fond de cet écran (voir le commentaire
|
||||
sur la colonne background_music_url dans ensure_schema.py). Chaîne vide
|
||||
-> NULL (pas de musique), comme un simple champ texte qu'on vide."""
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import json
|
||||
from typing import Any
|
||||
|
||||
import db
|
||||
|
||||
from .ensure_schema import ensure_schema
|
||||
|
||||
|
||||
def set_screen_triggers(slug, screen_id, rules):
|
||||
def set_screen_triggers(slug: str, screen_id: int, rules: list[dict[str, Any]]) -> None:
|
||||
"""Remplace TOUTES les règles "déclencheur d'écran -> action" de cet
|
||||
écran (voir screens/rendering/screen_triggers.py) — même patron que
|
||||
set_scene_object_collision_rules pour un objet, mais stocké
|
||||
|
||||
Reference in New Issue
Block a user