>>0;}
function rng(seed){let s=seed>>>0;return function(){s=(Math.imul(s,1664525)+1013904223)>>>0;return s/4294967296;};}
-const TIERCOL={'普通':'#9aa6c4','精良':'#5fd38a','稀有':'#4cc9f0','史诗':'#c77dff','传说':'#e7b85c'};
function starPts(cx,cy,spikes,outer,inner){let pts=[],rot=-Math.PI/2;for(let i=0;i`;if(v===0){x+=``;}else if(v===1){x+=``;}else{x+=``;}x+=``;return x;}
diff --git a/codex.html b/codex.html
index 584ba49..3110037 100644
--- a/codex.html
+++ b/codex.html
@@ -153,10 +153,11 @@ body::before{
.card.race .tags{display:flex;gap:4px;flex-wrap:wrap;justify-content:center;margin-top:2px;}
.card.race .tag{font-size:9px;padding:1px 6px;border-radius:4px;background:rgba(255,255,255,.06);color:#8899aa;}
-/* 装备/天赋/技能书等通用标签 */
-.card.equip .tags,.card.talent .tags,.card.skill_book .tags{display:flex;gap:4px;flex-wrap:wrap;justify-content:center;margin-top:2px;}
-.card.equip .tag,.card.talent .tag,.card.skill_book .tag{font-size:9px;padding:1px 6px;border-radius:4px;background:rgba(255,255,255,.06);color:#8899aa;}
+/* 装备/天赋/技能书/散落物等通用标签 */
+.card.equip .tags,.card.talent .tags,.card.skill_book .tags,.card.scatter .tags{display:flex;gap:4px;flex-wrap:wrap;justify-content:center;margin-top:2px;}
+.card.equip .tag,.card.talent .tag,.card.skill_book .tag,.card.scatter .tag{font-size:9px;padding:1px 6px;border-radius:4px;background:rgba(255,255,255,.06);color:#8899aa;}
.card.equip{border-color:rgba(120,200,255,.12);}
+.card.scatter{border-color:rgba(245,158,11,.12);}
/* 野怪等级角标 */
.card .tierbadge{position:absolute;top:8px;right:8px;font-size:9px;font-weight:700;padding:1px 6px;border-radius:6px;letter-spacing:.5px;}
@@ -284,6 +285,7 @@ body::before{
📖 技能书
📚 书籍
🛡️ 装备
+ 🎁 散落物
✨ 特殊
@@ -339,7 +341,7 @@ body::before{
const QUALITIES = ['common','uncommon','rare','epic','legendary','mythic'];
const QUALITY_NAMES = { common:'普通', uncommon:'优秀', rare:'精良', epic:'史诗', legendary:'传说', mythic:'神话' };
const TIER_NAMES = { normal:'普通野怪', elite:'精英野怪', boss:'Boss' };
-const CAT_ORDER = ['material','processed','creature','monster','race','talent','skill_book','book','equip','special'];
+const CAT_ORDER = ['material','processed','creature','monster','race','talent','skill_book','book','equip','scatter','special'];
const SLOT_NAMES = { weapon:'武器', armor:'护甲', accessory:'饰品', boots:'鞋子' };
// 当前玩家已拥有天赋(从后端加载){ code: { level, active } }
@@ -693,7 +695,7 @@ function showDetail(e){
const c = e.qual==='common'?'156,163,175':e.qual==='uncommon'?'34,197,94':e.qual==='rare'?'59,130,246':e.qual==='epic'?'168,85,247':e.qual==='legendary'?'245,158,11':'220,38,38';
const t = e.qual==='common'?'#9ca3af':e.qual==='uncommon'?'#4ade80':e.qual==='rare'?'#60a5fa':e.qual==='epic'?'#c084fc':e.qual==='legendary'?'#fbbf24':'#f87171';
html += `
${QUALITY_NAMES[e.qual]}`;
- const catLabel = e.type==='talent'?'🌟 天赋':e.type==='skill_book'?'📖 技能书':e.type==='equip'?'🛡️ 装备':e.cat==='material'?'材料':e.cat==='processed'?'加工品':'特殊';
+ const catLabel = e.type==='talent'?'🌟 天赋':e.type==='skill_book'?'📖 技能书':e.type==='equip'?'🛡️ 装备':e.cat==='material'?'材料':e.cat==='processed'?'加工品':e.cat==='scatter'?'🎁 散落物':'特殊';
html += `
${catLabel}`;
} else if(e.type==='creature'){
html += `
🐾 生物${e.hab||''}${e.diet||''}${e.tame?'✅ 可驯养':'❌ 野性'}`;
diff --git a/world.html b/world.html
index 5d2875f..045ff9e 100644
--- a/world.html
+++ b/world.html
@@ -1519,6 +1519,10 @@ body { background: #0a0e17; overflow: hidden; font-family: 'Microsoft YaHei', sa
成就
+
@@ -1540,10 +1544,6 @@ body { background: #0a0e17; overflow: hidden; font-family: 'Microsoft YaHei', sa
高度
-
@@ -2464,6 +2464,15 @@ let microPanStart = null; // 平移起点 {x,y}
let microViewOpen = false; // 微观视图是否打开:打开时主世界 render 暂停重绘,避免双循环抢 CPU
let microHoverB = null; // 鼠标当前悬停的建筑(已过滤 road/ground);非空时在渲染层额外绘高亮 footprint
+// 宝箱系统:测试期每 10s 在微观沙盘随机生成,地块生物自动前往开启
+let microChests = []; // 当前地块活跃宝箱 [{id,cell_i,cell_j,tier,reward_type,reward_amount,_claiming}]
+let microFloatTexts = []; // 开启飘字 [{x,y,text,color,t,life}]
+let microChestGenTimer = null; // 测试期生成定时器(10s)
+const MICRO_CHEST_GEN_MS = 60000; // 固定 1 分钟生成一个
+const MICRO_CHEST_ATTRACT_R = 1e9; // 吸引半径(全浮岛,任意散点生物都能感知最近宝箱)
+const MICRO_CHEST_CLAIM_R = 30; // 触发开启半径(画布像素)
+const MICRO_CHEST_SPEED = 150; // 生物走向宝箱速度(画布像素/秒)
+
// 建造面板展开时:世界整体上移 + 等比缩小,腾出底部给面板(仅改变换,不缩放画布本身 → 无闪烁)
let microBuildOpenT = 0; // 0=收起 1=展开(每帧缓动逼近)
let microBuildScale = 1; // 当前世界缩放系数(建造展开时 <1)
@@ -2746,6 +2755,150 @@ async function microLoadGame() {
} catch (e) { /* 离线兜底:不阻塞微观世界 */ }
}
+// ============ 宝箱系统:生成 / 加载 / 开启 / 表现 ============
+// 宝箱用网格坐标 (cell_i,cell_j),与建筑同一等距坐标系;后端生成时已排除建筑占用格,天然不重叠。
+async function microLoadChests(q, r) {
+ if (q == null || r == null) return;
+ try {
+ const j = await API.chests(q, r);
+ if (j && j.ok && Array.isArray(j.chests)) {
+ microChests = j.chests.map(c => ({ ...c, _claiming: false }));
+ window.microChests = microChests; // 同步给 characters.js(否则其读到的是旧空数组引用)
+ }
+ } catch (e) { /* 离线兜底 */ }
+}
+
+async function microGenerateChest() {
+ if (!microState || !microState.tile) return;
+ const t = microState.tile;
+ const gridN = (microGridDims().cols) || 20;
+ try {
+ const j = await API.chestGenerate(t.q, t.r, gridN);
+ if (j && j.ok && j.chest) microChests.push({ ...j.chest, _claiming: false });
+ } catch (e) { /* 离线兜底 */ }
+}
+
+function microStartChestGen() {
+ if (microChestGenTimer) return;
+ microChestGenTimer = setInterval(microGenerateChest, MICRO_CHEST_GEN_MS);
+}
+function microStopChestGen() {
+ if (microChestGenTimer) { clearInterval(microChestGenTimer); microChestGenTimer = null; }
+ microChests = [];
+ window.microChests = microChests; // 同步给 characters.js
+ microFloatTexts = [];
+}
+
+// 生物到达宝箱后调用:乐观飘字 + 后端发奖 + 更新货币 HUD
+async function microClaimChest(ch, u) {
+ if (ch._claiming) return;
+ ch._claiming = true;
+ const sp = microCellToScreen(ch.cell_i + 0.5, ch.cell_j + 0.5);
+ microFloatTexts.push({
+ x: sp.x, y: sp.y - 42,
+ text: '+' + ch.reward_amount + microChestIcon(ch.reward_type),
+ color: microChestColor(ch.reward_type), t: 0, life: 1.2,
+ });
+ const idx = microChests.indexOf(ch);
+ if (idx >= 0) microChests.splice(idx, 1); // 立即移除:其他生物失去目标、回原位
+ try {
+ const j = await API.chestClaim(ch.id, u.id || '');
+ if (j && j.ok && j.balance != null && microChar) {
+ microChar[ch.reward_type] = j.balance;
+ microRenderCharHud();
+ }
+ } catch (e) { /* 离线兜底 */ }
+}
+
+// 暴露给外部角色系统(characters.js)以驱动精灵图生物抢宝箱
+window.microChests = microChests;
+window.microCellToScreen = microCellToScreen;
+window.microClaimChest = microClaimChest;
+
+function microChestIcon(t) {
+ return ({ copper: '🥉', silver: '🥈', gold: '🪙', diamond: '💎' })[t] || '🪙';
+}
+function microChestColor(t) {
+ return ({ copper: '#c8854a', silver: '#cfd6e0', gold: '#ffd23f', diamond: '#7ad7ff' })[t] || '#ffd23f';
+}
+function microChestGlow(tier) {
+ return ({ common: '#9aa4b2', rare: '#4aa3ff', epic: '#b46bff', legendary: '#ffb13f' })[tier] || '#9aa4b2';
+}
+function microChestEmoji(tier) {
+ return ({ common: '📦', rare: '🎁', epic: '🧰', legendary: '💠' })[tier] || '📦';
+}
+
+// 宝箱图片映射(正视图用于图鉴/面板,俯视图用于微观世界地图)
+const MICRO_CHEST_TOP_IMAGES = {
+ common: 'assets/icons/scatter/chest_wood_top.png?v=20260911',
+ rare: 'assets/icons/scatter/chest_silver_top.png?v=20260911',
+ epic: 'assets/icons/scatter/chest_gold_top.png?v=20260911',
+ legendary: 'assets/icons/scatter/chest_diamond_top.png?v=20260911',
+};
+const MICRO_CHEST_ICON_IMAGES = {
+ common: 'assets/icons/scatter/chest_wood_front.png?v=20260911',
+ rare: 'assets/icons/scatter/chest_silver_front.png?v=20260911',
+ epic: 'assets/icons/scatter/chest_gold_front.png?v=20260911',
+ legendary: 'assets/icons/scatter/chest_diamond_front.png?v=20260911',
+};
+const _chestImageCache = {};
+function getChestImage(url) {
+ if (!url) return null;
+ if (_chestImageCache[url]) return _chestImageCache[url];
+ const img = new Image();
+ img.src = url;
+ _chestImageCache[url] = img;
+ return img;
+}
+function microChestTopImage(tier) { return MICRO_CHEST_TOP_IMAGES[tier] || null; }
+function microChestIconImage(tier) { return MICRO_CHEST_ICON_IMAGES[tier] || null; }
+
+// 在网格层之上绘制所有活跃宝箱(呼吸光晕 + 漂浮主体/俯视图)
+function microDrawChests(c) {
+ if (!microChests.length) return;
+ const t = microState.animTime;
+ microChests.forEach(ch => {
+ const sp = microCellToScreen(ch.cell_i + 0.5, ch.cell_j + 0.5);
+ const glow = microChestGlow(ch.tier);
+ // 主体:优先使用俯视图 PNG,未加载成功则回退 emoji
+ 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.restore();
+ } else {
+ c.save();
+ c.translate(sp.x, sp.y - 14);
+ c.font = '30px sans-serif'; c.textAlign = 'center'; c.textBaseline = 'middle';
+ c.fillText(microChestEmoji(ch.tier), 0, 0);
+ c.restore();
+ }
+ });
+}
+
+// 开启飘字:上飘 + 淡出
+function microDrawFloatTexts(c, dt) {
+ for (let i = microFloatTexts.length - 1; i >= 0; i--) {
+ const f = microFloatTexts[i];
+ f.t += dt;
+ const p = f.t / f.life;
+ if (p >= 1) { microFloatTexts.splice(i, 1); continue; }
+ c.save();
+ c.globalAlpha = 1 - p;
+ c.font = 'bold 18px sans-serif'; c.textAlign = 'center'; c.textBaseline = 'middle';
+ c.lineWidth = 3; c.strokeStyle = 'rgba(0,0,0,0.6)';
+ const y = f.y - p * 38;
+ c.strokeText(f.text, f.x, y);
+ c.fillStyle = f.color; c.fillText(f.text, f.x, y);
+ c.restore();
+ }
+}
+
// ===== 玩家生物持久化恢复(生物属于玩家账号,存于 yt.world.creature,按 tile_ref 归属地块) =====
// 服务端生物记录 → 地块居民 unit 对象
function microMakeUnitFromCreature(x) {
@@ -4256,6 +4409,8 @@ function enterMicroWorld(tile) {
microLoadIcons(); // 进入微观视图即预加载建造贴图,避免已放置建筑先显示文字
microLoadCatalog(); // 拉取 Odoo 后台建造目录(未登录时保留前端兜底常量),hover/弹窗即时生效
microLoadGame(); // 拉取角色 / 生物 / 订单(Odoo 后端),覆盖单位状态
+ microLoadChests(tile.q, tile.r); // 拉取本地块已生成的宝箱
+ microStartChestGen(); // 启动测试期 10s 生成定时器
fetchTasks(); // 拉取任务定义 + 角色进度(新手主线)
// 资源系统:登录时一次性结算离线产出并兜底回库,再启动在线自动进账
microSettle(true, true).then(() => microRefreshResources());
@@ -4705,6 +4860,8 @@ function startMicroRender() {
const padX = Rx * 0.12, padY = Ry * 0.12, minD = Math.min(Rx, Ry) * 0.24;
const roadSet = microRoadKeySet();
const onRoad = roadSet.size > 0;
+ // 宝箱层(建筑之上、居民之下)
+ microDrawChests(microCtx);
if (!hasBgImage) {
microState.units.forEach((u, k) => {
// 由建筑层负责渲染的种族角色不再在这里画 emoji,避免重影
@@ -4723,14 +4880,58 @@ function startMicroRender() {
px = a.x + (b.x - a.x) * e; py = a.y + (b.y - a.y) * e;
const vx = b.x - a.x; if (Math.abs(vx) > 0.5) face = vx > 0 ? 1 : -1;
moving = (p.ci !== p.nci) || (p.cj !== p.ncj);
+ // 道路巡逻生物同样会被宝箱吸引:有宝箱时离开道路直奔宝箱,没有时继续巡逻
+ let tChest = null, tDist = Infinity;
+ for (const ch of microChests) {
+ const sp = microCellToScreen(ch.cell_i + 0.5, ch.cell_j + 0.5);
+ const d = Math.hypot(sp.x - px, sp.y - py);
+ if (d < tDist) { tDist = d; tChest = { x: sp.x, y: sp.y, ch }; }
+ }
+ if (tChest && tDist < MICRO_CHEST_ATTRACT_R) {
+ if (u._seekPx == null) { u._seekPx = px; u._seekPy = py; }
+ const dx = tChest.x - u._seekPx, dy = tChest.y - u._seekPy, dist = Math.hypot(dx, dy);
+ if (dist > MICRO_CHEST_CLAIM_R) {
+ const step = Math.min(dist, MICRO_CHEST_SPEED * dt);
+ u._seekPx += dx / dist * step; u._seekPy += dy / dist * step;
+ px = u._seekPx; py = u._seekPy; moving = true;
+ if (Math.abs(dx) > 0.5) face = dx > 0 ? 1 : -1;
+ } else {
+ microClaimChest(tChest.ch, u);
+ }
+ } else {
+ u._seekPx = null; u._seekPy = null;
+ }
} else {
const h = microHash(u.id);
const rx1 = ((h >> 2) % 100) / 100;
const ry1 = ((h >> 9) % 100) / 100;
- px = cx + (rx1 * 2 - 1) * (Rx - padX);
- py = cy + (ry1 * 2 - 1) * (Ry - padY);
- const dx_ = px - cx, dy_ = py - cy, d = Math.hypot(dx_, dy_);
- if (d < minD && d > 0.001) { const kk = minD / d; px = cx + dx_ * kk; py = cy + dy_ * kk; }
+ const homeX = cx + (rx1 * 2 - 1) * (Rx - padX);
+ const homeY = cy + (ry1 * 2 - 1) * (Ry - padY);
+ const dxh = homeX - cx, dyh = homeY - cy, dh = Math.hypot(dxh, dyh);
+ const hx = (dh < minD && dh > 0.001) ? cx + dxh * (minD / dh) : homeX;
+ const hy = (dh < minD && dh > 0.001) ? cy + dyh * (minD / dh) : homeY;
+ if (u._px == null) { u._px = hx; u._py = hy; }
+ // 宝箱吸引:所有散点生物寻找最近活跃宝箱并前往开启
+ let tgt = null, bd = Infinity;
+ for (const ch of microChests) {
+ const sp = microCellToScreen(ch.cell_i + 0.5, ch.cell_j + 0.5);
+ const d = Math.hypot(sp.x - u._px, sp.y - u._py);
+ if (d < bd) { bd = d; tgt = { x: sp.x, y: sp.y, ch }; }
+ }
+ if (tgt && bd < MICRO_CHEST_ATTRACT_R) {
+ const dx = tgt.x - u._px, dy = tgt.y - u._py, dist = Math.hypot(dx, dy);
+ if (dist > MICRO_CHEST_CLAIM_R) {
+ const step = Math.min(dist, MICRO_CHEST_SPEED * dt);
+ u._px += dx / dist * step; u._py += dy / dist * step;
+ moving = true;
+ if (Math.abs(dx) > 0.5) face = dx > 0 ? 1 : -1;
+ } else {
+ microClaimChest(tgt.ch, u); // 到达 → 开启(内部移除宝箱,生物随后回原位)
+ }
+ } else {
+ u._px += (hx - u._px) * 0.04; u._py += (hy - u._py) * 0.04;
+ }
+ px = u._px; py = u._py;
}
// 行走 bob(缓动上下浮动)+ 呼吸脉冲(停步时)
const bob = moving ? Math.abs(Math.sin(microState.animTime * 6 + k)) * (Math.min(Rx, Ry) * 0.03) : 0;
@@ -4764,6 +4965,8 @@ function startMicroRender() {
// 大气粒子层(背景图模式跳过,保持纯净背景图)
if (!hasBgImage) drawMicroAtmoParticles(microCtx, mW, mH, terrain);
+ // 开启飘字(仍在变换内,跟随缩放)
+ microDrawFloatTexts(microCtx, dt);
// ---- 结束变换:恢复画布坐标系,绘制 UI 层不受 zoom 影响 ----
microCtx.restore();
@@ -4944,11 +5147,7 @@ function onMicroDown(e) {
charPlaceMode = false;
return;
}
- // 选中角色后:点击地面直接移动(无需再点按钮)
- if (typeof selectedCharId !== 'undefined' && selectedCharId && typeof setCharTarget === 'function') {
- setCharTarget(selectedCharId, w.x, w.y);
- return;
- }
+ // (已移除)玩家点击地面控制生物移动:selectChar 后点击地面不再 setCharTarget(按需求取消玩家移动控制);保留选中/打开面板能力
}
// 放大状态下(zoom>1)左键拖拽 = 平移查看浮岛内部细节;
// 仅初始大小(zoom=1)时左键拖拽才是滑动切相邻地块
@@ -5187,6 +5386,7 @@ function setMicroTile(tile) {
microAutoRoadCells(); // 切地块时重新计算自动道路贴图
microSanitizeBuildings(); // 切地块时清理越界建筑
microRefreshResources(); // 切地块时刷新建筑囤积徽标缓存
+ microLoadChests(tile.q, tile.r); // 切地块时刷新该地块宝箱
}
// 快照旧视图 → 切换地块 → 重建缓存 → 启动滑入过渡
function transitionMicroTo(tile, dx, dy) {
@@ -6869,6 +7069,7 @@ function exitMicroWorld() {
microBuildRevealAt = 0;
microZoom = 1; microPanX = 0; microPanY = 0;
microPanning = false; microPanStart = null;
+ microStopChestGen(); // 停止测试期生成定时器 + 清空宝箱/飘字
microViewOpen = false; // 恢复主世界重绘
}
@@ -8176,7 +8377,7 @@ window.addEventListener('keydown', e => {
// 进入时一次性把世界铺满视口并钳制边界(fitWorld 内部已调 clampCamera)。
if (typeof fitWorld === 'function') fitWorld();
-
+
@@ -8604,7 +8805,7 @@ if (typeof fitWorld === 'function') fitWorld();
}catch(e){}
}
-
+