533 lines
26 KiB
JavaScript
533 lines
26 KiB
JavaScript
"use strict";
|
|
|
|
// Item field rendering. Kept separate from construction and lifecycle logic.
|
|
|
|
Object.assign(Item.prototype, {
|
|
draw(ctx, t, lighting = null) {
|
|
const lightState = lighting || getLightingState(world);
|
|
const night = clamp(lightState.nightStrength * 1.35, 0, 1);
|
|
const warm = lightState.warmth;
|
|
const styleNight = lightState.light < 0.42;
|
|
const styleWarm = !styleNight && (lightState.goldenStrength > 0.22 || warm > 0.55);
|
|
if (isPinType(this.type) && this.pinState === "lodged") {
|
|
const owner = world?.liveTarinaiById?.(this.pinTargetId) || null;
|
|
if (owner?.insideNestBoxId) return;
|
|
}
|
|
const servingRatio = isServingFoodType(this.type) ? ((this.foodServingsRemaining ?? this.amount) / Math.max(1, this.foodServingsMax || foodServingsForSize(this.toolSize || "medium"))) : null;
|
|
const visibleAlpha = isServingFoodType(this.type) ? clamp(servingRatio ?? 1, 0.24, 1) : clamp(this.amount / 40, 0.24, 1);
|
|
const dropT = this.dropMax > 0 ? clamp(this.dropTimer / this.dropMax, 0, 1) : 0;
|
|
const dropFallHeight = this.type === "genkotsu" ? Math.max(360, this.r * 5.8) : 170;
|
|
const dropY = dropT > 0 ? -dropFallHeight * dropT : 0;
|
|
const shadow = projectedShadowParams(lightState, Math.max(0.4, this.r / 18));
|
|
if (this.type !== "trace" && this.type !== "splat") {
|
|
const zunchiId = this.type === "zunchi" ? (this.zunchiVariant || "zunchi") : null;
|
|
const zunchiImg = zunchiId ? (typeof getRenderableImage === "function" ? getRenderableImage(zunchiId, "zunchi") : (images.get(zunchiId) || images.get("zunchi"))) : null;
|
|
if (zunchiImg) {
|
|
const metrics = getImageMetrics(zunchiId || "zunchi") || getImageMetrics("zunchi");
|
|
const ratio = metrics?.ratio || 178 / 236;
|
|
const zunchiH = this.r * 1.66 * ratio;
|
|
drawImageProjectedShadow(ctx, zunchiImg, this.x, this.y + imageVisibleBottomFromTop(zunchiImg, -this.r * 1.35, zunchiH), this.r * 2.2, zunchiH, shadow, {
|
|
alpha: shadow.alpha * visibleAlpha * 1.18,
|
|
widthScale: 0.92,
|
|
heightScale: 0.92,
|
|
});
|
|
} else if (this.type === "grass") {
|
|
ctx.save();
|
|
ctx.globalAlpha = clamp(shadow.alpha * visibleAlpha * 0.62, 0.035, 0.13);
|
|
ctx.fillStyle = shadow.color;
|
|
ctx.beginPath();
|
|
ctx.ellipse(this.x, this.y + this.r * 0.46, this.r * 0.62, this.r * 0.12, 0, 0, Math.PI * 2);
|
|
ctx.fill();
|
|
ctx.restore();
|
|
} else {
|
|
drawProjectedShadow(ctx, this.x, this.y + this.r * 0.52, this.r * 0.82, this.r * 0.26, { ...shadow, alpha: shadow.alpha * visibleAlpha * 1.25 });
|
|
}
|
|
}
|
|
drawItemGlow(ctx, this, lightState, visibleAlpha);
|
|
if (world.pointer?.inside && this.type !== "trace" && this.type !== "splat" && distXY(this.x, this.y, world.pointer.x, world.pointer.y) < this.r * 2.2) {
|
|
ctx.save();
|
|
ctx.globalAlpha = 0.32;
|
|
ctx.strokeStyle = "rgba(255, 250, 220, 0.82)";
|
|
ctx.lineWidth = 1.4;
|
|
ctx.beginPath();
|
|
ctx.ellipse(this.x, this.y + this.r * 0.1, this.r * 1.35, this.r * 0.92, 0, 0, Math.PI * 2);
|
|
ctx.stroke();
|
|
ctx.restore();
|
|
}
|
|
ctx.save();
|
|
ctx.translate(this.x, this.y + dropY);
|
|
ctx.globalAlpha = visibleAlpha;
|
|
if (isConsumableSpriteType(this.type)) {
|
|
drawConsumableFieldSprite(ctx, this.type, this.r, this.seed);
|
|
} else if (this.type === "genkotsu") {
|
|
const img = typeof getRenderableImage === "function" ? getRenderableImage("genkotsu", "genkotsu") : images.get("genkotsu");
|
|
const impact = clamp(this.impactFlash || 0, 0, 1);
|
|
ctx.save();
|
|
ctx.shadowColor = "rgba(54,42,31,0.26)";
|
|
ctx.shadowBlur = 5 + impact * 8;
|
|
ctx.shadowOffsetY = 4;
|
|
const squashY = 1 - impact * 0.08;
|
|
const stretchX = 1 + impact * 0.05;
|
|
ctx.scale(stretchX, squashY);
|
|
if (img) {
|
|
const metrics = typeof getImageMetrics === "function" ? getImageMetrics("genkotsu") : null;
|
|
const w = this.r * 2.55;
|
|
const h = w * (metrics?.ratio || 1.27);
|
|
ctx.drawImage(img, -w * 0.5, -h * 0.76, w, h);
|
|
} else {
|
|
ctx.fillStyle = "rgba(255,255,255,0.94)";
|
|
ctx.strokeStyle = "rgba(64,64,64,0.78)";
|
|
ctx.lineWidth = 5;
|
|
roundedRect(ctx, -this.r * 0.72, -this.r * 1.45, this.r * 1.44, this.r * 1.84, 18);
|
|
ctx.fill(); ctx.stroke();
|
|
ctx.fillStyle = "rgba(42,46,52,0.92)";
|
|
ctx.font = `bold ${Math.round(this.r * 0.45)}px sans-serif`;
|
|
ctx.textAlign = "center";
|
|
ctx.textBaseline = "middle";
|
|
ctx.fillText("\u6b63", 0, -this.r * 0.74);
|
|
ctx.fillText("\u7fa9", 0, -this.r * 0.27);
|
|
}
|
|
if (impact > 0.01) {
|
|
ctx.globalAlpha = impact * 0.20;
|
|
ctx.fillStyle = "#ffffff";
|
|
ctx.beginPath();
|
|
ctx.ellipse(0, -this.r * 0.65, this.r * 0.92, this.r * 0.22, -0.10, 0, Math.PI * 2);
|
|
ctx.fill();
|
|
}
|
|
ctx.restore();
|
|
} else if (isPinType(this.type)) {
|
|
const stuck = this.pinState === "lodged";
|
|
const isOshibyo = this.type === "oshibyo";
|
|
const behavior = typeof pinBehaviorFor === "function" ? pinBehaviorFor(this.type) : null;
|
|
const assetId = behavior ? (stuck ? behavior.lodgedAsset : behavior.looseAsset) : (stuck ? "pushpin_stuck" : "pushpin");
|
|
const img = typeof getRenderableImage === "function" ? getRenderableImage(assetId, assetId) : images.get(assetId);
|
|
ctx.save();
|
|
if (stuck) ctx.scale((this.pinVisualSide || 1) < 0 ? -1 : 1, 1);
|
|
if (!stuck) ctx.rotate((this.spin || 0) + Math.PI * 0.5);
|
|
ctx.shadowColor = stuck ? "rgba(116,24,32,0.18)" : "rgba(54,42,31,0.16)";
|
|
ctx.shadowBlur = 3;
|
|
ctx.shadowOffsetY = 2;
|
|
if (img) {
|
|
const metrics = typeof getImageMetrics === "function" ? getImageMetrics(assetId) : null;
|
|
const ratio = metrics?.ratio || (stuck ? 1 : 2.0);
|
|
const w = isOshibyo ? (stuck ? this.r * 1.55 : this.r * 0.78) : (stuck ? this.r * 2.15 : this.r * 1.78);
|
|
const h = w * ratio;
|
|
ctx.drawImage(img, -w * 0.5, -h * 0.5, w, h);
|
|
} else if (stuck) {
|
|
ctx.fillStyle = "#d92736";
|
|
ctx.strokeStyle = "rgba(125,18,30,0.78)";
|
|
ctx.lineWidth = 2;
|
|
ctx.beginPath();
|
|
ctx.arc(0, 0, this.r * 0.68, 0, Math.PI * 2);
|
|
ctx.fill(); ctx.stroke();
|
|
ctx.beginPath();
|
|
ctx.arc(0, 0, this.r * 0.26, 0, Math.PI * 2);
|
|
ctx.fillStyle = "rgba(176,20,30,0.82)";
|
|
ctx.fill();
|
|
} else {
|
|
ctx.fillStyle = "#d92736";
|
|
ctx.strokeStyle = "rgba(125,18,30,0.78)";
|
|
ctx.lineWidth = 2;
|
|
ctx.beginPath();
|
|
ctx.ellipse(0, -this.r * 0.26, this.r * 0.70, this.r * 0.48, 0, 0, Math.PI * 2);
|
|
ctx.fill(); ctx.stroke();
|
|
ctx.beginPath();
|
|
ctx.ellipse(0, this.r * 0.18, this.r * 0.82, this.r * 0.30, 0, 0, Math.PI * 2);
|
|
ctx.fill(); ctx.stroke();
|
|
ctx.strokeStyle = "rgba(210,210,215,0.92)";
|
|
ctx.lineWidth = Math.max(1.5, this.r * 0.12);
|
|
ctx.beginPath();
|
|
ctx.moveTo(0, this.r * 0.42);
|
|
ctx.lineTo(0, this.r * 1.65);
|
|
ctx.stroke();
|
|
}
|
|
ctx.restore();
|
|
} else if (this.type === "water_bowl") {
|
|
const palette = visualPaletteFor("water_bowl");
|
|
ctx.fillStyle = palette.bowlFill || "rgba(171, 120, 76, 0.72)";
|
|
ctx.strokeStyle = palette.bowlStroke || "rgba(94, 69, 48, 0.55)";
|
|
ctx.lineWidth = 2.2;
|
|
ctx.beginPath();
|
|
ctx.ellipse(0, 2, this.r * 1.35, this.r * 0.78, 0, 0, Math.PI * 2);
|
|
ctx.fill(); ctx.stroke();
|
|
ctx.fillStyle = palette.waterFill || "rgba(86, 157, 224, 0.58)";
|
|
ctx.strokeStyle = palette.waterStroke || "rgba(62, 113, 178, 0.36)";
|
|
ctx.lineWidth = 1.6;
|
|
ctx.beginPath();
|
|
ctx.ellipse(0, -1, this.r * 1.02, this.r * 0.50, 0, 0, Math.PI * 2);
|
|
ctx.fill(); ctx.stroke();
|
|
ctx.fillStyle = palette.highlight || "rgba(255,255,255,0.54)";
|
|
ctx.beginPath(); ctx.arc(-this.r * 0.32, -this.r * 0.18, 2.8, 0, Math.PI * 2); ctx.fill();
|
|
} else if (this.type === "water") {
|
|
drawOvalWaterDropSprite(ctx, this.r * visualFieldScaleFor("water"), visualPaletteFor("water"));
|
|
} else if (this.type === "grass") {
|
|
const numericStage = Number.isFinite(this.grassStage) ? clampGrassStage(this.grassStage) : grassStageFromGrowth(this.growth, this.amount);
|
|
const growth = grassGrowthForStage(numericStage);
|
|
const eaten = (this.amount || 0) <= 0;
|
|
const stageLabel = numericStage <= 0 ? "sprout" : (numericStage <= 2 ? "young" : "mature");
|
|
const wither = clamp(this.wither || 0, 0, 1);
|
|
ctx.strokeStyle = eaten ? "#7b8755" : (wither > 0.08 ? `rgba(${Math.round(106 + wither * 40)}, ${Math.round(132 - wither * 32)}, ${Math.round(70 - wither * 18)}, 1)` : (styleNight ? "#294b40" : (styleWarm ? "#64ad3f" : "#4f8438")));
|
|
ctx.shadowColor = "transparent";
|
|
ctx.shadowBlur = 0;
|
|
ctx.lineCap = "round";
|
|
ctx.lineWidth = styleNight ? 1.5 : 1.8;
|
|
const bladeCount = Math.max(2, numericStage + 2);
|
|
for (let i = 0; i < bladeCount; i++) {
|
|
const spread = stageLabel === "mature" ? 1.05 : 0.68;
|
|
const a = -Math.PI / 2 + randSeed(this.seed + i, -spread, spread);
|
|
const len = this.r * randSeed(this.seed + i + 10, 0.42, stageLabel === "mature" ? 1.18 : 0.92) * clamp(growth, 0.18, 0.92);
|
|
const rootX = stageLabel === "mature" ? randSeed(this.seed + i + 31, -this.r * 0.28, this.r * 0.28) : 0;
|
|
ctx.beginPath();
|
|
ctx.moveTo(rootX, 8);
|
|
ctx.quadraticCurveTo(rootX + Math.cos(a) * len * 0.55, 8 - len * 0.24, rootX + Math.cos(a) * len, Math.sin(a) * len * 0.78);
|
|
ctx.stroke();
|
|
}
|
|
if (numericStage >= 3 && !eaten) {
|
|
ctx.save();
|
|
ctx.globalAlpha = 0.14;
|
|
ctx.fillStyle = styleNight ? "rgba(95,132,114,0.65)" : "rgba(138,189,108,0.72)";
|
|
ctx.beginPath();
|
|
ctx.ellipse(0, 1, this.r * 0.62, this.r * 0.20, 0, 0, Math.PI * 2);
|
|
ctx.fill();
|
|
ctx.restore();
|
|
}
|
|
} else if (this.type === "stone") {
|
|
ctx.fillStyle = styleNight ? "#8f94a6" : (styleWarm ? "#e4d6b5" : "#d9d0b8");
|
|
ctx.strokeStyle = styleNight ? "#5f6478" : "#8a7c63";
|
|
ctx.shadowColor = "transparent";
|
|
ctx.shadowBlur = 0;
|
|
ctx.lineWidth = 2;
|
|
roundedBlob(ctx, 0, 0, this.r * 1.25, this.r * 0.85, 8);
|
|
ctx.fill(); ctx.stroke();
|
|
ctx.fillStyle = "rgba(255,255,255,0.34)";
|
|
ctx.beginPath(); ctx.ellipse(-6, -5, 6, 3, -0.3, 0, Math.PI * 2); ctx.fill();
|
|
} else if (this.type === "bed") {
|
|
ctx.fillStyle = styleWarm ? "rgba(224, 190, 94, 0.90)" : (styleNight ? "rgba(177, 156, 95, 0.84)" : "rgba(205, 169, 86, 0.88)" );
|
|
ctx.strokeStyle = styleWarm ? "rgba(135, 101, 45, 0.62)" : (styleNight ? "rgba(102, 88, 56, 0.68)" : "rgba(117, 91, 47, 0.68)" );
|
|
ctx.lineWidth = 2.2;
|
|
ctx.beginPath();
|
|
ctx.ellipse(0, 3, this.r * 1.45, this.r * 0.72, -0.08, 0, Math.PI * 2);
|
|
ctx.fill();
|
|
ctx.stroke();
|
|
ctx.strokeStyle = "rgba(116, 89, 42, 0.45)";
|
|
ctx.lineWidth = 1.5;
|
|
for (let i = 0; i < 13; i++) {
|
|
const x = randSeed(this.seed + i, -this.r * 1.10, this.r * 1.08);
|
|
const y = randSeed(this.seed + i + 20, -this.r * 0.36, this.r * 0.46);
|
|
const len = randSeed(this.seed + i + 40, this.r * 0.34, this.r * 0.70);
|
|
const a = randSeed(this.seed + i + 60, -0.75, 0.75);
|
|
ctx.beginPath();
|
|
ctx.moveTo(x - Math.cos(a) * len * 0.5, y - Math.sin(a) * len * 0.5);
|
|
ctx.lineTo(x + Math.cos(a) * len * 0.5, y + Math.sin(a) * len * 0.5);
|
|
ctx.stroke();
|
|
}
|
|
} else if (this.type === "nest_box") {
|
|
ctx.fillStyle = styleNight ? "#5f4933" : (styleWarm ? "#89613e" : "#775237");
|
|
ctx.strokeStyle = styleNight ? "#31261f" : "#3f2e24";
|
|
ctx.lineWidth = 2.6;
|
|
roundedRect(ctx, -this.r * 1.28, -this.r * 0.80, this.r * 2.56, this.r * 1.52, 9);
|
|
ctx.fill(); ctx.stroke();
|
|
ctx.fillStyle = styleNight ? "#6e5439" : "#8a5f3b";
|
|
roundedRect(ctx, -this.r * 1.12, -this.r * 0.62, this.r * 2.24, this.r * 1.18, 6);
|
|
ctx.fill();
|
|
ctx.strokeStyle = styleNight ? "rgba(42,32,25,0.76)" : "rgba(65,45,32,0.72)";
|
|
ctx.lineWidth = 2.0;
|
|
ctx.beginPath();
|
|
ctx.moveTo(-this.r * 0.95, -this.r * 0.50);
|
|
ctx.lineTo(this.r * 0.95, -this.r * 0.50);
|
|
ctx.moveTo(-this.r * 0.95, this.r * 0.48);
|
|
ctx.lineTo(this.r * 0.95, this.r * 0.48);
|
|
ctx.stroke();
|
|
ctx.fillStyle = styleNight ? "#2b211c" : "#3c281f";
|
|
ctx.beginPath();
|
|
ctx.arc(0, -this.r * 0.08, this.r * 0.40, 0, Math.PI * 2);
|
|
ctx.fill();
|
|
} else if (this.type === "duplicator") {
|
|
ctx.save();
|
|
const loaded = !!this.storedFoodType;
|
|
const bodyGrad = ctx.createLinearGradient(-this.r * 1.1, -this.r * 1.0, this.r * 1.1, this.r * 0.9);
|
|
bodyGrad.addColorStop(0, styleNight ? "#3d4852" : "#d8e2e6");
|
|
bodyGrad.addColorStop(0.45, styleNight ? "#65717b" : "#f2f6f7");
|
|
bodyGrad.addColorStop(1, styleNight ? "#2c343c" : "#9fafb7");
|
|
ctx.fillStyle = bodyGrad;
|
|
ctx.strokeStyle = styleNight ? "#1b252c" : "#52636b";
|
|
ctx.lineWidth = Math.max(2, this.r * 0.075);
|
|
roundedRect(ctx, -this.r * 1.10, -this.r * 0.86, this.r * 1.52, this.r * 1.52, this.r * 0.20);
|
|
ctx.fill();
|
|
ctx.stroke();
|
|
|
|
ctx.fillStyle = styleNight ? "rgba(20,28,34,0.52)" : "rgba(255,255,255,0.72)";
|
|
roundedRect(ctx, -this.r * 0.92, -this.r * 0.64, this.r * 1.12, this.r * 0.42, this.r * 0.10);
|
|
ctx.fill();
|
|
ctx.strokeStyle = styleNight ? "rgba(160,190,210,0.25)" : "rgba(82,98,108,0.32)";
|
|
ctx.stroke();
|
|
|
|
ctx.fillStyle = loaded ? "rgba(83,221,116,0.88)" : "rgba(139,160,170,0.62)";
|
|
ctx.beginPath();
|
|
ctx.arc(-this.r * 0.70, -this.r * 0.43, this.r * 0.13, 0, Math.PI * 2);
|
|
ctx.fill();
|
|
ctx.strokeStyle = "rgba(35,47,54,0.52)";
|
|
ctx.stroke();
|
|
ctx.fillStyle = loaded ? "rgba(126,245,141,0.82)" : "rgba(255,255,255,0.45)";
|
|
ctx.beginPath();
|
|
ctx.arc(-this.r * 0.74, -this.r * 0.47, this.r * 0.045, 0, Math.PI * 2);
|
|
ctx.fill();
|
|
|
|
ctx.strokeStyle = styleNight ? "#232b30" : "#657079";
|
|
ctx.lineWidth = Math.max(1.6, this.r * 0.045);
|
|
ctx.beginPath();
|
|
ctx.moveTo(this.r * 0.18, -this.r * 0.34);
|
|
ctx.quadraticCurveTo(this.r * 0.48, -this.r * 0.52, this.r * 0.74, -this.r * 0.28);
|
|
ctx.stroke();
|
|
ctx.fillStyle = styleNight ? "#283139" : "#e6edf0";
|
|
ctx.beginPath();
|
|
ctx.arc(this.r * 0.78, -this.r * 0.28, this.r * 0.10, 0, Math.PI * 2);
|
|
ctx.fill();
|
|
ctx.stroke();
|
|
|
|
const dishGrad = ctx.createLinearGradient(this.r * 0.08, -this.r * 0.06, this.r * 1.28, this.r * 0.62);
|
|
dishGrad.addColorStop(0, styleNight ? "#3f454b" : "#eef4f4");
|
|
dishGrad.addColorStop(0.58, styleNight ? "#737c82" : "#ffffff");
|
|
dishGrad.addColorStop(1, styleNight ? "#242a30" : "#adb8bd");
|
|
ctx.fillStyle = dishGrad;
|
|
ctx.strokeStyle = styleNight ? "#12181d" : "#67767e";
|
|
ctx.lineWidth = Math.max(2, this.r * 0.06);
|
|
ctx.beginPath();
|
|
ctx.ellipse(this.r * 0.62, this.r * 0.42, this.r * 0.83, this.r * 0.39, -0.08, 0, Math.PI * 2);
|
|
ctx.fill();
|
|
ctx.stroke();
|
|
ctx.fillStyle = loaded ? "rgba(255,245,209,0.96)" : "rgba(230,235,238,0.48)";
|
|
ctx.beginPath();
|
|
ctx.ellipse(this.r * 0.62, this.r * 0.32, this.r * 0.50, this.r * 0.23, -0.08, 0, Math.PI * 2);
|
|
ctx.fill();
|
|
|
|
if (loaded) {
|
|
ctx.save();
|
|
ctx.translate(this.r * 0.62, this.r * 0.24);
|
|
ctx.scale(0.82, 0.82);
|
|
drawStoredConsumableSprite(ctx, this.storedFoodType || "food", Math.max(7, this.r * 0.35), this.seed + 241);
|
|
ctx.restore();
|
|
} else {
|
|
ctx.fillStyle = "rgba(72,80,86,0.62)";
|
|
ctx.font = `bold ${Math.max(10, Math.round(this.r * 0.34))}px ui-rounded, sans-serif`;
|
|
ctx.textAlign = "center";
|
|
ctx.textBaseline = "middle";
|
|
ctx.fillText("?", this.r * 0.62, this.r * 0.28);
|
|
}
|
|
|
|
ctx.fillStyle = styleNight ? "rgba(255,255,255,0.12)" : "rgba(255,255,255,0.45)";
|
|
ctx.beginPath();
|
|
ctx.ellipse(-this.r * 0.48, -this.r * 0.72, this.r * 0.34, this.r * 0.08, -0.14, 0, Math.PI * 2);
|
|
ctx.fill();
|
|
ctx.restore();
|
|
} else if (this.type === "signboard") {
|
|
ctx.save();
|
|
const boardGrad = ctx.createLinearGradient(0, -this.r * 1.28, 0, this.r * 0.12);
|
|
const boardHalfW = this.r * 1.20;
|
|
boardGrad.addColorStop(0, styleNight ? "#d5cab8" : "#fff8e8");
|
|
boardGrad.addColorStop(0.55, styleNight ? "#c0b29a" : "#f3e3c6");
|
|
boardGrad.addColorStop(1, styleNight ? "#aa9a82" : "#dfc397");
|
|
ctx.fillStyle = styleNight ? "#8a7557" : "#d6ba87";
|
|
ctx.strokeStyle = styleNight ? "#5d4d3b" : "#9b7444";
|
|
ctx.lineWidth = 2.2;
|
|
roundedRect(ctx, -this.r * 0.13, -this.r * 0.02, this.r * 0.26, this.r * 1.78, this.r * 0.04);
|
|
ctx.fill();
|
|
ctx.stroke();
|
|
ctx.fillStyle = boardGrad;
|
|
ctx.strokeStyle = styleNight ? "#63513c" : "#a77e48";
|
|
roundedRect(ctx, -boardHalfW, -this.r * 1.28, boardHalfW * 2, this.r * 1.38, this.r * 0.18);
|
|
ctx.fill();
|
|
ctx.stroke();
|
|
ctx.strokeStyle = styleNight ? "rgba(103,83,58,0.36)" : "rgba(167,126,72,0.38)";
|
|
ctx.lineWidth = 1.1;
|
|
for (let i = 0; i < 3; i++) {
|
|
const y = -this.r * 1.02 + i * this.r * 0.36;
|
|
ctx.beginPath();
|
|
ctx.moveTo(-boardHalfW * 0.82, y);
|
|
ctx.quadraticCurveTo(-this.r * 0.12, y + Math.sin(this.seed + i) * 2, boardHalfW * 0.82, y + Math.cos(this.seed + i) * 2);
|
|
ctx.stroke();
|
|
}
|
|
ctx.fillStyle = "rgba(255,255,255,0.28)";
|
|
roundedRect(ctx, -boardHalfW * 0.82, -this.r * 1.10, boardHalfW * 1.64, this.r * 0.20, this.r * 0.08);
|
|
ctx.fill();
|
|
const raw = String(this.text || "").replace(/\r/g, "").trim();
|
|
if (raw) {
|
|
const charsPerLine = 6;
|
|
const lines = [];
|
|
for (const rawLine of raw.split("\n")) {
|
|
let rest = rawLine.trim();
|
|
if (!rest && lines.length < 3) { lines.push(""); continue; }
|
|
while (rest && lines.length < 3) {
|
|
const chars = Array.from(rest);
|
|
lines.push(chars.slice(0, charsPerLine).join(""));
|
|
rest = chars.slice(charsPerLine).join("");
|
|
}
|
|
if (lines.length >= 3) break;
|
|
}
|
|
ctx.fillStyle = styleNight ? "rgba(37,31,24,0.95)" : "rgba(55,37,22,0.96)";
|
|
ctx.font = `bold ${Math.max(11, Math.round(this.r * 0.39))}px ui-rounded, sans-serif`;
|
|
ctx.textAlign = "center";
|
|
ctx.textBaseline = "middle";
|
|
const startY = -this.r * 0.66 - (lines.length - 1) * this.r * 0.16;
|
|
lines.forEach((line, index) => ctx.fillText(line, 0, startY + index * this.r * 0.34));
|
|
} else {
|
|
ctx.fillStyle = styleNight ? "rgba(55,45,33,0.58)" : "rgba(91,64,35,0.50)";
|
|
ctx.font = `bold ${Math.max(14, Math.round(this.r * 0.44))}px ui-rounded, sans-serif`;
|
|
ctx.textAlign = "center";
|
|
ctx.textBaseline = "middle";
|
|
ctx.fillText("\u2026", 0, -this.r * 0.58);
|
|
}
|
|
ctx.restore();
|
|
} else if (this.type === "ant_nest") {
|
|
const count = clamp(Math.round(this.antCount ?? ANT_NEST_START_COUNT ?? 6), 0, ANT_NEST_MAX_COUNT ?? 10);
|
|
ctx.fillStyle = styleNight ? "#5e4634" : (styleWarm ? "#956b42" : "#77583a");
|
|
ctx.strokeStyle = styleNight ? "rgba(43,32,26,0.78)" : "rgba(72,48,31,0.72)";
|
|
ctx.lineWidth = 2.2;
|
|
ctx.beginPath();
|
|
ctx.ellipse(0, 8, this.r * 1.35, this.r * 0.78, 0.08, 0, Math.PI * 2);
|
|
ctx.fill(); ctx.stroke();
|
|
ctx.fillStyle = styleNight ? "#211a17" : "#30231d";
|
|
ctx.beginPath();
|
|
ctx.ellipse(0, 2, this.r * 0.58, this.r * 0.36, 0, 0, Math.PI * 2);
|
|
ctx.fill();
|
|
ctx.fillStyle = "rgba(38,37,35,0.88)";
|
|
for (let i = 0; i < Math.min(6, count); i++) {
|
|
const ax = randSeed(this.seed + i * 11, -this.r * 1.0, this.r * 1.0);
|
|
const ay = randSeed(this.seed + i * 17, -this.r * 0.45, this.r * 0.55);
|
|
ctx.beginPath();
|
|
ctx.ellipse(ax, ay, 3.4, 2.1, randSeed(this.seed + i * 23, -0.8, 0.8), 0, Math.PI * 2);
|
|
ctx.fill();
|
|
}
|
|
ctx.fillStyle = "rgba(255,248,220,0.82)";
|
|
ctx.font = "bold 10px ui-rounded, sans-serif";
|
|
ctx.textAlign = "center";
|
|
ctx.textBaseline = "middle";
|
|
ctx.fillText(`${count}`, 0, this.r * 1.25);
|
|
} else if (this.type === "ant_corpse") {
|
|
const img = typeof getRenderableImage === "function" ? getRenderableImage("ant_worker", "ant_worker") : images.get("ant_worker");
|
|
ctx.save();
|
|
ctx.rotate((stableUnit(this.id || this.seed, "ant-corpse-rot") - 0.5) * 0.26);
|
|
ctx.scale(1, -1);
|
|
ctx.globalAlpha *= clamp((this.amount || 0) / 24, 0.35, 1);
|
|
if (img) {
|
|
const w = this.r * 3.9;
|
|
const metrics = getImageMetrics("ant_worker");
|
|
const h = w * (metrics?.ratio || 0.52);
|
|
ctx.drawImage(img, -w * 0.5, -h * 0.55, w, h);
|
|
} else {
|
|
ctx.fillStyle = "rgba(42,39,35,0.85)";
|
|
ctx.beginPath();
|
|
ctx.ellipse(0, 0, this.r * 1.35, this.r * 0.72, 0, 0, Math.PI * 2);
|
|
ctx.fill();
|
|
}
|
|
ctx.restore();
|
|
} else if (this.type === "ball") {
|
|
const speed = Math.hypot(this.vx || 0, this.vy || 0);
|
|
const moving = clamp(speed / 260, 0, 1);
|
|
ctx.save();
|
|
ctx.rotate(this.spin || 0);
|
|
ctx.shadowColor = styleNight ? "rgba(0,0,0,0.34)" : "rgba(52,40,26,0.18)";
|
|
ctx.shadowBlur = 2 + moving * 3;
|
|
ctx.shadowOffsetY = 2;
|
|
ctx.font = `${Math.round((this.r || 18) * 2.0)}px "Apple Color Emoji", "Segoe UI Emoji", "Noto Color Emoji", sans-serif`;
|
|
ctx.textAlign = "center";
|
|
ctx.textBaseline = "middle";
|
|
ctx.fillText("\u26bd", 0, 1);
|
|
if (moving > 0.55) {
|
|
ctx.globalAlpha = 0.14 + moving * 0.10;
|
|
ctx.shadowColor = "transparent";
|
|
ctx.fillStyle = "#ffffff";
|
|
ctx.beginPath();
|
|
ctx.ellipse(-this.r * 0.35, -this.r * 0.42, this.r * 0.25, this.r * 0.12, -0.45, 0, Math.PI * 2);
|
|
ctx.fill();
|
|
}
|
|
ctx.restore();
|
|
} else if (this.type === "firecracker") {
|
|
const fuse = clamp((this.fuseTimer ?? 5) / Math.max(this.fuseMax || 5, 0.1), 0, 1);
|
|
const flash = fuse < 0.35 ? (0.5 + Math.sin(t * 20 + this.seed) * 0.5) : 0;
|
|
ctx.rotate(-0.22 + Math.sin(this.seed) * 0.12);
|
|
ctx.fillStyle = flash > 0.65 ? "#ffed77" : "#d94b43";
|
|
ctx.strokeStyle = "rgba(96,48,34,0.78)";
|
|
ctx.lineWidth = 2;
|
|
roundedRect(ctx, -this.r * 0.72, -this.r * 0.86, this.r * 1.44, this.r * 1.72, 5);
|
|
ctx.fill();
|
|
ctx.stroke();
|
|
ctx.fillStyle = "#f4d15b";
|
|
ctx.fillRect(-this.r * 0.55, -this.r * 0.54, this.r * 1.1, this.r * 0.20);
|
|
ctx.fillRect(-this.r * 0.55, this.r * 0.34, this.r * 1.1, this.r * 0.20);
|
|
ctx.strokeStyle = "rgba(74,55,39,0.82)";
|
|
ctx.lineWidth = 2.2;
|
|
ctx.beginPath();
|
|
ctx.moveTo(0, -this.r * 0.88);
|
|
ctx.quadraticCurveTo(this.r * 0.32, -this.r * 1.34, this.r * 0.92, -this.r * 1.42);
|
|
ctx.stroke();
|
|
ctx.fillStyle = flash > 0.1 ? "rgba(255,221,82,0.92)" : "rgba(255,162,62,0.78)";
|
|
ctx.beginPath();
|
|
ctx.arc(this.r * 0.98, -this.r * 1.42, 2.5 + flash * 2.2, 0, Math.PI * 2);
|
|
ctx.fill();
|
|
ctx.fillStyle = "rgba(42,36,29,0.66)";
|
|
ctx.font = "bold 10px ui-rounded, sans-serif";
|
|
ctx.textAlign = "center";
|
|
ctx.fillText(String(Math.ceil(this.fuseTimer ?? 5)), 0, this.r * 1.42);
|
|
} else if (this.type === "fence_v" || this.type === "fence_h") {
|
|
const vertical = this.type === "fence_v";
|
|
const len = Math.max(112, this.r * 3.55);
|
|
const thick = Math.max(10, this.r * 0.31);
|
|
ctx.save();
|
|
ctx.rotate(vertical ? 0 : Math.PI / 2);
|
|
const wood = styleNight ? "#7a5b3c" : (styleWarm ? "#a96f35" : "#965f31");
|
|
const edge = styleNight ? "#4d3d2f" : "#5f3f25";
|
|
const hi = styleNight ? "rgba(198,166,116,0.20)" : "rgba(232,176,94,0.32)";
|
|
ctx.fillStyle = wood;
|
|
ctx.strokeStyle = edge;
|
|
ctx.lineWidth = 2.4;
|
|
ctx.beginPath();
|
|
roundedRect(ctx, -thick / 2, -len / 2, thick, len, thick / 2);
|
|
ctx.fill();
|
|
ctx.stroke();
|
|
ctx.fillStyle = hi;
|
|
ctx.beginPath();
|
|
roundedRect(ctx, -thick * 0.26, -len / 2 + 6, thick * 0.20, len - 12, thick / 3);
|
|
ctx.fill();
|
|
ctx.restore();
|
|
} else if (this.type === "zunchi") {
|
|
const zunchiId = this.zunchiVariant || "zunchi";
|
|
const img = images.get(zunchiId) || images.get("zunchi");
|
|
if (img) {
|
|
const metrics = getImageMetrics(zunchiId) || getImageMetrics("zunchi");
|
|
const ratio = metrics?.ratio || 178 / 236;
|
|
const stageAlpha = this.stage === "fresh" ? clamp(this.freshness ?? 1, 0.45, 1) : this.stage === "dry" ? 0.82 : this.stage === "decomposing" ? 0.62 : clamp(this.fertility ?? 0.28, 0.24, 0.46);
|
|
ctx.globalAlpha *= stageAlpha;
|
|
ctx.drawImage(img, -this.r * 1.1, -this.r * 1.35, this.r * 2.2, this.r * 1.66 * ratio);
|
|
}
|
|
} else if (this.type === "trace") {
|
|
ctx.fillStyle = "rgba(122, 100, 76, 0.12)";
|
|
ctx.strokeStyle = "rgba(122, 100, 76, 0.16)";
|
|
ctx.lineWidth = 1.5;
|
|
ctx.beginPath();
|
|
ctx.ellipse(0, 0, this.r * 1.3, this.r * 0.75, Math.sin(this.seed) * 0.7, 0, Math.PI * 2);
|
|
ctx.fill(); ctx.stroke();
|
|
} else if (this.type === "splat") {
|
|
const fade = clamp(this.amount / 260, 0, 1);
|
|
ctx.globalAlpha = visibleAlpha * fade;
|
|
ctx.fillStyle = `rgba(123, 214, 52, ${0.34 * fade})`;
|
|
ctx.strokeStyle = `rgba(85, 160, 38, ${0.22 * fade})`;
|
|
ctx.lineWidth = 1.8;
|
|
const blobs = [
|
|
[-12, -2, 10, 8], [0, 0, 16, 10], [13, 5, 10, 8], [-2, -12, 9, 7], [7, -8, 7, 5], [-18, 8, 6, 5]
|
|
];
|
|
for (const [bx, by, bw, bh] of blobs) {
|
|
ctx.beginPath();
|
|
ctx.ellipse(bx, by, bw, bh, randSeed(this.seed + bx * 0.1, -0.5, 0.5), 0, Math.PI * 2);
|
|
ctx.fill();
|
|
ctx.stroke();
|
|
}
|
|
for (let i = 0; i < 7; i++) {
|
|
ctx.beginPath();
|
|
ctx.arc(randSeed(this.seed + i, -28, 24), randSeed(this.seed + i + 9, -18, 18), randSeed(this.seed + i + 19, 2.5, 4.8), 0, Math.PI * 2);
|
|
ctx.fill();
|
|
}
|
|
}
|
|
ctx.restore();
|
|
}
|
|
});
|