301 lines
12 KiB
HTML
301 lines
12 KiB
HTML
<div class="edit-page">
|
|
|
|
<div class="page-header">
|
|
<div>
|
|
<h1>{{ scene?.name || ('sceneEdit.defaultTitle' | translate) }}</h1>
|
|
<p class="subtitle">{{ 'sceneEdit.subtitle' | translate }}</p>
|
|
</div>
|
|
<div class="header-actions">
|
|
<button type="button" class="btn-ai"
|
|
(click)="toggleChat()"
|
|
[class.active]="chatOpen"
|
|
[title]="'sceneEdit.aiAssistantTitle' | translate">
|
|
<lucide-icon [img]="Sparkles" [size]="14"></lucide-icon>
|
|
{{ 'sceneEdit.aiAssistant' | translate }}
|
|
</button>
|
|
<button type="button" class="btn-secondary" (click)="cancel()">{{ 'common.cancel' | translate }}</button>
|
|
<button type="button" class="btn-danger" (click)="delete()">
|
|
<lucide-icon [img]="Trash2" [size]="14"></lucide-icon>
|
|
{{ 'common.delete' | translate }}
|
|
</button>
|
|
<button type="button" class="btn-primary" (click)="submit()" [disabled]="form.invalid">
|
|
{{ 'common.save' | translate }}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<form [formGroup]="form" (ngSubmit)="submit()" class="edit-form">
|
|
|
|
<!-- Illustrations (galerie editable, rendu editorial) -->
|
|
<div class="field">
|
|
<label>{{ 'sceneEdit.illustrationsLabel' | translate }}</label>
|
|
<app-image-gallery
|
|
[imageIds]="illustrationImageIds"
|
|
[editable]="true"
|
|
[layout]="'EDITORIAL'"
|
|
(imageIdsChange)="illustrationImageIds = $event">
|
|
</app-image-gallery>
|
|
<small class="field-hint">{{ 'sceneEdit.illustrationsHint' | translate }}</small>
|
|
</div>
|
|
|
|
<!-- Battlemap Foundry : media + sidecar JSON Universal VTT (non affichee, export only) -->
|
|
<div class="field">
|
|
<label>{{ 'sceneEdit.battlemapLabel' | translate }}</label>
|
|
<small class="field-hint">{{ 'sceneEdit.battlemapHint' | translate }}</small>
|
|
|
|
<div class="battlemap-slot">
|
|
<span class="battlemap-slot-label">{{ 'sceneEdit.battlemapMedia' | translate }}</span>
|
|
@if (battlemapMediaFileId) {
|
|
<span class="battlemap-file">{{ battlemapMediaName || ('sceneEdit.battlemapAttached' | translate) }}</span>
|
|
<button type="button" class="battlemap-remove" (click)="removeBattlemapMedia()">
|
|
{{ 'sceneEdit.battlemapRemove' | translate }}
|
|
</button>
|
|
} @else {
|
|
<label class="battlemap-pick">
|
|
{{ (battlemapUploadingMedia ? 'sceneEdit.battlemapUploading' : 'sceneEdit.battlemapChoose') | translate }}
|
|
<input type="file" accept="image/*,video/mp4,video/webm" hidden (change)="onBattlemapMediaSelected($event)" />
|
|
</label>
|
|
}
|
|
</div>
|
|
|
|
<div class="battlemap-slot">
|
|
<span class="battlemap-slot-label">{{ 'sceneEdit.battlemapData' | translate }}</span>
|
|
@if (battlemapDataFileId) {
|
|
<span class="battlemap-file">{{ battlemapDataName || ('sceneEdit.battlemapAttached' | translate) }}</span>
|
|
<button type="button" class="battlemap-remove" (click)="removeBattlemapData()">
|
|
{{ 'sceneEdit.battlemapRemove' | translate }}
|
|
</button>
|
|
} @else {
|
|
<label class="battlemap-pick">
|
|
{{ (battlemapUploadingData ? 'sceneEdit.battlemapUploading' : 'sceneEdit.battlemapChoose') | translate }}
|
|
<input type="file" accept=".json,.dd2vtt,.uvtt,application/json" hidden (change)="onBattlemapDataSelected($event)" />
|
|
</label>
|
|
}
|
|
</div>
|
|
</div>
|
|
|
|
<div class="field">
|
|
<label for="scene-edit-name">{{ 'sceneEdit.nameLabel' | translate }}</label>
|
|
<input
|
|
id="scene-edit-name"
|
|
type="text"
|
|
formControlName="name"
|
|
[placeholder]="'sceneEdit.namePlaceholder' | translate"
|
|
[class.invalid]="form.get('name')?.invalid && form.get('name')?.touched"
|
|
/>
|
|
</div>
|
|
|
|
<div class="field">
|
|
<label for="scene-edit-description">{{ 'sceneEdit.descriptionLabel' | translate }}</label>
|
|
<textarea
|
|
id="scene-edit-description"
|
|
formControlName="description"
|
|
[placeholder]="'sceneEdit.descriptionPlaceholder' | translate"
|
|
rows="3">
|
|
</textarea>
|
|
</div>
|
|
|
|
<div class="field">
|
|
<label>{{ 'sceneEdit.iconLabel' | translate }}</label>
|
|
<app-icon-picker [options]="campaignIconOptions" [(selected)]="selectedIcon"></app-icon-picker>
|
|
</div>
|
|
|
|
<!-- Section : Contexte et ambiance -->
|
|
<app-expandable-section [title]="'sceneEdit.contextSectionTitle' | translate" icon="📍" [initiallyOpen]="true">
|
|
<div class="field-row">
|
|
<div class="field">
|
|
<label for="scene-edit-location">{{ 'sceneEdit.locationLabel' | translate }}</label>
|
|
<input id="scene-edit-location" type="text" formControlName="location" [placeholder]="'sceneEdit.locationPlaceholder' | translate" />
|
|
</div>
|
|
<div class="field">
|
|
<label for="scene-edit-timing">{{ 'sceneEdit.timingLabel' | translate }}</label>
|
|
<input id="scene-edit-timing" type="text" formControlName="timing" [placeholder]="'sceneEdit.timingPlaceholder' | translate" />
|
|
</div>
|
|
</div>
|
|
<div class="field">
|
|
<label for="scene-edit-atmosphere">{{ 'sceneEdit.atmosphereLabel' | translate }}</label>
|
|
<textarea
|
|
id="scene-edit-atmosphere"
|
|
formControlName="atmosphere"
|
|
[placeholder]="'sceneEdit.atmospherePlaceholder' | translate"
|
|
rows="4">
|
|
</textarea>
|
|
</div>
|
|
</app-expandable-section>
|
|
|
|
<!-- Section : Narration pour les joueurs -->
|
|
<app-expandable-section [title]="'sceneEdit.narrationSectionTitle' | translate" icon="📖">
|
|
<div class="field">
|
|
<textarea
|
|
formControlName="playerNarration"
|
|
[placeholder]="'sceneEdit.narrationPlaceholder' | translate"
|
|
rows="6">
|
|
</textarea>
|
|
<small class="field-hint">{{ 'sceneEdit.narrationHint' | translate }}</small>
|
|
</div>
|
|
</app-expandable-section>
|
|
|
|
<!-- Section : Notes et secrets du MJ (privé) -->
|
|
<app-expandable-section [title]="'sceneEdit.gmNotesSectionTitle' | translate" icon="🔒" variant="private">
|
|
<div class="field">
|
|
<textarea
|
|
formControlName="gmSecretNotes"
|
|
[placeholder]="'sceneEdit.gmNotesPlaceholder' | translate"
|
|
rows="5">
|
|
</textarea>
|
|
<small class="field-hint">{{ 'sceneEdit.gmNotesHint' | translate }}</small>
|
|
</div>
|
|
</app-expandable-section>
|
|
|
|
<!-- Section : Choix et conséquences -->
|
|
<app-expandable-section [title]="'sceneEdit.choicesSectionTitle' | translate" icon="🔀">
|
|
<div class="field">
|
|
<textarea
|
|
formControlName="choicesConsequences"
|
|
[placeholder]="'sceneEdit.choicesPlaceholder' | translate"
|
|
rows="5">
|
|
</textarea>
|
|
</div>
|
|
</app-expandable-section>
|
|
|
|
<!-- Section : Branches narratives (graphe intra-chapitre) -->
|
|
<app-expandable-section [title]="'sceneEdit.branchesSectionTitle' | translate" icon="🌿">
|
|
@if (siblingScenes.length === 0) {
|
|
<div class="branches-hint">
|
|
<small class="field-hint">
|
|
{{ 'sceneEdit.branchesNoSibling' | translate }}
|
|
</small>
|
|
</div>
|
|
}
|
|
|
|
@if (siblingScenes.length > 0) {
|
|
<div class="branches-list">
|
|
@for (branch of branches; track $index; let i = $index) {
|
|
<div class="branch-item">
|
|
<div class="field">
|
|
<label>{{ 'sceneEdit.branchLabelLabel' | translate }}</label>
|
|
<input
|
|
type="text"
|
|
[value]="branch.label"
|
|
(input)="updateBranchLabel(i, $any($event.target).value)"
|
|
[placeholder]="'sceneEdit.branchLabelPlaceholder' | translate" />
|
|
</div>
|
|
<div class="field">
|
|
<label>{{ 'sceneEdit.branchTargetLabel' | translate }}</label>
|
|
<select
|
|
(change)="updateBranchTarget(i, $any($event.target).value)">
|
|
<option value="" [selected]="!branch.targetSceneId">{{ 'sceneEdit.branchTargetPlaceholder' | translate }}</option>
|
|
@for (s of siblingScenes; track s) {
|
|
<option
|
|
[value]="s.id"
|
|
[selected]="s.id === branch.targetSceneId">{{ s.name }}</option>
|
|
}
|
|
</select>
|
|
</div>
|
|
<div class="field">
|
|
<label>{{ 'sceneEdit.branchConditionLabel' | translate }}</label>
|
|
<input
|
|
type="text"
|
|
[value]="branch.condition || ''"
|
|
(input)="updateBranchCondition(i, $any($event.target).value)"
|
|
[placeholder]="'sceneEdit.branchConditionPlaceholder' | translate" />
|
|
</div>
|
|
<button type="button" class="btn-remove-branch" (click)="removeBranch(i)"
|
|
[title]="'sceneEdit.branchRemoveTitle' | translate">
|
|
<lucide-icon [img]="Trash2" [size]="14"></lucide-icon>
|
|
{{ 'sceneEdit.branchRemove' | translate }}
|
|
</button>
|
|
</div>
|
|
}
|
|
<button type="button" class="btn-add-branch" (click)="addBranch()">
|
|
{{ 'sceneEdit.branchAdd' | translate }}
|
|
</button>
|
|
<small class="field-hint">
|
|
{{ 'sceneEdit.branchesHint' | translate }}
|
|
</small>
|
|
</div>
|
|
}
|
|
</app-expandable-section>
|
|
|
|
<!-- Section : Combat ou rencontre -->
|
|
<app-expandable-section [title]="'sceneEdit.combatSectionTitle' | translate" icon="⚔️">
|
|
<div class="field">
|
|
<label for="scene-edit-combat-difficulty">{{ 'sceneEdit.combatDifficultyLabel' | translate }}</label>
|
|
<input id="scene-edit-combat-difficulty" type="text" formControlName="combatDifficulty" [placeholder]="'sceneEdit.combatDifficultyPlaceholder' | translate" />
|
|
</div>
|
|
<div class="field">
|
|
<label>{{ 'sceneEdit.bestiaryEnemiesLabel' | translate }}</label>
|
|
<app-enemy-link-picker
|
|
[value]="enemyIds"
|
|
[availableEnemies]="availableEnemies"
|
|
[campaignId]="campaignId"
|
|
(valueChange)="enemyIds = $event">
|
|
</app-enemy-link-picker>
|
|
<small class="field-hint">
|
|
{{ 'sceneEdit.bestiaryEnemiesHint' | translate }}
|
|
</small>
|
|
</div>
|
|
<div class="field">
|
|
<label for="scene-edit-enemies">{{ 'sceneEdit.enemiesLabel' | translate }}</label>
|
|
<textarea
|
|
id="scene-edit-enemies"
|
|
formControlName="enemies"
|
|
[placeholder]="'sceneEdit.enemiesPlaceholder' | translate"
|
|
rows="4">
|
|
</textarea>
|
|
</div>
|
|
</app-expandable-section>
|
|
|
|
<!-- Section : Pages Lore associées (B2 cross-context) -->
|
|
@if (loreId) {
|
|
<app-expandable-section [title]="'sceneEdit.loreSectionTitle' | translate" icon="🔗">
|
|
<div class="field">
|
|
<app-lore-link-picker
|
|
[value]="relatedPageIds"
|
|
[availablePages]="availablePages"
|
|
[loreId]="loreId"
|
|
(valueChange)="relatedPageIds = $event">
|
|
</app-lore-link-picker>
|
|
<small class="field-hint">
|
|
{{ 'sceneEdit.loreHint' | translate }}
|
|
</small>
|
|
</div>
|
|
</app-expandable-section>
|
|
}
|
|
|
|
@if (!loreId) {
|
|
<div class="field">
|
|
<small class="field-hint">
|
|
{{ 'sceneEdit.noLoreHint' | translate }}
|
|
</small>
|
|
</div>
|
|
}
|
|
|
|
<!-- Lieu explorable : pièces / donjon -->
|
|
<app-expandable-section [title]="'sceneEdit.dungeonSectionTitle' | translate" icon="🏰" [initiallyOpen]="rooms.length > 0">
|
|
<small class="field-hint">
|
|
{{ 'sceneEdit.dungeonHint' | translate }}
|
|
</small>
|
|
<app-rooms-editor
|
|
[rooms]="rooms"
|
|
[availableEnemies]="availableEnemies"
|
|
[campaignId]="campaignId"
|
|
(roomsChange)="onRoomsChange($event)">
|
|
</app-rooms-editor>
|
|
</app-expandable-section>
|
|
|
|
</form>
|
|
|
|
</div>
|
|
|
|
<!-- Drawer chat IA (hors .edit-page pour couvrir le viewport à droite) -->
|
|
<app-ai-chat-drawer
|
|
[campaignId]="campaignId"
|
|
entityType="scene"
|
|
[entityId]="sceneId"
|
|
[isOpen]="chatOpen"
|
|
[welcomeMessage]="'sceneEdit.chatWelcome' | translate"
|
|
[quickSuggestions]="chatQuickSuggestions"
|
|
(close)="chatOpen = false">
|
|
</app-ai-chat-drawer>
|