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 `

${g.label}

${rows}
`; + }).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 ``+ + ``+ + ``+ + `${shape}`+ + ``; +} + +// ---------- 渲染:图鉴(分类 → 子级 → 条目) ---------- +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 + ? `${it.name}` + : ''; + 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
+
你不是玩家,你是园丁。
+
+ +
+ +
+ + +
+ + +
+
+ + +
+
+ + +
+ +
+ + + + +
+
+ + +
+ + + + +