docs: add design document workspace

This commit is contained in:
daixiawu 2026-05-26 16:10:16 +08:00
parent 19cf0ae820
commit 792081cb82
22 changed files with 2332 additions and 2 deletions

14
DOC/README.md Normal file
View File

@ -0,0 +1,14 @@
# DOC
`DOC` stores project operation data and documents that are consumed by the local dashboard.
Keep these data files here unless `Tools/Dashboard/serve.py` is updated at the same time:
- `bugs.json`
- `devplan.json`
- `marketing/*.json`
- `sns/*.json`
Marketing drafts, Steam announcement copies, public-opinion incident notes, and dashboard-backed task data also belong here.
Gameplay design documents should live under `Design/gameplay/`. Architecture and engineering documents should live under `MD/`.

13
Design/README.md Normal file
View File

@ -0,0 +1,13 @@
# Design
`Design` is the source area for TH1 design documents. Prefer HTML for durable design documents so they can be opened directly and embedded in the Dashboard.
## Structure
- `final/mechanics/`: completed or accepted game-mechanic designs.
- `final/narrative/`: completed or accepted worldbuilding, story, copywriting, and flavor text designs.
- `drafts/`: work in progress, discussion notes, temporary records, and planning material.
- `art/`: art direction and asset planning.
- `audio/`: music and sound direction.
Dashboard-backed operational data belongs in `DOC/`. Engineering architecture belongs in `MD/`.

View File

@ -0,0 +1,37 @@
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>TH1 核心循环</title>
<link rel="stylesheet" href="/api/design/shared/design-doc.css">
</head>
<body>
<main class="doc-shell narrow">
<header class="doc-hero compact">
<p class="eyebrow">Mechanics / Core</p>
<h1>核心循环</h1>
<p class="lead">核心循环必须同时服务玩家、AI、联机和回放。任何能改变战局的设计最终都要落到可验证的行动、数据变更和表现反馈。</p>
</header>
<section class="doc-section">
<h2>回合结构</h2>
<ol class="rule-list">
<li><strong>回合开始:</strong>刷新城市产出、技能状态、可行动单位和回合事件。</li>
<li><strong>玩家决策:</strong>移动、攻击、训练、建造、研发、外交、城市升级。</li>
<li><strong>行动执行:</strong>构造参数、校验合法性、进入 ActionLogic 执行并广播事件。</li>
<li><strong>表现播放:</strong>动画、特效、UI 和 Renderer 根据事件与 Presentation 队列更新。</li>
<li><strong>回合结束:</strong>处理持续状态、AI 队列、胜利条件、存档/上传等外围逻辑。</li>
</ol>
</section>
<section class="doc-section">
<h2>设计约束</h2>
<div class="callout-grid">
<article><h3>规则要短</h3><p>每个行动最好能用一句话解释,同时允许技能层叠形成深度。</p></article>
<article><h3>状态要可见</h3><p>恐惧、满月、金刚身、粗糙身等状态必须在 UI、伤害预览和战斗表现中一致。</p></article>
<article><h3>行为要同步</h3><p>联机、AI 和玩家输入都不应绕过共享 Action 层。</p></article>
</div>
</section>
<footer class="doc-footer"><a href="index.html">返回机制总览</a></footer>
</main>
</body>
</html>

View File

@ -0,0 +1,36 @@
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>TH1 设计原则</title>
<link rel="stylesheet" href="/api/design/shared/design-doc.css">
</head>
<body>
<main class="doc-shell principles-shell">
<section class="principles-page">
<header class="principles-header">
<p class="eyebrow">Design Principles</p>
<h1>产品原则</h1>
</header>
<ol class="principles-list">
<li>
<span class="principle-index">01</span>
<strong>东方 Project 角色展示优先</strong>
</li>
<li>
<span class="principle-index">02</span>
<strong>保守设计,以 Polytopia 为基本原则,小步迭代,不在一次设计内做大幅度创新</strong>
</li>
<li>
<span class="principle-index">03</span>
<strong>极简,任意时刻让玩家保持 3 个及以下的决策选项</strong>
</li>
</ol>
<footer class="doc-footer"><a href="index.html">返回设计树</a></footer>
</section>
</main>
</body>
</html>

View File

@ -0,0 +1,33 @@
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>TH1 阵营与文明系统</title>
<link rel="stylesheet" href="/api/design/shared/design-doc.css">
</head>
<body>
<main class="doc-shell narrow">
<header class="doc-hero compact">
<p class="eyebrow">Mechanics / Factions</p>
<h1>阵营与文明系统</h1>
<p class="lead">TH1 的文明基底继承 Polytopia 的地图、城市和科技结构;势力层负责东方主题、英雄名单、特色单位和专属机制。</p>
</header>
<section class="doc-section">
<h2>两层模型</h2>
<table class="doc-table">
<thead><tr><th>层级</th><th>负责内容</th><th>设计风险</th></tr></thead>
<tbody>
<tr><td>文明 Civ</td><td>城市名、基础资源倾向、起始科技、地图适应性。</td><td>如果差异过大,会破坏 Polytopia 式简洁。</td></tr>
<tr><td>势力 Force</td><td>东方角色、阵营主题、英雄、特色单位、专属建筑和技能。</td><td>如果技能过多,会造成不可读和难平衡。</td></tr>
</tbody>
</table>
</section>
<section class="doc-section">
<h2>当前规模</h2>
<p>Dashboard 摘要导出显示当前包含 17 个玩家阵营、20 名英雄、55 项科技。第一版设计框架先记录系统关系,后续再逐个阵营补全完整设计页。</p>
</section>
<footer class="doc-footer"><a href="index.html">返回机制总览</a></footer>
</main>
</body>
</html>

View File

@ -0,0 +1,183 @@
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>TH1 英雄基础定位</title>
<link rel="stylesheet" href="/api/design/shared/design-doc.css">
</head>
<body>
<main class="doc-shell hero-foundation-shell">
<header class="doc-hero compact">
<p class="eyebrow">Game Mechanics / Foundation / Heroes</p>
<h1>英雄</h1>
</header>
<section class="doc-section">
<h2>1. 基础定位:职阶设计</h2>
<div class="class-grid">
<article class="class-card class-xiang">
<div class="class-rank"></div>
<h3>探索型</h3>
<p>开局任务:探索。核心收益在探索信息和机动展开,适合无防主发展。</p>
</article>
<article class="class-card class-hou">
<div class="class-rank"></div>
<h3>防守探索型</h3>
<p>开局任务:探索。兼具探索与防御,能支撑无防或有防主发展。</p>
</article>
<article class="class-card class-wang">
<div class="class-rank"></div>
<h3>发展防御型</h3>
<p>开局任务:特色。发展能力高,适合有防主发展或防御型主战争。</p>
</article>
<article class="class-card class-che">
<div class="class-rank"></div>
<h3>正面战争型</h3>
<p>开局任务:伤害。防御和进攻都强,适合主战争策略。</p>
</article>
<article class="class-card class-ma">
<div class="class-rank"></div>
<h3>扩张突击型</h3>
<p>开局任务:扩张。进攻与移动突出,适合主动扩张和压迫。</p>
</article>
</div>
</section>
<section class="doc-section">
<h2>职阶定位矩阵</h2>
<div class="table-wrap">
<table class="doc-table role-table">
<thead>
<tr>
<th rowspan="2">职阶</th>
<th colspan="4">开局整体表现收益</th>
<th colspan="4">开局策略</th>
<th rowspan="2">开局任务</th>
<th colspan="5">开局属性</th>
<th colspan="5">满级属性</th>
</tr>
<tr>
<th>发展</th><th>探索</th><th>防御</th><th>进攻</th>
<th>主发展<br>无防</th><th>主发展<br>有防</th><th>主战争<br>防御</th><th>主战争<br>进攻</th>
<th>生命</th><th>防御</th><th>攻击</th><th>射程</th><th>移动</th>
<th>生命</th><th>防御</th><th>攻击</th><th>射程</th><th>移动</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong></strong></td>
<td>C</td><td class="grade-s">S</td><td>C</td><td>C</td>
<td class="check"></td><td></td><td></td><td></td>
<td>探索</td>
<td>A(15)</td><td>B(2)</td><td>C(1)</td><td>C(1)</td><td>A(2)</td>
<td>A(45)</td><td>B(3)</td><td>B(3)</td><td>A(2)</td><td>B(2)</td>
</tr>
<tr>
<td><strong></strong></td>
<td>C</td><td>A</td><td>A</td><td>C</td>
<td class="check"></td><td class="check"></td><td></td><td></td>
<td>探索</td>
<td>C(10)</td><td class="grade-s">S(3)</td><td>B(2)</td><td>A(2)</td><td>B(1.5)</td>
<td>C(30)</td><td>B(3)</td><td>B(3)</td><td class="grade-s">S(3)</td><td>B(2)</td>
</tr>
<tr>
<td><strong></strong></td>
<td>A</td><td>C</td><td>B</td><td>B</td>
<td></td><td class="check"></td><td class="check"></td><td></td>
<td>特色</td>
<td class="grade-s">S(20)</td><td>B(2)</td><td>B(2)</td><td>C(1)</td><td>C(1)</td>
<td>B(35)</td><td class="grade-s">S(4)</td><td>B(3)</td><td>C(1)</td><td>C(1)</td>
</tr>
<tr>
<td><strong></strong></td>
<td>C</td><td>C</td><td class="grade-s">S</td><td class="grade-s">S</td>
<td></td><td></td><td class="check"></td><td class="check"></td>
<td>伤害</td>
<td class="grade-s">S(20)</td><td class="grade-s">S(3)</td><td class="grade-s">S(3)</td><td>C(1)</td><td>C(1)</td>
<td class="grade-s">S(50)</td><td class="grade-s">S(4)</td><td class="grade-s">S(5)</td><td>C(1)</td><td>C(1)</td>
</tr>
<tr>
<td><strong></strong></td>
<td>C</td><td>C</td><td>C</td><td>A</td>
<td></td><td></td><td></td><td class="check"></td>
<td>扩张</td>
<td>C(10)</td><td>C(1)</td><td class="grade-s">S(3)</td><td>C(1)</td><td class="grade-s">S(2.5)</td>
<td>C(30)</td><td>B(3)</td><td>A(4)</td><td>C(1)</td><td class="grade-s">S(3)</td>
</tr>
</tbody>
</table>
</div>
<p class="table-note">√ 表示该职阶能满足对应开局策略,并能顺势升级。</p>
</section>
<section class="doc-section">
<h2>五阵营英雄职阶配置</h2>
<div class="table-wrap">
<table class="doc-table faction-class-table">
<thead>
<tr>
<th>职阶</th>
<th>基础定位</th>
<th>红魔馆</th>
<th>永远亭</th>
<th>守矢神社</th>
<th>地灵殿</th>
<th>博丽神社</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong></strong></td>
<td>发展防御型,承担阵营特色任务。</td>
<td>蕾米莉亚</td>
<td>辉夜</td>
<td>神奈子</td>
<td></td>
<td class="muted-cell">待补</td>
</tr>
<tr>
<td><strong></strong></td>
<td>防守探索型,兼顾安全开图和阵地保护。</td>
<td>帕秋莉</td>
<td>永琳</td>
<td>诹访子</td>
<td></td>
<td class="muted-cell">待补</td>
</tr>
<tr>
<td><strong></strong></td>
<td>探索型,承担开图、信息和机动收益。</td>
<td>咲夜</td>
<td>因幡帝</td>
<td>早苗</td>
<td></td>
<td class="muted-cell">待补</td>
</tr>
<tr>
<td><strong></strong></td>
<td>扩张突击型,负责压迫、追击和地图推进。</td>
<td>芙兰朵露</td>
<td>铃仙</td>
<td>射命丸文</td>
<td></td>
<td class="muted-cell">待补</td>
</tr>
<tr>
<td><strong></strong></td>
<td>正面战争型,承担伤害、防线和硬碰硬作战。</td>
<td>美铃</td>
<td>妹红</td>
<td>犬走椛</td>
<td>勇仪</td>
<td class="muted-cell">待补</td>
</tr>
</tbody>
</table>
</div>
</section>
<footer class="doc-footer"><a href="index.html">返回设计树</a></footer>
</main>
</body>
</html>

View File

@ -0,0 +1,35 @@
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>TH1 东方英雄系统</title>
<link rel="stylesheet" href="/api/design/shared/design-doc.css">
</head>
<body>
<main class="doc-shell narrow">
<header class="doc-hero compact">
<p class="eyebrow">Mechanics / Heroes</p>
<h1>东方英雄系统</h1>
<p class="lead">英雄是 TH1 相对 Polytopia 最大的机制扩展:它把东方角色设定转成高价值单位、技能触发器和阵营节奏。</p>
</header>
<section class="doc-section">
<h2>英雄的三层职责</h2>
<div class="flow-grid three">
<article><h3>战术核心</h3><p>英雄通常具备更高属性、独特移动/攻击方式或强触发技能,是一局游戏中的局部战场焦点。</p></article>
<article><h3>阵营识别</h3><p>每个势力通过英雄组合表达主题,例如红魔馆、永远亭、守矢、地灵殿等。</p></article>
<article><h3>成长目标</h3><p>英雄等级、文化消耗、出战位和技能解锁构成中长期投入目标。</p></article>
</div>
</section>
<section class="doc-section">
<h2>设计原则</h2>
<ul class="rule-list">
<li><strong>强但不万能:</strong>英雄应改变局部最优解,而不是替代城市、科技和普通单位。</li>
<li><strong>角色设定先转规则:</strong>先明确角色幻想,再落成移动、伤害、状态、召唤、复活、转换等规则动词。</li>
<li><strong>技能描述必须可验证:</strong>文案里的关键词要能追到技能类型、触发时机和代码实现。</li>
</ul>
</section>
<footer class="doc-footer"><a href="index.html">返回机制总览</a></footer>
</main>
</body>
</html>

View File

@ -0,0 +1,98 @@
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>TH1 设计树</title>
<link rel="stylesheet" href="/api/design/shared/design-doc.css">
</head>
<body>
<main class="doc-shell tree-shell">
<section class="design-tree" aria-label="TH1 设计树">
<a class="tree-node tree-root" href="design-principles.html">设计原则</a>
<div class="tree-branches two">
<section class="tree-card">
<a class="tree-node tree-l1" href="design-principles.html#mechanics">1. 游戏机制</a>
<div class="tree-branches three">
<section class="tree-card compact">
<a class="tree-node tree-l2" href="design-principles.html#foundation">基础</a>
<div class="tree-leaves">
<a href="design-principles.html#farming">种田</a>
<a href="design-principles.html#combat">战斗</a>
<a href="design-principles.html#tempo">节奏</a>
<a href="hero-foundation.html">英雄</a>
</div>
</section>
<section class="tree-card compact">
<a class="tree-node tree-l2" href="design-principles.html#battlefield">战场</a>
<div class="tree-group">
<div class="tree-label">地形</div>
<div class="tree-leaves inline">
<a href="design-principles.html#dryland">旱地</a>
<a href="design-principles.html#standard-map">标准</a>
<a href="design-principles.html#sea-map">海图</a>
</div>
</div>
<div class="tree-group">
<div class="tree-label">战场熵</div>
<div class="tree-leaves inline">
<a href="design-principles.html#duel">1v1</a>
<a href="design-principles.html#ffa">FFA</a>
<a href="design-principles.html#nvn">NvN</a>
<a href="design-principles.html#conquest">征服</a>
</div>
</div>
</section>
<section class="tree-card compact">
<a class="tree-node tree-l2" href="design-principles.html#factions">阵营</a>
<div class="tree-leaves">
<a href="design-principles.html#remilia">红魔馆</a>
<a href="design-principles.html#kaguya">永远亭</a>
<a href="design-principles.html#kanako">守矢神社</a>
<a href="design-principles.html#satori">地灵殿</a>
<a href="design-principles.html#reimu">博丽神社</a>
</div>
</section>
</div>
</section>
<section class="tree-card">
<a class="tree-node tree-l1" href="design-principles.html#narrative">2. 文案剧情</a>
<div class="tree-branches three">
<section class="tree-card compact">
<a class="tree-node tree-l2" href="design-principles.html#story">剧情</a>
</section>
<section class="tree-card compact">
<a class="tree-node tree-l2" href="/api/design/narrative/heroes.html">英雄</a>
<div class="tree-leaves">
<a href="/api/design/narrative/heroes.html#remilia">红魔馆</a>
<a href="/api/design/narrative/heroes.html#kaguya">永远亭</a>
<a href="/api/design/narrative/heroes.html#kanako">守矢神社</a>
<a href="/api/design/narrative/heroes.html#satori">地灵殿</a>
<a href="/api/design/narrative/heroes.html#reimu">博丽神社</a>
</div>
</section>
<section class="tree-card compact">
<a class="tree-node tree-l2" href="/api/design/narrative/civilizations.html">文明</a>
<div class="tree-leaves">
<a href="/api/design/narrative/civilizations.html#remilia">红魔馆</a>
<a href="/api/design/narrative/civilizations.html#kaguya">永远亭</a>
<a href="/api/design/narrative/civilizations.html#kanako">守矢神社</a>
<a href="/api/design/narrative/civilizations.html#satori">地灵殿</a>
<a href="/api/design/narrative/civilizations.html#reimu">博丽神社</a>
</div>
</section>
</div>
</section>
</div>
</section>
</main>
</body>
</html>

View File

@ -0,0 +1,370 @@
# 地脉Ley Line机制 — 策划·程序综合开发文档
> 文档目录:`Design/final/mechanics/ley-line-mechanism.md`
> 版本v1.1 日期2026-03-09 作者:待填写
---
## 一、策划需求
### 1.1 概述
地脉Ley Line是一种特殊地格属性SpType在地图生成阶段随机散布于地图各处。
建造在地脉附近的建筑将获得初始等级加成,代表"灵气汇聚"对建筑的增幅效果。
**阵营限制**:地脉机制**仅对古明地帝国Satori / Indian Empire阵营生效**。
- 只有 `CivEnum.Indian` 的玩家才能获得地脉建筑加成
- 只有 `CivEnum.Indian` 的玩家才能在地图上看到地脉格的深紫色标识
- 其他所有阵营Egyptian、French、Germany等的玩家**无法看到地脉,也不会获得任何地脉加成**
- 地脉数据仍然存在于地图中(所有阵营共用同一张地图),只是非 Indian 阵营在渲染层和逻辑层都被跳过
### 1.2 建筑加成规则
| 条件 | 加成 | 备注 |
|------|------|------|
| 建筑建在地脉格上isOnLeyLine | 初始 buildingLevel **+3** | 冶炼厂Forge**+2** |
| 建筑建在地脉周围 1 格内isAdjacentToLeyLine | 初始 buildingLevel **+1** | 所有建筑类型相同 |
| 两个条件叠加 | 不叠加,优先取在地脉上的规则 | 在地脉上不再享受周边加成 |
### 1.3 适用建筑范围
适用所有 `HasLevel = true` 的建筑,即当前的:
- 风车Windmill
- 冶炼厂Forge
- 锯木厂Sawmill
- 集市Market
- 学院Academy
- 保护区Preserve
- 卡古亚法式农庄KaguyaFrenchYard
- 埃及灌溉EgyptianIrrigation
> 等级上限(`resourceInfo.MaxLevel`)不变,地脉加成后仍受上限约束。
### 1.4 地脉格子的生成规则
地脉在 `MapGenerator.GenerateMap()` 的**最后阶段**随机生成,在所有地形与资源数据都确定之后进行。
| 规则 | 说明 |
|------|------|
| 生成时机 | 在 `InitGridMapData()``CivTerrainFeatureCountControl()` 之后,所有资源生成完毕后执行 |
| 禁止位置 | 不得生成在 **CityCenter 格本身**上(无论首都还是村庄) |
| 排除区 | 不得生成在**首都**`PlayerCivOri`)的 **1 格范围内**;非首都村庄 1 格范围内**允许**生成 |
| 生成数量 | 总数量 = 地图上城市中心总数(`_tribes.Count`)的 **1/3**(取整) |
| 地形限制 | 仅限陆地格(`Terrain == TerrainType.Land` |
| 文明保底 | **古明地帝国**`CivEnum.Indian`)首都周围 **2 格内必定有至少 1 个地脉**,随机放置未满足时强制补充 |
| 地脉持久性 | 地脉属于地形属性层,建造建筑后依然保留(持久型 SpType不消耗 |
### 1.5 视觉表现规则
地脉格子在视觉上**通过表格配置**,不再使用代码硬编码颜色。
- 地脉格子的 Sprite 和颜色走 `GridAndResourceDataAssets` 表格配置
- `GetTerrainSprite()` 方法已支持根据 `GridSpType.LeyLine` 返回对应配置的 Sprite
- 在 `TerrainInfo.SpriteList` 中添加 `IsGridSpType = true``GridSpType = LeyLine` 的配置项即可定义地脉格的地块外观
- 视觉表现与 `RemiliaGrid``KaguyaGrid` 机制一致,完全由策划在表格中配置
- **仅当本地玩家SelfPlayer为 `CivEnum.Indian` 时才渲染地脉特殊外观**;其他阵营玩家看到的地脉格显示为普通陆地地块
---
## 二、程序功能设计
### 2.1 涉及文件清单
| 文件路径 | 修改类型 | 说明 |
|----------|----------|------|
| `Assets/Scripts/TH1_Data/GridData.cs` | **枚举新增** | `GridSpType` 中加入 `LeyLine` |
| `Assets/Scripts/TH1_Logic/City/CityLogic.cs` | **逻辑修改** | `CalcGridBuildingLevel` 中注入地脉加成 |
| `Assets/Scripts/TH1_Logic/Map/MapGenerator.cs` | **新增方法** | `GenerateLeyLines()` 随机生成地脉格,在 `GenerateMap()` 末尾调用 |
| `Assets/Scripts/TH1_Renderer/GridRenderer.cs` | **无需修改** | 地脉视觉走 `GetTerrainSprite()` 表格配置,自动生效 |
| `Assets/Resources/DataAssets/GridAndResourceDataAssets.asset` | **表格配置** | 在 `TerrainInfo.SpriteList` 中添加 `LeyLine` 对应的美术资源配置(仅 Indian 阵营) |
| `Assets/Scripts/TH1_Logic/Action/BuildActionLogic.cs` | 无需修改 | 建造流程已调用 `CalcGridBuildingLevel`,自动生效 |
---
### 2.2 第一步枚举扩展GridData.cs
**文件**`Assets/Scripts/TH1_Data/GridData.cs`,第 313 行
**当前代码**
```csharp
public enum GridSpType { None, RemiliaGrid, KaguyaGrid, RemiliaGridDark, Max }
```
**修改为**
```csharp
public enum GridSpType { None, RemiliaGrid, KaguyaGrid, RemiliaGridDark, LeyLine, Max }
```
> `Max` 始终放最后,`LeyLine` 插在 `RemiliaGridDark` 之后。
> MemoryPack 对枚举按整数值序列化,新增枚举值放在 Max 之前不影响旧存档读取,完全向后兼容。
---
### 2.3 第二步地脉加成注入CityLogic.cs
**文件**`Assets/Scripts/TH1_Logic/City/CityLogic.cs`
**函数**`CalcGridBuildingLevel`(约第 317 行)
在函数末尾 `if (level > resourceInfo.MaxLevel)` **之前**注入:
```csharp
// ===== 地脉加成(仅 Indian 阵营生效) =====
if (mapData.GetPlayerDataByTerritoryGridId(gridData.Id, out var leyLinePlayer)
&& leyLinePlayer.CivEnum == CivEnum.Indian)
{
// 规则1建筑就建在地脉格上
if (gridData.HasSpType(GridSpType.LeyLine))
{
int bonus = (buildingType == ResourceType.Forge) ? 2 : 3;
level += bonus;
ret = true;
}
// 规则2建筑周围1格存在地脉格自身不是地脉格时才检查
else
{
foreach (var grid in aroundGridList)
{
if (grid == gridData) continue;
if (grid.HasSpType(GridSpType.LeyLine))
{
level += 1;
ret = true;
break;
}
}
}
}
// ====================
if (level > resourceInfo.MaxLevel)
level = resourceInfo.MaxLevel;
return ret;
```
**关键设计说明**
1. **阵营检查**——通过 `GetPlayerDataByTerritoryGridId` 获取该格所属玩家,仅 `CivEnum.Indian` 时才进入地脉加成逻辑
2. `else` 保证互斥——在地脉上时不再检查周边,防止同格加成叠加
3. `break` 保证邻格不叠加——相邻多个地脉格只加一次 +1
4. 上限保护——地脉加成后仍受 `resourceInfo.MaxLevel` 约束
5. `aroundGridList` 复用——函数顶部已初始化(`GetAroundGridData(1,1,gridData)`),含 center无需额外查表
---
### 2.4 第三步地脉地图生成MapGenerator.cs
**文件**`Assets/Scripts/TH1_Logic/Map/MapGenerator.cs`
#### 2.4.1 新增私有方法
```csharp
private void GenerateLeyLines(MapData mapData)
{
// Step #1 计算目标数量:地脉总数 = 城市中心总数 / 3取整
int leyLineCount = _tribes.Count / 3;
if (leyLineCount <= 0) return;
// Step #2 构建首都排除区仅首都±1范围非首都村庄不排除
var cradleExclude = new HashSet<uint>();
foreach (var cradle in PlayerCivOri)
for (int x = cradle.X - 1; x <= cradle.X + 1; x++)
for (int y = cradle.Y - 1; y <= cradle.Y + 1; y++)
cradleExclude.Add(MapPosition.CalculatePosId(x, y));
// Step #3 收集所有合法候选格子
// 条件:陆地格 + 不是CityCenter + 不在首都1格范围内
var candidates = new List<GridData>();
foreach (var gridData in mapData.GridMap.GridList)
{
if (gridData.Terrain != TerrainType.Land) continue;
if (gridData.Resource == ResourceType.CityCenter) continue;
if (cradleExclude.Contains(gridData.Pos.PosId)) continue;
candidates.Add(gridData);
}
// Step #4 随机打乱候选列表Fisher-Yates shuffle
for (int i = candidates.Count - 1; i > 0; i--)
{
int j = Random.Range(0, i + 1);
(candidates[i], candidates[j]) = (candidates[j], candidates[i]);
}
// Step #5 取前N个格子标记为地脉
int placed = 0;
foreach (var grid in candidates)
{
if (placed >= leyLineCount) break;
grid.SpTypeList.Add(GridSpType.LeyLine);
placed++;
}
// Step #6 古明地帝国保底首都2格内必定有地脉
for (int rk = 0; rk < mapData.PlayerMap.PlayerDataList.Count && rk < PlayerCivOri.Count; rk++)
{
var player = mapData.PlayerMap.PlayerDataList[rk];
if (player.CivEnum != CivEnum.Indian) continue;
if (!mapData.GridMap.GetGridDataByPos(PlayerCivOri[rk].X, PlayerCivOri[rk].Y, out var cradleGrid)) continue;
// 检查2格内是否已有地脉
var aroundGrids = mapData.GridMap.GetAroundGridData(2, 2, cradleGrid);
bool hasLeyLine = false;
foreach (var g in aroundGrids)
if (g.HasSpType(GridSpType.LeyLine)) { hasLeyLine = true; break; }
if (hasLeyLine) continue;
// 没有则强制在2格内随机选一个合法格子放置
for (int i = aroundGrids.Count - 1; i > 0; i--)
{
int j = Random.Range(0, i + 1);
(aroundGrids[i], aroundGrids[j]) = (aroundGrids[j], aroundGrids[i]);
}
foreach (var g in aroundGrids)
{
if (g.Terrain != TerrainType.Land) continue;
if (g.Resource == ResourceType.CityCenter) continue;
if (cradleExclude.Contains(g.Pos.PosId)) continue;
g.SpTypeList.Add(GridSpType.LeyLine);
break;
}
}
}
```
#### 2.4.2 调用时机
`GenerateMap()` 中,**`CivTerrainFeatureCountControl(mapData)` 之后**、`InitGeoData` 之前调用:
```csharp
public void GenerateMap(MapData mapData)
{
// ... 原有流程 ...
InitGridMapData(mapData);
CivTerrainFeatureCountControl(mapData);
// ===== 新增:生成地脉 =====
GenerateLeyLines(mapData);
// =========================
Table.Instance.GeoDataAssets.InitGeoData(mapData);
}
```
#### 2.4.3 排除区说明:仅排除首都,不排除村庄
`PlayerCivOri` 是玩家摇篮城市(首都)的坐标列表,是 `_tribes` 的子集。
`GenerateLeyLines` 在运行时动态构建首都 ±1 的排除区 `HashSet<uint> cradleExclude`
使用 `MapPosition.CalculatePosId(x, y)` 作为 key`gridData.Pos.PosId` 直接比较,无需 `Equals` 重载。
非首都城市无人占领的村庄1 格范围内允许生成地脉,使得村庄附近可以出现地脉,为玩家的扩张选择提供策略价值。但 CityCenter 格本身始终禁止生成地脉(`Resource == ResourceType.CityCenter` 被排除)。
#### 2.4.4 古明地帝国保底说明
随机放置完毕后,遍历 `PlayerDataList` 查找 `CivEnum.Indian` 的玩家,通过索引 `rk` 对应 `PlayerCivOri[rk]` 获取首都坐标。
调用 `GetAroundGridData(2, 2, cradleGrid)` 获取 2 格范围内所有格子,若其中无地脉则随机选取一个合法格子强制放置。
强制放置仍遵守所有约束(陆地、非 CityCenter、不在首都 1 格排除区内),因此保底地脉最近出现在首都切比雪夫距离 = 2 的位置。
---
### 2.5 第四步地脉渲染GridRenderer.cs走表格配置
**文件**`Assets/Scripts/TH1_Renderer/GridRenderer.cs`
**函数**`UpdateLand()`(约第 425 行)
地脉格子的视觉表现**完全由表格配置**,代码层面**不需要**像 RemiliaGrid/KaguyaGrid 那样添加特殊颜色覆写逻辑。
#### 当前实现
`GetTerrainSprite()` 已自动支持 `GridSpType.LeyLine`
```csharp
private void UpdateLand()
{
if(_land == null) return;
var com = _land.GetComponent<SpriteRenderer>();
if (com == null) return;
var oldSprite = com.sprite;
if (oldSprite == null) return;
com.color = _gridData.Player(Main.MapData, out var _) && _gridData.Terrain == TerrainType.Land
? terriColor : Color.white;
var newSprite = Table.Instance.GridAndResourceDataAssets.GetTerrainSprite(_gridData);
com.sprite = newSprite;
if (oldSprite != newSprite && _gridData.HasSpType(GridSpType.RemiliaGrid))
PlayVFXInSight(new GridVFXParams(GridVFXType.RedMistCreate));
}
```
**设计说明**
- **无需代码修改**——`GetTerrainSprite()` 会自动根据 `GridSpType.LeyLine` 返回表格中配置的 Sprite
- **阵营限制在表格层实现**——配置 `EmpireGridSpInfoPack` 时指定 `CivId` 为 Indian 对应的 CivId非 Indian 玩家将匹配不到对应 Sprite
- 颜色/图案完全由 `TerrainInfo.SpriteList` 中的美术资源决定
---
### 2.6 第五步建造流程回顾BuildActionLogic.cs无需修改
建造流程的核心路径如下:
```
BuildAction.Execute()
└─ Main.CityLogic.CalcGridBuildingLevel(...) // ← 地脉加成已在此注入,自动生效
gridData.buildingLevel = buildingLevel
└─ Main.PlayerLogic.UpdateTerritoryAllBuildingLevel(...)
└─ UpdateGridBuildingData_LogicView(...)
└─ CalcGridBuildingLevel(...) // ← 全量刷新时同样生效
```
第 62-64 行的 cityExp 计算使用最终的 `buildingLevel`,地脉加成后 cityExp 自动跟随正确数值,无需额外修改。
---
## 三、数值示例
Forge冶炼厂示例周围有 Mine基础 level = 2MaxLevel = 4
| 格子情况 | 基础 level | 地脉加成 | 最终 level |
|----------|-----------|---------|-----------|
| 无地脉 | 2 | 0 | **2** |
| 建在地脉上 | 2 | +2Forge专用 | **4**(触上限) |
| 周边1格有地脉 | 2 | +1 | **3** |
Windmill风车示例周围有 Farm基础 level = 1MaxLevel = 4
| 格子情况 | 基础 level | 地脉加成 | 最终 level |
|----------|-----------|---------|-----------|
| 无地脉 | 1 | 0 | **1** |
| 建在地脉上 | 1 | +3 | **4**(触上限) |
| 周边1格有地脉 | 1 | +1 | **2** |
---
## 四、开发任务拆解
### 必做
- [ ] `GridData.cs``GridSpType` 枚举加入 `LeyLine`
- [ ] `CityLogic.cs``CalcGridBuildingLevel` 注入地脉加成逻辑
- [ ] `MapGenerator.cs`:新增 `GenerateLeyLines()` 方法,并在 `GenerateMap()` 末尾调用
- [ ] `GridRenderer.cs`**无需修改**,地脉视觉走表格配置
- [ ] `GridAndResourceDataAssets.asset`:在 `TerrainInfoList``Land``SpriteList` 中添加 `LeyLine` 对应的 `EmpireGridSpInfoPack` 配置(指定印度阵营对应的 CivId
### 可选(视需求)
- [ ] UI Tooltip建造时预先显示地脉加成数值需在 `CheckCan` 或 UI Controller 调用 `CalcGridBuildingLevel` 做预算)
- [ ] `AddSpType`:为 `LeyLine` 添加专属校验规则(如只能在陆地格)
---
## 五、风险与注意事项
1. **MemoryPack 兼容性**`GridSpType.LeyLine` 是新枚举值,旧存档不含此值,反序列化时 `SpTypeList` 不包含 `LeyLine` 即视为无地脉,**完全向后兼容**。
2. **`_innerCityGrid` 含中心格**`GenerateTribes()``_innerCityGrid` 的循环范围是 `±1`已包含城市中心本身x==cent.X, y==cent.Y因此 Contains 检查已覆盖中心格,无需额外处理。
3. **`_innerCityGrid` 使用 `MapPosition` 相等比较**`MapPosition` 已重载 `Equals`/`GetHashCode`/`==`(见 GridData.cs 第 873-901 行),`List<MapPosition>.Contains` 语义正确,可放心使用。
4. **地脉生成在 `CivTerrainFeatureCountControl` 之后**:保底地形修正可能会改变某些格子的 Terrain`GenerateLeyLines` 在其之后执行,读取的 `Terrain` 是最终值,不存在顺序问题。
5. **`aroundGridList` 含 center**`CalcGridBuildingLevel``GetAroundGridData(1,1,gridData)` 返回含 center 的列表,邻格检查时已用 `grid == gridData` 跳过,逻辑正确。
6. **UpdateTerritoryAllBuildingLevel 刷新**:占领城市、新建城市等场景会触发全量刷新,地脉加成会被自动重新计算,无需额外处理。
7. **AI 评分**AI 在 `AITechScoreCalculator` / `AIActionBase` 中评估建筑价值,若需 AI 感知地脉优势,需额外修改 AI 评估逻辑(属于后续优化,非核心需求)。

View File

@ -0,0 +1,37 @@
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>TH1 地图、城市与科技</title>
<link rel="stylesheet" href="/api/design/shared/design-doc.css">
</head>
<body>
<main class="doc-shell narrow">
<header class="doc-hero compact">
<p class="eyebrow">Mechanics / Economy</p>
<h1>地图、城市与科技</h1>
<p class="lead">经营层负责把地形、资源、城市升级和科技选择串成清晰的长期规划。东方机制应尽量附着在这条主线上,而不是另开一套难以理解的资源系统。</p>
</header>
<section class="doc-section">
<h2>经营层主线</h2>
<ol class="rule-list">
<li><strong>地块:</strong>提供移动、建造和资源限制。</li>
<li><strong>资源:</strong>通过科技解锁采集或建筑转换。</li>
<li><strong>城市:</strong>将资源转化为发展度、生产能力和升级奖励。</li>
<li><strong>科技:</strong>决定玩家能对地图做什么,也决定单位与建筑的可用集合。</li>
<li><strong>文化卡:</strong>提供中后期方向选择,可承担部分东方化的国家制度表达。</li>
</ol>
</section>
<section class="doc-section">
<h2>设计检查项</h2>
<div class="callout-grid">
<article><h3>资源是否可读</h3><p>玩家看到地块时,应能判断它当前能做什么、研究科技后能做什么。</p></article>
<article><h3>城市升级是否有选择</h3><p>升级奖励不能只是数值堆叠,应提供扩张、经济、军事或英雄成长方向。</p></article>
<article><h3>科技是否改变行动</h3><p>科技最好解锁新的地图操作或战术路径,而不只是属性加成。</p></article>
</div>
</section>
<footer class="doc-footer"><a href="index.html">返回机制总览</a></footer>
</main>
</body>
</html>

View File

@ -0,0 +1,39 @@
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>TH1 单位与技能系统</title>
<link rel="stylesheet" href="/api/design/shared/design-doc.css">
</head>
<body>
<main class="doc-shell narrow">
<header class="doc-hero compact">
<p class="eyebrow">Mechanics / Units & Skills</p>
<h1>单位与技能系统</h1>
<p class="lead">单位提供可被玩家直接操作的棋子技能提供规则差异。TH1 的深度主要来自单位模板、英雄等级、状态技能和触发技能的组合。</p>
</header>
<section class="doc-section">
<h2>单位分层</h2>
<div class="flow-grid three">
<article><h3>基础单位</h3><p>战士、骑兵、弓兵、船只等低复杂度单位,支撑 Polytopia 式基础战斗。</p></article>
<article><h3>特色单位</h3><p>阵营替换或专属训练单位,用少量规则表达势力主题。</p></article>
<article><h3>英雄/巨人单位</h3><p>拥有高权重技能和成长价值,是东方扩展层的主要承载者。</p></article>
</div>
</section>
<section class="doc-section">
<h2>技能分类建议</h2>
<table class="doc-table">
<thead><tr><th>分类</th><th>例子</th><th>文档需要记录</th></tr></thead>
<tbody>
<tr><td>基础能力</td><td>移动、反击、射程、航海。</td><td>触发条件、是否影响 AI 评分。</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>
</section>
<footer class="doc-footer"><a href="index.html">返回机制总览</a></footer>
</main>
</body>
</html>

View File

@ -0,0 +1,149 @@
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>文案剧情 / 文明</title>
<link rel="stylesheet" href="/api/design/shared/design-doc.css">
</head>
<body>
<main class="doc-shell narrative-shell">
<header class="doc-hero compact">
<p class="eyebrow">Narrative / Civilizations</p>
<h1>文案剧情:文明</h1>
<p class="lead">按当前 PlayerDataAssets 与 CivDataAssets 归类阵营文案。这里把东方势力层和 Polytopia 文明底板并列展示,方便后续决定哪些文案保留、替换或扩写。</p>
<div class="source-strip" aria-label="DataAsset 来源">
<span>PlayerDataAssetsCivName / ForceName / LeaderName / EmpireDesc / LeaderDesc</span>
<span>CivDataAssetsCivName / CityInfoList / CityDescription</span>
<span>LibraryDataAssets奇观图鉴与英雄图鉴补充语气</span>
</div>
</header>
<nav class="faction-nav" aria-label="阵营导航">
<a href="#remilia">红魔馆</a>
<a href="#kaguya">永远亭</a>
<a href="#kanako">守矢神社</a>
<a href="#satori">地灵殿</a>
<a href="#reimu">博丽神社</a>
</nav>
<section id="remilia" class="doc-section faction-section">
<div class="faction-head">
<h2>红魔馆</h2>
<p>ForceName斯卡雷特帝国 / LeaderName蕾米莉亚 / CivName埃及</p>
</div>
<div class="civilization-grid">
<article class="copy-card">
<h3>阵营文案定位 <small>PlayerInfo</small></h3>
<p class="copy-desc">红雾笼罩尼罗河两岸,生死、智慧与禁忌分别由咲夜、帕秋莉、芙兰等红魔馆成员承接。领袖形象是立于金字塔之巅的永生法老蕾米莉亚。</p>
<p class="copy-diag">高傲的红魔馆大小姐抽到了埃及帝国卡。</p>
<div class="copy-meta"><span>红雾</span><span>法老</span><span>馆主权威</span></div>
</article>
<article class="copy-card">
<h3>文明底板 <small>CivDataAssets</small></h3>
<p class="copy-desc">埃及文明城市名已经完整配置,城市描述目前是通用占位句。</p>
<div class="city-list">
<span>开罗</span><span>亚历山大</span><span>孟斐斯</span><span>底比斯</span><span>卢克索</span><span>阿斯旺</span><span>吉萨</span><span>西奈</span>
</div>
</article>
</div>
</section>
<section id="kaguya" class="doc-section faction-section">
<div class="faction-head">
<h2>永远亭</h2>
<p>ForceName蓬莱山帝国 / LeaderName蓬莱山辉夜 / CivName法兰西</p>
</div>
<div class="civilization-grid">
<article class="copy-card">
<h3>阵营文案定位 <small>PlayerInfo</small></h3>
<p class="copy-desc">妖怪兔近卫队、竹林狼上校与帝国元帅构成战队化军团叙事。辉夜以月之公主与电竞高手双重身份,站在凯旋门前写下最强陆军传说。</p>
<p class="copy-diag">优雅而怠惰的月之公主抽到了法兰西帝国卡。</p>
<div class="copy-meta"><span>永夜</span><span>法兰西荣耀</span><span>战队训练</span></div>
</article>
<article class="copy-card">
<h3>文明底板 <small>CivDataAssets</small></h3>
<p class="copy-desc">法兰西文明城市名已经完整配置,城市描述目前是通用占位句。</p>
<div class="city-list">
<span>巴黎</span><span>凡尔赛</span><span>里昂</span><span>马赛</span><span>波尔多</span><span>图卢兹</span><span>里尔</span><span>南特</span>
</div>
</article>
</div>
</section>
<section id="kanako" class="doc-section faction-section">
<div class="faction-head">
<h2>守矢神社</h2>
<p>ForceName守矢帝国 / LeaderName八坂神奈子 / CivName德意志</p>
</div>
<div class="civilization-grid">
<article class="copy-card">
<h3>阵营文案定位 <small>PlayerInfo</small></h3>
<p class="copy-desc">土著神奠基,风雨神推动工业齿轮。神奈子把技术与信仰熔铸成铁与血,整体调性是产业化、技术化、商业化的守矢集团。</p>
<p class="copy-diag">野心勃勃的山神殿下抽到了德意志帝国卡。守矢神社已是比赛最大的冠名赞助商。</p>
<div class="copy-meta"><span>信仰即流量</span><span>钢铁王座</span><span>技术变革</span></div>
</article>
<article class="copy-card">
<h3>文明底板 <small>CivDataAssets</small></h3>
<p class="copy-desc">德意志文明城市名已经完整配置,城市描述目前是通用占位句。</p>
<div class="city-list">
<span>柏林</span><span>哥尼斯堡</span><span>波茨坦</span><span>马格德堡</span><span>汉堡</span><span>不来梅</span><span>德累斯顿</span><span>莱比锡</span>
</div>
</article>
</div>
</section>
<section id="satori" class="doc-section faction-section">
<div class="faction-head">
<h2>地灵殿</h2>
<p>ForceName古明地帝国 / LeaderName古明地觉 / CivName印度</p>
</div>
<div class="civilization-grid">
<article class="copy-card">
<h3>阵营文案定位 <small>PlayerInfo</small></h3>
<p class="copy-desc">雷霆鬼王、苏利耶神子、无心神明与旧地狱业火共同拼成吠陀史诗式的地底帝国。觉的核心形象是洞彻万心的第三只眼。</p>
<p class="copy-diag">沉静而可怖的地灵殿主人抽到了印度帝国卡。</p>
<div class="copy-meta"><span>旧地狱</span><span>吠陀史诗</span><span>读心</span></div>
</article>
<article class="copy-card">
<h3>文明底板 <small>CivDataAssets</small></h3>
<p class="copy-desc">印度文明城市名已经完整配置,城市描述目前是通用占位句。</p>
<div class="city-list">
<span>德里</span><span>新德里</span><span>瓦拉纳西</span><span>巴特那</span><span>华氏城</span><span>阿约提亚</span><span>阿格拉</span><span>斋浦尔</span>
</div>
</article>
</div>
</section>
<section id="reimu" class="doc-section faction-section">
<div class="faction-head">
<h2>博丽神社</h2>
<p>ForceName博丽帝国 / LeaderName博丽灵梦 / CivName维京</p>
</div>
<div class="civilization-grid">
<article class="copy-card">
<h3>阵营文案定位 <small>PlayerInfo</small></h3>
<p class="copy-desc empty-copy">EmpireDesc 与 LeaderDesc 当前为空。LibraryDataAssets 中灵梦还是“代理木偶”占位,因此博丽神社暂时只有阵营壳和文明底板,没有正式剧情定位。</p>
<div class="copy-meta"><span>待补</span><span>博丽结界</span><span>幻想乡中心</span></div>
</article>
<article class="copy-card">
<h3>文明底板 <small>CivDataAssets</small></h3>
<p class="copy-desc">维京文明城市名已经完整配置,城市描述目前是通用占位句。</p>
<div class="city-list">
<span>奥斯陆</span><span>卑尔根</span><span>特隆赫姆</span><span>斯塔万格</span><span>特罗姆瑟</span><span>克里斯蒂安桑</span><span>德拉门</span><span>博德</span>
</div>
</article>
</div>
</section>
<section class="doc-section note">
<h2>下一轮整理重点</h2>
<ol class="rule-list">
<li>博丽神社需要补齐 EmpireDesc、LeaderDesc、正式英雄组和图鉴文案。</li>
<li>CivDataAssets 的 CityDescription 目前基本是通用占位句,可以后续按文明或阵营改写。</li>
<li>LibraryDataAssets 的奇观文案现在更偏传统文明,需要决定是否要映射成东方阵营风格。</li>
</ol>
</section>
</main>
</body>
</html>

View File

@ -0,0 +1,245 @@
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>文案剧情 / 英雄</title>
<link rel="stylesheet" href="/api/design/shared/design-doc.css">
</head>
<body>
<main class="doc-shell narrative-shell">
<header class="doc-hero compact">
<p class="eyebrow">Narrative / Heroes</p>
<h1>文案剧情:英雄</h1>
<p class="lead">按阵营归档 Library、Hero、Player 三类 DataAsset 中已经存在的英雄文案。当前页只做归类和设计入口,不在这里重写正式文案。</p>
<div class="source-strip" aria-label="DataAsset 来源">
<span>LibraryDataAssetsName / SubTitle / Desc / Diag</span>
<span>HeroDataAssetsTaskList / Desc</span>
<span>PlayerDataAssetsForceName / LeaderName</span>
</div>
</header>
<nav class="faction-nav" aria-label="阵营导航">
<a href="#remilia">红魔馆</a>
<a href="#kaguya">永远亭</a>
<a href="#kanako">守矢神社</a>
<a href="#satori">地灵殿</a>
<a href="#reimu">博丽神社</a>
</nav>
<section id="remilia" class="doc-section faction-section">
<div class="faction-head">
<h2>红魔馆</h2>
<p>PlayerInfo斯卡雷特帝国 / 领袖 蕾米莉亚 / 文明底板 埃及</p>
</div>
<div class="hero-copy-grid">
<article class="copy-card">
<h3>蕾米莉亚 · 斯卡雷特 <small>GiantType 2</small></h3>
<p class="copy-subtitle">帝国的血之君主</p>
<p class="copy-desc">饰演奥西里斯的鲜红幼月。红魔馆至高无上的主人,命运与威严的化身,为了那座“高雅”的冠军奖杯而参赛。</p>
<p class="copy-diag">哼哼,这可是我重金买通裁判选到的最强阵营……你说我被骗了?不、不可能!</p>
<p class="copy-task">任务文案线索:帝国累计金币、创造红雾领地。</p>
<div class="copy-meta"><span>领袖</span><span></span><span>红雾</span></div>
</article>
<article class="copy-card">
<h3>帕秋莉 · 诺蕾姬 <small>GiantType 5</small></h3>
<p class="copy-subtitle">帝国的魔女</p>
<p class="copy-desc">饰演克里欧帕特拉的魔女。红魔馆地下大图书馆的贤者,对纷争兴趣不高,以扩建图书馆为条件答应参赛。</p>
<p class="copy-diag">……用“魅力”交涉的女王??那真是找错人了呢……看在皇家图书馆的份上,我勉强接受。</p>
<p class="copy-task">任务文案线索:探索迷雾、消耗魔力石。</p>
<div class="copy-meta"><span></span><span>知识</span><span>资源消耗</span></div>
</article>
<article class="copy-card">
<h3>十六夜咲夜 <small>GiantType 3</small></h3>
<p class="copy-subtitle">帝国的女仆长</p>
<p class="copy-desc">饰演阿努比斯的人类少女。红魔馆女仆长,陪任性的大小姐一同加入游戏。</p>
<p class="copy-diag">亡者引路人吗?不过大小姐是不会死的吧。我变得多余了呢。</p>
<p class="copy-task">任务文案线索:探索迷雾、承受伤害、击杀敌方单位。</p>
<div class="copy-meta"><span></span><span>时间</span><span>护卫</span></div>
</article>
<article class="copy-card">
<h3>芙兰朵露 · 斯卡雷特 <small>GiantType 1</small></h3>
<p class="copy-subtitle">帝国的禁忌</p>
<p class="copy-desc">饰演赛特的恶魔之妹。被囚禁于红魔馆地下室的终极秘密,纯粹的破坏化身。</p>
<p class="copy-diag">诶!这个叫赛特的剧本上,好像说要把姐姐大人干掉啊!嘻嘻……</p>
<p class="copy-task">任务文案线索:占领村庄或敌城、累计击杀单位。</p>
<div class="copy-meta"><span></span><span>破坏</span><span>扩张</span></div>
</article>
<article class="copy-card">
<h3>红美铃 <small>GiantType 4</small></h3>
<p class="copy-subtitle">帝国之龙</p>
<p class="copy-desc">饰演荷鲁斯的武术家。红魔馆忠诚的门卫,因工作期间睡觉,醒来时发现自己已经在比赛现场。</p>
<p class="copy-diag">为大小姐复仇?赴汤蹈……你说复仇的对象是妹妹大人?!我、我还得去看门……</p>
<p class="copy-task">任务文案线索:累计造成伤害。</p>
<div class="copy-meta"><span></span><span>门卫</span><span>正面战斗</span></div>
</article>
</div>
</section>
<section id="kaguya" class="doc-section faction-section">
<div class="faction-head">
<h2>永远亭</h2>
<p>PlayerInfo蓬莱山帝国 / 领袖 蓬莱山辉夜 / 文明底板 法兰西</p>
</div>
<div class="hero-copy-grid">
<article class="copy-card">
<h3>蓬莱山辉夜 <small>GiantType 6</small></h3>
<p class="copy-subtitle">帝国的永远</p>
<p class="copy-desc">饰演拿破仑的月之公主。永远亭主人,也是一位电竞高手,正在训练自家宠物打造战队。</p>
<p class="copy-diag">这游戏目前的阵营太少了,平衡性非常糟糕,我的介绍页面也不够华丽,勉强给个好评吧。</p>
<p class="copy-task">任务文案线索:累计造成伤害、施加状态。</p>
<div class="copy-meta"><span></span><span>领袖</span><span>战队叙事</span></div>
</article>
<article class="copy-card">
<h3>八意永琳 <small>GiantType 9</small></h3>
<p class="copy-subtitle">帝国的月之头脑</p>
<p class="copy-desc">饰演贝尔蒂埃元帅的月之头脑。永远亭最强大的贤者,配合辉夜参赛的同时借机推销新药。</p>
<p class="copy-diag">拿破仑与贝尔蒂埃……这结局还真是令人唏嘘。突然有点怀念与公主的过往呢。</p>
<p class="copy-task">任务文案线索:探索迷雾、提供生命回复。</p>
<div class="copy-meta"><span></span><span>医疗</span><span>月都贤者</span></div>
</article>
<article class="copy-card">
<h3>因幡帝 <small>GiantType 8</small></h3>
<p class="copy-subtitle">帝国的欺诈师</p>
<p class="copy-desc">饰演红衣主教的妖怪兔。迷途竹林的主人,奸诈而狡猾,不久前刚冒充比赛裁判敲了一笔。</p>
<p class="copy-diag">全体目光向我看齐!“幸运豁免券”都能让你的罪孽一笔勾销!</p>
<p class="copy-task">任务文案线索:挖掘遗迹、累计获得金币。</p>
<div class="copy-meta"><span></span><span>幸运</span><span>欺诈</span></div>
</article>
<article class="copy-card">
<h3>铃仙·优昙华院·因幡 <small>GiantType 7</small></h3>
<p class="copy-subtitle">帝国的狂气之瞳</p>
<p class="copy-desc">饰演让·拉纳元帅的月兔。作为辉夜的宠物,正在接受严格的电竞训练。</p>
<p class="copy-diag">呜呜……我、我不会再输了!</p>
<p class="copy-task">任务文案线索:占领村庄或敌城、创造月兔幻象、施加状态。</p>
<div class="copy-meta"><span></span><span>幻象</span><span>狂气</span></div>
</article>
<article class="copy-card">
<h3>藤原妹红 <small>GiantType 10</small></h3>
<p class="copy-subtitle">帝国的不死鸟</p>
<p class="copy-desc">饰演贝尔纳多特元帅的不死鸟。辉夜的仇敌,因为找辉夜决斗时被哄骗着参赛。</p>
<p class="copy-diag">不管辉夜你耍什么把戏,我都会正面把你……哈?不能攻击队友?我们是队友?</p>
<p class="copy-task">任务文案线索:累计造成伤害。</p>
<div class="copy-meta"><span></span><span>不死</span><span>正面战斗</span></div>
</article>
</div>
<p class="table-note">数据问题:永远亭多名英雄的 EnglishName 字段当前仍是 HONG MEILING需要在 LibraryDataAssets 中单独修正。</p>
</section>
<section id="kanako" class="doc-section faction-section">
<div class="faction-head">
<h2>守矢神社</h2>
<p>PlayerInfo守矢帝国 / 领袖 八坂神奈子 / 文明底板 德意志</p>
</div>
<div class="hero-copy-grid">
<article class="copy-card">
<h3>八坂神奈子 <small>GiantType 11</small></h3>
<p class="copy-subtitle">帝国的全能神</p>
<p class="copy-desc">饰演腓特烈二世的全能之神。风神、山神、战神与科技变革之神,正试图将比赛权利转入守矢产业名下。</p>
<p class="copy-diag">留存和日活都很完美。流量便是信仰,游戏市场就是新的战场,是时候让守矢集团接管了。</p>
<p class="copy-task">任务文案线索:完成科技研发、累计造成伤害。</p>
<div class="copy-meta"><span></span><span>科技</span><span>信仰商业化</span></div>
</article>
<article class="copy-card">
<h3>洩矢诹访子 <small>GiantType 12</small></h3>
<p class="copy-subtitle">帝国的土著神</p>
<p class="copy-desc">饰演路易斯·冯·梅克伦堡王后的土著神。守矢神社供奉的神之一,看到比赛后擅自给队友报名。</p>
<p class="copy-diag">虽说路易斯王后和神奈子的角色并非一对……不过我和她确实也只是商业伙伴的关系嘛。</p>
<p class="copy-task">任务文案线索:探索迷雾、召唤物等级、召唤物击杀。</p>
<div class="copy-meta"><span></span><span>召唤</span><span>土著神</span></div>
</article>
<article class="copy-card">
<h3>东风谷早苗 <small>GiantType 13</small></h3>
<p class="copy-subtitle">帝国的现人神</p>
<p class="copy-desc">饰演俾斯麦的现人神。守矢神社巫女、风祝与现人神,因为冠军限定机甲而参赛。</p>
<p class="copy-diag">为了冠军机甲……咳,为了帝国的荣耀,从今天起,我就是“铁血风祝”了!</p>
<p class="copy-task">任务文案线索:探索迷雾、施加乘风、掷出大吉或大凶。</p>
<div class="copy-meta"><span></span><span>风祝</span><span>随机</span></div>
</article>
<article class="copy-card">
<h3>射命丸文 <small>GiantType 14</small></h3>
<p class="copy-subtitle">帝国的最速记者</p>
<p class="copy-desc">饰演克劳塞维茨的鸦天狗。妖怪之山新闻记者,似乎与神奈子达成交易后加入比赛。</p>
<p class="copy-diag">黑哨操纵!权钱交易!想看更多比赛的惊天大瓜?请订阅《文文。新闻》特刊!</p>
<p class="copy-task">任务文案线索:占领村庄或敌城、累计造成溅射伤害。</p>
<div class="copy-meta"><span></span><span>高速</span><span>媒体</span></div>
</article>
<article class="copy-card">
<h3>犬走椛 <small>GiantType 15</small></h3>
<p class="copy-subtitle">帝国的狂犬</p>
<p class="copy-desc">饰演条顿骑士团长的白狼天狗。妖怪之山巡逻警卫,受文邀请后来参赛。</p>
<p class="copy-diag">这个“帝国”棋似乎和将棋有几分类似……应该也能磨练思维和意志吧。</p>
<p class="copy-task">任务文案线索:累计造成伤害、异端死亡数量。</p>
<div class="copy-meta"><span></span><span>巡逻</span><span>纪律</span></div>
</article>
</div>
</section>
<section id="satori" class="doc-section faction-section">
<div class="faction-head">
<h2>地灵殿</h2>
<p>PlayerInfo古明地帝国 / 领袖 古明地觉 / 文明底板 印度</p>
</div>
<div class="hero-copy-grid">
<article class="copy-card">
<h3>古明地觉 <small>GiantType 16</small></h3>
<p class="copy-subtitle">帝国的第三只眼</p>
<p class="copy-desc">饰演罗摩的读心妖怪。原本在家看书,却发现家人突然出现在电视上,于是踏上“寻亲”之旅。</p>
<p class="copy-diag">找回被魔王劫走的妻子悉多吗……恋是那个魔王才对吧?</p>
<p class="copy-task">任务文案线索:施加恐惧、施加心理创伤。</p>
<div class="copy-meta"><span></span><span>读心</span><span>恐惧</span></div>
</article>
<article class="copy-card">
<h3>古明地恋 <small>GiantType 17</small></h3>
<p class="copy-subtitle">帝国的本我</p>
<p class="copy-desc">饰演奎师那的无意识妖怪。正在野外排练“大变活猫”,突然出现在比赛舞台中央。</p>
<p class="copy-diag">…诶?大家为什么一副见鬼的表情呀?出场魔术太成功了吗,呼呼呼♪</p>
<p class="copy-task">任务文案线索:探索迷雾、累计造成伤害。</p>
<div class="copy-meta"><span></span><span>隐身</span><span>无意识</span></div>
</article>
<article class="copy-card">
<h3>火焰猫燐 <small>GiantType 20</small></h3>
<p class="copy-subtitle">帝国的地狱火车</p>
<p class="copy-desc">饰演阿周那的猫车。陪恋排练魔术节目,被装进魔术箱后出现在比赛现场聚光灯下。</p>
<p class="copy-diag">恋大人!这个宴会好像要“报名”才能参加哦,交给我来搞定吧!</p>
<p class="copy-task">任务文案线索:探索迷雾、获得粗钝身/金刚身层数。</p>
<div class="copy-meta"><span></span><span>猫车</span><span>承伤</span></div>
</article>
<article class="copy-card">
<h3>灵乌路空 <small>GiantType 18</small></h3>
<p class="copy-subtitle">帝国的神之火</p>
<p class="copy-desc">饰演迦尔纳的地狱鸦。陪恋排练魔术时睡在帽子里,醒来后已成为比赛队伍成员。</p>
<p class="copy-diag">只要把这里烧得一干二净,我们的魔术就算大成功了吧!</p>
<p class="copy-task">任务文案线索:占领村庄或敌城、累计击杀单位。</p>
<div class="copy-meta"><span></span><span>核热</span><span>破坏</span></div>
</article>
<article class="copy-card">
<h3>星熊勇仪 <small>GiantType 19</small></h3>
<p class="copy-subtitle">帝国的雷霆鬼王</p>
<p class="copy-desc">饰演因陀罗的鬼王。意外收到地灵殿主人的请求,还没听清楚就豪爽答应。</p>
<p class="copy-diag">交给我吧!保护三个小姑娘罢了!呃……你是说保护比赛现场的观众是嘛……</p>
<p class="copy-task">任务文案线索:累计造成伤害。</p>
<div class="copy-meta"><span></span><span>鬼王</span><span>正面战斗</span></div>
</article>
</div>
</section>
<section id="reimu" class="doc-section faction-section">
<div class="faction-head">
<h2>博丽神社</h2>
<p>PlayerInfo博丽帝国 / 领袖 博丽灵梦 / 文明底板 维京</p>
</div>
<div class="hero-copy-grid">
<article class="copy-card">
<h3>博丽灵梦(木偶) <small>GiantType 21</small></h3>
<p class="copy-subtitle empty-copy">正式英雄文案待补</p>
<p class="copy-desc">LibraryDataAssets 当前只有占位说明:大赛主办方提供的代理木偶。</p>
<p class="copy-task">HeroDataAssets 当前未归档到正式任务线。PlayerDataAssets 的 EmpireDesc / LeaderDesc 为空。</p>
<div class="copy-meta"><span>待设计</span><span>代理木偶</span></div>
</article>
</div>
<p class="table-note">博丽神社目前只建立阵营入口,正式英雄组、图鉴描述和领袖剧情需要后续补齐。</p>
</section>
</main>
</body>
</html>

View File

@ -0,0 +1,646 @@
:root {
--bg: #f5f7fb;
--paper: #ffffff;
--ink: #1f2937;
--muted: #64748b;
--line: #dbe3ef;
--blue: #2563eb;
--cyan: #0891b2;
--green: #059669;
--orange: #d97706;
--red: #dc2626;
}
* { box-sizing: border-box; }
body {
margin: 0;
font-family: "Segoe UI", "Microsoft YaHei", Arial, sans-serif;
background: var(--bg);
color: var(--ink);
line-height: 1.7;
}
a { color: var(--blue); text-decoration: none; }
a:hover { text-decoration: underline; }
.doc-shell {
width: min(1180px, calc(100vw - 40px));
margin: 0 auto;
padding: 28px 0 56px;
}
.doc-shell.narrow { width: min(920px, calc(100vw - 40px)); }
.doc-hero {
background: var(--paper);
border: 1px solid var(--line);
border-radius: 8px;
padding: 30px;
margin-bottom: 18px;
}
.doc-hero.compact { padding: 24px 28px; }
.eyebrow {
margin: 0 0 8px;
color: var(--cyan);
font-size: 12px;
font-weight: 800;
text-transform: uppercase;
letter-spacing: .08em;
}
h1, h2, h3 { line-height: 1.25; margin: 0; }
h1 { font-size: 34px; }
h2 { font-size: 22px; margin-bottom: 12px; }
h3 { font-size: 16px; margin-bottom: 8px; }
.lead {
margin: 14px 0 0;
color: var(--muted);
font-size: 16px;
}
.meta-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 12px;
margin-top: 22px;
}
.meta-grid div {
border: 1px solid var(--line);
border-radius: 6px;
padding: 10px 12px;
background: #f8fafc;
}
.meta-grid span {
display: block;
color: var(--muted);
font-size: 12px;
}
.meta-grid strong { font-size: 14px; }
.doc-toc {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin-bottom: 18px;
}
.doc-toc a {
background: var(--paper);
border: 1px solid var(--line);
border-radius: 6px;
padding: 8px 12px;
color: var(--ink);
font-size: 13px;
font-weight: 700;
}
.doc-section {
background: var(--paper);
border: 1px solid var(--line);
border-radius: 8px;
padding: 24px;
margin-bottom: 18px;
}
.doc-section.note {
border-left: 4px solid var(--orange);
}
.flow-grid,
.callout-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 14px;
}
.flow-grid.three,
.callout-grid {
grid-template-columns: repeat(3, minmax(0, 1fr));
}
.flow-grid article,
.callout-grid article,
.system-map a {
border: 1px solid var(--line);
border-radius: 8px;
padding: 16px;
background: #fbfdff;
}
.flow-grid p,
.callout-grid p { margin: 0; color: var(--muted); font-size: 14px; }
.split {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 18px;
}
.split p { color: var(--muted); margin-bottom: 0; }
.pill-list {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin-top: 14px;
}
.pill-list span {
background: #eff6ff;
color: #1d4ed8;
border: 1px solid #bfdbfe;
border-radius: 999px;
padding: 4px 10px;
font-size: 13px;
font-weight: 700;
}
.system-map {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(210px, 1fr));
gap: 12px;
}
.system-map strong,
.system-map span { display: block; }
.system-map span { color: var(--muted); font-size: 13px; margin-top: 4px; }
.doc-table {
width: 100%;
border-collapse: collapse;
}
.doc-table th,
.doc-table td {
border-bottom: 1px solid var(--line);
padding: 10px 12px;
text-align: left;
vertical-align: top;
}
.doc-table th {
color: var(--muted);
font-size: 12px;
background: #f8fafc;
}
.rule-list {
margin: 0;
padding-left: 22px;
}
.rule-list li { margin: 8px 0; }
.doc-footer {
color: var(--muted);
font-size: 14px;
padding: 8px 0 0;
}
/* Design tree landing page */
.tree-shell {
width: min(1280px, calc(100vw - 36px));
padding-top: 22px;
}
.design-tree {
min-height: calc(100vh - 44px);
display: flex;
flex-direction: column;
gap: 18px;
}
.tree-node {
display: inline-flex;
align-items: center;
justify-content: center;
border: 1px solid var(--line);
border-radius: 8px;
background: var(--paper);
color: var(--ink);
font-weight: 800;
text-decoration: none;
}
.tree-node:hover {
text-decoration: none;
border-color: #93c5fd;
color: var(--blue);
}
.tree-root {
align-self: center;
min-width: 220px;
padding: 16px 26px;
font-size: 26px;
border-color: #93c5fd;
background: #eff6ff;
color: #1d4ed8;
}
.tree-l1 {
width: 100%;
padding: 12px 18px;
font-size: 20px;
background: #f8fafc;
}
.tree-l2 {
width: 100%;
padding: 10px 14px;
font-size: 16px;
}
.tree-branches {
display: grid;
gap: 14px;
position: relative;
}
.tree-branches.two {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.tree-branches.three {
grid-template-columns: repeat(3, minmax(0, 1fr));
margin-top: 14px;
}
.tree-card {
border: 1px solid var(--line);
border-radius: 10px;
background: rgba(255,255,255,.74);
padding: 14px;
}
.tree-card.compact {
background: var(--paper);
}
.tree-leaves {
display: grid;
gap: 7px;
margin-top: 12px;
}
.tree-leaves.inline {
display: flex;
flex-wrap: wrap;
gap: 7px;
margin-top: 6px;
}
.tree-leaves a {
border: 1px solid var(--line);
border-radius: 7px;
padding: 7px 10px;
background: #fbfdff;
color: var(--ink);
font-size: 13px;
font-weight: 700;
text-decoration: none;
}
.tree-leaves a:hover {
border-color: #bfdbfe;
background: #eff6ff;
color: var(--blue);
}
.tree-group {
margin-top: 12px;
}
.tree-label {
color: var(--muted);
font-size: 12px;
font-weight: 800;
letter-spacing: .04em;
}
/* Product principles */
.principles-shell {
width: min(980px, calc(100vw - 40px));
min-height: 100vh;
display: flex;
align-items: flex-start;
}
.principles-page {
width: 100%;
margin-top: 28px;
}
.principles-header {
margin-bottom: 18px;
}
.principles-header h1 {
font-size: 34px;
}
.principles-list {
list-style: none;
padding: 0;
margin: 0 0 20px;
display: grid;
gap: 12px;
}
.principles-list li {
display: grid;
grid-template-columns: 72px minmax(0, 1fr);
align-items: center;
gap: 16px;
min-height: 86px;
padding: 18px 22px;
background: var(--paper);
border: 1px solid var(--line);
border-radius: 8px;
}
.principle-index {
display: inline-flex;
align-items: center;
justify-content: center;
width: 52px;
height: 52px;
border-radius: 8px;
background: #eff6ff;
color: #1d4ed8;
font-size: 18px;
font-weight: 900;
}
.principles-list strong {
font-size: 20px;
line-height: 1.45;
}
/* Hero foundation */
.hero-foundation-shell {
width: min(1280px, calc(100vw - 36px));
}
.class-grid {
display: grid;
grid-template-columns: repeat(5, minmax(0, 1fr));
gap: 12px;
}
.class-card {
border: 1px solid var(--line);
border-radius: 8px;
padding: 14px;
background: #fbfdff;
}
.class-rank {
display: inline-flex;
align-items: center;
justify-content: center;
width: 42px;
height: 42px;
border-radius: 8px;
margin-bottom: 12px;
color: #ffffff;
font-size: 20px;
font-weight: 900;
}
.class-card p {
margin: 0;
color: var(--muted);
font-size: 13px;
}
.class-xiang .class-rank { background: #0891b2; }
.class-hou .class-rank { background: #db2777; }
.class-wang .class-rank { background: #ca8a04; }
.class-che .class-rank { background: #dc2626; }
.class-ma .class-rank { background: #7c3aed; }
.role-table th,
.role-table td,
.faction-class-table th,
.faction-class-table td {
text-align: center;
white-space: nowrap;
}
.role-table td:first-child,
.faction-class-table td:first-child {
background: #f8fafc;
}
.grade-s {
color: #eab308;
font-weight: 900;
}
.check {
background: #86d47c;
color: #ffffff;
font-weight: 900;
}
.table-note {
margin: 10px 0 0;
color: var(--muted);
font-size: 13px;
}
.muted-cell {
color: var(--muted);
background: #f8fafc;
}
/* Narrative design archive */
.narrative-shell {
width: min(1320px, calc(100vw - 36px));
}
.source-strip {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin-top: 16px;
}
.source-strip span {
border: 1px solid var(--line);
border-radius: 6px;
background: #f8fafc;
color: var(--muted);
padding: 5px 9px;
font-size: 12px;
font-weight: 700;
}
.faction-nav {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin-bottom: 18px;
}
.faction-nav a {
border: 1px solid var(--line);
border-radius: 6px;
background: var(--paper);
color: var(--ink);
padding: 8px 12px;
font-size: 13px;
font-weight: 800;
text-decoration: none;
}
.faction-nav a:hover {
border-color: #bfdbfe;
background: #eff6ff;
color: var(--blue);
}
.faction-section {
scroll-margin-top: 18px;
}
.faction-head {
display: flex;
flex-wrap: wrap;
align-items: baseline;
justify-content: space-between;
gap: 10px;
margin-bottom: 14px;
}
.faction-head p {
margin: 0;
color: var(--muted);
font-size: 14px;
}
.hero-copy-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
gap: 12px;
}
.copy-card {
border: 1px solid var(--line);
border-radius: 8px;
background: #fbfdff;
padding: 14px;
}
.copy-card h3 {
display: flex;
align-items: baseline;
justify-content: space-between;
gap: 10px;
margin-bottom: 8px;
}
.copy-card small {
color: var(--muted);
font-size: 11px;
font-weight: 800;
}
.copy-subtitle {
margin: 0 0 8px;
color: var(--cyan);
font-size: 13px;
font-weight: 800;
}
.copy-desc,
.copy-diag,
.copy-task {
margin: 8px 0 0;
color: var(--ink);
font-size: 13px;
}
.copy-diag {
border-left: 3px solid var(--line);
color: #475569;
padding-left: 10px;
}
.copy-task {
color: var(--muted);
}
.copy-meta {
display: flex;
flex-wrap: wrap;
gap: 6px;
margin-top: 10px;
}
.copy-meta span {
border-radius: 999px;
background: #eef2ff;
color: #3730a3;
padding: 3px 8px;
font-size: 11px;
font-weight: 800;
}
.civilization-grid {
display: grid;
grid-template-columns: minmax(260px, .9fr) minmax(0, 1.1fr);
gap: 14px;
}
.city-list {
display: flex;
flex-wrap: wrap;
gap: 7px;
margin-top: 10px;
}
.city-list span {
border: 1px solid var(--line);
border-radius: 6px;
background: #ffffff;
padding: 5px 8px;
color: var(--muted);
font-size: 12px;
}
.empty-copy {
color: var(--muted);
font-style: italic;
}
@media (max-width: 760px) {
.doc-shell,
.doc-shell.narrow { width: min(100vw - 24px, 1180px); padding-top: 14px; }
.doc-hero,
.doc-section { padding: 18px; }
h1 { font-size: 26px; }
.meta-grid,
.flow-grid,
.flow-grid.three,
.callout-grid,
.split,
.tree-branches.two,
.tree-branches.three { grid-template-columns: 1fr; }
.tree-root { width: 100%; font-size: 22px; }
.principles-list li {
grid-template-columns: 1fr;
gap: 10px;
}
.principles-list strong { font-size: 18px; }
.class-grid { grid-template-columns: 1fr; }
.civilization-grid { grid-template-columns: 1fr; }
}

View File

@ -645,6 +645,125 @@ body::after {
.filter-select { cursor: pointer; }
.filter-select option { background: var(--bg-card); color: var(--text-primary); }
/* ========== Mechanics Documents ========== */
.mechanics-layout {
display: grid;
grid-template-columns: 310px minmax(0, 1fr);
gap: 16px;
height: calc(100vh - 100px);
min-height: 620px;
}
.mechanics-sidebar,
.mechanics-reader {
background: var(--bg-card);
border: 1px solid var(--border-color);
border-radius: 10px;
overflow: hidden;
}
.mechanics-sidebar {
display: flex;
flex-direction: column;
}
.mechanics-sidebar-head {
padding: 16px;
border-bottom: 1px solid var(--border-color);
}
.mechanics-title {
font-size: 15px;
font-weight: 800;
}
.mechanics-subtitle {
margin-top: 3px;
font-size: 11px;
color: var(--text-muted);
}
.mechanics-doc-list {
padding: 10px;
overflow-y: auto;
}
.mechanics-doc-btn {
width: 100%;
display: block;
text-align: left;
border: 1px solid transparent;
border-radius: 8px;
background: transparent;
padding: 11px 12px;
margin-bottom: 8px;
font-family: inherit;
cursor: pointer;
color: var(--text-primary);
}
.mechanics-doc-btn:hover {
background: rgba(59,130,246,0.05);
}
.mechanics-doc-btn.active {
border-color: rgba(59,130,246,0.35);
background: rgba(59,130,246,0.09);
}
.mechanics-doc-btn.disabled {
opacity: .45;
cursor: not-allowed;
}
.mechanics-doc-name,
.mechanics-doc-summary,
.mechanics-doc-meta {
display: block;
}
.mechanics-doc-name {
font-size: 13px;
font-weight: 800;
}
.mechanics-doc-summary {
margin-top: 4px;
font-size: 12px;
line-height: 1.45;
color: var(--text-secondary);
}
.mechanics-doc-meta {
margin-top: 6px;
font-size: 11px;
color: var(--text-muted);
}
.mechanics-reader {
min-width: 0;
}
.mechanics-frame {
width: 100%;
height: 100%;
border: 0;
background: #f5f7fb;
}
@media (max-width: 980px) {
.mechanics-layout {
grid-template-columns: 1fr;
height: auto;
}
.mechanics-sidebar {
max-height: 320px;
}
.mechanics-reader {
height: 720px;
}
}
/* ========== Story Module ========== */
.story-hero-card {
background: var(--bg-card); border: 1px solid var(--border-color);

View File

@ -66,6 +66,10 @@
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="12" y1="20" x2="12" y2="10"/><line x1="18" y1="20" x2="18" y2="4"/><line x1="6" y1="20" x2="6" y2="16"/></svg>
数值设计
</button>
<button class="sidebar-tab" data-tab="mechanics">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M4 4h16v16H4z"/><path d="M9 4v16M4 9h16"/><path d="M14 14h3v3h-3z"/></svg>
游戏机制
</button>
<button class="sidebar-tab" data-tab="story">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M4 19.5A2.5 2.5 0 016.5 17H20"/><path d="M6.5 2H20v20H6.5A2.5 2.5 0 014 19.5v-15A2.5 2.5 0 016.5 2z"/></svg>
文案剧情
@ -246,6 +250,23 @@
</div>
</div>
<div id="panel-mechanics" class="tab-panel">
<div class="mechanics-layout">
<aside class="mechanics-sidebar">
<div class="mechanics-sidebar-head">
<div class="mechanics-title">游戏机制文档</div>
<div class="mechanics-subtitle">Design/final/mechanics</div>
</div>
<div id="mechanics-doc-list" class="mechanics-doc-list">
<div class="loading-inline">正在加载机制文档...</div>
</div>
</aside>
<section class="mechanics-reader">
<iframe id="mechanics-frame" class="mechanics-frame" title="游戏机制文档"></iframe>
</section>
</div>
</div>
<div id="panel-story" class="tab-panel">
<!-- Sub-tabs -->
<div class="sub-tabs" id="story-sub-tabs">
@ -775,6 +796,7 @@
<script src="js/app.js"></script>
<script src="js/balance.js"></script>
<script src="js/mechanics.js"></script>
<script src="js/story.js"></script>
<script src="js/art.js"></script>
<script src="js/sentiment.js"></script>

View File

@ -683,6 +683,9 @@ function initTabs() {
document.querySelectorAll('.tab-panel').forEach(p => p.classList.remove('active'));
const target = document.getElementById('panel-' + tab.dataset.tab);
if (target) target.classList.add('active');
if (tab.dataset.tab === 'mechanics' && typeof mechanicsLoad === 'function') {
mechanicsLoad();
}
});
});
}

View File

@ -0,0 +1,62 @@
/**
* TH1 Dashboard - Mechanics Design Documents
*/
let mechanicsLoaded = false;
let mechanicsDocs = [];
async function mechanicsLoad() {
if (mechanicsLoaded) return;
mechanicsLoaded = true;
const list = document.getElementById('mechanics-doc-list');
const frame = document.getElementById('mechanics-frame');
if (!list || !frame) return;
try {
const resp = await fetch('/api/design/mechanics/list?t=' + Date.now());
if (!resp.ok) throw new Error(resp.status);
const data = await resp.json();
mechanicsDocs = data.docs || [];
mechanicsRenderList();
const first = mechanicsDocs.find(d => d.exists) || mechanicsDocs[0];
if (first) mechanicsOpen(first.file);
} catch (e) {
list.innerHTML = '<div class="loading-inline">机制文档加载失败</div>';
console.warn('mechanicsLoad failed:', e);
}
}
function mechanicsRenderList() {
const list = document.getElementById('mechanics-doc-list');
if (!list) return;
if (!mechanicsDocs.length) {
list.innerHTML = '<div class="loading-inline">暂无机制文档</div>';
return;
}
list.innerHTML = mechanicsDocs.map((doc, index) => {
const disabled = doc.exists ? '' : ' disabled';
const active = index === 0 ? ' active' : '';
const updated = doc.updatedAt ? new Date(doc.updatedAt * 1000).toLocaleDateString() : '';
return `<button class="mechanics-doc-btn${active}${disabled}" data-file="${escHtml(doc.file)}" ${doc.exists ? '' : 'disabled'}>
<span class="mechanics-doc-name">${escHtml(doc.title)}</span>
<span class="mechanics-doc-summary">${escHtml(doc.summary || '')}</span>
<span class="mechanics-doc-meta">${escHtml(doc.status || '')}${updated ? ' / ' + escHtml(updated) : ''}</span>
</button>`;
}).join('');
list.querySelectorAll('.mechanics-doc-btn').forEach(btn => {
btn.addEventListener('click', () => {
list.querySelectorAll('.mechanics-doc-btn').forEach(b => b.classList.remove('active'));
btn.classList.add('active');
mechanicsOpen(btn.dataset.file);
});
});
}
function mechanicsOpen(file) {
const frame = document.getElementById('mechanics-frame');
if (!frame || !file) return;
frame.src = '/api/design/mechanics/' + encodeURIComponent(file);
}

View File

@ -40,6 +40,7 @@ import uuid
import webbrowser
import urllib.request
import urllib.error
from urllib.parse import parse_qs, urlparse
from html.parser import HTMLParser
PORT = int(sys.argv[1]) if len(sys.argv) > 1 else 8080
@ -58,6 +59,67 @@ MKT_TODOS_FILE = os.path.join(SCRIPT_DIR, '..', '..', 'DOC', 'marketing', 'todos
OSS_DATA_DIR = os.path.join(SCRIPT_DIR, 'data', 'oss')
SNS_DIR = os.path.normpath(os.path.join(SCRIPT_DIR, '..', '..', 'DOC', 'sns'))
QUICK_REPLIES_FILE = os.path.join(SNS_DIR, 'quick_replies.json')
DESIGN_MECHANICS_DIR = os.path.normpath(os.path.join(SCRIPT_DIR, '..', '..', 'Design', 'final', 'mechanics'))
DESIGN_NARRATIVE_DIR = os.path.normpath(os.path.join(SCRIPT_DIR, '..', '..', 'Design', 'final', 'narrative'))
DESIGN_SHARED_DIR = os.path.normpath(os.path.join(SCRIPT_DIR, '..', '..', 'Design', 'shared'))
DESIGN_DOCS = [
{
'id': 'overview',
'title': '设计树',
'file': 'index.html',
'status': '首版',
'summary': '设计原则、游戏机制、文案剧情的树形入口。',
},
{
'id': 'design-principles',
'title': '设计原则',
'file': 'design-principles.html',
'status': '草案',
'summary': '设计树根节点,后续承载基础原则正文。',
},
{
'id': 'hero-foundation',
'title': '基础 / 英雄',
'file': 'hero-foundation.html',
'status': '草案',
'summary': '英雄基础定位与五职阶设计。',
},
{
'id': 'core-loop',
'title': '核心循环',
'file': 'core-loop.html',
'status': '首版',
'summary': '回合、行动、表现和结算的设计骨架。',
},
{
'id': 'hero-system',
'title': '东方英雄系统',
'file': 'hero-system.html',
'status': '首版',
'summary': '英雄作为战术核心、阵营识别和成长目标的设计说明。',
},
{
'id': 'faction-system',
'title': '阵营与文明系统',
'file': 'faction-system.html',
'status': '首版',
'summary': '文明基底与东方势力层的边界。',
},
{
'id': 'unit-skill-system',
'title': '单位与技能系统',
'file': 'unit-skill-system.html',
'status': '首版',
'summary': '基础单位、特色单位、英雄单位和技能分类。',
},
{
'id': 'map-city-tech',
'title': '地图、城市与科技',
'file': 'map-city-tech.html',
'status': '首版',
'summary': '经营层中地块、资源、城市、科技和文化卡的关系。',
},
]
def _load_bugs():
@ -207,6 +269,21 @@ class DashboardHandler(http.server.SimpleHTTPRequestHandler):
}
def do_GET(self):
if self.path.startswith('/api/design/mechanics/list'):
self._send_json(self._get_design_mechanics_docs())
return
if self.path.startswith('/api/design/mechanics/doc'):
self._handle_design_mechanics_doc()
return
if self.path.startswith('/api/design/mechanics/'):
self._handle_design_mechanics_static()
return
if self.path.startswith('/api/design/narrative/'):
self._handle_design_narrative_static()
return
if self.path.startswith('/api/design/shared/'):
self._handle_design_shared_asset()
return
if self.path.startswith('/api/bugs/list'):
self._send_json(_load_bugs())
return
@ -252,6 +329,91 @@ class DashboardHandler(http.server.SimpleHTTPRequestHandler):
return
super().do_GET()
def _get_design_mechanics_docs(self):
docs = []
for doc in DESIGN_DOCS:
path = os.path.join(DESIGN_MECHANICS_DIR, doc['file'])
item = dict(doc)
item['exists'] = os.path.exists(path)
item['updatedAt'] = os.path.getmtime(path) if item['exists'] else None
docs.append(item)
return {'base': 'Design/final/mechanics', 'docs': docs}
def _handle_design_mechanics_doc(self):
parsed = urlparse(self.path)
query = parse_qs(parsed.query)
file_name = query.get('file', ['index.html'])[0]
if os.path.basename(file_name) != file_name or not file_name.endswith('.html'):
self.send_error(400, 'Invalid design document path')
return
path = os.path.normpath(os.path.join(DESIGN_MECHANICS_DIR, file_name))
if not path.startswith(DESIGN_MECHANICS_DIR) or not os.path.exists(path):
self.send_error(404, 'Design document not found')
return
with open(path, 'rb') as f:
data = f.read()
self.send_response(200)
self.send_header('Content-Type', 'text/html; charset=utf-8')
self.send_header('Content-Length', str(len(data)))
self.end_headers()
self.wfile.write(data)
def _handle_design_mechanics_static(self):
parsed = urlparse(self.path)
file_name = parsed.path[len('/api/design/mechanics/'):]
if os.path.basename(file_name) != file_name or not file_name.endswith(('.html', '.md')):
self.send_error(400, 'Invalid design document path')
return
path = os.path.normpath(os.path.join(DESIGN_MECHANICS_DIR, file_name))
if not path.startswith(DESIGN_MECHANICS_DIR) or not os.path.exists(path):
self.send_error(404, 'Design document not found')
return
with open(path, 'rb') as f:
data = f.read()
content_type = 'text/html; charset=utf-8' if path.endswith('.html') else 'text/plain; charset=utf-8'
self.send_response(200)
self.send_header('Content-Type', content_type)
self.send_header('Content-Length', str(len(data)))
self.end_headers()
self.wfile.write(data)
def _handle_design_narrative_static(self):
parsed = urlparse(self.path)
file_name = parsed.path[len('/api/design/narrative/'):]
if os.path.basename(file_name) != file_name or not file_name.endswith('.html'):
self.send_error(400, 'Invalid narrative document path')
return
path = os.path.normpath(os.path.join(DESIGN_NARRATIVE_DIR, file_name))
if not path.startswith(DESIGN_NARRATIVE_DIR) or not os.path.exists(path):
self.send_error(404, 'Narrative document not found')
return
with open(path, 'rb') as f:
data = f.read()
self.send_response(200)
self.send_header('Content-Type', 'text/html; charset=utf-8')
self.send_header('Content-Length', str(len(data)))
self.end_headers()
self.wfile.write(data)
def _handle_design_shared_asset(self):
parsed = urlparse(self.path)
rel_path = parsed.path[len('/api/design/shared/'):]
if not rel_path or '..' in rel_path.replace('\\', '/').split('/'):
self.send_error(400, 'Invalid shared design asset path')
return
path = os.path.normpath(os.path.join(DESIGN_SHARED_DIR, rel_path))
if not path.startswith(DESIGN_SHARED_DIR) or not os.path.exists(path):
self.send_error(404, 'Shared design asset not found')
return
with open(path, 'rb') as f:
data = f.read()
content_type = 'text/css; charset=utf-8' if path.endswith('.css') else 'application/octet-stream'
self.send_response(200)
self.send_header('Content-Type', content_type)
self.send_header('Content-Length', str(len(data)))
self.end_headers()
self.wfile.write(data)
def do_POST(self):
if self.path == '/api/refresh':
self.handle_refresh()
@ -1669,7 +1831,7 @@ os.chdir(SCRIPT_DIR)
print(f'Checking port {PORT} for stale processes...')
kill_stale_servers(PORT)
with http.server.HTTPServer(('', PORT), DashboardHandler) as httpd:
with http.server.ThreadingHTTPServer(('', PORT), DashboardHandler) as httpd:
url = f'http://localhost:{PORT}'
print(f'TH1 Dashboard serving at {url}')
print('Press Ctrl+C to stop.')

View File

@ -1,5 +1,32 @@
@echo off
title TH1 Dashboard
cd /d "%~dp0"
python serve.py
set "PYTHON_EXE="
for %%P in (
"%LocalAppData%\Programs\Python\Python313\python.exe"
"%LocalAppData%\Programs\Python\Python312\python.exe"
"%LocalAppData%\Programs\Python\Python311\python.exe"
) do (
if not defined PYTHON_EXE if exist "%%~P" set "PYTHON_EXE=%%~P"
)
if not defined PYTHON_EXE (
for /f "delims=" %%P in ('where python 2^>nul') do (
echo %%P | findstr /I "\\WindowsApps\\python.exe" >nul
if errorlevel 1 if not defined PYTHON_EXE set "PYTHON_EXE=%%P"
)
)
if not defined PYTHON_EXE (
echo Could not find a real Python installation.
echo The WindowsApps python.exe launcher is not enough to run this dashboard.
echo Install Python or add Python to PATH, then run this file again.
pause
exit /b 1
)
echo Using Python: "%PYTHON_EXE%"
"%PYTHON_EXE%" serve.py
pause