1531 lines
71 KiB
JavaScript
1531 lines
71 KiB
JavaScript
"use strict";
|
|
|
|
|
|
function drawZunchiFallbackSprite(ctx, r = 15, alpha = 1) {
|
|
ctx.save();
|
|
ctx.globalAlpha *= alpha;
|
|
ctx.fillStyle = "rgba(102, 76, 42, 0.92)";
|
|
ctx.strokeStyle = "rgba(54, 39, 23, 0.42)";
|
|
ctx.lineWidth = Math.max(1.1, r * 0.08);
|
|
const lumps = [
|
|
[-0.58, 0.12, 0.42, 0.26],
|
|
[-0.24, -0.05, 0.48, 0.32],
|
|
[0.20, 0.07, 0.50, 0.30],
|
|
[0.54, 0.16, 0.34, 0.22],
|
|
[0.02, -0.31, 0.34, 0.22],
|
|
];
|
|
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.48;
|
|
ctx.strokeStyle = "rgba(255, 245, 210, 0.86)";
|
|
ctx.lineWidth = Math.max(1.0, r * 0.06);
|
|
ctx.beginPath();
|
|
ctx.moveTo(-r * 0.78, -r * 0.20);
|
|
ctx.quadraticCurveTo(-r * 0.20, -r * 0.50, r * 0.58, -r * 0.16);
|
|
ctx.stroke();
|
|
ctx.restore();
|
|
}
|
|
|
|
// 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 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 && world?.isTarinaiHiddenInNestBox?.(owner)) return;
|
|
}
|
|
const servingRatio = isServingFoodType(this.type) ? ((this.foodServingsRemaining ?? this.amount) / Math.max(1, this.foodServingsMax || foodServingsForSize(this.toolSize || "medium"))) : null;
|
|
const forceOpaque = this.type === "slave_chain" || this.type === "golden_crown";
|
|
const visibleAlpha = forceOpaque ? 1 : (isServingFoodType(this.type) ? clamp(0.72 + (servingRatio ?? 1) * 0.28, 0.72, 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" && shadow.alpha > 0) {
|
|
const zunchiId = this.type === "zunchi" ? (this.zunchiVariant || "zunchi") : null;
|
|
const zunchiImg = zunchiId ? getRenderableImage(zunchiId, "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 });
|
|
}
|
|
}
|
|
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 = getRenderableImage("genkotsu", "genkotsu");
|
|
const impact = clamp(this.impactFlash || 0, 0, 1);
|
|
ctx.save();
|
|
if (itemShadowsEnabled()) {
|
|
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 = getImageMetrics("genkotsu");
|
|
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 = pinBehaviorFor(this.type);
|
|
const assetId = behavior ? (stuck ? behavior.lodgedAsset : behavior.looseAsset) : (stuck ? "pushpin_stuck" : "pushpin");
|
|
const img = getRenderableImage(assetId, 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);
|
|
if (itemShadowsEnabled()) {
|
|
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 = getImageMetrics(assetId);
|
|
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 === "slave_chain") {
|
|
const r = this.r || 15;
|
|
ctx.save();
|
|
ctx.rotate(-0.35 + Math.sin(this.seed || 0) * 0.18);
|
|
ctx.lineCap = "round";
|
|
ctx.lineJoin = "round";
|
|
ctx.strokeStyle = styleNight ? "rgba(122,118,112,0.94)" : "rgba(92,86,78,0.94)";
|
|
ctx.lineWidth = Math.max(2.2, r * 0.18);
|
|
for (let i = -1; i <= 1; i++) {
|
|
ctx.save();
|
|
ctx.translate(i * r * 0.52, Math.sin(i + this.seed) * r * 0.08);
|
|
ctx.rotate(i % 2 ? 0.65 : -0.65);
|
|
ctx.beginPath();
|
|
ctx.ellipse(0, 0, r * 0.36, r * 0.20, 0, 0, Math.PI * 2);
|
|
ctx.stroke();
|
|
ctx.restore();
|
|
}
|
|
ctx.strokeStyle = "rgba(230,224,214,0.46)";
|
|
ctx.lineWidth = Math.max(1.0, r * 0.06);
|
|
ctx.beginPath();
|
|
ctx.moveTo(-r * 0.95, -r * 0.24);
|
|
ctx.lineTo(r * 0.86, -r * 0.18);
|
|
ctx.stroke();
|
|
ctx.restore();
|
|
} else if (this.type === "golden_crown") {
|
|
const r = this.r || 16;
|
|
ctx.save();
|
|
ctx.rotate(0.08 + Math.sin(this.seed || 0) * 0.05);
|
|
ctx.fillStyle = styleNight ? "#c28a22" : "#f0bf3f";
|
|
ctx.strokeStyle = styleNight ? "rgba(82,54,12,0.88)" : "rgba(124,78,12,0.82)";
|
|
ctx.lineWidth = Math.max(1.4, r * 0.10);
|
|
ctx.beginPath();
|
|
ctx.moveTo(-r * 0.92, r * 0.36);
|
|
ctx.lineTo(-r * 0.72, -r * 0.38);
|
|
ctx.lineTo(-r * 0.32, r * 0.02);
|
|
ctx.lineTo(0, -r * 0.62);
|
|
ctx.lineTo(r * 0.32, r * 0.02);
|
|
ctx.lineTo(r * 0.72, -r * 0.38);
|
|
ctx.lineTo(r * 0.92, r * 0.36);
|
|
ctx.closePath();
|
|
ctx.fill(); ctx.stroke();
|
|
ctx.fillStyle = "rgba(255,246,172,0.92)";
|
|
for (const px of [-0.72, 0, 0.72]) {
|
|
ctx.beginPath();
|
|
ctx.arc(r * px, px === 0 ? -r * 0.54 : -r * 0.32, Math.max(1.4, r * 0.09), 0, Math.PI * 2);
|
|
ctx.fill();
|
|
}
|
|
ctx.fillStyle = "rgba(255,238,128,0.94)";
|
|
ctx.fillRect(-r * 0.78, r * 0.20, r * 1.56, r * 0.24);
|
|
ctx.restore();
|
|
} else if (this.type === "toilet") {
|
|
const r = this.r || 48;
|
|
const flash = clamp(this.toiletFlash || 0, 0, 1);
|
|
const footprint = globalThis.TarinaiToiletSandSystem?.footprint?.(this) || {
|
|
centerX: this.x,
|
|
centerY: this.y + r * 0.08,
|
|
halfWidth: r * 1.55,
|
|
halfHeight: r * 0.72,
|
|
cornerRadius: r * 0.18,
|
|
angle: Number(this.angle || 0) || 0,
|
|
};
|
|
const localY = footprint.centerY - this.y;
|
|
ctx.save();
|
|
ctx.rotate(footprint.angle || 0);
|
|
if (itemShadowsEnabled()) {
|
|
ctx.shadowColor = "rgba(54,42,31,0.16)";
|
|
ctx.shadowBlur = 5;
|
|
ctx.shadowOffsetY = 3;
|
|
}
|
|
ctx.fillStyle = styleNight ? "rgba(91,77,68,0.96)" : "rgba(143,116,91,0.96)";
|
|
ctx.strokeStyle = styleNight ? "rgba(45,39,36,0.92)" : "rgba(79,62,49,0.90)";
|
|
ctx.lineWidth = Math.max(1.8, r * 0.060);
|
|
roundedRect(ctx, -footprint.halfWidth, localY - footprint.halfHeight, footprint.halfWidth * 2, footprint.halfHeight * 2, footprint.cornerRadius);
|
|
ctx.fill();
|
|
ctx.stroke();
|
|
const inset = Math.max(4, r * 0.14);
|
|
ctx.fillStyle = styleNight ? "rgba(146,128,91,0.96)" : "rgba(218,190,128,0.98)";
|
|
roundedRect(ctx, -footprint.halfWidth + inset, localY - footprint.halfHeight + inset, (footprint.halfWidth - inset) * 2, (footprint.halfHeight - inset) * 2, Math.max(2, footprint.cornerRadius * 0.62));
|
|
ctx.fill();
|
|
ctx.fillStyle = styleNight ? "rgba(176,156,111,0.72)" : "rgba(244,220,164,0.78)";
|
|
for (const grain of [[-1.08,-0.20,0.055],[-0.62,0.21,0.045],[-0.18,-0.15,0.050],[0.28,0.19,0.042],[0.72,-0.18,0.052],[1.12,0.16,0.040]]) {
|
|
ctx.beginPath();
|
|
ctx.arc(r * grain[0], localY + r * grain[1], Math.max(1, r * grain[2]), 0, Math.PI * 2);
|
|
ctx.fill();
|
|
}
|
|
if (flash > 0.02) {
|
|
ctx.globalAlpha *= flash * 0.42;
|
|
ctx.fillStyle = "rgba(255,244,204,0.96)";
|
|
roundedRect(ctx, -footprint.halfWidth + inset * 0.55, localY - footprint.halfHeight + inset * 0.55, (footprint.halfWidth - inset * 0.55) * 2, (footprint.halfHeight - inset * 0.55) * 2, Math.max(2, footprint.cornerRadius * 0.78));
|
|
ctx.fill();
|
|
}
|
|
ctx.restore();
|
|
} else if (this.type === "rain_shelter") {
|
|
const r = this.r || 50;
|
|
const footprint = globalThis.TarinaiParasolSystem?.parasolFootprint?.(this) || {
|
|
centerX: this.x,
|
|
centerY: this.y + r * 0.84,
|
|
radiusX: r * 1.58,
|
|
radiusY: r * 0.68,
|
|
};
|
|
ctx.save();
|
|
ctx.fillStyle = styleNight ? "rgba(18,18,28,0.26)" : "rgba(54,48,54,0.20)";
|
|
ctx.beginPath();
|
|
ctx.ellipse(footprint.centerX - this.x, footprint.centerY - this.y, footprint.radiusX, footprint.radiusY, 0, 0, Math.PI * 2);
|
|
ctx.fill();
|
|
if (itemShadowsEnabled()) {
|
|
ctx.shadowColor = "rgba(54,42,31,0.18)";
|
|
ctx.shadowBlur = 7;
|
|
ctx.shadowOffsetY = 5;
|
|
}
|
|
ctx.lineCap = "round";
|
|
ctx.lineJoin = "round";
|
|
ctx.strokeStyle = styleNight ? "rgba(70,70,78,0.90)" : "rgba(92,86,76,0.82)";
|
|
ctx.lineWidth = Math.max(2.2, r * 0.065);
|
|
ctx.beginPath();
|
|
ctx.moveTo(0, -r * 0.55);
|
|
ctx.lineTo(0, r * 0.84);
|
|
ctx.stroke();
|
|
ctx.beginPath();
|
|
ctx.moveTo(-r * 0.24, r * 0.84);
|
|
ctx.lineTo(r * 0.24, r * 0.84);
|
|
ctx.stroke();
|
|
ctx.fillStyle = styleNight ? "rgba(116,76,112,0.96)" : "rgba(225,104,126,0.96)";
|
|
ctx.strokeStyle = styleNight ? "rgba(62,42,68,0.92)" : "rgba(126,54,72,0.84)";
|
|
ctx.lineWidth = Math.max(1.8, r * 0.065);
|
|
ctx.beginPath();
|
|
ctx.moveTo(-r * 1.12, -r * 0.24);
|
|
ctx.quadraticCurveTo(-r * 0.72, -r * 0.98, 0, -r * 1.10);
|
|
ctx.quadraticCurveTo(r * 0.72, -r * 0.98, r * 1.12, -r * 0.24);
|
|
ctx.quadraticCurveTo(r * 0.76, -r * 0.06, r * 0.50, -r * 0.24);
|
|
ctx.quadraticCurveTo(r * 0.24, -r * 0.04, 0, -r * 0.24);
|
|
ctx.quadraticCurveTo(-r * 0.24, -r * 0.04, -r * 0.50, -r * 0.24);
|
|
ctx.quadraticCurveTo(-r * 0.76, -r * 0.06, -r * 1.12, -r * 0.24);
|
|
ctx.closePath();
|
|
ctx.fill();
|
|
ctx.stroke();
|
|
ctx.strokeStyle = styleNight ? "rgba(210,184,205,0.44)" : "rgba(255,220,224,0.62)";
|
|
ctx.lineWidth = Math.max(1, r * 0.025);
|
|
for (const x of [-0.50, 0, 0.50]) {
|
|
ctx.beginPath();
|
|
ctx.moveTo(0, -r * 1.08);
|
|
ctx.quadraticCurveTo(r * x * 0.72, -r * 0.68, r * x, -r * 0.24);
|
|
ctx.stroke();
|
|
}
|
|
ctx.restore();
|
|
} 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();
|
|
if (this.burning || (this.burnTimer || 0) > 0.02 || (this.nestFireEvacuationTimer || 0) > 0.02) {
|
|
const phase = t * 7.4 + this.seed;
|
|
ctx.save();
|
|
ctx.globalAlpha *= 0.78;
|
|
if (typeof drawSimpleFireShape === "function") {
|
|
drawSimpleFireShape(ctx, -this.r * 0.42, -this.r * 0.64, this.r * 0.38, { phase, shadow: itemShadowsEnabled() });
|
|
drawSimpleFireShape(ctx, this.r * 0.38, -this.r * 0.56, this.r * 0.32, { phase: phase + 1.7, shadow: false });
|
|
} else {
|
|
ctx.fillStyle = "rgba(236,46,24,0.78)";
|
|
ctx.beginPath();
|
|
ctx.ellipse(-this.r * 0.35, -this.r * 0.58, this.r * 0.18, this.r * 0.36, 0, 0, Math.PI * 2);
|
|
ctx.fill();
|
|
}
|
|
ctx.restore();
|
|
}
|
|
} else if (this.type === "pipe") {
|
|
const r = this.r || 36;
|
|
ctx.save();
|
|
const side = styleNight ? "#3b544c" : (styleWarm ? "#648166" : "#577064");
|
|
const lip = styleNight ? "#80998d" : (styleWarm ? "#b4c6af" : "#9fb7aa");
|
|
const rimShadow = styleNight ? "#26413b" : "#4b6a60";
|
|
const inner = styleNight ? "#0b1212" : "#14211f";
|
|
const outline = styleNight ? "rgba(8,14,14,0.88)" : "rgba(26,46,40,0.80)";
|
|
const mouthY = -r * 0.16;
|
|
const mouthRx = r * 1.05;
|
|
const mouthRy = r * 0.37;
|
|
const innerRx = r * 0.70;
|
|
const innerRy = r * 0.24;
|
|
const frontY = r * 0.34;
|
|
const frontRx = r * 0.97;
|
|
const frontRy = r * 0.30;
|
|
|
|
ctx.fillStyle = "rgba(18,24,22,0.21)";
|
|
ctx.beginPath();
|
|
ctx.ellipse(0, frontY + r * 0.10, r * 1.36, r * 0.34, 0, 0, Math.PI * 2);
|
|
ctx.fill();
|
|
|
|
// Exposed cylindrical wall: only the front half is visible so the pipe feels buried,
|
|
// not like a free-standing jar.
|
|
ctx.fillStyle = side;
|
|
ctx.strokeStyle = outline;
|
|
ctx.lineWidth = Math.max(2.2, r * 0.07);
|
|
ctx.beginPath();
|
|
ctx.moveTo(-mouthRx * 0.92, mouthY + r * 0.03);
|
|
ctx.bezierCurveTo(-mouthRx * 0.98, mouthY + r * 0.22, -frontRx * 1.00, frontY - r * 0.06, -frontRx, frontY);
|
|
ctx.ellipse(0, frontY, frontRx, frontRy, 0, Math.PI, 0, true);
|
|
ctx.bezierCurveTo(frontRx * 1.00, frontY - r * 0.06, mouthRx * 0.98, mouthY + r * 0.22, mouthRx * 0.92, mouthY + r * 0.03);
|
|
ctx.closePath();
|
|
ctx.fill();
|
|
ctx.stroke();
|
|
|
|
// Ground collar around the mouth to make it read as a pipe opening in the ground.
|
|
ctx.fillStyle = "rgba(92,108,104,0.22)";
|
|
ctx.beginPath();
|
|
ctx.ellipse(0, mouthY + r * 0.03, mouthRx * 1.08, mouthRy * 1.15, 0, 0, Math.PI * 2);
|
|
ctx.fill();
|
|
|
|
ctx.fillStyle = lip;
|
|
ctx.strokeStyle = outline;
|
|
ctx.lineWidth = Math.max(2.6, r * 0.08);
|
|
ctx.beginPath();
|
|
ctx.ellipse(0, mouthY, mouthRx, mouthRy, 0, 0, Math.PI * 2);
|
|
ctx.fill();
|
|
ctx.stroke();
|
|
|
|
ctx.strokeStyle = rimShadow;
|
|
ctx.lineWidth = Math.max(1.6, r * 0.05);
|
|
ctx.beginPath();
|
|
ctx.ellipse(0, mouthY + r * 0.01, mouthRx * 0.86, mouthRy * 0.70, 0, Math.PI * 1.02, Math.PI * 1.98);
|
|
ctx.stroke();
|
|
|
|
ctx.fillStyle = inner;
|
|
ctx.strokeStyle = styleNight ? "rgba(2,7,7,0.90)" : "rgba(14,24,22,0.92)";
|
|
ctx.lineWidth = Math.max(2.0, r * 0.065);
|
|
ctx.beginPath();
|
|
ctx.ellipse(0, mouthY + r * 0.01, innerRx, innerRy, 0, 0, Math.PI * 2);
|
|
ctx.fill();
|
|
ctx.stroke();
|
|
|
|
ctx.save();
|
|
ctx.globalAlpha = 0.24;
|
|
ctx.strokeStyle = "rgba(255,255,255,0.46)";
|
|
ctx.lineWidth = Math.max(1.2, r * 0.035);
|
|
ctx.beginPath();
|
|
ctx.moveTo(-mouthRx * 0.60, mouthY + r * 0.12);
|
|
ctx.quadraticCurveTo(-mouthRx * 0.68, frontY - r * 0.12, -mouthRx * 0.48, frontY + r * 0.12);
|
|
ctx.stroke();
|
|
ctx.restore();
|
|
ctx.restore();
|
|
} else if (this.type === "robot_cleaner") {
|
|
ctx.save();
|
|
const flash = clamp(Number(this.robotCleanerCleanFlash || 0) || 0, 0, 1);
|
|
const angle = Number(this.robotCleanerAngle || this.angle || 0) || 0;
|
|
const bodyA = styleNight ? "#39434a" : "#d8e1e3";
|
|
const bodyB = styleNight ? "#1f272d" : "#a7b4b8";
|
|
const trim = styleNight ? "#657781" : "#ffffff";
|
|
const dark = styleNight ? "#12191e" : "#526168";
|
|
ctx.fillStyle = "rgba(20,24,28,0.16)";
|
|
ctx.beginPath();
|
|
ctx.ellipse(0, this.r * 0.56, this.r * 1.05, this.r * 0.30, 0, 0, Math.PI * 2);
|
|
ctx.fill();
|
|
const bodyGrad = ctx.createRadialGradient(-this.r * 0.34, -this.r * 0.38, this.r * 0.18, 0, 0, this.r * 1.28);
|
|
bodyGrad.addColorStop(0, trim);
|
|
bodyGrad.addColorStop(0.46, bodyA);
|
|
bodyGrad.addColorStop(1, bodyB);
|
|
ctx.fillStyle = bodyGrad;
|
|
ctx.strokeStyle = dark;
|
|
ctx.lineWidth = Math.max(2, this.r * 0.08);
|
|
ctx.beginPath();
|
|
ctx.ellipse(0, 0, this.r * 1.04, this.r * 0.88, 0, 0, Math.PI * 2);
|
|
ctx.fill();
|
|
ctx.stroke();
|
|
ctx.save();
|
|
ctx.rotate(angle);
|
|
ctx.fillStyle = flash > 0.02 ? "rgba(105,210,225,0.84)" : (styleNight ? "#50636d" : "#71868e");
|
|
ctx.strokeStyle = dark;
|
|
ctx.lineWidth = Math.max(1, this.r * 0.045);
|
|
roundedRect(ctx, this.r * 0.18, -this.r * 0.15, this.r * 0.58, this.r * 0.30, this.r * 0.09);
|
|
ctx.fill();
|
|
ctx.stroke();
|
|
ctx.restore();
|
|
ctx.fillStyle = styleNight ? "#20292f" : "#eef4f5";
|
|
ctx.strokeStyle = styleNight ? "#11181d" : "#6a7980";
|
|
ctx.lineWidth = Math.max(1.4, this.r * 0.055);
|
|
ctx.beginPath();
|
|
ctx.arc(-this.r * 0.26, -this.r * 0.12, this.r * 0.22, 0, Math.PI * 2);
|
|
ctx.fill();
|
|
ctx.stroke();
|
|
ctx.beginPath();
|
|
ctx.arc(this.r * 0.26, -this.r * 0.12, this.r * 0.22, 0, Math.PI * 2);
|
|
ctx.fill();
|
|
ctx.stroke();
|
|
ctx.strokeStyle = flash > 0.02 ? `rgba(110,220,235,${0.42 + flash * 0.42})` : "rgba(80,100,110,0.38)";
|
|
ctx.lineWidth = Math.max(1.1, this.r * 0.04);
|
|
ctx.beginPath();
|
|
ctx.arc(0, 0, this.r * 0.56, 0.22, Math.PI - 0.22);
|
|
ctx.stroke();
|
|
ctx.fillStyle = flash > 0.02 ? `rgba(125,230,240,${0.28 + flash * 0.36})` : "rgba(60,78,86,0.20)";
|
|
ctx.beginPath();
|
|
ctx.arc(0, this.r * 0.20, this.r * (0.18 + flash * 0.10), 0, Math.PI * 2);
|
|
ctx.fill();
|
|
ctx.restore();
|
|
} else if (this.type === "duplicator") {
|
|
ctx.save();
|
|
const loaded = !!this.storedFoodType;
|
|
const powered = globalThis.TarinaiSignalSystem?.powerOverride?.(this) !== false;
|
|
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 = !powered ? "rgba(180,76,70,0.72)" : (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 === "seesaw") {
|
|
const r = this.r || 60;
|
|
const tilt = Number(this.seesawTilt || 0) || 0;
|
|
ctx.save();
|
|
ctx.fillStyle = styleNight ? "rgba(81,68,57,0.96)" : "rgba(130,99,69,0.98)";
|
|
ctx.strokeStyle = styleNight ? "rgba(41,34,29,0.86)" : "rgba(78,55,37,0.82)";
|
|
ctx.lineWidth = 2.4;
|
|
ctx.beginPath();
|
|
ctx.moveTo(-r * 0.22, r * 0.42);
|
|
ctx.lineTo(0, -r * 0.05);
|
|
ctx.lineTo(r * 0.22, r * 0.42);
|
|
ctx.closePath(); ctx.fill(); ctx.stroke();
|
|
ctx.save();
|
|
ctx.rotate(tilt);
|
|
const grad = ctx.createLinearGradient(0, -r * 0.14, 0, r * 0.14);
|
|
grad.addColorStop(0, styleNight ? "#c5895c" : "#f0ad6f");
|
|
grad.addColorStop(1, styleNight ? "#8d5d3f" : "#ba7447");
|
|
ctx.fillStyle = grad;
|
|
ctx.strokeStyle = styleNight ? "rgba(57,37,29,0.9)" : "rgba(104,59,35,0.85)";
|
|
roundedRect(ctx, -r * 1.08, -r * 0.13, r * 2.16, r * 0.26, r * 0.08);
|
|
ctx.fill(); ctx.stroke();
|
|
for (const side of [-1, 1]) {
|
|
ctx.fillStyle = side < 0 ? "rgba(88,177,231,0.98)" : "rgba(239,111,116,0.98)";
|
|
roundedRect(ctx, side * r * 0.79 - r * 0.18, -r * 0.26, r * 0.36, r * 0.22, r * 0.07);
|
|
ctx.fill(); ctx.stroke();
|
|
ctx.strokeStyle = "rgba(70,55,45,0.75)";
|
|
ctx.lineWidth = 2;
|
|
ctx.beginPath();
|
|
ctx.moveTo(side * r * 0.90, -r * 0.22);
|
|
ctx.lineTo(side * r * 0.90, -r * 0.52);
|
|
ctx.stroke();
|
|
}
|
|
ctx.restore();
|
|
ctx.fillStyle = "rgba(70,58,48,0.9)";
|
|
ctx.beginPath(); ctx.arc(0, 0, r * 0.13, 0, Math.PI * 2); ctx.fill();
|
|
ctx.restore();
|
|
} else if (this.type === "circuit_board") {
|
|
const circuit = globalThis.TarinaiCircuitBoardSystem;
|
|
const angle = Number(this.angle || 0) || 0;
|
|
const active = Object.values(this._circuitOutputState || {}).some(Boolean);
|
|
ctx.save();
|
|
ctx.rotate(angle);
|
|
ctx.fillStyle = active ? "rgba(44,139,82,0.98)" : (styleNight ? "rgba(41,91,62,0.98)" : "rgba(47,126,76,0.98)");
|
|
ctx.strokeStyle = styleNight ? "rgba(19,48,31,0.92)" : "rgba(26,74,42,0.90)";
|
|
ctx.lineWidth = 2.4;
|
|
const boardHalfW = Number(circuit?.boardHalfWidth?.(this) ?? circuit?.HALF_W ?? 32) || 32;
|
|
const boardHalfH = Number(circuit?.boardHalfHeight?.(this) ?? circuit?.HALF_H ?? 32) || 32;
|
|
roundedRect(ctx, -boardHalfW, -boardHalfH, boardHalfW * 2, boardHalfH * 2, 10); ctx.fill(); ctx.stroke();
|
|
ctx.strokeStyle = "rgba(216,190,81,0.64)";
|
|
ctx.lineWidth = 1.4;
|
|
const traceRows = Math.max(1, Math.min(7, Math.floor(boardHalfH / 9)));
|
|
for (let i = -traceRows; i <= traceRows; i++) {
|
|
const y = i * Math.max(5, boardHalfH / Math.max(2, traceRows * 1.7));
|
|
ctx.beginPath(); ctx.moveTo(-boardHalfW * 0.72, y); ctx.lineTo(-boardHalfW * 0.28, y); ctx.lineTo(0, y * 0.45); ctx.stroke();
|
|
ctx.beginPath(); ctx.moveTo(boardHalfW * 0.72, y); ctx.lineTo(boardHalfW * 0.28, y); ctx.lineTo(0, y * 0.45); ctx.stroke();
|
|
}
|
|
ctx.fillStyle = "rgba(30,39,34,0.94)";
|
|
roundedRect(ctx, -15, -16, 30, 32, 5); ctx.fill();
|
|
ctx.fillStyle = "rgba(236,245,235,0.92)";
|
|
ctx.font = "bold 11px ui-rounded, sans-serif"; ctx.textAlign = "center"; ctx.textBaseline = "middle";
|
|
ctx.fillText("LOGIC", 0, 0);
|
|
for (const port of circuit?.ports?.(this) || []) {
|
|
const local = circuit.localPortPosition(port, this);
|
|
const on = port.direction === "input" ? Boolean(this._circuitInputState?.[port.id]) : Boolean(this._circuitOutputState?.[port.id]);
|
|
ctx.fillStyle = on ? "rgba(255,228,91,1)" : (port.direction === "input" ? "rgba(92,189,255,0.96)" : "rgba(255,143,77,0.96)");
|
|
ctx.strokeStyle = "rgba(25,35,31,0.88)";
|
|
ctx.lineWidth = 1.4;
|
|
ctx.beginPath(); ctx.arc(local.x, local.y, 5.4, 0, Math.PI * 2); ctx.fill(); ctx.stroke();
|
|
}
|
|
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.34;
|
|
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.04, this.r * 0.26, this.r * 1.48, 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 === "dry_ice") {
|
|
const r = this.r || 21;
|
|
const time = this.world?.time || (typeof performance !== "undefined" ? performance.now() * 0.001 : 0);
|
|
const powered = globalThis.TarinaiSignalSystem?.powerOverride?.(this) !== false;
|
|
ctx.save();
|
|
if (powered && itemShadowsEnabled()) {
|
|
ctx.shadowColor = "rgba(90, 178, 255, 0.30)";
|
|
ctx.shadowBlur = 11;
|
|
}
|
|
|
|
// \u65E7\u7248\u306E\u6C37\u584A\u98A8\u304B\u3089\u3001\u767D\u3044\u4FDD\u51B7\u30C7\u30E5\u30EF\u30FC\u7F36\u98A8\u306E\u5916\u89B3\u3078\u5909\u66F4\u3002
|
|
const bodyW = r * 1.38;
|
|
const bodyH = r * 1.56;
|
|
const x = -bodyW / 2;
|
|
const y = -bodyH * 0.36;
|
|
const capH = r * 0.34;
|
|
const footH = r * 0.18;
|
|
|
|
const grad = ctx.createLinearGradient(x, y, x + bodyW, y);
|
|
grad.addColorStop(0, styleNight ? "rgba(134, 169, 188, 0.96)" : "rgba(214, 242, 252, 0.98)");
|
|
grad.addColorStop(0.48, styleNight ? "rgba(230, 247, 252, 0.98)" : "rgba(255, 255, 252, 1)");
|
|
grad.addColorStop(1, styleNight ? "rgba(108, 150, 176, 0.98)" : "rgba(164, 220, 244, 0.98)");
|
|
|
|
ctx.fillStyle = grad;
|
|
ctx.strokeStyle = styleNight ? "rgba(54, 96, 124, 0.82)" : "rgba(63, 138, 180, 0.76)";
|
|
ctx.lineWidth = 2.1;
|
|
roundedRect(ctx, x, y, bodyW, bodyH, r * 0.28);
|
|
ctx.fill();
|
|
ctx.stroke();
|
|
|
|
ctx.fillStyle = styleNight ? "rgba(72, 118, 148, 0.96)" : "rgba(88, 176, 220, 0.96)";
|
|
roundedRect(ctx, -bodyW * 0.42, y - capH * 0.52, bodyW * 0.84, capH, r * 0.14);
|
|
ctx.fill();
|
|
ctx.strokeStyle = styleNight ? "rgba(42, 74, 96, 0.78)" : "rgba(48, 119, 158, 0.72)";
|
|
ctx.stroke();
|
|
|
|
ctx.fillStyle = styleNight ? "rgba(41, 86, 118, 0.86)" : "rgba(39, 147, 198, 0.86)";
|
|
roundedRect(ctx, -bodyW * 0.34, y + bodyH * 0.24, bodyW * 0.68, bodyH * 0.22, r * 0.10);
|
|
ctx.fill();
|
|
ctx.fillStyle = "rgba(255,255,255,0.92)";
|
|
ctx.font = `bold ${Math.max(9, Math.round(r * 0.34))}px ui-rounded, sans-serif`;
|
|
ctx.textAlign = "center";
|
|
ctx.textBaseline = "middle";
|
|
ctx.fillText("CO\u2082", 0, y + bodyH * 0.35);
|
|
|
|
ctx.strokeStyle = "rgba(255,255,255,0.72)";
|
|
ctx.lineWidth = 1.2;
|
|
ctx.beginPath();
|
|
ctx.moveTo(x + bodyW * 0.25, y + bodyH * 0.05);
|
|
ctx.lineTo(x + bodyW * 0.25, y + bodyH * 0.72);
|
|
ctx.stroke();
|
|
|
|
ctx.fillStyle = styleNight ? "rgba(61, 89, 110, 0.72)" : "rgba(84, 132, 156, 0.58)";
|
|
roundedRect(ctx, -bodyW * 0.48, y + bodyH - footH * 0.4, bodyW * 0.96, footH, r * 0.10);
|
|
ctx.fill();
|
|
|
|
if (powered) for (let i = 0; i < 4; i++) {
|
|
const phase = (time * (0.42 + i * 0.06) + i * 0.23 + (this.seed || 0)) % 1;
|
|
const sx = -r * 0.50 + i * r * 0.33 + Math.sin(time * 1.4 + i) * r * 0.06;
|
|
const sy = y - capH * 0.62 - phase * r * 0.76;
|
|
ctx.globalAlpha = (0.30 + (1 - phase) * 0.28) * (styleNight ? 0.76 : 1);
|
|
ctx.fillStyle = "rgba(222, 247, 255, 0.72)";
|
|
ctx.beginPath();
|
|
ctx.ellipse(sx, sy, r * (0.12 + phase * 0.08), r * (0.07 + phase * 0.05), Math.sin(i) * 0.7, 0, Math.PI * 2);
|
|
ctx.fill();
|
|
}
|
|
ctx.restore();
|
|
} else if (this.type === "stove") {
|
|
const r = this.r || 24;
|
|
const powered = globalThis.TarinaiSignalSystem?.powerOverride?.(this) !== false;
|
|
ctx.save();
|
|
if (powered && itemShadowsEnabled()) {
|
|
ctx.shadowColor = "rgba(255, 120, 43, 0.34)";
|
|
ctx.shadowBlur = 9;
|
|
}
|
|
ctx.fillStyle = styleNight ? "rgba(96, 72, 58, 0.96)" : "rgba(150, 104, 76, 0.96)";
|
|
ctx.strokeStyle = styleNight ? "rgba(49, 35, 29, 0.78)" : "rgba(92, 62, 45, 0.72)";
|
|
ctx.lineWidth = 2.2;
|
|
roundedRect(ctx, -r * 0.78, -r * 0.62, r * 1.56, r * 1.24, r * 0.20);
|
|
ctx.fill();
|
|
ctx.stroke();
|
|
ctx.fillStyle = "rgba(42, 32, 28, 0.92)";
|
|
roundedRect(ctx, -r * 0.48, -r * 0.34, r * 0.96, r * 0.62, r * 0.14);
|
|
ctx.fill();
|
|
if (powered) {
|
|
const flame = 0.85 + 0.15 * Math.sin((this.world?.time || (typeof performance !== "undefined" ? performance.now() * 0.001 : 0)) * 4.2 + (this.seed || 0));
|
|
ctx.fillStyle = "rgba(255, 168, 57, 0.92)";
|
|
ctx.beginPath();
|
|
ctx.moveTo(0, r * 0.16);
|
|
ctx.bezierCurveTo(-r * 0.38, -r * 0.10, -r * 0.10, -r * 0.52 * flame, 0, -r * 0.66 * flame);
|
|
ctx.bezierCurveTo(r * 0.34, -r * 0.20, r * 0.40, -r * 0.02, 0, r * 0.16);
|
|
ctx.fill();
|
|
ctx.fillStyle = "rgba(255, 235, 120, 0.82)";
|
|
ctx.beginPath();
|
|
ctx.ellipse(0, -r * 0.10, r * 0.16, r * 0.28 * flame, 0, 0, Math.PI * 2);
|
|
ctx.fill();
|
|
}
|
|
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 = getRenderableImage("ant_worker", "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 === "balloon") {
|
|
const speed = Math.hypot(this.vx || 0, this.vy || 0);
|
|
const moving = clamp(speed / 260, 0, 1);
|
|
const r = this.r || 20;
|
|
const wobble = Math.sin(t * 3.0 + (this.seed || 0) * 0.037 + (this.balloonWobble || 0)) * 0.10 + (this.spin || 0) * 0.05;
|
|
ctx.save();
|
|
ctx.rotate(wobble);
|
|
if (itemShadowsEnabled()) {
|
|
ctx.shadowColor = styleNight ? "rgba(0,0,0,0.28)" : "rgba(68,34,45,0.16)";
|
|
ctx.shadowBlur = 2 + moving * 4;
|
|
ctx.shadowOffsetY = 2;
|
|
}
|
|
const grad = ctx.createRadialGradient(-r * 0.34, -r * 0.54, r * 0.16, 0, -r * 0.18, r * 1.18);
|
|
grad.addColorStop(0, "rgba(232,250,255,0.98)");
|
|
grad.addColorStop(0.44, "rgba(96,196,238,0.94)");
|
|
grad.addColorStop(1, "rgba(37,126,198,0.90)");
|
|
ctx.fillStyle = grad;
|
|
ctx.strokeStyle = "rgba(34,82,125,0.70)";
|
|
ctx.lineWidth = 1.8;
|
|
ctx.beginPath();
|
|
ctx.ellipse(0, -r * 0.18, r * 0.82, r * 1.06, 0, 0, Math.PI * 2);
|
|
ctx.fill();
|
|
ctx.stroke();
|
|
ctx.shadowColor = "transparent";
|
|
ctx.fillStyle = "rgba(255,255,255,0.50)";
|
|
ctx.beginPath();
|
|
ctx.ellipse(-r * 0.28, -r * 0.62, r * 0.18, r * 0.28, -0.42, 0, Math.PI * 2);
|
|
ctx.fill();
|
|
ctx.fillStyle = "rgba(35,106,168,0.88)";
|
|
ctx.beginPath();
|
|
ctx.moveTo(-r * 0.18, r * 0.78);
|
|
ctx.lineTo(r * 0.18, r * 0.78);
|
|
ctx.lineTo(0, r * 1.12);
|
|
ctx.closePath();
|
|
ctx.fill();
|
|
ctx.strokeStyle = "rgba(61,113,150,0.58)";
|
|
ctx.lineWidth = 1.2;
|
|
ctx.beginPath();
|
|
ctx.moveTo(0, r * 1.06);
|
|
ctx.bezierCurveTo(-r * 0.18, r * 1.34, r * 0.20, r * 1.52, -r * 0.04, r * 1.76);
|
|
ctx.stroke();
|
|
if (moving > 0.42) {
|
|
ctx.globalAlpha *= 0.10 + moving * 0.12;
|
|
ctx.strokeStyle = "rgba(231,249,255,0.90)";
|
|
ctx.lineWidth = 2;
|
|
ctx.beginPath();
|
|
ctx.arc(-r * 0.10, -r * 0.18, r * 0.92, -1.92, -0.95);
|
|
ctx.stroke();
|
|
}
|
|
ctx.restore();
|
|
} else if (this.type === "fan") {
|
|
const r = this.r || 26;
|
|
const bodyAngle = Number.isFinite(Number(this.fanBodyAngle)) ? normalizedItemAngle(Number(this.fanBodyAngle), itemAngleFor(this)) : itemAngleFor(this);
|
|
const windAngle = itemAngleFor(this);
|
|
const powered = globalThis.TarinaiSignalSystem?.powerOverride?.(this) !== false;
|
|
const spin = (this.fanSpin || 0) + (powered ? t * 10.5 : 0);
|
|
ctx.save();
|
|
ctx.rotate(windAngle);
|
|
if (powered) {
|
|
const windRange = 320;
|
|
const windStart = r * 1.02;
|
|
const windSpread = 0.62;
|
|
const windHalfStart = 26 + windStart * Math.tan(windSpread);
|
|
const windHalfEnd = 26 + windRange * Math.tan(windSpread);
|
|
const fanAlpha = ctx.globalAlpha;
|
|
ctx.globalAlpha = fanAlpha * 0.16;
|
|
ctx.fillStyle = "rgba(132,190,220,0.34)";
|
|
ctx.strokeStyle = "rgba(80,155,205,0.46)";
|
|
ctx.lineWidth = 1.4;
|
|
ctx.setLineDash([10, 8]);
|
|
ctx.beginPath();
|
|
ctx.moveTo(windStart, -windHalfStart);
|
|
ctx.lineTo(windRange, -windHalfEnd);
|
|
ctx.quadraticCurveTo(windRange + r * 0.36, 0, windRange, windHalfEnd);
|
|
ctx.lineTo(windStart, windHalfStart);
|
|
ctx.quadraticCurveTo(r * 1.28, 0, windStart, -windHalfStart);
|
|
ctx.closePath();
|
|
ctx.fill();
|
|
ctx.stroke();
|
|
ctx.setLineDash([]);
|
|
|
|
// Wind direction indicator: centerline + arrowheads. The cone shows the
|
|
// affected range; the arrows make the actual airflow direction explicit.
|
|
ctx.globalAlpha = fanAlpha * 0.52;
|
|
ctx.strokeStyle = "rgba(46,132,200,0.70)";
|
|
ctx.fillStyle = "rgba(46,132,200,0.62)";
|
|
ctx.lineWidth = 2.1;
|
|
ctx.setLineDash([16, 10]);
|
|
ctx.beginPath();
|
|
ctx.moveTo(windStart + r * 0.55, 0);
|
|
ctx.lineTo(windRange - r * 0.50, 0);
|
|
ctx.stroke();
|
|
ctx.setLineDash([]);
|
|
for (let i = 0; i < 3; i++) {
|
|
const x = windStart + r * 2.0 + i * ((windRange - windStart - r * 3.2) / 2);
|
|
const wobbleY = Math.sin(t * 2.4 + i * 1.7) * r * 0.035;
|
|
const arrow = Math.max(9, r * 0.42);
|
|
ctx.beginPath();
|
|
ctx.moveTo(x + arrow * 0.55, wobbleY);
|
|
ctx.lineTo(x - arrow * 0.55, wobbleY - arrow * 0.48);
|
|
ctx.lineTo(x - arrow * 0.30, wobbleY);
|
|
ctx.lineTo(x - arrow * 0.55, wobbleY + arrow * 0.48);
|
|
ctx.closePath();
|
|
ctx.fill();
|
|
}
|
|
|
|
ctx.globalAlpha = fanAlpha * 0.34;
|
|
ctx.strokeStyle = "rgba(132,190,220,0.42)";
|
|
ctx.lineWidth = 2.0;
|
|
for (let i = -2; i <= 2; i++) {
|
|
const y = i * r * 0.28 + Math.sin(t * 3 + i) * r * 0.08;
|
|
ctx.beginPath();
|
|
ctx.moveTo(r * 0.95, y);
|
|
ctx.bezierCurveTo(r * 1.55, y - r * 0.22, r * 2.30, y + r * 0.20, r * 3.05, y - r * 0.08);
|
|
ctx.stroke();
|
|
}
|
|
ctx.globalAlpha = fanAlpha;
|
|
}
|
|
ctx.restore();
|
|
ctx.save();
|
|
ctx.rotate(bodyAngle);
|
|
if (itemShadowsEnabled()) {
|
|
ctx.shadowColor = styleNight ? "rgba(0,0,0,0.28)" : "rgba(52,40,26,0.16)";
|
|
ctx.shadowBlur = 3;
|
|
ctx.shadowOffsetY = 2;
|
|
}
|
|
ctx.strokeStyle = "rgba(75,92,105,0.88)";
|
|
ctx.fillStyle = "rgba(221,231,235,0.96)";
|
|
ctx.lineWidth = 2;
|
|
ctx.beginPath();
|
|
ctx.arc(0, 0, r * 0.82, 0, Math.PI * 2);
|
|
ctx.fill();
|
|
ctx.stroke();
|
|
ctx.shadowColor = "transparent";
|
|
ctx.strokeStyle = "rgba(96,117,130,0.42)";
|
|
ctx.lineWidth = 1.0;
|
|
for (let i = 0; i < 8; i++) {
|
|
const a = i * Math.PI / 4;
|
|
ctx.beginPath();
|
|
ctx.moveTo(0, 0);
|
|
ctx.lineTo(Math.cos(a) * r * 0.76, Math.sin(a) * r * 0.76);
|
|
ctx.stroke();
|
|
}
|
|
ctx.save();
|
|
ctx.rotate(spin);
|
|
ctx.fillStyle = "rgba(126,170,196,0.88)";
|
|
ctx.strokeStyle = "rgba(62,82,96,0.44)";
|
|
ctx.lineWidth = 1.1;
|
|
for (let i = 0; i < 3; i++) {
|
|
ctx.rotate(Math.PI * 2 / 3);
|
|
ctx.beginPath();
|
|
ctx.ellipse(r * 0.30, 0, r * 0.38, r * 0.13, 0.10, 0, Math.PI * 2);
|
|
ctx.fill();
|
|
ctx.stroke();
|
|
}
|
|
ctx.restore();
|
|
ctx.fillStyle = "rgba(65,78,86,0.94)";
|
|
ctx.beginPath();
|
|
ctx.arc(0, 0, r * 0.16, 0, Math.PI * 2);
|
|
ctx.fill();
|
|
ctx.strokeStyle = "rgba(62,82,96,0.74)";
|
|
ctx.lineWidth = 3;
|
|
ctx.beginPath();
|
|
ctx.moveTo(-r * 0.22, r * 0.84);
|
|
ctx.lineTo(-r * 0.22, r * 1.35);
|
|
ctx.stroke();
|
|
ctx.fillStyle = "rgba(91,104,112,0.88)";
|
|
ctx.beginPath();
|
|
ctx.ellipse(-r * 0.22, r * 1.42, r * 0.62, r * 0.16, 0, 0, Math.PI * 2);
|
|
ctx.fill();
|
|
ctx.strokeStyle = "rgba(65,78,86,0.74)";
|
|
ctx.stroke();
|
|
ctx.fillStyle = "rgba(92,116,132,0.96)";
|
|
ctx.beginPath();
|
|
ctx.moveTo(r * 0.82, 0);
|
|
ctx.lineTo(r * 1.08, -r * 0.14);
|
|
ctx.lineTo(r * 1.08, r * 0.14);
|
|
ctx.closePath();
|
|
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);
|
|
const r = this.r || 18;
|
|
ctx.save();
|
|
ctx.rotate(this.spin || 0);
|
|
if (itemShadowsEnabled()) {
|
|
ctx.shadowColor = styleNight ? "rgba(0,0,0,0.34)" : "rgba(52,40,26,0.18)";
|
|
ctx.shadowBlur = 2 + moving * 3;
|
|
ctx.shadowOffsetY = 2;
|
|
}
|
|
// Use the original soccer-ball emoji as the visible sprite. The
|
|
// collision center remains item.x/item.y; rendering is centered via
|
|
// textAlign/textBaseline rather than replacing the emoji with vector art.
|
|
ctx.font = `${Math.max(18, r * 2.05)}px "Apple Color Emoji", "Segoe UI Emoji", "Noto Color Emoji", sans-serif`;
|
|
ctx.textAlign = "center";
|
|
ctx.textBaseline = "middle";
|
|
ctx.fillText("\u26BD", 0, 0);
|
|
if (moving > 0.55) {
|
|
ctx.globalAlpha *= 0.10 + moving * 0.10;
|
|
ctx.shadowColor = "transparent";
|
|
ctx.fillStyle = "#ffffff";
|
|
ctx.beginPath();
|
|
ctx.ellipse(-r * 0.42, -r * 0.45, r * 0.25, r * 0.12, -0.45, 0, Math.PI * 2);
|
|
ctx.fill();
|
|
}
|
|
ctx.restore();
|
|
} else if (this.type === "fire") {
|
|
const r = this.r || 14;
|
|
const phase = t * 7.0 + this.seed;
|
|
ctx.save();
|
|
ctx.globalAlpha *= 0.90;
|
|
if (typeof drawSimpleFireShape === "function") {
|
|
drawSimpleFireShape(ctx, 0, 0, r * 1.12, { phase, shadow: itemShadowsEnabled() });
|
|
} else {
|
|
ctx.fillStyle = "rgba(236,46,24,0.88)";
|
|
ctx.beginPath();
|
|
ctx.ellipse(0, -r * 0.18, r * 0.48, r * 0.96, 0, 0, Math.PI * 2);
|
|
ctx.fill();
|
|
}
|
|
ctx.restore();
|
|
} else if (this.type === "sticky_bomb") {
|
|
const r = this.r || 16;
|
|
const fuse = clamp((this.fuseTimer ?? 12) / Math.max(this.fuseMax || 12, 0.1), 0, 1);
|
|
const flash = fuse < 0.34 ? (0.5 + Math.sin(t * 24 + this.seed) * 0.5) : 0;
|
|
ctx.save();
|
|
ctx.rotate((this.spin || 0) * 0.18 + Math.sin(t * 2.0 + this.seed) * 0.05);
|
|
ctx.fillStyle = flash > 0.62 ? "#ffdf4e" : "#26272d";
|
|
ctx.strokeStyle = "rgba(12,13,18,0.86)";
|
|
ctx.lineWidth = 2.2;
|
|
ctx.beginPath();
|
|
ctx.arc(0, 0, r * 0.78, 0, Math.PI * 2);
|
|
ctx.fill();
|
|
ctx.stroke();
|
|
ctx.fillStyle = "rgba(255,255,255,0.20)";
|
|
ctx.beginPath();
|
|
ctx.ellipse(-r * 0.28, -r * 0.32, r * 0.20, r * 0.12, -0.55, 0, Math.PI * 2);
|
|
ctx.fill();
|
|
ctx.fillStyle = "#d7d7d2";
|
|
ctx.strokeStyle = "rgba(38,38,36,0.82)";
|
|
ctx.lineWidth = 1.5;
|
|
roundedRect(ctx, -r * 0.28, -r * 0.98, r * 0.56, r * 0.28, r * 0.08);
|
|
ctx.fill();
|
|
ctx.stroke();
|
|
ctx.strokeStyle = flash > 0.1 ? "rgba(255,238,90,0.95)" : "rgba(214,102,44,0.82)";
|
|
ctx.lineWidth = 2;
|
|
ctx.beginPath();
|
|
ctx.moveTo(r * 0.08, -r * 1.02);
|
|
ctx.quadraticCurveTo(r * 0.42, -r * 1.30, r * 0.88, -r * 1.08);
|
|
ctx.stroke();
|
|
ctx.fillStyle = flash > 0.1 ? "rgba(255,238,92,0.96)" : "rgba(255,92,42,0.80)";
|
|
ctx.beginPath();
|
|
ctx.arc(r * 0.92, -r * 1.06, 2.2 + flash * 2.0, 0, Math.PI * 2);
|
|
ctx.fill();
|
|
if (this.stickyBombCarrierId) {
|
|
ctx.fillStyle = "rgba(255,86,70,0.92)";
|
|
ctx.beginPath();
|
|
ctx.moveTo(0, r * 0.72);
|
|
ctx.lineTo(-r * 0.22, r * 1.02);
|
|
ctx.lineTo(r * 0.22, r * 1.02);
|
|
ctx.closePath();
|
|
ctx.fill();
|
|
}
|
|
ctx.restore();
|
|
} else if (this.type === "firecracker" || this.type === "flame_firecracker") {
|
|
const flameMode = this.type === "flame_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;
|
|
const timerText = String(Math.max(0, Math.ceil(this.fuseTimer ?? 5)));
|
|
ctx.rotate(-0.22 + Math.sin(this.seed) * 0.12);
|
|
ctx.fillStyle = flash > 0.65 ? "#ffed77" : (flameMode ? "#d8241f" : "#5a4744");
|
|
ctx.strokeStyle = flameMode ? "rgba(96,18,18,0.88)" : "rgba(35,30,29,0.88)";
|
|
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 = flameMode ? "#ff6b32" : "#897065";
|
|
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(48,38,31,0.86)";
|
|
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)" : (flameMode ? "rgba(255,80,40,0.78)" : "rgba(188,148,88,0.62)");
|
|
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(255,246,214,0.92)";
|
|
ctx.strokeStyle = flameMode ? "rgba(84,18,14,0.92)" : "rgba(24,22,21,0.92)";
|
|
ctx.lineWidth = 2.6;
|
|
ctx.font = `bold ${Math.max(10, this.r * 0.72)}px ui-rounded, sans-serif`;
|
|
ctx.textAlign = "center";
|
|
ctx.textBaseline = "middle";
|
|
ctx.strokeText(timerText, 0, this.r * 0.02);
|
|
ctx.fillText(timerText, 0, this.r * 0.02);
|
|
} else if (this.type === "fence_v" || this.type === "fence_h" || this.type === "glass_wall" || this.type === "bounce_fence" || this.type === "bounce_fence_v" || this.type === "gate_fence" || this.type === "one_way_fence") {
|
|
const bounce = this.type === "bounce_fence" || this.type === "bounce_fence_v";
|
|
const gate = this.type === "gate_fence";
|
|
const oneWay = this.type === "one_way_fence";
|
|
const glassWall = this.type === "glass_wall";
|
|
const angle = itemAngleFor(this);
|
|
const len = Math.max(112, this.r * 3.55);
|
|
const thick = Math.max(10, this.r * 0.31);
|
|
const open = gate && this.gateOpen;
|
|
ctx.save();
|
|
ctx.rotate(angle);
|
|
const wood = bounce ? (styleNight ? "#4e6f9f" : "#5aa2e0") : oneWay ? (styleNight ? "#6b5a86" : "#9b78c8") : gate ? (styleNight ? "#5d675f" : "#6f8b68") : (styleNight ? "#7a5b3c" : (styleWarm ? "#a96f35" : "#965f31"));
|
|
const edge = bounce ? (styleNight ? "#243755" : "#286093") : oneWay ? (styleNight ? "#352943" : "#593a80") : gate ? (styleNight ? "#334038" : "#375737") : (styleNight ? "#4d3d2f" : "#5f3f25");
|
|
const hi = bounce ? (styleNight ? "rgba(190,230,255,0.24)" : "rgba(235,252,255,0.54)") : oneWay ? (styleNight ? "rgba(232,220,255,0.25)" : "rgba(255,245,255,0.55)") : gate ? (styleNight ? "rgba(214,240,210,0.18)" : "rgba(220,255,210,0.34)") : (styleNight ? "rgba(198,166,116,0.20)" : "rgba(232,176,94,0.32)");
|
|
const drawRail = (x0, y0, w, h, alpha = 1) => {
|
|
ctx.save();
|
|
ctx.globalAlpha *= alpha;
|
|
ctx.fillStyle = wood;
|
|
ctx.strokeStyle = edge;
|
|
ctx.lineWidth = bounce ? 2.8 : 2.4;
|
|
ctx.beginPath();
|
|
roundedRect(ctx, x0, y0, w, h, thick / 2);
|
|
ctx.fill();
|
|
ctx.stroke();
|
|
ctx.fillStyle = hi;
|
|
ctx.beginPath();
|
|
roundedRect(ctx, x0 + 6, y0 + thick * 0.25, Math.max(4, w - 12), thick * 0.20, thick / 3);
|
|
ctx.fill();
|
|
ctx.restore();
|
|
};
|
|
if (glassWall) {
|
|
const glassGrad = ctx.createLinearGradient(-len / 2, -thick / 2, len / 2, thick / 2);
|
|
glassGrad.addColorStop(0, styleNight ? "rgba(92,134,154,0.30)" : "rgba(208,244,255,0.36)");
|
|
glassGrad.addColorStop(0.48, styleNight ? "rgba(152,196,210,0.18)" : "rgba(255,255,255,0.18)");
|
|
glassGrad.addColorStop(1, styleNight ? "rgba(66,104,128,0.36)" : "rgba(134,214,238,0.32)");
|
|
ctx.fillStyle = glassGrad;
|
|
ctx.strokeStyle = styleNight ? "rgba(184,228,244,0.78)" : "rgba(80,176,208,0.72)";
|
|
ctx.lineWidth = Math.max(2, thick * 0.18);
|
|
ctx.beginPath();
|
|
roundedRect(ctx, -len / 2, -thick / 2, len, thick, thick * 0.45);
|
|
ctx.fill();
|
|
ctx.stroke();
|
|
ctx.strokeStyle = styleNight ? "rgba(235,252,255,0.46)" : "rgba(255,255,255,0.72)";
|
|
ctx.lineWidth = Math.max(1.2, thick * 0.08);
|
|
for (let x = -len * 0.42; x <= len * 0.35; x += len * 0.26) {
|
|
ctx.beginPath();
|
|
ctx.moveTo(x, -thick * 0.28);
|
|
ctx.lineTo(x + len * 0.16, thick * 0.28);
|
|
ctx.stroke();
|
|
}
|
|
ctx.fillStyle = styleNight ? "rgba(220,248,255,0.74)" : "rgba(255,255,255,0.82)";
|
|
ctx.beginPath();
|
|
roundedRect(ctx, -len * 0.46, -thick * 0.32, len * 0.32, thick * 0.10, thick * 0.10);
|
|
ctx.fill();
|
|
} else if (gate) {
|
|
const pivotX = -len / 2;
|
|
ctx.save();
|
|
drawRail(pivotX, -thick / 2, len, thick, open ? 0.34 : 1);
|
|
ctx.strokeStyle = styleNight ? "rgba(210,230,210,0.55)" : "rgba(255,255,255,0.54)";
|
|
ctx.lineWidth = Math.max(1.6, thick * 0.12);
|
|
for (let x = pivotX + len * 0.26; x < pivotX + len * 0.88; x += len * 0.24) {
|
|
ctx.beginPath();
|
|
ctx.moveTo(x, -thick * 0.42);
|
|
ctx.lineTo(x, thick * 0.42);
|
|
ctx.stroke();
|
|
}
|
|
ctx.restore();
|
|
ctx.fillStyle = styleNight ? "#c7d7bf" : "#e8f2d8";
|
|
ctx.strokeStyle = edge;
|
|
ctx.lineWidth = 2;
|
|
ctx.beginPath();
|
|
ctx.arc(pivotX, 0, thick * 0.54, 0, Math.PI * 2);
|
|
ctx.fill(); ctx.stroke();
|
|
if (open) {
|
|
ctx.fillStyle = "rgba(80,120,72,0.65)";
|
|
ctx.font = `bold ${Math.max(9, thick * 0.62)}px ui-rounded, sans-serif`;
|
|
ctx.textAlign = "center";
|
|
ctx.textBaseline = "middle";
|
|
ctx.fillText("OPEN", 0, -thick * 1.08);
|
|
}
|
|
} else {
|
|
drawRail(-len / 2, -thick / 2, len, thick, 1);
|
|
if (bounce) {
|
|
ctx.strokeStyle = styleNight ? "rgba(204,236,255,0.58)" : "rgba(255,255,255,0.72)";
|
|
ctx.lineWidth = Math.max(1.3, thick * 0.11);
|
|
ctx.beginPath();
|
|
const coils = 9;
|
|
for (let i = 0; i <= coils; i += 1) {
|
|
const x = -len * 0.42 + len * 0.84 * (i / coils);
|
|
const y = (i % 2 === 0 ? -1 : 1) * thick * 0.23;
|
|
if (i === 0) ctx.moveTo(x, y);
|
|
else ctx.lineTo(x, y);
|
|
}
|
|
ctx.stroke();
|
|
ctx.fillStyle = styleNight ? "rgba(35,53,84,0.78)" : "rgba(36,93,145,0.72)";
|
|
for (const xx of [-0.32, 0, 0.32]) {
|
|
ctx.beginPath();
|
|
ctx.ellipse(len * xx, 0, thick * 0.19, thick * 0.37, 0, 0, Math.PI * 2);
|
|
ctx.fill();
|
|
}
|
|
}
|
|
if (oneWay) {
|
|
ctx.fillStyle = styleNight ? "rgba(246,235,255,0.92)" : "rgba(255,255,255,0.94)";
|
|
for (const xx of [-0.28, 0, 0.28]) {
|
|
ctx.beginPath();
|
|
ctx.moveTo(len * xx, -thick * 0.72);
|
|
ctx.lineTo(len * xx - thick * 0.34, -thick * 0.12);
|
|
ctx.lineTo(len * xx + thick * 0.34, -thick * 0.12);
|
|
ctx.closePath();
|
|
ctx.fill();
|
|
}
|
|
}
|
|
}
|
|
ctx.restore();
|
|
} else if (this.type === "pressure_switch") {
|
|
const active = Boolean(this.signalActive);
|
|
ctx.save(); ctx.globalAlpha *= active ? 1 : 0.78;
|
|
ctx.fillStyle = active ? (styleNight ? "#ffcf5a" : "#ffd74c") : (styleNight ? "#505866" : "#c7ced8");
|
|
ctx.strokeStyle = active ? "#9b6512" : "#69717a"; ctx.lineWidth=3;
|
|
ctx.beginPath(); ctx.roundRect?.(-27,-18,54,36,8); if (!ctx.roundRect) { ctx.rect(-27,-18,54,36); } ctx.fill(); ctx.stroke();
|
|
ctx.fillStyle = active ? "#fff6c9" : "#7b838d"; ctx.beginPath(); ctx.arc(0,-3,9,0,Math.PI*2); ctx.fill();
|
|
const detectorMark = { tarinai:"\u305f", item:"\u7269", time:"\u6642", hunger_avg:"\u8179", stress_avg:"\u5727", sleep_avg:"\u7720", low_hp_count:"HP", sick_count:"\u75c5", temperature:"\u2103", zunchi_count:"\u7cde", ant_count:"\u87fb", water_count:"\u6c34" }[this.pressureTarget] || "\u305f";
|
|
const detectorLabel = this.pressureTarget === "time" ? detectorMark : `${detectorMark}${Number(this.pressureMin ?? 1)}-${Number(this.pressureMax ?? 300)}`;
|
|
ctx.fillStyle = styleNight ? "#eef2f6" : "#26323e"; ctx.font="bold 10px ui-rounded, sans-serif"; ctx.textAlign="center"; ctx.fillText(detectorLabel,0,14);
|
|
ctx.restore();
|
|
} else if (this.type === "rotator") {
|
|
const angle = itemAngleFor(this);
|
|
const pb = window.TarinaiPhysicsBodySystem;
|
|
const powered = window.TarinaiMechanicalSystem?.isPowered?.(this) ?? (pb?.scalar?.(this, "motorOn", true) !== false);
|
|
const speed = Number((powered ? pb?.scalar?.(this, "motorSpeed", 0) : pb?.scalar?.(this, "spin", 0)) || 0) || 0;
|
|
const segments = (pb?.segments?.(this) || [[-78, 0, 78, 0], [0, -52, 0, 52]]);
|
|
const thick = Math.max(4, Math.min(34, Number(pb?.scalar?.(this, "thickness", 12) || 12) || 12));
|
|
ctx.save();
|
|
ctx.rotate(angle);
|
|
ctx.lineCap = "round";
|
|
ctx.lineJoin = "round";
|
|
ctx.globalAlpha *= powered ? 1 : 0.72;
|
|
ctx.strokeStyle = styleNight ? "#6e5f89" : "#8b6cc5";
|
|
ctx.lineWidth = thick;
|
|
ctx.shadowBlur = 0;
|
|
strokeSegments(ctx, this, segments, "body");
|
|
ctx.restore();
|
|
ctx.fillStyle = powered ? (styleNight ? "#3c324e" : "#f4eaff") : (styleNight ? "#34343a" : "#ece8ef");
|
|
ctx.strokeStyle = powered ? (styleNight ? "#a28cc8" : "#6e4fa4") : "rgba(92,88,98,0.82)";
|
|
ctx.lineWidth = 2.2;
|
|
ctx.beginPath();
|
|
ctx.arc(0, 0, Math.max(8, thick * 0.72), 0, Math.PI * 2);
|
|
ctx.fill(); ctx.stroke();
|
|
ctx.fillStyle = powered ? (styleNight ? "#d8c5ff" : "#6e4fa4") : "rgba(70,68,76,0.82)";
|
|
ctx.font = `bold ${Math.max(10, thick * 0.84)}px ui-rounded, sans-serif`;
|
|
ctx.textAlign = "center";
|
|
ctx.textBaseline = "middle";
|
|
ctx.fillText(powered ? (speed >= 0 ? "\u21bb" : "\u21ba") : "\u25cb", 0, 0);
|
|
} else if (this.type === "poison_block") {
|
|
const angle = itemAngleFor(this);
|
|
const pb = window.TarinaiPhysicsBodySystem;
|
|
const enabled = pb?.scalar?.(this, "solid", true) !== false;
|
|
const segments = (pb?.segments?.(this) || [[-52, -20, 52, -20], [52, -20, 52, 20], [52, 20, -52, 20], [-52, 20, -52, -20]]);
|
|
const thick = Math.max(4, Math.min(34, Number(pb?.scalar?.(this, "thickness", 11) || 11) || 11));
|
|
ctx.save();
|
|
ctx.rotate(angle);
|
|
ctx.lineCap = "round";
|
|
ctx.lineJoin = "round";
|
|
ctx.globalAlpha *= enabled ? 1 : 0.48;
|
|
ctx.strokeStyle = styleNight ? "#4d8e4e" : "#5cac4d";
|
|
ctx.lineWidth = thick;
|
|
ctx.shadowBlur = 0;
|
|
strokeSegments(ctx, this, segments, "body");
|
|
ctx.setLineDash([]);
|
|
ctx.restore();
|
|
// v39.15.53: \u6BD2\u30D6\u30ED\u30C3\u30AF\u306F\u7269\u7406\u8F2A\u90ED\u3060\u3051\u3092\u5B89\u5B9A\u63CF\u753B\u3059\u308B\u3002
|
|
// \u4E2D\u592E\u306E\u5857\u308A\u3064\u3076\u3057/\u6587\u5B57\u306F\u30AD\u30E3\u30C3\u30B7\u30E5\u66F4\u65B0\u6642\u306B\u660E\u6EC5\u3057\u3066\u898B\u3048\u308B\u305F\u3081\u63CF\u753B\u3057\u306A\u3044\u3002
|
|
} else if (this.type === "reciprocator") {
|
|
const bodyAngle = itemAngleFor(this);
|
|
const pb = window.TarinaiPhysicsBodySystem;
|
|
const axisAngle = window.TarinaiMechanicalSystem.railAxisAngle(this) ?? pb?.scalar?.(this, "railAxis", bodyAngle) ?? bodyAngle;
|
|
const powered = window.TarinaiMechanicalSystem?.isPowered?.(this) ?? (pb?.scalar?.(this, "railOn", true) !== false);
|
|
const thick = Math.max(4, Math.min(34, Number(pb?.scalar?.(this, "thickness", 12) || 12) || 12));
|
|
const tier = mechanicalRenderTier();
|
|
const travel = Math.max(24, Number(pb?.scalar?.(this, "railTravel", 150) || 150) || 150);
|
|
const halfTravel = travel * 0.5;
|
|
const phase = Math.max(-1, Math.min(1, Number(pb?.scalar?.(this, "railPhase", 0) || 0) || 0));
|
|
const reciprocatorDir = Math.sign(Number(pb?.scalar?.(this, "railDir", 1) || 1) || 1) || 1;
|
|
const passiveVelocity = Number(pb?.scalar?.(this, "slideSpeed", 0) || 0) || 0;
|
|
const motorVelocity = powered ? reciprocatorDir * Math.max(0, Number(pb?.scalar?.(this, "railMotorSpeed", 0) || 0) || 0) : 0;
|
|
const visualVelocity = motorVelocity + passiveVelocity;
|
|
const visualDirection = Math.abs(visualVelocity) > 0.8 ? Math.sign(visualVelocity) : reciprocatorDir;
|
|
const segments = (pb?.segments?.(this) || [[-78, 0, 78, 0]]);
|
|
ctx.save();
|
|
ctx.rotate(axisAngle);
|
|
ctx.save();
|
|
ctx.globalAlpha *= styleNight ? 0.24 : 0.30;
|
|
ctx.strokeStyle = styleNight ? "rgba(130,174,220,0.50)" : "rgba(70,120,170,0.42)";
|
|
ctx.lineWidth = 2;
|
|
ctx.setLineDash([7, 6]);
|
|
const trackCenterX = -phase * halfTravel;
|
|
ctx.beginPath();
|
|
ctx.moveTo(trackCenterX - halfTravel, 0);
|
|
ctx.lineTo(trackCenterX + halfTravel, 0);
|
|
ctx.stroke();
|
|
ctx.setLineDash([]);
|
|
for (const xx of [trackCenterX - halfTravel, trackCenterX + halfTravel]) {
|
|
ctx.beginPath();
|
|
ctx.arc(xx, 0, 4.5, 0, Math.PI * 2);
|
|
ctx.stroke();
|
|
}
|
|
ctx.restore();
|
|
ctx.rotate(bodyAngle - axisAngle);
|
|
ctx.lineCap = "round";
|
|
ctx.lineJoin = "round";
|
|
ctx.globalAlpha *= powered ? 1 : 0.72;
|
|
ctx.strokeStyle = styleNight ? "#4f7192" : "#6da6cf";
|
|
ctx.lineWidth = thick;
|
|
if (itemShadowsEnabled()) {
|
|
ctx.shadowColor = powered ? (styleNight ? "rgba(86,130,180,0.34)" : "rgba(80,150,205,0.28)") : "rgba(74,82,90,0.20)";
|
|
ctx.shadowBlur = tier === "low" ? 0 : (tier === "mid" ? Math.max(2, thick * 0.16) : Math.max(4, thick * 0.28));
|
|
}
|
|
strokeSegments(ctx, this, segments, "body");
|
|
ctx.shadowBlur = 0;
|
|
if (tier !== "low") {
|
|
ctx.strokeStyle = powered ? (styleNight ? "rgba(220,238,255,0.44)" : "rgba(255,255,255,0.48)") : "rgba(255,255,255,0.28)";
|
|
ctx.lineWidth = Math.max(1.4, thick * 0.22);
|
|
strokeSegments(ctx, this, segments, "body");
|
|
}
|
|
ctx.restore();
|
|
ctx.fillStyle = powered ? (styleNight ? "#2d3c4d" : "#eaf6ff") : (styleNight ? "#34383e" : "#e9edf0");
|
|
ctx.strokeStyle = powered ? (styleNight ? "#7faed0" : "#4d8cba") : "rgba(82,88,94,0.82)";
|
|
ctx.lineWidth = 2.2;
|
|
ctx.beginPath();
|
|
ctx.arc(0, 0, Math.max(8, thick * 0.72), 0, Math.PI * 2);
|
|
ctx.fill(); ctx.stroke();
|
|
ctx.fillStyle = powered ? (styleNight ? "#d8ecff" : "#235f91") : "rgba(72,76,82,0.82)";
|
|
ctx.font = `bold ${Math.max(10, thick * 0.78)}px ui-rounded, sans-serif`;
|
|
ctx.textAlign = "center";
|
|
ctx.textBaseline = "middle";
|
|
if (powered || Math.abs(visualVelocity) > 0.8) {
|
|
ctx.save();
|
|
ctx.rotate(axisAngle);
|
|
ctx.fillText(visualDirection >= 0 ? "\u2192" : "\u2190", 0, 0);
|
|
ctx.restore();
|
|
} else {
|
|
ctx.fillText("\u25cb", 0, 0);
|
|
}
|
|
} else if (["rope", "rod", "spring", "wire", "insulated_wire"].includes(this.type)) {
|
|
const runtime = window.TarinaiLinkRuntime;
|
|
const pb = window.TarinaiPhysicsBodySystem;
|
|
const a = runtime?.endpointWorld?.(pb?.endpoint?.(this, 0), world);
|
|
const b = runtime?.endpointWorld?.(pb?.endpoint?.(this, 1), world);
|
|
if (a && b) {
|
|
const ax = a.x - this.x, ay = a.y - this.y;
|
|
const bx = b.x - this.x, by = b.y - this.y;
|
|
ctx.save();
|
|
ctx.lineCap = "round";
|
|
ctx.lineJoin = "round";
|
|
if (this.type === "wire" || this.type === "insulated_wire") {
|
|
const active = Boolean(this.signalActive);
|
|
ctx.strokeStyle = this.type === "insulated_wire" ? (active ? "#ffd447" : "#506e92") : (active ? "#fff06a" : "#6f6254");
|
|
ctx.lineWidth = this.type === "insulated_wire" ? 5.2 : 3.2;
|
|
ctx.setLineDash(active ? [9,5] : []); ctx.beginPath(); ctx.moveTo(ax,ay); ctx.lineTo(bx,by); ctx.stroke(); ctx.setLineDash([]);
|
|
} else if (this.type === "spring") {
|
|
const dx=bx-ax, dy=by-ay, len=Math.max(1,Math.hypot(dx,dy)), nx=-dy/len, ny=dx/len;
|
|
ctx.strokeStyle=styleNight?"#d6d9df":"#6e737b"; ctx.lineWidth=4; ctx.beginPath(); ctx.moveTo(ax,ay);
|
|
const coils=16; for(let i=1;i<coils;i++){ const t=i/coils; const amp=(i%2?1:-1)*11; ctx.lineTo(ax+dx*t+nx*amp,ay+dy*t+ny*amp); } ctx.lineTo(bx,by); ctx.stroke();
|
|
} else if (this.type === "rod") {
|
|
ctx.strokeStyle = styleNight ? "rgba(176,166,150,0.96)" : "rgba(92,82,68,0.96)";
|
|
ctx.lineWidth = 7.2;
|
|
ctx.beginPath();
|
|
ctx.moveTo(ax, ay);
|
|
ctx.lineTo(bx, by);
|
|
ctx.stroke();
|
|
ctx.strokeStyle = styleNight ? "rgba(236,232,220,0.60)" : "rgba(255,248,228,0.56)";
|
|
ctx.lineWidth = 2.0;
|
|
ctx.beginPath();
|
|
ctx.moveTo(ax, ay);
|
|
ctx.lineTo(bx, by);
|
|
ctx.stroke();
|
|
} else {
|
|
const particles = Array.isArray(pb?.particles?.(this)) && pb.particles(this).length >= 2 ? pb.particles(this) : null;
|
|
ctx.strokeStyle = styleNight ? "rgba(180,150,116,0.88)" : "rgba(132,94,54,0.92)";
|
|
ctx.lineWidth = 4.2;
|
|
ctx.beginPath();
|
|
drawRopeRuntimePath(ctx, particles, this, pb, ax, ay, bx, by);
|
|
ctx.stroke();
|
|
ctx.strokeStyle = styleNight ? "rgba(236,214,180,0.56)" : "rgba(255,236,190,0.52)";
|
|
ctx.lineWidth = 1.5;
|
|
ctx.beginPath();
|
|
drawRopeRuntimePath(ctx, particles, this, pb, ax, ay, bx, by);
|
|
ctx.stroke();
|
|
}
|
|
ctx.fillStyle = this.type === "rod" ? (styleNight ? "rgba(120,112,98,0.96)" : "rgba(114,96,72,0.96)") : (this.type === "spring" ? "rgba(105,112,124,0.96)" : (this.type === "wire" || this.type === "insulated_wire" ? "rgba(70,80,92,0.96)" : (styleNight ? "rgba(112,84,54,0.95)" : "rgba(162,112,63,0.95)")));
|
|
for (const [kx, ky] of [[ax, ay], [bx, by]]) {
|
|
ctx.beginPath(); ctx.arc(kx, ky, (this.type === "rod" || this.type === "spring") ? 4.8 : 4.2, 0, Math.PI * 2); ctx.fill();
|
|
}
|
|
ctx.restore();
|
|
}
|
|
} else if (this.type === "zunchi") {
|
|
const zunchiId = this.zunchiVariant || "zunchi";
|
|
const img = getRenderableImage(zunchiId, "zunchi");
|
|
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;
|
|
if (img) {
|
|
const metrics = getImageMetrics(zunchiId) || getImageMetrics("zunchi");
|
|
const ratio = metrics?.ratio || 178 / 236;
|
|
ctx.drawImage(img, -this.r * 1.1, -this.r * 1.35, this.r * 2.2, this.r * 1.66 * ratio);
|
|
} else {
|
|
drawZunchiFallbackSprite(ctx, this.r, 1);
|
|
}
|
|
} 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();
|
|
}
|
|
});
|