640 lines
24 KiB
JavaScript
640 lines
24 KiB
JavaScript
// 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);
|
|
}
|
|
const PASSIVE_FOOD_DECAY_PER_SECOND = 0.004;
|
|
const PASSIVE_FOOD_HYGIENE_PENALTY_PER_SERVING = 0.055;
|
|
|
|
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 typeof itemVisualDefinition === "function" ? itemVisualDefinition(type) : null;
|
|
}
|
|
|
|
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"].includes(key)
|
|
|| (typeof isParamEffectItemType === "function" && 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" || 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 {
|
|
ctx.fillStyle = "#fff7dc";
|
|
const dots = compact ? 4 : 5;
|
|
for (let i = 0; i < dots; i++) {
|
|
ctx.beginPath();
|
|
ctx.arc(randSeed(localSeed + i, -localR * 0.55, localR * 0.55), randSeed(localSeed + i + 7, -localR * 0.34, localR * 0.34), Math.max(1.0, localR * 0.12), 0, Math.PI * 2);
|
|
ctx.fill();
|
|
}
|
|
}
|
|
} 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 (key === "sleep_drug") {
|
|
drawSleepTabletSprite(ctx, localR * visualFieldScaleFor("sleep_drug"), visualPaletteFor("sleep_drug"));
|
|
} else if (typeof isParamEffectItemType === "function" && 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 (typeof isPinType === "function" && isPinType(key)) {
|
|
const behavior = typeof pinBehaviorFor === "function" ? pinBehaviorFor(key) : null;
|
|
const assetId = behavior?.looseAsset || key;
|
|
const img = typeof getRenderableImage === "function" ? getRenderableImage(assetId, assetId) : images.get(assetId);
|
|
ctx.save();
|
|
ctx.rotate(-0.35);
|
|
if (img) {
|
|
const metrics = typeof getImageMetrics === "function" ? getImageMetrics(assetId) : null;
|
|
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 = typeof itemDefinition === "function" ? itemDefinition(type) : null;
|
|
const base = Number(def?.radius || itemRadiusFor?.(type, 16) || 16);
|
|
if (type === "genkotsu") return 30;
|
|
if (type === "fence_v" || type === "fence_h" || type === "bounce_fence" || type === "bounce_fence_v" || type === "gate_fence") return 24;
|
|
if (type === "fan" || type === "magnet") return 24;
|
|
if (type === "rotator") return 25;
|
|
if (type === "nest_box") return 26;
|
|
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",
|
|
antCount: typeof ANT_NEST_START_COUNT !== "undefined" ? ANT_NEST_START_COUNT : 6,
|
|
fuseTimer: 5,
|
|
fuseMax: 5,
|
|
impactFlash: 0,
|
|
foodServingsMax: typeof foodServingsForSize === "function" ? foodServingsForSize("medium") : 5,
|
|
foodServingsRemaining: typeof foodServingsForSize === "function" ? foodServingsForSize("medium") : 5,
|
|
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 === "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,
|
|
signboard: 0.76,
|
|
duplicator: 0.82,
|
|
fence_v: 0.70,
|
|
fence_h: 0.70,
|
|
bounce_fence: 0.74,
|
|
bounce_fence_v: 0.74,
|
|
gate_fence: 0.74,
|
|
fan: 0.86,
|
|
magnet: 0.88,
|
|
ant_nest: 0.92,
|
|
ball: 0.92,
|
|
pushpin: 0.92,
|
|
oshibyo: 0.62,
|
|
firecracker: 0.90,
|
|
giant_drug: 1.22,
|
|
dwarf_drug: 0.56,
|
|
};
|
|
let scale = scaleMap[type] || 0.92;
|
|
if (isConsumableSpriteType(type)) scale *= 1.95;
|
|
else if (["grass", "water", "water_bowl", "zunda_juice"].includes(type)) scale *= 1.85;
|
|
else if (type === "duplicator") scale *= 1.55;
|
|
else if (["signboard", "bed", "nest_box", "ant_nest", "pushpin", "oshibyo", "fence_v", "fence_h", "bounce_fence", "bounce_fence_v", "gate_fence", "fan", "magnet", "firecracker", "genkotsu"].includes(type)) scale *= 1.08;
|
|
else if (typeof isParamEffectItemType === "function" && 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.drawConsumableFieldSprite = drawConsumableFieldSprite;
|
|
window.drawStoredConsumableSprite = drawStoredConsumableSprite;
|
|
window.drawToolItemPreview = drawToolItemPreview;
|
|
window.TarinaiGrass = {
|
|
stageCount: GRASS_STAGE_COUNT,
|
|
normalize: normalizeGrassStage,
|
|
setStage: setGrassStage,
|
|
advance: advanceGrassStage,
|
|
regress: regressGrassStage,
|
|
amountForStage: grassAmountForStage,
|
|
growthForStage: grassGrowthForStage,
|
|
stageFromGrowth: grassStageFromGrowth,
|
|
};
|
|
}
|
|
|
|
|
|
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;
|