321 lines
8.3 KiB
JavaScript
321 lines
8.3 KiB
JavaScript
|
|
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);
|