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

@@ -231,6 +231,18 @@
</small>
</div>
<!-- Lieu explorable : pièces / donjon -->
<app-expandable-section title="Lieu explorable (donjon, crypte…)" icon="🏰" [initiallyOpen]="rooms.length > 0">
<small class="field-hint">
Si cette scène représente un lieu à parcourir pièce par pièce, ajoutez-les ici.
La scène bascule alors en mode « donjon » côté affichage.
</small>
<app-rooms-editor
[rooms]="rooms"
(roomsChange)="onRoomsChange($event)">
</app-rooms-editor>
</app-expandable-section>
</form>
</div>

View File

@@ -11,7 +11,7 @@ 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 { Scene, SceneBranch } from '../../../services/campaign.model';
import { Scene, SceneBranch, Room } from '../../../services/campaign.model';
import { Page } from '../../../services/page.model';
import { loadCampaignTreeData, buildCampaignSidebarConfig } from '../../campaign-tree.helper';
import { ExpandableSectionComponent } from '../../../shared/expandable-section/expandable-section.component';
@@ -19,6 +19,7 @@ import { LoreLinkPickerComponent } from '../../../shared/lore-link-picker/lore-l
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 { RoomsEditorComponent } from '../../../shared/rooms-editor/rooms-editor.component';
import { CAMPAIGN_ICON_OPTIONS } from '../../campaign-icons';
import { ConfirmDialogService } from '../../../shared/confirm-dialog/confirm-dialog.service';
@@ -29,7 +30,7 @@ import { ConfirmDialogService } from '../../../shared/confirm-dialog/confirm-dia
@Component({
selector: 'app-scene-edit',
standalone: true,
imports: [CommonModule, ReactiveFormsModule, LucideAngularModule, ExpandableSectionComponent, LoreLinkPickerComponent, AiChatDrawerComponent, ImageGalleryComponent, IconPickerComponent],
imports: [CommonModule, ReactiveFormsModule, LucideAngularModule, ExpandableSectionComponent, LoreLinkPickerComponent, AiChatDrawerComponent, ImageGalleryComponent, IconPickerComponent, RoomsEditorComponent],
templateUrl: './scene-edit.component.html',
styleUrls: ['./scene-edit.component.scss']
})
@@ -67,6 +68,11 @@ export class SceneEditComponent implements OnInit, OnDestroy {
/** Branches narratives (état local mutable, persisté au submit). */
branches: SceneBranch[] = [];
/** Pièces du lieu explorable (état local, persisté au submit). */
rooms: Room[] = [];
onRoomsChange(next: Room[]): void { this.rooms = next; }
constructor(
private fb: FormBuilder,
private route: ActivatedRoute,
@@ -144,6 +150,7 @@ export class SceneEditComponent implements OnInit, OnDestroy {
this.mapImageIds = [...(scene.mapImageIds ?? [])];
this.siblingScenes = chapterScenes.filter(s => s.id !== this.sceneId);
this.branches = (scene.branches ?? []).map(b => ({ ...b }));
this.rooms = (scene.rooms ?? []).map(r => ({ ...r, branches: [...(r.branches ?? [])] }));
this.form.patchValue({
name: scene.name,
description: scene.description ?? '',
@@ -180,6 +187,7 @@ export class SceneEditComponent implements OnInit, OnDestroy {
illustrationImageIds: this.illustrationImageIds,
mapImageIds: this.mapImageIds,
branches: this.branches,
rooms: this.rooms,
icon: this.selectedIcon
}).subscribe({
next: () => this.router.navigate(['/campaigns', this.campaignId, 'arcs', this.arcId, 'chapters', this.chapterId, 'scenes', this.sceneId]),

View File

@@ -100,4 +100,48 @@
</div>
</section>
<!-- Lieu explorable : pièces (mode donjon) -->
<section class="view-section" *ngIf="(scene.rooms?.length ?? 0) > 0">
<h2 class="view-section-title"><span class="view-section-icon">🏰</span> Pièces du lieu</h2>
<div class="rooms-readonly">
<article class="room-readonly" *ngFor="let r of scene.rooms; let i = index">
<header class="room-readonly-head">
<span class="room-readonly-index">#{{ i + 1 }}</span>
<span class="room-readonly-name">{{ r.name }}</span>
<span class="room-readonly-floor" *ngIf="r.floor !== null && r.floor !== undefined">
Étage {{ r.floor }}
</span>
</header>
<p class="room-readonly-desc" *ngIf="r.description?.trim()">{{ r.description }}</p>
<div class="room-readonly-grid">
<div *ngIf="r.enemies?.trim()">
<strong>⚔️ Ennemis</strong>
<p>{{ r.enemies }}</p>
</div>
<div *ngIf="r.loot?.trim()">
<strong>💰 Loot</strong>
<p>{{ r.loot }}</p>
</div>
<div *ngIf="r.traps?.trim()">
<strong>⚠️ Pièges</strong>
<p>{{ r.traps }}</p>
</div>
<div *ngIf="r.gmNotes?.trim()" class="room-readonly-private">
<strong>🔒 Notes MJ</strong>
<p>{{ r.gmNotes }}</p>
</div>
</div>
<div class="room-readonly-branches" *ngIf="(r.branches?.length ?? 0) > 0">
<strong>→ Sorties</strong>
<ul>
<li *ngFor="let b of r.branches">
<em>{{ b.label }}</em> → {{ roomNameById(scene, b.targetRoomId) }}
<span class="branch-cond" *ngIf="b.condition?.trim()">(si : {{ b.condition }})</span>
</li>
</ul>
</div>
</article>
</div>
</section>
</div>

View File

@@ -1 +1,84 @@
// Styles partagés via styles/_view.scss
.rooms-readonly {
display: flex;
flex-direction: column;
gap: 0.75rem;
}
.room-readonly {
border: 1px solid var(--color-border, #e2e2e2);
border-radius: 10px;
padding: 0.85rem 1rem;
background: var(--color-surface, #fff);
}
.room-readonly-head {
display: flex;
align-items: center;
gap: 0.5rem;
margin-bottom: 0.4rem;
}
.room-readonly-index {
font-family: 'JetBrains Mono', ui-monospace, monospace;
font-size: 0.78rem;
color: var(--color-text-muted, #777);
}
.room-readonly-name {
font-weight: 600;
font-size: 1rem;
flex: 1;
}
.room-readonly-floor {
font-size: 0.78rem;
padding: 0.15rem 0.55rem;
border-radius: 999px;
background: rgba(66, 133, 244, 0.1);
color: #2c6cd6;
}
.room-readonly-desc {
margin: 0 0 0.6rem;
font-size: 0.9rem;
line-height: 1.5;
color: var(--color-text, #333);
white-space: pre-wrap;
}
.room-readonly-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
gap: 0.7rem;
margin-bottom: 0.5rem;
> div {
strong { display: block; font-size: 0.82rem; margin-bottom: 0.2rem; color: var(--color-text-muted, #555); }
p { margin: 0; font-size: 0.88rem; white-space: pre-wrap; }
}
}
.room-readonly-private {
background: rgba(192, 57, 43, 0.05);
padding: 0.4rem 0.55rem;
border-radius: 6px;
border-left: 3px solid rgba(192, 57, 43, 0.4);
}
.room-readonly-branches {
margin-top: 0.45rem;
padding-top: 0.45rem;
border-top: 1px dashed var(--color-border, #ececec);
font-size: 0.85rem;
strong { display: block; font-size: 0.82rem; margin-bottom: 0.15rem; color: var(--color-text-muted, #555); }
ul { margin: 0; padding-left: 1.1rem; }
li { margin-bottom: 0.1rem; }
}
.branch-cond {
color: var(--color-text-muted, #777);
font-style: italic;
}

View File

@@ -99,6 +99,11 @@ export class SceneViewComponent implements OnInit, OnDestroy {
return this.availablePages.find(p => p.id === pageId)?.title ?? '(page supprimée)';
}
/** Résout le nom d'une pièce cible (pour afficher les sorties inter-pièces). */
roomNameById(scene: Scene | null, roomId: string): string {
return scene?.rooms?.find(r => r.id === roomId)?.name ?? '(pièce supprimée)';
}
editMode(): void {
this.router.navigate([
'/campaigns', this.campaignId, 'arcs', this.arcId,