291 lines
11 KiB
JavaScript
291 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: "\u6442\u990c",
|
|
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,
|
|
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: 56,
|
|
intimidate_enemy: 42,
|
|
panic_escape: 94,
|
|
hide_at_owned_structure: 90,
|
|
use_medicine: 86,
|
|
rest_to_recover: 84,
|
|
sunbath: 91,
|
|
sleep_in_bed: 86,
|
|
sleep_build_grass_bed: 85,
|
|
sleep_anywhere: 84,
|
|
birth_ritual: 72,
|
|
approach_mate: 68,
|
|
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: 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 (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 (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 };
|
|
}
|
|
|