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