2026-06-25 14:51:58 +09:00
|
|
|
"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 && !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;
|
|
|
|
|
if (ok(current) && dist(t, current) <= maxDist + 120) return current;
|
|
|
|
|
return world.nearestOther?.(t, maxDist, ok) || null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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 });
|
|
|
|
|
if (typeof setBehaviorText === "function") 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 conflict = conflictTargetFor(t, world, 30);
|
|
|
|
|
const forcedConflict = (t.fightMochiTimer || 0) > 0.04 || (other.fightMochiTimer || 0) > 0.04;
|
|
|
|
|
const socialPartsForContact = t.socialReasonParts || {};
|
|
|
|
|
const conflictDominant = Number(socialPartsForContact.conflict || 0) >= Math.max(Number(socialPartsForContact.bond || 0), Number(socialPartsForContact.mate || 0), Number(socialPartsForContact.family || 0)) + 8;
|
|
|
|
|
if (forcedConflict || (conflict?.target === other && conflict.score >= 34 && conflictDominant)) {
|
|
|
|
|
return updateFightBehavior(t, world, dt) || true;
|
|
|
|
|
}
|
|
|
|
|
const mate = mateTargetFor(t, world, 150);
|
|
|
|
|
const parts = t.socialReasonParts || {};
|
|
|
|
|
const selfBehaviorId = typeof getTarinaiBehaviorId === "function" ? getTarinaiBehaviorId(t) : t.behavior?.actionId;
|
|
|
|
|
const otherBehaviorId = typeof getTarinaiBehaviorId === "function" ? getTarinaiBehaviorId(other) : other.behavior?.actionId;
|
|
|
|
|
const mateIntent = mode === "mate" || selfBehaviorId === "approach_mate" || otherBehaviorId === "approach_mate" || (t.loveMochiTimer || 0) > 0.04 || (other.loveMochiTimer || 0) > 0.04;
|
|
|
|
|
const mateDominant = Number(parts.mate || 0) >= Math.max(Number(parts.bond || 0), Number(parts.conflict || 0)) + 10;
|
|
|
|
|
if (mate === other && (t.reproductionTimer || 0) <= 8 && (other.reproductionTimer || 0) <= 10 && (mateIntent || mateDominant)) {
|
|
|
|
|
if (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 otherWantsMate = (typeof getTarinaiBehaviorId === "function" ? getTarinaiBehaviorId(other) : other.behavior?.actionId) === "approach_mate" || (other.loveMochiTimer || 0) > 0.04 || (other.reproductionTimer || 0) <= 4;
|
|
|
|
|
const selfForced = isCurrentBehaviorForced(t) || (t.loveMochiTimer || 0) > 0.04;
|
|
|
|
|
if ((otherWantsMate || selfForced || (t.reproductionTimer || 0) <= 4) && 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);
|
2026-06-25 22:30:35 +09:00
|
|
|
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 + rand(4, 8));
|
|
|
|
|
if (isLiveTarinaiEntity(rival)) world.markFightPairCooldown?.(t, rival, CONFIG.fightCooldown + 6);
|
|
|
|
|
if (typeof clearForcedBehaviorQueue === "function") clearForcedBehaviorQueue(t, e => e && e.id === "fight_rival");
|
|
|
|
|
t.setActionState?.("idle", { target: null, reason: "相手を見失った。", sleeping: false, clearTarget: true });
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
t.setActionState?.("fight", { target: rival || t.target, reason: rival?.name ? `${rival.name}と喧嘩している` : "喧嘩している", sleeping: false });
|
2026-06-25 14:51:58 +09:00
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
const candidate = conflictTargetFor(t, world, (t?.fightMochiTimer || 0) > 0.04 || isCurrentBehaviorForced(t) ? 24 : 42);
|
|
|
|
|
const current = currentBehaviorTarget(t);
|
2026-06-25 22:30:35 +09:00
|
|
|
const rival = isLiveTarinaiEntity(candidate?.target) ? candidate.target : (isLiveTarinaiEntity(current) ? current : nearestForcedFightTarget(t, world, isCurrentBehaviorForced(t) ? 260 : 220));
|
2026-06-25 14:51:58 +09:00
|
|
|
if (!isLiveTarinaiEntity(rival)) return false;
|
|
|
|
|
t.conflictTargetId = rival.id;
|
|
|
|
|
if (dist(t, rival) > 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);
|
|
|
|
|
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;
|
|
|
|
|
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;
|
|
|
|
|
|
|
|
|
|
// 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;
|
|
|
|
|
}
|