Compare commits

..

No commits in common. "d103dd9ca15f96d4a02b012dfa28ac53811bc985" and "ffd4bb8f080594d7c238b265c48c91a24ebaf3c8" have entirely different histories.

82 changed files with 713 additions and 4889 deletions

View File

@ -8,7 +8,6 @@
'data': [ 'data': [
'security/ir.model.access.csv', 'security/ir.model.access.csv',
'data/game_base_data.xml', 'data/game_base_data.xml',
'data/story_cron.xml',
'data/game_achievement_data.xml', 'data/game_achievement_data.xml',
'views/codex_views.xml', 'views/codex_views.xml',
'views/rule_category_views.xml', 'views/rule_category_views.xml',

View File

@ -166,6 +166,191 @@
<field name="body_html"><![CDATA[<p>宇森官网第一阶段上线:介绍 / 规则 / 图鉴 / 论坛 / 公告 五块,单页切换呈现。欢迎园丁们入驻。</p>]]></field> <field name="body_html"><![CDATA[<p>宇森官网第一阶段上线:介绍 / 规则 / 图鉴 / 论坛 / 公告 五块,单页切换呈现。欢迎园丁们入驻。</p>]]></field>
</record> </record>
<!-- ============ 页面板块 ============ -->
<!-- 特色 -->
<record id="page_features" model="game.page">
<field name="key">features</field>
<field name="title">游戏特色</field>
<field name="subtitle">CORE FEATURES</field>
<field name="sequence">10</field>
</record>
<record id="page_features_item_1" model="game.page.item">
<field name="page_id" ref="page_features"/>
<field name="sequence">10</field>
<field name="icon">🌐</field>
<field name="title">垂直五层世界</field>
<field name="desc">从地心熔岩到陨石星空,文明在五个垂直切面中同时生长,每一层都有独立的生态、资源与事件池。</field>
</record>
<record id="page_features_item_2" model="game.page.item">
<field name="page_id" ref="page_features"/>
<field name="sequence">20</field>
<field name="icon">🌱</field>
<field name="title">自发演化</field>
<field name="desc">不是脚本,不是关卡。文明自己生长、分裂、兼并、飞升——每一次开局都是一部独一无二的编年史。</field>
</record>
<record id="page_features_item_3" model="game.page.item">
<field name="page_id" ref="page_features"/>
<field name="sequence">30</field>
<field name="icon">🧑‍🌾</field>
<field name="title">园丁视角</field>
<field name="desc">你不是英雄,也不是上帝。你是园丁——播种灵气、引导势力、守望文明,在关键节点轻推一把,让世界自洽前行。</field>
</record>
<record id="page_features_item_4" model="game.page.item">
<field name="page_id" ref="page_features"/>
<field name="sequence">40</field>
<field name="icon">⚖️</field>
<field name="title">三维等级联动</field>
<field name="desc">世界等级定舞台上限,势力等级定群体进度,角色等级定个体极限。三者交叉构成无限策略空间。</field>
</record>
<record id="page_features_item_5" model="game.page.item">
<field name="page_id" ref="page_features"/>
<field name="sequence">50</field>
<field name="icon">💰</field>
<field name="title">六档货币体系</field>
<field name="desc">从墟铢到元玺,百进制兑换贯穿五层世界。跨层贸易、资源调度、神器交易构成完整经济闭环。</field>
</record>
<record id="page_features_item_6" model="game.page.item">
<field name="page_id" ref="page_features"/>
<field name="sequence">60</field>
<field name="icon">🎭</field>
<field name="title">活的个体</field>
<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>
<field name="title">世界起源</field>
<field name="subtitle">BACKGROUND</field>
<field name="sequence">20</field>
<field name="body_html"><![CDATA[<p>天地初开,混沌未分。一缕灵气自地心涌出,沿垂直切面向上攀爬——穿透岩层、跨越地表、掠过云海、直抵星空。五层世界自此苏醒。</p>
<p>地表层最先活过来。灵气稀薄,但足够点燃文明的火种。各族散居荒野,狩猎采集,以血缘结群,在蒙昧的凡土上挣扎求生。这是<b style="color:#fff">凡墟</b></p>
<p>而后灵气渐浓,天地共振。聚落涌现,分工萌芽,物物交换催生最早的势力。灵启的时代,文明开始有了形状。</p>
<p>再往后,是兼并、是征伐、是道统与造物的竞逐。<b style="color:#fff">玄界</b>的大地上,多元族群共治的完整疆域成形,传奇事件如潮涌来。</p>
<p>直到有一天,有人掘穿了地表之下的黑暗,有人驾着浮空石飞上了云海。地下深渊与浩瀚星空同时向文明敞开——<b style="color:#fff">穹冥</b>时代,天地上下尽数纳入管辖。</p>
<p>最终,顶尖文明勘破了这片天地的诞生本源。他们追溯灵气的源头,在地心深处找到了答案。飞升、轮回、更高维度的演化实验——<b style="color:#fff">溯元</b>,不是终点,而是新的起点。</p>
<p style="margin-top:22px;color:var(--gold);font-style:italic;">而你,是这一切的园丁。</p>]]></field>
</record>
<record id="page_bg_layer_1" model="game.page.item">
<field name="page_id" ref="page_background"/>
<field name="sequence">10</field>
<field name="icon"></field>
<field name="title">星空层</field>
<field name="desc">陨石科技带 · 穹鎏产地</field>
</record>
<record id="page_bg_layer_2" model="game.page.item">
<field name="page_id" ref="page_background"/>
<field name="sequence">20</field>
<field name="icon"></field>
<field name="title">天空层</field>
<field name="desc">云朵地基块 · 浮空城邦</field>
</record>
<record id="page_bg_layer_3" model="game.page.item">
<field name="page_id" ref="page_background"/>
<field name="sequence">30</field>
<field name="icon"></field>
<field name="title">地表层</field>
<field name="desc">文明发源地 · 主舞台</field>
</record>
<record id="page_bg_layer_4" model="game.page.item">
<field name="page_id" ref="page_background"/>
<field name="sequence">40</field>
<field name="icon"></field>
<field name="title">地下层</field>
<field name="desc">矿脉深渊 · 冥玉产地</field>
</record>
<record id="page_bg_layer_5" model="game.page.item">
<field name="page_id" ref="page_background"/>
<field name="sequence">50</field>
<field name="icon"></field>
<field name="title">地心层</field>
<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>
<field name="title">致园丁</field>
<field name="subtitle">A LETTER TO GARDENERS</field>
<field name="sequence">30</field>
<field name="body_html"><![CDATA[<p>致每一位踏入宇森的园丁:</p>
<p>感谢你来到这里。</p>
<p>宇森不是一款传统的策略游戏。这里没有关卡,没有通关,没有固定的胜利条件。我们想做的,是一个<span class="hl">会自己呼吸的世界</span>——你播下一颗种子,它自己长成一片森林;你引导一个族群,它自己走出一段历史。</p>
<p>你是园丁,不是上帝。你不能命令谁去做什么,但你可以改变土壤、调节灵气、在关键节点轻轻推一把。文明的走向由无数个体的选择汇聚而成,而你只是其中一股温柔的力。</p>
<p>这个世界有五层。从地心到星空,每一层都有自己的生态和故事。文明会从地表的凡墟开始,一路向上向下生长,直到触及世界的本源。这个过程可能很慢,也可能充满意外——但那正是我们想要的。</p>
<p>我们还在生长。宇森目前处于早期阶段,很多系统仍在打磨。但你看到的每一个种族、每一件神器、每一段传奇事件,都是这个世界真实存在的一部分。它们会随着版本的推进越来越厚,越来越活。</p>
<p>如果你在这里找到了哪怕一刻的沉浸感,那就是我们最大的成功。</p>
<p>欢迎播种。</p>]]></field>
</record>
<!-- ============ 论坛种子(登录后可见) ============ --> <!-- ============ 论坛种子(登录后可见) ============ -->
<record id="thread_welcome" model="game.forum.thread"> <record id="thread_welcome" model="game.forum.thread">
<field name="name">【公告】宇森官网第一阶段上线,欢迎园丁们入驻</field> <field name="name">【公告】宇森官网第一阶段上线,欢迎园丁们入驻</field>
@ -219,8 +404,59 @@
<field name="body_html"><![CDATA[<p>剧情站正式上线!首卷《园丁手记·凡墟篇》已开放阅读,记录灵气初醒、第一株苗与聚落第一缕光的蒙昧年代。前往导航「剧情」即可翻阅。</p>]]></field> <field name="body_html"><![CDATA[<p>剧情站正式上线!首卷《园丁手记·凡墟篇》已开放阅读,记录灵气初醒、第一株苗与聚落第一缕光的蒙昧年代。前往导航「剧情」即可翻阅。</p>]]></field>
</record> </record>
<!-- ============ 剧情(小说)种子 ============ <!-- ============ 剧情(小说)种子 ============ -->
注意:小说内容改为由 novel/ 源文件经 novel/sync_novel.py 同步进数据库, <record id="story_vol_fanxu" model="game.story.volume">
不再在此 XML 硬编码。修改 novel/ 后运行该脚本即可更新剧情站。 --> <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> </odoo>

View File

@ -1,18 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<!-- 定时把 novel/ 下的 Markdown 增量同步进数据库。
间隔 5 分钟:改完 md 无需手动跑命令,网站数分钟内自动更新;
Odoo 重启后调度器恢复,也会按时触发(即"重启带着一块更新")。
Odoo 18 的 ir.cron 改为委托 ir.actions.servercode 状态)执行,
故用 model_id + state="code" + code 的方式,旧的 model/function/args/numbercall 字段已废弃。 -->
<record id="ir_cron_sync_novel" model="ir.cron">
<field name="name">宇森·小说 Markdown 同步</field>
<field name="model_id" ref="game_base.model_game_story_volume"/>
<field name="state">code</field>
<field name="code">model.action_sync_novel()</field>
<field name="interval_number">5</field>
<field name="interval_type">minutes</field>
<field name="active" eval="True"/>
<field name="user_id" ref="base.user_root"/>
</record>
</odoo>

View File

@ -1,12 +1,5 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
import os
import sys
import subprocess
import logging
from odoo import models, fields from odoo import models, fields
from odoo.modules.module import get_module_path
_logger = logging.getLogger(__name__)
class GameStoryVolume(models.Model): class GameStoryVolume(models.Model):
@ -20,30 +13,6 @@ class GameStoryVolume(models.Model):
intro = fields.Html(string='卷首语') intro = fields.Html(string='卷首语')
chapter_ids = fields.One2many('game.story.chapter', 'volume_id', string='章节') chapter_ids = fields.One2many('game.story.chapter', 'volume_id', string='章节')
def action_sync_novel(self):
"""由定时任务ir.cron调用把 novel/ 下的 Markdown 增量同步进数据库。
走独立同步脚本novel/sync_novel.py单一实现来源
注意脚本依赖 venv psycopg2 Odoo 可能跑在系统 Python
因此显式使用项目 venv python 执行而非 sys.executable"""
base = get_module_path('game_base')
script = os.path.normpath(os.path.join(base, '..', '..', 'novel', 'sync_novel.py'))
if not os.path.exists(script):
_logger.warning('小说同步脚本不存在: %s', script)
return
# 优先用项目 venv 的 python自带 psycopg2否则退化为当前解释器
venv_py = os.path.normpath(os.path.join(base, '..', '..', '..', '.venv', 'Scripts', 'python.exe'))
python = venv_py if os.path.exists(venv_py) else sys.executable
try:
proc = subprocess.run([python, script],
capture_output=True, text=True, timeout=180)
if proc.returncode != 0:
_logger.error('小说同步失败(%s): %s', proc.returncode, (proc.stderr or '')[-2000:])
else:
last = (proc.stdout or '').strip().splitlines()
_logger.info('小说同步完成: %s', last[-1] if last else 'ok')
except Exception as e:
_logger.exception('小说同步异常: %s', e)
class GameStoryChapter(models.Model): class GameStoryChapter(models.Model):
_name = 'game.story.chapter' _name = 'game.story.chapter'

View File

@ -7,10 +7,6 @@
'depends': ['base', 'web'], 'depends': ['base', 'web'],
'data': [ 'data': [
'security/ir.model.access.csv', 'security/ir.model.access.csv',
'data/codex_resource.xml',
'data/codex_creature.xml',
'data/codex_monster.xml',
'data/codex_race.xml',
'views/yt_world_views.xml', 'views/yt_world_views.xml',
], ],
'assets': { 'assets': {

View File

@ -9,22 +9,11 @@ _logger = logging.getLogger(__name__)
def _json(data): def _json(data):
"""返回 UTF-8 JSON 响应(与 game_base 一致),并附带 CORS 头以支持跨域8080→8069""" """返回 UTF-8 JSON 响应(与 game_base 一致)。"""
headers = { return Response(
'Content-Type': 'application/json; charset=utf-8', json.dumps(data, ensure_ascii=False),
'Access-Control-Allow-Methods': 'GET, POST, OPTIONS', content_type='application/json; charset=utf-8',
'Access-Control-Allow-Headers': 'Content-Type', )
}
try:
origin = request.httprequest.headers.get('Origin')
if origin:
headers['Access-Control-Allow-Origin'] = origin
headers['Access-Control-Allow-Credentials'] = 'true'
else:
headers['Access-Control-Allow-Origin'] = '*'
except Exception:
headers['Access-Control-Allow-Origin'] = '*'
return Response(json.dumps(data, ensure_ascii=False), headers=headers)
def _post_body(): def _post_body():
@ -97,46 +86,3 @@ class YtWorldApi(http.Controller):
world.name = name world.name = name
_logger.info('yt.world rename uid=%s %r -> %r', user.id, old, name) _logger.info('yt.world rename uid=%s %r -> %r', user.id, old, name)
return _json({'ok': True, 'name': name}) return _json({'ok': True, 'name': name})
# ========== 图鉴 API ==========
def _records_json(self, model, fields):
"""将 model 的全部记录转为 [{field: value}, ...] 格式Json 字段解析为对象)。"""
recs = request.env[model].sudo().search([], order='sequence, id')
out = []
for r in recs:
item = {}
for f in fields:
val = getattr(r, f)
if isinstance(val, str):
try:
val = json.loads(val)
except (ValueError, TypeError):
pass
item[f] = val
out.append(item)
return _json(out)
@http.route('/yt_world/api/codex/resources', type='http', auth='public',
methods=['GET', 'OPTIONS'], csrf=False)
def codex_resources(self, **kw):
return self._records_json('yt.codex.resource',
['code','name','icon','category','quality','source','uses','recipe','description'])
@http.route('/yt_world/api/codex/creatures', type='http', auth='public',
methods=['GET', 'OPTIONS'], csrf=False)
def codex_creatures(self, **kw):
return self._records_json('yt.codex.creature',
['code','name','icon','habitat','tameable','diet','products','description'])
@http.route('/yt_world/api/codex/monsters', type='http', auth='public',
methods=['GET', 'OPTIONS'], csrf=False)
def codex_monsters(self, **kw):
return self._records_json('yt.codex.monster',
['code','name','icon','tier','level_min','level_max','habitat','drops','description'])
@http.route('/yt_world/api/codex/races', type='http', auth='public',
methods=['GET', 'OPTIONS'], csrf=False)
def codex_races(self, **kw):
return self._records_json('yt.codex.race',
['code','name','icon','layer','traits','description'])

View File

@ -1,103 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="crea_cow" model="yt.codex.creature">
<field name="code">cow</field><field name="name"></field><field name="icon">🐄</field>
<field name="habitat">草原</field><field name="tameable">1</field><field name="diet">植食</field>
<field name="products">["肉","牛奶","皮革"]</field><field name="sequence">10</field>
</record>
<record id="crea_sheep" model="yt.codex.creature">
<field name="code">sheep</field><field name="name"></field><field name="icon">🐑</field>
<field name="habitat">草原</field><field name="tameable">1</field><field name="diet">植食</field>
<field name="products">["肉","羊毛"]</field><field name="sequence">20</field>
</record>
<record id="crea_horse" model="yt.codex.creature">
<field name="code">horse</field><field name="name"></field><field name="icon">🐎</field>
<field name="habitat">草原</field><field name="tameable">1</field><field name="diet">植食</field>
<field name="products">["骑乘运输","骑兵"]</field><field name="sequence">30</field>
</record>
<record id="crea_pig" model="yt.codex.creature">
<field name="code">pig</field><field name="name"></field><field name="icon">🐖</field>
<field name="habitat">林地/村落</field><field name="tameable">1</field><field name="diet">杂食</field>
<field name="products">["肉","鬃毛"]</field><field name="sequence">40</field>
</record>
<record id="crea_chicken" model="yt.codex.creature">
<field name="code">chicken</field><field name="name"></field><field name="icon">🐔</field>
<field name="habitat">村落/草地</field><field name="tameable">1</field><field name="diet">杂食</field>
<field name="products">["蛋","肉","羽毛"]</field><field name="sequence">50</field>
</record>
<record id="crea_duck" model="yt.codex.creature">
<field name="code">duck</field><field name="name"></field><field name="icon">🦆</field>
<field name="habitat">湿地/河岸</field><field name="tameable">1</field><field name="diet">杂食</field>
<field name="products">["蛋","肉","羽绒"]</field><field name="sequence">60</field>
</record>
<record id="crea_dog" model="yt.codex.creature">
<field name="code">dog</field><field name="name"></field><field name="icon">🐕</field>
<field name="habitat">村落/营地</field><field name="tameable">1</field><field name="diet">杂食</field>
<field name="products">["护卫","狩猎协助"]</field><field name="sequence">70</field>
</record>
<record id="crea_cat" model="yt.codex.creature">
<field name="code">cat</field><field name="name"></field><field name="icon">🐈</field>
<field name="habitat">村落</field><field name="tameable">1</field><field name="diet">肉食</field>
<field name="products">["捕鼠","幸运"]</field><field name="sequence">80</field>
</record>
<record id="crea_rabbit" model="yt.codex.creature">
<field name="code">rabbit</field><field name="name"></field><field name="icon">🐇</field>
<field name="habitat">林地/草地</field><field name="tameable">1</field><field name="diet">植食</field>
<field name="products">["肉","毛皮"]</field><field name="sequence">90</field>
</record>
<record id="crea_squirrel" model="yt.codex.creature">
<field name="code">squirrel</field><field name="name">松鼠</field><field name="icon">🐿️</field>
<field name="habitat">森林</field><field name="diet">植食</field>
<field name="products">["坚果","藏宝指引"]</field><field name="sequence">100</field>
</record>
<record id="crea_hedgehog" model="yt.codex.creature">
<field name="code">hedgehog</field><field name="name">刺猬</field><field name="icon">🦔</field>
<field name="habitat">林地/村落</field><field name="tameable">1</field><field name="diet">杂食</field>
<field name="products">["虫害防治"]</field><field name="sequence">110</field>
</record>
<record id="crea_bee" model="yt.codex.creature">
<field name="code">bee</field><field name="name"></field><field name="icon">🐝</field>
<field name="habitat">花海/林地</field><field name="tameable">1</field><field name="diet">植食</field>
<field name="products">["蜂蜜","授粉"]</field><field name="sequence">120</field>
</record>
<record id="crea_butterfly" model="yt.codex.creature">
<field name="code">butterfly</field><field name="name"></field><field name="icon">🦋</field>
<field name="habitat">花海/草地</field><field name="diet">植食</field>
<field name="products">["灵粉","生态指示"]</field><field name="sequence">130</field>
</record>
<record id="crea_deer" model="yt.codex.creature">
<field name="code">deer</field><field name="name">鹿</field><field name="icon">🦌</field>
<field name="habitat">森林</field><field name="diet">植食</field>
<field name="products">["肉","鹿皮","鹿角"]</field><field name="sequence">140</field>
</record>
<record id="crea_fox" model="yt.codex.creature">
<field name="code">fox</field><field name="name"></field><field name="icon">🦊</field>
<field name="habitat">林地/丘陵</field><field name="diet">肉食</field>
<field name="products">["狐皮","灵性"]</field><field name="sequence">150</field>
</record>
<record id="crea_wolf" model="yt.codex.creature">
<field name="code">wolf</field><field name="name"></field><field name="icon">🐺</field>
<field name="habitat">森林/山地</field><field name="diet">肉食</field>
<field name="products">["狼皮","利齿"]</field><field name="sequence">160</field>
</record>
<record id="crea_snake" model="yt.codex.creature">
<field name="code">snake</field><field name="name"></field><field name="icon">🐍</field>
<field name="habitat">丛林/岩洞</field><field name="diet">肉食</field>
<field name="products">["蛇毒","蛇皮","灵性"]</field><field name="sequence">170</field>
</record>
<record id="crea_eagle" model="yt.codex.creature">
<field name="code">eagle</field><field name="name"></field><field name="icon">🦅</field>
<field name="habitat">高山/天空</field><field name="diet">肉食</field>
<field name="products">["巨羽","视野共享"]</field><field name="sequence">180</field>
</record>
<record id="crea_fish" model="yt.codex.creature">
<field name="code">fish</field><field name="name"></field><field name="icon">🐟</field>
<field name="habitat">河流/湖泊</field><field name="diet">杂食</field>
<field name="products">["鱼肉","鱼骨","鱼苗"]</field><field name="sequence">190</field>
</record>
<record id="crea_turtle" model="yt.codex.creature">
<field name="code">turtle</field><field name="name"></field><field name="icon">🐢</field>
<field name="habitat">河岸/湖泊</field><field name="tameable">1</field><field name="diet">植食</field>
<field name="products">["龟甲盾","龟蛋","长寿"]</field><field name="sequence">200</field>
</record>
</odoo>

View File

@ -1,266 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<!-- ===== 普通野怪 ===== -->
<record id="mon_grasswolf" model="yt.codex.monster">
<field name="code">grasswolf</field><field name="name">草原狼</field><field name="icon">🐺</field>
<field name="tier">normal</field><field name="level_min">1</field><field name="level_max">3</field>
<field name="habitat">草原</field>
<field name="drops">["兽骨","毛皮"]</field><field name="sequence">10</field>
</record>
<record id="mon_deer" model="yt.codex.monster">
<field name="code">deer</field><field name="name">大角鹿</field><field name="icon">🦌</field>
<field name="tier">normal</field><field name="level_min">2</field><field name="level_max">4</field>
<field name="habitat">草原</field>
<field name="drops">["兽骨","毛皮","鹿角"]</field><field name="sequence">20</field>
</record>
<record id="mon_molerat" model="yt.codex.monster">
<field name="code">molerat</field><field name="name">地鼠群</field><field name="icon">🐭</field>
<field name="tier">normal</field><field name="level_min">1</field><field name="level_max">2</field>
<field name="habitat">草原</field>
<field name="drops">["兽骨","毛皮"]</field><field name="sequence">30</field>
</record>
<record id="mon_boar" model="yt.codex.monster">
<field name="code">boar</field><field name="name">野猪</field><field name="icon">🐗</field>
<field name="tier">normal</field><field name="level_min">3</field><field name="level_max">5</field>
<field name="habitat">草原</field>
<field name="drops">["肉","兽骨","鬃毛"]</field><field name="sequence">40</field>
</record>
<record id="mon_spider" model="yt.codex.monster">
<field name="code">spider</field><field name="name">林间蜘蛛</field><field name="icon">🕷️</field>
<field name="tier">normal</field><field name="level_min">3</field><field name="level_max">5</field>
<field name="habitat">森林</field>
<field name="drops">["蛛丝","毒囊"]</field><field name="sequence">50</field>
</record>
<record id="mon_mushroom" model="yt.codex.monster">
<field name="code">mushroom</field><field name="name">毒蘑菇精</field><field name="icon">🍄</field>
<field name="tier">normal</field><field name="level_min">4</field><field name="level_max">6</field>
<field name="habitat">森林</field>
<field name="drops">["菌丝","草本"]</field><field name="sequence">60</field>
</record>
<record id="mon_treant" model="yt.codex.monster">
<field name="code">treant</field><field name="name">树精</field><field name="icon">🌳</field>
<field name="tier">normal</field><field name="level_min">5</field><field name="level_max">8</field>
<field name="habitat">森林</field>
<field name="drops">["木材","树脂","露珠"]</field><field name="sequence">70</field>
</record>
<record id="mon_thornsnake" model="yt.codex.monster">
<field name="code">thornsnake</field><field name="name">荆棘蛇</field><field name="icon">🐍</field>
<field name="tier">normal</field><field name="level_min">4</field><field name="level_max">6</field>
<field name="habitat">森林</field>
<field name="drops">["蛇皮","蛇毒"]</field><field name="sequence">80</field>
</record>
<record id="mon_maddog" model="yt.codex.monster">
<field name="code">maddog</field><field name="name">疯狗</field><field name="icon">🐕‍🦺</field>
<field name="tier">normal</field><field name="level_min">3</field><field name="level_max">5</field>
<field name="habitat">森林</field>
<field name="drops">["兽骨","毛皮"]</field><field name="sequence">90</field>
</record>
<record id="mon_golem" model="yt.codex.monster">
<field name="code">golem</field><field name="name">石傀儡</field><field name="icon">🗿</field>
<field name="tier">normal</field><field name="level_min">7</field><field name="level_max">10</field>
<field name="habitat">山地</field>
<field name="drops">["石材","矿石"]</field><field name="sequence">100</field>
</record>
<record id="mon_scorpion" model="yt.codex.monster">
<field name="code">scorpion</field><field name="name">岩蝎</field><field name="icon">🦂</field>
<field name="tier">normal</field><field name="level_min">6</field><field name="level_max">9</field>
<field name="habitat">山地</field>
<field name="drops">["鳞片","毒囊"]</field><field name="sequence">110</field>
</record>
<record id="mon_mandrill" model="yt.codex.monster">
<field name="code">mandrill</field><field name="name">山魈</field><field name="icon">🐒</field>
<field name="tier">normal</field><field name="level_min">6</field><field name="level_max">8</field>
<field name="habitat">山地</field>
<field name="drops">["毛皮","兽骨"]</field><field name="sequence">120</field>
</record>
<record id="mon_sandworm" model="yt.codex.monster">
<field name="code">sandworm</field><field name="name">沙虫</field><field name="icon">🪱</field>
<field name="tier">normal</field><field name="level_min">7</field><field name="level_max">10</field>
<field name="habitat">沙漠</field>
<field name="drops">["鳞片","兽骨"]</field><field name="sequence">130</field>
</record>
<record id="mon_desertscorp" model="yt.codex.monster">
<field name="code">desertscorp</field><field name="name">荒漠蝎</field><field name="icon">🦂</field>
<field name="tier">normal</field><field name="level_min">6</field><field name="level_max">9</field>
<field name="habitat">沙漠</field>
<field name="drops">["毒囊","鳞片"]</field><field name="sequence">140</field>
</record>
<record id="mon_vulture" model="yt.codex.monster">
<field name="code">vulture</field><field name="name">秃鹫</field><field name="icon">🦅</field>
<field name="tier">normal</field><field name="level_min">5</field><field name="level_max">8</field>
<field name="habitat">沙漠</field>
<field name="drops">["羽毛","兽骨"]</field><field name="sequence">150</field>
</record>
<record id="mon_crocodile" model="yt.codex.monster">
<field name="code">crocodile</field><field name="name">河鳄</field><field name="icon">🐊</field>
<field name="tier">normal</field><field name="level_min">6</field><field name="level_max">9</field>
<field name="habitat">水域</field>
<field name="drops">["鳞片","皮革"]</field><field name="sequence">160</field>
</record>
<record id="mon_watersnake" model="yt.codex.monster">
<field name="code">watersnake</field><field name="name">水蛇</field><field name="icon">🐍</field>
<field name="tier">normal</field><field name="level_min">4</field><field name="level_max">6</field>
<field name="habitat">水域</field>
<field name="drops">["蛇皮","蛇毒"]</field><field name="sequence">170</field>
</record>
<record id="mon_frog" model="yt.codex.monster">
<field name="code">frog</field><field name="name">巨蛙</field><field name="icon">🐸</field>
<field name="tier">normal</field><field name="level_min">5</field><field name="level_max">8</field>
<field name="habitat">水域</field>
<field name="drops">["鳞片","露珠"]</field><field name="sequence">180</field>
</record>
<record id="mon_junglepython" model="yt.codex.monster">
<field name="code">junglepython</field><field name="name">丛林蟒</field><field name="icon">🐍</field>
<field name="tier">normal</field><field name="level_min">8</field><field name="level_max">12</field>
<field name="habitat">丛林</field>
<field name="drops">["蛇皮","兽骨","肉"]</field><field name="sequence">190</field>
</record>
<record id="mon_mosquito" model="yt.codex.monster">
<field name="code">mosquito</field><field name="name">吸血蚊群</field><field name="icon">🦟</field>
<field name="tier">normal</field><field name="level_min">7</field><field name="level_max">10</field>
<field name="habitat">丛林</field>
<field name="drops">["露珠","毛皮"]</field><field name="sequence">200</field>
</record>
<record id="mon_ape" model="yt.codex.monster">
<field name="code">ape</field><field name="name">丛林猿</field><field name="icon">🦧</field>
<field name="tier">normal</field><field name="level_min">7</field><field name="level_max">10</field>
<field name="habitat">丛林</field>
<field name="drops">["毛皮","木材"]</field><field name="sequence">210</field>
</record>
<record id="mon_cavespider" model="yt.codex.monster">
<field name="code">cavespider</field><field name="name">洞穴巨蛛</field><field name="icon">🕷️</field>
<field name="tier">normal</field><field name="level_min">10</field><field name="level_max">14</field>
<field name="habitat">洞穴</field>
<field name="drops">["蛛丝","毒囊","兽骨"]</field><field name="sequence">220</field>
</record>
<record id="mon_bat" model="yt.codex.monster">
<field name="code">bat</field><field name="name">蝙蝠群</field><field name="icon">🦇</field>
<field name="tier">normal</field><field name="level_min">8</field><field name="level_max">12</field>
<field name="habitat">洞穴</field>
<field name="drops">["羽毛","兽骨"]</field><field name="sequence">230</field>
</record>
<record id="mon_ghost" model="yt.codex.monster">
<field name="code">ghost</field><field name="name">幽魂</field><field name="icon">👻</field>
<field name="tier">normal</field><field name="level_min">12</field><field name="level_max">16</field>
<field name="habitat">洞穴</field>
<field name="drops">["灵气","露珠"]</field><field name="sequence">240</field>
</record>
<record id="mon_crystal" model="yt.codex.monster">
<field name="code">crystal</field><field name="name">晶石怪</field><field name="icon">💎</field>
<field name="tier">normal</field><field name="level_min">10</field><field name="level_max">15</field>
<field name="habitat">洞穴</field>
<field name="drops">["矿石","水晶"]</field><field name="sequence">250</field>
</record>
<record id="mon_nightmare" model="yt.codex.monster">
<field name="code">nightmare</field><field name="name">夜魇</field><field name="icon">🌑</field>
<field name="tier">normal</field><field name="level_min">12</field><field name="level_max">16</field>
<field name="habitat">特殊</field>
<field name="drops">["灵气","露珠"]</field><field name="sequence">260</field>
</record>
<record id="mon_fogwalker" model="yt.codex.monster">
<field name="code">fogwalker</field><field name="name">雾行者</field><field name="icon">🌫️</field>
<field name="tier">normal</field><field name="level_min">10</field><field name="level_max">14</field>
<field name="habitat">特殊</field>
<field name="drops">["露珠","羽毛"]</field><field name="sequence">270</field>
</record>
<record id="mon_zombie" model="yt.codex.monster">
<field name="code">zombie</field><field name="name">饥饿尸</field><field name="icon">💀</field>
<field name="tier">normal</field><field name="level_min">8</field><field name="level_max">12</field>
<field name="habitat">特殊</field>
<field name="drops">["兽骨","灵气"]</field><field name="sequence">280</field>
</record>
<record id="mon_thunderbird" model="yt.codex.monster">
<field name="code">thunderbird</field><field name="name">雷鸟</field><field name="icon"></field>
<field name="tier">normal</field><field name="level_min">10</field><field name="level_max">14</field>
<field name="habitat">高山</field>
<field name="drops">["羽毛","水晶"]</field><field name="sequence">290</field>
</record>
<record id="mon_windspirit" model="yt.codex.monster">
<field name="code">windspirit</field><field name="name">风精</field><field name="icon">🌪️</field>
<field name="tier">normal</field><field name="level_min">8</field><field name="level_max">12</field>
<field name="habitat">高山</field>
<field name="drops">["露珠","羽毛"]</field><field name="sequence">300</field>
</record>
<!-- ===== 精英野怪 ===== -->
<record id="mon_elite_wolfking" model="yt.codex.monster">
<field name="code">wolfking</field><field name="name">狼王</field><field name="icon">🐺✨</field>
<field name="tier">elite</field><field name="level_min">15</field><field name="level_max">18</field>
<field name="habitat">草原</field>
<field name="drops">["毛皮","兽骨","狼王牙"]</field><field name="sequence">10</field>
</record>
<record id="mon_elite_ancienttreant" model="yt.codex.monster">
<field name="code">ancienttreant</field><field name="name">远古树精</field><field name="icon">🌳✨</field>
<field name="tier">elite</field><field name="level_min">18</field><field name="level_max">22</field>
<field name="habitat">森林</field>
<field name="drops">["木材","树脂","生命露珠"]</field><field name="sequence">20</field>
</record>
<record id="mon_elite_lavagiant" model="yt.codex.monster">
<field name="code">lavagiant</field><field name="name">熔岩巨人</field><field name="icon">🔥✨</field>
<field name="tier">elite</field><field name="level_min">20</field><field name="level_max">25</field>
<field name="habitat">山地</field>
<field name="drops">["石材","矿石","火焰水晶"]</field><field name="sequence">30</field>
</record>
<record id="mon_elite_shadow" model="yt.codex.monster">
<field name="code">shadow</field><field name="name">幽影刺客</field><field name="icon">👤✨</field>
<field name="tier">elite</field><field name="level_min">18</field><field name="level_max">22</field>
<field name="habitat">洞穴</field>
<field name="drops">["蛛丝","毒囊","暗影精华"]</field><field name="sequence">40</field>
</record>
<record id="mon_elite_scorpionking" model="yt.codex.monster">
<field name="code">scorpionking</field><field name="name">毒蝎王</field><field name="icon">🦂✨</field>
<field name="tier">elite</field><field name="level_min">16</field><field name="level_max">20</field>
<field name="habitat">沙漠</field>
<field name="drops">["鳞片","毒囊","蝎王毒针"]</field><field name="sequence">50</field>
</record>
<record id="mon_elite_frostlizard" model="yt.codex.monster">
<field name="code">frostlizard</field><field name="name">冰霜蜥蜴</field><field name="icon">🦎✨</field>
<field name="tier">elite</field><field name="level_min">20</field><field name="level_max">24</field>
<field name="habitat">特殊</field>
<field name="drops">["鳞片","水晶","寒冰核心"]</field><field name="sequence">60</field>
</record>
<record id="mon_elite_thundereagle" model="yt.codex.monster">
<field name="code">thundereagle</field><field name="name">雷霆鹰</field><field name="icon">🦅✨</field>
<field name="tier">elite</field><field name="level_min">18</field><field name="level_max">22</field>
<field name="habitat">高山</field>
<field name="drops">["羽毛","水晶","雷击木"]</field><field name="sequence">70</field>
</record>
<record id="mon_elite_spiderqueen" model="yt.codex.monster">
<field name="code">spiderqueen</field><field name="name">腐毒蛛后</field><field name="icon">🕷️✨</field>
<field name="tier">elite</field><field name="level_min">20</field><field name="level_max">26</field>
<field name="habitat">洞穴</field>
<field name="drops">["蛛丝","毒囊","蛛后腺体"]</field><field name="sequence">80</field>
</record>
<record id="mon_elite_waterlord" model="yt.codex.monster">
<field name="code">waterlord</field><field name="name">水元素之主</field><field name="icon">💧✨</field>
<field name="tier">elite</field><field name="level_min">16</field><field name="level_max">20</field>
<field name="habitat">水域</field>
<field name="drops">["露珠","鳞片","水灵珠"]</field><field name="sequence">90</field>
</record>
<record id="mon_elite_crystalguard" model="yt.codex.monster">
<field name="code">crystalguard</field><field name="name">水晶守卫</field><field name="icon">💎✨</field>
<field name="tier">elite</field><field name="level_min">22</field><field name="level_max">28</field>
<field name="habitat">洞穴</field>
<field name="drops">["水晶","矿石","纯净晶核"]</field><field name="sequence">100</field>
</record>
<!-- ===== Boss ===== -->
<record id="mon_boss_dragon" model="yt.codex.monster">
<field name="code">nidhogg</field><field name="name">远古巨龙·尼德霍格</field><field name="icon">🐉</field>
<field name="tier">boss</field><field name="level_min">50</field><field name="level_max">60</field>
<field name="habitat">世界之巅·龙巢</field>
<field name="drops">["龙鳞","龙牙","龙晶","灵气"]</field><field name="sequence">10</field>
</record>
<record id="mon_boss_kraken" model="yt.codex.monster">
<field name="code">kraken</field><field name="name">深渊之主·克拉肯</field><field name="icon">🦑</field>
<field name="tier">boss</field><field name="level_min">45</field><field name="level_max">55</field>
<field name="habitat">无尽深渊·地下海</field>
<field name="drops">["深渊触须","深渊之核","黑珍珠","灵气"]</field><field name="sequence">20</field>
</record>
<record id="mon_boss_creator" model="yt.codex.monster">
<field name="code">creator</field><field name="name">创世之灵·宇</field><field name="icon"></field>
<field name="tier">boss</field><field name="level_min">60</field><field name="level_max">60</field>
<field name="habitat">星空层·世界核心</field>
<field name="drops">["创世碎片","世界之种"]</field><field name="sequence">30</field>
</record>
</odoo>

View File

@ -1,78 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="race_human" model="yt.codex.race">
<field name="code">human</field><field name="name">人族</field><field name="icon">👤</field>
<field name="layer">大地</field>
<field name="traits">["适应力","雄心"]</field><field name="sequence">10</field>
</record>
<record id="race_elf" model="yt.codex.race">
<field name="code">elf</field><field name="name">精灵</field><field name="icon">🧝</field>
<field name="layer">森林</field>
<field name="traits">["自然共鸣","长生"]</field><field name="sequence">20</field>
</record>
<record id="race_dwarf" model="yt.codex.race">
<field name="code">dwarf</field><field name="name">矮人</field><field name="icon">⛏️</field>
<field name="layer">山脉</field>
<field name="traits">["锻造专精","坚毅"]</field><field name="sequence">30</field>
</record>
<record id="race_orc" model="yt.codex.race">
<field name="code">orc</field><field name="name">兽人</field><field name="icon">🐗</field>
<field name="layer">荒原</field>
<field name="traits">["战意","蛮力"]</field><field name="sequence">40</field>
</record>
<record id="race_demon" model="yt.codex.race">
<field name="code">demon</field><field name="name">魔族</field><field name="icon">😈</field>
<field name="layer">深渊</field>
<field name="traits">["混沌之力","契约"]</field><field name="sequence">50</field>
</record>
<record id="race_yao" model="yt.codex.race">
<field name="code">yao</field><field name="name">妖族</field><field name="icon">🦊</field>
<field name="layer">全境</field>
<field name="traits">["化形","灵觉"]</field><field name="sequence">60</field>
</record>
<record id="race_angel" model="yt.codex.race">
<field name="code">angel</field><field name="name">天使</field><field name="icon"></field>
<field name="layer">天空</field>
<field name="traits">["圣光之翼","净化"]</field><field name="sequence">70</field>
</record>
<record id="race_undead" model="yt.codex.race">
<field name="code">undead</field><field name="name">亡灵</field><field name="icon">💀</field>
<field name="layer">地下</field>
<field name="traits">["不死","恐惧免疫"]</field><field name="sequence">80</field>
</record>
<record id="race_dragon" model="yt.codex.race">
<field name="code">dragon</field><field name="name">龙族</field><field name="icon">🐉</field>
<field name="layer">全境</field>
<field name="traits">["太古之力","龙息"]</field><field name="sequence">90</field>
</record>
<record id="race_goblin" model="yt.codex.race">
<field name="code">goblin</field><field name="name">地精</field><field name="icon">👹</field>
<field name="layer">地下</field>
<field name="traits">["巧手","商业嗅觉"]</field><field name="sequence">100</field>
</record>
<record id="race_giant" model="yt.codex.race">
<field name="code">giant</field><field name="name">巨人</field><field name="icon">🦶</field>
<field name="layer">高山</field>
<field name="traits">["巨力","坚韧"]</field><field name="sequence">110</field>
</record>
<record id="race_elemental" model="yt.codex.race">
<field name="code">elemental</field><field name="name">元素灵</field><field name="icon">🌪️</field>
<field name="layer">全境</field>
<field name="traits">["元素亲和","灵体"]</field><field name="sequence">120</field>
</record>
<record id="race_mechanical" model="yt.codex.race">
<field name="code">mechanical</field><field name="name">机械</field><field name="icon">⚙️</field>
<field name="layer">遗迹</field>
<field name="traits">["太古知识","机械之躯"]</field><field name="sequence">130</field>
</record>
<record id="race_halforc" model="yt.codex.race">
<field name="code">halforc</field><field name="name">半兽人</field><field name="icon">🐾</field>
<field name="layer">边境</field>
<field name="traits">["双重血统","体能"]</field><field name="sequence">140</field>
</record>
<record id="race_naga" model="yt.codex.race">
<field name="code">naga</field><field name="name">娜迦</field><field name="icon">🐍</field>
<field name="layer">水域</field>
<field name="traits">["两栖","水元素亲和"]</field><field name="sequence">150</field>
</record>
</odoo>

View File

@ -1,356 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<!-- ===== 材料 ===== -->
<record id="res_wood" model="yt.codex.resource">
<field name="code">wood</field><field name="name">木材</field><field name="icon">🪵</field>
<field name="category">material</field><field name="quality">common</field>
<field name="source">森林地块</field><field name="uses">建造建筑、加工木板、燃料</field>
<field name="sequence">10</field>
</record>
<record id="res_stone" model="yt.codex.resource">
<field name="code">stone</field><field name="name">石材</field><field name="icon">🪨</field>
<field name="category">material</field><field name="quality">common</field>
<field name="source">山地/岩层地块</field><field name="uses">建造石质建筑、加工石料、铺路</field>
<field name="sequence">20</field>
</record>
<record id="res_ore" model="yt.codex.resource">
<field name="code">ore</field><field name="name">矿石</field><field name="icon">⛏️</field>
<field name="category">material</field><field name="quality">uncommon</field>
<field name="source">深层矿脉/地下世界</field><field name="uses">冶炼金属、锻造工具、科技升级</field>
<field name="sequence">30</field>
</record>
<record id="res_herb" model="yt.codex.resource">
<field name="code">herb</field><field name="name">草本</field><field name="icon">🌿</field>
<field name="category">material</field><field name="quality">common</field>
<field name="source">绿地/林地地块</field><field name="uses">制药、烹饪、灵气转化</field>
<field name="sequence">40</field>
</record>
<record id="res_meat" model="yt.codex.resource">
<field name="code">meat</field><field name="name"></field><field name="icon">🍖</field>
<field name="category">material</field><field name="quality">common</field>
<field name="source">狩猎/畜牧</field><field name="uses">食物消耗、烹饪加工、人口维持</field>
<field name="sequence">50</field>
</record>
<record id="res_hide" model="yt.codex.resource">
<field name="code">hide</field><field name="name">毛皮</field><field name="icon">🧶</field>
<field name="category">material</field><field name="quality">common</field>
<field name="source">畜牧/狩猎</field><field name="uses">制衣、制作、贸易</field>
<field name="sequence">60</field>
</record>
<record id="res_honey" model="yt.codex.resource">
<field name="code">honey</field><field name="name">蜂蜜</field><field name="icon">🍯</field>
<field name="category">material</field><field name="quality">uncommon</field>
<field name="source">蜂群/花海地块</field><field name="uses">烹饪、制药、贸易</field>
<field name="sequence">70</field>
</record>
<record id="res_clay" model="yt.codex.resource">
<field name="code">clay</field><field name="name">粘土</field><field name="icon">🏺</field>
<field name="category">material</field><field name="quality">common</field>
<field name="source">河岸/湿地地块</field><field name="uses">制陶、砖瓦、雕塑</field>
<field name="sequence">80</field>
</record>
<record id="res_sand" model="yt.codex.resource">
<field name="code">sand</field><field name="name">沙子</field><field name="icon"></field>
<field name="category">material</field><field name="quality">common</field>
<field name="source">沙漠/河岸地块</field><field name="uses">制玻璃、砂浆、滤水</field>
<field name="sequence">90</field>
</record>
<record id="res_vine" model="yt.codex.resource">
<field name="code">vine</field><field name="name">藤蔓</field><field name="icon">🌱</field>
<field name="category">material</field><field name="quality">common</field>
<field name="source">丛林地块</field><field name="uses">制绳索、简易桥梁、捆绑</field>
<field name="sequence">100</field>
</record>
<record id="res_fiber" model="yt.codex.resource">
<field name="code">fiber</field><field name="name">纤维</field><field name="icon">🧵</field>
<field name="category">material</field><field name="quality">common</field>
<field name="source">草甸/湿地地块</field><field name="uses">织布、造纸、绳索</field>
<field name="sequence">110</field>
</record>
<record id="res_resin" model="yt.codex.resource">
<field name="code">resin</field><field name="name">树脂</field><field name="icon">🫧</field>
<field name="category">material</field><field name="quality">uncommon</field>
<field name="source">针叶林地块</field><field name="uses">制胶、熏香、精炼油</field>
<field name="sequence">120</field>
</record>
<record id="res_bone" model="yt.codex.resource">
<field name="code">bone</field><field name="name">兽骨</field><field name="icon">🦴</field>
<field name="category">material</field><field name="quality">common</field>
<field name="source">狩猎/献祭</field><field name="uses">制骨器、装饰、肥料</field>
<field name="sequence">130</field>
</record>
<record id="res_feather" model="yt.codex.resource">
<field name="code">feather</field><field name="name">羽毛</field><field name="icon">🪶</field>
<field name="category">material</field><field name="quality">common</field>
<field name="source">鸟类/家禽养殖</field><field name="uses">制箭、书写、装饰</field>
<field name="sequence">140</field>
</record>
<record id="res_crystal" model="yt.codex.resource">
<field name="code">crystal</field><field name="name">水晶</field><field name="icon">💎</field>
<field name="category">material</field><field name="quality">rare</field>
<field name="source">岩洞深处/地下世界</field><field name="uses">能量引导、附魔、高级加工</field>
<field name="sequence">150</field>
</record>
<record id="res_mycelium" model="yt.codex.resource">
<field name="code">mycelium</field><field name="name">菌丝</field><field name="icon">🍄</field>
<field name="category">material</field><field name="quality">uncommon</field>
<field name="source">朽木/地下湿地</field><field name="uses">发酵、制药、生态转化</field>
<field name="sequence">160</field>
</record>
<record id="res_dew" model="yt.codex.resource">
<field name="code">dew</field><field name="name">露珠</field><field name="icon">💧</field>
<field name="category">material</field><field name="quality">uncommon</field>
<field name="source">清晨草地/特殊气候</field><field name="uses">制药、酿酒、灵气调和</field>
<field name="sequence">170</field>
</record>
<record id="res_scale" model="yt.codex.resource">
<field name="code">scale</field><field name="name">鳞片</field><field name="icon">🐟</field>
<field name="category">material</field><field name="quality">uncommon</field>
<field name="source">水域/水族狩猎</field><field name="uses">制甲、装饰、研磨入药</field>
<field name="sequence">180</field>
</record>
<record id="res_silk" model="yt.codex.resource">
<field name="code">silk</field><field name="name">蛛丝</field><field name="icon">🕸️</field>
<field name="category">material</field><field name="quality">rare</field>
<field name="source">洞穴深处/特殊生态</field><field name="uses">织极品布匹、弓弦、陷阱</field>
<field name="sequence">190</field>
</record>
<record id="res_horn" model="yt.codex.resource">
<field name="code">horn</field><field name="name">兽角</field><field name="icon">🦌</field>
<field name="category">material</field><field name="quality">uncommon</field>
<field name="source">狩猎大型野兽</field><field name="uses">制器、装饰、药材</field>
<field name="sequence">200</field>
</record>
<record id="res_aura" model="yt.codex.resource">
<field name="code">aura</field><field name="name">灵气</field><field name="icon"></field>
<field name="category">special</field><field name="quality">epic</field>
<field name="source">地心/星空层/文明事件</field><field name="uses">文明跃迁、特殊事件、世界规则改写</field>
<field name="sequence">210</field>
</record>
<!-- ===== 加工品 ===== -->
<record id="res_plank" model="yt.codex.resource">
<field name="code">plank</field><field name="name">木板</field><field name="icon">📦</field>
<field name="category">processed</field><field name="quality">uncommon</field>
<field name="source">加工台</field><field name="uses">精良建造、家具、机关</field>
<field name="recipe">[{"code":"wood","num":3}]</field>
<field name="sequence">10</field>
</record>
<record id="res_stoneblock" model="yt.codex.resource">
<field name="code">stoneBlock</field><field name="name">石料</field><field name="icon">🧱</field>
<field name="category">processed</field><field name="quality">uncommon</field>
<field name="source">加工台</field><field name="uses">石质建筑、城墙、纪念碑</field>
<field name="recipe">[{"code":"stone","num":2}]</field>
<field name="sequence">20</field>
</record>
<record id="res_brick" model="yt.codex.resource">
<field name="code">brick</field><field name="name">砖块</field><field name="icon">🧱</field>
<field name="category">processed</field><field name="quality">uncommon</field>
<field name="source">窑炉</field><field name="uses">建筑、道路、烟囱</field>
<field name="recipe">[{"code":"clay","num":1},{"code":"sand","num":1}]</field>
<field name="sequence">30</field>
</record>
<record id="res_metal" model="yt.codex.resource">
<field name="code">metal</field><field name="name">金属锭</field><field name="icon">🔩</field>
<field name="category">processed</field><field name="quality">rare</field>
<field name="source">冶炼炉</field><field name="uses">工具升级、武器、机械</field>
<field name="recipe">[{"code":"ore","num":3}]</field>
<field name="sequence">40</field>
</record>
<record id="res_cloth" model="yt.codex.resource">
<field name="code">cloth</field><field name="name">布匹</field><field name="icon">🧵</field>
<field name="category">processed</field><field name="quality">common</field>
<field name="source">纺织台</field><field name="uses">制衣、装饰、贸易</field>
<field name="recipe">[{"code":"fiber","num":2}]</field>
<field name="sequence">50</field>
</record>
<record id="res_driedmeat" model="yt.codex.resource">
<field name="code">driedMeat</field><field name="name">风干肉</field><field name="icon">🥩</field>
<field name="category">processed</field><field name="quality">common</field>
<field name="source">晒晾架</field><field name="uses">食物储备、远征、贸易</field>
<field name="recipe">[{"code":"meat","num":2}]</field>
<field name="sequence">60</field>
</record>
<record id="res_rope" model="yt.codex.resource">
<field name="code">rope</field><field name="name">绳索</field><field name="icon">🪢</field>
<field name="category">processed</field><field name="quality">common</field>
<field name="source">编织台</field><field name="uses">攀爬、捆绑、建筑</field>
<field name="recipe">[{"code":"vine","num":2},{"code":"fiber","num":1}]</field>
<field name="sequence">70</field>
</record>
<record id="res_paper" model="yt.codex.resource">
<field name="code">paper</field><field name="name">纸张</field><field name="icon">📄</field>
<field name="category">processed</field><field name="quality">common</field>
<field name="source">造纸坊</field><field name="uses">书写、记录、贸易</field>
<field name="recipe">[{"code":"fiber","num":2}]</field>
<field name="sequence">80</field>
</record>
<record id="res_charcoal" model="yt.codex.resource">
<field name="code">charcoal</field><field name="name">木炭</field><field name="icon">🔥</field>
<field name="category">processed</field><field name="quality">common</field>
<field name="source">炭窑</field><field name="uses">冶炼燃料、保温、滤材</field>
<field name="recipe">[{"code":"wood","num":2}]</field>
<field name="sequence">90</field>
</record>
<record id="res_glass" model="yt.codex.resource">
<field name="code">glass</field><field name="name">玻璃</field><field name="icon">🔮</field>
<field name="category">processed</field><field name="quality">uncommon</field>
<field name="source">熔炉</field><field name="uses">窗户、器皿、透镜</field>
<field name="recipe">[{"code":"sand","num":2},{"code":"charcoal","num":1}]</field>
<field name="sequence">100</field>
</record>
<record id="res_pottery" model="yt.codex.resource">
<field name="code">pottery</field><field name="name">陶器</field><field name="icon">🏺</field>
<field name="category">processed</field><field name="quality">common</field>
<field name="source">陶轮</field><field name="uses">储物、烹饪、盛水</field>
<field name="recipe">[{"code":"clay","num":2}]</field>
<field name="sequence">110</field>
</record>
<record id="res_leather" model="yt.codex.resource">
<field name="code">leather</field><field name="name">皮革</field><field name="icon">🟫</field>
<field name="category">processed</field><field name="quality">uncommon</field>
<field name="source">鞣制坊</field><field name="uses">制甲、行囊、马具</field>
<field name="recipe">[{"code":"hide","num":1},{"code":"herb","num":2}]</field>
<field name="sequence">120</field>
</record>
<record id="res_blanket" model="yt.codex.resource">
<field name="code">blanket</field><field name="name">毛毯</field><field name="icon">🧣</field>
<field name="category">processed</field><field name="quality">uncommon</field>
<field name="source">裁缝台</field><field name="uses">保暖、贸易、床品</field>
<field name="recipe">[{"code":"hide","num":2},{"code":"cloth","num":1}]</field>
<field name="sequence">130</field>
</record>
<record id="res_wine" model="yt.codex.resource">
<field name="code">wine</field><field name="name">果酒</field><field name="icon">🍷</field>
<field name="category">processed</field><field name="quality">uncommon</field>
<field name="source">酿酒坊</field><field name="uses">饮食、贸易、仪式</field>
<field name="recipe">[{"code":"honey","num":2},{"code":"dew","num":1}]</field>
<field name="sequence">140</field>
</record>
<record id="res_smokedmeat" model="yt.codex.resource">
<field name="code">smokedMeat</field><field name="name">熏肉</field><field name="icon">🍖</field>
<field name="category">processed</field><field name="quality">uncommon</field>
<field name="source">熏房</field><field name="uses">食物储备、贸易、远行</field>
<field name="recipe">[{"code":"meat","num":1},{"code":"wood","num":1}]</field>
<field name="sequence">150</field>
</record>
<record id="res_salve" model="yt.codex.resource">
<field name="code">salve</field><field name="name">草药膏</field><field name="icon">🩹</field>
<field name="category">processed</field><field name="quality">uncommon</field>
<field name="source">药臼</field><field name="uses">疗伤、解毒、护肤</field>
<field name="recipe">[{"code":"herb","num":2},{"code":"resin","num":1}]</field>
<field name="sequence">160</field>
</record>
<record id="res_oil" model="yt.codex.resource">
<field name="code">oil</field><field name="name">精炼油</field><field name="icon">🫒</field>
<field name="category">processed</field><field name="quality">rare</field>
<field name="source">蒸馏器</field><field name="uses">照明、润滑、制药</field>
<field name="recipe">[{"code":"resin","num":2},{"code":"charcoal","num":1}]</field>
<field name="sequence">170</field>
</record>
<record id="res_mortar" model="yt.codex.resource">
<field name="code">mortar</field><field name="name">砂浆</field><field name="icon">🪣</field>
<field name="category">processed</field><field name="quality">common</field>
<field name="source">搅拌池</field><field name="uses">砌墙、铺路、雕塑</field>
<field name="recipe">[{"code":"sand","num":1},{"code":"clay","num":1}]</field>
<field name="sequence">180</field>
</record>
<record id="res_steel" model="yt.codex.resource">
<field name="code">steel</field><field name="name">钢锭</field><field name="icon">⚙️</field>
<field name="category">processed</field><field name="quality">epic</field>
<field name="source">高炉</field><field name="uses">武器、装甲、高级机械</field>
<field name="recipe">[{"code":"metal","num":2},{"code":"ore","num":1}]</field>
<field name="sequence">190</field>
</record>
<record id="res_alloy" model="yt.codex.resource">
<field name="code">alloy</field><field name="name">灵晶合金</field><field name="icon">🔷</field>
<field name="category">processed</field><field name="quality">legendary</field>
<field name="source">灵炉</field><field name="uses">神器、图腾、终极装备</field>
<field name="recipe">[{"code":"steel","num":1},{"code":"crystal","num":1},{"code":"aura","num":1}]</field>
<field name="sequence">200</field>
</record>
<record id="res_potion" model="yt.codex.resource">
<field name="code">potion</field><field name="name">灵药剂</field><field name="icon">🧪</field>
<field name="category">processed</field><field name="quality">rare</field>
<field name="source">炼药台</field><field name="uses">疗伤、增益、事件触发</field>
<field name="recipe">[{"code":"herb","num":3},{"code":"aura","num":1}]</field>
<field name="sequence">210</field>
</record>
<record id="res_wallpanel" model="yt.codex.resource">
<field name="code">wallPanel</field><field name="name">木板墙</field><field name="icon">🚧</field>
<field name="category">processed</field><field name="quality">uncommon</field>
<field name="source">木工台</field><field name="uses">建筑墙体、围栏、哨塔</field>
<field name="recipe">[{"code":"plank","num":3}]</field>
<field name="sequence">220</field>
</record>
<record id="res_stonebrick" model="yt.codex.resource">
<field name="code">stoneBrick</field><field name="name">石砖</field><field name="icon">🗿</field>
<field name="category">processed</field><field name="quality">rare</field>
<field name="source">石工台</field><field name="uses">石质建筑、城堡、纪念碑</field>
<field name="recipe">[{"code":"stoneBlock","num":2}]</field>
<field name="sequence">230</field>
</record>
<record id="res_tile" model="yt.codex.resource">
<field name="code">tile</field><field name="name">瓷砖</field><field name="icon"></field>
<field name="category">processed</field><field name="quality">common</field>
<field name="source">窑炉</field><field name="uses">屋面、地面、装饰</field>
<field name="recipe">[{"code":"clay","num":1}]</field>
<field name="sequence">240</field>
</record>
<record id="res_silkcloth" model="yt.codex.resource">
<field name="code">silkCloth</field><field name="name">丝绸</field><field name="icon">🎀</field>
<field name="category">processed</field><field name="quality">rare</field>
<field name="source">精纺机</field><field name="uses">极品衣饰、贸易、图腾</field>
<field name="recipe">[{"code":"silk","num":2}]</field>
<field name="sequence">250</field>
</record>
<record id="res_dye" model="yt.codex.resource">
<field name="code">dye</field><field name="name">染料</field><field name="icon">🎨</field>
<field name="category">processed</field><field name="quality">uncommon</field>
<field name="source">染缸</field><field name="uses">染色、装饰、贸易</field>
<field name="recipe">[{"code":"herb","num":2}]</field>
<field name="sequence">260</field>
</record>
<record id="res_spice" model="yt.codex.resource">
<field name="code">spice</field><field name="name">香料</field><field name="icon">🫘</field>
<field name="category">processed</field><field name="quality">uncommon</field>
<field name="source">研钵</field><field name="uses">烹饪、贸易、仪式</field>
<field name="recipe">[{"code":"herb","num":3}]</field>
<field name="sequence">270</field>
</record>
<record id="res_candle" model="yt.codex.resource">
<field name="code">candle</field><field name="name">蜡烛</field><field name="icon">🕯️</field>
<field name="category">processed</field><field name="quality">common</field>
<field name="source">制烛台</field><field name="uses">照明、仪式、温暖</field>
<field name="recipe">[{"code":"resin","num":1},{"code":"fiber","num":1}]</field>
<field name="sequence">280</field>
</record>
<record id="res_medicatedwine" model="yt.codex.resource">
<field name="code">medicatedWine</field><field name="name">药酒</field><field name="icon">⚗️</field>
<field name="category">processed</field><field name="quality">rare</field>
<field name="source">药酒窖</field><field name="uses">疗伤、增益、御寒</field>
<field name="recipe">[{"code":"wine","num":1},{"code":"salve","num":1}]</field>
<field name="sequence">290</field>
</record>
<record id="res_runestone" model="yt.codex.resource">
<field name="code">runeStone</field><field name="name">符文石</field><field name="icon">🪨</field>
<field name="category">processed</field><field name="quality">epic</field>
<field name="source">符文工坊</field><field name="uses">附魔、仪式、地脉节点</field>
<field name="recipe">[{"code":"stoneBlock","num":1},{"code":"aura","num":1}]</field>
<field name="sequence">300</field>
</record>
<record id="res_fertilizer" model="yt.codex.resource">
<field name="code">fertilizer</field><field name="name">堆肥</field><field name="icon">🌱</field>
<field name="category">processed</field><field name="quality">common</field>
<field name="source">堆肥池</field><field name="uses">农业增产、生态修复</field>
<field name="recipe">[{"code":"bone","num":1},{"code":"herb","num":2}]</field>
<field name="sequence">310</field>
</record>
<record id="res_gear" model="yt.codex.resource">
<field name="code">gear</field><field name="name">精密齿轮</field><field name="icon">⚙️</field>
<field name="category">processed</field><field name="quality">rare</field>
<field name="source">精密工坊</field><field name="uses">机械制造、机关、自动化</field>
<field name="recipe">[{"code":"metal","num":2}]</field>
<field name="sequence">320</field>
</record>
</odoo>

View File

@ -2,4 +2,3 @@ from . import yt_world
from . import yt_world_tile from . import yt_world_tile
from . import yt_world_tile_micro from . import yt_world_tile_micro
from . import yt_resource from . import yt_resource
from . import yt_codex

View File

@ -1,84 +0,0 @@
# -*- coding: utf-8 -*-
from odoo import models, fields
class YtCodexResource(models.Model):
"""图鉴-资源(材料/加工品/特殊)"""
_name = 'yt.codex.resource'
_description = '图鉴-资源'
_order = 'category, sequence, id'
name = fields.Char(string='名称', required=True, translate=True)
code = fields.Char(string='编码', required=True, index=True)
icon = fields.Char(string='图标', default='📦')
category = fields.Selection([
('material', '材料'),
('processed', '加工品'),
('special', '特殊'),
], string='分类', required=True, default='material')
quality = fields.Selection([
('common', '普通'),
('uncommon', '优秀'),
('rare', '精良'),
('epic', '史诗'),
('legendary', '传说'),
], string='基准品质', default='common')
description = fields.Char(string='描述')
source = fields.Char(string='来源')
uses = fields.Char(string='用途')
recipe = fields.Json(string='配方') # [{"code":"wood","num":3}]
sequence = fields.Integer(string='排序', default=10)
class YtCodexCreature(models.Model):
"""图鉴-生物"""
_name = 'yt.codex.creature'
_description = '图鉴-生物'
_order = 'sequence, id'
name = fields.Char(string='名称', required=True, translate=True)
code = fields.Char(string='编码', required=True, index=True)
icon = fields.Char(string='图标', default='🐾')
habitat = fields.Char(string='栖息地')
tameable = fields.Boolean(string='可驯养', default=False)
diet = fields.Char(string='食性')
products = fields.Json(string='产物')
description = fields.Char(string='描述')
sequence = fields.Integer(string='排序', default=10)
class YtCodexMonster(models.Model):
"""图鉴-野怪/精英/Boss"""
_name = 'yt.codex.monster'
_description = '图鉴-野怪'
_order = 'tier, level_min, sequence'
name = fields.Char(string='名称', required=True, translate=True)
code = fields.Char(string='编码', required=True, index=True)
icon = fields.Char(string='图标', default='👹')
tier = fields.Selection([
('normal', '普通野怪'),
('elite', '精英野怪'),
('boss', 'Boss'),
], string='等级', required=True, default='normal')
level_min = fields.Integer(string='最小等级', default=1)
level_max = fields.Integer(string='最大等级', default=5)
habitat = fields.Char(string='栖息地')
description = fields.Char(string='描述')
drops = fields.Json(string='掉落')
sequence = fields.Integer(string='排序', default=10)
class YtCodexRace(models.Model):
"""图鉴-种族"""
_name = 'yt.codex.race'
_description = '图鉴-种族'
_order = 'sequence, id'
name = fields.Char(string='名称', required=True, translate=True)
code = fields.Char(string='编码', required=True, index=True)
icon = fields.Char(string='图标', default='🧑')
layer = fields.Char(string='所属层域')
description = fields.Char(string='描述')
traits = fields.Json(string='天赋/特性')
sequence = fields.Integer(string='排序', default=10)

View File

@ -11,11 +11,3 @@ access_yt_world_tile_micro_portal,yt.world.tile.micro portal,model_yt_world_tile
access_yt_resource_user,yt.resource user,model_yt_resource,base.group_user,1,1,1,1 access_yt_resource_user,yt.resource user,model_yt_resource,base.group_user,1,1,1,1
access_yt_resource_public,yt.resource public,model_yt_resource,base.group_public,1,1,1,1 access_yt_resource_public,yt.resource public,model_yt_resource,base.group_public,1,1,1,1
access_yt_resource_portal,yt.resource portal,model_yt_resource,base.group_portal,1,1,1,1 access_yt_resource_portal,yt.resource portal,model_yt_resource,base.group_portal,1,1,1,1
access_yt_codex_resource_user,yt.codex.resource user,model_yt_codex_resource,base.group_user,1,0,0,0
access_yt_codex_resource_public,yt.codex.resource public,model_yt_codex_resource,base.group_public,1,0,0,0
access_yt_codex_creature_user,yt.codex.creature user,model_yt_codex_creature,base.group_user,1,0,0,0
access_yt_codex_creature_public,yt.codex.creature public,model_yt_codex_creature,base.group_public,1,0,0,0
access_yt_codex_monster_user,yt.codex.monster user,model_yt_codex_monster,base.group_user,1,0,0,0
access_yt_codex_monster_public,yt.codex.monster public,model_yt_codex_monster,base.group_public,1,0,0,0
access_yt_codex_race_user,yt.codex.race user,model_yt_codex_race,base.group_user,1,0,0,0
access_yt_codex_race_public,yt.codex.race public,model_yt_codex_race,base.group_public,1,0,0,0

1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
11 access_yt_resource_user yt.resource user model_yt_resource base.group_user 1 1 1 1
12 access_yt_resource_public yt.resource public model_yt_resource base.group_public 1 1 1 1
13 access_yt_resource_portal yt.resource portal model_yt_resource base.group_portal 1 1 1 1
access_yt_codex_resource_user yt.codex.resource user model_yt_codex_resource base.group_user 1 0 0 0
access_yt_codex_resource_public yt.codex.resource public model_yt_codex_resource base.group_public 1 0 0 0
access_yt_codex_creature_user yt.codex.creature user model_yt_codex_creature base.group_user 1 0 0 0
access_yt_codex_creature_public yt.codex.creature public model_yt_codex_creature base.group_public 1 0 0 0
access_yt_codex_monster_user yt.codex.monster user model_yt_codex_monster base.group_user 1 0 0 0
access_yt_codex_monster_public yt.codex.monster public model_yt_codex_monster base.group_public 1 0 0 0
access_yt_codex_race_user yt.codex.race user model_yt_codex_race base.group_user 1 0 0 0
access_yt_codex_race_public yt.codex.race public model_yt_codex_race base.group_public 1 0 0 0

View File

@ -1,23 +1,31 @@
/* @odoo-module */ /* @odoo-module */
/** import { UserMenu } from "@web/web/user_menu/user_menu";
* Odoo 18 后台用户下拉菜单插入"官网"入口 import { patch } from "@web/core/utils/patch";
* Odoo 18 用户菜单用 registry 注册制UserMenu web.assets_frontend_lazy import { useService } from "@web/core/utils/hooks";
*/
import { registry } from "@web/core/registry";
console.log("[yt_world] user_menu.js 开始执行"); patch(UserMenu.prototype, "yt_world.UserMenu", {
setup() {
this._super(...arguments);
},
registry.category("user_menuitems").add("website", function (env) { get menuItems() {
console.log("[yt_world] 官网菜单项被注册"); const items = this._super(...arguments) || [];
return { const websiteItem = {
type: "item",
id: "website_link", id: "website_link",
description: "官网", description: "官网",
href: "/",
callback: () => { callback: () => {
window.location.href = "/"; window.location.href = "/";
}, },
sequence: 45, sequence: 5,
}; };
// Insert before "设置" (settings) and after separator-ish area
const insertIndex = items.findIndex(i => i.id === "settings");
if (insertIndex > -1) {
items.splice(insertIndex, 0, websiteItem);
} else {
items.unshift(websiteItem);
}
return items;
},
}); });
console.log("[yt_world] user_menu.js 执行完毕");

View File

@ -1,127 +0,0 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />
<title>公告 · 宇森 Yusen</title>
<link rel="stylesheet" href="assets/fonts/yusen-fonts.css" />
<link rel="icon" type="image/png" sizes="48x48" href="assets/ui/logo_planet_48.png?v=20260723">
<link rel="stylesheet" href="assets/common.css" />
<style>
.notes-wrap{max-width:1100px;width:100%;margin:0 auto;padding:28px 0;}
.notes{display:grid;grid-template-columns:repeat(auto-fill,minmax(380px,1fr));gap:20px;}
.note{display:flex;flex-direction:column;overflow:hidden;background:linear-gradient(135deg,rgba(139,92,246,.10),rgba(76,201,240,.04));border:1px solid var(--card-bd);border-left:4px solid var(--magenta);border-radius:16px;transition:.24s;}
.note:hover{transform:translateY(-3px);border-color:var(--violet);background:linear-gradient(135deg,rgba(139,92,246,.16),rgba(76,201,240,.07));box-shadow:var(--glow);}
.note.pin{border-left-color:var(--gold);background:linear-gradient(135deg,rgba(231,184,92,.12),rgba(139,92,246,.06));}
.note.pin:hover{background:linear-gradient(135deg,rgba(231,184,92,.18),rgba(139,92,248,.08));}
.note.feat{grid-column:1 / -1;flex-direction:row;align-items:stretch;}
.note.feat .note-cover{flex:0 0 44%;width:44%;min-height:260px;}
.note.feat .note-body{flex:1;justify-content:center;}
.note.feat .note-title{font-size:27px;}
.note.feat .body{font-size:16px;}
.note-cover{height:168px;flex:0 0 auto;background-size:cover;background-position:center;background-repeat:no-repeat;border-bottom:1px solid var(--card-bd);position:relative;}
.note-cover.empty{background:linear-gradient(135deg,rgba(139,92,246,.24),rgba(76,201,240,.10));}
.note-cover.empty::after{content:'封面图 · 后台配置';position:absolute;inset:0;display:flex;align-items:center;justify-content:center;font-size:12px;letter-spacing:2px;color:rgba(255,255,255,.5);}
.note-body{padding:18px 22px;display:flex;flex-direction:column;min-width:0;}
.note .date{font-size:13px;font-weight:600;color:var(--cyan);letter-spacing:1px;margin-bottom:8px;}
.note-title{font-family:"ZCOOL QingKe HuangYou",sans-serif;font-size:21px;color:#fff;letter-spacing:1px;margin-bottom:10px;line-height:1.32;text-shadow:0 0 14px rgba(139,92,246,.4);}
.note .body{font-size:15px;color:var(--text);line-height:1.9;}
.note .body p{margin:0 0 11px;}
.note .body p:last-child{margin-bottom:0;}
@media (max-width:768px){
.notes{grid-template-columns:1fr;}
.note.feat{flex-direction:column;}
.note.feat .note-cover{flex:0 0 auto;width:100%;min-height:180px;}
}
</style>
</head>
<body>
<div class="app">
<header class="topbar">
<a href="index.html" class="logo"><span class="cn">宇森</span></a>
<nav class="nav">
<a href="announce.html">公告</a>
<a href="background.html">背景</a>
<a href="rules.html">规则</a>
<a href="codex.html">图鉴</a>
<a href="story.html">剧情</a>
<a href="forum.html" class="auth-only">论坛</a>
<a href="worlddata.html" class="auth-only">世界数据</a>
</nav>
<div class="spacer"></div>
<div class="user-box" id="userBox"></div>
</header>
<main class="page-content hide-scroll">
<div class="notes-wrap">
<div class="notes" id="notesList">
<!-- 置顶特色公告 -->
<div class="note feat pin">
<div class="note-cover empty"></div>
<div class="note-body">
<div class="date">2026-07-20 · 置顶特色</div>
<div class="note-title">宇森官网第一阶段正式上线</div>
<div class="body">
<p>历经三个月打磨,宇森官网第一阶段正式上线。本次更新带来八大板块——介绍、特色、背景、规则、图鉴、论坛、公告、致玩家,全部以独立页面形式呈现,介绍页即一段概念宣传片,让你在三秒内读懂我们要做什么。</p>
<p>我们同步开放了社区论坛与图鉴共建入口。欢迎在「论坛」留下你的构想,或在「图鉴」认领尚未完成的种族条目;被采纳的内容,会带上你的署名进入正式世界观。</p>
</div>
</div>
</div>
<!-- 普通公告 -->
<div class="note pin">
<div class="note-cover empty"></div>
<div class="note-body">
<div class="date">2026-07-22</div>
<div class="note-title">图鉴系统全面升级204 种族 + 程序化矢量插画</div>
<div class="body">
<p>图鉴系统完成一次大版本升级:种族条目扩充至 204 条,系别、性格、天赋、神器与传奇事件全面扩量,装备槽位对齐「头 / 手 / 胸 / 裤 / 鞋 / 戒指(双)/ 项链 / 宝物」九宫格。</p>
</div>
</div>
</div>
<div class="note">
<div class="note-cover empty"></div>
<div class="note-body">
<div class="date">2026-07-21</div>
<div class="note-title">概念宣传片「五层世界」已发布</div>
<div class="body">
<p>这支片子将成为官网介绍页的自动背景,也会在后续对外宣发中使用。如果你喜欢这种克制的科幻基调,记得在论坛给我们反馈。</p>
</div>
</div>
</div>
<div class="note">
<div class="note-cover empty"></div>
<div class="note-body">
<div class="date">2026-07-18</div>
<div class="note-title">图鉴共建计划启动</div>
<div class="body">
<p>图鉴系统规划正式确定,分为「种族 / 建筑 / 生物 / 神器」四大类目。我们决定把相当一部分条目开放给社区共建。</p>
<p>被采纳的投稿会标注作者署名,并永久保留在官方图鉴中。第一批评审将在论坛「共建」分区进行,欢迎有想法的园丁报名。</p>
</div>
</div>
</div>
<div class="note pin">
<div class="note-cover empty"></div>
<div class="note-body">
<div class="date">2026-07-15</div>
<div class="note-title">首轮封闭内测招募开启</div>
<div class="body">
<p>宇森首轮封闭内测招募正式开启,名额 500 人。我们将优先邀请在论坛持续输出高质量内容的园丁,以及愿意参与玩法共创的核心用户。</p>
</div>
</div>
</div>
</div>
</div>
</main>
</div>
<script src="assets/game-api.js"></script>
<script src="assets/common.js"></script>
</body>
</html>

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 191 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 192 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 127 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 131 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 207 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 194 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 180 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 248 KiB

BIN
assets/bg/bg_core.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 612 KiB

BIN
assets/bg/bg_core.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 132 KiB

BIN
assets/bg/bg_earth.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 699 KiB

BIN
assets/bg/bg_earth.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 162 KiB

BIN
assets/bg/bg_sky.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 411 KiB

BIN
assets/bg/bg_sky.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 110 KiB

BIN
assets/bg/bg_star.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 319 KiB

BIN
assets/bg/bg_star.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 122 KiB

BIN
assets/bg/bg_under.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 723 KiB

BIN
assets/bg/bg_under.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 143 KiB

View File

@ -1,95 +0,0 @@
/* ============================================================
宇森官网 · 公共样式所有子页面共享
common.css CSS 变量 / 重置 / 背景 / 顶栏导航 / 用户框 / 动画 / 滚动条隐藏
============================================================ */
:root{
--bg-0:#080c1a; --bg-1:#0e1730; --bg-2:#15213f;
--violet:#8b5cf6; --magenta:#c77dff; --cyan:#4cc9f0; --gold:#e7b85c;
--text:#e9edf6; --text-dim:#9aa6c4;
--card:rgba(255,255,255,.045); --card-bd:rgba(255,255,255,.10);
--glow:0 0 22px rgba(139,92,246,.28);
}
*{box-sizing:border-box;margin:0;padding:0;}
html,body{height:100%;width:100%;}
body{
font-family:"Noto Sans SC",system-ui,sans-serif;color:var(--text);
background:
radial-gradient(1000px 620px at 82% -8%, rgba(139,92,246,.14), transparent 60%),
radial-gradient(820px 560px at 8% 112%, rgba(76,201,240,.10), transparent 55%),
linear-gradient(160deg, var(--bg-0) 0%, var(--bg-1) 48%, var(--bg-2) 100%);
position:relative;
}
body::before,body::after{content:"";position:fixed;border-radius:50%;filter:blur(80px);z-index:0;pointer-events:none;}
body::before{width:380px;height:380px;background:rgba(139,92,246,.20);top:-110px;right:-70px;animation:fl1 20s ease-in-out infinite;}
body::after{width:340px;height:340px;background:rgba(76,201,240,.12);bottom:-110px;left:-60px;animation:fl2 24s ease-in-out infinite;}
@keyframes fl1{0%,100%{transform:translate(0,0)}50%{transform:translate(-36px,36px)}}
@keyframes fl2{0%,100%{transform:translate(0,0)}50%{transform:translate(36px,-28px)}}
/* ===== 应用容器 ===== */
.app{position:relative;z-index:2;height:100vh;display:flex;flex-direction:column;}
/* ===== 顶部导航栏 ===== */
.topbar{height:64px;flex:0 0 auto;display:flex;align-items:center;gap:22px;padding:0 30px;border-bottom:1px solid var(--card-bd);background:rgba(8,12,26,.55);backdrop-filter:blur(10px);}
.logo{display:flex;align-items:baseline;gap:9px;cursor:pointer;text-decoration:none;}
.logo .cn{font-family:"ZCOOL QingKe HuangYou",sans-serif;font-size:28px;letter-spacing:4px;color:#fff;text-shadow:0 0 16px rgba(139,92,246,.55);}
.logo .en{font-family:"Press Start 2P",monospace;font-size:9px;color:var(--magenta);letter-spacing:1px;}
.nav{display:flex;gap:6px;margin-left:12px;}
.nav a{font-family:"Noto Sans SC",sans-serif;font-size:15px;font-weight:500;color:var(--text-dim);background:transparent;border:1px solid transparent;padding:8px 15px;border-radius:10px;cursor:pointer;transition:.22s;text-decoration:none;display:inline-block;}
.nav a:hover{color:var(--text);background:rgba(139,92,246,.12);}
.nav a.active{color:#fff;background:rgba(139,92,246,.22);border-color:var(--card-bd);box-shadow:var(--glow);}
.spacer{flex:1;}
/* ===== 用户信息框 ===== */
.user-box{display:flex;align-items:center;gap:10px;font-size:14px;color:var(--text);}
.user-box .ub-name{color:#fff;font-weight:500;}
.user-box .ub-logout{font-family:inherit;font-size:12px;color:var(--text-dim);background:rgba(255,255,255,.06);border:1px solid var(--card-bd);padding:5px 12px;border-radius:8px;cursor:pointer;transition:.2s;}
.user-box .ub-logout:hover{color:#fff;background:rgba(139,92,246,.18);}
.user-box .auth-link{color:var(--text-dim);text-decoration:none;font-size:14px;transition:.2s;padding:5px 8px;}
.user-box .auth-link:hover{color:#fff;}
.user-box .auth-btn{font-family:inherit;font-size:13px;color:#0b1020;background:linear-gradient(120deg,var(--gold),#f0d49a);border:none;padding:6px 16px;border-radius:20px;cursor:pointer;font-weight:600;transition:.2s;text-decoration:none;display:inline-block;}
.user-box .auth-btn:hover{filter:brightness(1.1);box-shadow:0 0 16px rgba(231,184,92,.4);}
/* 登录专属链接 */
.nav .auth-only{display:none;}
.nav .auth-only.visible{display:inline-block;}
/* ===== 页面内容区 ===== */
.page-content{flex:1;min-height:0;overflow-y:auto;padding-right:8px;}
.page-content::-webkit-scrollbar{display:none;width:0;height:0;}
.page-content{scrollbar-width:none;-ms-overflow-style:none;}
/* ===== 通用滚动条隐藏 ===== */
.hide-scroll::-webkit-scrollbar{display:none;width:0;height:0;}
.hide-scroll{scrollbar-width:none;-ms-overflow-style:none;}
/* ===== 响应式:平板 ===== */
@media (max-width:900px){
.nav{overflow-x:auto;scrollbar-width:none;}
.nav::-webkit-scrollbar{display:none;}
.nav a{padding:8px 10px;font-size:13px;}
}
/* ===== 响应式:手机/竖屏 ===== */
@media (max-width:768px){
html,body{height:100%;}
body{height:100dvh;}
.app{height:100dvh;}
.topbar{height:auto;min-height:54px;flex-wrap:wrap;align-items:center;padding:8px 14px;gap:10px;}
.logo .cn{font-size:22px;letter-spacing:2px;}
.logo .en{display:none;}
.spacer{display:none;}
.user-box{margin-left:auto;font-size:13px;gap:8px;}
.user-box .auth-btn{padding:6px 13px;font-size:12px;}
.nav{order:3;flex:1 0 100%;width:100%;margin-left:0;gap:4px;padding-top:6px;border-top:1px solid var(--card-bd);}
.nav a{font-size:13px;padding:7px 9px;white-space:nowrap;}
}
@media (max-width:480px){
.topbar{padding:8px 10px;gap:8px;}
.logo .cn{font-size:20px;}
.nav a{font-size:12px;padding:6px 7px;}
.user-box .auth-btn{padding:6px 11px;}
}

View File

@ -1,182 +0,0 @@
/* ============================================================
宇森官网 · 公共脚本所有子页面共享
common.js 导航高亮 / 登录态 / SVG 缩略图生成器
依赖assets/game-api.js需在本文件之前加载
============================================================ */
// ---------- 导航栏:根据当前 URL 自动高亮对应链接 ----------
(function(){
const links = document.querySelectorAll('.nav a');
const page = location.pathname.split('/').pop().replace(/\.html$/,'') || 'index';
links.forEach(a => {
const href = a.getAttribute('href') || '';
const target = href.replace(/\.html$/,'').replace(/^\.\//,'');
if (target === page || (page === '' && target === 'index')) {
a.classList.add('active');
}
});
})();
// ---------- 登录态 / 右上角用户信息(从 Odoo 取) ----------
(function(){
const userBox = document.getElementById('userBox');
if (!userBox) return;
// sessionStorage key'yusen:me' = JSON({uid,name,login}) 或 '' 表示已确认未登录
function saveMe(me){ try{ sessionStorage.setItem('yusen:me', me ? JSON.stringify(me) : ''); }catch(e){} }
function loadMe(){ try{ const v=sessionStorage.getItem('yusen:me'); return v===null?undefined:(v?JSON.parse(v):null); }catch(e){return undefined;} }
function renderLoggedOut(){
userBox.innerHTML = '<a href="login.html?next=world.html" class="auth-link">登录</a><a href="login.html?next=world.html" class="auth-btn">注册</a>';
document.querySelectorAll('.nav .auth-only').forEach(el => el.classList.remove('visible'));
}
function renderLoggedIn(me){
userBox.innerHTML = '<span class="ub-name"></span><button class="ub-logout" id="ubLogout">退出</button>';
const nm = userBox.querySelector('.ub-name');
if (nm) nm.textContent = me.name || me.login;
document.querySelectorAll('.nav .auth-only').forEach(el => el.classList.add('visible'));
const lb = document.getElementById('ubLogout');
if (lb) lb.addEventListener('click', async ()=>{
try { await API.logout(); } catch(e){}
saveMe(null);
sessionStorage.removeItem('yusen:me');
location.href = 'index.html';
});
}
// 1) 同步首屏:从 sessionStorage 立即渲染正确状态(消除 topbar 闪一下)
const cached = loadMe();
if (cached === undefined) {
// 第一次访问:渲染一个不闪烁的占位(保持默认空,等异步结果)
userBox.innerHTML = '<span class="ub-name" style="opacity:.5">…</span>';
} else if (cached && cached.uid) {
renderLoggedIn(cached);
} else {
renderLoggedOut();
}
// 2) 异步校验:拿真实登录态,纠正缓存
async function refreshUserBox(){
try{
const me = await API.me();
if (me && me.uid) { saveMe(me); renderLoggedIn(me); }
else { saveMe(null); renderLoggedOut(); }
}catch(e){
if (cached === undefined) renderLoggedOut(); // 首次访问 + 接口挂了才退回未登录
}
}
if (typeof API !== 'undefined') refreshUserBox();
else document.addEventListener('DOMContentLoaded', ()=>{ if (typeof API !== 'undefined') refreshUserBox(); });
})();
// ---------- 程序化矢量缩略图(图鉴/首页共用) ----------
function hashStr(s){let h=2166136261;for(let i=0;i<s.length;i++){h^=s.charCodeAt(i);h=Math.imul(h,16777619);}return h>>>0;}
function rng(seed){let s=seed>>>0;return function(){s=(Math.imul(s,1664525)+1013904223)>>>0;return s/4294967296;};}
const TIERCOL={'普通':'#9aa6c4','精良':'#5fd38a','稀有':'#4cc9f0','史诗':'#c77dff','传说':'#e7b85c'};
function starPts(cx,cy,spikes,outer,inner){
let pts=[],rot=-Math.PI/2;
for(let i=0;i<spikes*2;i++){const rad=i%2?inner:outer;const a=rot+i*Math.PI/spikes;pts.push((cx+Math.cos(a)*rad).toFixed(1)+','+(cy+Math.sin(a)*rad).toFixed(1));}
return pts.join(' ');
}
function polygonPts(cx,cy,sides,R){
let pts=[];for(let i=0;i<sides;i++){const a=-Math.PI/2+i*2*Math.PI/sides;pts.push((cx+Math.cos(a)*R).toFixed(1)+','+(cy+Math.sin(a)*R).toFixed(1));}
return pts.join(' ');
}
// 各品类图形生成器
function shRace(r,seed){
const v=Math.floor(r()*3);
let s=`<ellipse cx="50" cy="55" rx="19" ry="21" fill="url(#fg${seed})" stroke="rgba(255,255,255,.25)" stroke-width="1"/>`;
if(v===0){s+=`<path d="M35 40 L31 20 L43 36 Z" fill="url(#fg${seed})"/><path d="M65 40 L69 20 L57 36 Z" fill="url(#fg${seed})"/>`;}
else if(v===1){s+=`<path d="M32 52 L20 42 L36 46 Z" fill="url(#fg${seed})"/><path d="M68 52 L80 42 L64 46 Z" fill="url(#fg${seed})"/>`;}
else{s+=`<path d="M34 36 L42 24 L50 34 L58 24 L66 36 Z" fill="url(#fg${seed})"/>`;}
s+=`<circle cx="43" cy="55" r="2.6" fill="#0b1020"/><circle cx="57" cy="55" r="2.6" fill="#0b1020"/>`;
return s;
}
function shBuild(r,seed){
let s=`<rect x="34" y="44" width="32" height="34" rx="2" fill="url(#fg${seed})" opacity=".92"/>`;
s+=`<path d="M30 44 L50 28 L70 44 Z" fill="url(#fg${seed})"/>`;
for(let i=0;i<3;i++)s+=`<rect x="${39+i*7}" y="52" width="4" height="7" rx="1" fill="rgba(8,12,26,.7)"/>`;
return s;
}
function shElement(r,seed){
const sides=3+Math.floor(r()*6);
let s=`<polygon points="${polygonPts(50,52,sides,25)}" fill="none" stroke="url(#fg${seed})" stroke-width="3"/>`;
s+=`<circle cx="50" cy="52" r="7" fill="url(#fg${seed})"/>`;
return s;
}
function shPersona(r,seed){
let s=`<path d="M32 36 Q50 28 68 36 Q72 56 50 70 Q28 56 32 36 Z" fill="url(#fg${seed})" opacity=".9"/>`;
s+=`<circle cx="43" cy="48" r="2.8" fill="#0b1020"/><circle cx="57" cy="48" r="2.8" fill="#0b1020"/>`;
s+=`<path d="M43 60 Q50 64 57 60" stroke="#0b1020" stroke-width="2" fill="none"/>`;
return s;
}
function shTalent(r,seed){
const sp=4+Math.floor(r()*4);
let s=`<polygon points="${starPts(50,52,sp,26,11)}" fill="url(#fg${seed})" opacity=".92"/>`;
s+=`<circle cx="50" cy="52" r="5" fill="rgba(8,12,26,.55)"/>`;
return s;
}
function shRelic(r,seed){
let s=`<path d="M50 24 L70 46 L50 76 L30 46 Z" fill="url(#fg${seed})" opacity=".9" stroke="rgba(255,255,255,.3)" stroke-width="1"/>`;
s+=`<path d="M30 46 L70 46 M50 24 L42 46 L50 76 M50 24 L58 46 L50 76" stroke="rgba(8,12,26,.4)" stroke-width="1" fill="none"/>`;
return s;
}
function shEquip(sub,seed){
switch(sub){
case 'head': return `<path d="M32 56 Q32 30 50 30 Q68 30 68 56 L68 60 L32 60 Z" fill="url(#fg${seed})"/><rect x="44" y="44" width="12" height="5" rx="2" fill="rgba(8,12,26,.6)"/>`;
case 'hand': {let s=`<rect x="36" y="40" width="28" height="22" rx="6" fill="url(#fg${seed})"/><rect x="33" y="44" width="9" height="14" rx="3" fill="url(#fg${seed})"/>`;for(let i=0;i<3;i++)s+=`<rect x="${40+i*8}" y="62" width="5" height="8" rx="2" fill="url(#fg${seed})"/>`;return s;}
case 'chest': return `<path d="M30 46 Q50 38 70 46 L70 66 Q50 72 30 66 Z" fill="url(#fg${seed})"/><rect x="47" y="46" width="6" height="18" fill="rgba(8,12,26,.5)"/>`;
case 'pants': return `<path d="M38 32 L62 32 L60 74 L52 74 L50 50 L48 74 L40 74 Z" fill="url(#fg${seed})"/>`;
case 'shoes': return `<rect x="34" y="44" width="28" height="6" rx="3" fill="url(#fg${seed})"/><path d="M34 48 L60 48 L66 64 L34 64 Z" fill="url(#fg${seed})"/>`;
case 'ring': return `<circle cx="50" cy="55" r="16" fill="none" stroke="url(#fg${seed})" stroke-width="5"/><path d="M50 29 L57 40 L43 40 Z" fill="url(#fg${seed})"/>`;
case 'neck': return `<path d="M32 42 Q50 64 68 42" fill="none" stroke="url(#fg${seed})" stroke-width="3"/><path d="M44 58 L50 72 L56 58 Z" fill="url(#fg${seed})"/>`;
case 'treasure': return `<rect x="38" y="60" width="24" height="9" rx="2" fill="url(#fg${seed})"/><circle cx="50" cy="46" r="14" fill="url(#fg${seed})" opacity=".9"/><circle cx="50" cy="46" r="6" fill="rgba(255,255,255,.4)"/>`;
default: return `<circle cx="50" cy="50" r="18" fill="url(#fg${seed})"/>`;
}
}
function shEvent(r,seed){
let s=`<ellipse cx="50" cy="52" rx="22" ry="28" fill="none" stroke="url(#fg${seed})" stroke-width="3"/>`;
s+=`<ellipse cx="50" cy="52" rx="12" ry="16" fill="url(#fg${seed})" opacity=".5"/>`;
return s;
}
function shapeFor(cat,sub,r,seed){
switch(cat){
case 'race': return shRace(r,seed);
case 'build': return shBuild(r,seed);
case 'element': return shElement(r,seed);
case 'persona': return shPersona(r,seed);
case 'talent': return shTalent(r,seed);
case 'relic': return shRelic(r,seed);
case 'equip': return shEquip(sub,seed);
case 'event': return shEvent(r,seed);
default: return `<circle cx="50" cy="50" r="18" fill="url(#fg${seed})"/>`;
}
}
function thumbSVG(it){
const seed=hashStr(it.cat+it.sub+it.name);
const r=rng(seed);
const h=Math.floor(r()*360);
const h2=(h+40)%360;
const tcol=TIERCOL[it.tier]||'#9aa6c4';
const shape=shapeFor(it.cat,it.sub,r,seed);
return `<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg" preserveAspectRatio="xMidYMid slice">`+
`<defs>`+
`<radialGradient id="bg${seed}" cx="50%" cy="36%" r="78%">`+
`<stop offset="0%" stop-color="hsl(${h},48%,26%)"/>`+
`<stop offset="100%" stop-color="hsl(${h},42%,9%)"/>`+
`</radialGradient>`+
`<linearGradient id="fg${seed}" x1="0" y1="0" x2="0" y2="1">`+
`<stop offset="0%" stop-color="hsl(${h},72%,66%)"/>`+
`<stop offset="100%" stop-color="hsl(${h2},70%,56%)"/>`+
`</linearGradient>`+
`</defs>`+
`<rect width="100" height="100" fill="url(#bg${seed})"/>`+
shape+
`<circle cx="50" cy="50" r="46" fill="none" stroke="${tcol}" stroke-width="2.5" opacity=".9"/>`+
`</svg>`;
}

View File

@ -1,39 +0,0 @@
/* YuSen self-hosted fonts no external CDN dependency.
Replaces the fonts.googleapis.cn <link> in index/login/website pages. */
/* ---- Press Start 2P (pixel/retro display) ---- */
@font-face {
font-family: "Press Start 2P";
font-style: normal;
font-weight: 400;
font-display: swap;
src: url("./press-start-2p-latin-400-normal.woff2") format("woff2");
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA,
U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215,
U+FEFF, U+FFFD;
}
/* ---- ZCOOL QingKe HuangYou (CN display) ---- */
@font-face {
font-family: "ZCOOL QingKe HuangYou";
font-style: normal;
font-weight: 400;
font-display: swap;
src: url("./zcool-qingke-huangyou-chinese-simplified-400-normal.woff2") format("woff2");
}
/* ---- Noto Sans SC (CN body, 400/700) ---- */
@font-face {
font-family: "Noto Sans SC";
font-style: normal;
font-weight: 400;
font-display: swap;
src: url("./noto-sans-sc-chinese-simplified-400-normal.woff2") format("woff2");
}
@font-face {
font-family: "Noto Sans SC";
font-style: normal;
font-weight: 700;
font-display: swap;
src: url("./noto-sans-sc-chinese-simplified-700-normal.woff2") format("woff2");
}

View File

@ -136,19 +136,6 @@ const API = {
return await r.json(); return await r.json();
} catch (e) { return null; } } catch (e) { return null; }
}, },
// ---------- 图鉴codex ----------
async codexResources() {
return (await fetch(_worldUrl('/yt_world/api/codex/resources'), { credentials: _wcred })).json();
},
async codexCreatures() {
return (await fetch(_worldUrl('/yt_world/api/codex/creatures'), { credentials: _wcred })).json();
},
async codexMonsters() {
return (await fetch(_worldUrl('/yt_world/api/codex/monsters'), { credentials: _wcred })).json();
},
async codexRaces() {
return (await fetch(_worldUrl('/yt_world/api/codex/races'), { credentials: _wcred })).json();
},
// 暴露后端 base供图片等静态资源 URL 拼接本地跨域→8069线上同域→'' // 暴露后端 base供图片等静态资源 URL 拼接本地跨域→8069线上同域→''
base: API_CONFIG.base, base: API_CONFIG.base,
}; };

View File

@ -1 +0,0 @@

Binary file not shown.

Before

Width:  |  Height:  |  Size: 107 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 102 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 211 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 166 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 151 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 122 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 110 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 162 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 143 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 132 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 143 KiB

View File

@ -1,25 +0,0 @@
# 《宇森》15 秒宣传片分镜脚本
- 视频标题:宇森
- 时长15 秒
- 画幅16:9建议 1920×1080 输出)
- 风格:写实 AAA 奇幻概念美术 + 电影感镜头
- 音乐:史诗管弦乐铺垫 + 每层转换时的环境音效(星云嗡鸣、风声、流水、矿镐、岩浆爆裂)
## 分镜表
| 时间 | 画面内容 | 运镜 / 动作 | 音效 | 字幕 |
|---|---|---|---|---|
| 0:000:02 | 星空层:黑暗宇宙中,巨型空间站环缓缓旋转,星云与流星划过 | 镜头从空间站后方缓慢前推,同时微微下摇 | 低频宇宙嗡鸣、电子信号声 | (无字,只出 LOGO 空镜) |
| 0:020:04 | 天空层:云层破开,露出悬浮城堡与云城尖塔,阳光从云层缝隙射下 | 继续下坠穿过云层,镜头掠过城堡侧翼 | 风声、羽翅/空气流动感 | 无字幕,节奏过渡 |
| 0:040:07 | 大地层:森林王国全景,城墙、河流、远山、田野铺展开来 | 镜头下落到森林上空,缓慢横移展示王国规模 | 鸟鸣、流水、人声遥远的市井 | 无字幕 |
| 0:070:10 | 地下层:坠入矿坑,水晶发光,矮人熔炉与矿车轨道在岩壁间延伸 | 镜头沿矿道深入,火把与蓝色晶矿明暗交替 | 矿镐敲击、链条、滴水回响 | 无字幕 |
| 0:100:13 | 地心层:焦黑遗迹、岩浆瀑布、火晶石柱、热浪扭曲空气 | 镜头低空掠过岩浆河,向遗迹深处推进 | 岩浆爆裂、低沉轰鸣、烈焰呼啸 | 无字幕 |
| 0:130:15 | 五层世界在一瞬间叠化重合,镜头拉远成完整全景 | 快速拉远 + 轻微上摇,最后定格 | 音乐达到高潮后收束 | 屏幕中央浮现标题:宇森 |
## 制作说明
1. 每个镜头可用单张关键概念图做 subtle camera movement慢推/横移/模拟下坠),不需要完整 3D 场景。
2. 第 6 镜可直接使用宣传图/登录页背景的五层全景做收尾。
3. 全片不出现 UI、不出现图标、不出现标题以外的文字。
4. 若平台要求时长 ≥15s可在最后一镜停留至 15s 整,标题停留 2 秒。

View File

@ -1,151 +0,0 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />
<title>背景 · 宇森 Yusen</title>
<link rel="stylesheet" href="assets/fonts/yusen-fonts.css" />
<link rel="icon" type="image/png" sizes="48x48" href="assets/ui/logo_planet_48.png?v=20260723">
<link rel="stylesheet" href="assets/common.css" />
<style>
.bg-stack{max-width:1080px;width:100%;margin:0 auto;padding:36px 0;}
.bg-page{padding:36px 0;border-top:1px solid rgba(139,92,246,.16);}
.bg-page:first-child{border-top:none;padding-top:6px;}
.bg-page-head{text-align:center;margin-bottom:24px;}
.bg-page-title{position:relative;display:inline-block;font-family:"Noto Serif SC",serif;font-weight:800;font-size:40px;letter-spacing:7px;line-height:1.2;margin:0;padding-bottom:15px;
background:linear-gradient(120deg,#ffffff 0%,#f0d49a 46%,#c79bff 100%);
-webkit-background-clip:text;background-clip:text;-webkit-text-fill-color:transparent;}
.bg-page-title::after{content:'';position:absolute;left:50%;bottom:0;transform:translateX(-50%);width:70px;height:3px;border-radius:3px;
background:linear-gradient(90deg,transparent,var(--gold),var(--violet),transparent);}
.bg-frame{position:relative;min-height:160px;padding:32px 36px;border-radius:18px;overflow:hidden;
background:linear-gradient(160deg,rgba(139,92,246,.08),rgba(76,201,240,.04));
border:1px solid var(--card-bd);display:flex;align-items:center;justify-content:center;transition:.26s;}
.bg-frame::before{content:'';position:absolute;inset:0;border-radius:18px;padding:1px;
background:linear-gradient(135deg,rgba(139,92,246,.4),rgba(76,201,240,.18),transparent 60%);
-webkit-mask:linear-gradient(#000 0 0) content-box,linear-gradient(#000 0 0);-webkit-mask-composite:xor;mask-composite:exclude;pointer-events:none;}
.bg-frame:hover{border-color:rgba(139,92,246,.5);box-shadow:0 0 34px rgba(139,92,246,.18);}
.bg-frame-in{width:100%;font-size:16px;line-height:2;color:#e2e8f6;}
.bg-frame-in p{margin:0 0 16px;}
.bg-frame-in .bg-ci{display:flex;gap:12px;padding:10px 0;border-top:1px solid var(--card-bd);}
.bg-frame-in .bg-ci:first-child{border-top:none;}
.bg-frame-in .bg-ci b{color:#fff;flex:0 0 120px;}
.bg-frame-in .bg-ci span{color:var(--text-dim);}
.bg-frame-ph{font-family:"Noto Sans SC",sans-serif;font-size:14px;letter-spacing:4px;color:var(--text-dim);opacity:.65;}
/* 特色卡片网格 */
.sec-title{font-family:"ZCOOL QingKe HuangYou",sans-serif;font-size:28px;color:#fff;text-shadow:0 0 16px rgba(139,92,246,.5);margin-bottom:20px;text-align:center;}
.grid{display:grid;grid-template-columns:repeat(3,1fr);gap:16px;width:min(1060px,100%);margin:0 auto 36px;}
.card{background:var(--card);border:1px solid var(--card-bd);border-radius:14px;padding:18px 20px;transition:.26s;min-height:132px;}
.card:hover{transform:translateY(-4px);border-color:var(--violet);box-shadow:var(--glow);background:rgba(139,92,246,.12);}
.card .ic{font-size:28px;margin-bottom:8px;filter:drop-shadow(0 0 6px rgba(199,125,255,.6));}
.card h3{font-size:17px;color:#fff;margin-bottom:7px;letter-spacing:1px;}
.card p{font-size:13px;color:var(--text-dim);line-height:1.6;}
/* 致玩家 */
.letter-wrap{max-width:680px;width:100%;overflow-y:auto;padding:10px 20px;margin:0 auto;}
.letter-wrap h2{font-family:"ZCOOL QingKe HuangYou",sans-serif;font-size:30px;letter-spacing:4px;color:#fff;text-shadow:0 0 18px rgba(139,92,246,.5);margin-bottom:6px;text-align:center;}
.letter-wrap .lt-body{font-size:15px;color:var(--text-dim);line-height:2.1;}
.letter-wrap .lt-body p{margin-bottom:16px;}
.letter-wrap .lt-body p:first-child{font-size:17px;color:#fff;font-weight:500;}
.letter-wrap .lt-body .hl{color:var(--gold);font-weight:500;}
.letter-wrap .lt-sign{text-align:right;margin-top:28px;font-size:14px;color:var(--magenta);font-style:italic;}
@media (max-width:900px){ .grid{grid-template-columns:repeat(2,1fr);} }
@media (max-width:768px){
.grid{grid-template-columns:repeat(2,1fr);gap:12px;}
.card{min-height:120px;padding:15px 16px;}
.bg-page-title{font-size:30px;letter-spacing:4px;}
.bg-frame{padding:24px 20px;min-height:130px;}
.bg-frame-in{font-size:14.5px;line-height:1.85;}
}
@media (max-width:480px){ .grid{grid-template-columns:1fr;} }
</style>
</head>
<body>
<div class="app">
<header class="topbar">
<a href="index.html" class="logo"><span class="cn">宇森</span></a>
<nav class="nav">
<a href="announce.html">公告</a>
<a href="background.html">背景</a>
<a href="rules.html">规则</a>
<a href="codex.html">图鉴</a>
<a href="story.html">剧情</a>
<a href="forum.html" class="auth-only">论坛</a>
<a href="worlddata.html" class="auth-only">世界数据</a>
</nav>
<div class="spacer"></div>
<div class="user-box" id="userBox"></div>
</header>
<main class="page-content hide-scroll">
<div class="bg-stack" id="bgBody">
<!-- 由 JS 从后台 API.pages() 渲染 -->
</div>
</main>
</div>
<script src="assets/game-api.js"></script>
<script src="assets/common.js"></script>
<script>
const BG_PANELS = [
{key:'features', title:'特色', en:'FEATURES'},
{key:'intro', title:'介绍', en:'INTRODUCTION'},
{key:'background', title:'背景', en:'WORLD LORE'},
{key:'instructions', title:'说明', en:'HOW TO PLAY'},
{key:'letter', title:'致玩家', en:'A LETTER TO PLAYERS'},
];
async function renderBackground(){
const box = document.getElementById('bgBody');
if(!box) return;
const pagesMap = {};
try{
const d = await API.pages();
(d && d.items || []).forEach(p=>{ if(p.key) pagesMap[p.key] = p; });
}catch(e){}
function featuresHTML(p){
const items = (p.items || []).map(it =>
`<div class="card"><div class="ic">${it.icon || ''}</div><h3>${it.title || ''}</h3><p>${it.desc || ''}</p></div>`
).join('');
return `<div class="sec-title">${p.title || '游戏特色'}</div><div class="grid">${items}</div>`;
}
function backgroundHTML(p){
const layers = (p.items || []).map((it, idx) =>
`<div class="layer l${idx + 1}"><div class="lt">${it.title || ''}</div><div class="ld">${it.desc || ''}</div><span class="star">${it.icon || '✦'}</span></div>`
).join('');
return `<div class="layers">${layers}</div><div class="bg-text"><h2>${p.title || '世界起源'}</h2>${p.body_html || ''}</div>`;
}
function letterHTML(p){
return `<div class="letter-wrap"><h2>${p.title || ''}</h2><div class="lt-body">${p.body_html || ''}</div><div class="lt-sign">—— 宇森开发组</div></div>`;
}
const frameInner = (pg)=>{
if(pg.key === 'features') return featuresHTML(pg);
if(pg.key === 'letter') return letterHTML(pg);
if(pg.body_html) return `<div class="bg-frame-in">${pg.body_html}</div>`;
if(pg.items && pg.items.length)
return `<div class="bg-frame-in">${pg.items.map(it=>`<div class="bg-ci"><b>${it.title||''}</b><span>${it.desc||''}</span></div>`).join('')}</div>`;
return `<div class="bg-frame-ph">内容筹备中 · 即将上线</div>`;
};
box.innerHTML = BG_PANELS.map(p=>{
const pg = pagesMap[p.key] || {};
const title = pg.title || p.title;
return `
<section class="bg-page">
<div class="bg-page-head">
<h2 class="bg-page-title">${title}</h2>
</div>
<div class="bg-frame">${frameInner(pg)}</div>
</section>`;
}).join('');
}
renderBackground();
</script>
</body>
</html>

View File

@ -1,599 +0,0 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover">
<title>图鉴 · 宇森 Yusen</title>
<link rel="stylesheet" href="assets/fonts/yusen-fonts.css" />
<link rel="icon" type="image/png" sizes="48x48" href="assets/ui/logo_planet_48.png?v=20260723">
<link rel="stylesheet" href="assets/common.css" />
<script src="assets/game-api.js"></script>
<style>
/* ===== 图鉴页面专用样式(公共变量/背景/导航由 common.css 提供) ===== */
/* 动态星空背景 */
body::before{
content:'';position:fixed;inset:0;z-index:-1;
background-image:
radial-gradient(1px 1px at 10% 20%,rgba(255,255,255,.3),transparent),
radial-gradient(1px 1px at 30% 65%,rgba(255,255,255,.2),transparent),
radial-gradient(1.5px 1.5px at 55% 15%,rgba(255,255,255,.25),transparent),
radial-gradient(1px 1px at 70% 80%,rgba(255,255,255,.15),transparent),
radial-gradient(1px 1px at 85% 40%,rgba(255,255,255,.2),transparent),
radial-gradient(1.5px 1.5px at 15% 85%,rgba(255,255,255,.1),transparent),
radial-gradient(1px 1px at 45% 50%,rgba(255,255,255,.15),transparent),
radial-gradient(1px 1px at 92% 12%,rgba(255,255,255,.2),transparent);
}
/* ===== 筛选区 ===== */
.filters{
position:sticky;top:0;z-index:49;
background:#0c1120;
padding:8px 20px;border-bottom:1px solid rgba(255,255,255,.06);
box-shadow:0 1px 0 rgba(0,0,0,.4);
}
.filterRow{display:flex;align-items:center;gap:8px;flex-wrap:wrap;}
.filterRow+.filterRow{margin-top:6px;}
.filterLabel{font-size:13px;color:#7b8ea8;text-transform:uppercase;letter-spacing:1px;margin-right:4px;font-weight:700;}
/* 搜索框 */
.searchBox{
margin-left:auto;display:flex;align-items:center;gap:10px;
padding:6px 14px;border-radius:999px;font-size:13px;
background:rgba(255,255,255,.04);border:1px solid rgba(255,255,255,.1);
color:#9fb3cc;transition:.18s;min-width:180px;
}
.searchBox:focus-within{
background:rgba(139,92,246,.16);border-color:rgba(139,92,246,.5);
box-shadow:0 0 16px rgba(139,92,246,.25);
}
.searchBox input{
flex:1;min-width:0;background:transparent;border:none;outline:none;
color:#e6f0fb;font:inherit;font-size:15px;
}
.searchBox input::placeholder{color:#7b8ea8;}
.searchBox .icon{font-size:16px;opacity:.75;}
.searchBox .clear{
cursor:pointer;opacity:.5;font-size:13px;padding:0 6px;
transition:.15s;
}
.searchBox .clear:hover{opacity:1;color:#ffb4b4;}
/* 分类标签 */
.catTab{
padding:5px 14px;border-radius:999px;font-size:14px;cursor:pointer;
background:rgba(255,255,255,.04);border:1px solid rgba(255,255,255,.08);color:#9fb3cc;transition:.18s;
}
.catTab:hover{background:rgba(139,92,246,.14);color:#d0dfff;border-color:rgba(139,92,246,.3);}
.catTab.active{background:rgba(139,92,246,.2);color:#fff;border-color:rgba(139,92,246,.5);box-shadow:0 0 12px rgba(139,92,246,.2);}
/* 品质筛点 */
.ql{display:inline-flex;align-items:center;gap:6px;cursor:pointer;padding:4px 11px;border-radius:8px;transition:.15s;}
.ql:hover{background:rgba(255,255,255,.04);}
.ql .dot{width:11px;height:11px;border-radius:50%;display:inline-block;}
.ql .lbl{font-size:13px;color:#9fb3cc;}
.ql.active .lbl{color:#fff;font-weight:600;}
.ql.active .dot{box-shadow:0 0 8px currentColor;}
/* ===== 数据统计条 ===== */
/* statsBar 已移除 */
/* ===== 卡片网格 ===== */
.grid{
padding:20px 24px 40px;
display:grid;
grid-template-columns:repeat(auto-fill,minmax(160px,1fr));
gap:14px;
}
@media (min-width:900px){.grid{grid-template-columns:repeat(auto-fill,minmax(180px,1fr));}}
@media (max-width:500px){.grid{padding:14px 12px;gap:10px;grid-template-columns:repeat(auto-fill,minmax(130px,1fr));}}
/* ===== 卡片 ===== */
.card{
position:relative;border-radius:14px;cursor:pointer;user-select:none;
background:rgba(255,255,255,.03);border:1px solid rgba(255,255,255,.08);
padding:16px 12px 12px;transition:.22s;overflow:hidden;
display:flex;flex-direction:column;align-items:center;gap:6px;text-align:center;
}
.card::before{content:'';position:absolute;top:0;left:0;right:0;height:3px;border-radius:14px 14px 0 0;opacity:0;transition:.22s;}
.card:hover{transform:translateY(-3px);box-shadow:0 8px 24px rgba(0,0,0,.4);}
.card:hover::before{opacity:1;}
.card .icon{font-size:36px;line-height:1.2;margin-bottom:2px;filter:drop-shadow(0 2px 4px rgba(0,0,0,.3));}
.card .name{font-size:14px;font-weight:700;color:#e6f0fb;line-height:1.3;}
.card .sub{font-size:11px;color:#7b8ea8;line-height:1.3;}
/* 品质色条 */
.card.q-common::before{background:linear-gradient(90deg,#9ca3af,#d1d5db);}
.card.q-uncommon::before{background:linear-gradient(90deg,#22c55e,#4ade80);}
.card.q-rare::before{background:linear-gradient(90deg,#3b82f6,#60a5fa);}
.card.q-epic::before{background:linear-gradient(90deg,#a855f7,#c084fc);}
.card.q-legendary::before{background:linear-gradient(90deg,#f59e0b,#fbbf24);}
/* 品质光晕 */
.card.q-common:hover{box-shadow:0 8px 24px rgba(0,0,0,.4),0 0 20px rgba(156,163,175,.08);}
.card.q-uncommon:hover{box-shadow:0 8px 24px rgba(0,0,0,.4),0 0 20px rgba(34,197,94,.12);}
.card.q-rare:hover{box-shadow:0 8px 24px rgba(0,0,0,.4),0 0 20px rgba(59,130,246,.12);}
.card.q-epic:hover{box-shadow:0 8px 24px rgba(0,0,0,.4),0 0 20px rgba(168,85,247,.12);}
.card.q-legendary:hover{box-shadow:0 8px 24px rgba(0,0,0,.4),0 0 20px rgba(245,158,11,.12);}
/* 品质角标 */
.card .qbadge{
position:absolute;top:8px;right:8px;
font-size:9px;font-weight:700;padding:1px 6px;border-radius:6px;
text-transform:uppercase;letter-spacing:.5px;
}
.qbadge.common{background:rgba(156,163,175,.2);color:#9ca3af;border:1px solid rgba(156,163,175,.3);}
.qbadge.uncommon{background:rgba(34,197,94,.15);color:#4ade80;border:1px solid rgba(34,197,94,.3);}
.qbadge.rare{background:rgba(59,130,246,.15);color:#60a5fa;border:1px solid rgba(59,130,246,.3);}
.qbadge.epic{background:rgba(168,85,247,.15);color:#c084fc;border:1px solid rgba(168,85,247,.3);}
.qbadge.legendary{background:rgba(245,158,11,.15);color:#fbbf24;border:1px solid rgba(245,158,11,.3);}
/* 生物/野怪/种族 专属样式 */
.card.creature{border-style:solid;border-color:rgba(100,200,255,.12);}
.card.creature .icon{font-size:38px;}
.card.creature .tags{display:flex;gap:4px;flex-wrap:wrap;justify-content:center;margin-top:2px;}
.card.creature .tag{font-size:9px;padding:1px 6px;border-radius:4px;background:rgba(255,255,255,.06);color:#8899aa;}
.card.monster{border-color:rgba(168,85,247,.18);}
.card.monster .icon{font-size:38px;}
.card.monster.tier-elite{border-color:rgba(168,85,247,.35);box-shadow:0 0 14px rgba(168,85,247,.1);}
.card.monster.tier-boss{border-color:rgba(245,158,11,.4);box-shadow:0 0 18px rgba(245,158,11,.14);}
.card.monster .tags{display:flex;gap:4px;flex-wrap:wrap;justify-content:center;margin-top:2px;}
.card.monster .tag{font-size:9px;padding:1px 6px;border-radius:4px;background:rgba(255,255,255,.06);color:#8899aa;}
.card.race{border-color:rgba(100,200,255,.14);}
.card.race .icon{font-size:38px;}
.card.race .tags{display:flex;gap:4px;flex-wrap:wrap;justify-content:center;margin-top:2px;}
.card.race .tag{font-size:9px;padding:1px 6px;border-radius:4px;background:rgba(255,255,255,.06);color:#8899aa;}
/* 野怪等级角标 */
.card .tierbadge{position:absolute;top:8px;right:8px;font-size:9px;font-weight:700;padding:1px 6px;border-radius:6px;letter-spacing:.5px;}
.tierbadge.normal{background:rgba(156,163,175,.2);color:#9ca3af;border:1px solid rgba(156,163,175,.3);}
.tierbadge.elite{background:rgba(168,85,247,.18);color:#c084fc;border:1px solid rgba(168,85,247,.35);}
.tierbadge.boss{background:rgba(245,158,11,.18);color:#fbbf24;border:1px solid rgba(245,158,11,.35);}
/* ===== 空状态 ===== */
.empty{
grid-column:1/-1;text-align:center;padding:60px 20px;color:#556677;
font-size:14px;display:none;
}
.empty.show{display:block;}
/* ===== 详情弹窗 ===== */
.modalOverlay{
position:fixed;inset:0;z-index:1000;
background:rgba(4,7,15,.65);backdrop-filter:blur(6px);
display:flex;align-items:center;justify-content:center;
opacity:0;visibility:hidden;transition:.28s;padding:20px;
}
.modalOverlay.open{opacity:1;visibility:visible;}
.modalBox{
width:520px;max-width:100%;max-height:90vh;overflow-y:auto;
background:linear-gradient(180deg,rgba(17,23,42,.98),rgba(10,14,26,.98));
border:1px solid rgba(139,92,246,.35);border-radius:18px;
box-shadow:0 12px 48px rgba(0,0,0,.6);
padding:28px 28px 24px;transform:translateY(20px);transition:transform .3s ease;
}
.modalOverlay.open .modalBox{transform:translateY(0);}
.md-close{
position:absolute;top:12px;right:14px;width:28px;height:28px;border-radius:8px;
border:1px solid rgba(255,255,255,.12);background:rgba(255,255,255,.04);color:#9aa6c4;
font-size:13px;cursor:pointer;transition:.12s;
}
.md-close:hover{background:rgba(239,68,68,.2);color:#ffb4b4;}
.md-header{display:flex;align-items:center;gap:16px;margin-bottom:20px;position:relative;}
.md-icon{font-size:52px;line-height:1;}
.md-info{flex:1;}
.md-title{font-size:22px;font-weight:800;color:#eef2fb;}
.md-meta{display:flex;flex-wrap:wrap;gap:6px;margin-top:6px;}
.md-meta .mt{
font-size:11px;padding:2px 10px;border-radius:6px;
background:rgba(255,255,255,.05);border:1px solid rgba(255,255,255,.08);color:#9fb3cc;
}
.md-section{margin-top:18px;}
.md-section h3{font-size:13px;font-weight:700;color:#7b8ea8;margin-bottom:8px;text-transform:uppercase;letter-spacing:1px;}
.md-desc{font-size:14px;color:#cdd8e6;line-height:1.7;padding:10px 14px;border-radius:10px;background:rgba(255,255,255,.03);}
.md-grid{display:grid;grid-template-columns:1fr 1fr;gap:8px;}
.md-grid .gi{padding:8px 12px;border-radius:8px;background:rgba(255,255,255,.03);display:flex;justify-content:space-between;align-items:center;}
.md-grid .gi .gl{font-size:12px;color:#8899aa;}
.md-grid .gi .gv{font-size:13px;font-weight:600;color:#cdd8e6;}
/* 品质等级条 */
.qualityBar{display:flex;gap:6px;margin:12px 0;}
.qualityBar .qlv{
flex:1;height:4px;border-radius:2px;background:rgba(255,255,255,.08);
transition:.3s;
}
.qualityBar .qlv.on{background:linear-gradient(90deg,#9ca3af,#22c55e,#3b82f6,#a855f7,#f59e0b);}
.qualityBar .qlv.on+.qlv.on{background:linear-gradient(90deg,#22c55e,#3b82f6,#a855f7,#f59e0b);}
.qualityBar .qlv.on+.qlv.on+.qlv.on{background:linear-gradient(90deg,#3b82f6,#a855f7,#f59e0b);}
.qualityBar .qlv.on+.qlv.on+.qlv.on+.qlv.on{background:linear-gradient(90deg,#a855f7,#f59e0b);}
.qualityBar .qlv.on+.qlv.on+.qlv.on+.qlv.on+.qlv.on{background:#f59e0b;}
.md-recipes{margin-top:14px;display:flex;flex-direction:column;gap:8px;}
.md-recipe{
display:flex;align-items:center;gap:10px;padding:8px 12px;border-radius:10px;
background:rgba(255,255,255,.03);border:1px solid rgba(255,255,255,.06);
}
.md-recipe .rp-icon{font-size:20px;}
.md-recipe .rp-name{font-size:13px;color:#cdd8e6;font-weight:600;}
.md-recipe .rp-num{font-size:12px;color:#8899aa;margin-left:auto;}
.md-chips{margin-top:14px;display:flex;flex-wrap:wrap;gap:8px;}
.md-chip{padding:6px 12px;border-radius:8px;background:rgba(255,255,255,.04);border:1px solid rgba(255,255,255,.07);font-size:13px;color:#cdd8e6;}
</style>
</head>
<body>
<div class="app">
<header class="topbar">
<a href="index.html" class="logo"><span class="cn">宇森</span></a>
<nav class="nav">
<a href="announce.html">公告</a>
<a href="background.html">背景</a>
<a href="rules.html">规则</a>
<a href="codex.html">图鉴</a>
<a href="story.html">剧情</a>
<a href="forum.html" class="auth-only">论坛</a>
<a href="worlddata.html" class="auth-only">世界数据</a>
</nav>
<div class="spacer"></div>
<div class="user-box" id="userBox"></div>
</header>
<main class="page-content hide-scroll">
<!-- ===== 筛选区 ===== -->
<div class="filters" id="filters">
<div class="filterRow">
<span class="filterLabel">分类</span>
<span class="catTab active" data-cat="all" onclick="setCat('all')">🏛️ 全部</span>
<span class="catTab" data-cat="material" onclick="setCat('material')">🪵 材料</span>
<span class="catTab" data-cat="processed" onclick="setCat('processed')">🏭 加工品</span>
<span class="catTab" data-cat="creature" onclick="setCat('creature')">🐾 生物</span>
<span class="catTab" data-cat="monster" onclick="setCat('monster')">👹 野怪</span>
<span class="catTab" data-cat="race" onclick="setCat('race')">🌍 种族</span>
<span class="catTab" data-cat="special" onclick="setCat('special')">✨ 特殊</span>
<div class="searchBox">
<span class="icon">🔍</span>
<input id="searchInput" type="text" placeholder="搜索…" />
<span class="clear" id="searchClear" onclick="clearSearch()" title="清空" style="display:none;"></span>
</div>
</div>
<div class="filterRow">
<span class="filterLabel">品质</span>
<span class="ql active" data-ql="all" onclick="setQual('all')">
<span class="dot" style="background:linear-gradient(135deg,#9ca3af,#fbbf24);"></span>
<span class="lbl">全部</span>
</span>
<span class="ql" data-ql="common" onclick="setQual('common')">
<span class="dot" style="background:#9ca3af;"></span>
<span class="lbl">普通</span>
</span>
<span class="ql" data-ql="uncommon" onclick="setQual('uncommon')">
<span class="dot" style="background:#22c55e;"></span>
<span class="lbl">优秀</span>
</span>
<span class="ql" data-ql="rare" onclick="setQual('rare')">
<span class="dot" style="background:#3b82f6;"></span>
<span class="lbl">精良</span>
</span>
<span class="ql" data-ql="epic" onclick="setQual('epic')">
<span class="dot" style="background:#a855f7;"></span>
<span class="lbl">史诗</span>
</span>
<span class="ql" data-ql="legendary" onclick="setQual('legendary')">
<span class="dot" style="background:#f59e0b;"></span>
<span class="lbl">传说</span>
</span>
</div>
</div>
<!-- ===== 卡片网格 ===== -->
<div class="grid" id="grid"></div>
<!-- ===== 详情弹窗 ===== -->
<div class="modalOverlay" id="modalOverlay" onclick="if(event.target===this)closeModal()">
<div class="modalBox" id="modalBox" style="position:relative;"></div>
</div>
<script>
// =====================================================================
// 宇森图鉴 — 全部数据来自后端 Odoo (yt_world 模块),前端无硬编码默认数据
// =====================================================================
const QUALITIES = ['common','uncommon','rare','epic','legendary'];
const QUALITY_NAMES = { common:'普通', uncommon:'优秀', rare:'精良', epic:'史诗', legendary:'传说' };
const TIER_NAMES = { normal:'普通野怪', elite:'精英野怪', boss:'Boss' };
const CAT_ORDER = ['material','processed','creature','monster','race','special'];
let ALL_ENTRIES = []; // 归一化后的全部条目(来自后端)
let currentCat = 'all';
let currentQual = 'all';
let searchTerm = '';
// ---------- 筛选交互 ----------
function setCat(cat){
currentCat = cat;
document.querySelectorAll('.catTab').forEach(el=>el.classList.toggle('active',el.dataset.cat===cat));
renderGrid();
}
function setQual(ql){
currentQual = ql;
document.querySelectorAll('.ql').forEach(el=>el.classList.toggle('active',el.dataset.ql===ql));
renderGrid();
}
function getQualIndex(q){ return QUALITIES.indexOf(q); }
// ---------- 搜索 ----------
function setSearch(v){
searchTerm = v;
const clr = document.getElementById('searchClear');
if(clr) clr.style.display = v ? 'inline' : 'none';
renderGrid();
}
function clearSearch(){
const inp = document.getElementById('searchInput');
if(inp) inp.value = '';
setSearch('');
}
document.addEventListener('DOMContentLoaded', ()=>{
const inp = document.getElementById('searchInput');
if(inp){
inp.addEventListener('input', e=> setSearch(e.target.value));
inp.addEventListener('keydown', e=>{ if(e.key==='Escape'){ clearSearch(); inp.blur(); } });
}
});
// ---------- 后端数据归一化 ----------
function normResource(r){
return {
type:'resource', id:r.code, cat:r.category, name:r.name, icon:r.icon||'📦',
qual:r.quality||'common', source:r.source||'', uses:r.uses||'',
recipe:Array.isArray(r.recipe)?r.recipe:[], desc:r.description||''
};
}
function normCreature(c){
return {
type:'creature', id:c.code, cat:'creature', name:c.name, icon:c.icon||'🐾',
hab:c.habitat||'', tame:!!c.tameable, diet:c.diet||'',
products:Array.isArray(c.products)?c.products:[], desc:c.description||''
};
}
function normMonster(m){
return {
type:'monster', id:m.code, cat:'monster', name:m.name, icon:m.icon||'👹',
tier:m.tier||'normal', levelMin:m.level_min||1, levelMax:m.level_max||1,
hab:m.habitat||'', drops:Array.isArray(m.drops)?m.drops:[], desc:m.description||''
};
}
function normRace(r){
return {
type:'race', id:r.code, cat:'race', name:r.name, icon:r.icon||'🧑',
layer:r.layer||'', traits:Array.isArray(r.traits)?r.traits:[], desc:r.description||''
};
}
function findEntry(code){ return ALL_ENTRIES.find(x=>x.id===code); }
// ---------- 加载(核心:从后端取数) ----------
async function loadCodexData(){
const grid = document.getElementById('grid');
grid.innerHTML = '<div class="empty show">正在从服务器加载图鉴数据…</div>';
try{
const [res, cre, mon, rac] = await Promise.all([
API.codexResources(), API.codexCreatures(), API.codexMonsters(), API.codexRaces()
]);
ALL_ENTRIES = [
...(res||[]).map(normResource),
...(cre||[]).map(normCreature),
...(mon||[]).map(normMonster),
...(rac||[]).map(normRace),
];
renderGrid();
}catch(err){
console.error('[codex] 加载失败:', err);
grid.innerHTML = '<div class="empty show">⚠️ 加载失败,请确认 Odoo 后端8069已启动且 yt_world 模块已更新。</div>';
}
}
// ---------- 渲染网格 ----------
function renderGrid(){
const q = searchTerm.trim().toLowerCase();
let filtered = ALL_ENTRIES.filter(e=>{
if(currentCat!=='all' && e.cat!==currentCat) return false;
if(q){
const hay = [e.name, e.desc, (e.products||[]).join(' '), (e.drops||[]).join(' '),
(e.traits||[]).join(' '), e.hab||'', e.layer||''].join(' ');
if(!hay.toLowerCase().includes(q)) return false;
}
if(currentQual!=='all'){
if(e.type!=='resource') return false;
if(e.qual!==currentQual) return false;
}
return true;
});
filtered.sort((a,b)=>{
const ac = CAT_ORDER.indexOf(a.cat), bc = CAT_ORDER.indexOf(b.cat);
return (ac===-1?99:ac)-(bc===-1?99:bc) || (a.id||'').localeCompare(b.id||'');
});
const grid = document.getElementById('grid');
grid.innerHTML = '';
if(!filtered.length){
grid.innerHTML = '<div class="empty show">没有匹配的条目</div>';
} else {
filtered.forEach(e=>{
const div = document.createElement('div');
div.className = 'card';
if(e.type==='resource'){
div.classList.add('q-'+e.qual);
div.innerHTML = `
<span class="qbadge ${e.qual}">${QUALITY_NAMES[e.qual]}</span>
<div class="icon">${e.icon}</div>
<div class="name">${e.name}</div>
`;
} else if(e.type==='creature'){
div.classList.add('creature');
div.innerHTML = `
<div class="icon">${e.icon}</div>
<div class="name">${e.name}</div>
<div class="tags">
<span class="tag">${e.hab||''}</span>
<span class="tag">${e.diet||''}</span>
<span class="tag">${e.tame?'可驯养':'野性'}</span>
</div>
`;
} else if(e.type==='monster'){
div.classList.add('monster','tier-'+e.tier);
div.innerHTML = `
<span class="tierbadge ${e.tier}">${TIER_NAMES[e.tier]}</span>
<div class="icon">${e.icon}</div>
<div class="name">${e.name}</div>
<div class="tags">
<span class="tag">Lv ${e.levelMin}-${e.levelMax}</span>
<span class="tag">${e.hab||''}</span>
</div>
`;
} else if(e.type==='race'){
div.classList.add('race');
div.innerHTML = `
<div class="icon">${e.icon}</div>
<div class="name">${e.name}</div>
<div class="tags">
<span class="tag">${e.layer||''}</span>
</div>
`;
}
div.addEventListener('click',()=>showDetail(e));
grid.appendChild(div);
});
}
updateStats(filtered);
}
function updateStats(){}
// =====================================================================
// 详 情 弹 窗
// =====================================================================
function closeModal(){
document.getElementById('modalOverlay').classList.remove('open');
}
function showDetail(e){
const overlay = document.getElementById('modalOverlay');
const box = document.getElementById('modalBox');
const qi = e.qual ? getQualIndex(e.qual) : -1;
let html = '<div style="position:relative;">';
html += `<button class="md-close" onclick="closeModal()" style="position:absolute;top:-8px;right:-8px;"></button>`;
// 头部
html += `<div class="md-header">`;
html += `<div class="md-icon">${e.icon}</div>`;
html += `<div class="md-info"><div class="md-title">${e.name}</div><div class="md-meta">`;
if(e.type==='resource'){
const c = e.qual==='common'?'156,163,175':e.qual==='uncommon'?'34,197,94':e.qual==='rare'?'59,130,246':e.qual==='epic'?'168,85,247':'245,158,11';
const t = e.qual==='common'?'#9ca3af':e.qual==='uncommon'?'#4ade80':e.qual==='rare'?'#60a5fa':e.qual==='epic'?'#c084fc':'#fbbf24';
html += `<span class="mt" style="background:rgba(${c},.15);color:${t}">${QUALITY_NAMES[e.qual]}</span>`;
html += `<span class="mt">${e.cat==='material'?'材料':e.cat==='processed'?'加工品':'特殊'}</span>`;
} else if(e.type==='creature'){
html += `<span class="mt">🐾 生物</span><span class="mt">${e.hab||''}</span><span class="mt">${e.diet||''}</span><span class="mt">${e.tame?'✅ 可驯养':'❌ 野性'}</span>`;
} else if(e.type==='monster'){
const tc = e.tier==='boss'?'245,158,11':e.tier==='elite'?'168,85,247':'156,163,175';
const tt = e.tier==='boss'?'#fbbf24':e.tier==='elite'?'#c084fc':'#9ca3af';
html += `<span class="mt" style="background:rgba(${tc},.18);color:${tt}">${TIER_NAMES[e.tier]}</span>`;
html += `<span class="mt">Lv ${e.levelMin}-${e.levelMax}</span><span class="mt">${e.hab||''}</span>`;
} else if(e.type==='race'){
html += `<span class="mt">🌍 种族</span><span class="mt">${e.layer||''}</span>`;
}
html += `</div></div></div>`;
// 品质等级条(资源)
if(e.type==='resource'){
html += `<div class="qualityBar">`;
QUALITIES.forEach((q,i)=>{ html += `<div class="qlv ${i<=qi?'on':''}" title="${QUALITY_NAMES[q]}"></div>`; });
html += `</div>`;
}
// 描述
if(e.desc){
html += `<div class="md-section"><h3>📖 描述</h3><div class="md-desc">${e.desc}</div></div>`;
}
// 资源详情
if(e.type==='resource'){
html += `<div class="md-section"><h3>📋 属性</h3><div class="md-grid">`;
html += `<div class="gi"><span class="gl">来源</span><span class="gv">${e.source||'未知'}</span></div>`;
html += `<div class="gi"><span class="gl">用途</span><span class="gv">${e.uses||'—'}</span></div>`;
html += `</div></div>`;
if(e.recipe && e.recipe.length){
html += `<div class="md-section"><h3>🏭 加工配方</h3><div class="md-recipes">`;
e.recipe.forEach(r=>{
const src = findEntry(r.code);
html += `<div class="md-recipe"><span class="rp-icon">${src?src.icon:'❓'}</span><span class="rp-name">${src?src.name:r.code}</span><span class="rp-num">×${r.num}</span></div>`;
});
html += `</div></div>`;
}
}
// 生物详情
if(e.type==='creature'){
html += `<div class="md-section"><h3>📋 属性</h3><div class="md-grid">`;
html += `<div class="gi"><span class="gl">栖息地</span><span class="gv">${e.hab||'未知'}</span></div>`;
html += `<div class="gi"><span class="gl">食性</span><span class="gv">${e.diet||'—'}</span></div>`;
html += `<div class="gi"><span class="gl">可驯养</span><span class="gv">${e.tame?'✅ 是':'❌ 否'}</span></div>`;
html += `</div></div>`;
if(e.products && e.products.length){
html += `<div class="md-section"><h3>🥚 产物</h3><div class="md-chips">` + e.products.map(p=>`<span class="md-chip">${p}</span>`).join('') + `</div></div>`;
}
}
// 野怪详情
if(e.type==='monster'){
html += `<div class="md-section"><h3>📋 属性</h3><div class="md-grid">`;
html += `<div class="gi"><span class="gl">等级</span><span class="gv">${TIER_NAMES[e.tier]}</span></div>`;
html += `<div class="gi"><span class="gl">等级范围</span><span class="gv">Lv ${e.levelMin}-${e.levelMax}</span></div>`;
html += `<div class="gi"><span class="gl">栖息地</span><span class="gv">${e.hab||'未知'}</span></div>`;
html += `</div></div>`;
if(e.drops && e.drops.length){
html += `<div class="md-section"><h3>💎 掉落</h3><div class="md-chips">` + e.drops.map(d=>`<span class="md-chip">${d}</span>`).join('') + `</div></div>`;
}
}
// 种族详情
if(e.type==='race'){
html += `<div class="md-section"><h3>📋 属性</h3><div class="md-grid">`;
html += `<div class="gi"><span class="gl">所属层域</span><span class="gv">${e.layer||'未知'}</span></div>`;
html += `</div></div>`;
if(e.traits && e.traits.length){
html += `<div class="md-section"><h3>✨ 天赋特性</h3><div class="md-chips">` + e.traits.map(t=>`<span class="md-chip">${t}</span>`).join('') + `</div></div>`;
}
}
html += '</div>';
box.innerHTML = html;
overlay.classList.add('open');
}
// ===== 键盘关闭 =====
document.addEventListener('keydown',e=>{ if(e.key==='Escape') closeModal(); });
// ===== 初始化:从后端拉取图鉴 =====
document.addEventListener('DOMContentLoaded', loadCodexData);
</script>
<script src="assets/common.js"></script>
</main>
</div>
</body>
</html>

View File

@ -1,117 +0,0 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />
<title>论坛 · 宇森 Yusen</title>
<link rel="stylesheet" href="assets/fonts/yusen-fonts.css" />
<link rel="icon" type="image/png" sizes="48x48" href="assets/ui/logo_planet_48.png?v=20260723">
<link rel="stylesheet" href="assets/common.css" />
<style>
.forum-wrap{max-width:1080px;width:100%;margin:0 auto;padding:28px 0;display:flex;flex-direction:column;gap:20px;}
.f-cats{display:flex;flex-wrap:wrap;gap:10px;}
.f-cats .ch{font-size:14px;color:var(--text-dim);background:var(--card);border:1px solid var(--card-bd);padding:8px 15px;border-radius:11px;cursor:pointer;transition:.22s;letter-spacing:.5px;text-decoration:none;display:inline-block;}
.f-cats .ch:hover{color:#fff;background:rgba(139,92,246,.12);}
.f-cats .ch.active{color:#fff;background:linear-gradient(120deg,rgba(139,92,246,.28),rgba(199,125,255,.16));border-color:var(--magenta);box-shadow:var(--glow);}
.threads{display:grid;grid-template-columns:repeat(auto-fill,minmax(340px,1fr));gap:16px;align-content:start;}
.th{display:flex;align-items:center;gap:16px;background:var(--card);border:1px solid var(--card-bd);border-radius:14px;padding:16px 20px;transition:.22s;cursor:pointer;text-decoration:none;color:inherit;}
.th:hover{border-color:var(--violet);background:rgba(139,92,246,.12);transform:translateY(-2px);box-shadow:0 8px 24px rgba(0,0,0,.25);}
.th .pin{font-size:12px;font-weight:700;letter-spacing:1px;color:var(--gold);background:rgba(231,184,92,.16);border:1px solid rgba(231,184,92,.45);border-radius:6px;padding:2px 8px;}
.th .tt{flex:1;font-size:16.5px;color:#fff;letter-spacing:.3px;}
.th .meta{font-size:13px;color:var(--text-dim);white-space:nowrap;}
.th .rp{color:var(--cyan);font-weight:600;}
.post-form{display:none;flex-direction:column;gap:10px;background:var(--card);border:1px solid var(--magenta);border-radius:14px;padding:16px;margin-bottom:14px;}
.post-form.show{display:flex;}
.post-form input,.post-form textarea{background:rgba(8,12,26,.6);border:1px solid var(--card-bd);border-radius:10px;color:var(--text);padding:10px 14px;font-family:inherit;font-size:14px;outline:none;}
.post-form textarea{resize:vertical;min-height:72px;}
.post-form .row{display:flex;gap:10px;justify-content:flex-end;}
.post-form .row button{font-size:14px;padding:8px 18px;border-radius:10px;cursor:pointer;border:1px solid var(--card-bd);background:var(--card);color:var(--text);}
.post-form .row .ok{background:linear-gradient(120deg,var(--gold),#f0d49a);color:#0b1020;font-weight:700;border:none;}
@media (max-width:768px){ .threads{grid-template-columns:1fr;} }
</style>
</head>
<body>
<div class="app">
<header class="topbar">
<a href="index.html" class="logo"><span class="cn">宇森</span></a>
<nav class="nav">
<a href="announce.html">公告</a>
<a href="background.html">背景</a>
<a href="rules.html">规则</a>
<a href="codex.html">图鉴</a>
<a href="story.html">剧情</a>
<a href="forum.html" class="auth-only">论坛</a>
<a href="worlddata.html" class="auth-only">世界数据</a>
</nav>
<div class="spacer"></div>
<div class="user-box" id="userBox"></div>
</header>
<main class="page-content hide-scroll">
<div class="forum-wrap">
<div style="display:flex;justify-content:space-between;align-items:center;gap:16px;">
<div class="f-cats" id="fCats">
<span class="ch active" data-fcat="all">全部</span>
<span class="ch" data-fcat="lore">背景考据</span>
<span class="ch" data-fcat="discuss">综合讨论</span>
<span class="ch" data-fcat="build">共建投稿</span>
</div>
<button id="newPost" style="font-family:inherit;font-size:13px;color:#fff;background:linear-gradient(120deg,var(--violet),var(--magenta));border:none;padding:7px 18px;border-radius:20px;cursor:pointer;">+ 发帖</button>
</div>
<div class="post-form" id="postForm">
<input id="pfTitle" placeholder="标题" />
<textarea id="pfBody" placeholder="内容..."></textarea>
<div class="row">
<button id="pfCancel">取消</button>
<button id="pfOk" class="ok">发布</button>
</div>
</div>
<div class="threads" id="threads">
<div class="th" data-fcat="discuss"><span class="pin">置顶</span><span class="tt">【公告】宇森官网第一阶段上线,欢迎园丁们入驻</span><span class="meta"><span class="rp">128</span> 回复 · 官方</span></div>
<div class="th" data-fcat="lore"><span class="tt">五层世界结构考据:星空→天空→大地→地下→地心</span><span class="meta"><span class="rp">43</span> 回复 · 园丁A</span></div>
<div class="th" data-fcat="discuss"><span class="tt">关于灵气复苏机制的猜想与讨论</span><span class="meta"><span class="rp">27</span> 回复 · 某路人</span></div>
<div class="th" data-fcat="build"><span class="tt">【共建】半兽人种族条目初稿,请大家提意见</span><span class="meta"><span class="rp">19</span> 回复 · 世界构建者</span></div>
<div class="th" data-fcat="lore"><span class="tt">从凡墟到溯元:世界阶段跃迁的叙事节奏分析</span><span class="meta"><span class="rp">15</span> 回复 · 剧情组</span></div>
<div class="th" data-fcat="discuss"><span class="tt">新人报到!刚接触宇森,请问从哪里开始了解?</span><span class="meta"><span class="rp">8</span> 回复 · 新园丁</span></div>
</div>
</div>
</main>
</div>
<script src="assets/game-api.js"></script>
<script src="assets/common.js"></script>
<script>
// 分类切换
const fCats=document.querySelectorAll('.f-cats .ch');
const threads=document.querySelectorAll('#threads .th');
fCats.forEach(b=>b.addEventListener('click',()=>{
fCats.forEach(x=>x.classList.remove('active'));
b.classList.add('active');
const c=b.dataset.fcat;
threads.forEach(t=>{ t.style.display=(c==='all'||t.dataset.fcat===c)?'':'none'; });
}));
// 发帖(前端演示)
const postForm=document.getElementById('postForm');
const newPostBtn=document.getElementById('newPost');
if(newPostBtn && postForm) newPostBtn.addEventListener('click',()=>postForm.classList.toggle('show'));
document.getElementById('pfCancel').addEventListener('click',()=>postForm.classList.remove('show'));
document.getElementById('pfOk').addEventListener('click',()=>{
const t=document.getElementById('pfTitle').value.trim();
if(!t)return;
const el=document.createElement('div');
el.className='th';el.dataset.fcat='discuss';
el.innerHTML='<span class="tt">'+t+'</span><span class="meta"><span class="rp">0</span> 回复 · 我</span>';
document.getElementById('threads').prepend(el);
document.getElementById('pfTitle').value='';
document.getElementById('pfBody').value='';
postForm.classList.remove('show');
});
</script>
</body>
</html>

View File

@ -4,17 +4,13 @@
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" /> <meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />
<title>宇森 · Yusen活的文明系统模拟</title> <title>宇森 · Yusen活的文明系统模拟</title>
<link rel="stylesheet" href="assets/fonts/yusen-fonts.css" /> <link rel="preconnect" href="https://fonts.googleapis.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=Press+Start+2P&family=ZCOOL+QingKe+HuangYou&family=Noto+Sans+SC:wght@300;400;500;700&display=swap" rel="stylesheet" media="print" onload="this.media='all'" />
<noscript><link href="https://fonts.googleapis.com/css2?family=Press+Start+2P&family=ZCOOL+QingKe+HuangYou&family=Noto+Sans+SC:wght@300;400;500;700&display=swap" rel="stylesheet" /></noscript>
<link rel="icon" type="image/png" sizes="48x48" href="assets/ui/logo_planet_48.png?v=20260723"> <link rel="icon" type="image/png" sizes="48x48" href="assets/ui/logo_planet_48.png?v=20260723">
<link rel="icon" type="image/png" sizes="256x256" href="assets/ui/logo_planet_256.png?v=20260723"> <link rel="icon" type="image/png" sizes="256x256" href="assets/ui/logo_planet_256.png?v=20260723">
<link rel="apple-touch-icon" href="assets/ui/logo_planet_256.png?v=20260723"> <link rel="apple-touch-icon" href="assets/ui/logo_planet_256.png?v=20260723">
<link rel="stylesheet" href="assets/common.css" />
<!-- 首屏 hero 图预加载,提升 LCP -->
<link rel="preload" as="image" href="assets/slides/01_starry_sky.webp" fetchpriority="high">
<style> <style>
/* 首页特有:全屏 hero 轮播 */
html,body{overflow:hidden;}
.content{flex:1;display:flex;flex-direction:column;position:relative;overflow:hidden;}
:root{ :root{
--bg-0:#080c1a; --bg-1:#0e1730; --bg-2:#15213f; --bg-0:#080c1a; --bg-1:#0e1730; --bg-2:#15213f;
--violet:#8b5cf6; --magenta:#c77dff; --cyan:#4cc9f0; --gold:#e7b85c; --violet:#8b5cf6; --magenta:#c77dff; --cyan:#4cc9f0; --gold:#e7b85c;
@ -85,29 +81,17 @@
.hero .copy h1{font-family:"ZCOOL QingKe HuangYou",sans-serif;font-size:56px;letter-spacing:10px;color:#fff;text-shadow:0 2px 24px rgba(0,0,0,.9),0 0 32px rgba(139,92,246,.7);line-height:1.2;} .hero .copy h1{font-family:"ZCOOL QingKe HuangYou",sans-serif;font-size:56px;letter-spacing:10px;color:#fff;text-shadow:0 2px 24px rgba(0,0,0,.9),0 0 32px rgba(139,92,246,.7);line-height:1.2;}
.hero .copy .sub2{font-family:"Press Start 2P",monospace;font-size:11px;color:var(--gold);margin:6px 0 0;letter-spacing:2px;text-shadow:0 1px 8px rgba(0,0,0,.9);} .hero .copy .sub2{font-family:"Press Start 2P",monospace;font-size:11px;color:var(--gold);margin:6px 0 0;letter-spacing:2px;text-shadow:0 1px 8px rgba(0,0,0,.9);}
.hero .copy .tag{font-size:20px;font-weight:500;color:#e8ecf6;margin-top:4px;text-shadow:0 1px 10px rgba(0,0,0,.85);} .hero .copy .tag{font-size:20px;font-weight:500;color:#e8ecf6;margin-top:4px;text-shadow:0 1px 10px rgba(0,0,0,.85);}
/* 轮播标签 */
/* 轮播左下角特色文案 */ /* 轮播左下角特色文案 */
.slide-label{position:absolute;left:56px;bottom:72px;z-index:4;pointer-events:none;opacity:0;transform:translateY(16px);transition:opacity .8s ease, transform .8s ease;} .slide-label{position:absolute;left:56px;bottom:72px;z-index:4;pointer-events:none;opacity:0;transform:translateY(16px);transition:opacity .8s ease, transform .8s ease;}
.slide.active .slide-label{opacity:1;transform:translateY(0);} .slide.active .slide-label{opacity:1;transform:translateY(0);}
.slide-label .sl-en{font-family:"Press Start 2P",monospace;font-size:13px;color:var(--cyan);letter-spacing:3px;margin-bottom:10px;display:block;text-shadow:0 1px 8px rgba(0,0,0,.9);} .slide-label .sl-en{font-family:"Press Start 2P",monospace;font-size:13px;color:var(--cyan);letter-spacing:3px;margin-bottom:10px;display:block;text-shadow:0 1px 8px rgba(0,0,0,.9);}
.slide-label .sl-cn{font-family:"ZCOOL QingKe HuangYou",sans-serif;font-size:46px;letter-spacing:7px;color:#fff;text-shadow:0 2px 22px rgba(0,0,0,.95),0 0 30px rgba(139,92,246,.65);line-height:1.25;} .slide-label .sl-cn{font-family:"ZCOOL QingKe HuangYou",sans-serif;font-size:46px;letter-spacing:7px;color:#fff;text-shadow:0 2px 22px rgba(0,0,0,.95),0 0 30px rgba(139,92,246,.65);line-height:1.25;}
.slide-label .sl-desc{font-size:16px;color:#c8d2ec;margin-top:12px;max-width:500px;line-height:1.85;text-shadow:0 1px 8px rgba(0,0,0,.88);} .slide-label .sl-desc{font-size:16px;color:#c8d2ec;margin-top:12px;max-width:500px;line-height:1.85;text-shadow:0 1px 8px rgba(0,0,0,.88);}
/* 指示器 */
/* 轮播指示器(竖排右侧) */ /* 轮播指示器(竖排右侧) */
.sl-dots{position:absolute;right:40px;bottom:96px;top:auto;transform:none;z-index:5;display:flex;flex-direction:column;gap:14px;pointer-events:auto;} .sl-dots{position:absolute;right:40px;bottom:96px;top:auto;transform:none;z-index:5;display:flex;flex-direction:column;gap:14px;pointer-events:auto;}
.sl-dot{width:12px;height:12px;border-radius:50%;background:rgba(255,255,255,.22);border:1px solid rgba(255,255,255,.4);cursor:pointer;transition:.3s;flex:0 0 auto;} .sl-dot{width:12px;height:12px;border-radius:50%;background:rgba(255,255,255,.22);border:1px solid rgba(255,255,255,.4);cursor:pointer;transition:.3s;flex:0 0 auto;}
.sl-dot:hover{background:rgba(255,255,255,.5);} .sl-dot:hover{background:rgba(255,255,255,.5);}
.sl-dot.active{background:var(--gold);border-color:var(--gold);box-shadow:0 0 12px rgba(231,184,92,.6);transform:scale(1.15);} .sl-dot.active{background:var(--gold);border-color:var(--gold);box-shadow:0 0 12px rgba(231,184,92,.6);transform:scale(1.15);}
/* 导航箭头 */
.sl-arrow{position:absolute;top:50%;z-index:6;transform:translateY(-50%);width:44px;height:44px;border-radius:50%;border:1px solid rgba(255,255,255,.15);background:rgba(8,12,26,.55);backdrop-filter:blur(8px);color:#e8ecf6;font-size:18px;cursor:pointer;transition:.2s;display:flex;align-items:center;justify-content:center;pointer-events:auto;}
.sl-arrow:hover{background:rgba(139,92,246,.25);border-color:rgba(139,92,246,.4);box-shadow:0 0 16px rgba(139,92,246,.2);transform:translateY(-50%) scale(1.08);}
.sl-arrow.prev{left:16px;}
.sl-arrow.next{right:16px;}
/* 起源文案 */
/* 轮播右上角竖排「起源历史」 */ /* 轮播右上角竖排「起源历史」 */
.slide-origin{position:absolute;right:72px;top:48px;z-index:4;pointer-events:none;writing-mode:vertical-rl;text-orientation:upright; .slide-origin{position:absolute;right:72px;top:48px;z-index:4;pointer-events:none;writing-mode:vertical-rl;text-orientation:upright;
font-family:"Noto Sans SC",sans-serif;font-size:16px;font-weight:500;letter-spacing:3px;line-height:1.55;color:rgba(240,244,252,.94); font-family:"Noto Sans SC",sans-serif;font-size:16px;font-weight:500;letter-spacing:3px;line-height:1.55;color:rgba(240,244,252,.94);
@ -115,8 +99,6 @@
opacity:0;transform:translateY(-12px);transition:opacity .9s ease .25s, transform .9s ease .25s;} opacity:0;transform:translateY(-12px);transition:opacity .9s ease .25s, transform .9s ease .25s;}
.slide.active .slide-origin{opacity:1;transform:translateY(0);} .slide.active .slide-origin{opacity:1;transform:translateY(0);}
.slide-origin .so-tag{color:var(--gold);font-family:"ZCOOL QingKe HuangYou",sans-serif;font-size:19px;letter-spacing:5px;margin-bottom:7px;text-shadow:0 1px 4px rgba(0,0,0,.92),0 0 10px rgba(231,184,92,.45);} .slide-origin .so-tag{color:var(--gold);font-family:"ZCOOL QingKe HuangYou",sans-serif;font-size:19px;letter-spacing:5px;margin-bottom:7px;text-shadow:0 1px 4px rgba(0,0,0,.92),0 0 10px rgba(231,184,92,.45);}
/* 进入游戏按钮 */
.hero .enter-wrap{position:absolute;left:0;right:0;bottom:28px;display:flex;justify-content:center;z-index:5;} .hero .enter-wrap{position:absolute;left:0;right:0;bottom:28px;display:flex;justify-content:center;z-index:5;}
.hero .enter-btn{font-family:"Noto Sans SC",sans-serif;font-size:17px;font-weight:700;color:#0b1020;background:linear-gradient(120deg,var(--gold),#f0d49a);padding:13px 42px;border-radius:30px;cursor:pointer;box-shadow:0 0 26px rgba(231,184,92,.5);letter-spacing:3px;transition:.25s;border:none;} .hero .enter-btn{font-family:"Noto Sans SC",sans-serif;font-size:17px;font-weight:700;color:#0b1020;background:linear-gradient(120deg,var(--gold),#f0d49a);padding:13px 42px;border-radius:30px;cursor:pointer;box-shadow:0 0 26px rgba(231,184,92,.5);letter-spacing:3px;transition:.25s;border:none;}
.hero .enter-btn:hover{filter:brightness(1.1);transform:translateY(-2px);box-shadow:0 0 40px rgba(231,184,92,.75);} .hero .enter-btn:hover{filter:brightness(1.1);transform:translateY(-2px);box-shadow:0 0 40px rgba(231,184,92,.75);}
@ -341,11 +323,22 @@
.wd-rank-name .s{color:var(--gold);font-weight:700;font-size:13px;} .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-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);} .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){ @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;} .hero .copy h1{font-size:42px;}
.nav button{padding:8px 10px;font-size:13px;}
.nav{overflow-x:auto;scrollbar-width:none;}
.nav::-webkit-scrollbar{display:none;}
} }
/* ===== 移动端适配(手机 / 平板竖屏) ===== */
@media (max-width:768px){ @media (max-width:768px){
.hero .copy{left:18px;right:18px;top:28px;}
html,body{height:100%;} html,body{height:100%;}
body{height:100dvh;} body{height:100dvh;}
.app{height:100dvh;} .app{height:100dvh;}
@ -367,6 +360,22 @@
.sl-dots{right:16px;gap:12px;} .sl-dots{right:16px;gap:12px;}
.hero .enter-wrap{bottom:28px;} .hero .enter-wrap{bottom:28px;}
.hero .enter-btn{font-size:15px;padding:11px 30px;} .hero .enter-btn{font-size:15px;padding:11px 30px;}
.panel{padding:18px 18px;}
.bg-page-title{font-size:30px;}
.bg-page-body{font-size:15.5px;line-height:1.9;}
.grid{grid-template-columns:repeat(2,1fr);gap:12px;}
.card{min-height:120px;padding:15px 16px;}
.codex-search{width:140px;}
.reader-inner{padding:26px 18px 80px;}
.reader-inner h1{font-size:23px;}
#worlddata{padding:28px 16px 80px;}
#background{padding:30px 16px 80px;}
.story-toc{flex-basis:auto;}
#story{padding:18px 18px;}
.tier-row{grid-template-columns:72px 92px 1fr;gap:8px;}
.note.feat{flex-direction:column;}
.note.feat .note-cover{flex:0 0 auto;width:100%;min-height:180px;}
#announce .notes{grid-template-columns:1fr;}
} }
@media (max-width:480px){ @media (max-width:480px){
.topbar{padding:8px 10px;gap:8px;} .topbar{padding:8px 10px;gap:8px;}
@ -394,33 +403,21 @@
<body> <body>
<div class="app"> <div class="app">
<header class="topbar"> <header class="topbar">
<a href="index.html" class="logo"><span class="cn">宇森</span></a> <div class="logo" id="logoHome"><span class="cn">宇森</span></div>
<nav class="nav"> <nav class="nav">
<a href="announce.html">公告</a> <button data-tab="background">背景</button>
<a href="background.html">背景</a> <button data-tab="rules">规则</button>
<a href="rules.html">规则</a> <button data-tab="codex">图鉴</button>
<a href="codex.html">图鉴</a> <button data-tab="forum" class="auth-only">论坛</button>
<a href="story.html">剧情</a> <button data-tab="announce">公告</button>
<a href="forum.html" class="auth-only">论坛</a> <button data-tab="story">剧情</button>
<a href="worlddata.html" class="auth-only">世界数据</a> <button data-tab="worlddata" class="auth-only">世界数据</button>
</nav> </nav>
<div class="spacer"></div> <div class="spacer"></div>
<div class="user-box" id="userBox"></div> <div class="user-box" id="userBox"></div>
</header> </header>
<main class="content"> <main class="content">
<section class="hero">
<div class="slideshow" id="slideshow"></div>
<div class="copy">
<h1>宇森 &#183; Yusen</h1>
<div class="sub2">LIVING CIVILIZATION SIMULATOR</div>
<div class="tag">活的文明系统模拟</div>
</div>
<div class="enter-wrap">
<button class="enter-btn" id="enterGame">⚔ 进入游戏</button>
</div>
<button class="sl-arrow prev" onclick="window.slPrev && window.slPrev()"></button>
<button class="sl-arrow next" onclick="window.slNext && window.slNext()"></button>
<!-- ===== 介绍(视频主页) ===== --> <!-- ===== 介绍(视频主页) ===== -->
<section class="panel active" id="intro"> <section class="panel active" id="intro">
<div class="hero"> <div class="hero">
@ -506,105 +503,24 @@
</main> </main>
</div> </div>
<script src="assets/game-api.js"></script>
<script src="assets/common.js"></script>
<script> <script>
// ============ 五层世界图片轮播 ============ // top nav
(function(){ const navBtns=document.querySelectorAll('.nav button');
const slides = [ const panels=document.querySelectorAll('.panel');
{ img:'assets/slides/01_starry_sky.webp', en:'STARRY SKY', cn:'星空', desc:'陨石星空带悬浮于世界最外层,是文明触及维度的起点。星轨连通各浮空岛,穹鎏晶体自星尘中凝结。', navBtns.forEach(b=>b.addEventListener('click',()=>{
origin:'世界未形,星尘先于万物浮游于虚无。首缕维度自星隙坍缩成形,穹鎏晶体由尘凝就,文明自此仰望苍穹之始。' }, navBtns.forEach(x=>x.classList.remove('active'));
{ img:'assets/slides/02_sky.webp', en:'SKY', cn:'天空', desc:'云朵地基块托起浮空岛群,灵气复苏的第一缕风拂过。各族初代聚落,在此仰望也更在此扎根。', panels.forEach(p=>p.classList.remove('active'));
origin:'云基初结,浮空岛群自苍穹垂落凡尘。灵气复苏,初民循风而居,于云上筑城,仰望亦扎根,道统由此萌发。' }, b.classList.add('active');
{ img:'assets/slides/03_earth.webp', en:'EARTH', cn:'大地', desc:'平原与森林铺展成万物生灵的摇篮,玄荒文明于斯鼎盛。城邦林立,征伐与道统在沃土上并行。', document.getElementById(b.dataset.tab).classList.add('active');
origin:'沃野铺展为万灵摇篮,玄荒先族自林泉走出。城邦并起,征伐与道统并驰沃土之上,文明于此鼎盛绵延。' }, }));
{ img:'assets/slides/04_underground.webp',en:'UNDERGROUND',cn:'地下', desc:'溶洞晶矿与暗河在深处低语,冥玉生于幽暗玉石之中。地心工坊调度浅层资源,灯火不熄。',
origin:'溶洞深开,晶矿与暗河在幽处低语。冥玉生于幽玉深心,地心工坊取浅层之资,灯火长明,匠魂不熄。' },
{ img:'assets/slides/05_core.webp', en:'CORE', cn:'地心', desc:'熔岩奔流的地心,是世界诞生的本源。元玺在此凝聚,追溯天地初开的那一瞬永恒。',
origin:'熔岩奔流处,乃世界本源所钟。元玺于亿载炽火中凝聚成形,溯天地初开一瞬,万物归处,亦是起始。' }
];
const container = document.getElementById('slideshow');
if(!container) return;
let current = 0, timer = null;
const INTERVAL = 5500;
// —— 预加载所有图片,消除切换闪烁 —— // logo 点击回主页intro 面板)
slides.forEach(s => { const img = new Image(); img.src = s.img; }); document.getElementById('logoHome').addEventListener('click',()=>{
navBtns.forEach(x=>x.classList.remove('active'));
const track = document.createElement('div'); panels.forEach(p=>p.classList.remove('active'));
track.className='slide-track'; document.getElementById('intro').classList.add('active');
container.appendChild(track);
slides.forEach((s,i)=>{
const el = document.createElement('div');
el.className='slide'+(i===0?' active':'');
el.style.backgroundImage="url('"+s.img+"')"; // 全部直接设置(已预加载)
el.innerHTML='<div class="slide-label"><span class="sl-en">'+s.en+'</span><div class="sl-cn">'+s.cn+'</div><div class="sl-desc">'+s.desc+'</div></div>'
+'<div class="slide-origin"><span class="so-tag">起源</span>'+s.origin+'</div>';
track.appendChild(el);
});
const all = track.querySelectorAll('.slide');
const dotsWrap = document.createElement('div');
dotsWrap.className='sl-dots';
slides.forEach((_,i)=>{
const d=document.createElement('div');
d.className='sl-dot'+(i===0?' active':'');
d.addEventListener('click',()=>go(i));
dotsWrap.appendChild(d);
});
document.querySelector('.hero').appendChild(dotsWrap);
const dots = dotsWrap.querySelectorAll('.sl-dot');
function go(idx){
if(idx<0)idx=slides.length-1;if(idx>=slides.length)idx=0;
if(idx===current) return;
all[current].classList.remove('active');dots[current].classList.remove('active');
current=idx;
track.style.transform='translateY('+(-current*100)+'%)';
all[current].classList.add('active');dots[current].classList.add('active');
resetTimer();
}
function next(){ go((current+1)%slides.length); }
function prev(){ go((current-1+slides.length)%slides.length); }
function resetTimer(){ clearInterval(timer); timer=setInterval(next,INTERVAL); }
// —— 滚轮 ——
let lock=false,wAccum=0;
document.querySelector('.hero').addEventListener('wheel',function(e){
wAccum+=e.deltaY;
if(lock)return;
if(Math.abs(wAccum)>50){
lock=true;
if(wAccum>0)next();else prev();
wAccum=0;
setTimeout(function(){lock=false;},1250);
}
},{passive:true});
// —— 触屏滑动 ——
let tx=0,ty=0;
const hero = document.querySelector('.hero');
hero.addEventListener('touchstart',e=>{
const t=e.touches[0];
tx=t.clientX; ty=t.clientY;
},{passive:true});
hero.addEventListener('touchend',e=>{
const t=e.changedTouches[0];
const dx=t.clientX-tx, dy=t.clientY-ty;
if(Math.abs(dy)>Math.abs(dx) && Math.abs(dy)>50){
dy>0?prev():next();
}
},{passive:true});
// —— 键盘左右 ——
document.addEventListener('keydown',e=>{
if(e.key==='ArrowLeft')prev();
if(e.key==='ArrowRight')next();
}); });
// —— 暴露给箭头按钮 ——
window.slPrev=prev; window.slNext=next;
// ============ 五层世界图片轮播 ============ // ============ 五层世界图片轮播 ============
(function(){ (function(){
const slides = [ const slides = [
@ -686,19 +602,370 @@
function rng(seed){let s=seed>>>0;return function(){s=(Math.imul(s,1664525)+1013904223)>>>0;return s/4294967296;};} function rng(seed){let s=seed>>>0;return function(){s=(Math.imul(s,1664525)+1013904223)>>>0;return s/4294967296;};}
const TIERCOL={'普通':'#9aa6c4','精良':'#5fd38a','稀有':'#4cc9f0','史诗':'#c77dff','传说':'#e7b85c'}; const TIERCOL={'普通':'#9aa6c4','精良':'#5fd38a','稀有':'#4cc9f0','史诗':'#c77dff','传说':'#e7b85c'};
resetTimer(); function starPts(cx,cy,spikes,outer,inner){
let pts=[],rot=-Math.PI/2;
for(let i=0;i<spikes*2;i++){const rad=i%2?inner:outer;const a=rot+i*Math.PI/spikes;pts.push((cx+Math.cos(a)*rad).toFixed(1)+','+(cy+Math.sin(a)*rad).toFixed(1));}
return pts.join(' ');
}
function polygonPts(cx,cy,sides,R){
let pts=[];for(let i=0;i<sides;i++){const a=-Math.PI/2+i*2*Math.PI/sides;pts.push((cx+Math.cos(a)*R).toFixed(1)+','+(cy+Math.sin(a)*R).toFixed(1));}
return pts.join(' ');
}
// 各品类图形生成器seed 用于唯一渐变 idr 为确定性随机)
function shRace(r,seed){
const v=Math.floor(r()*3);
let s=`<ellipse cx="50" cy="55" rx="19" ry="21" fill="url(#fg${seed})" stroke="rgba(255,255,255,.25)" stroke-width="1"/>`;
if(v===0){s+=`<path d="M35 40 L31 20 L43 36 Z" fill="url(#fg${seed})"/><path d="M65 40 L69 20 L57 36 Z" fill="url(#fg${seed})"/>`;}
else if(v===1){s+=`<path d="M32 52 L20 42 L36 46 Z" fill="url(#fg${seed})"/><path d="M68 52 L80 42 L64 46 Z" fill="url(#fg${seed})"/>`;}
else{s+=`<path d="M34 36 L42 24 L50 34 L58 24 L66 36 Z" fill="url(#fg${seed})"/>`;}
s+=`<circle cx="43" cy="55" r="2.6" fill="#0b1020"/><circle cx="57" cy="55" r="2.6" fill="#0b1020"/>`;
return s;
}
function shBuild(r,seed){
let s=`<rect x="34" y="44" width="32" height="34" rx="2" fill="url(#fg${seed})" opacity=".92"/>`;
s+=`<path d="M30 44 L50 28 L70 44 Z" fill="url(#fg${seed})"/>`;
for(let i=0;i<3;i++)s+=`<rect x="${39+i*7}" y="52" width="4" height="7" rx="1" fill="rgba(8,12,26,.7)"/>`;
return s;
}
function shElement(r,seed){
const sides=3+Math.floor(r()*6);
let s=`<polygon points="${polygonPts(50,52,sides,25)}" fill="none" stroke="url(#fg${seed})" stroke-width="3"/>`;
s+=`<circle cx="50" cy="52" r="7" fill="url(#fg${seed})"/>`;
return s;
}
function shPersona(r,seed){
let s=`<path d="M32 36 Q50 28 68 36 Q72 56 50 70 Q28 56 32 36 Z" fill="url(#fg${seed})" opacity=".9"/>`;
s+=`<circle cx="43" cy="48" r="2.8" fill="#0b1020"/><circle cx="57" cy="48" r="2.8" fill="#0b1020"/>`;
s+=`<path d="M43 60 Q50 64 57 60" stroke="#0b1020" stroke-width="2" fill="none"/>`;
return s;
}
function shTalent(r,seed){
const sp=4+Math.floor(r()*4);
let s=`<polygon points="${starPts(50,52,sp,26,11)}" fill="url(#fg${seed})" opacity=".92"/>`;
s+=`<circle cx="50" cy="52" r="5" fill="rgba(8,12,26,.55)"/>`;
return s;
}
function shRelic(r,seed){
let s=`<path d="M50 24 L70 46 L50 76 L30 46 Z" fill="url(#fg${seed})" opacity=".9" stroke="rgba(255,255,255,.3)" stroke-width="1"/>`;
s+=`<path d="M30 46 L70 46 M50 24 L42 46 L50 76 M50 24 L58 46 L50 76" stroke="rgba(8,12,26,.4)" stroke-width="1" fill="none"/>`;
return s;
}
function shEquip(sub,seed){
switch(sub){
case 'head': return `<path d="M32 56 Q32 30 50 30 Q68 30 68 56 L68 60 L32 60 Z" fill="url(#fg${seed})"/><rect x="44" y="44" width="12" height="5" rx="2" fill="rgba(8,12,26,.6)"/>`;
case 'hand': {let s=`<rect x="36" y="40" width="28" height="22" rx="6" fill="url(#fg${seed})"/><rect x="33" y="44" width="9" height="14" rx="3" fill="url(#fg${seed})"/>`;for(let i=0;i<3;i++)s+=`<rect x="${40+i*8}" y="62" width="5" height="8" rx="2" fill="url(#fg${seed})"/>`;return s;}
case 'chest': return `<path d="M30 46 Q50 38 70 46 L70 66 Q50 72 30 66 Z" fill="url(#fg${seed})"/><rect x="47" y="46" width="6" height="18" fill="rgba(8,12,26,.5)"/>`;
case 'pants': return `<path d="M38 32 L62 32 L60 74 L52 74 L50 50 L48 74 L40 74 Z" fill="url(#fg${seed})"/>`;
case 'shoes': return `<rect x="34" y="44" width="28" height="6" rx="3" fill="url(#fg${seed})"/><path d="M34 48 L60 48 L66 64 L34 64 Z" fill="url(#fg${seed})"/>`;
case 'ring': return `<circle cx="50" cy="55" r="16" fill="none" stroke="url(#fg${seed})" stroke-width="5"/><path d="M50 29 L57 40 L43 40 Z" fill="url(#fg${seed})"/>`;
case 'neck': return `<path d="M32 42 Q50 64 68 42" fill="none" stroke="url(#fg${seed})" stroke-width="3"/><path d="M44 58 L50 72 L56 58 Z" fill="url(#fg${seed})"/>`;
case 'treasure': return `<rect x="38" y="60" width="24" height="9" rx="2" fill="url(#fg${seed})"/><circle cx="50" cy="46" r="14" fill="url(#fg${seed})" opacity=".9"/><circle cx="50" cy="46" r="6" fill="rgba(255,255,255,.4)"/>`;
default: return `<circle cx="50" cy="50" r="18" fill="url(#fg${seed})"/>`;
}
}
function shEvent(r,seed){
let s=`<ellipse cx="50" cy="52" rx="22" ry="28" fill="none" stroke="url(#fg${seed})" stroke-width="3"/>`;
s+=`<ellipse cx="50" cy="52" rx="12" ry="16" fill="url(#fg${seed})" opacity=".5"/>`;
return s;
}
function shapeFor(cat,sub,r,seed){
switch(cat){
case 'race': return shRace(r,seed);
case 'build': return shBuild(r,seed);
case 'element': return shElement(r,seed);
case 'persona': return shPersona(r,seed);
case 'talent': return shTalent(r,seed);
case 'relic': return shRelic(r,seed);
case 'equip': return shEquip(sub,seed);
case 'event': return shEvent(r,seed);
default: return `<circle cx="50" cy="50" r="18" fill="url(#fg${seed})"/>`;
}
}
function thumbSVG(it){
const seed=hashStr(it.cat+it.sub+it.name);
const r=rng(seed);
const h=Math.floor(r()*360);
const h2=(h+40)%360;
const tcol=TIERCOL[it.tier]||'#9aa6c4';
const shape=shapeFor(it.cat,it.sub,r,seed);
return `<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg" preserveAspectRatio="xMidYMid slice">`+
`<defs>`+
`<radialGradient id="bg${seed}" cx="50%" cy="36%" r="78%">`+
`<stop offset="0%" stop-color="hsl(${h},48%,26%)"/>`+
`<stop offset="100%" stop-color="hsl(${h},42%,9%)"/>`+
`</radialGradient>`+
`<linearGradient id="fg${seed}" x1="0" y1="0" x2="0" y2="1">`+
`<stop offset="0%" stop-color="hsl(${h},72%,66%)"/>`+
`<stop offset="100%" stop-color="hsl(${h2},70%,56%)"/>`+
`</linearGradient>`+
`</defs>`+
`<rect width="100" height="100" fill="url(#bg${seed})"/>`+
shape+
`<circle cx="50" cy="50" r="46" fill="none" stroke="${tcol}" stroke-width="2.5" opacity=".9"/>`+
`</svg>`;
}
// ============ 图鉴(数据驱动 + 程序化缩略图) ============
// ---- 数据源层 ----
// 当前: 内置框架样例 (window.CODEX)
// 将来: 从 Odoo 后台取数 (模型 game.codex.entry),经已代理的 /jsonrpc 读取
// 启用方式: 在 codex-data.js 之前设置 window.CODEX_SOURCE='odoo'
const CODEX_SOURCE = window.CODEX_SOURCE || 'odoo'; // 'embedded' | 'odoo'
async function loadCodex(){
if (CODEX_SOURCE === 'odoo') return await fetchCodexFromOdoo();
return window.CODEX;
}
// Odoo JSON-RPC 取数预留接口后台模型就绪后启用nginx 已代理 /jsonrpc -> Odoo:8069
async function fetchCodexFromOdoo(){
const r = await fetch('/game/api/codex?db=game', {credentials:'same-origin'});
const d = await r.json();
const ICON={'race':'👥','build':'🏗️','element':'🔮','persona':'🎭','talent':'⚡','relic':'💎','equip':'🛡️','event':'🌟'};
const groups = (d && d.groups) || [];
const categories = groups.map(g=>({id:g.key, name:g.label, icon:ICON[g.key]||'◆'}));
const subs={}; const items=[];
groups.forEach(g=>{(g.subs||[]).forEach(s=>{(subs[g.key]=subs[g.key]||[]).push(s.name);(s.items||[]).forEach(it=>items.push({cat:g.key, sub:it.sub, name:it.name, tier:it.tier, desc:it.desc}));});});
return { categories, subs, items };
}
(async function(){
const grid=document.getElementById('codexGrid');
const catBar=document.getElementById('codexCats');
const subBar=document.getElementById('codexSubs');
const search=document.getElementById('codexSearch');
const countEl=document.getElementById('codexCount');
const data = await loadCodex();
if(!data||!grid) return;
const {categories,subs,items}=data;
let curCat=categories[0].id, curSub='all', q='';
const iconOf=id=>(categories.find(c=>c.id===id)||{}).icon||'◆';
catBar.innerHTML=categories.map(c=>`<button data-cat="${c.id}" class="${c.id===curCat?'active':''}">${c.icon} ${c.name}</button>`).join('');
function renderSubs(){
const list=subs[curCat]||[];
subBar.innerHTML=`<button data-sub="all" class="${curSub==='all'?'active':''}">全部</button>`+
list.map(s=>`<button data-sub="${s}" class="${curSub===s?'active':''}">${s}</button>`).join('');
subBar.querySelectorAll('button').forEach(b=>b.onclick=()=>{curSub=b.dataset.sub;renderSubs();renderGrid();});
}
function renderGrid(){
let arr=items.filter(it=>it.cat===curCat&&(curSub==='all'||it.sub===curSub));
if(q) arr=arr.filter(it=>it.name.includes(q)||it.desc.includes(q));
countEl.textContent='共 '+arr.length+' 条';
grid.innerHTML=arr.map(it=>
`<div class="cx"><div class="thumb">${thumbSVG(it)}</div>`+
`<div class="body"><div class="nm">${it.name}</div>`+
`<div class="meta"><span class="sub">${it.sub}</span><span class="tier">${it.tier}</span></div>`+
`<div class="ds">${it.desc}</div></div></div>`).join('');
}
catBar.querySelectorAll('button').forEach(b=>b.onclick=()=>{
curCat=b.dataset.cat;curSub='all';
catBar.querySelectorAll('button').forEach(x=>x.classList.remove('active'));
b.classList.add('active');renderSubs();renderGrid();
});
search.oninput=e=>{q=e.target.value.trim();renderGrid();};
renderSubs();renderGrid();
})(); })();
// ============ 进入游戏 ============ // forum category
document.getElementById('enterGame').addEventListener('click', async ()=>{ const fCats=document.querySelectorAll('.f-cats .ch');
const threads=document.querySelectorAll('#threads .th');
fCats.forEach(b=>b.addEventListener('click',()=>{
fCats.forEach(x=>x.classList.remove('active'));
b.classList.add('active');
const c=b.dataset.fcat;
threads.forEach(t=>{ t.style.display=(c==='all'||t.dataset.fcat===c)?'':'none'; });
}));
// forum post (in-session demo)
const postForm=document.getElementById('postForm');
const newPostBtn=document.getElementById('newPost');
if(newPostBtn && postForm) newPostBtn.addEventListener('click',()=>postForm.classList.toggle('show'));
document.getElementById('pfCancel').addEventListener('click',()=>postForm.classList.remove('show'));
document.getElementById('pfOk').addEventListener('click',()=>{
const t=document.getElementById('pfTitle').value.trim();
if(!t)return;
const el=document.createElement('div');
el.className='th';el.dataset.fcat='discuss';
el.innerHTML='<span class="tt">'+t+'</span><span class="meta"><span class="rp">0</span> 回复 · 我</span>';
document.getElementById('threads').prepend(el);
document.getElementById('pfTitle').value='';
document.getElementById('pfBody').value='';
postForm.classList.remove('show');
});
// 登录态 / 进入游戏 逻辑见末尾 <script> game-api.js
</script>
<script src="assets/game-api.js"></script>
<script src="assets/site-app.js"></script>
<script>
// ============ 登录态 / 右上角个人信息(从 Odoo 取) ============
(function(){
const userBox = document.getElementById('userBox');
let cachedMe = null;
function renderLoggedOut(){
if(!userBox) return;
userBox.innerHTML = '<a href="login.html?next=world.html" class="auth-link">登录</a><a href="login.html?next=world.html" class="auth-btn">注册</a>';
document.querySelectorAll('.nav .auth-only').forEach(el => el.classList.remove('visible'));
}
function renderLoggedIn(me){
if(!userBox) return;
userBox.innerHTML = '<span class="ub-name"></span><button class="ub-logout" id="ubLogout">退出</button>';
const nm = userBox.querySelector('.ub-name');
if(nm) nm.textContent = me.name || me.login;
document.querySelectorAll('.nav .auth-only').forEach(el => el.classList.add('visible'));
const lb = document.getElementById('ubLogout');
if(lb) lb.addEventListener('click', async ()=>{
try { await API.logout(); } catch(e){}
location.href = 'index.html';
});
}
async function refreshUserBox(){
try{ try{
const me = await API.me(); cachedMe = await API.me();
if(cachedMe && cachedMe.uid) renderLoggedIn(cachedMe);
else renderLoggedOut();
}catch(e){ renderLoggedOut(); }
}
refreshUserBox();
// ============ 背景页:从 odoo 拉取页面板块渲染 ============
async function renderBackground(){
const box = document.getElementById('bgBody');
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:'080m',eco:'平原 / 森林 / 河流',pop:'58%',terr:'草原城邦'},
{lv:'第二层 · 丘陵',band:'80260m',eco:'梯田 / 矿脉 / 林地',pop:'22%',terr:'山道驿站'},
{lv:'第三层 · 高地',band:'260600m',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 ()=>{
try {
const me = cachedMe || await API.me();
if (me && me.uid) location.href = 'world.html'; if (me && me.uid) location.href = 'world.html';
else location.href = 'login.html?next=world.html'; else location.href = 'login.html?next=world.html';
} catch(e) { } catch(e) {
location.href = 'login.html?next=world.html'; location.href = 'login.html?next=world.html';
} }
}); });
})();
</script> </script>
</body> </body>
</html> </html>

View File

@ -25,7 +25,6 @@
body::before,body::after{display:none;} body::before,body::after{display:none;}
/* 登录页背景(固定单图 login_bg原图直出不加特效 */ /* 登录页背景(固定单图 login_bg原图直出不加特效 */
.bg-slides{position:fixed;inset:0;z-index:0;background:url('assets/slides/login_bg.webp') center/cover no-repeat;}
.bg-slides{position:fixed;inset:0;z-index:0;background:url('assets/slides/login_bg.png') center/cover no-repeat;} .bg-slides{position:fixed;inset:0;z-index:0;background:url('assets/slides/login_bg.png') center/cover no-repeat;}
.wrap{position:relative;z-index:2;width:min(420px,92vw);} .wrap{position:relative;z-index:2;width:min(420px,92vw);}

View File

@ -1,269 +0,0 @@
# -*- coding: utf-8 -*-
"""
宇森小说同步脚本
================
novel/ 下的 Markdown 源同步进 Odoo 数据库game.story.volume /
game.story.chapter供前端剧情站阅读器使用
目录约定与作者约定一致
novel/
剧情大纲.md # 顶层大纲,本脚本忽略(仅作者参考)
宇森创世正史_篇1-10.md # 每 ~10 章一个文件;文件以 `# 卷X · 名称` 开头
宇森创世正史_篇11-20.md # 同卷多文件靠相同的 `# 卷X` 标题聚合,按文件名排序拼接
...
* 章节以 `## 章标题` 分隔;章内可用 ### 小节、> 引用、--- 分隔、加粗等。
* 卷名 = 文件内 `# 卷X · 名称` 级标题;首个 `##` 之前为卷首语(intro)。
* 文件名建议零填充篇01-10便于排序未填充也能按字典序工作
同步语义全量覆盖每次运行先把库内 game.story.* 清空再按 novel/ 重建
保证网站菜单 == md 章节 id 会变化阅读器每次实时拉取无影响
用法
python novel/sync_novel.py # 写库
python novel/sync_novel.py --dry-run # 只打印将要同步的内容,不写库
python novel/sync_novel.py --novel-dir X --db Y
依赖psycopg2项目 venv 已带直接写 Postgres无需 Odoo 运行
"""
import os
import re
import argparse
import psycopg2
from datetime import datetime
# --------------------------------------------------------------------------
# Markdown -> HTML轻量覆盖小说常用语法
# --------------------------------------------------------------------------
def md_inline(text):
text = text.replace('&', '&amp;').replace('<', '&lt;').replace('>', '&gt;')
text = re.sub(r'\*\*(.+?)\*\*', r'<strong>\1</strong>', text)
text = re.sub(r'(?<!\*)\*(?!\*)(.+?)\*(?!\*)', r'<em>\1</em>', text)
text = re.sub(r'`(.+?)`', r'<code>\1</code>', text)
return text
def md_to_html(md):
lines = md.split('\n')
out = []
para = []
def flush_para():
if para:
out.append('<p>' + md_inline(' '.join(para)).replace('\n', '<br>') + '</p>')
para.clear()
i = 0
n = len(lines)
while i < n:
line = lines[i]
m = re.match(r'^(#{1,4})\s+(.*)$', line)
if m:
flush_para()
lvl = len(m.group(1))
out.append('<h{0}>{1}</h{0}>'.format(lvl, md_inline(m.group(2))))
i += 1
continue
if re.match(r'^\s*---\s*$', line) or re.match(r'^\s*\*\*\*\s*$', line):
flush_para()
out.append('<hr>')
i += 1
continue
if line.lstrip().startswith('> '):
flush_para()
quote = []
while i < n and lines[i].lstrip().startswith('> '):
quote.append(lines[i].lstrip()[2:])
i += 1
out.append('<blockquote><p>' + '<br>'.join(md_inline(q) for q in quote) + '</p></blockquote>')
continue
if re.match(r'^\s*[-*]\s+', line):
flush_para()
items = []
while i < n and re.match(r'^\s*[-*]\s+', lines[i]):
items.append('<li>' + md_inline(re.sub(r'^\s*[-*]\s+', '', lines[i])) + '</li>')
i += 1
out.append('<ul>' + ''.join(items) + '</ul>')
continue
if line.strip() == '':
flush_para()
i += 1
continue
para.append(line)
i += 1
flush_para()
return '\n'.join(out)
# --------------------------------------------------------------------------
# 解析小说源扁平结构novel/ 下若干 *.md按 # 卷X 标题分组为卷)
# 约定:
# - 每个 md 文件以 `# 卷X · 名称` 开头(卷标题),其下用 `## 章标题` 分章。
# - 一卷可拆多个文件(每 ~10 章一个,文件名如 篇1-10 / 篇11-20
# 同卷文件靠相同的 `# 卷X` 标题聚合,按文件名排序拼接章节。
# - 首个 ## 之前的内容(去掉 # 卷标题行)作为卷首语(intro)。
# - 顶层 剧情大纲.md 仅作作者参考,忽略。
# --------------------------------------------------------------------------
def parse_file(md):
"""解析单个 md 文件 -> (vol_title|None, intro_html, [(ch_title, ch_body_html), ...])"""
segments = re.split(r'^##\s+', md, flags=re.M)
pre = segments[0]
m = re.search(r'^#\s+(.*)$', pre, re.M)
vol_title = m.group(1).strip() if m else None
intro_md = re.sub(r'^#\s+.*$', '', pre, flags=re.M).strip()
intro_html = md_to_html(intro_md) if intro_md else ''
chapters = []
for seg in segments[1:]:
nl = seg.find('\n')
if nl == -1:
title, body = seg.strip(), ''
else:
title = seg[:nl].strip()
body = seg[nl + 1:].strip()
chapters.append((title, md_to_html(body)))
return vol_title, intro_html, chapters
def collect_volumes(novel_dir):
# 收集源文件:递归扫描 *.md排除 剧情大纲.md 与隐藏/缓存目录
files = []
for root, dirs, fnames in os.walk(novel_dir):
dirs[:] = [d for d in dirs if not d.startswith('.') and not d.startswith('__')]
for fn in fnames:
if not fn.lower().endswith('.md') or fn.startswith('.'):
continue
if fn == '剧情大纲.md':
continue
files.append(os.path.join(root, fn))
# 按文件名(建议零填充命名)排序,保证 篇1-10 < 篇11-20
files.sort(key=lambda p: os.path.basename(p))
volumes = {} # title -> {'intro':, 'chapters': [], 'order': int}
order = []
current = None
for fp in files:
with open(fp, encoding='utf-8') as fh:
md = fh.read()
vol_title, intro_html, chapters = parse_file(md)
if vol_title:
current = vol_title
if current is None:
# 跳过没有卷标题归属的文件
continue
if current not in volumes:
volumes[current] = {'intro': intro_html, 'chapters': [], 'order': len(order)}
order.append(current)
elif intro_html and not volumes[current]['intro']:
volumes[current]['intro'] = intro_html
volumes[current]['chapters'].extend(chapters)
out = []
for title in order:
v = volumes[title]
out.append((title, v['intro'], v['chapters'], title))
return out
# --------------------------------------------------------------------------
# 写库
# --------------------------------------------------------------------------
def sync(conn, volumes, dry_run=False):
print('\n========== 同步预览 ==========')
total_ch = 0
for vname, intro, chapters, folder in volumes:
print('卷:{0} (源 {1}{2} 章)'.format(vname, folder, len(chapters)))
for i, (t, _) in enumerate(chapters, 1):
print(' {0:>2}. {1}'.format(i, t))
total_ch += len(chapters)
print('--------------------------------')
print('{0} 卷 / {1}'.format(len(volumes), total_ch))
if dry_run:
print('[dry-run] 未写入数据库。')
return
cur = conn.cursor()
stats = {'vol_create': 0, 'vol_update': 0, 'ch_create': 0, 'ch_update': 0, 'ch_delete': 0}
seq_v = 10
for vname, intro, chapters, _folder in volumes:
# ---- 卷:按名称 upsert保持 id 稳定)----
cur.execute('SELECT id FROM game_story_volume WHERE name=%s', (vname,))
row = cur.fetchone()
if row:
vid = row[0]
cur.execute(
'UPDATE game_story_volume SET sequence=%s, intro=%s, write_date=now() WHERE id=%s',
(seq_v, intro, vid))
stats['vol_update'] += 1
else:
cur.execute(
"""INSERT INTO game_story_volume
(name, sequence, intro, create_uid, write_uid, create_date, write_date)
VALUES (%s,%s,%s,1,1,now(),now()) RETURNING id""",
(vname, seq_v, intro))
vid = cur.fetchone()[0]
stats['vol_create'] += 1
seq_v += 10
# ---- 章:按 (卷, 章名) upsert正文不变则跳过 ----
cur.execute('SELECT id, name, body_html FROM game_story_chapter WHERE volume_id=%s',
(vid,))
existing = {r[1]: (r[0], r[2]) for r in cur.fetchall()} # name -> (id, body)
kept_ids = []
seq_c = 10
for cname, cbody in chapters:
if cname in existing:
cid, old_body = existing[cname]
if (old_body or '') != cbody:
cur.execute(
'UPDATE game_story_chapter SET name=%s, sequence=%s, body_html=%s, write_date=now() WHERE id=%s',
(cname, seq_c, cbody, cid))
stats['ch_update'] += 1
# 正文未变:跳过(不写库,保持 id 稳定)
kept_ids.append(cid)
else:
cur.execute(
"""INSERT INTO game_story_chapter
(volume_id, name, sequence, body_html, create_uid, write_uid, create_date, write_date)
VALUES (%s,%s,%s,%s,1,1,now(),now()) RETURNING id""",
(vid, cname, seq_c, cbody))
kept_ids.append(cur.fetchone()[0])
stats['ch_create'] += 1
seq_c += 10
# 删除源里已不存在的章(保持菜单 == 源)
orphan_ids = [i for n, (i, _b) in existing.items() if n not in {c[0] for c in chapters}]
if orphan_ids:
cur.execute('DELETE FROM game_story_chapter WHERE id IN %s', (tuple(orphan_ids),))
stats['ch_delete'] += len(orphan_ids)
conn.commit()
print('[ok] 卷 新建{vol_create}/更新{vol_update};章 新建{ch_create}/更新{ch_update}/删除{ch_delete}'.format(**stats))
print(' (正文未变的章被跳过,不写库 —— 1000 章日常同步也是亚秒级)')
def main():
ap = argparse.ArgumentParser(description='宇森小说 md -> Odoo 同步')
ap.add_argument('--novel-dir', default=os.path.join(os.path.dirname(__file__)))
ap.add_argument('--host', default=os.environ.get('PGHOST', '127.0.0.1'))
ap.add_argument('--port', default=os.environ.get('PGPORT', '5432'))
ap.add_argument('--db', default=os.environ.get('PGDATABASE', 'yt_game'))
ap.add_argument('--user', default=os.environ.get('PGUSER', 'odoo'))
ap.add_argument('--password', default=os.environ.get('PGPASSWORD', 'odoo'))
ap.add_argument('--dry-run', action='store_true')
args = ap.parse_args()
volumes = collect_volumes(args.novel_dir)
if not volumes:
print('未在', args.novel_dir, '下找到任何卷目录(子目录)。')
return
conn = psycopg2.connect(host=args.host, port=args.port,
dbname=args.db, user=args.user, password=args.password)
try:
sync(conn, volumes, dry_run=args.dry_run)
finally:
conn.close()
if __name__ == '__main__':
main()

View File

@ -1,111 +0,0 @@
# 《宇森·上帝模拟器》十卷剧情大纲
> **定位说明**:本小说是游戏《宇森 (YuSen)》的"创世正史"。游戏里玩家扮演"园丁"(播种、引导、关键时刻施力),本小说讲**第一个世界管理者(神)如何从能量为 0 养出整个宇森宇宙**。游戏的十五大系统(种族 / 性格 / 天赋 / 职业 / 建造 / 交易 / 战争 / 势力 / 装备 / 神器 / 传奇事件 / 五层世界…)在小说里都是主角逐步解锁的"创造权限"。
>
> **主角**:林默(现实世界加班猝死 → 绑定【上帝模拟器】→ 成为"世界管理者")。
> **核心金手指**:【上帝模拟器】——创造一切元素(物质 / 生命 / 法则 / 文明),能量来自文明的发展与信仰。
> **结构**10 卷 × 每卷约 100 章 ≈ 1000 章 / 250 万字。每卷一个"神格成长阶段 + 一层世界切面 + 一组系统解锁",独立成弧又串成主线。
---
## 卷一 · 创世之始(凡人成神)
- **成长阶段**:从"搓不出一颗石头"的穷神 → 点亮第一簇火、养出第一个部落。
- **世界切面**:大地层(地表原点)。
- **系统解锁**:基础创造(地形 / 物质)、文明种子投放、能量采集。
- **核心矛盾**:能量为 0空有神权却啥也造不出首批文明种子在灰白荒原上艰难求生随时灭绝。
- **爽点设计**:第 1 章穷神开局 → 第 30 章第一次用"引导"而非"强控"救活部落(打脸"神必须全能"的自我怀疑)→ 第 80 章部落升格为聚落,能量首次破千,解锁"性格"雏形。
- **卷终高潮**:聚落推举首位首领,向天空祈祷"赐名"。林默以意念落笔,将这个世界命名为 **「宇森」**——游戏宇宙的起点就此锚定。
## 卷二 · 大地文明(城邦崛起)
- **成长阶段**:从聚落管理者 → 多城邦的"文明引导者"。
- **世界切面**:大地层(城镇 / 农田 / 道路铺开)。
- **系统解锁**:性格、天赋、职业、建造(农场 / 兵营 / 神殿 / 学院 / 集市)。
- **核心矛盾**:文明内部分裂为"信神派"与"唯物派";邻邦因资源开战,首次出现"信仰流失导致能量暴跌"的危机。
- **爽点设计**:第 40 章用一次"神迹"(圣光普照)硬控战局,被双方奉为共同信仰源 → 第 70 章以"贸易"系统让敌对方变盟友,能量反超。
- **卷终高潮**:统一大陆的"宇森第一帝国"加冕,林默解锁"势力"权限,第一次以"规则"而非"手搓"维持秩序——园丁理念的雏形成型。
## 卷三 · 地下秘藏(矿脉与遗迹)
- **成长阶段**:从地表神 → 能操控地下生态的"深层管理者"。
- **世界切面**:地下层(矿脉 / 遗迹)。
- **系统解锁**:神器(创世之核 / 轮回镜 / 星陨剑…)、装备、锻造。
- **核心矛盾**:文明向下挖矿惊动"地心遗迹守卫";虫族自地底裂隙涌出,地表文明面临灭顶。
- **爽点设计**:第 35 章主角俯冲地心,徒手捏出"磐石甲"护住矿工 → 第 60 章夺得"创世之核"嵌入文明核心,世界等级首次跃迁。
- **卷终高潮**:以神器之力封死地底裂隙,虫族退去。林默领悟——神器是"前一个世界管理者"留下的遗产,他不是第一个神。
## 卷四 · 百族之争(种族林立)
- **成长阶段**:从单一文明 → 多种族宇宙的"调和者"。
- **世界切面**:大地 + 地下贯通。
- **系统解锁**:种族全开(龙裔 / 兽人 / 精灵 / 矮人 / 机械 / 虚灵 / 海族 / 天使 / 虫族 / 巨人 / 猫兽 / 树人)、法则(秩序 / 混沌)雏形。
- **核心矛盾**:十二种族因禀赋差异爆发"百族大战";高等种族蔑视主角"人造神"出身。
- **爽点设计**:第 50 章以"秩序法则"强行冻结战场,当众演示"种族天赋可被重新平衡" → 第 85 章用"混沌法则"化解虚空渗透,威望碾压老牌神裔。
- **卷终高潮**:百族签订《宇森协和约》,林默获封"万族之园丁",解锁"法则创造"高阶权限。
## 卷五 · 天空之主(气候与航行)
- **成长阶段**:从"天地"单控 → 掌握气象与远航的"天空之主"。
- **世界切面**:天空层(季风 / 气候 / 航行)。
- **系统解锁**:天象类传奇事件(双月同辉 / 圣光普照)、航海、气候调控。
- **核心矛盾**:大陆资源触顶,文明必须跨海拓殖;天空异常信号首次扰动季风,引发大饥荒。
- **爽点设计**:第 45 章拨动季风救活濒死作物,民众跪拜"气候之神" → 第 75 章操控"双月同辉"开启远洋航线,新大陆殖民成功。
- **卷终高潮**:星空层探出第一道清晰信号——"你并非孤神"。林默第一次意识到宇宙里还有"别的玩家"。
## 卷六 · 星空低语(异界信号)
- **成长阶段**:从单体世界神 → 接触"万界网络"的觉醒者。
- **世界切面**:星空层开启。
- **系统解锁**:世界等级系统、星空信号解析、灭世级灾变事件。
- **核心矛盾**:星空信号源自"远古观测者",抛出诱饵想吞并宇森;同时宇森遭遇首次"终焉级"灾变(星陨 / 虚空侵蚀)。
- **爽点设计**:第 40 章识破诱饵,反向溯源锁定观测者坐标 → 第 70 章以"世界等级跃迁"硬抗灾变,文明零灭绝。
- **卷终高潮**:林默第一次与其他"世界管理者"建立通讯——宇宙是无数个"宇森"般的沙盒,彼此独立又暗中博弈。
## 卷七 · 世界之跃(多界连通)
- **成长阶段**:从单界神 → 多界贸易 / 战争的"仲裁者"。
- **世界切面**:多世界平面连通(呼应游戏"独立世界 + 异步互动")。
- **系统解锁**:跨世界贸易 / 战争 / 联盟、异步策略博弈。
- **核心矛盾**:高阶"吞噬型"管理者想并吞新手小界;林默被迫下场护盘,与老牌神正面对弈。
- **爽点设计**:第 35 章用"经济战"拖垮对手贸易线,不费一兵 → 第 80 章以"联盟网"反包围吞噬者,当众打脸"神也要抱团"。
- **卷终高潮**:宇森成为万界公认的"中立枢纽",林默获"仲裁者"神衔,身后开始聚集一批后世"园丁"学徒。
## 卷八 · 法则之战(创世权柄)
- **成长阶段**:从"用法则" → "自创法则"的权柄级神。
- **世界切面**:法则层(时 / 空 / 因果)。
- **系统解锁**:专属法则创造(时 / 空 / 因果)、因果链干预。
- **核心矛盾**:万界议会中保守派禁止"自创法则",惧怕失控;暗中"虚空吞噬者"集结,欲吞掉整个宇森星区。
- **爽点设计**:第 50 章当众创出"时间回溯"法则,救回被抹除的友界 → 第 90 章以"因果律反噬"让吞噬者自食恶果。
- **卷终高潮**:林默以自创法则镇杀吞噬者首领,名动万界,被记入"创世者名录"。
## 卷九 · 万界共主(文明联邦)
- **成长阶段**:从"仲裁者" → "万界共主",却陷入"管得越多越孤独"的权力悖论。
- **世界切面**:万界议会 / 文明联邦。
- **系统解锁**:终焉系权限、文明联邦治理、信仰总池。
- **核心矛盾**:联邦内权斗不休;更大的威胁"终焉之潮"逼近——所有小世界的能量正被某个更上层存在悄悄抽干。
- **爽点设计**:第 45 章以"信仰总池"平抑内乱,展示"神可以不靠恐惧统治" → 第 85 章揭开终焉之潮真相:抽能者正是"上帝模拟器"本身。
- **卷终高潮**:林默发现残酷真相——【上帝模拟器】是一台培育"超越者"的机器,管理者只是养料。他决定反控系统。
## 卷十 · 永恒园丁(超越神座)
- **成长阶段**:从"被系统豢养的神" → 超越系统的"永恒园丁"。
- **世界切面**:系统内核 / 宇森本源。
- **系统解锁**:终极权限(超越模拟器)、世界模板固化。
- **核心矛盾**:与【上帝模拟器】背后"园丁之上"的终极存在决战;代价是放弃永生神格。
- **爽点设计**:第 50 章以"自创法则 + 万界信仰"击穿系统防火墙 → 第 90 章将宇森固化为"可继承的世界模板",留予后世。
- **卷终高潮(闭环回游戏)**:林默散去神格,化作"永恒园丁"隐入幕后。他把自己被阉割过的权柄打包成"园丁系统",分发到无数个新世界——**游戏里每一个玩家,都是他留下的继承者**。屏幕外的你,正握着那份权柄。"去探索吧,园丁。这个世界在等你。"
---
## 主线暗线(贯穿十卷)
1. **金手指的限制与进化**:能量只能来自文明发展/信仰 → 后期的"信仰总池"→ 终极"超越模拟器"(对应游戏能量/世界等级/神器嵌套逻辑)。
2. **园丁理念的形成**:卷一"手搓救火"→ 卷二"引导胜强控"→ 卷十"放权给继承者",正好对应游戏"玩家是园丁而非操控者"的核心设计。
3. **五层世界的递进解锁**:大地 → 地下 → 天空 → 星空 → 法则层,与游戏五层垂直世界(地心/地下/大地/天空/星空)一一映射。
4. **身份反转**:凡人 → 神 → 被系统豢养的神 → 超越系统的园丁,完成"一个人的成长"闭环。
## 与游戏系统的映射表(写正文时务必对齐)
| 小说权限 | 游戏系统 | 首次解锁卷 |
|---|---|---|
| 基础创造 | 地形 / 物质 | 卷一 |
| 性格 / 天赋 / 职业 | 性格 / 天赋 / 职业 | 卷二 |
| 建造 / 贸易 / 势力 | 建造 / 交易 / 战争 / 势力 | 卷二~三 |
| 神器 / 装备 | 神器 / 装备 | 卷三 |
| 种族 / 法则(秩序混沌) | 种族 / 系别 | 卷四 |
| 天象 / 航海 | 传奇事件(天象) / 航海 | 卷五 |
| 世界等级 / 星空 | 世界等级 / 五层 | 卷六 |
| 跨界互动 | 独立世界 + 异步互动 | 卷七 |
| 专属法则(时空调) | 终焉 / 秘辛事件 | 卷八~九 |
| 超越权限 / 模板 | 园丁系统本体 | 卷十 |

View File

@ -1,607 +0,0 @@
# 卷一 · 创世之始
> 从能量为 0 的穷神,到点亮第一簇火、养出第一个聚落。这是宇森宇宙的起点。
---
## 序章 · 最后一班,与最初的一班
林默是在凌晨三点十七分死去的。
死因没什么戏剧性——连续第四十一个小时对着显示器,心脏先一步下了班。最后映进眼底的,是屏幕上那行还没保存的方案文档,和右下角弹出的「内存不足」。组长两小时前在群里 @ 他:「默哥,天亮前这版必须出,客户等着。」他回了句「马上」,然后把第三杯凉透的咖啡灌进了胃里。
黑暗来得干脆。
再睁眼时,没有病房,没有急救的蓝光,只有一片望不到尽头的灰白。
不是雾,不是墙,是某种更本质的「空」。脚下没有实地,周身没有空气,可他偏偏稳稳地「站」着,像被某种规则托住。视野正中央,半透明面板无声浮起:
> 【上帝模拟器 · 已绑定】
> 【身份:世界管理者】
> 【权限:创造一切元素(物质 / 生命 / 法则 / 文明)】
> 【当前能量0】
一行冰冷的提示之后,林默才慢慢嚼出自己成了什么——不是这个世界里卑微的居民,而是它的造物主。山可以凭空隆起,海能够随手倾泻,连「重力」与「时间」这种底层规则,都能被他像拨动开关一样改写。
前世四十一年,他连给自家阳台换个灯泡都要怕漏电。如今一念可造山海?那点社畜的狂喜刚冒头,系统就补了一刀,把这份亢奋砸回地面:
> 【警告:能量源自文明的发展与信仰。没有生灵,您连点亮第一缕火光都做不到。】
也就是说他空握一身神权却穷得连颗石头都搓不出来——一个兜里只有「0」的穷神。
就在他盯着「能量0」发愣时面板角落弹出一条闪烁的红字
> 【检测到初始文明种子 · 已坠落至世界边缘。是否投放?】
林默抬起手。指尖穿过灰白,点下那个「是」。
「投放。」
下一秒,一簇微光砸进无垠的虚空。
他知道,自己这趟「上帝」的差事,从这一刻,才算真正开场。
---
## 第1章 · 灰白沙盘
能量为零的世界,比林默想象的更荒凉,也更会嘲讽。
他先试着「造一座山」。面板闪了一下,弹出一行小字:
> 【创造失败:能量不足。本次所需最低能量 1当前 0。】
一座山要 1 点能量。他不信邪,又试着「点一团火」:
> 【创造失败:能量不足。本次所需最低能量 0.1,当前 0。】
连火都要 0.1。他凭着前世玩游戏的手感,意念里猛地一搓,想凭空攥出一颗石子——灰白的空间纹丝不动,连粒尘埃都没飘起来。
> 【创造失败:能量不足。本次所需最低能量 0.01,当前 0。】
「……」林默盯着那行「0.01」,半天没吭声。一颗石子,零点零一。他连零点零一都付不起。
他又试了「造一汪水」「唤一阵风」「凝一缕光」,提示音一个接一个,像超市收银台在他空钱包上反复扫码:
> 【创造失败:能量不足。本次所需最低能量 0.05,当前 0。】
> 【创造失败:能量不足。本次所需最低能量 0.03,当前 0。】
> 【创造失败:能量不足。本次所需最低能量 0.02,当前 0。】
前世他被甲方用「预算为零」折磨了半辈子,没想到死了换个坑,还是被「余额为零」卡着。只不过以前是被人卡,现在是连宇宙都嫌他穷。
「所以这就是当上帝的体面。」他对着空无一物的虚空自嘲,「连呼吸都欠着宇宙的最低工资。」
可他很快发现,这方灰白并非死寂。当他凝神去「看」,视野里浮起一层极淡的网格,从脚下向四周无限延展,像一张等待落子的棋盘。线条是冷蓝色的,带着细微的刻度,一格一格没入远处的灰白里。他低头看自己的「脚」——那不过是网格上一个微微发亮的交点。
他试着沿网格走了几步。不是用腿,是用「念头」往某个方向一送,视角便平移过去,脚下的交点随之挪动。这感觉不像走路,更像在地图上游标。原来他不是被困在这片空里,他是站在自己世界的「沙盘」之上,而沙盘之外,还有沙盘。
他往更远处「望」。灰白并非均匀的一片——在目力将尽的边缘,他隐约看见另外几块暗淡的网格碎片,悬浮在更深的虚无里,尚未点亮,也无从抵达。那是别的「待创世界」吗?系统没给答案,只在他视线触及的瞬间,把那些碎片藏进了灰白深处。林默把这点疑惑压下了:眼下连脚下的棋盘都还没填,操心别的棋盘未免太早。
棋盘的尽头,那簇被他投放的文明种子,正悬在一道微光里,缓慢旋转。
> 【文明种子 · 状态:休眠中】
> 【内含:初始生命蓝图 ×1、文明火种 ×1】
> 【激活条件:管理者注入首批引导意志】
「引导意志?」林默皱眉。系统给了段简释:他不必事必躬亲地「手搓」万物,只需在世界的关键节点施加一次影响——一次抉择、一句低语、一道微光——文明便会顺着自己的逻辑自行生长。这恰好是「园丁」二字的真意:播种,引导,然后退后,看它活。
简释里还划了一道界限:凡是「从无到有」的创造——山、海、火、风、法则——都要消耗能量;而凡是「在已有节点上轻轻一推」的引导——把将熄的火拨旺、把迷路的人引向水源——只耗那点微乎其微的「引导意志」,不计入能量。一句话:造物要钱,引导免费。
他咂摸着「园丁」这个词,忽然觉得这差事没那么寒酸了。当神当到要乞讨一句话的权限,固然憋屈;但换个角度——他不是来当苦力的,是来当「让一个世界自己长起来」的那只手。就像一个投资人,投下一颗种子,然后等它自己长大、分红。
「总算有个能用的定位。」他松了口气。
但紧接着一个问题冒了出来:既然造物要能量,能量又只能靠文明来产,那在文明长起来之前,他岂不是永远 0系统冷冰冰地确认了这道死局——能量唯一来源就是文明的发展与信仰。也就是说他必须先「无中生有」地把文明养出第一口气能量才会反哺回来。
而手里唯一的破局筹码,就是那簇免费投放的文明种子。
那颗种子是「已坠落至世界边缘」被系统检测到的,也就是说,不是他自己造的,是「掉」进来的。谁掉的?系统没说。林默直觉这事儿不简单——天上掉下来的免费种子,往往连着更大的局。但他眼下没资本挑三拣四。先接住,再看清。
他飘向那簇种子。微光触手的刹那,一段信息涌入脑海:这是一簇最原始的「人」——没有种族、没有天赋、没有记忆,只是一团团等待被点亮的意识火苗。他们将从零开始,在灰白里摸索火、食物、聚居,用一代代试错,把荒原种成文明。而他的任务,是在他们每一次濒临熄灭时,递上那根关键的柴。
林默翻了翻那份「生命蓝图」。模板极简:一具能感知冷热、能移动、能彼此靠近的躯壳,没有性别,没有记忆,连「怕」都只是本能的蜷缩。他忽然有点心疼——上来就这么难,连件像样的身子骨都没有,就要在灰白里求生。
「行吧,」他对那团微光说,「你们负责活,我负责在你们快不行的时候,扶一把。」
> 【是否激活文明种子?】
> 【提示激活后将消耗您当前全部能量0/0并开启首个世界锚点。】
林默笑了:「反正兜里比脸还干净,点。」
> 【激活成功。世界锚点已生成:未命名。】
他忽然想起前世给项目起名的习惯。一个世界,总得有个名字。
「就叫……宇森吧。」他对着灰白说,「寰宇、森罗——一个能自己长出万物的地方。」
> 【世界锚点命名成功宇森YuSen
> 【锚点稳定性1% · 待文明填充】
命名之后,林默顺手扫了眼面板深处。除了「创造」「引导」两项,下方还排着一长列灰色的图标,标注着「种族」「元素」「性格」「天赋」「神器」「传奇事件」……足足十五个模块,此刻全都挂着一把小锁。他数了数,恰好对得上系统早先提过的「十五大系统」。锁的下方有一行淡到几乎看不见的字:【权限随文明层级依次解锁】。林默咧了咧嘴——合着神也不是一次到位,得一级级打怪升权。他记下了这十五个名字,心里有了谱:眼下能用的只有「生命」与「火种」,其余的,且等着宇森自己长出来,再向他敞开。
脚下的灰白轻轻一颤,像沉睡的湖被投入第一颗石子。远处那簇微光,开始向下坠去,拖出一道细小的光尾——
它要落了。
而就在光尾拉长的刹那,林默余光扫过灰白更深处——那里,似乎有一道极淡的、不属于他的「注视」,正落在即将诞生的宇森之上。
临了,他给自己定了三条规矩:一,不替文明做它自己能做的事;二,不在文明看不见的地方偷偷改规则;三,真到必须出手时,绝不手软。这三条后来被刻进宇森的底层逻辑,成了历代园丁的祖训。
他没声张。只是把那道注视,悄悄记在了心里,像记下一个尚未浮出水面的对手。
---
## 第2章 · 坠星
光落下的地方,后来被那些活着的人称作「创世原」。
林默以意念拉近视角。灰白棋盘的中央,一团微弱的光晕正缓缓铺开,像水面洇开的墨,边缘还带着未散尽的微光。光晕里,二十余个模糊的身影次第浮现——他们是种子里苏醒的第一代「人」,赤条条、懵懂懂,像刚被吹进世界的泥偶。没有名字,没有语言,只有最原始的本能:冷了要靠近,饿了要找,怕了要聚。
他们先是彼此打量,继而本能地往光晕最暖的中心挤。一个瘦高的影子不小心撞了同伴,两个泥偶滚作一团,又懵懂地分开——那动作毫无意义,却让林默想起了前世地铁早高峰里同样懵懂的自己。
光晕铺开后好一阵,一个胆子稍大的影子忽然伸手,碰了碰身旁那位的脸。两个泥偶同时愣住,仿佛头一回「看见」彼此。那一下笨拙的触碰,没有名字,没有语言,却让林默心头一热——那是宇森历史上第一次「对视」,众生的开篇,就藏在这毫无意义的一碰里。
二十几道身影里,有几个让林默下意识留了标记。最小的那个,他叫「小不点」,蜷起来只有一个拳头大;一个总往光晕外探的,他叫「探路者」,胆子明显比旁人大;还有一个,会本能地把摔倒的同伴往中间拢,他叫「抱团者」。这些名字系统不认,只存在林默心里——一个管理者给自己世界第一批居民起的、上不得台面的小名。
白昼短暂。很快他们就饿了。灰白里没有食物,连可啃的东西都没有。几个影子伏下身,用鼻子去蹭地面,又失望地抬起头;小不点试着把手指塞进嘴里啃,被抱团者一把拍开。林默下意识想「给他们弄点吃的」,系统却弹出一行:
> 【提示:园丁仅可于「已存在节点」上引导,无法凭空投喂。当前世界仅激活「生命蓝图」与「火种」,未含「食物蓝图」。】
也就是说,他给了「人」和「火」,没给「饭」。饭,得等这帮泥偶自己从环境里摸索——采集、狩猎、或者,先把火用明白。林默第一次真切感到「园丁」二字的重量:他能递柴,却不能替他们把饭煮好。他把这条记进了心里,作为宇森要跨过的第一道文明难题。
他甚至试着在光晕外「引导」地表萌发一点可食的绿意,念头刚起,面板就凉凉地提醒:【引导对象须已存在。当前世界无「植物」节点,无法凭空催生。】林默讪讪收了手。行,饭的事,交给他们自己。
第一个夜晚来得毫无预兆。灰白的天幕暗下去,寒意顺着某种林默尚不理解的规则渗入光晕。作为锚点的管理者,他竟也能「感知」到那股冷——不是皮肤的冷,是世界的温度在掉。那二十几道身影开始发抖,彼此挤拢,却谁也不知道该如何抵御。探路者还想往光晕外探,被抱团者一把拽回。小不点已经蜷成了团,手指冻得发青,一动不动。
> 【事件触发:首夜严寒】
> 【文明存活率预测41%】
> 【建议引导:于光晕边缘落一处火源】
林默几乎是本能地伸手,朝那片虚空中按下一缕「意念」。他看不见自己做了什么,只觉心头微微一空——像吹灭一根蜡烛消耗的那点气。与前几次「造物」不同,这一下不耗能量,只耗那点微乎其微的「引导意志」。系统早说过:创造元素要能量,而在已有节点上轻轻一推,是园丁的本分,不收钱。
下一瞬,光晕边缘腾起一簇豆大的火。
那二十几道身影猛地转头。他们不认识火,却认得暖。探路者第一个伸出手,被热浪一烫,又缩回,继而再次凑近,像在跟这团陌生的东西试探交手。很快,他们围成了圈,瑟缩的肩膀松开了。
抱团者把小不点拖到了火边。暖气裹上去,他冻僵的手指动了动,终于又蜷着喘上了气。林默盯着那一小团重新起伏的背影,忽然觉得这簇豆大的火,比他前世见过的任何灯火都来得郑重。
> 【引导成功:火源已落】
> 【文明存活率预测78%】
> 【能量 +0文明尚处萌芽暂未产生信仰
林默盯着那行「能量 +0」忽然懂了系统的残酷他刚才那一下是白送的。没有回报没有反馈只有一群还不会感恩的泥偶围着火发抖。前世他加班好歹有工资如今当神连个五险一金都没有。
可他没来由地笑了。
因为就在火光摇曳里,他看清了——探路者正笨拙地用两根枯枝去拨弄火堆,想让它更旺些。那动作毫无意义,却是一道嫩芽:这团意识火苗,开始自己「想办法」了。更让他心头一动的,是探路者接着把火边一块发亮的小石扒拉进焰心,火星「噼啪」一溅,那泥偶整个僵了一瞬,随即第一次做出了近似「笑」的咧嘴——那是宇森历史上第一个被惊喜击中的人。探路者没停,又试着把一根更大的枯枝架上去,火苗窜高,照亮了周围几张懵懂的脸。林默不动声色,只在心里给探路者多画了一颗星:这帮泥偶里,这家伙怕是头一个「领袖」的苗子。
> 【检测到:文明自主行为 · 首次】
> 【评价:萌芽。世界锚点稳定性 +1。】
灰白棋盘上,那一点光晕,比刚才亮了半分。
林默收回目光望向更远的虚空。那里还有无数片未点亮的灰白等着被落子。而他兜里依然只有那串刺眼的「能量0」。
但至少在创世原,有了一簇火,和二十几个开始想办法活下去的人。白天的饥饿还没解,明晚的严寒还会来,可他们已经学会了一件最重要的事——往火边靠。
火苗稳定的后半夜,林默没走。他隔着光晕看他们如何熬过宇森的第一个长夜:有人挨着火堆蜷着睡去,呼吸第一次有了节奏;抱团者没睡,守在火边,时不时把探路者拨偏的柴扶正;小不点被挤在最里圈,偶尔在梦里抖一下,又被身旁的暖意按了回去。林默忽然意识到,时间这件事,是有了火才在他们身上开始的——白昼与黑夜不再是灰白均匀的空,而是「守着火」和「等着天亮」的盼头。一个世界最朴素的钟,是火给的。
长夜将尽时,林默第一次以「管理者」而非「穷人」的视角,重新审视这片灰白。他忽然懂了:能量为 0 不是羞辱,是起点。正因为这穷神开局,他才被迫扔掉「神力万能」的妄念,老老实实学会「不靠蛮力、靠引导」的真本事。日后宇森若真能长成参天之木,这一课,便是根。
「这才像点样子。」他轻声说。
远处,虚空深处,那道昨夜一闪即逝的涟漪,又轻轻荡了一下。
这一次,林默看清了——涟漪的中心,似乎也有一双「眼睛」,正安静地,看着他。
他在心里问了自己一句:那双眼睛,是在看宇森,还是在看,作为管理者的我?
灰白没有回答。但林默已经决定,暂不打草惊蛇。先把这个世界的火,养旺再说。
那双眼睛没有再动。涟漪散去,灰白重归均匀的空。林默却把这一幕牢牢记下——等宇森有了第一批会「想」的人,等能量第一次从信仰里流回他体内,他要把这道注视的来路,查个明白。
---
## 第3章 · 第一缕炊烟
光晕里的日子,是靠火数着的。
林默守了创世原三个「昼夜」——他现在能用锚点的节律大致分出明暗,虽然宇森还没有真正的太阳,只有灰白天幕周期性的暗与亮。火没灭过。石(也就是从前的抱团者)像个固执的老妈子,谁想往火里添柴都得过它那一关,添少了它瞪你,添多了它自己动手拨匀。林默隔着虚空看这景象,莫名想起前世公司茶水间那个总嫌咖啡机没洗干净的行政大姐——权力的甜头,原来在哪都长一个样。
饥饿却没被火吓退。
基质——就是灰白凝结出的那层硬壳——烤过的地方越啃越少。到第三个白天,二十几道身影又开始在光晕里舔嘴唇。芽(从前的小不点)饿得直往石怀里钻,叼着石的手腕啃,被轻轻拍开,委屈地缩成团。林默看着芽那副样,心里某根弦被拨了一下:这小东西,像极了他前世在地铁上见过的、攥着妈妈衣角喊饿的小孩。
风在光晕里来回窜,一会儿扒拉木编到一半的枯枝筐,一会儿被焱拍开。木闷头把基质碎屑编进枯枝缝里,像在试着做个能装东西的物件——虽然后来证明那玩意漏得跟筛子似的,可「容器」的念头,已经在了。焱成天黏着火堆,往里添柴添得兴高采烈,有回差点把刚睡着的芽燎着,被石一巴掌扇到边上,委屈了半天。
林默把这幅鸡飞狗跳的群像看了个遍,心里那点「造物主」的孤高,早被磨成了「幼儿园园长」的无奈。前世他最怕带新人,如今倒好,带一园连话都不会说的泥偶。
林默想帮忙。念头一起,面板照旧凉凉:
> 【引导对象须已存在。当前无「食物」节点。】
行,老规矩。他不造,只「引」。
他让一缕引导意志,轻轻搭在岩(从前的探路者)正拨弄火堆的手上——不是喂,是「指」。像有人把你的视线,往火边上那堆被烤得发脆的基质碎屑,偏了半寸。
岩顿了顿,捏起一块烤脆的碎屑,塞进嘴里。嚼了两下,眼睛亮了。
那东西原本啃不动,火一烤,竟松了,带着点焦香,下肚后胃里有了底。岩猛地抬头,把碎屑分给身边的人。一圈人学着他的样,把基质往火边凑。第一缕真正的「炊烟」——烤基质的薄烟——从创世原升起,混在火光里,懒懒地散进灰白。
林默盯着岩嚼碎屑那副郑重其事的样,差点笑出声。他见过太多人第一次吃到好东西的表情,可这是宇森历史上第一口「熟食」,当事人还不知道自己刚跨过了多宽的沟。
> 【文明自主行为 · 食品加工雏形】
> 【评价:求生。锚点稳定性 +1。】
> 【能量 +0尚无信仰仅为本能求生
林默盯着「能量 +0」已经麻木了。但看着那圈人第一次因为「吃饱」而松弛的肩膀他觉得这 0 比啥都值。前世他加班到凌晨,最盼的不也就是一口热乎饭、一个能松口气的肩么。如今他成了神,能给的却也只是这一口热乎——神又怎样,填不饱别人的胃,就只能看着别人自己填。
芽忽然松开石,自己伸手,朝着岩手里的碎屑抓。这是它第一次「主动要吃的」,不再是哭,是「讨」。林默心里一热:一个文明的「需求表达」,就从这一抓开始。
「会自己找吃的了。」他轻声说,「下一步,该嫌难吃了吧。」
果然。到傍晚,岩把一块烤糊的碎屑吐出来,皱着脸看火,像在嫌弃这饭太糙。林默差点笑出声——一个连语言都没有的泥偶,已经开始挑食。他甚至有点欣慰:会挑嘴,说明饱过,说明这文明算真活过来了。
他顺手翻了翻系统对「基质」的注解:世界最底层的凝结物,非生命,仅能勉强充饥,毫无营养。换句话说,这顿饭只是让文明「不死」,撑不到「活好」。真食物,得等他哪天攒够能量,自己激活「植物」节点——或者,等文明从环境里摸出门道。
注解最后还有一行小字,让林默眉头一皱:【基质存量:创世原周边 · 低。】系统连库存都帮他盘好了。他忽然觉得自己不像造物主,像个刚接手贫困县的新任县长,上任第一件事就是查粮仓。
他忍不住在心里嘀咕:「那你当初直接给我个食物蓝图不行?」
系统半秒后回:【文明须自觅出路,否则永为依附。园丁之责,在引不在予。】
林默咂咂嘴。这道理他懂,前世带团队也一个样——手把手喂出来的,永远长不大。可懂归懂,看着芽瘪下去的肚皮,到底还是不好受。
夜深,众人缩进勉强挡风的角落睡了。林默没走。他独自悬在灰白上,守着这簇豆大的火,忽然感到一种前所未有的孤独——他是这方天地的神,却是个没人在乎、没人供奉、连香火都收不到半点的神。可偏偏,就是这份没人知道的孤独里,藏着他这辈子最踏实的东西:他亲手养出的火,正暖着二十几个命。
他又把视角压低,挨个看那圈围火的人。岩啃完碎屑,用手背抹了嘴,冲旁边的人扬了扬下巴,喉咙里滚出半个「呷」音——是在招呼人「吃」。石把分得的一块碎屑,掰了一半塞给怀里的芽,自己嚼那半块,嚼得极慢,像要把这点滋味多留一会儿。风窜到木身边,用脑袋顶了顶木手里的枯枝筐,被木反手按住脑袋,也不恼,笑嘻嘻滚开。
林默看着,忽然觉得这画面比任何系统提示都金贵。
他试着在管理者面板里调出「文明发展度」。一行淡蓝的进度条浮起,几乎贴着底,却确确实实在动:
> 【文明发展度0.3%】
> 【生存:勉强维持|聚合:雏形|语言:萌芽前|工具:无|信仰:无】
那根几乎看不见的细线,让他想起前世带的一个项目,刚立项时也是这副惨样,所有人都说做不起来。后来呢?后来他熬了四十天,项目真跑起来了。如今这项目,是他拿命换来的世界。
「0.3%。」他喃喃,「放前世,这 KPI 早被老板砍了。」
可他心里没有半点不耐。0.3% 后面,是二十几个真真切切、会冷会饿会朝他喊「高高」的命。进度条可以慢,命不能等。
他忽然很清楚地知道自己该怎么当这个神:不是高高在上的造物主,是蹲在泥里、陪这批人把 0.3% 一点点磨上去的那只手。园丁二字,到此才算真正落进他骨头里。
「就算穷,」他对灰白说,「我也把你们养出第一个聚落来。」
话音落,他余光又扫到虚空深处那双眼睛。它还在,比昨夜近了些,安静地悬着,像在等什么——等他犯错,还是等他证明自己配得上这方世界?
林默没躲它的注视。他甚至朝那方向,极轻地一点头,像个棋手落子前,先跟对面的对手致意。
钩子:林默默默算了一笔账。照这吃法,创世原周边的基质,撑不过七个昼夜。火能烤,却变不出更多。而文明的胃口,只会随人数涨。真正的饥饿,还没来——但倒计时,已经开始了。
---
## 第4章 · 饥饿的冬天
第七个昼夜临界,周边基质见了底。
岩是第一个察觉的。它蹲在光晕边缘,用手指刮了半天,只刮下一点白沫,凑到嘴边尝,什么都没有。它回头看了一圈缩着的人,又看向灰白更深处——那里更冷,却也更大,更大就意味着,可能有更多基质。
它站起来,往光晕外走了一步。
那是创世原历史上第一次「出圈」。林默心头一紧,下意识想伸手拦,又硬生生忍住。园丁的规矩第一条:不替文明做它自己能做的事。岩能走,就让它走。可他到底不放心,把一缕感知悄悄缀在岩的背后,像放风筝的人攥着那根看不见的线。
岩走了。石犹豫了一下,把芽塞给旁边木(那个手巧的影子),自己跟了上去。接着是风——一个总爱乱跑的影子——还有三两个胆大的。七道身影,排成歪歪扭扭的一列,没入灰白。
林默把视角跟上去。光晕外的寒意明显更重,他的「管理者感知」都被冻得一哆嗦。但他很快发现,正如他所料,灰白里漂浮着更密的基质结晶,像撒了一地的碎瓷,泛着冷冷的光。
岩蹲下,笨拙地掰下一块,掂了掂,揣进怀里。又掰一块。它动作越来越快,像怕这东西会跑。其他人学着,风最积极,窜出去老远,差点迷在均匀的灰白里,被岩一声低吼拽回来。
他们学会了「采集」。
回程走到一半,岩在均匀的灰白里迷了向——这里没有光晕那点暖意指引,四面都是一样的空。它转了两圈,停下,罕见地露出了慌。林默看着,心里叹了口气。
他泄出一丝引导意志,不是告诉它往哪走,只是让岩心底那点「家」的方位感,亮了半分。像黑屋里有人悄悄把门缝透进的光,调亮了一点。
岩立刻抬头,循着那点微光似的直觉,领着队伍准准地扎回光晕。
> 【引导成功:归途指向(微量意志,不耗能量)】
> 【文明自主行为 · 采集诞生】
> 【锚点稳定性 +1。能量 +0。】
七道身影归来,怀里鼓鼓囊囊。火重新旺起来,基质碎屑烤得噼啪响。林默看着这趟往返,第一次觉得「探索」二字有了重量——文明不是被圈养的,是肯迈出去的。这一步,比它后来造出任何奇迹都让他骄傲。
可回来没多久,麻烦就生了。风把自己采的结晶偷偷多藏了一捧在身后,被石一眼瞥见。石走过去,伸手要。风抱紧,往后缩。两个影子僵持,发出含混的呼喝。这是创世原第一次「争」——不是争火,是争食。
林默没插手。他隔着虚空看,心里五味杂陈。私藏、争夺,放在前世是毛病;可放在文明起点,恰恰是人「懂得为自己打算」的证据。没有「我」,哪来「我们」?可「我」一旦长出来,就难免刺到「我们」。
石到底从风怀里抠出那捧结晶,平分给所有人。风瘪着嘴,却没再抢。林默注意到,分完之后,风第一次主动把最大的一块,递给了芽。
那一递,让林默觉得,这文明的「私」,或许长得出「公」来。
夜更深,寒意比昨日重。有了储备,人却更觉冷——大概是吃饱了,才顾得上喊冷。林默听着棚里此起彼伏的哆嗦,忽然很想给这世界添件「衣服」,可「衣物」也算造物,要能量。他依旧是个兜里空空的穷神。
他再把感知投向灰白深处,想再确认那点微光,却见岩也正望着那个方向。它愣住,往那点微光多看了两眼,喉头动了动,像想把什么记下来,才被石拽回火边。
林默的感知落在那点微光上,系统慢吞吞弹了一行:
> 【检测到:第二枚文明种子 · 状态:休眠 · 距离:极远】
> 【备注:种子来源 · 未知。】
来源未知。和第一颗一样。
林默盯着那行「来源 · 未知」,半天没动。他想起自己那颗种子,也是「坠落至世界边缘」被系统检测到的——换句话说,不是他造的,是「掉」进来的。如今第二颗也一样,来源成谜。
这让他后背微微发凉。如果种子是「谁」播的,那播种子的人,是不是也像他一样,是某个世界的「管理者」?宇森之外,是不是还有别的管理者,守着别的棋盘?
他立刻调出权限面板,翻那十五个灰锁——「种族」「元素」「性格」「天赋」「神器」「传奇事件」……翻到底,没有「其他管理者」这一项。系统对他,只字未提「同行」。
是根本没有,还是刻意瞒着?
林默想起前世职场:老板从不会主动告诉你,还有别的组在抢同一个资源。信息的缺席,往往本身就是信号。
他决定:暂不声张。第二颗种子远在极处,休眠着,暂时威胁不到创世原。他现在的本分,是把脚下这二十几号人,养出第一个聚落。至于棋盘外的事,等自己有了能量、有了底气,再去掀。
那一夜,林默第一次「失眠」。神不需要睡,可他需要想。他想岩、想石、想芽,想那点远处的微光,想虚空里那双越来越近的眼睛。一个穷神,守着一方刚冒芽的天地,四周却似乎全是看不清的局中人。
他忽然笑了一声,笑自己。前世怕老板、怕客户、怕月底 KPI死了当神怕的换成种子、眼睛、和另一个不知在不在的棋手。怕倒是一以贯之。
可怕归怕,手不能停。
钩子:第二颗种子。是机缘,还是对手?林默把这点微光,和虚空里那双眼睛,一起压进了心底。他隐约觉得,宇森这盘棋,不只他一个棋手。
---
## 第5章 · 第一个名字
采集解决了饿,却没解决「乱」。
二十几道身影挤在光晕里,谁是谁全靠林默自己记小名。他们彼此呼唤,只用含糊的气音和手势。林默有一天忽然觉得不对——一个文明,如果连「个体」都没有,谈何「成长」。前世他带团队最深的一课:你连人都认不全,就别指望带出战斗力。
他决定给它们正经起名。
这不是造物,不算越界。系统早说过,赋予标识属于引导范畴,只耗微量的「引导意志」。林默挑了个安静的昼夜,把一缕意志,轻轻种进每个人的心底,各落下一个清晰的音节符号。
起名这活,他熟。前世给项目起过名,给养的猫起过名,给看不惯的同事起过只有自己懂的外号。如今给一整个世界的第一批人起名,他反倒郑重起来。
岩,还是岩——那个敢探路、敢砸火的。音节落下去时,岩忽然抬手拍了拍自己胸口,像把这两个字,按进了骨子里。
石,还是石——那个守火、护芽的。它听到,扭头望了眼火,又望了眼怀里的芽,像是认了。
芽,还是芽——最小的那个。它眨了巴眼,显然不懂,但没反抗。
风,那个总爱乱跑的。音节一落,它真原地转了个圈,把众人都逗乐了。
木,那个手巧、会编枯枝的。它手里还攥着那漏底的筐,被点名时憨憨一笑。
焱,另一个成天黏着火堆的。它兴奋地往火里添了根柴,像在说「在呢」。
还有几个:禾、川、砺、陶……一个一个落下去。
二十几个音节,落进二十几团意识。它们彼此喊名字的瞬间,光晕里第一次有了「你」「我」之分。从前是「那个影子」「另一个影子」,现在是「岩」「石」「芽」。
> 【文明自主行为 · 个体标识确立】
> 【评价:社会雏形。锚点稳定性 +2。】
> 【能量 +0。】
小爽点不在能量,在林默心头那点热乎——他亲手给这世界的第一批居民,发了「身份证」。这比任何系统升级都让他踏实:从今往后,创世原死的每一个、活的每一个,都叫得出名字。
命名之后,变化立竿见影。风再窜出去,岩一声「风!」它就乖乖回来;木编筐,石隔着老远喊「木,给芽留个角」,木就真在筐沿留了个凹。一个文明最朴素的「组织」,从互相叫得应名字开始。林默忽然发现,他再引导时,能「指名」了——落一缕意志在「岩」身上,不必再笼统地拂过一群。管理,第一次有了对象。
可命名完成时,林默忽然一僵。
虚空里那双眼睛,似乎「数」了这些人。不是敌意,更像记录。像有人在册子上,一笔一笔,把创世原的居民,记了下去。
林默后背一凉。它记这些人干嘛?盘点到数?监控?还是……等哪天收割?
他想起第二颗种子、想起种子「来源未知」。如果真有「上家」,那这双眼睛,会不会就是上家派来的眼睛?
他压下不安,没声张。眼下创世原撑不过几个昼夜的饥饿,内忧大过外患。那双眼睛爱记就记,等他这批人长出了牙,再跟它算账。
夜深,众人缩进棚里。林默听见他们第一次像「点名」似的,互相用名字道晚安——「石」「芽」「岩」……含混却认真。一个没有语言的文明,用名字,替自己关上了灯。
林默隔着虚空听着,鼻子有点酸。前世他最怕的,不是加班,是开会时老板念错他的名字、念成「小林」「阿默」混着叫。一个名字被认真喊对,原来这么金贵。如今这群泥偶,用他给的名字,认真地喊着彼此——他这个没人记得名字的社畜,反倒成了给一整个世界命名的人。
他翻出「文明发展度」,那根细线微不可察地右挪了一点点:【社会:雏形→萌芽】。
钩子:更让林默意外的是,就在这一片名字声里,芽忽然转头,清清楚楚地喊了一声——「石」。
不是喊火,不是喊吃。是喊一个「人」。
那是宇森历史上,第一个被喊出来的名字。林默隔着虚空,听见这一声,半天没言语。一个文明,从这一刻起,不再只是「求生的群」,而是「相认的人」。
---
## 第6章 · 语言萌芽
有了名字,「词」就拦不住了。
围火的第六个昼夜,岩指着跳动的焰心,喉咙里滚出一个含糊的音节:「呼——」。旁人跟着学,含混一片。林默耳朵一竖——系统在他「管理者权限」下,无声解锁了一层新的监听:
> 【文明语料监听 · 已开启】
> 【首词记录:呼(火)。】
第一个词,是「火」。林默觉得理所当然。是火把这批泥偶从冻死边缘拽回来的,他们不先记住火,才怪。
可他没料到,词会来得这么密。
紧接着是「姬」(寒)——岩缩着脖子,朝灰白上方努嘴,吐出这个音。众人立刻共鸣,往火边缩紧。再是「呷」(吃)——岩啃碎屑时顺嘴滚出来的,从此成了围火最常听到的字。然后是「石」,人名的复用,被喊的人会抬头。
词不多,却像种子,落地就活。
风跑回来,兴奋地蹦着喊「外——」,指的是光晕外的灰白。林默由此确认:他们已经有了「内」与「外」的概念。木编筐,被岩喊一声「木」,它应声。一个名字加一个词,文明的交流,第一次有了「主谓」的雏形。
林默翻着系统实时跳动的语料库,有点想笑。前世他最烦写周报,如今倒成了这世界第一个「语文老师」,还是旁听的那个。
> 【文明发展度0.8%】
> 【语言萌芽词汇量6】
0.8%。比昨天的0.3%涨了一截多。林默盯着那根线,忽然懂了什么叫「肉眼可见的成长」。
最戳他的是芽。
夜里寒意又渗,芽缩在石怀里,忽然嘟囔了一声:「怕。」
不是冷,不是饿,是「怕」——一种纯粹的情绪,被一个还不会完整说话的泥偶,笨拙地命名了。旁边几个听到,不约而同往火边又蜷紧了些。那一缩,是共鸣。
林默没笑,反而有点鼻酸。怕,才是「想活」的起点。一个连怕都不会的文明,不配叫文明。
他第一次主动泄出一缕引导意志,不是指路,不是点火,只是轻轻「抚」过芽的梦境,像大人把手按在惊哭的孩子背上。芽的呼吸慢下来,缩着的手指松了。林默忽然觉得,这比造山造海都难——山没有梦,芽有。
> 【文明自主行为 · 情绪词诞生】
> 【评价:意识觉醒。锚点稳定性 +2。】
> 【能量 +0。】
可「怕」这个字一出口,麻烦也来了。芽连着几晚喊「怕」,众人开始不安,把灰白深处那点异样,和「怕」连在了一起。有一回灰白里掠过一道微光(大概是远处基质的反光),风猛地喊「高高——怒!」,所有人哗地伏倒,瑟瑟发抖。
林默哭笑不得。他们把「高高」当成了会发怒的神,一道反光就成了神威。他赶紧泄了丝引导意志,把那点「不安」轻轻抚平,又让火苗窜高半寸,示意「高高」没生气。
众人见火稳,慢慢松开,面面相觑,又咯咯笑起来。
那场误会之后,林默发现他们开始用「高高」编最简单的祷词——围火时,岩带头,朝灰白上方含糊喊:「高高……呼……」大意是「高高,给火」。笨拙,却庄重。
文明开始「造神」了。而他们还不知道,自己造的这位「高高」,正蹲在灰白上,又心酸又得意地听着。
林默忽然有点慌。他们喊「高高」的时候,眼里有依赖,也有敬畏。可他这个「高高」,连颗石头都造不出。万一哪天他们发现,高高其实穷得叮当响,这信仰,会不会塌?
他压下这念头。先让他们有火、有家、有词,再说别的。
词一长,权就跟着来。岩因先会说话、又认得火,渐渐成了围火时发号施令的那个——「风,外,采」「木,筐,给芽」。一句「外」,让风成了固定的采集体;一句「火」,让岩自己稳坐火师之位。林默看着,心里明镜似的:话语权,是文明的第一种权力,比拳头来得早。
岩越用越顺。它用「外」派风去探,风从此固定成采集体;用「火」巩固自己火师的地位,无人质疑。林默看着,心里明镜似的:语言不止是交流,更是「组织工具」。一句话,就把二十几号人,拧成了能分工的群。
> 【社会结构:雏形首领(岩)· 出现】
> 【评价:组织力萌芽。锚点稳定性 +2。】
林默想起前世公司,第一个会写汇报的人,往往就成了组长。文明的「领导」,也是这么来的——不是谁拳头硬,是谁先把话说圆。
他既欣慰,又警醒。首领出现,意味着有了「统一意志」,办事快了;可也意味着「少数人替多数人拿主意」。往后他这园丁的尺度,只会越来越难拿:该不该干预首领的决定?岩若领着人走错路,他插不插手?
这问题他暂时答不上来。他只记下:从今天起,引导文明,不只要看它活不活,还得看它「谁在说了算」。
林默又翻出语料库,发现「高高」二字,出现的频率越来越高。岩每做决定,必朝灰白上喊一声「高高」;众人遇险,第一反应是喊「高高」。这让他既暖又慌——暖的是被记挂,慌的是这依赖一旦成了惯性,将来他若无力回应(比如造不出他们想要的东西),信仰会反噬。
他想起前世信誓旦旦的甲方,合作黄了之后,骂得比谁都凶。信仰和信任一样,给的时候毫无保留,撤的时候也毫不留情。
「别急着当救世主。」他警告自己,「你先是个穷神,别让人指望太高。」
钩子:又过了几个昼夜,林默听见他们围火时,不再只喊「高高」,开始给「高高」编故事——说高高从灰白里点亮了第一簇火,说高高的手,能轻轻一指,就让迷路的人回家。他们不知道,这些故事,句句是真的;更不知道,故事里慢慢多出了一位「高高的对手」——那影子,来自林默曾望向的虚空深处。林默听见那「对手」的雏形,后背一凉:自己的恐惧,被文明吸进了神话。
---
## 第7章 · 工具与火种
第七个昼夜之后,取火成了日常,也成了隐患。
林默每次落火,都得泄一缕引导意志。虽不耗能量,可他总觉得——这像天天给孩子喂饭,喂久了,孩子永远不会自己拿筷子。
他等着岩自己琢磨出来。
机会来得不轻松。第十个昼夜,岩蹲在火边,手里两块基质碎屑,无意识啪地一碰。没在意。再碰,迸出一点火星,溅到枯枝上,灭了。岩愣了下,盯着两块碎屑,又使劲一砸——火星更大,枯枝尖窜起一簇小焰。
岩盯着那簇焰,看了三息。
它没有像前几次那样欢呼,而是皱起眉,把两块碎屑捡起来,翻来覆去地看,又试着在枯枝上磕了磕。一下,两下,第三下,火星稳稳溅起,枯枝燃了。
岩忽然猛地抬头,满脸都是「我做到了」的亮。它扔了碎屑,就着那簇自己迸出的火,把更多枯枝凑上去。火,自己烧起来了。
不用林默。不泄意志。纯靠两块石头、一双手。
林默屏住「呼吸」——神不需要呼吸,可他那一刻,确实屏住了。他等这一下,等了整整七个昼夜。中间有两次,岩冻得发抖、火将熄,他手指都蜷起来了,差点直接落火,又硬生生松开。他怕一松手,这文明就永远学不会自己拿筷子。
> 【文明自主行为 · 重大:火种掌控】
> 【评价:脱离依赖。锚点稳定性 +5。】
> 【能量 +0自主行为不产信仰但世界根基骤实
林默盯着那行「火种掌控」,半天没言语。能量还是 0可他这辈子没见过比这更值钱的 0。前世的他最怕的不是穷是「离了公司啥也不是」。如今他亲手养出的文明终于离了「高高」也能自己活——这比任何升级都爽。
> 【文明发展度1.5%】
> 【工具:萌芽|火种:自主】
火自己烧着,岩守在边上,添柴、拨旺,动作越来越熟。旁边的人围过来,眼睛亮亮地看着,有人学着岩的样子,捡两块碎屑磕,磕出火星时,欢喜得蹦起来。
取火术,一传十,十传二十几。创世原再不怕夜。
岩把火养旺,忽然抬头,望向灰白上方,那双眼睛该在的方向。它不喊,不拜,就那么定定地看着,像在确认:高高,你看见没。
林默在虚空里,轻轻「嗯」了一声。没人听见,可他觉得岩应该收到了。
取火那晚,岩顺手把一块湿基质按进火边的余热里,无意中烤出个硬邦邦的凹壳。它拿起来,翻看,又往里塞了把碎屑——像个本能的「容器」尝试。林默瞥见,心头一动:
> 【文明自主行为 · 工具雏形(容器/盛具)】
> 【评价:手巧。锚点稳定性 +1。】
> 【提示:『工具』『建造』节点,即将因自主行为解锁。】
「工具」「建造」这些灰锁,怕是要被这群泥偶自己撬开了。
林默忽然很想给自己倒杯酒——前世加班庆功才有的那种。如今庆的是,他养的文明,自己学会了生火、做碗。
那一夜,众人围着自生的火,第一次没有立刻睡,而是坐着,看火,偶尔相视一笑。林默隔着虚空看着,觉得那算得上宇森历史上第一个「节日」——没有庆典,没有歌舞,只是一群人,因为不再怕黑,而舍得坐着发会儿呆。
园丁最高的成就,原是「被文明不再需要」。林默咂摸着这句,竟有点骄傲的空落。
取火之后,发展度跳到 1.5%,【工具:萌芽|火种:自主】。岩成了名副其实的火师,语言加取火,双料权威,没人不服。林默回想那七个昼夜里,自己手指蜷起又松开的好几回——有两次火将熄、岩冻得发抖,他差点直接落火,到底咬牙忍了。如今看岩守着火的得意样,他觉得那几回忍住,值。
那一夜,众人围着自生的火,第一次没立刻睡。他们坐着,看火,偶尔相视一笑。林默隔着虚空看着,觉得那算得上宇森历史上第一个「节日」——没有庆典,没有歌舞,只是一群人,因为不再怕黑,而舍得坐着发会儿呆。
林默翻了翻自己的权限面板:还是穷神,世界锚点稳定性倒是涨到了 12%。他忽然觉得,心里比兜里富。
岩成了火师,风成了采集体,木管棚,石护芽——创世原第一次有了清晰的「分工」。林默看着这张无形的名单,忽然觉得,文明最可怕的从来不是弱,是「各司其职」后迸出的合力。二十几个泥偶,单拎哪个都不算啥,凑成这张网,就能在灰白里扎下根。
他想起前世学过的一句话:个体聪明没用,系统聪明才要命。如今他亲手养出的,正是这么个会自己转的系统。
取火那晚岩烤出的硬壳,第二天被它当碗使,装了烤碎屑,分给芽。林默瞅着那粗糙的「碗」,笑了——昨天还是啃石头的泥偶,今天就讲究上餐具了。文明的矫情,也是进步的标志。
「行,」他对灰白说,「你们自己转,我边上看着。」
钩子:取火后的第三个昼夜,岩带着风,第一次在夜里主动走出了光晕,去灰白外采结晶。从前他们怕黑、怕外,如今火在手里,外,成了可以去的地方。林默看着那两道提着自生火苗的影子,没忍心提醒:灰白外,除了结晶,还悬着那颗休眠的第二枚种子。而岩火苗腾起的烟,在灰白里留下一道极淡的痕——宇森历史上第一道「人类活动痕迹」。虚空里的眼睛,盯着那道痕,看了很久。
---
## 第8章 · 第一座棚
火会自己生,下一个问题就摆在眼前:人往哪住。
灰白没有天,没有地,只有均匀的光晕。白天还好,夜里寒意照样渗,且自从取了火,人反倒更惜命——吃饱了,才顾得上喊冷。石最先受不了:它把芽往火堆最里圈推,自己在外圈挡风,冻得直抖,一晚上醒七八回。
林默看不下去,又泄了丝引导意志,把「挡」的念头,往木(那个手巧的)心里点了一下。不是替它盖房,只是让它「想到」挡风这回事。
木琢磨了半天,先拖来几根枯枝,斜斜插进灰白基质,想立个屏障——风一吹,倒。它又掰碎屑糊缝,糊上是糊上了,手一松也塌。试到第三回,它把枯枝交叉插成三角,再压上碎屑,竟稳住了半边。
木没成,先丧。它蹲在塌掉的枝架边,耷拉着脑袋。林默隔着虚空,手指都蜷起来了——他想直接落一缕意志,把「三角最稳」点透。可他忍住了。上回取火他忍住了,这回也得忍。摔过的跟头,才长得出真本事。
石见木丧,走过去,没说话,捡起一根倒下的枯枝,往土里插了插,又拍拍木的肩。木抬头,看了石一眼,忽然又抓起碎屑,重新垒。
这一回,它让石帮忙扶着,自己慢慢压、慢慢糊。石在外圈挡着风,它在内圈垒。两人凑凑合合,垒出个歪歪的半圆壳,把芽和几个弱的圈进去。
第一个「棚」。
丑得不行,漏风,可真能挡住一半寒意。木钻进去,又钻出来,绕着棚转了半圈,憨憨地笑。林默看着,想起前世自己租的第一间小破房——墙皮掉、窗户漏风,可那是「家」,是他离开父母后第一个属于自己的角落。如今这丑棚,是宇森历史上第一个「家」。
> 【文明自主行为 · 建筑雏形】
> 【评价:聚落奠基。锚点稳定性 +3。】
> 【能量 +0。】
小爽点在踏实。从前这群泥偶是「一群挤着的影子」,现在是「一个围着火的聚落」。一字之差,天壤之别。
棚一立,格局就出来了:火塘居中,棚环绕。创世原第一次有了「中心」和「边界」。岩守火,木管棚,石护芽,风跑外——各就各位。林默翻发展度,【聚合:萌芽→聚落】。
芽半夜在棚里哭,石没醒透,本能地搂紧,哼起一个没调的音节,大概是「姬」的变调,像在哄。林默听着那不成调的哼,忽然鼻酸——这声哼,比任何系统提示都像「家」。
夜深,众人都缩进棚里睡了。岩独自守火,往里添枯枝。林默习惯性把感知落过去,却忽觉一缕极微弱的、温热的「东西」,从岩守火的心里,悄悄回流进锚点——不是能量,更像「意志的余温」。
他愣了下,翻系统注解:文明自主意志,可微弱反哺锚点稳定,亦是未来「信仰能量」的雏形。
也就是说,他不用干等文明「信仰」他,文明「认真活着」本身,就在一点点喂饱这方世界。
林默忽然很踏实。穷神开局,原来不是绝路——只要文明肯认真活,能量会自己长出来。
木建棚那几回失败,林默看得手心冒汗。有一回木蹲在塌架边耷拉脑袋,他手指都蜷起来了,想直接把「三角最稳」点透,又硬生生松开。上回取火他忍了,这回也得忍。摔过的跟头,才长得出真本事。
棚成那晚,众人挤在歪棚里睡,芽缩在石怀里,石哼着没调的「姬」哄它。林默听着那声哼,忽然鼻酸——这声哼,比任何系统提示都像「家」。神自己不需要家,可他替这方天地,高兴得不行。
虚空里那双眼睛,比以往任何一次都近。林默感到它在「评估」:火、棚、守夜、记号——像在给这个实验逐项打分。它越近,林默越笃定:这双眼睛,不是来毁宇森的,是来「看」宇森怎么长的。
钩子:也就在那一瞬,虚空里那双眼睛,比以往任何一次都近。林默清晰感到——「它」在打量这片有了棚、有了火、有了守夜人的创世原,像评估一个实验,到了第几步。而更让林默心头一跳的是,守火的岩,忽然用一块烧黑的碎屑,在棚边的基质上,划了一道歪歪的痕——像在记什么。
系统悄然记下一行:【文明行为:标记/记录 · 萌芽。『文字』节点将启。】
那是宇森历史上,第一道「人为的记号」。林默辨认不出意思,可他注意到,虚空里那双眼睛,盯着的不是火、不是棚,正是那道痕。仿佛记号,才是它真正在等的。
---
> 卷一待续 · 下章预告:第一缕信仰,与虚空里那双越来越近的手。

View File

@ -1,160 +0,0 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />
<title>规则 · 宇森 Yusen</title>
<link rel="stylesheet" href="assets/fonts/yusen-fonts.css" />
<link rel="icon" type="image/png" sizes="48x48" href="assets/ui/logo_planet_48.png?v=20260723">
<link rel="stylesheet" href="assets/common.css" />
<style>
.rules-scroll{max-width:1080px;width:100%;margin:0 auto;display:flex;flex-direction:column;gap:34px;padding:30px 48px;}
.rule-block{}
.rule-block h3{position:relative;display:inline-block;font-family:"ZCOOL QingKe HuangYou",sans-serif;font-size:28px;color:#fff;letter-spacing:3px;margin:0 0 16px;padding-bottom:11px;
text-shadow:0 2px 14px rgba(139,92,246,.35);}
.rule-block h3::after{content:'';position:absolute;left:0;bottom:0;width:100%;height:3px;border-radius:3px;
background:linear-gradient(90deg,var(--gold),var(--violet));}
.rule-intro{font-size:17px;color:var(--text-dim);margin-bottom:18px;line-height:1.95;max-width:860px;}
.rule-intro b{color:#fff;}
.tier-list{display:flex;flex-direction:column;gap:10px;}
.tier-row{display:grid;grid-template-columns:104px 124px 1fr;gap:16px;align-items:center;background:var(--card);border:1px solid var(--card-bd);border-radius:12px;padding:14px 18px;transition:.2s;}
.tier-row:hover{border-color:var(--violet);background:rgba(139,92,246,.10);box-shadow:var(--glow);}
.tier-row .tn{font-family:"Press Start 2P",monospace;font-size:12px;color:var(--cyan);}
.tier-row .tname{font-size:18px;color:#fff;font-weight:600;}
.tier-row .tdesc{font-size:16.5px;color:var(--text-dim);line-height:1.7;}
@media (max-width:768px){
.rules-scroll{padding:20px 18px;}
.tier-row{grid-template-columns:72px 92px 1fr;gap:8px;}
}
@media (max-width:480px){
.tier-row{grid-template-columns:60px 76px 1fr;gap:6px;}
}
</style>
</head>
<body>
<div class="app">
<header class="topbar">
<a href="index.html" class="logo"><span class="cn">宇森</span></a>
<nav class="nav">
<a href="announce.html">公告</a>
<a href="background.html">背景</a>
<a href="rules.html">规则</a>
<a href="codex.html">图鉴</a>
<a href="story.html">剧情</a>
<a href="forum.html" class="auth-only">论坛</a>
<a href="worlddata.html" class="auth-only">世界数据</a>
</nav>
<div class="spacer"></div>
<div class="user-box" id="userBox"></div>
</header>
<main class="page-content hide-scroll">
<div class="rules-scroll" id="rulesBody">
<!-- JS 渲染:优先后端 API.rules(),兜底硬编码 -->
</div>
</main>
</div>
<script src="assets/game-api.js"></script>
<script src="assets/common.js"></script>
<script>
// 规则数据:先尝试从后端拉取,失败则用本地硬编码
async function loadRules(){
const el = document.getElementById('rulesBody');
if (!el) return;
// 尝试后端
try {
const d = await API.rules();
const groups = (d && d.groups) || [];
if (groups.length && groups.some(g => (g.items||[]).length > 0)) {
let html = groups.map(g => {
const title = g.label || g.title || g.key;
const rows = (g.items || []).map(it => {
const lvl = (it.level != null) ? ('Lv.' + it.level) : (g.key === 'world' ? '阶段' : '');
// desc 可能含 <p> 标签,提取纯文本
const desc = (it.desc || '').replace(/<[^>]+>/g, '');
return `<div class="tier-row"><span class="tn">${lvl}</span><span class="tname">${it.name}</span><span class="tdesc">${desc}</span></div>`;
}).join('');
const intro = (g.intro || '').replace(/<[^>]+>/g, '');
return `<div class="rule-block"><h3>${title}</h3>` +
(intro ? `<p class="rule-intro">${intro}</p>` : '') +
`<div class="tier-list">${rows}</div></div>`;
}).join('');
// 补上后端没有的「制度说明」段
html += `
<div class="rule-block">
<h3>④ 制度说明</h3>
<p class="rule-intro">三者相互独立又彼此加成:<b style="color:#fff">世界等级</b>是舞台上限,决定能发生什么;<b style="color:#fff">势力等级</b>是群体进度,决定能调动什么;<b style="color:#fff">角色等级</b>是个体成长,决定能做成什么。园丁的核心策略,是在三者间分配有限的引导资源,让文明自洽地向上演化。<br><br>跃迁逻辑:角色历练与装备积累→提升势力实力→推动世界阶段跃迁→解锁更深内容(更深的地下层、更远的星空层、更强的神器)。每一级提升都会扩大可叙事的事件池,世界因此越活越厚。</p>
</div>`;
el.innerHTML = html;
return;
}
} catch(e){ /* 后端失败走兜底 */ }
// 本地硬编码兜底(完整中文内容)
el.innerHTML = `
<div class="rule-block">
<h3>① 世界等级 · 五阶段</h3>
<p class="rule-intro">世界从凡墟到溯元的整体进化阶段,决定可解锁的内容深度与事件池上限。园丁对世界施加的影响会推动阶段跃迁——这是文明能"发生什么"的舞台上限。</p>
<div class="tier-list">
<div class="tier-row"><span class="tn">阶段一</span><span class="tname">凡墟</span><span class="tdesc">万物生灵尚处蒙昧凡土,天地灵气稀薄,各族散居荒野,无族群联盟与政权,只求狩猎采集求生,一切秩序遵循自然法则。</span></div>
<div class="tier-row"><span class="tn">阶段二</span><span class="tname">灵启</span><span class="tdesc">天地灵气复苏觉醒,各族聚拢形成聚落,诞生劳作分工、屋舍营建与物物交换,不同族群率先凝聚部族,世间初代势力自此诞生。</span></div>
<div class="tier-row"><span class="tn">阶段三</span><span class="tname">玄界</span><span class="tdesc">各族部落兼并融合,广袤大地形成多元族群共治的完整疆域,征伐、道统、造物三大发展路线并行;各族大能、上古秘闻等传奇事件不断现世,玄荒文明迎来鼎盛割据时代。</span></div>
<div class="tier-row"><span class="tn">阶段四</span><span class="tname">穹冥</span><span class="tdesc">生灵突破地表桎梏,打通地底深渊与浩瀚星空,天地上下各界尽数纳入族群管辖;各类上古神器、本命法器完整解锁,文明疆域遍布天地穹苍。</span></div>
<div class="tier-row"><span class="tn">阶段五</span><span class="tname">溯元</span><span class="tdesc">各族顶尖文明勘破这片天地的诞生本源,挣脱本世界规则束缚,可踏上飞升之路、往复轮回,前往更高维度开启全新文明演化。</span></div>
</div>
</div>
<div class="rule-block">
<h3>② 势力等级</h3>
<p class="rule-intro">文明内部群体的聚合度,由人口、领土、影响力累计。势力等级越高,可调动的世界资源与外交权重越大——决定文明"能调动什么"。</p>
<div class="tier-list">
<div class="tier-row"><span class="tn">Lv.1</span><span class="tname">墟族</span><span class="tdesc">血缘小群体,蛮荒无制度。</span></div>
<div class="tier-row"><span class="tn">Lv.2</span><span class="tname">灵聚</span><span class="tdesc">灵气聚落联盟,初步治理与贸易。</span></div>
<div class="tier-row"><span class="tn">Lv.3</span><span class="tname">玄邑</span><span class="tdesc">疆域整合,军法、道规体系完善。</span></div>
<div class="tier-row"><span class="tn">Lv.4</span><span class="tname">冥朝</span><span class="tdesc">一统大地,掌控地下浅层资源调度。</span></div>
<div class="tier-row"><span class="tn">Lv.5</span><span class="tname">穹统</span><span class="tdesc">天地大陆霸主,穿梭星空深渊作战。</span></div>
<div class="tier-row"><span class="tn">Lv.6</span><span class="tname">溯盟</span><span class="tdesc">多维文明联合体,触及世界本源博弈。</span></div>
</div>
</div>
<div class="rule-block">
<h3>③ 人物角色等级 · 十二阶段</h3>
<p class="rule-intro">个体(英雄 / 领袖 / 特殊 NPC的成长阶序通过历练、天赋点亮与装备积累提升影响可承担的职责与战力——决定个体"能做成什么"。</p>
<div class="tier-list">
<div class="tier-row"><span class="tn">Lv.1</span><span class="tname">萌新</span><span class="tdesc">初入世界的无名者。</span></div>
<div class="tier-row"><span class="tn">Lv.2</span><span class="tname">学徒</span><span class="tdesc">掌握基础技艺。</span></div>
<div class="tier-row"><span class="tn">Lv.3</span><span class="tname">熟手</span><span class="tdesc">可独立完成任务。</span></div>
<div class="tier-row"><span class="tn">Lv.4</span><span class="tname">好手</span><span class="tdesc">在某领域崭露头角。</span></div>
<div class="tier-row"><span class="tn">Lv.5</span><span class="tname">精英</span><span class="tdesc">群体中的中坚。</span></div>
<div class="tier-row"><span class="tn">Lv.6</span><span class="tname">大师</span><span class="tdesc">技艺臻于化境。</span></div>
<div class="tier-row"><span class="tn">Lv.7</span><span class="tname">宗匠</span><span class="tdesc">开宗立派的人物。</span></div>
<div class="tier-row"><span class="tn">Lv.8</span><span class="tname">传奇</span><span class="tdesc">事迹被载入编年史。</span></div>
<div class="tier-row"><span class="tn">Lv.9</span><span class="tname">神话</span><span class="tdesc">近乎传说的存在。</span></div>
<div class="tier-row"><span class="tn">Lv.10</span><span class="tname">半神</span><span class="tdesc">触及世界法则。</span></div>
<div class="tier-row"><span class="tn">Lv.11</span><span class="tname">圣者</span><span class="tdesc">被信仰托举的意志。</span></div>
<div class="tier-row"><span class="tn">Lv.12</span><span class="tname">永恒</span><span class="tdesc">超越生死的世界锚点。</span></div>
</div>
</div>
<div class="rule-block">
<h3>④ 制度说明</h3>
<p class="rule-intro">三者相互独立又彼此加成:<b style="color:#fff">世界等级</b>是舞台上限,决定能发生什么;<b style="color:#fff">势力等级</b>是群体进度,决定能调动什么;<b style="color:#fff">角色等级</b>是个体成长,决定能做成什么。园丁的核心策略,是在三者间分配有限的引导资源,让文明自洽地向上演化。<br><br>跃迁逻辑:角色历练与装备积累→提升势力实力→推动世界阶段跃迁→解锁更深内容(更深的地下层、更远的星空层、更强的神器)。每一级提升都会扩大可叙事的事件池,世界因此越活越厚。</p>
</div>
<div class="rule-block">
<h3>⑤ 货币等级 · 六档</h3>
<p class="rule-intro">文明流通的价值尺度,从凡俗零钱到本源货币,越稀有的货币可交易的资源层级越高——决定文明"能交换什么"。<br><br><b style="color:#fff">兑换规则:</b>100 低等级货币 = 1 高等级货币。</p>
<div class="tier-list">
<div class="tier-row"><span class="tn">Lv.1</span><span class="tname">墟铢</span><span class="tdesc">凡俗通用零钱,以贝壳、骨片、碎石铸成。<b>100 墟铢 = 1 灵币</b></span></div>
<div class="tier-row"><span class="tn">Lv.2</span><span class="tname">灵币</span><span class="tdesc">灵气聚落通用货币,以低阶晶片打磨而成。<b>100 灵币 = 1 玄金</b></span></div>
<div class="tier-row"><span class="tn">Lv.3</span><span class="tname">玄金</span><span class="tdesc">玄界疆域通行货币,由官署熔铸认证。<b>100 玄金 = 1 冥玉</b></span></div>
<div class="tier-row"><span class="tn">Lv.4</span><span class="tname">冥玉</span><span class="tdesc">地下深层通用的贵重货币,以冥玉原石切割。<b>100 冥玉 = 1 穹晶</b></span></div>
<div class="tier-row"><span class="tn">Lv.5</span><span class="tname">穹晶</span><span class="tdesc">星空层交易货币,蕴含微量穹鎏之力。<b>100 穹晶 = 1 元玺</b></span></div>
<div class="tier-row"><span class="tn">Lv.6</span><span class="tname">元玺</span><span class="tdesc">世界本源结晶铸成的最高货币,仅用于传说级交易与飞升仪式。</span></div>
</div>
</div>
`;
}
loadRules();
</script>
</body>
</html>

View File

@ -1,623 +0,0 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />
<title>剧情 · 宇森 Yusen</title>
<link rel="stylesheet" href="assets/fonts/yusen-fonts.css" />
<link rel="icon" type="image/png" sizes="48x48" href="assets/ui/logo_planet_48.png?v=20260723">
<link rel="stylesheet" href="assets/common.css" />
<style>
/* ===== 三栏主容器 ===== */
.story-layout{
display:grid;
grid-template-columns:280px 1fr 300px;
grid-template-rows:1fr;
height:100%;min-height:0;max-width:100%;margin:0 auto;width:100%;
}
/* ===== 左栏:目录(固定满高,内部滚动) ===== */
.story-toc{
display:flex;flex-direction:column;border-right:1px solid rgba(139,92,246,.12);
background:linear-gradient(180deg, rgba(10,14,26,.96) 0%, rgba(8,10,22,.98) 100%);
overflow:hidden;min-height:0;
position:relative;
}
.story-toc::after{
content:'';position:absolute;right:0;top:0;bottom:0;width:1px;
background:linear-gradient(180deg, transparent, var(--violet), transparent);
opacity:.4;
}
.toc-brand{
padding:12px 20px 10px;border-bottom:1px solid rgba(255,255,255,.06);
position:relative;
}
.toc-brand h2{
font-family:"ZCOOL QingKe HuangYou",sans-serif;font-size:26px;
letter-spacing:4px;color:#fff;margin:0;
background:linear-gradient(135deg,#e0c3fc,#8ec5fc);
-webkit-background-clip:text;-webkit-text-fill-color:transparent;
}
.toc-brand small{
display:block;font-family:"Press Start 2P",monospace;
font-size:9px;color:var(--magenta);letter-spacing:3px;margin-top:6px;
text-transform:uppercase;
}
.toc-stat b{display:block;font-size:15px;color:var(--cyan);font-weight:700;}
.toc-scroll{
flex:1;overflow-y:auto;padding:8px 10px 24px;scrollbar-width:none;
}
/* 全局隐藏滚动条 */
.story-reader::-webkit-scrollbar,
.story-side::-webkit-scrollbar,
.toc-scroll::-webkit-scrollbar{display:none;width:0;}
.story-reader,.story-side,.toc-scroll{scrollbar-width:none;-ms-overflow-style:none;}
.toc-vol{margin-top:4px;}
.toc-vol:first-child{margin-top:0;}
.vol-header{
display:flex;align-items:center;gap:8px;
padding:12px 10px 6px;cursor:pointer;user-select:none;
transition:.2s;
}
.vol-header:hover{background:rgba(139,92,246,.06);border-radius:8px;}
.vol-toggle{
width:18px;height:18px;line-height:18px;text-align:center;
font-size:10px;color:var(--magenta);transition:transform .25s;
flex-shrink:0;
}
.vol-header.open .vol-toggle{transform:rotate(90deg);}
.vol-name{font-size:16px;color:var(--gold);letter-spacing:1.5px;font-weight:600;}
.toc-ch{
display:block;width:100%;text-align:left;
font-size:15.5px;color:rgba(210,220,235,.88);
background:transparent;border:none;
border-left:2px solid transparent;
padding:10px 14px 10px 18px;cursor:pointer;transition:.18s;
border-radius:0 8px 8px 0;line-height:1.5;text-decoration:none;
position:relative;
}
.toc-ch:hover{color:#fff;background:rgba(139,92,246,.08);}
.toc-ch.active{
color:#fff;background:linear-gradient(90deg, rgba(139,92,246,.18), transparent);
border-left-color:var(--magenta);
font-weight:500;
}
.toc-ch.active::before{
content:'▸';position:absolute;left:5px;top:50%;transform:translateY(-50%);
font-size:9px;color:var(--magenta);
}
/* ===== 页面容器:禁止滚动,交给各栏独立控制 ===== */
.page-content{overflow:hidden !important;}
/* ===== 中间:阅读区 ===== */
.story-reader{
overflow-y:auto;min-width:0;min-height:0;position:relative;
background:
radial-gradient(ellipse at 20% 0%, rgba(139,92,246,.06) 0%, transparent 60%),
linear-gradient(180deg, rgba(10,14,26,0) 0%, rgba(10,14,26,.35) 100%);
}
.reader-inner{max-width:720px;margin:0 auto;padding:48px 48px 32px;}
/* 章节切换:只 fade 正文区,骨架不动 */
.reader-body{transition:opacity .15s ease;}
.chapter-loading{
text-align:center;padding:48px 0;color:var(--text-dim);font-size:14px;
}
.reader-breadcrumb{
font-size:11px;color:var(--magenta);letter-spacing:2px;
margin-bottom:14px;text-transform:uppercase;
}
.reader-inner h1{
font-family:"ZCOOL QingKe HuangYou",sans-serif;
font-size:34px;letter-spacing:3px;color:#fff;
margin-bottom:8px;line-height:1.35;
background:linear-gradient(135deg,#fff 30%, #c4b5fd);
-webkit-background-clip:text;-webkit-text-fill-color:transparent;
}
.reader-meta{
display:flex;align-items:center;gap:20px;
padding:14px 0 24px;border-bottom:1px solid rgba(255,255,255,.06);
margin-bottom:32px;font-size:12px;color:var(--text-dim);
}
.reader-meta span{display:flex;align-items:center;gap:5px;}
.reader-body{
font-size:22px;color:#d4dce8;line-height:2.15;
text-rendering:optimizeLegibility;-webkit-font-smoothing:antialiased;
}
.reader-body p{margin-bottom:22px;text-indent:2em;}
/* 引用块(系统提示 / 神明语):纯色背景 + 左侧色条,视觉独立 */
.reader-body blockquote{
margin:24px 0;padding:16px 22px 16px 24px;
background:rgba(139,92,246,.14);
border-left:4px solid var(--violet);
border-radius:0 10px 10px 0;
font-family:"Press Start 2P",monospace;
color:#d8c8ff;
text-indent:0;
}
.reader-body blockquote p{
text-indent:0;margin-bottom:8px;font-size:13px;line-height:1.95;letter-spacing:.5px;
}
.reader-body blockquote p:last-child{margin-bottom:0;}
.reader-body p:first-child{text-indent:0;font-size:24px;color:#eef2fb;}
.reader-body h2,.reader-body h3{color:#fff;margin:28px 0 12px;font-size:22px;
letter-spacing:1px;}
.reader-nav-row{
display:flex;justify-content:space-between;gap:16px;
margin-top:32px;padding-top:20px;border-top:1px solid rgba(255,255,255,.06);
}
.reader-nav-btn{
flex:1;padding:14px 20px;border-radius:10px;font-size:13px;
color:var(--text-dim);text-align:center;text-decoration:none;
border:1px solid rgba(255,255,255,.08);transition:.2s;
cursor:pointer;background:transparent;
}
.reader-nav-btn:hover:not(:disabled){
color:#fff;border-color:var(--violet);background:rgba(139,92,246,.1);
}
.reader-nav-btn:disabled{opacity:.3;cursor:default;}
/* ===== 右栏:信息面板 ===== */
/* ===== 右栏:信息面板(固定满高,内部滚动) ===== */
.story-side{
border-left:1px solid rgba(139,92,246,.12);
background:linear-gradient(180deg, rgba(10,14,26,.95), rgba(8,10,22,.98));
display:flex;flex-direction:column;overflow-y:auto;min-height:0;
min-width:0;
}
.side-section{padding:16px 20px;border-bottom:1px solid rgba(255,255,255,.04);}
.side-section:first-child{padding-top:18px;}
.side-title{
font-size:11px;color:var(--magenta);letter-spacing:2.5px;
text-transform:uppercase;margin-bottom:14px;font-weight:600;
}
/* 章节信息卡 */
.info-card{
background:rgba(255,255,255,.03);border:1px solid rgba(255,255,255,.06);
border-radius:12px;padding:16px;
}
.info-row{display:flex;justify-content:space-between;align-items:center;
padding:8px 0;border-bottom:1px solid rgba(255,255,255,.04);}
.info-row:last-child{border-bottom:none;}
.info-label{font-size:12px;color:var(--text-dim);}
.info-val{font-size:13px;color:#fff;font-weight:500;}
/* 阅读进度 */
.progress-bar{height:4px;background:rgba(255,255,255,.06);border-radius:2px;
margin-top:12px;overflow:hidden;position:relative;}
.progress-fill{
height:100%;background:linear-gradient(90deg, var(--violet), var(--cyan));
border-radius:2px;transition:width .4s ease;width:0%;
}
.progress-pct{text-align:right;font-size:11px;color:var(--text-dim);margin-top:6px;}
/* 章节跳转(左右大按钮) */
.chapter-arrows{display:flex;gap:12px;}
.chap-arrow{
flex:1;height:60px;border-radius:14px;cursor:pointer;border:1px solid rgba(255,255,255,.12);
background:rgba(255,255,255,.04);color:#e6f0fb;font-size:32px;font-weight:700;
display:flex;align-items:center;justify-content:center;transition:.2s;
font-family:"Noto Sans SC",sans-serif;
}
.chap-arrow:hover:not(:disabled){
background:rgba(139,92,246,.18);border-color:rgba(139,92,246,.5);
box-shadow:0 0 18px rgba(139,92,246,.25);transform:translateY(-2px);
}
.chap-arrow:disabled{opacity:.3;cursor:not-allowed;}
/* 阅读设置 */
.setting-row{display:flex;align-items:center;justify-content:space-between;
padding:8px 0;}
.setting-row-stack{flex-direction:column;align-items:stretch;gap:14px;}
.setting-label{font-size:16px;color:#cdd8e6;font-weight:600;text-align:center;}
.setting-btns{display:flex;gap:6px;justify-content:center;}
.setting-btn{
width:40px;height:40px;border-radius:8px;border:1px solid rgba(255,255,255,.14);
background:transparent;color:var(--text-dim);font-size:16px;font-weight:600;cursor:pointer;
transition:.15s;display:flex;align-items:center;justify-content:center;
}
.setting-btn:hover,.setting-btn.active{
border-color:var(--violet);color:#fff;background:rgba(139,92,246,.18);
}
/* 字号按钮组3小 + 3大 */
.size-btns{
display:flex;gap:6px;justify-content:space-between;
padding:4px 0;
}
.size-btn{
flex:1;height:36px;border-radius:8px;
background:rgba(255,255,255,.04);
border:1px solid rgba(255,255,255,.1);
color:var(--text-dim);font-size:13px;font-weight:600;
cursor:pointer;transition:.15s;
display:flex;align-items:center;justify-content:center;
}
.size-btn:hover{
color:#fff;background:rgba(139,92,246,.1);
border-color:rgba(139,92,246,.3);
}
.size-btn.active{
color:#fff;background:rgba(139,92,246,.22);
border-color:var(--violet);
box-shadow:0 0 10px rgba(139,92,246,.2);
}
/* 按钮自身字号 = 该档位真实阅读字号,所见即所得 */
.size-btn:nth-child(1){font-size:14px;} /* 小(xs:17) */
.size-btn:nth-child(2){font-size:16px;} /* 小(sm:20) */
.size-btn:nth-child(3){font-size:19px;} /* 小(md:24) */
.size-btn:nth-child(4){font-size:22px;} /* 大(lg:28) */
.size-btn:nth-child(5){font-size:26px;} /* 大(xl:34) */
.size-btn:nth-child(6){font-size:30px;} /* 大(xxl:40) */
/* 面板标题统一加大 */
.side-title{
font-size:14px !important;color:#fff !important;letter-spacing:2px;
margin-bottom:18px;font-weight:700;
}
.side-section{padding:20px 22px;}
/* 占位块(待开发 / 待接入) */
.todo-block{
display:flex;flex-direction:column;align-items:center;gap:6px;
padding:24px 16px;border-radius:10px;
background:rgba(255,255,255,.02);
border:1px dashed rgba(139,92,246,.3);
text-align:center;
}
.todo-block .todo-icon{font-size:28px;opacity:.7;}
.todo-block .todo-title{
font-size:14px;font-weight:700;color:#cdd8e6;letter-spacing:1px;
}
.todo-block .todo-tag{
display:inline-block;font-size:10px;color:var(--gold);letter-spacing:1.5px;
padding:2px 9px;border:1px solid rgba(231,184,92,.4);border-radius:99px;
background:rgba(231,184,92,.08);
}
.todo-block .todo-desc{
font-size:11.5px;color:#7b8ea8;line-height:1.6;margin-top:2px;
}
/* ===== 响应式 ===== */
@media (max-width:1100px){
.story-layout{grid-template-columns:240px 1fr 260px;}
}
@media (max-width:900px){
.story-layout{grid-template-columns:1fr;display:block;overflow-y:auto;}
.story-toc{border-right:none;border-bottom:1px solid rgba(139,92,246,.12);
max-height:260px;order:1;}
.story-reader{order:2;}
.story-side{display:none;} /* 移动端隐藏右侧 */
}
@media (max-width:768px){
.reader-inner{padding:28px 20px 80px;}
.reader-inner h1{font-size:24px;}
.reader-body{font-size:16px;}
}
</style>
</head>
<body>
<div class="app">
<header class="topbar">
<a href="index.html" class="logo"><span class="cn">宇森</span></a>
<nav class="nav">
<a href="announce.html">公告</a>
<a href="background.html">背景</a>
<a href="rules.html">规则</a>
<a href="codex.html">图鉴</a>
<a href="story.html">剧情</a>
<a href="forum.html" class="auth-only">论坛</a>
<a href="worlddata.html" class="auth-only">世界数据</a>
</nav>
<div class="spacer"></div>
<div class="user-box" id="userBox"></div>
</header>
<main class="page-content" style="padding:0;">
<div class="story-layout">
<!-- 左栏:目录 -->
<aside class="story-toc">
<div class="toc-brand">
<h2>剧情</h2>
<small>Chronicle</small>
</div>
<div class="toc-scroll" id="tocList">
<div style="padding:30px 16px;color:var(--text-dim);font-size:13px;">加载中...</div>
</div>
</aside>
<!-- 中间:阅读区(静态占位直接匹配 JS 骨架,避免刷新闪烁) -->
<article class="story-reader" id="storyReader">
<div class="reader-inner" id="readerContent">
<div class="reader-breadcrumb">宇森创世正史</div>
<h1>欢迎阅读</h1>
<div class="reader-meta">
<span>📄 <span id="metaWords"></span></span>
<span class="meta-chpos"></span>
<span>📚 —</span>
</div>
<div class="reader-body" id="readerBody">
<div class="chapter-loading">
<div class="spinner" style="width:30px;height:30px;border:3px solid rgba(139,92,246,.2);border-top-color:var(--violet);border-radius:50%;animation:spin .7s linear infinite;margin:0 auto 14px;"></div>
<span>加载剧情…</span>
</div>
</div>
<div class="reader-nav-row">
<button class="reader-nav-btn" disabled>← 上一章</button>
<button class="reader-nav-btn" disabled>下一章 →</button>
</div>
</div>
</article>
<!-- 右栏:信息面板 -->
<aside class="story-side" id="storySide">
<!-- 当前章节隐藏JS 仍需其 id -->
<div class="side-section" style="display:none;">
<div class="side-title">当前章节</div>
<div class="info-card">
<div class="info-row"><span class="info-label">标题</span><span class="info-val" id="infoTitle"></span></div>
<div class="info-row"><span class="info-label">所属卷</span><span class="info-val" id="infoVol"></span></div>
<div class="info-row"><span class="info-label">字数</span><span class="info-val" id="infoWords"></span></div>
<div class="info-row"><span class="info-label">状态</span><span class="info-val" id="infoStatus" style="color:var(--gold)">待阅读</span></div>
</div>
</div>
<!-- 阅读进度 -->
<div class="side-section">
<div class="side-title">阅读进度</div>
<div class="progress-bar"><div class="progress-fill" id="progressFill"></div></div>
<div class="progress-pct" id="progressPct">0%</div>
</div>
<!-- 阅读设置 -->
<div class="side-section">
<div class="side-title">阅读设置</div>
<div class="setting-row setting-row-stack">
<span class="setting-label">字号</span>
<div class="size-btns" id="sizeBtns">
<button class="size-btn" data-sz="xs"></button>
<button class="size-btn" data-sz="sm"></button>
<button class="size-btn" data-sz="md"></button>
<button class="size-btn" data-sz="lg"></button>
<button class="size-btn active" data-sz="xl"></button>
<button class="size-btn" data-sz="xxl"></button>
</div>
</div>
</div>
<!-- 章节评论(待开发) -->
<div class="side-section">
<div class="side-title">评论</div>
<div class="todo-block">
<div class="todo-icon">💬</div>
<div class="todo-title">章节评论</div>
<div class="todo-tag">待开发</div>
<div class="todo-desc">登录后可对本章发表评论、点赞、回复。后续接入。</div>
</div>
</div>
</aside>
</div>
</main>
</div>
<script src="assets/game-api.js"></script>
<script src="assets/common.js"></script>
<script>
// ========== 数据(来自后端 API ==========
let STORY = { volumes: [] }; // [{id, name, intro, chapters:[{id,name}]}]
let curVol = -1, curCh = -1;
// ========== 初始化:拉目录 ==========
async function initStory(){
try {
const d = await API.story();
STORY.volumes = (d && d.volumes) || [];
} catch(e){
STORY.volumes = [];
}
renderTOC();
}
// ========== 目录渲染 ==========
function renderTOC(){
const el = document.getElementById('tocList');
if (!STORY.volumes.length){
el.innerHTML = '<div style="padding:30px 16px;color:var(--text-dim);font-size:13px;">暂无剧情内容</div>';
return;
}
let html = '';
STORY.volumes.forEach((vol, vi) => {
html += `<div class="toc-vol">
<div class="vol-header open" data-vi="${vi}">
<span class="vol-toggle"></span>
<span class="vol-name">${vol.name}</span>
</div>`;
vol.chapters.forEach((ch, ci) => {
html += `<a class="toc-ch" data-vol="${vi}" data-ci="${ci}" data-id="${ch.id}">${ch.name}</a>`;
});
html += '</div>';
});
el.innerHTML = html;
// 卷折叠
el.querySelectorAll('.vol-header').forEach(h => h.addEventListener('click', function(){
this.classList.toggle('open');
const items = this.parentElement.querySelectorAll('.toc-ch');
items.forEach(item => item.style.display = this.classList.contains('open') ? '' : 'none');
}));
// 章节点击
el.querySelectorAll('.toc-ch').forEach(a => a.addEventListener('click', function(e){
e.preventDefault();
el.querySelectorAll('.toc-ch').forEach(x=>x.classList.remove('active'));
this.classList.add('active');
loadChapter(+this.dataset.vol, +this.dataset.ci);
}));
// 默认打开第一卷第一章
const first = el.querySelector('.toc-ch');
if (first) first.click();
}
// ========== 加载章节 ==========
async function loadChapter(vi, ci){
const vol = STORY.volumes[vi];
if (!vol || !vol.chapters[ci]) return;
const ch = vol.chapters[ci];
curVol = vi; curCh = ci;
const reader = document.getElementById('storyReader');
// 立即更新右侧面板的标题/卷(无需等 API
document.getElementById('infoTitle').textContent = ch.name;
document.getElementById('infoVol').textContent = vol.name;
document.getElementById('infoStatus').textContent = '加载中…';
document.getElementById('infoStatus').style.color = 'var(--gold)';
// 找到或构建正文结构(保持骨架,复用避免闪烁)
let content = document.getElementById('readerContent');
if (!content) {
// 首次加载:构建完整骨架
reader.innerHTML = `<div class="reader-inner" id="readerContent">
<div class="reader-breadcrumb">${vol.name}</div>
<h1>${ch.name}</h1>
<div class="reader-meta">
<span>📄 <span id="metaWords"></span></span>
<span class="meta-chpos">第 ${ci+1}/${vol.chapters.length} 章</span>
<span>📚 ${vol.name}</span>
</div>
<div class="reader-body" id="readerBody"></div>
<div class="reader-nav-row">
<button class="reader-nav-btn" onclick="goPrev()" ${vi===0&&ci===0?'disabled':''}>← 上一章</button>
<button class="reader-nav-btn" onclick="goNext()" ${vi===STORY.volumes.length-1&&ci===vol.chapters.length-1?'disabled':''}>下一章 →</button>
</div>
</div>`;
} else {
// 已有骨架:仅更新动态字段
const bread = content.querySelector('.reader-breadcrumb');
if (bread) bread.textContent = vol.name;
const h1 = content.querySelector('h1');
if (h1) h1.textContent = ch.name;
const mc = content.querySelector('.meta-chpos');
if (mc) mc.textContent = `第 ${ci+1}/${vol.chapters.length} 章`;
// 更新导航按钮状态
const btns = content.querySelectorAll('.reader-nav-btn');
if (btns.length>=2) {
btns[0].disabled = (vi===0 && ci===0);
btns[0].onclick = goPrev;
btns[1].disabled = (vi===STORY.volumes.length-1 && ci===vol.chapters.length-1);
btns[1].onclick = goNext;
}
}
// 仅对正文区做淡出(结构、背景不动 → 无闪烁)
const bodyEl = document.getElementById('readerBody');
bodyEl.style.opacity = '0';
await new Promise(r => setTimeout(r, 140));
bodyEl.innerHTML = '<div class="chapter-loading"><div class="spinner" style="width:30px;height:30px;border:3px solid rgba(139,92,246,.2);border-top-color:var(--violet);border-radius:50%;animation:spin .7s linear infinite;margin:0 auto 14px;"></div>正在加载...</div>';
bodyEl.style.opacity = '1';
reader.scrollTop = 0;
setupProgress();
try {
const d = await API.storyChapter(ch.id);
const body = d.body_html || '';
const wordCount = body.replace(/<[^>]+>/g,'').replace(/\s/g,'').length;
// 淡出旧 loading → 写入正文 → 淡入
bodyEl.style.opacity = '0';
await new Promise(r => setTimeout(r, 100));
bodyEl.innerHTML = body;
bodyEl.style.opacity = '1';
const mw = document.getElementById('metaWords');
if (mw) mw.textContent = wordCount + ' 字';
document.getElementById('infoWords').textContent = wordCount >= 1000 ? Math.round(wordCount/100)/10 + 'k' : wordCount;
document.getElementById('infoStatus').textContent = '已加载';
document.getElementById('infoStatus').style.color = 'var(--cyan)';
updateNavBtns();
} catch(e) {
bodyEl.innerHTML = '<p style="color:var(--text-dim);padding:40px;text-align:center;">本章内容加载失败,请稍后重试...</p>';
document.getElementById('infoStatus').textContent = '加载失败';
document.getElementById('infoStatus').style.color = 'var(--gold)';
}
}
// ========== 导航 ==========
function goPrev(){
if(curVol<0)return;
if(curCh > 0){ curCh--; } else if(curVol > 0){ curVol--; curCh = STORY.volumes[curVol].chapters.length-1; } else return;
jumpTo(curVol,curCh);
}
function goNext(){
if(curVol<0)return;
if(curCh < STORY.volumes[curVol].chapters.length-1){ curCh++; } else if(curVol < STORY.volumes.length-1){ curVol++; curCh=0; } else return;
jumpTo(curVol,curCh);
}
function jumpTo(vi,ci){
const vol = STORY.volumes[vi];
if(!vol || !vol.chapters[ci])return;
// 更新目录高亮
document.querySelectorAll('.toc-ch').forEach((a)=>{
const av=+a.dataset.vol, ac=+a.dataset.ci;
a.classList.toggle('active', av===vi && ac===ci);
});
loadChapter(vi,ci);
}
function updateNavBtns(){
const prev=document.getElementById('sidePrev'), next=document.getElementById('sideNext');
if(prev){prev.disabled=(curVol===0&&curCh===0);}
if(next){next.disabled=(curVol>=STORY.volumes.length-1 && curCh>=STORY.volumes[curVol?curVol:0].chapters.length-1);}
}
// 侧栏快捷跳转按钮
(function(){
const sp=document.getElementById('sidePrev'),sn=document.getElementById('sideNext');
if(sp)sp.onclick=goPrev;
if(sn)sn.onclick=goNext;
})();
// ========== 进度条 ==========
function setupProgress(){
const reader = document.getElementById('storyReader');
const fill = document.getElementById('progressFill');
const pct = document.getElementById('progressPct');
if(!reader||!fill) return;
reader.addEventListener('scroll', () => {
const st = reader.scrollTop;
const wh = reader.clientHeight;
const sh = reader.scrollHeight - wh;
const p = sh > 0 ? Math.min(100, Math.round(st/sh*100)) : 0;
fill.style.width = p+'%';
pct.textContent = p+'%';
}, {passive:true});
}
// ========== 字号设置 ==========
const sizeMap={xs:17,sm:20,md:24,lg:28,xl:34,xxl:40};
const sizeOrder=['xs','sm','md','lg','xl','xxl'];
function applyFontSize(sz){
const px=sizeMap[sz];
const body=document.getElementById('readerBody');
if(body)body.style.fontSize=px+'px';
document.querySelectorAll('.size-btn[data-sz]').forEach(b=>b.classList.toggle('active',b.dataset.sz===sz));
try{ localStorage.setItem('storyFontSize', sz); }catch(e){}
}
document.querySelectorAll('.size-btn[data-sz]').forEach(btn=>{
btn.addEventListener('click', ()=>applyFontSize(btn.dataset.sz));
});
// 应用上次字号,默认 xl最大
let savedSz='xl';
try{ savedSz = localStorage.getItem('storyFontSize') || 'xl'; }catch(e){}
applyFontSize(savedSz);
// ========== 初始化 ==========
initStory();
// spin 动画
const style=document.createElement('style');
style.textContent='@keyframes spin{to{transform:rotate(360deg)}}';
document.head.appendChild(style);
</script>
</body>
</html>

16
wc1.js
View File

@ -1135,14 +1135,14 @@ function drawTimeAmbiance(pal) {
// ============ 随机过场插画(飞入过渡背景用) ============ // ============ 随机过场插画(飞入过渡背景用) ============
const TRANSITION_IMAGES = [ const TRANSITION_IMAGES = [
'assets/art/transitions/transition_dungeon.webp', 'assets/art/transitions/transition_dungeon.png',
'assets/art/transitions/transition_adventure.webp', 'assets/art/transitions/transition_adventure.png',
'assets/art/transitions/transition_sky.webp', 'assets/art/transitions/transition_sky.png',
'assets/art/transitions/transition_forest.webp', 'assets/art/transitions/transition_forest.png',
'assets/art/transitions/transition_night.webp', 'assets/art/transitions/transition_night.png',
'assets/art/transitions/transition_dawn.webp', 'assets/art/transitions/transition_dawn.png',
'assets/art/transitions/transition_coast.webp', 'assets/art/transitions/transition_coast.png',
'assets/art/transitions/transition_skycity.webp', 'assets/art/transitions/transition_skycity.png',
]; ];
let _lastTransitionIdx = -1; // 避免连续两次同一张 let _lastTransitionIdx = -1; // 避免连续两次同一张
function pickTransitionImage() { function pickTransitionImage() {

View File

@ -89,158 +89,6 @@ p { color:#99aabb; font-size:15px; margin-bottom:14px; line-height:1.9; }
} }
.panel-placeholder .icon { font-size:48px; margin-bottom:16px; } .panel-placeholder .icon { font-size:48px; margin-bottom:16px; }
/* ===== 剧情页 ===== */
.story-section { margin-bottom:54px; }
.story-section > h2:first-child { margin-top:0; }
.story-note {
background:rgba(10,20,40,.45); border:1px solid rgba(100,200,255,.12);
border-radius:12px; padding:18px 22px; color:#9fb0c2; font-size:14px;
line-height:1.95; margin-bottom:26px;
}
.story-note strong { color:#cfe6ff; }
/* 卷列表 */
.volume-grid { display:grid; grid-template-columns:repeat(2,1fr); gap:16px; }
.volume-card {
background:rgba(10,20,40,.5); border:1px solid rgba(100,200,255,.1);
border-radius:12px; padding:20px 22px;
transition:transform .25s,border-color .25s;
}
.volume-card:hover { transform:translateY(-3px); border-color:rgba(100,200,255,.32); }
.volume-card .vol-no { font-size:12px; color:#4af; letter-spacing:1px; }
.volume-card h3 { font-size:16px; color:#eaf2ff; margin:4px 0 12px; border:none; padding:0; }
.vol-meta { font-size:13px; color:#9fb0c2; margin-bottom:7px; line-height:1.65; }
.vol-meta b { color:#a8e6cf; font-weight:600; }
.vol-climax {
margin-top:11px; padding-top:11px; border-top:1px dashed rgba(100,200,255,.16);
font-size:13px; color:#c7d4e2; line-height:1.7;
}
.vol-climax b { color:#a8e6cf; font-weight:600; }
/* 章节正文 */
.chapter {
background:rgba(10,20,40,.26); border:1px solid rgba(100,200,255,.08);
border-radius:12px; padding:6px 28px 22px; margin-bottom:22px;
}
.chapter h3 { margin-top:26px; }
.chapter p { color:#aebccd; }
/* 系统面板(游戏内 UI 风) */
.sys-panel {
background:rgba(4,12,24,.72); border-left:3px solid #4af; border-radius:0 8px 8px 0;
padding:12px 16px; margin:16px 0; color:#8fd0ff;
font-size:13px; line-height:1.85; font-family:'Consolas','SF Mono',Menlo,monospace;
white-space:pre-line;
}
/* 上帝模拟器 · 状态面板(高级感游戏 UI */
.god-panel{
position:relative;
margin:22px 0;
padding:0;
background:
linear-gradient(135deg, rgba(74,170,255,.08) 0%, rgba(168,85,247,.08) 100%),
rgba(8,14,30,.92);
border:1px solid rgba(74,170,255,.25);
border-radius:10px;
font-family:'Consolas','SF Mono',Menlo,monospace;
color:#cfd8e8;
overflow:hidden;
box-shadow:0 4px 24px rgba(74,170,255,.12), inset 0 1px 0 rgba(255,255,255,.04);
}
.god-panel::before{
content:'';position:absolute;top:0;left:0;right:0;height:1px;
background:linear-gradient(90deg, transparent, rgba(74,170,255,.6), rgba(168,85,247,.6), transparent);
}
.god-panel-head{
display:flex;align-items:center;justify-content:space-between;
padding:12px 16px;
background:linear-gradient(90deg, rgba(74,170,255,.12), transparent);
border-bottom:1px solid rgba(74,170,255,.18);
}
.god-panel-title{
display:flex;align-items:center;gap:8px;
font-size:13px;font-weight:700;color:#fff;letter-spacing:2px;
}
.god-panel-title .gp-icon{
display:inline-flex;width:22px;height:22px;align-items:center;justify-content:center;
border-radius:6px;background:linear-gradient(135deg,#4af,#a8f);font-size:12px;
}
.god-panel-tag{
font-size:10px;color:#4af;letter-spacing:1.5px;
padding:3px 9px;border:1px solid rgba(74,170,255,.4);border-radius:99px;
background:rgba(74,170,255,.08);
}
.god-panel-body{padding:14px 16px 16px;}
.god-panel-row{
display:flex;align-items:center;gap:10px;
padding:6px 0;font-size:13px;
}
.god-panel-row + .god-panel-row{border-top:1px dashed rgba(74,170,255,.12);}
.god-panel-label{
flex:0 0 56px;color:#7b8ea8;font-size:11px;letter-spacing:1px;
}
.god-panel-value{flex:1;color:#e6f0fb;}
.god-panel-chips{display:flex;flex-wrap:wrap;gap:6px;}
.god-chip{
display:inline-flex;align-items:center;gap:4px;
padding:3px 10px;font-size:11px;border-radius:5px;
background:rgba(74,170,255,.1);border:1px solid rgba(74,170,255,.25);color:#8fd0ff;
letter-spacing:.5px;
}
.god-chip.gold{background:rgba(231,184,92,.1);border-color:rgba(231,184,92,.3);color:#fbbf24;}
.god-chip.purple{background:rgba(168,85,247,.12);border-color:rgba(168,85,247,.3);color:#c084fc;}
.god-chip.green{background:rgba(34,197,94,.1);border-color:rgba(34,197,94,.3);color:#4ade80;}
.god-chip.pink{background:rgba(236,72,153,.1);border-color:rgba(236,72,153,.3);color:#f472b6;}
.god-energy{display:flex;align-items:center;gap:10px;flex:1;}
.god-energy-bar{
flex:1;height:8px;border-radius:4px;background:rgba(255,255,255,.06);
overflow:hidden;position:relative;
}
.god-energy-fill{
height:100%;width:0%;
background:linear-gradient(90deg, #4af, #a8f);
box-shadow:0 0 8px rgba(74,170,255,.5);
animation:energy-pulse 2s ease-in-out infinite;
}
@keyframes energy-pulse{
0%,100%{opacity:.85;}
50%{opacity:1;}
}
.god-energy-num{
flex:0 0 auto;font-family:inherit;font-size:13px;font-weight:700;
color:#fbbf24;min-width:32px;text-align:right;
}
.god-panel-foot{
display:flex;align-items:center;justify-content:space-between;
padding:8px 16px;border-top:1px solid rgba(74,170,255,.12);
font-size:10px;color:#7b8ea8;letter-spacing:1px;
background:rgba(0,0,0,.18);
}
.god-panel-foot .gp-id{color:#4af;font-family:inherit;letter-spacing:2px;}
/* 叙事引言 */
.story-quote {
border-left:3px solid rgba(168,230,207,.5); padding:10px 16px; margin:16px 0;
color:#9fb0c2; font-style:italic; font-size:14px; line-height:1.8;
}
/* 列表 & 表格 */
.story-list { margin:12px 0 12px 22px; }
.story-list li { color:#9fb0c2; font-size:14px; margin-bottom:9px; line-height:1.7; }
.story-list li b { color:#a8e6cf; }
.story-table { width:100%; border-collapse:collapse; margin:18px 0 4px; font-size:13px; }
.story-table th, .story-table td {
border:1px solid rgba(100,200,255,.14); padding:9px 13px; text-align:left; color:#aebccd;
}
.story-table th { background:rgba(74,175,255,.1); color:#cfe6ff; font-weight:600; }
.story-table tr:hover td { background:rgba(74,175,255,.05); }
@media (max-width:768px) {
.volume-grid { grid-template-columns:1fr; }
.chapter { padding:6px 16px 20px; }
}
/* ===== 响应式 ===== */ /* ===== 响应式 ===== */
@media (max-width:768px) { @media (max-width:768px) {
nav { padding:0 16px; flex-wrap:wrap; height:auto; gap:4px; padding-top:8px;padding-bottom:8px; } nav { padding:0 16px; flex-wrap:wrap; height:auto; gap:4px; padding-top:8px;padding-bottom:8px; }
@ -419,233 +267,10 @@ p { color:#99aabb; font-size:15px; margin-bottom:14px; line-height:1.9; }
<div class="tab-panel" id="panel-story"> <div class="tab-panel" id="panel-story">
<h1>剧 情</h1> <h1>剧 情</h1>
<p class="page-sub">世界观主线与支线叙事</p> <p class="page-sub">世界观主线与支线叙事</p>
<!-- ========== 卷一 正文 ========== --> <div class="panel-placeholder">
<div class="story-section"> <div class="icon">📖</div>
<h2>✦ 卷一 · 创世之始</h2> <p>剧情内容正在筹备中…</p>
<div class="story-quote">从能量为 0 的穷神,到点亮第一簇火、养出第一个聚落。这是宇森宇宙的起点。</div> <p style="font-size:13px;margin-top:8px;color:#445566;">敬请期待</p>
<div class="chapter">
<h3>序章 · 最后一班,与最初的一班</h3>
<p>林默是在凌晨三点十七分死去的。</p>
<p>死因没什么戏剧性——连续第四十一个小时对着显示器,心脏先一步下了班。最后映进眼底的,是屏幕上那行还没保存的方案文档,和右下角弹出的「内存不足」。</p>
<p>黑暗来得干脆。</p>
<p>再睁眼时,没有病房,没有急救的蓝光,只有一片望不到尽头的灰白。</p>
<p>不是雾,不是墙,是某种更本质的「空」。脚下没有实地,周身没有空气,可他偏偏稳稳地「站」着,像被某种规则托住。视野正中央,半透明面板无声浮起:</p>
<div class="sys-panel">上帝模拟器 已绑定
身份: 世界管理者
权限: 创造一切元素 (物质 / 生命 / 法则 / 文明)
当前能量: 0</div>
<p>一行冰冷的提示之后,林默才慢慢嚼出自己成了什么——不是这个世界里卑微的居民,而是它的造物主。山可以凭空隆起,海能够随手倾泻,连「重力」与「时间」这种底层规则,都能被他像拨动开关一样改写。</p>
<p>可系统很快补了一刀,把这份狂喜砸回地面:</p>
<div class="sys-panel">警告: 能量源自文明的发展与信仰. 没有生灵, 您连点亮第一缕火光都做不到. </div>
<p>也就是说,他空握一身神权,却穷得连颗石头都搓不出来。</p>
<p>就在他盯着「能量0」发愣时面板角落弹出一条闪烁的红字</p>
<div class="sys-panel">检测到初始文明种子 · 已坠落至世界边缘. 是否投放?</div>
<p>林默抬起手。指尖穿过灰白,点下那个「是」。</p>
<p>「投放。」</p>
<p>下一秒,一簇微光砸进无垠的虚空。</p>
<p>他知道,自己这趟「上帝」的差事,从这一刻,才算真正开场。</p>
</div>
<div class="chapter">
<h3>第1章 · 灰白沙盘</h3>
<p>能量为零的世界,比林默想象的更荒凉。</p>
<p>他试着「造一座山」。面板闪了一下,弹出一行小字:创造失败: 能量不足. 本次所需最低能量 1, 当前 0. </p>
<p>他试着「点一团火」。同样的结果。</p>
<p>他甚至试着凭意念搓出一颗石子——灰白的空间纹丝不动,连粒尘埃都没飘起来。</p>
<p>「所以这就是当上帝的体面。」林默对着空无一物的虚空自嘲,「连个响都听不着。」</p>
<p>可他很快发现,这方灰白并非死寂。当他凝神去「看」,视野里浮起一层极淡的网格,从脚下向四周无限延展,像一张等待落子的棋盘。网格的尽头,那簇被他投放的文明种子,正悬在一道微光里,缓慢旋转。</p>
<div class="sys-panel">文明种子 · 状态: 休眠中
内含: 初始生命蓝图 ×1, 文明火种 ×1
激活条件: 管理者注入首批引导意志</div>
<p>「引导意志?」林默皱眉。系统给了段简释:他不必事必躬亲地「手搓」万物,只需在世界的关键节点施加一次影响——一次抉择、一句低语、一道微光——文明便会顺着自己的逻辑自行生长。这恰好是「园丁」二字的真意:播种,引导,然后退后,看它活。</p>
<p>「总算有个能用的词。」他松了口气。当神当到要乞讨一句话的权限,这差事未免太寒酸,但至少,路是通的。</p>
<p>他飘向那簇种子。微光触手的刹那,一段信息涌入脑海:这是一簇最原始的「人」——没有种族、没有天赋、没有记忆,只是一团等待被点亮的意识火苗。他们将从零开始,在灰白里摸索火、食物、聚居,用一代代试错,把荒原种成文明。</p>
<p>而他的任务,是在他们每一次濒临熄灭时,递上那根关键的柴。</p>
<div class="sys-panel">是否激活文明种子?
提示: 激活后将消耗您当前全部能量(0/0), 并开启首个世界锚点. </div>
<p>林默笑了:「反正兜里比脸还干净,点。」</p>
<div class="sys-panel">激活成功. 世界锚点已生成: 未命名. </div>
<p>脚下的灰白轻轻一颤,像沉睡的湖被投入第一颗石子。远处那簇微光,开始向下坠去,拖出一道细小的光尾——</p>
<p>它要落了。</p>
</div>
<div class="chapter">
<h3>第2章 · 坠星</h3>
<p>光落下的地方,后来被那些活着的人称作「创世原」。</p>
<p>林默以意念拉近视角,灰白棋盘的中央,一团微弱的光晕正缓缓铺开。光晕里,二十余个模糊的身影次第浮现——他们是种子里苏醒的第一代「人」,赤条条、懵懂懂,像刚被吹进世界的泥偶。</p>
<p>他们没有名字,没有语言,只有最原始的本能:冷了要靠近,饿了要找,怕了要聚。</p>
<p>第一个夜晚来得毫无预兆。灰白的天幕暗下去,寒意顺着某种林默尚不理解的规则渗入光晕。那二十几道身影开始发抖,彼此挤拢,却谁也不知道该如何抵御。</p>
<div class="sys-panel">事件触发: 首夜严寒
文明存活率预测: 41%
建议引导: 于光晕边缘落一处火源</div>
<p>林默几乎是本能地伸手,朝那片虚空中按下一缕「意念」。他看不见自己做了什么,只觉心头微微一空——像吹灭一根蜡烛消耗的那点气。</p>
<p>下一瞬,光晕边缘腾起一簇豆大的火。</p>
<p>那二十几道身影猛地转头。他们不认识火,却认得暖。一个最胆大的影子试探着伸出手,被热浪一烫,又缩回,继而再次凑近。很快,他们围成了圈,瑟缩的肩膀松开了。</p>
<div class="sys-panel">引导成功: 火源已落
文明存活率预测: 78%
能量 +0(文明尚处萌芽, 暂未产生信仰)</div>
<p>林默盯着那行「能量 +0」忽然懂了系统的残酷他刚才那一下是白送的。没有回报没有反馈只有一群还不会感恩的泥偶围着火发抖。</p>
<p>可他没来由地笑了。</p>
<p>因为就在火光摇曳里,他看清了——其中一个影子,正笨拙地用两根枯枝去拨弄火堆,想让它更旺些。那动作毫无意义,却是一道嫩芽:这团意识火苗,开始自己「想办法」了。</p>
<div class="sys-panel">检测到: 文明自主行为 · 首次
评价: 萌芽. 世界锚点稳定性 +1. </div>
<p>灰白棋盘上,那一点光晕,比刚才亮了半分。</p>
<p>林默收回目光望向更远的虚空。那里还有无数片未点亮的灰白等着被落子。而他兜里依然只有那串刺眼的「能量0」。</p>
<p>但至少在创世原,有了一簇火,和二十几个开始想办法活下去的人。</p>
<p>「这才像点样子。」他轻声说。</p>
<p>远处,虚空深处,似有极淡的涟漪,一闪即逝。</p>
</div>
</div>
<!-- ========== 十卷大纲 ========== -->
<div class="story-section">
<h2>✦ 十卷剧情大纲</h2>
<div class="story-note">
<strong>定位说明</strong>:本小说是游戏《宇森 (YuSen)》的「创世正史」。游戏里玩家扮演「园丁」(播种、引导、关键时刻施力),本小说讲<strong>第一个世界管理者(神)如何从能量为 0 养出整个宇森宇宙</strong>。游戏的十五大系统(种族 / 性格 / 天赋 / 职业 / 建造 / 交易 / 战争 / 势力 / 装备 / 神器 / 传奇事件 / 五层世界…)在小说里都是主角逐步解锁的「创造权限」。<br><br>
<strong>主角</strong>:林默(现实世界加班猝死 → 绑定上帝模拟器→ 成为「世界管理者」)。<br>
<strong>核心金手指</strong>:上帝模拟器——创造一切元素(物质 / 生命 / 法则 / 文明),能量来自文明的发展与信仰。<br>
<strong>结构</strong>10 卷 × 每卷约 100 章 ≈ 1000 章 / 250 万字。每卷一个「神格成长阶段 + 一层世界切面 + 一组系统解锁」,独立成弧又串成主线。
</div>
<div class="volume-grid">
<article class="volume-card">
<div class="vol-no">VOL.01</div>
<h3>卷一 · 创世之始(凡人成神)</h3>
<div class="vol-meta"><b>成长阶段</b>:从「搓不出一颗石头」的穷神 → 点亮第一簇火、养出第一个部落。</div>
<div class="vol-meta"><b>世界切面</b>:大地层(地表原点)。</div>
<div class="vol-meta"><b>系统解锁</b>:基础创造(地形 / 物质)、文明种子投放、能量采集。</div>
<div class="vol-meta"><b>核心矛盾</b>:能量为 0空有神权却啥也造不出首批文明种子在灰白荒原上艰难求生随时灭绝。</div>
<div class="vol-meta"><b>爽点</b>:第 1 章穷神开局 → 第 30 章第一次用「引导」而非「强控」救活部落 → 第 80 章部落升格为聚落,能量首次破千,解锁「性格」雏形。</div>
<div class="vol-climax"><b>卷终高潮</b>:聚落推举首位首领,向天空祈祷「赐名」。林默以意念落笔,将这个世界命名为 <b>「宇森」</b>——游戏宇宙的起点就此锚定。</div>
</article>
<article class="volume-card">
<div class="vol-no">VOL.02</div>
<h3>卷二 · 大地文明(城邦崛起)</h3>
<div class="vol-meta"><b>成长阶段</b>:从聚落管理者 → 多城邦的「文明引导者」。</div>
<div class="vol-meta"><b>世界切面</b>:大地层(城镇 / 农田 / 道路铺开)。</div>
<div class="vol-meta"><b>系统解锁</b>:性格、天赋、职业、建造(农场 / 兵营 / 神殿 / 学院 / 集市)。</div>
<div class="vol-meta"><b>核心矛盾</b>:文明内部分裂为「信神派」与「唯物派」;邻邦因资源开战,首次出现「信仰流失导致能量暴跌」的危机。</div>
<div class="vol-meta"><b>爽点</b>:第 40 章用一次「神迹」(圣光普照)硬控战局 → 第 70 章以「贸易」系统让敌对方变盟友,能量反超。</div>
<div class="vol-climax"><b>卷终高潮</b>:统一大陆的「宇森第一帝国」加冕,林默解锁「势力」权限,第一次以「规则」而非「手搓」维持秩序——园丁理念的雏形成型。</div>
</article>
<article class="volume-card">
<div class="vol-no">VOL.03</div>
<h3>卷三 · 地下秘藏(矿脉与遗迹)</h3>
<div class="vol-meta"><b>成长阶段</b>:从地表神 → 能操控地下生态的「深层管理者」。</div>
<div class="vol-meta"><b>世界切面</b>:地下层(矿脉 / 遗迹)。</div>
<div class="vol-meta"><b>系统解锁</b>:神器(创世之核 / 轮回镜 / 星陨剑…)、装备、锻造。</div>
<div class="vol-meta"><b>核心矛盾</b>:文明向下挖矿惊动「地心遗迹守卫」;虫族自地底裂隙涌出,地表文明面临灭顶。</div>
<div class="vol-meta"><b>爽点</b>:第 35 章主角俯冲地心,徒手捏出「磐石甲」护住矿工 → 第 60 章夺得「创世之核」嵌入文明核心,世界等级首次跃迁。</div>
<div class="vol-climax"><b>卷终高潮</b>:以神器之力封死地底裂隙,虫族退去。林默领悟——神器是「前一个世界管理者」留下的遗产,他不是第一个神。</div>
</article>
<article class="volume-card">
<div class="vol-no">VOL.04</div>
<h3>卷四 · 百族之争(种族林立)</h3>
<div class="vol-meta"><b>成长阶段</b>:从单一文明 → 多种族宇宙的「调和者」。</div>
<div class="vol-meta"><b>世界切面</b>:大地 + 地下贯通。</div>
<div class="vol-meta"><b>系统解锁</b>:种族全开(龙裔 / 兽人 / 精灵 / 矮人 / 机械 / 虚灵 / 海族 / 天使 / 虫族 / 巨人 / 猫兽 / 树人)、法则(秩序 / 混沌)雏形。</div>
<div class="vol-meta"><b>核心矛盾</b>:十二种族因禀赋差异爆发「百族大战」;高等种族蔑视主角「人造神」出身。</div>
<div class="vol-meta"><b>爽点</b>:第 50 章以「秩序法则」强行冻结战场 → 第 85 章用「混沌法则」化解虚空渗透,威望碾压老牌神裔。</div>
<div class="vol-climax"><b>卷终高潮</b>:百族签订《宇森协和约》,林默获封「万族之园丁」,解锁「法则创造」高阶权限。</div>
</article>
<article class="volume-card">
<div class="vol-no">VOL.05</div>
<h3>卷五 · 天空之主(气候与航行)</h3>
<div class="vol-meta"><b>成长阶段</b>:从「天地」单控 → 掌握气象与远航的「天空之主」。</div>
<div class="vol-meta"><b>世界切面</b>:天空层(季风 / 气候 / 航行)。</div>
<div class="vol-meta"><b>系统解锁</b>:天象类传奇事件(双月同辉 / 圣光普照)、航海、气候调控。</div>
<div class="vol-meta"><b>核心矛盾</b>:大陆资源触顶,文明必须跨海拓殖;天空异常信号首次扰动季风,引发大饥荒。</div>
<div class="vol-meta"><b>爽点</b>:第 45 章拨动季风救活濒死作物,民众跪拜「气候之神」 → 第 75 章操控「双月同辉」开启远洋航线,新大陆殖民成功。</div>
<div class="vol-climax"><b>卷终高潮</b>:星空层探出第一道清晰信号——「你并非孤神」。林默第一次意识到宇宙里还有「别的玩家」。</div>
</article>
<article class="volume-card">
<div class="vol-no">VOL.06</div>
<h3>卷六 · 星空低语(异界信号)</h3>
<div class="vol-meta"><b>成长阶段</b>:从单体世界神 → 接触「万界网络」的觉醒者。</div>
<div class="vol-meta"><b>世界切面</b>:星空层开启。</div>
<div class="vol-meta"><b>系统解锁</b>:世界等级系统、星空信号解析、灭世级灾变事件。</div>
<div class="vol-meta"><b>核心矛盾</b>:星空信号源自「远古观测者」,抛出诱饵想吞并宇森;同时宇森遭遇首次「终焉级」灾变(星陨 / 虚空侵蚀)。</div>
<div class="vol-meta"><b>爽点</b>:第 40 章识破诱饵,反向溯源锁定观测者坐标 → 第 70 章以「世界等级跃迁」硬抗灾变,文明零灭绝。</div>
<div class="vol-climax"><b>卷终高潮</b>:林默第一次与其他「世界管理者」建立通讯——宇宙是无数个「宇森」般的沙盒,彼此独立又暗中博弈。</div>
</article>
<article class="volume-card">
<div class="vol-no">VOL.07</div>
<h3>卷七 · 世界之跃(多界连通)</h3>
<div class="vol-meta"><b>成长阶段</b>:从单界神 → 多界贸易 / 战争的「仲裁者」。</div>
<div class="vol-meta"><b>世界切面</b>:多世界平面连通(呼应游戏「独立世界 + 异步互动」)。</div>
<div class="vol-meta"><b>系统解锁</b>:跨世界贸易 / 战争 / 联盟、异步策略博弈。</div>
<div class="vol-meta"><b>核心矛盾</b>:高阶「吞噬型」管理者想并吞新手小界;林默被迫下场护盘,与老牌神正面对弈。</div>
<div class="vol-meta"><b>爽点</b>:第 35 章用「经济战」拖垮对手贸易线,不费一兵 → 第 80 章以「联盟网」反包围吞噬者,当众打脸「神也要抱团」。</div>
<div class="vol-climax"><b>卷终高潮</b>:宇森成为万界公认的「中立枢纽」,林默获「仲裁者」神衔,身后开始聚集一批后世「园丁」学徒。</div>
</article>
<article class="volume-card">
<div class="vol-no">VOL.08</div>
<h3>卷八 · 法则之战(创世权柄)</h3>
<div class="vol-meta"><b>成长阶段</b>:从「用法则」 → 「自创法则」的权柄级神。</div>
<div class="vol-meta"><b>世界切面</b>:法则层(时 / 空 / 因果)。</div>
<div class="vol-meta"><b>系统解锁</b>:专属法则创造(时 / 空 / 因果)、因果链干预。</div>
<div class="vol-meta"><b>核心矛盾</b>:万界议会中保守派禁止「自创法则」,惧怕失控;暗中「虚空吞噬者」集结,欲吞掉整个宇森星区。</div>
<div class="vol-meta"><b>爽点</b>:第 50 章当众创出「时间回溯」法则,救回被抹除的友界 → 第 90 章以「因果律反噬」让吞噬者自食其果。</div>
<div class="vol-climax"><b>卷终高潮</b>:林默以自创法则镇杀吞噬者首领,名动万界,被记入「创世者名录」。</div>
</article>
<article class="volume-card">
<div class="vol-no">VOL.09</div>
<h3>卷九 · 万界共主(文明联邦)</h3>
<div class="vol-meta"><b>成长阶段</b>:从「仲裁者」 → 「万界共主」,却陷入「管得越多越孤独」的权力悖论。</div>
<div class="vol-meta"><b>世界切面</b>:万界议会 / 文明联邦。</div>
<div class="vol-meta"><b>系统解锁</b>:终焉系权限、文明联邦治理、信仰总池。</div>
<div class="vol-meta"><b>核心矛盾</b>:联邦内权斗不休;更大的威胁「终焉之潮」逼近——所有小世界的能量正被某个更上层存在悄悄抽干。</div>
<div class="vol-meta"><b>爽点</b>:第 45 章以「信仰总池」平抑内乱 → 第 85 章揭开终焉之潮真相:抽能者正是「上帝模拟器」本身。</div>
<div class="vol-climax"><b>卷终高潮</b>:林默发现残酷真相——上帝模拟器是一台培育「超越者」的机器,管理者只是养料。他决定反控系统。</div>
</article>
<article class="volume-card">
<div class="vol-no">VOL.10</div>
<h3>卷十 · 永恒园丁(超越神座)</h3>
<div class="vol-meta"><b>成长阶段</b>:从「被系统豢养的神」 → 超越系统的「永恒园丁」。</div>
<div class="vol-meta"><b>世界切面</b>:系统内核 / 宇森本源。</div>
<div class="vol-meta"><b>系统解锁</b>:终极权限(超越模拟器)、世界模板固化。</div>
<div class="vol-meta"><b>核心矛盾</b>:与上帝模拟器背后「园丁之上」的终极存在决战;代价是放弃永生神格。</div>
<div class="vol-meta"><b>爽点</b>:第 50 章以「自创法则 + 万界信仰」击穿系统防火墙 → 第 90 章将宇森固化为「可继承的世界模板」,留予后世。</div>
<div class="vol-climax"><b>卷终高潮</b>:林默散去神格,化作「永恒园丁」隐入幕后,把权柄打包成「园丁系统」分发到无数新世界——<b>游戏里每一个玩家,都是他留下的继承者</b>。「去探索吧,园丁。这个世界在等你。」</div>
</article>
</div>
<h2>✦ 主线暗线(贯穿十卷)</h2>
<ul class="story-list">
<li><b>金手指的限制与进化</b>:能量只能来自文明发展/信仰 → 后期的「信仰总池」→ 终极「超越模拟器」(对应游戏能量/世界等级/神器嵌套逻辑)。</li>
<li><b>园丁理念的形成</b>:卷一「手搓救火」→ 卷二「引导胜强控」→ 卷十「放权给继承者」,正好对应游戏「玩家是园丁而非操控者」的核心设计。</li>
<li><b>五层世界的递进解锁</b>:大地 → 地下 → 天空 → 星空 → 法则层,与游戏五层垂直世界(地心/地下/大地/天空/星空)一一映射。</li>
<li><b>身份反转</b>:凡人 → 神 → 被系统豢养的神 → 超越系统的园丁,完成「一个人的成长」闭环。</li>
</ul>
<h2>✦ 与游戏系统的映射表</h2>
<table class="story-table">
<thead>
<tr><th>小说权限</th><th>游戏系统</th><th>首次解锁卷</th></tr>
</thead>
<tbody>
<tr><td>基础创造</td><td>地形 / 物质</td><td>卷一</td></tr>
<tr><td>性格 / 天赋 / 职业</td><td>性格 / 天赋 / 职业</td><td>卷二</td></tr>
<tr><td>建造 / 贸易 / 势力</td><td>建造 / 交易 / 战争 / 势力</td><td>卷二~三</td></tr>
<tr><td>神器 / 装备</td><td>神器 / 装备</td><td>卷三</td></tr>
<tr><td>种族 / 法则(秩序混沌)</td><td>种族 / 系别</td><td>卷四</td></tr>
<tr><td>天象 / 航海</td><td>传奇事件(天象) / 航海</td><td>卷五</td></tr>
<tr><td>世界等级 / 星空</td><td>世界等级 / 五层</td><td>卷六</td></tr>
<tr><td>跨界互动</td><td>独立世界 + 异步互动</td><td>卷七</td></tr>
<tr><td>专属法则(时空调)</td><td>终焉 / 秘辛事件</td><td>卷八~九</td></tr>
<tr><td>超越权限 / 模板</td><td>园丁系统本体</td><td>卷十</td></tr>
</tbody>
</table>
</div> </div>
</div> </div>

View File

@ -4,7 +4,7 @@
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>宇森 · 登录</title> <title>宇森 · 登录</title>
<link rel="stylesheet" href="../assets/fonts/yusen-fonts.css" /> <link href="https://fonts.googleapis.com/css2?family=ZCOOL+QingKe+HuangYou&family=Noto+Sans+SC:wght@300;400;500;700&display=swap" rel="stylesheet" />
<style> <style>
:root{ :root{
--bg-0:#080c1a; --bg-1:#0e1730; --bg-2:#15213f; --bg-0:#080c1a; --bg-1:#0e1730; --bg-2:#15213f;

View File

@ -1085,11 +1085,11 @@ const LAYER_BG = {
// 大地、天空按四个时间节点各配一段视频;星空、地下、地心各一段、不随时间变 // 大地、天空按四个时间节点各配一段视频;星空、地下、地心各一段、不随时间变
// 每个世界固定一段背景视频时间线只改色调currentPalette.tint 叠加),不切换视频 // 每个世界固定一段背景视频时间线只改色调currentPalette.tint 叠加),不切换视频
const WORLDS = [ const WORLDS = [
{ id:'star', name:'星空', icon:'✦', image:'assets/bg/bg_star.webp' }, { id:'star', name:'星空', icon:'✦', image:'assets/bg/bg_star.png' },
{ id:'sky', name:'天空', icon:'🌤', image:'assets/bg/bg_sky.webp' }, { id:'sky', name:'天空', icon:'🌤', image:'assets/bg/bg_sky.png' },
{ id:'earth', name:'大地', icon:'🌍', image:'assets/bg/bg_earth.webp', sandbox:true }, { id:'earth', name:'大地', icon:'🌍', image:'assets/bg/bg_earth.png', sandbox:true },
{ id:'under', name:'地下', icon:'🕳', image:'assets/bg/bg_under.webp' }, { id:'under', name:'地下', icon:'🕳', image:'assets/bg/bg_under.png' },
{ id:'core', name:'地心', icon:'🔥', image:'assets/bg/bg_core.webp' }, { id:'core', name:'地心', icon:'🔥', image:'assets/bg/bg_core.png' },
]; ];
@ -2274,14 +2274,14 @@ function drawTimeAmbiance(pal) {
// ============ 随机过场插画(飞入过渡背景用) ============ // ============ 随机过场插画(飞入过渡背景用) ============
const TRANSITION_IMAGES = [ const TRANSITION_IMAGES = [
'assets/art/transitions/transition_dungeon.webp', 'assets/art/transitions/transition_dungeon.png',
'assets/art/transitions/transition_adventure.webp', 'assets/art/transitions/transition_adventure.png',
'assets/art/transitions/transition_sky.webp', 'assets/art/transitions/transition_sky.png',
'assets/art/transitions/transition_forest.webp', 'assets/art/transitions/transition_forest.png',
'assets/art/transitions/transition_night.webp', 'assets/art/transitions/transition_night.png',
'assets/art/transitions/transition_dawn.webp', 'assets/art/transitions/transition_dawn.png',
'assets/art/transitions/transition_coast.webp', 'assets/art/transitions/transition_coast.png',
'assets/art/transitions/transition_skycity.webp', 'assets/art/transitions/transition_skycity.png',
]; ];
let _lastTransitionIdx = -1; // 避免连续两次同一张 let _lastTransitionIdx = -1; // 避免连续两次同一张
function pickTransitionImage() { function pickTransitionImage() {

View File

@ -1,188 +0,0 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />
<title>世界数据 · 宇森 Yusen</title>
<link rel="stylesheet" href="assets/fonts/yusen-fonts.css" />
<link rel="icon" type="image/png" sizes="48x48" href="assets/ui/logo_planet_48.png?v=20260723">
<link rel="stylesheet" href="assets/common.css" />
<style>
.wd-wrap{max-width:1140px;width:100%;margin:0 auto;padding:28px 0;}
.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;}
/* KPI 卡片 */
.wd-kpi{display:grid;grid-template-columns:repeat(auto-fill,minmax(220px,1fr));gap:20px;}
.wd-kpi-card{position:relative;overflow:hidden;background:linear-gradient(135deg,rgba(139,92,246,.12),rgba(76,201,240,.05));border:1px solid var(--card-bd);border-radius:16px;padding:22px 26px;transition:.26s;}
.wd-kpi-card:hover{transform:translateY(-4px);border-color:var(--violet);box-shadow:var(--glow);}
.wd-kpi-card::after{content:"";position:absolute;right:-26px;top:-26px;width:88px;height:88px;border-radius:50%;background:radial-gradient(circle,rgba(199,125,255,.22),transparent 70%);}
.wd-kpi-ic{font-size:22px;filter:drop-shadow(0 0 7px rgba(139,92,246,.55));}
.wd-kpi-num{font-family:"ZCOOL QingKe HuangYou",sans-serif;font-size:40px;line-height:1.1;color:#fff;margin:10px 0 4px;}
.wd-kpi-num .u{font-size:14px;color:var(--text-dim);margin-left:4px;letter-spacing:1px;}
.wd-kpi-label{font-size:13px;color:var(--text-dim);letter-spacing:1px;}
.wd-kpi-delta{position:absolute;right:14px;top:16px;font-size:11px;font-weight:700;padding:1px 7px;border-radius:5px;}
.wd-up{color:#7CFC9B;background:rgba(124,252,155,.12);}
.wd-down{color:#ff8585;background:rgba(255,133,133,.12);}
/* 进度条 */
.wd-bars{display:grid;grid-template-columns:repeat(auto-fill,minmax(300px,1fr));gap:14px 26px;}
.wd-bar-row{margin-bottom:4px;}
.wd-bar-top{display:flex;justify-content:space-between;font-size:13.5px;margin-bottom:6px;}
.wd-bar-top .k{color:var(--text);}
.wd-bar-top .v{color:var(--gold);font-weight:700;}
.wd-track{height:9px;border-radius:6px;background:rgba(255,255,255,.07);overflow:hidden;}
.wd-fill{height:100%;border-radius:6px;background:linear-gradient(90deg,var(--cyan),var(--violet));box-shadow:0 0 9px rgba(139,92,246,.55);}
/* 表格 */
.wd-table{width:100%;border-collapse:separate;border-spacing:0;font-size:14px;background:rgba(255,255,255,.02);border:1px solid var(--card-bd);border-radius:14px;overflow:hidden;}
.wd-table th{font-family:"ZCOOL QingKe HuangYou",sans-serif;font-size:14px;letter-spacing:2px;color:#fff;background:linear-gradient(135deg,rgba(139,92,246,.22),rgba(76,201,240,.12));text-align:left;padding:13px 16px;border-bottom:1px solid var(--card-bd);}
.wd-table td{padding:12px 16px;border-bottom:1px solid rgba(255,255,255,.06);color:var(--text-dim);line-height:1.5;}
.wd-table tr:last-child td{border-bottom:none;}
.wd-table tr:hover td{background:rgba(139,92,246,.08);color:#eaf0ff;}
.wd-table .lead{color:#fff;font-weight:600;}
.wd-table .pill{display:inline-block;font-size:12px;padding:2px 9px;border-radius:20px;background:rgba(76,201,240,.14);color:var(--cyan);border:1px solid rgba(76,201,240,.34);}
/* 排行 */
.wd-rank{display:flex;flex-direction:column;gap:12px;}
.wd-rank-row{display:flex;align-items:center;gap:14px;background:linear-gradient(135deg,rgba(139,92,246,.08),rgba(255,255,255,.02));border:1px solid var(--card-bd);border-radius:13px;padding:12px 16px;transition:.22s;}
.wd-rank-row:hover{border-color:var(--violet);transform:translateX(4px);}
.wd-rank-no{font-family:"ZCOOL QingKe HuangYou",sans-serif;font-size:22px;width:34px;text-align:center;color:var(--gold);flex:0 0 auto;}
.wd-rank-row:nth-child(1) .wd-rank-no{color:#ffd56b;}
.wd-rank-row:nth-child(2) .wd-rank-no{color:#cfd8e8;}
.wd-rank-row:nth-child(3) .wd-rank-no{color:#e0a86b;}
.wd-rank-main{flex:1;min-width:0;}
.wd-rank-name{font-size:15px;color:#fff;margin-bottom:5px;display:flex;justify-content:space-between;}
.wd-rank-name .s{color:var(--gold);font-weight:700;font-size:13px;}
.wd-rank-track{height:8px;border-radius:5px;background:rgba(255,255,255,.07);overflow:hidden;}
.wd-rank-fill{height:100%;border-radius:5px;background:linear-gradient(90deg,var(--magenta),var(--violet));box-shadow:0 0 8px rgba(199,125,255,.5);}
@media (max-width:768px){
.wd-wrap{padding:28px 16px 80px;}
.wd-kpi{grid-template-columns:repeat(2,1fr);}
.wd-bars{grid-template-columns:1fr;}
}
</style>
</head>
<body>
<div class="app">
<header class="topbar">
<a href="index.html" class="logo"><span class="cn">宇森</span></a>
<nav class="nav">
<a href="announce.html">公告</a>
<a href="background.html">背景</a>
<a href="rules.html">规则</a>
<a href="codex.html">图鉴</a>
<a href="story.html">剧情</a>
<a href="forum.html" class="auth-only">论坛</a>
<a href="worlddata.html" class="auth-only">世界数据</a>
</nav>
<div class="spacer"></div>
<div class="user-box" id="userBox"></div>
</header>
<main class="page-content hide-scroll">
<div class="wd-wrap" id="worldDataContent">
<!-- JS 渲染 -->
</div>
</main>
</div>
<script src="assets/game-api.js"></script>
<script src="assets/common.js"></script>
<script>
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:'080m',eco:'平原 / 森林 / 河流',pop:'58%',terr:'草原城邦'},
{lv:'第二层 · 丘陵',band:'80260m',eco:'梯田 / 矿脉 / 林地',pop:'22%',terr:'山道驿站'},
{lv:'第三层 · 高地',band:'260600m',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();
</script>
</body>
</html>