This commit is contained in:
33333-33333 2026-06-24 21:20:53 +09:00
commit 2741b93dea
31 changed files with 751 additions and 546 deletions

View file

@ -4,14 +4,145 @@
const Tarinai = global.Tarinai;
if (!Tarinai) throw new Error("Tarinai is not available for mixin: tarinai_render.js");
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) => typeof isImageReady === "function"
? isImageReady(id)
: !!(typeof images !== "undefined" && images?.get?.(id)?.complete && (images?.get?.(id)?.naturalWidth || images?.get?.(id)?.width));
const imageFor = (id) => {
if (!id) return null;
if (typeof getRenderableImage === "function") return getRenderableImage(id, "");
return (typeof images !== "undefined" ? images.get?.(id) : globalThis.TarinaiAssets?.images?.get?.(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 = typeof getRenderableImage === "function"
? getRenderableImage(fallbackId, "smile")
: (typeof images !== "undefined" ? (images.get?.(fallbackId) || images.get?.("smile")) : null);
if (!img) {
renderSprite = "smile";
img = typeof getRenderableImage === "function"
? getRenderableImage("smile", "smile")
: (typeof images !== "undefined" ? images.get?.("smile") : null);
}
}
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);
if (!Number.isFinite(this.sunbathAnchorX)) this.sunbathAnchorX = Number.isFinite(lastX) ? lastX : (Number.isFinite(this.x) ? this.x : pad);
if (!Number.isFinite(this.sunbathAnchorY)) this.sunbathAnchorY = Number.isFinite(lastY) ? lastY : (Number.isFinite(this.y) ? this.y : pad);
let drawX = this.sunbathAnchorX;
let drawY = this.sunbathAnchorY;
if (!Number.isFinite(drawX)) drawX = Number.isFinite(lastX) ? lastX : pad;
if (!Number.isFinite(drawY)) drawY = Number.isFinite(lastY) ? lastY : pad;
drawX = clamp(drawX, pad, Math.max(pad, worldW - pad));
drawY = clamp(drawY, pad, Math.max(pad, worldH - pad));
this.sunbathAnchorX = drawX;
this.sunbathAnchorY = drawY;
this._lastSunbathDrawX = drawX;
this._lastSunbathDrawY = drawY;
this._lastValidX = drawX;
this._lastValidY = drawY;
if (!Number.isFinite(this.x) || Math.abs(this.x - drawX) > 1.5) this.x = drawX;
if (!Number.isFinite(this.y) || Math.abs(this.y - drawY) > 1.5) this.y = 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 = true || this.world.selected === 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);
const quality = this.world.performanceQuality?.() || {};
const effectDensity = clamp(Number(quality.effectDensity || 1) || 1, 0.12, 1);
const showDecor = effectDensity > 0.45 || this.world.selected === this;
if (this.state === "sunbath" || (this.sunbathTimer || 0) > 0.04) this.preloadSunbathSprites?.();
const sprite = this.spriteId();
if (sunbathActive && this.drawSunbathPose?.(ctx, t, lightState)) return;
if (!sunbathActive && String(this.visibleSpriteId || "").startsWith("sunbath")) {
this.visibleSpriteId = "";
this.spriteLockUntil = 0;
}
const showDecor = true || this.world.selected === 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) {
@ -20,18 +151,28 @@
else if (globalThis.TarinaiAssets?.ensureImage) globalThis.TarinaiAssets.ensureImage(id);
}
}
const spriteFallback = sunbathSprite ? "" : "smile";
const spriteFallback = "smile";
let renderSprite = sprite;
let img = typeof getRenderableImage === "function" ? getRenderableImage(sprite, spriteFallback) : (images.get(sprite) || images.get(spriteFallback) || images.get(this.type));
if (sunbathSprite) {
const ready = (id) => typeof isImageReady === "function" ? isImageReady(id) : !!(images?.get?.(id)?.complete || images?.get?.(id)?.naturalWidth || images?.get?.(id)?.width);
if (!ready(sprite)) {
const fallbackSunbath = sunbathCandidates.find(id => ready(id));
const imageFor = (id) => (typeof images !== "undefined" ? images.get?.(id) : globalThis.TarinaiAssets?.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 = images.get(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 = typeof getRenderableImage === "function" ? getRenderableImage("smile", "smile") : (images.get("smile") || images.get(this.type));
}
const metrics = getImageMetrics(renderSprite) || getImageMetrics(sprite) || getImageMetrics(this.type) || getImageMetrics("smile");
@ -131,7 +272,7 @@
ctx.restore();
}
if (this.birthRitualTimer > 0.04 && this.birthRitualLeader) {
const partner = this.world.tarinai.find(o => o.id === this.birthPartnerId && !o.dead);
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;
@ -287,19 +428,19 @@
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 = sprite;
let spriteCacheKey = renderSprite || sprite;
try {
if (typeof images !== "undefined" && images.get?.(sprite) !== img) {
if (typeof images !== "undefined" && images.get?.(renderSprite || sprite) !== img) {
let fallbackId = "fallback";
for (const [imageId, imageObj] of images) {
if (imageObj === img) { fallbackId = imageId; break; }
}
spriteCacheKey = `${sprite}:fallback:${fallbackId}`;
spriteCacheKey = `${renderSprite || sprite}:fallback:${fallbackId}`;
}
} catch (err) {
spriteCacheKey = sprite;
spriteCacheKey = renderSprite || sprite;
}
const spriteCanvas = sunbathSprite ? img : (typeof getCachedSpriteCanvas === "function" ? getCachedSpriteCanvas(spriteCacheKey, img, cacheW, cacheH) : img);
const spriteCanvas = typeof getCachedSpriteCanvas === "function" ? getCachedSpriteCanvas(spriteCacheKey, img, cacheW, cacheH) : img;
ctx.drawImage(spriteCanvas, -dw / 2, -dh / 2, dw, dh);
} else {
ctx.save();
@ -398,87 +539,6 @@
}
},
drawSunbathSpriteDirect(ctx, t, lightState, showDecor = true) {
return false;
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 now = Number(this.world?.time ?? t ?? 0) || 0;
const frameKey = Math.floor(now / 0.5);
if (this._sunbathRenderFrameKey !== frameKey || !candidates.includes(this._sunbathRenderSpriteId)) {
const prev = this._sunbathRenderSpriteId || this.sunbathSpriteVariant || "";
const choices = candidates.length > 1 ? candidates.filter(id => id !== prev) : candidates;
this._sunbathRenderSpriteId = choices[Math.floor(Math.random() * choices.length)] || candidates[0] || "sunbath_1";
this._sunbathRenderFrameKey = frameKey;
}
const sprite = this._sunbathRenderSpriteId || this.sunbathSpriteVariant || candidates[0] || "sunbath_1";
this.sunbathSpriteVariant = sprite;
this.visibleSpriteId = sprite;
this.spriteLockUntil = now;
for (const id of candidates) {
if (typeof ensureImage === "function") ensureImage(id);
else if (globalThis.TarinaiAssets?.ensureImage) globalThis.TarinaiAssets.ensureImage(id);
}
let img = null;
const ordered = [sprite, this.sunbathSpriteVariant, ...candidates, "smile"];
const seen = new Set();
for (const id of ordered) {
if (!id || seen.has(id)) continue;
seen.add(id);
const next = typeof getRenderableImage === "function"
? getRenderableImage(id, id === "smile" ? "smile" : "smile")
: (typeof images !== "undefined" ? (images.get(id) || images.get("smile")) : null);
const ready = next && (next.complete || next.naturalWidth || next.width);
if (ready) { img = next; break; }
}
const metrics = (typeof getImageMetrics === "function" ? (getImageMetrics(sprite) || getImageMetrics(img) || getImageMetrics("smile")) : null) || {};
const w = metrics.w || img?.naturalWidth || img?.width || 512;
const h = metrics.h || img?.naturalHeight || img?.height || 468;
const visualScale = this.effectiveScale ? this.effectiveScale() : this.scale;
const breathing = Math.sin(t * 1.4 + this.age * 0.18);
const bob = breathing * 0.55;
const drawW = w * visualScale * 0.30;
const drawH = h * visualScale * 0.30;
const squish = 1 + breathing * 0.006;
const dw = drawW * squish;
const dh = drawH / squish;
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 shadow = projectedShadowParams(lightState, 0.95 + visualScale * 0.8);
const shadowContactY = this.y + bob + dh * 0.30;
if (showDecor && img) {
drawImageProjectedShadow(ctx, img, this.x, shadowContactY, dw, dh, shadow, {
faceRight,
alpha: shadow.alpha * 0.92 * nestHideAlpha,
});
} 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);
ctx.save();
ctx.translate(this.x, this.y + bob);
ctx.rotate((faceRight ? 1 : -1) * (Math.PI / 18));
if (faceRight) ctx.scale(-1, 1);
ctx.globalAlpha = nestHideAlpha;
if (img) {
ctx.drawImage(img, -dw / 2, -dh / 2, dw, dh);
} else {
ctx.fillStyle = "#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();
return true;
},
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;