gabraken's picture
feat: add game engine, voice commands, leaderboard, tutorial overlay, and stats tracking
29a88f8
import { writable, derived } from 'svelte/store';
import type { GameState, PlayerState, Room, Unit, Building } from '$lib/types';
import { UNIT_SUPPLY_COST, BUILDING_SUPPLY_PROVIDED } from '$lib/types';
// โ”€โ”€ Session โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
export const myPlayerId = writable<string | null>(null);
export const playerName = writable<string>('');
export const roomState = writable<Room | null>(null);
export const roomId = writable<string | null>(null);
// โ”€โ”€ Game โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
export const gameState = writable<GameState | null>(null);
export const winnerId = writable<string | null>(null);
export const isTutorial = writable<boolean>(false);
// โ”€โ”€ UI โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
export const selectedUnit = writable<Unit | null>(null);
export const selectedBuilding = writable<Building | null>(null);
export const mapViewport = writable<{ vx: number; vy: number; vw: number; vh: number }>({ vx: 0, vy: 0, vw: 40, vh: 40 });
export const mapVisibleCells = writable<Set<string>>(new Set());
export const mapExploredCells = writable<Set<string>>(new Set());
/** When set, main map recenters on (cx, cy); consumed by Map.svelte */
export const mapCenterRequest = writable<{ cx: number; cy: number } | null>(null);
export const voiceStatus = writable<'idle' | 'recording' | 'processing'>('idle');
export const lastTranscription = writable<string>('');
export const lastFeedback = writable<string>('');
export const feedbackLevel = writable<'ok' | 'warning' | 'error'>('ok');
/**
* Progressive map texture URL.
* Starts null โ†’ set to MAP_quarter blob โ†’ MAP_half blob
* by the landing-page preloader. Map.svelte falls back to the direct URL
* if this is still null (e.g. user navigated directly to /game).
*/
export const mapTextureUrl = writable<string | null>(null);
// โ”€โ”€ Derived โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
export const myPlayer = derived(
[gameState, myPlayerId],
([$gs, $id]): PlayerState | null => {
if (!$gs || !$id) return null;
return $gs.players[$id] ?? null;
}
);
export const enemyPlayer = derived(
[gameState, myPlayerId],
([$gs, $id]): PlayerState | null => {
if (!$gs || !$id) return null;
return Object.values($gs.players).find((p) => p.player_id !== $id) ?? null;
}
);
/** Supply computed client-side from actual units + active buildings. */
export const mySupply = derived(
[gameState, myPlayerId],
([$gs, $id]): { used: number; max: number } => {
if (!$gs || !$id) return { used: 0, max: 0 };
const player = $gs.players[$id];
if (!player) return { used: 0, max: 0 };
const used = Object.values(player.units).reduce(
(sum, u) => sum + (UNIT_SUPPLY_COST[u.unit_type] ?? 0),
0
);
const max = Object.values(player.buildings).reduce(
(sum, b) =>
b.status === 'constructing' || b.status === 'destroyed'
? sum
: sum + (BUILDING_SUPPLY_PROVIDED[b.building_type] ?? 0),
0
);
return { used, max };
}
);
export const allUnitsFlat = derived(gameState, ($gs) => {
if (!$gs) return [];
return Object.values($gs.players).flatMap((p) =>
Object.values(p.units).map((u) => ({ unit: u, isOwn: false }))
);
});