"use strict"; // Food/item/structure targeting helpers for Tarinai need actions. function effectiveFoodTypeForItem(item) { if (!item) return ""; return item.type === "duplicator" ? String(item.storedFoodType || "") : String(item.type || ""); } function isDuplicatorFoodSource(item) { return !!item && item.type === "duplicator"; } const NORMAL_POOP_MEAL_THRESHOLD = 4; function normalPoopUnitsFor(foodType = "", amount = 0, options = {}) { const type = String(foodType || ""); if (type === "laxative" || type === "first_aid" || options.laxative === true) return 0; if (isServingFoodType(type)) return 1; const value = Math.max(0, Number(amount) || 0); return value / 18; } function effectiveFoodRoleMatches(item, role) { const type = effectiveFoodTypeForItem(item); if (!type) return false; if (role === "food") return ["sweet", "food", "grass", "ant_corpse", "zunchi"].includes(type) || (isServingFoodType(type)); if (role === "drink") return type === "water" || type === "zunda_juice"; if (role === "medicine") return type === "sweet" || type === "water" || type === "zunda_juice" || ["sleep_drug", "slave_chain", "golden_crown"].includes(type) || (isParamEffectItemType(type)); return false; } function roleMatches(item, role) { if (!item || item.dead) return false; if (item.amount != null && item.amount <= 0 && item.type !== "duplicator") return false; if (item.type === "duplicator") return globalThis.TarinaiSignalSystem?.powerOverride?.(item) !== false && effectiveFoodRoleMatches(item, role); if (item.roles?.[role]) return true; if (role === "food" || role === "drink" || role === "medicine") return effectiveFoodRoleMatches(item, role); if (role === "sleepPlace") return item.type === "bed" || item.type === "nest_box" || item.type === "pipe" || item.type === "grass_bed"; if (role === "danger") return ["firecracker", "genkotsu", "pushpin", "oshibyo", "splat"].includes(item.type); if (role === "grassMaterial") return item.type === "grass"; return false; } function foodInteractionPoint(item) { if (!item) return { x: 0, y: 0 }; if (item.type === "duplicator") return { x: item.x + (item.r || 34) * 0.44, y: item.y + (item.r || 34) * 0.18 }; return { x: item.x, y: item.y }; } function foodInteractionDistance(tarinai, item) { const p = foodInteractionPoint(item); const mouth = tarinai?.mouthPosition?.(item) || { x: tarinai?.x || 0, y: tarinai?.y || 0 }; return distXY(mouth.x, mouth.y, p.x, p.y); } function foodInteractionReach(tarinai, item, role = "food") { if (item?.type === "slave_chain" || item?.type === "golden_crown") { // \u738B\u51A0\u30FB\u9396\u306F\u7DDA\u753B/\u6A2A\u9577\u306E\u5916\u89B3\u306A\u306E\u3067\u3001\u901A\u5E38\u306E\u98DF\u7269\u5224\u5B9A\u3088\u308A\u898B\u305F\u76EE\u57FA\u6E96\u3067\u5E83\u304F\u53D6\u308B\u3002 // \u4E2D\u5FC3\u70B9\u304C\u5C11\u3057\u96E2\u308C\u3066\u3044\u3066\u3082\u3001\u305F\u308A\u306A\u3044\u306E\u4F53\u8868\u3068\u30A2\u30A4\u30C6\u30E0\u5916\u89B3\u304C\u89E6\u308C\u305F\u6642\u70B9\u3067\u53D6\u5F97\u3067\u304D\u308B\u3002 const bodyR = Math.max(16, Number(tarinai?.radius || 20) || 20); const itemR = Math.max(item.type === "slave_chain" ? 15 : 16, Number(item?.r || 0) || 0); const visualPad = item.type === "slave_chain" ? 14 : 12; return Math.max(46, bodyR * 0.94 + itemR * 1.28 + visualPad); } if (item?.type === "duplicator") return Math.max(30, (tarinai?.radius || 20) * 0.72 + (item?.r || 34) * 0.36); if (role === "drink") return Math.max(16, (tarinai?.radius || 20) * 0.58 + (item?.r || 12) * 0.40); return Math.max(13, (tarinai?.radius || 20) * 0.50 + (item?.r || 12) * 0.28); } function foodPriorityRank(tarinai, item) { const type = effectiveFoodTypeForItem(item); if (!type) return -1; const need = getTarinaiBehaviorNeed(tarinai); const socialNeed = Number(tarinai?.needRaw?.social ?? tarinai?.needs?.social ?? 0) || 0; const healthNeed = Number(tarinai?.needRaw?.health ?? tarinai?.needs?.health ?? 0) || 0; if (type === "sweet") return 4300 + Math.min(120, Math.max(0, healthNeed - 40) * 3); if (type === "water") return 3650 + Math.max(0, healthNeed - 42) * 7; if (type === "zunda_juice") return 3720 + Math.max(0, healthNeed - 42) * 6; if (type === "love_mochi") return 3340 + Math.min(120, Math.max(0, socialNeed - 45) * 3); if (type === "fight_mochi") return 3280 + Math.min(90, Math.max(0, Number(tarinai?.fightMochiTimer || 0)) * 3); if (type === "sleep_drug") return need === "sleep" ? 3380 : 3220; if (isParamEffectItemType(type)) return 3260 + Math.min(120, Math.max(0, healthNeed - 45) * 3); if (type === "food" || type === "ant_corpse") return 3300; if (isServingFoodType(type)) return 3300; if (type === "grass") { const hunger = Number(tarinai?.hunger || 0) || 0; const foodNeed = Number(tarinai?.needRaw?.food ?? tarinai?.needs?.food ?? 0) || 0; return 2200 + Math.min(260, Math.max(0, hunger - 64) * 3 + Math.max(0, foodNeed - 58) * 2); } if (type === "zunchi") return 900 + (tarinai?.isZunchiSlave ? 180 : 0); return -1; } function foodPriorityScore(tarinai, item, distance = 0) { let rank = foodPriorityRank(tarinai, item); if (rank < 0) return -Infinity; const hunger = Number(tarinai?.hunger || 0) || 0; if (item?.type === "zunchi") { if (zunchiEatingBlocked(tarinai, item, tarinai?.world)) return -Infinity; if (!tarinai?.isZunchiSlave) rank -= 1400; if (effectiveFoodTypeForItem(item) === "zunchi" && !tarinai?.isZunchiSlave && hunger < 104) rank -= 1200; } if (item?.type === "duplicator") rank += 420; return rank - Math.max(0, Number(distance) || 0) * 0.65; } function hasBetterFoodThanZunchiNearby(tarinai, world, maxDist = 180) { if (!tarinai || !world) return false; const source = world.nearbyItems?.(tarinai.x, tarinai.y, maxDist, true) || world.items || []; for (const it of source) { if (!it || it.dead) continue; const type = effectiveFoodTypeForItem(it); if (!type || type === "zunchi") continue; if (!roleMatches(it, "food")) continue; if (tarinai.shouldAvoidTarget?.(it)) continue; if (tarinaiTargetBlockedByObstacle(world, tarinai, it, "food")) continue; if (it.type === "grass" && (it.amount || 0) <= 0) continue; if (it.type === "duplicator" || isServingFoodType(type) || ["sweet", "food", "zunda_juice"].includes(type)) { if (typeof tarinai.foodItemHasServingLeft === "function" && !tarinai.foodItemHasServingLeft(it)) continue; } return true; } return false; } function zunchiEatingBlocked(tarinai, item, world) { if (!tarinai || !item || item.type !== "zunchi") return false; if ((Number(item.amount || 0) || 0) <= 0) return true; const protectedUntil = Number(item.eatProtectedUntil || 0) || 0; // \u305A\u3093\u3061\u306F\u98DF\u7269\u306E\u6700\u4E0B\u4F4D\u5019\u88DC\u3002\u307B\u304B\u306B\u98DF\u3079\u7269\u304C\u3042\u308B\u9593\u3060\u3051\u4FDD\u8B77\u3057\u3001 // \u307B\u304B\u306B\u98DF\u3079\u7269\u304C\u7121\u3044\u5834\u5408\u306F\u512A\u5148\u9806\u3069\u304A\u308A\u6700\u5F8C\u306E\u98DF\u6599\u3068\u3057\u3066\u98DF\u3079\u3089\u308C\u308B\u3002 if (protectedUntil > 0 && (world?.time || 0) < protectedUntil && hasBetterFoodThanZunchiNearby(tarinai, world, 900)) return true; return false; } function tarinaiTargetBlockedByObstacle(world, tarinai, item, role = "") { if (!world || !tarinai || !item) return false; // \u305A\u3093\u3060\u9905\u3060\u3051\u306F\u969C\u5BB3\u7269\u8D8A\u3057\u3067\u3082\u98DF\u3079\u306B\u884C\u304F\u3002 if ((role === "food" || role === "medicine") && effectiveFoodTypeForItem(item) === "sweet") return false; return Boolean(world.targetBlockedByObstacle?.(tarinai, item, Math.max(18, (tarinai.radius || 20) * 0.65))); } function foodRoleSearchTypes(role = "") { const set = new Set(); if (role === "food") { for (const type of ["food", "sweet", "love_mochi", "fight_mochi", "sleep_drug", "laxative", "protein", "niteropu", "ammo", "mystery_drug", "mercury", "giant_drug", "dwarf_drug", "grass", "zunchi", "ant_corpse", "zunda_juice", "duplicator"]) set.add(type); if (typeof FOOD_SERVING_TYPES !== "undefined") for (const type of FOOD_SERVING_TYPES) set.add(type); } else if (role === "drink") { for (const type of ["water", "zunda_juice", "duplicator"]) set.add(type); } else if (role === "medicine") { for (const type of ["sweet", "water", "zunda_juice", "sleep_drug", "love_mochi", "fight_mochi", "laxative", "protein", "niteropu", "ammo", "mystery_drug", "mercury", "giant_drug", "dwarf_drug", "slave_chain", "golden_crown", "duplicator"]) set.add(type); if (typeof PARAM_EFFECT_ITEM_TYPES !== "undefined") for (const type of PARAM_EFFECT_ITEM_TYPES) set.add(type); } return set; } function findNearestItemWithRole(world, tarinai, role, maxDist = 520) { let best = null, bestScore = Infinity; const searchDist = Math.max(1, Number(maxDist) || 520); const foodLikeRole = role === "food" || role === "drink" || role === "medicine"; const primary = (foodLikeRole && world?.nearbyFood) ? world.nearbyFood(tarinai.x, tarinai.y, searchDist, true) : (world?.nearbyItems?.(tarinai.x, tarinai.y, searchDist, true) || world?.items || []); const candidates = []; const seen = new Set(); const addCandidate = (it) => { if (it && !it.dead && !seen.has(it)) { seen.add(it); candidates.push(it); } }; if (Array.isArray(primary)) for (const it of primary) addCandidate(it); // \u98df\u6599\u30fb\u6c34\u30fb\u85ac\u306f\u3001\u751f\u6210\u76f4\u5f8c/\u79fb\u52d5\u76f4\u5f8c\u306e spatial cache \u6f0f\u308c\u306b\u3082\u8010\u3048\u308b\u3002 // item type buckets \u304c\u4f7f\u3048\u308b\u5834\u5408\u306f role \u3054\u3068\u306e\u578b\u3060\u3051\u3092\u898b\u308b\u3053\u3068\u3067\u3001 // crowded world \u3067\u6bce\u56de world.items \u5168\u4f53\u3092\u8db3\u3059\u7d4c\u8def\u3092\u907f\u3051\u308b\u3002 if (foodLikeRole) { if (world?.itemsOfType) { for (const type of foodRoleSearchTypes(role)) for (const it of world.itemsOfType(type) || []) addCandidate(it); } else if (Array.isArray(world?.items)) { for (const it of world.items) addCandidate(it); } } for (const item of candidates) { if (!roleMatches(item, role)) continue; if (role === "food" && typeof tarinai?.foodItemHasServingLeft === "function") { const type = effectiveFoodTypeForItem(item); if ((item.type === "duplicator" || (isServingFoodType(type))) && !tarinai.foodItemHasServingLeft(item)) continue; if (item.type === "grass" && (Number(item.amount || 0) || 0) <= 0) continue; if (item.type === "zunchi" && (Number(item.amount || 0) || 0) <= 0) continue; } if (tarinai?.shouldAvoidTarget?.(item)) continue; const d = item.type === "duplicator" ? foodInteractionDistance(tarinai, item) : dist(tarinai, item); if (d > searchDist) continue; const blocked = tarinaiTargetBlockedByObstacle(world, tarinai, item, role); let score = d; if (role === "food" || role === "medicine") { const priority = foodPriorityScore(tarinai, item, d); if (!Number.isFinite(priority)) continue; score = -priority + (blocked ? 260 : 0); } else if (blocked && !foodLikeRole) { continue; } else if (blocked) { score += 180; } if (score < bestScore) { best = item; bestScore = score; } } return best; } function sleepPlaceAvailableFor(world, tarinai, item) { return globalThis.TarinaiNestSleepSystem.sleepPlaceAvailableFor(world, tarinai, item) || false; } function nearestSleepPlaceOfType(world, tarinai, type, maxDist = 520) { return globalThis.TarinaiNestSleepSystem.nearestSleepPlaceOfType(world, tarinai, type, maxDist) || null; } function findPrioritySleepPlaceBeforeGrassBedBuild(world, tarinai, maxDist = 520) { return globalThis.TarinaiNestSleepSystem.findPrioritySleepPlaceBeforeGrassBedBuild(world, tarinai, maxDist) || null; } function findNearestSleepPlace(world, tarinai, maxDist = 520) { return globalThis.TarinaiNestSleepSystem.findNearestSleepPlace(world, tarinai, maxDist) || null; } function findNearbyDanger(world, tarinai, maxDist = 150) { const ant = (world?.nearbyAnts?.(tarinai.x, tarinai.y, maxDist, true) || [])[0] || null; const item = findNearestItemWithRole(world, tarinai, "danger", maxDist); if (ant && item) return distXY(tarinai.x, tarinai.y, ant.x, ant.y) <= dist(tarinai, item) ? ant : item; return ant || item; } function activePanicBreaker(tarinai, world, maxDist = 260) { const breaker = tarinai?.lastNeedShockBreaker || null; if (!breaker || !tarinai) return null; const cause = String(tarinai.lastPanicCause || ""); // Relation/death shocks are intentionally not active dangers. Keeping them as // breakers made old panic spread and persist after the original event ended. if (cause === "relation_death" || cause === "pending_relation_death") { tarinai.lastNeedShockBreaker = null; return null; } if (breaker.dead) { tarinai.lastNeedShockBreaker = null; return null; } const now = Number(world?.time || 0) || 0; if (Number.isFinite(tarinai.lastPanicAt) && now - tarinai.lastPanicAt > 8.0) { tarinai.lastNeedShockBreaker = null; return null; } if (!Number.isFinite(breaker.x) || !Number.isFinite(breaker.y)) return null; const d = distXY(tarinai.x, tarinai.y, breaker.x, breaker.y); const isDangerItem = roleMatches(breaker, "danger") || breaker.kind === "ant" || breaker.type === "ant"; const isThreatTarinai = breaker !== tarinai && !breaker.dead && breaker.id && (breaker.fightTimer > 0.04 || breaker.intimidateTimer > 0.04 || breaker.fightMochiTimer > 0.04); const allowedRange = isDangerItem ? maxDist : (isThreatTarinai ? maxDist * 0.75 : maxDist * 0.45); if (d <= allowedRange) return breaker; tarinai.lastNeedShockBreaker = null; return null; } function survivalNeedShouldCancelPanic(tarinai, needs) { if (!tarinai) return false; const food = Number(needs?.food ?? tarinai.needRaw?.food ?? tarinai.needs?.food ?? 0) || 0; const sleep = Number(needs?.sleep ?? tarinai.needRaw?.sleep ?? tarinai.needs?.sleep ?? 0) || 0; const health = Number(needs?.health ?? tarinai.needRaw?.health ?? tarinai.needs?.health ?? 0) || 0; return food >= 72 || (tarinai.hunger || 0) >= 82 || sleep >= 82 || (tarinai.energy || 100) <= 14 || health >= 88; } function evaluateConflictUrge(tarinai, world, maxDist = 96) { if (!tarinai || tarinai.dead || !world) return null; if ((tarinai.fightCooldown || 0) > 0.04 || (tarinai.birthRitualTimer || 0) > 0.04 || (tarinai.postBirthPeaceTimer || 0) > 0.04) return null; const p = tarinai.personalityProfile?.() || tarinai.currentPersonality || {}; const aggression = Number(tarinai.currentPersonality?.aggression ?? p.fight ?? 0) || 0; const battleDrug = (tarinai.fightMochiTimer || 0) > 0.04 || tarinai.hasFightMochiEffect?.(); const near = typeof world.nearbyTarinai === "function" ? world.nearbyTarinai(tarinai.x, tarinai.y, maxDist) : (world.tarinai || []); let best = null; for (const other of near || []) { if (!other || other === tarinai || other.dead) continue; if (!world.canFightPair?.(tarinai, other)) continue; if ((other.fightCooldown || 0) > 0.04 || (other.postBirthPeaceTimer || 0) > 0.04) continue; if (world.areParentChild?.(tarinai, other) || world.areCoParents?.(tarinai, other)) continue; const d = Math.max(1, dist(tarinai, other)); if (d > maxDist) continue; const rel = tarinai.relationTo?.(other.id) || {}; const relOther = other.relationTo?.(tarinai.id) || {}; const mixedZunchi = !!tarinai.isZunchiSlave !== !!other.isZunchiSlave; const otherBattleDrug = (other.fightMochiTimer || 0) > 0.04 || other.hasFightMochiEffect?.(); const closePressure = clamp(1 - d / maxDist, 0, 1) * 18; const stressPressure = clamp(((tarinai.stress || 0) - 52) / 42, 0, 1) * 18; const angerPressure = (tarinai.type === "angry" ? 12 : 0) + ((other.type === "angry" || otherBattleDrug) ? 8 : 0); const fearPressure = clamp(((rel.fear || 0) + (relOther.fear || 0) - 22) / 90, 0, 1) * 14; const affinityBrake = battleDrug ? 0 : clamp(Math.max(rel.affinity || 0, relOther.affinity || 0) / 80, 0, 1) * 26; const calmBrake = tarinai.shouldApplyPersonalityBehavior?.("aggression", -1) ? 22 : 0; const drugBonus = battleDrug ? 56 : 0; const zunchiBonus = mixedZunchi ? 14 : 0; const personalityBonus = clamp(aggression, -1, 1) * 10; const score = closePressure + stressPressure + angerPressure + fearPressure + drugBonus + zunchiBonus + personalityBonus - affinityBrake - calmBrake; if (!best || score > best.score) { best = { target: other, score, forced: !!battleDrug || !!otherBattleDrug, defensive: mixedZunchi || (rel.fear || 0) > 35, reason: mixedZunchi ? "\u8fd1\u304f\u306e\u76f8\u624b\u304c\u843d\u3061\u7740\u304b\u306a\u3044" : battleDrug ? "\u304d\u305a\u3064\u304d\u9905\u3067\u6c17\u304c\u7acb\u3063\u3066\u3044\u308b" : "\u6c17\u306b\u5165\u3089\u306a\u3044\u76f8\u624b\u304c\u8fd1\u3044", }; } } return best && best.score >= (best.forced ? 4 : 7) ? best : null; } function conflictTargetFor(tarinai, world, minScore = 34) { const evaluated = evaluateConflictUrge(tarinai, world, 118); if (evaluated && evaluated.score >= minScore) return evaluated; const id = tarinai?.conflictTargetId || ""; const target = id ? world?.liveTarinaiById?.(id) : null; if (!target || !world?.canFightPair?.(tarinai, target)) return null; const d = dist(tarinai, target); const carried = Math.max(0, Number(tarinai.conflictUrge || 0) || 0) - Math.max(0, d - 70) * 0.28; return carried >= minScore ? { target, score: carried, forced: (tarinai.fightMochiTimer || 0) > 0.04, defensive: false, reason: "\u6c17\u306b\u5165\u3089\u306a\u3044\u76f8\u624b\u304c\u3044\u308b" } : null; } function findNearbyMaterial(world, tarinai, materialRole, maxDist = 520) { return findNearestItemWithRole(world, tarinai, materialRole, maxDist); } function findOwnedStructure(world, tarinai, type = "", maxDist = Infinity) { return globalThis.TarinaiNestSleepSystem.findOwnedStructure(world, tarinai, type, maxDist, { blocked: (worldRef, actor, item) => tarinaiTargetBlockedByObstacle(worldRef, actor, item, "structure"), }) || null; } function findUnownedStructure(world, tarinai, type = "", maxDist = Infinity) { return globalThis.TarinaiNestSleepSystem.findUnownedStructure(world, tarinai, type, maxDist, { blocked: (worldRef, actor, item) => tarinaiTargetBlockedByObstacle(worldRef, actor, item, "structure"), }) || null; } function consumeGrassMaterial(material, stages = 1) { if (!material) return false; const steps = Math.max(1, Math.floor(Number(stages) || 1)); const consumed = window.TarinaiGrass.regress(material, steps) ?? 0; if (!(consumed > 0)) return false; material.eatenAmount = (material.eatenAmount || 0) + consumed; material.world?.markTerrainDirty?.("grass-material-consumed"); return true; } function moveToOrUse(t, target, state = "seek_food", reason = "") { if (!target) return false; t.setActionState?.(state, { target, reason, sleeping: false }); return true; }