297 lines
17 KiB
JavaScript
297 lines
17 KiB
JavaScript
"use strict";
|
|
|
|
|
|
// Layer: entity-runtime/tarinai/update-step
|
|
// Owns per-frame timers, passive metabolism, disease/effect ticks, colony mood
|
|
// pressure, and early freeze exits. This is extracted from the former
|
|
// Tarinai.prototype.update body without changing behavior.
|
|
(function (global) {
|
|
function detRange(worldRef, salt, min, max, ...parts) {
|
|
return global.deterministicRange(worldRef, salt, min, max, ...parts);
|
|
}
|
|
|
|
function detSigned(worldRef, salt, ...parts) {
|
|
return global.deterministicSigned(worldRef, salt, ...parts);
|
|
}
|
|
|
|
function detChance(worldRef, salt, chance, ...parts) {
|
|
return global.deterministicChance(worldRef, salt, chance, ...parts);
|
|
}
|
|
|
|
const OCHIBI_HIGH_STRESS_SPEECHES = [
|
|
{ id: "voice_acquired_stress_001", text: "パパずんはどこなのだ?" },
|
|
{ id: "voice_acquired_stress_002", text: "ママずんはどこなのだ?" },
|
|
{ id: "voice_acquired_stress_003", text: "こんなはずじゃなかったのだ" },
|
|
{ id: "voice_acquired_stress_004", text: "どうして" },
|
|
{ id: "voice_acquired_stress_005", text: "おまえをゆるさないのだ" },
|
|
{ id: "voice_acquired_stress_006", text: "のろってやるのだ" },
|
|
{ id: "voice_acquired_stress_007", text: "てあしをかえせなのだ" },
|
|
{ id: "voice_acquired_stress_008", text: "すべすべしっぽしゃんが" },
|
|
{ id: "voice_acquired_stress_009", text: "くちょにんげん" },
|
|
{ id: "voice_ochibi_stress_008", text: "なんでぼくがこんなめに..." }
|
|
];
|
|
const OCHIBI_LOW_STRESS_SPEECHES = [
|
|
{ id: "voice_ochibi_calm_001", text: "ぼくはゆうしゅうなのだ" },
|
|
{ id: "voice_ochibi_calm_002", text: "しあわせなのだ" },
|
|
{ id: "voice_ochibi_calm_003", text: "てあしがなくてもげんきなのだ" },
|
|
{ id: "voice_ochibi_calm_004", text: "へこへこしたいのだ" },
|
|
{ id: "voice_ochibi_calm_005", text: "のだのだのだ" },
|
|
{ id: "voice_ochibi_calm_006", text: "くちょおやがいなくてもぼくはたたかえるのだ" },
|
|
{ id: "voice_ochibi_calm_007", text: "ぼくはげんきいっぱいなのだ" },
|
|
{ id: "voice_ochibi_calm_008", text: "かっきにみちみちしているのだ" },
|
|
{ id: "voice_ochibi_calm_009", text: "ずんちでそう" }
|
|
];
|
|
|
|
function maybePlayOchibiIdleSpeech(t) {
|
|
if (!t || t.dead || !t.isAcquiredTarinai || String(t.state || "") !== "idle") return false;
|
|
const now = Number(t.world?.time || 0) || 0;
|
|
const stress = Number(t.stress || 0) || 0;
|
|
const highStress = stress >= 68;
|
|
const pool = highStress ? OCHIBI_HIGH_STRESS_SPEECHES : OCHIBI_LOW_STRESS_SPEECHES;
|
|
if (!pool.length) return false;
|
|
if (!Number.isFinite(t.acquiredStressSpeechAt)) t.acquiredStressSpeechAt = now + detRange(t.world, "ochibi-idle-speech-init", 2.8, 6.5, t);
|
|
if ((t.acquiredStressSpeechCooldownUntil || 0) > now || (t.nextBubbleAt || 0) > now || now < t.acquiredStressSpeechAt) return false;
|
|
const idx = Math.max(0, Math.min(pool.length - 1, Math.floor(detRange(t.world, highStress ? "ochibi-high-line" : "ochibi-low-line", 0, pool.length, t, Math.floor(now * 10)))));
|
|
const line = pool[idx] || pool[0];
|
|
if (!line) return false;
|
|
const color = highStress ? "rgba(58,48,63,0.82)" : "rgba(62,84,45,0.78)";
|
|
const bubbled = typeof t.bubble === "function" ? t.bubble(line.text, 5.8, color) : false;
|
|
if (!bubbled) return false;
|
|
const audio = global.TarinaiAudio;
|
|
if (audio?.playSample) audio.playSample(line.id, 1.5, "voice") || audio.play(line.id, { category: "voice", minGap: 1.5 });
|
|
else if (audio?.play) audio.play(line.id, { category: "voice", minGap: 1.5 });
|
|
t.acquiredStressSpeechCooldownUntil = now + 1.5;
|
|
t.acquiredStressSpeechAt = now + detRange(t.world, highStress ? "ochibi-high-next" : "ochibi-low-next", highStress ? 6.5 : 7.0, highStress ? 12.0 : 13.0, t, line.id, Math.floor(now));
|
|
return true;
|
|
}
|
|
|
|
function update(t, dt, context = {}) {
|
|
if (!t || t.dead) return { done: true };
|
|
if (t.isAcquiredTarinai && (t.state === "sunbath" || (t.sunbathTimer || 0) > 0.04)) {
|
|
t.sunbathTimer = 0;
|
|
t.sunbathLeisure = false;
|
|
t.sunbathCooldown = Math.max(t.sunbathCooldown || 0, 18);
|
|
t.visibleSpriteId = "";
|
|
t.setActionState?.("idle", { target: null, reason: "おちびは日光浴をしない", sleeping: false, clearTarget: true }); // acquired-sunbath-cancel
|
|
}
|
|
t.prevX = t.x;
|
|
t.prevY = t.y;
|
|
|
|
const minute = dt / 60;
|
|
t.age += dt;
|
|
// reproductionTimer is advanced in the outer creature pass so low-frequency
|
|
// AI updates do not stretch the cooldown as population grows.
|
|
t.eatTimer = Math.max(0, t.eatTimer - dt);
|
|
if (t.state === "eat" && t.eatTimer <= 0.02) {
|
|
t.setActionState?.("idle", { target: null, reason: "\u98df\u3079\u7d42\u3048\u305f", sleeping: false, clearTarget: true });
|
|
}
|
|
t.eatCooldown = Math.max(0, t.eatCooldown - dt);
|
|
t.foodReactTimer = Math.max(0, t.foodReactTimer - dt);
|
|
t.surpriseTimer = Math.max(0, t.surpriseTimer - dt);
|
|
t.fearTimer = Math.max(0, t.fearTimer - dt);
|
|
t.intimidateTimer = Math.max(0, t.intimidateTimer - dt);
|
|
t.intimidatedTimer = Math.max(0, t.intimidatedTimer - dt);
|
|
t.intimidationCooldown = Math.max(0, (t.intimidationCooldown || 0) - dt);
|
|
t.blastSpinTimer = Math.max(0, (t.blastSpinTimer || 0) - dt);
|
|
t.postBirthPeaceTimer = Math.max(0, (t.postBirthPeaceTimer || 0) - dt);
|
|
t.hpBarTimer = Math.max(0, (t.hpBarTimer || 0) - dt);
|
|
t.stressBarTimer = Math.max(0, (t.stressBarTimer || 0) - dt);
|
|
t.updateZunchiDisease(dt);
|
|
t.updateSpecialDiseases(dt);
|
|
t._achievementLaxativeActiveThisFrame = Boolean(t.hasItemEffect?.("laxative"));
|
|
if (t.updateBurning) t.updateBurning(dt);
|
|
if (t.updateItemEffects) t.updateItemEffects(dt);
|
|
global.TarinaiMamekusaredakeSystem?.updateTarinai?.(t, dt);
|
|
if (t.hasItemEffect?.("sedative")) {
|
|
t.fearTimer = Math.max(0, (t.fearTimer || 0) - dt * 1.8);
|
|
t.intimidatedTimer = Math.max(0, (t.intimidatedTimer || 0) - dt * 1.4);
|
|
t.fightMochiTimer = Math.max(0, (t.fightMochiTimer || 0) - dt * 2.6);
|
|
if (typeof applyNeedRelief === "function") applyNeedRelief(t, { safety: -dt * 2.6, social: -dt * 0.5 });
|
|
}
|
|
if (t.dead) return { done: true };
|
|
t.loveMochiTimer = Math.max(0, (t.loveMochiTimer || 0) - dt);
|
|
t.fightMochiTimer = Math.max(0, (t.fightMochiTimer || 0) - dt);
|
|
if (t.fightMochiTimer > 0.04) {
|
|
applyNeedShock(t, { safety: dt * 4 });
|
|
if (t.state !== "fight") t.fearTimer = Math.max(t.fearTimer || 0, 0.10);
|
|
}
|
|
if (t.intimidateTimer <= 0.04) {
|
|
t.intimidateTargetId = null;
|
|
if (t.state === "intimidate" || t.state === "ant_intimidate") {
|
|
t.spriteLockUntil = 0;
|
|
t.setActionState?.("idle", { target: null, reason: "\u5a01\u5687\u3092\u7d42\u3048\u305f", clearTarget: true, sleeping: false });
|
|
}
|
|
}
|
|
if (t.birthRitualTimer > 0) {
|
|
const beforeBirthTimer = t.birthRitualTimer;
|
|
t.birthRitualTimer = Math.max(0, t.birthRitualTimer - dt);
|
|
if (beforeBirthTimer > 0 && t.birthRitualTimer <= 0.04 && t.birthRitualLeader) {
|
|
const partner = t.world.liveTarinaiById?.(t.birthPartnerId);
|
|
if (partner) t.world.finishBirthRitual(t, partner);
|
|
t.birthRitualLeader = false;
|
|
}
|
|
}
|
|
t.pokeFlashTimer = Math.max(0, t.pokeFlashTimer - dt);
|
|
t.focusPulseTimer = Math.max(0, (t.focusPulseTimer || 0) - dt);
|
|
t.cursorPetting = Math.max(0, t.cursorPetting - dt * 1.2);
|
|
t.pinchThoughtTimer = Math.max(0, (t.pinchThoughtTimer || 0) - dt);
|
|
t.sunbathCooldown = Math.max(0, (t.sunbathCooldown || 0) - dt);
|
|
if (t.freezeSleepDiseaseMotion(dt)) {
|
|
t.checkLife(dt);
|
|
if (!t.dead) t._achievementLaxativeActiveThisFrame = false;
|
|
return { done: true };
|
|
}
|
|
const wasFighting = t.fightTimer > 0.04;
|
|
const endingFightTargetIds = wasFighting ? [...new Set([t.fightTargetId, ...(Array.isArray(t.fightTargetIds) ? t.fightTargetIds : [])].filter(Boolean))] : [];
|
|
t.fightTimer = Math.max(0, t.fightTimer - dt);
|
|
t.fightCooldown = Math.max(0, t.fightCooldown - dt);
|
|
t.postConflictPeaceTimer = Math.max(0, (t.postConflictPeaceTimer || 0) - dt);
|
|
t.groundStumbleCooldown = Math.max(0, (t.groundStumbleCooldown || 0) - dt);
|
|
t.defeatedTimer = Math.max(0, t.defeatedTimer - dt);
|
|
if (wasFighting && t.fightTimer <= 0.04 && t.defeatedById) {
|
|
const winner = t.world.liveTarinaiById?.(t.defeatedById) || null;
|
|
t.defeatedTimer = Math.max(t.defeatedTimer, detRange(t.world, "defeated-recover-timer", 3.4, 5.2, t));
|
|
t.fearTimer = Math.max(t.fearTimer, 1.0 * t.personalityProfile().fear);
|
|
t.hurtTimer = Math.max(t.hurtTimer, 1.8);
|
|
t.fallTimer = Math.max(t.fallTimer, 1.15);
|
|
t.fallMax = Math.max(t.fallMax || 1.15, t.fallTimer);
|
|
t.fallDir = winner ? (t.x < winner.x ? -1 : 1) : t.fallDir;
|
|
t.world.spawnFallEffect(t.x, t.y + t.radius * 0.45);
|
|
if (winner) t.world.resolveFightOutcome(winner, t);
|
|
t.fightTargetIds = [];
|
|
t.fightTargetId = null;
|
|
}
|
|
if (wasFighting && t.fightTimer <= 0.04) {
|
|
const rivals = endingFightTargetIds.map(id => t.world?.liveTarinaiById?.(id)).filter(Boolean);
|
|
const cooldown = CONFIG.fightCooldown + detRange(t.world, "fight-ended-cooldown", 8, 14, t, endingFightTargetIds.join("/"));
|
|
t.fightCooldown = Math.max(t.fightCooldown || 0, cooldown);
|
|
t.postConflictPeaceTimer = Math.max(t.postConflictPeaceTimer || 0, detRange(t.world, "fight-ended-peace", 5.5, 9.0, t, endingFightTargetIds.join("/")));
|
|
for (const rival of rivals) t.world?.markFightPairCooldown?.(t, rival, cooldown + 2);
|
|
if (!t.defeatedById) {
|
|
t.fightTargetIds = [];
|
|
t.fightTargetId = null;
|
|
t.counterAttackFromId = null;
|
|
if (t.state === "fight" || t.state === "seek_enemy") t.setActionState?.("idle", { target: null, reason: "\u55A7\u5629\u304C\u7D42\u308F\u3063\u3066\u5C11\u3057\u8DDD\u96E2\u3092\u7F6E\u3044\u3066\u3044\u308B\u3002", sleeping: false, clearTarget: true });
|
|
}
|
|
}
|
|
t.grassEatTimer = Math.max(0, t.grassEatTimer - dt);
|
|
t.hurtTimer = Math.max(0, t.hurtTimer - dt);
|
|
t.fallTimer = Math.max(0, t.fallTimer - dt);
|
|
if ((t.world?.groundType || "soil") === "foot_massage"
|
|
&& (t.groundStumbleCooldown || 0) <= 0.04
|
|
&& (t.fallTimer || 0) <= 0.04
|
|
&& !t.dead
|
|
&& !["sleep", "fight", "panic", "birth_ritual", "frozen"].includes(String(t.state || ""))
|
|
&& detChance(t.world, "foot-massage-stumble", dt * 0.055, t)) {
|
|
const dir = detSigned(t.world, "foot-massage-stumble-dir", t);
|
|
t.groundStumbleCooldown = detRange(t.world, "foot-massage-stumble-cooldown", 8.5, 16.0, t);
|
|
t.fallTimer = Math.max(t.fallTimer || 0, detRange(t.world, "foot-massage-stumble-fall", 0.86, 1.18, t));
|
|
t.fallMax = Math.max(t.fallMax || 1.05, t.fallTimer);
|
|
t.fallDir = dir;
|
|
t.vx += dir * detRange(t.world, "foot-massage-stumble-vx", 26, 52, t);
|
|
t.vy += detRange(t.world, "foot-massage-stumble-vy", -18, 18, t);
|
|
t.world?.spawnFallEffect?.(t.x, t.y + t.radius * 0.45);
|
|
t.setActionState?.("idle", { target: null, reason: "\u8db3\u3064\u307c\u306e\u7a81\u8d77\u306b\u3064\u307e\u3065\u3044\u305f\u3002", sleeping: false, clearTarget: false });
|
|
if ((t.world?.time || 0) >= (t.nextBubbleAt || 0)) t.bubble?.("\u3046\u3063", 1.6, "rgba(86,70,96,0.78)");
|
|
}
|
|
maybePlayOchibiIdleSpeech(t);
|
|
if (!t.isAcquiredTarinai && t.stress > 92 && t.state !== "sleep" && t.state !== "fight" && t.state !== "eat" && detChance(t.world, "stress-breath-bubble", dt * 0.050, t)) {
|
|
t.bubble("\u306f\u3063...\u306f\u3063...", 3.4, "rgba(58,48,63,0.80)");
|
|
}
|
|
if (!t.isAcquiredTarinai && t.state === "idle" && t.stress < 78 && detChance(t.world, "idle-bubble", dt * 0.018, t)) {
|
|
t.bubble(pick(["\u306f\u3046", "\u306f\u3046\u3045", "\u306f\u3046\u3063", "\u306f\u3045"]), 4.2, "rgba(62,84,45,0.76)");
|
|
}
|
|
if (t.hurtTimer > 0 && detChance(t.world, "hurt-bleed-effect", dt * 5.2, t)) {
|
|
const side = t.facingDir();
|
|
t.world.spawnBleedEffect(t.x - side * t.radius * detRange(t.world, "hurt-bleed-x", 0.15, 0.42, t), t.y - t.radius * detRange(t.world, "hurt-bleed-y", 0.08, 0.34, t));
|
|
}
|
|
|
|
t.tempTimer += dt;
|
|
if (t.tempTimer >= 0.55) {
|
|
const felt = t.world?.feltTemperatureFor?.(t) ?? t.world?.temperatureAt?.(t.x, t.y, t) ?? (CONFIG.standardTemperature ?? 15);
|
|
t.tempComfort = felt;
|
|
t.feltTemperature = felt;
|
|
t.temperatureStatus = t.world?.temperatureStatusFor?.(felt, t) || null;
|
|
t.tempTimer = 0;
|
|
}
|
|
const tempStatus = t.temperatureStatus || t.world?.temperatureStatusFor?.(t.tempComfort ?? (CONFIG.standardTemperature ?? 15), t) || { discomfort: 0, stressExcess: 0, harmExcess: 0, direction: "comfort" };
|
|
const coldDiscomfort = tempStatus.direction === "cold" ? Math.max(0, tempStatus.discomfort || 0) : 0;
|
|
const hungerScale = t.metabolicHungerMultiplier ? t.metabolicHungerMultiplier() : (typeof tarinaiMetabolicHungerScale === "function" ? tarinaiMetabolicHungerScale(t) : 1);
|
|
t.hunger += CONFIG.hungerPerMinute * minute * 0.75 * t.trait.hunger * hungerScale * (1 + coldDiscomfort * 0.018);
|
|
t.loneliness += CONFIG.lonelinessPerMinute * minute * (2 - t.trait.social);
|
|
t.energy -= CONFIG.energyPerMinute * minute * t.trait.sleep * (t.state === "sleep" ? 0.025 : 1);
|
|
if (t.state === "sleep" || t.sleeping) {
|
|
const sleepBedLike = t.target && typeof t.isSleepFurniture === "function" && t.isSleepFurniture(t.target);
|
|
const sleepEnergyMax = Math.max(1, typeof tarinaiMaxEnergy === "function" ? tarinaiMaxEnergy(t) : (Number(t.maxEnergy) || 100));
|
|
t.energy = Math.min(sleepEnergyMax, (Number(t.energy || 0) || 0) + dt * (sleepBedLike ? 0.52 : 0.24));
|
|
if (typeof applyNeedRelief === "function") applyNeedRelief(t, { sleep: -dt * (sleepBedLike ? 2.0 : 1.1) });
|
|
}
|
|
// stress is derived from needs; no independent stress decay.
|
|
if ((tempStatus.stressExcess || 0) > 0) {
|
|
const excess = tempStatus.stressExcess || 0;
|
|
const stressRate = excess * (0.66 + Math.min(20, excess) * 0.035);
|
|
applyNeedShock(t, { safety: stressRate * dt, health: stressRate * dt * 0.42 });
|
|
if ((t.world?.time || 0) >= (t.nextBubbleAt || 0) && detChance(t.world, "temperature-stress-bubble", dt * 0.18, t)) {
|
|
if (t.isAcquiredTarinai && tempStatus.direction === "hot" && Math.random() < 0.68) {
|
|
if (t.bubble("おみじゅ...", 2.4, "rgba(76,62,72,0.78)")) {
|
|
const a = global.TarinaiAudio;
|
|
if (a?.playSample) a.playSample("voice_ochibi_hot_007", 1.2, "voice") || a.play?.("voice_ochibi_hot_007", { category: "voice", minGap: 1.2 });
|
|
}
|
|
} else {
|
|
t.bubble(tempStatus.direction === "cold" ? "ぶるぶる" : "あちゅい", 2.4, "rgba(76,62,72,0.78)");
|
|
}
|
|
}
|
|
}
|
|
if ((tempStatus.harmExcess || 0) > 0) {
|
|
const harm = tempStatus.harmExcess || 0;
|
|
t.energy -= dt * (0.10 + harm * 0.075 + harm * harm * 0.006);
|
|
const tempDamageCause = tempStatus.direction === "cold" ? "\u51CD\u7D50" : "\u71B1\u4E2D\u75C7";
|
|
const tempDamageAt = t.world?.time || 0;
|
|
t.lastDamageCause = tempDamageCause;
|
|
t.lastDamageAmount = Math.max(Number(t.lastDamageAmount || 0) || 0, harm);
|
|
t.lastDamageAt = tempDamageAt;
|
|
if (harm >= 1.5 || t.energy <= 30 || t.stress >= 104) {
|
|
t.lastMajorDamageCause = tempDamageCause;
|
|
t.lastMajorDamageAt = tempDamageAt;
|
|
}
|
|
t.lastTemperatureDamageAt = tempDamageAt;
|
|
}
|
|
|
|
const phase = t.world.lightLevel();
|
|
if (phase < 0.25) t.energy += dt * 0.07;
|
|
if (phase > 0.75) applyNeedShock(t, { health: dt * 1.2 });
|
|
if (t.world.weather === "cloudy") {
|
|
applyNeedRelief(t, { safety: -dt * 1 });
|
|
t.vx *= Math.pow(0.996, dt * 60);
|
|
t.vy *= Math.pow(0.996, dt * 60);
|
|
} else if (t.world.weather === "light_rain") {
|
|
const shelteredFromRain = global.TarinaiRainShelterSystem?.isSheltered?.(t.world, t.x, t.y, t) || false;
|
|
if (!shelteredFromRain) {
|
|
t.energy += dt * 0.018;
|
|
applyNeedRelief(t, { health: -dt * 1, safety: -dt * 1 });
|
|
}
|
|
} else if (t.world.weather === "sunny") {
|
|
t.vx *= Math.pow(1.001, dt * 60);
|
|
t.vy *= Math.pow(1.001, dt * 60);
|
|
}
|
|
|
|
if (t.generation > 1 && t.syncGrowthScale) t.syncGrowthScale();
|
|
|
|
t.hunger = clamp(t.hunger, 0, 115);
|
|
t.loneliness = clamp(t.loneliness, 0, 110);
|
|
t.energy = clamp(t.energy, 0, typeof tarinaiMaxEnergy === "function" ? tarinaiMaxEnergy(t) : (Number(t.maxEnergy) || 100));
|
|
t.stress = applyGroundStressModifier(t, calculateStressFromNeeds(t.needRaw || t.needs || createDefaultNeeds()));
|
|
|
|
context.wasSleepingAtFrameStart = t.state === "sleep" || t.sleeping;
|
|
t.awakeLockTimer = Math.max(0, (t.awakeLockTimer || 0) - dt);
|
|
|
|
if (t.freezeSleepDiseaseMotion(dt)) {
|
|
t.checkLife(dt);
|
|
if (!t.dead) t._achievementLaxativeActiveThisFrame = false;
|
|
return { done: true };
|
|
}
|
|
return { done: false };
|
|
}
|
|
|
|
global.TarinaiFrameUpdateStep = Object.freeze({ update });
|
|
})(typeof window !== "undefined" ? window : globalThis);
|