tarinai/js/tarinai_item_targeting.js

331 lines
17 KiB
JavaScript
Raw Normal View History

2026-06-25 14:51:58 +09:00
"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;
2026-06-29 16:21:58 +09:00
if (isServingFoodType(type)) return 1;
2026-06-25 14:51:58 +09:00
const value = Math.max(0, Number(amount) || 0);
return value / 18;
}
function effectiveFoodRoleMatches(item, role) {
const type = effectiveFoodTypeForItem(item);
if (!type) return false;
2026-06-29 16:21:58 +09:00
if (role === "food") return ["sweet", "food", "grass", "ant_corpse", "zunchi"].includes(type) || (isServingFoodType(type));
2026-06-27 21:50:05 +09:00
if (role === "drink") return type === "water" || type === "zunda_juice";
2026-06-29 16:21:58 +09:00
if (role === "medicine") return type === "sweet" || type === "water" || type === "zunda_juice" || type === "sleep_drug" || (isParamEffectItemType(type));
2026-06-25 14:51:58 +09:00
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);
2026-06-28 23:07:40 +09:00
if (role === "sleepPlace") return item.type === "bed" || item.type === "nest_box" || item.type === "grass_bed";
2026-06-25 22:30:35 +09:00
if (role === "danger") return ["firecracker", "genkotsu", "pushpin", "oshibyo", "splat"].includes(item.type);
2026-06-25 14:51:58 +09:00
if (role === "grassMaterial") return item.type === "grass";
return false;
}
function foodInteractionPoint(item) {
if (!item) return { x: 0, y: 0 };
2026-06-28 13:40:41 +09:00
if (item.type === "duplicator") return { x: item.x + (item.r || 34) * 0.44, y: item.y + (item.r || 34) * 0.18 };
2026-06-25 14:51:58 +09:00
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") {
2026-06-28 13:40:41 +09:00
if (item?.type === "duplicator") return Math.max(30, (tarinai?.radius || 20) * 0.72 + (item?.r || 34) * 0.36);
2026-06-25 14:51:58 +09:00
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;
2026-06-29 16:21:58 +09:00
const need = getTarinaiBehaviorNeed(tarinai);
2026-06-25 14:51:58 +09:00
const socialNeed = Number(tarinai?.needRaw?.social ?? tarinai?.needs?.social ?? 0) || 0;
const healthNeed = Number(tarinai?.needRaw?.health ?? tarinai?.needs?.health ?? 0) || 0;
2026-06-28 23:07:40 +09:00
if (type === "sweet") return 4300 + Math.min(120, Math.max(0, healthNeed - 40) * 3);
2026-06-27 21:50:05 +09:00
if (type === "water") return 3650 + Math.max(0, healthNeed - 42) * 7;
2026-06-25 14:51:58 +09:00
if (type === "zunda_juice") return 3720 + Math.max(0, healthNeed - 42) * 6;
2026-06-28 23:07:40 +09:00
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;
2026-06-29 16:21:58 +09:00
if (isParamEffectItemType(type)) return 3260 + Math.min(120, Math.max(0, healthNeed - 45) * 3);
2026-06-28 23:07:40 +09:00
if (type === "food" || type === "ant_corpse") return 3300;
2026-06-29 16:21:58 +09:00
if (isServingFoodType(type)) return 3300;
2026-06-25 14:51:58 +09:00
if (type === "grass") {
const hunger = Number(tarinai?.hunger || 0) || 0;
const foodNeed = Number(tarinai?.needRaw?.food ?? tarinai?.needs?.food ?? 0) || 0;
2026-06-28 23:07:40 +09:00
return 2200 + Math.min(260, Math.max(0, hunger - 64) * 3 + Math.max(0, foodNeed - 58) * 2);
2026-06-25 14:51:58 +09:00
}
2026-06-28 23:07:40 +09:00
if (type === "zunchi") return 900 + (tarinai?.isZunchiSlave ? 180 : 0);
2026-06-25 14:51:58 +09:00
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;
}
2026-06-28 13:40:41 +09:00
if (item?.type === "duplicator") rank += 420;
2026-06-25 14:51:58 +09:00
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;
2026-06-29 16:21:58 +09:00
if (["sweet", "food", "grass", "water", "zunda_juice", "ant_corpse"].includes(type) || (isServingFoodType(type))) {
2026-06-25 14:51:58 +09:00
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;
2026-06-28 13:40:41 +09:00
// \u6392\u6cc4\u76f4\u5f8c\u306e\u305a\u3093\u3061\u306f\u89b3\u5bdf\u30fb\u63a5\u89e6\u5bfe\u8c61\u3067\u3042\u308a\u3001\u901a\u5e38\u98df\u3068\u3057\u3066\u5373\u6d88\u8cbb\u3055\u305b\u306a\u3044\u3002
// \u305a\u3093\u3061\u3069\u308c\u3044\u3082\u6570\u79d2\u306f\u5f85\u3064\u3002\u901a\u5e38\u500b\u4f53\u306f\u6975\u9650\u98e2\u9913\u3067\u3082\u6700\u4f4e90\u79d2\u306f\u98df\u3079\u306a\u3044\u3002
2026-06-25 14:51:58 +09:00
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) {
2026-06-28 23:07:40 +09:00
const emergencyTypes = new Set(["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"]);
if (typeof FOOD_SERVING_TYPES !== "undefined") for (const type of FOOD_SERVING_TYPES) emergencyTypes.add(type);
for (const type of emergencyTypes) {
2026-06-25 14:51:58 +09:00
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;
2026-06-28 23:07:40 +09:00
if (role === "food" && typeof tarinai?.foodItemHasServingLeft === "function") {
const type = effectiveFoodTypeForItem(item);
2026-06-29 16:21:58 +09:00
if ((item.type === "duplicator" || (isServingFoodType(type))) && !tarinai.foodItemHasServingLeft(item)) continue;
2026-06-28 23:07:40 +09:00
if (item.type === "grass" && (Number(item.amount || 0) || 0) <= 0) continue;
if (item.type === "zunchi" && (Number(item.amount || 0) || 0) <= 0) continue;
}
2026-06-25 14:51:58 +09:00
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) {
2026-06-29 16:21:58 +09:00
return globalThis.TarinaiNestSleepSystem.sleepPlaceAvailableFor(world, tarinai, item) || false;
2026-06-25 14:51:58 +09:00
}
function nearestSleepPlaceOfType(world, tarinai, type, maxDist = 520) {
2026-06-29 16:21:58 +09:00
return globalThis.TarinaiNestSleepSystem.nearestSleepPlaceOfType(world, tarinai, type, maxDist) || null;
2026-06-28 23:07:40 +09:00
}
function findPrioritySleepPlaceBeforeGrassBedBuild(world, tarinai, maxDist = 520) {
2026-06-29 16:21:58 +09:00
return globalThis.TarinaiNestSleepSystem.findPrioritySleepPlaceBeforeGrassBedBuild(world, tarinai, maxDist) || null;
2026-06-25 14:51:58 +09:00
}
function findNearestSleepPlace(world, tarinai, maxDist = 520) {
2026-06-29 16:21:58 +09:00
return globalThis.TarinaiNestSleepSystem.findNearestSleepPlace(world, tarinai, maxDist) || null;
2026-06-25 14:51:58 +09:00
}
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));
2026-06-29 16:21:58 +09:00
const consumed = window.TarinaiGrass.regress(material, steps) ?? 0;
2026-06-25 14:51:58 +09:00
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;
}