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 + +