2026-06-21 14:09:35 +09:00
|
|
|
"use strict";
|
|
|
|
|
|
|
|
|
|
(function (global) {
|
|
|
|
|
const Tarinai = global.Tarinai;
|
|
|
|
|
if (!Tarinai) throw new Error("Tarinai is not available for mixin: tarinai_identity_social.js");
|
2026-06-26 12:46:32 +09:00
|
|
|
|
2026-07-18 13:06:02 +09:00
|
|
|
const relationPeerCache = new WeakMap();
|
|
|
|
|
|
|
|
|
|
function relationPeerEntry(tarinai) {
|
|
|
|
|
const relationships = tarinai?.relationships || {};
|
|
|
|
|
let entry = relationPeerCache.get(tarinai);
|
|
|
|
|
if (!entry || entry.relationships !== relationships) {
|
|
|
|
|
const keys = Object.keys(relationships);
|
|
|
|
|
entry = { relationships, ids: new Set(keys), ownCount: keys.length, incomingBuilt: false };
|
|
|
|
|
relationPeerCache.set(tarinai, entry);
|
|
|
|
|
}
|
|
|
|
|
return entry;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function dropRelationPeerId(tarinai, id) {
|
|
|
|
|
if (!tarinai || !id) return false;
|
|
|
|
|
const relationships = tarinai.relationships && typeof tarinai.relationships === "object" ? tarinai.relationships : null;
|
|
|
|
|
const entry = relationPeerCache.get(tarinai);
|
|
|
|
|
let removed = false;
|
|
|
|
|
if (relationships && Object.prototype.hasOwnProperty.call(relationships, id)) {
|
|
|
|
|
delete relationships[id];
|
|
|
|
|
removed = true;
|
|
|
|
|
}
|
|
|
|
|
if (entry) {
|
|
|
|
|
entry.ids.delete(id);
|
|
|
|
|
if (removed) entry.ownCount = Math.max(0, (entry.ownCount || 0) - 1);
|
|
|
|
|
}
|
|
|
|
|
if (removed) tarinai.relationCache = null;
|
|
|
|
|
return removed;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function lazyDropDeadRelationPeer(tarinai, id) {
|
|
|
|
|
const pending = tarinai?.world?._deadTarinaiIdsPendingCleanup;
|
|
|
|
|
if (!(pending instanceof Set) || pending.size === 0 || !pending.has(id)) return false;
|
|
|
|
|
if (tarinai.world?.liveTarinaiById?.(id)) return false;
|
|
|
|
|
dropRelationPeerId(tarinai, id);
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-26 12:46:32 +09:00
|
|
|
function relationIdSetFor(tarinai) {
|
2026-07-18 13:06:02 +09:00
|
|
|
const entry = relationPeerEntry(tarinai);
|
|
|
|
|
if (!entry.incomingBuilt) {
|
|
|
|
|
for (const other of tarinai?.world?.tarinai || []) {
|
|
|
|
|
if (!other || other.dead || other === tarinai || !other.id) continue;
|
|
|
|
|
if (other.relationships?.[tarinai.id]) entry.ids.add(other.id);
|
|
|
|
|
}
|
|
|
|
|
entry.incomingBuilt = true;
|
2026-06-26 12:46:32 +09:00
|
|
|
}
|
2026-07-18 13:06:02 +09:00
|
|
|
return entry.ids;
|
2026-06-26 12:46:32 +09:00
|
|
|
}
|
|
|
|
|
|
2026-06-21 14:09:35 +09:00
|
|
|
Object.defineProperties(Tarinai.prototype, Object.getOwnPropertyDescriptors({
|
|
|
|
|
get lack() {
|
2026-06-26 12:46:32 +09:00
|
|
|
const healthLack = 100 * (1 - (typeof tarinaiEnergyRatio === "function" ? tarinaiEnergyRatio(this) : clamp((this.energy || 0) / Math.max(1, this.maxEnergy || 100), 0, 1)));
|
|
|
|
|
return clamp((this.hunger + this.loneliness + this.stress + healthLack) / 4, 0, 100);
|
2026-06-21 14:09:35 +09:00
|
|
|
},
|
|
|
|
|
|
|
|
|
|
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;
|
|
|
|
|
},
|
|
|
|
|
|
2026-06-21 22:29:00 +09:00
|
|
|
syncGrowthScale() {
|
|
|
|
|
if (!Number.isFinite(this.adultScale)) this.adultScale = adultScaleFromGenetics(this.familyKey || this.id, this.genetics);
|
2026-07-18 22:15:26 +09:00
|
|
|
const currentGrowth = this.growth;
|
|
|
|
|
const isJuvenileNow = this.generation > 1 && currentGrowth < 1;
|
|
|
|
|
if (this.generation > 1) {
|
|
|
|
|
if (this._growthWasJuvenile === true && !isJuvenileNow) {
|
|
|
|
|
this.world?.log?.(`${this.name}\u304c\u6210\u9577\u3057\u3066\u6210\u4f53\u306b\u306a\u3063\u305f\u3002`, "birth", { participants: [this] });
|
|
|
|
|
}
|
|
|
|
|
this._growthWasJuvenile = isJuvenileNow;
|
|
|
|
|
}
|
|
|
|
|
const nextScale = this.generation > 1 ? scaleForGrowth(this.adultScale, currentGrowth) : this.adultScale;
|
2026-06-21 22:29:00 +09:00
|
|
|
this.scale = clamp(nextScale, 0.10, 0.42);
|
|
|
|
|
if (this.refreshEffectiveSize) this.refreshEffectiveSize();
|
|
|
|
|
else this.radius = 56 * this.scale;
|
2026-07-18 22:15:26 +09:00
|
|
|
const growthBucket = Math.floor(clamp(currentGrowth || 0, 0, 1) * 20);
|
2026-06-21 22:29:00 +09:00
|
|
|
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;
|
|
|
|
|
},
|
|
|
|
|
|
2026-06-21 14:09:35 +09:00
|
|
|
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;
|
|
|
|
|
},
|
|
|
|
|
|
2026-06-21 22:29:00 +09:00
|
|
|
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;
|
|
|
|
|
},
|
|
|
|
|
|
2026-06-21 14:09:35 +09:00
|
|
|
personalityProfile() {
|
|
|
|
|
const base = { label: "\u4e2d\u7acb", fear: 1, fight: 1, social: 1, relation: 1, sleep: 1, zunda: 1, play: 1 };
|
|
|
|
|
const e = {
|
2026-06-22 02:03:19 +09:00
|
|
|
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)),
|
2026-06-21 14:09:35 +09:00
|
|
|
};
|
|
|
|
|
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),
|
|
|
|
|
};
|
|
|
|
|
},
|
|
|
|
|
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-28 13:40:41 +09:00
|
|
|
|
|
|
|
|
|
|
|
|
|
tarinaiChampionEligible() {
|
|
|
|
|
const wins = Number(this.totalFightWins || 0) || 0;
|
|
|
|
|
const losses = Number(this.totalFightLosses || 0) || 0;
|
|
|
|
|
const total = wins + losses;
|
|
|
|
|
return !this.dead && !this.isZunchiSlave && total >= 6 && total > 0 && wins / total >= 0.75;
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
becomeTarinaiChampion(options = {}) {
|
|
|
|
|
if (!this.tarinaiChampionEligible?.() && !options.force) return false;
|
2026-06-30 22:30:37 +09:00
|
|
|
if (this.isTarinaiChampion && !options.forcePersonality) {
|
|
|
|
|
if (typeof tarinaiChampionName === "function") this.name = tarinaiChampionName(this.name);
|
|
|
|
|
return false;
|
|
|
|
|
}
|
2026-06-28 13:40:41 +09:00
|
|
|
this.isTarinaiChampion = true;
|
|
|
|
|
this.tarinaiChampionSince = Number.isFinite(this.tarinaiChampionSince) && this.tarinaiChampionSince > 0 ? this.tarinaiChampionSince : (this.world?.time || 0);
|
2026-06-30 22:30:37 +09:00
|
|
|
if (typeof tarinaiChampionName === "function") {
|
|
|
|
|
this.name = tarinaiChampionName(this.name);
|
|
|
|
|
} else {
|
|
|
|
|
const map = { "\u306F": "\u8987", "\u3046": "\u5B87", "\u3045": "\u30A5", "\u3063": "\uFF01" };
|
|
|
|
|
this.name = String(this.name || "").replace(/[\u306F\u3046\u3045\u3063]/g, ch => map[ch] || ch);
|
|
|
|
|
}
|
2026-06-28 13:40:41 +09:00
|
|
|
ensurePersonality(this);
|
|
|
|
|
const boosted = [];
|
|
|
|
|
for (const key of ["aggression", "openness", "sociability"]) {
|
|
|
|
|
const before = Number(this.currentPersonality?.[key]) || 0;
|
|
|
|
|
const next = clampPersonalityValue(Math.max(before + 0.48, 0.86));
|
|
|
|
|
this.currentPersonality[key] = next;
|
|
|
|
|
if (this.birthPersonality && Number(this.birthPersonality[key]) > next) this.currentPersonality[key] = clampPersonalityValue(this.birthPersonality[key]);
|
|
|
|
|
if (Math.abs((Number(this.currentPersonality[key]) || 0) - before) > 0.001) boosted.push(PERSONALITY_LABELS[key]?.label || key);
|
|
|
|
|
}
|
|
|
|
|
this.surpriseTimer = Math.max(this.surpriseTimer || 0, 0.65);
|
|
|
|
|
this.focusPulseTimer = Math.max(this.focusPulseTimer || 0, 1.0);
|
2026-06-30 22:30:37 +09:00
|
|
|
if (boosted.length) this.recordChangeCause?.("\u305F\u308A\u306A\u3044\u738B\u8005", boosted.join("\u30FB"), { value: +1 });
|
2026-07-18 13:06:02 +09:00
|
|
|
this.world?.syncTarinaiCountEntry?.(this);
|
2026-06-28 13:40:41 +09:00
|
|
|
this.world?.recordFamily?.(this);
|
|
|
|
|
return true;
|
|
|
|
|
},
|
|
|
|
|
|
2026-06-21 14:09:35 +09:00
|
|
|
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);
|
2026-06-23 18:11:42 +09:00
|
|
|
if (typeof applyNeedShock === "function") applyNeedShock(this, { safety: 9 });
|
2026-06-21 14:09:35 +09:00
|
|
|
return Boolean(changedA || changedB);
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
relationTo(id) {
|
|
|
|
|
if (!id) return relationDefaults();
|
2026-07-18 13:06:02 +09:00
|
|
|
const pendingDead = this.world?._deadTarinaiIdsPendingCleanup;
|
|
|
|
|
if (pendingDead instanceof Set && pendingDead.size > 0 && pendingDead.has(id) && !this.world?.liveTarinaiById?.(id)) {
|
|
|
|
|
dropRelationPeerId(this, id);
|
|
|
|
|
return relationDefaults();
|
|
|
|
|
}
|
2026-06-21 14:09:35 +09:00
|
|
|
if (!this.relationships) this.relationships = {};
|
2026-07-18 13:06:02 +09:00
|
|
|
if (!this.relationships[id]) {
|
|
|
|
|
this.relationships[id] = relationDefaults();
|
|
|
|
|
const entry = relationPeerEntry(this);
|
|
|
|
|
entry.ids.add(id);
|
|
|
|
|
entry.ownCount = (entry.ownCount || 0) + 1;
|
|
|
|
|
const other = this.world?.liveTarinaiById?.(id) || null;
|
|
|
|
|
if (other) relationPeerEntry(other).ids.add(this.id);
|
|
|
|
|
}
|
2026-06-21 14:09:35 +09:00
|
|
|
return this.relationships[id];
|
|
|
|
|
},
|
|
|
|
|
|
2026-07-18 13:06:02 +09:00
|
|
|
pruneDeadRelationshipRefs(deadIds) {
|
|
|
|
|
if (!(deadIds instanceof Set) || deadIds.size === 0) return 0;
|
|
|
|
|
let removed = 0;
|
|
|
|
|
for (const id of deadIds) if (dropRelationPeerId(this, id)) removed += 1;
|
|
|
|
|
return removed;
|
|
|
|
|
},
|
|
|
|
|
|
2026-06-21 14:09:35 +09:00
|
|
|
adjustRelation(other, affinityDelta = 0, fearDelta = 0, event = "") {
|
|
|
|
|
if (!other || !other.id || other === this) return;
|
|
|
|
|
const rel = this.relationTo(other.id);
|
2026-07-18 22:15:26 +09:00
|
|
|
const threshold = typeof FRIEND_AFFINITY_THRESHOLD === "number" ? FRIEND_AFFINITY_THRESHOLD : 14;
|
|
|
|
|
const reverseBefore = other.relationships?.[this.id] || null;
|
|
|
|
|
const wasFriend = Math.max(rel.affinity || 0, reverseBefore?.affinity || 0) >= threshold;
|
2026-06-21 14:09:35 +09:00
|
|
|
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-24 17:39:44 +09:00
|
|
|
this.relationCache = null;
|
2026-06-26 12:46:32 +09:00
|
|
|
if (affinityDelta > 0 && rel.affinity >= threshold && other.relationTo) {
|
|
|
|
|
const reverse = other.relationTo(this.id);
|
|
|
|
|
const mirrored = Math.max(reverse.affinity || 0, threshold + 0.25, Math.min(rel.affinity, rel.affinity * 0.78));
|
|
|
|
|
reverse.affinity = clamp(mirrored, -24, 40);
|
|
|
|
|
reverse.fear = clamp(Math.min(reverse.fear || 0, rel.fear || 0), 0, 40);
|
|
|
|
|
if (event && !reverse.lastEvent) { reverse.lastEvent = event; reverse.lastTime = this.world?.time || other.world?.time || 0; }
|
|
|
|
|
other.relationCache = null;
|
|
|
|
|
}
|
2026-07-18 22:15:26 +09:00
|
|
|
const reverseAfter = other.relationships?.[this.id] || null;
|
|
|
|
|
const isFriend = Math.max(rel.affinity || 0, reverseAfter?.affinity || 0) >= threshold;
|
|
|
|
|
if (!wasFriend && isFriend) {
|
|
|
|
|
this.world?.log?.(`${this.name}\u3068${other.name}\u306f\u53cb\u9054\u306b\u306a\u3063\u305f\u3002`, "relation", { participants: [this, other] });
|
|
|
|
|
}
|
2026-06-21 14:09:35 +09:00
|
|
|
},
|
|
|
|
|
|
|
|
|
|
strongestRelation(kind = "friend", liveOnly = true) {
|
|
|
|
|
let bestId = null;
|
|
|
|
|
let bestScore = kind === "fear" ? 5 : FRIEND_AFFINITY_THRESHOLD;
|
2026-06-26 12:46:32 +09:00
|
|
|
for (const id of relationIdSetFor(this)) {
|
|
|
|
|
const other = this.world?.liveTarinaiById?.(id) || null;
|
2026-07-18 13:06:02 +09:00
|
|
|
if (liveOnly && (!other || id === this.id)) {
|
|
|
|
|
if (!other) lazyDropDeadRelationPeer(this, id);
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
const rel = this.relationships?.[id] || relationDefaults();
|
2026-06-26 12:46:32 +09:00
|
|
|
const reverse = kind === "fear" ? null : (other?.relationships?.[this.id] || null);
|
|
|
|
|
const score = kind === "fear" ? (rel.fear || 0) : Math.max(rel.affinity || 0, reverse?.affinity || 0);
|
2026-06-21 14:09:35 +09:00
|
|
|
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) {
|
2026-06-24 17:39:44 +09:00
|
|
|
const now = this.world?.time || 0;
|
|
|
|
|
const cacheKey = `${minScore}:${liveOnly ? 1 : 0}`;
|
|
|
|
|
if (this.relationCache?.friendCountKey === cacheKey && now < (this.relationCache.friendCountUntil || 0)) return this.relationCache.friendCount;
|
2026-06-21 14:09:35 +09:00
|
|
|
let count = 0;
|
2026-06-26 12:46:32 +09:00
|
|
|
for (const id of relationIdSetFor(this)) {
|
|
|
|
|
const other = this.world?.liveTarinaiById?.(id) || null;
|
2026-07-18 13:06:02 +09:00
|
|
|
if (liveOnly && (!other || id === this.id)) {
|
|
|
|
|
if (!other) lazyDropDeadRelationPeer(this, id);
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
const rel = this.relationships?.[id] || relationDefaults();
|
2026-06-26 12:46:32 +09:00
|
|
|
const reverse = other?.relationships?.[this.id] || null;
|
|
|
|
|
if (Math.max(rel.affinity || 0, reverse?.affinity || 0) <= minScore) continue;
|
2026-06-21 14:09:35 +09:00
|
|
|
count += 1;
|
|
|
|
|
}
|
2026-06-24 17:39:44 +09:00
|
|
|
this.relationCache = { ...(this.relationCache || {}), friendCountKey: cacheKey, friendCount: count, friendCountUntil: now + 1.4 };
|
2026-06-21 14:09:35 +09:00
|
|
|
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-24 17:39:44 +09:00
|
|
|
const now = this.world?.time || 0;
|
|
|
|
|
const cacheKey = `${minScore}:${Number.isFinite(maxDist) ? Math.round(maxDist) : "inf"}`;
|
|
|
|
|
if (this.relationCache?.bestFriendKey === cacheKey && now < (this.relationCache.bestFriendUntil || 0)) {
|
|
|
|
|
const cached = this.world?.liveTarinaiById?.(this.relationCache.bestFriendId);
|
|
|
|
|
if (cached && (!Number.isFinite(maxDist) || dist(this, cached) <= maxDist)) return cached;
|
|
|
|
|
}
|
2026-06-21 14:09:35 +09:00
|
|
|
let best = null, bestScore = minScore;
|
2026-06-26 12:46:32 +09:00
|
|
|
for (const id of relationIdSetFor(this)) {
|
2026-06-24 17:39:44 +09:00
|
|
|
const t = this.world.liveTarinaiById?.(id) || null;
|
2026-07-18 13:06:02 +09:00
|
|
|
if (!t) {
|
|
|
|
|
lazyDropDeadRelationPeer(this, id);
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
const rel = this.relationships?.[id] || relationDefaults();
|
2026-06-26 12:46:32 +09:00
|
|
|
const reverse = t?.relationships?.[this.id] || null;
|
|
|
|
|
const score = Math.max(rel.affinity || 0, reverse?.affinity || 0);
|
|
|
|
|
if (score <= bestScore) continue;
|
2026-06-21 14:09:35 +09:00
|
|
|
if (Number.isFinite(maxDist) && dist(this, t) > maxDist) continue;
|
|
|
|
|
best = t;
|
|
|
|
|
bestScore = score;
|
|
|
|
|
}
|
2026-06-24 17:39:44 +09:00
|
|
|
this.relationCache = { ...(this.relationCache || {}), bestFriendKey: cacheKey, bestFriendId: best?.id || "", bestFriendUntil: now + 1.0 };
|
2026-06-21 14:09:35 +09:00
|
|
|
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;
|
2026-06-24 17:39:44 +09:00
|
|
|
const t = this.world.liveTarinaiById?.(id) || null;
|
2026-07-18 13:06:02 +09:00
|
|
|
if (!t) {
|
|
|
|
|
lazyDropDeadRelationPeer(this, id);
|
|
|
|
|
continue;
|
|
|
|
|
}
|
2026-06-21 14:09:35 +09:00
|
|
|
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 = {}) {
|
2026-06-21 22:29:00 +09:00
|
|
|
const tolerance = Number.isFinite(opts.tolerance) ? opts.tolerance : 0.88;
|
|
|
|
|
const delta = Math.max(0, (amount || 0) * tolerance);
|
2026-06-21 14:09:35 +09:00
|
|
|
if (delta <= 0) return 0;
|
2026-06-23 18:11:42 +09:00
|
|
|
if (!this.needShock) this.needShock = {};
|
|
|
|
|
const shock = Math.min(100, delta * 1.8);
|
|
|
|
|
this.needShock.safety = Math.max(this.needShock.safety || 0, shock);
|
2026-06-21 14:09:35 +09:00
|
|
|
const before = this.stress || 0;
|
2026-06-23 18:11:42 +09:00
|
|
|
if (this.needs && typeof calculateStressFromNeeds === "function") {
|
|
|
|
|
const nextSafety = typeof quantizeNeed === "function" ? quantizeNeed((this.needs.safety || 0) + shock) : Math.min(100, (this.needs.safety || 0) + shock);
|
2026-06-25 14:51:58 +09:00
|
|
|
this.stress = typeof applyGroundStressModifier === "function" ? applyGroundStressModifier(this, calculateStressFromNeeds({ ...this.needs, safety: nextSafety })) : calculateStressFromNeeds({ ...this.needs, safety: nextSafety });
|
2026-06-23 18:11:42 +09:00
|
|
|
}
|
|
|
|
|
const gained = Math.max(delta, Math.max(0, (this.stress || 0) - before));
|
2026-06-21 14:09:35 +09:00
|
|
|
const threshold = opts.threshold ?? 8;
|
|
|
|
|
if (gained >= threshold) this.showStressBar(opts.duration ?? 4.2);
|
|
|
|
|
return gained;
|
|
|
|
|
},
|
|
|
|
|
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); },
|
|
|
|
|
|
2026-07-15 14:44:29 +09:00
|
|
|
damage(amount, reason = "", options = {}) { return HEALTH.applyDamage(this, amount, reason, options); },
|
2026-06-21 14:09:35 +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;
|
2026-06-22 02:03:19 +09:00
|
|
|
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); }
|
2026-06-21 14:09:35 +09:00
|
|
|
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;
|
2026-06-26 12:46:32 +09:00
|
|
|
const maxEnergy = typeof tarinaiMaxEnergy === "function" ? tarinaiMaxEnergy(this) : (Number(this.maxEnergy) || 100);
|
|
|
|
|
this.maxEnergy = maxEnergy;
|
|
|
|
|
this.energy = clamp((this.energy || 0) + Math.max(0, amount || 0), 0, maxEnergy);
|
2026-06-21 14:09:35 +09:00
|
|
|
},
|
|
|
|
|
|
|
|
|
|
familyJoined() {
|
|
|
|
|
return Boolean(this.hasPaired || (this.parents || []).length || (this.children || []).length);
|
|
|
|
|
},
|
|
|
|
|
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-07-05 18:01:36 +09:00
|
|
|
const targetIsContainer = Boolean(target && globalThis.TarinaiToolRuntime?.isNestContainerItem?.(target, { alive: false }));
|
2026-07-03 00:45:22 +09:00
|
|
|
return Boolean(target?.type === "sweet" || targetIsContainer || (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-21 14:09:35 +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;
|
2026-06-26 19:04:32 +09:00
|
|
|
} else if (d < (this.targetBestDist || Infinity) - 6) {
|
2026-06-21 14:09:35 +09:00
|
|
|
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;
|
2026-06-24 13:48:22 +09:00
|
|
|
this.goIdle?.("\u67f5\u306b\u963b\u307e\u308c\u3066\u884c\u304d\u5148\u3092\u5909\u3048\u3066\u3044\u308b");
|
2026-06-21 14:09:35 +09:00
|
|
|
this.wanderAngle += rand(-1.4, 1.4);
|
|
|
|
|
return;
|
|
|
|
|
}
|
2026-06-26 19:04:32 +09:00
|
|
|
if (this.world.time - (this.targetSince || this.world.time) > 10.5 && d > this.radius + 24) {
|
2026-06-21 14:09:35 +09:00
|
|
|
this.targetGiveupKey = key;
|
|
|
|
|
this.targetGiveupUntil = this.world.time + 12;
|
|
|
|
|
this.target = null;
|
2026-06-24 13:48:22 +09:00
|
|
|
this.goIdle?.("\u8fbf\u308a\u7740\u3051\u305a\u3001\u3042\u304d\u3089\u3081\u305f");
|
2026-06-21 14:09:35 +09:00
|
|
|
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() {
|
2026-06-30 22:30:37 +09:00
|
|
|
// \u4F53\u529B\u6BD4\u7387\u306B\u3088\u308B\u6BB5\u968E\u8868\u793A: 30%\u4EE5\u4E0B cry\u300120%\u4EE5\u4E0B weak\u300110%\u4EE5\u4E0B stretch\u3002
|
2026-06-30 10:57:06 +09:00
|
|
|
const maxEnergy = typeof tarinaiMaxEnergy === "function" ? tarinaiMaxEnergy(this) : (Number(this.maxEnergy) || 100);
|
|
|
|
|
const energyRatio = (Number(this.energy) || 0) / Math.max(1, Number(maxEnergy) || 100);
|
|
|
|
|
if (energyRatio <= 0.10) return "stretch";
|
|
|
|
|
if (energyRatio <= 0.20) return "weak";
|
|
|
|
|
if (energyRatio <= 0.30) return "cry";
|
2026-06-21 14:09:35 +09:00
|
|
|
return null;
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
briefDeathReason(reason = this.deathReason) { return HEALTH.briefDeathReason(this, reason); },
|
|
|
|
|
|
2026-06-30 10:57:06 +09:00
|
|
|
temperatureDiscomfortSprite(baseId = "") {
|
|
|
|
|
const world = this.world || null;
|
2026-06-30 13:40:36 +09:00
|
|
|
const felt = world?.feltTemperatureFor?.(this) ?? world?.temperatureAt?.(this.x, this.y, this) ?? this.feltTemperature ?? this.tempComfort ?? (CONFIG.standardTemperature ?? 15);
|
|
|
|
|
const status = this.temperatureStatus || world?.temperatureStatusFor?.(felt, this) || null;
|
2026-06-30 10:57:06 +09:00
|
|
|
if (!status || status.comfortable || (status.discomfort || 0) < 2.5) return "";
|
|
|
|
|
const pool = (status.direction === "cold" ? ["cold_1", "cold_2", "cold_3"] : ["hot_1", "hot_2", "hot_3"]).filter(id => SPRITES.some(s => s.id === id));
|
|
|
|
|
if (!pool.length) return "";
|
2026-06-30 13:40:36 +09:00
|
|
|
const u = typeof stableUnit === "function" ? stableUnit(this.familyKey || this.id || this.birthSeed || baseId || "temperature", `temperature-sprite:${status.direction}`) : Math.random();
|
|
|
|
|
const index = Math.max(0, Math.min(pool.length - 1, Math.floor(u * pool.length)));
|
2026-06-30 10:57:06 +09:00
|
|
|
return pool[index] || pool[0] || "";
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
applyTemperatureSprite(baseId = "") {
|
|
|
|
|
const id = String(baseId || "");
|
|
|
|
|
if (!id) return id;
|
|
|
|
|
if (!["smile", "jito", "normal_smirk", "normal_tongue", "normal_happy", "pokan", "angry", "hungry_70"].includes(id)) return id;
|
|
|
|
|
return this.temperatureDiscomfortSprite(id) || id;
|
|
|
|
|
},
|
|
|
|
|
|
2026-06-21 14:09:35 +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));
|
|
|
|
|
return stableChoice(this.familyKey || this.id, `variant-${category}`, options) || options[0] || this.goodMode || "smile";
|
|
|
|
|
}
|
|
|
|
|
}));
|
|
|
|
|
})(typeof window !== "undefined" ? window : globalThis);
|