diff --git a/addons/yt_world/data/yt_world_task.xml b/addons/yt_world/data/yt_world_task.xml
index cb2b410..0c88334 100644
--- a/addons/yt_world/data/yt_world_task.xml
+++ b/addons/yt_world/data/yt_world_task.xml
@@ -45,7 +45,7 @@
2
ch2_harvest
第一次收获
- 在农田上使用「收获」动作,体验建筑产出资源的乐趣。
+ 建造农田并在农田上使用「收获」动作,体验建筑产出资源的乐趣。
40
diff --git a/addons/yt_world/models/yt_world_chest.py b/addons/yt_world/models/yt_world_chest.py
index 48158c6..6d6fa89 100644
--- a/addons/yt_world/models/yt_world_chest.py
+++ b/addons/yt_world/models/yt_world_chest.py
@@ -86,6 +86,7 @@ class YtWorldChest(models.Model):
('state', '=', 'active'),
('tile_ref', '=', {'q': q, 'r': r}),
], order='create_time asc')
+ to_remove = self.browse() # 空记录集:保证下方推导式在 existing= MAX_CHESTS_PER_TILE:
# 删掉最旧的,腾出一个空位给新宝箱
to_remove = existing[:len(existing) - MAX_CHESTS_PER_TILE + 1]
diff --git a/assets/buildings/spring/cottage/cottage_l2_l.png b/assets/buildings/spring/cottage/cottage_l2_l.png
new file mode 100644
index 0000000..462f092
Binary files /dev/null and b/assets/buildings/spring/cottage/cottage_l2_l.png differ
diff --git a/assets/buildings/spring/cottage/cottage_l2_r.png b/assets/buildings/spring/cottage/cottage_l2_r.png
new file mode 100644
index 0000000..a6a46a7
Binary files /dev/null and b/assets/buildings/spring/cottage/cottage_l2_r.png differ
diff --git a/assets/buildings/spring/cottage/gen/Keep_EXACT_same_painting_style_2026-09-22T07-34-05.png b/assets/buildings/spring/cottage/gen/Keep_EXACT_same_painting_style_2026-09-22T07-34-05.png
new file mode 100644
index 0000000..da36ec5
Binary files /dev/null and b/assets/buildings/spring/cottage/gen/Keep_EXACT_same_painting_style_2026-09-22T07-34-05.png differ
diff --git a/assets/buildings/spring/cottage/gen/_diag_compare.png b/assets/buildings/spring/cottage/gen/_diag_compare.png
new file mode 100644
index 0000000..ecfd0bf
Binary files /dev/null and b/assets/buildings/spring/cottage/gen/_diag_compare.png differ
diff --git a/assets/buildings/spring/cottage/gen/_diag_removed.png b/assets/buildings/spring/cottage/gen/_diag_removed.png
new file mode 100644
index 0000000..b4e0e2f
Binary files /dev/null and b/assets/buildings/spring/cottage/gen/_diag_removed.png differ
diff --git a/assets/buildings/spring/cottage/gen/_l1_flat.png b/assets/buildings/spring/cottage/gen/_l1_flat.png
new file mode 100644
index 0000000..ea25474
Binary files /dev/null and b/assets/buildings/spring/cottage/gen/_l1_flat.png differ
diff --git a/assets/buildings/spring/cottage/gen/_l2_nowm.png b/assets/buildings/spring/cottage/gen/_l2_nowm.png
new file mode 100644
index 0000000..b7c5c5b
Binary files /dev/null and b/assets/buildings/spring/cottage/gen/_l2_nowm.png differ
diff --git a/assets/buildings/spring/cottage/gen/cottage_l2_cut.png b/assets/buildings/spring/cottage/gen/cottage_l2_cut.png
new file mode 100644
index 0000000..462f092
Binary files /dev/null and b/assets/buildings/spring/cottage/gen/cottage_l2_cut.png differ
diff --git a/world.html b/world.html
index 8936f85..3527546 100644
--- a/world.html
+++ b/world.html
@@ -2679,7 +2679,7 @@ 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=6', name: '农舍', desc: '村民起居的基本居所,提供人口上限与夜间休息。', flat: false, w: 2, h: 2, statLevel: 1, statHp: 120, statYield: { population: 4 }, costGold: 20 },
+ { id: 'cottage', cat: 'build', icon: 'assets/buildings/spring/cottage/cottage_l.png?v=6', levels: { 2: 'assets/buildings/spring/cottage/cottage_l2_l.png?v=1' }, name: '农舍', desc: '村民起居的基本居所,提供人口上限与夜间休息。', flat: false, w: 2, h: 2, statLevel: 1, statHp: 120, statYield: { population: 4 }, costGold: 20 },
{ 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 }, costGold: 60 },
{ 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 }, costGold: 80 },
{ 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 }, costGold: 50 },
@@ -3502,7 +3502,7 @@ async function openTaskPanel() {
}
const panel = document.getElementById('taskSidePanel');
if (panel) panel.hidden = false;
- setMenuOpen(false);
+ if (window.setMenuOpen) window.setMenuOpen(false); // 关闭汉堡菜单;未初始化时跳过(原为直接调局部函数导致 ReferenceError)
// 绑定领取按钮(setTimeout 等待 DOM 注入)
setTimeout(() => {
document.querySelectorAll('.task-claim-btn').forEach(btn => {
@@ -3572,16 +3572,20 @@ const MICRO_ICON_IMGS = {}; // 贴图缓存:path -> Image
function microLoadIcons() {
microAllBuildItems().forEach(it => {
const srcs = [it.icon];
+ if (it.levels) Object.values(it.levels).forEach(l => srcs.push(l));
// 3/4 建筑有左右两个视角,预加载另一个视角避免切换时闪烁
if (typeof it.icon === 'string') {
if (it.icon.includes('_l.png')) srcs.push(it.icon.replace('_l.png', '_r.png'));
else if (it.icon.includes('_r.png')) srcs.push(it.icon.replace('_r.png', '_l.png'));
}
+ // 升级外观(levels)同样处理左右视角
+ if (it.levels) Object.values(it.levels).forEach(l => {
+ if (l.includes('_l.png')) srcs.push(l.replace('_l.png', '_r.png'));
+ else if (l.includes('_r.png')) srcs.push(l.replace('_r.png', '_l.png'));
+ });
srcs.forEach(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;
+ if (typeof src === 'string' && /\.(png|webp|jpg|jpeg)(\?.*)?$/i.test(src)) {
+ microEnsureIcon(src); // 统一走带失败重试的加载,避免预加载时文件未落盘被缓存 404
}
});
});
@@ -3596,6 +3600,23 @@ function microLoadIcons() {
});
});
}
+// 按需加载建筑贴图(幂等):若尚未加载则创建 Image;加载失败(如升级前文件未落盘被浏览器缓存了 404)
+// 自动换缓存号重试,确保文件到位后能重新取到,避免"升级后不换肤、只能刷新"的体验。
+function microEnsureIcon(src) {
+ if (!src || typeof src !== 'string') return null;
+ if (MICRO_ICON_IMGS[src]) return MICRO_ICON_IMGS[src];
+ const img = new Image();
+ let attempt = 0;
+ img.onerror = () => {
+ if (attempt++ < 2) {
+ const sep = src.includes('?') ? '&' : '?';
+ img.src = src + sep + 'retry=' + Date.now(); // 换缓存号强制重取,绕过 404 缓存
+ }
+ };
+ img.src = src;
+ MICRO_ICON_IMGS[src] = img;
+ return img;
+}
// 根据朝向选择 3/4 建筑素材:默认统一朝左,0/180° 用 _l,90/270° 用 _r
function microIconForRot(baseIcon, rot) {
if (typeof baseIcon !== 'string') return baseIcon;
@@ -3636,7 +3657,9 @@ function microDrawPlaced(ctx, b, it, cell, ew, eh) {
const isRoad = it && it.cat === 'road';
const flat = !!(it && it.flat);
const rot = (b && b.rot) || 0;
- const baseIcon = (b && b.sub) || (it && it.icon);
+ const lvl = (b && b.level) ? b.level : 1;
+ const lvlIcon = (it && it.levels && it.levels[lvl]) ? it.levels[lvl] : null;
+ const baseIcon = (b && b.sub) || lvlIcon || (it && it.icon);
const icon = (!isRoad && !flat) ? microIconForRot(baseIcon, rot) : baseIcon;
const img = (typeof icon === 'string') ? (MICRO_ICON_IMGS[icon] || null) : null;
if (img && img.complete && img.naturalWidth) {
@@ -6517,6 +6540,14 @@ async function microBuildingAct(type, action, label) {
if (action === 'upgrade' && microOpenBuilding && res.level != null) {
microOpenBuilding.level = res.level;
microOpenBuilding.exp = res.exp;
+ // 升级即时换肤:主动确保新等级贴图(含左右视角)已加载,不依赖进入微观时的预加载时机
+ const itU = microBuildItemById(microOpenBuilding.type);
+ if (itU && itU.levels && itU.levels[res.level]) {
+ const base = itU.levels[res.level];
+ microEnsureIcon(base);
+ if (base.includes('_l.png')) microEnsureIcon(base.replace('_l.png', '_r.png'));
+ else if (base.includes('_r.png')) microEnsureIcon(base.replace('_r.png', '_l.png'));
+ }
scheduleBuildSave();
}
// 治疗:回满本村所有生物生命
@@ -9175,6 +9206,7 @@ if (typeof fitWorld === 'function') fitWorld();
if (backdrop) backdrop.classList.toggle('open', open);
trigger.setAttribute('aria-expanded', open ? 'true' : 'false');
};
+ window.setMenuOpen = setMenuOpen; // 暴露给全局(openTaskPanel 等外部作用域需要)
// === 面板数据填充(从 me() 返回值动态渲染) ===
const cachedMe = me; // 保存供弹窗复用