Changement sur le Readme
Ajout d'une partie spécifique pour des PNJ dans la partie campagne
This commit is contained in:
@@ -0,0 +1,83 @@
|
||||
<div class="modal-backdrop" (click)="onCancel()">
|
||||
<div class="modal" (click)="$event.stopPropagation()">
|
||||
|
||||
<div class="modal-header">
|
||||
<h2>Créer une nouvelle Campagne</h2>
|
||||
<button class="btn-close" (click)="onCancel()">
|
||||
<lucide-icon [img]="X" [size]="18"></lucide-icon>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<form [formGroup]="form" (ngSubmit)="submit()">
|
||||
|
||||
<div class="field">
|
||||
<label for="campaign-name">Nom de la campagne *</label>
|
||||
<input
|
||||
id="campaign-name"
|
||||
type="text"
|
||||
formControlName="name"
|
||||
placeholder="Ex: L'Ombre du Nord, Les Héritiers Oubliés..."
|
||||
[class.invalid]="form.get('name')?.invalid && form.get('name')?.touched"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label for="campaign-description">Description / Pitch</label>
|
||||
<textarea
|
||||
id="campaign-description"
|
||||
formControlName="description"
|
||||
placeholder="Résumez l'intrigue principale de votre campagne..."
|
||||
rows="5"
|
||||
></textarea>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label for="campaign-player-count">Nombre de joueurs</label>
|
||||
<input id="campaign-player-count" type="number" formControlName="playerCount" min="1" />
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label for="campaign-lore">Univers associé</label>
|
||||
<select id="campaign-lore" formControlName="loreId">
|
||||
<option value="">— Aucun univers (campagne libre) —</option>
|
||||
<option *ngFor="let lore of availableLores" [value]="lore.id">{{ lore.name }}</option>
|
||||
</select>
|
||||
<p class="hint">
|
||||
Optionnel. Si associée, vous pourrez lier arcs, chapitres et scènes aux pages du Lore.
|
||||
Laissez vide pour un one-shot ou si vous créerez le Lore plus tard.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label for="campaign-game-system">Système de JDR</label>
|
||||
<select id="campaign-game-system" formControlName="gameSystemId">
|
||||
<option value="">— Aucun (campagne générique) —</option>
|
||||
<option *ngFor="let gs of availableGameSystems" [value]="gs.id">{{ gs.name }}</option>
|
||||
</select>
|
||||
<p class="hint">
|
||||
Optionnel. Si défini, l'IA injectera les règles du système (classes, combat, lore...)
|
||||
dans ses suggestions pour respecter les mécaniques du JDR.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="info-box">
|
||||
<p><strong>💡 Organisation :</strong> Votre campagne sera structurée en :</p>
|
||||
<ul>
|
||||
<li><strong>Arcs</strong> - Les grandes phases narratives</li>
|
||||
<li><strong>Chapitres</strong> - Les segments d'un arc</li>
|
||||
<li><strong>Scènes</strong> - Les moments de jeu individuels</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="modal-actions">
|
||||
<button type="submit" class="btn-primary" [disabled]="form.invalid">
|
||||
<lucide-icon [img]="BookCopy" [size]="16"></lucide-icon>
|
||||
Créer la campagne
|
||||
</button>
|
||||
<button type="button" class="btn-secondary" (click)="onCancel()">Annuler</button>
|
||||
</div>
|
||||
|
||||
</form>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,149 @@
|
||||
.modal-backdrop {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background: rgba(0, 0, 0, 0.7);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
.modal {
|
||||
background: #111827;
|
||||
border: 1px solid #1f2937;
|
||||
border-radius: 16px;
|
||||
padding: 2rem;
|
||||
width: 100%;
|
||||
max-width: 600px;
|
||||
// Le contenu peut dépasser la hauteur de l'écran (formulaire long) :
|
||||
// on borne la modale et on fait scroller l'intérieur en flex-column.
|
||||
max-height: 90vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.modal-header { flex-shrink: 0; }
|
||||
|
||||
form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 0;
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
// Marge interne pour que la scrollbar ne colle pas aux inputs.
|
||||
margin-right: -0.5rem;
|
||||
padding-right: 0.5rem;
|
||||
}
|
||||
|
||||
.modal-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 2rem;
|
||||
|
||||
h2 { color: white; font-size: 1.25rem; font-weight: 600; }
|
||||
}
|
||||
|
||||
.btn-close {
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: #6b7280;
|
||||
cursor: pointer;
|
||||
padding: 0.25rem;
|
||||
border-radius: 6px;
|
||||
display: flex;
|
||||
transition: color 0.2s;
|
||||
|
||||
&:hover { color: white; }
|
||||
}
|
||||
|
||||
.field {
|
||||
margin-bottom: 1.5rem;
|
||||
|
||||
label {
|
||||
display: block;
|
||||
font-size: 0.875rem;
|
||||
color: #d1d5db;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
input, textarea {
|
||||
width: 100%;
|
||||
background: #1f2937;
|
||||
border: 1px solid #374151;
|
||||
border-radius: 8px;
|
||||
padding: 0.75rem 1rem;
|
||||
color: white;
|
||||
font-size: 0.9rem;
|
||||
outline: none;
|
||||
resize: none;
|
||||
transition: border-color 0.2s;
|
||||
|
||||
&::placeholder { color: #4b5563; }
|
||||
&:focus { border-color: #6c63ff; }
|
||||
&.invalid { border-color: #ef4444; }
|
||||
}
|
||||
|
||||
input[type="number"] { width: 120px; }
|
||||
}
|
||||
|
||||
.info-box {
|
||||
background: #1f2937;
|
||||
border-radius: 8px;
|
||||
padding: 1rem 1.25rem;
|
||||
margin-bottom: 2rem;
|
||||
font-size: 0.875rem;
|
||||
color: #9ca3af;
|
||||
line-height: 1.8;
|
||||
|
||||
ul {
|
||||
margin: 0.5rem 0 0 1.25rem;
|
||||
li strong { color: #d1d5db; }
|
||||
}
|
||||
}
|
||||
|
||||
.modal-actions {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
// Actions collées en bas du scroll : visibles même si on n'a pas défilé
|
||||
// jusqu'en bas du formulaire.
|
||||
position: sticky;
|
||||
bottom: 0;
|
||||
background: #111827;
|
||||
padding-top: 1rem;
|
||||
margin-top: auto;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0.75rem 1.5rem;
|
||||
background: #6c63ff;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
font-size: 0.9rem;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: background 0.2s;
|
||||
|
||||
&:hover:not(:disabled) { background: #5b52e0; }
|
||||
&:disabled { opacity: 0.4; cursor: not-allowed; }
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
padding: 0.75rem 1.5rem;
|
||||
background: #1f2937;
|
||||
color: #d1d5db;
|
||||
border: 1px solid #374151;
|
||||
border-radius: 8px;
|
||||
font-size: 0.9rem;
|
||||
cursor: pointer;
|
||||
transition: background 0.2s;
|
||||
|
||||
&:hover { background: #374151; }
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
import { Component, EventEmitter, OnInit, Output } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { ReactiveFormsModule, FormBuilder, FormGroup, Validators } from '@angular/forms';
|
||||
import { LucideAngularModule, BookCopy, X } from 'lucide-angular';
|
||||
import { LoreService } from '../../../services/lore.service';
|
||||
import { Lore } from '../../../services/lore.model';
|
||||
import { GameSystemService } from '../../../services/game-system.service';
|
||||
import { GameSystem } from '../../../services/game-system.model';
|
||||
|
||||
/**
|
||||
* Payload émis vers le parent à la création d'une campagne.
|
||||
* `loreId` et `gameSystemId` sont optionnels (null = non associé).
|
||||
*/
|
||||
export interface CampaignCreatePayload {
|
||||
name: string;
|
||||
description: string;
|
||||
playerCount: number;
|
||||
loreId: string | null;
|
||||
gameSystemId: string | null;
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'app-campaign-create',
|
||||
standalone: true,
|
||||
imports: [CommonModule, ReactiveFormsModule, LucideAngularModule],
|
||||
templateUrl: './campaign-create.component.html',
|
||||
styleUrls: ['./campaign-create.component.scss']
|
||||
})
|
||||
export class CampaignCreateComponent implements OnInit {
|
||||
@Output() close = new EventEmitter<void>();
|
||||
@Output() created = new EventEmitter<CampaignCreatePayload>();
|
||||
|
||||
readonly BookCopy = BookCopy;
|
||||
readonly X = X;
|
||||
|
||||
form: FormGroup;
|
||||
/** Lores disponibles pour association. Chargés à l'ouverture de la modal. */
|
||||
availableLores: Lore[] = [];
|
||||
/** GameSystems disponibles pour association. */
|
||||
availableGameSystems: GameSystem[] = [];
|
||||
|
||||
constructor(
|
||||
private fb: FormBuilder,
|
||||
private loreService: LoreService,
|
||||
private gameSystemService: GameSystemService
|
||||
) {
|
||||
this.form = this.fb.group({
|
||||
name: ['', Validators.required],
|
||||
description: [''],
|
||||
playerCount: [4, [Validators.required, Validators.min(1)]],
|
||||
loreId: [''],
|
||||
gameSystemId: ['']
|
||||
});
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.loreService.getAllLores().subscribe({
|
||||
next: (lores) => this.availableLores = lores,
|
||||
error: () => this.availableLores = []
|
||||
});
|
||||
this.gameSystemService.getAll().subscribe({
|
||||
next: (gs) => this.availableGameSystems = gs,
|
||||
error: () => this.availableGameSystems = []
|
||||
});
|
||||
}
|
||||
|
||||
submit(): void {
|
||||
if (this.form.invalid) return;
|
||||
const raw = this.form.value;
|
||||
this.created.emit({
|
||||
name: raw.name,
|
||||
description: raw.description,
|
||||
playerCount: raw.playerCount,
|
||||
loreId: raw.loreId ? raw.loreId : null,
|
||||
gameSystemId: raw.gameSystemId ? raw.gameSystemId : null
|
||||
});
|
||||
}
|
||||
|
||||
onCancel(): void {
|
||||
this.close.emit();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user