feat: add server and frontend initial - wood chopping
This commit is contained in:
@@ -0,0 +1,65 @@
|
||||
/**
|
||||
* Контент M1: мир, деревья, экономика рубки.
|
||||
* Баланс правится данными здесь, без кода (принцип «контент как данные»).
|
||||
*/
|
||||
|
||||
import type { ZoneDef } from './protocol';
|
||||
|
||||
export const WORLD = { w: 1280, h: 720, spawn: { x: 180, y: 520 } } as const;
|
||||
|
||||
/** M1 — одна зона; в M2 мир станет полосой зон (лес | рудник | река | костёр | кухня). */
|
||||
export const ZONES: ZoneDef[] = [{ id: 'forest', name: 'Лес', x: 0, y: 0, w: 1280, h: 720 }];
|
||||
|
||||
export interface TreeSpot {
|
||||
x: number;
|
||||
y: number;
|
||||
}
|
||||
|
||||
export const TREE_SPOTS: TreeSpot[] = [
|
||||
{ x: 520, y: 320 },
|
||||
{ x: 680, y: 430 },
|
||||
{ x: 860, y: 250 },
|
||||
{ x: 430, y: 470 },
|
||||
{ x: 640, y: 180 },
|
||||
{ x: 950, y: 470 },
|
||||
{ x: 780, y: 560 },
|
||||
];
|
||||
|
||||
export const TREE = { maxHp: 5, respawnMs: 60_000, chopDurMs: 15_000 } as const;
|
||||
|
||||
export const XP_PER_CHOP = 10;
|
||||
export const ITEM_LOG = 'log';
|
||||
export const ITEM_TITLES: Record<string, string> = { [ITEM_LOG]: 'брёвна' };
|
||||
|
||||
export const WALK_SPEED = 110; // px/сек
|
||||
export const COMMAND_COOLDOWN_MS = 1200; // антиспам на смену действия
|
||||
|
||||
/** OSRS/Melvor-подобная кривая: суммарный XP, нужный для уровня. */
|
||||
export function totalXpForLevel(level: number): number {
|
||||
return Math.floor(((level - 1) + 300 * 2 ** ((level - 1) / 7)) / 4);
|
||||
}
|
||||
|
||||
const LEVEL_XP: number[] = Array.from({ length: 99 }, (_, i) => totalXpForLevel(i + 1));
|
||||
|
||||
export function levelForTotalXp(xp: number): number {
|
||||
let level = 1;
|
||||
for (let l = 2; l <= 99; l++) {
|
||||
const need = LEVEL_XP[l - 1];
|
||||
if (need === undefined || xp < need) break;
|
||||
level = l;
|
||||
}
|
||||
return level;
|
||||
}
|
||||
|
||||
/** Палитра для ников без цвета в чате и для фейковых зрителей. */
|
||||
const PALETTE = [
|
||||
'#ff8c3b', '#5bd7ff', '#b58cff', '#7ee2a8', '#ffd166',
|
||||
'#ff6b9d', '#8ad4a1', '#f4a4c0', '#a3c8ff', '#d9c17e',
|
||||
];
|
||||
|
||||
export function colorForName(name: string): string {
|
||||
let h = 0;
|
||||
for (let i = 0; i < name.length; i++) h = (h * 31 + name.charCodeAt(i)) >>> 0;
|
||||
const c = PALETTE[h % PALETTE.length];
|
||||
return c ?? '#ff8c3b';
|
||||
}
|
||||
@@ -1 +1,2 @@
|
||||
export * from './protocol';
|
||||
export * from './content';
|
||||
|
||||
@@ -3,39 +3,121 @@
|
||||
* Клиенты зависят только от него; язык сервера можно сменить позже.
|
||||
* Версия проверяется в рукопожатии (hello/welcome), чтобы OBS со старым
|
||||
* бандлом не висел молча.
|
||||
*
|
||||
* v2: мир с нодами ресурсов, декларативное движение (x→tx со скоростью),
|
||||
* дельты вместо полных снапшотов, игровые события, серверная камера.
|
||||
*/
|
||||
|
||||
export const PROTOCOL_VERSION = 1;
|
||||
export const PROTOCOL_VERSION = 2;
|
||||
|
||||
export interface ZoneDef {
|
||||
id: string;
|
||||
name: string;
|
||||
x: number;
|
||||
y: number;
|
||||
w: number;
|
||||
h: number;
|
||||
}
|
||||
|
||||
export interface WorldDef {
|
||||
w: number;
|
||||
h: number;
|
||||
spawn: { x: number; y: number };
|
||||
zones: ZoneDef[];
|
||||
}
|
||||
|
||||
export type AvatarAction = 'idle' | 'chop';
|
||||
|
||||
/**
|
||||
* Аватар — декларативное состояние: позиция анимируется клиентом
|
||||
* (из x в tx со скоростью speed, начиная с moveStart), прогресс действия
|
||||
* считается от actionStart/actionDur. Сервер остаётся авторитетом:
|
||||
* каждое сообщение несёт serverNow для выравнивания часов.
|
||||
*/
|
||||
export interface AvatarState {
|
||||
id: string;
|
||||
name: string;
|
||||
/** Цвет ника из тегов чата Twitch. */
|
||||
color: string;
|
||||
/** Позиция в мировых координатах, px. */
|
||||
x: number;
|
||||
y: number;
|
||||
face: 'left' | 'right';
|
||||
tx: number;
|
||||
ty: number;
|
||||
speed: number;
|
||||
moveStart: number;
|
||||
moving: boolean;
|
||||
action: AvatarAction;
|
||||
nodeId: string | null;
|
||||
actionStart: number | null;
|
||||
actionDur: number | null;
|
||||
level: number;
|
||||
xp: number;
|
||||
}
|
||||
|
||||
export interface NodeState {
|
||||
id: string;
|
||||
kind: 'tree';
|
||||
x: number;
|
||||
y: number;
|
||||
hp: number;
|
||||
maxHp: number;
|
||||
/** server epoch ms, когда нода восстановится; null — нода стоит. */
|
||||
respawnAt: number | null;
|
||||
}
|
||||
|
||||
export interface ViewerStat {
|
||||
id: string;
|
||||
name: string;
|
||||
color: string;
|
||||
level: number;
|
||||
xp: number;
|
||||
logs: number;
|
||||
action: AvatarAction;
|
||||
}
|
||||
|
||||
export interface CameraState {
|
||||
/** Прямоугольник окна камеры в мировых координатах. */
|
||||
x: number;
|
||||
y: number;
|
||||
w: number;
|
||||
h: number;
|
||||
}
|
||||
|
||||
export type GameEvent =
|
||||
| { k: 'spawn'; userId: string; name: string; color: string }
|
||||
| { k: 'item'; userId: string; name: string; color: string; item: string; qty: number }
|
||||
| { k: 'xp'; userId: string; name: string; color: string; amount: number; total: number }
|
||||
| { k: 'levelup'; userId: string; name: string; color: string; level: number }
|
||||
| { k: 'fell'; userId: string; name: string; color: string; nodeId: string };
|
||||
|
||||
export interface WelcomeMsg {
|
||||
t: 'welcome';
|
||||
v: number;
|
||||
/** Период тика симуляции, мс — клиенту для отрисовки прогрессов. */
|
||||
tickMs: number;
|
||||
serverNow: number;
|
||||
world: WorldDef;
|
||||
}
|
||||
|
||||
/** M0: полный снапшот каждый тик; дельты появятся с M1. */
|
||||
export interface SnapshotMsg {
|
||||
t: 'snapshot';
|
||||
tick: number;
|
||||
serverNow: number;
|
||||
camera: CameraState;
|
||||
avatars: AvatarState[];
|
||||
nodes: NodeState[];
|
||||
stats: ViewerStat[];
|
||||
}
|
||||
|
||||
export type ServerMessage = WelcomeMsg | SnapshotMsg;
|
||||
export interface DeltaMsg {
|
||||
t: 'delta';
|
||||
tick: number;
|
||||
serverNow: number;
|
||||
camera?: CameraState;
|
||||
avatars?: AvatarState[];
|
||||
nodes?: NodeState[];
|
||||
stats?: ViewerStat[];
|
||||
events?: GameEvent[];
|
||||
}
|
||||
|
||||
export type ClientMessage = {
|
||||
t: 'hello';
|
||||
v: number;
|
||||
};
|
||||
export type ServerMessage = WelcomeMsg | SnapshotMsg | DeltaMsg;
|
||||
|
||||
export type ClientMessage = { t: 'hello'; v: number };
|
||||
|
||||
Reference in New Issue
Block a user