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 @@
+
+
+
- 世界聊天
+ 频道
game.chat.channel
list,form
+
+
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
+
+
+
+
+
+
+
+
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 = `
@@ -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 += ``;
- // 品质等级条(资源)
- 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 += `
`;
+ }
+ }
+
+ // 技能书详情
+ 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 += `
`;
+ }
+ }
+
+ // 书籍详情
+ 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 @@