任务系统,新手引导
This commit is contained in:
+336
-26
@@ -558,10 +558,25 @@ body { background: #0a0e17; overflow: hidden; font-family: 'Microsoft YaHei', sa
|
||||
#appModal.active { display: flex; }
|
||||
#appModal .modalContent {
|
||||
background: #1a2030; border: 1px solid rgba(100,200,255,0.3); border-radius: 12px;
|
||||
padding: 24px; max-width: 360px; width: 90%; pointer-events: auto;
|
||||
padding: 24px; max-width: 360px; width: 90%; max-height: 80vh; pointer-events: auto;
|
||||
display: flex; flex-direction: column;
|
||||
}
|
||||
#appModal .modalTitle { font-size: 16px; font-weight: bold; color: #e0e0e0; margin-bottom: 16px; flex-shrink: 0; }
|
||||
#appModal .modalBody {
|
||||
color: #cfd5ea; font-size: 14px; line-height: 1.6; overflow-y: auto; padding-right: 4px;
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: rgba(100,180,255,0.35) rgba(255,255,255,0.05);
|
||||
}
|
||||
#appModal .modalBody::-webkit-scrollbar { width: 6px; }
|
||||
#appModal .modalBody::-webkit-scrollbar-track {
|
||||
background: rgba(255,255,255,0.05); border-radius: 4px; margin: 4px 0;
|
||||
}
|
||||
#appModal .modalBody::-webkit-scrollbar-thumb {
|
||||
background: rgba(100,180,255,0.35); border-radius: 4px;
|
||||
}
|
||||
#appModal .modalBody::-webkit-scrollbar-thumb:hover {
|
||||
background: rgba(100,200,255,0.55);
|
||||
}
|
||||
#appModal .modalTitle { font-size: 16px; font-weight: bold; color: #e0e0e0; margin-bottom: 16px; }
|
||||
#appModal .modalBody { color: #cfd5ea; font-size: 14px; line-height: 1.6; }
|
||||
#appModal .infoRow { display: flex; justify-content: space-between; margin: 8px 0; font-size: 13px; }
|
||||
#appModal .infoLabel { color: #8899aa; }
|
||||
#appModal .infoValue { color: #e0e0e0; }
|
||||
@@ -871,7 +886,7 @@ body { background: #0a0e17; overflow: hidden; font-family: 'Microsoft YaHei', sa
|
||||
<button class="backBtn" id="backBtn">← 返回世界</button>
|
||||
<span class="microTitle" id="microTitle">平原村落</span>
|
||||
<span class="microSubtitle" id="microSubtitle">微观世界视角</span>
|
||||
<div id="charHud" style="display:none;margin-left:auto;padding:6px 12px;background:rgba(0,0,0,.35);border:1px solid rgba(100,200,255,.3);border-radius:10px;font-size:13px;color:#e0e8f2;"></div>
|
||||
<div id="charHud" style="display:none;margin-left:auto;min-width:200px;padding:6px 12px;background:rgba(0,0,0,.35);border:1px solid rgba(100,200,255,.3);border-radius:10px;font-size:13px;color:#e0e8f2;"></div>
|
||||
</div>
|
||||
<div id="microBody">
|
||||
<div id="microTopBar">
|
||||
@@ -1749,22 +1764,22 @@ const MICRO_BUILD_ITEMS = [
|
||||
// 建筑 / 设施 目录(兜底:未登录或 Odoo 未提供时直接用此;登录后由 /yt_world/api/micro/catalog 覆盖)。
|
||||
// flat=true 表示平面俯视,岛上压成菱形贴地;否则为 3/4 立绘,竖直站立。
|
||||
const MICRO_BUILD_EXTRA = [
|
||||
{ id: 'cottage', cat: 'build', icon: 'assets/buildings/spring/cottage/cottage_l.png?v=4', name: '农舍', desc: '村民起居的基本居所,提供人口上限与夜间休息。', flat: false, w: 2, h: 2, statLevel: 1, statHp: 120, statYield: { population: 4 } },
|
||||
{ id: 'windmill', cat: 'build', icon: 'assets/buildings/spring/windmill/windmill_l.png?v=4', name: '风车', desc: '利用风力磨粉与提水,提升粮食加工效率。', flat: false, w: 3, h: 3, statLevel: 1, statHp: 110, statYield: { flour: 2 } },
|
||||
{ id: 'cottage', cat: 'build', icon: 'assets/buildings/spring/cottage/cottage_l.png?v=6', name: '农舍', desc: '村民起居的基本居所,提供人口上限与夜间休息。', flat: false, w: 2, h: 2, statLevel: 1, statHp: 120, statYield: { population: 4 } },
|
||||
{ id: 'windmill', cat: 'build', icon: 'assets/buildings/spring/windmill/windmill_l.png?v=5', name: '风车', desc: '利用风力磨粉与提水,提升粮食加工效率。', flat: false, w: 3, h: 3, statLevel: 1, statHp: 110, statYield: { flour: 2 } },
|
||||
{ id: 'blacksmith', cat: 'build', icon: 'assets/buildings/spring/blacksmith/blacksmith_l.png?v=5',name: '铁匠铺', desc: '锻造工具与武器,产出金属制品。', flat: false, w: 2, h: 2, statLevel: 1, statHp: 130, statYield: { metal: 2 } },
|
||||
{ id: 'grocery', cat: 'build', icon: 'assets/buildings/spring/grocery/grocery_l.png?v=5', name: '杂货铺', desc: '交易日用物资的小型商铺,促进经济流通。', flat: false, w: 2, h: 2, statLevel: 1, statHp: 90, statYield: { coin: 2 } },
|
||||
{ id: 'temple', cat: 'build', icon: 'assets/buildings/spring/temple/temple_l.png?v=4', name: '神庙', desc: '供奉信仰的场所,提升居民精神状态与领地声望。', flat: false, w: 3, h: 3, statLevel: 1, statHp: 160, statYield: { prestige: 3 } },
|
||||
{ id: 'clinic', cat: 'build', icon: 'assets/buildings/spring/clinic/clinic_l.png?v=5', name: '医务室', desc: '诊治伤病,恢复居民生命值。', flat: false, w: 2, h: 2, statLevel: 1, statHp: 100, statYield: { heal: 2 } },
|
||||
{ id: 'restaurant', cat: 'build', icon: 'assets/buildings/spring/restaurant/restaurant_l.png?v=4',name: '酒馆', desc: '烹制料理,为居民提供饱食与心情加成。', flat: false, w: 2, h: 3, statLevel: 1, statHp: 95, statYield: { satiety: 3 } },
|
||||
{ id: 'pasture', cat: 'business', icon: 'assets/buildings/spring/pasture/pasture_l.png?v=8', name: '牧场', desc: '放养牲畜,稳定产出肉奶类资源。', flat: false, w: 4, h: 4, statLevel: 1, statHp: 105, statYield: { meat: 2, milk: 1 } },
|
||||
{ id: 'restaurant', cat: 'build', icon: 'assets/buildings/spring/restaurant/restaurant_l.png?v=5',name: '酒馆', desc: '烹制料理,为居民提供饱食与心情加成。', flat: false, w: 2, h: 3, statLevel: 1, statHp: 95, statYield: { satiety: 3 } },
|
||||
{ id: 'pasture', cat: 'business', icon: 'assets/buildings/spring/pasture/pasture_l.png?v=13', name: '牧场', desc: '放养牲畜,稳定产出肉奶类资源。', flat: false, w: 4, h: 4, statLevel: 1, statHp: 105, statYield: { meat: 2, milk: 1 } },
|
||||
{ id: 'blackmarket', cat: 'build', icon: 'assets/buildings/spring/blackmarket/blackmarket_l.png?v=7',name:'商城', desc: '灰色交易点,可换取稀有物资但伴随风险。', flat: false, w: 2, h: 2, statLevel: 1, statHp: 80, statYield: { rare: 1 } },
|
||||
{ id: 'farmland', cat: 'business', icon: 'assets/buildings/spring/farmland/farmland.png', name: '农田', desc: '春季播种的耕地,定期产出粮食作物。', flat: true, w: 2, h: 2, statLevel: 1, statHp: 60, statYield: { food: 3 } },
|
||||
{ id: 'well', cat: 'build', icon: 'assets/buildings/default/props/well_l.png', name: '水井', desc: '汲取地下水的圆井,保障饮水与灌溉。', flat: false, w: 2, h: 2, statLevel: 1, statHp: 90, statYield: { water: 2 } },
|
||||
{ id: 'school', cat: 'build', icon: 'assets/buildings/spring/school/school_l.png?v=4', name: '学堂', desc: '授业解惑之所,研读典籍提升领地科研与基础教学产出。', flat: false, w: 3, h: 2, statLevel: 1, statHp: 100, statYield: { research: 3, prestige: 1 } },
|
||||
{ id: 'school', cat: 'build', icon: 'assets/buildings/spring/school/school_l.png?v=6', name: '学堂', desc: '授业解惑之所,研读典籍提升领地科研与基础教学产出。', flat: false, w: 3, h: 2, statLevel: 1, statHp: 100, statYield: { research: 3, prestige: 1 } },
|
||||
{ id: 'library', cat: 'build', icon: 'assets/buildings/spring/library/library_l.png?v=5', name: '图书馆', desc: '收藏典籍与卷轴,提升领地科研与知识产出。', flat: false, w: 2, h: 3, statLevel: 1, statHp: 110, statYield: { research: 4 } },
|
||||
{ id: 'barracks', cat: 'build', icon: 'assets/buildings/spring/barracks/barracks_l.png?v=6', name: '兵营', desc: '训练士兵与储备武备的军事建筑,提升领地军事产出。', flat: false, w: 3, h: 3, statLevel: 1, statHp: 130, statYield: { military: 4 } },
|
||||
{ id: 'guild', cat: 'build', icon: 'assets/buildings/spring/guild/guild_l.png?v=4', name: '公会', desc: '冒险者与住民集结之所,可发布委托与招募成员。', flat: false, w: 2, h: 2, statLevel: 1, statHp: 140, statYield: { prestige: 2, coin: 1 } },
|
||||
{ id: 'village', cat: 'build', icon: 'assets/buildings/spring/village/village_l.png?v=4', name: '村庄', desc: '族群聚落的核心,提升人口上限与基础产出。', flat: false, w: 3, h: 3, statLevel: 1, statHp: 150, statYield: { population: 6, food: 1 } },
|
||||
{ id: 'village', cat: 'build', icon: 'assets/buildings/spring/village/village_l.png?v=5', name: '村庄', desc: '族群聚落的核心,提升人口上限与基础产出。', flat: false, w: 3, h: 3, statLevel: 1, statHp: 150, statYield: { population: 6, food: 1 } },
|
||||
// 生物分类:9 种族角色(点击进入"点地面放置"模式,不进入建筑模式)
|
||||
{ id: 'race_human', cat: 'bio', icon: 'assets/characters/human/human_thumb.png', name: '人族', isChar: true, race: 'human' },
|
||||
{ id: 'race_demon', cat: 'bio', icon: 'assets/characters/demon/demon_thumb.png', name: '妖族', isChar: true, race: 'demon' },
|
||||
@@ -1922,6 +1937,7 @@ async function microLoadCatalog() {
|
||||
if (Array.isArray(it.actions) && it.actions.length) t.actions = it.actions; // 后端动作优先
|
||||
});
|
||||
microCatalogLoaded = true;
|
||||
microLoadIcons(); // 目录覆盖 icon 后,用最终(干净 URL)的图标重新预加载,避免带 ?v= 缓存号与后端干净 URL 不一致导致建筑不显示
|
||||
} catch (e) { /* 未登录/网络不可达:保留兜底常量 */ }
|
||||
}
|
||||
|
||||
@@ -1954,7 +1970,11 @@ async function microLoadGame() {
|
||||
|
||||
// 持久化当前地块全部单位的角色化状态(等级/经验/装备/生命)到 Odoo
|
||||
async function microSaveCreatures() {
|
||||
if (!microState || !microState.units || !microState.units.length) return;
|
||||
if (!microState || !microState.units) return;
|
||||
// 注意:_knownUnitCids 在 setMicroTile 加载服务端居民时统一种子,
|
||||
// 不要在这里用 microState.units 做延迟种子,否则刚放置的新角色 cid
|
||||
// 会被提前标记为"已知",导致 char_recruited 事件漏发。
|
||||
if (!microState.units.length) return;
|
||||
const list = microState.units.map(u => ({
|
||||
cid: u.id, name: u.name, species: u.role || 'none',
|
||||
level: u.level, exp: u.exp, hp: u.hp, hp_max: u.hp_max || 120,
|
||||
@@ -1965,7 +1985,15 @@ async function microSaveCreatures() {
|
||||
}));
|
||||
const res = await API.saveCreatures(list);
|
||||
if (res && res.ok && Array.isArray(res.creatures)) {
|
||||
res.creatures.forEach(x => { if (x.cid) microCreatureOverlay[x.cid] = x; });
|
||||
const roleByCid = {};
|
||||
list.forEach(u => { if (u.cid) roleByCid[u.cid] = u.role || u.species; });
|
||||
res.creatures.forEach(x => {
|
||||
if (x.cid) microCreatureOverlay[x.cid] = x;
|
||||
if (x.cid && !_knownUnitCids.has(x.cid)) {
|
||||
_knownUnitCids.add(x.cid);
|
||||
emitTaskEvent('char_recruited', { race: roleByCid[x.cid] || 'human' });
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1975,8 +2003,212 @@ function microRenderCharHud() {
|
||||
if (!el) return;
|
||||
if (!microChar) { el.style.display = 'none'; return; }
|
||||
el.style.display = '';
|
||||
el.innerHTML = `🪙 <b>${microChar.gold != null ? microChar.gold : 0}</b> · ⭐ Lv.${microChar.level || 1}`
|
||||
+ (microChar.name ? ` · ${microChar.name}` : '');
|
||||
const gold = microChar.gold != null ? microChar.gold : 0;
|
||||
const lv = microChar.level || 1;
|
||||
const exp = microChar.exp != null ? microChar.exp : 0;
|
||||
const expMax = microChar.exp_max && microChar.exp_max > 0 ? microChar.exp_max : 100;
|
||||
const pct = Math.max(0, Math.min(100, Math.round(exp / expMax * 100)));
|
||||
const name = microChar.name ? microChar.name : '';
|
||||
el.innerHTML = `
|
||||
<div style="display:flex;align-items:center;gap:8px;white-space:nowrap;">
|
||||
<span>🪙 <b>${gold}</b></span>
|
||||
<span style="color:#ffd27f;">⭐ Lv.${lv}</span>
|
||||
${name ? `<span style="color:#a9c7e0;">${name}</span>` : ''}
|
||||
</div>
|
||||
<div style="display:flex;align-items:center;gap:6px;margin-top:5px;">
|
||||
<div style="position:relative;flex:1;height:7px;background:rgba(255,255,255,.12);border-radius:5px;overflow:hidden;">
|
||||
<div style="position:absolute;left:0;top:0;bottom:0;width:${pct}%;background:linear-gradient(90deg,#3a7afe,#46e0c8);border-radius:5px;transition:width .35s ease;"></div>
|
||||
</div>
|
||||
<span style="font-size:11px;color:#9fb0c0;white-space:nowrap;">${exp}/${expMax} (${pct}%)</span>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
// ============ 任务系统(新手主线,后端驱动) ============
|
||||
let taskData = null; // {chapters, tasks, char}
|
||||
let _knownUnitCids = new Set(); // 已记录的单位 cid,用于检测"新入驻居民"
|
||||
// 建筑动作 → 产出的资源(用于发射 resource_collected 事件)
|
||||
const TASK_RES = {
|
||||
'farmland_harvest': 'food', 'farmland_sow': 'food', 'farmland_upgrade': 'food',
|
||||
'windmill_collect': 'flour', 'windmill_produce': 'food',
|
||||
'pasture_breed': 'meat', 'well_draw': 'water', 'blacksmith_forge': 'metal',
|
||||
'grocery_restock': 'food', 'market_stall': 'gold',
|
||||
};
|
||||
|
||||
// 发射游戏事件到服务端事件总线,推进任务进度;返回后刷新本地任务数据与(开着的)面板
|
||||
async function emitTaskEvent(event, payload) {
|
||||
try {
|
||||
const r = await API.taskProgress(event, payload || {});
|
||||
if (r && r.ok && r.tasks) {
|
||||
taskData = { chapters: r.chapters, tasks: r.tasks, char: r.char };
|
||||
if (microChar && r.char) {
|
||||
microChar.level = r.char.level; microChar.exp = r.char.exp;
|
||||
microChar.exp_max = r.char.exp_max; microChar.gold = r.char.gold;
|
||||
microRenderCharHud();
|
||||
}
|
||||
const body = document.getElementById('taskPanelBody');
|
||||
if (body) {
|
||||
body.innerHTML = renderTaskPanel();
|
||||
document.querySelectorAll('.task-claim-btn').forEach(b => {
|
||||
b.onclick = () => claimTask(b.getAttribute('data-code'), b);
|
||||
});
|
||||
}
|
||||
(r.claimable || []).forEach(code => {
|
||||
const t = (r.tasks || []).find(x => x.code === code);
|
||||
toast('🎯 新任务可领取:' + (t ? t.title : code));
|
||||
});
|
||||
}
|
||||
} catch (e) { /* 离线兜底 */ }
|
||||
}
|
||||
|
||||
async function fetchTasks() {
|
||||
try {
|
||||
const r = await API.tasks();
|
||||
if (r && r.ok) {
|
||||
taskData = r;
|
||||
if (r.char) {
|
||||
// 主世界登录时 microChar 可能尚未创建,这里兜底初始化,避免个人信息面板 fallback 到旧数据
|
||||
if (!microChar) {
|
||||
microChar = {
|
||||
name: r.char.name, level: r.char.level, exp: r.char.exp,
|
||||
exp_max: r.char.exp_max, gold: r.char.gold,
|
||||
equip: { weapon: null, armor: null, accessory: null },
|
||||
inventory: {}
|
||||
};
|
||||
} else {
|
||||
microChar.name = r.char.name;
|
||||
microChar.level = r.char.level;
|
||||
microChar.exp = r.char.exp;
|
||||
microChar.exp_max = r.char.exp_max;
|
||||
microChar.gold = r.char.gold;
|
||||
}
|
||||
microRenderCharHud();
|
||||
}
|
||||
}
|
||||
} catch (e) { /* 离线兜底 */ }
|
||||
}
|
||||
|
||||
function renderTaskPanel() {
|
||||
if (!taskData || !taskData.tasks) {
|
||||
return '<div style="text-align:center;padding:24px;color:#8899aa;font-size:13px;">任务加载中…</div>';
|
||||
}
|
||||
const stateLabel = {
|
||||
done: '✅ 已完成', claimable: '🎁 可领取',
|
||||
in_progress: '🟢 进行中', locked: '🔒 未解锁',
|
||||
};
|
||||
let html = '';
|
||||
taskData.chapters.forEach(ch => {
|
||||
const list = taskData.tasks.filter(t => t.chapter === ch);
|
||||
if (!list.length) return;
|
||||
html += `<div style="margin:14px 0 6px;font-weight:600;color:#cdd9e5;">第 ${ch} 章</div>`;
|
||||
list.forEach(t => {
|
||||
const locked = t.state === 'locked';
|
||||
const done = t.state === 'done';
|
||||
const claimable = t.state === 'claimable';
|
||||
const rewards = [];
|
||||
if (t.reward_exp) rewards.push(`⭐${t.reward_exp} 经验`);
|
||||
if (t.reward_res && t.reward_res.gold) rewards.push(`🪙${t.reward_res.gold} 金币`);
|
||||
// 目标进度
|
||||
let objHtml = '';
|
||||
(t.objectives || []).forEach(o => {
|
||||
const need = o.need || 1;
|
||||
const have = Math.min(o.have || 0, need);
|
||||
const pct = need ? Math.round(have / need * 100) : 100;
|
||||
objHtml += `
|
||||
<div style="margin:6px 0;">
|
||||
<div style="display:flex;justify-content:space-between;font-size:12px;color:#9fb0c0;">
|
||||
<span>${o.label || o.type}${o.hint ? ` <span style="color:#66788a;">(${o.hint})</span>` : ''}</span>
|
||||
<span>${have}/${need}</span>
|
||||
</div>
|
||||
<div style="height:6px;background:#1d2733;border-radius:4px;overflow:hidden;margin-top:3px;">
|
||||
<div style="height:100%;width:${pct}%;background:linear-gradient(90deg,#3a7afe,#38d0c8);border-radius:4px;transition:width .35s;"></div>
|
||||
</div>
|
||||
</div>`;
|
||||
});
|
||||
const btnLabel = done ? '已完成' : (claimable ? '领取奖励' : (locked ? '未解锁' : '进行中'));
|
||||
const btnColor = done ? '#33414f' : (claimable ? '#3a7afe' : '#33414f');
|
||||
const btnCursor = (done || locked || !claimable) ? 'not-allowed' : 'pointer';
|
||||
html += `
|
||||
<div style="border:1px solid #2a3645;border-radius:10px;padding:10px 12px;margin:8px 0;background:#16202c;${locked ? 'opacity:.5;' : ''}">
|
||||
<div style="display:flex;justify-content:space-between;align-items:center;">
|
||||
<span style="font-weight:600;color:#e8eef4;">${t.title}</span>
|
||||
<span style="font-size:12px;color:#8899aa;">${stateLabel[t.state] || t.state}</span>
|
||||
</div>
|
||||
<div style="font-size:12px;color:#9fb0c0;margin:4px 0;">${t.desc || ''}</div>
|
||||
${objHtml}
|
||||
${rewards.length ? `<div style="font-size:12px;color:#ffd27f;margin:2px 0;">奖励:${rewards.join(' · ')}</div>` : ''}
|
||||
${done ? '' : `<button class="task-claim-btn" data-code="${t.code}"
|
||||
${claimable ? '' : 'disabled'}
|
||||
style="margin-top:6px;padding:5px 14px;border:none;border-radius:8px;cursor:${btnCursor};
|
||||
background:${btnColor};color:#fff;font-size:13px;">${btnLabel}</button>`}
|
||||
</div>`;
|
||||
});
|
||||
});
|
||||
return html;
|
||||
}
|
||||
|
||||
async function openTaskPanel() {
|
||||
// 首次进入可能任务数据尚未拉取,先确保就绪;再发射"打开面板"事件(ch1_welcome 由此变可领取)
|
||||
if (!taskData) await fetchTasks();
|
||||
await emitTaskEvent('panel_opened');
|
||||
const body = `<div id="taskPanelBody">${renderTaskPanel()}</div>
|
||||
<div style="text-align:center;color:#66788a;font-size:12px;margin-top:10px;">完成主线任务可获经验与金币,推动世界成长。</div>`;
|
||||
openAppModal('📜 任务', body);
|
||||
// 绑定领取按钮(setTimeout 等待 modal 注入 DOM)
|
||||
setTimeout(() => {
|
||||
document.querySelectorAll('.task-claim-btn').forEach(btn => {
|
||||
btn.addEventListener('click', () => claimTask(btn.getAttribute('data-code'), btn));
|
||||
});
|
||||
}, 30);
|
||||
}
|
||||
|
||||
async function claimTask(code, btn) {
|
||||
try {
|
||||
if (btn) { btn.disabled = true; btn.textContent = '领取中…'; }
|
||||
const beforeLv = microChar ? microChar.level : 1;
|
||||
const r = await API.taskComplete(code);
|
||||
if (r && r.ok && r.char) {
|
||||
// 回写角色状态到 HUD
|
||||
if (microChar) {
|
||||
microChar.name = r.char.name;
|
||||
microChar.level = r.char.level;
|
||||
microChar.exp = r.char.exp;
|
||||
microChar.exp_max = r.char.exp_max;
|
||||
microChar.gold = r.char.gold;
|
||||
microRenderCharHud();
|
||||
}
|
||||
const gained = [];
|
||||
if (r.reward_exp) gained.push(`⭐${r.reward_exp} 经验`);
|
||||
if (r.reward_res && r.reward_res.gold) gained.push(`🪙${r.reward_res.gold} 金币`);
|
||||
// 刷新面板
|
||||
taskData = await API.tasks();
|
||||
const body = document.getElementById('taskPanelBody');
|
||||
if (body) body.innerHTML = renderTaskPanel();
|
||||
// 重新绑定领取按钮
|
||||
document.querySelectorAll('.task-claim-btn').forEach(b => {
|
||||
b.addEventListener('click', () => claimTask(b.getAttribute('data-code'), b));
|
||||
});
|
||||
if (gained.length && !r.already) {
|
||||
toast(`任务完成!获得 ${gained.join(' · ')}`);
|
||||
}
|
||||
// 升级事件 → 推进"升至 X 级"类任务
|
||||
if (r.char.level > beforeLv) {
|
||||
emitTaskEvent('level_up', { level: r.char.level });
|
||||
}
|
||||
} else if (r && r.error === 'prereq_unmet') {
|
||||
if (btn) { btn.disabled = false; btn.textContent = '未解锁'; }
|
||||
toast('请先完成前置任务');
|
||||
} else if (r && r.error === 'not_claimable') {
|
||||
if (btn) { btn.disabled = false; btn.textContent = '进行中'; }
|
||||
toast('任务尚未完成');
|
||||
} else {
|
||||
// 未知错误:恢复按钮并提示,避免卡死
|
||||
if (btn) { btn.disabled = false; btn.textContent = '领取'; }
|
||||
toast('领取失败:' + (r && (r.error || r.detail) ? (r.error || r.detail) : '未知错误'));
|
||||
}
|
||||
} catch (e) {
|
||||
if (btn) { btn.disabled = false; btn.textContent = '领取'; }
|
||||
toast('领取异常:' + (e && e.message ? e.message : String(e)));
|
||||
}
|
||||
}
|
||||
const MICRO_ICON_IMGS = {}; // 贴图缓存:path -> Image
|
||||
function microLoadIcons() {
|
||||
@@ -1988,7 +2220,7 @@ function microLoadIcons() {
|
||||
else if (it.icon.includes('_r.png')) srcs.push(it.icon.replace('_r.png', '_l.png'));
|
||||
}
|
||||
srcs.forEach(src => {
|
||||
if (typeof src === 'string' && /\.(png|webp|jpg|jpeg)$/i.test(src) && !MICRO_ICON_IMGS[src]) {
|
||||
if (typeof src === 'string' && /\.(png|webp|jpg|jpeg)(\?.*)?$/i.test(src) && !MICRO_ICON_IMGS[src]) {
|
||||
const img = new Image();
|
||||
img.src = src;
|
||||
MICRO_ICON_IMGS[src] = img;
|
||||
@@ -1998,7 +2230,7 @@ function microLoadIcons() {
|
||||
// 自动道路/栅栏的「子贴图」(straight/curve/t/cross)不出现在建造面板,需主动预加载,避免首次绘制时闪烁/占位
|
||||
[ROAD_AUTO_ICON].forEach(map => {
|
||||
Object.values(map).forEach(src => {
|
||||
if (typeof src === 'string' && /\.(png|webp|jpg|jpeg)$/i.test(src) && !MICRO_ICON_IMGS[src]) {
|
||||
if (typeof src === 'string' && /\.(png|webp|jpg|jpeg)(\?.*)?$/i.test(src) && !MICRO_ICON_IMGS[src]) {
|
||||
const img = new Image();
|
||||
img.src = src;
|
||||
MICRO_ICON_IMGS[src] = img;
|
||||
@@ -3062,6 +3294,7 @@ function enterMicroWorld(tile) {
|
||||
microLoadIcons(); // 进入微观视图即预加载建造贴图,避免已放置建筑先显示文字
|
||||
microLoadCatalog(); // 拉取 Odoo 后台建造目录(未登录时保留前端兜底常量),hover/弹窗即时生效
|
||||
microLoadGame(); // 拉取角色 / 生物 / 订单(Odoo 后端),覆盖单位状态
|
||||
fetchTasks(); // 拉取任务定义 + 角色进度(新手主线)
|
||||
// 初始化角色动画系统
|
||||
if (typeof preloadAllSheets === 'function') preloadAllSheets();
|
||||
if (typeof updateCharPlacePanel === 'function') updateCharPlacePanel();
|
||||
@@ -3386,6 +3619,8 @@ function startMicroRender() {
|
||||
const onRoad = roadSet.size > 0;
|
||||
if (!hasBgImage) {
|
||||
microState.units.forEach((u, k) => {
|
||||
// 由建筑层负责渲染的种族角色不再在这里画 emoji,避免重影
|
||||
if (u._buildingRender) return;
|
||||
// 初始化/重置巡逻状态(无路时清空,重铺路后自动接管)
|
||||
if (onRoad) { if (!u.patrol || u.patrol.roadSize !== roadSet.size) { u.patrol = microInitPatrol(u, roadSet); u.patrol.roadSize = roadSet.size; } }
|
||||
else if (u.patrol) u.patrol = null;
|
||||
@@ -3760,7 +3995,15 @@ function onMicroUp(e) {
|
||||
// 轻点:命中已放置建筑 → 弹出说明/交互弹窗(非建造模式)
|
||||
const p = getMicroPoint(e);
|
||||
const hit = microBuildingAt(microCellAt(p));
|
||||
if (hit) { hideMicroHoverTip(); showBuildModal(hit); }
|
||||
if (hit) {
|
||||
hideMicroHoverTip();
|
||||
const hitIt = microBuildItemById(hit.type);
|
||||
if (hitIt && hitIt.isChar && hit._charCid) {
|
||||
showUnitModal(hit._charCid); // 种族角色打开单位面板
|
||||
} else {
|
||||
showBuildModal(hit);
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
// 超阈值:尝试切相邻地块;若边缘即墙(无相邻地块)navigateMicro 返回 false,
|
||||
@@ -3800,6 +4043,9 @@ function setMicroTile(tile) {
|
||||
microState.tile = tile;
|
||||
applyMicroAccent(tile.terrain);
|
||||
microState.units = tile.units || [];
|
||||
// 把服务端已存在的居民标记为"已知",避免后续保存时把旧居民误判为新入驻
|
||||
_knownUnitCids.clear();
|
||||
(microState.units || []).forEach(u => { if (u.id) _knownUnitCids.add(u.id); });
|
||||
microState.buildings = tile.hasBuilding
|
||||
? [{ ...BUILDINGS[tile.buildingType], type: tile.buildingType, level: tile.buildingLevel, hp: 80 + tile.buildingLevel * 10 }]
|
||||
: [];
|
||||
@@ -4420,6 +4666,11 @@ 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();
|
||||
// 发射任务事件:建筑交互 + 资源产出
|
||||
const _bkey = type + '_' + action;
|
||||
emitTaskEvent('building_action', { key: _bkey });
|
||||
const _res = TASK_RES[_bkey];
|
||||
if (_res) emitTaskEvent('resource_collected', { res: _res });
|
||||
// 升级:用后端落库后的权威等级/经验回写并持久化
|
||||
if (action === 'upgrade' && microOpenBuilding && res.level != null) {
|
||||
microOpenBuilding.level = res.level;
|
||||
@@ -5513,7 +5764,25 @@ function microCommitPlace() {
|
||||
const g = microComputeGhost();
|
||||
if (!g || !g.valid) { microToast('这里不能建造(红色区域)'); return; }
|
||||
const it = microBuildItemById(microBuildSel);
|
||||
if (it && it.isChar) {
|
||||
// 种族角色:作为居民入驻(触发 char_recruited),同时保留建筑记录用于 PNG 渲染
|
||||
const race = it.race || it.id;
|
||||
const cid = race + '_' + Date.now().toString(36) + '_' + Math.random().toString(36).substr(2, 5);
|
||||
const raceEmoji = { human:'🧑', demon:'👹', dwarf:'🧔', elf:'🧝', dragon:'🐲', undead:'💀', orc:'👺', angel:'👼', werewolf:'🐺', golem:'🗿' };
|
||||
microState.units.push({
|
||||
id: cid, name: it.name, role: race, species: race, icon: raceEmoji[race] || '🧑',
|
||||
level: 1, exp: 0, hp: 80, hp_max: 120,
|
||||
energy: 80, mood: '😊', job: 'none',
|
||||
equip: { weapon: null, armor: null, accessory: null },
|
||||
status: 'resting', _buildingRender: true
|
||||
});
|
||||
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), _charCid: cid });
|
||||
microSaveCreatures();
|
||||
scheduleBuildSave();
|
||||
return;
|
||||
}
|
||||
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) });
|
||||
emitTaskEvent('build_placed', { code: microBuildSel });
|
||||
scheduleBuildSave();
|
||||
}
|
||||
// 右键:命中建筑→删除;否则取消当前选择/编辑
|
||||
@@ -5527,7 +5796,15 @@ function microBuildRightClick(p) {
|
||||
if (hit) {
|
||||
const list = microBuildList();
|
||||
const idx = list.indexOf(hit);
|
||||
if (idx >= 0) { list.splice(idx, 1); if (microEditRef === hit) microEditRef = null; scheduleBuildSave(); }
|
||||
if (idx >= 0) {
|
||||
// 删除种族角色时同步移除对应居民记录
|
||||
if (hit._charCid && microState.units) {
|
||||
const ui = microState.units.findIndex(u => u.id === hit._charCid);
|
||||
if (ui >= 0) microState.units.splice(ui, 1);
|
||||
microSaveCreatures();
|
||||
}
|
||||
list.splice(idx, 1); if (microEditRef === hit) microEditRef = null; scheduleBuildSave();
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (microEditRef) { microEditRef = null; microToast('退出编辑'); }
|
||||
@@ -6101,7 +6378,7 @@ function setupWorldNameEdit() {
|
||||
localStorage.setItem('world_name', next);
|
||||
try {
|
||||
const r = await API.renameWorld(next);
|
||||
if (r && r.ok && r.name) { restore(r.name); localStorage.setItem('world_name', r.name); }
|
||||
if (r && r.ok && r.name) { restore(r.name); localStorage.setItem('world_name', r.name); emitTaskEvent('world_named'); }
|
||||
else console.warn('rename 后端未响应,已本地保存', r);
|
||||
} catch (e) {
|
||||
console.warn('rename 后端失败,已本地保存', e);
|
||||
@@ -6421,6 +6698,7 @@ if (typeof fitWorld === 'function') fitWorld();
|
||||
const local = localStorage.getItem('world_name');
|
||||
if (wnEl) wnEl.textContent = local || '我的世界';
|
||||
}
|
||||
fetchTasks(); // 登录即拉取新手主线任务(主世界初次进入也要触发,不只进地块时)
|
||||
const backdrop = document.getElementById('uaBackdrop');
|
||||
const closeBtn = document.getElementById('uaClose');
|
||||
const setMenuOpen = (open) => {
|
||||
@@ -6508,9 +6786,7 @@ if (typeof fitWorld === 'function') fitWorld();
|
||||
const btnWebsite = document.getElementById('btnWebsite');
|
||||
if (btnWebsite) btnWebsite.addEventListener('click', () => { location.href = 'index.html'; });
|
||||
const btnTask = document.getElementById('btnTask');
|
||||
if (btnTask) btnTask.addEventListener('click', () => {
|
||||
openAppModal('📜 任务', '<div style="text-align:center;padding:24px;color:#8899aa;font-size:13px;">任务系统即将开放<br><br>完成每日/主线任务可获得经验、金币与稀有素材。</div>');
|
||||
});
|
||||
if (btnTask) btnTask.addEventListener('click', () => { openTaskPanel(); });
|
||||
const btnMall = document.getElementById('btnMall');
|
||||
if (btnMall) btnMall.addEventListener('click', () => {
|
||||
openAppModal('🛒 商城', '<div style="text-align:center;padding:24px;color:#8899aa;font-size:13px;">商城系统即将开放<br><br>可购买皮肤、道具、加速器与会员服务。</div>');
|
||||
@@ -6549,24 +6825,58 @@ if (typeof fitWorld === 'function') fitWorld();
|
||||
if (e.target === appModal) closeAppModal();
|
||||
});
|
||||
|
||||
// 个人信息(从 cachedMe 动态渲染,点顶部用户区打开)
|
||||
function openProfile() {
|
||||
// 个人信息(从 cachedMe + 后端实时角色数据渲染)
|
||||
async function openProfile() {
|
||||
const d = cachedMe || {};
|
||||
const name = d.name || d.login || '访客';
|
||||
const avatarLetter = (name || 'M').trim().charAt(0).toUpperCase();
|
||||
const gender = d.gender || '未设置';
|
||||
const desc = d.desc || d.bio || '这位缔造者行事低调,尚未在宇森留下任何足迹。';
|
||||
const frame = d.avatar_frame || d.frame || '初芒';
|
||||
|
||||
// 先弹出面板并显示加载中,避免点击后无反馈
|
||||
openAppModal('👤 个人信息', '<div style="text-align:center;padding:24px;color:#9fb0c0;">加载中…</div>');
|
||||
|
||||
try {
|
||||
// 强制从后端拉取最新角色数据,防止刷新后 microChar 为空导致经验清零
|
||||
const fresh = await API.char();
|
||||
if (fresh && fresh.ok) {
|
||||
microChar = {
|
||||
name: fresh.name, level: fresh.level, exp: fresh.exp,
|
||||
exp_max: fresh.exp_max || 100, gold: fresh.gold || 0,
|
||||
equip: fresh.equip || { weapon: null, armor: null, accessory: null },
|
||||
inventory: fresh.inventory || {}
|
||||
};
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('加载角色信息失败,使用缓存数据', e);
|
||||
}
|
||||
|
||||
const mv = (typeof microChar !== 'undefined' && microChar) ? microChar : null;
|
||||
const mlv = mv ? (mv.level || 1) : (d.game_level || 1);
|
||||
const mexp = mv ? (mv.exp || 0) : (d.game_exp || 0);
|
||||
const mexpMax = mv ? (mv.exp_max || 100) : (d.game_exp_max || 100);
|
||||
const mpct = mexpMax > 0 ? Math.min(100, Math.round(mexp / mexpMax * 100)) : 0;
|
||||
openAppModal('👤 个人信息', `
|
||||
<div style="display:flex;align-items:center;gap:14px;margin-bottom:12px;">
|
||||
<div style="width:58px;height:58px;border-radius:50%;padding:3px;background:linear-gradient(135deg,#c77dff,#4af);box-shadow:0 0 14px rgba(199,125,255,.45);">
|
||||
<div style="width:100%;height:100%;border-radius:50%;background:linear-gradient(135deg,#2a2348,#161b2e);display:flex;align-items:center;justify-content:center;font-size:22px;font-weight:700;color:#fff;">${avatarLetter}</div>
|
||||
<div style="position:relative;flex:0 0 auto;">
|
||||
<div style="width:58px;height:58px;border-radius:50%;padding:3px;background:linear-gradient(135deg,#c77dff,#4af);box-shadow:0 0 14px rgba(199,125,255,.45);">
|
||||
<div style="width:100%;height:100%;border-radius:50%;background:linear-gradient(135deg,#2a2348,#161b2e);display:flex;align-items:center;justify-content:center;font-size:22px;font-weight:700;color:#fff;">${avatarLetter}</div>
|
||||
</div>
|
||||
<div style="position:absolute;right:-2px;bottom:-2px;min-width:22px;height:22px;padding:0 5px;border-radius:11px;background:linear-gradient(135deg,#3a7afe,#46e0c8);border:2px solid #16202c;display:flex;align-items:center;justify-content:center;font-size:11px;font-weight:700;color:#fff;box-shadow:0 0 8px rgba(58,122,254,.5);">${mlv}</div>
|
||||
</div>
|
||||
<div style="min-width:0;">
|
||||
<div style="font-size:16px;font-weight:700;color:#e0e0e0;">${name}</div>
|
||||
<div style="font-size:12px;color:#8899aa;">UID: ${d.game_uid || d.uid || '--'} · 头像框:${frame}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="infoRow"><span class="infoLabel">等级</span><span class="infoValue">Lv.${mlv}</span></div>
|
||||
<div style="display:flex;align-items:center;gap:6px;margin:4px 0 10px;">
|
||||
<div style="position:relative;flex:1;height:7px;background:rgba(255,255,255,.12);border-radius:5px;overflow:hidden;">
|
||||
<div style="position:absolute;left:0;top:0;bottom:0;width:${mpct}%;background:linear-gradient(90deg,#3a7afe,#46e0c8);border-radius:5px;transition:width .35s ease;"></div>
|
||||
</div>
|
||||
<span style="font-size:11px;color:#9fb0c0;white-space:nowrap;">EXP ${mexp}/${mexpMax}</span>
|
||||
</div>
|
||||
<div class="infoRow"><span class="infoLabel">性别</span><span class="infoValue">${gender}</span></div>
|
||||
<div style="font-size:12px;color:#aab4c8;line-height:1.6;background:rgba(255,255,255,.04);border:1px solid rgba(255,255,255,.08);border-radius:10px;padding:10px 12px;margin:10px 0;">${desc}</div>
|
||||
<div class="infoRow"><span class="infoLabel">称号</span><span class="infoValue">${d.title || '萌新'}</span></div>
|
||||
|
||||
Reference in New Issue
Block a user