gabraken's picture
Cancel Culture
d530c6f
<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>