"use strict"; function tarinaiSpriteReversesFacing(sprite) { const id = String(sprite || ""); return id === "back" || id === "tarinai12_back" || /(^|_)12_back$/i.test(id); } (function (global) { const Tarinai = global.Tarinai; if (!Tarinai) throw new Error("Tarinai is not available for mixin: tarinai_render.js"); function shouldShowTarinaiDecor(tarinai) { const worldRef = tarinai?.world; if (!tarinai || !worldRef) return true; if (worldRef.selected === tarinai || tarinai.favorite) return true; if ((tarinai.hurtTimer || 0) > 0.04 || (tarinai.eatTimer || 0) > 0.02 || (tarinai.pokeFlashTimer || 0) > 0.03) return true; if ((tarinai.focusPulseTimer || 0) > 0.04 || tarinai.state === "cursor_friend" || tarinai.state === "cursor_enemy") return true; const tier = window.TarinaiPerf.renderQualityTier() || "high"; if (tier === "low" && worldRef._mechanicalCrowdPressure?.active) return false; if (tier === "low") return false; if (tier === "mid") { const p = worldRef.pointer; return Boolean(p?.inside && typeof distXY === "function" && distXY(tarinai.x, tarinai.y, p.x, p.y) < tarinai.radius * 2.0); } return true; } function tarinaiShadowsEnabled() { return (global.TarinaiPerf?.shadowQualityTier?.() || "high") !== "off"; } function drawTarinaiChampionCrown(ctx, tarinai, drawH, nestHideAlpha = 1, showDecor = true) { if (!ctx || !tarinai?.isTarinaiChampion || tarinai.dead) return false; const alpha = clamp(Number(nestHideAlpha ?? 1) || 0, 0, 1); if (alpha <= 0.01) return false; ctx.save(); ctx.globalAlpha = alpha; const crownW = Math.max(24, tarinai.radius * 1.34); const crownH = Math.max(14, tarinai.radius * 0.58); const cx = tarinai.x; const cy = tarinai.y - drawH * 0.64 - crownH * 0.55; if (showDecor) { ctx.shadowColor = "rgba(244, 184, 42, 0.78)"; ctx.shadowBlur = 12; } ctx.fillStyle = "rgba(248, 201, 55, 0.96)"; ctx.strokeStyle = "rgba(116, 78, 18, 0.92)"; ctx.lineWidth = Math.max(1.4, crownH * 0.10); ctx.beginPath(); ctx.moveTo(cx - crownW * 0.48, cy + crownH * 0.26); ctx.lineTo(cx - crownW * 0.38, cy - crownH * 0.22); ctx.lineTo(cx - crownW * 0.17, cy + crownH * 0.05); ctx.lineTo(cx, cy - crownH * 0.42); ctx.lineTo(cx + crownW * 0.17, cy + crownH * 0.05); ctx.lineTo(cx + crownW * 0.38, cy - crownH * 0.22); ctx.lineTo(cx + crownW * 0.48, cy + crownH * 0.26); ctx.closePath(); ctx.fill(); ctx.stroke(); ctx.shadowBlur = 0; ctx.fillStyle = "rgba(255, 243, 154, 0.92)"; ctx.fillRect(cx - crownW * 0.38, cy + crownH * 0.16, crownW * 0.76, crownH * 0.16); ctx.fillStyle = "rgba(151, 88, 38, 0.82)"; for (const px of [-0.28, 0, 0.28]) { ctx.beginPath(); ctx.arc(cx + crownW * px, cy + crownH * 0.02, Math.max(1.4, crownH * 0.075), 0, Math.PI * 2); ctx.fill(); } ctx.restore(); return true; } Object.defineProperties(Tarinai.prototype, Object.getOwnPropertyDescriptors({ drawChampionCrown(ctx, t = 0, lighting = null) { if (!this.isTarinaiChampion || this.dead) return false; const nestHideAlpha = this.nestFade ? clamp(1 - this.nestFade, 0, 1) : 1; if (nestHideAlpha <= 0.01) return false; const sprite = this.visibleSpriteId || this.goodMode || this.type || "smile"; const metrics = getImageMetrics(sprite) || getImageMetrics("smile") || {}; const visualScale = this.effectiveScale ? this.effectiveScale() : this.scale; const drawH = (metrics?.h || 512) * visualScale * 0.30; return drawTarinaiChampionCrown(ctx, this, drawH, nestHideAlpha, shouldShowTarinaiDecor(this)); }, drawSunbathPose(ctx, t, lighting = null) { if (this.dead) return true; const active = this.state === "sunbath" || (this.sunbathTimer || 0) > 0.04; if (!active) return false; this.preloadSunbathSprites?.(); const ids = this.sunbathSpriteCandidates ? this.sunbathSpriteCandidates() : ["sunbath_1", "sunbath_2", "sunbath_3"]; const candidates = (Array.isArray(ids) && ids.length ? ids : ["sunbath_1", "sunbath_2", "sunbath_3"]).filter(id => String(id || "").startsWith("sunbath")); const ready = (id) => isImageReady(id); const imageFor = (id) => id ? getRenderableImage(id, "") : null; let requested = this.sunbathSpriteId ? this.sunbathSpriteId() : this.normalizeSunbathSpriteId?.(this.sunbathSpriteVariant); if (!requested || !candidates.includes(requested)) requested = candidates[0] || "sunbath_1"; const locked = this._activeSunbathSpriteId && candidates.includes(this._activeSunbathSpriteId) && ready(this._activeSunbathSpriteId) ? this._activeSunbathSpriteId : ""; const ordered = [locked, requested, this._lastRenderableSunbathSpriteId, ...candidates].filter(Boolean); let renderSprite = ordered.find(id => candidates.includes(id) && ready(id)); let img = renderSprite ? imageFor(renderSprite) : null; if (img && ready(renderSprite)) { this._activeSunbathSpriteId = renderSprite; this._lastRenderableSunbathSpriteId = renderSprite; this.sunbathSpriteVariant = renderSprite; this.visibleSpriteId = renderSprite; } else { const fallbackId = this._lastNonSunbathSpriteId || this.goodMode || this.type || "smile"; renderSprite = fallbackId; img = getRenderableImage(fallbackId, "smile"); if (!img) { renderSprite = "smile"; img = getRenderableImage("smile", "smile"); } } const worldW = Math.max(1, Number(this.world?.w || 1000) || 1000); const worldH = Math.max(1, Number(this.world?.h || 720) || 720); const pad = Math.max(28, CONFIG.worldPadding || 30); const lastX = Number.isFinite(this._lastSunbathDrawX) ? this._lastSunbathDrawX : (Number.isFinite(this._lastValidX) ? this._lastValidX : NaN); const lastY = Number.isFinite(this._lastSunbathDrawY) ? this._lastSunbathDrawY : (Number.isFinite(this._lastValidY) ? this._lastValidY : NaN); let drawX = Number.isFinite(this.x) ? this.x : (Number.isFinite(this.sunbathAnchorX) ? this.sunbathAnchorX : (Number.isFinite(lastX) ? lastX : pad)); let drawY = Number.isFinite(this.y) ? this.y : (Number.isFinite(this.sunbathAnchorY) ? this.sunbathAnchorY : (Number.isFinite(lastY) ? lastY : pad)); drawX = clamp(drawX, pad, Math.max(pad, worldW - pad)); drawY = clamp(drawY, pad, Math.max(pad, worldH - pad)); this.x = drawX; this.y = drawY; this.sunbathAnchorX = drawX; this.sunbathAnchorY = drawY; this._lastSunbathDrawX = drawX; this._lastSunbathDrawY = drawY; this._lastValidX = drawX; this._lastValidY = drawY; const metrics = getImageMetrics(renderSprite) || getImageMetrics(img) || getImageMetrics("smile") || {}; const w = metrics?.w || img?.naturalWidth || img?.width || 512; const h = metrics?.h || img?.naturalHeight || img?.height || 512; const visualScale = this.effectiveScale ? this.effectiveScale() : this.scale; const drawW = w * visualScale * 0.30; const drawH = h * visualScale * 0.30; const breathing = Math.sin(t * 1.25 + this.age * 0.18); const bob = breathing * 0.55; const faceRight = this.facingDir ? this.facingDir() > 0 : (this.facing || 1) > 0; const nestHideAlpha = this.nestFade ? clamp(1 - this.nestFade, 0, 1) : 1; if (nestHideAlpha <= 0.01) return true; const lightState = lighting || getLightingState(this.world); const showDecor = shouldShowTarinaiDecor(this); if (showDecor) { const shadow = projectedShadowParams(lightState, 0.95 + visualScale * 0.8); drawProjectedShadow(ctx, drawX, drawY + drawH * 0.30, drawW * 0.58, drawH * 0.13, { ...shadow, alpha: shadow.alpha * 0.90 * nestHideAlpha }); } ctx.save(); ctx.globalAlpha = nestHideAlpha; ctx.translate(drawX, drawY + bob); ctx.rotate((faceRight ? 1 : -1) * Math.PI / 28); if (faceRight) ctx.scale(-1, 1); if (img) { ctx.drawImage(img, -drawW / 2, -drawH / 2, drawW, drawH); } else { ctx.fillStyle = "#f4ead6"; ctx.strokeStyle = "rgba(92,71,49,0.48)"; ctx.lineWidth = 2; ctx.beginPath(); ctx.ellipse(0, 0, drawW * 0.42, drawH * 0.36, 0, 0, Math.PI * 2); ctx.fill(); ctx.stroke(); } ctx.restore(); if (showDecor) { ctx.save(); ctx.globalAlpha = 0.18 * nestHideAlpha; ctx.fillStyle = lightState.goldenStrength > 0.25 ? "rgba(255, 223, 137, 0.95)" : "rgba(255, 244, 172, 0.82)"; ctx.beginPath(); ctx.arc(drawX - drawW * 0.28, drawY - drawH * 0.35, Math.max(3, drawW * 0.06), 0, Math.PI * 2); ctx.fill(); ctx.restore(); } return true; }, draw(ctx, t, lighting = null) { if (this.dead) return; const sunbathActive = this.state === "sunbath" || (this.sunbathTimer || 0) > 0.04; if (!sunbathActive && Number.isFinite(this.x) && Number.isFinite(this.y)) { this._lastValidX = this.x; this._lastValidY = this.y; } const lightState = lighting || getLightingState(this.world); if (sunbathActive && this.drawSunbathPose?.(ctx, t, lightState)) return; if (!sunbathActive && String(this.visibleSpriteId || "").startsWith("sunbath")) { this.visibleSpriteId = ""; this.spriteLockUntil = 0; } const showDecor = shouldShowTarinaiDecor(this); if (sunbathActive) this.preloadSunbathSprites?.(); let sprite = this.spriteId(); if (!sunbathActive && String(sprite || "").startsWith("sunbath")) { this.visibleSpriteId = ""; this.spriteLockUntil = 0; sprite = this.rawSpriteId ? this.rawSpriteId() : (this.goodMode || this.type || "smile"); } if (!String(sprite || "").startsWith("sunbath")) this._lastNonSunbathSpriteId = sprite; const sunbathSprite = String(sprite || "").startsWith("sunbath"); const sunbathCandidates = sunbathSprite && this.sunbathSpriteCandidates ? this.sunbathSpriteCandidates() : []; if (sunbathSprite) { for (const id of sunbathCandidates) ensureImage(id); } const spriteFallback = "smile"; let renderSprite = sprite; let img = getRenderableImage(sprite, spriteFallback); if (sunbathSprite) { const ready = (id) => isImageReady(id); const imageFor = (id) => images.get(id) || null; if (ready(sprite)) { this._lastRenderableSunbathSpriteId = sprite; } else { const orderedFallbacks = [this._lastRenderableSunbathSpriteId, ...sunbathCandidates].filter(Boolean); const fallbackSunbath = orderedFallbacks.find(id => ready(id)); if (fallbackSunbath) { renderSprite = fallbackSunbath; img = imageFor(fallbackSunbath); this._lastRenderableSunbathSpriteId = fallbackSunbath; this.sunbathSpriteVariant = fallbackSunbath; } } if (!img && this._lastRenderableSunbathSpriteId && ready(this._lastRenderableSunbathSpriteId)) { renderSprite = this._lastRenderableSunbathSpriteId; img = imageFor(this._lastRenderableSunbathSpriteId); } if (!img) img = getRenderableImage("smile", "smile"); } const metrics = getImageMetrics(renderSprite) || getImageMetrics(sprite) || getImageMetrics(this.type) || getImageMetrics("smile"); const w = metrics?.w || 512; const h = metrics?.h || 512; const moveSpeed = Math.hypot(this.vx, this.vy); const gait = this.state === "sleep" ? 0.8 : clamp(moveSpeed / 42, 0.2, 1.8); const breathing = Math.sin(t * (this.state === "sleep" ? 1.1 : 2.3) + this.age * 0.18); const headbuttPulse = this.state === "fight" ? Math.max(0, Math.sin(t * 18 + this.age * 0.37)) : 0; const eatingShake = (this.eatTimer > 0.02 || this.grassEatTimer > 0) ? Math.sin(t * 62 + this.age) * clamp(Math.max(this.eatTimer / 1.0, this.grassEatTimer / 1.0), 0, 1) : 0; const fallPose = this.fallTimer > 0 ? clamp(this.fallTimer / Math.max(this.fallMax || 1, 0.01), 0, 1) : 0; const fallLift = Math.sin(fallPose * Math.PI) * this.radius * 0.20; const intimidateShake = this.intimidateTimer > 0.04 ? Math.sin(t * 44 + this.age) * this.radius * 0.09 : 0; const fearShakeY = this.intimidatedTimer > 0.04 ? Math.sin(t * 52 + this.age * 0.7) * this.radius * 0.11 : 0; const birthShake = this.birthRitualTimer > 0.04 ? Math.sin(t * 34 + this.age) * this.radius * 0.07 : 0; const coldStatus = this.temperatureStatus || this.world?.temperatureStatusFor?.(this.feltTemperature ?? this.tempComfort ?? (CONFIG.standardTemperature ?? 15), this) || null; const coldShakeActive = Boolean(coldStatus && coldStatus.direction === "cold" && (coldStatus.discomfort || 0) > 0.5 && this.state !== "sunbath"); const coldShakeStrength = coldShakeActive ? clamp((coldStatus.discomfort || 0) / 12, 0.25, 1.25) : 0; const coldShakeX = coldShakeActive ? (Math.sin(t * 46 + this.age * 0.9) + Math.sin(t * 71 + this.age * 0.31) * 0.55) * this.radius * 0.035 * coldShakeStrength : 0; const coldShakeY = coldShakeActive ? Math.sin(t * 58 + this.age * 0.47) * this.radius * 0.012 * coldShakeStrength : 0; const bob = breathing * (this.state === "sleep" ? 0.9 : 1.2 + gait * 1.6) + headbuttPulse * 0.9 + fallLift + birthShake * 0.25; const blastSpin = this.blastSpinTimer > 0.04 ? this.fallDir * ((this.blastSpinMax || 1) - this.blastSpinTimer) * Math.PI * 70.0 : 0; const fallSpin = this.blastSpinTimer > 0.04 ? blastSpin : (fallPose > 0 ? this.fallDir * (1 - fallPose) * Math.PI * 2 : 0); const diseaseSpin = this.fightDisease ? (t * 3.6 + this.age) : 0; const motionTilt = clamp(this.vx / 240, -0.22, 0.22) + (this.state === "fight" ? headbuttPulse * 0.05 : 0) + eatingShake * 0.145 + this.fallDir * fallPose * 0.18 + fallSpin + diseaseSpin; const insideCardboard = false; const visualScale = this.effectiveScale ? this.effectiveScale() : this.scale; let drawW = w * visualScale * 0.30; let drawH = h * visualScale * 0.30; if (insideCardboard) { drawW *= 0.72; drawH *= 0.72; } if (sprite === "stretch") { drawW *= 1.16; drawH *= 1.24; } if (sprite === "intimidate") { drawW *= 1.32; drawH *= 1.32; } if (sprite === "birth_ritual") { drawW *= 0.64; drawH *= 0.64; } if (fallPose > 0) { drawW *= 1 + fallPose * 0.08; drawH *= 1 - fallPose * 0.07; } const squish = this.state === "sleep" ? 1 + breathing * 0.018 : 1 + Math.sin(t * (4.5 + gait * 1.6) + this.age) * (0.012 + gait * 0.018) + headbuttPulse * 0.03; let faceRight = this.state === "sleep" ? this.sleepFacing > 0 : this.facingDir() > 0; if (tarinaiSpriteReversesFacing(renderSprite || sprite)) faceRight = !faceRight; const angle = (faceRight ? 1 : -1) * (Math.PI / 12) + motionTilt; const nestHideAlpha = this.nestFade ? clamp(1 - this.nestFade, 0, 1) : 1; if (nestHideAlpha <= 0.01) return; const shadow = projectedShadowParams(lightState, 0.95 + visualScale * 0.8); const shadowContactY = this.y + bob + (drawH / squish) * 0.30; if (showDecor && img) { drawImageProjectedShadow(ctx, img, this.x, shadowContactY, drawW * squish, drawH / squish, shadow, { faceRight, alpha: shadow.alpha * (this.state === "sleep" ? 1.10 : 0.92) * nestHideAlpha, widthScale: this.state === "sleep" ? 1.12 : 1, heightScale: this.state === "sleep" ? 0.88 : 1, }); } else if (showDecor) { drawProjectedShadow(ctx, this.x, shadowContactY, drawW * 0.52, drawH * 0.13, { ...shadow, alpha: shadow.alpha * 0.82 * nestHideAlpha }); } if (showDecor) drawCreatureGlow(ctx, this, drawW, drawH, lightState); if (showDecor && (this.powerItemMode === "protein" || this.powerItemMode === "niteropu")) { ctx.save(); const up = this.powerItemMode === "protein"; const baseColor = up ? "rgba(224,54,40,0.92)" : "rgba(116,80,190,0.88)"; const glowColor = up ? "rgba(255,72,44,0.45)" : "rgba(118,80,190,0.38)"; ctx.shadowColor = glowColor; ctx.shadowBlur = up ? 10 : 7; const particleCount = up ? 10 : 12; for (let i = 0; i < particleCount; i++) { const phase = (t * (up ? 0.92 : 0.78) + stableUnit(`${this.id || this.familyKey}:power:${i}`, "phase") * 9.7) % 1; const side = stableUnit(`${this.id || this.familyKey}:power:${i}`, "side") * 2 - 1; const drift = Math.sin(t * 1.7 + i * 0.91) * drawW * 0.06; const px = this.x + side * drawW * randSeed(i + this.seed, 0.12, 0.45) + drift; const py = up ? this.y + drawH * 0.38 - phase * drawH * 0.94 : this.y - drawH * 0.46 + phase * drawH * 0.98; const fade = up ? Math.sin(phase * Math.PI) : (0.42 + 0.58 * Math.sin(phase * Math.PI)); ctx.globalAlpha = fade * (up ? 0.78 : 0.70); ctx.fillStyle = baseColor; ctx.beginPath(); ctx.ellipse(px, py, Math.max(2, drawW * randSeed(i + 83, 0.025, 0.045)), Math.max(2, drawW * randSeed(i + 137, 0.020, 0.040)), randSeed(i + 223, -0.6, 0.6), 0, Math.PI * 2); ctx.fill(); } ctx.globalAlpha = up ? 0.10 : 0.08; ctx.strokeStyle = baseColor; ctx.lineWidth = up ? 1.8 : 1.4; ctx.beginPath(); ctx.ellipse(this.x, this.y + drawH * 0.08, drawW * 0.58, drawH * 0.44, 0, 0, Math.PI * 2); ctx.stroke(); ctx.restore(); } if (this.birthRitualTimer > 0.04 && this.birthRitualLeader) { const partner = this.world.liveTarinaiById?.(this.birthPartnerId); const hx = partner ? (this.x + partner.x) / 2 : this.x; const hy = partner ? Math.min(this.y, partner.y) - Math.max(drawH, partner.radius * 2.6) * 0.50 : this.y - drawH * 0.60; const pulse = 1 + Math.sin(t * 5.5 + this.age) * 0.10; const size = Math.max(10, this.radius * 0.62) * pulse; drawHeartShape(ctx, hx, hy, size, { fill: "rgba(240,91,135,0.90)", stroke: "rgba(255,252,246,0.92)" }); } ctx.save(); ctx.translate(this.x + intimidateShake + coldShakeX, this.y + bob + fearShakeY + coldShakeY); const observeKind = this.world.observeHighlightKind ? this.world.observeHighlightKind(this) : ""; if (observeKind && this.world.selected !== this) { const style = { parent: { color: "rgba(86,142,232,0.86)", label: "\u89aa" }, child: { color: "rgba(93,170,92,0.88)", label: "\u5b50" }, friend: { color: "rgba(240,91,135,0.90)", label: "\u53cb\u9054" }, enemy: { color: "rgba(214,82,66,0.90)", label: "\u6575\u5bfe" }, }[observeKind] || { color: "rgba(255,250,220,0.78)", label: "" }; ctx.save(); ctx.globalAlpha = 0.96 * nestHideAlpha; ctx.shadowColor = style.color; ctx.shadowBlur = 16; ctx.strokeStyle = style.color; ctx.lineWidth = 5.2; ctx.beginPath(); ctx.ellipse(0, drawH * 0.14, drawW * 0.78, drawH * 0.60, 0, 0, Math.PI * 2); ctx.stroke(); ctx.shadowBlur = 0; ctx.strokeStyle = "rgba(255,255,255,0.92)"; ctx.lineWidth = 2.0; ctx.setLineDash([8, 5]); ctx.beginPath(); ctx.ellipse(0, drawH * 0.14, drawW * 0.86, drawH * 0.66, 0, 0, Math.PI * 2); ctx.stroke(); ctx.setLineDash([]); if (style.label) { const labelY = -drawH * 0.62; ctx.font = `${Math.max(11, this.radius * 0.52)}px Yomogi, ui-rounded, sans-serif`; ctx.textAlign = "center"; ctx.textBaseline = "middle"; const labelW = ctx.measureText(style.label).width + 16; ctx.fillStyle = "rgba(255,252,238,0.92)"; ctx.strokeStyle = style.color; ctx.lineWidth = 2.2; tarinaiRoundRectPath(ctx, -labelW / 2, labelY - 10, labelW, 20, 10); ctx.fill(); ctx.stroke(); ctx.fillStyle = style.color; ctx.fillText(style.label, 0, labelY); } ctx.restore(); } if (this.world.selected === this) { ctx.save(); const pulse = 0.5 + 0.5 * Math.sin(this.world.time * 7.5); const jumpPulse = clamp(this.focusPulseTimer || 0, 0, 1); ctx.globalAlpha = (0.82 + pulse * 0.12) * nestHideAlpha; ctx.shadowColor = "rgba(124, 210, 76, 0.92)"; ctx.shadowBlur = 14 + 18 * Math.max(pulse, jumpPulse); ctx.strokeStyle = "rgba(112, 198, 70, 0.98)"; ctx.lineWidth = 4.2 + jumpPulse * 2.0; ctx.beginPath(); ctx.ellipse(0, drawH * 0.14, drawW * (0.74 + jumpPulse * 0.10), drawH * (0.56 + jumpPulse * 0.08), 0, 0, Math.PI * 2); ctx.stroke(); ctx.shadowBlur = 0; ctx.setLineDash([9, 5]); ctx.lineWidth = 2.0; ctx.strokeStyle = "rgba(255,255,255,0.96)"; ctx.beginPath(); ctx.ellipse(0, drawH * 0.14, drawW * 0.88, drawH * 0.68, 0, 0, Math.PI * 2); ctx.stroke(); ctx.setLineDash([]); ctx.fillStyle = "rgba(255,252,232,0.94)"; ctx.strokeStyle = "rgba(112,198,70,0.88)"; ctx.lineWidth = 2; const focusLabel = "\u6ce8\u76ee"; ctx.font = `${Math.max(11, this.radius * 0.54)}px Yomogi, ui-rounded, sans-serif`; const lw = ctx.measureText(focusLabel).width + 18; const ly = -drawH * 0.64; tarinaiRoundRectPath(ctx, -lw / 2, ly - 11, lw, 22, 11); ctx.fill(); ctx.stroke(); ctx.fillStyle = "rgba(72,126,42,0.96)"; ctx.textAlign = "center"; ctx.textBaseline = "middle"; ctx.fillText(focusLabel, 0, ly); ctx.restore(); } else if (showDecor && this.world.pointer?.inside && distXY(this.x, this.y, this.world.pointer.x, this.world.pointer.y) < this.radius * 2.0) { ctx.save(); ctx.globalAlpha = 0.28; ctx.strokeStyle = this.state === "cursor_enemy" ? "rgba(180, 92, 92, 0.70)" : "rgba(255, 250, 220, 0.78)"; ctx.lineWidth = 1.4; ctx.beginPath(); ctx.ellipse(0, drawH * 0.14, drawW * 0.58, drawH * 0.43, 0, 0, Math.PI * 2); ctx.stroke(); ctx.restore(); } ctx.rotate(angle); ctx.globalAlpha *= nestHideAlpha; if (insideCardboard) ctx.globalAlpha *= 0.58; if (this.eatTimer > 0.02) { ctx.save(); ctx.globalAlpha = 0.46 * clamp(this.eatTimer / 1.0, 0, 1); ctx.strokeStyle = this.lastMealColor || "#f7cf67"; ctx.lineWidth = 1.6; ctx.beginPath(); ctx.arc(0, -drawH * 0.02, drawW * 0.22 + (1 - clamp(this.eatTimer / 1.0, 0, 1)) * 5, 0, Math.PI * 2); ctx.stroke(); ctx.restore(); } if (this.pokeFlashTimer > 0.03) { ctx.save(); ctx.globalAlpha = this.pokeFlashTimer * 0.9 * nestHideAlpha; ctx.fillStyle = "rgba(255, 120, 120, 0.22)"; ctx.beginPath(); ctx.ellipse(0, 0, drawW * 0.60, drawH * 0.42, 0, 0, Math.PI * 2); ctx.fill(); ctx.restore(); } if (showDecor && (this.goodMode === "smile" || this.state === "cursor_friend") && this.mood > 58 && this.stress < 44 && this.state !== "sleep") { ctx.save(); const cursorLove = this.state === "cursor_friend" ? Math.max(0, this.cursorPetting || 0) : 0; ctx.globalAlpha = this.state === "cursor_friend" ? clamp(0.82 + cursorLove * 0.16, 0.82, 1) : 0.34; ctx.fillStyle = this.state === "cursor_friend" ? "#f05b87" : (lightState.nightStrength > 0.20 ? "#eef5ff" : (lightState.goldenStrength > 0.35 ? "#ffe398" : "#d6f5a9")); const count = this.state === "cursor_friend" ? (cursorLove > 1.0 ? 10 : (cursorLove > 0.45 ? 8 : (cursorLove > 0.18 ? 6 : 4))) : 2; for (let i = 0; i < count; i++) { const orbit = 0.24 + (i % 3) * 0.08 + cursorLove * 0.05; const px = Math.cos(t * 2.2 + this.age + i * 1.42) * drawW * orbit; const py = -drawH * (0.22 + (i % 2) * 0.06) + Math.sin(t * 2.9 + this.age * 0.2 + i) * drawH * (0.08 + cursorLove * 0.02); if (this.state === "cursor_friend") { const s = Math.max(5, drawW * (0.070 + (i % 3) * 0.012 + cursorLove * 0.01)); drawHeartShape(ctx, px, py, s, { fill: "rgba(240,91,135,0.90)", stroke: "rgba(255,252,246,0.92)", alpha: 1, rotation: Math.sin(t * 3 + i) * 0.18 }); } else { ctx.beginPath(); ctx.arc(px, py, drawW * 0.045, 0, Math.PI * 2); ctx.fill(); } } ctx.restore(); } if (faceRight) ctx.scale(-1, 1); const dw = drawW * squish; const dh = drawH / squish; const renderScale = SPRITE_CACHE_SCALE; const cacheW = Math.max(1, Math.round((dw * renderScale) / 8) * 8); const cacheH = Math.max(1, Math.round((dh * renderScale) / 8) * 8); if (img) { let spriteCacheKey = renderSprite || sprite; try { if (images.get(renderSprite || sprite) !== img) { let fallbackId = "fallback"; for (const [imageId, imageObj] of images) { if (imageObj === img) { fallbackId = imageId; break; } } spriteCacheKey = `${renderSprite || sprite}:fallback:${fallbackId}`; } } catch (err) { spriteCacheKey = renderSprite || sprite; } const spriteCanvas = getCachedSpriteCanvas(spriteCacheKey, img, cacheW, cacheH); ctx.drawImage(spriteCanvas, -dw / 2, -dh / 2, dw, dh); } else { ctx.save(); ctx.fillStyle = this.stress > 62 ? "#f1c3a8" : "#f4ead6"; ctx.strokeStyle = "rgba(92,71,49,0.48)"; ctx.lineWidth = 2; ctx.beginPath(); ctx.ellipse(0, 0, dw * 0.42, dh * 0.36, 0, 0, Math.PI * 2); ctx.fill(); ctx.stroke(); ctx.restore(); } if (img && (this.zunchiStain || 0) > 3) { const stainCanvas = getCachedStainOverlayCanvas(renderSprite || sprite, img, cacheW, cacheH, clamp((this.zunchiStain || 0) / 100, 0, 1), this.zunchiStainSeed || 0.5); if (stainCanvas) { ctx.save(); ctx.globalAlpha = clamp((this.zunchiStain || 0) / 100, 0.10, 0.88); ctx.drawImage(stainCanvas, -dw / 2, -dh / 2, dw, dh); ctx.restore(); } } if (String(sprite || "").startsWith("zunchi_slave")) { const zImg = getRenderableImage("zunchi", "zunchi"); if (zImg) { ctx.save(); if (tarinaiShadowsEnabled()) { ctx.shadowColor = "rgba(46, 36, 24, 0.28)"; ctx.shadowBlur = 5; ctx.shadowOffsetY = Math.max(1, drawH * 0.012); } const zw = Math.max(16, drawW * 0.42); const zh = zw * 0.74; ctx.drawImage(zImg, -zw * 0.78, -dh * 0.50 - zh * 0.28, zw, zh); ctx.restore(); } } if (this.burning || (this.burnTimer || 0) > 0.02) { ctx.save(); const phase = t * 8.0 + this.age * 0.37 + this.seed; const fireAlpha = 0.82 * nestHideAlpha; const drawFire = typeof drawSimpleFireShape === "function" ? drawSimpleFireShape : null; if (drawFire) { drawFire(ctx, -dw * 0.20, -dh * 0.18, Math.max(10, dw * 0.17), { phase, alpha: fireAlpha }); drawFire(ctx, dw * 0.16, -dh * 0.04, Math.max(8, dw * 0.13), { phase: phase + 1.7, alpha: fireAlpha * 0.86, shadow: false }); drawFire(ctx, 0, -dh * 0.34, Math.max(9, dw * 0.14), { phase: phase + 3.1, alpha: fireAlpha * 0.76, shadow: false }); } else { ctx.fillStyle = "rgba(236,46,24,0.78)"; ctx.beginPath(); ctx.ellipse(0, -dh * 0.20, Math.max(8, dw * 0.15), Math.max(12, dh * 0.22), 0, 0, Math.PI * 2); ctx.fill(); } ctx.restore(); } if (faceRight) ctx.scale(-1, 1); ctx.restore(); // \u305F\u308A\u306A\u3044\u738B\u306E\u738B\u51A0\u306F\u3001\u643A\u884C\u306C\u3044\u3050\u308B\u307F\u3088\u308A\u524D\u9762\u306B\u51FA\u3059\u305F\u3081 render.js \u306E\u5F8C\u6BB5\u3067\u63CF\u753B\u3059\u308B\u3002 if (this.favorite) { ctx.save(); ctx.globalAlpha = nestHideAlpha; const starSize = Math.max(18, this.radius * 0.88); const sx = this.x; const sy = this.y - drawH * 0.64 - 20; ctx.font = `${starSize}px "Apple Color Emoji", "Segoe UI Emoji", sans-serif`; ctx.textAlign = "center"; ctx.textBaseline = "middle"; if (showDecor) { ctx.shadowColor = "rgba(255, 198, 28, 0.82)"; ctx.shadowBlur = 14; } ctx.lineWidth = Math.max(2, starSize * 0.12); ctx.strokeStyle = "rgba(92, 62, 0, 0.86)"; ctx.fillStyle = "rgba(255, 218, 46, 1)"; ctx.strokeText("\u2605", sx, sy); ctx.fillText("\u2605", sx, sy); ctx.restore(); } const hpActive = (this.hpBarTimer || 0) > 0; const stressActive = (this.stressBarTimer || 0) > 0; if (hpActive || stressActive) { const bw = Math.max(28, this.radius * 2.35); const bh = 5; const x = this.x - bw / 2; const baseY = this.y - drawH * 0.55 - 16; const stressY = hpActive ? baseY + 7 : baseY; const hpY = stressActive ? baseY - 7 : baseY; const drawMeter = (y, pct, fill, alpha) => { ctx.save(); ctx.globalAlpha = alpha; const safePct = clamp(Number(pct) || 0, 0, 1); const fillW = safePct <= 0 ? 0 : Math.min(bw, Math.max(2, bw * safePct)); ctx.fillStyle = "rgba(255,252,242,0.86)"; roundedRect(ctx, x - 2, y - 2, bw + 4, bh + 4, 5); ctx.fill(); ctx.fillStyle = "rgba(72,58,45,0.22)"; roundedRect(ctx, x, y, bw, bh, 4); ctx.fill(); if (fillW > 0) { const fillR = Math.min(4, bh / 2, fillW / 2); ctx.fillStyle = fill; roundedRect(ctx, x, y, fillW, bh, fillR); ctx.fill(); } ctx.restore(); }; if (stressActive) { const pct = clamp((this.stress || 0) / 130, 0, 1); drawMeter(stressY, pct, "rgba(212,126,84,0.90)", clamp((this.stressBarTimer || 0) / 0.7, 0.35, 1)); } if (hpActive) { const pct = typeof tarinaiEnergyRatio === "function" ? tarinaiEnergyRatio(this) : clamp(this.energy / Math.max(1, this.maxEnergy || 100), 0, 1); drawMeter(hpY, pct, "rgba(118,190,80,0.88)", clamp((this.hpBarTimer || 0) / 0.7, 0.35, 1)); } } }, contains(x, y) { if (this.insideNestBoxId || (this.nestFade || 0) > 0.05) return false; const hit = this.world.screenSizeToWorld ? this.world.screenSizeToWorld(this.radius * 1.9) : this.radius * 1.9; return distXY(this.x, this.y, x, y) < hit; } })); })(typeof window !== "undefined" ? window : globalThis);