Files
2026-09-10 19:23:18 +08:00

882 lines
42 KiB
HTML
Raw Permalink 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 .icon img.ic-img{width:46px;height:46px;object-fit:contain;display:block;margin:2px auto 0;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.equip .tags,.card.talent .tags,.card.skill_book .tags,.card.scatter .tags{display:flex;gap:4px;flex-wrap:wrap;justify-content:center;margin-top:2px;}
.card.equip .tag,.card.talent .tag,.card.skill_book .tag,.card.scatter .tag{font-size:9px;padding:1px 6px;border-radius:4px;background:rgba(255,255,255,.06);color:#8899aa;}
.card.equip{border-color:rgba(120,200,255,.12);}
.card.scatter{border-color:rgba(245,158,11,.12);}
/* 野怪等级角标 */
.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;display:flex;align-items:center;justify-content:center;}
.md-icon img{width:52px;height:52px;object-fit:contain;filter:drop-shadow(0 2px 4px rgba(0,0,0,.3));}
.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;}
.md-action-btn{
width:100%;padding:12px 0;border:none;border-radius:10px;
background:linear-gradient(135deg,#8b5cf6,#4aafff);color:#fff;font-size:15px;font-weight:600;cursor:pointer;
box-shadow:0 8px 24px rgba(74,175,255,.25);transition:.2s;
}
.md-action-btn:hover{opacity:.92;transform:translateY(-1px);}
.md-action-btn:disabled{opacity:.55;cursor:not-allowed;transform:none;}
.md-action-error{margin-top:8px;font-size:13px;color:#ff8a8a;text-align:center;}
</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="equip" onclick="setCat('equip')">🛡️ 装备</span>
<span class="rule-tab" data-cat="scatter" onclick="setCat('scatter')">🎁 散落物</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','equip','scatter','special'];
const SLOT_NAMES = { weapon:'武器', armor:'护甲', accessory:'饰品', boots:'鞋子' };
// 当前玩家已拥有天赋(从后端加载){ code: { level, active } }
let MY_TALENTS = {};
// ===== 技能书预置数据(前端硬编码,后端就绪后迁移) =====
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){
const mine = MY_TALENTS[t.code];
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||'', maxLevel:t.max_level||1,
costType:t.cost_type||'point', costValue:t.cost_value||0,
owned:!!mine, level:(mine&&mine.level)||0, active:(mine&&mine.active)||false,
prerequisiteCode:t.prerequisite_code||null
};
}
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||''
};
}
function normEquip(e){
return {
type:'equip', id:e.code, cat:'equip', name:e.name, icon:e.icon||'⚔️',
qual:e.rarity||'common', slot:e.slot||'weapon', slotName:SLOT_NAMES[e.slot]||'装备',
stats:e.stats||{}, bonus:e.bonus||'', desc:e.description||e.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, talents, equips] = await Promise.all([
API.codexResources(), API.codexCreatures(), API.codexMonsters(), API.codexRaces(), API.codexTalents(), API.equipCatalog()
]);
// 同时尝试拉取当前角色天赋状态;未登录或无角色时忽略。
MY_TALENTS = {};
try{
const ct = await API.charTalents();
if(ct && ct.ok && Array.isArray(ct.talents)){
(ct.talents || []).forEach(t => {
if(t.owned) MY_TALENTS[t.code] = { level: t.level || 0, active: t.active || false };
});
}
}catch(e){ /* 未登录,忽略 */ }
ALL_ENTRIES = [
...(res||[]).map(normResource),
...(cre||[]).map(normCreature),
...(mon||[]).map(normMonster),
...(rac||[]).map(normRace),
...(talents||[]).map(normTalent),
...SKILL_BOOK_DATA.map(normSkillBook),
...BOOK_DATA.map(normBook),
...((equips && equips.ok && Array.isArray(equips.items) ? equips.items : equips) || []).map(normEquip),
];
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||'',
e.slotName||'', e.bonus||''].join(' ');
if(!hay.toLowerCase().includes(q)) return false;
}
if(currentQual!=='all'){
if(e.type!=='resource' && e.type!=='talent' && e.type!=='skill_book' && e.type!=='equip') 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">${iconHTML(e.icon)}</div>
<div class="name">${e.name}</div>
`;
} else if(e.type==='creature'){
div.classList.add('creature');
div.innerHTML = `
<div class="icon">${iconHTML(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">${iconHTML(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">${iconHTML(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');
const mine = e.owned ? `<span class="tag" style="background:rgba(139,92,246,.25);color:#d0bcff;">Lv ${e.level}</span>` : '';
div.innerHTML = `
<span class="qbadge ${e.qual}">${QUALITY_NAMES[e.qual]}</span>
<div class="icon">${iconHTML(e.icon)}</div>
<div class="name">${e.name}</div>
<div class="tags">
<span class="tag">${e.trigger||'被动'}</span>
${mine}
</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">${iconHTML(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==='equip'){
div.classList.add('q-'+e.qual, 'equip');
div.innerHTML = `
<span class="qbadge ${e.qual}">${QUALITY_NAMES[e.qual]}</span>
<div class="icon">${iconHTML(e.icon)}</div>
<div class="name">${e.name}</div>
<div class="tags">
<span class="tag">${e.slotName||'装备'}</span>
<span class="tag">${e.bonus||''}</span>
</div>
`;
} else if(e.type==='book'){
div.classList.add('book');
div.innerHTML = `
<div class="icon">${iconHTML(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(){}
// 图标渲染:图片路径→<img>,否则原样(emoji)
function iconHTML(ic){
if(!ic) return '';
if(/^(assets\/|\/)|\.(png|webp|jpe?g|gif|svg)(\?.*)?$/i.test(ic)){
return '<img class="ic-img" src="'+ic+'" alt="">';
}
return ic;
}
// =====================================================================
// 详 情 弹 窗
// =====================================================================
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">${iconHTML(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' || e.type==='equip'){
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.type==='equip'?'🛡️ 装备':e.cat==='material'?'材料':e.cat==='processed'?'加工品':e.cat==='scatter'?'🎁 散落物':'特殊';
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' || e.type==='equip'){
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 class="gi"><span class="gl">当前等级</span><span class="gv">${e.owned ? 'Lv ' + e.level : '未激活'}</span></div>`;
html += `<div class="gi"><span class="gl">最大等级</span><span class="gv">Lv ${e.maxLevel||1}</span></div>`;
if(e.costValue){
const costLabel = e.costType === 'gold' ? '金币' : '天赋点';
const costTitle = e.owned ? '升级消耗' : '激活消耗';
html += `<div class="gi"><span class="gl">${costTitle}</span><span class="gv">${e.costValue} ${costLabel}</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>`;
}
const canUpgrade = e.owned && e.level < e.maxLevel;
if(!e.owned){
html += `<div class="md-section"><button class="md-action-btn" id="talentAction" data-code="${e.id}" data-action="activate">激活天赋</button></div>`;
} else if(canUpgrade){
html += `<div class="md-section"><button class="md-action-btn" id="talentAction" data-code="${e.id}" data-action="upgrade">升级天赋</button></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==='equip'){
html += `<div class="md-section"><h3>📋 属性</h3><div class="md-grid">`;
html += `<div class="gi"><span class="gl">部位</span><span class="gv">${e.slotName||'—'}</span></div>`;
html += `<div class="gi"><span class="gl">品质</span><span class="gv">${QUALITY_NAMES[e.qual]||'—'}</span></div>`;
if(e.bonus){
html += `<div class="gi" style="grid-column:1/-1;"><span class="gl">加成</span><span class="gv">${e.bonus}</span></div>`;
}
html += `</div></div>`;
if(e.desc){
html += `<div class="md-section"><h3>📖 描述</h3><div class="md-desc">${e.desc}</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');
// 天赋激活/升级按钮事件绑定
const actionBtn = document.getElementById('talentAction');
if(actionBtn){
actionBtn.addEventListener('click', async function onTalentAction(){
const code = this.dataset.code;
const action = this.dataset.action;
this.disabled = true;
const oldText = this.textContent;
this.textContent = '处理中…';
let res;
if(action === 'activate') res = await API.activateTalent(code);
else res = await API.upgradeTalent(code);
if(res && res.ok){
await loadCodexData();
const entry = findEntry(code);
if(entry) showDetail(entry);
}else{
this.disabled = false;
this.textContent = oldText;
const msg = (res && res.error) ? res.error : '失败';
let errBox = document.getElementById('talentActionError');
if(!errBox){
errBox = document.createElement('div');
errBox.id = 'talentActionError';
errBox.className = 'md-action-error';
this.parentNode.appendChild(errBox);
}
errBox.textContent = '操作失败:' + msg;
}
});
}
}
// ===== 键盘关闭 =====
document.addEventListener('keydown',e=>{ if(e.key==='Escape') closeModal(); });
// ===== 初始化:从后端拉取图鉴 =====
document.addEventListener('DOMContentLoaded', loadCodexData);
</script>
<script src="assets/common.js"></script>
</main>
</div>
</body>
</html>