Co-MJ v1 : quêtes de première classe, guidage de préparation, assistant IA,
All checks were successful
Build & Push Images / build (brain) (push) Successful in 1m38s
Build & Push Images / build (web) (push) Successful in 1m53s
Build & Push Images / build (core) (push) Successful in 3m40s
Build & Push Images / build-switcher (push) Successful in 16s

mode séance, battlemaps multiples et export Foundry ciblé

- Quêtes (Niveau 1) : entité Quest orthogonale à l'arbre, rattachée à un arc HUB
  ou libre (migrations V9-V12, V18, V20 ; réconciliation des jumeaux V10).
  Fusion quête/conteneur dans la sidebar, progression par partie (statuts
  disponible/en cours/terminée), et quêtes libres avec espace de scènes créé
  automatiquement (arc technique « Quêtes libres », V21 : levée de la
  contrainte arcs.type héritée du baseline).
- Guidage (Pilier B) : bilan de préparation 100 % dérivé (règles arc/chapitre/
  scène/quête), panneau « Prochaines étapes » avec boutons « Corriger », et
  pastilles détaillées (tooltip des manques) dans l'arbre.
- Assistant IA (Pilier A) : étoffage champ par champ (scène, chapitre, arc) et
  brouillons de scènes en propose→applique (brain : narrative-fields,
  scene-drafts).
- Horloges & menaces (V15-V17) : clocks à segments avec déclencheurs, fronts.
- Mode séance : préparation de séance (readiness + quêtes dispo), scène
  épinglée (V19), récap « précédemment » (brain : session-recap), onglet
  « Partie » du panneau de référence, graphe amélioré (pan/zoom, éditeur de
  liens).
- Perf : endpoint agrégé GET /api/campaigns/{id}/tree — la sidebar charge en
  1 requête au lieu de ~15.
- Battlemaps multiples par scène (variantes jour/nuit, étages…) : liste JSON
  étiquetée (V22, reprise automatique de la carte existante), rendu PDF avec
  légendes, export/import rétro-compatible.
- Export Foundry ciblé : modale de périmètre (cartes+ennemis / journaux /
  tables), bundle filtré côté serveur (zip allégé), module Foundry à jour
  (respect du périmètre + une Scene Foundry par variante de carte,
  rétro-compatible anciens bundles).
This commit is contained in:
2026-07-03 15:29:23 +02:00
parent 268f4721ce
commit b2c3800bf8
278 changed files with 15570 additions and 1451 deletions

View File

@@ -0,0 +1,50 @@
"""Prompt d'étoffage des champs d'une entité narrative (arc / chapitre / scène) — Pilier A.
Générique : le Core (Java) est la SOURCE DE VÉRITÉ des champs (clé + libellé) et les
passe en entrée ; ce module ne fait que formuler le prompt. Le modèle ne renvoie que les
clés fournies et OMET celles pour lesquelles il n'a rien de pertinent (pas de remplissage forcé).
"""
from app.core.language import language_name
# Étiquette lisible du type d'entité, pour la formulation du prompt.
ENTITY_LABEL: dict[str, str] = {
"arc": "cet arc narratif",
"chapter": "ce chapitre",
"scene": "cette scène",
}
def narrative_fields_prompt(entity_type: str, entity_context: str, instruction: str,
fields: list[dict], language: str) -> str:
"""Construit le prompt d'étoffage. `fields` = [{key, label}] (whitelist du Core)."""
label = ENTITY_LABEL.get(entity_type or "", "cette entité narrative")
lines = []
for f in fields or []:
key = str(f.get("key") or "").strip()
if not key:
continue
flabel = str(f.get("label") or key).strip()
lines.append(f'- "{key}" : {flabel}')
fields_list = "\n".join(lines)
instruction_block = (
f"\nConsigne particulière du MJ : {instruction.strip()}\n"
if instruction and instruction.strip() else ""
)
return (
f"Tu es un co-Maître de Jeu. On te donne l'état ACTUEL d'{label} de jeu de rôle. "
"Propose des valeurs pour l'ÉTOFFER, cohérentes avec ce qui existe déjà.\n\n"
f"{entity_context.strip()}\n"
f"{instruction_block}\n"
"Champs que tu peux remplir (n'utilise QUE ces clés) :\n"
f"{fields_list}\n\n"
"Règles IMPÉRATIVES :\n"
"- Réponds UNIQUEMENT par un objet JSON valide, sans texte autour.\n"
'- Format exact : {"fields": {"cle": "valeur proposée", ...}}\n'
"- N'inclus QUE des clés de la liste ci-dessus. N'invente AUCUNE autre clé.\n"
"- Si un champ est déjà bien rempli ou si tu n'as rien de pertinent, OMETS-le "
"(ne le renvoie pas) plutôt que de le remplir de force.\n"
"- Reste cohérent avec le contexte : n'invente pas d'élément qui contredit "
"l'entité ou la campagne.\n"
f"- Rédige les valeurs en {language_name(language)}.\n"
"Renvoie maintenant le JSON."
)

View File

@@ -0,0 +1,30 @@
"""Prompt d'ébauche de scènes pour un chapitre (Pilier A — capacité « create »).
Le co-MJ propose plusieurs scènes cohérentes pour PEUPLER un chapitre vide (ou en manque).
JSON structuré, une liste de scènes ; l'humain révise et ne crée que celles qu'il retient.
"""
from app.core.language import language_name
def scene_drafts_prompt(context: str, instruction: str, count: int, language: str) -> str:
instruction_block = (
f"\nConsigne particulière du MJ : {instruction.strip()}\n"
if instruction and instruction.strip() else ""
)
return (
f"Tu es un co-Maître de Jeu. Propose {count} SCÈNES de jeu de rôle pour PEUPLER ce "
"chapitre, cohérentes entre elles et avec le contexte.\n\n"
f"{context.strip()}\n"
f"{instruction_block}\n"
"Règles IMPÉRATIVES :\n"
"- Réponds UNIQUEMENT par un objet JSON valide, sans texte autour.\n"
'- Format exact : {"scenes": [{"name": "...", "description": "...", "playerNarration": "..."}]}\n'
f"- Propose AU PLUS {count} scènes, distinctes et complémentaires (une progression du chapitre).\n"
"- 'name' : titre court et évocateur (OBLIGATOIRE).\n"
"- 'description' : un résumé bref (une phrase).\n"
"- 'playerNarration' : 2-3 phrases de mise en scène lues aux joueurs.\n"
"- Ne DUPLIQUE pas les scènes déjà présentes ; reste cohérent avec le chapitre et la campagne "
"(n'invente pas d'élément qui les contredit).\n"
f"- Rédige en {language_name(language)}.\n"
"Renvoie maintenant le JSON."
)

View File

@@ -0,0 +1,25 @@
"""Prompt du récap « précédemment dans… » (mode séance).
Le Core envoie le journal chronologique de la séance PRÉCÉDENTE ; le modèle rédige un
récapitulatif court, à lire aux joueurs à l'ouverture de la séance suivante. Texte libre
(pas de JSON) : c'est de la narration.
"""
from app.core.language import language_name
def session_recap_prompt(transcript: str, context: str, language: str) -> str:
context_block = f"\n{context.strip()}\n" if context and context.strip() else ""
return (
"Tu es le Maître du Jeu. Voici le journal de la SÉANCE PRÉCÉDENTE de ta table "
"(entrées chronologiques : notes, évènements, jets de dés, actions des joueurs).\n"
f"{context_block}\n"
"Journal :\n"
f"{transcript.strip()}\n\n"
"Rédige un récapitulatif « Précédemment… » à LIRE AUX JOUEURS pour ouvrir la "
"nouvelle séance :\n"
"- 4 à 8 phrases, ton narratif et vivant, au passé.\n"
"- Uniquement ce qui s'est réellement passé dans le journal — n'invente RIEN, "
"ne révèle aucun secret du MJ.\n"
"- Termine sur la situation où les joueurs se sont arrêtés (le « cliffhanger »).\n"
f"- Rédige en {language_name(language)}. Pas de préambule ni de méta : juste le récit."
)