diff --git a/addons/game_base/controllers/chat_api.py b/addons/game_base/controllers/chat_api.py index 129b7c1..6b1e02f 100644 --- a/addons/game_base/controllers/chat_api.py +++ b/addons/game_base/controllers/chat_api.py @@ -2,6 +2,7 @@ from odoo import http from odoo.http import request from .game_api import _json, _post_body +from ..models.chat import GUIDE_NAME class ChatApiController(http.Controller): @@ -12,13 +13,14 @@ class ChatApiController(http.Controller): @http.route('/game/api/chat/channels', type='http', auth='public', methods=['GET'], csrf=False) def chat_channels(self, **kw): - """当前用户可见频道:世界频道(公开) + 自己参与的私聊/公会。""" + """当前用户可见频道:世界频道 + AI向导频道(均公开) + 自己参与的私聊/公会。""" uid = request.session.uid Channel = request.env['game.chat.channel'].sudo() - domain = [('channel_type', '=', 'world')] + public = [('channel_type', 'in', ['world', 'guide'])] if uid: - domain = ['|', ('channel_type', '=', 'world'), - ('member_ids', 'in', [uid])] + domain = ['|'] + public + [('member_ids', 'in', [uid])] + else: + domain = public recs = Channel.search_read(domain, ['id', 'name', 'channel_type'], order='sequence, id') return _json({'items': [{ @@ -60,6 +62,7 @@ class ChatApiController(http.Controller): 'create_date': str(r['create_date']), 'is_self': bool(uid and r.get('author_id') and r['author_id'][0] == uid), + 'is_guide': bool(r['author_name'] == GUIDE_NAME), } for r in recs] return _json({'items': items}) @@ -92,6 +95,26 @@ class ChatApiController(http.Controller): 'author_name': author_name, 'body': body, }) + # AI 向导自动回复:仅对 guide 类型频道生效,与世界频道隔离 + guide_reply = None + ch = request.env['game.chat.channel'].sudo().browse(channel_id) + if ch.exists() and ch.channel_type == 'guide': + reply_text = ch.generate_guide_reply(body) + if reply_text: + reply = request.env['game.chat.message'].sudo().create({ + 'channel_id': channel_id, + 'author_id': False, + 'author_name': GUIDE_NAME, + 'body': reply_text, + }) + guide_reply = { + 'id': reply.id, + 'author_name': GUIDE_NAME, + 'body': reply_text, + 'create_date': str(reply.create_date), + 'is_self': False, + 'is_guide': True, + } return _json({ 'ok': True, 'message': { @@ -100,5 +123,6 @@ class ChatApiController(http.Controller): 'body': body, 'create_date': str(msg.create_date), 'is_self': True, - } + }, + 'guide_reply': guide_reply, }) diff --git a/addons/game_base/controllers/game_api.py b/addons/game_base/controllers/game_api.py index ab64c46..ed6f643 100644 --- a/addons/game_base/controllers/game_api.py +++ b/addons/game_base/controllers/game_api.py @@ -22,10 +22,18 @@ def _json(data): def _post_body(): try: - raw = request.httprequest.get_data(as_text=True) or '{}' - return json.loads(raw) + # Odoo 官方解析 JSON 请求体(正确处理 UTF-8),优先使用。 + return request.get_json_data() except Exception: - return {} + try: + # 兜底:显式按 UTF-8 解码原始字节,避免 Werkzeug 在 application/json + # 无 charset 时按 latin1 解码中文导致乱码。 + raw = request.httprequest.get_data(cache=True) or b'{}' + if isinstance(raw, str): + raw = raw.encode('utf-8') + return json.loads(raw.decode('utf-8')) + except Exception: + return {} class GameApiController(http.Controller): diff --git a/addons/game_base/data/chat_data.xml b/addons/game_base/data/chat_data.xml index 73b0462..0960726 100644 --- a/addons/game_base/data/chat_data.xml +++ b/addons/game_base/data/chat_data.xml @@ -1,11 +1,18 @@ - - + + 世界频道 world 10 + + + + 游戏助手 + guide + 20 + diff --git a/addons/game_base/models/chat.py b/addons/game_base/models/chat.py index 69c908c..192ecfd 100644 --- a/addons/game_base/models/chat.py +++ b/addons/game_base/models/chat.py @@ -1,6 +1,17 @@ # -*- coding: utf-8 -*- +import re from odoo import models, fields +# 游戏助手的展示昵称(前后端共用,落库消息的作者名) +GUIDE_NAME = '游戏助手' + + +def _plain(text): + """去掉 HTML 标签,用于把 rule.desc(Html) 当作纯文本兜底。""" + if not text: + return '' + return re.sub(r'<[^>]+>', '', text).strip() + class GameChatChannel(models.Model): _name = 'game.chat.channel' @@ -12,12 +23,76 @@ class GameChatChannel(models.Model): ('world', '世界频道'), ('guild', '公会'), ('pm', '私聊'), + ('guide', 'AI向导'), ], string='类型', default='world', index=True) sequence = fields.Integer(string='排序', default=10) creator_id = fields.Many2one('res.users', string='创建者') member_ids = fields.Many2many('res.users', string='成员') message_ids = fields.One2many('game.chat.message', 'channel_id', string='消息') + def generate_guide_reply(self, question): + """内置游戏知识引擎(零外部依赖):按关键词检索 game.rule / game.rule.category 作答。 + 预留扩展点:若需接入真实 LLM,替换本方法实现、返回模型生成的文本即可,其余调用方不变。 + """ + q = (question or '').strip() + if not q: + return '你好,我是游戏助手。你可以问我关于「属性」「等级」「货币」「制度」的问题~' + env = self.env + Rule = env['game.rule'] + Cat = env['game.rule.category'] + + def rules_of(key): + c = Cat.search([('key', '=', key)], limit=1) + return Rule.search([('category_id', '=', c.id)], order='name') if c else [] + + def line(r): + return f'· {r.name}:{r.summary or _plain(r.desc)}' + + # 问候 + if re.search(r'(你好|您好|hi|hello|在吗|嗨|早上好|晚上好)', q, re.I): + return ('你好,我是游戏助手 🤖 随时为你解答宇森世界的玩法。\n' + '可以问我:属性怎么算?怎么升级?货币有什么用?有哪些制度?') + + # 属性(关键词覆盖攻击/物攻/法攻/防御/暴击等;穿透/韧性已移除) + _attr_re = re.search(r'(属性|攻击|物攻|法攻|防御|物防|法防|血量|生命|法力|速度|攻速|暴击|爆伤|命中|闪避|格挡|抗性|吸血|幸运)', q) + if _attr_re: + rules = list(rules_of('attribute_basic')) + list(rules_of('attribute_advanced')) + if rules: + head = '【属性规则】不同属性决定你的战斗表现,核心属性如下:' + return head + '\n' + '\n'.join(line(r) for r in rules[:12]) + '\n\n想深入了解某项,直接问我它的名字~' + + # 等级 + if re.search(r'(等级|升级|经验|世界等级|角色等级|怎么变强|变强)', q): + for key in ('world', 'character'): + rules = rules_of(key) + if rules: + c = Cat.search([('key', '=', key)], limit=1) + head = f'【{c.name}】' + lines = [f'· {r.name}(Lv{r.level}):{r.summary or ""}' for r in rules[:6]] + return head + '\n' + '\n'.join(l for l in lines if l.strip()) + + # 货币 + if re.search(r'(货币|金币|钱|元宝|钻石|充值|兑换|经济)', q): + rules = rules_of('currency') + if rules: + head = '【货币规则】游戏内货币与获取/消耗方式:' + return head + '\n' + '\n'.join(line(r) for r in rules[:8]) + + # 制度 / 规则 + if re.search(r'(制度|规定|规则|条款|公约|不能|禁止|惩罚|封号)', q): + rules = rules_of('system') + if rules: + head = '【制度说明】请遵守以下基本规则:' + return head + '\n' + '\n'.join(line(r) for r in rules[:8]) + + # 兜底:给出可问的方向 + cats = Cat.search( + [('key', 'in', ['world', 'character', 'currency', 'attribute', 'system'])], + order='sequence') + tips = '、'.join(c.name for c in cats) if cats else '属性、等级、货币、制度' + return ('我是游戏助手,这个问题我还在学习中 🤔\n' + f'你可以问我这些方向:{tips}。\n例如:「攻击和物防怎么算?」「怎么升级最快?」') + class GameChatMessage(models.Model): _name = 'game.chat.message' diff --git a/addons/game_base/views/asset_views.xml b/addons/game_base/views/asset_views.xml index 9a850c5..addf8e6 100644 --- a/addons/game_base/views/asset_views.xml +++ b/addons/game_base/views/asset_views.xml @@ -2,10 +2,52 @@ + + + yusen.attachment.kanban + ir.attachment + + + + + + + + +
+
+ 🖼️ + 📎 +
+
+ +
+ + — +
+
+ + MB + + + KB + + + B + +
+
+
+
+
+
+
+
+ 附件 ir.attachment - list,form + kanban,list,form {'search_default_filter_website': 0} diff --git a/addons/game_base/views/chat_views.xml b/addons/game_base/views/chat_views.xml index d4dbc26..977bffe 100644 --- a/addons/game_base/views/chat_views.xml +++ b/addons/game_base/views/chat_views.xml @@ -1,13 +1,26 @@ + + + 世界聊天 + 10 + + - 世界聊天 + 频道 game.chat.channel list,form + + 频道 + + + 10 + + game.chat.channel.list game.chat.channel @@ -46,4 +59,53 @@ + + + 聊天消息 + game.chat.message + list,form + {'search_default_group_channel': 1} + + + + game.chat.message.list + game.chat.message + + + + + + + + + + + + game.chat.message.form + game.chat.message + +
+ + + + + + + + + + + +
+
+
+ + + + 聊天消息 + + + 20 + +
diff --git a/addons/game_base/views/game_employee_views.xml b/addons/game_base/views/game_employee_views.xml index bca1b93..abccf30 100644 --- a/addons/game_base/views/game_employee_views.xml +++ b/addons/game_base/views/game_employee_views.xml @@ -7,6 +7,14 @@ hr.employee + + + 1 + + + 1 + + diff --git a/addons/yt_world/views/yt_world_views.xml b/addons/yt_world/views/yt_world_views.xml index 6a48fd1..fa2918d 100644 --- a/addons/yt_world/views/yt_world_views.xml +++ b/addons/yt_world/views/yt_world_views.xml @@ -442,15 +442,14 @@ - - + + 玩家 - res.users + hr.employee list,form - {'search_default_active': 1, 'search_default_share': 0} - 游戏玩家列表。点「玩家世界」菜单可按用户筛其下属的全部世界/地块/微观/角色/生物等数据。 + {'search_default_active': 1} + 游戏玩家(员工)列表。每位玩家对应一个 hr.employee,关联其 Odoo 登录账户。 diff --git a/addons/yusen_website/data/rule_data.xml b/addons/yusen_website/data/rule_data.xml index 72b1e83..cb30354 100644 --- a/addons/yusen_website/data/rule_data.xml +++ b/addons/yusen_website/data/rule_data.xml @@ -124,4 +124,132 @@ + + + attribute_basic + 基础属性 + 50 + + + attribute_advanced + 高级属性 + 51 + + + + + + 0生命值 + + + + + 0法力值 + + + + + + + 0物攻 + + + + + 0法攻 + + + + + + + 0防御 + + + + + 0物防 + + + + + 0法防 + + + + + + + 0速度 + + + + + 0攻速 + + + + + + + + 0暴击 + + + + + 0爆伤 + + + + + + + 0命中 + + + + + 0闪避 + + + + + + + 0格挡 + + + + + + + 0抗性 + + + + + 0吸血 + + + + + 0幸运 + + + +
diff --git a/assets/buildings/default/resource_4x4_20260813.png b/assets/buildings/default/resource_4x4_20260813.png new file mode 100644 index 0000000..69299ac Binary files /dev/null and b/assets/buildings/default/resource_4x4_20260813.png differ diff --git a/assets/chat.js b/assets/chat.js index 9e60d75..e7bad00 100644 --- a/assets/chat.js +++ b/assets/chat.js @@ -6,9 +6,10 @@ let channels = []; let activeChannel = null; // {id, name} let lastId = 0; // 已渲染的最大消息 id(增量轮询基准) + let renderedIds = new Set(); // 已渲染消息 id,避免即时渲染与轮询重复 let pollTimer = null; - const TYPE_ICON = { world: '🌐', guild: '🛡️', pm: '💬' }; + const TYPE_ICON = { world: '🌐', guild: '🛡️', pm: '💬', guide: '🤖' }; // ---------- 工具 ---------- function fmtTime(s) { @@ -54,9 +55,12 @@ } function renderMessage(m, prepend) { + if (renderedIds.has(m.id)) return; // 去重:即时渲染与轮询可能重复拉到同一消息 + renderedIds.add(m.id); + const isGuide = !!m.is_guide; const row = document.createElement('div'); - row.className = 'row' + (m.is_self ? ' self' : ''); - const initial = (m.author_name || '?').slice(0, 1); + row.className = 'row' + (m.is_self ? ' self' : '') + (isGuide ? ' guide' : ''); + const initial = isGuide ? '🤖' : (m.author_name || '?').slice(0, 1); row.innerHTML = `
${escapeHtml(initial)}
@@ -99,8 +103,12 @@ async function selectChannel(ch) { activeChannel = ch; lastId = 0; + renderedIds.clear(); $('messages').innerHTML = ''; $('chTitle').textContent = ch.name; + $('chSub').textContent = ch.type === 'guide' + ? '向宇森向导提问,了解属性 / 等级 / 货币 / 制度' + : '和大家聊聊宇森世界吧'; renderChannels(); setStatus('加载中…'); const data = await api(`/game/api/chat/messages?channel_id=${ch.id}&limit=50`); @@ -138,8 +146,8 @@ btn.disabled = false; if (data.ok) { ta.value = ''; - // 本地立即渲染(轮询也会拉到,靠 id 去重:renderMessage 只更新 lastId,不重复插入) renderMessage(data.message, false); + if (data.guide_reply) renderMessage(data.guide_reply, false); scrollBottom(); } else { setStatus(data.error || '发送失败'); diff --git a/chat.html b/chat.html index acef137..fdfb385 100644 --- a/chat.html +++ b/chat.html @@ -81,43 +81,48 @@ .chat-head .sub { font-size: 12px; color: var(--txt-dim); } .messages { - flex: 1; overflow-y: auto; padding: 22px 22px 8px; - display: flex; flex-direction: column; gap: 14px; + flex: 1 1 auto; max-height: 290px; overflow-y: auto; padding: 16px 22px 6px; + display: flex; flex-direction: column; gap: 10px; scroll-behavior: smooth; } - .row { display: flex; gap: 10px; max-width: 76%; } + .row { display: flex; gap: 8px; max-width: 76%; } .row.self { align-self: flex-end; flex-direction: row-reverse; } .row .ava { - width: 36px; height: 36px; border-radius: 10px; flex-shrink: 0; + width: 34px; height: 34px; border-radius: 10px; flex-shrink: 0; background: var(--bg-card); display: flex; align-items: center; justify-content: center; font-size: 13px; color: var(--txt-dim); border: 1px solid var(--line); } .row.self .ava { background: linear-gradient(135deg, var(--accent), #3b82f6); color: #06121a; border: none; } - .bubble-wrap { display: flex; flex-direction: column; gap: 4px; min-width: 0; } + .bubble-wrap { display: flex; flex-direction: column; gap: 3px; min-width: 0; } .row.self .bubble-wrap { align-items: flex-end; } .meta { font-size: 11px; color: var(--txt-dim); display: flex; gap: 8px; } .bubble { - padding: 10px 14px; border-radius: 14px; line-height: 1.55; + padding: 8px 12px; border-radius: 13px; line-height: 1.5; background: var(--bubble-other); color: var(--txt); font-size: 14px; word-break: break-word; white-space: pre-wrap; } .row.self .bubble { background: var(--bubble-self); color: #06121a; font-weight: 500; } + /* AI 向导消息:紫色区分,与世界频道玩家消息隔离 */ + .row.guide .ava { background: linear-gradient(135deg, #a78bfa, #7c3aed); color: #fff; border: none; } + .row.guide .bubble { background: linear-gradient(135deg, #2a2150, #3b2f63); color: #ede9fe; border: 1px solid #4c3a82; } + .row.guide .meta span:first-child { color: #c4b5fd; } + /* 输入区 */ .composer { - border-top: 1px solid var(--line); padding: 14px 18px; - display: flex; gap: 12px; align-items: flex-end; background: var(--bg-soft); + border-top: 1px solid var(--line); padding: 8px 14px; + display: flex; gap: 8px; align-items: flex-end; background: var(--bg-soft); } .composer textarea { - flex: 1; resize: none; height: 46px; max-height: 120px; - background: var(--bg-card); border: 1px solid var(--line); border-radius: 12px; - color: var(--txt); padding: 12px 14px; font-size: 14px; font-family: inherit; + flex: 1; resize: none; height: 34px; max-height: 120px; + background: var(--bg-card); border: 1px solid var(--line); border-radius: 10px; + color: var(--txt); padding: 7px 11px; font-size: 13.5px; font-family: inherit; outline: none; transition: border-color .18s; } .composer textarea:focus { border-color: var(--accent); } .composer button { - height: 46px; padding: 0 22px; border: none; border-radius: 12px; - background: var(--bubble-self); color: #06121a; font-weight: 600; font-size: 14px; + height: 34px; padding: 0 16px; border: none; border-radius: 10px; + background: var(--bubble-self); color: #06121a; font-weight: 600; font-size: 13px; cursor: pointer; transition: transform .12s, filter .12s; } .composer button:hover { filter: brightness(1.06); } diff --git a/codex.html b/codex.html index 19af7dc..246c174 100644 --- a/codex.html +++ b/codex.html @@ -58,13 +58,18 @@ body::before{ } .searchBox .clear:hover{opacity:1;color:#ffb4b4;} -/* 分类标签 */ -.catTab{ - padding:5px 14px;border-radius:999px;font-size:14px;cursor:pointer; - background:rgba(255,255,255,.04);border:1px solid rgba(255,255,255,.08);color:#9fb3cc;transition:.18s; +/* ===== 分类页签(与规则页统一风格) ===== */ +.rule-tabs{ + display:flex;gap:10px;flex-wrap:wrap;justify-content:flex-start; + padding:6px 0 8px; } -.catTab:hover{background:rgba(139,92,246,.14);color:#d0dfff;border-color:rgba(139,92,246,.3);} -.catTab.active{background:rgba(139,92,246,.2);color:#fff;border-color:rgba(139,92,246,.5);box-shadow:0 0 12px rgba(139,92,246,.2);} +.rule-tab{ + padding:8px 20px;border-radius:999px;font-size:15px;cursor:pointer;user-select:none; + background:rgba(255,255,255,.04);border:1px solid rgba(255,255,255,.08);color:#9fb3cc;transition:.18s; + font-family:"ZCOOL QingKe HuangYou",sans-serif;letter-spacing:1px; +} +.rule-tab:hover{background:rgba(139,92,246,.14);color:#d0dfff;border-color:rgba(139,92,246,.3);} +.rule-tab.active{background:rgba(139,92,246,.2);color:#fff;border-color:rgba(139,92,246,.5);box-shadow:0 0 12px rgba(139,92,246,.25);} /* 品质筛点 */ .ql{display:inline-flex;align-items:center;gap:6px;cursor:pointer;padding:4px 11px;border-radius:8px;transition:.15s;} @@ -107,12 +112,14 @@ body::before{ .card.q-rare::before{background:linear-gradient(90deg,#3b82f6,#60a5fa);} .card.q-epic::before{background:linear-gradient(90deg,#a855f7,#c084fc);} .card.q-legendary::before{background:linear-gradient(90deg,#f59e0b,#fbbf24);} +.card.q-mythic::before{background:linear-gradient(90deg,#dc2626,#f59e0b);} /* 品质光晕 */ .card.q-common:hover{box-shadow:0 8px 24px rgba(0,0,0,.4),0 0 20px rgba(156,163,175,.08);} .card.q-uncommon:hover{box-shadow:0 8px 24px rgba(0,0,0,.4),0 0 20px rgba(34,197,94,.12);} .card.q-rare:hover{box-shadow:0 8px 24px rgba(0,0,0,.4),0 0 20px rgba(59,130,246,.12);} .card.q-epic:hover{box-shadow:0 8px 24px rgba(0,0,0,.4),0 0 20px rgba(168,85,247,.12);} .card.q-legendary:hover{box-shadow:0 8px 24px rgba(0,0,0,.4),0 0 20px rgba(245,158,11,.12);} +.card.q-mythic:hover{box-shadow:0 8px 24px rgba(0,0,0,.4),0 0 24px rgba(220,38,38,.18),0 0 12px rgba(245,158,11,.1);} /* 品质角标 */ .card .qbadge{ @@ -125,6 +132,7 @@ body::before{ .qbadge.rare{background:rgba(59,130,246,.15);color:#60a5fa;border:1px solid rgba(59,130,246,.3);} .qbadge.epic{background:rgba(168,85,247,.15);color:#c084fc;border:1px solid rgba(168,85,247,.3);} .qbadge.legendary{background:rgba(245,158,11,.15);color:#fbbf24;border:1px solid rgba(245,158,11,.3);} +.qbadge.mythic{background:linear-gradient(135deg,rgba(220,38,38,.18),rgba(245,158,11,.18));color:#f87171;border:1px solid rgba(220,38,38,.35);text-shadow:0 0 6px rgba(245,158,11,.4);} /* 生物/野怪/种族 专属样式 */ .card.creature{border-style:solid;border-color:rgba(100,200,255,.12);} @@ -249,23 +257,19 @@ body::before{
-
- 分类 - 🏛️ 全部 - 🪵 材料 - 🏭 加工品 - 🐾 生物 - 👹 野怪 - 🌍 种族 - ✨ 特殊 - +
+ 🏛️ 全部 + 🪵 材料 + 🏭 加工品 + 🐾 生物 + 👹 野怪 + 🌍 种族 + 🌟 天赋 + 📖 技能书 + 📚 书籍 + ✨ 特殊
- 品质 全部 @@ -290,6 +294,15 @@ body::before{ 传说 + + + 神话 + +
@@ -306,10 +319,110 @@ body::before{ // 宇森图鉴 — 全部数据来自后端 Odoo (yt_world 模块),前端无硬编码默认数据 // ===================================================================== -const QUALITIES = ['common','uncommon','rare','epic','legendary']; -const QUALITY_NAMES = { common:'普通', uncommon:'优秀', rare:'精良', epic:'史诗', legendary:'传说' }; +const QUALITIES = ['common','uncommon','rare','epic','legendary','mythic']; +const QUALITY_NAMES = { common:'普通', uncommon:'优秀', rare:'精良', epic:'史诗', legendary:'传说', mythic:'神话' }; const TIER_NAMES = { normal:'普通野怪', elite:'精英野怪', boss:'Boss' }; -const CAT_ORDER = ['material','processed','creature','monster','race','special']; +const CAT_ORDER = ['material','processed','creature','monster','race','talent','skill_book','book','special']; + +// ===== 天赋预置数据(前端硬编码,后端就绪后迁移) ===== +const TALENT_DATA = [ + { code:'talent_ancient_blood', name:'先民血脉', icon:'🩸', quality:'mythic', + trigger:'被动', effect:'生命回复+20%;濒死时触发先民护盾,5秒内免伤', + desc:'流淌着远古先民的血脉,身体拥有超乎寻常的自愈与守护之力。传说中先民以此血脉抵御了灭世之灾。' }, + { code:'talent_dragon_soul', name:'龙裔之魂', icon:'🐉', quality:'mythic', + trigger:'被动', effect:'火焰抗性+50%;攻击附带灼烧效果', + desc:'体内沉睡着巨龙的灵魂碎片,对火焰有着天然的亲和与免疫,攻击时能唤出龙息灼烧敌人。' }, + { code:'talent_star_resonance', name:'星辰共鸣', icon:'✨', quality:'legendary', + trigger:'被动', effect:'法力回复+15%;夜间额外+10%', + desc:'与星辰之力产生共鸣,能够汲取星光转化为法力。夜幕降临时,这份共鸣尤为强烈。' }, + { code:'talent_beast_king', name:'兽王本能', icon:'🐾', quality:'epic', + trigger:'被动', effect:'驯养生物好感度+30%;生物产出+20%', + desc:'天生与野兽亲和,能感知它们的情绪与需求。驯养的生物更加忠诚,产出也更加丰厚。' }, + { code:'talent_smith_hand', name:'铁匠之手', icon:'🔨', quality:'rare', + trigger:'被动', effect:'加工品品质+1阶;加工成功率+10%', + desc:'双手有着与生俱来的锻造天赋,加工材料时更加精准,成品的品质也往往超出预期。' }, + { code:'talent_forest_walker', name:'林间行者', icon:'🌿', quality:'rare', + trigger:'被动', effect:'森林地形移速+25%;隐蔽+15%', + desc:'在密林中如鱼得水,脚步轻盈无声,仿佛与森林融为一体。敌人很难在林间发现你的踪迹。' }, + { code:'talent_ore_sense', name:'矿脉感应', icon:'⛏️', quality:'uncommon', + trigger:'被动', effect:'发现稀有矿脉概率+20%', + desc:'对地底的矿脉有着敏锐的直觉,总能在别人忽略的地方发现珍贵的矿石。' }, + { code:'talent_harvest_blessing', name:'丰收祝福', icon:'🌾', quality:'uncommon', + trigger:'被动', effect:'农作物产量+15%;生长速度+10%', + desc:'受到大地之灵的祝福,经手种植的作物总是长势喜人,收获时也比旁人更加丰盈。' }, +]; + +// ===== 技能书预置数据(前端硬编码,后端就绪后迁移) ===== +const SKILL_BOOK_DATA = [ + { code:'sb_inferno_v1', name:'烈焰术·卷一', icon:'🔥', quality:'rare', + skillType:'主动', learnLevel:5, cost:'法力 20', + effect:'释放火焰法术,对前方3格内敌人造成灼烧伤害,持续4秒', + desc:'记载着基础火焰法术的入门卷轴。虽然只是卷一,但修习者能感受到火焰之力的澎湃。' }, + { code:'sb_frost_domain_frag', name:'冰霜领域·残卷', icon:'❄️', quality:'epic', + skillType:'主动', learnLevel:15, cost:'法力 50', + effect:'冻结周围3格内所有敌人,持续3秒,破冰时受到额外伤害', + desc:'残缺的古老卷轴,记载着冰霜领域的部分奥义。虽不完整,但冰封万物的威力已然惊人。' }, + { code:'sb_thunder_judgment_full', name:'雷霆审判·全本', icon:'⚡', quality:'mythic', + skillType:'主动', learnLevel:30, cost:'法力 80', + effect:'召唤天雷轰击目标区域,无视火焰/冰霜抗性,造成毁灭性雷击伤害', + desc:'完整的雷霆审判秘典。传说修习至大成者可引天雷降世,审判一切罪恶。非大毅力者不可修习。' }, + { code:'sb_ironwall_secret', name:'铁壁守护·秘典', icon:'🛡️', quality:'rare', + skillType:'被动', learnLevel:8, cost:'—', + effect:'物理减伤+15%;格挡率+10%', + desc:'铁壁一脉代代相传的守护秘典。修习者身如铁壁,能为同伴抵挡致命一击。' }, + { code:'sb_gale_step_frag', name:'疾风步·残页', icon:'💨', quality:'uncommon', + skillType:'主动', learnLevel:3, cost:'法力 10', + effect:'短暂提升移速40%,持续5秒,冷却15秒', + desc:'仅存数页的残破秘籍,记载着疾风步的基本步法。虽不完整,但足以让修习者步履如风。' }, + { code:'sb_life_drain_forbidden', name:'生命汲取·禁典', icon:'💀', quality:'legendary', + skillType:'被动', learnLevel:20, cost:'—', + effect:'攻击时回复造成伤害10%的生命;击杀时额外回复15%最大生命', + desc:'被列为禁术的黑暗典籍。修习者以杀养身,越战越强,但也容易迷失在嗜血的渴望中。慎之。' }, +]; + +// ===== 书籍预置数据(前端硬编码,后端就绪后迁移) ===== +const BOOK_DATA = [ + { code:'book_genesis', name:'宇森创世纪', icon:'📖', bookType:'历史', + author:'佚名·先民', chapters:12, + desc:'记载世界诞生的远古文献。从混沌初开到万物生长,从先民觉醒到百族兴起,宇森世界的起源尽在此书中。' }, + { code:'book_hundred_wars', name:'百族大战纪略', icon:'📜', bookType:'历史', + author:'史官·凌岳', chapters:8, + desc:'记录各族群之间那场旷日持久的远古战争。战争的起因、过程与结局,以及那些被遗忘的英雄与罪人。' }, + { code:'book_star_child', name:'星辰之子传说', icon:'✨', bookType:'传说', + author:'游吟诗人·散人', chapters:6, + desc:'流传于民间的英雄史诗。讲述一个诞生于星光之下的孩子,如何走出荒野、拯救苍生的故事。真伪已不可考。' }, + { code:'book_vein_walk', name:'地脉行走指南', icon:'🗺️', bookType:'秘闻', + author:'探险家·石锤', chapters:15, + desc:'记录地底矿脉走向与秘境入口的探险手册。地脉行走者的必备指南,据说按图索骥可寻得珍稀矿藏。' }, + { code:'book_codex_vol1', name:'万物图鉴·初卷', icon:'📔', bookType:'图鉴', + author:'图鉴师·青木', chapters:20, + desc:'汇编已知生物与资源的初步图鉴。虽只是初卷,却已涵盖数百种生灵与材料,是图鉴师的入门必读。' }, + { code:'book_ancient_remnant', name:'先民遗书·残篇', icon:'🔏', bookType:'秘闻', + author:'先民·佚名', chapters:3, + desc:'先民留下的残缺手稿,字迹斑驳难辨。隐约可见关于古老力量与禁忌之地的记载,暗藏着不为人知的秘密。' }, +]; + +function normTalent(t){ + return { + type:'talent', id:t.code, cat:'talent', name:t.name, icon:t.icon||'🌟', + qual:t.quality||'rare', trigger:t.trigger||'被动', effect:t.effect||'', + desc:t.description||t.desc||'' + }; +} +function normSkillBook(s){ + return { + type:'skill_book', id:s.code, cat:'skill_book', name:s.name, icon:s.icon||'📖', + qual:s.quality||'common', skillType:s.skillType||'主动', learnLevel:s.learnLevel||1, + cost:s.cost||'—', effect:s.effect||'', desc:s.description||s.desc||'' + }; +} +function normBook(b){ + return { + type:'book', id:b.code, cat:'book', name:b.name, icon:b.icon||'📚', + bookType:b.bookType||'其他', author:b.author||'佚名', chapters:b.chapters||0, + desc:b.description||b.desc||'' + }; +} let ALL_ENTRIES = []; // 归一化后的全部条目(来自后端) let currentCat = 'all'; @@ -319,7 +432,7 @@ let searchTerm = ''; // ---------- 筛选交互 ---------- function setCat(cat){ currentCat = cat; - document.querySelectorAll('.catTab').forEach(el=>el.classList.toggle('active',el.dataset.cat===cat)); + document.querySelectorAll('.rule-tab').forEach(el=>el.classList.toggle('active',el.dataset.cat===cat)); renderGrid(); } function setQual(ql){ @@ -392,6 +505,9 @@ async function loadCodexData(){ ...(cre||[]).map(normCreature), ...(mon||[]).map(normMonster), ...(rac||[]).map(normRace), + ...TALENT_DATA.map(normTalent), + ...SKILL_BOOK_DATA.map(normSkillBook), + ...BOOK_DATA.map(normBook), ]; renderGrid(); }catch(err){ @@ -406,12 +522,14 @@ function renderGrid(){ let filtered = ALL_ENTRIES.filter(e=>{ if(currentCat!=='all' && e.cat!==currentCat) return false; if(q){ - const hay = [e.name, e.desc, (e.products||[]).join(' '), (e.drops||[]).join(' '), + const hay = [e.name, e.desc, e.effect||'', e.trigger||'', e.skillType||'', + e.bookType||'', e.author||'', e.cost||'', + (e.products||[]).join(' '), (e.drops||[]).join(' '), (e.traits||[]).join(' '), e.hab||'', e.layer||''].join(' '); if(!hay.toLowerCase().includes(q)) return false; } if(currentQual!=='all'){ - if(e.type!=='resource') return false; + if(e.type!=='resource' && e.type!=='talent' && e.type!=='skill_book') return false; if(e.qual!==currentQual) return false; } return true; @@ -470,6 +588,37 @@ function renderGrid(){ ${e.layer||''}
`; + } else if(e.type==='talent'){ + div.classList.add('q-'+e.qual, 'talent'); + div.innerHTML = ` + ${QUALITY_NAMES[e.qual]} +
${e.icon}
+
${e.name}
+
+ ${e.trigger||'被动'} +
+ `; + } else if(e.type==='skill_book'){ + div.classList.add('q-'+e.qual, 'skill_book'); + div.innerHTML = ` + ${QUALITY_NAMES[e.qual]} +
${e.icon}
+
${e.name}
+
+ ${e.skillType||'主动'} + Lv ${e.learnLevel||1} +
+ `; + } else if(e.type==='book'){ + div.classList.add('book'); + div.innerHTML = ` +
${e.icon}
+
${e.name}
+
+ ${e.bookType||'其他'} + ${(e.chapters||0)}章 +
+ `; } div.addEventListener('click',()=>showDetail(e)); @@ -502,11 +651,12 @@ function showDetail(e){ html += `
`; html += `
${e.icon}
`; html += `
${e.name}
`; - if(e.type==='resource'){ - const c = e.qual==='common'?'156,163,175':e.qual==='uncommon'?'34,197,94':e.qual==='rare'?'59,130,246':e.qual==='epic'?'168,85,247':'245,158,11'; - const t = e.qual==='common'?'#9ca3af':e.qual==='uncommon'?'#4ade80':e.qual==='rare'?'#60a5fa':e.qual==='epic'?'#c084fc':'#fbbf24'; + if(e.type==='resource' || e.type==='talent' || e.type==='skill_book'){ + const c = e.qual==='common'?'156,163,175':e.qual==='uncommon'?'34,197,94':e.qual==='rare'?'59,130,246':e.qual==='epic'?'168,85,247':e.qual==='legendary'?'245,158,11':'220,38,38'; + const t = e.qual==='common'?'#9ca3af':e.qual==='uncommon'?'#4ade80':e.qual==='rare'?'#60a5fa':e.qual==='epic'?'#c084fc':e.qual==='legendary'?'#fbbf24':'#f87171'; html += `${QUALITY_NAMES[e.qual]}`; - html += `${e.cat==='material'?'材料':e.cat==='processed'?'加工品':'特殊'}`; + const catLabel = e.type==='talent'?'🌟 天赋':e.type==='skill_book'?'📖 技能书':e.cat==='material'?'材料':e.cat==='processed'?'加工品':'特殊'; + html += `${catLabel}`; } else if(e.type==='creature'){ html += `🐾 生物${e.hab||''}${e.diet||''}${e.tame?'✅ 可驯养':'❌ 野性'}`; } else if(e.type==='monster'){ @@ -519,8 +669,8 @@ function showDetail(e){ } html += `
`; - // 品质等级条(资源) - if(e.type==='resource'){ + // 品质等级条(资源 / 天赋 / 技能书) + if(e.type==='resource' || e.type==='talent' || e.type==='skill_book'){ html += `
`; QUALITIES.forEach((q,i)=>{ html += `
`; }); html += `
`; @@ -581,6 +731,39 @@ function showDetail(e){ } } + // 天赋详情 + if(e.type==='talent'){ + html += `

📋 属性

`; + html += `
触发方式${e.trigger||'被动'}
`; + html += `
品质${QUALITY_NAMES[e.qual]||'—'}
`; + html += `
`; + if(e.effect){ + html += `

⚡ 效果

${e.effect}
`; + } + } + + // 技能书详情 + if(e.type==='skill_book'){ + html += `

📋 属性

`; + html += `
技能类型${e.skillType||'主动'}
`; + html += `
学习等级Lv ${e.learnLevel||1}
`; + html += `
消耗${e.cost||'—'}
`; + html += `
品质${QUALITY_NAMES[e.qual]||'—'}
`; + html += `
`; + if(e.effect){ + html += `

⚡ 技能效果

${e.effect}
`; + } + } + + // 书籍详情 + if(e.type==='book'){ + html += `

📋 属性

`; + html += `
类型${e.bookType||'其他'}
`; + html += `
作者${e.author||'佚名'}
`; + html += `
章节数${e.chapters||0} 章
`; + html += `
`; + } + html += '
'; box.innerHTML = html; overlay.classList.add('open'); diff --git a/rules.html b/rules.html index d71a8e1..1e66a66 100644 --- a/rules.html +++ b/rules.html @@ -8,13 +8,25 @@ @@ -707,11 +748,10 @@ body { background: #0a0e17; overflow: hidden; font-family: 'Microsoft YaHei', sa
星元 1年 · 春
- + +
+ + + + + + + + + +
+ +
@@ -791,6 +810,16 @@ body { background: #0a0e17; overflow: hidden; font-family: 'Microsoft YaHei', sa + + +
+
+
+ 世界 Lv.-- + -- + EXP -- / -- +
+
@@ -1101,7 +1130,9 @@ body { background: #0a0e17; overflow: hidden; font-family: 'Microsoft YaHei', sa + + + +
+
+ 👥 好友 + +
+
+ + + + +
+
+
+ + diff --git a/yuthon_playable_loop_plan.md b/yuthon_playable_loop_plan.md new file mode 100644 index 0000000..c514a01 --- /dev/null +++ b/yuthon_playable_loop_plan.md @@ -0,0 +1,73 @@ +# yuthon 微世界 · 最小可玩闭环规划 + +> 目标:把"已定义但没接活"的规则(属性/等级/货币)真正驱动玩法,让玩家登录后有 **目标 → 动作 → 可感知成长** 的循环。后台让运营能配置规则、管理玩家进度,而不是只能看静态文案。 +> 原则:先打通 P0 最小闭环(可玩性),再补 P1 后台运营能力,P2 扩展社交/战斗。 + +--- + +## 一、最小可玩闭环(单一场景讲清) + +玩家每天登录 → 收到 1 个每日任务(如"建造 1 个建筑 / 训练 1 只生物")→ 去沙盘或微观世界做动作 → 系统检测进度完成 → 发经验+金币 → 经验满则角色升级(升级按规则消耗金币、并按属性规则加属性点)→ 员工卡/图鉴上的"战力"数字实时涨了 → 看到成长,第二天还想来。 + +这条链路当前断了 4 处:经验不累加、升级不耗币、属性不生效、没有任务给目标。P0 就是补这 4 处。 + +--- + +## 二、P0 最小闭环(必须先做,可独立验收) + +### 1. 经验/等级/货币 接通(后端) +- 文件:`game_base` 或 `yt_world` 的 `hr.employee` 扩展(`game_employee.py`,已有 `game_exp/game_level/game_coins` 字段但无逻辑)。 +- 新增方法 `add_exp(amount)`: + - 累加 `game_exp`;按"角色等级"规则里的经验曲线判断升级; + - 升级时调用 `GameRule.convert()` 消耗金币(接通当前从未被调用的货币换算); + - 升级按"属性规则"给基础属性点(让属性第一次进入数值)。 +- 等级规则种子当前 `noupdate=1` 只读 → 改为可编辑(`rule_data.xml` 去掉 noupdate 或单独建可配表),后台能调曲线。 + +### 2. 战力计算(让属性有意义) +- 新增 `compute_power(employee)`:用基础属性(物攻/法攻/防御/物防/法防/速度/生命/法力等)加权求和得到"战力"。 +- 员工卡(`world.html` 员工卡区块)、图鉴、AI 向导问答都展示战力,让升级反馈可见。 + +### 3. 每日任务机制(给目标+奖励) +- 新增模型 `game.daily.task`(任务定义:动作类型、目标次数、奖励经验/金币)+ `game.player.task`(玩家每日进度)。 +- 接口:`API.dailyTask`(拉今日任务+进度)、`completeTask`(领奖)。 +- 在现有动作完成时挂钩:建筑动作(`world_api.py buildingAction`)、生物训练(`microUnitTrain`)→ 上报进度。 + +### 4. 前端成长反馈 +- `world.html` 员工卡加:战力数字、经验进度条、今日任务面板(API 对接)。 +- 升级/完成任务时弹"获得经验+XX / 金币+XX / 战力+XX"的轻提示。 + +**P0 验收标准**:登录 → 看到今日任务 → 做个建造/训练 → 任务完成领经验金币 → 经验满升级(扣金币、属性+战力涨)→ 员工卡数字变化可见。全程无需管理员手动改数据。 + +--- + +## 三、P1 后台功能优化(运营可配置) + +- **任务管理后台**:`game.daily.task` 的表单/列表视图(定义任务、奖励、周期),运营可增删改,不必改代码。 +- **经济/等级曲线后台**:把升级所需经验、货币消耗、属性增益做成可编辑规则(替代当前硬编码种子),运营调数值立刻生效。 +- **属性生效验证**:员工表单加 smart button 显示"当前战力/属性明细",后台能验证规则确实驱动了数值(解决当前"升级实际效果后台不可验证")。 +- **签到 / 简单抽奖**:P1 轻量正反馈,签到发币、抽奖给属性点/图鉴碎片,进一步拉日活。 +- **玩家进度管理**:运营可查看/修正单个玩家等级、货币、任务进度(客服/调试用)。 + +--- + +## 四、P2 扩展(闭环跑通后再做) + +- 战斗系统(用属性+战力做单位对战,当前 `yt.world.creature` 已有养成骨架)。 +- AI NPC 深度互动(已有 `game_npc` 模型与知识视图,接对话触发任务)。 +- 社交玩法(公会/排行榜,复用 `channel_type` 体系)。 + +--- + +## 五、执行建议(分期,每期可验收) + +1. **第 1 期(P0 后端)**:`add_exp` + 战力计算 + 每日任务模型 + 规则可编辑。升级 Odoo 落库。 +2. **第 2 期(P0 前端)**:员工卡战力/经验条 + 任务面板 + 升级提示。本地联调闭环跑通。 +3. **第 3 期(P1 后台)**:任务管理视图 + 经济曲线后台 + 属性验证 smart button + 签到/抽奖。 +4. 每期做完停下给你验收,不一次性闷头跑完。 + +--- + +## 附:当前已具备、无需重做的资产 +- 登录/会话、员工卡展示、沙盘建造、世界+AI向导聊天、图鉴、邮件/成就、建筑经济结算、生物训练升级(均已在 `world.html` / `yt_world` / `game_base` 落地)。 +- 规则文案(属性18条已分基础/高级、等级/货币/制度)已有,只需从"展示"改为"被逻辑读取"。 +- 后台图鉴/规则/聊天/公告/员工等视图已较完善,缺的是任务/经济/属性生效三类管理界面。