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

@@ -26,6 +26,22 @@
<form [formGroup]="form" (ngSubmit)="submit()" class="edit-form">
<!-- Section Hub : conditions de déblocage (scénario). Visible si l'arc parent est HUB. -->
<div class="hub-section" *ngIf="parentArc?.type === 'HUB'">
<h2 class="hub-section-title">🗺️ Conditions de déblocage de la quête</h2>
<small class="field-hint">
Définition du scénario : toutes les conditions doivent être remplies (ET) pour
que la quête soit débloquée dans une Partie. La progression en cours et l'état
des faits se gèrent dans l'écran de la Partie, pas ici.
</small>
<app-prerequisite-editor
[prerequisites]="prerequisites"
[availableQuests]="availableQuests"
[availableFlags]="availableFlagNames"
(prerequisitesChange)="onPrerequisitesChange($event)">
</app-prerequisite-editor>
</div>
<!-- Illustrations (galerie editable, rendu editorial) -->
<div class="field">
<label>Illustrations</label>

View File

@@ -3,6 +3,57 @@
max-width: 640px;
}
// === Section Hub : prérequis + progression ===
.hub-section {
border: 1px solid rgba(66, 133, 244, 0.25);
background: rgba(66, 133, 244, 0.03);
border-radius: 12px;
padding: 1rem 1.1rem 1.25rem;
margin-bottom: 1rem;
}
.hub-section-title {
margin: 0 0 0.85rem;
font-size: 1rem;
font-weight: 600;
color: var(--color-primary, #2c6cd6);
}
.progression-choice {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 0.5rem;
}
.progression-option {
padding: 0.5rem 0.7rem;
border: 1px solid var(--color-border, #e2e2e2);
border-radius: 8px;
text-align: center;
font-size: 0.88rem;
cursor: pointer;
background: var(--color-surface, #fff);
transition: border-color 120ms ease, background 120ms ease;
&:hover { border-color: var(--color-primary, #2c6cd6); }
&.selected {
border-color: var(--color-primary, #2c6cd6);
background: rgba(66, 133, 244, 0.08);
font-weight: 600;
}
input[type="radio"] { display: none; }
}
.hub-flags-panel {
margin-top: 0.75rem;
padding: 0.85rem;
border: 1px dashed var(--color-border, #d8d8d8);
border-radius: 8px;
background: var(--color-surface, #fff);
}
// Header local : titre à gauche, actions (Assistant IA) à droite.
.page-header {
display: flex;

View File

@@ -11,27 +11,38 @@ import { NpcService } from '../../../services/npc.service';
import { PageService } from '../../../services/page.service';
import { LayoutService } from '../../../services/layout.service';
import { PageTitleService } from '../../../services/page-title.service';
import { Chapter } from '../../../services/campaign.model';
import { Chapter, Prerequisite, Arc } from '../../../services/campaign.model';
import { Page } from '../../../services/page.model';
import { loadCampaignTreeData, buildCampaignSidebarConfig } from '../../campaign-tree.helper';
import { LoreLinkPickerComponent } from '../../../shared/lore-link-picker/lore-link-picker.component';
import { AiChatDrawerComponent } from '../../../shared/ai-chat-drawer/ai-chat-drawer.component';
import { ImageGalleryComponent } from '../../../shared/image-gallery/image-gallery.component';
import { IconPickerComponent } from '../../../shared/icon-picker/icon-picker.component';
import { PrerequisiteEditorComponent } from '../../../shared/prerequisite-editor/prerequisite-editor.component';
import { CampaignFlagService } from '../../../services/campaign-flag.service';
import { CAMPAIGN_ICON_OPTIONS } from '../../campaign-icons';
import { ConfirmDialogService } from '../../../shared/confirm-dialog/confirm-dialog.service';
/**
* Écran de détail/modification d'un Chapitre.
* Route : /campaigns/:campaignId/arcs/:arcId/chapters/:chapterId
* Écran d'édition d'un Chapitre. Donnée de SCÉNARIO uniquement.
*
* Inclut le picker de pages Lore (B2 cross-context) si la campagne parente
* est associée à un Lore.
* Depuis Playthrough : la progression et le toggle des flags se font dans le
* contexte d'une Partie (voir Playthrough Detail). On garde ici uniquement les
* prérequis (conditions de déblocage), qui font partie du scénario.
*/
@Component({
selector: 'app-chapter-edit',
standalone: true,
imports: [CommonModule, ReactiveFormsModule, LucideAngularModule, LoreLinkPickerComponent, AiChatDrawerComponent, ImageGalleryComponent, IconPickerComponent],
imports: [
CommonModule,
ReactiveFormsModule,
LucideAngularModule,
LoreLinkPickerComponent,
AiChatDrawerComponent,
ImageGalleryComponent,
IconPickerComponent,
PrerequisiteEditorComponent
],
templateUrl: './chapter-edit.component.html',
styleUrls: ['./chapter-edit.component.scss']
})
@@ -41,7 +52,6 @@ export class ChapterEditComponent implements OnInit, OnDestroy {
readonly campaignIconOptions = CAMPAIGN_ICON_OPTIONS;
selectedIcon: string | null = null;
/** État drawer chat IA (b5.7 — intégration Campagne). */
chatOpen = false;
readonly chatQuickSuggestions = [
'Propose des objectifs clairs pour les joueurs dans ce chapitre',
@@ -63,6 +73,18 @@ export class ChapterEditComponent implements OnInit, OnDestroy {
illustrationImageIds: string[] = [];
mapImageIds: string[] = [];
/** Prérequis (donnée de scénario). */
prerequisites: Prerequisite[] = [];
/** Quêtes candidates pour QUEST_COMPLETED (chapitres de la campagne, sauf celui-ci). */
availableQuests: Chapter[] = [];
/** Faits déclarés au niveau Campagne (autocomplete FLAG_SET). */
availableFlagNames: string[] = [];
/** L'arc parent — pour conditionner la section Hub (prérequis pertinents). */
parentArc: Arc | null = null;
constructor(
private fb: FormBuilder,
private route: ActivatedRoute,
@@ -73,7 +95,8 @@ export class ChapterEditComponent implements OnInit, OnDestroy {
private pageService: PageService,
private layoutService: LayoutService,
private pageTitleService: PageTitleService,
private confirmDialog: ConfirmDialogService
private confirmDialog: ConfirmDialogService,
private campaignFlagService: CampaignFlagService
) {
this.form = this.fb.group({
name: ['', Validators.required],
@@ -85,9 +108,6 @@ export class ChapterEditComponent implements OnInit, OnDestroy {
}
ngOnInit(): void {
// On s'abonne à paramMap plutôt que de lire snapshot une fois : Angular
// réutilise le composant quand on navigue entre chapitres frères via
// l'arbre (même route pattern), et ngOnInit ne se relance pas.
this.route.paramMap.subscribe(pm => {
const newCampaignId = pm.get('campaignId')!;
const newArcId = pm.get('arcId')!;
@@ -124,6 +144,21 @@ export class ChapterEditComponent implements OnInit, OnDestroy {
this.selectedIcon = chapter.icon ?? null;
this.illustrationImageIds = [...(chapter.illustrationImageIds ?? [])];
this.mapImageIds = [...(chapter.mapImageIds ?? [])];
this.prerequisites = [...(chapter.prerequisites ?? [])];
const allChapters: Chapter[] = [];
Object.values(treeData.chaptersByArc).forEach(list => allChapters.push(...list));
this.availableQuests = allChapters.filter(c => c.id !== this.chapterId);
this.parentArc = treeData.arcs.find(a => a.id === this.arcId) ?? null;
// Autocomplete des FLAG_SET : les noms de faits déjà référencés ailleurs
// dans la campagne (déduit des autres quêtes).
this.campaignFlagService.listReferenced(this.campaignId).subscribe({
next: names => { this.availableFlagNames = names; }
});
this.form.patchValue({
name: chapter.name,
description: chapter.description ?? '',
@@ -136,6 +171,10 @@ export class ChapterEditComponent implements OnInit, OnDestroy {
});
}
onPrerequisitesChange(next: Prerequisite[]): void {
this.prerequisites = next;
}
submit(): void {
if (this.form.invalid || !this.chapter) return;
this.campaignService.updateChapter(this.chapterId, {
@@ -146,6 +185,7 @@ export class ChapterEditComponent implements OnInit, OnDestroy {
gmNotes: this.form.value.gmNotes,
playerObjectives: this.form.value.playerObjectives,
narrativeStakes: this.form.value.narrativeStakes,
prerequisites: this.prerequisites,
relatedPageIds: this.relatedPageIds,
illustrationImageIds: this.illustrationImageIds,
mapImageIds: this.mapImageIds,
@@ -176,10 +216,5 @@ export class ChapterEditComponent implements OnInit, OnDestroy {
this.router.navigate(['/campaigns', this.campaignId, 'arcs', this.arcId, 'chapters', this.chapterId]);
}
ngOnDestroy(): void {
// Volontairement vide : la sidebar reste prise en charge par le composant
// suivant (autre sous-route ou le composant detail parent) qui appellera
// show(). Eviter d'appeler hide() ici previent le clignotement / la
// disparition de la sidebar lors des navigations internes a la section.
}
ngOnDestroy(): void {}
}