"use strict"; (function (global) { const Tarinai = global.Tarinai; if (!Tarinai) throw new Error("Tarinai is not available for mixin: tarinai_identity_social.js"); Object.defineProperties(Tarinai.prototype, Object.getOwnPropertyDescriptors({ 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; }, syncGrowthScale() { if (!Number.isFinite(this.adultScale)) this.adultScale = adultScaleFromGenetics(this.familyKey || this.id, this.genetics); const nextScale = this.generation > 1 ? scaleForGrowth(this.adultScale, this.growth) : this.adultScale; this.scale = clamp(nextScale, 0.10, 0.42); if (this.refreshEffectiveSize) this.refreshEffectiveSize(); else this.radius = 56 * this.scale; const growthBucket = Math.floor(clamp(this.growth || 0, 0, 1) * 20); if (this.generation > 1 && this.world?.recordFamily && this.familyJoined?.() && this.familyGrowthBucket !== growthBucket) { this.familyGrowthBucket = growthBucket; this.world.recordFamily(this); } return this.scale; }, geneticStatRatio(key = "") { this.genetics = normalizeGenetics(this.genetics, this.familyKey || this.id); if (key === "life") return this.genetics.lifeSpanMul || 1; if (key === "attack") return this.genetics.attackMul || 1; if (key === "speed") return this.genetics.speedMul || 1; if (key === "size") return this.genetics.sizeMul || 1; return 1; }, parentToFollow() { if (!this.juvenile || !this.parents?.length) return null; let best = null, bestD = Infinity; for (const id of this.parents) { const p = this.world.tarinai.find(t => (t.familyKey || t.id) === id && !t.dead); 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; }, addRecord(text, kind = "note", options = {}) { if (!text) return; if (options.hiddenFromObservation) return; 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; }, recordChangeCause(reason = "", target = "", options = {}) { const text = String(reason || "").trim(); const affected = String(target || "").trim(); if (!text && !affected) return false; if (!Array.isArray(this.recentChangeCauses)) this.recentChangeCauses = []; const now = this.world?.time || 0; const entry = { time: now, reason: text || "\u5909\u5316", target: affected || "\u72b6\u614b", value: options.value ?? "", }; const head = this.recentChangeCauses[0]; if (head && head.reason === entry.reason && head.target === entry.target) { const oldValue = Number(head.value); const addValue = Number(entry.value); if (Number.isFinite(oldValue) || Number.isFinite(addValue)) head.value = (Number.isFinite(oldValue) ? oldValue : 0) + (Number.isFinite(addValue) ? addValue : 0); else head.value = entry.value || head.value || ""; head.time = now; return true; } this.recentChangeCauses.unshift(entry); if (this.recentChangeCauses.length > 8) this.recentChangeCauses.length = 8; return true; }, personalityProfile() { const base = { label: "\u4e2d\u7acb", fear: 1, fight: 1, social: 1, relation: 1, sleep: 1, zunda: 1, play: 1 }; const e = { neuroticism: personalityEffectValue(typeof effectivePersonalityValue === "function" ? effectivePersonalityValue(this, "neuroticism") : (ensurePersonality(this).currentPersonality.neuroticism || 0)), aggression: personalityEffectValue(typeof effectivePersonalityValue === "function" ? effectivePersonalityValue(this, "aggression") : (ensurePersonality(this).currentPersonality.aggression || 0)), sociability: personalityEffectValue(typeof effectivePersonalityValue === "function" ? effectivePersonalityValue(this, "sociability") : (ensurePersonality(this).currentPersonality.sociability || 0)), openness: personalityEffectValue(typeof effectivePersonalityValue === "function" ? effectivePersonalityValue(this, "openness") : (ensurePersonality(this).currentPersonality.openness || 0)), }; 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), }; }, personalityLabel() { const tags = getPersonalityTraitTags(this); return tags.length ? tags.join(" / ") : "\u4e2d\u7acb"; }, personalityTags() { return getPersonalityTraitTags(this); }, adjustPersonality(key, delta, reason = "") { return adjustPersonality(this, key, delta, reason); }, shouldApplyPersonalityBehavior(key, direction = 1) { return shouldApplyPersonalityBehavior(this, key, direction); }, fightWinCount() { return this.totalFightWins || 0; }, fightLossCount() { return this.totalFightLosses || 0; }, maybeBecomeTimidAfterDamage(cause = "") { if (this.dead) return false; const losses = this.totalFightLosses || 0; const wins = this.totalFightWins || 0; if (losses <= wins) return false; if (Math.random() >= 0.05) return false; const changedA = this.adjustPersonality?.("neuroticism", 0.025, "after being hurt") || 0; const changedB = this.adjustPersonality?.("aggression", -0.015, "after being hurt") || 0; this.fearTimer = Math.max(this.fearTimer || 0, 1.2); this.stress = clamp((this.stress || 0) + 5, 0, 130); return Boolean(changedA || changedB); }, 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; } }, strongestRelation(kind = "friend", liveOnly = true) { let bestId = null; let bestScore = kind === "fear" ? 5 : FRIEND_AFFINITY_THRESHOLD; const liveIds = liveOnly ? new Set((this.world?.tarinai || []).filter(o => o && !o.dead && o !== this).map(o => o.id).filter(Boolean)) : null; for (const [id, rel] of Object.entries(this.relationships || {})) { if (liveIds && !liveIds.has(id)) continue; 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 }; }, 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) { 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"); this.addStress(22, { threshold: 8 }); 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); }, showStressBar(duration = 4.2) { this.stressBarTimer = Math.max(this.stressBarTimer || 0, duration); }, addStress(amount, opts = {}) { const tolerance = Number.isFinite(opts.tolerance) ? opts.tolerance : 0.88; const delta = Math.max(0, (amount || 0) * tolerance); 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; }, 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); }, 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; if (this.enterPanic) this.enterPanic({ target: null, reason: "\u30c0\u30e1\u30fc\u30b8\u3067\u76ee\u304c\u899a\u3081\u305f", fear: 0.85, wake: false, cause: "wake_from_damage" }); else { this.setActionState?.("panic", { target: null, reason: "\u30c0\u30e1\u30fc\u30b8\u3067\u76ee\u304c\u899a\u3081\u305f", wake: false }); 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; }, 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); }, 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; }, 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) { 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"); }, 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.goIdle?.("柵に阻まれて行き先を変えている"); 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.goIdle?.("辿り着けず、あきらめた"); this.wanderAngle += rand(-2.2, 2.2); } }, 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() { // 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; if (critical) return this.stretchTimer > 0 ? "stretch" : "weak"; const weak = this.hunger > 102 || this.energy < 18; if (weak) return "weak"; return null; }, briefDeathReason(reason = this.deathReason) { return HEALTH.briefDeathReason(this, reason); }, 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)); return stableChoice(this.familyKey || this.id, `variant-${category}`, options) || options[0] || this.goodMode || "smile"; } })); })(typeof window !== "undefined" ? window : globalThis);