466 lines
No EOL
14 KiB
JavaScript
466 lines
No EOL
14 KiB
JavaScript
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); |