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:
@@ -0,0 +1,79 @@
|
||||
<div class="prereq-editor">
|
||||
|
||||
<p class="prereq-editor-empty" *ngIf="prerequisites.length === 0">
|
||||
Aucune condition — la quête est immédiatement disponible.
|
||||
</p>
|
||||
|
||||
<ul class="prereq-list" *ngIf="prerequisites.length > 0">
|
||||
<li class="prereq-row" *ngFor="let p of prerequisites; let i = index; trackBy: trackByIndex">
|
||||
|
||||
<ng-container [ngSwitch]="p.kind">
|
||||
|
||||
<!-- QUEST_COMPLETED -->
|
||||
<ng-container *ngSwitchCase="'QUEST_COMPLETED'">
|
||||
<span class="prereq-row-label">Quête terminée :</span>
|
||||
<select class="prereq-input"
|
||||
[ngModel]="asQuestCompleted(p).questId"
|
||||
(ngModelChange)="onQuestIdChange(i, $event)"
|
||||
[ngModelOptions]="{ standalone: true }"
|
||||
[disabled]="availableQuests.length === 0">
|
||||
<option *ngIf="availableQuests.length === 0" value="">(aucune autre quête)</option>
|
||||
<option *ngFor="let q of availableQuests" [value]="q.id">{{ q.name }}</option>
|
||||
</select>
|
||||
</ng-container>
|
||||
|
||||
<!-- SESSION_REACHED -->
|
||||
<ng-container *ngSwitchCase="'SESSION_REACHED'">
|
||||
<span class="prereq-row-label">À partir de la session :</span>
|
||||
<input type="number"
|
||||
class="prereq-input prereq-input-number"
|
||||
min="1"
|
||||
[ngModel]="asSessionReached(p).minSessionNumber"
|
||||
(ngModelChange)="onMinSessionChange(i, $event)"
|
||||
[ngModelOptions]="{ standalone: true }" />
|
||||
</ng-container>
|
||||
|
||||
<!-- FLAG_SET -->
|
||||
<ng-container *ngSwitchCase="'FLAG_SET'">
|
||||
<span class="prereq-row-label">Quand le fait est vrai :</span>
|
||||
<input type="text"
|
||||
class="prereq-input"
|
||||
placeholder="nom_du_fait"
|
||||
[attr.list]="'prereq-flag-list-' + i"
|
||||
[ngModel]="asFlagSet(p).flagName"
|
||||
(ngModelChange)="onFlagNameChange(i, $event)"
|
||||
[ngModelOptions]="{ standalone: true }" />
|
||||
<datalist [attr.id]="'prereq-flag-list-' + i">
|
||||
<option *ngFor="let f of availableFlags" [value]="f"></option>
|
||||
</datalist>
|
||||
</ng-container>
|
||||
|
||||
</ng-container>
|
||||
|
||||
<button type="button" class="prereq-remove" (click)="removeAt(i)" title="Retirer ce prérequis">
|
||||
<lucide-icon [img]="Trash2" [size]="14"></lucide-icon>
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<div class="prereq-add">
|
||||
<button type="button" class="btn-secondary prereq-add-btn" (click)="toggleAddMenu()">
|
||||
<lucide-icon [img]="Plus" [size]="14"></lucide-icon>
|
||||
Ajouter une condition
|
||||
<lucide-icon [img]="ChevronDown" [size]="14"></lucide-icon>
|
||||
</button>
|
||||
|
||||
<div class="prereq-add-menu" *ngIf="addMenuOpen">
|
||||
<button type="button" (click)="addPrerequisite('QUEST_COMPLETED')">
|
||||
Après une autre quête
|
||||
</button>
|
||||
<button type="button" (click)="addPrerequisite('SESSION_REACHED')">
|
||||
À partir d'une session
|
||||
</button>
|
||||
<button type="button" (click)="addPrerequisite('FLAG_SET')">
|
||||
Quand un fait est vrai
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
@@ -0,0 +1,108 @@
|
||||
.prereq-editor {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.6rem;
|
||||
}
|
||||
|
||||
.prereq-editor-empty {
|
||||
margin: 0;
|
||||
font-size: 0.85rem;
|
||||
color: var(--color-text-muted, #777);
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.prereq-list {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.45rem;
|
||||
}
|
||||
|
||||
.prereq-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0.45rem 0.6rem;
|
||||
border: 1px solid var(--color-border, #e2e2e2);
|
||||
border-radius: 8px;
|
||||
background: var(--color-surface, #fff);
|
||||
}
|
||||
|
||||
.prereq-row-label {
|
||||
font-size: 0.85rem;
|
||||
color: var(--color-text-muted, #555);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.prereq-input {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
padding: 0.3rem 0.5rem;
|
||||
border: 1px solid var(--color-border, #d8d8d8);
|
||||
border-radius: 6px;
|
||||
font-size: 0.88rem;
|
||||
background: var(--color-bg, #fafafa);
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
border-color: var(--color-primary, #2c6cd6);
|
||||
}
|
||||
}
|
||||
|
||||
.prereq-input-number {
|
||||
flex: 0 0 90px;
|
||||
}
|
||||
|
||||
.prereq-remove {
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: var(--color-danger, #c0392b);
|
||||
cursor: pointer;
|
||||
padding: 0.3rem;
|
||||
border-radius: 6px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
&:hover { background: rgba(192, 57, 43, 0.08); }
|
||||
}
|
||||
|
||||
.prereq-add {
|
||||
position: relative;
|
||||
align-self: flex-start;
|
||||
}
|
||||
|
||||
.prereq-add-btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.35rem;
|
||||
padding: 0.4rem 0.7rem;
|
||||
}
|
||||
|
||||
.prereq-add-menu {
|
||||
position: absolute;
|
||||
top: calc(100% + 4px);
|
||||
left: 0;
|
||||
z-index: 5;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-width: 220px;
|
||||
background: var(--color-surface, #fff);
|
||||
border: 1px solid var(--color-border, #e2e2e2);
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
|
||||
overflow: hidden;
|
||||
|
||||
button {
|
||||
background: transparent;
|
||||
border: none;
|
||||
text-align: left;
|
||||
padding: 0.55rem 0.75rem;
|
||||
font-size: 0.88rem;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover { background: rgba(66, 133, 244, 0.08); }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
import { Component, EventEmitter, Input, Output } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { LucideAngularModule, Plus, Trash2, ChevronDown } from 'lucide-angular';
|
||||
import { Chapter, Prerequisite } from '../../services/campaign.model';
|
||||
|
||||
/**
|
||||
* Éditeur des prérequis (conditions de déblocage) d'une quête.
|
||||
* Composant standalone & contrôlé : reçoit la liste, émet la nouvelle liste à chaque changement.
|
||||
*
|
||||
* Combinaison ET (MVP). UI : liste de lignes + bouton "Ajouter une condition" qui ouvre
|
||||
* un menu à 3 entrées (quête, session, fait).
|
||||
*/
|
||||
@Component({
|
||||
selector: 'app-prerequisite-editor',
|
||||
standalone: true,
|
||||
imports: [CommonModule, FormsModule, LucideAngularModule],
|
||||
templateUrl: './prerequisite-editor.component.html',
|
||||
styleUrls: ['./prerequisite-editor.component.scss']
|
||||
})
|
||||
export class PrerequisiteEditorComponent {
|
||||
/** Liste courante. */
|
||||
@Input() prerequisites: Prerequisite[] = [];
|
||||
|
||||
/** Quêtes candidates pour QUEST_COMPLETED (typiquement les chapitres frères du Hub). */
|
||||
@Input() availableQuests: Chapter[] = [];
|
||||
|
||||
/** Flags déjà connus de la campagne (autocomplete pour FLAG_SET). */
|
||||
@Input() availableFlags: string[] = [];
|
||||
|
||||
/** Émis chaque fois que la liste change (toute édition / ajout / suppression). */
|
||||
@Output() prerequisitesChange = new EventEmitter<Prerequisite[]>();
|
||||
|
||||
readonly Plus = Plus;
|
||||
readonly Trash2 = Trash2;
|
||||
readonly ChevronDown = ChevronDown;
|
||||
|
||||
/** Ouvre/ferme le menu d'ajout. */
|
||||
addMenuOpen = false;
|
||||
|
||||
toggleAddMenu(): void { this.addMenuOpen = !this.addMenuOpen; }
|
||||
|
||||
addPrerequisite(kind: Prerequisite['kind']): void {
|
||||
let next: Prerequisite;
|
||||
switch (kind) {
|
||||
case 'QUEST_COMPLETED':
|
||||
next = { kind: 'QUEST_COMPLETED', questId: this.availableQuests[0]?.id ?? '' };
|
||||
break;
|
||||
case 'SESSION_REACHED':
|
||||
next = { kind: 'SESSION_REACHED', minSessionNumber: 1 };
|
||||
break;
|
||||
case 'FLAG_SET':
|
||||
next = { kind: 'FLAG_SET', flagName: this.availableFlags[0] ?? '' };
|
||||
break;
|
||||
}
|
||||
this.prerequisites = [...this.prerequisites, next];
|
||||
this.prerequisitesChange.emit(this.prerequisites);
|
||||
this.addMenuOpen = false;
|
||||
}
|
||||
|
||||
removeAt(index: number): void {
|
||||
this.prerequisites = this.prerequisites.filter((_, i) => i !== index);
|
||||
this.prerequisitesChange.emit(this.prerequisites);
|
||||
}
|
||||
|
||||
/** Le champ binding change : on émet une nouvelle liste (immutable update). */
|
||||
updateAt(index: number, patched: Prerequisite): void {
|
||||
this.prerequisites = this.prerequisites.map((p, i) => (i === index ? patched : p));
|
||||
this.prerequisitesChange.emit(this.prerequisites);
|
||||
}
|
||||
|
||||
// === Handlers spécifiques par type (pour garder les templates simples) ===
|
||||
|
||||
onQuestIdChange(index: number, questId: string): void {
|
||||
this.updateAt(index, { kind: 'QUEST_COMPLETED', questId });
|
||||
}
|
||||
onMinSessionChange(index: number, n: number): void {
|
||||
this.updateAt(index, { kind: 'SESSION_REACHED', minSessionNumber: Number(n) || 1 });
|
||||
}
|
||||
onFlagNameChange(index: number, flagName: string): void {
|
||||
this.updateAt(index, { kind: 'FLAG_SET', flagName });
|
||||
}
|
||||
|
||||
/** trackBy stable pour *ngFor (sinon les inputs reset à chaque édition). */
|
||||
trackByIndex(i: number): number { return i; }
|
||||
|
||||
// === Casts typés pour le template ===
|
||||
// Angular strict templates ne fait pas le narrowing d'une union discriminée à travers
|
||||
// *ngSwitchCase. Ces helpers castent vers la bonne variante pour rendre les inputs lisibles.
|
||||
asQuestCompleted(p: Prerequisite): { kind: 'QUEST_COMPLETED'; questId: string } {
|
||||
return p as { kind: 'QUEST_COMPLETED'; questId: string };
|
||||
}
|
||||
asSessionReached(p: Prerequisite): { kind: 'SESSION_REACHED'; minSessionNumber: number } {
|
||||
return p as { kind: 'SESSION_REACHED'; minSessionNumber: number };
|
||||
}
|
||||
asFlagSet(p: Prerequisite): { kind: 'FLAG_SET'; flagName: string } {
|
||||
return p as { kind: 'FLAG_SET'; flagName: string };
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user