注册数据

This commit is contained in:
李鹏宇 2026-07-28 19:06:32 +08:00
parent b0ec5c9ab8
commit ffd4bb8f08
17 changed files with 977 additions and 244 deletions

View File

@ -1,13 +1,14 @@
{ {
'name': '宇森游戏基础数据', 'name': '宇森游戏基础数据',
'summary': '宇森游戏基础数据:图鉴 / 规则 / 论坛 / 公告', 'summary': '宇森游戏基础数据:图鉴 / 规则 / 论坛 / 公告 / 玩家员工',
'version': '1.2', 'version': '1.3',
'category': 'Game', 'category': 'Game',
'sequence': 10, 'sequence': 10,
'depends': ['base'], 'depends': ['base', 'hr'],
'data': [ 'data': [
'security/ir.model.access.csv', 'security/ir.model.access.csv',
'data/game_base_data.xml', 'data/game_base_data.xml',
'data/game_achievement_data.xml',
'views/codex_views.xml', 'views/codex_views.xml',
'views/rule_category_views.xml', 'views/rule_category_views.xml',
'views/rule_views.xml', 'views/rule_views.xml',
@ -15,10 +16,13 @@
'views/forum_views.xml', 'views/forum_views.xml',
'views/announcement_views.xml', 'views/announcement_views.xml',
'views/story_views.xml', 'views/story_views.xml',
'views/game_employee_views.xml',
'views/game_achievement_views.xml',
'views/game_mail_views.xml',
'views/menu.xml', 'views/menu.xml',
], ],
'application': True, 'application': True,
'license': 'LGPL-3', 'license': 'LGPL-3',
'pre_init_hook': 'pre_init_hook', 'pre_init_hook': 'pre_init_hook',
'description': '基于官网内容沉淀的游戏基础数据模型,供后台维护、前端经 /game/api/* 读取;并提供与 Odoo 用户打通的登录/注册接口。', 'description': '基于官网内容沉淀的游戏基础数据模型,供后台维护、前端经 /game/api/* 读取;并提供与 Odoo 用户打通的登录/注册接口。注册时自动创建 hr.employee 并初始化游戏数据。',
} }

View File

@ -1,6 +1,6 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
import json import json
from odoo import http from odoo import http, fields
from odoo.http import request, Response from odoo.http import request, Response
@ -309,12 +309,52 @@ class GameApiController(http.Controller):
'groups_id': [(6, 0, [portal.id])], 'groups_id': [(6, 0, [portal.id])],
}) })
request.env.cr.commit() # 让新用户立即对其他游标可见,便于随后自动登录 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': '<p>欢迎来到宇森!请查收新手礼包与世界探索指南。</p>',
})
request.env.cr.commit()
db = request.session.db or 'game' db = request.session.db or 'game'
auth_info = request.session.authenticate(db, {'type': 'password', 'login': login, 'password': password}) auth_info = request.session.authenticate(db, {'type': 'password', 'login': login, 'password': password})
uid = auth_info['uid'] uid = auth_info['uid']
if uid: if uid:
return _json({'ok': True, 'uid': uid, 'name': user.name, 'login': user.login}) 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}) '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: except Exception as e:
return _json({'ok': False, 'error': '注册失败:%s' % str(e)}) return _json({'ok': False, 'error': '注册失败:%s' % str(e)})
@ -324,9 +364,123 @@ class GameApiController(http.Controller):
if uid: if uid:
user = request.env['res.users'].sudo().browse(uid) user = request.env['res.users'].sudo().browse(uid)
if user.exists(): 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}) 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', @http.route('/game/api/logout', type='http', auth='public',
methods=['POST', 'OPTIONS'], csrf=False) methods=['POST', 'OPTIONS'], csrf=False)
def logout(self, **kw): def logout(self, **kw):

View File

@ -0,0 +1,95 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data noupdate="1">
<!-- 成就模板初始数据 -->
<record model="game.achievement.template" id="ach_first_land">
<field name="code">first_land</field>
<field name="sequence">10</field>
<field name="name">开天辟地</field>
<field name="icon">star</field>
<field name="tier">gold</field>
<field name="points">50</field>
<field name="target">1</field>
<field name="desc">已点亮第一块领土,文明之火开始燃烧。</field>
</record>
<record model="game.achievement.template" id="ach_civilization_cradle">
<field name="code">civilization_cradle</field>
<field name="sequence">20</field>
<field name="name">文明摇篮</field>
<field name="icon">sprout</field>
<field name="tier">silver</field>
<field name="points">30</field>
<field name="target">5</field>
<field name="desc">建立 5 个聚落,文明初步成形。</field>
</record>
<record model="game.achievement.template" id="ach_sky_tower">
<field name="code">sky_tower</field>
<field name="sequence">30</field>
<field name="name">通天塔</field>
<field name="icon">lock</field>
<field name="tier">platinum</field>
<field name="points">100</field>
<field name="target">10</field>
<field name="desc">建造 10 座奇观,文明疆域遍布穹苍。</field>
</record>
<record model="game.achievement.template" id="ach_world_explorer">
<field name="code">world_explorer</field>
<field name="sequence">40</field>
<field name="name">世界探索者</field>
<field name="icon">compass</field>
<field name="tier">bronze</field>
<field name="points">10</field>
<field name="target">10</field>
<field name="desc">探索 10 块未知领土。</field>
</record>
<record model="game.achievement.template" id="ach_resource_master">
<field name="code">resource_master</field>
<field name="sequence">50</field>
<field name="name">资源大亨</field>
<field name="icon">gem</field>
<field name="tier">silver</field>
<field name="points">30</field>
<field name="target">1000</field>
<field name="desc">累计获得 1000 金币。</field>
</record>
<record model="game.achievement.template" id="ach_spirit_awakening">
<field name="code">spirit_awakening</field>
<field name="sequence">60</field>
<field name="name">灵气觉醒</field>
<field name="icon">sparkle</field>
<field name="tier">gold</field>
<field name="points">50</field>
<field name="target">1</field>
<field name="desc">世界阶段跃迁至「灵启」。</field>
</record>
<record model="game.achievement.template" id="ach_mystic_realm">
<field name="code">mystic_realm</field>
<field name="sequence">70</field>
<field name="name">玄界之主</field>
<field name="icon">crown</field>
<field name="tier">legend</field>
<field name="points">200</field>
<field name="target">1</field>
<field name="desc">世界阶段跃迁至「玄界」。</field>
</record>
<record model="game.achievement.template" id="ach_eternal">
<field name="code">eternal</field>
<field name="sequence">80</field>
<field name="name">永恒之锚</field>
<field name="icon">infinity</field>
<field name="tier">legend</field>
<field name="points">500</field>
<field name="target">1</field>
<field name="desc">世界阶段达到「溯元」,触及世界本源。</field>
</record>
</data>
</odoo>

View File

@ -6,3 +6,6 @@ from . import rule
from . import page from . import page
from . import story from . import story
from . import ir_http from . import ir_http
from . import game_employee
from . import game_achievement
from . import game_mail

View File

@ -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

View File

@ -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,
}

View File

@ -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='系统')

View File

@ -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_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,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_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

1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
19 access_game_story_volume_user game.story.volume user model_game_story_volume base.group_user 1 1 1 1
20 access_game_story_chapter game.story.chapter public model_game_story_chapter base.group_public 1 0 0 0
21 access_game_story_chapter_user game.story.chapter user model_game_story_chapter base.group_user 1 1 1 1
22 access_game_achievement_template game.achievement.template public model_game_achievement_template base.group_public 1 0 0 0
23 access_game_achievement_template_user game.achievement.template user model_game_achievement_template base.group_user 1 1 1 1
24 access_game_player_achievement game.player.achievement public model_game_player_achievement base.group_public 1 0 0 0
25 access_game_player_achievement_user game.player.achievement user model_game_player_achievement base.group_user 1 1 1 1
26 access_game_player_mail game.player.mail public model_game_player_mail base.group_public 1 0 0 0
27 access_game_player_mail_user game.player.mail user model_game_player_mail base.group_user 1 1 1 1

View File

@ -28,7 +28,7 @@
</group> </group>
<field name="body_html"/> <field name="body_html"/>
<field name="post_ids"> <field name="post_ids">
<tree><field name="author_name"/><field name="create_date"/></tree> <list><field name="author_name"/><field name="create_date"/></list>
<form><field name="author_name"/><field name="body_html"/></form> <form><field name="author_name"/><field name="body_html"/></form>
</field> </field>
</form> </form>

View File

@ -0,0 +1,81 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<!-- 成就模板列表 -->
<record model="ir.ui.view" id="view_game_achievement_template_tree">
<field name="name">game.achievement.template.tree</field>
<field name="model">game.achievement.template</field>
<field name="arch" type="xml">
<list>
<field name="sequence" widget="handle"/>
<field name="code"/>
<field name="name"/>
<field name="icon"/>
<field name="tier"/>
<field name="points"/>
<field name="target"/>
<field name="active" widget="boolean_toggle"/>
</list>
</field>
</record>
<!-- 成就模板表单 -->
<record model="ir.ui.view" id="view_game_achievement_template_form">
<field name="name">game.achievement.template.form</field>
<field name="model">game.achievement.template</field>
<field name="arch" type="xml">
<form>
<sheet>
<group>
<group>
<field name="code"/>
<field name="name"/>
<field name="icon" placeholder="emoji 或符号"/>
<field name="tier"/>
</group>
<group>
<field name="points"/>
<field name="target"/>
<field name="sequence"/>
<field name="active"/>
</group>
</group>
<group>
<field name="desc" placeholder="解锁条件/说明"/>
</group>
</sheet>
</form>
</field>
</record>
<!-- 动作 -->
<record model="ir.actions.act_window" id="action_game_achievement_template">
<field name="name">成就模板</field>
<field name="res_model">game.achievement.template</field>
<field name="view_mode">tree,form</field>
</record>
<!-- 玩家成就列表(只读查看) -->
<record model="ir.ui.view" id="view_game_player_achievement_tree">
<field name="name">game.player.achievement.tree</field>
<field name="model">game.player.achievement</field>
<field name="arch" type="xml">
<list>
<field name="name"/>
<field name="employee_id"/>
<field name="tier"/>
<field name="points"/>
<field name="progress"/>
<field name="unlocked"/>
<field name="unlock_date"/>
</list>
</field>
</record>
<record model="ir.actions.act_window" id="action_game_player_achievement">
<field name="name">玩家成就</field>
<field name="res_model">game.player.achievement</field>
<field name="view_mode">tree,form</field>
</record>
</odoo>

View File

@ -0,0 +1,60 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<!-- 继承 hr.employee 表单视图,添加「游戏数据」页签 -->
<record model="ir.ui.view" id="view_employee_game_form">
<field name="name">hr.employee.game.form</field>
<field name="model">hr.employee</field>
<field name="inherit_id" ref="hr.view_employee_form"/>
<field name="arch" type="xml">
<xpath expr="//notebook" position="inside">
<page string="游戏数据" name="game_data">
<group>
<group string="等级系统">
<field name="game_uid"/>
<field name="game_level"/>
<field name="game_exp"/>
<field name="game_exp_max"/>
<field name="game_title"/>
<field name="game_reg_date"/>
</group>
<group string="资源">
<field name="game_coins"/>
<field name="game_energy"/>
<field name="game_wood"/>
<field name="game_stone"/>
</group>
</group>
<group>
<group string="世界数据">
<field name="game_world_name"/>
<field name="game_era"/>
<field name="game_tiles"/>
<field name="game_population"/>
<field name="game_civ_level"/>
</group>
<group string="统计">
<field name="game_achievement_count"/>
<field name="game_unread_mail"/>
</group>
</group>
</page>
</xpath>
</field>
</record>
<!-- 员工列表:增加游戏 UID/等级/称号 列 -->
<record model="ir.ui.view" id="view_employee_game_tree">
<field name="name">hr.employee.game.tree</field>
<field name="model">hr.employee</field>
<field name="inherit_id" ref="hr.view_employee_tree"/>
<field name="arch" type="xml">
<xpath expr="//field[@name='name']" position="after">
<field name="game_uid" string="游戏UID"/>
<field name="game_level" string="等级"/>
<field name="game_title" string="称号"/>
</xpath>
</field>
</record>
</odoo>

View File

@ -0,0 +1,56 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<!-- 玩家邮件列表 -->
<record model="ir.ui.view" id="view_game_player_mail_tree">
<field name="name">game.player.mail.tree</field>
<field name="model">game.player.mail</field>
<field name="arch" type="xml">
<list>
<field name="is_pinned" widget="boolean_toggle"/>
<field name="subject"/>
<field name="employee_id" string="收件人"/>
<field name="category"/>
<field name="sender"/>
<field name="date"/>
<field name="is_read" widget="boolean_toggle"/>
</list>
</field>
</record>
<!-- 玩家邮件表单 -->
<record model="ir.ui.view" id="view_game_player_mail_form">
<field name="name">game.player.mail.form</field>
<field name="model">game.player.mail</field>
<field name="arch" type="xml">
<form>
<sheet>
<group>
<group>
<field name="employee_id" string="收件人"/>
<field name="category"/>
<field name="sender"/>
</group>
<group>
<field name="date"/>
<field name="is_pinned"/>
<field name="is_read"/>
</group>
</group>
<group>
<field name="subject"/>
<field name="body" widget="html"/>
</group>
</sheet>
</form>
</field>
</record>
<!-- 动作 -->
<record model="ir.actions.act_window" id="action_game_player_mail">
<field name="name">玩家邮件</field>
<field name="res_model">game.player.mail</field>
<field name="view_mode">tree,form</field>
</record>
</odoo>

View File

@ -26,6 +26,11 @@
<!-- 公告 --> <!-- 公告 -->
<menuitem id="menu_game_announcement" name="公告" parent="menu_game_root" action="action_game_announcement" sequence="40"/> <menuitem id="menu_game_announcement" name="公告" parent="menu_game_root" action="action_game_announcement" sequence="40"/>
<!-- 玩家 -->
<menuitem id="menu_game_player" name="玩家" parent="menu_game_root" sequence="45"/>
<menuitem id="menu_game_player_achievement" name="玩家成就" parent="menu_game_player" action="action_game_player_achievement" sequence="46"/>
<menuitem id="menu_game_player_mail" name="玩家邮件" parent="menu_game_player" action="action_game_player_mail" sequence="47"/>
<!-- 小说(剧情) --> <!-- 小说(剧情) -->
<menuitem id="menu_game_story" name="小说" parent="menu_game_root" sequence="35"/> <menuitem id="menu_game_story" name="小说" parent="menu_game_root" sequence="35"/>
<menuitem id="menu_game_story_volume" name="卷" parent="menu_game_story" action="action_game_story_volume" sequence="36"/> <menuitem id="menu_game_story_volume" name="卷" parent="menu_game_story" action="action_game_story_volume" sequence="36"/>
@ -37,5 +42,6 @@
<field name="action" eval="False"/> <field name="action" eval="False"/>
</record> </record>
<menuitem id="menu_game_rule_category" name="规则类别" parent="menu_game_config" action="action_game_rule_category" sequence="51"/> <menuitem id="menu_game_rule_category" name="规则类别" parent="menu_game_config" action="action_game_rule_category" sequence="51"/>
<menuitem id="menu_game_achievement_template" name="成就模板" parent="menu_game_config" action="action_game_achievement_template" sequence="52"/>
</odoo> </odoo>

View File

@ -59,6 +59,19 @@ const API = {
method: 'POST', credentials: _cred, method: 'POST', credentials: _cred,
})).json(); })).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() { async codex() {
return (await fetch(_url('/game/api/codex'), { credentials: _cred })).json(); return (await fetch(_url('/game/api/codex'), { credentials: _cred })).json();
}, },
@ -123,4 +136,6 @@ const API = {
return await r.json(); return await r.json();
} catch (e) { return null; } } catch (e) { return null; }
}, },
// 暴露后端 base供图片等静态资源 URL 拼接本地跨域→8069线上同域→''
base: API_CONFIG.base,
}; };

View File

@ -53,6 +53,102 @@ async function loadRules(){
} catch (e) { /* 保留静态兜底内容 */ } } 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 = '<div style="grid-column:1/-1;text-align:center;color:var(--text-dim);padding:60px 0">暂无图鉴数据</div>';
return;
}
// 选中状态
let curGroup = groups[0].key;
let curSub = 'all';
let curQ = '';
function renderCats(){
if (!catsEl) return;
catsEl.innerHTML = groups.map(g =>
`<button class="${g.key === curGroup ? 'active' : ''}" data-gk="${g.key}">${g.label}</button>`
).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 = `<button class="${curSub === 'all' ? 'active' : ''}" data-sn="all">全部</button>` +
subs.map(s => `<button class="${curSub === s.name ? 'active' : ''}" data-sn="${s.name}">${s.name}</button>`).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 = '<div style="grid-column:1/-1;text-align:center;color:var(--text-dim);padding:60px 0">未找到匹配项</div>';
return;
}
const base = API.base || '';
grid.innerHTML = items.map(it => {
const thumb = it.image
? `<div class="thumb" style="background:url('${base}${it.image}') center/cover"></div>`
: `<div class="thumb"></div>`;
return `<div class="cx">
${thumb}
<div class="body">
<div class="nm">${it.name || ''}</div>
<div class="desc">${it.desc || ''}</div>
<div class="meta"><span class="sub">${it.sub || ''}</span><span class="tier">${it.tier || ''}</span></div>
</div>
</div>`;
}).join('');
}
if (searchEl){
searchEl.addEventListener('input', () => { curQ = searchEl.value.trim(); renderGrid(); });
}
renderCats();
renderSubs();
renderGrid();
}
// ---------- 论坛 ---------- // ---------- 论坛 ----------
async function loadForum(){ async function loadForum(){
const threadsEl = document.getElementById('threads'); const threadsEl = document.getElementById('threads');
@ -155,6 +251,7 @@ async function loadStory(){
document.addEventListener('DOMContentLoaded', () => { document.addEventListener('DOMContentLoaded', () => {
loadPages(); loadPages();
loadRules(); loadRules();
loadCodex();
loadForum(); loadForum();
loadAnnounce(); loadAnnounce();
loadStory(); loadStory();

View File

@ -182,6 +182,7 @@
.cx .meta{display:flex;justify-content:space-between;margin-top:5px;font-size:11px;} .cx .meta{display:flex;justify-content:space-between;margin-top:5px;font-size:11px;}
.cx .meta .sub{color:var(--cyan);} .cx .meta .sub{color:var(--cyan);}
.cx .meta .tier{color:var(--gold);} .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;} .cx .ds{font-size:11.5px;color:var(--text-dim);line-height:1.5;margin-top:6px;}
/* ===== 论坛 ===== */ /* ===== 论坛 ===== */
@ -437,68 +438,8 @@
<!-- ===== 规则 ===== --> <!-- ===== 规则 ===== -->
<section class="panel" id="rules"> <section class="panel" id="rules">
<div class="rules-scroll"> <div class="rules-scroll">
<!-- ① 世界等级 · 五阶段 --> <!-- 静态兜底数据已注释 — 验证 API 是否取到后台数据 -->
<div class="rule-block"> <!-- 如页面此处为空白 = Odoo API 未返回规则数据 -->
<h3>① 世界等级 · 五阶段</h3>
<p class="rule-intro">世界从凡墟到溯元的整体进化阶段,决定可解锁的内容深度与事件池上限。园丁对世界施加的影响会推动阶段跃迁——这是文明能"发生什么"的舞台上限。</p>
<div class="tier-list">
<div class="tier-row"><span class="tn">阶段一</span><span class="tname">凡墟</span><span class="tdesc">万物生灵尚处蒙昧凡土,天地灵气稀薄,各族散居荒野,无族群联盟与政权,只求狩猎采集求生,一切秩序遵循自然法则。</span></div>
<div class="tier-row"><span class="tn">阶段二</span><span class="tname">灵启</span><span class="tdesc">天地灵气复苏觉醒,各族聚拢形成聚落,诞生劳作分工、屋舍营建与物物交换,不同族群率先凝聚部族,世间初代势力自此诞生。</span></div>
<div class="tier-row"><span class="tn">阶段三</span><span class="tname">玄界</span><span class="tdesc">各族部落兼并融合,广袤大地形成多元族群共治的完整疆域,征伐、道统、造物三大发展路线并行;各族大能、上古秘闻等传奇事件不断现世,玄荒文明迎来鼎盛割据时代。</span></div>
<div class="tier-row"><span class="tn">阶段四</span><span class="tname">穹冥</span><span class="tdesc">生灵突破地表桎梏,打通地底深渊与浩瀚星空,天地上下各界尽数纳入族群管辖;各类上古神器、本命法器完整解锁,文明疆域遍布天地穹苍。</span></div>
<div class="tier-row"><span class="tn">阶段五</span><span class="tname">溯元</span><span class="tdesc">各族顶尖文明勘破这片天地的诞生本源,挣脱本世界规则束缚,可踏上飞升之路、往复轮回,前往更高维度开启全新文明演化。</span></div>
</div>
</div>
<!-- ② 势力等级 -->
<div class="rule-block">
<h3>② 势力等级</h3>
<p class="rule-intro">文明内部群体的聚合度,由人口、领土、影响力累计。势力等级越高,可调动的世界资源与外交权重越大——决定文明"能调动什么"。</p>
<div class="tier-list">
<div class="tier-row"><span class="tn">Lv.1</span><span class="tname">墟族</span><span class="tdesc">血缘小群体,蛮荒无制度。</span></div>
<div class="tier-row"><span class="tn">Lv.2</span><span class="tname">灵聚</span><span class="tdesc">灵气聚落联盟,初步治理与贸易。</span></div>
<div class="tier-row"><span class="tn">Lv.3</span><span class="tname">玄邑</span><span class="tdesc">疆域整合,军法、道规体系完善。</span></div>
<div class="tier-row"><span class="tn">Lv.4</span><span class="tname">冥朝</span><span class="tdesc">一统大地,掌控地下浅层资源调度。</span></div>
<div class="tier-row"><span class="tn">Lv.5</span><span class="tname">穹统</span><span class="tdesc">天地大陆霸主,穿梭星空深渊作战。</span></div>
<div class="tier-row"><span class="tn">Lv.6</span><span class="tname">溯盟</span><span class="tdesc">多维文明联合体,触及世界本源博弈。</span></div>
</div>
</div>
<!-- ③ 人物角色等级 -->
<div class="rule-block">
<h3>③ 人物角色等级 · 十二阶段</h3>
<p class="rule-intro">个体(英雄 / 领袖 / 特殊 NPC的成长阶序通过历练、天赋点亮与装备积累提升影响可承担的职责与战力——决定个体"能做成什么"。</p>
<div class="tier-list">
<div class="tier-row"><span class="tn">Lv.1</span><span class="tname">萌新</span><span class="tdesc">初入世界的无名者。</span></div>
<div class="tier-row"><span class="tn">Lv.2</span><span class="tname">学徒</span><span class="tdesc">掌握基础技艺。</span></div>
<div class="tier-row"><span class="tn">Lv.3</span><span class="tname">熟手</span><span class="tdesc">可独立完成任务。</span></div>
<div class="tier-row"><span class="tn">Lv.4</span><span class="tname">好手</span><span class="tdesc">在某领域崭露头角。</span></div>
<div class="tier-row"><span class="tn">Lv.5</span><span class="tname">精英</span><span class="tdesc">群体中的中坚。</span></div>
<div class="tier-row"><span class="tn">Lv.6</span><span class="tname">大师</span><span class="tdesc">技艺臻于化境。</span></div>
<div class="tier-row"><span class="tn">Lv.7</span><span class="tname">宗匠</span><span class="tdesc">开宗立派的人物。</span></div>
<div class="tier-row"><span class="tn">Lv.8</span><span class="tname">传奇</span><span class="tdesc">事迹被载入编年史。</span></div>
<div class="tier-row"><span class="tn">Lv.9</span><span class="tname">神话</span><span class="tdesc">近乎传说的存在。</span></div>
<div class="tier-row"><span class="tn">Lv.10</span><span class="tname">半神</span><span class="tdesc">触及世界法则。</span></div>
<div class="tier-row"><span class="tn">Lv.11</span><span class="tname">圣者</span><span class="tdesc">被信仰托举的意志。</span></div>
<div class="tier-row"><span class="tn">Lv.12</span><span class="tname">永恒</span><span class="tdesc">超越生死的世界锚点。</span></div>
</div>
</div>
<!-- ④ 制度说明 -->
<div class="rule-block">
<h3>④ 制度说明</h3>
<p class="rule-intro">三者相互独立又彼此加成:<b style="color:#fff">世界等级</b>是舞台上限,决定能发生什么;<b style="color:#fff">势力等级</b>是群体进度,决定能调动什么;<b style="color:#fff">角色等级</b>是个体成长,决定能做成什么。园丁的核心策略,是在三者间分配有限的引导资源,让文明自洽地向上演化。<br><br>跃迁逻辑:角色历练与装备积累→提升势力实力→推动世界阶段跃迁→解锁更深内容(更深的地下层、更远的星空层、更强的神器)。每一级提升都会扩大可叙事的事件池,世界因此越活越厚。</p>
</div>
<!-- ⑤ 货币等级 -->
<div class="rule-block">
<h3>⑤ 货币等级 · 六档</h3>
<p class="rule-intro">文明流通的价值尺度,从凡俗零钱到本源货币,越稀有的货币可交易的资源层级越高——决定文明"能交换什么"。<br><br><b style="color:#fff">兑换规则:</b>100 低等级货币 = 1 高等级货币。</p>
<div class="tier-list">
<div class="tier-row"><span class="tn">Lv.1</span><span class="tname">墟铢</span><span class="tdesc">凡俗通用零钱,以贝壳、骨片、碎石铸成。<b>100 墟铢 = 1 灵币</b></span></div>
<div class="tier-row"><span class="tn">Lv.2</span><span class="tname">灵币</span><span class="tdesc">蕴含微量灵气的金属铸币,聚落以上势力日常使用。<b>100 灵币 = 1 玄锭</b></span></div>
<div class="tier-row"><span class="tn">Lv.3</span><span class="tname">玄锭</span><span class="tdesc">刻有玄纹的稀有锭块,用于高价值交易与功法图纸。<b>100 玄锭 = 1 冥玉</b></span></div>
<div class="tier-row"><span class="tn">Lv.4</span><span class="tname">冥玉</span><span class="tdesc">产自地底深渊的幽暗玉石,蕴含浓郁能量。<b>100 冥玉 = 1 穹鎏</b></span></div>
<div class="tier-row"><span class="tn">Lv.5</span><span class="tname">穹鎏</span><span class="tdesc">来自星空矿脉的稀有鎏金晶体,跨层贸易硬通货。<b>100 穹鎏 = 1 元玺</b></span></div>
<div class="tier-row"><span class="tn">Lv.6</span><span class="tname">元玺</span><span class="tdesc">追溯世界本源凝聚而成的至高货币,可兑换神器、传说事件与维度权限。</span></div>
</div>
</div>
</div> </div>
</section> </section>
@ -518,12 +459,8 @@
<!-- ===== 论坛 ===== --> <!-- ===== 论坛 ===== -->
<section class="panel" id="forum"> <section class="panel" id="forum">
<div class="f-cats"> <div class="f-cats">
<div class="ch active" data-fcat="all">全部</div> <!-- 静态分类已注释 — 验证 API 是否取到后台数据 -->
<div class="ch" data-fcat="discuss">综合讨论</div> <!-- 如页面此处为空白 = Odoo API 未返回论坛分类 -->
<div class="ch" data-fcat="strategy">攻略</div>
<div class="ch" data-fcat="lore">背景考据</div>
<div class="ch" data-fcat="showcase">作品秀</div>
<div class="ch" data-fcat="bug">反馈</div>
</div> </div>
<div class="f-main"> <div class="f-main">
<div class="post-form" id="postForm"> <div class="post-form" id="postForm">
@ -532,12 +469,8 @@
<div class="row"><button id="pfCancel">取消</button><button class="ok" id="pfOk">发布</button></div> <div class="row"><button id="pfCancel">取消</button><button class="ok" id="pfOk">发布</button></div>
</div> </div>
<div class="threads" id="threads"> <div class="threads" id="threads">
<div class="th" data-fcat="discuss"><span class="pin">置顶</span><span class="tt">【公告】宇森官网第一阶段上线,欢迎园丁们入驻</span><span class="meta"><span class="rp">128</span> 回复 · 官方</span></div> <!-- 静态兜底数据已注释 — 验证 API 是否取到后台数据 -->
<div class="th" data-fcat="strategy"><span class="tt">新手向:园丁视角的三种开局思路</span><span class="meta"><span class="rp">42</span> 回复 · 行者</span></div> <!-- 如页面此处为空白 = Odoo API 未返回论坛数据 -->
<div class="th" data-fcat="discuss"><span class="tt">你们更喜欢哪种族?来投个票</span><span class="meta"><span class="rp">76</span> 回复 · 雾岛</span></div>
<div class="th" data-fcat="showcase"><span class="tt">手绘了我的主世界浮空岛,求轻喷</span><span class="meta"><span class="rp">31</span> 回复 · 青柠</span></div>
<div class="th" data-fcat="bug"><span class="tt">反馈:飞入微观时偶现卡顿</span><span class="meta"><span class="rp">9</span> 回复 · K</span></div>
<div class="th" data-fcat="strategy"><span class="tt">地下矿脉怎么规划最省心?</span><span class="meta"><span class="rp">18</span> 回复 · 老周</span></div>
</div> </div>
</div> </div>
</section> </section>
@ -545,84 +478,8 @@
<!-- ===== 公告 ===== --> <!-- ===== 公告 ===== -->
<section class="panel" id="announce"> <section class="panel" id="announce">
<div class="notes"> <div class="notes">
<div class="note pin feat"> <!-- 静态兜底数据已注释 — 验证 API 是否取到后台数据 -->
<div class="note-cover empty"></div> <!-- 如页面此处为空白 = Odoo API 未返回公告数据 -->
<div class="note-body">
<div class="date">2026.07.09 · 官方</div>
<div class="note-title">宇森官网第一阶段上线,欢迎园丁们入驻</div>
<div class="body">
<p>历经三个月打磨,宇森官网第一阶段正式上线。本次更新带来八大板块——介绍、特色、背景、规则、图鉴、论坛、公告、致玩家,全部以单页切换形式呈现,介绍页即一段概念宣传片,让你在三秒内读懂我们要做什么。</p>
<p>「园丁视角」是宇森的核心:你不是旁观者,而是世界的第一位居民。上传你的世界种子,观看五层世界自上而下生长——从星空陨石带到地心熔岩,每一寸地形、每一个种族,都由你的选择塑造。</p>
<p>我们同步开放了社区论坛与图鉴共建入口。欢迎在「论坛」留下你的构想,或在「图鉴」认领尚未完成的种族条目;被采纳的内容,会带上你的署名进入正式世界观。</p>
</div>
</div>
</div>
<div class="note">
<div class="note-cover empty"></div>
<div class="note-body">
<div class="date">2026.07.09</div>
<div class="note-title">图鉴系统全面升级204 种族 + 程序化矢量插画</div>
<div class="body">
<p>图鉴系统完成一次大版本升级:种族条目扩充至 204 条,系别、性格、天赋、神器与传奇事件全面扩量,装备槽位对齐「头 / 手 / 胸 / 裤 / 鞋 / 戒指(双)/ 项链 / 宝物」九宫格。</p>
<p>所有缩略图改用程序化矢量插画生成,零 AI 生图、零版权风险,且在任意分辨率下都不糊。后续会逐步开放玩家投稿的「同人词条」分区。</p>
</div>
</div>
</div>
<div class="note">
<div class="note-cover empty"></div>
<div class="note-body">
<div class="date">2026.07.08</div>
<div class="note-title">概念宣传片《垂直生长》完成</div>
<div class="body">
<p>概念宣传片《垂直生长》正式杀青。片长 90 秒,以「自上而下」的镜头语言呈现五层世界的生长过程:天空层改为云朵地基块,星空层改为陨石科技带,地心则翻涌着熔岩血脉。</p>
<p>这支片子将成为官网介绍页的自动背景,也会在后续对外宣发中使用。如果你喜欢这种克制的科幻基调,记得在论坛给我们反馈。</p>
</div>
</div>
</div>
<div class="note">
<div class="note-cover empty"></div>
<div class="note-body">
<div class="date">2026.07.01</div>
<div class="note-title">园丁视角核心玩法原型启动</div>
<div class="body">
<p>园丁视角核心玩法原型进入验证阶段。首版采用六边形地图,配合「活 NPC」系统——NPC 拥有独立的日程、记忆与情绪,会对你的长期行为产生不同反应。</p>
<p>这一阶段我们重点关注「放手感」:玩家拖拽、缩放、种植、引导文明,每一步都要足够顺滑。手感达标后,再叠加叙事与冲突。</p>
</div>
</div>
</div>
<div class="note">
<div class="note-cover empty"></div>
<div class="note-body">
<div class="date">2026.06.25</div>
<div class="note-title">五大世界层设定公开</div>
<div class="body">
<p>我们公开了宇森的底层世界观——五大世界层:星空、天空、大地、地下、地心。每一层对应一种文明形态与资源循环,层与层之间通过「根系」与「星轨」双向连通。</p>
<p>这套设定不是装饰,而是玩法骨架:你在大地的每一次开采,都会改变地心的压力;地心的每一次喷发,又会重塑天空的气候。</p>
</div>
</div>
</div>
<div class="note">
<div class="note-cover empty"></div>
<div class="note-body">
<div class="date">2026.06.20</div>
<div class="note-title">社区共建计划:你的名字可以写进世界观</div>
<div class="body">
<p>图鉴系统规划正式确定,分为「种族 / 建筑 / 生物 / 神器」四大类目。我们决定把相当一部分条目开放给社区共建。</p>
<p>被采纳的投稿会标注作者署名,并永久保留在官方图鉴中。第一批评审将在论坛「共建」分区进行,欢迎有想法的园丁报名。</p>
</div>
</div>
</div>
<div class="note">
<div class="note-cover empty"></div>
<div class="note-body">
<div class="date">2026.06.12</div>
<div class="note-title">限量内测招募开启</div>
<div class="body">
<p>宇森首轮封闭内测招募正式开启,名额 500 人。我们将优先邀请在论坛持续输出高质量内容的园丁,以及愿意参与玩法共创的核心用户。</p>
<p>内测的重点不是「好不好玩」,而是「哪里别扭」——你的每一条吐槽,都会直接进入我们的周度复盘会。</p>
</div>
</div>
</div>
</div> </div>
</section> </section>

View File

@ -619,51 +619,49 @@ body { background: #0a0e17; overflow: hidden; font-family: 'Microsoft YaHei', sa
</div> </div>
<div class="ua-scroll"> <div class="ua-scroll">
<!-- 缔造者等级卡 --> <!-- 缔造者等级卡(数据从 me() 动态填充) -->
<section class="ua-card"> <section class="ua-card">
<div class="ua-card-top"> <div class="ua-card-top">
<span class="ua-lv">Lv.<b id="uaLv">12</b></span> <span class="ua-lv">Lv.<b id="uaLv">--</b></span>
<span class="ua-title">星界旅人</span> <span class="ua-title" id="uaTitle">--</span>
</div> </div>
<div class="ua-exp"><div class="ua-exp-fill" style="width:64%"></div></div> <div class="ua-exp"><div class="ua-exp-fill" id="uaExpBar" style="width:0%"></div></div>
<div class="ua-exp-txt">EXP 640 / 1000 · 距下一级还差 360</div> <div class="ua-exp-txt" id="uaExpTxt">EXP -- / --</div>
</section> </section>
<!-- 我的世界 概览 --> <!-- 我的世界 概览(数据从 me() 动态填充) -->
<section class="ua-card"> <section class="ua-card">
<div class="ua-card-h">🌍 我的世界</div> <div class="ua-card-h">🌍 我的世界</div>
<div class="ua-world-name" id="uaWorldName">我的世界</div> <div class="ua-world-name" id="uaWorldName">--</div>
<div class="ua-grid"> <div class="ua-grid">
<div class="ua-stat"><div class="ua-stat-v" id="uaEra">星元1年·春</div><div class="ua-stat-l">历元</div></div> <div class="ua-stat"><div class="ua-stat-v" id="uaEra">--</div><div class="ua-stat-l">历元</div></div>
<div class="ua-stat"><div class="ua-stat-v" id="uaTiles">0</div><div class="ua-stat-l">疆域地块</div></div> <div class="ua-stat"><div class="ua-stat-v" id="uaTiles">--</div><div class="ua-stat-l">疆域地块</div></div>
<div class="ua-stat"><div class="ua-stat-v">382</div><div class="ua-stat-l">人口</div></div> <div class="ua-stat"><div class="ua-stat-v" id="uaPop">--</div><div class="ua-stat-l">人口</div></div>
<div class="ua-stat"><div class="ua-stat-v">68%</div><div class="ua-stat-l">文明度</div></div> <div class="ua-stat"><div class="ua-stat-v" id="uaCiv">--</div><div class="ua-stat-l">文明度</div></div>
</div> </div>
<div class="ua-res"> <div class="ua-res">
<div class="ua-res-row"><span>🪙 金币</span><span class="ua-res-bar"><i style="width:80%"></i></span><b>1280</b></div> <div class="ua-res-row"><span>🪙 金币</span><span class="ua-res-bar"><i id="uaCoinBar" style="width:0%"></i></span><b id="uaCoins">--</b></div>
<div class="ua-res-row"><span>✨ 灵气</span><span class="ua-res-bar"><i style="width:45%"></i></span><b>540</b></div> <div class="ua-res-row"><span>✨ 灵气</span><span class="ua-res-bar"><i id="uaEnergyBar" style="width:0%"></i></span><b id="uaEnergy">--</b></div>
<div class="ua-res-row"><span>🪵 木材</span><span class="ua-res-bar"><i style="width:60%"></i></span><b>360</b></div> <div class="ua-res-row"><span>🪵 木材</span><span class="ua-res-bar"><i id="uaWoodBar" style="width:0%"></i></span><b id="uaWood">--</b></div>
<div class="ua-res-row"><span>🪨 石料</span><span class="ua-res-bar"><i style="width:30%"></i></span><b>180</b></div> <div class="ua-res-row"><span>🪨 石料</span><span class="ua-res-bar"><i id="uaStoneBar" style="width:0%"></i></span><b id="uaStone">--</b></div>
</div> </div>
</section> </section>
<!-- 成就 --> <!-- 成就预览(数据从 API.achievements() 动态填充) -->
<section class="ua-card"> <section class="ua-card">
<div class="ua-card-h">🏆 成就</div> <div class="ua-card-h">🏆 成就 <span id="uaAchSummary" style="font-size:11px;color:var(--text-dim);float:right;">-- / --</span></div>
<div class="ua-ach"> <div class="ua-ach" id="uaAchPreview">
<div class="ua-ach-item done"><span class="ua-ach-ic">🌟</span><div><div class="ua-ach-t">开天辟地</div><div class="ua-ach-d">已点亮第一块领土</div></div></div> <!-- 成就预览项由 JS 动态渲染 -->
<div class="ua-ach-item"><span class="ua-ach-ic">🌱</span><div><div class="ua-ach-t">文明摇篮</div><div class="ua-ach-bar"><i style="width:60%"></i></div></div></div>
<div class="ua-ach-item locked"><span class="ua-ach-ic">🔒</span><div><div class="ua-ach-t">通天塔</div><div class="ua-ach-d">建造 10 座奇观解锁</div></div></div>
</div> </div>
</section> </section>
<!-- 快捷操作 --> <!-- 快捷操作 -->
<div class="ua-actions"> <div class="ua-actions">
<button class="ua-item" id="uaProfile" type="button">👤 个人信息</button> <button class="ua-item" id="uaProfile" type="button">👤 个人</button>
<button class="ua-item" id="uaAchievements" type="button">🏆 成就</button>
<button class="ua-item" id="uaMessages" type="button">✉️ 邮箱 <span class="ua-badge" id="uaMailBadge" style="display:none;">0</span></button>
<button class="ua-item" id="uaSettings" type="button">⚙️ 设置</button> <button class="ua-item" id="uaSettings" type="button">⚙️ 设置</button>
<button class="ua-item" id="uaMessages" type="button">✉️ 邮件 <span class="ua-badge">3</span></button> <button class="ua-item" id="uaWebsite" type="button">🌐 官网</button>
<button class="ua-item" id="uaAchievements" type="button">🏆 成就墙</button>
<button class="ua-item" id="uaWebsite" type="button">🌐 官网 / 社区</button>
</div> </div>
</div> </div>
@ -4364,17 +4362,94 @@ if (typeof fitWorld === 'function') fitWorld();
trigger.setAttribute('aria-expanded', open ? 'true' : 'false'); trigger.setAttribute('aria-expanded', open ? 'true' : 'false');
}; };
// 面板内容(游戏视角 demo同步真实可读字段 // === 面板数据填充(从 me() 返回值动态渲染) ===
const cachedMe = me; // 保存供弹窗复用
function fillPanelData(d) {
const name = d.name || d.login || '';
const uaName2 = document.getElementById('uaName2'); const uaName2 = document.getElementById('uaName2');
if (uaName2) uaName2.textContent = name;
const uaAv = document.getElementById('uaAvatarLg'); const uaAv = document.getElementById('uaAvatarLg');
const wnEl2 = document.getElementById('worldName'); if (uaAv) uaAv.textContent = (name || 'M').trim().charAt(0).toUpperCase();
const eraEl = document.getElementById('calendarLabel'); const uaUid = document.getElementById('uaUid');
const tilesEl = document.getElementById('uaTiles'); if (uaUid) uaUid.textContent = d.game_uid || d.uid || '--';
if (uaName2) uaName2.textContent = me.name || me.login; // 等级卡
if (uaAv) uaAv.textContent = (me.name || me.login || 'M').trim().charAt(0).toUpperCase(); const uaLv = document.getElementById('uaLv');
if (wnEl2) { const wn = document.getElementById('uaWorldName'); if (wn) wn.textContent = wnEl2.textContent; } if (uaLv) uaLv.textContent = d.level || 1;
if (eraEl) { const er = document.getElementById('uaEra'); if (er) er.textContent = eraEl.textContent; } const uaTitle = document.getElementById('uaTitle');
if (tilesEl && typeof tiles !== 'undefined' && Array.isArray(tiles)) tilesEl.textContent = tiles.length; if (uaTitle) uaTitle.textContent = d.title || '萌新';
const expMax = d.exp_max || 100;
const expPct = expMax > 0 ? Math.min(100, (d.exp || 0) / expMax * 100) : 0;
const uaExpBar = document.getElementById('uaExpBar');
if (uaExpBar) uaExpBar.style.width = expPct + '%';
const uaExpTxt = document.getElementById('uaExpTxt');
if (uaExpTxt) uaExpTxt.textContent = 'EXP ' + (d.exp || 0) + ' / ' + expMax;
// 世界概览
const uaWorldName = document.getElementById('uaWorldName');
if (uaWorldName) uaWorldName.textContent = d.world_name || '我的世界';
const uaEra = document.getElementById('uaEra');
if (uaEra) uaEra.textContent = d.era || '--';
const uaTiles = document.getElementById('uaTiles');
if (uaTiles) uaTiles.textContent = d.tiles || 0;
const uaPop = document.getElementById('uaPop');
if (uaPop) uaPop.textContent = d.population || 0;
const uaCiv = document.getElementById('uaCiv');
if (uaCiv) uaCiv.textContent = d.civ_level || 0;
// 资源条(按上限折算宽度)
function setRes(barId, valId, val, max) {
const el = document.getElementById(valId);
if (el) el.textContent = val || 0;
const bar = document.getElementById(barId);
if (bar) bar.style.width = (max > 0 ? Math.min(100, (val || 0) / max * 100) : 0) + '%';
}
setRes('uaCoinBar', 'uaCoins', d.coins, 10000);
setRes('uaEnergyBar', 'uaEnergy', d.energy, 1000);
setRes('uaWoodBar', 'uaWood', d.wood, 1000);
setRes('uaStoneBar', 'uaStone', d.stone, 1000);
// 邮件未读角标
const uaMailBadge = document.getElementById('uaMailBadge');
if (uaMailBadge) {
const n = d.unread_mail || 0;
uaMailBadge.textContent = n;
uaMailBadge.style.display = n > 0 ? '' : 'none';
}
}
fillPanelData(me);
// === 异步加载成就预览 + 邮件未读数 ===
(async () => {
try {
const ach = await API.achievements();
if (ach && ach.items) {
const summary = document.getElementById('uaAchSummary');
if (summary) summary.textContent = (ach.unlocked_count || 0) + ' / ' + (ach.total_count || 0);
const preview = document.getElementById('uaAchPreview');
if (preview) {
const top3 = ach.items.slice(0, 3);
preview.innerHTML = top3.length ? top3.map(a => {
const icon = a.unlocked ? (a.icon || '🏆') : '🔒';
const pct = a.target > 0 ? Math.min(100, (a.progress || 0) / a.target * 100) : (a.unlocked ? 100 : 0);
return `<div class="ua-ach-item${a.unlocked ? ' done' : ''}">
<span class="ua-ach-ic">${icon}</span>
<div style="flex:1;min-width:0;">
<div class="ua-ach-t">${a.name}</div>
<div class="ua-ach-bar"><i style="width:${pct}%"></i></div>
</div>
</div>`;
}).join('') : '<div style="font-size:12px;color:#8899aa;text-align:center;padding:12px;">暂无成就数据</div>';
}
}
} catch (e) { console.warn('加载成就预览失败', e); }
try {
const ml = await API.mails();
if (ml && typeof ml.unread === 'number') {
const badge = document.getElementById('uaMailBadge');
if (badge) {
badge.textContent = ml.unread;
badge.style.display = ml.unread > 0 ? '' : 'none';
}
}
} catch (e) { console.warn('加载邮件数失败', e); }
})();
trigger.addEventListener('click', (ev) => { trigger.addEventListener('click', (ev) => {
ev.stopPropagation(); ev.stopPropagation();
@ -4418,27 +4493,40 @@ if (typeof fitWorld === 'function') fitWorld();
if (e.target === appModal) closeAppModal(); if (e.target === appModal) closeAppModal();
}); });
// 个人信息 // 个人信息(从 cachedMe 动态渲染)
const uaProfile = document.getElementById('uaProfile'); const uaProfile = document.getElementById('uaProfile');
if (uaProfile) uaProfile.addEventListener('click', () => { if (uaProfile) uaProfile.addEventListener('click', () => {
const d = cachedMe;
const name = d.name || d.login || '';
const avatarLetter = (name || 'M').trim().charAt(0).toUpperCase();
const expMax = d.exp_max || 100;
const expPct = expMax > 0 ? Math.min(100, (d.exp || 0) / expMax * 100) : 0;
openAppModal('👤 个人信息', ` openAppModal('👤 个人信息', `
<div style="display:flex;align-items:center;gap:14px;margin-bottom:14px;"> <div style="display:flex;align-items:center;gap:14px;margin-bottom:14px;">
<div style="width:54px;height:54px;border-radius:50%;background:linear-gradient(135deg,#c77dff,#4af);display:flex;align-items:center;justify-content:center;font-size:22px;font-weight:700;color:#fff;box-shadow:0 0 14px rgba(199,125,255,.5);">M</div> <div style="width:54px;height:54px;border-radius:50%;background:linear-gradient(135deg,#c77dff,#4af);display:flex;align-items:center;justify-content:center;font-size:22px;font-weight:700;color:#fff;box-shadow:0 0 14px rgba(199,125,255,.5);">${avatarLetter}</div>
<div> <div>
<div style="font-size:16px;font-weight:700;color:#e0e0e0;">Mitchell</div> <div style="font-size:16px;font-weight:700;color:#e0e0e0;">${name}</div>
<div style="font-size:12px;color:#8899aa;">UID: 10086001</div> <div style="font-size:12px;color:#8899aa;">UID: ${d.game_uid || d.uid || '--'}</div>
</div> </div>
</div> </div>
<div class="infoRow"><span class="infoLabel">等级</span><span class="infoValue">Lv.12</span></div> <div class="infoRow"><span class="infoLabel">等级</span><span class="infoValue">Lv.${d.level || 1}</span></div>
<div style="margin:6px 0 10px;"> <div style="margin:6px 0 10px;">
<div style="height:8px;background:rgba(255,255,255,.08);border-radius:4px;overflow:hidden;"> <div style="height:8px;background:rgba(255,255,255,.08);border-radius:4px;overflow:hidden;">
<div style="height:100%;width:64%;background:linear-gradient(90deg,#8b5cf6,#4af);border-radius:4px;"></div> <div style="height:100%;width:${expPct}%;background:linear-gradient(90deg,#8b5cf6,#4af);border-radius:4px;"></div>
</div> </div>
<div style="font-size:11px;color:#8899aa;text-align:right;margin-top:3px;">EXP 640 / 1000</div> <div style="font-size:11px;color:#8899aa;text-align:right;margin-top:3px;">EXP ${d.exp || 0} / ${expMax}</div>
</div> </div>
<div class="infoRow"><span class="infoLabel">金币</span><span class="infoValue">🪙 1280</span></div> <div class="infoRow"><span class="infoLabel">称号</span><span class="infoValue">${d.title || '萌新'}</span></div>
<div class="infoRow"><span class="infoLabel">世界</span><span class="infoValue">星元 1年 · 春</span></div> <div class="infoRow"><span class="infoLabel">金币</span><span class="infoValue">🪙 ${d.coins || 0}</span></div>
<div class="infoRow"><span class="infoLabel">注册</span><span class="infoValue">2026-07-20</span></div> <div class="infoRow"><span class="infoLabel">灵气</span><span class="infoValue">✨ ${d.energy || 0}</span></div>
<div class="infoRow"><span class="infoLabel">木材</span><span class="infoValue">🪵 ${d.wood || 0}</span></div>
<div class="infoRow"><span class="infoLabel">石料</span><span class="infoValue">🪨 ${d.stone || 0}</span></div>
<div class="infoRow"><span class="infoLabel">世界</span><span class="infoValue">${d.world_name || '我的世界'}</span></div>
<div class="infoRow"><span class="infoLabel">历元</span><span class="infoValue">${d.era || '--'}</span></div>
<div class="infoRow"><span class="infoLabel">疆域</span><span class="infoValue">${d.tiles || 0} 块</span></div>
<div class="infoRow"><span class="infoLabel">人口</span><span class="infoValue">${d.population || 0}</span></div>
<div class="infoRow"><span class="infoLabel">文明度</span><span class="infoValue">${d.civ_level || 0}</span></div>
<div class="infoRow"><span class="infoLabel">注册</span><span class="infoValue">${d.reg_date || '--'}</span></div>
`); `);
}); });
@ -4475,57 +4563,81 @@ if (typeof fitWorld === 'function') fitWorld();
}; };
}); });
// 邮件 // 邮件(从 API.mails() 动态取值)
const uaMessages = document.getElementById('uaMessages'); const uaMessages = document.getElementById('uaMessages');
if (uaMessages) uaMessages.addEventListener('click', () => { if (uaMessages) uaMessages.addEventListener('click', async () => {
const mails = [ openAppModal('✉️ 邮件', '<div style="text-align:center;padding:20px;color:#8899aa;font-size:13px;">加载中…</div>');
{ cat: '系统', title: '欢迎来到宇森', date: '2026-07-26', body: '请查收新手礼包与世界探索指南。', unread: true }, let mails = [];
{ cat: '系统', title: '版本更新 v0.2', date: '2026-07-25', body: '新增世界导航与微观视图功能。', unread: false }, try {
{ cat: '活动', title: '夏日创作大赛', date: '2026-07-24', body: '上传你的世界截图,赢限定头像框。', unread: true } const resp = await API.mails();
]; mails = (resp && resp.items) || [];
const cats = ['全部', '系统', '活动']; } catch (e) {
if (appModalBody) appModalBody.innerHTML = '<div class="mailEmpty">邮件加载失败</div>';
return;
}
const cats = ['全部', ...new Set(mails.map(m => m.category_label).filter(Boolean))];
function renderMail(cat) { function renderMail(cat) {
const list = cat === '全部' ? mails : mails.filter(m => m.cat === cat); const list = cat === '全部' ? mails : mails.filter(m => m.category_label === cat);
const tabs = cats.map(c => `<span class="mailTab${c === cat ? ' active' : ''}" data-cat="${c}">${c}</span>`).join(''); const tabs = cats.map(c => `<span class="mailTab${c === cat ? ' active' : ''}" data-cat="${c}">${c}</span>`).join('');
const items = list.length const items = list.length
? list.map(m => `<div class="mailItem"><div class="mailTitle">${m.unread ? '● ' : ''}${m.title}</div><div class="mailMeta">${m.cat} · ${m.date}</div><div>${m.body}</div></div>`).join('') ? list.map(m => `<div class="mailItem" data-id="${m.id}" style="cursor:pointer;${!m.is_read ? 'border-left:3px solid #8b5cf6;' : ''}">
<div class="mailTitle">${!m.is_read ? '● ' : ''}${m.subject || '(无标题)'}</div>
<div class="mailMeta">${m.category_label || ''} · ${m.date || ''}${m.sender ? ' · ' + m.sender : ''}</div>
<div>${m.body || ''}</div>
</div>`).join('')
: '<div class="mailEmpty">暂无邮件</div>'; : '<div class="mailEmpty">暂无邮件</div>';
if (appModalBody) appModalBody.innerHTML = `<div class="mailTabs">${tabs}</div>${items}`; if (appModalBody) appModalBody.innerHTML = `<div class="mailTabs">${tabs}</div>${items}`;
appModalBody.querySelectorAll('.mailTab').forEach(t => { t.onclick = () => renderMail(t.dataset.cat); }); appModalBody.querySelectorAll('.mailTab').forEach(t => { t.onclick = () => renderMail(t.dataset.cat); });
appModalBody.querySelectorAll('.mailItem').forEach(item => {
item.onclick = async () => {
const id = parseInt(item.dataset.id);
if (!id) return;
try { await API.mailRead(id); } catch (e) {}
const m = mails.find(x => x.id === id);
if (m) m.is_read = true;
const badge = document.getElementById('uaMailBadge');
const unread = mails.filter(x => !x.is_read).length;
if (badge) { badge.textContent = unread; badge.style.display = unread > 0 ? '' : 'none'; }
const activeTab = appModalBody.querySelector('.mailTab.active');
renderMail(activeTab ? activeTab.dataset.cat : '全部');
};
});
} }
openAppModal('✉️ 邮件', '');
renderMail('全部'); renderMail('全部');
}); });
// 成就 // 成就(从 API.achievements() 动态取值)
const uaAchievements = document.getElementById('uaAchievements'); const uaAchievements = document.getElementById('uaAchievements');
if (uaAchievements) uaAchievements.addEventListener('click', () => { if (uaAchievements) uaAchievements.addEventListener('click', async () => {
openAppModal('🏆 成就', ` openAppModal('🏆 成就', '<div style="text-align:center;padding:20px;color:#8899aa;font-size:13px;">加载中…</div>');
let ach = { items: [], total_points: 0, unlocked_count: 0, total_count: 0 };
try {
ach = await API.achievements();
} catch (e) {
if (appModalBody) appModalBody.innerHTML = '<div style="text-align:center;padding:20px;color:#8899aa;">成就加载失败</div>';
return;
}
const items = ach.items || [];
const html = `
<div style="display:flex;justify-content:space-between;margin-bottom:14px;"> <div style="display:flex;justify-content:space-between;margin-bottom:14px;">
<div><div style="font-size:22px;font-weight:700;color:#e0e0e0;">320</div><div style="font-size:11px;color:#8899aa;">成就点数</div></div> <div><div style="font-size:22px;font-weight:700;color:#e0e0e0;">${ach.total_points || 0}</div><div style="font-size:11px;color:#8899aa;">成就点数</div></div>
<div style="text-align:right;"><div style="font-size:22px;font-weight:700;color:#e0e0e0;">8 / 20</div><div style="font-size:11px;color:#8899aa;">已解锁</div></div> <div style="text-align:right;"><div style="font-size:22px;font-weight:700;color:#e0e0e0;">${ach.unlocked_count || 0} / ${ach.total_count || 0}</div><div style="font-size:11px;color:#8899aa;">已解锁</div></div>
</div> </div>
<div class="achItem done"> ${items.length ? items.map(a => {
<div class="achIcon">🏆</div> const pct = a.target > 0 ? Math.min(100, (a.progress || 0) / a.target * 100) : (a.unlocked ? 100 : 0);
<div class="achMain"><div class="achName">初临世界</div><div class="achDesc">创建你的第一个世界</div></div> const icon = a.unlocked ? (a.icon || '🏆') : '🔒';
<div class="achState">已达成</div> return `<div class="achItem${a.unlocked ? ' done' : ''}">
<div class="achIcon${a.unlocked ? '' : ' locked'}">${icon}</div>
<div class="achMain">
<div class="achName">${a.name}</div>
<div class="achDesc">${a.desc || ''}${a.tier_label ? ' [' + a.tier_label + ']' : ''} · ${a.points}点</div>
${a.unlocked ? '' : `<div class="achBar"><div class="achFill" style="width:${pct}%"></div></div>`}
</div> </div>
<div class="achItem done"> <div class="achState">${a.unlocked ? '已达成' : Math.round(pct) + '%'}</div>
<div class="achIcon">🌍</div> </div>`;
<div class="achMain"><div class="achName">五界行者</div><div class="achDesc">解锁全部五层世界</div></div> }).join('') : '<div style="text-align:center;padding:20px;color:#8899aa;font-size:13px;">暂无成就数据</div>'}
<div class="achState">已达成</div> `;
</div> if (appModalBody) appModalBody.innerHTML = html;
<div class="achItem">
<div class="achIcon locked">🔒</div>
<div class="achMain"><div class="achName">大地建筑师</div><div class="achDesc">在大地世界放置 50 个地块</div><div class="achBar"><div class="achFill" style="width:40%"></div></div></div>
<div class="achState">40%</div>
</div>
<div class="achItem">
<div class="achIcon locked">🔒</div>
<div class="achMain"><div class="achName">熔岩征服者</div><div class="achDesc">在地心世界停留 10 分钟</div><div class="achBar"><div class="achFill" style="width:15%"></div></div></div>
<div class="achState">15%</div>
</div>
`);
}); });
} catch (e) { } catch (e) {
location.replace('login.html?next=world.html'); return; location.replace('login.html?next=world.html'); return;