tarinai/js/tarinai.js
2026-06-27 19:22:38 +09:00

276 lines
19 KiB
JavaScript

"use strict";
(function ensureDeterministicHelpers(global) {
if (typeof global.deterministicChance === "function" && typeof global.deterministicRange === "function") return;
const clamp01 = v => Math.max(0, Math.min(1, Number(v) || 0));
const keyPart = value => {
if (value == null) return "null";
if (typeof value === "number") return Number.isFinite(value) ? value.toFixed(3) : "nan";
if (typeof value === "string" || typeof value === "boolean") return String(value);
if (typeof value === "object") {
const id = value.familyKey || value.id || value.seed || value.liveToken || value.name;
if (id) return `${value.type || value.constructor?.name || "entity"}:${id}`;
const x = Number.isFinite(value.x) ? value.x.toFixed(1) : "x";
const y = Number.isFinite(value.y) ? value.y.toFixed(1) : "y";
return `${value.type || value.constructor?.name || "entity"}@${x},${y}`;
}
return String(value);
};
const hashUnit = (seed, salt = "") => {
const str = `${seed}:${salt}`;
let h = 2166136261;
for (let i = 0; i < str.length; i++) { h ^= str.charCodeAt(i); h = Math.imul(h, 16777619); }
return ((h >>> 0) % 100000) / 100000;
};
global.deterministicFrame = global.deterministicFrame || ((worldRef = null, hz = 60) => Math.floor((Number(worldRef?.time || 0) || 0) * Math.max(1, Number(hz) || 60) + 1e-6));
global.deterministicUnit = global.deterministicUnit || ((worldRef = null, salt = "", ...parts) => hashUnit(String(worldRef?.worldSeed || "tarinai-world"), [salt, global.deterministicFrame(worldRef, 60), Number(worldRef?._deterministicEpoch || 0) || 0, ...parts.map(keyPart)].join("|")));
global.deterministicRange = global.deterministicRange || ((worldRef = null, salt = "", min = 0, max = 1, ...parts) => (Number(min) || 0) + global.deterministicUnit(worldRef, salt, ...parts) * ((Number(max) || 0) - (Number(min) || 0)));
global.deterministicSigned = global.deterministicSigned || ((worldRef = null, salt = "", ...parts) => global.deterministicUnit(worldRef, salt, ...parts) < 0.5 ? -1 : 1);
global.deterministicChance = global.deterministicChance || ((worldRef = null, salt = "", chance = 0, ...parts) => { const p = clamp01(chance); return p >= 1 || (p > 0 && global.deterministicUnit(worldRef, salt, ...parts) < p); });
global.deterministicAngle = global.deterministicAngle || ((worldRef = null, salt = "", ...parts) => global.deterministicRange(worldRef, salt, 0, Math.PI * 2, ...parts));
})(typeof window !== "undefined" ? window : globalThis);
function tarinaiRoundRectPath(ctx, x, y, w, h, r) {
const rr = Math.max(0, Math.min(r || 0, Math.abs(w) / 2, Math.abs(h) / 2));
ctx.beginPath();
ctx.moveTo(x + rr, y);
ctx.lineTo(x + w - rr, y);
ctx.quadraticCurveTo(x + w, y, x + w, y + rr);
ctx.lineTo(x + w, y + h - rr);
ctx.quadraticCurveTo(x + w, y + h, x + w - rr, y + h);
ctx.lineTo(x + rr, y + h);
ctx.quadraticCurveTo(x, y + h, x, y + h - rr);
ctx.lineTo(x, y + rr);
ctx.quadraticCurveTo(x, y, x + rr, y);
}
class Tarinai { constructor(world, opts = {}) {
if (world && !opts.birthSeed && !opts.familyKey && !opts.id && globalThis.TarinaiSeedFactory?.prepareAddOptions) {
opts = globalThis.TarinaiSeedFactory.prepareAddOptions(world, opts) || opts;
}
this.world = world;
this.birthSeed = opts.birthSeed || opts.familyKey || opts.id || (globalThis.TarinaiSeedFactory?.childSeed?.(world?.worldSeed || "", Number(world?.birthSerial || 0) + 1, []) || (crypto.randomUUID ? crypto.randomUUID() : `${Date.now()}-${Math.random()}`));
this.birthSerial = Number.isFinite(opts.birthSerial) ? Number(opts.birthSerial) : (globalThis.TarinaiSeedFactory?.serialFromBirthSeed?.(this.birthSeed) || 0);
const birthProfile = globalThis.TarinaiSeedFactory?.birthProfile?.(this.birthSeed, opts) || {};
this.familyKey = opts.familyKey || this.birthSeed;
this.id = opts.id || "";
this.liveToken = opts.liveToken || 0;
this.name = opts.name || birthProfile.name || makeName(world);
this.personality = "";
this.type = opts.type || birthProfile.type || baseSpriteId();
const typeMeta = SPRITES.find(s => s.id === this.type);
if (typeMeta?.actionOnly || typeMeta?.displayOnly) this.type = birthProfile.type || baseSpriteId();
this.birthPersonality = normalizePersonality(opts.birthPersonality || birthProfile.birthPersonality);
this.currentPersonality = normalizePersonality(opts.currentPersonality || this.birthPersonality);
this.personalityDaily = opts.personalityDaily && typeof opts.personalityDaily === "object" ? JSON.parse(JSON.stringify(opts.personalityDaily)) : null;
ensurePersonality(this);
this.genetics = normalizeGenetics(opts.genetics || birthProfile.genetics, this.birthSeed);
this.trait = makeTrait(this.type);
this.trait.speed *= this.genetics.speedMul || 1;
this.birthTime = opts.birthTime ?? this.world.time;
const generationOpt = opts.generation ?? 1;
const growthAtConstruct = generationOpt > 1 ? clamp(((this.world?.time || 0) - this.birthTime) / Math.max(1, CONFIG.childGrowTime || 120), 0, 1) : 1;
this.x = opts.x ?? rand(100, world.w - 100);
this.y = opts.y ?? rand(100, world.h - 100);
this._lastValidX = Number.isFinite(this.x) ? this.x : null;
this._lastValidY = Number.isFinite(this.y) ? this.y : null;
this._lastSunbathDrawX = Number.isFinite(opts._lastSunbathDrawX) ? opts._lastSunbathDrawX : null;
this._lastSunbathDrawY = Number.isFinite(opts._lastSunbathDrawY) ? opts._lastSunbathDrawY : null;
this._activeSunbathSpriteId = typeof opts._activeSunbathSpriteId === "string" ? opts._activeSunbathSpriteId : "";
this.vx = opts.vx ?? rand(-12, 12);
this.vy = opts.vy ?? rand(-12, 12);
this.impulseVx = opts.impulseVx ?? 0;
this.impulseVy = opts.impulseVy ?? 0;
this.adultScale = Number.isFinite(opts.adultScale) ? opts.adultScale : (Number.isFinite(opts.baseAdultScale) ? opts.baseAdultScale : (Number.isFinite(opts.scale) && growthAtConstruct >= 1 ? opts.scale : (Number.isFinite(birthProfile.adultScale) ? birthProfile.adultScale : adultScaleFromGenetics(this.birthSeed, this.genetics))));
this.scale = Number.isFinite(opts.scale) ? opts.scale : (generationOpt > 1 ? scaleForGrowth(this.adultScale, growthAtConstruct) : this.adultScale);
this.radius = 56 * this.scale;
this.age = opts.age ?? rand(0, 20);
this.lifeSpan = opts.lifeSpan ?? (Number.isFinite(birthProfile.lifeSpan) ? birthProfile.lifeSpan : rand(1400, 2200) * (this.genetics.lifeSpanMul || 1));
if (this.lifeSpan < 900) this.lifeSpan = Number.isFinite(birthProfile.lifeSpan) ? birthProfile.lifeSpan : rand(1200, 1800) * (this.genetics.lifeSpanMul || 1);
this.generation = generationOpt;
this.hasPaired = opts.hasPaired ?? false;
this.hunger = opts.hunger ?? rand(16, 45);
this.loneliness = opts.loneliness ?? rand(18, 62);
const initialDayP = world?.dayProgress?.() || 0;
const initialNight = initialDayP >= 0.66 || initialDayP < 0.18;
const initialAfternoon = initialDayP >= 0.46 && initialDayP < 0.58;
this.energy = opts.energy ?? (initialNight ? rand(34, 68) : (initialAfternoon ? rand(50, 82) : rand(66, 98)));
this.circadianSleepPressure = Number.isFinite(opts.circadianSleepPressure) ? opts.circadianSleepPressure : (initialNight ? rand(58, 78) : (initialAfternoon ? rand(8, 18) : rand(0, 5)));
const defaultNeeds = typeof createDefaultNeeds === "function" ? createDefaultNeeds() : { food: 0, sleep: 0, health: 0, safety: 0, social: 0, fulfill: 0 };
this.needs = opts.needs && typeof opts.needs === "object" ? { ...defaultNeeds, ...opts.needs } : { ...defaultNeeds };
this.needDisplay = opts.needDisplay && typeof opts.needDisplay === "object" ? { ...defaultNeeds, ...opts.needDisplay } : { ...this.needs };
this.needRaw = opts.needRaw && typeof opts.needRaw === "object" ? { ...defaultNeeds, ...opts.needRaw } : { ...this.needs };
this.needRawBase = opts.needRawBase && typeof opts.needRawBase === "object" ? { ...defaultNeeds, ...opts.needRawBase } : { ...this.needRaw };
this.needSatisfaction = opts.needSatisfaction && typeof opts.needSatisfaction === "object" ? { ...defaultNeeds, ...opts.needSatisfaction } : { ...defaultNeeds };
this.lastSatisfiedAt = opts.lastSatisfiedAt && typeof opts.lastSatisfiedAt === "object" ? { ...opts.lastSatisfiedAt } : {};
this.lastSatisfactionSource = opts.lastSatisfactionSource || "";
this.stress = typeof calculateStressFromNeeds === "function" ? calculateStressFromNeeds(this.needRaw || this.needs) : 0;
this.previousNeeds = opts.previousNeeds && typeof opts.previousNeeds === "object" ? { ...defaultNeeds, ...opts.previousNeeds } : { ...this.needs };
this.need = "";
this.state = opts.state || "idle";
this.behaviorSerial = opts.behaviorSerial ?? 0;
this.behavior = typeof globalThis.normalizeTarinaiBehaviorState === "function"
? globalThis.normalizeTarinaiBehaviorState(opts, this)
: (opts.behavior && typeof opts.behavior === "object" ? { ...opts.behavior } : null);
this.forcedBehaviorQueue = Array.isArray(opts.forcedBehaviorQueue) ? opts.forcedBehaviorQueue.map(v => ({ ...(v || {}) })).slice(-8) : [];
this.actionCooldowns = opts.actionCooldowns && typeof opts.actionCooldowns === "object" ? { ...opts.actionCooldowns } : {};
this.needShock = opts.needShock && typeof opts.needShock === "object" ? { ...opts.needShock } : {};
this.buildPlan = opts.buildPlan && typeof opts.buildPlan === "object" ? { ...opts.buildPlan } : null;
this.sleepSession = opts.sleepSession && typeof opts.sleepSession === "object" ? { ...opts.sleepSession } : null;
this.hpBarTimer = opts.hpBarTimer ?? 0;
this.stressBarTimer = opts.stressBarTimer ?? 0;
this.affection = opts.affection ?? rand(0, 30);
this.dead = !!opts.dead;
this.lifeStatus = opts.lifeStatus || (this.dead ? "dead" : "alive");
this.deathReason = opts.deathReason || "";
this.lastDamageCause = opts.lastDamageCause || "";
this.lastDamageAmount = opts.lastDamageAmount ?? 0;
this.lastDamageAt = opts.lastDamageAt ?? -999;
this.lastMajorDamageCause = opts.lastMajorDamageCause || "";
this.lastMajorDamageAmount = opts.lastMajorDamageAmount ?? 0;
this.lastMajorDamageAt = opts.lastMajorDamageAt ?? -999;
this.thought = "";
this.target = null;
this.targetKey = "";
this.targetSince = 0;
this.targetBestDist = Infinity;
this.targetGiveupKey = opts.targetGiveupKey || "";
this.targetGiveupUntil = opts.targetGiveupUntil ?? 0;
this.panicTarget = opts.panicTarget || null;
this.panicTargetUntil = opts.panicTargetUntil ?? 0;
this.panicStuckTimer = opts.panicStuckTimer ?? 0;
this.wanderAngle = rand(0, Math.PI * 2);
this.wanderTarget = opts.wanderTarget && typeof opts.wanderTarget === "object" ? { ...opts.wanderTarget } : null;
this.reproductionTimer = rand(4, CONFIG.reproductionCooldown);
this.lastLog = 0;
this.sleeping = false;
this.awakeLockTimer = opts.awakeLockTimer ?? 0;
this.wasSleepingLastTick = false;
this.blink = rand(0, 10);
this.lastSocial = 0;
this.goodMode = opts.goodMode ?? birthProfile.goodMode ?? stableChoice(this.birthSeed || this.familyKey, "good-mode", displayNormalSprites()) ?? "smile";
this.facing = opts.facing ?? (stableUnit(this.familyKey, "facing") < 0.5 ? -1 : 1);
this.visualFacing = opts.visualFacing ?? this.facing;
this.facingLockUntil = opts.facingLockUntil ?? 0;
this.eatTimer = opts.eatTimer ?? 0;
this.eatCooldown = opts.eatCooldown ?? 0;
this.lastAteAt = Number.isFinite(opts.lastAteAt) ? opts.lastAteAt : (this.world?.time || 0);
this.mealCooldownUntil = Number.isFinite(opts.mealCooldownUntil) ? opts.mealCooldownUntil : 0;
this.foodReactTimer = opts.foodReactTimer ?? 0;
this.surpriseTimer = opts.surpriseTimer ?? 0;
this.fearTimer = opts.fearTimer ?? 0;
this.sunbathTimer = opts.sunbathTimer ?? 0;
this.sunbathCooldown = opts.sunbathCooldown ?? 0;
this.sunbathFrameTimer = opts.sunbathFrameTimer ?? 0;
this.sunbathSpriteVariant = opts.sunbathSpriteVariant ?? 1;
this.sunbathAnchorX = Number.isFinite(opts.sunbathAnchorX) ? opts.sunbathAnchorX : null;
this.sunbathAnchorY = Number.isFinite(opts.sunbathAnchorY) ? opts.sunbathAnchorY : null;
this.intimidateTimer = opts.intimidateTimer ?? 0;
this.intimidateTargetId = opts.intimidateTargetId || null;
this.intimidatedTimer = opts.intimidatedTimer ?? 0;
this.birthRitualTimer = opts.birthRitualTimer ?? 0;
this.birthRitualMax = opts.birthRitualMax ?? 0;
this.birthPartnerId = opts.birthPartnerId || null;
this.birthRitualRole = opts.birthRitualRole || 0;
this.birthRitualLeader = !!opts.birthRitualLeader;
this.pokeFlashTimer = opts.pokeFlashTimer ?? 0;
this.zunchiStain = opts.zunchiStain ?? 0;
this.zunchiStainSeed = opts.zunchiStainSeed ?? stableUnit(this.familyKey, "zunchi-stain");
this.zunchiDisease = Boolean(opts.zunchiDisease);
this.zunchiDiseaseSeverity = opts.zunchiDiseaseSeverity ?? (this.zunchiDisease ? 1 : 0);
this.zunchiDiseaseCooldown = opts.zunchiDiseaseCooldown ?? 0;
this.totalFightLosses = opts.totalFightLosses || 0;
this.totalFightWins = opts.totalFightWins || 0;
this.isZunchiSlave = Boolean(opts.isZunchiSlave || opts.zunchiSlaveLocked);
this.zunchiSlaveLocked = Boolean(opts.zunchiSlaveLocked || this.isZunchiSlave);
this.formerName = opts.formerName || null;
this.loveMochiTimer = opts.loveMochiTimer ?? 0;
this.fightMochiTimer = opts.fightMochiTimer ?? 0;
this.sleepDisease = Boolean(opts.sleepDisease);
this.sleepDiseaseCooldown = opts.sleepDiseaseCooldown ?? 0;
this.explosionDisease = Boolean(opts.explosionDisease);
this.explosionDiseaseTimer = opts.explosionDiseaseTimer ?? (this.explosionDisease ? 60 : 0);
this.fightDisease = Boolean(opts.fightDisease);
this.fightDiseaseCooldown = opts.fightDiseaseCooldown ?? 0;
this.lastBleedAt = opts.lastBleedAt ?? -999;
this.favorite = Boolean(opts.favorite);
this.insideNestBoxId = opts.insideNestBoxId || null;
this.nestFade = opts.nestFade ?? (this.insideNestBoxId ? 1 : 0);
this.nestBoxEnteredAt = opts.nestBoxEnteredAt ?? (this.insideNestBoxId ? (world?.time || 0) : -Infinity);
this.nestBoxStayUntil = opts.nestBoxStayUntil ?? (this.insideNestBoxId ? ((world?.time || 0) + 10) : -Infinity);
this.focusPulseTimer = opts.focusPulseTimer ?? 0;
if (this.isZunchiSlave) {
this.zunchiSlaveLocked = true;
this.name = "\u305a\u3093\u3061\u3069\u308c\u3044";
applyZunchiSlavePersonality(this, { record: false, birth: false });
this.visibleSpriteId = "zunchi_slave";
}
this.tempComfort = opts.tempComfort ?? 0.62;
this.tempTimer = opts.tempTimer ?? stableUnit(this.familyKey, "temp-start") * 0.55;
this.parents = opts.parents || [];
this.parentNames = opts.parentNames || [];
this.children = opts.children || [];
this.nextEatSound = opts.nextEatSound ?? 0;
this.fightTimer = opts.fightTimer ?? 0;
this.fightCooldown = opts.fightCooldown ?? deterministicRange(world, "initial-fight-cooldown", 0, CONFIG.fightCooldown, this.familyKey || this.id || opts.birthSeed);
this.fightTargetId = opts.fightTargetId || null;
this.fightTargetIds = Array.isArray(opts.fightTargetIds) ? [...opts.fightTargetIds] : (this.fightTargetId ? [this.fightTargetId] : []);
this.nextHeadbutt = opts.nextHeadbutt ?? 0;
const savedDigest = Number(opts.digest);
this.digest = Number.isFinite(savedDigest)
? (savedDigest > 3 ? Math.min(2.99, savedDigest / 12.4 * 3) : Math.max(0, savedDigest))
: 0;
this.poopCount = opts.poopCount ?? 0;
this.oshiriByoZunchiStock = opts.oshiriByoZunchiStock ?? 0;
this.stretchTimer = opts.stretchTimer ?? 0;
this.lastMealColor = opts.lastMealColor || "#f7cf67";
this.grassEatTimer = opts.grassEatTimer ?? 0;
this.hurtTimer = opts.hurtTimer ?? 0;
this.stuckPushpinId = opts.stuckPushpinId || null;
this.defeatedTimer = opts.defeatedTimer ?? 0;
this.defeatedById = opts.defeatedById || null;
this.fightWinnerId = opts.fightWinnerId || null;
this.relationships = opts.relationships && typeof opts.relationships === "object" ? JSON.parse(JSON.stringify(opts.relationships)) : {};
this.fallTimer = opts.fallTimer ?? 0;
this.fallMax = opts.fallMax ?? 1.15;
this.fallDir = opts.fallDir ?? (stableUnit(this.familyKey, "fall-dir") < 0.5 ? -1 : 1);
this.blastSpinTimer = opts.blastSpinTimer ?? 0;
this.blastSpinMax = opts.blastSpinMax ?? 0;
this.postBirthPeaceTimer = opts.postBirthPeaceTimer ?? 0;
this.sleepStart = 0.60 + stableUnit(this.familyKey, "sleep-start") * 0.22;
this.sleepEnd = 0.15 + stableUnit(this.familyKey, "sleep-end") * 0.14;
this.sleepFacing = stableUnit(this.familyKey, "sleep-facing") < 0.5 ? -1 : 1;
this.nextBubbleAt = 0;
this.nextSleepBubbleAt = 0;
this.nextFearBubbleAt = 0;
this.nextCursorHeartAt = opts.nextCursorHeartAt ?? 0;
this.cursorPetting = opts.cursorPetting ?? 0;
this.aiTimer = opts.aiTimer ?? stableUnit(this.familyKey, "ai-start") * 0.22;
this.envTimer = opts.envTimer ?? stableUnit(this.familyKey, "env-start") * 0.55;
this.visibleSpriteId = opts.visibleSpriteId || null;
this.zunchiSlaveSpriteVariant = opts.zunchiSlaveSpriteVariant || (opts.isZunchiSlave ? (Math.random() < 0.5 ? "zunchi_slave" : "zunchi_slave_alt") : "");
this.spriteLockUntil = opts.spriteLockUntil ?? 0;
this.entryTimer = opts.entryTimer ?? 0;
const effectTimers = opts.itemEffectTimers && typeof opts.itemEffectTimers === "object" ? opts.itemEffectTimers : {};
this.itemEffectTimers = {
laxative: Math.max(0, Number(effectTimers.laxative || opts.laxativeTimer || 0) || 0),
ammo: Math.max(0, Number(effectTimers.ammo || opts.ammoTimer || 0) || 0),
};
this.laxativePoopTimer = Number.isFinite(opts.laxativePoopTimer) ? opts.laxativePoopTimer : 3;
this.ammoBumpCooldown = opts.ammoBumpCooldown || 0;
this.powerItemMode = opts.powerItemMode === "protein" || opts.powerItemMode === "niteropu" ? opts.powerItemMode : "";
this.sizeItemMode = opts.sizeItemMode === "giant_drug" || opts.sizeItemMode === "dwarf_drug" ? opts.sizeItemMode : "";
this.lifeItemMode = opts.lifeItemMode === "mercury" ? opts.lifeItemMode : "";
this.mercuryLifeSpanBase = Number.isFinite(opts.mercuryLifeSpanBase) ? opts.mercuryLifeSpanBase : null;
this.mercuryLifeMultiplier = Number.isFinite(opts.mercuryLifeMultiplier) ? Math.max(1, opts.mercuryLifeMultiplier) : (this.lifeItemMode === "mercury" ? 1.3 : 1);
this.itemDamageTimer = Number.isFinite(opts.itemDamageTimer) ? opts.itemDamageTimer : 1;
this.powerEffectPulse = opts.powerEffectPulse || 0;
this.powerEffectKind = opts.powerEffectKind || this.powerItemMode || this.sizeItemMode || this.lifeItemMode || "";
this.mysteryDrugHistory = Array.isArray(opts.mysteryDrugHistory) ? opts.mysteryDrugHistory.slice(-8) : [];
this.recentChangeCauses = Array.isArray(opts.recentChangeCauses) ? opts.recentChangeCauses.slice(-8) : [];
if (this.refreshEffectiveSize) this.refreshEffectiveSize();
}
}
if (typeof window !== "undefined") window.Tarinai = Tarinai;
if (typeof globalThis !== "undefined") globalThis.Tarinai = Tarinai;