sounds
This commit is contained in:
parent
c6ba3e38a2
commit
37087d08a7
189 changed files with 12864 additions and 9589 deletions
395
js/tarinai_identity_social.js
Normal file
395
js/tarinai_identity_social.js
Normal file
|
|
@ -0,0 +1,395 @@
|
|||
"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;
|
||||
},
|
||||
|
||||
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;
|
||||
},
|
||||
|
||||
personalityProfile() {
|
||||
const base = { label: "\u4e2d\u7acb", fear: 1, fight: 1, social: 1, relation: 1, sleep: 1, zunda: 1, play: 1 };
|
||||
const p = ensurePersonality(this).currentPersonality;
|
||||
const e = {
|
||||
neuroticism: personalityEffectValue(p.neuroticism),
|
||||
aggression: personalityEffectValue(p.aggression),
|
||||
sociability: personalityEffectValue(p.sociability),
|
||||
openness: personalityEffectValue(p.openness),
|
||||
};
|
||||
return {
|
||||
...base,
|
||||
fear: clamp((base.fear || 1) * (1 + e.neuroticism * 0.38), 0.55, 1.85),
|
||||
fight: clamp((base.fight || 1) * (1 + e.aggression * 0.55), 0.35, 2.35),
|
||||
social: clamp((base.social || 1) * (1 + e.sociability * 0.24), 0.62, 1.52),
|
||||
relation: clamp((base.relation || 1) * (1 + e.sociability * 0.24), 0.62, 1.60),
|
||||
play: clamp((base.play || 1) * (1 + e.openness * 0.45), 0.45, 2.10),
|
||||
};
|
||||
},
|
||||
|
||||
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 delta = Math.max(0, amount || 0);
|
||||
if (delta <= 0) return 0;
|
||||
const before = this.stress || 0;
|
||||
this.stress = clamp(before + delta, 0, 130);
|
||||
const gained = Math.max(0, this.stress - before);
|
||||
const threshold = opts.threshold ?? 8;
|
||||
if (gained >= threshold) this.showStressBar(opts.duration ?? 4.2);
|
||||
return gained;
|
||||
},
|
||||
|
||||
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;
|
||||
this.state = "panic";
|
||||
this.target = null;
|
||||
this.thought = "\u30c0\u30e1\u30fc\u30b8\u3067\u76ee\u304c\u899a\u3081\u305f";
|
||||
this.fearTimer = Math.max(this.fearTimer || 0, 0.85);
|
||||
this.hurtTimer = Math.max(this.hurtTimer || 0, 1.2);
|
||||
this.nestBoxStayUntil = -Infinity;
|
||||
if (this.insideNestBoxId) this.leaveNestBox(box);
|
||||
this.vx += rand(-34, 34);
|
||||
this.vy += rand(-26, 8);
|
||||
return true;
|
||||
},
|
||||
|
||||
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.state = "idle";
|
||||
this.wanderAngle += rand(-1.4, 1.4);
|
||||
return;
|
||||
}
|
||||
if (this.world.time - (this.targetSince || this.world.time) > 30 && d > this.radius + 24) {
|
||||
this.targetGiveupKey = key;
|
||||
this.targetGiveupUntil = this.world.time + 12;
|
||||
this.target = null;
|
||||
this.state = "idle";
|
||||
this.wanderAngle += rand(-2.2, 2.2);
|
||||
this.thought = "\u8fbf\u308a\u7740\u3051\u305a\u3001\u3042\u304d\u3089\u3081\u305f";
|
||||
}
|
||||
},
|
||||
|
||||
panicDestination(threat = null, force = false) {
|
||||
const now = this.world.time || 0;
|
||||
const current = this.panicTarget;
|
||||
if (!force && current && now < (this.panicTargetUntil || 0) && distXY(this.x, this.y, current.x, current.y) > 34) return current;
|
||||
let ax = Math.cos(this.wanderAngle || 0);
|
||||
let ay = Math.sin(this.wanderAngle || 0);
|
||||
if (threat && Number.isFinite(threat.x) && Number.isFinite(threat.y)) {
|
||||
const dx = this.x - threat.x;
|
||||
const dy = this.y - threat.y;
|
||||
const d = Math.hypot(dx, dy) || 1;
|
||||
ax = dx / d;
|
||||
ay = dy / d;
|
||||
}
|
||||
const turn = rand(-0.75, 0.75);
|
||||
const cos = Math.cos(turn);
|
||||
const sin = Math.sin(turn);
|
||||
const dirX = ax * cos - ay * sin;
|
||||
const dirY = ax * sin + ay * cos;
|
||||
const distance = rand(150, 250);
|
||||
const p = CONFIG.worldPadding + this.radius;
|
||||
let x = clamp(this.x + dirX * distance, p, this.world.w - p);
|
||||
let y = clamp(this.y + dirY * distance, p, this.world.h - p);
|
||||
if (distXY(this.x, this.y, x, y) < 70) {
|
||||
x = clamp(this.x - dirY * distance, p, this.world.w - p);
|
||||
y = clamp(this.y + dirX * distance, p, this.world.h - p);
|
||||
}
|
||||
this.panicTarget = { x, y, dead: false, detour: true, panic: true };
|
||||
this.panicTargetUntil = now + rand(1.6, 3.0);
|
||||
this.panicStuckTimer = 0;
|
||||
this.wanderAngle = Math.atan2(y - this.y, x - this.x);
|
||||
return this.panicTarget;
|
||||
},
|
||||
|
||||
lowHealthSprite() {
|
||||
// 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);
|
||||
Loading…
Add table
Add a link
Reference in a new issue