Refonte du système JDR + système de personnage joueurs / non joueurs :
Some checks failed
E2E Tests / e2e (push) Failing after 21s

- Système de templating dans le game system : en effet, les templates sont liés au game system car les fiches personnages ne sont pas forcément les même selon les jeux (perso Dnd possède + de compétences que Nimble par exemple)
- changement des fiches personnages pour adapter le templating au niveau des campagnes et remplir des pages de perso
This commit is contained in:
2026-04-30 10:42:09 +02:00
parent efaf5a3794
commit 52e389db24
67 changed files with 1610 additions and 255 deletions

View File

@@ -35,18 +35,28 @@
/>
</div>
<div class="field content-field">
<label>Fiche (markdown)</label>
<p class="hint">
Tout en markdown libre : apparence, motivations, faction, secrets, stats, notes MJ…
À terme, l'IA pourra exploiter ces infos pour incarner le PNJ avec cohérence dans les scènes.
</p>
<textarea
[(ngModel)]="markdownContent"
name="markdownContent"
rows="22"
placeholder="# Borin le forgeron&#10;&#10;**Race :** Nain&#10;**Faction :** Clan Feuillefer&#10;**Statut :** Vivant&#10;&#10;## Apparence&#10;Barbe rousse tressée, tablier de cuir brûlé...&#10;&#10;## Motivations&#10;Venger son clan décimé par les orcs il y a 10 hivers.&#10;&#10;## Notes MJ (secret)&#10;Connaît l'emplacement du marteau de Durin..."
></textarea>
<div class="field-row">
<div class="field">
<label>Portrait (ID image)</label>
<input type="text" [(ngModel)]="portraitImageId" name="portraitImageId" placeholder="ID de l'image portrait" />
</div>
<div class="field">
<label>Bandeau / Header (ID image)</label>
<input type="text" [(ngModel)]="headerImageId" name="headerImageId" placeholder="ID de l'image bandeau" />
</div>
</div>
<p class="hint">
Les portraits et bandeaux acceptent un ID d'image (MVP). Picker visuel a venir.
</p>
<div class="template-fields">
<app-dynamic-fields-form
[fields]="templateFields"
[values]="values"
[imageValues]="imageValues"
(valuesChange)="values = $event"
(imageValuesChange)="imageValues = $event">
</app-dynamic-fields-form>
</div>
<div class="actions">

View File

@@ -4,21 +4,22 @@ import { ActivatedRoute, Router } from '@angular/router';
import { FormsModule } from '@angular/forms';
import { LucideAngularModule, Save, ArrowLeft, Drama, Trash2, Sparkles } from 'lucide-angular';
import { NpcService } from '../../../services/npc.service';
import { CampaignService } from '../../../services/campaign.service';
import { GameSystemService } from '../../../services/game-system.service';
import { TemplateField } from '../../../services/template-field.model';
import { AiChatDrawerComponent } from '../../../shared/ai-chat-drawer/ai-chat-drawer.component';
import { DynamicFieldsFormComponent } from '../../../shared/dynamic-fields-form/dynamic-fields-form.component';
/**
* Éditeur plein écran d'une fiche de PNJ.
* Double rôle création/édition :
* - `/campaigns/:campaignId/npcs/create` → POST
* - `/campaigns/:campaignId/npcs/:npcId/edit` → PUT
*
* MVP : name + markdown libre. L'Assistant IA est branché en mode édition
* (focus entityType="npc") pour proposer apparence, motivations, secrets...
* Editeur plein ecran d'une fiche de PNJ.
* Refonte 2026-04-30 : meme principe que CharacterEditComponent — markdown
* libre remplace par un formulaire dynamique pilote par le npcTemplate du
* GameSystem associe a la campagne.
*/
@Component({
selector: 'app-npc-edit',
standalone: true,
imports: [CommonModule, FormsModule, LucideAngularModule, AiChatDrawerComponent],
imports: [CommonModule, FormsModule, LucideAngularModule, AiChatDrawerComponent, DynamicFieldsFormComponent],
templateUrl: './npc-edit.component.html',
styleUrls: ['./npc-edit.component.scss']
})
@@ -29,12 +30,11 @@ export class NpcEditComponent implements OnInit {
readonly Trash2 = Trash2;
readonly Sparkles = Sparkles;
/** État drawer chat IA focalisé sur ce PNJ. */
chatOpen = false;
readonly chatQuickSuggestions = [
'Propose une apparence et une posture marquantes',
'Suggère 2 motivations et un secret pour ce PNJ',
'Imagine 3 répliques signatures qui le caractérisent'
'Suggere 2 motivations et un secret pour ce PNJ',
'Imagine 3 repliques signatures qui le caracterisent'
];
toggleChat(): void { this.chatOpen = !this.chatOpen; }
@@ -43,13 +43,19 @@ export class NpcEditComponent implements OnInit {
npcId: string | null = null;
name = '';
markdownContent = '';
portraitImageId = '';
headerImageId = '';
values: Record<string, string> = {};
imageValues: Record<string, string[]> = {};
templateFields: TemplateField[] = [];
private order = 0;
constructor(
private route: ActivatedRoute,
private router: Router,
private service: NpcService
private service: NpcService,
private campaignService: CampaignService,
private gameSystemService: GameSystemService
) {}
ngOnInit(): void {
@@ -57,11 +63,18 @@ export class NpcEditComponent implements OnInit {
this.campaignId = params.get('campaignId');
this.npcId = params.get('npcId');
if (this.campaignId) {
this.loadTemplateForCampaign(this.campaignId);
}
if (this.npcId) {
this.service.getById(this.npcId).subscribe({
next: (n) => {
this.name = n.name;
this.markdownContent = n.markdownContent ?? '';
this.portraitImageId = n.portraitImageId ?? '';
this.headerImageId = n.headerImageId ?? '';
this.values = n.values ?? {};
this.imageValues = n.imageValues ?? {};
this.order = n.order ?? 0;
},
error: () => this.back()
@@ -69,21 +82,35 @@ export class NpcEditComponent implements OnInit {
}
}
private loadTemplateForCampaign(campaignId: string): void {
this.campaignService.getCampaignById(campaignId).subscribe({
next: (campaign) => {
if (!campaign.gameSystemId) {
this.templateFields = [];
return;
}
this.gameSystemService.getById(campaign.gameSystemId).subscribe({
next: (gs) => { this.templateFields = gs.npcTemplate ?? []; },
error: () => { this.templateFields = []; }
});
},
error: () => { this.templateFields = []; }
});
}
submit(): void {
if (!this.name.trim() || !this.campaignId) return;
const payload = {
name: this.name.trim(),
portraitImageId: this.portraitImageId.trim() || null,
headerImageId: this.headerImageId.trim() || null,
values: this.values,
imageValues: this.imageValues,
campaignId: this.campaignId
};
const req = this.npcId
? this.service.update(this.npcId, {
id: this.npcId,
name: this.name.trim(),
markdownContent: this.markdownContent || null,
campaignId: this.campaignId,
order: this.order
})
: this.service.create({
name: this.name.trim(),
markdownContent: this.markdownContent || null,
campaignId: this.campaignId
});
? this.service.update(this.npcId, { ...payload, id: this.npcId, order: this.order })
: this.service.create(payload);
req.subscribe({
next: () => this.back(),
error: () => console.error('Erreur sauvegarde Npc')
@@ -92,7 +119,7 @@ export class NpcEditComponent implements OnInit {
deleteNpc(): void {
if (!this.npcId) return;
if (!confirm(`Supprimer la fiche de "${this.name}" ? Cette action est irréversible.`)) return;
if (!confirm(`Supprimer la fiche de "${this.name}" ? Cette action est irreversible.`)) return;
this.service.delete(this.npcId).subscribe({
next: () => this.back(),
error: () => console.error('Erreur suppression Npc')