diff --git a/src/ui/combat-hud.d.ts b/src/ui/combat-hud.d.ts new file mode 100644 index 0000000..db8f4f7 --- /dev/null +++ b/src/ui/combat-hud.d.ts @@ -0,0 +1,54 @@ +export interface CombatState { + /** The unit currently taking their turn */ + activeUnit: UnitStatus | null; + + /** Sorted list of units acting next */ + turnQueue: QueueEntry[]; + + /** Is the player currently targeting a skill? */ + targetingMode: boolean; + + /** Global combat info */ + roundNumber: number; +} + +export interface UnitStatus { + id: string; + name: string; + portrait: string; + hp: { current: number; max: number }; + ap: { current: number; max: number }; + charge: number; // 0-100 + statuses: StatusIcon[]; + skills: SkillButton[]; +} + +export interface QueueEntry { + unitId: string; + portrait: string; + team: "PLAYER" | "ENEMY"; + /** 0-100 progress to next turn */ + initiative: number; +} + +export interface StatusIcon { + id: string; + icon: string; // URL or Emoji + turnsRemaining: number; + description: string; +} + +export interface SkillButton { + id: string; + name: string; + icon: string; + costAP: number; + cooldown: number; // 0 = Ready + isAvailable: boolean; // True if affordable and ready +} + +export interface CombatEvents { + "skill-click": { skillId: string }; + "end-turn": void; + "hover-skill": { skillId: string }; // For showing range grid +} diff --git a/src/ui/combat-hud.js b/src/ui/combat-hud.js index e39a4ad..7b56e48 100644 --- a/src/ui/combat-hud.js +++ b/src/ui/combat-hud.js @@ -13,85 +13,574 @@ export class CombatHUD extends LitElement { pointer-events: none; font-family: "Courier New", monospace; color: white; + z-index: 1000; } - .header { + /* Top Bar */ + .top-bar { position: absolute; - top: 20px; - left: 50%; - transform: translateX(-50%); + top: 0; + left: 0; + right: 0; + height: 120px; + background: linear-gradient( + to bottom, + rgba(0, 0, 0, 0.9) 0%, + rgba(0, 0, 0, 0.7) 80%, + transparent 100% + ); + pointer-events: auto; + display: flex; + align-items: center; + justify-content: space-between; + padding: 20px 30px; + } + + .turn-queue { + display: flex; + align-items: center; + gap: 15px; + flex: 1; + } + + .queue-portrait { + width: 60px; + height: 60px; + border-radius: 50%; + border: 2px solid #666; + overflow: hidden; background: rgba(0, 0, 0, 0.8); - border: 2px solid #ff0000; - padding: 15px 30px; - text-align: center; + position: relative; pointer-events: auto; } - .status-bar { - margin-top: 5px; - font-size: 1.2rem; + .queue-portrait.active { + width: 80px; + height: 80px; + border: 3px solid #ffd700; + box-shadow: 0 0 10px rgba(255, 215, 0, 0.5); + } + + .queue-portrait img { + width: 100%; + height: 100%; + object-fit: cover; + } + + .queue-portrait.enemy { + border-color: #ff6666; + } + + .queue-portrait.player { + border-color: #66ff66; + } + + .enemy-intent { + position: absolute; + bottom: -5px; + right: -5px; + width: 20px; + height: 20px; + background: rgba(0, 0, 0, 0.9); + border: 1px solid #ff6666; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + font-size: 12px; + } + + .global-info { + display: flex; + flex-direction: column; + align-items: flex-end; + gap: 8px; + background: rgba(0, 0, 0, 0.8); + border: 2px solid #555; + padding: 10px 20px; + pointer-events: auto; + } + + .round-counter { + font-size: 1.1rem; + font-weight: bold; + } + + .threat-level { + font-size: 0.9rem; + padding: 2px 8px; + border-radius: 3px; + } + + .threat-level.low { + background: rgba(0, 255, 0, 0.3); + color: #66ff66; + } + + .threat-level.medium { + background: rgba(255, 255, 0, 0.3); + color: #ffff66; + } + + .threat-level.high { + background: rgba(255, 0, 0, 0.3); color: #ff6666; } - .turn-indicator { + /* Bottom Bar */ + .bottom-bar { position: absolute; - top: 100px; - left: 30px; - background: rgba(0, 0, 0, 0.8); - border: 2px solid #ff0000; - padding: 10px 20px; - font-size: 1rem; + bottom: 0; + left: 0; + right: 0; + height: 180px; + background: linear-gradient( + to top, + rgba(0, 0, 0, 0.9) 0%, + rgba(0, 0, 0, 0.7) 80%, + transparent 100% + ); + pointer-events: auto; + display: flex; + align-items: flex-end; + justify-content: space-between; + padding: 20px 30px; } - .instructions { + /* Unit Status (Bottom-Left) */ + .unit-status { + display: flex; + flex-direction: column; + gap: 10px; + background: rgba(0, 0, 0, 0.8); + border: 2px solid #555; + padding: 15px; + min-width: 200px; + pointer-events: auto; + } + + .unit-portrait { + width: 120px; + height: 120px; + border: 2px solid #666; + overflow: hidden; + background: rgba(0, 0, 0, 0.9); + margin: 0 auto; + } + + .unit-portrait img { + width: 100%; + height: 100%; + object-fit: cover; + } + + .unit-name { + text-align: center; + font-size: 1rem; + font-weight: bold; + margin-top: 5px; + } + + .status-icons { + display: flex; + gap: 5px; + justify-content: center; + flex-wrap: wrap; + margin-top: 5px; + } + + .status-icon { + width: 24px; + height: 24px; + border: 1px solid #555; + border-radius: 3px; + display: flex; + align-items: center; + justify-content: center; + font-size: 14px; + background: rgba(0, 0, 0, 0.7); + cursor: help; + position: relative; + } + + .status-icon:hover::after { + content: attr(data-description); position: absolute; - bottom: 30px; + bottom: 100%; left: 50%; transform: translateX(-50%); + background: rgba(0, 0, 0, 0.95); + border: 1px solid #555; + padding: 5px 10px; + white-space: nowrap; + font-size: 0.8rem; + margin-bottom: 5px; + pointer-events: none; + } + + .bar-container { + display: flex; + flex-direction: column; + gap: 5px; + margin-top: 10px; + } + + .bar-label { + font-size: 0.8rem; + display: flex; + justify-content: space-between; + } + + .bar { + height: 20px; background: rgba(0, 0, 0, 0.7); border: 1px solid #555; - padding: 10px 20px; - font-size: 0.9rem; - color: #ccc; + position: relative; + overflow: hidden; + } + + .bar-fill { + height: 100%; + transition: width 0.3s ease; + } + + .bar-fill.hp { + background: #ff0000; + } + + .bar-fill.ap { + background: #ffaa00; + } + + .bar-fill.charge { + background: #0066ff; + } + + /* Action Bar (Bottom-Center) */ + .action-bar { + display: flex; + gap: 10px; + align-items: center; + pointer-events: auto; + } + + .skill-button { + width: 70px; + height: 70px; + background: rgba(0, 0, 0, 0.8); + border: 2px solid #666; + cursor: pointer; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 5px; + position: relative; + transition: all 0.2s; + pointer-events: auto; + } + + .skill-button:hover:not(:disabled) { + border-color: #ffd700; + box-shadow: 0 0 10px rgba(255, 215, 0, 0.5); + transform: translateY(-2px); + } + + .skill-button:disabled { + opacity: 0.5; + cursor: not-allowed; + border-color: #333; + } + + .skill-button .hotkey { + position: absolute; + top: 2px; + left: 2px; + font-size: 0.7rem; + background: rgba(0, 0, 0, 0.8); + padding: 2px 4px; + border: 1px solid #555; + } + + .skill-button .icon { + font-size: 1.5rem; + margin-top: 8px; + } + + .skill-button .name { + font-size: 0.7rem; text-align: center; + padding: 0 4px; + } + + .skill-button .cost { + position: absolute; + bottom: 2px; + right: 2px; + font-size: 0.7rem; + color: #ffaa00; + } + + .skill-button .cooldown { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: rgba(0, 0, 0, 0.7); + display: flex; + align-items: center; + justify-content: center; + font-size: 1.2rem; + font-weight: bold; + } + + /* End Turn Button (Bottom-Right) */ + .end-turn-button { + background: rgba(0, 0, 0, 0.8); + border: 2px solid #ff6666; + padding: 15px 30px; + font-size: 1.1rem; + font-weight: bold; + color: white; + cursor: pointer; + transition: all 0.2s; + pointer-events: auto; + font-family: "Courier New", monospace; + } + + .end-turn-button:hover { + background: rgba(255, 102, 102, 0.2); + box-shadow: 0 0 15px rgba(255, 102, 102, 0.5); + transform: translateY(-2px); + } + + .end-turn-button:active { + transform: translateY(0); + } + + /* Responsive Design - Mobile (< 768px) */ + @media (max-width: 767px) { + .bottom-bar { + flex-direction: column; + align-items: stretch; + gap: 15px; + height: auto; + min-height: 180px; + } + + .unit-status { + width: 100%; + min-width: auto; + } + + .action-bar { + justify-content: center; + flex-wrap: wrap; + } + + .end-turn-button { + width: 100%; + margin-top: 10px; + } + + .top-bar { + flex-direction: column; + height: auto; + min-height: 120px; + gap: 10px; + } + + .turn-queue { + justify-content: center; + flex-wrap: wrap; + } + + .global-info { + align-items: center; + width: 100%; + } } `; } static get properties() { return { - currentState: { type: String }, - currentTurn: { type: String }, + combatState: { type: Object }, }; } constructor() { super(); - this.currentState = null; - this.currentTurn = "PLAYER"; - window.addEventListener("gamestate-changed", (e) => { - this.currentState = e.detail.newState; - }); + this.combatState = null; + } + + _handleSkillClick(skillId) { + this.dispatchEvent( + new CustomEvent("skill-click", { + detail: { skillId }, + bubbles: true, + composed: true, + }) + ); + } + + _handleEndTurn() { + this.dispatchEvent( + new CustomEvent("end-turn", { + bubbles: true, + composed: true, + }) + ); + } + + _handleSkillHover(skillId) { + this.dispatchEvent( + new CustomEvent("hover-skill", { + detail: { skillId }, + bubbles: true, + composed: true, + }) + ); + } + + _getThreatLevel() { + if (!this.combatState?.turnQueue) return "low"; + const enemyCount = this.combatState.turnQueue.filter( + (entry) => entry.team === "ENEMY" + ).length; + if (enemyCount >= 3) return "high"; + if (enemyCount >= 2) return "medium"; + return "low"; + } + + _renderBar(label, current, max, type) { + const percentage = max > 0 ? (current / max) * 100 : 0; + return html` +
+ `; } render() { - // Only show during COMBAT state - if (this.currentState !== "STATE_COMBAT") { + if (!this.combatState) { return html``; } + const { activeUnit, turnQueue, roundNumber } = this.combatState; + const threatLevel = this._getThreatLevel(); + return html` -