// Item construction and shared item helpers. Type-specific initialization is in item_type_initializers.js. "use strict"; const FOOD_SERVING_TYPES = new Set(window.TarinaiItemRegistry.food?.servingTypes?.() || ["food", "sweet", "love_mochi", "fight_mochi", "sleep_drug", "laxative", "protein", "niteropu", "ammo", "mystery_drug", "mercury", "giant_drug", "dwarf_drug", "zunda_juice"]); const PARAM_EFFECT_ITEM_TYPES = new Set(window.TarinaiItemRegistry.food?.paramEffectTypes?.() || ["laxative", "protein", "niteropu", "ammo", "mystery_drug", "mercury", "giant_drug", "dwarf_drug", "zunda_juice"]); function isParamEffectItemType(type = "") { return PARAM_EFFECT_ITEM_TYPES.has(type); } const FOOD_SERVINGS_BY_SIZE = { small: 1, medium: 5, large: 15 }; function foodServingsForSize(size = "medium") { return FOOD_SERVINGS_BY_SIZE[size] || FOOD_SERVINGS_BY_SIZE.medium; } function isServingFoodType(type = "") { return FOOD_SERVING_TYPES.has(type); } function foodServingSizeForRemaining(remaining = FOOD_SERVINGS_BY_SIZE.medium) { const n = Math.max(0, Number(remaining) || 0); if (n <= FOOD_SERVINGS_BY_SIZE.small + 0.05) return "small"; if (n <= FOOD_SERVINGS_BY_SIZE.medium + 0.05) return "medium"; return "large"; } function foodServingVisualScaleFor(item) { if (!item || !isServingFoodType(item.type)) return 1; const remaining = Math.max(0, Number(item.foodServingsRemaining ?? item.amount ?? FOOD_SERVINGS_BY_SIZE.medium) || 0); const ratio = remaining / FOOD_SERVINGS_BY_SIZE.medium; return clamp(Math.sqrt(Math.max(0.04, ratio)), 0.72, 1.35); } function updateServingFoodVisualSize(item) { if (!item || !isServingFoodType(item.type)) return false; const baseRadius = itemRadiusFor(item.type, 12); const scale = foodServingVisualScaleFor(item); const nextR = Math.max(5, baseRadius * scale); const changed = Math.abs((Number(item.r) || 0) - nextR) > 0.05; item.r = nextR; item.foodServingScale = scale; item.foodServingVisualSize = foodServingSizeForRemaining(item.foodServingsRemaining ?? item.amount); return changed; } const PASSIVE_FOOD_DECAY_PER_SECOND = 0.004; const GRASS_STAGE_COUNT = 5; const GRASS_STAGE_AMOUNTS = [24, 48, 72, 96, 120]; const GRASS_STAGE_GROWTH = [0.18, 0.36, 0.54, 0.72, 0.88]; function clampGrassStage(stage = 0) { return clamp(Math.round(Number(stage) || 0), 0, GRASS_STAGE_COUNT - 1); } function grassStageFromGrowth(growth = 0.18, amount = null) { if (Number.isFinite(amount) && amount <= 0) return 0; if (Number.isFinite(amount) && amount > 0) { for (let i = GRASS_STAGE_AMOUNTS.length - 1; i >= 0; i--) { if (amount >= GRASS_STAGE_AMOUNTS[i] - 4) return i; } } const g = clamp(Number(growth) || 0, 0, GRASS_STAGE_GROWTH[GRASS_STAGE_GROWTH.length - 1]); let best = 0, bestD = Infinity; for (let i = 0; i < GRASS_STAGE_GROWTH.length; i++) { const d = Math.abs(GRASS_STAGE_GROWTH[i] - g); if (d < bestD) { best = i; bestD = d; } } return best; } function grassAmountForStage(stage = 0) { return GRASS_STAGE_AMOUNTS[clampGrassStage(stage)] || GRASS_STAGE_AMOUNTS[0]; } function grassGrowthForStage(stage = 0) { return GRASS_STAGE_GROWTH[clampGrassStage(stage)] || GRASS_STAGE_GROWTH[0]; } function setGrassStage(item, stage = 0, opts = {}) { if (!item || item.type !== "grass") return false; const oldStage = Number.isFinite(item.grassStage) ? clampGrassStage(item.grassStage) : grassStageFromGrowth(item.growth, item.amount); if (stage < 0) { item.grassStage = 0; item.growth = 0; item.amount = 0; return oldStage !== -1 || opts.force; } const nextStage = clampGrassStage(stage); item.grassStage = nextStage; item.growth = grassGrowthForStage(nextStage); item.health = 1; item.wither = 0; item.fertilityBoost = 0; item.seedTimer = Math.max(Number(item.seedTimer || 0), 12); item.amount = grassAmountForStage(nextStage); return opts.force || nextStage !== oldStage; } function normalizeGrassStage(item) { if (!item || item.type !== "grass") return 0; const stage = Number.isFinite(item.grassStage) ? clampGrassStage(item.grassStage) : grassStageFromGrowth(item.growth, item.amount); setGrassStage(item, stage, { force: true }); return stage; } function advanceGrassStage(item, steps = 1) { if (!item || item.type !== "grass" || item.dead || (item.amount || 0) <= 0) return false; const stage = normalizeGrassStage(item); if (stage >= GRASS_STAGE_COUNT - 1) return false; return setGrassStage(item, stage + Math.max(1, Math.floor(steps || 1))); } function regressGrassStage(item, steps = 1) { if (!item || item.type !== "grass" || item.dead || (item.amount || 0) <= 0) return 0; const stage = normalizeGrassStage(item); const before = grassAmountForStage(stage); const next = stage - Math.max(1, Math.floor(steps || 1)); if (next < 0) { setGrassStage(item, -1); return before; } setGrassStage(item, next); return Math.max(1, before - grassAmountForStage(next)); } function passiveFoodDecayInterval(worldRef = null) { // Two decay ticks per half-day: dayLength/4. Default day is 120s -> 30s interval. const day = Number(worldRef?.config?.dayLength || CONFIG?.dayLength || 120) || 120; return Math.max(12, day / 4); } function passiveFoodDecayRate(item) { const size = item?.toolSize || "medium"; const sizeMul = size === "small" ? 0.74 : (size === "large" ? 1.32 : 1); const typeMul = window.TarinaiItemRegistry.food?.passiveDecayMultiplier?.(item?.type) ?? (item?.type === "sweet" ? 0.82 : (item?.type === "zunda_juice" ? 1.12 : 1)); return PASSIVE_FOOD_DECAY_PER_SECOND * sizeMul * typeMul; } function itemVisualFor(type = "") { return itemVisualDefinition(type); } function visualPaletteFor(type = "", fallback = {}) { return itemVisualFor(type)?.palette || fallback || {}; } function visualFieldScaleFor(type = "") { const visual = itemVisualFor(type); return visual?.fieldScale || visual?.scale || 1; } function drawOvalWaterDropSprite(ctx, r, palette = {}) { const fill = palette.fill || "rgba(86, 157, 224, 0.82)"; const stroke = palette.stroke || "rgba(45, 101, 168, 0.52)"; const highlight = palette.highlight || "rgba(255,255,255,0.64)"; ctx.save(); ctx.fillStyle = fill; ctx.strokeStyle = stroke; ctx.lineWidth = Math.max(1.6, r * 0.13); // Horizontal oval item droplet silhouette for fallen water / zunda juice / mercury; rain overlay itself is a separate diagonal-line effect. ctx.beginPath(); ctx.ellipse(0, 0, r * 1.42, r * 0.56, -0.04, 0, Math.PI * 2); ctx.fill(); ctx.stroke(); ctx.globalAlpha = 0.72; ctx.fillStyle = highlight; ctx.beginPath(); ctx.ellipse(-r * 0.46, -r * 0.16, r * 0.34, r * 0.13, -0.18, 0, Math.PI * 2); ctx.fill(); ctx.restore(); } function drawSleepTabletSprite(ctx, r, palette = {}) { const fill = palette.fill || "#ebe4ff"; const stroke = palette.stroke || "rgba(74, 60, 144, 0.72)"; const accent = palette.accent || "rgba(112, 91, 188, 0.82)"; ctx.save(); ctx.rotate(-0.18); ctx.fillStyle = fill; ctx.strokeStyle = stroke; ctx.lineWidth = Math.max(1.8, r * 0.14); ctx.beginPath(); ctx.ellipse(0, 0, r * 0.92, r * 0.66, 0, 0, Math.PI * 2); ctx.fill(); ctx.stroke(); ctx.fillStyle = accent; ctx.beginPath(); ctx.arc(r * 0.12, -r * 0.04, r * 0.38, 0, Math.PI * 2); ctx.fill(); ctx.fillStyle = fill; ctx.beginPath(); ctx.arc(r * 0.28, -r * 0.11, r * 0.38, 0, Math.PI * 2); ctx.fill(); ctx.globalAlpha = 0.52; ctx.strokeStyle = "rgba(255,255,255,0.94)"; ctx.lineWidth = Math.max(1.2, r * 0.08); ctx.beginPath(); ctx.moveTo(-r * 0.56, -r * 0.28); ctx.quadraticCurveTo(-r * 0.14, -r * 0.48, r * 0.48, -r * 0.20); ctx.stroke(); ctx.restore(); } function drawLaxativeTabletSprite(ctx, r) { ctx.save(); ctx.fillStyle = "#f4d49a"; ctx.strokeStyle = "rgba(98, 61, 29, 0.82)"; ctx.lineWidth = Math.max(1.8, r * 0.14); roundedRect(ctx, -r * 1.02, -r * 0.54, r * 2.04, r * 1.08, r * 0.34); ctx.fill(); ctx.stroke(); // Embedded zunchi mound motif: silhouette-based, not color-only. ctx.fillStyle = "rgba(96, 70, 38, 0.88)"; ctx.strokeStyle = "rgba(56, 39, 22, 0.42)"; ctx.lineWidth = Math.max(1.0, r * 0.07); const lumps = [ [-0.38, 0.11, 0.25, 0.17], [-0.12, -0.03, 0.29, 0.20], [0.20, 0.10, 0.27, 0.18], [0.03, -0.23, 0.20, 0.14], ]; for (const [x,y,w,h] of lumps) { ctx.beginPath(); ctx.ellipse(r*x, r*y, r*w, r*h, 0, 0, Math.PI * 2); ctx.fill(); ctx.stroke(); } ctx.globalAlpha = 0.45; ctx.strokeStyle = "rgba(255,255,255,0.90)"; ctx.lineWidth = Math.max(1.0, r * 0.07); ctx.beginPath(); ctx.moveTo(-r * 0.68, -r * 0.32); ctx.quadraticCurveTo(-r * 0.24, -r * 0.48, r * 0.46, -r * 0.26); ctx.stroke(); ctx.restore(); } function drawMysteryTabletSprite(ctx, r) { ctx.save(); ctx.fillStyle = "#f3ecff"; ctx.strokeStyle = "rgba(91, 55, 150, 0.84)"; ctx.lineWidth = Math.max(1.9, r * 0.15); ctx.beginPath(); ctx.ellipse(0, 0, r * 0.94, r * 0.78, 0.10, 0, Math.PI * 2); ctx.fill(); ctx.stroke(); ctx.fillStyle = "rgba(91,55,150,0.92)"; ctx.font = `bold ${Math.round(r * 1.35)}px ui-rounded, sans-serif`; ctx.textAlign = "center"; ctx.textBaseline = "middle"; ctx.fillText("?", 0, -r * 0.04); ctx.globalAlpha = 0.52; ctx.strokeStyle = "rgba(255,255,255,0.90)"; ctx.lineWidth = Math.max(1.1, r * 0.08); ctx.beginPath(); ctx.moveTo(-r * 0.52, -r * 0.34); ctx.quadraticCurveTo(-r * 0.12, -r * 0.54, r * 0.42, -r * 0.28); ctx.stroke(); ctx.restore(); } function drawBulletSprite(ctx, r) { ctx.save(); ctx.rotate(0.0); const g = ctx.createLinearGradient(-r, 0, r, 0); g.addColorStop(0, "#b06f23"); g.addColorStop(0.48, "#f4c46a"); g.addColorStop(1, "#a05a1f"); ctx.fillStyle = g; ctx.strokeStyle = "rgba(87, 51, 22, 0.78)"; ctx.lineWidth = Math.max(1.7, r * 0.13); ctx.beginPath(); ctx.moveTo(r * 0.86, 0); ctx.quadraticCurveTo(r * 0.42, -r * 0.52, -r * 0.34, -r * 0.52); ctx.lineTo(-r * 0.88, -r * 0.40); ctx.lineTo(-r * 0.88, r * 0.40); ctx.lineTo(-r * 0.34, r * 0.52); ctx.quadraticCurveTo(r * 0.42, r * 0.52, r * 0.86, 0); ctx.closePath(); ctx.fill(); ctx.stroke(); ctx.strokeStyle = "rgba(255,237,176,0.62)"; ctx.lineWidth = Math.max(1.0, r * 0.07); ctx.beginPath(); ctx.moveTo(-r * 0.44, -r * 0.34); ctx.lineTo(r * 0.42, -r * 0.20); ctx.stroke(); ctx.restore(); } function isConsumableSpriteType(type = "") { const key = String(type || ""); return ["food", "sweet", "love_mochi", "fight_mochi", "sleep_drug", "water", "zunda_juice", "mercury"].includes(key) || isParamEffectItemType(key); } function drawConsumableFieldSprite(ctx, type = "", r = 10, seed = 1, opts = {}) { const key = String(type || "food"); const localSeed = Number(seed || 1) || 1; const localR = Math.max(1, Number(r) || 10); const compact = !!opts.compact; ctx.save(); if (key === "food") { ctx.fillStyle = "#b97842"; ctx.strokeStyle = "#754725"; ctx.lineWidth = Math.max(1.2, localR * 0.14); ctx.beginPath(); ctx.ellipse(0, localR * 0.16, localR * 1.18, localR * 0.48, 0, 0, Math.PI * 2); ctx.fill(); ctx.stroke(); ctx.fillStyle = "#d9aa70"; ctx.beginPath(); ctx.ellipse(0, -localR * 0.02, localR * 0.92, localR * 0.42, 0, 0, Math.PI * 2); ctx.fill(); const dots = compact ? 5 : 7; ctx.fillStyle = "#7d4d2c"; for (let i = 0; i < dots; i++) { ctx.beginPath(); ctx.arc(randSeed(localSeed + i, -localR * 0.62, localR * 0.62), randSeed(localSeed + i + 9, -localR * 0.25, localR * 0.20), Math.max(1.1, localR * 0.14), 0, Math.PI * 2); ctx.fill(); } } else if (key === "sweet" || key === "love_mochi" || key === "fight_mochi") { const sweet = key === "sweet"; const loveMochi = key === "love_mochi"; const fightMochi = key === "fight_mochi"; ctx.fillStyle = loveMochi ? "#fff1f6" : (fightMochi ? "#fff3ec" : (sweet ? "#fffaf1" : "#f1dfb7")); ctx.strokeStyle = loveMochi ? "#bf6f8b" : (fightMochi ? "#bc6d42" : (sweet ? "#9a8a72" : "#8d6f48")); ctx.lineWidth = Math.max(1.2, localR * 0.15); ctx.beginPath(); ctx.ellipse(0, 0, localR * 1.25, localR * 0.75, Math.sin(localSeed) * 0.4, 0, Math.PI * 2); ctx.fill(); ctx.stroke(); if (sweet) { ctx.beginPath(); ctx.fillStyle = "#76b94d"; ctx.ellipse(-localR * 0.07, -localR * 0.24, localR * 0.76, localR * 0.26, Math.sin(localSeed) * 0.25, 0, Math.PI * 2); ctx.fill(); ctx.fillStyle = "rgba(255,255,255,0.62)"; ctx.beginPath(); ctx.ellipse(-localR * 0.32, -localR * 0.16, localR * 0.24, localR * 0.10, -0.35, 0, Math.PI * 2); ctx.fill(); } else if (loveMochi) { ctx.fillStyle = "#ff6a9c"; ctx.font = `${Math.round(localR * 1.15)}px sans-serif`; ctx.textAlign = "center"; ctx.textBaseline = "middle"; ctx.fillText("\u2764", 0, 1); ctx.fillStyle = "rgba(255,255,255,0.55)"; ctx.beginPath(); ctx.ellipse(-localR * 0.28, -localR * 0.18, localR * 0.23, localR * 0.10, -0.35, 0, Math.PI * 2); ctx.fill(); } else if (fightMochi) { ctx.strokeStyle = "rgba(168,76,42,0.82)"; ctx.lineWidth = Math.max(1.2, localR * 0.13); ctx.beginPath(); ctx.moveTo(-localR * 0.44, -localR * 0.42); ctx.lineTo(localR * 0.44, localR * 0.42); ctx.moveTo(-localR * 0.44, localR * 0.42); ctx.lineTo(localR * 0.44, -localR * 0.42); ctx.stroke(); } } else if (key === "grass") { ctx.strokeStyle = "#5fa53f"; ctx.lineWidth = Math.max(1.3, localR * 0.14); ctx.lineCap = "round"; for (let i = 0; i < 5; i++) { const a = -Math.PI / 2 + randSeed(localSeed + i, -0.75, 0.75); const len = localR * randSeed(localSeed + i + 10, 0.65, 1.15); const rootX = randSeed(localSeed + i + 20, -localR * 0.32, localR * 0.32); ctx.beginPath(); ctx.moveTo(rootX, localR * 0.35); ctx.quadraticCurveTo(rootX + Math.cos(a) * len * 0.46, -localR * 0.05, rootX + Math.cos(a) * len, Math.sin(a) * len * 0.72); ctx.stroke(); } } else if (["water", "zunda_juice", "mercury"].includes(key)) { const visual = itemVisualFor(key); const palette = visual?.palette || {}; drawOvalWaterDropSprite(ctx, localR * (visual?.fieldScale || visual?.scale || 1), { fill: palette.fill, stroke: palette.stroke, highlight: palette.highlight || palette.accent }); } else if (key === "sleep_drug") { drawSleepTabletSprite(ctx, localR * visualFieldScaleFor("sleep_drug"), visualPaletteFor("sleep_drug")); } else if (isParamEffectItemType(key)) { const visual = itemVisualFor(key); const palette = visual?.palette || {}; const renderer = visual?.renderer || "split_pill"; const fill = palette.fill || "#fffaf1"; const stroke = palette.stroke || "#8d6f48"; const accent = palette.highlight || palette.accent || "#ddd"; const rr = localR * (visual?.fieldScale || visual?.scale || 1); if (renderer === "powder_pile") { ctx.fillStyle = "rgba(68,46,28,0.18)"; ctx.beginPath(); ctx.ellipse(0, rr * 0.50, rr * 1.22, rr * 0.34, 0, 0, Math.PI * 2); ctx.fill(); const count = compact ? 10 : 18; for (let i = 0; i < count; i++) { const px = randSeed(localSeed + i * 7, -rr * 0.85, rr * 0.85); const layer = 1 - Math.abs(px) / Math.max(1, rr * 0.90); const py = randSeed(localSeed + i * 11, -rr * 0.15, rr * 0.48) - layer * rr * 0.48; const blobR = randSeed(localSeed + i * 13, rr * 0.14, rr * 0.32) * (0.75 + layer * 0.45); ctx.fillStyle = i % 3 === 0 ? accent : fill; ctx.strokeStyle = stroke; ctx.globalAlpha = 0.86 + layer * 0.12; ctx.lineWidth = Math.max(0.8, rr * 0.055); ctx.beginPath(); ctx.ellipse(px, py, blobR * 1.18, blobR * 0.72, randSeed(localSeed + i, -0.45, 0.45), 0, Math.PI * 2); ctx.fill(); ctx.stroke(); } ctx.globalAlpha = 1; ctx.fillStyle = "rgba(255,255,255,0.62)"; ctx.beginPath(); ctx.ellipse(-rr * 0.30, -rr * 0.42, rr * 0.28, rr * 0.10, -0.35, 0, Math.PI * 2); ctx.fill(); } else if (renderer === "oval_droplet") { drawOvalWaterDropSprite(ctx, rr, { fill, stroke, highlight: accent }); } else if (renderer === "laxative_tablet") { drawLaxativeTabletSprite(ctx, rr); } else if (renderer === "mystery_tablet") { drawMysteryTabletSprite(ctx, rr); } else if (renderer === "bullet") { drawBulletSprite(ctx, rr); } else { ctx.save(); ctx.rotate(-0.38); ctx.fillStyle = fill; ctx.strokeStyle = stroke; ctx.lineWidth = Math.max(1.3, rr * 0.12); roundedRect(ctx, -rr * 1.15, -rr * 0.46, rr * 2.30, rr * 0.92, rr * 0.46); ctx.fill(); ctx.stroke(); ctx.fillStyle = accent; ctx.globalAlpha = 0.78; ctx.fillRect(-rr * 0.10, -rr * 0.42, rr * 0.20, rr * 0.84); ctx.globalAlpha = 0.36; ctx.fillStyle = "rgba(255,255,255,0.92)"; ctx.beginPath(); ctx.ellipse(-rr * 0.48, -rr * 0.16, rr * 0.32, rr * 0.10, -0.10, 0, Math.PI * 2); ctx.fill(); ctx.restore(); } } else { ctx.fillStyle = "#e4ba5b"; ctx.strokeStyle = "rgba(105,74,39,0.62)"; ctx.lineWidth = Math.max(1.1, localR * 0.11); ctx.beginPath(); ctx.ellipse(0, 0, localR * 0.98, localR * 0.58, -0.08, 0, Math.PI * 2); ctx.fill(); ctx.stroke(); } ctx.restore(); } function drawStoredConsumableSprite(ctx, type = "", r = 10, seed = 1) { const key = String(type || "food"); if (isPinType(key)) { const behavior = pinBehaviorFor(key); const assetId = behavior?.looseAsset || key; const img = getRenderableImage(assetId, assetId); ctx.save(); ctx.rotate(-0.35); if (img) { const metrics = getImageMetrics(assetId); const ratio = metrics?.ratio || 2.0; const w = key === "oshibyo" ? r * 1.30 : r * 1.70; const h = w * ratio; ctx.drawImage(img, -w * 0.5, -h * 0.5, w, h); } else { ctx.fillStyle = key === "oshibyo" ? "#4d86d9" : "#d92736"; ctx.strokeStyle = "rgba(84,36,42,0.72)"; ctx.lineWidth = Math.max(1, r * 0.10); ctx.beginPath(); ctx.ellipse(0, -r * 0.18, r * 0.58, r * 0.38, 0, 0, Math.PI * 2); ctx.fill(); ctx.stroke(); ctx.strokeStyle = "rgba(220,220,226,0.92)"; ctx.beginPath(); ctx.moveTo(0, r * 0.18); ctx.lineTo(0, r * 1.25); ctx.stroke(); } ctx.restore(); return; } drawConsumableFieldSprite(ctx, key, r, seed, { compact: true }); } function previewItemRadiusFor(type = "") { const def = itemDefinition(type); const base = Number(def?.radius || itemRadiusFor?.(type, 16) || 16); if (type === "genkotsu") return 30; if (type === "fence_v" || type === "fence_h" || type === "glass_wall" || type === "bounce_fence" || type === "bounce_fence_v" || type === "gate_fence") return 24; if (type === "rotator") return 25; if (type === "reciprocator") return 26; if (type === "nest_box") return 26; if (type === "pipe") return 24; if (type === "duplicator") return 24; if (type === "signboard") return 22; if (type === "bed") return 22; if (type === "pushpin" || type === "oshibyo") return 12; return clamp(base, 8, 24); } function canvasAlphaBounds(canvas, alphaThreshold = 8) { const w = Math.max(1, Math.floor(canvas?.width || 0)); const h = Math.max(1, Math.floor(canvas?.height || 0)); if (!w || !h) return null; const ctx = canvas.getContext?.("2d", { willReadFrequently: true }); if (!ctx) return null; let data = null; try { data = ctx.getImageData(0, 0, w, h).data; } catch (_) { return null; } let minX = w; let minY = h; let maxX = -1; let maxY = -1; for (let y = 0; y < h; y++) { for (let x = 0; x < w; x++) { if (data[(y * w + x) * 4 + 3] <= alphaThreshold) continue; if (x < minX) minX = x; if (y < minY) minY = y; if (x > maxX) maxX = x; if (y > maxY) maxY = y; } } if (maxX < minX || maxY < minY) return null; return { x: minX, y: minY, w: maxX - minX + 1, h: maxY - minY + 1 }; } function normalizeToolPreviewCanvas(targetCtx, sourceCanvas, w, h, opts = {}) { if (!targetCtx || !sourceCanvas) return false; const box = canvasAlphaBounds(sourceCanvas); targetCtx.clearRect(0, 0, w, h); if (!box) return false; const watermark = !!opts.watermark; const targetScale = Number.isFinite(Number(opts.targetScale)) ? Math.max(0.35, Number(opts.targetScale)) : 1; const maxW = w * (watermark ? 0.68 : 0.86) * targetScale; const maxH = h * (watermark ? 0.68 : 0.82) * targetScale; const scale = Math.min(maxW / Math.max(1, box.w), maxH / Math.max(1, box.h)); const drawW = box.w * scale; const drawH = box.h * scale; const centerX = watermark ? w * 0.78 : w * 0.50; const centerY = watermark ? h * 0.80 : h * 0.48; const dx = centerX - drawW / 2; const dy = centerY - drawH / 2; targetCtx.save(); targetCtx.imageSmoothingEnabled = true; targetCtx.imageSmoothingQuality = "high"; targetCtx.drawImage(sourceCanvas, box.x, box.y, box.w, box.h, dx, dy, drawW, drawH); targetCtx.restore(); return true; } function drawToolItemPreview(ctx, type = "", opts = {}) { if (!ctx || !type) return false; const w = Math.max(1, Number(opts.width || ctx.canvas?.width || 72)); const h = Math.max(1, Number(opts.height || ctx.canvas?.height || 72)); const watermark = !!opts.watermark; const localR = previewItemRadiusFor(type); const item = Object.create(Item.prototype); Object.assign(item, { id: `tool-preview-${type}`, type, x: 0, y: 0, r: localR, amount: 999, age: 0, seed: 72.43, roles: {}, dropTimer: 0, dropMax: 0, dropImpactDone: false, growth: type === "grass" ? 0.86 : undefined, health: 1, wear: 0, text: type === "signboard" ? "" : undefined, storedFoodType: type === "duplicator" ? "sweet" : "", storedFoodLabel: type === "duplicator" ? "\u305a\u3093\u3060\u9905" : "", pinState: "loose", spin: type === "pushpin" || type === "oshibyo" ? -0.55 : 0, zunchiVariant: "zunchi", stage: type === "zunchi" ? "fresh" : undefined, freshness: type === "zunchi" ? 1 : undefined, fertility: type === "zunchi" ? 0 : undefined, antCount: typeof ANT_NEST_START_COUNT !== "undefined" ? ANT_NEST_START_COUNT : 6, fuseTimer: 5, fuseMax: 5, impactFlash: 0, foodServingsMax: foodServingsForSize("medium"), foodServingsRemaining: foodServingsForSize("medium"), toolSize: "medium", }); if (type === "water") item.amount = 50; const outputCtx = ctx; let sourceCanvas = null; let drawCtx = ctx; try { const ownerDocument = ctx.canvas?.ownerDocument || (typeof document !== "undefined" ? document : null); sourceCanvas = ownerDocument?.createElement?.("canvas") || null; if (sourceCanvas) { sourceCanvas.width = w; sourceCanvas.height = h; drawCtx = sourceCanvas.getContext("2d") || ctx; } } catch (_) { sourceCanvas = null; drawCtx = ctx; } ctx = drawCtx; ctx.save(); ctx.clearRect(0, 0, w, h); const baseOffsetY = type === "signboard" ? 11 : type === "genkotsu" ? 12 : type === "fence_v" || type === "fence_h" || type === "glass_wall" || type === "bounce_fence" || type === "bounce_fence_v" || type === "gate_fence" ? 1 : 4; const centerX = watermark ? w * 0.78 : w / 2; const centerY = watermark ? (h * 0.80 + baseOffsetY * 0.15) : (h / 2 + baseOffsetY); ctx.translate(centerX, centerY); const scaleMap = { genkotsu: 0.52, bed: 0.80, nest_box: 0.78, pipe: 0.84, signboard: 0.76, toilet: 0.45, rain_shelter: 0.62, duplicator: 0.82, fence_v: 0.70, fence_h: 0.70, glass_wall: 0.74, bounce_fence: 0.74, bounce_fence_v: 0.74, gate_fence: 0.74, reciprocator: 0.76, ant_nest: 0.92, ball: 0.92, balloon: 0.86, fan: 0.82, pushpin: 0.92, oshibyo: 0.62, firecracker: 0.90, flame_firecracker: 0.90, sticky_bomb: 0.92, fire: 1.05, giant_drug: 1.22, dwarf_drug: 0.56, }; let scale = scaleMap[type] || 0.92; if (isConsumableSpriteType(type)) scale *= 1.95; else if (["grass", "water", "zunda_juice", "mercury", "fire"].includes(type)) scale *= 1.85; else if (type === "duplicator") scale *= 1.55; else if (["signboard", "toilet", "rain_shelter", "bed", "nest_box", "pipe", "ant_nest", "pushpin", "oshibyo", "fence_v", "fence_h", "glass_wall", "bounce_fence", "bounce_fence_v", "gate_fence", "reciprocator", "firecracker", "flame_firecracker", "sticky_bomb", "fire", "genkotsu"].includes(type)) scale *= 1.08; else if (isParamEffectItemType(type)) scale *= 1.35; if (watermark) scale *= 1.42; ctx.scale(scale, scale); try { item.draw(ctx, 0, typeof getLightingState === "function" ? getLightingState(globalThis.world) : null); } catch (err) { ctx.restore(); return false; } ctx.restore(); if (sourceCanvas && outputCtx !== ctx) { const previewScaleOverrides = { giant_drug: 1.55, dwarf_drug: 0.55, oshibyo: 0.58 }; normalizeToolPreviewCanvas(outputCtx, sourceCanvas, w, h, { watermark, targetScale: previewScaleOverrides[type] || 1 }); } return true; } if (typeof window !== "undefined") { window.isConsumableSpriteType = isConsumableSpriteType; window.updateServingFoodVisualSize = updateServingFoodVisualSize; window.drawConsumableFieldSprite = drawConsumableFieldSprite; window.drawStoredConsumableSprite = drawStoredConsumableSprite; window.drawToolItemPreview = drawToolItemPreview; window.TarinaiGrass = { normalize: normalizeGrassStage, setStage: setGrassStage, advance: advanceGrassStage, regress: regressGrassStage, }; } class Item { constructor(type, x, y) { this.id = crypto.randomUUID ? crypto.randomUUID() : `${Date.now()}-${Math.random()}`; this.type = type; this.x = x; this.y = y; this.r = itemRadiusFor(type, 12); this.amount = itemAmountFor(type, 80); this.age = 0; this.seed = Math.random() * 1000; this.roles = {}; this.needEffects = { food: 0, sleep: 0, health: 0, safety: 0, social: 0, fulfill: 0 }; this.dropTimer = 0; this.dropMax = 0; this.dropImpactDone = false; this.lifecycleTimer = rand(0, type === "grass" ? 1.15 : 0.55); this.lifecycleInterval = type === "grass" ? (1.75 + stableUnit(this.id, "grass-life") * 0.90) : (type === "zunchi" ? (0.95 + stableUnit(this.id, "zunchi-life") * 0.45) : 0); if (typeof initializeItemTypeState === "function") initializeItemTypeState(this, type, x, y); } get dead() { return this.amount <= 0; } } if (typeof window !== "undefined") window.Item = Item;