aether-shards/src/ui/screens/MissionDebrief.js

642 lines
18 KiB
JavaScript
Raw Normal View History

import { LitElement, html, css } from "lit";
import { theme, buttonStyles } from "../styles/theme.js";
/**
* MissionDebrief.js
* After Action Report - UI component for displaying mission results.
* Shows XP, rewards, loot, reputation changes, and squad status.
*/
export class MissionDebrief extends LitElement {
static get properties() {
return {
result: { type: Object },
};
}
static get styles() {
return [
theme,
buttonStyles,
css`
:host {
display: block;
}
dialog {
margin: auto;
padding: 0;
border: none;
background: transparent;
max-width: 900px;
max-height: 90vh;
width: 90vw;
}
dialog::backdrop {
background: rgba(0, 0, 0, 0.85);
backdrop-filter: blur(4px);
}
.modal-content {
background: var(--color-bg-tertiary);
border: var(--border-width-thick) solid var(--color-border-default);
box-shadow: var(--shadow-lg);
width: 100%;
max-height: 90vh;
overflow-y: auto;
display: grid;
grid-template-rows: auto 1fr auto;
grid-template-areas:
"header"
"content"
"footer";
font-family: var(--font-family);
color: var(--color-text-primary);
}
/* Header */
.header {
grid-area: header;
padding: var(--spacing-lg);
text-align: center;
border-bottom: var(--border-width-medium) solid
var(--color-border-default);
}
.header.victory {
color: var(--color-accent-gold);
text-shadow: 0 0 10px rgba(255, 215, 0, 0.5);
}
.header.defeat {
color: var(--color-accent-red);
text-shadow: 0 0 10px rgba(255, 102, 102, 0.5);
}
.header h1 {
margin: 0;
font-size: var(--font-size-4xl);
text-transform: uppercase;
letter-spacing: 0.1em;
}
.mission-title {
margin-top: var(--spacing-sm);
font-size: var(--font-size-lg);
color: var(--color-text-secondary);
}
/* Content */
.content {
grid-area: content;
padding: var(--spacing-lg);
display: flex;
flex-direction: column;
gap: var(--spacing-lg);
}
/* Primary Stats Row */
.primary-stats {
display: grid;
grid-template-columns: 1fr 1fr;
gap: var(--spacing-lg);
}
.stat-card {
background: var(--color-bg-card);
border: var(--border-width-medium) solid var(--color-border-default);
padding: var(--spacing-md);
display: flex;
flex-direction: column;
gap: var(--spacing-sm);
}
.stat-label {
font-size: var(--font-size-sm);
color: var(--color-text-secondary);
text-transform: uppercase;
}
.stat-value {
font-size: var(--font-size-2xl);
color: var(--color-accent-cyan);
font-weight: var(--font-weight-bold);
}
.xp-bar-container {
width: 100%;
height: 20px;
background: var(--color-bg-primary);
border: var(--border-width-thin) solid var(--color-border-default);
position: relative;
overflow: hidden;
}
.xp-bar-fill {
height: 100%;
background: linear-gradient(
90deg,
var(--color-accent-gold) 0%,
var(--color-accent-orange) 100%
);
transition: width 1s ease-out;
width: 0%;
}
/* Rewards Panel */
.rewards-panel {
background: var(--color-bg-panel);
border: var(--border-width-medium) solid var(--color-border-default);
padding: var(--spacing-md);
display: flex;
flex-direction: column;
gap: var(--spacing-md);
}
.rewards-panel h2 {
margin: 0;
font-size: var(--font-size-xl);
color: var(--color-accent-gold);
border-bottom: var(--border-width-thin) solid
var(--color-border-default);
padding-bottom: var(--spacing-sm);
}
.currency-display {
display: flex;
gap: var(--spacing-lg);
align-items: center;
}
.currency-item {
display: flex;
align-items: center;
gap: var(--spacing-sm);
font-size: var(--font-size-lg);
}
.loot-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(100px, 1fr));
gap: var(--spacing-md);
margin-top: var(--spacing-sm);
}
.item-card {
background: var(--color-bg-card);
border: var(--border-width-medium) solid var(--color-border-default);
padding: var(--spacing-sm);
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
min-height: 120px;
cursor: pointer;
transition: all var(--transition-normal);
position: relative;
}
.item-card:hover {
border-color: var(--color-accent-cyan);
box-shadow: var(--shadow-glow-cyan);
transform: translateY(-2px);
}
.item-card img {
width: 48px;
height: 48px;
object-fit: contain;
}
.item-card .item-name {
margin-top: var(--spacing-xs);
font-size: var(--font-size-xs);
text-align: center;
color: var(--color-text-primary);
}
.reputation-display {
margin-top: var(--spacing-sm);
}
.reputation-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: var(--spacing-xs) 0;
border-bottom: var(--border-width-thin) solid
var(--color-border-dashed);
}
.reputation-item:last-child {
border-bottom: none;
}
.reputation-name {
color: var(--color-text-secondary);
}
.reputation-amount {
color: var(--color-accent-green);
font-weight: var(--font-weight-bold);
}
/* Roster Status */
.roster-status {
background: var(--color-bg-panel);
border: var(--border-width-medium) solid var(--color-border-default);
padding: var(--spacing-md);
}
.roster-status h2 {
margin: 0 0 var(--spacing-md) 0;
font-size: var(--font-size-xl);
color: var(--color-accent-cyan);
border-bottom: var(--border-width-thin) solid
var(--color-border-default);
padding-bottom: var(--spacing-sm);
}
.roster-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
gap: var(--spacing-md);
}
.unit-status {
background: var(--color-bg-card);
border: var(--border-width-medium) solid var(--color-border-default);
padding: var(--spacing-sm);
display: flex;
flex-direction: column;
align-items: center;
gap: var(--spacing-xs);
position: relative;
}
.unit-status.dead {
opacity: 0.5;
filter: grayscale(100%);
}
.unit-status.injured {
border-color: var(--color-accent-orange);
}
.unit-portrait {
width: 60px;
height: 60px;
border: var(--border-width-thin) solid var(--color-border-default);
background: var(--color-bg-primary);
display: flex;
align-items: center;
justify-content: center;
font-size: 32px;
}
.unit-name {
font-size: var(--font-size-sm);
font-weight: var(--font-weight-bold);
}
.unit-status-text {
font-size: var(--font-size-xs);
color: var(--color-text-secondary);
}
.level-up-badge {
position: absolute;
top: -8px;
right: -8px;
background: var(--color-accent-gold);
color: var(--color-bg-primary);
padding: 2px 6px;
font-size: var(--font-size-xs);
font-weight: var(--font-weight-bold);
border-radius: var(--border-radius-sm);
box-shadow: var(--shadow-glow-gold);
}
/* Footer */
.footer {
grid-area: footer;
padding: var(--spacing-lg);
border-top: var(--border-width-medium) solid
var(--color-border-default);
display: flex;
justify-content: center;
}
/* Typewriter Effect */
.typewriter {
display: inline-block;
overflow: hidden;
white-space: nowrap;
border-right: 2px solid var(--color-accent-cyan);
animation: typing 2s steps(40, end),
blink-caret 0.75s step-end infinite;
}
@keyframes typing {
from {
width: 0;
}
to {
width: 100%;
}
}
@keyframes blink-caret {
from,
to {
border-color: transparent;
}
50% {
border-color: var(--color-accent-cyan);
}
}
/* Mobile Responsive */
@media (max-width: 768px) {
.modal-content {
max-width: 100%;
max-height: 100vh;
border-radius: 0;
}
.primary-stats {
grid-template-columns: 1fr;
}
.loot-grid {
grid-template-columns: repeat(auto-fill, minmax(80px, 1fr));
}
.roster-grid {
grid-template-columns: 1fr;
}
}
`,
];
}
constructor() {
super();
this.result = null;
this._typewriterTexts = new Map();
}
firstUpdated() {
const dialog = this.shadowRoot?.querySelector("dialog");
if (dialog && this.result) {
dialog.showModal();
// Prevent closing on backdrop click or ESC (user must click return button)
dialog.addEventListener("cancel", (e) => {
e.preventDefault();
});
dialog.addEventListener("click", (e) => {
// Only close if clicking the backdrop, not the content
if (e.target === dialog) {
e.preventDefault();
}
});
this._startAnimations();
}
}
updated(changedProperties) {
if (changedProperties.has("result") && this.result) {
const dialog = this.shadowRoot?.querySelector("dialog");
if (dialog && !dialog.open) {
dialog.showModal();
}
this._startAnimations();
}
}
/**
* Starts animations for XP bar and typewriter effects
* @private
*/
_startAnimations() {
// Animate XP bar
this.updateComplete.then(() => {
const xpBar = this.shadowRoot?.querySelector(".xp-bar-fill");
if (xpBar && this.result) {
// Calculate percentage (assuming max XP of 1000 for now)
const maxXp = 1000;
const percentage = Math.min((this.result.xpEarned / maxXp) * 100, 100);
setTimeout(() => {
xpBar.style.width = `${percentage}%`;
}, 100);
}
});
}
/**
* Gets item display name
* @param {Object} item - Item instance
* @returns {string}
*/
_getItemName(item) {
return item.name || item.defId || "Unknown Item";
}
/**
* Gets item icon
* @param {Object} item - Item instance
* @returns {string|null}
*/
_getItemIcon(item) {
return item.icon || null;
}
/**
* Handles return to hub button click
* @private
*/
_handleReturn() {
const dialog = this.shadowRoot?.querySelector("dialog");
if (dialog) {
dialog.close();
}
this.dispatchEvent(
new CustomEvent("return-to-hub", {
bubbles: true,
composed: true,
})
);
}
render() {
if (!this.result) {
return html``;
}
const isVictory = this.result.outcome === "VICTORY";
const headerClass = isVictory ? "victory" : "defeat";
const headerText = isVictory ? "MISSION ACCOMPLISHED" : "MISSION FAILED";
return html`
<dialog>
<div class="modal-content">
<!-- Header -->
<header class="header ${headerClass}">
<h1 class="typewriter">${headerText}</h1>
<div class="mission-title">
${this.result.missionTitle || "Mission"}
</div>
</header>
<!-- Content -->
<div class="content">
<!-- Primary Stats -->
<div class="primary-stats">
<div class="stat-card">
<div class="stat-label">XP Gained</div>
<div class="stat-value xp-display">${this.result.xpEarned}</div>
<div class="xp-bar-container">
<div class="xp-bar-fill"></div>
</div>
</div>
${this.result.turnsTaken !== undefined
? html`
<div class="stat-card">
<div class="stat-label">Turns Taken</div>
<div class="stat-value turns-display">
${this.result.turnsTaken}
</div>
</div>
`
: html`<div class="stat-card">
<div class="stat-label">Turns Taken</div>
<div class="stat-value turns-display">-</div>
</div>`}
</div>
<!-- Rewards Panel -->
<div class="rewards-panel">
<h2>Rewards</h2>
<!-- Currency -->
<div class="currency-display">
<div class="currency-item">
<span>💎</span>
<span>${this.result.currency?.shards || 0} Shards</span>
</div>
<div class="currency-item">
<span></span>
<span>${this.result.currency?.cores || 0} Cores</span>
</div>
</div>
<!-- Loot Grid -->
${this.result.loot && this.result.loot.length > 0
? html`
<div class="loot-grid">
${this.result.loot.map(
(item) => html`
<div
class="item-card"
title="${this._getItemName(item)}"
>
${this._getItemIcon(item)
? html`<img
src="${this._getItemIcon(item)}"
alt="${this._getItemName(item)}"
/>`
: html`<span aria-hidden="true">📦</span>`}
<div class="item-name">
${this._getItemName(item)}
</div>
${item.quantity > 1
? html`<div class="item-quantity">
x${item.quantity}
</div>`
: html``}
</div>
`
)}
</div>
`
: html`<p style="color: var(--color-text-secondary);">
No loot found
</p>`}
<!-- Reputation -->
${this.result.reputationChanges &&
this.result.reputationChanges.length > 0
? html`
<div class="reputation-display">
${this.result.reputationChanges.map(
(rep) => html`
<div class="reputation-item">
<span class="reputation-name"
>${rep.factionId}</span
>
<span class="reputation-amount">
${rep.amount > 0 ? "+" : ""}${rep.amount}
</span>
</div>
`
)}
</div>
`
: html``}
</div>
<!-- Roster Status -->
${this.result.squadUpdates && this.result.squadUpdates.length > 0
? html`
<div class="roster-status">
<h2>Squad Status</h2>
<div class="roster-grid">
${this.result.squadUpdates.map(
(unit) => html`
<div
class="unit-status ${unit.isDead
? "dead"
: unit.damageTaken > 0
? "injured"
: ""}"
>
${unit.leveledUp
? html`<div class="level-up-badge">
Level Up!
</div>`
: html``}
<div class="unit-portrait"></div>
<div class="unit-name">${unit.unitId}</div>
<div class="unit-status-text">
${unit.isDead
? "Dead"
: unit.damageTaken > 0
? "Injured"
: "OK"}
</div>
</div>
`
)}
</div>
</div>
`
: html``}
</div>
<!-- Footer -->
<footer class="footer">
<button
class="btn btn-primary return-button"
@click=${this._handleReturn}
>
RETURN TO BASE
</button>
</footer>
</div>
</dialog>
`;
}
}
customElements.define("mission-debrief", MissionDebrief);