"use strict"; 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 = {}) { this.world = world; this.familyKey = opts.familyKey || opts.archiveKey || opts.id || (crypto.randomUUID ? crypto.randomUUID() : `${Date.now()}-${Math.random()}`); this.id = opts.id || ""; this.liveToken = opts.liveToken || 0; this.name = opts.name || makeName(world); // Legacy string personality is intentionally ignored. The game now uses only the four-axis personality model. this.personality = ""; this.type = opts.type || baseSpriteId(); const typeMeta = SPRITES.find(s => s.id === this.type); if (typeMeta?.actionOnly || typeMeta?.displayOnly) this.type = baseSpriteId(); this.birthPersonality = normalizePersonality(opts.birthPersonality || opts.personalityBirth); if (!opts.birthPersonality && !opts.personalityBirth) this.birthPersonality = randomBirthPersonality(this.familyKey); this.currentPersonality = normalizePersonality(opts.currentPersonality || opts.personalityCurrent || this.birthPersonality); this.personalityDaily = opts.personalityDaily && typeof opts.personalityDaily === "object" ? JSON.parse(JSON.stringify(opts.personalityDaily)) : null; ensurePersonality(this); this.genetics = normalizeGenetics(opts.genetics, this.familyKey); 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.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 : adultScaleFromGenetics(this.familyKey, 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 ?? rand(1400, 2200) * (this.genetics.lifeSpanMul || 1); if (this.lifeSpan < 900) this.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); this.energy = opts.energy ?? rand(54, 98); this.stress = opts.stress ?? rand(0, 20); this.hpBarTimer = opts.hpBarTimer ?? 0; this.stressBarTimer = opts.stressBarTimer ?? 0; this.affection = opts.affection ?? rand(0, 30); 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.previousNeeds = opts.previousNeeds && typeof opts.previousNeeds === "object" ? { ...this.needs, ...opts.previousNeeds } : { ...this.needs }; this.needsInitialized = Boolean(opts.needsInitialized); this.needShock = opts.needShock && typeof opts.needShock === "object" ? { ...defaultNeeds, ...opts.needShock } : { ...defaultNeeds }; this.need = opts.need || opts.intent?.need || "fulfill"; this.intent = opts.intent && typeof opts.intent === "object" ? { ...opts.intent } : { need: this.need, tiedNeeds: [this.need], actionId: "idle", actionLabel: "\u5f85\u3063\u3066\u3044\u308b", reasonText: "\u5f85\u3063\u3066\u3044\u308b", }; this.panicTimer = opts.panicTimer ?? 0; this.buildTimer = opts.buildTimer ?? 0; this.buildType = opts.buildType || ""; this.buildMaterialId = opts.buildMaterialId || ""; this.state = opts.state || "idle"; this.dead = !!opts.dead; 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.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 ?? stableChoice(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 = 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.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 ?? rand(0, CONFIG.fightCooldown); this.fightTargetId = opts.fightTargetId || null; this.fightTargetIds = Array.isArray(opts.fightTargetIds) ? [...opts.fightTargetIds] : (this.fightTargetId ? [this.fightTargetId] : []); this.nextHeadbutt = opts.nextHeadbutt ?? 0; this.digest = opts.digest ?? rand(0, 2.4); 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;