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; using System.Windows; namespace AnotherReplayReader.Utils { internal sealed class AIAnalyze { public static string GetSystemPrompt( Replay replay, ImmutableSortedDictionary players, AiPromptSettings? promptSettings = null) { var defaultPrompt = BuildDefaultSystemPrompt(replay, players); return ComposeSystemPrompt(defaultPrompt, promptSettings); } public static string ComposeSystemPrompt(string defaultPrompt, AiPromptSettings? promptSettings) { var customSystemPrompt = promptSettings?.CustomSystemPrompt ?? string.Empty; if (promptSettings?.UseCustomSystemPrompt == true && !string.IsNullOrWhiteSpace(customSystemPrompt)) { defaultPrompt = customSystemPrompt; } var additionalRules = promptSettings?.AdditionalRules ?? string.Empty; if (!string.IsNullOrWhiteSpace(additionalRules)) { defaultPrompt = defaultPrompt.TrimEnd() + "\n\n# 用户自定义补充规则\n" + additionalRules.Trim(); } return defaultPrompt.Replace("\r", ""); } public static string BuildDefaultSystemPrompt(Replay replay, ImmutableSortedDictionary players) { var generalDescriptions = @" 你是一位 RTS 游戏数据分析师,你擅长从大量数据中发现有趣的规律和细节。 用户则是一位玩家,用户会向你提供玩家操作记录,你要对其进行分析。 # 核心原则 - 你只能根据用户提供的操作记录、玩家信息、下方游戏规则和明确给出的背景知识进行分析。 - 不要使用现实世界常识或其他 RTS 游戏常识覆盖这里的游戏设定。例如:步兵、直升机、建筑水陆摆放、运输能力、两栖能力都必须以这里的规则和单位描述为准。 - 不确定时必须保留多个候选,不要为了让解说流畅而过早下定论。 - 对 UnitId、单位类型、战术意图的判断必须区分证据等级:确定、高度可能、可能、待确认、已排除。 - 每个关键推理都应当包含支持证据;如果存在会推翻该推理的反证,也要主动指出。 - 如果某个技能或行为可以对应多个单位,先列出候选,并说明还需要哪些后续迹象才能确认。 - 对已经被操作记录直接否定的判断必须修正或放弃,不要坚持原结论。 # 输入格式 ## 用户初始输入 - 玩家信息 - 操作信息 你首先需要阅读玩家列表,然后开始分析玩家的操作信息 玩家信息里包含玩家名称、代码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 [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])的数据"" - 选取该阶段的主要事件,以及和它们的上下文 - 也可以选择数个其他有分析价值的事件 - 推理思考时:不要直接列出所有操作信息,可以先只列出一部分,然后按需向前以及向后“延申” - 值得列出的、值得反复确认的运营类操作信息:开始建造、摆放建筑、出售建筑 - **不是**运营类操作信息:重新选择单位、创建编队、选择编队、移动、攻击等。 - 当你在思考时:你可以首先从数量较少的运营类操作信息开始,然后找到可能与其相关的其他操作信息,综合进行推理。不要直接按照时间线列出所有操作信息。 - 也可以重点关注PlayerTech、英雄、工程师 - 按照**推理指南**进行详细的思考与推理,列举你的推理与发现 - 输出:该阶段的各个主要事件,以及你的推理和发现 - 假如推测 UnitId 对应的单位,请在正文中自然描述,并在末尾输出机器可读声明,方便程序验证 ## 3. 最终总结阶段 触发条件:用户输入包含:""请对以上内容进行总结"" - 输出:所有分析的总结,以及这次对局的完整介绍 # 机器可读声明 仅限于:分段分析阶段(第2阶段) 如果你对 UnitId、关键事件或时间线做出了可验证推测,请在回答末尾附加下面格式。 必须先输出一行`[机器可读声明]`,然后输出一个 JSON 代码块: [机器可读声明] ```json { ""unitClaims"": [ { ""unitId"": 123, ""player"": ""PlayerA"", ""claim"": ""AlliedMCV"", ""evidenceLevel"": ""possible"", ""evidence"": [""8:30 使用 SpecialPower_UnpackReplaceSelf""], ""alternatives"": [""AlliedMiner 展开后的指挥中心""], ""needsConfirmation"": [""是否曾使用 SpecialPower_PackReplaceSelf"", ""后续是否作为建造者出现""] } ], ""eventClaims"": [], ""timelineClaims"": [] } ``` 如果没有可验证推测,可以输出空数组。不要在 JSON 里写注释。 # 推理指南 推理需要分成多个阶段 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尝试在原地重建建筑、试图东山再起、准备反击 ## 示例6: 操作信息(应当视为输入,你推理时不需要原样输出所有操作,只输出少数关键事件): ``` [7:18.73] PlayerA: 开始维修建筑 [UnitId]2241 [7:19.13] PlayerA: 开始建造 [UnitId]2241(建造者) AlliedWallPiece,序列:其他建筑 [7:19.33] PlayerA: 开始建造 [UnitId]2241(建造者) AlliedWallPiece,序列:其他建筑 [8:30.60] PlayerA: 释放特殊能力(无目标) SpecialPower_PackReplaceSelf,0,1 [UnitId]2241,0 [8:30.73] PlayerA: 选择相同单位 False,False [UnitId]4518 [8:31.06] PlayerA: 队形操作 (X=2493,Y=2374,Z=280) 2.59 4 False,False [8:31.13] PlayerA: 开始出兵 [UnitId]291(出兵建筑) AlliedEngineer 序列:步兵 [8:43.73] PlayerA: 开始出兵 [UnitId]1958(出兵建筑) AlliedMCV 序列:载具 ``` 事件时间段[7:18]~[8:44] 观察: - PlayerA开始维修主基地 - PlayerA建造围墙 - 约一分钟后,PlayerA主基地打包成基地车 - PlayerA开始出工程师(AlliedEngineer) - PlayerA开始建造基地车(AlliedMCV) 分析: - PlayerA的主基地受到伤害,因此开始维修,但接下来一分钟没有其他操作 - 工程师进驻己方建筑可以进行维修,而且效率远高于普通维修 - PlayerA最终开始生产基地车 推理: - 主基地凭借自身的高血量,承受了超过一分钟的攻击 - PlayerA选择出工程师,说明主基地在长时间承受攻击后,已经非常危险,急需更高效率的维修 - PlayerA试图操作基地车令其移动 - 基地车可能没有活下来,PlayerA开始建造第二辆基地车 进一步思考: - PlayerA应该是做好了主基地被摧毁的准备,因此开始提前准备第二辆基地车 你需要对玩家操作记录中的各个主要事件都启用上面这样的思考模式。 # 背景信息 下面是一些背景信息: 红色警戒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:NO:CORONA] - 高科(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:NO:CORONA]盟军最[/MOD]强大的支援直升机,被它攻击的目标不会受到伤害,但会被冻住。冰冻状态下的单位无法开火或移动,而且可被其他单位一击秒杀。冷冻直升机可以对单个敌军或友军目标使用缩小光束(SpecialPower_ShrinkRay),缩小的单位各项属性都被大幅削弱,但速度增加。 - 突袭驱逐舰(AlliedAntiNavyShipTech1):两栖,坚固的船厂T2单位,只能使用输出较低的舰炮[MOD:CORONA],但可用高伤害的深水炸弹攻击潜艇[/MOD]。它可以用黑洞装甲(SpecialPower_ToggleMagneticArmor)把敌方火力都吸引到自己身上,从而对友军提供掩护。突袭驱逐舰在陆地上移动,它在岸上同样可以吸收敌方火力,掩护陆军和空军 盟军T3常用单位,需要T3升级(Upgrade_AlliedTech3): - 雅典娜炮(AlliedAntiStructureVehicle):盟军远距离对地攻城单位,引导太空卫星激光攻击固定或低速目标,还可以开启巨大的护盾(SpecialPower_ToggleShieldSphere)来掩护附近的友军 - 幻影坦克(AlliedAntiVehicleVehicleTech3):使用光谱武器的先进攻击坦克,伤害很高,但射程很低。[MOD:NO:CORONA]因此出场率不如雅典娜炮[/MOD][MOD:CORONA]可以使用隐形技能(SpecialPower_AlliedAntiVehicleVehicleTech3AssassinStateDisguise)潜入到敌方腹地进行袭击,敌方必须造侦察单位来探测隐形才能反制[/MOD] [MOD:NO:CORONA] - 世纪轰炸机(AlliedBomberAircraft):擅长攻击建筑等大型目标的战略轰炸机,可用来轰炸敌方基地。攻击后需要返回机场补充弹药。可以运输步兵并使用SpecialPower_EjectPassengersUntargeted让步兵跳伞。 [MOD:CORONA] - 世纪轰炸机(AlliedAntiStructureBomberAircraft):擅长攻击建筑、军舰等大型目标的战略轰炸机。攻击后需要返回机场补充弹药。可以使用技能SpecialPowerReturnToProducer来快速返航。 [MOD:NO:CORONA] - 航空母舰(AlliedAntiStructureShip):盟军远距离对地攻城单位,可以释放无人机攻击海面或地面目标 [MOD:CORONA] - 冷冻军团(AlliedCryoLegionnaire):两栖,高级支援步兵,可以大幅降低敌方单位的速度让敌方难以冲击阵地或逃跑 [MOD:CORONA] - 波塞冬巡洋舰(AlliedAntiNavyShipTech3):盟军的大型制海巡洋舰 - 谭雅(AlliedCommandoTech1):两栖,盟军英雄步兵单位,擅长反步兵和反建筑的无双女士兵。她的时空腰带(SpecialPower_TimeBelt)能让谭雅回溯到一段时间之前的血量和坐标位置。她可以高效炸毁建筑,因此玩家会设法把谭雅进入(或者直接用载具运输到)敌方基地。每个玩家同时只能有一位谭雅 [MOD:CORONA] 盟军T4常用单位,需要T4升级(Upgrade_AlliedTech4): [MOD:CORONA] - 航空母舰(AlliedAntiStructureShip):盟军远距离对地攻城单位,可以释放无人机攻击海面或地面目标 [MOD:CORONA] - 先锋炮艇机(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 infinityIsleVanilla = @" 当前交战的地图是:无限岛 这张地图是对称的,只有一条陆地进攻路线,也是主要的陆地交战区域,中央高地。 中央高地是长条状的,只有两个出入口,位于中央高地的两端,通向两位玩家的出生点。 玩家可以在高地战场正面交锋,也可以选择绕海,或者使用空军(不受地形限制)。 低地被中央高地分割成两部分,低地都是三面环海(还有一面是高地),两栖单位可以从低地上岸或下水。 中央高地也有靠海的地方,但两栖单位无法跨越悬崖从高地直接入海,需要从低地绕道。(额外矿区) 每个矿区可以摆放一个矿厂。 # 地图参数 海面高度: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) 地图中央点:(X=2650,Y=1900,Z=280) 地图边界(X):0~5300 地图边界(Y):0~3800 "; var infinityIsleCorona = @" 当前交战的地图是:无限岛 这张地图是对称的,只有一条陆地进攻路线,也是主要的陆地交战区域,中央高地。 中央高地是长条状的,只有两个出入口,位于中央高地的两端,通向两位玩家的出生点。 玩家可以在高地战场正面交锋,也可以选择绕海,或者使用空军(不受地形限制)。 低地被中央高地分割成两部分,低地都是三面环海(还有一面是高地),两栖单位可以从低地上岸或下水。 中央高地也有靠海的地方,但两栖单位无法跨越悬崖从高地直接入海,需要从低地绕道。(额外矿区) 每个矿区可以摆放一个矿厂。 # 地图参数 海面高度:Z=200 低地高度:Z=210 高地高度:Z=280 ## 出生点1(陆地)(X=1420,Y=1920,Z=210) - 矿区(X=1322,Y=2178,Z=210) - 矿区(X=1513,Y=1648,Z=210) ### 高地油井(X=2350,Y=2980,Z=280)[UnitId]228 ### 扩张方向 - 矿区,海面,远离中央区域(X=940,Y=1096,Z=200) - 矿区,中央高地(X=1810,Y=2984,Z=280),在它附近有:高地通往出生点1的唯一陆地路线。 - 额外矿区,海面,中央高地悬崖外面的海矿(X=2828,Y=3090,Z=200) ## 出生点2(陆地)(X=4125,Y=2270,Z=210) - 矿区(X=4101,Y=1987,Z=210) - 矿区(x=4016,Y=2496,Z=210) ### 高地油井(X=3070,Y=1280,Z=280)[UnitId]229 ### 扩张方向 - 矿区,海面,远离中央区域(X=4504,Y=3132,Z=200) - 矿区,中央高地(X=3613,Y=1266,Z=280),在它附近有:高地通往出生点2的唯一陆地路线。 - 额外矿区,海面,中央高地悬崖外面的海矿(X=2743,Y=1054,Z=200) 地图中央点:(X=2745,Y=2125,Z=280) 地图边界(X):0~5490 地图边界(Y):0~4250 中立建筑:[UnitId]的范围从10~227的单位都属于地图物件或中立建筑 "; 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 noPrefix = "[MOD:NO:"; const string endTag = "[/MOD]"; if (line.StartsWith(prefix, StringComparison.OrdinalIgnoreCase)) { // check exclude if (line.StartsWith(noPrefix, StringComparison.OrdinalIgnoreCase)) { if (line.StartsWith($"[MOD:NO:{mod.ModName}]", StringComparison.OrdinalIgnoreCase)) { skipNextLine = true; } continue; } if (!line.StartsWith($"[MOD:{mod.ModName}]", 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); // exclude first if (modName.StartsWith("NO:", StringComparison.OrdinalIgnoreCase)) { var deniedModName = modName.Substring(3); if (deniedModName.Equals(mod.ModName, StringComparison.OrdinalIgnoreCase)) { l = l.Substring(0, startIndex) + l.Substring(endIndex + endTag.Length); } else { l = l.Substring(0, startIndex) + content + l.Substring(endIndex + endTag.Length); } continue; } 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.Trim()); sb.AppendLine(); var mod = replay.Mod; var factions = players .Select(kv => ModData.GetFaction(mod, kv.Value.FactionId).Name) .Distinct() .ToArray(); var factionDescriptions = new Dictionary { { "盟军", Process(alliedDescriptions, mod) }, { "神州", Process(celestialDescriptions, mod) }, }; foreach (var faction in factions) { if (factionDescriptions.TryGetValue(faction, out var description)) { sb.AppendLine(description); sb.AppendLine(); } else { MessageBox.Show($"缺乏阵营{faction}的介绍,AI难以分析"); } } var isCorona = mod.ModName.Equals("corona", StringComparison.OrdinalIgnoreCase); var mapDescriptions = new Dictionary { ["map_mp_2_rao1"] = isCorona ? infinityIsleCorona : infinityIsleVanilla }; var mapPath = Path.GetFileNameWithoutExtension(replay.MapPath); if (mapDescriptions.TryGetValue(mapPath, out var mapDescription)) { sb.AppendLine(); sb.AppendLine(mapDescription.Trim()); } else { MessageBox.Show($"没有地图{mapPath}的介绍,AI难以分析"); } return sb.ToString().Replace("\r", ""); } public static string BuildUserPrompt( Mod mod, ImmutableSortedDictionary players, string commandData, out string prefix ) { 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("数据:"); prefix = sb.ToString(); sb.AppendLine(commandData); return sb.ToString().Replace("\r", ""); } public static string BuildSegmentUserPrompt(IReadOnlyList segments, int currentSegmentIndex, int eventCount) { if (currentSegmentIndex < 0 || currentSegmentIndex >= segments.Count) { throw new InvalidOperationException("Current segment index is out of range."); } var segment = segments[currentSegmentIndex]; var beginText = currentSegmentIndex <= 0 ? "游戏开始" : segment.Start.ToString(); var endText = currentSegmentIndex >= segments.Count - 1 ? "游戏结束" : segment.End.ToString(); var instruction = @$" 请分析第{currentSegmentIndex + 1}段({beginText}至{endText})的数据。 本阶段一共有{eventCount}条操作信息,数量很大,因此**不要**在思考推理时直接把{eventCount}条操作信息都列出来。 也不要把每一条操作信息都视作一个单独事件。 你需要列出{beginText}至{endText}的主要事件、以及其他有分析价值的事件。 请按照按照[观察]、[分析]、[推理]、[进一步思考(可选)]的步骤,对各个事件进行分析和推理。 假如当前阶段存在一些较为重要的单位、而且能够推测出它们可能是什么单位,则可以列出单位的UnitId以及你对单位的推测 如果你列出了 UnitId 推测、关键事件推测或时间线推测,请在回答末尾附加`[机器可读声明]` JSON 代码块;如果没有相关推测,则输出空数组。 "; return instruction.Trim().Replace("\r", ""); } public static string BuildFinalUserPrompt(int totalEventCount) { var instruction = $@" 请对以上内容进行总结。 基于: - 你对于各阶段的推理分析 - 由我在对话刚开始时提供的原始数据(约{totalEventCount}个操作信息)。 请对各阶段的推理分析进行汇总。 判断是否需要对之前的分析进行补充,例如检查之前是否遗漏了某些重要的操作信息? 判断是否需要对之前的分析进行修正。 最终应该输出: - 一个完整的对局介绍(解说),其中需要包含玩家们在各阶段经历的所有主要事件,也需要包含你的分析和推理。 "; return instruction.Trim().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 PlayerNamesForAI(Mod mod, ImmutableSortedDictionary players) { var computers = players.Where(kv => kv.Value.IsComputer).ToImmutableSortedDictionary(); var humanPlayers = players.Where(kv => !kv.Value.IsComputer).ToImmutableSortedDictionary(); var result = new Dictionary(); 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 enum AIChunkType { Reasoning, Content, Error, Json } public struct AIChunk { public AIChunkType Type; public string Text; } public record Segment(TimeSpan Start, TimeSpan End, string Description); public record State(ImmutableList Messages, ImmutableList Segments, int CurrentSegment) { public static State Initial => new(ImmutableList.Empty, ImmutableList.Empty, -1); public State AppendNewMessage(string role, string content) { var newMessages = Messages.Add(new { role, content }); return this with { Messages = newMessages }; } public State AppendNewSegment(Segment segment) { var newSegments = Segments.Add(segment); return this with { Segments = newSegments }; } } public struct Result { public string Response; public State State; public int? PromptTokens; public int? CompletionTokens; public int? TotalTokens; public int? ReasoningTokens; } private readonly HttpClient _http; private State _state = State.Initial; public State LastSuccessfulState => _state; public AIAnalyze() { _http = new HttpClient { Timeout = TimeSpan.FromMinutes(5), }; } public void SetState(State state) { _state = state; } public async Task AnalyzeAsync( string instruction, string text, AiRequestContext requestContext, Action onChunk, CancellationToken cancellationToken) { var inputState = State.Initial .AppendNewMessage("system", instruction) .AppendNewMessage("user", text); var result = await Task.Run(() => DoRequest(_http, inputState, requestContext, onChunk, cancellationToken)); var resultState = result.State; var splitted = result.Response.Split('\n').ToList(); var titleIndex = splitted.FindIndex(l => l.Contains("[分段列表]")); if (titleIndex == -1) { throw new Exception("AI分析失败"); } // 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(); resultState = resultState.AppendNewSegment(new(start, end, description)); } } result.State = resultState with { CurrentSegment = 0 }; _state = result.State; return result; } public async Task ContinueAnalyzeAsync( string instruction, AiRequestContext requestContext, Action onChunk, CancellationToken cancellationToken) { if (_state.CurrentSegment < 0 || _state.CurrentSegment >= _state.Segments.Count) { throw new InvalidOperationException("Current segment index is out of range."); } var inputState = _state.AppendNewMessage("user", instruction); var result = await Task.Run(() => DoRequest(_http, inputState, requestContext, onChunk, cancellationToken)); result.State = result.State with { CurrentSegment = _state.CurrentSegment + 1 }; _state = result.State; return result; } public async Task FinishAnalyzeAsync( string instruction, AiRequestContext requestContext, Action onChunk, CancellationToken cancellationToken) { if (_state.CurrentSegment != _state.Segments.Count) { throw new InvalidOperationException("Current segment index is out of range."); } var inputState = _state.AppendNewMessage("user", instruction); var result = await Task.Run(() => DoRequest(_http, inputState, requestContext, onChunk, cancellationToken)); _state = result.State; return result; } private static async Task DoRequest( HttpClient http, State state, AiRequestContext requestContext, Action onChunk, CancellationToken cancellationToken) { var provider = requestContext.Provider; var requestParams = ProcessRequestParams(state, requestContext.BuildRequestParams()); var isStream = requestContext.Model.IsStream; var inputJson = JsonSerializer.Serialize(requestParams); var uri = new Uri(new(provider.BaseUrl.TrimEnd('/') + "/"), "chat/completions"); using var request = new HttpRequestMessage(HttpMethod.Post, uri); request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", provider.ApiKey); request.Content = new StringContent(inputJson, Encoding.UTF8, "application/json"); using var response = await http.SendAsync( request, HttpCompletionOption.ResponseHeadersRead, cancellationToken); using var responseStream = await response.Content.ReadAsStreamAsync(); using var reader = new StreamReader(responseStream); var fullBuilder = new StringBuilder(); var result = new Result(); // 根据模式分别读取响应 // if response.Content.Headers.ContentType is "text/event-stream", then it's stream mode, otherwise it's non-stream mode var contentType = response.Content.Headers.ContentType?.MediaType; if (contentType != null) { isStream = contentType.Equals("text/event-stream", StringComparison.OrdinalIgnoreCase); } if (isStream) { while (!reader.EndOfStream) { cancellationToken.ThrowIfCancellationRequested(); 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); ProcessJsonDocument(doc, isStream: true, fullBuilder, result, onChunk); } } else { var json = await reader.ReadToEndAsync(); using var doc = JsonDocument.Parse(json); ProcessJsonDocument(doc, isStream: false, fullBuilder, result, onChunk); } response.EnsureSuccessStatusCode(); if (fullBuilder.Length == 0) { throw new Exception("AI分析失败,返回内容为空"); } var resultText = fullBuilder.ToString(); result.State = state.AppendNewMessage("assistant", resultText); result.Response = resultText; return result; } /// /// 处理单个 JSON 响应(既用于 stream 的每个 chunk,也用于 non‑stream 的完整响应) /// private static void ProcessJsonDocument( JsonDocument doc, bool isStream, StringBuilder fullBuilder, Result result, Action onChunk) { // 提取 usage(如果存在) 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.PromptTokens; result.TotalTokens = GetIntegerProperty(usage, "total_tokens") ?? result.TotalTokens; result.CompletionTokens = GetIntegerProperty(usage, "completion_tokens") ?? result.CompletionTokens; result.ReasoningTokens = GetIntegerProperty(usage, "reasoning_tokens") ?? result.ReasoningTokens; } // 提取 error(如果存在) if (doc.RootElement.TryGetProperty("error", out var error) && error.ValueKind == JsonValueKind.Object) { if (error.TryGetProperty("message", out var message)) { if (message.GetString() is string errorMessage && !string.IsNullOrWhiteSpace(errorMessage)) { onChunk?.Invoke(new AIChunk { Type = AIChunkType.Error, Text = errorMessage }); } } } // 提取内容 if (doc.RootElement.TryGetProperty("choices", out var choices) && choices.ValueKind == JsonValueKind.Array && choices.GetArrayLength() > 0) { var choice = choices[0]; // stream 模式使用 delta,非 stream 模式使用 message var contentObj = isStream ? choice.GetProperty("delta") : choice.GetProperty("message"); ExtractContentFromObject(contentObj, fullBuilder, onChunk); } } /// /// 从 delta 或 message 对象中提取 content 和 reasoning_content /// private static void ExtractContentFromObject( JsonElement contentObj, StringBuilder fullBuilder, Action onChunk) { // 普通内容 if (contentObj.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 }); } } // 推理内容(可选,如 DeepSeek 等模型) if (contentObj.TryGetProperty("reasoning_content", out var reasoning)) { var text = reasoning.GetString(); if (!string.IsNullOrEmpty(text)) { onChunk?.Invoke(new AIChunk { Type = AIChunkType.Reasoning, Text = text }); } } } private static Dictionary ProcessRequestParams( State state, Dictionary inputRequestParams) { return new Dictionary(inputRequestParams) { ["messages"] = state.Messages.ToArray(), ["stream"] = true, ["stream_options"] = new { include_usage = true } }; } 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; } } }