feat: add ore and blacksmith
This commit is contained in:
@@ -1 +1,2 @@
|
||||
VITE_WS_URL=ws://localhost:3000/ws
|
||||
VITE_API_URL=http://localhost:3000
|
||||
|
||||
+108
-9
@@ -1,8 +1,14 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import { ITEM_TITLES, levelForTotalXp, type GameEvent, type ViewerStat } from '@idle/shared';
|
||||
import {
|
||||
SKILL_TITLES,
|
||||
itemTitle,
|
||||
type GameEvent,
|
||||
type SkillState,
|
||||
type ViewerStat,
|
||||
} from '@idle/shared';
|
||||
import { mountWorld, type WorldHandle } from '@idle/render';
|
||||
import { connectWorld, type NetStatus } from './net';
|
||||
import { apiUrl, connectWorld, type NetStatus } from './net';
|
||||
|
||||
const STATUS_TEXT: Record<NetStatus, string> = {
|
||||
connecting: 'подключение…',
|
||||
@@ -10,33 +16,77 @@
|
||||
reconnecting: 'переподключение…',
|
||||
};
|
||||
|
||||
const ACTION_EMOJI: Record<string, string> = {
|
||||
chop: '🌲',
|
||||
mine: '⛏️',
|
||||
smith: '🔨',
|
||||
idle: '💤',
|
||||
};
|
||||
|
||||
interface FeedItem {
|
||||
id: number;
|
||||
color: string;
|
||||
text: string;
|
||||
}
|
||||
|
||||
interface InvData {
|
||||
found: boolean;
|
||||
name?: string;
|
||||
color?: string;
|
||||
skills?: Record<string, SkillState>;
|
||||
items?: Record<string, number>;
|
||||
}
|
||||
|
||||
function eventText(e: GameEvent): string {
|
||||
switch (e.k) {
|
||||
case 'spawn':
|
||||
return `${e.name} пришёл(ла) в мир`;
|
||||
case 'item':
|
||||
return `${e.name}: +${e.qty} ${ITEM_TITLES[e.item] ?? e.item}`;
|
||||
return `${e.name}: +${e.qty} ${itemTitle(e.item)}`;
|
||||
case 'xp':
|
||||
return `${e.name}: +${e.amount} XP · ур.${levelForTotalXp(e.total)}`;
|
||||
return `${e.name}: +${e.amount} XP ${SKILL_TITLES[e.skill] ?? e.skill}`;
|
||||
case 'levelup':
|
||||
return `⭐ ${e.name}: уровень ${e.level}!`;
|
||||
return `⭐ ${e.name}: ${SKILL_TITLES[e.skill] ?? e.skill} — уровень ${e.level}!`;
|
||||
case 'fell':
|
||||
return `${e.name} свалил(а) дерево!`;
|
||||
return `${e.name} свалил(ла) дерево!`;
|
||||
case 'depleted':
|
||||
return `${e.name} выработал(ла) жилу`;
|
||||
case 'blocked':
|
||||
return `${e.name}: ${e.reason}`;
|
||||
}
|
||||
}
|
||||
|
||||
function totalXp(s: ViewerStat): number {
|
||||
return Object.values(s.skills).reduce((sum, v) => sum + v.xp, 0);
|
||||
}
|
||||
|
||||
function maxLevel(s: ViewerStat): number {
|
||||
return Math.max(1, ...Object.values(s.skills).map((v) => v.level));
|
||||
}
|
||||
|
||||
let host = $state<HTMLDivElement | undefined>(undefined);
|
||||
let status = $state<NetStatus>('connecting');
|
||||
let stats = $state<ViewerStat[]>([]);
|
||||
let feed = $state<FeedItem[]>([]);
|
||||
let feedId = 0;
|
||||
|
||||
let invName = $state('');
|
||||
let inv = $state<InvData | null>(null);
|
||||
let invSearched = $state(false);
|
||||
|
||||
async function lookupInventory(e: SubmitEvent): Promise<void> {
|
||||
e.preventDefault();
|
||||
const name = invName.trim();
|
||||
if (!name) return;
|
||||
invSearched = true;
|
||||
try {
|
||||
const res = await fetch(apiUrl(`/api/inventory?name=${encodeURIComponent(name)}`));
|
||||
inv = (await res.json()) as InvData;
|
||||
} catch {
|
||||
inv = { found: false };
|
||||
}
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
let world: WorldHandle | undefined;
|
||||
let disposeNet: (() => void) | undefined;
|
||||
@@ -80,20 +130,42 @@
|
||||
<section>
|
||||
<h2>Топ зрителей</h2>
|
||||
{#if stats.length === 0}
|
||||
<p class="muted">пока никто не написал !рубить</p>
|
||||
<p class="muted">пока никто не написал команду</p>
|
||||
{:else}
|
||||
<ul>
|
||||
{#each stats as s (s.id)}
|
||||
<li>
|
||||
<span class="dot" style:background={s.color}></span>
|
||||
<span class="name" style:color={s.color}>{s.name}</span>
|
||||
<span class="meta">ур.{s.level} · {s.xp} XP · {s.logs} 🪵 {s.action === 'chop' ? '🌲' : '💤'}</span>
|
||||
<span class="meta">ур.{maxLevel(s)} · {totalXp(s)} XP {ACTION_EMOJI[s.action] ?? ''}</span>
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
{/if}
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>Инвентарь</h2>
|
||||
<form onsubmit={lookupInventory}>
|
||||
<input bind:value={invName} placeholder="ник зрителя" aria-label="ник зрителя" />
|
||||
<button type="submit">показать</button>
|
||||
</form>
|
||||
{#if inv && inv.found}
|
||||
<ul class="inv">
|
||||
{#each Object.entries(inv.skills ?? {}) as [id, s] (id)}
|
||||
<li><span class="k">{SKILL_TITLES[id] ?? id}</span><span class="meta">ур.{s.level} · {s.xp} XP</span></li>
|
||||
{/each}
|
||||
{#each Object.entries(inv.items ?? {}) as [id, qty] (id)}
|
||||
{#if qty > 0}
|
||||
<li><span class="k">{itemTitle(id)}</span><span class="meta">×{qty}</span></li>
|
||||
{/if}
|
||||
{/each}
|
||||
</ul>
|
||||
{:else if invSearched && inv && !inv.found}
|
||||
<p class="muted">не найден — пусть напишет любую команду в чате</p>
|
||||
{/if}
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>События</h2>
|
||||
{#if feed.length === 0}
|
||||
@@ -148,7 +220,7 @@
|
||||
min-width: 0;
|
||||
}
|
||||
aside {
|
||||
width: 320px;
|
||||
width: 340px;
|
||||
border-left: 1px solid #2a3242;
|
||||
padding: 12px 16px;
|
||||
overflow: auto;
|
||||
@@ -186,6 +258,33 @@
|
||||
color: #8a99ab;
|
||||
font-size: 12px;
|
||||
margin-left: auto;
|
||||
white-space: nowrap;
|
||||
}
|
||||
form {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
input {
|
||||
flex: 1;
|
||||
background: #232b38;
|
||||
border: 1px solid #2a3242;
|
||||
border-radius: 6px;
|
||||
color: #dfe7f0;
|
||||
padding: 4px 8px;
|
||||
font-size: 13px;
|
||||
}
|
||||
button {
|
||||
background: #2f3a4c;
|
||||
color: #dfe7f0;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
padding: 4px 10px;
|
||||
font-size: 13px;
|
||||
cursor: pointer;
|
||||
}
|
||||
.inv .k {
|
||||
color: #c7d3e0;
|
||||
}
|
||||
.feed li {
|
||||
display: block;
|
||||
|
||||
@@ -10,6 +10,13 @@ export function wsUrl(): string {
|
||||
return `${scheme}://${location.host}/ws`;
|
||||
}
|
||||
|
||||
/** База HTTP API (в dev сервер на другом порту, в проде — тот же origin). */
|
||||
export function apiUrl(path: string): string {
|
||||
const base = import.meta.env.VITE_API_URL;
|
||||
if (typeof base === 'string' && base.length > 0) return base + path;
|
||||
return path;
|
||||
}
|
||||
|
||||
export interface ConnectHandlers {
|
||||
onStatus: (status: NetStatus) => void;
|
||||
onWelcome: (msg: WelcomeMsg) => void;
|
||||
|
||||
Reference in New Issue
Block a user