582 lines
27 KiB
JavaScript
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);
|