Ajout d'un mode "jeu" (possibilité de lancer des sessions dans une campagne). Cela permet de faire de prendre des notes en live au cours d'une partie et d'avoir plusieurs outils sous la main pour aider le mj :
All checks were successful
Build & Push Images / build (brain) (push) Successful in 1m20s
Build & Push Images / build (core) (push) Successful in 1m50s
Build & Push Images / build-switcher (push) Successful in 18s
Build & Push Images / build (web) (push) Successful in 1m47s

- Possibilité de parler à une IA pour règle de jeu ou élément de lore / campagne au cours d'une partie comme aide mémoire
- Onglet dédié aux personnages de la campagne
- Onglet dédié aux scènes
- Onglet avec dès pour ceux qui souhaitent ;

Possibilité de rajouté une note en tant qu'évènement, jet de dès ou encore action du joueur par exemple. D'autres ajouts seront fait dans le futur (notamment des tables aléatoires pour PNJ en live).
This commit is contained in:
2026-05-20 14:59:26 +02:00
parent 87865338a0
commit 694f687fec
53 changed files with 3614 additions and 17 deletions

View File

@@ -0,0 +1,138 @@
import { Component, EventEmitter, Input, OnChanges, Output, SimpleChanges } from '@angular/core';
import { CommonModule } from '@angular/common';
import { LucideAngularModule, User, Drama, Swords, Dices, ExternalLink, Sparkles } from 'lucide-angular';
import { catchError, of } from 'rxjs';
import { CampaignService } from '../../services/campaign.service';
import { CharacterService } from '../../services/character.service';
import { NpcService } from '../../services/npc.service';
import { Character } from '../../services/character.model';
import { Npc } from '../../services/npc.model';
import { Arc, Chapter, Scene } from '../../services/campaign.model';
import { loadCampaignTreeData, CampaignTreeData } from '../../campaigns/campaign-tree.helper';
import {
SessionDicePanelComponent, DiceRollResult
} from '../session-dice-panel/session-dice-panel.component';
import { SessionAiChatPanelComponent } from '../session-ai-chat-panel/session-ai-chat-panel.component';
type TabId = 'dice' | 'characters' | 'scenes' | 'ai';
/**
* Panneau latéral du mode jeu : référence rapide en lecture seule.
*
* <p>Charge à la volée les PJ/PNJ et l'arbre de scènes de la campagne associée
* à la session. La navigation vers les fiches s'ouvre dans un nouvel onglet
* pour ne pas casser le flux de la session en cours.</p>
*
* <p>Le sous-composant {@link SessionDicePanelComponent} émet un événement
* de jet qui remonte ici puis vers le parent via {@link rolled}.</p>
*/
@Component({
selector: 'app-session-reference-panel',
standalone: true,
imports: [CommonModule, LucideAngularModule, SessionDicePanelComponent, SessionAiChatPanelComponent],
templateUrl: './session-reference-panel.component.html',
styleUrls: ['./session-reference-panel.component.scss']
})
export class SessionReferencePanelComponent implements OnChanges {
readonly User = User;
readonly Drama = Drama;
readonly Swords = Swords;
readonly Dices = Dices;
readonly ExternalLink = ExternalLink;
readonly Sparkles = Sparkles;
@Input() campaignId!: string;
@Input() sessionId!: string;
@Input() canAddToJournal = true;
@Output() rolled = new EventEmitter<DiceRollResult>();
/** Émis quand l'IA répond et que le MJ veut sauvegarder la réponse comme entrée. */
@Output() aiReplyToJournal = new EventEmitter<string>();
activeTab: TabId = 'dice';
characters: Character[] = [];
npcs: Npc[] = [];
treeData: CampaignTreeData | null = null;
loadingChars = false;
loadingTree = false;
/** True dès qu'un tab "lourd" a été chargé pour éviter de rappeler l'API en boucle. */
private charsLoaded = false;
private treeLoaded = false;
constructor(
private campaignService: CampaignService,
private characterService: CharacterService,
private npcService: NpcService
) {}
ngOnChanges(changes: SimpleChanges): void {
if (changes['campaignId']) {
this.charsLoaded = false;
this.treeLoaded = false;
this.characters = [];
this.npcs = [];
this.treeData = null;
}
}
selectTab(tab: TabId): void {
this.activeTab = tab;
if (tab === 'characters') this.ensureCharactersLoaded();
if (tab === 'scenes') this.ensureTreeLoaded();
}
private ensureCharactersLoaded(): void {
if (this.charsLoaded || this.loadingChars || !this.campaignId) return;
this.loadingChars = true;
this.characterService.getByCampaign(this.campaignId).pipe(catchError(() => of([] as Character[])))
.subscribe(list => { this.characters = list; this.tryFinishCharsLoad(); });
this.npcService.getByCampaign(this.campaignId).pipe(catchError(() => of([] as Npc[])))
.subscribe(list => { this.npcs = list; this.tryFinishCharsLoad(); });
}
private tryFinishCharsLoad(): void {
// On considère que le chargement est fini quand au moins une des deux listes
// a été assignée (vide ou pleine). Le double subscribe ci-dessus garantit
// qu'on tombe ici deux fois ; idempotent.
this.loadingChars = false;
this.charsLoaded = true;
}
private ensureTreeLoaded(): void {
if (this.treeLoaded || this.loadingTree || !this.campaignId) return;
this.loadingTree = true;
loadCampaignTreeData(this.campaignService, this.campaignId, this.characterService, this.npcService).pipe(
catchError(() => of({ arcs: [], chaptersByArc: {}, scenesByChapter: {}, characters: [], npcs: [] } as CampaignTreeData))
).subscribe(data => {
this.treeData = data;
this.loadingTree = false;
this.treeLoaded = true;
});
}
/**
* Ouvre une fiche dans un nouvel onglet pour préserver l'écran de session.
* Le MJ peut consulter sans perdre son journal ni son historique de dés.
*/
openInNewTab(path: (string | number)[]): void {
const url = path.map(p => String(p)).join('/');
window.open('/' + url, '_blank', 'noopener');
}
/** Helpers de typage pour le template (Angular n'infère pas bien sans). */
chaptersOf(arc: Arc): Chapter[] {
return this.treeData?.chaptersByArc[arc.id!] ?? [];
}
scenesOf(chapter: Chapter): Scene[] {
return this.treeData?.scenesByChapter[chapter.id!] ?? [];
}
onDiceRolled(result: DiceRollResult): void {
this.rolled.emit(result);
}
onAiSaveToJournal(content: string): void {
this.aiReplyToJournal.emit(content);
}
}