Compare commits
2 Commits
954cbc6823
...
3892948d8d
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3892948d8d | ||
|
|
037261e228 |
4
.gitignore
vendored
4
.gitignore
vendored
@ -1,5 +1,7 @@
|
|||||||
# 备份与临时文件,不进版本库
|
|
||||||
__bak_pre_20260722/
|
__bak_pre_20260722/
|
||||||
|
__pycache__/
|
||||||
|
*.pyc
|
||||||
*.bak
|
*.bak
|
||||||
*.bak2
|
*.bak2
|
||||||
*.backup
|
*.backup
|
||||||
|
website/*.bak.*
|
||||||
|
|||||||
2
addons/game_base/__init__.py
Normal file
2
addons/game_base/__init__.py
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
from . import models
|
||||||
|
from . import controllers
|
||||||
16
addons/game_base/__manifest__.py
Normal file
16
addons/game_base/__manifest__.py
Normal file
@ -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 用户打通的登录/注册接口。',
|
||||||
|
}
|
||||||
1
addons/game_base/controllers/__init__.py
Normal file
1
addons/game_base/controllers/__init__.py
Normal file
@ -0,0 +1 @@
|
|||||||
|
from . import game_api
|
||||||
183
addons/game_base/controllers/game_api.py
Normal file
183
addons/game_base/controllers/game_api.py
Normal file
@ -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})
|
||||||
109
addons/game_base/data/game_base_data.xml
Normal file
109
addons/game_base/data/game_base_data.xml
Normal file
@ -0,0 +1,109 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<odoo noupdate="0">
|
||||||
|
|
||||||
|
<!-- ============ 规则 / 等级制度(基于官网) ============ -->
|
||||||
|
<!-- 世界等级 · 五阶段 -->
|
||||||
|
<record id="rule_world_1" model="game.rule">
|
||||||
|
<field name="type">world</field><field name="level">1</field><field name="name">蒙昧</field>
|
||||||
|
<field name="desc"><![CDATA[文明萌芽,仅基础采集与居住,无势力概念,事件以自然为主。]]></field>
|
||||||
|
</record>
|
||||||
|
<record id="rule_world_2" model="game.rule">
|
||||||
|
<field name="type">world</field><field name="level">2</field><field name="name">聚落</field>
|
||||||
|
<field name="desc"><![CDATA[出现村落与分工,解锁基础建造与贸易,可建立首个势力。]]></field>
|
||||||
|
</record>
|
||||||
|
<record id="rule_world_3" model="game.rule">
|
||||||
|
<field name="type">world</field><field name="level">3</field><field name="name">王国</field>
|
||||||
|
<field name="desc"><![CDATA[统一政权成形,解锁军事/宗教/科技分支,传奇事件开始频现。]]></field>
|
||||||
|
</record>
|
||||||
|
<record id="rule_world_4" model="game.rule">
|
||||||
|
<field name="type">world</field><field name="level">4</field><field name="name">文明</field>
|
||||||
|
<field name="desc"><![CDATA[跨层互通成熟,地下与星空纳入治理,神器与装备体系全开。]]></field>
|
||||||
|
</record>
|
||||||
|
<record id="rule_world_5" model="game.rule">
|
||||||
|
<field name="type">world</field><field name="level">5</field><field name="name">飞升</field>
|
||||||
|
<field name="desc"><![CDATA[文明触及世界本质,可开启飞升轮回,进入更高维的演化实验。]]></field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<!-- 势力等级 · Lv.1-6 -->
|
||||||
|
<record id="rule_faction_1" model="game.rule">
|
||||||
|
<field name="type">faction</field><field name="level">1</field><field name="name">部族</field>
|
||||||
|
<field name="desc"><![CDATA[血缘纽带的小群体,无正式制度。]]></field>
|
||||||
|
</record>
|
||||||
|
<record id="rule_faction_2" model="game.rule">
|
||||||
|
<field name="type">faction</field><field name="level">2</field><field name="name">城邦</field>
|
||||||
|
<field name="desc"><![CDATA[多聚落联盟,出现雏形治理与税收。]]></field>
|
||||||
|
</record>
|
||||||
|
<record id="rule_faction_3" model="game.rule">
|
||||||
|
<field name="type">faction</field><field name="level">3</field><field name="name">公国</field>
|
||||||
|
<field name="desc"><![CDATA[领地整合,常备军与法典成型。]]></field>
|
||||||
|
</record>
|
||||||
|
<record id="rule_faction_4" model="game.rule">
|
||||||
|
<field name="type">faction</field><field name="level">4</field><field name="name">王国</field>
|
||||||
|
<field name="desc"><![CDATA[统一王朝,跨层资源调度能力。]]></field>
|
||||||
|
</record>
|
||||||
|
<record id="rule_faction_5" model="game.rule">
|
||||||
|
<field name="type">faction</field><field name="level">5</field><field name="name">帝国</field>
|
||||||
|
<field name="desc"><![CDATA[大陆级霸权,可发起跨世界行动。]]></field>
|
||||||
|
</record>
|
||||||
|
<record id="rule_faction_6" model="game.rule">
|
||||||
|
<field name="type">faction</field><field name="level">6</field><field name="name">星盟</field>
|
||||||
|
<field name="desc"><![CDATA[联结多方世界,参与宏观文明博弈。]]></field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<!-- 人物角色等级 · Lv.1-12 -->
|
||||||
|
<record id="rule_char_1" model="game.rule"><field name="type">character</field><field name="level">1</field><field name="name">萌新</field><field name="desc"><![CDATA[初入世界的无名者。]]></field></record>
|
||||||
|
<record id="rule_char_2" model="game.rule"><field name="type">character</field><field name="level">2</field><field name="name">学徒</field><field name="desc"><![CDATA[掌握基础技艺。]]></field></record>
|
||||||
|
<record id="rule_char_3" model="game.rule"><field name="type">character</field><field name="level">3</field><field name="name">熟手</field><field name="desc"><![CDATA[可独立完成任务。]]></field></record>
|
||||||
|
<record id="rule_char_4" model="game.rule"><field name="type">character</field><field name="level">4</field><field name="name">好手</field><field name="desc"><![CDATA[在某领域崭露头角。]]></field></record>
|
||||||
|
<record id="rule_char_5" model="game.rule"><field name="type">character</field><field name="level">5</field><field name="name">精英</field><field name="desc"><![CDATA[群体中的中坚。]]></field></record>
|
||||||
|
<record id="rule_char_6" model="game.rule"><field name="type">character</field><field name="level">6</field><field name="name">大师</field><field name="desc"><![CDATA[技艺臻于化境。]]></field></record>
|
||||||
|
<record id="rule_char_7" model="game.rule"><field name="type">character</field><field name="level">7</field><field name="name">宗匠</field><field name="desc"><![CDATA[开宗立派的人物。]]></field></record>
|
||||||
|
<record id="rule_char_8" model="game.rule"><field name="type">character</field><field name="level">8</field><field name="name">传奇</field><field name="desc"><![CDATA[事迹被载入编年史。]]></field></record>
|
||||||
|
<record id="rule_char_9" model="game.rule"><field name="type">character</field><field name="level">9</field><field name="name">神话</field><field name="desc"><![CDATA[近乎传说的存在。]]></field></record>
|
||||||
|
<record id="rule_char_10" model="game.rule"><field name="type">character</field><field name="level">10</field><field name="name">半神</field><field name="desc"><![CDATA[触及世界法则。]]></field></record>
|
||||||
|
<record id="rule_char_11" model="game.rule"><field name="type">character</field><field name="level">11</field><field name="name">圣者</field><field name="desc"><![CDATA[被信仰托举的意志。]]></field></record>
|
||||||
|
<record id="rule_char_12" model="game.rule"><field name="type">character</field><field name="level">12</field><field name="name">永恒</field><field name="desc"><![CDATA[超越生死的世界锚点。]]></field></record>
|
||||||
|
|
||||||
|
<!-- ============ 图鉴样例(每类 1 条,供用户参考扩展;按分类+子级分组) ============ -->
|
||||||
|
<record id="codex_race_sample" model="game.codex.entry">
|
||||||
|
<field name="cat">race</field><field name="sub">龙裔</field><field name="name">龙裔</field><field name="tier">传说</field>
|
||||||
|
<field name="desc"><![CDATA[高战斗·低社交的古老主支系,火焰耐性,战斗力突出。文明创建时选定,决定 NPC 性格分布与初始偏向。]]></field>
|
||||||
|
</record>
|
||||||
|
<record id="codex_build_sample" model="game.codex.entry">
|
||||||
|
<field name="cat">build</field><field name="sub">生产</field><field name="name">农场</field><field name="tier">精良</field>
|
||||||
|
<field name="desc"><![CDATA[生产类建筑·农场。提供食物产出,受世界等级与势力等级加成。]]></field>
|
||||||
|
</record>
|
||||||
|
<record id="codex_element_sample" model="game.codex.entry">
|
||||||
|
<field name="cat">element</field><field name="sub">火</field><field name="name">烈焰术</field><field name="tier">稀有</field>
|
||||||
|
<field name="desc"><![CDATA[火系别·应用「烈焰术」。影响对应属性与技能树分支,可被天赋与神器强化。]]></field>
|
||||||
|
</record>
|
||||||
|
<record id="codex_persona_sample" model="game.codex.entry">
|
||||||
|
<field name="cat">persona</field><field name="sub">勇敢</field><field name="name">无畏冲锋</field><field name="tier">精良</field>
|
||||||
|
<field name="desc"><![CDATA[性格「勇敢」表现·无畏冲锋。影响 NPC 决策权重与事件倾向。]]></field>
|
||||||
|
</record>
|
||||||
|
<record id="codex_talent_sample" model="game.codex.entry">
|
||||||
|
<field name="cat">talent</field><field name="sub">战斗</field><field name="name">连击</field><field name="tier">稀有</field>
|
||||||
|
<field name="desc"><![CDATA[天赋「战斗」·连击。角色成长可点亮,提供被动增益,可经事件洗点。]]></field>
|
||||||
|
</record>
|
||||||
|
<record id="codex_relic_sample" model="game.codex.entry">
|
||||||
|
<field name="cat">relic</field><field name="sub">创世</field><field name="name">创世之核</field><field name="tier">传说</field>
|
||||||
|
<field name="desc"><![CDATA[传说神器·创世之核。源自地心遗迹碎片,可镶嵌于文明核心激活特效,影响世界等级跃迁。]]></field>
|
||||||
|
</record>
|
||||||
|
<record id="codex_equip_sample" model="game.codex.entry">
|
||||||
|
<field name="cat">equip</field><field name="sub">头部</field><field name="name">守护木头部</field><field name="tier">精良</field>
|
||||||
|
<field name="desc"><![CDATA[头部装备·守护木头部。提供对应部位属性加成,可强化与镶嵌,受角色等级解锁。]]></field>
|
||||||
|
</record>
|
||||||
|
<record id="codex_event_sample" model="game.codex.entry">
|
||||||
|
<field name="cat">event</field><field name="sub">天象</field><field name="name">双月同辉</field><field name="tier">史诗</field>
|
||||||
|
<field name="desc"><![CDATA[传奇事件·双月同辉(天象)。触发后改变世界走向,可记叙入编年史,影响势力等级。]]></field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<!-- ============ 公告 ============ -->
|
||||||
|
<record id="ann_launch" model="game.announcement">
|
||||||
|
<field name="name">【公告】宇森官网第一阶段上线</field>
|
||||||
|
<field name="date">2026-07-09</field>
|
||||||
|
<field name="priority">important</field>
|
||||||
|
<field name="body_html"><![CDATA[<p>宇森官网第一阶段上线:介绍 / 规则 / 图鉴 / 论坛 / 公告 五块,单页切换呈现。欢迎园丁们入驻。</p>]]></field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
</odoo>
|
||||||
4
addons/game_base/models/__init__.py
Normal file
4
addons/game_base/models/__init__.py
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
from . import codex
|
||||||
|
from . import forum
|
||||||
|
from . import announcement
|
||||||
|
from . import rule
|
||||||
20
addons/game_base/models/announcement.py
Normal file
20
addons/game_base/models/announcement.py
Normal file
@ -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)
|
||||||
33
addons/game_base/models/codex.py
Normal file
33
addons/game_base/models/codex.py
Normal file
@ -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='图片')
|
||||||
34
addons/game_base/models/forum.py
Normal file
34
addons/game_base/models/forum.py
Normal file
@ -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='内容')
|
||||||
19
addons/game_base/models/rule.py
Normal file
19
addons/game_base/models/rule.py
Normal file
@ -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='说明')
|
||||||
11
addons/game_base/security/ir.model.access.csv
Normal file
11
addons/game_base/security/ir.model.access.csv
Normal file
@ -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
|
||||||
|
143
addons/game_base/views/game_base_views.xml
Normal file
143
addons/game_base/views/game_base_views.xml
Normal file
@ -0,0 +1,143 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<odoo>
|
||||||
|
|
||||||
|
<!-- ============ 顶层菜单 ============ -->
|
||||||
|
<menuitem id="menu_game_root" name="宇森游戏" sequence="10"/>
|
||||||
|
|
||||||
|
<!-- ============ 图鉴(按分类 + 子级分组) ============ -->
|
||||||
|
<record id="action_game_codex" model="ir.actions.act_window">
|
||||||
|
<field name="name">图鉴</field>
|
||||||
|
<field name="res_model">game.codex.entry</field>
|
||||||
|
<field name="view_mode">list,form</field>
|
||||||
|
</record>
|
||||||
|
<menuitem id="menu_game_codex" name="图鉴" parent="menu_game_root" action="action_game_codex" sequence="10"/>
|
||||||
|
|
||||||
|
<record id="view_game_codex_list" model="ir.ui.view">
|
||||||
|
<field name="name">game.codex.entry.list</field>
|
||||||
|
<field name="model">game.codex.entry</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<list>
|
||||||
|
<field name="cat"/><field name="sub"/><field name="name"/><field name="tier"/><field name="image"/>
|
||||||
|
</list>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
<record id="view_game_codex_form" model="ir.ui.view">
|
||||||
|
<field name="name">game.codex.entry.form</field>
|
||||||
|
<field name="model">game.codex.entry</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<form>
|
||||||
|
<group>
|
||||||
|
<field name="cat"/><field name="sub"/><field name="tier"/>
|
||||||
|
</group>
|
||||||
|
<group>
|
||||||
|
<field name="name"/>
|
||||||
|
</group>
|
||||||
|
<field name="image" widget="image"/>
|
||||||
|
<field name="desc"/>
|
||||||
|
</form>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<!-- ============ 规则(菜单仅保留「规则」二字,按类别分组) ============ -->
|
||||||
|
<record id="action_game_rule" model="ir.actions.act_window">
|
||||||
|
<field name="name">规则</field>
|
||||||
|
<field name="res_model">game.rule</field>
|
||||||
|
<field name="view_mode">list,form</field>
|
||||||
|
</record>
|
||||||
|
<menuitem id="menu_game_rule" name="规则" parent="menu_game_root" action="action_game_rule" sequence="20"/>
|
||||||
|
<record id="view_game_rule_list" model="ir.ui.view">
|
||||||
|
<field name="name">game.rule.list</field>
|
||||||
|
<field name="model">game.rule</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<list><field name="type"/><field name="level"/><field name="name"/></list>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
<record id="view_game_rule_form" model="ir.ui.view">
|
||||||
|
<field name="name">game.rule.form</field>
|
||||||
|
<field name="model">game.rule</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<form><group><field name="type"/><field name="level"/><field name="name"/></group><field name="desc"/></form>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<!-- ============ 论坛(放后面) ============ -->
|
||||||
|
<menuitem id="menu_game_forum" name="论坛" parent="menu_game_root" sequence="30"/>
|
||||||
|
<record id="action_game_forum_thread" model="ir.actions.act_window">
|
||||||
|
<field name="name">主题</field>
|
||||||
|
<field name="res_model">game.forum.thread</field>
|
||||||
|
<field name="view_mode">list,form</field>
|
||||||
|
</record>
|
||||||
|
<menuitem id="menu_game_forum_thread" name="主题" parent="menu_game_forum" action="action_game_forum_thread" sequence="31"/>
|
||||||
|
<record id="action_game_forum_post" model="ir.actions.act_window">
|
||||||
|
<field name="name">回复</field>
|
||||||
|
<field name="res_model">game.forum.post</field>
|
||||||
|
<field name="view_mode">list,form</field>
|
||||||
|
</record>
|
||||||
|
<menuitem id="menu_game_forum_post" name="回复" parent="menu_game_forum" action="action_game_forum_post" sequence="32"/>
|
||||||
|
|
||||||
|
<record id="view_game_forum_thread_list" model="ir.ui.view">
|
||||||
|
<field name="name">game.forum.thread.list</field>
|
||||||
|
<field name="model">game.forum.thread</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<list>
|
||||||
|
<field name="pinned"/><field name="name"/><field name="category"/><field name="author_name"/><field name="create_date"/>
|
||||||
|
</list>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
<record id="view_game_forum_thread_form" model="ir.ui.view">
|
||||||
|
<field name="name">game.forum.thread.form</field>
|
||||||
|
<field name="model">game.forum.thread</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<form>
|
||||||
|
<group>
|
||||||
|
<field name="name"/><field name="category"/><field name="author_name"/><field name="pinned"/>
|
||||||
|
</group>
|
||||||
|
<field name="body_html"/>
|
||||||
|
<field name="post_ids">
|
||||||
|
<tree><field name="author_name"/><field name="create_date"/></tree>
|
||||||
|
<form><field name="author_name"/><field name="body_html"/></form>
|
||||||
|
</field>
|
||||||
|
</form>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
<record id="view_game_forum_post_list" model="ir.ui.view">
|
||||||
|
<field name="name">game.forum.post.list</field>
|
||||||
|
<field name="model">game.forum.post</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<list><field name="thread_id"/><field name="author_name"/><field name="create_date"/></list>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
<record id="view_game_forum_post_form" model="ir.ui.view">
|
||||||
|
<field name="name">game.forum.post.form</field>
|
||||||
|
<field name="model">game.forum.post</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<form><field name="thread_id"/><field name="author_name"/><field name="body_html"/></form>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<!-- ============ 公告(放最后) ============ -->
|
||||||
|
<record id="action_game_announcement" model="ir.actions.act_window">
|
||||||
|
<field name="name">公告</field>
|
||||||
|
<field name="res_model">game.announcement</field>
|
||||||
|
<field name="view_mode">list,form</field>
|
||||||
|
</record>
|
||||||
|
<menuitem id="menu_game_announcement" name="公告" parent="menu_game_root" action="action_game_announcement" sequence="40"/>
|
||||||
|
<record id="view_game_announcement_list" model="ir.ui.view">
|
||||||
|
<field name="name">game.announcement.list</field>
|
||||||
|
<field name="model">game.announcement</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<list><field name="name"/><field name="date"/><field name="priority"/><field name="active"/></list>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
<record id="view_game_announcement_form" model="ir.ui.view">
|
||||||
|
<field name="name">game.announcement.form</field>
|
||||||
|
<field name="model">game.announcement</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<form>
|
||||||
|
<group><field name="name"/><field name="date"/><field name="priority"/><field name="active"/></group>
|
||||||
|
<field name="body_html"/>
|
||||||
|
</form>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
</odoo>
|
||||||
42
website/assets/game-api.js
Normal file
42
website/assets/game-api.js
Normal file
@ -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();
|
||||||
|
},
|
||||||
|
};
|
||||||
69
website/assets/site-app.js
Normal file
69
website/assets/site-app.js
Normal file
@ -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 = '<div class="tdesc">暂无规则数据</div>'; return; }
|
||||||
|
el.innerHTML = groups.map(g => {
|
||||||
|
const rows = (g.items || []).map(it => {
|
||||||
|
const lvl = (it.level != null) ? ('Lv.' + it.level) : (g.key === 'world' ? '阶段' : '');
|
||||||
|
return `<div class="tier-row"><span class="tn">${lvl}</span><span class="tname">${it.name}</span><span class="tdesc">${it.desc || ''}</span></div>`;
|
||||||
|
}).join('');
|
||||||
|
return `<div class="rule-block"><h3>${g.label}</h3><div class="tier-list">${rows}</div></div>`;
|
||||||
|
}).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 = '<div class="th"><span class="tt">暂无主题</span></div>'; return; }
|
||||||
|
// 分类筛选
|
||||||
|
const cats = [];
|
||||||
|
list.forEach(t => { if (t.category && cats.indexOf(t.category) < 0) cats.push(t.category); });
|
||||||
|
if (catsEl){
|
||||||
|
catsEl.innerHTML = `<div class="ch active" data-fcat="all">全部</div>` +
|
||||||
|
cats.map(c => `<div class="ch" data-fcat="${c}">${c}</div>`).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 =>
|
||||||
|
`<div class="th" data-fcat="${t.category || 'all'}">` +
|
||||||
|
(t.pinned ? `<span class="pin">置顶</span>` : '') +
|
||||||
|
`<span class="tt">${t.name}</span>` +
|
||||||
|
`<span class="meta"><span class="rp">${(t.replies || []).length}</span> 回复 · ${t.author_name || '匿名'}</span></div>`
|
||||||
|
).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 = '<div class="note"><div class="body">暂无公告</div></div>'; return; }
|
||||||
|
el.innerHTML = list.map(n =>
|
||||||
|
`<div class="note"><div class="date">${n.date}</div><div class="body">${n.body_html || ''}</div></div>`
|
||||||
|
).join('');
|
||||||
|
} catch (e) { /* 保留静态兜底内容 */ }
|
||||||
|
}
|
||||||
|
|
||||||
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
loadRules();
|
||||||
|
loadForum();
|
||||||
|
loadAnnounce();
|
||||||
|
});
|
||||||
157
website/assets/world-app.js
Normal file
157
website/assets/world-app.js
Normal file
@ -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 = '<div class="dp-loading">加载中…</div>';
|
||||||
|
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 = '<div class="dp-loading">加载失败,请稍后重试</div>';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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<s.length;i++){h^=s.charCodeAt(i);h=Math.imul(h,16777619);}return h>>>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<spikes*2;i++){const rad=i%2?inner:outer;const a=rot+i*Math.PI/spikes;pts.push((cx+Math.cos(a)*rad).toFixed(1)+','+(cy+Math.sin(a)*rad).toFixed(1));}return pts.join(' ');}
|
||||||
|
function polygonPts(cx,cy,sides,R){let pts=[];for(let i=0;i<sides;i++){const a=-Math.PI/2+i*2*Math.PI/sides;pts.push((cx+Math.cos(a)*R).toFixed(1)+','+(cy+Math.sin(a)*R).toFixed(1));}return pts.join(' ');}
|
||||||
|
function shRace(r,s){const v=Math.floor(r()*3);let x=`<ellipse cx="50" cy="55" rx="19" ry="21" fill="url(#fg${s})" stroke="rgba(255,255,255,.25)" stroke-width="1"/>`;if(v===0){x+=`<path d="M35 40 L31 20 L43 36 Z" fill="url(#fg${s})"/><path d="M65 40 L69 20 L57 36 Z" fill="url(#fg${s})"/>`;}else if(v===1){x+=`<path d="M32 52 L20 42 L36 46 Z" fill="url(#fg${s})"/><path d="M68 52 L80 42 L64 46 Z" fill="url(#fg${s})"/>`;}else{x+=`<path d="M34 36 L42 24 L50 34 L58 24 L66 36 Z" fill="url(#fg${s})"/>`;}x+=`<circle cx="43" cy="55" r="2.6" fill="#0b1020"/><circle cx="57" cy="55" r="2.6" fill="#0b1020"/>`;return x;}
|
||||||
|
function shBuild(r,s){let x=`<rect x="34" y="44" width="32" height="34" rx="2" fill="url(#fg${s})" opacity=".92"/>`;x+=`<path d="M30 44 L50 28 L70 44 Z" fill="url(#fg${s})"/>`;for(let i=0;i<3;i++)x+=`<rect x="${39+i*7}" y="52" width="4" height="7" rx="1" fill="rgba(8,12,26,.7)"/>`;return x;}
|
||||||
|
function shElement(r,s){const sides=3+Math.floor(r()*6);let x=`<polygon points="${polygonPts(50,52,sides,25)}" fill="none" stroke="url(#fg${s})" stroke-width="3"/>`;x+=`<circle cx="50" cy="52" r="7" fill="url(#fg${s})"/>`;return x;}
|
||||||
|
function shPersona(r,s){let x=`<path d="M32 36 Q50 28 68 36 Q72 56 50 70 Q28 56 32 36 Z" fill="url(#fg${s})" opacity=".9"/>`;x+=`<circle cx="43" cy="48" r="2.8" fill="#0b1020"/><circle cx="57" cy="48" r="2.8" fill="#0b1020"/>`;x+=`<path d="M43 60 Q50 64 57 60" stroke="#0b1020" stroke-width="2" fill="none"/>`;return x;}
|
||||||
|
function shTalent(r,s){const sp=4+Math.floor(r()*4);let x=`<polygon points="${starPts(50,52,sp,26,11)}" fill="url(#fg${s})" opacity=".92"/>`;x+=`<circle cx="50" cy="52" r="5" fill="rgba(8,12,26,.55)"/>`;return x;}
|
||||||
|
function shRelic(r,s){let x=`<path d="M50 24 L70 46 L50 76 L30 46 Z" fill="url(#fg${s})" opacity=".9" stroke="rgba(255,255,255,.3)" stroke-width="1"/>`;x+=`<path d="M30 46 L70 46 M50 24 L42 46 L50 76 M50 24 L58 46 L50 76" stroke="rgba(8,12,26,.4)" stroke-width="1" fill="none"/>`;return x;}
|
||||||
|
function shEquip(sub,s){switch(sub){case 'head':return `<path d="M32 56 Q32 30 50 30 Q68 30 68 56 L68 60 L32 60 Z" fill="url(#fg${s})"/><rect x="44" y="44" width="12" height="5" rx="2" fill="rgba(8,12,26,.6)"/>`;case 'hand':{let x=`<rect x="36" y="40" width="28" height="22" rx="6" fill="url(#fg${s})"/><rect x="33" y="44" width="9" height="14" rx="3" fill="url(#fg${s})"/>`;for(let i=0;i<3;i++)x+=`<rect x="${40+i*8}" y="62" width="5" height="8" rx="2" fill="url(#fg${s})"/>`;return x;}case 'chest':return `<path d="M30 46 Q50 38 70 46 L70 66 Q50 72 30 66 Z" fill="url(#fg${s})"/><rect x="47" y="46" width="6" height="18" fill="rgba(8,12,26,.5)"/>`;case 'pants':return `<path d="M38 32 L62 32 L60 74 L52 74 L50 50 L48 74 L40 74 Z" fill="url(#fg${s})"/>`;case 'shoes':return `<rect x="34" y="44" width="28" height="6" rx="3" fill="url(#fg${s})"/><path d="M34 48 L60 48 L66 64 L34 64 Z" fill="url(#fg${s})"/>`;case 'ring':return `<circle cx="50" cy="55" r="16" fill="none" stroke="url(#fg${s})" stroke-width="5"/><path d="M50 29 L57 40 L43 40 Z" fill="url(#fg${s})"/>`;case 'neck':return `<path d="M32 42 Q50 64 68 42" fill="none" stroke="url(#fg${s})" stroke-width="3"/><path d="M44 58 L50 72 L56 58 Z" fill="url(#fg${s})"/>`;case 'treasure':return `<rect x="38" y="60" width="24" height="9" rx="2" fill="url(#fg${s})"/><circle cx="50" cy="46" r="14" fill="url(#fg${s})" opacity=".9"/><circle cx="50" cy="46" r="6" fill="rgba(255,255,255,.4)"/>`;default:return `<circle cx="50" cy="50" r="18" fill="url(#fg${s})"/>`;}}
|
||||||
|
function shEvent(r,s){let x=`<ellipse cx="50" cy="52" rx="22" ry="28" fill="none" stroke="url(#fg${s})" stroke-width="3"/>`;x+=`<ellipse cx="50" cy="52" rx="12" ry="16" fill="url(#fg${s})" opacity=".5"/>`;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 `<circle cx="50" cy="50" r="18" fill="url(#fg${s})"/>`;}}
|
||||||
|
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 `<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg" preserveAspectRatio="xMidYMid slice">`+
|
||||||
|
`<defs><radialGradient id="bg${seed}" cx="50%" cy="36%" r="78%"><stop offset="0%" stop-color="hsl(${h},48%,26%)"/><stop offset="100%" stop-color="hsl(${h},42%,9%)"/></radialGradient>`+
|
||||||
|
`<linearGradient id="fg${seed}" x1="0" y1="0" x2="0" y2="1"><stop offset="0%" stop-color="hsl(${h},72%,66%)"/><stop offset="100%" stop-color="hsl(${h2},70%,56%)"/></linearGradient></defs>`+
|
||||||
|
`<rect width="100" height="100" fill="url(#bg${seed})"/>${shape}`+
|
||||||
|
`<circle cx="50" cy="50" r="46" fill="none" stroke="${tcol}" stroke-width="2.5" opacity=".9"/></svg>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------- 渲染:图鉴(分类 → 子级 → 条目) ----------
|
||||||
|
function renderCodex(data){
|
||||||
|
const groups = (data && data.groups) || [];
|
||||||
|
if (!groups.length){ dpBody.innerHTML = '<div class="dp-empty">暂无图鉴数据</div>'; return; }
|
||||||
|
let html = '';
|
||||||
|
groups.forEach(g => {
|
||||||
|
html += `<div class="dp-block"><h3>${g.label}</h3>`;
|
||||||
|
(g.subs || []).forEach(sub => {
|
||||||
|
if (sub.name && sub.name !== '其他') html += `<div class="dp-sublabel">${sub.name}</div>`;
|
||||||
|
html += '<div class="dp-grid">';
|
||||||
|
sub.items.forEach(it => {
|
||||||
|
const imgTag = it.image
|
||||||
|
? `<img class="dp-thumb-img" src="${it.image}" alt="${it.name}" onerror="this.outerHTML=window.__thumbCache?window.__thumbCache:''" data-cat="${it.cat}" data-sub="${it.sub}" data-name="${it.name}" data-tier="${it.tier}"/>`
|
||||||
|
: '';
|
||||||
|
const fallback = `<div class="dp-thumb-svg">${thumbSVG(it)}</div>`;
|
||||||
|
html += `<div class="dp-card">`+
|
||||||
|
`<div class="dp-thumb">${imgTag || fallback}</div>`+
|
||||||
|
`<div class="dp-card-body"><div class="dp-nm">${it.name}</div>`+
|
||||||
|
`<div class="dp-meta"><span class="dp-sub">${it.sub||''}</span><span class="dp-tier">${it.tier||''}</span></div>`+
|
||||||
|
`<div class="dp-ds">${it.desc||''}</div></div></div>`;
|
||||||
|
});
|
||||||
|
html += '</div>';
|
||||||
|
});
|
||||||
|
html += '</div>';
|
||||||
|
});
|
||||||
|
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 = '<div class="dp-empty">暂无规则数据</div>'; return; }
|
||||||
|
let html = '';
|
||||||
|
groups.forEach(g => {
|
||||||
|
html += `<div class="dp-block"><h3>${g.label}</h3><div class="dp-rows">`;
|
||||||
|
(g.items || []).forEach(it => {
|
||||||
|
const lvl = (it.level!=null) ? ('Lv.'+it.level) : '';
|
||||||
|
html += `<div class="dp-row"><span class="dp-lvl">${lvl}</span><span class="dp-rname">${it.name}</span><span class="dp-rdesc">${it.desc||''}</span></div>`;
|
||||||
|
});
|
||||||
|
html += '</div></div>';
|
||||||
|
});
|
||||||
|
dpBody.innerHTML = html;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------- 渲染:论坛 ----------
|
||||||
|
function renderForum(threads){
|
||||||
|
threads = threads || [];
|
||||||
|
if (!threads.length){ dpBody.innerHTML = '<div class="dp-empty">暂无主题</div>'; return; }
|
||||||
|
let html = '<div class="dp-threads">';
|
||||||
|
threads.forEach(t => {
|
||||||
|
const replies = (t.replies || []).map(p =>
|
||||||
|
`<div class="dp-reply"><div class="dp-reply-head"><b>${p.author_name||'匿名'}</b><span>${p.create_date}</span></div><div class="dp-reply-body">${p.body_html||''}</div></div>`
|
||||||
|
).join('');
|
||||||
|
html += `<div class="dp-thread">`+
|
||||||
|
`<div class="dp-thread-head"><span class="dp-cat">${t.category_label||''}</span>`+
|
||||||
|
(t.pinned?`<span class="dp-pin">置顶</span>`:'')+`<span class="dp-tt">${t.name}</span>`+
|
||||||
|
`<span class="dp-author">${t.author_name||''}</span></div>`+
|
||||||
|
`<div class="dp-thread-body">${t.body_html||''}</div>`+
|
||||||
|
`<div class="dp-replies">${replies}</div></div>`;
|
||||||
|
});
|
||||||
|
html += '</div>';
|
||||||
|
dpBody.innerHTML = html;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------- 渲染:公告 ----------
|
||||||
|
function renderAnnounce(list){
|
||||||
|
list = list || [];
|
||||||
|
if (!list.length){ dpBody.innerHTML = '<div class="dp-empty">暂无公告</div>'; return; }
|
||||||
|
let html = '<div class="dp-notes">';
|
||||||
|
list.forEach(n => {
|
||||||
|
const pri = n.priority==='urgent' ? 'urgent' : (n.priority==='important' ? 'imp' : '');
|
||||||
|
html += `<div class="dp-note ${pri}"><div class="dp-date">${n.date}</div>`+
|
||||||
|
`<div class="dp-note-body">${n.body_html||''}</div></div>`;
|
||||||
|
});
|
||||||
|
html += '</div>';
|
||||||
|
dpBody.innerHTML = html;
|
||||||
|
}
|
||||||
BIN
website/assets/yusen-trailer.mp4
Normal file
BIN
website/assets/yusen-trailer.mp4
Normal file
Binary file not shown.
428
website/codex-data.js
Normal file
428
website/codex-data.js
Normal file
@ -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": "传奇事件·遗迹现世(发现)。触发后改变世界走向,可记叙入编年史,影响势力等级。"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
||||||
193
website/login.html
Normal file
193
website/login.html
Normal file
@ -0,0 +1,193 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>宇森 · 登录</title>
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=ZCOOL+QingKe+HuangYou&family=Noto+Sans+SC:wght@300;400;500;700&display=swap" rel="stylesheet" />
|
||||||
|
<style>
|
||||||
|
:root{
|
||||||
|
--bg-0:#080c1a; --bg-1:#0e1730; --bg-2:#15213f;
|
||||||
|
--violet:#8b5cf6; --magenta:#c77dff; --cyan:#4cc9f0; --gold:#e7b85c;
|
||||||
|
--text:#e9edf6; --text-dim:#9aa6c4;
|
||||||
|
--card:rgba(255,255,255,.05); --card-bd:rgba(255,255,255,.12);
|
||||||
|
--glow:0 0 22px rgba(139,92,246,.30);
|
||||||
|
}
|
||||||
|
*{box-sizing:border-box;margin:0;padding:0;}
|
||||||
|
html,body{height:100%;}
|
||||||
|
body{
|
||||||
|
font-family:"Noto Sans SC",system-ui,sans-serif;color:var(--text);
|
||||||
|
background:
|
||||||
|
radial-gradient(1000px 620px at 82% -8%, rgba(139,92,246,.16), transparent 60%),
|
||||||
|
radial-gradient(820px 560px at 8% 112%, rgba(76,201,240,.12), transparent 55%),
|
||||||
|
linear-gradient(160deg, var(--bg-0), var(--bg-1) 48%, var(--bg-2));
|
||||||
|
display:flex;align-items:center;justify-content:center;min-height:100vh;overflow:hidden;position:relative;
|
||||||
|
}
|
||||||
|
body::before,body::after{content:"";position:fixed;border-radius:50%;filter:blur(80px);z-index:0;pointer-events:none;}
|
||||||
|
body::before{width:380px;height:380px;background:rgba(139,92,246,.20);top:-110px;right:-70px;animation:f1 20s ease-in-out infinite;}
|
||||||
|
body::after{width:340px;height:340px;background:rgba(76,201,240,.12);bottom:-110px;left:-60px;animation:f2 24s ease-in-out infinite;}
|
||||||
|
@keyframes f1{0%,100%{transform:translate(0,0)}50%{transform:translate(-36px,36px)}}
|
||||||
|
@keyframes f2{0%,100%{transform:translate(0,0)}50%{transform:translate(36px,-28px)}}
|
||||||
|
|
||||||
|
.wrap{position:relative;z-index:2;width:min(420px,92vw);}
|
||||||
|
.brand{text-align:center;margin-bottom:26px;}
|
||||||
|
.brand .cn{font-family:"ZCOOL QingKe HuangYou",sans-serif;font-size:38px;letter-spacing:6px;color:#fff;text-shadow:0 0 18px rgba(139,92,246,.6);}
|
||||||
|
.brand .en{font-size:11px;color:var(--magenta);letter-spacing:3px;margin-top:6px;}
|
||||||
|
.brand .tag{font-size:13px;color:var(--text-dim);margin-top:10px;}
|
||||||
|
|
||||||
|
.card{background:var(--card);border:1px solid var(--card-bd);border-radius:18px;padding:26px 26px 30px;backdrop-filter:blur(10px);box-shadow:var(--glow);}
|
||||||
|
.tabs{display:flex;gap:6px;margin-bottom:20px;background:rgba(8,12,26,.5);border-radius:12px;padding:5px;}
|
||||||
|
.tabs button{flex:1;font-family:inherit;font-size:14px;font-weight:500;color:var(--text-dim);background:transparent;border:none;padding:9px;border-radius:9px;cursor:pointer;transition:.2s;}
|
||||||
|
.tabs button.active{color:#fff;background:rgba(139,92,246,.28);box-shadow:var(--glow);}
|
||||||
|
|
||||||
|
.field{margin-bottom:14px;}
|
||||||
|
.field label{display:block;font-size:12px;color:var(--text-dim);margin-bottom:6px;letter-spacing:1px;}
|
||||||
|
.field input{width:100%;background:rgba(8,12,26,.6);border:1px solid var(--card-bd);border-radius:10px;color:var(--text);padding:11px 13px;font-family:inherit;font-size:14px;outline:none;transition:.2s;}
|
||||||
|
.field input:focus{border-color:var(--violet);box-shadow:0 0 0 3px rgba(139,92,246,.18);}
|
||||||
|
|
||||||
|
.submit{width:100%;margin-top:6px;font-size:15px;font-weight:700;color:#0b1020;background:linear-gradient(120deg,var(--gold),#f0d49a);padding:12px;border-radius:11px;cursor:pointer;border:none;letter-spacing:2px;transition:.22s;box-shadow:0 0 18px rgba(231,184,92,.35);}
|
||||||
|
.submit:hover{filter:brightness(1.08);}
|
||||||
|
.submit:disabled{opacity:.6;cursor:default;}
|
||||||
|
|
||||||
|
.msg{margin-top:14px;font-size:13px;text-align:center;min-height:18px;}
|
||||||
|
.msg.err{color:#ff8a8a;}
|
||||||
|
.msg.ok{color:#8be39a;}
|
||||||
|
|
||||||
|
.back{text-align:center;margin-top:18px;font-size:13px;color:var(--text-dim);}
|
||||||
|
.back a{color:var(--cyan);text-decoration:none;}
|
||||||
|
.back a:hover{text-decoration:underline;}
|
||||||
|
|
||||||
|
.hint{margin-bottom:16px;font-size:13px;color:var(--cyan);background:rgba(76,201,240,.10);border:1px solid rgba(76,201,240,.30);border-radius:10px;padding:9px 12px;text-align:center;}
|
||||||
|
|
||||||
|
.hidden{display:none;}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="wrap">
|
||||||
|
<div class="brand">
|
||||||
|
<div class="cn">宇森</div>
|
||||||
|
<div class="en">YUSEN · GARDEN OF CIVILIZATION</div>
|
||||||
|
<div class="tag">你不是玩家,你是园丁。</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<div class="hint hidden" id="hint">登录后即可进入游戏</div>
|
||||||
|
<div class="tabs">
|
||||||
|
<button id="tabLogin" class="active">登录</button>
|
||||||
|
<button id="tabReg">注册</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 登录 -->
|
||||||
|
<form id="loginForm" autocomplete="off">
|
||||||
|
<div class="field">
|
||||||
|
<label>账号</label>
|
||||||
|
<input id="lLogin" type="text" placeholder="Odoo 用户名 / 邮箱" />
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<label>密码</label>
|
||||||
|
<input id="lPwd" type="password" placeholder="密码" />
|
||||||
|
</div>
|
||||||
|
<button class="submit" type="submit">进入世界</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<!-- 注册 -->
|
||||||
|
<form id="regForm" class="hidden" autocomplete="off">
|
||||||
|
<div class="field">
|
||||||
|
<label>昵称</label>
|
||||||
|
<input id="rName" type="text" placeholder="显示名称(可空)" />
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<label>账号</label>
|
||||||
|
<input id="rLogin" type="text" placeholder="登录用账号" />
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<label>邮箱</label>
|
||||||
|
<input id="rEmail" type="email" placeholder="邮箱(可空)" />
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<label>密码</label>
|
||||||
|
<input id="rPwd" type="password" placeholder="至少 6 位" />
|
||||||
|
</div>
|
||||||
|
<button class="submit" type="submit">创建园丁账号</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<div class="msg" id="msg"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="back"><a href="index.html">← 返回官网</a></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script src="assets/game-api.js"></script>
|
||||||
|
<script>
|
||||||
|
const tabLogin = document.getElementById('tabLogin');
|
||||||
|
const tabReg = document.getElementById('tabReg');
|
||||||
|
const loginForm = document.getElementById('loginForm');
|
||||||
|
const regForm = document.getElementById('regForm');
|
||||||
|
const msg = document.getElementById('msg');
|
||||||
|
|
||||||
|
// 回跳目标(进入游戏流程带 ?next=world.html)
|
||||||
|
const params = new URLSearchParams(location.search);
|
||||||
|
const nextUrl = params.get('next') || 'world.html';
|
||||||
|
const hint = document.getElementById('hint');
|
||||||
|
if (params.get('next')) hint.classList.remove('hidden');
|
||||||
|
// 已登录则直接跳走
|
||||||
|
(async () => {
|
||||||
|
try { const me = await API.me(); if (me && me.uid) location.href = nextUrl; } catch (e) {}
|
||||||
|
})();
|
||||||
|
|
||||||
|
function showMsg(text, kind){ msg.textContent = text || ''; msg.className = 'msg' + (kind ? ' ' + kind : ''); }
|
||||||
|
function switchTab(which){
|
||||||
|
if (which === 'login'){
|
||||||
|
tabLogin.classList.add('active'); tabReg.classList.remove('active');
|
||||||
|
loginForm.classList.remove('hidden'); regForm.classList.add('hidden');
|
||||||
|
} else {
|
||||||
|
tabReg.classList.add('active'); tabLogin.classList.remove('active');
|
||||||
|
regForm.classList.remove('hidden'); loginForm.classList.add('hidden');
|
||||||
|
}
|
||||||
|
showMsg('');
|
||||||
|
}
|
||||||
|
tabLogin.onclick = () => switchTab('login');
|
||||||
|
tabReg.onclick = () => switchTab('reg');
|
||||||
|
|
||||||
|
loginForm.addEventListener('submit', async (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
showMsg('');
|
||||||
|
const login = document.getElementById('lLogin').value.trim();
|
||||||
|
const password = document.getElementById('lPwd').value;
|
||||||
|
if (!login || !password){ showMsg('请填写账号和密码', 'err'); return; }
|
||||||
|
const btn = loginForm.querySelector('.submit'); btn.disabled = true; btn.textContent = '登录中…';
|
||||||
|
try {
|
||||||
|
const r = await API.login(login, password);
|
||||||
|
if (r.ok){ location.href = nextUrl; }
|
||||||
|
else { showMsg(r.error || '登录失败', 'err'); }
|
||||||
|
} catch (err) {
|
||||||
|
showMsg('网络错误,请稍后重试', 'err');
|
||||||
|
} finally {
|
||||||
|
btn.disabled = false; btn.textContent = '进入世界';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
regForm.addEventListener('submit', async (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
showMsg('');
|
||||||
|
const payload = {
|
||||||
|
name: document.getElementById('rName').value.trim(),
|
||||||
|
login: document.getElementById('rLogin').value.trim(),
|
||||||
|
email: document.getElementById('rEmail').value.trim(),
|
||||||
|
password: document.getElementById('rPwd').value,
|
||||||
|
};
|
||||||
|
if (!payload.login || !payload.password){ showMsg('账号和密码必填', 'err'); return; }
|
||||||
|
const btn = regForm.querySelector('.submit'); btn.disabled = true; btn.textContent = '注册中…';
|
||||||
|
try {
|
||||||
|
const r = await API.register(payload);
|
||||||
|
if (r.ok){ location.href = nextUrl; }
|
||||||
|
else { showMsg(r.error || '注册失败', 'err'); }
|
||||||
|
} catch (err) {
|
||||||
|
showMsg('网络错误,请稍后重试', 'err');
|
||||||
|
} finally {
|
||||||
|
btn.disabled = false; btn.textContent = '创建园丁账号';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
Loading…
Reference in New Issue
Block a user