"use strict"; const images = new Map(); const imageBoundsCache = new WeakMap(); const imageMetrics = new Map(); const imagePromises = new Map(); const imageAssetIndex = new Map(); const scaledSpriteCache = new Map(); const stainOverlayCache = new Map(); const SPRITE_CACHE_LIMIT = 128; const STAIN_CACHE_LIMIT = 64; const INITIAL_IMAGE_IDS = new Set(["smile", "hungry_70", "zunchi_slave", "zunchi_slave_alt", "zunchi", "zunchi_02", "genkotsu", "pushpin", "pushpin_stuck", "oshibyo", "oshibyo_stuck"]); const EARLY_IMAGE_IDS = new Set(["smile", "angry", "teary", "jito", "drool", "cry", "sleep", "pokan", "normal_smirk", "normal_tongue", "normal_happy", "weak", "hurt", "hurt2", "fear", "flee", "flee_fear2", "fear_blue", "fear_cry", "sleep2", "stress_dizzy", "stress_sweat", "intimidate", "birth_ritual", "zunchi_slave", "zunchi_slave_alt", "hungry_70", "zunchi", "zunchi_02", "genkotsu", "pushpin", "pushpin_stuck", "oshibyo", "oshibyo_stuck"]); function trimCanvasCache(cache, limit = 128) { while (cache.size > limit) { const first = cache.keys().next().value; if (first === undefined) break; cache.delete(first); } } function quantizedCanvasSize(v, step = 24) { return Math.max(1, Math.round(v / step) * step); } const SPRITE_CACHE_SCALE = 1.55; function getCachedSpriteCanvas(id, img, w, h) { if (!img || !w || !h) return img; const qw = quantizedCanvasSize(w); const qh = quantizedCanvasSize(h); const key = `${id}:${qw}x${qh}`; if (window.TarinaiPhysics?.createCachedCanvas) { return window.TarinaiPhysics.createCachedCanvas(scaledSpriteCache, key, qw, qh, (c) => { if ("imageSmoothingQuality" in c) c.imageSmoothingQuality = "high"; c.clearRect(0, 0, qw, qh); c.drawImage(img, 0, 0, qw, qh); }, { limit: SPRITE_CACHE_LIMIT }); } let canvas = scaledSpriteCache.get(key); if (canvas) return canvas; canvas = document.createElement("canvas"); canvas.width = qw; canvas.height = qh; const c = canvas.getContext("2d"); c.imageSmoothingEnabled = true; if ("imageSmoothingQuality" in c) c.imageSmoothingQuality = "high"; c.clearRect(0, 0, qw, qh); c.drawImage(img, 0, 0, qw, qh); scaledSpriteCache.set(key, canvas); trimCanvasCache(scaledSpriteCache, SPRITE_CACHE_LIMIT); return canvas; } function getCachedStainOverlayCanvas(id, img, w, h, stain = 0, seed = 0.5) { if (!img || stain <= 0.01 || !w || !h) return null; const qw = quantizedCanvasSize(w); const qh = quantizedCanvasSize(h); const bucket = Math.max(1, Math.min(8, Math.ceil(stain * 8))); const seedBucket = Math.round((seed || 0) * 16); const key = `${id}:${qw}x${qh}:s${bucket}:r${seedBucket}`; const buildOverlay = (c) => { if ("imageSmoothingQuality" in c) c.imageSmoothingQuality = "high"; c.clearRect(0, 0, qw, qh); c.drawImage(img, 0, 0, qw, qh); c.globalCompositeOperation = "source-in"; c.globalAlpha = 0.34 + bucket * 0.075; c.fillStyle = "rgba(28, 92, 22, 0.96)"; const s = (seedBucket % 16) / 16; const spots = [ [0.40 + s * 0.035, 0.82, 0.14, 0.040, -0.10], [0.59 - s * 0.030, 0.87, 0.15, 0.044, 0.12], [0.50, 0.91, 0.20, 0.036, 0.02], [0.48 + s * 0.020, 0.78, 0.10, 0.030, -0.18], ]; for (const [sx, sy, rw, rh, rot] of spots) { c.beginPath(); c.ellipse(sx * qw, sy * qh, rw * qw, rh * qh, rot, 0, Math.PI * 2); c.fill(); } c.globalAlpha = 1; c.globalCompositeOperation = "source-over"; }; if (window.TarinaiPhysics?.createCachedCanvas) return window.TarinaiPhysics.createCachedCanvas(stainOverlayCache, key, qw, qh, buildOverlay, { limit: STAIN_CACHE_LIMIT }); let canvas = stainOverlayCache.get(key); if (canvas) return canvas; canvas = document.createElement("canvas"); canvas.width = qw; canvas.height = qh; const c = canvas.getContext("2d"); c.imageSmoothingEnabled = true; buildOverlay(c); stainOverlayCache.set(key, canvas); trimCanvasCache(stainOverlayCache, STAIN_CACHE_LIMIT); return canvas; } function primeImageMetrics(asset) { if (!asset || imageMetrics.has(asset.id)) return; const bounds = ALPHA_BOUNDS[asset.id]; const w = bounds?.imageW || 512; const h = bounds?.imageH || 512; imageMetrics.set(asset.id, { w, h, ratio: h / Math.max(w, 1), visibleBottomRatio: bounds ? (bounds.y + bounds.h) / Math.max(bounds.imageH, 1) : 1, }); } function setupAssetIndex() { if (imageAssetIndex.size) return; for (const asset of [...SPRITES, ...DECOR_ASSETS]) { imageAssetIndex.set(asset.id, asset); primeImageMetrics(asset); } } function markImageLoaded(asset, img) { const w = img.naturalWidth || img.width || ALPHA_BOUNDS[asset.id]?.imageW || 512; const h = img.naturalHeight || img.height || ALPHA_BOUNDS[asset.id]?.imageH || 512; const bounds = ALPHA_BOUNDS[asset.id] || { x: 0, y: 0, w, h, imageW: w, imageH: h }; imageMetrics.set(asset.id, { w, h, ratio: h / Math.max(w, 1), visibleBottomRatio: (bounds.y + bounds.h) / Math.max(bounds.imageH, 1), }); } function isImageReady(id) { const img = images.get(id); return !!(img && img.complete && (img.naturalWidth || img.width)); } function loadImageAsset(asset, priority = "auto") { if (!asset) return Promise.resolve(null); setupAssetIndex(); primeImageMetrics(asset); if (isImageReady(asset.id)) return Promise.resolve(images.get(asset.id)); if (imagePromises.has(asset.id)) return imagePromises.get(asset.id); const img = images.get(asset.id) || new Image(); img.decoding = "async"; if ("fetchPriority" in img) img.fetchPriority = priority; img.alphaBounds = ALPHA_BOUNDS[asset.id] || null; images.set(asset.id, img); const promise = new Promise((resolve) => { img.onload = () => { markImageLoaded(asset, img); resolve(img); }; img.onerror = () => { resolve(null); }; }); imagePromises.set(asset.id, promise); if (!img.src) img.src = (window.TarinaiVersion?.withStaticVersion ? window.TarinaiVersion.withStaticVersion(asset.path) : asset.path); return promise; } function ensureImage(id) { setupAssetIndex(); const asset = imageAssetIndex.get(id); if (!asset) return null; primeImageMetrics(asset); const existing = images.get(id); if (!existing || !existing.src) loadImageAsset(asset, "low"); return images.get(id) || null; } function getRenderableImage(id, fallbackId = "smile") { if (id) ensureImage(id); if (isImageReady(id)) return images.get(id); if (fallbackId && fallbackId !== id) ensureImage(fallbackId); if (isImageReady(fallbackId)) return images.get(fallbackId); return null; } function loadImages(ids = null, onProgress = null) { setupAssetIndex(); const requested = ids ? new Set(ids) : INITIAL_IMAGE_IDS; const assets = [...requested].map(id => imageAssetIndex.get(id)).filter(Boolean); const total = Math.max(assets.length, 1); let done = 0; if (typeof onProgress === "function") onProgress(done, total, null); const tasks = assets.map(asset => loadImageAsset(asset, "high").then((img) => { done += 1; if (typeof onProgress === "function") onProgress(done, total, asset); return img; })); return Promise.all(tasks).then(() => undefined); } function trimImageMemory(aggressive = false) { trimCanvasCache(scaledSpriteCache, aggressive ? 48 : SPRITE_CACHE_LIMIT); trimCanvasCache(stainOverlayCache, aggressive ? 16 : STAIN_CACHE_LIMIT); } if (typeof document !== "undefined") { document.addEventListener("visibilitychange", () => { if (document.hidden) trimImageMemory(true); }); } if (typeof window !== "undefined") { window.setInterval(() => trimImageMemory(false), 45000); } function startBackgroundImageLoading() { setupAssetIndex(); const loadGroup = (ids, priority = "low") => { for (const id of ids) { const asset = imageAssetIndex.get(id); if (asset) loadImageAsset(asset, priority); } }; const schedule = window.requestIdleCallback ? (fn, timeout = 1200) => window.requestIdleCallback(fn, { timeout }) : (fn) => setTimeout(fn, 60); schedule(() => loadGroup(EARLY_IMAGE_IDS, "auto"), 450); schedule(() => { for (const asset of [...SPRITES, ...DECOR_ASSETS]) loadImageAsset(asset, "low"); trimImageMemory(false); }, 2200); } function imageAlphaBounds(img) { if (!img) return null; if (img.alphaBounds) return img.alphaBounds; if (imageBoundsCache.has(img)) return imageBoundsCache.get(img); const w = img.naturalWidth || img.width || 0; const h = img.naturalHeight || img.height || 0; if (!w || !h) return null; const bounds = { x: 0, y: 0, w, h, imageW: w, imageH: h }; imageBoundsCache.set(img, bounds); return bounds; } function getImageMetrics(idOrImg) { if (typeof idOrImg === "string") { setupAssetIndex(); const asset = imageAssetIndex.get(idOrImg); if (asset) primeImageMetrics(asset); return imageMetrics.get(idOrImg) || null; } if (!idOrImg) return null; for (const [id, img] of images) { if (img === idOrImg) return imageMetrics.get(id) || null; } const w = idOrImg.naturalWidth || idOrImg.width || 0; const h = idOrImg.naturalHeight || idOrImg.height || 0; return w && h ? { w, h, ratio: h / w, visibleBottomRatio: 1 } : null; } setupAssetIndex(); window.TarinaiAssets = { images, imageBoundsCache, imageMetrics, imagePromises, scaledSpriteCache, stainOverlayCache, loadImages, startBackgroundImageLoading, trimImageMemory, ensureImage, isImageReady, getRenderableImage, imageAlphaBounds, getImageMetrics, getCachedSpriteCanvas, getCachedStainOverlayCanvas };