Plusieurs ajouts :
Some checks failed
Build & Push Images / build (brain) (push) Successful in 1m47s
Build & Push Images / build (core) (push) Failing after 1m56s
Build & Push Images / build-switcher (push) Successful in 20s
Build & Push Images / build (web) (push) Successful in 2m11s

- 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:
2026-06-03 15:44:48 +02:00
parent 694f687fec
commit c3d3394ec7
147 changed files with 5347 additions and 392 deletions

View File

@@ -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>

View File

@@ -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); }
}
}

View File

@@ -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 };
}
}