diff --git a/addons/yt_world/controllers/world_api.py b/addons/yt_world/controllers/world_api.py
index 0e9736a..e6a01ba 100644
--- a/addons/yt_world/controllers/world_api.py
+++ b/addons/yt_world/controllers/world_api.py
@@ -199,13 +199,14 @@ class YtWorldApi(http.Controller):
seen = []
out = []
for r in items.read(['code', 'name', 'category', 'icon', 'footprint_w',
- 'footprint_h', 'terrains', 'desc', 'flat',
+ 'footprint_h', 'cost_gold', 'terrains', 'desc', 'flat',
'stat_level', 'stat_hp', 'stat_yield', 'actions']):
if r['category'] not in seen:
seen.append(r['category'])
out.append({
'code': r['code'], 'name': r['name'], 'category': r['category'],
'icon': r['icon'], 'w': r['footprint_w'], 'h': r['footprint_h'],
+ 'costGold': r['cost_gold'] or 0,
'terrains': r['terrains'] or [],
'desc': r['desc'] or '',
'flat': bool(r['flat']),
@@ -217,6 +218,42 @@ class YtWorldApi(http.Controller):
cats = [{'id': c, 'name': CAT_LABELS.get(c, c)} for c in seen]
return _json({'cats': cats, 'items': out})
+ @http.route('/yt_world/api/micro/build/charge', type='http', auth='public',
+ methods=['POST', 'OPTIONS'], csrf=False)
+ def charge_build(self, **kw):
+ """建造扣费(落库前调用)。body: {"code": "cottage", "count": 1}
+ 校验建筑存在并按 cost_gold * count 扣减玩家金币;余额不足明确报错。"""
+ auth_err = self._require_user()
+ if auth_err:
+ return _json(auth_err)
+ world = self._get_world()
+ if not world:
+ return _json({'ok': False, 'error': 'no_world'})
+ body = _post_body()
+ code = (body.get('code') or '').strip()
+ count = body.get('count') or 1
+ try:
+ count = int(count)
+ except (TypeError, ValueError):
+ count = 1
+ if count < 1:
+ count = 1
+ if not code:
+ return _json({'ok': False, 'error': 'bad_params'})
+ item = request.env['yt.world.build.item'].sudo().search(
+ [('code', '=', code), ('active', '=', True)], limit=1)
+ if not item:
+ return _json({'ok': False, 'error': 'unknown_build', 'code': code})
+ cost = (item.cost_gold or 0) * count
+ char = self._get_char(world)
+ gold = char.gold or 0
+ if gold < cost:
+ return _json({'ok': False, 'error': 'no_gold', 'need': cost, 'have': gold, 'code': code})
+ char.sudo().write({'gold': gold - cost})
+ _logger.info('yt.world build charge uid=%s code=%s count=%s cost=%s remain=%s',
+ request.session.uid, code, count, cost, gold - cost)
+ return _json({'ok': True, 'code': code, 'count': count, 'cost': cost, 'gold': gold - cost})
+
# ========== 图鉴 API ==========
def _records_json(self, model, fields):
diff --git a/addons/yt_world/data/yt_world_build_item.xml b/addons/yt_world/data/yt_world_build_item.xml
index e6b85fa..821f9d7 100644
--- a/addons/yt_world/data/yt_world_build_item.xml
+++ b/addons/yt_world/data/yt_world_build_item.xml
@@ -1,29 +1,40 @@
-
+
+
+ road_auto道路
+ roadassets/roads/5.png
+ 115
+ 1
+ ["plain","forest"]
+
road_tT字路口
roadassets/roads/1.png
1110
+ 1
["plain","forest"]
road_cross十字路口
roadassets/roads/2.png
1120
+ 1
["plain","forest"]
road_corner弯道
roadassets/roads/4.png
1130
+ 1
["plain","forest"]
road_straight直路
roadassets/roads/5.png
1140
+ 1
["plain","forest"]
@@ -31,18 +42,21 @@
ground_grass_a草地A
groundassets/roads/3.png
11110
+ 0
["plain","forest"]
ground_grass_b草地B
groundassets/roads/6.png
11120
+ 0
["plain","forest"]
ground_grass_c草地C
groundassets/roads/7.png
11130
+ 0
["plain","forest"]
@@ -51,6 +65,7 @@
cottage农舍
buildassets/buildings/spring/cottage/cottage_r.png
33210
+ 20
村民起居的基本居所,提供人口上限与夜间休息。
1120
@@ -63,7 +78,8 @@
windmill风车
buildassets/buildings/spring/windmill/windmill_r.png
- 33220
+ 3 3220
+ 60
利用风力磨粉与提水,提升粮食加工效率。
1110
@@ -71,7 +87,8 @@
blacksmith铁匠铺
buildassets/buildings/spring/blacksmith/blacksmith_r.png
- 33230
+ 3 3230
+ 80
锻造工具与武器,产出金属制品。
1130
@@ -79,7 +96,8 @@
grocery杂货铺
buildassets/buildings/spring/grocery/grocery_r.png
- 33240
+ 3 3240
+ 50
交易日用物资的小型商铺,促进经济流通。
190
@@ -87,7 +105,8 @@
temple神庙
buildassets/buildings/spring/temple/temple_r.png
- 33250
+ 3 3250
+ 100
供奉信仰的场所,提升居民精神状态与领地声望。
1160
@@ -95,7 +114,8 @@
clinic医务室
buildassets/buildings/spring/clinic/clinic_r.png
- 33260
+ 3 3260
+ 55
诊治伤病,恢复居民生命值。
1100
@@ -103,7 +123,8 @@
restaurant酒馆
buildassets/buildings/spring/restaurant/restaurant_r.png
- 33270
+ 3 3270
+ 45
烹制料理,为居民提供饱食与心情加成。
195
@@ -111,7 +132,8 @@
pasture牧场
buildassets/buildings/spring/pasture/pasture_r.png
- 33280
+ 3 3280
+ 70
放养牲畜,稳定产出肉奶类资源。
1105
@@ -119,7 +141,8 @@
blackmarket商城
buildassets/buildings/spring/blackmarket/blackmarket_r.png
- 33290
+ 3 3290
+ 120
灰色交易点,可换取稀有物资但伴随风险。
180
@@ -129,7 +152,8 @@
farmland农田
facilityassets/buildings/spring/farmland/farmland.png
- 22310
+ 2 2310
+ 10
1
春季播种的耕地,定期产出粮食作物。
160
@@ -138,7 +162,8 @@
fence栅栏
facilityassets/buildings/spring/fence/fence.png
- 33320
+ 3 3320
+ 8
1
圈定范围的木质围栏,划分地块边界。
150
@@ -147,7 +172,8 @@
wall围墙
facilityassets/buildings/spring/wall/wall.png
- 33330
+ 3 3330
+ 15
1
石砌围墙,提供防御与领地分隔。
1200
@@ -156,7 +182,8 @@
storage存储箱
facilityassets/buildings/spring/storage/storage.png
- 33340
+ 3 3340
+ 12
1
存放物资的木箱,扩充本地仓储上限。
170
@@ -165,7 +192,8 @@
well水井
buildassets/buildings/default/props/well_r.png
- 22350
+ 2 2350
+ 15
0
汲取地下水的圆井,保障饮水与灌溉。_r/_l 为同一水井的两种朝向。
190
@@ -174,7 +202,8 @@
school学堂
buildassets/buildings/spring/school/school_l.png
- 33380
+ 3 3380
+ 90
0
授业解惑之所,研读典籍提升领地科研与基础教学产出。
1100
@@ -183,7 +212,8 @@
library图书馆
buildassets/buildings/spring/library/library_l.png
- 33360
+ 3 3360
+ 110
0
收藏典籍与卷轴,提升领地科研与知识产出。
1110
@@ -192,10 +222,29 @@
barracks兵营
buildassets/buildings/spring/barracks/barracks_l.png
- 33370
+ 3 3370
+ 130
0
训练士兵与储备武备的军事建筑,提升领地军事产出。
1130
+
+ guild公会
+ buildassets/buildings/spring/guild/guild_l.png
+ 22390
+ 100
+ 冒险者与住民集结之所,可发布委托与招募成员。
+ 1140
+
+
+
+ village村庄
+ buildassets/buildings/spring/village/village_l.png
+ 33400
+ 200
+ 族群聚落的核心,提升人口上限与基础产出。
+ 1150
+
+
diff --git a/addons/yt_world/models/yt_world_build_item.py b/addons/yt_world/models/yt_world_build_item.py
index 70b68e9..5111db6 100644
--- a/addons/yt_world/models/yt_world_build_item.py
+++ b/addons/yt_world/models/yt_world_build_item.py
@@ -32,6 +32,8 @@ class YtWorldBuildItem(models.Model):
help='建造时占据的网格列数')
footprint_h = fields.Integer(string='占位高(格)', default=1,
help='建造时占据的网格行数')
+ cost_gold = fields.Integer(string='金币花费', default=0,
+ help='建造一次该建筑所需金币;0 表示免费')
sequence = fields.Integer(string='排序', default=100)
active = fields.Boolean(string='启用', default=True)
world_id = fields.Many2one('yt.world', string='关联世界',
diff --git a/assets/audio/bgm.mp3 b/assets/audio/bgm.mp3
new file mode 100644
index 0000000..b6c99b0
Binary files /dev/null and b/assets/audio/bgm.mp3 differ
diff --git a/assets/game-api.js b/assets/game-api.js
index 3aebf5d..22ed2eb 100644
--- a/assets/game-api.js
+++ b/assets/game-api.js
@@ -162,6 +162,24 @@ const API = {
return { ok: false, error: 'network', detail: String(e && e.message || e) };
}
},
+ // 建造扣费(落库前调用):扣成功才允许放置建筑。
+ // 返回 {ok:true, cost, gold} 或 {ok:false, error:'no_gold'|'auth'|'network', need, have}
+ async chargeBuild(code, count = 1) {
+ try {
+ const r0 = await fetch(_worldUrl('/yt_world/api/micro/build/charge'), {
+ method: 'POST', credentials: _wcred,
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ code, count }),
+ });
+ if (r0.redirected || (r0.url && r0.url.indexOf('/web/login') >= 0)) {
+ return { ok: false, error: 'auth' };
+ }
+ const j = await r0.json();
+ return j && j.ok === false ? j : (j || { ok: true });
+ } catch (e) {
+ return { ok: false, error: 'network', detail: String(e && e.message || e) };
+ }
+ },
// ---------- 世界数据 / 包裹(后端接口预留:yt_world 暂未实现路由,前端 mock 兜底) ----------
async worldData() {
try {
diff --git a/world.html b/world.html
index ef7f459..d86dac8 100644
--- a/world.html
+++ b/world.html
@@ -378,6 +378,8 @@ body { background: #0a0e17; overflow: hidden; font-family: 'Microsoft YaHei', sa
.mv-cell .mv-ico { font-size: 30px; line-height: 1; pointer-events: none; }
.mv-cell .mv-ico-img { width: 44px; height: 44px; object-fit: contain; pointer-events: none; border-radius: 6px; }
.mv-cell .mv-nm { font-size: 11px; color: #9fb3c8; pointer-events: none; }
+.mv-cost { font-size: 10px; color: #ffd54f; pointer-events: none; font-weight: 600; }
+.mv-cost.free { color: #7fdca0; }
.mv-empty-cat { height: 100%; display: flex; align-items: center; justify-content: center; padding: 0 14px; color: #6c8098; font-size: 13px; text-align: center; }
.mv-clear {
background: rgba(255,90,90,.1); border: 1px solid rgba(255,90,90,.35);
@@ -587,6 +589,10 @@ body { background: #0a0e17; overflow: hidden; font-family: 'Microsoft YaHei', sa
#appModal .mailTitle { color: #7cc7ff; font-weight: 600; margin-bottom: 4px; }
#appModal .mailMeta { color: #778; font-size: 11px; margin-bottom: 4px; }
#appModal .settingRow { display: flex; justify-content: space-between; align-items: center; margin: 10px 0; font-size: 13px; }
+#appModal .toggleSwitch { position: relative; width: 42px; height: 22px; border-radius: 11px; background: rgba(255,255,255,.12); cursor: pointer; transition: background .2s; }
+#appModal .toggleSwitch.active { background: linear-gradient(135deg,#8b5cf6,#4aafff); }
+#appModal .toggleSwitch::after { content: ''; position: absolute; top: 2px; left: 2px; width: 18px; height: 18px; border-radius: 50%; background: #fff; box-shadow: 0 1px 3px rgba(0,0,0,.3); transition: transform .2s; }
+#appModal .toggleSwitch.active::after { transform: translateX(20px); }
#appModal .mailTabs { display: flex; gap: 6px; margin-bottom: 12px; }
#appModal .mailTab { flex: 1; text-align: center; padding: 6px 0; font-size: 13px; color: #8899aa; background: rgba(255,255,255,.05); border: 1px solid rgba(255,255,255,.1); border-radius: 8px; cursor: pointer; transition: .15s; }
#appModal .mailTab.active { color: #fff; background: rgba(139,92,246,.25); border-color: rgba(139,92,246,.5); }
@@ -1152,6 +1158,8 @@ body { background: #0a0e17; overflow: hidden; font-family: 'Microsoft YaHei', sa
+
+