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

@@ -42,6 +42,7 @@ export class CampaignSidebarService {
return forkJoin({
campaign: this.campaignService.getCampaignById(campaignId),
allCampaigns: this.campaignService.getAllCampaigns(),
// L'arbre agrégé embarque déjà quêtes + readiness (pastilles) en une requête.
treeData: loadCampaignTreeData(
this.campaignService,
campaignId,
@@ -50,7 +51,9 @@ export class CampaignSidebarService {
this.enemyService
)
}).subscribe(({ campaign, allCampaigns, treeData }) => {
this.layoutService.show(buildCampaignSidebarConfig(campaign, allCampaigns, treeData, campaignId, this.translate));
this.layoutService.show(
buildCampaignSidebarConfig(campaign, allCampaigns, treeData, campaignId, this.translate)
);
});
}
}

View File

@@ -21,8 +21,11 @@ export interface CampaignCreate {
gameSystemId?: string | null;
}
/** Type structurel d'un Arc (miroir de l'enum Java ArcType). */
export type ArcType = 'LINEAR' | 'HUB';
/**
* Type structurel d'un Arc (miroir de l'enum Java ArcType).
* SYSTEM = arc technique « Quêtes libres » (conteneurs des quêtes hors arc), masqué de la narration.
*/
export type ArcType = 'LINEAR' | 'HUB' | 'SYSTEM';
/** Statut de progression piloté manuellement par le MJ (persisté). */
export type ProgressionStatus = 'NOT_STARTED' | 'IN_PROGRESS' | 'COMPLETED';
@@ -89,6 +92,65 @@ export interface ArcCreate {
illustrationImageIds?: string[];
}
/** Type de nœud narratif référencé par une quête (miroir Java NodeType). */
export type NodeType = 'CHAPTER' | 'SCENE';
/** Lien faible d'une quête vers un nœud narratif (miroir Java QuestNodeRef). */
export interface QuestNodeRef {
nodeType: NodeType;
nodeId: string;
order: number;
}
/**
* Quête (Niveau 1) — entité de première classe, ORTHOGONALE à l'arbre
* Arc→Chapitre→Scène, rattachée à la campagne. Porte ses prérequis (réutilise
* l'union Prerequisite) et référence des nœuds via QuestNodeRef.
*/
export interface Quest {
id?: string;
campaignId: string;
/** Arc de rattachement (nullable). Non nul ⇒ quête d'un arc HUB ; null ⇒ transverse. */
arcId?: string | null;
name: string;
description?: string;
icon?: string | null;
order?: number;
/** Conditions de déblocage (ET logique). Donnée de SCÉNARIO. */
prerequisites?: Prerequisite[];
/** Nœuds narratifs (Chapitres / Scènes) traversés par la quête. */
nodes?: QuestNodeRef[];
/** Read-only — peuplé par le backend quand un playthroughId est passé. */
progressionStatus?: ProgressionStatus;
effectiveStatus?: QuestStatus;
gmNotes?: string;
playerObjectives?: string;
narrativeStakes?: string;
relatedPageIds?: string[];
illustrationImageIds?: string[];
}
export interface QuestCreate {
name: string;
/** Arc de rattachement (nullable). Renseigné quand la quête est créée depuis un arc HUB. */
arcId?: string | null;
description?: string;
icon?: string | null;
order?: number;
prerequisites?: Prerequisite[];
nodes?: QuestNodeRef[];
gmNotes?: string;
playerObjectives?: string;
narrativeStakes?: string;
relatedPageIds?: string[];
illustrationImageIds?: string[];
}
export interface Chapter {
id?: string;
name: string;
@@ -97,20 +159,6 @@ export interface Chapter {
order?: number;
icon?: string | null;
/** Conditions de déblocage (ET logique). Donnée de SCÉNARIO. */
prerequisites?: Prerequisite[];
/**
* Statut de progression read-only — peuplé par le backend uniquement quand
* un playthroughId est passé en query param. Ne pas inclure en écriture.
*/
progressionStatus?: ProgressionStatus;
/**
* Statut effectif read-only — idem, dépend du Playthrough.
*/
effectiveStatus?: QuestStatus;
// Champs narratifs enrichis
gmNotes?: string;
playerObjectives?: string;
@@ -127,8 +175,6 @@ export interface ChapterCreate {
order: number;
icon?: string | null;
prerequisites?: Prerequisite[];
gmNotes?: string;
playerObjectives?: string;
narrativeStakes?: string;
@@ -166,6 +212,12 @@ export interface PlaythroughFlag {
value: boolean;
}
/** Type narratif d'un nœud (Scène) — Niveau 2. Miroir de l'enum Java SceneType. */
export type SceneType = 'GENERIC' | 'LOCATION' | 'ENCOUNTER' | 'NPC' | 'EVENT' | 'REVELATION';
/** Type d'un lien narratif entre scènes — Niveau 2. Miroir de l'enum Java LinkType. */
export type LinkType = 'EXIT' | 'CLUE' | 'LEAD';
/**
* Branche narrative : sortie possible d'une scène vers une autre du même chapitre.
* Pendant TS du Value Object Java SceneBranch.
@@ -174,6 +226,21 @@ export interface SceneBranch {
label: string;
targetSceneId: string;
condition?: string;
/** Type de lien (Niveau 2). Absent => EXIT côté backend. */
kind?: LinkType;
}
/**
* Battlemap étiquetée d'une scène (export Foundry) : variante Jour/Nuit, étage…
* Pendant TS du record domaine SceneBattlemap.
*/
export interface SceneBattlemap {
/** Libellé libre de la variante (ex : "Jour", "Nuit"). Peut être vide. */
label: string;
/** ID du fichier media (image/video). Null = carte sans fond. */
mediaFileId: string | null;
/** ID du fichier sidecar Universal VTT (.json/.dd2vtt). Null si absent. */
dataFileId: string | null;
}
/**
@@ -217,6 +284,9 @@ export interface Scene {
order?: number;
icon?: string | null;
/** Type narratif du nœud (Niveau 2). Absent => GENERIC. */
type?: SceneType;
// Champs narratifs enrichis
location?: string;
timing?: string;
@@ -234,11 +304,15 @@ export interface Scene {
illustrationImageIds?: string[];
/**
* Battlemap Foundry : ID du fichier media (image/video) + ID du sidecar JSON
* Universal VTT. Non affichee dans l'appli ; transportee a l'export Foundry.
* Battlemaps Foundry : variantes étiquetées (Jour/Nuit, étages…), chacune =
* media (image/video) + sidecar JSON Universal VTT. Non affichées dans
* l'appli ; transportées à l'export Foundry.
*/
battlemapMediaFileId?: string | null;
battlemapDataFileId?: string | null;
battlemaps?: SceneBattlemap[];
/** Position du nœud dans la vue graphe du chapitre (Niveau 2). Absent => layout auto. */
graphX?: number;
graphY?: number;
/** Sorties narratives (graphe intra-chapitre). */
branches?: SceneBranch[];
@@ -254,6 +328,9 @@ export interface SceneCreate {
order: number;
icon?: string | null;
/** Type narratif du nœud (Niveau 2). */
type?: SceneType;
location?: string;
timing?: string;
atmosphere?: string;
@@ -268,8 +345,9 @@ export interface SceneCreate {
relatedPageIds?: string[];
illustrationImageIds?: string[];
battlemapMediaFileId?: string | null;
battlemapDataFileId?: string | null;
battlemaps?: SceneBattlemap[];
graphX?: number;
graphY?: number;
branches?: SceneBranch[];
rooms?: Room[];
}

View File

@@ -1,7 +1,38 @@
import { Injectable } from '@angular/core';
import { HttpClient, HttpParams } from '@angular/common/http';
import { Observable } from 'rxjs';
import { Campaign, CampaignCreate, Arc, ArcCreate, Chapter, ChapterCreate, Scene, SceneCreate } from './campaign.model';
import { Campaign, CampaignCreate, Arc, ArcCreate, Chapter, ChapterCreate, Scene, SceneCreate, Quest } from './campaign.model';
import { CampaignReadinessAssessment } from './readiness.model';
import { Npc } from './npc.model';
import { RandomTable } from './random-table.model';
import { Enemy } from './enemy.model';
/**
* Arbre de campagne AGRÉGÉ (une seule requête HTTP) — miroir de CampaignTreeDTO.
* Remplace la rafale d'appels (~15-20) que la sidebar déclenchait à chaque navigation.
*/
export interface CampaignTreeResponse {
arcs: Arc[];
chaptersByArc: Record<string, Chapter[]>;
scenesByChapter: Record<string, Scene[]>;
npcs: Npc[];
randomTables: RandomTable[];
enemies: Enemy[];
quests: Quest[];
readiness: CampaignReadinessAssessment;
}
/**
* Périmètre de l'export Foundry (modale) :
* - maps : Scenes Foundry (battlemaps) + acteurs/tokens des ennemis liés.
* - journals : journaux narratifs (arcs, chapitres, scènes, PNJ, bestiaire).
* - tables : tables aléatoires (RollTables).
*/
export interface FoundryExportOptions {
maps: boolean;
journals: boolean;
tables: boolean;
}
/** Compte des entités qui seront supprimées en cascade avec la campagne. */
export interface CampaignDeletionImpact {
@@ -42,9 +73,29 @@ export class CampaignService {
return this.http.get<Campaign>(`${this.apiUrl}/${id}`);
}
/** Télécharge le bundle d'export Foundry de la campagne (.zip). */
exportFoundry(id: string): Observable<Blob> {
return this.http.get(`${this.apiUrl}/${id}/foundry-export`, { responseType: 'blob' });
/** Bilan de préparation (Pilier B) — alimente les pastilles de l'arbre de la sidebar. */
getReadiness(id: string): Observable<CampaignReadinessAssessment> {
return this.http.get<CampaignReadinessAssessment>(`${this.apiUrl}/${id}/readiness`);
}
/** Arbre complet de la campagne en UNE requête (sidebar : structure + quêtes + readiness). */
getTree(id: string): Observable<CampaignTreeResponse> {
return this.http.get<CampaignTreeResponse>(`${this.apiUrl}/${id}/tree`);
}
/**
* Télécharge le bundle d'export Foundry de la campagne (.zip).
* Périmètre optionnel (tout par défaut) : cartes+ennemis / journaux / tables.
*/
exportFoundry(id: string, opts?: FoundryExportOptions): Observable<Blob> {
let params = new HttpParams();
if (opts) {
params = params
.set('maps', String(opts.maps))
.set('journals', String(opts.journals))
.set('tables', String(opts.tables));
}
return this.http.get(`${this.apiUrl}/${id}/foundry-export`, { responseType: 'blob', params });
}
/** Génère et télécharge le livret PDF de la campagne. */
@@ -100,20 +151,14 @@ export class CampaignService {
}
// ========== CHAPTER ==========
/**
* Liste les chapitres d'un arc. Si {@code playthroughId} est fourni, le backend
* enrichit les DTOs avec progressionStatus + effectiveStatus relatifs à la Partie.
*/
getChapters(arcId: string, playthroughId?: string): Observable<Chapter[]> {
let params = new HttpParams().set('arcId', arcId);
if (playthroughId) params = params.set('playthroughId', playthroughId);
/** Liste les chapitres d'un arc (donnée de scénario pure). */
getChapters(arcId: string): Observable<Chapter[]> {
const params = new HttpParams().set('arcId', arcId);
return this.http.get<Chapter[]>('/api/chapters', { params });
}
getChapterById(id: string, playthroughId?: string): Observable<Chapter> {
let params = new HttpParams();
if (playthroughId) params = params.set('playthroughId', playthroughId);
return this.http.get<Chapter>(`/api/chapters/${id}`, { params });
getChapterById(id: string): Observable<Chapter> {
return this.http.get<Chapter>(`/api/chapters/${id}`);
}
createChapter(payload: ChapterCreate): Observable<Chapter> {

View File

@@ -0,0 +1,32 @@
/** Déclencheur d'avancement auto d'une horloge (co-MJ). Miroir de l'enum Java ClockTrigger. */
export type ClockTrigger = 'NONE' | 'FLAG_SET' | 'QUEST_COMPLETED' | 'SESSION_ENDED';
/**
* Horloge de progression (Clock) — état dynamique d'une Partie. Miroir du domaine Java.
*/
export interface Clock {
id: string;
playthroughId: string;
name: string;
description?: string;
segments: number;
filled: number;
order: number;
/** Déclencheur auto (co-MJ). Absent => NONE. */
triggerType?: ClockTrigger;
/** Cible : nom du fait (FLAG_SET) ou id de quête (QUEST_COMPLETED). */
triggerRef?: string;
/** Front (menace) auquel l'horloge appartient, ou undefined (libre). */
frontId?: string;
/** Read-only : filled >= segments. */
complete: boolean;
}
export interface ClockCreate {
name: string;
description?: string;
segments: number;
triggerType?: ClockTrigger;
triggerRef?: string;
frontId?: string;
}

View File

@@ -0,0 +1,42 @@
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';
import { Clock, ClockCreate } from './clock.model';
/**
* Service HTTP pour les Horloges de progression (Clocks) d'une Partie (Play Context).
* API imbriquée sous le Playthrough : /api/playthroughs/{playthroughId}/clocks.
*/
@Injectable({ providedIn: 'root' })
export class ClockService {
constructor(private http: HttpClient) {}
private base(playthroughId: string): string {
return `/api/playthroughs/${playthroughId}/clocks`;
}
list(playthroughId: string): Observable<Clock[]> {
return this.http.get<Clock[]>(this.base(playthroughId));
}
create(playthroughId: string, payload: ClockCreate): Observable<Clock> {
return this.http.post<Clock>(this.base(playthroughId), payload);
}
update(playthroughId: string, clockId: string, payload: ClockCreate): Observable<Clock> {
return this.http.put<Clock>(`${this.base(playthroughId)}/${clockId}`, payload);
}
advance(playthroughId: string, clockId: string): Observable<Clock> {
return this.http.put<Clock>(`${this.base(playthroughId)}/${clockId}/advance`, {});
}
regress(playthroughId: string, clockId: string): Observable<Clock> {
return this.http.put<Clock>(`${this.base(playthroughId)}/${clockId}/regress`, {});
}
delete(playthroughId: string, clockId: string): Observable<void> {
return this.http.delete<void>(`${this.base(playthroughId)}/${clockId}`);
}
}

View File

@@ -0,0 +1,18 @@
/**
* Modèles du Pilier A (co-création « propose → applique »), génériques par type
* d'entité narrative (arc / chapitre / scène). Miroir des records domaine.
*/
/** Proposition IA pour un champ (clé alignée sur les contrôles du formulaire de l'entité). */
export interface FieldProposal {
key: string;
currentValue: string;
proposedValue: string;
}
export interface EntityFieldPatchProposal {
target: string;
targetId: string;
type: string;
fields: FieldProposal[];
}

View File

@@ -0,0 +1,27 @@
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';
import { EntityFieldPatchProposal } from './entity-assist.model';
/**
* Service HTTP du Pilier A (co-création), générique par type d'entité narrative
* ({@code arc|chapter|scene}). One-shot (pas de SSE). `generate` propose (non persisté) ;
* `apply` patche l'entité persistée (primitif pour les flux hors-éditeur — l'éditeur, lui,
* patche le formulaire).
*/
@Injectable({ providedIn: 'root' })
export class EntityAssistService {
constructor(private http: HttpClient) {}
generateFields(entityType: string, entityId: string, campaignId: string, instruction?: string): Observable<EntityFieldPatchProposal> {
return this.http.post<EntityFieldPatchProposal>(
`/api/assist/${entityType}/${entityId}/generate`,
{ campaignId, instruction: instruction ?? '' }
);
}
applyFields(entityType: string, entityId: string, proposal: EntityFieldPatchProposal): Observable<unknown> {
return this.http.post(`/api/assist/${entityType}/${entityId}/apply`, proposal);
}
}

View File

@@ -0,0 +1,15 @@
/**
* Front (menace regroupant des horloges) d'une Partie. Miroir du domaine Java.
*/
export interface Front {
id: string;
playthroughId: string;
name: string;
description?: string;
order: number;
}
export interface FrontCreate {
name: string;
description?: string;
}

View File

@@ -0,0 +1,34 @@
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';
import { Front, FrontCreate } from './front.model';
/**
* Service HTTP pour les Fronts (menaces regroupant des horloges) d'une Partie.
* API imbriquée sous le Playthrough : /api/playthroughs/{playthroughId}/fronts.
*/
@Injectable({ providedIn: 'root' })
export class FrontService {
constructor(private http: HttpClient) {}
private base(playthroughId: string): string {
return `/api/playthroughs/${playthroughId}/fronts`;
}
list(playthroughId: string): Observable<Front[]> {
return this.http.get<Front[]>(this.base(playthroughId));
}
create(playthroughId: string, payload: FrontCreate): Observable<Front> {
return this.http.post<Front>(this.base(playthroughId), payload);
}
update(playthroughId: string, frontId: string, payload: FrontCreate): Observable<Front> {
return this.http.put<Front>(`${this.base(playthroughId)}/${frontId}`, payload);
}
delete(playthroughId: string, frontId: string): Observable<void> {
return this.http.delete<void>(`${this.base(playthroughId)}/${frontId}`);
}
}

View File

@@ -28,6 +28,14 @@ export interface TreeItem {
dropParentId?: string | null;
/** Petit badge affiché à droite (ex: "3" pour compter les pages d'un dossier). */
meta?: string;
/**
* Pastille de readiness (Pilier B — guidage) : 'blocking' (rouge) ou
* 'recommended' (orange), agrégée depuis les manques de l'entité ET de ses
* descendants. Absente = rien à signaler. Alimentée par buildCampaignTree.
*/
statusDot?: 'blocking' | 'recommended';
/** Tooltip de la pastille : le(s) message(s) réel(s) du manque (multi-lignes). */
statusDotTitle?: string;
/**
* Libellé de section affiché AU-DESSUS du nœud, avec un filet de séparation.
* Utilisé pour grouper visuellement des nœuds racines (ex: "Personnages" vs "Narration").
@@ -46,6 +54,8 @@ export interface TreeCreateAction {
id: string;
label: string; // tooltip au hover, texte complet en empty-state
route: string;
/** Query params optionnels (ex: { arcId } pour créer une quête rattachée à un arc HUB). */
queryParams?: Record<string, string>;
/** Cle d'icone cote sidebar (plus | folder-plus | file-plus). */
actionIcon?: 'plus' | 'folder-plus' | 'file-plus';
}

View File

@@ -0,0 +1,55 @@
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';
import { Quest, QuestCreate } from './campaign.model';
/**
* Service HTTP pour les Quêtes (Niveau 1). API nestée sous la campagne :
* /api/campaigns/{campaignId}/quests. Si {@code playthroughId} est fourni, les
* quêtes renvoyées sont enrichies de leur progressionStatus / effectiveStatus.
*/
@Injectable({ providedIn: 'root' })
export class QuestService {
constructor(private http: HttpClient) {}
private base(campaignId: string): string {
return `/api/campaigns/${campaignId}/quests`;
}
getByCampaign(campaignId: string, playthroughId?: string): Observable<Quest[]> {
const options = playthroughId ? { params: { playthroughId } } : {};
return this.http.get<Quest[]>(this.base(campaignId), options);
}
getById(campaignId: string, questId: string, playthroughId?: string): Observable<Quest> {
const options = playthroughId ? { params: { playthroughId } } : {};
return this.http.get<Quest>(`${this.base(campaignId)}/${questId}`, options);
}
create(campaignId: string, payload: QuestCreate): Observable<Quest> {
return this.http.post<Quest>(this.base(campaignId), payload);
}
update(campaignId: string, questId: string, payload: Quest): Observable<Quest> {
return this.http.put<Quest>(`${this.base(campaignId)}/${questId}`, payload);
}
delete(campaignId: string, questId: string): Observable<void> {
return this.http.delete<void>(`${this.base(campaignId)}/${questId}`);
}
reorder(campaignId: string, orderedIds: string[]): Observable<void> {
return this.http.put<void>(`${this.base(campaignId)}/reorder`, { orderedIds });
}
/**
* Progression d'une quête DANS UNE PARTIE (Play Context) : NOT_STARTED efface la
* ligne (modèle « absence = non commencée »), IN_PROGRESS / COMPLETED la posent.
* C'est ce qui pilote le statut effectif (Disponible / En cours / Terminée).
*/
setProgression(playthroughId: string, questId: string,
status: 'NOT_STARTED' | 'IN_PROGRESS' | 'COMPLETED'): Observable<void> {
return this.http.put<void>(`/api/playthroughs/${playthroughId}/quest-progressions/${questId}`, { status });
}
}

View File

@@ -0,0 +1,30 @@
/**
* Modèles du Pilier B (« guidage / readiness ») : bilan de préparation d'une
* campagne renvoyé par GET /api/campaigns/{id}/readiness.
*/
export type ReadinessStatus = 'DRAFT' | 'PLAYABLE' | 'POLISHED';
export type ReadinessSeverity = 'BLOCKING' | 'RECOMMENDED' | 'OPTIONAL';
export type ReadinessEntityType =
| 'CAMPAIGN' | 'ARC' | 'CHAPTER' | 'SCENE' | 'QUEST' | 'NPC' | 'ENEMY';
/** Un manque de préparation détecté, cliquable vers l'éditeur concerné. */
export interface ReadinessGap {
entityType: ReadinessEntityType;
entityId: string;
entityName?: string | null;
ruleId: string;
message: string;
severity: ReadinessSeverity;
/** Contexte de navigation (le front construit le lien profond). */
arcId?: string | null;
chapterId?: string | null;
}
export interface CampaignReadinessAssessment {
campaignId: string;
overallStatus: ReadinessStatus;
/** Nombre de gaps par sévérité : { BLOCKING, RECOMMENDED, OPTIONAL }. */
counts: Record<string, number>;
gaps: ReadinessGap[];
}

View File

@@ -0,0 +1,18 @@
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';
import { CampaignReadinessAssessment } from './readiness.model';
/**
* Service HTTP du Pilier B (« guidage / readiness ») : récupère le bilan de
* préparation d'une campagne. Read-model pur, déterministe, sans effet de bord.
*/
@Injectable({ providedIn: 'root' })
export class ReadinessService {
constructor(private http: HttpClient) {}
getReadiness(campaignId: string): Observable<CampaignReadinessAssessment> {
return this.http.get<CampaignReadinessAssessment>(`/api/campaigns/${campaignId}/readiness`);
}
}

View File

@@ -0,0 +1,15 @@
/**
* Modèles du Pilier A — capacité « create » : ébauches de scènes pour un chapitre.
* Miroir des records domaine SceneDraft / SceneDraftProposal.
*/
export interface SceneDraft {
name: string;
description?: string;
playerNarration?: string;
}
export interface SceneDraftProposal {
chapterId: string;
scenes: SceneDraft[];
}

View File

@@ -0,0 +1,26 @@
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';
import { SceneDraftProposal } from './scene-draft.model';
import { Scene } from './campaign.model';
/**
* Service HTTP du Pilier A — capacité « create » : peupler un chapitre en scènes.
* `generate` propose (non persisté) ; `apply` CRÉE les scènes retenues dans le chapitre.
*/
@Injectable({ providedIn: 'root' })
export class SceneDraftService {
constructor(private http: HttpClient) {}
generate(chapterId: string, campaignId: string, instruction: string, count: number): Observable<SceneDraftProposal> {
return this.http.post<SceneDraftProposal>(
`/api/chapters/${chapterId}/draft-scenes/generate`,
{ campaignId, instruction: instruction ?? '', count }
);
}
apply(chapterId: string, proposal: SceneDraftProposal): Observable<Scene[]> {
return this.http.post<Scene[]>(`/api/chapters/${chapterId}/draft-scenes/apply`, proposal);
}
}

View File

@@ -0,0 +1,48 @@
import { ReadinessGap } from './readiness.model';
/**
* Modèles « Préparer la prochaine séance » (Phase 3 co-MJ) — miroir de SessionPrepReport.
*/
export interface PrepLastSession {
id: string;
name: string;
startedAt?: string | null;
endedAt?: string | null;
active: boolean;
}
export interface PrepQuest {
id: string;
name: string;
icon?: string | null;
}
/** Chapitre / scène probable, avec le contexte pour le lien profond. */
export interface PrepNode {
nodeType: 'CHAPTER' | 'SCENE';
id: string;
name: string;
arcId?: string | null;
chapterId?: string | null;
}
export interface PrepClock {
id: string;
name: string;
segments: number;
filled: number;
frontName?: string | null;
}
export interface SessionPrepReport {
playthroughId: string;
lastSession?: PrepLastSession | null;
questsInProgress: PrepQuest[];
questsAvailable: PrepQuest[];
questsCompleted: PrepQuest[];
hotspots: PrepNode[];
gaps: ReadinessGap[];
otherGapCount: number;
clocks: PrepClock[];
}

View File

@@ -0,0 +1,18 @@
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';
import { SessionPrepReport } from './session-prep.model';
/**
* Service HTTP « Préparer la prochaine séance » (Phase 3 co-MJ). Read-model pur :
* position des joueurs + contenu probable + manques ciblés + horloges en mouvement.
*/
@Injectable({ providedIn: 'root' })
export class SessionPrepService {
constructor(private http: HttpClient) {}
getPrep(playthroughId: string): Observable<SessionPrepReport> {
return this.http.get<SessionPrepReport>(`/api/playthroughs/${playthroughId}/session-prep`);
}
}

View File

@@ -9,6 +9,8 @@ export interface Session {
startedAt: string;
/** Null/undefined = session en cours. */
endedAt: string | null;
/** Scène courante épinglée (mode cockpit) ; null = rien d'épinglé. */
currentSceneId?: string | null;
createdAt: string;
updatedAt: string;
active: boolean;

View File

@@ -59,4 +59,14 @@ export class SessionService {
deleteSession(id: string): Observable<void> {
return this.http.delete<void>(`${this.apiUrl}/${id}`);
}
/** Épingle (sceneId) ou dés-épingle (null) la scène courante — mode cockpit. */
setCurrentScene(id: string, sceneId: string | null): Observable<Session> {
return this.http.put<Session>(`${this.apiUrl}/${id}/current-scene`, { sceneId });
}
/** Récap « précédemment… » : résume le journal de la séance précédente de la Partie. */
recap(id: string): Observable<{ previousSessionName: string; recap: string }> {
return this.http.post<{ previousSessionName: string; recap: string }>(`${this.apiUrl}/${id}/recap`, {});
}
}