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

466 lines
14 KiB
JavaScript
Raw Normal View History

import { LitElement, html, css } from 'lit';
// Import Tier 1 Class Definitions
import vanguardDef from '../assets/data/classes/vanguard.json' with { type: 'json' };
import weaverDef from '../assets/data/classes/aether_weaver.json' with { type: 'json' };
import scavengerDef from '../assets/data/classes/scavenger.json' with { type: 'json' };
import tinkerDef from '../assets/data/classes/tinker.json' with { type: 'json' };
import custodianDef from '../assets/data/classes/custodian.json' with { type: 'json' };
// UI Metadata Mapping
const CLASS_METADATA = {
'CLASS_VANGUARD': {
icon: '🛡️',
image: 'assets/images/portraits/vanguard.png',
role: 'Tank',
description: 'A heavy frontline tank specialized in absorbing damage.'
},
'CLASS_WEAVER': {
icon: '✨',
image: 'assets/images/portraits/weaver.png',
role: 'Magic DPS',
description: 'A master of elemental magic capable of creating synergy chains.'
},
'CLASS_SCAVENGER': {
icon: '🎒',
image: 'assets/images/portraits/scavenger.png',
role: 'Utility',
description: 'Highly mobile utility expert who excels at finding loot.'
},
'CLASS_TINKER': {
icon: '🔧',
image: 'assets/images/portraits/tinker.png',
role: 'Tech',
description: 'Uses ancient technology to deploy turrets.'
},
'CLASS_CUSTODIAN': {
icon: '🌿',
image: 'assets/images/portraits/custodian.png',
role: 'Healer',
description: 'A spiritual healer focused on removing corruption.'
}
};
const RAW_TIER_1_CLASSES = [vanguardDef, weaverDef, scavengerDef, tinkerDef, custodianDef];
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;
color: white;
pointer-events: none;
z-index: 10;
box-sizing: border-box;
}
.container {
display: grid;
grid-template-columns: 280px 1fr 300px;
grid-template-rows: 1fr 100px;
grid-template-areas: "roster squad details" "footer footer footer";
height: 100%; width: 100%;
pointer-events: auto;
background: rgba(0, 0, 0, 0.85);
backdrop-filter: blur(4px);
}
@media (max-width: 1024px) {
.container {
grid-template-columns: 1fr;
grid-template-rows: 200px 1fr 200px 80px;
grid-template-areas: "roster" "squad" "details" "footer";
}
}
/* --- LEFT PANEL: ROSTER --- */
.roster-panel {
grid-area: roster;
background: rgba(20, 20, 30, 0.9);
border-right: 2px solid #555;
padding: 1rem;
overflow-y: auto;
display: flex;
flex-direction: column;
gap: 10px;
}
h3 { margin-top: 0; color: #00ffff; border-bottom: 1px solid #555; padding-bottom: 10px; }
.card {
background: #333;
border: 2px solid #555;
padding: 15px;
cursor: pointer;
transition: all 0.2s;
display: flex;
align-items: center;
gap: 15px;
width: 100%;
text-align: left;
font-family: inherit;
color: inherit;
appearance: none;
}
.card:hover:not(:disabled) {
border-color: #00ffff;
background: #444;
transform: translateX(5px);
}
.card.selected {
border-color: #00ff00;
background: #224422;
}
.card:disabled {
opacity: 0.5;
cursor: not-allowed;
filter: grayscale(1);
}
/* --- CENTER PANEL: SQUAD SLOTS --- */
.squad-panel {
grid-area: squad;
display: flex;
justify-content: center;
align-items: center;
padding: 2rem;
gap: 30px;
flex-wrap: wrap;
}
.slot-wrapper {
position: relative;
width: 180px; /* Wider for portraits */
height: 240px; /* Taller for portraits */
transition: transform 0.2s;
}
.slot-wrapper:hover { transform: scale(1.05); }
.squad-slot {
width: 100%; height: 100%;
background: rgba(10, 10, 10, 0.8);
border: 3px dashed #666;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
cursor: pointer;
font-family: inherit; color: inherit; padding: 0; appearance: none;
overflow: hidden;
}
/* Image placeholder style */
.unit-image {
width: 100%;
height: 75%;
object-fit: cover;
background-color: #222;
border-bottom: 2px solid #555;
}
.unit-info {
height: 25%;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
width: 100%;
background: rgba(30,30,40,0.95);
padding: 5px;
box-sizing: border-box;
}
.squad-slot.filled {
border: 3px solid #00ff00;
background: rgba(0, 20, 0, 0.8);
}
.squad-slot.selected {
border-color: #00ffff;
box-shadow: 0 0 15px rgba(0,255,255,0.3);
}
.remove-btn {
position: absolute; top: -12px; right: -12px;
background: #cc0000; color: white;
width: 28px; height: 28px;
border: 2px solid white; border-radius: 50%;
cursor: pointer; font-weight: bold; z-index: 2;
}
.placeholder-img {
display: flex;
align-items: center;
justify-content: center;
background: transparent;
color: #555;
font-size: 3rem;
height: 100%;
}
/* --- RIGHT PANEL: DETAILS --- */
.details-panel {
grid-area: details;
background: rgba(20, 20, 30, 0.9);
border-left: 2px solid #555;
padding: 1.5rem;
overflow-y: auto;
}
.footer {
grid-area: footer;
display: flex;
justify-content: center;
align-items: center;
background: rgba(10, 10, 20, 0.95);
border-top: 2px solid #555;
}
.embark-btn {
padding: 15px 60px;
font-size: 1.8rem;
background: #008800;
color: white;
border: 3px solid #00ff00;
cursor: pointer;
text-transform: uppercase;
font-weight: bold;
font-family: inherit;
letter-spacing: 2px;
}
.embark-btn:disabled {
background: #333; border-color: #555; color: #777; cursor: not-allowed;
}
`;
}
static get properties() {
return {
mode: { type: String }, // 'DRAFT' (Classes) or 'ROSTER' (Existing Units)
availablePool: { type: Array }, // List of Classes OR Units
squad: { type: Array }, // The 4 slots
selectedSlotIndex: { type: Number },
hoveredItem: { type: Object }
};
}
constructor() {
super();
this.squad = [null, null, null, null];
this.selectedSlotIndex = 0;
this.hoveredItem = null;
this.mode = 'DRAFT'; // Default
this.availablePool = [];
}
connectedCallback() {
super.connectedCallback();
this._initializeData();
}
/**
* Configures the component based on provided data.
*/
_initializeData() {
// 1. If we were passed an existing roster (e.g. from RosterManager), use it.
if (this.availablePool && this.availablePool.length > 0) {
this.mode = 'ROSTER';
console.log("TeamBuilder: Using Provided Roster", this.availablePool);
return;
}
// 2. Default: Draft Mode (New Game)
// Populate with Tier 1 classes
this.mode = 'DRAFT';
this.availablePool = RAW_TIER_1_CLASSES.map(cls => {
const meta = CLASS_METADATA[cls.id] || {};
return { ...cls, ...meta, unlocked: true };
});
console.log("TeamBuilder: Initializing Draft Mode");
}
render() {
const isSquadValid = this.squad.some(u => u !== null);
return html`
<div class="container">
<!-- ROSTER PANEL -->
<div class="roster-panel">
<h3>${this.mode === 'DRAFT' ? 'Recruit Explorers' : 'Barracks Roster'}</h3>
${this.availablePool.map(item => {
const isSelected = this.squad.some(s => s && (this.mode === 'ROSTER' ? s.id === item.id : false));
return html`
<button
type="button"
class="card ${isSelected ? 'selected' : ''}"
?disabled="${this.mode === 'DRAFT' && !item.unlocked || isSelected}"
@click="${() => this._assignItem(item)}"
@mouseenter="${() => this.hoveredItem = item}"
@mouseleave="${() => this.hoveredItem = null}"
>
<div class="icon" style="font-size: 1.5rem;">
${item.icon || CLASS_METADATA[item.classId]?.icon || '⚔️'}
</div>
<div>
<strong>${item.name}</strong><br>
<small>${this.mode === 'ROSTER' ? `Lvl ${item.level || 1} ${item.classId.replace('CLASS_', '')}` : item.role}</small>
</div>
</button>
`;
})}
</div>
<!-- SQUAD SLOTS -->
<div class="squad-panel">
${this.squad.map((unit, index) => html`
<div class="slot-wrapper">
<button
type="button"
class="squad-slot ${unit ? 'filled' : ''} ${this.selectedSlotIndex === index ? 'selected' : ''}"
@click="${() => this._selectSlot(index)}"
>
${unit
? html`
<!-- Use image property if available, otherwise show large icon placeholder -->
${unit.image
? html`<img src="${unit.image}" alt="${unit.name}" class="unit-image" onerror="this.style.display='none'; this.nextElementSibling.style.display='flex'">`
: ''
}
<div class="placeholder-img" style="${unit.image ? 'display:none;' : ''} font-size: 3rem;">
${unit.icon || '🛡️'}
</div>
<div class="unit-info">
<strong>${unit.name}</strong>
<small style="font-size: 0.7rem; color: #aaa;">${this.mode === 'DRAFT' ? unit.role : unit.classId.replace('CLASS_', '')}</small>
</div>
`
: html`
<div class="placeholder-img">+</div>
<div class="unit-info" style="background:transparent;">
<span>Slot ${index + 1}</span>
<small>Select ${this.mode === 'DRAFT' ? 'Class' : 'Unit'}</small>
</div>
`
}
</button>
${unit ? html`<button type="button" class="remove-btn" @click="${() => this._removeUnit(index)}">X</button>` : ''}
</div>
`)}
</div>
<!-- DETAILS PANEL -->
<div class="details-panel">
${this._renderDetails()}
</div>
<!-- FOOTER -->
<div class="footer">
<button type="button" class="embark-btn" ?disabled="${!isSquadValid}" @click="${this._handleEmbark}">
${this.mode === 'DRAFT' ? 'INITIALIZE SQUAD' : 'EMBARK'}
</button>
</div>
</div>
`;
}
_renderDetails() {
if (!this.hoveredItem) return html`<p>Hover over a unit to see details.</p>`;
// Handle data structure diffs between ClassDef and UnitInstance
const name = this.hoveredItem.name;
const role = this.hoveredItem.role || this.hoveredItem.classId;
const stats = this.hoveredItem.base_stats || this.hoveredItem.stats || {};
return html`
<h2>${name}</h2>
<p><em>${role}</em></p>
<hr>
<p>${this.hoveredItem.description || 'Ready for deployment.'}</p>
<h4>Stats</h4>
<ul>
<li>HP: ${stats.health}</li>
<li>Atk: ${stats.attack || 0}</li>
<li>Spd: ${stats.speed}</li>
</ul>
`;
}
_selectSlot(index) {
this.selectedSlotIndex = index;
}
_assignItem(item) {
if (this.mode === 'DRAFT' && !item.unlocked) return;
let unitManifest;
if (this.mode === 'DRAFT') {
// Create new unit definition
unitManifest = {
classId: item.id,
name: item.name,
icon: item.icon,
image: item.image, // Pass image path
role: item.role,
isNew: true // Flag for GameLoop/Manager to generate ID
};
} else {
// Select existing unit
// Try to recover image from CLASS_METADATA if not stored on unit instance
const meta = CLASS_METADATA[item.classId] || {};
unitManifest = {
id: item.id,
classId: item.classId,
name: item.name,
icon: meta.icon,
image: meta.image,
role: meta.role,
...item
};
}
const newSquad = [...this.squad];
newSquad[this.selectedSlotIndex] = unitManifest;
this.squad = newSquad;
if (this.selectedSlotIndex < 3) this.selectedSlotIndex++;
}
_removeUnit(index) {
const newSquad = [...this.squad];
newSquad[index] = null;
this.squad = newSquad;
this.selectedSlotIndex = index;
}
_handleEmbark() {
const manifest = this.squad.filter(u => u !== null);
this.dispatchEvent(new CustomEvent('embark', {
detail: { squad: manifest, mode: this.mode },
bubbles: true,
composed: true
}));
}
// Helpers to make IDs readable (e.g. "ITEM_RUSTY_BLADE" -> "Rusty Blade")
_formatItemName(id) {
return id.replace('ITEM_', '').replace(/_/g, ' ').toLowerCase().replace(/\b\w/g, l => l.toUpperCase());
}
_formatSkillName(id) {
return id.replace('SKILL_', '').replace(/_/g, ' ').toLowerCase().replace(/\b\w/g, l => l.toUpperCase());
}
}
customElements.define('team-builder', TeamBuilder);