feat: fishing and cooking

This commit is contained in:
2026-09-05 17:46:15 +05:00
parent 6273f2849c
commit 32527ec2ed
10 changed files with 518 additions and 113 deletions
+98 -7
View File
@@ -1,18 +1,24 @@
/**
* Контент M2: мир-полоса зон (лес | рудник | кузница), ноды, навыки,
* предметы, инструменты тирами, рецепты. Баланс правится данными здесь.
* Контент M3: полная полоса зон (лес | рудник | кузница | река | кухня),
* ноды (деревья, жилы, рыбные места), навыки, предметы, инструменты,
* рецепты и отдых у костра. Баланс правится данными здесь.
*/
import type { ZoneDef } from './protocol';
export const WORLD = { w: 2100, h: 720, spawn: { x: 180, y: 520 } } as const;
export const WORLD = { w: 3200, h: 720, spawn: { x: 180, y: 520 } } as const;
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 },
{ id: 'river', name: 'Река', x: 2100, y: 0, w: 680, h: 720 },
{ id: 'kitchen', name: 'Кухня', x: 2780, y: 0, w: 420, h: 720 },
];
/** Уровень воды в зоне реки: выше — вода, ниже — берег. */
export const RIVER_WATER_Y = 360;
// ---- лес ----
export const TREE_SPOTS: { x: number; y: number }[] = [
@@ -61,7 +67,50 @@ export const ORES: Record<
/** Точка, куда встаёт аватар для ковки. */
export const FORGE_SPOT = { x: 1830, y: 415 } as const;
export const PROPS = [{ kind: 'anvil', x: 1830, y: 370 }] as const;
/** Точка отдыха у костра. */
export const REST_SPOT = { x: 1960, y: 515 } as const;
/** Точка ковки на кухне (у печи). */
export const KITCHEN_SPOT = { x: 2990, y: 425 } as const;
export const PROPS = [
{ kind: 'anvil', x: 1830, y: 370 },
{ kind: 'campfire', x: 1960, y: 480 },
{ kind: 'stove', x: 2990, y: 380 },
] as const;
// ---- река ----
export interface FishSpot {
x: number;
y: number;
}
export const FISH_SPOTS: FishSpot[] = [
{ x: 2250, y: 330 },
{ x: 2450, y: 240 },
{ x: 2650, y: 330 },
];
export const FISH = {
item: 'raw_fish',
level: 1,
xp: 15,
cycleMs: 14_000,
respawnMs: 60_000,
maxHp: 4,
} as const;
// ---- отдых у костра ----
export const REST = {
/** Длительность одного отдыха (план: «+20% добычи за 1 минуту отдыха»). */
cycleMs: 60_000,
buffMs: 300_000,
maxStacks: 2,
/** Прибавка к добыче за стак (дробные циклы копятся аккумулятором). */
bonusPerStack: 0.2,
} as const;
// ---- навыки ----
@@ -69,6 +118,8 @@ export const SKILLS = [
{ id: 'woodcutting', title: 'Рубка леса' },
{ id: 'mining', title: 'Добыча руды' },
{ id: 'smithing', title: 'Кузнечное дело' },
{ id: 'fishing', title: 'Рыбалка' },
{ id: 'cooking', title: 'Кулинария' },
] as const;
export type SkillId = (typeof SKILLS)[number]['id'];
@@ -87,6 +138,8 @@ export const ITEMS: Record<string, { title: string }> = {
iron_ore: { title: 'железная руда' },
copper_bar: { title: 'медный слиток' },
iron_bar: { title: 'железный слиток' },
raw_fish: { title: 'сырая рыба' },
cooked_fish: { title: 'жареная рыба' },
axe_rusty: { title: 'ржавый топор' },
axe_iron: { title: 'железный топор' },
pick_rusty: { title: 'ржавая кирка' },
@@ -114,7 +167,7 @@ export const TOOLS: ToolDef[] = [
{ item: 'pick_iron', kind: 'pick', multiplier: 2 },
];
// ---- рецепты (ковка) ----
// ---- рецепты ----
export interface RecipeDef {
id: string;
@@ -122,9 +175,12 @@ export interface RecipeDef {
output: { item: string; qty: number };
inputs: { item: string; qty: number }[];
cycleMs: number;
/** Требование к навыку кузнечного дела. */
/** Требование к навыку recipe.skill. */
level: number;
xp: number;
/** Какой навык качает рецепт и где готовится. */
skill: 'smithing' | 'cooking';
place: 'forge' | 'kitchen';
aliases: string[];
}
@@ -137,6 +193,8 @@ export const RECIPES: RecipeDef[] = [
cycleMs: 8_000,
level: 1,
xp: 15,
skill: 'smithing',
place: 'forge',
aliases: ['медный слиток', 'медь'],
},
{
@@ -147,6 +205,8 @@ export const RECIPES: RecipeDef[] = [
cycleMs: 10_000,
level: 2,
xp: 25,
skill: 'smithing',
place: 'forge',
aliases: ['железный слиток', 'железо'],
},
{
@@ -160,6 +220,8 @@ export const RECIPES: RecipeDef[] = [
cycleMs: 15_000,
level: 3,
xp: 50,
skill: 'smithing',
place: 'forge',
aliases: ['железный топор', 'топор'],
},
{
@@ -173,13 +235,42 @@ export const RECIPES: RecipeDef[] = [
cycleMs: 15_000,
level: 3,
xp: 50,
skill: 'smithing',
place: 'forge',
aliases: ['железная кирка', 'кирка'],
},
{
id: 'cooked_fish',
title: 'жареная рыба',
output: { item: 'cooked_fish', qty: 1 },
inputs: [{ item: 'raw_fish', qty: 2 }],
cycleMs: 10_000,
level: 1,
xp: 20,
skill: 'cooking',
place: 'kitchen',
aliases: ['жареная рыба', 'рыба'],
},
];
/** Грубый стем для склонений: «жареную рыбу» → «жар», «рыб». */
function stem(word: string): string {
return word.slice(0, 3);
}
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));
const exact = RECIPES.find((r) => r.id === q || r.title === q || r.aliases.includes(q));
if (exact) return exact;
// фолбэк для склонений: каждое слово запроса должно попасть в слова рецепта
const qStems = q.split(' ').map(stem);
if (qStems.length === 0) return undefined;
return RECIPES.find((r) => {
const stems = new Set(
[...r.title.split(' '), ...r.aliases.flatMap((a) => a.split(' '))].map(stem),
);
return qStems.every((s) => stems.has(s));
});
}
// ---- прочее ----
+8 -5
View File
@@ -8,7 +8,7 @@
* дельты вместо полных снапшотов, игровые события, серверная камера.
*/
export const PROTOCOL_VERSION = 3;
export const PROTOCOL_VERSION = 4;
export interface ZoneDef {
id: string;
@@ -20,7 +20,7 @@ export interface ZoneDef {
}
export interface PropDef {
kind: 'anvil';
kind: 'anvil' | 'campfire' | 'stove';
x: number;
y: number;
}
@@ -33,7 +33,7 @@ export interface WorldDef {
props: PropDef[];
}
export type AvatarAction = 'idle' | 'chop' | 'mine' | 'smith';
export type AvatarAction = 'idle' | 'chop' | 'mine' | 'smith' | 'cook' | 'fish' | 'rest';
export interface SkillState {
xp: number;
@@ -64,12 +64,14 @@ export interface AvatarState {
actionStart: number | null;
actionDur: number | null;
skills: Record<string, SkillState>;
/** Стаки баффа отдыха (каждый +20% к добыче). */
restStacks: number;
}
export interface NodeState {
id: string;
kind: 'tree' | 'rock';
/** Для руды — какая жила; для дерева отсутствует. */
kind: 'tree' | 'rock' | 'fish';
/** Для руды — какая жила, для рыбного места — 'river'. */
variant?: string;
x: number;
y: number;
@@ -102,6 +104,7 @@ export type GameEvent =
| { 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: 'rest'; userId: string; name: string; color: string; stacks: number }
| { k: 'blocked'; userId: string; name: string; color: string; reason: string };
export interface WelcomeMsg {