From dbfa9929dddd6832b7f8e6a2303db16c9d63db4a Mon Sep 17 00:00:00 2001 From: Matthew Mone Date: Wed, 14 Jan 2026 11:11:51 -0800 Subject: [PATCH] refactor: Enhance character sheet and roster management --- src/managers/RosterManager.js | 92 +++++- src/ui/components/character-sheet.js | 8 +- src/ui/screens/BarracksScreen.js | 401 +++++++++++++++++---------- 3 files changed, 354 insertions(+), 147 deletions(-) diff --git a/src/managers/RosterManager.js b/src/managers/RosterManager.js index 9fde131..efa066f 100644 --- a/src/managers/RosterManager.js +++ b/src/managers/RosterManager.js @@ -16,6 +16,8 @@ export class RosterManager { this.roster = []; // List of active Explorer objects (Data only) /** @type {ExplorerData[]} */ this.graveyard = []; // List of dead units + /** @type {ExplorerData[]} */ + this.candidates = []; // Pool of recruitable units /** @type {number} */ this.rosterLimit = 12; } @@ -27,6 +29,20 @@ export class RosterManager { load(saveData) { this.roster = saveData.roster || []; this.graveyard = saveData.graveyard || []; + this.candidates = saveData.candidates || []; + + // DATA MIGRATION: Fix legacy portrait paths + const fixPortrait = (unit) => { + if (unit.portrait && unit.portrait.includes("class_")) { + // Replace "class_" with "" in the filename part + // e.g. assets/images/portraits/class_weaver.png -> assets/images/portraits/weaver.png + // Use regex to be safe: /class_([a-z]+)\.png/ + unit.portrait = unit.portrait.replace(/class_/g, ""); + } + }; + + this.roster.forEach(fixPortrait); + this.candidates.forEach(fixPortrait); } /** @@ -37,6 +53,7 @@ export class RosterManager { return { roster: this.roster, graveyard: this.graveyard, + candidates: this.candidates, }; } @@ -98,7 +115,80 @@ export class RosterManager { * Clears the roster and graveyard. Used when starting a new game. */ clear() { - this.roster = []; this.graveyard = []; + this.candidates = []; + } + + /** + * Generates a new set of candidates for recruitment. + * @param {number} count - Number of candidates to generate + */ + async generateCandidates(count = 3) { + this.candidates = []; + + // Lazy import name generator + const { generateCharacterName } = await import("../utils/nameGenerator.js"); + + // Available classes (basic pool) + const classes = [ + "CLASS_VANGUARD", + "CLASS_WEAVER", + "CLASS_SCAVENGER", + "CLASS_TINKER", + ]; + + for (let i = 0; i < count; i++) { + const randomClass = classes[Math.floor(Math.random() * classes.length)]; + const name = generateCharacterName(); + + const candidate = { + id: `CANDIDATE_${Date.now()}_${i}`, + name: name, + className: randomClass, // ID used for lookup + activeClassId: randomClass, + level: 1, + status: "READY", + hiringCost: 100, // Fixed cost for now + portrait: `assets/images/portraits/${randomClass + .replace("CLASS_", "") + .toLowerCase()}.png`, + }; + + this.candidates.push(candidate); + } + } + + /** + * Hires a candidate, moving them to the roster. + * @param {string} candidateId + * @param {Object} wallet - Reference to wallet to deduct funds (if handled here, but manager acts on data) + * @returns {Promise} success + */ + async hireCandidate(candidateId) { + if (this.roster.length >= this.rosterLimit) { + console.warn("Roster full."); + return false; + } + + const index = this.candidates.findIndex((c) => c.id === candidateId); + if (index === -1) return false; + + const candidate = this.candidates[index]; + + // Recruit logic specific to candidate -> unit conversion + // (We can reused recruitUnit but we need to ensure ID is unique/updated if needed, though candidate ID is fine usually) + // Actually, recruitUnit generates a new ID. Let's use that to be safe and consistent. + + await this.recruitUnit({ + name: candidate.name, + className: candidate.className, + activeClassId: candidate.activeClassId, + portrait: candidate.portrait, + }); + + // Remove from candidates + this.candidates.splice(index, 1); + + return true; } } diff --git a/src/ui/components/character-sheet.js b/src/ui/components/character-sheet.js index 4b1e3d7..e4f30f9 100644 --- a/src/ui/components/character-sheet.js +++ b/src/ui/components/character-sheet.js @@ -433,8 +433,8 @@ export class CharacterSheet extends LitElement { } .equipment-slot { - width: clamp(50px, 7cqw, 70px); - height: clamp(50px, 7cqw, 70px); + width: 100%; + height: 100%; background: rgba(0, 0, 0, 0.6); border: 2px solid #555; display: flex; @@ -443,6 +443,7 @@ export class CharacterSheet extends LitElement { cursor: pointer; transition: all 0.2s; position: relative; + padding: 0; } .equipment-slot:hover { @@ -508,7 +509,6 @@ export class CharacterSheet extends LitElement { width: 100%; height: 100%; object-fit: contain; - padding: 5px; } .slot-label { @@ -591,6 +591,7 @@ export class CharacterSheet extends LitElement { cursor: pointer; transition: all 0.2s; position: relative; + padding: 0; } .item-card:hover { @@ -630,7 +631,6 @@ export class CharacterSheet extends LitElement { width: 100%; height: 100%; object-fit: contain; - padding: 5px; } .skills-container { diff --git a/src/ui/screens/BarracksScreen.js b/src/ui/screens/BarracksScreen.js index b386208..c805fab 100644 --- a/src/ui/screens/BarracksScreen.js +++ b/src/ui/screens/BarracksScreen.js @@ -33,11 +33,12 @@ export class BarracksScreen extends LitElement { font-family: var(--font-family); display: grid; grid-template-columns: 60% 40%; - grid-template-rows: auto 1fr; + grid-template-rows: auto auto 1fr; grid-template-areas: "header header" - "roster detail"; - gap: var(--spacing-lg); + "tabs tabs" + "content content"; + gap: var(--spacing-md); } .header { @@ -118,9 +119,8 @@ export class BarracksScreen extends LitElement { color: var(--color-text-primary); } - /* Roster List */ .roster-list { - grid-area: roster; + /* grid-area: roster; - Removed to allow nesting */ display: flex; flex-direction: column; gap: var(--spacing-md); @@ -244,7 +244,7 @@ export class BarracksScreen extends LitElement { /* Detail Sidebar */ .detail-sidebar { - grid-area: detail; + /* grid-area: detail; - Removed to allow nesting */ background: var(--color-bg-panel); border: var(--border-width-medium) solid var(--color-border-default); padding: var(--spacing-lg); @@ -339,7 +339,9 @@ export class BarracksScreen extends LitElement { filter: { type: String }, sort: { type: String }, healingCostPerHp: { type: Number }, + wallet: { type: Object }, + viewMode: { type: String }, // "ROSTER" | "RECRUIT" }; } @@ -351,12 +353,23 @@ export class BarracksScreen extends LitElement { this.sort = "LEVEL_DESC"; this.healingCostPerHp = 0.5; // 1 HP = 0.5 Shards this.wallet = { aetherShards: 0, ancientCores: 0 }; + this.viewMode = "ROSTER"; } connectedCallback() { super.connectedCallback(); this._loadRoster(); this._loadWallet(); + + // Auto-generate candidates if none exist + if ( + !gameStateManager.rosterManager.candidates || + gameStateManager.rosterManager.candidates.length === 0 + ) { + gameStateManager.rosterManager + .generateCandidates(4) + .then(() => this.requestUpdate()); + } } _loadRoster() { @@ -455,7 +468,9 @@ export class BarracksScreen extends LitElement { let portrait = unitData.portrait || unitData.image; if (!portrait) { // Default portrait path based on class - portrait = `assets/images/portraits/${activeClassId.toLowerCase()}.png`; + portrait = `assets/images/portraits/${activeClassId + .replace("CLASS_", "") + .toLowerCase()}.png`; } return { @@ -688,7 +703,9 @@ export class BarracksScreen extends LitElement { explorer.portrait = rosterData.portrait || rosterData.image; } else { // Default portrait path - explorer.portrait = `assets/images/portraits/${activeClassId.toLowerCase()}.png`; + explorer.portrait = `assets/images/portraits/${activeClassId + .replace("CLASS_", "") + .toLowerCase()}.png`; } // Generate skill tree if gameLoop isn't running @@ -844,109 +861,232 @@ export class BarracksScreen extends LitElement { `; } - _renderDetailSidebar() { - const unit = this._getSelectedUnit(); - if (!unit) { - return html` -
-
-

Select a unit to view details

-
-
- `; + _renderRecruitView() { + const candidates = gameStateManager.rosterManager.candidates || []; + + if (candidates.length === 0) { + return html`
+ No recruits available at this time. +
`; } - const healCost = this._calculateHealCost(unit); + return html` +
+ ${candidates.map( + (candidate) => html` +
+
+ Portrait +
+
+
${candidate.name}
+
+ ${candidate.className.replace("CLASS_", "")} + Lvl 1 +
+
+
+ +
+
+ ` + )} +
+
+
+

Recruitment Office

+

+ New candidates arrive periodically. Hire them to expand your squad. +

+
+
+ `; + } + + async _onHireClick(candidate) { + if (this.wallet.aetherShards < candidate.hiringCost) { + alert("Insufficient funds."); + return; + } + + // Deduct Funds + if (gameStateManager.hubStash && gameStateManager.hubStash.currency) { + gameStateManager.hubStash.currency.aetherShards -= candidate.hiringCost; + this.wallet.aetherShards = + gameStateManager.hubStash.currency.aetherShards; + + // Calls RosterManager to move candidate to roster + const success = await gameStateManager.rosterManager.hireCandidate( + candidate.id + ); + + if (success) { + // Save everything + await gameStateManager.persistence.saveRoster( + gameStateManager.rosterManager.save() + ); + await gameStateManager.persistence.saveHubStash( + gameStateManager.hubStash + ); + + // Update UI + this._loadRoster(); + this.requestUpdate(); + + window.dispatchEvent( + new CustomEvent("wallet-updated", { + detail: { wallet: { ...this.wallet } }, + bubbles: true, + composed: true, + }) + ); + } else { + alert("Roster is full!"); + // Refund if failed (simple rollback) + gameStateManager.hubStash.currency.aetherShards += candidate.hiringCost; + this.wallet.aetherShards = + gameStateManager.hubStash.currency.aetherShards; + } + } + } + + _renderRosterView() { + const filteredUnits = this._getFilteredUnits(); + const selectedUnit = this._getSelectedUnit(); + + return html` +
+
+
+ + + +
+ +
+ ${filteredUnits.length === 0 + ? html`
+ No units found matching filter. +
` + : filteredUnits.map((unit) => this._renderUnitCard(unit))} +
+
+ + + ${selectedUnit + ? this._renderDetailContent(selectedUnit) + : html` +
+
Select a unit to view details
+
+ `} +
+ `; + } + + _renderDetailContent(selectedUnit) { + const healCost = this._calculateHealCost(selectedUnit); const canHeal = - unit.currentHp < unit.maxHp && this.wallet.aetherShards >= healCost; - const isInjured = unit.currentHp < unit.maxHp; - const hpPercent = (unit.currentHp / unit.maxHp) * 100; + selectedUnit.currentHp < selectedUnit.maxHp && + this.wallet.aetherShards >= healCost; return html`
- ${unit.portrait + ${selectedUnit.portrait ? html`${unit.name} { - e.target.style.display = "none"; - }} - onerror="this.style.display='none'; this.nextElementSibling.style.display='flex';" - /> - - ${unit.classId - ? unit.classId.replace("CLASS_", "")[0] - : "?"} - ` - : html` - ${unit.classId ? unit.classId.replace("CLASS_", "")[0] : "?"} - `} + src="${selectedUnit.portrait}" + alt="${selectedUnit.name}" + />` + : html`👤`}
-
-

- ${unit.name} -

-

- ${unit.classId?.replace("CLASS_", "") || "Unknown"} • Level - ${unit.level} -

+
${selectedUnit.name}
+
+ ${selectedUnit.classId.replace("CLASS_", "")} + Lv ${selectedUnit.level}
Health - ${unit.currentHp} / ${unit.maxHp} + ${selectedUnit.currentHp}/${selectedUnit.maxHp}
-
-
-
${Math.round(hpPercent)}%
+
+
+
+
Status - ${unit.status} + ${selectedUnit.status}
- - ${isInjured + ${selectedUnit.currentHp < selectedUnit.maxHp ? html` - ${!canHeal && healCost > 0 - ? html` -
- Insufficient funds. Need ${healCost} Shards. -
- ` - : ""} ` - : html` - - `} + : ""} @@ -956,81 +1096,58 @@ export class BarracksScreen extends LitElement { } render() { - const filteredUnits = this._getFilteredUnits(); - const rosterCount = this.units.length; - const rosterLimit = gameStateManager.rosterManager.rosterLimit || 12; + const isRecruit = this.viewMode === "RECRUIT"; return html`
-
-

The Squad Quarters

-
- Roster: ${rosterCount}/${rosterLimit} -
- - - -
-
- - Sort: - - - - -
+
+

Squad Quarters

+
+ ${gameStateManager.rosterManager.roster.length} / + ${gameStateManager.rosterManager.rosterLimit} Units
- + +
+
+ 💎 ${this.wallet.aetherShards} +
+ +
-
- ${filteredUnits.length === 0 - ? html` -
-

No units found matching the current filter.

-
- ` - : filteredUnits.map((unit) => this._renderUnitCard(unit))} +
+ +
- ${this._renderDetailSidebar()} +
+ ${isRecruit ? this._renderRecruitView() : this._renderRosterView()} +
`; } }