@@ -1903,6 +1908,7 @@ async function microLoadCatalog() {
if (it.statLevel) t.statLevel = it.statLevel;
if (it.statHp) t.statHp = it.statHp;
if (it.statYield && Object.keys(it.statYield).length) t.statYield = it.statYield;
+ if (Array.isArray(it.actions) && it.actions.length) t.actions = it.actions; // 后端动作优先
});
microCatalogLoaded = true;
} catch (e) { /* 未登录/网络不可达:保留兜底常量 */ }
@@ -3045,6 +3051,39 @@ function enterMicroWorld(tile) {
microLoadIcons(); // 进入微观视图即预加载建造贴图,避免已放置建筑先显示文字
microLoadCatalog(); // 拉取 Odoo 后台建造目录(未登录时保留前端兜底常量),hover/弹窗即时生效
microLoadGame(); // 拉取角色 / 生物 / 订单(Odoo 后端),覆盖单位状态
+ // 初始化角色动画系统
+ if (typeof preloadAllSheets === 'function') preloadAllSheets();
+ if (typeof updateCharPlacePanel === 'function') updateCharPlacePanel();
+ if (typeof updateCharPanel === 'function') updateCharPanel();
+ if (typeof loadMicroChars === 'function') loadMicroChars(); // 从后端拉取该用户放置的角色(刷新/重进不丢)
+ // 注册角色障碍避让:随机游走 / 用户点击都被吸附到非建筑空地
+ if (typeof setCharAvoid === 'function') {
+ setCharAvoid({
+ isBlocked: (wx, wy) => {
+ const sp = worldToScreen(wx, wy);
+ const cell = microCellAt(sp);
+ return !!(cell && microBuildingAt(cell));
+ },
+ // 螺旋搜索最近空地:从目标点向外 16 方向 × 12 圈,最多 192 个候选点
+ findClear: (wx, wy) => {
+ const isFree = (x, y) => {
+ const sp = worldToScreen(x, y);
+ const cell = microCellAt(sp);
+ return !(!cell || microBuildingAt(cell));
+ };
+ for (let r = 0; r < 12; r++) {
+ for (let i = 0; i < 16; i++) {
+ const ang = (i / 16) * Math.PI * 2;
+ const rad = r * 6;
+ const x = wx + Math.cos(ang) * rad;
+ const y = wy + Math.sin(ang) * rad;
+ if (isFree(x, y)) return { x, y };
+ }
+ }
+ return { x: wx, y: wy }; // 实在找不到就原样返回(极端兜底)
+ },
+ });
+ }
initMicroCanvas();
startMicroRender();
microSanitizeBuildings(); // 进入即清理本地块越界建筑(登录期 microState 未建,setMicroTile 不跑初始地块)
@@ -3128,10 +3167,16 @@ function startMicroRender() {
function loop() {
if (!microState) return;
microAnimId = requestAnimationFrame(loop);
- microState.animTime += 0.016;
+ const now = performance.now();
+ const dt = Math.min((now - (loop._last || now)) / 1000, 0.05);
+ loop._last = now;
+ microState.animTime += dt;
const mW = microCanvas.width, mH = microCanvas.height;
const terrain = TERRAIN[microState.tile.terrain];
+ // ---- 更新角色动画与位置 ----
+ if (typeof updateChars === 'function') updateChars(dt);
+
// 建造面板展开时:世界整体上移 + 等比缩小,腾出底部给面板(仅改变换,画布尺寸不变 → 无闪烁)
const boTarget = microBuildMode ? 1 : 0;
microBuildOpenT += (boTarget - microBuildOpenT) * 0.18;
@@ -3384,6 +3429,9 @@ function startMicroRender() {
microCtx.globalAlpha = 1;
}
+ // ---- 渲染角色(精灵图动画)----
+ if (typeof renderChars === 'function') renderChars(microCtx, microState.animTime, mW, mH);
+
// 大气粒子层(背景图模式跳过,保持纯净背景图)
if (!hasBgImage) drawMicroAtmoParticles(microCtx, mW, mH, terrain);
@@ -3426,6 +3474,15 @@ function screenToWorld(sx, sy) {
y: (sy - mH / 2 - microPanY + microBuildLift) / s + mH / 2
};
}
+// 将世界坐标还原为屏幕坐标(screenToWorld 的逆),用于角色碰撞检测
+function worldToScreen(wx, wy) {
+ const mW = microCanvas.width, mH = microCanvas.height;
+ const s = microZoom * microBuildScale;
+ return {
+ x: (wx - mW / 2) * s + mW / 2 + microPanX,
+ y: (wy - mH / 2) * s + mH / 2 + microPanY - microBuildLift,
+ };
+}
// 平移限位:像缩放下限一样,浮岛不能拖出视野
// zoom=1 时限位为 0(浮岛居中、不可平移);zoom>1 时允许平移到「浮岛边缘抵达屏幕边」为止
function clampMicroPan() {
@@ -3518,15 +3575,49 @@ function onMicroDown(e) {
}
return;
}
- // 中键/右键 → 进入平移模式;左键 → 切地块滑动
- if (e.button === 1 || e.button === 2) { // 中键 or 右键
+ // 中键 → 纯平移;右键 → 单击取消选中角色,按住拖拽仍是平移
+ if (e.button === 1) {
e.preventDefault();
microPanning = true;
microPanStart = getMicroPoint(e);
microCanvas.style.cursor = 'move';
return; // 不启动 swipe
}
+ if (e.button === 2) {
+ e.preventDefault();
+ // 右键单击 = 取消选中角色(RTS 习惯)
+ if (typeof selectChar === 'function' && typeof selectedCharId !== 'undefined' && selectedCharId) {
+ selectChar(null);
+ }
+ microPanning = true; // 右键拖拽仍可平移视图
+ microPanStart = getMicroPoint(e);
+ microCanvas.style.cursor = 'move';
+ return;
+ }
const p = getMicroPoint(e);
+ // 角色交互优先(非建造模式、左键)
+ if (!microBuildMode && e.button === 0 && typeof hitChar === 'function') {
+ const w = screenToWorld(p.x, p.y);
+ const ch = hitChar(w.x, w.y);
+ if (ch) {
+ // 点击已选中的角色 → 取消选中;点击其他角色 → 切换选中
+ if (typeof selectedCharId !== 'undefined' && selectedCharId === ch.id) selectChar(null);
+ else selectChar(ch.id);
+ e.preventDefault(); return;
+ }
+ // 放置模式:点击地面放置角色(优先级高于移动)
+ if (typeof charPlaceMode !== 'undefined' && charPlaceMode && typeof createChar === 'function') {
+ createChar(charPlaceRace, w.x, w.y);
+ charPlaceMode = false;
+ if (typeof updateCharPlacePanel === 'function') updateCharPlacePanel();
+ return;
+ }
+ // 选中角色后:点击地面直接移动(无需再点按钮)
+ if (typeof selectedCharId !== 'undefined' && selectedCharId && typeof setCharTarget === 'function') {
+ setCharTarget(selectedCharId, w.x, w.y);
+ return;
+ }
+ }
// 放大状态下(zoom>1)左键拖拽 = 平移查看浮岛内部细节;
// 仅初始大小(zoom=1)时左键拖拽才是滑动切相邻地块
if (microZoom > 1.001) {
@@ -4269,7 +4360,10 @@ function showBuildModal(b) {
if (b.exp == null) b.exp = 0;
if (b.hp == null) b.hp = it.statHp || 100;
microOpenBuilding = b;
- const cfg = BUILD_INTERACT[b.type] || { title: it.name, tagline: it.desc || '', actions: [] };
+ // 优先用后端目录(micro_catalog)同步来的动作;没有则回退前端兜底 BUILD_INTERACT
+ const cfg = (it.actions && it.actions.length)
+ ? { title: it.name, tagline: it.desc || '', actions: it.actions }
+ : (BUILD_INTERACT[b.type] || { title: it.name, tagline: it.desc || '', actions: [] });
const name = it.name || b.type;
const img = (typeof it.icon === 'string')
? `

` : '';
@@ -4281,10 +4375,11 @@ function showBuildModal(b) {
const hpPct = Math.max(0, Math.min(100, Math.round(b.hp / (it.statHp || 100) * 100)));
const hpColor = hpPct > 50 ? '#8bc34a' : hpPct > 25 ? '#ff9800' : '#f44336';
const acts = (cfg.actions || []).map(a =>
- `
${a.icon} ${a.label}
${a.desc}
`
+ `
${a.icon} ${a.label}
${a.desc}
`
).join('');
document.getElementById('modalTitle').textContent = `${name} · Lv.${b.level}`;
- document.getElementById('modalBody').innerHTML = `
+ const body = document.getElementById('modalBody');
+ body.innerHTML = `
${img}
${it.desc ? `
${it.desc}
` : ''}
类别${microCatName(it.cat)}
@@ -4296,6 +4391,11 @@ function showBuildModal(b) {
`;
+ body.querySelectorAll('.cmdBtn').forEach(btn => {
+ btn.addEventListener('click', () => {
+ microBuildingAct(btn.dataset.btype, btn.dataset.baction, btn.dataset.blabel);
+ });
+ });
document.getElementById('unitModal').classList.add('active');
}
@@ -4303,7 +4403,8 @@ function showBuildModal(b) {
async function microBuildingAct(type, action, label) {
const tile = microState && microState.tile;
if (!tile) return;
- const res = await API.buildingAction({ code: type, action, q: tile.q, r: tile.r });
+ const bpos = microOpenBuilding || {};
+ const res = await API.buildingAction({ code: type, action, q: tile.q, r: tile.r, i: bpos.i, j: bpos.j });
if (!res || res.ok === false) {
const msg = res && res.error === 'no_gold' ? '金币不足'
: (res && res.error === 'no_res' ? ('资源不足:' + (res.res || '')) : '操作失败(未登录或网络异常)');
@@ -4313,10 +4414,10 @@ async function microBuildingAct(type, action, label) {
if (res.gold != null) { microChar = microChar || {}; microChar.gold = res.gold; }
if (res.inventory && microChar) microChar.inventory = res.inventory;
microRenderCharHud();
- // 升级:抬高该建筑等级并持久化
- if (action === 'upgrade' && microOpenBuilding) {
- microOpenBuilding.level = (microOpenBuilding.level || 1) + 1;
- microOpenBuilding.exp = (microOpenBuilding.exp || 0) + 50;
+ // 升级:用后端落库后的权威等级/经验回写并持久化
+ if (action === 'upgrade' && microOpenBuilding && res.level != null) {
+ microOpenBuilding.level = res.level;
+ microOpenBuilding.exp = res.exp;
scheduleBuildSave();
}
// 治疗:回满本村所有生物生命
@@ -5105,9 +5206,27 @@ document.addEventListener('keydown', e => { if (e.key === 'Escape') exitMicroWor
document.getElementById('backBtn').addEventListener('click', exitMicroWorld);
document.getElementById('unitModal').addEventListener('click', e => { if (e.target === document.getElementById('unitModal')) closeUnitModal(); });
+// WASD 方向键:选中角色后控制移动(8 方向,相反键抵消)
+const wasdKeys = { w: false, a: false, s: false, d: false };
+window.addEventListener('keydown', e => {
+ const k = e.key.toLowerCase();
+ if (k === 'w' || k === 'a' || k === 's' || k === 'd') {
+ const tag = (e.target && e.target.tagName || '').toLowerCase();
+ if (tag === 'input' || tag === 'textarea') return; // 输入框聚焦时不响应
+ wasdKeys[k] = true;
+ e.preventDefault(); // 阻止页面滚动
+ }
+});
+window.addEventListener('keyup', e => {
+ const k = e.key.toLowerCase();
+ if (k in wasdKeys) wasdKeys[k] = false;
+});
+window.wasdKeys = wasdKeys;
+
function exitMicroWorld() {
document.getElementById('microView').classList.remove('active');
if (microAnimId) { cancelAnimationFrame(microAnimId); microAnimId = null; }
+ if (typeof saveMicroCharsNow === 'function') saveMicroCharsNow(); // 退出前落库角色(删除/移动)
microState = null;
microGridCache = null;
microBuildRevealAt = 0;
@@ -5388,7 +5507,7 @@ function microCommitPlace() {
const g = microComputeGhost();
if (!g || !g.valid) { microToast('这里不能建造(红色区域)'); return; }
const it = microBuildItemById(microBuildSel);
- microBuildList().push({ i: g.i, j: g.j, type: microBuildSel, rot: microBuildRot, w: it.w || 1, h: it.h || 1 });
+ microBuildList().push({ i: g.i, j: g.j, type: microBuildSel, rot: microBuildRot, w: it.w || 1, h: it.h || 1, level: 1, exp: 0, hp: (it.statHp || 100) });
scheduleBuildSave();
}
// 右键:命中建筑→删除;否则取消当前选择/编辑
@@ -6247,6 +6366,7 @@ window.addEventListener('keydown', e => {
if (typeof fitWorld === 'function') fitWorld();
+