feat: add ore and blacksmith
This commit is contained in:
+167
-14
@@ -1,21 +1,21 @@
|
||||
/**
|
||||
* Контент M1: мир, деревья, экономика рубки.
|
||||
* Баланс правится данными здесь, без кода (принцип «контент как данные»).
|
||||
* Контент M2: мир-полоса зон (лес | рудник | кузница), ноды, навыки,
|
||||
* предметы, инструменты тирами, рецепты. Баланс правится данными здесь.
|
||||
*/
|
||||
|
||||
import type { ZoneDef } from './protocol';
|
||||
|
||||
export const WORLD = { w: 1280, h: 720, spawn: { x: 180, y: 520 } } as const;
|
||||
export const WORLD = { w: 2100, 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 const ZONES: ZoneDef[] = [
|
||||
{ id: 'forest', name: 'Лес', x: 0, y: 0, w: 1000, h: 720 },
|
||||
{ id: 'mine', name: 'Рудник', x: 1000, y: 0, w: 680, h: 720 },
|
||||
{ id: 'forge', name: 'Кузница', x: 1680, y: 0, w: 420, h: 720 },
|
||||
];
|
||||
|
||||
export interface TreeSpot {
|
||||
x: number;
|
||||
y: number;
|
||||
}
|
||||
// ---- лес ----
|
||||
|
||||
export const TREE_SPOTS: TreeSpot[] = [
|
||||
export const TREE_SPOTS: { x: number; y: number }[] = [
|
||||
{ x: 520, y: 320 },
|
||||
{ x: 680, y: 430 },
|
||||
{ x: 860, y: 250 },
|
||||
@@ -25,16 +25,169 @@ export const TREE_SPOTS: TreeSpot[] = [
|
||||
{ x: 780, y: 560 },
|
||||
];
|
||||
|
||||
export const TREE = { maxHp: 5, respawnMs: 60_000, chopDurMs: 15_000 } as const;
|
||||
export const TREE = { maxHp: 5, respawnMs: 60_000, chopDurMs: 15_000, xp: 10 } as const;
|
||||
|
||||
// ---- рудник ----
|
||||
|
||||
export type OreKind = 'copper' | 'iron';
|
||||
|
||||
export interface RockSpot {
|
||||
x: number;
|
||||
y: number;
|
||||
ore: OreKind;
|
||||
}
|
||||
|
||||
export const ROCK_SPOTS: RockSpot[] = [
|
||||
{ x: 1150, y: 320, ore: 'copper' },
|
||||
{ x: 1310, y: 440, ore: 'copper' },
|
||||
{ x: 1470, y: 260, ore: 'copper' },
|
||||
{ x: 1230, y: 540, ore: 'copper' },
|
||||
{ x: 1580, y: 420, ore: 'iron' },
|
||||
{ x: 1390, y: 150, ore: 'iron' },
|
||||
];
|
||||
|
||||
export const ROCK = { maxHp: 4 } as const;
|
||||
|
||||
export const ORES: Record<
|
||||
OreKind,
|
||||
{ item: string; level: number; xp: number; cycleMs: number; respawnMs: number }
|
||||
> = {
|
||||
copper: { item: 'copper_ore', level: 1, xp: 15, cycleMs: 12_000, respawnMs: 45_000 },
|
||||
iron: { item: 'iron_ore', level: 3, xp: 30, cycleMs: 16_000, respawnMs: 90_000 },
|
||||
};
|
||||
|
||||
// ---- кузница ----
|
||||
|
||||
/** Точка, куда встаёт аватар для ковки. */
|
||||
export const FORGE_SPOT = { x: 1830, y: 415 } as const;
|
||||
|
||||
export const PROPS = [{ kind: 'anvil', x: 1830, y: 370 }] as const;
|
||||
|
||||
// ---- навыки ----
|
||||
|
||||
export const SKILLS = [
|
||||
{ id: 'woodcutting', title: 'Рубка леса' },
|
||||
{ id: 'mining', title: 'Добыча руды' },
|
||||
{ id: 'smithing', title: 'Кузнечное дело' },
|
||||
] as const;
|
||||
|
||||
export type SkillId = (typeof SKILLS)[number]['id'];
|
||||
|
||||
export const SKILL_TITLES: Record<string, string> = Object.fromEntries(
|
||||
SKILLS.map((s) => [s.id, s.title]),
|
||||
);
|
||||
|
||||
// ---- предметы и инструменты ----
|
||||
|
||||
export const XP_PER_CHOP = 10;
|
||||
export const ITEM_LOG = 'log';
|
||||
export const ITEM_TITLES: Record<string, string> = { [ITEM_LOG]: 'брёвна' };
|
||||
|
||||
export const ITEMS: Record<string, { title: string }> = {
|
||||
log: { title: 'брёвна' },
|
||||
copper_ore: { title: 'медная руда' },
|
||||
iron_ore: { title: 'железная руда' },
|
||||
copper_bar: { title: 'медный слиток' },
|
||||
iron_bar: { title: 'железный слиток' },
|
||||
axe_rusty: { title: 'ржавый топор' },
|
||||
axe_iron: { title: 'железный топор' },
|
||||
pick_rusty: { title: 'ржавая кирка' },
|
||||
pick_iron: { title: 'железная кирка' },
|
||||
};
|
||||
|
||||
export function itemTitle(id: string): string {
|
||||
return ITEMS[id]?.title ?? id;
|
||||
}
|
||||
|
||||
/** Выдаются при первом спавне. */
|
||||
export const START_ITEMS = ['axe_rusty', 'pick_rusty'] as const;
|
||||
|
||||
export interface ToolDef {
|
||||
item: string;
|
||||
kind: 'axe' | 'pick';
|
||||
/** Во сколько раз быстрее базовый цикл действия. */
|
||||
multiplier: number;
|
||||
}
|
||||
|
||||
export const TOOLS: ToolDef[] = [
|
||||
{ item: 'axe_rusty', kind: 'axe', multiplier: 1 },
|
||||
{ item: 'axe_iron', kind: 'axe', multiplier: 2 },
|
||||
{ item: 'pick_rusty', kind: 'pick', multiplier: 1 },
|
||||
{ item: 'pick_iron', kind: 'pick', multiplier: 2 },
|
||||
];
|
||||
|
||||
// ---- рецепты (ковка) ----
|
||||
|
||||
export interface RecipeDef {
|
||||
id: string;
|
||||
title: string;
|
||||
output: { item: string; qty: number };
|
||||
inputs: { item: string; qty: number }[];
|
||||
cycleMs: number;
|
||||
/** Требование к навыку кузнечного дела. */
|
||||
level: number;
|
||||
xp: number;
|
||||
aliases: string[];
|
||||
}
|
||||
|
||||
export const RECIPES: RecipeDef[] = [
|
||||
{
|
||||
id: 'copper_bar',
|
||||
title: 'медный слиток',
|
||||
output: { item: 'copper_bar', qty: 1 },
|
||||
inputs: [{ item: 'copper_ore', qty: 2 }],
|
||||
cycleMs: 8_000,
|
||||
level: 1,
|
||||
xp: 15,
|
||||
aliases: ['медный слиток', 'медь'],
|
||||
},
|
||||
{
|
||||
id: 'iron_bar',
|
||||
title: 'железный слиток',
|
||||
output: { item: 'iron_bar', qty: 1 },
|
||||
inputs: [{ item: 'iron_ore', qty: 2 }],
|
||||
cycleMs: 10_000,
|
||||
level: 2,
|
||||
xp: 25,
|
||||
aliases: ['железный слиток', 'железо'],
|
||||
},
|
||||
{
|
||||
id: 'axe_iron',
|
||||
title: 'железный топор',
|
||||
output: { item: 'axe_iron', qty: 1 },
|
||||
inputs: [
|
||||
{ item: 'copper_bar', qty: 1 },
|
||||
{ item: 'iron_bar', qty: 2 },
|
||||
],
|
||||
cycleMs: 15_000,
|
||||
level: 3,
|
||||
xp: 50,
|
||||
aliases: ['железный топор', 'топор'],
|
||||
},
|
||||
{
|
||||
id: 'pick_iron',
|
||||
title: 'железная кирка',
|
||||
output: { item: 'pick_iron', qty: 1 },
|
||||
inputs: [
|
||||
{ item: 'copper_bar', qty: 1 },
|
||||
{ item: 'iron_bar', qty: 2 },
|
||||
],
|
||||
cycleMs: 15_000,
|
||||
level: 3,
|
||||
xp: 50,
|
||||
aliases: ['железная кирка', 'кирка'],
|
||||
},
|
||||
];
|
||||
|
||||
export function findRecipe(text: string): RecipeDef | undefined {
|
||||
const q = text.trim().toLowerCase().replace(/\s+/g, ' ');
|
||||
return RECIPES.find((r) => r.id === q || r.title === q || r.aliases.includes(q));
|
||||
}
|
||||
|
||||
// ---- прочее ----
|
||||
|
||||
export const WALK_SPEED = 110; // px/сек
|
||||
export const COMMAND_COOLDOWN_MS = 1200; // антиспам на смену действия
|
||||
|
||||
/** OSRS/Melvor-подобная кривая: суммарный XP, нужный для уровня. */
|
||||
/** OSRS/Melvor-подобная кривая: суммарный XP, нужный для уровня (у каждого навыка своя). */
|
||||
export function totalXpForLevel(level: number): number {
|
||||
return Math.floor(((level - 1) + 300 * 2 ** ((level - 1) / 7)) / 4);
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
* дельты вместо полных снапшотов, игровые события, серверная камера.
|
||||
*/
|
||||
|
||||
export const PROTOCOL_VERSION = 2;
|
||||
export const PROTOCOL_VERSION = 3;
|
||||
|
||||
export interface ZoneDef {
|
||||
id: string;
|
||||
@@ -19,14 +19,26 @@ export interface ZoneDef {
|
||||
h: number;
|
||||
}
|
||||
|
||||
export interface PropDef {
|
||||
kind: 'anvil';
|
||||
x: number;
|
||||
y: number;
|
||||
}
|
||||
|
||||
export interface WorldDef {
|
||||
w: number;
|
||||
h: number;
|
||||
spawn: { x: number; y: number };
|
||||
zones: ZoneDef[];
|
||||
props: PropDef[];
|
||||
}
|
||||
|
||||
export type AvatarAction = 'idle' | 'chop';
|
||||
export type AvatarAction = 'idle' | 'chop' | 'mine' | 'smith';
|
||||
|
||||
export interface SkillState {
|
||||
xp: number;
|
||||
level: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Аватар — декларативное состояние: позиция анимируется клиентом
|
||||
@@ -46,16 +58,19 @@ export interface AvatarState {
|
||||
moveStart: number;
|
||||
moving: boolean;
|
||||
action: AvatarAction;
|
||||
/** Чем работает в данный момент: item id инструмента, 'hammer' или 'none'. */
|
||||
tool: string;
|
||||
nodeId: string | null;
|
||||
actionStart: number | null;
|
||||
actionDur: number | null;
|
||||
level: number;
|
||||
xp: number;
|
||||
skills: Record<string, SkillState>;
|
||||
}
|
||||
|
||||
export interface NodeState {
|
||||
id: string;
|
||||
kind: 'tree';
|
||||
kind: 'tree' | 'rock';
|
||||
/** Для руды — какая жила; для дерева отсутствует. */
|
||||
variant?: string;
|
||||
x: number;
|
||||
y: number;
|
||||
hp: number;
|
||||
@@ -68,9 +83,7 @@ export interface ViewerStat {
|
||||
id: string;
|
||||
name: string;
|
||||
color: string;
|
||||
level: number;
|
||||
xp: number;
|
||||
logs: number;
|
||||
skills: Record<string, SkillState>;
|
||||
action: AvatarAction;
|
||||
}
|
||||
|
||||
@@ -85,9 +98,11 @@ export interface CameraState {
|
||||
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 };
|
||||
| { k: 'xp'; userId: string; name: string; color: string; skill: string; amount: number; total: number }
|
||||
| { k: 'levelup'; userId: string; name: string; color: string; skill: string; level: number }
|
||||
| { k: 'fell'; userId: string; name: string; color: string; nodeId: string }
|
||||
| { k: 'depleted'; userId: string; name: string; color: string; nodeId: string }
|
||||
| { k: 'blocked'; userId: string; name: string; color: string; reason: string };
|
||||
|
||||
export interface WelcomeMsg {
|
||||
t: 'welcome';
|
||||
|
||||
Reference in New Issue
Block a user