diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..2457480
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,5 @@
+__pycache__/
+*.pyc
+*.bak
+*.bak2
+website/*.bak.*
diff --git a/addons/game_base/__init__.py b/addons/game_base/__init__.py
new file mode 100644
index 0000000..f7209b1
--- /dev/null
+++ b/addons/game_base/__init__.py
@@ -0,0 +1,2 @@
+from . import models
+from . import controllers
diff --git a/addons/game_base/__manifest__.py b/addons/game_base/__manifest__.py
new file mode 100644
index 0000000..6fd9f7c
--- /dev/null
+++ b/addons/game_base/__manifest__.py
@@ -0,0 +1,16 @@
+{
+ 'name': 'Yusen Game Base',
+ 'summary': '宇森游戏基础数据:图鉴 / 规则 / 论坛 / 公告',
+ 'version': '1.1',
+ 'category': 'Game',
+ 'sequence': 10,
+ 'depends': ['base'],
+ 'data': [
+ 'security/ir.model.access.csv',
+ 'data/game_base_data.xml',
+ 'views/game_base_views.xml',
+ ],
+ 'application': True,
+ 'license': 'LGPL-3',
+ 'description': '基于官网内容沉淀的游戏基础数据模型,供后台维护、前端经 /game/api/* 读取;并提供与 Odoo 用户打通的登录/注册接口。',
+}
diff --git a/addons/game_base/controllers/__init__.py b/addons/game_base/controllers/__init__.py
new file mode 100644
index 0000000..cc1b105
--- /dev/null
+++ b/addons/game_base/controllers/__init__.py
@@ -0,0 +1 @@
+from . import game_api
diff --git a/addons/game_base/controllers/game_api.py b/addons/game_base/controllers/game_api.py
new file mode 100644
index 0000000..6917d2b
--- /dev/null
+++ b/addons/game_base/controllers/game_api.py
@@ -0,0 +1,183 @@
+# -*- coding: utf-8 -*-
+import json
+from odoo import http
+from odoo.http import request, Response
+
+
+def _json(data):
+ """返回 UTF-8 JSON 响应。"""
+ return Response(
+ json.dumps(data, ensure_ascii=False),
+ content_type='application/json; charset=utf-8',
+ )
+
+
+def _post_body():
+ try:
+ raw = request.httprequest.get_data(as_text=True) or '{}'
+ return json.loads(raw)
+ except Exception:
+ return {}
+
+
+class GameApiController(http.Controller):
+
+ # ============ 公开只读数据接口(前端从 Odoo 取数) ============
+
+ @http.route('/game/api/codex', type='http', auth='public', methods=['GET'], csrf=False)
+ def codex(self, **kw):
+ """图鉴:按分类(cat)分组,每组内按子级(sub)分组。"""
+ model = request.env['game.codex.entry'].sudo()
+ cat_labels = dict(model._fields['cat'].selection)
+ recs = model.search_read([], ['id', 'cat', 'sub', 'name', 'tier', 'desc'])
+ by_cat = {}
+ for r in recs:
+ by_cat.setdefault(r['cat'], []).append(r)
+ groups = []
+ for cat in by_cat:
+ subs = {}
+ for r in by_cat[cat]:
+ sub = r['sub'] or '其他'
+ subs.setdefault(sub, []).append({
+ 'id': r['id'],
+ 'name': r['name'],
+ 'tier': r['tier'],
+ 'sub': r['sub'] or '',
+ 'desc': r['desc'] or '',
+ 'image': '/web/image/game.codex.entry/%s/image' % r['id'],
+ })
+ groups.append({
+ 'key': cat,
+ 'label': cat_labels.get(cat, cat),
+ 'subs': [{'name': s, 'items': subs[s]} for s in subs],
+ })
+ return _json({'groups': groups})
+
+ @http.route('/game/api/rules', type='http', auth='public', methods=['GET'], csrf=False)
+ def rules(self, **kw):
+ """规则 / 等级制度:按类别(type)分组。"""
+ model = request.env['game.rule'].sudo()
+ type_labels = dict(model._fields['type'].selection)
+ recs = model.search_read([], ['id', 'type', 'level', 'name', 'desc'])
+ by_type = {}
+ for r in recs:
+ by_type.setdefault(r['type'], []).append(r)
+ groups = []
+ for t in by_type:
+ groups.append({
+ 'key': t,
+ 'label': type_labels.get(t, t),
+ 'items': [{
+ 'id': r['id'],
+ 'level': r['level'],
+ 'name': r['name'],
+ 'desc': r['desc'] or '',
+ } for r in by_type[t]],
+ })
+ return _json({'groups': groups})
+
+ @http.route('/game/api/threads', type='http', auth='public', methods=['GET'], csrf=False)
+ def threads(self, **kw):
+ """论坛主题(含回复),按置顶+时间排序。"""
+ model = request.env['game.forum.thread'].sudo()
+ cat_labels = dict(model._fields['category'].selection)
+ recs = model.search_read(
+ [], ['id', 'name', 'category', 'author_name', 'pinned', 'create_date', 'body_html'])
+ out = []
+ for r in recs:
+ posts = request.env['game.forum.post'].sudo().search_read(
+ [('thread_id', '=', r['id'])],
+ ['id', 'author_name', 'body_html', 'create_date'])
+ out.append({
+ 'id': r['id'],
+ 'name': r['name'],
+ 'category': r['category'],
+ 'category_label': cat_labels.get(r['category'], r['category']),
+ 'author_name': r['author_name'] or '',
+ 'pinned': r['pinned'],
+ 'create_date': str(r['create_date']),
+ 'body_html': r['body_html'] or '',
+ 'replies': posts,
+ })
+ return _json(out)
+
+ @http.route('/game/api/announcements', type='http', auth='public', methods=['GET'], csrf=False)
+ def announcements(self, **kw):
+ """公告(仅生效的)。"""
+ model = request.env['game.announcement'].sudo()
+ pri_labels = dict(model._fields['priority'].selection)
+ recs = model.search_read(
+ [('active', '=', True)], ['id', 'name', 'date', 'priority', 'body_html'])
+ out = [{
+ 'id': r['id'],
+ 'name': r['name'],
+ 'date': str(r['date']),
+ 'priority': r['priority'],
+ 'priority_label': pri_labels.get(r['priority'], r['priority']),
+ 'body_html': r['body_html'] or '',
+ } for r in recs]
+ return _json(out)
+
+ # ============ 登录 / 注册 / 会话(与 Odoo res.users 打通) ============
+
+ @http.route('/game/api/login', type='http', auth='public',
+ methods=['POST', 'OPTIONS'], csrf=False)
+ def login(self, **kw):
+ data = _post_body()
+ db = data.get('db') or request.session.db or 'game'
+ login = (data.get('login') or '').strip()
+ password = data.get('password') or ''
+ if not login or not password:
+ return _json({'ok': False, 'error': '账号和密码必填'})
+ uid = request.session.authenticate(db, login, password)
+ if uid:
+ user = request.env['res.users'].sudo().browse(uid)
+ return _json({'ok': True, 'uid': uid, 'name': user.name, 'login': user.login})
+ return _json({'ok': False, 'error': '账号或密码错误'})
+
+ @http.route('/game/api/register', type='http', auth='public',
+ methods=['POST', 'OPTIONS'], csrf=False)
+ def register(self, **kw):
+ data = _post_body()
+ name = (data.get('name') or '').strip()
+ login = (data.get('login') or '').strip()
+ email = (data.get('email') or '').strip()
+ password = data.get('password') or ''
+ if not login or not password:
+ return _json({'ok': False, 'error': '账号和密码必填'})
+ if len(password) < 6:
+ return _json({'ok': False, 'error': '密码至少 6 位'})
+ User = request.env['res.users'].sudo()
+ if User.search([('login', '=', login)]):
+ return _json({'ok': False, 'error': '该账号已存在'})
+ try:
+ portal = request.env.ref('base.group_portal')
+ user = User.create({
+ 'name': name or login,
+ 'login': login,
+ 'email': email or False,
+ 'password': password,
+ 'groups_id': [(6, 0, [portal.id])],
+ })
+ db = request.session.db or 'game'
+ uid = request.session.authenticate(db, login, password)
+ if uid:
+ return _json({'ok': True, 'uid': uid, 'name': user.name, 'login': user.login})
+ return _json({'ok': True, 'uid': user.id, 'name': user.name, 'login': user.login})
+ except Exception as e:
+ return _json({'ok': False, 'error': '注册失败:%s' % str(e)})
+
+ @http.route('/game/api/me', type='http', auth='public', methods=['GET'], csrf=False)
+ def me(self, **kw):
+ uid = request.session.uid
+ if uid:
+ user = request.env['res.users'].sudo().browse(uid)
+ if user.exists():
+ return _json({'uid': uid, 'name': user.name, 'login': user.login})
+ return _json({'uid': None})
+
+ @http.route('/game/api/logout', type='http', auth='public',
+ methods=['POST', 'OPTIONS'], csrf=False)
+ def logout(self, **kw):
+ request.session.logout()
+ return _json({'ok': True})
diff --git a/addons/game_base/data/game_base_data.xml b/addons/game_base/data/game_base_data.xml
new file mode 100644
index 0000000..fbf7460
--- /dev/null
+++ b/addons/game_base/data/game_base_data.xml
@@ -0,0 +1,109 @@
+
+
+
+
+
+
+ world1蒙昧
+
+
+
+ world2聚落
+
+
+
+ world3王国
+
+
+
+ world4文明
+
+
+
+ world5飞升
+
+
+
+
+
+ faction1部族
+
+
+
+ faction2城邦
+
+
+
+ faction3公国
+
+
+
+ faction4王国
+
+
+
+ faction5帝国
+
+
+
+ faction6星盟
+
+
+
+
+ character1萌新
+ character2学徒
+ character3熟手
+ character4好手
+ character5精英
+ character6大师
+ character7宗匠
+ character8传奇
+ character9神话
+ character10半神
+ character11圣者
+ character12永恒
+
+
+
+ race龙裔龙裔传说
+
+
+
+ build生产农场精良
+
+
+
+ element火烈焰术稀有
+
+
+
+ persona勇敢无畏冲锋精良
+
+
+
+ talent战斗连击稀有
+
+
+
+ relic创世创世之核传说
+
+
+
+ equip头部守护木头部精良
+
+
+
+ event天象双月同辉史诗
+
+
+
+
+
+ 【公告】宇森官网第一阶段上线
+ 2026-07-09
+ important
+ 宇森官网第一阶段上线:介绍 / 规则 / 图鉴 / 论坛 / 公告 五块,单页切换呈现。欢迎园丁们入驻。
]]>
+
+
+
diff --git a/addons/game_base/models/__init__.py b/addons/game_base/models/__init__.py
new file mode 100644
index 0000000..1e5fe2a
--- /dev/null
+++ b/addons/game_base/models/__init__.py
@@ -0,0 +1,4 @@
+from . import codex
+from . import forum
+from . import announcement
+from . import rule
diff --git a/addons/game_base/models/announcement.py b/addons/game_base/models/announcement.py
new file mode 100644
index 0000000..582e8ac
--- /dev/null
+++ b/addons/game_base/models/announcement.py
@@ -0,0 +1,20 @@
+from odoo import models, fields
+
+
+class GameAnnouncement(models.Model):
+ _name = 'game.announcement'
+ _description = '公告'
+ _rec_name = 'name'
+ _order = 'date desc, id desc'
+
+ PRIORITY = [
+ ('normal', '普通'),
+ ('important', '重要'),
+ ('urgent', '紧急'),
+ ]
+
+ name = fields.Char(string='标题', required=True)
+ body_html = fields.Html(string='正文')
+ date = fields.Date(string='发布日期', default=fields.Date.today)
+ priority = fields.Selection(PRIORITY, string='优先级', default='normal', index=True)
+ active = fields.Boolean(string='生效', default=True)
diff --git a/addons/game_base/models/codex.py b/addons/game_base/models/codex.py
new file mode 100644
index 0000000..24365b4
--- /dev/null
+++ b/addons/game_base/models/codex.py
@@ -0,0 +1,33 @@
+from odoo import models, fields
+
+
+class GameCodexEntry(models.Model):
+ _name = 'game.codex.entry'
+ _description = '图鉴条目'
+ _rec_name = 'name'
+ _order = 'cat, sub, name'
+
+ CAT = [
+ ('race', '种族'),
+ ('build', '建造'),
+ ('element', '系别'),
+ ('persona', '性格'),
+ ('talent', '天赋'),
+ ('relic', '神器'),
+ ('equip', '装备'),
+ ('event', '传奇事件'),
+ ]
+ TIER = [
+ ('普通', '普通'),
+ ('精良', '精良'),
+ ('稀有', '稀有'),
+ ('史诗', '史诗'),
+ ('传说', '传说'),
+ ]
+
+ cat = fields.Selection(CAT, string='分类', required=True, index=True)
+ sub = fields.Char(string='子级', index=True)
+ name = fields.Char(string='名称', required=True, translate=True)
+ tier = fields.Selection(TIER, string='品质', default='普通', index=True)
+ desc = fields.Html(string='描述')
+ image = fields.Image(string='图片')
diff --git a/addons/game_base/models/forum.py b/addons/game_base/models/forum.py
new file mode 100644
index 0000000..1056f88
--- /dev/null
+++ b/addons/game_base/models/forum.py
@@ -0,0 +1,34 @@
+from odoo import models, fields
+
+
+class GameForumThread(models.Model):
+ _name = 'game.forum.thread'
+ _description = '论坛主题'
+ _rec_name = 'name'
+ _order = 'pinned desc, create_date desc'
+
+ CAT = [
+ ('discuss', '综合讨论'),
+ ('strategy', '攻略'),
+ ('lore', '背景考据'),
+ ('bug', '反馈'),
+ ('showcase', '作品秀'),
+ ]
+
+ name = fields.Char(string='标题', required=True)
+ category = fields.Selection(CAT, string='分类', default='discuss', index=True)
+ author_name = fields.Char(string='作者')
+ body_html = fields.Html(string='正文')
+ pinned = fields.Boolean(string='置顶', default=False)
+ post_ids = fields.One2many('game.forum.post', 'thread_id', string='回复')
+
+
+class GameForumPost(models.Model):
+ _name = 'game.forum.post'
+ _description = '论坛回复'
+ _rec_name = 'author_name'
+ _order = 'create_date asc'
+
+ thread_id = fields.Many2one('game.forum.thread', string='主题', ondelete='cascade', required=True)
+ author_name = fields.Char(string='作者')
+ body_html = fields.Html(string='内容')
diff --git a/addons/game_base/models/rule.py b/addons/game_base/models/rule.py
new file mode 100644
index 0000000..cdf9ca1
--- /dev/null
+++ b/addons/game_base/models/rule.py
@@ -0,0 +1,19 @@
+from odoo import models, fields
+
+
+class GameRule(models.Model):
+ _name = 'game.rule'
+ _description = '规则 / 等级制度'
+ _rec_name = 'name'
+ _order = 'type, level'
+
+ TYPE = [
+ ('world', '世界等级'),
+ ('faction', '势力等级'),
+ ('character', '人物角色等级'),
+ ]
+
+ type = fields.Selection(TYPE, string='类别', required=True, index=True)
+ level = fields.Integer(string='等级', index=True)
+ name = fields.Char(string='名称', required=True)
+ desc = fields.Html(string='说明')
diff --git a/addons/game_base/security/ir.model.access.csv b/addons/game_base/security/ir.model.access.csv
new file mode 100644
index 0000000..6dca7cf
--- /dev/null
+++ b/addons/game_base/security/ir.model.access.csv
@@ -0,0 +1,11 @@
+id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
+access_game_codex_entry,game.codex.entry public,model_game_codex_entry,base.group_public,1,0,0,0
+access_game_codex_entry_user,game.codex.entry user,model_game_codex_entry,base.group_user,1,1,1,1
+access_game_forum_thread,game.forum.thread public,model_game_forum_thread,base.group_public,1,0,0,0
+access_game_forum_thread_user,game.forum.thread user,model_game_forum_thread,base.group_user,1,1,1,1
+access_game_forum_post,game.forum.post public,model_game_forum_post,base.group_public,1,0,0,0
+access_game_forum_post_user,game.forum.post user,model_game_forum_post,base.group_user,1,1,1,1
+access_game_announcement,game.announcement public,model_game_announcement,base.group_public,1,0,0,0
+access_game_announcement_user,game.announcement user,model_game_announcement,base.group_user,1,1,1,1
+access_game_rule,game.rule public,model_game_rule,base.group_public,1,0,0,0
+access_game_rule_user,game.rule user,model_game_rule,base.group_user,1,1,1,1
diff --git a/addons/game_base/views/game_base_views.xml b/addons/game_base/views/game_base_views.xml
new file mode 100644
index 0000000..90a90fd
--- /dev/null
+++ b/addons/game_base/views/game_base_views.xml
@@ -0,0 +1,143 @@
+
+
+
+
+
+
+
+
+ 图鉴
+ game.codex.entry
+ list,form
+
+
+
+
+ game.codex.entry.list
+ game.codex.entry
+
+
+
+
+
+
+
+ game.codex.entry.form
+ game.codex.entry
+
+
+
+
+
+
+
+ 规则
+ game.rule
+ list,form
+
+
+
+ game.rule.list
+ game.rule
+
+
+
+
+
+ game.rule.form
+ game.rule
+
+
+
+
+
+
+
+
+ 主题
+ game.forum.thread
+ list,form
+
+
+
+ 回复
+ game.forum.post
+ list,form
+
+
+
+
+ game.forum.thread.list
+ game.forum.thread
+
+
+
+
+
+
+
+ game.forum.thread.form
+ game.forum.thread
+
+
+
+
+
+ game.forum.post.list
+ game.forum.post
+
+
+
+
+
+ game.forum.post.form
+ game.forum.post
+
+
+
+
+
+
+
+ 公告
+ game.announcement
+ list,form
+
+
+
+ game.announcement.list
+ game.announcement
+
+
+
+
+
+ game.announcement.form
+ game.announcement
+
+
+
+
+
+
diff --git a/website/assets/game-api.js b/website/assets/game-api.js
new file mode 100644
index 0000000..d114327
--- /dev/null
+++ b/website/assets/game-api.js
@@ -0,0 +1,42 @@
+// 宇森游戏 · 前端与 Odoo 通信层
+// 所有接口同源(yt.pengyuthon.cn),由 nginx 代理 /game/* 到 Odoo(game 库)。
+// 关键点:Odoo 通过 URL 上的 ?db=game 选定数据库(与 game.conf 的 /web/login?db=game 一致),
+// 否则请求到达时未选中库,控制器路由找不到 → 404。因此所有调用都带 ?db=game。
+// 登录态通过 Odoo session cookie 保持,fetch 带 credentials:'same-origin'。
+
+const API = {
+ async me() {
+ return (await fetch('/game/api/me?db=game', { credentials: 'same-origin' })).json();
+ },
+ async login(login, password) {
+ return (await fetch('/game/api/login?db=game', {
+ method: 'POST', credentials: 'same-origin',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ login, password }),
+ })).json();
+ },
+ async register(payload) {
+ return (await fetch('/game/api/register?db=game', {
+ method: 'POST', credentials: 'same-origin',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify(payload),
+ })).json();
+ },
+ async logout() {
+ return (await fetch('/game/api/logout?db=game', {
+ method: 'POST', credentials: 'same-origin',
+ })).json();
+ },
+ async codex() {
+ return (await fetch('/game/api/codex?db=game', { credentials: 'same-origin' })).json();
+ },
+ async rules() {
+ return (await fetch('/game/api/rules?db=game', { credentials: 'same-origin' })).json();
+ },
+ async threads() {
+ return (await fetch('/game/api/threads?db=game', { credentials: 'same-origin' })).json();
+ },
+ async announcements() {
+ return (await fetch('/game/api/announcements?db=game', { credentials: 'same-origin' })).json();
+ },
+};
diff --git a/website/assets/site-app.js b/website/assets/site-app.js
new file mode 100644
index 0000000..2fc071b
--- /dev/null
+++ b/website/assets/site-app.js
@@ -0,0 +1,69 @@
+// 宇森官网 · 公开页数据打通(图鉴/规则/论坛/公告 从 Odoo 取数)
+// 依赖 assets/game-api.js
+
+// ---------- 规则 ----------
+async function loadRules(){
+ const el = document.querySelector('#rules .rules-scroll');
+ if (!el) return;
+ try {
+ const d = await API.rules();
+ const groups = (d && d.groups) || [];
+ if (!groups.length){ el.innerHTML = '暂无规则数据
'; return; }
+ el.innerHTML = groups.map(g => {
+ const rows = (g.items || []).map(it => {
+ const lvl = (it.level != null) ? ('Lv.' + it.level) : (g.key === 'world' ? '阶段' : '');
+ return `${lvl}${it.name}${it.desc || ''}
`;
+ }).join('');
+ return ``;
+ }).join('');
+ } catch (e) { /* 保留静态兜底内容 */ }
+}
+
+// ---------- 论坛 ----------
+async function loadForum(){
+ const threadsEl = document.getElementById('threads');
+ const catsEl = document.querySelector('#forum .f-cats');
+ if (!threadsEl) return;
+ try {
+ const list = await API.threads();
+ if (!list || !list.length){ threadsEl.innerHTML = '暂无主题
'; return; }
+ // 分类筛选
+ const cats = [];
+ list.forEach(t => { if (t.category && cats.indexOf(t.category) < 0) cats.push(t.category); });
+ if (catsEl){
+ catsEl.innerHTML = `全部
` +
+ cats.map(c => `${c}
`).join('');
+ catsEl.querySelectorAll('.ch').forEach(b => b.addEventListener('click', () => {
+ catsEl.querySelectorAll('.ch').forEach(x => x.classList.remove('active'));
+ b.classList.add('active');
+ const c = b.dataset.fcat;
+ threadsEl.querySelectorAll('.th').forEach(t => { t.style.display = (c === 'all' || t.dataset.fcat === c) ? '' : 'none'; });
+ }));
+ }
+ threadsEl.innerHTML = list.map(t =>
+ `` +
+ (t.pinned ? `置顶` : '') +
+ `${t.name}` +
+ `${(t.replies || []).length} 回复 · ${t.author_name || '匿名'}
`
+ ).join('');
+ } catch (e) { /* 保留静态兜底内容 */ }
+}
+
+// ---------- 公告 ----------
+async function loadAnnounce(){
+ const el = document.querySelector('#announce .notes');
+ if (!el) return;
+ try {
+ const list = await API.announcements();
+ if (!list || !list.length){ el.innerHTML = ''; return; }
+ el.innerHTML = list.map(n =>
+ `${n.date}
${n.body_html || ''}
`
+ ).join('');
+ } catch (e) { /* 保留静态兜底内容 */ }
+}
+
+document.addEventListener('DOMContentLoaded', () => {
+ loadRules();
+ loadForum();
+ loadAnnounce();
+});
diff --git a/website/assets/world-app.js b/website/assets/world-app.js
new file mode 100644
index 0000000..c227246
--- /dev/null
+++ b/website/assets/world-app.js
@@ -0,0 +1,157 @@
+// 宇森 · 世界内面板逻辑(图鉴 / 规则 / 论坛 / 公告)
+// 依赖 assets/game-api.js(提供 API)
+
+// ---------- 登录态守卫 ----------
+(async () => {
+ try {
+ const me = await API.me();
+ if (!me.uid) { location.href = 'login.html?next=world.html'; return; }
+ const u = document.getElementById('userName');
+ if (u) u.textContent = me.name || me.login;
+ } catch (e) {
+ location.href = 'login.html';
+ }
+})();
+
+// ---------- 退出 ----------
+const logoutBtn = document.getElementById('logoutBtn');
+if (logoutBtn) logoutBtn.addEventListener('click', async () => {
+ await API.logout();
+ location.href = 'index.html';
+});
+
+// ---------- 面板控制 ----------
+const panel = document.getElementById('dataPanel');
+const dpTitle = document.getElementById('dpTitle');
+const dpBody = document.getElementById('dpBody');
+const dpClose = document.getElementById('dpClose');
+if (dpClose) dpClose.addEventListener('click', () => panel.classList.remove('open'));
+
+async function openPanel(kind) {
+ if (!panel) return;
+ panel.classList.add('open');
+ dpBody.innerHTML = '加载中…
';
+ try {
+ if (kind === 'codex') { dpTitle.textContent = '图鉴'; renderCodex(await API.codex()); }
+ else if (kind === 'rules') { dpTitle.textContent = '规则'; renderRules(await API.rules()); }
+ else if (kind === 'forum') { dpTitle.textContent = '论坛'; renderForum(await API.threads()); }
+ else if (kind === 'announce') { dpTitle.textContent = '公告'; renderAnnounce(await API.announcements()); }
+ } catch (e) {
+ dpBody.innerHTML = '加载失败,请稍后重试
';
+ }
+}
+
+document.querySelectorAll('.world-nav button[data-panel]').forEach(b => {
+ b.addEventListener('click', () => openPanel(b.dataset.panel));
+});
+
+// ---------- 程序化矢量缩略图(零 AI 生图,不消耗积分) ----------
+function hashStr(s){let h=2166136261;for(let i=0;i>>0;}
+function rng(seed){let s=seed>>>0;return function(){s=(Math.imul(s,1664525)+1013904223)>>>0;return s/4294967296;};}
+const TIERCOL={'普通':'#9aa6c4','精良':'#5fd38a','稀有':'#4cc9f0','史诗':'#c77dff','传说':'#e7b85c'};
+function starPts(cx,cy,spikes,outer,inner){let pts=[],rot=-Math.PI/2;for(let i=0;i`;if(v===0){x+=``;}else if(v===1){x+=``;}else{x+=``;}x+=``;return x;}
+function shBuild(r,s){let x=``;x+=``;for(let i=0;i<3;i++)x+=``;return x;}
+function shElement(r,s){const sides=3+Math.floor(r()*6);let x=``;x+=``;return x;}
+function shPersona(r,s){let x=``;x+=``;x+=``;return x;}
+function shTalent(r,s){const sp=4+Math.floor(r()*4);let x=``;x+=``;return x;}
+function shRelic(r,s){let x=``;x+=``;return x;}
+function shEquip(sub,s){switch(sub){case 'head':return ``;case 'hand':{let x=``;for(let i=0;i<3;i++)x+=``;return x;}case 'chest':return ``;case 'pants':return ``;case 'shoes':return ``;case 'ring':return ``;case 'neck':return ``;case 'treasure':return ``;default:return ``;}}
+function shEvent(r,s){let x=``;x+=``;return x;}
+function shapeFor(cat,sub,r,s){switch(cat){case 'race':return shRace(r,s);case 'build':return shBuild(r,s);case 'element':return shElement(r,s);case 'persona':return shPersona(r,s);case 'talent':return shTalent(r,s);case 'relic':return shRelic(r,s);case 'equip':return shEquip(sub,s);case 'event':return shEvent(r,s);default:return ``;}}
+function thumbSVG(it){
+ const seed=hashStr(it.cat+it.sub+it.name);const r=rng(seed);const h=Math.floor(r()*360);const h2=(h+40)%360;
+ const tcol=TIERCOL[it.tier]||'#9aa6c4';const shape=shapeFor(it.cat,it.sub,r,seed);
+ return ``;
+}
+
+// ---------- 渲染:图鉴(分类 → 子级 → 条目) ----------
+function renderCodex(data){
+ const groups = (data && data.groups) || [];
+ if (!groups.length){ dpBody.innerHTML = '暂无图鉴数据
'; return; }
+ let html = '';
+ groups.forEach(g => {
+ html += `${g.label}
`;
+ (g.subs || []).forEach(sub => {
+ if (sub.name && sub.name !== '其他') html += `
${sub.name}
`;
+ html += '
';
+ sub.items.forEach(it => {
+ const imgTag = it.image
+ ? `

`
+ : '';
+ const fallback = `
${thumbSVG(it)}
`;
+ html += `
`+
+ `
${imgTag || fallback}
`+
+ `
${it.name}
`+
+ `
${it.sub||''}${it.tier||''}
`+
+ `
${it.desc||''}
`;
+ });
+ html += '
';
+ });
+ html += '
';
+ });
+ dpBody.innerHTML = html;
+ // 处理有 image 但加载失败的:替换为程序化 SVG
+ dpBody.querySelectorAll('img.dp-thumb-img').forEach(img => {
+ img.addEventListener('error', () => {
+ const svg = thumbSVG({cat:img.dataset.cat, sub:img.dataset.sub, name:img.dataset.name, tier:img.dataset.tier});
+ const div = document.createElement('div'); div.className='dp-thumb-svg'; div.innerHTML = svg;
+ img.replaceWith(div);
+ });
+ });
+}
+
+// ---------- 渲染:规则(按类别分组) ----------
+function renderRules(data){
+ const groups = (data && data.groups) || [];
+ if (!groups.length){ dpBody.innerHTML = '暂无规则数据
'; return; }
+ let html = '';
+ groups.forEach(g => {
+ html += `${g.label}
`;
+ (g.items || []).forEach(it => {
+ const lvl = (it.level!=null) ? ('Lv.'+it.level) : '';
+ html += `
${lvl}${it.name}${it.desc||''}
`;
+ });
+ html += '
';
+ });
+ dpBody.innerHTML = html;
+}
+
+// ---------- 渲染:论坛 ----------
+function renderForum(threads){
+ threads = threads || [];
+ if (!threads.length){ dpBody.innerHTML = '暂无主题
'; return; }
+ let html = '';
+ threads.forEach(t => {
+ const replies = (t.replies || []).map(p =>
+ `
${p.author_name||'匿名'}${p.create_date}
${p.body_html||''}
`
+ ).join('');
+ html += `
`+
+ `
${t.category_label||''}`+
+ (t.pinned?`置顶`:'')+`${t.name}`+
+ `${t.author_name||''}
`+
+ `
${t.body_html||''}
`+
+ `
${replies}
`;
+ });
+ html += '
';
+ dpBody.innerHTML = html;
+}
+
+// ---------- 渲染:公告 ----------
+function renderAnnounce(list){
+ list = list || [];
+ if (!list.length){ dpBody.innerHTML = '暂无公告
'; return; }
+ let html = '';
+ list.forEach(n => {
+ const pri = n.priority==='urgent' ? 'urgent' : (n.priority==='important' ? 'imp' : '');
+ html += `
${n.date}
`+
+ `
${n.body_html||''}
`;
+ });
+ html += '
';
+ dpBody.innerHTML = html;
+}
diff --git a/website/assets/yusen-trailer.mp4 b/website/assets/yusen-trailer.mp4
new file mode 100644
index 0000000..ab0cd98
Binary files /dev/null and b/website/assets/yusen-trailer.mp4 differ
diff --git a/website/codex-data.js b/website/codex-data.js
new file mode 100644
index 0000000..96ceddf
--- /dev/null
+++ b/website/codex-data.js
@@ -0,0 +1,428 @@
+window.CODEX={
+ "categories": [
+ {
+ "id": "race",
+ "name": "种族",
+ "icon": "🧬"
+ },
+ {
+ "id": "build",
+ "name": "建造",
+ "icon": "🏛️"
+ },
+ {
+ "id": "element",
+ "name": "系别",
+ "icon": "🔮"
+ },
+ {
+ "id": "persona",
+ "name": "性格",
+ "icon": "🎭"
+ },
+ {
+ "id": "talent",
+ "name": "天赋",
+ "icon": "✨"
+ },
+ {
+ "id": "relic",
+ "name": "神器",
+ "icon": "🗿"
+ },
+ {
+ "id": "equip",
+ "name": "装备",
+ "icon": "⚔️"
+ },
+ {
+ "id": "event",
+ "name": "传奇事件",
+ "icon": "🌟"
+ }
+ ],
+ "subs": {
+ "race": [
+ "龙裔",
+ "兽人",
+ "精灵",
+ "矮人",
+ "机械",
+ "虚灵",
+ "海族",
+ "天使",
+ "虫族",
+ "巨人",
+ "猫兽",
+ "树人"
+ ],
+ "build": [
+ "生产",
+ "军事",
+ "宗教",
+ "科技",
+ "贸易",
+ "奇观",
+ "居住",
+ "防御"
+ ],
+ "element": [
+ "火",
+ "水",
+ "风",
+ "土",
+ "光",
+ "暗",
+ "自然",
+ "金",
+ "雷",
+ "冰",
+ "毒",
+ "时"
+ ],
+ "persona": [
+ "勇敢",
+ "谨慎",
+ "狂热",
+ "冷静",
+ "狡黠",
+ "仁慈",
+ "傲慢",
+ "忧郁",
+ "忠义",
+ "贪婪",
+ "闲散",
+ "偏执"
+ ],
+ "talent": [
+ "战斗",
+ "统率",
+ "建造",
+ "贸易",
+ "研究",
+ "信仰",
+ "潜行",
+ "治疗",
+ "采集",
+ "外交",
+ "锻造",
+ "航海"
+ ],
+ "relic": [
+ "创世",
+ "星辰",
+ "海洋",
+ "熔火",
+ "虚空",
+ "自然",
+ "亡冥",
+ "圣光",
+ "秩序",
+ "混沌"
+ ],
+ "equip": [
+ "头部",
+ "手部",
+ "胸部",
+ "裤装",
+ "鞋履",
+ "戒指",
+ "项链",
+ "宝物"
+ ],
+ "event": [
+ "天象",
+ "灾变",
+ "神迹",
+ "战争",
+ "发现",
+ "背叛",
+ "繁荣",
+ "终焉",
+ "秘辛",
+ "奇观"
+ ]
+ },
+ "items": [
+ {
+ "cat": "race",
+ "sub": "龙裔",
+ "name": "赤焰龙裔",
+ "tier": "精良",
+ "desc": "龙裔主支系·赤焰变体。高战斗·低社交,战斗力+30%,交易-15%。文明创建时选定,决定 NPC 性格分布与初始偏向。"
+ },
+ {
+ "cat": "race",
+ "sub": "兽人",
+ "name": "赤焰兽人",
+ "tier": "稀有",
+ "desc": "兽人主支系·赤焰变体。高体力·低智慧,人口增长+20%,研究-20%。文明创建时选定,决定 NPC 性格分布与初始偏向。"
+ },
+ {
+ "cat": "race",
+ "sub": "精灵",
+ "name": "赤焰精灵",
+ "tier": "史诗",
+ "desc": "精灵主支系·赤焰变体。高智慧·低生育,研究+20%,人口-15%。文明创建时选定,决定 NPC 性格分布与初始偏向。"
+ },
+ {
+ "cat": "race",
+ "sub": "矮人",
+ "name": "赤焰矮人",
+ "tier": "传说",
+ "desc": "矮人主支系·赤焰变体。高生产·低移动,建造+25%,移动-2格。文明创建时选定,决定 NPC 性格分布与初始偏向。"
+ },
+ {
+ "cat": "race",
+ "sub": "机械",
+ "name": "赤焰机械",
+ "tier": "普通",
+ "desc": "机械主支系·赤焰变体。高秩序·低情感,维护耗能,失误率-25%。文明创建时选定,决定 NPC 性格分布与初始偏向。"
+ },
+ {
+ "cat": "build",
+ "sub": "生产",
+ "name": "农场",
+ "tier": "普通",
+ "desc": "生产类建筑·农场。提供对应产出与效能,受世界等级与势力等级加成。"
+ },
+ {
+ "cat": "build",
+ "sub": "军事",
+ "name": "兵营",
+ "tier": "精良",
+ "desc": "军事类建筑·兵营。提供对应产出与效能,受世界等级与势力等级加成。"
+ },
+ {
+ "cat": "build",
+ "sub": "宗教",
+ "name": "神殿",
+ "tier": "稀有",
+ "desc": "宗教类建筑·神殿。提供对应产出与效能,受世界等级与势力等级加成。"
+ },
+ {
+ "cat": "build",
+ "sub": "科技",
+ "name": "学院",
+ "tier": "史诗",
+ "desc": "科技类建筑·学院。提供对应产出与效能,受世界等级与势力等级加成。"
+ },
+ {
+ "cat": "build",
+ "sub": "贸易",
+ "name": "集市",
+ "tier": "传说",
+ "desc": "贸易类建筑·集市。提供对应产出与效能,受世界等级与势力等级加成。"
+ },
+ {
+ "cat": "element",
+ "sub": "火",
+ "name": "烈焰术",
+ "tier": "普通",
+ "desc": "火系别·应用「烈焰术」。影响对应属性与技能树分支,可被天赋与神器强化。"
+ },
+ {
+ "cat": "element",
+ "sub": "水",
+ "name": "寒冰术",
+ "tier": "精良",
+ "desc": "水系别·应用「寒冰术」。影响对应属性与技能树分支,可被天赋与神器强化。"
+ },
+ {
+ "cat": "element",
+ "sub": "风",
+ "name": "疾风步",
+ "tier": "稀有",
+ "desc": "风系别·应用「疾风步」。影响对应属性与技能树分支,可被天赋与神器强化。"
+ },
+ {
+ "cat": "element",
+ "sub": "土",
+ "name": "磐石甲",
+ "tier": "史诗",
+ "desc": "土系别·应用「磐石甲」。影响对应属性与技能树分支,可被天赋与神器强化。"
+ },
+ {
+ "cat": "element",
+ "sub": "光",
+ "name": "圣愈",
+ "tier": "传说",
+ "desc": "光系别·应用「圣愈」。影响对应属性与技能树分支,可被天赋与神器强化。"
+ },
+ {
+ "cat": "persona",
+ "sub": "勇敢",
+ "name": "无畏冲锋",
+ "tier": "普通",
+ "desc": "性格「勇敢」表现·无畏冲锋。影响 NPC 决策权重与事件倾向,可通过经历缓慢改变。"
+ },
+ {
+ "cat": "persona",
+ "sub": "谨慎",
+ "name": "步步为营",
+ "tier": "精良",
+ "desc": "性格「谨慎」表现·步步为营。影响 NPC 决策权重与事件倾向,可通过经历缓慢改变。"
+ },
+ {
+ "cat": "persona",
+ "sub": "狂热",
+ "name": "信仰燃烧",
+ "tier": "稀有",
+ "desc": "性格「狂热」表现·信仰燃烧。影响 NPC 决策权重与事件倾向,可通过经历缓慢改变。"
+ },
+ {
+ "cat": "persona",
+ "sub": "冷静",
+ "name": "战术分析",
+ "tier": "史诗",
+ "desc": "性格「冷静」表现·战术分析。影响 NPC 决策权重与事件倾向,可通过经历缓慢改变。"
+ },
+ {
+ "cat": "persona",
+ "sub": "狡黠",
+ "name": "诡诈",
+ "tier": "传说",
+ "desc": "性格「狡黠」表现·诡诈。影响 NPC 决策权重与事件倾向,可通过经历缓慢改变。"
+ },
+ {
+ "cat": "talent",
+ "sub": "战斗",
+ "name": "连击",
+ "tier": "普通",
+ "desc": "天赋「战斗」·连击。角色成长可点亮,提供被动增益,可经事件洗点。"
+ },
+ {
+ "cat": "talent",
+ "sub": "统率",
+ "name": "鼓舞",
+ "tier": "稀有",
+ "desc": "天赋「统率」·鼓舞。角色成长可点亮,提供被动增益,可经事件洗点。"
+ },
+ {
+ "cat": "talent",
+ "sub": "建造",
+ "name": "巧匠",
+ "tier": "传说",
+ "desc": "天赋「建造」·巧匠。角色成长可点亮,提供被动增益,可经事件洗点。"
+ },
+ {
+ "cat": "talent",
+ "sub": "贸易",
+ "name": "议价",
+ "tier": "精良",
+ "desc": "天赋「贸易」·议价。角色成长可点亮,提供被动增益,可经事件洗点。"
+ },
+ {
+ "cat": "talent",
+ "sub": "研究",
+ "name": "聪慧",
+ "tier": "史诗",
+ "desc": "天赋「研究」·聪慧。角色成长可点亮,提供被动增益,可经事件洗点。"
+ },
+ {
+ "cat": "relic",
+ "sub": "创世",
+ "name": "创世之核",
+ "tier": "普通",
+ "desc": "传说神器·创世之核。源自地心遗迹碎片,可镶嵌于文明核心激活特效,影响世界等级跃迁。"
+ },
+ {
+ "cat": "relic",
+ "sub": "星辰",
+ "name": "轮回镜",
+ "tier": "精良",
+ "desc": "传说神器·轮回镜。源自地心遗迹碎片,可镶嵌于文明核心激活特效,影响世界等级跃迁。"
+ },
+ {
+ "cat": "relic",
+ "sub": "海洋",
+ "name": "星陨剑",
+ "tier": "稀有",
+ "desc": "传说神器·星陨剑。源自地心遗迹碎片,可镶嵌于文明核心激活特效,影响世界等级跃迁。"
+ },
+ {
+ "cat": "relic",
+ "sub": "熔火",
+ "name": "森罗盘",
+ "tier": "史诗",
+ "desc": "传说神器·森罗盘。源自地心遗迹碎片,可镶嵌于文明核心激活特效,影响世界等级跃迁。"
+ },
+ {
+ "cat": "relic",
+ "sub": "虚空",
+ "name": "永夜冠",
+ "tier": "传说",
+ "desc": "传说神器·永夜冠。源自地心遗迹碎片,可镶嵌于文明核心激活特效,影响世界等级跃迁。"
+ },
+ {
+ "cat": "equip",
+ "sub": "头部",
+ "name": "守护木头部",
+ "tier": "普通",
+ "desc": "头部装备·守护木头部。提供对应部位属性加成,可强化与镶嵌,受角色等级解锁。"
+ },
+ {
+ "cat": "equip",
+ "sub": "手部",
+ "name": "守护木手部",
+ "tier": "精良",
+ "desc": "手部装备·守护木手部。提供对应部位属性加成,可强化与镶嵌,受角色等级解锁。"
+ },
+ {
+ "cat": "equip",
+ "sub": "胸部",
+ "name": "守护木胸部",
+ "tier": "稀有",
+ "desc": "胸部装备·守护木胸部。提供对应部位属性加成,可强化与镶嵌,受角色等级解锁。"
+ },
+ {
+ "cat": "equip",
+ "sub": "裤装",
+ "name": "守护木裤装",
+ "tier": "史诗",
+ "desc": "裤装装备·守护木裤装。提供对应部位属性加成,可强化与镶嵌,受角色等级解锁。"
+ },
+ {
+ "cat": "equip",
+ "sub": "鞋履",
+ "name": "守护木鞋履",
+ "tier": "传说",
+ "desc": "鞋履装备·守护木鞋履。提供对应部位属性加成,可强化与镶嵌,受角色等级解锁。"
+ },
+ {
+ "cat": "event",
+ "sub": "天象",
+ "name": "双月同辉",
+ "tier": "普通",
+ "desc": "传奇事件·双月同辉(天象)。触发后改变世界走向,可记叙入编年史,影响势力等级。"
+ },
+ {
+ "cat": "event",
+ "sub": "灾变",
+ "name": "大地震裂",
+ "tier": "精良",
+ "desc": "传奇事件·大地震裂(灾变)。触发后改变世界走向,可记叙入编年史,影响势力等级。"
+ },
+ {
+ "cat": "event",
+ "sub": "神迹",
+ "name": "圣光普照",
+ "tier": "稀有",
+ "desc": "传奇事件·圣光普照(神迹)。触发后改变世界走向,可记叙入编年史,影响势力等级。"
+ },
+ {
+ "cat": "event",
+ "sub": "战争",
+ "name": "邻邦来犯",
+ "tier": "史诗",
+ "desc": "传奇事件·邻邦来犯(战争)。触发后改变世界走向,可记叙入编年史,影响势力等级。"
+ },
+ {
+ "cat": "event",
+ "sub": "发现",
+ "name": "遗迹现世",
+ "tier": "传说",
+ "desc": "传奇事件·遗迹现世(发现)。触发后改变世界走向,可记叙入编年史,影响势力等级。"
+ }
+ ]
+};
\ No newline at end of file
diff --git a/website/login.html b/website/login.html
new file mode 100644
index 0000000..d4b84dc
--- /dev/null
+++ b/website/login.html
@@ -0,0 +1,193 @@
+
+
+
+
+
+宇森 · 登录
+
+
+
+
+
+
+
宇森
+
YUSEN · GARDEN OF CIVILIZATION
+
你不是玩家,你是园丁。
+
+
+
+
+
+
+
+
+
+
+