86 lines
2.8 KiB
TypeScript
86 lines
2.8 KiB
TypeScript
import { Container, Sprite } 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 { getSpriteTexture } from '../textures';
|
|
|
|
const NODE_SPRITES: Record<NodeKind, PixelGrid> = { tree: TREE_SPRITE, bush: BUSH_SPRITE };
|
|
const SHADOW_OFFSET: Record<NodeKind, { x: number; y: number; radiusX: number; radiusY: number }> = {
|
|
tree: { x: -3, y: 7, radiusX: 8, radiusY: 4 },
|
|
bush: { x: -2, y: 5, radiusX: 7, radiusY: 3 },
|
|
};
|
|
// 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: 11,
|
|
bush: 9,
|
|
};
|
|
|
|
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 entities: Container;
|
|
|
|
constructor(entities: Container) {
|
|
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(NODE_SPRITES[kind]);
|
|
const sprite = new Sprite(info.texture);
|
|
sprite.anchor.set(0.5);
|
|
|
|
this.entities.addChild(shadow, sprite);
|
|
const visual: NodeVisual = { shadow, sprite };
|
|
return visual;
|
|
}
|
|
}
|