diff --git a/addons/game_base/__manifest__.py b/addons/game_base/__manifest__.py
index c94b53d..7754e44 100644
--- a/addons/game_base/__manifest__.py
+++ b/addons/game_base/__manifest__.py
@@ -1,14 +1,15 @@
{
'name': '宇森游戏基础数据',
- 'summary': '宇森游戏基础数据:图鉴 / 规则 / 论坛 / 公告',
- 'version': '1.2',
+ 'summary': '宇森游戏基础数据:图鉴 / 规则 / 论坛 / 公告 / 玩家员工',
+ 'version': '1.3',
'category': 'Game',
'sequence': 10,
- 'depends': ['base'],
+ 'depends': ['base', 'hr'],
'data': [
'security/ir.model.access.csv',
'data/game_base_data.xml',
'data/story_cron.xml',
+ 'data/game_achievement_data.xml',
'views/codex_views.xml',
'views/rule_category_views.xml',
'views/rule_views.xml',
@@ -16,10 +17,13 @@
'views/forum_views.xml',
'views/announcement_views.xml',
'views/story_views.xml',
+ 'views/game_employee_views.xml',
+ 'views/game_achievement_views.xml',
+ 'views/game_mail_views.xml',
'views/menu.xml',
],
'application': True,
'license': 'LGPL-3',
'pre_init_hook': 'pre_init_hook',
- 'description': '基于官网内容沉淀的游戏基础数据模型,供后台维护、前端经 /game/api/* 读取;并提供与 Odoo 用户打通的登录/注册接口。',
+ 'description': '基于官网内容沉淀的游戏基础数据模型,供后台维护、前端经 /game/api/* 读取;并提供与 Odoo 用户打通的登录/注册接口。注册时自动创建 hr.employee 并初始化游戏数据。',
}
diff --git a/addons/game_base/controllers/game_api.py b/addons/game_base/controllers/game_api.py
index db230ca..452d637 100644
--- a/addons/game_base/controllers/game_api.py
+++ b/addons/game_base/controllers/game_api.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
import json
-from odoo import http
+from odoo import http, fields
from odoo.http import request, Response
@@ -309,12 +309,52 @@ class GameApiController(http.Controller):
'groups_id': [(6, 0, [portal.id])],
})
request.env.cr.commit() # 让新用户立即对其他游标可见,便于随后自动登录
+ # === 自动创建 hr.employee 并初始化游戏数据 ===
+ Emp = request.env['hr.employee'].sudo()
+ emp = Emp.create({
+ 'name': name or login,
+ 'user_id': user.id,
+ 'work_email': email or False,
+ 'game_title': '萌新',
+ 'game_level': 1,
+ 'game_exp': 0,
+ 'game_exp_max': 100,
+ 'game_coins': 0,
+ 'game_energy': 0,
+ 'game_wood': 0,
+ 'game_stone': 0,
+ 'game_reg_date': fields.Date.today(),
+ })
+ # 生成游戏 UID(格式 10086 + 4位员工ID)
+ emp.game_uid = '10086%04d' % emp.id
+ # 给新玩家解锁第一个成就"开天辟地"
+ tmpl = request.env['game.achievement.template'].sudo().search(
+ [('code', '=', 'first_land')], limit=1)
+ if tmpl:
+ request.env['game.player.achievement'].sudo().create({
+ 'employee_id': emp.id,
+ 'template_id': tmpl.id,
+ 'progress': tmpl.target,
+ 'unlocked': True,
+ 'unlock_date': fields.Datetime.now(),
+ })
+ # 给新玩家发一封欢迎邮件
+ request.env['game.player.mail'].sudo().create({
+ 'employee_id': emp.id,
+ 'category': 'system',
+ 'subject': '欢迎来到宇森',
+ 'sender': '系统',
+ 'body': '
欢迎来到宇森!请查收新手礼包与世界探索指南。
',
+ })
+ request.env.cr.commit()
db = request.session.db or 'game'
auth_info = request.session.authenticate(db, {'type': 'password', 'login': login, 'password': password})
uid = auth_info['uid']
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})
+ return _json({'ok': True, 'uid': uid, 'name': user.name, 'login': user.login,
+ 'emp_id': emp.id, 'game_uid': emp.game_uid})
+ return _json({'ok': True, 'uid': user.id, 'name': user.name, 'login': user.login,
+ 'emp_id': emp.id, 'game_uid': emp.game_uid})
except Exception as e:
return _json({'ok': False, 'error': '注册失败:%s' % str(e)})
@@ -324,9 +364,123 @@ class GameApiController(http.Controller):
if uid:
user = request.env['res.users'].sudo().browse(uid)
if user.exists():
- return _json({'uid': uid, 'name': user.name, 'login': user.login})
+ data = {'uid': uid, 'name': user.name, 'login': user.login,
+ 'email': user.email or ''}
+ emp = request.env['hr.employee'].sudo().search(
+ [('user_id', '=', uid)], limit=1)
+ if emp:
+ data.update(emp.export_game_data())
+ return _json(data)
return _json({'uid': None})
+ @http.route('/game/api/achievements', type='http', auth='public',
+ methods=['GET'], csrf=False)
+ def achievements(self, **kw):
+ """返回当前登录玩家的成就列表(含模板+进度)。"""
+ uid = request.session.uid
+ if not uid:
+ return _json({'error': '未登录'})
+ emp = request.env['hr.employee'].sudo().search(
+ [('user_id', '=', uid)], limit=1)
+ if not emp:
+ return _json({'error': '玩家不存在', 'items': [], 'total_points': 0})
+ # 取所有启用的成就模板
+ tmpl_model = request.env['game.achievement.template'].sudo()
+ tier_labels = dict(tmpl_model._fields['tier'].selection)
+ templates = tmpl_model.search_read(
+ [('active', '=', True)],
+ ['id', 'name', 'icon', 'tier', 'desc', 'points', 'target', 'sequence'],
+ order='sequence, id')
+ # 取该玩家已解锁的成就
+ player_recs = request.env['game.player.achievement'].sudo().search_read(
+ [('employee_id', '=', emp.id)],
+ ['template_id', 'progress', 'unlocked', 'unlock_date'])
+ progress_map = {
+ r['template_id'][0]: r for r in player_recs
+ }
+ items = []
+ total_points = 0
+ unlocked_count = 0
+ for t in templates:
+ pr = progress_map.get(t['id'])
+ unlocked = pr and pr['unlocked']
+ progress = pr['progress'] if pr else 0
+ if unlocked:
+ total_points += t['points']
+ unlocked_count += 1
+ items.append({
+ 'id': t['id'],
+ 'name': t['name'],
+ 'icon': t.get('icon') or '',
+ 'tier': t['tier'],
+ 'tier_label': tier_labels.get(t['tier'], t['tier']),
+ 'desc': t.get('desc') or '',
+ 'points': t['points'],
+ 'target': t['target'],
+ 'progress': progress,
+ 'unlocked': bool(unlocked),
+ 'unlock_date': str(pr['unlock_date']) if pr and pr['unlock_date'] else None,
+ })
+ return _json({
+ 'items': items,
+ 'total_points': total_points,
+ 'unlocked_count': unlocked_count,
+ 'total_count': len(items),
+ })
+
+ @http.route('/game/api/mails', type='http', auth='public',
+ methods=['GET'], csrf=False)
+ def mails(self, **kw):
+ """返回当前登录玩家的邮件列表。"""
+ uid = request.session.uid
+ if not uid:
+ return _json({'error': '未登录', 'items': []})
+ emp = request.env['hr.employee'].sudo().search(
+ [('user_id', '=', uid)], limit=1)
+ if not emp:
+ return _json({'error': '玩家不存在', 'items': []})
+ cat_labels = dict(
+ request.env['game.player.mail'].sudo()._fields['category'].selection)
+ recs = request.env['game.player.mail'].sudo().search_read(
+ [('employee_id', '=', emp.id)],
+ ['id', 'category', 'subject', 'body', 'date', 'is_read',
+ 'is_pinned', 'sender'],
+ order='is_pinned desc, date desc')
+ items = [{
+ 'id': r['id'],
+ 'category': r['category'],
+ 'category_label': cat_labels.get(r['category'], r['category']),
+ 'subject': r['subject'],
+ 'body': r.get('body') or '',
+ 'date': str(r['date']) if r['date'] else '',
+ 'is_read': r['is_read'],
+ 'is_pinned': r['is_pinned'],
+ 'sender': r.get('sender') or '',
+ } for r in recs]
+ unread = len([r for r in items if not r['is_read']])
+ return _json({'items': items, 'unread': unread})
+
+ @http.route('/game/api/mail/read', type='http', auth='public',
+ methods=['POST', 'OPTIONS'], csrf=False)
+ def mail_read(self, **kw):
+ """标记邮件为已读。"""
+ uid = request.session.uid
+ if not uid:
+ return _json({'ok': False, 'error': '未登录'})
+ data = _post_body()
+ mail_id = data.get('mail_id')
+ if not mail_id:
+ return _json({'ok': False, 'error': '缺少 mail_id'})
+ emp = request.env['hr.employee'].sudo().search(
+ [('user_id', '=', uid)], limit=1)
+ if not emp:
+ return _json({'ok': False, 'error': '玩家不存在'})
+ mail = request.env['game.player.mail'].sudo().browse(mail_id)
+ if not mail.exists() or mail.employee_id != emp:
+ return _json({'ok': False, 'error': '邮件不存在或无权操作'})
+ mail.write({'is_read': True})
+ return _json({'ok': True})
+
@http.route('/game/api/logout', type='http', auth='public',
methods=['POST', 'OPTIONS'], csrf=False)
def logout(self, **kw):
diff --git a/addons/game_base/data/game_achievement_data.xml b/addons/game_base/data/game_achievement_data.xml
new file mode 100644
index 0000000..b1f7683
--- /dev/null
+++ b/addons/game_base/data/game_achievement_data.xml
@@ -0,0 +1,95 @@
+
+
+
+
+
+
+ first_land
+ 10
+ 开天辟地
+ star
+ gold
+ 50
+ 1
+ 已点亮第一块领土,文明之火开始燃烧。
+
+
+
+ civilization_cradle
+ 20
+ 文明摇篮
+ sprout
+ silver
+ 30
+ 5
+ 建立 5 个聚落,文明初步成形。
+
+
+
+ sky_tower
+ 30
+ 通天塔
+ lock
+ platinum
+ 100
+ 10
+ 建造 10 座奇观,文明疆域遍布穹苍。
+
+
+
+ world_explorer
+ 40
+ 世界探索者
+ compass
+ bronze
+ 10
+ 10
+ 探索 10 块未知领土。
+
+
+
+ resource_master
+ 50
+ 资源大亨
+ gem
+ silver
+ 30
+ 1000
+ 累计获得 1000 金币。
+
+
+
+ spirit_awakening
+ 60
+ 灵气觉醒
+ sparkle
+ gold
+ 50
+ 1
+ 世界阶段跃迁至「灵启」。
+
+
+
+ mystic_realm
+ 70
+ 玄界之主
+ crown
+ legend
+ 200
+ 1
+ 世界阶段跃迁至「玄界」。
+
+
+
+ eternal
+ 80
+ 永恒之锚
+ infinity
+ legend
+ 500
+ 1
+ 世界阶段达到「溯元」,触及世界本源。
+
+
+
+
diff --git a/addons/game_base/models/__init__.py b/addons/game_base/models/__init__.py
index 634af88..1297619 100644
--- a/addons/game_base/models/__init__.py
+++ b/addons/game_base/models/__init__.py
@@ -6,3 +6,6 @@ from . import rule
from . import page
from . import story
from . import ir_http
+from . import game_employee
+from . import game_achievement
+from . import game_mail
diff --git a/addons/game_base/models/game_achievement.py b/addons/game_base/models/game_achievement.py
new file mode 100644
index 0000000..588d4f7
--- /dev/null
+++ b/addons/game_base/models/game_achievement.py
@@ -0,0 +1,85 @@
+# -*- coding: utf-8 -*-
+from odoo import api, fields, models
+
+
+class GameAchievementTemplate(models.Model):
+ """全局成就模板:后台定义,所有玩家共用同一套成就定义。"""
+ _name = 'game.achievement.template'
+ _description = '成就模板'
+ _order = 'sequence, id'
+ _rec_name = 'name'
+
+ TIER = [
+ ('bronze', '铜'),
+ ('silver', '银'),
+ ('gold', '金'),
+ ('platinum', '铂金'),
+ ('legend', '传奇'),
+ ]
+
+ sequence = fields.Integer('排序', default=10, index=True)
+ code = fields.Char('标识', index=True,
+ help='唯一标识,用于代码引用,如 first_land')
+ name = fields.Char('成就名称', required=True, translate=True)
+ icon = fields.Char('图标', help='emoji 或符号,如 star / sprout / lock')
+ tier = fields.Selection(TIER, '品质', default='bronze', index=True)
+ desc = fields.Text('描述', help='解锁条件/说明')
+ target = fields.Integer('目标值', default=1,
+ help='解锁所需进度值,如 10 表示需完成 10 次')
+ points = fields.Integer('成就点数', default=10)
+ active = fields.Boolean('启用', default=True)
+ player_achievement_ids = fields.One2many(
+ 'game.player.achievement', 'template_id', '玩家解锁记录')
+
+
+class GamePlayerAchievement(models.Model):
+ """玩家已解锁/进行中的成就,关联到 hr.employee。"""
+ _name = 'game.player.achievement'
+ _description = '玩家成就'
+ _order = 'template_id, id'
+ _rec_name = 'name'
+
+ template_id = fields.Many2one(
+ 'game.achievement.template', '成就模板',
+ required=True, ondelete='cascade', index=True)
+ employee_id = fields.Many2one(
+ 'hr.employee', '所属玩家',
+ required=True, ondelete='cascade', index=True)
+ name = fields.Char('名称', related='template_id.name', store=True)
+ icon = fields.Char('图标', related='template_id.icon')
+ tier = fields.Selection(
+ related='template_id.tier', store=True)
+ desc = fields.Text('描述', related='template_id.desc')
+ points = fields.Integer('点数', related='template_id.points')
+ progress = fields.Integer('进度', default=0)
+ target = fields.Integer('目标值', related='template_id.target')
+ unlocked = fields.Boolean('已解锁', default=False)
+ unlock_date = fields.Datetime('解锁时间')
+
+ @api.model
+ def _update_progress(self, employee_id, template_key, progress):
+ """更新某玩家某成就的进度,达标自动解锁。"""
+ tmpl = self.env['game.achievement.template'].search(
+ [('id', '=', template_key)], limit=1)
+ if not tmpl:
+ return False
+ rec = self.search([
+ ('employee_id', '=', employee_id),
+ ('template_id', '=', tmpl.id),
+ ], limit=1)
+ if not rec:
+ rec = self.create({
+ 'employee_id': employee_id,
+ 'template_id': tmpl.id,
+ 'progress': min(progress, tmpl.target),
+ 'unlocked': progress >= tmpl.target,
+ 'unlock_date': fields.Datetime.now() if progress >= tmpl.target else False,
+ })
+ elif not rec.unlocked:
+ rec.write({
+ 'progress': min(progress, tmpl.target),
+ 'unlocked': progress >= tmpl.target,
+ })
+ if rec.unlocked:
+ rec.unlock_date = fields.Datetime.now()
+ return True
diff --git a/addons/game_base/models/game_employee.py b/addons/game_base/models/game_employee.py
new file mode 100644
index 0000000..f2c1961
--- /dev/null
+++ b/addons/game_base/models/game_employee.py
@@ -0,0 +1,74 @@
+# -*- coding: utf-8 -*-
+from odoo import api, fields, models
+
+
+class GameEmployee(models.Model):
+ """在 hr.employee 上扩展游戏数据字段。"""
+ _inherit = 'hr.employee'
+
+ # === 等级系统 ===
+ game_level = fields.Integer('游戏等级', default=1)
+ game_exp = fields.Integer('经验值', default=0)
+ game_exp_max = fields.Integer('升级经验', default=100)
+ game_title = fields.Char('称号', default='萌新')
+
+ # === 资源系统 ===
+ game_coins = fields.Integer('金币', default=0)
+ game_energy = fields.Integer('灵气', default=0)
+ game_wood = fields.Integer('木材', default=0)
+ game_stone = fields.Integer('石料', default=0)
+
+ # === 世界数据 ===
+ game_world_name = fields.Char('世界名')
+ game_era = fields.Char('历元', default='星元1年·春')
+ game_tiles = fields.Integer('疆域地块', default=0)
+ game_population = fields.Integer('人口', default=0)
+ game_civ_level = fields.Integer('文明度', default=0)
+
+ # === 身份 ===
+ game_uid = fields.Char('游戏UID', index=True)
+ game_reg_date = fields.Date('注册日期', default=fields.Date.today)
+
+ # === 关联 ===
+ achievement_ids = fields.One2many('game.player.achievement', 'employee_id', '成就')
+ mail_ids = fields.One2many('game.player.mail', 'employee_id', '邮件')
+ game_achievement_count = fields.Integer(
+ '已解锁成就数', compute='_compute_achievement_count')
+ game_unread_mail = fields.Integer(
+ '未读邮件数', compute='_compute_unread_mail')
+
+ @api.depends('achievement_ids.unlocked')
+ def _compute_achievement_count(self):
+ for rec in self:
+ rec.game_achievement_count = len(
+ rec.achievement_ids.filtered('unlocked'))
+
+ @api.depends('mail_ids.is_read')
+ def _compute_unread_mail(self):
+ for rec in self:
+ rec.game_unread_mail = len(
+ rec.mail_ids.filtered(lambda m: not m.is_read))
+
+ def export_game_data(self):
+ """将游戏数据导出为前端可用的字典。"""
+ self.ensure_one()
+ return {
+ 'emp_id': self.id,
+ 'level': self.game_level,
+ 'exp': self.game_exp,
+ 'exp_max': self.game_exp_max,
+ 'title': self.game_title,
+ 'coins': self.game_coins,
+ 'energy': self.game_energy,
+ 'wood': self.game_wood,
+ 'stone': self.game_stone,
+ 'world_name': self.game_world_name or '',
+ 'era': self.game_era or '',
+ 'tiles': self.game_tiles,
+ 'population': self.game_population,
+ 'civ_level': self.game_civ_level,
+ 'game_uid': self.game_uid or '',
+ 'reg_date': str(self.game_reg_date) if self.game_reg_date else None,
+ 'achievement_count': self.game_achievement_count,
+ 'unread_mail': self.game_unread_mail,
+ }
diff --git a/addons/game_base/models/game_mail.py b/addons/game_base/models/game_mail.py
new file mode 100644
index 0000000..dbea734
--- /dev/null
+++ b/addons/game_base/models/game_mail.py
@@ -0,0 +1,28 @@
+# -*- coding: utf-8 -*-
+from odoo import api, fields, models
+
+
+class GamePlayerMail(models.Model):
+ """玩家邮件:后台可发送系统邮件给指定玩家(员工)。"""
+ _name = 'game.player.mail'
+ _description = '玩家邮件'
+ _order = 'date desc, id desc'
+ _rec_name = 'subject'
+
+ CATEGORY = [
+ ('system', '系统'),
+ ('activity', '活动'),
+ ('reward', '奖励'),
+ ('announcement', '公告'),
+ ]
+
+ employee_id = fields.Many2one(
+ 'hr.employee', '收件人',
+ required=True, ondelete='cascade', index=True)
+ category = fields.Selection(CATEGORY, '分类', default='system', index=True)
+ subject = fields.Char('标题', required=True)
+ body = fields.Html('正文')
+ date = fields.Datetime('发送时间', default=fields.Datetime.now)
+ is_read = fields.Boolean('已读', default=False)
+ is_pinned = fields.Boolean('置顶', default=False)
+ sender = fields.Char('发件人', default='系统')
diff --git a/addons/game_base/security/ir.model.access.csv b/addons/game_base/security/ir.model.access.csv
index d0a25cc..37155c0 100644
--- a/addons/game_base/security/ir.model.access.csv
+++ b/addons/game_base/security/ir.model.access.csv
@@ -19,3 +19,9 @@ access_game_story_volume,game.story.volume public,model_game_story_volume,base.g
access_game_story_volume_user,game.story.volume user,model_game_story_volume,base.group_user,1,1,1,1
access_game_story_chapter,game.story.chapter public,model_game_story_chapter,base.group_public,1,0,0,0
access_game_story_chapter_user,game.story.chapter user,model_game_story_chapter,base.group_user,1,1,1,1
+access_game_achievement_template,game.achievement.template public,model_game_achievement_template,base.group_public,1,0,0,0
+access_game_achievement_template_user,game.achievement.template user,model_game_achievement_template,base.group_user,1,1,1,1
+access_game_player_achievement,game.player.achievement public,model_game_player_achievement,base.group_public,1,0,0,0
+access_game_player_achievement_user,game.player.achievement user,model_game_player_achievement,base.group_user,1,1,1,1
+access_game_player_mail,game.player.mail public,model_game_player_mail,base.group_public,1,0,0,0
+access_game_player_mail_user,game.player.mail user,model_game_player_mail,base.group_user,1,1,1,1
diff --git a/addons/game_base/views/forum_views.xml b/addons/game_base/views/forum_views.xml
index 080c6a5..437a770 100644
--- a/addons/game_base/views/forum_views.xml
+++ b/addons/game_base/views/forum_views.xml
@@ -28,7 +28,7 @@
-
+
diff --git a/addons/game_base/views/game_achievement_views.xml b/addons/game_base/views/game_achievement_views.xml
new file mode 100644
index 0000000..8b392c0
--- /dev/null
+++ b/addons/game_base/views/game_achievement_views.xml
@@ -0,0 +1,81 @@
+
+
+
+
+
+ game.achievement.template.tree
+ game.achievement.template
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ game.achievement.template.form
+ game.achievement.template
+
+
+
+
+
+
+
+ 成就模板
+ game.achievement.template
+ tree,form
+
+
+
+
+ game.player.achievement.tree
+ game.player.achievement
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 玩家成就
+ game.player.achievement
+ tree,form
+
+
+
diff --git a/addons/game_base/views/game_employee_views.xml b/addons/game_base/views/game_employee_views.xml
new file mode 100644
index 0000000..bca1b93
--- /dev/null
+++ b/addons/game_base/views/game_employee_views.xml
@@ -0,0 +1,60 @@
+
+
+
+
+
+ hr.employee.game.form
+ hr.employee
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ hr.employee.game.tree
+ hr.employee
+
+
+
+
+
+
+
+
+
+
+
diff --git a/addons/game_base/views/game_mail_views.xml b/addons/game_base/views/game_mail_views.xml
new file mode 100644
index 0000000..c634379
--- /dev/null
+++ b/addons/game_base/views/game_mail_views.xml
@@ -0,0 +1,56 @@
+
+
+
+
+
+ game.player.mail.tree
+ game.player.mail
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ game.player.mail.form
+ game.player.mail
+
+
+
+
+
+
+
+ 玩家邮件
+ game.player.mail
+ tree,form
+
+
+
diff --git a/addons/game_base/views/menu.xml b/addons/game_base/views/menu.xml
index daa2008..305054f 100644
--- a/addons/game_base/views/menu.xml
+++ b/addons/game_base/views/menu.xml
@@ -26,6 +26,11 @@
+
+
+
+
+
@@ -37,5 +42,6 @@
+
diff --git a/assets/game-api.js b/assets/game-api.js
index 2d4c25f..f87e5df 100644
--- a/assets/game-api.js
+++ b/assets/game-api.js
@@ -59,6 +59,19 @@ const API = {
method: 'POST', credentials: _cred,
})).json();
},
+ async achievements() {
+ return (await fetch(_url('/game/api/achievements'), { credentials: _cred })).json();
+ },
+ async mails() {
+ return (await fetch(_url('/game/api/mails'), { credentials: _cred })).json();
+ },
+ async mailRead(mailId) {
+ return (await fetch(_url('/game/api/mail/read'), {
+ method: 'POST', credentials: _cred,
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ mail_id: mailId }),
+ })).json();
+ },
async codex() {
return (await fetch(_url('/game/api/codex'), { credentials: _cred })).json();
},
@@ -136,4 +149,6 @@ const API = {
async codexRaces() {
return (await fetch(_worldUrl('/yt_world/api/codex/races'), { credentials: _wcred })).json();
},
+ // 暴露后端 base,供图片等静态资源 URL 拼接(本地跨域→8069;线上同域→'')
+ base: API_CONFIG.base,
};
diff --git a/assets/site-app.js b/assets/site-app.js
index c50403e..b8e13af 100644
--- a/assets/site-app.js
+++ b/assets/site-app.js
@@ -53,6 +53,102 @@ async function loadRules(){
} catch (e) { /* 保留静态兜底内容 */ }
}
+// ---------- 图鉴 ----------
+async function loadCodex(){
+ const grid = document.getElementById('codexGrid');
+ const catsEl = document.getElementById('codexCats');
+ const subsEl = document.getElementById('codexSubs');
+ const searchEl= document.getElementById('codexSearch');
+ const countEl = document.getElementById('codexCount');
+ if (!grid) return;
+
+ let data;
+ try { data = await API.codex(); }
+ catch (e) { return; } // 失败时保留静态兜底内容
+
+ const groups = (data && data.groups) || [];
+ if (!groups.length){
+ grid.innerHTML = '暂无图鉴数据
';
+ return;
+ }
+
+ // 选中状态
+ let curGroup = groups[0].key;
+ let curSub = 'all';
+ let curQ = '';
+
+ function renderCats(){
+ if (!catsEl) return;
+ catsEl.innerHTML = groups.map(g =>
+ ``
+ ).join('');
+ catsEl.querySelectorAll('button').forEach(b => b.addEventListener('click', () => {
+ curGroup = b.dataset.gk; curSub = 'all'; curQ = '';
+ if (searchEl) searchEl.value = '';
+ renderCats(); renderSubs(); renderGrid();
+ }));
+ }
+
+ function renderSubs(){
+ if (!subsEl) return;
+ const g = groups.find(x => x.key === curGroup);
+ const subs = (g && g.subs) || [];
+ subsEl.innerHTML = `` +
+ subs.map(s => ``).join('');
+ subsEl.querySelectorAll('button').forEach(b => b.addEventListener('click', () => {
+ curSub = b.dataset.sn; renderSubs(); renderGrid();
+ }));
+ }
+
+ function renderGrid(){
+ const g = groups.find(x => x.key === curGroup);
+ if (!g){ grid.innerHTML = ''; return; }
+ // 按子分类筛选
+ let items = [];
+ (g.subs || []).forEach(s => {
+ (s.items || []).forEach(it => {
+ if (curSub !== 'all' && it.sub !== curSub) return;
+ items.push(it);
+ });
+ });
+ // 搜索过滤
+ if (curQ){
+ const q = curQ.toLowerCase();
+ items = items.filter(it =>
+ (it.name || '').toLowerCase().includes(q) ||
+ (it.desc || '').replace(/<[^>]+>/g, '').toLowerCase().includes(q)
+ );
+ }
+ if (countEl) countEl.textContent = items.length + ' 项';
+ if (!items.length){
+ grid.innerHTML = '未找到匹配项
';
+ return;
+ }
+ const base = API.base || '';
+ grid.innerHTML = items.map(it => {
+ const thumb = it.image
+ ? ``
+ : ``;
+ return `
+ ${thumb}
+
+
${it.name || ''}
+
${it.desc || ''}
+
${it.sub || ''}${it.tier || ''}
+
+
`;
+ }).join('');
+ }
+
+ if (searchEl){
+ searchEl.addEventListener('input', () => { curQ = searchEl.value.trim(); renderGrid(); });
+ }
+
+ renderCats();
+ renderSubs();
+ renderGrid();
+}
+
// ---------- 论坛 ----------
async function loadForum(){
const threadsEl = document.getElementById('threads');
@@ -155,6 +251,7 @@ async function loadStory(){
document.addEventListener('DOMContentLoaded', () => {
loadPages();
loadRules();
+ loadCodex();
loadForum();
loadAnnounce();
loadStory();
diff --git a/assets/slides/01_starry_sky.png b/assets/slides/01_starry_sky.png
new file mode 100644
index 0000000..ae6faaf
Binary files /dev/null and b/assets/slides/01_starry_sky.png differ
diff --git a/assets/slides/02_sky.png b/assets/slides/02_sky.png
new file mode 100644
index 0000000..3261a34
Binary files /dev/null and b/assets/slides/02_sky.png differ
diff --git a/assets/slides/03_earth.png b/assets/slides/03_earth.png
new file mode 100644
index 0000000..1b35426
Binary files /dev/null and b/assets/slides/03_earth.png differ
diff --git a/assets/slides/04_underground.png b/assets/slides/04_underground.png
new file mode 100644
index 0000000..d936a4e
Binary files /dev/null and b/assets/slides/04_underground.png differ
diff --git a/assets/slides/05_core.png b/assets/slides/05_core.png
new file mode 100644
index 0000000..52f7f0f
Binary files /dev/null and b/assets/slides/05_core.png differ
diff --git a/assets/slides/06_starry_sky_b.png b/assets/slides/06_starry_sky_b.png
new file mode 100644
index 0000000..71141a8
Binary files /dev/null and b/assets/slides/06_starry_sky_b.png differ
diff --git a/assets/slides/07_sky_b.png b/assets/slides/07_sky_b.png
new file mode 100644
index 0000000..1fd4e54
Binary files /dev/null and b/assets/slides/07_sky_b.png differ
diff --git a/assets/slides/08_earth_b.png b/assets/slides/08_earth_b.png
new file mode 100644
index 0000000..e020ee7
Binary files /dev/null and b/assets/slides/08_earth_b.png differ
diff --git a/assets/slides/09_underground_b.png b/assets/slides/09_underground_b.png
new file mode 100644
index 0000000..6ab21a6
Binary files /dev/null and b/assets/slides/09_underground_b.png differ
diff --git a/assets/slides/10_core_b.png b/assets/slides/10_core_b.png
new file mode 100644
index 0000000..c33df9c
Binary files /dev/null and b/assets/slides/10_core_b.png differ
diff --git a/assets/slides/login_bg.png b/assets/slides/login_bg.png
new file mode 100644
index 0000000..4a8be66
Binary files /dev/null and b/assets/slides/login_bg.png differ
diff --git a/index.html b/index.html
index 920e968..2e5cd96 100644
--- a/index.html
+++ b/index.html
@@ -15,6 +15,65 @@
/* 首页特有:全屏 hero 轮播 */
html,body{overflow:hidden;}
.content{flex:1;display:flex;flex-direction:column;position:relative;overflow:hidden;}
+ :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,.045); --card-bd:rgba(255,255,255,.10);
+ --glow:0 0 22px rgba(139,92,246,.28);
+ }
+ *{box-sizing:border-box;margin:0;padding:0;}
+ html,body{height:100%;width:100%;overflow:hidden;}
+ 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,.14), transparent 60%),
+ radial-gradient(820px 560px at 8% 112%, rgba(76,201,240,.10), transparent 55%),
+ linear-gradient(160deg, var(--bg-0) 0%, var(--bg-1) 48%, var(--bg-2) 100%);
+ 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:fl1 20s ease-in-out infinite;}
+ body::after{width:340px;height:340px;background:rgba(76,201,240,.12);bottom:-110px;left:-60px;animation:fl2 24s ease-in-out infinite;}
+ @keyframes fl1{0%,100%{transform:translate(0,0)}50%{transform:translate(-36px,36px)}}
+ @keyframes fl2{0%,100%{transform:translate(0,0)}50%{transform:translate(36px,-28px)}}
+
+ .app{position:relative;z-index:2;height:100vh;display:flex;flex-direction:column;}
+ .topbar{height:64px;flex:0 0 auto;display:flex;align-items:center;gap:22px;padding:0 30px;border-bottom:1px solid var(--card-bd);background:rgba(8,12,26,.55);backdrop-filter:blur(10px);}
+ .logo{display:flex;align-items:baseline;gap:9px;cursor:pointer;}
+ .logo .cn{font-family:"ZCOOL QingKe HuangYou",sans-serif;font-size:28px;letter-spacing:4px;color:#fff;text-shadow:0 0 16px rgba(139,92,246,.55);}
+ .logo .en{font-family:"Press Start 2P",monospace;font-size:9px;color:var(--magenta);letter-spacing:1px;}
+ .nav{display:flex;gap:6px;margin-left:12px;}
+ .nav button{font-family:"Noto Sans SC",sans-serif;font-size:15px;font-weight:500;color:var(--text-dim);background:transparent;border:1px solid transparent;padding:8px 15px;border-radius:10px;cursor:pointer;transition:.22s;}
+ .nav button:hover{color:var(--text);background:rgba(139,92,246,.12);}
+ .nav button.active{color:#fff;background:rgba(139,92,246,.22);border-color:var(--card-bd);box-shadow:var(--glow);}
+ .spacer{flex:1;}
+ .user-box{display:flex;align-items:center;gap:10px;font-size:14px;color:var(--text);}
+ .user-box .ub-name{color:#fff;font-weight:500;}
+ .user-box .ub-logout{font-family:inherit;font-size:12px;color:var(--text-dim);background:rgba(255,255,255,.06);border:1px solid var(--card-bd);padding:5px 12px;border-radius:8px;cursor:pointer;transition:.2s;}
+ .user-box .ub-logout:hover{color:#fff;background:rgba(139,92,246,.18);}
+ .user-box .auth-link{color:var(--text-dim);text-decoration:none;font-size:14px;transition:.2s;padding:5px 8px;}
+ .user-box .auth-link:hover{color:#fff;}
+ .user-box .auth-btn{font-family:inherit;font-size:13px;color:#0b1020;background:linear-gradient(120deg,var(--gold),#f0d49a);border:none;padding:6px 16px;border-radius:20px;cursor:pointer;font-weight:600;transition:.2s;}
+ .user-box .auth-btn:hover{filter:brightness(1.1);box-shadow:0 0 16px rgba(231,184,92,.4);}
+ .nav .auth-only{display:none;} /* 登录专属 tab,未登录时隐藏 */
+ .nav .auth-only.visible{display:inline-block;}
+ .cta:hover{filter:brightness(1.08);}
+
+ .content{flex:1;position:relative;overflow:hidden;}
+ .panel{position:absolute;inset:0;display:none;flex-direction:column;padding:28px 48px;overflow:hidden;}
+ .panel.active{display:flex;}
+ .panel-scroll{flex:1;min-height:0;overflow-y:auto;padding-right:8px;}
+ /* 隐藏内部滚动条但保留滚轮/触控滑动(规则/世界数据/公告/论坛/图鉴/剧情等) */
+ .rules-scroll,#worldDataContent,#announce .notes,.threads,.codex-grid,.story-reader,.panel-scroll,.toc-list{
+ scrollbar-width:none; -ms-overflow-style:none;
+ }
+ .rules-scroll::-webkit-scrollbar,#worldDataContent::-webkit-scrollbar,#announce .notes::-webkit-scrollbar,
+ .threads::-webkit-scrollbar,.codex-grid::-webkit-scrollbar,.story-reader::-webkit-scrollbar,
+ .panel-scroll::-webkit-scrollbar,.toc-list::-webkit-scrollbar{display:none;width:0;height:0;}
+
+ /* ===== 介绍(图片轮播主页) ===== */
+ #intro{padding:0;}
.hero{position:relative;flex:1;border-radius:0;overflow:hidden;}
.hero .slideshow{position:absolute;inset:0;overflow:hidden;}
.hero .slide-track{position:absolute;inset:0;display:flex;flex-direction:column;transition:transform 1.25s cubic-bezier(0.4,0,0.2,1);will-change:transform;}
@@ -28,6 +87,7 @@
.hero .copy .tag{font-size:20px;font-weight:500;color:#e8ecf6;margin-top:4px;text-shadow:0 1px 10px rgba(0,0,0,.85);}
/* 轮播标签 */
+ /* 轮播左下角特色文案 */
.slide-label{position:absolute;left:56px;bottom:72px;z-index:4;pointer-events:none;opacity:0;transform:translateY(16px);transition:opacity .8s ease, transform .8s ease;}
.slide.active .slide-label{opacity:1;transform:translateY(0);}
.slide-label .sl-en{font-family:"Press Start 2P",monospace;font-size:13px;color:var(--cyan);letter-spacing:3px;margin-bottom:10px;display:block;text-shadow:0 1px 8px rgba(0,0,0,.9);}
@@ -35,6 +95,7 @@
.slide-label .sl-desc{font-size:16px;color:#c8d2ec;margin-top:12px;max-width:500px;line-height:1.85;text-shadow:0 1px 8px rgba(0,0,0,.88);}
/* 指示器 */
+ /* 轮播指示器(竖排右侧) */
.sl-dots{position:absolute;right:40px;bottom:96px;top:auto;transform:none;z-index:5;display:flex;flex-direction:column;gap:14px;pointer-events:auto;}
.sl-dot{width:12px;height:12px;border-radius:50%;background:rgba(255,255,255,.22);border:1px solid rgba(255,255,255,.4);cursor:pointer;transition:.3s;flex:0 0 auto;}
.sl-dot:hover{background:rgba(255,255,255,.5);}
@@ -47,6 +108,7 @@
.sl-arrow.next{right:16px;}
/* 起源文案 */
+ /* 轮播右上角竖排「起源历史」 */
.slide-origin{position:absolute;right:72px;top:48px;z-index:4;pointer-events:none;writing-mode:vertical-rl;text-orientation:upright;
font-family:"Noto Sans SC",sans-serif;font-size:16px;font-weight:500;letter-spacing:3px;line-height:1.55;color:rgba(240,244,252,.94);
text-shadow:0 1px 3px rgba(0,0,0,.92);max-height:28vh;overflow:hidden;text-rendering:optimizeLegibility;-webkit-font-smoothing:antialiased;
@@ -59,11 +121,244 @@
.hero .enter-btn{font-family:"Noto Sans SC",sans-serif;font-size:17px;font-weight:700;color:#0b1020;background:linear-gradient(120deg,var(--gold),#f0d49a);padding:13px 42px;border-radius:30px;cursor:pointer;box-shadow:0 0 26px rgba(231,184,92,.5);letter-spacing:3px;transition:.25s;border:none;}
.hero .enter-btn:hover{filter:brightness(1.1);transform:translateY(-2px);box-shadow:0 0 40px rgba(231,184,92,.75);}
+ /* ===== 特色 ===== */
+ #features{align-items:center;justify-content:center;gap:22px;}
+ .grid{display:grid;grid-template-columns:repeat(3,1fr);gap:16px;width:min(1060px,100%);}
+ .card{background:var(--card);border:1px solid var(--card-bd);border-radius:14px;padding:18px 20px;transition:.26s;min-height:132px;}
+ .card:hover{transform:translateY(-4px);border-color:var(--violet);box-shadow:var(--glow);background:rgba(139,92,246,.12);}
+ .card .ic{font-size:28px;margin-bottom:8px;filter:drop-shadow(0 0 6px rgba(199,125,255,.6));}
+ .card h3{font-size:17px;color:#fff;margin-bottom:7px;letter-spacing:1px;}
+ .card p{font-size:13px;color:var(--text-dim);line-height:1.6;}
+
+ /* ===== 背景 ===== */
+ #background{flex-direction:column;align-items:stretch;gap:0;overflow:hidden;padding:28px 48px;}
+ .bg-page{max-width:1080px;width:100%;margin:0 auto;padding:24px 0;border-top:1px solid var(--card-bd);}
+ .bg-page:first-child{border-top:none;padding-top:0;}
+ .bg-page-head{text-align:center;margin-bottom:28px;}
+ .bg-page-title{font-family:"Noto Serif SC",serif;font-weight:800;font-size:40px;letter-spacing:3px;line-height:1.2;margin:0 0 10px;
+ background:linear-gradient(120deg,#ffffff 0%,#f0d49a 48%,#c79bff 100%);
+ -webkit-background-clip:text;background-clip:text;-webkit-text-fill-color:transparent;}
+ .bg-page-sub{font-size:13px;letter-spacing:7px;color:var(--text-dim);text-transform:uppercase;}
+ .bg-page-body{font-size:17px;line-height:2.05;color:#e2e8f6;max-width:840px;margin:0 auto 34px;}
+ .bg-page-body p{margin:0 0 18px;}
+ .bg-page-items{display:grid;grid-template-columns:repeat(auto-fill,minmax(262px,1fr));gap:18px;}
+ .bg-card{display:flex;gap:15px;align-items:flex-start;background:linear-gradient(135deg,rgba(139,92,246,.10),rgba(76,201,240,.05));
+ border:1px solid var(--card-bd);border-radius:16px;padding:18px 20px;transition:.26s;}
+ .bg-card:hover{transform:translateY(-4px);border-color:var(--violet);box-shadow:var(--glow);background:rgba(139,92,246,.16);}
+ .bg-card-ic{font-size:27px;line-height:1.15;filter:drop-shadow(0 0 9px rgba(139,92,246,.55));flex:0 0 auto;}
+ .bg-card-title{font-size:17px;font-weight:700;color:#fff;margin-bottom:6px;}
+ .bg-card-desc{font-size:14.5px;line-height:1.66;color:var(--text-dim);}
+ .bg-empty{text-align:center;color:var(--text-dim);padding:70px 0;font-size:16px;}
+ .layers{flex:0 0 270px;display:flex;flex-direction:column;gap:8px;justify-content:center;}
+ .layer{border-radius:12px;padding:11px 15px;border:1px solid var(--card-bd);position:relative;}
+ .layer .lt{font-size:15px;color:#fff;font-weight:500;letter-spacing:1px;}
+ .layer .ld{font-size:12px;color:var(--text-dim);margin-top:4px;line-height:1.5;}
+ .layer.l1{background:linear-gradient(100deg,rgba(76,201,240,.18),rgba(139,92,246,.08));}
+ .layer.l2{background:linear-gradient(100deg,rgba(199,125,255,.18),rgba(139,92,246,.07));}
+ .layer.l3{background:linear-gradient(100deg,rgba(139,92,246,.24),rgba(76,201,240,.06));border-color:var(--violet);}
+ .layer.l4{background:linear-gradient(100deg,rgba(139,92,246,.14),rgba(8,12,26,.18));}
+ .layer.l5{background:linear-gradient(100deg,rgba(231,184,92,.14),rgba(8,12,26,.26));}
+ .layer .star{position:absolute;right:12px;top:9px;font-size:11px;color:var(--cyan);}
+ .bg-text{flex:1;display:flex;flex-direction:column;justify-content:center;overflow-y:auto;}
+ .bg-text h2{font-family:"ZCOOL QingKe HuangYou",sans-serif;font-size:30px;letter-spacing:3px;color:#fff;text-shadow:0 0 16px rgba(139,92,246,.5);margin-bottom:16px;}
+ .bg-text p{font-size:14.5px;color:var(--text-dim);line-height:1.95;max-width:640px;}
+
+ /* ===== 规则 ===== */
+ #rules{flex-direction:column;gap:0;}
+ .rules-scroll{flex:1;min-height:0;overflow-y:auto;display:flex;flex-direction:column;gap:22px;padding-right:8px;}
+ .rule-block h3{font-family:"ZCOOL QingKe HuangYou",sans-serif;font-size:22px;color:#fff;letter-spacing:2px;margin-bottom:6px;}
+ .rule-intro{font-size:13.5px;color:var(--text-dim);margin-bottom:12px;line-height:1.7;max-width:780px;}
+ .tier-list{display:flex;flex-direction:column;gap:8px;}
+ .tier-row{display:grid;grid-template-columns:96px 116px 1fr;gap:12px;align-items:center;background:var(--card);border:1px solid var(--card-bd);border-radius:10px;padding:10px 14px;transition:.2s;}
+ .tier-row:hover{border-color:var(--violet);background:rgba(139,92,246,.10);}
+ .tier-row .tn{font-family:"Press Start 2P",monospace;font-size:10px;color:var(--cyan);}
+ .tier-row .tname{font-size:15px;color:#fff;font-weight:500;}
+ .tier-row .tdesc{font-size:13px;color:var(--text-dim);line-height:1.5;}
+
+ /* ===== 图鉴 ===== */
+ #codex{padding:28px 48px;gap:0;}
+ .codex-wrap{display:flex;flex-direction:column;gap:13px;flex:1;min-height:0;}
+ .codex-cats{display:flex;gap:6px;flex-wrap:wrap;}
+ .codex-cats button{font-family:"Noto Sans SC",sans-serif;font-size:14px;color:var(--text-dim);background:var(--card);border:1px solid var(--card-bd);padding:8px 15px;border-radius:11px;cursor:pointer;transition:.2s;}
+ .codex-cats button:hover{color:#fff;}
+ .codex-cats button.active{color:#fff;background:rgba(139,92,246,.24);border-color:var(--magenta);box-shadow:var(--glow);}
+ .codex-subrow{display:flex;align-items:center;gap:12px;}
+ .codex-subs{display:flex;gap:6px;flex-wrap:wrap;flex:1;}
+ .codex-subs button{font-size:12px;color:var(--text-dim);background:var(--card);border:1px solid var(--card-bd);padding:5px 12px;border-radius:14px;cursor:pointer;transition:.2s;}
+ .codex-subs button:hover{color:#fff;}
+ .codex-subs button.active{color:#fff;background:rgba(76,201,240,.22);border-color:var(--cyan);}
+ .codex-search{background:rgba(8,12,26,.6);border:1px solid var(--card-bd);border-radius:18px;color:var(--text);padding:7px 14px;font-size:13px;outline:none;width:200px;flex:0 0 auto;}
+ .codex-search:focus{border-color:var(--violet);}
+ .codex-count{font-size:12px;color:var(--text-dim);white-space:nowrap;flex:0 0 auto;}
+ .codex-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(300px,1fr));gap:16px;overflow-y:auto;padding:4px 6px 4px 0;align-content:start;flex:1;min-height:0;}
+ .cx{background:var(--card);border:1px solid var(--card-bd);border-radius:13px;overflow:hidden;transition:.24s;display:flex;flex-direction:column;cursor:default;}
+ .cx:hover{transform:translateY(-3px);border-color:var(--violet);box-shadow:var(--glow);}
+ .cx .thumb{width:100%;aspect-ratio:1/1;display:block;overflow:hidden;background:linear-gradient(135deg,rgba(139,92,246,.22),rgba(76,201,240,.10));}
+ .cx .thumb svg{width:100%;height:100%;display:block;}
+ .cx .body{padding:10px 12px;}
+ .cx .nm{font-size:14.5px;color:#fff;font-weight:500;}
+ .cx .meta{display:flex;justify-content:space-between;margin-top:5px;font-size:11px;}
+ .cx .meta .sub{color:var(--cyan);}
+ .cx .meta .tier{color:var(--gold);}
+ .cx .desc{font-size:12px;color:var(--text-dim);line-height:1.5;margin-top:4px;max-height:60px;overflow:hidden;}
+ .cx .ds{font-size:11.5px;color:var(--text-dim);line-height:1.5;margin-top:6px;}
+
+ /* ===== 论坛 ===== */
+ #forum{flex-direction:row;gap:28px;}
+ .f-cats{flex:0 0 200px;display:flex;flex-direction:column;gap:12px;}
+ .f-cats .ch{font-size:15.5px;color:var(--text-dim);background:var(--card);border:1px solid var(--card-bd);padding:13px 18px;border-radius:12px;cursor:pointer;transition:.22s;letter-spacing:.5px;}
+ .f-cats .ch:hover{color:#fff;background:rgba(139,92,246,.12);}
+ .f-cats .ch.active{color:#fff;background:linear-gradient(120deg,rgba(139,92,246,.28),rgba(199,125,255,.16));border-color:var(--magenta);box-shadow:var(--glow);}
+ .f-main{flex:1;display:flex;flex-direction:column;min-width:0;}
+ .threads{flex:1;overflow-y:auto;display:grid;grid-template-columns:repeat(auto-fill,minmax(340px,1fr));gap:16px;padding-right:8px;align-content:start;}
+ .th{display:flex;align-items:center;gap:16px;background:var(--card);border:1px solid var(--card-bd);border-radius:14px;padding:16px 20px;transition:.22s;cursor:pointer;}
+ .th:hover{border-color:var(--violet);background:rgba(139,92,246,.12);transform:translateY(-2px);box-shadow:0 8px 24px rgba(0,0,0,.25);}
+ .th .pin{font-size:12px;font-weight:700;letter-spacing:1px;color:var(--gold);background:rgba(231,184,92,.16);border:1px solid rgba(231,184,92,.45);border-radius:6px;padding:2px 8px;}
+ .th .tt{flex:1;font-size:16.5px;color:#fff;letter-spacing:.3px;}
+ .th .meta{font-size:13px;color:var(--text-dim);white-space:nowrap;}
+ .th .rp{color:var(--cyan);font-weight:600;}
+ .post-form{display:none;flex-direction:column;gap:10px;background:var(--card);border:1px solid var(--magenta);border-radius:14px;padding:16px;margin-bottom:14px;}
+ .post-form.show{display:flex;}
+ .post-form input,.post-form textarea{background:rgba(8,12,26,.6);border:1px solid var(--card-bd);border-radius:10px;color:var(--text);padding:10px 14px;font-family:inherit;font-size:14px;outline:none;}
+ .post-form textarea{resize:vertical;min-height:72px;}
+ .post-form .row{display:flex;gap:10px;justify-content:flex-end;}
+ .post-form .row button{font-size:14px;padding:8px 18px;border-radius:10px;cursor:pointer;border:1px solid var(--card-bd);background:var(--card);color:var(--text);}
+ .post-form .row .ok{background:linear-gradient(120deg,var(--gold),#f0d49a);color:#0b1020;font-weight:700;border:none;}
+
+ /* ===== 公告 ===== */
+ #announce{gap:0;align-items:stretch;}
+ #announce .notes{flex:1;min-height:0;width:100%;display:grid;grid-template-columns:repeat(auto-fill,minmax(360px,1fr));gap:18px;overflow-y:auto;padding-right:8px;align-content:start;}
+ #announce .notes{grid-template-columns:repeat(auto-fill,minmax(380px,1fr));gap:20px;}
+ .note{display:flex;flex-direction:column;overflow:hidden;background:linear-gradient(135deg,rgba(139,92,246,.10),rgba(76,201,240,.04));border:1px solid var(--card-bd);border-left:4px solid var(--magenta);border-radius:16px;transition:.24s;}
+ .note:hover{transform:translateY(-3px);border-color:var(--violet);background:linear-gradient(135deg,rgba(139,92,246,.16),rgba(76,201,240,.07));box-shadow:var(--glow);}
+ .note.pin{border-left-color:var(--gold);background:linear-gradient(135deg,rgba(231,184,92,.12),rgba(139,92,246,.06));}
+ .note.pin:hover{background:linear-gradient(135deg,rgba(231,184,92,.18),rgba(139,92,246,.08));}
+ .note.feat{grid-column:1 / -1;flex-direction:row;align-items:stretch;}
+ .note.feat .note-cover{flex:0 0 44%;width:44%;min-height:260px;}
+ .note.feat .note-body{flex:1;justify-content:center;}
+ .note.feat .note-title{font-size:27px;}
+ .note.feat .body{font-size:16px;}
+ .note-cover{height:168px;flex:0 0 auto;background-size:cover;background-position:center;background-repeat:no-repeat;border-bottom:1px solid var(--card-bd);position:relative;}
+ .note-cover.empty{background:linear-gradient(135deg,rgba(139,92,246,.24),rgba(76,201,240,.10));}
+ .note-cover.empty::after{content:'封面图 · 后台配置';position:absolute;inset:0;display:flex;align-items:center;justify-content:center;font-size:12px;letter-spacing:2px;color:rgba(255,255,255,.5);}
+ .note-body{padding:18px 22px;display:flex;flex-direction:column;min-width:0;}
+ .note .date{font-size:13px;font-weight:600;color:var(--cyan);letter-spacing:1px;margin-bottom:8px;}
+ .note-title{font-family:"ZCOOL QingKe HuangYou",sans-serif;font-size:21px;color:#fff;letter-spacing:1px;margin-bottom:10px;line-height:1.32;text-shadow:0 0 14px rgba(139,92,246,.4);}
+ .note .body{font-size:15px;color:var(--text);line-height:1.9;}
+ .note .body p{margin:0 0 11px;}
+ .note .body p:last-child{margin-bottom:0;}
+
+ /* ===== 致玩家 ===== */
+ #letter{align-items:center;justify-content:center;}
+ #letter .letter-wrap{max-width:680px;width:100%;overflow-y:auto;padding:10px 20px;}
+ #letter .letter-wrap h2{font-family:"ZCOOL QingKe HuangYou",sans-serif;font-size:30px;letter-spacing:4px;color:#fff;text-shadow:0 0 18px rgba(139,92,246,.5);margin-bottom:6px;text-align:center;}
+ #letter .letter-wrap .lt-sub{text-align:center;font-family:"Press Start 2P",monospace;font-size:9px;color:var(--magenta);letter-spacing:1px;margin-bottom:28px;}
+ #letter .letter-wrap .lt-body{font-size:15px;color:var(--text-dim);line-height:2.1;}
+ #letter .letter-wrap .lt-body p{margin-bottom:16px;}
+ #letter .letter-wrap .lt-body p:first-child{font-size:17px;color:#fff;font-weight:500;}
+ #letter .letter-wrap .lt-body .hl{color:var(--gold);font-weight:500;}
+ #letter .letter-wrap .lt-sign{text-align:right;margin-top:28px;font-size:14px;color:var(--magenta);font-style:italic;}
+
+ /* ===== 论坛(竖排:顶部分类条 + 下方纵向帖子流) ===== */
+ #forum{flex-direction:column;gap:20px;}
+ .f-cats{flex:0 0 auto;flex-direction:row;flex-wrap:wrap;gap:10px;}
+ .f-cats .ch{flex:0 0 auto;}
+ .f-main{flex:1;min-height:0;}
+ .threads{width:100%;}
+
+ /* ===== 公告(内容居中) ===== */
+ #announce .notes{margin:0;}
+
+ /* ===== 剧情(小说阅读器:左目录 + 右正文,竖排阅读感) ===== */
+ #story{padding:28px 48px;}
+ .story-wrap{display:flex;flex-direction:row;width:100%;flex:1;min-height:0;}
+ .story-toc{flex:0 0 286px;display:flex;flex-direction:column;border-right:1px solid var(--card-bd);background:rgba(8,12,26,.35);min-height:0;}
+ .toc-head{font-family:"ZCOOL QingKe HuangYou",sans-serif;font-size:24px;letter-spacing:3px;color:#fff;padding:24px 0 12px;text-shadow:0 0 16px rgba(139,92,246,.5);}
+ .toc-head small{display:block;font-family:"Press Start 2P",monospace;font-size:8px;color:var(--magenta);letter-spacing:1px;margin-top:6px;}
+ .toc-list{flex:1;overflow-y:auto;padding:0 12px 24px;}
+ .toc-loading{padding:30px 16px;color:var(--text-dim);font-size:14px;}
+ .toc-vol{margin-top:10px;}
+ .toc-vol .vname{font-size:14px;color:var(--gold);letter-spacing:1px;padding:8px 10px 4px;font-weight:500;}
+ .toc-vol .vintro{font-size:12px;color:var(--text-dim);line-height:1.6;padding:0 10px 8px;}
+ .toc-ch{display:block;width:100%;text-align:left;font-family:inherit;font-size:15px;color:var(--text-dim);background:transparent;border:none;border-left:2px solid transparent;padding:10px 12px;cursor:pointer;transition:.18s;border-radius:0 8px 8px 0;line-height:1.45;}
+ .toc-ch:hover{color:#fff;background:rgba(139,92,246,.12);}
+ .toc-ch.active{color:#fff;background:rgba(139,92,246,.20);border-left-color:var(--magenta);}
+ .story-reader{flex:1;overflow-y:auto;min-width:0;background:linear-gradient(180deg, rgba(139,92,246,.04), transparent 240px);}
+ .reader-loading{padding:80px 20px;text-align:center;color:var(--text-dim);font-size:15px;}
+ .reader-inner{max-width:840px;margin:0 auto;padding:48px 52px 100px;}
+ .reader-inner .r-vol{font-size:12px;color:var(--cyan);letter-spacing:2px;margin-bottom:10px;}
+ .reader-inner h1{font-family:"ZCOOL QingKe HuangYou",sans-serif;font-size:32px;letter-spacing:2px;color:#fff;text-shadow:0 0 16px rgba(139,92,246,.5);margin-bottom:18px;line-height:1.3;}
+ .reader-inner .body{font-size:17.5px;color:#eef2fb;line-height:2.0;text-rendering:optimizeLegibility;-webkit-font-smoothing:antialiased;}
+ .reader-inner .body p{margin-bottom:20px;text-indent:2em;}
+ .reader-inner .body p:first-child{text-indent:0;}
+ .reader-inner .body h2,.reader-inner .body h3{color:#fff;margin:22px 0 10px;font-size:18px;}
+
+ /* ===== 世界数据报表 ===== */
+ #worlddata{overflow:hidden;padding:28px 48px;}
+ #worldDataContent{flex:1;min-height:0;overflow-y:auto;width:100%;padding-right:8px;}
+ .wd-tag{display:inline-block;font-size:12px;letter-spacing:2px;color:var(--gold);background:rgba(231,184,92,.12);border:1px solid rgba(231,184,92,.38);border-radius:6px;padding:3px 11px;margin-bottom:22px;}
+ .wd-h{font-family:"ZCOOL QingKe HuangYou",sans-serif;font-size:22px;letter-spacing:3px;color:#fff;margin:38px 0 18px;text-shadow:0 0 14px rgba(139,92,246,.45);display:flex;align-items:center;gap:10px;}
+ .wd-h::before{content:"";width:4px;height:22px;border-radius:3px;background:linear-gradient(180deg,var(--violet),var(--cyan));box-shadow:0 0 10px rgba(139,92,246,.6);}
+ .wd-h small{font-family:"Press Start 2P",monospace;font-size:8px;color:var(--magenta);letter-spacing:1px;}
+
+ .wd-kpi{display:grid;grid-template-columns:repeat(auto-fill,minmax(220px,1fr));gap:20px;}
+ .wd-kpi-card{position:relative;overflow:hidden;background:linear-gradient(135deg,rgba(139,92,246,.12),rgba(76,201,240,.05));border:1px solid var(--card-bd);border-radius:16px;padding:22px 26px;transition:.26s;}
+ .wd-kpi-card:hover{transform:translateY(-4px);border-color:var(--violet);box-shadow:var(--glow);}
+ .wd-kpi-card::after{content:"";position:absolute;right:-26px;top:-26px;width:88px;height:88px;border-radius:50%;background:radial-gradient(circle,rgba(199,125,255,.22),transparent 70%);}
+ .wd-kpi-ic{font-size:22px;filter:drop-shadow(0 0 7px rgba(139,92,246,.55));}
+ .wd-kpi-num{font-family:"ZCOOL QingKe HuangYou",sans-serif;font-size:40px;line-height:1.1;color:#fff;margin:10px 0 4px;}
+ .wd-kpi-num .u{font-size:14px;color:var(--text-dim);margin-left:4px;letter-spacing:1px;}
+ .wd-kpi-label{font-size:13px;color:var(--text-dim);letter-spacing:1px;}
+ .wd-kpi-delta{position:absolute;right:14px;top:16px;font-size:11px;font-weight:700;padding:1px 7px;border-radius:5px;}
+ .wd-up{color:#7CFC9B;background:rgba(124,252,155,.12);}
+ .wd-down{color:#ff8585;background:rgba(255,133,133,.12);}
+
+ .wd-bars{display:grid;grid-template-columns:repeat(auto-fill,minmax(300px,1fr));gap:14px 26px;}
+ .wd-bar-row{margin-bottom:4px;}
+ .wd-bar-top{display:flex;justify-content:space-between;font-size:13.5px;margin-bottom:6px;}
+ .wd-bar-top .k{color:var(--text);}
+ .wd-bar-top .v{color:var(--gold);font-weight:700;}
+ .wd-track{height:9px;border-radius:6px;background:rgba(255,255,255,.07);overflow:hidden;}
+ .wd-fill{height:100%;border-radius:6px;background:linear-gradient(90deg,var(--cyan),var(--violet));box-shadow:0 0 9px rgba(139,92,246,.55);}
+
+ .wd-table{width:100%;border-collapse:separate;border-spacing:0;font-size:14px;background:rgba(255,255,255,.02);border:1px solid var(--card-bd);border-radius:14px;overflow:hidden;}
+ .wd-table th{font-family:"ZCOOL QingKe HuangYou",sans-serif;font-size:14px;letter-spacing:2px;color:#fff;background:linear-gradient(135deg,rgba(139,92,246,.22),rgba(76,201,240,.12));text-align:left;padding:13px 16px;border-bottom:1px solid var(--card-bd);}
+ .wd-table td{padding:12px 16px;border-bottom:1px solid rgba(255,255,255,.06);color:var(--text-dim);line-height:1.5;}
+ .wd-table tr:last-child td{border-bottom:none;}
+ .wd-table tr:hover td{background:rgba(139,92,246,.08);color:#eaf0ff;}
+ .wd-table .lead{color:#fff;font-weight:600;}
+ .wd-table .pill{display:inline-block;font-size:12px;padding:2px 9px;border-radius:20px;background:rgba(76,201,240,.14);color:var(--cyan);border:1px solid rgba(76,201,240,.34);}
+
+ .wd-rank{display:flex;flex-direction:column;gap:12px;}
+ .wd-rank-row{display:flex;align-items:center;gap:14px;background:linear-gradient(135deg,rgba(139,92,246,.08),rgba(255,255,255,.02));border:1px solid var(--card-bd);border-radius:13px;padding:12px 16px;transition:.22s;}
+ .wd-rank-row:hover{border-color:var(--violet);transform:translateX(4px);}
+ .wd-rank-no{font-family:"ZCOOL QingKe HuangYou",sans-serif;font-size:22px;width:34px;text-align:center;color:var(--gold);flex:0 0 auto;}
+ .wd-rank-row:nth-child(1) .wd-rank-no{color:#ffd56b;}
+ .wd-rank-row:nth-child(2) .wd-rank-no{color:#cfd8e8;}
+ .wd-rank-row:nth-child(3) .wd-rank-no{color:#e0a86b;}
+ .wd-rank-main{flex:1;min-width:0;}
+ .wd-rank-name{font-size:15px;color:#fff;margin-bottom:5px;display:flex;justify-content:space-between;}
+ .wd-rank-name .s{color:var(--gold);font-weight:700;font-size:13px;}
+ .wd-rank-track{height:8px;border-radius:5px;background:rgba(255,255,255,.07);overflow:hidden;}
+ .wd-rank-fill{height:100%;border-radius:5px;background:linear-gradient(90deg,var(--magenta),var(--violet));box-shadow:0 0 8px rgba(199,125,255,.5);}
@media (max-width:900px){
.hero .copy h1{font-size:42px;}
}
@media (max-width:768px){
.hero .copy{left:18px;right:18px;top:28px;}
+ html,body{height:100%;}
+ body{height:100dvh;}
+ .app{height:100dvh;}
+ .topbar{height:auto;min-height:54px;flex-wrap:wrap;align-items:center;padding:8px 14px;gap:10px;}
+ .logo .cn{font-size:22px;letter-spacing:2px;}
+ .logo .en{display:none;}
+ .spacer{display:none;}
+ .user-box{margin-left:auto;font-size:13px;gap:8px;}
+ .user-box .auth-btn{padding:6px 13px;font-size:12px;}
+ .nav{order:3;flex:1 0 100%;width:100%;margin-left:0;gap:4px;padding-top:6px;border-top:1px solid var(--card-bd);}
+ .nav button{font-size:13px;padding:7px 9px;white-space:nowrap;}
+ .hero .copy{left:18px;right:18px;bottom:82px;}
+ .hero .copy{left:18px;top:28px;}
.hero .copy h1{font-size:32px;letter-spacing:4px;}
.hero .copy .tag{font-size:16px;}
.slide-label{left:18px;bottom:78px;}
@@ -74,6 +369,10 @@
.hero .enter-btn{font-size:15px;padding:11px 30px;}
}
@media (max-width:480px){
+ .topbar{padding:8px 10px;gap:8px;}
+ .logo .cn{font-size:20px;}
+ .nav button{font-size:12px;padding:6px 7px;}
+ .user-box .auth-btn{padding:6px 11px;}
.hero .copy{left:14px;top:20px;}
.hero .copy h1{font-size:26px;letter-spacing:2px;}
.hero .copy .tag{font-size:14px;}
@@ -81,6 +380,14 @@
.slide-label .sl-cn{font-size:20px;}
.slide-label .sl-desc{font-size:12.5px;}
.sl-dots{right:12px;gap:10px;}
+ .grid{grid-template-columns:1fr;}
+ .panel{padding:14px 14px;}
+ .codex-search{width:118px;flex:1 1 100%;}
+ .codex-subrow{flex-wrap:wrap;}
+ .reader-inner{padding:22px 14px 70px;}
+ #story{padding:14px 14px;}
+ .tier-row{grid-template-columns:60px 76px 1fr;gap:6px;}
+ .codex-cats button,.codex-subs button{padding:7px 11px;font-size:13px;}
}
@@ -114,6 +421,87 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -217,6 +605,86 @@
// —— 暴露给箭头按钮 ——
window.slPrev=prev; window.slNext=next;
+ // ============ 五层世界图片轮播 ============
+ (function(){
+ const slides = [
+ { img:'assets/slides/01_starry_sky.png', en:'STARRY SKY', cn:'星空', desc:'陨石星空带悬浮于世界最外层,是文明触及维度的起点。星轨连通各浮空岛,穹鎏晶体自星尘中凝结。',
+ origin:'世界未形,星尘先于万物浮游于虚无。首缕维度自星隙坍缩成形,穹鎏晶体由尘凝就,文明自此仰望苍穹之始。' },
+ { img:'assets/slides/02_sky.png', en:'SKY', cn:'天空', desc:'云朵地基块托起浮空岛群,灵气复苏的第一缕风拂过。各族初代聚落,在此仰望也更在此扎根。',
+ origin:'云基初结,浮空岛群自苍穹垂落凡尘。灵气复苏,初民循风而居,于云上筑城,仰望亦扎根,道统由此萌发。' },
+ { img:'assets/slides/03_earth.png', en:'EARTH', cn:'大地', desc:'平原与森林铺展成万物生灵的摇篮,玄荒文明于斯鼎盛。城邦林立,征伐与道统在沃土上并行。',
+ origin:'沃野铺展为万灵摇篮,玄荒先族自林泉走出。城邦并起,征伐与道统并驰沃土之上,文明于此鼎盛绵延。' },
+ { img:'assets/slides/04_underground.png',en:'UNDERGROUND',cn:'地下', desc:'溶洞晶矿与暗河在深处低语,冥玉生于幽暗玉石之中。地心工坊调度浅层资源,灯火不熄。',
+ origin:'溶洞深开,晶矿与暗河在幽处低语。冥玉生于幽玉深心,地心工坊取浅层之资,灯火长明,匠魂不熄。' },
+ { img:'assets/slides/05_core.png', en:'CORE', cn:'地心', desc:'熔岩奔流的地心,是世界诞生的本源。元玺在此凝聚,追溯天地初开的那一瞬永恒。',
+ origin:'熔岩奔流处,乃世界本源所钟。元玺于亿载炽火中凝聚成形,溯天地初开一瞬,万物归处,亦是起始。' }
+ ];
+ const container = document.getElementById('slideshow');
+ if(!container) return;
+ let current = 0, timer = null;
+ const INTERVAL = 5500;
+
+ // 竖向轨道:5 张图上下滑动
+ const track = document.createElement('div');
+ track.className='slide-track';
+ container.appendChild(track);
+
+ // 渲染 slide + label
+ slides.forEach((s,i)=>{
+ const el = document.createElement('div');
+ el.className='slide'+(i===0?' active':'');
+ el.style.backgroundImage="url('"+s.img+"')";
+ el.innerHTML=''+s.en+''+s.cn+'
'+s.desc+'
'
+ +'起源'+s.origin+'
';
+ track.appendChild(el);
+ });
+ const all = track.querySelectorAll('.slide');
+
+ // 指示器
+ const dotsWrap = document.createElement('div');
+ dotsWrap.className='sl-dots';
+ slides.forEach((_,i)=>{
+ const d=document.createElement('div');
+ d.className='sl-dot'+(i===0?' active':'');
+ d.addEventListener('click',()=>go(i));
+ dotsWrap.appendChild(d);
+ });
+ document.querySelector('.hero').appendChild(dotsWrap);
+ const dots = dotsWrap.querySelectorAll('.sl-dot');
+
+ function go(idx){
+ if(idx<0)idx=slides.length-1;if(idx>=slides.length)idx=0;
+ if(idx===current) return;
+ all[current].classList.remove('active');dots[current].classList.remove('active');
+ current=idx;
+ track.style.transform='translateY('+(-current*100)+'%)';
+ all[current].classList.add('active');dots[current].classList.add('active');
+ resetTimer();
+ }
+ function next(){ go((current+1)%slides.length); }
+ function prev(){ go((current-1+slides.length)%slides.length); }
+ function resetTimer(){ clearInterval(timer); timer=setInterval(next,INTERVAL); }
+
+ // 鼠标滚轮上下滑动
+ let lock=false,wAccum=0;
+ document.querySelector('.hero').addEventListener('wheel',function(e){
+ wAccum+=e.deltaY;
+ if(lock)return;
+ if(Math.abs(wAccum)>50){
+ lock=true;
+ if(wAccum>0)next();else prev();
+ wAccum=0;
+ setTimeout(function(){lock=false;},1250);
+ }
+ },{passive:true});
+
+ resetTimer();
+ })();
+
+ // ============ 程序化矢量缩略图(零 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'};
resetTimer();
})();
diff --git a/login.html b/login.html
index ac9f2ce..5ee9fab 100644
--- a/login.html
+++ b/login.html
@@ -26,6 +26,7 @@
/* 登录页背景(固定单图 login_bg,原图直出,不加特效) */
.bg-slides{position:fixed;inset:0;z-index:0;background:url('assets/slides/login_bg.webp') center/cover no-repeat;}
+ .bg-slides{position:fixed;inset:0;z-index:0;background:url('assets/slides/login_bg.png') center/cover no-repeat;}
.wrap{position:relative;z-index:2;width:min(420px,92vw);}
.brand{text-align:center;margin-bottom:26px;}
diff --git a/world.html b/world.html
index 399940d..41c1409 100644
--- a/world.html
+++ b/world.html
@@ -619,51 +619,49 @@ body { background: #0a0e17; overflow: hidden; font-family: 'Microsoft YaHei', sa