миграция на pixi

This commit is contained in:
azykov@mail.ru
2026-09-09 13:45:30 +03:00
parent a3f2f4eeef
commit 4e30c585d1
41 changed files with 1303 additions and 899 deletions
+91
View File
@@ -0,0 +1,91 @@
import { Container, Sprite, type Application } from 'pixi.js';
import type { GameStore } from '../../game/GameStore';
import type { NodeKind } from '../../game/types';
import { BUSH_SPRITE, TREE_SPRITE, type PixelGrid } from '../../game/sprites';
import { chopShakeOffset } from '../animations';
import { createShadow } from '../shadow';
import { applySpriteSize, getSpriteTexture } from '../textures';
const NODE_SPRITES: Record<NodeKind, PixelGrid> = { tree: TREE_SPRITE, bush: BUSH_SPRITE };
const NODE_SIZES: Record<NodeKind, number> = { tree: 64, bush: 40 };
// .entity.tree-shadow: width 64 height 32, margin (26, 0, 0, -11) -> center offset (-11, +26).
// .entity.bush-shadow: width 40 height 20, margin (16, 0, 0, -6) -> center offset (-6, +16).
const SHADOW_OFFSET: Record<NodeKind, { x: number; y: number; radiusX: number; radiusY: number }> = {
tree: { x: -11, y: 26, radiusX: 32, radiusY: 16 },
bush: { x: -6, y: 16, radiusX: 20, radiusY: 10 },
};
// .entity.tree/.bush click padding (padding: Npx; margin: -Npx) expands the clickable area beyond
// the sprite's own box — approximated here as a circle for hit-testing (see GameCanvas).
export const NODE_CLICK_RADIUS: Record<NodeKind, number> = {
tree: NODE_SIZES.tree / 2 + 12,
bush: NODE_SIZES.bush / 2 + 8,
};
interface NodeVisual {
shadow: ReturnType<typeof createShadow>;
sprite: Sprite;
}
/** Renders every tree/bush, syncing to store.nodes each frame (mirrors NodeEntity.tsx, one per
* live node) — entities are added/removed as nodes are chopped down or grown in. */
export class NodesView {
private readonly visuals = new Map<string, NodeVisual>();
private readonly app: Application;
private readonly entities: Container;
constructor(app: Application, entities: Container) {
this.app = app;
this.entities = entities;
}
sync(store: GameStore, elapsedSeconds: number): void {
const seen = new Set<string>();
for (const node of store.nodes) {
seen.add(node.id);
let visual = this.visuals.get(node.id);
if (!visual) {
visual = this.create(node.kind);
this.visuals.set(node.id, visual);
}
const offset = SHADOW_OFFSET[node.kind];
visual.shadow.position.set(node.x + offset.x, node.y + offset.y);
visual.shadow.zIndex = node.y - 1;
visual.sprite.zIndex = node.y;
const isChopping = node.id === store.heldNodeId && store.isChopping;
if (isChopping) {
const shake = chopShakeOffset(elapsedSeconds);
visual.sprite.position.set(node.x + shake.x, node.y + shake.y);
visual.sprite.rotation = shake.rotation;
} else {
visual.sprite.position.set(node.x, node.y);
visual.sprite.rotation = 0;
}
}
for (const [id, visual] of this.visuals) {
if (!seen.has(id)) {
this.entities.removeChild(visual.shadow, visual.sprite);
visual.shadow.destroy();
visual.sprite.destroy();
this.visuals.delete(id);
}
}
}
private create(kind: NodeKind): NodeVisual {
const offset = SHADOW_OFFSET[kind];
const shadow = createShadow(offset.radiusX, offset.radiusY);
const info = getSpriteTexture(this.app, NODE_SPRITES[kind]);
const sprite = new Sprite(info.texture);
sprite.anchor.set(0.5);
applySpriteSize(sprite, info, NODE_SIZES[kind]);
this.entities.addChild(shadow, sprite);
const visual: NodeVisual = { shadow, sprite };
return visual;
}
}