feat: add server and frontend initial - wood chopping

This commit is contained in:
2026-09-05 16:37:45 +05:00
parent c79681dc59
commit f6c338e1b0
28 changed files with 1857 additions and 195 deletions
+96 -20
View File
@@ -1,7 +1,8 @@
<script lang="ts">
import { onMount } from 'svelte';
import { ITEM_TITLES, levelForTotalXp, type GameEvent, type ViewerStat } from '@idle/shared';
import { mountWorld, type WorldHandle } from '@idle/render';
import { connectWorld, type ConnectHandlers, type NetStatus } from './net';
import { connectWorld, type NetStatus } from './net';
const STATUS_TEXT: Record<NetStatus, string> = {
connecting: 'подключение…',
@@ -9,8 +10,32 @@
reconnecting: 'переподключение…',
};
interface FeedItem {
id: number;
color: string;
text: string;
}
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}`;
case 'xp':
return `${e.name}: +${e.amount} XP · ур.${levelForTotalXp(e.total)}`;
case 'levelup':
return `⭐ ${e.name}: уровень ${e.level}!`;
case 'fell':
return `${e.name} свалил(а) дерево!`;
}
}
let host = $state<HTMLDivElement | undefined>(undefined);
let status = $state<NetStatus>('connecting');
let stats = $state<ViewerStat[]>([]);
let feed = $state<FeedItem[]>([]);
let feedId = 0;
onMount(() => {
let world: WorldHandle | undefined;
@@ -19,13 +44,19 @@
if (host) {
void mountWorld(host, { background: 0x1d232d }).then((w) => {
world = w;
const handlers: ConnectHandlers = {
disposeNet = connectWorld({
onStatus: (s) => {
status = s;
},
onSnapshot: (snap) => world?.applySnapshot(snap.avatars),
};
disposeNet = connectWorld(handlers);
onWelcome: (msg) => world?.setWelcome(msg),
onSnapshot: (msg) => world?.applySnapshot(msg),
onDelta: (msg) => {
world?.applyDelta(msg);
for (const e of msg.events ?? []) {
feed = [{ id: ++feedId, color: e.color, text: eventText(e) }, ...feed].slice(0, 30);
}
},
});
});
}
@@ -47,20 +78,33 @@
<aside>
<section>
<h2>Инвентарь</h2>
<p class="muted">свой инвентарь по нику — с M2</p>
<h2>Топ зрителей</h2>
{#if stats.length === 0}
<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>
</li>
{/each}
</ul>
{/if}
</section>
<section>
<h2>Рецепты</h2>
<p class="muted">дерево рецептов — с M2</p>
</section>
<section>
<h2>Лидерборды</h2>
<p class="muted">топ зрителей — с M5</p>
</section>
<section>
<h2>Костёр</h2>
<p class="muted">общий прогресс — с M4</p>
<h2>События</h2>
{#if feed.length === 0}
<p class="muted">тишина в лесу…</p>
{:else}
<ul class="feed">
{#each feed as f (f.id)}
<li style:color={f.color}>{f.text}</li>
{/each}
</ul>
{/if}
</section>
</aside>
</div>
@@ -104,19 +148,51 @@
min-width: 0;
}
aside {
width: 300px;
width: 320px;
border-left: 1px solid #2a3242;
padding: 12px 16px;
overflow: auto;
}
section {
margin-bottom: 18px;
margin-bottom: 20px;
}
h2 {
font-size: 14px;
margin: 0 0 4px;
margin: 0 0 8px;
color: #b7c4d4;
}
ul {
list-style: none;
margin: 0;
padding: 0;
font-size: 13px;
}
li {
display: flex;
align-items: baseline;
gap: 6px;
padding: 3px 0;
}
.dot {
width: 8px;
height: 8px;
border-radius: 50%;
flex: none;
}
.name {
font-weight: 600;
}
.meta {
color: #8a99ab;
font-size: 12px;
margin-left: auto;
}
.feed li {
display: block;
font-size: 12.5px;
padding: 2px 0;
color: #c7d3e0;
}
.muted {
font-size: 12px;
color: #6d7d90;
+7 -3
View File
@@ -1,6 +1,5 @@
import { PROTOCOL_VERSION, type ClientMessage, type ServerMessage } from '@idle/shared';
import { PROTOCOL_VERSION, type ClientMessage, type DeltaMsg, type ServerMessage, type SnapshotMsg, type WelcomeMsg } from '@idle/shared';
export type Snapshot = Extract<ServerMessage, { t: 'snapshot' }>;
export type NetStatus = 'connecting' | 'online' | 'reconnecting';
export function wsUrl(): string {
@@ -13,7 +12,9 @@ export function wsUrl(): string {
export interface ConnectHandlers {
onStatus: (status: NetStatus) => void;
onSnapshot: (snap: Snapshot) => void;
onWelcome: (msg: WelcomeMsg) => void;
onSnapshot: (msg: SnapshotMsg) => void;
onDelta: (msg: DeltaMsg) => void;
}
/** Подключение к миру с автопереподключением; возвращает функцию закрытия. */
@@ -41,8 +42,11 @@ export function connectWorld(handlers: ConnectHandlers): () => void {
}
if (msg.t === 'welcome') {
handlers.onStatus('online');
handlers.onWelcome(msg);
} else if (msg.t === 'snapshot') {
handlers.onSnapshot(msg);
} else if (msg.t === 'delta') {
handlers.onDelta(msg);
}
};
ws.onclose = () => {