"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 = 3; function normalPoopUnitsFor(foodType = "", amount = 0, options = {}) { const type = String(foodType || ""); if (type === "laxative" || options.laxative === true) return 0; if (typeof isServingFoodType === "function" && 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) || (typeof isServingFoodType === "function" && isServingFoodType(type)); if (role === "drink") return type === "water" || type === "water_bowl" || type === "zunda_juice"; if (role === "medicine") return type === "sweet" || type === "water" || type === "water_bowl" || type === "zunda_juice" || type === "sleep_drug" || (typeof isParamEffectItemType === "function" && 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 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"; 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.62, y: item.y + (item.r || 34) * 0.24 }; 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 === "duplicator") return Math.max(14, (tarinai?.radius || 20) * 0.44 + 5); 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 = typeof getTarinaiBehaviorNeed === "function" ? getTarinaiBehaviorNeed(tarinai) : tarinai?.behavior?.need; 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 4100 + Math.max(0, healthNeed - 40) * 6; if (type === "water" || type === "water_bowl") 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 3300 + Math.max(0, socialNeed - 45) * 7; if (type === "fight_mochi") return 2600 + Math.max(0, Number(tarinai?.fightMochiTimer || 0)) * 4; if (type === "sleep_drug") return need === "sleep" ? 3600 : 2450; if (typeof isParamEffectItemType === "function" && isParamEffectItemType(type)) return 3000 + Math.max(0, healthNeed - 45) * 5; if (type === "grass") { const hunger = Number(tarinai?.hunger || 0) || 0; const foodNeed = Number(tarinai?.needRaw?.food ?? tarinai?.needs?.food ?? 0) || 0; return 2000 + Math.max(0, hunger - 64) * 18 + Math.max(0, foodNeed - 58) * 12; } if (type === "zunchi") return 1000 + (tarinai?.isZunchiSlave ? 250 : 0); if (type === "food" || type === "ant_corpse") return 3000; if (typeof isServingFoodType === "function" && isServingFoodType(type)) return 3000; 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 += 85; return rank - Math.max(0, Number(distance) || 0) * 0.65; } function hasBetterFoodThanZunchiNearby(tarinai, world, maxDist = 180) { if (!tarinai || !world) return false; for (const it of world.nearbyItems?.(tarinai.x, tarinai.y, maxDist) || world.items || []) { if (!it || it.dead || it.type === "zunchi") continue; if (tarinai.shouldAvoidTarget?.(it)) continue; const type = effectiveFoodTypeForItem(it); if (!type) continue; if (["sweet", "food", "grass", "water", "water_bowl", "zunda_juice", "ant_corpse"].includes(type) || (typeof isServingFoodType === "function" && isServingFoodType(type))) { if (it.type === "grass" && (it.amount || 0) <= 0) continue; if (typeof tarinai.foodItemHasServingLeft === "function" && ["sweet", "food", "zunda_juice"].includes(type) && !tarinai.foodItemHasServingLeft(it)) continue; return true; } } return false; } function zunchiEatingBlocked(tarinai, item, world) { if (!tarinai || !item || item.type !== "zunchi") return false; const itemAge = Number(item.age || 0) || 0; const worldAge = Number.isFinite(item.producedAt) ? Math.max(0, (world?.time || 0) - item.producedAt) : itemAge; const protectedAge = Math.max(itemAge, worldAge); const protectedUntil = Number(item.eatProtectedUntil || 0) || 0; if (protectedUntil > 0 && (world?.time || 0) < protectedUntil) return true; const foodNeed = Number(tarinai.needRaw?.food ?? tarinai.needs?.food ?? 0) || 0; const hunger = Number(tarinai.hunger || 0) || 0; // 排泄直後のずんちは観察・接触対象であり、通常食として即消費させない。 // ずんちどれいも数秒は待つ。通常個体は極限飢餓でも最低90秒は食べない。 const emergency = hunger >= 104 || foodNeed >= 94; const grace = tarinai.isZunchiSlave ? (emergency ? 4 : 18) : (emergency ? 20 : 90); if (protectedAge < grace) return true; if (!emergency && !tarinai.isZunchiSlave && item.stage === "fresh" && protectedAge < 150 && foodNeed < 94 && hunger < 104) return true; return false; } function findNearestItemWithRole(world, tarinai, role, maxDist = 520) { let best = null, bestScore = Infinity; const hunger = Number(tarinai?.hunger || 0) || 0; const foodNeed = Number(tarinai?.needRaw?.food ?? tarinai?.needs?.food ?? 0) || 0; const foodEmergency = role === "food" && (hunger >= 96 || foodNeed >= 84); const searchDist = foodEmergency ? Math.max(maxDist, 1180) : maxDist; const primary = ((role === "food" || role === "drink" || role === "medicine") && world?.nearbyFood) ? world.nearbyFood(tarinai.x, tarinai.y, searchDist, true) : (world?.nearbyItems?.(tarinai.x, tarinai.y, searchDist, true) || world?.items || []); const candidates = Array.isArray(primary) ? primary.slice() : []; if (foodEmergency && world?.itemsOfType) { for (const type of ["food", "sweet", "grass", "zunchi", "ant_corpse", "zunda_juice", "duplicator"]) { for (const it of world.itemsOfType(type) || []) { if (it && !it.dead && !candidates.includes(it)) candidates.push(it); } } } for (const item of candidates) { if (!roleMatches(item, role)) continue; if (tarinai?.shouldAvoidTarget?.(item)) continue; const d = item.type === "duplicator" ? foodInteractionDistance(tarinai, item) : dist(tarinai, item); if (d > searchDist) continue; let score = d; if (role === "food" || role === "medicine") { const priority = foodPriorityScore(tarinai, item, d); if (!Number.isFinite(priority)) continue; score = -priority; } if (score < bestScore) { best = item; bestScore = score; } } return best; } function sleepPlaceAvailableFor(world, tarinai, item) { if (!world || !tarinai || !item || item.dead) return false; if (!(item.type === "nest_box" || item.type === "bed")) return false; if (tarinai.shouldAvoidTarget?.(item)) return false; const capacity = item.type === "nest_box" ? (world.nestBoxCapacity ? world.nestBoxCapacity(item) : 5) : 1; const occupancy = item.type === "nest_box" ? (world.nestBoxOccupants ? world.nestBoxOccupants(item, Infinity).length : world.bedOccupancy?.(item) || 0) : (world.bedOccupancy?.(item) || 0); return occupancy < capacity; } function nearestSleepPlaceOfType(world, tarinai, type, maxDist = 520) { let best = null; let bestScore = Infinity; const list = world?.nearbyItems?.(tarinai.x, tarinai.y, maxDist) || world?.items || []; for (const item of list) { if (!item || item.dead || item.type !== type) continue; if (!sleepPlaceAvailableFor(world, tarinai, item)) continue; const d = dist(tarinai, item); if (d > maxDist) continue; const comfort = world?.bedComfort ? world.bedComfort(item) : 1; const score = d - comfort * 18; if (score < bestScore) { best = item; bestScore = score; } } return best; } function findNearestSleepPlace(world, tarinai, maxDist = 520) { return nearestSleepPlaceOfType(world, tarinai, "nest_box", maxDist) || nearestSleepPlaceOfType(world, tarinai, "bed", maxDist) || findOwnedStructure(world, tarinai, "grass_bed", maxDist) || findUnownedStructure(world, tarinai, "grass_bed", 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 : 12) ? 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) { let best = null, bestD = maxDist; for (const item of world?.items || []) { if (!item?.isStructure || item.dead || item.ownerId !== tarinai.id) continue; if (type && item.type !== type) continue; const d = dist(tarinai, item); if (d < bestD) { best = item; bestD = d; } } return best; } function findUnownedStructure(world, tarinai, type = "", maxDist = Infinity) { let best = null, bestD = maxDist; for (const item of world?.items || []) { if (!item?.isStructure || item.dead || item.ownerId) continue; if (type && item.type !== type) continue; if (tarinai?.shouldAvoidTarget?.(item)) continue; const d = dist(tarinai, item); if (d < bestD) { best = item; bestD = d; } } return best; } 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; }