825 lines
42 KiB
JavaScript
825 lines
42 KiB
JavaScript
"use strict";
|
|
|
|
(function (global) {
|
|
const World = global.World;
|
|
if (!World) throw new Error("World is not available for mixin: world_family_social.js");
|
|
Object.defineProperties(World.prototype, Object.getOwnPropertyDescriptors({
|
|
reset(seedPopulation = null, fieldType = this.fieldType || "garden") {
|
|
const field = FIELD_TYPES?.[fieldType] || FIELD_TYPES.garden;
|
|
this.fieldType = field.id;
|
|
this.fieldZoom = 1;
|
|
this.setViewportSize(this.viewportW, this.viewportH);
|
|
this.time = 0;
|
|
this.day = 1;
|
|
this.paused = false;
|
|
this.speed = 1;
|
|
this.toolSize = this.toolSize || "medium";
|
|
this.toolSizes = this.toolSizes || {};
|
|
this.tarinai = [];
|
|
this.items = [];
|
|
this.ants = [];
|
|
this.effects = [];
|
|
this.logs = [];
|
|
this.events = window.TarinaiEvents || null;
|
|
this.eventCounters = {};
|
|
this.countsTimer = 0;
|
|
this.compactTimer = 0;
|
|
this.drawSortTimer = 0;
|
|
this.drawListDirty = true;
|
|
this.antUpdatePhase = 0;
|
|
this.selected = null;
|
|
this.deadCount = 0;
|
|
this.liveIdNext = 1;
|
|
this.liveIdFree = [];
|
|
this.liveIdSerial = 0;
|
|
this.liveTarinai = new Map();
|
|
this.lastBirthAt = -999;
|
|
this.maxGeneration = 1;
|
|
this.family = {};
|
|
this.familyVersion = 0;
|
|
this.relationNotices = {};
|
|
this.resolvedFightIds = {};
|
|
this.fightPairCooldowns = {};
|
|
this.foodSpoilagePenalty = 0;
|
|
this.foodSpoilageEvents = 0;
|
|
this.pointer = { x: this.w / 2, y: this.h / 2, inside: false, motion: 0, movedAt: 0 };
|
|
this.cameraX = Math.max(0, (this.w - (this.viewportW || this.w)) / 2);
|
|
this.cameraY = Math.max(0, (this.h - (this.viewportH || this.h)) / 2);
|
|
this.shakeTimer = 0;
|
|
this.shakeDuration = 0;
|
|
this.shakeStrength = 0;
|
|
this.clampCamera();
|
|
this.lastPhase = this.phaseName();
|
|
this.weather = "sunny";
|
|
this.nextWeatherChange = rand(CONFIG.weatherChangeMin, CONFIG.weatherChangeMax);
|
|
seedPopulation = seedPopulation ?? field.population ?? CONFIG.initialPopulation;
|
|
const grassCount = field.grass ?? 18;
|
|
const types = spawnableSprites().map(s => s.id);
|
|
for (let i = 0; i < seedPopulation; i++) {
|
|
this.addTarinai({
|
|
type: types[i % types.length],
|
|
x: rand(80, this.w - 80),
|
|
y: rand(80, this.h - 80),
|
|
generation: 1,
|
|
});
|
|
}
|
|
for (let i = 0; i < grassCount; i++) this.addItem?.(new Item("grass", rand(70, this.w - 70), rand(70, this.h - 70)), "reset-grass") || this.items.push(new Item("grass", rand(70, this.w - 70), rand(70, this.h - 70)));
|
|
this.addItem?.(new Item("stone", this.w * 0.68, this.h * 0.32), "reset-stone") || this.items.push(new Item("stone", this.w * 0.68, this.h * 0.32));
|
|
this.updateItemCounts();
|
|
this.updateEffectCounts();
|
|
this.markTerrainDirty?.("reset");
|
|
this.rebuildSpatial(true);
|
|
showToast("\u65b0\u3057\u3044\u89b3\u5bdf\u3092\u958b\u59cb\u3057\u307e\u3057\u305f\u3002");
|
|
syncTopButtons();
|
|
},
|
|
|
|
tarinaiFamilyKey(t) {
|
|
return t?.familyKey || t?.archiveKey || t?.id || "";
|
|
},
|
|
|
|
assignTarinaiLiveId(t) {
|
|
if (!t) return "";
|
|
if (!this.liveTarinai) this.liveTarinai = new Map();
|
|
// Live IDs are never recycled. Relationships and transient targets are keyed by
|
|
// live ID, so reuse can make dead friends/enemies leak onto new individuals.
|
|
this.liveIdFree = [];
|
|
if (!Number.isFinite(this.liveIdNext)) this.liveIdNext = 1;
|
|
if (!Number.isFinite(this.liveIdSerial)) this.liveIdSerial = 0;
|
|
const id = `t${this.liveIdNext++}`;
|
|
t.id = id;
|
|
t.liveToken = ++this.liveIdSerial;
|
|
this.liveTarinai.set(id, { token: t.liveToken, target: t });
|
|
return id;
|
|
},
|
|
|
|
releaseTarinaiLiveId(t) {
|
|
if (!t?.id || !this.liveTarinai) return false;
|
|
const current = this.liveTarinai.get(t.id);
|
|
if (!current || current.target !== t || current.token !== t.liveToken) return false;
|
|
const id = t.id;
|
|
this.liveTarinai.delete(id);
|
|
// Do not recycle live IDs. Relationships are keyed by live ID; recycling can
|
|
// make a dead friend's/enemy's relationship appear on a newly spawned individual.
|
|
t.releasedLiveId = id;
|
|
t.id = "";
|
|
t.liveToken = 0;
|
|
return true;
|
|
},
|
|
|
|
liveTarinaiById(id, token = null) {
|
|
if (!id || !this.liveTarinai) return null;
|
|
const entry = this.liveTarinai.get(id);
|
|
if (!entry || (token !== null && entry.token !== token)) return null;
|
|
const t = entry.target;
|
|
return t && !t.dead ? t : null;
|
|
},
|
|
|
|
recordFamily(t) {
|
|
if (!this.family) this.family = {};
|
|
const familyKey = this.tarinaiFamilyKey(t);
|
|
if (!familyKey) return;
|
|
const uniqueIds = (list) => Array.from(new Set((list || []).filter(Boolean)));
|
|
const sameList = (a, b) => {
|
|
const aa = uniqueIds(a);
|
|
const bb = uniqueIds(b);
|
|
return aa.length === bb.length && aa.every((id, i) => id === bb[i]);
|
|
};
|
|
let changed = false;
|
|
const nextParents = uniqueIds(t.parents || []);
|
|
const nextChildren = uniqueIds(t.children || []);
|
|
const nextPersonalityTags = getPersonalityTraitTags(t);
|
|
const nextScale = Number.isFinite(t.scale) ? t.scale : (Number.isFinite(t.adultScale) ? t.adultScale : 0.28);
|
|
const nextAdultScale = Number.isFinite(t.adultScale) ? t.adultScale : nextScale;
|
|
const nextGrowth = Number.isFinite(t.growth) ? t.growth : 1;
|
|
if (!this.family[familyKey]) {
|
|
this.family[familyKey] = {
|
|
id: familyKey,
|
|
name: t.name,
|
|
type: t.type,
|
|
personalityTags: nextPersonalityTags,
|
|
generation: t.generation,
|
|
hasPaired: !!t.hasPaired,
|
|
parents: nextParents,
|
|
parentNames: [...(t.parentNames || [])],
|
|
children: nextChildren,
|
|
birthTime: t.birthTime,
|
|
scale: nextScale,
|
|
adultScale: nextAdultScale,
|
|
growth: nextGrowth,
|
|
alive: !t.dead,
|
|
deathReason: t.deathReason || "",
|
|
};
|
|
changed = true;
|
|
} else {
|
|
const entry = this.family[familyKey];
|
|
const nextAlive = !t.dead;
|
|
if (entry.name !== t.name) changed = true;
|
|
if (entry.type !== t.type) changed = true;
|
|
if (!sameList(entry.personalityTags, nextPersonalityTags)) changed = true;
|
|
if (entry.hasPaired !== !!t.hasPaired) changed = true;
|
|
if (entry.generation !== t.generation) changed = true;
|
|
if (Math.abs((Number(entry.scale) || 0) - nextScale) > 0.002) changed = true;
|
|
if (Math.abs((Number(entry.adultScale) || 0) - nextAdultScale) > 0.002) changed = true;
|
|
if (Math.abs((Number(entry.growth) || 0) - nextGrowth) > 0.02) changed = true;
|
|
if ((entry.deathReason || "") !== (t.deathReason || entry.deathReason || "")) changed = true;
|
|
if (entry.alive !== nextAlive) changed = true;
|
|
if (!sameList(entry.parents, nextParents)) changed = true;
|
|
if (!sameList(entry.children, nextChildren)) changed = true;
|
|
entry.name = t.name;
|
|
entry.type = t.type;
|
|
delete entry.personality;
|
|
entry.personalityTags = nextPersonalityTags;
|
|
entry.alive = nextAlive;
|
|
entry.generation = t.generation;
|
|
entry.scale = nextScale;
|
|
entry.adultScale = nextAdultScale;
|
|
entry.growth = nextGrowth;
|
|
entry.hasPaired = !!t.hasPaired;
|
|
entry.deathReason = t.deathReason || entry.deathReason || "";
|
|
entry.parents = nextParents;
|
|
entry.parentNames = [...(t.parentNames || entry.parentNames || [])];
|
|
entry.children = nextChildren;
|
|
}
|
|
if (changed) {
|
|
this.familyVersion = (this.familyVersion || 0) + 1;
|
|
this.markFamilyTreeDirty("family-record");
|
|
}
|
|
},
|
|
|
|
markFamilyTreeDirty(reason = "family") {
|
|
this.familyTreeDirty = true;
|
|
this.familyTreeDirtyReason = reason;
|
|
this.familyTreeDirtyAt = this.time || 0;
|
|
},
|
|
|
|
normalizeFamily() {
|
|
if (!this.family) this.family = {};
|
|
if (this.familyCleanVersion === (this.familyVersion || 0)) return false;
|
|
const family = this.family;
|
|
const liveById = new Map((this.tarinai || []).map(t => [this.tarinaiFamilyKey(t), t]).filter(([id]) => id));
|
|
let changed = false;
|
|
const uniqueIds = (list, allow) => Array.from(new Set((list || []).filter(id => id && (!allow || allow.has(id)))));
|
|
const sameList = (a, b) => a.length === b.length && a.every((id, i) => id === b[i]);
|
|
for (const t of this.tarinai || []) {
|
|
const familyKey = this.tarinaiFamilyKey(t);
|
|
if (!familyKey || (!this.family[familyKey] && !(t.hasPaired || (t.parents || []).length || (t.children || []).length))) continue;
|
|
const before = this.familyVersion || 0;
|
|
this.recordFamily(t);
|
|
changed = changed || before !== (this.familyVersion || 0);
|
|
}
|
|
const known = new Set(Object.keys(family));
|
|
for (const id of Object.keys(family)) {
|
|
const n = family[id] || {};
|
|
const live = liveById.get(id);
|
|
const parents = uniqueIds(n.parents, known);
|
|
const children = uniqueIds(n.children, known);
|
|
if (live) {
|
|
const liveParents = uniqueIds(live.parents, known);
|
|
const liveChildren = uniqueIds(live.children, known);
|
|
for (const p of liveParents) if (!parents.includes(p)) parents.push(p);
|
|
for (const c of liveChildren) if (!children.includes(c)) children.push(c);
|
|
}
|
|
const next = {
|
|
id,
|
|
name: live?.name || n.name || id,
|
|
type: live?.type || n.type || "smile",
|
|
personalityTags: live ? getPersonalityTraitTags(live) : [...(n.personalityTags || [])],
|
|
generation: Math.max(1, live?.generation || n.generation || 1),
|
|
hasPaired: !!(live?.hasPaired || n.hasPaired),
|
|
parents,
|
|
parentNames: live?.parentNames ? [...live.parentNames] : [...(n.parentNames || [])],
|
|
children,
|
|
birthTime: live?.birthTime ?? n.birthTime ?? 0,
|
|
scale: Number.isFinite(live?.scale) ? live.scale : (Number.isFinite(n.scale) ? n.scale : (Number.isFinite(live?.adultScale) ? live.adultScale : (Number.isFinite(n.adultScale) ? n.adultScale : 0.28))),
|
|
adultScale: Number.isFinite(live?.adultScale) ? live.adultScale : (Number.isFinite(n.adultScale) ? n.adultScale : (Number.isFinite(live?.scale) ? live.scale : (Number.isFinite(n.scale) ? n.scale : 0.28))),
|
|
growth: Number.isFinite(live?.growth) ? live.growth : (Number.isFinite(n.growth) ? n.growth : 1),
|
|
alive: live ? !live.dead : n.alive !== false,
|
|
deathReason: live?.deathReason || n.deathReason || "",
|
|
};
|
|
for (const key of Object.keys(next)) {
|
|
const a = family[id]?.[key];
|
|
const b = next[key];
|
|
const equal = Array.isArray(a) && Array.isArray(b) ? sameList(a, b) : a === b;
|
|
if (!equal) changed = true;
|
|
}
|
|
family[id] = next;
|
|
}
|
|
for (const child of Object.values(family)) {
|
|
for (const parentId of child.parents || []) {
|
|
const parent = family[parentId];
|
|
if (!parent) continue;
|
|
if (!parent.children.includes(child.id)) {
|
|
parent.children.push(child.id);
|
|
changed = true;
|
|
}
|
|
}
|
|
}
|
|
for (const parent of Object.values(family)) {
|
|
const nextChildren = uniqueIds(parent.children, known).filter(childId => family[childId]?.parents?.includes(parent.id));
|
|
if (!sameList(parent.children || [], nextChildren)) {
|
|
parent.children = nextChildren;
|
|
changed = true;
|
|
}
|
|
}
|
|
for (const id of Object.keys(family)) {
|
|
const n = family[id];
|
|
if (!n?.hasPaired && !(n?.parents || []).length && !(n?.children || []).length) {
|
|
delete family[id];
|
|
changed = true;
|
|
}
|
|
}
|
|
for (const [id, live] of liveById.entries()) {
|
|
const n = family[id];
|
|
if (!n) continue;
|
|
if (!sameList(live.parents || [], n.parents || [])) {
|
|
live.parents = [...(n.parents || [])];
|
|
changed = true;
|
|
}
|
|
if (!sameList(live.children || [], n.children || [])) {
|
|
live.children = [...(n.children || [])];
|
|
changed = true;
|
|
}
|
|
if (live.hasPaired !== !!n.hasPaired) {
|
|
live.hasPaired = !!n.hasPaired;
|
|
changed = true;
|
|
}
|
|
if (live.generation !== n.generation) {
|
|
live.generation = n.generation;
|
|
changed = true;
|
|
}
|
|
}
|
|
if (changed) {
|
|
this.familyVersion = (this.familyVersion || 0) + 1;
|
|
this.markFamilyTreeDirty("family-normalize");
|
|
}
|
|
this.familyCleanVersion = this.familyVersion || 0;
|
|
return changed;
|
|
},
|
|
|
|
resetFamilyTree() {
|
|
this.family = {};
|
|
this.familyCleanVersion = 0;
|
|
this.familyVersion = (this.familyVersion || 0) + 1;
|
|
this.markFamilyTreeDirty("family-reset");
|
|
this.maxGeneration = 1;
|
|
for (const t of this.tarinai || []) {
|
|
t.parents = [];
|
|
t.parentNames = [];
|
|
t.children = [];
|
|
t.generation = 1;
|
|
t.hasPaired = false;
|
|
}
|
|
},
|
|
|
|
validateFamily() {
|
|
this.normalizeFamily();
|
|
const family = this.family || {};
|
|
const issues = [];
|
|
const seenRenderIds = new Set();
|
|
for (const n of Object.values(family)) {
|
|
if (!n?.id) continue;
|
|
const childSeen = new Set();
|
|
for (const p of n.parents || []) {
|
|
if (!family[p]) issues.push({ type: "missing-parent", id: n.id, parent: p });
|
|
else if (!family[p].children?.includes(n.id)) issues.push({ type: "missing-reciprocal-child", id: n.id, parent: p });
|
|
}
|
|
for (const c of n.children || []) {
|
|
if (childSeen.has(c)) issues.push({ type: "duplicate-child", id: n.id, child: c });
|
|
childSeen.add(c);
|
|
if (!family[c]) issues.push({ type: "missing-child", id: n.id, child: c });
|
|
else if (!family[c].parents?.includes(n.id)) issues.push({ type: "missing-reciprocal-parent", id: n.id, child: c });
|
|
}
|
|
if (seenRenderIds.has(n.id)) issues.push({ type: "duplicate-render-node", id: n.id });
|
|
seenRenderIds.add(n.id);
|
|
}
|
|
return issues;
|
|
},
|
|
|
|
clearLiveReferencesTo(dead) {
|
|
const deadId = dead?.id || dead?.releasedLiveId || "";
|
|
if (!deadId) return false;
|
|
let changed = false;
|
|
for (const other of this.tarinai || []) {
|
|
if (!other || other === dead || other.dead) continue;
|
|
if (other.target === dead || other.target?.id === deadId) {
|
|
other.target = null;
|
|
if (["seek_friend", "panic", "fight", "intimidate"].includes(other.state)) other.goIdle?.("\u53c2\u7167\u3057\u3066\u3044\u305f\u76f8\u624b\u304c\u3044\u306a\u304f\u306a\u3063\u305f");
|
|
changed = true;
|
|
}
|
|
if (other.panicTarget === dead || other.panicTarget?.id === deadId) { other.panicTarget = null; changed = true; }
|
|
if (other.fightTargetId === deadId) { other.fightTargetId = null; changed = true; }
|
|
if (Array.isArray(other.fightTargetIds) && other.fightTargetIds.includes(deadId)) {
|
|
other.fightTargetIds = other.fightTargetIds.filter(id => id !== deadId);
|
|
changed = true;
|
|
}
|
|
for (const key of ["defeatedById", "fightWinnerId", "intimidateTargetId", "birthPartnerId"]) {
|
|
if (other[key] === deadId) { other[key] = null; changed = true; }
|
|
}
|
|
}
|
|
return changed;
|
|
},
|
|
|
|
markDead(t, reason) {
|
|
this.clearLiveReferencesTo(t);
|
|
if (!this.family) this.family = {};
|
|
const finalReason = t?.normalizeDeathReason ? t.normalizeDeathReason(reason) : reason;
|
|
const familyKey = this.tarinaiFamilyKey(t);
|
|
if (familyKey && this.family[familyKey]) {
|
|
if (this.family[familyKey].alive !== false || this.family[familyKey].deathReason !== finalReason) {
|
|
this.family[familyKey].alive = false;
|
|
this.family[familyKey].deathReason = finalReason;
|
|
delete this.family[familyKey].personality;
|
|
this.familyVersion = (this.familyVersion || 0) + 1;
|
|
this.markFamilyTreeDirty("death");
|
|
}
|
|
}
|
|
this.notifyDeathToRelations?.(t, finalReason);
|
|
this.familyPrunePending = true;
|
|
},
|
|
|
|
pruneExtinctFamilies(opts = {}) {
|
|
if (opts.normalize !== false) this.normalizeFamily();
|
|
const family = this.family || {};
|
|
const nodes = Object.values(family).filter(Boolean).filter(n => n.hasPaired || (n.parents || []).length || (n.children || []).length);
|
|
if (!nodes.length) return;
|
|
const components = globalThis.TarinaiFamilyGraph?.connectedComponents
|
|
? globalThis.TarinaiFamilyGraph.connectedComponents(nodes)
|
|
: [];
|
|
const remove = components.length
|
|
? components.filter(comp => !comp.some(n => family[n.id]?.alive)).flat().map(n => n.id)
|
|
: [];
|
|
if (!remove.length) return;
|
|
for (const id of remove) delete family[id];
|
|
this.familyVersion = (this.familyVersion || 0) + 1;
|
|
this.markFamilyTreeDirty("family-prune");
|
|
this.familyCleanVersion = this.familyVersion || 0;
|
|
},
|
|
|
|
addTarinai(opts = {}) {
|
|
const t = new Tarinai(this, opts);
|
|
this.assignTarinaiLiveId(t);
|
|
this.tarinai.push(t);
|
|
this.drawListDirty = true;
|
|
if (t.hasPaired || (t.parents || []).length || (t.children || []).length) this.recordFamily(t);
|
|
this.maxGeneration = Math.max(this.maxGeneration, t.generation);
|
|
this.emit?.("tarinai:born", { tarinai: t, source: opts.parents?.length ? "family" : "spawn" });
|
|
return t;
|
|
},
|
|
|
|
startBirthRitual(a, b) {
|
|
if (!a || !b || a.dead || b.dead) return false;
|
|
if (!!a.isZunchiSlave !== !!b.isZunchiSlave) return false;
|
|
if (this.areParentChild(a, b)) return false;
|
|
if (a.sleepDisease || b.sleepDisease || a.fightDisease || b.fightDisease) return false;
|
|
if (a.birthRitualTimer > 0.04 || b.birthRitualTimer > 0.04) return false;
|
|
const aJoined = a.familyJoined ? a.familyJoined() : Boolean((a.parents || []).length || (a.children || []).length);
|
|
const bJoined = b.familyJoined ? b.familyJoined() : Boolean((b.parents || []).length || (b.children || []).length);
|
|
if (aJoined && !bJoined) b.generation = a.generation;
|
|
else if (bJoined && !aJoined) a.generation = b.generation;
|
|
else if (!aJoined && !bJoined) { a.generation = 1; b.generation = 1; }
|
|
// Do not mark either parent as part of a family during the ritual.
|
|
// The archive should only gain nodes after a child has actually been created;
|
|
// otherwise childless ritual participants appear as isolated family trees.
|
|
const duration = 10.0;
|
|
a.birthRitualTimer = b.birthRitualTimer = duration;
|
|
a.birthRitualMax = b.birthRitualMax = duration;
|
|
a.birthPartnerId = b.id;
|
|
b.birthPartnerId = a.id;
|
|
a.birthRitualRole = -1;
|
|
b.birthRitualRole = 1;
|
|
a.birthRitualLeader = true;
|
|
b.birthRitualLeader = false;
|
|
a.setActionState("birth_ritual", { target: b, reason: "\u7e41\u6b96\u306e\u524d\u3076\u308c\u3092\u3057\u3066\u3044\u308b" });
|
|
b.setActionState("birth_ritual", { target: a, reason: "\u7e41\u6b96\u306e\u524d\u3076\u308c\u3092\u3057\u3066\u3044\u308b" });
|
|
if (typeof setLiveActionText === "function") {
|
|
const aBehavior = typeof currentTarinaiBehavior === "function" ? currentTarinaiBehavior(a) : a.behavior;
|
|
const bBehavior = typeof currentTarinaiBehavior === "function" ? currentTarinaiBehavior(b) : b.behavior;
|
|
const aLoveCause = (aBehavior?.source === "love_mochi" || (a.loveMochiTimer || 0) > 0.04) ? "\u3078\u3053\u9905\u306e\u52b9\u679c" : `${b.name || "\u76f8\u624b"}\u304c\u6c17\u306b\u306a\u308b`;
|
|
const bLoveCause = (bBehavior?.source === "love_mochi" || (b.loveMochiTimer || 0) > 0.04) ? "\u3078\u3053\u9905\u306e\u52b9\u679c" : `${a.name || "\u76f8\u624b"}\u304c\u6c17\u306b\u306a\u308b`;
|
|
const aText = typeof buildReasonText === "function" ? buildReasonText("social", aBehavior?.tiedNeeds || ["social"], { id: "birth_ritual", need: "social", subNeed: "mate", label: `${b.name || "\u76f8\u624b"}\u3068\u7e41\u6b96\u306e\u524d\u3076\u308c\u3092\u3057\u3066\u3044\u308b` }, a, this, { causeText: aLoveCause }) : `${b.name || "\u76f8\u624b"}\u3068\u7e41\u6b96\u306e\u524d\u3076\u308c\u3092\u3057\u3066\u3044\u308b`;
|
|
const bText = typeof buildReasonText === "function" ? buildReasonText("social", bBehavior?.tiedNeeds || ["social"], { id: "birth_ritual", need: "social", subNeed: "mate", label: `${a.name || "\u76f8\u624b"}\u3068\u7e41\u6b96\u306e\u524d\u3076\u308c\u3092\u3057\u3066\u3044\u308b` }, b, this, { causeText: bLoveCause }) : `${a.name || "\u76f8\u624b"}\u3068\u7e41\u6b96\u306e\u524d\u3076\u308c\u3092\u3057\u3066\u3044\u308b`;
|
|
setLiveActionText(a, { need: "social", subNeed: "mate", actionId: "birth_ritual", actionLabel: `${b.name || "\u76f8\u624b"}\u3068\u7e41\u6b96\u306e\u524d\u3076\u308c\u3092\u3057\u3066\u3044\u308b`, reasonText: aText, causeText: aLoveCause, target: b });
|
|
setLiveActionText(b, { need: "social", subNeed: "mate", actionId: "birth_ritual", actionLabel: `${a.name || "\u76f8\u624b"}\u3068\u7e41\u6b96\u306e\u524d\u3076\u308c\u3092\u3057\u3066\u3044\u308b`, reasonText: bText, causeText: bLoveCause, target: a });
|
|
}
|
|
a.reproductionTimer = CONFIG.reproductionCooldown + rand(2, 10);
|
|
b.reproductionTimer = CONFIG.reproductionCooldown + rand(2, 10);
|
|
a.surpriseTimer = Math.max(a.surpriseTimer, 0.35);
|
|
b.surpriseTimer = Math.max(b.surpriseTimer, 0.35);
|
|
return true;
|
|
},
|
|
|
|
finishBirthRitual(a, b) {
|
|
if (!a || !b || a.dead || b.dead) return null;
|
|
const reproductionBlockedByDisease = a.sleepDisease || b.sleepDisease || a.fightDisease || b.fightDisease;
|
|
a.birthRitualTimer = b.birthRitualTimer = 0;
|
|
a.birthPartnerId = b.birthPartnerId = null;
|
|
a.birthRitualLeader = b.birthRitualLeader = false;
|
|
a.goIdle("\u8a95\u751f\u306e\u524d\u3076\u308c\u304c\u7d42\u308f\u3063\u305f");
|
|
b.goIdle("\u8a95\u751f\u306e\u524d\u3076\u308c\u304c\u7d42\u308f\u3063\u305f");
|
|
if (reproductionBlockedByDisease || (!!a.isZunchiSlave !== !!b.isZunchiSlave)) {
|
|
a.birthRitualRole = b.birthRitualRole = 0;
|
|
return null;
|
|
}
|
|
a.fightTimer = b.fightTimer = 0;
|
|
a.fightTargetIds = []; b.fightTargetIds = [];
|
|
a.fightTargetId = b.fightTargetId = null;
|
|
a.defeatedById = b.defeatedById = null;
|
|
a.postBirthPeaceTimer = b.postBirthPeaceTimer = 18;
|
|
a.fightCooldown = b.fightCooldown = Math.max(CONFIG.fightCooldown + 8, a.fightCooldown || 0, b.fightCooldown || 0);
|
|
const children = [];
|
|
const countRoll = Math.random();
|
|
const childCount = countRoll < 0.08 ? 3 : (countRoll < 0.36 ? 2 : 1);
|
|
for (let i = 0; i < childCount; i++) {
|
|
const child = this.spawnChild(a, b, i, childCount);
|
|
if (child) { child.postBirthPeaceTimer = 18; children.push(child); }
|
|
}
|
|
if (children.length > 0) this.lastBirthAt = this.time;
|
|
if (children.length > 1) this.log(`${a.name}\u3068${b.name}\u306e\u5b50\u304c${children.length}\u5339\u751f\u307e\u308c\u305f\u3002`, "birth", { participants: [a, b, ...children] });
|
|
return children[0] || null;
|
|
},
|
|
|
|
areParentChild(a, b) {
|
|
if (!a || !b) return false;
|
|
const aId = this.tarinaiFamilyKey(a) || a;
|
|
const bId = this.tarinaiFamilyKey(b) || b;
|
|
if (!aId || !bId) return false;
|
|
return Boolean((a.parents || []).includes(bId) || (a.children || []).includes(bId) || (b.parents || []).includes(aId) || (b.children || []).includes(aId));
|
|
},
|
|
|
|
areCoParents(a, b) {
|
|
if (!a || !b) return false;
|
|
const aLiveId = a.id || a;
|
|
const bLiveId = b.id || b;
|
|
if (!aLiveId || !bLiveId || aLiveId === bLiveId) return false;
|
|
if (a.birthPartnerId && a.birthPartnerId === bLiveId) return true;
|
|
if (b.birthPartnerId && b.birthPartnerId === aLiveId) return true;
|
|
const aId = this.tarinaiFamilyKey(a) || aLiveId;
|
|
const bId = this.tarinaiFamilyKey(b) || bLiveId;
|
|
if (!aId || !bId || aId === bId) return false;
|
|
const aChildren = new Set((a.children || []).filter(Boolean));
|
|
if (!aChildren.size) return false;
|
|
for (const id of (b.children || [])) {
|
|
if (aChildren.has(id)) return true;
|
|
}
|
|
return false;
|
|
},
|
|
|
|
canStartFight(t) {
|
|
if (!t || t.dead || typeof t.relationTo !== "function") return false;
|
|
if (t.sleepDisease) return false;
|
|
// Disease fight rule: explosion, zunchi, and fight disease can lash out;
|
|
// sleep disease cannot fight or be challenged, and other diseases cannot start fights.
|
|
if (t.explosionDisease || t.zunchiDisease || t.fightDisease) return true;
|
|
const flag = (value) => typeof value === "function" ? Boolean(value.call(t)) : Boolean(value);
|
|
const hasOtherDisease = flag(t.disease) || flag(t.sick) || flag(t.infection) || flag(t.hasDisease);
|
|
if (hasOtherDisease) return false;
|
|
return true;
|
|
},
|
|
|
|
canBeFightTarget(t) {
|
|
return Boolean(t && !t.dead && typeof t.relationTo === "function" && !t.sleepDisease);
|
|
},
|
|
|
|
canFightPair(actor, target) {
|
|
return this.canStartFight(actor) && this.canBeFightTarget(target);
|
|
},
|
|
|
|
spawnChild(a, b, index = 0, total = 1) {
|
|
const zunchiSlaveChild = !!(a.isZunchiSlave && b.isZunchiSlave);
|
|
const inherited = Math.random() < 0.56 ? a.type : b.type;
|
|
const mutation = zunchiSlaveChild ? "zunchi_slave" : (Math.random() < 0.18 ? baseSpriteId() : inherited);
|
|
const childSeed = crypto.randomUUID ? crypto.randomUUID() : `${Date.now()}-${Math.random()}-${index}`;
|
|
const childGenetics = inheritedGenetics(a, b, childSeed);
|
|
const childAdultScale = adultScaleFromGenetics(childSeed, childGenetics);
|
|
const childBirthPersonality = zunchiSlaveChild ? zunchiSlavePersonalityValue() : personalityFromParents(a, b);
|
|
const child = this.addTarinai({
|
|
familyKey: childSeed,
|
|
type: mutation,
|
|
birthPersonality: childBirthPersonality,
|
|
currentPersonality: childBirthPersonality,
|
|
genetics: childGenetics,
|
|
adultScale: childAdultScale,
|
|
name: zunchiSlaveChild ? "\u305a\u3093\u3061\u3069\u308c\u3044" : makeName(),
|
|
isZunchiSlave: zunchiSlaveChild,
|
|
age: 0,
|
|
x: (a.x + b.x) / 2 + rand(-14, 14) + (index - (total - 1) / 2) * 18,
|
|
y: (a.y + b.y) / 2 + rand(-14, 14) + (total > 1 ? rand(-10, 10) : 0),
|
|
scale: scaleForGrowth(childAdultScale, 0),
|
|
hunger: rand(20, 40),
|
|
loneliness: rand(30, 56),
|
|
energy: rand(55, 95),
|
|
stress: rand(0, 14),
|
|
need: "",
|
|
generation: Math.max(a.generation, b.generation) + 1,
|
|
lifeSpan: rand(1500, 2200) * (childGenetics.lifeSpanMul || 1),
|
|
parents: [this.tarinaiFamilyKey(a), this.tarinaiFamilyKey(b)],
|
|
parentNames: [a.name, b.name],
|
|
birthTime: this.time,
|
|
powerItemMode: "",
|
|
sizeItemMode: "",
|
|
lifeItemMode: "",
|
|
});
|
|
if (zunchiSlaveChild) child.becomeZunchiSlave?.({ birth: true });
|
|
this.maxGeneration = Math.max(this.maxGeneration, child.generation);
|
|
a.hasPaired = true;
|
|
b.hasPaired = true;
|
|
a.affection = 0; b.affection = 0;
|
|
a.loneliness *= 0.65; b.loneliness *= 0.65;
|
|
a.adjustRelation?.(b, 0.65, -0.20, "co_parent");
|
|
b.adjustRelation?.(a, 0.65, -0.20, "co_parent");
|
|
a.postBirthPeaceTimer = Math.max(a.postBirthPeaceTimer || 0, 28);
|
|
b.postBirthPeaceTimer = Math.max(b.postBirthPeaceTimer || 0, 28);
|
|
const childKey = this.tarinaiFamilyKey(child);
|
|
const aKey = this.tarinaiFamilyKey(a);
|
|
const bKey = this.tarinaiFamilyKey(b);
|
|
a.children.push(childKey);
|
|
b.children.push(childKey);
|
|
this.recordFamily(a);
|
|
this.recordFamily(b);
|
|
this.recordFamily(child);
|
|
if (this.family[aKey] && !this.family[aKey].children.includes(childKey)) this.family[aKey].children.push(childKey);
|
|
if (this.family[bKey] && !this.family[bKey].children.includes(childKey)) this.family[bKey].children.push(childKey);
|
|
this.familyCleanVersion = null;
|
|
audio.birth();
|
|
this.lastBirthAt = this.time;
|
|
if (total <= 1) this.log(`${child.name}\u304c${a.name}\u3068${b.name}\u306e\u5b50\u3068\u3057\u3066\u751f\u307e\u308c\u305f\u3002`, "birth", { participants: [child, a, b] });
|
|
return child;
|
|
},
|
|
|
|
fightPairKey(a, b) {
|
|
const aid = a?.id || a?.familyKey || "";
|
|
const bid = b?.id || b?.familyKey || "";
|
|
if (!aid || !bid) return "";
|
|
return String(aid) < String(bid) ? `${aid}:${bid}` : `${bid}:${aid}`;
|
|
},
|
|
|
|
fightPairCooldownRemaining(a, b) {
|
|
const key = this.fightPairKey?.(a, b);
|
|
if (!key) return 0;
|
|
const until = Number(this.fightPairCooldowns?.[key] || 0) || 0;
|
|
return Math.max(0, until - (this.time || 0));
|
|
},
|
|
|
|
markFightPairCooldown(a, b, seconds = 3.2) {
|
|
const key = this.fightPairKey?.(a, b);
|
|
if (!key) return;
|
|
this.fightPairCooldowns = this.fightPairCooldowns || {};
|
|
this.fightPairCooldowns[key] = Math.max(Number(this.fightPairCooldowns[key] || 0) || 0, (this.time || 0) + Math.max(0.2, Number(seconds) || 3.2));
|
|
},
|
|
|
|
isAlreadyFightingPair(a, b) {
|
|
if (!a || !b) return false;
|
|
return a.fightTimer > 0.04 && b.fightTimer > 0.04 && ((a.fightTargetIds || []).includes(b.id) || a.fightTargetId === b.id) && ((b.fightTargetIds || []).includes(a.id) || b.fightTargetId === a.id);
|
|
},
|
|
|
|
startForcedFight(a, b) {
|
|
if (!a || !b || a === b || a.dead || b.dead) return false;
|
|
if (!this.canFightPair(a, b)) return false;
|
|
const alreadyFighting = this.isAlreadyFightingPair?.(a, b);
|
|
if (alreadyFighting) return true;
|
|
if ((a.fightTimer || 0) > 0.04 || (b.fightTimer || 0) > 0.04) return false;
|
|
if (this.fightPairCooldownRemaining?.(a, b) > 0.04) return false;
|
|
for (const t of [a, b]) {
|
|
t.birthRitualTimer = 0;
|
|
t.birthRitualMax = 0;
|
|
t.birthPartnerId = null;
|
|
t.birthRitualLeader = false;
|
|
t.birthRitualRole = 0;
|
|
t.postBirthPeaceTimer = 0;
|
|
t.fightCooldown = 0;
|
|
t.intimidateTimer = 0;
|
|
t.intimidateTargetId = null;
|
|
t.intimidatedTimer = 0;
|
|
t.defeatedTimer = 0;
|
|
t.defeatedById = null;
|
|
t.fightWinnerId = null;
|
|
t.goIdle("");
|
|
}
|
|
this.startFightMochiIntimidation(a, b);
|
|
this.startFight(a, b, { forced: true, initiator: a });
|
|
return true;
|
|
},
|
|
|
|
startFightMochiIntimidation(a, b) {
|
|
if (!a || !b || a.dead || b.dead) return false;
|
|
const aDrug = (a.fightMochiTimer || 0) > 0.04;
|
|
const bDrug = (b.fightMochiTimer || 0) > 0.04;
|
|
const actor = aDrug && !bDrug ? a : (!aDrug && bDrug ? b : (a.energy + a.stress * 0.36 >= b.energy + b.stress * 0.36 ? a : b));
|
|
const target = actor === a ? b : a;
|
|
if (!this.canFightPair(actor, target)) return false;
|
|
actor.intimidateTimer = Math.max(actor.intimidateTimer || 0, rand(0.85, 1.35));
|
|
actor.intimidateTargetId = target.id;
|
|
actor.surpriseTimer = Math.max(actor.surpriseTimer || 0, 0.28);
|
|
target.intimidatedTimer = Math.max(target.intimidatedTimer || 0, rand(0.75, 1.25));
|
|
target.fearTimer = Math.max(target.fearTimer || 0, 0.72 * target.personalityProfile().fear);
|
|
actor.adjustRelation(target, -0.05, 0.06, "intimidate");
|
|
target.adjustRelation(actor, -0.12, 0.30 * target.personalityProfile().fear, "intimidate");
|
|
this.effects.push(new Effect("ring", actor.x, actor.y - actor.radius * 0.65, {
|
|
life: 0.34,
|
|
size: actor.radius * 0.62,
|
|
color: "rgba(154, 88, 42, 0.74)",
|
|
}));
|
|
this.spawnBubble(actor.x, actor.y - actor.radius * 1.30, "!", "rgba(120,62,38,0.82)");
|
|
return true;
|
|
},
|
|
|
|
startConflict(a, b) {
|
|
if (!a || !b || a.dead || b.dead) return false;
|
|
if (typeof a.relationTo !== "function" || typeof b.relationTo !== "function") return false;
|
|
if ((a.fightMochiTimer || 0) > 0.04 || (b.fightMochiTimer || 0) > 0.04) return this.startForcedFight(a, b);
|
|
if (this.areParentChild(a, b) || this.areCoParents?.(a, b)) return false;
|
|
if ((a.postBirthPeaceTimer || 0) > 0 || (b.postBirthPeaceTimer || 0) > 0) return false;
|
|
if (this.fightPairCooldownRemaining?.(a, b) > 0.04) return false;
|
|
if (a.fightTimer > 0.04 || b.fightTimer > 0.04 || a.intimidateTimer > 0.04 || b.intimidateTimer > 0.04) return false;
|
|
const aAggressiveIntent = a.shouldApplyPersonalityBehavior?.("aggression", 1) ? 1 : 0;
|
|
const bAggressiveIntent = b.shouldApplyPersonalityBehavior?.("aggression", 1) ? 1 : 0;
|
|
const scoreA = a.energy * 0.38 + a.mood * 0.18 + a.stress * 0.12 + (a.type === "angry" ? 12 : 0) + aAggressiveIntent * 8 + ((typeof b.relationTo === "function" ? (b.relationTo(a.id).fear || 0) : 0) * 0.65) + (a.isZunchiSlave ? -10 : 0) + ((a.fightMochiTimer || 0) > 0.04 ? 9 : 0) + rand(-5, 5);
|
|
const scoreB = b.energy * 0.38 + b.mood * 0.18 + b.stress * 0.12 + (b.type === "angry" ? 12 : 0) + bAggressiveIntent * 8 + ((typeof a.relationTo === "function" ? (a.relationTo(b.id).fear || 0) : 0) * 0.65) + (b.isZunchiSlave ? -10 : 0) + ((b.fightMochiTimer || 0) > 0.04 ? 9 : 0) + rand(-5, 5);
|
|
const actor = scoreA >= scoreB ? a : b;
|
|
const target = actor === a ? b : a;
|
|
if (!this.canFightPair(actor, target)) return false;
|
|
const actorScore = actor === a ? scoreA : scoreB;
|
|
const targetScore = actor === a ? scoreB : scoreA;
|
|
const canIntimidate = !actor.lowHealthSprite || !actor.lowHealthSprite();
|
|
const battleDrug = (actor.fightMochiTimer || 0) > 0.04 || (target.fightMochiTimer || 0) > 0.04;
|
|
const intimidationChance = clamp(0.40 + (battleDrug ? 0.24 : 0) + (target.isZunchiSlave ? 0.18 : 0), 0.24, 0.90);
|
|
if (canIntimidate && Math.random() < intimidationChance) {
|
|
return this.startIntimidation(actor, target, actorScore, targetScore);
|
|
}
|
|
return this.startFight(actor, target, { initiator: actor });
|
|
},
|
|
|
|
startIntimidation(actor, target, actorScore = 0, targetScore = 0) {
|
|
if (!actor || !target || actor.dead || target.dead) return false;
|
|
if ((actor.fightTimer || 0) > 0.04 || (target.fightTimer || 0) > 0.04) return false;
|
|
if ((actor.intimidateTimer || 0) > 0.04 || (target.intimidateTimer || 0) > 0.04) return false;
|
|
if (this.fightPairCooldownRemaining?.(actor, target) > 0.04) return false;
|
|
if (actor.lowHealthSprite && actor.lowHealthSprite()) return false;
|
|
const fearLoad = ((typeof target.relationTo === "function" ? (target.relationTo(actor.id).fear || 0) : 0) * 0.72) + target.personalityProfile().fear * 8;
|
|
const actorAggression = Math.max(0, actor.currentPersonality?.aggression || 0);
|
|
const targetAggression = Math.max(0, target.currentPersonality?.aggression || 0);
|
|
const actorSize = actor.effectiveScale ? actor.effectiveScale() : (actor.scale || 0.28);
|
|
const targetSize = target.effectiveScale ? target.effectiveScale() : (target.scale || 0.28);
|
|
const sizeEdge = clamp((actorSize - targetSize) / 0.16, -1, 1);
|
|
const aggressionEdge = clamp(actorAggression - targetAggression * 0.55, -1, 1);
|
|
const resistance = target.energy * 0.30 + target.mood * 0.16 + targetAggression * 8 + (target.shouldApplyPersonalityBehavior?.("aggression", 1) ? 5 : 0) + rand(-5, 5);
|
|
const successChance = clamp(0.56 + (actorScore - targetScore + fearLoad - resistance) / 82 + aggressionEdge * 0.13 + sizeEdge * 0.16, 0.30, 0.96);
|
|
actor.surpriseTimer = Math.max(actor.surpriseTimer, 0.20);
|
|
target.surpriseTimer = Math.max(target.surpriseTimer, 0.32);
|
|
actor.fightCooldown = CONFIG.fightCooldown + rand(1, 4);
|
|
target.fightCooldown = CONFIG.fightCooldown + rand(1, 4);
|
|
actor.intimidateTimer = Math.max(actor.intimidateTimer, rand(1.05, 1.65));
|
|
actor.intimidateTargetId = target.id;
|
|
actor.setActionState("intimidate", { target, reason: "\u76f8\u624b\u3092\u5a01\u5687\u3057\u3066\u3044\u308b" });
|
|
if (typeof setLiveActionText === "function") setLiveActionText(actor, { need: "social", subNeed: "conflict", actionId: "intimidate_enemy", actionLabel: "\u5a01\u5687\u3057\u3066\u3044\u308b", reasonText: `${target.name || "\u76f8\u624b"}\u3092\u5a01\u5687\u3057\u3066\u3044\u308b`, target, phase: "perform", source: "behavior" });
|
|
this.effects.push(new Effect("ring", actor.x, actor.y - actor.radius * 0.65, {
|
|
life: 0.42,
|
|
size: actor.radius * 0.55,
|
|
color: "rgba(145, 106, 55, 0.70)",
|
|
}));
|
|
this.spawnBubble(actor.x, actor.y - actor.radius * 1.30, "\u306f\u3046\u30fc\uff01", "rgba(92,62,34,0.82)");
|
|
target.intimidatedTimer = Math.max(target.intimidatedTimer || 0, rand(0.95, 1.55));
|
|
target.fearTimer = Math.max(target.fearTimer, 0.65 * target.personalityProfile().fear);
|
|
if (Math.random() >= successChance) {
|
|
actor.adjustPersonality?.("aggression", -0.018, "after failed intimidation");
|
|
target.adjustRelation(actor, -0.08, 0.22 * target.personalityProfile().fear, "intimidate");
|
|
actor.intimidateTimer = 0;
|
|
actor.intimidateTargetId = null;
|
|
target.intimidatedTimer = 0;
|
|
if (actor.state === "intimidate") actor.goIdle("\u5a01\u5687\u306b\u5931\u6557\u3057\u305f");
|
|
if (target.state === "panic") target.goIdle("\u5a01\u5687\u304b\u3089\u623b\u3063\u305f");
|
|
actor.spriteLockUntil = 0;
|
|
target.spriteLockUntil = 0;
|
|
this.startFight(actor, target, { initiator: actor });
|
|
return true;
|
|
}
|
|
actor.adjustPersonality?.("aggression", 0.018, "after successful intimidation");
|
|
target.defeatedById = actor.id;
|
|
target.fightWinnerId = actor.id;
|
|
target.defeatedTimer = Math.max(target.defeatedTimer, rand(1.8, 2.8));
|
|
target.intimidatedTimer = Math.max(target.intimidatedTimer || 0, rand(1.2, 2.0));
|
|
if (target.enterPanic) target.enterPanic({ threat: actor, target: actor, reason: "\u5a01\u5687\u3055\u308c\u3066\u9003\u3052\u3066\u3044\u308b", fear: 1.35, wake: true, cause: "intimidated" });
|
|
else { target.fearTimer = Math.max(target.fearTimer, 1.35 * target.personalityProfile().fear); target.setActionState?.("panic", { target: actor, reason: "\u5a01\u5687\u3055\u308c\u3066\u9003\u3052\u3066\u3044\u308b", wake: true }); }
|
|
this.spawnBubble(target.x, target.y - target.radius * 1.28, "\u306f\u3041\u3041\u3041\uff01", "rgba(84,66,86,0.80)");
|
|
target.vx += (target.x < actor.x ? -1 : 1) * rand(26, 54);
|
|
target.vy += rand(-18, 18);
|
|
actor.adjustRelation(target, -0.08, 0.06, "intimidate");
|
|
target.adjustRelation(actor, -0.22, 0.85 * target.personalityProfile().fear, "intimidate");
|
|
if (this.time - Math.max(actor.lastLog, target.lastLog) > 5.5) {
|
|
this.log(`${actor.name}\u306f${target.name}\u3092\u5a01\u5687\u3057\u3066\u8ffd\u3044\u6255\u3063\u305f\u3002`, "fight", { participants: [actor, target] });
|
|
actor.lastLog = target.lastLog = this.time;
|
|
}
|
|
this.markFightPairCooldown?.(actor, target, 2.8);
|
|
return true;
|
|
},
|
|
|
|
startFight(a, b, opts = {}) {
|
|
if (!a || !b || a.dead || b.dead) return false;
|
|
if (!this.canFightPair(a, b) && !this.canFightPair(b, a)) return false;
|
|
if ((a.postBirthPeaceTimer || 0) > 0 || (b.postBirthPeaceTimer || 0) > 0) return false;
|
|
if (this.isAlreadyFightingPair?.(a, b)) return true;
|
|
const allowForcedStart = Boolean(opts?.forced);
|
|
if (!allowForcedStart && ((a.fightTimer || 0) > 0.04 || (b.fightTimer || 0) > 0.04 || (a.intimidateTimer || 0) > 0.04 || (b.intimidateTimer || 0) > 0.04)) return false;
|
|
if (!allowForcedStart && this.fightPairCooldownRemaining?.(a, b) > 0.04) return false;
|
|
this.markFightPairCooldown?.(a, b, 3.4);
|
|
const initiator = opts?.initiator === b ? b : a;
|
|
const receiver = initiator === a ? b : a;
|
|
const causeFor = (self, other) => {
|
|
if (((typeof currentTarinaiBehavior === "function" ? currentTarinaiBehavior(self) : self.behavior)?.source === "fight_mochi") || (self.fightMochiTimer || 0) > 0.04) return "\u3051\u3093\u304b\u9905\u306e\u52b9\u679c";
|
|
if (self === receiver && other === initiator) return `${other.name || "\u76f8\u624b"}\u306b\u3051\u3093\u304b\u3092\u58f2\u3089\u308c\u305f`;
|
|
return `${other.name || "\u76f8\u624b"}\u304c\u6c17\u306b\u5165\u3089\u306a\u3044`;
|
|
};
|
|
const textFor = (self, other) => typeof buildReasonText === "function"
|
|
? buildReasonText("social", (typeof getTarinaiBehaviorTiedNeeds === "function" ? getTarinaiBehaviorTiedNeeds(self, "social") : self.behavior?.tiedNeeds) || ["social"], { id: "fight_rival", need: "social", subNeed: "conflict", label: self === receiver ? "\u53cd\u6483\u3057\u3066\u3044\u308b" : `${other.name || "\u76f8\u624b"}\u3068\u55a7\u5629\u3057\u3066\u3044\u308b` }, self, this, { causeText: causeFor(self, other) })
|
|
: `${other.name || "\u76f8\u624b"}\u3068\u55a7\u5629\u3057\u3066\u3044\u308b`;
|
|
const reasonA = textFor(a, b);
|
|
const reasonB = textFor(b, a);
|
|
a.counterAttackFromId = a === receiver ? initiator.id : null;
|
|
b.counterAttackFromId = b === receiver ? initiator.id : null;
|
|
a.setActionState("fight", { target: b, reason: reasonA });
|
|
b.setActionState("fight", { target: a, reason: reasonB });
|
|
if (typeof setLiveActionText === "function") {
|
|
setLiveActionText(a, { need: "social", subNeed: "conflict", actionId: "fight_rival", actionLabel: a === receiver ? "\u53cd\u6483\u3057\u3066\u3044\u308b" : "\u55a7\u5629\u3057\u3066\u3044\u308b", reasonText: reasonA, causeText: causeFor(a, b), target: b, phase: "perform", source: "behavior" });
|
|
setLiveActionText(b, { need: "social", subNeed: "conflict", actionId: "fight_rival", actionLabel: b === receiver ? "\u53cd\u6483\u3057\u3066\u3044\u308b" : "\u55a7\u5629\u3057\u3066\u3044\u308b", reasonText: reasonB, causeText: causeFor(b, a), target: a, phase: "perform", source: "behavior" });
|
|
}
|
|
a.fightTimer = Math.max(a.fightTimer, rand(3.2, 4.8));
|
|
b.fightTimer = Math.max(b.fightTimer, rand(3.2, 4.8));
|
|
a.nextHeadbutt = this.time + rand(0.08, 0.18);
|
|
b.nextHeadbutt = a.nextHeadbutt;
|
|
a.fightCooldown = CONFIG.fightCooldown + rand(1, 5);
|
|
b.fightCooldown = CONFIG.fightCooldown + rand(1, 5);
|
|
if (!a.fightTargetIds) a.fightTargetIds = [];
|
|
if (!b.fightTargetIds) b.fightTargetIds = [];
|
|
if (!a.fightTargetIds.includes(b.id)) a.fightTargetIds.push(b.id);
|
|
if (!b.fightTargetIds.includes(a.id)) b.fightTargetIds.push(a.id);
|
|
a.fightTargetIds = a.fightTargetIds.slice(-4);
|
|
b.fightTargetIds = b.fightTargetIds.slice(-4);
|
|
a.fightTargetId = a.fightTargetIds[0];
|
|
b.fightTargetId = b.fightTargetIds[0];
|
|
if (a.addStress) a.addStress(rand(8, 18), { threshold: 8 });
|
|
if (b.addStress) b.addStress(rand(8, 18), { threshold: 8 });
|
|
const damageToA = b.outgoingDamage ? b.outgoingDamage(rand(2, 7)) : rand(2, 7);
|
|
const damageToB = a.outgoingDamage ? a.outgoingDamage(rand(2, 7)) : rand(2, 7);
|
|
a.damage(damageToA, "\u55a7\u5629");
|
|
b.damage(damageToB, "\u55a7\u5629");
|
|
this.maybeDefeatFromFightDamage?.(a, b, damageToA);
|
|
this.maybeDefeatFromFightDamage?.(b, a, damageToB);
|
|
const dx = b.x - a.x, dy = b.y - a.y;
|
|
const d = Math.hypot(dx, dy) || 1;
|
|
a.vx -= dx / d * rand(32, 62); a.vy -= dy / d * rand(32, 62);
|
|
b.vx += dx / d * rand(32, 62); b.vy += dy / d * rand(32, 62);
|
|
a.surpriseTimer = 0.24; b.surpriseTimer = 0.24;
|
|
this.effects.push(new Effect("fight", (a.x + b.x) / 2, (a.y + b.y) / 2 - 8, {
|
|
size: rand(12, 20),
|
|
life: rand(0.32, 0.52),
|
|
color: "rgba(116, 73, 38, 0.82)",
|
|
}));
|
|
audio.fight();
|
|
if (this.time - Math.max(a.lastLog, b.lastLog) > 6) {
|
|
this.log(`${a.name}\u3068${b.name}\u304c\u5c0f\u3055\u306a\u55a7\u5629\u3092\u3057\u305f\u3002`, "fight", { participants: [a, b] });
|
|
a.lastLog = b.lastLog = this.time;
|
|
}
|
|
return true;
|
|
}
|
|
}));
|
|
})(typeof window !== "undefined" ? window : globalThis);
|