Ajout de tableaux dans la partie templates / pages de lore : possibilité d'ajouter un tableau multiligne (par exemple pour faire des tableaux d'objets dans les boutiques) ; tableau type liste clé / valeur (pour des statistiques et ce genre de chose).
All checks were successful
Build & Push Images / build (brain) (push) Successful in 1m36s
Build & Push Images / build (core) (push) Successful in 1m53s
Build & Push Images / build-switcher (push) Successful in 25s
Build & Push Images / build (web) (push) Successful in 1m59s

Ajout de la possibilité de lié un PNJ à une page de lore
Ajout d'un graphe de liaison entre lore / PNJs
Passage en v.0.12.3-beta
This commit is contained in:
2026-06-12 13:23:46 +02:00
parent 7f519588b6
commit 14fc1c28fe
51 changed files with 1647 additions and 88 deletions

View File

@@ -87,6 +87,20 @@
</app-dynamic-fields-form>
</div>
<!-- Pages de Lore liées (uniquement si la campagne est rattachée à un Lore) -->
@if (loreId) {
<div class="field">
<label>Pages de Lore liées</label>
<app-lore-link-picker
[value]="relatedPageIds"
[availablePages]="lorePages"
[loreId]="loreId"
(valueChange)="relatedPageIds = $event">
</app-lore-link-picker>
<p class="hint">Reliez ce PNJ aux pages de l'univers (sa ville, sa faction, sa région…). Ces liens apparaissent dans le graphe du Lore.</p>
</div>
}
<div class="actions">
<button type="button" class="btn-primary" [disabled]="!name.trim()" (click)="submit()">
<lucide-icon [img]="Save" [size]="16"></lucide-icon>

View File

@@ -6,11 +6,14 @@ import { LucideAngularModule, Save, ArrowLeft, Drama, Trash2, Sparkles } from 'l
import { NpcService } from '../../../services/npc.service';
import { CampaignService } from '../../../services/campaign.service';
import { GameSystemService } from '../../../services/game-system.service';
import { PageService } from '../../../services/page.service';
import { CampaignSidebarService } from '../../../services/campaign-sidebar.service';
import { TemplateField } from '../../../services/template.model';
import { Page } from '../../../services/page.model';
import { AiChatDrawerComponent } from '../../../shared/ai-chat-drawer/ai-chat-drawer.component';
import { DynamicFieldsFormComponent } from '../../../shared/dynamic-fields-form/dynamic-fields-form.component';
import { SingleImagePickerComponent } from '../../../shared/single-image-picker/single-image-picker.component';
import { LoreLinkPickerComponent } from '../../../shared/lore-link-picker/lore-link-picker.component';
import { ConfirmDialogService } from '../../../shared/confirm-dialog/confirm-dialog.service';
/**
@@ -21,7 +24,7 @@ import { ConfirmDialogService } from '../../../shared/confirm-dialog/confirm-dia
*/
@Component({
selector: 'app-npc-edit',
imports: [FormsModule, LucideAngularModule, AiChatDrawerComponent, DynamicFieldsFormComponent, SingleImagePickerComponent],
imports: [FormsModule, LucideAngularModule, AiChatDrawerComponent, DynamicFieldsFormComponent, SingleImagePickerComponent, LoreLinkPickerComponent],
templateUrl: './npc-edit.component.html',
styleUrls: ['./npc-edit.component.scss']
})
@@ -56,12 +59,20 @@ export class NpcEditComponent implements OnInit {
templateFields: TemplateField[] = [];
private order = 0;
/** Lore lié à la campagne (null = pas de lore → section liens masquée). */
loreId: string | null = null;
/** Pages du lore lié — référentiel du picker. */
lorePages: Page[] = [];
/** IDs des pages de lore référencées par ce PNJ. */
relatedPageIds: string[] = [];
constructor(
private route: ActivatedRoute,
private router: Router,
private service: NpcService,
private campaignService: CampaignService,
private gameSystemService: GameSystemService,
private pageService: PageService,
private campaignSidebar: CampaignSidebarService,
private confirmDialog: ConfirmDialogService
) {}
@@ -87,6 +98,7 @@ export class NpcEditComponent implements OnInit {
this.values = n.values ?? {};
this.imageValues = n.imageValues ?? {};
this.keyValueValues = n.keyValueValues ?? {};
this.relatedPageIds = [...(n.relatedPageIds ?? [])];
this.order = n.order ?? 0;
},
error: () => this.back()
@@ -108,6 +120,14 @@ export class NpcEditComponent implements OnInit {
private loadTemplateForCampaign(campaignId: string): void {
this.campaignService.getCampaignById(campaignId).subscribe({
next: (campaign) => {
// Lore lié → charge ses pages pour le picker de références.
if (campaign.loreId) {
this.loreId = campaign.loreId;
this.pageService.getByLoreId(campaign.loreId).subscribe({
next: (pages) => { this.lorePages = pages; },
error: () => { this.lorePages = []; }
});
}
if (!campaign.gameSystemId) {
this.templateFields = [];
return;
@@ -132,7 +152,8 @@ export class NpcEditComponent implements OnInit {
values: this.values,
imageValues: this.imageValues,
keyValueValues: this.keyValueValues,
campaignId: this.campaignId
campaignId: this.campaignId,
relatedPageIds: this.relatedPageIds
};
const isCreation = !this.npcId;
const req = this.npcId

View File

@@ -18,6 +18,23 @@
</div>
<app-persona-view [persona]="npc" [templateFields]="templateFields"></app-persona-view>
<!-- Pages de Lore liées à ce PNJ (sa ville, sa faction…) -->
@if (loreId && (npc?.relatedPageIds?.length ?? 0) > 0) {
<section class="nv-lore-links">
<h2 class="nv-lore-links-title">
<lucide-icon [img]="Link2" [size]="14"></lucide-icon>
Pages de Lore liées
</h2>
<div class="nv-lore-chips">
@for (pageId of npc!.relatedPageIds; track pageId) {
<a class="nv-lore-chip" [routerLink]="['/lore', loreId, 'pages', pageId]">
{{ titleOfPage(pageId) }}
</a>
}
</div>
</section>
}
</div>
@if (npcId && campaignId) {

View File

@@ -49,3 +49,46 @@
border-color: rgba(168, 85, 247, 0.5);
color: #d8b4fe;
}
// Pages de Lore liées au PNJ — chips cliquables sous la fiche.
.nv-lore-links {
max-width: 1100px;
margin: 24px auto 0;
padding: 0 32px;
.nv-lore-links-title {
display: flex;
align-items: center;
gap: 0.5rem;
font-size: 0.72rem;
font-weight: 600;
color: #a5b4fc;
text-transform: uppercase;
letter-spacing: 0.08em;
margin: 0 0 0.6rem;
}
.nv-lore-chips {
display: flex;
flex-wrap: wrap;
gap: 0.4rem;
}
.nv-lore-chip {
display: inline-flex;
align-items: center;
padding: 0.3rem 0.7rem;
background: #1a1a2e;
border: 1px solid #2a2a3d;
border-radius: 999px;
color: #d1d5db;
font-size: 0.82rem;
text-decoration: none;
transition: border-color 0.15s, color 0.15s;
&:hover {
border-color: #6c63ff;
color: white;
}
}
}

View File

@@ -1,14 +1,16 @@
import { Component, OnDestroy, OnInit } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router';
import { ActivatedRoute, Router, RouterLink } from '@angular/router';
import { Subscription } from 'rxjs';
import { LucideAngularModule, ArrowLeft, Edit3, Sparkles } from 'lucide-angular';
import { LucideAngularModule, ArrowLeft, Edit3, Sparkles, Link2 } from 'lucide-angular';
import { NpcService } from '../../../services/npc.service';
import { CampaignService } from '../../../services/campaign.service';
import { GameSystemService } from '../../../services/game-system.service';
import { PageService } from '../../../services/page.service';
import { CampaignSidebarService } from '../../../services/campaign-sidebar.service';
import { TemplateField } from '../../../services/template.model';
import { Npc } from '../../../services/npc.model';
import { Page } from '../../../services/page.model';
import { PersonaViewComponent } from '../../../shared/persona-view/persona-view.component';
import { AiChatDrawerComponent } from '../../../shared/ai-chat-drawer/ai-chat-drawer.component';
@@ -18,7 +20,7 @@ import { AiChatDrawerComponent } from '../../../shared/ai-chat-drawer/ai-chat-dr
*/
@Component({
selector: 'app-npc-view',
imports: [LucideAngularModule, PersonaViewComponent, AiChatDrawerComponent],
imports: [LucideAngularModule, RouterLink, PersonaViewComponent, AiChatDrawerComponent],
templateUrl: './npc-view.component.html',
styleUrls: ['./npc-view.component.scss']
})
@@ -26,12 +28,17 @@ export class NpcViewComponent implements OnInit, OnDestroy {
readonly ArrowLeft = ArrowLeft;
readonly Edit3 = Edit3;
readonly Sparkles = Sparkles;
readonly Link2 = Link2;
campaignId: string | null = null;
npcId: string | null = null;
npc: Npc | null = null;
templateFields: TemplateField[] = [];
/** Lore lié à la campagne (résolution des chips de pages liées). */
loreId: string | null = null;
/** Pages du lore lié, indexées pour résoudre les titres des chips. */
private lorePagesById = new Map<string, Page>();
chatOpen = false;
toggleChat(): void { this.chatOpen = !this.chatOpen; }
@@ -44,6 +51,7 @@ export class NpcViewComponent implements OnInit, OnDestroy {
private service: NpcService,
private campaignService: CampaignService,
private gameSystemService: GameSystemService,
private pageService: PageService,
private campaignSidebar: CampaignSidebarService
) {}
@@ -75,6 +83,13 @@ export class NpcViewComponent implements OnInit, OnDestroy {
this.templateFields = gs.npcTemplate ?? [];
});
}
// Lore lié → référentiel de pages pour résoudre les chips de liens.
if (camp.loreId) {
this.loreId = camp.loreId;
this.pageService.getByLoreId(camp.loreId).subscribe(pages => {
this.lorePagesById = new Map(pages.map(p => [p.id!, p]));
});
}
});
} else if (newCampaignId) {
this.campaignId = newCampaignId;
@@ -86,6 +101,11 @@ export class NpcViewComponent implements OnInit, OnDestroy {
this.paramsSub?.unsubscribe();
}
/** Titre d'une page de lore liée (pour les chips). */
titleOfPage(pageId: string): string {
return this.lorePagesById.get(pageId)?.title ?? '(page supprimée)';
}
edit(): void {
if (this.campaignId && this.npcId) {
this.router.navigate(['/campaigns', this.campaignId, 'npcs', this.npcId, 'edit']);