import { makeAutoObservable } from 'mobx'; import { LOGS_PER_TREE, TWIGS_PER_BUSH, TWIGS_PER_TREE } from '../constants'; import { randomId } from '../id'; import type { NodeKind, NodeResourceType, ResourceNodeData, Vec2 } from '../types'; /** A node yields its logs before its twigs. */ const RESOURCE_ORDER: NodeResourceType[] = ['log', 'twig']; /** A choppable tree or bush. Framework-free — no React/DOM dependency. */ export class ResourceNode { readonly id: string; readonly kind: NodeKind; x: number; y: number; /** Remaining yield per resource type; the node disappears once every count reaches 0. */ remaining: Record; constructor(kind: NodeKind, x: number, y: number, remaining: Record, id = randomId(kind)) { this.id = id; this.kind = kind; this.x = x; this.y = y; this.remaining = remaining; makeAutoObservable(this, {}, { autoBind: true }); } /** Builds a fresh tree or bush at `pos`, with its kind's full starting yield. */ static spawnNew(kind: NodeKind, pos: Vec2): ResourceNode { const remaining = kind === 'tree' ? { log: LOGS_PER_TREE, twig: TWIGS_PER_TREE } : { log: 0, twig: TWIGS_PER_BUSH }; return new ResourceNode(kind, pos.x, pos.y, remaining); } /** Reconstructs a node from saved data (a straight deep copy — every field has always been * required, so no defensive fallback is needed here). */ static restore(data: ResourceNodeData): ResourceNode { return new ResourceNode(data.kind, data.x, data.y, { ...data.remaining }, data.id); } /** The nearest node to `pos`, optionally restricted to one kind, or null if there are none. */ static findNearestToPosition(nodes: ResourceNode[], pos: Vec2, kind?: NodeKind): ResourceNode | null { let nearest: ResourceNode | null = null; let nearestDistance = Infinity; for (const node of nodes) { if (kind && node.kind !== kind) continue; const distance = Math.hypot(node.x - pos.x, node.y - pos.y); if (distance < nearestDistance) { nearestDistance = distance; nearest = node; } } return nearest; } get isDepleted(): boolean { return RESOURCE_ORDER.every((resource) => this.remaining[resource] <= 0); } /** The next resource this node should yield when chopped, or null once fully depleted. */ get nextResource(): NodeResourceType | null { return RESOURCE_ORDER.find((resource) => this.remaining[resource] > 0) ?? null; } /** Removes one unit of its next resource. Returns which resource was taken, or null if already * depleted. */ chopOnce(): NodeResourceType | null { const resource = this.nextResource; if (!resource) return null; this.remaining[resource] -= 1; return resource; } }