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

1221 lines
69 KiB
JavaScript

"use strict";
// Need planning runtime and Tarinai prototype extensions.
// Targeting, ActionSpec definitions, consumables, social ticks, and building are split into dedicated modules.
// This file keeps its local needs-loop helpers private; cross-file helpers come
// from their owning modules loaded earlier in index.html.
(function (global) {
function decayNeedSatisfaction(tarinai, dt = 0) {
if (!tarinai) return;
tarinai.needSatisfaction = tarinai.needSatisfaction || createDefaultNeeds();
const step = Math.max(0, Number(dt) || 0);
if (step <= 0) return;
for (const key of TARINAI_NEED_KEYS) {
const decay = (TARINAI_NEED_SATISFACTION_DECAY[key] || 1) * step;
tarinai.needSatisfaction[key] = Math.max(0, (Number(tarinai.needSatisfaction[key] || 0) || 0) - decay);
}
}
function decayNeedShock(tarinai, dt = 0) {
if (!tarinai?.needShock) return;
const step = Math.max(0, Number(dt) || 0);
const decay = step > 0 ? step * 18 : 0;
if (decay <= 0) return;
for (const key of TARINAI_NEED_KEYS) {
const shock = Number(tarinai.needShock[key] || 0) || 0;
tarinai.needShock[key] = shock > 0 ? Math.max(0, shock - decay) : Math.min(0, shock + decay);
}
}
function circadianSleepPhase(world) {
const p = world?.dayProgress?.() || 0;
// Sleep pressure should visibly rise before dusk and fade around dawn.
// Keep the behavior controlled by need deltas rather than forcing sleep/wake
// at a fixed clock time. The steep increase begins before the evening
// phase, and sleeping reduces the pressure slowly enough to settle near dawn.
const sunsetHour = 17;
const sunriseHour = 5;
const highGrowthStart = Math.max(0, (sunsetHour - 2) / 24);
const decayStart = sunriseHour / 24;
const night = p >= highGrowthStart || p < decayStart;
const eveningRamp = p >= Math.max(0, highGrowthStart - 2 / 24) && p < highGrowthStart;
const afternoon = p >= 0.46 && p < Math.max(0.46, highGrowthStart - 2 / 24);
return { p, night, eveningRamp, afternoon, highGrowthStart, decayStart };
}
function updateCircadianSleepPressure(tarinai, world, dt = 0) {
const phase = circadianSleepPhase(world);
const sleeping = Boolean(tarinai.state === "sleep" || tarinai.sleeping);
if (!Number.isFinite(tarinai.circadianSleepPressure)) {
tarinai.circadianSleepPressure = phase.night ? rand(46, 64) : (phase.afternoon || phase.eveningRamp ? rand(8, 18) : rand(0, 5));
}
const step = Math.max(0, Number(dt) || 0);
if (sleeping) {
const target = phase.night ? 18 : 0;
const current = Number(tarinai.circadianSleepPressure || 0) || 0;
const extra = Math.max(0, current - target) * (phase.night ? 0.002 : 0.018);
const bedLike = tarinai.target && typeof tarinai.isSleepFurniture === "function" && tarinai.isSleepFurniture(tarinai.target);
const sleepDecay = (phase.night ? (bedLike ? 0.45 : 0.36) : (bedLike ? 2.15 : 1.45)) + extra;
tarinai.circadianSleepPressure = clamp(current - step * sleepDecay, 0, 94);
} else if (phase.night) {
const duskBoost = phase.p >= phase.highGrowthStart && phase.p < 0.66 ? 1.0 : 0;
tarinai.circadianSleepPressure = clamp(tarinai.circadianSleepPressure + step * (2.35 + duskBoost), 0, 94);
} else if (phase.eveningRamp) {
tarinai.circadianSleepPressure = clamp(tarinai.circadianSleepPressure + step * 0.82, 0, 60);
} else if (phase.afternoon) {
tarinai.circadianSleepPressure = clamp(tarinai.circadianSleepPressure + step * 0.10, 0, 22);
} else {
tarinai.circadianSleepPressure = clamp(tarinai.circadianSleepPressure - step * 0.74, 0, 94);
}
return { ...phase, pressure: tarinai.circadianSleepPressure };
}
function updateNeeds(tarinai, world, dt = 0) {
const p = tarinai.currentPersonality || {};
const aggression = Number(p.aggression) || 0;
const openness = Number(p.openness) || 0;
const sociability = Number(p.sociability) || 0;
const neuroticism = Number(p.neuroticism) || 0;
const raw = createDefaultNeeds();
raw.food = (tarinai.hunger || 0) * 0.88;
const sleepPhase = updateCircadianSleepPressure(tarinai, world, dt);
const energyMax = Math.max(1, typeof tarinaiMaxEnergy === "function" ? tarinaiMaxEnergy(tarinai) : (Number(tarinai.maxEnergy) || 100));
tarinai.maxEnergy = energyMax;
const energyPct = clamp((Number(tarinai.energy) || 0) / energyMax, 0, 1);
const energyLack = (1 - energyPct) * 100;
const energySleepNeed = energyLack * 0.22;
raw.sleep = energySleepNeed + sleepPhase.pressure;
if (tarinai.sleepDisease) raw.sleep += 34;
raw.health = Math.max(0, energyLack * 0.18 + (tarinai.zunchiStain || 0) * 0.32);
if (tarinai.zunchiDisease || tarinai.sleepDisease || tarinai.explosionDisease || tarinai.fightDisease) raw.health += 42;
const lodgedBehavior = tarinai.currentLodgedPinBehavior?.() || null;
if ((tarinai.hurtTimer || 0) > 0.04 || ((tarinai.stuckPushpinId || "") && (lodgedBehavior?.panicOnAttach || (lodgedBehavior?.damagePerTick || 0) > 0 || (lodgedBehavior?.damageOnAttach || 0) > 0))) raw.health += 36;
const danger = findNearbyDanger(world, tarinai, 170);
if (danger) raw.safety += danger.kind ? 78 : 48;
const feltTemperature = world?.feltTemperatureFor?.(tarinai) ?? world?.temperatureAt?.(tarinai.x, tarinai.y, tarinai);
if (Number.isFinite(Number(feltTemperature))) {
const tempStatus = world.temperatureStatusFor?.(feltTemperature, tarinai) || null;
tarinai.feltTemperature = Number(feltTemperature);
tarinai.tempComfort = Number(feltTemperature);
tarinai.temperatureStatus = tempStatus;
if (tempStatus && (tempStatus.discomfort || 0) > 0) {
raw.health += Math.max(0, (tempStatus.discomfort || 0) - 2) * 6.4;
raw.safety += Math.max(0, (tempStatus.stressExcess || 0)) * 5.6;
if ((tempStatus.harmExcess || 0) > 0) raw.health += (tempStatus.harmExcess || 0) * 7.2;
}
}
if ((tarinai.fearTimer || 0) > 0.12 || (tarinai.lastDamageAt || -999) + 5 > (world?.time || 0)) raw.safety += 34;
const socialParts = { bond: 0, family: 0, mate: 0, conflict: 0, fearSocial: 0 };
socialParts.bond = (tarinai.loneliness || 0) * (0.88 + Math.max(0, sociability) * 0.16);
raw.social = socialParts.bond;
if (tarinai.parentToFollow?.()) { socialParts.family += 18; raw.social += 18; }
const championMate = tarinai.isTarinaiChampion && (tarinai.reproductionTimer || 0) <= 8 && (world?.canAddTarinai?.(1) ?? true)
? (world?.nearestOther?.(tarinai, 780, other => other && other !== tarinai && !other.dead && other.isTarinaiChampion && !world.areParentChild?.(tarinai, other) && !!tarinai.isZunchiSlave === !!other.isZunchiSlave && !other.sleepDisease && !other.fightDisease && (other.reproductionTimer || 0) <= 10) || null)
: null;
if (championMate) { socialParts.mate += 48; socialParts.championMate = 1; raw.social += 48; }
else if ((tarinai.reproductionTimer || 0) <= 8 && !tarinai.hasPaired) { socialParts.mate += 16 + ((tarinai.loveMochiTimer || 0) > 0.04 ? 42 : 0); raw.social += socialParts.mate; }
const enemy = tarinai.strongestRelation?.("fear");
if (enemy?.score > 12) { socialParts.fearSocial += 10; raw.social += 10; }
const conflict = evaluateConflictUrge(tarinai, world, 112);
if (conflict?.target && (conflict.forced || conflict.score >= 14)) {
tarinai.conflictTargetId = conflict.target.id;
tarinai.conflictUrge = Math.max(Number(tarinai.conflictUrge || 0) || 0, conflict.score);
const conflictPart = conflict.score;
socialParts.conflict += conflictPart;
raw.social += conflictPart;
if (conflict.defensive) raw.safety += conflict.score * 0.24;
} else if (tarinai.conflictUrge) {
tarinai.conflictUrge = Math.max(0, (Number(tarinai.conflictUrge || 0) || 0) - Math.max(0, Number(dt) || 0) * 12);
const carriedConflict = Math.min(18, tarinai.conflictUrge * 0.22);
socialParts.conflict += carriedConflict;
raw.social += carriedConflict;
}
if ((tarinai.fightMochiTimer || 0) > 0.04) {
socialParts.conflict += 34;
raw.social += 34;
raw.safety += 8;
}
tarinai.socialReasonParts = socialParts;
if ((tarinai.defeatedTimer || 0) > 0.04 || (tarinai.intimidatedTimer || 0) > 0.04) raw.safety += 46;
const ownBed = findOwnedStructure(world, tarinai, "grass_bed");
const ownPlushie = findOwnedStructure(world, tarinai, "plushie");
const needDt = Math.max(0, Number(dt) || 0);
const boredomDelta = needDt * ((tarinai.state === "idle" || tarinai.state === "wander") ? 0.55 : -0.30);
const boredom = clamp((tarinai.boredom || 0) + boredomDelta, 0, 36);
tarinai.boredom = boredom;
raw.fulfill = 18 + boredom + (openness > 0 ? openness * 16 : 0);
if (!ownBed) raw.fulfill += 18;
if (ownBed) {
raw.sleep -= dist(tarinai, ownBed) < 80 ? 16 : 6;
raw.safety -= dist(tarinai, ownBed) < 100 ? 14 : 4;
raw.fulfill -= dist(tarinai, ownBed) < 120 ? 20 : 7;
}
const plushiePart = !ownPlushie ? 14 : 0;
const plushieComfortPart = ownPlushie ? -(ownPlushie.carriedById === tarinai.id ? 28 : 13) : 0;
if (!ownPlushie) raw.fulfill += plushiePart;
else raw.fulfill += plushieComfortPart;
const materialPart = (!ownBed || !ownPlushie) && findNearestItemWithRole(world, tarinai, "grassMaterial", 420) ? 8 : 0;
if (materialPart) raw.fulfill += materialPart;
tarinai.fulfillReasonParts = {
boredom,
bed: ownBed ? 0 : 18,
bedComfort: ownBed ? (dist(tarinai, ownBed) < 120 ? -20 : -7) : 0,
plushie: plushiePart,
plushieComfort: plushieComfortPart,
material: materialPart,
openness: openness > 0 ? openness * 16 : 0,
};
for (const key of TARINAI_NEED_KEYS) {
const shock = Number(tarinai.needShock?.[key] || 0) || 0;
if (shock !== 0) raw[key] += shock;
}
raw.safety *= clamp(1 - Math.max(0, aggression) * 0.12 + Math.max(0, neuroticism) * 0.16, 0.75, 1.25);
raw.health *= clamp(1 + Math.max(0, neuroticism) * 0.14, 0.9, 1.22);
raw.fulfill *= clamp(1 + Math.max(0, openness) * 0.14, 0.9, 1.24);
decayNeedSatisfaction(tarinai, dt);
const effective = createDefaultNeeds();
const display = createDefaultNeeds();
tarinai.needSatisfaction = tarinai.needSatisfaction || createDefaultNeeds();
for (const key of TARINAI_NEED_KEYS) {
const base = clamp(Number(raw[key] || 0) || 0, 0, 100);
const satisfaction = Math.max(0, Number(tarinai.needSatisfaction[key] || 0) || 0);
effective[key] = clamp(base - satisfaction, 0, 100);
display[key] = quantizeNeed(effective[key]);
}
tarinai.previousNeeds = tarinai.needs ? { ...tarinai.needs } : createDefaultNeeds();
tarinai.needRawBase = raw;
tarinai.needRaw = effective;
tarinai.needDisplay = display;
tarinai.needs = display;
tarinai.stress = applyGroundStressModifier(tarinai, calculateStressFromNeeds(effective));
decayNeedShock(tarinai, dt);
return effective;
}
function tarinaiNeedCalcInterval(tarinai, world) {
const jitter = stableUnit(tarinai?.familyKey || tarinai?.id || "tarinai", "need-calc-interval") * 0.35;
const urgent = (tarinai?.fightTimer || 0) > 0.04 || (tarinai?.birthRitualTimer || 0) > 0.04 || (tarinai?.eatTimer || 0) > 0.04 || (tarinai?.sunbathTimer || 0) > 0.04 || forcedBehaviorQueueOf(tarinai).length > 0;
return urgent ? (0.55 + jitter * 0.25) : (1.0 + jitter);
}
function shouldForceNeedRecalc(tarinai, world) {
if (!tarinai?.needRaw) return true;
if (forcedBehaviorQueueOf(tarinai).length > 0) return true;
if ((tarinai?.lastNeedShockBreaker || null)) return true;
if (tarinai?.stuckPushpinId) {
const behavior = tarinai.currentLodgedPinBehavior?.() || null;
if (behavior?.panicOnAttach || (behavior?.damagePerTick || 0) > 0 || (behavior?.damageOnAttach || 0) > 0) return true;
}
if ((tarinai?.hurtTimer || 0) > 0.04 || (tarinai?.defeatedTimer || 0) > 0.04 || (tarinai?.fightTimer || 0) > 0.04) return true;
return false;
}
function updateNeedsCached(tarinai, world, dt = 0, options = {}) {
if (!tarinai) return createDefaultNeeds();
const now = Number(world?.time || 0) || 0;
if (!Number.isFinite(tarinai.nextNeedCalcAt)) {
const firstInterval = tarinaiNeedCalcInterval(tarinai, world);
const offset = stableUnit(tarinai.familyKey || tarinai.id || "tarinai", "need-calc-offset") * firstInterval;
tarinai.nextNeedCalcAt = now + offset;
}
const force = Boolean(options.force || shouldForceNeedRecalc(tarinai, world));
if (force || now >= (tarinai.nextNeedCalcAt || 0)) {
// Low-frequency AI uses a fixed needs step. This intentionally avoids
// carrying accumulated elapsed time into need arithmetic; the player only
// perceives the resulting trend, not sub-second precision.
const urgent = (tarinai?.fightTimer || 0) > 0.04 || (tarinai?.birthRitualTimer || 0) > 0.04 || (tarinai?.eatTimer || 0) > 0.04 || (tarinai?.sunbathTimer || 0) > 0.04 || forcedBehaviorQueueOf(tarinai).length > 0;
const calcDt = Number(options.fixedNeedDt) || (urgent ? 0.50 : 1.00);
const result = updateNeeds(tarinai, world, calcDt);
tarinai.nextNeedCalcAt = now + tarinaiNeedCalcInterval(tarinai, world);
return result;
}
return tarinai.needRaw || tarinai.needs || createDefaultNeeds();
}
function weightedPickAction(actions) {
const total = actions.reduce((sum, action) => sum + Math.max(0.01, Number(action.weight) || 1), 0);
let roll = Math.random() * total;
for (const action of actions) {
roll -= Math.max(0.01, Number(action.weight) || 1);
if (roll <= 0) return action;
}
return actions[0] || null;
}
function actionSelectionBonus(action, need, tarinai, options = {}) {
if (!action) return 0;
let bonus = 0;
if (need === "social") {
const p = tarinai?.socialReasonParts || {};
const prof = tarinai?.personalityProfile?.() || {};
const cur = tarinai?.currentPersonality || {};
const aggression = Number(cur.aggression ?? prof.fight ?? 0) || 0;
const sociability = Number(cur.sociability ?? prof.social ?? 0) || 0;
const openness = Number(cur.openness ?? prof.play ?? 0) || 0;
if (action.subNeed === "mate") bonus += Number(p.mate || 0) * 2.2 + Math.max(0, openness) * 22;
else if (action.subNeed === "conflict") bonus += Number(p.conflict || 0) * 1.10 + Math.max(0, aggression) * 14;
else if (action.subNeed === "family") bonus += Number(p.family || 0) * 1.6 + Math.max(0, sociability) * 10;
else if (action.subNeed === "bond") bonus += Number(p.bond || 0) * 1.15 + Math.max(0, sociability) * 24;
} else if (need === "fulfill") {
const p = tarinai?.fulfillReasonParts || {};
if (action.id === "build_grass_bed") bonus += Number(p.bed || 0) * 1.6 + Number(p.material || 0);
else if (action.id === "build_plushie") bonus += Number(p.plushie || 0) * 1.8 + Number(p.material || 0);
else if (action.id === "play" || action.id === "play_seesaw" || action.id === "wander_lightly") bonus += Number(p.boredom || 0) * 1.2;
else if (options.includeComfortActions && (action.id === "return_owned_structure" || action.id === "use_plushie")) {
bonus += Math.max(0, -Number(p.bedComfort || 0) - Number(p.plushieComfort || 0)) * 0.5;
}
}
return bonus;
}
function socialFightMateActionWeights(tarinai) {
const parts = tarinai?.socialReasonParts || {};
const prof = tarinai?.personalityProfile?.() || {};
const cur = tarinai?.currentPersonality || {};
const aggression = Number(cur.aggression ?? prof.fight ?? 0) || 0;
const openness = Number(cur.openness ?? prof.play ?? 0) || 0;
const sociability = Number(cur.sociability ?? prof.social ?? 0) || 0;
let fightWeight = 4;
let mateWeight = 6;
fightWeight *= clamp(1 + aggression * 0.55, 0.25, 2.8);
mateWeight *= clamp(1 + openness * 0.34 + sociability * 0.20, 0.34, 2.4);
fightWeight *= 1 + Math.min(1.9, Math.max(0, Number(parts.conflict || 0) || 0) / 34);
mateWeight *= 1 + Math.min(1.9, Math.max(0, Number(parts.mate || 0) || 0) / 34);
fightWeight *= clamp(1 - Math.max(0, Number(parts.bond || 0) || 0) / 180, 0.55, 1.04);
if ((tarinai?.fightMochiTimer || 0) > 0.04) { fightWeight *= 3.4; mateWeight *= 0.55; }
if ((tarinai?.loveMochiTimer || 0) > 0.04) { mateWeight *= 3.15; fightWeight *= 0.58; }
if (parts.championMate) { mateWeight *= 4.0; fightWeight *= 0.35; }
const total = Math.max(0, fightWeight) + Math.max(0, mateWeight);
return { fightWeight, mateWeight, fightProbability: total > 0 ? fightWeight / total : 0.4 };
}
// regression guard baseline: return ({ conflict: 34, mate: 24, family: 12, bond: 14
// regression guard baseline: subValue >= threshold && subValue >= socialPull * 0.82
function socialSubNeedStartThreshold(subNeed = "") {
return ({ conflict: 18, mate: 24, family: 12, bond: 14, fearSocial: 18 })[subNeed] ?? 16;
}
function shouldStartActionBySubNeed(tarinai, action, needs) {
const need = action?.need || "fulfill";
if (action?.id === "seek_comfort_temperature") {
const status = tarinai?.temperatureStatus || tarinai?.world?.temperatureStatusFor?.(tarinai?.feltTemperature ?? tarinai?.tempComfort ?? (CONFIG.standardTemperature ?? 15), tarinai);
return Boolean(status && (status.discomfort || 0) >= 2.5);
}
if (action?.id === "play_seesaw") {
return Boolean(globalThis.TarinaiSeesawSystem?.shouldSeek?.(tarinai?.world, tarinai, needs));
}
if (need !== "social" || !action?.subNeed) return shouldStartNeedAction(tarinai, need, needs);
const parts = tarinai?.socialReasonParts || {};
const subNeed = action.subNeed;
const subValue = Number(parts[subNeed] || 0) || 0;
const total = Number(needs?.social || 0) || 0;
const prof = tarinai?.personalityProfile?.() || {};
const cur = tarinai?.currentPersonality || {};
const modifier = subNeed === "conflict" ? Math.max(0, Number(cur.aggression ?? prof.fight ?? 0) || 0) * 8
: subNeed === "bond" || subNeed === "family" ? Math.max(0, Number(cur.sociability ?? prof.social ?? 0) || 0) * 7
: subNeed === "mate" ? Math.max(0, Number(cur.openness ?? prof.play ?? 0) || 0) * 7
: 0;
const threshold = Math.max(6, socialSubNeedStartThreshold(subNeed) - modifier);
const isContinuingSameAction = (getTarinaiBehaviorId(tarinai)) === action.id;
if (subNeed === "mate" || subNeed === "conflict") {
const relationDrive = Math.max(Number(parts.mate || 0) || 0, Number(parts.conflict || 0) || 0, total * 0.36);
const drugBoosted = (tarinai?.loveMochiTimer || 0) > 0.04 || (tarinai?.fightMochiTimer || 0) > 0.04;
return total >= needThreshold("social", "start") && (drugBoosted || relationDrive >= Math.min(socialSubNeedStartThreshold("mate"), socialSubNeedStartThreshold("conflict")) * 0.48);
}
if (subValue >= threshold) return true;
if (isContinuingSameAction && subValue >= threshold * 0.55) return true;
return total >= needThreshold("social", "start") && subValue >= threshold * 0.36;
}
function conflictNeedsForChoice(selectedAction, candidates, needs) {
const chosenNeed = selectedAction?.need || "fulfill";
const chosenPriority = actionPriority(selectedAction);
const chosenNeedValue = Number(needs?.[chosenNeed] || 0) || 0;
const result = [];
const add = key => { if (key && key !== chosenNeed && !result.includes(key)) result.push(key); };
for (const c of candidates || []) {
const need = c.action?.need || "fulfill";
const value = Number(needs?.[need] || 0) || 0;
if (need === chosenNeed || value <= 0.5) continue;
if (c.priority > chosenPriority && value >= needThreshold(need, "start")) add(need);
else if (value >= 82 && value >= chosenNeedValue - 8) add(need);
}
return result;
}
function eligiblePriorityActions(tarinai, world, needs) {
const result = [];
for (const action of TARINAI_ACTIONS) {
if (!action || action.id === "birth_ritual") continue;
if ((Number(tarinai?.actionIdCooldowns?.[action.id] || 0) || 0) > 0.01) continue;
const need = action.need || "fulfill";
if (!shouldStartActionBySubNeed(tarinai, action, needs)) continue;
if (!canStartTarinaiAction(action, tarinai, world)) continue;
result.push(action);
}
return result;
}
function choosePriorityNeedAction(needs, tarinai, world) {
const actions = eligiblePriorityActions(tarinai, world, needs);
if (!actions.length) return null;
const weighted = actions.map(action => {
const need = action.need || "fulfill";
const bonus = actionSelectionBonus(action, need, tarinai);
let priority = actionPriority(action);
const needValue = Number(needs?.[need] || 0) || 0;
if (action.id === "seek_comfort_temperature") {
const felt = tarinai?.world?.feltTemperatureFor?.(tarinai) ?? tarinai?.world?.temperatureAt?.(tarinai?.x, tarinai?.y, tarinai) ?? tarinai?.feltTemperature ?? tarinai?.tempComfort ?? (CONFIG.standardTemperature ?? 15);
const status = tarinai?.world?.temperatureStatusFor?.(felt, tarinai) || tarinai?.temperatureStatus || null;
priority = (status?.harmful || (status?.harmExcess || 0) > 0) ? 92 : 53;
}
if (action.id === "play_seesaw") {
priority = Math.max(priority, Number(globalThis.TarinaiSeesawSystem?.actionPriority?.(world, tarinai)) || 0);
}
if (action.id === "eat_food") {
const hunger = Number(tarinai?.hunger || 0) || 0;
if (hunger >= 98 || needValue >= 88) priority = Math.max(priority, 120);
else if (hunger >= 88 || needValue >= 78) priority = Math.max(priority, 98);
else if (hunger >= 78 || needValue >= 70) priority = Math.max(priority, 82);
}
const urgency = Math.max(0, needValue - needThreshold(need, "start"));
return { action, priority, need, needValue, urgency, bonus, score: priority * 1000 + needValue * 3 + (Number(action.weight || 1) + bonus) };
});
weighted.sort((a, b) => (b.priority - a.priority) || (b.score - a.score));
let selectedEntry = weighted[0];
// \u539f\u5247\u306f\u884c\u52d5\u512a\u5148\u9806\u4f4d\u3002\u305f\u3060\u3057\u3001\u4f4e\u4f4d\u6b32\u6c42\u304c\u5371\u6a5f\u7684\u306b\u9ad8\u3044\u5834\u5408\u3060\u3051\u3001\u305d\u306e\u6b32\u6c42\u884c\u52d5\u3092\u9078\u3073\u3001
// \u8868\u793a\u6587\u306f\u300c\u7720\u305f\u3044\u3051\u3069\u3001\u98df\u3079\u7269\u3092\u63a2\u3057\u3066\u3044\u308b\u3002\u300d\u306e\u3088\u3046\u306b\u5bfe\u7acb\u3092\u660e\u793a\u3059\u308b\u3002
const crisis = weighted
.filter(e => e.needValue >= 88 && e.urgency >= 24 && ((selectedEntry.priority - e.priority) <= 36 || e.action?.id === "eat_food"))
.sort((a, b) => (b.needValue - a.needValue) || (b.urgency - a.urgency) || (b.score - a.score))[0] || null;
if (crisis && crisis.needValue >= selectedEntry.needValue + 16) selectedEntry = crisis;
const samePriority = weighted.filter(e => e.priority === selectedEntry.priority);
let action = selectedEntry.action;
const sameSocial = samePriority.filter(e => e.need === "social");
const normalFightEntry = sameSocial.find(e => e.action?.id === "fight_rival");
const normalMateEntry = sameSocial.find(e => e.action?.id === "approach_mate");
if (!isCurrentBehaviorForced(tarinai) && normalFightEntry && normalMateEntry && samePriority.includes(selectedEntry)) {
const weights = socialFightMateActionWeights(tarinai);
const bucket = Math.floor((Number(world?.time || 0) || 0) / 3);
const pickFight = deterministicChance(world, "social-fight-vs-mate-weighted", weights.fightProbability, tarinai, normalFightEntry.action.id, normalMateEntry.action.id, bucket);
selectedEntry = pickFight ? normalFightEntry : normalMateEntry;
action = selectedEntry.action;
} else if (samePriority.length > 1 && samePriority.includes(selectedEntry)) {
const relationSameBand = samePriority.some(e => e.need === "social")
? samePriority.filter(e => e.need === "social")
: samePriority;
const pool = relationSameBand.length > 1 ? relationSameBand : samePriority;
const top = pool.map(e => ({
...e.action,
weight: Math.max(0.01, (Number(e.action.weight) || 1) + (e.score - e.priority * 1000) * 0.02),
}));
const picked = weightedPickAction(top) || selectedEntry.action;
action = TARINAI_ACTIONS.find(a => a.id === picked.id) || picked;
selectedEntry = weighted.find(e => e.action.id === action.id) || selectedEntry;
}
const need = action.need || "fulfill";
const conflictNeeds = conflictNeedsForChoice(action, weighted, needs);
return { action, choice: { need, tiedNeeds: [need, ...conflictNeeds], max: Number(needs?.[need] || 0) || 0, priority: selectedEntry.priority } };
}
function shouldStartNeedAction(tarinai, need, needs) {
const value = Number(needs?.[need]) || 0;
if (value <= 0.5) return false;
if (need === "food") {
const now = tarinai?.world?.time || 0;
const inMealCooldown = (tarinai?.mealCooldownUntil || 0) > now;
const hunger = Number(tarinai?.hunger || 0) || 0;
if (inMealCooldown && hunger < 82 && value < 78 && !(isCurrentBehaviorForced(tarinai))) return false;
}
if (need === "sleep" && circadianSleepPhase(tarinai?.world).night && value >= Math.max(44, needThreshold("sleep", "start") - 16)) return true;
if (value >= needThreshold(need, "start")) return true;
if ((getTarinaiBehaviorNeed(tarinai)) === need && value >= needThreshold(need, "continue")) return true;
return false;
}
function startNeedAction(tarinai, world, choice, action, reasonText, options = {}) {
const lockSeconds = Number(options.lockSeconds || actionLockSeconds(action, tarinai)) || 1.0;
setBehaviorFromAction(tarinai, action, choice, reasonText, {
...options,
lockSeconds,
minDuration: options.minDuration || Math.max(0.45, lockSeconds * 0.72),
phase: "start",
});
tarinai.behaviorLockTimer = Math.max(tarinai.behaviorLockTimer || 0, lockSeconds);
const ran = startTarinaiAction(action, tarinai, world, options);
if (!ran) {
failTarinaiAction(action, tarinai, world, { phase: "start_failed" });
if (isCurrentBehaviorForced(tarinai)) retryForcedBehavior(tarinai, (currentTarinaiBehavior(tarinai)), "start_failed");
tarinai.actionCooldowns = tarinai.actionCooldowns || {};
tarinai.actionIdCooldowns = tarinai.actionIdCooldowns || {};
tarinai.actionCooldowns[action?.need || choice?.need || "fulfill"] = Math.max(tarinai.actionCooldowns[action?.need || choice?.need || "fulfill"] || 0, 1.4);
tarinai.actionIdCooldowns[action?.id || "unknown"] = Math.max(tarinai.actionIdCooldowns[action?.id || "unknown"] || 0, 2.2);
tarinai.behaviorSwitchCooldownUntil = Math.max(tarinai.behaviorSwitchCooldownUntil || 0, (world?.time || 0) + 0.55);
clearBehavior(tarinai, getTarinaiBehaviorText(tarinai) || action?.label || "\u5f85\u3063\u3066\u3044\u308b");
tarinai.goIdle?.(action?.label || "\u5f85\u3063\u3066\u3044\u308b");
} else {
const target = tarinai.target || options.target;
setBehaviorFromAction(tarinai, action, choice, reasonText, {
...options,
target,
state: tarinai.state || action?.state || "idle",
lockSeconds,
minDuration: (currentTarinaiBehavior(tarinai))?.minDuration || Math.max(0.45, lockSeconds * 0.72),
phase: "active",
});
}
tarinai.thought = reasonText;
return Boolean(ran);
}
function continueCurrentBehavior(tarinai, world, needs, dt) {
const behavior = currentTarinaiBehavior(tarinai);
const actionId = behavior?.actionId;
if (!actionId) return false;
const action = actionById(actionId);
if (!action) return false;
if (tarinai.target && tarinai.target.dead) return false;
const timerBacked = Boolean(action.timerBacked || ["eat_food", "drink_water", "use_medicine", "fight_rival", "intimidate_enemy", "approach_mate", "birth_ritual", "panic_escape", "sunbath"].includes(action.id));
if (!timerBacked && !canStartTarinaiAction(action, tarinai, world)) return false;
const need = action.need || behavior?.need;
const value = Number(needs?.[need]) || 0;
const now = world?.time || 0;
const startedAt = Number(behavior?.startedAt ?? now) || now;
const minDuration = Number(behavior?.minDuration || actionLockSeconds(action, tarinai) * 0.72) || 0.8;
const elapsed = Math.max(0, now - startedAt);
const mustContinueByTimer = (action.id === "fight_rival" && (tarinai.fightTimer || 0) > 0.04)
|| (action.id === "birth_ritual" && (tarinai.birthRitualTimer || 0) > 0.04)
|| (action.id === "panic_escape" && (tarinai.fearTimer || 0) > 0.04 && tarinai.state === "panic")
|| (action.id === "intimidate_enemy" && (tarinai.intimidateTimer || 0) > 0.04)
|| (action.id === "sunbath" && (tarinai.sunbathTimer || 0) > 0.04)
|| (action.id === "play_seesaw" && Boolean(globalThis.TarinaiSeesawSystem?.validReservation?.(tarinai, world)))
|| ((action.id === "eat_food" || action.id === "drink_water" || action.id === "use_medicine") && (tarinai.eatTimer || 0) > 0.04 && elapsed < 1.15);
const foodSuppressed = need === "food" && !isCurrentBehaviorForced(tarinai) && (tarinai.mealCooldownUntil || 0) > now && (tarinai.hunger || 0) < 82 && value < 78 && elapsed >= Math.min(minDuration, 1.15);
if (!foodSuppressed && (elapsed < minDuration || value >= needThreshold(need, "continue") || mustContinueByTimer || isCurrentBehaviorForced(tarinai))) {
tarinai.behaviorLockTimer = Math.max(tarinai.behaviorLockTimer || 0, Math.min(0.9, Math.max(0.35, minDuration - elapsed)));
const refreshedReason = refreshBehaviorReasonText(tarinai, action);
const ranUpdate = tickTarinaiAction(action, tarinai, world, dt, needs, { phase: "continue" });
if (ranUpdate === "finished") {
finishTarinaiAction(action, tarinai, world, { phase: "finished" });
const finishedBehavior = behavior;
if (behaviorIsForced(finishedBehavior)) completeForcedBehavior(tarinai, finishedBehavior, "completed");
tarinai.actionCooldowns = tarinai.actionCooldowns || {};
tarinai.actionCooldowns[need] = Math.max(tarinai.actionCooldowns[need] || 0, 1.6);
clearBehavior(tarinai, refreshedReason || action.label || "");
return false;
}
if (ranUpdate === false) {
failTarinaiAction(action, tarinai, world, { phase: "update_failed" });
if (behaviorIsForced(behavior)) retryForcedBehavior(tarinai, behavior, "update_failed");
tarinai.actionCooldowns = tarinai.actionCooldowns || {};
tarinai.actionIdCooldowns = tarinai.actionIdCooldowns || {};
const consumableAction = action.id === "eat_food" || action.id === "drink_water" || action.id === "use_medicine";
if (consumableAction) {
// \u98DF\u6599\u30FB\u6C34\u30FB\u85AC\u306F\u74B0\u5883\u5909\u5316\u304C\u901F\u3044\u306E\u3067\u3001\u5931\u6557\u6642\u3082\u77ED\u3044\u518D\u8A55\u4FA1\u3060\u3051\u306B\u3059\u308B\u3002
tarinai.actionCooldowns[need] = Math.max(tarinai.actionCooldowns[need] || 0, 0.35);
tarinai.actionIdCooldowns[action.id] = Math.max(tarinai.actionIdCooldowns[action.id] || 0, 0.45);
tarinai.behaviorSwitchCooldownUntil = Math.max(tarinai.behaviorSwitchCooldownUntil || 0, (world?.time || 0) + 0.18);
tarinai.target = null;
tarinai.targetKey = "";
} else {
tarinai.actionCooldowns[need] = Math.max(tarinai.actionCooldowns[need] || 0, 1.6);
tarinai.actionIdCooldowns[action.id] = Math.max(tarinai.actionIdCooldowns[action.id] || 0, 2.4);
tarinai.behaviorSwitchCooldownUntil = Math.max(tarinai.behaviorSwitchCooldownUntil || 0, (world?.time || 0) + 0.55);
}
clearBehavior(tarinai);
return false;
}
setBehaviorFromAction(tarinai, action, { need, tiedNeeds: behavior?.tiedNeeds || [need] }, refreshedReason || behavior?.reason || action.label, {
target: tarinai.target,
state: tarinai.state || action.state || "idle",
source: behavior?.source || "need",
forced: behaviorIsForced(behavior),
priority: behavior?.priority || 0,
causeText: (behaviorIsForced(behavior) || !/\u306E\u52B9\u679C$/.test(String(behavior?.causeText || "").trim().replace(/[\u3002\uFF01\uFF1F]+$/, ""))) ? (behavior?.causeText || "") : "",
sourceReasonText: behavior?.sourceReasonText || "",
forcedReasonText: behavior?.sourceReasonText || "",
forcedRequest: behavior?.forcedRequest || null,
startedAt,
lockSeconds: behavior?.lockSeconds,
minDuration,
phase: mustContinueByTimer ? "perform" : "active",
});
tarinai.thought = behaviorText(tarinai) || refreshedReason || behavior?.label || tarinai.thought;
const needsRefresh = !tarinai.target || tarinai.target.dead || ["rest_to_recover", "use_plushie", "intimidate_enemy"].includes(action.id);
if (needsRefresh && elapsed >= Math.min(0.7, minDuration)) {
startTarinaiAction(action, tarinai, world, { phase: "refresh" });
}
return true;
}
tarinai.actionCooldowns = tarinai.actionCooldowns || {};
tarinai.actionCooldowns[need] = Math.max(tarinai.actionCooldowns[need] || 0, 2.8);
tarinai.behaviorSwitchCooldownUntil = Math.max(tarinai.behaviorSwitchCooldownUntil || 0, (world?.time || 0) + 0.35);
clearBehavior(tarinai);
return false;
}
function decayActionCooldowns(tarinai, dt = 0) {
if (!tarinai?.actionCooldowns) return;
const step = Math.max(0, Number(dt) || 0);
if (step <= 0) return;
for (const key of Object.keys(tarinai.actionCooldowns)) tarinai.actionCooldowns[key] = Math.max(0, (Number(tarinai.actionCooldowns[key]) || 0) - step);
if (tarinai.actionIdCooldowns) {
for (const key of Object.keys(tarinai.actionIdCooldowns)) tarinai.actionIdCooldowns[key] = Math.max(0, (Number(tarinai.actionIdCooldowns[key]) || 0) - step);
}
}
function shouldWakeFromSleep(tarinai, world, needs) {
if (!tarinai || tarinai.dead) return true;
if (tarinai.sleepDisease) return false;
const now = world?.time || 0;
const session = tarinai.sleepSession || { startedAt: now, minDuration: 8, targetEnergy: 74, maxDuration: 28 };
const started = Number(session.startedAt);
const elapsed = Math.max(0, now - (Number.isFinite(started) ? started : now));
const minSleepDuration = Math.max(2, Number(session.minDuration) || 8);
// \u660e\u78ba\u306a\u59a8\u5bb3\u3060\u3051\u306f\u6700\u4f4e\u7761\u7720\u6642\u9593\u3088\u308a\u512a\u5148\u3057\u3066\u8d77\u5e8a\u3055\u305b\u308b\u3002
if ((tarinai.hurtTimer || 0) > 0.06 || (tarinai.pokeFlashTimer || 0) > 0.08) return true;
if (findNearbyDanger(world, tarinai, 160)) return true;
// \u7a7a\u8179\u30fb\u7761\u7720\u6b32\u306e\u56de\u5fa9\u30fb\u4f53\u529b\u56de\u5fa9\u306a\u3069\u306e\u901a\u5e38\u8981\u56e0\u3067\u306f\u3001\u6700\u4f4e2\u6642\u9593\u306f\u5bdd\u7d9a\u3051\u308b\u3002
if (elapsed < minSleepDuration) return false;
const foodNeed = Number(needs?.food ?? tarinai.needRaw?.food ?? tarinai.needs?.food ?? 0) || 0;
if ((tarinai.hunger || 0) >= 84 || foodNeed >= needThreshold("food", "start")) return true;
const sleepNeed = Number(needs?.sleep ?? tarinai.needRaw?.sleep ?? tarinai.needs?.sleep ?? 0) || 0;
const sleepContinue = (typeof needThreshold === "function" ? needThreshold("sleep", "continue") : 34);
const restedEnough = sleepNeed <= sleepContinue + 4;
if (restedEnough) return true;
// Energy can recover faster than the explicit sleep need, especially inside
// nest containers. Do not wake and immediately re-enter while the individual
// is still classified as sleepy.
if ((tarinai.energy || 0) >= (Number(session.targetEnergy) || 74) && sleepNeed <= sleepContinue + 14) return true;
if (elapsed >= (Number(session.maxDuration) || 28) && sleepNeed <= sleepContinue + 18) return true;
return false;
}
function protectSleepSession(tarinai, world, needs) {
if (!(tarinai?.state === "sleep" || tarinai?.sleeping)) return false;
tarinai.sleeping = true;
if (!tarinai.sleepSession) {
const now = world?.time || 0;
const bedLike = tarinai.target && typeof tarinai.isSleepFurniture === "function" && tarinai.isSleepFurniture(tarinai.target);
tarinai.sleepSession = { startedAt: now, minDuration: rand(bedLike ? 8 : 7, bedLike ? 14 : 12), targetEnergy: rand(bedLike ? 70 : 64, bedLike ? 82 : 76), maxDuration: rand(bedLike ? 22 : 18, bedLike ? 34 : 28), targetId: tarinai.target?.id || null, consumesGrassBedOnWake: tarinai.target?.type === "grass_bed" };
if (tarinai.target?.type === "grass_bed") tarinai.pendingGrassBedWakeId = tarinai.target.id || tarinai.pendingGrassBedWakeId || "";
}
if (!shouldWakeFromSleep(tarinai, world, needs)) {
const step = Math.max(0.10, Math.min(1.0, Number(tarinai.aiTimer || 0.42) || 0.42));
const bedLike = tarinai.target && typeof tarinai.isSleepFurniture === "function" && tarinai.isSleepFurniture(tarinai.target);
const comfort = bedLike ? Math.max(0.5, Math.min(1.4, Number(world?.bedComfort?.(tarinai.target) || tarinai.target?.comfort || 1) || 1)) : 0.55;
const energyMax = Math.max(1, typeof tarinaiMaxEnergy === "function" ? tarinaiMaxEnergy(tarinai) : (Number(tarinai.maxEnergy) || 100));
tarinai.energy = Math.min(energyMax, (Number(tarinai.energy || 0) || 0) + step * (bedLike ? (1.2 + comfort * 1.35) : 0.65));
if (typeof applyNeedRelief === "function") applyNeedRelief(tarinai, { sleep: -step * (bedLike ? (0.16 + comfort * 0.18) : 0.18), safety: bedLike ? -step * 0.9 : 0 });
tarinai.thought = tarinai.thought || "\u7720\u3063\u3066\u3044\u308b";
setBehavior(tarinai, { id: "sleep", need: "sleep", label: "\u7720\u3063\u3066\u3044\u308b", reason: "\u306d\u3080\u3044\u306e\u3067\u3001\u7720\u3063\u3066\u3044\u308b\u3002", text: "\u7720\u3063\u3066\u3044\u308b", state: "sleep", tiedNeeds: ["sleep"], startedAt: tarinai.sleepSession.startedAt, minDuration: tarinai.sleepSession.minDuration, lockSeconds: tarinai.sleepSession.minDuration, phase: "perform" });
tarinai.behaviorLockTimer = Math.max(tarinai.behaviorLockTimer || 0, 0.8);
const now = world?.time || 0;
if (now >= (tarinai.nextSleepBubbleAt || 0)) {
tarinai.nextSleepBubbleAt = now + rand(3.6, 5.6);
world?.spawnBubble?.(tarinai.x, tarinai.y - (tarinai.radius || 20) * 1.18, "Zzz...", "rgba(65,70,92,0.72)");
}
return true;
}
const sleptLongEnough = (world?.time || 0) - (Number(tarinai.sleepSession?.startedAt) || 0) >= 2;
if (sleptLongEnough) tarinai.consumePendingGrassBedOnWake?.("\u76ee\u304c\u899a\u3081\u305f");
tarinai.sleeping = false;
tarinai.sleepSession = null;
clearBehavior(tarinai);
if (sleptLongEnough) applyNeedSatisfaction(tarinai, { sleep: 28, safety: 6 }, "sleep");
tarinai.goIdle?.("\u76ee\u304c\u899a\u3081\u305f");
return false;
}
function resolveNeedsCore(dt) {
decayActionCooldowns(this, dt);
const beforeNeeds = this.needs ? { ...this.needs } : createDefaultNeeds();
const needs = updateNeedsCached(this, this.world, dt);
synchronizeActionTextFromState(this);
if (this.state === "seek_toilet" || this.toiletUrgeTargetId) {
this.toiletUrgeTargetId = "";
this.toiletUrgeStartedAt = 0;
this.toiletUrgeDeadline = 0;
if (this.state === "seek_toilet") {
this.target = null;
this.behaviorLockTimer = 0;
this.goIdle?.("\u5f85\u3063\u3066\u3044\u308b");
}
}
if (processForcedBehaviorQueue(this, this.world, needs)) return;
if (protectSleepSession(this, this.world, needs)) return;
if (continueBuildPlan(this, this.world, dt)) return;
const lockedTargetInvalid = this.target && this.target.dead;
const lockedBehavior = currentTarinaiBehavior(this);
if ((this.behaviorLockTimer || 0) > 0 && lockedBehavior && !lockedTargetInvalid && !["panic", "intimidate", "fight", "birth_ritual"].includes(this.state)) {
if (continueCurrentBehavior(this, this.world, needs, dt)) return;
this.thought = lockedBehavior.reason || lockedBehavior.label || this.thought;
return;
}
if (this.birthRitualTimer > 0.04) {
const partner = this.world.liveTarinaiById?.(this.birthPartnerId) || null;
this.setActionState?.("birth_ritual", { target: partner, reason: "\u3078\u3053\u3078\u3053\u3057\u3066\u3044\u308b" });
synchronizeActionTextFromState(this);
return;
}
if (this.intimidateTimer > 0.04) {
const target = this.world.liveTarinaiById?.(this.intimidateTargetId) || null;
this.setActionState?.("intimidate", { target, reason: "\u76f8\u624b\u3092\u5a01\u5687\u3057\u3066\u3044\u308b" });
setBehaviorText(this, { need: "social", subNeed: "conflict", actionId: "intimidate_enemy", actionLabel: "\u5a01\u5687\u3057\u3066\u3044\u308b", reason: target?.name ? `${target.name}\u3092\u5a01\u5687\u3057\u3066\u3044\u308b` : "\u76f8\u624b\u3092\u5a01\u5687\u3057\u3066\u3044\u308b", target, phase: "perform", source: "behavior" });
this.bubble("!", 1.2);
return;
}
if (this.fightTimer > 0.04) {
this.fightTargetIds = (this.fightTargetIds || []).filter(id => Boolean(this.world.liveTarinaiById?.(id)));
this.fightTargetId = this.fightTargetIds[0] || this.fightTargetId;
let best = null, bestD = Infinity;
for (const id of this.fightTargetIds) {
const t = this.world.liveTarinaiById?.(id);
if (!t) continue;
const d = dist(this, t);
if (d < bestD) { best = t; bestD = d; }
}
const maxFightContinueDistance = Math.max(150, (this.radius || 20) + (best?.radius || 20) + 116);
if (!best || bestD > maxFightContinueDistance) {
const rival = best || (this.fightTargetId ? this.world.liveTarinaiById?.(this.fightTargetId) : null);
this.fightTimer = 0;
this.fightTargetIds = [];
this.fightTargetId = null;
this.counterAttackFromId = null;
this.fightCooldown = Math.max(this.fightCooldown || 0, CONFIG.fightCooldown + deterministicRange(this.world, "fight-lost-target-cooldown", 4, 8, this, rival));
if (rival) this.world?.markFightPairCooldown?.(this, rival, CONFIG.fightCooldown + 6);
clearForcedBehaviorQueue(this, e => e && e.id === "fight_rival");
this.setActionState?.("idle", { target: null, reason: "\u76f8\u624b\u3092\u898b\u5931\u3063\u305f\u3002", sleeping: false, clearTarget: true });
return;
}
const reasonText = best ? `${best.name || "\u76f8\u624b"}\u304c\u6c17\u306b\u5165\u3089\u306a\u3044\u306e\u3067\u3001\u55a7\u5629\u3057\u3066\u3044\u308b\u3002` : "\u6c17\u306b\u5165\u3089\u306a\u3044\u76f8\u624b\u304c\u3044\u308b\u306e\u3067\u3001\u55a7\u5629\u3057\u3066\u3044\u308b\u3002";
this.setActionState?.("fight", { target: best, reason: reasonText });
setBehaviorText(this, { need: "social", subNeed: "conflict", actionId: "fight_rival", actionLabel: "\u55a7\u5629\u3057\u3066\u3044\u308b", reasonText, target: best, phase: "perform", source: "behavior" });
this.bubble("!", 1.6);
return;
}
if (this.defeatedTimer > 0.04) {
const opponent = this.world.liveTarinaiById?.(this.defeatedById);
applyNeedShock(this, { safety: 54, health: 16 }, opponent || null);
startNeedDrivenEmergencyReaction(this, opponent || null, "\u55a7\u5629\u306b\u8ca0\u3051\u3066\u9003\u3052\u3066\u3044\u308b\u3002");
if (!opponent && this.defeatedTimer < 0.12) this.defeatedById = null;
return;
} else if (this.defeatedById && this.fightTimer <= 0.04) {
this.defeatedById = null;
this.fightWinnerId = null;
}
if (this.stuckPushpinId) {
const pin = this.currentLodgedPin?.() || null;
const pinBehavior = this.currentLodgedPinBehavior?.() || null;
if (pin && pinBehavior?.panicOnAttach) {
this.needShock = this.needShock || {};
this.needShock.safety = Math.max(this.needShock.safety || 0, 42);
this.needShock.health = Math.max(this.needShock.health || 0, 46);
this.lastNeedShockBreaker = pin;
this.hurtTimer = Math.max(this.hurtTimer || 0, 0.24);
this.awakeLockTimer = Math.max(this.awakeLockTimer || 0, 1.8);
const now = this.world?.time || 0;
if (now >= (this.nextPinPanicBubbleAt || 0)) {
this.bubble("!!", 0.9, "rgba(168,72,72,0.82)");
this.nextPinPanicBubbleAt = now + 1.6;
}
this.thought = "\u753b\u92f2\u304c\u523a\u3055\u3063\u3066\u30d1\u30cb\u30c3\u30af\u306b\u306a\u3063\u3066\u3044\u308b";
}
if (!pin || pin.pinState !== "lodged") this.stuckPushpinId = null;
}
const breaker = this.lastNeedShockBreaker || null;
const safetyDelta = (this.needs?.safety || 0) - (beforeNeeds.safety || 0);
const healthDelta = (this.needs?.health || 0) - (beforeNeeds.health || 0);
const fulfillDelta = (this.needs?.fulfill || 0) - (beforeNeeds.fulfill || 0);
const shockTriggered = safetyDelta >= 40 || healthDelta >= 45 || fulfillDelta >= 50;
if (breaker && shockTriggered && !["panic", "intimidate", "fight", "birth_ritual"].includes(this.state)) {
if (startNeedDrivenEmergencyReaction(this, breaker, "\u30d1\u30cb\u30c3\u30af\u306b\u306a\u3063\u3066\u3044\u308b\u3002")) return;
clearTarinaiBehavior(this, { reason: this.thought });
this.lastNeedShockBreaker = null;
return;
}
if (continueCurrentBehavior(this, this.world, needs, dt)) return;
if ((this.behaviorSwitchCooldownUntil || 0) > (this.world?.time || 0) && !this.hasForcedBehavior?.()) {
this.behaviorLockTimer = Math.max(this.behaviorLockTimer || 0, 0.25);
if (!(currentTarinaiBehavior(this)) && !["panic", "fight", "birth_ritual", "intimidate", "sleep"].includes(this.state)) this.setActionState?.("idle", { target: null, reason: this.thought || "\u5c11\u3057\u69d8\u5b50\u3092\u898b\u3066\u3044\u308b", sleeping: false, clearTarget: true });
return;
}
const foodNeedForLeisure = Number(needs?.food || 0) || 0;
const safetyNeedForLeisure = Number(needs?.safety || 0) || 0;
const sleepNeedForLeisure = Number(needs?.sleep || 0) || 0;
const sunbathActionForLeisure = actionById("sunbath");
if (sunbathActionForLeisure
&& this.canStartSunbath?.({ leisure: true })
&& foodNeedForLeisure < 58
&& safetyNeedForLeisure < 50
&& sleepNeedForLeisure < 68
&& Math.random() < Math.max(0.010, dt * 0.055)) {
const choice = { need: "fulfill", tiedNeeds: ["fulfill"], max: Number(needs?.fulfill || 0) || 0, priority: actionPriority(sunbathActionForLeisure) };
startNeedAction(this, this.world, choice, sunbathActionForLeisure, "\u4f59\u88d5\u304c\u3042\u308b\u306e\u3067\u3001\u65e5\u5149\u6d74\u3057\u3066\u3044\u308b\u3002", { leisure: true, priority: actionPriority(sunbathActionForLeisure), lockSeconds: 6.0 });
return;
}
const picked = choosePriorityNeedAction(needs, this, this.world);
if (!picked?.action) {
const sunbathAction = actionById("sunbath");
if (sunbathAction && this.canStartSunbath?.({ leisure: true }) && Math.random() < Math.max(0.006, dt * 0.08)) {
const choice = { need: "fulfill", tiedNeeds: ["fulfill"], max: Number(needs?.fulfill || 0) || 0, priority: actionPriority(sunbathAction) };
startNeedAction(this, this.world, choice, sunbathAction, "\u4f59\u88d5\u304c\u3042\u308b\u306e\u3067\u3001\u65e5\u5149\u6d74\u3057\u3066\u3044\u308b\u3002", { leisure: true, priority: actionPriority(sunbathAction), lockSeconds: 6.0 });
return;
}
const choice = chooseTopNeedRandom(needs, this, this.world);
setBehavior(this, { id: "idle", need: choice.need, tiedNeeds: [...(choice.tiedNeeds || [])], label: "\u5f85\u3063\u3066\u3044\u308b", reason: "\u5c11\u3057\u843d\u3061\u7740\u3044\u3066\u3044\u308b\u3002", state: "idle", phase: "idle", startedAt: this.world?.time || 0 });
clearBehavior(this);
this.behaviorLockTimer = Math.max(this.behaviorLockTimer || 0, 3.0);
this.goIdle?.("\u5c11\u3057\u843d\u3061\u7740\u3044\u3066\u3044\u308b");
return;
}
const action = picked.action;
const choice = picked.choice;
const reasonText = buildReasonText(choice.need, choice.tiedNeeds, action, this, this.world);
startNeedAction(this, this.world, choice, action, reasonText, { priority: choice.priority ?? actionPriority(action) });
return;
}
(function (global) {
const Tarinai = global.Tarinai;
if (!Tarinai) throw new Error("Tarinai is not available for mixin: tarinai_needs_items.js");
if (!global.TarinaiNeedPlannerCoreSystem) {
global.TarinaiNeedPlannerCoreSystem = Object.freeze({
updateOne(tarinai, dt = 0) {
if (!tarinai || tarinai.dead) return false;
resolveNeedsCore.call(tarinai, dt);
return true;
},
});
}
Object.defineProperties(Tarinai.prototype, Object.getOwnPropertyDescriptors({
forceBehavior(actionId, options = {}) {
return queueForcedTarinaiBehavior(this, actionId, options);
},
hasForcedBehavior(actionId = "") {
const key = String(actionId || "");
const active = typeof currentForcedBehaviorRequest === "function" ? currentForcedBehaviorRequest(this) : null;
if (active && (!key || active.id === key)) return true;
return forcedBehaviorQueueOf(this).some(entry => entry && (!key || entry.id === key));
},
hasLoveMochiEffect() {
return (this.loveMochiTimer || 0) > 0.04;
},
hasFightMochiEffect() {
return (this.fightMochiTimer || 0) > 0.04;
},
zunchiSlaveSpriteId() {
if (!this.zunchiSlaveSpriteVariant) this.zunchiSlaveSpriteVariant = Math.random() < 0.5 ? "zunchi_slave" : "zunchi_slave_alt";
return this.zunchiSlaveSpriteVariant;
},
becomeZunchiSlave(options = {}) {
if (this.isZunchiSlave) {
this.zunchiSlaveLocked = true;
applyZunchiSlavePersonality(this, { record: false, birth: !!options.birth });
this.visibleSpriteId = this.zunchiSlaveSpriteId();
return false;
}
this.formerName = this.formerName || this.name;
this.isZunchiSlave = true;
this.zunchiSlaveLocked = true;
this.name = "\u305a\u3093\u3061\u3069\u308c\u3044";
this.birthRitualTimer = 0;
this.birthPartnerId = null;
this.birthRitualLeader = false;
this.birthRitualRole = 0;
this.target = null;
this.reproductionTimer = options.birth ? Math.max(0, this.reproductionTimer || 0) : Math.max(this.reproductionTimer || 0, CONFIG.reproductionCooldown + rand(8, 18));
if (!options.birth) this.loveMochiTimer = 0;
this.addStress(options.birth ? 0 : 14, { threshold: 8 });
this.hunger = Math.max(this.hunger, 40);
applyZunchiSlavePersonality(this, { birth: !!options.birth });
this.visibleSpriteId = this.zunchiSlaveSpriteId();
this.spriteLockUntil = 0;
this.recordChangeCause?.("\u305a\u3093\u3061\u3069\u308c\u3044\u5316", "\u72b6\u614b");
return true;
},
rawSpriteId() {
if (this.dead) return this.variantSprite("fear");
if (this.state === "sunbath" || (this.sunbathTimer || 0) > 0.04) return this.sunbathSpriteId ? this.sunbathSpriteId() : "sunbath_1";
if (this.sleepDisease) return this.variantSprite("sleep");
if (this.isZunchiSlave) return this.zunchiSlaveSpriteId();
const low = this.lowHealthSprite();
if (low) return low;
if ((this.state === "seek_food" || this.state === "idle" || this.state === "follow_parent") && (this.vy < -18 || (this.target && this.target.y < this.y - 36 && Math.abs(this.target.y - this.y) > Math.abs(this.target.x - this.x) * 0.55))) return "back";
if (this.explosionDisease && Math.sin((this.world?.time || 0) * 8) > 0.15) return "pokan";
if (this.fightDisease) return "stress_dizzy";
if ((this.oshiriByoZunchiStock || 0) >= 6) return this.variantSprite ? this.variantSprite("hurt") : "weak";
const foodNeedForSprite = Number(this.needRaw?.food ?? this.needs?.food ?? this.hunger) || 0;
const socialNeedForSprite = Number(this.needRaw?.social ?? this.needs?.social ?? 0) || 0;
const fulfillNeedForSprite = Number(this.needRaw?.fulfill ?? this.needs?.fulfill ?? 0) || 0;
if (this.type === "cry" && (this.stress > 78 || foodNeedForSprite > 92 || this.energy < 12 || (this.needs?.safety || 0) >= 90)) return "cry";
if ((this.state === "eat" || this.eatTimer > 0.04) && this.target?.type === "sweet") return "zunda_eat";
if ((this.state === "seek_food" || this.foodReactTimer > 0.04) && this.target?.type === "sweet" && dist(this, this.target) < 80) return "zunda_eat";
if (this.hurtTimer > 0.08 || this.pokeFlashTimer > 0.08) return this.variantSprite("hurt");
if (this.birthRitualTimer > 0.04 || this.state === "birth_ritual") return "birth_ritual";
if (foodNeedForSprite >= 72) return "hungry_70";
if (this.defeatedTimer > 0.04) return this.variantSprite("flee");
if (this.state === "intimidate" || this.state === "ant_intimidate" || this.intimidateTimer > 0.04) return this.variantSprite("intimidate");
if (this.state === "panic" || this.state === "cursor_enemy" || this.fearTimer > 0.08 || this.intimidatedTimer > 0.08) return this.variantSprite("fear");
if (this.state === "fight" || this.state === "seek_enemy" || this.fightTimer > 0.04) return "angry";
if (this.stress > 62) return this.variantSprite("stress");
if (this.state === "sleep" || this.energy < 18) return this.variantSprite("sleep");
if (this.surpriseTimer > 0.08) return "pokan";
if (this.state === "cursor_friend") return this.applyTemperatureSprite?.("smile") || "smile";
if (this.state === "eat" || this.eatTimer > 0.04) return this.goodMode;
if (this.state === "seek_food") return "drool";
const seekTempSprite = (this.state === "seek_temperature") ? (this.temperatureDiscomfortSprite?.(this.goodMode || this.variantSprite("normal")) || "") : "";
if (seekTempSprite) return seekTempSprite;
if (this.stress < 24 && foodNeedForSprite < 60 && socialNeedForSprite < 60 && fulfillNeedForSprite < 60) return this.applyTemperatureSprite?.(this.variantSprite("low_stress")) || this.variantSprite("low_stress");
if (this.stress < 44 && foodNeedForSprite < 60) return this.applyTemperatureSprite?.(this.goodMode) || this.goodMode;
if (foodNeedForSprite > 78 || socialNeedForSprite > 84) return "angry";
return this.applyTemperatureSprite?.(this.goodMode || this.variantSprite("normal")) || this.goodMode || this.variantSprite("normal");
},
spriteId() {
const now = this.world?.time || 0;
const sunbathActive = this.state === "sunbath" || (this.sunbathTimer || 0) > 0.04;
if (sunbathActive) {
this.preloadSunbathSprites?.();
const sunbathId = this.sunbathSpriteId ? this.sunbathSpriteId() : (String(this.sunbathSpriteVariant || "").startsWith("sunbath") ? this.sunbathSpriteVariant : "sunbath_1");
this.visibleSpriteId = sunbathId;
this.spriteLockUntil = 0;
return sunbathId;
}
if (String(this.visibleSpriteId || "").startsWith("sunbath")) {
this.visibleSpriteId = "";
this.spriteLockUntil = 0;
}
const next = this.rawSpriteId();
const isCurrentIntimidator = this.state === "intimidate" || this.state === "ant_intimidate" || this.intimidateTimer > 0.04;
const isCurrentIntimidated = !isCurrentIntimidator && this.intimidatedTimer > 0.04;
const forceImmediate = String(next || "").startsWith("zunchi_slave") || next === "birth_ritual" || next === "intimidate" || isCurrentIntimidator || isCurrentIntimidated || (this.visibleSpriteId === "intimidate" && next !== "intimidate");
if (!this.visibleSpriteId || forceImmediate) {
this.visibleSpriteId = next;
this.spriteLockUntil = forceImmediate ? now : now + 1.0;
return next;
}
if (next !== this.visibleSpriteId && now >= this.spriteLockUntil) {
this.visibleSpriteId = next;
this.spriteLockUntil = now + 1.0;
}
return this.visibleSpriteId;
},
isAwakeCursorFriendly() {
if (this.dead || this.sleepDisease || this.sleeping || this.state === "sleep" || this.state === "seek_bed") return false;
if (["fight", "panic", "cursor_enemy", "hurt", "defeated", "fight_sick"].includes(this.state)) return false;
if ((this.fearTimer || 0) > 0.18 || (this.intimidatedTimer || 0) > 0.04 || (this.intimidateTimer || 0) > 0.04) return false;
if (this.stress > 64 || (this.needs?.safety || 0) >= 70) return false;
return (this.needs?.social || 0) < 70 || this.goodMode === "smile" || this.shouldApplyPersonalityBehavior("sociability", 1);
},
sunbathSpriteCandidates() {
const ids = SPRITES.map(s => s?.id).filter(id => typeof id === "string" && id.startsWith("sunbath"));
const ordered = ids.length ? ids : ["sunbath_1", "sunbath_2", "sunbath_3"];
return ordered.slice().sort((a, b) => {
const an = Number(String(a).match(/(\d+)$/)?.[1] || 0);
const bn = Number(String(b).match(/(\d+)$/)?.[1] || 0);
return an - bn || String(a).localeCompare(String(b));
});
},
preloadSunbathSprites() {
const ids = this.sunbathSpriteCandidates ? this.sunbathSpriteCandidates() : ["sunbath_1", "sunbath_2", "sunbath_3"];
for (const id of ids) ensureImage(id);
},
normalizeSunbathSpriteId(value = "") {
if (typeof value === "number" && Number.isFinite(value)) return `sunbath_${Math.max(1, Math.floor(value))}`;
const str = String(value || "");
if (/^sunbath_\d+$/.test(str) || str.startsWith("sunbath")) return str;
return "";
},
isSunbathSpriteReady(id = "") {
if (!id) return false;
return isImageReady(id);
},
randomSunbathSpriteId(exclude = "", options = {}) {
const ids = this.sunbathSpriteCandidates ? this.sunbathSpriteCandidates() : ["sunbath_1", "sunbath_2", "sunbath_3"];
if (!ids.length) return "sunbath_1";
const normalizedExclude = this.normalizeSunbathSpriteId ? this.normalizeSunbathSpriteId(exclude) : String(exclude || "");
let candidates = ids;
if (options.readyOnly) {
const ready = ids.filter(id => this.isSunbathSpriteReady ? this.isSunbathSpriteReady(id) : true);
if (ready.length) candidates = ready;
else return (this._lastRenderableSunbathSpriteId && ids.includes(this._lastRenderableSunbathSpriteId))
? this._lastRenderableSunbathSpriteId
: ((normalizedExclude && ids.includes(normalizedExclude)) ? normalizedExclude : (ids[0] || "sunbath_1"));
}
const choices = candidates.length > 1 ? candidates.filter(id => id !== normalizedExclude) : candidates;
return choices[Math.floor(Math.random() * choices.length)] || candidates[0] || ids[0] || "sunbath_1";
},
sunbathSpriteId() {
const ids = this.sunbathSpriteCandidates ? this.sunbathSpriteCandidates() : ["sunbath_1", "sunbath_2", "sunbath_3"];
let current = this.normalizeSunbathSpriteId ? this.normalizeSunbathSpriteId(this.sunbathSpriteVariant) : String(this.sunbathSpriteVariant || "");
if (!current || !ids.includes(current)) {
this.sunbathSpriteVariant = this.randomSunbathSpriteId ? this.randomSunbathSpriteId("", { readyOnly: true }) : "sunbath_1";
current = this.normalizeSunbathSpriteId ? this.normalizeSunbathSpriteId(this.sunbathSpriteVariant) : String(this.sunbathSpriteVariant || "sunbath_1");
}
if (!this.isSunbathSpriteReady?.(current)) {
const last = this._lastRenderableSunbathSpriteId;
if (last && ids.includes(last) && this.isSunbathSpriteReady?.(last)) {
this.sunbathSpriteVariant = last;
current = last;
} else {
const ready = ids.find(id => this.isSunbathSpriteReady?.(id));
if (ready) {
this.sunbathSpriteVariant = ready;
current = ready;
}
}
}
return current || "sunbath_1";
},
isSunbathTime() {
const weather = this.world?.weather || "";
const weatherOk = weather === "sunny" || weather === "cloudy";
const hour = this.world?.hourOfDay ? this.world.hourOfDay() : ((((this.world?.time || 0) / Math.max(1, CONFIG.dayLength || 120)) * 24) % 24);
return weatherOk && hour >= 6 && hour <= 13.5;
},
canStartSunbath(options = {}) {
if (this.dead || this.sleeping) return false;
if (!Number.isFinite(this.x) || !Number.isFinite(this.y)) return false;
if (!Number.isFinite(this.world?.w) || !Number.isFinite(this.world?.h)) return false;
if ((this.entryTimer || 0) > 0.04) return false;
if (!this.isSunbathTime()) return false;
const parasolSystem = globalThis.TarinaiParasolSystem || globalThis.TarinaiRainShelterSystem;
if (parasolSystem?.blocksWeatherAt?.(this.world, this.x, this.y)) return false;
const currentFelt = this.world?.feltTemperatureFor?.(this) ?? this.world?.temperatureAt?.(this.x, this.y, this) ?? this.feltTemperature ?? this.tempComfort ?? (CONFIG.standardTemperature ?? 15);
if (Number.isFinite(Number(currentFelt)) && Number(currentFelt) >= 22) return false;
if ((this.sunbathCooldown || 0) > 0) return false;
if (this.fightDisease) return false;
if (this.stuckPushpinId || this.currentLodgedPin?.()) return false;
if (["fight", "panic", "cursor_enemy", "eat", "seek_food", "seek_water", "seek_bed", "sleep", "birth_ritual", "ant_attack", "intimidate", "ant_intimidate"].includes(this.state)) return false;
if (this.hunger > (options.leisure ? 62 : 72) || this.energy < (options.leisure ? 24 : 16)) return false;
const healthNeed = Number(this.needRaw?.health ?? this.needs?.health ?? 0) || 0;
const sick = this.zunchiDisease || this.sleepDisease || this.explosionDisease;
if (sick && healthNeed >= 34) return true;
return options.leisure ? healthNeed < needThreshold("health", "start") : healthNeed >= Math.max(38, needThreshold("health", "start") - 12);
},
tryRecoverBySunbath() {
if (this.sunbathRecoveryChecked) return false;
this.sunbathRecoveryChecked = true;
const sick = this.zunchiDisease || this.sleepDisease || this.explosionDisease;
if (!sick || Math.random() >= 0.25) return false;
let healed = false;
if (this.zunchiDisease && this.recoverZunchiDisease) healed = this.recoverZunchiDisease("\u65e5\u5149\u6d74\u3067\u305a\u3093\u3061\u75c5\u304c\u6cbb\u3063\u305f") || healed;
if (this.sleepDisease && this.recoverSleepDisease) healed = this.recoverSleepDisease("\u65e5\u5149\u6d74\u3067\u306d\u3080\u308a\u75c5\u304c\u6cbb\u3063\u305f") || healed;
if (this.explosionDisease && this.recoverExplosionDisease) healed = this.recoverExplosionDisease("\u65e5\u5149\u6d74\u3067\u7206\u767a\u75c5\u304c\u6cbb\u3063\u305f") || healed;
if (healed) this.world?.log?.(`${this.name}\u306f\u65e5\u5149\u6d74\u3067\u75c5\u6c17\u304c\u8efd\u304f\u306a\u3063\u305f\u3002`, "event", { participants: [this] });
return healed;
},
finishSunbath() {
if (this.sunbathFinishedAt === this.world?.time) return false;
this.sunbathFinishedAt = this.world?.time || 0;
applyNeedSatisfaction(this, { health: 56, safety: 6, fulfill: 8 }, "sunbath");
this.zunchiStain = 0;
this.tryRecoverBySunbath();
this.sunbathCooldown = Math.max(this.sunbathCooldown || 0, rand(20, 38));
if ((this.world?.time || 0) >= (this.nextBubbleAt || 0)) this.bubble("\u307d\u304b\u3063", 3.4, "rgba(112,86,36,0.78)");
this.sunbathLeisure = false;
return true;
},
startSunbath(options = {}) {
if (!this.canStartSunbath(options)) return false;
this.sunbathLeisure = Boolean(options.leisure);
const pad = Math.max(28, CONFIG.worldPadding || 30);
const safeW = Math.max(pad * 2 + 1, Number(this.world?.w || 1000) || 1000);
const safeH = Math.max(pad * 2 + 1, Number(this.world?.h || 720) || 720);
const startX = Number.isFinite(this.x) ? this.x : (Number.isFinite(this._lastValidX) ? this._lastValidX : pad);
const startY = Number.isFinite(this.y) ? this.y : (Number.isFinite(this._lastValidY) ? this._lastValidY : pad);
this.x = clamp(startX, pad, safeW - pad);
this.y = clamp(startY, pad, safeH - pad);
this.sunbathAnchorX = this.x;
this.sunbathAnchorY = this.y;
this._lastSunbathDrawX = this.x;
this._lastSunbathDrawY = this.y;
this._lastValidX = this.x;
this._lastValidY = this.y;
this.entryTimer = 0;
const reason = options.leisure ? "\u5c11\u3057\u843d\u3061\u7740\u3044\u3066\u65e5\u5149\u6d74\u3057\u3066\u3044\u308b" : "\u65e5\u5149\u6d74\u3057\u3066\u3044\u308b";
this.setActionState?.("sunbath", { target: null, reason, sleeping: false });
this.preloadSunbathSprites?.();
this.sunbathTimer = Math.max(6, (CONFIG.dayLength || 120) / 12);
this.sunbathCooldown = 0;
this.sunbathFrameTimer = 0.5;
this.sunbathSpriteVariant = this.randomSunbathSpriteId ? this.randomSunbathSpriteId("", { readyOnly: true }) : `sunbath_${1 + Math.floor(Math.random() * 3)}`;
this._activeSunbathSpriteId = this.sunbathSpriteId ? this.sunbathSpriteId() : this.sunbathSpriteVariant;
this._lastRenderableSunbathSpriteId = this._activeSunbathSpriteId;
this.visibleSpriteId = this._activeSunbathSpriteId;
this.spriteLockUntil = 0;
this.sunbathRecoveryChecked = false;
this.vx = 0;
this.vy = 0;
setBehaviorText(this, { need: options.leisure ? "fulfill" : "health", subNeed: "sunbath", actionId: "sunbath", actionLabel: "\u65e5\u5149\u6d74\u3057\u3066\u3044\u308b", reason: options.leisure ? "\u4f59\u88d5\u304c\u3042\u308b\u306e\u3067\u3001\u65e5\u5149\u6d74\u3057\u3066\u3044\u308b\u3002" : "\u8abf\u5b50\u304c\u60aa\u3044\u306e\u3067\u3001\u65e5\u5149\u6d74\u3057\u3066\u3044\u308b\u3002", target: null, phase: "perform", source: "behavior", causeText: options.leisure ? "\u4f59\u88d5\u304c\u3042\u308b" : "" });
if ((this.world?.time || 0) >= (this.nextBubbleAt || 0) && Math.random() < 0.45) this.bubble("\u307d\u304b\u3063", 4.0, "rgba(112,86,36,0.78)");
return true;
},
bubble(text, cooldown = 2.4, color = "rgba(42,36,29,0.78)", options = {}) {
if (!options.force && this.world.time < this.nextBubbleAt) return false;
this.nextBubbleAt = this.world.time + cooldown;
this.world.spawnBubble(this.x, this.y - this.radius * 1.35, text, color);
return true;
},
mouthPosition(target = null) {
const dir = target ? Math.sign((target.x ?? this.x) - this.x) || this.facingDir() : this.facingDir();
return {
x: this.x + dir * this.radius * 0.40 - this.radius * 0.34,
y: this.y - this.radius * 0.28,
};
},
isSleepFurniture(target) {
return Boolean(target && (target.roles?.sleepPlace || (typeof isSleepFurnitureType === "function" ? isSleepFurnitureType(target.type) : (target.type === "bed" || target.type === "nest_box" || target.type === "pipe"))));
},
sleepSpotFor(bed) {
if (!bed) return { x: this.x, y: this.y };
if (bed.type === "nest_box" || bed.type === "pipe") {
if (this.insideNestBoxId === bed.id && this.world?.nestBoxInnerPoint) return this.world.nestBoxInnerPoint(bed, this);
if (this.world?.nestBoxEntryPoint) return this.world.nestBoxEntryPoint(bed);
return { x: clamp(bed.x, CONFIG.worldPadding, this.world.w - CONFIG.worldPadding), y: clamp(bed.y + (bed.r || 42) * 0.24, CONFIG.worldPadding, this.world.h - CONFIG.worldPadding) };
}
const angle = stableUnit(this.familyKey || this.id, `bed-angle-${bed.id || bed.seed || "bed"}`) * Math.PI * 2;
const baseRing = bed.r * 0.52;
const ring = baseRing + stableUnit(this.familyKey || this.id, `bed-ring-${bed.id || bed.seed || "bed"}`) * bed.r * 0.95;
return {
x: clamp(bed.x + Math.cos(angle) * ring, CONFIG.worldPadding, this.world.w - CONFIG.worldPadding),
y: clamp(bed.y + Math.sin(angle) * ring * 0.56 - this.radius * 0.10, CONFIG.worldPadding, this.world.h - CONFIG.worldPadding),
};
},
desiredFacingDir() {
if (this.state === "sleep") return this.sleepFacing || this.facing || 1;
if (this.target && Number.isFinite(this.target.x)) return Math.sign(this.target.x - this.x) || this.facing || 1;
if (Math.abs(this.vx) > 2.2) return Math.sign(this.vx);
return this.facing || 1;
},
facingDir() {
return this.visualFacing || this.facing || 1;
},
updateFacing() {
const desired = this.desiredFacingDir();
const now = this.world?.time || 0;
this.facing = desired;
if (!this.visualFacing) {
this.visualFacing = desired;
this.facingLockUntil = now + 1.0;
return;
}
if (Math.sign(desired) !== Math.sign(this.visualFacing) && now >= (this.facingLockUntil || 0)) {
this.visualFacing = Math.sign(desired) || this.visualFacing || 1;
this.facingLockUntil = now + 1.0;
}
},
reactToFreshZunchi(dt) {
if (this.isZunchiSlave) return;
if (this.state === "panic" || this.fearTimer > 0.2) return;
const z = this.world.nearest(this, ["zunchi"], 90);
if (!z || z.stage !== "fresh") return;
const d = dist(this, z);
const push = clamp(1 - d / 90, 0, 1);
applyNeedShock(this, { safety: push * dt * 6 });
if (push > 0.2) this.adjustPersonality("neuroticism", -dt * push * 0.0035, "after repeated contact with poop.");
this.vx += (this.x - z.x) / Math.max(d, 1) * push * dt * 40;
this.vy += (this.y - z.y) / Math.max(d, 1) * push * dt * 40;
if (push > 0.35 && (this.state === "idle" || !this.thought)) this.thought = "\u65b0\u9bae\u306a\u305a\u3093\u3061\u3092\u907f\u3051\u3066\u3044\u308b";
},
resolveNeeds(dt) {
return global.TarinaiNeedPlannerSystem.updateOne(this, dt);
},
dropPoopNow(source = "normal") {
const threshold = NORMAL_POOP_MEAL_THRESHOLD || 3;
if ((Number(this.digest) || 0) + 1e-6 < threshold) return false;
this.digest = Math.max(0, (Number(this.digest) || 0) - threshold);
this.poopCount = (this.poopCount || 0) + 1;
const side = this.facingDir();
const backX = this.x - side * this.radius * rand(0.72, 0.96);
const backY = this.y + this.radius * rand(0.25, 0.48);
this.world.spawnZunchi(backX, backY, this);
this.bubble("\u3076\u308a\u3085\u3063", 3.2, "rgba(62,84,45,0.78)", { force: true });
if (Math.random() < 0.35) this.world.log(`${this.name}\u304c\u305a\u3093\u3061\u3092\u843d\u3068\u3057\u305f\u3002`, null, { participants: [this] });
return true;
},
makePoop(force = 1) {
const add = Math.max(0, Number(force) || 0);
if (add <= 0) return false;
const threshold = NORMAL_POOP_MEAL_THRESHOLD || 3;
this.digest = Math.max(0, Number(this.digest) || 0) + add;
if (this.digest + 1e-6 < threshold) return false;
if (this.hasLodgedPinEffect?.("blocksZunchi")) {
this.digest = Math.max(0, this.digest - threshold);
this.oshiriByoZunchiStock = Math.min(18, Math.max(0, this.oshiriByoZunchiStock || 0) + 1);
this.thought = (this.oshiriByoZunchiStock || 0) >= 6 ? "\u304a\u3057\u308a\u75c5\u3067\u305a\u3093\u3061\u304c\u6e9c\u307e\u3063\u3066\u3064\u3089\u3044" : "\u304a\u3057\u308a\u75c5\u3067\u305a\u3093\u3061\u3092\u6211\u6162\u3057\u3066\u3044\u308b";
if ((this.oshiriByoZunchiStock || 0) >= 6) applyNeedShock(this, { health: 8 });
return false;
}
return this.dropPoopNow?.("normal") || false;
},
interactWithItems(dt) {
return window.TarinaiItemInteractionSystem.updateOne(this, dt);
}
}));
})(typeof window !== "undefined" ? window : globalThis);
global.TarinaiNeedsRuntime = Object.freeze({
updateNeeds,
startNeedAction,
});
})(typeof window !== "undefined" ? window : globalThis);