Plusieurs ajouts :
Some checks failed
Build & Push Images / build (brain) (push) Has been cancelled
Build & Push Images / build (core) (push) Has been cancelled
Build & Push Images / build (web) (push) Has been cancelled
Build & Push Images / build-switcher (push) Has been cancelled

- Possibilité de configurer des lieux dans une scène : permet de configurer un donjon par exemple avec les pièces, les trésors par pièce, la narration..... Mise en place également de conditions permettant de conditionner le déblocage des quêtes.
- Possibilité de transformé un arc en instance non linéaire afin de faire un hub. Permet de jouer de préparer des campagnes type Dragon of Icespire peak plus facilement.
- Configuration de partie : chaque partie va contenir les séances, ce qui permettra de suivre le déblocage des conditions pour les quêtes.

Passage en 0.9.1-beta
This commit is contained in:
2026-06-03 15:44:48 +02:00
parent e3fc96a1bc
commit 504c4b7b6c
147 changed files with 5347 additions and 392 deletions

View File

@@ -13,6 +13,8 @@ import { GameSystem } from '../../../services/game-system.model';
import { CharacterService } from '../../../services/character.service';
import { NpcService } from '../../../services/npc.service';
import { SessionService } from '../../../services/session.service';
import { PlaythroughService } from '../../../services/playthrough.service';
import { Playthrough } from '../../../services/campaign.model';
import { Session } from '../../../services/session.model';
import { Character } from '../../../services/character.model';
import { Npc } from '../../../services/npc.model';
@@ -69,6 +71,11 @@ export class CampaignDetailComponent implements OnInit, OnDestroy {
/** Indicateur de lancement en cours pour éviter les double-clics. */
startingSession = false;
/** Parties (Playthroughs) de cette campagne. */
playthroughs: Playthrough[] = [];
/** Partie par défaut (1re) — fallback pour les actions session/PJ tant que l'UI Playthrough n'est pas finie. */
defaultPlaythroughId: string | null = null;
/** Mode édition inline. */
editing = false;
editName = '';
@@ -92,6 +99,7 @@ export class CampaignDetailComponent implements OnInit, OnDestroy {
private characterService: CharacterService,
private npcService: NpcService,
private sessionService: SessionService,
private playthroughService: PlaythroughService,
private layoutService: LayoutService,
private pageTitleService: PageTitleService,
private confirmDialog: ConfirmDialogService
@@ -109,11 +117,14 @@ export class CampaignDetailComponent implements OnInit, OnDestroy {
allCampaigns: this.campaignService.getAllCampaigns(),
treeData: loadCampaignTreeData(this.campaignService, id, this.characterService, this.npcService).pipe(
catchError(() => of({ arcs: [], chaptersByArc: {}, scenesByChapter: {}, characters: [], npcs: [] } as CampaignTreeData))
)
),
playthroughs: this.playthroughService.listByCampaign(id).pipe(catchError(() => of([] as Playthrough[])))
}))
).subscribe(({ campaign, allCampaigns, treeData }) => {
).subscribe(({ campaign, allCampaigns, treeData, playthroughs }) => {
this.campaign = campaign;
this.editing = false;
this.playthroughs = playthroughs;
this.defaultPlaythroughId = playthroughs.length > 0 ? playthroughs[0].id! : null;
this.loadLinkedLore(campaign);
this.loadLinkedGameSystem(campaign);
this.loadCharacters(campaign.id!);
@@ -145,10 +156,13 @@ export class CampaignDetailComponent implements OnInit, OnDestroy {
allCampaigns: this.campaignService.getAllCampaigns(),
treeData: loadCampaignTreeData(this.campaignService, id, this.characterService, this.npcService).pipe(
catchError(() => of({ arcs: [], chaptersByArc: {}, scenesByChapter: {}, characters: [], npcs: [] } as CampaignTreeData))
)
}).subscribe(({ campaign, allCampaigns, treeData }) => {
),
playthroughs: this.playthroughService.listByCampaign(id).pipe(catchError(() => of([] as Playthrough[])))
}).subscribe(({ campaign, allCampaigns, treeData, playthroughs }) => {
this.campaign = campaign;
this.editing = false;
this.playthroughs = playthroughs;
this.defaultPlaythroughId = playthroughs.length > 0 ? playthroughs[0].id! : null;
this.loadLinkedLore(campaign);
this.loadLinkedGameSystem(campaign);
this.loadCharacters(campaign.id!);
@@ -186,9 +200,13 @@ export class CampaignDetailComponent implements OnInit, OnDestroy {
).subscribe(gs => this.linkedGameSystem = gs);
}
/** Charge les fiches de personnages (PJ) de la campagne. */
private loadCharacters(campaignId: string): void {
this.characterService.getByCampaign(campaignId).pipe(
/** Charge les PJ de la Partie par défaut (refonte Playthrough). */
private loadCharacters(_campaignId: string): void {
if (!this.defaultPlaythroughId) {
this.characters = [];
return;
}
this.characterService.getByPlaythrough(this.defaultPlaythroughId).pipe(
catchError(() => of([] as Character[]))
).subscribe(list => this.characters = list);
}
@@ -200,55 +218,33 @@ export class CampaignDetailComponent implements OnInit, OnDestroy {
).subscribe(list => this.npcs = list);
}
/**
* Charge les sessions de cette campagne ET la session active globale.
* La session globale conditionne si le bouton "Lancer" est activable
* (règle métier : une seule session active simultanément dans l'app).
*/
private loadSessions(campaignId: string): void {
this.sessionService.getSessions(campaignId).pipe(
catchError(() => of([] as Session[]))
).subscribe(list => this.sessions = list);
// Sessions retirées de cette vue (vivent dans Playthrough Detail).
private loadSessions(_campaignId: string): void { this.sessions = []; }
this.sessionService.getActiveSession().pipe(
catchError(() => of(null))
).subscribe(active => this.activeSessionGlobal = active);
// ─────────────── Playthroughs (Parties) ───────────────
openPlaythrough(p: Playthrough): void {
this.router.navigate(['/campaigns', this.campaign?.id, 'playthroughs', p.id]);
}
/** True si une session est active mais sur une AUTRE campagne (lancement bloqué). */
get isLaunchBlockedByOtherCampaign(): boolean {
return !!this.activeSessionGlobal
&& !!this.campaign
&& this.activeSessionGlobal.campaignId !== this.campaign.id;
}
/** Session active sur la campagne courante (le MJ joue déjà ici). */
get activeSessionOnCurrentCampaign(): Session | null {
if (!this.activeSessionGlobal || !this.campaign) return null;
return this.activeSessionGlobal.campaignId === this.campaign.id
? this.activeSessionGlobal
: null;
}
startSession(): void {
if (!this.campaign || this.startingSession || this.isLaunchBlockedByOtherCampaign) return;
this.startingSession = true;
this.sessionService.startSession(this.campaign.id!).subscribe({
next: session => {
this.startingSession = false;
this.router.navigate(['/sessions', session.id]);
/** Crée une nouvelle Partie avec un nom par défaut, puis y navigue. */
newPlaythroughInFlight = false;
createPlaythrough(): void {
if (!this.campaign || this.newPlaythroughInFlight) return;
this.newPlaythroughInFlight = true;
const defaultName = `Nouvelle partie ${this.playthroughs.length + 1}`;
this.playthroughService.create({
campaignId: this.campaign.id!,
name: defaultName
}).subscribe({
next: created => {
this.newPlaythroughInFlight = false;
this.router.navigate(['/campaigns', this.campaign?.id, 'playthroughs', created.id]);
},
error: () => {
this.startingSession = false;
console.error('Erreur lors du lancement de la session');
}
error: () => { this.newPlaythroughInFlight = false; }
});
}
openSession(session: Session): void {
this.router.navigate(['/sessions', session.id]);
}
createCharacter(): void {
if (!this.campaign) return;
this.router.navigate(['/campaigns', this.campaign.id, 'characters', 'create']);
@@ -444,7 +440,7 @@ export class CampaignDetailComponent implements OnInit, OnDestroy {
if (impact.arcs > 0) parts.push(`${impact.arcs} arc${impact.arcs > 1 ? 's' : ''}`);
if (impact.chapters > 0) parts.push(`${impact.chapters} chapitre${impact.chapters > 1 ? 's' : ''}`);
if (impact.scenes > 0) parts.push(`${impact.scenes} scène${impact.scenes > 1 ? 's' : ''}`);
if (impact.characters > 0) parts.push(`${impact.characters} personnage${impact.characters > 1 ? 's' : ''}`);
if (impact.playthroughs > 0) parts.push(`${impact.playthroughs} partie${impact.playthroughs > 1 ? 's' : ''} (avec ses PJ, sessions, faits)`);
const details: string[] = [];
if (parts.length) details.push(`Sera aussi supprime : ${parts.join(', ')}.`);