tarinai/js/tarinai_social_action_runtime.js

163 lines
11 KiB
JavaScript
Raw Normal View History

2026-06-25 14:51:58 +09:00
"use strict";
2026-06-27 19:22:38 +09:00
(function ensureDeterministicHelpers(global) {
if (typeof global.deterministicChance === "function" && typeof global.deterministicRange === "function") return;
const clamp01 = v => Math.max(0, Math.min(1, Number(v) || 0));
const keyPart = value => {
if (value == null) return "null";
if (typeof value === "number") return Number.isFinite(value) ? value.toFixed(3) : "nan";
if (typeof value === "string" || typeof value === "boolean") return String(value);
if (typeof value === "object") {
const id = value.familyKey || value.id || value.seed || value.liveToken || value.name;
if (id) return `${value.type || value.constructor?.name || "entity"}:${id}`;
const x = Number.isFinite(value.x) ? value.x.toFixed(1) : "x";
const y = Number.isFinite(value.y) ? value.y.toFixed(1) : "y";
return `${value.type || value.constructor?.name || "entity"}@${x},${y}`;
}
return String(value);
};
const hashUnit = (seed, salt = "") => {
const str = `${seed}:${salt}`;
let h = 2166136261;
for (let i = 0; i < str.length; i++) { h ^= str.charCodeAt(i); h = Math.imul(h, 16777619); }
return ((h >>> 0) % 100000) / 100000;
};
global.deterministicFrame = global.deterministicFrame || ((worldRef = null, hz = 60) => Math.floor((Number(worldRef?.time || 0) || 0) * Math.max(1, Number(hz) || 60) + 1e-6));
global.deterministicUnit = global.deterministicUnit || ((worldRef = null, salt = "", ...parts) => hashUnit(String(worldRef?.worldSeed || "tarinai-world"), [salt, global.deterministicFrame(worldRef, 60), Number(worldRef?._deterministicEpoch || 0) || 0, ...parts.map(keyPart)].join("|")));
global.deterministicRange = global.deterministicRange || ((worldRef = null, salt = "", min = 0, max = 1, ...parts) => (Number(min) || 0) + global.deterministicUnit(worldRef, salt, ...parts) * ((Number(max) || 0) - (Number(min) || 0)));
global.deterministicSigned = global.deterministicSigned || ((worldRef = null, salt = "", ...parts) => global.deterministicUnit(worldRef, salt, ...parts) < 0.5 ? -1 : 1);
global.deterministicChance = global.deterministicChance || ((worldRef = null, salt = "", chance = 0, ...parts) => { const p = clamp01(chance); return p >= 1 || (p > 0 && global.deterministicUnit(worldRef, salt, ...parts) < p); });
global.deterministicAngle = global.deterministicAngle || ((worldRef = null, salt = "", ...parts) => global.deterministicRange(worldRef, salt, 0, Math.PI * 2, ...parts));
})(typeof window !== "undefined" ? window : globalThis);
2026-06-25 14:51:58 +09:00
// 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;
2026-06-27 19:22:38 +09:00
t.fightCooldown = Math.max(t.fightCooldown || 0, CONFIG.fightCooldown + deterministicRange(world, "fight-target-lost-cooldown", 4, 8, t, rival));
2026-06-25 22:30:35 +09:00
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;
}