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. * *

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.

* *

Le sous-composant {@link SessionDicePanelComponent} émet un événement * de jet qui remonte ici puis vers le parent via {@link rolled}.

*/ @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(); /** Émis quand l'IA répond et que le MJ veut sauvegarder la réponse comme entrée. */ @Output() aiReplyToJournal = new EventEmitter(); 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); } }