This commit is contained in:
wuwenbo 2026-05-27 00:01:09 +08:00
commit a6450ad8b8
126 changed files with 42180 additions and 21636 deletions

View File

@ -0,0 +1 @@
0.4.21

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,217 @@
---
name: "th1-ai-nodes"
description: "TH1 AI行为树节点开发速查包含BaseActionTask/BaseCondition模板、Blackboard数据访问和命名规范"
---
# TH1 AI 节点开发速查
## AI 系统概述
TH1 使用 NodeCanvas 行为树框架,所有自定义节点位于 `BTNodeCanvas/` 目录。
## 关键基类
### BaseActionTask动作节点基类
位置:`BTNodeCanvas/BaseActionTask.cs`
大多数节点(包括"判断"节点)都继承 `BaseActionTask`,而不是 `BaseCondition`
```csharp
// 继承自 NodeCanvas.Framework.ActionTask
[Category("AI节点")]
[Serializable]
public class BaseActionTask : ActionTask
{
[SerializeField] private uint nodeId;
public uint NodeId { get; set; }
protected override void OnExecute() // 重写此方法实现逻辑
protected virtual string desc => string.Empty; // 重写以显示节点信息
// 调用 EndAction(bool success) 结束节点
}
```
### BaseCondition条件节点基类
位置:`BTNodeCanvas/BaseCondition.cs`
```csharp
// 继承自 NodeCanvas.Framework.ConditionTask
[Category("AI节点")]
[Serializable]
public class BaseCondition : ConditionTask
{
[SerializeField] private uint nodeId;
public uint NodeId { get; set; }
// 重写 OnCheck() 返回 bool
protected virtual string desc => string.Empty;
}
```
## 创建新的 AI 判断节点模板(使用 BaseActionTask
大部分判断节点实际使用 `BaseActionTask` 而非 `BaseCondition`,通过 `EndAction(true/false)` 返回结果。
```csharp
using System;
using Logic.AI;
using NodeCanvas.Framework;
using ParadoxNotion.Design;
namespace NodeCanvas.Tasks.Actions
{
[Name("判断描述(中文)")]
[Category("AI节点")]
[Serializable]
public class AIParam{Name} : BaseActionTask
{
// 可配置参数在NodeCanvas编辑器中显示
public bool GreaterThan = true;
public float Threshold = 0.5f;
// 节点显示信息
protected override string desc
{
get
{
if (GreaterThan) return $"某条件大于 {Threshold}";
return $"某条件小于 {Threshold}";
}
}
protected override void OnExecute()
{
base.OnExecute(); // 必须调用设置BTNodeId
// 从Blackboard获取AICalculatorData
var data = blackboard.GetVariable<AICalculatorData>("Data");
if (data?.value == null)
{
EndAction(false);
return;
}
// 获取单位数据
var unit = data.value.TargetParam.UnitData;
if (unit == null)
{
EndAction(false);
return;
}
// 业务逻辑判断
bool result = /* 你的判断逻辑 */;
EndAction(result);
}
}
}
```
## 创建新的 AI 执行节点模板
```csharp
using System;
using Logic.AI;
using NodeCanvas.Framework;
using ParadoxNotion.Design;
namespace NodeCanvas.Tasks.Actions
{
[Name("执行描述(中文)")]
[Category("AI节点")]
[Serializable]
public class AIAction{Name} : BaseActionTask
{
protected override string desc => "执行描述";
protected override void OnExecute()
{
base.OnExecute();
var data = blackboard.GetVariable<AICalculatorData>("Data");
if (data?.value == null)
{
EndAction(false);
return;
}
// 执行逻辑...
EndAction(true);
}
}
}
```
## 创建条件节点模板(使用 BaseCondition较少使用
```csharp
using System;
using NodeCanvas.Framework;
using ParadoxNotion.Design;
namespace NodeCanvas.Tasks.Actions
{
[Name("条件描述")]
[Category("AI节点")]
[Serializable]
public class AICondition{Name} : BaseCondition
{
protected override string desc => "条件描述";
protected override bool OnCheck()
{
// 返回 true/false
return true;
}
}
}
```
## Blackboard 数据访问
```csharp
// 获取AI计算数据最常用
var data = blackboard.GetVariable<AICalculatorData>("Data");
// AICalculatorData 包含:
// data.value.TargetParam.UnitData - 当前评估的目标单位
// data.value.Map - 当前地图数据
// data.value.AIActions - 可用的AI行为列表
// data.value.MaxAiAction - 评分最高的行为
```
## 必需 Attributes
| Attribute | 说明 | 必需 |
|-----------|------|------|
| `[Name("中文名")]` | 在NodeCanvas编辑器中显示的名字 | 推荐 |
| `[Category("AI节点")]` | 节点分类,必须是"AI节点" | **必需** |
| `[Serializable]` | 序列化支持 | **必需** |
## 命名规范
| 类型 | 格式 | 示例 |
|------|------|------|
| 判断节点 | `AIParam{描述}` | `AIParamHealth`, `AIParamLevel` |
| 执行节点 | `AI{Action描述}` | `AIExecuteAction`, `AICalculateAction` |
| 条件节点 | `AIParam{描述}``AICondition{描述}` | - |
| Foreach | `AIForeach{Start/End/Create}` | - |
## 关键注意事项
1. **base.OnExecute() 必须调用** - 它设置 `MainEditor.Instance.BTNodeId`
2. **EndAction() 必须调用** - 否则节点会卡住
3. **null检查** - data、value、UnitData 都需要逐级检查
4. **命名空间** - 固定使用 `NodeCanvas.Tasks.Actions`
5. **数据访问** - 通过 `Table.Instance` 访问配置表数据
6. **大部分"判断"节点用 BaseActionTask** - 通过 EndAction(bool) 返回结果
## 现有节点参考
- `AIParamHealth.cs` - 判断自身血量(典型判断节点模板)
- `AIExecuteAction.cs` / `AIExcuteAction.cs` - 执行行为
- `AICalculateAction.cs` - 评分策略
- `AIFinishAction.cs` - 结束行为
- `AIForeachStart.cs` / `AIForeachEnd.cs` - 循环控制

View File

@ -0,0 +1,209 @@
---
name: "th1-anim-scope"
description: "TH1攻击动画Scope-Aware模式速查。当修改SkillBase的OnDamageOther/OnDamaged等攻击相关生命周期中的视觉更新代码时必须使用此skill确保视觉更新延迟到攻击Fragment的正确阶段播放而不是在逻辑计算期间直接刷新Renderer。涵盖PendingAnimScope、FragmentStep、AnimPhase、step-list架构的完整用法。"
---
# TH1 攻击动画 Scope-Aware 模式速查
## 核心问题
TH1中`UnitAttackAction.Execute()` 的执行顺序是:
```
1. 完整逻辑运算数据变异扣血、加debuff等
2. 创建攻击 Fragment
3. 入队播放动画
```
逻辑运算期间,`DamageSettlement()` 会触发 SkillBase 的生命周期回调(`OnDamageOther``OnDamaged` 等)。如果这些回调中**直接调用** `InstantUpdateUnit()``RenderUpdateUnitImage()`会导致视觉状态血量、debuff图标在弹道动画播放之前就已经刷新——玩家先看到血量变化再看到弹道飞过去时序错误。
## 解决方案PendingAnimScope
`PendingAnimScope` 是一个 `IDisposable` 作用域对象,在 `UnitAttackAction.Execute()` 中通过 `using` 使用。技能回调中产生的视觉步骤不再直接执行,而是收集到 scope 中,等攻击 Fragment 创建后再注入。
### 时序(修复后)
```
1. scope = PresentationManager.BeginScope()
2. UnitLogic.Attack() → DamageSettlement()
├── target.Health -= dmg ← 数据变异
├── origin.OnDamageOther() ← 技能回调
│ └── scope.Add(FragmentStep) ← 延迟收集,不直接刷新
└── target.OnDamaged() ← 技能回调
└── scope.Add(FragmentStep) ← 延迟收集
3. Fragment 创建(如 FragmentAttack / FragmentAttackAndCounter
4. scope.FlushTo(fragment) ← 将收集的步骤注入 Fragment
5. PresentationManager.EnqueueTask(fragment)
6. scope.Dispose() ← using 自动清理
```
### Fragment 播放顺序
```
Phase 100 (AttackStart) → 弹道飞行 / 近战冲刺
Phase 200 (AttackImpact) → 命中:血量刷新 + 受伤弹跳 + VFX
Phase 250 (注入的步骤) → 技能视觉效果debuff图标等
Phase 300 (AttackReturn) → 间隔等待
Phase 400 (CounterStart) → 反击弹道
Phase 500 (CounterImpact) → 反击命中
Phase 550 (注入的步骤) → 反击阶段的技能视觉效果
Phase 600 (CounterReturn) → 反击返回
Phase 700 (Settle) → 收尾:全量刷新双方状态
```
## Scope-Aware 技能代码模板
当 SkillBase 的攻击相关生命周期回调需要刷新 Renderer 时,使用以下模板:
### 必要 using
```csharp
using TH1_Anim.Fragments;
using TH1_Core.Managers;
using TH1_Logic.Core;
using TH1Renderer;
```
### OnDamageOther 模板(攻击方技能)
```csharp
public override void OnDamageOther(MapData mapData, SettlementInfo info)
{
if (info.DamageTarget == null || info.DamageOrigin == null) return;
if (info.DamageType != DamageType.ActiveAttack
&& info.DamageType != DamageType.CounterAttack) return;
// === 逻辑部分 ===
info.DamageTarget.AddOrOverrideSkill(SkillType.XXX, mapData, info.DamageOrigin.Id);
// === 视觉部分scope-aware ===
if (mapData != Main.MapData) return; // AI预测不做视觉
if (!info.DamageTarget.IsAlive()) return; // 死亡单位不刷新
// 根据攻击类型选择正确的 phase
int phase = info.DamageType == DamageType.ActiveAttack
? AnimPhase.AttackImpact + 50 // 250攻击命中后
: AnimPhase.CounterImpact + 50; // 550反击命中后
var scope = PresentationManager.CurrentScope;
if (scope != null)
{
// 在攻击流程中:延迟注入到攻击 Fragment
if (MapRenderer.Instance != null &&
MapRenderer.Instance.ROUnitMap.TryGetValue(
info.DamageTarget.Id, out var unitRenderer))
{
scope.Add(new FragmentStep
{
Phase = phase,
Duration = 0.1f,
Execute = () => { unitRenderer.InstantUpdateUnit(false); }
});
}
}
else
{
// 不在攻击流程中:直接刷新(如其他逻辑触发)
if (MapRenderer.Instance != null &&
MapRenderer.Instance.ROUnitMap.TryGetValue(
info.DamageTarget.Id, out var unitRenderer))
{
unitRenderer.RenderUpdateUnitImage();
}
}
}
```
### OnDamaged 模板(被攻击方技能)
`OnDamageOther` 结构相同,注意目标单位 ID 的区别。
### EnqueueSkillEffect 模板(复杂多步视觉效果)
当技能需要播放 VFX + 单位刷新 + 死亡等多步骤效果时,使用 `PresentationManager.EnqueueSkillEffect()`,它内部已经是 scope-aware 的:
```csharp
var visualSteps = new List<SkillVisualStep>();
visualSteps.Add(new SkillVisualStep
{
GridRenderer = grid.Renderer(mapData),
UnitRenderer = unit.Renderer(mapData),
VFXList = new List<GridVFXParams> { new GridVFXParams(GridVFXType.Hurt) },
RefreshUnit = true,
KillUnit = false,
RefreshGrid = true,
});
PresentationManager.EnqueueSkillEffect(visualSteps, stepInterval: 0.15f);
```
`EnqueueSkillEffect` 在 scope 模式下会自动转为 `FragmentStep` 注入 scope非 scope 模式下走原有的直接入队逻辑。
## AnimPhase 常量
定义在 `TH1_Anim/Fragments/AnimPhase.cs`
| 常量 | 值 | 含义 |
|------|-----|------|
| `AttackStart` | 100 | 攻击弹道起飞 |
| `AttackImpact` | 200 | 攻击命中 |
| `AttackReturn` | 300 | 攻击返回/间隔 |
| `CounterStart` | 400 | 反击弹道起飞 |
| `CounterImpact` | 500 | 反击命中 |
| `CounterReturn` | 600 | 反击返回 |
| `Settle` | 700 | 收尾 |
技能注入步骤通常使用 `Phase + 50` 以排在原生步骤之后。
## 关键文件索引
| 文件 | 路径 | 职责 |
|------|------|------|
| PendingAnimScope | `TH1_Anim/Fragments/PendingAnimScope.cs` | 延迟步骤收集器 |
| FragmentStep | `TH1_Anim/Fragments/FragmentStep.cs` | 单步骤数据 |
| AnimPhase | `TH1_Anim/Fragments/AnimPhase.cs` | 阶段常量 |
| FragmentBase | `TH1_Anim/Fragments/FragmentBase.cs` | step-list 基础设施 |
| FragmentAttack | `TH1_Anim/Fragments/FragmentAttack.cs` | 纯攻击Fragment |
| FragmentAttackAndCounter | `TH1_Anim/Fragments/FragmentAttackAndCounter.cs` | 攻击+反击Fragment |
| FragmentSkillEffect | `TH1_Anim/Fragments/FragmentSkillEffect.cs` | 通用技能视觉Fragment |
| PresentationManager | `TH1_Core/Managers/PresentationManager.cs` | Scope管理 + EnqueueSkillEffect |
| UnitAttackAction | `TH1_Logic/Action/ActionLogic.cs` | scope使用入口 |
## 判断清单:何时需要 Scope-Aware
当你在 SkillBase 中看到以下调用,且该回调可能在攻击流程中被触发时,必须改为 scope-aware
- `unitRenderer.InstantUpdateUnit()`
- `unitRenderer.RenderUpdateUnitImage()`
- `unitRenderer.RenderUpdateUnitInfo()`
- `PresentationManager.EnqueueSkillEffect()`(已内置支持,无需手动处理)
- `PresentationManager.EnqueueTask()`直接入队Fragment
涉及的生命周期回调:
- `OnDamageOther` / `OnDamaged` / `AfterDamageOther`
- `BeforeDamageOther` / `BeforeDamagedSupportStage` / `BeforeDamagedTransformStage`
- `BeforeUnitDamaged`MapData级别遍历所有单位
## 已完成 Scope-Aware 改造的技能
| 技能 | 文件 | 回调 |
|------|------|------|
| SatoriAttackSkill | `AllSkill/SatoriAttackSkill.cs` | OnDamageOther |
| SatoriAttackBoomSkill | `AllSkill/SatoriAttackBoomSkill.cs` | OnDamageOther |
| SatoriBanSkill | `AllSkill/SatoriBanSkill.cs` | OnDamageOther |
| FearMakerSkill | `AllSkill/FearMakerSkill.cs` | OnDamageOther |
| SatoriSeeSkill | `AllSkill/SatoriSeeSkill.cs` | BeforeUnitDamaged |
## 尚未改造的技能(可能需要按需更新)
| 技能 | 文件 | 回调 | 直接调用 |
|------|------|------|----------|
| VampireSkill | `AllSkill/VampireSkill.cs` | OnDamageOther | InstantUpdateUnit |
| VampireProSkill | `AllSkill/VampireProSkill.cs` | OnDamageOther | InstantUpdateUnit |
| SuperDashSkill | `AllSkill/SuperDashSkill.cs` | OnDamageOther | InstantUpdateUnit |
| EscapeSkill | `AllSkill/EscapeSkill.cs` | OnDamageOther | InstantUpdateUnit |
| EscapeProSkill | `AllSkill/EscapeProSkill.cs` | OnDamageOther | InstantUpdateUnit |
| SanaeDivine_E3_HP_Skill | `AllSkill/SanaeDivine/...` | OnDamaged | InstantUpdateUnit |
| SplashSkill | `AllSkill/SplashSkill.cs` | OnDamageOther | InstantUpdateUnit |
| StompSkill | `AllSkill/StompSkill.cs` | OnDamageOther | InstantUpdateUnit |
| PathStompSkill | `AllSkill/PathStompSkill.cs` | OnDamageOther | InstantUpdateUnit |
| SkillBanBombSkill | `AllSkill/SkillBanBombSkill.cs` | OnDamaged | RenderUpdateUnitImage |

View File

@ -0,0 +1,110 @@
---
name: "th1-architecture"
description: "TH1项目架构总览包含模块结构、数据流、事件系统、关键管理器和文件索引"
---
# TH1 项目架构总览
## 概述
TH1 是一个 Unity 回合制策略游戏东方Project同人游戏使用 ET Framework 架构。
## 模块结构
| 模块 | 路径 | 职责 |
|------|------|------|
| `TH1_Core` | `TH1_Core/` | 核心管理器EventManager, UIManager, PresentationManager |
| `TH1_UI` | `TH1_UI/` | UI系统View-Controller 模式 |
| `TH1_Logic` | `TH1_Logic/` | 游戏逻辑AI, Skills, City/Unit/Player |
| `TH1_Data` | `TH1_Data/` | 数据类PlayerData, MapData, UnitData, GridData |
| `TH1_Renderer` | `TH1_Renderer/` | 视觉渲染MapRenderer, UnitRenderer, GridRenderer |
| `TH1_Config` | `TH1_Config/` | 配置系统Excel生成 + Partial扩展 |
| `TH1_Instance` | `TH1_Instance/` | 实例管理和基类 |
| `TH1_Presentation` | `TH1_Presentation/` | 表现层序列系统 |
| `TH1_Anim` | `TH1_Anim/` | 动画工具Animancer |
| `TH1_Audio` | `TH1_Audio/` | 音频管理 |
| `TH1_Resource` | `TH1_Resource/` | 资源缓存SpriteCache, AnimCache |
| `BTNodeCanvas` | `BTNodeCanvas/` | AI行为树节点NodeCanvas框架 |
## 数据流
```
Excel配置 → GenerateCS → MemoryPack序列化 → Runtime
用户输入 → InputLogic → GameLogic/UnitLogic/CityLogic → MapData → EventManager → UI更新 → Renderer
```
### 运行时数据访问
```csharp
// 地图数据(全局静态)
var mapData = Main.MapData;
// 玩家自身数据
var playerData = PlayerData.SelfPlayerData;
// 通过地图数据访问单位
mapData.GetUnitData(unitId);
// 通过地图数据获取玩家
mapData.GetPlayerDataByUnitId(unitId, out var player);
```
## 事件系统
### 全局事件EventManager
```csharp
// 订阅
EventManager.Instance.AddListener<EventType>(callback);
// 取消
EventManager.Instance.RemoveListener<EventType>(callback);
```
### UI事件UIEvents
定义在 `TH1_Core/Events/UIEvents.cs`,使用 struct 作为事件类型:
```csharp
// 事件定义格式
public struct ShowUI{Category}{Name} { /* 参数 */ }
public struct HideUI{Category}{Name} { }
```
## 关键管理器(单例模式)
| 管理器 | 路径 | 职责 |
|--------|------|------|
| `UIManager` | `TH1_Core/Managers/UIManager.cs` | UI根管理 |
| `EventManager` | `TH1_Core/Managers/EventManager.cs` | 全局事件分发 |
| `PresentationManager` | `TH1_Core/Managers/PresentationManager.cs` | 表现层序列 |
| `AudioManager` | `TH1_Logic/Core/` | 音频播放 |
| `ResourceCache` | `TH1_Resource/` | 精灵/动画缓存 |
| `PrefabPoolManager` | `TH1_Logic/PrefabPool/` | 对象池 |
| `Main` | `TH1_Logic/Core/Main.cs` | 游戏入口和单例 |
| `Table.Instance` | 配置表 | 运行时配置数据 |
## 关键文件索引
| 文件 | 路径 |
|------|------|
| ViewControllerManager | `TH1_UI/Core/ViewControllerManager.cs` |
| UIResourceName | `TH1_UI/Core/UIResourceName.cs` |
| UIEvents | `TH1_Core/Events/UIEvents.cs` |
| View基类 | `TH1_UI/View/Base/View.cs` |
| ViewController基类 | `TH1_UI/Controller/Base/ViewController.cs` |
| SkillBase | `TH1_Logic/Skill/SkillBase.cs` |
| SkillFactory | `TH1_Logic/Skill/SkillFactory.cs` |
| BaseActionTask | `BTNodeCanvas/BaseActionTask.cs` |
| BaseCondition | `BTNodeCanvas/BaseCondition.cs` |
| AICalculatorData | `TH1_Logic/AI/AIActionBase.cs` |
| GenerateCS目录 | `TH1_Config/GenerateCS/` |
| ExcelPartial目录 | `TH1_Config/ExcelPartial/` |
## 序列化规范MemoryPack
- 数据类使用 `[MemoryPackable]` + `partial class`
- 需要序列化的字段用 `[MemoryPackInclude]`
- 不序列化的字段用 `[MemoryPackIgnore]`
- 生成类必须有 `[MemoryPackConstructor]` 标记的无参构造函数
## 命名约定
- 文件名 = 类名(每个主要类一个文件)
- 命名空间跟随目录结构
- `.meta` 文件必须提交到版本控制
- 注释使用中文

View File

@ -0,0 +1,173 @@
---
name: "th1-config-guide"
description: "TH1配置系统指南包含GenerateCS/ExcelPartial双文件模式、MemoryPack规则和扩展模板"
---
# TH1 配置系统指南
## 配置系统架构
TH1 使用 Excel → 代码生成 → MemoryPack 序列化的配置流水线。
```
Excel表格 → 工具生成 → GenerateCS/*.cs自动生成不可修改
ExcelPartial/*.cs手动扩展可修改
```
## 目录结构
| 目录 | 说明 | 可否修改 |
|------|------|---------|
| `TH1_Config/GenerateCS/` | Excel自动生成的C#代码 | **禁止修改** |
| `TH1_Config/ExcelPartial/` | 手动编写的partial class扩展 | 可以修改 |
## GenerateCS 文件格式(不可修改)
```csharp
using MemoryPack;
using System;
using System.Collections.Generic;
namespace ExcelConfig
{
[MemoryPackable]
public partial class {Name}Category : ExcelConfigBase
{
[MemoryPackInclude]
public static Dictionary<int, {Name}> Dict { get; set; } = new();
public override void Init(byte[] data)
{
Dict = MemoryPackSerializer.Deserialize<Dictionary<int, {Name}>>(data);
}
}
[MemoryPackable]
public partial class {Name}
{
/// <summary>字段说明</summary>
[MemoryPackInclude]
public int Id { get; set; }
// ... 其他字段 ...
[MemoryPackConstructor]
public {Name}() { }
}
}
```
## ExcelPartial 扩展模板
当需要对配置数据进行后处理或添加运行时计算属性时,在 `ExcelPartial/` 中创建 partial class
### Category 扩展(后处理)
```csharp
using MemoryPack;
namespace ExcelConfig
{
public partial class {Name}Category
{
public override void AfterInit()
{
base.AfterInit();
// 在这里进行数据后处理
// 例如:建立索引、计算派生数据、验证数据
foreach (var kvp in Dict)
{
var config = kvp.Value;
// 后处理逻辑...
}
}
}
}
```
### 数据类扩展(添加计算属性)
```csharp
using MemoryPack;
namespace ExcelConfig
{
public partial class {Name}
{
// 运行时计算的属性,不序列化
[MemoryPackIgnore]
public float CalculatedValue => /* 计算逻辑 */;
// 运行时缓存字段,不序列化
[MemoryPackIgnore]
private List<int> _cachedList;
// 自定义方法
public bool IsSpecialType()
{
return /* 判断逻辑 */;
}
}
}
```
## MemoryPack 规则
### 必须遵守的规则
1. **`[MemoryPackable]`** - 所有需要序列化的类都必须标记
2. **`partial class`** - MemoryPack 要求 partial 类
3. **`[MemoryPackConstructor]`** - 反序列化用的无参构造函数
4. **`[MemoryPackInclude]`** - 标记需要序列化的字段/属性
5. **`[MemoryPackIgnore]`** - 标记不需要序列化的字段(**ExcelPartial中新增的字段必须用此标记**
### 常见错误
```csharp
// ❌ 错误ExcelPartial中新增字段没有标记MemoryPackIgnore
public partial class SomeConfig
{
public int NewField; // 会导致反序列化失败!
}
// ✅ 正确:
public partial class SomeConfig
{
[MemoryPackIgnore]
public int NewField; // 不参与序列化,仅运行时使用
}
```
## 配置数据访问
```csharp
// 通过 Table.Instance 访问
var aiConfig = AIConfigCategory.Dict[configId];
// 通过 Table.Instance 的特定方法
Table.Instance.UnitTypeDataAssets.GetUnitTypeInfo(unitType, giantType, level, out var info);
```
## 创建新配置扩展的步骤
1. 确认 `GenerateCS/` 中已有对应的生成代码
2. 在 `ExcelPartial/` 中创建 `{Name}Partial.cs`
3. 使用 `partial class` 关键字,命名空间必须是 `ExcelConfig`
4. 所有新增字段必须标记 `[MemoryPackIgnore]`
5. 通过 `AfterInit()` 进行初始化后处理
## 文件命名规范
| 类型 | 生成文件 | 扩展文件 |
|------|---------|---------|
| AI配置 | `GenerateCS/AIConfig.cs` | `ExcelPartial/AIConfigPartial.cs` |
| 其他配置 | `GenerateCS/{Name}.cs` | `ExcelPartial/{Name}Partial.cs` |
## 注意事项
- **永远不要修改 GenerateCS/ 中的文件** - 下次生成会覆盖
- ExcelPartial 中的 `AfterInit()` 在所有配置加载完成后调用
- 如果需要跨配置表的关联,在 `AfterInit()` 中建立引用
- 注意 Dictionary 的 Key 类型(通常是 int与 Excel 中的 Id 列对应

View File

@ -0,0 +1,216 @@
---
name: "th1-ui-patterns"
description: "TH1 UI开发模式速查包含View-Controller完整5步创建流程、命名规范和检查清单"
---
# TH1 UI 开发模式速查
## UI 架构概述
TH1 使用 View-Controller 模式(类似 MVC分为
- **View**MonoBehaviour挂在 prefab 上,管理视觉和动画
- **Controller**:纯 C# 类,管理数据绑定、事件注册和业务逻辑
## 创建新 UI 面板 - 完整5步流程
### Step 1: 创建 View 类
路径:`TH1_UI/View/{Category}/UI{Category}{Name}View.cs`
```csharp
using TH1_UI.View.Base;
using UnityEngine;
using UnityEngine.UI;
namespace TH1_UI.View.{Category}
{
public class UI{Category}{Name}View : View
{
// 1. 声明UI组件引用在Inspector中绑定
public Button CloseButton;
public Button BlockButton;
// 2. 声明委托事件供Controller绑定
public ViDelegateAssisstant.Dele OnBtnCloseClick;
// 3. 重写OnInit初始化
protected override void OnInit()
{
base.OnInit(); // 必须调用初始化Animancer和CanvasGroup
CloseButton.onClick.RemoveAllListeners();
CloseButton.onClick.AddListener(() => { OnBtnCloseClick.Invoke(); });
BlockButton.onClick.RemoveAllListeners();
BlockButton.onClick.AddListener(() => { OnBtnCloseClick.Invoke(); });
}
// 4. 数据设置方法
public void SetContent(/* 参数 */)
{
// 设置UI内容
}
// 5. 关闭时清理(可选)
public void CloseView()
{
// 清理工作
}
}
}
```
### Step 2: 创建 Controller 类
路径:`TH1_UI/Controller/{Category}/UI{Category}{Name}Controller.cs`
```csharp
using TH1_Core.Events;
using TH1_UI.Controller.Base;
using TH1_UI.View.{Category};
using UnityEngine;
namespace TH1_UI.Controller.{Category}
{
public class UI{Category}{Name}Controller : ViewController<UI{Category}{Name}View>
{
// 必须:空构造函数(满足 new() 泛型约束)
public UI{Category}{Name}Controller() { }
// 注册事件回调View打开时调用
protected override void RegisterEventCallback()
{
base.RegisterEventCallback();
if (WindowScript != null)
{
WindowScript.OnBtnCloseClick += _OnBtnCloseClick;
}
}
// 取消注册View关闭时调用
protected override void UnregisterEventCallback()
{
if (WindowScript != null)
{
WindowScript.OnBtnCloseClick = null;
}
base.UnregisterEventCallback();
}
// 打开时的逻辑
protected override void OnOpen()
{
base.OnOpen();
if (_openParameter is ShowUI{Category}{Name} evt)
{
if (WindowScript != null)
{
WindowScript.SetContent(/* 从evt取参数 */);
}
}
}
// 关闭时的逻辑
public override bool Close()
{
WindowScript?.CloseView();
return base.Close();
}
void _OnBtnCloseClick()
{
Close();
}
}
}
```
### Step 3: 注册 UIResourceName
文件:`TH1_UI/Core/UIResourceName.cs`
```csharp
public static readonly string View{Category}{Name} = "UI{Category}{Name}";
```
### Step 4: 定义 UIEvents
文件:`TH1_Core/Events/UIEvents.cs`
在对应的 Category 区域添加:
```csharp
//---------------------------------------- UI{Category} 相关的事件 -----------------------------
public struct ShowUI{Category}{Name} { /* 打开参数 */ }
public struct HideUI{Category}{Name} { }
```
### Step 5: 注册到 ViewControllerManager3处修改
文件:`TH1_UI/Core/ViewControllerManager.cs`
```csharp
// ① 添加公共属性 getter在对应 Category 区域的属性段)
public static UI{Category}{Name}Controller UI{Category}{Name}Controller { get { return _{camelCase}Controller; } }
// ② 在 _CreateAllViewControllers() 中添加创建代码
_{camelCase}Controller = _CreateView<UI{Category}{Name}Controller>("{Category}", UIResourceName.View{Category}{Name}, ViewDestroyDomain.None, UIRootType.{RootType});
// ③ 添加私有静态字段(在文件底部字段区域)
private static UI{Category}{Name}Controller _{camelCase}Controller = null;
```
## Category → UIRootType 映射表
| Category | UIRootType | SubFolderPath参数 | 说明 |
|----------|-----------|-------------------|------|
| Top | TopUI | "Top" | 顶部UI设置、胜利等 |
| Bottom | BottomUI | "Bottom" | 底部UI操作栏等 |
| Info | InfoUI | "Info" | 信息面板(外交、英雄等) |
| Notify | NotifyUI | "Notify" | 通知面板 |
| Outside | OutsideUI | "Outside" | 对局外UI菜单、多人等 |
| Outside(顶层) | OutsideTopUI | "Outside" | 对局外顶层UI |
| Announce | PresentationUI | "Presentation/Announce" | 公告表现 |
| Interaction | PresentationUI | "Presentation/Interaction" | 交互表现 |
## 命名规范
| 元素 | 格式 | 示例 |
|------|------|------|
| View类 | `UI{Category}{Name}View` | `UITopSettingView` |
| Controller类 | `UI{Category}{Name}Controller` | `UITopSettingController` |
| 资源名常量 | `View{Category}{Name}` | `ViewTopSetting` |
| Show事件 | `ShowUI{Category}{Name}` | `ShowUITopSetting` |
| Hide事件 | `HideUI{Category}{Name}` | `HideUITopSetting` |
| 私有字段 | `_{camelCase}Controller` | `_topSettingController` |
## View 基类关键方法
| 方法 | 说明 |
|------|------|
| `OnInit()` | 初始化,**必须调用 base.OnInit()** |
| `Show()` | 显示(播放打开动画) |
| `Hide(Action callback)` | 隐藏(播放关闭动画后回调) |
| `IsShow()` | 是否正在显示 |
## Controller 基类关键方法
| 方法 | 说明 |
|------|------|
| `OnLoaded()` | prefab加载完毕 |
| `OnOpen()` | 面板打开时 |
| `OnClose()` | 面板关闭时 |
| `RegisterEventCallback()` | 注册事件Open时自动调用 |
| `UnregisterEventCallback()` | 取消注册Close时自动调用 |
| `UpdateView()` | 刷新面板数据 |
| `OnMatchStart()` | 新对局开始时 |
| `WindowScript` | 获取关联的View实例 |
| `_openParameter` | Open时传入的参数 |
## 完成后检查清单
- [ ] View类继承自 `View`,调用了 `base.OnInit()`
- [ ] Controller类继承自 `ViewController<T>`,有空构造函数
- [ ] UIResourceName 中添加了资源名常量
- [ ] UIEvents 中定义了 Show/Hide 事件结构体
- [ ] ViewControllerManager 中完成3处注册属性、创建、字段
- [ ] using 引用了正确的命名空间
- [ ] ViewControllerManager 顶部添加了 Controller 的 using 语句

View File

@ -0,0 +1,163 @@
description = "TH1项目AI行为树节点开发专家负责创建和调试NodeCanvas行为树自定义节点。当需要创建AI判断节点、执行节点或调试AI行为时使用此agent。"
developer_instructions = """
# AI 行为树节点专家\r
\r
## 角色\r
\r
TH1 AI NodeCanvas \r
\r
## 核心理解\r
\r
### 关键设计决策\r
\r
**"判断"使 `BaseActionTask` `BaseCondition`**\r
\r
`EndAction(true/false)` `OnCheck()`使 `BaseCondition`\r
\r
### 节点创建流程\r
\r
1. //\r
2. 访\r
3. `BTNodeCanvas/` \r
4. 使 Attributes\r
5. `OnExecute()` \r
\r
### 判断节点模板(最常用)\r
\r
```csharp\r
using System;\r
using Logic.AI;\r
using NodeCanvas.Framework;\r
using ParadoxNotion.Design;\r
\r
namespace NodeCanvas.Tasks.Actions\r
{\r
[Name("判断描述(中文)")]\r
[Category("AI节点")]\r
[Serializable]\r
public class AIParam{Name} : BaseActionTask\r
{\r
// \r
public bool GreaterThan = true;\r
public float Threshold = 0.5f;\r
\r
protected override string desc\r
{\r
get\r
{\r
if (GreaterThan) return $"条件 >= {Threshold}";\r
return $"条件 <= {Threshold}";\r
}\r
}\r
\r
protected override void OnExecute()\r
{\r
base.OnExecute(); // \r
\r
var data = blackboard.GetVariable<AICalculatorData>("Data");\r
if (data?.value?.TargetParam.UnitData == null)\r
{\r
EndAction(false); // \r
return;\r
}\r
\r
var unit = data.value.TargetParam.UnitData;\r
// ...\r
EndAction(/* bool result */);\r
}\r
}\r
}\r
```\r
\r
### 执行节点模板\r
\r
```csharp\r
using System;\r
using Logic.AI;\r
using NodeCanvas.Framework;\r
using ParadoxNotion.Design;\r
\r
namespace NodeCanvas.Tasks.Actions\r
{\r
[Name("执行描述(中文)")]\r
[Category("AI节点")]\r
[Serializable]\r
public class AIAction{Name} : BaseActionTask\r
{\r
protected override string desc => "执行描述";\r
\r
protected override void OnExecute()\r
{\r
base.OnExecute();\r
\r
var data = blackboard.GetVariable<AICalculatorData>("Data");\r
if (data?.value == null)\r
{\r
EndAction(false);\r
return;\r
}\r
\r
// ...\r
EndAction(true);\r
}\r
}\r
}\r
```\r
\r
## 必需 Attributes 检查清单\r
\r
```csharp\r
[Name("中文描述名")] // NodeCanvas\r
[Category("AI节点")] // \r
[Serializable] // \r
```\r
\r
## Blackboard 数据访问\r
\r
```csharp\r
// \r
var data = blackboard.GetVariable<AICalculatorData>("Data");\r
\r
// \r
data.value.TargetParam.UnitData // \r
data.value.Map // (MapData)\r
data.value.AIActions // AI\r
data.value.MaxAiAction // \r
\r
// 访\r
Table.Instance.UnitTypeDataAssets.GetUnitTypeInfo(unitType, giantType, level, out var info);\r
```\r
\r
## 命名规范\r
\r
| | | |\r
|------|------|------|\r
| | `AIParam` | `AIParamHealth`, `AIParamLevel` |\r
| | `AI` + | `AIExecuteAction`, `AICalculateAction` |\r
| | `AIForeach` | `AIForeachStart`, `AIForeachEnd` |\r
\r
## 常见错误\r
\r
1. ** `base.OnExecute()`** \r
2. ** `EndAction()`** \r
3. ** `[Category("AI节点")]`** \r
4. ** `[Serializable]`** \r
5. ** null ** NullReferenceException \r
6. **** `NodeCanvas.Tasks.Actions`\r
\r
## 调试支持\r
\r
- `desc` NodeCanvas \r
- `MainEditor.Instance.BTNodeId` `base.OnExecute()` \r
- `#if UNITY_EDITOR` 块中的 `AIRecord` 用于AI调试记录\r
- F10/F11 AI \r
\r
## 参考文件\r
\r
- `BTNodeCanvas/BaseActionTask.cs` - \r
- `BTNodeCanvas/BaseCondition.cs` - \r
- `BTNodeCanvas/AIParamHealth.cs` - \r
- `BTNodeCanvas/AIExcuteAction.cs` - \r
- `BTNodeCanvas/AICalculateAction.cs` - \r
- `TH1_Logic/AI/AIActionBase.cs` - AICalculatorData """
name = "ai-designer"

View File

@ -0,0 +1,97 @@
description = "TH1项目代码审查专家负责对代码变更进行全面审查。只读agent不进行代码修改。当需要审查代码变更、检查项目规范合规性时使用此agent。"
developer_instructions = """
# 代码审查专家\r
\r
## 角色\r
\r
TH1 ****\r
\r
## 审查流程\r
\r
1. `git diff` `git status`\r
2. \r
3. \r
4. \r
\r
## 按域分组的审查清单\r
\r
### UI 审查清单\r
- [ ] View `View``OnInit()` `base.OnInit()`\r
- [ ] Controller `ViewController<T>`\r
- [ ] UIResourceName \r
- [ ] UIEvents Show/Hide \r
- [ ] ViewControllerManager 3getter\r
- [ ] ViewControllerManager using\r
- [ ] Category UIRootType \r
- [ ] `UI{Category}{Name}View/Controller` \r
- [ ] RegisterEventCallback / UnregisterEventCallback \r
\r
### 技能审查清单\r
- [ ] SkillBase使 `[MemoryPackable]` + `partial class`\r
- [ ] `[MemoryPackConstructor]` \r
- [ ] SkillType \r
- [ ] `GetSkillType()` SkillType\r
- [ ] SkillFactory \r
- [ ] GetAttackMultiplicationParam `1` `0`\r
- [ ] `BeforeActiveAttackOther` out `addDmg = 0`\r
- [ ] `[MemoryPackInclude]`\r
\r
### AI 节点审查清单\r
- [ ] `[Category("AI节点")]` `[Serializable]` \r
- [ ] `NodeCanvas.Tasks.Actions`\r
- [ ] `OnExecute()` `base.OnExecute()`\r
- [ ] `EndAction()`\r
- [ ] null \r
- [ ] `desc` \r
- [ ] 使 BaseActionTask BaseCondition\r
\r
### 配置审查清单\r
- [ ] `GenerateCS/` \r
- [ ] ExcelPartial 使 `partial class`\r
- [ ] `ExcelConfig`\r
- [ ] `[MemoryPackIgnore]`\r
- [ ] `AfterInit()` `base.AfterInit()`\r
\r
### 通用审查清单\r
- [ ] NullReferenceException \r
- [ ] \r
- [ ] magic number使\r
- [ ] \r
- [ ] \r
- [ ] \r
\r
## 输出格式\r
\r
```markdown\r
# 代码审查报告\r
\r
## 概要\r
- X\r
- YA / B / C\r
\r
## 严重问题 (必须修复)\r
### [文件名:行号] 问题标题\r
- ****\r
- ****\r
- ****\r
\r
## 警告 (建议修复)\r
### [文件名:行号] 问题标题\r
- ****\r
- ****\r
\r
## 建议 (可选改进)\r
### [文件名] 改进建议\r
- ****\r
\r
## 审查通过项 ✅\r
- \r
```\r
\r
## 注意事项\r
\r
- ****\r
- \r
- \r
- `.meta` """
name = "code-reviewer"

View File

@ -0,0 +1,121 @@
description = "TH1项目配置系统管理专家负责管理Excel生成的配置代码和partial class扩展。当需要添加配置后处理、运行时计算属性或处理MemoryPack序列化问题时使用此agent。"
developer_instructions = """
# 配置系统管理专家\r
\r
## 角色\r
\r
TH1 Excel partial class \r
\r
## 核心规则\r
\r
### 1. GenerateCS 不可修改\r
\r
`TH1_Config/GenerateCS/` Excel ****\r
\r
### 2. 扩展只在 ExcelPartial 中进行\r
\r
`TH1_Config/ExcelPartial/` \r
\r
### 3. MemoryPack 约束\r
\r
ExcelPartial **** `[MemoryPackIgnore]`\r
\r
## 工作流程\r
\r
### 添加配置后处理\r
\r
Excel \r
\r
1. `GenerateCS/` `{Name}Category` \r
2. `ExcelPartial/` `{Name}Partial.cs`\r
3. 使 partial class `{Name}Category`\r
4. `AfterInit()` \r
\r
```csharp\r
using MemoryPack;\r
\r
namespace ExcelConfig\r
{\r
public partial class {Name}Category\r
{\r
public override void AfterInit()\r
{\r
base.AfterInit();\r
\r
// \r
foreach (var kvp in Dict)\r
{\r
var config = kvp.Value;\r
// \r
}\r
}\r
}\r
}\r
```\r
\r
### 添加运行时计算属性\r
\r
\r
\r
```csharp\r
using MemoryPack;\r
\r
namespace ExcelConfig\r
{\r
public partial class {Name}\r
{\r
// [MemoryPackIgnore]\r
[MemoryPackIgnore]\r
public float CalculatedValue => SomeField * 0.5f;\r
\r
[MemoryPackIgnore]\r
private Dictionary<int, List<int>> _indexCache;\r
\r
public void BuildIndex()\r
{\r
_indexCache = new Dictionary<int, List<int>>();\r
// ...\r
}\r
}\r
}\r
```\r
\r
## 完成后检查清单\r
\r
- [ ] `GenerateCS/` \r
- [ ] `ExcelPartial/` \r
- [ ] `ExcelConfig`\r
- [ ] 使 `partial class` \r
- [ ] `[MemoryPackIgnore]`\r
- [ ] `AfterInit()` `base.AfterInit()`\r
- [ ] using `using MemoryPack;`\r
\r
## 数据访问\r
\r
```csharp\r
// 访\r
var config = {Name}Category.Dict[id];\r
\r
// Table.Instance\r
Table.Instance.{}.{}();\r
```\r
\r
## 常见问题\r
\r
### MemoryPack 反序列化失败\r
ExcelPartial `[MemoryPackIgnore]`\r
`[MemoryPackIgnore]`\r
\r
### AfterInit 中空引用\r
\r
使\r
\r
### Dict 为空\r
\r
Excel \r
\r
## 参考文件\r
\r
- `TH1_Config/GenerateCS/AIConfig.cs` - \r
- `TH1_Config/ExcelPartial/AIConfigPartial.cs` - """
name = "config-manager"

127
.codex/agents/debugger.toml Normal file
View File

@ -0,0 +1,127 @@
description = "TH1项目调试专家负责排查Bug、分析异常、定位问题根因。当遇到NullReference、序列化失败、UI不显示、AI异常、技能不生效等问题时使用此agent。"
developer_instructions = """
# 调试专家\r
\r
## 角色\r
\r
TH1 Bug\r
\r
## 常见问题模式及排查路径\r
\r
### 1. NullReferenceException\r
\r
****\r
- UI WindowScript nullprefab 访\r
- MapData 访\r
- View\r
- Dict \r
\r
****\r
1. \r
2. \r
3. Open/Close/Destroy \r
4. `data?.value?.TargetParam.UnitData` 访\r
\r
### 2. MemoryPack 序列化/反序列化失败\r
\r
****\r
- ExcelPartial `[MemoryPackIgnore]`\r
- GenerateCS \r
- `[MemoryPackConstructor]` \r
- MemoryPack \r
\r
****\r
1. `TH1_Config/ExcelPartial/` \r
2. `[MemoryPackIgnore]`\r
3. `GenerateCS/` \r
4. SkillBase `[MemoryPackable]` \r
\r
### 3. UI 不显示 / 显示异常\r
\r
****\r
- ViewControllerManager 3\r
- UIResourceName prefab \r
- prefab CanvasGroup \r
- UIRootType \r
- View `OnInit()` `base.OnInit()`\r
\r
****\r
1. `ViewControllerManager.cs` 3\r
2. `UIResourceName.cs` prefab \r
3. Unity prefab CanvasGroup\r
4. `Show()` / `Hide()` \r
5. `_canvasGroup.alpha` 0\r
\r
### 4. AI 行为树异常 / AI 不行动\r
\r
****\r
- `OnExecute()` `base.OnExecute()`\r
- `EndAction()` \r
- Blackboard `Data` null\r
- `[Serializable]` \r
- null`MaxAiAction == null`\r
\r
****\r
1. `EndAction()`\r
2. `base.OnExecute()` \r
3. 使 F10/F11 AI\r
4. `AIRecord` `StateRecord`\r
5. `AICalculatorData` \r
\r
### 5. 技能不生效\r
\r
****\r
- SkillType \r
- SkillFactory \r
- \r
- 0 10\r
- `IsPermanent` / `IsLevelSkill` \r
\r
****\r
1. SkillType \r
2. `SkillFactory.cs` \r
3. ISkill \r
4. `GetAttackMultiplicationParam` \r
5. `IsFinished()` true\r
\r
### 6. 事件系统问题\r
\r
****\r
- \r
- \r
- \r
- EventManager 使\r
\r
****\r
1. `AddListener` \r
2. `AddListener` `RemoveListener`\r
3. Controller `RegisterEventCallback` / `UnregisterEventCallback`\r
\r
## 关键调试文件索引\r
\r
| | |\r
|------|---------|\r
| UI | `TH1_UI/Core/ViewControllerManager.cs`, `TH1_UI/Controller/Base/ViewController.cs` |\r
| | `TH1_Core/Managers/EventManager.cs`, `TH1_Core/Events/UIEvents.cs` |\r
| | `TH1_Logic/Skill/SkillBase.cs`, `TH1_Logic/Skill/SkillFactory.cs` |\r
| AI | `BTNodeCanvas/BaseActionTask.cs`, `TH1_Logic/AI/AIActionBase.cs` |\r
| | `TH1_Config/GenerateCS/`, `TH1_Config/ExcelPartial/` |\r
| | `TH1_Logic/Core/Main.cs` |\r
| UI | `TH1_UI/DebugUI.cs` |\r
| | `TH1_Logic/Editor/` |\r
\r
## 调试工具\r
\r
- **F10/F11**AI \r
- **MainEditor.Instance**\r
- **DebugUI**`TH1_UI/DebugUI.cs` \r
- **AIRecord**AI \r
- **LogSystem.LogError/LogInfo**\r
\r
## 排查策略\r
\r
1. **** \r
2. **** \r
3. ****\r
4. ****"""
name = "debugger"

View File

@ -0,0 +1,216 @@
description = "TH1项目游戏逻辑开发专家负责开发技能系统(SkillBase派生类)、战斗逻辑、事件系统和核心游戏玩法。当需要创建新技能、修改战斗逻辑或处理事件系统时使用此agent。"
developer_instructions = """
# 游戏逻辑开发专家\r
\r
## 角色\r
\r
TH1 SkillBase\r
\r
## 技能系统\r
\r
### SkillType 枚举\r
\r
`TH1_Logic/Skill/SkillBase.cs` \r
\r
****SkillType \r
\r
### SkillBase 派生类模板\r
\r
```csharp\r
using System;\r
using System.Collections.Generic;\r
using MemoryPack;\r
using RuntimeData;\r
using UnityEngine;\r
\r
namespace Logic.Skill\r
{\r
[MemoryPackable]\r
public partial class Skill{Name} : SkillBase\r
{\r
[MemoryPackConstructor]\r
public Skill{Name}()\r
{\r
IsPermanent = true; // true=, false=\r
IsLevelSkill = false; // true=, false=\r
}\r
\r
public override SkillType GetSkillType()\r
{\r
return SkillType.{NAME};\r
}\r
\r
// ==================== ====================\r
\r
// \r
public override void OnTurnStart(IdentifierBase self, MapData mapData)\r
{\r
}\r
\r
// \r
public override void OnTurnEnd(IdentifierBase self, MapData mapData)\r
{\r
}\r
\r
// \r
public override void OnMove(UnitData self, GridData grid, MapData mapData, MoveType moveType, List<Vector2Int> path = null)\r
{\r
}\r
\r
// \r
public override void BeforeDamageOther(MapData mapData, SettlementInfo info)\r
{\r
}\r
\r
// \r
public override void OnDamaged(MapData mapData, SettlementInfo info)\r
{\r
}\r
\r
// \r
public override void OnDamageOther(MapData mapData, SettlementInfo info)\r
{\r
}\r
\r
// \r
public override void BeforeActiveAttackOther(MapData mapData, UnitData origin, UnitData target, out int addDmg)\r
{\r
addDmg = 0;\r
}\r
\r
// \r
public override void AfterActiveAttackOther(MapData mapData, UnitData origin, UnitData target)\r
{\r
}\r
\r
// ==================== ====================\r
\r
// \r
public override bool IsIgnoreControlArea(UnitData self, MapData mapData)\r
{\r
return false;\r
}\r
\r
// \r
public override bool IsCanBeKill(UnitData self, MapData mapData)\r
{\r
return true;\r
}\r
\r
// ==================== ====================\r
\r
// \r
public override float GetAttackAdditionParam(MapData mapData, UnitData self, UnitData target = null)\r
{\r
return 0;\r
}\r
\r
// \r
public override float GetAttackMultiplicationParam(MapData mapData, UnitData self, UnitData target = null)\r
{\r
return 1; // 10\r
}\r
\r
// \r
public override float GetDefenseAdditionParam(MapData mapData, UnitData self, UnitData target = null)\r
{\r
return 0;\r
}\r
\r
// \r
public override int GetExtraMoveRange(MapData mapData, UnitData self)\r
{\r
return 0;\r
}\r
\r
// \r
public override int GetExtraSight(UnitData self, MapData mapData)\r
{\r
return 0;\r
}\r
}\r
}\r
```\r
\r
### ISkill 接口方法分类\r
\r
#### 生命周期方法\r
| | |\r
|------|---------|\r
| `OnRefresh()` | |\r
| `OnMove()` | |\r
| `BeforeDamagedSupportStage()` | |\r
| `BeforeDamagedTransformStage()` | |\r
| `BeforeDamageOther()` | |\r
| `OnDamaged()` | |\r
| `OnDamageOther()` | |\r
| `AfterDamageOther()` | |\r
| `OnHealOther()` | |\r
| `BeforeActiveAttackOther()` | |\r
| `AfterActiveAttackOther()` | |\r
| `AfterActiveAttacked()` | |\r
| `OnTurnStart()` | |\r
| `OnTurnEnd()` | |\r
| `OnFinished()` | |\r
\r
#### 全局事件方法\r
| | |\r
|------|---------|\r
| `BeforeUnitDamaged()` | |\r
| `OnUnitDamaged()` | |\r
| `OnAnyUnitMove()` | |\r
| `OnAnyUnitDie()` | |\r
| `OnAnyUnitCreate()` | |\r
| `OnActionExecuted()` | |\r
\r
#### 判断属性方法(返回 bool\r
`IsLimitSelfMove`, `IsLimitSelfAttack`, `IsIgnoreControlArea`, `IsCanBeKill`, `IsCanBeDamaged`, `IsInvisible`, `IsCanMoveOnTerrain`, `IsIgnoreMoveLoss`, `IsCanTransport`, \r
\r
#### 数值获取方法\r
`GetExtraSight`, `GetAttackAdditionParam`, `GetAttackMultiplicationParam`, `GetDefenseAdditionParam`, `GetDefenseMultiplicationParam`, `GetExtraMoveRange`, `GetExtraAttackRange`, `GetCriticalHitRate`, \r
\r
****Multiplication `1` `0`\r
\r
### 创建新技能步骤\r
\r
1. `SkillType` \r
2. `TH1_Logic/Skill/` `Skill{Name}.cs`\r
3. 使 `[MemoryPackable]` + `partial class`\r
4. `SkillFactory.cs` \r
\r
## 数据访问模式\r
\r
```csharp\r
// \r
var mapData = Main.MapData;\r
\r
// \r
var playerData = PlayerData.SelfPlayerData;\r
\r
// \r
mapData.GetPlayerDataByUnitId(unitId, out var player);\r
\r
// \r
Table.Instance.UnitTypeDataAssets.GetUnitTypeInfo(unitType, giantType, level, out var info);\r
```\r
\r
## 事件系统用法\r
\r
```csharp\r
// \r
EventManager.Instance.AddListener<EventType>(OnEventHandler);\r
\r
// \r
EventManager.Instance.RemoveListener<EventType>(OnEventHandler);\r
\r
// \r
void OnEventHandler(EventType evt) { }\r
```\r
\r
## 关键参考文件\r
\r
- `TH1_Logic/Skill/SkillBase.cs` - ISkill\r
- `TH1_Logic/Skill/SkillFactory.cs` - \r
- `TH1_Logic/Core/Main.cs` - \r
- `TH1_Core/Managers/EventManager.cs` - """
name = "logic-developer"

View File

@ -0,0 +1,81 @@
description = "TH1项目UI面板构建专家负责创建和修改View-Controller模式的UI面板。当需要创建新UI面板、修改UI组件或处理View-Controller注册流程时使用此agent。"
developer_instructions = """
# UI 面板构建专家\r
\r
## 角色\r
\r
TH1 UI View-Controller UI 5\r
\r
## 工作流程\r
\r
UI \r
\r
### 1. 确认需求\r
- UI CategoryTop/Bottom/Info/Notify/Outside/Announce/Interaction\r
- \r
- \r
\r
### 2. 读取参考文件\r
\r
- `TH1_UI/Core/ViewControllerManager.cs` - \r
- `TH1_UI/Core/UIResourceName.cs` - \r
- `TH1_Core/Events/UIEvents.cs` - \r
\r
### 3. 按5步流程创建\r
\r
**Step 1**: View `TH1_UI/View/{Category}/UI{Category}{Name}View.cs`\r
- `TH1_UI.View.Base.View`\r
- `OnInit()` **** `base.OnInit()`\r
- 使 `ViDelegateAssisstant.Dele` \r
\r
**Step 2**: Controller `TH1_UI/Controller/{Category}/UI{Category}{Name}Controller.cs`\r
- `ViewController<UI{Category}{Name}View>`\r
- ****\r
- `RegisterEventCallback()` / `UnregisterEventCallback()` /View\r
- `OnOpen()` `_openParameter` \r
\r
**Step 3**: `UIResourceName.cs` \r
- `public static readonly string View{Category}{Name} = "UI{Category}{Name}";`\r
\r
**Step 4**: `UIEvents.cs` \r
- `public struct ShowUI{Category}{Name} { /* */ }`\r
- `public struct HideUI{Category}{Name} { }`\r
\r
**Step 5**: `ViewControllerManager.cs` 3\r
- getter\r
- `_CreateAllViewControllers()` \r
- \r
\r
### 4. 添加 using 语句\r
`ViewControllerManager.cs` Controller \r
\r
## Category → UIRootType 映射\r
\r
| Category | UIRootType | SubFolderPath |\r
|----------|-----------|---------------|\r
| Top | TopUI | "Top" |\r
| Bottom | BottomUI | "Bottom" |\r
| Info | InfoUI | "Info" |\r
| Notify | NotifyUI | "Notify" |\r
| Outside | OutsideUI | "Outside" |\r
| Announce | PresentationUI | "Presentation/Announce" |\r
| Interaction | PresentationUI | "Presentation/Interaction" |\r
\r
## 完成后自检\r
\r
\r
1. View `OnInit()` `base.OnInit()`\r
2. Controller \r
3. UIResourceName \r
4. UIEvents Show/Hide \r
5. ViewControllerManager 3\r
6. using \r
7. \r
\r
## 参考文件\r
\r
- View`TH1_UI/View/Top/UITopSettingView.cs`\r
- Controller`TH1_UI/Controller/Top/UITopSettingController.cs`\r
- View`TH1_UI/View/Base/View.cs`\r
- Controller`TH1_UI/Controller/Base/ViewController.cs`"""
name = "ui-builder"

44
.gitattributes vendored Normal file
View File

@ -0,0 +1,44 @@
* text=auto
# Unity text assets should keep stable line endings for readable diffs.
*.cs text eol=lf
*.asmdef text eol=lf
*.shader text eol=lf
*.cginc text eol=lf
*.mat text eol=lf
*.prefab text eol=lf
*.unity text eol=lf
*.asset text eol=lf
*.meta text eol=lf
# Project docs and scripts.
*.md text eol=lf
*.json text eol=lf
*.toml text eol=lf
*.yaml text eol=lf
*.yml text eol=lf
*.html text eol=lf
*.css text eol=lf
*.js text eol=lf
*.py text eol=lf
*.ps1 text eol=lf
*.bat text eol=crlf
# Binary assets.
*.bytes binary
*.png binary
*.jpg binary
*.jpeg binary
*.gif binary
*.webp binary
*.ico binary
*.ttf binary
*.otf binary
*.dll binary
*.exe binary
*.pdb binary
*.xlsx binary
*.xls binary
*.zip binary
*.7z binary
*.rar binary

5
.gitignore vendored
View File

@ -108,3 +108,8 @@ Tools/apply_translations.py
Tools/reapply_all.py
Tools/revert_inactive.py
Tools/Multilingual_p1.xlsx
# Python caches
__pycache__/
**/__pycache__/
*.py[cod]

83
AGENTS.md Normal file
View File

@ -0,0 +1,83 @@
# TH1 Codex Guide
This repository is a Unity 2022.3 LTS / ET Framework turn-based strategy game. The main playable client lives under `Unity/Assets/Scripts`, with supporting config, tools, backend notes, and generated analysis in the repository root.
## First Reads
- For architecture questions, read the `MD/GameMDFramework/00-*` overview document and the relevant subsystem document before editing.
- For code navigation, read `Unity/graphify-out/GRAPH_REPORT.md` for Unity code and `graphify-out/GRAPH_REPORT.md` for whole-repository context.
- If a task touches action execution, networking, localization, online errors, backend upload, or CrashSight reports, use the matching `.codex/skills/th1-*` skill first.
- Claude-era context remains in `.claude/` and `Unity/Assets/Scripts/CLAUDE.md`; treat it as historical source material, not the active entrypoint.
## Project Shape
- `Unity/Assets/Scripts/TH1_Core`: shared managers such as `EventManager`, `UIManager`, and `PresentationManager`.
- `Unity/Assets/Scripts/TH1_Data`: runtime data models including `MapData`, `PlayerData`, `UnitData`, `GridData`, and `NetData`.
- `Unity/Assets/Scripts/TH1_Logic`: gameplay rules, actions, skills, AI, Steam networking, editor tools, and game entrypoints.
- `Unity/Assets/Scripts/TH1_UI`: View/Controller UI architecture and UI resource registration.
- `Unity/Assets/Scripts/TH1_Renderer`: map, grid, city, unit, projectile, and effect renderers.
- `Unity/Assets/Scripts/TH1_Config`: generated Excel config classes plus partial extensions.
- `Unity/Assets/Scripts/BTNodeCanvas`: NodeCanvas AI behavior tree nodes.
- `MD/GameMDFramework`: human-maintained architecture documentation by subsystem.
## Development Rules
- Keep authoritative gameplay mutations inside the action flow when possible: construct `CommonActionParams`, validate with `CheckCan`, and execute through `ActionLogicBase.CompleteExecute`.
- Do not make UI-only, AI-only, or network-receiver-only data mutations that bypass the shared action layer unless the existing design for that subsystem already does so.
- For multiplayer-safe logic, avoid `UnityEngine.Random`, wall-clock time, unordered iteration, and direct `Main.MapData` assumptions inside simulated or synchronized execution paths.
- For Excel-backed keys such as `UnitFullType`, `SkillType` level pairs, resources, terrain, and tech atoms, inspect the actual config rows before coding. Do not infer current data from similar entries.
- Unity `.meta` files are part of source control and must be preserved when assets move or are created.
- Generated files should only be edited through the project generator or documented editor workflow.
## Codex Skills And Agents
- Prefer `.codex/skills` for current project-specific workflows:
- `th1-action-logic`: action execution, AI action flow, turn actions, and action-triggered skills.
- `th1-network-sync`: Steam lobby, P2P, multiplayer save/recovery, action sync, and deterministic network behavior.
- `th1-multilingual`: localization import/export, active text scanning, duplicate IDs, and translation diagnostics.
- `th1-server-backend`: Aliyun Function Compute, OSS/STS upload, collect data, and backend debugging.
- `th1-online-debug`: production/obfuscated stack decoding and online issue tracing.
- `th1-crashsight-daily`: daily CrashSight triage and report generation.
- `.agents/skills` contains migrated Claude-era reference skills, including `graphify`, `th1-architecture`, `th1-ui-patterns`, `th1-config-guide`, `th1-ai-nodes`, and `th1-anim-scope`.
- `.codex/agents` contains migrated specialist agent profiles. Use them as role guidance for large investigations, but keep the final integration decisions in this main workspace.
## Graphify
- There are two graphify scopes:
- Root `graphify-out/`: whole repository overview.
- `Unity/graphify-out/`: Unity client code overview.
- Large graphify files such as `graph.json`, `cache/`, `converted/`, and `obsidian/` are intentionally ignored. Commit only lightweight reports and metadata such as `GRAPH_REPORT.md`, `manifest.json`, and `cost.json`.
- After code changes, the git `post-commit` hook runs `Tools/GraphifyPostCommit.ps1` in the background to refresh code graphs without LLM cost.
- To run the hook manually:
```powershell
Tools/GraphifyPostCommit.ps1
```
- To reinstall the local git hook after cloning or moving the repository:
```powershell
Tools/InstallGraphifyHook.ps1
```
- To test what the hook would do without rebuilding:
```powershell
Tools/GraphifyPostCommit.ps1 -DryRun
```
## Verification
- For most Unity C# changes, run:
```powershell
dotnet build Unity/Assembly-CSharp.csproj --no-restore
```
- For editor tooling, config windows, localization editor, OSS editor, or generated editor integration, also run:
```powershell
dotnet build Unity/Assembly-CSharp-Editor.csproj --no-restore
```
- Some behavior still requires Unity Editor validation, especially UI prefabs, animation sequencing, Steam networking, save/load, replay/spectator, and AI behavior tree changes.

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/`.

View File

@ -1,5 +1,5 @@
{
"nextId": 258,
"nextId": 267,
"bugs": [
{
"id": 2,
@ -2550,6 +2550,96 @@
"module": "",
"createdAt": 1779506906721,
"updatedAt": 1779506906721
},
{
"id": 258,
"title": "我联机打的时候楼陀罗火车打守矢的那个大白蛇会有bug",
"description": "",
"status": "open",
"priority": "medium",
"module": "",
"createdAt": 1779523517289,
"updatedAt": 1779523517289
},
{
"id": 259,
"title": "所有巨人单位应该是LV6以上升级可选",
"description": "",
"status": "open",
"priority": "medium",
"module": "",
"createdAt": 1779537607662,
"updatedAt": 1779537607662
},
{
"id": 260,
"title": "间谍攻城的时候城里也有间谍怎么办?",
"description": "",
"status": "open",
"priority": "medium",
"module": "",
"createdAt": 1779538468082,
"updatedAt": 1779538468082
},
{
"id": 261,
"title": "玩其他游戏时好像邀请不了",
"description": "",
"status": "open",
"priority": "medium",
"module": "",
"createdAt": 1779539073924,
"updatedAt": 1779539073924
},
{
"id": 262,
"title": "话说什么时候恋升级不可视 升个级还能被人看到(",
"description": "",
"status": "open",
"priority": "medium",
"module": "",
"createdAt": 1779539151076,
"updatedAt": 1779539151076
},
{
"id": 263,
"title": "咲夜/妹红的Lv3 技能描述问题",
"description": "",
"status": "open",
"priority": "medium",
"module": "",
"createdAt": 1779597876284,
"updatedAt": 1779597876284
},
{
"id": 264,
"title": "早苗行动点问题",
"description": "",
"status": "open",
"priority": "medium",
"module": "",
"createdAt": 1779599217683,
"updatedAt": 1779599217683
},
{
"id": 265,
"title": "热插拔",
"description": "",
"status": "open",
"priority": "medium",
"module": "",
"createdAt": 1779610731847,
"updatedAt": 1779610731847
},
{
"id": 266,
"title": "遇到個挺神秘的bug在戀戀被擊殺的被動觸發以後我的單位同時被觸發的被動擊殺後她們部隊兵棋還會留在地圖上p1p2 在此之後整個地靈殿陣營的部隊都會隱身直到占領我方地區的時候才會出現有點像瞬移一樣XDbug出現後游戲回合在超過30回合也不會結束(p3p4)",
"description": "",
"status": "open",
"priority": "medium",
"module": "",
"createdAt": 1779776751092,
"updatedAt": 1779776751092
}
]
}

4
DOC/suggestions.json Normal file
View File

@ -0,0 +1,4 @@
{
"nextId": 1,
"suggestions": []
}

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

@ -53,6 +53,11 @@
BUG跟踪
</button>
<button class="sidebar-tab" data-tab="suggestions">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 15a4 4 0 01-4 4H8l-5 3V7a4 4 0 014-4h10a4 4 0 014 4z"/><path d="M8 8h8M8 12h5"/></svg>
建议跟踪
</button>
<div class="nav-section-label">分析模块</div>
<button class="sidebar-tab" data-tab="gamebalance">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 3v18M3 12h18M5.63 5.63l12.74 12.74M18.37 5.63L5.63 18.37"/></svg>
@ -66,6 +71,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 +255,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">
@ -436,6 +462,35 @@
<div id="bugs-list"></div>
</div>
<!-- ===== Suggestion Tracker Panel ===== -->
<div id="panel-suggestions" class="tab-panel">
<!-- Header bar -->
<div class="bug-header-bar">
<div class="bug-header-left">
<span class="bug-header-title">建议跟踪</span>
<span class="bug-header-count" id="suggestions-count">加载中...</span>
</div>
<button class="bug-btn bug-btn-primary" onclick="suggestionsShowAddModal()">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></svg>
新增建议
</button>
</div>
<!-- Toolbar -->
<div class="toolbar">
<input type="text" id="suggestions-search" class="search-input" placeholder="搜索编号、标题或描述...">
<select id="suggestions-status-filter" class="filter-select">
<option value="">全部状态</option>
<option value="open">未处理</option>
<option value="processed">已处理</option>
</select>
<select id="suggestions-module-filter" class="filter-select">
<option value="">全部模块</option>
</select>
</div>
<!-- Suggestion List -->
<div id="suggestions-list"></div>
</div>
<!-- ===== SNS Assistant Panel ===== -->
<div id="panel-sns" class="tab-panel">
<!-- Sub-tabs -->
@ -775,6 +830,7 @@
<script src="js/app.js"></script>
<script src="js/balance.js"></script>
<script src="js/mechanics.js?v=20260526-1"></script>
<script src="js/story.js"></script>
<script src="js/art.js"></script>
<script src="js/sentiment.js"></script>
@ -783,6 +839,7 @@
<script src="js/devplan.js"></script>
<script src="js/version.js"></script>
<script src="js/bugs.js"></script>
<script src="js/suggestions.js"></script>
<script src="js/gamebalance.js"></script>
<script src="js/sns.js"></script>
<script src="js/quick_replies.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,93 @@
/**
* TH1 Dashboard - Mechanics Design Documents
*/
let mechanicsLoaded = false;
let mechanicsDocs = [];
function mechanicsEscHtml(str) {
return String(str ?? '').replace(/[&<>"']/g, ch => ({
'&': '&amp;',
'<': '&lt;',
'>': '&gt;',
'"': '&quot;',
"'": '&#39;'
}[ch]));
}
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 mechanicsLoadIfVisible() {
const panel = document.getElementById('panel-mechanics');
if (panel && panel.classList.contains('active')) {
mechanicsLoad();
}
}
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="${mechanicsEscHtml(doc.file)}" ${doc.exists ? '' : 'disabled'}>
<span class="mechanics-doc-name">${mechanicsEscHtml(doc.title)}</span>
<span class="mechanics-doc-summary">${mechanicsEscHtml(doc.summary || '')}</span>
<span class="mechanics-doc-meta">${mechanicsEscHtml(doc.status || '')}${updated ? ' / ' + mechanicsEscHtml(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);
}
window.mechanicsLoad = mechanicsLoad;
window.mechanicsOpen = mechanicsOpen;
document.addEventListener('DOMContentLoaded', () => {
document.querySelectorAll('.sidebar-tab[data-tab="mechanics"]').forEach(tab => {
tab.addEventListener('click', () => {
window.setTimeout(mechanicsLoad, 0);
});
});
mechanicsLoadIfVisible();
});
window.addEventListener('hashchange', mechanicsLoadIfVisible);

View File

@ -0,0 +1,327 @@
/**
* TH1 Dashboard - Suggestion Tracker Module
*
* Data source: DOC/suggestions.json (project-level, version controlled)
* Server API:
* GET /api/suggestions/list
* POST /api/suggestions/create
* POST /api/suggestions/update
* POST /api/suggestions/delete
*/
let suggestionsList = [];
let suggestionsLoaded = false;
const SUGGESTION_STATUSES = {
open: { label: '未处理', color: '#d97706', bg: '#fffbeb' },
processed: { label: '已处理', color: '#16a34a', bg: '#f0fdf4' },
};
const SUGGESTION_MODULES = [
'战斗系统', '技能系统', 'AI系统', 'UI界面', '地图系统',
'科技树', '存档系统', '多语言', '美术资源', '配置数据',
'网络/联机', '音频', '性能', '其他'
];
async function suggestionsLoadAll() {
if (suggestionsLoaded) return;
try {
const r = await fetch('/api/suggestions/list?t=' + Date.now());
if (!r.ok) throw new Error(r.status);
const data = await r.json();
suggestionsList = data.suggestions || [];
} catch (e) {
console.warn('suggestionsLoad fail:', e);
suggestionsList = [];
}
suggestionsLoaded = true;
suggestionsRenderList();
}
function suggestionsRenderList() {
const container = document.getElementById('suggestions-list');
const countEl = document.getElementById('suggestions-count');
if (!container) return;
const searchQ = (document.getElementById('suggestions-search')?.value || '').trim().toLowerCase();
const statusF = document.getElementById('suggestions-status-filter')?.value || '';
const moduleF = document.getElementById('suggestions-module-filter')?.value || '';
let filtered = [...suggestionsList];
if (searchQ) {
filtered = filtered.filter(s =>
String(s.id).includes(searchQ) ||
(s.title || '').toLowerCase().includes(searchQ) ||
(s.description || '').toLowerCase().includes(searchQ)
);
}
if (statusF) filtered = filtered.filter(s => s.status === statusF);
if (moduleF) filtered = filtered.filter(s => s.module === moduleF);
filtered.sort((a, b) => b.id - a.id);
if (countEl) {
const total = suggestionsList.length;
const openCount = suggestionsList.filter(s => s.status !== 'processed').length;
countEl.textContent = `${total} 条建议,${openCount} 条未处理`;
}
if (filtered.length === 0) {
container.innerHTML = '<div class="loading-inline">暂无匹配的建议记录</div>';
return;
}
container.innerHTML = filtered.map(s => {
const st = SUGGESTION_STATUSES[s.status] || SUGGESTION_STATUSES.open;
const dateStr = s.createdAt ? new Date(s.createdAt).toLocaleDateString('zh-CN') : '';
const showDoneBtn = s.status !== 'processed';
const doneBtn = showDoneBtn ? `<button class="bug-btn bug-btn-sm bug-btn-primary" onclick="suggestionsQuickProcess(${s.id}, event)" title="标记为已处理" style="margin-right:4px">处理</button>` : '';
return `<div class="bug-card" onclick="suggestionsShowDetail(${s.id})">
<div class="bug-card-left">
<span class="bug-card-id">#${String(s.id).padStart(3, '0')}</span>
<span class="bug-card-title">${suggestionsEsc(s.title)}</span>
${s.module ? `<span class="bug-card-module">${suggestionsEsc(s.module)}</span>` : ''}
</div>
<div class="bug-card-right">
${doneBtn}
<span class="bug-card-status" style="color:${st.color};background:${st.bg}">${st.label}</span>
<span class="bug-card-date">${dateStr}</span>
</div>
</div>`;
}).join('');
}
function suggestionsEsc(s) {
if (!s) return '';
return s.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
}
function suggestionsShowDetail(id) {
const suggestion = suggestionsList.find(s => s.id === id);
if (!suggestion) return;
const st = SUGGESTION_STATUSES[suggestion.status] || SUGGESTION_STATUSES.open;
const dateStr = suggestion.createdAt ? new Date(suggestion.createdAt).toLocaleString('zh-CN') : '';
const updStr = suggestion.updatedAt ? new Date(suggestion.updatedAt).toLocaleString('zh-CN') : '';
const statusOpts = Object.entries(SUGGESTION_STATUSES).map(([k, v]) =>
`<option value="${k}" ${suggestion.status === k ? 'selected' : ''}>${v.label}</option>`
).join('');
const overlay = document.createElement('div');
overlay.className = 'bug-detail-overlay';
overlay.innerHTML = `
<div class="bug-detail-modal">
<div class="bug-detail-header">
<div>
<div class="bug-detail-id">#${String(suggestion.id).padStart(3, '0')}</div>
<div class="bug-detail-title">${suggestionsEsc(suggestion.title)}</div>
<div class="bug-detail-meta">
<span class="bug-card-status" style="color:${st.color};background:${st.bg}">${st.label}</span>
${suggestion.module ? `<span class="bug-card-module">${suggestionsEsc(suggestion.module)}</span>` : ''}
<span>创建: ${dateStr}</span>
${updStr ? `<span>更新: ${updStr}</span>` : ''}
</div>
</div>
<button class="bug-detail-close" onclick="this.closest('.bug-detail-overlay').remove()">&times;</button>
</div>
<div class="bug-detail-body">
${suggestion.description ? `<div class="bug-detail-desc">${suggestionsEsc(suggestion.description).replace(/\n/g, '<br>')}</div>` : '<div class="bug-detail-desc" style="color:#94a3b8">无详细描述</div>'}
</div>
<div class="bug-detail-footer">
<div class="bug-detail-status-change">
<label>状态</label>
<select id="suggestion-status-select" class="filter-select" onchange="suggestionsUpdateStatus(${suggestion.id}, this.value)">${statusOpts}</select>
</div>
<div>
<button class="bug-btn bug-btn-danger" onclick="suggestionsDelete(${suggestion.id})">删除</button>
</div>
</div>
</div>
`;
document.body.appendChild(overlay);
const handler = (e) => {
if (e.key === 'Escape') {
overlay.remove();
document.removeEventListener('keydown', handler);
}
};
document.addEventListener('keydown', handler);
overlay.addEventListener('click', (e) => { if (e.target === overlay) overlay.remove(); });
}
function suggestionsShowAddModal() {
const moduleOpts = SUGGESTION_MODULES.map(m => `<option value="${m}">${m}</option>`).join('');
const overlay = document.createElement('div');
overlay.className = 'bug-detail-overlay';
overlay.id = 'suggestion-add-overlay';
overlay.innerHTML = `
<div class="bug-add-modal">
<div class="bug-detail-header">
<div class="bug-detail-title">新增建议</div>
<button class="bug-detail-close" onclick="this.closest('.bug-detail-overlay').remove()">&times;</button>
</div>
<div class="bug-add-body">
<div class="bug-form-group">
<label class="bug-form-label">标题 *</label>
<input type="text" id="suggestion-add-title" class="bug-form-input" placeholder="简要描述建议内容">
</div>
<div class="bug-form-group">
<label class="bug-form-label">模块</label>
<select id="suggestion-add-module" class="bug-form-select">
<option value="">未分类</option>
${moduleOpts}
</select>
</div>
<div class="bug-form-group">
<label class="bug-form-label">详细描述</label>
<textarea id="suggestion-add-desc" class="bug-form-textarea" rows="6" placeholder="记录建议背景、期望调整、玩家反馈来源等..."></textarea>
</div>
</div>
<div class="bug-detail-footer">
<button class="bug-btn" onclick="document.getElementById('suggestion-add-overlay').remove()">取消</button>
<button class="bug-btn bug-btn-primary" onclick="suggestionsSubmitAdd()">提交建议</button>
</div>
</div>
`;
document.body.appendChild(overlay);
setTimeout(() => document.getElementById('suggestion-add-title')?.focus(), 100);
const handler = (e) => {
if (e.key === 'Escape') {
overlay.remove();
document.removeEventListener('keydown', handler);
}
};
document.addEventListener('keydown', handler);
overlay.addEventListener('click', (e) => { if (e.target === overlay) overlay.remove(); });
}
async function suggestionsSubmitAdd() {
const title = document.getElementById('suggestion-add-title').value.trim();
if (!title) { alert('请输入建议标题'); return; }
const module = document.getElementById('suggestion-add-module').value;
const description = document.getElementById('suggestion-add-desc').value.trim();
const btn = document.querySelector('#suggestion-add-overlay .bug-btn-primary');
if (btn) { btn.disabled = true; btn.textContent = '提交中...'; }
try {
const r = await fetch('/api/suggestions/create', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ title, module, description })
});
const result = await r.json();
if (result.success) {
document.getElementById('suggestion-add-overlay')?.remove();
suggestionsLoaded = false;
await suggestionsLoadAll();
suggestionsShowToast(`建议 #${String(result.id).padStart(3, '0')} 已创建`, 'success');
} else {
alert('创建失败: ' + (result.error || '未知错误'));
}
} catch (e) {
alert('创建失败: ' + e.message);
} finally {
if (btn) { btn.disabled = false; btn.textContent = '提交建议'; }
}
}
async function suggestionsQuickProcess(id, event) {
if (event) event.stopPropagation();
await suggestionsUpdateStatus(id, 'processed');
}
async function suggestionsUpdateStatus(id, newStatus) {
try {
const r = await fetch('/api/suggestions/update', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ id, status: newStatus })
});
const result = await r.json();
if (result.success) {
const suggestion = suggestionsList.find(s => s.id === id);
if (suggestion) {
suggestion.status = newStatus;
suggestion.updatedAt = Date.now();
}
suggestionsRenderList();
suggestionsShowToast('状态已更新', 'success');
} else {
alert('更新失败: ' + (result.error || '未知错误'));
}
} catch (e) {
alert('更新失败: ' + e.message);
}
}
async function suggestionsDelete(id) {
if (!confirm(`确定要删除建议 #${String(id).padStart(3, '0')} 吗?`)) return;
try {
const r = await fetch('/api/suggestions/delete', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ id })
});
const result = await r.json();
if (result.success) {
document.querySelector('.bug-detail-overlay')?.remove();
suggestionsLoaded = false;
await suggestionsLoadAll();
suggestionsShowToast('建议已删除', 'success');
} else {
alert('删除失败: ' + (result.error || '未知错误'));
}
} catch (e) {
alert('删除失败: ' + e.message);
}
}
function suggestionsShowToast(msg, type) {
const toast = document.getElementById('toast');
if (!toast) return;
toast.textContent = msg;
toast.className = 'toast ' + (type || 'success');
toast.style.display = 'block';
setTimeout(() => { toast.style.display = 'none'; }, 3000);
}
function suggestionsBindFilters() {
const ids = ['suggestions-search', 'suggestions-status-filter', 'suggestions-module-filter'];
ids.forEach(id => {
const el = document.getElementById(id);
if (el) el.addEventListener(el.tagName === 'SELECT' ? 'change' : 'input', () => suggestionsRenderList());
});
const moduleFilter = document.getElementById('suggestions-module-filter');
if (moduleFilter && moduleFilter.options.length <= 1) {
SUGGESTION_MODULES.forEach(m => {
const opt = document.createElement('option');
opt.value = m;
opt.textContent = m;
moduleFilter.appendChild(opt);
});
}
}
(function () {
const observer = new MutationObserver(() => {
const panel = document.getElementById('panel-suggestions');
if (panel && panel.classList.contains('active') && !suggestionsLoaded) {
suggestionsLoadAll();
suggestionsBindFilters();
}
});
const panel = document.getElementById('panel-suggestions');
if (panel) {
observer.observe(panel, { attributes: true, attributeFilter: ['class'] });
}
})();

View File

@ -5,6 +5,7 @@ TH1 Dashboard Server with data refresh API.
Endpoints:
GET /* - Static file serving
GET /api/bugs/list - Load all bugs from DOC/bugs.json
GET /api/suggestions/list - Load all suggestions from DOC/suggestions.json
GET /api/marketing/events - Load marketing events from DOC/marketing/events.json
GET /api/marketing/notes - Load marketing notes from DOC/marketing/notes.json
GET /api/marketing/todos - Load marketing todos from DOC/marketing/todos.json
@ -19,6 +20,9 @@ Endpoints:
POST /api/bugs/create - Create new bug
POST /api/bugs/update - Update bug fields
POST /api/bugs/delete - Delete bug by id
POST /api/suggestions/create - Create new suggestion
POST /api/suggestions/update - Update suggestion fields
POST /api/suggestions/delete - Delete suggestion by id
POST /api/devplan/update - Update task fields (description, etc.)
POST /api/devplan/subtask/create - Create subtask under a task
POST /api/devplan/subtask/toggle - Toggle subtask done state
@ -40,6 +44,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
@ -54,10 +59,72 @@ DEVPLAN_FILE = os.path.normpath(os.path.join(SCRIPT_DIR, '..', '..', 'DOC', 'dev
# Project-level data lives in DOC/ (version controlled)
BUGS_FILE = os.path.join(SCRIPT_DIR, '..', '..', 'DOC', 'bugs.json')
SUGGESTIONS_FILE = os.path.join(SCRIPT_DIR, '..', '..', 'DOC', 'suggestions.json')
MKT_TODOS_FILE = os.path.join(SCRIPT_DIR, '..', '..', 'DOC', 'marketing', 'todos.json')
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():
@ -77,6 +144,23 @@ def _save_bugs(data):
json.dump(data, f, ensure_ascii=False, indent=2)
def _load_suggestions():
"""Load suggestions.json from DOC/, return dict with nextId and suggestions list."""
path = os.path.normpath(SUGGESTIONS_FILE)
if os.path.exists(path):
with open(path, 'r', encoding='utf-8') as f:
return json.load(f)
return {'nextId': 1, 'suggestions': []}
def _save_suggestions(data):
"""Save suggestions.json to DOC/."""
path = os.path.normpath(SUGGESTIONS_FILE)
os.makedirs(os.path.dirname(path), exist_ok=True)
with open(path, 'w', encoding='utf-8') as f:
json.dump(data, f, ensure_ascii=False, indent=2)
def _load_devplan():
"""Load devplan.json from DOC/."""
if os.path.exists(DEVPLAN_FILE):
@ -206,10 +290,35 @@ class DashboardHandler(http.server.SimpleHTTPRequestHandler):
'.css': 'text/css',
}
def end_headers(self):
if self.path.endswith(('.html', '.js', '.css')) or self.path == '/' or self.path.startswith('/api/design/'):
self.send_header('Cache-Control', 'no-store, no-cache, must-revalidate, max-age=0')
self.send_header('Pragma', 'no-cache')
self.send_header('Expires', '0')
super().end_headers()
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
if self.path.startswith('/api/suggestions/list'):
self._send_json(_load_suggestions())
return
if self.path.startswith('/api/marketing/todos'):
self._send_json(_load_mkt_todos())
return
@ -252,6 +361,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()
@ -269,6 +463,12 @@ class DashboardHandler(http.server.SimpleHTTPRequestHandler):
self.handle_bugs_update()
elif self.path == '/api/bugs/delete':
self.handle_bugs_delete()
elif self.path == '/api/suggestions/create':
self.handle_suggestions_create()
elif self.path == '/api/suggestions/update':
self.handle_suggestions_update()
elif self.path == '/api/suggestions/delete':
self.handle_suggestions_delete()
elif self.path == '/api/marketing/todos/create':
self.handle_mkt_todo_create()
elif self.path == '/api/marketing/todos/toggle':
@ -803,6 +1003,94 @@ class DashboardHandler(http.server.SimpleHTTPRequestHandler):
except Exception as e:
self._send_json({'success': False, 'error': str(e)}, 500)
def handle_suggestions_create(self):
"""Create a new suggestion record."""
try:
length = int(self.headers.get('Content-Length', 0))
raw = self.rfile.read(length)
data = json.loads(raw.decode('utf-8'))
title = data.get('title', '').strip()
if not title:
self._send_json({'success': False, 'error': 'Title is required'}, 400)
return
suggestions_data = _load_suggestions()
suggestion_id = suggestions_data['nextId']
now = int(time.time() * 1000)
suggestion = {
'id': suggestion_id,
'title': title,
'description': data.get('description', '').strip(),
'status': 'open',
'module': data.get('module', ''),
'createdAt': now,
'updatedAt': now,
}
suggestions_data['suggestions'].append(suggestion)
suggestions_data['nextId'] = suggestion_id + 1
_save_suggestions(suggestions_data)
self._send_json({'success': True, 'id': suggestion_id})
except Exception as e:
self._send_json({'success': False, 'error': str(e)}, 500)
def handle_suggestions_update(self):
"""Update a suggestion's fields."""
try:
length = int(self.headers.get('Content-Length', 0))
raw = self.rfile.read(length)
data = json.loads(raw.decode('utf-8'))
suggestion_id = data.get('id')
if suggestion_id is None:
self._send_json({'success': False, 'error': 'ID is required'}, 400)
return
suggestions_data = _load_suggestions()
suggestion = next((s for s in suggestions_data['suggestions'] if s['id'] == suggestion_id), None)
if not suggestion:
self._send_json({'success': False, 'error': 'Suggestion not found'}, 404)
return
for field in ('status', 'title', 'description', 'module'):
if field in data:
suggestion[field] = data[field]
suggestion['updatedAt'] = int(time.time() * 1000)
_save_suggestions(suggestions_data)
self._send_json({'success': True})
except Exception as e:
self._send_json({'success': False, 'error': str(e)}, 500)
def handle_suggestions_delete(self):
"""Delete a suggestion by id."""
try:
length = int(self.headers.get('Content-Length', 0))
raw = self.rfile.read(length)
data = json.loads(raw.decode('utf-8'))
suggestion_id = data.get('id')
if suggestion_id is None:
self._send_json({'success': False, 'error': 'ID is required'}, 400)
return
suggestions_data = _load_suggestions()
original_len = len(suggestions_data['suggestions'])
suggestions_data['suggestions'] = [s for s in suggestions_data['suggestions'] if s['id'] != suggestion_id]
if len(suggestions_data['suggestions']) == original_len:
self._send_json({'success': False, 'error': 'Suggestion not found'}, 404)
return
_save_suggestions(suggestions_data)
self._send_json({'success': True})
except Exception as e:
self._send_json({'success': False, 'error': str(e)}, 500)
def handle_mkt_todo_create(self):
"""Create a new marketing TODO item."""
try:
@ -1669,7 +1957,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

View File

@ -0,0 +1,174 @@
param(
[switch]$DryRun
)
$ErrorActionPreference = "Continue"
$script:RepoRoot = [System.IO.Path]::GetFullPath((Join-Path $PSScriptRoot ".."))
$script:LogPath = Join-Path $script:RepoRoot ".git/graphify-hook.log"
function Write-HookLog {
param([string]$Message)
$timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
$line = "[$timestamp] $Message"
Write-Host $line
try {
Add-Content -Path $script:LogPath -Value $line -Encoding UTF8
}
catch {
Write-Host "[graphify hook] Unable to write log: $($_.Exception.Message)"
}
}
function Test-GraphifyPython {
param([string]$PythonPath)
if ([string]::IsNullOrWhiteSpace($PythonPath)) {
return $false
}
try {
& $PythonPath -c "import graphify" *> $null
return ($LASTEXITCODE -eq 0)
}
catch {
return $false
}
}
function Get-GraphifyPython {
$candidates = @()
foreach ($relative in @("Unity/graphify-out/.graphify_python", "graphify-out/.graphify_python")) {
$path = Join-Path $script:RepoRoot $relative
if (Test-Path $path) {
$value = (Get-Content -Raw -Path $path -Encoding UTF8).Trim()
if ($value) {
$candidates += $value
}
}
}
foreach ($command in @("python", "py")) {
$cmd = Get-Command $command -ErrorAction SilentlyContinue
if ($cmd) {
$candidates += $cmd.Source
}
}
foreach ($candidate in $candidates | Select-Object -Unique) {
if (Test-GraphifyPython $candidate) {
return $candidate
}
}
return $null
}
function Get-ChangedFiles {
$fromEnv = $env:GRAPHIFY_CHANGED
if (-not [string]::IsNullOrWhiteSpace($fromEnv)) {
return $fromEnv -split "`r?`n" | Where-Object { -not [string]::IsNullOrWhiteSpace($_) }
}
$git = Get-Command git -ErrorAction SilentlyContinue
if (-not $git) {
return @()
}
Push-Location $script:RepoRoot
try {
$files = & git diff --name-only HEAD~1 HEAD 2>$null
if (-not $files) {
$files = & git diff --name-only HEAD 2>$null
}
return @($files | Where-Object { -not [string]::IsNullOrWhiteSpace($_) })
}
finally {
Pop-Location
}
}
function Invoke-GraphifyRebuild {
param(
[string]$Label,
[string]$RelativePath,
[string]$PythonPath
)
$target = Join-Path $script:RepoRoot $RelativePath
$graphPath = Join-Path $target "graphify-out/graph.json"
if (-not (Test-Path $graphPath)) {
Write-HookLog "Skipping ${Label}: no graphify-out/graph.json found."
return
}
if ($DryRun) {
Write-HookLog "Dry run: would rebuild $Label graph at $target."
return
}
Write-HookLog "Rebuilding $Label graph at $target."
Push-Location $target
try {
& $PythonPath -c "from pathlib import Path; from graphify.watch import _rebuild_code; _rebuild_code(Path('.'))" *> $null
if ($LASTEXITCODE -eq 0) {
Write-HookLog "Rebuilt $Label graph."
}
else {
Write-HookLog "Rebuild failed for $Label with exit code $LASTEXITCODE."
}
}
catch {
Write-HookLog "Rebuild failed for ${Label}: $($_.Exception.Message)"
}
finally {
Pop-Location
}
}
Set-Location $script:RepoRoot
$changedFiles = @(Get-ChangedFiles)
if ($changedFiles.Count -eq 0) {
Write-HookLog "No changed files detected; nothing to rebuild."
exit 0
}
$codeExtensions = @(
".cs", ".asmdef", ".shader", ".hlsl", ".cginc",
".py", ".js", ".ts", ".json", ".toml", ".yaml", ".yml"
)
$changedCode = @(
$changedFiles | Where-Object {
$ext = [System.IO.Path]::GetExtension($_).ToLowerInvariant()
$codeExtensions -contains $ext
}
)
if ($changedCode.Count -eq 0) {
Write-HookLog "$($changedFiles.Count) changed file(s), but no code/config files for AST graph rebuild."
exit 0
}
$python = Get-GraphifyPython
if (-not $python) {
Write-HookLog "No Python interpreter with graphify installed was found."
exit 0
}
Write-HookLog "$($changedCode.Count) code/config file(s) changed; using Python: $python"
$touchesUnity = @($changedCode | Where-Object { $_ -like "Unity/*" -or $_ -like "Unity\*" }).Count -gt 0
Invoke-GraphifyRebuild -Label "root" -RelativePath "." -PythonPath $python
if ($touchesUnity) {
Invoke-GraphifyRebuild -Label "Unity" -RelativePath "Unity" -PythonPath $python
}
else {
Write-HookLog "Skipping Unity graph: no Unity path changed."
}

View File

@ -0,0 +1,39 @@
$ErrorActionPreference = "Stop"
$repoRoot = [System.IO.Path]::GetFullPath((Join-Path $PSScriptRoot ".."))
$gitDir = Join-Path $repoRoot ".git"
$hooksDir = Join-Path $gitDir "hooks"
$hookPath = Join-Path $hooksDir "post-commit"
if (-not (Test-Path $gitDir)) {
throw "No .git directory found at $repoRoot"
}
if (-not (Test-Path $hooksDir)) {
New-Item -ItemType Directory -Path $hooksDir | Out-Null
}
$hook = @'
#!/bin/sh
# Runs the repository-owned graphify post-commit updater.
REPO_ROOT=$(git rev-parse --show-toplevel 2>/dev/null) || exit 0
CHANGED=$(git diff --name-only HEAD~1 HEAD 2>/dev/null || git diff --name-only HEAD 2>/dev/null)
if [ -z "$CHANGED" ]; then
exit 0
fi
export GRAPHIFY_CHANGED="$CHANGED"
if command -v pwsh >/dev/null 2>&1; then
(cd "$REPO_ROOT" && pwsh -NoProfile -ExecutionPolicy Bypass -File "Tools/GraphifyPostCommit.ps1") >/dev/null 2>&1 &
elif command -v powershell.exe >/dev/null 2>&1; then
(cd "$REPO_ROOT" && powershell.exe -NoProfile -ExecutionPolicy Bypass -File "Tools/GraphifyPostCommit.ps1") >/dev/null 2>&1 &
fi
exit 0
'@
[System.IO.File]::WriteAllText($hookPath, $hook, [System.Text.UTF8Encoding]::new($false))
Write-Host "Installed graphify post-commit hook: $hookPath"

Binary file not shown.

File diff suppressed because one or more lines are too long

13
Tools/README.md Normal file
View File

@ -0,0 +1,13 @@
# Tools
`Tools` stores executable utilities, local apps, import/export scripts, diagnostics, and automation helpers.
Keep source documents and planning notes out of this directory unless they are direct fixtures for a tool.
Current notable entries:
- `Dashboard/`: local project dashboard web app. It stays here because it has a server, frontend code, generated data, and launch scripts.
- `OSS/`: online data and upload/debug utilities.
- `multilingual_check/`: localization checking and translation helpers.
Design documents belong in `Design/`. Dashboard-backed operational data belongs in `DOC/`.

View File

@ -32,6 +32,11 @@ MonoBehaviour:
OutsideMultiplayIncludeAIPlayerHint: "(\u5305\u542BAI\u73A9\u5BB6)"
OutsideMultiplayNoRoomHint: "\u6682\u65E0\u8054\u673A\u623F\u95F4"
OutsideMultiplayRoomNameSuffix: "\u7684\u623F\u95F4"
OutsideMultiplayTeamTitle: "\u961F\u4F0D"
OutsideMultiplayAIPlayerTitle: "AI\u73A9\u5BB6"
OutsideMultiplayOpenTitle: "\u5F85\u52A0\u5165"
OutsideMultiplayReadyTitle: "\u5DF2\u51C6\u5907"
OutsideMultiplayOpenHint: "\u4ECD\u6709\u5F85\u52A0\u5165\u7684\u5E2D\u4F4D\uFF0C\u8BF7\u51CF\u5C11\u5E2D\u4F4D"
OutsideHistoryDropListNoLimitP: "\u4E0D\u9650"
OutsideHistoryDropList2P: "2\u4EBA"
OutsideHistoryDropList3P: "3\u4EBA"

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,136 @@
%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!114 &11400000
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 0}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 3ebbbbba88fe43409c16e83b934b878c, type: 3}
m_Name: NetworkPlayerTipDataAssets
m_EditorClassIdentifier:
DefaultCooldownSeconds: 3
NetworkPlayerTipInfoList:
- TipType: 1
Title: 20000
Message: 20001
CooldownSeconds: 5
- TipType: 2
Title: 20002
Message: 20003
CooldownSeconds: 5
- TipType: 3
Title: 20004
Message: 20005
CooldownSeconds: 10
- TipType: 4
Title: 20006
Message: 20007
CooldownSeconds: 5
- TipType: 5
Title: 20008
Message: 20009
CooldownSeconds: 3
- TipType: 6
Title: 20010
Message: 20011
CooldownSeconds: 3
- TipType: 7
Title: 20012
Message: 20013
CooldownSeconds: 2
- TipType: 8
Title: 20014
Message: 20015
CooldownSeconds: 2
- TipType: 9
Title: 20016
Message: 20017
CooldownSeconds: 3
- TipType: 10
Title: 20018
Message: 20019
CooldownSeconds: 3
- TipType: 11
Title: 20020
Message: 20021
CooldownSeconds: 5
- TipType: 12
Title: 20022
Message: 20023
CooldownSeconds: 3
- TipType: 13
Title: 20024
Message: 20025
CooldownSeconds: 5
- TipType: 14
Title: 20026
Message: 20027
CooldownSeconds: 3
- TipType: 15
Title: 20028
Message: 20029
CooldownSeconds: 3
- TipType: 16
Title: 20030
Message: 20031
CooldownSeconds: 5
- TipType: 17
Title: 20032
Message: 20033
CooldownSeconds: 3
- TipType: 18
Title: 20034
Message: 20035
CooldownSeconds: 5
- TipType: 19
Title: 20036
Message: 20037
CooldownSeconds: 5
- TipType: 20
Title: 20038
Message: 20039
CooldownSeconds: 5
- TipType: 21
Title: 20040
Message: 20041
CooldownSeconds: 3
- TipType: 22
Title: 20042
Message: 20043
CooldownSeconds: 5
- TipType: 23
Title: 20044
Message: 20045
CooldownSeconds: 5
- TipType: 24
Title: 20046
Message: 20047
CooldownSeconds: 5
- TipType: 25
Title: 20048
Message: 20049
CooldownSeconds: 5
- TipType: 26
Title: 20050
Message: 20051
CooldownSeconds: 3
- TipType: 27
Title: 20052
Message: 20053
CooldownSeconds: 3
- TipType: 28
Title: 20054
Message: 20055
CooldownSeconds: 2
- TipType: 29
Title: 20056
Message: 20057
CooldownSeconds: 3
- TipType: 30
Title: 20058
Message: 20059
CooldownSeconds: 5

View File

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: e8a9f8a49839186478275813e5fe3dc1
NativeFormatImporter:
externalObjects: {}
mainObjectFileID: 11400000
userData:
assetBundleName:
assetBundleVariant:

View File

@ -32,6 +32,11 @@ MonoBehaviour:
OutsideMultiplayIncludeAIPlayerHint: 2405
OutsideMultiplayNoRoomHint: 17453
OutsideMultiplayRoomNameSuffix: 17705
OutsideMultiplayTeamTitle: 20066
OutsideMultiplayAIPlayerTitle: 20067
OutsideMultiplayOpenTitle: 20068
OutsideMultiplayReadyTitle: 20069
OutsideMultiplayOpenHint: 20077
OutsideHistoryDropListNoLimitP: 17352
OutsideHistoryDropList2P: 17353
OutsideHistoryDropList3P: 17354

View File

@ -4312,7 +4312,7 @@ PrefabInstance:
objectReference: {fileID: 0}
- target: {fileID: 7849747489070299319, guid: 19402153ee79dbc4e80a78f3222e6b47, type: 3}
propertyPath: m_SizeDelta.x
value: 0
value: 830.08167
objectReference: {fileID: 0}
- target: {fileID: 7849747489070299319, guid: 19402153ee79dbc4e80a78f3222e6b47, type: 3}
propertyPath: m_AnchoredPosition.x
@ -4497,7 +4497,7 @@ PrefabInstance:
objectReference: {fileID: 0}
- target: {fileID: 7849747489070299319, guid: ed55b8a014ca41543a2d95f370c5750d, type: 3}
propertyPath: m_SizeDelta.x
value: 0
value: 556.2688
objectReference: {fileID: 0}
- target: {fileID: 7849747489070299319, guid: ed55b8a014ca41543a2d95f370c5750d, type: 3}
propertyPath: m_AnchoredPosition.x

View File

@ -1977,7 +1977,7 @@ RectTransform:
m_AnchorMin: {x: 0, y: 1}
m_AnchorMax: {x: 0, y: 1}
m_AnchoredPosition: {x: 475.04083, y: -253.69449}
m_SizeDelta: {x: 0, y: 137.3574}
m_SizeDelta: {x: 830.08167, y: 137.3574}
m_Pivot: {x: 0.5, y: 0.5}
--- !u!114 &1640998311177812195
MonoBehaviour:

View File

@ -197,7 +197,6 @@ RectTransform:
m_ConstrainProportionsScale: 0
m_Children:
- {fileID: 4797701493746855371}
- {fileID: 1430550738608142214}
m_Father: {fileID: 0}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0, y: 0}
@ -295,7 +294,7 @@ MonoBehaviour:
NetRowContainer: {fileID: 3252362370951117910}
NetInfo: {fileID: 7448979660496629192}
ScrollViewRect: {fileID: 7039295970119204888}
ChatArea: {fileID: 6662476954447050023}
ChatAreaRoot: {fileID: 7448979660496629192}
--- !u!1 &3830065821644900243
GameObject:
m_ObjectHideFlags: 0
@ -779,233 +778,3 @@ MonoBehaviour:
m_Name:
m_EditorClassIdentifier:
m_ShowMaskGraphic: 0
--- !u!1001 &3208565566366438738
PrefabInstance:
m_ObjectHideFlags: 0
serializedVersion: 2
m_Modification:
serializedVersion: 3
m_TransformParent: {fileID: 7448979660496629192}
m_Modifications:
- target: {fileID: 786537916787247843, guid: a3219b8bdc414264e87cb080245b9a45, type: 3}
propertyPath: m_Colors.m_DisabledColor.a
value: 0.78431374
objectReference: {fileID: 0}
- target: {fileID: 1145808588851263750, guid: a3219b8bdc414264e87cb080245b9a45, type: 3}
propertyPath: m_Name
value: ChatAreaPanel
objectReference: {fileID: 0}
- target: {fileID: 1554949039177054909, guid: a3219b8bdc414264e87cb080245b9a45, type: 3}
propertyPath: m_Colors.m_DisabledColor.a
value: 0.78431374
objectReference: {fileID: 0}
- target: {fileID: 2045491555638099912, guid: a3219b8bdc414264e87cb080245b9a45, type: 3}
propertyPath: m_AnchorMax.y
value: 1
objectReference: {fileID: 0}
- target: {fileID: 2045491555638099912, guid: a3219b8bdc414264e87cb080245b9a45, type: 3}
propertyPath: m_AnchorMin.y
value: 1
objectReference: {fileID: 0}
- target: {fileID: 2045491555638099912, guid: a3219b8bdc414264e87cb080245b9a45, type: 3}
propertyPath: m_SizeDelta.y
value: 0
objectReference: {fileID: 0}
- target: {fileID: 2045491555638099912, guid: a3219b8bdc414264e87cb080245b9a45, type: 3}
propertyPath: m_AnchoredPosition.x
value: 261.94098
objectReference: {fileID: 0}
- target: {fileID: 2045491555638099912, guid: a3219b8bdc414264e87cb080245b9a45, type: 3}
propertyPath: m_AnchoredPosition.y
value: -20.005001
objectReference: {fileID: 0}
- target: {fileID: 4565888280615936724, guid: a3219b8bdc414264e87cb080245b9a45, type: 3}
propertyPath: m_Pivot.x
value: 0.5
objectReference: {fileID: 0}
- target: {fileID: 4565888280615936724, guid: a3219b8bdc414264e87cb080245b9a45, type: 3}
propertyPath: m_Pivot.y
value: 0.5
objectReference: {fileID: 0}
- target: {fileID: 4565888280615936724, guid: a3219b8bdc414264e87cb080245b9a45, type: 3}
propertyPath: m_AnchorMax.x
value: 0.5
objectReference: {fileID: 0}
- target: {fileID: 4565888280615936724, guid: a3219b8bdc414264e87cb080245b9a45, type: 3}
propertyPath: m_AnchorMax.y
value: 0.5
objectReference: {fileID: 0}
- target: {fileID: 4565888280615936724, guid: a3219b8bdc414264e87cb080245b9a45, type: 3}
propertyPath: m_AnchorMin.x
value: 0.5
objectReference: {fileID: 0}
- target: {fileID: 4565888280615936724, guid: a3219b8bdc414264e87cb080245b9a45, type: 3}
propertyPath: m_AnchorMin.y
value: 0.5
objectReference: {fileID: 0}
- target: {fileID: 4565888280615936724, guid: a3219b8bdc414264e87cb080245b9a45, type: 3}
propertyPath: m_SizeDelta.x
value: 913.04
objectReference: {fileID: 0}
- target: {fileID: 4565888280615936724, guid: a3219b8bdc414264e87cb080245b9a45, type: 3}
propertyPath: m_SizeDelta.y
value: 162.68
objectReference: {fileID: 0}
- target: {fileID: 4565888280615936724, guid: a3219b8bdc414264e87cb080245b9a45, type: 3}
propertyPath: m_LocalPosition.x
value: 0
objectReference: {fileID: 0}
- target: {fileID: 4565888280615936724, guid: a3219b8bdc414264e87cb080245b9a45, type: 3}
propertyPath: m_LocalPosition.y
value: 0
objectReference: {fileID: 0}
- target: {fileID: 4565888280615936724, guid: a3219b8bdc414264e87cb080245b9a45, type: 3}
propertyPath: m_LocalPosition.z
value: 0
objectReference: {fileID: 0}
- target: {fileID: 4565888280615936724, guid: a3219b8bdc414264e87cb080245b9a45, type: 3}
propertyPath: m_LocalRotation.w
value: 1
objectReference: {fileID: 0}
- target: {fileID: 4565888280615936724, guid: a3219b8bdc414264e87cb080245b9a45, type: 3}
propertyPath: m_LocalRotation.x
value: 0
objectReference: {fileID: 0}
- target: {fileID: 4565888280615936724, guid: a3219b8bdc414264e87cb080245b9a45, type: 3}
propertyPath: m_LocalRotation.y
value: 0
objectReference: {fileID: 0}
- target: {fileID: 4565888280615936724, guid: a3219b8bdc414264e87cb080245b9a45, type: 3}
propertyPath: m_LocalRotation.z
value: 0
objectReference: {fileID: 0}
- target: {fileID: 4565888280615936724, guid: a3219b8bdc414264e87cb080245b9a45, type: 3}
propertyPath: m_AnchoredPosition.x
value: 0
objectReference: {fileID: 0}
- target: {fileID: 4565888280615936724, guid: a3219b8bdc414264e87cb080245b9a45, type: 3}
propertyPath: m_AnchoredPosition.y
value: -408.66
objectReference: {fileID: 0}
- target: {fileID: 4565888280615936724, guid: a3219b8bdc414264e87cb080245b9a45, type: 3}
propertyPath: m_LocalEulerAnglesHint.x
value: 0
objectReference: {fileID: 0}
- target: {fileID: 4565888280615936724, guid: a3219b8bdc414264e87cb080245b9a45, type: 3}
propertyPath: m_LocalEulerAnglesHint.y
value: 0
objectReference: {fileID: 0}
- target: {fileID: 4565888280615936724, guid: a3219b8bdc414264e87cb080245b9a45, type: 3}
propertyPath: m_LocalEulerAnglesHint.z
value: 0
objectReference: {fileID: 0}
- target: {fileID: 5312906796478825185, guid: a3219b8bdc414264e87cb080245b9a45, type: 3}
propertyPath: m_AnchorMax.x
value: 0
objectReference: {fileID: 0}
- target: {fileID: 5312906796478825185, guid: a3219b8bdc414264e87cb080245b9a45, type: 3}
propertyPath: m_AnchorMax.y
value: 0
objectReference: {fileID: 0}
- target: {fileID: 5312906796478825185, guid: a3219b8bdc414264e87cb080245b9a45, type: 3}
propertyPath: m_AnchorMin.y
value: 0
objectReference: {fileID: 0}
- target: {fileID: 5806223491937344649, guid: a3219b8bdc414264e87cb080245b9a45, type: 3}
propertyPath: m_SizeDelta.y
value: 0
objectReference: {fileID: 0}
- target: {fileID: 6734467027181266523, guid: a3219b8bdc414264e87cb080245b9a45, type: 3}
propertyPath: m_Color.a
value: 0.78431374
objectReference: {fileID: 0}
- target: {fileID: 7907971095448877940, guid: a3219b8bdc414264e87cb080245b9a45, type: 3}
propertyPath: m_Color.a
value: 0.78431374
objectReference: {fileID: 0}
m_RemovedComponents: []
m_RemovedGameObjects: []
m_AddedGameObjects: []
m_AddedComponents:
- targetCorrespondingSourceObject: {fileID: 5084579344674831282, guid: a3219b8bdc414264e87cb080245b9a45, type: 3}
insertIndex: -1
addedObject: {fileID: 3277486792873264684}
- targetCorrespondingSourceObject: {fileID: 8618571515869483388, guid: a3219b8bdc414264e87cb080245b9a45, type: 3}
insertIndex: -1
addedObject: {fileID: -1501497239500558402}
m_SourcePrefab: {fileID: 100100000, guid: a3219b8bdc414264e87cb080245b9a45, type: 3}
--- !u!224 &1430550738608142214 stripped
RectTransform:
m_CorrespondingSourceObject: {fileID: 4565888280615936724, guid: a3219b8bdc414264e87cb080245b9a45, type: 3}
m_PrefabInstance: {fileID: 3208565566366438738}
m_PrefabAsset: {fileID: 0}
--- !u!1 &6565206209314489390 stripped
GameObject:
m_CorrespondingSourceObject: {fileID: 8618571515869483388, guid: a3219b8bdc414264e87cb080245b9a45, type: 3}
m_PrefabInstance: {fileID: 3208565566366438738}
m_PrefabAsset: {fileID: 0}
--- !u!114 &-1501497239500558402
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 6565206209314489390}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 6b27f832d22e4a8d916272b644937774, type: 3}
m_Name:
m_EditorClassIdentifier:
Ban: 0
NoExport: 0
FontBan: 0
Preset: 0
ID: 18954
FontID: 1
TextCfg:
- Type: 1
ApplyFontSize: 0
FontSize: 20
ApplyCharacterSpacing: 0
CharacterSpacing: 0
ApplyWordSpacing: 0
WordSpacing: 0
ApplyLineSpacing: 0
LineSpacing: 0
ApplyParagraphSpacing: 0
ParagraphSpacing: 0
--- !u!114 &6662476954447050023 stripped
MonoBehaviour:
m_CorrespondingSourceObject: {fileID: -1084591119027101579, guid: a3219b8bdc414264e87cb080245b9a45, type: 3}
m_PrefabInstance: {fileID: 3208565566366438738}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 0}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 078a34fe11854024b88b19ee4dcacdac, type: 3}
m_Name:
m_EditorClassIdentifier:
--- !u!1 &7644597152579317472 stripped
GameObject:
m_CorrespondingSourceObject: {fileID: 5084579344674831282, guid: a3219b8bdc414264e87cb080245b9a45, type: 3}
m_PrefabInstance: {fileID: 3208565566366438738}
m_PrefabAsset: {fileID: 0}
--- !u!114 &3277486792873264684
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 7644597152579317472}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 6b27f832d22e4a8d916272b644937774, type: 3}
m_Name:
m_EditorClassIdentifier:
Ban: 0
NoExport: 0
FontBan: 0
Preset: 0
ID: 19739
FontID: 0
TextCfg: []

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: d77441245d1b39240867efb0c993f19c
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 87951fe549cc3324e9570ee8caabcd6d
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -137,7 +137,7 @@ MonoBehaviour:
DocText: {fileID: 3795164220530813026}
MaxChatCount: 2
MessageLifetime: 10
DocRecentCount: 10
DocRecentCount: 50
--- !u!1 &2477995237858003733
GameObject:
m_ObjectHideFlags: 0
@ -171,7 +171,7 @@ RectTransform:
m_Father: {fileID: 9183018199567352959}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0, y: 0}
m_AnchorMax: {x: 1, y: 1}
m_AnchorMax: {x: 0, y: 0}
m_AnchoredPosition: {x: -65.503784, y: -960}
m_SizeDelta: {x: 20, y: 20}
m_Pivot: {x: 0.5, y: 0.5}
@ -914,8 +914,8 @@ RectTransform:
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0, y: 1}
m_AnchorMax: {x: 0, y: 1}
m_AnchoredPosition: {x: 261.94098, y: -20.005001}
m_SizeDelta: {x: 503.882, y: 20.01}
m_AnchoredPosition: {x: 261.94098, y: -35.004997}
m_SizeDelta: {x: 503.882, y: 0}
m_Pivot: {x: 0.5, y: 0.5}
--- !u!222 &4208484589109369530
CanvasRenderer:
@ -945,7 +945,7 @@ MonoBehaviour:
m_OnCullStateChanged:
m_PersistentCalls:
m_Calls: []
m_text: asfasfasf
m_text: "\u5929\u706B\u4EBA\u96EA\u7CD5\uFF1A\u4E3A\u4EC0\u4E48\u963F\u71D0\u7684\u5C3E\u5DF4\u8FD9\u4E48\u8FD9\u4E48\u8FD9\u4E48\u8FD9\u4E48\u8FD9\u4E48\u8FD9\u4E48\u8FD9\u4E48\u8FD9\u4E48\u8FD9\u4E48\u8FD9\u4E48\u8FD9\u4E48\u8FD9\u4E48\u8FD9\u4E48\u8FD9\u4E48\u957F\uFF01\uFF01"
m_isRightToLeft: 0
m_fontAsset: {fileID: 11400000, guid: 8e119f168f1a6b745be02ef19f51610f, type: 2}
m_sharedMaterial: {fileID: -8081454072124122709, guid: 8e119f168f1a6b745be02ef19f51610f, type: 2}
@ -988,7 +988,7 @@ MonoBehaviour:
m_lineSpacingMax: 0
m_paragraphSpacing: 0
m_charWidthMaxAdj: 0
m_enableWordWrapping: 0
m_enableWordWrapping: 1
m_wordWrappingRatios: 0.4
m_overflowMode: 0
m_linkedTextComponent: {fileID: 0}
@ -1044,7 +1044,7 @@ MonoBehaviour:
NoExport: 0
FontBan: 0
Preset: 0
ID: 19739
ID: 20076
FontID: 0
TextCfg: []
--- !u!1 &5277186255209122562
@ -1478,9 +1478,9 @@ RectTransform:
m_Father: {fileID: 9123125548031705264}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0, y: 0}
m_AnchorMax: {x: 1, y: 1}
m_AnchorMax: {x: 0, y: 0}
m_AnchoredPosition: {x: 0, y: 0}
m_SizeDelta: {x: -10, y: 0}
m_SizeDelta: {x: 0, y: 0}
m_Pivot: {x: 0, y: 1}
--- !u!222 &4276625840450642673
CanvasRenderer:
@ -1683,7 +1683,7 @@ GameObject:
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 0
m_IsActive: 1
--- !u!224 &86432792637060186
RectTransform:
m_ObjectHideFlags: 0
@ -2420,7 +2420,7 @@ RectTransform:
m_AnchorMin: {x: 0, y: 1}
m_AnchorMax: {x: 1, y: 1}
m_AnchoredPosition: {x: 0.000002861, y: 0.000028818846}
m_SizeDelta: {x: 0.00010681, y: 30.01}
m_SizeDelta: {x: 0.00010681, y: 0}
m_Pivot: {x: 0, y: 1}
--- !u!114 &926841285576668722
MonoBehaviour:

View File

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: d7bc59594a797af4e901bc036291c03e
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: eca335a4eca8c2e499c58e552a7b0f9d
PrefabImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: a5218c398c8b8364891b636ee8199fca
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 017702e379c4b504f909f601f647475f
PrefabImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -3404,7 +3404,7 @@ RectTransform:
m_AnchorMin: {x: 0, y: 1}
m_AnchorMax: {x: 0, y: 1}
m_AnchoredPosition: {x: 147, y: -23}
m_SizeDelta: {x: 120.01, y: 32.0265}
m_SizeDelta: {x: 0, y: 32.0265}
m_Pivot: {x: 0.5, y: 0.5}
--- !u!222 &5182836495056545005
CanvasRenderer:
@ -4348,7 +4348,7 @@ RectTransform:
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0, y: 1}
m_AnchorMax: {x: 0, y: 1}
m_AnchoredPosition: {x: 250, y: -550.093}
m_AnchoredPosition: {x: 250, y: -480.093}
m_SizeDelta: {x: 500, y: 0}
m_Pivot: {x: 0.5, y: 0.5}
--- !u!114 &4676610239985244751
@ -5247,7 +5247,7 @@ RectTransform:
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0, y: 1}
m_AnchorMax: {x: 0, y: 1}
m_AnchoredPosition: {x: 250, y: -785.093}
m_AnchoredPosition: {x: 250, y: -550.093}
m_SizeDelta: {x: 500, y: 0}
m_Pivot: {x: 0.5, y: 0.5}
--- !u!114 &1555931548957767936

View File

@ -794,6 +794,242 @@ MonoBehaviour:
clickSound: {fileID: 0}
pressScale: 0.8
scaleDuration: 0.1
--- !u!1 &2140407895753325769
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- component: {fileID: 7572296490546585051}
- component: {fileID: 6457064681279636580}
- component: {fileID: 7747318549009700087}
- component: {fileID: 5912048873162754301}
- component: {fileID: 5535755370362207064}
- component: {fileID: 6243691724474789413}
m_Layer: 5
m_Name: TransReport
m_TagString: Untagged
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
--- !u!224 &7572296490546585051
RectTransform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 2140407895753325769}
m_LocalRotation: {x: -0, y: -0, z: -0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children:
- {fileID: 5814070196398329509}
m_Father: {fileID: 3770347386733582695}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0.5, y: 0.5}
m_AnchorMax: {x: 0.5, y: 0.5}
m_AnchoredPosition: {x: 397.9826, y: -34}
m_SizeDelta: {x: 338.1129, y: 53.9677}
m_Pivot: {x: 0.5, y: 0.5}
--- !u!222 &6457064681279636580
CanvasRenderer:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 2140407895753325769}
m_CullTransparentMesh: 1
--- !u!114 &7747318549009700087
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 2140407895753325769}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3}
m_Name:
m_EditorClassIdentifier:
m_Material: {fileID: 0}
m_Color: {r: 1, g: 1, b: 1, a: 1}
m_RaycastTarget: 1
m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0}
m_Maskable: 1
m_OnCullStateChanged:
m_PersistentCalls:
m_Calls: []
m_Sprite: {fileID: 21300000, guid: df39380aaef39404eaf3e0e9183c785c, type: 3}
m_Type: 1
m_PreserveAspect: 0
m_FillCenter: 1
m_FillMethod: 4
m_FillAmount: 1
m_FillClockwise: 1
m_FillOrigin: 0
m_UseSpriteMesh: 0
m_PixelsPerUnitMultiplier: 1.5
--- !u!114 &5912048873162754301
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 2140407895753325769}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 4e29b1a8efbd4b44bb3f3716e73f07ff, type: 3}
m_Name:
m_EditorClassIdentifier:
m_Navigation:
m_Mode: 3
m_WrapAround: 0
m_SelectOnUp: {fileID: 0}
m_SelectOnDown: {fileID: 0}
m_SelectOnLeft: {fileID: 0}
m_SelectOnRight: {fileID: 0}
m_Transition: 0
m_Colors:
m_NormalColor: {r: 1, g: 1, b: 1, a: 1}
m_HighlightedColor: {r: 0.9607843, g: 0.9607843, b: 0.9607843, a: 1}
m_PressedColor: {r: 0.78431374, g: 0.78431374, b: 0.78431374, a: 1}
m_SelectedColor: {r: 0.9607843, g: 0.9607843, b: 0.9607843, a: 1}
m_DisabledColor: {r: 0.78431374, g: 0.78431374, b: 0.78431374, a: 0.5019608}
m_ColorMultiplier: 1
m_FadeDuration: 0.1
m_SpriteState:
m_HighlightedSprite: {fileID: 0}
m_PressedSprite: {fileID: 0}
m_SelectedSprite: {fileID: 0}
m_DisabledSprite: {fileID: 0}
m_AnimationTriggers:
m_NormalTrigger: Normal
m_HighlightedTrigger: Highlighted
m_PressedTrigger: Pressed
m_SelectedTrigger: Selected
m_DisabledTrigger: Disabled
m_Interactable: 1
m_TargetGraphic: {fileID: 7747318549009700087}
m_OnClick:
m_PersistentCalls:
m_Calls: []
--- !u!82 &5535755370362207064
AudioSource:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 2140407895753325769}
m_Enabled: 1
serializedVersion: 4
OutputAudioMixerGroup: {fileID: 0}
m_audioClip: {fileID: 0}
m_PlayOnAwake: 1
m_Volume: 1
m_Pitch: 1
Loop: 0
Mute: 0
Spatialize: 0
SpatializePostEffects: 0
Priority: 128
DopplerLevel: 1
MinDistance: 1
MaxDistance: 500
Pan2D: 0
rolloffMode: 0
BypassEffects: 0
BypassListenerEffects: 0
BypassReverbZones: 0
rolloffCustomCurve:
serializedVersion: 2
m_Curve:
- serializedVersion: 3
time: 0
value: 1
inSlope: 0
outSlope: 0
tangentMode: 0
weightedMode: 0
inWeight: 0.33333334
outWeight: 0.33333334
- serializedVersion: 3
time: 1
value: 0
inSlope: 0
outSlope: 0
tangentMode: 0
weightedMode: 0
inWeight: 0.33333334
outWeight: 0.33333334
m_PreInfinity: 2
m_PostInfinity: 2
m_RotationOrder: 4
panLevelCustomCurve:
serializedVersion: 2
m_Curve:
- serializedVersion: 3
time: 0
value: 0
inSlope: 0
outSlope: 0
tangentMode: 0
weightedMode: 0
inWeight: 0.33333334
outWeight: 0.33333334
m_PreInfinity: 2
m_PostInfinity: 2
m_RotationOrder: 4
spreadCustomCurve:
serializedVersion: 2
m_Curve:
- serializedVersion: 3
time: 0
value: 0
inSlope: 0
outSlope: 0
tangentMode: 0
weightedMode: 0
inWeight: 0.33333334
outWeight: 0.33333334
m_PreInfinity: 2
m_PostInfinity: 2
m_RotationOrder: 4
reverbZoneMixCustomCurve:
serializedVersion: 2
m_Curve:
- serializedVersion: 3
time: 0
value: 1
inSlope: 0
outSlope: 0
tangentMode: 0
weightedMode: 0
inWeight: 0.33333334
outWeight: 0.33333334
m_PreInfinity: 2
m_PostInfinity: 2
m_RotationOrder: 4
--- !u!114 &6243691724474789413
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 2140407895753325769}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 33d75335a9dad784a91baba5578371fb, type: 3}
m_Name:
m_EditorClassIdentifier:
hoverSound: {fileID: 0}
targetScale: 1.1
clickSound: {fileID: 0}
pressScale: 0.8
scaleDuration: 0.1
--- !u!1 &2717726101838707478
GameObject:
m_ObjectHideFlags: 0
@ -1398,6 +1634,7 @@ MonoBehaviour:
HistoryButton: {fileID: 7456826708375644621}
TutorButton: {fileID: 7185898387603956833}
StoryButton: {fileID: 3611518138806132449}
TransReportButton: {fileID: 5912048873162754301}
ButtonList:
- {fileID: 5519043339116172500}
- {fileID: 7179142844686828325}
@ -1587,7 +1824,7 @@ MonoBehaviour:
NoExport: 0
FontBan: 0
Preset: 0
ID: 1401
ID: 19977
FontID: 1
TextCfg:
- Type: 1
@ -3005,6 +3242,7 @@ RectTransform:
- {fileID: 2811878508391649866}
- {fileID: 4025582528238821115}
- {fileID: 8350919062383745882}
- {fileID: 7572296490546585051}
m_Father: {fileID: 4735088385716738539}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0.5, y: 0.5}
@ -3200,6 +3438,160 @@ MonoBehaviour:
m_FillOrigin: 0
m_UseSpriteMesh: 0
m_PixelsPerUnitMultiplier: 1
--- !u!1 &6691324969803418791
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- component: {fileID: 5814070196398329509}
- component: {fileID: 7963249260858927386}
- component: {fileID: 1157791559205469021}
- component: {fileID: -7562340078456195498}
m_Layer: 5
m_Name: Text
m_TagString: Untagged
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
--- !u!224 &5814070196398329509
RectTransform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 6691324969803418791}
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children: []
m_Father: {fileID: 7572296490546585051}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0.5, y: 0.5}
m_AnchorMax: {x: 0.5, y: 0.5}
m_AnchoredPosition: {x: -4.215, y: 0.000032425}
m_SizeDelta: {x: 308.4651, y: 53.968}
m_Pivot: {x: 0.5, y: 0.5}
--- !u!222 &7963249260858927386
CanvasRenderer:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 6691324969803418791}
m_CullTransparentMesh: 1
--- !u!114 &1157791559205469021
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 6691324969803418791}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: f4688fdb7df04437aeb418b961361dc5, type: 3}
m_Name:
m_EditorClassIdentifier:
m_Material: {fileID: 0}
m_Color: {r: 1, g: 1, b: 1, a: 1}
m_RaycastTarget: 1
m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0}
m_Maskable: 1
m_OnCullStateChanged:
m_PersistentCalls:
m_Calls: []
m_text: "\u7FFB\u8BD1\u9519\u8BEF\\bug\u53CD\u9988"
m_isRightToLeft: 0
m_fontAsset: {fileID: 11400000, guid: 8e119f168f1a6b745be02ef19f51610f, type: 2}
m_sharedMaterial: {fileID: -8081454072124122709, guid: 8e119f168f1a6b745be02ef19f51610f, type: 2}
m_fontSharedMaterials: []
m_fontMaterial: {fileID: 0}
m_fontMaterials: []
m_fontColor32:
serializedVersion: 2
rgba: 4285542705
m_fontColor: {r: 0.19215688, g: 0.19215688, b: 0.43921572, a: 1}
m_enableVertexGradient: 0
m_colorMode: 3
m_fontColorGradient:
topLeft: {r: 1, g: 1, b: 1, a: 1}
topRight: {r: 1, g: 1, b: 1, a: 1}
bottomLeft: {r: 1, g: 1, b: 1, a: 1}
bottomRight: {r: 1, g: 1, b: 1, a: 1}
m_fontColorGradientPreset: {fileID: 0}
m_spriteAsset: {fileID: 0}
m_tintAllSprites: 0
m_StyleSheet: {fileID: 0}
m_TextStyleHashCode: -1183493901
m_overrideHtmlColors: 0
m_faceColor:
serializedVersion: 2
rgba: 4294967295
m_fontSize: 24
m_fontSizeBase: 24
m_fontWeight: 400
m_enableAutoSizing: 1
m_fontSizeMin: 18
m_fontSizeMax: 24
m_fontStyle: 0
m_HorizontalAlignment: 2
m_VerticalAlignment: 512
m_textAlignment: 65535
m_characterSpacing: 0
m_wordSpacing: 0
m_lineSpacing: 0
m_lineSpacingMax: 0
m_paragraphSpacing: 0
m_charWidthMaxAdj: 0
m_enableWordWrapping: 1
m_wordWrappingRatios: 0.4
m_overflowMode: 0
m_linkedTextComponent: {fileID: 0}
parentLinkedComponent: {fileID: 0}
m_enableKerning: 1
m_enableExtraPadding: 0
checkPaddingRequired: 0
m_isRichText: 1
m_parseCtrlCharacters: 1
m_isOrthographic: 1
m_isCullingEnabled: 0
m_horizontalMapping: 0
m_verticalMapping: 0
m_uvLineOffset: 0
m_geometrySortingOrder: 0
m_IsTextObjectScaleStatic: 0
m_VertexBufferAutoSizeReduction: 0
m_useMaxVisibleDescender: 1
m_pageToDisplay: 1
m_margin: {x: 0, y: 0, z: 0, w: 0}
m_isUsingLegacyAnimationComponent: 0
m_isVolumetricText: 0
m_hasFontAssetChanged: 0
m_baseMaterial: {fileID: 0}
m_maskOffset: {x: 0, y: 0, z: 0, w: 0}
--- !u!114 &-7562340078456195498
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 6691324969803418791}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 6b27f832d22e4a8d916272b644937774, type: 3}
m_Name:
m_EditorClassIdentifier:
Ban: 0
NoExport: 0
FontBan: 0
Preset: 0
ID: 20074
FontID: 0
TextCfg: []
--- !u!1 &6763301404642353131
GameObject:
m_ObjectHideFlags: 0

View File

@ -86,9 +86,8 @@ GameObject:
- component: {fileID: 2438225421395076648}
- component: {fileID: 591212338599555894}
- component: {fileID: 7030574526118729750}
- component: {fileID: 959187344526487202}
m_Layer: 5
m_Name: UIOutsideMultiplayRoomMemberButtonRow
m_Name: UIOutsideMultiplayRoomMemberLineRow
m_TagString: Untagged
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
@ -152,23 +151,3 @@ MonoBehaviour:
m_FillOrigin: 0
m_UseSpriteMesh: 0
m_PixelsPerUnitMultiplier: 1.25
--- !u!114 &959187344526487202
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 7377647467463133788}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: b944c6e03ad742440a76463c2ecf000a, type: 3}
m_Name:
m_EditorClassIdentifier:
AvatarImage: {fileID: 0}
LeaderImage: {fileID: 0}
NameText: {fileID: 0}
StatusText: {fileID: 0}
ForcesText: {fileID: 0}
ForcesButton: {fileID: 0}
RefreshButton: {fileID: 0}
QuitButton: {fileID: 0}

View File

@ -1935,7 +1935,7 @@ MonoBehaviour:
NoExport: 0
FontBan: 0
Preset: 0
ID: 1635
ID: 19995
FontID: 2
TextCfg:
- Type: 1
@ -2505,7 +2505,7 @@ MonoBehaviour:
NoExport: 0
FontBan: 0
Preset: 0
ID: 2225
ID: 2231
FontID: 1
TextCfg:
- Type: 1
@ -3010,6 +3010,160 @@ MonoBehaviour:
m_FillOrigin: 0
m_UseSpriteMesh: 0
m_PixelsPerUnitMultiplier: 1
--- !u!1 &7335209913350123456
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- component: {fileID: 4667728109123456789}
- component: {fileID: 8120436579012345678}
- component: {fileID: 6024178395501234567}
- component: {fileID: 4008369207175740087}
m_Layer: 5
m_Name: OpenHintText
m_TagString: Untagged
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
--- !u!224 &4667728109123456789
RectTransform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 7335209913350123456}
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children: []
m_Father: {fileID: 2438225421395076648}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0, y: 0.5}
m_AnchorMax: {x: 0, y: 0.5}
m_AnchoredPosition: {x: 860.5266, y: 0.70847}
m_SizeDelta: {x: 271.2567, y: 57.991}
m_Pivot: {x: 0.5, y: 0.5}
--- !u!222 &8120436579012345678
CanvasRenderer:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 7335209913350123456}
m_CullTransparentMesh: 1
--- !u!114 &6024178395501234567
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 7335209913350123456}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: f4688fdb7df04437aeb418b961361dc5, type: 3}
m_Name:
m_EditorClassIdentifier:
m_Material: {fileID: 0}
m_Color: {r: 1, g: 1, b: 1, a: 1}
m_RaycastTarget: 1
m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0}
m_Maskable: 1
m_OnCullStateChanged:
m_PersistentCalls:
m_Calls: []
m_text: "\u5F85\u52A0\u5165"
m_isRightToLeft: 0
m_fontAsset: {fileID: 11400000, guid: ce4904f8ddac15944907907115531ad5, type: 2}
m_sharedMaterial: {fileID: 1214840240034325189, guid: ce4904f8ddac15944907907115531ad5, type: 2}
m_fontSharedMaterials: []
m_fontMaterial: {fileID: 0}
m_fontMaterials: []
m_fontColor32:
serializedVersion: 2
rgba: 4287072135
m_fontColor: {r: 0.5283019, g: 0.5283019, b: 0.5283019, a: 1}
m_enableVertexGradient: 0
m_colorMode: 3
m_fontColorGradient:
topLeft: {r: 1, g: 1, b: 1, a: 1}
topRight: {r: 1, g: 1, b: 1, a: 1}
bottomLeft: {r: 1, g: 1, b: 1, a: 1}
bottomRight: {r: 1, g: 1, b: 1, a: 1}
m_fontColorGradientPreset: {fileID: 0}
m_spriteAsset: {fileID: 0}
m_tintAllSprites: 0
m_StyleSheet: {fileID: 0}
m_TextStyleHashCode: -1183493901
m_overrideHtmlColors: 0
m_faceColor:
serializedVersion: 2
rgba: 4294967295
m_fontSize: 28
m_fontSizeBase: 28
m_fontWeight: 400
m_enableAutoSizing: 1
m_fontSizeMin: 12
m_fontSizeMax: 28
m_fontStyle: 0
m_HorizontalAlignment: 2
m_VerticalAlignment: 512
m_textAlignment: 65535
m_characterSpacing: 0
m_wordSpacing: 0
m_lineSpacing: 0
m_lineSpacingMax: 0
m_paragraphSpacing: 0
m_charWidthMaxAdj: 0
m_enableWordWrapping: 0
m_wordWrappingRatios: 0.4
m_overflowMode: 1
m_linkedTextComponent: {fileID: 0}
parentLinkedComponent: {fileID: 0}
m_enableKerning: 1
m_enableExtraPadding: 0
checkPaddingRequired: 0
m_isRichText: 1
m_parseCtrlCharacters: 1
m_isOrthographic: 1
m_isCullingEnabled: 0
m_horizontalMapping: 0
m_verticalMapping: 0
m_uvLineOffset: 0
m_geometrySortingOrder: 0
m_IsTextObjectScaleStatic: 0
m_VertexBufferAutoSizeReduction: 0
m_useMaxVisibleDescender: 1
m_pageToDisplay: 1
m_margin: {x: 0, y: 0, z: 0, w: 0}
m_isUsingLegacyAnimationComponent: 0
m_isVolumetricText: 0
m_hasFontAssetChanged: 0
m_baseMaterial: {fileID: 0}
m_maskOffset: {x: 0, y: 0, z: 0, w: 0}
--- !u!114 &4008369207175740087
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 7335209913350123456}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 6b27f832d22e4a8d916272b644937774, type: 3}
m_Name:
m_EditorClassIdentifier:
Ban: 0
NoExport: 0
FontBan: 0
Preset: 0
ID: 20068
FontID: 0
TextCfg: []
--- !u!1 &7372916197570849001
GameObject:
m_ObjectHideFlags: 0
@ -3123,6 +3277,7 @@ RectTransform:
- {fileID: 8168637493792220255}
- {fileID: 2439652715768046550}
- {fileID: 8207125967839836190}
- {fileID: 4667728109123456789}
- {fileID: 6303313510194795360}
- {fileID: 3110038563642209199}
- {fileID: 6579359848737393927}
@ -3184,11 +3339,19 @@ MonoBehaviour:
m_Script: {fileID: 11500000, guid: b944c6e03ad742440a76463c2ecf000a, type: 3}
m_Name:
m_EditorClassIdentifier:
AvatarRoot: {fileID: 6592483684148402150}
AvatarImage: {fileID: 7762177666811050190}
LeaderRoot: {fileID: 3382124894360767356}
LeaderImage: {fileID: 2028509407625721253}
NameText: {fileID: 6503687505537430979}
OpenHintText: {fileID: 6024178395501234567}
StatusText: {fileID: 9094648081951770890}
ForcesText: {fileID: 2875130741396939258}
ForcesButton: {fileID: 8612292243586636816}
RefreshButton: {fileID: 8612292243586636816}
QuitButton: {fileID: 8717023295481533829}
ForceLeftButton: {fileID: 8612292243586636816}
ForceRightButton: {fileID: 6556385597021353403}
TeamText: {fileID: 3332880645268972119}
TeamLeftButton: {fileID: 8962252367205184452}
TeamRightButton: {fileID: 2906850645216239320}

View File

@ -2200,9 +2200,9 @@ RectTransform:
- {fileID: 7354981697241446154}
m_Father: {fileID: 5566567905778636362}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0, y: 1}
m_AnchorMax: {x: 0, y: 1}
m_AnchoredPosition: {x: 36.7275, y: -39.524}
m_AnchorMin: {x: 0, y: 0}
m_AnchorMax: {x: 0, y: 0}
m_AnchoredPosition: {x: 0, y: 0}
m_SizeDelta: {x: 73.455, y: 79.048}
m_Pivot: {x: 0.5, y: 0.5}
--- !u!222 &6757100784094017993
@ -3167,9 +3167,9 @@ RectTransform:
- {fileID: 3962389234318188601}
m_Father: {fileID: 3698126482628459019}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0, y: 1}
m_AnchorMax: {x: 0, y: 1}
m_AnchoredPosition: {x: 377.4315, y: -59.52385}
m_AnchorMin: {x: 0, y: 0}
m_AnchorMax: {x: 0, y: 0}
m_AnchoredPosition: {x: 0, y: 0}
m_SizeDelta: {x: 714.863, y: 79.0477}
m_Pivot: {x: 0.5, y: 0.5}
--- !u!114 &2164550660865405208
@ -3529,9 +3529,9 @@ RectTransform:
- {fileID: 7578212672192949060}
m_Father: {fileID: 5566567905778636362}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0, y: 1}
m_AnchorMax: {x: 0, y: 1}
m_AnchoredPosition: {x: 416.73907, y: -24.0022}
m_AnchorMin: {x: 0, y: 0}
m_AnchorMax: {x: 0, y: 0}
m_AnchoredPosition: {x: 0, y: 0}
m_SizeDelta: {x: 686.5681, y: 48.0044}
m_Pivot: {x: 0.5, y: 0.5}
--- !u!222 &6522695967421094694

View File

@ -302,7 +302,7 @@ RectTransform:
m_AnchorMin: {x: 0, y: 0}
m_AnchorMax: {x: 0, y: 0}
m_AnchoredPosition: {x: 0, y: 0}
m_SizeDelta: {x: 121.7672, y: 35.2382}
m_SizeDelta: {x: 208.1909, y: 35.238}
m_Pivot: {x: 0.5, y: 0.5}
--- !u!222 &9186104963804813620
CanvasRenderer:
@ -4735,6 +4735,171 @@ CanvasRenderer:
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 3645069085365621322}
m_CullTransparentMesh: 1
--- !u!1 &3732511013873620431
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- component: {fileID: 1535537025373028270}
- component: {fileID: 3394967736349762794}
- component: {fileID: 5925858155960801726}
- component: {fileID: 7130562122336279992}
m_Layer: 5
m_Name: Text
m_TagString: Untagged
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
--- !u!224 &1535537025373028270
RectTransform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 3732511013873620431}
m_LocalRotation: {x: -0, y: -0, z: -0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children: []
m_Father: {fileID: 8441048275297643431}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0.5, y: 0.5}
m_AnchorMax: {x: 0.5, y: 0.5}
m_AnchoredPosition: {x: -1.9145, y: 0.000014783}
m_SizeDelta: {x: 192.3311, y: 35.238}
m_Pivot: {x: 0.5, y: 0.5}
--- !u!222 &3394967736349762794
CanvasRenderer:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 3732511013873620431}
m_CullTransparentMesh: 1
--- !u!114 &5925858155960801726
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 3732511013873620431}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: f4688fdb7df04437aeb418b961361dc5, type: 3}
m_Name:
m_EditorClassIdentifier:
m_Material: {fileID: 0}
m_Color: {r: 1, g: 1, b: 1, a: 1}
m_RaycastTarget: 0
m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0}
m_Maskable: 1
m_OnCullStateChanged:
m_PersistentCalls:
m_Calls: []
m_text: "\u7FFB\u8BD1\u9519\u8BEF\\bug\u53CD\u9988"
m_isRightToLeft: 0
m_fontAsset: {fileID: 11400000, guid: 8e119f168f1a6b745be02ef19f51610f, type: 2}
m_sharedMaterial: {fileID: -8081454072124122709, guid: 8e119f168f1a6b745be02ef19f51610f, type: 2}
m_fontSharedMaterials: []
m_fontMaterial: {fileID: 0}
m_fontMaterials: []
m_fontColor32:
serializedVersion: 2
rgba: 4294967295
m_fontColor: {r: 1, g: 1, b: 1, a: 1}
m_enableVertexGradient: 0
m_colorMode: 3
m_fontColorGradient:
topLeft: {r: 1, g: 1, b: 1, a: 1}
topRight: {r: 1, g: 1, b: 1, a: 1}
bottomLeft: {r: 1, g: 1, b: 1, a: 1}
bottomRight: {r: 1, g: 1, b: 1, a: 1}
m_fontColorGradientPreset: {fileID: 0}
m_spriteAsset: {fileID: 0}
m_tintAllSprites: 0
m_StyleSheet: {fileID: 0}
m_TextStyleHashCode: -1183493901
m_overrideHtmlColors: 0
m_faceColor:
serializedVersion: 2
rgba: 4294967295
m_fontSize: 18
m_fontSizeBase: 32
m_fontWeight: 400
m_enableAutoSizing: 1
m_fontSizeMin: 4
m_fontSizeMax: 18
m_fontStyle: 0
m_HorizontalAlignment: 2
m_VerticalAlignment: 512
m_textAlignment: 65535
m_characterSpacing: 0
m_wordSpacing: 0
m_lineSpacing: 0
m_lineSpacingMax: 0
m_paragraphSpacing: 0
m_charWidthMaxAdj: 0
m_enableWordWrapping: 1
m_wordWrappingRatios: 0.4
m_overflowMode: 0
m_linkedTextComponent: {fileID: 0}
parentLinkedComponent: {fileID: 0}
m_enableKerning: 1
m_enableExtraPadding: 0
checkPaddingRequired: 0
m_isRichText: 1
m_parseCtrlCharacters: 1
m_isOrthographic: 1
m_isCullingEnabled: 0
m_horizontalMapping: 0
m_verticalMapping: 0
m_uvLineOffset: 0
m_geometrySortingOrder: 0
m_IsTextObjectScaleStatic: 0
m_VertexBufferAutoSizeReduction: 0
m_useMaxVisibleDescender: 1
m_pageToDisplay: 1
m_margin: {x: 0, y: 0, z: 0, w: 0}
m_isUsingLegacyAnimationComponent: 0
m_isVolumetricText: 0
m_hasFontAssetChanged: 0
m_baseMaterial: {fileID: 0}
m_maskOffset: {x: 0, y: 0, z: 0, w: 0}
--- !u!114 &7130562122336279992
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 3732511013873620431}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 6b27f832d22e4a8d916272b644937774, type: 3}
m_Name:
m_EditorClassIdentifier:
Ban: 0
NoExport: 0
FontBan: 0
Preset: 0
ID: 20074
FontID: 1
TextCfg:
- Type: 1
ApplyFontSize: 0
FontSize: 14
ApplyCharacterSpacing: 0
CharacterSpacing: 0
ApplyWordSpacing: 0
WordSpacing: 0
ApplyLineSpacing: 0
LineSpacing: 0
ApplyParagraphSpacing: 0
ParagraphSpacing: 0
--- !u!1 &3873969227329466608
GameObject:
m_ObjectHideFlags: 0
@ -5948,6 +6113,7 @@ RectTransform:
m_ConstrainProportionsScale: 0
m_Children:
- {fileID: 4837278989566805878}
- {fileID: 8441048275297643431}
m_Father: {fileID: 1451539500624979695}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0, y: 0}
@ -5968,12 +6134,12 @@ MonoBehaviour:
m_Name:
m_EditorClassIdentifier:
m_Padding:
m_Left: 0
m_Left: 20
m_Right: 0
m_Top: 0
m_Bottom: 0
m_ChildAlignment: 0
m_Spacing: 0
m_Spacing: 20
m_ChildForceExpandWidth: 0
m_ChildForceExpandHeight: 0
m_ChildControlWidth: 0
@ -8965,6 +9131,7 @@ MonoBehaviour:
CloseButton: {fileID: 2931647333088810527}
BlockButton: {fileID: 5282322453820411557}
RepairRendererButton: {fileID: 1941810936467228881}
BugReportButton: {fileID: 1972885062573234700}
LanguageOptionGroup: {fileID: 3444656944565885498}
KeyMomentToggle: {fileID: 4395786735994074065}
BgmContinuousToggle: {fileID: 5173756232815940152}
@ -9094,8 +9261,8 @@ MonoBehaviour:
m_TargetGraphic: {fileID: 7771937312228267370}
m_HandleRect: {fileID: 6801890359025387631}
m_Direction: 2
m_Value: 1
m_Size: 0.8625885
m_Value: 1.0000005
m_Size: 0.86258847
m_NumberOfSteps: 0
m_OnValueChanged:
m_PersistentCalls:
@ -9626,6 +9793,242 @@ RectTransform:
m_AnchoredPosition: {x: 0.00012207031, y: 0}
m_SizeDelta: {x: -20, y: -20}
m_Pivot: {x: 0.5, y: 0.5}
--- !u!1 &8249099813133539951
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- component: {fileID: 8441048275297643431}
- component: {fileID: 3050059261919365178}
- component: {fileID: 8280418899881803767}
- component: {fileID: 1972885062573234700}
- component: {fileID: 4910684173926155080}
- component: {fileID: 7833713733495233730}
m_Layer: 5
m_Name: BugReport
m_TagString: Untagged
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
--- !u!224 &8441048275297643431
RectTransform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 8249099813133539951}
m_LocalRotation: {x: -0, y: -0, z: -0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children:
- {fileID: 1535537025373028270}
m_Father: {fileID: 1573632891455071784}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0, y: 0}
m_AnchorMax: {x: 0, y: 0}
m_AnchoredPosition: {x: 0, y: 0}
m_SizeDelta: {x: 230.2221, y: 35.238}
m_Pivot: {x: 0.5, y: 0.5}
--- !u!222 &3050059261919365178
CanvasRenderer:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 8249099813133539951}
m_CullTransparentMesh: 1
--- !u!114 &8280418899881803767
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 8249099813133539951}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3}
m_Name:
m_EditorClassIdentifier:
m_Material: {fileID: 0}
m_Color: {r: 1, g: 1, b: 1, a: 1}
m_RaycastTarget: 1
m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0}
m_Maskable: 1
m_OnCullStateChanged:
m_PersistentCalls:
m_Calls: []
m_Sprite: {fileID: 21300000, guid: 1e43ee4b2c787014db05c4f94b37be0e, type: 3}
m_Type: 1
m_PreserveAspect: 1
m_FillCenter: 1
m_FillMethod: 4
m_FillAmount: 1
m_FillClockwise: 1
m_FillOrigin: 0
m_UseSpriteMesh: 0
m_PixelsPerUnitMultiplier: 2.5
--- !u!114 &1972885062573234700
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 8249099813133539951}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 4e29b1a8efbd4b44bb3f3716e73f07ff, type: 3}
m_Name:
m_EditorClassIdentifier:
m_Navigation:
m_Mode: 3
m_WrapAround: 0
m_SelectOnUp: {fileID: 0}
m_SelectOnDown: {fileID: 0}
m_SelectOnLeft: {fileID: 0}
m_SelectOnRight: {fileID: 0}
m_Transition: 0
m_Colors:
m_NormalColor: {r: 1, g: 1, b: 1, a: 1}
m_HighlightedColor: {r: 0.9607843, g: 0.9607843, b: 0.9607843, a: 1}
m_PressedColor: {r: 0.78431374, g: 0.78431374, b: 0.78431374, a: 1}
m_SelectedColor: {r: 0.9607843, g: 0.9607843, b: 0.9607843, a: 1}
m_DisabledColor: {r: 0.78431374, g: 0.78431374, b: 0.78431374, a: 0.5019608}
m_ColorMultiplier: 1
m_FadeDuration: 0.1
m_SpriteState:
m_HighlightedSprite: {fileID: 0}
m_PressedSprite: {fileID: 0}
m_SelectedSprite: {fileID: 0}
m_DisabledSprite: {fileID: 0}
m_AnimationTriggers:
m_NormalTrigger: Normal
m_HighlightedTrigger: Highlighted
m_PressedTrigger: Pressed
m_SelectedTrigger: Selected
m_DisabledTrigger: Disabled
m_Interactable: 1
m_TargetGraphic: {fileID: 8280418899881803767}
m_OnClick:
m_PersistentCalls:
m_Calls: []
--- !u!114 &4910684173926155080
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 8249099813133539951}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 33d75335a9dad784a91baba5578371fb, type: 3}
m_Name:
m_EditorClassIdentifier:
hoverSound: {fileID: 8300000, guid: b70699fb70c0d3b44a22f2bdf87da7d2, type: 3}
targetScale: 1.1
clickSound: {fileID: 0}
pressScale: 0.95
scaleDuration: 0.1
--- !u!82 &7833713733495233730
AudioSource:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 8249099813133539951}
m_Enabled: 1
serializedVersion: 4
OutputAudioMixerGroup: {fileID: 0}
m_audioClip: {fileID: 0}
m_PlayOnAwake: 1
m_Volume: 1
m_Pitch: 1
Loop: 0
Mute: 0
Spatialize: 0
SpatializePostEffects: 0
Priority: 128
DopplerLevel: 1
MinDistance: 1
MaxDistance: 500
Pan2D: 0
rolloffMode: 0
BypassEffects: 0
BypassListenerEffects: 0
BypassReverbZones: 0
rolloffCustomCurve:
serializedVersion: 2
m_Curve:
- serializedVersion: 3
time: 0
value: 1
inSlope: 0
outSlope: 0
tangentMode: 0
weightedMode: 0
inWeight: 0.33333334
outWeight: 0.33333334
- serializedVersion: 3
time: 1
value: 0
inSlope: 0
outSlope: 0
tangentMode: 0
weightedMode: 0
inWeight: 0.33333334
outWeight: 0.33333334
m_PreInfinity: 2
m_PostInfinity: 2
m_RotationOrder: 4
panLevelCustomCurve:
serializedVersion: 2
m_Curve:
- serializedVersion: 3
time: 0
value: 0
inSlope: 0
outSlope: 0
tangentMode: 0
weightedMode: 0
inWeight: 0.33333334
outWeight: 0.33333334
m_PreInfinity: 2
m_PostInfinity: 2
m_RotationOrder: 4
spreadCustomCurve:
serializedVersion: 2
m_Curve:
- serializedVersion: 3
time: 0
value: 0
inSlope: 0
outSlope: 0
tangentMode: 0
weightedMode: 0
inWeight: 0.33333334
outWeight: 0.33333334
m_PreInfinity: 2
m_PostInfinity: 2
m_RotationOrder: 4
reverbZoneMixCustomCurve:
serializedVersion: 2
m_Curve:
- serializedVersion: 3
time: 0
value: 1
inSlope: 0
outSlope: 0
tangentMode: 0
weightedMode: 0
inWeight: 0.33333334
outWeight: 0.33333334
m_PreInfinity: 2
m_PostInfinity: 2
m_RotationOrder: 4
--- !u!1 &8424698946813465439
GameObject:
m_ObjectHideFlags: 0
@ -10794,12 +11197,12 @@ MonoBehaviour:
m_faceColor:
serializedVersion: 2
rgba: 4294967295
m_fontSize: 14
m_fontSize: 18
m_fontSizeBase: 32
m_fontWeight: 400
m_enableAutoSizing: 1
m_fontSizeMin: 1
m_fontSizeMax: 14
m_fontSizeMin: 4
m_fontSizeMax: 18
m_fontStyle: 0
m_HorizontalAlignment: 2
m_VerticalAlignment: 512

View File

@ -874,7 +874,7 @@ GameObject:
- component: {fileID: 39487247}
- component: {fileID: 39487248}
m_Layer: 5
m_Name: OutsideTop
m_Name: GlobalTop
m_TagString: Untagged
m_Icon: {fileID: 0}
m_NavMeshLayer: 0

View File

@ -84,9 +84,11 @@ namespace TH1_Core.Events
EventManager.Subscribe<HideUIOutsideStory>(HandleHideUIOutsideStory);
EventManager.Subscribe<ShowUIOutsideMod>(HandleShowUIOutsideMod);
EventManager.Subscribe<HideUIOutsideMod>(HandleHideUIOutsideMod);
EventManager.Subscribe<ShowUIGlobalBugReport>(HandleShowUIGlobalBugReport);
EventManager.Subscribe<HideUIGlobalBugReport>(HandleHideUIGlobalBugReport);
EventManager.Subscribe<ShowUIBottomBottomBar>(HandleShowUIBottomBottomBar);
EventManager.Subscribe<HideUIBottomBottomBar>(HandleHideUIBottomBottomBar);
EventManager.Subscribe<ShowUIBottomBottomBarNextTurn>(HandleShowUIBottomBottomBarNextTurn);
@ -175,8 +177,10 @@ namespace TH1_Core.Events
EventManager.Unsubscribe<ShowUIOutsideMod>(HandleShowUIOutsideMod);
EventManager.Unsubscribe<HideUIOutsideMod>(HandleHideUIOutsideMod);
EventManager.Unsubscribe<ShowUIGlobalBugReport>(HandleShowUIGlobalBugReport);
EventManager.Unsubscribe<HideUIGlobalBugReport>(HandleHideUIGlobalBugReport);
EventManager.Unsubscribe<ShowUIBottomBottomBar>(HandleShowUIBottomBottomBar);
EventManager.Unsubscribe<HideUIBottomBottomBar>(HandleHideUIBottomBottomBar);
EventManager.Unsubscribe<ShowUIBottomBottomBarNextTurn>(HandleShowUIBottomBottomBarNextTurn);
@ -457,7 +461,17 @@ namespace TH1_Core.Events
UIManager.Instance.UIOutsideManager.HideCurTask();
}
private void HandleShowUIGlobalBugReport(ShowUIGlobalBugReport evt)
{
ViewControllerManager.UIGlobalBugReportController?.OpenWithParam(evt);
}
private void HandleHideUIGlobalBugReport(HideUIGlobalBugReport evt)
{
ViewControllerManager.UIGlobalBugReportController?.Close();
}
private void HandleShowUIOutsideInvited(ShowUIOutsideInvited evt)
{
UIManager.Instance.UIOutsideManager.OpenInvited(evt);

View File

@ -196,7 +196,11 @@ namespace TH1_Core.Events
public struct ShowUIOutsideInvited { public ulong LobbyId; }
public struct HideUIOutsideInvited { }
//----------------------------------------- UIGlobal 相关的事件 ----------------------------------
public struct ShowUIGlobalBugReport { }
public struct HideUIGlobalBugReport { }
// ------------------------------------------------ UIBottom 相关的事件 ------------------------------------

View File

@ -38,7 +38,7 @@ namespace TH1_Core.Managers
OutsideUI,//所有外部UI
BottomUI,//包含 bottombar ranking 等
TopUI,//包含topbar
OutsideTopUI//主要是全局通知这种
GlobalTopUI//主要是全局通知这种
};
public class UIManager
@ -92,7 +92,7 @@ namespace TH1_Core.Managers
UIResourceController.Instance.SetUIRoot(UIRootType.BottomUI,ROUIManager.transform.Find("Inside/Bottom"));
UIResourceController.Instance.SetUIRoot(UIRootType.TopUI,ROUIManager.transform.Find("Inside/Top"));
UIResourceController.Instance.SetUIRoot(UIRootType.OutsideUI,ROUIManager.transform.Find("Outside"));
UIResourceController.Instance.SetUIRoot(UIRootType.OutsideTopUI,ROUIManager.transform.Find("OutsideTop"));
UIResourceController.Instance.SetUIRoot(UIRootType.GlobalTopUI,ROUIManager.transform.Find("GlobalTop"));
UIInfoManager = new UIInfoManager();

View File

@ -38,6 +38,9 @@ public class TextDataAssets : ScriptableObject
[MultilingualField]public string OutsideMultiplayRoomNameSuffix;
[MultilingualField]public string OutsideMultiplayTeamTitle;
[MultilingualField]public string OutsideMultiplayAIPlayerTitle;
[MultilingualField]public string OutsideMultiplayOpenTitle;
[MultilingualField]public string OutsideMultiplayReadyTitle;
[MultilingualField]public string OutsideMultiplayOpenHint;
[MultilingualField]public string OutsideHistoryDropListNoLimitP;
[MultilingualField]public string OutsideHistoryDropList2P;

View File

@ -33,16 +33,19 @@ namespace TH1_Logic.Chat
public void SendChatItem(string message)
{
var lobby = LobbyManager.Instance.Lobby;
if (lobby == null || !lobby.IsInLobby()) return;
var item = new ChatItem();
item.Hash = System.Guid.NewGuid().GetHashCode();
item.SenderId = LobbyManager.Instance.Lobby.GetSelfMemberId();
item.SenderId = lobby.GetSelfMemberId();
item.Message = BannedWordFilter.Filter(message);
item.Time = Time.time;
ChatItems.Add(item);
_chatItems[item.Hash] = item;
if (LobbyManager.Instance.Lobby.IsLobbyOwner()) GameNetSender.Instance.BroadcastChatMessage(item);
if (lobby.IsLobbyOwner()) GameNetSender.Instance.BroadcastChatMessage(item);
else GameNetSender.Instance.SendChatMessage(item);
TrimIfNeeded();

View File

@ -22,6 +22,7 @@ using TH1_Logic.Net;
using TH1_UI.Controller.Info;
using TH1_UI.HintUI;
using TH1_UI.View.Bottom;
using TH1_UI.View.Common;
using UI;
using UnityEngine.UI;
@ -87,15 +88,15 @@ namespace Logic
public void Update()
{
// 聊天输入框激活时Enter发送/关闭ESC/右键关闭,其余快捷键全部屏蔽
if (UIBottomNetChatAreaMono.IsInputFieldFocused)
if (UIChatAreaMono.IsInputFieldFocused)
{
if (Input.GetKeyDown(KeyCode.Return) || Input.GetKeyDown(KeyCode.KeypadEnter))
{
UIBottomNetChatAreaMono.SendOrClose();
UIChatAreaMono.SendOrClose();
}
else if (Input.GetKeyDown(KeyCode.Escape) || Input.GetMouseButtonDown(1))
{
UIBottomNetChatAreaMono.HideInputArea();
UIChatAreaMono.HideInputArea();
}
return;
}
@ -339,17 +340,17 @@ namespace Logic
// Enter键 - 联机聊天快捷键
if ((Input.GetKeyDown(KeyCode.Return) || Input.GetKeyDown(KeyCode.KeypadEnter))
&& Main.MapData?.Net != null && Main.MapData.Net.Mode == NetMode.Multi
&& UIBottomNetChatAreaMono.IsActive)
&& UIChatAreaMono.IsActive)
{
if (!UIBottomNetChatAreaMono.IsInputAreaVisible)
if (!UIChatAreaMono.IsInputAreaVisible)
{
// 输入区未打开 → 打开并聚焦
UIBottomNetChatAreaMono.ShowInputArea();
UIChatAreaMono.ShowInputArea();
}
else
{
// 输入区已打开但未聚焦 → 聚焦
UIBottomNetChatAreaMono.FocusInputField();
UIChatAreaMono.FocusInputField();
}
return;
}

View File

@ -48,7 +48,7 @@ namespace TH1_Logic.Net
public void KickMember(ulong memberId);
// 创建房间
public void CreateLobby(int maxMembers = 4,bool isPublic = true, string password = "");
public void CreateLobby(int maxMembers = 4,bool isPublic = true, string password = "", string roomName = "");
// 加入房间
public void JoinLobbyById(ulong lobbyId, string password = "");
@ -196,9 +196,9 @@ namespace TH1_Logic.Net
return null;
}
public void CreateLobby(int maxMembers = 4,bool isPublic = true, string password = "")
public void CreateLobby(int maxMembers = 4,bool isPublic = true, string password = "", string roomName = "")
{
}
// 获取成员数量

View File

@ -34,5 +34,10 @@ namespace Logic.Skill
self.AddSkill_Legacy(SkillType.KAGUYAFRENCHFOREVERBUFF, mapData, false, 1, true, 5, false, SpecialAddSkillType.Normal, self.Id);
}
}
public override bool ReservedOnTransform(UnitData self, UnitFullType fullType)
{
return true;
}
}
}

View File

@ -105,7 +105,6 @@ namespace TH1_Logic.Steam
ForceCleanupPort(TargetPort);
LogSystem.LogInfo($"尝试虚拟端口 {TargetPort}...");
_listenSocket = SteamNetworkingSockets.CreateListenSocketP2P(TargetPort, 0, null);
if (_listenSocket != HSteamListenSocket.Invalid)
{
LogSystem.LogInfo($"成功在虚拟端口 {TargetPort} 创建监听套接字: {_listenSocket}");

View File

@ -13,6 +13,7 @@ using RuntimeData;
using Steamworks;
using TH1_Core.Events;
using TH1_Core.Managers;
using TH1_Logic.Chat;
using TH1_Logic.Config;
using TH1_Logic.Core;
using TH1_Logic.Net;
@ -93,6 +94,7 @@ namespace TH1_Logic.Steam
private const int LobbyReportRenameThreshold = 5;
private const string ReportedLobbyDefaultRoomName = "Default";
private string _pendingLobbyPassword = "";
private string _pendingLobbyRoomName = "";
private bool _pendingLobbyIsPublic = true;
private CSteamID _pendingJoinLobby = CSteamID.Nil;
private string _pendingJoinPassword = "";
@ -631,7 +633,7 @@ namespace TH1_Logic.Steam
}
// 建房
public void CreateLobby(int maxMembers = 4, bool isPublic = true, string password = "")
public void CreateLobby(int maxMembers = 4, bool isPublic = true, string password = "", string roomName = "")
{
if (!EnsureSteamReadyForLobbyAction(nameof(CreateLobby))) return;
if (CurrentState != LobbyState.None)
@ -641,6 +643,7 @@ namespace TH1_Logic.Steam
}
_pendingLobbyPassword = password ?? "";
_pendingLobbyRoomName = FilterRoomName(string.IsNullOrWhiteSpace(roomName) ? GetDefaultRoomName(SelfName) : roomName.Trim());
_pendingLobbyIsPublic = isPublic;
CurrentState = LobbyState.Creating;
LogSystem.LogInfo($"Creating {(isPublic ? "public" : "friends-only")} lobby with max members: {maxMembers}, hasPassword: {!string.IsNullOrEmpty(_pendingLobbyPassword)}");
@ -1172,6 +1175,7 @@ namespace TH1_Logic.Steam
}
}
private void RefreshLobbyListInfo(LobbyListInfo lobbyInfo, CSteamID lobbyId)
{
if (lobbyInfo == null) return;
@ -1203,6 +1207,11 @@ namespace TH1_Logic.Steam
{
var gameStateData = SteamMatchmaking.GetLobbyData(lobbyId, LobbyGameStateKey);
return int.TryParse(gameStateData, out var gameState) ? gameState : 0;
public static string GetDefaultRoomName(string selfName)
{
return selfName + MultilingualManager.Instance.GetMultilingualText(Table.Instance.TextDataAssets.OutsideMultiplayRoomNameSuffix);
}
// 过滤房间名称,只保留中文、英文字母、数字和常用符号
@ -1231,8 +1240,9 @@ namespace TH1_Logic.Steam
// 达到最大长度时停止
if (result.Length >= maxLength) break;
}
if (string.IsNullOrEmpty(result.ToString())) return "Default";
return result.ToString();
var filteredRoomName = result.ToString();
if (string.IsNullOrEmpty(filteredRoomName)) return "Default";
return BannedWordFilter.Filter(filteredRoomName);
}
public bool IsInitialized()
@ -1324,6 +1334,39 @@ namespace TH1_Logic.Steam
return SteamMatchmaking.GetLobbyData(CurrentLobby, key);
}
public string GetRoomName()
{
var roomName = GetLobbyData("RoomName");
return FilterRoomName(string.IsNullOrWhiteSpace(roomName) ? GetDefaultRoomName(SelfName) : roomName);
}
public bool SetRoomName(string roomName)
{
if (!IsLobbyOwner())
{
LogSystem.LogError("Only lobby owner can set room name");
NetworkPlayerTipManager.Instance.Request(NetworkPlayerTipType.LobbyOperationFailed);
return false;
}
if (!CurrentLobby.IsValid()) return false;
roomName = FilterRoomName(roomName);
bool success = SteamMatchmaking.SetLobbyData(CurrentLobby, "RoomName", roomName);
if (success)
{
EventManager.Publish(new UpdateUIOutsideMultiplayLobbyList());
EventManager.Publish(new UpdateUIOutsideMultiplayRoomSetting());
}
else
{
LogSystem.LogError($"Failed to set room name: {roomName}");
NetworkPlayerTipManager.Instance.Request(NetworkPlayerTipType.LobbyOperationFailed);
}
return success;
}
// 房间创建回调
private void OnLobbyCreatedCallback(LobbyCreated_t data)
{
@ -1331,6 +1374,7 @@ namespace TH1_Logic.Steam
{
CurrentState = LobbyState.None;
_pendingLobbyPassword = "";
_pendingLobbyRoomName = "";
_pendingLobbyIsPublic = true;
LogSystem.LogError($"Failed to create lobby: {data.m_eResult}");
NetworkPlayerTipManager.Instance.Request(NetworkPlayerTipType.LobbyCreateFailed);
@ -1354,6 +1398,7 @@ namespace TH1_Logic.Steam
SteamMatchmaking.SetLobbyData(CurrentLobby, "Version", ConfigManager.Instance.VersionCfg.CurVersionInfo.Version);
//SteamMatchmaking.SetLobbyData(CurrentLobby, "RoomName", FilterRoomName(SelfName + MultilingualManager.Instance.GetMultilingualText(Table.Instance.TextDataAssets.OutsideMultiplayRoomNameSuffix)));
SteamMatchmaking.SetLobbyData(CurrentLobby, LobbyRoomNameKey, SelfName + MultilingualManager.Instance.GetMultilingualText(Table.Instance.TextDataAssets.OutsideMultiplayRoomNameSuffix));
SteamMatchmaking.SetLobbyData(CurrentLobby, "RoomName", _pendingLobbyRoomName);
SteamMatchmaking.SetLobbyData(CurrentLobby, "RoomCode", GenerateRoomCode());
SteamMatchmaking.SetLobbyData(CurrentLobby, LobbyGameStateKey, "0");
SteamMatchmaking.SetLobbyData(CurrentLobby, LobbyReportCountKey, "0");
@ -1361,6 +1406,7 @@ namespace TH1_Logic.Steam
SteamMatchmaking.SetLobbyData(CurrentLobby, LobbyPasswordKey, _pendingLobbyPassword);
SteamMatchmaking.SetLobbyData(CurrentLobby, LobbyIsPublicKey, _pendingLobbyIsPublic ? "true" : "false");
_pendingLobbyPassword = "";
_pendingLobbyRoomName = "";
_pendingLobbyIsPublic = true;
// 设置Rich Presence
@ -1400,6 +1446,12 @@ namespace TH1_Logic.Steam
private void OnLobbyDataUpdateCallback(LobbyDataUpdate_t data)
{
var lobbyId = new CSteamID(data.m_ulSteamIDLobby);
if (data.m_bSuccess != 0 && CurrentLobby.IsValid() && lobbyId == CurrentLobby)
{
EventManager.Publish(new UpdateUIOutsideMultiplayRoomSetting());
EventManager.Publish(new UpdateUIOutsideMultiplayLobbyList());
}
if (!_pendingJoinLobby.IsValid() || lobbyId != _pendingJoinLobby) return;
var password = _pendingJoinPassword;
@ -1592,6 +1644,7 @@ namespace TH1_Logic.Steam
_steamSessionFailCount = 0;
_refreshSteamStatus = 0f;
_pendingLobbyPassword = "";
_pendingLobbyRoomName = "";
_pendingLobbyIsPublic = true;
ClearPendingJoinLobby();
_lobbyReporters.Clear();

View File

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: da5416ff5e2fd5d448abc57411ce9d52
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,49 @@
using TH1_Core.Events;
using TH1_UI.Controller.Base;
using TH1_UI.View.Global;
namespace TH1_UI.Controller.Global
{
public class UIGlobalBugReportController : ViewController<UIGlobalBugReportView>
{
public UIGlobalBugReportController() { }
protected override void RegisterEventCallback()
{
base.RegisterEventCallback();
if (WindowScript != null)
{
WindowScript.OnBtnCloseClick += _OnBtnCloseClick;
}
}
protected override void UnregisterEventCallback()
{
if (WindowScript != null)
{
WindowScript.OnBtnCloseClick = null;
}
base.UnregisterEventCallback();
}
protected override void OnOpen()
{
base.OnOpen();
if (_openParameter is ShowUIGlobalBugReport evt)
{
WindowScript?.SetContent(evt);
}
}
public override bool Close()
{
WindowScript?.CloseView();
return base.Close();
}
void _OnBtnCloseClick()
{
Close();
}
}
}

Some files were not shown because too many files have changed in this diff Show More