diff --git a/addons/yt_world/models/yt_world_chest.py b/addons/yt_world/models/yt_world_chest.py index a329acd..48158c6 100644 --- a/addons/yt_world/models/yt_world_chest.py +++ b/addons/yt_world/models/yt_world_chest.py @@ -90,8 +90,12 @@ class YtWorldChest(models.Model): # 删掉最旧的,腾出一个空位给新宝箱 to_remove = existing[:len(existing) - MAX_CHESTS_PER_TILE + 1] to_remove.unlink() - # 2) 占用格集合(建筑 footprint) + # 2) 占用格集合(建筑 footprint + 已有 active 宝箱格子) + # 宝箱作为 1×1 建筑:不能和建筑重叠,也不能多个宝箱叠在同一格 occ = self._occupied_cells(world, q, r, grid_n) + keep_chests = [c for c in existing if c not in to_remove] + chest_cells = {(int(c.cell_i), int(c.cell_j)) for c in keep_chests} + occ |= chest_cells # 3) 随机空格(留 1 格边距,防止贴边/溢出岛外) margin = 1 free = [] diff --git a/world.html b/world.html index c71e633..9ecd866 100644 --- a/world.html +++ b/world.html @@ -2780,7 +2780,13 @@ async function microLoadChests(q, r) { try { const j = await API.chests(q, r); if (j && j.ok && Array.isArray(j.chests)) { - microChests = j.chests.map(c => ({ ...c, _claiming: false })); + // 同格去重:宝箱为 1×1 建筑,每格只保留一个(防御性,避免后端异常时叠箱) + const seen = new Set(); + microChests = j.chests.filter(c => { + const k = Math.round(c.cell_i) + ',' + Math.round(c.cell_j); + if (seen.has(k)) return false; + seen.add(k); return true; + }).map(c => ({ ...c, _claiming: false })); window.microChests = microChests; // 同步给 characters.js(否则其读到的是旧空数组引用) } } catch (e) { /* 离线兜底 */ } @@ -2868,27 +2874,32 @@ function getChestImage(url) { function microChestTopImage(tier) { return MICRO_CHEST_TOP_IMAGES[tier] || null; } function microChestIconImage(tier) { return MICRO_CHEST_ICON_IMAGES[tier] || null; } -// 在网格层之上绘制所有活跃宝箱(呼吸光晕 + 漂浮主体/俯视图) +// 在网格层之上绘制所有活跃宝箱。 +// 宝箱 = 1×1 建筑:俯视图素材本身已是等距 3D 视角(无需再做 iso 投影), +// 直接以格心为原点绘制、clip 到 1×1 菱形 footprint 防溢出。 function microDrawChests(c) { if (!microChests.length) return; - const t = microState.animTime; + const g = microBuildGeom(); + const cell = g.cell, hw = cell / 2, hh = g.hh; // 单格菱形半宽 hw、半高 hh microChests.forEach(ch => { - const sp = microCellToScreen(ch.cell_i + 0.5, ch.cell_j + 0.5); - // 主体:优先使用俯视图 PNG,未加载成功则回退 emoji + const sp = microCellToScreen(ch.cell_i + 0.5, ch.cell_j + 0.5); // 1×1 占位中心 = 格心 const imgUrl = microChestTopImage(ch.tier); const img = imgUrl ? getChestImage(imgUrl) : null; if (img && img.complete && img.naturalWidth) { - const baseW = 42, baseH = 42; c.save(); - c.translate(sp.x, sp.y - 14); - // 45度俯视:轻微压扁并旋转,贴合等距地面 - c.scale(1, 0.72); - c.drawImage(img, -baseW / 2, -baseH / 2, baseW, baseH); + c.translate(sp.x, sp.y); + // 1×1 菱形 footprint clip(防溢出到相邻格子) + c.beginPath(); + c.moveTo(0, -hh); c.lineTo(hw, 0); c.lineTo(0, hh); c.lineTo(-hw, 0); c.closePath(); + c.clip(); + // 俯视图本身已是 3D 等距视角,直接按格子比例缩放绘制,不再做 rotate/scale 投影 + const s = cell * 0.46; // 缩小到完整落入单格菱形内,不被 clip 裁切 + c.drawImage(img, -s / 2, -s / 2, s, s); c.restore(); } else { c.save(); - c.translate(sp.x, sp.y - 14); - c.font = '30px sans-serif'; c.textAlign = 'center'; c.textBaseline = 'middle'; + c.translate(sp.x, sp.y); + c.font = '28px sans-serif'; c.textAlign = 'center'; c.textBaseline = 'middle'; c.fillText(microChestEmoji(ch.tier), 0, 0); c.restore(); } @@ -7268,6 +7279,14 @@ function microFootprintHit(list, i, j, ew, eh, ignore) { const c = microItemCells(o); if (i < o.i + c.ew && i + ew > o.i && j < o.j + c.eh && j + eh > o.j) return true; } + // 宝箱作为 1×1 建筑占用格:不在建筑栏,但生成出来占网格,建造时不能放到宝箱所在格 + if (typeof microChests !== 'undefined' && microChests && microChests.length) { + for (const ch of microChests) { + if (ch === ignore) continue; + const ci = Math.round(ch.cell_i), cj = Math.round(ch.cell_j); + if (i < ci + 1 && i + ew > ci && j < cj + 1 && j + eh > cj) return true; + } + } return false; } // 可见矩形(内缩后)在 (i,j) 连续坐标下的覆盖范围,用于约束建筑落点不出框