"use strict"; // Owned-structure building behavior runtime. function structureBuildProbe(type, x, y) { const def = globalThis.StructureRegistry?.get?.(type) || {}; const r = type === "plushie" ? 6.5 : (type === "grass_bed" ? 18 : 24); return { type, x, y, r, dead: false, isStructure: true, roles: def.roles || {}, ownerId: "" }; } function canBuildOwnedStructureByAge(tarinai) { if (!tarinai || tarinai.dead) return false; if (tarinai.generation <= 1) return true; const growth = Number.isFinite(tarinai.growth) ? tarinai.growth : clamp(((tarinai.world?.time || 0) - (tarinai.birthTime || 0)) / Math.max(1, CONFIG.childGrowTime || 58), 0, 1); return growth >= 0.55; } function stopFailedBuildPlan(t, world, type = "", reason = "") { if (!t) return false; t.buildPlan = null; t.actionCooldowns = t.actionCooldowns || {}; t.actionIdCooldowns = t.actionIdCooldowns || {}; const actionId = type === "grass_bed" ? "build_grass_bed" : (type === "plushie" ? "build_plushie" : "build"); t.actionCooldowns.fulfill = Math.max(t.actionCooldowns.fulfill || 0, 5.0); t.actionCooldowns.sleep = Math.max(t.actionCooldowns.sleep || 0, type === "grass_bed" ? 3.8 : 0); t.actionIdCooldowns[actionId] = Math.max(t.actionIdCooldowns[actionId] || 0, 9.0); if (type === "grass_bed") { t.actionIdCooldowns.sleep_build_grass_bed = Math.max(t.actionIdCooldowns.sleep_build_grass_bed || 0, 9.0); t.lastFailedGrassBedBuildAt = world?.time || 0; } t.behaviorSwitchCooldownUntil = Math.max(t.behaviorSwitchCooldownUntil || 0, (world?.time || 0) + 1.2); const text = reason || "\u4f5c\u308b\u5834\u6240\u304c\u898b\u3064\u304b\u3089\u306a\u3044"; clearBehavior(t, text); t.goIdle?.(text); return false; } function forceBuiltStructureVisualRefresh(world, structure, reason = "structure-built") { if (!world) return; world.drawListDirty = true; if (world._renderStack) world._renderStack.signature = ""; if (world._visibleRenderStack) { world._visibleRenderStack.backItems = []; world._visibleRenderStack.layered = []; world._visibleRenderStack.carriedPlushies = []; world._visibleRenderStack.lodgedPins = []; } world.markItemBucketsDirty?.(reason); world.markSpatialDirty?.(reason); world.ensureItemBuckets?.(reason); world.ensureSpatial?.(reason); if (structure) { structure.world = world; } } function findStructureBuildSpot(t, world, type) { const baseX = Number.isFinite(t?.x) ? t.x : 0; const baseY = Number.isFinite(t?.y) ? t.y : 0; const candidates = []; for (let i = 0; i < 28; i++) { const angle = (i / 28) * Math.PI * 2 + rand(-0.18, 0.18); const ring = type === "grass_bed" ? rand(34, 72) : rand(18, 42); candidates.push({ x: clamp(baseX + Math.cos(angle) * ring, CONFIG.worldPadding || 30, (world?.w || 1000) - (CONFIG.worldPadding || 30)), y: clamp(baseY + Math.sin(angle) * ring, CONFIG.worldPadding || 30, (world?.h || 720) - (CONFIG.worldPadding || 30)) }); } candidates.unshift({ x: clamp(baseX + rand(-18, 18), CONFIG.worldPadding || 30, (world?.w || 1000) - (CONFIG.worldPadding || 30)), y: clamp(baseY + rand(16, 28), CONFIG.worldPadding || 30, (world?.h || 720) - (CONFIG.worldPadding || 30)) }); let best = null; let bestScore = -Infinity; for (const c of candidates) { const probe = structureBuildProbe(type, c.x, c.y); if (world?.placementBlocked?.(probe)) continue; let nearestSame = Infinity; if (type === "grass_bed") { const spacing = Math.max(46, (probe.r || 18) * 3.0); for (const it of world?.nearbyItems?.(c.x, c.y, spacing + 90) || world?.items || []) { if (!it || it.dead || it.type !== "grass_bed") continue; const d = distXY(c.x, c.y, it.x, it.y); const required = spacing + Math.max(0, ((it.r || 18) - (probe.r || 18)) * 0.5); nearestSame = Math.min(nearestSame, d); if (d < required) { nearestSame = -Infinity; break; } } if (nearestSame === -Infinity) continue; } const score = (Number.isFinite(nearestSame) ? nearestSame : 160) - distXY(c.x, c.y, baseX, baseY) * 0.12; if (score > bestScore) { best = c; bestScore = score; } } if (type === "grass_bed") return best; return best || candidates[0] || { x: baseX, y: baseY }; } function structureGrassStageCost(type = "") { return (type === "grass_bed" || type === "plushie") ? 4 : 1; } function grassMaterialAvailableStages(material) { if (!material || material.dead || material.type !== "grass" || (material.amount || 0) <= 0) return 0; const stage = typeof normalizeGrassStage === "function" ? normalizeGrassStage(material) : (typeof grassStageFromGrowth === "function" ? grassStageFromGrowth(material.growth, material.amount) : Math.floor((Number(material.amount || 0) || 0) / 24) - 1); return Math.max(0, Math.floor(Number(stage) || 0) + 1); } function buildGrassReservationIndex(world, self = null) { const index = new Map(); if (!world) return index; for (const other of world.tarinai || []) { if (!other || other.dead || other === self) continue; const plan = other.buildPlan || null; const planType = plan?.type || (/build_plushie/.test(other.behavior?.actionId || "") ? "plushie" : (/build_grass_bed/.test(other.behavior?.actionId || "") ? "grass_bed" : "")); const targetId = plan?.materialId || other.target?.id || ""; if (!targetId) continue; const state = String(other.state || ""); const actionId = String(other.behavior?.actionId || other.currentActionId || ""); if (state !== "seek_material" && state !== "build" && !/build_grass_bed|build_plushie|seek_material/.test(actionId)) continue; const cost = structureGrassStageCost(planType || ""); if (cost <= 0) continue; let rec = index.get(targetId); if (!rec) { rec = { total: 0, byType: Object.create(null), unknownReservations: 0 }; index.set(targetId, rec); } rec.total += cost; if (planType) rec.byType[planType] = (rec.byType[planType] || 0) + cost; else rec.unknownReservations += 1; } return index; } function reservedBuildGrassStages(world, material, type, self = null, reservationIndex = null) { if (!world || !material) return 0; const id = material.id || ""; if (!id) return 0; const index = reservationIndex || buildGrassReservationIndex(world, self); const rec = index.get(id); if (!rec) return 0; if (!type) return rec.total || 0; const unknownCost = (rec.unknownReservations || 0) * structureGrassStageCost(type); return (rec.byType?.[type] || 0) + unknownCost; } function findNearbyBuildMaterial(world, tarinai, type, maxDist = 520) { const required = structureGrassStageCost(type); let best = null; let bestScore = Infinity; const candidates = world?.nearbyItems?.(tarinai.x, tarinai.y, maxDist + 40, true) || world?.items || []; const reservationIndex = buildGrassReservationIndex(world, tarinai); for (const item of candidates) { if (!item || item.dead || !roleMatches(item, "grassMaterial")) continue; if (tarinai?.shouldAvoidTarget?.(item)) continue; if (world?.targetBlockedByObstacle?.(tarinai, item, Math.max(18, (tarinai.radius || 20) * 0.65))) continue; const available = grassMaterialAvailableStages(item); if (available < required) continue; const reserved = reservedBuildGrassStages(world, item, type, tarinai, reservationIndex); if (available - reserved < required) continue; const d = dist(tarinai, item); if (d > maxDist) continue; // Prefer nearby grass, but lightly spread simultaneous builders across // separate tufts so a crowd does not all reserve one valid material. const jitter = stableUnit(tarinai.id || tarinai.familyKey || "", `build-material:${item.id || ""}`); const score = d + Math.max(0, reserved) * 42 + jitter * 12; if (score < bestScore) { best = item; bestScore = score; } } return best; } function buildOwnedStructureFromGrass(t, world, type, label) { if (!(world?.canAddObjects?.(1) ?? true)) return false; const material = findNearbyBuildMaterial(world, t, type, 520); if (!material || !globalThis.StructureRegistry) return false; t.buildPlan = { type, materialId: material.id, timer: Math.max(0.4, t.buildPlan?.type === type ? Number(t.buildPlan.timer || 0) : 4), }; if (dist(t, material) > (t.radius || 20) + (material.r || 12) + 18) return moveToOrUse(t, material, "seek_material", label); t.setActionState?.("build", { target: material, reason: label, sleeping: false }); return true; } function continueBuildPlan(t, world, dt) { const plan = t.buildPlan; if (!plan?.type || !globalThis.StructureRegistry) return false; if (findOwnedStructure(world, t, plan.type)) { t.buildPlan = null; return false; } let material = (world?.itemById?.(plan.materialId) || null); if (material && (material.dead || !roleMatches(material, "grassMaterial") || grassMaterialAvailableStages(material) < structureGrassStageCost(plan.type))) material = null; if (!material) material = findNearbyBuildMaterial(world, t, plan.type, 520); if (!material) { return stopFailedBuildPlan(t, world, plan.type, "\u6750\u6599\u304c\u898b\u3064\u304b\u3089\u306a\u3044"); } plan.materialId = material.id; const label = globalThis.StructureRegistry.get(plan.type)?.label || "\u69cb\u9020\u7269"; if (dist(t, material) > (t.radius || 20) + (material.r || 12) + 18) { moveToOrUse(t, material, "seek_material", `${label}\u3092\u4f5c\u3063\u3066\u3044\u308b`); return true; } plan.timer = Math.max(0, Number(plan.timer || 0) - Math.max(0.05, dt || 0.1)); t.setActionState?.("build", { target: material, reason: `${label}\u3092\u4f5c\u3063\u3066\u3044\u308b`, sleeping: false }); if (plan.timer > 0) return true; const spot = plan.type === "plushie" ? { x: t.x, y: t.y - Math.max(28, (t.radius || 24) * 1.28) } : findStructureBuildSpot(t, world, plan.type); if (!spot) { return stopFailedBuildPlan(t, world, plan.type, "\u4F5C\u308B\u5834\u6240\u304C\u898B\u3064\u304B\u3089\u306A\u3044"); } if (!(world?.canAddObjects?.(1) ?? true)) { return stopFailedBuildPlan(t, world, plan.type, "\u30aa\u30d6\u30b8\u30a7\u30af\u30c8\u4e0a\u9650\u306b\u9054\u3057\u3066\u3044\u308b"); } consumeGrassMaterial(material, structureGrassStageCost(plan.type)); const structure = globalThis.StructureRegistry.create(plan.type, t, spot.x, spot.y, world); if (!structure) { return stopFailedBuildPlan(t, world, plan.type, "\u4f5c\u308b\u5834\u6240\u304c\u898b\u3064\u304b\u3089\u306a\u3044"); } if (plan.type === "plushie") { structure.carriedById = t.id; structure.onHead = true; structure.x = t.x; structure.y = t.y - Math.max(28, (t.radius || 24) * 1.28); } const addedStructure = world.addItem?.(structure, `build:${plan.type}`) || null; if (!addedStructure) { return stopFailedBuildPlan(t, world, plan.type, "\u4f5c\u308b\u5834\u6240\u304c\u898b\u3064\u304b\u3089\u306a\u3044"); } structure.use?.(t, world); forceBuiltStructureVisualRefresh(world, structure, `build:${plan.type}:visual-refresh`); t.setActionState?.("idle", { target: structure, reason: `${label}\u3092\u4f5c\u3063\u305f`, sleeping: false }); applyNeedSatisfaction(t, { fulfill: plan.type === "plushie" ? 50 : 36, safety: plan.type === "plushie" ? 4 : 8 }, plan.type); t.buildPlan = null; return true; }