Compare commits
2 Commits
467e260d80
...
760cd517c2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
760cd517c2 | ||
|
|
d9269e0f56 |
@ -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'")
|
||||
|
||||
@ -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 用户打通的登录/注册接口。',
|
||||
}
|
||||
|
||||
@ -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):
|
||||
@ -172,6 +216,49 @@ class GameApiController(http.Controller):
|
||||
} for r in recs]
|
||||
return _json(out)
|
||||
|
||||
@http.route('/game/api/story', type='http', auth='public', methods=['GET'], csrf=False)
|
||||
def story(self, **kw):
|
||||
"""小说目录:返回 卷 -> 章节 结构,供前端左侧目录(TOC)使用。"""
|
||||
Volume = request.env['game.story.volume'].sudo()
|
||||
Chapter = request.env['game.story.chapter'].sudo()
|
||||
vols = Volume.search_read([], ['id', 'name', 'sequence', 'intro'], order='sequence, id')
|
||||
chapters = Chapter.search_read([], ['id', 'volume_id', 'name', 'sequence'],
|
||||
order='volume_id, sequence, id')
|
||||
by_vol = {}
|
||||
for c in chapters:
|
||||
vid = c['volume_id'][0] if c['volume_id'] else None
|
||||
by_vol.setdefault(vid, []).append({
|
||||
'id': c['id'],
|
||||
'name': c['name'],
|
||||
'sequence': c['sequence'],
|
||||
})
|
||||
out = []
|
||||
for v in vols:
|
||||
out.append({
|
||||
'id': v['id'],
|
||||
'name': v['name'],
|
||||
'sequence': v['sequence'],
|
||||
'intro': v.get('intro') or '',
|
||||
'chapters': by_vol.get(v['id'], []),
|
||||
})
|
||||
return _json({'volumes': out})
|
||||
|
||||
@http.route('/game/api/story/chapter/<int:cid>', type='http', auth='public', methods=['GET'], csrf=False)
|
||||
def story_chapter(self, cid, **kw):
|
||||
"""单章正文(点击目录时按需拉取,避免一次性下发整本小说)。"""
|
||||
rec = request.env['game.story.chapter'].sudo().search_read(
|
||||
[('id', '=', cid)], ['id', 'volume_id', 'name', 'sequence', 'body_html'])
|
||||
if not rec:
|
||||
return _json({'error': '章节不存在'})
|
||||
r = rec[0]
|
||||
return _json({
|
||||
'id': r['id'],
|
||||
'volume_id': r['volume_id'][0] if r['volume_id'] else None,
|
||||
'name': r['name'],
|
||||
'sequence': r['sequence'],
|
||||
'body_html': r['body_html'] or '',
|
||||
})
|
||||
|
||||
# ============ 登录 / 注册 / 会话(与 Odoo res.users 打通) ============
|
||||
|
||||
@http.route('/game/api/login', type='http', auth='public',
|
||||
|
||||
@ -1,93 +1,127 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo noupdate="0">
|
||||
|
||||
<!-- ============ 规则类别(后台可配置) ============ -->
|
||||
<record id="rule_category_world" model="game.rule.category">
|
||||
<field name="key">world</field>
|
||||
<field name="name">世界等级</field>
|
||||
<field name="sequence">10</field>
|
||||
</record>
|
||||
<record id="rule_category_faction" model="game.rule.category">
|
||||
<field name="key">faction</field>
|
||||
<field name="name">势力等级</field>
|
||||
<field name="sequence">20</field>
|
||||
</record>
|
||||
<record id="rule_category_character" model="game.rule.category">
|
||||
<field name="key">character</field>
|
||||
<field name="name">人物角色等级</field>
|
||||
<field name="sequence">30</field>
|
||||
</record>
|
||||
<record id="rule_category_currency" model="game.rule.category">
|
||||
<field name="key">currency</field>
|
||||
<field name="name">货币等级</field>
|
||||
<field name="sequence">40</field>
|
||||
</record>
|
||||
|
||||
<!-- ============ 规则 / 等级制度(基于官网) ============ -->
|
||||
<!-- 世界等级 · 五阶段 -->
|
||||
<record id="rule_world_1" model="game.rule">
|
||||
<field name="type">world</field><field name="level">1</field><field name="name">凡墟</field>
|
||||
<field name="category_id" ref="rule_category_world"/><field name="level">1</field><field name="name">凡墟</field>
|
||||
<field name="desc"><![CDATA[万物生灵尚处蒙昧凡土,天地灵气稀薄,各族散居荒野,无族群联盟与政权,只求狩猎采集求生,一切秩序遵循自然法则。]]></field>
|
||||
</record>
|
||||
<record id="rule_world_2" model="game.rule">
|
||||
<field name="type">world</field><field name="level">2</field><field name="name">灵启</field>
|
||||
<field name="category_id" ref="rule_category_world"/><field name="level">2</field><field name="name">灵启</field>
|
||||
<field name="desc"><![CDATA[天地灵气复苏觉醒,各族聚拢形成聚落,诞生劳作分工、屋舍营建与物物交换,不同族群率先凝聚部族,世间初代势力自此诞生。]]></field>
|
||||
</record>
|
||||
<record id="rule_world_3" model="game.rule">
|
||||
<field name="type">world</field><field name="level">3</field><field name="name">玄界</field>
|
||||
<field name="category_id" ref="rule_category_world"/><field name="level">3</field><field name="name">玄界</field>
|
||||
<field name="desc"><![CDATA[各族部落兼并融合,广袤大地形成多元族群共治的完整疆域,征伐、道统、造物三大发展路线并行;各族大能、上古秘闻等传奇事件不断现世,玄荒文明迎来鼎盛割据时代。]]></field>
|
||||
</record>
|
||||
<record id="rule_world_4" model="game.rule">
|
||||
<field name="type">world</field><field name="level">4</field><field name="name">穹冥</field>
|
||||
<field name="category_id" ref="rule_category_world"/><field name="level">4</field><field name="name">穹冥</field>
|
||||
<field name="desc"><![CDATA[生灵突破地表桎梏,打通地底深渊与浩瀚星空,天地上下各界尽数纳入族群管辖;各类上古神器、本命法器完整解锁,文明疆域遍布天地穹苍。]]></field>
|
||||
</record>
|
||||
<record id="rule_world_5" model="game.rule">
|
||||
<field name="type">world</field><field name="level">5</field><field name="name">溯元</field>
|
||||
<field name="category_id" ref="rule_category_world"/><field name="level">5</field><field name="name">溯元</field>
|
||||
<field name="desc"><![CDATA[各族顶尖文明勘破这片天地的诞生本源,挣脱本世界规则束缚,可踏上飞升之路、往复轮回,前往更高维度开启全新文明演化。]]></field>
|
||||
</record>
|
||||
|
||||
<!-- 势力等级 · Lv.1-6 -->
|
||||
<record id="rule_faction_1" model="game.rule">
|
||||
<field name="type">faction</field><field name="level">1</field><field name="name">墟族</field>
|
||||
<field name="category_id" ref="rule_category_faction"/><field name="level">1</field><field name="name">墟族</field>
|
||||
<field name="desc"><![CDATA[血缘小群体,蛮荒无制度。]]></field>
|
||||
</record>
|
||||
<record id="rule_faction_2" model="game.rule">
|
||||
<field name="type">faction</field><field name="level">2</field><field name="name">灵聚</field>
|
||||
<field name="category_id" ref="rule_category_faction"/><field name="level">2</field><field name="name">灵聚</field>
|
||||
<field name="desc"><![CDATA[灵气聚落联盟,初步治理与贸易。]]></field>
|
||||
</record>
|
||||
<record id="rule_faction_3" model="game.rule">
|
||||
<field name="type">faction</field><field name="level">3</field><field name="name">玄邑</field>
|
||||
<field name="category_id" ref="rule_category_faction"/><field name="level">3</field><field name="name">玄邑</field>
|
||||
<field name="desc"><![CDATA[疆域整合,军法、道规体系完善。]]></field>
|
||||
</record>
|
||||
<record id="rule_faction_4" model="game.rule">
|
||||
<field name="type">faction</field><field name="level">4</field><field name="name">冥朝</field>
|
||||
<field name="category_id" ref="rule_category_faction"/><field name="level">4</field><field name="name">冥朝</field>
|
||||
<field name="desc"><![CDATA[一统大地,掌控地下浅层资源调度。]]></field>
|
||||
</record>
|
||||
<record id="rule_faction_5" model="game.rule">
|
||||
<field name="type">faction</field><field name="level">5</field><field name="name">穹统</field>
|
||||
<field name="category_id" ref="rule_category_faction"/><field name="level">5</field><field name="name">穹统</field>
|
||||
<field name="desc"><![CDATA[天地大陆霸主,穿梭星空深渊作战。]]></field>
|
||||
</record>
|
||||
<record id="rule_faction_6" model="game.rule">
|
||||
<field name="type">faction</field><field name="level">6</field><field name="name">溯盟</field>
|
||||
<field name="category_id" ref="rule_category_faction"/><field name="level">6</field><field name="name">溯盟</field>
|
||||
<field name="desc"><![CDATA[多维文明联合体,触及世界本源博弈。]]></field>
|
||||
</record>
|
||||
|
||||
<!-- 人物角色等级 · Lv.1-12 -->
|
||||
<record id="rule_char_1" model="game.rule"><field name="type">character</field><field name="level">1</field><field name="name">萌新</field><field name="desc"><![CDATA[初入世界的无名者。]]></field></record>
|
||||
<record id="rule_char_2" model="game.rule"><field name="type">character</field><field name="level">2</field><field name="name">学徒</field><field name="desc"><![CDATA[掌握基础技艺。]]></field></record>
|
||||
<record id="rule_char_3" model="game.rule"><field name="type">character</field><field name="level">3</field><field name="name">熟手</field><field name="desc"><![CDATA[可独立完成任务。]]></field></record>
|
||||
<record id="rule_char_4" model="game.rule"><field name="type">character</field><field name="level">4</field><field name="name">好手</field><field name="desc"><![CDATA[在某领域崭露头角。]]></field></record>
|
||||
<record id="rule_char_5" model="game.rule"><field name="type">character</field><field name="level">5</field><field name="name">精英</field><field name="desc"><![CDATA[群体中的中坚。]]></field></record>
|
||||
<record id="rule_char_6" model="game.rule"><field name="type">character</field><field name="level">6</field><field name="name">大师</field><field name="desc"><![CDATA[技艺臻于化境。]]></field></record>
|
||||
<record id="rule_char_7" model="game.rule"><field name="type">character</field><field name="level">7</field><field name="name">宗匠</field><field name="desc"><![CDATA[开宗立派的人物。]]></field></record>
|
||||
<record id="rule_char_8" model="game.rule"><field name="type">character</field><field name="level">8</field><field name="name">传奇</field><field name="desc"><![CDATA[事迹被载入编年史。]]></field></record>
|
||||
<record id="rule_char_9" model="game.rule"><field name="type">character</field><field name="level">9</field><field name="name">神话</field><field name="desc"><![CDATA[近乎传说的存在。]]></field></record>
|
||||
<record id="rule_char_10" model="game.rule"><field name="type">character</field><field name="level">10</field><field name="name">半神</field><field name="desc"><![CDATA[触及世界法则。]]></field></record>
|
||||
<record id="rule_char_11" model="game.rule"><field name="type">character</field><field name="level">11</field><field name="name">圣者</field><field name="desc"><![CDATA[被信仰托举的意志。]]></field></record>
|
||||
<record id="rule_char_12" model="game.rule"><field name="type">character</field><field name="level">12</field><field name="name">永恒</field><field name="desc"><![CDATA[超越生死的世界锚点。]]></field></record>
|
||||
<record id="rule_char_1" model="game.rule"><field name="category_id" ref="rule_category_character"/><field name="level">1</field><field name="name">萌新</field><field name="desc"><![CDATA[初入世界的无名者。]]></field></record>
|
||||
<record id="rule_char_2" model="game.rule"><field name="category_id" ref="rule_category_character"/><field name="level">2</field><field name="name">学徒</field><field name="desc"><![CDATA[掌握基础技艺。]]></field></record>
|
||||
<record id="rule_char_3" model="game.rule"><field name="category_id" ref="rule_category_character"/><field name="level">3</field><field name="name">熟手</field><field name="desc"><![CDATA[可独立完成任务。]]></field></record>
|
||||
<record id="rule_char_4" model="game.rule"><field name="category_id" ref="rule_category_character"/><field name="level">4</field><field name="name">好手</field><field name="desc"><![CDATA[在某领域崭露头角。]]></field></record>
|
||||
<record id="rule_char_5" model="game.rule"><field name="category_id" ref="rule_category_character"/><field name="level">5</field><field name="name">精英</field><field name="desc"><![CDATA[群体中的中坚。]]></field></record>
|
||||
<record id="rule_char_6" model="game.rule"><field name="category_id" ref="rule_category_character"/><field name="level">6</field><field name="name">大师</field><field name="desc"><![CDATA[技艺臻于化境。]]></field></record>
|
||||
<record id="rule_char_7" model="game.rule"><field name="category_id" ref="rule_category_character"/><field name="level">7</field><field name="name">宗匠</field><field name="desc"><![CDATA[开宗立派的人物。]]></field></record>
|
||||
<record id="rule_char_8" model="game.rule"><field name="category_id" ref="rule_category_character"/><field name="level">8</field><field name="name">传奇</field><field name="desc"><![CDATA[事迹被载入编年史。]]></field></record>
|
||||
<record id="rule_char_9" model="game.rule"><field name="category_id" ref="rule_category_character"/><field name="level">9</field><field name="name">神话</field><field name="desc"><![CDATA[近乎传说的存在。]]></field></record>
|
||||
<record id="rule_char_10" model="game.rule"><field name="category_id" ref="rule_category_character"/><field name="level">10</field><field name="name">半神</field><field name="desc"><![CDATA[触及世界法则。]]></field></record>
|
||||
<record id="rule_char_11" model="game.rule"><field name="category_id" ref="rule_category_character"/><field name="level">11</field><field name="name">圣者</field><field name="desc"><![CDATA[被信仰托举的意志。]]></field></record>
|
||||
<record id="rule_char_12" model="game.rule"><field name="category_id" ref="rule_category_character"/><field name="level">12</field><field name="name">永恒</field><field name="desc"><![CDATA[超越生死的世界锚点。]]></field></record>
|
||||
|
||||
<!-- ============ 货币等级 · Lv.1-6 ============ -->
|
||||
<record id="currency_1" model="game.currency">
|
||||
<field name="level">1</field><field name="name">墟铢</field><field name="rate">100</field>
|
||||
<!-- ============ 货币等级 · Lv.1-6(合并进 game.rule) ============ -->
|
||||
<record id="currency_1" model="game.rule">
|
||||
<field name="category_id" ref="rule_category_currency"/><field name="level">1</field><field name="name">墟铢</field>
|
||||
<field name="rate">100</field>
|
||||
<field name="desc"><![CDATA[凡俗通用零钱,以贝壳、骨片、碎石铸成。100 墟铢 = 1 灵币。]]></field>
|
||||
<field name="note"><![CDATA[发行方:各地聚落自治铸坊。流通范围:地表层全域通用,地下层部分区域接受。]]></field>
|
||||
</record>
|
||||
<record id="currency_2" model="game.currency">
|
||||
<field name="level">2</field><field name="name">灵币</field><field name="rate">100</field>
|
||||
<record id="currency_2" model="game.rule">
|
||||
<field name="category_id" ref="rule_category_currency"/><field name="level">2</field><field name="name">灵币</field>
|
||||
<field name="rate">100</field>
|
||||
<field name="desc"><![CDATA[蕴含微量灵气的金属铸币,聚落以上势力日常使用。100 灵币 = 1 玄锭。]]></field>
|
||||
<field name="note"><![CDATA[发行方:玄邑级势力统一铸造。流通范围:凡墟/灵启/玄界三层硬通货。]]></field>
|
||||
</record>
|
||||
<record id="currency_3" model="game.currency">
|
||||
<field name="level">3</field><field name="name">玄锭</field><field name="rate">100</field>
|
||||
<record id="currency_3" model="game.rule">
|
||||
<field name="category_id" ref="rule_category_currency"/><field name="level">3</field><field name="name">玄锭</field>
|
||||
<field name="rate">100</field>
|
||||
<field name="desc"><![CDATA[刻有玄纹的稀有锭块,用于高价值交易与功法图纸。100 玄锭 = 1 冥玉。]]></field>
|
||||
<field name="note"><![CDATA[发行方:冥朝中央造币司。流通范围:跨层贸易结算单位。]]></field>
|
||||
</record>
|
||||
<record id="currency_4" model="game.currency">
|
||||
<field name="level">4</field><field name="name">冥玉</field><field name="rate">100</field>
|
||||
<record id="currency_4" model="game.rule">
|
||||
<field name="category_id" ref="rule_category_currency"/><field name="level">4</field><field name="name">冥玉</field>
|
||||
<field name="rate">100</field>
|
||||
<field name="desc"><![CDATA[产自地底深渊的幽暗玉石,蕴含浓郁能量。100 冥玉 = 1 穹鎏。]]></field>
|
||||
<field name="note"><![CDATA[产地:地下层深渊矿脉。流通范围:穹冥时代以上跨层交易核心货币。]]></field>
|
||||
</record>
|
||||
<record id="currency_5" model="game.currency">
|
||||
<field name="level">5</field><field name="name">穹鎏</field><field name="rate">100</field>
|
||||
<record id="currency_5" model="game.rule">
|
||||
<field name="category_id" ref="rule_category_currency"/><field name="level">5</field><field name="name">穹鎏</field>
|
||||
<field name="rate">100</field>
|
||||
<field name="desc"><![CDATA[来自星空矿脉的稀有鎏金晶体,跨层贸易硬通货。100 穹鎏 = 1 元玺。]]></field>
|
||||
<field name="note"><![CDATA[产地:星空层陨石带。流通范围:溯盟级势力间储备货币。]]></field>
|
||||
</record>
|
||||
<record id="currency_6" model="game.currency">
|
||||
<field name="level">6</field><field name="name">元玺</field><field name="rate">1</field>
|
||||
<record id="currency_6" model="game.rule">
|
||||
<field name="category_id" ref="rule_category_currency"/><field name="level">6</field><field name="name">元玺</field>
|
||||
<field name="rate">1</field>
|
||||
<field name="desc"><![CDATA[追溯世界本源凝聚而成的至高货币,可兑换神器、传说事件与维度权限。]]></field>
|
||||
<field name="note"><![CDATA[至高货币,不可增发。总量由世界本源锚定,仅可通过特定事件获取。]]></field>
|
||||
</record>
|
||||
|
||||
<!-- ============ 图鉴样例(每类 1 条,供用户参考扩展;按分类+子级分组) ============ -->
|
||||
@ -183,6 +217,32 @@
|
||||
<field name="desc">每个 NPC 都有独立的天赋、性格与成长轨迹。英雄在历练中觉醒,传奇在时间中自发生成,无人是棋子。</field>
|
||||
</record>
|
||||
|
||||
<!-- 介绍 -->
|
||||
<record id="page_intro" model="game.page">
|
||||
<field name="key">intro</field>
|
||||
<field name="title">关于宇森</field>
|
||||
<field name="subtitle">ABOUT YUSEN</field>
|
||||
<field name="sequence">15</field>
|
||||
<field name="body_html"><![CDATA[<p>宇森,是一颗会自己呼吸的种子。</p>
|
||||
<p>你播种的不是一个角色,而是一个<b style="color:#fff">世界</b>——从地心熔岩到陨石星空,五层垂直切面层层相托,文明在其中自发地生长、分裂、兼并、飞升。没有脚本替你演绎剧情,没有关卡替你定义胜利,每一次开局都是一部独一无二的编年史。</p>
|
||||
<p>我们相信,最好的模拟不在于你操控了多少,而在于你<b style="color:#fff">见证</b>了多少。所以宇森把「玩家」重新定义为<b style="color:var(--gold)">园丁</b>:你不能命令谁,却可以改变土壤、调节灵气、在关键节点轻轻推一把。世界的走向,由无数个体的选择汇聚而成,而你只是其中一股温柔的力。</p>
|
||||
<p style="margin-top:22px;color:var(--gold);font-style:italic;">往下翻,看这颗种子如何长成一片森林。</p>]]></field>
|
||||
</record>
|
||||
<record id="page_intro_item_1" model="game.page.item">
|
||||
<field name="page_id" ref="page_intro"/>
|
||||
<field name="sequence">10</field>
|
||||
<field name="icon">🌍</field>
|
||||
<field name="title">一颗会呼吸的世界</field>
|
||||
<field name="desc">五层垂直世界,文明自发演化,没有固定剧情与通关条件。</field>
|
||||
</record>
|
||||
<record id="page_intro_item_2" model="game.page.item">
|
||||
<field name="page_id" ref="page_intro"/>
|
||||
<field name="sequence">20</field>
|
||||
<field name="icon">🧑🌾</field>
|
||||
<field name="title">你是园丁,不是上帝</field>
|
||||
<field name="desc">改变土壤与灵气,在关键节点轻推一把,而非发号施令。</field>
|
||||
</record>
|
||||
|
||||
<!-- 背景 -->
|
||||
<record id="page_background" model="game.page">
|
||||
<field name="key">background</field>
|
||||
@ -233,6 +293,48 @@
|
||||
<field name="desc">本源之地 · 元玺凝聚</field>
|
||||
</record>
|
||||
|
||||
<!-- 说明 -->
|
||||
<record id="page_notes" model="game.page">
|
||||
<field name="key">notes</field>
|
||||
<field name="title">制度说明</field>
|
||||
<field name="subtitle">HOW THE WORLD WORKS</field>
|
||||
<field name="sequence">25</field>
|
||||
<field name="body_html"><![CDATA[<p>宇森的世界由三套相互独立的等级体系共同驱动,它们彼此加成,又彼此制约:</p>
|
||||
<p><b style="color:#fff">世界等级</b>是舞台的上限——它决定这片土地上「能发生什么」。从凡墟到溯元,每一级跃迁都会解锁更深的内容层(更深的地下、更远的星空)与更厚的事件池。</p>
|
||||
<p><b style="color:#fff">势力等级</b>是群体的进度——它决定文明「能调动什么」。聚落、联盟到多维联合体,等级越高,可调度资源与外交权重越大。</p>
|
||||
<p><b style="color:#fff">角色等级</b>是个体的成长——它决定一个生命「能做成什么」。从无名萌新到超越生死的永恒,英雄在历练中觉醒。</p>
|
||||
<p>而贯穿三者的,是<b style="color:var(--gold)">六档货币</b>:从凡俗墟铢到本源元玺,百进制兑换构成完整经济闭环,决定文明「能交换什么」。</p>
|
||||
<p>园丁的核心策略,便是在这四方之间分配有限的引导资源,让文明自洽地向上演化——每一级提升,世界都因此越活越厚。</p>]]></field>
|
||||
</record>
|
||||
<record id="page_notes_item_1" model="game.page.item">
|
||||
<field name="page_id" ref="page_notes"/>
|
||||
<field name="sequence">10</field>
|
||||
<field name="icon">🌐</field>
|
||||
<field name="title">世界等级</field>
|
||||
<field name="desc">舞台上限:决定能发生什么。凡墟 → 灵启 → 玄界 → 穹冥 → 溯元。</field>
|
||||
</record>
|
||||
<record id="page_notes_item_2" model="game.page.item">
|
||||
<field name="page_id" ref="page_notes"/>
|
||||
<field name="sequence">20</field>
|
||||
<field name="icon">🏛️</field>
|
||||
<field name="title">势力等级</field>
|
||||
<field name="desc">群体进度:决定能调动什么。墟族 → 灵聚 → 玄邑 → 冥朝 → 穹统 → 溯盟。</field>
|
||||
</record>
|
||||
<record id="page_notes_item_3" model="game.page.item">
|
||||
<field name="page_id" ref="page_notes"/>
|
||||
<field name="sequence">30</field>
|
||||
<field name="icon">⚔️</field>
|
||||
<field name="title">角色等级</field>
|
||||
<field name="desc">个体成长:决定能做成什么。萌新 → …… → 永恒,十二阶序。</field>
|
||||
</record>
|
||||
<record id="page_notes_item_4" model="game.page.item">
|
||||
<field name="page_id" ref="page_notes"/>
|
||||
<field name="sequence">40</field>
|
||||
<field name="icon">💰</field>
|
||||
<field name="title">六档货币</field>
|
||||
<field name="desc">经济闭环:决定能交换什么。墟铢 → 灵币 → 玄锭 → 冥玉 → 穹鎏 → 元玺。</field>
|
||||
</record>
|
||||
|
||||
<!-- 致玩家 -->
|
||||
<record id="page_letter" model="game.page">
|
||||
<field name="key">letter</field>
|
||||
@ -249,4 +351,112 @@
|
||||
<p>欢迎播种。</p>]]></field>
|
||||
</record>
|
||||
|
||||
<!-- ============ 论坛种子(登录后可见) ============ -->
|
||||
<record id="thread_welcome" model="game.forum.thread">
|
||||
<field name="name">【公告】宇森官网第一阶段上线,欢迎园丁们入驻</field>
|
||||
<field name="category">discuss</field>
|
||||
<field name="author_name">官方</field>
|
||||
<field name="pinned">1</field>
|
||||
<field name="body_html"><![CDATA[<p>宇森官网第一阶段上线:介绍 / 规则 / 图鉴 / 论坛 / 公告 / 剧情 六块,单页切换呈现。欢迎园丁们入驻,一起把这个世界养活。</p>]]></field>
|
||||
</record>
|
||||
<record id="thread_newbie" model="game.forum.thread">
|
||||
<field name="name">新手向:园丁视角的三种开局思路</field>
|
||||
<field name="category">strategy</field>
|
||||
<field name="author_name">行者</field>
|
||||
<field name="body_html"><![CDATA[<p>① 静观其变:前期少干预,先把灵气浓度调匀;② 重点扶持:选一个族群定向加灵气,加速聚落成型;③ 制造意外:故意制造资源稀缺,观察文明如何应对。你更偏向哪种?</p>]]></field>
|
||||
</record>
|
||||
<record id="thread_lore" model="game.forum.thread">
|
||||
<field name="name">世界观考据:五层世界是怎么来的</field>
|
||||
<field name="category">lore</field>
|
||||
<field name="author_name">雾岛</field>
|
||||
<field name="body_html"><![CDATA[<p>从地心熔岩到陨石星空,五层世界是灵气自下而上攀爬的切面。凡墟→灵启→玄界→穹冥→溯元,每一层都有独立的生态与事件池,文明则是被这五层同时托举的。</p>]]></field>
|
||||
</record>
|
||||
<record id="thread_showcase" model="game.forum.thread">
|
||||
<field name="name">手绘了我的主世界浮空岛,求轻喷</field>
|
||||
<field name="category">showcase</field>
|
||||
<field name="author_name">青柠</field>
|
||||
<field name="body_html"><![CDATA[<p>肝了三个晚上,把我主世界那座浮空岛画出来了,云朵地基块 + 中央灵泉,欢迎来捞图也欢迎拍砖。</p>]]></field>
|
||||
</record>
|
||||
<record id="thread_bug" model="game.forum.thread">
|
||||
<field name="name">反馈:飞入微观时偶现卡顿</field>
|
||||
<field name="category">bug</field>
|
||||
<field name="author_name">K</field>
|
||||
<field name="body_html"><![CDATA[<p>进入微观沙盘的前一两秒偶尔会掉帧,退出再进就正常了。设备是集显笔记本,疑似首帧资源加载没预载,供开发组参考。</p>]]></field>
|
||||
</record>
|
||||
|
||||
<!-- ============ 公告补充 ============ -->
|
||||
<record id="ann_roadmap" model="game.announcement">
|
||||
<field name="name">【前瞻】微观沙盘与剧情站即将开放</field>
|
||||
<field name="date">2026-07-12</field>
|
||||
<field name="priority">important</field>
|
||||
<field name="body_html"><![CDATA[<p>下一阶段将开放「微观沙盘」与「剧情站」。前者让你亲手摆放六边形地块、引导文明生长;后者将连载《园丁手记》系列小说,记录这个世界的来龙去脉。</p>]]></field>
|
||||
</record>
|
||||
<record id="ann_convention" model="game.announcement">
|
||||
<field name="name">【须知】社区公约与发帖规范</field>
|
||||
<field name="date">2026-07-15</field>
|
||||
<field name="priority">normal</field>
|
||||
<field name="body_html"><![CDATA[<p>论坛仅对登录用户开放。请友善交流、尊重原创;同人创作欢迎标注「二创」,攻略与考据可自由转载,转载请注明出处。</p>]]></field>
|
||||
</record>
|
||||
<record id="ann_story" model="game.announcement">
|
||||
<field name="name">【公告】剧情站上线:《园丁手记·凡墟篇》</field>
|
||||
<field name="date">2026-07-20</field>
|
||||
<field name="priority">important</field>
|
||||
<field name="body_html"><![CDATA[<p>剧情站正式上线!首卷《园丁手记·凡墟篇》已开放阅读,记录灵气初醒、第一株苗与聚落第一缕光的蒙昧年代。前往导航「剧情」即可翻阅。</p>]]></field>
|
||||
</record>
|
||||
|
||||
<!-- ============ 剧情(小说)种子 ============ -->
|
||||
<record id="story_vol_fanxu" model="game.story.volume">
|
||||
<field name="name">第一卷 · 园丁手记·凡墟篇</field>
|
||||
<field name="sequence">10</field>
|
||||
<field name="intro"><![CDATA[<p>凡墟篇·园丁手记。灵气初醒的蒙昧年代,文明的种子刚刚落进泥土。</p>]]></field>
|
||||
</record>
|
||||
<record id="story_ch_fx_1" model="game.story.chapter">
|
||||
<field name="volume_id" ref="story_vol_fanxu"/>
|
||||
<field name="sequence">10</field>
|
||||
<field name="name">楔子 · 灵气初醒</field>
|
||||
<field name="body_html"><![CDATA[<p>天地初开的时候,没有名字。</p>
|
||||
<p>只有一团混沌的灵气,从地心最深处艰难地往上爬——它穿过滚烫的岩浆,穿过沉默的岩层,在某个谁也说不清的时刻,第一次触到了冰冷的地表。</p>
|
||||
<p>那一刻,凡土醒了。</p>
|
||||
<p>后来的人把它叫做「灵气初醒」。但在当时,没有谁记得这一刻。只有风,把第一缕微弱的生机,吹向了荒野里零散的族群。</p>]]></field>
|
||||
</record>
|
||||
<record id="story_ch_fx_2" model="game.story.chapter">
|
||||
<field name="volume_id" ref="story_vol_fanxu"/>
|
||||
<field name="sequence">20</field>
|
||||
<field name="name">第一章 · 第一株苗</field>
|
||||
<field name="body_html"><![CDATA[<p>园丁第一次伸手,是在一片荒芜的斜坡上。</p>
|
||||
<p>他什么也没做,只是把掌心的灵气,轻轻按进了一粒埋在石缝里的种子。没有咒语,没有仪式——只是给予,然后等待。</p>
|
||||
<p>三天后,一株嫩绿的苗破土而出。它不知道自己被谁照料,也不在乎。它只是生长,像这个世界本该有的样子。</p>
|
||||
<p>园丁看着那点绿色,忽然明白了自己该做的事:不是统治,不是创造,而是——让生长发生。</p>]]></field>
|
||||
</record>
|
||||
<record id="story_ch_fx_3" model="game.story.chapter">
|
||||
<field name="volume_id" ref="story_vol_fanxu"/>
|
||||
<field name="sequence">30</field>
|
||||
<field name="name">第二章 · 聚落的第一缕光</field>
|
||||
<field name="body_html"><![CDATA[<p>苗活下来之后,荒野里有了第二个、第三个生命。</p>
|
||||
<p>零散的族群循着灵气的痕迹聚拢。他们不再只是狩猎采集,开始用碎石垒出简陋的屋舍,用兽骨交换 surplus 的猎物。第一缕属于「文明」的光,在凡墟的夜色里亮起。</p>
|
||||
<p>园丁没有现身。他只是把灵气的浓度,往聚落的方向,悄悄调浓了半分。</p>]]></field>
|
||||
</record>
|
||||
|
||||
<record id="story_vol_lingqi" model="game.story.volume">
|
||||
<field name="name">第二卷 · 园丁手记·灵启篇</field>
|
||||
<field name="sequence">20</field>
|
||||
<field name="intro"><![CDATA[<p>灵启篇·园丁手记。当聚落连成部族,争与和同时降临。</p>]]></field>
|
||||
</record>
|
||||
<record id="story_ch_lq_1" model="game.story.chapter">
|
||||
<field name="volume_id" ref="story_vol_lingqi"/>
|
||||
<field name="sequence">10</field>
|
||||
<field name="name">第一章 · 部族初成</field>
|
||||
<field name="body_html"><![CDATA[<p>灵气浓到一定程度,聚落开始兼并。</p>
|
||||
<p>三个相邻的族群,在一场没有流血的议事之后,决定共用一口深井。这是凡墟历史上第一次「联盟」——没有王,只有约定。</p>
|
||||
<p>园丁在云端记下了这一笔。他知道,从今往后,世界不再只是一群野生的人,而是一个会自己谈判、自己妥协的活物。</p>]]></field>
|
||||
</record>
|
||||
<record id="story_ch_lq_2" model="game.story.chapter">
|
||||
<field name="volume_id" ref="story_vol_lingqi"/>
|
||||
<field name="sequence">20</field>
|
||||
<field name="name">第二章 · 交易与争端</field>
|
||||
<field name="body_html"><![CDATA[<p>联盟带来了繁荣,也带来了第一场争端。</p>
|
||||
<p>东边的墟族说井水该归他们,西边的人不肯。争执了七天,最后是园丁在不经意间,让井边多长出一片可食用的苔藓——谁都能采,谁都不必独占。</p>
|
||||
<p>争端平息的方式,从来不在胜负,而在有没有第三种选择。这是园丁学到的第二课。</p>]]></field>
|
||||
</record>
|
||||
|
||||
</odoo>
|
||||
|
||||
9
addons/game_base/migrations/1.2/pre-migrate.py
Normal file
@ -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'")
|
||||
@ -1,6 +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
|
||||
|
||||
@ -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
|
||||
@ -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
|
||||
|
||||
18
addons/game_base/models/rule_category.py
Normal file
@ -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)
|
||||
27
addons/game_base/models/story.py
Normal file
@ -0,0 +1,27 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from odoo import models, fields
|
||||
|
||||
|
||||
class GameStoryVolume(models.Model):
|
||||
_name = 'game.story.volume'
|
||||
_description = '小说卷'
|
||||
_order = 'sequence, id'
|
||||
_rec_name = 'name'
|
||||
|
||||
name = fields.Char(string='卷名', required=True, index=True)
|
||||
sequence = fields.Integer(string='序号', default=10, index=True)
|
||||
intro = fields.Html(string='卷首语')
|
||||
chapter_ids = fields.One2many('game.story.chapter', 'volume_id', string='章节')
|
||||
|
||||
|
||||
class GameStoryChapter(models.Model):
|
||||
_name = 'game.story.chapter'
|
||||
_description = '小说章节'
|
||||
_order = 'volume_id, sequence, id'
|
||||
_rec_name = 'name'
|
||||
|
||||
volume_id = fields.Many2one('game.story.volume', string='所属卷',
|
||||
required=True, ondelete='cascade', index=True)
|
||||
name = fields.Char(string='章名', required=True, index=True)
|
||||
sequence = fields.Integer(string='序号', default=10, index=True)
|
||||
body_html = fields.Html(string='正文')
|
||||
@ -7,11 +7,15 @@ 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
|
||||
access_game_page_item_user,game.page.item user,model_game_page_item,base.group_user,1,1,1,1
|
||||
access_game_story_volume,game.story.volume public,model_game_story_volume,base.group_public,1,0,0,0
|
||||
access_game_story_volume_user,game.story.volume user,model_game_story_volume,base.group_user,1,1,1,1
|
||||
access_game_story_chapter,game.story.chapter public,model_game_story_chapter,base.group_public,1,0,0,0
|
||||
access_game_story_chapter_user,game.story.chapter user,model_game_story_chapter,base.group_user,1,1,1,1
|
||||
|
||||
|
30
addons/game_base/views/announcement_views.xml
Normal file
@ -0,0 +1,30 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
|
||||
<!-- ============ 公告 ============ -->
|
||||
<record id="action_game_announcement" model="ir.actions.act_window">
|
||||
<field name="name">公告</field>
|
||||
<field name="res_model">game.announcement</field>
|
||||
<field name="view_mode">list,form</field>
|
||||
</record>
|
||||
|
||||
<record id="view_game_announcement_list" model="ir.ui.view">
|
||||
<field name="name">game.announcement.list</field>
|
||||
<field name="model">game.announcement</field>
|
||||
<field name="arch" type="xml">
|
||||
<list><field name="name"/><field name="date"/><field name="priority"/><field name="active"/></list>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="view_game_announcement_form" model="ir.ui.view">
|
||||
<field name="name">game.announcement.form</field>
|
||||
<field name="model">game.announcement</field>
|
||||
<field name="arch" type="xml">
|
||||
<form>
|
||||
<group><field name="name"/><field name="date"/><field name="priority"/><field name="active"/></group>
|
||||
<field name="body_html"/>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
</odoo>
|
||||
39
addons/game_base/views/codex_views.xml
Normal file
@ -0,0 +1,39 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
|
||||
<!-- ============ 图鉴(按分类 + 子级分组) ============ -->
|
||||
<record id="action_game_codex" model="ir.actions.act_window">
|
||||
<field name="name">图鉴</field>
|
||||
<field name="res_model">game.codex.entry</field>
|
||||
<field name="view_mode">list,form</field>
|
||||
<field name="context">{'group_by': 'cat'}</field>
|
||||
</record>
|
||||
|
||||
<record id="view_game_codex_list" model="ir.ui.view">
|
||||
<field name="name">game.codex.entry.list</field>
|
||||
<field name="model">game.codex.entry</field>
|
||||
<field name="arch" type="xml">
|
||||
<list>
|
||||
<field name="cat" column_invisible="1"/><field name="sub"/><field name="name"/><field name="tier"/><field name="image"/>
|
||||
</list>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="view_game_codex_form" model="ir.ui.view">
|
||||
<field name="name">game.codex.entry.form</field>
|
||||
<field name="model">game.codex.entry</field>
|
||||
<field name="arch" type="xml">
|
||||
<form>
|
||||
<group>
|
||||
<field name="cat"/><field name="sub"/><field name="tier"/>
|
||||
</group>
|
||||
<group>
|
||||
<field name="name"/>
|
||||
</group>
|
||||
<field name="image" widget="image"/>
|
||||
<field name="desc"/>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
</odoo>
|
||||
61
addons/game_base/views/forum_views.xml
Normal file
@ -0,0 +1,61 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
|
||||
<!-- ============ 论坛 · 主题 ============ -->
|
||||
<record id="action_game_forum_thread" model="ir.actions.act_window">
|
||||
<field name="name">主题</field>
|
||||
<field name="res_model">game.forum.thread</field>
|
||||
<field name="view_mode">list,form</field>
|
||||
</record>
|
||||
|
||||
<record id="view_game_forum_thread_list" model="ir.ui.view">
|
||||
<field name="name">game.forum.thread.list</field>
|
||||
<field name="model">game.forum.thread</field>
|
||||
<field name="arch" type="xml">
|
||||
<list>
|
||||
<field name="pinned"/><field name="name"/><field name="category"/><field name="author_name"/><field name="create_date"/>
|
||||
</list>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="view_game_forum_thread_form" model="ir.ui.view">
|
||||
<field name="name">game.forum.thread.form</field>
|
||||
<field name="model">game.forum.thread</field>
|
||||
<field name="arch" type="xml">
|
||||
<form>
|
||||
<group>
|
||||
<field name="name"/><field name="category"/><field name="author_name"/><field name="pinned"/>
|
||||
</group>
|
||||
<field name="body_html"/>
|
||||
<field name="post_ids">
|
||||
<tree><field name="author_name"/><field name="create_date"/></tree>
|
||||
<form><field name="author_name"/><field name="body_html"/></form>
|
||||
</field>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- ============ 论坛 · 回复 ============ -->
|
||||
<record id="action_game_forum_post" model="ir.actions.act_window">
|
||||
<field name="name">回复</field>
|
||||
<field name="res_model">game.forum.post</field>
|
||||
<field name="view_mode">list,form</field>
|
||||
</record>
|
||||
|
||||
<record id="view_game_forum_post_list" model="ir.ui.view">
|
||||
<field name="name">game.forum.post.list</field>
|
||||
<field name="model">game.forum.post</field>
|
||||
<field name="arch" type="xml">
|
||||
<list><field name="thread_id"/><field name="author_name"/><field name="create_date"/></list>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="view_game_forum_post_form" model="ir.ui.view">
|
||||
<field name="name">game.forum.post.form</field>
|
||||
<field name="model">game.forum.post</field>
|
||||
<field name="arch" type="xml">
|
||||
<form><field name="thread_id"/><field name="author_name"/><field name="body_html"/></form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
</odoo>
|
||||
@ -1,206 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
|
||||
<!-- ============ 顶层菜单(带品牌 logo) ============ -->
|
||||
<menuitem id="menu_game_root" name="宇森游戏" sequence="10"
|
||||
web_icon="game_base,static/description/icon.png"/>
|
||||
|
||||
<!-- ============ 图鉴(按分类 + 子级分组) ============ -->
|
||||
<record id="action_game_codex" model="ir.actions.act_window">
|
||||
<field name="name">图鉴</field>
|
||||
<field name="res_model">game.codex.entry</field>
|
||||
<field name="view_mode">list,form</field>
|
||||
</record>
|
||||
<menuitem id="menu_game_codex" name="图鉴" parent="menu_game_root" action="action_game_codex" sequence="10"/>
|
||||
|
||||
<record id="view_game_codex_list" model="ir.ui.view">
|
||||
<field name="name">game.codex.entry.list</field>
|
||||
<field name="model">game.codex.entry</field>
|
||||
<field name="arch" type="xml">
|
||||
<list>
|
||||
<field name="cat"/><field name="sub"/><field name="name"/><field name="tier"/><field name="image"/>
|
||||
</list>
|
||||
</field>
|
||||
</record>
|
||||
<record id="view_game_codex_form" model="ir.ui.view">
|
||||
<field name="name">game.codex.entry.form</field>
|
||||
<field name="model">game.codex.entry</field>
|
||||
<field name="arch" type="xml">
|
||||
<form>
|
||||
<group>
|
||||
<field name="cat"/><field name="sub"/><field name="tier"/>
|
||||
</group>
|
||||
<group>
|
||||
<field name="name"/>
|
||||
</group>
|
||||
<field name="image" widget="image"/>
|
||||
<field name="desc"/>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- ============ 规则(菜单仅保留「规则」二字,按类别分组) ============ -->
|
||||
<record id="action_game_rule" model="ir.actions.act_window">
|
||||
<field name="name">规则</field>
|
||||
<field name="res_model">game.rule</field>
|
||||
<field name="view_mode">list,form</field>
|
||||
</record>
|
||||
<menuitem id="menu_game_rule" name="规则" parent="menu_game_root" action="action_game_rule" sequence="20"/>
|
||||
<record id="view_game_rule_list" model="ir.ui.view">
|
||||
<field name="name">game.rule.list</field>
|
||||
<field name="model">game.rule</field>
|
||||
<field name="arch" type="xml">
|
||||
<list><field name="type"/><field name="level"/><field name="name"/></list>
|
||||
</field>
|
||||
</record>
|
||||
<record id="view_game_rule_form" model="ir.ui.view">
|
||||
<field name="name">game.rule.form</field>
|
||||
<field name="model">game.rule</field>
|
||||
<field name="arch" type="xml">
|
||||
<form><group><field name="type"/><field name="level"/><field name="name"/></group><field name="desc"/></form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- ============ 货币等级 ============ -->
|
||||
<record id="action_game_currency" model="ir.actions.act_window">
|
||||
<field name="name">货币等级</field>
|
||||
<field name="res_model">game.currency</field>
|
||||
<field name="view_mode">list,form</field>
|
||||
</record>
|
||||
<menuitem id="menu_game_currency" name="货币等级" parent="menu_game_root" action="action_game_currency" sequence="25"/>
|
||||
<record id="view_game_currency_list" model="ir.ui.view">
|
||||
<field name="name">game.currency.list</field>
|
||||
<field name="model">game.currency</field>
|
||||
<field name="arch" type="xml">
|
||||
<list><field name="level"/><field name="name"/><field name="rate"/></list>
|
||||
</field>
|
||||
</record>
|
||||
<record id="view_game_currency_form" model="ir.ui.view">
|
||||
<field name="name">game.currency.form</field>
|
||||
<field name="model">game.currency</field>
|
||||
<field name="arch" type="xml">
|
||||
<form><group><field name="level"/><field name="name"/><field name="rate"/></group><field name="desc"/></form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- ============ 页面板块 ============ -->
|
||||
<record id="action_game_page" model="ir.actions.act_window">
|
||||
<field name="name">页面板块</field>
|
||||
<field name="res_model">game.page</field>
|
||||
<field name="view_mode">list,form</field>
|
||||
</record>
|
||||
<menuitem id="menu_game_page" name="页面板块" parent="menu_game_root" action="action_game_page" sequence="27"/>
|
||||
<record id="view_game_page_list" model="ir.ui.view">
|
||||
<field name="name">game.page.list</field>
|
||||
<field name="model">game.page</field>
|
||||
<field name="arch" type="xml">
|
||||
<list>
|
||||
<field name="sequence"/><field name="key"/><field name="title"/><field name="subtitle"/><field name="active"/>
|
||||
</list>
|
||||
</field>
|
||||
</record>
|
||||
<record id="view_game_page_form" model="ir.ui.view">
|
||||
<field name="name">game.page.form</field>
|
||||
<field name="model">game.page</field>
|
||||
<field name="arch" type="xml">
|
||||
<form>
|
||||
<group>
|
||||
<field name="key"/><field name="title"/><field name="subtitle"/><field name="sequence"/><field name="active"/>
|
||||
</group>
|
||||
<notebook>
|
||||
<page string="正文" name="body">
|
||||
<field name="body_html" widget="html" options="{'collaborative': true}"/>
|
||||
</page>
|
||||
<page string="条目" name="items">
|
||||
<field name="item_ids">
|
||||
<list editable="bottom">
|
||||
<field name="sequence" widget="handle"/><field name="icon"/><field name="title"/><field name="desc"/>
|
||||
</list>
|
||||
</field>
|
||||
</page>
|
||||
</notebook>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- ============ 论坛(放后面) ============ -->
|
||||
<menuitem id="menu_game_forum" name="论坛" parent="menu_game_root" sequence="30"/>
|
||||
<record id="action_game_forum_thread" model="ir.actions.act_window">
|
||||
<field name="name">主题</field>
|
||||
<field name="res_model">game.forum.thread</field>
|
||||
<field name="view_mode">list,form</field>
|
||||
</record>
|
||||
<menuitem id="menu_game_forum_thread" name="主题" parent="menu_game_forum" action="action_game_forum_thread" sequence="31"/>
|
||||
<record id="action_game_forum_post" model="ir.actions.act_window">
|
||||
<field name="name">回复</field>
|
||||
<field name="res_model">game.forum.post</field>
|
||||
<field name="view_mode">list,form</field>
|
||||
</record>
|
||||
<menuitem id="menu_game_forum_post" name="回复" parent="menu_game_forum" action="action_game_forum_post" sequence="32"/>
|
||||
|
||||
<record id="view_game_forum_thread_list" model="ir.ui.view">
|
||||
<field name="name">game.forum.thread.list</field>
|
||||
<field name="model">game.forum.thread</field>
|
||||
<field name="arch" type="xml">
|
||||
<list>
|
||||
<field name="pinned"/><field name="name"/><field name="category"/><field name="author_name"/><field name="create_date"/>
|
||||
</list>
|
||||
</field>
|
||||
</record>
|
||||
<record id="view_game_forum_thread_form" model="ir.ui.view">
|
||||
<field name="name">game.forum.thread.form</field>
|
||||
<field name="model">game.forum.thread</field>
|
||||
<field name="arch" type="xml">
|
||||
<form>
|
||||
<group>
|
||||
<field name="name"/><field name="category"/><field name="author_name"/><field name="pinned"/>
|
||||
</group>
|
||||
<field name="body_html"/>
|
||||
<field name="post_ids">
|
||||
<tree><field name="author_name"/><field name="create_date"/></tree>
|
||||
<form><field name="author_name"/><field name="body_html"/></form>
|
||||
</field>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
<record id="view_game_forum_post_list" model="ir.ui.view">
|
||||
<field name="name">game.forum.post.list</field>
|
||||
<field name="model">game.forum.post</field>
|
||||
<field name="arch" type="xml">
|
||||
<list><field name="thread_id"/><field name="author_name"/><field name="create_date"/></list>
|
||||
</field>
|
||||
</record>
|
||||
<record id="view_game_forum_post_form" model="ir.ui.view">
|
||||
<field name="name">game.forum.post.form</field>
|
||||
<field name="model">game.forum.post</field>
|
||||
<field name="arch" type="xml">
|
||||
<form><field name="thread_id"/><field name="author_name"/><field name="body_html"/></form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- ============ 公告(放最后) ============ -->
|
||||
<record id="action_game_announcement" model="ir.actions.act_window">
|
||||
<field name="name">公告</field>
|
||||
<field name="res_model">game.announcement</field>
|
||||
<field name="view_mode">list,form</field>
|
||||
</record>
|
||||
<menuitem id="menu_game_announcement" name="公告" parent="menu_game_root" action="action_game_announcement" sequence="40"/>
|
||||
<record id="view_game_announcement_list" model="ir.ui.view">
|
||||
<field name="name">game.announcement.list</field>
|
||||
<field name="model">game.announcement</field>
|
||||
<field name="arch" type="xml">
|
||||
<list><field name="name"/><field name="date"/><field name="priority"/><field name="active"/></list>
|
||||
</field>
|
||||
</record>
|
||||
<record id="view_game_announcement_form" model="ir.ui.view">
|
||||
<field name="name">game.announcement.form</field>
|
||||
<field name="model">game.announcement</field>
|
||||
<field name="arch" type="xml">
|
||||
<form>
|
||||
<group><field name="name"/><field name="date"/><field name="priority"/><field name="active"/></group>
|
||||
<field name="body_html"/>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
</odoo>
|
||||
41
addons/game_base/views/menu.xml
Normal file
@ -0,0 +1,41 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
|
||||
<!-- ============ 顶层菜单(带品牌 logo) ============ -->
|
||||
<menuitem id="menu_game_root" name="宇森游戏" sequence="10"
|
||||
web_icon="game_base,static/description/icon.png"/>
|
||||
|
||||
<!-- 图鉴 -->
|
||||
<menuitem id="menu_game_codex" name="图鉴" parent="menu_game_root" action="action_game_codex" sequence="10"/>
|
||||
|
||||
<!-- 规则(容器:规则内容) -->
|
||||
<menuitem id="menu_game_rule" name="规则" parent="menu_game_root" sequence="20"/>
|
||||
<record model="ir.ui.menu" id="menu_game_rule">
|
||||
<field name="action" eval="False"/>
|
||||
</record>
|
||||
<menuitem id="menu_game_rule_entry" name="规则" parent="menu_game_rule" action="action_game_rule" sequence="21"/>
|
||||
|
||||
<!-- 页面板块 -->
|
||||
<menuitem id="menu_game_page" name="页面板块" parent="menu_game_root" action="action_game_page" sequence="27"/>
|
||||
|
||||
<!-- 论坛 -->
|
||||
<menuitem id="menu_game_forum" name="论坛" parent="menu_game_root" sequence="30"/>
|
||||
<menuitem id="menu_game_forum_thread" name="主题" parent="menu_game_forum" action="action_game_forum_thread" sequence="31"/>
|
||||
<menuitem id="menu_game_forum_post" name="回复" parent="menu_game_forum" action="action_game_forum_post" sequence="32"/>
|
||||
|
||||
<!-- 公告 -->
|
||||
<menuitem id="menu_game_announcement" name="公告" parent="menu_game_root" action="action_game_announcement" sequence="40"/>
|
||||
|
||||
<!-- 小说(剧情) -->
|
||||
<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_chapter" name="章节" parent="menu_game_story" action="action_game_story_chapter" sequence="37"/>
|
||||
|
||||
<!-- 配置 -->
|
||||
<menuitem id="menu_game_config" name="配置" parent="menu_game_root" sequence="50"/>
|
||||
<record model="ir.ui.menu" id="menu_game_config">
|
||||
<field name="action" eval="False"/>
|
||||
</record>
|
||||
<menuitem id="menu_game_rule_category" name="规则类别" parent="menu_game_config" action="action_game_rule_category" sequence="51"/>
|
||||
|
||||
</odoo>
|
||||
45
addons/game_base/views/page_views.xml
Normal file
@ -0,0 +1,45 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
|
||||
<!-- ============ 页面板块 ============ -->
|
||||
<record id="action_game_page" model="ir.actions.act_window">
|
||||
<field name="name">页面板块</field>
|
||||
<field name="res_model">game.page</field>
|
||||
<field name="view_mode">list,form</field>
|
||||
</record>
|
||||
|
||||
<record id="view_game_page_list" model="ir.ui.view">
|
||||
<field name="name">game.page.list</field>
|
||||
<field name="model">game.page</field>
|
||||
<field name="arch" type="xml">
|
||||
<list>
|
||||
<field name="title"/>
|
||||
</list>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="view_game_page_form" model="ir.ui.view">
|
||||
<field name="name">game.page.form</field>
|
||||
<field name="model">game.page</field>
|
||||
<field name="arch" type="xml">
|
||||
<form>
|
||||
<group>
|
||||
<field name="key"/><field name="title"/><field name="subtitle"/><field name="sequence"/><field name="active"/>
|
||||
</group>
|
||||
<notebook>
|
||||
<page string="正文" name="body">
|
||||
<field name="body_html" widget="html" options="{'collaborative': true}"/>
|
||||
</page>
|
||||
<page string="条目" name="items">
|
||||
<field name="item_ids">
|
||||
<list editable="bottom">
|
||||
<field name="sequence" widget="handle"/><field name="icon"/><field name="title"/><field name="desc"/>
|
||||
</list>
|
||||
</field>
|
||||
</page>
|
||||
</notebook>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
</odoo>
|
||||
39
addons/game_base/views/rule_category_views.xml
Normal file
@ -0,0 +1,39 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
|
||||
<!-- ============ 规则类别(配置菜单入口) ============ -->
|
||||
<record id="action_game_rule_category" model="ir.actions.act_window">
|
||||
<field name="name">规则类别</field>
|
||||
<field name="res_model">game.rule.category</field>
|
||||
<field name="view_mode">list,form</field>
|
||||
</record>
|
||||
|
||||
<record id="view_game_rule_category_list" model="ir.ui.view">
|
||||
<field name="name">game.rule.category.list</field>
|
||||
<field name="model">game.rule.category</field>
|
||||
<field name="arch" type="xml">
|
||||
<list editable="top">
|
||||
<field name="sequence" widget="handle"/>
|
||||
<field name="key"/>
|
||||
<field name="name"/>
|
||||
<field name="active" widget="boolean_toggle"/>
|
||||
</list>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="view_game_rule_category_form" model="ir.ui.view">
|
||||
<field name="name">game.rule.category.form</field>
|
||||
<field name="model">game.rule.category</field>
|
||||
<field name="arch" type="xml">
|
||||
<form>
|
||||
<group>
|
||||
<field name="key"/>
|
||||
<field name="name"/>
|
||||
<field name="sequence"/>
|
||||
<field name="active" widget="boolean_toggle"/>
|
||||
</group>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
</odoo>
|
||||
43
addons/game_base/views/rule_views.xml
Normal file
@ -0,0 +1,43 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
|
||||
<!-- ============ 规则(按类别分组,列表隐藏类别列) ============ -->
|
||||
<record id="action_game_rule" model="ir.actions.act_window">
|
||||
<field name="name">规则</field>
|
||||
<field name="res_model">game.rule</field>
|
||||
<field name="view_mode">list,form</field>
|
||||
<field name="context">{'group_by': 'category_id'}</field>
|
||||
</record>
|
||||
|
||||
<record id="view_game_rule_list" model="ir.ui.view">
|
||||
<field name="name">game.rule.list</field>
|
||||
<field name="model">game.rule</field>
|
||||
<field name="arch" type="xml">
|
||||
<list>
|
||||
<field name="category_id" column_invisible="1"/>
|
||||
<field name="level"/>
|
||||
<field name="name"/>
|
||||
</list>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="view_game_rule_form" model="ir.ui.view">
|
||||
<field name="name">game.rule.form</field>
|
||||
<field name="model">game.rule</field>
|
||||
<field name="arch" type="xml">
|
||||
<form>
|
||||
<group>
|
||||
<field name="category_id"/>
|
||||
<field name="level"/>
|
||||
<field name="name"/>
|
||||
</group>
|
||||
<group invisible="not category_id or category_id.key != 'currency'">
|
||||
<field name="rate"/>
|
||||
<field name="note"/>
|
||||
</group>
|
||||
<field name="desc"/>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
</odoo>
|
||||
61
addons/game_base/views/story_views.xml
Normal file
@ -0,0 +1,61 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
|
||||
<!-- ============ 小说(剧情) · 卷 ============ -->
|
||||
<record id="action_game_story_volume" model="ir.actions.act_window">
|
||||
<field name="name">卷</field>
|
||||
<field name="res_model">game.story.volume</field>
|
||||
<field name="view_mode">list,form</field>
|
||||
</record>
|
||||
|
||||
<record id="view_game_story_volume_list" model="ir.ui.view">
|
||||
<field name="name">game.story.volume.list</field>
|
||||
<field name="model">game.story.volume</field>
|
||||
<field name="arch" type="xml">
|
||||
<list><field name="sequence"/><field name="name"/><field name="chapter_ids"/></list>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="view_game_story_volume_form" model="ir.ui.view">
|
||||
<field name="name">game.story.volume.form</field>
|
||||
<field name="model">game.story.volume</field>
|
||||
<field name="arch" type="xml">
|
||||
<form>
|
||||
<group><field name="name"/><field name="sequence"/></group>
|
||||
<field name="intro"/>
|
||||
<field name="chapter_ids">
|
||||
<list editable="bottom">
|
||||
<field name="sequence" widget="handle"/><field name="name"/><field name="body_html"/>
|
||||
</list>
|
||||
</field>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- ============ 小说(剧情) · 章节 ============ -->
|
||||
<record id="action_game_story_chapter" model="ir.actions.act_window">
|
||||
<field name="name">章节</field>
|
||||
<field name="res_model">game.story.chapter</field>
|
||||
<field name="view_mode">list,form</field>
|
||||
</record>
|
||||
|
||||
<record id="view_game_story_chapter_list" model="ir.ui.view">
|
||||
<field name="name">game.story.chapter.list</field>
|
||||
<field name="model">game.story.chapter</field>
|
||||
<field name="arch" type="xml">
|
||||
<list><field name="volume_id"/><field name="sequence"/><field name="name"/></list>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="view_game_story_chapter_form" model="ir.ui.view">
|
||||
<field name="name">game.story.chapter.form</field>
|
||||
<field name="model">game.story.chapter</field>
|
||||
<field name="arch" type="xml">
|
||||
<form>
|
||||
<group><field name="volume_id"/><field name="sequence"/><field name="name"/></group>
|
||||
<field name="body_html"/>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
</odoo>
|
||||
@ -15,8 +15,6 @@
|
||||
<list>
|
||||
<field name="name"/>
|
||||
<field name="user_id"/>
|
||||
<field name="seed"/>
|
||||
<field name="world_version"/>
|
||||
<field name="last_active"/>
|
||||
</list>
|
||||
</field>
|
||||
@ -31,8 +29,6 @@
|
||||
<group>
|
||||
<field name="name"/>
|
||||
<field name="user_id"/>
|
||||
<field name="seed"/>
|
||||
<field name="world_version"/>
|
||||
<field name="settings" widget="json"/>
|
||||
<field name="last_active"/>
|
||||
</group>
|
||||
|
||||
BIN
assets/art/transitions/transition_adventure.png
Normal file
|
After Width: | Height: | Size: 1.8 MiB |
BIN
assets/art/transitions/transition_coast.png
Normal file
|
After Width: | Height: | Size: 1.7 MiB |
BIN
assets/art/transitions/transition_dawn.png
Normal file
|
After Width: | Height: | Size: 1.5 MiB |
BIN
assets/art/transitions/transition_dungeon.png
Normal file
|
After Width: | Height: | Size: 1.6 MiB |
BIN
assets/art/transitions/transition_forest.png
Normal file
|
After Width: | Height: | Size: 1.8 MiB |
BIN
assets/art/transitions/transition_night.png
Normal file
|
After Width: | Height: | Size: 1.7 MiB |
BIN
assets/art/transitions/transition_sky.png
Normal file
|
After Width: | Height: | Size: 1.7 MiB |
BIN
assets/art/transitions/transition_skycity.png
Normal file
|
After Width: | Height: | Size: 1.9 MiB |
@ -60,6 +60,12 @@ const API = {
|
||||
async announcements() {
|
||||
return (await fetch(_url('/game/api/announcements'), { credentials: _cred })).json();
|
||||
},
|
||||
async story() {
|
||||
return (await fetch(_url('/game/api/story'), { credentials: _cred })).json();
|
||||
},
|
||||
async storyChapter(cid) {
|
||||
return (await fetch(_url('/game/api/story/chapter/' + cid), { credentials: _cred })).json();
|
||||
},
|
||||
// ---------- 沙盘记忆(yt_world 模块) ----------
|
||||
async world() {
|
||||
return (await fetch('/yt_world/api/world?db=yt_game', { credentials: 'same-origin' })).json();
|
||||
@ -78,4 +84,19 @@ const API = {
|
||||
body: JSON.stringify({ settings }),
|
||||
})).json();
|
||||
},
|
||||
// ---------- 世界数据 / 包裹(后端接口预留:yt_world 暂未实现路由,前端 mock 兜底) ----------
|
||||
async worldData() {
|
||||
try {
|
||||
const r = await fetch('/yt_world/api/data?db=yt_game', { credentials: 'same-origin' });
|
||||
if (!r.ok) return null;
|
||||
return await r.json();
|
||||
} catch (e) { return null; }
|
||||
},
|
||||
async inventory() {
|
||||
try {
|
||||
const r = await fetch('/yt_world/api/inventory?db=yt_game', { credentials: 'same-origin' });
|
||||
if (!r.ok) return null;
|
||||
return await r.json();
|
||||
} catch (e) { return null; }
|
||||
},
|
||||
};
|
||||
|
||||
@ -32,9 +32,6 @@ async function loadPages(){
|
||||
else if (p.key === 'letter') html += letterHTML(p);
|
||||
});
|
||||
if (bg) bg.innerHTML = html; // 特色 + 背景 + 致玩家 平铺进同一页,无子菜单切换
|
||||
|
||||
const story = document.getElementById('story');
|
||||
if (story) story.innerHTML = '<div class="sec-title">剧情<small>STORY</small></div><div style="text-align:center;padding:80px 20px;color:#556677;font-size:15px;"><div style="font-size:48px;margin-bottom:16px;">📖</div>剧情内容正在筹备中…<div style="font-size:13px;margin-top:8px;color:#445566;">敬请期待</div></div>';
|
||||
} catch (e) { /* 失败时保留 HTML 中的静态兜底内容 */ }
|
||||
}
|
||||
|
||||
@ -99,9 +96,51 @@ async function loadAnnounce(){
|
||||
} catch (e) { /* 保留静态兜底内容 */ }
|
||||
}
|
||||
|
||||
// ---------- 剧情(小说阅读器:左侧目录 + 右侧正文) ----------
|
||||
async function loadStory(){
|
||||
const tocList = document.getElementById('storyTocList');
|
||||
const reader = document.getElementById('storyReader');
|
||||
if (!tocList || !reader) return;
|
||||
let data;
|
||||
try { data = await API.story(); }
|
||||
catch (e) { tocList.innerHTML = '<div class="toc-loading">目录加载失败</div>'; return; }
|
||||
const volumes = (data && data.volumes) || [];
|
||||
if (!volumes.length){ tocList.innerHTML = '<div class="toc-loading">暂无内容</div>'; return; }
|
||||
|
||||
const volNameByCh = {};
|
||||
let firstCid = null;
|
||||
const html = volumes.map(v => {
|
||||
const chs = (v.chapters || []).map(c => {
|
||||
volNameByCh[c.id] = v.name;
|
||||
if (firstCid === null) firstCid = c.id;
|
||||
return '<button class="toc-ch" data-cid="' + c.id + '">' + c.name + '</button>';
|
||||
}).join('');
|
||||
return '<div class="toc-vol"><div class="vname">' + v.name + '</div>' +
|
||||
(v.intro ? '<div class="vintro">' + v.intro + '</div>' : '') + chs + '</div>';
|
||||
}).join('');
|
||||
tocList.innerHTML = html;
|
||||
|
||||
async function openChapter(cid){
|
||||
tocList.querySelectorAll('.toc-ch').forEach(b => b.classList.toggle('active', String(b.dataset.cid) === String(cid)));
|
||||
reader.innerHTML = '<div class="reader-loading">加载中…</div>';
|
||||
try {
|
||||
const d = await API.storyChapter(cid);
|
||||
if (!d || d.error){ reader.innerHTML = '<div class="reader-loading">' + ((d && d.error) || '章节加载失败') + '</div>'; return; }
|
||||
reader.innerHTML =
|
||||
'<div class="reader-inner"><div class="r-vol">' + (volNameByCh[cid] || '') + '</div>' +
|
||||
'<h1>' + d.name + '</h1><div class="body">' + (d.body_html || '<p>(暂无正文)</p>') + '</div></div>';
|
||||
reader.scrollTop = 0;
|
||||
} catch (e) { reader.innerHTML = '<div class="reader-loading">章节加载失败</div>'; }
|
||||
}
|
||||
|
||||
tocList.querySelectorAll('.toc-ch').forEach(b => b.addEventListener('click', () => openChapter(b.dataset.cid)));
|
||||
if (firstCid !== null) openChapter(firstCid);
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
loadPages();
|
||||
loadRules();
|
||||
loadForum();
|
||||
loadAnnounce();
|
||||
loadStory();
|
||||
});
|
||||
|
||||
265
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;}
|
||||
@ -186,10 +203,97 @@
|
||||
#letter .letter-wrap .lt-body .hl{color:var(--gold);font-weight:500;}
|
||||
#letter .letter-wrap .lt-sign{text-align:right;margin-top:28px;font-size:14px;color:var(--magenta);font-style:italic;}
|
||||
|
||||
/* ===== 论坛(竖排:顶部分类条 + 下方纵向帖子流) ===== */
|
||||
#forum{flex-direction:column;gap:16px;}
|
||||
.f-cats{flex:0 0 auto;flex-direction:row;flex-wrap:wrap;gap:8px;}
|
||||
.f-cats .ch{flex:0 0 auto;}
|
||||
.f-main{flex:1;min-height:0;}
|
||||
.threads{max-width:900px;width:100%;}
|
||||
|
||||
/* ===== 公告(居中竖排) ===== */
|
||||
#announce{align-items:center;}
|
||||
#announce .notes{max-width:880px;width:100%;margin:0 auto;}
|
||||
|
||||
/* ===== 剧情(小说阅读器:左目录 + 右正文,竖排阅读感) ===== */
|
||||
#story{padding:0;}
|
||||
.story-wrap{display:flex;flex-direction:row;width:100%;height:100%;min-height:0;}
|
||||
.story-toc{flex:0 0 286px;display:flex;flex-direction:column;border-right:1px solid var(--card-bd);background:rgba(8,12,26,.35);min-height:0;}
|
||||
.toc-head{font-family:"ZCOOL QingKe HuangYou",sans-serif;font-size:22px;letter-spacing:3px;color:#fff;padding:24px 22px 10px;text-shadow:0 0 16px rgba(139,92,246,.5);}
|
||||
.toc-head small{display:block;font-family:"Press Start 2P",monospace;font-size:8px;color:var(--magenta);letter-spacing:1px;margin-top:6px;}
|
||||
.toc-list{flex:1;overflow-y:auto;padding:0 12px 24px;}
|
||||
.toc-loading{padding:30px 16px;color:var(--text-dim);font-size:14px;}
|
||||
.toc-vol{margin-top:10px;}
|
||||
.toc-vol .vname{font-size:13px;color:var(--gold);letter-spacing:1px;padding:8px 10px 4px;font-weight:500;}
|
||||
.toc-vol .vintro{font-size:12px;color:var(--text-dim);line-height:1.6;padding:0 10px 8px;}
|
||||
.toc-ch{display:block;width:100%;text-align:left;font-family:inherit;font-size:13.5px;color:var(--text-dim);background:transparent;border:none;border-left:2px solid transparent;padding:9px 12px;cursor:pointer;transition:.18s;border-radius:0 8px 8px 0;line-height:1.4;}
|
||||
.toc-ch:hover{color:#fff;background:rgba(139,92,246,.12);}
|
||||
.toc-ch.active{color:#fff;background:rgba(139,92,246,.20);border-left-color:var(--magenta);}
|
||||
.story-reader{flex:1;overflow-y:auto;min-width:0;background:linear-gradient(180deg, rgba(139,92,246,.04), transparent 240px);}
|
||||
.reader-loading{padding:80px 20px;text-align:center;color:var(--text-dim);font-size:15px;}
|
||||
.reader-inner{max-width:760px;margin:0 auto;padding:44px 48px 90px;}
|
||||
.reader-inner .r-vol{font-size:12px;color:var(--cyan);letter-spacing:2px;margin-bottom:10px;}
|
||||
.reader-inner h1{font-family:"ZCOOL QingKe HuangYou",sans-serif;font-size:30px;letter-spacing:2px;color:#fff;text-shadow:0 0 16px rgba(139,92,246,.5);margin-bottom:18px;line-height:1.3;}
|
||||
.reader-inner .body{font-size:15.5px;color:var(--text);line-height:2.15;}
|
||||
.reader-inner .body p{margin-bottom:18px;text-indent:2em;}
|
||||
.reader-inner .body p:first-child{text-indent:0;}
|
||||
.reader-inner .body h2,.reader-inner .body h3{color:#fff;margin:22px 0 10px;font-size:18px;}
|
||||
|
||||
/* ===== 世界数据报表 ===== */
|
||||
#worlddata{overflow-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;}
|
||||
.layers,.f-cats{flex:none;}
|
||||
#story .story-wrap{flex-direction:column;overflow-y:auto;}
|
||||
.story-toc{flex:none;border-right:none;border-bottom:1px solid var(--card-bd);}
|
||||
.toc-list{max-height:220px;}
|
||||
.hero .copy h1{font-size:42px;}
|
||||
.nav button{padding:8px 10px;font-size:13px;}
|
||||
.nav{overflow-x:auto;scrollbar-width:none;}
|
||||
@ -200,13 +304,12 @@
|
||||
<body>
|
||||
<div class="app">
|
||||
<header class="topbar">
|
||||
<div class="logo"><span class="cn">宇森</span></div>
|
||||
<div class="logo" id="logoHome"><span class="cn">宇森</span></div>
|
||||
<nav class="nav">
|
||||
<button data-tab="intro" class="active">介绍</button>
|
||||
<button data-tab="background">背景</button>
|
||||
<button data-tab="rules">规则</button>
|
||||
<button data-tab="codex">图鉴</button>
|
||||
<button data-tab="forum">论坛</button>
|
||||
<button data-tab="forum" class="auth-only">论坛</button>
|
||||
<button data-tab="announce">公告</button>
|
||||
<button data-tab="story">剧情</button>
|
||||
<button data-tab="worlddata" class="auth-only">世界数据</button>
|
||||
@ -339,7 +442,7 @@
|
||||
<div class="row"><button id="pfCancel">取消</button><button class="ok" id="pfOk">发布</button></div>
|
||||
</div>
|
||||
<div class="threads" id="threads">
|
||||
<div class="th" data-fcat="discuss"><span class="pin">PIN</span><span class="tt">【公告】宇森官网第一阶段上线,欢迎园丁们入驻</span><span class="meta"><span class="rp">128</span> 回复 · 官方</span></div>
|
||||
<div class="th" data-fcat="discuss"><span class="pin">置顶</span><span class="tt">【公告】宇森官网第一阶段上线,欢迎园丁们入驻</span><span class="meta"><span class="rp">128</span> 回复 · 官方</span></div>
|
||||
<div class="th" data-fcat="guide"><span class="tt">新手向:园丁视角的三种开局思路</span><span class="meta"><span class="rp">42</span> 回复 · 行者</span></div>
|
||||
<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="fan"><span class="tt">手绘了我的主世界浮空岛,求轻喷</span><span class="meta"><span class="rp">31</span> 回复 · 青柠</span></div>
|
||||
@ -361,16 +464,23 @@
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ===== 剧情 ===== -->
|
||||
<section class="panel" id="story"></section>
|
||||
<!-- ===== 剧情(小说阅读器:左目录 + 右正文) ===== -->
|
||||
<section class="panel" id="story">
|
||||
<div class="story-wrap">
|
||||
<aside class="story-toc">
|
||||
<div class="toc-head">目录<small>CONTENTS</small></div>
|
||||
<div class="toc-list" id="storyTocList"><div class="toc-loading">加载中…</div></div>
|
||||
</aside>
|
||||
<article class="story-reader" id="storyReader">
|
||||
<div class="reader-loading">从左侧目录选择章节开始阅读</div>
|
||||
</article>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ===== 世界数据(登录后可见) ===== -->
|
||||
<section class="panel" id="worlddata">
|
||||
<div class="sec-title">世界数据<small>WORLD DATA</small></div>
|
||||
<div id="worldDataContent" style="text-align:center;padding:60px 20px;color:#556677;font-size:15px;">
|
||||
<div style="font-size:40px;margin-bottom:14px;">🌍</div>
|
||||
加载中…
|
||||
</div>
|
||||
<div id="worldDataContent"></div>
|
||||
</section>
|
||||
</main>
|
||||
</div>
|
||||
@ -386,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<s.length;i++){h^=s.charCodeAt(i);h=Math.imul(h,16777619);}return h>>>0;}
|
||||
function rng(seed){let s=seed>>>0;return function(){s=(Math.imul(s,1664525)+1013904223)>>>0;return s/4294967296;};}
|
||||
@ -620,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 = '<div class="bg-empty">板块内容即将上线…</div>'; return; }
|
||||
box.innerHTML = pages.map(p=>{
|
||||
const cards = (p.items||[]).map(it=>`
|
||||
<div class="bg-card">
|
||||
<div class="bg-card-ic">${it.icon||'✦'}</div>
|
||||
<div class="bg-card-body">
|
||||
<div class="bg-card-title">${it.title||''}</div>
|
||||
<div class="bg-card-desc">${it.desc||''}</div>
|
||||
</div>
|
||||
</div>`).join('');
|
||||
return `
|
||||
<section class="bg-page">
|
||||
<div class="bg-page-head">
|
||||
<h2 class="bg-page-title">${p.title||''}</h2>
|
||||
${p.subtitle?`<div class="bg-page-sub">${p.subtitle}</div>`:''}
|
||||
</div>
|
||||
${p.body_html?`<div class="bg-page-body">${p.body_html}</div>`:''}
|
||||
${cards?`<div class="bg-page-items">${cards}</div>`:''}
|
||||
</section>`;
|
||||
}).join('');
|
||||
}catch(e){
|
||||
box.innerHTML = '<div class="bg-empty">板块加载失败,请刷新重试。</div>';
|
||||
}
|
||||
}
|
||||
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=>`
|
||||
<div class="wd-kpi-card">
|
||||
<span class="wd-kpi-delta ${k.delta>=0?'wd-up':'wd-down'}">${k.delta>=0?'▲':'▼'} ${Math.abs(k.delta)}%</span>
|
||||
<div class="wd-kpi-ic">${k.icon}</div>
|
||||
<div class="wd-kpi-num">${k.num}<span class="u">${k.unit||''}</span></div>
|
||||
<div class="wd-kpi-label">${k.label}</div>
|
||||
</div>`).join('');
|
||||
const bars = data.bars.map(b=>`
|
||||
<div class="wd-bar-row">
|
||||
<div class="wd-bar-top"><span class="k">${b.k}</span><span class="v">${b.v}%</span></div>
|
||||
<div class="wd-track"><div class="wd-fill" style="width:${b.v}%"></div></div>
|
||||
</div>`).join('');
|
||||
const rows = data.layers.map(l=>`
|
||||
<tr>
|
||||
<td class="lead">${l.lv}</td>
|
||||
<td>${l.band}</td>
|
||||
<td>${l.eco}</td>
|
||||
<td>${l.pop}</td>
|
||||
<td><span class="pill">${l.terr}</span></td>
|
||||
</tr>`).join('');
|
||||
const ranks = data.factions.map((f,i)=>`
|
||||
<div class="wd-rank-row">
|
||||
<div class="wd-rank-no">${i+1}</div>
|
||||
<div class="wd-rank-main">
|
||||
<div class="wd-rank-name"><span>${f.name}</span><span class="s">${f.score} 影响力</span></div>
|
||||
<div class="wd-rank-track"><div class="wd-rank-fill" style="width:${f.pct}%"></div></div>
|
||||
</div>
|
||||
</div>`).join('');
|
||||
box.innerHTML = `
|
||||
<span class="wd-tag">演示数据 · 后端接口待接入</span>
|
||||
<div class="wd-kpi">${kpis}</div>
|
||||
|
||||
<div class="wd-h">核心指标进度 <small>CORE METRICS</small></div>
|
||||
<div class="wd-bars">${bars}</div>
|
||||
|
||||
<div class="wd-h">世界五层结构 <small>WORLD LAYERS</small></div>
|
||||
<table class="wd-table">
|
||||
<thead><tr><th>层级</th><th>高度带</th><th>地表生态</th><th>人口占比</th><th>代表地形</th></tr></thead>
|
||||
<tbody>${rows}</tbody>
|
||||
</table>
|
||||
|
||||
<div class="wd-h">势力影响力排行 <small>FACTION RANKING</small></div>
|
||||
<div class="wd-rank">${ranks}</div>
|
||||
`;
|
||||
}
|
||||
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 ()=>{
|
||||
|
||||
144
website/announce_preview.html
Normal file
@ -0,0 +1,144 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>公告 · 新版预览</title>
|
||||
<!-- 直接复用真实 auth.css,新公告样式就在里面 -->
|
||||
<link rel="stylesheet" href="auth.css">
|
||||
<style>
|
||||
/* 复用 intro.html 的页面壳样式(只这一页用,不污染生产代码) */
|
||||
* { margin:0; padding:0; box-sizing:border-box; }
|
||||
html { scroll-behavior:smooth; }
|
||||
body {
|
||||
font-family:'Microsoft YaHei','PingFang SC',sans-serif;
|
||||
background:#06090f;
|
||||
color:#c8d6e5;
|
||||
line-height:1.7;
|
||||
min-height:100vh;
|
||||
}
|
||||
body::before {
|
||||
content:"";
|
||||
position:fixed; inset:0; z-index:0; pointer-events:none;
|
||||
background:
|
||||
radial-gradient(800px circle at 20% 10%, rgba(74,175,255,.08), transparent 50%),
|
||||
radial-gradient(600px circle at 80% 30%, rgba(167,139,250,.06), transparent 50%);
|
||||
}
|
||||
.page {
|
||||
position:relative; z-index:1;
|
||||
padding:48px 36px 60px;
|
||||
max-width:980px; margin:0 auto;
|
||||
}
|
||||
h1 {
|
||||
font-size:28px; font-weight:bold; margin-bottom:6px;
|
||||
background:linear-gradient(135deg,#4af,#a8e6cf);
|
||||
-webkit-background-clip:text; -webkit-text-fill-color:transparent;
|
||||
}
|
||||
.page-sub { color:#556677; font-size:14px; margin-bottom:32px; }
|
||||
.preview-bar {
|
||||
position:fixed; bottom:14px; left:14px; z-index:3000;
|
||||
background:rgba(15,23,42,.85); border:1px solid rgba(167,139,250,.3);
|
||||
color:#a78bfa; font-size:12px; padding:8px 14px; border-radius:8px;
|
||||
backdrop-filter:blur(8px); letter-spacing:1px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div class="page">
|
||||
<!-- ===== 公告面板 ===== -->
|
||||
<div class="tab-panel active" id="panel-announce">
|
||||
<h1>公 告</h1>
|
||||
<p class="page-sub">官方通知 · ANNOUNCEMENTS</p>
|
||||
<div id="announce" class="announce-list" aria-live="polite"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="preview-bar">⚡ 新版公告预览 · 点击卡片可弹窗</div>
|
||||
|
||||
<!-- Mock API.announcements 返回的数据,与截图 5 条对应 -->
|
||||
<script>
|
||||
window.API = {
|
||||
announcements: async () => ([
|
||||
{
|
||||
id: 1,
|
||||
name: '宇森官网第一阶段上线:介绍 / 特色 / 背景 / 规则 / 图鉴 / 论坛 / 公告 / 致玩家 八块,单页切换呈现。介绍页即概念宣传片。',
|
||||
date: '2026-07-09',
|
||||
priority: 'normal',
|
||||
priority_label: '普通',
|
||||
body_html: '<p>官网第一阶段上线,全站共 8 个板块,全部通过顶部 Tab 单页切换呈现。</p>' +
|
||||
'<p>介绍页即概念宣传片,从世界五层结构、玩家园丁视角、活的文明系统三方面切入,让你 30 秒看懂宇森在做什么。</p>' +
|
||||
'<p>其他板块将在后续逐步充实内容,欢迎常回来看看。</p>'
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: '图鉴系统升级:种族 204 条、系别 / 性格 / 天赋 / 神器 / 传奇事件全面扩量,装备槽位对齐 头 / 手 / 胸 / 裤 / 鞋 / 戒指(双) / 项链 / 宝物;全部缩略图改为程序化矢量插画,零 AI 生图。',
|
||||
date: '2026-07-09',
|
||||
priority: 'important',
|
||||
priority_label: '重要',
|
||||
body_html: '<p>本次图鉴系统全面升级,主要变化:</p>' +
|
||||
'<ul>' +
|
||||
'<li><strong>种族条目扩至 204 条</strong>,覆盖主流奇幻 + 自创世界观。</li>' +
|
||||
'<li>系别 / 性格 / 天赋 / 神器 / 传奇事件 五类同步扩量。</li>' +
|
||||
'<li>装备槽位与游戏内对齐:头 / 手 / 胸 / 裤 / 鞋 / 戒指(双) / 项链 / 宝物。</li>' +
|
||||
'<li>所有缩略图改为<strong>程序化矢量插画</strong>,彻底告别 AI 生图,风格统一、可缩放、无水印。</li>' +
|
||||
'</ul>' +
|
||||
'<p>欢迎在「图鉴」页查看新版视觉。</p>'
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
name: '概念宣传片《垂直生长》完成,呈现五层世界自上而下生长的镜头语言(天空层改为云朵地基块,星空层改为陨石科技带)。',
|
||||
date: '2026-07-08',
|
||||
priority: 'normal',
|
||||
priority_label: '普通',
|
||||
body_html: '<p>《垂直生长》是一支 90 秒的概念宣传片,镜头从星空层一直钻到地心熔岩,逐层展现五层世界如何"自上而下"生长。</p>' +
|
||||
'<p>制作中我们对两层做了重要调整:</p>' +
|
||||
'<ul>' +
|
||||
'<li><strong>天空层</strong>:从原来的气流层改为云朵地基块,视觉上更"可踩"。</li>' +
|
||||
'<li><strong>星空层</strong>:从星座点缀改为陨石科技带,强调"远古文明残骸"的世界感。</li>' +
|
||||
'</ul>' +
|
||||
'<p>后续会放出完整版本到官网与 B 站。</p>'
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
name: '园丁视角核心玩法原型启动,首版六边形地图与活 NPC 验证中。',
|
||||
date: '2026-07-01',
|
||||
priority: 'normal',
|
||||
priority_label: '普通',
|
||||
body_html: '<p>核心玩法原型正式立项,目标验证两件事:</p>' +
|
||||
'<ol>' +
|
||||
'<li><strong>六边形地图</strong>:从正方形切到正六边形密铺,缩放 / 平移 / 邻居查询是否顺畅。</li>' +
|
||||
'<li><strong>活 NPC</strong>:单个 NPC 是否有"性格 + 记忆 + 自主决策"三个属性,能否在简单场景里产生涌现行为。</li>' +
|
||||
'</ol>' +
|
||||
'<p>首批反馈预计 7 月中下旬出,会同步到公告和论坛。</p>'
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
name: '图鉴系统规划确定:种族 / 建筑 / 生物 / 神器 四大类目,美术持续补充中。',
|
||||
date: '2026-06-20',
|
||||
priority: 'normal',
|
||||
priority_label: '普通',
|
||||
body_html: '<p>图鉴系统四大类目确定:</p>' +
|
||||
'<ul>' +
|
||||
'<li><strong>种族</strong>:所有可玩 / NPC / 敌对种族</li>' +
|
||||
'<li><strong>建筑</strong>:所有可建造 / 不可建造但可见的结构</li>' +
|
||||
'<li><strong>生物</strong>:野外生态群与城镇人口</li>' +
|
||||
'<li><strong>神器</strong>:剧情道具 + 玩家可制造的高阶装备</li>' +
|
||||
'</ul>' +
|
||||
'<p>美术采用程序化矢量插画路线,目标是"无 AI 生图也能保持高质量"。</p>'
|
||||
}
|
||||
])
|
||||
};
|
||||
</script>
|
||||
|
||||
<!-- 真实 site-app.js,会调用 loadAnnounce() -->
|
||||
<script src="assets/site-app.js"></script>
|
||||
|
||||
<!-- 预览页只关心公告,所以手动触发一次(生产页由 DOMContentLoaded 自动触发) -->
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
if (typeof loadAnnounce === 'function') loadAnnounce();
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@ -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, '"').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 = '<div class="loading">加载中…</div>';
|
||||
let list = [];
|
||||
try {
|
||||
const list = await API.announcements();
|
||||
if (!list || !list.length){ el.innerHTML = '<div class="note"><div class="body">暂无公告</div></div>'; return; }
|
||||
el.innerHTML = list.map(n =>
|
||||
`<div class="note"><div class="date">${n.date}</div><div class="body">${n.body_html || ''}</div></div>`
|
||||
).join('');
|
||||
} catch (e) { /* 保留静态兜底内容 */ }
|
||||
list = await API.announcements();
|
||||
} catch (e) { /* 落到空态 */ }
|
||||
if (!list || !list.length) {
|
||||
root.innerHTML = '<div class="announce-empty">暂无公告</div>';
|
||||
return;
|
||||
}
|
||||
root.innerHTML = list.map((n, i) => {
|
||||
const pri = n.priority || 'normal';
|
||||
const preview = annStripHtml(n.body_html).slice(0, 160);
|
||||
return `
|
||||
<div class="announce-card priority-${annEscapeHtml(pri)}"
|
||||
data-idx="${i}" role="button" tabindex="0"
|
||||
aria-label="阅读公告: ${annEscapeHtml(n.name)}">
|
||||
<div class="meta-row">
|
||||
<span class="date">${annEscapeHtml(annFormatDate(n.date))}</span>
|
||||
<span class="dot"></span>
|
||||
<span class="badge ${annEscapeHtml(pri)}">${annEscapeHtml(n.priority_label || pri)}</span>
|
||||
</div>
|
||||
<div class="title">${annEscapeHtml(n.name)}</div>
|
||||
<div class="preview">${annEscapeHtml(preview)}</div>
|
||||
<div class="more">阅读全文 <span class="arrow">→</span></div>
|
||||
</div>`;
|
||||
}).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 = `
|
||||
<div class="backdrop" data-close="1" aria-label="关闭"></div>
|
||||
<div class="dialog" role="dialog" aria-modal="true" aria-labelledby="ann-modal-title">
|
||||
<div class="head">
|
||||
<div>
|
||||
<div class="title" id="ann-modal-title" data-title></div>
|
||||
<div class="meta" data-meta></div>
|
||||
</div>
|
||||
<button class="close" type="button" data-close="1" aria-label="关闭">×</button>
|
||||
</div>
|
||||
<div class="body" data-body></div>
|
||||
</div>`;
|
||||
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 || '<p style="color:#556677">(无正文)</p>';
|
||||
_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', () => {
|
||||
|
||||
279
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; }
|
||||
}
|
||||
|
||||
@ -259,8 +259,8 @@ p { color:#99aabb; font-size:15px; margin-bottom:14px; line-height:1.9; }
|
||||
<!-- ========== 公告 ========== -->
|
||||
<div class="tab-panel" id="panel-announce">
|
||||
<h1>公 告</h1>
|
||||
<p class="page-sub">官方通知与更新日志</p>
|
||||
<div id="announce"><div class="loading">加载中...</div></div>
|
||||
<p class="page-sub">官方通知 · ANNOUNCEMENTS</p>
|
||||
<div id="announce" class="announce-list" aria-live="polite"></div>
|
||||
</div>
|
||||
|
||||
<!-- ========== 剧情(新增) ========== -->
|
||||
|
||||
540
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 */
|
||||
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="authSplash">登录校验中…</div>
|
||||
<div id="authSplash"><div class="auth-spinner"><div id="authSpinner"></div><span>登录校验中…</span></div></div>
|
||||
|
||||
<!-- 加载界面 -->
|
||||
<div id="loadingScreen">
|
||||
@ -422,7 +444,6 @@ body { background: #0a0e17; overflow: hidden; font-family: 'Microsoft YaHei', sa
|
||||
|
||||
<div id="topBar">
|
||||
<span class="title"><img src="assets/ui/logo_planet_48.png" alt=""><span class="brand">宇森</span><span class="worldName" id="worldName">大地</span></span>
|
||||
<button id="buildToggle">🔨 建造</button>
|
||||
<div class="topRight">
|
||||
<span class="coords" id="coords">坐标: (0, 0)</span>
|
||||
<div class="userArea" id="userArea" hidden>
|
||||
@ -443,14 +464,11 @@ body { background: #0a0e17; overflow: hidden; font-family: 'Microsoft YaHei', sa
|
||||
|
||||
<!-- 主沙盘底部功能框架(预留区,放功能按钮 / 素材槽) -->
|
||||
<div id="bottomBar">
|
||||
<span class="bb-label">功能栏</span>
|
||||
<div class="bb-slots">
|
||||
<div class="bb-slot bb-slot--empty"></div>
|
||||
<div class="bb-slot bb-slot--empty"></div>
|
||||
<div class="bb-slot bb-slot--empty"></div>
|
||||
<div class="bb-slot bb-slot--empty"></div>
|
||||
<div class="bb-slot bb-slot--empty"></div>
|
||||
<div class="bb-slot bb-slot--empty"></div>
|
||||
<button class="bb-btn" id="btnData" type="button">📊 数据</button>
|
||||
<button class="bb-btn" id="btnInventory" type="button">🎒 包裹</button>
|
||||
<button class="bb-btn" id="btnBuild" type="button">🔨 建造</button>
|
||||
<button class="bb-btn" id="btnTech" type="button">🔬 科技</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -510,7 +528,7 @@ body { background: #0a0e17; overflow: hidden; font-family: 'Microsoft YaHei', sa
|
||||
<img id="sceneImage" style="display:none;" alt="">
|
||||
|
||||
|
||||
<div id="dataPanel">
|
||||
<div id="dataPanel" style="display:none;">
|
||||
<div id="dpHeader"><span id="dpTitle">面板</span><button id="dpClose">关闭</button></div>
|
||||
<div id="dpBody"></div>
|
||||
</div>
|
||||
@ -518,6 +536,7 @@ body { background: #0a0e17; overflow: hidden; font-family: 'Microsoft YaHei', sa
|
||||
|
||||
<!-- 飞入过渡 -->
|
||||
<div id="flyInOverlay">
|
||||
<img class="flyBg" id="flyBg" alt="">
|
||||
<div class="flyTip" id="flyTip">💡 拖拽空白处可平移视角,滚轮缩放沙盘</div>
|
||||
<div class="flyLine">
|
||||
<div class="flyBar"><div class="flyFill" id="flyFill"></div></div>
|
||||
@ -576,11 +595,71 @@ body { background: #0a0e17; overflow: hidden; font-family: 'Microsoft YaHei', sa
|
||||
|
||||
<!-- 背景切换面板 -->
|
||||
<style>
|
||||
#buildToggle{margin-left:14px;background:rgba(100,200,255,.12);border:1px solid rgba(100,200,255,.3);color:#9fd;padding:7px 14px;border-radius:8px;font-size:14px;cursor:pointer;pointer-events:auto;transition:.2s;}
|
||||
#buildToggle:hover{background:rgba(100,200,255,.22);}
|
||||
#buildToggle.active{background:linear-gradient(135deg,#8b5cf6,#4aafff);color:#fff;border-color:transparent;box-shadow:0 0 14px rgba(100,170,255,.5);}
|
||||
#buildPanel{position:fixed;left:16px;top:64px;z-index:60;width:440px;max-height:82vh;overflow-y:auto;background:linear-gradient(180deg,rgba(12,16,28,.98),rgba(16,24,44,.98));border:1px solid rgba(120,200,255,.28);border-radius:16px;box-shadow:0 12px 40px rgba(0,0,0,.55);color:#e8eef6;font-size:15px;padding:20px;backdrop-filter:blur(8px);}
|
||||
#buildPanel.hidden{display:none;}
|
||||
#btnBuild{background:rgba(100,200,255,.12);border:1px solid rgba(100,200,255,.3);color:#9fd;padding:9px 14px;border-radius:10px;font-size:13px;font-weight:700;cursor:pointer;white-space:nowrap;pointer-events:auto;transition:.2s;box-shadow:0 4px 16px rgba(80,120,255,.35);}
|
||||
#btnBuild:hover{filter:brightness(1.12);}
|
||||
#btnBuild.active{background:linear-gradient(135deg,#8b5cf6,#4aafff);color:#fff;border-color:transparent;box-shadow:0 0 14px rgba(100,170,255,.5);}
|
||||
#btnTech{background:rgba(120,255,200,.12);border:1px solid rgba(120,255,200,.3);color:#9ff;padding:9px 14px;border-radius:10px;font-size:13px;font-weight:700;cursor:pointer;white-space:nowrap;pointer-events:auto;transition:.2s;box-shadow:0 4px 16px rgba(80,255,180,.3);}
|
||||
#btnTech:hover{filter:brightness(1.12);}
|
||||
#btnTech.active{background:linear-gradient(135deg,#10b981,#4aafff);color:#fff;border-color:transparent;box-shadow:0 0 14px rgba(100,220,180,.5);}
|
||||
.bb-btn.active{outline:2px solid rgba(255,255,255,.75);box-shadow:0 0 18px rgba(140,180,255,.6);}
|
||||
|
||||
/* ===== 统一左侧面板(数据 / 包裹 / 建造 三合一) ===== */
|
||||
#sidePanel{
|
||||
position:fixed;top:58px;left:0;bottom:72px;width:380px;z-index:150;
|
||||
background:linear-gradient(180deg,rgba(10,14,23,.97),rgba(14,22,40,.97));
|
||||
border-right:1px solid rgba(100,200,255,.22);
|
||||
display:flex;flex-direction:column;
|
||||
transform:translateX(-100%);transition:transform .28s ease;
|
||||
box-shadow:8px 0 40px rgba(0,0,0,.45);
|
||||
}
|
||||
#sidePanel.open{transform:translateX(0);}
|
||||
#spTabs{display:flex;gap:0;flex-shrink:0;border-bottom:1px solid rgba(100,200,255,.15);
|
||||
background:linear-gradient(180deg,rgba(12,18,32,.98),rgba(8,12,22,.95));}
|
||||
.sp-tab{flex:1;padding:12px 6px;background:none;border:none;color:#8899aa;font-size:14px;font-weight:600;
|
||||
cursor:pointer;transition:.18s;border-bottom:2px solid transparent;text-align:center;}
|
||||
.sp-tab:hover{color:#cfe6ff;background:rgba(100,200,255,.06);}
|
||||
.sp-tab.active{color:#fff;border-bottom-color:#8b5cf6;background:rgba(139,92,246,.10);}
|
||||
.sp-tab .sp-ico{display:block;font-size:18px;margin-bottom:3px;}
|
||||
.sp-body{flex:1;overflow-y:auto;overflow-x:hidden;padding:14px 16px;display:none;min-height:0;}
|
||||
.sp-body.active{display:block;}
|
||||
.sp-body::-webkit-scrollbar{width:4px;}
|
||||
.sp-body::-webkit-scrollbar-thumb{background:rgba(100,200,255,.28);border-radius:2px;}
|
||||
|
||||
/* 沙盘左移(给侧面板让路) */
|
||||
#gameCanvas{transition:transform .28s ease,left .28s ease,width .28s ease;}
|
||||
body.side-open #gameCanvas{transform:translateX(380px);}
|
||||
|
||||
/* ----- 包裹:7列 × 行(物资分类 + 小格子 + 滚动条) ----- */
|
||||
.inv-cat{font-size:11px;color:#8b5cf6;letter-spacing:1.5px;text-transform:uppercase;margin:14px 0 6px;
|
||||
padding-bottom:4px;border-bottom:1px solid rgba(139,92,246,.2);}
|
||||
.inv-cat:first-child{margin-top:0;}
|
||||
.inv-grid-7{display:grid;grid-template-columns:repeat(7,1fr);gap:3px;}
|
||||
.inv-slot{
|
||||
aspect-ratio:1;border-radius:5px;display:flex;align-items:center;justify-content:center;
|
||||
font-size:15px;color:#56708c;background:rgba(255,255,255,.04);
|
||||
border:1px dashed rgba(120,200,255,.18);cursor:pointer;transition:.15s;position:relative;
|
||||
}
|
||||
.inv-slot:hover{border-color:rgba(120,200,255,.5);background:rgba(120,200,255,.08);color:#cfe6ff;}
|
||||
.inv-slot.has-item{border-style:solid;border-color:rgba(120,200,255,.25);background:rgba(120,200,255,.06);}
|
||||
.inv-slot .inv-ct{position:absolute;bottom:1px;right:2px;font-size:8px;color:#8aa0b8;line-height:1;
|
||||
background:rgba(0,0,0,.45);padding:0 3px;border-radius:3px;}
|
||||
|
||||
/* ----- 建造(嵌入侧面板,保留4子Tab) ----- */
|
||||
#buildPanel{position:static;width:auto;max-height:none;overflow:visible;
|
||||
background:transparent;border:none;border-radius:0;box-shadow:none;padding:0;
|
||||
display:none;height:100%;flex-direction:column;}
|
||||
#buildPanel.active{display:flex;}
|
||||
.bp-head{margin-bottom:10px;padding-bottom:8px;border-bottom:1px solid rgba(100,200,255,.12);}
|
||||
.bp-tools{gap:4px;margin-bottom:10px;}
|
||||
.bp-tool{padding:7px 2px;font-size:12px;}
|
||||
.bp-brush{gap:4px;min-height:32px;}
|
||||
.bp-chip{padding:6px 9px;font-size:12px;}
|
||||
.bp-hint{font-size:13px;margin-bottom:10px;}
|
||||
.bp-hctl{margin-bottom:10px;}
|
||||
.bp-hctl-row{font-size:13px;}
|
||||
.bp-flatten,.bp-manage{padding:9px;font-size:13px;}
|
||||
.bp-mgmt{gap:10px;margin-bottom:10px;}
|
||||
.bp-variants{max-height:160px;grid-template-columns:repeat(4,1fr);}
|
||||
.bp-head{display:flex;justify-content:space-between;align-items:center;font-weight:700;font-size:17px;margin-bottom:14px;}
|
||||
#bpClose{background:none;border:none;color:#9fb0c4;font-size:22px;cursor:pointer;}
|
||||
#bpClose:hover{color:#fff;}
|
||||
@ -616,38 +695,117 @@ body { background: #0a0e17; overflow: hidden; font-family: 'Microsoft YaHei', sa
|
||||
.bp-mgmt-presets button:hover{background:rgba(139,92,246,.3);}
|
||||
#bpMgmtApply{background:linear-gradient(135deg,#8b5cf6,#4aafff);border:none;color:#fff;border-radius:11px;padding:13px;font-weight:700;cursor:pointer;font-size:15px;transition:.2s;}
|
||||
#bpMgmtApply:hover{filter:brightness(1.08);}
|
||||
|
||||
/* 包裹面板(复用 buildPanel 弹窗风格,从左侧滑出) */
|
||||
#inventoryPanel{position:fixed;left:16px;top:64px;z-index:60;width:440px;max-height:82vh;overflow-y:auto;background:linear-gradient(180deg,rgba(12,16,28,.98),rgba(16,24,44,.98));border:1px solid rgba(120,200,255,.28);border-radius:16px;box-shadow:0 12px 40px rgba(0,0,0,.55);color:#e8eef6;font-size:15px;padding:20px;backdrop-filter:blur(8px);}
|
||||
#inventoryPanel.hidden{display:none;}
|
||||
.inv-hint{font-size:12px;color:#8aa0b8;margin-bottom:14px;line-height:1.5;}
|
||||
.demo-tag{display:inline-block;background:rgba(255,180,80,.18);border:1px solid rgba(255,180,80,.4);color:#ffcf86;font-size:11px;padding:1px 7px;border-radius:6px;margin-left:4px;}
|
||||
.inv-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:10px;}
|
||||
.inv-item{background:rgba(255,255,255,.04);border:1px solid rgba(120,200,255,.18);border-radius:10px;padding:10px;display:flex;flex-direction:column;align-items:center;gap:6px;transition:.15s;}
|
||||
.inv-item:hover{border-color:rgba(120,200,255,.5);background:rgba(120,200,255,.08);}
|
||||
.inv-item .ico{font-size:26px;}
|
||||
.inv-item .nm{font-size:12px;color:#dce6f2;text-align:center;}
|
||||
.inv-item .ct{font-size:11px;color:#8aa0b8;}
|
||||
.inv-item .ct.limited{color:#ffcf86;}
|
||||
|
||||
/* 数据面板子项 */
|
||||
#dpBody .dp-section{margin-bottom:18px;}
|
||||
#dpBody .dp-h{font-size:12px;color:#7f93a8;text-transform:uppercase;letter-spacing:1px;margin-bottom:8px;border-bottom:1px solid rgba(120,200,255,.15);padding-bottom:5px;}
|
||||
#dpBody .dp-row{display:flex;justify-content:space-between;font-size:14px;color:#dbe6f2;padding:5px 0;border-bottom:1px dashed rgba(255,255,255,.06);}
|
||||
#dpBody .dp-note{font-size:12px;color:#8aa0b8;margin-top:10px;line-height:1.5;}
|
||||
#dpBody code{background:rgba(255,255,255,.08);padding:1px 5px;border-radius:4px;font-size:11px;}
|
||||
|
||||
/* ===== 科技树(演示 + 预留 /yt_world/api/tech) ===== */
|
||||
.tech-pool{display:flex;align-items:center;justify-content:space-between;gap:8px;background:rgba(16,185,129,.1);
|
||||
border:1px solid rgba(16,185,129,.3);border-radius:10px;padding:9px 12px;margin-bottom:14px;}
|
||||
.tech-pool .tp-label{font-size:12px;color:#9ff;letter-spacing:1px;}
|
||||
.tech-pool .tp-val{font-size:20px;font-weight:800;color:#5eead4;}
|
||||
.tech-pool .tp-reset{background:none;border:1px solid rgba(255,255,255,.18);color:#9fb0c4;border-radius:7px;
|
||||
padding:4px 9px;font-size:11px;cursor:pointer;transition:.15s;}
|
||||
.tech-pool .tp-reset:hover{color:#fff;border-color:rgba(255,120,120,.5);}
|
||||
.tech-tier{margin-bottom:16px;}
|
||||
.tech-tier-h{font-size:11px;color:#10b981;letter-spacing:1.5px;text-transform:uppercase;margin:0 0 8px;
|
||||
padding-bottom:4px;border-bottom:1px solid rgba(16,185,129,.22);}
|
||||
.tech-nodes{display:flex;flex-wrap:wrap;gap:8px;}
|
||||
.tech-node{position:relative;width:calc(50% - 4px);box-sizing:border-box;background:rgba(255,255,255,.04);
|
||||
border:1px solid rgba(120,255,200,.18);border-radius:11px;padding:10px 11px;cursor:default;transition:.18s;}
|
||||
.tech-node:hover{border-color:rgba(120,255,200,.45);background:rgba(120,255,200,.07);}
|
||||
.tech-node .tn-top{display:flex;align-items:center;gap:7px;margin-bottom:5px;}
|
||||
.tech-node .tn-ico{font-size:20px;}
|
||||
.tech-node .tn-name{font-size:13px;font-weight:700;color:#e8f4ef;}
|
||||
.tech-node .tn-desc{font-size:11px;color:#9fb0c4;line-height:1.45;margin-bottom:6px;min-height:30px;}
|
||||
.tech-node .tn-pre{font-size:10px;color:#7f93a8;margin-bottom:7px;}
|
||||
.tech-node .tn-pre b{color:#ffb454;font-weight:600;}
|
||||
.tech-node .tn-pre .met{color:#5eead4;}
|
||||
.tech-node .tn-cost{font-size:11px;color:#9ff;font-weight:700;}
|
||||
.tech-node .tn-btn{width:100%;margin-top:7px;border:none;border-radius:8px;padding:7px;font-size:12px;font-weight:700;
|
||||
cursor:pointer;transition:.15s;background:rgba(16,185,129,.18);color:#5eead4;border:1px solid rgba(16,185,129,.4);}
|
||||
.tech-node .tn-btn:hover{filter:brightness(1.15);}
|
||||
.tech-node.locked{opacity:.5;}
|
||||
.tech-node.locked .tn-btn{background:rgba(255,255,255,.05);color:#7f93a8;border-color:rgba(255,255,255,.12);cursor:not-allowed;}
|
||||
.tech-node.done{background:rgba(16,185,129,.14);border-color:rgba(16,185,129,.55);}
|
||||
.tech-node.done .tn-btn{background:rgba(16,185,129,.28);color:#d6fff2;cursor:default;}
|
||||
.tech-node.afford .tn-btn{background:linear-gradient(135deg,#10b981,#4aafff);color:#fff;border-color:transparent;
|
||||
box-shadow:0 0 10px rgba(16,185,129,.35);}
|
||||
.tech-note{font-size:11px;color:#8aa0b8;margin-top:8px;line-height:1.5;}
|
||||
</style>
|
||||
<div id="buildPanel" class="hidden">
|
||||
<div class="bp-head"><span>🔨 建造</span><button id="bpClose" aria-label="关闭">×</button></div>
|
||||
<div class="bp-tools">
|
||||
<button class="bp-tool active" data-tool="terrain">地形</button>
|
||||
<button class="bp-tool" data-tool="move">移动</button>
|
||||
<button class="bp-tool" data-tool="height">高度</button>
|
||||
<button class="bp-tool" data-tool="manage">管理</button>
|
||||
<!-- ===== 统一左侧面板(数据 / 包裹 / 建造) ===== -->
|
||||
<div id="sidePanel">
|
||||
<div id="spTabs">
|
||||
<button class="sp-tab" data-sp="data"><span class="sp-ico">📊</span>数据</button>
|
||||
<button class="sp-tab" data-sp="inventory"><span class="sp-ico">🎒</span>包裹</button>
|
||||
<button class="sp-tab" data-sp="build"><span class="sp-ico">🔨</span>建造</button>
|
||||
<button class="sp-tab" data-sp="tech"><span class="sp-ico">🔬</span>科技</button>
|
||||
</div>
|
||||
<div class="bp-brush" id="bpBrush"></div>
|
||||
<div class="bp-hctl" id="bpHeightControl" style="display:none;">
|
||||
<div class="bp-hctl-row">
|
||||
<span>高度</span>
|
||||
<input type="range" id="bpHeightSlider" min="1" max="12" step="1" value="3">
|
||||
<span id="bpHeightVal">3 · 森林</span>
|
||||
</div>
|
||||
<button id="bpFlatten" class="bp-flatten">⤓ 平铺到最低高度</button>
|
||||
<!-- 数据 Tab -->
|
||||
<div class="sp-body" id="spDataBody"></div>
|
||||
<!-- 包裹 Tab(7×50 格 + 物资分类) -->
|
||||
<div class="sp-body" id="spInvBody">
|
||||
<div class="inv-cat">笔刷 · Brush</div>
|
||||
<div class="inv-grid-7" id="invGridBrush"></div>
|
||||
<div class="inv-cat">模板 · Template</div>
|
||||
<div class="inv-grid-7" id="invGridTemplate"></div>
|
||||
<div class="inv-cat">事件卡 · Event</div>
|
||||
<div class="inv-grid-7" id="invGridEvent"></div>
|
||||
</div>
|
||||
<div class="bp-mgmt" id="bpMgmt" style="display:none;">
|
||||
<div class="bp-mgmt-body" id="bpMgmtBody"></div>
|
||||
<div class="bp-mgmt-foot">
|
||||
<div class="bp-mgmt-presets">
|
||||
<span>预设</span>
|
||||
<button data-preset="default">默认</button>
|
||||
<button data-preset="island">小岛</button>
|
||||
<button data-preset="peaks">雪峰</button>
|
||||
<button data-preset="water">水乡</button>
|
||||
<!-- 建造 Tab(4 子Tab:移动/地形/高度/管理) -->
|
||||
<div class="sp-body" id="spBuildBody">
|
||||
<div id="buildPanel">
|
||||
<div class="bp-head"><span>🔨 建造工具</span></div>
|
||||
<div class="bp-tools">
|
||||
<button class="bp-tool active" data-tool="terrain">地形</button>
|
||||
<button class="bp-tool" data-tool="move">移动</button>
|
||||
<button class="bp-tool" data-tool="height">高度</button>
|
||||
<button class="bp-tool" data-tool="manage">管理</button>
|
||||
</div>
|
||||
<button id="bpMgmtApply">应用</button>
|
||||
<div class="bp-brush" id="bpBrush"></div>
|
||||
<div class="bp-hctl" id="bpHeightControl" style="display:none;">
|
||||
<div class="bp-hctl-row">
|
||||
<span>高度</span>
|
||||
<input type="range" id="bpHeightSlider" min="1" max="12" step="1" value="3">
|
||||
<span id="bpHeightVal">3 · 森林</span>
|
||||
</div>
|
||||
<button id="bpFlatten" class="bp-flatten">⤓ 平铺到最低高度</button>
|
||||
</div>
|
||||
<div class="bp-mgmt" id="bpMgmt" style="display:none;">
|
||||
<div class="bp-mgmt-body" id="bpMgmtBody"></div>
|
||||
<div class="bp-mgmt-foot">
|
||||
<div class="bp-mgmt-presets">
|
||||
<span>预设</span>
|
||||
<button data-preset="default">默认</button>
|
||||
<button data-preset="island">小岛</button>
|
||||
<button data-preset="peaks">雪峰</button>
|
||||
<button data-preset="water">水乡</button>
|
||||
</div>
|
||||
<button id="bpMgmtApply">应用</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="bp-hint" id="bpHint"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="bp-hint" id="bpHint"></div>
|
||||
<!-- 科技树 Tab -->
|
||||
<div class="sp-body" id="spTechBody"></div>
|
||||
</div>
|
||||
|
||||
<!-- 单位详情弹窗 -->
|
||||
@ -1794,11 +1952,34 @@ function drawTimeAmbiance(pal) {
|
||||
}
|
||||
}
|
||||
|
||||
// ============ 随机过场插画(飞入过渡背景用) ============
|
||||
const TRANSITION_IMAGES = [
|
||||
'assets/art/transitions/transition_dungeon.png',
|
||||
'assets/art/transitions/transition_adventure.png',
|
||||
'assets/art/transitions/transition_sky.png',
|
||||
'assets/art/transitions/transition_forest.png',
|
||||
'assets/art/transitions/transition_team.png',
|
||||
'assets/art/transitions/transition_night.png',
|
||||
'assets/art/transitions/transition_dawn.png',
|
||||
'assets/art/transitions/transition_coast.png',
|
||||
'assets/art/transitions/transition_skycity.png',
|
||||
];
|
||||
let _lastTransitionIdx = -1; // 避免连续两次同一张
|
||||
function pickTransitionImage() {
|
||||
let idx;
|
||||
do { idx = Math.floor(Math.random() * TRANSITION_IMAGES.length); } while (idx === _lastTransitionIdx && TRANSITION_IMAGES.length > 1);
|
||||
_lastTransitionIdx = idx;
|
||||
return TRANSITION_IMAGES[idx];
|
||||
}
|
||||
|
||||
// ============ 进入微观世界 ============
|
||||
function enterMicroWorld(tile) {
|
||||
if (!tile) return;
|
||||
const overlay = document.getElementById('flyInOverlay');
|
||||
const microView = document.getElementById('microView');
|
||||
// 0) 随机图作飞入遮罩背景(铺满 + 放大遮水印),进度条/提示仍在前景
|
||||
const flyBg = document.getElementById('flyBg');
|
||||
if (flyBg) flyBg.src = pickTransitionImage();
|
||||
// 1) 先盖屏,立刻在遮罩下把微观世界建好并跑起来:重活消化在幕后,
|
||||
// 揭幕那一帧不再有主线程尖峰 → 消除"加载完却一顿"的卡顿
|
||||
overlay.classList.add('active');
|
||||
@ -1806,8 +1987,8 @@ function enterMicroWorld(tile) {
|
||||
const flyPct = document.getElementById('flyPct');
|
||||
if (flyFill) flyFill.style.width = '0%';
|
||||
if (flyPct) flyPct.textContent = '0%';
|
||||
// 进度条动画:与 1 秒保持同步,给玩家明确的加载反馈
|
||||
const MICRO_ENTER_HOLD = 1000; // [PLACEHOLDER] 体感测试:1s 是否够?快机可调到 700
|
||||
// 进度条动画:与最短停留时长保持同步,给玩家明确的加载反馈
|
||||
const MICRO_ENTER_HOLD = 1100; // [PLACEHOLDER] 体感测试:最短 1.1s,快机可调到 900
|
||||
const startT = performance.now();
|
||||
let rafId;
|
||||
function tick(now) {
|
||||
@ -1837,7 +2018,7 @@ function enterMicroWorld(tile) {
|
||||
updateSidebar();
|
||||
initMicroCanvas();
|
||||
startMicroRender();
|
||||
// 2) 至少保持 1 秒(遮罩掩护重活 + 入场动画兜底),到点纯淡出揭幕:无尖峰、无 pop-in
|
||||
// 2) 至少保持最短时长(遮罩掩护重活 + 入场动画兜底),到点纯淡出揭幕:无尖峰、无 pop-in
|
||||
setTimeout(() => {
|
||||
if (rafId) cancelAnimationFrame(rafId);
|
||||
if (flyFill) flyFill.style.width = '100%';
|
||||
@ -2779,7 +2960,7 @@ function swapTiles(a, b) {
|
||||
|
||||
// 建造模式 UI 绑定
|
||||
function setupBuildUI() {
|
||||
const toggle = document.getElementById('buildToggle');
|
||||
const toggle = document.getElementById('btnBuild');
|
||||
const panel = document.getElementById('buildPanel');
|
||||
const bpClose = document.getElementById('bpClose');
|
||||
const bpBrush = document.getElementById('bpBrush');
|
||||
@ -2849,10 +3030,269 @@ function setupBuildUI() {
|
||||
renderBrush();
|
||||
};
|
||||
bpClose.onclick = () => {
|
||||
buildMode = false; toggle.classList.remove('active'); panel.classList.add('hidden');
|
||||
buildMode = false; toggle.classList.remove('active'); panel.classList.remove('active');
|
||||
swapFirst = null; draggingSwap = false; dragSwapFrom = null;
|
||||
closeSidePanel();
|
||||
};
|
||||
|
||||
// ===== 底部三按钮 → 统一左侧面板(数据/包裹/建造 Tab 切换) =====
|
||||
const btnData = document.getElementById('btnData');
|
||||
const btnInventory = document.getElementById('btnInventory');
|
||||
const btnBuild = document.getElementById('btnBuild');
|
||||
const btnTech = document.getElementById('btnTech');
|
||||
const sidePanel = document.getElementById('sidePanel');
|
||||
const spTabs = document.querySelectorAll('#spTabs .sp-tab');
|
||||
const spBodies = document.querySelectorAll('.sp-body');
|
||||
let currentSpTab = null; // 'data' | 'inventory' | 'build' | 'tech'
|
||||
const SP_BODY = { data:'spDataBody', inventory:'spInvBody', build:'spBuildBody', tech:'spTechBody' };
|
||||
|
||||
function openSidePanel(tab) {
|
||||
sidePanel.classList.add('open');
|
||||
document.body.classList.add('side-open');
|
||||
// switch tab
|
||||
spTabs.forEach(t => t.classList.toggle('active', t.dataset.sp === tab));
|
||||
spBodies.forEach(b => b.classList.toggle('active', b.id === SP_BODY[tab]));
|
||||
// button states
|
||||
btnData.classList.toggle('active', tab === 'data');
|
||||
btnInventory.classList.toggle('active', tab === 'inventory');
|
||||
btnBuild.classList.toggle('active', tab === 'build');
|
||||
btnTech.classList.toggle('active', tab === 'tech');
|
||||
currentSpTab = tab;
|
||||
// render content on first open
|
||||
if (tab === 'data') renderData();
|
||||
if (tab === 'inventory') renderInventory();
|
||||
if (tab === 'build') { buildMode = true; document.getElementById('buildPanel').classList.add('active'); }
|
||||
if (tab === 'tech') renderTech();
|
||||
}
|
||||
|
||||
function closeSidePanel() {
|
||||
sidePanel.classList.remove('open');
|
||||
document.body.classList.remove('side-open');
|
||||
btnData.classList.remove('active'); btnInventory.classList.remove('active'); btnBuild.classList.remove('active'); btnTech.classList.remove('active');
|
||||
// exit build mode if was active
|
||||
if (currentSpTab === 'build') {
|
||||
buildMode = false; toggle.classList.remove('active');
|
||||
document.getElementById('buildPanel').classList.remove('active');
|
||||
swapFirst = null; draggingSwap = false; dragSwapFrom = null;
|
||||
}
|
||||
currentSpTab = null;
|
||||
}
|
||||
|
||||
btnData.addEventListener('click', () => {
|
||||
if (currentSpTab === 'data') { closeSidePanel(); return; }
|
||||
openSidePanel('data');
|
||||
});
|
||||
btnInventory.addEventListener('click', () => {
|
||||
if (currentSpTab === 'inventory') { closeSidePanel(); return; }
|
||||
openSidePanel('inventory');
|
||||
});
|
||||
btnBuild.addEventListener('click', () => {
|
||||
if (currentSpTab === 'build') { closeSidePanel(); return; }
|
||||
openSidePanel('build');
|
||||
});
|
||||
btnTech.addEventListener('click', () => {
|
||||
if (currentSpTab === 'tech') { closeSidePanel(); return; }
|
||||
openSidePanel('tech');
|
||||
});
|
||||
// tab clicks inside side panel
|
||||
spTabs.forEach(t => t.addEventListener('click', () => openSidePanel(t.dataset.sp)));
|
||||
|
||||
// buildPanel close → also close side panel
|
||||
bpClose.onclick = () => {
|
||||
buildMode = false; toggle.classList.remove('active');
|
||||
document.getElementById('buildPanel').classList.remove('active');
|
||||
swapFirst = null; draggingSwap = false; dragSwapFrom = null;
|
||||
closeSidePanel();
|
||||
};
|
||||
|
||||
// ===== 世界数据面板(演示 + 预留 /yt_world/api/data) =====
|
||||
function computeWorldStats() {
|
||||
const byTerrain = {};
|
||||
let sumH = 0, n = 0, minH = Infinity, maxH = -Infinity;
|
||||
for (const t of tiles) {
|
||||
if (t.layer !== 'earth') continue;
|
||||
byTerrain[t.terrain] = (byTerrain[t.terrain] || 0) + 1;
|
||||
sumH += t.height; n++;
|
||||
if (t.height < minH) minH = t.height;
|
||||
if (t.height > maxH) maxH = t.height;
|
||||
}
|
||||
return { total: n, byTerrain, avgH: n ? sumH / n : 0, minH: n ? minH : 0, maxH: n ? maxH : 0 };
|
||||
}
|
||||
async function renderData() {
|
||||
const body = document.getElementById('spDataBody');
|
||||
const s = computeWorldStats();
|
||||
let remote = null;
|
||||
try { remote = await API.worldData(); } catch (e) {}
|
||||
const usingRemote = !!(remote && remote.stats);
|
||||
const terr = s.byTerrain;
|
||||
const TICON = { water:'🔵', desert:'🟡', plain:'🟢', forest:'🌲', mountain:'🟤', snow:'❄' };
|
||||
const terrRows = Object.keys(TERRAIN).map(id => {
|
||||
const c = terr[id] || 0;
|
||||
const pct = s.total ? Math.round(c / s.total * 100) : 0;
|
||||
return `<div class="dp-row"><span>${TICON[id] || ''} ${TERRAIN[id].name}</span><span>${c} 块 (${pct}%)</span></div>`;
|
||||
}).join('');
|
||||
body.innerHTML = `
|
||||
<div class="dp-section">
|
||||
<div class="dp-h">概览</div>
|
||||
<div class="dp-row"><span>地块总数</span><span>${s.total}</span></div>
|
||||
<div class="dp-row"><span>平均高度</span><span>${s.avgH.toFixed(1)}</span></div>
|
||||
<div class="dp-row"><span>高度区间</span><span>${s.minH} – ${s.maxH}</span></div>
|
||||
</div>
|
||||
<div class="dp-section">
|
||||
<div class="dp-h">地形分布</div>
|
||||
${terrRows}
|
||||
</div>
|
||||
<div class="dp-note">${usingRemote ? '已连接后端世界数据接口(/yt_world/api/data)' : '演示数据:前端实时从地块计算。后端接口 <code>/yt_world/api/data</code> 待接入。'}</div>
|
||||
`;
|
||||
}
|
||||
|
||||
// ===== 包裹面板(7列 × 行格 + 物资分类) =====
|
||||
const INV_CATEGORIES = {
|
||||
brush: [
|
||||
{ ico:'🟢', nm:'平原', ct:'∞' }, { ico:'🔵', nm:'水域', ct:'∞' }, { ico:'🟤', nm:'山地', ct:'∞' },
|
||||
{ ico:'🌲', nm:'森林', ct:'∞' }, { ico:'🟡', nm:'荒漠', ct:'∞' }, { ico:'❄', nm:'雪原', ct:'∞' },
|
||||
{ ico:'⬜', nm:'空地', ct:'∞' },
|
||||
],
|
||||
template: [
|
||||
{ ico:'🏠', nm:'村落', ct:3 }, { ico:'🏰', nm:'城寨', ct:2 }, { ico:'⛪', nm:'神殿', ct:1 },
|
||||
{ ico:'🌾', nm:'农田', ct:5 }, { ico:'🪨', nm:'矿场', ct:4 }, { ico:'🛤', nm:'道路', ct:8 },
|
||||
{ ico:'🌉', nm:'桥梁', ct:3 }, { ico:'🗼', nm:'高塔', ct:1 }, { ico:'⚱', nm:'雕像', ct:2 },
|
||||
{ ico:'🏕', nm:'营地', ct:4 }, { ico:'🔭', nm:'观星台', ct:1 }, { ico:'📯', nm:'烽火', ct:6 },
|
||||
{ ico:'💧', nm:'水井', ct:5 }, { ico:'🌳', nm:'灵树', ct:2 }, { ico:'🪨', nm:'石碑', ct:3 },
|
||||
],
|
||||
event: [
|
||||
{ ico:'🌟', nm:'丰收', ct:2 }, { ico:'⚡', nm:'雷暴', ct:1 }, { ico:'❄️', nm:'冰封', ct:1 },
|
||||
{ ico:'🌋', nm:'地动', ct:1 }, { ico:'🌈', nm:'虹息', ct:2 }, { ico:'💫', nm:'陨落', ct:1 },
|
||||
{ ico:'🌀', nm:'风暴', ct:1 }, { ico:'☀️', nm:'大旱', ct:1 }, { ico:'🌙', nm:'月蚀', ct:1 },
|
||||
{ ico:'✨', nm:'灵潮', ct:3 }, { ico:'🔥', nm:'野火', ct:1 }, { ico:'💎', nm:'矿脉', ct:2 },
|
||||
{ ico:'🎋', nm:'竹生', ct:4 }, { ico:'🍂', nm:'落叶', ct:3 }, { ico:'🐦', nm:'迁徙', ct:2 },
|
||||
],
|
||||
};
|
||||
|
||||
function renderInvGrid(containerId, items) {
|
||||
const el = document.getElementById(containerId);
|
||||
// pad to multiple of 7 for clean grid
|
||||
const padded = [...items];
|
||||
while (padded.length % 7 !== 0) padded.push(null);
|
||||
el.innerHTML = padded.map(it => {
|
||||
if (!it) return '<div class="inv-slot"></div>';
|
||||
const has = it.ct !== '∞';
|
||||
return `<div class="inv-slot${has ? ' has-item' : ''}" title="${it.nm}${has ? ' ×'+it.ct : ' 无限'}">
|
||||
${it.ico}<span class="inv-ct">${has ? it.ct : ''}</span></div>`;
|
||||
}).join('');
|
||||
}
|
||||
|
||||
async function renderInventory() {
|
||||
let remote = null;
|
||||
try { remote = await API.inventory(); } catch (e) {}
|
||||
if (remote && Array.isArray(remote.items)) {
|
||||
// TODO: 后端返回带 category 字段时按分类渲染
|
||||
renderInvGrid('invGridBrush', remote.items.filter(i => i.cat === 'brush'));
|
||||
renderInvGrid('invGridTemplate', remote.items.filter(i => i.cat === 'template'));
|
||||
renderInvGrid('invGridEvent', remote.items.filter(i => i.cat === 'event'));
|
||||
} else {
|
||||
renderInvGrid('invGridBrush', INV_CATEGORIES.brush);
|
||||
renderInvGrid('invGridTemplate', INV_CATEGORIES.template);
|
||||
renderInvGrid('invGridEvent', INV_CATEGORIES.event);
|
||||
}
|
||||
}
|
||||
|
||||
// ===== 科技树(演示 + 预留 /yt_world/api/tech) =====
|
||||
// 节点按 tier 分阶:前置满足且研究点充足才可研习;状态存 localStorage,预留后端 /yt_world/api/tech 同步。
|
||||
const TECH_TREE = [
|
||||
{ id:'gather', tier:0, ico:'🌿', name:'采集术', cost:1, pre:[], desc:'高效采集资源,建造笔刷消耗 −10%。' },
|
||||
{ id:'fire', tier:0, ico:'🔥', name:'火耕', cost:1, pre:[], desc:'点燃荒原,解锁烧荒造地与基础冶炼。' },
|
||||
{ id:'farm', tier:1, ico:'🌾', name:'农艺', cost:2, pre:['gather'], desc:'开垦农田产量 +30%,解锁轮作。' },
|
||||
{ id:'path', tier:1, ico:'🛤', name:'道途', cost:2, pre:['gather'], desc:'道路连通使单位移动更快。' },
|
||||
{ id:'masonry', tier:1, ico:'🧱', name:'砌筑', cost:2, pre:['fire'], desc:'解锁石质建筑(城寨 / 神殿)。' },
|
||||
{ id:'irrig', tier:2, ico:'💧', name:'水利', cost:3, pre:['farm'], desc:'灌溉网络,干旱事件减伤。' },
|
||||
{ id:'market', tier:2, ico:'🏪', name:'集市', cost:3, pre:['farm','path'], desc:'贸易节点,灵气积累加速。' },
|
||||
{ id:'fort', tier:2, ico:'🏰', name:'城防', cost:3, pre:['masonry'], desc:'城墙与守卫,抵御地动 / 风暴。' },
|
||||
{ id:'spirit', tier:3, ico:'🔮', name:'灵术', cost:4, pre:['irrig','market'], desc:'操控灵气,解锁灵树速生。' },
|
||||
{ id:'astral', tier:3, ico:'🔭', name:'星图', cost:4, pre:['fort'], desc:'观星推演,预测事件与灾异。' },
|
||||
{ id:'ascend', tier:4, ico:'🌟', name:'升灵', cost:5, pre:['spirit','astral'], desc:'世界飞升,解锁星空层建造。' },
|
||||
];
|
||||
const TECH_TIER_NAMES = { 0:'启明', 1:'匠作', 2:'兴业', 3:'通玄', 4:'飞升' };
|
||||
const TECH_POINTS_START = 6; // [PLACEHOLDER] 演示起始研究点,待后端 / 平衡校准
|
||||
const TECH_SAVE_KEY = 'world_tech_save';
|
||||
|
||||
let techState = loadTechState();
|
||||
function loadTechState() {
|
||||
try {
|
||||
const raw = localStorage.getItem(TECH_SAVE_KEY);
|
||||
if (raw) {
|
||||
const o = JSON.parse(raw);
|
||||
return { done: new Set(o.done || []), points: (o.points != null ? o.points : TECH_POINTS_START) };
|
||||
}
|
||||
} catch (e) {}
|
||||
return { done: new Set(), points: TECH_POINTS_START };
|
||||
}
|
||||
function saveTechState() {
|
||||
try { localStorage.setItem(TECH_SAVE_KEY, JSON.stringify({ done:[...techState.done], points: techState.points })); } catch (e) {}
|
||||
}
|
||||
function techPreMet(t) { return t.pre.every(p => techState.done.has(p)); }
|
||||
|
||||
function renderTech() {
|
||||
const body = document.getElementById('spTechBody');
|
||||
const byTier = {};
|
||||
for (const t of TECH_TREE) (byTier[t.tier] = byTier[t.tier] || []).push(t);
|
||||
const tiers = Object.keys(byTier).sort((a,b)=>a-b).map(tier => {
|
||||
const nodes = byTier[tier].map(t => {
|
||||
const done = techState.done.has(t.id);
|
||||
const met = techPreMet(t);
|
||||
const afford = techState.points >= t.cost;
|
||||
const cls = ['tech-node'];
|
||||
if (done) cls.push('done'); else if (!met) cls.push('locked'); else if (afford) cls.push('afford');
|
||||
const preHtml = t.pre.length
|
||||
? '前置:' + t.pre.map(p => {
|
||||
const d = TECH_TREE.find(x => x.id === p);
|
||||
const ok = techState.done.has(p);
|
||||
return `<b class="${ok ? 'met' : ''}">${d ? d.name : p}</b>`;
|
||||
}).join('、')
|
||||
: '前置:无(启明科技)';
|
||||
let btn;
|
||||
if (done) btn = `<button class="tn-btn" disabled>✓ 已研习</button>`;
|
||||
else if (!met) btn = `<button class="tn-btn" disabled>未解锁</button>`;
|
||||
else btn = `<button class="tn-btn" data-tech="${t.id}">研究(${t.cost} 点)</button>`;
|
||||
return `<div class="${cls.join(' ')}">
|
||||
<div class="tn-top"><span class="tn-ico">${t.ico}</span><span class="tn-name">${t.name}</span></div>
|
||||
<div class="tn-desc">${t.desc}</div>
|
||||
<div class="tn-pre">${preHtml}</div>
|
||||
<div class="tn-cost">花费 ${t.cost} 研究点</div>
|
||||
${btn}
|
||||
</div>`;
|
||||
}).join('');
|
||||
return `<div class="tech-tier"><div class="tech-tier-h">${TECH_TIER_NAMES[tier] || ('阶'+tier)} · 第 ${+tier+1} 阶</div><div class="tech-nodes">${nodes}</div></div>`;
|
||||
}).join('');
|
||||
|
||||
body.innerHTML = `
|
||||
<div class="tech-pool">
|
||||
<span class="tp-label">研究点</span>
|
||||
<span class="tp-val">${techState.points}</span>
|
||||
<button class="tp-reset" id="techReset">重置</button>
|
||||
</div>
|
||||
${tiers}
|
||||
<div class="tech-note">演示科技树:前置满足且研究点充足即可研习,状态存本地。后端接口 <code>/yt_world/api/tech</code> 待接入,将支持跨端同步与真正的增益结算。</div>
|
||||
`;
|
||||
body.querySelectorAll('.tn-btn[data-tech]').forEach(b => {
|
||||
b.addEventListener('click', () => {
|
||||
const id = b.dataset.tech;
|
||||
const t = TECH_TREE.find(x => x.id === id);
|
||||
if (!t || techState.done.has(id) || !techPreMet(t) || techState.points < t.cost) return;
|
||||
techState.points -= t.cost;
|
||||
techState.done.add(id);
|
||||
saveTechState();
|
||||
renderTech();
|
||||
});
|
||||
});
|
||||
const reset = body.querySelector('#techReset');
|
||||
if (reset) reset.onclick = () => {
|
||||
if (!confirm('重置科技树?已研习进度与研究点将清空。')) return;
|
||||
techState = { done: new Set(), points: TECH_POINTS_START };
|
||||
saveTechState();
|
||||
renderTech();
|
||||
};
|
||||
}
|
||||
|
||||
// 平铺:把所有地块高度设为最低(1),一键压平(沿用 setHeight 派生 terrain/tier/variant)
|
||||
const bpFlatten = document.getElementById('bpFlatten');
|
||||
|
||||