tarinai/js/world_family_social.js
2026-06-30 10:57:06 +09:00

1334 lines
71 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"use strict";
(function (global) {
const World = global.World;
if (!World) throw new Error("World is not available for mixin: world_family_social.js");
const RESET_PRESETS = Object.freeze({
empty: { label: "空白" },
default: { label: "デフォルト" },
athletic: { label: "アスレチック" },
crowded: { label: "過密" },
family: { label: "家族" },
war: { label: "戦争" },
happy: { label: "幸福" },
});
function normalizeResetPresetId(presetId = "default") {
const id = String(presetId || "default");
return Object.prototype.hasOwnProperty.call(RESET_PRESETS, id) ? id : "default";
}
function presetScaleForField(field) {
const id = field?.id || "garden";
if (id === "cage") return 0.65;
if (id === "park") return 1.45;
return 1;
}
function presetCount(field, base, min = 0, max = Infinity) {
const value = Math.round((Number(base) || 0) * presetScaleForField(field));
return Math.max(min, Math.min(Number.isFinite(max) ? max : value, value));
}
function randPoint(worldRef, pad = 78, zone = null) {
const x0 = Math.max(pad, Number(zone?.x0 ?? pad));
const x1 = Math.min(worldRef.w - pad, Number(zone?.x1 ?? (worldRef.w - pad)));
const y0 = Math.max(pad, Number(zone?.y0 ?? pad));
const y1 = Math.min(worldRef.h - pad, Number(zone?.y1 ?? (worldRef.h - pad)));
return { x: rand(Math.min(x0, x1), Math.max(x0, x1)), y: rand(Math.min(y0, y1), Math.max(y0, y1)) };
}
function randomPresetAngle(step = Math.PI / 4) {
const n = Math.max(1, Math.round((Math.PI * 2) / Math.max(0.001, step)));
return Math.floor(rand(0, n)) * step;
}
function presetShapeExtent(segments) {
let max = 0;
for (const seg of segments || []) {
for (let i = 0; i < 4; i += 2) max = Math.max(max, Math.hypot(Number(seg[i]) || 0, Number(seg[i + 1]) || 0));
}
return max;
}
function configurePresetMechanicalItem(item, opts = {}) {
const body = item?.physicsBody;
if (!body || (item.type !== "rotator" && item.type !== "reciprocator")) return item;
body.pose = body.pose || { x: item.x, y: item.y, angle: Number(item.angle) || 0 };
body.pose.x = item.x;
body.pose.y = item.y;
body.pose.angle = Number(item.angle) || 0;
body.velocity = body.velocity || { x: 0, y: 0, angular: 0, linear: 0 };
body.velocity.x = 0;
body.velocity.y = 0;
body.velocity.angular = 0;
body.velocity.linear = 0;
body.shape = body.shape || { model: "segments", thickness: 12, segments: [], version: 0 };
if (item.type === "rotator") {
const segments = opts.segments || [
[-92, 0, 92, 0],
[0, -58, 0, 58],
[-58, -42, 58, 42],
[-58, 42, 58, -42],
];
body.motor = body.motor || { powered: true, speed: Math.PI * 0.18, direction: 1 };
body.motor.powered = opts.motorOn !== false;
body.motor.speed = Number.isFinite(Number(opts.motorSpeed)) ? Number(opts.motorSpeed) : Math.PI * 0.18;
body.motor.direction = Number(opts.direction) < 0 ? -1 : 1;
body.shape.model = "segments";
body.shape.thickness = Number.isFinite(Number(opts.thickness)) ? Number(opts.thickness) : 14;
body.shape.segments = segments.map(seg => seg.slice(0, 4));
body.shape.version = (Number(body.shape.version) || 0) + 1;
item.spin = 0;
item.r = Math.max(item.r || 64, presetShapeExtent(segments) + body.shape.thickness + 8);
} else if (item.type === "reciprocator") {
const railAxis = Number.isFinite(Number(opts.railAxis)) ? Number(opts.railAxis) : (Number(item.angle) || 0);
const segments = opts.segments || [
[-92, -24, -36, -24],
[-36, -24, -36, 24],
[-36, 24, 36, 24],
[36, 24, 36, -24],
[36, -24, 92, -24],
];
body.motor = body.motor || { powered: true, speed: 36, direction: 1 };
body.motor.powered = opts.railOn !== false;
body.motor.speed = Number.isFinite(Number(opts.railMotorSpeed)) ? Number(opts.railMotorSpeed) : 36;
body.motor.direction = Number(opts.direction) < 0 ? -1 : 1;
body.rail = body.rail || { axisAngle: railAxis, travel: 120, phase: 0, anchorX: item.x, anchorY: item.y };
body.rail.axisAngle = railAxis;
body.rail.travel = Number.isFinite(Number(opts.railTravel)) ? Number(opts.railTravel) : 120;
body.rail.phase = Number.isFinite(Number(opts.railPhase)) ? Number(opts.railPhase) : 0;
body.rail.anchorX = item.x;
body.rail.anchorY = item.y;
body.shape.model = "segments";
body.shape.thickness = Number.isFinite(Number(opts.thickness)) ? Number(opts.thickness) : 14;
body.shape.segments = segments.map(seg => seg.slice(0, 4));
body.shape.version = (Number(body.shape.version) || 0) + 1;
item.r = Math.max(item.r || 64, presetShapeExtent(segments) + body.shape.thickness + 8);
}
globalThis.TarinaiPhysicsBodySystem?.invalidateItem?.(item, "preset-mechanical-shape");
globalThis.TarinaiMechanicalSystem?.invalidateGeometry?.(item);
return item;
}
function syncPresetMechanicalPose(item) {
const body = item?.physicsBody;
if (!body) return;
body.pose = body.pose || { x: item.x, y: item.y, angle: Number(item.angle) || 0 };
body.pose.x = item.x;
body.pose.y = item.y;
body.pose.angle = Number(item.angle) || 0;
if (body.rail) {
body.rail.anchorX = item.x;
body.rail.anchorY = item.y;
}
globalThis.TarinaiPhysicsBodySystem?.invalidateItem?.(item, "preset-placement-pose");
globalThis.TarinaiMechanicalSystem?.invalidateGeometry?.(item);
}
function presetItemFootprintRadius(worldRef, item) {
const radius = globalThis.TarinaiCollisionFootprints?.itemFootprintRadius?.(worldRef, item);
if (Number.isFinite(Number(radius))) {
if (item?.type === "ball") return Math.max(34, Number(radius) * 1.35);
return Math.max(14, Number(radius));
}
return Math.max(14, Number(item?.r || 16) || 16);
}
function presetItemPlacementBlocked(worldRef, item, opts = {}) {
if (!worldRef || !item || item.dead) return true;
if (typeof worldRef.placementBlocked === "function" && worldRef.placementBlocked(item)) return true;
const itemRects = worldRef.solidObstacleRects?.(item) || globalThis.TarinaiCollisionFootprints?.placementRectsFor?.(worldRef, item) || [];
const itemRadius = presetItemFootprintRadius(worldRef, item);
const search = Math.max(120, itemRadius * 3.2);
for (const other of worldRef.nearbyItems?.(item.x, item.y, search) || worldRef.items || []) {
if (!other || other === item || other.dead) continue;
const sameType = other.type === item.type;
const otherIsFence = worldRef.isFenceType?.(other.type) || false;
const itemIsFence = worldRef.isFenceType?.(item.type) || false;
if (!opts.avoidAllObstacles && !sameType && !otherIsFence && !itemIsFence) continue;
const otherRects = worldRef.solidObstacleRects?.(other) || globalThis.TarinaiCollisionFootprints?.placementRectsFor?.(worldRef, other) || [];
const otherRadius = presetItemFootprintRadius(worldRef, other);
const margin = (itemIsFence || otherIsFence) ? 4 : Math.max(8, Math.min(itemRadius, otherRadius) * 0.28);
if (itemRects.length && otherRects.length) {
if (itemRects.some(a => otherRects.some(b => globalThis.TarinaiCollisionFootprints?.rectsOverlap?.(a, b, margin)))) return true;
continue;
}
if (itemRects.length) {
if (itemRects.some(r => globalThis.TarinaiCollisionFootprints?.rectCircleOverlap?.(r, other.x, other.y, otherRadius, margin))) return true;
continue;
}
if (otherRects.length) {
if (otherRects.some(r => globalThis.TarinaiCollisionFootprints?.rectCircleOverlap?.(r, item.x, item.y, itemRadius, margin))) return true;
continue;
}
if (Math.hypot((item.x || 0) - (other.x || 0), (item.y || 0) - (other.y || 0)) < itemRadius + otherRadius + margin) return true;
}
return false;
}
function findPresetItemSpot(worldRef, item, opts = {}) {
const allowOriginal = opts.allowOriginal !== false;
if (allowOriginal && !presetItemPlacementBlocked(worldRef, item, opts)) return { x: item.x, y: item.y };
const solid = Boolean(worldRef?.isFenceType?.(item.type) || item.type === "rotator" || item.type === "reciprocator" || item.type === "nest_box");
const count = Math.max(12, Number(opts.attempts || 0) || (solid ? 96 : 58));
const maxRadius = Math.max(70, Number(opts.maxRadius || 0) || (solid ? 220 : 170));
const golden = Math.PI * (3 - Math.sqrt(5));
let best = null;
let bestD = Infinity;
const seed = `${item.type}:${Math.round(item.x)},${Math.round(item.y)}`;
for (let i = 0; i < count; i++) {
const t = count <= 1 ? 1 : i / (count - 1);
const radius = Math.max(10, maxRadius * Math.sqrt(t));
const angle = i * golden + (globalThis.stableUnit ? globalThis.stableUnit(seed, "preset-place") : Math.random()) * Math.PI * 2;
const rawX = item.x + Math.cos(angle) * radius;
const rawY = item.y + Math.sin(angle) * radius * 0.74;
const p = worldRef.placementClampPointFor?.(item, rawX, rawY) || { x: clamp(rawX, 42, worldRef.w - 42), y: clamp(rawY, 42, worldRef.h - 42) };
const oldX = item.x, oldY = item.y;
item.x = p.x; item.y = p.y; syncPresetMechanicalPose(item);
const blocked = presetItemPlacementBlocked(worldRef, item, opts);
item.x = oldX; item.y = oldY; syncPresetMechanicalPose(item);
if (blocked) continue;
const d = Math.hypot(p.x - oldX, p.y - oldY);
if (d < bestD) { best = p; bestD = d; }
}
return best;
}
function addPresetItem(worldRef, type, x, y, opts = {}) {
const item = new Item(type, x, y);
if (Number.isFinite(Number(opts.r))) item.r = Number(opts.r);
if (Number.isFinite(Number(opts.angle))) item.angle = Number(opts.angle);
if (opts.toolSize) item.toolSize = opts.toolSize;
if (type === "duplicator") {
item.storedFoodType = opts.storedFoodType || "sweet";
item.storedFoodLabel = typeof foodLabel === "function" ? foodLabel(item.storedFoodType) : (typeof toolLabel === "function" ? toolLabel(item.storedFoodType) : item.storedFoodType);
if (item.roles) item.roles.food = true;
}
if (type === "rotator" || type === "reciprocator") configurePresetMechanicalItem(item, opts);
if (opts.avoidOverlap) {
const spot = findPresetItemSpot(worldRef, item, opts);
if (!spot) return null;
item.x = spot.x;
item.y = spot.y;
syncPresetMechanicalPose(item);
}
if (worldRef.addItem) worldRef.addItem(item, opts.reason || `preset-${type}`);
else worldRef.items.push(item);
return item;
}
function addPresetGrass(worldRef, count, zone = null) {
for (let i = 0; i < count; i++) {
const p = randPoint(worldRef, 70, zone);
const grass = addPresetItem(worldRef, "grass", p.x, p.y, { reason: "preset-grass" });
globalThis.TarinaiGrass?.setStage?.(grass, Math.floor(rand(0, 2.999)), { force: true });
}
}
function addPresetTarinai(worldRef, count, zone = null, opts = {}) {
for (let i = 0; i < count; i++) {
const p = randPoint(worldRef, 82, zone);
worldRef.addTarinai({ x: p.x, y: p.y, generation: opts.generation || 1 });
}
}
function addPresetItemsScattered(worldRef, type, count, zone = null, opts = {}) {
let placed = 0;
const n = Math.max(0, Number(count) | 0);
const attempts = opts.avoidOverlap ? Math.max(n * 4, n + 8) : n;
for (let i = 0; i < attempts && placed < n; i++) {
const p = randPoint(worldRef, opts.pad || 82, zone);
const item = addPresetItem(worldRef, type, p.x, p.y, opts);
if (item) placed++;
}
}
function addPresetFenceLine(worldRef, count, yRatio, angle = 0, type = "fence_h", opts = {}) {
const n = Math.max(1, Number(count) | 0);
const gap = worldRef.w / (n + 1);
for (let i = 1; i <= n; i++) {
addPresetItem(worldRef, type, gap * i, worldRef.h * yRatio + rand(-24, 24), { angle, reason: opts.reason || "preset-fence", avoidOverlap: !!opts.avoidOverlap, avoidAllObstacles: !!opts.avoidAllObstacles, attempts: opts.attempts || 96, maxRadius: opts.maxRadius || 190 });
}
}
function addPresetFencesScattered(worldRef, count, zone = null, opts = {}) {
const n = Math.max(0, Number(count) | 0);
let placed = 0;
const attempts = opts.avoidOverlap ? Math.max(n * 4, n + 8) : n;
for (let i = 0; i < attempts && placed < n; i++) {
const p = randPoint(worldRef, opts.pad || 78, zone);
const vertical = Math.random() < 0.5;
const item = addPresetItem(worldRef, vertical ? "fence_v" : "fence_h", p.x, p.y, { angle: randomPresetAngle(Math.PI / 4), reason: opts.reason || "preset-fence-scatter", avoidOverlap: !!opts.avoidOverlap, attempts: opts.attempts || 80, maxRadius: opts.maxRadius || 180 });
if (item) placed++;
}
}
function generateDefaultPreset(worldRef, field, seedPopulation) {
const population = seedPopulation ?? field.population ?? CONFIG.initialPopulation;
const grassCount = field.grass ?? 18;
addPresetTarinai(worldRef, population);
addPresetGrass(worldRef, grassCount);
addPresetItem(worldRef, "stone", worldRef.w * 0.68, worldRef.h * 0.32, { reason: "reset-stone" });
}
function generateAthleticPreset(worldRef, field) {
const factor = presetScaleForField(field);
const fieldId = field?.id || "garden";
const compact = fieldId === "cage" || fieldId === "garden";
const center = { x0: worldRef.w * 0.10, x1: worldRef.w * 0.90, y0: worldRef.h * 0.14, y1: worldRef.h * 0.86 };
addPresetTarinai(worldRef, presetCount(field, 14, 7, 24), { x0: worldRef.w * 0.08, x1: worldRef.w * 0.30, y0: worldRef.h * 0.18, y1: worldRef.h * 0.82 });
// 物理ブロックは柵より先に置く。狭い中小フィールドでは柵側を間引き、ブロックを優先する。
addPresetItemsScattered(worldRef, "stone", presetCount(field, compact ? 4 : 6, 3, 9), center, { pad: compact ? 118 : 132, reason: "preset-athletic-physical-block", avoidOverlap: true, avoidAllObstacles: true, attempts: 140, maxRadius: compact ? 260 : 220 });
addPresetItemsScattered(worldRef, "stove", presetCount(field, 2, 1, 3), center, { pad: 118, reason: "preset-athletic-stove", avoidOverlap: true, avoidAllObstacles: true, attempts: 100, maxRadius: 210 });
addPresetItemsScattered(worldRef, "ball", presetCount(field, compact ? 8 : 10, 4, 18), null, { pad: 96, reason: "preset-ball", avoidOverlap: true, avoidAllObstacles: true, attempts: 90, maxRadius: compact ? 230 : 190 });
addPresetFenceLine(worldRef, Math.max(2, Math.round((compact ? 3 : 4) * factor)), 0.36, 0, "fence_h", { avoidOverlap: true, avoidAllObstacles: true, attempts: 120, maxRadius: compact ? 250 : 210, reason: "preset-athletic-fence-line" });
addPresetFenceLine(worldRef, Math.max(1, Math.round((compact ? 2 : 3) * factor)), 0.62, Math.PI / 2, "fence_v", { avoidOverlap: true, avoidAllObstacles: true, attempts: 120, maxRadius: compact ? 250 : 210, reason: "preset-athletic-fence-line" });
addPresetFencesScattered(worldRef, Math.max(1, Math.round((compact ? 2 : 4) * factor)), null, { pad: 108, reason: "preset-athletic-fence", avoidOverlap: true, avoidAllObstacles: true, attempts: 120, maxRadius: compact ? 250 : 210 });
addPresetItemsScattered(worldRef, "rotator", Math.max(1, Math.round(2 * factor)), null, { pad: 148, reason: "preset-rotator-slow-custom", motorSpeed: Math.PI * 0.18, thickness: 14, avoidOverlap: true, avoidAllObstacles: true, attempts: 140, maxRadius: compact ? 280 : 240 });
addPresetItemsScattered(worldRef, "reciprocator", Math.max(1, Math.round(2 * factor)), null, { pad: 148, reason: "preset-reciprocator-slow-custom", railMotorSpeed: 36, railTravel: 120, thickness: 14, avoidOverlap: true, avoidAllObstacles: true, attempts: 140, maxRadius: compact ? 280 : 240 });
}
function generateCrowdedPreset(worldRef, field) {
const center = { x0: worldRef.w * 0.18, x1: worldRef.w * 0.82, y0: worldRef.h * 0.18, y1: worldRef.h * 0.82 };
addPresetTarinai(worldRef, presetCount(field, 36, 18, 58), center);
addPresetItemsScattered(worldRef, "nest_box", presetCount(field, 13, 7, 22), center, { pad: 96, reason: "preset-nest-box" });
addPresetItemsScattered(worldRef, "duplicator", presetCount(field, 3, 2, 5), center, { pad: 110, storedFoodType: "sweet", reason: "preset-duplicator-zunda" });
addPresetGrass(worldRef, presetCount(field, 18, 8, 34), center);
addPresetItemsScattered(worldRef, "water", presetCount(field, 8, 4, 16), center, { pad: 84, reason: "preset-water" });
}
function generateFamilyPreset(worldRef, field) {
const cx = worldRef.w * 0.5;
const cy = worldRef.h * 0.52;
worldRef.addTarinai({ x: cx - 52, y: cy + 20, generation: 1 });
worldRef.addTarinai({ x: cx + 52, y: cy + 20, generation: 1 });
addPresetItem(worldRef, "nest_box", cx, cy - 58, { reason: "preset-family-nest" });
addPresetItem(worldRef, "duplicator", cx, cy + 112, { storedFoodType: "love_mochi", reason: "preset-family-duplicator" });
}
function generateWarPreset(worldRef, field) {
const left = { x0: worldRef.w * 0.08, x1: worldRef.w * 0.45, y0: worldRef.h * 0.16, y1: worldRef.h * 0.84 };
const right = { x0: worldRef.w * 0.55, x1: worldRef.w * 0.92, y0: worldRef.h * 0.16, y1: worldRef.h * 0.84 };
addPresetTarinai(worldRef, Math.ceil(presetCount(field, 28, 14, 48) / 2), left);
addPresetTarinai(worldRef, Math.floor(presetCount(field, 28, 14, 48) / 2), right);
addPresetItemsScattered(worldRef, "duplicator", presetCount(field, 3, 2, 5), { x0: worldRef.w * 0.38, x1: worldRef.w * 0.62, y0: worldRef.h * 0.22, y1: worldRef.h * 0.78 }, { pad: 96, storedFoodType: "fight_mochi", reason: "preset-war-duplicator" });
addPresetItemsScattered(worldRef, "ball", presetCount(field, 8, 4, 14), null, { pad: 90, reason: "preset-war-ball" });
addPresetItemsScattered(worldRef, "ant_nest", presetCount(field, 1.25, 1, 2), null, { pad: 116, reason: "preset-war-ant-nest", avoidOverlap: true, attempts: 80, maxRadius: 190 });
addPresetItemsScattered(worldRef, "pushpin", presetCount(field, 5, 2, 9), null, { pad: 88, reason: "preset-war-pushpin-reduced" });
addPresetItemsScattered(worldRef, "oshibyo", presetCount(field, 8, 4, 16), null, { pad: 88, reason: "preset-war-oshibyo" });
addPresetFencesScattered(worldRef, presetCount(field, 8, 4, 14), null, { pad: 94, reason: "preset-war-fence" });
}
function generateHappyPreset(worldRef, field) {
const center = { x0: worldRef.w * 0.16, x1: worldRef.w * 0.84, y0: worldRef.h * 0.16, y1: worldRef.h * 0.84 };
addPresetTarinai(worldRef, presetCount(field, 16, 8, 28), center);
addPresetItemsScattered(worldRef, "nest_box", presetCount(field, 8, 4, 14), center, { pad: 96, reason: "preset-happy-nest-box" });
addPresetItemsScattered(worldRef, "robot_cleaner", presetCount(field, 2, 1, 3), center, { pad: 110, reason: "preset-happy-robot-cleaner" });
addPresetItemsScattered(worldRef, "duplicator", presetCount(field, 2, 1, 4), center, { pad: 110, storedFoodType: "sweet", reason: "preset-happy-duplicator-zunda" });
addPresetItemsScattered(worldRef, "ball", presetCount(field, 6, 3, 10), center, { pad: 88, reason: "preset-happy-ball" });
}
function generatePresetWorld(worldRef, field, presetId, seedPopulation) {
switch (normalizeResetPresetId(presetId)) {
case "empty": return;
case "athletic": return generateAthleticPreset(worldRef, field);
case "crowded": return generateCrowdedPreset(worldRef, field);
case "family": return generateFamilyPreset(worldRef, field);
case "war": return generateWarPreset(worldRef, field);
case "happy": return generateHappyPreset(worldRef, field);
case "default":
default:
return generateDefaultPreset(worldRef, field, seedPopulation);
}
}
function groundForResetPreset(presetId = "default") {
const id = normalizeResetPresetId(presetId);
if (id === "athletic") return "ice";
if (id === "war") return "foot_massage";
if (id === "happy") return "blanket";
return "";
}
global.TarinaiResetPresets = Object.freeze({ ids: Object.keys(RESET_PRESETS), meta: RESET_PRESETS, normalize: normalizeResetPresetId });
Object.defineProperties(World.prototype, Object.getOwnPropertyDescriptors({
reset(seedPopulation = null, fieldType = this.fieldType || "garden", presetId = "default") {
const field = FIELD_TYPES?.[fieldType] || FIELD_TYPES.garden;
const preset = normalizeResetPresetId(presetId);
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.itemCounts = {};
this.effectCounts = {};
this.itemTypeBuckets = new Map();
this.itemIdMap = new Map();
this.itemBucketsDirty = false;
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.grassGrowthTimer = 0;
this.nextGrassLimitCheckAt = 0;
this.selected = null;
this.deadCount = 0;
this.liveIdNext = 1;
this.liveIdSerial = 0;
this.liveTarinai = new Map();
this.worldSeed = global.TarinaiSeedFactory.createWorldSeed() || `w${Date.now().toString(36)}`;
this.birthSerial = 0;
this.lastBirthAt = -999;
this.maxGeneration = 1;
this.family = {};
this.familyVersion = 0;
this.relationNotices = {};
this.resolvedFightIds = {};
this.fightPairCooldowns = {};
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.groundType = window.TarinaiGround.exists(this.groundType || "soil") ? (this.groundType || "soil") : "soil";
const presetGround = groundForResetPreset(preset);
if (presetGround && window.TarinaiGround.exists(presetGround)) this.groundType = presetGround;
this.nextWeatherChange = rand(CONFIG.weatherChangeMin, CONFIG.weatherChangeMax);
generatePresetWorld(this, field, preset, seedPopulation);
this.updateItemCounts();
this.enforceGrassLimit?.("reset-grass-limit");
this.updateItemCounts();
this.updateEffectCounts();
this.markTerrainDirty?.("reset");
this.rebuildSpatial(true);
const label = RESET_PRESETS[preset]?.label || "デフォルト";
showToast(`${label}で新しい観察を開始しました。`);
syncTopButtons();
},
tarinaiFamilyKey(t) {
return t?.familyKey || 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.
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;
const nextGenetics = typeof normalizeGenetics === "function" ? normalizeGenetics(t.genetics, t.familyKey || t.id || "") : { ...(t.genetics || {}) };
const nextFightWins = Number(t.totalFightWins || 0) || 0;
const nextFightLosses = Number(t.totalFightLosses || 0) || 0;
const nextChampion = !!t.isTarinaiChampion;
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,
genetics: nextGenetics,
lifeSpan: Number(t.lifeSpan) || 0,
totalFightWins: nextFightWins,
totalFightLosses: nextFightLosses,
isTarinaiChampion: nextChampion,
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;
for (const key of ["lifeSpanMul", "attackMul", "speedMul", "sizeMul"]) if (Math.abs((Number(entry.genetics?.[key]) || 1) - (Number(nextGenetics?.[key]) || 1)) > 0.002) changed = true;
if (Math.abs((Number(entry.lifeSpan) || 0) - (Number(t.lifeSpan) || 0)) > 1) changed = true;
if ((Number(entry.totalFightWins) || 0) !== nextFightWins) changed = true;
if ((Number(entry.totalFightLosses) || 0) !== nextFightLosses) changed = true;
if (!!entry.isTarinaiChampion !== nextChampion) 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.genetics = nextGenetics;
entry.lifeSpan = Number(t.lifeSpan) || entry.lifeSpan || 0;
entry.totalFightWins = nextFightWins;
entry.totalFightLosses = nextFightLosses;
entry.isTarinaiChampion = nextChampion;
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;
},
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),
genetics: live && typeof normalizeGenetics === "function" ? normalizeGenetics(live.genetics, live.familyKey || live.id || "") : (typeof normalizeGenetics === "function" ? normalizeGenetics(n.genetics, id) : { ...(n.genetics || {}) }),
lifeSpan: Number(live?.lifeSpan || n.lifeSpan || 0) || 0,
totalFightWins: Number(live?.totalFightWins ?? n.totalFightWins ?? 0) || 0,
totalFightLosses: Number(live?.totalFightLosses ?? n.totalFightLosses ?? 0) || 0,
isTarinaiChampion: !!(live?.isTarinaiChampion || n.isTarinaiChampion),
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];
let equal = false;
if (key === "genetics") {
equal = ["lifeSpanMul", "attackMul", "speedMul", "sizeMul"].every(gk => Math.abs((Number(a?.[gk]) || 1) - (Number(b?.[gk]) || 1)) <= 0.002);
} else {
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;
}
},
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.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 = {}) {
opts = global.TarinaiSeedFactory.prepareAddOptions(this, opts) || opts;
const seedParents = Array.isArray(opts.seedParents) ? opts.seedParents.filter(p => p && typeof p === "object") : [];
if (!opts.genetics && seedParents.length >= 2 && typeof inheritedGenetics === "function") {
opts.genetics = inheritedGenetics(seedParents[0], seedParents[1], opts.birthSeed || opts.familyKey || "");
}
if (opts.genetics) {
if (!Number.isFinite(opts.adultScale) && typeof adultScaleFromGenetics === "function") opts.adultScale = adultScaleFromGenetics(opts.birthSeed || opts.familyKey || "", opts.genetics);
if (!Number.isFinite(opts.lifeSpan)) {
const profile = global.TarinaiSeedFactory.birthProfile(opts.birthSeed || opts.familyKey || "", opts) || {};
if (Number.isFinite(profile.lifeSpan)) opts.lifeSpan = profile.lifeSpan;
}
}
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;
},
nestBoxOccupants(box, limit = 5) {
return globalThis.TarinaiNestSleepSystem.nestBoxOccupants(this, box, limit) || [];
},
pointerNestBoxInfo() {
const p = this.pointer;
if (!p?.inside) return null;
let best = null, bestD = Infinity;
for (const it of this.nearbyItems(p.x, p.y, 140)) {
if (!it || it.dead || it.type !== "nest_box") continue;
const d = distXY(p.x, p.y, it.x, it.y);
if (d < bestD && d <= Math.max(80, it.r * 2.1)) { best = it; bestD = d; }
}
if (!best) return null;
const occupants = this.nestBoxOccupants(best, Infinity);
const capacity = this.nestBoxCapacity(best);
return { box: best, occupants, capacity };
},
nestBoxTooltipLines(box) {
if (!box || box.dead || box.type !== "nest_box") return [];
const occupants = this.nestBoxOccupants(box, Infinity);
const names = occupants.map(t => t?.name).filter(Boolean).slice(0, 5);
const more = occupants.length > names.length ? `\u3001\u307b\u304b${occupants.length - names.length}\u5339` : "";
return ["\u5de3\u7bb1", names.length ? `\u5165\u3063\u3066\u308b\u5b50: ${names.join("\u3001")}${more}` : "\u5165\u3063\u3066\u308b\u5b50: \u306a\u3057", this.temperatureTooltipLineFor?.(box, "内部気温")].filter(Boolean);
},
temperatureTooltipLineFor(target, label = "体感気温") {
if (!target || !this.temperatureAt) return "";
const temp = this.temperatureAt(target.x, target.y);
if (!Number.isFinite(Number(temp))) return "";
const status = this.temperatureStatusFor?.(temp) || null;
const suffix = status?.label ? `(${status.label})` : "";
return `${label}: ${Number(temp).toFixed(1)}℃${suffix}`;
},
pointerTarinaiTooltipInfo() {
const p = this.pointer;
if (!p?.inside || !Array.isArray(this.tarinai)) return null;
let best = null, bestD = Infinity;
for (const t of this.tarinai) {
if (!t || t.dead) continue;
const d = distXY(p.x, p.y, t.x, t.y);
const limit = Math.max(46, (t.radius || 20) * 1.35);
if (d <= limit && d < bestD) { best = t; bestD = d; }
}
if (!best) return null;
const behavior = (typeof behaviorText === "function" ? behaviorText(best) : "") || window.TEXT_CATALOG?.stateLabel?.(best.state, best) || best.thought || "";
const maxEnergy = typeof tarinaiMaxEnergy === "function" ? tarinaiMaxEnergy(best) : (Number(best.maxEnergy) || 100);
const statusLine = `体力: ${Math.round(best.energy || 0)}/${Math.round(maxEnergy)} 空腹: ${Math.round(best.hunger || 0)} ストレス: ${Math.round(best.stress || 0)}`;
const tempLine = this.temperatureTooltipLineFor(best, "体感気温");
return { target: best, lines: [best.name || "たりない", behavior, statusLine, tempLine].filter(Boolean), kind: "tarinai" };
},
pointerOwnedBedInfo() {
const p = this.pointer;
if (!p?.inside) return null;
let best = null, bestD = Infinity;
for (const it of this.nearbyItems(p.x, p.y, 140)) {
if (!it || it.dead || it.type !== "grass_bed") continue;
const d = distXY(p.x, p.y, it.x, it.y);
if (d < bestD && d <= Math.max(60, (it.r || 24) * 2.0)) { best = it; bestD = d; }
}
if (!best) return null;
const owner = this.liveTarinaiById?.(best.ownerId) || null;
return { bed: best, owner };
},
ownedBedTooltipLines(bed) {
if (!bed || bed.dead || bed.type !== "grass_bed") return [];
const owner = this.liveTarinaiById?.(bed.ownerId) || null;
return ["\u304b\u3093\u305f\u3093\u30d9\u30c3\u30c9", `\u6301\u3061\u4e3b: ${owner?.name || "\u4e0d\u660e"}`, this.temperatureTooltipLineFor?.(bed, "周辺気温")].filter(Boolean);
},
pointerItemTooltipInfo() {
const tarinaiInfo = this.pointerTarinaiTooltipInfo?.();
if (tarinaiInfo?.target) return tarinaiInfo;
const nest = this.pointerNestBoxInfo?.();
if (nest?.box) return { target: nest.box, lines: this.nestBoxTooltipLines(nest.box), kind: "nest_box" };
const bedInfo = this.pointerOwnedBedInfo?.();
if (bedInfo?.bed) return { target: bedInfo.bed, lines: this.ownedBedTooltipLines(bedInfo.bed), kind: "grass_bed" };
return null;
},
startBirthRitual(a, b) {
if (!a || !b || a.dead || b.dead) return false;
if (!this.tarinai?.includes?.(a) || !this.tarinai?.includes?.(b)) 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;
const ritualReason = "\u3078\u3053\u3078\u3053\u3057\u3066\u3044\u308b";
const applyRitualAction = (actor, partner) => {
if (!actor) return false;
if (typeof actor.setActionState === "function") {
actor.setActionState("birth_ritual", { target: partner || null, reason: ritualReason, need: "social", subNeed: "mate", actionId: "birth_ritual", phase: "acting" });
return true;
}
actor.state = "birth_ritual";
actor.target = partner || null;
actor.thought = ritualReason;
actor.sleeping = false;
setBehaviorText(actor, { need: "social", subNeed: "mate", actionId: "birth_ritual", actionLabel: ritualReason, reasonText: ritualReason, target: partner || null, phase: "acting", source: "state" });
return true;
};
applyRitualAction(a, b);
applyRitualAction(b, a);
const aBehavior = currentTarinaiBehavior(a);
const bBehavior = currentTarinaiBehavior(b);
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 = buildReasonText("social", aBehavior?.tiedNeeds || ["social"], { id: "birth_ritual", need: "social", subNeed: "mate", label: `${b.name || "\u76f8\u624b"}\u3068\u3078\u3053\u3078\u3053\u3057\u3066\u3044\u308b` }, a, this, { causeText: aLoveCause });
const bText = buildReasonText("social", bBehavior?.tiedNeeds || ["social"], { id: "birth_ritual", need: "social", subNeed: "mate", label: `${a.name || "\u76f8\u624b"}\u3068\u3078\u3053\u3078\u3053\u3057\u3066\u3044\u308b` }, b, this, { causeText: bLoveCause });
setBehaviorText(a, { need: "social", subNeed: "mate", actionId: "birth_ritual", actionLabel: `${b.name || "\u76f8\u624b"}\u3068\u3078\u3053\u3078\u3053\u3057\u3066\u3044\u308b`, reasonText: aText, causeText: aLoveCause, target: b });
setBehaviorText(b, { need: "social", subNeed: "mate", actionId: "birth_ritual", actionLabel: `${a.name || "\u76f8\u624b"}\u3068\u3078\u3053\u3078\u3053\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;
},
cancelBirthRitual(actor, options = {}) {
const source = actor || null;
const partnerId = source?.birthPartnerId || null;
const partner = partnerId ? this.liveTarinaiById?.(partnerId) : null;
const participants = [];
if (source) participants.push(source);
if (partner && partner !== source) participants.push(partner);
if (!participants.length) return false;
const reason = options.reason || "\u7e41\u6b96\u304c\u4e2d\u65ad\u3055\u308c\u305f";
let canceled = false;
for (const t of participants) {
if (!t || t.dead) continue;
if ((t.birthRitualTimer || 0) <= 0.04 && t.state !== "birth_ritual") continue;
t.birthRitualTimer = 0;
t.birthRitualMax = 0;
t.birthPartnerId = null;
t.birthRitualLeader = false;
t.birthRitualRole = 0;
t.postBirthPeaceTimer = Math.max(t.postBirthPeaceTimer || 0, 1.4);
t.behaviorLockTimer = Math.max(t.behaviorLockTimer || 0, 0.25);
clearTarinaiBehavior(t, { reason });
clearForcedBehaviorQueue(t, e => e && (e.id === "approach_mate" || e.id === "birth_ritual"));
if (options.panic && t === source) {
if (t.enterPanic) t.enterPanic({ target: options.target || null, reason, fear: options.fear ?? 0.95, wake: true, cause: options.cause || "birth_ritual_interrupted" });
else t.setActionState?.("panic", { target: options.target || null, reason, wake: true });
t.fearTimer = Math.max(t.fearTimer || 0, options.fear ?? 0.95);
} else {
t.goIdle?.(reason);
t.thought = reason;
}
canceled = true;
}
if (canceled && !options.silentLog && this.time - (this.lastBirthCancelLogAt || -999) > 1.2) {
this.log?.(reason, "birth", { participants });
this.lastBirthCancelLogAt = this.time || 0;
}
return canceled;
},
cancelBirthRitualOnForce(tarinai, force = 0, options = {}) {
if (!tarinai || tarinai.dead) return false;
if ((tarinai.birthRitualTimer || 0) <= 0.04 && tarinai.state !== "birth_ritual") return false;
const amount = Number(force) || 0;
const threshold = Number(options.threshold || 180) || 180;
if (amount < threshold) return false;
return this.cancelBirthRitual(tarinai, {
reason: options.reason || "\u5f37\u3044\u885d\u6483\u3067\u7e41\u6b96\u304c\u4e2d\u65ad\u3055\u308c\u305f",
target: options.target || null,
fear: options.fear ?? 1.0,
cause: options.cause || "external_force",
panic: options.panic !== false,
silentLog: !!options.silentLog,
});
},
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 child = this.addTarinai({
seedParents: [a, b],
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),
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,
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;
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;
const maxForcedFightDistance = Math.max(142, (a.radius || 20) + (b.radius || 20) + 96);
if (dist(a, b) > maxForcedFightDistance) return false;
if ((a.fightCooldown || 0) > 0.04 || (b.fightCooldown || 0) > 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.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, deterministicRange(this, "fight-mochi-intimidate-actor-timer", 0.85, 1.35, actor, target));
actor.intimidateTargetId = target.id;
actor.surpriseTimer = Math.max(actor.surpriseTimer || 0, 0.28);
target.intimidatedTimer = Math.max(target.intimidatedTimer || 0, deterministicRange(this, "fight-mochi-intimidate-target-timer", 0.75, 1.25, actor, target));
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) + deterministicRange(this, "conflict-score-a", -5, 5, a, b);
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) + deterministicRange(this, "conflict-score-b", -5, 5, a, b);
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 intimidationChance = 0.30;
if (canIntimidate && deterministicChance(this, "conflict-intimidation-choice", intimidationChance, actor, target)) {
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) + deterministicRange(this, "intimidation-resistance", -5, 5, actor, target);
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 + deterministicRange(this, "intimidation-actor-cooldown", 1, 4, actor, target);
target.fightCooldown = CONFIG.fightCooldown + deterministicRange(this, "intimidation-target-cooldown", 1, 4, actor, target);
actor.intimidateTimer = Math.max(actor.intimidateTimer, deterministicRange(this, "intimidation-actor-timer", 1.05, 1.65, actor, target));
actor.intimidateTargetId = target.id;
actor.setActionState("intimidate", { target, reason: "\u76f8\u624b\u3092\u5a01\u5687\u3057\u3066\u3044\u308b" });
setBehaviorText(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, deterministicRange(this, "intimidation-target-prep-timer", 0.95, 1.55, actor, target));
target.fearTimer = Math.max(target.fearTimer, 0.65 * target.personalityProfile().fear);
if (!deterministicChance(this, "intimidation-success", successChance, actor, target)) {
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, deterministicRange(this, "intimidation-defeated-timer", 1.8, 2.8, actor, target));
target.intimidatedTimer = Math.max(target.intimidatedTimer || 0, deterministicRange(this, "intimidation-success-timer", 1.2, 2.0, actor, target));
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) * deterministicRange(this, "intimidation-knockback-x", 26, 54, actor, target);
target.vy += deterministicRange(this, "intimidation-knockback-y", -18, 18, actor, target);
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;
const maxStartFightDistance = Math.max(150, (a.radius || 20) + (b.radius || 20) + 104);
if (dist(a, b) > maxStartFightDistance) return false;
this.markFightPairCooldown?.(a, b, CONFIG.fightCooldown + 6);
const initiator = opts?.initiator === b ? b : a;
const receiver = initiator === a ? b : a;
const causeFor = (self, other) => {
if (((currentTarinaiBehavior(self))?.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) => buildReasonText("social", getTarinaiBehaviorTiedNeeds(self, "social") || ["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) });
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 });
setBehaviorText(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" });
setBehaviorText(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, deterministicRange(this, "fight-start-timer-a", 3.2, 4.8, a, b));
b.fightTimer = Math.max(b.fightTimer, deterministicRange(this, "fight-start-timer-b", 3.2, 4.8, a, b));
a.nextHeadbutt = this.time + deterministicRange(this, "fight-start-headbutt", 0.08, 0.18, a, b);
b.nextHeadbutt = a.nextHeadbutt;
a.fightCooldown = CONFIG.fightCooldown + deterministicRange(this, "fight-start-cooldown-a", 1, 5, a, b);
b.fightCooldown = CONFIG.fightCooldown + deterministicRange(this, "fight-start-cooldown-b", 1, 5, a, b);
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(deterministicRange(this, "fight-start-stress-a", 8, 18, a, b), { threshold: 8 });
if (b.addStress) b.addStress(deterministicRange(this, "fight-start-stress-b", 8, 18, a, b), { threshold: 8 });
const rawDamageToA = deterministicRange(this, "fight-pulse-damage-a", 2, 7, a, b);
const rawDamageToB = deterministicRange(this, "fight-pulse-damage-b", 2, 7, a, b);
const damageToA = b.outgoingDamage ? b.outgoingDamage(rawDamageToA) : rawDamageToA;
const damageToB = a.outgoingDamage ? a.outgoingDamage(rawDamageToB) : rawDamageToB;
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 * deterministicRange(this, "fight-pulse-knockback-ax", 32, 62, a, b); a.vy -= dy / d * deterministicRange(this, "fight-pulse-knockback-ay", 32, 62, a, b);
b.vx += dx / d * deterministicRange(this, "fight-pulse-knockback-bx", 32, 62, a, b); b.vy += dy / d * deterministicRange(this, "fight-pulse-knockback-by", 32, 62, a, b);
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: deterministicRange(this, "fight-pulse-effect-size", 12, 20, a, b),
life: deterministicRange(this, "fight-pulse-effect-life", 0.32, 0.52, a, b),
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], sound: false });
a.lastLog = b.lastLog = this.time;
}
return true;
}
}));
})(typeof window !== "undefined" ? window : globalThis);