tarinai/js/tarinai_render.js
2026-06-29 21:02:04 +09:00

582 lines
27 KiB
JavaScript

"use strict";
(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") 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;
}
Object.defineProperties(Tarinai.prototype, Object.getOwnPropertyDescriptors({
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 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;
const faceRight = this.state === "sleep"
? this.sleepFacing > 0
: this.facingDir() > 0;
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, this.y + bob + fearShakeY);
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 brightness = clamp(0.82 + lightState.light * 0.30 + lightState.dawnStrength * 0.08 + lightState.goldenStrength * 0.06 + lightState.noonStrength * 0.04, 0.80, 1.17);
const saturation = clamp(0.86 + lightState.light * 0.22 + lightState.goldenStrength * 0.10 - lightState.nightStrength * 0.04, 0.82, 1.14);
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();
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 (faceRight) ctx.scale(-1, 1);
ctx.restore();
if (this.isTarinaiChampion) {
ctx.save();
ctx.globalAlpha = nestHideAlpha;
const crownW = Math.max(24, this.radius * 1.34);
const crownH = Math.max(14, this.radius * 0.58);
const cx = this.x;
const cy = this.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();
}
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) && (showDecor || this.world.selected === this || this.hurtTimer > 0.04)) {
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 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(baseY, pct, pct > 0.72 ? "rgba(178,74,110,0.92)" : "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(baseY - (stressActive ? 11 : 0), pct, pct > 0.45 ? "rgba(118,190,80,0.88)" : (pct > 0.20 ? "rgba(232,172,68,0.90)" : "rgba(218,82,70,0.92)"), clamp((this.hpBarTimer || 0) / 0.7, 0.35, 1));
}
}
},
contains(x, y) {
if (this.insideNestBoxId || (this.nestFade || 0) >= 0.92) 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);