"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; }