diff --git a/addons/game_base/__init__.py b/addons/game_base/__init__.py
index f7209b1..6ecf639 100644
--- a/addons/game_base/__init__.py
+++ b/addons/game_base/__init__.py
@@ -1,2 +1,10 @@
from . import models
from . import controllers
+
+
+def pre_init_hook(cr):
+ """模块升级前清理旧的 game.currency 记录,避免模型删除时产生冲突。"""
+ cr.execute("SELECT 1 FROM information_schema.tables WHERE table_name='game_currency'")
+ if cr.fetchone():
+ cr.execute("DELETE FROM game_currency")
+ cr.execute("DELETE FROM ir_model_data WHERE model='game.currency'")
diff --git a/addons/game_base/__manifest__.py b/addons/game_base/__manifest__.py
index 6fd9f7c..8fd8aba 100644
--- a/addons/game_base/__manifest__.py
+++ b/addons/game_base/__manifest__.py
@@ -1,16 +1,24 @@
{
- 'name': 'Yusen Game Base',
+ 'name': '宇森游戏基础数据',
'summary': '宇森游戏基础数据:图鉴 / 规则 / 论坛 / 公告',
- 'version': '1.1',
+ 'version': '1.2',
'category': 'Game',
'sequence': 10,
'depends': ['base'],
'data': [
'security/ir.model.access.csv',
'data/game_base_data.xml',
- 'views/game_base_views.xml',
+ 'views/codex_views.xml',
+ 'views/rule_category_views.xml',
+ 'views/rule_views.xml',
+ 'views/page_views.xml',
+ 'views/forum_views.xml',
+ 'views/announcement_views.xml',
+ 'views/story_views.xml',
+ 'views/menu.xml',
],
'application': True,
'license': 'LGPL-3',
+ 'pre_init_hook': 'pre_init_hook',
'description': '基于官网内容沉淀的游戏基础数据模型,供后台维护、前端经 /game/api/* 读取;并提供与 Odoo 用户打通的登录/注册接口。',
}
diff --git a/addons/game_base/controllers/game_api.py b/addons/game_base/controllers/game_api.py
index f57e62f..02ecff3 100644
--- a/addons/game_base/controllers/game_api.py
+++ b/addons/game_base/controllers/game_api.py
@@ -63,42 +63,86 @@ class GameApiController(http.Controller):
@http.route('/game/api/rules', type='http', auth='public', methods=['GET'], csrf=False)
def rules(self, **kw):
- """规则 / 等级制度:按类别(type)分组。"""
- model = request.env['game.rule'].sudo()
- type_labels = dict(model._fields['type'].selection)
- recs = model.search_read([], ['id', 'type', 'level', 'name', 'desc'])
- by_type = {}
+ """规则 / 等级制度 / 货币:按类别分组。"""
+ Rule = request.env['game.rule'].sudo()
+ Category = request.env['game.rule.category'].sudo()
+ cats = Category.search_read(
+ [('active', '=', True)],
+ ['id', 'key', 'name', 'sequence'],
+ order='sequence, id'
+ )
+ cat_map = {c['id']: c for c in cats}
+ recs = Rule.search_read(
+ [],
+ ['id', 'category_id', 'level', 'name', 'desc', 'rate', 'note'],
+ order='category_id, level'
+ )
+ by_cat = {}
for r in recs:
- by_type.setdefault(r['type'], []).append(r)
+ cid = r['category_id'][0] if r['category_id'] else None
+ if cid not in cat_map:
+ continue
+ by_cat.setdefault(cid, []).append(r)
groups = []
- for t in by_type:
+ for c in cats:
+ items = by_cat.get(c['id'], [])
+ if not items:
+ continue
groups.append({
- 'key': t,
- 'label': type_labels.get(t, t),
+ 'key': c['key'],
+ 'label': c['name'],
'items': [{
- 'id': r['id'],
- 'level': r['level'],
- 'name': r['name'],
- 'desc': r['desc'] or '',
- } for r in by_type[t]],
+ 'id': it['id'],
+ 'level': it['level'],
+ 'name': it['name'],
+ 'desc': it['desc'] or '',
+ 'rate': it.get('rate') or 1,
+ 'note': it.get('note') or '',
+ } for it in items],
})
return _json({'groups': groups})
@http.route('/game/api/currencies', type='http', auth='public', methods=['GET'], csrf=False)
def currencies(self, **kw):
- """货币等级:按 level 排序返回。"""
- recs = request.env['game.currency'].sudo().search_read(
- [], ['id', 'level', 'name', 'desc', 'rate'], order='level asc'
+ """货币等级:兼容旧接口,实际从 game.rule 按类别过滤。"""
+ Rule = request.env['game.rule'].sudo()
+ Category = request.env['game.rule.category'].sudo()
+ currency_keys = ['currency', 'material', 'token', 'honor']
+ cats = Category.search_read(
+ [('active', '=', True), ('key', 'in', currency_keys)],
+ ['id', 'key', 'name', 'sequence'],
+ order='sequence, id'
)
- return _json({
- 'items': [{
- 'id': r['id'],
- 'level': r['level'],
- 'name': r['name'],
- 'desc': r['desc'] or '',
- 'rate': r['rate'] or 1,
- } for r in recs]
- })
+ cat_map = {c['id']: c for c in cats}
+ recs = Rule.search_read(
+ [('category_id.key', 'in', currency_keys)],
+ ['id', 'category_id', 'level', 'name', 'desc', 'note', 'rate'],
+ order='category_id, level'
+ )
+ by_type = {}
+ for r in recs:
+ cid = r['category_id'][0] if r['category_id'] else None
+ if cid not in cat_map:
+ continue
+ by_type.setdefault(cid, []).append(r)
+ groups = []
+ for c in cats:
+ items = by_type.get(c['id'], [])
+ if not items:
+ continue
+ groups.append({
+ 'key': c['key'],
+ 'label': c['name'],
+ 'items': [{
+ 'id': it['id'],
+ 'level': it['level'],
+ 'name': it['name'],
+ 'desc': it['desc'] or '',
+ 'note': it.get('note') or '',
+ 'rate': it.get('rate') or 1,
+ } for it in items],
+ })
+ return _json({'groups': groups})
@http.route('/game/api/pages', type='http', auth='public', methods=['GET'], csrf=False)
def pages(self, **kw):
diff --git a/addons/game_base/data/game_base_data.xml b/addons/game_base/data/game_base_data.xml
index 5c74301..df6d5f5 100644
--- a/addons/game_base/data/game_base_data.xml
+++ b/addons/game_base/data/game_base_data.xml
@@ -1,93 +1,127 @@
+
+
+ world
+ 世界等级
+ 10
+
+
+ faction
+ 势力等级
+ 20
+
+
+ character
+ 人物角色等级
+ 30
+
+
+ currency
+ 货币等级
+ 40
+
+
- world1凡墟
+ 1凡墟
- world2灵启
+ 2灵启
- world3玄界
+ 3玄界
- world4穹冥
+ 4穹冥
- world5溯元
+ 5溯元
- faction1墟族
+ 1墟族
- faction2灵聚
+ 2灵聚
- faction3玄邑
+ 3玄邑
- faction4冥朝
+ 4冥朝
- faction5穹统
+ 5穹统
- faction6溯盟
+ 6溯盟
- character1萌新
- character2学徒
- character3熟手
- character4好手
- character5精英
- character6大师
- character7宗匠
- character8传奇
- character9神话
- character10半神
- character11圣者
- character12永恒
+ 1萌新
+ 2学徒
+ 3熟手
+ 4好手
+ 5精英
+ 6大师
+ 7宗匠
+ 8传奇
+ 9神话
+ 10半神
+ 11圣者
+ 12永恒
-
-
- 1墟铢100
+
+
+ 1墟铢
+ 100
+
-
- 2灵币100
+
+ 2灵币
+ 100
+
-
- 3玄锭100
+
+ 3玄锭
+ 100
+
-
- 4冥玉100
+
+ 4冥玉
+ 100
+
-
- 5穹鎏100
+
+ 5穹鎏
+ 100
+
-
- 6元玺1
+
+ 6元玺
+ 1
+
@@ -183,6 +217,32 @@
每个 NPC 都有独立的天赋、性格与成长轨迹。英雄在历练中觉醒,传奇在时间中自发生成,无人是棋子。
+
+
+ intro
+ 关于宇森
+ ABOUT YUSEN
+ 15
+ 宇森,是一颗会自己呼吸的种子。
+你播种的不是一个角色,而是一个世界——从地心熔岩到陨石星空,五层垂直切面层层相托,文明在其中自发地生长、分裂、兼并、飞升。没有脚本替你演绎剧情,没有关卡替你定义胜利,每一次开局都是一部独一无二的编年史。
+我们相信,最好的模拟不在于你操控了多少,而在于你见证了多少。所以宇森把「玩家」重新定义为园丁:你不能命令谁,却可以改变土壤、调节灵气、在关键节点轻轻推一把。世界的走向,由无数个体的选择汇聚而成,而你只是其中一股温柔的力。
+往下翻,看这颗种子如何长成一片森林。
]]>
+
+
+
+ 10
+ 🌍
+ 一颗会呼吸的世界
+ 五层垂直世界,文明自发演化,没有固定剧情与通关条件。
+
+
+
+ 20
+ 🧑🌾
+ 你是园丁,不是上帝
+ 改变土壤与灵气,在关键节点轻推一把,而非发号施令。
+
+
background
@@ -233,6 +293,48 @@
本源之地 · 元玺凝聚
+
+
+ notes
+ 制度说明
+ HOW THE WORLD WORKS
+ 25
+ 宇森的世界由三套相互独立的等级体系共同驱动,它们彼此加成,又彼此制约:
+世界等级是舞台的上限——它决定这片土地上「能发生什么」。从凡墟到溯元,每一级跃迁都会解锁更深的内容层(更深的地下、更远的星空)与更厚的事件池。
+势力等级是群体的进度——它决定文明「能调动什么」。聚落、联盟到多维联合体,等级越高,可调度资源与外交权重越大。
+角色等级是个体的成长——它决定一个生命「能做成什么」。从无名萌新到超越生死的永恒,英雄在历练中觉醒。
+而贯穿三者的,是六档货币:从凡俗墟铢到本源元玺,百进制兑换构成完整经济闭环,决定文明「能交换什么」。
+园丁的核心策略,便是在这四方之间分配有限的引导资源,让文明自洽地向上演化——每一级提升,世界都因此越活越厚。
]]>
+
+
+
+ 10
+ 🌐
+ 世界等级
+ 舞台上限:决定能发生什么。凡墟 → 灵启 → 玄界 → 穹冥 → 溯元。
+
+
+
+ 20
+ 🏛️
+ 势力等级
+ 群体进度:决定能调动什么。墟族 → 灵聚 → 玄邑 → 冥朝 → 穹统 → 溯盟。
+
+
+
+ 30
+ ⚔️
+ 角色等级
+ 个体成长:决定能做成什么。萌新 → …… → 永恒,十二阶序。
+
+
+
+ 40
+ 💰
+ 六档货币
+ 经济闭环:决定能交换什么。墟铢 → 灵币 → 玄锭 → 冥玉 → 穹鎏 → 元玺。
+
+
letter
diff --git a/addons/game_base/migrations/1.2/pre-migrate.py b/addons/game_base/migrations/1.2/pre-migrate.py
new file mode 100644
index 0000000..f1d6c87
--- /dev/null
+++ b/addons/game_base/migrations/1.2/pre-migrate.py
@@ -0,0 +1,9 @@
+# -*- coding: utf-8 -*-
+
+def migrate(cr, version):
+ """升级到 1.2:清理旧的 game.currency 记录与外部 ID,避免合并到 game.rule 时冲突。"""
+ # 先删数据表记录,再删对应 ir.model.data 行
+ cr.execute("SELECT 1 FROM information_schema.tables WHERE table_name='game_currency'")
+ if cr.fetchone():
+ cr.execute("DELETE FROM game_currency")
+ cr.execute("DELETE FROM ir_model_data WHERE model='game.currency'")
diff --git a/addons/game_base/models/__init__.py b/addons/game_base/models/__init__.py
index 5ecebe0..42dfeb2 100644
--- a/addons/game_base/models/__init__.py
+++ b/addons/game_base/models/__init__.py
@@ -1,7 +1,7 @@
from . import codex
from . import forum
from . import announcement
+from . import rule_category
from . import rule
-from . import currency
from . import page
from . import story
diff --git a/addons/game_base/models/currency.py b/addons/game_base/models/currency.py
deleted file mode 100644
index 4e4e0fd..0000000
--- a/addons/game_base/models/currency.py
+++ /dev/null
@@ -1,42 +0,0 @@
-# -*- coding: utf-8 -*-
-from odoo import models, fields, api
-
-
-class GameCurrency(models.Model):
- _name = 'game.currency'
- _description = '游戏货币等级'
- _order = 'level asc'
-
- level = fields.Integer(string='等级', required=True)
- name = fields.Char(string='名称', required=True)
- desc = fields.Text(string='描述')
- rate = fields.Integer(
- string='兑换比例',
- default=1,
- help='多少单位本货币可兑换 1 单位下一等级货币。例如 100 墟铢 = 1 灵币,则墟铢的 rate 为 100。'
- )
-
- @api.model
- def convert(self, amount, from_level, to_level):
- """按等级汇率将 amount 从 from_level 换算为 to_level 货币。
-
- 汇率方向:低等级 → 高等级 为除以 rate,高等级 → 低等级 为乘以 rate。
- 最高等级货币(元玺)的 rate 固定为 1,仅作占位。
- """
- if from_level == to_level:
- return amount
- currencies = self.search_read([], ['level', 'rate'], order='level asc')
- rate_map = {c['level']: c['rate'] or 1 for c in currencies}
-
- if from_level < to_level:
- # 低等级 → 高等级:逐级除以兑换比例
- result = amount
- for lv in range(from_level, to_level):
- result = result / rate_map.get(lv, 1)
- return result
- else:
- # 高等级 → 低等级:逐级乘以兑换比例
- result = amount
- for lv in range(to_level, from_level):
- result = result * rate_map.get(lv, 1)
- return result
diff --git a/addons/game_base/models/rule.py b/addons/game_base/models/rule.py
index cdf9ca1..0b6c76f 100644
--- a/addons/game_base/models/rule.py
+++ b/addons/game_base/models/rule.py
@@ -1,19 +1,64 @@
-from odoo import models, fields
+# -*- coding: utf-8 -*-
+from odoo import models, fields, api
class GameRule(models.Model):
_name = 'game.rule'
- _description = '规则 / 等级制度'
+ _description = '规则 / 等级制度 / 货币'
_rec_name = 'name'
- _order = 'type, level'
+ _order = 'category_id, level'
- TYPE = [
- ('world', '世界等级'),
- ('faction', '势力等级'),
- ('character', '人物角色等级'),
- ]
-
- type = fields.Selection(TYPE, string='类别', required=True, index=True)
- level = fields.Integer(string='等级', index=True)
+ category_id = fields.Many2one(
+ 'game.rule.category',
+ string='类别',
+ required=True,
+ index=True,
+ ondelete='restrict',
+ )
+ level = fields.Integer(
+ string='等级',
+ index=True,
+ aggregator=False, # 分组时不累计
+ )
name = fields.Char(string='名称', required=True)
desc = fields.Html(string='说明')
+ rate = fields.Integer(
+ string='兑换比例',
+ default=1,
+ help='多少单位本货币可兑换 1 单位下一等级货币。例如 100 墟铢 = 1 灵币,则墟铢的 rate 为 100。仅货币类有效。',
+ )
+ note = fields.Text(
+ string='备注',
+ help='额外说明,仅后台可见(如发行背景、流通范围等)。',
+ )
+
+ @api.model
+ def convert(self, amount, from_level, to_level, category_key='currency'):
+ """按等级汇率将 amount 从 from_level 换算为 to_level 货币。
+
+ 默认只在「货币」类别内换算。汇率方向:低等级 → 高等级 为除以 rate,
+ 高等级 → 低等级 为乘以 rate。最高等级货币(元玺)的 rate 固定为 1。
+ """
+ if from_level == to_level:
+ return amount
+ category = self.env['game.rule.category'].sudo().search(
+ [('key', '=', category_key)], limit=1
+ )
+ if not category:
+ return amount
+ recs = self.search(
+ [('category_id', '=', category.id)],
+ order='level asc',
+ )
+ rate_map = {r.level: r.rate or 1 for r in recs}
+
+ if from_level < to_level:
+ result = amount
+ for lv in range(from_level, to_level):
+ result = result / rate_map.get(lv, 1)
+ return result
+ else:
+ result = amount
+ for lv in range(to_level, from_level):
+ result = result * rate_map.get(lv, 1)
+ return result
diff --git a/addons/game_base/models/rule_category.py b/addons/game_base/models/rule_category.py
new file mode 100644
index 0000000..8c2b79b
--- /dev/null
+++ b/addons/game_base/models/rule_category.py
@@ -0,0 +1,18 @@
+# -*- coding: utf-8 -*-
+from odoo import models, fields
+
+
+class GameRuleCategory(models.Model):
+ _name = 'game.rule.category'
+ _description = '规则类别'
+ _order = 'sequence, id'
+ _rec_name = 'name'
+
+ _sql_constraints = [
+ ('key_uniq', 'unique(key)', '规则类别标识必须唯一'),
+ ]
+
+ key = fields.Char(string='标识', required=True, index=True)
+ name = fields.Char(string='名称', required=True)
+ sequence = fields.Integer(string='排序', default=10)
+ active = fields.Boolean(string='启用', default=True)
diff --git a/addons/game_base/security/ir.model.access.csv b/addons/game_base/security/ir.model.access.csv
index d588c72..d0a25cc 100644
--- a/addons/game_base/security/ir.model.access.csv
+++ b/addons/game_base/security/ir.model.access.csv
@@ -7,10 +7,10 @@ access_game_forum_post,game.forum.post public,model_game_forum_post,base.group_p
access_game_forum_post_user,game.forum.post user,model_game_forum_post,base.group_user,1,1,1,1
access_game_announcement,game.announcement public,model_game_announcement,base.group_public,1,0,0,0
access_game_announcement_user,game.announcement user,model_game_announcement,base.group_user,1,1,1,1
+access_game_rule_category,game.rule.category public,model_game_rule_category,base.group_public,1,0,0,0
+access_game_rule_category_user,game.rule.category user,model_game_rule_category,base.group_user,1,1,1,1
access_game_rule,game.rule public,model_game_rule,base.group_public,1,0,0,0
access_game_rule_user,game.rule user,model_game_rule,base.group_user,1,1,1,1
-access_game_currency,game.currency public,model_game_currency,base.group_public,1,0,0,0
-access_game_currency_user,game.currency user,model_game_currency,base.group_user,1,1,1,1
access_game_page,game.page public,model_game_page,base.group_public,1,0,0,0
access_game_page_user,game.page user,model_game_page,base.group_user,1,1,1,1
access_game_page_item,game.page.item public,model_game_page_item,base.group_public,1,0,0,0
diff --git a/addons/game_base/views/announcement_views.xml b/addons/game_base/views/announcement_views.xml
new file mode 100644
index 0000000..1b8213a
--- /dev/null
+++ b/addons/game_base/views/announcement_views.xml
@@ -0,0 +1,30 @@
+
+
+
+
+
+ 公告
+ game.announcement
+ list,form
+
+
+
+ game.announcement.list
+ game.announcement
+
+
+
+
+
+
+ game.announcement.form
+ game.announcement
+
+
+
+
+
+
diff --git a/addons/game_base/views/codex_views.xml b/addons/game_base/views/codex_views.xml
new file mode 100644
index 0000000..daafb14
--- /dev/null
+++ b/addons/game_base/views/codex_views.xml
@@ -0,0 +1,39 @@
+
+
+
+
+
+ 图鉴
+ game.codex.entry
+ list,form
+ {'group_by': 'cat'}
+
+
+
+ game.codex.entry.list
+ game.codex.entry
+
+
+
+
+
+
+
+
+ game.codex.entry.form
+ game.codex.entry
+
+
+
+
+
+
diff --git a/addons/game_base/views/forum_views.xml b/addons/game_base/views/forum_views.xml
new file mode 100644
index 0000000..080c6a5
--- /dev/null
+++ b/addons/game_base/views/forum_views.xml
@@ -0,0 +1,61 @@
+
+
+
+
+
+ 主题
+ game.forum.thread
+ list,form
+
+
+
+ game.forum.thread.list
+ game.forum.thread
+
+
+
+
+
+
+
+
+ game.forum.thread.form
+ game.forum.thread
+
+
+
+
+
+
+
+ 回复
+ game.forum.post
+ list,form
+
+
+
+ game.forum.post.list
+ game.forum.post
+
+
+
+
+
+
+ game.forum.post.form
+ game.forum.post
+
+
+
+
+
+
diff --git a/addons/game_base/views/game_base_views.xml b/addons/game_base/views/game_base_views.xml
deleted file mode 100644
index e9c4add..0000000
--- a/addons/game_base/views/game_base_views.xml
+++ /dev/null
@@ -1,261 +0,0 @@
-
-
-
-
-
-
-
-
- 图鉴
- game.codex.entry
- list,form
-
-
-
-
- game.codex.entry.list
- game.codex.entry
-
-
-
-
-
-
-
- game.codex.entry.form
- game.codex.entry
-
-
-
-
-
-
-
- 规则
- game.rule
- list,form
-
-
-
- game.rule.list
- game.rule
-
-
-
-
-
- game.rule.form
- game.rule
-
-
-
-
-
-
-
- 货币等级
- game.currency
- list,form
-
-
-
- game.currency.list
- game.currency
-
-
-
-
-
- game.currency.form
- game.currency
-
-
-
-
-
-
-
- 页面板块
- game.page
- list,form
-
-
-
- game.page.list
- game.page
-
-
-
-
-
-
-
- game.page.form
- game.page
-
-
-
-
-
-
-
-
- 主题
- game.forum.thread
- list,form
-
-
-
- 回复
- game.forum.post
- list,form
-
-
-
-
- game.forum.thread.list
- game.forum.thread
-
-
-
-
-
-
-
- game.forum.thread.form
- game.forum.thread
-
-
-
-
-
- game.forum.post.list
- game.forum.post
-
-
-
-
-
- game.forum.post.form
- game.forum.post
-
-
-
-
-
-
-
- 公告
- game.announcement
- list,form
-
-
-
- game.announcement.list
- game.announcement
-
-
-
-
-
- game.announcement.form
- game.announcement
-
-
-
-
-
-
-
-
-
-
-
- 卷
- game.story.volume
- list,form
-
-
-
- 章节
- game.story.chapter
- list,form
-
-
-
-
- game.story.volume.list
- game.story.volume
-
-
-
-
-
- game.story.volume.form
- game.story.volume
-
-
-
-
-
-
-
-
-
-
-
-
-
- game.story.chapter.list
- game.story.chapter
-
-
-
-
-
- game.story.chapter.form
- game.story.chapter
-
-
-
-
-
-
-
-
-
diff --git a/addons/game_base/views/menu.xml b/addons/game_base/views/menu.xml
new file mode 100644
index 0000000..daa2008
--- /dev/null
+++ b/addons/game_base/views/menu.xml
@@ -0,0 +1,41 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/addons/game_base/views/page_views.xml b/addons/game_base/views/page_views.xml
new file mode 100644
index 0000000..ac0ff40
--- /dev/null
+++ b/addons/game_base/views/page_views.xml
@@ -0,0 +1,45 @@
+
+
+
+
+
+ 页面板块
+ game.page
+ list,form
+
+
+
+ game.page.list
+ game.page
+
+
+
+
+
+
+
+
+ game.page.form
+ game.page
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/addons/game_base/views/rule_category_views.xml b/addons/game_base/views/rule_category_views.xml
new file mode 100644
index 0000000..8df06ff
--- /dev/null
+++ b/addons/game_base/views/rule_category_views.xml
@@ -0,0 +1,39 @@
+
+
+
+
+
+ 规则类别
+ game.rule.category
+ list,form
+
+
+
+ game.rule.category.list
+ game.rule.category
+
+
+
+
+
+
+
+
+
+
+
+ game.rule.category.form
+ game.rule.category
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/addons/game_base/views/rule_views.xml b/addons/game_base/views/rule_views.xml
new file mode 100644
index 0000000..76f6c47
--- /dev/null
+++ b/addons/game_base/views/rule_views.xml
@@ -0,0 +1,43 @@
+
+
+
+
+
+ 规则
+ game.rule
+ list,form
+ {'group_by': 'category_id'}
+
+
+
+ game.rule.list
+ game.rule
+
+
+
+
+
+
+
+
+
+
+ game.rule.form
+ game.rule
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/addons/game_base/views/story_views.xml b/addons/game_base/views/story_views.xml
new file mode 100644
index 0000000..5105b75
--- /dev/null
+++ b/addons/game_base/views/story_views.xml
@@ -0,0 +1,61 @@
+
+
+
+
+
+ 卷
+ game.story.volume
+ list,form
+
+
+
+ game.story.volume.list
+ game.story.volume
+
+
+
+
+
+
+ game.story.volume.form
+ game.story.volume
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 章节
+ game.story.chapter
+ list,form
+
+
+
+ game.story.chapter.list
+ game.story.chapter
+
+
+
+
+
+
+ game.story.chapter.form
+ game.story.chapter
+
+
+
+
+
+
+
+
+
diff --git a/addons/yt_world/views/yt_world_views.xml b/addons/yt_world/views/yt_world_views.xml
index 1890677..ce8e500 100644
--- a/addons/yt_world/views/yt_world_views.xml
+++ b/addons/yt_world/views/yt_world_views.xml
@@ -15,8 +15,6 @@
-
-
@@ -31,8 +29,6 @@
-
-
diff --git a/assets/art/transitions/transition_adventure.png b/assets/art/transitions/transition_adventure.png
new file mode 100644
index 0000000..a176842
Binary files /dev/null and b/assets/art/transitions/transition_adventure.png differ
diff --git a/assets/art/transitions/transition_coast.png b/assets/art/transitions/transition_coast.png
new file mode 100644
index 0000000..539910d
Binary files /dev/null and b/assets/art/transitions/transition_coast.png differ
diff --git a/assets/art/transitions/transition_dawn.png b/assets/art/transitions/transition_dawn.png
new file mode 100644
index 0000000..16b0f38
Binary files /dev/null and b/assets/art/transitions/transition_dawn.png differ
diff --git a/assets/art/transitions/transition_dungeon.png b/assets/art/transitions/transition_dungeon.png
new file mode 100644
index 0000000..bc45cbc
Binary files /dev/null and b/assets/art/transitions/transition_dungeon.png differ
diff --git a/assets/art/transitions/transition_forest.png b/assets/art/transitions/transition_forest.png
new file mode 100644
index 0000000..0cd4e6d
Binary files /dev/null and b/assets/art/transitions/transition_forest.png differ
diff --git a/assets/art/transitions/transition_night.png b/assets/art/transitions/transition_night.png
new file mode 100644
index 0000000..ba0ff4a
Binary files /dev/null and b/assets/art/transitions/transition_night.png differ
diff --git a/assets/art/transitions/transition_sky.png b/assets/art/transitions/transition_sky.png
new file mode 100644
index 0000000..1448d27
Binary files /dev/null and b/assets/art/transitions/transition_sky.png differ
diff --git a/assets/art/transitions/transition_skycity.png b/assets/art/transitions/transition_skycity.png
new file mode 100644
index 0000000..64e1c5a
Binary files /dev/null and b/assets/art/transitions/transition_skycity.png differ
diff --git a/index.html b/index.html
index b0ac6e8..ac8424a 100644
--- a/index.html
+++ b/index.html
@@ -36,7 +36,7 @@
.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;}
+ .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;}
@@ -87,7 +87,24 @@
.card p{font-size:13px;color:var(--text-dim);line-height:1.6;}
/* ===== 背景 ===== */
- #background{flex-direction:row;gap:38px;align-items:stretch;}
+ #background{flex-direction:column;align-items:stretch;gap:0;overflow-y:auto;padding:48px 6% 90px;}
+ .bg-page{max-width:1060px;width:100%;margin:0 auto;padding:30px 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;}
@@ -155,7 +172,7 @@
.threads{flex:1;overflow-y:auto;display:flex;flex-direction:column;gap:10px;padding-right:6px;}
.th{display:flex;align-items:center;gap:14px;background:var(--card);border:1px solid var(--card-bd);border-radius:11px;padding:13px 16px;transition:.2s;cursor:pointer;}
.th:hover{border-color:var(--violet);background:rgba(139,92,246,.10);}
- .th .pin{font-size:10px;font-family:"Press Start 2P",monospace;color:var(--gold);}
+ .th .pin{font-size:11px;font-weight:700;letter-spacing:1px;color:var(--gold);background:rgba(231,184,92,.14);border:1px solid rgba(231,184,92,.42);border-radius:5px;padding:1px 7px;}
.th .tt{flex:1;font-size:15px;color:#fff;}
.th .meta{font-size:12px;color:var(--text-dim);white-space:nowrap;}
.th .rp{color:var(--cyan);font-weight:500;}
@@ -221,6 +238,55 @@
.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-y:auto;padding:46px 6% 90px;}
+ #worldDataContent{max-width:1080px;width:100%;margin:0 auto;}
+ .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(190px,1fr));gap:16px;}
+ .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:18px 20px;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:34px;line-height:1.1;color:#fff;margin:8px 0 2px;}
+ .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){
.grid{grid-template-columns:repeat(2,1fr);}
#background,#forum{flex-direction:column;overflow-y:auto;}
@@ -238,9 +304,8 @@
- 宇森
+ 宇森
-
PIN【公告】宇森官网第一阶段上线,欢迎园丁们入驻128 回复 · 官方
+
置顶【公告】宇森官网第一阶段上线,欢迎园丁们入驻128 回复 · 官方
新手向:园丁视角的三种开局思路42 回复 · 行者
你们更喜欢哪种族?来投个票76 回复 · 雾岛
手绘了我的主世界浮空岛,求轻喷31 回复 · 青柠
@@ -415,10 +480,7 @@
@@ -434,6 +496,13 @@
document.getElementById(b.dataset.tab).classList.add('active');
}));
+ // logo 点击回主页(intro 面板)
+ document.getElementById('logoHome').addEventListener('click',()=>{
+ navBtns.forEach(x=>x.classList.remove('active'));
+ panels.forEach(p=>p.classList.remove('active'));
+ document.getElementById('intro').classList.add('active');
+ });
+
// ============ 程序化矢量缩略图(零 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;};}
@@ -668,6 +737,128 @@
}
refreshUserBox();
+ // ============ 背景页:从 odoo 拉取页面板块渲染 ============
+ async function renderBackground(){
+ const box = document.getElementById('background');
+ if(!box) return;
+ try{
+ const r = await fetch('/game/api/pages?db=game', {credentials:'same-origin'});
+ const d = await r.json();
+ const pages = (d && d.items) || [];
+ if(!pages.length){ box.innerHTML = '板块内容即将上线…
'; return; }
+ box.innerHTML = pages.map(p=>{
+ const cards = (p.items||[]).map(it=>`
+
+
${it.icon||'✦'}
+
+
${it.title||''}
+
${it.desc||''}
+
+
`).join('');
+ return `
+
+
+
${p.title||''}
+ ${p.subtitle?`
${p.subtitle}
`:''}
+
+ ${p.body_html?`${p.body_html}
`:''}
+ ${cards?`${cards}
`:''}
+ `;
+ }).join('');
+ }catch(e){
+ box.innerHTML = '板块加载失败,请刷新重试。
';
+ }
+ }
+ renderBackground();
+
+ // ============ 世界数据:大报表(演示数据,后端接口待接入) ============
+ async function renderWorldData(){
+ const box = document.getElementById('worldDataContent');
+ if(!box) return;
+ // TODO: 后端接入后改为 await API.worldData() 取真实数据
+ const data = worldDataMock();
+ const kpis = data.kpi.map(k=>`
+
+
${k.delta>=0?'▲':'▼'} ${Math.abs(k.delta)}%
+
${k.icon}
+
${k.num}${k.unit||''}
+
${k.label}
+
`).join('');
+ const bars = data.bars.map(b=>`
+ `).join('');
+ const rows = data.layers.map(l=>`
+
+ | ${l.lv} |
+ ${l.band} |
+ ${l.eco} |
+ ${l.pop} |
+ ${l.terr} |
+
`).join('');
+ const ranks = data.factions.map((f,i)=>`
+
+
${i+1}
+
+
${f.name}${f.score} 影响力
+
+
+
`).join('');
+ box.innerHTML = `
+ 演示数据 · 后端接口待接入
+ ${kpis}
+
+ 核心指标进度 CORE METRICS
+ ${bars}
+
+ 世界五层结构 WORLD LAYERS
+
+ | 层级 | 高度带 | 地表生态 | 人口占比 | 代表地形 |
+ ${rows}
+
+
+ 势力影响力排行 FACTION RANKING
+ ${ranks}
+ `;
+ }
+ function worldDataMock(){
+ return {
+ kpi:[
+ {icon:'👥',num:'12,840',unit:'人',label:'世界总人口',delta:3},
+ {icon:'🌍',num:'Lv.7',unit:'',label:'当前世界等级',delta:1},
+ {icon:'🏛️',num:'38',unit:'个',label:'活跃势力',delta:5},
+ {icon:'⛪',num:'9.6M',unit:'',label:'信仰值总量',delta:8},
+ {icon:'⚔️',num:'214',unit:'',label:'在线角色',delta:-2},
+ {icon:'💎',num:'1.3M',unit:'',label:'流通晶币',delta:4}
+ ],
+ bars:[
+ {k:'世界安定度',v:78},
+ {k:'生态健康度',v:64},
+ {k:'信仰传播度',v:88},
+ {k:'文明探索度',v:52},
+ {k:'势力平衡度',v:71},
+ {k:'资源充裕度',v:60}
+ ],
+ layers:[
+ {lv:'第一层 · 地表',band:'0–80m',eco:'平原 / 森林 / 河流',pop:'58%',terr:'草原城邦'},
+ {lv:'第二层 · 丘陵',band:'80–260m',eco:'梯田 / 矿脉 / 林地',pop:'22%',terr:'山道驿站'},
+ {lv:'第三层 · 高地',band:'260–600m',eco:'苔原 / 雪线 / 圣峰',pop:'12%',terr:'云端神殿'},
+ {lv:'第四层 · 地下',band:'-30–-200m',eco:'溶洞 / 晶矿 / 暗河',pop:'6%',terr:'地心工坊'},
+ {lv:'第五层 · 秘境',band:'位面裂隙',eco:'虚空 / 混沌 / 星界',pop:'2%',terr:'裂隙哨站'}
+ ],
+ factions:[
+ {name:'曦光同盟',score:9420,pct:100},
+ {name:'玄铁工盟',score:8730,pct:93},
+ {name:'碧波联邦',score:7610,pct:81},
+ {name:'夜枭议会',score:6880,pct:73},
+ {name:'荒原游商',score:5240,pct:56},
+ {name:'星语祭司团',score:4310,pct:46}
+ ]
+ };
+ }
+ renderWorldData();
+
// ============ 进入游戏:已登录→世界页;未登录→登录页 ============
const enterBtn = document.getElementById('enterGame');
if(enterBtn) enterBtn.addEventListener('click', async ()=>{
diff --git a/website/announce_preview.html b/website/announce_preview.html
new file mode 100644
index 0000000..7a93f9f
--- /dev/null
+++ b/website/announce_preview.html
@@ -0,0 +1,144 @@
+
+
+
+
+
+公告 · 新版预览
+
+
+
+
+
+
+
+
+
+
公 告
+
官方通知 · ANNOUNCEMENTS
+
+
+
+
+⚡ 新版公告预览 · 点击卡片可弹窗
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/website/assets/site-app.js b/website/assets/site-app.js
index 2fc071b..cfbc77e 100644
--- a/website/assets/site-app.js
+++ b/website/assets/site-app.js
@@ -49,17 +49,111 @@ async function loadForum(){
} catch (e) { /* 保留静态兜底内容 */ }
}
-// ---------- 公告 ----------
+// ---------- 公告(卡片 + 弹窗) ----------
+function annEscapeHtml(s) {
+ return String(s == null ? '' : s)
+ .replace(/&/g, '&').replace(//g, '>')
+ .replace(/"/g, '"').replace(/'/g, ''');
+}
+function annFormatDate(d) {
+ // "2026-07-09" -> "2026.07.09"
+ return d ? String(d).replace(/-/g, '.') : '';
+}
+function annStripHtml(html) {
+ const tmp = document.createElement('div');
+ tmp.innerHTML = html || '';
+ return (tmp.textContent || '').replace(/\s+/g, ' ').trim();
+}
+
async function loadAnnounce(){
- const el = document.querySelector('#announce .notes');
- if (!el) return;
+ const root = document.getElementById('announce');
+ if (!root) return;
+ root.innerHTML = '加载中…
';
+ let list = [];
try {
- const list = await API.announcements();
- if (!list || !list.length){ el.innerHTML = ''; return; }
- el.innerHTML = list.map(n =>
- `${n.date}
${n.body_html || ''}
`
- ).join('');
- } catch (e) { /* 保留静态兜底内容 */ }
+ list = await API.announcements();
+ } catch (e) { /* 落到空态 */ }
+ if (!list || !list.length) {
+ root.innerHTML = '暂无公告
';
+ return;
+ }
+ root.innerHTML = list.map((n, i) => {
+ const pri = n.priority || 'normal';
+ const preview = annStripHtml(n.body_html).slice(0, 160);
+ return `
+
+
+ ${annEscapeHtml(annFormatDate(n.date))}
+
+ ${annEscapeHtml(n.priority_label || pri)}
+
+
${annEscapeHtml(n.name)}
+
${annEscapeHtml(preview)}
+
阅读全文 →
+
`;
+ }).join('');
+
+ root.querySelectorAll('.announce-card').forEach(card => {
+ const idx = +card.dataset.idx;
+ const open = () => openAnnounceModal(list[idx]);
+ card.addEventListener('click', open);
+ card.addEventListener('keydown', e => {
+ if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); open(); }
+ });
+ });
+}
+
+// ---- 弹窗(懒创建一次) ----
+let _annModal = null;
+let _annLastFocus = null;
+function ensureAnnounceModal() {
+ if (_annModal) return _annModal;
+ _annModal = document.createElement('div');
+ _annModal.className = 'announce-modal';
+ _annModal.setAttribute('role', 'presentation');
+ _annModal.innerHTML = `
+
+ `;
+ document.body.appendChild(_annModal);
+ _annModal.addEventListener('click', e => {
+ if (e.target.closest('[data-close]')) closeAnnounceModal();
+ });
+ document.addEventListener('keydown', e => {
+ if (e.key === 'Escape' && _annModal.classList.contains('open')) closeAnnounceModal();
+ });
+ return _annModal;
+}
+function openAnnounceModal(item) {
+ if (!item) return;
+ const m = ensureAnnounceModal();
+ m.querySelector('[data-title]').textContent = item.name || '';
+ m.querySelector('[data-meta]').textContent =
+ [annFormatDate(item.date), item.priority_label].filter(Boolean).join(' · ');
+ // body_html 来自后台管理员撰写,可信;非管理员输入路径不存在,OK 直接渲染
+ m.querySelector('[data-body]').innerHTML = item.body_html || '(无正文)
';
+ _annLastFocus = document.activeElement;
+ m.classList.add('open');
+ document.body.style.overflow = 'hidden';
+ // 焦点移入弹窗(关闭按钮)
+ const closeBtn = m.querySelector('.close');
+ if (closeBtn) closeBtn.focus({ preventScroll: true });
+}
+function closeAnnounceModal() {
+ if (!_annModal || !_annModal.classList.contains('open')) return;
+ _annModal.classList.remove('open');
+ document.body.style.overflow = '';
+ if (_annLastFocus && _annLastFocus.focus) _annLastFocus.focus({ preventScroll: true });
}
document.addEventListener('DOMContentLoaded', () => {
diff --git a/website/auth.css b/website/auth.css
index 60085fa..a33f533 100644
--- a/website/auth.css
+++ b/website/auth.css
@@ -144,3 +144,282 @@
.login-footer a:hover {
text-decoration: underline;
}
+
+/* ==================================================
+ 公告页 · 大气版(卡片 + 弹窗)
+ ================================================== */
+
+/* ---- 页面头 ---- */
+#panel-announce h1 {
+ font-size: 44px;
+ letter-spacing: 8px;
+ margin-bottom: 10px;
+ text-align: center;
+}
+#panel-announce .page-sub {
+ text-align: center;
+ font-size: 13px;
+ letter-spacing: 5px;
+ color: #6b8aa8;
+ display: block;
+ margin: 0 auto 36px;
+ position: relative;
+ padding: 0 32px;
+ width: max-content;
+}
+#panel-announce .page-sub::before,
+#panel-announce .page-sub::after {
+ content: "";
+ position: absolute;
+ top: 50%;
+ width: 36px;
+ height: 1px;
+ background: linear-gradient(90deg, transparent, #4af, transparent);
+ opacity: .7;
+}
+#panel-announce .page-sub::before { right: 100%; }
+#panel-announce .page-sub::after { left: 100%; }
+
+/* ---- 卡片列表 ---- */
+.announce-list {
+ display: flex;
+ flex-direction: column;
+ gap: 14px;
+}
+.announce-card {
+ position: relative;
+ background: linear-gradient(135deg, rgba(20, 28, 48, 0.72), rgba(12, 18, 32, 0.72));
+ border: 1px solid rgba(120, 140, 200, 0.14);
+ border-left: 3px solid #a78bfa;
+ border-radius: 12px;
+ padding: 22px 28px 22px 26px;
+ cursor: pointer;
+ overflow: hidden;
+ transition: transform .22s ease, border-color .22s, box-shadow .22s, background .22s;
+ outline: none;
+}
+.announce-card::before {
+ content: "";
+ position: absolute;
+ inset: 0;
+ background: radial-gradient(600px circle at 0% 0%, rgba(167, 139, 250, 0.10), transparent 45%);
+ pointer-events: none;
+ opacity: 0;
+ transition: opacity .25s;
+}
+.announce-card:hover,
+.announce-card:focus-visible {
+ transform: translateY(-2px);
+ border-color: rgba(167, 139, 250, 0.4);
+ box-shadow: 0 14px 38px -18px rgba(167, 139, 250, 0.55),
+ 0 0 0 1px rgba(167, 139, 250, 0.12);
+}
+.announce-card:hover::before,
+.announce-card:focus-visible::before { opacity: 1; }
+
+.announce-card.priority-important { border-left-color: #fbbf24; }
+.announce-card.priority-important:hover,
+.announce-card.priority-important:focus-visible {
+ box-shadow: 0 14px 38px -18px rgba(251, 191, 36, 0.5),
+ 0 0 0 1px rgba(251, 191, 36, 0.14);
+}
+.announce-card.priority-urgent {
+ border-left-color: #f87171;
+ background: linear-gradient(135deg, rgba(42, 20, 30, 0.72), rgba(22, 12, 18, 0.72));
+}
+.announce-card.priority-urgent:hover,
+.announce-card.priority-urgent:focus-visible {
+ box-shadow: 0 14px 38px -18px rgba(248, 113, 113, 0.55),
+ 0 0 0 1px rgba(248, 113, 113, 0.18);
+}
+
+.announce-card .meta-row {
+ display: flex;
+ align-items: center;
+ gap: 10px;
+ font-size: 12px;
+ color: #6b7d92;
+ letter-spacing: 1px;
+ margin-bottom: 10px;
+ position: relative;
+}
+.announce-card .date { font-variant-numeric: tabular-nums; }
+.announce-card .dot {
+ width: 3px; height: 3px; border-radius: 50%;
+ background: #4a5568;
+}
+.announce-card .badge {
+ font-size: 11px;
+ padding: 2px 8px;
+ border-radius: 4px;
+ letter-spacing: 1.5px;
+ border: 1px solid currentColor;
+}
+.announce-card .badge.normal { color: #6b7d92; }
+.announce-card .badge.important { color: #fbbf24; background: rgba(251, 191, 36, 0.08); }
+.announce-card .badge.urgent { color: #f87171; background: rgba(248, 113, 113, 0.10); }
+
+.announce-card .title {
+ font-size: 18px;
+ font-weight: 600;
+ color: #e8eef7;
+ line-height: 1.55;
+ margin-bottom: 8px;
+ display: -webkit-box;
+ -webkit-line-clamp: 2;
+ -webkit-box-orient: vertical;
+ overflow: hidden;
+}
+.announce-card .preview {
+ font-size: 14px;
+ color: #8b9bb2;
+ line-height: 1.75;
+ display: -webkit-box;
+ -webkit-line-clamp: 2;
+ -webkit-box-orient: vertical;
+ overflow: hidden;
+ margin-bottom: 12px;
+}
+.announce-card .more {
+ font-size: 13px;
+ color: #a78bfa;
+ letter-spacing: 1.5px;
+ display: inline-flex;
+ align-items: center;
+ gap: 4px;
+ transition: gap .2s;
+}
+.announce-card:hover .more,
+.announce-card:focus-visible .more { gap: 8px; }
+.announce-card .more .arrow { transition: transform .2s; display: inline-block; }
+.announce-card:hover .more .arrow,
+.announce-card:focus-visible .more .arrow { transform: translateX(3px); }
+
+.announce-empty {
+ text-align: center;
+ padding: 60px 20px;
+ color: #556677;
+ font-size: 15px;
+}
+
+/* ---- 弹窗 ---- */
+.announce-modal {
+ position: fixed;
+ inset: 0;
+ z-index: 2000;
+ display: none;
+ align-items: center;
+ justify-content: center;
+ padding: 40px 20px;
+}
+.announce-modal.open {
+ display: flex;
+ animation: annFade .2s ease;
+}
+@keyframes annFade { from { opacity: 0; } to { opacity: 1; } }
+
+.announce-modal .backdrop {
+ position: absolute;
+ inset: 0;
+ background: rgba(4, 7, 14, 0.72);
+ backdrop-filter: blur(10px);
+ -webkit-backdrop-filter: blur(10px);
+ cursor: pointer;
+}
+.announce-modal .dialog {
+ position: relative;
+ width: 100%;
+ max-width: 720px;
+ max-height: calc(100vh - 80px);
+ background: linear-gradient(160deg, #0e1626, #0a1020);
+ border: 1px solid rgba(167, 139, 250, 0.22);
+ border-radius: 16px;
+ box-shadow: 0 30px 80px -20px rgba(0, 0, 0, 0.7),
+ 0 0 60px -20px rgba(167, 139, 250, 0.3);
+ display: flex;
+ flex-direction: column;
+ animation: annPop .28s cubic-bezier(0.2, 0.9, 0.3, 1.2);
+ overflow: hidden;
+}
+@keyframes annPop {
+ from { opacity: 0; transform: translateY(20px) scale(0.96); }
+ to { opacity: 1; transform: none; }
+}
+.announce-modal .head {
+ display: flex;
+ align-items: flex-start;
+ justify-content: space-between;
+ padding: 28px 32px 20px;
+ border-bottom: 1px solid rgba(120, 140, 200, 0.12);
+ flex-shrink: 0;
+ gap: 16px;
+}
+.announce-modal .head .title {
+ font-size: 22px;
+ font-weight: 600;
+ color: #f0f4fb;
+ line-height: 1.5;
+ margin-bottom: 8px;
+ padding-right: 8px;
+}
+.announce-modal .head .meta {
+ font-size: 12px;
+ color: #6b7d92;
+ letter-spacing: 1.5px;
+}
+.announce-modal .close {
+ width: 34px;
+ height: 34px;
+ border-radius: 8px;
+ background: rgba(120, 140, 200, 0.08);
+ border: none;
+ color: #8b9bb2;
+ font-size: 20px;
+ line-height: 1;
+ cursor: pointer;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ flex-shrink: 0;
+ transition: background .2s, color .2s;
+}
+.announce-modal .close:hover {
+ background: rgba(167, 139, 250, 0.18);
+ color: #a78bfa;
+}
+.announce-modal .body {
+ padding: 24px 32px 32px;
+ overflow-y: auto;
+ font-size: 15px;
+ line-height: 1.9;
+ color: #c0cad9;
+}
+.announce-modal .body p { color: #c0cad9; font-size: 15px; margin-bottom: 12px; line-height: 1.9; }
+.announce-modal .body p:last-child { margin-bottom: 0; }
+.announce-modal .body strong { color: #e8eef7; }
+.announce-modal .body a { color: #a78bfa; }
+.announce-modal .body h1,
+.announce-modal .body h2,
+.announce-modal .body h3 { color: #e8eef7; margin: 18px 0 10px; }
+.announce-modal .body ul,
+.announce-modal .body ol { padding-left: 22px; margin-bottom: 12px; }
+.announce-modal .body li { margin-bottom: 4px; }
+.announce-modal .body code {
+ background: rgba(120, 140, 200, 0.12);
+ padding: 1px 6px;
+ border-radius: 4px;
+ font-size: 13px;
+}
+
+/* ---- 响应式 ---- */
+@media (max-width: 768px) {
+ #panel-announce h1 { font-size: 34px; letter-spacing: 5px; }
+ #panel-announce .page-sub { font-size: 12px; letter-spacing: 4px; margin-bottom: 28px; }
+ .announce-card { padding: 18px 18px 18px 20px; }
+ .announce-card .title { font-size: 16px; }
+ .announce-card .preview { font-size: 13px; }
+ .announce-modal { padding: 16px 12px; }
+ .announce-modal .head { padding: 20px 20px 14px; }
+ .announce-modal .head .title { font-size: 18px; }
+ .announce-modal .body { padding: 18px 20px 24px; font-size: 14px; }
+}
diff --git a/website/intro.html b/website/intro.html
index c0e59aa..1d8e374 100644
--- a/website/intro.html
+++ b/website/intro.html
@@ -259,8 +259,8 @@ p { color:#99aabb; font-size:15px; margin-bottom:14px; line-height:1.9; }
公 告
-
官方通知与更新日志
-
+
官方通知 · ANNOUNCEMENTS
+
diff --git a/world.html b/world.html
index 0a81dd2..297b520 100644
--- a/world.html
+++ b/world.html
@@ -214,13 +214,26 @@ body { background: #0a0e17; overflow: hidden; font-family: 'Microsoft YaHei', sa
.status-moving { color: #4af; }
/* 飞入遮罩 —— 底部提示药丸 + 全宽进度条 */
+/* ===== 飞入过渡:随机动漫插画作背景 + 进度条/提示在前景 ===== */
#flyInOverlay {
position: fixed; top: 0; left: 0; width: 100%; height: 100%;
- background: #0a0e17; z-index: 300;
+ background: transparent; z-index: 300;
display: flex; flex-direction: column; justify-content: flex-end;
padding: 0 24px 48px 24px;
opacity: 0; visibility: hidden; pointer-events: none;
}
+/* 背景插画层:铺满 + 放大遮水印,置于内容之下 */
+#flyInOverlay .flyBg {
+ position: absolute; inset: 0; width: 100%; height: 100%;
+ object-fit: cover; transform: scale(1.08);
+ z-index: 0;
+}
+/* 深色压暗层,保证前景文字/进度条可读 */
+#flyInOverlay::before {
+ content: ''; position: absolute; inset: 0; z-index: 1;
+ background: linear-gradient(180deg, rgba(10,14,23,.35) 0%, rgba(10,14,23,.65) 100%);
+}
+#flyInOverlay > *:not(.flyBg) { position: relative; z-index: 2; }
#flyInOverlay.active { opacity: 1; visibility: visible; pointer-events: auto; }
#flyInOverlay .flyLine {
display: flex; align-items: center; gap: 14px;
@@ -397,16 +410,25 @@ body { background: #0a0e17; overflow: hidden; font-family: 'Microsoft YaHei', sa
#navControls .nc-down{grid-column:2;grid-row:3;}
#navControls .nc-se{grid-column:3;grid-row:3;}
-/* ===== 登录闸门 ===== */
+/* ===== 登录闸门(小型居中转圈,不遮挡主屏幕) ===== */
#authSplash{position:fixed;inset:0;z-index:9999;display:flex;align-items:center;justify-content:center;
- background:linear-gradient(160deg,#080c1a,#0e1730 48%,#15213f);color:#9aa6c4;
- font-family:"Noto Sans SC",system-ui,sans-serif;font-size:15px;letter-spacing:3px;}
+ background:rgba(8,12,26,.45);backdrop-filter:blur(4px);color:#cdd8e6;
+ font-family:"Noto Sans SC",system-ui,sans-serif;font-size:13px;letter-spacing:2px;}
+#authSplash .auth-spinner{
+ display:flex;flex-direction:column;align-items:center;gap:14px;
+ padding:28px 36px;border-radius:16px;
+ background:rgba(14,23,48,.75);border:1px solid rgba(139,92,246,.25);
+ box-shadow:0 8px 32px rgba(0,0,0,.5);}
+#authSpinner{width:32px;height:32px;border:3px solid rgba(139,92,246,.25);
+ border-top-color:#a78bfa;border-radius:50%;
+ animation:authSpin .7s linear infinite;}
+@keyframes authSpin{to{transform:rotate(360deg);}}
/* #authBar 已融合进 #topBar .userArea */
-登录校验中…
+
@@ -422,7 +444,6 @@ body { background: #0a0e17; overflow: hidden; font-family: 'Microsoft YaHei', sa
宇森大地
-
坐标: (0, 0)
@@ -443,14 +464,11 @@ body { background: #0a0e17; overflow: hidden; font-family: 'Microsoft YaHei', sa
-
功能栏
-
-
-
-
-
-
+
+
+
+
@@ -510,7 +528,7 @@ body { background: #0a0e17; overflow: hidden; font-family: 'Microsoft YaHei', sa
![]()
-
+
@@ -518,6 +536,7 @@ body { background: #0a0e17; overflow: hidden; font-family: 'Microsoft YaHei', sa
+
💡 拖拽空白处可平移视角,滚轮缩放沙盘
@@ -576,11 +595,71 @@ body { background: #0a0e17; overflow: hidden; font-family: 'Microsoft YaHei', sa
-