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:
@@ -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>
|
||||
|
||||
@@ -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]),
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user