not bad
This commit is contained in:
parent
2741b93dea
commit
434f07cc4e
103 changed files with 8387 additions and 8152 deletions
344
js/tarinai_item_targeting.js
Normal file
344
js/tarinai_item_targeting.js
Normal file
|
|
@ -0,0 +1,344 @@
|
|||
"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", "zunchi", "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_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;
|
||||
}
|
||||
|
||||
Loading…
Add table
Add a link
Reference in a new issue