tarinai/js/tarinai.js

2540 lines
120 KiB
JavaScript
Raw Normal View History

2026-06-19 20:52:40 +09:00
"use strict";
2026-06-19 14:00:48 +09:00
2026-06-20 02:07:01 +09:00
function tarinaiRoundRectPath(ctx, x, y, w, h, r) {
const rr = Math.max(0, Math.min(r || 0, Math.abs(w) / 2, Math.abs(h) / 2));
ctx.beginPath();
ctx.moveTo(x + rr, y);
ctx.lineTo(x + w - rr, y);
ctx.quadraticCurveTo(x + w, y, x + w, y + rr);
ctx.lineTo(x + w, y + h - rr);
ctx.quadraticCurveTo(x + w, y + h, x + w - rr, y + h);
ctx.lineTo(x + rr, y + h);
ctx.quadraticCurveTo(x, y + h, x, y + h - rr);
ctx.lineTo(x, y + rr);
ctx.quadraticCurveTo(x, y, x + rr, y);
}
2026-06-19 14:00:48 +09:00
class Tarinai {
constructor(world, opts = {}) {
this.world = world;
2026-06-20 23:27:39 +09:00
this.familyKey = opts.familyKey || opts.archiveKey || opts.id || (crypto.randomUUID ? crypto.randomUUID() : `${Date.now()}-${Math.random()}`);
this.id = opts.id || "";
this.liveToken = opts.liveToken || 0;
2026-06-19 14:00:48 +09:00
this.name = opts.name || makeName(world);
2026-06-20 20:29:44 +09:00
// Legacy string personality is intentionally ignored. The game now uses only the four-axis personality model.
this.personality = "";
2026-06-19 14:00:48 +09:00
this.type = opts.type || baseSpriteId();
const typeMeta = SPRITES.find(s => s.id === this.type);
if (typeMeta?.actionOnly || typeMeta?.displayOnly) this.type = baseSpriteId();
2026-06-20 20:29:44 +09:00
this.birthPersonality = normalizePersonality(opts.birthPersonality || opts.personalityBirth);
2026-06-20 23:27:39 +09:00
if (!opts.birthPersonality && !opts.personalityBirth) this.birthPersonality = randomBirthPersonality(this.familyKey);
2026-06-20 20:29:44 +09:00
this.currentPersonality = normalizePersonality(opts.currentPersonality || opts.personalityCurrent || this.birthPersonality);
this.personalityDaily = opts.personalityDaily && typeof opts.personalityDaily === "object" ? JSON.parse(JSON.stringify(opts.personalityDaily)) : null;
ensurePersonality(this);
2026-06-19 14:00:48 +09:00
this.trait = makeTrait(this.type);
this.x = opts.x ?? rand(100, world.w - 100);
this.y = opts.y ?? rand(100, world.h - 100);
this.vx = opts.vx ?? rand(-12, 12);
this.vy = opts.vy ?? rand(-12, 12);
2026-06-20 20:29:44 +09:00
this.impulseVx = opts.impulseVx ?? 0;
this.impulseVy = opts.impulseVy ?? 0;
2026-06-19 14:00:48 +09:00
this.scale = opts.scale ?? rand(0.22, 0.33);
this.radius = 56 * this.scale;
this.age = opts.age ?? rand(0, 20);
this.lifeSpan = opts.lifeSpan ?? rand(1400, 2200);
if (this.lifeSpan < 1200) this.lifeSpan = rand(1400, 2200);
this.generation = opts.generation ?? 1;
this.hasPaired = opts.hasPaired ?? false;
this.hunger = opts.hunger ?? rand(16, 45);
this.loneliness = opts.loneliness ?? rand(18, 62);
this.energy = opts.energy ?? rand(54, 98);
this.stress = opts.stress ?? rand(0, 20);
this.hpBarTimer = opts.hpBarTimer ?? 0;
2026-06-19 23:22:15 +09:00
this.stressBarTimer = opts.stressBarTimer ?? 0;
2026-06-19 14:00:48 +09:00
this.affection = opts.affection ?? rand(0, 30);
this.need = opts.need || pick(NEEDS);
this.state = opts.state || "idle";
2026-06-19 20:52:40 +09:00
this.dead = !!opts.dead;
this.deathReason = opts.deathReason || "";
2026-06-19 23:22:15 +09:00
this.lastDamageCause = opts.lastDamageCause || "";
this.lastDamageAmount = opts.lastDamageAmount ?? 0;
this.lastDamageAt = opts.lastDamageAt ?? -999;
this.lastMajorDamageCause = opts.lastMajorDamageCause || "";
this.lastMajorDamageAmount = opts.lastMajorDamageAmount ?? 0;
this.lastMajorDamageAt = opts.lastMajorDamageAt ?? -999;
2026-06-19 14:00:48 +09:00
this.thought = "";
this.target = null;
this.targetKey = "";
this.targetSince = 0;
this.targetBestDist = Infinity;
this.targetGiveupKey = opts.targetGiveupKey || "";
this.targetGiveupUntil = opts.targetGiveupUntil ?? 0;
this.panicTarget = opts.panicTarget || null;
this.panicTargetUntil = opts.panicTargetUntil ?? 0;
this.panicStuckTimer = opts.panicStuckTimer ?? 0;
this.wanderAngle = rand(0, Math.PI * 2);
this.reproductionTimer = rand(4, CONFIG.reproductionCooldown);
this.lastLog = 0;
this.sleeping = false;
this.blink = rand(0, 10);
this.lastSocial = 0;
2026-06-20 23:27:39 +09:00
this.goodMode = opts.goodMode ?? stableChoice(this.familyKey, "good-mode", displayNormalSprites()) ?? "smile";
this.facing = opts.facing ?? (stableUnit(this.familyKey, "facing") < 0.5 ? -1 : 1);
2026-06-19 14:00:48 +09:00
this.visualFacing = opts.visualFacing ?? this.facing;
this.facingLockUntil = opts.facingLockUntil ?? 0;
this.eatTimer = opts.eatTimer ?? 0;
this.eatCooldown = 0;
this.foodReactTimer = opts.foodReactTimer ?? 0;
this.surpriseTimer = opts.surpriseTimer ?? 0;
this.fearTimer = opts.fearTimer ?? 0;
this.intimidateTimer = opts.intimidateTimer ?? 0;
this.intimidateTargetId = opts.intimidateTargetId || null;
this.intimidatedTimer = opts.intimidatedTimer ?? 0;
this.birthRitualTimer = opts.birthRitualTimer ?? 0;
this.birthRitualMax = opts.birthRitualMax ?? 0;
this.birthPartnerId = opts.birthPartnerId || null;
this.birthRitualRole = opts.birthRitualRole || 0;
this.birthRitualLeader = !!opts.birthRitualLeader;
this.pokeFlashTimer = opts.pokeFlashTimer ?? 0;
2026-06-19 20:52:40 +09:00
this.zunchiStain = opts.zunchiStain ?? 0;
2026-06-20 23:27:39 +09:00
this.zunchiStainSeed = opts.zunchiStainSeed ?? stableUnit(this.familyKey, "zunchi-stain");
2026-06-19 20:52:40 +09:00
this.zunchiDisease = Boolean(opts.zunchiDisease);
this.zunchiDiseaseSeverity = opts.zunchiDiseaseSeverity ?? (this.zunchiDisease ? 1 : 0);
this.zunchiDiseaseCooldown = opts.zunchiDiseaseCooldown ?? 0;
this.totalFightLosses = opts.totalFightLosses || 0;
2026-06-20 15:55:52 +09:00
this.totalFightWins = opts.totalFightWins || 0;
2026-06-19 20:52:40 +09:00
this.isZunchiSlave = Boolean(opts.isZunchiSlave);
this.formerName = opts.formerName || null;
this.loveMochiTimer = opts.loveMochiTimer ?? 0;
this.fightMochiTimer = opts.fightMochiTimer ?? 0;
2026-06-20 02:07:01 +09:00
this.sleepDisease = Boolean(opts.sleepDisease);
this.sleepDiseaseCooldown = opts.sleepDiseaseCooldown ?? 0;
this.explosionDisease = Boolean(opts.explosionDisease);
this.explosionDiseaseTimer = opts.explosionDiseaseTimer ?? (this.explosionDisease ? (CONFIG.dayLength || 120) * 0.5 : 0);
this.fightDisease = Boolean(opts.fightDisease);
this.fightDiseaseCooldown = opts.fightDiseaseCooldown ?? 0;
this.lastBleedAt = opts.lastBleedAt ?? -999;
this.favorite = Boolean(opts.favorite);
2026-06-20 20:29:44 +09:00
this.insideNestBoxId = opts.insideNestBoxId || null;
this.nestFade = opts.nestFade ?? (this.insideNestBoxId ? 1 : 0);
this.nestBoxEnteredAt = opts.nestBoxEnteredAt ?? (this.insideNestBoxId ? (world?.time || 0) : -Infinity);
this.nestBoxStayUntil = opts.nestBoxStayUntil ?? (this.insideNestBoxId ? ((world?.time || 0) + 10) : -Infinity);
this.focusPulseTimer = opts.focusPulseTimer ?? 0;
2026-06-19 23:22:15 +09:00
if (this.isZunchiSlave) this.name = "\u305a\u3093\u3061\u3069\u308c\u3044";
2026-06-19 14:00:48 +09:00
this.tempComfort = opts.tempComfort ?? 0.62;
2026-06-20 23:27:39 +09:00
this.tempTimer = opts.tempTimer ?? stableUnit(this.familyKey, "temp-start") * 0.55;
2026-06-19 14:00:48 +09:00
this.parents = opts.parents || [];
this.parentNames = opts.parentNames || [];
this.children = opts.children || [];
this.birthTime = opts.birthTime ?? this.world.time;
this.nextEatSound = opts.nextEatSound ?? 0;
this.fightTimer = opts.fightTimer ?? 0;
this.fightCooldown = opts.fightCooldown ?? rand(0, CONFIG.fightCooldown);
this.fightTargetId = opts.fightTargetId || null;
this.fightTargetIds = Array.isArray(opts.fightTargetIds) ? [...opts.fightTargetIds] : (this.fightTargetId ? [this.fightTargetId] : []);
this.nextHeadbutt = opts.nextHeadbutt ?? 0;
this.digest = opts.digest ?? rand(0, 2.4);
this.poopCount = opts.poopCount ?? 0;
this.stretchTimer = opts.stretchTimer ?? 0;
this.lastMealColor = opts.lastMealColor || "#f7cf67";
this.grassEatTimer = opts.grassEatTimer ?? 0;
this.hurtTimer = opts.hurtTimer ?? 0;
this.defeatedTimer = opts.defeatedTimer ?? 0;
this.defeatedById = opts.defeatedById || null;
this.fightWinnerId = opts.fightWinnerId || null;
this.relationships = opts.relationships && typeof opts.relationships === "object" ? JSON.parse(JSON.stringify(opts.relationships)) : {};
this.fallTimer = opts.fallTimer ?? 0;
this.fallMax = opts.fallMax ?? 1.15;
2026-06-20 23:27:39 +09:00
this.fallDir = opts.fallDir ?? (stableUnit(this.familyKey, "fall-dir") < 0.5 ? -1 : 1);
2026-06-19 14:00:48 +09:00
this.blastSpinTimer = opts.blastSpinTimer ?? 0;
this.blastSpinMax = opts.blastSpinMax ?? 0;
this.postBirthPeaceTimer = opts.postBirthPeaceTimer ?? 0;
2026-06-20 23:27:39 +09:00
this.sleepStart = 0.60 + stableUnit(this.familyKey, "sleep-start") * 0.22;
this.sleepEnd = 0.15 + stableUnit(this.familyKey, "sleep-end") * 0.14;
this.sleepFacing = stableUnit(this.familyKey, "sleep-facing") < 0.5 ? -1 : 1;
2026-06-19 14:00:48 +09:00
this.nextBubbleAt = 0;
this.nextSleepBubbleAt = 0;
this.nextFearBubbleAt = 0;
2026-06-19 20:52:40 +09:00
this.nextCursorHeartAt = opts.nextCursorHeartAt ?? 0;
this.cursorPetting = opts.cursorPetting ?? 0;
2026-06-20 23:27:39 +09:00
this.aiTimer = opts.aiTimer ?? stableUnit(this.familyKey, "ai-start") * 0.22;
this.envTimer = opts.envTimer ?? stableUnit(this.familyKey, "env-start") * 0.55;
2026-06-19 14:00:48 +09:00
this.visibleSpriteId = opts.visibleSpriteId || null;
this.spriteLockUntil = opts.spriteLockUntil ?? 0;
2026-06-19 20:52:40 +09:00
this.entryTimer = opts.entryTimer ?? 0;
2026-06-19 14:00:48 +09:00
}
get lack() {
return clamp((this.hunger + this.loneliness + this.stress + (100 - this.energy)) / 4, 0, 100);
}
get mood() {
return clamp(100 - (this.hunger * 0.40 + this.loneliness * 0.20 + this.stress * 0.40), 0, 100);
}
get ageSinceBirth() {
return Math.max(0, this.world.time - this.birthTime);
}
get growth() {
return clamp(this.ageSinceBirth / CONFIG.childGrowTime, 0, 1);
}
get juvenile() {
return this.generation > 1 && this.growth < 1;
}
parentToFollow() {
if (!this.juvenile || !this.parents?.length) return null;
let best = null, bestD = Infinity;
for (const id of this.parents) {
2026-06-20 23:27:39 +09:00
const p = this.world.tarinai.find(t => (t.familyKey || t.id) === id && !t.dead);
2026-06-19 14:00:48 +09:00
if (!p) continue;
const d = dist(this, p);
if (d < bestD) { best = p; bestD = d; }
}
return best;
}
label() {
const sp = SPRITES.find(s => s.id === this.type);
return sp ? sp.label : this.type;
}
2026-06-20 02:07:01 +09:00
2026-06-20 20:29:44 +09:00
addRecord(text, kind = "note", options = {}) {
2026-06-20 02:07:01 +09:00
if (!text) return;
2026-06-20 20:29:44 +09:00
if (options.hiddenFromObservation) return;
2026-06-20 02:07:01 +09:00
if (!Array.isArray(this.records)) this.records = [];
const entry = { time: this.world?.time || 0, text: String(text), kind: kind || "note" };
const head = this.records[0];
if (head && head.text === entry.text && Math.abs((head.time || 0) - entry.time) < 0.05) return;
this.records.unshift(entry);
if (this.records.length > 42) this.records.length = 42;
}
2026-06-19 14:00:48 +09:00
personalityProfile() {
2026-06-20 20:29:44 +09:00
const base = { label: "\u4e2d\u7acb", fear: 1, fight: 1, social: 1, relation: 1, sleep: 1, zunda: 1, play: 1 };
const p = ensurePersonality(this).currentPersonality;
const e = {
neuroticism: personalityEffectValue(p.neuroticism),
aggression: personalityEffectValue(p.aggression),
sociability: personalityEffectValue(p.sociability),
openness: personalityEffectValue(p.openness),
};
return {
...base,
fear: clamp((base.fear || 1) * (1 + e.neuroticism * 0.38), 0.55, 1.85),
fight: clamp((base.fight || 1) * (1 + e.aggression * 0.55), 0.35, 2.35),
social: clamp((base.social || 1) * (1 + e.sociability * 0.24), 0.62, 1.52),
relation: clamp((base.relation || 1) * (1 + e.sociability * 0.24), 0.62, 1.60),
play: clamp((base.play || 1) * (1 + e.openness * 0.45), 0.45, 2.10),
};
2026-06-19 14:00:48 +09:00
}
personalityLabel() {
2026-06-20 20:29:44 +09:00
const tags = getPersonalityTraitTags(this);
return tags.length ? tags.join(" / ") : "\u4e2d\u7acb";
2026-06-19 14:00:48 +09:00
}
2026-06-20 20:29:44 +09:00
personalityTags() { return getPersonalityTraitTags(this); }
adjustPersonality(key, delta, reason = "") { return adjustPersonality(this, key, delta, reason); }
shouldApplyPersonalityBehavior(key, direction = 1) { return shouldApplyPersonalityBehavior(this, key, direction); }
2026-06-20 15:55:52 +09:00
fightWinCount() {
return this.totalFightWins || 0;
}
fightLossCount() {
return this.totalFightLosses || 0;
}
maybeBecomeTimidAfterDamage(cause = "") {
2026-06-20 20:29:44 +09:00
if (this.dead) return false;
2026-06-20 15:55:52 +09:00
const losses = this.totalFightLosses || 0;
const wins = this.totalFightWins || 0;
if (losses <= wins) return false;
if (Math.random() >= 0.05) return false;
2026-06-20 20:29:44 +09:00
const changedA = this.adjustPersonality?.("neuroticism", 0.025, "after being hurt") || 0;
const changedB = this.adjustPersonality?.("aggression", -0.015, "after being hurt") || 0;
2026-06-20 15:55:52 +09:00
this.fearTimer = Math.max(this.fearTimer || 0, 1.2);
this.stress = clamp((this.stress || 0) + 5, 0, 130);
2026-06-20 20:29:44 +09:00
return Boolean(changedA || changedB);
2026-06-20 15:55:52 +09:00
}
2026-06-19 14:00:48 +09:00
relationTo(id) {
if (!id) return relationDefaults();
if (!this.relationships) this.relationships = {};
if (!this.relationships[id]) this.relationships[id] = relationDefaults();
return this.relationships[id];
}
adjustRelation(other, affinityDelta = 0, fearDelta = 0, event = "") {
if (!other || !other.id || other === this) return;
const rel = this.relationTo(other.id);
rel.affinity = clamp((rel.affinity || 0) + affinityDelta, -24, 40);
rel.fear = clamp((rel.fear || 0) + fearDelta, 0, 40);
rel.fightsWon = rel.fightsWon || 0;
rel.fightsLost = rel.fightsLost || 0;
if (event) { rel.lastEvent = event; rel.lastTime = this.world.time; }
}
2026-06-20 23:27:39 +09:00
strongestRelation(kind = "friend", liveOnly = true) {
2026-06-19 14:00:48 +09:00
let bestId = null;
2026-06-20 20:29:44 +09:00
let bestScore = kind === "fear" ? 5 : FRIEND_AFFINITY_THRESHOLD;
2026-06-20 23:27:39 +09:00
const liveIds = liveOnly ? new Set((this.world?.tarinai || []).filter(o => o && !o.dead && o !== this).map(o => o.id).filter(Boolean)) : null;
2026-06-19 14:00:48 +09:00
for (const [id, rel] of Object.entries(this.relationships || {})) {
2026-06-20 23:27:39 +09:00
if (liveIds && !liveIds.has(id)) continue;
2026-06-19 14:00:48 +09:00
const score = kind === "fear" ? (rel.fear || 0) : (rel.affinity || 0);
if (score > bestScore) { bestId = id; bestScore = score; }
}
return bestId ? { id: bestId, score: bestScore, name: relationDisplayName(this.world, bestId) } : null;
}
relationSummary() {
const friend = this.strongestRelation("friend");
const fear = this.strongestRelation("fear");
return { friend, fear };
}
2026-06-20 20:29:44 +09:00
currentFriendCount(minScore = FRIEND_AFFINITY_THRESHOLD, liveOnly = true) {
let count = 0;
const liveIds = liveOnly ? new Set((this.world?.tarinai || []).filter(o => o && !o.dead && o !== this).map(o => o.id)) : null;
for (const [id, rel] of Object.entries(this.relationships || {})) {
if ((rel.affinity || 0) <= minScore) continue;
if (liveIds && !liveIds.has(id)) continue;
count += 1;
}
return count;
}
sociabilityFriendScale() {
const friends = this.currentFriendCount ? this.currentFriendCount(FRIEND_AFFINITY_THRESHOLD, true) : 0;
return clamp(0.82 + Math.min(6, friends) * 0.16, 0.82, 1.78);
}
bestFriendLive(minScore = FRIEND_AFFINITY_THRESHOLD, maxDist = Infinity) {
2026-06-19 14:00:48 +09:00
let best = null, bestScore = minScore;
for (const [id, rel] of Object.entries(this.relationships || {})) {
const score = rel.affinity || 0;
if (score <= bestScore) continue;
const t = this.world.tarinai.find(o => o.id === id && !o.dead);
if (!t) continue;
if (Number.isFinite(maxDist) && dist(this, t) > maxDist) continue;
best = t;
bestScore = score;
}
return best;
}
recentFightRival(maxAge = 52, maxDist = Infinity) {
let best = null, bestT = -Infinity;
for (const [id, rel] of Object.entries(this.relationships || {})) {
const event = String(rel.lastEvent || "");
const when = rel.lastTime || -Infinity;
if (this.world.time - when > maxAge) continue;
if (!(event.includes("fight") || event.includes("\u55a7\u5629") || (rel.fightsWon || 0) || (rel.fightsLost || 0))) continue;
const t = this.world.tarinai.find(o => o.id === id && !o.dead);
if (!t) continue;
if (Number.isFinite(maxDist) && dist(this, t) > maxDist) continue;
if (when > bestT) { best = t; bestT = when; }
}
return best;
}
poke() {
audio.poke();
this.damage(rand(9, 16), "\u3064\u3064\u304b\u308c\u3059\u304e");
2026-06-19 23:22:15 +09:00
this.addStress(22, { threshold: 8 });
2026-06-19 14:00:48 +09:00
this.surpriseTimer = 0.55;
this.fearTimer = Math.max(this.fearTimer, 1.2);
this.pokeFlashTimer = 0.55;
this.hurtTimer = Math.max(this.hurtTimer, 2.4);
this.vx += rand(-100, 100);
this.vy += rand(-100, 100);
if (this.energy <= 0.5) this.die("\u3064\u3064\u304b\u308c\u3059\u304e");
}
showHpBar(duration = 4.8) {
this.hpBarTimer = Math.max(this.hpBarTimer || 0, duration);
}
2026-06-19 23:22:15 +09:00
showStressBar(duration = 4.2) {
this.stressBarTimer = Math.max(this.stressBarTimer || 0, duration);
}
addStress(amount, opts = {}) {
const delta = Math.max(0, amount || 0);
if (delta <= 0) return 0;
const before = this.stress || 0;
this.stress = clamp(before + delta, 0, 130);
const gained = Math.max(0, this.stress - before);
const threshold = opts.threshold ?? 8;
if (gained >= threshold) this.showStressBar(opts.duration ?? 4.2);
return gained;
}
2026-06-20 02:07:01 +09:00
damageCauseLabel(reason = "") { return HEALTH.causeLabel(reason); }
recentDamageCause(maxAge = 10) { return HEALTH.recentDamageCause(this, maxAge); }
rememberDamage(amount, reason = "") { return HEALTH.rememberDamage(this, amount, reason); }
weakenedDeathReasonFor(cause = "") { return HEALTH.weakenedDeathReasonFor(cause); }
dominantDeathCause(reason = "", opts = {}) { return HEALTH.dominantDeathCause(this, reason, opts); }
normalizeDeathReason(reason = "", opts = {}) { return HEALTH.normalizeDeathReason(this, reason, opts); }
damage(amount, reason = "") { return HEALTH.applyDamage(this, amount, reason); }
2026-06-19 14:00:48 +09:00
2026-06-20 20:29:44 +09:00
wakeFromDamage(reason = "") {
if (this.dead) return false;
const wasSleeping = this.state === "sleep" || this.sleeping || this.insideNestBoxId;
if (!wasSleeping) return false;
const box = this.nestBoxById ? this.nestBoxById(this.insideNestBoxId) : null;
this.sleeping = false;
this.state = "panic";
this.target = null;
this.thought = "\u30c0\u30e1\u30fc\u30b8\u3067\u76ee\u304c\u899a\u3081\u305f";
this.fearTimer = Math.max(this.fearTimer || 0, 0.85);
this.hurtTimer = Math.max(this.hurtTimer || 0, 1.2);
this.nestBoxStayUntil = -Infinity;
if (this.insideNestBoxId) this.leaveNestBox(box);
this.vx += rand(-34, 34);
this.vy += rand(-26, 8);
return true;
}
2026-06-19 14:00:48 +09:00
recoverHealth(amount) {
if (this.dead) return;
this.energy = clamp(this.energy + Math.max(0, amount || 0), 0, 100);
}
familyJoined() {
return Boolean(this.hasPaired || (this.parents || []).length || (this.children || []).length);
}
2026-06-19 20:52:40 +09:00
nearestCoParent(maxRange = 220) {
let best = null;
let bestD = maxRange;
for (const o of this.world.tarinai || []) {
if (!o || o === this || o.dead) continue;
if (!(this.world.areCoParents?.(this, o))) continue;
const d = dist(this, o);
if (d < bestD) {
best = o;
bestD = d;
}
}
return best;
}
2026-06-19 14:00:48 +09:00
displayGeneration() {
return this.familyJoined() ? this.generation : null;
}
targetIdentity(target = this.target) {
if (!target) return "";
if (target.id) return `${target.type || "tarinai"}:${target.id}`;
if (target.type) return `${target.type}:${Math.round(target.x || 0)}:${Math.round(target.y || 0)}`;
if (Number.isFinite(target.x) && Number.isFinite(target.y)) return `point:${Math.round(target.x / 8)}:${Math.round(target.y / 8)}`;
return "";
}
targetIgnoresFence(target = this.target) {
2026-06-20 20:29:44 +09:00
return Boolean(target?.type === "sweet" || target?.type === "nest_box" || (target?.type === "bed" && (this.state === "seek_bed" || this.state === "sleep")) || this.state === "panic" || this.state === "fight" || this.state === "intimidate" || this.state === "birth_ritual" || this.state === "cursor_enemy");
2026-06-19 14:00:48 +09:00
}
shouldAvoidTarget(target) {
if (!target || !Number.isFinite(target.x) || !Number.isFinite(target.y)) return false;
if (this.targetIgnoresFence(target)) return false;
const key = this.targetIdentity(target);
if (key && this.targetGiveupKey === key && this.world.time < (this.targetGiveupUntil || 0)) return true;
return this.world.pathBlockedByFence ? this.world.pathBlockedByFence(this.x, this.y, target.x, target.y, this.radius + 10) : false;
}
maintainTargetProgress(dt) {
const target = this.target;
if (!target || target.dead || !Number.isFinite(target.x) || !Number.isFinite(target.y)) {
this.targetKey = "";
this.targetSince = this.world.time;
this.targetBestDist = Infinity;
return;
}
if (this.targetIgnoresFence(target)) return;
const key = this.targetIdentity(target);
const d = distXY(this.x, this.y, target.x, target.y);
if (key !== this.targetKey) {
this.targetKey = key;
this.targetSince = this.world.time;
this.targetBestDist = d;
} else if (d < (this.targetBestDist || Infinity) - 8) {
this.targetBestDist = d;
this.targetSince = this.world.time;
}
if (this.world.pathBlockedByFence && this.world.pathBlockedByFence(this.x, this.y, target.x, target.y, this.radius + 10)) {
this.targetGiveupKey = key;
this.targetGiveupUntil = this.world.time + 8;
this.target = null;
this.state = "idle";
this.wanderAngle += rand(-1.4, 1.4);
return;
}
if (this.world.time - (this.targetSince || this.world.time) > 30 && d > this.radius + 24) {
this.targetGiveupKey = key;
this.targetGiveupUntil = this.world.time + 12;
this.target = null;
this.state = "idle";
this.wanderAngle += rand(-2.2, 2.2);
this.thought = "\u8fbf\u308a\u7740\u3051\u305a\u3001\u3042\u304d\u3089\u3081\u305f";
}
}
panicDestination(threat = null, force = false) {
const now = this.world.time || 0;
const current = this.panicTarget;
if (!force && current && now < (this.panicTargetUntil || 0) && distXY(this.x, this.y, current.x, current.y) > 34) return current;
let ax = Math.cos(this.wanderAngle || 0);
let ay = Math.sin(this.wanderAngle || 0);
if (threat && Number.isFinite(threat.x) && Number.isFinite(threat.y)) {
const dx = this.x - threat.x;
const dy = this.y - threat.y;
const d = Math.hypot(dx, dy) || 1;
ax = dx / d;
ay = dy / d;
}
const turn = rand(-0.75, 0.75);
const cos = Math.cos(turn);
const sin = Math.sin(turn);
const dirX = ax * cos - ay * sin;
const dirY = ax * sin + ay * cos;
const distance = rand(150, 250);
const p = CONFIG.worldPadding + this.radius;
let x = clamp(this.x + dirX * distance, p, this.world.w - p);
let y = clamp(this.y + dirY * distance, p, this.world.h - p);
if (distXY(this.x, this.y, x, y) < 70) {
x = clamp(this.x - dirY * distance, p, this.world.w - p);
y = clamp(this.y + dirX * distance, p, this.world.h - p);
}
this.panicTarget = { x, y, dead: false, detour: true, panic: true };
this.panicTargetUntil = now + rand(1.6, 3.0);
this.panicStuckTimer = 0;
this.wanderAngle = Math.atan2(y - this.y, x - this.x);
return this.panicTarget;
}
lowHealthSprite() {
2026-06-19 20:52:40 +09:00
// This is physical weakness only. Stress is handled by the stress sprite branch
// below so high-stress individuals do not incorrectly render as low-health.
const critical = this.hunger > 112 || this.energy < 7;
2026-06-19 14:00:48 +09:00
if (critical) return this.stretchTimer > 0 ? "stretch" : "weak";
2026-06-19 20:52:40 +09:00
const weak = this.hunger > 102 || this.energy < 18;
2026-06-19 14:00:48 +09:00
if (weak) return "weak";
return null;
}
2026-06-20 02:07:01 +09:00
briefDeathReason(reason = this.deathReason) { return HEALTH.briefDeathReason(this, reason); }
2026-06-19 20:52:40 +09:00
2026-06-19 14:00:48 +09:00
variantSprite(category) {
const pools = {
hurt: ["hurt", "hurt2"],
fear: ["teary", "fear", "flee_fear2", "fear_blue", "fear_cry"],
flee: ["flee", "flee_fear2"],
sleep: ["sleep", "sleep2"],
stress: ["stress_dizzy", "stress_sweat"],
low_stress: lowStressSprites(),
intimidate: ["intimidate"],
normal: displayNormalSprites(),
};
const options = (pools[category] || []).filter(id => SPRITES.some(s => s.id === id));
2026-06-20 23:27:39 +09:00
return stableChoice(this.familyKey || this.id, `variant-${category}`, options) || options[0] || this.goodMode || "smile";
2026-06-19 14:00:48 +09:00
}
2026-06-19 20:52:40 +09:00
infectZunchiDisease(source = null, force = false) {
if (this.dead || this.zunchiDisease) return false;
if (!force && (this.zunchiDiseaseCooldown || 0) > 0) return false;
this.zunchiDisease = true;
this.zunchiDiseaseSeverity = Math.max(this.zunchiDiseaseSeverity || 0, source ? 0.72 : 0.86);
this.zunchiDiseaseCooldown = 12;
this.fearTimer = Math.max(this.fearTimer || 0, 0.35);
this.surpriseTimer = Math.max(this.surpriseTimer || 0, 0.45);
2026-06-19 23:22:15 +09:00
this.thought = "\u305a\u3093\u3061\u75c5\u306b\u304b\u304b\u3063\u305f";
this.world?.spawnBubble?.(this.x, this.y - this.radius * 1.20, "\u3046\u3045\u2026", "rgba(38,82,38,0.82)");
2026-06-19 20:52:40 +09:00
return true;
}
recoverZunchiDisease(reason = "") {
if (!this.zunchiDisease) return false;
this.zunchiDisease = false;
this.zunchiDiseaseSeverity = 0;
this.zunchiDiseaseCooldown = 18;
this.zunchiStain = clamp((this.zunchiStain || 0) - 35, 0, 100);
this.stress = clamp(this.stress - 8, 0, 130);
2026-06-19 23:22:15 +09:00
this.thought = reason || "\u305a\u3093\u3061\u75c5\u304c\u6cbb\u307e\u3063\u305f";
this.world?.spawnBubble?.(this.x, this.y - this.radius * 1.18, "\u306f\u3046", "rgba(62,104,62,0.78)");
2026-06-19 20:52:40 +09:00
return true;
}
emitZunchiDiseaseEffect(dt) {
if (!this.zunchiDisease || !this.world?.effects) return;
if (Math.random() < dt * (1.2 + (this.zunchiDiseaseSeverity || 0) * 1.4)) {
this.world.effects.push(new Effect("zunchi_miasma", this.x + rand(-this.radius * 0.45, this.radius * 0.45), this.y - this.radius * rand(0.05, 0.72), {
vx: rand(-5, 5),
vy: rand(-10, -3),
life: rand(1.4, 2.4),
size: rand(8, 18) * (0.8 + (this.zunchiDiseaseSeverity || 0) * 0.45),
color: "rgba(16,78,28,0.70)",
}));
}
}
updateZunchiDisease(dt) {
this.zunchiStain = clamp(this.zunchiStain || 0, 0, 100);
this.zunchiDiseaseCooldown = Math.max(0, (this.zunchiDiseaseCooldown || 0) - dt);
if (this.world?.weather === "light_rain") {
this.zunchiStain = clamp(this.zunchiStain - dt * 2.4, 0, 100);
if (this.zunchiDisease) this.zunchiDiseaseSeverity = Math.max(0, (this.zunchiDiseaseSeverity || 1) - dt * 0.006);
}
if (!this.zunchiDisease && this.zunchiStain > 82 && Math.random() < dt * clamp((this.zunchiStain - 82) / 18, 0, 1) * 0.045) {
this.infectZunchiDisease(null, true);
2026-06-20 23:27:39 +09:00
this.world?.log?.(`${this.name}\u306f\u305a\u3093\u3061\u75c5\u3092\u767a\u75c7\u3057\u305f\u3002`, "accident", { participants: [this] });
2026-06-19 20:52:40 +09:00
}
if (this.zunchiDisease) {
2026-06-19 23:22:15 +09:00
this.zunchiDiseaseSeverity = clamp((this.zunchiDiseaseSeverity || 0.7) + dt * 0.004, 0, 1.25);
this.stress = clamp(this.stress + dt * (0.28 + 0.18 * this.zunchiDiseaseSeverity), 0, 130);
2026-06-19 20:52:40 +09:00
this.energy = clamp(this.energy - dt * 0.035 * this.zunchiDiseaseSeverity, 0, 100);
if (this.energy > 82) this.zunchiDiseaseSeverity = Math.max(0, this.zunchiDiseaseSeverity - dt * (this.energy > 94 ? 0.020 : 0.010));
this.emitZunchiDiseaseEffect(dt);
if (this.zunchiDiseaseSeverity <= 0.04) {
2026-06-20 23:27:39 +09:00
if (this.recoverZunchiDisease("\u4f53\u529b\u304c\u623b\u308a\u3001\u305a\u3093\u3061\u75c5\u304c\u6cbb\u307e\u3063\u305f")) this.world?.log?.(`${this.name}\u306e\u305a\u3093\u3061\u75c5\u304c\u6cbb\u307e\u3063\u305f\u3002`, "accident", { participants: [this] });
2026-06-19 20:52:40 +09:00
}
}
}
2026-06-20 02:07:01 +09:00
hasWaterCurableDisease() {
return Boolean(this.zunchiDisease || this.explosionDisease);
}
hasZundaCurableDisease() {
return Boolean(this.explosionDisease || this.fightDisease);
}
infectSleepDisease(source = null) {
if (this.dead || this.sleepDisease) return false;
this.sleepDisease = true;
this.sleepDiseaseCooldown = 8;
this.sleepDiseaseAnchorX = this.x;
this.sleepDiseaseAnchorY = this.y;
this.state = "sleep";
this.target = null;
this.sleeping = true;
this.fightTimer = 0;
this.intimidateTimer = 0;
this.fightTargetIds = [];
this.thought = "\u306d\u3080\u308a\u75c5\u3067\u7720\u308a\u7d9a\u3051\u3066\u3044\u308b";
this.world?.spawnBubble?.(this.x, this.y - this.radius * 1.18, "Zzz...", "rgba(72,76,120,0.76)");
2026-06-20 23:27:39 +09:00
this.world?.log?.(`${this.name}\u306f\u306d\u3080\u308a\u75c5\u306b\u304b\u304b\u3063\u305f\u3002`, "accident", { participants: [this] });
2026-06-20 02:07:01 +09:00
return true;
}
recoverSleepDisease(reason = "") {
if (!this.sleepDisease) return false;
this.sleepDisease = false;
this.sleepDiseaseCooldown = 14;
this.sleepDiseaseAnchorX = null;
this.sleepDiseaseAnchorY = null;
this.state = "idle";
this.sleeping = false;
this.thought = reason || "\u306d\u3080\u308a\u75c5\u304c\u6cbb\u3063\u305f";
this.surpriseTimer = Math.max(this.surpriseTimer || 0, 0.65);
this.world?.spawnBubble?.(this.x, this.y - this.radius * 1.18, "!", "rgba(72,76,120,0.76)");
2026-06-20 23:27:39 +09:00
this.world?.log?.(`${this.name}\u306e\u306d\u3080\u308a\u75c5\u304c\u6cbb\u3063\u305f\u3002`, "accident", { participants: [this] });
2026-06-20 02:07:01 +09:00
return true;
}
infectExplosionDisease(source = null) {
if (this.dead || this.explosionDisease) return false;
this.explosionDisease = true;
this.explosionDiseaseTimer = Math.max(this.explosionDiseaseTimer || 0, (CONFIG.dayLength || 120) * 0.5);
this.fearTimer = Math.max(this.fearTimer || 0, 0.55);
this.thought = "\u7206\u767a\u75c5\u3067\u843d\u3061\u7740\u304b\u306a\u3044";
this.world?.spawnBubble?.(this.x, this.y - this.radius * 1.2, "!", "rgba(178,78,42,0.82)");
2026-06-20 23:27:39 +09:00
this.world?.log?.(`${this.name}\u306f\u7206\u767a\u75c5\u3092\u767a\u75c7\u3057\u305f\u3002`, "accident", { participants: [this] });
2026-06-20 02:07:01 +09:00
return true;
}
recoverExplosionDisease(reason = "") {
if (!this.explosionDisease) return false;
this.explosionDisease = false;
this.explosionDiseaseTimer = 0;
this.thought = reason || "\u7206\u767a\u75c5\u304c\u6cbb\u3063\u305f";
this.stress = clamp(this.stress - 10, 0, 130);
this.world?.spawnBubble?.(this.x, this.y - this.radius * 1.18, "\u306f\u3046", "rgba(82,126,88,0.76)");
2026-06-20 23:27:39 +09:00
this.world?.log?.(`${this.name}\u306e\u7206\u767a\u75c5\u304c\u6cbb\u3063\u305f\u3002`, "accident", { participants: [this] });
2026-06-20 02:07:01 +09:00
return true;
}
recordBleedExposure() {
this.lastBleedAt = this.world?.time || 0;
}
infectFightDisease(source = null) {
if (this.dead || this.fightDisease) return false;
this.fightDisease = true;
this.fightDiseaseCooldown = 18;
this.fearTimer = Math.max(this.fearTimer || 0, 0.35);
2026-06-20 20:29:44 +09:00
this.thought = "\u304d\u305a\u3064\u304d\u75c5\u3067\u5f53\u3066\u3082\u306a\u304f\u5f77\u5fa8\u3063\u3066\u3044\u308b";
2026-06-20 02:07:01 +09:00
this.world?.spawnBubble?.(this.x, this.y - this.radius * 1.20, "?", "rgba(150,78,44,0.80)");
2026-06-20 23:27:39 +09:00
this.world?.log?.(`${this.name}\u306f\u304d\u305a\u3064\u304d\u75c5\u3092\u767a\u75c7\u3057\u305f\u3002`, "fight", { participants: [this] });
2026-06-20 02:07:01 +09:00
return true;
}
recoverFightDisease(reason = "") {
if (!this.fightDisease) return false;
this.fightDisease = false;
this.fightDiseaseCooldown = 20;
2026-06-20 20:29:44 +09:00
this.thought = reason || "\u304d\u305a\u3064\u304d\u75c5\u304c\u6cbb\u3063\u305f";
2026-06-20 02:07:01 +09:00
this.stress = clamp(this.stress - 12, 0, 130);
this.world?.spawnBubble?.(this.x, this.y - this.radius * 1.18, "\u306f\u3046", "rgba(82,126,88,0.76)");
2026-06-20 23:27:39 +09:00
this.world?.log?.(`${this.name}\u306e\u304d\u305a\u3064\u304d\u75c5\u304c\u6cbb\u3063\u305f\u3002`, "fight", { participants: [this] });
2026-06-20 02:07:01 +09:00
return true;
}
freezeSleepDiseaseMotion(dt = 0) {
if (!this.sleepDisease) return false;
if (!Number.isFinite(this.sleepDiseaseAnchorX)) this.sleepDiseaseAnchorX = this.x;
if (!Number.isFinite(this.sleepDiseaseAnchorY)) this.sleepDiseaseAnchorY = this.y;
this.state = "sleep";
this.sleeping = true;
this.target = null;
this.vx = 0;
this.vy = 0;
this.x = this.sleepDiseaseAnchorX;
this.y = this.sleepDiseaseAnchorY;
this.wanderAngle = this.wanderAngle || 0;
this.thought = "\u306d\u3080\u308a\u75c5\u3067\u7720\u308a\u7d9a\u3051\u3066\u3044\u308b";
if (this.world && this.world.time > (this.nextSleepBubbleAt || 0)) {
this.nextSleepBubbleAt = this.world.time + 5.0;
this.world.spawnBubble?.(this.x, this.y - this.radius * 1.18, "Zzz...", "rgba(65,70,92,0.72)");
}
return true;
}
2026-06-20 20:29:44 +09:00
nestBoxById(id) {
if (!id) return null;
return this.world?.items?.find?.(it => it && !it.dead && it.id === id && it.type === "nest_box") || null;
}
isInsideNestBox() {
return Boolean(this.insideNestBoxId);
}
wantsNestBox(box = null) {
if (!box || box.dead || box.type !== "nest_box") return false;
if (this.insideNestBoxId === box.id && (this.world?.time || 0) < (this.nestBoxStayUntil || -Infinity)) return true;
if (!(this.state === "sleep" || this.state === "seek_bed")) return false;
return this.target === box || this.insideNestBoxId === box.id;
}
enterNestBox(box, dt = 0.016) {
if (!box || box.dead || box.type !== "nest_box" || this.dead) return false;
const capacity = this.world?.nestBoxCapacity ? this.world.nestBoxCapacity(box) : 5;
const occupants = this.world?.nestBoxOccupants ? this.world.nestBoxOccupants(box, Infinity) : [];
const alreadyInside = this.insideNestBoxId === box.id;
if (!alreadyInside && occupants.length >= capacity) return false;
if (!alreadyInside) {
this.nestBoxEnteredAt = this.world?.time || 0;
this.nestBoxStayUntil = Math.max(this.nestBoxStayUntil || -Infinity, (this.world?.time || 0) + 14);
}
this.insideNestBoxId = box.id;
this.target = box;
if (this.state === "seek_bed") this.state = "sleep";
if (this.state === "sleep") this.sleeping = true;
const inner = this.world?.nestBoxInnerPoint ? this.world.nestBoxInnerPoint(box, this) : { x: box.x, y: box.y };
const pull = clamp(dt * (alreadyInside ? 10.0 : 13.5), 0, 1);
this.x = lerp(this.x, inner.x, pull);
this.y = lerp(this.y, inner.y, pull);
if (this.nestFade >= 0.88 || distXY(this.x, this.y, inner.x, inner.y) < 3.5) {
this.x = inner.x;
this.y = inner.y;
}
this.nestFade = clamp((this.nestFade || 0) + dt * 5.8, 0, 1);
this.vx = 0;
this.vy = 0;
this.impulseVx = 0;
this.impulseVy = 0;
return true;
}
leaveNestBox(box = null) {
const b = box || this.nestBoxById(this.insideNestBoxId);
if (b && this.world?.nestBoxEntryPoint) {
const entry = this.world.nestBoxExitPoint ? this.world.nestBoxExitPoint(b, this) : this.world.nestBoxEntryPoint(b);
this.x = clamp(entry.x, CONFIG.worldPadding, this.world.w - CONFIG.worldPadding);
this.y = clamp(entry.y, CONFIG.worldPadding, this.world.h - CONFIG.worldPadding);
this.vx = rand(-8, 8);
this.vy = rand(6, 18);
}
this.insideNestBoxId = null;
this.nestFade = 0;
this.nestBoxEnteredAt = -Infinity;
this.nestBoxStayUntil = -Infinity;
return true;
}
2026-06-20 02:07:01 +09:00
updateNestBoxPresence(dt = 0) {
2026-06-20 20:29:44 +09:00
const currentBox = this.nestBoxById(this.insideNestBoxId);
const targetBox = this.target && !this.target.dead && this.target.type === "nest_box" ? this.target : null;
const box = currentBox || targetBox;
2026-06-20 02:07:01 +09:00
if (!box) {
this.insideNestBoxId = null;
2026-06-20 20:29:44 +09:00
this.nestFade = clamp((this.nestFade || 0) - dt * 3.0, 0, 1);
2026-06-20 02:07:01 +09:00
return null;
}
2026-06-20 20:29:44 +09:00
if (currentBox && !this.wantsNestBox(currentBox)) {
this.leaveNestBox(currentBox);
return null;
2026-06-20 02:07:01 +09:00
}
2026-06-20 20:29:44 +09:00
if (currentBox) {
if ((this.world?.time || 0) < (this.nestBoxStayUntil || -Infinity)) {
this.state = "sleep";
this.sleeping = true;
this.target = currentBox;
}
this.enterNestBox(currentBox, dt);
return currentBox;
}
if (!this.wantsNestBox(targetBox)) {
this.nestFade = clamp((this.nestFade || 0) - dt * 3.0, 0, 1);
return null;
}
const entry = this.world?.nestBoxEntryPoint ? this.world.nestBoxEntryPoint(targetBox) : { x: targetBox.x, y: targetBox.y };
const base = this.world?.nestBoxBaseRect ? this.world.nestBoxBaseRect(targetBox) : null;
const r = targetBox.r || 42;
const entryD = distXY(this.x, this.y, entry.x, entry.y);
const centerD = distXY(this.x, this.y, targetBox.x, targetBox.y);
const inDoorColumn = base && this.x >= base.left + r * 0.18 && this.x <= base.right - r * 0.18 && this.y >= base.top + r * 0.06 && this.y <= base.bottom + r * 0.48;
const captureRange = Math.max(118, r * 2.05, this.radius + 82);
const nearEnough = entryD <= captureRange || centerD <= Math.max(104, r * 1.90) || inDoorColumn;
if (nearEnough) {
this.enterNestBox(targetBox, dt || 0.016);
return targetBox;
}
this.nestFade = clamp((this.nestFade || 0) - dt * 3.0, 0, 1);
2026-06-20 02:07:01 +09:00
return null;
}
applyWaterEffect(dt, source = "water") {
const power = Math.max(0, dt || 0);
this.stress = clamp(this.stress - power * 2.4, 0, 130);
this.energy = clamp(this.energy + power * 0.5, 0, 100);
this.hunger = clamp(this.hunger - power * 0.2, 0, 115);
this.zunchiStain = clamp((this.zunchiStain || 0) - power * 10.5, 0, 100);
if (this.zunchiDisease) {
this.zunchiDiseaseSeverity = Math.max(0, (this.zunchiDiseaseSeverity || 1) - power * (this.energy > 70 ? 0.050 : 0.028));
if (this.zunchiDiseaseSeverity <= 0.04 && this.recoverZunchiDisease("\u6c34\u3067\u305a\u3093\u3061\u75c5\u304c\u6cbb\u307e\u3063\u305f")) {
2026-06-20 23:27:39 +09:00
this.world?.log?.(`${this.name}\u306f\u6c34\u3067\u305a\u3093\u3061\u75c5\u304c\u6cbb\u307e\u3063\u305f\u3002`, "accident", { participants: [this] });
2026-06-20 02:07:01 +09:00
}
}
if (this.explosionDisease) this.recoverExplosionDisease("\u6c34\u3067\u7206\u767a\u75c5\u304c\u6cbb\u3063\u305f");
}
updateSpecialDiseases(dt) {
this.sleepDiseaseCooldown = Math.max(0, (this.sleepDiseaseCooldown || 0) - dt);
this.fightDiseaseCooldown = Math.max(0, (this.fightDiseaseCooldown || 0) - dt);
if (this.explosionDisease) {
this.explosionDiseaseTimer = Math.max(0, (this.explosionDiseaseTimer || 0) - dt);
this.addStress ? this.addStress(dt * 0.22, { threshold: 9, duration: 3.6 }) : (this.stress = clamp(this.stress + dt * 0.22, 0, 130));
if (Math.random() < dt * 2.1) {
const a = rand(0, Math.PI * 2);
this.world?.effects?.push(new Effect("explosion", this.x + Math.cos(a) * this.radius * rand(0.2, 0.9), this.y + Math.sin(a) * this.radius * rand(0.1, 0.8), {
vx: Math.cos(a) * rand(10, 38), vy: Math.sin(a) * rand(10, 38) - rand(8, 22), size: rand(5, 12), life: rand(0.18, 0.42), color: "rgba(255,64,48,0.86)"
}));
}
if (this.explosionDiseaseTimer <= 0.01) {
this.world?.explodeDiseaseTarinai?.(this);
return;
}
if (this.explosionDiseaseTimer < 8 && Math.random() < dt * 0.45) this.bubble("!", 1.0, "rgba(178,78,42,0.82)");
}
const bleedAge = (this.world?.time || 0) - (this.lastBleedAt || -999);
if (!this.fightDisease && this.fightDiseaseCooldown <= 0 && bleedAge >= (CONFIG.dayLength || 120) * 5 / 24 && bleedAge < (CONFIG.dayLength || 120) * 0.85) {
if (Math.random() < dt * 0.000375) this.infectFightDisease();
}
if (this.zunchiDisease && Math.random() < dt * 0.00055 * (this.energy > 66 ? 1.8 : 1.0)) {
2026-06-20 23:27:39 +09:00
if (this.recoverZunchiDisease("\u81ea\u7136\u306b\u305a\u3093\u3061\u75c5\u304c\u6cbb\u307e\u3063\u305f")) this.world?.log?.(`${this.name}\u306e\u305a\u3093\u3061\u75c5\u304c\u81ea\u7136\u306b\u6cbb\u3063\u305f\u3002`, "accident", { participants: [this] });
2026-06-20 02:07:01 +09:00
}
if (this.fightDisease && Math.random() < dt * 0.00070 * (this.energy > 58 ? 1.4 : 1.0)) {
2026-06-20 20:29:44 +09:00
this.recoverFightDisease("\u81ea\u7136\u306b\u304d\u305a\u3064\u304d\u75c5\u304c\u6cbb\u3063\u305f");
2026-06-20 02:07:01 +09:00
}
if (this.fightDisease) {
this.stress = clamp(this.stress + dt * 0.16, 0, 130);
if (Math.random() < dt * 0.30) this.wanderAngle += rand(-2.8, 2.8);
if (Math.random() < dt * 0.08) this.bubble("?", 2.6, "rgba(150,78,44,0.80)");
}
}
2026-06-19 20:52:40 +09:00
canEatItemType(type) {
return this.isZunchiSlave ? type === "zunchi" : true;
}
hasLoveMochiEffect() {
return (this.loveMochiTimer || 0) > 0.04;
}
hasFightMochiEffect() {
return (this.fightMochiTimer || 0) > 0.04;
}
becomeZunchiSlave() {
if (this.isZunchiSlave) return false;
this.formerName = this.formerName || this.name;
this.isZunchiSlave = true;
2026-06-19 23:22:15 +09:00
this.name = "\u305a\u3093\u3061\u3069\u308c\u3044";
2026-06-19 20:52:40 +09:00
this.birthRitualTimer = 0;
this.birthPartnerId = null;
this.birthRitualLeader = false;
this.birthRitualRole = 0;
this.target = null;
this.reproductionTimer = Math.max(this.reproductionTimer || 0, CONFIG.reproductionCooldown + rand(8, 18));
this.loveMochiTimer = 0;
2026-06-19 23:22:15 +09:00
this.addStress(14, { threshold: 8 });
2026-06-19 20:52:40 +09:00
this.hunger = Math.max(this.hunger, 40);
this.visibleSpriteId = "zunchi_slave";
this.spriteLockUntil = 0;
return true;
}
2026-06-19 14:00:48 +09:00
rawSpriteId() {
if (this.dead) return this.variantSprite("fear");
2026-06-20 02:07:01 +09:00
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";
2026-06-19 20:52:40 +09:00
if (this.isZunchiSlave) return "zunchi_slave";
if (this.type === "cry" && (this.mood < 26 || this.stress > 78 || this.hunger > 96 || this.energy < 12)) return "cry";
2026-06-19 14:00:48 +09:00
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();
2026-06-19 20:52:40 +09:00
if (low === "stretch") return low;
2026-06-19 14:00:48 +09:00
if (this.defeatedTimer > 0.04) return this.variantSprite("flee");
2026-06-20 21:41:15 +09:00
if (this.state === "intimidate" || this.state === "ant_intimidate" || this.intimidateTimer > 0.04) return this.variantSprite("intimidate");
2026-06-19 14:00:48 +09:00
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";
2026-06-19 20:52:40 +09:00
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");
2026-06-19 14:00:48 +09:00
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;
if (!this.visibleSpriteId) {
this.visibleSpriteId = next;
this.spriteLockUntil = now + 1.0;
return next;
}
if (next !== this.visibleSpriteId && now >= this.spriteLockUntil) {
this.visibleSpriteId = next;
this.spriteLockUntil = now + 1.0;
}
return this.visibleSpriteId;
}
2026-06-20 02:07:01 +09:00
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;
2026-06-20 20:29:44 +09:00
return this.affection > 12 || this.mood > 58 || this.goodMode === "smile" || this.shouldApplyPersonalityBehavior("sociability", 1);
2026-06-20 02:07:01 +09:00
}
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);
2026-06-20 20:29:44 +09:00
this.adjustPersonality("openness", dt * 0.006 * (0.4 + p), "after being petted.");
2026-06-20 02:07:01 +09:00
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)"
}));
}
}
}
2026-06-19 14:00:48 +09:00
update(dt) {
if (this.dead) return;
2026-06-20 15:55:52 +09:00
this.prevX = this.x;
this.prevY = this.y;
2026-06-19 14:00:48 +09:00
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.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);
2026-06-19 23:22:15 +09:00
this.stressBarTimer = Math.max(0, (this.stressBarTimer || 0) - dt);
2026-06-19 20:52:40 +09:00
this.updateZunchiDisease(dt);
2026-06-20 02:07:01 +09:00
this.updateSpecialDiseases(dt);
if (this.dead) return;
2026-06-19 20:52:40 +09:00
this.loveMochiTimer = Math.max(0, (this.loveMochiTimer || 0) - dt);
this.fightMochiTimer = Math.max(0, (this.fightMochiTimer || 0) - dt);
2026-06-19 23:22:15 +09:00
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);
}
2026-06-19 14:00:48 +09:00
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);
2026-06-20 20:29:44 +09:00
this.focusPulseTimer = Math.max(0, (this.focusPulseTimer || 0) - dt);
2026-06-19 20:52:40 +09:00
this.cursorPetting = Math.max(0, this.cursorPetting - dt * 1.2);
2026-06-20 02:07:01 +09:00
if (this.freezeSleepDiseaseMotion(dt)) {
this.checkLife(dt);
return;
}
this.applyCursorContactCare(dt);
2026-06-19 14:00:48 +09:00
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.juvenile) {
const adultScale = 0.25 + (makeTrait(this.type).speed % 0.08);
this.scale = lerp(this.scale, adultScale, dt * 0.018 + 0.002);
this.radius = 56 * this.scale;
}
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);
2026-06-20 02:07:01 +09:00
if (this.freezeSleepDiseaseMotion(dt)) {
this.checkLife(dt);
return;
}
2026-06-19 14:00:48 +09:00
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;
2026-06-20 23:27:39 +09:00
const aiInterval = urgentAi ? (popPerf >= 2 ? 0.105 : 0.075) : (0.18 + stableUnit(this.familyKey || this.id, "ai-interval") * 0.09 + popPerf * 0.055);
2026-06-19 14:00:48 +09:00
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.maintainTargetProgress(dt);
this.move(dt);
2026-06-20 02:07:01 +09:00
this.updateNestBoxPresence(dt);
2026-06-19 14:00:48 +09:00
if (this.world.resolveFenceCollision) this.world.resolveFenceCollision(this);
2026-06-19 23:22:15 +09:00
this.checkLife(dt);
2026-06-19 14:00:48 +09:00
}
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);
2026-06-20 20:29:44 +09:00
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.");
2026-06-19 14:00:48 +09:00
}
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;
}
2026-06-20 02:07:01 +09:00
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);
2026-06-19 14:00:48 +09:00
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));
2026-06-20 20:29:44 +09:00
if (this.need === "\u9759\u3051\u3055" || this.shouldApplyPersonalityBehavior("neuroticism", 1)) this.affection += dt * Math.min(0.06, grassComfort * 0.012);
2026-06-19 14:00:48 +09:00
}
}
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,
};
}
2026-06-19 20:52:40 +09:00
isSleepFurniture(target) {
2026-06-20 02:07:01 +09:00
return Boolean(target && (target.type === "bed" || target.type === "nest_box"));
2026-06-19 20:52:40 +09:00
}
sleepFurnitureLabel(target) {
2026-06-20 02:07:01 +09:00
return target?.type === "nest_box" ? "\u5de3\u7bb1" : "\u5e72\u8349\u5bdd\u5e8a";
2026-06-19 20:52:40 +09:00
}
2026-06-20 20:29:44 +09:00
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 }[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;
it.foodServingsRemaining = Math.max(0, (it.foodServingsRemaining || 0) - 1);
it.amount = it.foodServingsRemaining;
return nutrition;
}
2026-06-19 14:00:48 +09:00
sleepSpotFor(bed) {
if (!bed) return { x: this.x, y: this.y };
2026-06-20 02:07:01 +09:00
if (bed.type === "nest_box") {
2026-06-20 20:29:44 +09:00
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) };
2026-06-20 02:07:01 +09:00
}
2026-06-20 23:27:39 +09:00
const angle = stableUnit(this.familyKey || this.id, `bed-angle-${bed.id || bed.seed || "bed"}`) * Math.PI * 2;
2026-06-19 14:00:48 +09:00
const baseRing = bed.r * 0.52;
2026-06-20 23:27:39 +09:00
const ring = baseRing + stableUnit(this.familyKey || this.id, `bed-ring-${bed.id || bed.seed || "bed"}`) * bed.r * 0.95;
2026-06-19 14:00:48 +09:00
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) {
2026-06-19 20:52:40 +09:00
if (this.isZunchiSlave) return;
2026-06-19 14:00:48 +09:00
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);
2026-06-20 20:29:44 +09:00
if (push > 0.2) this.adjustPersonality("neuroticism", -dt * push * 0.0035, "after repeated contact with poop.");
2026-06-19 14:00:48 +09:00
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;
}
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.state = "birth_ritual";
this.target = this.world.tarinai.find(o => o.id === this.birthPartnerId && !o.dead) || null;
this.thought = "\u8fd1\u304f\u3067\u4f53\u3092\u3086\u3059\u3063\u3066\u3044\u308b";
return;
}
if (this.intimidateTimer > 0.04) {
this.state = "intimidate";
this.target = this.world.tarinai.find(o => o.id === this.intimidateTargetId && !o.dead) || null;
this.bubble("!", 1.2);
this.thought = "\u76f8\u624b\u3092\u5a01\u5687\u3057\u3066\u3044\u308b";
return;
}
if (this.fightTimer > 0.04) {
this.state = "fight";
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.target = best;
this.bubble("!", 1.6);
this.thought = "\u55a7\u5629\u3057\u3066\u3044\u308b";
return;
}
if (this.defeatedTimer > 0.04) {
const opponent = this.world.tarinai.find(o => o.id === this.defeatedById && !o.dead);
this.state = "panic";
this.target = this.panicDestination(opponent);
this.thought = "\u55a7\u5629\u306b\u8ca0\u3051\u3066\u9003\u3052\u3066\u3044\u308b";
if (!opponent && this.defeatedTimer < 0.12) this.defeatedById = null;
return;
} else if (this.defeatedById && this.fightTimer <= 0.04) {
this.defeatedById = null;
this.fightWinnerId = null;
}
2026-06-20 20:29:44 +09:00
const friendNearby = this.bestFriendLive ? this.bestFriendLive(FRIEND_AFFINITY_THRESHOLD, 170) : null;
2026-06-19 14:00:48 +09:00
if (friendNearby && friendNearby.state === "panic" && friendNearby.target) {
this.state = "panic";
this.target = this.panicDestination(friendNearby.target);
this.fearTimer = Math.max(this.fearTimer, 0.32);
2026-06-20 20:29:44 +09:00
this.thought = "\u53cb\u9054\u3068\u4e00\u7dd2\u306b\u9003\u3052\u3066\u3044\u308b";
2026-06-19 14:00:48 +09:00
return;
}
2026-06-20 21:41:15 +09:00
const nearbyAnts = (this.world.ants || []).filter(a => a && !a.dead && distXY(this.x, this.y, a.x, a.y) <= (a.kind === "queen" ? 112 : 82));
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.state = "ant_intimidate";
this.target = nearestAnt;
this.bubble("!", 0.9, "rgba(145,68,52,0.78)");
this.thought = "\u30a2\u30ea\u3092\u5a01\u5687\u3057\u3066\u3044\u308b";
return;
}
this.state = "ant_attack";
this.target = nearestAnt;
this.goodMode = "angry";
this.thought = "\u30a2\u30ea\u3092\u653b\u6483\u3057\u3066\u3044\u308b";
return;
}
this.state = "panic";
this.target = this.panicDestination(nearestAnt);
this.fearTimer = Math.max(this.fearTimer, 0.34);
this.thought = "\u30a2\u30ea\u304b\u3089\u9003\u3052\u3066\u3044\u308b";
return;
}
2026-06-19 14:00:48 +09:00
const dayP = this.world.dayProgress();
const personalSleep = this.sleepStart < this.sleepEnd
? dayP >= this.sleepStart && dayP < this.sleepEnd
: dayP >= this.sleepStart || dayP < this.sleepEnd;
2026-06-20 15:55:52 +09:00
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));
2026-06-19 23:22:15 +09:00
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.state = "seek_friend";
this.target = opponent;
this.goodMode = "angry";
this.thought = "\u3051\u3093\u304b\u9905\u3067\u76f8\u624b\u3092\u63a2\u3057\u3066\u3044\u308b";
if (Math.random() < dt * 0.24) this.bubble("!", 1.4, "rgba(160,74,45,0.78)");
return;
}
}
2026-06-19 14:00:48 +09:00
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) {
this.state = "panic";
this.target = this.panicDestination(rival);
this.fearTimer = Math.max(this.fearTimer, 0.35 * this.personalityProfile().fear);
this.thought = `${rival.name}\u3092\u907f\u3051\u3066\u9060\u56de\u308a\u3057\u3066\u3044\u308b`;
2026-06-20 15:55:52 +09:00
this.world.relationNotice(this.id, rival.id, "avoid", 22);
2026-06-19 14:00:48 +09:00
return;
}
}
2026-06-20 02:07:01 +09:00
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);
2026-06-19 20:52:40 +09:00
if (water && !water.dead) {
this.state = "seek_water";
this.target = water;
2026-06-20 02:07:01 +09:00
this.thought = this.explosionDisease ? "\u7206\u767a\u75c5\u3067\u6c34\u3092\u63a2\u3057\u3066\u3044\u308b" : "\u305a\u3093\u3061\u75c5\u3067\u6c34\u3092\u63a2\u3057\u3066\u3044\u308b";
return;
}
if (this.zunchiDisease) {
this.state = "zunchi_sick";
this.target = null;
this.thought = "\u305a\u3093\u3061\u75c5\u3067\u5f53\u3066\u3082\u306a\u304f\u5f77\u5fa8\u3063\u3066\u3044\u308b";
if (Math.random() < dt * 0.35) this.wanderAngle += rand(-2.4, 2.4);
2026-06-19 20:52:40 +09:00
return;
}
2026-06-20 02:07:01 +09:00
}
if (this.hasZundaCurableDisease?.() && this.fightTimer <= 0.04 && this.birthRitualTimer <= 0.04 && this.defeatedTimer <= 0.04) {
const cureFood = this.world.nearest(this, ["sweet"], 700);
2026-06-20 20:29:44 +09:00
if (cureFood && this.foodItemHasServingLeft(cureFood) && !this.shouldAvoidTarget(cureFood)) {
2026-06-20 02:07:01 +09:00
this.state = "seek_food";
this.target = cureFood;
this.foodReactTimer = Math.max(this.foodReactTimer, 0.42);
2026-06-20 20:29:44 +09:00
this.thought = this.fightDisease ? "\u304d\u305a\u3064\u304d\u75c5\u3067\u305a\u3093\u3060\u9905\u3092\u63a2\u3057\u3066\u3044\u308b" : "\u7206\u767a\u75c5\u3067\u305a\u3093\u3060\u9905\u3092\u63a2\u3057\u3066\u3044\u308b";
2026-06-20 02:07:01 +09:00
return;
}
}
if (this.fightDisease && this.fightTimer <= 0.04 && this.birthRitualTimer <= 0.04 && this.defeatedTimer <= 0.04) {
this.state = "fight_sick";
2026-06-19 20:52:40 +09:00
this.target = null;
2026-06-20 20:29:44 +09:00
this.thought = "\u304d\u305a\u3064\u304d\u75c5\u3067\u5f53\u3066\u3082\u306a\u304f\u5f77\u5fa8\u3063\u3066\u3044\u308b";
2026-06-20 02:07:01 +09:00
if (Math.random() < dt * 0.45) this.wanderAngle += rand(-3.4, 3.4);
2026-06-19 20:52:40 +09:00
return;
}
if (!this.isZunchiSlave) {
2026-06-20 02:07:01 +09:00
const specialMochi = this.world.nearest(this, ["love_mochi", "fight_mochi", "sleep_drug"], wasSleeping ? 520 : 390);
2026-06-20 20:29:44 +09:00
if (specialMochi && this.foodItemHasServingLeft(specialMochi) && this.hunger > 10 && !this.shouldAvoidTarget(specialMochi)) {
2026-06-19 20:52:40 +09:00
this.state = "seek_food";
this.target = specialMochi;
this.foodReactTimer = Math.max(this.foodReactTimer, 0.34);
2026-06-20 02:07:01 +09:00
this.thought = specialMochi.type === "love_mochi" ? "\u3078\u3053\u9905\u3092\u63a2\u3057\u3066\u3044\u308b" : (specialMochi.type === "fight_mochi" ? "\u3051\u3093\u304b\u9905\u3092\u63a2\u3057\u3066\u3044\u308b" : "\u306d\u3080\u308a\u85ac\u3092\u63a2\u3057\u3066\u3044\u308b");
2026-06-19 20:52:40 +09:00
return;
}
} else if (this.hunger > 20) {
const zOnly = this.world.nearest(this, ["zunchi"], 520);
if (zOnly && zOnly.amount > 1.2) {
this.state = "seek_food";
this.target = zOnly;
this.foodReactTimer = Math.max(this.foodReactTimer, 0.30);
2026-06-19 23:22:15 +09:00
this.thought = "\u305a\u3093\u3061\u3092\u63a2\u3057\u3066\u3044\u308b";
2026-06-19 20:52:40 +09:00
return;
}
}
2026-06-19 14:00:48 +09:00
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;
2026-06-20 20:29:44 +09:00
if (zunda && this.foodItemHasServingLeft(zunda) && ((wasSleeping && this.hunger > 7) || zundaHungry || zundaReward)) {
2026-06-19 14:00:48 +09:00
this.state = "seek_food";
this.target = zunda;
this.foodReactTimer = Math.max(this.foodReactTimer, 0.36);
this.thought = "\u305a\u3093\u3060\u9905\u3092\u63a2\u3057\u3066\u3044\u308b";
return;
}
if (this.fearTimer > 0.22 || this.stress > 86 / this.personalityProfile().fear) {
this.state = "panic";
this.target = this.panicDestination();
this.thought = "\u9003\u3052\u3066\u3044\u308b";
return;
}
const sleepBias = this.personalityProfile().sleep;
const keepSleeping = wasSleeping && (personalSleep || this.energy < 66 + 8 * sleepBias);
if (keepSleeping || (personalSleep && this.energy < 90 + 8 * sleepBias) || this.energy < 28 + 8 * sleepBias) {
2026-06-19 20:52:40 +09:00
let bed = this.isSleepFurniture(previousTarget) ? previousTarget : null;
if (!bed && friendNearby && this.isSleepFurniture(friendNearby.target) && (friendNearby.state === "sleep" || friendNearby.state === "seek_bed")) bed = friendNearby.target;
2026-06-20 20:29:44 +09:00
if (!bed) bed = this.world.bestBedFor(this, wasSleeping ? 480 : 420);
2026-06-19 14:00:48 +09:00
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.state = "seek_bed";
this.target = bed;
2026-06-19 23:22:15 +09:00
this.thought = `${this.sleepFurnitureLabel(bed)}\u3078\u5411\u304b\u3063\u3066\u3044\u308b`;
2026-06-19 14:00:48 +09:00
return;
}
}
this.state = "sleep";
this.target = bed;
this.sleeping = true;
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)");
}
2026-06-20 20:29:44 +09:00
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";
2026-06-19 14:00:48 +09:00
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";
2026-06-20 02:07:01 +09:00
const friendly = !hostile && this.isAwakeCursorFriendly();
2026-06-19 14:00:48 +09:00
if (hostile) {
this.state = "cursor_enemy";
this.target = { x: pointer.x, y: pointer.y, dead: false };
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.state = "cursor_friend";
this.target = { x: pointer.x, y: pointer.y, dead: false };
this.goodMode = "smile";
this.thought = "\u30ab\u30fc\u30bd\u30eb\u306b\u3064\u3044\u3066\u3044\u304f";
return;
}
}
}
2026-06-19 20:52:40 +09:00
const playTrait = this.personalityProfile().play ?? 1;
2026-06-20 20:29:44 +09:00
const openPlay = this.shouldApplyPersonalityBehavior("openness", 1);
const closedPlay = this.shouldApplyPersonalityBehavior("openness", -1);
const ballRange = openPlay ? 540 : (closedPlay ? 170 : 240 + 60 * playTrait);
2026-06-19 20:52:40 +09:00
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;
2026-06-20 20:29:44 +09:00
const baseChance = openPlay ? 0.74 : (closedPlay ? 0.025 : 0.08);
2026-06-19 20:52:40 +09:00
const movingBonus = clamp(ballSpeed / 120, 0, 0.42);
const socialBonus = this.loneliness > 48 ? 0.10 : 0;
2026-06-20 20:29:44 +09:00
if (chasers < 5 && (openPlay || ballSpeed > 7 || Math.random() < baseChance * playTrait + movingBonus + socialBonus)) {
2026-06-19 20:52:40 +09:00
this.state = "play_ball";
this.target = ball;
this.goodMode = "smile";
2026-06-20 20:29:44 +09:00
this.thought = openPlay ? "\u30dc\u30fc\u30eb\u306b\u5f15\u304d\u5bc4\u305b\u3089\u308c\u3066\u3044\u308b" : "\u8ee2\u304c\u308b\u3082\u306e\u304c\u6c17\u306b\u306a\u3063\u3066\u3044\u308b";
2026-06-19 20:52:40 +09:00
return;
}
}
2026-06-19 14:00:48 +09:00
const parent = this.parentToFollow();
if (parent && !this.shouldAvoidTarget(parent)) {
const d = dist(this, parent);
if (d > 34 || (this.hunger < 78 && this.energy > 24)) {
this.state = "follow_parent";
this.target = parent;
this.bubble("?", 4.0);
this.thought = "\u89aa\u306b\u3064\u3044\u3066\u3044\u304f";
return;
}
}
2026-06-20 02:07:01 +09:00
if (friendNearby && this.hunger > 44 && friendNearby.target && (this.isZunchiSlave ? ["zunchi"] : ["sweet", "love_mochi", "fight_mochi", "sleep_drug", "grass"]).includes(friendNearby.target.type) && !friendNearby.target.dead && !this.shouldAvoidTarget(friendNearby.target)) {
2026-06-19 14:00:48 +09:00
this.state = "seek_food";
this.target = friendNearby.target;
this.foodReactTimer = Math.max(this.foodReactTimer, 0.24);
2026-06-20 20:29:44 +09:00
this.thought = "\u53cb\u9054\u3068\u4e00\u7dd2\u306b\u98df\u3079\u306b\u884c\u304f";
2026-06-19 14:00:48 +09:00
return;
}
if (this.hunger > 62) {
2026-06-20 21:41:15 +09:00
const itemTypes = this.isZunchiSlave ? ["zunchi"] : ["sweet", "love_mochi", "fight_mochi", "sleep_drug", "grass", "ant_corpse"];
2026-06-19 14:00:48 +09:00
const item = this.world.nearest(this, itemTypes, 420);
if (item) {
this.state = "seek_food";
this.target = item;
this.foodReactTimer = Math.max(this.foodReactTimer, 0.30);
this.thought = "\u98df\u3079\u7269\u3092\u63a2\u3057\u3066\u3044\u308b";
return;
}
}
2026-06-20 20:29:44 +09:00
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));
2026-06-19 14:00:48 +09:00
if (other) {
this.state = "seek_friend";
this.target = other;
this.thought = "\u4ef2\u9593\u3092\u63a2\u3057\u3066\u3044\u308b";
return;
}
}
if (this.stress > 74 / this.personalityProfile().fear || this.fearTimer > 0.22) {
this.state = "panic";
this.thought = "\u30d1\u30cb\u30c3\u30af\u306b\u306a\u3063\u3066\u3044\u308b";
return;
}
if (Math.random() < dt * 0.018) this.wanderAngle += rand(-1.7, 1.7);
this.state = "idle";
this.thought = `${this.need}\u304c\u305f\u308a\u306a\u3044`;
}
makePoop(force = 1) {
this.digest += force;
if (this.digest < 12.4) 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);
this.bubble("\u3076\u308a\u3085\u3063", 3.2, "rgba(62,84,45,0.78)");
2026-06-20 23:27:39 +09:00
if (Math.random() < 0.35) this.world.log(`${this.name}\u304c\u305a\u3093\u3061\u3092\u843d\u3068\u3057\u305f\u3002`, null, { participants: [this] });
2026-06-19 14:00:48 +09:00
}
interactWithItems(dt) {
2026-06-19 20:52:40 +09:00
const canBite = !this.isZunchiSlave && this.eatCooldown <= 0 && this.hunger > 18;
const canSweetBite = !this.isZunchiSlave && this.eatCooldown <= 0 && this.hunger > 7;
const canZunchiBite = this.eatCooldown <= 0 && this.hunger > (this.isZunchiSlave ? 16 : 104);
2026-06-19 14:00:48 +09:00
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;
2026-06-20 20:29:44 +09:00
if (((it.type === "sweet" && canSweetBite) || (it.type === "food" && canBite)) && this.foodItemHasServingLeft(it)) {
const eating = this.consumeFoodServing(it, it.type === "sweet" ? 7.2 : 8.5);
2026-06-19 14:00:48 +09:00
if (eating > 0) {
this.state = "eat";
this.target = it;
this.vx *= Math.pow(0.25, dt * 60);
this.vy *= Math.pow(0.25, dt * 60);
this.hunger -= eating * (it.type === "sweet" ? 0.75 : 0.95);
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);
2026-06-20 02:07:01 +09:00
if (this.explosionDisease) this.recoverExplosionDisease("\u305a\u3093\u3060\u9905\u3067\u7206\u767a\u75c5\u304c\u6cbb\u3063\u305f");
2026-06-20 20:29:44 +09:00
if (this.fightDisease) this.recoverFightDisease("\u305a\u3093\u3060\u9905\u3067\u304d\u305a\u3064\u304d\u75c5\u304c\u6cbb\u3063\u305f");
2026-06-19 14:00:48 +09:00
}
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) {
2026-06-20 23:27:39 +09:00
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] });
2026-06-19 14:00:48 +09:00
this.lastLog = this.world.time;
}
break;
}
}
2026-06-20 20:29:44 +09:00
if (canSweetBite && (it.type === "love_mochi" || it.type === "fight_mochi" || it.type === "sleep_drug") && this.foodItemHasServingLeft(it)) {
const eating = this.consumeFoodServing(it, 6.8);
if (eating <= 0) continue;
2026-06-19 20:52:40 +09:00
this.state = "eat";
this.target = it;
this.vx *= Math.pow(0.25, dt * 60);
this.vy *= Math.pow(0.25, dt * 60);
this.hunger -= eating * 0.82;
this.recoverHealth(eating * 0.24);
this.affection += eating * 0.04;
if (it.type === "love_mochi") {
2026-06-19 23:22:15 +09:00
this.stress = clamp(this.stress - eating * 0.25, 0, 130);
2026-06-19 20:52:40 +09:00
this.loveMochiTimer = Math.max(this.loveMochiTimer || 0, 42 + rand(4, 12));
2026-06-19 23:22:15 +09:00
this.bubble("\u2661", 2.4, "rgba(190,82,132,0.78)");
2026-06-19 20:52:40 +09:00
this.lastMealColor = "#ff7bab";
2026-06-20 20:29:44 +09:00
} else if (it.type === "fight_mochi") {
2026-06-19 23:22:15 +09:00
this.stress = clamp(this.stress + eating * 0.22, 0, 130);
this.fearTimer = Math.max(this.fearTimer || 0, 0.32);
this.fightMochiTimer = Math.max(this.fightMochiTimer || 0, 52 + rand(10, 18));
2026-06-19 20:52:40 +09:00
this.bubble("!", 2.0, "rgba(180,86,52,0.80)");
this.lastMealColor = "#e07c43";
2026-06-20 20:29:44 +09:00
} else {
this.stress = clamp(this.stress - eating * 0.10, 0, 130);
this.energy = clamp(this.energy - eating * 0.20, 0, 100);
this.fearTimer = Math.max(0, (this.fearTimer || 0) - 0.18);
this.fightMochiTimer = Math.max(0, (this.fightMochiTimer || 0) - 2.5);
this.bubble("Zzz...", 2.3, "rgba(104,84,168,0.78)");
this.lastMealColor = "#b89cff";
if (!this.sleepDisease && Math.random() < Math.min(0.98, 0.35 + eating * 0.075)) this.infectSleepDisease(it);
2026-06-19 20:52:40 +09:00
}
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);
2026-06-20 02:07:01 +09:00
this.world.spawnEatEffect(mouth.x, mouth.y, it.type === "love_mochi" ? "#ff7bab" : (it.type === "fight_mochi" ? "#e07c43" : "#b89cff"));
2026-06-19 20:52:40 +09:00
}
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) {
2026-06-20 23:27:39 +09:00
this.world.log(`${this.name}\u306f${it.type === "love_mochi" ? "\u3078\u3053\u9905" : (it.type === "fight_mochi" ? "\u3051\u3093\u304b\u9905" : "\u306d\u3080\u308a\u85ac")}\u3092\u3057\u3070\u3089\u304f\u5473\u308f\u3063\u305f\u3002`, "food", { participants: [this] });
2026-06-19 20:52:40 +09:00
this.lastLog = this.world.time;
}
break;
}
2026-06-19 14:00:48 +09:00
if (canBite && it.type === "grass") {
2026-06-20 20:29:44 +09:00
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);
2026-06-19 14:00:48 +09:00
this.state = "eat";
this.target = 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);
it.amount -= bite;
it.eatenAmount = (it.eatenAmount || 0) + bite;
it.growth = clamp((it.growth ?? it.amount / 120) - bite / 150, 0.04, 1.2);
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;
}
2026-06-20 21:41:15 +09:00
if (canBite && it.type === "ant_corpse") {
const bite = Math.min(it.amount, rand(3.2, 5.4));
if (bite > 0) {
this.state = "eat";
this.target = it;
this.vx *= Math.pow(0.18, dt * 60);
this.vy *= Math.pow(0.18, dt * 60);
it.amount -= bite;
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;
}
}
2026-06-19 14:00:48 +09:00
if (it.type === "zunchi") {
const touch = clamp(1 - d / (this.radius + it.r + 12), 0, 1);
2026-06-20 20:29:44 +09:00
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.");
}
2026-06-19 20:52:40 +09:00
this.zunchiStain = clamp((this.zunchiStain || 0) + touch * dt * (this.isZunchiSlave ? 5.2 : 9.5), 0, 100);
if (canZunchiBite) {
2026-06-20 20:29:44 +09:00
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);
2026-06-19 14:00:48 +09:00
this.state = "eat";
this.target = it;
this.vx *= Math.pow(0.18, dt * 60);
this.vy *= Math.pow(0.18, dt * 60);
it.amount -= bite;
2026-06-19 20:52:40 +09:00
this.hunger -= bite * (this.isZunchiSlave ? 0.66 : 0.18);
this.recoverHealth(bite * (this.isZunchiSlave ? 0.10 : 0.02));
2026-06-20 20:29:44 +09:00
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.");
2026-06-19 20:52:40 +09:00
this.zunchiStain = clamp((this.zunchiStain || 0) + bite * (this.isZunchiSlave ? 0.75 : 1.75), 0, 100);
this.lastMealColor = "#6b8d3f";
2026-06-19 14:00:48 +09:00
this.eatTimer = Math.max(this.eatTimer, 0.24);
this.eatCooldown = rand(0.9, 1.4);
2026-06-19 23:22:15 +09:00
this.bubble(this.isZunchiSlave ? "\u3082\u3050" : "...", 2.2, "rgba(74,91,50,0.78)");
2026-06-19 14:00:48 +09:00
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;
}
}
2026-06-20 02:07:01 +09:00
if (it.type === "water" || it.type === "water_bowl") {
this.applyWaterEffect(dt, it.type);
if (it.type === "water") it.amount -= dt * 2.4;
2026-06-19 14:00:48 +09:00
if (this.type === "teary" || this.type === "cry") this.affection += dt * 0.18;
}
if (it.type === "stone") {
this.stress -= dt * 0.85;
this.energy += dt * 0.18;
if (this.need === "\u3042\u305f\u305f\u304b\u3055") this.affection += dt * 0.25;
}
2026-06-19 20:52:40 +09:00
if (this.isSleepFurniture(it)) {
2026-06-19 14:00:48 +09:00
if (this.energy < 74 + 6 * this.personalityProfile().sleep) {
const occ = this.world.bedOccupancy(it);
const comfort = this.world.bedComfort(it);
this.state = "sleep";
2026-06-19 20:52:40 +09:00
this.target = it;
2026-06-19 14:00:48 +09:00
this.sleeping = true;
if (this.world.time > this.nextSleepBubbleAt) {
this.nextSleepBubbleAt = this.world.time + 4.5;
2026-06-19 20:52:40 +09:00
const crowded = occ > 5;
this.world.spawnBubble(this.x, this.y - this.radius * 1.18, crowded ? "mm..." : "Zzz...", "rgba(65,70,92,0.72)");
2026-06-19 14:00:48 +09:00
}
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);
}
}
2026-06-19 20:52:40 +09:00
if (it.type === "ball") {
const touch = clamp(1 - d / (this.radius + it.r + 12), 0, 1);
2026-06-20 20:29:44 +09:00
if (this.state === "play_ball" || this.target === it || this.shouldApplyPersonalityBehavior("openness", 1)) {
2026-06-19 20:52:40 +09:00
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);
}
}
2026-06-20 20:29:44 +09:00
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");
}
}
2026-06-19 14:00:48 +09:00
if (it.type === "trace") {
this.loneliness -= dt * 0.25;
2026-06-20 20:29:44 +09:00
this.stress += dt * 0.06 * this.personalityProfile().fear;
2026-06-19 14:00:48 +09:00
}
}
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);
}
interactWithOthers(dt) {
let closeCount = 0;
for (const o of this.world.nearbyTarinai(this.x, this.y, 78)) {
if (o === this || o.dead) continue;
const dx = o.x - this.x, dy = o.y - this.y;
const d = Math.hypot(dx, dy);
if (d < 0.01 || d > 74) continue;
closeCount += 1;
2026-06-20 15:55:52 +09:00
const fightMochiContact = this.world.canFightPair?.(this, o) && (this.hasFightMochiEffect() || o.hasFightMochiEffect());
2026-06-19 23:22:15 +09:00
if (fightMochiContact && d <= Math.max(42, this.radius + o.radius + 16)) {
if (this.world.startForcedFight?.(this, o)) continue;
}
2026-06-19 20:52:40 +09:00
if (this.zunchiDisease && !o.zunchiDisease && (o.zunchiDiseaseCooldown || 0) <= 0 && d < 54 && Math.random() < dt * 0.065 * clamp(1 - d / 58, 0.12, 1)) {
2026-06-20 23:27:39 +09:00
if (o.infectZunchiDisease(this)) this.world.log(`${this.name}\u306e\u305a\u3093\u3061\u75c5\u304c${o.name}\u306b\u3046\u3064\u3063\u305f\u3002`, "accident", { participants: [o, this] });
2026-06-19 20:52:40 +09:00
}
if (o.zunchiDisease && !this.zunchiDisease && (this.zunchiDiseaseCooldown || 0) <= 0 && d < 54 && Math.random() < dt * 0.065 * clamp(1 - d / 58, 0.12, 1)) {
2026-06-20 23:27:39 +09:00
if (this.infectZunchiDisease(o)) this.world.log(`${o.name}\u306e\u305a\u3093\u3061\u75c5\u304c${this.name}\u306b\u3046\u3064\u3063\u305f\u3002`, "accident", { participants: [o, this] });
2026-06-19 20:52:40 +09:00
}
const pairBond = this.world.areCoParents?.(this, o);
2026-06-19 14:00:48 +09:00
const overlap = this.radius + o.radius - d;
if (overlap > -2) {
2026-06-19 20:52:40 +09:00
const push = (overlap + 6) * (pairBond ? 0.08 : 0.17);
2026-06-19 14:00:48 +09:00
this.vx -= (dx / d) * push;
this.vy -= (dy / d) * push;
2026-06-19 20:52:40 +09:00
if (!pairBond && Math.random() < dt * 1.2) this.surpriseTimer = Math.max(this.surpriseTimer, 0.12);
}
if (pairBond && d > 26 && d < 120) {
this.vx += (dx / d) * dt * 4.8;
this.vy += (dy / d) * dt * 4.8;
2026-06-19 14:00:48 +09:00
}
if (d < 60) {
const rel = this.relationTo(o.id);
const pdef = this.personalityProfile();
this.loneliness -= dt * 1.65 * this.trait.social;
this.affection += dt * 0.28 * this.trait.social;
this.adjustRelation(o, dt * 0.18 * pdef.relation * (rel.fear > 10 ? 0.35 : 1), -dt * 0.018, "nearby");
2026-06-20 20:29:44 +09:00
if ((rel.affinity || 0) >= FRIEND_AFFINITY_THRESHOLD) {
2026-06-19 14:00:48 +09:00
this.stress = clamp(this.stress - dt * 0.62 * this.trait.social, 0, 130);
this.affection = clamp(this.affection + dt * 0.18, 0, 80);
2026-06-20 23:27:39 +09:00
if (this.world.relationNotice(this.id, o.id, "friend-calm", 55)) this.world.log(`${this.name}\u306f${o.name}\u3068\u3044\u308b\u3068\u5c11\u3057\u843d\u3061\u7740\u304f\u3002`, "relation", { participants: [o, this] });
2026-06-19 14:00:48 +09:00
}
2026-06-20 20:29:44 +09:00
if ((rel.affinity || 0) >= FRIEND_AFFINITY_THRESHOLD && this.world.relationNotice(this.id, o.id, "friend", 32)) {
2026-06-20 23:27:39 +09:00
this.world.log(`${this.name}\u306f\u3088\u304f${o.name}\u306e\u8fd1\u304f\u306b\u3044\u308b\u3002`, "relation", { participants: [o, this] });
2026-06-19 14:00:48 +09:00
}
if (this.type === "angry") this.stress += dt * 0.08;
if (this.type === "teary" && o.type === "teary") this.stress -= dt * 0.12;
}
const fightingThis = this.fightTimer > 0 && (this.fightTargetId === o.id || (this.fightTargetIds || []).includes(o.id));
if (fightingThis && d < 36 && this.id < o.id) {
const pulse = Math.sin(this.world.time * 18 + this.age * 0.37) > 0.35;
if (pulse && this.world.time > this.nextHeadbutt) {
const nx = dx / d, ny = dy / d;
this.vx -= nx * rand(24, 44);
this.vy -= ny * rand(24, 44);
o.vx += nx * rand(34, 64);
o.vy += ny * rand(34, 64);
this.stress = clamp(this.stress + 1.6, 0, 130);
o.stress = clamp(o.stress + 2.8, 0, 130);
2026-06-19 23:22:15 +09:00
this.damage(1.2, "\u55a7\u5629");
o.damage(1.6, "\u55a7\u5629");
2026-06-19 14:00:48 +09:00
this.hurtTimer = Math.max(this.hurtTimer, 1.4);
o.hurtTimer = Math.max(o.hurtTimer, 1.8);
this.adjustRelation(o, -0.16, 0.12, "fight");
o.adjustRelation(this, -0.22, 0.28 * o.personalityProfile().fear, "fight");
if (Math.random() < 0.22 || o.energy < 24) {
o.fallTimer = Math.max(o.fallTimer, 0.58);
o.fallMax = Math.max(o.fallMax || 0.58, o.fallTimer);
o.fallDir = nx > 0 ? 1 : -1;
this.world.spawnFallEffect(o.x, o.y + o.radius * 0.45, 0.55);
}
if (o.energy + o.mood * 0.25 < this.energy + this.mood * 0.25 - 8) {
o.defeatedById = this.id;
o.fightWinnerId = this.id;
}
this.world.spawnHeadbuttEffect((this.x + o.x) / 2, (this.y + o.y) / 2 - 5);
this.nextHeadbutt = this.world.time + rand(0.34, 0.55);
}
}
const relA = this.relationTo(o.id);
const relB = o.relationTo(this.id);
2026-06-19 20:52:40 +09:00
const battleDrug = this.hasFightMochiEffect() || o.hasFightMochiEffect();
const loveDrug = this.hasLoveMochiEffect() || o.hasLoveMochiEffect();
2026-06-20 20:29:44 +09:00
const aggressiveThis = this.shouldApplyPersonalityBehavior("aggression", 1);
const aggressiveOther = o.shouldApplyPersonalityBehavior?.("aggression", 1);
const calmThis = this.shouldApplyPersonalityBehavior("aggression", -1);
const calmOther = o.shouldApplyPersonalityBehavior?.("aggression", -1);
const fightRisk = (this.mood < 55 || o.mood < 55 || this.stress > 46 || o.stress > 46 || this.type === "angry" || o.type === "angry" || closeCount > 3 || aggressiveThis || aggressiveOther || battleDrug || o.isZunchiSlave || this.isZunchiSlave);
2026-06-19 20:52:40 +09:00
const fearBrake = battleDrug ? clamp(1 - ((relA.fear || 0) + (relB.fear || 0)) / 220, 0.74, 1.15) : clamp(1 - ((relA.fear || 0) + (relB.fear || 0)) / 120, 0.34, 1);
const friendBrake = battleDrug ? clamp(1 - Math.max(relA.affinity || 0, relB.affinity || 0) / 180, 0.72, 1.12) : clamp(1 - Math.max(relA.affinity || 0, relB.affinity || 0) / 90, 0.48, 1);
2026-06-20 20:29:44 +09:00
const personalityRisk = this.personalityProfile().fight * o.personalityProfile().fight * (calmThis ? 0.62 : 1) * (calmOther ? 0.62 : 1);
2026-06-19 20:52:40 +09:00
const conflictBias = (battleDrug ? 2.9 : 1) * (o.isZunchiSlave ? 2.2 : 1) * (this.isZunchiSlave ? 1.15 : 1);
2026-06-19 14:00:48 +09:00
const birthPeace = (this.postBirthPeaceTimer || 0) > 0 || (o.postBirthPeaceTimer || 0) > 0;
2026-06-19 20:52:40 +09:00
const familyFightBlocked = this.world.areParentChild ? this.world.areParentChild(this, o) : false;
const pairFightBlocked = this.world.areCoParents ? this.world.areCoParents(this, o) : false;
2026-06-20 15:55:52 +09:00
if (this.world.canFightPair?.(this, o) && d < 56 && !familyFightBlocked && !pairFightBlocked && !birthPeace && fightRisk && this.fightCooldown <= 0 && o.fightCooldown <= 0 && Math.random() < dt * 0.23 * personalityRisk * fearBrake * friendBrake * conflictBias) {
2026-06-19 14:00:48 +09:00
this.world.startConflict(this, o);
}
2026-06-20 20:29:44 +09:00
if (!this.isZunchiSlave && !o.isZunchiSlave && !this.sleepDisease && !o.sleepDisease && !this.fightDisease && !o.fightDisease && d < (loveDrug ? 44 : 36) && this.reproductionTimer <= 0 && o.reproductionTimer <= 0 && this.fightTimer <= 0 && o.fightTimer <= 0) {
2026-06-19 20:52:40 +09:00
const enough = loveDrug ? (this.hunger < 90 && o.hunger < 92 && this.energy > 18 && o.energy > 18) : (this.hunger < 55 && o.hunger < 60 && this.energy > 44 && o.energy > 44);
const lonelyBond = loveDrug ? (this.loneliness > 4 || o.loneliness > 4 || this.affection > 0 || o.affection > 0) : (this.loneliness > 24 || o.loneliness > 24);
if (enough && lonelyBond && Math.random() < dt * (loveDrug ? 0.72 : 0.25)) {
2026-06-19 14:00:48 +09:00
this.world.startBirthRitual(this, o);
}
}
}
if (closeCount > 8) {
2026-06-20 20:29:44 +09:00
this.stress += dt * 0.9 * this.personalityProfile().fear;
2026-06-19 14:00:48 +09:00
this.energy -= dt * 0.25;
this.fearTimer = Math.max(this.fearTimer, 0.18);
}
if (this.loneliness < 12 && Math.random() < dt * 0.002) {
2026-06-20 23:27:39 +09:00
this.world.log(`${this.name}\u306f\u8ab0\u304b\u306e\u8fd1\u304f\u3067\u3001\u305f\u308a\u306a\u3044\u3053\u3068\u3092\u5c11\u3057\u5fd8\u308c\u305f\u3002`, null, { participants: [this] });
2026-06-19 14:00:48 +09:00
}
}
move(dt) {
2026-06-20 02:07:01 +09:00
if (this.freezeSleepDiseaseMotion(dt)) return;
2026-06-19 14:00:48 +09:00
let ax = 0, ay = 0;
2026-06-19 23:22:15 +09:00
const baseSpeed = 46.5 * 1.5 * this.trait.speed * (this.zunchiDisease ? 0.5 : 1) * (this.energy < 20 ? 0.42 : 1) * (this.fallTimer > 0 ? 0.35 : 1);
2026-06-19 14:00:48 +09:00
if (this.state === "sleep") {
2026-06-20 02:07:01 +09:00
const bedComfort = this.isSleepFurniture(this.target) ? this.world.bedComfort(this.target) * (this.target?.type === "nest_box" ? 1.55 : 1) : 0.72;
2026-06-19 20:52:40 +09:00
const bedCrowd = this.isSleepFurniture(this.target) ? this.world.bedOccupancy(this.target) : 0;
const crowdedSleep = bedCrowd > 5;
2026-06-19 14:00:48 +09:00
this.energy = clamp(this.energy + dt * (this.target ? 1.8 + bedComfort * 1.35 : 2.1), 0, 100);
2026-06-19 20:52:40 +09:00
this.stress = clamp(this.stress + dt * (crowdedSleep ? 0.18 * Math.max(1, bedCrowd - 5) : -0.12 * bedComfort), 0, 130);
2026-06-19 14:00:48 +09:00
this.hunger = clamp(this.hunger + dt * 0.018, 0, 115);
2026-06-20 20:29:44 +09:00
if (this.target && !this.target.dead && this.target.type === "nest_box") {
this.updateNestBoxPresence(dt);
this.vx = 0;
this.vy = 0;
return;
}
2026-06-19 20:52:40 +09:00
if (this.target && !this.target.dead && this.isSleepFurniture(this.target)) {
2026-06-19 14:00:48 +09:00
const spot = this.sleepSpotFor(this.target);
const dx = spot.x - this.x;
const dy = spot.y - this.y;
const d = Math.hypot(dx, dy);
2026-06-19 20:52:40 +09:00
if (d > 2.2) {
2026-06-19 14:00:48 +09:00
const step = Math.min(d, dt * (12 + bedComfort * 14));
this.x += dx / d * step;
this.y += dy / d * step;
2026-06-19 20:52:40 +09:00
} else {
this.x = spot.x;
this.y = spot.y;
2026-06-19 14:00:48 +09:00
}
}
2026-06-20 02:07:01 +09:00
this.updateNestBoxPresence(dt);
2026-06-19 14:00:48 +09:00
this.vx *= Math.pow(0.55, dt * 60);
this.vy *= Math.pow(0.55, dt * 60);
return;
}
if (this.birthRitualTimer > 0.04 || this.state === "birth_ritual") {
const partner = this.world.tarinai.find(o => o.id === this.birthPartnerId && !o.dead);
if (partner) {
const midX = (this.x + partner.x) / 2;
const midY = (this.y + partner.y) / 2;
2026-06-19 20:52:40 +09:00
const pdx = partner.x - this.x;
const pdy = partner.y - this.y;
const plen = Math.max(0.001, Math.hypot(pdx, pdy));
const nx = -pdy / plen;
const ny = pdx / plen;
const desiredGap = this.radius * 0.36;
const side = this.birthRitualRole || 1;
const tx = midX + nx * desiredGap * side;
const ty = midY + ny * desiredGap * side;
this.vx += (tx - this.x) * dt * 3.8;
this.vy += (ty - this.y) * dt * 3.8;
2026-06-19 14:00:48 +09:00
}
2026-06-19 20:52:40 +09:00
this.vx *= Math.pow(0.48, dt * 60);
this.vy *= Math.pow(0.48, dt * 60);
this.x += Math.sin(this.world.time * 26 + this.age) * 0.20;
2026-06-19 14:00:48 +09:00
return;
}
2026-06-20 21:41:15 +09:00
if (this.intimidateTimer > 0.04 || this.state === "intimidate" || this.state === "ant_intimidate") {
2026-06-19 14:00:48 +09:00
this.vx *= Math.pow(0.22, dt * 60);
this.vy *= Math.pow(0.22, dt * 60);
return;
}
if (this.eatTimer > 0.04 || this.state === "eat") {
this.vx = 0;
this.vy = 0;
return;
}
if (this.state === "panic") {
const targetDist = this.target && !this.target.dead ? distXY(this.x, this.y, this.target.x, this.target.y) : Infinity;
const speed = Math.hypot(this.vx, this.vy);
this.panicStuckTimer = speed < 8 ? (this.panicStuckTimer || 0) + dt : 0;
if (!this.target || this.target.dead || targetDist < 28 || this.panicStuckTimer > 0.75) {
this.target = this.panicDestination(null, true);
}
}
if (this.target && !this.target.dead) {
2026-06-19 20:52:40 +09:00
const moveTarget = this.state === "seek_bed" && this.isSleepFurniture(this.target) ? this.sleepSpotFor(this.target) : this.target;
const dx = moveTarget.x - this.x;
const dy = moveTarget.y - this.y;
2026-06-19 14:00:48 +09:00
const d = Math.hypot(dx, dy) || 1;
2026-06-19 20:52:40 +09:00
if (this.state === "seek_bed" && this.isSleepFurniture(this.target)) {
const bedDx = this.target.x - this.x;
const bedDy = this.target.y - this.y;
const bedDist = Math.hypot(bedDx, bedDy);
2026-06-20 20:29:44 +09:00
const reachedSleepFurniture = this.target.type === "nest_box"
? (this.insideNestBoxId === this.target.id || d <= Math.max(this.radius + 34, this.target.r * 1.08) || bedDist <= Math.max(this.target.r * 1.05, 62))
: (d <= Math.max(this.radius + 8, 26) || bedDist <= Math.max(this.target.r * 0.58, 20));
if (reachedSleepFurniture) {
2026-06-19 20:52:40 +09:00
this.state = "sleep";
this.sleeping = true;
2026-06-20 20:29:44 +09:00
if (this.target.type === "nest_box") this.enterNestBox(this.target, dt);
2026-06-19 20:52:40 +09:00
return;
}
}
2026-06-19 14:00:48 +09:00
let sign = (this.state === "panic" || this.state === "cursor_enemy") ? -1 : 1;
if (this.target?.detour) sign = 1;
let strength = 1;
if (this.state === "follow_parent") {
strength = d < 42 ? 0.18 : 1.22;
sign = 1;
} else if (this.state === "cursor_friend") {
strength = d < 30 ? 0.12 : 0.95;
sign = 1;
} else if (this.state === "cursor_enemy") {
strength = d < 150 ? 1.35 : 0.35;
sign = -1;
2026-06-19 20:52:40 +09:00
} else if (this.state === "play_ball") {
strength = d < this.radius + 18 ? 1.75 : 1.28;
sign = 1;
2026-06-19 14:00:48 +09:00
} else if (this.state === "fight") {
const headbutt = Math.sin(this.world.time * 18 + this.age * 0.37);
if (d < 18) {
strength = 1.05;
sign = -1;
} else {
strength = headbutt > -0.15 ? 2.65 : 0.65;
sign = headbutt > -0.15 ? 1 : -1;
}
2026-06-20 21:41:15 +09:00
} else if (this.state === "ant_attack") {
strength = d < this.radius + ((this.target && this.target.r) || 4) + 6 ? 0.18 : 1.32;
sign = 1;
if (d < this.radius + ((this.target && this.target.r) || 4) + 10) {
const aggression = Math.max(0, this.currentPersonality?.aggression || 0);
if ((this.world.time || 0) >= (this.nextAntAttackAt || 0)) {
this.nextAntAttackAt = (this.world.time || 0) + 0.34;
if (Number.isFinite(this.target.hp)) this.target.hp = Math.max(0, this.target.hp - (4.6 + aggression * 3.4));
this.fearTimer = Math.max(this.fearTimer, 0.12);
this.thought = "\u30a2\u30ea\u3092\u653b\u6483\u3057\u3066\u3044\u308b";
if (Math.random() < 0.55) this.bubble("!", 0.8, "rgba(145,68,52,0.76)");
}
}
2026-06-19 14:00:48 +09:00
}
ax += (dx / d) * baseSpeed * sign * strength;
ay += (dy / d) * baseSpeed * sign * strength;
} else if (this.state === "panic") {
this.wanderAngle += rand(-3.8, 3.8) * dt;
ax += Math.cos(this.wanderAngle) * baseSpeed * 1.5;
ay += Math.sin(this.wanderAngle) * baseSpeed * 1.5;
2026-06-19 20:52:40 +09:00
} else if (this.state === "zunchi_sick") {
this.wanderAngle += rand(-2.4, 2.4) * dt + Math.sin(this.world.time * 1.7 + this.age) * dt * 0.8;
ax += Math.cos(this.wanderAngle) * baseSpeed * 0.72;
ay += Math.sin(this.wanderAngle) * baseSpeed * 0.72;
2026-06-20 02:07:01 +09:00
} else if (this.state === "fight_sick") {
this.wanderAngle += rand(-4.2, 4.2) * dt + Math.sin(this.world.time * 3.2 + this.age) * dt * 1.6;
ax += Math.cos(this.wanderAngle) * baseSpeed * 0.88;
ay += Math.sin(this.wanderAngle) * baseSpeed * 0.88;
2026-06-19 14:00:48 +09:00
} else {
ax += Math.cos(this.wanderAngle) * baseSpeed * 0.34;
ay += Math.sin(this.wanderAngle) * baseSpeed * 0.34;
}
this.vx += ax * dt;
this.vy += ay * dt;
this.vx *= Math.pow(0.84, dt * 8);
this.vy *= Math.pow(0.84, dt * 8);
2026-06-20 02:07:01 +09:00
const maxV = this.state === "panic" ? 116 : (this.state === "zunchi_sick" ? 54 : (this.state === "fight_sick" ? 68 : 76));
2026-06-19 14:00:48 +09:00
const v = Math.hypot(this.vx, this.vy);
if (v > maxV) {
this.vx = this.vx / v * maxV;
this.vy = this.vy / v * maxV;
}
2026-06-20 20:29:44 +09:00
const impulseVx = Number.isFinite(this.impulseVx) ? this.impulseVx : 0;
const impulseVy = Number.isFinite(this.impulseVy) ? this.impulseVy : 0;
this.x += (this.vx + impulseVx) * dt;
this.y += (this.vy + impulseVy) * dt;
const impulseDrag = Math.pow(0.58, dt * 3.2);
this.impulseVx = Math.abs(impulseVx) < 0.35 ? 0 : impulseVx * impulseDrag;
this.impulseVy = Math.abs(impulseVy) < 0.35 ? 0 : impulseVy * impulseDrag;
2026-06-19 14:00:48 +09:00
2026-06-19 20:52:40 +09:00
if (this.entryTimer > 0) {
this.entryTimer = Math.max(0, this.entryTimer - dt);
if (this.x >= CONFIG.worldPadding && this.y >= CONFIG.worldPadding && this.x <= this.world.w - CONFIG.worldPadding && this.y <= this.world.h - CONFIG.worldPadding) {
this.entryTimer = 0;
}
}
const p = this.entryTimer > 0 ? -this.radius * 2.4 : CONFIG.worldPadding;
const maxX = this.entryTimer > 0 ? this.world.w + this.radius * 2.4 : this.world.w - CONFIG.worldPadding;
const maxY = this.entryTimer > 0 ? this.world.h + this.radius * 2.4 : this.world.h - CONFIG.worldPadding;
2026-06-19 14:00:48 +09:00
if (this.x < p) { this.x = p; this.vx = Math.abs(this.vx) * 0.5; this.wanderAngle = rand(-0.8, 0.8); this.surpriseTimer = 0.15; }
if (this.y < p) { this.y = p; this.vy = Math.abs(this.vy) * 0.5; this.wanderAngle = rand(0.3, 1.8); this.surpriseTimer = 0.15; }
2026-06-19 20:52:40 +09:00
if (this.x > maxX) { this.x = maxX; this.vx = -Math.abs(this.vx) * 0.5; this.wanderAngle = rand(2.5, 3.8); this.surpriseTimer = 0.15; }
if (this.y > maxY) { this.y = maxY; this.vy = -Math.abs(this.vy) * 0.5; this.wanderAngle = rand(-2.2, -0.7); this.surpriseTimer = 0.15; }
2026-06-19 14:00:48 +09:00
}
2026-06-19 23:22:15 +09:00
checkLife(dt = 0.016) {
2026-06-19 14:00:48 +09:00
let reason = "";
2026-06-19 23:22:15 +09:00
if (this.energy <= 0) {
reason = this.dominantDeathCause("", { lowHealth: true }) || "\u4e8b\u6545";
}
else if (this.hunger >= 108) reason = "\u98e2\u9913";
2026-06-20 20:29:44 +09:00
else if (this.age >= this.lifeSpan) reason = "\u5929\u5bff\u3092\u5168\u3046\u3057\u305f";
2026-06-19 23:22:15 +09:00
else if (this.stress >= 124) {
reason = this.dominantDeathCause("\u30b9\u30c8\u30ec\u30b9\u904e\u591a", { stressDeath: true }) || "\u30b9\u30c8\u30ec\u30b9\u904e\u591a";
}
else if (this.mood <= 20) {
const lowMood = clamp((20 - this.mood) / 20, 0, 1);
const bodyLoad = clamp((this.hunger - 72) / 42, 0, 1) * 0.018 + clamp((28 - this.energy) / 28, 0, 1) * 0.020 + clamp((this.stress - 86) / 44, 0, 1) * 0.022;
const deathRisk = 0.010 + lowMood * 0.060 + bodyLoad;
if (Math.random() < Math.max(0, dt) * deathRisk) {
reason = this.dominantDeathCause("", { lowMood: true }) || "\u30b9\u30c8\u30ec\u30b9\u904e\u591a";
}
}
2026-06-19 14:00:48 +09:00
if (reason) this.die(reason);
}
die(reason) {
if (this.dead) return;
2026-06-19 23:22:15 +09:00
const finalReason = this.normalizeDeathReason ? this.normalizeDeathReason(reason) : (reason || "\u4e8b\u6545");
2026-06-19 14:00:48 +09:00
this.dead = true;
2026-06-19 23:22:15 +09:00
this.deathReason = finalReason;
2026-06-19 14:00:48 +09:00
this.world.deadCount += 1;
this.world.items.push(new Item("trace", this.x, this.y));
this.world.items.push(new Item("splat", this.x, this.y));
if (Math.random() < 0.55) {
const spot = this.world.findGrassPlantingSpot(this.x, this.y, { allowOriginal: false, minRadius: 26, maxRadius: 76 });
if (spot) this.world.items.push(new Item("grass", spot.x, spot.y));
}
2026-06-19 23:22:15 +09:00
this.world.markDead(this, finalReason);
2026-06-20 23:27:39 +09:00
this.releasedLiveToken = this.liveToken || 0;
this.world.releaseTarinaiLiveId?.(this);
2026-06-19 14:00:48 +09:00
audio.death();
2026-06-20 23:27:39 +09:00
this.world.log(`${this.name}\u306f\u9759\u304b\u306a\u75d5\u8de1\u3092\u6b8b\u3057\u305f\u3002\u6b7b\u56e0: ${finalReason}`, "death", { participants: [this] });
2026-06-19 14:00:48 +09:00
if (this.world.selected === this) this.world.selected = null;
}
draw(ctx, t, lighting = null) {
if (this.dead) return;
const lightState = lighting || getLightingState(this.world);
const sprite = this.spriteId();
2026-06-19 23:22:15 +09:00
const img = typeof getRenderableImage === "function" ? getRenderableImage(sprite, "smile") : (images.get(sprite) || images.get(this.type));
const metrics = getImageMetrics(sprite) || getImageMetrics(this.type) || getImageMetrics("smile");
2026-06-19 14:00:48 +09:00
const w = metrics?.w || 512;
const h = metrics?.h || 512;
const moveSpeed = Math.hypot(this.vx, this.vy);
const gait = this.state === "sleep" ? 0.8 : clamp(moveSpeed / 42, 0.2, 1.8);
const breathing = Math.sin(t * (this.state === "sleep" ? 1.1 : 2.3) + this.age * 0.18);
const headbuttPulse = this.state === "fight" ? Math.max(0, Math.sin(t * 18 + this.age * 0.37)) : 0;
const eatingShake = (this.eatTimer > 0.02 || this.grassEatTimer > 0) ? Math.sin(t * 54 + this.age) * clamp(Math.max(this.eatTimer / 0.32, this.grassEatTimer / 0.22), 0, 1) : 0;
const fallPose = this.fallTimer > 0 ? clamp(this.fallTimer / Math.max(this.fallMax || 1, 0.01), 0, 1) : 0;
const fallLift = Math.sin(fallPose * Math.PI) * this.radius * 0.20;
const intimidateShake = this.intimidateTimer > 0.04 ? Math.sin(t * 44 + this.age) * this.radius * 0.09 : 0;
const fearShakeY = this.intimidatedTimer > 0.04 ? Math.sin(t * 52 + this.age * 0.7) * this.radius * 0.11 : 0;
const birthShake = this.birthRitualTimer > 0.04 ? Math.sin(t * 34 + this.age) * this.radius * 0.07 : 0;
const bob = breathing * (this.state === "sleep" ? 0.9 : 1.2 + gait * 1.6) + headbuttPulse * 0.9 + fallLift + birthShake * 0.25;
const blastSpin = this.blastSpinTimer > 0.04 ? this.fallDir * ((this.blastSpinMax || 1) - this.blastSpinTimer) * Math.PI * 70.0 : 0;
const fallSpin = this.blastSpinTimer > 0.04 ? blastSpin : (fallPose > 0 ? this.fallDir * (1 - fallPose) * Math.PI * 2 : 0);
2026-06-20 02:07:01 +09:00
const diseaseSpin = this.fightDisease ? (t * 3.6 + this.age) : 0;
const motionTilt = clamp(this.vx / 240, -0.22, 0.22) + (this.state === "fight" ? headbuttPulse * 0.05 : 0) + eatingShake * 0.105 + this.fallDir * fallPose * 0.18 + fallSpin + diseaseSpin;
2026-06-19 20:52:40 +09:00
const insideCardboard = false;
2026-06-19 14:00:48 +09:00
let drawW = w * this.scale * 0.30;
let drawH = h * this.scale * 0.30;
2026-06-19 20:52:40 +09:00
if (insideCardboard) {
drawW *= 0.72;
drawH *= 0.72;
}
2026-06-19 14:00:48 +09:00
if (sprite === "stretch") {
drawW *= 1.16;
drawH *= 1.24;
}
if (sprite === "intimidate") {
drawW *= 1.32;
drawH *= 1.32;
}
if (sprite === "birth_ritual") {
drawW *= 0.64;
drawH *= 0.64;
}
if (fallPose > 0) {
drawW *= 1 + fallPose * 0.08;
drawH *= 1 - fallPose * 0.07;
}
const squish = this.state === "sleep"
? 1 + breathing * 0.018
: 1 + Math.sin(t * (4.5 + gait * 1.6) + this.age) * (0.012 + gait * 0.018) + headbuttPulse * 0.03;
const faceRight = this.state === "sleep"
? this.sleepFacing > 0
: this.facingDir() > 0;
const angle = (faceRight ? 1 : -1) * (Math.PI / 12) + motionTilt;
2026-06-20 15:55:52 +09:00
const nestHideAlpha = this.nestFade ? clamp(1 - this.nestFade, 0, 1) : 1;
if (nestHideAlpha <= 0.01) return;
2026-06-19 14:00:48 +09:00
const shadow = projectedShadowParams(lightState, 0.95 + this.scale * 0.8);
const shadowContactY = this.y + bob + (drawH / squish) * 0.30;
2026-06-19 23:22:15 +09:00
if (img) {
drawImageProjectedShadow(ctx, img, this.x, shadowContactY, drawW * squish, drawH / squish, shadow, {
faceRight,
2026-06-20 02:07:01 +09:00
alpha: shadow.alpha * (this.state === "sleep" ? 1.10 : 0.92) * nestHideAlpha,
2026-06-19 23:22:15 +09:00
widthScale: this.state === "sleep" ? 1.12 : 1,
heightScale: this.state === "sleep" ? 0.88 : 1,
});
} else {
2026-06-20 02:07:01 +09:00
drawProjectedShadow(ctx, this.x, shadowContactY, drawW * 0.52, drawH * 0.13, { ...shadow, alpha: shadow.alpha * 0.82 * nestHideAlpha });
2026-06-19 23:22:15 +09:00
}
2026-06-19 14:00:48 +09:00
drawCreatureGlow(ctx, this, drawW, drawH, lightState);
if (this.birthRitualTimer > 0.04 && this.birthRitualLeader) {
const partner = this.world.tarinai.find(o => o.id === this.birthPartnerId && !o.dead);
const hx = partner ? (this.x + partner.x) / 2 : this.x;
const hy = partner ? Math.min(this.y, partner.y) - Math.max(drawH, partner.radius * 2.6) * 0.50 : this.y - drawH * 0.60;
const pulse = 1 + Math.sin(t * 5.5 + this.age) * 0.10;
const size = Math.max(10, this.radius * 0.62) * pulse;
drawHeartShape(ctx, hx, hy, size, { fill: "rgba(240,91,135,0.90)", stroke: "rgba(255,252,246,0.92)" });
}
ctx.save();
ctx.translate(this.x + intimidateShake, this.y + bob + fearShakeY);
2026-06-20 02:07:01 +09:00
const observeKind = this.world.observeHighlightKind ? this.world.observeHighlightKind(this) : "";
if (observeKind && this.world.selected !== this) {
const style = {
parent: { color: "rgba(86,142,232,0.86)", label: "\u89aa" },
child: { color: "rgba(93,170,92,0.88)", label: "\u5b50" },
2026-06-20 20:29:44 +09:00
friend: { color: "rgba(240,91,135,0.90)", label: "\u53cb\u9054" },
2026-06-20 02:07:01 +09:00
enemy: { color: "rgba(214,82,66,0.90)", label: "\u6575\u5bfe" },
}[observeKind] || { color: "rgba(255,250,220,0.78)", label: "" };
ctx.save();
2026-06-20 15:55:52 +09:00
ctx.globalAlpha = 0.96 * nestHideAlpha;
2026-06-20 02:07:01 +09:00
ctx.shadowColor = style.color;
ctx.shadowBlur = 16;
ctx.strokeStyle = style.color;
ctx.lineWidth = 5.2;
ctx.beginPath();
ctx.ellipse(0, drawH * 0.14, drawW * 0.78, drawH * 0.60, 0, 0, Math.PI * 2);
ctx.stroke();
ctx.shadowBlur = 0;
ctx.strokeStyle = "rgba(255,255,255,0.92)";
ctx.lineWidth = 2.0;
ctx.setLineDash([8, 5]);
ctx.beginPath();
ctx.ellipse(0, drawH * 0.14, drawW * 0.86, drawH * 0.66, 0, 0, Math.PI * 2);
ctx.stroke();
ctx.setLineDash([]);
if (style.label) {
const labelY = -drawH * 0.62;
ctx.font = `${Math.max(11, this.radius * 0.52)}px Yomogi, ui-rounded, sans-serif`;
ctx.textAlign = "center";
ctx.textBaseline = "middle";
const labelW = ctx.measureText(style.label).width + 16;
ctx.fillStyle = "rgba(255,252,238,0.92)";
ctx.strokeStyle = style.color;
ctx.lineWidth = 2.2;
tarinaiRoundRectPath(ctx, -labelW / 2, labelY - 10, labelW, 20, 10);
ctx.fill();
ctx.stroke();
ctx.fillStyle = style.color;
ctx.fillText(style.label, 0, labelY);
}
ctx.restore();
}
2026-06-19 14:00:48 +09:00
if (this.world.selected === this) {
ctx.save();
2026-06-20 20:29:44 +09:00
const pulse = 0.5 + 0.5 * Math.sin(this.world.time * 7.5);
const jumpPulse = clamp(this.focusPulseTimer || 0, 0, 1);
ctx.globalAlpha = (0.82 + pulse * 0.12) * nestHideAlpha;
ctx.shadowColor = "rgba(124, 210, 76, 0.92)";
ctx.shadowBlur = 14 + 18 * Math.max(pulse, jumpPulse);
ctx.strokeStyle = "rgba(112, 198, 70, 0.98)";
ctx.lineWidth = 4.2 + jumpPulse * 2.0;
2026-06-19 14:00:48 +09:00
ctx.beginPath();
2026-06-20 20:29:44 +09:00
ctx.ellipse(0, drawH * 0.14, drawW * (0.74 + jumpPulse * 0.10), drawH * (0.56 + jumpPulse * 0.08), 0, 0, Math.PI * 2);
2026-06-19 14:00:48 +09:00
ctx.stroke();
2026-06-20 20:29:44 +09:00
ctx.shadowBlur = 0;
ctx.setLineDash([9, 5]);
ctx.lineWidth = 2.0;
ctx.strokeStyle = "rgba(255,255,255,0.96)";
2026-06-19 14:00:48 +09:00
ctx.beginPath();
2026-06-20 20:29:44 +09:00
ctx.ellipse(0, drawH * 0.14, drawW * 0.88, drawH * 0.68, 0, 0, Math.PI * 2);
2026-06-19 14:00:48 +09:00
ctx.stroke();
ctx.setLineDash([]);
2026-06-20 20:29:44 +09:00
ctx.fillStyle = "rgba(255,252,232,0.94)";
ctx.strokeStyle = "rgba(112,198,70,0.88)";
ctx.lineWidth = 2;
const focusLabel = "\u6ce8\u76ee";
ctx.font = `${Math.max(11, this.radius * 0.54)}px Yomogi, ui-rounded, sans-serif`;
const lw = ctx.measureText(focusLabel).width + 18;
const ly = -drawH * 0.64;
tarinaiRoundRectPath(ctx, -lw / 2, ly - 11, lw, 22, 11);
ctx.fill();
ctx.stroke();
ctx.fillStyle = "rgba(72,126,42,0.96)";
ctx.textAlign = "center";
ctx.textBaseline = "middle";
ctx.fillText(focusLabel, 0, ly);
2026-06-19 14:00:48 +09:00
ctx.restore();
} else if ((this.world.performanceLevel ? this.world.performanceLevel() : 0) < 2 && this.world.pointer?.inside && distXY(this.x, this.y, this.world.pointer.x, this.world.pointer.y) < this.radius * 2.0) {
ctx.save();
ctx.globalAlpha = 0.28;
ctx.strokeStyle = this.state === "cursor_enemy" ? "rgba(180, 92, 92, 0.70)" : "rgba(255, 250, 220, 0.78)";
ctx.lineWidth = 1.4;
ctx.beginPath();
ctx.ellipse(0, drawH * 0.14, drawW * 0.58, drawH * 0.43, 0, 0, Math.PI * 2);
ctx.stroke();
ctx.restore();
}
2026-06-20 20:29:44 +09:00
ctx.rotate(angle);
ctx.globalAlpha *= nestHideAlpha;
if (insideCardboard) ctx.globalAlpha *= 0.58;
2026-06-19 14:00:48 +09:00
if (this.eatTimer > 0.02) {
ctx.save();
ctx.globalAlpha = 0.46 * (this.eatTimer / 0.32);
ctx.strokeStyle = this.lastMealColor || "#f7cf67";
ctx.lineWidth = 1.6;
ctx.beginPath();
ctx.arc(0, -drawH * 0.02, drawW * 0.22 + (1 - this.eatTimer / 0.32) * 5, 0, Math.PI * 2);
ctx.stroke();
ctx.restore();
}
if (this.pokeFlashTimer > 0.03) {
ctx.save();
2026-06-20 02:07:01 +09:00
ctx.globalAlpha = this.pokeFlashTimer * 0.9 * nestHideAlpha;
2026-06-19 14:00:48 +09:00
ctx.fillStyle = "rgba(255, 120, 120, 0.22)";
ctx.beginPath();
ctx.ellipse(0, 0, drawW * 0.60, drawH * 0.42, 0, 0, Math.PI * 2);
ctx.fill();
ctx.restore();
}
if ((this.world.performanceLevel ? this.world.performanceLevel() : 0) < 2 && (this.goodMode === "smile" || this.state === "cursor_friend") && this.mood > 58 && this.stress < 44 && this.state !== "sleep") {
ctx.save();
2026-06-19 20:52:40 +09:00
const cursorLove = this.state === "cursor_friend" ? Math.max(0, this.cursorPetting || 0) : 0;
ctx.globalAlpha = this.state === "cursor_friend" ? clamp(0.82 + cursorLove * 0.16, 0.82, 1) : 0.34;
2026-06-19 14:00:48 +09:00
ctx.fillStyle = this.state === "cursor_friend" ? "#f05b87" : (lightState.nightStrength > 0.20 ? "#eef5ff" : (lightState.goldenStrength > 0.35 ? "#ffe398" : "#d6f5a9"));
2026-06-20 02:07:01 +09:00
const count = this.state === "cursor_friend" ? (cursorLove > 1.0 ? 10 : (cursorLove > 0.45 ? 8 : (cursorLove > 0.18 ? 6 : 4))) : 2;
2026-06-19 20:52:40 +09:00
for (let i = 0; i < count; i++) {
const orbit = 0.24 + (i % 3) * 0.08 + cursorLove * 0.05;
const px = Math.cos(t * 2.2 + this.age + i * 1.42) * drawW * orbit;
const py = -drawH * (0.22 + (i % 2) * 0.06) + Math.sin(t * 2.9 + this.age * 0.2 + i) * drawH * (0.08 + cursorLove * 0.02);
2026-06-19 14:00:48 +09:00
if (this.state === "cursor_friend") {
2026-06-19 20:52:40 +09:00
const s = Math.max(5, drawW * (0.070 + (i % 3) * 0.012 + cursorLove * 0.01));
2026-06-19 14:00:48 +09:00
drawHeartShape(ctx, px, py, s, { fill: "rgba(240,91,135,0.90)", stroke: "rgba(255,252,246,0.92)", alpha: 1, rotation: Math.sin(t * 3 + i) * 0.18 });
} else {
ctx.beginPath();
ctx.arc(px, py, drawW * 0.045, 0, Math.PI * 2);
ctx.fill();
}
}
ctx.restore();
}
if (faceRight) ctx.scale(-1, 1);
const dw = drawW * squish;
const dh = drawH / squish;
const brightness = clamp(0.82 + lightState.light * 0.30 + lightState.dawnStrength * 0.08 + lightState.goldenStrength * 0.06 + lightState.noonStrength * 0.04, 0.80, 1.17);
const saturation = clamp(0.86 + lightState.light * 0.22 + lightState.goldenStrength * 0.10 - lightState.nightStrength * 0.04, 0.82, 1.14);
const renderScale = typeof SPRITE_CACHE_SCALE === "number" ? SPRITE_CACHE_SCALE : 2.0;
const cacheW = Math.max(1, Math.round((dw * renderScale) / 8) * 8);
const cacheH = Math.max(1, Math.round((dh * renderScale) / 8) * 8);
2026-06-19 23:22:15 +09:00
if (img) {
const spriteCanvas = typeof getCachedSpriteCanvas === "function" ? getCachedSpriteCanvas(sprite, img, cacheW, cacheH) : img;
ctx.drawImage(spriteCanvas, -dw / 2, -dh / 2, dw, dh);
} else {
ctx.save();
ctx.fillStyle = this.stress > 62 ? "#f1c3a8" : "#f4ead6";
ctx.strokeStyle = "rgba(92,71,49,0.48)";
ctx.lineWidth = 2;
ctx.beginPath();
ctx.ellipse(0, 0, dw * 0.42, dh * 0.36, 0, 0, Math.PI * 2);
ctx.fill();
ctx.stroke();
ctx.restore();
}
if (img && (this.zunchiStain || 0) > 3 && typeof getCachedStainOverlayCanvas === "function") {
2026-06-19 20:52:40 +09:00
const stainCanvas = getCachedStainOverlayCanvas(sprite, img, cacheW, cacheH, clamp((this.zunchiStain || 0) / 100, 0, 1), this.zunchiStainSeed || 0.5);
if (stainCanvas) {
ctx.save();
ctx.globalAlpha = clamp((this.zunchiStain || 0) / 100, 0.10, 0.88);
ctx.drawImage(stainCanvas, -dw / 2, -dh / 2, dw, dh);
ctx.restore();
}
}
2026-06-19 14:00:48 +09:00
drawDawnRimLight(ctx, dw, dh, lightState);
if (faceRight) ctx.scale(-1, 1);
if ((this.world.performanceLevel ? this.world.performanceLevel() : 0) < 3 && (this.hunger > 90 || this.loneliness > 88 || this.energy < 16)) {
ctx.save();
ctx.globalAlpha = 0.80;
ctx.font = `${Math.max(10, drawW * 0.14)}px ui-rounded, sans-serif`;
ctx.textAlign = "center";
ctx.fillStyle = "rgba(42,36,29,0.72)";
const mark = this.hunger > 90 ? "..." : (this.energy < 16 ? "Z" : "?");
ctx.fillText(mark, 0, -drawH * 0.72);
ctx.restore();
}
ctx.restore();
2026-06-20 02:07:01 +09:00
if (this.favorite) {
ctx.save();
2026-06-20 15:55:52 +09:00
ctx.globalAlpha = nestHideAlpha;
2026-06-20 02:07:01 +09:00
const starSize = Math.max(18, this.radius * 0.88);
const sx = this.x;
const sy = this.y - drawH * 0.64 - 20;
ctx.font = `${starSize}px "Apple Color Emoji", "Segoe UI Emoji", sans-serif`;
ctx.textAlign = "center";
ctx.textBaseline = "middle";
ctx.shadowColor = "rgba(255, 198, 28, 0.82)";
ctx.shadowBlur = 14;
ctx.lineWidth = Math.max(2, starSize * 0.12);
ctx.strokeStyle = "rgba(92, 62, 0, 0.86)";
ctx.fillStyle = "rgba(255, 218, 46, 1)";
ctx.strokeText("\u2605", sx, sy);
ctx.fillText("\u2605", sx, sy);
ctx.restore();
}
2026-06-19 23:22:15 +09:00
const hpActive = (this.hpBarTimer || 0) > 0;
const stressActive = (this.stressBarTimer || 0) > 0;
if (hpActive || stressActive) {
2026-06-19 14:00:48 +09:00
const bw = Math.max(28, this.radius * 2.35);
const bh = 5;
const x = this.x - bw / 2;
2026-06-19 23:22:15 +09:00
const baseY = this.y - drawH * 0.55 - 16;
const drawMeter = (y, pct, fill, alpha) => {
ctx.save();
ctx.globalAlpha = alpha;
ctx.fillStyle = "rgba(255,252,242,0.86)";
roundedRect(ctx, x - 2, y - 2, bw + 4, bh + 4, 5);
ctx.fill();
ctx.fillStyle = "rgba(72,58,45,0.22)";
roundedRect(ctx, x, y, bw, bh, 4);
ctx.fill();
ctx.fillStyle = fill;
roundedRect(ctx, x, y, bw * pct, bh, 4);
ctx.fill();
ctx.restore();
};
if (stressActive) {
const pct = clamp((this.stress || 0) / 130, 0, 1);
2026-06-20 20:29:44 +09:00
drawMeter(baseY, pct, pct > 0.72 ? "rgba(178,74,110,0.92)" : "rgba(212,126,84,0.90)", clamp((this.stressBarTimer || 0) / 0.7, 0.35, 1));
2026-06-19 23:22:15 +09:00
}
if (hpActive) {
const pct = clamp(this.energy / 100, 0, 1);
2026-06-20 20:29:44 +09:00
drawMeter(baseY - (stressActive ? 11 : 0), pct, pct > 0.45 ? "rgba(118,190,80,0.88)" : (pct > 0.20 ? "rgba(232,172,68,0.90)" : "rgba(218,82,70,0.92)"), clamp((this.hpBarTimer || 0) / 0.7, 0.35, 1));
2026-06-19 23:22:15 +09:00
}
2026-06-19 14:00:48 +09:00
}
}
contains(x, y) {
2026-06-20 20:29:44 +09:00
if (this.insideNestBoxId || (this.nestFade || 0) >= 0.92) return false;
2026-06-19 14:00:48 +09:00
const hit = this.world.screenSizeToWorld ? this.world.screenSizeToWorld(this.radius * 1.9) : this.radius * 1.9;
return distXY(this.x, this.y, x, y) < hit;
}
}