import { LitElement, html, css } from 'lit'; // Import Tier 1 Class Definitions // Note: This assumes the build environment supports JSON imports (e.g. Import Attributes or a loader) 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 (Data not in the raw engine JSONs) const CLASS_METADATA = { 'CLASS_VANGUARD': { icon: '🛡️', image: 'assets/images/portraits/vanguard.png', // Placeholder path role: 'Tank', description: 'A heavy frontline tank specialized in absorbing damage and protecting allies.' }, 'CLASS_WEAVER': { icon: '✨', image: 'assets/images/portraits/weaver.png', role: 'Magic DPS', description: 'A master of elemental magic capable of creating powerful synergy chains.' }, 'CLASS_SCAVENGER': { icon: '🎒', image: 'assets/images/portraits/scavenger.png', role: 'Utility', description: 'Highly mobile utility expert who excels at finding loot and avoiding traps.' }, 'CLASS_TINKER': { icon: '🔧', image: 'assets/images/portraits/tinker.png', role: 'Tech', description: 'Uses ancient technology to deploy turrets and control the battlefield.' }, 'CLASS_CUSTODIAN': { icon: '🌿', image: 'assets/images/portraits/custodian.png', role: 'Healer', description: 'A spiritual healer focused on removing corruption and sustaining the squad.' } }; 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; /* Placeholder for Voxel Font */ color: white; pointer-events: none; /* Let clicks pass through to 3D scene where empty */ z-index: 10; box-sizing: border-box; } /* Responsive Container Layout */ .container { display: grid; grid-template-columns: 280px 1fr 300px; /* Wider side panels on desktop */ 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.6); /* Slightly darker background for readability */ backdrop-filter: blur(4px); } /* Mobile Layout (< 1024px) */ @media (max-width: 1024px) { .container { grid-template-columns: 1fr; grid-template-rows: 200px 1fr 200px 80px; /* Roster, Squad, Details, Footer */ 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; } @media (max-width: 1024px) { .roster-panel { flex-direction: row; overflow-x: auto; overflow-y: hidden; border-right: none; border-bottom: 2px solid #555; align-items: center; } } .class-card { background: #333; border: 2px solid #555; padding: 15px; cursor: pointer; transition: all 0.2s; display: flex; align-items: center; gap: 15px; /* Button Reset */ width: 100%; text-align: left; font-family: inherit; color: inherit; appearance: none; } @media (max-width: 1024px) { .class-card { width: 200px; /* Fixed width cards for horizontal scroll */ flex-shrink: 0; height: 80%; } } .class-card:hover:not(:disabled) { border-color: #00ffff; background: #444; transform: translateX(5px); } @media (max-width: 1024px) { .class-card:hover:not(:disabled) { transform: translateY(-5px); /* Hop up on mobile */ } } .class-card:disabled { opacity: 0.5; cursor: not-allowed; filter: grayscale(1); border-color: #444; } /* --- CENTER PANEL: SLOTS --- */ .squad-panel { grid-area: squad; display: flex; justify-content: center; align-items: center; padding: 2rem; gap: 30px; flex-wrap: wrap; /* Allow wrapping on very small screens */ overflow-y: auto; } /* Wrapper to hold the slot button and the absolute remove button as siblings */ .slot-wrapper { position: relative; width: 180px; /* Increased size */ height: 240px; /* Increased size */ 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; position: relative; overflow: hidden; /* Button Reset */ font-family: inherit; color: inherit; padding: 0; appearance: none; } /* Image placeholder style */ .unit-image { width: 100%; height: 70%; object-fit: cover; background-color: #222; /* Fallback */ border-bottom: 2px solid #555; } .unit-info { height: 30%; display: flex; flex-direction: column; justify-content: center; align-items: center; width: 100%; background: rgba(30,30,40,0.9); } .squad-slot.filled { border: 3px solid #00ff00; border-style: solid; background: rgba(0, 20, 0, 0.8); } .squad-slot.selected { border-color: #00ffff; box-shadow: 0 0 20px rgba(0, 255, 255, 0.3); } .remove-btn { position: absolute; top: -15px; right: -15px; background: #cc0000; border: 2px solid white; color: white; width: 32px; height: 32px; border-radius: 50%; cursor: pointer; font-weight: bold; z-index: 2; /* Ensure it sits on top of the slot button */ display: flex; align-items: center; justify-content: center; font-size: 1.2rem; box-shadow: 2px 2px 5px rgba(0,0,0,0.5); } .remove-btn:hover { background: #ff0000; transform: scale(1.1); } /* --- 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; } @media (max-width: 1024px) { .details-panel { border-left: none; border-top: 2px solid #555; display: grid; grid-template-columns: 1fr 1fr; /* Split content on mobile */ gap: 20px; } } /* --- FOOTER --- */ .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; transition: all 0.2s; box-shadow: 0 0 15px rgba(0, 255, 0, 0.2); } .embark-btn:hover:not(:disabled) { background: #00aa00; box-shadow: 0 0 25px rgba(0, 255, 0, 0.6); transform: scale(1.02); } .embark-btn:disabled { background: #333; border-color: #555; color: #777; cursor: not-allowed; box-shadow: none; } h2, h3, h4 { margin-top: 0; color: #00ffff; } ul { padding-left: 1.2rem; } li { margin-bottom: 5px; } /* Helper for placeholder images */ .placeholder-img { display: flex; align-items: center; justify-content: center; background: #444; color: #888; font-size: 3rem; } `; } 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.hoveredClass = null; // Initialize by merging Raw Data with UI Metadata this.availableClasses = RAW_TIER_1_CLASSES.map(cls => { const meta = CLASS_METADATA[cls.id] || {}; return { ...cls, ...meta, // Adds icon, role, description, image path unlocked: true // Default all Tier 1s to unlocked }; }); } connectedCallback() { super.connectedCallback(); this._loadMetaProgression(); } /** * Loads unlocked classes from persistence (Local Storage / Game State). * Merges Tier 2 classes into availableClasses if unlocked. */ _loadMetaProgression() { // Mock Implementation: Retrieve unlocked Tier 2 classes from a service or storage // In a real implementation, you would import a MetaProgressionManager here. // Example: const unlockedIds = MetaProgression.getUnlockedClasses(); const storedData = localStorage.getItem('aether_shards_unlocks'); if (storedData) { try { const unlocks = JSON.parse(storedData); // This is where you would fetch the full class definition for unlocked Tier 2s // and append them to this.availableClasses console.log('Loaded unlocks:', unlocks); } catch (e) { console.error('Failed to load meta progression', e); } } } render() { const isSquadValid = this.squad.some(u => u !== null); return html`

Roster

${this.availableClasses.map(cls => html` `)}
${this.squad.map((unit, index) => html`
${unit ? html` ` : '' }
`)}
${this.hoveredClass ? html`

${this.hoveredClass.name}

${this.hoveredClass.role || 'Tier ' + this.hoveredClass.tier} Class


${this.hoveredClass.description || 'A skilled explorer ready for the depths.'}

Base Stats

  • HP: ${this.hoveredClass.base_stats?.health}
  • Atk: ${this.hoveredClass.base_stats?.attack}
  • Def: ${this.hoveredClass.base_stats?.defense}
  • Mag: ${this.hoveredClass.base_stats?.magic}
  • Spd: ${this.hoveredClass.base_stats?.speed}
  • Will: ${this.hoveredClass.base_stats?.willpower}
  • Move: ${this.hoveredClass.base_stats?.movement}
  • ${this.hoveredClass.base_stats?.tech ? html`
  • Tech: ${this.hoveredClass.base_stats.tech}
  • ` : ''}

Starting Gear

    ${this.hoveredClass.starting_equipment ? this.hoveredClass.starting_equipment.map(item => html`
  • ${this._formatItemName(item)}
  • `) : html`
  • None
  • `}
` : html`

Hover over a class or squad member to see details.

` }
`; } // --- LOGIC --- _selectSlot(index) { this.selectedSlotIndex = index; // If slot has a unit, show its details in hover panel if (this.squad[index]) { // Need to find the original class ref to show details const originalClass = this.availableClasses.find(c => c.id === this.squad[index].classId); if (originalClass) this.hoveredClass = originalClass; } } _assignClass(classDef) { if (!classDef.unlocked && classDef.unlocked !== undefined) return; // Logic check redundancy for tests without DOM checks // 1. Create a lightweight manifest for the slot const unitManifest = { classId: classDef.id, name: classDef.name, // In real app, auto-generate name like "Valerius" icon: classDef.icon, image: classDef.image // Pass image path }; // 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(index) { // No stopPropagation needed as elements are siblings now 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 })); } // 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);