Co-MJ v1 : quêtes de première classe, guidage de préparation, assistant IA,
All checks were successful
All checks were successful
mode séance, battlemaps multiples et export Foundry ciblé
- Quêtes (Niveau 1) : entité Quest orthogonale à l'arbre, rattachée à un arc HUB
ou libre (migrations V9-V12, V18, V20 ; réconciliation des jumeaux V10).
Fusion quête/conteneur dans la sidebar, progression par partie (statuts
disponible/en cours/terminée), et quêtes libres avec espace de scènes créé
automatiquement (arc technique « Quêtes libres », V21 : levée de la
contrainte arcs.type héritée du baseline).
- Guidage (Pilier B) : bilan de préparation 100 % dérivé (règles arc/chapitre/
scène/quête), panneau « Prochaines étapes » avec boutons « Corriger », et
pastilles détaillées (tooltip des manques) dans l'arbre.
- Assistant IA (Pilier A) : étoffage champ par champ (scène, chapitre, arc) et
brouillons de scènes en propose→applique (brain : narrative-fields,
scene-drafts).
- Horloges & menaces (V15-V17) : clocks à segments avec déclencheurs, fronts.
- Mode séance : préparation de séance (readiness + quêtes dispo), scène
épinglée (V19), récap « précédemment » (brain : session-recap), onglet
« Partie » du panneau de référence, graphe amélioré (pan/zoom, éditeur de
liens).
- Perf : endpoint agrégé GET /api/campaigns/{id}/tree — la sidebar charge en
1 requête au lieu de ~15.
- Battlemaps multiples par scène (variantes jour/nuit, étages…) : liste JSON
étiquetée (V22, reprise automatique de la carte existante), rendu PDF avec
légendes, export/import rétro-compatible.
- Export Foundry ciblé : modale de périmètre (cartes+ennemis / journaux /
tables), bundle filtré côté serveur (zip allégé), module Foundry à jour
(respect du périmètre + une Scene Foundry par variante de carte,
rétro-compatible anciens bundles).
This commit is contained in:
@@ -0,0 +1,130 @@
|
||||
@if (report) {
|
||||
<div class="prep-panel">
|
||||
|
||||
<!-- Dernière séance -->
|
||||
@if (report.lastSession; as last) {
|
||||
<p class="prep-last">
|
||||
<lucide-icon [img]="CalendarCheck" [size]="14"></lucide-icon>
|
||||
{{ (last.active ? 'sessionPrep.lastSessionActive' : 'sessionPrep.lastSessionEnded') | translate:{ name: last.name } }}
|
||||
</p>
|
||||
}
|
||||
|
||||
@if (allQuiet) {
|
||||
<p class="prep-quiet">
|
||||
<lucide-icon [img]="CheckCircle2" [size]="15"></lucide-icon>
|
||||
{{ 'sessionPrep.allQuiet' | translate }}
|
||||
</p>
|
||||
}
|
||||
|
||||
<!-- Où en sont les joueurs -->
|
||||
@if (report.questsInProgress.length > 0 || report.questsAvailable.length > 0) {
|
||||
<div class="prep-section">
|
||||
<h3><lucide-icon [img]="Compass" [size]="14"></lucide-icon> {{ 'sessionPrep.positionTitle' | translate }}</h3>
|
||||
@if (report.questsInProgress.length > 0) {
|
||||
<div class="prep-chip-row">
|
||||
<span class="prep-chip-label">{{ 'sessionPrep.inProgress' | translate }}</span>
|
||||
@for (q of report.questsInProgress; track q.id) {
|
||||
<span class="prep-chip-group">
|
||||
<a class="prep-chip in-progress" [routerLink]="['/campaigns', campaignId, 'quests', q.id]">{{ q.name }}</a>
|
||||
<button type="button" class="chip-action" (click)="completeQuest(q)"
|
||||
[title]="'sessionPrep.markCompleted' | translate">
|
||||
<lucide-icon [img]="Check" [size]="12"></lucide-icon>
|
||||
</button>
|
||||
</span>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
@if (report.questsAvailable.length > 0) {
|
||||
<div class="prep-chip-row">
|
||||
<span class="prep-chip-label">{{ 'sessionPrep.available' | translate }}</span>
|
||||
@for (q of report.questsAvailable; track q.id) {
|
||||
<span class="prep-chip-group">
|
||||
<a class="prep-chip available" [routerLink]="['/campaigns', campaignId, 'quests', q.id]">{{ q.name }}</a>
|
||||
<button type="button" class="chip-action" (click)="startQuest(q)"
|
||||
[title]="'sessionPrep.markStarted' | translate">
|
||||
<lucide-icon [img]="Play" [size]="12"></lucide-icon>
|
||||
</button>
|
||||
<button type="button" class="chip-action" (click)="completeQuest(q)"
|
||||
[title]="'sessionPrep.markCompleted' | translate">
|
||||
<lucide-icon [img]="Check" [size]="12"></lucide-icon>
|
||||
</button>
|
||||
</span>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
|
||||
<!-- Quêtes terminées : rappel discret + rouvrir en cas d'erreur -->
|
||||
@if (report.questsCompleted.length > 0) {
|
||||
<div class="prep-chip-row prep-completed">
|
||||
<span class="prep-chip-label">{{ 'sessionPrep.completed' | translate }}</span>
|
||||
@for (q of report.questsCompleted; track q.id) {
|
||||
<span class="prep-chip-group">
|
||||
<a class="prep-chip done" [routerLink]="['/campaigns', campaignId, 'quests', q.id]">{{ q.name }}</a>
|
||||
<button type="button" class="chip-action" (click)="reopenQuest(q)"
|
||||
[title]="'sessionPrep.reopen' | translate">
|
||||
<lucide-icon [img]="RotateCcw" [size]="12"></lucide-icon>
|
||||
</button>
|
||||
</span>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
|
||||
<!-- Contenu probable de la prochaine séance -->
|
||||
@if (report.hotspots.length > 0) {
|
||||
<div class="prep-section">
|
||||
<h3><lucide-icon [img]="MapPin" [size]="14"></lucide-icon> {{ 'sessionPrep.hotspotsTitle' | translate }}</h3>
|
||||
<div class="prep-chip-row">
|
||||
@for (node of report.hotspots; track node.nodeType + node.id) {
|
||||
<a class="prep-chip node" [routerLink]="nodeLink(node)">
|
||||
{{ node.name }}
|
||||
<span class="node-kind">{{ ('sessionPrep.nodeKind.' + node.nodeType) | translate }}</span>
|
||||
</a>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
<!-- À combler avant de jouer -->
|
||||
@if (report.gaps.length > 0) {
|
||||
<div class="prep-section">
|
||||
<h3 class="warn"><lucide-icon [img]="AlertTriangle" [size]="14"></lucide-icon> {{ 'sessionPrep.gapsTitle' | translate }}</h3>
|
||||
<ul class="prep-gap-list">
|
||||
@for (gap of report.gaps; track gap.ruleId + gap.entityId) {
|
||||
<li class="prep-gap" [class.blocking]="gap.severity === 'BLOCKING'">
|
||||
<span class="gap-msg">{{ gap.message }}</span>
|
||||
<a class="gap-cta" [routerLink]="gapLink(gap)" [queryParams]="gapQuery(gap)">
|
||||
{{ 'sessionPrep.fix' | translate }}
|
||||
<lucide-icon [img]="ArrowRight" [size]="12"></lucide-icon>
|
||||
</a>
|
||||
</li>
|
||||
}
|
||||
</ul>
|
||||
</div>
|
||||
}
|
||||
@if (report.otherGapCount > 0) {
|
||||
<p class="prep-other">
|
||||
<a [routerLink]="['/campaigns', campaignId]">{{ 'sessionPrep.otherGaps' | translate:{ n: report.otherGapCount } }}</a>
|
||||
</p>
|
||||
}
|
||||
|
||||
<!-- Menaces en mouvement -->
|
||||
@if (report.clocks.length > 0) {
|
||||
<div class="prep-section">
|
||||
<h3><lucide-icon [img]="Timer" [size]="14"></lucide-icon> {{ 'sessionPrep.clocksTitle' | translate }}</h3>
|
||||
<div class="prep-chip-row">
|
||||
@for (clock of report.clocks; track clock.id) {
|
||||
<span class="prep-clock" [class.nearly]="isNearlyFull(clock)" [class.full]="isFull(clock)">
|
||||
{{ clock.name }} — {{ clock.filled }}/{{ clock.segments }}
|
||||
@if (clock.frontName) { <span class="clock-front">({{ clock.frontName }})</span> }
|
||||
@if (isFull(clock)) { <strong>{{ 'sessionPrep.clockFull' | translate }}</strong> }
|
||||
@else if (isNearlyFull(clock)) { <strong>{{ 'sessionPrep.clockNearly' | translate }}</strong> }
|
||||
</span>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
</div>
|
||||
}
|
||||
Reference in New Issue
Block a user