368 lines
20 KiB
JavaScript
368 lines
20 KiB
JavaScript
"use strict";
|
|
|
|
|
|
// Social, panic, fight, mate behavior runtime ticks.
|
|
|
|
function mateTargetFor(t, world, maxDist = 520) {
|
|
if (!t || !world) return null;
|
|
const current = currentBehaviorTarget(t) || t.target;
|
|
const ok = o => o && o !== t && !o.dead && !t.juvenile && !o.juvenile && !world.areParentChild?.(t, o) && !!t.isZunchiSlave === !!o.isZunchiSlave && !o.sleepDisease && !o.fightDisease && (o.reproductionTimer || 0) <= 10 && (o.fightTimer || 0) <= 0.04 && (o.birthRitualTimer || 0) <= 0.04;
|
|
const championOk = o => ok(o) && !!t.isTarinaiChampion && !!o.isTarinaiChampion;
|
|
if (championOk(current) && dist(t, current) <= maxDist + 260) return current;
|
|
if (t.isTarinaiChampion) {
|
|
const champion = world.nearestOther?.(t, maxDist + 260, championOk) || null;
|
|
if (champion) return champion;
|
|
}
|
|
if (ok(current) && dist(t, current) <= maxDist + 120) return current;
|
|
return world.nearestOther?.(t, maxDist, ok) || null;
|
|
}
|
|
|
|
function socialFightMateWeights(t, other, world, options = {}) {
|
|
if (!t || !other || !world || t === other || t.dead || other.dead) return { fightWeight: 0, mateWeight: 0, fightEligible: false, mateEligible: false, fightProbability: 0 };
|
|
const d = Number.isFinite(Number(options.distance)) ? Number(options.distance) : dist(t, other);
|
|
const fightMochi = (t.fightMochiTimer || 0) > 0.04 || (other.fightMochiTimer || 0) > 0.04 || t.hasFightMochiEffect?.() || other.hasFightMochiEffect?.();
|
|
const loveMochi = (t.loveMochiTimer || 0) > 0.04 || (other.loveMochiTimer || 0) > 0.04 || t.hasLoveMochiEffect?.() || other.hasLoveMochiEffect?.();
|
|
const aProf = t.personalityProfile?.() || {};
|
|
const bProf = other.personalityProfile?.() || {};
|
|
const aCur = t.currentPersonality || {};
|
|
const bCur = other.currentPersonality || {};
|
|
const avg = (a, b, fallback = 0) => ((Number.isFinite(Number(a)) ? Number(a) : fallback) + (Number.isFinite(Number(b)) ? Number(b) : fallback)) * 0.5;
|
|
const aggression = avg(aCur.aggression ?? aProf.fight, bCur.aggression ?? bProf.fight, 0);
|
|
const openness = avg(aCur.openness ?? aProf.play, bCur.openness ?? bProf.play, 0);
|
|
const sociability = avg(aCur.sociability ?? aProf.social, bCur.sociability ?? bProf.social, 0);
|
|
const fearTrait = avg(aProf.fear, bProf.fear, 1);
|
|
const relationA = typeof t.relationTo === "function" ? t.relationTo(other.id) : {};
|
|
const relationB = typeof other.relationTo === "function" ? other.relationTo(t.id) : {};
|
|
const affinity = Math.max(Number(relationA.affinity || 0) || 0, Number(relationB.affinity || 0) || 0);
|
|
const fear = Math.max(Number(relationA.fear || 0) || 0, Number(relationB.fear || 0) || 0);
|
|
const postBirthPeace = (t.postBirthPeaceTimer || 0) > 0.04 || (other.postBirthPeaceTimer || 0) > 0.04;
|
|
const postConflictPeace = (t.postConflictPeaceTimer || 0) > 0.04 || (other.postConflictPeaceTimer || 0) > 0.04;
|
|
const familyFightBlocked = world.areParentChild?.(t, other) || world.areCoParents?.(t, other);
|
|
const fightEligible = Boolean(world.canFightPair?.(t, other))
|
|
&& !familyFightBlocked
|
|
&& !postBirthPeace
|
|
&& !postConflictPeace
|
|
&& (t.fightCooldown || 0) <= 0.04
|
|
&& (other.fightCooldown || 0) <= 0.04
|
|
&& (t.fightTimer || 0) <= 0.04
|
|
&& (other.fightTimer || 0) <= 0.04
|
|
&& (t.birthRitualTimer || 0) <= 0.04
|
|
&& (other.birthRitualTimer || 0) <= 0.04
|
|
&& (world.fightPairCooldownRemaining?.(t, other) || 0) <= 0.04
|
|
&& d <= Math.max(Number(options.fightRange || 58) || 58, (t.radius || 20) + (other.radius || 20) + 18);
|
|
const sameStatus = !!t.isZunchiSlave === !!other.isZunchiSlave;
|
|
const championPair = Boolean(t.isTarinaiChampion && other.isTarinaiChampion);
|
|
const mateWindow = Number.isFinite(Number(options.mateTimerWindow)) ? Number(options.mateTimerWindow) : 10;
|
|
const requireBirthReady = options.requireBirthReady !== false;
|
|
const timerOk = requireBirthReady ? ((t.reproductionTimer || 0) <= 0.04 && (other.reproductionTimer || 0) <= 0.04) : ((t.reproductionTimer || 0) <= mateWindow && (other.reproductionTimer || 0) <= mateWindow);
|
|
const mateDistanceOk = d <= Math.max(loveMochi ? 54 : 44, (t.radius || 20) + (other.radius || 20) + 14);
|
|
const enough = championPair
|
|
? ((t.hunger || 0) < 76 && (other.hunger || 0) < 78 && (t.energy || 0) > 30 && (other.energy || 0) > 30)
|
|
: (loveMochi ? ((t.hunger || 0) < 90 && (other.hunger || 0) < 92 && (t.energy || 0) > 18 && (other.energy || 0) > 18) : ((t.hunger || 0) < 55 && (other.hunger || 0) < 60 && (t.energy || 0) > 44 && (other.energy || 0) > 44));
|
|
const lonelyBond = championPair || (loveMochi ? ((t.loneliness || 0) > 4 || (other.loneliness || 0) > 4 || (t.affection || 0) > 0 || (other.affection || 0) > 0) : ((t.loneliness || 0) > 24 || (other.loneliness || 0) > 24));
|
|
const mateEligible = sameStatus
|
|
&& !t.juvenile && !other.juvenile
|
|
&& timerOk
|
|
&& mateDistanceOk
|
|
&& !world.areParentChild?.(t, other)
|
|
&& !t.sleepDisease && !other.sleepDisease && !t.fightDisease && !other.fightDisease
|
|
&& (t.fightTimer || 0) <= 0.04 && (other.fightTimer || 0) <= 0.04
|
|
&& (t.birthRitualTimer || 0) <= 0.04 && (other.birthRitualTimer || 0) <= 0.04
|
|
&& (!requireBirthReady || (enough && lonelyBond));
|
|
let fightWeight = fightEligible ? 4 : 0;
|
|
let mateWeight = mateEligible ? 6 : 0;
|
|
if (fightWeight > 0) {
|
|
const stressPressure = clamp((((t.stress || 0) + (other.stress || 0)) * 0.5 - 36) / 72, 0, 1.8);
|
|
const safetyPressure = clamp((((t.needs?.safety || 0) + (other.needs?.safety || 0)) * 0.5 - 36) / 72, 0, 1.4);
|
|
fightWeight *= clamp(1 + aggression * 0.55, 0.25, 2.8);
|
|
fightWeight *= 1 + stressPressure * 0.58 + safetyPressure * 0.24;
|
|
if (t.type === "angry" || other.type === "angry") fightWeight *= 1.42;
|
|
if (t.isZunchiSlave || other.isZunchiSlave) fightWeight *= 1.22;
|
|
if (fightMochi) fightWeight *= 3.4;
|
|
if (t.hasItemEffect?.("sedative") || other.hasItemEffect?.("sedative")) fightWeight *= 0.28;
|
|
if (loveMochi) fightWeight *= 0.58;
|
|
fightWeight *= clamp(1 - affinity / 150, 0.34, 1.08);
|
|
fightWeight *= clamp(1 - fear / 170, fightMochi ? 0.72 : 0.42, 1.05);
|
|
fightWeight *= clamp(1.14 - Math.max(0, fearTrait - 1) * 0.16, 0.62, 1.22);
|
|
if (championPair) fightWeight *= 0.35;
|
|
if (options.mode === "conflict") fightWeight *= 1.35;
|
|
}
|
|
if (mateWeight > 0) {
|
|
const stressPressure = clamp((((t.stress || 0) + (other.stress || 0)) * 0.5 - 42) / 84, 0, 1.4);
|
|
mateWeight *= clamp(1 + openness * 0.34 + sociability * 0.22, 0.34, 2.4);
|
|
mateWeight *= clamp(1 - stressPressure * 0.22, 0.55, 1.0);
|
|
mateWeight *= 1 + Math.min(0.70, affinity / 140);
|
|
if (loveMochi) mateWeight *= 3.15;
|
|
if (fightMochi) mateWeight *= 0.52;
|
|
if (championPair) mateWeight *= 4.0;
|
|
if (options.mode === "mate") mateWeight *= 1.42;
|
|
}
|
|
const total = Math.max(0, fightWeight) + Math.max(0, mateWeight);
|
|
return { fightWeight, mateWeight, fightEligible, mateEligible, fightProbability: total > 0 ? fightWeight / total : 0, totalWeight: total };
|
|
}
|
|
|
|
function pickSocialFightMate(world, key, weights, ...seeds) {
|
|
const fightWeight = Math.max(0, Number(weights?.fightWeight || 0) || 0);
|
|
const mateWeight = Math.max(0, Number(weights?.mateWeight || 0) || 0);
|
|
if (fightWeight <= 0 && mateWeight <= 0) return "none";
|
|
if (fightWeight > 0 && mateWeight <= 0) return "fight";
|
|
if (mateWeight > 0 && fightWeight <= 0) return "mate";
|
|
const bucket = Math.floor((Number(world?.time || 0) || 0) / 3);
|
|
return deterministicChance(world, key || "social-fight-mate-choice", fightWeight / (fightWeight + mateWeight), ...seeds, bucket) ? "fight" : "mate";
|
|
}
|
|
|
|
|
|
function socialCrowdThresholds(t) {
|
|
const cur = t?.currentPersonality || {};
|
|
const social = clamp(Number(cur.sociability || 0) || 0, -1, 1);
|
|
const open = clamp(Number(cur.openness || 0) || 0, -1, 1);
|
|
const social01 = (social + 1) * 0.5;
|
|
const open01 = (open + 1) * 0.5;
|
|
const preferred = 4 + social01 * 6;
|
|
const lower = Math.floor(clamp(preferred - (3 + open01 * 3), 0, 7));
|
|
const upper = Math.ceil(clamp(preferred + 5 + open01 * 7, Math.max(8, lower + 4), 24));
|
|
return { lower, upper };
|
|
}
|
|
|
|
function findSocialCrowdEscapeTarget(t, world = t?.world) {
|
|
if (!t || !world) return null;
|
|
const source = typeof world.nearbyTarinai === "function"
|
|
? (world.nearbyTarinai(t.x, t.y, 118, true) || [])
|
|
: (world.tarinai || []);
|
|
let cx = 0, cy = 0, count = 0, inspected = 0;
|
|
for (const o of source) {
|
|
if (!o || o === t || o.dead) continue;
|
|
const dx = o.x - t.x, dy = o.y - t.y;
|
|
const d2 = dx * dx + dy * dy;
|
|
if (d2 > 118 * 118 || d2 < 0.0001) continue;
|
|
const w = 1 / Math.max(24, Math.sqrt(d2));
|
|
cx += dx * w;
|
|
cy += dy * w;
|
|
count++;
|
|
if (++inspected >= 28) break;
|
|
}
|
|
const seed = typeof stableUnit === "function" ? stableUnit(t.familyKey || t.id || "crowd", "crowd-escape-angle") : 0.5;
|
|
let base = Math.atan2(-cy, -cx);
|
|
if (!count || !Number.isFinite(base)) base = seed * Math.PI * 2;
|
|
const pad = Math.max(30, (CONFIG.worldPadding || 30) + (t.radius || 22));
|
|
const radius = Math.max(14, (t.radius || 22) * 0.72);
|
|
for (const escapeDistance of [150, 215]) {
|
|
for (const off of [0, 0.42, -0.42, 0.84, -0.84]) {
|
|
const a = base + off + (seed - 0.5) * 0.18;
|
|
const x = clamp(t.x + Math.cos(a) * escapeDistance, pad, world.w - pad);
|
|
const y = clamp(t.y + Math.sin(a) * escapeDistance, pad, world.h - pad);
|
|
if (world.pointBlockedByObstacle?.(x, y, radius, { maxChecks: 12, directionalOneWay: true })) continue;
|
|
return { x, y, dead: false, detour: true, crowdEscape: true };
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
const SOCIAL_CROWD_ESCAPE_REASON = "\u4ed6\u306e\u305f\u308a\u306a\u3044\u304c\u591a\u3059\u304e\u308b\u306e\u3067\u3001\u5c11\u3057\u96e2\u308c\u3088\u3046\u3068\u3057\u3066\u3044\u308b\u3002";
|
|
|
|
function updateCrowdEscapeBehavior(t, world) {
|
|
if (!t || !world) return false;
|
|
const sample = t.socialCrowdSample || null;
|
|
const thresholds = socialCrowdThresholds(t);
|
|
const now = Number(world.time || 0) || 0;
|
|
const age = sample ? now - (Number(sample.at || 0) || 0) : Infinity;
|
|
const count = Math.max(0, Number(sample?.count || 0) || 0);
|
|
const upper = Math.max(thresholds.lower + 4, Number(sample?.upper ?? thresholds.upper) || thresholds.upper);
|
|
if (age > 3.5 || count < Math.max(thresholds.lower + 2, upper - 1)) return "finished";
|
|
|
|
let target = t.target?.crowdEscape ? t.target : null;
|
|
if (!target || target.dead || distXY(t.x, t.y, target.x, target.y) <= Math.max(28, (t.radius || 20) + 8)) {
|
|
target = findSocialCrowdEscapeTarget(t, world);
|
|
if (!target) return "finished";
|
|
t.target = target;
|
|
t.setActionState?.("wander", {
|
|
target,
|
|
reason: SOCIAL_CROWD_ESCAPE_REASON,
|
|
wake: true,
|
|
sleeping: false,
|
|
actionId: "crowd_escape",
|
|
actionLabel: "\u6df7\u96d1\u304b\u3089\u5c11\u3057\u96e2\u308c\u3088\u3046\u3068\u3057\u3066\u3044\u308b",
|
|
need: "social",
|
|
subNeed: "crowd",
|
|
});
|
|
} else {
|
|
t.setActionState?.("wander", {
|
|
target,
|
|
reason: SOCIAL_CROWD_ESCAPE_REASON,
|
|
sleeping: false,
|
|
actionId: "crowd_escape",
|
|
actionLabel: "\u6df7\u96d1\u304b\u3089\u5c11\u3057\u96e2\u308c\u3088\u3046\u3068\u3057\u3066\u3044\u308b",
|
|
need: "social",
|
|
subNeed: "crowd",
|
|
});
|
|
}
|
|
if (typeof setBehaviorText === "function") setBehaviorText(t, {
|
|
need: "social",
|
|
subNeed: "crowd",
|
|
actionId: "crowd_escape",
|
|
actionLabel: "\u6df7\u96d1\u304b\u3089\u5c11\u3057\u96e2\u308c\u3088\u3046\u3068\u3057\u3066\u3044\u308b",
|
|
reasonText: SOCIAL_CROWD_ESCAPE_REASON,
|
|
target,
|
|
phase: "perform",
|
|
source: "behavior",
|
|
});
|
|
return true;
|
|
}
|
|
|
|
function triggerFriendContact(t, other, world, dt = 0) {
|
|
if (!t || !other || !world) return false;
|
|
const amount = 0.30 + Math.max(0, Number(dt) || 0) * 0.85;
|
|
t.adjustRelation?.(other, amount, -0.05, "friend_contact");
|
|
other.adjustRelation?.(t, amount * 0.78, -0.04, "friend_contact");
|
|
t.loneliness = clamp((t.loneliness || 0) - 8, 0, 110);
|
|
if (typeof applyNeedRelief === "function") applyNeedRelief(t, { social: -16, fulfill: -3 });
|
|
if ((world.time || 0) >= (t.nextBubbleAt || 0)) t.bubble?.("\u306f\u3046", 2.2, "rgba(92,132,78,0.78)");
|
|
t.setActionState?.("idle", { target: other, reason: `${other.name || "\u4ef2\u9593"}\u3068\u4e00\u7dd2\u306b\u3044\u308b`, sleeping: false });
|
|
setBehaviorText(t, { need: "social", subNeed: "bond", actionId: "approach_friend", actionLabel: "\u4ef2\u9593\u3068\u4e00\u7dd2\u306b\u3044\u308b", reason: `${other.name || "\u4ef2\u9593"}\u3068\u4e00\u7dd2\u306b\u3044\u308b`, target: other, phase: "perform", source: "behavior" });
|
|
return "finished";
|
|
}
|
|
|
|
function updateSocialContactBehavior(t, world, dt, mode = "bond") {
|
|
if (!t || !world) return false;
|
|
let other = currentBehaviorTarget(t) || t.target || null;
|
|
if (!other || other.dead || other === t) {
|
|
other = mode === "family" ? t.parentToFollow?.() : (t.bestFriendLive?.(FRIEND_AFFINITY_THRESHOLD, 560) || world.nearestOther?.(t, 520));
|
|
}
|
|
if (!other || other.dead || other === t) return false;
|
|
const near = dist(t, other) <= Math.max(48, (t.radius || 20) + (other.radius || 20) + 12);
|
|
if (!near) return moveToOrUse(t, other, mode === "family" ? "follow_parent" : "seek_friend", mode === "family" ? "\u5bb6\u65cf\u306b\u8fd1\u3065\u3044\u3066\u3044\u308b" : "\u4ef2\u9593\u306b\u8fd1\u3065\u3044\u3066\u3044\u308b");
|
|
|
|
const weights = socialFightMateWeights(t, other, world, { mode, requireBirthReady: true, distance: dist(t, other) });
|
|
const choice = pickSocialFightMate(world, "social-contact-weighted-fight-mate", weights, t, other, mode);
|
|
if (choice === "fight") {
|
|
t.conflictTargetId = other.id;
|
|
other.conflictTargetId = t.id;
|
|
return updateFightBehavior(t, world, dt) || true;
|
|
}
|
|
if (choice === "mate" && world.startBirthRitual?.(t, other)) return "finished";
|
|
return triggerFriendContact(t, other, world, dt);
|
|
}
|
|
|
|
function updateMateBehavior(t, world, dt) {
|
|
const other = mateTargetFor(t, world, 560);
|
|
if (!other) return false;
|
|
const d = dist(t, other);
|
|
if (d <= Math.max(50, (t.radius || 20) + (other.radius || 20) + 14)) {
|
|
const forced = isCurrentBehaviorForced(t);
|
|
if (!forced) {
|
|
const weights = socialFightMateWeights(t, other, world, { mode: "mate", requireBirthReady: true, distance: d });
|
|
const choice = pickSocialFightMate(world, "mate-contact-weighted-fight-mate", weights, t, other);
|
|
if (choice === "fight") {
|
|
t.conflictTargetId = other.id;
|
|
other.conflictTargetId = t.id;
|
|
return updateFightBehavior(t, world, dt) || true;
|
|
}
|
|
if (choice === "mate" && world.startBirthRitual?.(t, other)) return "finished";
|
|
} else if (world.startBirthRitual?.(t, other)) return "finished";
|
|
moveToOrUse(t, other, "seek_friend", "\u7e41\u6b96\u3067\u304d\u308b\u76f8\u624b\u306b\u8fd1\u3065\u3044\u3066\u3044\u308b");
|
|
if (typeof applyNeedShock === "function") applyNeedShock(other, { social: dt * 10 }, t);
|
|
return true;
|
|
}
|
|
return moveToOrUse(t, other, "seek_friend", "\u7e41\u6b96\u3067\u304d\u308b\u76f8\u624b\u306b\u8fd1\u3065\u3044\u3066\u3044\u308b");
|
|
}
|
|
|
|
function updateFightBehavior(t, world, dt) {
|
|
if (!t || !world) return false;
|
|
if ((t.fightTimer || 0) > 0.04) {
|
|
const rival = currentBehaviorTarget(t) || (t.fightTargetId ? world.liveTarinaiById?.(t.fightTargetId) : null);
|
|
const maxFightContinueDistance = Math.max(150, (t.radius || 20) + (rival?.radius || 20) + 116);
|
|
if (!isLiveTarinaiEntity(rival) || dist(t, rival) > maxFightContinueDistance) {
|
|
t.fightTimer = 0;
|
|
t.fightTargetIds = [];
|
|
t.fightTargetId = null;
|
|
t.counterAttackFromId = null;
|
|
t.fightCooldown = Math.max(t.fightCooldown || 0, CONFIG.fightCooldown + deterministicRange(world, "fight-target-lost-cooldown", 4, 8, t, rival));
|
|
if (isLiveTarinaiEntity(rival)) world.markFightPairCooldown?.(t, rival, CONFIG.fightCooldown + 6);
|
|
clearForcedBehaviorQueue(t, e => e && e.id === "fight_rival");
|
|
t.setActionState?.("idle", { target: null, reason: "\u76f8\u624b\u3092\u898b\u5931\u3063\u305f\u3002", sleeping: false, clearTarget: true });
|
|
return true;
|
|
}
|
|
t.setActionState?.("fight", { target: rival || t.target, reason: rival?.name ? `${rival.name}\u3068\u55a7\u5629\u3057\u3066\u3044\u308b` : "\u55a7\u5629\u3057\u3066\u3044\u308b", sleeping: false });
|
|
return true;
|
|
}
|
|
const candidate = conflictTargetFor(t, world, (t?.fightMochiTimer || 0) > 0.04 || isCurrentBehaviorForced(t) ? 24 : 14);
|
|
const current = currentBehaviorTarget(t);
|
|
const rival = isLiveTarinaiEntity(candidate?.target) ? candidate.target : (isLiveTarinaiEntity(current) ? current : nearestForcedFightTarget(t, world, isCurrentBehaviorForced(t) ? 260 : 220));
|
|
if (!isLiveTarinaiEntity(rival)) return false;
|
|
t.conflictTargetId = rival.id;
|
|
const nearDistance = dist(t, rival);
|
|
if (nearDistance > Math.max(42, (t.radius || 20) + (rival.radius || 20) + 12)) return moveToOrUse(t, rival, "seek_enemy", "\u6c17\u306b\u5165\u3089\u306a\u3044\u76f8\u624b\u306b\u5411\u304b\u3063\u3066\u3044\u308b");
|
|
if (candidate?.forced || isCurrentBehaviorForced(t) || (t.fightMochiTimer || 0) > 0.04 || (rival.fightMochiTimer || 0) > 0.04) return !!world.startForcedFight?.(t, rival);
|
|
const weights = socialFightMateWeights(t, rival, world, { mode: "conflict", requireBirthReady: true, distance: nearDistance });
|
|
const choice = pickSocialFightMate(world, "fight-contact-weighted-fight-mate", weights, t, rival);
|
|
if (choice === "mate" && world.startBirthRitual?.(t, rival)) return true;
|
|
world.startConflict?.(t, rival);
|
|
return true;
|
|
}
|
|
|
|
function updatePanicBehavior(t, world, dt, needs) {
|
|
const breakerDanger = activePanicBreaker(t, world, 260);
|
|
const nearbyDanger = findNearbyDanger(world, t, 260) || null;
|
|
const realDanger = breakerDanger || nearbyDanger || null;
|
|
if (realDanger && realDanger !== t) t.lastPanicThreat = realDanger;
|
|
const currentTarget = currentBehaviorTarget(t);
|
|
const safety = Number(needs?.safety || 0) || 0;
|
|
const now = Number(world?.time || 0) || 0;
|
|
if (!Number.isFinite(t.panicStartedAt) || t.state !== "panic") t.panicStartedAt = now;
|
|
|
|
if (t.lastPanicCause === "tarinai_champion") {
|
|
const kingFleeReason = "\u305F\u308A\u306A\u3044\u738B\u304C\u6016\u304F\u3066\u9003\u3052\u3066\u3044\u308B\u3002";
|
|
const kingHardStop = Number(t.panicHardStopAt || 0) || 0;
|
|
if (kingHardStop > 0 && now >= kingHardStop) {
|
|
t.lastPanicCause = "";
|
|
t.fearTimer = 0;
|
|
t.lastNeedShockBreaker = null;
|
|
return "finished";
|
|
}
|
|
const nest = t.target?.type === "nest_box" ? t.target : null;
|
|
if (nest && !nest.dead) {
|
|
if (typeof setBehaviorText === "function") {
|
|
setBehaviorText(t, {
|
|
need: "safety",
|
|
actionId: "panic_escape",
|
|
actionLabel: "\u9003\u3052\u3066\u3044\u308B",
|
|
reasonText: kingFleeReason,
|
|
target: nest,
|
|
phase: "perform",
|
|
source: "behavior",
|
|
});
|
|
}
|
|
if (t.state !== "panic") t.setActionState?.("panic", { target: nest, reason: kingFleeReason, wake: true, sleeping: false });
|
|
t.fearTimer = Math.max(t.fearTimer || 0, 0.34);
|
|
if (dist(t, nest) <= Math.max(50, (t.radius || 20) + (nest.r || 24) + 18)) {
|
|
t.enterNestBox?.(nest, dt, { forceDisplace: false, reason: kingFleeReason });
|
|
}
|
|
return true;
|
|
}
|
|
}
|
|
|
|
// Survival needs must be able to break panic. Otherwise a stale fear state
|
|
// can suppress eating/sleeping long enough to kill the colony.
|
|
if (!realDanger && survivalNeedShouldCancelPanic(t, needs)) {
|
|
t.fearTimer = 0;
|
|
t.lastNeedShockBreaker = null;
|
|
return "finished";
|
|
}
|
|
|
|
const panicAge = Math.max(0, now - (Number(t.panicStartedAt) || now));
|
|
const hardStop = Number(t.panicHardStopAt || 0) || 0;
|
|
if (!realDanger && (panicAge >= 7.5 || (hardStop > 0 && now >= hardStop))) {
|
|
t.fearTimer = 0;
|
|
t.lastNeedShockBreaker = null;
|
|
return "finished";
|
|
}
|
|
|
|
if (!realDanger && safety < needThreshold("safety", "continue") && (t.fearTimer || 0) <= 0.12) return "finished";
|
|
const danger = realDanger || currentTarget;
|
|
if (t.state !== "panic" || !t.target || (t.target.dead && !Number.isFinite(t.target.x))) {
|
|
t.setActionState?.("panic", { target: t.panicDestination?.(danger, true), reason: "\u6016\u304f\u3066\u9003\u3052\u3066\u3044\u308b", wake: true, sleeping: false });
|
|
}
|
|
if (realDanger || (t.defeatedTimer || 0) > 0.04) t.fearTimer = Math.max(t.fearTimer || 0, 0.26);
|
|
if (!realDanger && safety < needThreshold("safety", "continue") && (t.fearTimer || 0) <= 0.18) return "finished";
|
|
return true;
|
|
}
|