tarinai/js/tarinai_needs_core.js
2026-07-16 22:12:03 +09:00

294 lines
11 KiB
JavaScript

"use strict";
const TARINAI_NEED_KEYS = ["food", "sleep", "health", "safety", "social", "fulfill"];
const TARINAI_NEED_PRIORITY = ["safety", "health", "food", "sleep", "social", "fulfill"];
const TARINAI_NEED_LABELS = {
food: "\u98df\u6b32",
sleep: "\u7761\u7720",
health: "\u5065\u5eb7",
safety: "\u5b89\u5168",
social: "\u95a2\u4fc2",
fulfill: "\u5145\u8db3",
};
const TARINAI_NEED_PHRASES = {
food: "\u304a\u306a\u304b\u304c\u3059\u3044\u3066\u3044\u308b",
sleep: "\u306d\u3080\u3044",
health: "\u8abf\u5b50\u304c\u60aa\u3044",
safety: "\u3042\u3076\u306a\u3044",
social: "\u3060\u308c\u304b\u304c\u6c17\u306b\u306a\u308b",
fulfill: "\u306a\u306b\u304b\u6e80\u305f\u3055\u308c\u306a\u3044",
};
const TARINAI_NEED_THRESHOLDS = {
food: { start: 64, continue: 20 },
sleep: { start: 62, continue: 30 },
health: { start: 54, continue: 32 },
safety: { start: 42, continue: 20 },
social: { start: 52, continue: 30 },
fulfill: { start: 48, continue: 26 },
};
const TARINAI_NEED_SATISFACTION_DECAY = {
food: 0.85,
sleep: 0.75,
health: 0.65,
safety: 1.10,
social: 1.05,
fulfill: 1.15,
};
const TARINAI_ACTION_LOCK_SECONDS = {
eat_food: 0.85,
drink_water: 1.8,
sleep_in_bed: 18.0,
sleep_build_grass_bed: 10.0,
sleep_anywhere: 12.0,
use_medicine: 2.4,
rest_to_recover: 6.0,
sunbath: 10.5,
seek_comfort_temperature: 5.8,
panic_escape: 2.1,
hide_at_owned_structure: 6.4,
approach_friend: 6.4,
approach_parent_or_child: 7.0,
approach_mate: 5.6,
fight_rival: 3.6,
intimidate_enemy: 1.15,
play: 8.2,
build_grass_bed: 10.0,
build_plushie: 10.0,
return_owned_structure: 6.4,
use_plushie: 7.4,
wander_lightly: 4.2,
};
const TARINAI_ACTION_PRIORITY = {
fight_rival: 72,
intimidate_enemy: 0,
panic_escape: 94,
hide_at_owned_structure: 90,
use_medicine: 86,
rest_to_recover: 84,
sunbath: 91,
seek_comfort_temperature: 53,
sleep_in_bed: 86,
sleep_build_grass_bed: 85,
sleep_anywhere: 84,
birth_ritual: 72,
approach_mate: 72,
approach_parent_or_child: 66,
approach_friend: 66,
eat_food: 50,
drink_water: 48,
build_grass_bed: 30,
build_plushie: 28,
use_plushie: 24,
return_owned_structure: 22,
play_seesaw: 27,
play: 20,
wander_lightly: 10,
};
function actionPriority(actionOrId) {
const id = typeof actionOrId === "string" ? actionOrId : actionOrId?.id;
if (!id) return 0;
return Number(TARINAI_ACTION_PRIORITY[id] ?? actionOrId?.priority ?? 0) || 0;
}
const TARINAI_FORCED_BEHAVIOR_SOURCE_PRIORITY = {
fight_mochi: 180,
love_mochi: 150,
sleep_drug: 145,
drug: 140,
user: 200,
external: 130,
};
function createDefaultNeeds() {
return { food: 0, sleep: 0, health: 0, safety: 0, social: 0, fulfill: 0 };
}
function quantizeNeed(value) {
return Math.max(0, Math.min(100, Math.round((Number(value) || 0) / 10) * 10));
}
function getNeedDisplayValue(value) {
return Math.max(0, Math.min(10, Math.round((Number(value) || 0) / 10)));
}
function needPressure(value) {
const x = Math.max(0, Math.min(100, Number(value) || 0)) / 100;
return x * x * 100;
}
function groundStressMultiplierFor(tarinai) {
const mult = Number(tarinai?.world?.groundStressMultiplier?.());
return Number.isFinite(mult) && mult > 0 ? mult : 1;
}
function applyGroundStressModifier(tarinai, nextStress) {
const next = clamp(Number(nextStress) || 0, 0, 130);
const before = Number(tarinai?.stress);
if (!Number.isFinite(before) || next <= before) return next;
return clamp(before + (next - before) * groundStressMultiplierFor(tarinai), 0, 130);
}
function calculateStressFromNeeds(needs) {
const weightedLinear =
(Number(needs.food) || 0) * 0.09 +
(Number(needs.sleep) || 0) * 0.11 +
(Number(needs.health) || 0) * 0.22 +
(Number(needs.safety) || 0) * 0.26 +
(Number(needs.social) || 0) * 0.07 +
(Number(needs.fulfill) || 0) * 0.13;
const peakNeed = Math.max(0, ...TARINAI_NEED_KEYS.map(key => Number(needs?.[key] || 0) || 0));
const peakPressure = Math.max(0, peakNeed - 62) * 0.10;
const quadraticPressure =
needPressure(needs.food) * 0.10 +
needPressure(needs.sleep) * 0.12 +
needPressure(needs.health) * 0.25 +
needPressure(needs.safety) * 0.30 +
needPressure(needs.social) * 0.08 +
needPressure(needs.fulfill) * 0.15;
return clamp(
weightedLinear +
peakPressure +
quadraticPressure * 0.35,
0,
130
);
}
function applyNeedShock(tarinai, deltas = {}, breaker = null) {
if (!tarinai || tarinai.dead) return false;
tarinai.needShock = tarinai.needShock || {};
for (const key of TARINAI_NEED_KEYS) {
const delta = Number(deltas[key] || 0) || 0;
if (delta > 0) tarinai.needShock[key] = Math.max(tarinai.needShock[key] || 0, delta);
else if (delta < 0) tarinai.needShock[key] = Math.min(tarinai.needShock[key] || 0, delta);
}
if (breaker) tarinai.lastNeedShockBreaker = breaker;
if (tarinai.needs && typeof calculateStressFromNeeds === "function") {
const projected = { ...tarinai.needs };
for (const key of TARINAI_NEED_KEYS) projected[key] = quantizeNeed((projected[key] || 0) + (tarinai.needShock[key] || 0));
tarinai.stress = applyGroundStressModifier(tarinai, calculateStressFromNeeds(projected));
}
return true;
}
function applyNeedSatisfaction(tarinai, deltas = {}) {
if (!tarinai || tarinai.dead) return false;
tarinai.needSatisfaction = tarinai.needSatisfaction || createDefaultNeeds();
let changed = false;
for (const key of TARINAI_NEED_KEYS) {
const amount = Math.max(0, Number(deltas[key] || 0) || 0);
if (amount <= 0) continue;
tarinai.needSatisfaction[key] = clamp((tarinai.needSatisfaction[key] || 0) + amount, 0, 88);
changed = true;
}
return changed;
}
function applyNeedRelief(tarinai, deltas = {}) {
if (!tarinai || tarinai.dead) return false;
const shockDeltas = {};
const satisfactionDeltas = {};
let hasShock = false;
let hasSatisfaction = false;
for (const key of TARINAI_NEED_KEYS) {
const delta = Number(deltas[key] || 0) || 0;
if (delta < 0) {
satisfactionDeltas[key] = -delta;
hasSatisfaction = true;
} else if (delta > 0) {
shockDeltas[key] = delta;
hasShock = true;
}
}
if (hasSatisfaction) applyNeedSatisfaction(tarinai, satisfactionDeltas, "relief");
if (hasShock) applyNeedShock(tarinai, shockDeltas, null);
return hasSatisfaction || hasShock;
}
function startNeedDrivenEmergencyReaction(tarinai, breaker = null, reason = "\u30d1\u30cb\u30c3\u30af\u306b\u306a\u3063\u3066\u3044\u308b") {
let actions = null;
try { actions = TARINAI_ACTIONS; } catch (err) { actions = null; }
if (!tarinai || tarinai.dead || !Array.isArray(actions)) return false;
const world = tarinai.world || globalThis.world || null;
const aggression = Number(tarinai.currentPersonality?.aggression || tarinai.personalityProfile?.().fight || 0);
const fightLikeTarget = breaker && breaker.type !== "firecracker" && breaker.kind !== "ant" && breaker.id && world?.canFightPair?.(tarinai, breaker);
const fightAction = actions.find(a => a.id === "fight_rival");
if (fightLikeTarget && aggression > 0.62 && fightAction) {
tarinai.conflictTargetId = breaker.id;
tarinai.conflictUrge = Math.max(tarinai.conflictUrge || 0, 72);
const choice = { need: "social", tiedNeeds: ["social", "safety"], max: Number(tarinai.needRaw?.social || tarinai.needs?.social || 0) || 72 };
const reasonText = buildReasonText("social", choice.tiedNeeds, fightAction, tarinai, world);
if (globalThis.TarinaiNeedsRuntime?.startNeedAction?.(tarinai, world, choice, fightAction, reasonText)) {
tarinai.lastNeedShockBreaker = null;
return true;
}
}
const panicAction = actions.find(a => a.id === "panic_escape");
if (!panicAction) return false;
const choice = { need: "safety", tiedNeeds: ["safety"], max: Number(tarinai.needRaw?.safety || tarinai.needs?.safety || 0) || 80 };
const reasonText = reason && reason.endsWith("\u3002") ? reason : buildReasonText("safety", choice.tiedNeeds, panicAction, tarinai, world);
if (globalThis.TarinaiNeedsRuntime?.startNeedAction?.(tarinai, world, choice, panicAction, reasonText)) {
tarinai.lastNeedShockBreaker = null;
return true;
}
return false;
}
function triggerNeedShockReaction(tarinai, breaker = null, reason = "\u30d1\u30cb\u30c3\u30af\u306b\u306a\u3063\u3066\u3044\u308b") {
if (!tarinai || tarinai.dead) return false;
const before = tarinai.previousNeeds || createDefaultNeeds();
const projected = { ...(tarinai.needs || createDefaultNeeds()) };
for (const key of TARINAI_NEED_KEYS) projected[key] = quantizeNeed((projected[key] || 0) + (tarinai.needShock?.[key] || 0));
const safetyDelta = (projected.safety || 0) - (before.safety || 0);
const healthDelta = (projected.health || 0) - (before.health || 0);
const fulfillDelta = (projected.fulfill || 0) - (before.fulfill || 0);
const shockTriggered = safetyDelta >= 40 || healthDelta >= 45 || fulfillDelta >= 50;
if (!shockTriggered) return false;
if (["panic", "intimidate", "fight", "birth_ritual"].includes(tarinai.state)) return false;
return startNeedDrivenEmergencyReaction(tarinai, breaker, reason);
}
function needThreshold(need, kind = "start") {
return TARINAI_NEED_THRESHOLDS[need]?.[kind] ?? (kind === "continue" ? 30 : 50);
}
function actionLockSeconds(action, tarinai = null) {
const base = Number(action?.lockSeconds ?? TARINAI_ACTION_LOCK_SECONDS[action?.id] ?? 2.0) || 2.0;
const key = `${tarinai?.familyKey || tarinai?.id || "tarinai"}:${action?.id || "idle"}`;
const jitter = (stableUnit(key, "action-lock") - 0.5) * 0.18;
return Math.max(0.5, base * (1 + jitter));
}
function scoreNeedForAction(needs, tarinai, world, need) {
const value = Number(needs?.[need]) || 0;
let score = value;
if (need === "safety") score += 14;
else if (need === "health") score += 8;
else if (need === "food") {
const hunger = Number(tarinai?.hunger || 0) || 0;
score += 3 + Math.max(0, hunger - 70) * 0.55 + Math.max(0, hunger - 92) * 0.95;
}
if ((getTarinaiBehaviorNeed(tarinai)) === need) score += 10;
const cooldown = Number(tarinai?.actionCooldowns?.[need] || 0) || 0;
if (cooldown > 0) score -= Math.min(22, cooldown * 2.8);
const nowBucket = Math.floor((world?.time || 0) / 8);
const key = `${tarinai?.familyKey || tarinai?.id || "tarinai"}:${need}:${nowBucket}`;
const noise = (stableUnit(key, "need-score") - 0.5) * 3.2;
return score + noise;
}
function chooseTopNeedRandom(needs, tarinai = null, world = null) {
const scores = {};
for (const key of TARINAI_NEED_KEYS) scores[key] = scoreNeedForAction(needs, tarinai, world, key);
const max = Math.max(...TARINAI_NEED_KEYS.map(key => scores[key]));
const tiedNeeds = TARINAI_NEED_KEYS.filter(key => max - scores[key] <= 1.5);
const need = TARINAI_NEED_PRIORITY.find(key => tiedNeeds.includes(key)) || tiedNeeds[0] || "fulfill";
return { need, tiedNeeds, max: Number(needs?.[need]) || 0, score: max, scores };
}