Plusieurs ajouts :
- 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:
@@ -196,60 +196,38 @@
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ============ Sessions de jeu ============ -->
|
||||
<section class="detail-section sessions-section" *ngIf="!editing">
|
||||
<!-- ============ Parties (Playthroughs) ============ -->
|
||||
<section class="detail-section playthroughs-section" *ngIf="!editing">
|
||||
<div class="section-header">
|
||||
<h2>
|
||||
<lucide-icon [img]="Dices" [size]="18"></lucide-icon>
|
||||
Sessions de jeu
|
||||
Mes parties
|
||||
</h2>
|
||||
|
||||
<!-- Cas 1 : aucune session active dans l'app → on peut lancer -->
|
||||
<button *ngIf="!activeSessionGlobal"
|
||||
class="btn-add"
|
||||
[disabled]="startingSession"
|
||||
(click)="startSession()">
|
||||
<lucide-icon [img]="Play" [size]="14"></lucide-icon>
|
||||
Lancer une nouvelle session
|
||||
</button>
|
||||
|
||||
<!-- Cas 2 : session active sur cette campagne → reprendre -->
|
||||
<button *ngIf="activeSessionOnCurrentCampaign"
|
||||
class="btn-add"
|
||||
(click)="openSession(activeSessionOnCurrentCampaign)">
|
||||
<lucide-icon [img]="Play" [size]="14"></lucide-icon>
|
||||
Reprendre la session en cours
|
||||
</button>
|
||||
|
||||
<!-- Cas 3 : session active sur une autre campagne → bloqué -->
|
||||
<button *ngIf="isLaunchBlockedByOtherCampaign"
|
||||
class="btn-add"
|
||||
disabled
|
||||
title="Une session est déjà en cours sur une autre campagne. Termine-la d'abord.">
|
||||
<lucide-icon [img]="Play" [size]="14"></lucide-icon>
|
||||
Session en cours ailleurs
|
||||
<button class="btn-add" [disabled]="newPlaythroughInFlight" (click)="createPlaythrough()">
|
||||
<lucide-icon [img]="Plus" [size]="14"></lucide-icon>
|
||||
Nouvelle partie
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="sessions-grid" *ngIf="sessions.length > 0">
|
||||
<div class="session-card"
|
||||
*ngFor="let session of sessions"
|
||||
[class.session-card--active]="session.active"
|
||||
(click)="openSession(session)">
|
||||
<lucide-icon [img]="Dices" [size]="20" class="session-icon"></lucide-icon>
|
||||
<div class="session-info">
|
||||
<span class="session-name">{{ session.name }}</span>
|
||||
<span class="session-meta">
|
||||
<span class="session-status" *ngIf="session.active">● En cours</span>
|
||||
<span *ngIf="!session.active">Terminée le {{ session.endedAt | date:'dd/MM/yyyy' }}</span>
|
||||
</span>
|
||||
<div class="playthroughs-grid" *ngIf="playthroughs.length > 0">
|
||||
<div class="playthrough-card"
|
||||
*ngFor="let p of playthroughs"
|
||||
(click)="openPlaythrough(p)">
|
||||
<lucide-icon [img]="Dices" [size]="22" class="playthrough-icon"></lucide-icon>
|
||||
<div class="playthrough-info">
|
||||
<span class="playthrough-name">{{ p.name }}</span>
|
||||
<span class="playthrough-meta" *ngIf="p.description">{{ p.description }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="empty-state empty-state--compact" *ngIf="sessions.length === 0">
|
||||
<p>Aucune session de jeu pour le moment.</p>
|
||||
<div class="empty-state empty-state--compact" *ngIf="playthroughs.length === 0">
|
||||
<p>Aucune partie. Créez-en une pour démarrer une session avec une table.</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Sessions retirées : elles vivent désormais dans une Partie.
|
||||
Les faits narratifs sont définis dans les conditions de chaque quête
|
||||
(chapter-edit > « Conditions de déblocage »). -->
|
||||
|
||||
</div>
|
||||
|
||||
@@ -1,3 +1,58 @@
|
||||
.section-hint {
|
||||
margin: -0.25rem 0 0.85rem;
|
||||
font-size: 0.85rem;
|
||||
color: var(--color-text-muted, #666);
|
||||
max-width: 70ch;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
// ==== Parties (Playthroughs) ====
|
||||
.playthroughs-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.playthrough-card {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.7rem;
|
||||
padding: 0.85rem 1rem;
|
||||
border: 1px solid var(--color-border, #e2e2e2);
|
||||
border-radius: 10px;
|
||||
background: var(--color-surface, #fff);
|
||||
cursor: pointer;
|
||||
transition: border-color 120ms ease, transform 120ms ease, box-shadow 120ms ease;
|
||||
|
||||
&:hover {
|
||||
border-color: var(--color-primary, #2c6cd6);
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
}
|
||||
|
||||
.playthrough-icon { color: var(--color-primary, #2c6cd6); flex: 0 0 auto; }
|
||||
|
||||
.playthrough-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.15rem;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.playthrough-name {
|
||||
font-weight: 600;
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
|
||||
.playthrough-meta {
|
||||
font-size: 0.78rem;
|
||||
color: var(--color-text-muted, #777);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.campaign-detail {
|
||||
padding: 2.5rem 2rem;
|
||||
display: flex;
|
||||
|
||||
@@ -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(', ')}.`);
|
||||
|
||||
Reference in New Issue
Block a user