Files
game/codex.html
T
李鹏宇 73d764938d feat: 前端 UI 精修 + 后端视图调整 + 资源预置
前端:
- world.html 游戏内聊天组件重构:表情选择器改固定方格+自适应列+去滚动条;
  输入框清空占位文字+隐藏滚动条;聊天面板整体高度缩短约 1/3;
  顶部用户区改为纯文字并点开个人信息弹窗(头像/头像框/性别/说明/称号/注册)
- chat.html 聊天输入框压扁(46→34px)
- codex.html 新增神话品质、天赋/技能书/书籍分类与预置数据
- rules.html 规则文档更新

后端(Odoo):
- game_base 附件改为看板视图(图片/名称/备注/大小)
- yt_world 玩家菜单直接指向 hr.employee
- game_base 员工表单隐藏原生联系人/组织架构页签
- chat 模型/接口/视图与规则数据扩充

资源:生成 4x4 资源精灵表(树木/石材/芦苇/宝箱)
2026-08-14 14:27:15 +08:00

783 lines
37 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover">
<title>图鉴 · 宇森 Yusen</title>
<link rel="stylesheet" href="assets/fonts/yusen-fonts.css" />
<link rel="icon" type="image/png" sizes="48x48" href="assets/ui/logo_planet_48.png?v=20260723">
<link rel="stylesheet" href="assets/common.css" />
<script src="assets/game-api.js"></script>
<style>
/* ===== 图鉴页面专用样式(公共变量/背景/导航由 common.css 提供) ===== */
/* 动态星空背景 */
body::before{
content:'';position:fixed;inset:0;z-index:-1;
background-image:
radial-gradient(1px 1px at 10% 20%,rgba(255,255,255,.3),transparent),
radial-gradient(1px 1px at 30% 65%,rgba(255,255,255,.2),transparent),
radial-gradient(1.5px 1.5px at 55% 15%,rgba(255,255,255,.25),transparent),
radial-gradient(1px 1px at 70% 80%,rgba(255,255,255,.15),transparent),
radial-gradient(1px 1px at 85% 40%,rgba(255,255,255,.2),transparent),
radial-gradient(1.5px 1.5px at 15% 85%,rgba(255,255,255,.1),transparent),
radial-gradient(1px 1px at 45% 50%,rgba(255,255,255,.15),transparent),
radial-gradient(1px 1px at 92% 12%,rgba(255,255,255,.2),transparent);
}
/* ===== 筛选区 ===== */
.filters{
position:sticky;top:0;z-index:49;
background:#0c1120;
padding:8px 20px;border-bottom:1px solid rgba(255,255,255,.06);
box-shadow:0 1px 0 rgba(0,0,0,.4);
}
.filterRow{display:flex;align-items:center;gap:8px;flex-wrap:wrap;}
.filterRow+.filterRow{margin-top:6px;}
.filterLabel{font-size:13px;color:#7b8ea8;text-transform:uppercase;letter-spacing:1px;margin-right:4px;font-weight:700;}
/* 搜索框 */
.searchBox{
margin-left:auto;display:flex;align-items:center;gap:10px;
padding:6px 14px;border-radius:999px;font-size:13px;
background:rgba(255,255,255,.04);border:1px solid rgba(255,255,255,.1);
color:#9fb3cc;transition:.18s;min-width:180px;
}
.searchBox:focus-within{
background:rgba(139,92,246,.16);border-color:rgba(139,92,246,.5);
box-shadow:0 0 16px rgba(139,92,246,.25);
}
.searchBox input{
flex:1;min-width:0;background:transparent;border:none;outline:none;
color:#e6f0fb;font:inherit;font-size:15px;
}
.searchBox input::placeholder{color:#7b8ea8;}
.searchBox .icon{font-size:16px;opacity:.75;}
.searchBox .clear{
cursor:pointer;opacity:.5;font-size:13px;padding:0 6px;
transition:.15s;
}
.searchBox .clear:hover{opacity:1;color:#ffb4b4;}
/* ===== 分类页签(与规则页统一风格) ===== */
.rule-tabs{
display:flex;gap:10px;flex-wrap:wrap;justify-content:flex-start;
padding:6px 0 8px;
}
.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;}
.ql:hover{background:rgba(255,255,255,.04);}
.ql .dot{width:11px;height:11px;border-radius:50%;display:inline-block;}
.ql .lbl{font-size:13px;color:#9fb3cc;}
.ql.active .lbl{color:#fff;font-weight:600;}
.ql.active .dot{box-shadow:0 0 8px currentColor;}
/* ===== 数据统计条 ===== */
/* statsBar 已移除 */
/* ===== 卡片网格 ===== */
.grid{
padding:20px 24px 40px;
display:grid;
grid-template-columns:repeat(auto-fill,minmax(160px,1fr));
gap:14px;
}
@media (min-width:900px){.grid{grid-template-columns:repeat(auto-fill,minmax(180px,1fr));}}
@media (max-width:500px){.grid{padding:14px 12px;gap:10px;grid-template-columns:repeat(auto-fill,minmax(130px,1fr));}}
/* ===== 卡片 ===== */
.card{
position:relative;border-radius:14px;cursor:pointer;user-select:none;
background:rgba(255,255,255,.03);border:1px solid rgba(255,255,255,.08);
padding:16px 12px 12px;transition:.22s;overflow:hidden;
display:flex;flex-direction:column;align-items:center;gap:6px;text-align:center;
}
.card::before{content:'';position:absolute;top:0;left:0;right:0;height:3px;border-radius:14px 14px 0 0;opacity:0;transition:.22s;}
.card:hover{transform:translateY(-3px);box-shadow:0 8px 24px rgba(0,0,0,.4);}
.card:hover::before{opacity:1;}
.card .icon{font-size:36px;line-height:1.2;margin-bottom:2px;filter:drop-shadow(0 2px 4px rgba(0,0,0,.3));}
.card .name{font-size:14px;font-weight:700;color:#e6f0fb;line-height:1.3;}
.card .sub{font-size:11px;color:#7b8ea8;line-height:1.3;}
/* 品质色条 */
.card.q-common::before{background:linear-gradient(90deg,#9ca3af,#d1d5db);}
.card.q-uncommon::before{background:linear-gradient(90deg,#22c55e,#4ade80);}
.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{
position:absolute;top:8px;right:8px;
font-size:9px;font-weight:700;padding:1px 6px;border-radius:6px;
text-transform:uppercase;letter-spacing:.5px;
}
.qbadge.common{background:rgba(156,163,175,.2);color:#9ca3af;border:1px solid rgba(156,163,175,.3);}
.qbadge.uncommon{background:rgba(34,197,94,.15);color:#4ade80;border:1px solid rgba(34,197,94,.3);}
.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);}
.card.creature .icon{font-size:38px;}
.card.creature .tags{display:flex;gap:4px;flex-wrap:wrap;justify-content:center;margin-top:2px;}
.card.creature .tag{font-size:9px;padding:1px 6px;border-radius:4px;background:rgba(255,255,255,.06);color:#8899aa;}
.card.monster{border-color:rgba(168,85,247,.18);}
.card.monster .icon{font-size:38px;}
.card.monster.tier-elite{border-color:rgba(168,85,247,.35);box-shadow:0 0 14px rgba(168,85,247,.1);}
.card.monster.tier-boss{border-color:rgba(245,158,11,.4);box-shadow:0 0 18px rgba(245,158,11,.14);}
.card.monster .tags{display:flex;gap:4px;flex-wrap:wrap;justify-content:center;margin-top:2px;}
.card.monster .tag{font-size:9px;padding:1px 6px;border-radius:4px;background:rgba(255,255,255,.06);color:#8899aa;}
.card.race{border-color:rgba(100,200,255,.14);}
.card.race .icon{font-size:38px;}
.card.race .tags{display:flex;gap:4px;flex-wrap:wrap;justify-content:center;margin-top:2px;}
.card.race .tag{font-size:9px;padding:1px 6px;border-radius:4px;background:rgba(255,255,255,.06);color:#8899aa;}
/* 野怪等级角标 */
.card .tierbadge{position:absolute;top:8px;right:8px;font-size:9px;font-weight:700;padding:1px 6px;border-radius:6px;letter-spacing:.5px;}
.tierbadge.normal{background:rgba(156,163,175,.2);color:#9ca3af;border:1px solid rgba(156,163,175,.3);}
.tierbadge.elite{background:rgba(168,85,247,.18);color:#c084fc;border:1px solid rgba(168,85,247,.35);}
.tierbadge.boss{background:rgba(245,158,11,.18);color:#fbbf24;border:1px solid rgba(245,158,11,.35);}
/* ===== 空状态 ===== */
.empty{
grid-column:1/-1;text-align:center;padding:60px 20px;color:#556677;
font-size:14px;display:none;
}
.empty.show{display:block;}
/* ===== 详情弹窗 ===== */
.modalOverlay{
position:fixed;inset:0;z-index:1000;
background:rgba(4,7,15,.65);backdrop-filter:blur(6px);
display:flex;align-items:center;justify-content:center;
opacity:0;visibility:hidden;transition:.28s;padding:20px;
}
.modalOverlay.open{opacity:1;visibility:visible;}
.modalBox{
width:520px;max-width:100%;max-height:90vh;overflow-y:auto;
background:linear-gradient(180deg,rgba(17,23,42,.98),rgba(10,14,26,.98));
border:1px solid rgba(139,92,246,.35);border-radius:18px;
box-shadow:0 12px 48px rgba(0,0,0,.6);
padding:28px 28px 24px;transform:translateY(20px);transition:transform .3s ease;
}
.modalOverlay.open .modalBox{transform:translateY(0);}
.md-close{
position:absolute;top:12px;right:14px;width:28px;height:28px;border-radius:8px;
border:1px solid rgba(255,255,255,.12);background:rgba(255,255,255,.04);color:#9aa6c4;
font-size:13px;cursor:pointer;transition:.12s;
}
.md-close:hover{background:rgba(239,68,68,.2);color:#ffb4b4;}
.md-header{display:flex;align-items:center;gap:16px;margin-bottom:20px;position:relative;}
.md-icon{font-size:52px;line-height:1;}
.md-info{flex:1;}
.md-title{font-size:22px;font-weight:800;color:#eef2fb;}
.md-meta{display:flex;flex-wrap:wrap;gap:6px;margin-top:6px;}
.md-meta .mt{
font-size:11px;padding:2px 10px;border-radius:6px;
background:rgba(255,255,255,.05);border:1px solid rgba(255,255,255,.08);color:#9fb3cc;
}
.md-section{margin-top:18px;}
.md-section h3{font-size:13px;font-weight:700;color:#7b8ea8;margin-bottom:8px;text-transform:uppercase;letter-spacing:1px;}
.md-desc{font-size:14px;color:#cdd8e6;line-height:1.7;padding:10px 14px;border-radius:10px;background:rgba(255,255,255,.03);}
.md-grid{display:grid;grid-template-columns:1fr 1fr;gap:8px;}
.md-grid .gi{padding:8px 12px;border-radius:8px;background:rgba(255,255,255,.03);display:flex;justify-content:space-between;align-items:center;}
.md-grid .gi .gl{font-size:12px;color:#8899aa;}
.md-grid .gi .gv{font-size:13px;font-weight:600;color:#cdd8e6;}
/* 品质等级条 */
.qualityBar{display:flex;gap:6px;margin:12px 0;}
.qualityBar .qlv{
flex:1;height:4px;border-radius:2px;background:rgba(255,255,255,.08);
transition:.3s;
}
.qualityBar .qlv.on{background:linear-gradient(90deg,#9ca3af,#22c55e,#3b82f6,#a855f7,#f59e0b);}
.qualityBar .qlv.on+.qlv.on{background:linear-gradient(90deg,#22c55e,#3b82f6,#a855f7,#f59e0b);}
.qualityBar .qlv.on+.qlv.on+.qlv.on{background:linear-gradient(90deg,#3b82f6,#a855f7,#f59e0b);}
.qualityBar .qlv.on+.qlv.on+.qlv.on+.qlv.on{background:linear-gradient(90deg,#a855f7,#f59e0b);}
.qualityBar .qlv.on+.qlv.on+.qlv.on+.qlv.on+.qlv.on{background:#f59e0b;}
.md-recipes{margin-top:14px;display:flex;flex-direction:column;gap:8px;}
.md-recipe{
display:flex;align-items:center;gap:10px;padding:8px 12px;border-radius:10px;
background:rgba(255,255,255,.03);border:1px solid rgba(255,255,255,.06);
}
.md-recipe .rp-icon{font-size:20px;}
.md-recipe .rp-name{font-size:13px;color:#cdd8e6;font-weight:600;}
.md-recipe .rp-num{font-size:12px;color:#8899aa;margin-left:auto;}
.md-chips{margin-top:14px;display:flex;flex-wrap:wrap;gap:8px;}
.md-chip{padding:6px 12px;border-radius:8px;background:rgba(255,255,255,.04);border:1px solid rgba(255,255,255,.07);font-size:13px;color:#cdd8e6;}
</style>
</head>
<body>
<div class="app">
<header class="topbar">
<a href="index.html" class="logo"><span class="cn">宇森</span></a>
<nav class="nav">
<a href="announce.html">公告</a>
<a href="background.html">背景</a>
<a href="rules.html">规则</a>
<a href="codex.html">图鉴</a>
<a href="story.html">剧情</a>
<a href="forum.html" class="auth-only">论坛</a>
<a href="worlddata.html" class="auth-only">世界数据</a>
</nav>
<div class="spacer"></div>
<div class="user-box" id="userBox"></div>
</header>
<main class="page-content hide-scroll">
<!-- ===== 筛选区 ===== -->
<div class="filters" id="filters">
<div class="rule-tabs" id="catTabs">
<span class="rule-tab active" data-cat="all" onclick="setCat('all')">🏛️ 全部</span>
<span class="rule-tab" data-cat="material" onclick="setCat('material')">🪵 材料</span>
<span class="rule-tab" data-cat="processed" onclick="setCat('processed')">🏭 加工品</span>
<span class="rule-tab" data-cat="creature" onclick="setCat('creature')">🐾 生物</span>
<span class="rule-tab" data-cat="monster" onclick="setCat('monster')">👹 野怪</span>
<span class="rule-tab" data-cat="race" onclick="setCat('race')">🌍 种族</span>
<span class="rule-tab" data-cat="talent" onclick="setCat('talent')">🌟 天赋</span>
<span class="rule-tab" data-cat="skill_book" onclick="setCat('skill_book')">📖 技能书</span>
<span class="rule-tab" data-cat="book" onclick="setCat('book')">📚 书籍</span>
<span class="rule-tab" data-cat="special" onclick="setCat('special')">✨ 特殊</span>
</div>
<div class="filterRow">
<span class="ql active" data-ql="all" onclick="setQual('all')">
<span class="dot" style="background:linear-gradient(135deg,#9ca3af,#fbbf24);"></span>
<span class="lbl">全部</span>
</span>
<span class="ql" data-ql="common" onclick="setQual('common')">
<span class="dot" style="background:#9ca3af;"></span>
<span class="lbl">普通</span>
</span>
<span class="ql" data-ql="uncommon" onclick="setQual('uncommon')">
<span class="dot" style="background:#22c55e;"></span>
<span class="lbl">优秀</span>
</span>
<span class="ql" data-ql="rare" onclick="setQual('rare')">
<span class="dot" style="background:#3b82f6;"></span>
<span class="lbl">精良</span>
</span>
<span class="ql" data-ql="epic" onclick="setQual('epic')">
<span class="dot" style="background:#a855f7;"></span>
<span class="lbl">史诗</span>
</span>
<span class="ql" data-ql="legendary" onclick="setQual('legendary')">
<span class="dot" style="background:#f59e0b;"></span>
<span class="lbl">传说</span>
</span>
<span class="ql" data-ql="mythic" onclick="setQual('mythic')">
<span class="dot" style="background:linear-gradient(135deg,#dc2626,#f59e0b);"></span>
<span class="lbl">神话</span>
</span>
<div class="searchBox">
<span class="icon">🔍</span>
<input id="searchInput" type="text" placeholder="搜索…" />
<span class="clear" id="searchClear" onclick="clearSearch()" title="清空" style="display:none;">✕</span>
</div>
</div>
</div>
<!-- ===== 卡片网格 ===== -->
<div class="grid" id="grid"></div>
<!-- ===== 详情弹窗 ===== -->
<div class="modalOverlay" id="modalOverlay" onclick="if(event.target===this)closeModal()">
<div class="modalBox" id="modalBox" style="position:relative;"></div>
</div>
<script>
// =====================================================================
// 宇森图鉴 — 全部数据来自后端 Odoo (yt_world 模块),前端无硬编码默认数据
// =====================================================================
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','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';
let currentQual = 'all';
let searchTerm = '';
// ---------- 筛选交互 ----------
function setCat(cat){
currentCat = cat;
document.querySelectorAll('.rule-tab').forEach(el=>el.classList.toggle('active',el.dataset.cat===cat));
renderGrid();
}
function setQual(ql){
currentQual = ql;
document.querySelectorAll('.ql').forEach(el=>el.classList.toggle('active',el.dataset.ql===ql));
renderGrid();
}
function getQualIndex(q){ return QUALITIES.indexOf(q); }
// ---------- 搜索 ----------
function setSearch(v){
searchTerm = v;
const clr = document.getElementById('searchClear');
if(clr) clr.style.display = v ? 'inline' : 'none';
renderGrid();
}
function clearSearch(){
const inp = document.getElementById('searchInput');
if(inp) inp.value = '';
setSearch('');
}
document.addEventListener('DOMContentLoaded', ()=>{
const inp = document.getElementById('searchInput');
if(inp){
inp.addEventListener('input', e=> setSearch(e.target.value));
inp.addEventListener('keydown', e=>{ if(e.key==='Escape'){ clearSearch(); inp.blur(); } });
}
});
// ---------- 后端数据归一化 ----------
function normResource(r){
return {
type:'resource', id:r.code, cat:r.category, name:r.name, icon:r.icon||'📦',
qual:r.quality||'common', source:r.source||'', uses:r.uses||'',
recipe:Array.isArray(r.recipe)?r.recipe:[], desc:r.description||''
};
}
function normCreature(c){
return {
type:'creature', id:c.code, cat:'creature', name:c.name, icon:c.icon||'🐾',
hab:c.habitat||'', tame:!!c.tameable, diet:c.diet||'',
products:Array.isArray(c.products)?c.products:[], desc:c.description||''
};
}
function normMonster(m){
return {
type:'monster', id:m.code, cat:'monster', name:m.name, icon:m.icon||'👹',
tier:m.tier||'normal', levelMin:m.level_min||1, levelMax:m.level_max||1,
hab:m.habitat||'', drops:Array.isArray(m.drops)?m.drops:[], desc:m.description||''
};
}
function normRace(r){
return {
type:'race', id:r.code, cat:'race', name:r.name, icon:r.icon||'🧑',
layer:r.layer||'', traits:Array.isArray(r.traits)?r.traits:[], desc:r.description||''
};
}
function findEntry(code){ return ALL_ENTRIES.find(x=>x.id===code); }
// ---------- 加载(核心:从后端取数) ----------
async function loadCodexData(){
const grid = document.getElementById('grid');
grid.innerHTML = '<div class="empty show">正在从服务器加载图鉴数据…</div>';
try{
const [res, cre, mon, rac] = await Promise.all([
API.codexResources(), API.codexCreatures(), API.codexMonsters(), API.codexRaces()
]);
ALL_ENTRIES = [
...(res||[]).map(normResource),
...(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){
console.error('[codex] 加载失败:', err);
grid.innerHTML = '<div class="empty show">⚠️ 加载失败,请确认 Odoo 后端(8069)已启动且 yt_world 模块已更新。</div>';
}
}
// ---------- 渲染网格 ----------
function renderGrid(){
const q = searchTerm.trim().toLowerCase();
let filtered = ALL_ENTRIES.filter(e=>{
if(currentCat!=='all' && e.cat!==currentCat) return false;
if(q){
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' && e.type!=='talent' && e.type!=='skill_book') return false;
if(e.qual!==currentQual) return false;
}
return true;
});
filtered.sort((a,b)=>{
const ac = CAT_ORDER.indexOf(a.cat), bc = CAT_ORDER.indexOf(b.cat);
return (ac===-1?99:ac)-(bc===-1?99:bc) || (a.id||'').localeCompare(b.id||'');
});
const grid = document.getElementById('grid');
grid.innerHTML = '';
if(!filtered.length){
grid.innerHTML = '<div class="empty show">没有匹配的条目</div>';
} else {
filtered.forEach(e=>{
const div = document.createElement('div');
div.className = 'card';
if(e.type==='resource'){
div.classList.add('q-'+e.qual);
div.innerHTML = `
<span class="qbadge ${e.qual}">${QUALITY_NAMES[e.qual]}</span>
<div class="icon">${e.icon}</div>
<div class="name">${e.name}</div>
`;
} else if(e.type==='creature'){
div.classList.add('creature');
div.innerHTML = `
<div class="icon">${e.icon}</div>
<div class="name">${e.name}</div>
<div class="tags">
<span class="tag">${e.hab||''}</span>
<span class="tag">${e.diet||''}</span>
<span class="tag">${e.tame?'可驯养':'野性'}</span>
</div>
`;
} else if(e.type==='monster'){
div.classList.add('monster','tier-'+e.tier);
div.innerHTML = `
<span class="tierbadge ${e.tier}">${TIER_NAMES[e.tier]}</span>
<div class="icon">${e.icon}</div>
<div class="name">${e.name}</div>
<div class="tags">
<span class="tag">Lv ${e.levelMin}-${e.levelMax}</span>
<span class="tag">${e.hab||''}</span>
</div>
`;
} else if(e.type==='race'){
div.classList.add('race');
div.innerHTML = `
<div class="icon">${e.icon}</div>
<div class="name">${e.name}</div>
<div class="tags">
<span class="tag">${e.layer||''}</span>
</div>
`;
} else if(e.type==='talent'){
div.classList.add('q-'+e.qual, 'talent');
div.innerHTML = `
<span class="qbadge ${e.qual}">${QUALITY_NAMES[e.qual]}</span>
<div class="icon">${e.icon}</div>
<div class="name">${e.name}</div>
<div class="tags">
<span class="tag">${e.trigger||'被动'}</span>
</div>
`;
} else if(e.type==='skill_book'){
div.classList.add('q-'+e.qual, 'skill_book');
div.innerHTML = `
<span class="qbadge ${e.qual}">${QUALITY_NAMES[e.qual]}</span>
<div class="icon">${e.icon}</div>
<div class="name">${e.name}</div>
<div class="tags">
<span class="tag">${e.skillType||'主动'}</span>
<span class="tag">Lv ${e.learnLevel||1}</span>
</div>
`;
} else if(e.type==='book'){
div.classList.add('book');
div.innerHTML = `
<div class="icon">${e.icon}</div>
<div class="name">${e.name}</div>
<div class="tags">
<span class="tag">${e.bookType||'其他'}</span>
<span class="tag">${(e.chapters||0)}章</span>
</div>
`;
}
div.addEventListener('click',()=>showDetail(e));
grid.appendChild(div);
});
}
updateStats(filtered);
}
function updateStats(){}
// =====================================================================
// 详 情 弹 窗
// =====================================================================
function closeModal(){
document.getElementById('modalOverlay').classList.remove('open');
}
function showDetail(e){
const overlay = document.getElementById('modalOverlay');
const box = document.getElementById('modalBox');
const qi = e.qual ? getQualIndex(e.qual) : -1;
let html = '<div style="position:relative;">';
html += `<button class="md-close" onclick="closeModal()" style="position:absolute;top:-8px;right:-8px;">✕</button>`;
// 头部
html += `<div class="md-header">`;
html += `<div class="md-icon">${e.icon}</div>`;
html += `<div class="md-info"><div class="md-title">${e.name}</div><div class="md-meta">`;
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 += `<span class="mt" style="background:rgba(${c},.15);color:${t}">${QUALITY_NAMES[e.qual]}</span>`;
const catLabel = e.type==='talent'?'🌟 天赋':e.type==='skill_book'?'📖 技能书':e.cat==='material'?'材料':e.cat==='processed'?'加工品':'特殊';
html += `<span class="mt">${catLabel}</span>`;
} else if(e.type==='creature'){
html += `<span class="mt">🐾 生物</span><span class="mt">${e.hab||''}</span><span class="mt">${e.diet||''}</span><span class="mt">${e.tame?'✅ 可驯养':'❌ 野性'}</span>`;
} else if(e.type==='monster'){
const tc = e.tier==='boss'?'245,158,11':e.tier==='elite'?'168,85,247':'156,163,175';
const tt = e.tier==='boss'?'#fbbf24':e.tier==='elite'?'#c084fc':'#9ca3af';
html += `<span class="mt" style="background:rgba(${tc},.18);color:${tt}">${TIER_NAMES[e.tier]}</span>`;
html += `<span class="mt">Lv ${e.levelMin}-${e.levelMax}</span><span class="mt">${e.hab||''}</span>`;
} else if(e.type==='race'){
html += `<span class="mt">🌍 种族</span><span class="mt">${e.layer||''}</span>`;
}
html += `</div></div></div>`;
// 品质等级条(资源 / 天赋 / 技能书)
if(e.type==='resource' || e.type==='talent' || e.type==='skill_book'){
html += `<div class="qualityBar">`;
QUALITIES.forEach((q,i)=>{ html += `<div class="qlv ${i<=qi?'on':''}" title="${QUALITY_NAMES[q]}"></div>`; });
html += `</div>`;
}
// 描述
if(e.desc){
html += `<div class="md-section"><h3>📖 描述</h3><div class="md-desc">${e.desc}</div></div>`;
}
// 资源详情
if(e.type==='resource'){
html += `<div class="md-section"><h3>📋 属性</h3><div class="md-grid">`;
html += `<div class="gi"><span class="gl">来源</span><span class="gv">${e.source||'未知'}</span></div>`;
html += `<div class="gi"><span class="gl">用途</span><span class="gv">${e.uses||'—'}</span></div>`;
html += `</div></div>`;
if(e.recipe && e.recipe.length){
html += `<div class="md-section"><h3>🏭 加工配方</h3><div class="md-recipes">`;
e.recipe.forEach(r=>{
const src = findEntry(r.code);
html += `<div class="md-recipe"><span class="rp-icon">${src?src.icon:'❓'}</span><span class="rp-name">${src?src.name:r.code}</span><span class="rp-num">×${r.num}</span></div>`;
});
html += `</div></div>`;
}
}
// 生物详情
if(e.type==='creature'){
html += `<div class="md-section"><h3>📋 属性</h3><div class="md-grid">`;
html += `<div class="gi"><span class="gl">栖息地</span><span class="gv">${e.hab||'未知'}</span></div>`;
html += `<div class="gi"><span class="gl">食性</span><span class="gv">${e.diet||'—'}</span></div>`;
html += `<div class="gi"><span class="gl">可驯养</span><span class="gv">${e.tame?'✅ 是':'❌ 否'}</span></div>`;
html += `</div></div>`;
if(e.products && e.products.length){
html += `<div class="md-section"><h3>🥚 产物</h3><div class="md-chips">` + e.products.map(p=>`<span class="md-chip">${p}</span>`).join('') + `</div></div>`;
}
}
// 野怪详情
if(e.type==='monster'){
html += `<div class="md-section"><h3>📋 属性</h3><div class="md-grid">`;
html += `<div class="gi"><span class="gl">等级</span><span class="gv">${TIER_NAMES[e.tier]}</span></div>`;
html += `<div class="gi"><span class="gl">等级范围</span><span class="gv">Lv ${e.levelMin}-${e.levelMax}</span></div>`;
html += `<div class="gi"><span class="gl">栖息地</span><span class="gv">${e.hab||'未知'}</span></div>`;
html += `</div></div>`;
if(e.drops && e.drops.length){
html += `<div class="md-section"><h3>💎 掉落</h3><div class="md-chips">` + e.drops.map(d=>`<span class="md-chip">${d}</span>`).join('') + `</div></div>`;
}
}
// 种族详情
if(e.type==='race'){
html += `<div class="md-section"><h3>📋 属性</h3><div class="md-grid">`;
html += `<div class="gi"><span class="gl">所属层域</span><span class="gv">${e.layer||'未知'}</span></div>`;
html += `</div></div>`;
if(e.traits && e.traits.length){
html += `<div class="md-section"><h3>✨ 天赋特性</h3><div class="md-chips">` + e.traits.map(t=>`<span class="md-chip">${t}</span>`).join('') + `</div></div>`;
}
}
// 天赋详情
if(e.type==='talent'){
html += `<div class="md-section"><h3>📋 属性</h3><div class="md-grid">`;
html += `<div class="gi"><span class="gl">触发方式</span><span class="gv">${e.trigger||'被动'}</span></div>`;
html += `<div class="gi"><span class="gl">品质</span><span class="gv">${QUALITY_NAMES[e.qual]||'—'}</span></div>`;
html += `</div></div>`;
if(e.effect){
html += `<div class="md-section"><h3>⚡ 效果</h3><div class="md-desc" style="color:#f0e6d3;">${e.effect}</div></div>`;
}
}
// 技能书详情
if(e.type==='skill_book'){
html += `<div class="md-section"><h3>📋 属性</h3><div class="md-grid">`;
html += `<div class="gi"><span class="gl">技能类型</span><span class="gv">${e.skillType||'主动'}</span></div>`;
html += `<div class="gi"><span class="gl">学习等级</span><span class="gv">Lv ${e.learnLevel||1}</span></div>`;
html += `<div class="gi"><span class="gl">消耗</span><span class="gv">${e.cost||'—'}</span></div>`;
html += `<div class="gi"><span class="gl">品质</span><span class="gv">${QUALITY_NAMES[e.qual]||'—'}</span></div>`;
html += `</div></div>`;
if(e.effect){
html += `<div class="md-section"><h3>⚡ 技能效果</h3><div class="md-desc" style="color:#f0e6d3;">${e.effect}</div></div>`;
}
}
// 书籍详情
if(e.type==='book'){
html += `<div class="md-section"><h3>📋 属性</h3><div class="md-grid">`;
html += `<div class="gi"><span class="gl">类型</span><span class="gv">${e.bookType||'其他'}</span></div>`;
html += `<div class="gi"><span class="gl">作者</span><span class="gv">${e.author||'佚名'}</span></div>`;
html += `<div class="gi"><span class="gl">章节数</span><span class="gv">${e.chapters||0} 章</span></div>`;
html += `</div></div>`;
}
html += '</div>';
box.innerHTML = html;
overlay.classList.add('open');
}
// ===== 键盘关闭 =====
document.addEventListener('keydown',e=>{ if(e.key==='Escape') closeModal(); });
// ===== 初始化:从后端拉取图鉴 =====
document.addEventListener('DOMContentLoaded', loadCodexData);
</script>
<script src="assets/common.js"></script>
</main>
</div>
</body>
</html>