2025-12-19 23:07:36 +00:00
|
|
|
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];
|
2025-12-19 16:38:22 +00:00
|
|
|
|
|
|
|
|
export class TeamBuilder extends LitElement {
|
|
|
|
|
static get styles() {
|
|
|
|
|
return css`
|
|
|
|
|
:host {
|
|
|
|
|
display: block;
|
|
|
|
|
position: absolute;
|
|
|
|
|
top: 0;
|
|
|
|
|
left: 0;
|
|
|
|
|
width: 100%;
|
|
|
|
|
height: 100%;
|
2025-12-19 23:07:36 +00:00
|
|
|
font-family: 'Courier New', monospace; /* Placeholder for Voxel Font */
|
2025-12-19 16:38:22 +00:00
|
|
|
color: white;
|
|
|
|
|
pointer-events: none; /* Let clicks pass through to 3D scene where empty */
|
|
|
|
|
z-index: 10;
|
2025-12-19 23:07:36 +00:00
|
|
|
box-sizing: border-box;
|
2025-12-19 16:38:22 +00:00
|
|
|
}
|
|
|
|
|
|
2025-12-19 23:07:36 +00:00
|
|
|
/* Responsive Container Layout */
|
2025-12-19 16:38:22 +00:00
|
|
|
.container {
|
|
|
|
|
display: grid;
|
2025-12-19 23:07:36 +00:00
|
|
|
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";
|
2025-12-19 16:38:22 +00:00
|
|
|
height: 100%;
|
|
|
|
|
width: 100%;
|
|
|
|
|
pointer-events: auto;
|
2025-12-19 23:07:36 +00:00
|
|
|
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";
|
|
|
|
|
}
|
2025-12-19 16:38:22 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* --- LEFT PANEL: ROSTER --- */
|
|
|
|
|
.roster-panel {
|
2025-12-19 23:07:36 +00:00
|
|
|
grid-area: roster;
|
2025-12-19 16:38:22 +00:00
|
|
|
background: rgba(20, 20, 30, 0.9);
|
|
|
|
|
border-right: 2px solid #555;
|
|
|
|
|
padding: 1rem;
|
|
|
|
|
overflow-y: auto;
|
2025-12-19 23:07:36 +00:00
|
|
|
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;
|
|
|
|
|
}
|
2025-12-19 16:38:22 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.class-card {
|
|
|
|
|
background: #333;
|
|
|
|
|
border: 2px solid #555;
|
2025-12-19 23:07:36 +00:00
|
|
|
padding: 15px;
|
2025-12-19 16:38:22 +00:00
|
|
|
cursor: pointer;
|
|
|
|
|
transition: all 0.2s;
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
2025-12-19 23:07:36 +00:00
|
|
|
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%;
|
|
|
|
|
}
|
2025-12-19 16:38:22 +00:00
|
|
|
}
|
|
|
|
|
|
2025-12-19 23:07:36 +00:00
|
|
|
.class-card:hover:not(:disabled) {
|
2025-12-19 16:38:22 +00:00
|
|
|
border-color: #00ffff;
|
|
|
|
|
background: #444;
|
2025-12-19 23:07:36 +00:00
|
|
|
transform: translateX(5px);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@media (max-width: 1024px) {
|
|
|
|
|
.class-card:hover:not(:disabled) {
|
|
|
|
|
transform: translateY(-5px); /* Hop up on mobile */
|
|
|
|
|
}
|
2025-12-19 16:38:22 +00:00
|
|
|
}
|
|
|
|
|
|
2025-12-19 23:07:36 +00:00
|
|
|
.class-card:disabled {
|
2025-12-19 16:38:22 +00:00
|
|
|
opacity: 0.5;
|
2025-12-19 23:07:36 +00:00
|
|
|
cursor: not-allowed;
|
2025-12-19 16:38:22 +00:00
|
|
|
filter: grayscale(1);
|
2025-12-19 23:07:36 +00:00
|
|
|
border-color: #444;
|
2025-12-19 16:38:22 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* --- CENTER PANEL: SLOTS --- */
|
|
|
|
|
.squad-panel {
|
2025-12-19 23:07:36 +00:00
|
|
|
grid-area: squad;
|
2025-12-19 16:38:22 +00:00
|
|
|
display: flex;
|
|
|
|
|
justify-content: center;
|
2025-12-19 23:07:36 +00:00
|
|
|
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);
|
2025-12-19 16:38:22 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.squad-slot {
|
2025-12-19 23:07:36 +00:00
|
|
|
width: 100%;
|
|
|
|
|
height: 100%;
|
|
|
|
|
background: rgba(10, 10, 10, 0.8);
|
|
|
|
|
border: 3px dashed #666;
|
2025-12-19 16:38:22 +00:00
|
|
|
display: flex;
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
align-items: center;
|
|
|
|
|
justify-content: center;
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
position: relative;
|
2025-12-19 23:07:36 +00:00
|
|
|
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);
|
2025-12-19 16:38:22 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.squad-slot.filled {
|
2025-12-19 23:07:36 +00:00
|
|
|
border: 3px solid #00ff00;
|
|
|
|
|
border-style: solid;
|
|
|
|
|
background: rgba(0, 20, 0, 0.8);
|
2025-12-19 16:38:22 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.squad-slot.selected {
|
|
|
|
|
border-color: #00ffff;
|
2025-12-19 23:07:36 +00:00
|
|
|
box-shadow: 0 0 20px rgba(0, 255, 255, 0.3);
|
2025-12-19 16:38:22 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.remove-btn {
|
|
|
|
|
position: absolute;
|
2025-12-19 23:07:36 +00:00
|
|
|
top: -15px;
|
|
|
|
|
right: -15px;
|
|
|
|
|
background: #cc0000;
|
|
|
|
|
border: 2px solid white;
|
2025-12-19 16:38:22 +00:00
|
|
|
color: white;
|
2025-12-19 23:07:36 +00:00
|
|
|
width: 32px;
|
|
|
|
|
height: 32px;
|
2025-12-19 16:38:22 +00:00
|
|
|
border-radius: 50%;
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
font-weight: bold;
|
2025-12-19 23:07:36 +00:00
|
|
|
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);
|
2025-12-19 16:38:22 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* --- RIGHT PANEL: DETAILS --- */
|
|
|
|
|
.details-panel {
|
2025-12-19 23:07:36 +00:00
|
|
|
grid-area: details;
|
2025-12-19 16:38:22 +00:00
|
|
|
background: rgba(20, 20, 30, 0.9);
|
|
|
|
|
border-left: 2px solid #555;
|
2025-12-19 23:07:36 +00:00
|
|
|
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;
|
|
|
|
|
}
|
2025-12-19 16:38:22 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* --- FOOTER --- */
|
|
|
|
|
.footer {
|
2025-12-19 23:07:36 +00:00
|
|
|
grid-area: footer;
|
2025-12-19 16:38:22 +00:00
|
|
|
display: flex;
|
|
|
|
|
justify-content: center;
|
|
|
|
|
align-items: center;
|
|
|
|
|
background: rgba(10, 10, 20, 0.95);
|
|
|
|
|
border-top: 2px solid #555;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.embark-btn {
|
2025-12-19 23:07:36 +00:00
|
|
|
padding: 15px 60px;
|
|
|
|
|
font-size: 1.8rem;
|
2025-12-19 16:38:22 +00:00
|
|
|
background: #008800;
|
|
|
|
|
color: white;
|
2025-12-19 23:07:36 +00:00
|
|
|
border: 3px solid #00ff00;
|
2025-12-19 16:38:22 +00:00
|
|
|
cursor: pointer;
|
|
|
|
|
text-transform: uppercase;
|
|
|
|
|
font-weight: bold;
|
2025-12-19 23:07:36 +00:00
|
|
|
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);
|
2025-12-19 16:38:22 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.embark-btn:disabled {
|
|
|
|
|
background: #333;
|
|
|
|
|
border-color: #555;
|
|
|
|
|
color: #777;
|
|
|
|
|
cursor: not-allowed;
|
2025-12-19 23:07:36 +00:00
|
|
|
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;
|
2025-12-19 16:38:22 +00:00
|
|
|
}
|
|
|
|
|
`;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static get properties() {
|
|
|
|
|
return {
|
|
|
|
|
availableClasses: { type: Array }, // Input: List of class definition objects
|
|
|
|
|
squad: { type: Array }, // Internal State: The 4 slots
|
|
|
|
|
selectedSlotIndex: { type: Number },
|
2025-12-19 23:07:36 +00:00
|
|
|
hoveredClass: { type: Object }
|
2025-12-19 16:38:22 +00:00
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
constructor() {
|
|
|
|
|
super();
|
|
|
|
|
this.squad = [null, null, null, null];
|
|
|
|
|
this.selectedSlotIndex = 0; // Default to first slot
|
|
|
|
|
this.hoveredClass = null;
|
2025-12-19 23:07:36 +00:00
|
|
|
|
|
|
|
|
// 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);
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-12-19 16:38:22 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
render() {
|
2025-12-19 23:07:36 +00:00
|
|
|
const isSquadValid = this.squad.some(u => u !== null);
|
2025-12-19 16:38:22 +00:00
|
|
|
|
|
|
|
|
return html`
|
|
|
|
|
<div class="container">
|
2025-12-19 23:07:36 +00:00
|
|
|
|
2025-12-19 16:38:22 +00:00
|
|
|
<!-- ROSTER LIST -->
|
|
|
|
|
<div class="roster-panel">
|
|
|
|
|
<h3>Roster</h3>
|
2025-12-19 23:07:36 +00:00
|
|
|
${this.availableClasses.map(cls => html`
|
|
|
|
|
<button
|
|
|
|
|
type="button"
|
|
|
|
|
class="class-card"
|
|
|
|
|
?disabled="${!cls.unlocked}"
|
|
|
|
|
@click="${() => this._assignClass(cls)}"
|
|
|
|
|
@mouseenter="${() => this.hoveredClass = cls}"
|
|
|
|
|
@mouseleave="${() => this.hoveredClass = null}"
|
|
|
|
|
aria-label="Select Class: ${cls.name}"
|
|
|
|
|
>
|
|
|
|
|
<div class="icon" style="font-size: 1.5rem;">${cls.icon || '⚔️'}</div>
|
|
|
|
|
<div>
|
|
|
|
|
<strong>${cls.name}</strong><br>
|
|
|
|
|
<small>${cls.role || 'Tier ' + cls.tier}</small>
|
2025-12-19 16:38:22 +00:00
|
|
|
</div>
|
2025-12-19 23:07:36 +00:00
|
|
|
</button>
|
|
|
|
|
`)}
|
2025-12-19 16:38:22 +00:00
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- CENTER SQUAD SLOTS -->
|
|
|
|
|
<div class="squad-panel">
|
2025-12-19 23:07:36 +00:00
|
|
|
${this.squad.map((unit, index) => html`
|
|
|
|
|
<div class="slot-wrapper">
|
|
|
|
|
<button
|
|
|
|
|
type="button"
|
|
|
|
|
class="squad-slot ${unit ? 'filled' : ''} ${this.selectedSlotIndex === index ? 'selected' : ''}"
|
2025-12-19 16:38:22 +00:00
|
|
|
@click="${() => this._selectSlot(index)}"
|
2025-12-19 23:07:36 +00:00
|
|
|
aria-label="${unit ? `Slot ${index + 1}: ${unit.name}` : `Slot ${index + 1}: Empty`}"
|
|
|
|
|
aria-pressed="${this.selectedSlotIndex === index}"
|
2025-12-19 16:38:22 +00:00
|
|
|
>
|
2025-12-19 23:07:36 +00:00
|
|
|
${unit
|
2025-12-19 16:38:22 +00:00
|
|
|
? html`
|
2025-12-19 23:07:36 +00:00
|
|
|
<!-- 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="unit-image placeholder-img" style="${unit.image ? 'display:none' : ''}">
|
|
|
|
|
${unit.icon || '🛡️'}
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div class="unit-info">
|
|
|
|
|
<strong>${unit.name}</strong>
|
|
|
|
|
<small>${this.availableClasses.find(c => c.id === unit.classId)?.role}</small>
|
|
|
|
|
</div>
|
|
|
|
|
`
|
|
|
|
|
: html`
|
|
|
|
|
<div class="placeholder-img" style="background:transparent; color: #555;">+</div>
|
|
|
|
|
<div class="unit-info" style="background:transparent;">
|
|
|
|
|
<span>Slot ${index + 1}</span>
|
|
|
|
|
<small>Empty</small>
|
2025-12-19 16:38:22 +00:00
|
|
|
</div>
|
|
|
|
|
`
|
2025-12-19 23:07:36 +00:00
|
|
|
}
|
|
|
|
|
</button>
|
|
|
|
|
|
|
|
|
|
${unit
|
|
|
|
|
? html`
|
|
|
|
|
<button
|
|
|
|
|
type="button"
|
|
|
|
|
class="remove-btn"
|
|
|
|
|
@click="${() => this._removeUnit(index)}"
|
|
|
|
|
aria-label="Remove ${unit.name} from Slot ${index + 1}"
|
|
|
|
|
>
|
|
|
|
|
X
|
|
|
|
|
</button>`
|
|
|
|
|
: ''
|
|
|
|
|
}
|
|
|
|
|
</div>
|
|
|
|
|
`)}
|
2025-12-19 16:38:22 +00:00
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- RIGHT DETAILS PANEL -->
|
|
|
|
|
<div class="details-panel">
|
2025-12-19 23:07:36 +00:00
|
|
|
${this.hoveredClass
|
2025-12-19 16:38:22 +00:00
|
|
|
? html`
|
2025-12-19 23:07:36 +00:00
|
|
|
<div>
|
|
|
|
|
<h2>${this.hoveredClass.name}</h2>
|
|
|
|
|
<p><em>${this.hoveredClass.role || 'Tier ' + this.hoveredClass.tier} Class</em></p>
|
|
|
|
|
<hr>
|
|
|
|
|
<p>${this.hoveredClass.description || 'A skilled explorer ready for the depths.'}</p>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div>
|
|
|
|
|
<h4>Base Stats</h4>
|
|
|
|
|
<ul>
|
|
|
|
|
<li>HP: ${this.hoveredClass.base_stats?.health}</li>
|
|
|
|
|
<li>Atk: ${this.hoveredClass.base_stats?.attack}</li>
|
|
|
|
|
<li>Def: ${this.hoveredClass.base_stats?.defense}</li>
|
|
|
|
|
<li>Mag: ${this.hoveredClass.base_stats?.magic}</li>
|
|
|
|
|
<li>Spd: ${this.hoveredClass.base_stats?.speed}</li>
|
|
|
|
|
<li>Will: ${this.hoveredClass.base_stats?.willpower}</li>
|
|
|
|
|
<li>Move: ${this.hoveredClass.base_stats?.movement}</li>
|
|
|
|
|
${this.hoveredClass.base_stats?.tech ? html`<li>Tech: ${this.hoveredClass.base_stats.tech}</li>` : ''}
|
|
|
|
|
</ul>
|
|
|
|
|
|
|
|
|
|
<h4>Starting Gear</h4>
|
|
|
|
|
<ul>
|
|
|
|
|
${this.hoveredClass.starting_equipment
|
|
|
|
|
? this.hoveredClass.starting_equipment.map(item => html`<li>${this._formatItemName(item)}</li>`)
|
|
|
|
|
: html`<li>None</li>`}
|
|
|
|
|
</ul>
|
|
|
|
|
</div>
|
2025-12-19 16:38:22 +00:00
|
|
|
`
|
2025-12-19 23:07:36 +00:00
|
|
|
: html`<p>Hover over a class or squad member to see details.</p>`
|
|
|
|
|
}
|
2025-12-19 16:38:22 +00:00
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- FOOTER -->
|
|
|
|
|
<div class="footer">
|
2025-12-19 23:07:36 +00:00
|
|
|
<button
|
|
|
|
|
type="button"
|
|
|
|
|
class="embark-btn"
|
2025-12-19 16:38:22 +00:00
|
|
|
?disabled="${!isSquadValid}"
|
|
|
|
|
@click="${this._handleEmbark}"
|
|
|
|
|
>
|
|
|
|
|
DESCEND
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
`;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// --- LOGIC ---
|
|
|
|
|
|
|
|
|
|
_selectSlot(index) {
|
|
|
|
|
this.selectedSlotIndex = index;
|
2025-12-19 23:07:36 +00:00
|
|
|
// 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;
|
|
|
|
|
}
|
2025-12-19 16:38:22 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
_assignClass(classDef) {
|
2025-12-19 23:07:36 +00:00
|
|
|
if (!classDef.unlocked && classDef.unlocked !== undefined) return; // Logic check redundancy for tests without DOM checks
|
2025-12-19 16:38:22 +00:00
|
|
|
|
|
|
|
|
// 1. Create a lightweight manifest for the slot
|
|
|
|
|
const unitManifest = {
|
|
|
|
|
classId: classDef.id,
|
2025-12-19 23:07:36 +00:00
|
|
|
name: classDef.name, // In real app, auto-generate name like "Valerius"
|
2025-12-19 16:38:22 +00:00
|
|
|
icon: classDef.icon,
|
2025-12-19 23:07:36 +00:00
|
|
|
image: classDef.image // Pass image path
|
2025-12-19 16:38:22 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// 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)
|
2025-12-19 23:07:36 +00:00
|
|
|
this.dispatchEvent(new CustomEvent('squad-update', {
|
|
|
|
|
detail: { slot: this.selectedSlotIndex, unit: unitManifest },
|
|
|
|
|
bubbles: true,
|
|
|
|
|
composed: true
|
|
|
|
|
}));
|
2025-12-19 16:38:22 +00:00
|
|
|
}
|
|
|
|
|
|
2025-12-19 23:07:36 +00:00
|
|
|
_removeUnit(index) {
|
|
|
|
|
// No stopPropagation needed as elements are siblings now
|
2025-12-19 16:38:22 +00:00
|
|
|
const newSquad = [...this.squad];
|
|
|
|
|
newSquad[index] = null;
|
|
|
|
|
this.squad = newSquad;
|
|
|
|
|
this.selectedSlotIndex = index; // Select the empty slot
|
|
|
|
|
|
|
|
|
|
// Dispatch Event (To clear 3D model)
|
2025-12-19 23:07:36 +00:00
|
|
|
this.dispatchEvent(new CustomEvent('squad-update', {
|
|
|
|
|
detail: { slot: index, unit: null },
|
|
|
|
|
bubbles: true,
|
|
|
|
|
composed: true
|
|
|
|
|
}));
|
2025-12-19 16:38:22 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
_handleEmbark() {
|
2025-12-19 23:07:36 +00:00
|
|
|
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());
|
2025-12-19 16:38:22 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-19 23:07:36 +00:00
|
|
|
customElements.define('team-builder', TeamBuilder);
|