aether-shards/src/ui/team-builder.js

321 lines
8.3 KiB
JavaScript
Raw Normal View History

import { LitElement, html, css } from "lit";
export class TeamBuilder extends LitElement {
static get styles() {
return css`
:host {
display: block;
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
font-family: "Courier New", monospace; /* Placeholder for Voxel Font */
color: white;
pointer-events: none; /* Let clicks pass through to 3D scene where empty */
z-index: 10;
}
.container {
display: grid;
grid-template-columns: 250px 1fr 250px;
grid-template-rows: 1fr 80px;
height: 100%;
width: 100%;
pointer-events: auto;
background: rgba(0, 0, 0, 0.4); /* Dim background */
}
/* --- LEFT PANEL: ROSTER --- */
.roster-panel {
background: rgba(20, 20, 30, 0.9);
border-right: 2px solid #555;
padding: 1rem;
overflow-y: auto;
}
.class-card {
background: #333;
border: 2px solid #555;
padding: 10px;
margin-bottom: 10px;
cursor: pointer;
transition: all 0.2s;
display: flex;
align-items: center;
gap: 10px;
}
.class-card:hover {
border-color: #00ffff;
background: #444;
}
.class-card.locked {
opacity: 0.5;
pointer-events: none;
filter: grayscale(1);
}
/* --- CENTER PANEL: SLOTS --- */
.squad-panel {
display: flex;
justify-content: center;
align-items: flex-end;
padding-bottom: 2rem;
gap: 20px;
}
.squad-slot {
width: 120px;
height: 150px;
background: rgba(0, 0, 0, 0.6);
border: 2px dashed #666;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
cursor: pointer;
position: relative;
}
.squad-slot.filled {
border: 2px solid #00ff00;
background: rgba(0, 50, 0, 0.6);
}
.squad-slot.selected {
border-color: #00ffff;
box-shadow: 0 0 10px #00ffff;
}
.remove-btn {
position: absolute;
top: -10px;
right: -10px;
background: red;
border: none;
color: white;
width: 24px;
height: 24px;
border-radius: 50%;
cursor: pointer;
font-weight: bold;
}
/* --- RIGHT PANEL: DETAILS --- */
.details-panel {
background: rgba(20, 20, 30, 0.9);
border-left: 2px solid #555;
padding: 1rem;
}
/* --- FOOTER --- */
.footer {
grid-column: 1 / -1;
display: flex;
justify-content: center;
align-items: center;
background: rgba(10, 10, 20, 0.95);
border-top: 2px solid #555;
}
.embark-btn {
padding: 15px 40px;
font-size: 1.5rem;
background: #008800;
color: white;
border: 2px solid #00ff00;
cursor: pointer;
text-transform: uppercase;
font-weight: bold;
}
.embark-btn:disabled {
background: #333;
border-color: #555;
color: #777;
cursor: not-allowed;
}
`;
}
static get properties() {
return {
availableClasses: { type: Array }, // Input: List of class definition objects
squad: { type: Array }, // Internal State: The 4 slots
selectedSlotIndex: { type: Number },
hoveredClass: { type: Object },
};
}
constructor() {
super();
this.squad = [null, null, null, null];
this.selectedSlotIndex = 0; // Default to first slot
this.availableClasses = []; // Passed in by parent
this.hoveredClass = null;
}
render() {
const isSquadValid = this.squad.some((u) => u !== null);
return html`
<div class="container">
<!-- ROSTER LIST -->
<div class="roster-panel">
<h3>Roster</h3>
${this.availableClasses.map(
(cls) => html`
<div
class="class-card ${cls.unlocked ? "" : "locked"}"
@click="${() => this._assignClass(cls)}"
@mouseenter="${() => (this.hoveredClass = cls)}"
@mouseleave="${() => (this.hoveredClass = null)}"
>
<div class="icon">${cls.icon || "⚔️"}</div>
<div>
<strong>${cls.name}</strong><br />
<small>${cls.role}</small>
</div>
</div>
`
)}
</div>
<!-- CENTER SQUAD SLOTS -->
<div class="squad-panel">
${this.squad.map(
(unit, index) => html`
<div
class="squad-slot ${unit ? "filled" : ""} ${this
.selectedSlotIndex === index
? "selected"
: ""}"
@click="${() => this._selectSlot(index)}"
>
${unit
? html`
<div class="icon" style="font-size: 2rem;">
${unit.icon || "🛡️"}
</div>
<span>${unit.name}</span>
<button
class="remove-btn"
@click="${(e) => this._removeUnit(e, index)}"
>
X
</button>
`
: html`<span
>Slot ${index + 1}<br /><small>Select Class</small></span
>`}
</div>
`
)}
</div>
<!-- RIGHT DETAILS PANEL -->
<div class="details-panel">
${this.hoveredClass
? html`
<h2>${this.hoveredClass.name}</h2>
<p><em>${this.hoveredClass.role}</em></p>
<hr />
<p>
${this.hoveredClass.description ||
"No description available."}
</p>
<h4>Base Stats</h4>
<ul>
<li>HP: ${this.hoveredClass.base_stats?.health}</li>
<li>AP: ${this.hoveredClass.base_stats?.speed}</li>
<!-- Simplified AP calc -->
<li>Move: ${this.hoveredClass.base_stats?.movement}</li>
</ul>
`
: html`<p>Hover over a class to see details.</p>`}
</div>
<!-- FOOTER -->
<div class="footer">
<button
class="embark-btn"
?disabled="${!isSquadValid}"
@click="${this._handleEmbark}"
>
DESCEND
</button>
</div>
</div>
`;
}
// --- LOGIC ---
_selectSlot(index) {
this.selectedSlotIndex = index;
}
_assignClass(classDef) {
if (!classDef.unlocked) return;
// 1. Create a lightweight manifest for the slot
const unitManifest = {
classId: classDef.id,
name: classDef.name, // In real app, auto-generate name
icon: classDef.icon,
};
// 2. Update State (Trigger Re-render)
const newSquad = [...this.squad];
newSquad[this.selectedSlotIndex] = unitManifest;
this.squad = newSquad;
// 3. Auto-advance selection
if (this.selectedSlotIndex < 3) {
this.selectedSlotIndex++;
}
// 4. Dispatch Event (For 3D Scene to show model)
this.dispatchEvent(
new CustomEvent("squad-update", {
detail: { slot: this.selectedSlotIndex, unit: unitManifest },
bubbles: true,
composed: true,
})
);
}
_removeUnit(e, index) {
e.stopPropagation(); // Prevent slot selection
const newSquad = [...this.squad];
newSquad[index] = null;
this.squad = newSquad;
this.selectedSlotIndex = index; // Select the empty slot
// Dispatch Event (To clear 3D model)
this.dispatchEvent(
new CustomEvent("squad-update", {
detail: { slot: index, unit: null },
bubbles: true,
composed: true,
})
);
}
_handleEmbark() {
const manifest = this.squad.filter((u) => u !== null);
this.dispatchEvent(
new CustomEvent("embark", {
detail: { squad: manifest },
bubbles: true,
composed: true,
})
);
}
}
customElements.define("team-builder", TeamBuilder);