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