Co-MJ v1 : quêtes de première classe, guidage de préparation, assistant IA,
All checks were successful
All checks were successful
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:
@@ -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)
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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[];
|
||||
}
|
||||
|
||||
@@ -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> {
|
||||
|
||||
32
web/src/app/services/clock.model.ts
Normal file
32
web/src/app/services/clock.model.ts
Normal 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;
|
||||
}
|
||||
42
web/src/app/services/clock.service.ts
Normal file
42
web/src/app/services/clock.service.ts
Normal 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}`);
|
||||
}
|
||||
}
|
||||
18
web/src/app/services/entity-assist.model.ts
Normal file
18
web/src/app/services/entity-assist.model.ts
Normal 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[];
|
||||
}
|
||||
27
web/src/app/services/entity-assist.service.ts
Normal file
27
web/src/app/services/entity-assist.service.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
15
web/src/app/services/front.model.ts
Normal file
15
web/src/app/services/front.model.ts
Normal 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;
|
||||
}
|
||||
34
web/src/app/services/front.service.ts
Normal file
34
web/src/app/services/front.service.ts
Normal 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}`);
|
||||
}
|
||||
}
|
||||
@@ -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';
|
||||
}
|
||||
|
||||
55
web/src/app/services/quest.service.ts
Normal file
55
web/src/app/services/quest.service.ts
Normal 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 });
|
||||
}
|
||||
}
|
||||
30
web/src/app/services/readiness.model.ts
Normal file
30
web/src/app/services/readiness.model.ts
Normal 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[];
|
||||
}
|
||||
18
web/src/app/services/readiness.service.ts
Normal file
18
web/src/app/services/readiness.service.ts
Normal 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`);
|
||||
}
|
||||
}
|
||||
15
web/src/app/services/scene-draft.model.ts
Normal file
15
web/src/app/services/scene-draft.model.ts
Normal 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[];
|
||||
}
|
||||
26
web/src/app/services/scene-draft.service.ts
Normal file
26
web/src/app/services/scene-draft.service.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
48
web/src/app/services/session-prep.model.ts
Normal file
48
web/src/app/services/session-prep.model.ts
Normal 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[];
|
||||
}
|
||||
18
web/src/app/services/session-prep.service.ts
Normal file
18
web/src/app/services/session-prep.service.ts
Normal 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`);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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`, {});
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user