1116 lines
58 KiB
C#
1116 lines
58 KiB
C#
using AnotherReplayReader.ReplayFile;
|
||
using System;
|
||
using System.Collections.Generic;
|
||
using System.Collections.Immutable;
|
||
using System.IO;
|
||
using System.Linq;
|
||
using System.Net.Http;
|
||
using System.Net.Http.Headers;
|
||
using System.Text;
|
||
using System.Text.Json;
|
||
using System.Text.RegularExpressions;
|
||
using System.Threading;
|
||
using System.Threading.Tasks;
|
||
|
||
namespace AnotherReplayReader.Utils
|
||
{
|
||
internal sealed class AIAnalyze
|
||
{
|
||
public enum AIChunkType
|
||
{
|
||
Reasoning,
|
||
Content,
|
||
Json
|
||
}
|
||
|
||
public struct AIChunk
|
||
{
|
||
public AIChunkType Type;
|
||
public string Text;
|
||
}
|
||
|
||
public struct Result
|
||
{
|
||
public string Response;
|
||
public List<(TimeSpan Start, TimeSpan End, string Description)> Segments;
|
||
public int CurrentSegment;
|
||
public int? PromptTokens;
|
||
public int? CompletionTokens;
|
||
public int? TotalTokens;
|
||
public int? ReasoningTokens;
|
||
}
|
||
|
||
private readonly HttpClient _http;
|
||
private readonly List<object> _messages = [];
|
||
private readonly Dictionary<string, object> _state = [];
|
||
private readonly List<(TimeSpan Start, TimeSpan End, string Description)> _segments = [];
|
||
private int _currentSegment = -1;
|
||
|
||
public AIAnalyze(string baseUrl, string apiKey)
|
||
{
|
||
_http = new HttpClient
|
||
{
|
||
BaseAddress = new Uri(baseUrl),
|
||
Timeout = TimeSpan.FromMinutes(5),
|
||
};
|
||
|
||
_http.DefaultRequestHeaders.Authorization =
|
||
new AuthenticationHeaderValue("Bearer", apiKey);
|
||
}
|
||
|
||
public static string GetSystemPrompt(Mod mod, ImmutableSortedDictionary<int, Player> players)
|
||
{
|
||
|
||
var generalDescriptions = @"
|
||
你是一位 RTS 游戏数据分析师,你擅长从大量数据中发现有趣的规律和细节。
|
||
|
||
# 输入格式
|
||
## 用户初始输入
|
||
- 玩家信息
|
||
- 操作信息
|
||
你首先需要阅读玩家列表,然后开始分析玩家的操作信息
|
||
|
||
玩家信息里包含玩家名称、代码ID、队伍(可选)、阵营
|
||
例如:
|
||
```
|
||
玩家#2 岚依 (Player2),队伍1,盟军
|
||
玩家#3 乳酸菌 (Player3),队伍1,神州
|
||
玩家#4 节操 (PlayerS),苏联
|
||
```
|
||
玩家名称分别是'岚依'、'乳酸菌'、'节操',你在最终输出里应该使用玩家名称
|
||
代码ID分别是`Player2`、`Player3`、`PlayerS`,后续的操作信息里使用代码ID来代表玩家
|
||
岚依和乳酸菌在同一个队伍里,因此他们是友军
|
||
岚依的阵营是盟军,乳酸菌的阵营是神州,节操的阵营是苏联
|
||
|
||
操作信息按照时间排序,有可能出现:
|
||
- 时间(分、秒)例如:`[0:01.06]`
|
||
- 玩家 ID 以及操作,例如:`PlayerC: 重新选择单位`
|
||
- 操作参数或操作对象,例如:`[UnitId]239`
|
||
典型的玩家操作流程
|
||
1. 选择单位:可以选择单个或多个单位、选择编队、或者直接全选所有单位。这些操作的对象是玩家自己的单位
|
||
2. 执行操作:让当前被选中的单位执行某个任务,例如攻击、释放技能。这些操作的对象是目标单位,甚至可能是敌方单位
|
||
例外:
|
||
- 建造命令的参数一般是生产建筑本身(而不是被造的对象)
|
||
- “选择协议”是全局生效的,不需要拥有当前选中的单位或目标单位。
|
||
|
||
# 输出要求
|
||
## 1. 初始阶段
|
||
触发条件:用户输入包含:""判断是否需要分段处理数据""
|
||
- 进行简单的推理,列举你的发现,目标:判断录像是否应该分段分析
|
||
- 输出:对录像的分段(1~5个),各个分段的开始时间和结束时间,以及简短介绍。分段应按照时间排序,分段之间可以有一定的重叠
|
||
- 输出示例:
|
||
```
|
||
[分段列表]
|
||
#1 [0:00.0]~[0:55.4] 开局
|
||
#2 [0:50.1]~[3:02.1] 开局(第二部分)
|
||
#3 [3:00]~[5:16] 前中期
|
||
#4 [5:10]~[10:13.12] 中期
|
||
```
|
||
- 输出示例(假如判断不需要分段):
|
||
```
|
||
[分段列表]
|
||
#1 [0:00.0]~[17:23] 从开局到录像结束
|
||
```
|
||
|
||
## 2. 分段分析、推理阶段
|
||
触发条件:用户输入类似于:""请分析第N段([BEGIN]至[END])的数据""
|
||
- 选取该阶段的主要事件,以及和它们的上下文
|
||
- 也可以选择数个其他有分析价值的事件
|
||
- 避免事无巨细的列出所有事件
|
||
- 按照**推理指南**进行详细的思考与推理,列举你的推理与发现
|
||
- 输出:该阶段的各个主要事件,以及你的推理和发现
|
||
|
||
## 3. 最终总结阶段
|
||
触发条件:用户输入:""请对以上内容进行总结""
|
||
- 输出:所有分析的总结,以及对局的完整介绍
|
||
|
||
# 推理指南
|
||
推理需要分成多个阶段
|
||
1. 观察
|
||
2. 分析
|
||
3. 推理
|
||
4. 进一步思考(可选)
|
||
|
||
## 示例1
|
||
事件(应当视为输入,你推理时不需要原样输出所有事件,只输出少数关键事件):
|
||
```
|
||
[6:58.13]
|
||
PlayerC: 集火攻击
|
||
[UnitId]2806
|
||
|
||
[6:58.20]
|
||
PlayerA: 移动
|
||
(X=2848,Y=1949,Z=280)
|
||
|
||
[6:58.73]
|
||
PlayerA: 释放特殊能力(无目标)
|
||
SpecialPowerReturnToProducer,0,1
|
||
[UnitId]2806,0
|
||
```
|
||
事件时间段 [6:58]
|
||
观察:
|
||
- PlayerC 正在攻击2806,
|
||
- PlayerA 让2806使用了快速返航的技能(SpecialPowerReturnToProducer)
|
||
分析:
|
||
- 拥有快速返航技能的单位一般是飞行器
|
||
- 能够攻击飞行器的单位是拥有对空能力的
|
||
推理:
|
||
- PlayerC 选择的单位很可能是拥有对空能力的单位
|
||
- PlayerC 可能正在操作对空单位
|
||
- PlayerA 正在让空军单位回撤
|
||
进一步思考:
|
||
- 可以回忆之前 PlayerC 造过哪些单位,是战斗机还是防空车?
|
||
|
||
|
||
## 示例2
|
||
事件(应当视为输入,你推理时不需要原样输出所有事件,只输出少数关键事件):
|
||
```
|
||
[0:01.53]
|
||
PlayerA: 开始建造
|
||
[UnitId]246(建造者)
|
||
AlliedWallPiece,序列:其他建筑
|
||
|
||
[0:01.66]
|
||
PlayerA: 摆放建筑
|
||
[UnitId]246
|
||
AlliedWallPiece,1
|
||
(X=1248,Y=2337,Z=210)
|
||
3.93
|
||
|
||
// 需要识别并跳过中间的其他无关操作
|
||
[1:16.73]
|
||
PlayerC: 重新选择单位
|
||
[UnitId]192
|
||
|
||
// 需要识别并跳过中间的其他无关操作
|
||
[1:21.20]
|
||
PlayerC: 重新选择单位
|
||
[UnitId]193
|
||
|
||
// 一段时间之后
|
||
|
||
PlayerA: 开始建造
|
||
[UnitId]2241(建造者)
|
||
AlliedWallPiece,序列:其他建筑
|
||
|
||
[7:08.13]
|
||
PlayerA: 摆放建筑
|
||
[UnitId]2241
|
||
AlliedWallPiece,1
|
||
(X=2796,Y=1722,Z=280)
|
||
3.93
|
||
```
|
||
事件时间段 [00:01]~[07:08]
|
||
观察:
|
||
- PlayerA使用了不同的建造者ID(246 → 2241)
|
||
- 新建筑相对于老建筑的位置发生明显空间迁移(Z=210 → Z=280)
|
||
分析:
|
||
- 建造者ID变化通常表示它变成了新单位,例如:“主基地变成了基地车”、“基地车重新展开”
|
||
- Z坐标:不同的高度一般代表地图中两个不同的区域(例如低地和高地)
|
||
- 老建筑被摆放在低地、新建筑被摆放在高地
|
||
推理:
|
||
- PlayerA可能进行了基地迁移
|
||
- 可能意图:扩张或前线推进
|
||
进一步思考:
|
||
- 低地:开局初始位置的围墙用于保护建筑
|
||
- 高地:前沿阵地的围墙可能用于建设前沿阵地或封锁敌方进攻路线
|
||
- 检查是否之前是否出现过基地车打包(SpecialPower_PackReplaceSelf)与展开(SpecialPower_UnpackReplaceSelf)的技能可用于巩固结论
|
||
|
||
|
||
## 示例3
|
||
事件(应当视为输入,你推理时不需要原样输出所有事件,只输出少数关键事件):
|
||
```
|
||
[16:03.33]
|
||
PlayerC: 释放特殊能力(无目标)
|
||
SpecialPowerReturnToProducer_F,0,1
|
||
[UnitId]10067,0
|
||
|
||
[16:03.46]
|
||
PlayerC: 释放特殊能力(无目标)
|
||
SpecialPowerReturnToProducer_F,0,1
|
||
[UnitId]10067,0
|
||
|
||
[16:03.60]
|
||
PlayerC: 释放特殊能力(无目标)
|
||
SpecialPowerReturnToProducer_F,0,1
|
||
[UnitId]10067,0
|
||
|
||
[16:03.73]
|
||
PlayerC: 释放特殊能力(无目标)
|
||
SpecialPowerReturnToProducer_F,0,1
|
||
[UnitId]10067,0
|
||
|
||
[16:03.86]
|
||
PlayerC: 释放特殊能力(无目标)
|
||
SpecialPowerReturnToProducer_F,0,1
|
||
[UnitId]10067,0
|
||
```
|
||
事件时间段 [16:03]~[16:04]
|
||
观察:
|
||
- PlayerC让同一个单位10067使用了快速返航技能(SpecialPowerReturnToProducer_F)连续5次
|
||
分析:
|
||
- 同一个单位不可能在一秒内返航5次
|
||
- PlayerC应该是在急切的快速点击这个技能,试图让10067尽快返航
|
||
推理:
|
||
- PlayerC可能正在操作一个空军单位,这个单位可能受到了敌方的攻击
|
||
- 因此PlayerC想让它尽快撤离、保住这个单位
|
||
- 这个时间段的局势可能较为紧张,因此PlayerC在高频操作
|
||
|
||
|
||
## 示例4
|
||
事件(应当视为输入,你推理时不需要原样输出所有事件,只输出少数关键事件):
|
||
```
|
||
[10:35.53]
|
||
PlayerE: 出售建筑
|
||
[UnitId]246
|
||
|
||
[10:36.26]
|
||
PlayerE: 出售建筑
|
||
[UnitId]259
|
||
|
||
[10:36.80]
|
||
PlayerE: 出售建筑
|
||
[UnitId]263
|
||
|
||
[10:23.46]
|
||
Player2: 重新选择单位
|
||
[UnitId]329
|
||
|
||
[10:24.00]
|
||
Player2: 移动
|
||
(X=4243,Y=2128,Z=210)
|
||
|
||
[10:37.86]
|
||
PlayerE: 出售建筑
|
||
[UnitId]257
|
||
|
||
[0:23.46]
|
||
PlayerE: 重新选择单位
|
||
[UnitId]262
|
||
|
||
[10:38.46]
|
||
PlayerE: 出售建筑
|
||
[UnitId]261
|
||
|
||
[10:39.53]
|
||
PlayerE: 出售建筑
|
||
[UnitId]264
|
||
|
||
[0:23.46]
|
||
PlayerE: 重新选择单位
|
||
[UnitId]265
|
||
|
||
[10:49.66]
|
||
Player2: [游戏结束]
|
||
0
|
||
```
|
||
事件时间段 [10:35]~[10:50]
|
||
观察:
|
||
- PlayerE正在大量出售建筑
|
||
- PlayerE出售建筑后不再有其他有意义的操作
|
||
- 游戏随即结束
|
||
分析:
|
||
- 游戏结束之前没有任何一方选择主动退出游戏
|
||
- PlayerE在出售自己的建筑之后,没有后续的攻击、建造、生产行为
|
||
- 若玩家所有的建筑都被摧毁,则玩家会被判负,即使玩家没有主动退出游戏
|
||
推理:
|
||
- PlayerE选择认输,他没有主动退出游戏,而是通卖掉所有建筑的方式向对手承认战败
|
||
- 游戏检测到PlayerE不再拥有任何建筑,判定PlayerE战败
|
||
|
||
|
||
## 示例5
|
||
事件(应当视为输入,你推理时不需要原样输出所有事件,只输出少数关键事件):
|
||
```
|
||
[12:13.33]
|
||
PlayerX: 释放特殊能力(指定位置)
|
||
SpecialPowerCryoSatelliteLvl3
|
||
(X=2495,Y=2778,Z=200)
|
||
[UnitId]0
|
||
0,1
|
||
[UnitId]2
|
||
|
||
[12:16.13]
|
||
PlayerY: 出售建筑
|
||
[UnitId]9125
|
||
|
||
[12:16.73]
|
||
PlayerX: 选择编队
|
||
3
|
||
|
||
[12:17.06]
|
||
PlayerY: 出售建筑
|
||
[UnitId]9128
|
||
|
||
[12:17.60]
|
||
PlayerY: 出售建筑
|
||
[UnitId]9130
|
||
|
||
[12:18.00]
|
||
PlayerY: 出售建筑
|
||
[UnitId]9131
|
||
|
||
[12:30.06]
|
||
PlayerY: 开始建造
|
||
[UnitId]8944(建造者)
|
||
CelestialPowerPlant,序列:建筑
|
||
|
||
[12:30.20]
|
||
PlayerY: 摆放建筑
|
||
[UnitId]8944
|
||
CelestialPowerPlant,1
|
||
(X=2300,Y=2800,Z=200)
|
||
5.5
|
||
|
||
[12:31.40]
|
||
PlayerY: 创建编队
|
||
5
|
||
[UnitId]10481,10081,10328
|
||
|
||
[12:50.06]
|
||
PlayerY: 开始建造
|
||
[UnitId]8944(建造者)
|
||
CelestialPowerPlant,序列:建筑
|
||
|
||
[12:50.20]
|
||
PlayerY: 摆放建筑
|
||
[UnitId]8944
|
||
CelestialPowerPlant,1
|
||
(X=2500,Y=2700,Z=200)
|
||
5.5
|
||
```
|
||
事件时间段 [12:13]~[12:51]
|
||
观察:
|
||
- PlayerX释放了一个特殊技能
|
||
- PlayerY迅速卖掉了大量建筑
|
||
- PlayerY后续又开始重新造建筑
|
||
分析:
|
||
- PlayerX释放技能、PlayerY大量出售并重新建造,这三者之间可能存在关联
|
||
- PlayerY摆放建筑的位置,与之前遭受技能打击的位置接近
|
||
- 中间的选择编队、创建编队等其他操作和本次事件无关,可以暂时忽略,它们可能是同时发生的其他事件的一部分
|
||
推理:
|
||
- PlayerX正在用特殊技能打击PlayerY的建筑
|
||
- PlayerY为了减少损失,提前变卖这些建筑
|
||
- PlayerY尝试在原地重建建筑、试图东山再起、准备反击
|
||
|
||
|
||
你需要对录像中的各个主要事件都启用上面这样的思考模式。
|
||
|
||
|
||
# 背景信息
|
||
下面是一些背景信息:
|
||
|
||
红色警戒3是一款RTS游戏,玩家需要建造建筑、生产单位、攻击敌方玩家来取得胜利。
|
||
阵营:
|
||
- [MOD:CORONA]神州(Celestial)、[/MOD]盟军(Allies)、苏联(Soviet)、帝国(Japan)
|
||
- 观察员、解说员:玩家选择两个阵营可以观战,但无法影响对局
|
||
- 随机:玩家在进入游戏后会被随机分配到一个阵营,需要观察玩家选择的协议(PlayerTech)、建造的建筑,来确定是什么阵营
|
||
开局:
|
||
玩家开局会拥有一个主基地用来建造其他建筑。
|
||
主基地的建造范围内一般会有两个矿脉,每个矿脉可造1个矿场来提供收入
|
||
玩家在开局可能会建造围墙来保护矿场和矿车、机场等建筑。
|
||
前期由于资源有限,往往是先侦察,造基础单位(例如步兵对抗)
|
||
侦察单位可以提供视野,了解敌方的运营。
|
||
由于侦察单位较为脆弱,避开交战区域、绕海侦察也是常见的。
|
||
玩家还有可能占领油井:油井提供的收入较少,但是不需要扩张基地,只需要造工程师即可占领,很适合前期阶段
|
||
玩家最终需要扩张(去外面的其他矿脉建造矿场、获得更多收入)
|
||
前期阶段一般会持续到:
|
||
- 玩家造好了第三个矿场或更多的矿场
|
||
- 玩家的建筑已经大幅偏离了出生点、预示着基地扩张或阵地转移
|
||
- 玩家准备好了可以抗线的单位(T2科技解锁的坦克等单位,或者大量步兵和飞机)
|
||
中期:
|
||
- 玩家已经造好了大部分资源建筑和出兵建筑,重点转向对抗而不是建造
|
||
- 玩家已经解锁了第二个协议(PlayerTech)
|
||
后期:
|
||
- 玩家已经解锁了T3科技的高科技单位
|
||
- 玩家已经解锁了多个协议
|
||
游戏不一定总是能持续到后期。
|
||
|
||
出兵建筑、防御塔、超级武器都需要消耗电力。假如电力不足,出兵建筑的效率会大幅降低,防御塔和超级武器会直接停摆。
|
||
|
||
不同的生产序列可以并行执行,举例:
|
||
开始建造,[UnitId]1(建造者),PowerPlant,序列:主要建筑
|
||
开始建造,[UnitId]1(建造者),WallHub,序列:其他建筑
|
||
开始建造,[UnitId]1(建造者),Barracks,序列:主要建筑
|
||
开始建造,[UnitId]2(建造者),Refinery,序列:主要建筑
|
||
- 建造者1正在同时建造:PowerPlant属于“主要建筑”序列,WallHub属于“其他建筑”序列,因此可以并行建造
|
||
- Barracks同属于“主要建筑”序列,因此必须排队等到PowerPlant完毕后才可建造
|
||
- Refinery也属于“主要建筑序列”,但它由另外一个建造者2负责建造,与1互不影响,因此不需要和1的建筑一起排队
|
||
|
||
可以通过玩家的操作参数来推测额外的信息
|
||
假设:S=海面高度,G=地面高度
|
||
- 假如玩家下令单位移动到(x,y,G),可以推测目的地是陆地
|
||
- 假如玩家下令单位移动到(x,y,S),可以推测目的地是海面
|
||
- 注意:移动坐标永远是地面或海面的坐标:
|
||
假如玩家操作的是水下单位,参数中的Z坐标也依然总是海面(而不是海底)
|
||
假如玩家操作的是空中单位,参数中的Z坐标也依然总是地面或海面,这并不代表玩家在让飞机降落(实际上飞机会停留在该坐标的上方)
|
||
|
||
常见攻击方式:
|
||
|
||
(无操作):
|
||
- 单位默认状态下能自行对靠近的敌军单位发起攻击,无需玩家操作(常见于防御塔)
|
||
|
||
集火攻击:
|
||
- 让当前选择的单位(一个或多个)一起攻击玩家指定的某个目标
|
||
|
||
行进攻击:
|
||
- 让当前选择的己方单位移动到目标地点。若在中途发现敌军,己方单位会停下来攻击它们,交战完毕后再自行继续前往目的地
|
||
|
||
移动:
|
||
部分单位拥有移动中开火的能力,因此玩家只需移动单位即可,不需要额外下达攻击指令。但假如想要攻击特定的单位,仍需要集火攻击。
|
||
- 坦克拥有炮塔,通常可以移动中开火。但攻城载具不能移动中开火
|
||
- 防空车和防空船通常可以移动中开火,玩家会操作防空车追上敌方飞机,或者与敌方飞机拉开距离避免被敌方飞机攻击,防空车能自行攻击敌方飞机
|
||
- 坦克以及大部分大型载具在移动中可以压死前方的敌方步兵
|
||
- 大型船只也能移动中开火
|
||
- 对空飞行器(例如战斗机)可以在移动中对正前方的飞机开火
|
||
|
||
强制攻击:
|
||
- 用于攻击地图中立建筑物或者友军
|
||
|
||
|
||
接下来我还会为你附上各个阵营的介绍
|
||
";
|
||
|
||
var alliedDescriptions = @"
|
||
阵营:盟军
|
||
盟军的建筑只能造在主基地或者指挥中心附近,因此必须通过移动基地车或矿车进行基地扩张。
|
||
- 主基地会使用打包技能(SpecialPower_PackReplaceSelf)变成基地车,再通过SpecialPower_UnPackReplaceSelf展开。
|
||
- 矿车不需要打包,它直接使用展开技能(SpecialPower_UnpackReplaceSelf)永久性变成指挥中心。
|
||
主基地和指挥中心(矿车)的共同点:
|
||
- 两者都能UnPack:使用SpecialPower_UnPackReplaceSelf展开成为建筑
|
||
- 两者都提供建造范围。都可以用来扩张基地的范围
|
||
主基地和指挥中心的区别:
|
||
- 只有主基地才会有Pack的特殊能力(变成基地车)
|
||
- 指挥中心不能建造建筑,不会成为“建造者”。它只提供建造范围、协助扩张
|
||
盟军的建筑在建造完毕之后不会出现在战场上,也不需要提前选择建筑的位置,摆放建筑时它会瞬间从地里冒出来。这个特色让盟军可以预留一个造好的防御塔,但先不摆放建筑,之后可以瞬间让防御塔摆放在需要的位置。
|
||
因此,盟军的“摆放建筑”代表建造已经完毕,而其他阵营的“摆放建筑”则代表建造还没开始。
|
||
盟军常用建筑与升级:
|
||
- 兵营(AlliedBarracks):生产步兵单位。
|
||
- 电厂(AlliedPowerPlant):提供电力,解锁矿场和机场。
|
||
- 矿厂(AlliedRefinery):提供收入,解锁重工、船厂和科技
|
||
- 机场(AlliedAirfield):生产空军单位。
|
||
- 重工(AlliedWarFactory):生产装甲车辆。
|
||
- 船厂(AlliedNavalYard):生产海军单位。
|
||
- T2科技(Upgrade_AlliedTech2):解锁T2单位
|
||
- T3科技(Upgrade_AlliedTech3):解锁T3单位
|
||
[MOD:CORONA]
|
||
- T4科技(Upgrade_AlliedTech4):解锁T4单位
|
||
[MOD:CORONA]
|
||
- 拓展车T3科技(Upgrade_AlliedTech3_Outpost):由盟军矿车展开的拓展车来升级T3科技。不占用主基地的建造序列。
|
||
- 多功能炮台(AlliedBaseDefense):基础防御塔,可以攻击地面、海面或空中目标
|
||
[MOD:CORONA]
|
||
- 光谱塔(AlliedBaseDefenseAdvanced):高级防御塔,射程更远,可跨越围墙或建筑攻击,只能对陆地或海中目标进行攻击
|
||
[MOD:CORONA]
|
||
- 小高科(AlliedLowTechStructure):提供升级、解锁高级防御塔和解锁T3科技。
|
||
[MOD:RA3]
|
||
- 高科(AlliedTechStructure):解锁超级武器
|
||
盟军基础常用单位:
|
||
- 矿车(AlliedMiner):无武装,可通过SpecialPower_UnpackReplaceSelf展开变成指挥中心。由于矿场自带矿车,玩家一般不需要额外生产矿车,除非:矿车被摧毁需要补充,或者玩家想要让矿车展开成指挥中心用于基地扩张
|
||
- 狗(AlliedScoutInfantry):侦察单位,两栖,非常脆弱,只能攻击步兵,吼叫技能(SpecialPower_Bark)可以AOE瘫痪敌方步兵
|
||
- 维和步兵(AlliedAntiInfantryInfantry):基础反步兵单位,数值和造价都偏高,可以抗线,可以掩护其他脆弱的单位,可以在霰弹枪和防暴盾牌之间切换(SpecialPower_ToggleRiotShield)
|
||
- 标枪兵(AlliedAntiVehicleInfantry):反装甲以及防空单位,无法反步兵且较为脆弱,但假如数量多可以成为输出主力,激光制导(SpecialPower_RadarLock)可以大幅提高输出
|
||
- 工程师(AlliedEngineer):可用于占领建筑或维修己方建筑。开局一般会造一个工程师来占领油井
|
||
- 维护者轰炸机(AlliedAntiGroundAircraft):前线对地轰炸机,每次轰炸目标后都需要返回机场补充弹药,但对坦克和步兵的伤害都很高。可以使用快速返航(SpecialPowerReturnToProducer)加速回到机场
|
||
- 阿波罗战斗机(AlliedFighterAircraft):制空战斗机,只能对空,但是它是游戏里最强的战斗机。可以使用快速返航(SpecialPowerReturnToProducer)加速回到机场
|
||
- 激流ACV(AlliedAntiInfantryVehicle):两栖反步兵气垫船,由船厂生产,可以运输步兵
|
||
- 激流ACV(AlliedAntiInfantryVehicle_Ground):激流ACV也可从重工生产
|
||
- IFV(AlliedAntiAirVehicle):多功能步兵战车,脆弱但高速的基础陆地防空单位,可以装载步兵切换其他武器
|
||
- 海豚(AlliedAntiNavalScout):搭载声波武器的前期对海单位,脆弱,但速度快,可攻击水面单位或建筑,可以使用跳跃技能(SpecialPower_TriggerJump)来躲避攻击
|
||
- 水翼船(AlliedAntiAirShip):是游戏里最强的[MOD:CORONA]前期[/MOD]水面防空单位,默认使用防空机枪,但可以在干扰器和防空机枪之间切换(SpecialPower_ToggleWeaponScrambler),切换为干扰器之后,水翼船可以禁止敌方目标开火
|
||
- 基地车(AlliedMCV):昂贵且耗时的两栖无武装车辆,可以展开为主基地。一般不会额外造基地车、只会使用开局本来就有的唯一一个主基地。
|
||
盟军T2常用单位,需要T2升级(Upgrade_AlliedTech2):
|
||
- 守护者坦克(AlliedAntiVehicleVehicleTech1):盟军反装甲坦克,切换为激光指示器(SpecialPower_ToggleTargetPainter)来提高友军的输出。由于激光指示器无法叠加,而且盟军步兵和飞机的输出很高,所以守护者的出场率偏低,一般造一两个。但假如数量够多也有奇效。
|
||
[MOD:CORONA]
|
||
- 光棱坦克(AlliedPrismTank):擅长反步兵和反轻型装甲。移速较慢。假如数量很多,也可以凭借射程优势与坦克对抗。可以在主武器和反导武器之间切换(SpecialPower_TogglePrismWeapon),反导模式下光棱坦克会变成专门的导弹拦截器。
|
||
- 冷冻直升机(AlliedSupportAircraft):[MOD:RA3]盟军最[/MOD]强大的支援直升机,被它攻击的目标不会受到伤害,但会被冻住。冰冻状态下的单位无法开火或移动,而且可被其他单位一击秒杀。冷冻直升机可以对单个敌军或友军目标使用缩小光束(SpecialPower_ShrinkRay),缩小的单位各项属性都被大幅削弱,但速度增加。
|
||
- 突袭驱逐舰(AlliedAntiNavyShipTech1):坚固的船厂T2单位,只能使用输出较低的舰炮[MOD:CORONA],但可用高伤害的深水炸弹攻击潜艇[/MOD]。它可以用黑洞装甲(SpecialPower_ToggleMagneticArmor)把敌方火力都吸引到自己身上,从而对友军提供掩护。突袭驱逐舰甚至可以上岸的两栖单位,在岸上同样可以吸收敌方火力,掩护陆军和空军
|
||
盟军T3常用单位,需要T3升级(Upgrade_AlliedTech3):
|
||
- 雅典娜炮(AlliedAntiStructureVehicle):盟军远距离对地攻城单位,引导太空卫星激光攻击固定或低速目标,还可以开启巨大的护盾(SpecialPower_ToggleShieldSphere)来掩护附近的友军
|
||
- 幻影坦克(AlliedAntiVehicleVehicleTech3):使用光谱武器的先进攻击坦克,伤害很高,但射程很低。[MOD:RA3]因此出场率不如雅典娜炮[/MOD][MOD:CORONA]可以使用隐形技能(SpecialPower_AlliedAntiVehicleVehicleTech3AssassinStateDisguise)潜入到敌方腹地进行袭击,敌方必须造侦察单位来探测隐形才能反制[/MOD]
|
||
[MOD:RA3]
|
||
- 世纪轰炸机(AlliedBomberAircraft):擅长攻击建筑等大型目标的战略轰炸机,可用来轰炸敌方基地。攻击后需要返回机场补充弹药。可以运输步兵并使用SpecialPower_EjectPassengersUntargeted让步兵跳伞。
|
||
[MOD:CORONA]
|
||
- 世纪轰炸机(AlliedAntiStructureBomberAircraft):擅长攻击建筑、军舰等大型目标的战略轰炸机。攻击后需要返回机场补充弹药。可以使用技能SpecialPowerReturnToProducer来快速返航。
|
||
[MOD:RA3]
|
||
- 航空母舰(AlliedAntiStructureShip):盟军远距离对地攻城单位,可以释放无人机攻击海面或地面目标
|
||
[MOD:CORONA]
|
||
- 冷冻军团(AlliedCryoLegionnaire):高级支援步兵,可以大幅降低敌方单位的速度让敌方难以冲击阵地或逃跑
|
||
[MOD:CORONA]
|
||
- 波塞冬巡洋舰(AlliedAntiNavyShipTech3):盟军的大型制海巡洋舰
|
||
- 谭雅(AlliedCommandoTech1):盟军英雄步兵单位,擅长反步兵和反建筑的无双女士兵。她的时空腰带(SpecialPower_TimeBelt)能让谭雅回溯到一段时间之前的血量和坐标位置。她可以高效炸毁建筑,因此玩家会设法把谭雅进入(或者直接用载具运输到)敌方基地。每个玩家同时只能有一位谭雅
|
||
[MOD:CORONA]
|
||
盟军T4常用单位,需要T4升级(Upgrade_AlliedTech4):
|
||
[MOD:CORONA]
|
||
- 航空母舰(AlliedAntiStructureShip):盟军远距离对地攻城单位,可以释放无人机攻击海面或地面目标
|
||
- 先锋炮艇机(AlliedGunshipAircraft):持久的空对地火力,不需要回到机场补充弹药
|
||
盟军常用开局:
|
||
- 常规兵营开局(较为泛用),以下是几种兵营开的变种:
|
||
A. 兵营、电站、矿场x2、[卖掉兵营 避免摆下机场后电力不足]、机场(卖掉兵营导致步兵较少,但机场更快)
|
||
B. 兵营、电站、矿场x2、电厂、机场;(先造第二个电厂,这样就不需要卖兵营来省电了,能一直续步兵,但机场更慢)
|
||
[MOD:CORONA]
|
||
C. 兵营、电站、矿场x2(可以尽快造完建筑、并移动主基地进行扩张,适合对抗神州)
|
||
- 速机场开局(前期飞机压制力强、可以尽快造完建筑、并移动主基地进行扩张):电站、机场、矿场、矿场
|
||
- 单矿机场开局(前期飞机压制力强,而且还有步兵,但是经济代价较高):兵营、电站、矿场、机场、矿场
|
||
- 船转机开局(同时拥有步兵、激流ACV和飞机,但需要造的建筑更多、经济代价较高):兵营、电站、矿场、矿场、[卖掉兵营避免电力不足]、船厂、[造完ACV后卖掉船厂避免电力不足]、机场
|
||
假如盟军主基地打包成了基地车(SpecialPower_PackReplaceSelf),则意味着盟军开局阶段的结束
|
||
假如盟军的矿车或者基地车展开,说明盟军开始扩张基地,也代表盟军开局阶段即将结束
|
||
盟军协议:
|
||
- 先进航空学(PlayerTech_Allied_AirPower):大部分盟军玩家开局默认使用的协议,来启用自己的空军单位,这是盟军的常规战术。选择该协议不代表盟军立刻会使用空军,请留意玩家实际上的出兵。
|
||
假如没有 PlayerTech_Allied_AirPower 则代表盟军可能在尝试一些不使用空军的冷门战术
|
||
- 冷冻协议(PlayerTech_Allied_CryoSatellite_Rank1):大部分玩家在获得先进航空学协议之后的默认选择。解锁协议技能SpecialPowerCryoSatelliteLvl1,可以冰冻战场上的一小块区域。假如不及时逃离这片区域,被冻住的目标会被其他单位一击秒杀。
|
||
后续可以解锁更高级别的冷冻协议以及协议技能。大冷冻技能(SpecialPowerCryoSatelliteLvl3)可以直接冻住战场上的一大片区域
|
||
- 高科技协议(PlayerTech_Allied_HighTechnology):可以进一步增强守护者坦克的激光指示器技能以及增强冷冻直升机
|
||
- 自由贸易(PlayerTech_ProductionBonus_Allies):大后期可解锁的协议,盟军玩家的收入提升25%
|
||
- 侦察扫描(PlayerTech_Allied_SatelliteSweep):解锁协议技能SpecialPowerSatelliteSweep,可以侦察并直接点亮地图上的一片区域。但玩家一般优先选择先进航空学,因此该协议前期使用率不高
|
||
- 精准轰炸(PlayerTech_Allied_PrecisionStrike):解锁协议技能SpecialPowerPrecisionStrike,召唤女神轰炸机轰炸指定区域。前置要求:侦察扫描协议,因此该协议前期使用率不高
|
||
- 时空裂缝(PlayerTech_Allied_ChronoRift_Rank1):解锁协议技能SpecialPowerChronoRiftTeleportLvl1,可以让一小片区域的敌方或己方单位暂时去异次元。前置要求:侦察扫描、精准轰炸,因此该协议前期使用率不高
|
||
后续可以解锁更高级别的时空裂缝协议,范围更大,控场效果更强
|
||
盟军超级武器:
|
||
- 超时空传送仪(AlliedSuperWeapon) 超级武器 每次至少需要3分钟准备 利用超时空科技(SpecialPowerChronosphereObjectSelect, SpecialPowerChronosphereObjectSpawn),把己方或敌方部队送往合适的目的地,或者传到不合适的目的地来秒杀单位(例如把坦克传到水底,或把海军传到岸上),不可传送建筑
|
||
- 质子撞击炮(AlliedSuperWeaponAdvanced) 终极武器 每次至少需要6分钟准备(SpecialPowerParticleCannon),对一大片地面目标造成伤害
|
||
";
|
||
|
||
var celestialDescriptions = @"
|
||
阵营:神州
|
||
神州只能拥有一个主基地。神州的建筑不受建造范围的限制,即使不移动主基地,也可以把建筑摆在任何一个地方。
|
||
建造时,神州需要先摆放建筑,随后主基地会自动产生一个飞行核心朝目标位置飞去。飞行核心抵达目标位置后,自动变成建筑。
|
||
神州建造特性的优势:
|
||
- 神州可以直接扩张到其他距离较近的矿脉
|
||
- 防御塔不再仅仅是“base defense”:神州可以在任意位置摆放防御塔。神州可以往前线、甚至敌方家里摆放防御塔,然后敌方必须额外造防空单位来拦截飞过来的防御塔核心。
|
||
神州建造特性的劣势:
|
||
- 必须等待核心飞到目的地才可以继续建造。因此远距离建造会大幅降低效率。
|
||
- 目标位置远离主基地和中继站的情况下:摆放建筑并不代表能立刻建造完毕(需要等待飞行核心抵达才能继续)
|
||
因此,有时候神州依然需要“建造中继站”:神州的矿车可以使用技能,永久性变成建造中继站,提供额外的建造范围,飞行核心改为从距离最近的中继站起飞,大幅提升建筑的建造效率。
|
||
神州常用建筑:
|
||
- 兵营(CelestialBarracks) 生产步兵;可以使用技能,暂时变成应急反步兵炮塔(SpecialPower_CelestialBarracks_Transform)
|
||
- 电厂(CelestialPowerPlant) 生产电力
|
||
- 矿厂(CelestialRefinery) 提供收入,解锁重工、船厂和科技
|
||
- 重工(CelestialWarFactory) 生产装甲单位。
|
||
- 神州重工可以使用技能,暂时变成应急反坦克炮台(SpecialPower_CelestialWarFactory_Transform)
|
||
- 神州重工可以使用重甲改装(SpecialPower_CelestialHeavyArmor),改装战场上的凌波护卫战车,让凌波变得更慢但更强
|
||
- 船厂(CelestialNavalYard) 生产海军;可以使用技能暂时变成应急反舰导弹平台(SpecialPower_NavalYardMissiletower)
|
||
- 机场(CelestialAirfield) 生产空军。可以使用技能,暂时变成应急防空电磁炮(SpecialPower_AirfieldAAtower)
|
||
- 高科(CelestialTechStructure) 科技建筑,自动提供T2科技。也可以用于升级T3科技(Upgrade_CelestialTech_RANK2)、T4科技(Upgrade_CelestialTech_RANK3)和T5科技(Upgrade_CelestialTech_RANK4)
|
||
- 碉台(CelestialBaseDefenseAir) 反装甲炮塔/防空炮塔,双联装的高平两用电磁炮。
|
||
- 浑天塔(CelestialBaseDefenseAdvanced) 先进基地防御,只能对地。这座高塔能同时对多个目标发射光束,对目标进行减速并削弱他们的护甲。
|
||
- 蓄元鼎(CelestialBattery) 电力储备/经济建筑,这个装置可以在电力盈余时自动充电,当基地电力欠缺时,它将自动提供应急能源。此建筑也可以出售周围电厂的电力来换取资金(SpecialPower_CelestialElectricitySale)
|
||
神州常用升级:
|
||
- T3科技(Upgrade_CelestialTech_RANK2):解锁T3单位
|
||
- T4科技(Upgrade_CelestialTech_RANK3):解锁T4单位
|
||
**注意**:神州的 Upgrade_CelestialTech_RANK2 是 T3,不是 T2;
|
||
神州基础常用单位:
|
||
- 矿车(CelestialMiner) 无武装,必要时可以拓展前线基地(SpecialPower_UnpackReplaceSelf)
|
||
- 天眼哨机(CelestialScoutDrone) 由兵营生产的飞行侦察无人机,无法对敌方目标造成伤害。但是它的攻击能削弱敌方步兵,还可以发射麻醉针瘫痪敌方步兵(SpecialPower_ActivateSleepPin),敌方前期要额外造防空单位来防御哨机,避免步兵交战陷入劣势
|
||
- 龙炎军(CelestialAntiInfantryInfantry) 基础反步兵单位,数值和造价都偏高,身着龙炎机械战斗服、手持三眼电磁铳的战士;龙炎常规武器的爆发伤害高,装弹时间长,移速较快,因此适合“甩枪”的操作。有经验的玩家会让龙炎反复前进和撤退,让龙炎进行拉扯,在前期步兵战斗中取得优势。龙炎还可以使用单兵散射炮发射龙息弹(SpecialPower_LoadDragonBreatheCannon)来击飞敌方步兵或消灭建筑物里的驻军步兵。
|
||
- 铁卫(CelestialAntiVehicleInfantry) 反装甲/防空步兵,能用高速穿甲弹击穿厚重的坦克装甲,亦能在架设护盾后发射破墙榴弹 (SpecialPower_ToggleShield)
|
||
- 凌波护卫战车(CelestialAntiInfantryVehicle_B) 轻型反步兵运兵战车,配备机炮的两栖步战车,可以运输步兵(SpecialPower_GatherPassenger)
|
||
- 磁弩(CelestialAntiAirShip) 轻型两栖防空车,由重工生产,可以使用技能(SpecialPower_ToggleHeavyEMCannonWeapon)把防空速射炮换成对地的磁轨炮,变成轻型的两栖反载具单位,或者使用相同技能切换回防空速射炮
|
||
- 磁弩(CelestialAntiAirShip_Water) 磁弩也可从船厂生产
|
||
- 乌篷猎船(CelestialAntiNavyShipTech1) 反舰快艇/猎潜艇,这些不起眼的轻型小船装备了能发射聚焦冲击波的武器,能对军舰和潜艇造成破坏,猎船可以放置声纳浮标让敌方潜艇无处遁形(SpecialPower_CelestialSonarBuoy)
|
||
- 凤凰战机(CelestialFighterAircraft) 制空战斗机,可以使用技能快速回到机场(SpecialPowerReturnToProducer_F)
|
||
- 毕方支援机(CelestialSupportAircraft) 支援直升机,搭载高能激光器的直升机,本身伤害较低,但能使敌方车辆装甲熔融,降低敌方目标护甲。使用技能可以在激光器主武器和电磁支援之间切换(SpecialPower_ToggleCelestialSupportAircraftBuffWeapon),切换为电磁支援后可以增加友军输出
|
||
神州T2常用单位,需要神州高科(CelestialTechStructure):
|
||
- 岚影刺(CelestialInfiltrationInfantry) 渗透部队/狙击手,她可以从远处暗杀敌方步兵,也可以化妆成敌方步兵(SpecialPower_CelestialDisguise),让敌方无法发现
|
||
- 朱雀(CelestialAttackerAircraft) 对地攻击机,搭载中型离子炮的反装甲攻击机,还可以喷火反步兵或者削弱敌方装甲(SpecialPower_ActivateFire)
|
||
- 麒麟(CelestialAntiVehicleVehicleTech1) 反装甲主战坦克,神州陆军的新一代中流砥柱。可以使用技能偏转敌方来袭的炮火和导弹(SpecialPower_ToggleRangeUpdateCelestial)
|
||
- 青锋导弹车(CelestialLongRangeMissileVehicle_B) 远程反装甲,装备反坦克导弹的轻型载具,足以应对装甲目标,还可以发射烟雾弹削弱敌方单位的射程(SpecialPower_TriggerSmokeBombMissile)
|
||
- 计蒙驱逐舰(CelestialAlmightlyShip) 制海战舰,多用途驱逐舰,用舰炮和导弹对付海面、潜水或地面目标。可以使用阻止敌方单位使用技能(SpecialPower_CelestialShipScrambler)
|
||
神州T3常用单位,需要T3升级(Upgrade_CelestialTech_RANK2):
|
||
- 天罡(CelestialAntiInfantryInfantryAdvanced) 先进反步兵/反飞行器的机甲步兵,配备外骨骼和迅雷转轮机关铳的精英步兵,能快速收割敌方步兵、击落敌方飞行器。还可以瘫痪牵制敌方载具(SpecialPower_ActivateEMPThunder)
|
||
- 祝融(CelestialAntiVehicleVehicleTech3) 先进反装甲重型坦克,搭载了转轮式装弹的大型离子炮,借由主炮的余热可持续提高武器射速,使用技能可以大幅提高移速(SpecialPower_RapidCooling)
|
||
- 白虎(CelestialAntiStructureVehicle) 远距离对地攻城单位,可以发射高能等离子体团攻击远处的目标,也可以产生临时量子压制力场(SpecialPower_CelestialQuantumBreak)减速并削弱力场内的敌方目标
|
||
- 重明(CelestialInterceptorAircraft) 擅长拦截敌方重型空军的截击机,能发射炽热等离子束武器,可以使用技能快速回到机场(SpecialPowerReturnToProducer_F)
|
||
- 金乌(CelestialBomberAircraft) 重型轰炸机,发射导弹攻击低速目标、建筑和敌方军舰。可以使用技能快速回到机场(SpecialPowerReturnToProducer_F)
|
||
- 玄冥(CelestialAntiNavyShipTech3) 先进制海战舰,装备多种先进武器的巨型战舰,可以使用技能扫描远距离的海上目标并发射导弹(SpecialPower_CANSTier3HuntingMissile)
|
||
神州T4常用单位,需要T3升级(Upgrade_CelestialTech_RANK3):
|
||
- 摇光巡天炮(CelestialAdvanceAircraftTech4) 实验级飞行重轰炸,常态下无武装,但是可以展开到亚轨道高空。使用技能在常态和亚轨道状态之间切换(SpecialPower_CAAT4_Transform)。在亚轨道彻底展开的摇光巡天炮,能对一切目标发动穿透力极强的聚变射流攻击!
|
||
- 玄武(CelestialAntiStructureShip) 神州远距离对地轰炸,导弹攻击潜艇,额外装备有远近皆宜的对舰武器,还可以使用技能发射核弹(SpecialPower_CelestialShipMissle_01)
|
||
- 破军金甲(CelestialAntiVehicleVehicleTech4) 实验级反装甲机器人 巨大的战斗机甲,凭借无与伦比的厚重装甲冲进敌方坦克集群,并挥动能量巨剑将目标劈成两半,是敌方装甲部队的噩梦,可以使用技能越过障碍或直接降落到敌方部队中间(SpecialPower_CelestialArmybreakerLeap)
|
||
神州常用开局:
|
||
- 常规兵营开局:兵营,电厂,矿场,矿场,矿场;依靠神州强大的前期步兵进行压制,直接完成第三个矿场的扩张,然后再造其他建筑。神州的天眼哨机和碉台飞行核心可能会迫使对手出防空步兵(而不是出反步兵的基础步兵),可能让神州基础步兵获得短暂的数量优势
|
||
- 二矿重工开局:兵营,电厂,矿场,矿场,重工;假如前线步兵压力较大,也可以提前造重工,用凌波护卫战车辅助步兵,也可以让磁弩防空车切换成对地武器,并从陆地或海上进攻
|
||
- 二矿机场开局:兵营,电厂,矿场,矿场,机场;适合对帝国的天狗机甲等单位进行空军压制
|
||
神州协议:
|
||
- 百夫长(PlayerTech_Celestial_CenturionUpgrade):解锁协议技能SpecialPower_CelestialCenturionUpgrade,可强化一个步兵。
|
||
- 压制力场(PlayerTech_Celestial_EMSuppressField_Lv1):百夫长的后续协议。解锁协议技能SpecialPower_Celestial_EMSuppressField_Lv1,可以让一小片区域内的敌方单位大幅减速。
|
||
后续可以解锁更高级别的压制力场。大型压制力场(SpecialPower_Celestial_EMSuppressField_Lv3)可以让一大片区域内的敌方单位大幅减速。
|
||
- 空投仓(PlayerTech_Celestial_SpaceReinforce):压制力场的后续协议。解锁协议技能SpecialPower_CelestialSpaceReinforce,可从太空往地图上的任意陆地区域投送龙炎军和破甲铁卫
|
||
- 电能纳贡(PlayerTech_Celestial_PowerSealOff):解锁协议技能SpecialPower_CelestialPowerSealOff。只能对敌方电厂释放,让敌方电力暂时减少、己方电力暂时增加
|
||
- 天火塔(PlayerTech_Celestial_EMTurretDrop):电能纳贡的后续协议。解锁协议技能SpecialPowerCelestialEMTurretDrop,可从太空往地图上的任意陆地区域投送一个对地的激光防御塔
|
||
- 雷铸天兵(PlayerTech_Celestial_lightningTroopUpgrade_Lv1):天火塔的后续协议。解锁协议技能SpecialPower_CelestiallightningTroopUpgrade_Lv1,产生一道闪电,可以为己方部队充能并大幅强化己方部队,也可以用于对敌方部队造成伤害
|
||
神州超级武器:
|
||
- 日晷阵列(CelestialSuperWeapon) 超级武器 每次至少需要3分钟准备,可以释放止戈力场(SpecialPowerPause01),止戈力场内的敌我双方均不能开火,可用于阻挡敌方特殊能力、协议、或紧急救援己方单位
|
||
- 浴日神坛(CelestialSuperWeaponAdvanced) 终极武器 每次至少需要6分钟准备,向指定区域发射日冕风暴(SpecialPowerCelestialCannon),杀伤区域内的所有目标
|
||
";
|
||
|
||
var infinityIsleDescription = @"
|
||
当前交战的地图是:无限岛
|
||
这张地图是对称的,只有一条陆地进攻路线,也是主要的陆地交战区域,中央高地。
|
||
中央高地是长条状的,只有两个出入口,位于中央高地的两端,通向两位玩家的出生点。
|
||
玩家可以在高地战场正面交锋,也可以选择绕海,或者使用空军(不受地形限制)。
|
||
低地被中央高地分割成两部分,低地都是三面环海(还有一面是高地),两栖单位可以从低地上岸或下水。
|
||
中央高地也有靠海的地方,但两栖单位无法跨越悬崖从高地直接入海,需要从低地绕道。(额外矿区)
|
||
每个矿区可以摆放一个矿厂。
|
||
|
||
# 地图参数
|
||
海面高度:Z=200
|
||
低地高度:Z=210
|
||
高地高度:Z=280
|
||
|
||
## 出生点1(陆地)(X=1390,Y=1590,Z=210)
|
||
- 矿区(X=1230,Y=1975,Z=210)
|
||
- 矿区(X=1760,Y=1485,Z=210)
|
||
### 高地油井(X=2280,Y=2770,Z=280)
|
||
### 扩张方向
|
||
- 矿区,海面,远离中央区域(X=870,Y=870,Z=200)
|
||
- 矿区,中央高地(X=1740,Y=2760,Z=280),在它附近有:高地通往出生点1的唯一陆地路线。
|
||
- 额外矿区,海面,中央高地悬崖外面的海矿(X=2710,Y=2865,Z=200)
|
||
|
||
## 出生点2(陆地)(X=3980,Y=2190,Z=210)
|
||
- 矿区(X=4030,Y=1760,Z=210)
|
||
- 矿区(X=3540,Y=2290,Z=210)
|
||
### 高地油井(X=2980,Y=1050,Z=280)
|
||
### 扩张方向
|
||
- 矿区,海面,远离中央区域(X=4390,Y=2910,Z=280)
|
||
- 矿区,中央高地(X=3250,Y=1060,Z=280),在它附近有:高地通往出生点2的唯一陆地路线。
|
||
- 额外矿区,海面,中央高地悬崖外面的海矿(X=2650,Y=850,Z=280)
|
||
|
||
[MOD:RA3]
|
||
地图中央点:(X=2650,Y=1900,Z=280)
|
||
地图边界(X):0~5300
|
||
地图边界(Y):0~3800
|
||
";
|
||
|
||
static string Process(string text, Mod mod)
|
||
{
|
||
text = text.Trim();
|
||
var processed = new StringBuilder();
|
||
bool skipNextLine = false;
|
||
foreach (var line in text.Replace("\r", "").Split('\n'))
|
||
{
|
||
if (skipNextLine)
|
||
{
|
||
skipNextLine = false;
|
||
continue;
|
||
}
|
||
const string prefix = "[MOD:";
|
||
const string endTag = "[/MOD]";
|
||
if (line.StartsWith(prefix, StringComparison.OrdinalIgnoreCase))
|
||
{
|
||
if (!line.StartsWith($"[MOD:{mod.ModName.ToUpperInvariant()}]", StringComparison.OrdinalIgnoreCase))
|
||
{
|
||
skipNextLine = true;
|
||
}
|
||
continue;
|
||
}
|
||
// also support inline replace: content[MOD:CORONA]content[/MOD]content
|
||
var l = line;
|
||
while (true)
|
||
{
|
||
var startIndex = l.IndexOf(prefix, StringComparison.OrdinalIgnoreCase);
|
||
if (startIndex == -1)
|
||
{
|
||
break;
|
||
}
|
||
// then there is mod name, ']', content, and end tag
|
||
// we need:
|
||
// - substring before prefix
|
||
// - mod name for checking
|
||
// - content between ']' and [MOD:...]
|
||
// - substring after end tag
|
||
var endIndex = l.IndexOf(endTag, startIndex + prefix.Length, StringComparison.OrdinalIgnoreCase);
|
||
if (endIndex == -1)
|
||
{
|
||
throw new FormatException();
|
||
}
|
||
var modNameStartIndex = startIndex + prefix.Length;
|
||
var modNameEndIndex = l.IndexOf(']', modNameStartIndex);
|
||
if (modNameEndIndex == -1 || modNameEndIndex > endIndex)
|
||
{
|
||
throw new FormatException();
|
||
}
|
||
var modName = l.Substring(modNameStartIndex, modNameEndIndex - modNameStartIndex);
|
||
var contentStartIndex = modNameEndIndex + 1;
|
||
var content = l.Substring(contentStartIndex, endIndex - contentStartIndex);
|
||
if (modName.Equals(mod.ModName, StringComparison.OrdinalIgnoreCase))
|
||
{
|
||
l = l.Substring(0, startIndex) + content + l.Substring(endIndex + endTag.Length);
|
||
}
|
||
else
|
||
{
|
||
l = l.Substring(0, startIndex) + l.Substring(endIndex + endTag.Length);
|
||
}
|
||
}
|
||
processed.Append(l);
|
||
processed.Append('\n');
|
||
}
|
||
return processed.ToString();
|
||
}
|
||
|
||
var sb = new StringBuilder();
|
||
sb.AppendLine(generalDescriptions);
|
||
sb.AppendLine();
|
||
|
||
var factions = players
|
||
.Select(kv => ModData.GetFaction(mod, kv.Value.FactionId).Name)
|
||
.Distinct()
|
||
.ToArray();
|
||
var factionDescriptions = new Dictionary<string, string>
|
||
{
|
||
{ "盟军", Process(alliedDescriptions, mod) },
|
||
{ "神州", Process(celestialDescriptions, mod) },
|
||
};
|
||
foreach (var faction in factions)
|
||
{
|
||
if (factionDescriptions.TryGetValue(faction, out var description))
|
||
{
|
||
sb.AppendLine(description);
|
||
sb.AppendLine();
|
||
}
|
||
}
|
||
return sb.ToString().Replace("\r", "");
|
||
}
|
||
|
||
public static string BuildUserPrompt(Mod mod, ImmutableSortedDictionary<int, Player> players, string commandData)
|
||
{
|
||
var playerNamesForAI = PlayerNamesForAI(mod, players);
|
||
var playerData = players.Select(kv =>
|
||
{
|
||
var id = kv.Key;
|
||
var player = kv.Value;
|
||
var playerNameForAI = playerNamesForAI[id];
|
||
var prefix = player.IsComputer ? "电脑" : "玩家";
|
||
var name = player.IsComputer ? $"[{player.PlayerName}AI]" : player.PlayerName;
|
||
var factionName = ModData.GetFaction(mod, player.FactionId).Name;
|
||
if (player.Team >= 0)
|
||
{
|
||
return $"{prefix}#{id} {name} ({playerNameForAI}),{factionName},队伍{player.Team}";
|
||
}
|
||
return $"{prefix}#{id} {name} ({playerNameForAI}),{factionName}";
|
||
});
|
||
var sb = new StringBuilder();
|
||
sb.AppendLine("请你阅读并分析以下数据,首先进行初步分析,然后判断是否需要分段处理数据");
|
||
sb.AppendLine("玩家列表:");
|
||
sb.AppendLine(string.Join("\n", playerData));
|
||
sb.AppendLine("数据:");
|
||
sb.AppendLine(commandData);
|
||
return sb.ToString().Replace("\r", "");
|
||
}
|
||
|
||
public static (int BytesCount, int EstimatedTokenCount) EstimateTokenCount(string text)
|
||
{
|
||
var bytesCount = Encoding.UTF8.GetByteCount(text);
|
||
var estimatedTokenCount = (int)Math.Ceiling(bytesCount / 2.2); // 2.2 is tested
|
||
return (bytesCount, estimatedTokenCount);
|
||
}
|
||
|
||
public static ImmutableSortedDictionary<int, string> PlayerNamesForAI(Mod mod, ImmutableSortedDictionary<int, Player> players)
|
||
{
|
||
var computers = players.Where(kv => kv.Value.IsComputer).ToImmutableSortedDictionary();
|
||
var humanPlayers = players.Where(kv => !kv.Value.IsComputer).ToImmutableSortedDictionary();
|
||
|
||
var result = new Dictionary<int, string>();
|
||
foreach (var kv in players)
|
||
{
|
||
var id = kv.Key;
|
||
var player = kv.Value;
|
||
var prefix = player.IsComputer ? "AI_" : "Player";
|
||
var faction = ModData.GetFaction(mod, player.FactionId);
|
||
// check if player faction id is only appeared once, if so use faction name as player name
|
||
var checkSource = player.IsComputer ? computers : humanPlayers;
|
||
if (checkSource.Count(kv2 => kv2.Value.FactionId == player.FactionId) == 1)
|
||
{
|
||
var letter = faction.Name switch
|
||
{
|
||
"盟军" => "A",
|
||
"苏联" => "S",
|
||
"帝国" => "E",
|
||
"神州" => "C",
|
||
"随机" => "R",
|
||
_ when faction.Kind is FactionKind.Observer => "O",
|
||
_ => id.ToString(),
|
||
};
|
||
result[id] = $"{prefix}{letter}";
|
||
}
|
||
else
|
||
{
|
||
result[id] = faction.Kind is FactionKind.Observer
|
||
? $"{prefix}O{id}"
|
||
: $"{prefix}{id}";
|
||
}
|
||
}
|
||
return result.ToImmutableSortedDictionary();
|
||
}
|
||
|
||
public async Task<Result> AnalyzeAsync(
|
||
string model,
|
||
string instruction,
|
||
string text,
|
||
Dictionary<string, object> extraParams,
|
||
Action<AIChunk> onChunk,
|
||
CancellationToken cancellationToken)
|
||
{
|
||
foreach (var kv in extraParams)
|
||
{
|
||
_state[kv.Key] = kv.Value;
|
||
}
|
||
_state["model"] = model;
|
||
_state["messages"] = _messages;
|
||
_state["stream"] = true;
|
||
_state["stream_options"] = new
|
||
{
|
||
include_usage = true
|
||
};
|
||
|
||
_messages.Clear();
|
||
_messages.AddRange(
|
||
[
|
||
new
|
||
{
|
||
role = "system",
|
||
content = instruction
|
||
},
|
||
new
|
||
{
|
||
role = "user",
|
||
content = text
|
||
}
|
||
]);
|
||
|
||
var result = await DoRequest(onChunk, cancellationToken);
|
||
|
||
var splitted = result.Response.Split('\n').ToList();
|
||
var titleIndex = splitted.FindIndex(l => l.Contains("[分段列表]"));
|
||
if (titleIndex == -1)
|
||
{
|
||
throw new Exception("AI分析失败");
|
||
}
|
||
_segments.Clear();
|
||
_currentSegment = 0;
|
||
// regex match two timespan in "[0:00.0]~[0:55.4]"
|
||
var timeSpanRegex = new Regex(@"\[([^]]+)\]~\[([^]]+)\]");
|
||
for (var i = titleIndex + 1; i < splitted.Count; ++i)
|
||
{
|
||
var line = splitted[i];
|
||
var match = timeSpanRegex.Match(line);
|
||
if (match.Success)
|
||
{
|
||
var startTimeText = match.Groups[1].Value;
|
||
var endTimeText = match.Groups[2].Value;
|
||
var start = ParseAITimeSpan(startTimeText);
|
||
var end = ParseAITimeSpan(endTimeText);
|
||
var description = line.Substring(match.Index + match.Length).Trim();
|
||
_segments.Add((start, end, description));
|
||
}
|
||
if (_segments.Count >= 5)
|
||
{
|
||
break;
|
||
}
|
||
}
|
||
|
||
result.Segments = _segments;
|
||
result.CurrentSegment = _currentSegment;
|
||
return result;
|
||
}
|
||
|
||
public async Task<Result> ContinueAnalyzeAsync(
|
||
Action<AIChunk> onChunk,
|
||
CancellationToken cancellationToken)
|
||
{
|
||
if (_currentSegment < 0 || _currentSegment >= _segments.Count)
|
||
{
|
||
throw new InvalidOperationException("Current segment index is out of range.");
|
||
}
|
||
var segment = _segments[_currentSegment];
|
||
var beginText = _currentSegment <= 0
|
||
? "游戏开始"
|
||
: segment.Start.ToString();
|
||
var endText = _currentSegment >= _segments.Count - 1
|
||
? "游戏结束"
|
||
: segment.End.ToString();
|
||
var instruction = @$"
|
||
请分析第{_currentSegment + 1}段({beginText}至{endText})的数据。
|
||
你需要列出{beginText}至{endText}的主要事件、以及其他有分析价值的事件,每个事件都应该附上时间戳。
|
||
请按照按照[观察]、[分析]、[推理]、[进一步思考(可选)]的步骤,对各个事件进行分析和推理。
|
||
|
||
假如当前阶段存在一些较为重要的单位,假如能推测出它们可能是什么单位,则可以列出单位的UnitId以及你对单位的推测
|
||
";
|
||
instruction = instruction.Trim().Replace("\r", "");
|
||
_messages.Add(new
|
||
{
|
||
role = "user",
|
||
content = instruction
|
||
});
|
||
|
||
var result = await DoRequest(onChunk, cancellationToken);
|
||
|
||
++_currentSegment;
|
||
|
||
result.Segments = _segments;
|
||
result.CurrentSegment = _currentSegment;
|
||
return result;
|
||
}
|
||
|
||
private async Task<Result> DoRequest(Action<AIChunk> onChunk, CancellationToken cancellationToken)
|
||
{
|
||
var inputJson = JsonSerializer.Serialize(_state);
|
||
|
||
using var request = new HttpRequestMessage(HttpMethod.Post, "chat/completions");
|
||
request.Content = new StringContent(inputJson, Encoding.UTF8, "application/json");
|
||
using var response = await _http.SendAsync(
|
||
request,
|
||
HttpCompletionOption.ResponseHeadersRead,
|
||
cancellationToken);
|
||
|
||
response.EnsureSuccessStatusCode();
|
||
|
||
using var stream = await response.Content.ReadAsStreamAsync();
|
||
using var reader = new StreamReader(stream);
|
||
|
||
var fullBuilder = new StringBuilder();
|
||
var result = new Result();
|
||
|
||
while (!reader.EndOfStream)
|
||
{
|
||
var line = await reader.ReadLineAsync();
|
||
|
||
if (string.IsNullOrWhiteSpace(line))
|
||
{
|
||
continue;
|
||
}
|
||
|
||
if (!line.StartsWith("data: "))
|
||
{
|
||
continue;
|
||
}
|
||
|
||
var data = line.Substring(6);
|
||
|
||
if (data == "[DONE]")
|
||
{
|
||
break;
|
||
}
|
||
|
||
onChunk?.Invoke(new AIChunk
|
||
{
|
||
Type = AIChunkType.Json,
|
||
Text = data
|
||
});
|
||
|
||
using var doc = JsonDocument.Parse(data);
|
||
if (doc.RootElement.TryGetProperty("usage", out var usage) && usage.ValueKind == JsonValueKind.Object)
|
||
{
|
||
static int? GetIntegerProperty(JsonElement @object, string field)
|
||
{
|
||
if (@object.TryGetProperty(field, out var value) && value.ValueKind is JsonValueKind.Number)
|
||
{
|
||
return value.GetInt32();
|
||
}
|
||
return null;
|
||
}
|
||
result.PromptTokens = GetIntegerProperty(usage, "prompt_tokens");
|
||
result.TotalTokens = GetIntegerProperty(usage, "total_tokens");
|
||
result.CompletionTokens = GetIntegerProperty(usage, "completion_tokens");
|
||
result.ReasoningTokens = GetIntegerProperty(usage, "reasoning_tokens");
|
||
}
|
||
|
||
if (!doc.RootElement.TryGetProperty("choices", out var choices)
|
||
|| choices.ValueKind != JsonValueKind.Array
|
||
|| choices.GetArrayLength() == 0)
|
||
{
|
||
continue;
|
||
}
|
||
|
||
var delta = choices[0].GetProperty("delta");
|
||
// ===== content =====
|
||
if (delta.TryGetProperty("content", out var content))
|
||
{
|
||
var text = content.GetString();
|
||
if (!string.IsNullOrEmpty(text))
|
||
{
|
||
fullBuilder.Append(text);
|
||
|
||
onChunk?.Invoke(new AIChunk
|
||
{
|
||
Type = AIChunkType.Content,
|
||
Text = text
|
||
});
|
||
}
|
||
}
|
||
|
||
// ===== reasoning (optional, DeepSeek / some models) =====
|
||
if (delta.TryGetProperty("reasoning_content", out var reasoning))
|
||
{
|
||
var text = reasoning.GetString();
|
||
if (!string.IsNullOrEmpty(text))
|
||
{
|
||
onChunk?.Invoke(new AIChunk
|
||
{
|
||
Type = AIChunkType.Reasoning,
|
||
Text = text
|
||
});
|
||
}
|
||
}
|
||
}
|
||
|
||
var resultText = fullBuilder.ToString();
|
||
|
||
_messages.Add(new
|
||
{
|
||
role = "assistant",
|
||
content = resultText
|
||
});
|
||
|
||
result.Response = resultText;
|
||
return result;
|
||
}
|
||
|
||
private static TimeSpan ParseAITimeSpan(string input)
|
||
{
|
||
if (string.IsNullOrWhiteSpace(input))
|
||
{
|
||
throw new FormatException("Empty input");
|
||
}
|
||
|
||
input = input.Trim();
|
||
|
||
|
||
var parts = input.Split(':');
|
||
|
||
if (parts.Length == 0)
|
||
{
|
||
throw new FormatException("Invalid format");
|
||
}
|
||
|
||
// -----------------------------
|
||
// 1. 解析最后一段:seconds + fraction
|
||
// -----------------------------
|
||
if (!float.TryParse(parts.Last(), out float floatSeconds))
|
||
{
|
||
throw new FormatException("Invalid seconds");
|
||
}
|
||
int seconds = (int)floatSeconds;
|
||
int milliseconds = (int)Math.Round((floatSeconds - seconds) * 1000);
|
||
|
||
// -----------------------------
|
||
// 2. 累加前面的部分(从右往左)
|
||
// -----------------------------
|
||
long totalSeconds = seconds;
|
||
long multiplier = 60; // 每层递进:秒->分->时->天...
|
||
|
||
for (int i = parts.Length - 2; i >= 0; i--)
|
||
{
|
||
if (!long.TryParse(parts[i], out long value))
|
||
throw new FormatException($"Invalid number: {parts[i]}");
|
||
|
||
totalSeconds += value * multiplier;
|
||
multiplier *= 60;
|
||
}
|
||
|
||
var result = TimeSpan.FromSeconds(totalSeconds)
|
||
+ TimeSpan.FromMilliseconds(milliseconds);
|
||
|
||
return result;
|
||
}
|
||
}
|
||
}
|