From 585bbe8f0fb63e6001f76251e62a03130c0082c5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9D=8E=E9=B9=8F=E5=AE=87?= Date: Thu, 3 Sep 2026 18:48:48 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E8=A7=92=E8=89=B2=E5=A2=9E=E5=8A=A0?= =?UTF-8?q?=E9=93=9C=E5=B8=81/=E9=93=B6=E5=B8=81/=E9=92=BB=E7=9F=B3?= =?UTF-8?q?=E5=AD=97=E6=AE=B5=E5=B9=B6=E4=B8=8E=E9=87=91=E5=B8=81=E5=9C=A8?= =?UTF-8?q?=E5=BA=95=E9=83=A8HUD=E6=A8=AA=E5=90=91=E5=B1=95=E7=A4=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- addons/yt_world/controllers/world_api.py | 28 +++++--- addons/yt_world/models/yt_world_game.py | 3 + addons/yt_world/views/yt_world_views.xml | 9 +++ assets/game-api.js | 12 ++-- world.html | 90 +++++++++++++++++++----- 5 files changed, 110 insertions(+), 32 deletions(-) diff --git a/addons/yt_world/controllers/world_api.py b/addons/yt_world/controllers/world_api.py index f12be93..75941b0 100644 --- a/addons/yt_world/controllers/world_api.py +++ b/addons/yt_world/controllers/world_api.py @@ -388,7 +388,7 @@ class YtWorldApi(http.Controller): return _json({ 'ok': True, 'talent_points': char.talent_points or 0, - 'gold': char.gold or 0, + **self._char_currency(char), 'talents': self._serialize_char_talents(char), }) @@ -458,7 +458,7 @@ class YtWorldApi(http.Controller): 'code': code, 'level': 1, 'talent_points': char.talent_points or 0, - 'gold': char.gold or 0, + **self._char_currency(char), }) @http.route('/yt_world/api/char/talent/upgrade', type='http', auth='public', @@ -498,7 +498,7 @@ class YtWorldApi(http.Controller): 'code': code, 'level': res['level'], 'talent_points': char.talent_points or 0, - 'gold': char.gold or 0, + **self._char_currency(char), }) @http.route('/yt_world/api/char/talent/draw', type='http', auth='public', @@ -656,6 +656,15 @@ class YtWorldApi(http.Controller): 'name': emp.name or char.name}) return char + def _char_currency(self, char): + """角色货币字段统一序列化(金币/铜币/银币/钻石)。""" + return { + 'gold': char.gold or 0, + 'copper': char.copper or 0, + 'silver': char.silver or 0, + 'diamond': char.diamond or 0, + } + # ========== 任务系统序列化(get_tasks / task_progress 共用) ========== def _serialize_tasks(self, char): """把任务定义 + 角色进度汇总成前端面板可直接渲染的结构。""" @@ -719,7 +728,7 @@ class YtWorldApi(http.Controller): 'tasks': out, 'char': { 'name': char.name, 'level': char.level, 'exp': char.exp, - 'exp_max': char.exp_max, 'gold': char.gold, + 'exp_max': char.exp_max, **self._char_currency(char), }, }) @@ -750,7 +759,7 @@ class YtWorldApi(http.Controller): 'tasks': out, 'char': { 'name': char.name, 'level': char.level, 'exp': char.exp, - 'exp_max': char.exp_max, 'gold': char.gold, + 'exp_max': char.exp_max, **self._char_currency(char), }, 'claimable': res.get('claimable', []), }) @@ -779,7 +788,7 @@ class YtWorldApi(http.Controller): 'already': res.get('already', False), 'char': { 'name': char.name, 'level': char.level, 'exp': char.exp, - 'exp_max': char.exp_max, 'gold': char.gold, + 'exp_max': char.exp_max, **self._char_currency(char), }, 'reward_exp': res.get('reward_exp', 0), 'reward_res': res.get('reward_res', {}), @@ -799,7 +808,7 @@ class YtWorldApi(http.Controller): return _json({ 'ok': True, 'name': char.name, 'level': char.level, 'exp': char.exp, - 'exp_max': char.exp_max or 100, 'gold': char.gold, + 'exp_max': char.exp_max or 100, **self._char_currency(char), 'inventory': char.inventory or {}, 'equip': char.equip or {'weapon': None, 'armor': None, 'accessory': None}, }) @@ -817,7 +826,7 @@ class YtWorldApi(http.Controller): char = self._get_char(world) body = _post_body() vals = {} - for f in ('name', 'level', 'exp', 'gold'): + for f in ('name', 'level', 'exp', 'gold', 'copper', 'silver', 'diamond'): if f in body and body[f] is not None: vals[f] = body[f] if 'inventory' in body and body['inventory'] is not None: @@ -827,7 +836,8 @@ class YtWorldApi(http.Controller): char.sudo().write(vals) return _json({'ok': True, 'char': { 'name': char.name, 'level': char.level, 'exp': char.exp, - 'gold': char.gold, 'inventory': char.inventory or {}, + **self._char_currency(char), + 'inventory': char.inventory or {}, 'equip': char.equip or {}, }}) diff --git a/addons/yt_world/models/yt_world_game.py b/addons/yt_world/models/yt_world_game.py index 2eca6e8..d37f5b9 100644 --- a/addons/yt_world/models/yt_world_game.py +++ b/addons/yt_world/models/yt_world_game.py @@ -36,6 +36,9 @@ class YtWorldCharacter(models.Model): exp = fields.Integer(string='经验', default=0) exp_max = fields.Integer(string='升级所需经验', default=100) gold = fields.Integer(string='金币', default=100) + copper = fields.Integer(string='铜币', default=0) + silver = fields.Integer(string='银币', default=0) + diamond = fields.Integer(string='钻石', default=0) talent_points = fields.Integer(string='天赋点', default=0, help='用于激活/升级天赋的专用点数') inventory = fields.Json(string='背包', default=dict) diff --git a/addons/yt_world/views/yt_world_views.xml b/addons/yt_world/views/yt_world_views.xml index fa2918d..c21fdf2 100644 --- a/addons/yt_world/views/yt_world_views.xml +++ b/addons/yt_world/views/yt_world_views.xml @@ -83,6 +83,9 @@ + + + @@ -256,6 +259,9 @@ + + + @@ -283,6 +289,9 @@ + + + (port === 80 && (location.port === '' || location.port === '80')); const API_CONFIG = { + // 本地开发一律跨域直连 Odoo 8069(python http.server 无代理能力); + // 仅 8080 dev-server(自带反向代理到 Odoo)才走同源空 base。 base: _isLocalDev() - ? ((_isDevPort(8080) || _isDevPort(8888)) ? '' : `http://${location.hostname}:8080`) - : '', - db: _isLocalDev() ? 'study' : 'game', + ? (_isDevPort(8080) ? '' : `http://${location.hostname}:8069`) + : '', + db: _isLocalDev() ? 'game_a' : 'game', }; -// 世界(yt_world)所在库:本地 Odoo 钉 yt_game;线上游戏库名为 game。按域名自适应,git 同步不冲突。 -const WORLD_DB = _isLocalDev() ? 'yt_game' : 'game'; +// 世界(yt_world)所在库:与 API_CONFIG.db 保持一致(本地均为 game_a),避免跨库导致 auth 失败。 +const WORLD_DB = API_CONFIG.db; // Odoo 后端基础 URL(世界 API 专用,兼容本地跨域 / 生产同域) const WORLD_BASE = API_CONFIG.base || ''; diff --git a/world.html b/world.html index 32b245a..ec66718 100644 --- a/world.html +++ b/world.html @@ -243,7 +243,7 @@ body { background: #0a0e17; overflow: hidden; font-family: 'Microsoft YaHei', sa /* 世界等级经验条(位于 bottomBar 内最左侧,加大加清晰) */ #worldExpBarWrap{ flex:0 0 auto; width:520px; height:100%; - display:flex; flex-direction:column; justify-content:center; gap:7px; + display:flex; flex-direction:column; justify-content:center; gap:6px; padding:0 16px 0 0; background:transparent; border:none; } .we-info{display:flex; align-items:center; gap:12px; flex-shrink:0; color:#e6f0fb;} @@ -253,6 +253,16 @@ body { background: #0a0e17; overflow: hidden; font-family: 'Microsoft YaHei', sa .we-pct{margin-left:2px; font-size:13px; font-weight:700; color:#c4b5fd; min-width:34px; text-align:right;} .we-bar{width:100%; height:14px; background:rgba(255,255,255,.10); border-radius:7px; overflow:hidden;} .we-fill{height:100%; width:0%; background:linear-gradient(90deg,#8b5cf6,#4af); border-radius:7px; transition:width .3s ease; box-shadow:0 0 12px rgba(139,92,246,.45);} +.we-currencies{display:flex; align-items:center; gap:8px; margin-left:auto; flex-shrink:0;} +.wc-item{display:flex; align-items:center; gap:4px; font-size:12px; font-weight:700; color:#e6f0fb; background:rgba(255,255,255,.06); padding:3px 8px; border-radius:8px; border:1px solid rgba(255,255,255,.08); white-space:nowrap;} +.wc-item b{font-variant-numeric:tabular-nums;} +@media (max-width:768px){ + .we-currencies{gap:5px;} + .wc-item{font-size:11px; padding:2px 6px;} +} +@media (max-width:420px){ + .we-currencies{display:none;} +} /* 主沙盘底部功能框架(预留区:放功能按钮 / 素材槽,比微观边框更高) */ #bottomBar { @@ -1254,6 +1264,12 @@ body { background: #0a0e17; overflow: hidden; font-family: 'Microsoft YaHei', sa
世界 Lv.-- -- +
+ 🪙 0 + 🥉 0 + 🥈 0 + 💎 0 +
EXP -- / -- --%
@@ -2399,8 +2415,11 @@ async function microLoadGame() { try { const [c, cr] = await Promise.all([API.char(), API.creatures(), microLoadEquipCatalog()]); microChar = (c && c.ok !== false) ? c : (c || null); - if (microChar && !microChar.equip) microChar.equip = { weapon: null, armor: null, accessory: null }; - if (microChar && !microChar.inventory) microChar.inventory = {}; + if (microChar) { + if (!microChar.equip) microChar.equip = { weapon: null, armor: null, accessory: null }; + if (!microChar.inventory) microChar.inventory = {}; + ['gold','copper','silver','diamond'].forEach(k => { if (microChar[k] == null) microChar[k] = 0; }); + } microCreatureOverlay = {}; if (cr && cr.ok && Array.isArray(cr.creatures)) { cr.creatures.forEach(x => { if (x.cid) microCreatureOverlay[x.cid] = x; }); @@ -2476,14 +2495,21 @@ function microRenderCharHud() { updateGoldEditorDisplay(); } -// 顶部金币编辑器:测试阶段调试角色金币 +// 顶部金币编辑器 + 底部世界经验条货币展示(共用角色货币数据) function updateGoldEditorDisplay() { const editor = document.getElementById('goldEditor'); const current = document.getElementById('goldCurrent'); - if (!editor || !current) return; - if (!microChar) { editor.style.display = 'none'; return; } - editor.style.display = 'inline-flex'; - current.textContent = Math.floor(microChar.gold || 0); + if (editor) editor.style.display = microChar ? 'inline-flex' : 'none'; + if (current) current.textContent = Math.floor((microChar && microChar.gold) || 0); + // 底部世界经验条:金币/铜币/银币/钻石 + const setWc = (id, val) => { + const el = document.getElementById(id); + if (el) el.textContent = Math.floor(val || 0); + }; + setWc('wcGold', microChar ? microChar.gold : 0); + setWc('wcCopper', microChar ? microChar.copper : 0); + setWc('wcSilver', microChar ? microChar.silver : 0); + setWc('wcDiamond', microChar ? microChar.diamond : 0); } async function changeGoldEditor(delta) { if (!microChar) { microToast('角色未加载,无法修改金币'); return; } @@ -2494,7 +2520,10 @@ async function changeGoldEditor(delta) { try { const r = await API.saveChar({ gold: next }); if (r && r.ok && r.char) { - microChar.gold = r.char.gold; + microChar.gold = r.char.gold || 0; + microChar.copper = r.char.copper || 0; + microChar.silver = r.char.silver || 0; + microChar.diamond = r.char.diamond || 0; updateGoldEditorDisplay(); microRenderCharHud(); microToast(`金币已调整为 🪙${microChar.gold}`); @@ -2536,8 +2565,11 @@ async function emitTaskEvent(event, payload) { 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; + microChar.exp_max = r.char.exp_max; microChar.gold = r.char.gold || 0; + microChar.copper = r.char.copper || 0; microChar.silver = r.char.silver || 0; + microChar.diamond = r.char.diamond || 0; microRenderCharHud(); + updateGoldEditorDisplay(); } const body = document.getElementById('taskPanelBody'); if (body) { @@ -2564,7 +2596,9 @@ async function fetchTasks() { 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, + exp_max: r.char.exp_max, gold: r.char.gold || 0, + copper: r.char.copper || 0, silver: r.char.silver || 0, + diamond: r.char.diamond || 0, equip: { weapon: null, armor: null, accessory: null }, inventory: {} }; @@ -2573,7 +2607,10 @@ async function fetchTasks() { microChar.level = r.char.level; microChar.exp = r.char.exp; microChar.exp_max = r.char.exp_max; - microChar.gold = r.char.gold; + microChar.gold = r.char.gold || 0; + microChar.copper = r.char.copper || 0; + microChar.silver = r.char.silver || 0; + microChar.diamond = r.char.diamond || 0; } microRenderCharHud(); updateGoldEditorDisplay(); @@ -2668,8 +2705,12 @@ async function claimTask(code, btn) { microChar.level = r.char.level; microChar.exp = r.char.exp; microChar.exp_max = r.char.exp_max; - microChar.gold = r.char.gold; + microChar.gold = r.char.gold || 0; + microChar.copper = r.char.copper || 0; + microChar.silver = r.char.silver || 0; + microChar.diamond = r.char.diamond || 0; microRenderCharHud(); + updateGoldEditorDisplay(); } const gained = []; if (r.reward_exp) gained.push(`⭐${r.reward_exp} 经验`); @@ -4610,7 +4651,7 @@ async function onMicroUp(e) { return; } if (microChar) microChar.gold = res.gold; - microRenderCharHud(); + microRenderCharHud(); updateGoldEditorDisplay(); } microRoadPaintBefore = 0; scheduleBuildSave(); microToast('道路已铺设'); @@ -5307,8 +5348,16 @@ async function microUnitRest(unitId) { // 保存角色(金币/背包/装备)到 Odoo async function updateChar() { if (!microChar) return; - const r = await API.saveChar({ gold: microChar.gold, inventory: microChar.inventory, equip: microChar.equip }); - if (r && r.ok && r.char) microChar = r.char; + const r = await API.saveChar({ + gold: microChar.gold, copper: microChar.copper, silver: microChar.silver, + diamond: microChar.diamond, inventory: microChar.inventory, equip: microChar.equip + }); + if (r && r.ok && r.char) { + microChar = r.char; + if (!microChar.equip) microChar.equip = { weapon: null, armor: null, accessory: null }; + if (!microChar.inventory) microChar.inventory = {}; + ['gold','copper','silver','diamond'].forEach(k => { if (microChar[k] == null) microChar[k] = 0; }); + } } function closeUnitModal() { microOpenUnitId = null; document.getElementById('unitModal').classList.remove('active'); } @@ -5413,8 +5462,11 @@ async function microBuildingAct(type, action, label) { } // 同步角色金币 / 背包 if (res.gold != null) { microChar = microChar || {}; microChar.gold = res.gold; } + if (res.copper != null && microChar) microChar.copper = res.copper; + if (res.silver != null && microChar) microChar.silver = res.silver; + if (res.diamond != null && microChar) microChar.diamond = res.diamond; if (res.inventory && microChar) microChar.inventory = res.inventory; - microRenderCharHud(); + microRenderCharHud(); updateGoldEditorDisplay(); // 发射任务事件:建筑交互 + 资源产出 const _bkey = type + '_' + action; emitTaskEvent('building_action', { key: _bkey }); @@ -6611,7 +6663,7 @@ async function microCommitPlace() { return; } if (microChar) microChar.gold = res.gold; - microRenderCharHud(); + microRenderCharHud(); updateGoldEditorDisplay(); } 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 }); @@ -8277,6 +8329,8 @@ if (typeof fitWorld === 'function') fitWorld(); microChar = { name: fresh.name, level: fresh.level, exp: fresh.exp, exp_max: fresh.exp_max || 100, gold: fresh.gold || 0, + copper: fresh.copper || 0, silver: fresh.silver || 0, + diamond: fresh.diamond || 0, equip: fresh.equip || { weapon: null, armor: null, accessory: null }, inventory: fresh.inventory || {} }; -- 2.34.1