"use strict"; (function (global) { const World = global.World; if (!World) throw new Error("World is not available for mixin: world_view.js"); const BASE_ZOOM_MIN = 0.42; const BASE_ZOOM_MAX = 2.45; function terrainSignature(worldRef) { const counts = worldRef.itemCounts || {}; return `${worldRef.fieldType || "garden"}:${worldRef.groundType || "soil"}:${counts.grass || 0}:${counts.trace || 0}:${counts.splat || 0}:${counts.zunchi || 0}:${worldRef.weather || "sunny"}`; } function maxCameraX(worldRef) { return Math.max(0, worldRef.w - worldRef.visibleWorldW()); } function maxCameraY(worldRef) { return Math.max(0, worldRef.h - worldRef.visibleWorldH()); } function fieldWorldScale(worldRef) { const field = worldRef.fieldDefinition(); return Math.max(0.22, field.worldScale || 1); } Object.defineProperties(World.prototype, Object.getOwnPropertyDescriptors({ dayProgress() { return (this.time % CONFIG.dayLength) / CONFIG.dayLength; }, lightLevel() { return (Math.sin(this.dayProgress() * Math.PI * 2 - Math.PI / 2) + 1) / 2; }, triggerGroundShake(duration = 0.4, strength = 6) { this.shakeTimer = Math.max(this.shakeTimer || 0, duration); this.shakeDuration = Math.max(this.shakeDuration || 0, duration); this.shakeStrength = Math.max(this.shakeStrength || 0, strength); }, currentShakeOffset() { const timeLeft = Math.max(0, this.shakeTimer || 0); if (timeLeft <= 0) return { x: 0, y: 0 }; const duration = Math.max(0.001, this.shakeDuration || timeLeft); const strength = Math.max(0, this.shakeStrength || 0); const fade = clamp(timeLeft / duration, 0.18, 1); const t = this.time || 0; return { x: Math.sin(t * 48) * strength * 0.34 * fade + Math.sin(t * 91) * strength * 0.12 * fade, y: Math.cos(t * 60) * strength * 0.18 * fade, }; }, emit(type, detail = {}) { const events = this.events; if (!events?.emit) return null; if (typeof events.hasListeners === "function" && !events.hasListeners(type)) return null; return events.emit(type, { ...detail, world: this }) || null; }, markSpatialDirty(reason = "manual") { this.spatialDirty = true; this.spatialDirtyReason = reason; if (!this.spatialDirtyReasonCountsThisFrame) this.spatialDirtyReasonCountsThisFrame = {}; this.spatialDirtyReasonCountsThisFrame[String(reason || "manual")] = (this.spatialDirtyReasonCountsThisFrame[String(reason || "manual")] || 0) + 1; this.spatialDirtyMarksThisFrame = (this.spatialDirtyMarksThisFrame || 0) + 1; this.spatialDirtyMarksTotal = (this.spatialDirtyMarksTotal || 0) + 1; this.lastSpatialDirtyReason = reason; const r = String(reason || "manual"); if (/tarinai-moved|post-tarinai|creature/i.test(r)) { this.spatialTarinaiDirty = true; } else if (/ants?-moved|post-ant|ant/i.test(r) && !/ant_nest|ant-nest/i.test(r)) { this.spatialAntDirty = true; } else if (/items?-moved|ball-ball|mobile|kinematic|pushpin|oshibyo|firecracker|genkotsu|zunchi|mechanical|rotator|reciprocator|poison-block|constraint|rod-/i.test(r)) { this.spatialDynamicItemsDirty = true; } else { this.spatialStaticItemsDirty = true; this.spatialDynamicItemsDirty = true; this.spatialTarinaiDirty = true; this.spatialAntDirty = true; } }, markTerrainDirty(reason = "manual") { const reasonKey = String(reason || "manual"); if (!this.terrainDirtyStatsThisFrame) this.terrainDirtyStatsThisFrame = { raw: 0, global: 0, chunk: 0, coalesced: 0, reasons: {} }; this.terrainDirtyStatsThisFrame.raw += 1; this.terrainDirtyStatsThisFrame.global += 1; this.terrainDirtyStatsThisFrame.reasons[reasonKey] = (this.terrainDirtyStatsThisFrame.reasons[reasonKey] || 0) + 1; if (this.terrainDirtyGlobal && this.terrainDirtyReason === reasonKey) { this.terrainDirtyStatsThisFrame.coalesced += 1; this.terrainDirty = true; this.terrainLastDirtyAt = this.time || 0; return; } this.terrainDirty = true; this.terrainDirtyGlobal = true; this.terrainDirtyReason = reasonKey; this.terrainVersion = (this.terrainVersion || 0) + 1; this.terrainLastDirtyAt = this.time || 0; }, markTerrainDirtyAt(x, y, radius = 48, reason = "manual") { const reasonKey = String(reason || "manual"); const cx = Math.floor((Number(x) || 0) / 256); const cy = Math.floor((Number(y) || 0) / 256); const cr = Math.max(0, Math.ceil((Number(radius) || 0) / 256)); if (!this.terrainDirtyStatsThisFrame) this.terrainDirtyStatsThisFrame = { raw: 0, global: 0, chunk: 0, coalesced: 0, reasons: {} }; this.terrainDirtyStatsThisFrame.raw += 1; this.terrainDirtyStatsThisFrame.chunk += 1; this.terrainDirtyStatsThisFrame.reasons[reasonKey] = (this.terrainDirtyStatsThisFrame.reasons[reasonKey] || 0) + 1; if (!this.terrainDirtyChunks) this.terrainDirtyChunks = new Set(); let added = 0; for (let yy = cy - cr; yy <= cy + cr; yy++) { for (let xx = cx - cr; xx <= cx + cr; xx++) { const key = `${xx}:${yy}`; if (!this.terrainDirtyChunks.has(key)) added += 1; this.terrainDirtyChunks.add(key); } } this.terrainDirty = true; this.terrainDirtyReason = reasonKey; this.terrainLastDirtyAt = this.time || 0; if (added <= 0) { this.terrainDirtyStatsThisFrame.coalesced += 1; return true; } this.terrainVersion = (this.terrainVersion || 0) + 1; return true; }, updateTerrainDirtyState(dt = 0) { const signature = terrainSignature(this); if (signature !== this.lastTerrainSignature) { this.lastTerrainSignature = signature; this.markTerrainDirty("terrain-signature"); return; } const interval = 3.0; if ((this.time || 0) - (this.terrainLastDirtyAt || 0) >= interval) this.markTerrainDirty("terrain-periodic"); }, phaseName() { const hour = this.dayProgress() * 24; if (hour >= 5 && hour < 10) return "\u671d"; if (hour >= 10 && hour < 17) return "\u663c"; if (hour >= 17 && hour < 20) return "\u5915\u65b9"; return "\u591c"; }, clockString() { const totalMinutes = Math.floor(this.dayProgress() * 24 * 60); const hh = String(Math.floor(totalMinutes / 60)).padStart(2, "0"); const mm = String(totalMinutes % 60).padStart(2, "0"); return `${hh}:${mm}`; }, fieldDefinition() { return FIELD_TYPES?.[this.fieldType] || FIELD_TYPES.garden; }, groundDefinition() { return window.TarinaiGround.definition(this.groundType || "soil"); }, groundLabel() { return window.TarinaiGround.label(this.groundType || "soil"); }, groundStressMultiplier() { return window.TarinaiGround.stressMultiplier(this.groundType || "soil"); }, groundGrassMultiplier() { return window.TarinaiGround.grassMultiplier(this.groundType || "soil"); }, setGroundType(id = "soil", opts = {}) { const next = window.TarinaiGround.exists(id) ? id : "soil"; const previous = this.groundType || "soil"; this.groundType = next; if (next !== previous || opts.force) { window.TarinaiWeatherSystem.enforceFixedWeather(this); this.markTerrainDirty?.("ground-type"); this.enforceGrassLimit?.("ground-type"); this.updateItemCounts?.("ground-type"); this.drawListDirty = true; window.TarinaiRender.invalidateRenderCaches("ground-type"); window.TarinaiGroundUI.update(this); if (window.uiCache) window.uiCache.selectedSnapshot = ""; renderSelected(); if (typeof document !== "undefined") { const groundTextEl = document.getElementById("statGroundType"); if (groundTextEl) groundTextEl.textContent = this.groundLabel?.() || "\u5730\u9762"; } if (!opts.silent) { const label = this.groundLabel?.() || "\u5730\u9762"; this.log?.(`\u5730\u9762: ${label}`); showToast(`\u5730\u9762\u3092 ${label} \u306b\u3057\u307e\u3057\u305f\u3002`); } } return this.groundDefinition?.(); }, cycleGroundType() { return this.setGroundType(window.TarinaiGround.nextId(this.groundType || "soil")); }, isFenceType(type = "") { return isFenceItemType(type); }, toolAngleFor(type = "") { const key = String(type || ""); const fallback = defaultItemAngle(key); const value = this.toolAngles?.[key]; return normalizedItemAngle(value, fallback); }, setToolAngle(type = "", angle = 0, { silent = false } = {}) { const key = String(type || ""); if (!key || !isRotatableItemType(key)) return false; if (!this.toolAngles) this.toolAngles = {}; const fallback = defaultItemAngle(key); const next = normalizedItemAngle(angle, fallback); this.toolAngles[key] = next; this.drawListDirty = true; if (!silent) showToast(`\u5411\u304d: ${Math.round(next * 180 / Math.PI)}\u00b0`); return true; }, rotateToolAngle(type = "", delta = 0, options = {}) { const key = String(type || ""); const current = this.toolAngleFor(key); return this.setToolAngle?.(key, current + Number(delta || 0), options) || false; }, fitFieldZoom() { const vw = Math.max(1, this.viewportW || this.w || 1000); const vh = Math.max(1, this.viewportH || this.h || 720); const ww = Math.max(1, this.w || vw); const wh = Math.max(1, this.h || vh); // The minimum visible zoom is field-size dependent. Small fields must not // be allowed to shrink below the viewport, otherwise empty margins appear. return Math.max(vw / ww, vh / wh); }, zoomLimits() { const min = Math.max(BASE_ZOOM_MIN, this.fitFieldZoom()); // Keep the old upper bound for normal/large fields, but give small fields // proportional zoom-in headroom because their no-margin minimum is higher. const max = Math.max(BASE_ZOOM_MAX, min * BASE_ZOOM_MAX); return { min, max }; }, normalizedFieldZoom(value = this.fieldZoom) { const limits = this.zoomLimits(); return clamp(Number(value) || limits.min, limits.min, limits.max); }, viewScale() { return this.normalizedFieldZoom(this.fieldZoom); }, visibleWorldW() { return (this.viewportW || this.w) / this.viewScale(); }, visibleWorldH() { return (this.viewportH || this.h) / this.viewScale(); }, screenSizeToWorld(size) { // Hit tests use logical world units here, so this adapter intentionally // returns the supplied size unchanged. return size; }, fieldScreenOffset() { const scale = this.viewScale(); return { x: Math.max(0, ((this.viewportW || this.w) - this.w * scale) / 2), y: Math.max(0, ((this.viewportH || this.h) - this.h * scale) / 2), }; }, clampCamera() { this.cameraX = clamp(this.cameraX || 0, 0, maxCameraX(this)); this.cameraY = clamp(this.cameraY || 0, 0, maxCameraY(this)); }, panCamera(dx, dy) { const scale = this.viewScale(); this.cameraX = (this.cameraX || 0) + dx / scale; this.cameraY = (this.cameraY || 0) + dy / scale; this.clampCamera(); }, screenToWorld(x, y) { const scale = this.viewScale(); const off = this.fieldScreenOffset(); const rawX = (x - off.x) / scale + (this.cameraX || 0); const rawY = (y - off.y) / scale + (this.cameraY || 0); return { x: clamp(rawX, 0, this.w), y: clamp(rawY, 0, this.h), inside: rawX >= 0 && rawY >= 0 && rawX <= this.w && rawY <= this.h, }; }, worldToScreen(x, y) { const scale = this.viewScale(); const off = this.fieldScreenOffset(); return { x: (x - (this.cameraX || 0)) * scale + off.x, y: (y - (this.cameraY || 0)) * scale + off.y, }; }, setViewportSize(width, height, opts = {}) { const oldW = this.w || width; const oldH = this.h || height; this.viewportW = Math.max(1, width || this.viewportW || 1000); this.viewportH = Math.max(1, height || this.viewportH || 720); const requestedScale = fieldWorldScale(this); const scale = Math.max(requestedScale, 220 / this.viewportW, 180 / this.viewportH); this.w = this.viewportW * scale; this.h = this.viewportH * scale; this.fieldZoom = this.normalizedFieldZoom(this.fieldZoom); this.clampCamera(); if (opts.scaleContents && oldW > 0 && oldH > 0) { const sx = this.w / oldW; const sy = this.h / oldH; const scalePoint = (p) => { if (!p) return; if (Number.isFinite(p.x)) p.x *= sx; if (Number.isFinite(p.y)) p.y *= sy; }; for (const t of this.tarinai || []) scalePoint(t); for (const it of this.items || []) scalePoint(it); for (const ef of this.effects || []) scalePoint(ef); scalePoint(this.pointer); this.cameraX *= sx; this.cameraY *= sy; this.clampCamera(); this.markTerrainDirty?.("viewport-scale"); this.rebuildSpatial(true); this.drawListDirty = true; } }, setFieldZoom(nextZoom) { const current = this.normalizedFieldZoom(this.fieldZoom); const next = this.normalizedFieldZoom(nextZoom); if (Math.abs(next - current) < 0.001) { this.fieldZoom = current; this.clampCamera(); return false; } this.fieldZoom = next; this.clampCamera(); this.markTerrainDirty?.("field-zoom"); this.drawListDirty = true; return true; }, edgeSpawnPoint(targetX = null, targetY = null, margin = 34, outside = false) { const w = Math.max(this.w || 1000, margin * 2 + 1); const h = Math.max(this.h || 720, margin * 2 + 1); let side; if (Number.isFinite(targetX) && Number.isFinite(targetY)) { const distances = [targetY, w - targetX, h - targetY, targetX]; side = distances.indexOf(Math.min(...distances)); } else { side = Math.floor(Math.random() * 4); } const p = outside ? -(margin + 24) : margin; if (side === 0) return { x: clamp(targetX ?? rand(margin, w - margin), margin, w - margin), y: p, vx: rand(-10, 10), vy: rand(28, 48), angle: Math.PI / 2 }; if (side === 1) return { x: outside ? w - p : w - margin, y: clamp(targetY ?? rand(margin, h - margin), margin, h - margin), vx: rand(-48, -28), vy: rand(-10, 10), angle: Math.PI }; if (side === 2) return { x: clamp(targetX ?? rand(margin, w - margin), margin, w - margin), y: outside ? h - p : h - margin, vx: rand(-10, 10), vy: rand(-48, -28), angle: -Math.PI / 2 }; return { x: p, y: clamp(targetY ?? rand(margin, h - margin), margin, h - margin), vx: rand(28, 48), vy: rand(-10, 10), angle: 0 }; }, toolSizeFor(type = "") { return normalizedToolSizeFor(this, type); }, toolSizeScale(type = "") { return toolSizeScaleFor(this, type); }, applyToolSize(item) { if (!item) return item; const scale = this.toolSizeScale(item.type); const size = this.toolSizeFor(item.type); item.toolSize = size; this.toolSize = size; item.r *= scale; if (isServingFoodType(item.type)) { const servings = foodServingsForSize(size); item.foodServingsMax = servings; item.foodServingsRemaining = servings; item.amount = servings; (typeof updateServingFoodVisualSize === "function" ? updateServingFoodVisualSize(item) : false); } else if (["grass", "zunchi"].includes(item.type)) { item.amount *= scale * scale; item.foodServingScale = scale; } if (item.type === "firecracker") item.blastScale = scale; return item; } })); })(typeof window !== "undefined" ? window : globalThis);