From 0f1cb2fe6cb787f1b7ab2de5f89c3211b255ce5b Mon Sep 17 00:00:00 2001 From: 33333-33333 Date: Fri, 26 Jun 2026 22:35:26 +0900 Subject: [PATCH] published --- app_manifest.json | 2 +- index.html | 280 +++++++++++++++++++------------------- js/constraint_system.js | 121 +++++++++++++++- js/render.js | 29 ++-- js/snapshot_system.js | 63 +++++++-- js/ui_layout_dialogs.js | 6 + js/version.js | 2 +- js/world_placement_log.js | 186 +++++++++++++++++++++---- js/world_tool_actions.js | 4 + service-worker.js | 2 +- 10 files changed, 497 insertions(+), 198 deletions(-) diff --git a/app_manifest.json b/app_manifest.json index b498e50..e030626 100644 --- a/app_manifest.json +++ b/app_manifest.json @@ -1,5 +1,5 @@ { - "version": "38.00.00", + "version": "39.00.00", "css": [ "css/base.css", "css/layout.css", diff --git a/index.html b/index.html index 2fe75ae..84efbfd 100644 --- a/index.html +++ b/index.html @@ -7,11 +7,11 @@ - - - - - + + + + +
@@ -189,140 +189,140 @@
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/js/constraint_system.js b/js/constraint_system.js index df4f2fc..b549762 100644 --- a/js/constraint_system.js +++ b/js/constraint_system.js @@ -30,22 +30,129 @@ function resolveEndpointObject(endpoint, worldRef) { if (!endpoint || !worldRef) return null; + const allowFuzzy = endpoint.fuzzyResolve === true || endpoint.legacyFuzzy === true; if (endpoint.kind === "tarinai") { let found = (worldRef.tarinai || []).find(t => t && !t.dead && t.id === endpoint.id); - if (!found && Number.isFinite(endpoint.x) && Number.isFinite(endpoint.y)) { + if (!found && allowFuzzy && Number.isFinite(endpoint.x) && Number.isFinite(endpoint.y)) { found = (worldRef.tarinai || []).find(t => t && !t.dead && distXY(t.x, t.y, endpoint.x, endpoint.y) <= Math.max(40, (t.radius || 20) * 1.6)); - if (found) endpoint.id = found.id; + if (found) { + endpoint.id = found.id; + endpoint.fuzzyResolve = false; + endpoint.legacyFuzzy = false; + } } return found || null; } let found = (worldRef.items || []).find(it => it && !it.dead && it.id === endpoint.id); - if (!found && Number.isFinite(endpoint.x) && Number.isFinite(endpoint.y)) { + if (!found && allowFuzzy && Number.isFinite(endpoint.x) && Number.isFinite(endpoint.y)) { found = (worldRef.items || []).find(it => it && !it.dead && it.type === endpoint.type && distXY(it.x, it.y, endpoint.x, endpoint.y) <= Math.max(44, (it.r || 20) * 1.8)); - if (found) endpoint.id = found.id; + if (found) { + endpoint.id = found.id; + endpoint.fuzzyResolve = false; + endpoint.legacyFuzzy = false; + } } return found || null; } + function itemAngle(item) { + return typeof itemAngleFor === "function" ? itemAngleFor(item) : (Number(item?.angle) || 0); + } + + function isMechanicalLinkTarget(item) { + return Boolean(item && !item.dead && global.TarinaiMechanicalSystem?.isMechanicalType?.(item.type)); + } + + function localToWorld(item, lx, ly) { + const a = itemAngle(item); + const c = Math.cos(a), s = Math.sin(a); + return { + x: (Number(item?.x || 0) || 0) + lx * c - ly * s, + y: (Number(item?.y || 0) || 0) + lx * s + ly * c, + }; + } + + function worldToLocal(item, x, y) { + const a = itemAngle(item); + const dx = (Number(x || 0) || 0) - (Number(item?.x || 0) || 0); + const dy = (Number(y || 0) || 0) - (Number(item?.y || 0) || 0); + const c = Math.cos(-a), s = Math.sin(-a); + return { x: dx * c - dy * s, y: dx * s + dy * c }; + } + + function nearestMechanicalLocalPoint(item, lx, ly) { + if (!isMechanicalLinkTarget(item)) return null; + const segments = global.TarinaiMechanicalSystem?.sanitizeSegments?.(item) || []; + let best = null; + for (const seg of segments) { + if (!Array.isArray(seg) || seg.length < 4) continue; + const x1 = Number(seg[0]) || 0, y1 = Number(seg[1]) || 0; + const x2 = Number(seg[2]) || 0, y2 = Number(seg[3]) || 0; + const vx = x2 - x1, vy = y2 - y1; + const len2 = vx * vx + vy * vy; + if (len2 < 16) continue; + const t = clamp(((lx - x1) * vx + (ly - y1) * vy) / len2, 0, 1); + const x = x1 + vx * t; + const y = y1 + vy * t; + const d = Math.hypot(lx - x, ly - y); + if (!best || d < best.d) best = { x, y, d }; + } + return best; + } + + function snapEndpointToTarget(endpoint, worldRef) { + const obj = resolveEndpointObject(endpoint, worldRef); + if (!endpoint || !isMechanicalLinkTarget(obj)) return endpoint; + let local; + if (!endpoint.center && (Number.isFinite(Number(endpoint.localX)) || Number.isFinite(Number(endpoint.localY)))) { + local = { x: Number(endpoint.localX || 0) || 0, y: Number(endpoint.localY || 0) || 0 }; + } else if (Number.isFinite(Number(endpoint.x)) && Number.isFinite(Number(endpoint.y))) { + local = worldToLocal(obj, endpoint.x, endpoint.y); + } else { + local = { x: 0, y: 0 }; + } + const snapped = nearestMechanicalLocalPoint(obj, local.x, local.y); + if (!snapped) return endpoint; + const world = localToWorld(obj, snapped.x, snapped.y); + endpoint.localX = snapped.x; + endpoint.localY = snapped.y; + endpoint.x = world.x; + endpoint.y = world.y; + endpoint.center = false; + return endpoint; + } + + function remapLinksForEditedMechanicalItem(worldRef, target, reason = "mechanical-shape-edited") { + if (!worldRef || !isMechanicalLinkTarget(target)) return 0; + let changed = 0; + const remap = (endpoint) => { + if (!endpoint || endpoint.kind !== "item" || endpoint.id !== target.id) return false; + const beforeX = Number(endpoint.localX || 0) || 0; + const beforeY = Number(endpoint.localY || 0) || 0; + const beforeWorldX = Number(endpoint.x || 0) || 0; + const beforeWorldY = Number(endpoint.y || 0) || 0; + snapEndpointToTarget(endpoint, worldRef); + const localDelta = Math.hypot((Number(endpoint.localX || 0) || 0) - beforeX, (Number(endpoint.localY || 0) || 0) - beforeY); + const worldDelta = Math.hypot((Number(endpoint.x || 0) || 0) - beforeWorldX, (Number(endpoint.y || 0) || 0) - beforeWorldY); + return localDelta > 0.001 || worldDelta > 0.001; + }; + for (const item of worldRef.items || []) { + if (!item || item.dead || (item.type !== "rope" && item.type !== "rod")) continue; + const a = remap(item.linkA); + const b = remap(item.linkB); + if (a || b) { + item.world = worldRef; + changed += 1; + } + } + if (remap(worldRef.pendingLinkEndpoint)) changed += 1; + if (changed) { + worldRef.drawListDirty = true; + worldRef.markSpatialDirty?.(reason); + } + return changed; + } + function endpointWorld(endpoint, worldRef, depth = 0, seen = null) { const obj = resolveEndpointObject(endpoint, worldRef); if (obj && (obj.type === "rope" || obj.type === "rod") && worldRef) obj.world = worldRef; @@ -66,7 +173,7 @@ const lx = Number(endpoint.localX || 0) || 0; const ly = Number(endpoint.localY || 0) || 0; if (endpoint.center || (!lx && !ly)) return { x: obj.x || 0, y: obj.y || 0, obj }; - const a = typeof itemAngleFor === "function" ? itemAngleFor(obj) : (Number(obj.angle) || 0); + const a = itemAngle(obj); const c = Math.cos(a), s = Math.sin(a); return { x: (obj.x || 0) + lx * c - ly * s, y: (obj.y || 0) + lx * s + ly * c, obj }; } @@ -283,6 +390,8 @@ resolveEndpointObject, endpointMass, moveEndpointObject, + snapEndpointToTarget, + remapLinksForEditedMechanicalItem, updateFlexibleLink, updateRigidLink, updateLink, @@ -290,5 +399,5 @@ }); global.TarinaiConstraintSystem = api; - global.TarinaiLinkRuntime = Object.freeze({ endpointWorld, pointSegmentDistance }); + global.TarinaiLinkRuntime = Object.freeze({ endpointWorld, pointSegmentDistance, snapEndpointToTarget, remapLinksForEditedMechanicalItem }); })(typeof window !== "undefined" ? window : globalThis); diff --git a/js/render.js b/js/render.js index 37ab3b2..e4a3a81 100644 --- a/js/render.js +++ b/js/render.js @@ -258,10 +258,23 @@ function drawOperationToolPreview(ctx, world) { const type = world.copyBuffer.type || ""; const item = world.copyPreviewItemAt?.(p.x, p.y); if (item) { + const pad = typeof CONFIG !== "undefined" ? (CONFIG.worldPadding ?? 0) : 0; + const rectOutside = (rect) => Boolean(rect && (rect.left < pad || rect.top < pad || rect.right > world.w - pad || rect.bottom > world.h - pad)); + const circleOutside = (cx, cy, radius) => cx - radius < pad || cy - radius < pad || cx + radius > world.w - pad || cy + radius > world.h - pad; + const previewRects = world.solidObstacleRects?.(item) || []; + const boundsBlocked = previewRects.length + ? previewRects.some(rr => rectOutside(rr.oriented ? { + left: (rr.cx || p.x) - (rr.halfW || 10) - (rr.halfH || 5), + right: (rr.cx || p.x) + (rr.halfW || 10) + (rr.halfH || 5), + top: (rr.cy || p.y) - (rr.halfW || 10) - (rr.halfH || 5), + bottom: (rr.cy || p.y) + (rr.halfW || 10) + (rr.halfH || 5), + } : rr)) + : circleOutside(p.x, p.y, Math.max(14, (item.r || itemRadiusFor?.(type, 16) || 16) * 1.25)); + const blocked = boundsBlocked || Boolean(world.placementBlocked?.(item) || world.fencePlacementBlocked?.(item)); let copyPreviewDrew = false; if (typeof item.draw === "function") { ctx.save(); - ctx.globalAlpha = 0.74; + ctx.globalAlpha = blocked ? 0.44 : 0.74; try { item.draw(ctx, world.time || 0, typeof getLightingState === "function" ? getLightingState(world) : null); copyPreviewDrew = true; } catch (_) { copyPreviewDrew = false; } ctx.restore(); } @@ -273,18 +286,18 @@ function drawOperationToolPreview(ctx, world) { const bctx = buffer.getContext("2d"); if (bctx && globalThis.drawToolItemPreview(bctx, type, { width: buffer.width, height: buffer.height, compact: true }) !== false) { ctx.save(); - ctx.globalAlpha = 0.74; + ctx.globalAlpha = blocked ? 0.44 : 0.74; ctx.drawImage(buffer, p.x - previewSize / 2, p.y - previewSize / 2, previewSize, previewSize); ctx.restore(); } } ctx.save(); - ctx.globalAlpha = 0.50; - ctx.setLineDash([7, 5]); - ctx.lineWidth = 2; - ctx.strokeStyle = "rgba(128,92,198,0.86)"; - ctx.fillStyle = "rgba(150,110,220,0.12)"; - const rects = world.solidObstacleRects?.(item) || []; + ctx.globalAlpha = blocked ? 0.58 : 0.50; + ctx.setLineDash(blocked ? [5, 4] : [7, 5]); + ctx.lineWidth = blocked ? 2.2 : 2; + ctx.strokeStyle = blocked ? "rgba(194,70,58,0.88)" : "rgba(110,178,55,0.88)"; + ctx.fillStyle = blocked ? "rgba(232,84,72,0.14)" : "rgba(190,236,92,0.15)"; + const rects = previewRects; if (rects.length) { for (const rr of rects) { if (rr.oriented) { diff --git a/js/snapshot_system.js b/js/snapshot_system.js index 3c1ed9c..d76bb81 100644 --- a/js/snapshot_system.js +++ b/js/snapshot_system.js @@ -311,7 +311,7 @@ if (typeof t.refreshEffectiveSize === "function") t.refreshEffectiveSize(); return t; } - function itemExtra(item, tarinaiIndex = new Map()) { + function itemExtra(item, tarinaiIndex = new Map(), itemIndex = new Map()) { const type = item?.type || ""; if (item?.isStructure) { const owner = tarinaiIndex.get(item.ownerId) ?? -1; @@ -346,7 +346,24 @@ ]; if (type === "rope" || type === "rod") { - const cloneEndpoint = ep => ep ? { kind: ep.kind || "item", id: ep.id || "", type: ep.type || "", label: ep.label || "", localX: q(ep.localX || 0, 1), localY: q(ep.localY || 0, 1), center: ep.center ? 1 : 0, x: q(ep.x || 0, 1), y: q(ep.y || 0, 1), attachT: Number.isFinite(Number(ep.attachT)) ? q(ep.attachT, 1000) : null } : null; + const cloneEndpoint = ep => { + if (!ep) return null; + const kind = ep.kind || "item"; + const refIdx = kind === "tarinai" ? (tarinaiIndex.get(ep.id) ?? -1) : (itemIndex.get(ep.id) ?? -1); + return { + kind, + id: ep.id || "", + refIdx, + type: ep.type || "", + label: ep.label || "", + localX: q(ep.localX || 0, 1), + localY: q(ep.localY || 0, 1), + center: ep.center ? 1 : 0, + x: q(ep.x || 0, 1), + y: q(ep.y || 0, 1), + attachT: Number.isFinite(Number(ep.attachT)) ? q(ep.attachT, 1000) : null, + }; + }; return [cloneEndpoint(item.linkA), cloneEndpoint(item.linkB), q(item.linkLength || item.r * 2 || 80, 1), q(item.linkMidX || item.x || 0, 1), q(item.linkMidY || item.y || 0, 1), q(item.linkMidVX || 0, 10), q(item.linkMidVY || 0, 10)]; } if (type === "duplicator") return [enumIndex(ITEM_TYPE_IDS, item.storedFoodType || "", -1)]; @@ -359,12 +376,12 @@ if (typeof global.isServingFoodType === "function" && global.isServingFoodType(type)) return [q(item.foodServingsRemaining ?? item.amount, 10), q(item.foodServingsMax, 10)]; return []; } - function compactItem(item, index, tarinaiIndex = new Map()) { + function compactItem(item, index, tarinaiIndex = new Map(), itemIndex = new Map()) { const typeId = enumIndex(ITEM_TYPE_IDS, item?.type || "", -1); if (typeId < 0 || item?.type === "trace") return null; - return [typeId, q(item?.x, 1), q(item?.y, 1), q(item?.amount ?? item?.hp, 10), itemExtra(item, tarinaiIndex)]; + return [typeId, q(item?.x, 1), q(item?.y, 1), q(item?.amount ?? item?.hp, 10), itemExtra(item, tarinaiIndex, itemIndex)]; } - function applyItemExtra(item, extra, tarinaiList = []) { + function applyItemExtra(item, extra, tarinaiList = [], itemList = []) { if (!Array.isArray(extra)) return; const type = item.type || ""; if (item.isStructure) { @@ -426,7 +443,27 @@ item.r = Math.max(item.r || 64, Math.min(460, extent)); } else if (type === "rope" || type === "rod") { const e = Array.isArray(extra) ? extra : []; - const normalizeEndpoint = ep => ep ? { kind: ep.kind || "item", id: ep.id || "", type: ep.type || "", label: ep.label || "", localX: u(ep.localX, 1, 0), localY: u(ep.localY, 1, 0), center: !!ep.center, x: u(ep.x, 1, item.x || 0), y: u(ep.y, 1, item.y || 0), attachT: ep.attachT == null ? null : clamp(u(ep.attachT, 1000, 0.5), 0, 1) } : null; + const normalizeEndpoint = ep => { + if (!ep) return null; + const kind = ep.kind || "item"; + const refIdx = Number(ep.refIdx); + const indexedTarget = Number.isInteger(refIdx) && refIdx >= 0 + ? (kind === "tarinai" ? tarinaiList[refIdx] : itemList[refIdx]) + : null; + return { + kind, + id: indexedTarget?.id || ep.id || "", + type: ep.type || indexedTarget?.type || "", + label: ep.label || indexedTarget?.name || "", + localX: u(ep.localX, 1, 0), + localY: u(ep.localY, 1, 0), + center: !!ep.center, + x: u(ep.x, 1, item.x || 0), + y: u(ep.y, 1, item.y || 0), + attachT: ep.attachT == null ? null : clamp(u(ep.attachT, 1000, 0.5), 0, 1), + fuzzyResolve: !indexedTarget && !Number.isInteger(refIdx), + }; + }; item.linkA = normalizeEndpoint(e[0]); item.linkB = normalizeEndpoint(e[1]); item.linkLength = Math.max(24, u(e[2], 1, item.linkLength || 80)); @@ -467,13 +504,13 @@ const itemRecords = []; for (const it of (worldRef.items || [])) { if (!it || it.dead || it.type === "trace" || enumIndex(ITEM_TYPE_IDS, it.type || "", -1) < 0) continue; - const row = compactItem(it, itemRecords.length, tarinaiIndex); - if (row) itemRecords.push({ item: it, row }); + const typeId = enumIndex(ITEM_TYPE_IDS, it.type || "", -1); + itemRecords.push({ item: it, sortTypeId: typeId, sortX: q(it.x, 1), sortY: q(it.y, 1) }); } - itemRecords.sort((a, b) => (a.row[0] || 0) - (b.row[0] || 0) || mortonKeyXY(a.row[1], a.row[2]) - mortonKeyXY(b.row[1], b.row[2]) || String(a.item?.id || "").localeCompare(String(b.item?.id || ""))); + itemRecords.sort((a, b) => (a.sortTypeId || 0) - (b.sortTypeId || 0) || mortonKeyXY(a.sortX, a.sortY) - mortonKeyXY(b.sortX, b.sortY) || String(a.item?.id || "").localeCompare(String(b.item?.id || ""))); const itemIndex = new Map(); itemRecords.forEach((rec, i) => { if (rec.item?.id) itemIndex.set(rec.item.id, i); }); - const itemRows = itemRecords.map(rec => rec.row); + const itemRows = itemRecords.map((rec, i) => compactItem(rec.item, i, tarinaiIndex, itemIndex)).filter(Boolean); return { v: SNAPSHOT_VERSION, a: "tj1", @@ -535,9 +572,13 @@ item.id = `li${idx}`; item.world = worldRef; item.amount = u(row[3], 10, item.amount || 1); - applyItemExtra(item, extra, worldRef.tarinai); + item.__savedExtra = extra; worldRef.items.push(item); } + for (const item of worldRef.items) { + applyItemExtra(item, item.__savedExtra || [], worldRef.tarinai, worldRef.items); + delete item.__savedExtra; + } for (const t of worldRef.tarinai) { if (Number.isInteger(t.__savedPinIdx) && t.__savedPinIdx >= 0) t.stuckPushpinId = worldRef.items[t.__savedPinIdx]?.id || null; if (Number.isInteger(t.__savedNestIdx) && t.__savedNestIdx >= 0) t.insideNestBoxId = worldRef.items[t.__savedNestIdx]?.id || null; diff --git a/js/ui_layout_dialogs.js b/js/ui_layout_dialogs.js index 4b2cdc5..331ef1b 100644 --- a/js/ui_layout_dialogs.js +++ b/js/ui_layout_dialogs.js @@ -125,9 +125,15 @@ function resizeFieldFromWheel(e) { } function selectTool(tool) { + const wasCopyArmed = world.tool === "copy" && tool === "copy" && Boolean(world.copyBuffer); const result = window.TarinaiCommands?.dispatch?.(world, { type: "tool.select", toolId: tool }); const activeTool = result?.toolId || world.tool || tool; if (activeTool !== "rope" && activeTool !== "rod") world.pendingLinkEndpoint = null; + if (activeTool !== "copy") world.copyBuffer = null; + else if (wasCopyArmed) { + world.copyBuffer = null; + showToast("\u30b3\u30d4\u30fc\u3092\u89e3\u9664\u3057\u307e\u3057\u305f\u3002"); + } for (const btn of uiCache.toolButtons) { btn.classList.toggle("selected", btn.dataset.tool === activeTool); } diff --git a/js/version.js b/js/version.js index 36e9222..51ab968 100644 --- a/js/version.js +++ b/js/version.js @@ -1,7 +1,7 @@ "use strict"; (function () { - const APP_VERSION = "38.00.00"; + const APP_VERSION = "39.00.00"; const APP_BUILD = "lab-ground-wash-and-freezer-removal-v38"; const APP_CACHE_NAME = `tarinai-colony-${APP_VERSION}`; const STATIC_VERSION_PARAM = `v=${APP_VERSION}`; diff --git a/js/world_placement_log.js b/js/world_placement_log.js index 7669abd..ce97607 100644 --- a/js/world_placement_log.js +++ b/js/world_placement_log.js @@ -392,6 +392,7 @@ const extent = worldRef?.rotatorExtent?.(item); if (Number.isFinite(extent)) item.r = Math.max(32, Math.min(460, extent)); item.rotatorEditorOpenAt = worldRef?.time || 0; + global.TarinaiLinkRuntime?.remapLinksForEditedMechanicalItem?.(worldRef, item, "rotator-link-remap"); worldRef?.markSpatialDirty?.("rotator-edited"); worldRef.drawListDirty = true; worldRef?.log?.("回転体の設計を変更した。", "observe"); @@ -672,6 +673,7 @@ item.reciprocatorAnchorY = item.y - Math.sin(sa) * halfTravel * (Number(item.reciprocatorPhase || 0) || 0); const extent = worldRef?.rotatorExtent?.(item); if (Number.isFinite(extent)) item.r = Math.max(32, Math.min(460, extent)); + global.TarinaiLinkRuntime?.remapLinksForEditedMechanicalItem?.(worldRef, item, "reciprocator-link-remap"); worldRef?.markSpatialDirty?.("reciprocator-edited"); worldRef.drawListDirty = true; worldRef?.log?.("往復体の設計を変更した。", "observe"); @@ -687,11 +689,47 @@ return true; } + function copyTargetAt(worldRef, x, y) { + let blockedReason = ""; + const collision = global.TarinaiCollisionFootprints; + const items = worldRef?.items || []; + for (let i = items.length - 1; i >= 0; i--) { + const it = items[i]; + if (!it || it.dead) continue; + const type = String(it.type || ""); + if (type === "trace" || type === "splat" || type === "ant_corpse") continue; + if (type === "rope" || type === "rod") { + const runtime = global.TarinaiLinkRuntime; + const a = runtime?.endpointWorld?.(it.linkA, worldRef); + const b = runtime?.endpointWorld?.(it.linkB, worldRef); + const d = runtime?.pointSegmentDistance?.(x, y, a?.x ?? it.x, a?.y ?? it.y, b?.x ?? it.x, b?.y ?? it.y) ?? Infinity; + const tolerance = worldRef?.screenSizeToWorld ? worldRef.screenSizeToWorld(12) : 12; + if (d <= tolerance && !blockedReason) blockedReason = "link"; + continue; + } + if (typeof isPinType === "function" && isPinType(type) && it.pinState === "lodged") { + const d = distXY(x, y, it.x, it.y); + if (d <= Math.max(24, (it.r || 12) * 1.8) && !blockedReason) blockedReason = "lodged_pin"; + continue; + } + const d = distXY(x, y, it.x, it.y); + let result = null; + if (collision?.hitTestItem) { + const isPrecise = Boolean(global.TarinaiMechanicalSystem?.isMechanicalType?.(type) || worldRef?.isFenceType?.(type) || type === "nest_box"); + result = collision.hitTestItem(worldRef, it, x, y, { + padding: isPrecise ? 10 : 4, + radiusMultiplier: isPrecise ? 1.12 : 1.18, + }); + } + const hit = result ? Boolean(result.hit) : d <= Math.max(16, (it.r || 16) * 1.18); + if (!hit) continue; + return { item: it, reason: "" }; + } + return { item: null, reason: blockedReason }; + } + function copyableItemAt(worldRef, x, y) { - const it = worldRef?.findDeleteToolTargetAt?.(x, y); - if (!it || it.dead || it.type === "trace" || it.type === "splat" || it.type === "ant_corpse") return null; - if (typeof isPinType === "function" && isPinType(it.type) && it.pinState === "lodged") return null; - return it; + return copyTargetAt(worldRef, x, y).item; } function makeCopyBufferForItem(item) { @@ -704,17 +742,12 @@ toolSize: item.toolSize || "medium", foodServingScale: Number(item.foodServingScale || 1) || 1, foodServingsMax: Number(item.foodServingsMax || 0) || 0, - foodServingsRemaining: Number(item.foodServingsRemaining || 0) || 0, seed: Number.isFinite(Number(item.seed)) ? Number(item.seed) : null, spin: Number.isFinite(Number(item.spin)) ? Number(item.spin) : 0, zunchiVariant: item.zunchiVariant || "", - stage: item.stage || "", - freshness: Number.isFinite(Number(item.freshness)) ? Number(item.freshness) : null, - fertility: Number.isFinite(Number(item.fertility)) ? Number(item.fertility) : null, grassStage: Number.isFinite(Number(item.grassStage)) ? Number(item.grassStage) : null, growth: Number.isFinite(Number(item.growth)) ? Number(item.growth) : null, health: Number.isFinite(Number(item.health)) ? Number(item.health) : null, - fuseTimer: Number.isFinite(Number(item.fuseTimer)) ? Number(item.fuseTimer) : null, fuseMax: Number.isFinite(Number(item.fuseMax)) ? Number(item.fuseMax) : null, }; if (item.type === "signboard") data.text = item.text || ""; @@ -747,9 +780,11 @@ if (Number.isFinite(Number(data.spin))) item.spin = Number(data.spin); if (item.type === "zunchi") { if (data.zunchiVariant) item.zunchiVariant = String(data.zunchiVariant); - if (data.stage) item.stage = String(data.stage); - if (Number.isFinite(Number(data.freshness))) item.freshness = Number(data.freshness); - if (Number.isFinite(Number(data.fertility))) item.fertility = Number(data.fertility); + item.stage = "fresh"; + item.stageTimer = 0; + item.freshness = 1; + item.fertility = 0; + item.spawnGrace = Math.max(Number(item.spawnGrace || 0) || 0, 10); } if (item.type === "grass") { if (Number.isFinite(Number(data.grassStage))) item.grassStage = Number(data.grassStage); @@ -757,8 +792,8 @@ if (Number.isFinite(Number(data.health))) item.health = Number(data.health); } if (item.type === "firecracker") { - if (Number.isFinite(Number(data.fuseTimer))) item.fuseTimer = Number(data.fuseTimer); if (Number.isFinite(Number(data.fuseMax))) item.fuseMax = Number(data.fuseMax); + item.fuseTimer = Math.max(0.1, Number(item.fuseMax || 5) || 5); } if (typeof isRotatableItemType === "function" && isRotatableItemType(item.type)) item.angle = Number(data.angle || 0) || 0; if (item.type === "signboard") item.text = String(data.text || ""); @@ -797,13 +832,23 @@ item.toolSize = data.toolSize || item.toolSize || "medium"; item.foodServingScale = Number(data.foodServingScale || item.foodServingScale || 1) || 1; item.foodServingsMax = Math.max(1, Number(data.foodServingsMax || item.foodServingsMax || item.amount || 1)); - item.foodServingsRemaining = Math.max(1, Number(data.foodServingsRemaining || item.foodServingsRemaining || item.amount || 1)); + item.foodServingsRemaining = item.foodServingsMax; item.amount = item.foodServingsRemaining; } + if (Number.isFinite(Number(item.x))) item.prevX = item.x; + if (Number.isFinite(Number(item.y))) item.prevY = item.y; + item.vx = 0; + item.vy = 0; + item.spinVelocity = 0; if (typeof isPinType === "function" && isPinType(item.type)) { item.pinState = "loose"; item.pinTargetId = ""; - item.vx = 0; item.vy = 0; item.spinVelocity = 0; + item.pinAttachAngle = 0; + item.pinAttachDistance = 0; + item.pinOffsetY = 0; + item.pinDamageTick = 0; + item.pinFallCheckTimer = 0; + item.pinLogAt = -999; } return item; } @@ -819,8 +864,14 @@ for (let i = (worldRef?.items || []).length - 1; i >= 0; i--) { const it = worldRef.items[i]; if (!isEditableItem(it)) continue; - const d = distXY(x, y, it.x, it.y); - if (d <= Math.max(54, (it.r || 24) * 2.4) && d < bestD) { best = it; bestD = d; } + let d = distXY(x, y, it.x, it.y); + let hit = d <= Math.max(30, (it.r || 24) * 1.35); + if (global.TarinaiMechanicalSystem?.isMechanicalType?.(it.type) || worldRef?.isFenceType?.(it.type)) { + const result = global.TarinaiCollisionFootprints?.hitTestItem?.(worldRef, it, x, y, { padding: 14, radiusMultiplier: 1.25 }) || { hit: false, distance: d }; + hit = Boolean(result.hit); + d = Math.min(d, result.distance ?? d); + } + if (hit && d < bestD) { best = it; bestD = d; } } return best; } @@ -885,9 +936,82 @@ return true; } + function isPhysicsObstacleItemType(type = "") { + const key = String(type || ""); + return Boolean( + key === "rope" || key === "rod" + || global.TarinaiMechanicalSystem?.isMechanicalType?.(key) + || (typeof isFenceItemType === "function" && isFenceItemType(key)) + ); + } + + function isSoftPhysicsIgnoredItemType(type = "") { + const key = String(type || ""); + return key === "water" || key === "grass_bed"; + } + + function softItemRadius(item) { + if (!item) return 12; + return Math.max(8, Number(item.r || item.radius || itemRadiusFor?.(item.type, 12) || 12) || 12); + } + + function physicsItemOverlapsSoftItem(worldRef, physicsItem, softItem) { + if (!worldRef || !physicsItem || !softItem || softItem.dead) return false; + const r = softItemRadius(softItem); + if (physicsItem.type === "rope" || physicsItem.type === "rod") { + const runtime = global.TarinaiLinkRuntime; + const a = runtime?.endpointWorld?.(physicsItem.linkA, worldRef); + const b = runtime?.endpointWorld?.(physicsItem.linkB, worldRef); + if (a && b) { + const d = runtime?.pointSegmentDistance?.(softItem.x, softItem.y, a.x, a.y, b.x, b.y); + return Number.isFinite(d) && d <= r + (physicsItem.type === "rod" ? 9 : 7); + } + } + const rects = worldRef.solidObstacleRects?.(physicsItem) || []; + if (rects.length) { + return rects.some(rect => global.TarinaiCollisionFootprints?.rectCircleOverlap?.(rect, softItem.x, softItem.y, r, 0.5)); + } + const pr = Math.max(10, Number(physicsItem.r || physicsItem.radius || itemRadiusFor?.(physicsItem.type, 12) || 12) || 12); + return distXY(physicsItem.x, physicsItem.y, softItem.x, softItem.y) <= pr + r; + } + + function clearSoftItemsUnderPhysicsItem(worldRef, item, reason = "physics-overlap-clear") { + if (!worldRef || !item || !isPhysicsObstacleItemType(item.type)) return 0; + let search = Math.max(80, Number(item.r || item.radius || 36) + 90); + if (global.TarinaiMechanicalSystem?.isMechanicalType?.(item.type)) { + search = Math.max(search, (global.TarinaiMechanicalSystem?.reach?.(item) || item.r || 80) + 96); + } else if (item.type === "rope" || item.type === "rod") { + search = Math.max(search, Number(item.linkLength || (item.r || 40) * 2 || 80) * 0.55 + 96); + } + let removed = 0; + for (const soft of worldRef.nearbyItems?.(item.x, item.y, search, true) || worldRef.items || []) { + if (!soft || soft === item || soft.dead || !isSoftPhysicsIgnoredItemType(soft.type)) continue; + if (!physicsItemOverlapsSoftItem(worldRef, item, soft)) continue; + const ok = global.TarinaiStructureLifecycle?.deleteItem?.(worldRef, soft, { + reason, + userReason: "物理アイテムに重なった", + wake: true, + }); + if (ok) removed += 1; + } + if (removed) { + worldRef.markItemBucketsDirty?.(reason); + worldRef.markSpatialDirty?.(reason); + worldRef.drawListDirty = true; + } + return removed; + } + + global.TarinaiPhysicsSoftClear = Object.freeze({ + isPhysicsType: isPhysicsObstacleItemType, + isSoftType: isSoftPhysicsIgnoredItemType, + overlaps: physicsItemOverlapsSoftItem, + clearForItem: clearSoftItemsUnderPhysicsItem, + }); + function clearOverlappingGrassBedsForPlacement(worldRef, item) { if (!worldRef || !item || item.type === "grass_bed" || item.type === "trace" || item.type === "splat") return 0; - if (global.TarinaiMechanicalSystem?.isMechanicalType?.(item.type) || item.type === "rope" || item.type === "rod" || (typeof isFenceItemType === "function" && isFenceItemType(item.type))) return 0; + if (isPhysicsObstacleItemType(item.type)) return clearSoftItemsUnderPhysicsItem(worldRef, item, "physics-placement-soft-clear"); const radius = Math.max(16, item.r || item.radius || 12); let removed = 0; for (const bed of worldRef.nearbyItems?.(item.x, item.y, radius + 72) || worldRef.items || []) { @@ -944,11 +1068,17 @@ useCopyToolAt(x, y) { if (!this.copyBuffer) { - const target = copyableItemAt(this, x, y); - if (!target) { showToast("コピーできるものがありません。"); return true; } + const targetInfo = copyTargetAt(this, x, y); + const target = targetInfo.item; + if (!target) { + if (targetInfo.reason === "link") showToast("\u30ed\u30fc\u30d7\u3068\u68d2\u306f\u30b3\u30d4\u30fc\u3067\u304d\u307e\u305b\u3093\u3002"); + else if (targetInfo.reason === "lodged_pin") showToast("\u523a\u3055\u3063\u305f\u30d4\u30f3\u306f\u30b3\u30d4\u30fc\u3067\u304d\u307e\u305b\u3093\u3002"); + else showToast("\u30b3\u30d4\u30fc\u3067\u304d\u308b\u3082\u306e\u304c\u3042\u308a\u307e\u305b\u3093\u3002"); + return true; + } this.copyBuffer = makeCopyBufferForItem(target); - showToast(`${toolLabel(target.type)}をコピーしました。`); - this.log?.(`${toolLabel(target.type)}をコピーした。`, "observe"); + showToast(`${toolLabel(target.type)}\u3092\u30b3\u30d4\u30fc\u3057\u307e\u3057\u305f\u3002`); + this.log?.(`${toolLabel(target.type)}\u3092\u30b3\u30d4\u30fc\u3057\u305f\u3002`, "observe"); return true; } const item = this.copyPreviewItemAt?.(x, y); @@ -960,17 +1090,12 @@ if (placed.type === "grass" || placed.type === "trace" || placed.type === "splat") this.markTerrainDirtyAt?.(placed.x, placed.y, Math.max(placed.r || 24, 36), "copy-paste"); forceImmediateToolVisualRefresh(this, "copy-paste"); const label = toolLabel(item.type); - this.log?.(`${label}を貼り付けた。`, "observe"); - showToast(`${label}を貼り付けました。`); + this.log?.(`${label}\u3092\u8cbc\u308a\u4ed8\u3051\u305f\u3002`, "observe"); + showToast(`${label}\u3092\u8cbc\u308a\u4ed8\u3051\u307e\u3057\u305f\u3002`); return true; } } - const target = copyableItemAt(this, x, y); - if (target) { - this.copyBuffer = makeCopyBufferForItem(target); - showToast(`${toolLabel(target.type)}をコピーしました。`); - this.log?.(`${toolLabel(target.type)}をコピーした。`, "observe"); - } + showToast("\u305d\u3053\u306b\u306f\u8cbc\u308a\u4ed8\u3051\u3067\u304d\u307e\u305b\u3093\u3002"); return true; }, @@ -1158,6 +1283,7 @@ if (item.type === "grass") showToast(`草はこのフィールドでは${this.grassLimit?.() ?? CONFIG.grassLimit ?? 99}本までです。`); return null; } + clearSoftItemsUnderPhysicsItem(this, item, `place:${item.type}:soft-overlap-clear`); this.emit("tool:place", { item, type: item.type, dropped }); this.emit("tool:placed", { item, type: item.type, dropped, tool: item.type }); this.drawListDirty = true; diff --git a/js/world_tool_actions.js b/js/world_tool_actions.js index 254aad5..3e59a10 100644 --- a/js/world_tool_actions.js +++ b/js/world_tool_actions.js @@ -178,6 +178,7 @@ if (!hit || d >= bestD) continue; const local = this.linkPointLocalForItem?.(it, x, y, isMechanism || isFence) || { localX: 0, localY: 0 }; best = { kind: "item", id: it.id, type: it.type, label: toolLabel(it.type), x: isMechanism || isFence ? x : it.x, y: isMechanism || isFence ? y : it.y, localX: local.localX, localY: local.localY, center: !(isMechanism || isFence) }; + if (isMechanism) global.TarinaiLinkRuntime?.snapEndpointToTarget?.(best, this); bestD = d; } return best; @@ -195,6 +196,8 @@ return true; } const first = this.pendingLinkEndpoint; + global.TarinaiLinkRuntime?.snapEndpointToTarget?.(first, this); + global.TarinaiLinkRuntime?.snapEndpointToTarget?.(endpoint, this); if (first.kind === endpoint.kind && first.id === endpoint.id && distXY(first.x, first.y, endpoint.x, endpoint.y) < 8) { showToast("別の位置または対象を選んでください。"); return true; @@ -217,6 +220,7 @@ const placed = this.addItem?.(item, constraintType); this.pendingLinkEndpoint = null; if (!placed) { showToast(`${toolName}を追加できませんでした。`); return true; } + global.TarinaiPhysicsSoftClear?.clearForItem?.(this, item, `${constraintType}-soft-overlap-clear`); forceImmediateToolVisualRefresh(this, `${constraintType}-create`); this.log?.(`${toolName}をつないだ。`, "observe"); showToast(`${toolName}をつなぎました。`); diff --git a/service-worker.js b/service-worker.js index 4c2783a..b0e832c 100644 --- a/service-worker.js +++ b/service-worker.js @@ -1,6 +1,6 @@ "use strict"; -const APP_VERSION = "38.00.00"; +const APP_VERSION = "39.00.00"; const CACHE_NAME = `tarinai-colony-${APP_VERSION}`; const v = `v=${APP_VERSION}`; // Generated from app_manifest.json. Run scripts/generate_app_files.py after changing static files.