1131 lines
61 KiB
JavaScript
1131 lines
61 KiB
JavaScript
"use strict";
|
|
|
|
(function (global) {
|
|
const Tarinai = global.Tarinai;
|
|
if (!Tarinai) throw new Error("Tarinai is not available for mixin: tarinai_needs_items.js");
|
|
Object.defineProperties(Tarinai.prototype, Object.getOwnPropertyDescriptors({
|
|
canEatItemType(type) {
|
|
const t = String(type || "");
|
|
if (!this.isZunchiSlave) return true;
|
|
if (!t || t === "sweet" || t === "grass" || t === "water" || t === "water_bowl") return false;
|
|
if (t === "zunchi" || t === "food" || t === "ant_corpse") return true;
|
|
const foodDef = global.TarinaiFoodRegistry?.get?.(t);
|
|
return Boolean(foodDef && foodDef.interest !== "water");
|
|
},
|
|
|
|
zunchiSlaveFoodTypes() {
|
|
const defs = global.TARINAI_FOOD_DEFINITIONS || {};
|
|
const types = Object.keys(defs).filter(type => this.canEatItemType(type));
|
|
if (!types.includes("zunchi")) types.push("zunchi");
|
|
return types;
|
|
},
|
|
|
|
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.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";
|
|
if (this.isZunchiSlave) return this.zunchiSlaveSpriteId();
|
|
if (this.type === "cry" && (this.mood < 26 || this.stress > 78 || this.hunger > 96 || this.energy < 12)) 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";
|
|
const low = this.lowHealthSprite();
|
|
if (low === "stretch") return low;
|
|
if (this.hunger >= 70 && !low) 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.fightTimer > 0.04) return "angry";
|
|
if (this.stress > 62 || (this.stress > 50 && this.mood < 46)) return this.variantSprite("stress");
|
|
if (low) return low;
|
|
if (this.state === "sleep" || this.energy < 18) return this.variantSprite("sleep");
|
|
if (this.surpriseTimer > 0.08) return "pokan";
|
|
if (this.state === "cursor_friend") return "smile";
|
|
if (this.state === "eat" || this.eatTimer > 0.04) return this.goodMode;
|
|
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.state === "seek_food") return "drool";
|
|
if (this.mood >= 66 && this.stress < 24 && this.hunger < 58 && this.loneliness < 62) return this.variantSprite("low_stress");
|
|
if (this.mood >= 63 && this.stress < 44 && this.hunger < 58) return this.goodMode;
|
|
if (this.mood < 38 || this.hunger > 72 || this.loneliness > 74) return "angry";
|
|
return this.goodMode || this.variantSprite("normal");
|
|
},
|
|
|
|
spriteId() {
|
|
const next = this.rawSpriteId();
|
|
const now = this.world?.time || 0;
|
|
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.mood < 46 || this.stress > 64) return false;
|
|
return this.affection > 12 || this.mood > 58 || this.goodMode === "smile" || this.shouldApplyPersonalityBehavior("sociability", 1);
|
|
},
|
|
|
|
sunbathSpriteId() {
|
|
if (!this.sunbathSpriteVariant) this.sunbathSpriteVariant = 1 + Math.floor(Math.random() * 3);
|
|
return `sunbath_${this.sunbathSpriteVariant}`;
|
|
},
|
|
|
|
isSunbathTime() {
|
|
const weatherOk = (this.world?.weather || "") === "sunny";
|
|
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 <= 15.5;
|
|
},
|
|
|
|
canStartSunbath() {
|
|
if (this.dead || this.sleepDisease || this.sleeping) return false;
|
|
if (!this.isSunbathTime()) return false;
|
|
if ((this.sunbathCooldown || 0) > 0) return false;
|
|
if (this.hasLodgedPinEffect?.("blocksZunchi")) return false;
|
|
if (["fight", "panic", "cursor_enemy", "eat", "seek_food", "seek_bed", "sleep", "birth_ritual", "ant_attack", "intimidate", "ant_intimidate"].includes(this.state)) return false;
|
|
if (this.hunger > 68 || this.energy < 18) return false;
|
|
return true;
|
|
},
|
|
|
|
tryRecoverBySunbath() {
|
|
if (this.sunbathRecoveryChecked) return false;
|
|
this.sunbathRecoveryChecked = true;
|
|
const sick = this.zunchiDisease || this.sleepDisease || this.explosionDisease || this.fightDisease;
|
|
if (!sick || Math.random() >= 0.75) return false;
|
|
let healed = false;
|
|
if (this.zunchiDisease && this.recoverZunchiDisease) healed = this.recoverZunchiDisease("日光浴でずんち病が治まった") || healed;
|
|
if (this.sleepDisease && this.recoverSleepDisease) healed = this.recoverSleepDisease("日光浴でねむり病が治った") || healed;
|
|
if (this.explosionDisease && this.recoverExplosionDisease) healed = this.recoverExplosionDisease("日光浴で爆発病が治った") || healed;
|
|
if (this.fightDisease && this.recoverFightDisease) healed = this.recoverFightDisease("日光浴できずつき病が治った") || healed;
|
|
if (healed) this.world?.log?.(`${this.name}は日光浴で病気が軽くなった。`, "event", { participants: [this] });
|
|
return healed;
|
|
},
|
|
|
|
startSunbath() {
|
|
if (!this.canStartSunbath()) return false;
|
|
this.setActionState?.("sunbath", { target: null, reason: "日なたぼっこ", sleeping: false });
|
|
this.sunbathTimer = rand(12, 24);
|
|
this.sunbathCooldown = rand(8, 18);
|
|
this.sunbathFrameTimer = 0;
|
|
this.sunbathSpriteVariant = 1 + Math.floor(Math.random() * 3);
|
|
this.sunbathRecoveryChecked = false;
|
|
this.tryRecoverBySunbath();
|
|
if ((this.world?.time || 0) >= (this.nextBubbleAt || 0) && Math.random() < 0.45) this.bubble("ぽかぽか", 4.0, "rgba(112,86,36,0.78)");
|
|
return true;
|
|
},
|
|
|
|
updateSunbath(dt) {
|
|
if ((this.sunbathTimer || 0) <= 0) return;
|
|
this.sunbathTimer = Math.max(0, (this.sunbathTimer || 0) - dt);
|
|
this.sunbathFrameTimer = Math.max(0, (this.sunbathFrameTimer || 0) - dt);
|
|
if ((this.sunbathFrameTimer || 0) <= 0) {
|
|
this.sunbathSpriteVariant = 1 + Math.floor(Math.random() * 3);
|
|
this.sunbathFrameTimer = rand(0.55, 1.15);
|
|
}
|
|
this.stress = clamp((this.stress || 0) - dt * 1.15, 0, 130);
|
|
this.energy = clamp((this.energy || 0) + dt * 0.08, 0, 100);
|
|
if (!this.isSunbathTime() || this.hunger > 86 || this.energy < 8 || this.state === "panic") this.sunbathTimer = 0;
|
|
if ((this.sunbathTimer || 0) <= 0 && this.state === "sunbath") {
|
|
this.goIdle("");
|
|
}
|
|
},
|
|
|
|
applyCursorContactCare(dt) {
|
|
const pointer = this.world?.pointer;
|
|
if (!pointer?.inside || !this.isAwakeCursorFriendly()) return;
|
|
const d = distXY(this.x, this.y, pointer.x, pointer.y);
|
|
const touch = this.radius * 1.35;
|
|
if (d > touch) return;
|
|
const p = clamp(1 - d / touch, 0, 1);
|
|
const relief = dt * (1.35 + 2.65 * p);
|
|
this.stress = clamp((this.stress || 0) - relief, 0, 130);
|
|
this.adjustPersonality("openness", dt * 0.006 * (0.4 + p), "after being petted.");
|
|
this.moodLiftTimer = Math.max(this.moodLiftTimer || 0, 0.5);
|
|
this.cursorPetting = clamp((this.cursorPetting || 0) + dt * (1.0 + p * 2.4), 0, 1.75);
|
|
this.goodMode = "smile";
|
|
if ((this.world.time || 0) >= (this.nextCursorHeartAt || 0)) {
|
|
this.nextCursorHeartAt = (this.world.time || 0) + Math.max(0.08, 0.22 - p * 0.10);
|
|
for (let i = 0; i < (p > 0.62 ? 3 : 2); i++) {
|
|
this.world.effects?.push(new Effect("heart", this.x + rand(-this.radius * 0.58, this.radius * 0.58), this.y - this.radius * rand(0.68, 1.58), {
|
|
vx: rand(-16, 16), vy: rand(-44, -20), size: rand(7, 13) * (0.8 + p * 0.45), life: rand(0.62, 0.95), color: "rgba(240,91,135,0.92)"
|
|
}));
|
|
}
|
|
}
|
|
},
|
|
|
|
update(dt) {
|
|
if (this.dead) return;
|
|
this.prevX = this.x;
|
|
this.prevY = this.y;
|
|
|
|
const minute = dt / 60;
|
|
this.age += dt;
|
|
this.reproductionTimer -= dt;
|
|
this.blink += dt;
|
|
this.eatTimer = Math.max(0, this.eatTimer - dt);
|
|
this.eatCooldown = Math.max(0, this.eatCooldown - dt);
|
|
this.foodReactTimer = Math.max(0, this.foodReactTimer - dt);
|
|
this.surpriseTimer = Math.max(0, this.surpriseTimer - dt);
|
|
this.fearTimer = Math.max(0, this.fearTimer - dt);
|
|
this.intimidateTimer = Math.max(0, this.intimidateTimer - dt);
|
|
this.intimidatedTimer = Math.max(0, this.intimidatedTimer - dt);
|
|
this.panicTimer = Math.max(0, (this.panicTimer || 0) - dt);
|
|
this.blastSpinTimer = Math.max(0, (this.blastSpinTimer || 0) - dt);
|
|
this.postBirthPeaceTimer = Math.max(0, (this.postBirthPeaceTimer || 0) - dt);
|
|
this.hpBarTimer = Math.max(0, (this.hpBarTimer || 0) - dt);
|
|
this.stressBarTimer = Math.max(0, (this.stressBarTimer || 0) - dt);
|
|
this.updateZunchiDisease(dt);
|
|
this.updateSpecialDiseases(dt);
|
|
if (this.updateItemEffects) this.updateItemEffects(dt);
|
|
if (this.dead) return;
|
|
this.loveMochiTimer = Math.max(0, (this.loveMochiTimer || 0) - dt);
|
|
this.fightMochiTimer = Math.max(0, (this.fightMochiTimer || 0) - dt);
|
|
if (this.fightMochiTimer > 0.04) {
|
|
this.stress = clamp(this.stress + dt * 0.11, 0, 130);
|
|
if (this.state !== "fight") this.fearTimer = Math.max(this.fearTimer || 0, 0.10);
|
|
}
|
|
if (this.intimidateTimer <= 0.04) this.intimidateTargetId = null;
|
|
if (this.birthRitualTimer > 0) {
|
|
const beforeBirthTimer = this.birthRitualTimer;
|
|
this.birthRitualTimer = Math.max(0, this.birthRitualTimer - dt);
|
|
if (beforeBirthTimer > 0 && this.birthRitualTimer <= 0.04 && this.birthRitualLeader) {
|
|
const partner = this.world.tarinai.find(o => o.id === this.birthPartnerId && !o.dead);
|
|
if (partner) this.world.finishBirthRitual(this, partner);
|
|
this.birthRitualLeader = false;
|
|
}
|
|
}
|
|
this.pokeFlashTimer = Math.max(0, this.pokeFlashTimer - dt);
|
|
this.focusPulseTimer = Math.max(0, (this.focusPulseTimer || 0) - dt);
|
|
this.cursorPetting = Math.max(0, this.cursorPetting - dt * 1.2);
|
|
this.pinchThoughtTimer = Math.max(0, (this.pinchThoughtTimer || 0) - dt);
|
|
this.sunbathCooldown = Math.max(0, (this.sunbathCooldown || 0) - dt);
|
|
if (this.freezeSleepDiseaseMotion(dt)) {
|
|
this.checkLife(dt);
|
|
return;
|
|
}
|
|
this.applyCursorContactCare(dt);
|
|
const colonyMoodId = this.world?.colonyMood?.id || "relaxed";
|
|
if (colonyMoodId === "weaklings" && !this.isZunchiSlave) this.stress = clamp((this.stress || 0) - dt * 0.28, 0, 130);
|
|
if (colonyMoodId === "zunda_party" || colonyMoodId === "ball_party") this.stress = clamp((this.stress || 0) - dt * 0.34, 0, 130);
|
|
if (colonyMoodId === "ant_overrun" && this.state !== "panic") {
|
|
const antSeen = (this.world?.nearbyAnts?.(this.x, this.y, 110, true) || []).some(a => a && !a.dead);
|
|
if (antSeen) window.TarinaiNeedSystem?.addNeedShock?.(this, { safety: 18 }, null);
|
|
}
|
|
const wasFighting = this.fightTimer > 0.04;
|
|
this.fightTimer = Math.max(0, this.fightTimer - dt);
|
|
this.fightCooldown = Math.max(0, this.fightCooldown - dt);
|
|
this.defeatedTimer = Math.max(0, this.defeatedTimer - dt);
|
|
if (wasFighting && this.fightTimer <= 0.04 && this.defeatedById) {
|
|
const winner = this.world.tarinai.find(o => o.id === this.defeatedById && !o.dead) || null;
|
|
this.defeatedTimer = Math.max(this.defeatedTimer, rand(3.4, 5.2));
|
|
this.fearTimer = Math.max(this.fearTimer, 1.0 * this.personalityProfile().fear);
|
|
this.hurtTimer = Math.max(this.hurtTimer, 1.8);
|
|
this.fallTimer = Math.max(this.fallTimer, 1.15);
|
|
this.fallMax = Math.max(this.fallMax || 1.15, this.fallTimer);
|
|
this.fallDir = winner ? (this.x < winner.x ? -1 : 1) : this.fallDir;
|
|
this.world.spawnFallEffect(this.x, this.y + this.radius * 0.45);
|
|
if (winner) this.world.resolveFightOutcome(winner, this);
|
|
this.fightTargetIds = [];
|
|
this.fightTargetId = null;
|
|
}
|
|
this.stretchTimer = Math.max(0, this.stretchTimer - dt);
|
|
this.grassEatTimer = Math.max(0, this.grassEatTimer - dt);
|
|
this.hurtTimer = Math.max(0, this.hurtTimer - dt);
|
|
this.fallTimer = Math.max(0, this.fallTimer - dt);
|
|
if (this.stress > 92 && this.state !== "sleep" && this.state !== "fight" && this.state !== "eat" && Math.random() < dt * 0.050) {
|
|
this.bubble("\u306f\u3063...\u306f\u3063...", 3.4, "rgba(58,48,63,0.80)");
|
|
}
|
|
if (this.state === "idle" && this.stress < 78 && Math.random() < dt * 0.018) {
|
|
this.bubble(pick(["\u306f\u3046", "\u306f\u3046\u3045", "\u306f\u3046\u3063", "\u306f\u3045"]), 4.2, "rgba(62,84,45,0.76)");
|
|
}
|
|
if (this.hurtTimer > 0 && Math.random() < dt * 5.2) {
|
|
const side = this.facingDir();
|
|
this.world.spawnBleedEffect(this.x - side * this.radius * rand(0.15, 0.42), this.y - this.radius * rand(0.08, 0.34));
|
|
}
|
|
|
|
this.tempTimer += dt;
|
|
if (this.tempTimer >= 0.55) {
|
|
this.tempComfort = this.world.temperatureAt(this.x, this.y);
|
|
this.tempTimer = 0;
|
|
}
|
|
const tempComfort = this.tempComfort ?? 0.62;
|
|
const tooCold = Math.max(0, 0.48 - tempComfort);
|
|
this.hunger += CONFIG.hungerPerMinute * minute * this.trait.hunger * (1 + tooCold * 0.8);
|
|
this.loneliness += CONFIG.lonelinessPerMinute * minute * (2 - this.trait.social);
|
|
this.energy -= CONFIG.energyPerMinute * minute * this.trait.sleep * (this.state === "sleep" ? 0.025 : 1);
|
|
this.stress -= CONFIG.stressDecayPerMinute * minute / this.trait.stress;
|
|
this.stress += tooCold * dt * 0.25;
|
|
|
|
const phase = this.world.lightLevel();
|
|
if (phase < 0.25) this.energy += dt * 0.07;
|
|
if (phase > 0.75) this.stress += dt * 0.04;
|
|
if (this.world.weather === "cloudy") {
|
|
this.stress -= dt * 0.018;
|
|
this.vx *= Math.pow(0.996, dt * 60);
|
|
this.vy *= Math.pow(0.996, dt * 60);
|
|
} else if (this.world.weather === "light_rain") {
|
|
this.energy += dt * 0.018;
|
|
this.stress -= dt * 0.025;
|
|
} else if (this.world.weather === "sunny") {
|
|
this.vx *= Math.pow(1.001, dt * 60);
|
|
this.vy *= Math.pow(1.001, dt * 60);
|
|
}
|
|
|
|
if (this.generation > 1 && this.syncGrowthScale) this.syncGrowthScale();
|
|
|
|
this.hunger = clamp(this.hunger, 0, 115);
|
|
this.loneliness = clamp(this.loneliness, 0, 110);
|
|
this.energy = clamp(this.energy, 0, 100);
|
|
this.stress = clamp(this.stress, 0, 130);
|
|
|
|
const wasSleepingAtFrameStart = this.state === "sleep" || this.sleeping;
|
|
this.awakeLockTimer = Math.max(0, (this.awakeLockTimer || 0) - dt);
|
|
|
|
if (this.freezeSleepDiseaseMotion(dt)) {
|
|
this.checkLife(dt);
|
|
return;
|
|
}
|
|
|
|
this.aiTimer += dt;
|
|
const urgentAi = this.fightTimer > 0.04 || this.intimidateTimer > 0.04 || this.intimidatedTimer > 0.04 || this.birthRitualTimer > 0.04 || this.defeatedTimer > 0.04 || this.eatTimer > 0.02 || this.hurtTimer > 0.02;
|
|
const popPerf = this.world.performanceLevel ? this.world.performanceLevel() : 0;
|
|
const aiInterval = urgentAi ? (popPerf >= 2 ? 0.105 : 0.075) : (0.18 + stableUnit(this.familyKey || this.id, "ai-interval") * 0.09 + popPerf * 0.055);
|
|
if (this.aiTimer >= aiInterval) {
|
|
const aiDt = Math.min(this.aiTimer, 0.48);
|
|
this.aiTimer = 0;
|
|
this.resolveNeeds(aiDt);
|
|
const sleepingNow = this.state === "sleep";
|
|
if (!sleepingNow) {
|
|
this.reactToFreshZunchi(aiDt);
|
|
this.updateFacing();
|
|
this.interactWithItems(aiDt);
|
|
this.interactWithOthers(aiDt);
|
|
} else {
|
|
this.updateFacing();
|
|
}
|
|
} else {
|
|
this.updateFacing();
|
|
}
|
|
this.envTimer += dt;
|
|
const envInterval = (this.world.performanceLevel && this.world.performanceLevel() >= 2) ? 0.82 : 0.48;
|
|
if (this.envTimer >= envInterval) {
|
|
const envDt = Math.min(this.envTimer, 1.2);
|
|
this.envTimer = 0;
|
|
this.applyEnvironment(envDt);
|
|
}
|
|
this.updateSunbath(dt);
|
|
this.maintainTargetProgress(dt);
|
|
this.move(dt);
|
|
if (this.ammoCollisionKnockback) this.ammoCollisionKnockback(dt);
|
|
this.updateNestBoxPresence(dt);
|
|
if (this.world.resolveFenceCollision) this.world.resolveFenceCollision(this);
|
|
const sleepingAtFrameEnd = this.state === "sleep" || this.sleeping;
|
|
if (wasSleepingAtFrameStart && !sleepingAtFrameEnd && !this.dead) this.awakeLockTimer = Math.max(this.awakeLockTimer || 0, 10);
|
|
if (this.needsInitialized && window.TarinaiNeedSystem?.deriveStressFromNeeds) this.stress = window.TarinaiNeedSystem.deriveStressFromNeeds(this.needs || {});
|
|
this.checkLife(dt);
|
|
},
|
|
|
|
applyEnvironment(dt) {
|
|
const parent = this.parentToFollow ? this.parentToFollow() : null;
|
|
if (parent && dist(this, parent) < 96) {
|
|
this.stress = clamp(this.stress - dt * 0.42, 0, 130);
|
|
this.loneliness = clamp(this.loneliness - dt * 0.32, 0, 110);
|
|
this.adjustPersonality("sociability", dt * 0.004 * this.sociabilityFriendScale(), "after staying near family.");
|
|
}
|
|
const friend = this.bestFriendLive ? this.bestFriendLive(FRIEND_AFFINITY_THRESHOLD, 92) : null;
|
|
const friendCount = this.currentFriendCount ? this.currentFriendCount(FRIEND_AFFINITY_THRESHOLD, true) : 0;
|
|
if (friend) {
|
|
this.stress = clamp(this.stress - dt * 0.36, 0, 130);
|
|
this.adjustPersonality("sociability", dt * 0.004 * this.sociabilityFriendScale(), "after staying near friends.");
|
|
} else if (friendCount <= 0 && this.loneliness > 72 && this.age > 8) {
|
|
this.adjustPersonality("sociability", -dt * 0.0018, "after having no friends.");
|
|
}
|
|
let grassComfort = 0;
|
|
for (const it of this.world.nearbyItems(this.x, this.y, 105)) {
|
|
if (it.type !== "grass" || it.dead) continue;
|
|
const d = distXY(this.x, this.y, it.x, it.y);
|
|
const lush = clamp((it.growth ?? it.amount / 120) * (it.health ?? 1), 0, 1.2);
|
|
grassComfort += clamp(1 - d / 105, 0, 1) * lush;
|
|
}
|
|
let nestComfort = 0;
|
|
for (const it of this.world.nearbyItems(this.x, this.y, 120)) {
|
|
if (!it || it.dead || it.type !== "nest_box") continue;
|
|
nestComfort += clamp(1 - distXY(this.x, this.y, it.x, it.y) / 120, 0, 1);
|
|
}
|
|
if (nestComfort > 0) this.stress = clamp(this.stress - dt * Math.min(1.15, nestComfort * 0.62), 0, 130);
|
|
if (grassComfort > 0) {
|
|
this.stress = clamp(this.stress - dt * Math.min(0.32, grassComfort * 0.08), 0, 130);
|
|
if (this.state === "panic") this.fearTimer = Math.max(0, this.fearTimer - dt * Math.min(0.22, grassComfort * 0.06));
|
|
if (this.need === "\u9759\u3051\u3055" || this.shouldApplyPersonalityBehavior("neuroticism", 1)) this.affection += dt * Math.min(0.06, grassComfort * 0.012);
|
|
}
|
|
},
|
|
|
|
bubble(text, cooldown = 2.4, color = "rgba(42,36,29,0.78)") {
|
|
if (this.world.time < this.nextBubbleAt) return;
|
|
this.nextBubbleAt = this.world.time + cooldown;
|
|
this.world.spawnBubble(this.x, this.y - this.radius * 1.35, text, color);
|
|
},
|
|
|
|
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 && (typeof isSleepFurnitureType === "function" ? isSleepFurnitureType(target.type) : (target.type === "bed" || target.type === "nest_box")));
|
|
},
|
|
|
|
sleepFurnitureLabel(target) {
|
|
return target?.type === "nest_box" ? "\u5de3\u7bb1" : "\u5e72\u8349\u5bdd\u5e8a";
|
|
},
|
|
|
|
isServingFoodItem(it) {
|
|
return Boolean(it && typeof isServingFoodType === "function" && isServingFoodType(it.type));
|
|
},
|
|
|
|
foodItemHasServingLeft(it) {
|
|
if (!it || it.dead) return false;
|
|
if (!this.isServingFoodItem(it)) return (it.amount || 0) > 1.2;
|
|
this.ensureFoodServings(it);
|
|
return (it.foodServingsRemaining || 0) > 0;
|
|
},
|
|
|
|
ensureFoodServings(it) {
|
|
if (!this.isServingFoodItem(it)) return;
|
|
const max = typeof foodServingsForSize === "function" ? foodServingsForSize(it.toolSize || "medium") : 5;
|
|
if (!Number.isFinite(it.foodServingsMax) || it.foodServingsMax <= 0) it.foodServingsMax = max;
|
|
if (!Number.isFinite(it.foodServingsRemaining)) {
|
|
const original = { food: 85, sweet: 62, love_mochi: 62, fight_mochi: 62, sleep_drug: 58, laxative: 58, protein: 58, niteropu: 58, ammo: 48, mystery_drug: 52, mercury: 44, giant_drug: 52, dwarf_drug: 52, zunda_juice: 70 }[it.type] || 62;
|
|
const ratio = clamp((it.amount ?? original) / original, 0.05, 1);
|
|
it.foodServingsRemaining = Math.max(1, Math.ceil(it.foodServingsMax * ratio));
|
|
}
|
|
it.foodServingsRemaining = clamp(it.foodServingsRemaining, 0, it.foodServingsMax);
|
|
it.amount = it.foodServingsRemaining;
|
|
},
|
|
|
|
consumeFoodServing(it, nutrition) {
|
|
this.ensureFoodServings(it);
|
|
if (!this.isServingFoodItem(it) || (it.foodServingsRemaining || 0) <= 0) return 0;
|
|
const value = Number.isFinite(nutrition) ? nutrition : (window.TarinaiFoodRegistry?.nutrition?.(it.type, 0) ?? 0);
|
|
if (!it.infiniteServing) {
|
|
it.foodServingsRemaining = Math.max(0, (it.foodServingsRemaining || 0) - 1);
|
|
it.amount = it.foodServingsRemaining;
|
|
} else {
|
|
it.foodServingsRemaining = Math.max(it.foodServingsRemaining || 0, it.foodServingsMax || 1);
|
|
it.amount = it.foodServingsRemaining;
|
|
}
|
|
this.world?.emit?.("item:eaten", { tarinai: this, item: it, type: it.type, nutrition: value });
|
|
return value;
|
|
},
|
|
|
|
foodHungerReliefFor(type = "", nutrition = 0, fallbackMultiplier = 0) {
|
|
const relief = window.TarinaiFoodRegistry?.hungerRelief?.(type, fallbackMultiplier) ?? fallbackMultiplier;
|
|
return (Number(nutrition) || 0) * relief;
|
|
},
|
|
|
|
sleepSpotFor(bed) {
|
|
if (!bed) return { x: this.x, y: this.y };
|
|
if (bed.type === "nest_box") {
|
|
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);
|
|
this.stress = clamp(this.stress + push * dt * 1.2 * this.personalityProfile().fear, 0, 130);
|
|
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 = "新鮮なずんちを避けている";
|
|
},
|
|
|
|
resolveNeeds(dt) {
|
|
const wasSleeping = this.state === "sleep";
|
|
const previousTarget = this.target && !this.target.dead ? this.target : null;
|
|
this.target = null;
|
|
this.sleeping = false;
|
|
|
|
if (this.birthRitualTimer > 0.04) {
|
|
this.setActionState?.("birth_ritual", { target: this.world.tarinai.find(o => o.id === this.birthPartnerId && !o.dead) || null, reason: "近くで体をゆすっている" });
|
|
return;
|
|
}
|
|
|
|
if (this.intimidateTimer > 0.04) {
|
|
this.setActionState?.("intimidate", { target: this.world.tarinai.find(o => o.id === this.intimidateTargetId && !o.dead) || null, reason: "相手を威嚇している" });
|
|
this.bubble("!", 1.2);
|
|
return;
|
|
}
|
|
|
|
if (this.fightTimer > 0.04) {
|
|
this.setActionState?.("fight", { reason: "喧嘩している" });
|
|
this.fightTargetIds = (this.fightTargetIds || []).filter(id => this.world.tarinai.some(t => t.id === id && !t.dead));
|
|
this.fightTargetId = this.fightTargetIds[0] || this.fightTargetId;
|
|
let best = null, bestD = Infinity;
|
|
for (const id of this.fightTargetIds) {
|
|
const t = this.world.tarinai.find(o => o.id === id && !o.dead);
|
|
if (!t) continue;
|
|
const d = dist(this, t);
|
|
if (d < bestD) { best = t; bestD = d; }
|
|
}
|
|
this.setActionState?.("fight", { target: best, reason: "喧嘩している" });
|
|
this.bubble("!", 1.6);
|
|
return;
|
|
}
|
|
|
|
if (this.defeatedTimer > 0.04) {
|
|
const opponent = this.world.tarinai.find(o => o.id === this.defeatedById && !o.dead);
|
|
if (this.enterPanic) this.enterPanic({ threat: opponent, reason: "\u55a7\u5629\u306b\u8ca0\u3051\u3066\u9003\u3052\u3066\u3044\u308b", fear: 1.0, wake: true, cause: "defeated_escape" });
|
|
else this.setActionState?.("panic", { target: this.panicDestination(opponent), reason: "\u55a7\u5629\u306b\u8ca0\u3051\u3066\u9003\u3052\u3066\u3044\u308b", wake: true });
|
|
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) {
|
|
window.TarinaiNeedSystem?.addNeedShock?.(this, { safety: 45, health: 20 }, pin);
|
|
this.hurtTimer = Math.max(this.hurtTimer || 0, 0.24);
|
|
this.awakeLockTimer = Math.max(this.awakeLockTimer || 0, 1.8);
|
|
this.bubble("!!", 0.9, "rgba(168,72,72,0.82)");
|
|
this.thought = "\u753b\u92f2\u304c\u523a\u3055\u3063\u3066\u3044\u308b";
|
|
}
|
|
this.stuckPushpinId = null;
|
|
}
|
|
|
|
if ((this.panicTimer || 0) > 0 && this.state === "panic") {
|
|
this.setActionState?.("panic", {
|
|
target: previousTarget || this.panicDestination?.(null, true) || null,
|
|
reason: this.intent?.reasonText || "\u304a\u3069\u308d\u3044\u3066\u30d1\u30cb\u30c3\u30af\u306b\u306a\u3063\u3066\u3044\u308b",
|
|
wake: true,
|
|
});
|
|
return;
|
|
}
|
|
|
|
if (window.TarinaiNeedSystem?.runNeedDecision) {
|
|
window.TarinaiNeedSystem.runNeedDecision(this, this.world, dt);
|
|
return;
|
|
}
|
|
|
|
const friendNearby = this.bestFriendLive ? this.bestFriendLive(FRIEND_AFFINITY_THRESHOLD, 170) : null;
|
|
const panicSource = (this.world?.nearbyTarinai?.(this.x, this.y, 128, true) || [])
|
|
.find(o => o && o !== this && !o.dead && !this.world?.isTarinaiHiddenInNestBox?.(o) && o.state === "panic" && (o.fearTimer || 0) > 0.12);
|
|
if (panicSource && !["panic", "sleep", "seek_bed", "fight", "birth_ritual", "eat", "sunbath"].includes(this.state)) {
|
|
const d = distXY(this.x, this.y, panicSource.x, panicSource.y);
|
|
const neuro = typeof effectivePersonalityValue === "function" ? effectivePersonalityValue(this, "neuroticism") : (this.currentPersonality?.neuroticism || 0);
|
|
const distFactor = clamp(1 - d / 128, 0, 1);
|
|
const stressFactor = clamp(((this.stress || 0) - 35) / 80, 0, 0.22);
|
|
const chance = clamp(0.10 + Math.max(0, neuro) * 0.36 + distFactor * 0.18 + stressFactor, 0.05, 0.76);
|
|
if (Math.random() < chance) {
|
|
if (this.enterPanic) this.enterPanic({ threat: panicSource.target || panicSource, reason: `${panicSource.name || "近くのたりない"}のパニックにつられている`, fear: 0.75, stress: 3 + distFactor * 5, stressThreshold: 6, cause: "nearby_panic_chain" });
|
|
else { this.setActionState?.("panic", { target: this.panicDestination(panicSource.target || panicSource), reason: `${panicSource.name || "近くのたりない"}のパニックにつられている`, wake: true }); this.fearTimer = Math.max(this.fearTimer || 0, 0.75 * this.personalityProfile().fear); this.addStress ? this.addStress(3 + distFactor * 5, { threshold: 6 }) : (this.stress = clamp((this.stress || 0) + 3 + distFactor * 5, 0, 130)); }
|
|
return;
|
|
}
|
|
}
|
|
|
|
const nearbyAnts = this.world.nearbyAnts?.(this.x, this.y, 112, true) || [];
|
|
let nearestAnt = null;
|
|
let nearestAntDist = Infinity;
|
|
for (const ant of nearbyAnts) {
|
|
const ad = distXY(this.x, this.y, ant.x, ant.y);
|
|
if (ad < nearestAntDist) {
|
|
nearestAnt = ant;
|
|
nearestAntDist = ad;
|
|
}
|
|
}
|
|
if (nearestAnt) {
|
|
const aggressiveToAnts = this.shouldApplyPersonalityBehavior("aggression", 1);
|
|
if (aggressiveToAnts && this.energy > 12 && this.fightTimer <= 0.04 && this.defeatedTimer <= 0.04) {
|
|
if (nearestAntDist < Math.max(36, this.radius + (nearestAnt.r || 4) + 8) && Math.random() < dt * 0.28) {
|
|
// Intimidation has no effect on ants. This is only a visible reaction.
|
|
this.setActionState?.("ant_intimidate", { target: nearestAnt, reason: "アリを威嚇している" });
|
|
this.bubble("!", 0.9, "rgba(145,68,52,0.78)");
|
|
return;
|
|
}
|
|
this.setActionState?.("ant_attack", { target: nearestAnt, reason: "アリを攻撃している" });
|
|
this.goodMode = "angry";
|
|
return;
|
|
}
|
|
if (this.enterPanic) this.enterPanic({ threat: nearestAnt, reason: "\u30a2\u30ea\u304b\u3089\u9003\u3052\u3066\u3044\u308b", fear: 0.34, wake: true, cause: "ant_seen" });
|
|
else this.setActionState?.("panic", { target: this.panicDestination(nearestAnt), reason: "\u30a2\u30ea\u304b\u3089\u9003\u3052\u3066\u3044\u308b", wake: true });
|
|
return;
|
|
}
|
|
|
|
if (this.canStartSunbath?.()) {
|
|
const moodBonus = (this.world?.colonyMood?.id === "sun_party") ? 0.72 : 0;
|
|
const sickBonus = (this.zunchiDisease || this.explosionDisease || this.fightDisease) ? 0.48 : 0;
|
|
const stressBonus = this.stress > 28 ? 0.22 : 0;
|
|
const idleBonus = this.state === "idle" ? 0.18 : 0;
|
|
if (Math.random() < dt * (0.34 + moodBonus + sickBonus + stressBonus + idleBonus)) {
|
|
if (this.startSunbath()) return;
|
|
}
|
|
}
|
|
|
|
const dayP = this.world.dayProgress();
|
|
const personalSleep = this.sleepStart < this.sleepEnd
|
|
? dayP >= this.sleepStart && dayP < this.sleepEnd
|
|
: dayP >= this.sleepStart || dayP < this.sleepEnd;
|
|
|
|
if (this.world.canStartFight?.(this) && this.hasFightMochiEffect() && this.fightTimer <= 0.04 && this.intimidateTimer <= 0.04 && this.defeatedTimer <= 0.04) {
|
|
const opponent = this.world.nearestOther(this, 320, o => this.world.canBeFightTarget?.(o));
|
|
if (opponent) {
|
|
const d = dist(this, opponent);
|
|
if (d <= Math.max(48, this.radius + opponent.radius + 18)) {
|
|
if (this.world.startForcedFight?.(this, opponent)) return;
|
|
}
|
|
this.setActionState?.("seek_friend", { target: opponent, reason: "けんか餅で相手を探している" });
|
|
this.goodMode = "angry";
|
|
if (Math.random() < dt * 0.24) this.bubble("!", 1.4, "rgba(160,74,45,0.78)");
|
|
return;
|
|
}
|
|
}
|
|
|
|
const recentRival = this.recentFightRival ? this.recentFightRival(60, 170) : null;
|
|
const feared = this.strongestRelation("fear");
|
|
if (!wasSleeping && (recentRival || (feared && feared.score > 12))) {
|
|
const rival = recentRival || this.world.tarinai.find(o => o.id === feared.id && !o.dead);
|
|
if (rival && dist(this, rival) < 150) {
|
|
if (this.enterPanic) this.enterPanic({ threat: rival, reason: `${rival.name}\u3092\u907f\u3051\u3066\u9060\u56de\u308a\u3057\u3066\u3044\u308b`, fear: 0.35, wake: true, cause: "avoid_rival" });
|
|
else this.setActionState?.("panic", { target: this.panicDestination(rival), reason: `${rival.name}\u3092\u907f\u3051\u3066\u9060\u56de\u308a\u3057\u3066\u3044\u308b`, wake: true });
|
|
this.world.relationNotice(this.id, rival.id, "avoid", 22);
|
|
return;
|
|
}
|
|
}
|
|
|
|
if (this.hasWaterCurableDisease?.() && this.fightTimer <= 0.04 && this.birthRitualTimer <= 0.04 && this.defeatedTimer <= 0.04) {
|
|
const water = this.world.nearest(this, ["water", "water_bowl"], 760);
|
|
if (water && !water.dead) {
|
|
this.setActionState?.("seek_water", { target: water, reason: this.explosionDisease ? "爆発病で水を探している" : "ずんち病で水を探している" });
|
|
return;
|
|
}
|
|
if (this.zunchiDisease) {
|
|
this.setActionState?.("zunchi_sick", { target: null, reason: "ずんち病で当てもなく彷徨っている" });
|
|
if (Math.random() < dt * 0.35) this.wanderAngle += rand(-2.4, 2.4);
|
|
return;
|
|
}
|
|
}
|
|
|
|
if (this.hasZundaCurableDisease?.() && this.fightTimer <= 0.04 && this.birthRitualTimer <= 0.04 && this.defeatedTimer <= 0.04) {
|
|
const cureFood = this.world.nearest(this, ["sweet"], 700);
|
|
if (cureFood && this.foodItemHasServingLeft(cureFood) && !this.shouldAvoidTarget(cureFood)) {
|
|
this.setActionState?.("seek_food", { target: cureFood, reason: this.fightDisease ? "きずつき病でずんだ餅を探している" : "爆発病でずんだ餅を探している" });
|
|
this.foodReactTimer = Math.max(this.foodReactTimer, 0.42);
|
|
return;
|
|
}
|
|
}
|
|
|
|
if (this.fightDisease && this.fightTimer <= 0.04 && this.birthRitualTimer <= 0.04 && this.defeatedTimer <= 0.04) {
|
|
this.setActionState?.("fight_sick", { target: null, reason: "きずつき病で当てもなく彷徨っている" });
|
|
if (Math.random() < dt * 0.45) this.wanderAngle += rand(-3.4, 3.4);
|
|
return;
|
|
}
|
|
|
|
if (!this.isZunchiSlave) {
|
|
const medicineTypes = window.TarinaiFoodRegistry?.typesByInterest?.("medicine_low") || ["love_mochi", "fight_mochi", "sleep_drug", "laxative", "protein", "niteropu", "ammo", "mystery_drug", "mercury", "giant_drug", "dwarf_drug", "zunda_juice"];
|
|
const specialMochi = this.world.nearest(this, medicineTypes, wasSleeping ? 190 : 155);
|
|
const specialDist = specialMochi ? distXY(this.x, this.y, specialMochi.x, specialMochi.y) : Infinity;
|
|
const weakInterest = this.hunger > (wasSleeping ? 68 : 76) && (specialDist < 72 || Math.random() < dt * 0.028);
|
|
if (specialMochi && this.foodItemHasServingLeft(specialMochi) && weakInterest && !this.shouldAvoidTarget(specialMochi)) {
|
|
this.setActionState?.("seek_food", { target: specialMochi, reason: `${toolLabel(specialMochi.type)}が少し気になっている` });
|
|
this.foodReactTimer = Math.max(this.foodReactTimer, 0.18);
|
|
return;
|
|
}
|
|
} else if (this.hunger > 20) {
|
|
const zOnly = this.world.nearest(this, ["zunchi"], 520);
|
|
if (zOnly && zOnly.amount > 1.2) {
|
|
this.setActionState?.("seek_food", { target: zOnly, reason: "ずんちを探している" });
|
|
this.foodReactTimer = Math.max(this.foodReactTimer, 0.30);
|
|
return;
|
|
}
|
|
}
|
|
|
|
const zundaRange = wasSleeping ? 540 : 430 + 70 * this.personalityProfile().zunda;
|
|
const zunda = this.world.nearest(this, ["sweet"], zundaRange);
|
|
const zundaHungry = this.hunger > Math.max(7, 18 - 10 * (this.personalityProfile().zunda - 1));
|
|
const zundaReward = this.affection > 8 && this.hunger > 7;
|
|
if (zunda && this.foodItemHasServingLeft(zunda) && ((wasSleeping && this.hunger > 7) || zundaHungry || zundaReward)) {
|
|
this.setActionState?.("seek_food", { target: zunda, reason: "ずんだ餅を探している" });
|
|
this.foodReactTimer = Math.max(this.foodReactTimer, 0.36);
|
|
return;
|
|
}
|
|
|
|
if (this.fearTimer > 0.22 || this.stress > 86 / this.personalityProfile().fear) {
|
|
if (this.enterPanic) this.enterPanic({ reason: "\u9003\u3052\u3066\u3044\u308b", fear: 0.65, wake: true, cause: "fear_or_stress" });
|
|
else this.setActionState?.("panic", { target: this.panicDestination(), reason: "\u9003\u3052\u3066\u3044\u308b", wake: true });
|
|
return;
|
|
}
|
|
|
|
const sleepBias = this.personalityProfile().sleep;
|
|
const wakeLocked = (this.awakeLockTimer || 0) > 0 && !wasSleeping;
|
|
const keepSleeping = wasSleeping && (personalSleep || this.energy < 66 + 8 * sleepBias);
|
|
if (!wakeLocked && (keepSleeping || (personalSleep && this.energy < 90 + 8 * sleepBias) || this.energy < 28 + 8 * sleepBias)) {
|
|
let bed = this.isSleepFurniture(previousTarget) ? previousTarget : null;
|
|
if (!bed && friendNearby && this.isSleepFurniture(friendNearby.target) && (friendNearby.state === "sleep" || friendNearby.state === "seek_bed")) bed = friendNearby.target;
|
|
if (!bed) bed = this.world.bestBedFor(this, wasSleeping ? 480 : 420);
|
|
if (bed && !wasSleeping && this.energy > 18) {
|
|
const spot = this.sleepSpotFor(bed);
|
|
if (distXY(this.x, this.y, spot.x, spot.y) > this.radius + 10) {
|
|
this.setActionState?.("seek_bed", { target: bed, reason: `${this.sleepFurnitureLabel(bed)}へ向かっている` });
|
|
return;
|
|
}
|
|
}
|
|
this.startSleeping?.(bed, bed ? (bed.type === "nest_box" ? "巣箱の中で眠っている" : "ベッドの近くで眠っている") : "眠っている");
|
|
if (this.world.time > this.nextSleepBubbleAt) {
|
|
this.nextSleepBubbleAt = this.world.time + 4.5;
|
|
this.world.spawnBubble(this.x, this.y - this.radius * 1.18, "Zzz...", "rgba(65,70,92,0.72)");
|
|
}
|
|
this.thought = bed ? (bed.type === "nest_box" ? "\u5de3\u7bb1\u306e\u4e2d\u3067\u7720\u3063\u3066\u3044\u308b" : "\u30d9\u30c3\u30c9\u306e\u8fd1\u304f\u3067\u7720\u3063\u3066\u3044\u308b") : "\u7720\u3063\u3066\u3044\u308b";
|
|
return;
|
|
}
|
|
|
|
const pointer = this.world.pointer;
|
|
if (pointer?.inside) {
|
|
const pd = distXY(this.x, this.y, pointer.x, pointer.y);
|
|
if (pd < 170) {
|
|
const hostile = this.mood < 38 || this.stress > 58 || this.type === "angry";
|
|
const friendly = !hostile && this.isAwakeCursorFriendly();
|
|
if (hostile) {
|
|
this.setActionState?.("cursor_enemy", { target: { x: pointer.x, y: pointer.y, dead: false }, reason: "カーソルを避けている" });
|
|
this.fearTimer = Math.max(this.fearTimer, 0.18);
|
|
if (this.world.time > this.nextFearBubbleAt) {
|
|
this.nextFearBubbleAt = this.world.time + 2.4;
|
|
this.world.spawnBubble(this.x, this.y - this.radius * 1.2, "!", "rgba(120,62,62,0.78)");
|
|
}
|
|
this.thought = "\u30ab\u30fc\u30bd\u30eb\u3092\u907f\u3051\u3066\u3044\u308b";
|
|
return;
|
|
}
|
|
if (friendly) {
|
|
this.setActionState?.("cursor_friend", { target: { x: pointer.x, y: pointer.y, dead: false }, reason: "カーソルについていく" });
|
|
this.goodMode = "smile";
|
|
this.thought = "\u30ab\u30fc\u30bd\u30eb\u306b\u3064\u3044\u3066\u3044\u304f";
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
const playTrait = this.personalityProfile().play ?? 1;
|
|
const openPlay = this.shouldApplyPersonalityBehavior("openness", 1);
|
|
const closedPlay = this.shouldApplyPersonalityBehavior("openness", -1);
|
|
const ballRange = openPlay ? 540 : (closedPlay ? 170 : 240 + 60 * playTrait);
|
|
const ball = this.world.nearest(this, ["ball"], ballRange);
|
|
if (ball && this.energy > 26 && this.hunger < 98 && !this.shouldAvoidTarget(ball)) {
|
|
const ballSpeed = Math.hypot(ball.vx || 0, ball.vy || 0);
|
|
const chasers = this.world.countBallChasers ? this.world.countBallChasers(ball, this) : 0;
|
|
const baseChance = openPlay ? 0.74 : (closedPlay ? 0.025 : 0.08);
|
|
const movingBonus = clamp(ballSpeed / 120, 0, 0.42);
|
|
const socialBonus = this.loneliness > 48 ? 0.10 : 0;
|
|
if (chasers < 5 && (openPlay || ballSpeed > 7 || Math.random() < baseChance * playTrait + movingBonus + socialBonus)) {
|
|
this.setActionState?.("play_ball", { target: ball, reason: openPlay ? "ボールに引き寄せられている" : "転がるものが気になっている" });
|
|
this.goodMode = "smile";
|
|
return;
|
|
}
|
|
}
|
|
|
|
const parent = this.parentToFollow();
|
|
if (parent && !this.shouldAvoidTarget(parent)) {
|
|
const d = dist(this, parent);
|
|
if (d > 34 || (this.hunger < 78 && this.energy > 24)) {
|
|
this.setActionState?.("follow_parent", { target: parent, reason: "親についていく" });
|
|
this.bubble("?", 4.0);
|
|
return;
|
|
}
|
|
}
|
|
|
|
if (friendNearby && this.hunger > 58 && friendNearby.target && (this.isZunchiSlave ? this.canEatItemType(friendNearby.target.type) : ["sweet", "grass", "ant_corpse"].includes(friendNearby.target.type)) && !friendNearby.target.dead && !this.shouldAvoidTarget(friendNearby.target)) {
|
|
this.setActionState?.("seek_food", { target: friendNearby.target, reason: "友達と一緒に食べに行く" });
|
|
this.foodReactTimer = Math.max(this.foodReactTimer, 0.24);
|
|
return;
|
|
}
|
|
|
|
if (this.hunger > 62) {
|
|
const itemTypes = this.isZunchiSlave ? this.zunchiSlaveFoodTypes() : ["sweet", ...((window.TarinaiFoodRegistry?.typesByInterest?.("grass_like", "corpse")) || ["grass", "ant_corpse"])];
|
|
const item = this.world.nearest(this, itemTypes, 420);
|
|
if (item) {
|
|
this.setActionState?.("seek_food", { target: item, reason: "食べ物を探している" });
|
|
this.foodReactTimer = Math.max(this.foodReactTimer, 0.30);
|
|
return;
|
|
}
|
|
}
|
|
|
|
const socialSeek = this.shouldApplyPersonalityBehavior("sociability", 1);
|
|
const lonerSeek = this.shouldApplyPersonalityBehavior("sociability", -1);
|
|
if (this.loneliness > 63 - (socialSeek ? 16 : 0) + (lonerSeek ? 10 : 0)) {
|
|
const other = this.world.nearestOther(this, socialSeek ? 390 : (lonerSeek ? 190 : 300));
|
|
if (other) {
|
|
this.setActionState?.("seek_friend", { target: other, reason: "仲間を探している" });
|
|
return;
|
|
}
|
|
}
|
|
|
|
if (this.stress > 74 / this.personalityProfile().fear || this.fearTimer > 0.22) {
|
|
if (this.enterPanic) this.enterPanic({ reason: "\u30d1\u30cb\u30c3\u30af\u306b\u306a\u3063\u3066\u3044\u308b", fear: 0.55, wake: true, cause: "stress_panic" });
|
|
else this.setActionState?.("panic", { target: this.panicDestination(), reason: "\u30d1\u30cb\u30c3\u30af\u306b\u306a\u3063\u3066\u3044\u308b", wake: true });
|
|
return;
|
|
}
|
|
|
|
if (Math.random() < dt * 0.018) this.wanderAngle += rand(-1.7, 1.7);
|
|
this.goIdle(`${this.need}がたりない`);
|
|
},
|
|
|
|
makePoop(force = 1) {
|
|
this.digest += force;
|
|
if (this.digest < 12.4) return;
|
|
if (this.hasLodgedPinEffect?.("blocksZunchi")) {
|
|
this.digest = rand(0.4, 1.6);
|
|
this.oshiriByoZunchiStock = Math.min(18, Math.max(0, this.oshiriByoZunchiStock || 0) + 1);
|
|
this.thought = (this.oshiriByoZunchiStock || 0) >= 6 ? "おしり鋲でずんちが溜まってつらい" : "おしり鋲でずんちを我慢している";
|
|
if ((this.oshiriByoZunchiStock || 0) >= 6) this.stress = clamp((this.stress || 0) + 1.8, 0, 130);
|
|
return;
|
|
}
|
|
this.digest = rand(0.4, 1.6);
|
|
this.poopCount += 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);
|
|
const now = this.world?.time || 0;
|
|
if (now >= (this.nextPoopBubbleAt || -999)) {
|
|
this.nextPoopBubbleAt = now + 2.8;
|
|
this.bubble("\u3076\u308a\u3085\u3063", 3.2, "rgba(62,84,45,0.78)");
|
|
}
|
|
if (Math.random() < 0.35) this.world.log(`${this.name}\u304c\u305a\u3093\u3061\u3092\u843d\u3068\u3057\u305f\u3002`, null, { participants: [this] });
|
|
},
|
|
|
|
interactWithItems(dt) {
|
|
const canBite = !this.isZunchiSlave && this.eatCooldown <= 0 && this.hunger > 18;
|
|
const canSweetBite = !this.isZunchiSlave && this.eatCooldown <= 0 && this.hunger > 7;
|
|
const slaveCanFoodBite = this.isZunchiSlave && this.eatCooldown <= 0 && this.hunger > 16;
|
|
const canZunchiBite = this.eatCooldown <= 0 && this.hunger > (this.isZunchiSlave ? 16 : 104);
|
|
for (const it of this.world.nearbyItems(this.x, this.y, this.radius + 42)) {
|
|
const d = dist(this, it);
|
|
if (d > this.radius + it.r + 12) continue;
|
|
|
|
if (((it.type === "sweet" && canSweetBite) || (it.type === "food" && (canBite || (slaveCanFoodBite && this.canEatItemType(it.type))))) && this.foodItemHasServingLeft(it)) {
|
|
const eating = this.consumeFoodServing(it);
|
|
if (eating > 0) {
|
|
this.beginEating(it, `${toolLabel(it.type)}を食べている`);
|
|
this.vx *= Math.pow(0.25, dt * 60);
|
|
this.vy *= Math.pow(0.25, dt * 60);
|
|
const hungerRelief = this.foodHungerReliefFor(it.type, eating, it.type === "sweet" ? 0.75 : 0.95);
|
|
this.applyFoodHungerRelief ? this.applyFoodHungerRelief(hungerRelief) : (this.hunger -= hungerRelief);
|
|
this.recoverHealth(eating * (it.type === "sweet" ? 0.28 : 0.46));
|
|
this.affection += it.type === "sweet" ? eating * 0.03 : eating * 0.012;
|
|
if (it.type === "sweet") {
|
|
this.stress = clamp(this.stress - eating * 0.42, 0, 130);
|
|
this.fearTimer = Math.max(0, this.fearTimer - eating * 0.035);
|
|
if (this.explosionDisease) this.recoverExplosionDisease("\u305a\u3093\u3060\u9905\u3067\u7206\u767a\u75c5\u304c\u6cbb\u3063\u305f");
|
|
if (this.fightDisease) this.recoverFightDisease("\u305a\u3093\u3060\u9905\u3067\u304d\u305a\u3064\u304d\u75c5\u304c\u6cbb\u3063\u305f");
|
|
}
|
|
this.eatTimer = Math.max(this.eatTimer, 0.32);
|
|
this.eatCooldown = it.type === "sweet" ? rand(0.34, 0.62) : rand(0.44, 0.78);
|
|
this.bubble("\u3082\u3050", 2.0, "rgba(88,70,42,0.78)");
|
|
this.foodReactTimer = Math.max(this.foodReactTimer, 0.16);
|
|
if (Math.random() < dt * 2.2) {
|
|
const mouth = this.mouthPosition(it);
|
|
this.world.spawnEatEffect(mouth.x, mouth.y, it.type === "sweet" ? "#78b957" : "#f1dfb7");
|
|
}
|
|
this.lastMealColor = it.type === "sweet" ? "#78b957" : "#f1dfb7";
|
|
this.makePoop(eating * (it.type === "sweet" ? 0.18 : 0.22));
|
|
if (this.world.time > this.nextEatSound) { audio.eat(); this.nextEatSound = this.world.time + 0.42; }
|
|
if (this.world.time - this.lastLog > 9 && Math.random() < 0.014) {
|
|
this.world.log(`${this.name}\u306f${it.type === "sweet" ? "\u305a\u3093\u3060\u9905" : "\u98df\u3079\u7269"}\u3092\u3057\u3070\u3089\u304f\u5473\u308f\u3063\u305f\u3002`, null, { participants: [this] });
|
|
this.lastLog = this.world.time;
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
|
|
if ((canSweetBite || (slaveCanFoodBite && this.canEatItemType(it.type))) && (it.type === "love_mochi" || it.type === "fight_mochi" || it.type === "sleep_drug" || (typeof isParamEffectItemType === "function" && isParamEffectItemType(it.type))) && this.foodItemHasServingLeft(it)) {
|
|
const eating = this.consumeFoodServing(it);
|
|
if (eating <= 0) continue;
|
|
this.beginEating(it, `${toolLabel(it.type)}を食べている`);
|
|
this.vx *= Math.pow(0.25, dt * 60);
|
|
this.vy *= Math.pow(0.25, dt * 60);
|
|
const hungerRelief = this.foodHungerReliefFor(it.type, eating, 0.82);
|
|
this.applyFoodHungerRelief ? this.applyFoodHungerRelief(hungerRelief) : (this.hunger -= hungerRelief);
|
|
this.recoverHealth(eating * 0.24);
|
|
this.affection += eating * 0.04;
|
|
const mealColors = {
|
|
love_mochi: "#ff7bab", fight_mochi: "#e07c43", sleep_drug: "#b89cff",
|
|
protein: "#f28d3d", niteropu: "#6255a8", ammo: "#d0942f", laxative: "#788c3b",
|
|
mercury: "#6e95a8", giant_drug: "#e47b3a", dwarf_drug: "#7773c8", zunda_juice: "#61b94f",
|
|
};
|
|
const mealBubbles = {
|
|
love_mochi: ["\u2661", 2.4, "rgba(190,82,132,0.78)"],
|
|
fight_mochi: ["!", 2.0, "rgba(180,86,52,0.80)"],
|
|
sleep_drug: ["Zzz...", 2.3, "rgba(104,84,168,0.78)"],
|
|
};
|
|
if (this.applyParamItemEffect) this.applyParamItemEffect(it.type, it, {
|
|
source: "eating",
|
|
nutrition: eating,
|
|
silentLog: it.type === "love_mochi" || it.type === "fight_mochi" || it.type === "sleep_drug",
|
|
});
|
|
if (mealBubbles[it.type]) this.bubble(...mealBubbles[it.type]);
|
|
this.lastMealColor = mealColors[it.type] || "#41aa66";
|
|
this.eatTimer = Math.max(this.eatTimer, 0.30);
|
|
this.eatCooldown = rand(0.38, 0.68);
|
|
this.foodReactTimer = Math.max(this.foodReactTimer, 0.16);
|
|
if (Math.random() < dt * 2.2) {
|
|
const mouth = this.mouthPosition(it);
|
|
this.world.spawnEatEffect(mouth.x, mouth.y, it.type === "love_mochi" ? "#ff7bab" : (it.type === "fight_mochi" ? "#e07c43" : (it.type === "sleep_drug" ? "#b89cff" : (this.lastMealColor || "#41aa66"))));
|
|
}
|
|
this.makePoop(eating * 0.20);
|
|
if (this.world.time > this.nextEatSound) { audio.eat(); this.nextEatSound = this.world.time + 0.42; }
|
|
if (this.world.time - this.lastLog > 9 && Math.random() < 0.02) {
|
|
this.world.log(`${this.name}\u306f${toolLabel(it.type)}\u3092\u3057\u3070\u3089\u304f\u5473\u308f\u3063\u305f\u3002`, "food", { participants: [this] });
|
|
this.lastLog = this.world.time;
|
|
}
|
|
break;
|
|
}
|
|
|
|
if (canBite && it.type === "grass") {
|
|
const servingScale = Number.isFinite(it.foodServingScale) ? it.foodServingScale : (it.toolSize === "large" ? 1.18 : (it.toolSize === "small" ? 0.82 : 1));
|
|
const bite = Math.min(it.amount, rand(4.2, 7.2) * servingScale);
|
|
this.beginEating(it, "草を食べている");
|
|
this.grassEatTimer = Math.max(this.grassEatTimer, 0.22);
|
|
this.vx *= Math.pow(0.20, dt * 60);
|
|
this.vy *= Math.pow(0.20, dt * 60);
|
|
if (!it.infiniteServing) {
|
|
it.amount -= bite;
|
|
it.eatenAmount = (it.eatenAmount || 0) + bite;
|
|
it.growth = clamp((it.growth ?? it.amount / 120) - bite / 150, 0.04, 1.2);
|
|
} else {
|
|
it.amount = Math.max(it.amount || 0, 120);
|
|
it.growth = Math.max(it.growth || 0, 0.82);
|
|
}
|
|
this.applyFoodHungerRelief ? this.applyFoodHungerRelief(bite * 0.46) : (this.hunger -= bite * 0.46);
|
|
this.recoverHealth(bite * 0.18);
|
|
this.stress -= dt * 0.5;
|
|
this.eatTimer = Math.max(this.eatTimer, 0.22);
|
|
this.eatCooldown = rand(0.68, 1.15);
|
|
this.bubble("\u3082\u3050", 2.4, "rgba(72,96,48,0.78)");
|
|
if (Math.random() < dt * 2.0) {
|
|
const mouth = this.mouthPosition(it);
|
|
this.world.spawnEatEffect(mouth.x, mouth.y, "#5b9d39");
|
|
}
|
|
if (this.world.time > this.nextEatSound) { audio.eat(); this.nextEatSound = this.world.time + 0.55; }
|
|
this.makePoop(bite * 0.10);
|
|
if (this.type === "jito") this.affection += dt * 0.25;
|
|
break;
|
|
}
|
|
|
|
if ((canBite || (slaveCanFoodBite && this.canEatItemType(it.type))) && it.type === "ant_corpse") {
|
|
const bite = Math.min(it.amount, rand(3.2, 5.4));
|
|
if (bite > 0) {
|
|
this.beginEating(it, "アリの死骸を食べている");
|
|
this.vx *= Math.pow(0.18, dt * 60);
|
|
this.vy *= Math.pow(0.18, dt * 60);
|
|
if (!it.infiniteServing) it.amount -= bite;
|
|
else it.amount = Math.max(it.amount || 0, 24);
|
|
this.applyFoodHungerRelief ? this.applyFoodHungerRelief(bite * 0.62) : (this.hunger -= bite * 0.62);
|
|
this.recoverHealth(bite * 0.12);
|
|
this.stress = clamp(this.stress + bite * 0.05 * this.personalityProfile().fear, 0, 130);
|
|
this.eatTimer = Math.max(this.eatTimer, 0.26);
|
|
this.eatCooldown = rand(0.75, 1.25);
|
|
this.bubble("\u3082\u3050", 2.2, "rgba(72,60,48,0.78)");
|
|
const mouth = this.mouthPosition(it);
|
|
this.world.spawnEatEffect(mouth.x, mouth.y, "#4a3f36");
|
|
this.lastMealColor = "#4a3f36";
|
|
this.makePoop(bite * 0.08);
|
|
if (this.world.time > this.nextEatSound) { audio.eat(); this.nextEatSound = this.world.time + 0.62; }
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (it.type === "zunchi") {
|
|
const touch = clamp(1 - d / (this.radius + it.r + 12), 0, 1);
|
|
if (!this.isZunchiSlave) {
|
|
this.stress = clamp(this.stress + touch * dt * 3.4 * this.personalityProfile().fear, 0, 130);
|
|
if (touch > 0.08) this.adjustPersonality("neuroticism", -dt * touch * 0.0045, "after repeated contact with poop.");
|
|
}
|
|
this.zunchiStain = clamp((this.zunchiStain || 0) + touch * dt * (this.isZunchiSlave ? 5.2 : 9.5), 0, 100);
|
|
if (canZunchiBite) {
|
|
const servingScale = Number.isFinite(it.foodServingScale) ? it.foodServingScale : (it.toolSize === "large" ? 1.18 : (it.toolSize === "small" ? 0.82 : 1));
|
|
const bite = Math.min(it.amount, (this.isZunchiSlave ? rand(4.8, 7.4) : rand(3.4, 5.8)) * servingScale);
|
|
this.beginEating(it, this.isZunchiSlave ? "ずんちどれいなのでずんちを食べている" : "ずんちを食べている");
|
|
this.vx *= Math.pow(0.18, dt * 60);
|
|
this.vy *= Math.pow(0.18, dt * 60);
|
|
if (!it.infiniteServing) it.amount -= bite;
|
|
else it.amount = Math.max(it.amount || 0, 240);
|
|
this.applyFoodHungerRelief ? this.applyFoodHungerRelief(bite * (this.isZunchiSlave ? 0.66 : 0.18)) : (this.hunger -= bite * (this.isZunchiSlave ? 0.66 : 0.18));
|
|
this.recoverHealth(bite * (this.isZunchiSlave ? 0.10 : 0.02));
|
|
this.stress = clamp(this.stress + bite * (this.isZunchiSlave ? 0.06 : 0.52) * this.personalityProfile().fear, 0, 130);
|
|
if (!this.isZunchiSlave) this.adjustPersonality("neuroticism", -bite * 0.0015, "after repeated contact with poop.");
|
|
this.zunchiStain = clamp((this.zunchiStain || 0) + bite * (this.isZunchiSlave ? 0.75 : 1.75), 0, 100);
|
|
this.lastMealColor = "#6b8d3f";
|
|
this.eatTimer = Math.max(this.eatTimer, 0.24);
|
|
this.eatCooldown = rand(0.9, 1.4);
|
|
this.bubble(this.isZunchiSlave ? "\u3082\u3050" : "...", 2.2, "rgba(74,91,50,0.78)");
|
|
const mouth = this.mouthPosition(it);
|
|
this.world.spawnEatEffect(mouth.x, mouth.y, "#6b8d3f");
|
|
if (this.world.time > this.nextEatSound) { audio.eat(); this.nextEatSound = this.world.time + 0.7; }
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (it.type === "water" || it.type === "water_bowl") {
|
|
this.thought = it.type === "water_bowl" ? "水の皿で水を飲んでいる" : "水を浴びて落ち着いている";
|
|
this.target = it;
|
|
this.applyWaterEffect(dt, it.type);
|
|
if ((this.world?.time || 0) > (this.nextDrinkSound || -999)) {
|
|
audio.play?.("sfx_drink", { category: "ops", minGap: 0.20 });
|
|
this.nextDrinkSound = (this.world?.time || 0) + 0.72;
|
|
}
|
|
if (it.type === "water" && !it.infiniteServing) it.amount -= dt * 2.4;
|
|
if (it.type === "water" && it.infiniteServing) it.amount = Math.max(it.amount || 0, 120);
|
|
if (this.type === "teary" || this.type === "cry") this.affection += dt * 0.18;
|
|
}
|
|
|
|
if (it.type === "stone") {
|
|
if (this.state === "idle" && (this.need === "あたたかさ" || this.stress > 48)) this.thought = "石の近くで落ち着いている";
|
|
this.stress -= dt * 0.85;
|
|
this.energy += dt * 0.18;
|
|
if (this.need === "\u3042\u305f\u305f\u304b\u3055") this.affection += dt * 0.25;
|
|
}
|
|
|
|
if (this.isSleepFurniture(it)) {
|
|
if (this.energy < 74 + 6 * this.personalityProfile().sleep) {
|
|
const occ = this.world.bedOccupancy(it);
|
|
const comfort = this.world.bedComfort(it);
|
|
this.startSleeping?.(it, it.type === "nest_box" ? "巣箱の中で休んでいる" : "寝床の近くで休んでいる");
|
|
if (this.world.time > this.nextSleepBubbleAt) {
|
|
this.nextSleepBubbleAt = this.world.time + 4.5;
|
|
const crowded = occ > 5;
|
|
this.world.spawnBubble(this.x, this.y - this.radius * 1.18, "Zzz...", "rgba(65,70,92,0.72)");
|
|
}
|
|
this.energy += dt * (1.9 + comfort * 1.4);
|
|
this.stress += dt * (occ > 5 ? 0.55 : -1.3 * comfort);
|
|
it.wear = clamp((it.wear || 0) + dt * 0.0008, 0, 1);
|
|
}
|
|
}
|
|
|
|
if (it.type === "ball") {
|
|
const touch = clamp(1 - d / (this.radius + it.r + 12), 0, 1);
|
|
if (this.state === "play_ball" || this.target === it || this.shouldApplyPersonalityBehavior("openness", 1)) {
|
|
this.goodMode = "smile";
|
|
this.stress = clamp(this.stress - dt * (0.9 + touch * 1.8), 0, 130);
|
|
this.loneliness = clamp(this.loneliness - dt * (0.4 + touch), 0, 110);
|
|
if (this.state === "play_ball") this.foodReactTimer = Math.max(this.foodReactTimer, 0.10);
|
|
}
|
|
}
|
|
|
|
if (it.type === "splat") {
|
|
const touch = clamp(1 - d / (this.radius + it.r + 16), 0, 1);
|
|
if (touch > 0) {
|
|
this.stress = clamp(this.stress + touch * dt * 2.8 * this.personalityProfile().fear, 0, 130);
|
|
if (touch > 0.06) this.adjustPersonality("neuroticism", -dt * touch * 0.0042, "after repeated contact with corpses");
|
|
}
|
|
}
|
|
|
|
if (it.type === "trace") {
|
|
this.loneliness -= dt * 0.25;
|
|
this.stress += dt * 0.06 * this.personalityProfile().fear;
|
|
}
|
|
}
|
|
this.hunger = clamp(this.hunger, 0, 115);
|
|
this.energy = clamp(this.energy, 0, 100);
|
|
this.stress = clamp(this.stress, 0, 130);
|
|
this.loneliness = clamp(this.loneliness, 0, 110);
|
|
}
|
|
}));
|
|
})(typeof window !== "undefined" ? window : globalThis);
|