@@ -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 = `🪙
${microChar.gold != null ? microChar.gold : 0} · ⭐ 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 = `
+
+ 🪙 ${gold}
+ ⭐ Lv.${lv}
+ ${name ? `${name}` : ''}
+
+
+
+
${exp}/${expMax} (${pct}%)
+
`;
+}
+
+// ============ 任务系统(新手主线,后端驱动) ============
+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 '
任务加载中…
';
+ }
+ 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 += `
第 ${ch} 章
`;
+ 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 += `
+
+
+ ${o.label || o.type}${o.hint ? ` (${o.hint})` : ''}
+ ${have}/${need}
+
+
+
`;
+ });
+ const btnLabel = done ? '已完成' : (claimable ? '领取奖励' : (locked ? '未解锁' : '进行中'));
+ const btnColor = done ? '#33414f' : (claimable ? '#3a7afe' : '#33414f');
+ const btnCursor = (done || locked || !claimable) ? 'not-allowed' : 'pointer';
+ html += `
+
+
+ ${t.title}
+ ${stateLabel[t.state] || t.state}
+
+
${t.desc || ''}
+ ${objHtml}
+ ${rewards.length ? `
奖励:${rewards.join(' · ')}
` : ''}
+ ${done ? '' : `
`}
+
`;
+ });
+ });
+ return html;
+}
+
+async function openTaskPanel() {
+ // 首次进入可能任务数据尚未拉取,先确保就绪;再发射"打开面板"事件(ch1_welcome 由此变可领取)
+ if (!taskData) await fetchTasks();
+ await emitTaskEvent('panel_opened');
+ const body = `
${renderTaskPanel()}
+
完成主线任务可获经验与金币,推动世界成长。
`;
+ 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('📜 任务', '
任务系统即将开放
完成每日/主线任务可获得经验、金币与稀有素材。
');
- });
+ if (btnTask) btnTask.addEventListener('click', () => { openTaskPanel(); });
const btnMall = document.getElementById('btnMall');
if (btnMall) btnMall.addEventListener('click', () => {
openAppModal('🛒 商城', '
商城系统即将开放
可购买皮肤、道具、加速器与会员服务。
');
@@ -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('👤 个人信息', '
加载中…
');
+
+ 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('👤 个人信息', `
-
-
${avatarLetter}
+
${name}
UID: ${d.game_uid || d.uid || '--'} · 头像框:${frame}
+
等级Lv.${mlv}
+
+
+
EXP ${mexp}/${mexpMax}
+
性别${gender}
${desc}
称号${d.title || '萌新'}