y
This commit is contained in:
parent
61718e2981
commit
f657b6a4a4
94 changed files with 3970 additions and 1241 deletions
|
|
@ -22,6 +22,11 @@
|
|||
|
||||
const TARINAI_KING_FLEE_REASON = "\u305F\u308A\u306A\u3044\u738B\u304C\u6016\u304F\u3066\u9003\u3052\u3066\u3044\u308B\u3002";
|
||||
|
||||
const ROUTE_STATES = new Set([
|
||||
"seek_food", "seek_water", "seek_bed", "seek_material", "build", "follow_parent",
|
||||
"seek_friend", "seek_enemy", "fight", "play_ball", "play_seesaw", "ant_attack", "seek_temperature",
|
||||
]);
|
||||
|
||||
function isNestContainerItem(item) {
|
||||
return Boolean(global.TarinaiToolRuntime?.isNestContainerItem?.(item));
|
||||
}
|
||||
|
|
@ -47,7 +52,7 @@
|
|||
function nearestKingFleeNestBox(tarinai, maxDist = 460) {
|
||||
const worldRef = tarinai?.world;
|
||||
if (!tarinai || !worldRef) return null;
|
||||
const source = typeof worldRef.nearbyItems === "function" ? worldRef.nearbyItems(tarinai.x, tarinai.y, maxDist) : (worldRef.items || []);
|
||||
const source = typeof worldRef.nearbyItems === "function" ? (worldRef.nearbyNonGrassItems?.(tarinai.x, tarinai.y, maxDist) || worldRef.nearbyItems(tarinai.x, tarinai.y, maxDist)) : (worldRef.items || []);
|
||||
let best = null;
|
||||
let bestScore = Infinity;
|
||||
for (const item of source || []) {
|
||||
|
|
@ -78,6 +83,7 @@
|
|||
};
|
||||
const now = avoider.world?.time || 0;
|
||||
avoider.lastNeedShockBreaker = champion;
|
||||
avoider.lastPanicThreat = champion;
|
||||
avoider.lastPanicCause = "tarinai_champion";
|
||||
avoider.lastPanicDetail = "\u305F\u308A\u306A\u3044\u738B\u304C\u6016\u3044";
|
||||
avoider.panicStartedAt = now;
|
||||
|
|
@ -103,23 +109,125 @@
|
|||
return true;
|
||||
}
|
||||
|
||||
|
||||
const CROWD_ESCAPE_REASON = "\u4ed6\u306e\u305f\u308a\u306a\u3044\u304c\u591a\u3059\u304e\u308b\u306e\u3067\u3001\u9003\u3052\u3066\u3044\u308b\u3002";
|
||||
const LONELY_REASON = "\u5bc2\u3057\u3055\u3092\u611f\u3058\u3066\u3044\u308b\u3002";
|
||||
|
||||
function crowdThresholds(t) {
|
||||
const cur = t?.currentPersonality || {};
|
||||
const social = clamp(Number(cur.sociability || 0) || 0, -1, 1);
|
||||
const open = clamp(Number(cur.openness || 0) || 0, -1, 1);
|
||||
const social01 = (social + 1) * 0.5;
|
||||
const open01 = (open + 1) * 0.5;
|
||||
// Sociability raises preferred group size. Openness broadens the comfort band,
|
||||
// especially on the crowded side, without turning ordinary colonies into panic.
|
||||
const preferred = 4 + social01 * 6;
|
||||
const lower = Math.floor(clamp(preferred - (3 + open01 * 3), 0, 7));
|
||||
const upper = Math.ceil(clamp(preferred + 5 + open01 * 7, Math.max(8, lower + 4), 24));
|
||||
return { lower, upper };
|
||||
}
|
||||
|
||||
function nearestLonelyCompanion(t, local = [], maxDist = 640) {
|
||||
if (!t?.world) return null;
|
||||
let best = null;
|
||||
let bestD2 = Infinity;
|
||||
const consider = (o) => {
|
||||
if (!o || o === t || o.dead) return;
|
||||
const dx = o.x - t.x, dy = o.y - t.y;
|
||||
const d2 = dx * dx + dy * dy;
|
||||
if (d2 < 0.0001 || d2 > maxDist * maxDist || d2 >= bestD2) return;
|
||||
best = o;
|
||||
bestD2 = d2;
|
||||
};
|
||||
for (const entry of local || []) consider(entry?.o || entry);
|
||||
if (!best) {
|
||||
const source = typeof t.world.nearbyTarinai === "function"
|
||||
? (t.world.nearbyTarinai(t.x, t.y, maxDist, true) || [])
|
||||
: (t.world.tarinai || []);
|
||||
// This fallback runs only while lonely. Stop once a good nearby companion is
|
||||
// found instead of sorting or retaining the full wide-radius population.
|
||||
for (const o of source) consider(o);
|
||||
}
|
||||
return best;
|
||||
}
|
||||
|
||||
function findCrowdEscapeTarget(t, nearby) {
|
||||
const world = t?.world;
|
||||
if (!t || !world) return null;
|
||||
let cx = 0, cy = 0, count = 0;
|
||||
for (const o of nearby || []) {
|
||||
if (!o || o === t || o.dead) continue;
|
||||
const dx = o.x - t.x, dy = o.y - t.y;
|
||||
const d2 = dx * dx + dy * dy;
|
||||
if (d2 > 118 * 118 || d2 < 0.0001) continue;
|
||||
const w = 1 / Math.max(24, Math.sqrt(d2));
|
||||
cx += dx * w;
|
||||
cy += dy * w;
|
||||
count++;
|
||||
}
|
||||
const seed = typeof stableUnit === "function" ? stableUnit(t.familyKey || t.id || "crowd", "crowd-escape-angle") : 0.5;
|
||||
let base = Math.atan2(-cy, -cx);
|
||||
if (!count || !Number.isFinite(base)) base = seed * Math.PI * 2;
|
||||
const pad = Math.max(30, (CONFIG.worldPadding || 30) + (t.radius || 22));
|
||||
const radius = Math.max(14, (t.radius || 22) * 0.72);
|
||||
const offsets = [0, 0.42, -0.42, 0.84, -0.84];
|
||||
for (const dist of [190, 270]) {
|
||||
for (const off of offsets) {
|
||||
const a = base + off + (seed - 0.5) * 0.18;
|
||||
const x = clamp(t.x + Math.cos(a) * dist, pad, world.w - pad);
|
||||
const y = clamp(t.y + Math.sin(a) * dist, pad, world.h - pad);
|
||||
if (world.pointBlockedByObstacle?.(x, y, radius, { maxChecks: 14, directionalOneWay: true })) continue;
|
||||
return { x, y, dead: false, detour: true, crowdEscape: true };
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function trySpreadZunchiDisease(source, target, dt, d, salt) {
|
||||
if (!source?.zunchiDisease || !target || target.zunchiDisease || (target.zunchiDiseaseCooldown || 0) > 0 || d >= 54) return false;
|
||||
const tempMul = typeof global.zunchiDiseaseTemperatureInfectionMultiplier === "function" ? global.zunchiDiseaseTemperatureInfectionMultiplier(target) : 1;
|
||||
const base = dt * 0.065 * clamp(1 - d / 58, 0.12, 1) * tempMul;
|
||||
const chance = target.diseaseChance ? target.diseaseChance(base) : base;
|
||||
if (!deterministicChance(source.world, `social-zunchi-spread-${salt}`, chance, source, target)) return false;
|
||||
if (!target.infectZunchiDisease(source)) return false;
|
||||
source.world.log(`${source.name}\u306e\u305a\u3093\u3061\u75c5\u304c${target.name}\u306b\u3046\u3064\u3063\u305f\u3002`, "accident", { participants: [target, source] });
|
||||
return true;
|
||||
}
|
||||
|
||||
Object.defineProperties(Tarinai.prototype, Object.getOwnPropertyDescriptors({
|
||||
interactWithOthers(dt) {
|
||||
let closeCount = 0;
|
||||
const scanLimit = 8;
|
||||
const scanLimit = (this.fightTimer > 0.04 || this.birthRitualTimer > 0.04) ? 12 : 8;
|
||||
const selfRadius = Math.max(12, Number(this.radius || 22) || 22);
|
||||
const scanRadius = Math.max(96, selfRadius * 2.4 + 88);
|
||||
let scanned = 0;
|
||||
const scanRadius = Math.max(92, selfRadius * 2.0 + 52);
|
||||
const selfProfile = this.personalityProfile();
|
||||
const detailed = [];
|
||||
// nearbyTarinai() has already paid the spatial lookup cost and returns only
|
||||
// candidates inside the requested radius when precise=true. Do not stop at
|
||||
// an arbitrary iteration count here: the incremental grid intentionally does
|
||||
// not preserve bucket insertion order, so an early break can miss the actual
|
||||
// nearest mate/contact candidates after enough cell crossings. Keep only the
|
||||
// nearest scanLimit entries with a tiny bounded insertion list instead.
|
||||
for (const o of this.world.nearbyTarinai(this.x, this.y, scanRadius, true)) {
|
||||
if (o === this || o.dead) continue;
|
||||
if (scanned >= scanLimit && this.fightTimer <= 0.04 && this.birthRitualTimer <= 0.04) break;
|
||||
scanned += 1;
|
||||
const dx = o.x - this.x, dy = o.y - this.y;
|
||||
const d = Math.hypot(dx, dy);
|
||||
const d2 = dx * dx + dy * dy;
|
||||
if (d2 > scanRadius * scanRadius || d2 < 0.0001) continue;
|
||||
const entry = { o, dx, dy, d2 };
|
||||
let pos = detailed.length;
|
||||
while (pos > 0 && detailed[pos - 1].d2 > d2) pos--;
|
||||
if (pos < scanLimit) {
|
||||
detailed.splice(pos, 0, entry);
|
||||
if (detailed.length > scanLimit) detailed.pop();
|
||||
}
|
||||
}
|
||||
for (const entry of detailed) {
|
||||
const o = entry.o;
|
||||
const pairLeader = String(this.id) < String(o.id);
|
||||
const dx = entry.dx, dy = entry.dy;
|
||||
const otherRadius = Math.max(12, Number(o.radius || 22) || 22);
|
||||
const bodyContactRange = Math.max(74, selfRadius + otherRadius + 18);
|
||||
if (d < 0.01 || d > bodyContactRange) continue;
|
||||
closeCount += 1;
|
||||
if (entry.d2 > bodyContactRange * bodyContactRange) continue;
|
||||
const d = Math.sqrt(entry.d2);
|
||||
|
||||
const champion = this.isTarinaiChampion ? this : (o.isTarinaiChampion ? o : null);
|
||||
if (champion && this.isTarinaiChampion !== o.isTarinaiChampion) {
|
||||
|
|
@ -153,7 +261,7 @@
|
|||
}
|
||||
}
|
||||
|
||||
const fightMochiContact = this.world.canFightPair?.(this, o) && (this.hasFightMochiEffect() || o.hasFightMochiEffect());
|
||||
const fightMochiContact = pairLeader && this.world.canFightPair?.(this, o) && (this.hasFightMochiEffect() || o.hasFightMochiEffect());
|
||||
if (fightMochiContact && (this.world.fightPairCooldownRemaining?.(this, o) || 0) <= 0.04 && this.fightCooldown <= 0.04 && o.fightCooldown <= 0.04 && d <= Math.max(42, this.radius + o.radius + 16)) {
|
||||
this.conflictTargetId = o.id;
|
||||
o.conflictTargetId = this.id;
|
||||
|
|
@ -167,53 +275,57 @@
|
|||
}
|
||||
}
|
||||
|
||||
const zunchiTempMulA = typeof global.zunchiDiseaseTemperatureInfectionMultiplier === "function" ? global.zunchiDiseaseTemperatureInfectionMultiplier(o) : 1;
|
||||
const zunchiSpreadBaseA = dt * 0.065 * clamp(1 - d / 58, 0.12, 1) * zunchiTempMulA;
|
||||
const zunchiSpreadChanceA = o.diseaseChance ? o.diseaseChance(zunchiSpreadBaseA) : zunchiSpreadBaseA;
|
||||
if (this.zunchiDisease && !o.zunchiDisease && (o.zunchiDiseaseCooldown || 0) <= 0 && d < 54 && deterministicChance(this.world, "social-zunchi-spread-a", zunchiSpreadChanceA, this, o)) {
|
||||
if (o.infectZunchiDisease(this)) this.world.log(`${this.name}\u306e\u305a\u3093\u3061\u75c5\u304c${o.name}\u306b\u3046\u3064\u3063\u305f\u3002`, "accident", { participants: [o, this] });
|
||||
}
|
||||
const zunchiTempMulB = typeof global.zunchiDiseaseTemperatureInfectionMultiplier === "function" ? global.zunchiDiseaseTemperatureInfectionMultiplier(this) : 1;
|
||||
const zunchiSpreadBaseB = dt * 0.065 * clamp(1 - d / 58, 0.12, 1) * zunchiTempMulB;
|
||||
const zunchiSpreadChanceB = this.diseaseChance ? this.diseaseChance(zunchiSpreadBaseB) : zunchiSpreadBaseB;
|
||||
if (o.zunchiDisease && !this.zunchiDisease && (this.zunchiDiseaseCooldown || 0) <= 0 && d < 54 && deterministicChance(this.world, "social-zunchi-spread-b", zunchiSpreadChanceB, this, o)) {
|
||||
if (this.infectZunchiDisease(o)) this.world.log(`${o.name}\u306e\u305a\u3093\u3061\u75c5\u304c${this.name}\u306b\u3046\u3064\u3063\u305f\u3002`, "accident", { participants: [o, this] });
|
||||
if (pairLeader) {
|
||||
trySpreadZunchiDisease(this, o, dt, d, "a");
|
||||
trySpreadZunchiDisease(o, this, dt, d, "b");
|
||||
}
|
||||
|
||||
const pairBond = this.world.areCoParents?.(this, o);
|
||||
const mixedZunchiSlave = !!this.isZunchiSlave !== !!o.isZunchiSlave;
|
||||
if (mixedZunchiSlave && d < 86) {
|
||||
const nonSlave = this.isZunchiSlave ? o : this;
|
||||
const slave = this.isZunchiSlave ? this : o;
|
||||
const nx = (nonSlave.x - slave.x) / d;
|
||||
const ny = (nonSlave.y - slave.y) / d;
|
||||
nonSlave.vx += nx * dt * 34;
|
||||
nonSlave.vy += ny * dt * 34;
|
||||
if (typeof applyNeedShock === "function") applyNeedShock(nonSlave, { safety: dt * 24, social: dt * 5 }, slave);
|
||||
nonSlave.lastNeedShockBreaker = slave;
|
||||
nonSlave.fearTimer = Math.max(nonSlave.fearTimer || 0, 0.22 * nonSlave.personalityProfile().fear);
|
||||
nonSlave.target = slave;
|
||||
nonSlave.thought = "\u305a\u3093\u3061\u3069\u308c\u3044\u304c\u8fd1\u3044";
|
||||
nonSlave.adjustRelation?.(slave, -dt * 0.08, dt * 0.24, "zunchi_slave_avoid");
|
||||
slave.adjustRelation?.(nonSlave, -dt * 0.02, 0, "zunchi_slave_avoid");
|
||||
if (deterministicChance(this.world, "zunchi-slave-surprise", dt * 0.35, nonSlave, slave)) nonSlave.surpriseTimer = Math.max(nonSlave.surpriseTimer || 0, 0.18);
|
||||
if (pairLeader) {
|
||||
const nonSlave = this.isZunchiSlave ? o : this;
|
||||
const slave = this.isZunchiSlave ? this : o;
|
||||
const nx = (nonSlave.x - slave.x) / d;
|
||||
const ny = (nonSlave.y - slave.y) / d;
|
||||
nonSlave.vx += nx * dt * 34;
|
||||
nonSlave.vy += ny * dt * 34;
|
||||
if (typeof applyNeedShock === "function") applyNeedShock(nonSlave, { safety: dt * 24, social: dt * 5 }, slave);
|
||||
nonSlave.lastNeedShockBreaker = slave;
|
||||
nonSlave.fearTimer = Math.max(nonSlave.fearTimer || 0, 0.22 * nonSlave.personalityProfile().fear);
|
||||
nonSlave.target = slave;
|
||||
nonSlave.thought = "\u305a\u3093\u3061\u3069\u308c\u3044\u304c\u8fd1\u3044";
|
||||
nonSlave.adjustRelation?.(slave, -dt * 0.08, dt * 0.24, "zunchi_slave_avoid");
|
||||
slave.adjustRelation?.(nonSlave, -dt * 0.02, 0, "zunchi_slave_avoid");
|
||||
if (deterministicChance(this.world, "zunchi-slave-surprise", dt * 0.35, nonSlave, slave)) nonSlave.surpriseTimer = Math.max(nonSlave.surpriseTimer || 0, 0.18);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
const overlap = this.radius + o.radius - d;
|
||||
if (overlap > -2) {
|
||||
if (pairLeader && overlap > -2) {
|
||||
const nx = dx / d, ny = dy / d;
|
||||
const push = (overlap + 6) * (pairBond ? 0.08 : 0.17);
|
||||
this.vx -= (dx / d) * push;
|
||||
this.vy -= (dy / d) * push;
|
||||
if (!pairBond && deterministicChance(this.world, "body-overlap-surprise", dt * 1.2, this, o)) this.surpriseTimer = Math.max(this.surpriseTimer, 0.12);
|
||||
this.vx -= nx * push;
|
||||
this.vy -= ny * push;
|
||||
o.vx += nx * push;
|
||||
o.vy += ny * push;
|
||||
if (!pairBond) {
|
||||
if (deterministicChance(this.world, "body-overlap-surprise-a", dt * 1.2, this, o)) this.surpriseTimer = Math.max(this.surpriseTimer, 0.12);
|
||||
if (deterministicChance(this.world, "body-overlap-surprise-b", dt * 1.2, o, this)) o.surpriseTimer = Math.max(o.surpriseTimer || 0, 0.12);
|
||||
}
|
||||
}
|
||||
if (pairBond && d > 26 && d < 120) {
|
||||
this.vx += (dx / d) * dt * 4.8;
|
||||
this.vy += (dy / d) * dt * 4.8;
|
||||
if (pairLeader && pairBond && d > 26 && d < 120) {
|
||||
const nx = dx / d, ny = dy / d;
|
||||
const pull = dt * 4.8;
|
||||
this.vx += nx * pull;
|
||||
this.vy += ny * pull;
|
||||
o.vx -= nx * pull;
|
||||
o.vy -= ny * pull;
|
||||
}
|
||||
|
||||
if (d < Math.max(60, selfRadius + otherRadius + 8)) {
|
||||
const rel = this.relationTo(o.id);
|
||||
const pdef = this.personalityProfile();
|
||||
const pdef = selfProfile;
|
||||
this.loneliness -= dt * 1.65 * this.trait.social;
|
||||
this.affection += dt * 0.28 * this.trait.social;
|
||||
this.adjustRelation(o, dt * 0.18 * pdef.relation * (rel.fear > 10 ? 0.35 : 1), -dt * 0.018, "nearby");
|
||||
|
|
@ -269,8 +381,11 @@
|
|||
}
|
||||
|
||||
const weights = socialFightMateWeights(this, o, this.world, { mode: "contact", requireBirthReady: true, distance: d, fightRange: 58 });
|
||||
const combinedStartChance = dt * 0.30 * clamp((weights.totalWeight || 0) / 10, 0, 2.6);
|
||||
if ((weights.totalWeight || 0) > 0 && deterministicChance(this.world, "social-weighted-fight-mate-start", combinedStartChance, this, o)) {
|
||||
const oneDirectionChance = clamp(dt * 0.30 * clamp((weights.totalWeight || 0) / 10, 0, 2.6), 0, 0.95);
|
||||
// Pair processing now runs once instead of once from each participant. Preserve
|
||||
// the old two-attempt probability: 1 - (1 - p)^2.
|
||||
const combinedStartChance = 1 - (1 - oneDirectionChance) * (1 - oneDirectionChance);
|
||||
if (pairLeader && (weights.totalWeight || 0) > 0 && deterministicChance(this.world, "social-weighted-fight-mate-start", combinedStartChance, this, o)) {
|
||||
const choice = pickSocialFightMate(this.world, "social-weighted-fight-mate-pick", weights, this, o);
|
||||
if (choice === "fight") {
|
||||
this.conflictTargetId = o.id;
|
||||
|
|
@ -291,12 +406,6 @@
|
|||
}
|
||||
}
|
||||
|
||||
if (closeCount > 8) {
|
||||
if (typeof applyNeedShock === "function") applyNeedShock(this, { social: dt * 5, safety: dt * 3 * this.personalityProfile().fear });
|
||||
this.energy -= dt * 0.25;
|
||||
this.fearTimer = Math.max(this.fearTimer, 0.18);
|
||||
}
|
||||
|
||||
if (this.loneliness < 12 && deterministicChance(this.world, "nearby-calm-log", dt * 0.002, this)) {
|
||||
this.world.log(`${this.name}\u306f\u8ab0\u304b\u306e\u8fd1\u304f\u3067\u3001\u305f\u308a\u306a\u3044\u3053\u3068\u3092\u5c11\u3057\u5fd8\u308c\u305f\u3002`, null, { participants: [this] });
|
||||
}
|
||||
|
|
@ -474,12 +583,11 @@
|
|||
|
||||
if (this.target && !this.target.dead) {
|
||||
const baseMoveTarget = this.state === "seek_bed" && this.isSleepFurniture(this.target) ? this.sleepSpotFor(this.target) : (this.state === "play_seesaw" ? (globalThis.TarinaiSeesawSystem?.seatWorld?.(this.target, this.seesawSeatIndex) || this.target) : this.target);
|
||||
const routeStates = new Set(["seek_food", "seek_water", "seek_bed", "seek_material", "build", "follow_parent", "seek_friend", "seek_enemy", "fight", "play_ball", "play_seesaw", "ant_attack", "seek_temperature"]);
|
||||
const routeTargetItem = this.target?.shelterItem || this.target?.hostItem || this.target;
|
||||
const pathPadding = Math.max(18, (this.radius || 20) * 0.82);
|
||||
const targetFoodType = this.target?.type === "duplicator" ? this.target?.storedFoodType : this.target?.type;
|
||||
const zundaFoodTarget = this.state === "seek_food" && (targetFoodType === "sweet" || targetFoodType === "zunda_juice");
|
||||
const waypoint = routeStates.has(this.state) && this.world?.findTarinaiPathWaypoint
|
||||
const waypoint = ROUTE_STATES.has(this.state) && this.world?.findTarinaiPathWaypoint
|
||||
? this.world.findTarinaiPathWaypoint(this, baseMoveTarget, { targetItem: routeTargetItem, padding: pathPadding, state: this.state, preferFullPath: zundaFoodTarget })
|
||||
: null;
|
||||
if (this.state === "seek_bed" && this.target?.type === "grass_bed") {
|
||||
|
|
@ -707,7 +815,9 @@
|
|||
const finalReason = this.normalizeDeathReason ? this.normalizeDeathReason(reason, opts) : (reason || "\u4e8b\u6545");
|
||||
this.dead = true;
|
||||
this.deathReason = finalReason;
|
||||
this.world.syncTarinaiCountEntry?.(this);
|
||||
this.world.deadCount += 1;
|
||||
globalThis.TarinaiAchievements?.evaluateEvent?.(this.world, "population", { tarinai: this, delta: -1, reason: finalReason });
|
||||
globalThis.TarinaiAchievements?.recordDeath?.({ world: this.world, tarinai: this, reason: finalReason, fromDamage: Boolean(opts.fromDamage), achievementDangerKill: Boolean(opts.achievementDangerKill) });
|
||||
this.world.lastDeathAt = this.world.time || 0;
|
||||
if (!Array.isArray(this.world.recentDeathTimes)) this.world.recentDeathTimes = [];
|
||||
|
|
@ -736,6 +846,7 @@
|
|||
this.world.markDead(this, finalReason);
|
||||
this.releasedLiveToken = this.liveToken || 0;
|
||||
this.world.releaseTarinaiLiveId?.(this);
|
||||
this.world.noteTarinaiDeathForRuntimeCleanup?.(this.id);
|
||||
audio.death();
|
||||
this.world.log(`${this.name}\u306f\u9759\u304b\u306a\u75d5\u8de1\u3092\u6b8b\u3057\u305f\u3002\u6b7b\u56e0: ${finalReason}`, "death", { participants: [this] });
|
||||
if (this.world.selected === this) this.world.selected = null;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue