Spaces:
Starting
Starting
| <script lang="ts"> | |
| import { selectedUnit, selectedBuilding, myPlayerId } from '$lib/stores/game'; | |
| import { UNIT_DESCRIPTIONS, BUILDING_LABELS } from '$lib/types'; | |
| import { getSocket } from '$lib/socket'; | |
| $: unit = $selectedUnit; | |
| $: building = $selectedBuilding; | |
| $: myId = $myPlayerId; | |
| $: isVisible = !!unit || !!building; | |
| $: isOwn = unit ? unit.owner === myId : building ? building.owner === myId : false; | |
| function close() { | |
| selectedUnit.set(null); | |
| selectedBuilding.set(null); | |
| } | |
| function hpPercent(hp: number, max: number) { | |
| return Math.round((hp / max) * 100); | |
| } | |
| function hpColor(hp: number, max: number) { | |
| const r = hp / max; | |
| if (r > 0.6) return 'var(--success)'; | |
| if (r > 0.3) return 'var(--warning)'; | |
| return 'var(--danger)'; | |
| } | |
| function statusLabel(s: string): string { | |
| const map: Record<string, string> = { | |
| idle: 'Idle', | |
| moving: 'Moving', | |
| attacking: 'Attacking', | |
| mining_minerals: 'Mining minerals', | |
| mining_gas: 'Mining gas', | |
| building: 'Building', | |
| healing: 'Healing', | |
| sieged: 'Siege mode', | |
| patrolling: 'Patrolling', | |
| }; | |
| return map[s] ?? s; | |
| } | |
| function secFromTicks(t: number) { | |
| return (t / 4).toFixed(0) + 's'; | |
| } | |
| const socket = getSocket(); | |
| function cancelConstruction() { | |
| if (!building) return; | |
| socket.emit('cancel_construction', { building_id: building.id }); | |
| selectedBuilding.set(null); | |
| } | |
| </script> | |
| {#if isVisible} | |
| <!-- svelte-ignore a11y-click-events-have-key-events a11y-no-static-element-interactions --> | |
| <div class="panel-backdrop" on:click={close}></div> | |
| <div class="panel" class:is-enemy={!isOwn}> | |
| <button class="close-btn" on:click={close} aria-label="Close">✕</button> | |
| {#if unit} | |
| <div class="panel-header"> | |
| <span class="owner-dot" class:own={isOwn}></span> | |
| <span class="unit-name">{unit.unit_type.toUpperCase()}</span> | |
| <span class="owner-label">{isOwn ? 'Ally' : 'Enemy'}</span> | |
| </div> | |
| <p class="description">{UNIT_DESCRIPTIONS[unit.unit_type]}</p> | |
| <div class="stat-row"> | |
| <span class="stat-label">HP</span> | |
| <div class="hp-bar-wrap"> | |
| <div class="hp-bar" style="width:{hpPercent(unit.hp, unit.max_hp)}%; background:{hpColor(unit.hp, unit.max_hp)}"></div> | |
| </div> | |
| <span class="stat-val">{Math.ceil(unit.hp)} / {unit.max_hp}</span> | |
| </div> | |
| <div class="stat-row"> | |
| <span class="stat-label">Status</span> | |
| <span class="stat-val status">{statusLabel(unit.status)}</span> | |
| </div> | |
| {#if unit.is_sieged} | |
| <div class="badge-row"> | |
| <span class="badge orange">🔒 Siege mode active</span> | |
| </div> | |
| {/if} | |
| {#if unit.is_cloaked} | |
| <div class="badge-row"> | |
| <span class="badge purple">🫥 Cloaked</span> | |
| </div> | |
| {/if} | |
| {:else if building} | |
| {@const label = BUILDING_LABELS[building.building_type]} | |
| <div class="panel-header"> | |
| <span class="owner-dot" class:own={isOwn}></span> | |
| <span class="unit-name">{building.building_type.replace(/_/g, ' ').toUpperCase()}</span> | |
| <span class="owner-label">{isOwn ? 'Ally' : 'Enemy'}</span> | |
| </div> | |
| <div class="stat-row"> | |
| <span class="stat-label">HP</span> | |
| <div class="hp-bar-wrap"> | |
| <div class="hp-bar" style="width:{hpPercent(building.hp, building.max_hp)}%; background:{hpColor(building.hp, building.max_hp)}"></div> | |
| </div> | |
| <span class="stat-val">{Math.ceil(building.hp)} / {building.max_hp}</span> | |
| </div> | |
| {#if building.status === 'constructing'} | |
| <div class="stat-row"> | |
| <span class="stat-label">Building</span> | |
| <span class="stat-val">{secFromTicks(building.construction_ticks_remaining)} remaining</span> | |
| </div> | |
| {#if isOwn} | |
| <button class="cancel-btn" on:click={cancelConstruction}> | |
| ✕ Annuler (remb. 75%) | |
| </button> | |
| {/if} | |
| {/if} | |
| {#if building.production_queue.length > 0} | |
| <div class="section-label">Production Queue</div> | |
| {#each building.production_queue as item, i} | |
| <div class="queue-item" class:active={i === 0}> | |
| <span class="queue-icon">{i === 0 ? '▶' : '·'}</span> | |
| <span class="queue-name">{item.unit_type.toUpperCase()}</span> | |
| {#if i === 0} | |
| <span class="queue-time">{secFromTicks(item.ticks_remaining)}</span> | |
| {/if} | |
| </div> | |
| {/each} | |
| {/if} | |
| {/if} | |
| </div> | |
| {/if} | |
| <style> | |
| .panel-backdrop { | |
| position: fixed; | |
| inset: 0; | |
| z-index: 9; | |
| /* transparent backdrop to catch taps outside panel */ | |
| } | |
| .panel { | |
| position: absolute; | |
| bottom: 110px; | |
| left: 12px; | |
| right: 12px; | |
| background: rgba(22, 27, 34, 0.97); | |
| border: 1px solid var(--border); | |
| border-radius: var(--radius-lg); | |
| padding: 16px; | |
| z-index: 10; | |
| backdrop-filter: blur(12px); | |
| box-shadow: 0 -4px 24px rgba(0,0,0,0.5); | |
| animation: slide-up 0.18s ease; | |
| } | |
| .panel.is-enemy { border-color: rgba(248, 81, 73, 0.4); } | |
| @keyframes slide-up { | |
| from { transform: translateY(20px); opacity: 0; } | |
| to { transform: translateY(0); opacity: 1; } | |
| } | |
| .close-btn { | |
| position: absolute; | |
| top: 10px; right: 12px; | |
| font-size: 0.9rem; | |
| color: var(--text-muted); | |
| padding: 4px 8px; | |
| } | |
| .panel-header { | |
| display: flex; | |
| align-items: center; | |
| gap: 8px; | |
| margin-bottom: 10px; | |
| } | |
| .owner-dot { | |
| width: 10px; height: 10px; | |
| border-radius: 50%; | |
| background: var(--enemy); | |
| flex-shrink: 0; | |
| } | |
| .owner-dot.own { background: var(--player); } | |
| .unit-name { | |
| font-weight: 800; | |
| font-size: 1rem; | |
| letter-spacing: 0.05em; | |
| flex: 1; | |
| } | |
| .owner-label { | |
| font-size: 0.72rem; | |
| color: var(--text-muted); | |
| font-weight: 600; | |
| } | |
| .description { | |
| font-size: 0.8rem; | |
| color: var(--text-muted); | |
| margin-bottom: 12px; | |
| line-height: 1.4; | |
| } | |
| .stat-row { | |
| display: flex; | |
| align-items: center; | |
| gap: 8px; | |
| margin-bottom: 8px; | |
| } | |
| .stat-label { | |
| font-size: 0.72rem; | |
| color: var(--text-muted); | |
| text-transform: uppercase; | |
| letter-spacing: 0.06em; | |
| width: 60px; | |
| flex-shrink: 0; | |
| } | |
| .stat-val { | |
| font-size: 0.82rem; | |
| font-weight: 600; | |
| font-family: var(--font-mono); | |
| } | |
| .stat-val.status { font-family: inherit; color: var(--accent); } | |
| .hp-bar-wrap { | |
| flex: 1; | |
| height: 8px; | |
| background: rgba(255,255,255,0.08); | |
| border-radius: 99px; | |
| overflow: hidden; | |
| } | |
| .hp-bar { | |
| height: 100%; | |
| border-radius: 99px; | |
| transition: width 0.3s; | |
| } | |
| .badge-row { margin-top: 6px; } | |
| .badge { | |
| display: inline-flex; | |
| align-items: center; | |
| gap: 4px; | |
| padding: 3px 8px; | |
| border-radius: 99px; | |
| font-size: 0.72rem; | |
| font-weight: 600; | |
| } | |
| .orange { background: rgba(255, 167, 38, 0.15); color: var(--supply); } | |
| .purple { background: rgba(179, 157, 219, 0.15); color: #b39ddb; } | |
| .section-label { | |
| font-size: 0.7rem; | |
| color: var(--text-muted); | |
| text-transform: uppercase; | |
| letter-spacing: 0.08em; | |
| margin: 12px 0 6px; | |
| } | |
| .queue-item { | |
| display: flex; | |
| align-items: center; | |
| gap: 6px; | |
| padding: 5px 8px; | |
| border-radius: var(--radius); | |
| font-size: 0.8rem; | |
| color: var(--text-muted); | |
| } | |
| .queue-item.active { | |
| background: rgba(88, 166, 255, 0.08); | |
| color: var(--text); | |
| } | |
| .queue-icon { font-size: 0.65rem; color: var(--player); } | |
| .queue-name { flex: 1; font-weight: 600; font-family: var(--font-mono); } | |
| .queue-time { font-family: var(--font-mono); font-size: 0.75rem; color: var(--supply); } | |
| .cancel-btn { | |
| margin-top: 10px; | |
| width: 100%; | |
| padding: 8px 12px; | |
| background: rgba(248, 81, 73, 0.12); | |
| border: 1px solid rgba(248, 81, 73, 0.35); | |
| border-radius: var(--radius); | |
| color: var(--danger); | |
| font-size: 0.8rem; | |
| font-weight: 700; | |
| cursor: pointer; | |
| transition: background 0.15s; | |
| } | |
| .cancel-btn:hover { | |
| background: rgba(248, 81, 73, 0.22); | |
| } | |
| </style> | |