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 = { tree: TREE_SPRITE, bush: BUSH_SPRITE }; const SHADOW_OFFSET: Record = { 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 = { tree: 11, bush: 9, }; interface NodeVisual { shadow: ReturnType; 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(); private readonly entities: Container; constructor(entities: Container) { this.entities = entities; } sync(store: GameStore, elapsedSeconds: number): void { const seen = new Set(); 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; } }