"use strict"; (function (global) { const World = global.World; if (!World) throw new Error("World is not available for mixin: world_environment.js"); function normalizedAngleLocal(angle = 0, fallback = 0) { if (typeof normalizedItemAngle === "function") return normalizedItemAngle(angle, fallback); const twoPi = Math.PI * 2; const n = Number(angle); const base = Number.isFinite(n) ? n : fallback; return ((base % twoPi) + twoPi) % twoPi; } function angleForItemLocal(it) { if (typeof itemAngleFor === "function") return itemAngleFor(it); const type = String(it?.type || ""); const fallback = (type === "fence_v" || type === "bounce_fence_v") ? Math.PI / 2 : 0; return normalizedAngleLocal(it?.angle, fallback); } function orientedRectAabb(cx, cy, halfW, halfH, angle) { const c = Math.cos(angle); const s = Math.sin(angle); const ex = Math.abs(c) * halfW + Math.abs(s) * halfH; const ey = Math.abs(s) * halfW + Math.abs(c) * halfH; return { left: cx - ex, right: cx + ex, top: cy - ey, bottom: cy + ey, cos: c, sin: s }; } function sanitizeRotatorSegmentsLocal(item) { const raw = Array.isArray(item?.rotatorSegments) ? item.rotatorSegments : []; const out = []; const clampCoord = (v) => Math.max(-420, Math.min(420, Number(v) || 0)); for (const seg of raw) { if (!Array.isArray(seg) || seg.length < 4) continue; const x1 = clampCoord(seg[0]); const y1 = clampCoord(seg[1]); const x2 = clampCoord(seg[2]); const y2 = clampCoord(seg[3]); if (Math.hypot(x2 - x1, y2 - y1) < 4) continue; out.push([x1, y1, x2, y2]); if (out.length >= 96) break; } if (!out.length) out.push([-78, 0, 78, 0], [0, -52, 0, 52]); return out; } function rotatorExtentLocal(item) { let maxD = 48; for (const seg of sanitizeRotatorSegmentsLocal(item)) { maxD = Math.max(maxD, Math.hypot(seg[0], seg[1]), Math.hypot(seg[2], seg[3])); } return Math.min(460, maxD + Math.max(8, Number(item?.rotatorThickness || 12) || 12) + 8); } function rotatorWorldSegmentsLocal(item) { const angle = angleForItemLocal(item); const c = Math.cos(angle); const ss = Math.sin(angle); const cx = Number(item?.x) || 0; const cy = Number(item?.y) || 0; return sanitizeRotatorSegmentsLocal(item).map(([x1, y1, x2, y2]) => [ cx + x1 * c - y1 * ss, cy + x1 * ss + y1 * c, cx + x2 * c - y2 * ss, cy + x2 * ss + y2 * c, ]); } function rotatorSegmentRectLocal(item, x1, y1, x2, y2) { const len = Math.max(4, Math.hypot(x2 - x1, y2 - y1)); const angle = Math.atan2(y2 - y1, x2 - x1); const halfW = len / 2; const halfH = Math.max(4, Math.min(34, Number(item?.rotatorThickness || 12) || 12)) / 2; const cx = (x1 + x2) / 2; const cy = (y1 + y2) / 2; const aabb = orientedRectAabb(cx, cy, halfW, halfH, angle); return { left: aabb.left, right: aabb.right, top: aabb.top, bottom: aabb.bottom, cx, cy, halfW, halfH, angle, cos: aabb.cos, sin: aabb.sin, oriented: true, type: "rotator", item, rotator: true, restitution: 0.72, angularVelocity: Number(item?.rotatorSpeed || 0) || 0, centerX: Number(item?.x) || 0, centerY: Number(item?.y) || 0, }; } function rotatorObstacleRectsLocal(item) { if (!item || item.dead || item.type !== "rotator") return []; const rects = []; for (const seg of rotatorWorldSegmentsLocal(item)) rects.push(rotatorSegmentRectLocal(item, seg[0], seg[1], seg[2], seg[3])); return rects; } function rectLocalPoint(r, x, y) { const dx = x - (r.cx ?? 0); const dy = y - (r.cy ?? 0); const c = Number.isFinite(r.cos) ? r.cos : Math.cos(r.angle || 0); const s = Number.isFinite(r.sin) ? r.sin : Math.sin(r.angle || 0); return { x: dx * c + dy * s, y: -dx * s + dy * c }; } function rectWorldNormal(r, nx, ny) { const c = Number.isFinite(r.cos) ? r.cos : Math.cos(r.angle || 0); const s = Number.isFinite(r.sin) ? r.sin : Math.sin(r.angle || 0); return { x: nx * c - ny * s, y: nx * s + ny * c }; } function segmentIntersectsOrientedRectLocal(x1, y1, x2, y2, r, padding = 0) { const a = rectLocalPoint(r, x1, y1); const b = rectLocalPoint(r, x2, y2); const minX = -(r.halfW || 0) - padding; const maxX = (r.halfW || 0) + padding; const minY = -(r.halfH || 0) - padding; const maxY = (r.halfH || 0) + padding; let t0 = 0, t1 = 1; const clip = (p, q) => { if (Math.abs(p) < 1e-9) return q >= 0; const t = q / p; if (p < 0) { if (t > t1) return false; if (t > t0) t0 = t; } else { if (t < t0) return false; if (t < t1) t1 = t; } return true; }; const dx = b.x - a.x; const dy = b.y - a.y; return clip(-dx, a.x - minX) && clip(dx, maxX - a.x) && clip(-dy, a.y - minY) && clip(dy, maxY - a.y); } Object.defineProperties(World.prototype, Object.getOwnPropertyDescriptors({ nearest(entity, types, maxDist = Infinity) { let best = null, bestD = maxDist; const candidates = Number.isFinite(maxDist) ? this.nearbyItems(entity.x, entity.y, maxDist) : this.items; for (const it of candidates) { if (it.dead || !types.includes(it.type)) continue; if (entity?.shouldAvoidTarget && entity.shouldAvoidTarget(it)) continue; const d = dist(entity, it); if (d < bestD) { best = it; bestD = d; } } return best; }, nearestOther(entity, maxDist = Infinity, predicate = null) { let best = null, bestD = maxDist; const candidates = Number.isFinite(maxDist) ? this.nearbyTarinai(entity.x, entity.y, maxDist) : this.tarinai; for (const o of candidates) { if (o === entity || o.dead || this.isTarinaiHiddenInNestBox(o)) continue; if (predicate && !predicate(o)) continue; if (entity?.shouldAvoidTarget && entity.shouldAvoidTarget(o)) continue; const d = dist(entity, o); if (d < bestD) { best = o; bestD = d; } } return best; }, temperatureAt(x, y) { let temp = 0.54 + Math.sin(this.time / 55) * 0.05; for (const it of this.nearbyItems(x, y, 190)) { if (it.type === "stone") { const d = distXY(x, y, it.x, it.y); temp += clamp(1 - d / 190, 0, 1) * 0.35; } } return clamp(temp, 0, 1); }, fenceRect(it) { const type = it?.type || ""; if (!this.isFenceType(type)) return null; const bounce = type === "bounce_fence" || type === "bounce_fence_v"; const gate = type === "gate_fence"; const len = Math.max(112, (it.r || 42) * 3.55); const thick = Math.max(10, (it.r || 42) * 0.31); const angle = angleForItemLocal(it); const halfW = len / 2; const halfH = thick / 2; const aabb = orientedRectAabb(it.x || 0, it.y || 0, halfW, halfH, angle); return { left: aabb.left, right: aabb.right, top: aabb.top, bottom: aabb.bottom, cx: it.x || 0, cy: it.y || 0, halfW, halfH, angle, cos: aabb.cos, sin: aabb.sin, oriented: true, vertical: Math.abs(Math.sin(angle)) > Math.abs(Math.cos(angle)), horizontal: Math.abs(Math.cos(angle)) >= Math.abs(Math.sin(angle)), bounce, gate, gateOpen: Boolean(it.gateOpen), restitution: bounce ? 1.08 : undefined, minBounceSpeed: bounce ? 260 : undefined }; }, deviceRect(it) { const type = String(it?.type || ""); if (type !== "fan" && type !== "magnet") return null; const r = it.r || (type === "fan" ? 32 : 30); const angle = angleForItemLocal(it); const cx = (it.x || 0) - Math.cos(angle) * r * (type === "fan" ? 0.18 : 0.08); const cy = (it.y || 0) - Math.sin(angle) * r * (type === "fan" ? 0.18 : 0.08); const halfW = Math.max(22, r * (type === "fan" ? 0.92 : 0.88)); const halfH = Math.max(18, r * (type === "fan" ? 0.72 : 0.78)); const aabb = orientedRectAabb(cx, cy, halfW, halfH, angle); return { left: aabb.left, right: aabb.right, top: aabb.top, bottom: aabb.bottom, cx, cy, halfW, halfH, angle, cos: aabb.cos, sin: aabb.sin, oriented: true, device: true, type, item: it, restitution: 0.68, }; }, fanInfluenceShape(it) { if (!it || it.type !== "fan") return null; const angle = angleForItemLocal(it); const r = it.r || 32; return { type: "fan", x: it.x || 0, y: it.y || 0, angle, range: Math.max(170, r * 7.2), nearWidth: Math.max(42, r * 1.0), farWidth: Math.max(42, r * 4.1), }; }, magnetInfluenceShape(it) { if (!it || it.type !== "magnet") return null; const angle = angleForItemLocal(it); const r = it.r || 30; return { type: "magnet", x: (it.x || 0) + Math.cos(angle) * r * 1.10, y: (it.y || 0) + Math.sin(angle) * r * 1.10, angle, range: Math.max(150, r * 7.2), }; }, nestBoxCapacity() { return 5; }, nestBoxBaseRect(it) { if (!it || it.dead || it.type !== "nest_box") return null; const r = it.r || 42; return { left: it.x - r * 1.28, right: it.x + r * 1.28, top: it.y - r * 0.80, bottom: it.y + r * 0.72, type: "nest_box", item: it, }; }, nestBoxSolidRects(it) { const base = this.nestBoxBaseRect(it); if (!base) return []; const w = base.right - base.left; const h = base.bottom - base.top; const colW = w / 3; const rowH = h / 3; const mk = (left, right, top, bottom, cell) => ({ left, right, top, bottom, type: "nest_box", cell, item: it }); return [ mk(base.left, base.right, base.top, base.top + rowH, "top"), mk(base.left, base.left + colW, base.top + rowH, base.top + rowH * 2, "middle-left"), mk(base.right - colW, base.right, base.top + rowH, base.top + rowH * 2, "middle-right"), ]; }, nestBoxEntryPoint(box) { const base = this.nestBoxBaseRect(box); if (!base) return { x: box?.x || 0, y: box?.y || 0 }; const r = box.r || 42; // Move the approach point to the center of the open middle cell. The // collision resolver also ignores this nest box for active sleepers near // the door, so tarinai can cross the threshold instead of sliding off posts. return { x: clamp(box.x, CONFIG.worldPadding, this.w - CONFIG.worldPadding), y: clamp(box.y + r * 0.02, base.top + r * 0.46, base.bottom - r * 0.18), }; }, nestBoxExitPoint(box, occupant = null) { const base = this.nestBoxBaseRect(box); if (!base) return this.nestBoxEntryPoint(box); const r = box.r || 42; const side = occupant ? (stableUnit(occupant.id || "nest", `nest-exit-${box.id || "box"}`) - 0.5) * r * 0.36 : 0; return { x: clamp(box.x + side, CONFIG.worldPadding, this.w - CONFIG.worldPadding), y: clamp(base.bottom + r * 0.22, CONFIG.worldPadding, this.h - CONFIG.worldPadding), }; }, nestBoxInnerPoint(box, occupant = null) { const base = this.nestBoxBaseRect(box); if (!base) return { x: box?.x || 0, y: box?.y || 0 }; const occupants = this.nestBoxOccupants ? this.nestBoxOccupants(box, Infinity) : []; let index = occupants.indexOf(occupant); if (index < 0) index = Math.min(occupants.length, this.nestBoxCapacity(box) - 1); const r = box.r || 42; const slots = [ [0.00, -0.02], [-0.22, 0.04], [0.22, 0.04], [-0.11, 0.16], [0.11, 0.16], ]; const slot = slots[index % slots.length]; return { x: clamp(box.x + slot[0] * r, base.left + r * 0.46, base.right - r * 0.46), y: clamp(box.y + slot[1] * r, base.top + r * 0.34, base.bottom - r * 0.18), }; }, rotatorSegments(it) { return sanitizeRotatorSegmentsLocal(it); }, rotatorWorldSegments(it) { return rotatorWorldSegmentsLocal(it); }, rotatorExtent(it) { return rotatorExtentLocal(it); }, rotatorObstacleRects(it) { return rotatorObstacleRectsLocal(it); }, rotatorBoundsAabb(it) { const rects = rotatorObstacleRectsLocal(it); if (!rects.length) return null; return { left: Math.min(...rects.map(r => r.left)), right: Math.max(...rects.map(r => r.right)), top: Math.min(...rects.map(r => r.top)), bottom: Math.max(...rects.map(r => r.bottom)), type: "rotator", item: it, }; }, solidObstacleRects(it) { if (!it || it.dead) return []; if (this.isFenceType(it.type)) { if (it.type === "gate_fence" && it.gateOpen) return []; const rect = this.fenceRect(it); return rect ? [{ ...rect, type: it.type, item: it }] : []; } if (it.type === "fan" || it.type === "magnet") { const rect = this.deviceRect?.(it); return rect ? [{ ...rect, type: it.type, item: it }] : []; } if (it.type === "rotator") return this.rotatorObstacleRects?.(it) || []; if (it.type === "nest_box") return this.nestBoxSolidRects(it); return []; }, shouldIgnoreNestBoxCollisionFor(t, box) { if (!t || !box || box.dead || box.type !== "nest_box") return false; if (t.insideNestBoxId) return true; if (t.target !== box || (t.state !== "seek_bed" && t.state !== "sleep")) return false; const entry = this.nestBoxEntryPoint(box); const base = this.nestBoxBaseRect(box); const r = box.r || 42; const entryD = distXY(t.x, t.y, entry.x, entry.y); const centerD = distXY(t.x, t.y, box.x, box.y); const aroundDoor = entryD <= Math.max(128, r * 2.25) || centerD <= Math.max(116, r * 2.0); const inDoorColumn = base && t.x >= base.left + r * 0.18 && t.x <= base.right - r * 0.18 && t.y >= base.top + r * 0.02 && t.y <= base.bottom + r * 0.58; return Boolean(aroundDoor || inDoorColumn); }, nearbySolidObstacleRects(x, y, radius, { include = null, exclude = null, maxChecks = CONFIG.maxFenceCollisionChecks ?? 24 } = {}) { const rects = []; const seen = new Set(); let checked = 0; const addRectsFor = (it) => { if (!it || it === exclude || it.dead || seen.has(it)) return false; if (include && !include(it)) return false; const partRects = this.solidObstacleRects(it); if (!partRects.length) return false; seen.add(it); checked += 1; for (const rect of partRects) rects.push(rect); return true; }; for (const it of this.nearbyItems(x, y, radius)) { addRectsFor(it); if (checked >= maxChecks) break; } // Large custom rotators can extend well beyond their center cell; scan them // explicitly so their blades keep colliding even when the hub is far away. if (checked < maxChecks) { for (const it of this.items || []) { if (!it || it.type !== "rotator" || it.dead || seen.has(it)) continue; const reach = this.rotatorExtent?.(it) || Math.max(60, it.r || 64); if (distXY(x, y, it.x, it.y) > radius + reach + 36) continue; addRectsFor(it); if (checked >= maxChecks) break; } } return rects; }, pointInRect(x, y, r, padding = 0) { return Boolean(r && x >= r.left - padding && x <= r.right + padding && y >= r.top - padding && y <= r.bottom + padding); }, pushTarinaiOutOfRect(t, r, rr) { if (!t || !r) return false; let cx, cy, dx, dy, d, nx, ny; let insidePenetration = 0; if (r.oriented) { const local = rectLocalPoint(r, t.x, t.y); const clx = clamp(local.x, -(r.halfW || 0), r.halfW || 0); const cly = clamp(local.y, -(r.halfH || 0), r.halfH || 0); dx = local.x - clx; dy = local.y - cly; d = Math.hypot(dx, dy); if (d >= rr) return false; if (d < 0.001) { const left = Math.abs(local.x + (r.halfW || 0)); const right = Math.abs((r.halfW || 0) - local.x); const top = Math.abs(local.y + (r.halfH || 0)); const bottom = Math.abs((r.halfH || 0) - local.y); const m = Math.min(left, right, top, bottom); insidePenetration = m; if (m === left) { dx = -1; dy = 0; d = 1; } else if (m === right) { dx = 1; dy = 0; d = 1; } else if (m === top) { dx = 0; dy = -1; d = 1; } else { dx = 0; dy = 1; d = 1; } } const n = rectWorldNormal(r, dx / d, dy / d); nx = n.x; ny = n.y; cx = t.x - dx; cy = t.y - dy; } else { cx = clamp(t.x, r.left, r.right); cy = clamp(t.y, r.top, r.bottom); dx = t.x - cx; dy = t.y - cy; d = Math.hypot(dx, dy); if (d >= rr) return false; if (d < 0.001) { const left = Math.abs(t.x - r.left); const right = Math.abs(r.right - t.x); const top = Math.abs(t.y - r.top); const bottom = Math.abs(r.bottom - t.y); const m = Math.min(left, right, top, bottom); if (m === left) { dx = -1; dy = 0; d = 1; } else if (m === right) { dx = 1; dy = 0; d = 1; } else if (m === top) { dx = 0; dy = -1; d = 1; } else { dx = 0; dy = 1; d = 1; } } nx = dx / d; ny = dy / d; } const push = r.oriented && insidePenetration > 0 ? Math.min(insidePenetration + rr + 0.8, Math.max(18, rr * 2.8)) : Math.min(rr - d + 0.8, Math.max(10, rr * 0.92)); t.x += nx * push; t.y += ny * push; if (r.bounce) { const restitution = Number.isFinite(r.restitution) ? r.restitution : 1.08; const vx = t.vx || 0; const vy = t.vy || 0; const toward = vx * nx + vy * ny; if (toward < 0) { t.vx = vx - (1 + restitution) * toward * nx; t.vy = vy - (1 + restitution) * toward * ny; } else { const kick = Math.max(46, Math.min(120, rr * 3.2)); t.vx = vx + nx * kick; t.vy = vy + ny * kick; } const minBounceSpeed = Math.max(0, Number(r.minBounceSpeed || 0) || 0); if (minBounceSpeed > 0) { const currentOut = (t.vx || 0) * nx + (t.vy || 0) * ny + (t.impulseVx || 0) * nx + (t.impulseVy || 0) * ny; const now = this.time || 0; if (currentOut < minBounceSpeed && ((t.lastBounceFenceImpulseAt || -999) + 0.16 <= now || toward < 0)) { const add = minBounceSpeed - currentOut; t.impulseVx = clamp((t.impulseVx || 0) + nx * add, -520, 520); t.impulseVy = clamp((t.impulseVy || 0) + ny * add, -520, 520); t.lastBounceFenceImpulseAt = now; } } t.vx = clamp(t.vx || 0, -520, 520); t.vy = clamp(t.vy || 0, -520, 520); t.surpriseTimer = Math.max(t.surpriseTimer || 0, 0.18); if (t.bubble && (this.time || 0) >= (t.lastBounceFenceBubbleAt || -999) + 0.55) { t.lastBounceFenceBubbleAt = this.time || 0; t.bubble("!", 0.45, "rgba(80,130,210,0.72)"); } if ((this.time || 0) >= (t.lastBounceFenceEffectAt || -999) + 0.08) { t.lastBounceFenceEffectAt = this.time || 0; this.effects?.push(new Effect("ring", t.x, t.y, { size: Math.max(18, rr * 1.15), life: 0.18, color: "rgba(82,153,230,0.46)" })); } } else if (r.rotator) { const omega = clamp(Number(r.angularVelocity || 0) || 0, -8, 8); const rx = t.x - (r.centerX ?? r.item?.x ?? r.cx ?? 0); const ry = t.y - (r.centerY ?? r.item?.y ?? r.cy ?? 0); const tvx = -ry * omega; const tvy = rx * omega; t.vx = clamp((t.vx || 0) * 0.78 + tvx * 0.52 + nx * 42, -680, 680); t.vy = clamp((t.vy || 0) * 0.78 + tvy * 0.52 + ny * 42, -680, 680); t.impulseVx = clamp((t.impulseVx || 0) + tvx * 0.10, -360, 360); t.impulseVy = clamp((t.impulseVy || 0) + tvy * 0.10, -360, 360); t.surpriseTimer = Math.max(t.surpriseTimer || 0, 0.12); if ((this.time || 0) >= (t.lastRotatorHitEffectAt || -999) + 0.10) { t.lastRotatorHitEffectAt = this.time || 0; this.effects?.push(new Effect("ring", t.x, t.y, { size: Math.max(14, rr * 0.95), life: 0.16, color: "rgba(175,116,230,0.42)" })); } } else if (Math.abs(dx) > Math.abs(dy)) t.vx *= -0.18; else t.vy *= -0.18; return true; }, resolveSolidObstacleCollision(t) { if (!t || t.dead || this.isTarinaiHiddenInNestBox(t)) return; const rr = Math.max(8, t.radius * 0.74); let pushed = false; let bounced = false; for (const rect of this.nearbySolidObstacleRects(t.x, t.y, rr + 150)) { if (rect?.type === "nest_box" && this.shouldIgnoreNestBoxCollisionFor(t, rect.item)) continue; if (this.pushTarinaiOutOfRect(t, rect, rr)) { pushed = true; if (rect?.bounce) bounced = true; } } if (pushed) { const pad = CONFIG.worldPadding + Math.max(2, t.radius * 0.18); t.x = clamp(t.x, pad, this.w - pad); t.y = clamp(t.y, pad, this.h - pad); const maxV = bounced ? 520 : 130; t.vx = clamp(t.vx || 0, -maxV, maxV); t.vy = clamp(t.vy || 0, -maxV, maxV); } }, resolveFenceCollision(t) { this.resolveSolidObstacleCollision(t); }, segmentIntersectsRect(x1, y1, x2, y2, r) { if (!r) return false; if (r.oriented) return segmentIntersectsOrientedRectLocal(x1, y1, x2, y2, r, 0); if ((x1 >= r.left && x1 <= r.right && y1 >= r.top && y1 <= r.bottom) || (x2 >= r.left && x2 <= r.right && y2 >= r.top && y2 <= r.bottom)) return true; const intersects = (ax, ay, bx, by, cx, cy, dx, dy) => { const ccw = (px, py, qx, qy, rx, ry) => (ry - py) * (qx - px) > (qy - py) * (rx - px); return ccw(ax, ay, cx, cy, dx, dy) !== ccw(bx, by, cx, cy, dx, dy) && ccw(ax, ay, bx, by, cx, cy) !== ccw(ax, ay, bx, by, dx, dy); }; return intersects(x1, y1, x2, y2, r.left, r.top, r.right, r.top) || intersects(x1, y1, x2, y2, r.right, r.top, r.right, r.bottom) || intersects(x1, y1, x2, y2, r.right, r.bottom, r.left, r.bottom) || intersects(x1, y1, x2, y2, r.left, r.bottom, r.left, r.top); }, pathBlockedByFence(x1, y1, x2, y2, padding = 16) { const cx = (x1 + x2) / 2; const cy = (y1 + y2) / 2; const maxD = Math.hypot(x2 - x1, y2 - y1) / 2 + 120; for (const rect of this.nearbySolidObstacleRects(cx, cy, maxD + padding + 170)) { if (rect?.oriented) { const r = { ...rect, halfW: (rect.halfW || 0) + padding, halfH: (rect.halfH || 0) + padding }; if (this.segmentIntersectsRect(x1, y1, x2, y2, r)) return true; } else { const r = { left: rect.left - padding, right: rect.right + padding, top: rect.top - padding, bottom: rect.bottom + padding }; if (this.segmentIntersectsRect(x1, y1, x2, y2, r)) return true; } } return false; }, grassBlockedAt(x, y, self = null) { for (const it of this.nearbyItems(x, y, 96)) { if (it === self || it.dead) continue; for (const r of this.solidObstacleRects(it)) { if (this.pointInRect(x, y, r, 10)) return true; } if (it.type !== "zunchi") continue; const dx = (x - it.x) / Math.max(22, it.r * 1.35); const dy = (y - it.y) / Math.max(12, it.r * 0.82); if (dx * dx + dy * dy < 1) return true; } return false; }, grassCrowdedAt(x, y, minGap = 30, self = null) { for (const it of this.nearbyItems(x, y, Math.max(42, minGap + 16))) { if (it === self || it.dead || it.type !== "grass") continue; if (distXY(x, y, it.x, it.y) < minGap) return true; } return false; }, grassOnTarinaiAt(x, y, minGap = 24) { for (const t of this.nearbyTarinai(x, y, minGap + 36)) { if (t.dead || this.isTarinaiHiddenInNestBox(t)) continue; if (distXY(x, y, t.x, t.y) < Math.max(minGap, t.radius * 1.15)) return true; } return false; }, grassSpotOpen(x, y, { minGrassGap = 30, avoidTarinai = true, self = null } = {}) { if (x < 44 || y < 44 || x > this.w - 44 || y > this.h - 44) return false; if (this.grassBlockedAt(x, y, self)) return false; if (this.grassCrowdedAt(x, y, minGrassGap, self)) return false; if (avoidTarinai && this.grassOnTarinaiAt(x, y)) return false; return true; }, findGrassPlantingSpot(x, y, { allowOriginal = true, minRadius = 18, maxRadius = 160, attempts = 36, avoidTarinai = true } = {}) { const cx = clamp(x, 44, this.w - 44); const cy = clamp(y, 44, this.h - 44); if (allowOriginal && this.grassSpotOpen(cx, cy, { avoidTarinai })) return { x: cx, y: cy }; const blockedAtCenter = this.grassBlockedAt(cx, cy); const inner = blockedAtCenter ? Math.max(minRadius, 34) : minRadius; const outer = Math.max(maxRadius, inner + 42); const golden = Math.PI * (3 - Math.sqrt(5)); for (let i = 0; i < attempts; i++) { const t = attempts <= 1 ? 1 : i / (attempts - 1); const radius = lerp(inner, outer, Math.sqrt(t)); const angle = i * golden + stableUnit(`${cx},${cy}`, "grass-plant") * Math.PI * 2; const px = clamp(cx + Math.cos(angle) * radius, 44, this.w - 44); const py = clamp(cy + Math.sin(angle) * radius * 0.72, 44, this.h - 44); if (this.grassSpotOpen(px, py, { avoidTarinai })) return { x: px, y: py }; } return null; }, })); })(typeof window !== "undefined" ? window : globalThis);