feat: add ore and blacksmith

This commit is contained in:
2026-09-05 17:24:44 +05:00
parent f6c338e1b0
commit 6273f2849c
15 changed files with 1333 additions and 334 deletions
+44 -21
View File
@@ -6,6 +6,9 @@ const LEG_COLOR = 0x3b4657;
const CHIP_COLOR = 0xc79b5b;
const BAR_BG = 0x10131a;
const BAR_FG = 0x9be870;
const HANDLE = 0x8a6238;
const STEEL = 0xb8c4d0;
const RUSTY = 0x9a6a3f;
interface Chip {
g: Graphics;
@@ -14,18 +17,32 @@ interface Chip {
born: number;
}
function makeTool(tool: string): Graphics {
const g = new Graphics().rect(-1, -12, 2, 12).fill(HANDLE);
if (tool === 'axe_rusty' || tool === 'axe_iron') {
g.poly([-1, -12, 7, -12, 7, -7, -1, -8]).fill(tool === 'axe_iron' ? STEEL : RUSTY);
} else if (tool === 'pick_rusty' || tool === 'pick_iron') {
const head = tool === 'pick_iron' ? STEEL : RUSTY;
g.poly([-1, -11, 8, -15, 8, -12, -1, -8]).fill(head);
g.poly([-1, -11, -8, -15, -8, -12, -1, -8]).fill(head);
} else if (tool === 'hammer') {
g.rect(-4, -15, 8, 4).fill(0x55606c);
}
return g;
}
/**
* Аватар M1: процедурный человечек (код вместо спрайтов — ноль лицензионных
* рисков). С M2 сюда придут текстуры из паков; состав (тело + ник + прогресс)
* сохранится. Прогресс рубки клиент считает сам из actionStart/actionDur
* серверу не нужно слать его каждый тик.
* Аватар M2: процедурный человечек, в руках — текущий инструмент
* (axe/pick тирами или молот у наковальни). Прогресс цикла клиент считает сам
* из actionStart/actionDur.
*/
export class AvatarView {
readonly container = new Container();
private readonly rig = new Container();
private readonly legL: Graphics;
private readonly legR: Graphics;
private readonly tool: Graphics;
private readonly toolHolder = new Container();
private readonly tools = new Map<string, Graphics>();
private readonly label: Text;
private readonly bar: Graphics;
private chips: Chip[] = [];
@@ -33,6 +50,7 @@ export class AvatarView {
private lastChopPhase = 0;
private prevNow = 0;
private lastLabelText = '';
private lastTool = '';
constructor(av: AvatarState) {
const shadow = new Graphics().ellipse(0, 1, 9, 3.5).fill({ color: 0x000000, alpha: 0.3 });
@@ -42,13 +60,13 @@ export class AvatarView {
this.legR.position.set(3, -6);
const body = new Graphics().roundRect(-7, -18, 14, 13, 4).fill(av.color);
const head = new Graphics().circle(0, -23, 6.5).fill(SKIN);
// топор: точка вращения — плечо; покачивается только при рубке
this.tool = new Graphics()
.rect(-1, -12, 2, 12)
.fill(0x8a6238)
.poly([-1, -12, 7, -12, 7, -7, -1, -8])
.fill(0xb8c4d0);
this.tool.position.set(6, -14);
this.toolHolder.position.set(6, -14);
for (const tool of ['axe_rusty', 'axe_iron', 'pick_rusty', 'pick_iron', 'hammer']) {
const g = makeTool(tool);
g.visible = false;
this.tools.set(tool, g);
this.toolHolder.addChild(g);
}
this.label = new Text({
text: '',
style: {
@@ -63,16 +81,16 @@ export class AvatarView {
this.label.y = -42;
this.bar = new Graphics();
this.rig.addChild(shadow, this.legL, this.legR, body, head, this.tool);
this.rig.addChild(shadow, this.legL, this.legR, body, head, this.toolHolder);
this.container.addChild(this.rig, this.bar, this.label);
this.container.position.set(av.x, av.y);
this.applyLabel(av);
this.tool.visible = false;
}
applyLabel(av: AvatarState): void {
const text = `${av.name} [${av.level}]`;
const maxLevel = Math.max(1, ...Object.values(av.skills).map((s) => s.level));
const text = `${av.name} [${maxLevel}]`;
if (text !== this.lastLabelText) {
this.lastLabelText = text;
this.label.text = text;
@@ -105,19 +123,24 @@ export class AvatarView {
this.container.position.set(x, y);
this.rig.scale.x = this.face;
if (av.tool !== this.lastTool) {
this.lastTool = av.tool;
for (const [id, g] of this.tools) g.visible = id === av.tool;
}
const working = av.action === 'chop' || av.action === 'mine' || av.action === 'smith';
this.toolHolder.visible = working && av.tool !== 'none';
const t = now / 1000;
if (av.moving) {
const swing = Math.sin(t * 12);
this.legL.rotation = swing * 0.5;
this.legR.rotation = -swing * 0.5;
this.rig.y = -Math.abs(Math.cos(t * 12)) * 1.5;
this.tool.visible = false;
this.bar.visible = false;
} else if (av.action === 'chop' && av.actionStart !== null && av.actionDur) {
const p = (((now - av.actionStart) / av.actionDur) % 1 + 1) % 1;
} else if (working && av.actionStart !== null && av.actionDur) {
const p = ((((now - av.actionStart) / av.actionDur) % 1) + 1) % 1;
const swing = Math.sin((Math.min(p, 0.35) / 0.35) * Math.PI);
this.tool.visible = true;
this.tool.rotation = -1.0 + swing * 1.7;
this.toolHolder.rotation = -1.0 + swing * 1.7;
this.rig.y = -swing * 1.5;
this.legL.rotation = 0;
this.legR.rotation = 0;
@@ -132,7 +155,7 @@ export class AvatarView {
this.rig.y = Math.sin(t * 2) * 0.8;
this.legL.rotation = 0;
this.legR.rotation = 0;
this.tool.visible = false;
this.toolHolder.rotation = 0;
this.bar.visible = false;
this.lastChopPhase = 0;
}