"use strict"; class Item { constructor(type, x, y) { this.id = crypto.randomUUID ? crypto.randomUUID() : `${Date.now()}-${Math.random()}`; this.type = type; this.x = x; this.y = y; this.r = { food: 14, sweet: 13, love_mochi: 13, fight_mochi: 13, water: 12, grass: 17, stone: 20, bed: 37, ball: 18, firecracker: 15, fence_v: 42, fence_h: 42, trace: 16, splat: 26, zunchi: 14 }[type] || 12; this.amount = { food: 85, sweet: 62, love_mochi: 62, fight_mochi: 62, water: 64, grass: 120, stone: 999, bed: 999, ball: 999, firecracker: 999, fence_v: 999, fence_h: 999, trace: 220, splat: 260, zunchi: 240 }[type] || 80; this.age = 0; this.seed = Math.random() * 1000; this.dropTimer = 0; this.dropMax = 0; this.dropImpactDone = false; this.lifecycleTimer = rand(0, type === "grass" ? 1.15 : 0.55); this.lifecycleInterval = type === "grass" ? (1.75 + stableUnit(this.id, "grass-life") * 0.90) : (type === "zunchi" ? (0.95 + stableUnit(this.id, "zunchi-life") * 0.45) : 0); if (type === "grass") { this.growth = rand(0.18, 0.42); this.health = 1; this.seedTimer = rand(18, 38); this.eatenAmount = 0; this.fertilityBoost = 0; this.lifeSpan = rand(420, 860); this.wither = 0; } if (type === "bed") { this.comfort = rand(0.86, 1.14); this.wear = 0; } if (type === "ball") { this.vx = rand(-5, 5); this.vy = rand(-5, 5); this.prevX = x; this.prevY = y; this.spin = rand(0, Math.PI * 2); this.spinVelocity = rand(-1.6, 1.6); this.lastKickedAt = -999; this.lastKickerId = ""; this.lastPokedAt = -999; this.pokeCombo = 0; this.lastPokeAngle = rand(0, Math.PI * 2); } if (type === "zunchi") { this.stage = "fresh"; this.stageTimer = 0; this.fertility = 0; this.freshness = 1; this.zunchiVariant = stableUnit(this.id, "zunchi-variant") < 0.5 ? "zunchi" : "zunchi_02"; } if (type === "firecracker") { this.fuseTimer = 5; this.fuseMax = 5; } } update(dt, worldRef = world) { this.age += dt; if (this.dropTimer > 0) { const beforeDrop = this.dropTimer; this.dropTimer = Math.max(0, this.dropTimer - dt); if (beforeDrop > 0 && this.dropTimer <= 0 && !this.dropImpactDone) { this.dropImpactDone = true; if (worldRef.itemDropImpact) worldRef.itemDropImpact(this); } } if (this.type === "water") this.amount -= dt * 0.9; if (["sweet", "love_mochi", "fight_mochi"].includes(this.type)) this.amount -= dt * 0.12; if (this.type === "trace") this.amount -= dt * 1.35; if (this.type === "splat") this.amount -= dt * 1.05; if (this.type === "firecracker") { this.fuseTimer = Math.max(0, (this.fuseTimer ?? 5) - dt); if (this.fuseTimer <= 0) { if (worldRef.explodeFirecracker) worldRef.explodeFirecracker(this); else this.amount = 0; } } if (this.type === "ball") this.updateBall(dt, worldRef); if (this.type === "grass") { this.lifecycleTimer += dt; const factor = worldRef.grassUpdateFactor ? worldRef.grassUpdateFactor() : 1; const interval = (this.lifecycleInterval || 1.9) * factor; if (this.lifecycleTimer >= interval) { const lifecycleDt = Math.min(this.lifecycleTimer, 4.8); this.lifecycleTimer = 0; this.updateGrass(lifecycleDt, worldRef); } } else if (this.type === "zunchi") { this.lifecycleTimer += dt; const interval = this.lifecycleInterval || (0.95 + stableUnit(this.id, "zunchi-life") * 0.45); if (this.lifecycleTimer >= interval) { const lifecycleDt = Math.min(this.lifecycleTimer, 2.4); this.lifecycleTimer = 0; this.updateZunchi(lifecycleDt, worldRef); } } } updateBall(dt, worldRef) { this.prevX = this.x; this.prevY = this.y; const maxSpeed = 1250; const clampVelocity = () => { const speed = Math.hypot(this.vx || 0, this.vy || 0); if (speed > maxSpeed) { this.vx = (this.vx || 0) / speed * maxSpeed; this.vy = (this.vy || 0) / speed * maxSpeed; } return Math.hypot(this.vx || 0, this.vy || 0); }; const moving = clampVelocity(); if (moving > 0.04) { this.x += (this.vx || 0) * dt; this.y += (this.vy || 0) * dt; const p = Math.max(28, CONFIG.worldPadding || 30); if (this.x < p) { this.x = p; this.vx = Math.abs(this.vx || 0) * 0.72; this.spinVelocity *= -0.72; } if (this.x > worldRef.w - p) { this.x = worldRef.w - p; this.vx = -Math.abs(this.vx || 0) * 0.72; this.spinVelocity *= -0.72; } if (this.y < p) { this.y = p; this.vy = Math.abs(this.vy || 0) * 0.72; this.spinVelocity *= -0.72; } if (this.y > worldRef.h - p) { this.y = worldRef.h - p; this.vy = -Math.abs(this.vy || 0) * 0.72; this.spinVelocity *= -0.72; } this.resolveBallObstacleCollisions(dt, worldRef); clampVelocity(); const groundFriction = Math.pow(0.72, dt); this.vx *= groundFriction; this.vy *= groundFriction; } this.spin = (this.spin || 0) + (this.spinVelocity || 0) * dt + (this.vx || 0) * dt / Math.max(8, this.r || 18); this.spinVelocity *= Math.pow(0.65, dt); if (Math.hypot(this.vx || 0, this.vy || 0) < 0.18) { this.vx = 0; this.vy = 0; } } resolveBallObstacleCollisions(dt, worldRef) { if (!worldRef?.nearbyItems) return; const speed = Math.hypot(this.vx || 0, this.vy || 0); const searchRadius = Math.max(130, (this.r || 18) + speed * Math.max(dt || 0.016, 0.016) + 96); const items = worldRef.nearbyItems(this.x, this.y, searchRadius) || []; for (const it of items) { if (!it || it === this || it.dead) continue; if (it.type === "bed") { this.applyBallHayDrag(it, dt, worldRef); } else if (it.type === "stone") { this.resolveBallCircleBounce(it, (it.r || 20) * 1.08 + (this.r || 18), 0.82, worldRef); } else if (it.type === "fence_v" || it.type === "fence_h") { this.resolveBallFenceBounce(it, worldRef); } } } applyBallHayDrag(bed, dt, worldRef) { const rx = Math.max(26, (bed.r || 37) * 1.72 + (this.r || 18) * 0.40); const ry = Math.max(18, (bed.r || 37) * 0.92 + (this.r || 18) * 0.34); const dx = this.x - bed.x; const dy = this.y - bed.y; const inside = (dx * dx) / (rx * rx) + (dy * dy) / (ry * ry) <= 1; if (!inside) return; const slow = Math.pow(0.16, Math.max(0.016, dt || 0.016)); this.vx *= slow; this.vy *= slow; this.spinVelocity *= Math.pow(0.24, Math.max(0.016, dt || 0.016)); if ((this.lastHaySlowLogAt || -999) + 3.5 < (worldRef.time || 0) && Math.hypot(this.vx || 0, this.vy || 0) > 120) { this.lastHaySlowLogAt = worldRef.time || 0; worldRef.effects?.push(new Effect("ring", this.x, this.y, { size: Math.max(14, this.r * 0.95), life: 0.20, color: "rgba(214,184,96,0.42)" })); } } resolveBallCircleBounce(obstacle, hitRadius, restitution, worldRef) { let dx = this.x - obstacle.x; let dy = this.y - obstacle.y; let d = Math.hypot(dx, dy); if (d >= hitRadius) return; if (d < 0.001) { const speed = Math.hypot(this.vx || 0, this.vy || 0); if (speed > 0.001) { dx = -(this.vx || 0) / speed; dy = -(this.vy || 0) / speed; d = 1; } else { dx = Math.cos(this.seed || 0); dy = Math.sin(this.seed || 0); d = 1; } } const nx = dx / d; const ny = dy / d; const toward = (this.vx || 0) * nx + (this.vy || 0) * ny; this.x = obstacle.x + nx * (hitRadius + 0.5); this.y = obstacle.y + ny * (hitRadius + 0.5); if (toward < 0) { this.vx = (this.vx || 0) - (1 + restitution) * toward * nx; this.vy = (this.vy || 0) - (1 + restitution) * toward * ny; } else { this.vx = (this.vx || 0) + nx * 24; this.vy = (this.vy || 0) + ny * 24; } this.vx *= 0.96; this.vy *= 0.96; this.spinVelocity = clamp((this.spinVelocity || 0) + (nx >= 0 ? -1 : 1) * 5.5, -38, 38); this.emitBallBounce(worldRef, obstacle); } resolveBallFenceBounce(fence, worldRef) { const vertical = fence.type === "fence_v"; const len = Math.max(112, (fence.r || 42) * 3.55); const thick = Math.max(10, (fence.r || 42) * 0.31); const halfLen = len / 2; const sx1 = vertical ? fence.x : fence.x - halfLen; const sy1 = vertical ? fence.y - halfLen : fence.y; const sx2 = vertical ? fence.x : fence.x + halfLen; const sy2 = vertical ? fence.y + halfLen : fence.y; const vx = sx2 - sx1; const vy = sy2 - sy1; const segLenSq = vx * vx + vy * vy || 1; const u = clamp(((this.x - sx1) * vx + (this.y - sy1) * vy) / segLenSq, 0, 1); const cx = sx1 + vx * u; const cy = sy1 + vy * u; const hitRadius = (this.r || 18) + thick / 2 + 2; let dx = this.x - cx; let dy = this.y - cy; let d = Math.hypot(dx, dy); if (d >= hitRadius) return; if (d < 0.001) { if (vertical) { dx = (this.vx || 0) >= 0 ? -1 : 1; dy = 0; } else { dx = 0; dy = (this.vy || 0) >= 0 ? -1 : 1; } d = 1; } const nx = dx / d; const ny = dy / d; const toward = (this.vx || 0) * nx + (this.vy || 0) * ny; this.x = cx + nx * (hitRadius + 0.5); this.y = cy + ny * (hitRadius + 0.5); if (toward < 0) { this.vx = (this.vx || 0) - 1.78 * toward * nx; this.vy = (this.vy || 0) - 1.78 * toward * ny; } this.vx *= 0.94; this.vy *= 0.94; this.spinVelocity = clamp((this.spinVelocity || 0) + (nx >= 0 ? -1 : 1) * 6.2, -38, 38); this.emitBallBounce(worldRef, fence); } emitBallBounce(worldRef, obstacle) { const now = worldRef?.time || 0; if ((this.lastObstacleBounceAt || -999) + 0.08 > now) return; this.lastObstacleBounceAt = now; worldRef?.effects?.push(new Effect("ring", this.x, this.y, { size: Math.max(13, (this.r || 18) * 0.82), life: 0.18, color: obstacle?.type === "stone" ? "rgba(165,165,150,0.45)" : "rgba(160,116,64,0.42)" })); } updateGrass(dt, worldRef) { const light = worldRef.lightLevel ? worldRef.lightLevel() : 0.7; const weather = worldRef.weather || "sunny"; const grassLoad = worldRef.grassLoadLevel ? worldRef.grassLoadLevel() : 0; if (worldRef.grassBlockedAt && worldRef.grassBlockedAt(this.x, this.y, this)) { this.growth = Math.max(0, (this.growth ?? 0.25) - dt * 0.28); this.amount -= dt * 34; return; } let fertility = 0; let fertileSource = null; let bestFertility = 0; let waterNear = weather === "light_rain" ? 0.35 : 0; const nearbyRadius = grassLoad >= 3 ? 190 : grassLoad >= 2 ? 230 : grassLoad >= 1 ? 290 : 360; const nearby = worldRef.nearbyItems ? worldRef.nearbyItems(this.x, this.y, nearbyRadius) : (worldRef.items || []); let scanned = 0; const scanLimit = grassLoad >= 3 ? 18 : grassLoad >= 2 ? 26 : grassLoad >= 1 ? 40 : 9999; for (const it of nearby) { if (++scanned > scanLimit) break; if (it === this) continue; const d = distXY(this.x, this.y, it.x, it.y); if ((it.type === "trace" || it.type === "splat") && d < 170) { const influence = clamp(1 - d / 170, 0, 1) * (it.type === "splat" ? 0.92 : 0.55); fertility += influence; if (influence > bestFertility) { bestFertility = influence; fertileSource = it; } } if (it.type === "zunchi" && (it.stage === "decomposing" || it.stage === "fertile_soil")) { const influence = clamp(1 - d / nearbyRadius, 0, 1) * (it.stage === "fertile_soil" ? 1.25 : 0.72); fertility += influence; if (influence > bestFertility) { bestFertility = influence; fertileSource = it; } } if (it.type === "water") waterNear += clamp(1 - d / 90, 0, 1) * 0.25; } this.fertilityBoost = clamp(fertility, 0, 1.4); const weatherBoost = weather === "sunny" ? 0.09 : weather === "light_rain" ? 0.42 : 0.08; const dryStress = weather === "sunny" && light > 0.78 && waterNear < 0.04 && this.fertilityBoost < 0.12 ? 0.012 : 0; const growthRate = (0.006 + light * 0.004 + weatherBoost * 0.01 + waterNear * 0.014 + this.fertilityBoost * 0.014) * dt * 0.5; this.growth = clamp((this.growth ?? 0.25) + growthRate - dryStress * dt, 0, 0.88); this.health = clamp((this.health ?? 1) + dt * (waterNear > 0.05 || weather === "light_rain" ? 0.017 : -0.0015 - dryStress * 0.28), 0.18, 1); const grassLimit = worldRef.grassLimit ? worldRef.grassLimit() : (CONFIG.grassLimit ?? 96); if (this.growth >= 0.86 && (worldRef.itemCounts?.grass || 0) < grassLimit) { const spawned = this.seedAround(worldRef, fertileSource, this.fertilityBoost > 0.25, 2); if (spawned > 0) this.growth = rand(0.52, 0.68); this.seedTimer = rand(18, 34); } if (this.age > (this.lifeSpan || 640) * 0.82) { const fade = clamp((this.age - (this.lifeSpan || 640) * 0.82) / Math.max((this.lifeSpan || 640) * 0.18, 1), 0, 1); this.wither = fade; this.health = clamp((this.health ?? 1) - dt * (0.004 + fade * 0.020), 0, 1); this.growth = clamp((this.growth ?? 0.25) - dt * fade * 0.005, 0, 0.88); } else { this.wither = Math.max(0, (this.wither || 0) - dt * 0.08); } this.amount = clamp(this.growth * 120 * this.health, 0, 130); if (this.age > (this.lifeSpan || 640) * 1.05) this.amount -= dt * (1.2 + (this.wither || 0) * 24); this.seedTimer -= dt * (this.growth > 0.72 ? 1 : 0.25) * 0.5; if (this.seedTimer <= 0 && this.growth > 0.64 && (worldRef.itemCounts?.grass || 0) < grassLimit) { this.seedTimer = rand(24, 54); if (Math.random() < 0.42 + this.fertilityBoost * 0.18) this.seedAround(worldRef, fertileSource, this.fertilityBoost > 0.25, 1); } } seedAround(worldRef, fertileSource = null, fertile = false, maxNew = 1) { let made = 0; const limit = worldRef.grassLimit ? worldRef.grassLimit() : (CONFIG.grassLimit ?? 96); for (let n = 0; n < maxNew && (worldRef.itemCounts?.grass || 0) < limit; n++) { const source = fertile && fertileSource ? fertileSource : this; const spot = worldRef.findGrassSproutSpot ? worldRef.findGrassSproutSpot(source, Boolean(fertile && fertileSource)) : { x: clamp(this.x + rand(-58, 58), 44, worldRef.w - 44), y: clamp(this.y + rand(-42, 42), 44, worldRef.h - 44) }; if (!spot) break; const sprout = new Item("grass", spot.x, spot.y); sprout.growth = rand(0.08, 0.18); sprout.amount = sprout.growth * 120; worldRef.items.push(sprout); if (worldRef.itemCounts) worldRef.itemCounts.grass = (worldRef.itemCounts.grass || 0) + 1; made += 1; } return made; } updateZunchi(dt, worldRef) { const rainy = worldRef.weather === "light_rain"; const rainBoost = rainy ? 1.95 : 1; const decayBoost = rainy ? 2.15 : 1; let waterBoost = 0; const nearby = worldRef.nearbyItems ? worldRef.nearbyItems(this.x, this.y, 84) : (worldRef.items || []); for (const it of nearby) { if (it.type !== "water") continue; waterBoost += clamp(1 - distXY(this.x, this.y, it.x, it.y) / 80, 0, 1); } this.stageTimer += dt * 2.0 * (rainBoost + waterBoost * 1.3); if (this.stage === "fresh") { this.freshness = clamp(1 - this.stageTimer / 110, 0, 1); this.amount -= dt * 0.05 * decayBoost; if (this.stageTimer > 110) { this.stage = "dry"; this.stageTimer = 0; } } else if (this.stage === "dry") { this.freshness = clamp(0.55 - this.stageTimer / 220, 0.20, 0.55); this.amount -= dt * 0.09 * decayBoost; if (this.stageTimer > 150) { this.stage = "decomposing"; this.stageTimer = 0; this.fertility = 0.35; } } else if (this.stage === "decomposing") { this.fertility = clamp(this.fertility + dt * 0.015, 0, 1); this.amount -= dt * 0.12 * decayBoost; if (this.stageTimer > 190) { this.stage = "fertile_soil"; this.stageTimer = 0; this.amount = Math.min(this.amount, 140); } } else if (this.stage === "fertile_soil") { this.fertility = clamp(1 - this.stageTimer / 360, 0, 1); this.amount -= dt * 0.26 * decayBoost; } } get dead() { return this.amount <= 0; } serialize() { return { type: this.type, x: this.x, y: this.y, amount: this.amount, age: this.age, seed: this.seed, stage: this.stage, stageTimer: this.stageTimer, fertility: this.fertility, freshness: this.freshness, growth: this.growth, health: this.health, seedTimer: this.seedTimer, eatenAmount: this.eatenAmount, fertilityBoost: this.fertilityBoost, lifeSpan: this.lifeSpan, wither: this.wither, comfort: this.comfort, wear: this.wear, crawlOpen: this.crawlOpen, zunchiVariant: this.zunchiVariant, toolSize: this.toolSize, blastScale: this.blastScale, fuseTimer: this.fuseTimer, fuseMax: this.fuseMax, dropTimer: this.dropTimer, dropMax: this.dropMax, vx: this.vx, vy: this.vy, prevX: this.prevX, prevY: this.prevY, spin: this.spin, spinVelocity: this.spinVelocity, lastKickedAt: this.lastKickedAt, lastKickerId: this.lastKickerId, lastPokedAt: this.lastPokedAt, pokeCombo: this.pokeCombo, lastPokeAngle: this.lastPokeAngle }; } static from(o) { const type = o?.type || "grass"; const it = Object.create(Item.prototype); Item._loadId = (Item._loadId || 0) + 1; it.id = o.id || `loaded-${Item._loadId}`; it.type = type; it.x = o.x; it.y = o.y; it.r = { food: 14, sweet: 13, love_mochi: 13, fight_mochi: 13, water: 12, grass: 17, stone: 20, bed: 37, ball: 18, firecracker: 15, fence_v: 42, fence_h: 42, trace: 16, splat: 26, zunchi: 14 }[type] || 12; it.amount = o.amount ?? ({ food: 85, sweet: 62, love_mochi: 62, fight_mochi: 62, water: 64, grass: 120, stone: 999, bed: 999, ball: 999, firecracker: 999, fence_v: 999, fence_h: 999, trace: 220, splat: 260, zunchi: 240 }[type] || 80); it.age = o.age || 0; it.seed = o.seed || Math.random() * 1000; it.dropTimer = 0; it.dropMax = 0; it.dropImpactDone = false; it.lifecycleTimer = o.lifecycleTimer ?? rand(0, type === "grass" ? 1.15 : 0.55); it.lifecycleInterval = o.lifecycleInterval ?? (type === "grass" ? (1.75 + stableUnit(it.id, "grass-life") * 0.90) : (type === "zunchi" ? (0.95 + stableUnit(it.id, "zunchi-life") * 0.45) : 0)); if (type === "grass") { it.growth = 0.25; it.health = 1; it.seedTimer = rand(18, 38); it.eatenAmount = 0; it.fertilityBoost = 0; } if (type === "bed") { it.comfort = 1; it.wear = 0; } if (type === "ball") { it.vx = o.vx || 0; it.vy = o.vy || 0; it.prevX = o.prevX ?? o.x; it.prevY = o.prevY ?? o.y; it.spin = o.spin || 0; it.spinVelocity = o.spinVelocity || 0; it.lastKickedAt = o.lastKickedAt ?? -999; it.lastKickerId = o.lastKickerId || ""; it.lastPokedAt = o.lastPokedAt ?? -999; it.pokeCombo = o.pokeCombo || 0; it.lastPokeAngle = o.lastPokeAngle || rand(0, Math.PI * 2); } if (type === "zunchi") { it.stage = "fresh"; it.stageTimer = 0; it.fertility = 0; it.freshness = 1; it.zunchiVariant = "zunchi"; } if (type === "firecracker") { it.fuseTimer = 5; it.fuseMax = 5; } it.stage = o.stage || it.stage; it.stageTimer = o.stageTimer || 0; it.fertility = o.fertility || 0; it.freshness = o.freshness ?? it.freshness; it.growth = o.growth ?? it.growth; it.health = o.health ?? it.health; it.seedTimer = o.seedTimer ?? it.seedTimer; it.eatenAmount = o.eatenAmount || 0; it.fertilityBoost = o.fertilityBoost || 0; it.lifeSpan = o.lifeSpan ?? it.lifeSpan; it.wither = o.wither ?? it.wither ?? 0; it.comfort = o.comfort ?? it.comfort; it.wear = o.wear ?? it.wear; it.crawlOpen = o.crawlOpen ?? it.crawlOpen; it.vx = o.vx ?? it.vx ?? 0; it.vy = o.vy ?? it.vy ?? 0; it.prevX = o.prevX ?? it.prevX ?? it.x; it.prevY = o.prevY ?? it.prevY ?? it.y; it.spin = o.spin ?? it.spin ?? 0; it.spinVelocity = o.spinVelocity ?? it.spinVelocity ?? 0; it.lastKickedAt = o.lastKickedAt ?? it.lastKickedAt ?? -999; it.lastKickerId = o.lastKickerId || it.lastKickerId || ""; it.lastPokedAt = o.lastPokedAt ?? it.lastPokedAt ?? -999; it.pokeCombo = o.pokeCombo ?? it.pokeCombo ?? 0; it.lastPokeAngle = o.lastPokeAngle ?? it.lastPokeAngle ?? rand(0, Math.PI * 2); it.zunchiVariant = o.zunchiVariant || it.zunchiVariant || "zunchi"; it.toolSize = o.toolSize || it.toolSize || "medium"; it.blastScale = o.blastScale || it.blastScale || 1; it.fuseTimer = o.fuseTimer ?? it.fuseTimer; it.fuseMax = o.fuseMax ?? it.fuseMax; it.dropTimer = 0; it.dropMax = 0; it.dropImpactDone = true; return it; } 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); const visibleAlpha = clamp(this.amount / 40, 0.24, 1); const dropT = this.dropMax > 0 ? clamp(this.dropTimer / this.dropMax, 0, 1) : 0; const dropY = dropT > 0 ? -170 * Math.pow(dropT, 0.42) : 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 ? (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 (this.type === "food" || this.type === "sweet" || this.type === "love_mochi" || this.type === "fight_mochi") { const sweet = this.type === "sweet"; const loveMochi = this.type === "love_mochi"; const fightMochi = this.type === "fight_mochi"; ctx.fillStyle = loveMochi ? "#fff1f6" : (fightMochi ? "#fff3ec" : (sweet ? "#fffaf1" : "#f1dfb7")); ctx.strokeStyle = loveMochi ? "#bf6f8b" : (fightMochi ? "#bc6d42" : (sweet ? "#9a8a72" : "#8d6f48")); ctx.lineWidth = 2; ctx.beginPath(); const bob = 0; ctx.ellipse(0, bob, this.r * 1.25, this.r * 0.75, Math.sin(this.seed) * 0.4, 0, Math.PI * 2); ctx.fill(); ctx.stroke(); if (sweet) { ctx.beginPath(); ctx.fillStyle = "#76b94d"; ctx.ellipse(-1, -this.r * 0.24, this.r * 0.76, this.r * 0.26, Math.sin(this.seed) * 0.25, 0, Math.PI * 2); ctx.fill(); ctx.fillStyle = "rgba(255,255,255,0.62)"; ctx.beginPath(); ctx.ellipse(-this.r * 0.32, -this.r * 0.16, this.r * 0.24, this.r * 0.10, -0.35, 0, Math.PI * 2); ctx.fill(); } else if (loveMochi) { ctx.fillStyle = "#ff6a9c"; ctx.font = `${Math.round((this.r || 13) * 1.15)}px sans-serif`; ctx.textAlign = "center"; ctx.textBaseline = "middle"; ctx.fillText("❤", 0, 1); ctx.fillStyle = "rgba(255,255,255,0.55)"; ctx.beginPath(); ctx.ellipse(-this.r * 0.28, -this.r * 0.18, this.r * 0.23, this.r * 0.10, -0.35, 0, Math.PI * 2); ctx.fill(); } else if (fightMochi) { ctx.fillStyle = "#d55632"; ctx.font = `${Math.round((this.r || 13) * 1.02)}px sans-serif`; ctx.textAlign = "center"; ctx.textBaseline = "middle"; ctx.fillText("✦", 0, 1); ctx.strokeStyle = "rgba(168,76,42,0.72)"; ctx.lineWidth = 1.4; ctx.beginPath(); ctx.moveTo(-this.r * 0.44, -this.r * 0.42); ctx.lineTo(this.r * 0.44, this.r * 0.42); ctx.moveTo(-this.r * 0.44, this.r * 0.42); ctx.lineTo(this.r * 0.44, -this.r * 0.42); ctx.stroke(); } else { ctx.fillStyle = "#fff7dc"; for (let i = 0; i < 5; i++) { ctx.beginPath(); ctx.arc(randSeed(this.seed + i, -8, 8), randSeed(this.seed + i + 7, -5, 5), 1.6, 0, Math.PI * 2); ctx.fill(); } } } else if (this.type === "water") { ctx.fillStyle = "rgba(86, 157, 224, 0.50)"; ctx.strokeStyle = "rgba(62, 113, 178, 0.42)"; ctx.lineWidth = 2; ctx.beginPath(); ctx.ellipse(0, Math.sin(t * 1.7 + this.seed) * 1.5, this.r * 1.05, this.r * 0.8, 0, 0, Math.PI * 2); ctx.fill(); ctx.stroke(); ctx.fillStyle = "rgba(255,255,255,0.55)"; ctx.beginPath(); ctx.arc(-4, -4, 3, 0, Math.PI * 2); ctx.fill(); } else if (this.type === "grass") { const growth = this.growth ?? clamp(this.amount / 120, 0, 1); const stage = growth < 0.22 ? "sprout" : growth < 0.48 ? "young" : "mature"; const eaten = this.eatenAmount > 14 || growth < 0.16; const grassLoad = 1; 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 = grassLoad >= 2 ? (styleNight ? 1.3 : 1.5) : (styleNight ? 1.7 : 2); const matureBlades = grassLoad >= 3 ? 2 : grassLoad >= 2 ? 3 : grassLoad >= 1 ? 5 : 7; const bladeCount = eaten ? 2 : (stage === "sprout" ? 2 : stage === "young" ? (grassLoad >= 2 ? 2 : 3) : matureBlades); for (let i = 0; i < bladeCount; i++) { const spread = stage === "mature" ? (grassLoad >= 2 ? 0.92 : 1.18) : 0.70; const a = -Math.PI / 2 + randSeed(this.seed + i, -spread, spread); const len = this.r * randSeed(this.seed + i + 10, 0.45, stage === "mature" ? (grassLoad >= 2 ? 1.08 : 1.30) : 1.05) * clamp(growth, 0.18, 1.00) * (eaten ? 0.42 : 1); const rootX = stage === "mature" ? randSeed(this.seed + i + 31, -this.r * (grassLoad >= 2 ? 0.24 : 0.32), this.r * (grassLoad >= 2 ? 0.24 : 0.32)) : 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 (stage === "mature" && !eaten && grassLoad <= 1) { ctx.save(); ctx.globalAlpha = 0.16; ctx.fillStyle = styleNight ? "rgba(95,132,114,0.65)" : "rgba(138,189,108,0.72)"; ctx.beginPath(); ctx.ellipse(0, 1, this.r * 0.72, this.r * 0.22, 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 === "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("⚽", 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(); } }