tarinai/js/tarinai_social_move_life.js

877 lines
48 KiB
JavaScript
Raw Normal View History

2026-06-21 14:09:35 +09:00
"use strict";
2026-06-27 19:22:38 +09:00
2026-06-21 14:09:35 +09:00
(function (global) {
const Tarinai = global.Tarinai;
if (!Tarinai) throw new Error("Tarinai is not available for mixin: tarinai_social_move_life.js");
2026-06-30 13:40:36 +09:00
function fightHeadbuttDamage(attacker, target, worldRef, salt = "") {
const profile = attacker?.personalityProfile?.() || {};
const aggression = Number(profile.aggression || 1) || 1;
const fear = Number(profile.fear || 1) || 1;
const stress = Number(attacker?.stress || 0) || 0;
const mood = Number(attacker?.mood || 50) || 50;
const personalityCorrection = (aggression - 1) * 4.0 - (fear - 1) * 1.8;
const stateCorrection = clamp((stress - 50) * 0.035 + (50 - mood) * 0.020, -3.2, 4.2);
const typeCorrection = attacker?.type === "angry" ? 2.0 : (attacker?.type === "teary" ? -1.2 : 0);
const drugCorrection = (attacker?.fightMochiTimer || 0) > 0.04 ? 3.0 : 0;
const jitter = deterministicRange(worldRef, `headbutt-damage-jitter-${salt}`, -1.4, 1.4, attacker, target);
const base = 15;
const raw = Math.max(1, base + personalityCorrection + stateCorrection + typeCorrection + drugCorrection + jitter);
return attacker?.outgoingDamage ? attacker.outgoingDamage(raw) : raw;
}
2026-06-30 22:30:37 +09:00
const TARINAI_KING_FLEE_REASON = "\u305F\u308A\u306A\u3044\u738B\u304C\u6016\u304F\u3066\u9003\u3052\u3066\u3044\u308B\u3002";
2026-07-18 13:06:02 +09:00
const ROUTE_STATES = new Set([
"seek_food", "seek_water", "seek_bed", "seek_material", "build", "follow_parent",
"seek_friend", "seek_enemy", "fight", "play_ball", "play_seesaw", "ant_attack", "seek_temperature",
2026-07-20 14:36:59 +09:00
"wander", "panic", "bell_call", "build_memorial", "visit_grave",
2026-07-18 13:06:02 +09:00
]);
2026-07-03 00:45:22 +09:00
function isNestContainerItem(item) {
2026-07-05 18:01:36 +09:00
return Boolean(global.TarinaiToolRuntime?.isNestContainerItem?.(item));
2026-07-03 00:45:22 +09:00
}
function sleepContainerText(item, verb = "\u4f11\u3093\u3067\u3044\u308b") {
return item?.type === "pipe" ? `\u571f\u7ba1\u306e\u4e2d\u3067${verb}` : `\u5de3\u7bb1\u306e\u4e2d\u3067${verb}`;
}
2026-06-30 22:30:37 +09:00
function tarinaiIsChampionBreedingPartner(champion, candidate) {
if (!champion || !candidate || champion === candidate) return false;
if (champion.birthPartnerId && champion.birthPartnerId === candidate.id) return true;
if (candidate.birthPartnerId && candidate.birthPartnerId === champion.id) return true;
const championBehavior = typeof currentTarinaiBehavior === "function" ? currentTarinaiBehavior(champion) : null;
const candidateBehavior = typeof currentTarinaiBehavior === "function" ? currentTarinaiBehavior(candidate) : null;
const breedingIds = new Set(["approach_mate", "birth_ritual"]);
if (breedingIds.has(championBehavior?.actionId) && champion.target === candidate) return true;
if (breedingIds.has(candidateBehavior?.actionId) && candidate.target === champion) return true;
if ((champion.birthRitualTimer || 0) > 0.04 && champion.target === candidate) return true;
if ((candidate.birthRitualTimer || 0) > 0.04 && candidate.target === champion) return true;
return false;
}
function nearestKingFleeNestBox(tarinai, maxDist = 460) {
const worldRef = tarinai?.world;
if (!tarinai || !worldRef) return null;
2026-07-18 13:06:02 +09:00
const source = typeof worldRef.nearbyItems === "function" ? (worldRef.nearbyNonGrassItems?.(tarinai.x, tarinai.y, maxDist) || worldRef.nearbyItems(tarinai.x, tarinai.y, maxDist)) : (worldRef.items || []);
2026-06-30 22:30:37 +09:00
let best = null;
let bestScore = Infinity;
for (const item of source || []) {
if (!item || item.dead || item.type !== "nest_box") continue;
const d = distXY(tarinai.x, tarinai.y, item.x, item.y);
if (d > maxDist) continue;
const padding = Math.max(16, (tarinai.radius || 20) * 0.72);
if (typeof worldRef.targetBlockedByObstacle === "function" && worldRef.targetBlockedByObstacle(tarinai, item, padding)) continue;
2026-07-03 20:57:02 +09:00
if (typeof worldRef.sleepPlaceAvailableFor === "function" && !worldRef.sleepPlaceAvailableFor(tarinai, item)) continue;
2026-06-30 22:30:37 +09:00
const occupants = typeof worldRef.nestBoxOccupants === "function" ? worldRef.nestBoxOccupants(item, Infinity) : [];
const capacity = typeof worldRef.nestBoxCapacity === "function" ? worldRef.nestBoxCapacity(item) : 5;
const fullnessPenalty = Math.max(0, occupants.length - capacity + 1) * 18;
const score = d + fullnessPenalty;
if (score < bestScore) {
bestScore = score;
best = item;
}
}
return best;
}
function forceKingFlee(avoider, champion, dt = 0.016) {
if (!avoider || !champion || avoider.dead) return false;
const nest = nearestKingFleeNestBox(avoider);
const target = nest || avoider.panicDestination?.(champion, true) || {
x: avoider.x + (avoider.x - champion.x || 1) * 1.8,
y: avoider.y + (avoider.y - champion.y || 0) * 1.8,
};
const now = avoider.world?.time || 0;
avoider.lastNeedShockBreaker = champion;
2026-07-18 13:06:02 +09:00
avoider.lastPanicThreat = champion;
2026-06-30 22:30:37 +09:00
avoider.lastPanicCause = "tarinai_champion";
avoider.lastPanicDetail = "\u305F\u308A\u306A\u3044\u738B\u304C\u6016\u3044";
avoider.panicStartedAt = now;
avoider.panicHardStopAt = Math.max(avoider.panicHardStopAt || 0, now + 6.5);
avoider.target = target;
avoider.setActionState?.("panic", { target, reason: TARINAI_KING_FLEE_REASON, wake: true, sleeping: false });
if (typeof setBehaviorText === "function") {
setBehaviorText(avoider, {
need: "safety",
actionId: "panic_escape",
actionLabel: "\u9003\u3052\u3066\u3044\u308B",
reasonText: TARINAI_KING_FLEE_REASON,
target,
phase: "perform",
source: "behavior",
});
}
avoider.thought = TARINAI_KING_FLEE_REASON;
avoider.fearTimer = Math.max(avoider.fearTimer || 0, 1.05 * avoider.personalityProfile().fear);
if (nest && distXY(avoider.x, avoider.y, nest.x, nest.y) <= Math.max(50, (avoider.radius || 20) + (nest.r || 24) + 18)) {
2026-07-03 20:57:02 +09:00
avoider.enterNestBox?.(nest, dt, { forceDisplace: false, reason: TARINAI_KING_FLEE_REASON });
2026-06-30 22:30:37 +09:00
}
return true;
}
2026-07-18 13:06:02 +09:00
const CROWD_ESCAPE_REASON = "\u4ed6\u306e\u305f\u308a\u306a\u3044\u304c\u591a\u3059\u304e\u308b\u306e\u3067\u3001\u9003\u3052\u3066\u3044\u308b\u3002";
const LONELY_REASON = "\u5bc2\u3057\u3055\u3092\u611f\u3058\u3066\u3044\u308b\u3002";
function crowdThresholds(t) {
const cur = t?.currentPersonality || {};
const social = clamp(Number(cur.sociability || 0) || 0, -1, 1);
const open = clamp(Number(cur.openness || 0) || 0, -1, 1);
const social01 = (social + 1) * 0.5;
const open01 = (open + 1) * 0.5;
// Sociability raises preferred group size. Openness broadens the comfort band,
// especially on the crowded side, without turning ordinary colonies into panic.
const preferred = 4 + social01 * 6;
const lower = Math.floor(clamp(preferred - (3 + open01 * 3), 0, 7));
const upper = Math.ceil(clamp(preferred + 5 + open01 * 7, Math.max(8, lower + 4), 24));
return { lower, upper };
}
function nearestLonelyCompanion(t, local = [], maxDist = 640) {
if (!t?.world) return null;
let best = null;
let bestD2 = Infinity;
const consider = (o) => {
if (!o || o === t || o.dead) return;
const dx = o.x - t.x, dy = o.y - t.y;
const d2 = dx * dx + dy * dy;
if (d2 < 0.0001 || d2 > maxDist * maxDist || d2 >= bestD2) return;
best = o;
bestD2 = d2;
};
for (const entry of local || []) consider(entry?.o || entry);
if (!best) {
const source = typeof t.world.nearbyTarinai === "function"
? (t.world.nearbyTarinai(t.x, t.y, maxDist, true) || [])
: (t.world.tarinai || []);
// This fallback runs only while lonely. Stop once a good nearby companion is
// found instead of sorting or retaining the full wide-radius population.
for (const o of source) consider(o);
}
return best;
}
function findCrowdEscapeTarget(t, nearby) {
const world = t?.world;
if (!t || !world) return null;
let cx = 0, cy = 0, count = 0;
for (const o of nearby || []) {
if (!o || o === t || o.dead) continue;
const dx = o.x - t.x, dy = o.y - t.y;
const d2 = dx * dx + dy * dy;
if (d2 > 118 * 118 || d2 < 0.0001) continue;
const w = 1 / Math.max(24, Math.sqrt(d2));
cx += dx * w;
cy += dy * w;
count++;
}
const seed = typeof stableUnit === "function" ? stableUnit(t.familyKey || t.id || "crowd", "crowd-escape-angle") : 0.5;
let base = Math.atan2(-cy, -cx);
if (!count || !Number.isFinite(base)) base = seed * Math.PI * 2;
const pad = Math.max(30, (CONFIG.worldPadding || 30) + (t.radius || 22));
const radius = Math.max(14, (t.radius || 22) * 0.72);
const offsets = [0, 0.42, -0.42, 0.84, -0.84];
for (const dist of [190, 270]) {
for (const off of offsets) {
const a = base + off + (seed - 0.5) * 0.18;
const x = clamp(t.x + Math.cos(a) * dist, pad, world.w - pad);
const y = clamp(t.y + Math.sin(a) * dist, pad, world.h - pad);
if (world.pointBlockedByObstacle?.(x, y, radius, { maxChecks: 14, directionalOneWay: true })) continue;
return { x, y, dead: false, detour: true, crowdEscape: true };
}
}
return null;
}
function trySpreadZunchiDisease(source, target, dt, d, salt) {
if (!source?.zunchiDisease || !target || target.zunchiDisease || (target.zunchiDiseaseCooldown || 0) > 0 || d >= 54) return false;
const tempMul = typeof global.zunchiDiseaseTemperatureInfectionMultiplier === "function" ? global.zunchiDiseaseTemperatureInfectionMultiplier(target) : 1;
const base = dt * 0.065 * clamp(1 - d / 58, 0.12, 1) * tempMul;
const chance = target.diseaseChance ? target.diseaseChance(base) : base;
if (!deterministicChance(source.world, `social-zunchi-spread-${salt}`, chance, source, target)) return false;
if (!target.infectZunchiDisease(source)) return false;
source.world.log(`${source.name}\u306e\u305a\u3093\u3061\u75c5\u304c${target.name}\u306b\u3046\u3064\u3063\u305f\u3002`, "accident", { participants: [target, source] });
return true;
}
2026-06-21 14:09:35 +09:00
Object.defineProperties(Tarinai.prototype, Object.getOwnPropertyDescriptors({
interactWithOthers(dt) {
2026-07-18 13:06:02 +09:00
const scanLimit = (this.fightTimer > 0.04 || this.birthRitualTimer > 0.04) ? 12 : 8;
2026-06-26 19:04:32 +09:00
const selfRadius = Math.max(12, Number(this.radius || 22) || 22);
2026-07-18 13:06:02 +09:00
const scanRadius = Math.max(92, selfRadius * 2.0 + 52);
2026-07-18 22:15:26 +09:00
const candidateBudget = Math.max(20, scanLimit + 8);
2026-07-18 13:06:02 +09:00
const selfProfile = this.personalityProfile();
const detailed = [];
2026-07-18 22:15:26 +09:00
let inspected = 0;
const currentBehavior = typeof currentTarinaiBehavior === "function" ? currentTarinaiBehavior(this) : null;
const currentMateTarget = (currentBehavior?.actionId === "approach_mate" || this.actionId === "approach_mate")
? (currentBehavior?.target || this.target || null)
: null;
// Contact physics/social work must stay bounded in dense colonies. Scan a
// small candidate budget, retain the nearest entries, and explicitly add the
// already-selected mate target so reproduction does not depend on spatial
// bucket insertion order.
2026-06-26 19:04:32 +09:00
for (const o of this.world.nearbyTarinai(this.x, this.y, scanRadius, true)) {
2026-06-21 14:09:35 +09:00
if (o === this || o.dead) continue;
const dx = o.x - this.x, dy = o.y - this.y;
2026-07-18 13:06:02 +09:00
const d2 = dx * dx + dy * dy;
if (d2 > scanRadius * scanRadius || d2 < 0.0001) continue;
2026-07-18 22:15:26 +09:00
inspected += 1;
2026-07-18 13:06:02 +09:00
const entry = { o, dx, dy, d2 };
let pos = detailed.length;
while (pos > 0 && detailed[pos - 1].d2 > d2) pos--;
if (pos < scanLimit) {
detailed.splice(pos, 0, entry);
if (detailed.length > scanLimit) detailed.pop();
}
2026-07-18 22:15:26 +09:00
if (inspected >= candidateBudget) break;
}
if (currentMateTarget && currentMateTarget !== this && !currentMateTarget.dead) {
let alreadyIncluded = false;
for (const entry of detailed) {
if (entry.o === currentMateTarget) { alreadyIncluded = true; break; }
}
if (!alreadyIncluded) {
const dx = currentMateTarget.x - this.x;
const dy = currentMateTarget.y - this.y;
const d2 = dx * dx + dy * dy;
if (d2 > 0.0001 && d2 <= scanRadius * scanRadius) detailed.push({ o: currentMateTarget, dx, dy, d2 });
}
2026-07-18 13:06:02 +09:00
}
for (const entry of detailed) {
const o = entry.o;
const pairLeader = String(this.id) < String(o.id);
const dx = entry.dx, dy = entry.dy;
2026-06-26 19:04:32 +09:00
const otherRadius = Math.max(12, Number(o.radius || 22) || 22);
const bodyContactRange = Math.max(74, selfRadius + otherRadius + 18);
2026-07-18 13:06:02 +09:00
if (entry.d2 > bodyContactRange * bodyContactRange) continue;
const d = Math.sqrt(entry.d2);
2026-06-28 13:40:41 +09:00
const champion = this.isTarinaiChampion ? this : (o.isTarinaiChampion ? o : null);
if (champion && this.isTarinaiChampion !== o.isTarinaiChampion) {
const avoider = champion === this ? o : this;
2026-06-30 22:30:37 +09:00
if (tarinaiIsChampionBreedingPartner(champion, avoider)) {
avoider.adjustRelation?.(champion, dt * 0.035, -dt * 0.06, "tarinai_champion_partner");
if (this !== champion) continue;
} else {
const nx = (avoider.x - champion.x) / d;
const ny = (avoider.y - champion.y) / d;
const avoidRange = Math.max(92, selfRadius + otherRadius + 34);
const urgency = clamp(1 - d / avoidRange, 0, 1);
const push = dt * (64 + urgency * 260);
avoider.vx += nx * push;
avoider.vy += ny * push;
if (d <= selfRadius + otherRadius + 14) {
avoider.vx += nx * dt * 190;
avoider.vy += ny * dt * 190;
champion.vx -= nx * dt * 8;
champion.vy -= ny * dt * 8;
avoider.surpriseTimer = Math.max(avoider.surpriseTimer || 0, 0.20);
avoider.fearTimer = Math.max(avoider.fearTimer || 0, 0.32 * avoider.personalityProfile().fear);
if (typeof applyNeedShock === "function") applyNeedShock(avoider, { safety: dt * 26, social: dt * 5 }, champion);
2026-06-28 13:40:41 +09:00
}
2026-06-30 22:30:37 +09:00
forceKingFlee(avoider, champion, dt);
avoider.adjustRelation?.(champion, -dt * 0.025, dt * 0.18, "tarinai_champion_avoid");
if (this !== champion) continue;
2026-06-28 13:40:41 +09:00
}
}
2026-06-21 14:09:35 +09:00
2026-07-18 13:06:02 +09:00
const fightMochiContact = pairLeader && this.world.canFightPair?.(this, o) && (this.hasFightMochiEffect() || o.hasFightMochiEffect());
2026-06-25 22:30:35 +09:00
if (fightMochiContact && (this.world.fightPairCooldownRemaining?.(this, o) || 0) <= 0.04 && this.fightCooldown <= 0.04 && o.fightCooldown <= 0.04 && d <= Math.max(42, this.radius + o.radius + 16)) {
2026-06-23 22:39:20 +09:00
this.conflictTargetId = o.id;
o.conflictTargetId = this.id;
this.conflictUrge = Math.max(this.conflictUrge || 0, 92);
o.conflictUrge = Math.max(o.conflictUrge || 0, 82);
if (typeof applyNeedShock === "function") applyNeedShock(this, { social: dt * 36, safety: dt * 5 }, o);
if (typeof applyNeedShock === "function") applyNeedShock(o, { social: dt * 30, safety: dt * 5 }, this);
if (typeof queueForcedTarinaiBehavior === "function") {
2026-06-25 22:30:35 +09:00
queueForcedTarinaiBehavior(this, "fight_rival", { target: o, source: "fight_mochi", priority: 180, ttl: 3.2, retryDelay: 0.55, searchRange: 140, replaceSameSource: true, causeText: "\u3051\u3093\u304b\u9905\u306e\u52b9\u679c" });
queueForcedTarinaiBehavior(o, "fight_rival", { target: this, source: "fight_mochi", priority: 180, ttl: 3.2, retryDelay: 0.55, searchRange: 140, replaceSameSource: true, causeText: "\u3051\u3093\u304b\u9905\u306e\u52b9\u679c" });
2026-06-23 22:39:20 +09:00
}
2026-06-21 14:09:35 +09:00
}
2026-07-18 13:06:02 +09:00
if (pairLeader) {
trySpreadZunchiDisease(this, o, dt, d, "a");
trySpreadZunchiDisease(o, this, dt, d, "b");
2026-06-21 14:09:35 +09:00
}
const pairBond = this.world.areCoParents?.(this, o);
2026-06-21 22:29:00 +09:00
const mixedZunchiSlave = !!this.isZunchiSlave !== !!o.isZunchiSlave;
if (mixedZunchiSlave && d < 86) {
2026-07-18 13:06:02 +09:00
if (pairLeader) {
const nonSlave = this.isZunchiSlave ? o : this;
const slave = this.isZunchiSlave ? this : o;
const nx = (nonSlave.x - slave.x) / d;
const ny = (nonSlave.y - slave.y) / d;
nonSlave.vx += nx * dt * 34;
nonSlave.vy += ny * dt * 34;
if (typeof applyNeedShock === "function") applyNeedShock(nonSlave, { safety: dt * 24, social: dt * 5 }, slave);
nonSlave.lastNeedShockBreaker = slave;
nonSlave.fearTimer = Math.max(nonSlave.fearTimer || 0, 0.22 * nonSlave.personalityProfile().fear);
nonSlave.target = slave;
nonSlave.thought = "\u305a\u3093\u3061\u3069\u308c\u3044\u304c\u8fd1\u3044";
nonSlave.adjustRelation?.(slave, -dt * 0.08, dt * 0.24, "zunchi_slave_avoid");
slave.adjustRelation?.(nonSlave, -dt * 0.02, 0, "zunchi_slave_avoid");
if (deterministicChance(this.world, "zunchi-slave-surprise", dt * 0.35, nonSlave, slave)) nonSlave.surpriseTimer = Math.max(nonSlave.surpriseTimer || 0, 0.18);
}
2026-06-21 22:29:00 +09:00
continue;
}
2026-06-21 14:09:35 +09:00
const overlap = this.radius + o.radius - d;
2026-07-18 13:06:02 +09:00
if (pairLeader && overlap > -2) {
const nx = dx / d, ny = dy / d;
2026-06-21 14:09:35 +09:00
const push = (overlap + 6) * (pairBond ? 0.08 : 0.17);
2026-07-18 13:06:02 +09:00
this.vx -= nx * push;
this.vy -= ny * push;
o.vx += nx * push;
o.vy += ny * push;
if (!pairBond) {
if (deterministicChance(this.world, "body-overlap-surprise-a", dt * 1.2, this, o)) this.surpriseTimer = Math.max(this.surpriseTimer, 0.12);
if (deterministicChance(this.world, "body-overlap-surprise-b", dt * 1.2, o, this)) o.surpriseTimer = Math.max(o.surpriseTimer || 0, 0.12);
}
2026-06-21 14:09:35 +09:00
}
2026-07-18 13:06:02 +09:00
if (pairLeader && pairBond && d > 26 && d < 120) {
const nx = dx / d, ny = dy / d;
const pull = dt * 4.8;
this.vx += nx * pull;
this.vy += ny * pull;
o.vx -= nx * pull;
o.vy -= ny * pull;
2026-06-21 14:09:35 +09:00
}
2026-06-26 19:04:32 +09:00
if (d < Math.max(60, selfRadius + otherRadius + 8)) {
2026-06-21 14:09:35 +09:00
const rel = this.relationTo(o.id);
2026-07-18 13:06:02 +09:00
const pdef = selfProfile;
2026-06-21 14:09:35 +09:00
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");
if ((rel.affinity || 0) >= FRIEND_AFFINITY_THRESHOLD) {
2026-06-23 18:11:42 +09:00
if (typeof applyNeedRelief === "function") applyNeedRelief(this, { social: -dt * 5 * this.trait.social, fulfill: -dt * 2 });
2026-06-21 14:09:35 +09:00
this.affection = clamp(this.affection + dt * 0.18, 0, 80);
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] });
}
if ((rel.affinity || 0) >= FRIEND_AFFINITY_THRESHOLD && this.world.relationNotice(this.id, o.id, "friend", 32)) {
this.world.log(`${this.name}\u306f\u3088\u304f${o.name}\u306e\u8fd1\u304f\u306b\u3044\u308b\u3002`, "relation", { participants: [o, this] });
}
2026-06-23 18:11:42 +09:00
if (this.type === "angry" && typeof applyNeedShock === "function") applyNeedShock(this, { social: dt * 1.5, safety: dt * 1 });
if (this.type === "teary" && o.type === "teary" && typeof applyNeedRelief === "function") applyNeedRelief(this, { social: -dt * 2 });
2026-06-21 14:09:35 +09:00
}
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;
2026-06-27 19:22:38 +09:00
this.vx -= nx * deterministicRange(this.world, "headbutt-self-x", 24, 44, this, o);
this.vy -= ny * deterministicRange(this.world, "headbutt-self-y", 24, 44, this, o);
o.vx += nx * deterministicRange(this.world, "headbutt-other-x", 34, 64, this, o);
o.vy += ny * deterministicRange(this.world, "headbutt-other-y", 34, 64, this, o);
2026-06-23 18:11:42 +09:00
if (typeof applyNeedShock === "function") applyNeedShock(this, { safety: 8, health: 4 });
if (typeof applyNeedShock === "function") applyNeedShock(o, { safety: 12, health: 6 });
2026-06-30 13:40:36 +09:00
const damageToThis = fightHeadbuttDamage(o, this, this.world, "to-this");
const damageToOther = fightHeadbuttDamage(this, o, this.world, "to-other");
2026-07-15 14:44:29 +09:00
this._achievementLastFightAttackerId = o.id;
this._achievementLastFightDamageAt = this.world.time || 0;
o._achievementLastFightAttackerId = this.id;
o._achievementLastFightDamageAt = this.world.time || 0;
2026-06-21 16:26:12 +09:00
this.damage(damageToThis, "\u55a7\u5629");
o.damage(damageToOther, "\u55a7\u5629");
this.world.maybeDefeatFromFightDamage?.(this, o, damageToThis);
this.world.maybeDefeatFromFightDamage?.(o, this, damageToOther);
2026-06-21 14:09:35 +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");
2026-06-27 19:22:38 +09:00
if (deterministicChance(this.world, "headbutt-fall", 0.22, this, o) || o.energy < 24) {
2026-06-21 14:09:35 +09:00
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);
}
2026-06-23 18:11:42 +09:00
if (o.energy < this.energy - 8) {
2026-06-21 16:26:12 +09:00
this.world.maybeDefeatFromFightDamage?.(o, this, Math.max(damageToOther, 2.4));
2026-06-21 14:09:35 +09:00
}
this.world.spawnHeadbuttEffect((this.x + o.x) / 2, (this.y + o.y) / 2 - 5);
2026-06-30 13:40:36 +09:00
this.nextHeadbutt = this.world.time + deterministicRange(this.world, "next-headbutt", 0.28, 0.46, this, o);
2026-06-21 14:09:35 +09:00
}
}
2026-06-30 13:40:36 +09:00
const weights = socialFightMateWeights(this, o, this.world, { mode: "contact", requireBirthReady: true, distance: d, fightRange: 58 });
2026-07-18 13:06:02 +09:00
const oneDirectionChance = clamp(dt * 0.30 * clamp((weights.totalWeight || 0) / 10, 0, 2.6), 0, 0.95);
// Pair processing now runs once instead of once from each participant. Preserve
// the old two-attempt probability: 1 - (1 - p)^2.
const combinedStartChance = 1 - (1 - oneDirectionChance) * (1 - oneDirectionChance);
if (pairLeader && (weights.totalWeight || 0) > 0 && deterministicChance(this.world, "social-weighted-fight-mate-start", combinedStartChance, this, o)) {
2026-06-30 13:40:36 +09:00
const choice = pickSocialFightMate(this.world, "social-weighted-fight-mate-pick", weights, this, o);
if (choice === "fight") {
this.conflictTargetId = o.id;
o.conflictTargetId = this.id;
const urge = 26 + Math.min(64, weights.fightWeight * 4.5) + (mixedZunchiSlave ? 10 : 0);
this.conflictUrge = Math.max(this.conflictUrge || 0, urge);
o.conflictUrge = Math.max(o.conflictUrge || 0, urge * 0.82);
if (typeof applyNeedShock === "function") applyNeedShock(this, { social: dt * urge * 0.18, safety: dt * 1.0 }, o);
if (typeof applyNeedShock === "function") applyNeedShock(o, { social: dt * urge * 0.14, safety: dt * 1.0 }, this);
this.world.startConflict?.(this, o);
} else if (choice === "mate") {
2026-06-23 22:39:20 +09:00
if (typeof applyNeedShock === "function") {
2026-06-30 13:40:36 +09:00
applyNeedShock(this, { social: dt * 18 }, o);
applyNeedShock(o, { social: dt * 18 }, this);
2026-06-23 22:39:20 +09:00
}
2026-06-30 13:40:36 +09:00
this.world.startBirthRitual?.(this, o);
2026-06-21 14:09:35 +09:00
}
}
}
2026-06-27 19:22:38 +09:00
if (this.loneliness < 12 && deterministicChance(this.world, "nearby-calm-log", dt * 0.002, this)) {
2026-06-21 14:09:35 +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] });
}
},
move(dt) {
2026-06-30 22:30:37 +09:00
if (!(this.burning || (this.burnTimer || 0) > 0.02) && this.freezeSleepDiseaseMotion(dt)) return;
2026-07-20 14:36:59 +09:00
const specialMotion = global.TarinaiMemorialBellSystem?.updateMotion?.(this, dt);
if (specialMotion?.handled) return;
2026-06-21 14:09:35 +09:00
let ax = 0, ay = 0;
const itemSpeedMul = this.itemSpeedMultiplier ? this.itemSpeedMultiplier() : 1;
const baseSpeed = 46.5 * 1.5 * this.trait.speed * itemSpeedMul * (this.zunchiDisease ? 0.5 : 1) * (this.energy < 20 ? 0.42 : 1) * (this.fallTimer > 0 ? 0.35 : 1);
2026-06-22 02:03:19 +09:00
if (this.state === "sunbath") {
this.target = null;
2026-06-24 18:49:17 +09:00
const pad = Math.max(28, CONFIG.worldPadding || 30);
const safeW = Math.max(pad * 2 + 1, Number(this.world?.w || 1000) || 1000);
const safeH = Math.max(pad * 2 + 1, Number(this.world?.h || 720) || 720);
2026-06-26 19:04:32 +09:00
this.x = clamp(Number.isFinite(this.x) ? this.x : (Number.isFinite(this.sunbathAnchorX) ? this.sunbathAnchorX : pad), pad, safeW - pad);
this.y = clamp(Number.isFinite(this.y) ? this.y : (Number.isFinite(this.sunbathAnchorY) ? this.sunbathAnchorY : pad), pad, safeH - pad);
this.sunbathAnchorX = this.x;
this.sunbathAnchorY = this.y;
2026-06-24 21:20:53 +09:00
this._lastSunbathDrawX = this.x;
this._lastSunbathDrawY = this.y;
this._lastValidX = this.x;
this._lastValidY = this.y;
2026-06-22 02:03:19 +09:00
return;
}
2026-06-21 14:09:35 +09:00
if (this.state === "sleep") {
const bedComfort = this.isSleepFurniture(this.target) ? this.world.bedComfort(this.target) * (this.target?.type === "nest_box" ? 1.55 : 1) : 0.72;
const bedCrowd = this.isSleepFurniture(this.target) ? this.world.bedOccupancy(this.target) : 0;
const crowdedSleep = bedCrowd > 5;
2026-06-26 12:46:32 +09:00
this.energy = clamp(this.energy + dt * (this.target ? 1.8 + bedComfort * 1.35 : 2.1), 0, typeof tarinaiMaxEnergy === "function" ? tarinaiMaxEnergy(this) : (Number(this.maxEnergy) || 100));
2026-06-24 13:48:22 +09:00
if (crowdedSleep && typeof applyNeedShock === "function") applyNeedShock(this, { safety: dt * Math.max(1, bedCrowd - 5) });
2026-06-26 12:46:32 +09:00
const sleepHungerScale = this.metabolicHungerMultiplier ? this.metabolicHungerMultiplier() : (typeof tarinaiMetabolicHungerScale === "function" ? tarinaiMetabolicHungerScale(this) : 1);
2026-07-03 20:57:02 +09:00
this.hunger = clamp(this.hunger + dt * 0.018 * 0.75 * sleepHungerScale, 0, 115);
2026-07-03 00:45:22 +09:00
if (this.target && !this.target.dead && isNestContainerItem(this.target)) {
2026-06-21 14:09:35 +09:00
this.updateNestBoxPresence(dt);
this.vx = 0;
this.vy = 0;
2026-06-27 19:22:38 +09:00
this.impulseVx = 0;
this.impulseVy = 0;
2026-06-21 14:09:35 +09:00
return;
}
2026-06-27 19:22:38 +09:00
const marker = window.TarinaiMovementUpdateStep;
const externalActive = marker?.markSleepExternalMotion?.(this, dt, "sleep-full-update") || marker?.sleepExternalMotionActive?.(this);
2026-06-21 14:09:35 +09:00
if (this.target && !this.target.dead && this.isSleepFurniture(this.target)) {
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-28 13:40:41 +09:00
if (externalActive && d > 8.5 && !this.sleepDisease && marker?.wakeSleepingFromExternalMotion?.(this, "\u52d5\u304b\u3055\u308c\u3066\u76ee\u304c\u899a\u3081\u305f")) return;
2026-06-21 14:09:35 +09:00
if (d > 2.2) {
2026-06-27 19:22:38 +09:00
const restore = externalActive ? (2.0 + bedComfort * 2.0) : (12 + bedComfort * 14);
const maxRestore = externalActive ? Math.min(d, 3.2) : d;
const step = Math.min(maxRestore, dt * restore);
const sx = dx / d * step;
const sy = dy / d * step;
2026-07-11 21:13:39 +09:00
if (this.world?.moveTarinaiWithCollision) this.world.moveTarinaiWithCollision(this, sx, sy, { dt });
2026-06-27 19:22:38 +09:00
else { this.x += sx; this.y += sy; }
} else if (!externalActive && Math.hypot((this.vx || 0) + (this.impulseVx || 0), (this.vy || 0) + (this.impulseVy || 0)) < 0.35) {
2026-06-21 14:09:35 +09:00
this.x = spot.x;
this.y = spot.y;
}
}
2026-06-27 19:22:38 +09:00
const impulseVx = Number.isFinite(this.impulseVx) ? this.impulseVx : 0;
const impulseVy = Number.isFinite(this.impulseVy) ? this.impulseVy : 0;
const moveDx = ((Number.isFinite(this.vx) ? this.vx : 0) + impulseVx) * dt;
const moveDy = ((Number.isFinite(this.vy) ? this.vy : 0) + impulseVy) * dt;
if (Math.hypot(moveDx, moveDy) > 0.001) {
2026-07-11 21:13:39 +09:00
if (this.world?.moveTarinaiWithCollision) this.world.moveTarinaiWithCollision(this, moveDx, moveDy, { dt, maxChecks: 18, maxStep: 10 });
2026-06-27 19:22:38 +09:00
else { this.x += moveDx; this.y += moveDy; }
}
if (this.world?.resolveSolidObstacleCollision) {
const beforeContactX = Number(this.x || 0) || 0;
const beforeContactY = Number(this.y || 0) || 0;
2026-07-11 21:13:39 +09:00
const pushed = this.world.resolveSolidObstacleCollision(this, { maxPasses: 2, searchRadius: Math.max(72, (Number(this.radius) || 22) + 120), maxChecks: 18, slop: 0.28 });
2026-06-27 19:22:38 +09:00
if (pushed) {
this.lastSolidObstacleCollisionAt = this.world.time || 0;
marker?.markSleepExternalMotion?.(this, dt, "sleep-full-contact");
const cdx = (Number(this.x || 0) || 0) - beforeContactX;
const cdy = (Number(this.y || 0) || 0) - beforeContactY;
2026-06-28 13:40:41 +09:00
if (Math.hypot(cdx, cdy) > 1.8 && !this.sleepDisease && marker?.wakeSleepingFromExternalMotion?.(this, "\u3076\u3064\u304b\u3063\u3066\u76ee\u304c\u899a\u3081\u305f")) return;
2026-06-27 19:22:38 +09:00
if (Math.hypot(cdx, cdy) > 0.05) {
this.vx = clamp((Number(this.vx) || 0) + cdx / Math.max(0.016, dt) * 0.085, -170, 170);
this.vy = clamp((Number(this.vy) || 0) + cdy / Math.max(0.016, dt) * 0.085, -170, 170);
}
}
}
2026-06-21 14:09:35 +09:00
this.updateNestBoxPresence(dt);
2026-06-27 19:22:38 +09:00
const bodyDrag = externalActive ? 0.86 : 0.55;
this.vx *= Math.pow(bodyDrag, dt * 60);
this.vy *= Math.pow(bodyDrag, dt * 60);
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-21 14:09:35 +09:00
return;
}
if (this.birthRitualTimer > 0.04 || this.state === "birth_ritual") {
2026-06-27 19:22:38 +09:00
const pendingImpulse = Math.hypot(Number.isFinite(this.impulseVx) ? this.impulseVx : 0, Number.isFinite(this.impulseVy) ? this.impulseVy : 0);
const canceledByForce = this.world?.cancelBirthRitualOnForce?.(this, pendingImpulse, {
2026-06-28 13:40:41 +09:00
reason: "\u5f37\u3044\u885d\u6483\u3067\u7e41\u6b96\u304c\u4e2d\u65ad\u3055\u308c\u305f",
2026-06-27 19:22:38 +09:00
target: this.target || null,
fear: 1.0,
cause: "stored_impulse",
panic: true,
silentLog: true,
});
if (!canceledByForce) {
const partner = this.world.liveTarinaiById?.(this.birthPartnerId);
if (partner) {
const midX = (this.x + partner.x) / 2;
const midY = (this.y + partner.y) / 2;
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;
}
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;
return;
2026-06-21 14:09:35 +09:00
}
}
if (this.intimidateTimer > 0.04 || this.state === "intimidate" || this.state === "ant_intimidate") {
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;
}
2026-06-30 22:30:37 +09:00
const burningNow = Boolean(this.burning || (this.burnTimer || 0) > 0.02);
if (burningNow) {
const burnText = globalThis.TarinaiItemDynamicToolSystem?.fireBurningText || "\u71c3\u3048\u3066\u9003\u3052\u307e\u3069\u3063\u3066\u3044\u308b\u3002";
this.state = "panic";
this.target = null;
this.lastPanicCause = "burning";
this.lastPanicDetail = burnText;
this.thought = burnText;
2026-07-01 14:41:05 +09:00
globalThis.TarinaiBurnMotionUtil?.updateBurnWanderAngle(this, dt, {
angleKey: "tarinai-burn-run-angle",
turnKey: "tarinai-burn-run-turn",
jitterKey: "tarinai-burn-run-jitter",
bucketScale: 5,
turnMin: 0.10,
turnMax: 0.34,
jitterMin: -8.0,
jitterMax: 8.0,
});
2026-06-30 22:30:37 +09:00
ax += Math.cos(this.burnWanderAngle || this.wanderAngle || 0) * baseSpeed * 3.15;
ay += Math.sin(this.burnWanderAngle || this.wanderAngle || 0) * baseSpeed * 3.15;
} else {
2026-06-21 14:09:35 +09:00
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-07-16 22:12:03 +09:00
const baseMoveTarget = this.state === "seek_bed" && this.isSleepFurniture(this.target) ? this.sleepSpotFor(this.target) : (this.state === "play_seesaw" ? (globalThis.TarinaiSeesawSystem?.seatWorld?.(this.target, this.seesawSeatIndex) || this.target) : this.target);
2026-07-05 18:01:36 +09:00
const routeTargetItem = this.target?.shelterItem || this.target?.hostItem || this.target;
2026-06-30 10:57:06 +09:00
const pathPadding = Math.max(18, (this.radius || 20) * 0.82);
const targetFoodType = this.target?.type === "duplicator" ? this.target?.storedFoodType : this.target?.type;
const zundaFoodTarget = this.state === "seek_food" && (targetFoodType === "sweet" || targetFoodType === "zunda_juice");
2026-07-18 13:06:02 +09:00
const waypoint = ROUTE_STATES.has(this.state) && this.world?.findTarinaiPathWaypoint
2026-06-30 10:57:06 +09:00
? this.world.findTarinaiPathWaypoint(this, baseMoveTarget, { targetItem: routeTargetItem, padding: pathPadding, state: this.state, preferFullPath: zundaFoodTarget })
2026-06-29 21:02:04 +09:00
: null;
2026-06-30 10:57:06 +09:00
if (this.state === "seek_bed" && this.target?.type === "grass_bed") {
const bedId = this.target.id || "";
if (this._bedUnreachableTargetId !== bedId) {
this._bedUnreachableTargetId = bedId;
this._bedUnreachableTimer = 0;
this._bedLastDistance = Infinity;
}
const bedDistance = distXY(this.x, this.y, baseMoveTarget.x, baseMoveTarget.y);
const directBlocked = this.world?.pathBlockedByFence?.(this.x, this.y, baseMoveTarget.x, baseMoveTarget.y, pathPadding, { exclude: routeTargetItem });
const notImproving = Number.isFinite(this._bedLastDistance) && bedDistance > this._bedLastDistance - 1.2;
const slow = Math.hypot(this.vx || 0, this.vy || 0) < 7;
if ((directBlocked && !waypoint) || (directBlocked && notImproving && slow)) this._bedUnreachableTimer = (this._bedUnreachableTimer || 0) + dt;
else this._bedUnreachableTimer = Math.max(0, (this._bedUnreachableTimer || 0) - dt * 0.55);
this._bedLastDistance = bedDistance;
if ((this._bedUnreachableTimer || 0) > 4.2) {
this.abandonUnreachableGrassBed?.(this.target);
return;
}
} else {
this._bedUnreachableTimer = 0;
this._bedUnreachableTargetId = "";
}
2026-06-29 21:02:04 +09:00
const moveTarget = waypoint || baseMoveTarget;
2026-06-21 14:09:35 +09:00
const dx = moveTarget.x - this.x;
const dy = moveTarget.y - this.y;
const d = Math.hypot(dx, dy) || 1;
2026-06-23 22:39:20 +09:00
if (this.state === "wander" && this.target?.detour && d < Math.max(22, (this.radius || 20) * 0.9)) {
if (typeof applyNeedSatisfaction === "function") applyNeedSatisfaction(this, { fulfill: 6 }, "wander");
2026-06-29 16:21:58 +09:00
clearTarinaiBehavior(this, { reason: this.thought });
2026-06-25 14:51:58 +09:00
this.behaviorLockTimer = 0;
2026-06-24 13:48:22 +09:00
this.goIdle?.("\u5c11\u3057\u6b69\u3044\u305f");
2026-06-23 22:39:20 +09:00
return;
}
2026-06-21 14:09:35 +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-07-03 00:45:22 +09:00
const reachedSleepFurniture = isNestContainerItem(this.target)
2026-06-21 14:09:35 +09:00
? (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-25 14:51:58 +09:00
const sleepTarget = this.target;
const isEasyBed = sleepTarget?.type === "grass_bed";
sleepTarget?.use?.(this, this.world, isEasyBed ? { purpose: "sleep" } : undefined);
2026-07-03 00:45:22 +09:00
const sleepText = isNestContainerItem(sleepTarget) ? sleepContainerText(sleepTarget, "\u4f11\u3093\u3067\u3044\u308b") : (isEasyBed ? "\u304b\u3093\u305f\u3093\u30d9\u30c3\u30c9\u3067\u5bdd\u3066\u3044\u308b" : "\u30d9\u30c3\u30c9\u3067\u4f11\u3093\u3067\u3044\u308b");
this.startSleeping?.(sleepTarget, sleepText);
if (isNestContainerItem(sleepTarget)) this.enterNestBox(sleepTarget, dt);
2026-06-21 14:09:35 +09:00
return;
}
}
2026-07-03 00:45:22 +09:00
if (this.state === "panic" && this.lastPanicCause === "tarinai_champion" && isNestContainerItem(this.target)) {
2026-06-30 22:30:37 +09:00
const nestDist = distXY(this.x, this.y, this.target.x, this.target.y);
if (nestDist <= Math.max(50, (this.radius || 20) + (this.target.r || 24) + 18)) {
this.enterNestBox?.(this.target, dt, { forceDisplace: true, reason: TARINAI_KING_FLEE_REASON });
return;
}
}
2026-06-21 14:09:35 +09:00
let sign = (this.state === "panic" || this.state === "cursor_enemy") ? -1 : 1;
2026-07-03 00:45:22 +09:00
if (this.state === "panic" && this.lastPanicCause === "tarinai_champion" && isNestContainerItem(this.target)) sign = 1;
2026-06-21 14:09:35 +09:00
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;
} else if (this.state === "play_ball") {
strength = d < this.radius + 18 ? 1.75 : 1.28;
sign = 1;
2026-07-16 22:12:03 +09:00
} else if (this.state === "play_seesaw") {
strength = d < this.radius + 12 ? 0.42 : 1.18;
sign = 1;
2026-06-21 14:09:35 +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;
}
} 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;
2026-07-15 14:44:29 +09:00
if (Number.isFinite(this.target.hp)) {
globalThis.TarinaiAchievements?.recordAntDamageSource?.(this.target, this, { world: this.world, reason: "tarinai_attack" });
this.target.hp = Math.max(0, this.target.hp - (this.outgoingDamage ? this.outgoingDamage(4.6 + aggression * 3.4) : (4.6 + aggression * 3.4)));
}
2026-06-21 14:09:35 +09:00
this.fearTimer = Math.max(this.fearTimer, 0.12);
this.thought = "\u30a2\u30ea\u3092\u653b\u6483\u3057\u3066\u3044\u308b";
2026-06-27 19:22:38 +09:00
if (deterministicChance(this.world, "ant-attack-bubble", 0.55, this, this.target)) this.bubble("!", 0.8, "rgba(145,68,52,0.76)");
2026-06-21 14:09:35 +09:00
}
}
}
ax += (dx / d) * baseSpeed * sign * strength;
ay += (dy / d) * baseSpeed * sign * strength;
} else if (this.state === "panic") {
2026-06-27 19:22:38 +09:00
this.wanderAngle += deterministicRange(this.world, "panic-wander", -3.8, 3.8, this) * dt;
2026-06-21 14:09:35 +09:00
ax += Math.cos(this.wanderAngle) * baseSpeed * 1.5;
ay += Math.sin(this.wanderAngle) * baseSpeed * 1.5;
} else if (this.state === "zunchi_sick") {
2026-06-27 19:22:38 +09:00
this.wanderAngle += deterministicRange(this.world, "zunchi-sick-wander", -2.4, 2.4, this) * dt + Math.sin(this.world.time * 1.7 + this.age) * dt * 0.8;
2026-06-21 14:09:35 +09:00
ax += Math.cos(this.wanderAngle) * baseSpeed * 0.72;
ay += Math.sin(this.wanderAngle) * baseSpeed * 0.72;
} else if (this.state === "fight_sick") {
2026-06-27 19:22:38 +09:00
this.wanderAngle += deterministicRange(this.world, "fight-sick-wander", -4.2, 4.2, this) * dt + Math.sin(this.world.time * 3.2 + this.age) * dt * 1.6;
2026-06-21 14:09:35 +09:00
ax += Math.cos(this.wanderAngle) * baseSpeed * 0.88;
ay += Math.sin(this.wanderAngle) * baseSpeed * 0.88;
} else {
ax += Math.cos(this.wanderAngle) * baseSpeed * 0.34;
ay += Math.sin(this.wanderAngle) * baseSpeed * 0.34;
}
2026-06-30 22:30:37 +09:00
}
2026-06-21 14:09:35 +09:00
this.vx += ax * dt;
this.vy += ay * dt;
2026-06-25 22:30:35 +09:00
const iceGround = (this.world?.groundType || "soil") === "ice";
const moveFriction = iceGround ? 0.965 : 0.84;
this.vx *= Math.pow(moveFriction, dt * 8);
this.vy *= Math.pow(moveFriction, dt * 8);
2026-06-21 14:09:35 +09:00
2026-06-30 22:30:37 +09:00
const maxV = (burningNow ? 168 : (this.state === "panic" ? 116 : (this.state === "zunchi_sick" ? 54 : (this.state === "fight_sick" ? 68 : 76)))) * Math.max(1, itemSpeedMul || 1) * (this.geneticStatRatio ? this.geneticStatRatio("speed") : 1) * (iceGround ? 1.35 : 1);
2026-06-21 14:09:35 +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;
}
const impulseVx = Number.isFinite(this.impulseVx) ? this.impulseVx : 0;
const impulseVy = Number.isFinite(this.impulseVy) ? this.impulseVy : 0;
2026-06-27 19:22:38 +09:00
const moveDx = (this.vx + impulseVx) * dt;
const moveDy = (this.vy + impulseVy) * dt;
if (this.world?.moveTarinaiWithCollision) {
const motionOnlyPass = Boolean(this._motionOnlyPhysicsPass);
this.world.moveTarinaiWithCollision(this, moveDx, moveDy, motionOnlyPass
2026-07-11 21:13:39 +09:00
? { dt, light: true, maxChecks: this.state === "panic" ? 18 : 14, maxStep: this.state === "panic" ? 12 : 14 }
: { dt });
2026-06-27 19:22:38 +09:00
}
else {
this.x += moveDx;
this.y += moveDy;
}
2026-06-21 14:09:35 +09:00
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;
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-27 19:22:38 +09:00
if (this.x < p) { this.x = p; this.vx = Math.abs(this.vx) * 0.5; this.wanderAngle = deterministicRange(this.world, "wall-bounce-left", -0.8, 0.8, this); this.surpriseTimer = 0.15; }
if (this.y < p) { this.y = p; this.vy = Math.abs(this.vy) * 0.5; this.wanderAngle = deterministicRange(this.world, "wall-bounce-top", 0.3, 1.8, this); this.surpriseTimer = 0.15; }
if (this.x > maxX) { this.x = maxX; this.vx = -Math.abs(this.vx) * 0.5; this.wanderAngle = deterministicRange(this.world, "wall-bounce-right", 2.5, 3.8, this); this.surpriseTimer = 0.15; }
if (this.y > maxY) { this.y = maxY; this.vy = -Math.abs(this.vy) * 0.5; this.wanderAngle = deterministicRange(this.world, "wall-bounce-bottom", -2.2, -0.7, this); this.surpriseTimer = 0.15; }
2026-06-21 14:09:35 +09:00
},
2026-06-30 10:57:06 +09:00
temperatureDeathReason() {
2026-06-30 13:40:36 +09:00
const status = this.temperatureStatus || this.world?.temperatureStatusFor?.(this.feltTemperature ?? this.tempComfort ?? (CONFIG.standardTemperature ?? 15), this) || null;
2026-06-30 10:57:06 +09:00
const recent = (this.world?.time || 0) - Number(this.lastTemperatureDamageAt || -999) <= 36;
2026-06-30 22:30:37 +09:00
if (recent && status && (status.harmExcess || 0) > 0) return status.direction === "cold" ? "\u51CD\u7D50" : "\u71B1\u4E2D\u75C7";
2026-06-30 10:57:06 +09:00
return "";
},
2026-06-30 22:30:37 +09:00
abandonUnreachableGrassBed(bed, reason = "\u8FBF\u308A\u3064\u3051\u306A\u3044\u306E\u3067\u653E\u68C4\u3057\u305F") {
2026-06-30 10:57:06 +09:00
if (!bed || bed.dead || bed.type !== "grass_bed") return false;
bed.ownerId = "";
bed.unreachableUntil = 0;
bed.abandonedById = this.id || "";
bed.abandonedUntil = (this.world?.time || 0) + 90;
bed.carriedById = "";
this.target = null;
this.targetKey = "";
this._bedUnreachableTimer = 0;
this._bedUnreachableTargetId = "";
this.actionIdCooldowns = this.actionIdCooldowns || {};
this.actionIdCooldowns.sleep_in_bed = Math.max(Number(this.actionIdCooldowns.sleep_in_bed || 0) || 0, 6.0);
this.actionCooldowns = this.actionCooldowns || {};
this.actionCooldowns.sleep = Math.max(Number(this.actionCooldowns.sleep || 0) || 0, 3.0);
if (typeof clearTarinaiBehavior === "function") clearTarinaiBehavior(this, { reason });
this.goIdle?.(reason);
this.world?.markItemBucketsDirty?.("grass-bed-abandoned-unreachable");
this.world?.markSpatialDirty?.("grass-bed-abandoned-unreachable");
return true;
},
2026-06-21 14:09:35 +09:00
checkLife(dt = 0.016) {
let reason = "";
2026-07-03 20:57:02 +09:00
const activeDiseaseDeathReason = () => {
2026-07-11 21:13:39 +09:00
if (this.zunchiDisease) return "\u305a\u3093\u3061\u75c5";
if (this.sleepDisease) return "\u306d\u3080\u308a\u75c5";
if (this.explosionDisease) return "\u7206\u767a\u75c5";
if (this.fightDisease) return "\u304d\u305a\u3064\u304d\u75c5";
2026-07-03 20:57:02 +09:00
return "";
};
2026-06-21 14:09:35 +09:00
if (this.energy <= 0) {
2026-07-11 21:13:39 +09:00
reason = this.temperatureDeathReason?.() || activeDiseaseDeathReason() || this.dominantDeathCause("", { lowHealth: true }) || "\u4e8b\u6545";
2026-06-21 14:09:35 +09:00
}
2026-07-11 21:13:39 +09:00
else if (this.hunger >= 108) reason = "\u98e2\u9913";
else if (this.age >= this.lifeSpan) reason = "\u5929\u5bff\u3092\u5168\u3046\u3057\u305f";
2026-06-21 14:09:35 +09:00
else if (this.stress >= 124) {
2026-07-11 21:13:39 +09:00
reason = activeDiseaseDeathReason() || this.dominantDeathCause("\u30b9\u30c8\u30ec\u30b9\u904e\u591a", { stressDeath: true }) || "\u30b9\u30c8\u30ec\u30b9\u904e\u591a";
2026-06-21 14:09:35 +09:00
}
if (reason) this.die(reason);
},
2026-06-28 13:40:41 +09:00
die(reason, opts = {}) {
2026-06-21 14:09:35 +09:00
if (this.dead) return;
2026-06-28 13:40:41 +09:00
const finalReason = this.normalizeDeathReason ? this.normalizeDeathReason(reason, opts) : (reason || "\u4e8b\u6545");
2026-06-21 14:09:35 +09:00
this.dead = true;
this.deathReason = finalReason;
2026-07-20 14:36:59 +09:00
this.deathTime = Number(this.world?.time || 0) || 0;
2026-07-18 13:06:02 +09:00
this.world.syncTarinaiCountEntry?.(this);
2026-06-21 14:09:35 +09:00
this.world.deadCount += 1;
2026-07-18 13:06:02 +09:00
globalThis.TarinaiAchievements?.evaluateEvent?.(this.world, "population", { tarinai: this, delta: -1, reason: finalReason });
2026-07-15 14:44:29 +09:00
globalThis.TarinaiAchievements?.recordDeath?.({ world: this.world, tarinai: this, reason: finalReason, fromDamage: Boolean(opts.fromDamage), achievementDangerKill: Boolean(opts.achievementDangerKill) });
2026-06-28 13:40:41 +09:00
this.world.lastDeathAt = this.world.time || 0;
if (!Array.isArray(this.world.recentDeathTimes)) this.world.recentDeathTimes = [];
this.world.recentDeathTimes.push(this.world.lastDeathAt);
if (this.world.recentDeathTimes.length > 24) this.world.recentDeathTimes = this.world.recentDeathTimes.slice(-24);
2026-07-05 18:01:36 +09:00
const plushieSource = this.world.itemsOfType?.("plushie") || this.world.items || [];
for (const item of plushieSource) {
2026-06-26 19:04:32 +09:00
if (!item || item.dead || !item.isStructure || item.type !== "plushie" || item.ownerId !== this.id) continue;
item.hp = 0;
item.amount = 0;
item.carriedById = "";
item.onHead = false;
}
this.world.drawListDirty = true;
this.world.markItemBucketsDirty?.("tarinai-death-plushie-cleanup");
this.world.markSpatialDirty?.("tarinai-death-plushie-cleanup");
2026-07-20 14:36:59 +09:00
this.world.addResidue?.("trace", this.x, this.y, { reason: "tarinai-death-trace" });
// Favorite Tarinai are memorialized immediately and therefore do not leave
// the splat resource that normal graves are built from.
if (!this.favorite) this.world.addResidue?.("splat", this.x, this.y, { reason: "tarinai-death-splat" });
this.world.markTerrainDirtyAt?.(this.x, this.y, Math.max(42, (this.radius || 22) * 2.1), "tarinai-death-remains");
2026-06-27 19:22:38 +09:00
if (deterministicChance(this.world, "death-grass-spawn", 0.55, this, finalReason)) {
2026-06-21 14:09:35 +09:00
const spot = this.world.findGrassPlantingSpot(this.x, this.y, { allowOriginal: false, minRadius: 26, maxRadius: 76 });
2026-06-24 21:20:53 +09:00
if (spot) {
const deathGrass = new Item("grass", spot.x, spot.y);
2026-07-09 16:08:11 +09:00
this.world.addItem?.(deathGrass, "tarinai-death-grass");
2026-06-24 21:20:53 +09:00
}
2026-06-21 14:09:35 +09:00
}
this.world.markDead(this, finalReason);
this.releasedLiveToken = this.liveToken || 0;
this.world.releaseTarinaiLiveId?.(this);
2026-07-18 13:06:02 +09:00
this.world.noteTarinaiDeathForRuntimeCleanup?.(this.id);
2026-06-21 14:09:35 +09:00
audio.death();
this.world.log(`${this.name}\u306f\u9759\u304b\u306a\u75d5\u8de1\u3092\u6b8b\u3057\u305f\u3002\u6b7b\u56e0: ${finalReason}`, "death", { participants: [this] });
if (this.world.selected === this) this.world.selected = null;
}
}));
})(typeof window !== "undefined" ? window : globalThis);