From b4288aab68b390d42fe7b3731970194049102594 Mon Sep 17 00:00:00 2001 From: a16239438 <953678589@qq.com> Date: Thu, 8 Oct 2026 12:00:39 +0800 Subject: [PATCH] Add RyougiMio duty scripts --- .../(M10S)AAC Heavyweight M2 (Savage).cs | 753 ++++ .../(M11S)AAC Heavyweight M3 (Savage).cs | 2394 +++++++++++ a16239438/(M9S)AAC Heavyweight M1 (Savage).cs | 912 +++++ ...52\345\212\250\347\247\273\345\212\250.cs" | 2816 +++++++++++++ ...52\345\212\250\347\247\273\345\212\250.cs" | 3610 +++++++++++++++++ ...07\350\260\255)Another Merchant's Tale.cs" | 1073 +++++ ...233\346\210\230 Hell on Rails(Extreme).cs" | 567 +++ 7 files changed, 12125 insertions(+) create mode 100644 a16239438/(M10S)AAC Heavyweight M2 (Savage).cs create mode 100644 a16239438/(M11S)AAC Heavyweight M3 (Savage).cs create mode 100644 a16239438/(M9S)AAC Heavyweight M1 (Savage).cs create mode 100644 "a16239438/(\345\246\226\346\230\237\344\271\261\350\210\236\347\273\235\345\242\203\346\210\230)P2 2222\346\214\207\350\267\257&\350\207\252\345\212\250\347\247\273\345\212\250.cs" create mode 100644 "a16239438/(\345\246\226\346\230\237\344\271\261\350\210\236\347\273\235\345\242\203\346\210\230)P4 \346\214\207\350\267\257&\350\207\252\345\212\250\347\247\273\345\212\250.cs" create mode 100644 "a16239438/(\345\274\202\351\227\273\345\225\206\346\227\205\345\245\207\350\260\255)Another Merchant's Tale.cs" create mode 100644 "a16239438/\346\240\274\350\216\261\346\235\250\346\213\211\346\263\242\345\260\224\346\255\274\346\256\233\346\210\230 Hell on Rails(Extreme).cs" diff --git a/a16239438/(M10S)AAC Heavyweight M2 (Savage).cs b/a16239438/(M10S)AAC Heavyweight M2 (Savage).cs new file mode 100644 index 0000000..261cfc1 --- /dev/null +++ b/a16239438/(M10S)AAC Heavyweight M2 (Savage).cs @@ -0,0 +1,753 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Numerics; +using System.Threading.Tasks; +using KodakkuAssist.Script; +using KodakkuAssist.Module.GameEvent; +using KodakkuAssist.Module.Draw; +using KodakkuAssist.Module.GameEvent.Struct; +using Dalamud.Utility.Numerics; +using System.Collections.Concurrent; + +using KodakkuAssist.Data; +using KodakkuAssist.Extensions; + +namespace RyougiMioScriptNamespace +{ + [ScriptType(name: "(M10S)AAC Heavyweight M2 (Savage)", territorys: [1322, 1323], guid: "d244ff34-5a89-41e4-a937-7d5c7c6b1348", version: "0.1.0.1", author: "RyougiMio", note: "M10S,脚本同时在M10N/S中生效")] + public class RyougiMio_1323 + { + #region Settings + // ==================== 用户设置区域 ==================== + [UserSetting("是否开启屏幕文字提示")] + public bool EnableText { get; set; } = true; + [UserSetting("是否开启TTS语音提示")] + public bool EnableTTS { get; set; } = true; + + [UserSetting("常用危险色")] + public ScriptColor DangerColor { get; set; } = new ScriptColor() { V4 = new Vector4(1.0f, 0.0f, 0.0f, 0.01f) }; + [UserSetting("常用安全色")] + public ScriptColor SafeColor { get; set; } = new ScriptColor() { V4 = new Vector4(0.0f, 1.0f, 0.0f, 0.01f) }; + [UserSetting("水蓝色")] + public ScriptColor _waterColor { get; set; } = new ScriptColor() { V4 = new Vector4(0f, 1f, 1f, 0.5f) }; // 默认淡蓝 + + + [UserSetting("指路/引导颜色 (默认为青)")] + public ScriptColor GuideColor { get; set; } = new ScriptColor() { V4 = new Vector4(0.0f, 1.0f, 1.0f, 0.01f) }; + #endregion + + #region Variables + // M10S 火浪回切:存储点名目标与时间 {(ID, Time)} + private List<(uint Id, DateTime Time)> _fireWaveMarkList = new(); + private ScriptAccessory _acc; + private static int _waterMechanicParam = 0; + // ========================================================= + // 辅助:坐标映射 + // ========================================================= + + + + #endregion + + #region Methods + // 封装:画死刑圆 (自动绑玩家) + private void DrawCircle(ScriptAccessory accessory, string name, Vector3 pos, Vector4 color, int duration) + { + var dp = accessory.Data.GetDefaultDrawProperties(); + dp.Name = name; + dp.Scale = new Vector2(6f); + dp.Color = color; + dp.DestoryAt = duration; + + dp.Position = pos; // 搜索基准点 + dp.CentreResolvePattern = PositionResolvePatternEnum.PlayerNearestOrder; // 圆心=最近玩家 + dp.CentreOrderIndex = 1; + + accessory.Method.SendDraw(DrawModeEnum.Default, DrawTypeEnum.Circle, dp); + } + + // 封装:画扇形 (格子指向玩家) + private void DrawFan(ScriptAccessory accessory, string name, Vector3 pos, Vector4 color, int duration, uint orderIndex) + { + var dp = accessory.Data.GetDefaultDrawProperties(); + dp.Name = name; + dp.Scale = new Vector2(60f); + dp.Radian = float.Pi / 4; + dp.Color = color; + dp.DestoryAt = duration; + + dp.Position = pos; // 扇形根部在格子 + dp.TargetPosition = pos; // 搜索基准点也在格子 (找离格子最近的人) + + dp.TargetResolvePattern = PositionResolvePatternEnum.PlayerNearestOrder; // 目标=最近玩家 + dp.TargetOrderIndex = orderIndex; // 第几个最近 + + accessory.Method.SendDraw(DrawModeEnum.Default, DrawTypeEnum.Fan, dp); + } + + + // 自定义TTS方法:自动检查 EnableTTS 开关 + private void QTTS(string text, int rate = 0) + { + if (!EnableTTS) return; + _acc.Method.TTS(text, rate); + } + // 自定义文字提示方法:自动检查 EnableText 开关 + private void QText(string text, int duration, bool isWarning = false) + { + if (!EnableText) return; + _acc.Method.TextInfo(text, duration, isWarning); + } + + + + + #endregion + + #region Initialization + + public void Init(ScriptAccessory accessory) + { + accessory.Method.RemoveDraw(".*"); + _acc = accessory; + _waterMechanicParam = 0; + + + lock (_fireWaveMarkList) + { + _fireWaveMarkList.Clear(); + } + + accessory.Method.SendChat("/e M10S Initialized."); + } + + #endregion + #region TTSonly + [ScriptMethod(name: "斗志昂扬TTS", eventType: EventTypeEnum.StartCasting, eventCondition: ["ActionId:regex:^(46466|46520|46467|46521)$"])] + public void Elemental_AOE_Alert(Event @event, ScriptAccessory accessory) + { + if (!uint.TryParse(@event["ActionId"], out var aid)) return; + // 46466(N), 46520(S) -> 火 AOE + if (aid == 46466 || aid == 46520) + { + QTTS("火AOE"); + QText("火 AOE", 5000, true); + } + // 46467(N), 46521(S) -> 水 AOE + else if (aid == 46467 || aid == 46521) + { + QTTS("水AOE"); + QText("水 AOE", 5000, true); + } + } + [ScriptMethod(name: "浪花飞溅_浪涛翻涌TTS(猜测)", eventType: EventTypeEnum.StartCasting, eventCondition: ["ActionId:regex:^(46543|46544|46551|46552)$"])] + public void SpreadStack_Alert(Event @event, ScriptAccessory accessory) + { + if (!uint.TryParse(@event["ActionId"], out var aid)) return; + + if (aid == 46543 || aid == 46551) + { + QTTS("分散"); + QText("分散", 5000, true); + } + else if (aid == 46544 || aid == 46552) + { + QTTS("分摊"); + QText("分摊", 5000, true); + } + } + [ScriptMethod(name: "双重旋水_交错旋水TTS", eventType: EventTypeEnum.StartCasting, eventCondition: ["ActionId:regex:^(46557|46560)$"])] + public void SpreadStack_Alert1(Event @event, ScriptAccessory accessory) + { + if (!uint.TryParse(@event["ActionId"], out var aid)) return; + + if (aid == 46557 || aid == 46551) + { + QTTS("水波后躲避"); + QText("水波后躲避", 5000, true); + } + else if (aid == 46560 || aid == 46552) + { + QTTS("水波后不动"); + QText("水波后不动", 5000, true); + } + } + [ScriptMethod(name: "浪尖转体TTS(猜测)", eventType: EventTypeEnum.StartCasting, eventCondition: ["ActionId:regex:^(47249|47250)$"])] + public void DelayedMechanic_Alert(Event @event, ScriptAccessory accessory) + { + if (!uint.TryParse(@event["ActionId"], out var aid)) return; + + if (aid == 47249) + { + QTTS("稍后扇形"); + QText("稍后扇形", 5000, true); + } + else if (aid == 47250) + { + QTTS("稍后击退"); + QText("稍后击退", 5000, true); + } + } + [ScriptMethod(name: "炽焰冲击_深海冲击TTS", eventType: EventTypeEnum.StartCasting, eventCondition: ["ActionId:regex:^(46464|46518|46465|46519)$"])] + public void TankBuster_Alert(Event @event, ScriptAccessory accessory) + { + if (!uint.TryParse(@event["ActionId"], out var aid)) return; + + // 46464(N), 46518(S) -> 分摊死刑 + if (aid == 46464 || aid == 46518) + { + QTTS("分摊死刑"); + QText("分摊死刑", 5000, true); + } + // 46465(N), 46519(S) -> 单体死刑 + else if (aid == 46465 || aid == 46519) + { + QTTS("死刑"); + QText("死刑", 5000, true); + } + } + [ScriptMethod(name: "极限炫技TTS", eventType: EventTypeEnum.StartCasting, eventCondition: ["ActionId:regex:^(46499|46500)$"])] + public void LimitCut_AOE_Alert(Event @event, ScriptAccessory accessory) + { + // 46499(N), 46500(S) + QTTS("连续AOE"); + QText("连续AOE", 5000, true); + } + [ScriptMethod(name: "腾水踏浪_腾火踏浪TTS(猜测)", eventType: EventTypeEnum.StartCasting, eventCondition: ["ActionId:regex:^(46532|46563)$"])] + public void JumpAndTower_Alert(Event @event, ScriptAccessory accessory) + { + if (!uint.TryParse(@event["ActionId"], out var aid)) return; + + // 46532 -> 炽红4连跳 + if (aid == 46532) + { + QTTS("炽红4连跳"); + QText("炽红4连跳", 3000, true); + } + // 46563 -> 双人塔 + else if (aid == 46563) + { + QTTS("双人塔"); + QText("双人塔", 3000, true); + } + } + + [ScriptMethod(name: "深蓝分散分摊", eventType: EventTypeEnum.StatusAdd, eventCondition: ["StatusID:2056"])] + public void Status_2056_Add(Event @event, ScriptAccessory accessory) + { + // 1. 获取 StackCount + if (!int.TryParse(@event["Param"], out var stack)) return; + + // 2. 仅处理机制预告 (1005-1008) + if (stack == 1007 || stack == 1005 || stack == 1008 || stack == 1006) + { + // 记录到全局变量 + _waterMechanicParam = stack; + + // 3. 根据 StackCount 触发 TTS 和 文字提示 + // 1007: 全队分摊 + // 1005: 双组分摊 + // 1008: 两人分摊 + // 1006: 全体分散 + + if (stack == 1007) + { + QTTS("全队分摊"); + QText("全队分摊", 5000, true); + } + else if (stack == 1005) + { + QTTS("双组分摊"); + QText("双组分摊", 5000, true); + } + else if (stack == 1008) + { + QTTS("水组分散"); + QText("水组分散", 5000, true); + } + else if (stack == 1006) + { + QTTS("全体分散"); + QText("全体分散", 5000, true); + } + } + } + #endregion + + #region 火浪回切 + [ScriptMethod(name: "M10S:火浪回切点名记录", eventType: EventTypeEnum.TargetIcon, eventCondition: ["Id:regex:^(0298)$"], userControl: false)] + public void FireWave_MarkRecord(Event @event, ScriptAccessory accessory) + { + var idStr = @event["TargetId"]; + if (string.IsNullOrEmpty(idStr)) return; + if (!uint.TryParse(idStr.Replace("0x", ""), System.Globalization.NumberStyles.HexNumber, null, out var tid)) return; + lock (_fireWaveMarkList) + { + _fireWaveMarkList.Add((tid, DateTime.Now)); + } + } + + [ScriptMethod(name: "火浪回切", eventType: EventTypeEnum.StartCasting, eventCondition: ["ActionId:regex:^(46478|46537)$"])] + public void FireWave_Draw(Event @event, ScriptAccessory accessory) + { + // 1. 解析 SourceId (Boss ID) + var srcIdStr = @event["SourceId"]; + if (string.IsNullOrEmpty(srcIdStr)) return; + if (!uint.TryParse(srcIdStr.Replace("0x", ""), System.Globalization.NumberStyles.HexNumber, null, out var sid)) return; + // 2. 解析 ActionId + if (!uint.TryParse(@event["ActionId"], out var aid)) return; + // 3. 解析 Duration + if (!int.TryParse(@event["DurationMilliseconds"], out var dur)) return; + // 获取 Boss 位置 + var boss = accessory.Data.Objects.SearchById(sid); + if (boss == null) return; + var spos = boss.Position; + var now = DateTime.Now; + float fanAngle = (aid == 46478) ? float.Pi / 3 : float.Pi * 2 * 330 / 360; + lock (_fireWaveMarkList) + { + // 清理 10秒 以前的过期数据 + _fireWaveMarkList.RemoveAll(x => (now - x.Time).TotalSeconds > 10); + foreach (var mark in _fireWaveMarkList) + { + var tObj = accessory.Data.Objects.SearchById(mark.Id); + if (tObj == null) continue; + var dp = accessory.Data.GetDefaultDrawProperties(); + dp.Name = $"火浪回切-{mark.Id}-{aid}"; + dp.Scale = new Vector2(60f); + dp.Radian = fanAngle; + dp.Owner = sid; + dp.TargetObject = mark.Id; + dp.Rotation = 0; + dp.Color = accessory.Data.DefaultDangerColor; + dp.DestoryAt = dur; + dp.ScaleMode = ScaleMode.ByTime; + + accessory.Method.SendDraw(DrawModeEnum.Default, DrawTypeEnum.Fan, dp); + } + } + } + + #endregion + + + + + #region 瞎猜环节 + [ScriptMethod(name: "浪尖转体(猜测)", eventType: EventTypeEnum.StartCasting, eventCondition: ["ActionId:regex:^(46547|46488|46550)$"])] + public void FanAndKnockback_Draw(Event @event, ScriptAccessory accessory) + { + if (!uint.TryParse(@event["ActionId"], out var aid)) return; + if (!int.TryParse(@event["DurationMilliseconds"], out var dur)) return; + + var dp = accessory.Data.GetDefaultDrawProperties(); + dp.Name = $"Draw_{aid}"; + dp.Position = @event.SourcePosition; + dp.Rotation = @event.SourceRotation; + dp.DestoryAt = dur; + dp.Color = accessory.Data.DefaultDangerColor; + if (aid == 46547 || aid == 46488) + { + dp.Radian = float.Pi * 2 / 3; + dp.Scale = new Vector2(60f); + dp.ScaleMode = ScaleMode.ByTime; + accessory.Method.SendDraw(DrawModeEnum.Default, DrawTypeEnum.Fan, dp); + } + // 击退 (箭头) + else if (aid == 46550) + { + // 箭头 Scale: X=宽度, Y=长度 + dp.Scale = new Vector2(5f, 30f); + dp.ScaleMode = ScaleMode.YByTime; + accessory.Method.SendDraw(DrawModeEnum.Default, DrawTypeEnum.Arrow, dp); + } + } + [ScriptMethod(name: "炽焰冲击", eventType: EventTypeEnum.StartCasting, eventCondition: ["ActionId:regex:^(46518|46464)$"])] + public void Action_46518_ColorCheck(Event @event, ScriptAccessory accessory) + { + // 1. 获取持续时间 + if (!int.TryParse(@event["DurationMilliseconds"], out var duration)) return; + + // 2. 获取目标ID (StartCasting 的 TargetId 是十六进制字符串) + var tidStr = @event["TargetId"]; + if (string.IsNullOrEmpty(tidStr) || + !uint.TryParse(tidStr, System.Globalization.NumberStyles.HexNumber, null, out var tid)) + return; + + // 3. 获取自己的索引,判断自己是否为坦克 + uint myId = accessory.Data.Me; + var partyIds = accessory.Data.PartyList; + int myIndex = -1; + + for (int i = 0; i < partyIds.Count; i++) + { + if (partyIds[i] == myId) + { + myIndex = i; + break; + } + } + + // 默认小队列表顺序:0,1 是坦克 + bool amITank = (myIndex == 0 || myIndex == 1); + + // 4. 颜色逻辑 + // 如果我是T -> 绿色(安全);否则 -> 红色(危险) + var drawColor = amITank ? accessory.Data.DefaultSafeColor : accessory.Data.DefaultDangerColor; + + // 5. 绘图 + var dp = accessory.Data.GetDefaultDrawProperties(); + dp.Name = $"Action_46518_{tid}_{DateTime.Now.Ticks}"; + + dp.Owner = tid; // 绑在目标身上 + dp.Scale = new Vector2(6f); // 半径 6m + dp.Color = drawColor; // 根据职能决定的颜色 + dp.DestoryAt = duration; // 持续时间 + dp.ScaleMode = ScaleMode.ByTime; // 渐变填充 + + accessory.Method.SendDraw(DrawModeEnum.Default, DrawTypeEnum.Circle, dp); + } + [ScriptMethod(name: "交错旋水/双重旋水_修正版", eventType: EventTypeEnum.StartCasting, eventCondition: ["ActionId:regex:^(46560|46557)$"])] + public void Boss_Tracking_Fan(Event @event, ScriptAccessory accessory) + { + if (!int.TryParse(@event["DurationMilliseconds"], out var duration)) return; + float rad30 = float.Pi / 6; + + // 遍历小队列表 + var partyIds = accessory.Data.PartyList; + foreach (var tid in partyIds) + { + // 1. 先获取基础对象 (IGameObject) + var obj = accessory.Data.Objects.FirstOrDefault(x => x.EntityId == tid); + + // 2. 【核心修正】必须转换为 IBattleChara 才能访问 StatusList + if (obj is IBattleChara player) + { + // 3. 现在可以安全地检查 StatusList 了 + bool hasBuff = player.StatusList.Any(s => s.StatusId == 4974); + + // 如果有这个 Buff,跳过不画 + if (hasBuff) continue; + + // --- 绘图逻辑 --- + var dp = accessory.Data.GetDefaultDrawProperties(); + dp.Name = $"TrackFan_{@event["ActionId"]}_{tid}"; + dp.Owner = @event.SourceId; + dp.TargetObject = tid; + dp.Radian = rad30; + dp.Scale = new Vector2(60f); + dp.Color = accessory.Data.DefaultDangerColor; + dp.DestoryAt = duration; + dp.ScaleMode = ScaleMode.ByTime; + + accessory.Method.SendDraw(DrawModeEnum.Default, DrawTypeEnum.Fan, dp); + } + } + } + [ScriptMethod(name: "交错旋水/双重旋水2", eventType: EventTypeEnum.ActionEffect, eventCondition: ["ActionId:regex:^(46561|46558)$"])] + public void Action_FollowUp_Fans(Event @event, ScriptAccessory accessory) + { + if (!uint.TryParse(@event["ActionId"], out var aid)) return; + + // 1. 定义常量 + int duration = 2600; // 2.6秒 + float rad30 = float.Pi / 6; // 30度 + float rad24 = 22.5f * (float.Pi / 180f); // 24度 (偏移量) + float rad15 = 15f * (float.Pi / 180f); // 15度 (扇形大小) + + // 2. 获取 Boss 当时的位置和朝向 + var pos = @event.SourcePosition; + var rot = @event.SourceRotation; + + if (aid == 46558) + { + // --- 情况 A: 46558 (保持原方向 30度) --- + var dp = accessory.Data.GetDefaultDrawProperties(); + dp.Name = $"Fan_46558_{DateTime.Now.Ticks}"; + + dp.Position = pos; + dp.Rotation = rot; // 保持原方向 + dp.Radian = rad30; // 30度扇形 + + dp.Scale = new Vector2(60f); // 半径 60m + dp.Color = accessory.Data.DefaultDangerColor; + dp.DestoryAt = duration; + dp.ScaleMode = ScaleMode.ByTime; // 渐变 + + accessory.Method.SendDraw(DrawModeEnum.Default, DrawTypeEnum.Fan, dp); + } + else if (aid == 46561) + { + // --- 情况 B: 46561 (左右偏转 24度,各画一个 15度扇形) --- + + // 左侧扇形 (+24度) + var dpLeft = accessory.Data.GetDefaultDrawProperties(); + dpLeft.Name = $"Fan_46561_Left_{DateTime.Now.Ticks}"; + dpLeft.Position = pos; + dpLeft.Rotation = rot + rad24; // 向左偏 24度 + dpLeft.Radian = rad15; // 15度扇形 + dpLeft.Scale = new Vector2(60f); + dpLeft.Color = accessory.Data.DefaultDangerColor; + dpLeft.DestoryAt = duration; + dpLeft.ScaleMode = ScaleMode.ByTime; + accessory.Method.SendDraw(DrawModeEnum.Default, DrawTypeEnum.Fan, dpLeft); + + // 右侧扇形 (-24度) + var dpRight = accessory.Data.GetDefaultDrawProperties(); + dpRight.Name = $"Fan_46561_Right_{DateTime.Now.Ticks}"; + dpRight.Position = pos; + dpRight.Rotation = rot - rad24; // 向右偏 24度 + dpRight.Radian = rad15; // 15度扇形 + dpRight.Scale = new Vector2(60f); + dpRight.Color = accessory.Data.DefaultDangerColor; + dpRight.DestoryAt = duration; + dpRight.ScaleMode = ScaleMode.ByTime; + accessory.Method.SendDraw(DrawModeEnum.Default, DrawTypeEnum.Fan, dpRight); + } + } + [ScriptMethod(name: "EnvControl15-21", eventType: EventTypeEnum.EnvControl, eventCondition: ["Index:regex:^\\d+$"])] + public void EnvControl_Grid_Mechanic(Event @event, ScriptAccessory accessory) + { + if (!int.TryParse(@event["Index"], out var index)) return; + if (index < 14 || index > 22) return; + if (!int.TryParse(@event["Flag"], out var flag)) return; + + Vector3 gridPos = index switch + { + 14 => new Vector3(87f, 0f, 87f), + 15 => new Vector3(100f, 0f, 87f), + 16 => new Vector3(113f, 0f, 87f), + 17 => new Vector3(87f, 0f, 100f), + 18 => new Vector3(100f, 0f, 100f), + 19 => new Vector3(113f, 0f, 100f), + 20 => new Vector3(87f, 0f, 113f), + 21 => new Vector3(100f, 0f, 113f), + 22 => new Vector3(113f, 0f, 113f), + _ => Vector3.Zero + }; + + string baseName = $"Grid_{index}"; + + // ========================================================= + // 【修正】只有 Flag 是 4 或 8 时,才执行清除逻辑 + // ========================================================= + if (flag == 4 || flag == 8) + { + accessory.Method.RemoveDraw($"{baseName}_TB"); + accessory.Method.RemoveDraw($"{baseName}_Stack"); + for (int i = 1; i <= 4; i++) accessory.Method.RemoveDraw($"{baseName}_Spread_{i}"); + return; // 清除完直接退出 + } + + // ========================================================= + // 下面是画图逻辑 (只有 Flag 为机制值时才执行) + // ========================================================= + + bool isWater = (flag == 2 || flag == 32 || flag == 128); + // 直接用变量,不重复 GetUserSetting + Vector4 drawColor = isWater ? _waterColor.V4 : accessory.Data.DefaultDangerColor; + + // 1. 死刑 (圆) + if (flag == 128 || flag == 8192) + { + // 保险措施:虽然不是4/8,但为了防止同一格子机制切换产生重叠,画新图前先把旧的同名图顶掉 + // (Kodakku底层会自动替换同名绘图,所以这里不需要手动Remove同名的,直接Send即可) + + var dp = accessory.Data.GetDefaultDrawProperties(); + dp.Name = $"{baseName}_TB"; + dp.Scale = new Vector2(6f); + dp.Color = drawColor; + dp.DestoryAt = 9999999; + dp.ScaleMode = ScaleMode.None; + + dp.Position = gridPos; + dp.CentreResolvePattern = PositionResolvePatternEnum.PlayerNearestOrder; + dp.CentreOrderIndex = 1; + + accessory.Method.SendDraw(DrawModeEnum.Default, DrawTypeEnum.Circle, dp); + } + // 2. 分摊 (扇形) + else if (flag == 32 || flag == 2048) + { + var dp = accessory.Data.GetDefaultDrawProperties(); + dp.Name = $"{baseName}_Stack"; + dp.Scale = new Vector2(60f); + dp.Radian = float.Pi / 4; + dp.Color = drawColor; + dp.DestoryAt = 9999999; + dp.ScaleMode = ScaleMode.None; + + dp.Position = gridPos; + dp.TargetResolvePattern = PositionResolvePatternEnum.PlayerNearestOrder; + dp.TargetOrderIndex = 1; + + accessory.Method.SendDraw(DrawModeEnum.Default, DrawTypeEnum.Fan, dp); + } + // 3. 分散 (扇形 x 4) + else if (flag == 2 || flag == 512) + { + for (uint i = 1; i <= 4; i++) + { + var dp = accessory.Data.GetDefaultDrawProperties(); + dp.Name = $"{baseName}_Spread_{i}"; + dp.Scale = new Vector2(60f); + dp.Radian = float.Pi / 4; + dp.Color = drawColor; + dp.DestoryAt = 9999999; + dp.ScaleMode = ScaleMode.None; + + dp.Position = gridPos; + dp.TargetResolvePattern = PositionResolvePatternEnum.PlayerNearestOrder; + dp.TargetOrderIndex = i; + + accessory.Method.SendDraw(DrawModeEnum.Default, DrawTypeEnum.Fan, dp); + } + } + } + [ScriptMethod(name: "Env_2-5", eventType: EventTypeEnum.EnvControl, eventCondition: ["Index:regex:^[2-5]$"])] + public void Env_Buff_Draw(Event @event, ScriptAccessory accessory) + { + // 1. 解析 Index (2, 3, 4, 5) + if (!int.TryParse(@event["Index"], out var index)) return; + if (index < 2 || index > 5) return; + + // 2. 解析 Flag + if (!int.TryParse(@event["Flag"], out var flag)) return; + + // 定义常量 + uint buffId = 4975; + int duration = 10000; // 10秒 + + // ========================================================= + // 情况 A: Flag 128 -> 检测【自己】身上有没有 Buff -> 画安全圆 + // ========================================================= + if (flag == 128) + { + var myId = accessory.Data.Me; + // 获取自己的对象 + var myObj = accessory.Data.Objects.FirstOrDefault(x => x.EntityId == myId); + + // 转类型查 Buff + if (myObj is IBattleChara me) + { + if (me.StatusList.Any(s => s.StatusId == buffId)) + { + var dp = accessory.Data.GetDefaultDrawProperties(); + dp.Name = $"Safe_{index}_{myId}"; + dp.Owner = myId; // 绑在自己身上 + dp.Scale = new Vector2(6f); // 6m + dp.Color = accessory.Data.DefaultSafeColor; // 安全色 + dp.DestoryAt = duration; + dp.ScaleMode = ScaleMode.ByTime; // 渐变 + + accessory.Method.SendDraw(DrawModeEnum.Default, DrawTypeEnum.Circle, dp); + } + } + } + // ========================================================= + // 情况 B: Flag 2048 -> 检测【所有人】身上有没有 Buff -> 画危险圆 + // ========================================================= + else if (flag == 2048) + { + var partyIds = accessory.Data.PartyList; + + foreach (var tid in partyIds) + { + var obj = accessory.Data.Objects.FirstOrDefault(x => x.EntityId == tid); + + // 转类型查 Buff + if (obj is IBattleChara player) + { + if (player.StatusList.Any(s => s.StatusId == buffId)) + { + var dp = accessory.Data.GetDefaultDrawProperties(); + dp.Name = $"Danger_{index}_{tid}"; + dp.Owner = tid; // 绑在该玩家身上 + dp.Scale = new Vector2(5f); // 5m + dp.Color = accessory.Data.DefaultDangerColor; // 危险色 + dp.DestoryAt = duration; + dp.ScaleMode = ScaleMode.ByTime; // 渐变 + + accessory.Method.SendDraw(DrawModeEnum.Default, DrawTypeEnum.Circle, dp); + } + } + } + } + } + [ScriptMethod(name: "混合爆炸", eventType: EventTypeEnum.StartCasting, eventCondition: ["ActionId:46587"])] + public void Action_46587_Circle(Event @event, ScriptAccessory accessory) + { + // 1. 获取技能持续时间 + if (!int.TryParse(@event["DurationMilliseconds"], out var duration)) return; + + var dp = accessory.Data.GetDefaultDrawProperties(); + // 使用唯一名字防止冲突 + dp.Name = $"Circle_46587_{@event.SourceId}_{DateTime.Now.Ticks}"; + + // 2. 位置:SourcePosition (施法者脚下) + dp.Position = @event.SourcePosition; + + // 3. 大小:9m 圆 + dp.Scale = new Vector2(9f); + + // 4. 样式:危险色、渐变填充 + dp.Color = accessory.Data.DefaultDangerColor; + dp.ScaleMode = ScaleMode.ByTime; // 渐变 + dp.DestoryAt = duration; + + accessory.Method.SendDraw(DrawModeEnum.Default, DrawTypeEnum.Circle, dp); + } + [ScriptMethod(name: "连线矩形_Rect版", eventType: EventTypeEnum.TargetIcon, eventCondition: ["Id:regex:^(027B|027C)$"])] + public void Draw_Link_Rect(Event @event, ScriptAccessory accessory) + { + + + + string iconId = @event["Id"]; + uint targetPlayerId = (uint)@event.TargetId; // 强转 uint + + // A. 确定要找哪个 NPC (DataId) + uint searchDataId = 0; + if (iconId == "027B") searchDataId = 19288; + else if (iconId == "027C") searchDataId = 19287; + + if (searchDataId == 0) return; + + // B. 查找 NPC + var sourceNpc = accessory.Data.Objects.FirstOrDefault(x => x.DataId == searchDataId); + if (sourceNpc == null) return; + + // C. 确定颜色 + // 027B -> 水色, 027C -> 危险红 + Vector4 drawColor = (iconId == "027B") ? _waterColor.V4 : accessory.Data.DefaultDangerColor; + + // D. 绘图逻辑 + var dp = accessory.Data.GetDefaultDrawProperties(); + dp.Name = $"Link_Rect_{iconId}_{targetPlayerId}_{DateTime.Now.Ticks}"; + + // 起点:NPC + dp.Owner = sourceNpc.EntityId; + + // 终点:玩家 (矩形会自动旋转朝向玩家) + dp.TargetObject = targetPlayerId; + + // 尺寸:X=宽度(8), Y=长度(初始给1就行,会被ScaleMode覆盖) + dp.Scale = new Vector2(8f, 1f); + + dp.Color = drawColor; + dp.DestoryAt = 4700; // 4.7s + + // 【核心关键】 + // 使用 Rect + YByDistance + // 效果:画一个矩形,宽度固定为8,长度自动拉伸直到填满 NPC 到 玩家 的距离 + dp.ScaleMode = ScaleMode.YByDistance; + + accessory.Method.SendDraw(DrawModeEnum.Default, DrawTypeEnum.Rect, dp); + } + + #endregion + + + } +} \ No newline at end of file diff --git a/a16239438/(M11S)AAC Heavyweight M3 (Savage).cs b/a16239438/(M11S)AAC Heavyweight M3 (Savage).cs new file mode 100644 index 0000000..64d5838 --- /dev/null +++ b/a16239438/(M11S)AAC Heavyweight M3 (Savage).cs @@ -0,0 +1,2394 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Numerics; +using System.Threading.Tasks; +using KodakkuAssist.Script; +using KodakkuAssist.Module.GameEvent; +using KodakkuAssist.Module.Draw; +using KodakkuAssist.Module.GameEvent.Struct; +using Dalamud.Utility.Numerics; + +using KodakkuAssist.Data; +using KodakkuAssist.Extensions; + +namespace RyougiMioScriptNamespace +{ + [ScriptType(name: "(M11S)AAC Heavyweight M3 (Savage)", territorys: [1324, 1325], guid: "725bcd38-1173-420e-a248-b3e11a1ff1b3", version: "0.1.5.0", author: "RyougiMio", note: "M11S,脚本同时在M11N/S中生效。")] + public class RyougiMio_1325 + { + #region Settings + // ==================== 用户设置区域 ==================== + [UserSetting("是否开启屏幕文字提示")] + public bool EnableText { get; set; } = true; + [UserSetting("是否开启TTS语音提示")] + public bool EnableTTS { get; set; } = true; + + [UserSetting("常用危险色")] + public ScriptColor DangerColor { get; set; } = new ScriptColor() { V4 = new Vector4(1.0f, 0.0f, 0.0f, 0.01f) }; + [UserSetting("常用安全色")] + public ScriptColor SafeColor { get; set; } = new ScriptColor() { V4 = new Vector4(0.0f, 1.0f, 0.0f, 0.01f) }; + [UserSetting("陨石颜色")] + public ScriptColor SafeColor1 { get; set; } = new ScriptColor() { V4 = new Vector4(0.0f, 0.0f, 0.6f, 1.0f) }; + + [UserSetting("指路/引导颜色 (默认为青)")] + public ScriptColor GuideColor { get; set; } = new ScriptColor() { V4 = new Vector4(0.0f, 1.0f, 1.0f, 0.01f) }; + #endregion + + #region Variables + // 定义一个类用来存物体信�? + public class ObjectState + { + public uint DataId; + public Vector3 Position; + public float Rotation; + public int GroupId; // 【新增】直接存储它�?1, 2 还是 3 �? + } + public class ObjectStateSix + { + public uint DataId; + public Vector3 Position; + public float Rotation; + public int GroupId; + public int Index; + public bool IsDrawn; // 【新增】标记是否已经画�? + } + private ScriptAccessory _acc; + + + [UserSetting("P4踩塔 攻略")] + public Phase4_Towers Phase4_Towers1 { get; set; } = Phase4_Towers.全引导; + + public enum Phase4_Towers + { + 全引导, + 近战顺远程逆 + } + + [UserSetting("铸兵之令:统治 指路配置")] + public DominionGuidance DominionGuidance1 { get; set; } = DominionGuidance.标准22分摊; + + public enum DominionGuidance + { + 标准22分摊, + 近战固定分摊 + } + + #endregion + + #region Methods + + + // 自定义TTS方法:自动检�?EnableTTS 开�? + private void QTTS(string text, int rate = 0) + { + if (!EnableTTS) return; + _acc.Method.TTS(text, rate); + } + // 自定义文字提示方法:自动检�?EnableText 开�? + private void QText(string text, int duration, bool isWarning = false) + { + if (!EnableText) return; + _acc.Method.TextInfo(text, duration, isWarning); + } + // 1. 定义存储�?(Key: SourceId, Value: 物体状�? + private Dictionary _objStorage = new Dictionary(); + // 【修改】字典类型也跟着�? + private Dictionary _objStorage1 = new Dictionary(); + private int _setPosCount = 0; + // 全局计数器,用于记录是第几个出现�? + private int _orderCounter = 0; + // 【新增】记�?47086 机制的开始时�? + private long _mechanic47086StartTime = 0; + // 在类成员变量区域定义计数器,用于记录该技能出现了多少�? + private int _castCount_46131 = 0; + private bool _hasCast46148 = false; + // 存储被点�?001E 的玩�?ID + private HashSet _markedPlayers = new HashSet(); + + // 存储读条物体的列�? + private List _castingObjects = new List(); + // 默认 false (没读�? + private bool _hasCast46162 = false; + // 变量定义区域添加 + private Dictionary _tether0039DrawnTime = new Dictionary(); + private HashSet _targetIcon001EPlayers = new HashSet(); + private List<(uint SourceId, uint ActionId, int Quadrant)> _castingObjects46166_46167 = new List<(uint, uint, int)>(); + private HashSet _dominion46112Regions = new HashSet(); + private long _dominion46112LastEventTicks = 0; + private long _dominion46112LastDrawTicks = 0; + private long _starTrackLastCastTicks = 0; + private Dictionary _starTrackLastCastBySource + = new Dictionary(); + private long _starTrackFirstCastTicks = 0; + private HashSet _starTrackFirstBlocks = new HashSet(); + // 定义物体结构 + private class MechanicObject + { + public uint ActionId; // 46166 �?46167 + public uint SourceId; + public int Quadrant; // 1, 2, 3, 4 + public int Duration; + } + // --- 坐标定义 (忽略Y�? --- + // Group 1 (0 ~ pi/2) 对应的坐�? + private readonly List _group1Coords = new List + { + new Vector2(103.11f, 111.59f), + new Vector2(111.59f, 103.11f) + }; + + // Group 2 (pi/2 ~ pi & -3pi/4 ~ -pi) 对应的坐�? + private readonly List _group2Coords = new List + { + new Vector2(108.49f, 91.51f), + new Vector2(96.89f, 88.41f) + }; + + // Group 3 (0 ~ -3pi/4) 对应的坐�? + private readonly List _group3Coords = new List + { + new Vector2(88.41f, 96.90f), + new Vector2(91.51f, 108.49f) + }; + // 固定�?1-3-2 循环顺序 + private readonly int[] _fixedSequence = new int[] { 1, 3, 2 }; + + // 合并所有合法坐标用�?SetObjPos 校验 + private List _allValidCoords; + private readonly List _validCoords = new List + { + new Vector2(101.42f, 112.14f), new Vector2(106.00f, 110.39f), + new Vector2(110.60f, 100.12f), new Vector2(108.92f, 99.15f), + new Vector2(110.40f, 93.97f), new Vector2(100.00f, 87.97f), + new Vector2(89.60f, 93.97f), new Vector2(91.08f, 99.15f), + new Vector2(89.40f, 100.12f), new Vector2(94.00f, 110.39f), + new Vector2(98.58f, 112.14f) + }; + + // 定义 DataId 集合 + private readonly HashSet _targetDataIds = new HashSet { 19184, 19185, 19186 }; + + // --- E. 绘图辅助方法 --- + private void DrawMechanic(ObjectState obj, uint objId, int delay, int duration, uint bossId, ScriptAccessory accessory) + { + string baseName = $"Triple_{obj.DataId}_{delay}_{DateTime.Now.Ticks}"; + + // 1. 物体范围 + if (obj.DataId == 19184) // 钢铁 + { + var dp = accessory.Data.GetDefaultDrawProperties(); + dp.Name = baseName + "_Iron_Obj"; + dp.Position = obj.Position; + dp.Scale = new Vector2(8f); + dp.Color = accessory.Data.DefaultDangerColor; + dp.Delay = delay; dp.DestoryAt = duration; dp.ScaleMode = ScaleMode.ByTime; + accessory.Method.SendDraw(DrawModeEnum.Default, DrawTypeEnum.Circle, dp); + } + else if (obj.DataId == 19185) // 月环 + { + var dp = accessory.Data.GetDefaultDrawProperties(); + dp.Name = baseName + "_Moon_Obj"; + dp.Position = obj.Position; + dp.Scale = new Vector2(60f); dp.InnerScale = new Vector2(5f); + dp.Radian = float.Pi * 2; + dp.Color = accessory.Data.DefaultDangerColor; + dp.Delay = delay; dp.DestoryAt = duration; dp.ScaleMode = ScaleMode.ByTime; + accessory.Method.SendDraw(DrawModeEnum.Default, DrawTypeEnum.Donut, dp); + } + else if (obj.DataId == 19186) // 十字 + { + for (int k = 0; k < 4; k++) + { + var dp = accessory.Data.GetDefaultDrawProperties(); + dp.Name = $"{baseName}_Cross_Obj_{k}"; + dp.Position = obj.Position; + dp.Rotation = obj.Rotation + (float)(Math.PI / 2 * k); + dp.Scale = new Vector2(10f, 40f); + dp.Color = accessory.Data.DefaultDangerColor; + dp.Delay = delay; dp.DestoryAt = duration; dp.ScaleMode = ScaleMode.YByTime; + accessory.Method.SendDraw(DrawModeEnum.Default, DrawTypeEnum.Rect, dp); + } + } + + // 2. 玩家机制 + if (obj.DataId == 19184) // 钢铁 -> 玩家�? + { + var dp = accessory.Data.GetDefaultDrawProperties(); + dp.Name = baseName + "_Iron_Player"; + dp.Owner = accessory.Data.Me; + dp.Scale = new Vector2(6f); + dp.Color = GuideColor.V4; + dp.Delay = delay; dp.DestoryAt = duration; dp.ScaleMode = ScaleMode.ByTime; + accessory.Method.SendDraw(DrawModeEnum.Default, DrawTypeEnum.Circle, dp); + } + else if (obj.DataId == 19185) // 月环 -> 扇形 + { + var party = accessory.Data.PartyList; + foreach (var tid in party) + { + var dp = accessory.Data.GetDefaultDrawProperties(); + dp.Name = $"{baseName}_Moon_Player_{tid}"; + dp.Owner = objId; dp.TargetObject = tid; + dp.Radian = float.Pi / 6; dp.Scale = new Vector2(60f); + dp.Color = accessory.Data.DefaultDangerColor; + dp.Delay = delay; dp.DestoryAt = duration; dp.ScaleMode = ScaleMode.ByTime; + accessory.Method.SendDraw(DrawModeEnum.Default, DrawTypeEnum.Fan, dp); + } + } + else if (obj.DataId == 19186) // 十字 -> T连线 + { + var party = accessory.Data.PartyList; + for (int i = 2; i <= 3; i++) + { + if (i >= party.Count) break; + var tid = party[i]; + var dp = accessory.Data.GetDefaultDrawProperties(); + dp.Name = $"{baseName}_Cross_Tank_{tid}"; + dp.Owner = objId; dp.TargetObject = tid; + dp.Scale = new Vector2(6f); dp.ScaleMode = ScaleMode.YByDistance; + dp.Color = accessory.Data.DefaultDangerColor; + dp.Delay = delay; dp.DestoryAt = duration; + accessory.Method.SendDraw(DrawModeEnum.Default, DrawTypeEnum.Rect, dp); + } + } + } + // 【修改】参数类型改�?ObjectStateSix + private void DrawMechanic(ObjectStateSix obj, uint objId, int delay, int duration, uint bossId, ScriptAccessory accessory) + { + string baseName = $"SixCombo_{obj.DataId}_{delay}_{DateTime.Now.Ticks}"; + + // 1. 钢铁 (19184) + if (obj.DataId == 19184) + { + var dp = accessory.Data.GetDefaultDrawProperties(); + dp.Name = baseName + "_Iron_Obj"; + dp.Position = obj.Position; + dp.Scale = new Vector2(8f); + dp.Color = accessory.Data.DefaultDangerColor; + dp.Delay = delay; dp.DestoryAt = duration; dp.ScaleMode = ScaleMode.ByTime; + accessory.Method.SendDraw(DrawModeEnum.Default, DrawTypeEnum.Circle, dp); + + // 玩家安全�? + var dp2 = accessory.Data.GetDefaultDrawProperties(); + dp2.Name = baseName + "_Iron_Player"; + dp2.Owner = accessory.Data.Me; + dp2.Scale = new Vector2(6f); + dp2.Color = accessory.Data.DefaultSafeColor; // �? + dp2.Delay = delay; dp2.DestoryAt = duration; dp2.ScaleMode = ScaleMode.ByTime; + accessory.Method.SendDraw(DrawModeEnum.Default, DrawTypeEnum.Circle, dp2); + } + // 2. 月环 (19185) + else if (obj.DataId == 19185) + { + var dp = accessory.Data.GetDefaultDrawProperties(); + dp.Name = baseName + "_Moon_Obj"; + dp.Position = obj.Position; + dp.Scale = new Vector2(60f); dp.InnerScale = new Vector2(5f); + dp.Radian = float.Pi * 2; + dp.Color = accessory.Data.DefaultDangerColor; + dp.Delay = delay; dp.DestoryAt = duration; dp.ScaleMode = ScaleMode.ByTime; + accessory.Method.SendDraw(DrawModeEnum.Default, DrawTypeEnum.Donut, dp); + + // 玩家扇形 + var party = accessory.Data.PartyList; + foreach (var tid in party) + { + var dpP = accessory.Data.GetDefaultDrawProperties(); + dpP.Name = $"{baseName}_Moon_Player_{tid}"; + dpP.Owner = objId; dpP.TargetObject = tid; + dpP.Radian = float.Pi / 6; dpP.Scale = new Vector2(60f); + dpP.Color = accessory.Data.DefaultDangerColor; + dpP.Delay = delay; dpP.DestoryAt = duration; dpP.ScaleMode = ScaleMode.ByTime; + accessory.Method.SendDraw(DrawModeEnum.Default, DrawTypeEnum.Fan, dpP); + } + } + // 3. 十字 (19186) + else if (obj.DataId == 19186) + { + for (int k = 0; k < 4; k++) + { + var dp = accessory.Data.GetDefaultDrawProperties(); + dp.Name = $"{baseName}_Cross_Obj_{k}"; + dp.Position = obj.Position; + dp.Rotation = obj.Rotation + (float)(Math.PI / 2 * k); + dp.Scale = new Vector2(10f, 40f); + dp.Color = accessory.Data.DefaultDangerColor; + dp.Delay = delay; dp.DestoryAt = duration; dp.ScaleMode = ScaleMode.YByTime; + accessory.Method.SendDraw(DrawModeEnum.Default, DrawTypeEnum.Rect, dp); + } + + // 奶妈连线 (索引 2, 3) + var party = accessory.Data.PartyList; + for (int hi = 2; hi <= 3; hi++) + { + if (hi >= party.Count) break; + var tid = party[hi]; + var dpH = accessory.Data.GetDefaultDrawProperties(); + dpH.Name = $"{baseName}_Cross_Healer_{tid}"; + dpH.Owner = objId; dpH.TargetObject = tid; + dpH.Scale = new Vector2(6f); dpH.ScaleMode = ScaleMode.YByDistance; + dpH.Color = accessory.Data.DefaultDangerColor; + dpH.Delay = delay; dpH.DestoryAt = duration; + accessory.Method.SendDraw(DrawModeEnum.Default, DrawTypeEnum.Rect, dpH); + } + } + + uint myId = accessory.Data.Me; + var partyList = accessory.Data.PartyList; + int myIndex = partyList.IndexOf(myId); + if (myIndex < 0) return; + + Vector3 targetPos; + bool canGuide = true; + if (obj.DataId == 19184) + { + float guideRot = obj.Rotation + MathF.PI; + targetPos = GetPointByRotation(obj.Position, guideRot, 11f); + } + else if (obj.DataId == 19185) + { + int step = myIndex switch + { + 0 => 0, // MT + 7 => 1, // D4 + 3 => 2, // H2 + 5 => 3, // D2 + 1 => 4, // ST + 4 => 5, // D1 + 2 => 6, // H1 + 6 => 7, // D3 + _ => -1 + }; + if (step < 0) return; + float guideRot = obj.Rotation - (MathF.PI / 4f * step); + targetPos = GetPointByRotation(obj.Position, guideRot, 3.5f); + } + else if (obj.DataId == 19186) + { + bool isStGroup = myIndex == 1 || myIndex == 3 || myIndex == 5 || myIndex == 7; + float angle = isStGroup + ? obj.Rotation - (MathF.PI * 3f / 4f) + : obj.Rotation - (MathF.PI * 5f / 4f); + targetPos = GetPointByRotation(obj.Position, angle, 11f); + } + else + { + canGuide = false; + targetPos = default; + } + + if (!canGuide) return; + + var dpGuide = accessory.Data.GetDefaultDrawProperties(); + dpGuide.Name = baseName + "_Guide"; + dpGuide.Owner = myId; + dpGuide.TargetPosition = targetPos; + dpGuide.Scale = new Vector2(0.5f); + dpGuide.ScaleMode = ScaleMode.YByDistance; + dpGuide.Color = GuideColor.V4; + dpGuide.Delay = delay; + dpGuide.DestoryAt = duration; + + accessory.Method.SendDraw(DrawModeEnum.Default, DrawTypeEnum.Displacement, dpGuide); + } + private void TryDrawSingleObject(ObjectStateSix obj, uint objId, uint bossId, ScriptAccessory accessory) + { + if (obj.IsDrawn) return; // 避免重复�? + + // 计算理论上的时间�? + // Index �?1 开始,所�?i = Index - 1 + int i = obj.Index - 1; + + int plannedDelayFromStart = 0; + int duration = 0; + + if (i == 0) + { + plannedDelayFromStart = 0; + duration = 7050; + } + else + { + // �?个物�?i=1) -> 延迟 7050 + 5140*1 + plannedDelayFromStart = 7050 + 5140 * (i - 1); + duration = 5140; + } + + // 计算实际需�?Delay 多久 + long now = DateTime.Now.Ticks; + long targetTick = _mechanic47086StartTime + (plannedDelayFromStart * 10000); // 1ms = 10000 ticks + + long remainingDelayMs = (targetTick - now) / 10000; + + // 如果结果 < 0,说明时间已经过了,立刻画出来(Delay=0�? + if (remainingDelayMs < 0) remainingDelayMs = 0; + + // 调用底层绘图 + DrawMechanic(obj, objId, (int)remainingDelayMs, duration, bossId, accessory); + + // 标记已绘�? + obj.IsDrawn = true; + } + // ==================== 3. 核心处理逻辑 ==================== + private void ProcessMechanicLogic(ScriptAccessory accessory) + { + // 1. 获取自己的索�? + var myId = accessory.Data.Me; + var party = accessory.Data.PartyList; + int myIndex = -1; + + for (int i = 0; i < party.Count; i++) + { + if (party[i] == myId) { myIndex = i; break; } + } + if (myIndex == -1) + { + _castingObjects46166_46167.Clear(); + _targetIcon001EPlayers.Clear(); + return; + } + + // 2. 对列表中的物体进行分类和排序 (按象限从小到�? + // 46166 列表 + var objs46166 = _castingObjects + .Where(x => x.ActionId == 46166) + .OrderBy(x => x.Quadrant) + .ToList(); + + // 46167 列表 + var objs46167 = _castingObjects + .Where(x => x.ActionId == 46167) + .OrderBy(x => x.Quadrant) + .ToList(); + + MechanicObject targetObj = null; + + // 3. 职能分配逻辑 + + // --- MT (Index 0) --- + if (myIndex == 0) + { + // 找第 1 �?46166 + if (objs46166.Count >= 1) targetObj = objs46166[0]; + } + // --- ST (Index 1) --- + else if (myIndex == 1) + { + // 找第 2 �?46166 + if (objs46166.Count >= 2) targetObj = objs46166[1]; + } + // --- DPS & H (Index 2~7) --- + else + { + // 先检查有没有 001E 点名,有则不�? + if (_markedPlayers.Contains(myId)) return; + + // Index 4, 5 -> 找第 1 �?46167 + if (myIndex == 4 || myIndex == 5) + { + if (objs46167.Count >= 1) targetObj = objs46167[0]; + } + // Index 2, 3, 6, 7 -> 找第 2 �?46167 + else if (myIndex == 2 || myIndex == 3 || myIndex == 6 || myIndex == 7) + { + if (objs46167.Count >= 2) targetObj = objs46167[1]; + } + } + + // 4. 绘图 + if (targetObj != null) + { + var dp = accessory.Data.GetDefaultDrawProperties(); + dp.Name = $"Displace_Link_{targetObj.SourceId}_{DateTime.Now.Ticks}"; + + // Displacement: Owner=物体, Target=玩家 -> 击退/指向效果 + dp.Owner = targetObj.SourceId; + dp.TargetObject = myId; + + dp.Color = accessory.Data.DefaultDangerColor; + dp.Scale = new Vector2(0.5f); + dp.DestoryAt = targetObj.Duration; + + accessory.Method.SendDraw(DrawModeEnum.Default, DrawTypeEnum.Displacement, dp); + } + } + + + #endregion + + #region Initialization + + public void Init(ScriptAccessory accessory) + { + accessory.Method.RemoveDraw(".*"); + _acc = accessory; + _setPosCount = 0; + _hasCast46148 = false; + _tripleComboSetPosCount = 0; + _tripleComboRecordedIds.Clear(); // 新增 + _tether0039DrawnTime.Clear(); + _targetIcon001EPlayers.Clear(); + _castingObjects46166_46167.Clear(); + _dominion46112Regions.Clear(); + _dominion46112LastEventTicks = 0; + _dominion46112LastDrawTicks = 0; + _starTrackLastCastTicks = 0; + _starTrackLastCastBySource.Clear(); + _starTrackFirstCastTicks = 0; + _starTrackFirstBlocks.Clear(); + + + // 清空存储 + _objStorage.Clear(); + _objStorage1.Clear(); + _tripleComboStorage.Clear(); // 新增这行 + + _hasCast46162 = false; + _orderCounter = 0; + _castCount_46131 = 0; + _mechanic47086StartTime = 0; // 也建议重置这�? + _allValidCoords = _group1Coords.Concat(_group2Coords).Concat(_group3Coords).ToList(); + _markedPlayers.Clear(); + _castingObjects.Clear(); + + accessory.Method.SendChat("/e M11S Initialized."); + } + + #endregion + #region TTSonly + + [ScriptMethod(name: "铸兵猛攻", eventType: EventTypeEnum.StartCasting, eventCondition: ["ActionId:regex:^(46087|46088|46089|46010|46012|46014)$"])] + public void WeaponCall_Alert(Event @event, ScriptAccessory accessory) + { + if (!uint.TryParse(@event["ActionId"], out var aid)) return; + // 46087 -> 斧子 + if (aid == 46087 || aid == 46010) + { + QTTS("钢铁"); + QText("钢铁", 3000, true); + } + // 46088 -> 镰刀 + else if (aid == 46088 || aid == 46012) + { + QTTS("月环"); + QText("月环", 3000, true); + } + // 46089 -> 大剑 + else if (aid == 46089 || aid == 46014) + { + QTTS("十字"); + QText("十字", 3000, true); + } + } + [ScriptMethod(name: "历战之兵武TTS", eventType: EventTypeEnum.StartCasting, eventCondition: ["ActionId:regex:^(46028|46102)$"])] + public void TripleCharge_Alert(Event @event, ScriptAccessory accessory) + { + // 46028, 46102 -> 准备三连冲锋 + QTTS("准备三连冲锋"); + QText("准备三连冲锋", 3000, true); + } + [ScriptMethod(name: "铸兵之令:轰击TTS", eventType: EventTypeEnum.StartCasting, eventCondition: ["ActionId:regex:^(46037|46114|46115)$"])] + public void TankBuster_Combo_Alert(Event @event, ScriptAccessory accessory) + { + if (!uint.TryParse(@event["ActionId"], out var aid)) return; + + // 46037(N), 46114(S) -> 圆形分散 + 死刑 + if (aid == 46037 || aid == 46114) + { + QTTS("圆形分散加死刑"); + QText("圆形分散 + 死刑", 3000, true); + } + // 46115(S) -> 扇形分摊 + 死刑 + else if (aid == 46115) + { + QTTS("扇形分摊加死刑"); + QText("扇形分摊 + 死刑", 3000, true); + } + } + [ScriptMethod(name: "霸王大漩涡TTS", eventType: EventTypeEnum.StartCasting, eventCondition: ["ActionId:regex:^(46039|46117)$"])] + public void HPtoOne_Alert(Event @event, ScriptAccessory accessory) + { + QTTS("清1血"); + QText("清1血", 3000, true); + } + + [ScriptMethod(name: "万劫不朽的统治TTS", eventType: EventTypeEnum.StartCasting, eventCondition: ["ActionId:regex:^(46042|46120)$"])] + public void AOE_Alert_46042(Event @event, ScriptAccessory accessory) + { + QTTS("AOE"); + QText("AOE", 3000, true); + } + + [ScriptMethod(name: "重陨石TTS(猜测)", eventType: EventTypeEnum.StartCasting, eventCondition: ["ActionId:46152"])] + public void Stack_Alert_46152(Event @event, ScriptAccessory accessory) + { + QTTS("分摊"); + QText("分摊", 3000, true); + } + [ScriptMethod(name: "冲击波TTS(猜测)", eventType: EventTypeEnum.StartCasting, eventCondition: ["ActionId:46140"])] + public void Meteor_Alert_46140(Event @event, ScriptAccessory accessory) + { + QTTS("大陨石"); + QText("大陨石", 3000, true); + } + [ScriptMethod(name: "回旋火TTS", eventType: EventTypeEnum.StartCasting, eventCondition: ["ActionId:regex:^(47037|46170)$"])] + public void RotatingFire_Alert(Event @event, ScriptAccessory accessory) + { + if (!uint.TryParse(@event["ActionId"], out var aid)) return; + + // 47038 双向回旋�?-> 双向 22分摊 + if (aid == 47038) + { + QTTS("双向 22分摊"); + // 如果需要文字提示可以把下面这行注释解开 + // QText("双向 22分摊", 4000, true); + } + // 46171 四向回旋�?-> 四向 四人分散 + else if (aid == 46171) + { + QTTS("四向 四人分散"); + // 如果需要文字提示可以把下面这行注释解开 + // QText("四向 四人分散", 4000, true); + } + } + + #endregion + + + #region 铸兵之令 + [ScriptMethod(name: "铸兵之令:统治", eventType: EventTypeEnum.StartCasting, eventCondition: ["ActionId:regex:^(46035|46112)$"])] + public void DoubleRectCleave_Draw(Event @event, ScriptAccessory accessory) + { + if (!uint.TryParse(@event["ActionId"], out var aid)) return; + if (!int.TryParse(@event["DurationMilliseconds"], out var dur)) return; + float baseRotation = @event.SourceRotation; + var tidStr = @event["TargetId"]; + if (!string.IsNullOrEmpty(tidStr) && + ulong.TryParse(tidStr.Replace("0x", ""), System.Globalization.NumberStyles.HexNumber, null, out var tid)) + { + var tObj = accessory.Data.Objects.SearchById(tid); + if (tObj != null) + { + baseRotation = tObj.Rotation; + } + } + // 2. 循环�?2 条矩�?(0�?�?180�? + for (int i = 0; i < 2; i++) + { + var dp = accessory.Data.GetDefaultDrawProperties(); + dp.Name = $"Rect_Cleave_{aid}_{i}_{DateTime.Now.Ticks}"; + dp.Position = @event.SourcePosition; + // i=0 -> baseRotation + // i=1 -> baseRotation + PI (180�? + dp.Rotation = baseRotation + (float)(Math.PI * i); + dp.Scale = new Vector2(10f, 60f); + dp.Color = accessory.Data.DefaultDangerColor; + dp.DestoryAt = dur; + dp.ScaleMode = ScaleMode.YByTime; + accessory.Method.SendDraw(DrawModeEnum.Default, DrawTypeEnum.Rect, dp); + } + + if (aid == 46112) + { + TrackDominionGuidance(@event.SourcePosition, dur, accessory); + } + } + + + + #endregion + + + + + + #region 瞎猜环节 + + + + [ScriptMethod(name: "重斩击(猜测)", eventType: EventTypeEnum.StartCasting, eventCondition: ["ActionId:46096"])] + public void TrackingFan_Alert(Event @event, ScriptAccessory accessory) + { + if (!uint.TryParse(@event["ActionId"], out var aid)) return; + if (!int.TryParse(@event["DurationMilliseconds"], out var dur)) return; + var tidStr = @event["TargetId"]; + if (string.IsNullOrEmpty(tidStr) || + !ulong.TryParse(tidStr.Replace("0x", ""), System.Globalization.NumberStyles.HexNumber, null, out var tid)) + return; + var dp = accessory.Data.GetDefaultDrawProperties(); + dp.Name = $"Track_Fan_{aid}_{DateTime.Now.Ticks}"; + dp.Owner = @event.SourceId; + dp.ScaleMode = ScaleMode.ByTime; + dp.TargetObject = tid; + dp.Scale = new Vector2(60f); + dp.Radian = float.Pi / 4; + dp.Color = accessory.Data.DefaultDangerColor; + dp.DestoryAt = dur; + + accessory.Method.SendDraw(DrawModeEnum.Default, DrawTypeEnum.Fan, dp); + } + [ScriptMethod(name: "轰击(猜测)", eventType: EventTypeEnum.StartCasting, eventCondition: ["ActionId:46133"])] + public void TargetCircle_46133(Event @event, ScriptAccessory accessory) + { + if (!int.TryParse(@event["DurationMilliseconds"], out var dur)) return; + + // 解析 TargetId + var tidStr = @event["TargetId"]; + if (string.IsNullOrEmpty(tidStr) || + !ulong.TryParse(tidStr.Replace("0x", ""), System.Globalization.NumberStyles.HexNumber, null, out var tid)) + return; + + var dp = accessory.Data.GetDefaultDrawProperties(); + dp.Name = $"Target_Circle_46133_{tid}_{DateTime.Now.Ticks}"; + dp.Owner = tid; + dp.Scale = new Vector2(4f); + dp.Color = accessory.Data.DefaultDangerColor; + dp.ScaleMode = ScaleMode.ByTime; + dp.DestoryAt = dur; + accessory.Method.SendDraw(DrawModeEnum.Default, DrawTypeEnum.Circle, dp); + } + [ScriptMethod(name: "兽焰连尾击", eventType: EventTypeEnum.StartCasting, eventCondition: ["ActionId:regex:^(46072|46128|46073|46129)$"])] + public void FrontBackFan_Draw(Event @event, ScriptAccessory accessory) + { + if (!uint.TryParse(@event["ActionId"], out var aid)) return; + if (!int.TryParse(@event["DurationMilliseconds"], out var dur)) return; + + var dp = accessory.Data.GetDefaultDrawProperties(); + dp.Name = $"Fan_FB_{aid}_{DateTime.Now.Ticks}"; + dp.Position = @event.SourcePosition; + dp.Color = accessory.Data.DefaultDangerColor; + dp.Scale = new Vector2(60f); // 半径 60m + dp.Radian = float.Pi / 2; // 90�?(π/2) + dp.DestoryAt = dur; + dp.ScaleMode = ScaleMode.ByTime; + // 1. 前扇�?(46072, 46128) + if (aid == 46072 || aid == 46128) + { + dp.Rotation = @event.SourceRotation; + } + // 2. 后扇�?(46073, 46129) + + else if (aid == 46073 || aid == 46129) + { + dp.Rotation = @event.SourceRotation + float.Pi; // �?80�? + } + accessory.Method.SendDraw(DrawModeEnum.Default, DrawTypeEnum.Fan, dp); + } + [ScriptMethod(name: "登天碎地", eventType: EventTypeEnum.StartCasting, eventCondition: ["ActionId:regex:^(46064|46066|46068|46070|46155|46157|46159|46161)$"])] + public void Rect_Gradient_40x40(Event @event, ScriptAccessory accessory) + { + if (!uint.TryParse(@event["ActionId"], out var aid)) return; + if (!int.TryParse(@event["DurationMilliseconds"], out var dur)) return; + + // 1. 画原来的:正�?40x40 + var dp = accessory.Data.GetDefaultDrawProperties(); + dp.Name = $"Rect_Front_40x40_{aid}_{DateTime.Now.Ticks}"; + dp.Position = @event.SourcePosition; + dp.Rotation = @event.SourceRotation; + dp.Scale = new Vector2(40f, 40f); // �?0 �?0 + dp.Color = accessory.Data.DefaultDangerColor; + dp.DestoryAt = dur; + dp.ScaleMode = ScaleMode.YByTime; // 随时间填充长�? + accessory.Method.SendDraw(DrawModeEnum.Default, DrawTypeEnum.Rect, dp); + + // 2. 画新增的:反�?60x60 + var dpBack = accessory.Data.GetDefaultDrawProperties(); + dpBack.Name = $"Rect_Back_60x60_{aid}_{DateTime.Now.Ticks}"; // 名字区分一下 + dpBack.Position = @event.SourcePosition; + + // 反方�?= 原方�?+ PI (180�? + dpBack.Rotation = @event.SourceRotation + (float)Math.PI; + + dpBack.Scale = new Vector2(60f, 60f); // �?0 �?0 + dpBack.Color = accessory.Data.DefaultDangerColor; + dpBack.DestoryAt = dur; // 时长相同 + dpBack.ScaleMode = ScaleMode.YByTime; // 也是渐变 + + accessory.Method.SendDraw(DrawModeEnum.Default, DrawTypeEnum.Rect, dpBack); + } + [ScriptMethod(name: "铸兵之令:轰击圆", eventType: EventTypeEnum.StartCasting, eventCondition: ["ActionId:46114"])] + public void Action_46114_Index(Event @event, ScriptAccessory accessory) + { + // 1. 获取持续时间�?+ 1�? + if (!int.TryParse(@event["DurationMilliseconds"], out var castDuration)) return; + int finalDuration = castDuration + 7300; + + // 2. 获取自己的索引,判断自己是否为坦�?(0, 1) + uint myId = accessory.Data.Me; + var partyIds = accessory.Data.PartyList; + int myIndex = -1; + + for (int i = 0; i < partyIds.Count; i++) + { + if (partyIds[i] == myId) + { + myIndex = i; + break; + } + } + + // 默认小队列表顺序�?,1 是坦�? + bool amITank = (myIndex == 0 || myIndex == 1); + + // ========================================================= + // 第一部分:对仇恨列表�?2 位画�? + // ========================================================= + + var dpAggro = accessory.Data.GetDefaultDrawProperties(); + dpAggro.Name = $"Aggro2_{@event.SourceId}_{DateTime.Now.Ticks}"; + dpAggro.Owner = @event.SourceId; // 绑在BOSS身上 + + // 使用 OwnerEnmityOrder (BOSS的仇恨列�? + dpAggro.CentreResolvePattern = PositionResolvePatternEnum.OwnerEnmityOrder; + dpAggro.CentreOrderIndex = 2; // �?�? + + dpAggro.Scale = new Vector2(6f); + dpAggro.DestoryAt = finalDuration; + dpAggro.ScaleMode = ScaleMode.ByTime; // 渐变 + + // 颜色逻辑:我是T(0,1) -> 绿色(安全),我是H/D -> 红色(危险) + dpAggro.Color = amITank ? accessory.Data.DefaultSafeColor : accessory.Data.DefaultDangerColor; + + accessory.Method.SendDraw(DrawModeEnum.Default, DrawTypeEnum.Circle, dpAggro); + + + // ========================================================= + // 第二部分:对所�?H �?D (索引 2-7) 画图 + // ========================================================= + + for (int i = 0; i < partyIds.Count; i++) + { + // 如果索引 > 1,说明是 H (2,3) �?D (4,5,6,7) + if (i > 1) + { + var tid = partyIds[i]; + + var dpHD = accessory.Data.GetDefaultDrawProperties(); + dpHD.Name = $"HD_Danger_{tid}_{DateTime.Now.Ticks}"; + + dpHD.Owner = tid; // 绑在该玩家身�? + dpHD.Scale = new Vector2(6f); + dpHD.Color = accessory.Data.DefaultDangerColor; // 永远是危险红 + dpHD.DestoryAt = finalDuration; + dpHD.ScaleMode = ScaleMode.ByTime; // 渐变 + + accessory.Method.SendDraw(DrawModeEnum.Default, DrawTypeEnum.Circle, dpHD); + } + } + } + [ScriptMethod(name: "铸兵之令:轰击扇", eventType: EventTypeEnum.StartCasting, eventCondition: ["ActionId:46115"])] + public void Action_46115_Logic(Event @event, ScriptAccessory accessory) + { + // 1. 获取原本持续时间�?+ 7.3�?(7300ms) + if (!int.TryParse(@event["DurationMilliseconds"], out var castDuration)) return; + int finalDuration = castDuration + 7300; + + // 2. 获取自己的索�?(0,1=T, >1=H/D) + uint myId = accessory.Data.Me; + var partyIds = accessory.Data.PartyList; + int myIndex = -1; + + for (int i = 0; i < partyIds.Count; i++) + { + if (partyIds[i] == myId) + { + myIndex = i; + break; + } + } + + // 定义弧度 + float rad90 = float.Pi / 2; + float rad45 = float.Pi / 4; + + // ========================================================= + // 逻辑 A: 无论我是谁,两个 T (索引0�?) 身上都要�?90度危险扇�? + // ========================================================= + for (int i = 0; i <= 1; i++) + { + // 防止小队人数不足报错 + if (i < partyIds.Count) + { + var targetId = partyIds[i]; + + var dp = accessory.Data.GetDefaultDrawProperties(); + dp.Name = $"Tank_Fan_90_{targetId}_{DateTime.Now.Ticks}"; + + // Boss 指向并绑定到玩家 + dp.Owner = @event.SourceId; // 起点:Boss + dp.TargetObject = targetId; // 终点/朝向:玩�? + + dp.Radian = rad90; // 90�? + dp.Scale = new Vector2(60f); // 长度 60 + dp.Color = accessory.Data.DefaultDangerColor; // 危险�? + + dp.DestoryAt = finalDuration; + dp.ScaleMode = ScaleMode.ByTime; // 渐变 + + accessory.Method.SendDraw(DrawModeEnum.Default, DrawTypeEnum.Fan, dp); + } + } + + // ========================================================= + // 逻辑 B: 如果我是 H/D (索引 > 1),对自己�?45度安全扇�? + // ========================================================= + if (myIndex > 1) + { + var dpSafe = accessory.Data.GetDefaultDrawProperties(); + dpSafe.Name = $"HD_Safe_Fan_45_{myId}_{DateTime.Now.Ticks}"; + + // Boss 指向并绑定到玩家(自己) + dpSafe.Owner = @event.SourceId; + dpSafe.TargetObject = myId; + + dpSafe.Radian = rad45; // 45�? + dpSafe.Scale = new Vector2(60f); // 长度 60 + dpSafe.Color = accessory.Data.DefaultSafeColor; // 安全�? + + dpSafe.DestoryAt = finalDuration; + dpSafe.ScaleMode = ScaleMode.ByTime; // 渐变 + + accessory.Method.SendDraw(DrawModeEnum.Default, DrawTypeEnum.Fan, dpSafe); + } + } + #region 三连斧镰剑改进版 + + // ==================== 变量定义 ==================== + private Dictionary _tripleComboStorage = new Dictionary(); + private int _tripleComboSetPosCount = 0; // SetObjPos 计数�? + private HashSet _tripleComboRecordedIds = new HashSet(); // 新增:记录已处理�?SourceId + + // ==================== 辅助方法 ==================== + + /// + /// 检查坐标是否在列表中的某个点附�? + /// + private bool IsCloseToAny(List coords, Vector2 pos, float threshold = 1.0f) + { + foreach (var v in coords) + { + if (Vector2.Distance(pos, v) < threshold) return true; + } + return false; + } + + /// + /// 检查坐标是否在三连机制的有效范围内 + /// + private bool IsValidTripleComboPosition(Vector2 pos) + { + return IsCloseToAny(_group1Coords, pos) || + IsCloseToAny(_group2Coords, pos) || + IsCloseToAny(_group3Coords, pos); + } + + /// + /// 根据 Boss 朝向,将物体按顺时针排序 + /// + private List SortWeaponsClockwiseWithTolerance(List weapons, Vector3 center, float bossRotationRad) + { + if (weapons == null || weapons.Count == 0) + { + return new List(); + } + + // Boss 朝向转角�? + float bossRotationDeg = bossRotationRad * 180f / MathF.PI; + if (bossRotationDeg < 0) bossRotationDeg += 360f; + + // 计算每个物体相对于中心的角度 + var weaponsWithAngle = weapons + .Where(w => w != null) + .Select(w => new + { + Weapon = w, + Angle = GetAbsoluteAngle(w.Position, center) + }) + .ToList(); + + // 找到�?Boss 面向最接近的物体作为第一�? + var firstWeapon = weaponsWithAngle + .OrderBy(w => GetAngleDifference(w.Angle, bossRotationDeg)) + .First(); + + float startAngle = firstWeapon.Angle; + + // 从第一个物体开始,按顺时针排序 + return weaponsWithAngle + .OrderBy(w => GetRelativeAngleFrom(w.Angle, startAngle)) + .Select(w => w.Weapon) + .ToList(); + } + /// + /// 计算点相对于中心和起始角度的顺时针角�? + /// + + + /// + /// 计算两个角度之间的最小差�?(0-180) + /// + private float GetAngleDifference(float angle1, float angle2) + { + float diff = MathF.Abs(angle1 - angle2); + if (diff > 180f) diff = 360f - diff; + return diff; + } + + /// + /// 计算从起始角度开始的顺时针相对角�?(0-360) + /// + private float GetRelativeAngleFrom(float angle, float startAngle) + { + float relative = angle - startAngle; + if (relative < 0) relative += 360f; + return relative; + } + + // ==================== 事件处理 ==================== + + + [ScriptMethod(name: "三连斧镰剑", eventType: EventTypeEnum.StartCasting, eventCondition: ["ActionId:46103"])] + public async void Triple_Combo_Draw_Improved(Event @event, ScriptAccessory accessory) + { + uint bossId = (uint)@event.SourceId; + + await Task.Delay(2000); + + Vector3 center = new Vector3(100f, 0f, 100f); + + var weapons = accessory.Data.Objects + .Where(obj => obj.DataId == 19184 || obj.DataId == 19185 || obj.DataId == 19186) + .Where(obj => Vector2.Distance(new Vector2(obj.Position.X, obj.Position.Z), new Vector2(100f, 100f)) > 5f) + .ToList(); + + if (weapons.Count < 3) + { + accessory.Method.SendChat($"/e [警告] 物体数量不足: {weapons.Count}"); + return; + } + + float bossRotation = @event.SourceRotation; + try + { + var bossObj = accessory.Data.Objects.SearchById(bossId); + if (bossObj != null) + { + bossRotation = bossObj.Rotation; + } + } + catch { } + + // 按顺时针排序 + var sortedWeapons = weapons + .OrderBy(w => { + float dx = w.Position.X - center.X; + float dz = w.Position.Z - center.Z; + float angle = MathF.Atan2(dx, dz); + + float relative = angle - bossRotation; + // 归一化到 (-2π, 0] + while (relative > 0) relative -= MathF.PI * 2; + while (relative <= -MathF.PI * 2) relative += MathF.PI * 2; + + // �?-2π (�?-360°) 当作 0 处理 + if (MathF.Abs(relative + MathF.PI * 2) < 0.01f) relative = 0; + + return -relative; + }) + .ToList(); + + int[] delays = { 0, 6300, 11400 }; + int[] durations = { 6300, 5100, 5100 }; + + for (int i = 0; i < 3; i++) + { + var weapon = sortedWeapons[i]; + + var obj = new ObjectState + { + DataId = weapon.DataId, + Position = weapon.Position, + Rotation = weapon.Rotation, + GroupId = 0 + }; + + DrawTripleComboMechanic(obj, weapon.EntityId, delays[i], durations[i], bossId, accessory); + } + } + + + + private float GetAbsoluteAngle(Vector3 point, Vector3 center) + { + float dx = point.X - center.X; + float dz = point.Z - center.Z; + + float angleRad = MathF.Atan2(dx, dz); + float angleDeg = angleRad * 180f / MathF.PI; + if (angleDeg < 0) angleDeg += 360f; + + return angleDeg; + } + + /// + /// 直接对游戏物体按顺时针排�? + /// + private List SortWeaponsClockwise(List weapons, Vector3 center, float bossRotationRad) + { + if (weapons == null || weapons.Count == 0) + { + return new List(); + } + + float bossRotationDeg = bossRotationRad * 180f / MathF.PI; + if (bossRotationDeg < 0) bossRotationDeg += 360f; + + float startRotationDeg = bossRotationDeg; + + return weapons + .Where(w => w != null) + .OrderBy(w => GetRelativeAngle(w.Position, center, startRotationDeg)) + .ToList(); + } + private float GetRelativeAngle(Vector3 point, Vector3 center, float startRotationDeg) + { + float dx = point.X - center.X; + float dz = point.Z - center.Z; + + float angleRad = MathF.Atan2(dx, -dz); + float angleDeg = angleRad * 180f / MathF.PI; + if (angleDeg < 0) angleDeg += 360f; + + float relative = angleDeg - startRotationDeg; + if (relative < 0) relative += 360f; + + return relative; + } + + private static Vector3 GetPointByRotation(Vector3 origin, float rotation, float distance) + { + return new Vector3( + origin.X + MathF.Sin(rotation) * distance, + origin.Y, + origin.Z + MathF.Cos(rotation) * distance + ); + } + + private static int? GetDominionRegion(Vector3 position) + { + float dx = position.X - 100f; + float dz = position.Z - 100f; + + if (MathF.Abs(dx) <= 1f && MathF.Abs(dz) <= 1f) + { + return null; + } + + float absDx = MathF.Abs(dx); + float absDz = MathF.Abs(dz); + + if (absDz > absDx) + { + return dz >= 0 ? 2 : 0; // south or north + } + + return dx >= 0 ? 1 : 3; // east or west + } + private static float NormalizeAngle(float angle) + { + float twoPi = MathF.PI * 2f; + while (angle > MathF.PI) angle -= twoPi; + while (angle < -MathF.PI) angle += twoPi; + return angle; + } + + private static readonly Vector3[] _starTrackCenterBlockCenters = new[] + { + new Vector3(95f, 0f, 95f), // NW + new Vector3(105f, 0f, 95f), // NE + new Vector3(95f, 0f, 105f), // SW + new Vector3(105f, 0f, 105f) // SE + }; + + private static HashSet GetStarTrackHitBlocks(Vector3 lineOrigin, float lineRotation) + { + var result = new HashSet(); + float sin = MathF.Sin(lineRotation); + float cos = MathF.Cos(lineRotation); + + const float halfWidth = 5f; + const float length = 60f; + const float halfBlock = 5f; + const float tolerance = 0.5f; + float xLimit = halfWidth + halfBlock + tolerance; + float zMin = -halfBlock - tolerance; + float zMax = length + halfBlock + tolerance; + + for (int i = 0; i < _starTrackCenterBlockCenters.Length; i++) + { + Vector3 blockCenter = _starTrackCenterBlockCenters[i]; + float dx = blockCenter.X - lineOrigin.X; + float dz = blockCenter.Z - lineOrigin.Z; + + float localX = dx * cos - dz * sin; + float localZ = dx * sin + dz * cos; + + if (MathF.Abs(localX) <= xLimit && + localZ >= zMin && + localZ <= zMax) + { + result.Add(i); + } + } + + return result; + } + + private struct DominionSafePositions + { + public Vector3 NearRight; + public Vector3 NearLeft; + public Vector3 FarRight; + public Vector3 FarLeft; + } + + private static DominionSafePositions GetDominionSafePositions(int region) + { + Vector3 center = new Vector3(100f, 0f, 100f); + Vector3 vertex = region switch + { + 0 => new Vector3(100f, 0f, 80f), // north + 1 => new Vector3(120f, 0f, 100f), // east + 2 => new Vector3(100f, 0f, 120f), // south + 3 => new Vector3(80f, 0f, 100f), // west + _ => new Vector3(100f, 0f, 100f) + }; + + bool axisIsX = MathF.Abs(vertex.X - 100f) > 0.01f; + float axisBase = axisIsX ? vertex.X : vertex.Z; + float axisDir = axisBase > 100f ? -1f : 1f; + float nearAxis = axisBase + axisDir * 3.5f; + float farAxis = axisBase + axisDir * 20f; + float perpBase = axisIsX ? vertex.Z : vertex.X; + + Vector3 nearA; + Vector3 nearB; + Vector3 farA; + Vector3 farB; + + if (axisIsX) + { + nearA = new Vector3(nearAxis, 0f, perpBase + 7f); + nearB = new Vector3(nearAxis, 0f, perpBase - 7f); + farA = new Vector3(farAxis, 0f, perpBase + 8.5f); + farB = new Vector3(farAxis, 0f, perpBase - 8.5f); + } + else + { + nearA = new Vector3(perpBase + 7f, 0f, nearAxis); + nearB = new Vector3(perpBase - 7f, 0f, nearAxis); + farA = new Vector3(perpBase + 8.5f, 0f, farAxis); + farB = new Vector3(perpBase - 8.5f, 0f, farAxis); + } + + Vector3 forward = Vector3.Normalize(new Vector3(vertex.X - center.X, 0f, vertex.Z - center.Z)); + Vector3 right = new Vector3(-forward.Z, 0f, forward.X); + + bool nearAIsRight = Vector3.Dot(new Vector3(nearA.X - center.X, 0f, nearA.Z - center.Z), right) > 0f; + bool farAIsRight = Vector3.Dot(new Vector3(farA.X - center.X, 0f, farA.Z - center.Z), right) > 0f; + + return new DominionSafePositions + { + NearRight = nearAIsRight ? nearA : nearB, + NearLeft = nearAIsRight ? nearB : nearA, + FarRight = farAIsRight ? farA : farB, + FarLeft = farAIsRight ? farB : farA + }; + } + + private static float GetDominionAxisValue(Vector3 point) + { + return MathF.Abs(point.X - 100f) > 0.01f ? point.X : point.Z; + } + + private static IEnumerable GetDominionSafePoints(int region) + { + DominionSafePositions safe = GetDominionSafePositions(region); + return new[] { safe.NearRight, safe.NearLeft, safe.FarRight, safe.FarLeft }; + } + + private void TrackDominionGuidance(Vector3 position, int duration, ScriptAccessory accessory) + { + long now = DateTime.Now.Ticks; + long resetTicks = 150000000L; // 15s + + if (now - _dominion46112LastEventTicks > resetTicks) + { + _dominion46112Regions.Clear(); + _dominion46112LastDrawTicks = 0; + } + + _dominion46112LastEventTicks = now; + + if (_dominion46112LastDrawTicks != 0 && now - _dominion46112LastDrawTicks < resetTicks) + { + return; + } + + var region = GetDominionRegion(position); + if (!region.HasValue) + { + return; + } + + _dominion46112Regions.Add(region.Value); + + if (_dominion46112Regions.Count == 3) + { + int missing = Enumerable.Range(0, 4).First(r => !_dominion46112Regions.Contains(r)); + + uint myId = accessory.Data.Me; + var party = accessory.Data.PartyList; + int myIndex = party.IndexOf(myId); + + if (myIndex >= 0) + { + DominionSafePositions safe = GetDominionSafePositions(missing); + Vector3 targetPos = default; + bool hasTarget = true; + + if (myIndex == 2 || myIndex == 6) + { + targetPos = safe.NearRight; + } + else if (myIndex == 3 || myIndex == 7) + { + targetPos = safe.NearLeft; + } + else if (DominionGuidance1 == DominionGuidance.标准22分摊) + { + if (myIndex == 0 || myIndex == 4) + { + targetPos = safe.FarRight; + } + else if (myIndex == 1 || myIndex == 5) + { + targetPos = safe.FarLeft; + } + else + { + hasTarget = false; + } + } + else + { + Vector3 farNegative = GetDominionAxisValue(safe.FarRight) < 100f ? safe.FarRight : safe.FarLeft; + Vector3 farPositive = GetDominionAxisValue(safe.FarRight) > 100f ? safe.FarRight : safe.FarLeft; + + if (myIndex == 0 || myIndex == 4) + { + targetPos = farNegative; + } + else if (myIndex == 1 || myIndex == 5) + { + targetPos = farPositive; + } + else + { + hasTarget = false; + } + } + + if (hasTarget) + { + var dpGuide = accessory.Data.GetDefaultDrawProperties(); + dpGuide.Name = $"Dominion_46112_GuideLine_{myId}_{DateTime.Now.Ticks}"; + dpGuide.Owner = myId; + dpGuide.TargetPosition = targetPos; + dpGuide.Scale = new Vector2(0.5f); + dpGuide.ScaleMode = ScaleMode.YByDistance; + dpGuide.Color = GuideColor.V4; + dpGuide.DestoryAt = duration; + + accessory.Method.SendDraw(DrawModeEnum.Default, DrawTypeEnum.Displacement, dpGuide); + } + } + + _dominion46112LastDrawTicks = now; + _dominion46112Regions.Clear(); + } + else if (_dominion46112Regions.Count > 3) + { + _dominion46112Regions.Clear(); + } + } + + private void DrawTripleComboMechanic(ObjectState obj, uint objId, int delay, int duration, uint bossId, ScriptAccessory accessory) + { + string baseName = $"Triple_{obj.DataId}_{delay}_{DateTime.Now.Ticks}"; + + // ========== 19184 = 斧子 = 钢铁 (圆形AOE) ========== + if (obj.DataId == 19184) + { + var dp = accessory.Data.GetDefaultDrawProperties(); + dp.Name = baseName + "_Iron_Obj"; + dp.Position = obj.Position; + dp.Scale = new Vector2(8f); + dp.Color = accessory.Data.DefaultDangerColor; + dp.Delay = delay; + dp.DestoryAt = duration; + dp.ScaleMode = ScaleMode.ByTime; + accessory.Method.SendDraw(DrawModeEnum.Default, DrawTypeEnum.Circle, dp); + + var dpPlayer = accessory.Data.GetDefaultDrawProperties(); + dpPlayer.Name = baseName + "_Iron_Player"; + dpPlayer.Owner = accessory.Data.Me; + dpPlayer.Scale = new Vector2(6f); + dpPlayer.Color = accessory.Data.DefaultSafeColor; + dpPlayer.Delay = delay; + dpPlayer.DestoryAt = duration; + dpPlayer.ScaleMode = ScaleMode.ByTime; + accessory.Method.SendDraw(DrawModeEnum.Default, DrawTypeEnum.Circle, dpPlayer); + } + // ========== 19185 = 镰刀 = 月环 (甜甜圈AOE) ========== + else if (obj.DataId == 19185) + { + var dp = accessory.Data.GetDefaultDrawProperties(); + dp.Name = baseName + "_Moon_Obj"; + dp.Position = obj.Position; + dp.Scale = new Vector2(60f); + dp.InnerScale = new Vector2(5f); + dp.Radian = float.Pi * 2; + dp.Color = accessory.Data.DefaultDangerColor; + dp.Delay = delay; + dp.DestoryAt = duration; + dp.ScaleMode = ScaleMode.ByTime; + accessory.Method.SendDraw(DrawModeEnum.Default, DrawTypeEnum.Donut, dp); + + var party = accessory.Data.PartyList; + foreach (var tid in party) + { + var dpFan = accessory.Data.GetDefaultDrawProperties(); + dpFan.Name = $"{baseName}_Moon_Fan_{tid}"; + dpFan.Owner = objId; + dpFan.TargetObject = tid; + dpFan.Radian = float.Pi / 6; + dpFan.Scale = new Vector2(60f); + dpFan.Color = accessory.Data.DefaultDangerColor; + dpFan.Delay = delay; + dpFan.DestoryAt = duration; + dpFan.ScaleMode = ScaleMode.ByTime; + accessory.Method.SendDraw(DrawModeEnum.Default, DrawTypeEnum.Fan, dpFan); + } + } + // ========== 19186 = 大剑 = 十字 (十字AOE) ========== + else if (obj.DataId == 19186) + { + for (int k = 0; k < 4; k++) + { + var dp = accessory.Data.GetDefaultDrawProperties(); + dp.Name = $"{baseName}_Cross_Obj_{k}"; + dp.Position = obj.Position; + dp.Rotation = obj.Rotation + (float)(Math.PI / 2 * k); + dp.Scale = new Vector2(10f, 40f); + dp.Color = accessory.Data.DefaultDangerColor; + dp.Delay = delay; + dp.DestoryAt = duration; + dp.ScaleMode = ScaleMode.YByTime; + accessory.Method.SendDraw(DrawModeEnum.Default, DrawTypeEnum.Rect, dp); + } + + var party = accessory.Data.PartyList; + for (int hi = 2; hi <= 3; hi++) + { + if (hi >= party.Count) break; + var tid = party[hi]; + + var dpRect = accessory.Data.GetDefaultDrawProperties(); + dpRect.Name = $"{baseName}_Cross_Healer_Rect_{tid}"; + dpRect.Owner = objId; + dpRect.TargetObject = tid; + dpRect.Scale = new Vector2(6f, 60f); + dpRect.Color = accessory.Data.DefaultDangerColor; + dpRect.Delay = delay + 2500; + dpRect.DestoryAt = Math.Max(duration - 2500, 1000); + dpRect.ScaleMode = ScaleMode.YByTime; + accessory.Method.SendDraw(DrawModeEnum.Default, DrawTypeEnum.Rect, dpRect); + } + } + + uint myId = accessory.Data.Me; + var partyList = accessory.Data.PartyList; + int myIndex = partyList.IndexOf(myId); + if (myIndex < 0) return; + + Vector3 targetPos; + bool canGuide = true; + if (obj.DataId == 19184) + { + float guideRot = obj.Rotation + MathF.PI; + targetPos = GetPointByRotation(obj.Position, guideRot, 11f); + } + else if (obj.DataId == 19185) + { + int step = myIndex switch + { + 0 => 0, // MT + 7 => 1, // D4 + 3 => 2, // H2 + 5 => 3, // D2 + 1 => 4, // ST + 4 => 5, // D1 + 2 => 6, // H1 + 6 => 7, // D3 + _ => -1 + }; + if (step < 0) return; + float guideRot = obj.Rotation - (MathF.PI / 4f * step); + targetPos = GetPointByRotation(obj.Position, guideRot, 3.5f); + } + else if (obj.DataId == 19186) + { + bool isStGroup = myIndex == 1 || myIndex == 3 || myIndex == 5 || myIndex == 7; + float angle = isStGroup + ? obj.Rotation - (MathF.PI * 3f / 4f) + : obj.Rotation - (MathF.PI * 5f / 4f); + targetPos = GetPointByRotation(obj.Position, angle, 11f); + } + else + { + canGuide = false; + targetPos = default; + } + + if (!canGuide) return; + + var dpGuide = accessory.Data.GetDefaultDrawProperties(); + dpGuide.Name = baseName + "_Guide"; + dpGuide.Owner = myId; + dpGuide.TargetPosition = targetPos; + dpGuide.Scale = new Vector2(0.5f); + dpGuide.ScaleMode = ScaleMode.YByDistance; + dpGuide.Color = GuideColor.V4; + dpGuide.Delay = delay; + dpGuide.DestoryAt = duration; + + accessory.Method.SendDraw(DrawModeEnum.Default, DrawTypeEnum.Displacement, dpGuide); + } + + #endregion + + [ScriptMethod(name: "记录6连斧镰剑", eventType: EventTypeEnum.SetObjPos, eventCondition: ["SourceDataId:regex:^(19184|19185|19186)$"])] + public void Record_Obj_Pos1(Event @event, ScriptAccessory accessory) + { + Vector3 rawPos = @event.SourcePosition; + Vector2 checkPos = new Vector2(rawPos.X, rawPos.Z); + + bool isValid = false; + foreach (var v in _validCoords) + { + if (Vector2.Distance(checkPos, v) < 1.0f) + { + isValid = true; + break; + } + } + if (!isValid) return; + + uint sid = (uint)@event.SourceId; + + if (_objStorage1.ContainsKey(sid)) + { + _objStorage1.Remove(sid); + } + else + { + if (uint.TryParse(@event["SourceDataId"], out var did)) + { + _orderCounter++; + + var newObj = new ObjectStateSix + { + DataId = did, + Position = rawPos, + Rotation = @event.SourceRotation, + Index = _orderCounter, + IsDrawn = false + }; + _objStorage1[sid] = newObj; + + // 【关键逻辑】如果机制已经开�?在最�?0秒内),且该物体还没画,立刻补�? + // 这种情况属于:BOSS先读条,物体后刷出来 + long now = DateTime.Now.Ticks; + if (_mechanic47086StartTime > 0 && (now - _mechanic47086StartTime < 20 * 10000000)) + { + TryDrawSingleObject(newObj, sid, (uint)@event.SourceId, accessory); + } + } + } + } + [ScriptMethod(name: "6连斧镰剑", eventType: EventTypeEnum.StartCasting, eventCondition: ["ActionId:47086"])] + public void Action_47086_Draw(Event @event, ScriptAccessory accessory) + { + // 1. 记录机制开始时�? + _mechanic47086StartTime = DateTime.Now.Ticks; + + // 2. 遍历当前已有的物体进行绘�? + // (防止物体是先刷出来,BOSS后读条的情况) + if (_objStorage1.Count == 0) return; + + foreach (var kvp in _objStorage1) + { + TryDrawSingleObject(kvp.Value, kvp.Key, (uint)@event.SourceId, accessory); + } + } + [ScriptMethod(name: "大漩涡", eventType: EventTypeEnum.StartCasting, eventCondition: ["ActionId:46120"])] + public void Action_46120_Fan(Event @event, ScriptAccessory accessory) + { + // 1. 找到场上所�?DataId �?19183 的物�? + var towers = accessory.Data.Objects.Where(x => x.DataId == 19183).ToList(); + + if (towers.Count == 0) return; + + // 2. 遍历每一个物�? + foreach (var tower in towers) + { + // 3. 对最近的 2 个玩家画�?(Index 1 �?2) + for (uint i = 1; i <= 2; i++) + { + var dp = accessory.Data.GetDefaultDrawProperties(); + dp.Name = $"Fan_19183_{tower.EntityId}_{i}_{DateTime.Now.Ticks}"; + + // 【关键】Owner 设为物体,这样“最近”就是相对于物体的距�? + dp.Owner = tower.EntityId; + + // 使用你要求的方法:自动解析最近的玩家 + dp.TargetResolvePattern = PositionResolvePatternEnum.PlayerNearestOrder; + dp.TargetOrderIndex = i; // 1 = 最近的, 2 = 第二近的 + + dp.Scale = new Vector2(60f); // �?60 + dp.Radian = float.Pi / 2; // 90�? + dp.Color = accessory.Data.DefaultDangerColor; // 渐变危险 + dp.DestoryAt = 2300; // 4�? + dp.ScaleMode = ScaleMode.ByTime; // 渐变填充 + + // Fan 类型会自动追�?Target (即解析出的玩�? + accessory.Method.SendDraw(DrawModeEnum.Default, DrawTypeEnum.Fan, dp); + } + } + } + [ScriptMethod(name: "19183危险圆", eventType: EventTypeEnum.AddCombatant, eventCondition: ["DataId:19183"])] + public void OnAddCombatant_19183_Circle(Event @event, ScriptAccessory accessory) + { + var dp = accessory.Data.GetDefaultDrawProperties(); + dp.Name = $"Circle_19183_{@event.SourceId}"; + dp.Position = @event.SourcePosition; + dp.Scale = new Vector2(4f); + dp.Color = accessory.Data.DefaultDangerColor; + dp.DestoryAt = 120000; + accessory.Method.SendDraw(DrawModeEnum.Default, DrawTypeEnum.Circle, dp); + } + + [ScriptMethod(name: "19183危险圆移除", eventType: EventTypeEnum.RemoveCombatant, eventCondition: ["DataId:19183"])] + public void OnRemoveCombatant_19183_Circle(Event @event, ScriptAccessory accessory) + { + accessory.Method.RemoveDraw($"Circle_19183_{@event.SourceId}"); + } + [ScriptMethod(name: "星轨链", eventType: EventTypeEnum.StartCasting, eventCondition: ["ActionId:46131"])] + public void OnCast_46131(Event @event, ScriptAccessory accessory) + { + long nowTicks = DateTime.Now.Ticks; + uint sourceId = (uint)@event.SourceId; + Vector3 sourcePos = @event.SourcePosition; + float sourceRot = @event.SourceRotation; + const int duplicateWindowMs = 300; + const float duplicatePosTolerance = 0.2f; + const float duplicateRotTolerance = 0.05f; + if (_starTrackLastCastBySource.TryGetValue(sourceId, out var lastCast)) + { + bool withinTime = nowTicks - lastCast.Ticks < duplicateWindowMs * 10000L; + bool samePos = Vector3.Distance(lastCast.Position, sourcePos) <= duplicatePosTolerance; + bool sameRot = MathF.Abs(NormalizeAngle(sourceRot - lastCast.Rotation)) <= duplicateRotTolerance; + if (withinTime && samePos && sameRot) + { + return; + } + } + _starTrackLastCastBySource[sourceId] = (nowTicks, sourcePos, sourceRot); + + int castIndex = System.Threading.Interlocked.Increment(ref _castCount_46131); + _starTrackLastCastTicks = nowTicks; + + int roundIndex = ((castIndex - 1) / 2) % 8 + 1; + bool isFirstInRound = castIndex % 2 == 1; + int delayDanger = 3800; + int dangerDuration = 2200; + bool isStartRound = roundIndex == 1 || roundIndex == 5; + if (isStartRound) + { + delayDanger = 0; + dangerDuration = 6000; + } + + // 4. 构建绘图 + var dpDanger = accessory.Data.GetDefaultDrawProperties(); + dpDanger.Name = $"Rect_46131_Danger_{castIndex}_{DateTime.Now.Ticks}"; + dpDanger.Color = accessory.Data.DefaultDangerColor; + dpDanger.Scale = new Vector2(10f, 60f); + dpDanger.Position = sourcePos; + dpDanger.Rotation = sourceRot; + dpDanger.ScaleMode = ScaleMode.ByTime; + dpDanger.Delay = delayDanger; + dpDanger.DestoryAt = dangerDuration; + accessory.Method.SendDraw(DrawModeEnum.Default, DrawTypeEnum.Rect, dpDanger); + + if (isFirstInRound) + { + _starTrackFirstCastTicks = nowTicks; + _starTrackFirstBlocks = new HashSet(GetStarTrackHitBlocks(sourcePos, sourceRot)); + return; + } + + if (_starTrackFirstBlocks.Count == 0) + { + _starTrackFirstCastTicks = 0; + _starTrackFirstBlocks.Clear(); + return; + } + + _starTrackFirstCastTicks = 0; + _starTrackFirstBlocks.Clear(); + } + // ==================== 3. 监听 46148 读条并记�?==================== + [ScriptMethod(name: "记录状态", eventType: EventTypeEnum.StartCasting, eventCondition: ["ActionId:46148"])] + public void Record_46148(Event @event, ScriptAccessory accessory) + { + // 一旦监测到这个读条,就标记�?true + _hasCast46148 = true; + + // (可�? 可以在屏幕打印一句调试信息,确认脚本记录到了 + // accessory.Method.SendChat("/e Detected 46148, Flag set to true."); + } + + + [ScriptMethod(name: "彗星/火焰吐息", eventType: EventTypeEnum.TargetIcon, eventCondition: ["Id:00F4"])] + public void OnTargetIcon_00F4(Event @event, ScriptAccessory accessory) + { + // 通用步骤:解析被点名玩家 ID + string tidStr = @event["TargetId"]; + if (string.IsNullOrEmpty(tidStr) || + !ulong.TryParse(tidStr.Replace("0x", ""), System.Globalization.NumberStyles.HexNumber, null, out var targetId)) + { + return; + } + + var dp = accessory.Data.GetDefaultDrawProperties(); + dp.Color = accessory.Data.DefaultDangerColor; + // 渐变模式都是一样的 + dp.ScaleMode = ScaleMode.ByTime; + + // ================= 分支逻辑 ================= + + if (!_hasCast46148) + { + // Case A: 46148 还没读过 -> �?8.2s 的圆 (4m) + dp.Name = $"Icon_00F4_Circle_{targetId}_{DateTime.Now.Ticks}"; + dp.Owner = targetId; // 绑在玩家身上 + dp.Scale = new Vector2(4f); // 半径 4m + dp.DestoryAt = 8200; // 持续 8.2s + + // 只有这里需�?ScaleMode �?ByTime (圆扩�?,下面矩形需�?YByTime + dp.ScaleMode = ScaleMode.ByTime; + + accessory.Method.SendDraw(DrawModeEnum.Default, DrawTypeEnum.Circle, dp); + } + else + { + // Case B: 46148 已经读过 -> 延迟 3s,从 19180 连线 (�?m) + + // 1. 寻找场上�?19180 物体 + // (如果有多个,这里默认找第一个;如果需要找最近的,可以用 OrderByDistance) + var sourceObj = accessory.Data.Objects.FirstOrDefault(x => x.DataId == 19180); + if (sourceObj == null) return; // 没找到物体就不画 + + dp.Name = $"Link_Delay3s_19180_{targetId}_{DateTime.Now.Ticks}"; + + // 2. 连线关系:起�?19180 -> 终点 玩家 + dp.Owner = sourceObj.EntityId; + dp.TargetObject = targetId; + + // 3. 尺寸:宽 6m,长 60m + dp.Scale = new Vector2(6f, 60f); + + // 4. 时间控制:延�?3s,持�?5s + dp.Delay = 3000; + dp.DestoryAt = 6500; + + // 5. 动画:矩形伸�? + dp.ScaleMode = ScaleMode.YByTime; + + accessory.Method.SendDraw(DrawModeEnum.Default, DrawTypeEnum.Rect, dp); + } + } + [ScriptMethod(name: "王者陨石震", eventType: EventTypeEnum.Tether, eventCondition: ["Id:0039"])] + public void OnTether_0039(Event @event, ScriptAccessory accessory) + { + // 【关键修改】如果还没读�?46162,直接结束,不画�? + if (!_hasCast46162) + { + return; + } + + // --- 以下是原本的绘图逻辑 --- + + // 1. 解析目标玩家 (TargetId) + string tidStr = @event["TargetId"]; + if (string.IsNullOrEmpty(tidStr) || + !ulong.TryParse(tidStr.Replace("0x", ""), System.Globalization.NumberStyles.HexNumber, null, out var targetId)) + { + return; + } + + // 2. 构建绘图属�? + var dp = accessory.Data.GetDefaultDrawProperties(); + + dp.Name = $"Tether_0039_Rect_{targetId}_{DateTime.Now.Ticks}"; + + // 颜色:危�? + dp.Color = accessory.Data.DefaultDangerColor; + + // 尺寸:宽 10m,长 60m + dp.Scale = new Vector2(10f, 60f); + + // 起点:连线的发起�?(Source) + dp.Owner = @event.SourceId; + + // 终点/朝向:连线的接受�?(Target玩家) + dp.TargetObject = targetId; + + // 持续时间 7.5�? + dp.DestoryAt = 7500; + + // 动画:随时间填充 (渐变效果) + dp.ScaleMode = ScaleMode.YByTime; + + // 3. 发送绘�? + accessory.Method.SendDraw(DrawModeEnum.Default, DrawTypeEnum.Rect, dp); + } + [ScriptMethod(name: "回旋火", eventType: EventTypeEnum.StartCasting, eventCondition: ["ActionId:regex:^(47037|46170)$"])] + public void RotatingFire_Draw(Event @event, ScriptAccessory accessory) + { + if (!uint.TryParse(@event["ActionId"], out var aid)) return; + // 获取读条持续时间 + if (!int.TryParse(@event["DurationMilliseconds"], out var dur)) return; + + // 1. 根据 ID 判断连线人数 + // 47038 (双向) -> 最�?2 �? + // 46171 (四向) -> 最�?4 �? + int targetCount = (aid == 47037) ? 2 : 4; + + // 2. 循环绘制每一条矩�? + for (uint i = 1; i <= targetCount; i++) + { + var dp = accessory.Data.GetDefaultDrawProperties(); + + dp.Name = $"Fire_Rect_Link_{aid}_{i}_{DateTime.Now.Ticks}"; + dp.Color = GuideColor.V4; + + // 尺寸:宽 6m,长 60m (给长一点以保证覆盖) + dp.Scale = new Vector2(6f, 60f); + + // 起点:BOSS (SourcePosition) + dp.Owner = @event.SourceId; + + // 终点/朝向:自动解析最近的玩家 + // i=1 为最近的�?个,i=2 为最近的�?�?.. + dp.TargetResolvePattern = PositionResolvePatternEnum.PlayerNearestOrder; + dp.TargetOrderIndex = i; + + // 持续时间 + dp.DestoryAt = dur; + + // 动画:随时间填充 (渐变) + dp.ScaleMode = ScaleMode.YByTime; + + accessory.Method.SendDraw(DrawModeEnum.Default, DrawTypeEnum.Rect, dp); + } + } + // ==================== 1. 记录 TargetIcon 001E ==================== + [ScriptMethod(name: "记录点名001E", eventType: EventTypeEnum.TargetIcon, eventCondition: ["Id:001E"])] + public void OnTargetIcon_001E(Event @event, ScriptAccessory accessory) + { + string tidStr = @event["TargetId"]; + if (string.IsNullOrEmpty(tidStr) || + !ulong.TryParse(tidStr.Replace("0x", ""), System.Globalization.NumberStyles.HexNumber, null, out var targetId)) + { + return; + } + _markedPlayers.Add((uint)targetId); + } + + // ==================== 2. 处理读条 + 计数触发 ==================== + // [ScriptMethod(name: "象限连线机制_计数触发", eventType: EventTypeEnum.StartCasting, eventCondition: ["ActionId:regex:^(46166|46167)$"])] + // public void OnCast_Mechanic_Count(Event @event, ScriptAccessory accessory) + // { + // if (!uint.TryParse(@event["ActionId"], out var aid)) return; + // if (!int.TryParse(@event["DurationMilliseconds"], out var dur)) return; + + // Vector3 pos = @event.SourcePosition; + + // // --- A. 计算象限 --- + // int quadrant = 0; + // // 按照您的定义�? + // if (pos.Z < 100 && pos.X > 100) quadrant = 1; // 右上 + // else if (pos.X > 100 && pos.Z > 100) quadrant = 2; // 右下 + // else if (pos.X < 100 && pos.Z > 100) quadrant = 3; // 左下 + // else if (pos.X < 100 && pos.Z < 100) quadrant = 4; // 左上 + + // if (quadrant == 0) return; + + // // --- B. 加入列表 --- + // _castingObjects.Add(new MechanicObject + // { + // ActionId = aid, + // SourceId = (uint)@event.SourceId, + // Quadrant = quadrant, + // Duration = dur + // }); + + // // --- C. 计数判定 --- + // // 当且仅当收集到第 4 个物体时,触发逻辑 + // if (_castingObjects.Count == 4) + // { + // ProcessMechanicLogic(accessory); + // } + // } + [ScriptMethod(name: "ENV22-25", eventType: EventTypeEnum.EnvControl, eventCondition: ["Index:regex:^(22|23|24|25)$"])] + public void OnEnvControl_Rect_Draw(Event @event, ScriptAccessory accessory) + { + // ============================================================ + // 1. 直接读取 Flag + // ============================================================ + // 不用 ContainsKey,直接信你用 Flag + string flagStr = @event["Flag"]; + + // 解析十六进制字符�? + if (string.IsNullOrEmpty(flagStr) || + !uint.TryParse(flagStr, System.Globalization.NumberStyles.HexNumber, null, out uint flagValue)) + { + return; + } + + // 2. 核心判定:Flag 必须�?2 + if (flagValue != 2) return; + + // ============================================================ + // 3. 解析 Index 并确�?X 坐标 + // ============================================================ + if (!int.TryParse(@event["Index"], out int index)) return; + + float posX = 0; + switch (index) + { + case 22: posX = 79f; break; + case 23: posX = 89f; break; + case 24: posX = 111f; break; + case 25: posX = 121f; break; + default: return; + } + + // ============================================================ + // 4. 绘图执行 + // ============================================================ + var dp = accessory.Data.GetDefaultDrawProperties(); + dp.Name = $"Env_Rect_{index}_{DateTime.Now.Ticks}"; + dp.Color = SafeColor1.V4; + + // 尺寸: 40x5 (X=5, Y=40) + dp.Scale = new Vector2(10f, 40f); + + // 位置与朝�? + // Z范围 80~120 (全长40) + // 设起�?Z=80,朝�?0 (正南/Z增加方向),长 40 -> 完美覆盖 + dp.Position = new Vector3(posX, 0, 80f); + dp.Rotation = 0f; + + // 时间控制: 延时 23秒,持续 5�? + dp.Delay = 23000; + dp.DestoryAt = 5600; + + // 动画: 渐变 + dp.ScaleMode = ScaleMode.YByTime; + + accessory.Method.SendDraw(DrawModeEnum.Default, DrawTypeEnum.Rect, dp); + } + [ScriptMethod(name: "直线连线", eventType: EventTypeEnum.Tether, eventCondition: ["Id:regex:^(0039|00F9)$"])] + public void OnTether_0039_00F9(Event @event, ScriptAccessory accessory) + { + // 如果已经读过 46162,不处理 + if (_hasCast46162) + { + return; + } + + // 解析 TargetId + string tidStr = @event["TargetId"]; + if (string.IsNullOrEmpty(tidStr) || + !ulong.TryParse(tidStr.Replace("0x", ""), System.Globalization.NumberStyles.HexNumber, null, out var targetId)) + { + return; + } + + uint tid = (uint)targetId; + long now = DateTime.Now.Ticks; + long cooldown = 28 * 10000000L; // 28秒,单位�?ticks (1�?= 10000000 ticks) + + // 检查是否在冷却时间�? + if (_tether0039DrawnTime.TryGetValue(tid, out long lastTime)) + { + if (now - lastTime < cooldown) + { + // 还在冷却中,不重复画 + return; + } + } + + // 记录当前时间 + _tether0039DrawnTime[tid] = now; + + // 画矩�? + var dp = accessory.Data.GetDefaultDrawProperties(); + dp.Name = $"0039_{tid}"; + dp.Owner = @event.SourceId; + dp.TargetObject = tid; + dp.Scale = new Vector2(10f, 60f); + dp.Color = accessory.Data.DefaultDangerColor; + dp.Delay = 23000; + dp.DestoryAt = 5600; + dp.ScaleMode = ScaleMode.YByTime; + + accessory.Method.SendDraw(DrawModeEnum.Default, DrawTypeEnum.Rect, dp); + } + [ScriptMethod(name: "检测46162", eventType: EventTypeEnum.StartCasting, eventCondition: ["ActionId:46162"])] + public void OnCast_46162(Event @event, ScriptAccessory accessory) + { + _hasCast46162 = true; + } + #endregion + #region �? + [ScriptMethod(name: "记录点名001E", eventType: EventTypeEnum.TargetIcon, eventCondition: ["Id:001E"])] + public void OnTargetIcon_001E_Record(Event @event, ScriptAccessory accessory) + { + string tidStr = @event["TargetId"]; + if (string.IsNullOrEmpty(tidStr) || + !ulong.TryParse(tidStr.Replace("0x", ""), System.Globalization.NumberStyles.HexNumber, null, out var targetId)) + { + return; + } + + _targetIcon001EPlayers.Add((uint)targetId); + } + + private object _lock46166_46167 = new object(); + + [ScriptMethod(name: "1122塔", eventType: EventTypeEnum.StartCasting, eventCondition: ["ActionId:regex:^(46166|46167)$"])] + public void OnCast_46166_46167_Record(Event @event, ScriptAccessory accessory) + { + if (!uint.TryParse(@event["ActionId"], out var actionId)) return; + if (!int.TryParse(@event["DurationMilliseconds"], out var duration)) return; + + Vector3 pos = @event.SourcePosition; + uint sourceId = (uint)@event.SourceId; + + int quadrant = 0; + if (pos.X > 100 && pos.Z < 100) quadrant = 1; + else if (pos.X > 100 && pos.Z > 100) quadrant = 2; + else if (pos.X < 100 && pos.Z > 100) quadrant = 3; + else if (pos.X < 100 && pos.Z < 100) quadrant = 4; + + if (quadrant == 0) return; + + int currentCount = 0; + bool shouldDraw = false; + + lock (_lock46166_46167) + { + if (_castingObjects46166_46167.Any(x => x.SourceId == sourceId)) + { + return; + } + + _castingObjects46166_46167.Add((sourceId, actionId, quadrant)); + currentCount = _castingObjects46166_46167.Count; + + if (currentCount >= 4) + { + shouldDraw = true; + } + } + + if (shouldDraw) + { + if (Phase4_Towers1 == Phase4_Towers.近战顺远程逆) + { + DrawDisplacementLogic(accessory, duration); + } + if (Phase4_Towers1 == Phase4_Towers.全引导) + { + DrawDisplacementLogic1(accessory, duration); + } + } + } + private void DrawDisplacementLogic1(ScriptAccessory accessory, int duration) + { + uint myId = accessory.Data.Me; + var party = accessory.Data.PartyList; + int myIndex = -1; + + for (int i = 0; i < party.Count; i++) + { + if (party[i] == myId) + { + myIndex = i; + break; + } + } + + if (myIndex == -1) + { + _castingObjects46166_46167.Clear(); + _targetIcon001EPlayers.Clear(); + return; + } + + var objs46166 = _castingObjects46166_46167 + .Where(x => x.ActionId == 46166) + .OrderBy(x => x.Quadrant) + .ToList(); + + var objs46167 = _castingObjects46166_46167 + .Where(x => x.ActionId == 46167) + .OrderBy(x => x.Quadrant) + .ToList(); + + uint targetSourceId = 0; + bool skipDraw = false; + + if (myIndex == 0) + { + if (objs46166.Count >= 1) + { + targetSourceId = objs46166[0].SourceId; + } + } + else if (myIndex == 1) + { + if (objs46166.Count >= 2) + { + targetSourceId = objs46166[1].SourceId; + } + } + else if (myIndex == 4) + { + if (objs46167.Count >= 1) + { + targetSourceId = objs46167[0].SourceId; + } + } + else if (myIndex == 5) + { + if (objs46167.Count >= 2) + { + targetSourceId = objs46167[1].SourceId; + } + } + else if (myIndex == 2 || myIndex == 3 || myIndex == 6 || myIndex == 7) + { + if (_targetIcon001EPlayers.Contains(myId)) + { + skipDraw = true; + } + else + { + int[] priorityOrder = { 2, 6, 7, 3 }; + + var availablePlayers = priorityOrder + .Where(idx => idx < party.Count && !_targetIcon001EPlayers.Contains(party[idx])) + .ToList(); + + int myRank = -1; + for (int i = 0; i < availablePlayers.Count; i++) + { + if (availablePlayers[i] == myIndex) + { + myRank = i; + break; + } + } + + if (myRank == -1) + { + skipDraw = true; + } + else if (myRank == 0) + { + if (objs46167.Count >= 1) + { + targetSourceId = objs46167[0].SourceId; + } + } + else if (myRank == 1) + { + if (objs46167.Count >= 2) + { + targetSourceId = objs46167[1].SourceId; + } + } + else + { + skipDraw = true; + } + } + } + + if (!skipDraw && targetSourceId != 0) + { + var dp = accessory.Data.GetDefaultDrawProperties(); + dp.Name = $"Displacement_{myId}_{targetSourceId}_{DateTime.Now.Ticks}"; + dp.Owner = myId; + dp.TargetObject = targetSourceId; + dp.Scale = new Vector2(0.5f); + dp.ScaleMode = ScaleMode.YByDistance; + dp.Color = GuideColor.V4; + dp.DestoryAt = duration; + + accessory.Method.SendDraw(DrawModeEnum.Default, DrawTypeEnum.Displacement, dp); + } + + _castingObjects46166_46167.Clear(); + _targetIcon001EPlayers.Clear(); + } + + + private void DrawDisplacementLogic(ScriptAccessory accessory, int duration) + { + uint myId = accessory.Data.Me; + var party = accessory.Data.PartyList; + int myIndex = -1; + + for (int i = 0; i < party.Count; i++) + { + if (party[i] == myId) + { + myIndex = i; + break; + } + } + + if (myIndex == -1) + { + _castingObjects46166_46167.Clear(); + _targetIcon001EPlayers.Clear(); + return; + } + + var objs46166 = _castingObjects46166_46167 + .Where(x => x.ActionId == 46166) + .OrderBy(x => x.Quadrant) + .ToList(); + + var objs46167 = _castingObjects46166_46167 + .Where(x => x.ActionId == 46167) + .OrderBy(x => x.Quadrant) + .ToList(); + + uint targetSourceId = 0; + bool skipDraw = false; + + if (myIndex == 0) + { + if (objs46166.Count >= 1) + { + targetSourceId = objs46166[0].SourceId; + } + } + else if (myIndex == 1) + { + if (objs46166.Count >= 2) + { + targetSourceId = objs46166[1].SourceId; + } + } + else if (myIndex >= 2 && myIndex <= 7) + { + if (_targetIcon001EPlayers.Contains(myId)) + { + skipDraw = true; + } + else + { + if (myIndex == 4 || myIndex == 5) + { + if (objs46167.Count >= 1) + { + targetSourceId = objs46167[0].SourceId; + } + } + else if (myIndex == 2 || myIndex == 3 || myIndex == 6 || myIndex == 7) + { + if (objs46167.Count >= 2) + { + targetSourceId = objs46167[1].SourceId; + } + } + } + } + + if (!skipDraw && targetSourceId != 0) + { + var dp = accessory.Data.GetDefaultDrawProperties(); + dp.Name = $"Displacement_{myId}_{targetSourceId}_{DateTime.Now.Ticks}"; + dp.Owner = myId; + dp.TargetObject = targetSourceId; + dp.Scale = new Vector2(0.5f); + dp.ScaleMode = ScaleMode.YByDistance; + dp.Color = GuideColor.V4; + dp.DestoryAt = duration; + + accessory.Method.SendDraw(DrawModeEnum.Default, DrawTypeEnum.Displacement, dp); + } + + _castingObjects46166_46167.Clear(); + _targetIcon001EPlayers.Clear(); + } + [ScriptMethod(name: "玩家死亡移除连线", eventType: EventTypeEnum.Death)] + public void OnPlayerDeath(Event @event, ScriptAccessory accessory) + { + // 解析死亡�?TargetId + string tidStr = @event["TargetId"]; + if (string.IsNullOrEmpty(tidStr) || + !ulong.TryParse(tidStr.Replace("0x", ""), System.Globalization.NumberStyles.HexNumber, null, out var targetId)) + { + return; + } + + uint tid = (uint)targetId; + + // 移除对应的画图元�? + accessory.Method.RemoveDraw($"0039_{tid}"); + + // 同时从冷却记录中移除,以便复活后可以重新�? + if (_tether0039DrawnTime.ContainsKey(tid)) + { + _tether0039DrawnTime.Remove(tid); + } + } + #endregion + + + } +} + + + diff --git a/a16239438/(M9S)AAC Heavyweight M1 (Savage).cs b/a16239438/(M9S)AAC Heavyweight M1 (Savage).cs new file mode 100644 index 0000000..2110150 --- /dev/null +++ b/a16239438/(M9S)AAC Heavyweight M1 (Savage).cs @@ -0,0 +1,912 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Numerics; +using System.Threading.Tasks; +using KodakkuAssist.Script; +using KodakkuAssist.Module.GameEvent; +using KodakkuAssist.Module.Draw; +using KodakkuAssist.Module.GameEvent.Struct; +using Dalamud.Utility.Numerics; + +using KodakkuAssist.Data; +using KodakkuAssist.Extensions; + +namespace RyougiMioScriptNamespace +{ + [ScriptType(name: "(M9S)AAC Heavyweight M1 (Savage)", territorys: [1320, 1321], guid: "ced5c285-484c-4750-bc85-241e927848f1", version: "1.0.0.0", author: "RyougiMio", note: "M9S,脚本同时在M9N/S中生效,注明TTS的机制仅有播报。")] + public class RyougiMio_1321 + { + #region Settings + // ==================== 用户设置区域 ==================== + [UserSetting("是否开启屏幕文字提示")] + public bool EnableText { get; set; } = true; + [UserSetting("是否开启TTS语音提示")] + public bool EnableTTS { get; set; } = true; + + [UserSetting("常用危险色")] + public ScriptColor DangerColor { get; set; } = new ScriptColor() { V4 = new Vector4(1.0f, 0.0f, 0.0f, 0.01f) }; + [UserSetting("常用安全色")] + public ScriptColor SafeColor { get; set; } = new ScriptColor() { V4 = new Vector4(0.0f, 1.0f, 0.0f, 0.01f) }; + + [UserSetting("指路/引导颜色 (默认为青)")] + public ScriptColor GuideColor { get; set; } = new ScriptColor() { V4 = new Vector4(0.0f, 1.0f, 1.0f, 0.01f) }; + #endregion + + #region Variables + private ScriptAccessory _acc; + private static long _chainsawTime = 0; + private static int _chainsawDelay = 0; + private int _moonCount = 0; + // 【修正】45969 扇形多组控制 + private static int _fan45969Count = 0; + private static long _fan45969LastTime = 0; + // 用字典记录每一组(GroupIndex)的预计结束时间 + private static Dictionary _fanGroupEndTimes = new Dictionary(); + + #endregion + + #region Methods + + // 静态字典:用来跨方法记录第一刀的时间 + private static Dictionary _moonPhaseDurations = new Dictionary(); + // 【新增】定义一个集合,用来记录当前场上所有有BUFF 4729 的玩家ID + private HashSet _active4729Tids = new HashSet(); + // 自定义TTS方法:自动检查 EnableTTS 开关 + private void QTTS(string text, int rate = 0) + { + if (!EnableTTS) return; + _acc.Method.TTS(text, rate); + } + // 自定义文字提示方法:自动检查 EnableText 开关 + private void QText(string text, int duration, bool isWarning = false) + { + if (!EnableText) return; + _acc.Method.TextInfo(text, duration, isWarning); + } + + + + #endregion + + #region Initialization + + public void Init(ScriptAccessory accessory) + { + accessory.Method.RemoveDraw(".*"); + _moonPhaseDurations.Clear(); + _acc = accessory; + _chainsawTime = 0; + _chainsawDelay = 0; + _moonCount = 0; + // 【修正】重置 + _fan45969Count = 0; + _fan45969LastTime = 0; + _fanGroupEndTimes.Clear(); + accessory.Method.SendChat("/e M9S Initialized."); + } + + #endregion + #region TTSonly + + [ScriptMethod(name: "魅亡之音TTS", eventType: EventTypeEnum.StartCasting, eventCondition: ["ActionId:regex:^(45921|45956)$"])] + public void AOE_Alert(Event @event, ScriptAccessory accessory) + { + QTTS("AOE"); + QText("AOE", 4700, true); + } + [ScriptMethod(name: "音速流散TTS", eventType: EventTypeEnum.StartCasting, eventCondition: ["ActionId:regex:^(45980)$"])] + public void AOE_Alert1(Event @event, ScriptAccessory accessory) + { + QTTS("分散"); + } + [ScriptMethod(name: "音速集聚TTS", eventType: EventTypeEnum.StartCasting, eventCondition: ["ActionId:regex:^(45981)$"])] + public void AOE_Aler2t(Event @event, ScriptAccessory accessory) + { + QTTS("分摊"); + } + + [ScriptMethod(name: "硬核之声TTS", eventType: EventTypeEnum.StartCasting, eventCondition: ["ActionId:regex:^(45915|45951|45916|45952)$"])] + public void Hardcore_Voice(Event @event, ScriptAccessory accessory) + { + if (!uint.TryParse(@event["ActionId"], out var aid)) return; + + // 45916 和 45952 是强化版 + if (aid == 45916 || aid == 45952) + { + QTTS("强化双T死刑"); + QText("强化双T死刑", 5000, true); + } + else + { + // 45915 和 45951 是普通版 + QTTS("双T死刑"); + QText("双T死刑", 5000, true); + } + } + [ScriptMethod(name: "剥蚀的低嗓_尖锐的音调TTS", eventType: EventTypeEnum.StartCasting, eventCondition: ["ActionId:regex:^(45957|45958)$"])] + public void Erosion_Voice_Pitch(Event @event, ScriptAccessory accessory) + { + if (!uint.TryParse(@event["ActionId"], out var aid)) return; + // 45957 全体分散 + if (aid == 45957) + { + QTTS("分散"); + QText("全体分散", 5000, true); + } + // 45958 分组分摊 + else if (aid == 45958) + { + QTTS("分摊"); + QText("分组分摊", 5000, true); + } + } + #endregion + + + + + + #region 左右刀 + + [ScriptMethod(name: "月之半相_左右半场刀", eventType: EventTypeEnum.StartCasting, eventCondition: ["ActionId:regex:^(45906|45907|45910|45911|45943|45944|45947|45948)$"])] + public void Half_Moon(Event @event, ScriptAccessory accessory) + { + // 1. 获取时间 + var totalDuration = int.Parse(@event["DurationMilliseconds"]); + if (!uint.TryParse(@event["ActionId"], out var aid)) return; + + // 2. ID 分组 + var leftSideIds = new HashSet { 45906, 45911, 45943, 45948 }; // 左组 + var secondHitIds = new HashSet { 45907, 45911, 45944, 45948 }; // 后手组 + var pairMap = new Dictionary + { + { 45907, 45906 }, { 45911, 45910 }, + { 45944, 45943 }, { 45948, 45947 } + }; + + bool isLeft = leftSideIds.Contains(aid); + bool isSecond = secondHitIds.Contains(aid); + + // 【关键修改】计数逻辑:只有是第一刀的时候,才算作新的一次机制,计数+1 + + _moonCount++; + + + // 3. 基础参数与位置判断 + Vector3 drawPos; + + // 【关键修改】如果是第3次或第4次,使用 SourcePosition;否则使用场地中心 + if (_moonCount == 3 || _moonCount == 4) + { + drawPos = @event.SourcePosition; + } + else + { + drawPos = new Vector3(100f, 0f, 100f); + } + + var srcRot = @event.SourceRotation; + float width = 40f; + float length = 40f; + + // 4. 角度旋转逻辑 + // 左组:转 90 度 (+PI/2) + // 右组:转 -90 度 (-PI/2) + float rotOffset = isLeft ? (MathF.PI / 2) : -(MathF.PI / 2); + float finalRot = srcRot + rotOffset; + + // 5. 时间控制逻辑 + int delay = 0; + int destory = totalDuration; + + if (!isSecond) + { + _moonPhaseDurations[aid] = totalDuration; + } + else + { + // 如果是第二刀,尝试获取第一刀的时长 + if (pairMap.TryGetValue(aid, out var firstId)) + { + var firstDuration = _moonPhaseDurations.ContainsKey(firstId) ? _moonPhaseDurations[firstId] : 5000; + delay = firstDuration; + destory = totalDuration - delay; + } + } + + // 6. 绘制 + var dp = accessory.Data.GetDefaultDrawProperties(); + dp.Name = $"月之半相_{aid}_{DateTime.Now.Ticks}"; // 加个时间戳防止重名 + + dp.Position = drawPos; + dp.Rotation = finalRot; + dp.Scale = new Vector2(width, length); + dp.Color = accessory.Data.DefaultDangerColor; + dp.Delay = delay; + dp.DestoryAt = destory; + dp.ScaleMode = ScaleMode.YByTime; + + accessory.Method.SendDraw(DrawModeEnum.Default, DrawTypeEnum.Rect, dp); + } + [ScriptMethod(name: "强化月之半相_左右刀", eventType: EventTypeEnum.StartCasting, eventCondition: ["ActionId:regex:^(45908|45909|45912|45913|45945|45946|45949|45950)$"])] + public void Enhanced_Half_Moon(Event @event, ScriptAccessory accessory) + { + // 1. 获取时间 + var totalDuration = int.Parse(@event["DurationMilliseconds"]); + if (!uint.TryParse(@event["ActionId"], out var aid)) return; + // 左组:转 +90 度 + var leftSideIds = new HashSet { 45908, 45913, 45945, 45950 }; + // 后手组:需要延迟显示 (Wait for First) + var secondHitIds = new HashSet { 45909, 45913, 45946, 45950 }; + // 配对表:Key = 后手ID, Value = 先手ID + var pairMap = new Dictionary + { + { 45909, 45908 }, + { 45913, 45912 }, + { 45946, 45945 }, + { 45950, 45949 } + }; + + // 3. 坐标处理 (EffectPosition 四舍五入取整, Y=0) + // 这里使用 EffectPosition 并取整 + float px = MathF.Round(@event.EffectPosition.X); + float pz = MathF.Round(@event.EffectPosition.Z); + var drawPos = new Vector3(px, 0f, pz); + // 基础参数 + var srcRot = @event.SourceRotation; + bool isLeft = leftSideIds.Contains(aid); + bool isSecond = secondHitIds.Contains(aid); + float width = 40f; + float length = 40f; + // 4. 角度旋转逻辑 + // 左组:转 90 度 (+PI/2) + // 右组:转 -90 度 (-PI/2) + float rotOffset = isLeft ? (MathF.PI / 2) : -(MathF.PI / 2); + float finalRot = srcRot + rotOffset; + // 5. 时间控制逻辑 + int delay = 0; + int destory = totalDuration; + // 如果是先手刀,记录 Duration 供后手查询 + if (!isSecond) + { + _moonPhaseDurations[aid] = totalDuration; + } + // 如果是后手刀,查询先手刀的 Duration 设定延迟 + else + { + if (pairMap.TryGetValue(aid, out var firstId)) + { + // 尝试获取先手的时间,默认5000 + var firstDuration = _moonPhaseDurations.ContainsKey(firstId) ? _moonPhaseDurations[firstId] : 5000; + delay = firstDuration; + destory = totalDuration - delay; // 保证同时结束 + } + } + // 6. 绘制 + var dp = accessory.Data.GetDefaultDrawProperties(); + dp.Name = $"强化月之半相_{aid}"; + + dp.Position = drawPos; + dp.Rotation = finalRot; + dp.Scale = new Vector2(width, length); + dp.Color = accessory.Data.DefaultDangerColor; + + dp.Delay = delay; + dp.DestoryAt = destory; + + dp.ScaleMode = ScaleMode.YByTime; + + accessory.Method.SendDraw(DrawModeEnum.Default, DrawTypeEnum.Rect, dp); + } + #endregion + #region 蝙蝠机制 + [ScriptMethod(name: "蝙蝠", eventType: EventTypeEnum.StartCasting, eventCondition: ["ActionId:45940"])] + public void Bat_Auto_Rotate_Direction(Event @event, ScriptAccessory accessory) + { + var bats = accessory.Data.Objects.Where(obj => obj.DataId == 19503); + var center = new Vector3(100f, 0, 100f); + + foreach (var bat in bats) + { + var batPos = bat.Position; + var batRot = bat.Rotation; + + // --- 2. 基础向量计算 --- + // 半径向量:从中心指向蝙蝠 + Vector3 vecRadius = batPos - center; + double dist = vecRadius.Length(); + + // 蝙蝠的朝向向量 + Vector3 vecBatFace = new Vector3((float)Math.Sin(batRot), 0, (float)Math.Cos(batRot)); + + // --- 3. 核心:计算两个候选点 (顺时针 vs 逆时针) --- + + // 候选点 A (顺时针旋转 90度): (z, -x) + Vector3 vecCW = new Vector3(vecRadius.Z, 0, -vecRadius.X); + + // 候选点 B (逆时针旋转 90度): (-z, x) + Vector3 vecCCW = new Vector3(-vecRadius.Z, 0, vecRadius.X); + + // --- 4. 决策:选哪一个? --- + // 计算点乘:如果 (向量A 与 蝙蝠朝向) 的夹角更小,点乘结果就越大 + // 意思是:蝙蝠脸朝向哪边,我们就往哪边转 + + Vector3 finalVec; + if (Vector3.Dot(vecCW, vecBatFace) > Vector3.Dot(vecCCW, vecBatFace)) + { + finalVec = vecCW; // 蝙蝠是顺时针跑的,选顺时针点 + } + else + { + finalVec = vecCCW; // 蝙蝠是逆时针跑的,选逆时针点 + } + + // 计算最终圆心坐标 = 中心 + 旋转后的向量 + Vector3 drawPos = center + finalVec; + + + // --- 5. 时序逻辑 (保持不变) --- + int delay = 0; + int duration = 0; + + if (dist > 5 && dist < 10) + { + delay = 0; duration = 8200; + } + else if (dist >= 10 && dist < 15) + { + delay = 8200; duration = 3500; + } + else if (dist >= 15 && dist < 30) + { + delay = 11700; duration = 3500; + } + else continue; + + // --- 6. 绘图 --- + var dp = accessory.Data.GetDefaultDrawProperties(); + dp.Name = $"Bat_SmartRot_{bat.EntityId}_{DateTime.Now.Ticks}"; + + dp.Owner = 0; // 不绑定 + dp.Position = drawPos; // 修正后的位置 + + dp.Scale = new Vector2(8f); + dp.Color = accessory.Data.DefaultDangerColor; + + dp.Delay = delay; + dp.DestoryAt = duration; + dp.ScaleMode = ScaleMode.ByTime; + + accessory.Method.SendDraw(DrawModeEnum.Default, DrawTypeEnum.Circle, dp); + } + } + // 1. 获得Buff:画圈 (持续时间设为无限,名字固定) + [ScriptMethod(name: "蝙蝠BUFF", eventType: EventTypeEnum.StatusAdd, eventCondition: ["StatusID:4729"])] + public void Status_4729_Add(Event @event, ScriptAccessory accessory) + { + var tidStr = @event["TargetId"]; + if (!ulong.TryParse(tidStr, System.Globalization.NumberStyles.HexNumber, null, out var tid)) + { + if (!ulong.TryParse(tidStr, out tid)) return; + } + + var dp = accessory.Data.GetDefaultDrawProperties(); + + dp.Name = $"Buff_4729_Ring_{tid}"; + + dp.Owner = tid; + dp.Color = accessory.Data.DefaultDangerColor; + dp.DestoryAt = 600000; + + // 空心月环 + dp.Scale = new Vector2(8.0f); + dp.InnerScale = new Vector2(7.95f); + dp.Radian = float.Pi * 2; + + accessory.Method.SendDraw(DrawModeEnum.Default, DrawTypeEnum.Donut, dp); + } + + // 2. 移除Buff:删除圈 + [ScriptMethod(name: "蝙蝠BUFF2", eventType: EventTypeEnum.StatusRemove, eventCondition: ["StatusID:4729"])] + public void Status_4729_Remove(Event @event, ScriptAccessory accessory) + { + var tidStr = @event["TargetId"]; + if (!ulong.TryParse(tidStr, System.Globalization.NumberStyles.HexNumber, null, out var tid)) + { + if (!ulong.TryParse(tidStr, out tid)) return; + } + + // 拼出和 Add 时候一样的名字 + string drawName = $"Buff_4729_Ring_{tid}"; + + // 手动删除 + accessory.Method.RemoveDraw(drawName); + } + + #endregion + #region 锤锯刃 + [ScriptMethod(name: "致命刑锯_前进", eventType: EventTypeEnum.StartCasting, eventCondition: ["ActionId:regex:^(45877|45927)$"])] + public void Chainsaw_Forward(Event @event, ScriptAccessory accessory) + { + var duration = int.Parse(@event["DurationMilliseconds"]); + var rawPos = @event.EffectPosition; + // 四舍五入取整 + var drawPos = new Vector3( + MathF.Round(rawPos.X), + 0, + MathF.Round(rawPos.Z) + ); + var dp = accessory.Data.GetDefaultDrawProperties(); + dp.Name = "致命刑锯_前进"; + dp.Position = drawPos; + dp.Rotation = @event.SourceRotation; + dp.Scale = new Vector2(20f, 10f); + dp.Color = accessory.Data.DefaultDangerColor; + dp.DestoryAt = duration; + dp.ScaleMode = ScaleMode.YByTime; + accessory.Method.SendDraw(DrawModeEnum.Default, DrawTypeEnum.Rect, dp); + } + [ScriptMethod(name: "致命刑锯_冲出", eventType: EventTypeEnum.StartCasting, eventCondition: ["ActionId:regex:^(45878|45879|45880|45928|45929|45930)$"])] + public void Chainsaw_RushOut(Event @event, ScriptAccessory accessory) + { + var duration = int.Parse(@event["DurationMilliseconds"]); + if (!uint.TryParse(@event["ActionId"], out var aid)) return; + // 1. 长度判断 + float length = 32f; + if (aid == 45879 || aid == 45929) length = 22f; + if (aid == 45880 || aid == 45930) length = 12f; + // 2. 时间逻辑 + long now = DateTimeOffset.Now.ToUnixTimeMilliseconds(); + long diff = now - _chainsawTime; + int finalDelay = 0; + int finalDuration = duration; + // 情况A: 初始化 或 间隔太久(>5s) -> 新的一轮机制 + if (_chainsawTime == 0 || Math.Abs(diff) > 5000) + { + finalDelay = 0; + _chainsawDelay = 0; + _chainsawTime = now; + } + // 情况B: 时差 < 1s -> 同一波次 + else if (diff < 1000) + { + finalDelay = _chainsawDelay; + finalDuration = duration - _chainsawDelay; + } + // 情况C: 时差 > 1s -> 连续波次 (第二波、第三波...) + else + { + finalDelay = duration - (int)diff; + finalDuration = (int)diff; + _chainsawDelay = finalDelay; + _chainsawTime = now; + } + // 3. 绘制 + var dp = accessory.Data.GetDefaultDrawProperties(); + dp.Name = $"致命刑锯_{aid}"; + dp.Position = @event.SourcePosition; + dp.Rotation = @event.SourceRotation; + dp.Scale = new Vector2(5f, length); + dp.Color = accessory.Data.DefaultDangerColor; + dp.Delay = finalDelay; + dp.DestoryAt = finalDuration; + dp.ScaleMode = ScaleMode.YByTime; + accessory.Method.SendDraw(DrawModeEnum.Default, DrawTypeEnum.Rect, dp); + } + [ScriptMethod(name: "致命刑轮锯", eventType: EventTypeEnum.AddCombatant, eventCondition: ["DataId:regex:^(19189|19190)$"])] + public void Straight_Entity_Spawn(Event @event, ScriptAccessory accessory) + { + if (!uint.TryParse(@event["DataId"], out var id)) return; + int duration = 120000; + float width = 0f; + float length = 0f; + if (id == 19189) + { + width = 6f; + length = 8f; + } + else if (id == 19190) + { + width = 12f; + length = 5f; + } + var dp = accessory.Data.GetDefaultDrawProperties(); + dp.Name = $"致命刑轮锯_{@event.SourceId}"; + dp.Owner = @event.SourceId; + dp.Rotation = @event.SourceRotation; + dp.Scale = new Vector2(width, length); + dp.Color = accessory.Data.DefaultDangerColor; + dp.DestoryAt = duration; + dp.ScaleMode = ScaleMode.None; + accessory.Method.SendDraw(DrawModeEnum.Default, DrawTypeEnum.Straight, dp); + } + #endregion + #region 以太流失 + [ScriptMethod(name: "以太流失_十字", eventType: EventTypeEnum.StartCasting, eventCondition: ["ActionId:regex:^(45897|45971)$"])] + public void Fatal_Cross_AOE(Event @event, ScriptAccessory accessory) + { + var duration = int.Parse(@event["DurationMilliseconds"]); + if (!uint.TryParse(@event["ActionId"], out var aid)) return; + float radius = 40f; + float width = (aid == 45971) ? 10f : 6f; + // 2. 循环 4 次,画 4 个方向 + for (int i = 0; i < 4; i++) + { + var dp = accessory.Data.GetDefaultDrawProperties(); + dp.Name = $"以太流失_{aid}_{i}"; + dp.Position = @event.SourcePosition; + dp.Rotation = @event.SourceRotation + (float)(Math.PI / 2 * i); + dp.Scale = new Vector2(width, radius); + dp.Color = accessory.Data.DefaultDangerColor; + dp.DestoryAt = duration; + dp.ScaleMode = ScaleMode.YByTime; + accessory.Method.SendDraw(DrawModeEnum.Default, DrawTypeEnum.Rect, dp); + } + } + #endregion + #region 瞎猜环节 + + + [ScriptMethod(name: "标记蝙蝠", eventType: EventTypeEnum.AddCombatant, eventCondition: ["DataId:regex:^(19502)$"])] + public void Mark_19502_Circle(Event @event, ScriptAccessory accessory) + { + var dp = accessory.Data.GetDefaultDrawProperties(); + dp.Name = $"Mark_19502_{@event.SourceId}"; + dp.Owner = @event.SourceId; + dp.Scale = new Vector2(0.5f); + dp.Color = accessory.Data.DefaultDangerColor; + dp.DestoryAt = 60000; + dp.ScaleMode = ScaleMode.None; + accessory.Method.SendDraw(DrawModeEnum.Default, DrawTypeEnum.Circle, dp); + } + // 定义防抖变量 + private long _lastTriggerTime_45989_Single = 0; + + [ScriptMethod(name: "M11S:旋转扇形 (单向6连/随动)", eventType: EventTypeEnum.StartCasting, eventCondition: ["ActionId:45989"])] + public void RotateFan_Single_Sequence(Event @event, ScriptAccessory accessory) + { + // --- 基础参数 --- + Vector3 centerPos = @event.SourcePosition; + + // 【修正1】起始方向:使用读条者当前的面向 (SourceRotation) + float startAngle = @event.SourceRotation; + + // 每一轮的旋转增量:22.5度 (Pi/8) + // 假设顺时针 (如方向反了请把下面计算的 + 改为 -) + float rotationStep = (float)(Math.PI / 8); + + // 扇形大小 + float fanRadian = (float)(Math.PI / 6); // 30度 + float fanRadius = 60f; + + // 累积延迟 + int currentDelay = 0; + + // --- 循环:6轮 (第1轮~第6轮) --- + // 只保留时间维度的循环,不再画空间上的8方(交给8个读条来源各自处理) + for (int i = 0; i < 6; i++) + { + // 计算当前轮持续时间 + int duration = (i == 0) ? 2700 : 2400; + + // 计算当前轮的角度 (基础面向 + 轮次 * 22.5度) + float finalRot = startAngle + (i * rotationStep); + + var dp = accessory.Data.GetDefaultDrawProperties(); + + // 【关键命名】必须包含 SourceId,因为有8个怪同时读条,不能重名 + dp.Name = $"RotFan_Single_{@event.SourceId}_{i}"; + + dp.Position = centerPos; + dp.Rotation = finalRot; + + dp.Radian = fanRadian; // 30度 + dp.Scale = new Vector2(fanRadius); // 60米 + + dp.Color = accessory.Data.DefaultDangerColor; + + // 时序控制 + dp.Delay = currentDelay; // 这一轮什么时候开始 + dp.DestoryAt = duration; // 这一轮存在多久 + dp.ScaleMode = ScaleMode.ByTime; // 渐变填充 + + accessory.Method.SendDraw(DrawModeEnum.Default, DrawTypeEnum.Fan, dp); + + // 累加延迟,让下一轮在这一轮结束后开始 + currentDelay += duration; + } + } + [ScriptMethod(name: "硬核之声", eventType: EventTypeEnum.StartCasting, eventCondition: ["ActionId:45951"])] + public void Circle_Target_6m(Event @event, ScriptAccessory accessory) + { + // --- 1. 获取时间 --- + if (!int.TryParse(@event["DurationMilliseconds"], out var dur)) return; + + // --- 2. 获取目标 (TargetId) --- + var tidStr = @event["TargetId"]; + if (string.IsNullOrEmpty(tidStr) || + !ulong.TryParse(tidStr.Replace("0x", ""), System.Globalization.NumberStyles.HexNumber, null, out var tid)) + return; + + // --- 3. 绘图 --- + var dp = accessory.Data.GetDefaultDrawProperties(); + dp.Name = $"Circle_6m_{tid}_{DateTime.Now.Ticks}"; + + dp.Owner = tid; // 跟随目标 + dp.Scale = new Vector2(6f); // 半径 6m + dp.Color = accessory.Data.DefaultDangerColor; + dp.DestoryAt = dur; + dp.ScaleMode = ScaleMode.ByTime; // 渐变 + + accessory.Method.SendDraw(DrawModeEnum.Default, DrawTypeEnum.Circle, dp); + } + + [ScriptMethod(name: "强化硬核之声", eventType: EventTypeEnum.StartCasting, eventCondition: ["ActionId:45952"])] + public void Circle_15m(Event @event, ScriptAccessory accessory) + { + // --- 1. 获取时间 --- + if (!int.TryParse(@event["DurationMilliseconds"], out var dur)) return; + + // --- 2. 绘图 --- + var dp = accessory.Data.GetDefaultDrawProperties(); + dp.Name = $"Circle_15m_{@event.SourceId}_{DateTime.Now.Ticks}"; + + // 45952 通常是 Boss 自身释放的 AOE,所以用 SourcePosition + // 如果它也是点名,改用 dp.Owner = TargetId 即可 + dp.Position = @event.SourcePosition; + + dp.Scale = new Vector2(15f); // 半径 15m + dp.Color = accessory.Data.DefaultDangerColor; + dp.DestoryAt = dur; + dp.ScaleMode = ScaleMode.ByTime; // 渐变 + + accessory.Method.SendDraw(DrawModeEnum.Default, DrawTypeEnum.Circle, dp); + } + [ScriptMethod(name: "以太流失_扇形", eventType: EventTypeEnum.StartCasting, eventCondition: ["ActionId:45969"])] + public void Fan_45_AOE(Event @event, ScriptAccessory accessory) + { + var duration = int.Parse(@event["DurationMilliseconds"]); + long now = DateTimeOffset.Now.ToUnixTimeMilliseconds(); + + // 1. 计数器重置逻辑 (防止灭团后计数不对) + // 如果距离上次读条超过15秒,视为新的一轮 + if (now - _fan45969LastTime > 15000) + { + _fan45969Count = 0; + _fanGroupEndTimes.Clear(); + } + _fan45969LastTime = now; + + // 计数 +1 + _fan45969Count++; + + // 2. 组别计算 + // 1, 2 -> Group 0 (第一组) + // 3, 4 -> Group 1 (第二组) + // 5, 6 -> Group 2 (第三组) ... + int groupIndex = (_fan45969Count - 1) / 2; + + // 3. 记录这一组的【绝对结束时间】 + long myEndTime = now + duration; + if (!_fanGroupEndTimes.ContainsKey(groupIndex) || myEndTime > _fanGroupEndTimes[groupIndex]) + { + _fanGroupEndTimes[groupIndex] = myEndTime; + } + + // 4. 计算延迟 + int delay = 0; + if (groupIndex > 0) + { + // 如果不是第一组,需要查上一组的结束时间 + if (_fanGroupEndTimes.TryGetValue(groupIndex - 1, out long prevEndTime)) + { + delay = (int)Math.Max(0, prevEndTime - now); + } + } + + // 5. 计算剩余显示的持续时间 (Duration - Delay) + int finalDuration = duration - delay; + if (finalDuration <= 0) finalDuration = 100; + + // 6. 绘图 + var dp = accessory.Data.GetDefaultDrawProperties(); + + // 【关键修改】名字加上 Ticks,保证绝对不会因为重名而“只画一个” + dp.Name = $"Fan45_{@event.SourceId}_{_fan45969Count}_{DateTime.Now.Ticks}"; + + dp.Position = @event.SourcePosition; + dp.Rotation = @event.SourceRotation; + + // 45度扇形 (X=半径60, Y=角度PI/4) + // 注意:你的框架好像是用 Scale.Y 来表示角度,Scale.X 表示半径 + // 请根据你之前给的正确范例:Radian=角度,Scale=半径 + dp.Radian = (float)Math.PI / 4; // 45度 + dp.Scale = new Vector2(60f); // 半径60 + + dp.Color = accessory.Data.DefaultDangerColor; + + dp.Delay = delay; + dp.DestoryAt = finalDuration; + dp.ScaleMode = ScaleMode.ByTime; + + accessory.Method.SendDraw(DrawModeEnum.Default, DrawTypeEnum.Fan, dp); + } + [ScriptMethod(name: "音速", eventType: EventTypeEnum.StartCasting, eventCondition: ["ActionId:regex:^(45980|45981)$"])] + public void Dual_Spell_Mechanic(Event @event, ScriptAccessory accessory) + { + if (!uint.TryParse(@event["ActionId"], out var aid)) return; + var duration = int.Parse(@event["DurationMilliseconds"]); + + // 1. 获取自己的ID + uint myId = accessory.Data.Me; + var partyIds = accessory.Data.PartyList; + + // 2. 预先检查“自己”是否有 4730 BUFF (这对分摊逻辑很重要) + bool iHave4730 = false; + var myObj = accessory.Data.Objects.FirstOrDefault(x => x.EntityId == myId); + if (myObj is IBattleChara me) + { + iHave4730 = me.StatusList.Any(s => s.StatusId == 4730); + } + + // 3. 确定自己的职能索引 (用于分散时的颜色判断) + int myIndex = -1; + for (int i = 0; i < partyIds.Count; i++) + { + if (partyIds[i] == myId) + { + myIndex = i; + break; + } + } + + // 4. 定义弧度常量 + float rad100 = 100f * (float.Pi / 180f); + float rad45 = float.Pi / 4; + + // 5. 遍历小队列表 + for (int i = 0; i < partyIds.Count; i++) + { + var tid = partyIds[i]; + var obj = accessory.Data.Objects.FirstOrDefault(x => x.EntityId == tid); + + if (obj is IBattleChara player) + { + // --- 【规则1】全局过滤:如果目标玩家有 4730,直接不画 --- + bool targetHas4730 = player.StatusList.Any(s => s.StatusId == 4730); + if (targetHas4730) continue; + + // 判断是否是“我” + bool isMe = (i == myIndex); + + // --- 【规则2】分摊逻辑控制 --- + if (aid == 45981) + { + // 如果“不是我” 且 “我自己没有4730”,则跳过不画 + // 意思是:只有当我有了4730,我才看得到别人的圈;否则我只看我自己的 + if (!isMe && !iHave4730) continue; + } + + // --- 角度逻辑 --- + float angle = 0f; + if (aid == 45980) + { + // 45980 (分散): T(0,1) 100度, 其他 45度 + bool isTank = (i == 0 || i == 1); + angle = isTank ? rad100 : rad45; + } + else + { + // 45981 (分摊): 全员 100度 + angle = rad100; + } + + // --- 颜色逻辑 --- + var drawColor = accessory.Data.DefaultDangerColor; // 默认红 + + if (aid == 45980) // 分散 + { + // 如果我是 D (索引>=4),看其他的 D (索引>=4) 为绿色 + if (myIndex >= 4) + { + if (i >= 4 && !isMe) + { + drawColor = accessory.Data.DefaultSafeColor; + } + } + } + else // 分摊 + { + // 如果画出来的不是我 (能进来说明我有4730),画绿色 + if (!isMe) + { + drawColor = accessory.Data.DefaultSafeColor; + } + } + + // --- 绘图 --- + var dp = accessory.Data.GetDefaultDrawProperties(); + dp.Name = $"DualSpell_{aid}_{player.EntityId}"; + + // 【关键】原点 Boss,目标 玩家 + dp.Owner = @event.SourceId; + dp.TargetObject = player.EntityId; + + dp.Radian = angle; + dp.Scale = new Vector2(60f); + + dp.Color = drawColor; + dp.DestoryAt = duration; + dp.ScaleMode = ScaleMode.ByTime; + + accessory.Method.SendDraw(DrawModeEnum.Default, DrawTypeEnum.Fan, dp); + } + } + } + [ScriptMethod(name: "Status2056绘图", eventType: EventTypeEnum.StatusAdd, eventCondition: ["StatusID:2056"])] + public void Status_2056_Add(Event @event, ScriptAccessory accessory) + { + // 1. 获取 StackCount (Buff层数) + if (!int.TryParse(@event["StackCount"], out var stack)) return; + + // 2. 获取 TargetId (目标ID) + var tidStr = @event["TargetId"]; + // StatusAdd 事件中的 TargetId 通常是 Hex 字符串,需要解析 + if (!uint.TryParse(tidStr, System.Globalization.NumberStyles.HexNumber, null, out var tid)) return; + + // 3. 定义画图名字 (格式:前缀_ID),方便后续移除 + string drawName = $"Status_2056_{tid}"; + + // 4. 判断逻辑 + if (stack == 38) + { + // --- 画 7m 危险圆形 --- + var dp = accessory.Data.GetDefaultDrawProperties(); + dp.Name = drawName; + dp.Owner = tid; // 绑定在玩家身上 + dp.Scale = new Vector2(7f); // 半径 7m + dp.Color = accessory.Data.DefaultDangerColor; + dp.DestoryAt = 99999; // 持续很久,直到手动移除 + dp.ScaleMode = ScaleMode.None; // 大小不变 + + accessory.Method.SendDraw(DrawModeEnum.Default, DrawTypeEnum.Circle, dp); + } + else if (stack == 39) + { + // --- 画 内径4 外径15 月环 --- + var dp = accessory.Data.GetDefaultDrawProperties(); + dp.Name = drawName; + dp.Owner = tid; // 绑定在玩家身上 + + dp.Scale = new Vector2(15f); // 外径 15 + dp.InnerScale = new Vector2(4f);// 内径 4 + dp.Radian = float.Pi * 2; // 360度整圆 + + dp.Color = accessory.Data.DefaultDangerColor; + dp.DestoryAt = 99999; + dp.ScaleMode = ScaleMode.None; + + accessory.Method.SendDraw(DrawModeEnum.Default, DrawTypeEnum.Donut, dp); + } + // 如果不是 38 或 39,什么都不做 + } + + [ScriptMethod(name: "Status2056移除", eventType: EventTypeEnum.StatusRemove, eventCondition: ["StatusID:2056"])] + public void Status_2056_Remove(Event @event, ScriptAccessory accessory) + { + // 解析 TargetId + var tidStr = @event["TargetId"]; + if (!uint.TryParse(tidStr, System.Globalization.NumberStyles.HexNumber, null, out var tid)) return; + + // 使用和 Add 时完全一样的名字规则 + string drawName = $"Status_2056_{tid}"; + + // 移除画图 + accessory.Method.RemoveDraw(drawName); + } + #endregion + + + } +} \ No newline at end of file diff --git "a/a16239438/(\345\246\226\346\230\237\344\271\261\350\210\236\347\273\235\345\242\203\346\210\230)P2 2222\346\214\207\350\267\257&\350\207\252\345\212\250\347\247\273\345\212\250.cs" "b/a16239438/(\345\246\226\346\230\237\344\271\261\350\210\236\347\273\235\345\242\203\346\210\230)P2 2222\346\214\207\350\267\257&\350\207\252\345\212\250\347\247\273\345\212\250.cs" new file mode 100644 index 0000000..a46c24c --- /dev/null +++ "b/a16239438/(\345\246\226\346\230\237\344\271\261\350\210\236\347\273\235\345\242\203\346\210\230)P2 2222\346\214\207\350\267\257&\350\207\252\345\212\250\347\247\273\345\212\250.cs" @@ -0,0 +1,2816 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using System.Linq; +using System.Numerics; +using System.Reflection; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using KodakkuAssist.Data; +using KodakkuAssist.Module.Draw; +using KodakkuAssist.Module.GameEvent; +using KodakkuAssist.Module.GameEvent.Struct; +using KodakkuAssist.Module.GameOperate; +using KodakkuAssist.Script; + +#nullable disable + +namespace RyougiMioScriptNamespace +{ + [ScriptType(name: "(妖星乱舞绝境战)P2 2222指路&自动移动", territorys: [1363], guid: "4c8f82b2-ab7f-45dc-bff1-ffce01bc67c8", version: "0.0.4.16", author: "RyougiMio", note: "电!\n指挥模式:每轮前4.5秒不显示头标。\n每轮后4.5秒显示本轮攻击1~4、禁止1~2、锁链1~2。\n攻击1234是踩塔的4人从左往右顺,禁止12是左侧的2闲人,锁链12是右侧的的2闲人 禁止1锁链1是靠近boss的 禁止2锁链2是远离boss的。\n!!!!!!!!自动移动依赖于PromeRotation!!!!!!!!")] + public class ScriptDraft + { + #region Settings + + [UserSetting("-----全局设置-----")] + public bool _____Global_Settings_____ { get; set; } = true; + + [UserSetting("是否开启屏幕文字提示")] + public bool EnableText { get; set; } = true; + + [UserSetting("是否开启TTS语音提示")] + public bool EnableTTS { get; set; } = true; + + [UserSetting("指挥模式(开启后允许脚本给全员上头标)")] + public bool EnableCommandMode { get; set; } = false; + + [UserSetting("指挥模式头标显示秒数(0-9)")] + public float CommandMarkDisplaySeconds { get; set; } = 4.5f; + + [UserSetting("启用PR GreenMove自动移动")] + public bool EnableGreenMove { get; set; } = false; + + [UserSetting("PR移动判定距离")] + public float GreenMoveTolerance { get; set; } = 0.1f; + + [UserSetting("PR移动速度倍率")] + public float GreenMoveSpeedMultiplier { get; set; } = 1.0f; + + [UserSetting("PR移动忽略Y轴")] + public bool GreenMoveIgnoreY { get; set; } = true; + + [UserSetting("PR移动前清空队列")] + public bool GreenMoveClearQueueBeforeMove { get; set; } = true; + + [UserSetting("PR移动最大读条等待ms")] + public int GreenMoveMaxCastWaitMs { get; set; } = 2500; + + [UserSetting("PR移动等待队列读条/滑步")] + public bool GreenMoveWaitForQueuedCast { get; set; } = true; + + [UserSetting("P2闲人处理机制时的左右站位")] + public P2TowerStrategy P2TowerStrategySetting { get; set; } = P2TowerStrategy.扇左钢右; + public enum P2TowerStrategy + { + 扇左钢右, + TN左D右_BBY闲固, + } + + [UserSetting("P2偶数轮闲人组站位")] + public EvenGroup2PriorityMode EvenGroup2PrioritySetting { get; set; } = EvenGroup2PriorityMode.近南远北; + public enum EvenGroup2PriorityMode + { + 近南远北, + 近北远南, + } + + [UserSetting("P2第八轮结束后集合位置")] + public P2LastRoundOuterEdgeMode P2LastRoundOuterEdgeSetting { get; set; } = P2LastRoundOuterEdgeMode.塔中间; + public enum P2LastRoundOuterEdgeMode + { + 塔中间, + A点, + } + + [UserSetting("P2第二轮按刷新头标重新分组")] + public bool RegroupOnRound2TargetIconRefresh { get; set; } = false; + + [UserSetting("Developer mode")] + public bool DeveloperMode { get; set; } = false; + + [UserSetting("开发模式:详细Debug日志")] + public bool VerboseDebugLog { get; set; } = false; + + [UserSetting("开发模式:记录读条ActionId")] + public bool LogStartCastingActionId { get; set; } = false; + + [UserSetting("常用危险色")] + public ScriptColor DangerColor { get; set; } = new ScriptColor { V4 = new Vector4(1.0f, 0.0f, 0.0f, 0.01f) }; + + [UserSetting("常用安全色")] + public ScriptColor SafeColor { get; set; } = new ScriptColor { V4 = new Vector4(0.0f, 1.0f, 0.0f, 0.01f) }; + + [UserSetting("指路/引导颜色")] + public ScriptColor GuideColor { get; set; } = new ScriptColor { V4 = new Vector4(0.0f, 1.0f, 1.0f, 1.0f) }; + + [UserSetting("P1打法")] + public Phase1Strategy P1StrategySetting { get; set; } = Phase1Strategy.Default; + public enum Phase1Strategy + { + Default, + Alternative, + } + + #endregion + + #region State + + private enum Phase + { + Init, + P1, + P2, + P3, + P4, + P5, + P6, + Done, + } + + private enum MarkerAlias + { + None, + OL1, + OL2, + OL3, + OL4, + OR1, + OR2, + OR3, + OR4, + EL1, + EL2, + EL3, + ER1, + ER2, + ER3, + EC1, + EC2, + } + + private const uint InvalidObjectId = 0xE0000000; + private const int MaxLoggedActionIds = 256; + private const string DrawPrefix = "KDYD"; + private const string DebugLogFileName = "KDYD_P2L_Debug.log"; + private const string GreenMovePrefix = "PromeRotation.GreenMove."; + private const uint TargetIcon02CB = 0x02CB; + private const uint TargetIcon02CC = 0x02CC; + private const uint TargetIcon02CD = 0x02CD; + private const uint BossOuterEdgeNorthActionId = 47826; + private const uint BossOuterEdgeSouthActionId = 47827; + private const uint BossOuterEdgeResolveNorthActionId = 47836; + private const uint BossOuterEdgeResolveSouthActionId = 47837; + private const int EnvBigCircleFirstIndex = 1; + private const int EnvBigCircleLastIndex = 8; + private const int EnvBigCircleGuideDuration = 60000; + private const int EnvBigCircleTargetIconUpdateCount = 4; + private const int EnvBigCircleLastRound = 8; + private const int EnvBigCircleLastRoundStartDelayMs = 650; + private const int EnvBigCircleLastRoundGuideDuration = 11000; + private const int EnvBigCircleLastRoundResolveGuideDuration = 5000; + private const int CommandMarkWindowMs = 9000; + private const float DefaultCommandMarkDisplaySeconds = 4.5f; + private const float EnvBigCircleDistance = 8.0f; + private const float EnvBigCircleRadius = 4.0f; + private const float EnvMarkerCircleRadius = 0.25f; + private const float EnvOuterEdgeGuideDistance = 8.0f; + private const float EnvLastRoundDefaultOe3Distance = 8.0f; + private const float EnvLastRoundDefaultOe4Distance = 6.0f; + private const float EnvOddLeftGreenOffset = 3.25f; + private const float EnvOddLeftRedSixOffset = 5.0f; + private const float EnvInnerMarkerOffset = EnvBigCircleRadius - EnvMarkerCircleRadius; + private const float EnvOuterMarkerOffset = EnvBigCircleRadius + EnvMarkerCircleRadius; + private const float EnvCenterCircleRadius = 6.0f; + private const float EnvCenterOuterMarkerOffset = EnvCenterCircleRadius + EnvMarkerCircleRadius; + private static readonly Vector3 ArenaCenter = new Vector3(100.0f, 0.0f, 100.0f); + private static readonly Vector3 EnvLastRoundFixedOe5Position = new Vector3(100.0f, 0.0f, 92.0f); + private static readonly Vector3 EnvLastRoundFixedOe6Position = new Vector3(100.0f, 0.0f, 106.0f); + private static readonly Vector3 DefaultNorth = new Vector3(0.0f, 0.0f, -1.0f); + private static readonly Vector4 SolidDangerRed = new Vector4(1.0f, 0.0f, 0.0f, 1.0f); + private static readonly Vector4 SolidSafeGreen = new Vector4(0.0f, 1.0f, 0.0f, 1.0f); + private static readonly string[] PartyPriorityLabels = new[] { "MT", "ST", "H1", "H2", "D1", "D2", "D3", "D4" }; + private static readonly int[][] TargetIconGroupPairs = new[] + { + new[] { 0, 2 }, + new[] { 1, 3 }, + new[] { 4, 6 }, + new[] { 5, 7 }, + }; + private static readonly int[] FanLeftSteelRightGroup2PriorityPartyIndexes = new[] { 0, 1, 4, 5, 2, 3, 6, 7 }; + private static readonly MarkType[] MarkerVariableMarkTypes = new[] + { + MarkType.Attack1, + MarkType.Attack2, + MarkType.Attack3, + MarkType.Attack4, + MarkType.Stop1, + MarkType.Stop2, + MarkType.Bind1, + MarkType.Bind2, + }; + + private ScriptAccessory _acc; + private Phase _phase = Phase.Init; + private int _generation; + private long _lastMechanicAt; + private readonly object _lock = new object(); + private readonly object _debugFileLock = new object(); + private string _debugLogFilePath; + private readonly HashSet _seenCasts = new HashSet(); + private readonly HashSet _loggedActionIds = new HashSet(); + private object _greenMoveToPointSub; + private MethodInfo _greenMoveToPointInvoke; + private object _greenMoveClearQueueSub; + private MethodInfo _greenMoveClearQueueInvoke; + private object _greenMoveStopSub; + private MethodInfo _greenMoveStopInvoke; + private bool _warnedGreenMove; + private FieldInfo _legacyMarkingControllerField; + private MethodInfo _legacyDoMarkingByActorIdMethod; + private bool _warnedLegacyCommandMarkClear; + + private readonly object _targetIconLock = new object(); + private readonly uint[] _lastTargetIconByPartyIndex = new uint[8]; + private readonly uint[] _initialGroup2TargetIconByPartyIndex = new uint[8]; + private readonly int[] _targetIconGroupByPartyIndex = new int[8]; + private readonly List _targetIconGroup1 = new List(); + private readonly List _targetIconGroup2 = new List(); + private readonly List _activeTargetIconGroup1 = new List(); + private readonly List _activeTargetIconGroup2 = new List(); + private readonly MarkType?[] _roundMarkByPartyIndex = new MarkType?[8]; + private readonly int[] _markerVariableOwnerByIndex = new int[8]; + private bool _targetIconGroupsResolved; + private bool _activeTargetIconGroupsReady; + private bool _round2TargetIconRegroupDone; + + private readonly object _collectLock = new object(); + private readonly List _tetherTargets = new List(); + private readonly List<(uint Id, Vector3 Position, float Rotation)> _objects = new List<(uint Id, Vector3 Position, float Rotation)>(); + + private readonly object _envBigCircleLock = new object(); + private readonly List _pendingEnvBigCircleIndexes = new List(); + private readonly HashSet _pendingEnvBigCircleTargetIconUpdates = new HashSet(); + private readonly Vector3[] _envBigCircleRoundTwelveDirections = new Vector3[EnvBigCircleLastRound + 1]; + private readonly bool[] _envBigCircleRoundTwelveDirectionReady = new bool[EnvBigCircleLastRound + 1]; + private int _envBigCircleRound; + private int _pendingEnvBigCircleRoundToStart; + private int _pendingEnvBigCircleFirstIndexToStart; + private int _pendingEnvBigCircleSecondIndexToStart; + private int _roundCommandMarkGeneration; + private readonly object _commandMarkTimerLock = new object(); + private Timer _commandMarkApplyTimer; + private Timer _commandMarkClearTimer; + private readonly object _bossCastLock = new object(); + private uint _lastBossOuterEdgeActionId; + private readonly object _outerEdgeGuideLock = new object(); + private int _outerEdgeResolveGeneration; + private bool _activeOuterEdgeGuide; + private int _activeOuterEdgeGuideRound; + private MarkerAlias _activeOuterEdgeGuideFinalAlias = MarkerAlias.None; + private Vector3 _activeOuterEdgeGuideFinalPosition; + private int _activeOuterEdgeGuideDuration; + private bool _activeOuterEdgeGuideHasFinalPosition; + private bool _activeOuterEdgeGuideUseLastRoundConfig; + private P2LastRoundOuterEdgeMode _activeOuterEdgeGuideLastRoundMode = P2LastRoundOuterEdgeMode.塔中间; + private Vector3 _activeOuterEdgeGuideTwelveDirection; + + private readonly object _buffLock = new object(); + private readonly int[] _buffByPartyIndex = new int[8]; + private int _buffCount; + private readonly AutoResetEvent _buffReady = new AutoResetEvent(false); + + private sealed class EnvBigCircleRoundPlan + { + public int Round; + public int FirstIndex; + public int SecondIndex; + public int LeftIndex; + public int RightIndex; + public Vector3 LeftCenter; + public Vector3 RightCenter; + public Vector3 NewTwelveDirection; + public MarkType?[] RoundMarks; + } + + #endregion + + #region Initialization + + public void Init(ScriptAccessory accessory) + { + _acc = accessory; + _phase = Phase.Init; + _generation++; + if (DeveloperMode) + StartDebugFileSession(); + ResetMechanicState(); + ResetTargetIconRecords(); + + lock (_lock) + { + _seenCasts.Clear(); + _loggedActionIds.Clear(); + } + _warnedGreenMove = false; + + accessory.Method.RemoveDraw(".*"); + CommandMarkClear(accessory); + if (DeveloperMode) + DebugEcho(accessory, $"Debug file => {_debugLogFilePath}"); + DebugEcho(accessory, "New Duty Draft initialized."); + } + + private void ResetMechanic(ScriptAccessory accessory, bool removeDraw = true) + { + _generation++; + ResetMechanicState(); + + lock (_lock) + _seenCasts.Clear(); + + if (removeDraw) + accessory.Method.RemoveDraw($"{DrawPrefix}_.*"); + } + + private void ResetMechanicState() + { + _lastMechanicAt = 0; + ClearRoundMarkerVariables(); + + lock (_collectLock) + { + _tetherTargets.Clear(); + _objects.Clear(); + } + + lock (_buffLock) + { + Array.Clear(_buffByPartyIndex, 0, _buffByPartyIndex.Length); + _buffCount = 0; + _buffReady.Reset(); + } + + lock (_envBigCircleLock) + { + _pendingEnvBigCircleIndexes.Clear(); + ClearPendingEnvBigCircleRoundStartLocked(); + Array.Clear(_envBigCircleRoundTwelveDirections, 0, _envBigCircleRoundTwelveDirections.Length); + Array.Clear(_envBigCircleRoundTwelveDirectionReady, 0, _envBigCircleRoundTwelveDirectionReady.Length); + _envBigCircleRound = 0; + } + + lock (_bossCastLock) + _lastBossOuterEdgeActionId = 0; + + _outerEdgeResolveGeneration = 0; + ClearActiveOuterEdgeGuideState(); + + Interlocked.Increment(ref _roundCommandMarkGeneration); + CancelCommandMarkTimers(); + } + + private void ResetTargetIconRecords() + { + lock (_targetIconLock) + { + Array.Clear(_lastTargetIconByPartyIndex, 0, _lastTargetIconByPartyIndex.Length); + Array.Clear(_initialGroup2TargetIconByPartyIndex, 0, _initialGroup2TargetIconByPartyIndex.Length); + Array.Clear(_targetIconGroupByPartyIndex, 0, _targetIconGroupByPartyIndex.Length); + _targetIconGroup1.Clear(); + _targetIconGroup2.Clear(); + _activeTargetIconGroup1.Clear(); + _activeTargetIconGroup2.Clear(); + ClearRoundMarkerVariables(); + _targetIconGroupsResolved = false; + _activeTargetIconGroupsReady = false; + _round2TargetIconRegroupDone = false; + } + } + + private void ClearRoundMarkerVariables() + { + Array.Clear(_roundMarkByPartyIndex, 0, _roundMarkByPartyIndex.Length); + + for (var i = 0; i < _markerVariableOwnerByIndex.Length; i++) + _markerVariableOwnerByIndex[i] = -1; + } + + private void ClearPendingEnvBigCircleRoundStartLocked() + { + _pendingEnvBigCircleTargetIconUpdates.Clear(); + _pendingEnvBigCircleRoundToStart = 0; + _pendingEnvBigCircleFirstIndexToStart = 0; + _pendingEnvBigCircleSecondIndexToStart = 0; + } + + private bool TryStartCollectedEnvBigCircleRoundLocked( + out int round, + out int firstIndex, + out int secondIndex, + out int updateCount, + out List targetIconUpdatePartyIndexes) + { + round = _pendingEnvBigCircleRoundToStart > 0 + ? _pendingEnvBigCircleRoundToStart + : _envBigCircleRound + 1; + firstIndex = _pendingEnvBigCircleFirstIndexToStart; + secondIndex = _pendingEnvBigCircleSecondIndexToStart; + updateCount = _pendingEnvBigCircleTargetIconUpdates.Count; + targetIconUpdatePartyIndexes = _pendingEnvBigCircleTargetIconUpdates.OrderBy(index => index).ToList(); + + if (_pendingEnvBigCircleRoundToStart <= 0) return false; + if (_pendingEnvBigCircleRoundToStart != EnvBigCircleLastRound && updateCount < EnvBigCircleTargetIconUpdateCount) return false; + + _envBigCircleRound = _pendingEnvBigCircleRoundToStart; + ClearPendingEnvBigCircleRoundStartLocked(); + return true; + } + + private void StoreEnvBigCircleRoundTwelveDirection(int round, Vector3 newTwelveDirection) + { + if (round < 0 || round >= _envBigCircleRoundTwelveDirections.Length) return; + + lock (_envBigCircleLock) + { + _envBigCircleRoundTwelveDirections[round] = newTwelveDirection; + _envBigCircleRoundTwelveDirectionReady[round] = true; + } + } + + private bool TryGetEnvBigCircleRoundTwelveDirection(int round, out Vector3 newTwelveDirection) + { + newTwelveDirection = default; + if (round < 0 || round >= _envBigCircleRoundTwelveDirections.Length) return false; + + lock (_envBigCircleLock) + { + if (!_envBigCircleRoundTwelveDirectionReady[round]) + return false; + + newTwelveDirection = _envBigCircleRoundTwelveDirections[round]; + return true; + } + } + + private bool TryCollectTargetIconAndStartEnvBigCircleRound( + int partyIndex, + out int round, + out int firstIndex, + out int secondIndex, + out int updateCount, + out List targetIconUpdatePartyIndexes) + { + round = 0; + firstIndex = 0; + secondIndex = 0; + updateCount = 0; + targetIconUpdatePartyIndexes = new List(); + + if (partyIndex < 0) return false; + + lock (_envBigCircleLock) + { + if (_envBigCircleRound <= 0) return false; + + _pendingEnvBigCircleTargetIconUpdates.Add(partyIndex); + return TryStartCollectedEnvBigCircleRoundLocked(out round, out firstIndex, out secondIndex, out updateCount, out targetIconUpdatePartyIndexes); + } + } + + private void SetPhase(ScriptAccessory accessory, Phase phase) + { + if (_phase == phase) return; + + _phase = phase; + ResetMechanic(accessory); + DebugEcho(accessory, $"Phase => {phase}"); + } + + #endregion + + #region Helpers + + private void QTTS(string text, int rate = 0) + { + if (!EnableTTS || _acc == null) return; + _acc.Method.TTS(text, rate); + } + + private void QText(string text, int duration, bool isWarning = false) + { + if (!EnableText || _acc == null) return; + _acc.Method.TextInfo(text, duration, isWarning); + } + + private void Alert(string text, int duration = 3000, bool isWarning = true) + { + QTTS(text); + QText(text, duration, isWarning); + } + + private string DefaultDebugLogPath() + { + var root = Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments); + if (string.IsNullOrWhiteSpace(root)) + root = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile); + if (string.IsNullOrWhiteSpace(root)) + root = AppContext.BaseDirectory; + + return Path.Combine(root, "KodakkuAssist", DebugLogFileName); + } + + private void EnsureDebugLogPath() + { + if (!string.IsNullOrWhiteSpace(_debugLogFilePath)) return; + _debugLogFilePath = DefaultDebugLogPath(); + } + + private void AppendDebugFileLine(string message) + { + if (!DeveloperMode) return; + + try + { + EnsureDebugLogPath(); + var line = $"{DateTime.Now:yyyy-MM-dd HH:mm:ss.fff} [{DrawPrefix}] {message}{Environment.NewLine}"; + + lock (_debugFileLock) + { + var directory = Path.GetDirectoryName(_debugLogFilePath); + if (!string.IsNullOrWhiteSpace(directory)) + Directory.CreateDirectory(directory); + + File.AppendAllText(_debugLogFilePath, line, Encoding.UTF8); + } + } + catch + { + // File logging must never break mechanic handling. + } + } + + private void StartDebugFileSession() + { + if (!DeveloperMode) return; + + try + { + EnsureDebugLogPath(); + var line = $"{Environment.NewLine}===== {DateTime.Now:yyyy-MM-dd HH:mm:ss.fff} New Script Session ====={Environment.NewLine}"; + + lock (_debugFileLock) + { + var directory = Path.GetDirectoryName(_debugLogFilePath); + if (!string.IsNullOrWhiteSpace(directory)) + Directory.CreateDirectory(directory); + + File.AppendAllText(_debugLogFilePath, line, Encoding.UTF8); + } + } + catch + { + // Keep normal debug output alive even if the filesystem is unavailable. + } + } + + private void DebugEcho(ScriptAccessory accessory, string message) + { + AppendDebugFileLine(message); + accessory.Log.Debug($"[{DrawPrefix}] {message}"); + if (DeveloperMode) + accessory.Method.SendChat($"/e [{DrawPrefix}] {message}"); + } + + private void DebugTrace(ScriptAccessory accessory, string message) + { + if (VerboseDebugLog) + DebugEcho(accessory, message); + } + + private void GreenMoveToPoint(Vector3 target, ScriptAccessory accessory, string reason) + { + if (!EnableGreenMove) return; + + try + { + if (GreenMoveClearQueueBeforeMove) + GreenMoveClearQueue(); + + InvokeGreenMove6( + "MoveToPoint", + ref _greenMoveToPointSub, + ref _greenMoveToPointInvoke, + target, + GreenMoveTolerance > 0.0f ? GreenMoveTolerance : 0.1f, + GreenMoveSpeedMultiplier > 0.0f ? GreenMoveSpeedMultiplier : 1.0f, + GreenMoveIgnoreY, + Math.Max(0, GreenMoveMaxCastWaitMs), + GreenMoveWaitForQueuedCast); + + DebugTrace(accessory, $"GreenMove MoveToPoint {reason}: ({target.X:F2}, {target.Y:F2}, {target.Z:F2})"); + } + catch (Exception ex) + { + WarnGreenMoveFailure(accessory, ex); + } + } + + private void GreenMoveStopAndClear(ScriptAccessory accessory, string reason) + { + if (!EnableGreenMove) return; + + try + { + GreenMoveClearQueue(); + GreenMoveStop(); + DebugTrace(accessory, $"GreenMove Stop/Clear {reason}"); + } + catch (Exception ex) + { + WarnGreenMoveFailure(accessory, ex); + } + } + + private void GreenMoveClearQueue() + { + InvokeGreenMoveAction0("ClearQueue", ref _greenMoveClearQueueSub, ref _greenMoveClearQueueInvoke); + } + + private void GreenMoveStop() + { + InvokeGreenMoveAction0("Stop", ref _greenMoveStopSub, ref _greenMoveStopInvoke); + } + + private void WarnGreenMoveFailure(ScriptAccessory accessory, Exception ex) + { + accessory.Log.Debug($"GreenMove IPC failed: {ex}"); + AppendDebugFileLine($"GreenMove IPC failed: {ex.Message}"); + if (_warnedGreenMove) return; + + _warnedGreenMove = true; + accessory.Method.SendChat("/e GreenMove IPC调用失败,请确认PromeRotation已加载且GreenMove IPC已注册。"); + } + + private static void InvokeGreenMove6( + string name, + ref object sub, + ref MethodInfo invoke, + T1 arg1, + float tolerance, + float speed, + bool ignoreY, + int maxCastWaitMs, + bool waitForQueuedCast) + { + if (sub == null || invoke == null) + { + sub = CreateSubscriber(GreenMovePrefix + name); + invoke = sub.GetType().GetMethods(BindingFlags.Instance | BindingFlags.Public) + .FirstOrDefault(m => m.Name == "InvokeAction" && m.GetParameters().Length == 6) + ?? throw new MissingMethodException($"{name} missing InvokeAction."); + } + + try + { + invoke.Invoke(sub, new object[] { arg1, tolerance, speed, ignoreY, maxCastWaitMs, waitForQueuedCast }); + } + catch (TargetInvocationException ex) when (ex.InnerException != null) + { + throw ex.InnerException; + } + } + + private static void InvokeGreenMoveAction0(string name, ref object sub, ref MethodInfo invoke) + { + var fullName = GreenMovePrefix + name; + if (sub == null || invoke == null) + { + sub = CreateSubscriber(fullName); + invoke = sub.GetType().GetMethods(BindingFlags.Instance | BindingFlags.Public) + .FirstOrDefault(m => m.Name == "InvokeAction" && m.GetParameters().Length == 0) + ?? throw new MissingMethodException($"{name} missing InvokeAction."); + } + + try + { + invoke.Invoke(sub, Array.Empty()); + } + catch (TargetInvocationException ex) when (ex.InnerException != null) + { + throw ex.InnerException; + } + } + + private static object CreateSubscriber(string fullName) + { + var pluginInterface = GetKodakkuPluginInterface(); + var method = pluginInterface.GetType().GetMethods(BindingFlags.Instance | BindingFlags.Public) + .FirstOrDefault(m => m.Name == "GetIpcSubscriber" && m.IsGenericMethodDefinition && m.GetGenericArguments().Length == 1); + if (method == null) throw new MissingMethodException("GetIpcSubscriber not found."); + + return method.MakeGenericMethod(typeof(TRet)).Invoke(pluginInterface, new object[] { fullName }) + ?? throw new InvalidOperationException($"{fullName} subscriber is null."); + } + + private static object CreateSubscriber(string fullName) + { + var pluginInterface = GetKodakkuPluginInterface(); + var method = pluginInterface.GetType().GetMethods(BindingFlags.Instance | BindingFlags.Public) + .FirstOrDefault(m => m.Name == "GetIpcSubscriber" && m.IsGenericMethodDefinition && m.GetGenericArguments().Length == 7); + if (method == null) throw new MissingMethodException("GetIpcSubscriber not found."); + + return method.MakeGenericMethod(typeof(T1), typeof(T2), typeof(T3), typeof(T4), typeof(T5), typeof(T6), typeof(TRet)).Invoke(pluginInterface, new object[] { fullName }) + ?? throw new InvalidOperationException($"{fullName} subscriber is null."); + } + + private static object GetKodakkuPluginInterface() + { + var serviceType = typeof(ScriptAccessory).Assembly.GetType("KodakkuAssist.Data.Service") + ?? throw new TypeLoadException("KodakkuAssist.Data.Service not found."); + var prop = serviceType.GetProperty("PluginInterface", BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic) + ?? throw new MissingMemberException("KodakkuAssist.Data.Service.PluginInterface not found."); + + return prop.GetValue(null) + ?? throw new InvalidOperationException("KodakkuAssist PluginInterface is null."); + } + + private void CancelCommandMarkTimers() + { + lock (_commandMarkTimerLock) + { + _commandMarkApplyTimer?.Dispose(); + _commandMarkApplyTimer = null; + _commandMarkClearTimer?.Dispose(); + _commandMarkClearTimer = null; + } + } + + private void CancelCommandMarkApplyTimer() + { + lock (_commandMarkTimerLock) + { + _commandMarkApplyTimer?.Dispose(); + _commandMarkApplyTimer = null; + } + } + + private void CancelCommandMarkClearTimer() + { + lock (_commandMarkTimerLock) + { + _commandMarkClearTimer?.Dispose(); + _commandMarkClearTimer = null; + } + } + + private void ClearActiveOuterEdgeGuideState() + { + lock (_outerEdgeGuideLock) + { + _activeOuterEdgeGuide = false; + _activeOuterEdgeGuideRound = 0; + _activeOuterEdgeGuideFinalAlias = MarkerAlias.None; + _activeOuterEdgeGuideFinalPosition = default; + _activeOuterEdgeGuideDuration = 0; + _activeOuterEdgeGuideHasFinalPosition = false; + _activeOuterEdgeGuideUseLastRoundConfig = false; + _activeOuterEdgeGuideLastRoundMode = P2LastRoundOuterEdgeMode.塔中间; + _activeOuterEdgeGuideTwelveDirection = default; + } + } + + private static bool IsBossOuterEdgeAction(uint actionId) + { + return actionId == BossOuterEdgeNorthActionId || actionId == BossOuterEdgeSouthActionId; + } + + private static bool IsBossOuterEdgeResolveAction(uint actionId) + { + return actionId == BossOuterEdgeResolveNorthActionId || actionId == BossOuterEdgeResolveSouthActionId; + } + + private uint GetLastBossOuterEdgeActionId() + { + lock (_bossCastLock) + return _lastBossOuterEdgeActionId; + } + + private void SetLastBossOuterEdgeActionId(uint actionId) + { + lock (_bossCastLock) + _lastBossOuterEdgeActionId = actionId; + } + + private static string OuterEdgeAlias(uint actionId) + { + return actionId == BossOuterEdgeNorthActionId ? "OE1" : "OE2"; + } + + private static Vector3 OuterEdgePosition(Vector3 newTwelveDirection, uint actionId) + { + var direction = actionId == BossOuterEdgeNorthActionId + ? newTwelveDirection + : -newTwelveDirection; + return ArenaCenter + direction * EnvOuterEdgeGuideDistance; + } + + private static Vector3 LastRoundDefaultOuterEdgeInitialPosition(Vector3 newTwelveDirection) + { + return ArenaCenter - newTwelveDirection * EnvLastRoundDefaultOe3Distance; + } + + private static Vector3 LastRoundDefaultOuterEdgeResolvePosition(Vector3 newTwelveDirection, uint bossOuterEdgeActionId) + { + return bossOuterEdgeActionId == BossOuterEdgeNorthActionId + ? ArenaCenter + newTwelveDirection * EnvLastRoundDefaultOe4Distance + : LastRoundDefaultOuterEdgeInitialPosition(newTwelveDirection); + } + + private static Vector3 LastRoundFixedOuterEdgeResolvePosition(uint bossOuterEdgeActionId) + { + return bossOuterEdgeActionId == BossOuterEdgeNorthActionId + ? EnvLastRoundFixedOe6Position + : EnvLastRoundFixedOe5Position; + } + + private void ReplaceCommandMarkApplyTimer(Timer timer) + { + lock (_commandMarkTimerLock) + { + _commandMarkApplyTimer?.Dispose(); + _commandMarkApplyTimer = timer; + } + } + + private void ReplaceCommandMarkClearTimer(Timer timer) + { + lock (_commandMarkTimerLock) + { + _commandMarkClearTimer?.Dispose(); + _commandMarkClearTimer = timer; + } + } + + private (int DelayMs, int DurationMs) GetCommandMarkTiming() + { + var displaySeconds = CommandMarkDisplaySeconds; + if (float.IsNaN(displaySeconds) || float.IsInfinity(displaySeconds)) + displaySeconds = DefaultCommandMarkDisplaySeconds; + + if (displaySeconds < 0.0f) + displaySeconds = 0.0f; + else if (displaySeconds > CommandMarkWindowMs / 1000.0f) + displaySeconds = CommandMarkWindowMs / 1000.0f; + + var durationMs = (int)Math.Round(displaySeconds * 1000.0f, MidpointRounding.AwayFromZero); + durationMs = Math.Max(0, Math.Min(CommandMarkWindowMs, durationMs)); + return (CommandMarkWindowMs - durationMs, durationMs); + } + + private void RunCommandMarkOnFrameworkThread(ScriptAccessory accessory, string context, System.Action action) + { + try + { + var task = accessory.Method.RunOnMainThreadAsync(action); + task?.ContinueWith(t => + { + var ex = t.Exception?.GetBaseException(); + if (ex != null) + DebugEcho(accessory, $"{context}: command mark framework action failed: {ex.Message}"); + }, TaskContinuationOptions.OnlyOnFaulted); + } + catch (Exception ex) + { + DebugEcho(accessory, $"{context}: command mark framework action schedule failed: {ex.Message}"); + } + } + + private int BeginCommandMarkWait(ScriptAccessory accessory, string context, int assignedCount, int totalCount, int delayMs, int durationMs) + { + var generation = Interlocked.Increment(ref _roundCommandMarkGeneration); + CancelCommandMarkApplyTimer(); + DebugTrace(accessory, $"{context}: command mark wait assigned={assignedCount}/{totalCount} delay={delayMs}ms duration={durationMs}ms generation={generation}"); + return generation; + } + + private void ScheduleCommandMarkClear(ScriptAccessory accessory, string context, int generation, int durationMs) + { + if (durationMs <= 0) return; + + var mechanicGeneration = _generation; + Timer timer = null; + timer = new Timer(_ => + { + try + { + if (mechanicGeneration != _generation) + { + DebugTrace(accessory, $"{context}: command mark clear skipped, mechanic generation {mechanicGeneration}->{_generation}"); + return; + } + + RunCommandMarkOnFrameworkThread(accessory, context, () => + { + if (mechanicGeneration != _generation) + { + DebugTrace(accessory, $"{context}: command mark clear skipped on framework thread, mechanic generation {mechanicGeneration}->{_generation}"); + return; + } + + DebugTrace(accessory, $"{context}: command mark duration expired, clear generation={generation}"); + CommandMarkClearCore(accessory, context); + }); + } + catch (Exception ex) + { + DebugEcho(accessory, $"{context}: command mark duration clear schedule failed: {ex.Message}"); + } + finally + { + lock (_commandMarkTimerLock) + { + if (ReferenceEquals(_commandMarkClearTimer, timer)) + _commandMarkClearTimer = null; + } + + timer?.Dispose(); + } + }, null, durationMs, Timeout.Infinite); + + ReplaceCommandMarkClearTimer(timer); + } + + private bool TryLegacyCommandMarkClear(ScriptAccessory accessory, string context) + { + try + { + if (_legacyMarkingControllerField == null || _legacyDoMarkingByActorIdMethod == null) + { + var markType = typeof(ScriptAccessory).Assembly.GetType("KodakkuAssist.Module.GameOperate.Mark") + ?? throw new TypeLoadException("KodakkuAssist.Module.GameOperate.Mark not found."); + + _legacyMarkingControllerField = markType.GetField("MarkingController", BindingFlags.Static | BindingFlags.NonPublic) + ?? throw new MissingFieldException("KodakkuAssist.Module.GameOperate.Mark.MarkingController not found."); + _legacyDoMarkingByActorIdMethod = markType.GetMethod("DoMarkingByActorID", BindingFlags.Static | BindingFlags.NonPublic) + ?? throw new MissingMethodException("KodakkuAssist.Module.GameOperate.Mark.DoMarkingByActorID not found."); + } + + var controller = (IntPtr)_legacyMarkingControllerField.GetValue(null); + if (controller == IntPtr.Zero) + return false; + + var clearedCount = 0; + for (var i = 0; i < 17; i++) + { + var actorId = (uint)System.Runtime.InteropServices.Marshal.ReadInt32(IntPtr.Add(controller, 16 + i * 8)); + if (actorId == 0 || actorId == InvalidObjectId) + continue; + + _legacyDoMarkingByActorIdMethod.Invoke(null, new object[] { actorId, (MarkType)(i + 1), false }); + clearedCount++; + } + + DebugTrace(accessory, $"{context}: legacy command mark clear queued {clearedCount} current marks."); + return true; + } + catch (Exception ex) + { + if (!_warnedLegacyCommandMarkClear) + { + _warnedLegacyCommandMarkClear = true; + DebugEcho(accessory, $"{context}: legacy command mark clear failed: {ex.Message}"); + } + + return false; + } + } + + private void CommandMarkClearCore(ScriptAccessory accessory, string context) + { + if (!EnableCommandMode) return; + TryLegacyCommandMarkClear(accessory, context); + } + + private void CommandMarkClear(ScriptAccessory accessory, string context = "Command mark clear") + { + if (!EnableCommandMode) return; + RunCommandMarkOnFrameworkThread(accessory, context, () => + { + CommandMarkClearCore(accessory, context); + }); + } + + private void CommandMarkPartyMemberCore(ScriptAccessory accessory, int partyIndex, MarkType markType) + { + if (!EnableCommandMode) return; + if (partyIndex < 0 || partyIndex >= accessory.Data.PartyList.Count) return; + + accessory.Method.Mark(accessory.Data.PartyList[partyIndex], markType); + } + + private void CommandMarkRoundMarkersCore(ScriptAccessory accessory, int round, IReadOnlyList roundMarks) + { + if (!EnableCommandMode) return; + + DebugTrace(accessory, $"Env big circle round {round}: command mark apply start assigned={roundMarks.Count(mark => mark.HasValue)}/{roundMarks.Count}"); + + for (var i = 0; i < roundMarks.Count; i++) + { + var objectId = i >= 0 && i < accessory.Data.PartyList.Count + ? accessory.Data.PartyList[i] + : 0; + + if (!roundMarks[i].HasValue) + { + DebugTrace(accessory, $"Env big circle round {round}: command mark skip {PartyPriorityLabel(i)} no mark {FormatObjectId(objectId)}"); + continue; + } + + if (i < 0 || i >= accessory.Data.PartyList.Count) + { + DebugTrace(accessory, $"Env big circle round {round}: command mark skip {PartyPriorityLabel(i)} no party object"); + continue; + } + + DebugTrace(accessory, $"Env big circle round {round}: command mark {PartyPriorityLabel(i)}={roundMarks[i].Value} {FormatObjectId(objectId)}"); + CommandMarkPartyMemberCore(accessory, i, roundMarks[i].Value); + } + + DebugTrace(accessory, $"Env big circle round {round}: command mark apply done"); + } + + private void CommandMarkRoundMarkersForDuration(ScriptAccessory accessory, int round, IReadOnlyList roundMarks) + { + try + { + if (!EnableCommandMode) + { + DebugTrace(accessory, $"Env big circle round {round}: command mark skipped, command mode disabled"); + return; + } + + var roundMarksSnapshot = roundMarks.ToArray(); + var context = $"Env big circle round {round}"; + var (delayMs, durationMs) = GetCommandMarkTiming(); + var generation = BeginCommandMarkWait(accessory, context, roundMarksSnapshot.Count(mark => mark.HasValue), roundMarksSnapshot.Length, delayMs, durationMs); + if (durationMs <= 0) + { + DebugTrace(accessory, $"{context}: command mark display duration is 0ms, apply skipped generation={generation}"); + return; + } + + Timer timer = null; + timer = new Timer(_ => + { + try + { + if (!EnableCommandMode) + { + DebugTrace(accessory, $"{context}: command mark apply skipped, command mode disabled"); + return; + } + + if (generation != Volatile.Read(ref _roundCommandMarkGeneration)) + { + DebugTrace(accessory, $"{context}: command mark apply skipped, generation {generation}->{Volatile.Read(ref _roundCommandMarkGeneration)}"); + return; + } + + DebugTrace(accessory, $"{context}: command mark wait done, apply duration={durationMs}ms generation={generation}"); + RunCommandMarkOnFrameworkThread(accessory, context, () => + { + if (!EnableCommandMode) + { + DebugTrace(accessory, $"{context}: command mark apply skipped on framework thread, command mode disabled"); + return; + } + + if (generation != Volatile.Read(ref _roundCommandMarkGeneration)) + { + DebugTrace(accessory, $"{context}: command mark apply skipped on framework thread, generation {generation}->{Volatile.Read(ref _roundCommandMarkGeneration)}"); + return; + } + + CancelCommandMarkClearTimer(); + CommandMarkRoundMarkersCore(accessory, round, roundMarksSnapshot); + ScheduleCommandMarkClear(accessory, context, generation, durationMs); + }); + } + catch (Exception ex) + { + DebugEcho(accessory, $"{context}: command mark delayed apply failed: {ex.Message}"); + } + finally + { + lock (_commandMarkTimerLock) + { + if (ReferenceEquals(_commandMarkApplyTimer, timer)) + _commandMarkApplyTimer = null; + } + + timer?.Dispose(); + } + }, null, delayMs, Timeout.Infinite); + + ReplaceCommandMarkApplyTimer(timer); + } + catch (Exception ex) + { + DebugEcho(accessory, $"Env big circle round {round}: command mark schedule failed: {ex.Message}"); + } + } + + private static long NowMs() + { + return DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(); + } + + private static int Duration(Event @event, int fallback = 5000) + { + return int.TryParse(@event["DurationMilliseconds"], NumberStyles.Integer, CultureInfo.InvariantCulture, out var duration) + && duration > 0 + ? duration + : fallback; + } + + private static bool TryParseObjectId(string text, out uint id) + { + id = 0; + if (string.IsNullOrWhiteSpace(text)) return false; + + text = text.StartsWith("0x", StringComparison.OrdinalIgnoreCase) ? text[2..] : text; + return uint.TryParse(text, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out id); + } + + private static bool TryParseUInt(string text, out uint value) + { + value = 0; + return !string.IsNullOrWhiteSpace(text) + && uint.TryParse(text, NumberStyles.Integer, CultureInfo.InvariantCulture, out value); + } + + private static bool TryGetActionId(Event @event, out uint actionId) + { + return TryParseUInt(@event["ActionId"], out actionId); + } + + private static bool TryGetStatusId(Event @event, out uint statusId) + { + var raw = @event["StatusID"]; + if (string.IsNullOrWhiteSpace(raw)) + raw = @event["StatusId"]; + + return TryParseUInt(raw, out statusId); + } + + private static bool TryGetSourceId(Event @event, out uint sourceId) + { + var raw = @event["SourceId"]; + if (string.IsNullOrWhiteSpace(raw)) + raw = @event["SourceID"]; + + if (!TryParseObjectId(raw, out sourceId)) + sourceId = (uint)@event.SourceId; + + return sourceId != 0 && sourceId != InvalidObjectId; + } + + private static bool TryGetTargetId(Event @event, out uint targetId) + { + var raw = @event["TargetId"]; + if (string.IsNullOrWhiteSpace(raw)) + raw = @event["TargetID"]; + + if (!TryParseObjectId(raw, out targetId)) + targetId = (uint)@event.TargetId; + + return targetId != 0 && targetId != InvalidObjectId; + } + + private static bool TryGetTargetIconId(Event @event, out uint iconId) + { + iconId = 0; + var raw = NormalizeIconId(@event["Id"]); + return uint.TryParse(raw, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out iconId); + } + + private static bool IsTrackedTargetIcon(uint iconId) + { + return iconId == TargetIcon02CB + || iconId == TargetIcon02CC + || iconId == TargetIcon02CD; + } + + private static string FormatTargetIconId(uint iconId) + { + return iconId == 0 ? "-" : iconId.ToString("X4", CultureInfo.InvariantCulture); + } + + private static string PartyPriorityLabel(int partyIndex) + { + return partyIndex >= 0 && partyIndex < PartyPriorityLabels.Length + ? PartyPriorityLabels[partyIndex] + : $"P{partyIndex + 1}"; + } + + private static string PartyPriorityLabelOrNone(int partyIndex) + { + return partyIndex >= 0 ? PartyPriorityLabel(partyIndex) : "-"; + } + + private string FormatPartyMember(ScriptAccessory accessory, int partyIndex) + { + var objectId = partyIndex >= 0 && partyIndex < accessory.Data.PartyList.Count + ? accessory.Data.PartyList[partyIndex] + : 0; + + return $"{PartyPriorityLabel(partyIndex)}:{FormatTargetIconId(_lastTargetIconByPartyIndex[partyIndex])}:{FormatObjectId(objectId)}"; + } + + private string FormatTargetIconGroupText(ScriptAccessory accessory, IEnumerable partyIndexes) + { + return string.Join(", ", partyIndexes.OrderBy(index => index).Select(index => FormatPartyMember(accessory, index))); + } + + private void SetTargetIconGroupsLocked(IEnumerable group1Indexes) + { + var group1Set = new HashSet(group1Indexes.Where(index => index >= 0 && index < _lastTargetIconByPartyIndex.Length)); + + _targetIconGroup1.Clear(); + _targetIconGroup2.Clear(); + Array.Clear(_targetIconGroupByPartyIndex, 0, _targetIconGroupByPartyIndex.Length); + + for (var i = 0; i < _lastTargetIconByPartyIndex.Length; i++) + { + if (group1Set.Contains(i)) + { + _targetIconGroupByPartyIndex[i] = 1; + _targetIconGroup1.Add(i); + } + else + { + _targetIconGroupByPartyIndex[i] = 2; + _targetIconGroup2.Add(i); + } + } + + _targetIconGroupsResolved = true; + Array.Clear(_initialGroup2TargetIconByPartyIndex, 0, _initialGroup2TargetIconByPartyIndex.Length); + foreach (var index in _targetIconGroup2) + _initialGroup2TargetIconByPartyIndex[index] = _lastTargetIconByPartyIndex[index]; + + _activeTargetIconGroup1.Clear(); + _activeTargetIconGroup2.Clear(); + _activeTargetIconGroup1.AddRange(_targetIconGroup1); + _activeTargetIconGroup2.AddRange(_targetIconGroup2); + _activeTargetIconGroupsReady = true; + } + + private bool TryResolveTargetIconGroupsLocked(ScriptAccessory accessory, out string group1Text, out string group2Text) + { + group1Text = string.Empty; + group2Text = string.Empty; + + if (_targetIconGroupsResolved) return false; + if (_lastTargetIconByPartyIndex.Any(iconId => iconId == 0)) return false; + + var group1Set = new HashSet(); + foreach (var pair in TargetIconGroupPairs) + { + if (!pair.Any(index => _lastTargetIconByPartyIndex[index] == TargetIcon02CB)) + continue; + + foreach (var index in pair) + group1Set.Add(index); + } + + if (group1Set.Count != 4) + return false; + + SetTargetIconGroupsLocked(group1Set); + group1Text = FormatTargetIconGroupText(accessory, _targetIconGroup1); + group2Text = FormatTargetIconGroupText(accessory, _targetIconGroup2); + return true; + } + + private void TryRegroupRound2TargetIconGroupsLocked(ScriptAccessory accessory, IReadOnlyList refreshedPartyIndexes) + { + if (!RegroupOnRound2TargetIconRefresh || _round2TargetIconRegroupDone) + return; + + if (refreshedPartyIndexes == null) + { + DebugEcho(accessory, "TargetIcon groups regroup round 2 skipped: refreshed target icon list is missing."); + return; + } + + var newGroup1 = refreshedPartyIndexes + .Where(index => index >= 0 && index < _lastTargetIconByPartyIndex.Length) + .Distinct() + .OrderBy(index => index) + .ToList(); + + if (newGroup1.Count != 4) + { + DebugEcho(accessory, $"TargetIcon groups regroup round 2 skipped: refreshed={string.Join(", ", newGroup1.Select(PartyPriorityLabel))} count={newGroup1.Count}/4."); + return; + } + + SetTargetIconGroupsLocked(newGroup1); + _round2TargetIconRegroupDone = true; + DebugEcho(accessory, $"TargetIcon groups regrouped round 2: G1={FormatTargetIconGroupText(accessory, _targetIconGroup1)} | G2={FormatTargetIconGroupText(accessory, _targetIconGroup2)}"); + } + + private int FirstPartyIndexByIcon(IEnumerable partyIndexes, uint iconId) + { + foreach (var index in partyIndexes.OrderBy(index => index)) + { + if (index >= 0 && index < _lastTargetIconByPartyIndex.Length && _lastTargetIconByPartyIndex[index] == iconId) + return index; + } + + return -1; + } + + private List PartyIndexesByIcon(IEnumerable partyIndexes, uint iconId) + { + return partyIndexes + .Where(index => index >= 0 && index < _lastTargetIconByPartyIndex.Length && _lastTargetIconByPartyIndex[index] == iconId) + .OrderBy(index => index) + .ToList(); + } + + private static int TargetIconPairPartner(int partyIndex) + { + foreach (var pair in TargetIconGroupPairs) + { + if (pair.Length < 2) continue; + if (pair[0] == partyIndex) return pair[1]; + if (pair[1] == partyIndex) return pair[0]; + } + + return -1; + } + + private bool TryGetPartyPosition(ScriptAccessory accessory, int partyIndex, out Vector3 position) + { + position = default; + if (partyIndex < 0 || partyIndex >= accessory.Data.PartyList.Count) return false; + + var obj = accessory.Data.Objects.SearchById(accessory.Data.PartyList[partyIndex]); + if (obj == null) return false; + + position = obj.Position; + return true; + } + + private float PartyClockDirectionScore(ScriptAccessory accessory, int partyIndex, Vector3 newTwelveDirection, float clockHour) + { + if (!TryGetPartyPosition(accessory, partyIndex, out var position)) + return float.NegativeInfinity; + + var playerDirection = NormalizeXZ(position - ArenaCenter, DefaultNorth); + var clockDirection = ClockDirection(newTwelveDirection, clockHour); + return DotXZ(playerDirection, clockDirection); + } + + private List PartyIndexesByClockCloseness(ScriptAccessory accessory, IEnumerable partyIndexes, Vector3 newTwelveDirection, float clockHour) + { + return partyIndexes + .OrderByDescending(index => PartyClockDirectionScore(accessory, index, newTwelveDirection, clockHour)) + .ThenBy(index => index) + .ToList(); + } + + private string FormatClockCloseness(ScriptAccessory accessory, int partyIndex, Vector3 newTwelveDirection, float clockHour) + { + var score = PartyClockDirectionScore(accessory, partyIndex, newTwelveDirection, clockHour); + var scoreText = float.IsNegativeInfinity(score) + ? "missing" + : score.ToString("F3", CultureInfo.InvariantCulture); + return $"{PartyPriorityLabel(partyIndex)}={scoreText}"; + } + + private static int MarkerVariableIndex(MarkType markType) + { + for (var i = 0; i < MarkerVariableMarkTypes.Length; i++) + { + if (MarkerVariableMarkTypes[i] == markType) + return i; + } + + return -1; + } + + private void AssignMarkerVariable(MarkType markType, int partyIndex) + { + if (partyIndex < 0 || partyIndex >= _roundMarkByPartyIndex.Length) return; + + var markerIndex = MarkerVariableIndex(markType); + if (markerIndex < 0 || markerIndex >= _markerVariableOwnerByIndex.Length) return; + + _markerVariableOwnerByIndex[markerIndex] = partyIndex; + _roundMarkByPartyIndex[partyIndex] = markType; + } + + private void AssignMarkerVariables(IEnumerable partyIndexes, params MarkType[] markTypes) + { + AssignMarkerVariablesByPriority(partyIndexes, false, markTypes); + } + + private void AssignMarkerVariablesByPriority(IEnumerable partyIndexes, bool reversePriority, params MarkType[] markTypes) + { + var ordered = partyIndexes.OrderBy(index => index).ToList(); + if (reversePriority) + ordered.Reverse(); + + for (var i = 0; i < ordered.Count && i < markTypes.Length; i++) + AssignMarkerVariable(markTypes[i], ordered[i]); + } + + private bool UseReverseEvenGroup2Priority() + { + return EvenGroup2PrioritySetting == EvenGroup2PriorityMode.近北远南; + } + + private void AssignTnDpsGroup2MarkerVariables(bool reversePriority) + { + AssignMarkerVariablesByPriority(_activeTargetIconGroup2.Where(IsTnPartyIndex), reversePriority, MarkType.Stop1, MarkType.Stop2); + AssignMarkerVariablesByPriority(_activeTargetIconGroup2.Where(IsDpsPartyIndex), reversePriority, MarkType.Bind1, MarkType.Bind2); + } + + private List FanLeftSteelRightGroup2MembersByIcon(uint iconId, bool useInitialGroup2Icon, bool reversePriority = false) + { + var ordered = new List(); + var priorityIndexes = reversePriority + ? FanLeftSteelRightGroup2PriorityPartyIndexes.Reverse() + : FanLeftSteelRightGroup2PriorityPartyIndexes; + + foreach (var index in priorityIndexes) + { + if (!_activeTargetIconGroup2.Contains(index)) continue; + + var storedIconId = useInitialGroup2Icon + ? _initialGroup2TargetIconByPartyIndex[index] + : _lastTargetIconByPartyIndex[index]; + if (storedIconId == iconId) + ordered.Add(index); + } + + return ordered; + } + + private bool IsTnPartyIndex(int partyIndex) + { + return partyIndex >= 0 && partyIndex <= 3; + } + + private bool IsDpsPartyIndex(int partyIndex) + { + return partyIndex >= 4 && partyIndex <= 7; + } + + private bool TryGetPreviousRoundNineOrder(ScriptAccessory accessory, int round, IEnumerable partyIndexes, out List ordered) + { + ordered = null; + if (!TryGetEnvBigCircleRoundTwelveDirection(round - 1, out var previousRoundTwelveDirection)) + { + DebugEcho(accessory, $"Env big circle round {round}: previous round twelve direction not ready for near 9 order"); + return false; + } + + ordered = PartyIndexesByClockCloseness(accessory, partyIndexes, previousRoundTwelveDirection, 9.0f); + return true; + } + + private string FormatClockClosenessForPreviousRound(ScriptAccessory accessory, int round, int partyIndex, float clockHour) + { + if (!TryGetEnvBigCircleRoundTwelveDirection(round - 1, out var previousRoundTwelveDirection)) + return $"{PartyPriorityLabel(partyIndex)}=missing-direction"; + + return FormatClockCloseness(accessory, partyIndex, previousRoundTwelveDirection, clockHour); + } + + private void SwapActiveTargetIconGroupsLocked() + { + var oldGroup1 = _activeTargetIconGroup1.ToList(); + _activeTargetIconGroup1.Clear(); + _activeTargetIconGroup1.AddRange(_activeTargetIconGroup2); + _activeTargetIconGroup2.Clear(); + _activeTargetIconGroup2.AddRange(oldGroup1); + } + + private bool PrepareRoundMarkerVariablesLocked(ScriptAccessory accessory, int round, Vector3 newTwelveDirection, out MarkType?[] roundMarks) + { + roundMarks = null; + if (!_activeTargetIconGroupsReady) return false; + + DebugTrace(accessory, $"Env big circle round {round}: round start"); + + if (round == 4 || round == 8) + { + SwapActiveTargetIconGroupsLocked(); + DebugTrace(accessory, $"Env big circle round {round}: active target icon groups swapped"); + } + + DebugStoredTargetIconRecords(accessory, round); + + ClearRoundMarkerVariables(); + + var ok = round % 2 == 1 + ? AssignOddRoundMarkerVariablesLocked(accessory, round, newTwelveDirection) + : AssignEvenRoundMarkerVariablesLocked(accessory, round); + + roundMarks = new MarkType?[_roundMarkByPartyIndex.Length]; + Array.Copy(_roundMarkByPartyIndex, roundMarks, _roundMarkByPartyIndex.Length); + return ok; + } + + private bool AssignOddRoundMarkerVariablesLocked(ScriptAccessory accessory, int round, Vector3 newTwelveDirection) + { + var cd = FirstPartyIndexByIcon(_activeTargetIconGroup1, TargetIcon02CD); + if (cd < 0) return false; + + var cc = FirstPartyIndexByIcon(_activeTargetIconGroup1, TargetIcon02CC); + + if (round == 1) + { + if (cc < 0) return false; + + var cbWithCdPartner = -1; + var cbWithCcPartner = -1; + var group1Cb = PartyIndexesByIcon(_activeTargetIconGroup1, TargetIcon02CB); + foreach (var cb in group1Cb) + { + var partner = TargetIconPairPartner(cb); + var partnerIcon = partner >= 0 && partner < _lastTargetIconByPartyIndex.Length + ? _lastTargetIconByPartyIndex[partner] + : 0; + + DebugTrace(accessory, $"Env big circle round {round}: first G1 02CB pair check {PartyPriorityLabel(cb)} partner={PartyPriorityLabelOrNone(partner)}:{FormatTargetIconId(partnerIcon)}"); + + if (partnerIcon == TargetIcon02CD) + { + if (cbWithCdPartner >= 0) + { + DebugEcho(accessory, $"Env big circle round {round}: first G1 duplicate 02CB with 02CD partner"); + return false; + } + + cbWithCdPartner = cb; + } + else if (partnerIcon == TargetIcon02CC) + { + if (cbWithCcPartner >= 0) + { + DebugEcho(accessory, $"Env big circle round {round}: first G1 duplicate 02CB with 02CC partner"); + return false; + } + + cbWithCcPartner = cb; + } + else + { + DebugEcho(accessory, $"Env big circle round {round}: first G1 unexpected 02CB partner icon {PartyPriorityLabel(cb)} partner={PartyPriorityLabelOrNone(partner)}:{FormatTargetIconId(partnerIcon)}"); + } + } + + if (cbWithCdPartner < 0 || cbWithCcPartner < 0) + { + DebugEcho(accessory, $"Env big circle round {round}: first G1 02CB pair rule incomplete Attack1={PartyPriorityLabelOrNone(cbWithCdPartner)} Attack4={PartyPriorityLabelOrNone(cbWithCcPartner)}"); + return false; + } + + AssignMarkerVariable(MarkType.Attack1, cbWithCdPartner); + AssignMarkerVariable(MarkType.Attack2, cd); + AssignMarkerVariable(MarkType.Attack3, cc); + AssignMarkerVariable(MarkType.Attack4, cbWithCcPartner); + } + else + { + if (!TryGetEnvBigCircleRoundTwelveDirection(round - 1, out var previousRoundTwelveDirection)) + { + DebugEcho(accessory, $"Env big circle round {round}: previous round twelve direction not ready for 02CB near 9 order"); + return false; + } + + var cbByNine = PartyIndexesByClockCloseness( + accessory, + PartyIndexesByIcon(_activeTargetIconGroup1, TargetIcon02CB), + previousRoundTwelveDirection, + 9.0f); + + DebugTrace(accessory, $"Env big circle round {round}: odd G1 02CB previous round near 9 order: {string.Join(", ", cbByNine.Select(index => FormatClockCloseness(accessory, index, previousRoundTwelveDirection, 9.0f)))}"); + + AssignMarkerVariable(MarkType.Attack1, cbByNine.Count > 0 ? cbByNine[0] : -1); + AssignMarkerVariable(MarkType.Attack2, cd); + AssignMarkerVariable(MarkType.Attack3, cc); + AssignMarkerVariable(MarkType.Attack4, cbByNine.Count > 1 ? cbByNine[1] : -1); + } + + if (P2TowerStrategySetting == P2TowerStrategy.TN左D右_BBY闲固) + { + AssignTnDpsGroup2MarkerVariables(false); + } + else + { + var stopOrder = FanLeftSteelRightGroup2MembersByIcon(TargetIcon02CD, false); + var bindOrder = FanLeftSteelRightGroup2MembersByIcon(TargetIcon02CC, false); + + DebugTrace(accessory, $"Env big circle round {round}: odd fan-left G2 priority Stop={string.Join(", ", stopOrder.Select(PartyPriorityLabel))} Bind={string.Join(", ", bindOrder.Select(PartyPriorityLabel))}"); + AssignMarkerVariable(MarkType.Stop1, stopOrder.Count > 0 ? stopOrder[0] : -1); + AssignMarkerVariable(MarkType.Stop2, stopOrder.Count > 1 ? stopOrder[1] : -1); + AssignMarkerVariable(MarkType.Bind1, bindOrder.Count > 0 ? bindOrder[0] : -1); + AssignMarkerVariable(MarkType.Bind2, bindOrder.Count > 1 ? bindOrder[1] : -1); + } + + return true; + } + + private bool AssignEvenRoundMarkerVariablesLocked(ScriptAccessory accessory, int round) + { + if (P2TowerStrategySetting == P2TowerStrategy.TN左D右_BBY闲固) + return AssignEvenRoundMarkerVariablesTnLeftDRightLocked(accessory, round); + + return AssignEvenRoundMarkerVariablesFanLeftSteelRightLocked(accessory, round); + } + + private bool AssignEvenRoundMarkerVariablesFanLeftSteelRightLocked(ScriptAccessory accessory, int round) + { + var group1Cd = PartyIndexesByIcon(_activeTargetIconGroup1, TargetIcon02CD); + var group1Cc = PartyIndexesByIcon(_activeTargetIconGroup1, TargetIcon02CC); + + if (!TryGetPreviousRoundNineOrder(accessory, round, group1Cd, out var group1CdByNine)) + return false; + if (!TryGetPreviousRoundNineOrder(accessory, round, group1Cc, out var group1CcByNine)) + return false; + + DebugTrace(accessory, $"Env big circle round {round}: even fan-left G1 02CD previous round near 9 order: {string.Join(", ", group1CdByNine.Select(index => FormatClockClosenessForPreviousRound(accessory, round, index, 9.0f)))}"); + DebugTrace(accessory, $"Env big circle round {round}: even fan-left G1 02CC previous round near 9 order: {string.Join(", ", group1CcByNine.Select(index => FormatClockClosenessForPreviousRound(accessory, round, index, 9.0f)))}"); + + AssignMarkerVariable(MarkType.Attack1, group1CdByNine.Count > 0 ? group1CdByNine[0] : -1); + AssignMarkerVariable(MarkType.Attack2, group1CdByNine.Count > 1 ? group1CdByNine[1] : -1); + AssignMarkerVariable(MarkType.Attack3, group1CcByNine.Count > 0 ? group1CcByNine[0] : -1); + AssignMarkerVariable(MarkType.Attack4, group1CcByNine.Count > 1 ? group1CcByNine[1] : -1); + + var useInitialGroup2Icon = round == EnvBigCircleLastRound; + var reverseGroup2Priority = UseReverseEvenGroup2Priority(); + var stopOrder = FanLeftSteelRightGroup2MembersByIcon(TargetIcon02CD, useInitialGroup2Icon, reverseGroup2Priority); + var bindOrder = FanLeftSteelRightGroup2MembersByIcon(TargetIcon02CC, useInitialGroup2Icon, reverseGroup2Priority); + + DebugTrace(accessory, $"Env big circle round {round}: even fan-left G2 priority Stop={string.Join(", ", stopOrder.Select(PartyPriorityLabel))} Bind={string.Join(", ", bindOrder.Select(PartyPriorityLabel))} mode={EvenGroup2PrioritySetting} iconSource={(useInitialGroup2Icon ? "initialG2" : "current")}"); + AssignMarkerVariable(MarkType.Stop1, stopOrder.Count > 0 ? stopOrder[0] : -1); + AssignMarkerVariable(MarkType.Stop2, stopOrder.Count > 1 ? stopOrder[1] : -1); + AssignMarkerVariable(MarkType.Bind1, bindOrder.Count > 0 ? bindOrder[0] : -1); + AssignMarkerVariable(MarkType.Bind2, bindOrder.Count > 1 ? bindOrder[1] : -1); + + return true; + } + + private bool AssignEvenRoundMarkerVariablesTnLeftDRightLocked(ScriptAccessory accessory, int round) + { + var group1Cd = PartyIndexesByIcon(_activeTargetIconGroup1, TargetIcon02CD); + var group1Cc = PartyIndexesByIcon(_activeTargetIconGroup1, TargetIcon02CC); + + if (!TryGetPreviousRoundNineOrder(accessory, round, group1Cd, out var group1CdByNine)) + return false; + if (!TryGetPreviousRoundNineOrder(accessory, round, group1Cc, out var group1CcByNine)) + return false; + + DebugTrace(accessory, $"Env big circle round {round}: even G1 02CD previous round near 9 order: {string.Join(", ", group1CdByNine.Select(index => FormatClockClosenessForPreviousRound(accessory, round, index, 9.0f)))}"); + DebugTrace(accessory, $"Env big circle round {round}: even G1 02CC previous round near 9 order: {string.Join(", ", group1CcByNine.Select(index => FormatClockClosenessForPreviousRound(accessory, round, index, 9.0f)))}"); + + AssignMarkerVariable(MarkType.Attack1, group1CdByNine.Count > 0 ? group1CdByNine[0] : -1); + AssignMarkerVariable(MarkType.Attack2, group1CdByNine.Count > 1 ? group1CdByNine[1] : -1); + AssignMarkerVariable(MarkType.Attack3, group1CcByNine.Count > 0 ? group1CcByNine[0] : -1); + AssignMarkerVariable(MarkType.Attack4, group1CcByNine.Count > 1 ? group1CcByNine[1] : -1); + + AssignTnDpsGroup2MarkerVariables(UseReverseEvenGroup2Priority()); + + return true; + } + + private uint GetLastTargetIconByPartyIndex(int partyIndex) + { + if (partyIndex < 0 || partyIndex >= _lastTargetIconByPartyIndex.Length) return 0; + + lock (_targetIconLock) + return _lastTargetIconByPartyIndex[partyIndex]; + } + + private uint GetLastTargetIcon(ScriptAccessory accessory, uint objectId) + { + return GetLastTargetIconByPartyIndex(GetPlayerIndex(accessory, objectId)); + } + + private int GetTargetIconGroupByPartyIndex(int partyIndex) + { + if (partyIndex < 0 || partyIndex >= _targetIconGroupByPartyIndex.Length) return 0; + + lock (_targetIconLock) + return _targetIconGroupByPartyIndex[partyIndex]; + } + + private int GetTargetIconGroup(ScriptAccessory accessory, uint objectId) + { + return GetTargetIconGroupByPartyIndex(GetPlayerIndex(accessory, objectId)); + } + + private static uint GetMyId(ScriptAccessory accessory) + { + var myObject = accessory.Data.MyObject; + if (myObject != null) + { + var myObjectId = (uint)myObject.EntityId; + if (myObjectId != 0 && myObjectId != InvalidObjectId) + return myObjectId; + } + + return accessory.Data.Me; + } + + private static int GetPlayerIndex(ScriptAccessory accessory, uint objectId) + { + return accessory.Data.PartyList.IndexOf(objectId); + } + + private static int GetMyIndex(ScriptAccessory accessory) + { + return GetPlayerIndex(accessory, GetMyId(accessory)); + } + + private static bool IsMe(ScriptAccessory accessory, uint objectId) + { + return objectId == GetMyId(accessory); + } + + private static string NormalizeIconId(string iconId) + { + if (string.IsNullOrWhiteSpace(iconId)) return string.Empty; + + iconId = iconId.Trim(); + if (iconId.StartsWith("0x", StringComparison.OrdinalIgnoreCase)) + iconId = iconId[2..]; + + return iconId.ToUpperInvariant().PadLeft(4, '0'); + } + + private static string FormatObjectId(uint objectId) + { + return objectId == 0 ? "-" : objectId.ToString("X8", CultureInfo.InvariantCulture); + } + + private static Vector3 ExtendGame(Vector3 position, float gameRotation, float length) + { + return new Vector3( + position.X + MathF.Sin(gameRotation) * length, + position.Y, + position.Z + MathF.Cos(gameRotation) * length); + } + + private static Vector3 ExtendDir(Vector3 position, float directionRadians, float length) + { + return new Vector3( + position.X + MathF.Sin(directionRadians) * length, + position.Y, + position.Z - MathF.Cos(directionRadians) * length); + } + + private static float Norm2Pi(float radians) + { + var value = radians % (2.0f * MathF.PI); + return value < 0 ? value + 2.0f * MathF.PI : value; + } + + private static float GameToDir(float gameRotation) + { + return Norm2Pi(MathF.PI - gameRotation); + } + + private static float DirToGame(float directionRadians) + { + return MathF.PI - directionRadians; + } + + private static int Dir8(Vector3 position, Vector3 center) + { + var direction = Math.Round(4.0 - 4.0 * Math.Atan2(position.X - center.X, position.Z - center.Z) / Math.PI) % 8.0; + return direction < 0 ? (int)direction + 8 : (int)direction; + } + + private static Vector3 RotateAround(Vector3 point, Vector3 center, float radians) + { + var x = point.X - center.X; + var z = point.Z - center.Z; + var sin = MathF.Sin(radians); + var cos = MathF.Cos(radians); + + return new Vector3( + center.X + x * cos + z * sin, + point.Y, + center.Z - x * sin + z * cos); + } + + private static bool IsEnvBigCircleIndex(int index) + { + return index >= EnvBigCircleFirstIndex && index <= EnvBigCircleLastIndex; + } + + private static Vector3 GetEnvBigCircleCenter(int index) + { + var angle = (index - EnvBigCircleFirstIndex) * MathF.PI / 4.0f; + return ArenaCenter + new Vector3( + MathF.Sin(angle) * EnvBigCircleDistance, + 0.0f, + -MathF.Cos(angle) * EnvBigCircleDistance); + } + + private static Vector3 NormalizeXZ(Vector3 vector, Vector3 fallback) + { + var length = MathF.Sqrt(vector.X * vector.X + vector.Z * vector.Z); + if (length < 0.001f) + return fallback; + + return new Vector3(vector.X / length, 0.0f, vector.Z / length); + } + + private static Vector3 RotateClockwiseXZ(Vector3 vector, float radians) + { + var sin = MathF.Sin(radians); + var cos = MathF.Cos(radians); + return new Vector3( + vector.X * cos - vector.Z * sin, + 0.0f, + vector.Z * cos + vector.X * sin); + } + + private static Vector3 ClockDirection(Vector3 newTwelveDirection, float clockHour) + { + return RotateClockwiseXZ(newTwelveDirection, clockHour * MathF.PI / 6.0f); + } + + private static float DotXZ(Vector3 a, Vector3 b) + { + return a.X * b.X + a.Z * b.Z; + } + + private static Vector3 MarkerCircleCenter(Vector3 origin, Vector3 newTwelveDirection, float clockHour, float offset) + { + return origin + ClockDirection(newTwelveDirection, clockHour) * offset; + } + + private static void GetRoundDirections( + Vector3 firstCenter, + Vector3 secondCenter, + out Vector3 newTwelveDirection, + out Vector3 newRightDirection) + { + var midpoint = (firstCenter + secondCenter) / 2.0f; + newTwelveDirection = NormalizeXZ(ArenaCenter - midpoint, DefaultNorth); + newRightDirection = RotateClockwiseXZ(newTwelveDirection, MathF.PI / 2.0f); + } + + private static void SortEnvBigCirclesByRoundSide( + int firstIndex, + int secondIndex, + Vector3 newRightDirection, + out int leftIndex, + out Vector3 leftCenter, + out int rightIndex, + out Vector3 rightCenter) + { + var firstCenter = GetEnvBigCircleCenter(firstIndex); + var secondCenter = GetEnvBigCircleCenter(secondIndex); + var firstRight = DotXZ(firstCenter - ArenaCenter, newRightDirection); + var secondRight = DotXZ(secondCenter - ArenaCenter, newRightDirection); + + if (firstRight <= secondRight) + { + leftIndex = firstIndex; + leftCenter = firstCenter; + rightIndex = secondIndex; + rightCenter = secondCenter; + } + else + { + leftIndex = secondIndex; + leftCenter = secondCenter; + rightIndex = firstIndex; + rightCenter = firstCenter; + } + } + + private Vector3 DrawEnvMarkerCircle( + ScriptAccessory accessory, + int round, + string side, + string colorName, + string clockName, + Vector3 origin, + Vector3 newTwelveDirection, + float clockHour, + float offset, + Vector4 color) + { + var position = MarkerCircleCenter(origin, newTwelveDirection, clockHour, offset); + var duration = EnvBigCircleGuideDurationForRound(round); + DrawCircle( + accessory, + $"{DrawPrefix}_EnvBigCircleRound_{round}_{side}_{colorName}_{clockName}", + position, + EnvMarkerCircleRadius, + duration, + 0, + color, + ScaleMode.None); + return position; + } + + private void DrawEnvMarkerCircleWithGuide( + ScriptAccessory accessory, + int round, + MarkerAlias alias, + string colorName, + string clockName, + Vector3 origin, + Vector3 newTwelveDirection, + float clockHour, + float offset, + Vector4 color) + { + var position = DrawEnvMarkerCircle( + accessory, + round, + alias.ToString(), + colorName, + clockName, + origin, + newTwelveDirection, + clockHour, + offset, + color); + + DrawGuide(accessory, $"{DrawPrefix}_EnvBigCircleRound_{round}_Guide_{alias}", position, EnvBigCircleGuideDurationForRound(round)); + } + + private static int EnvBigCircleGuideDurationForRound(int round) + { + return round == EnvBigCircleLastRound + ? EnvBigCircleLastRoundGuideDuration + : EnvBigCircleGuideDuration; + } + + private MarkerAlias MarkerAliasForRoundMark(MarkType markType, bool oddRound) + { + if (oddRound) + { + switch (markType) + { + case MarkType.Attack1: return MarkerAlias.OL1; + case MarkType.Attack2: return MarkerAlias.OL2; + case MarkType.Attack3: return MarkerAlias.OR1; + case MarkType.Attack4: return MarkerAlias.OR2; + case MarkType.Stop1: return MarkerAlias.OL3; + case MarkType.Stop2: return MarkerAlias.OL4; + case MarkType.Bind1: return MarkerAlias.OR3; + case MarkType.Bind2: return MarkerAlias.OR4; + default: return MarkerAlias.None; + } + } + + switch (markType) + { + case MarkType.Attack1: return MarkerAlias.EL1; + case MarkType.Attack2: return MarkerAlias.EL2; + case MarkType.Attack3: return MarkerAlias.ER2; + case MarkType.Attack4: return MarkerAlias.ER1; + case MarkType.Stop1: return MarkerAlias.EL3; + case MarkType.Stop2: return MarkerAlias.EC1; + case MarkType.Bind1: return MarkerAlias.ER3; + case MarkType.Bind2: return MarkerAlias.EC2; + default: return MarkerAlias.None; + } + } + + private string FormatStoredTargetIconRecord(ScriptAccessory accessory, int partyIndex) + { + var objectId = partyIndex >= 0 && partyIndex < accessory.Data.PartyList.Count + ? accessory.Data.PartyList[partyIndex] + : 0; + var group = _activeTargetIconGroup1.Contains(partyIndex) + ? "G1" + : _activeTargetIconGroup2.Contains(partyIndex) + ? "G2" + : "G-"; + + return $"{PartyPriorityLabel(partyIndex)}={group}:{FormatTargetIconId(_lastTargetIconByPartyIndex[partyIndex])}:{FormatObjectId(objectId)}"; + } + + private void DebugStoredTargetIconRecords(ScriptAccessory accessory, int round) + { + var firstHalf = Enumerable.Range(0, 4) + .Select(index => FormatStoredTargetIconRecord(accessory, index)); + var secondHalf = Enumerable.Range(4, 4) + .Select(index => FormatStoredTargetIconRecord(accessory, index)); + + DebugTrace(accessory, $"Env big circle round {round}: stored targeticons 1/2: {string.Join(", ", firstHalf)}"); + DebugTrace(accessory, $"Env big circle round {round}: stored targeticons 2/2: {string.Join(", ", secondHalf)}"); + } + + private string FormatRoundMarkerAssignment(int partyIndex, IReadOnlyList roundMarks, bool oddRound) + { + if (partyIndex < 0 || partyIndex >= roundMarks.Count || !roundMarks[partyIndex].HasValue) + return null; + + var mark = roundMarks[partyIndex].Value; + var alias = MarkerAliasForRoundMark(mark, oddRound); + return $"{PartyPriorityLabel(partyIndex)}={mark}({alias})"; + } + + private void DebugRoundMarkerAssignments(ScriptAccessory accessory, int round, IReadOnlyList roundMarks) + { + var oddRound = round % 2 == 1; + var assignments = Enumerable.Range(0, roundMarks.Count) + .Select(index => FormatRoundMarkerAssignment(index, roundMarks, oddRound)) + .Where(text => !string.IsNullOrWhiteSpace(text)); + + DebugEcho(accessory, $"Env big circle round {round}: headmarks {string.Join(", ", assignments)}"); + } + + private bool DrawEnvMarkerAlias( + ScriptAccessory accessory, + int round, + MarkerAlias alias, + int leftIndex, + Vector3 leftCenter, + int rightIndex, + Vector3 rightCenter, + Vector3 newTwelveDirection) + { + return DrawEnvMarkerAlias( + accessory, + round, + alias, + leftIndex, + leftCenter, + rightIndex, + rightCenter, + newTwelveDirection, + true, + out _); + } + + private bool DrawEnvMarkerAlias( + ScriptAccessory accessory, + int round, + MarkerAlias alias, + int leftIndex, + Vector3 leftCenter, + int rightIndex, + Vector3 rightCenter, + Vector3 newTwelveDirection, + bool drawGuide, + out Vector3 position) + { + position = default; + + switch (alias) + { + case MarkerAlias.OL1: + return DrawEnvMarkerAliasPoint(accessory, round, alias, "GreenOffset", "0900", leftCenter, newTwelveDirection, 9.0f, EnvOddLeftGreenOffset, SolidSafeGreen, drawGuide, out position); + case MarkerAlias.OL2: + return DrawEnvMarkerAliasPoint(accessory, round, alias, "GreenOffset", "0620", leftCenter, newTwelveDirection, 6.0f + 1.0f / 3.0f, EnvOddLeftGreenOffset, SolidSafeGreen, drawGuide, out position); + case MarkerAlias.OL3: + return DrawEnvMarkerAliasPoint(accessory, round, alias, "RedOuter", "0900", leftCenter, newTwelveDirection, 9.0f, EnvOuterMarkerOffset, SolidDangerRed, drawGuide, out position); + case MarkerAlias.OL4: + return DrawEnvMarkerAliasPoint(accessory, round, alias, "RedOffset", "0600", leftCenter, newTwelveDirection, 6.0f, EnvOddLeftRedSixOffset, SolidDangerRed, drawGuide, out position); + case MarkerAlias.OR1: + return DrawEnvMarkerAliasPoint(accessory, round, alias, "GreenInner", "0840", rightCenter, newTwelveDirection, 8.0f + 2.0f / 3.0f, EnvInnerMarkerOffset, SolidSafeGreen, drawGuide, out position); + case MarkerAlias.OR2: + return DrawEnvMarkerAliasPoint(accessory, round, alias, "GreenInner", "0300", rightCenter, newTwelveDirection, 3.0f, EnvInnerMarkerOffset, SolidSafeGreen, drawGuide, out position); + case MarkerAlias.OR3: + return DrawEnvMarkerAliasPoint(accessory, round, alias, "RedOuter", "0300", rightCenter, newTwelveDirection, 3.0f, EnvOuterMarkerOffset, SolidDangerRed, drawGuide, out position); + case MarkerAlias.OR4: + return DrawEnvMarkerAliasPoint(accessory, round, alias, "RedOuter", "0320", rightCenter, newTwelveDirection, 3.0f + 1.0f / 3.0f, EnvOuterMarkerOffset, SolidDangerRed, drawGuide, out position); + case MarkerAlias.EL1: + return DrawEnvMarkerAliasPoint(accessory, round, alias, "GreenInner", "0835", leftCenter, newTwelveDirection, 8.0f + 7.0f / 12.0f, 3.625f, SolidSafeGreen, drawGuide, out position); + case MarkerAlias.EL2: + return DrawEnvMarkerAliasPoint(accessory, round, alias, "GreenInner", "0730", leftCenter, newTwelveDirection, 7.5f, EnvInnerMarkerOffset, SolidSafeGreen, drawGuide, out position); + case MarkerAlias.EL3: + return DrawEnvMarkerAliasPoint(accessory, round, alias, "RedOuter", "1245", leftCenter, newTwelveDirection, 12.0f + 3.0f / 4.0f, EnvOuterMarkerOffset, SolidDangerRed, drawGuide, out position); + case MarkerAlias.ER1: + return DrawEnvMarkerAliasPoint(accessory, round, alias, "GreenInner", "0300", rightCenter, newTwelveDirection, 3.0f, EnvInnerMarkerOffset, SolidSafeGreen, drawGuide, out position); + case MarkerAlias.ER2: + return DrawEnvMarkerAliasPoint(accessory, round, alias, "GreenInner", "0730", rightCenter, newTwelveDirection, 7.5f, EnvInnerMarkerOffset, SolidSafeGreen, drawGuide, out position); + case MarkerAlias.ER3: + return DrawEnvMarkerAliasPoint(accessory, round, alias, "RedOuter", "1040", rightCenter, newTwelveDirection, 10.0f + 2.0f / 3.0f, EnvOuterMarkerOffset, SolidDangerRed, drawGuide, out position); + case MarkerAlias.EC1: + return DrawEnvMarkerAliasPoint(accessory, round, alias, "RedOuter", "1120", ArenaCenter, newTwelveDirection, 11.0f + 1.0f / 3.0f, EnvCenterOuterMarkerOffset, SolidDangerRed, drawGuide, out position); + case MarkerAlias.EC2: + return DrawEnvMarkerAliasPoint(accessory, round, alias, "RedOuter", "0130", ArenaCenter, newTwelveDirection, 1.5f, EnvCenterOuterMarkerOffset, SolidDangerRed, drawGuide, out position); + default: + return false; + } + } + + private bool DrawEnvMarkerAliasPoint( + ScriptAccessory accessory, + int round, + MarkerAlias alias, + string colorName, + string clockName, + Vector3 origin, + Vector3 newTwelveDirection, + float clockHour, + float offset, + Vector4 color, + bool drawGuide, + out Vector3 position) + { + position = DrawEnvMarkerCircle( + accessory, + round, + alias.ToString(), + colorName, + clockName, + origin, + newTwelveDirection, + clockHour, + offset, + color); + + if (drawGuide) + DrawGuide(accessory, $"{DrawPrefix}_EnvBigCircleRound_{round}_Guide_{alias}", position, EnvBigCircleGuideDurationForRound(round)); + + return true; + } + + private void DrawOuterEdgeRelayGuide( + ScriptAccessory accessory, + int round, + uint bossOuterEdgeActionId, + Vector3 newTwelveDirection, + Vector3 finalPosition, + MarkerAlias finalAlias, + int duration) + { + var oeAlias = OuterEdgeAlias(bossOuterEdgeActionId); + var oePosition = OuterEdgePosition(newTwelveDirection, bossOuterEdgeActionId); + DrawCircle( + accessory, + $"{DrawPrefix}_EnvBigCircleRound_{round}_OE_{oeAlias}_Circle", + oePosition, + EnvMarkerCircleRadius, + duration, + 0, + SolidSafeGreen, + ScaleMode.None); + DrawGuide(accessory, $"{DrawPrefix}_EnvBigCircleRound_{round}_OE_Guide_To_{oeAlias}", oePosition, duration); + DrawInactiveGuideBetweenPositions(accessory, $"{DrawPrefix}_EnvBigCircleRound_{round}_OE_Guide_{oeAlias}_To_{finalAlias}", oePosition, finalPosition, duration); + SaveActiveOuterEdgeGuide(round, finalAlias, finalPosition, duration, true); + GreenMoveToPoint(oePosition, accessory, $"round {round} {oeAlias}"); + DebugTrace(accessory, $"Env big circle round {round}: outer edge relay {oeAlias}->{finalAlias}"); + } + + private void DrawOuterEdgeGuideOnly( + ScriptAccessory accessory, + int round, + uint bossOuterEdgeActionId, + Vector3 newTwelveDirection, + int duration) + { + var oeAlias = OuterEdgeAlias(bossOuterEdgeActionId); + var oePosition = OuterEdgePosition(newTwelveDirection, bossOuterEdgeActionId); + DrawCircle( + accessory, + $"{DrawPrefix}_EnvBigCircleRound_{round}_OE_{oeAlias}_Circle", + oePosition, + EnvMarkerCircleRadius, + duration, + 0, + SolidSafeGreen, + ScaleMode.None); + DrawGuide(accessory, $"{DrawPrefix}_EnvBigCircleRound_{round}_OE_Guide_To_{oeAlias}", oePosition, duration); + SaveActiveOuterEdgeGuide(round, MarkerAlias.None, default, duration, false); + GreenMoveToPoint(oePosition, accessory, $"round {round} {oeAlias}"); + DebugTrace(accessory, $"Env big circle round {round}: outer edge guide {oeAlias}"); + } + + private void DrawLastRoundOuterEdgeInitialGuide( + ScriptAccessory accessory, + int round, + Vector3 newTwelveDirection, + P2LastRoundOuterEdgeMode mode, + int duration) + { + var oeAlias = mode == P2LastRoundOuterEdgeMode.A点 ? "OE5" : "OE3"; + var oePosition = mode == P2LastRoundOuterEdgeMode.A点 + ? EnvLastRoundFixedOe5Position + : LastRoundDefaultOuterEdgeInitialPosition(newTwelveDirection); + + DrawCircle( + accessory, + $"{DrawPrefix}_EnvBigCircleRound_{round}_OE_{oeAlias}_Circle", + oePosition, + EnvMarkerCircleRadius, + duration, + 0, + SolidSafeGreen, + ScaleMode.None); + DrawGuide(accessory, $"{DrawPrefix}_EnvBigCircleRound_{round}_OE_Guide_To_{oeAlias}", oePosition, duration); + SaveActiveLastRoundOuterEdgeGuide(round, mode, newTwelveDirection, duration); + GreenMoveToPoint(oePosition, accessory, $"round {round} {oeAlias}"); + DebugTrace(accessory, $"Env big circle round {round}: last round outer edge initial guide {oeAlias} mode={mode}"); + } + + private void DrawLastRoundOuterEdgeResolveGuide( + ScriptAccessory accessory, + int round, + P2LastRoundOuterEdgeMode mode, + Vector3 newTwelveDirection, + uint bossOuterEdgeActionId) + { + var northOuterEdge = bossOuterEdgeActionId == BossOuterEdgeNorthActionId; + var oeAlias = mode == P2LastRoundOuterEdgeMode.A点 + ? (northOuterEdge ? "OE6" : "OE5") + : (northOuterEdge ? "OE4" : "OE3"); + var oePosition = mode == P2LastRoundOuterEdgeMode.A点 + ? LastRoundFixedOuterEdgeResolvePosition(bossOuterEdgeActionId) + : LastRoundDefaultOuterEdgeResolvePosition(newTwelveDirection, bossOuterEdgeActionId); + + DrawCircle( + accessory, + $"{DrawPrefix}_EnvBigCircleRound_{round}_OEResolve_{oeAlias}_Circle", + oePosition, + EnvMarkerCircleRadius, + EnvBigCircleLastRoundResolveGuideDuration, + 0, + SolidSafeGreen, + ScaleMode.None); + DrawGuide(accessory, $"{DrawPrefix}_EnvBigCircleRound_{round}_OEResolve_Guide_To_{oeAlias}", oePosition, EnvBigCircleLastRoundResolveGuideDuration); + GreenMoveToPoint(oePosition, accessory, $"round {round} {oeAlias} after {bossOuterEdgeActionId}"); + DebugTrace(accessory, $"Boss cast {bossOuterEdgeActionId}: last round outer edge resolve guide {oeAlias} mode={mode}"); + } + + private void SaveActiveOuterEdgeGuide(int round, MarkerAlias finalAlias, Vector3 finalPosition, int duration, bool hasFinalPosition) + { + lock (_outerEdgeGuideLock) + { + _activeOuterEdgeGuide = true; + _activeOuterEdgeGuideRound = round; + _activeOuterEdgeGuideFinalAlias = finalAlias; + _activeOuterEdgeGuideFinalPosition = finalPosition; + _activeOuterEdgeGuideDuration = duration; + _activeOuterEdgeGuideHasFinalPosition = hasFinalPosition; + _activeOuterEdgeGuideUseLastRoundConfig = false; + _activeOuterEdgeGuideLastRoundMode = P2LastRoundOuterEdgeMode.塔中间; + _activeOuterEdgeGuideTwelveDirection = default; + } + } + + private void SaveActiveLastRoundOuterEdgeGuide(int round, P2LastRoundOuterEdgeMode mode, Vector3 newTwelveDirection, int duration) + { + lock (_outerEdgeGuideLock) + { + _activeOuterEdgeGuide = true; + _activeOuterEdgeGuideRound = round; + _activeOuterEdgeGuideFinalAlias = MarkerAlias.None; + _activeOuterEdgeGuideFinalPosition = default; + _activeOuterEdgeGuideDuration = duration; + _activeOuterEdgeGuideHasFinalPosition = false; + _activeOuterEdgeGuideUseLastRoundConfig = true; + _activeOuterEdgeGuideLastRoundMode = mode; + _activeOuterEdgeGuideTwelveDirection = newTwelveDirection; + } + } + + private void ResolveActiveOuterEdgeGuide(ScriptAccessory accessory, uint actionId) + { + int round; + MarkerAlias finalAlias; + Vector3 finalPosition; + int duration; + bool hasFinalPosition; + bool useLastRoundConfig; + P2LastRoundOuterEdgeMode lastRoundMode; + Vector3 lastRoundTwelveDirection; + + lock (_outerEdgeGuideLock) + { + if (!_activeOuterEdgeGuide) + return; + + round = _activeOuterEdgeGuideRound; + finalAlias = _activeOuterEdgeGuideFinalAlias; + finalPosition = _activeOuterEdgeGuideFinalPosition; + duration = _activeOuterEdgeGuideDuration > 0 ? _activeOuterEdgeGuideDuration : EnvBigCircleGuideDuration; + hasFinalPosition = _activeOuterEdgeGuideHasFinalPosition; + useLastRoundConfig = _activeOuterEdgeGuideUseLastRoundConfig; + lastRoundMode = _activeOuterEdgeGuideLastRoundMode; + lastRoundTwelveDirection = _activeOuterEdgeGuideTwelveDirection; + _activeOuterEdgeGuide = false; + _activeOuterEdgeGuideRound = 0; + _activeOuterEdgeGuideFinalAlias = MarkerAlias.None; + _activeOuterEdgeGuideFinalPosition = default; + _activeOuterEdgeGuideDuration = 0; + _activeOuterEdgeGuideHasFinalPosition = false; + _activeOuterEdgeGuideUseLastRoundConfig = false; + _activeOuterEdgeGuideLastRoundMode = P2LastRoundOuterEdgeMode.塔中间; + _activeOuterEdgeGuideTwelveDirection = default; + } + + accessory.Method.RemoveDraw($"{DrawPrefix}_EnvBigCircleRound_{round}_OE_.*"); + if (useLastRoundConfig) + { + var bossOuterEdgeActionId = GetLastBossOuterEdgeActionId(); + if (IsBossOuterEdgeAction(bossOuterEdgeActionId)) + { + DrawLastRoundOuterEdgeResolveGuide(accessory, round, lastRoundMode, lastRoundTwelveDirection, bossOuterEdgeActionId); + } + else + { + GreenMoveStopAndClear(accessory, $"after {actionId}"); + DebugEcho(accessory, $"Boss cast {actionId}: last round outer edge resolve skipped, last boss cast={bossOuterEdgeActionId}"); + } + + return; + } + + if (hasFinalPosition && finalAlias != MarkerAlias.None) + { + DrawGuide(accessory, $"{DrawPrefix}_EnvBigCircleRound_{round}_Guide_{finalAlias}", finalPosition, duration); + GreenMoveToPoint(finalPosition, accessory, $"round {round} {finalAlias} after {actionId}"); + DebugTrace(accessory, $"Boss cast {actionId}: outer edge guide resolved, direct guide={finalAlias}"); + } + else + { + GreenMoveStopAndClear(accessory, $"after {actionId}"); + DebugTrace(accessory, $"Boss cast {actionId}: outer edge guide resolved, no final guide"); + } + } + + private void ScheduleLastRoundOuterEdgeGuide(ScriptAccessory accessory, int round, Vector3 newTwelveDirection) + { + var generation = _generation; + var resolveGeneration = Volatile.Read(ref _outerEdgeResolveGeneration); + var mode = P2LastRoundOuterEdgeSetting; + SaveActiveLastRoundOuterEdgeGuide(round, mode, newTwelveDirection, EnvBigCircleGuideDuration); + Task.Run(async () => + { + try + { + await Task.Delay(EnvBigCircleLastRoundGuideDuration); + if (generation != _generation) + return; + + if (resolveGeneration != Volatile.Read(ref _outerEdgeResolveGeneration)) + return; + + lock (_envBigCircleLock) + { + if (_envBigCircleRound != round) + return; + } + + DrawLastRoundOuterEdgeInitialGuide(accessory, round, newTwelveDirection, mode, EnvBigCircleGuideDuration); + } + catch (Exception ex) + { + DebugEcho(accessory, $"Env big circle round {round}: outer edge delayed guide failed: {ex.Message}"); + } + }); + } + + private int LastRoundStartDelayMs() + { + return EnvBigCircleLastRoundStartDelayMs; + } + + private EnvBigCircleRoundPlan PrepareEnvBigCircleRoundPlan(ScriptAccessory accessory, int round, int firstIndex, int secondIndex, IReadOnlyList roundTargetIconRefreshPartyIndexes) + { + var firstCenter = GetEnvBigCircleCenter(firstIndex); + var secondCenter = GetEnvBigCircleCenter(secondIndex); + GetRoundDirections(firstCenter, secondCenter, out var newTwelveDirection, out var newRightDirection); + StoreEnvBigCircleRoundTwelveDirection(round, newTwelveDirection); + SortEnvBigCirclesByRoundSide(firstIndex, secondIndex, newRightDirection, out var leftIndex, out var leftCenter, out var rightIndex, out var rightCenter); + + MarkType?[] roundMarks; + var markersReady = false; + lock (_targetIconLock) + { + if (round == 2) + TryRegroupRound2TargetIconGroupsLocked(accessory, roundTargetIconRefreshPartyIndexes); + + markersReady = PrepareRoundMarkerVariablesLocked(accessory, round, newTwelveDirection, out roundMarks); + } + + if (!markersReady || roundMarks == null) + { + DebugEcho(accessory, $"Env big circle round {round}: marker variables not ready."); + return null; + } + + DebugRoundMarkerAssignments(accessory, round, roundMarks); + return new EnvBigCircleRoundPlan + { + Round = round, + FirstIndex = firstIndex, + SecondIndex = secondIndex, + LeftIndex = leftIndex, + RightIndex = rightIndex, + LeftCenter = leftCenter, + RightCenter = rightCenter, + NewTwelveDirection = newTwelveDirection, + RoundMarks = roundMarks, + }; + } + + private void ExecuteEnvBigCircleRoundPlan(ScriptAccessory accessory, EnvBigCircleRoundPlan plan) + { + var round = plan.Round; + ClearActiveOuterEdgeGuideState(); + + if (round > 1) + accessory.Method.RemoveDraw($"{DrawPrefix}_EnvBigCircleRound_{round - 1}_.*"); + + var myIndex = GetMyIndex(accessory); + var roundMarks = plan.RoundMarks; + if (myIndex >= 0 && myIndex < roundMarks.Length && roundMarks[myIndex].HasValue) + { + var alias = MarkerAliasForRoundMark(roundMarks[myIndex].Value, round % 2 == 1); + var bossOuterEdgeActionId = GetLastBossOuterEdgeActionId(); + var useOuterEdgeRelay = round > 1 && round % 2 == 1 && IsBossOuterEdgeAction(bossOuterEdgeActionId); + if (DrawEnvMarkerAlias(accessory, round, alias, plan.LeftIndex, plan.LeftCenter, plan.RightIndex, plan.RightCenter, plan.NewTwelveDirection, !useOuterEdgeRelay, out var finalPosition)) + { + if (useOuterEdgeRelay) + DrawOuterEdgeRelayGuide(accessory, round, bossOuterEdgeActionId, plan.NewTwelveDirection, finalPosition, alias, EnvBigCircleGuideDurationForRound(round)); + else + GreenMoveToPoint(finalPosition, accessory, $"round {round} {alias}"); + + DebugTrace(accessory, $"Env big circle round {round}: my marker={roundMarks[myIndex].Value} alias={alias}"); + } + } + else + { + DebugTrace(accessory, $"Env big circle round {round}: no marker for my index={myIndex}"); + } + + DebugTrace(accessory, $"Env big circle round {round}: indexes={plan.FirstIndex},{plan.SecondIndex} left={plan.LeftIndex} right={plan.RightIndex}"); + CommandMarkRoundMarkersForDuration(accessory, round, roundMarks); + + if (round == EnvBigCircleLastRound) + ScheduleLastRoundOuterEdgeGuide(accessory, round, plan.NewTwelveDirection); + } + + private void ScheduleDelayedEnvBigCircleRoundPlan(ScriptAccessory accessory, EnvBigCircleRoundPlan plan, int delayMs) + { + var generation = _generation; + DebugTrace(accessory, $"Env big circle round {plan.Round}: delayed start {delayMs}ms"); + Task.Run(async () => + { + try + { + await Task.Delay(delayMs); + if (generation != _generation) + return; + + lock (_envBigCircleLock) + { + if (_envBigCircleRound != plan.Round) + return; + } + + ExecuteEnvBigCircleRoundPlan(accessory, plan); + } + catch (Exception ex) + { + DebugEcho(accessory, $"Env big circle round {plan.Round}: delayed start failed: {ex.Message}"); + } + }); + } + + private void DrawEnvBigCircleRound(ScriptAccessory accessory, int round, int firstIndex, int secondIndex, IReadOnlyList roundTargetIconRefreshPartyIndexes = null) + { + var plan = PrepareEnvBigCircleRoundPlan(accessory, round, firstIndex, secondIndex, roundTargetIconRefreshPartyIndexes); + if (plan == null) + return; + + var delayMs = round == EnvBigCircleLastRound ? LastRoundStartDelayMs() : 0; + if (delayMs > 0) + ScheduleDelayedEnvBigCircleRoundPlan(accessory, plan, delayMs); + else + ExecuteEnvBigCircleRoundPlan(accessory, plan); + } + + private async Task WaitUntilAsync(Func condition, int timeoutMs, int intervalMs = 25) + { + var start = Environment.TickCount64; + + while (!condition()) + { + if (Environment.TickCount64 - start >= timeoutMs) + return false; + + await Task.Delay(intervalMs); + } + + return true; + } + + #endregion + + #region Draw Helpers + + private void DrawCircle(ScriptAccessory accessory, string name, Vector3 position, float radius, int duration, int delay = 0, Vector4? color = null, ScaleMode scaleMode = ScaleMode.ByTime) + { + var dp = accessory.Data.GetDefaultDrawProperties(); + dp.Name = name; + dp.Position = position; + dp.Scale = new Vector2(radius); + dp.Color = color ?? accessory.Data.DefaultDangerColor; + dp.Delay = delay; + dp.DestoryAt = duration; + dp.ScaleMode = scaleMode; + accessory.Method.SendDraw(DrawModeEnum.Default, DrawTypeEnum.Circle, dp); + } + + private void DrawCircleOnOwner(ScriptAccessory accessory, string name, uint ownerId, float radius, int duration, int delay = 0, Vector4? color = null, ScaleMode scaleMode = ScaleMode.ByTime) + { + var dp = accessory.Data.GetDefaultDrawProperties(); + dp.Name = name; + dp.Owner = ownerId; + dp.Scale = new Vector2(radius); + dp.Color = color ?? accessory.Data.DefaultDangerColor; + dp.Delay = delay; + dp.DestoryAt = duration; + dp.ScaleMode = scaleMode; + accessory.Method.SendDraw(DrawModeEnum.Default, DrawTypeEnum.Circle, dp); + } + + private void DrawDonut(ScriptAccessory accessory, string name, Vector3 position, float innerRadius, float outerRadius, int duration, int delay = 0, Vector4? color = null, ScaleMode scaleMode = ScaleMode.ByTime) + { + var dp = accessory.Data.GetDefaultDrawProperties(); + dp.Name = name; + dp.Position = position; + dp.Scale = new Vector2(outerRadius); + dp.InnerScale = new Vector2(innerRadius); + dp.Radian = MathF.PI * 2.0f; + dp.Color = color ?? accessory.Data.DefaultDangerColor; + dp.Delay = delay; + dp.DestoryAt = duration; + dp.ScaleMode = scaleMode; + accessory.Method.SendDraw(DrawModeEnum.Default, DrawTypeEnum.Donut, dp); + } + + private void DrawRect(Event @event, ScriptAccessory accessory, string name, float width, float length, int duration, int delay = 0, Vector4? color = null, ScaleMode scaleMode = ScaleMode.YByTime) + { + DrawRect(accessory, name, @event.SourcePosition, @event.SourceRotation, width, length, duration, delay, color, scaleMode); + } + + private void DrawRect(ScriptAccessory accessory, string name, Vector3 position, float rotation, float width, float length, int duration, int delay = 0, Vector4? color = null, ScaleMode scaleMode = ScaleMode.YByTime) + { + var dp = accessory.Data.GetDefaultDrawProperties(); + dp.Name = name; + dp.Position = position; + dp.Rotation = rotation; + dp.Scale = new Vector2(width, length); + dp.Color = color ?? accessory.Data.DefaultDangerColor; + dp.Delay = delay; + dp.DestoryAt = duration; + dp.ScaleMode = scaleMode; + accessory.Method.SendDraw(DrawModeEnum.Default, DrawTypeEnum.Rect, dp); + } + + private void DrawFan(Event @event, ScriptAccessory accessory, string name, float radius, float radian, int duration, int delay = 0, Vector4? color = null, ScaleMode scaleMode = ScaleMode.ByTime) + { + DrawFan(accessory, name, @event.SourcePosition, @event.SourceRotation, radius, radian, duration, delay, color, scaleMode); + } + + private void DrawFan(ScriptAccessory accessory, string name, Vector3 position, float rotation, float radius, float radian, int duration, int delay = 0, Vector4? color = null, ScaleMode scaleMode = ScaleMode.ByTime) + { + var dp = accessory.Data.GetDefaultDrawProperties(); + dp.Name = name; + dp.Position = position; + dp.Rotation = rotation; + dp.Scale = new Vector2(radius); + dp.Radian = radian; + dp.Color = color ?? accessory.Data.DefaultDangerColor; + dp.Delay = delay; + dp.DestoryAt = duration; + dp.ScaleMode = scaleMode; + accessory.Method.SendDraw(DrawModeEnum.Default, DrawTypeEnum.Fan, dp); + } + + private void DrawLineToTarget(ScriptAccessory accessory, string name, uint sourceId, uint targetId, float width, int duration, int delay = 0, Vector4? color = null) + { + var dp = accessory.Data.GetDefaultDrawProperties(); + dp.Name = name; + dp.Owner = sourceId; + dp.TargetObject = targetId; + dp.Scale = new Vector2(width, 1.0f); + dp.Color = color ?? accessory.Data.DefaultDangerColor; + dp.Delay = delay; + dp.DestoryAt = duration; + dp.ScaleMode = ScaleMode.YByDistance; + accessory.Method.SendDraw(DrawModeEnum.Default, DrawTypeEnum.Rect, dp); + } + + private void DrawGuide(ScriptAccessory accessory, string name, Vector3 targetPosition, int duration, int delay = 0, Vector4? color = null) + { + var dp = accessory.Data.GetDefaultDrawProperties(); + dp.Name = name; + dp.Owner = GetMyId(accessory); + dp.TargetPosition = targetPosition; + dp.Scale = new Vector2(0.5f); + dp.ScaleMode = ScaleMode.YByDistance; + dp.Color = color ?? GuideColor.V4; + dp.Delay = delay; + dp.DestoryAt = duration; + accessory.Method.SendDraw(DrawModeEnum.Imgui, DrawTypeEnum.Displacement, dp); + } + + private void DrawGuideBetweenPositions(ScriptAccessory accessory, string name, Vector3 sourcePosition, Vector3 targetPosition, int duration, int delay = 0, Vector4? color = null) + { + var dp = accessory.Data.GetDefaultDrawProperties(); + dp.Name = name; + dp.Position = sourcePosition; + dp.TargetPosition = targetPosition; + dp.Scale = new Vector2(0.5f); + dp.ScaleMode = ScaleMode.YByDistance; + dp.Color = color ?? GuideColor.V4; + dp.Delay = delay; + dp.DestoryAt = duration; + accessory.Method.SendDraw(DrawModeEnum.Imgui, DrawTypeEnum.Displacement, dp); + } + + private void DrawInactiveGuideBetweenPositions(ScriptAccessory accessory, string name, Vector3 sourcePosition, Vector3 targetPosition, int duration, int delay = 0) + { + DrawGuideBetweenPositions(accessory, name, sourcePosition, targetPosition, duration, delay, accessory.Data.DefaultDangerColor); + } + + #endregion + + #region Mechanisms + + [ScriptMethod(name: "OE读条记录与指路切换", eventType: EventTypeEnum.StartCasting, eventCondition: ["ActionId:regex:^(47826|47827|47836|47837)$"], userControl: false)] + public void StartCasting_OuterEdgeGuide(Event @event, ScriptAccessory accessory) + { + _acc = accessory; + + if (!TryGetActionId(@event, out var actionId)) return; + + if (IsBossOuterEdgeAction(actionId)) + { + SetLastBossOuterEdgeActionId(actionId); + DebugTrace(accessory, $"Boss outer edge cast recorded: {actionId}->{OuterEdgeAlias(actionId)}"); + return; + } + + if (IsBossOuterEdgeResolveAction(actionId)) + { + Interlocked.Increment(ref _outerEdgeResolveGeneration); + ResolveActiveOuterEdgeGuide(accessory, actionId); + } + } + + [ScriptMethod(name: "璁板綍02CB-02CD鐐瑰悕", eventType: EventTypeEnum.TargetIcon, eventCondition: ["Id:regex:^(02CB|02CC|02CD|02cb|02cc|02cd)$"], userControl: false)] + public void RecordTargetIcon02CBTo02CD(Event @event, ScriptAccessory accessory) + { + _acc = accessory; + _lastMechanicAt = NowMs(); + + if (!TryGetTargetId(@event, out var targetId)) return; + if (!TryGetTargetIconId(@event, out var iconId)) return; + if (!IsTrackedTargetIcon(iconId)) return; + + var index = GetPlayerIndex(accessory, targetId); + if (index < 0 || index >= _lastTargetIconByPartyIndex.Length) return; + + string group1Text = null; + string group2Text = null; + var groupsResolvedNow = false; + lock (_targetIconLock) + { + _lastTargetIconByPartyIndex[index] = iconId; + groupsResolvedNow = TryResolveTargetIconGroupsLocked(accessory, out group1Text, out group2Text); + } + + if (DeveloperMode) + DebugTrace(accessory, $"TargetIcon P{index + 1}={FormatTargetIconId(iconId)} target={FormatObjectId(targetId)}"); + + if (groupsResolvedNow) + { + DebugEcho(accessory, $"TargetIcon groups fixed: G1={group1Text} | G2={group2Text}"); + CommandMarkClear(accessory, "TargetIcon groups fixed"); + } + + var collectedReady = TryCollectTargetIconAndStartEnvBigCircleRound( + index, + out var collectedRound, + out var collectedFirstIndex, + out var collectedSecondIndex, + out var collectedUpdateCount, + out var collectedTargetIconUpdatePartyIndexes); + + if (collectedUpdateCount > 0 && !collectedReady && DeveloperMode) + DebugTrace(accessory, $"Env big circle round {collectedRound}: target icons collected {collectedUpdateCount}/{EnvBigCircleTargetIconUpdateCount}"); + + if (collectedReady) + { + if (collectedRound == EnvBigCircleLastRound && collectedUpdateCount < EnvBigCircleTargetIconUpdateCount) + DebugTrace(accessory, $"Env big circle round {collectedRound}: big circles ready, no target icon refresh expected {collectedUpdateCount}/{EnvBigCircleTargetIconUpdateCount}"); + else + DebugTrace(accessory, $"Env big circle round {collectedRound}: target icons and big circles ready {collectedUpdateCount}/{EnvBigCircleTargetIconUpdateCount}"); + + DrawEnvBigCircleRound(accessory, collectedRound, collectedFirstIndex, collectedSecondIndex, collectedTargetIconUpdatePartyIndexes); + } + } + + [ScriptMethod(name: "EnvControl澶у湀杞", eventType: EventTypeEnum.EnvControl, eventCondition: ["Index:regex:^\\d+$"], userControl: false)] + public void EnvControl_BigCircleRound(Event @event, ScriptAccessory accessory) + { + _acc = accessory; + + if (!int.TryParse(@event["Index"], NumberStyles.Integer, CultureInfo.InvariantCulture, out var index)) return; + if (!int.TryParse(@event["Flag"], NumberStyles.Integer, CultureInfo.InvariantCulture, out var flag)) return; + + var name = $"{DrawPrefix}_Env_{index}"; + if (flag == 0 || flag == 4 || flag == 8) + { + accessory.Method.RemoveDraw($"{name}.*"); + return; + } + + if (!IsEnvBigCircleIndex(index) || flag != 2) + { + DebugTrace(accessory, $"EnvControl index={index} flag={flag}"); + return; + } + + int round = 0; + int firstIndex = 0; + int secondIndex = 0; + int collectedUpdateCount = 0; + List collectedTargetIconUpdatePartyIndexes = null; + var startImmediately = false; + var collectedReady = false; + var waitForBothConditions = false; + lock (_envBigCircleLock) + { + if (_envBigCircleRound > 0 && _pendingEnvBigCircleRoundToStart > 0) + return; + + if (_pendingEnvBigCircleIndexes.Contains(index)) + return; + + _pendingEnvBigCircleIndexes.Add(index); + if (_pendingEnvBigCircleIndexes.Count < 2) + return; + + firstIndex = _pendingEnvBigCircleIndexes[0]; + secondIndex = _pendingEnvBigCircleIndexes[1]; + _pendingEnvBigCircleIndexes.Clear(); + + if (_envBigCircleRound == 0) + { + round = 1; + _envBigCircleRound = round; + ClearPendingEnvBigCircleRoundStartLocked(); + startImmediately = true; + } + else + { + _pendingEnvBigCircleRoundToStart = _envBigCircleRound + 1; + _pendingEnvBigCircleFirstIndexToStart = firstIndex; + _pendingEnvBigCircleSecondIndexToStart = secondIndex; + collectedReady = TryStartCollectedEnvBigCircleRoundLocked(out round, out firstIndex, out secondIndex, out collectedUpdateCount, out collectedTargetIconUpdatePartyIndexes); + waitForBothConditions = !collectedReady; + } + } + + if (startImmediately) + { + DrawEnvBigCircleRound(accessory, round, firstIndex, secondIndex); + } + else if (collectedReady) + { + if (round == EnvBigCircleLastRound && collectedUpdateCount < EnvBigCircleTargetIconUpdateCount) + DebugTrace(accessory, $"Env big circle round {round}: big circles ready, no target icon refresh expected {collectedUpdateCount}/{EnvBigCircleTargetIconUpdateCount}"); + else + DebugTrace(accessory, $"Env big circle round {round}: target icons and big circles ready {collectedUpdateCount}/{EnvBigCircleTargetIconUpdateCount}"); + + DrawEnvBigCircleRound(accessory, round, firstIndex, secondIndex, collectedTargetIconUpdatePartyIndexes); + } + else if (waitForBothConditions) + { + DebugTrace(accessory, $"Env big circle round {round}: indexes={firstIndex},{secondIndex} waiting target icons {collectedUpdateCount}/{EnvBigCircleTargetIconUpdateCount}"); + } + + DebugTrace(accessory, $"EnvControl index={index} flag={flag}"); + } + + #endregion + } +} diff --git "a/a16239438/(\345\246\226\346\230\237\344\271\261\350\210\236\347\273\235\345\242\203\346\210\230)P4 \346\214\207\350\267\257&\350\207\252\345\212\250\347\247\273\345\212\250.cs" "b/a16239438/(\345\246\226\346\230\237\344\271\261\350\210\236\347\273\235\345\242\203\346\210\230)P4 \346\214\207\350\267\257&\350\207\252\345\212\250\347\247\273\345\212\250.cs" new file mode 100644 index 0000000..76261ed --- /dev/null +++ "b/a16239438/(\345\246\226\346\230\237\344\271\261\350\210\236\347\273\235\345\242\203\346\210\230)P4 \346\214\207\350\267\257&\350\207\252\345\212\250\347\247\273\345\212\250.cs" @@ -0,0 +1,3610 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using System.Linq; +using System.Numerics; +using System.Reflection; +using System.Threading; +using System.Threading.Tasks; +using KodakkuAssist.Data; +using KodakkuAssist.Module.Draw; +using KodakkuAssist.Module.GameEvent; +using KodakkuAssist.Module.GameEvent.Struct; +using KodakkuAssist.Module.GameOperate; +using KodakkuAssist.Script; + +#nullable disable + +namespace RyougiMioScriptNamespace +{ + [ScriptType(name: "(妖星乱舞绝境战)P4 指路&自动移动", territorys: [1363], guid: "79ae48d3-c462-4e4a-8108-9eb507e131b2", version: "0.0.1.3", author: "RyougiMio", note: "P4脚本。\n鸳鸯锅:攻击12345678左 其他圈右\n后续:攻击12是钢铁 禁止12是背对 锁链12是正对\n!!!!!!!!自动移动依赖于PromeRotation!!!!!!!!")] + public class Script1363P4 + { + #region Settings + + [UserSetting("-----全局设置-----")] + public bool _____Global_Settings_____ { get; set; } = true; + + [UserSetting("是否开启屏幕文字提示")] + public bool EnableText { get; set; } = true; + + [UserSetting("是否开启TTS语音提示")] + public bool EnableTTS { get; set; } = true; + + [UserSetting("鸳鸯锅标记")] + public bool EnableP4HotpotMarks { get; set; } = false; + + [UserSetting("后续机制标记")] + public bool EnableP4LaterMarks { get; set; } = false; + + [UserSetting("P4真假预测播报(Off关闭 Echo默语 Party小队频道)")] + public P4PlanMessageMode PlanMessageMode { get; set; } = P4PlanMessageMode.Off; + + [UserSetting("启用PR GreenMove自动移动")] + public bool EnableGreenMove { get; set; } = false; + + [UserSetting("PR移动判定距离")] + public float GreenMoveTolerance { get; set; } = 0.1f; + + [UserSetting("PR移动速度倍率")] + public float GreenMoveSpeedMultiplier { get; set; } = 1.0f; + + [UserSetting("PR移动忽略Y轴")] + public bool GreenMoveIgnoreY { get; set; } = true; + + [UserSetting("PR移动前清空队列")] + public bool GreenMoveClearQueueBeforeMove { get; set; } = true; + + [UserSetting("PR移动最大读条等待ms")] + public int GreenMoveMaxCastWaitMs { get; set; } = 2500; + + [UserSetting("PR移动等待队列读条/滑步")] + public bool GreenMoveWaitForQueuedCast { get; set; } = true; + + [UserSetting("Developer mode")] + public bool DeveloperMode { get; set; } = false; + + [UserSetting("常用危险色")] + public ScriptColor DangerColor { get; set; } = new ScriptColor { V4 = new Vector4(1.0f, 0.0f, 0.0f, 0.01f) }; + + [UserSetting("常用安全色")] + public ScriptColor SafeColor { get; set; } = new ScriptColor { V4 = new Vector4(0.0f, 1.0f, 0.0f, 0.01f) }; + + [UserSetting("指路/引导颜色")] + public ScriptColor GuideColor { get; set; } = new ScriptColor { V4 = new Vector4(0.0f, 1.0f, 1.0f, 1.0f) }; + + #endregion + + #region State + + private enum Phase + { + Init, + P4, + Done, + } + + public enum P4PlanMessageMode + { + Off, + Echo, + Party, + } + + private enum P4HalfSide + { + Unknown, + Left, + Right, + } + + private enum P4HalfColor + { + Unknown, + Blue, + Purple, + } + + private enum P4ChainStep + { + None, + Move, + Petrify, + Element, + } + + private enum P4ClockDirection + { + Unknown, + Twelve, + Three, + Six, + Nine, + } + + private enum P4ElementCall + { + Unknown, + FireSteel, + WaterMoon, + } + + private enum P4BuffGroupKind + { + Unknown, + A, + B, + C, + D, + E, + F, + } + + private enum P4DangerShape + { + Rect, + Fan, + } + + private enum P4BuffCategory + { + Mixed, + ThunderShare, + Petrify, + Element, + } + + private sealed class P4BuffRecord + { + public int PartyIndex; + public uint StatusId; + public int State; + public int DurationMs; + public long ExpiresAt; + public int SnapshotRemainingMs; + } + + private sealed class P4BuffGroup + { + public P4BuffGroupKind Kind; + public P4BuffCategory Category; + public int DurationMs; + public int SnapshotRemainingMs; + public long ExpiresAt; + public readonly List Records = new List(); + } + + private sealed class P4DangerRecord + { + public uint ActionId; + public P4DangerShape Shape; + public Vector3 Position; + public float Rotation; + public long CapturedAt; + } + + private const uint InvalidObjectId = 0xE0000000; + private const uint P4StartActionId = 49884; + private const uint P4PurpleSourceActionId = 50068; + private const uint P4ResolveActionId = 50069; + private const uint P4ChainStartActionEffectId = 50070; + private const uint P4TruthStatusId = 2056; + private const uint P4XDataId = 19510; + private const uint P4CDataId = 19507; + private const int P4StatusWindowMs = 11000; + private const int P4GuideDurationMs = 6000; + private const int P4ChainGuideDurationMs = 8000; + private const int P4ChainDelayMs = 500; + private const int P4LaterMarkApplyDelayMs = 300; + private const int P4StateUnknown = 0; + private const int P4StateTrue = 1; + private const int P4StateFalse = 2; + private const float P4GuideOffset = 4.0f; + private const float P4ChainGuideOffset = 8.0f; + private const int P4BuffGroupToleranceMs = 1200; + private const int P4MaxScheduledGroupCount = 6; + private const float P4RectDangerWidth = 10.0f; + private const float P4RectDangerLength = 40.0f; + private const float P4FanDangerRadius = 40.0f; + private const float P4FanDangerRadian = MathF.PI / 2.0f; + private const int P4BRectLookbackMs = 7000; + private const int P4DFanLookbackMs = 10000; + private const string DrawPrefix = "KDYD_P4"; + private const string GreenMovePrefix = "PromeRotation.GreenMove."; + private const string DebugLogPath = @"e:\game\Workplace\Kodd\KDYD_P4_debug.log"; + + private static readonly Vector3 P4ArenaCenter = new Vector3(100.0f, 0.0f, 100.0f); + private static readonly Vector3 DefaultNorth = new Vector3(0.0f, 0.0f, -1.0f); + private static readonly Vector4 SolidDangerRed = new Vector4(1.0f, 0.0f, 0.0f, 1.0f); + private static readonly string[] PartyPriorityLabels = new[] { "MT", "ST", "H1", "H2", "D1", "D2", "D3", "D4" }; + private static readonly uint[] P4TrackedStatusIds = new uint[] { 5541, 5542, 5543, 5544, 5545, 5546, 5547, 5548, 454, 1382 }; + private static readonly MarkType[] P4LeftMarks = new[] { MarkType.Attack1, MarkType.Attack2, MarkType.Attack3, MarkType.Attack4, MarkType.Attack5, MarkType.Attack6, MarkType.Attack7, MarkType.Attack8 }; + private static readonly MarkType[] P4RightMarks = new[] { MarkType.Bind1, MarkType.Bind2, MarkType.Bind3, MarkType.Circle, MarkType.Stop1, MarkType.Stop2, MarkType.Cross, MarkType.Square }; + private static readonly MarkType[] P4PetrifyTrueMarks = new[] { MarkType.Stop1, MarkType.Stop2 }; + private static readonly MarkType[] P4PetrifyFalseMarks = new[] { MarkType.Bind1, MarkType.Bind2 }; + private static readonly float[] P4CardinalClocks = new[] { 0.0f, 3.0f, 6.0f, 9.0f }; + private static readonly float[] P4FinalHalfClocks = new[] { 11.5f, 12.5f, 2.5f, 3.5f, 5.5f, 6.5f, 8.5f, 9.5f }; + + private ScriptAccessory _acc; + private Phase _phase = Phase.Init; + private int _generation; + private readonly object _p4Lock = new object(); + private readonly object _p4CommandMarkTimerLock = new object(); + private readonly int[,] _p4StatusStateByPartyAndStatus = new int[8, 10]; + private readonly long[,] _p4StatusExpiresAtByPartyAndStatus = new long[8, 10]; + private readonly P4HalfSide[] _p4ResolvedSideByPartyIndex = new P4HalfSide[8]; + private readonly P4HalfColor[] _p4ResolvedColorByPartyIndex = new P4HalfColor[8]; + private readonly MarkType?[] _p4CommandMarkByPartyIndex = new MarkType?[8]; + private readonly List _p4BuffRecords = new List(); + private readonly List _p4BuffGroups = new List(); + private readonly List _p4DangerRecords = new List(); + private readonly object _p4DebugFileLock = new object(); + private Timer _p4CommandMarkClearTimer; + private int _p4LaterMarkApplyToken; + private P4ChainStep _p4ChainStep = P4ChainStep.None; + private int _p4ChainRound; + private int _p4ChainStepGeneration; + private bool _p4BuffGroupsScheduled; + private bool _p4BuffGroupsScheduleQueued; + private bool _p4FGroupDangerDrawn; + private long _p4FGroupStartedAt; + private P4ElementCall _p4CElementCall = P4ElementCall.Unknown; + private P4ElementCall _p4FElementCall = P4ElementCall.Unknown; + private int _p4XParam; + private long _p4XExpiresAt; + private long _p4XUpdatedAt; + private int _p4CParam; + private long _p4CExpiresAt; + private long _p4CUpdatedAt; + private int _p4XEventCount; + private int _p4GroupSnapshotParam; + private long _p4GroupSnapshotAt; + private int _p4FourthXParam; + private long _p4FourthSnapshotAt; + private Vector3 _p4FourthTargetPosition; + private Vector3 _p4FourthNewTwelveDirection; + private bool _p4FourthDirectionReady; + private Vector3 _p4HotpotBlueSourcePosition; + private Vector3 _p4HotpotPurpleSourcePosition; + private bool _p4HotpotBlueSourceReady; + private bool _p4HotpotPurpleSourceReady; + private bool _p4HotpotResolved; + private object _greenMoveToPointSub; + private MethodInfo _greenMoveToPointInvoke; + private object _greenMoveClearQueueSub; + private MethodInfo _greenMoveClearQueueInvoke; + private object _greenMoveStopSub; + private MethodInfo _greenMoveStopInvoke; + private bool _warnedGreenMove; + + #endregion + + #region Initialization + + public void Init(ScriptAccessory accessory) + { + _acc = accessory; + _phase = Phase.Init; + _generation++; + ResetP4State(); + _warnedGreenMove = false; + + accessory.Method.RemoveDraw(".*"); + CommandMarkClear(accessory); + } + + private void ResetMechanic(ScriptAccessory accessory, bool removeDraw = true) + { + _generation++; + ResetP4State(); + + GreenMoveStopAndClear(accessory, "reset mechanic"); + + if (removeDraw) + accessory.Method.RemoveDraw($"{DrawPrefix}_.*"); + + CommandMarkClear(accessory); + } + + private void SetPhase(ScriptAccessory accessory, Phase phase) + { + if (_phase == phase) return; + + _phase = phase; + ResetMechanic(accessory); + } + + private void ResetP4State() + { + CancelP4CommandMarkClearTimer(); + CancelP4LaterCommandMarkApply(); + + lock (_p4Lock) + { + Array.Clear(_p4StatusStateByPartyAndStatus, 0, _p4StatusStateByPartyAndStatus.Length); + Array.Clear(_p4StatusExpiresAtByPartyAndStatus, 0, _p4StatusExpiresAtByPartyAndStatus.Length); + Array.Clear(_p4ResolvedSideByPartyIndex, 0, _p4ResolvedSideByPartyIndex.Length); + Array.Clear(_p4ResolvedColorByPartyIndex, 0, _p4ResolvedColorByPartyIndex.Length); + Array.Clear(_p4CommandMarkByPartyIndex, 0, _p4CommandMarkByPartyIndex.Length); + _p4BuffRecords.Clear(); + _p4BuffGroups.Clear(); + _p4DangerRecords.Clear(); + + _p4ChainStep = P4ChainStep.None; + _p4ChainRound = 0; + _p4ChainStepGeneration++; + _p4BuffGroupsScheduled = false; + _p4BuffGroupsScheduleQueued = false; + _p4FGroupDangerDrawn = false; + _p4FGroupStartedAt = 0; + _p4CElementCall = P4ElementCall.Unknown; + _p4FElementCall = P4ElementCall.Unknown; + _p4XParam = 0; + _p4XExpiresAt = 0; + _p4XUpdatedAt = 0; + _p4CParam = 0; + _p4CExpiresAt = 0; + _p4CUpdatedAt = 0; + _p4XEventCount = 0; + _p4GroupSnapshotParam = 0; + _p4GroupSnapshotAt = 0; + _p4FourthXParam = 0; + _p4FourthSnapshotAt = 0; + _p4FourthTargetPosition = default; + _p4FourthNewTwelveDirection = DefaultNorth; + _p4FourthDirectionReady = false; + _p4HotpotBlueSourcePosition = default; + _p4HotpotPurpleSourcePosition = default; + _p4HotpotBlueSourceReady = false; + _p4HotpotPurpleSourceReady = false; + _p4HotpotResolved = false; + } + } + + #endregion + + #region Helpers + + private void QTTS(string text, int rate = 0) + { + if (!EnableTTS || _acc == null) return; + _acc.Method.TTS(text, rate); + } + + private void QText(string text, int duration, bool isWarning = false) + { + if (!EnableText || _acc == null) return; + _acc.Method.TextInfo(text, duration, isWarning); + } + + private void Alert(string text, int duration = 3000, bool isWarning = true) + { + QTTS(text); + QText(text, duration, isWarning); + } + + private void DebugEcho(ScriptAccessory accessory, string message) + { + if (!DeveloperMode) return; + accessory.Log.Debug($"[KDYD_P4] {message}"); + accessory.Method.SendChat($"/e [KDYD_P4] {message}"); + try + { + lock (_p4DebugFileLock) + { + File.AppendAllText( + DebugLogPath, + $"{DateTime.Now:yyyy-MM-dd HH:mm:ss.fff} [KDYD_P4] {message}{Environment.NewLine}"); + } + } + catch + { + } + } + + private bool AnyP4MarksEnabled => EnableP4HotpotMarks || EnableP4LaterMarks; + + private void CommandMarkClear(ScriptAccessory accessory) + { + if (!AnyP4MarksEnabled) return; + accessory.Method.MarkClear(); + } + + private void CommandMarkPartyMember(ScriptAccessory accessory, int partyIndex, MarkType markType) + { + if (!AnyP4MarksEnabled) return; + if (partyIndex < 0 || partyIndex >= accessory.Data.PartyList.Count) return; + + accessory.Method.Mark(accessory.Data.PartyList[partyIndex], markType); + } + + private void ClearP4CommandMarksNow(ScriptAccessory accessory) + { + CancelP4CommandMarkClearTimer(); + CancelP4LaterCommandMarkApply(); + CommandMarkClear(accessory); + } + + private void CancelP4LaterCommandMarkApply() + { + Interlocked.Increment(ref _p4LaterMarkApplyToken); + } + + private void CancelP4CommandMarkClearTimer() + { + lock (_p4CommandMarkTimerLock) + { + _p4CommandMarkClearTimer?.Dispose(); + _p4CommandMarkClearTimer = null; + } + } + + private void ReplaceP4CommandMarkClearTimer(Timer timer) + { + lock (_p4CommandMarkTimerLock) + { + _p4CommandMarkClearTimer?.Dispose(); + _p4CommandMarkClearTimer = timer; + } + } + + private void ScheduleP4CommandMarkClear(ScriptAccessory accessory, int generation, int durationMs) + { + Timer timer = null; + timer = new Timer(_ => + { + try + { + if (_generation != generation) + return; + + accessory.Method.MarkClear(); + } + finally + { + lock (_p4CommandMarkTimerLock) + { + if (ReferenceEquals(_p4CommandMarkClearTimer, timer)) + _p4CommandMarkClearTimer = null; + } + + timer?.Dispose(); + } + }, null, Math.Max(0, durationMs), Timeout.Infinite); + + ReplaceP4CommandMarkClearTimer(timer); + } + + private static long NowMs() + { + return DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(); + } + + private static int Duration(Event @event, int fallback = 5000) + { + return int.TryParse(@event["DurationMilliseconds"], NumberStyles.Integer, CultureInfo.InvariantCulture, out var duration) + && duration > 0 + ? duration + : fallback; + } + + private static bool TryGetDurationMs(Event @event, out int durationMs) + { + durationMs = 0; + + var raw = @event["DurationMilliseconds"]; + if (string.IsNullOrWhiteSpace(raw)) + return false; + + if (!double.TryParse(raw, NumberStyles.Float, CultureInfo.InvariantCulture, out var duration)) + return false; + + if (duration <= 0.0 || duration > int.MaxValue) + return false; + + durationMs = (int)Math.Round(duration); + return durationMs > 0; + } + + private static bool TryParseObjectId(string text, out uint id) + { + id = 0; + if (string.IsNullOrWhiteSpace(text)) return false; + + text = text.StartsWith("0x", StringComparison.OrdinalIgnoreCase) ? text.Substring(2) : text; + return uint.TryParse(text, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out id); + } + + private static bool TryParseUInt(string text, out uint value) + { + value = 0; + return !string.IsNullOrWhiteSpace(text) + && uint.TryParse(text, NumberStyles.Integer, CultureInfo.InvariantCulture, out value); + } + + private static bool TryGetActionId(Event @event, out uint actionId) + { + return TryParseUInt(@event["ActionId"], out actionId); + } + + private static bool TryGetStatusId(Event @event, out uint statusId) + { + var raw = @event["StatusID"]; + if (string.IsNullOrWhiteSpace(raw)) + raw = @event["StatusId"]; + + return TryParseUInt(raw, out statusId); + } + + private static bool TryGetSourceId(Event @event, out uint sourceId) + { + var raw = @event["SourceId"]; + if (string.IsNullOrWhiteSpace(raw)) + raw = @event["SourceID"]; + + if (!TryParseObjectId(raw, out sourceId)) + sourceId = (uint)@event.SourceId; + + return sourceId != 0 && sourceId != InvalidObjectId; + } + + private static bool TryGetTargetId(Event @event, out uint targetId) + { + var raw = @event["TargetId"]; + if (string.IsNullOrWhiteSpace(raw)) + raw = @event["TargetID"]; + + if (!TryParseObjectId(raw, out targetId)) + targetId = (uint)@event.TargetId; + + return targetId != 0 && targetId != InvalidObjectId; + } + + private static bool TryGetTargetDataId(Event @event, ScriptAccessory accessory, out uint dataId) + { + dataId = 0; + + var raw = @event["TargetDataId"]; + if (string.IsNullOrWhiteSpace(raw)) + raw = @event["TargetDataID"]; + if (string.IsNullOrWhiteSpace(raw)) + raw = @event["DataId"]; + if (string.IsNullOrWhiteSpace(raw)) + raw = @event["DataID"]; + + if (TryParseUInt(raw, out dataId) && dataId != 0) + return true; + + if (!TryGetTargetId(@event, out var targetId)) + return false; + + var obj = accessory.Data.Objects.SearchById(targetId); + if (obj == null) return false; + + dataId = obj.DataId; + return dataId != 0; + } + + private static Vector3 ResolveEventTargetPosition(Event @event, ScriptAccessory accessory) + { + var position = @event.TargetPosition; + if (IsLikelyArenaPosition(position)) + return position; + + if (TryGetTargetId(@event, out var targetId)) + { + var obj = accessory.Data.Objects.SearchById(targetId); + if (obj != null) + return obj.Position; + } + + return position; + } + + private static uint GetMyId(ScriptAccessory accessory) + { + var myObject = accessory.Data.MyObject; + if (myObject != null) + { + var myObjectId = (uint)myObject.EntityId; + if (myObjectId != 0 && myObjectId != InvalidObjectId) + return myObjectId; + } + + return accessory.Data.Me; + } + + private static int GetPlayerIndex(ScriptAccessory accessory, uint objectId) + { + for (var i = 0; i < accessory.Data.PartyList.Count; i++) + { + if (accessory.Data.PartyList[i] == objectId) + return i; + } + + return -1; + } + + private static int GetMyIndex(ScriptAccessory accessory) + { + return GetPlayerIndex(accessory, GetMyId(accessory)); + } + + private static string FormatObjectId(uint objectId) + { + return objectId == 0 ? "-" : objectId.ToString("X8", CultureInfo.InvariantCulture); + } + + private static string FormatPosition(Vector3 position) + { + return $"({position.X:F2},{position.Y:F2},{position.Z:F2})"; + } + + private static string PartyPriorityLabel(int partyIndex) + { + if (partyIndex < 0) + return "-"; + + return partyIndex < PartyPriorityLabels.Length + ? PartyPriorityLabels[partyIndex] + : $"P{partyIndex + 1}"; + } + + private static bool IsLikelyArenaPosition(Vector3 position) + { + return position.X > 50.0f && position.X < 150.0f + && position.Z > 50.0f && position.Z < 150.0f; + } + + private static bool TryNormalizeFromCenter(Vector3 position, out Vector3 direction) + { + var delta = new Vector3(position.X - P4ArenaCenter.X, 0.0f, position.Z - P4ArenaCenter.Z); + var length = MathF.Sqrt(delta.X * delta.X + delta.Z * delta.Z); + if (length < 0.001f) + { + direction = DefaultNorth; + return false; + } + + direction = new Vector3(delta.X / length, 0.0f, delta.Z / length); + return true; + } + + private static Vector3 RightVectorFromNewTwelve(Vector3 newTwelveDirection) + { + return new Vector3(-newTwelveDirection.Z, 0.0f, newTwelveDirection.X); + } + + private static P4HalfSide SideOfPosition(Vector3 position, Vector3 rightVector) + { + var delta = new Vector3(position.X - P4ArenaCenter.X, 0.0f, position.Z - P4ArenaCenter.Z); + var dot = delta.X * rightVector.X + delta.Z * rightVector.Z; + return dot >= 0.0f ? P4HalfSide.Right : P4HalfSide.Left; + } + + private static P4HalfSide OppositeSide(P4HalfSide side) + { + if (side == P4HalfSide.Left) return P4HalfSide.Right; + if (side == P4HalfSide.Right) return P4HalfSide.Left; + return P4HalfSide.Unknown; + } + + private static P4HalfColor OppositeColor(P4HalfColor color) + { + if (color == P4HalfColor.Blue) return P4HalfColor.Purple; + if (color == P4HalfColor.Purple) return P4HalfColor.Blue; + return P4HalfColor.Unknown; + } + + private static bool TryGetP4TrackedStatusIndex(uint statusId, out int index) + { + if (statusId >= 5541 && statusId <= 5548) + { + index = (int)(statusId - 5541); + return true; + } + + if (statusId == 454) + { + index = 8; + return true; + } + + if (statusId == 1382) + { + index = 9; + return true; + } + + index = -1; + return false; + } + + private static bool TryGetP4StateValueFromParam(int param, out int value) + { + if (param == 1122 || param == 1120) + { + value = P4StateTrue; + return true; + } + + if (param == 1121 || param == 1119) + { + value = P4StateFalse; + return true; + } + + value = P4StateUnknown; + return false; + } + + #region P4 Hotpot Helpers + + private static bool TryGetP4DirectTrueStatus(uint statusId, out uint trueStatusId) + { + switch (statusId) + { + case 4887: + trueStatusId = 5542; + return true; + case 4888: + trueStatusId = 5541; + return true; + case 5464: + trueStatusId = 454; + return true; + default: + trueStatusId = 0; + return false; + } + } + + private bool TryGetActiveP4StateValueLocked(long now, out int value) + { + return TryGetActiveP4StateValueLocked(now, out value, out _, out _, out _); + } + + private bool TryGetActiveP4StateValueLocked(long now, out int value, out string windowName, out int param, out int remainingMs) + { + value = P4StateUnknown; + windowName = "-"; + param = 0; + remainingMs = 0; + + var hasX = _p4XParam != 0 && now <= _p4XExpiresAt; + var hasC = _p4CParam != 0 && now <= _p4CExpiresAt; + if (!hasX && !hasC) + return false; + + if (hasX && (!hasC || _p4XUpdatedAt >= _p4CUpdatedAt)) + { + windowName = "X"; + param = _p4XParam; + remainingMs = RemainingMs(_p4XExpiresAt, now); + } + else + { + windowName = "C"; + param = _p4CParam; + remainingMs = RemainingMs(_p4CExpiresAt, now); + } + + return TryGetP4StateValueFromParam(param, out value); + } + + private static Vector3 ExtendGame(Vector3 position, float gameRotation, float length) + { + return new Vector3( + position.X + MathF.Sin(gameRotation) * length, + position.Y, + position.Z + MathF.Cos(gameRotation) * length); + } + + private static Vector3 ExtendDir(Vector3 position, float directionRadians, float length) + { + return new Vector3( + position.X + MathF.Sin(directionRadians) * length, + position.Y, + position.Z - MathF.Cos(directionRadians) * length); + } + + private static float Norm2Pi(float radians) + { + var value = radians % (2.0f * MathF.PI); + return value < 0 ? value + 2.0f * MathF.PI : value; + } + + private static float GameToDir(float gameRotation) + { + return Norm2Pi(MathF.PI - gameRotation); + } + + private static float DirToGame(float directionRadians) + { + return MathF.PI - directionRadians; + } + + private static int Dir8(Vector3 position, Vector3 center) + { + var direction = Math.Round(4.0 - 4.0 * Math.Atan2(position.X - center.X, position.Z - center.Z) / Math.PI) % 8.0; + return direction < 0 ? (int)direction + 8 : (int)direction; + } + + private static bool TryResolveExclusiveP4Truth(int stateA, uint statusA, int stateB, uint statusB, out uint trueStatus) + { + if (stateA == P4StateTrue) + { + trueStatus = statusA; + return true; + } + + if (stateB == P4StateTrue) + { + trueStatus = statusB; + return true; + } + + if (stateA == P4StateFalse) + { + trueStatus = statusB; + return true; + } + + if (stateB == P4StateFalse) + { + trueStatus = statusA; + return true; + } + + trueStatus = 0; + return false; + } + + private static bool TryResolveP4PlayerColor(int[,] states, int partyIndex, out P4HalfColor color, out uint trueShapeStatus, out uint trueColorStatus) + { + color = P4HalfColor.Unknown; + trueShapeStatus = 0; + trueColorStatus = 0; + + if (partyIndex < 0 || partyIndex >= 8) + return false; + + if (!TryResolveExclusiveP4Truth(states[partyIndex, 0], 5541, states[partyIndex, 1], 5542, out trueShapeStatus)) + return false; + + if (!TryResolveExclusiveP4Truth(states[partyIndex, 8], 454, states[partyIndex, 9], 1382, out trueColorStatus)) + return false; + + if ((trueShapeStatus == 5541 && trueColorStatus == 454) + || (trueShapeStatus == 5542 && trueColorStatus == 1382)) + { + color = P4HalfColor.Blue; + return true; + } + + if ((trueShapeStatus == 5541 && trueColorStatus == 1382) + || (trueShapeStatus == 5542 && trueColorStatus == 454)) + { + color = P4HalfColor.Purple; + return true; + } + + return false; + } + + private static P4HalfSide SideForColor(P4HalfColor color, P4HalfColor leftColor, P4HalfColor rightColor) + { + if (color == leftColor) return P4HalfSide.Left; + if (color == rightColor) return P4HalfSide.Right; + return P4HalfSide.Unknown; + } + + #endregion + + private static string FormatP4Side(P4HalfSide side) + { + switch (side) + { + case P4HalfSide.Left: return "L"; + case P4HalfSide.Right: return "R"; + default: return "-"; + } + } + + private static string FormatP4Color(P4HalfColor color) + { + switch (color) + { + case P4HalfColor.Blue: return "蓝"; + case P4HalfColor.Purple: return "紫"; + default: return "-"; + } + } + + private static string FormatP4Mark(MarkType? mark) + { + return mark.HasValue ? mark.Value.ToString() : "-"; + } + + private static string FormatP4StatusStates(int[,] states, int partyIndex) + { + return string.Join(" ", P4TrackedStatusIds.Select((statusId, statusIndex) => $"{statusId}={states[partyIndex, statusIndex]}")); + } + + private static int RemainingMs(long expiresAt, long now) + { + if (expiresAt <= now) + return 0; + + var remaining = expiresAt - now; + return remaining > int.MaxValue ? int.MaxValue : (int)remaining; + } + + private static string FormatRemainingMs(int remainingMs) + { + return remainingMs > 0 + ? $"{remainingMs / 1000.0:F1}s" + : "-"; + } + + private static string FormatP4StatusStatesWithRemaining(int[,] states, long[,] expiresAt, int partyIndex, long now) + { + return string.Join( + " ", + P4TrackedStatusIds.Select((statusId, statusIndex) => + $"{statusId}={states[partyIndex, statusIndex]}({FormatRemainingMs(RemainingMs(expiresAt[partyIndex, statusIndex], now))})")); + } + + private static bool IsP4ChainDebugStatus(uint statusId) + { + return statusId >= 5543 && statusId <= 5548; + } + + private static bool IsP4LaterXStatus(uint statusId) + { + return statusId == 5543 || statusId == 5544 || statusId == 5545 || statusId == 5546; + } + + private static bool IsP4LaterCStatus(uint statusId) + { + return statusId == 5547 || statusId == 5548; + } + + private static bool TryGetP4BuffCategory(uint statusId, out P4BuffCategory category) + { + switch (statusId) + { + case 5544: + case 5545: + case 5546: + category = P4BuffCategory.ThunderShare; + return true; + case 5543: + category = P4BuffCategory.Petrify; + return true; + case 5547: + case 5548: + category = P4BuffCategory.Element; + return true; + default: + category = default; + return false; + } + } + + private static string P4BuffName(uint statusId) + { + switch (statusId) + { + case 5547: return "Fire"; + case 5548: return "Water"; + case 5545: return "WaterShare"; + case 5544: return "Thunder"; + case 5543: return "Petrify"; + case 5546: return "Accel"; + default: return statusId.ToString(CultureInfo.InvariantCulture); + } + } + + private static bool TryResolveP4LaterStateFromWindow(uint statusId, int xParam, long xExpiresAt, int cParam, long cExpiresAt, long now, out int state, out string windowName, out int usedParam, out int remainingMs) + { + state = P4StateUnknown; + windowName = "-"; + usedParam = 0; + remainingMs = 0; + + if (IsP4LaterXStatus(statusId)) + { + if (xParam == 0 || now > xExpiresAt) + return false; + + windowName = "X"; + usedParam = xParam; + remainingMs = RemainingMs(xExpiresAt, now); + return TryGetP4StateValueFromParam(xParam, out state); + } + + if (IsP4LaterCStatus(statusId)) + { + if (cParam == 0 || now > cExpiresAt) + return false; + + windowName = "C"; + usedParam = cParam; + remainingMs = RemainingMs(cExpiresAt, now); + return TryGetP4StateValueFromParam(cParam, out state); + } + + return false; + } + + private static string FormatP4StatusStateWithRemaining(int[,] states, long[,] expiresAt, int partyIndex, uint statusId, long now) + { + if (partyIndex < 0 + || partyIndex >= 8 + || !TryGetP4TrackedStatusIndex(statusId, out var statusIndex)) + return $"{statusId}=0(-)"; + + return $"{statusId}={states[partyIndex, statusIndex]}({FormatRemainingMs(RemainingMs(expiresAt[partyIndex, statusIndex], now))})"; + } + + private static string FormatP4StatusesWithRemaining(int[,] states, long[,] expiresAt, int partyIndex, long now, params uint[] statusIds) + { + return string.Join(" ", statusIds.Select(statusId => FormatP4StatusStateWithRemaining(states, expiresAt, partyIndex, statusId, now))); + } + + private static string FormatP4WindowDebug(string name, int param, int remainingMs, bool active) + { + if (param == 0) + return $"{name}-"; + + return $"{name}{param}({(active ? FormatRemainingMs(remainingMs) : "-")})"; + } + + private static string FormatP4UsedWindowDebug(string name, int param, int remainingMs) + { + if (string.IsNullOrWhiteSpace(name) || name == "-") + return "-"; + + if (name == "direct") + return "direct"; + + return $"{name}{param}({FormatRemainingMs(remainingMs)})"; + } + + private static bool IsTnPartyIndex(int partyIndex) + { + return partyIndex >= 0 && partyIndex <= 3; + } + + private static bool IsDpsPartyIndex(int partyIndex) + { + return partyIndex >= 4 && partyIndex <= 7; + } + + private static bool TryGetP4ActiveSoonState( + int[,] states, + long[,] expiresAt, + int partyIndex, + uint statusId, + long now, + out int state, + out int remainingMs) + { + state = P4StateUnknown; + remainingMs = 0; + + if (partyIndex < 0 || partyIndex >= 8) + return false; + + if (!TryGetP4TrackedStatusIndex(statusId, out var statusIndex)) + return false; + + state = states[partyIndex, statusIndex]; + remainingMs = RemainingMs(expiresAt[partyIndex, statusIndex], now); + return state != P4StateUnknown && remainingMs > 0 && remainingMs < 10000; + } + + private static bool HasP4StatusStateSoon( + int[,] states, + long[,] expiresAt, + int partyIndex, + uint statusId, + int expectedState, + long now, + out int remainingMs) + { + return TryGetP4ActiveSoonState(states, expiresAt, partyIndex, statusId, now, out var state, out remainingMs) + && state == expectedState; + } + + private static void NormalizeP4ExclusivePairByTruth(int[,] states, long[,] expiresAt, int partyIndex, int statusIndexA, int statusIndexB, long now) + { + if (partyIndex < 0 || partyIndex >= 8) + return; + + var stateA = RemainingMs(expiresAt[partyIndex, statusIndexA], now) > 0 + ? states[partyIndex, statusIndexA] + : P4StateUnknown; + var stateB = RemainingMs(expiresAt[partyIndex, statusIndexB], now) > 0 + ? states[partyIndex, statusIndexB] + : P4StateUnknown; + + if (stateA == P4StateTrue || stateB == P4StateTrue) + return; + + if (stateA == P4StateFalse) + { + var oldExpiresAt = expiresAt[partyIndex, statusIndexA]; + states[partyIndex, statusIndexA] = P4StateUnknown; + expiresAt[partyIndex, statusIndexA] = 0; + states[partyIndex, statusIndexB] = P4StateTrue; + if (expiresAt[partyIndex, statusIndexB] <= 0) + expiresAt[partyIndex, statusIndexB] = oldExpiresAt; + return; + } + + if (stateB == P4StateFalse) + { + var oldExpiresAt = expiresAt[partyIndex, statusIndexB]; + states[partyIndex, statusIndexB] = P4StateUnknown; + expiresAt[partyIndex, statusIndexB] = 0; + states[partyIndex, statusIndexA] = P4StateTrue; + if (expiresAt[partyIndex, statusIndexA] <= 0) + expiresAt[partyIndex, statusIndexA] = oldExpiresAt; + } + } + + private static P4ClockDirection ResolveP4MoveClock(int[,] states, long[,] expiresAt, int partyIndex, long now) + { + if (!IsTnPartyIndex(partyIndex) && !IsDpsPartyIndex(partyIndex)) + return P4ClockDirection.Unknown; + + var hasTrue5544 = HasP4StatusStateSoon(states, expiresAt, partyIndex, 5544, P4StateTrue, now, out _); + if (hasTrue5544) + return IsTnPartyIndex(partyIndex) ? P4ClockDirection.Three : P4ClockDirection.Nine; + + return IsTnPartyIndex(partyIndex) ? P4ClockDirection.Twelve : P4ClockDirection.Six; + } + + private static Vector3 P4ClockTarget(Vector3 newTwelveDirection, P4ClockDirection clock) + { + var rightVector = RightVectorFromNewTwelve(newTwelveDirection); + switch (clock) + { + case P4ClockDirection.Twelve: + return P4ArenaCenter + newTwelveDirection * P4ChainGuideOffset; + case P4ClockDirection.Three: + return P4ArenaCenter + rightVector * P4ChainGuideOffset; + case P4ClockDirection.Six: + return P4ArenaCenter - newTwelveDirection * P4ChainGuideOffset; + case P4ClockDirection.Nine: + return P4ArenaCenter - rightVector * P4ChainGuideOffset; + default: + return P4ArenaCenter; + } + } + + private static string FormatP4Clock(P4ClockDirection clock) + { + switch (clock) + { + case P4ClockDirection.Twelve: return "12"; + case P4ClockDirection.Three: return "3"; + case P4ClockDirection.Six: return "6"; + case P4ClockDirection.Nine: return "9"; + default: return "-"; + } + } + + private static string FormatP4BuffGroupKind(P4BuffGroupKind kind) + { + switch (kind) + { + case P4BuffGroupKind.A: return "A"; + case P4BuffGroupKind.B: return "B"; + case P4BuffGroupKind.C: return "C"; + case P4BuffGroupKind.D: return "D"; + case P4BuffGroupKind.E: return "E"; + case P4BuffGroupKind.F: return "F"; + default: return "-"; + } + } + + private static float ClockToRadians(float clock) + { + return clock / 6.0f * MathF.PI; + } + + private static Vector3 P4ClockPoint(Vector3 newTwelveDirection, float clock, float distance) + { + var rightVector = RightVectorFromNewTwelve(newTwelveDirection); + var radians = ClockToRadians(clock); + var direction = newTwelveDirection * MathF.Cos(radians) + rightVector * MathF.Sin(radians); + return P4ArenaCenter + direction * distance; + } + + private static P4ClockDirection RoleDefaultClock(bool isThunder, int partyIndex) + { + if (IsTnPartyIndex(partyIndex)) + return isThunder ? P4ClockDirection.Three : P4ClockDirection.Twelve; + + if (IsDpsPartyIndex(partyIndex)) + return isThunder ? P4ClockDirection.Nine : P4ClockDirection.Six; + + return P4ClockDirection.Unknown; + } + + private static float ClockDirectionToClock(P4ClockDirection clock) + { + switch (clock) + { + case P4ClockDirection.Twelve: return 0.0f; + case P4ClockDirection.Three: return 3.0f; + case P4ClockDirection.Six: return 6.0f; + case P4ClockDirection.Nine: return 9.0f; + default: return 0.0f; + } + } + + private static MarkType?[] BuildP4ChainMoveMarks(IReadOnlyList clocks) + { + var marks = new MarkType?[8]; + var markedThree = false; + var markedNine = false; + + for (var i = 0; i < marks.Length && i < clocks.Count; i++) + { + if (clocks[i] == P4ClockDirection.Three && !markedThree) + { + marks[i] = MarkType.Attack1; + markedThree = true; + } + else if (clocks[i] == P4ClockDirection.Nine && !markedNine) + { + marks[i] = MarkType.Attack2; + markedNine = true; + } + } + + return marks; + } + + private static MarkType?[] BuildP4PetrifyMarks(int[,] states, long[,] expiresAt, long now) + { + var marks = new MarkType?[8]; + var trueCount = 0; + var falseCount = 0; + + for (var i = 0; i < marks.Length; i++) + { + if (!TryGetP4ActiveSoonState(states, expiresAt, i, 5543, now, out var state, out _)) + continue; + + if (state == P4StateTrue && trueCount < P4PetrifyTrueMarks.Length) + { + marks[i] = P4PetrifyTrueMarks[trueCount]; + trueCount++; + } + else if (state == P4StateFalse && falseCount < P4PetrifyFalseMarks.Length) + { + marks[i] = P4PetrifyFalseMarks[falseCount]; + falseCount++; + } + } + + return marks; + } + + private bool ApplyP4CommandMarksNoTimer(ScriptAccessory accessory, IReadOnlyList marks) + { + if (!EnableP4LaterMarks) return false; + + ClearP4CommandMarksNow(accessory); + var token = Interlocked.Increment(ref _p4LaterMarkApplyToken); + var generation = _generation; + var markSnapshot = marks == null ? Array.Empty() : marks.Take(8).ToArray(); + + Task.Run(async () => + { + try + { + await Task.Delay(P4LaterMarkApplyDelayMs); + if (!EnableP4LaterMarks + || generation != _generation + || _phase != Phase.P4 + || token != Volatile.Read(ref _p4LaterMarkApplyToken)) + return; + + var totalCount = Math.Min(markSnapshot.Length, accessory.Data.PartyList.Count); + for (var i = 0; i < totalCount; i++) + { + if (markSnapshot[i].HasValue) + CommandMarkPartyMember(accessory, i, markSnapshot[i].Value); + } + } + catch (Exception ex) + { + DebugEcho(accessory, $"P4 later mark delayed apply fail {ex.Message}"); + } + }); + + return true; + } + + private static string P4MoveCallText(int state) + { + if (state == P4StateTrue) return "停停停"; + if (state == P4StateFalse) return "动动动"; + return null; + } + + private static string P4PetrifyCallText(int state, bool isSelf) + { + if (state == P4StateTrue) return isSelf ? "你是背对石化" : "背对石化"; + if (state == P4StateFalse) return isSelf ? "你是正对石化" : "正对石化"; + return null; + } + + private static bool TryResolveP4ElementCall( + int[,] states, + long[,] expiresAt, + long now, + out P4ElementCall call, + out int partyIndex, + out uint statusId, + out int state, + out int remainingMs) + { + call = P4ElementCall.Unknown; + partyIndex = -1; + statusId = 0; + state = P4StateUnknown; + remainingMs = 0; + + for (var i = 0; i < 8; i++) + { + if (TryGetP4ActiveSoonState(states, expiresAt, i, 5547, now, out state, out remainingMs)) + { + call = state == P4StateTrue ? P4ElementCall.FireSteel : P4ElementCall.WaterMoon; + partyIndex = i; + statusId = 5547; + return call != P4ElementCall.Unknown; + } + + if (TryGetP4ActiveSoonState(states, expiresAt, i, 5548, now, out state, out remainingMs)) + { + call = state == P4StateFalse ? P4ElementCall.FireSteel : P4ElementCall.WaterMoon; + partyIndex = i; + statusId = 5548; + return call != P4ElementCall.Unknown; + } + } + + return false; + } + + private static string P4ElementCallText(P4ElementCall call) + { + switch (call) + { + case P4ElementCall.FireSteel: return "火钢铁 中间集合稍后散开"; + case P4ElementCall.WaterMoon: return "水月环 中间集合稍后不动"; + default: return null; + } + } + + private static string FormatP4ElementCall(P4ElementCall call) + { + switch (call) + { + case P4ElementCall.FireSteel: return "火钢铁"; + case P4ElementCall.WaterMoon: return "水月环"; + default: return "-"; + } + } + + private bool TryStartP4Chain(out int round, out int stepGeneration) + { + lock (_p4Lock) + { + if (_p4ChainStep != P4ChainStep.None || _p4ChainRound > 0) + { + round = _p4ChainRound; + stepGeneration = _p4ChainStepGeneration; + return false; + } + + _p4ChainRound = 1; + _p4ChainStep = P4ChainStep.Move; + _p4ChainStepGeneration++; + round = _p4ChainRound; + stepGeneration = _p4ChainStepGeneration; + return true; + } + } + + private bool TryAdvanceP4ChainStep(P4ChainStep requiredStep, P4ChainStep nextStep, out int round, out int stepGeneration) + { + lock (_p4Lock) + { + if (_p4ChainRound <= 0 || _p4ChainStep != requiredStep) + { + round = _p4ChainRound; + stepGeneration = _p4ChainStepGeneration; + return false; + } + + _p4ChainStep = nextStep; + _p4ChainStepGeneration++; + round = _p4ChainRound; + stepGeneration = _p4ChainStepGeneration; + return true; + } + } + + private bool TryAdvanceP4ChainRound(out int round, out int stepGeneration) + { + lock (_p4Lock) + { + if (_p4ChainRound <= 0 || _p4ChainStep != P4ChainStep.Element) + { + round = _p4ChainRound; + stepGeneration = _p4ChainStepGeneration; + return false; + } + + _p4ChainRound++; + _p4ChainStep = P4ChainStep.Move; + _p4ChainStepGeneration++; + round = _p4ChainRound; + stepGeneration = _p4ChainStepGeneration; + return true; + } + } + + private void ScheduleP4DelayedStep( + ScriptAccessory accessory, + int generation, + int stepGeneration, + int round, + P4ChainStep step, + int delayMs, + Action action) + { + Task.Run(async () => + { + try + { + await Task.Delay(Math.Max(0, delayMs)); + + if (generation != _generation || _phase != Phase.P4) + return; + + lock (_p4Lock) + { + if (_p4ChainStepGeneration != stepGeneration + || _p4ChainRound != round + || _p4ChainStep != step) + return; + } + + action(); + } + catch (Exception ex) + { + DebugEcho(accessory, $"P4链式阶段{step}延迟执行失败:{ex.Message}"); + } + }); + } + + private void ScheduleP4SelfTtsAtRemaining3(ScriptAccessory accessory, int generation, int round, int partyIndex, int expectedState, int remainingMs, string text) + { + if (string.IsNullOrWhiteSpace(text) || remainingMs <= 0) + return; + + var delayMs = Math.Max(0, remainingMs - 3000); + Task.Run(async () => + { + try + { + await Task.Delay(delayMs); + + if (generation != _generation || _phase != Phase.P4) + return; + + lock (_p4Lock) + { + if (_p4ChainRound != round || _p4ChainStep == P4ChainStep.None) + return; + + if (partyIndex < 0 + || partyIndex >= 8 + || !TryGetP4TrackedStatusIndex(5546, out var statusIndex) + || _p4StatusStateByPartyAndStatus[partyIndex, statusIndex] != expectedState) + return; + + var currentRemaining = RemainingMs(_p4StatusExpiresAtByPartyAndStatus[partyIndex, statusIndex], NowMs()); + if (currentRemaining <= 0 || currentRemaining > 4500) + return; + } + + QTTS(text); + } + catch (Exception ex) + { + DebugEcho(accessory, $"P4 5546 TTS延迟执行失败:{ex.Message}"); + } + }); + } + + private void SendP4PartyCallouts(ScriptAccessory accessory, IReadOnlyList messages, int generation, int round) + { + if (PlanMessageMode != P4PlanMessageMode.Party || messages == null || messages.Count == 0) + return; + + Task.Run(async () => + { + try + { + for (var i = 0; i < messages.Count; i++) + { + if (generation != _generation || _phase != Phase.P4) + return; + + lock (_p4Lock) + { + if (_p4ChainRound != round || _p4ChainStep == P4ChainStep.None) + return; + } + + accessory.Method.SendChat($"/p {messages[i]}"); + if (i + 1 < messages.Count) + await Task.Delay(150); + } + } + catch (Exception ex) + { + DebugEcho(accessory, $"P4小队频道提示发送失败:{ex.Message}"); + } + }); + } + + private void ScheduleP4LaterGroupsIfReady(ScriptAccessory accessory) + { + var generation = _generation; + + lock (_p4Lock) + { + if (_p4BuffGroupsScheduled || _p4BuffGroupsScheduleQueued) + return; + + if (_p4GroupSnapshotAt <= 0 || (_p4GroupSnapshotParam != 1121 && _p4GroupSnapshotParam != 1122)) + return; + + if (BuildP4BuffGroupsLocked().Count < P4MaxScheduledGroupCount) + return; + + _p4BuffGroupsScheduleQueued = true; + } + + Task.Run(async () => + { + try + { + await Task.Delay(500); + FinalizeAndScheduleP4LaterGroups(accessory, generation); + } + catch (Exception ex) + { + DebugEcho(accessory, $"P4GROUP queue fail {ex.Message}"); + } + }); + } + + private void FinalizeAndScheduleP4LaterGroups(ScriptAccessory accessory, int generation) + { + var groups = new List(); + lock (_p4Lock) + { + if (generation != _generation || _phase != Phase.P4) + return; + + if (_p4BuffGroupsScheduled) + return; + + groups = BuildP4BuffGroupsLocked(); + if (groups.Count < P4MaxScheduledGroupCount) + { + _p4BuffGroupsScheduleQueued = false; + return; + } + + _p4BuffGroups.Clear(); + _p4BuffGroups.AddRange(groups); + _p4BuffGroupsScheduled = true; + _p4BuffGroupsScheduleQueued = false; + } + + DebugEcho(accessory, $"P4GROUP ready {string.Join(" ", groups.Select(FormatP4GroupReadyDebug))}"); + SendP4GroupPlanSummaries(accessory, groups, generation); + foreach (var group in groups) + ScheduleP4BuffGroup(accessory, generation, group); + } + + private List BuildP4BuffGroupsLocked() + { + if (_p4GroupSnapshotAt <= 0) + return new List(); + + var snapshotAt = _p4GroupSnapshotAt; + var sorted = _p4BuffRecords + .Where(r => r.ExpiresAt > snapshotAt && TryGetP4BuffCategory(r.StatusId, out _)) + .Select(r => new + { + Record = r, + SnapshotRemainingMs = (int)Math.Max(1L, r.ExpiresAt - snapshotAt), + }) + .OrderBy(x => x.SnapshotRemainingMs) + .ThenBy(x => x.Record.StatusId) + .ThenBy(x => x.Record.PartyIndex) + .ToList(); + + var groups = new List(); + foreach (var item in sorted) + { + var record = item.Record; + record.SnapshotRemainingMs = item.SnapshotRemainingMs; + + var group = groups.FirstOrDefault(g => + Math.Abs(g.SnapshotRemainingMs - item.SnapshotRemainingMs) <= P4BuffGroupToleranceMs); + if (group == null) + { + group = new P4BuffGroup + { + Category = TryGetP4BuffCategory(record.StatusId, out var category) ? category : P4BuffCategory.Mixed, + DurationMs = record.DurationMs, + SnapshotRemainingMs = item.SnapshotRemainingMs, + ExpiresAt = snapshotAt + item.SnapshotRemainingMs, + }; + groups.Add(group); + } + + group.Records.Add(record); + group.DurationMs = (int)Math.Round(group.Records.Average(r => r.DurationMs)); + group.SnapshotRemainingMs = (int)Math.Round(group.Records.Average(r => r.SnapshotRemainingMs)); + group.ExpiresAt = snapshotAt + group.SnapshotRemainingMs; + group.Category = ResolveP4BuffGroupCategory(group.Records); + } + + groups = groups + .OrderBy(g => g.SnapshotRemainingMs) + .Take(P4MaxScheduledGroupCount) + .ToList(); + for (var i = 0; i < groups.Count; i++) + groups[i].Kind = (P4BuffGroupKind)(i + 1); + + return groups; + } + + private static P4BuffCategory ResolveP4BuffGroupCategory(IReadOnlyList records) + { + var categories = records + .Select(r => TryGetP4BuffCategory(r.StatusId, out var category) ? category : P4BuffCategory.Mixed) + .Distinct() + .ToList(); + + return categories.Count == 1 ? categories[0] : P4BuffCategory.Mixed; + } + + private static string FormatP4GroupReadyDebug(P4BuffGroup group) + { + return $"{FormatP4BuffGroupKind(group.Kind)}:{group.Category}:{group.Records.Count} snap={FormatRemainingMs(group.SnapshotRemainingMs)} now={FormatRemainingMs(RemainingMs(group.ExpiresAt, NowMs()))} ids={FormatP4GroupStatusIds(group)} dur={FormatP4GroupDurations(group)}"; + } + + private static string FormatP4GroupStatusIds(P4BuffGroup group) + { + return string.Join( + "/", + group.Records + .Select(r => r.StatusId) + .Distinct() + .OrderBy(id => id) + .Select(P4BuffName)); + } + + private static string FormatP4GroupDurations(P4BuffGroup group) + { + var durations = group.Records + .Select(r => r.DurationMs) + .Where(ms => ms > 0) + .Distinct() + .OrderBy(ms => ms) + .Select(FormatRemainingMs) + .ToList(); + + return durations.Count > 0 ? string.Join("/", durations) : "-"; + } + + private void ScheduleP4BuffGroup(ScriptAccessory accessory, int generation, P4BuffGroup group) + { + var triggerRemainingMs = P4GroupTriggerRemainingMs(group.Kind); + if (triggerRemainingMs <= 0) + return; + + var delayMs = (int)Math.Max(0L, group.ExpiresAt - NowMs() - triggerRemainingMs); + Task.Run(async () => + { + try + { + await Task.Delay(delayMs); + if (generation != _generation || _phase != Phase.P4) + return; + + ExecuteP4BuffGroup(accessory, generation, group.Kind); + } + catch (Exception ex) + { + DebugEcho(accessory, $"P4GROUP {FormatP4BuffGroupKind(group.Kind)} delayed fail {ex.Message}"); + } + }); + } + + private static int P4GroupTriggerRemainingMs(P4BuffGroupKind kind) + { + switch (kind) + { + case P4BuffGroupKind.A: return 6500; + case P4BuffGroupKind.B: return 4900; + case P4BuffGroupKind.C: return 6000; + case P4BuffGroupKind.D: return 8500; + case P4BuffGroupKind.E: return 5500; + case P4BuffGroupKind.F: return 4900; + default: return 0; + } + } + + private void ExecuteP4BuffGroup(ScriptAccessory accessory, int generation, P4BuffGroupKind kind) + { + P4BuffGroup group = null; + Vector3 newTwelveDirection; + lock (_p4Lock) + { + group = _p4BuffGroups.FirstOrDefault(g => g.Kind == kind); + newTwelveDirection = DefaultNorth; + if (kind == P4BuffGroupKind.F) + _p4FGroupStartedAt = NowMs(); + } + + if (group == null) + { + DebugEcho(accessory, $"P4GROUP {FormatP4BuffGroupKind(kind)} missing"); + return; + } + + DebugEcho(accessory, $"P4GROUP {FormatP4BuffGroupKind(kind)} fire records={FormatP4GroupRecords(group)}"); + + switch (kind) + { + case P4BuffGroupKind.A: + ExecuteP4ThunderShareGroup(accessory, generation, group, newTwelveDirection, 8.0f, 4000, "P4_A", false, 0); + break; + case P4BuffGroupKind.B: + ExecuteP4PetrifyGroupB(accessory, group, newTwelveDirection); + break; + case P4BuffGroupKind.C: + ExecuteP4ElementGroup(accessory, group, newTwelveDirection, P4BuffGroupKind.C, 5500); + break; + case P4BuffGroupKind.D: + ExecuteP4GroupD(accessory, generation, group, newTwelveDirection); + break; + case P4BuffGroupKind.E: + ExecuteP4PetrifyGroupE(accessory, group, newTwelveDirection); + break; + case P4BuffGroupKind.F: + ExecuteP4ElementGroup(accessory, group, newTwelveDirection, P4BuffGroupKind.F, 4500); + TryDrawP4FinalSafe(accessory, generation); + break; + } + } + + private static string FormatP4GroupRecords(P4BuffGroup group) + { + return string.Join( + " ", + group.Records + .OrderBy(r => r.PartyIndex) + .ThenBy(r => r.StatusId) + .Select(r => $"{PartyPriorityLabel(r.PartyIndex)}:{P4BuffName(r.StatusId)}={r.State}/{FormatRemainingMs(RemainingMs(r.ExpiresAt, NowMs()))}")); + } + + private bool TryGetP4ThunderState(IReadOnlyList records, int partyIndex, out bool isThunder) + { + isThunder = false; + + var thunder = records.FirstOrDefault(r => r.PartyIndex == partyIndex && r.StatusId == 5544); + if (thunder != null) + { + if (thunder.State == P4StateTrue) + { + isThunder = true; + return true; + } + + if (thunder.State == P4StateFalse) + { + isThunder = false; + return true; + } + } + + var waterShare = records.FirstOrDefault(r => r.PartyIndex == partyIndex && r.StatusId == 5545); + if (waterShare != null) + { + if (waterShare.State == P4StateTrue) + { + isThunder = false; + return true; + } + + if (waterShare.State == P4StateFalse) + { + isThunder = true; + return true; + } + } + + return false; + } + + private bool TryGetP4AccelCall(IReadOnlyList records, int partyIndex, out string call) + { + call = null; + var accel = records.FirstOrDefault(r => r.PartyIndex == partyIndex && r.StatusId == 5546); + if (accel == null) + return false; + + call = P4MoveCallText(accel.State); + return !string.IsNullOrWhiteSpace(call); + } + + private void SendP4GroupPlanSummaries(ScriptAccessory accessory, IReadOnlyList groups, int generation) + { + if (groups == null || PlanMessageMode == P4PlanMessageMode.Off) + return; + + var messages = new List(); + AddP4RoundPlanSummary( + accessory, + messages, + "\u7b2c\u4e00\u8f6e", + groups.FirstOrDefault(g => g.Kind == P4BuffGroupKind.A), + groups.FirstOrDefault(g => g.Kind == P4BuffGroupKind.B), + groups.FirstOrDefault(g => g.Kind == P4BuffGroupKind.C)); + messages.Add("---------------"); + AddP4RoundPlanSummary( + accessory, + messages, + "\u7b2c\u4e8c\u8f6e", + groups.FirstOrDefault(g => g.Kind == P4BuffGroupKind.D), + groups.FirstOrDefault(g => g.Kind == P4BuffGroupKind.E), + groups.FirstOrDefault(g => g.Kind == P4BuffGroupKind.F)); + SendP4PlanMessages(accessory, messages, generation); + } + + private void AddP4RoundPlanSummary( + ScriptAccessory accessory, + List messages, + string roundLabel, + P4BuffGroup thunderGroup, + P4BuffGroup petrifyGroup, + P4BuffGroup elementGroup) + { + if (messages == null) + return; + + AddP4ThunderSharePlanSummary(accessory, messages, thunderGroup, roundLabel); + AddP4PetrifyPlanSummary(accessory, messages, petrifyGroup, roundLabel); + AddP4ElementPlanSummary(messages, elementGroup, roundLabel); + } + + private void AddP4ThunderSharePlanSummary(ScriptAccessory accessory, List messages, P4BuffGroup group, string roundLabel) + { + if (messages == null || group == null) + return; + + var accelParts = new List(); + var accelStop = new List(); + var accelMove = new List(); + var thunder = new List(); + var records = group.Records; + + for (var i = 0; i < 8; i++) + { + var accel = records.FirstOrDefault(r => r.PartyIndex == i && r.StatusId == 5546); + if (accel != null) + { + if (accel.State == P4StateTrue) + accelStop.Add(PartyMemberJobName(accessory, i)); + else if (accel.State == P4StateFalse) + accelMove.Add(PartyMemberJobName(accessory, i)); + } + + if (TryGetP4ThunderState(records, i, out var isThunder) && isThunder) + thunder.Add(PartyMemberJobName(accessory, i)); + } + + if (accelMove.Count > 0) + accelParts.Add($"{string.Join("\uff0c", accelMove)} \u52a8\u52a8\u52a8"); + if (accelStop.Count > 0) + accelParts.Add($"{string.Join("\uff0c", accelStop)} \u505c\u505c\u505c"); + + if (accelParts.Count > 0) + messages.Add($"{roundLabel}\uff1a{string.Join(" ", accelParts)}"); + if (thunder.Count > 0) + messages.Add($"{roundLabel}\uff1a{string.Join(" ", thunder)} \u96f7\u5206\u6563"); + } + + private void AddP4PetrifyPlanSummary(ScriptAccessory accessory, List messages, P4BuffGroup group, string roundLabel) + { + if (messages == null || group == null) + return; + + var faceOut = new List(); + var faceEachOther = new List(); + foreach (var record in group.Records.Where(r => r.StatusId == 5543).OrderBy(r => r.PartyIndex)) + { + if (record.State == P4StateTrue) + faceOut.Add(PartyMemberJobName(accessory, record.PartyIndex)); + else if (record.State == P4StateFalse) + faceEachOther.Add(PartyMemberJobName(accessory, record.PartyIndex)); + } + + if (faceOut.Count > 0) + messages.Add($"{roundLabel}\uff1a{string.Join(" ", faceOut)} \u80cc\u5bf9"); + if (faceEachOther.Count > 0) + messages.Add($"{roundLabel}\uff1a{string.Join(" ", faceEachOther)} \u6b63\u5bf9"); + } + + private void AddP4ElementPlanSummary(List messages, P4BuffGroup group, string roundLabel) + { + if (messages == null || group == null) + return; + + var call = ResolveP4ElementCallFromRecords(group.Records); + if (call == P4ElementCall.FireSteel) + messages.Add($"{roundLabel}\uff1a\u5168\u4f53\u94a2\u94c1"); + else if (call == P4ElementCall.WaterMoon) + messages.Add($"{roundLabel}\uff1a\u5168\u4f53\u6708\u73af"); + } + + private void SendP4PlanMessages(ScriptAccessory accessory, IReadOnlyList messages, int generation) + { + if (messages == null || messages.Count == 0) + return; + + switch (PlanMessageMode) + { + case P4PlanMessageMode.Party: + SendP4PartyMessages(accessory, messages, generation); + break; + case P4PlanMessageMode.Echo: + SendP4EchoMessages(accessory, messages, generation); + break; + } + } + + private void SendP4PartyMessages(ScriptAccessory accessory, IReadOnlyList messages, int generation) + { + if (messages == null || messages.Count == 0) + return; + + Task.Run(async () => + { + for (var i = 0; i < messages.Count; i++) + { + if (generation != _generation || _phase != Phase.P4) + return; + + accessory.Method.SendChat($"/p {messages[i]}"); + if (i + 1 < messages.Count) + await Task.Delay(150); + } + }); + } + + private void SendP4EchoMessages(ScriptAccessory accessory, IReadOnlyList messages, int generation) + { + if (messages == null || messages.Count == 0) + return; + + Task.Run(async () => + { + for (var i = 0; i < messages.Count; i++) + { + if (generation != _generation || _phase != Phase.P4) + return; + + accessory.Method.SendChat($"/e {messages[i]}"); + if (i + 1 < messages.Count) + await Task.Delay(150); + } + }); + } + + private void ExecuteP4ThunderShareGroup( + ScriptAccessory accessory, + int generation, + P4BuffGroup group, + Vector3 newTwelveDirection, + float distance, + int duration, + string drawName, + bool sendAccelParty, + int delay) + { + var records = group.Records; + var targets = new Vector3[8]; + var marks = new MarkType?[8]; + var thunderPlayers = new List(); + var partyMessages = new List(); + + for (var i = 0; i < 8; i++) + { + var hasThunderState = TryGetP4ThunderState(records, i, out var isThunder); + if (isThunder) + { + thunderPlayers.Add(i); + partyMessages.Add($"{PartyPriorityLabel(i)}{PartyMemberJobName(accessory, i)}\u96f7\u5206\u6563"); + } + + var clock = RoleDefaultClock(isThunder, i); + targets[i] = P4ClockPoint(newTwelveDirection, ClockDirectionToClock(clock), distance); + + if (TryGetP4AccelCall(records, i, out var accelCall)) + partyMessages.Add($"{PartyPriorityLabel(i)}{PartyMemberJobName(accessory, i)}{accelCall}"); + + DebugEcho(accessory, $"P4{FormatP4BuffGroupKind(group.Kind)} MOVE {PartyPriorityLabel(i)} thunder={(hasThunderState ? isThunder.ToString() : "-")} accel={(TryGetP4AccelCall(records, i, out var c) ? c : "-")} clock={FormatP4Clock(clock)} target={FormatPosition(targets[i])}"); + } + + for (var i = 0; i < thunderPlayers.Count && i < 2; i++) + marks[thunderPlayers[i]] = i == 0 ? MarkType.Attack1 : MarkType.Attack2; + + if (ApplyP4CommandMarksNoTimer(accessory, marks)) + ScheduleP4CommandMarkClear(accessory, generation, P4LaterMarkApplyDelayMs + 4000); + + if (sendAccelParty) + SendP4PartyCalloutsSimple(accessory, partyMessages, generation); + + var myIndex = GetMyIndex(accessory); + if (myIndex >= 0 && myIndex < 8) + { + var hasThunderState = TryGetP4ThunderState(records, myIndex, out var isThunder); + var parts = new List { isThunder ? "钢铁" : "分摊" }; + if (TryGetP4AccelCall(records, myIndex, out var accelCall)) + parts.Add(accelCall); + Alert(string.Join(" ", parts.Where(p => !string.IsNullOrWhiteSpace(p)))); + + if (hasThunderState || IsTnPartyIndex(myIndex) || IsDpsPartyIndex(myIndex)) + DrawP4SelfGuide(accessory, $"{DrawPrefix}_{drawName}_Guide", $"{DrawPrefix}_{drawName}_Target", targets[myIndex], duration, delay); + } + } + + private void SendP4PartyCalloutsSimple(ScriptAccessory accessory, IReadOnlyList messages, int generation) + { + if (PlanMessageMode != P4PlanMessageMode.Party || messages == null || messages.Count == 0) + return; + + Task.Run(async () => + { + for (var i = 0; i < messages.Count; i++) + { + if (generation != _generation || _phase != Phase.P4) + return; + + accessory.Method.SendChat($"/p {messages[i]}"); + if (i + 1 < messages.Count) + await Task.Delay(150); + } + }); + } + + private void DrawP4SelfGuide(ScriptAccessory accessory, string guideName, string targetName, Vector3 target, int duration, int delay = 0) + { + DrawGuide(accessory, guideName, target, duration, delay); + DrawStaticCircle(accessory, targetName, target, 0.25f, duration, GuideColor.V4); + if (delay <= 0) + GreenMoveToPoint(target, accessory, guideName); + } + + private void ExecuteP4PetrifyGroupB(ScriptAccessory accessory, P4BuffGroup group, Vector3 newTwelveDirection) + { + var safeClock = FindSafeNorthSouthClock(newTwelveDirection); + ExecuteP4PetrifyGroup(accessory, group, newTwelveDirection, safeClock, 4000, "P4_B"); + } + + private void ExecuteP4PetrifyGroupE(ScriptAccessory accessory, P4BuffGroup group, Vector3 newTwelveDirection) + { + ExecuteP4PetrifyGroup(accessory, group, newTwelveDirection, 6.0f, 5000, "P4_E"); + } + + private void ExecuteP4PetrifyGroup(ScriptAccessory accessory, P4BuffGroup group, Vector3 newTwelveDirection, float safeClock, int duration, string drawName) + { + var petrifies = group.Records + .Where(r => r.StatusId == 5543) + .OrderBy(r => r.PartyIndex) + .ToList(); + var petrifySet = new HashSet(petrifies.Select(r => r.PartyIndex)); + var targets = new Vector3[8]; + var marks = new MarkType?[8]; + + for (var i = 0; i < petrifies.Count && i < 2; i++) + { + var record = petrifies[i]; + var distance = record.State == P4StateTrue ? 8.0f : record.State == P4StateFalse ? 3.0f : 6.0f; + targets[record.PartyIndex] = P4ClockPoint(newTwelveDirection, safeClock, distance); + marks[record.PartyIndex] = record.State == P4StateTrue + ? (i == 0 ? MarkType.Stop1 : MarkType.Stop2) + : (i == 0 ? MarkType.Bind1 : MarkType.Bind2); + } + + var others = Enumerable.Range(0, 8) + .Where(i => !petrifySet.Contains(i)) + .OrderBy(i => i) + .ToList(); + for (var i = 0; i < others.Count; i++) + { + targets[others[i]] = P4ClockPoint(newTwelveDirection, safeClock, 6.0f); + } + + if (ApplyP4CommandMarksNoTimer(accessory, marks)) + ScheduleP4CommandMarkClear(accessory, _generation, P4LaterMarkApplyDelayMs + 4000); + + var myIndex = GetMyIndex(accessory); + var myPetrify = petrifies.FirstOrDefault(r => r.PartyIndex == myIndex); + var oldPetrifyAlertEnabled = false; + if (myPetrify != null) + Alert(myPetrify.State == P4StateTrue ? "\u9762\u5411boss" : "\u4e92\u76f8\u9762\u5411"); + if (oldPetrifyAlertEnabled && myPetrify != null) + Alert(myPetrify.State == P4StateTrue ? "出去背对" : "出去正对"); + else if (oldPetrifyAlertEnabled && petrifies.Count > 0) + Alert(petrifies[0].State == P4StateTrue ? "背对" : "正对"); + + if (myIndex >= 0 && myIndex < 8) + DrawP4SelfGuide(accessory, $"{DrawPrefix}_{drawName}_Guide", $"{DrawPrefix}_{drawName}_Target", targets[myIndex], duration); + + DebugEcho(accessory, $"P4{FormatP4BuffGroupKind(group.Kind)} PETRI safe={safeClock:F1} petrify={string.Join(",", petrifies.Select(r => $"{PartyPriorityLabel(r.PartyIndex)}:{r.State}/{(r.State == P4StateTrue ? "8m" : r.State == P4StateFalse ? "3m" : "6m")}"))} idle=6m"); + } + + private void ExecuteP4ElementGroup(ScriptAccessory accessory, P4BuffGroup group, Vector3 newTwelveDirection, P4BuffGroupKind kind, int duration) + { + var call = ResolveP4ElementCallFromRecords(group.Records); + if (kind == P4BuffGroupKind.C) + _p4CElementCall = call; + if (kind == P4BuffGroupKind.F) + _p4FElementCall = call; + + var text = call == P4ElementCall.FireSteel ? "场中集合钢铁" : call == P4ElementCall.WaterMoon ? "场中集合月环" : null; + if (!string.IsNullOrWhiteSpace(text)) + QTTS(text); + + DrawP4SelfGuide(accessory, $"{DrawPrefix}_P4_{FormatP4BuffGroupKind(kind)}_Center_Guide", $"{DrawPrefix}_P4_{FormatP4BuffGroupKind(kind)}_Center_Target", P4ArenaCenter, duration); + DebugEcho(accessory, $"P4{FormatP4BuffGroupKind(kind)} ELEM result={FormatP4ElementCall(call)} records={FormatP4GroupRecords(group)}"); + } + + private P4ElementCall ResolveP4ElementCallFromRecords(IReadOnlyList records) + { + var record = records + .Where(r => r.StatusId == 5547 || r.StatusId == 5548) + .OrderBy(r => r.PartyIndex) + .ThenBy(r => r.StatusId) + .FirstOrDefault(); + if (record == null) + return P4ElementCall.Unknown; + + if (record.StatusId == 5547) + return record.State == P4StateTrue ? P4ElementCall.FireSteel : P4ElementCall.WaterMoon; + + return record.State == P4StateFalse ? P4ElementCall.FireSteel : P4ElementCall.WaterMoon; + } + + private void ExecuteP4GroupD(ScriptAccessory accessory, int generation, P4BuffGroup group, Vector3 newTwelveDirection) + { + var startedAt = NowMs(); + var firstDistance = _p4CElementCall == P4ElementCall.WaterMoon ? 3.0f : 8.0f; + ExecuteP4ThunderShareGroup(accessory, generation, group, newTwelveDirection, firstDistance, 5000, "P4_D_First", false, 0); + + Task.Run(async () => + { + await Task.Delay(5000); + + var deadline = NowMs() + 1500; + while (NowMs() < deadline) + { + if (generation != _generation || _phase != Phase.P4) + return; + + if (CountP4Dangers(P4DangerShape.Fan, startedAt) >= 2) + break; + + await Task.Delay(100); + } + + if (generation != _generation || _phase != Phase.P4) + return; + + DrawP4GroupDSafe(accessory, group, newTwelveDirection, startedAt); + }); + } + + private void DrawP4GroupDSafe(ScriptAccessory accessory, P4BuffGroup group, Vector3 newTwelveDirection, long minCapturedAt) + { + var safeByRoleClock = FindSafeHalfClockByQuadrant(newTwelveDirection, minCapturedAt); + var myIndex = GetMyIndex(accessory); + if (myIndex < 0 || myIndex >= 8) + return; + + TryGetP4ThunderState(group.Records, myIndex, out var isThunder); + var baseClock = ClockDirectionToClock(RoleDefaultClock(isThunder, myIndex)); + var targetClock = safeByRoleClock.ContainsKey(baseClock) ? safeByRoleClock[baseClock] : baseClock; + var target = P4ClockPoint(newTwelveDirection, targetClock, 8.0f); + DrawP4SelfGuide(accessory, $"{DrawPrefix}_P4_D_Safe_Guide", $"{DrawPrefix}_P4_D_Safe_Target", target, 3000); + DebugEcho(accessory, $"P4D SAFE my={PartyPriorityLabel(myIndex)} base={baseClock:F1} safe={targetClock:F1} target={FormatPosition(target)}"); + } + + private int CountP4Dangers(P4DangerShape shape, long minCapturedAt) + { + lock (_p4Lock) + { + return _p4DangerRecords.Count(r => r.Shape == shape && r.CapturedAt >= minCapturedAt); + } + } + + private List GetP4RecentDangers(P4DangerShape shape, int count, int lookbackMs, long minCapturedAt = 0) + { + var now = NowMs(); + lock (_p4Lock) + { + return _p4DangerRecords + .Where(r => r.Shape == shape + && r.CapturedAt >= minCapturedAt + && now - r.CapturedAt <= lookbackMs) + .OrderByDescending(r => r.CapturedAt) + .Take(count) + .OrderBy(r => r.CapturedAt) + .ToList(); + } + } + + private float FindSafeNorthSouthClock(Vector3 newTwelveDirection) + { + var dangers = GetP4RecentDangers(P4DangerShape.Rect, 2, P4BRectLookbackMs); + var candidates = new[] { 0.0f, 6.0f }; + var safe = candidates + .Where(clock => + IsPointSafeForDangers(P4ClockPoint(newTwelveDirection, clock, 3.0f), dangers) + && IsPointSafeForDangers(P4ClockPoint(newTwelveDirection, clock, 6.0f), dangers) + && IsPointSafeForDangers(P4ClockPoint(newTwelveDirection, clock, 8.0f), dangers)) + .ToList(); + + if (safe.Count <= 0) + { + if (_acc != null) + DebugEcho(_acc, $"P4B SAFE fallback rects={dangers.Count} safe={string.Join(",", safe.Select(c => c.ToString("F1", CultureInfo.InvariantCulture)))}"); + + return 0.0f; + } + + return safe[0]; + } + + private Dictionary FindSafeHalfClockByQuadrant(Vector3 newTwelveDirection, long minCapturedAt) + { + var dangers = GetP4RecentDangers(P4DangerShape.Fan, 2, P4DFanLookbackMs, minCapturedAt); + var result = new Dictionary(); + var candidates = new Dictionary + { + { 0.0f, new[] { 11.5f, 12.5f } }, + { 3.0f, new[] { 2.5f, 3.5f } }, + { 6.0f, new[] { 5.5f, 6.5f } }, + { 9.0f, new[] { 8.5f, 9.5f } }, + }; + + foreach (var pair in candidates) + { + var safeClock = pair.Value.FirstOrDefault(clock => + IsPointSafeForDangers(P4ClockPoint(newTwelveDirection, clock, 8.0f), dangers)); + result[pair.Key] = safeClock == 0.0f && pair.Key != 0.0f ? pair.Key : safeClock; + } + + return result; + } + + private bool TryDrawP4FinalSafe(ScriptAccessory accessory, int generation) + { + long startedAt; + Vector3 newTwelveDirection; + P4ElementCall call; + List dangers; + + lock (_p4Lock) + { + if (_p4FGroupDangerDrawn || _p4FGroupStartedAt <= 0) + return false; + + startedAt = _p4FGroupStartedAt; + newTwelveDirection = DefaultNorth; + call = _p4FElementCall; + var rects = _p4DangerRecords + .Where(r => r.Shape == P4DangerShape.Rect && r.CapturedAt >= startedAt) + .OrderBy(r => r.CapturedAt) + .Take(2) + .ToList(); + var fans = _p4DangerRecords + .Where(r => r.Shape == P4DangerShape.Fan && r.CapturedAt >= startedAt) + .OrderBy(r => r.CapturedAt) + .Take(2) + .ToList(); + + if (rects.Count < 2 || fans.Count < 2) + return false; + + dangers = rects.Concat(fans).ToList(); + } + + var finalQuadrants = new[] + { + new[] { 11.5f, 12.5f }, + new[] { 2.5f, 3.5f }, + new[] { 5.5f, 6.5f }, + new[] { 8.5f, 9.5f }, + }; + var safeClocks = new List(); + foreach (var quadrant in finalQuadrants) + { + var safeClock = quadrant.FirstOrDefault(clock => + IsPointSafeForDangers(P4ClockPoint(newTwelveDirection, clock, 3.0f), dangers) + && IsPointSafeForDangers(P4ClockPoint(newTwelveDirection, clock, 8.0f), dangers)); + if (safeClock > 0.0f) + safeClocks.Add(safeClock); + if (safeClocks.Count >= 2) + break; + } + + if (safeClocks.Count < 2) + { + DebugEcho(accessory, $"P4F FINAL no-safe dangers={FormatP4Dangers(dangers)} safe={string.Join(",", safeClocks.Select(c => c.ToString("F1", CultureInfo.InvariantCulture)))}"); + return false; + } + + lock (_p4Lock) + { + if (_p4FGroupDangerDrawn || generation != _generation || _phase != Phase.P4) + return false; + + _p4FGroupDangerDrawn = true; + } + + var myIndex = GetMyIndex(accessory); + var distance = call == P4ElementCall.WaterMoon ? 3.0f : 8.0f; + if (myIndex >= 0 && myIndex < 8) + { + var targetClock = myIndex < 4 ? safeClocks[0] : safeClocks[1]; + var target = P4ClockPoint(newTwelveDirection, targetClock, distance); + DrawP4SelfGuide(accessory, $"{DrawPrefix}_P4_F_Final_Guide", $"{DrawPrefix}_P4_F_Final_Target", target, 5000); + DebugEcho(accessory, $"P4F FINAL my={PartyPriorityLabel(myIndex)} clock={targetClock:F1} dist={distance:F1} target={FormatPosition(target)} call={FormatP4ElementCall(call)} dangers={FormatP4Dangers(dangers)}"); + } + + return true; + } + + private static bool IsPointSafeForDangers(Vector3 point, IReadOnlyList dangers) + { + return dangers == null || dangers.All(danger => !IsPointInP4Danger(point, danger)); + } + + private static bool IsPointInP4Danger(Vector3 point, P4DangerRecord danger) + { + return danger.Shape == P4DangerShape.Rect + ? IsPointInP4RectDanger(point, danger) + : IsPointInP4FanDanger(point, danger); + } + + private static bool IsPointInP4RectDanger(Vector3 point, P4DangerRecord danger) + { + var forward = new Vector3(MathF.Sin(danger.Rotation), 0.0f, MathF.Cos(danger.Rotation)); + var right = new Vector3(forward.Z, 0.0f, -forward.X); + var delta = new Vector3(point.X - danger.Position.X, 0.0f, point.Z - danger.Position.Z); + var along = delta.X * forward.X + delta.Z * forward.Z; + var side = delta.X * right.X + delta.Z * right.Z; + + return along >= -0.25f + && along <= P4RectDangerLength + 0.25f + && MathF.Abs(side) <= P4RectDangerWidth / 2.0f + 0.25f; + } + + private static bool IsPointInP4FanDanger(Vector3 point, P4DangerRecord danger) + { + var delta = new Vector3(point.X - danger.Position.X, 0.0f, point.Z - danger.Position.Z); + var distance = MathF.Sqrt(delta.X * delta.X + delta.Z * delta.Z); + if (distance > P4FanDangerRadius + 0.25f) + return false; + + if (distance < 0.001f) + return true; + + var forward = new Vector3(MathF.Sin(danger.Rotation), 0.0f, MathF.Cos(danger.Rotation)); + var dot = (delta.X * forward.X + delta.Z * forward.Z) / distance; + var limit = MathF.Cos(P4FanDangerRadian / 2.0f + 0.02f); + return dot >= limit; + } + + private static string FormatP4Dangers(IReadOnlyList dangers) + { + if (dangers == null || dangers.Count == 0) + return "-"; + + return string.Join( + ",", + dangers.Select(d => $"{d.ActionId}/{d.Shape}@{FormatPosition(d.Position)}/{d.Rotation:F2}")); + } + + private static bool TryConvertToUInt(object value, out uint result) + { + result = 0; + if (value == null) + return false; + + switch (value) + { + case byte v: + result = v; + return true; + case ushort v: + result = v; + return true; + case uint v: + result = v; + return true; + case int v when v >= 0: + result = (uint)v; + return true; + case long v when v >= 0 && v <= uint.MaxValue: + result = (uint)v; + return true; + case string s: + return TryParseUInt(s, out result); + } + + try + { + if (value is IConvertible convertible) + { + result = convertible.ToUInt32(CultureInfo.InvariantCulture); + return true; + } + } + catch + { + result = 0; + } + + return false; + } + + private static object ReadPropertyValue(object value, string propertyName) + { + if (value == null) + return null; + + var property = value.GetType().GetProperty(propertyName, BindingFlags.Instance | BindingFlags.Public); + return property?.GetValue(value); + } + + private static bool TryReadUIntProperty(object value, string propertyName, out uint result) + { + result = 0; + var propertyValue = ReadPropertyValue(value, propertyName); + return TryConvertToUInt(propertyValue, out result); + } + + private static bool TryExtractClassJobRowId(object value, out uint rowId) + { + rowId = 0; + if (value == null) + return false; + + if (TryConvertToUInt(value, out rowId)) + return true; + + if (TryReadUIntProperty(value, "RowId", out rowId) + || TryReadUIntProperty(value, "Id", out rowId) + || TryReadUIntProperty(value, "ID", out rowId)) + return true; + + var nestedValue = ReadPropertyValue(value, "Value"); + return nestedValue != null + && !ReferenceEquals(nestedValue, value) + && TryExtractClassJobRowId(nestedValue, out rowId); + } + + private static bool TryGetPartyMemberClassJobRowId(ScriptAccessory accessory, int partyIndex, out uint rowId) + { + rowId = 0; + if (partyIndex < 0 || partyIndex >= accessory.Data.PartyList.Count) + return false; + + var obj = accessory.Data.Objects.SearchById(accessory.Data.PartyList[partyIndex]); + if (obj == null) + return false; + + if (TryReadUIntProperty(obj, "ClassJobId", out rowId) + || TryReadUIntProperty(obj, "ClassJobID", out rowId)) + return true; + + return TryExtractClassJobRowId(ReadPropertyValue(obj, "ClassJob"), out rowId); + } + + private static string PartyMemberJobName(ScriptAccessory accessory, int partyIndex) + { + return TryGetPartyMemberClassJobRowId(accessory, partyIndex, out var rowId) + ? JobShortName(rowId) + : "未知职业"; + } + + private static string JobShortName(uint classJobRowId) + { + switch (classJobRowId) + { + case 1: return "剑术"; + case 2: return "格斗"; + case 3: return "斧术"; + case 4: return "枪术"; + case 5: return "弓术"; + case 6: return "幻术"; + case 7: return "咒术"; + case 19: return "骑士"; + case 20: return "武僧"; + case 21: return "战士"; + case 22: return "龙骑"; + case 23: return "诗人"; + case 24: return "白魔"; + case 25: return "黑魔"; + case 26: return "秘术"; + case 27: return "召唤"; + case 28: return "学者"; + case 29: return "双剑"; + case 30: return "忍者"; + case 31: return "机工"; + case 32: return "暗骑"; + case 33: return "占星"; + case 34: return "武士"; + case 35: return "赤魔"; + case 36: return "青魔"; + case 37: return "绝枪"; + case 38: return "舞者"; + case 39: return "钐镰"; + case 40: return "贤者"; + case 41: return "蝰蛇"; + case 42: return "绘灵"; + default: return "未知"; + } + } + + private static string JobChineseName(uint classJobRowId) + { + switch (classJobRowId) + { + case 1: return "剑术师"; + case 2: return "格斗家"; + case 3: return "斧术师"; + case 4: return "枪术师"; + case 5: return "弓箭手"; + case 6: return "幻术师"; + case 7: return "咒术师"; + case 19: return "骑士"; + case 20: return "武僧"; + case 21: return "战士"; + case 22: return "龙骑士"; + case 23: return "吟游诗人"; + case 24: return "白魔法师"; + case 25: return "黑魔法师"; + case 26: return "秘术师"; + case 27: return "召唤师"; + case 28: return "学者"; + case 29: return "双剑师"; + case 30: return "忍者"; + case 31: return "机工士"; + case 32: return "暗黑骑士"; + case 33: return "占星术士"; + case 34: return "武士"; + case 35: return "赤魔法师"; + case 36: return "青魔法师"; + case 37: return "绝枪战士"; + case 38: return "舞者"; + case 39: return "钐镰客"; + case 40: return "贤者"; + case 41: return "蝰蛇剑士"; + case 42: return "绘灵法师"; + default: return "未知职业"; + } + } + + private MarkType?[] BuildP4CommandMarks(IReadOnlyList sides) + { + var marks = new MarkType?[8]; + var leftCount = 0; + var rightCount = 0; + + for (var i = 0; i < marks.Length && i < sides.Count; i++) + { + if (sides[i] == P4HalfSide.Left && leftCount < P4LeftMarks.Length) + { + marks[i] = P4LeftMarks[leftCount]; + leftCount++; + } + else if (sides[i] == P4HalfSide.Right && rightCount < P4RightMarks.Length) + { + marks[i] = P4RightMarks[rightCount]; + rightCount++; + } + } + + return marks; + } + + private void ApplyP4CommandMarks(ScriptAccessory accessory, IReadOnlyList marks) + { + if (!EnableP4HotpotMarks) return; + + CommandMarkClear(accessory); + + var totalCount = Math.Min(marks.Count, accessory.Data.PartyList.Count); + for (var i = 0; i < totalCount; i++) + { + if (marks[i].HasValue) + CommandMarkPartyMember(accessory, i, marks[i].Value); + } + + ScheduleP4CommandMarkClear(accessory, _generation, P4GuideDurationMs); + } + + private void DebugP4Resolve( + ScriptAccessory accessory, + int[,] states, + IReadOnlyList sides, + IReadOnlyList colors, + IReadOnlyList marks, + int fourthParam, + P4HalfSide sourceSide, + P4HalfColor leftColor, + P4HalfColor rightColor, + Vector3 newTwelveDirection, + Vector3 l1, + Vector3 r1) + { + if (!DeveloperMode) return; + + DebugEcho(accessory, $"P4一运结算 fourthParam={fourthParam} sourceSide={FormatP4Side(sourceSide)} left={FormatP4Color(leftColor)} right={FormatP4Color(rightColor)} new12={FormatPosition(newTwelveDirection)} L1={FormatPosition(l1)} R1={FormatPosition(r1)}"); + + for (var i = 0; i < 8; i++) + { + var objectId = i < accessory.Data.PartyList.Count ? accessory.Data.PartyList[i] : 0; + DebugEcho( + accessory, + $"{PartyPriorityLabel(i)} {FormatObjectId(objectId)} => color={FormatP4Color(colors[i])} side={FormatP4Side(sides[i])} mark={FormatP4Mark(marks[i])}"); + } + } + + private void ExecuteP4ChainMove(ScriptAccessory accessory, int generation, int round) + { + var states = new int[8, 10]; + var expiresAt = new long[8, 10]; + Vector3 newTwelveDirection; + bool directionReady; + var now = NowMs(); + + lock (_p4Lock) + { + for (var i = 0; i < 8; i++) + NormalizeP4ExclusivePairByTruth(_p4StatusStateByPartyAndStatus, _p4StatusExpiresAtByPartyAndStatus, i, 3, 4, now); + + Array.Copy(_p4StatusStateByPartyAndStatus, states, _p4StatusStateByPartyAndStatus.Length); + Array.Copy(_p4StatusExpiresAtByPartyAndStatus, expiresAt, _p4StatusExpiresAtByPartyAndStatus.Length); + newTwelveDirection = _p4FourthDirectionReady ? _p4FourthNewTwelveDirection : DefaultNorth; + directionReady = _p4FourthDirectionReady; + } + + var clocks = new P4ClockDirection[8]; + var targets = new Vector3[8]; + var moveCalls = new string[8]; + var moveCallStates = new int[8]; + var moveCallRemaining = new int[8]; + var partyMessages = new List(); + + for (var i = 0; i < 8; i++) + { + clocks[i] = ResolveP4MoveClock(states, expiresAt, i, now); + targets[i] = P4ClockTarget(newTwelveDirection, clocks[i]); + + if (TryGetP4ActiveSoonState(states, expiresAt, i, 5546, now, out var state5546, out var remaining5546)) + { + var callText = P4MoveCallText(state5546); + if (!string.IsNullOrWhiteSpace(callText)) + { + moveCalls[i] = callText; + moveCallStates[i] = state5546; + moveCallRemaining[i] = remaining5546; + + if (i < accessory.Data.PartyList.Count) + partyMessages.Add($"{PartyMemberJobName(accessory, i)} {PartyPriorityLabel(i)} {callText}"); + } + } + } + + var marks = BuildP4ChainMoveMarks(clocks); + lock (_p4Lock) + Array.Copy(marks, _p4CommandMarkByPartyIndex, marks.Length); + + ApplyP4CommandMarksNoTimer(accessory, marks); + + var myIndex = GetMyIndex(accessory); + if (myIndex >= 0 && myIndex < 8 && clocks[myIndex] != P4ClockDirection.Unknown) + { + var target = targets[myIndex]; + DrawGuide(accessory, $"{DrawPrefix}_P4_Chain_Move_Guide", target, P4ChainGuideDurationMs); + DrawStaticCircle(accessory, $"{DrawPrefix}_P4_Chain_Move_Target", target, 0.25f, P4ChainGuideDurationMs, GuideColor.V4); + DebugEcho(accessory, $"P4循环移动指路已画:round={round} my={PartyPriorityLabel(myIndex)} clock={FormatP4Clock(clocks[myIndex])} target={FormatPosition(target)} duration={P4ChainGuideDurationMs}ms greenMove={EnableGreenMove}"); + GreenMoveToPoint(target, accessory, $"P4 chain move round {round}"); + + if (!string.IsNullOrWhiteSpace(moveCalls[myIndex])) + ScheduleP4SelfTtsAtRemaining3(accessory, generation, round, myIndex, moveCallStates[myIndex], moveCallRemaining[myIndex], moveCalls[myIndex]); + } + else + { + DebugEcho(accessory, $"P4循环移动指路未画:round={round} myIndex={myIndex}"); + } + + SendP4PartyCallouts(accessory, partyMessages, generation, round); + DebugP4ChainMove(accessory, round, states, expiresAt, clocks, targets, marks, moveCalls, moveCallRemaining, newTwelveDirection, directionReady, now); + } + + private void ExecuteP4Petrify(ScriptAccessory accessory, int round) + { + var states = new int[8, 10]; + var expiresAt = new long[8, 10]; + var now = NowMs(); + + lock (_p4Lock) + { + Array.Copy(_p4StatusStateByPartyAndStatus, states, _p4StatusStateByPartyAndStatus.Length); + Array.Copy(_p4StatusExpiresAtByPartyAndStatus, expiresAt, _p4StatusExpiresAtByPartyAndStatus.Length); + } + + var marks = BuildP4PetrifyMarks(states, expiresAt, now); + lock (_p4Lock) + Array.Copy(marks, _p4CommandMarkByPartyIndex, marks.Length); + + ApplyP4CommandMarksNoTimer(accessory, marks); + + var myIndex = GetMyIndex(accessory); + string alertText = null; + var alertPartyIndex = -1; + var alertState = P4StateUnknown; + var alertRemaining = 0; + + if (TryGetP4ActiveSoonState(states, expiresAt, myIndex, 5543, now, out var myState5543, out var myRemaining5543)) + { + alertText = P4PetrifyCallText(myState5543, true); + alertPartyIndex = myIndex; + alertState = myState5543; + alertRemaining = myRemaining5543; + } + else + { + for (var i = 0; i < 8; i++) + { + if (!TryGetP4ActiveSoonState(states, expiresAt, i, 5543, now, out var state5543, out var remaining5543)) + continue; + + alertText = P4PetrifyCallText(state5543, false); + alertPartyIndex = i; + alertState = state5543; + alertRemaining = remaining5543; + break; + } + } + + if (!string.IsNullOrWhiteSpace(alertText)) + Alert(alertText); + + DebugP4Petrify(accessory, round, states, expiresAt, marks, alertPartyIndex, alertState, alertRemaining, alertText, now); + } + + private void ExecuteP4Element(ScriptAccessory accessory, int round) + { + var states = new int[8, 10]; + var expiresAt = new long[8, 10]; + var now = NowMs(); + + lock (_p4Lock) + { + Array.Copy(_p4StatusStateByPartyAndStatus, states, _p4StatusStateByPartyAndStatus.Length); + Array.Copy(_p4StatusExpiresAtByPartyAndStatus, expiresAt, _p4StatusExpiresAtByPartyAndStatus.Length); + } + + TryResolveP4ElementCall(states, expiresAt, now, out var call, out var partyIndex, out var statusId, out var state, out var remainingMs); + var text = P4ElementCallText(call); + if (!string.IsNullOrWhiteSpace(text)) + QTTS(text); + + DebugP4Element(accessory, round, states, expiresAt, call, partyIndex, statusId, state, remainingMs, now); + } + + private void DebugP4ChainMove( + ScriptAccessory accessory, + int round, + int[,] states, + long[,] expiresAt, + IReadOnlyList clocks, + IReadOnlyList targets, + IReadOnlyList marks, + IReadOnlyList moveCalls, + IReadOnlyList moveCallRemaining, + Vector3 newTwelveDirection, + bool directionReady, + long now) + { + if (!DeveloperMode) return; + + DebugEcho(accessory, $"P4MOVE r={round} new12={FormatPosition(newTwelveDirection)} ready={directionReady}"); + for (var i = 0; i < 8; i++) + { + var objectId = i < accessory.Data.PartyList.Count ? accessory.Data.PartyList[i] : 0; + var buffs = FormatP4StatusesWithRemaining(states, expiresAt, i, now, 5544, 5545, 5546); + DebugEcho( + accessory, + $"{PartyPriorityLabel(i)} {FormatObjectId(objectId)} {buffs} => clock={FormatP4Clock(clocks[i])} target={FormatPosition(targets[i])} call={moveCalls[i] ?? "-"}({FormatRemainingMs(moveCallRemaining[i])}) mark={FormatP4Mark(marks[i])}"); + } + } + private void DebugP4Petrify( + ScriptAccessory accessory, + int round, + int[,] states, + long[,] expiresAt, + IReadOnlyList marks, + int alertPartyIndex, + int alertState, + int alertRemaining, + string alertText, + long now) + { + if (!DeveloperMode) return; + + DebugEcho(accessory, $"P4PETRI r={round} alert={PartyPriorityLabel(alertPartyIndex)} state={alertState}({FormatRemainingMs(alertRemaining)}) text={alertText ?? "-"}"); + for (var i = 0; i < 8; i++) + { + TryGetP4ActiveSoonState(states, expiresAt, i, 5543, now, out var state5543, out var remaining5543); + var call = P4PetrifyCallText(state5543, false) ?? "-"; + var objectId = i < accessory.Data.PartyList.Count ? accessory.Data.PartyList[i] : 0; + DebugEcho( + accessory, + $"{PartyPriorityLabel(i)} {FormatObjectId(objectId)} {FormatP4StatusStateWithRemaining(states, expiresAt, i, 5543, now)} => active={state5543}({FormatRemainingMs(remaining5543)}) call={call} mark={FormatP4Mark(marks[i])}"); + } + } + private void DebugP4Element( + ScriptAccessory accessory, + int round, + int[,] states, + long[,] expiresAt, + P4ElementCall call, + int partyIndex, + uint statusId, + int state, + int remainingMs, + long now) + { + if (!DeveloperMode) return; + + DebugEcho(accessory, $"P4ELEM r={round} result={FormatP4ElementCall(call)} src={PartyPriorityLabel(partyIndex)} id={statusId} state={state}({FormatRemainingMs(remainingMs)})"); + for (var i = 0; i < 8; i++) + { + TryGetP4ActiveSoonState(states, expiresAt, i, 5547, now, out var active5547, out var activeRemaining5547); + TryGetP4ActiveSoonState(states, expiresAt, i, 5548, now, out var active5548, out var activeRemaining5548); + var objectId = i < accessory.Data.PartyList.Count ? accessory.Data.PartyList[i] : 0; + DebugEcho( + accessory, + $"{PartyPriorityLabel(i)} {FormatObjectId(objectId)} {FormatP4StatusesWithRemaining(states, expiresAt, i, now, 5547, 5548)} => active5547={active5547}({FormatRemainingMs(activeRemaining5547)}) active5548={active5548}({FormatRemainingMs(activeRemaining5548)})"); + } + } + #endregion + + #region GreenMove + + private void GreenMoveToPoint(Vector3 target, ScriptAccessory accessory, string reason) + { + if (!EnableGreenMove) return; + + try + { + if (GreenMoveClearQueueBeforeMove) + GreenMoveClearQueue(); + + InvokeGreenMove6( + "MoveToPoint", + ref _greenMoveToPointSub, + ref _greenMoveToPointInvoke, + target, + GreenMoveTolerance > 0.0f ? GreenMoveTolerance : 0.1f, + GreenMoveSpeedMultiplier > 0.0f ? GreenMoveSpeedMultiplier : 1.0f, + GreenMoveIgnoreY, + Math.Max(0, GreenMoveMaxCastWaitMs), + GreenMoveWaitForQueuedCast); + } + catch (Exception ex) + { + WarnGreenMoveFailure(accessory, ex); + } + } + + private void GreenMoveStopAndClear(ScriptAccessory accessory, string reason) + { + if (!EnableGreenMove) return; + + try + { + GreenMoveClearQueue(); + GreenMoveStop(); + } + catch (Exception ex) + { + WarnGreenMoveFailure(accessory, ex); + } + } + + private void GreenMoveClearQueue() + { + InvokeGreenMoveAction0("ClearQueue", ref _greenMoveClearQueueSub, ref _greenMoveClearQueueInvoke); + } + + private void GreenMoveStop() + { + InvokeGreenMoveAction0("Stop", ref _greenMoveStopSub, ref _greenMoveStopInvoke); + } + + private void WarnGreenMoveFailure(ScriptAccessory accessory, Exception ex) + { + if (_warnedGreenMove) return; + + _warnedGreenMove = true; + accessory.Method.SendChat("/e GreenMove IPC调用失败,请确认PromeRotation已加载且GreenMove IPC已注册。"); + } + + private static void InvokeGreenMove6( + string name, + ref object sub, + ref MethodInfo invoke, + T1 arg1, + float tolerance, + float speed, + bool ignoreY, + int maxCastWaitMs, + bool waitForQueuedCast) + { + var fullName = GreenMovePrefix + name; + if (sub == null || invoke == null) + { + sub = CreateSubscriber(fullName); + invoke = sub.GetType().GetMethods(BindingFlags.Instance | BindingFlags.Public) + .FirstOrDefault(m => m.Name == "InvokeAction" && m.GetParameters().Length == 6) + ?? throw new MissingMethodException($"{name} missing InvokeAction."); + } + + try + { + invoke.Invoke(sub, new object[] { arg1, tolerance, speed, ignoreY, maxCastWaitMs, waitForQueuedCast }); + } + catch (TargetInvocationException ex) when (ex.InnerException != null) + { + throw ex.InnerException; + } + } + + private static void InvokeGreenMoveAction0(string name, ref object sub, ref MethodInfo invoke) + { + var fullName = GreenMovePrefix + name; + if (sub == null || invoke == null) + { + sub = CreateSubscriber(fullName); + invoke = sub.GetType().GetMethods(BindingFlags.Instance | BindingFlags.Public) + .FirstOrDefault(m => m.Name == "InvokeAction" && m.GetParameters().Length == 0) + ?? throw new MissingMethodException($"{name} missing InvokeAction."); + } + + try + { + invoke.Invoke(sub, Array.Empty()); + } + catch (TargetInvocationException ex) when (ex.InnerException != null) + { + throw ex.InnerException; + } + } + + private static object CreateSubscriber(string fullName) + { + var pluginInterface = GetKodakkuPluginInterface(); + var method = pluginInterface.GetType().GetMethods(BindingFlags.Instance | BindingFlags.Public) + .FirstOrDefault(m => m.Name == "GetIpcSubscriber" && m.IsGenericMethodDefinition && m.GetGenericArguments().Length == 1); + + if (method == null) + throw new MissingMethodException("GetIpcSubscriber not found."); + + return method.MakeGenericMethod(typeof(TRet)).Invoke(pluginInterface, new object[] { fullName }) + ?? throw new InvalidOperationException($"{fullName} subscriber is null."); + } + + private static object CreateSubscriber(string fullName) + { + var pluginInterface = GetKodakkuPluginInterface(); + var method = pluginInterface.GetType().GetMethods(BindingFlags.Instance | BindingFlags.Public) + .FirstOrDefault(m => m.Name == "GetIpcSubscriber" && m.IsGenericMethodDefinition && m.GetGenericArguments().Length == 7); + + if (method == null) + throw new MissingMethodException("GetIpcSubscriber not found."); + + return method.MakeGenericMethod(typeof(T1), typeof(T2), typeof(T3), typeof(T4), typeof(T5), typeof(T6), typeof(TRet)).Invoke(pluginInterface, new object[] { fullName }) + ?? throw new InvalidOperationException($"{fullName} subscriber is null."); + } + + private static object GetKodakkuPluginInterface() + { + var serviceType = typeof(ScriptAccessory).Assembly.GetType("KodakkuAssist.Data.Service") + ?? throw new TypeLoadException("KodakkuAssist.Data.Service not found."); + var prop = serviceType.GetProperty("PluginInterface", BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic) + ?? throw new MissingMemberException("KodakkuAssist.Data.Service.PluginInterface not found."); + + return prop.GetValue(null) + ?? throw new InvalidOperationException("KodakkuAssist PluginInterface is null."); + } + + #endregion + + #region Draw Helpers + + private void DrawCircle(ScriptAccessory accessory, string name, Vector3 position, float radius, int duration, int delay = 0, Vector4? color = null, ScaleMode scaleMode = ScaleMode.ByTime) + { + var dp = accessory.Data.GetDefaultDrawProperties(); + dp.Name = name; + dp.Position = position; + dp.Scale = new Vector2(radius); + dp.Color = color ?? accessory.Data.DefaultDangerColor; + dp.Delay = delay; + dp.DestoryAt = duration; + dp.ScaleMode = scaleMode; + accessory.Method.SendDraw(DrawModeEnum.Default, DrawTypeEnum.Circle, dp); + } + + private void DrawStaticCircle(ScriptAccessory accessory, string name, Vector3 position, float radius, int duration, Vector4? color = null) + { + var dp = accessory.Data.GetDefaultDrawProperties(); + dp.Name = name; + dp.Position = position; + dp.Scale = new Vector2(radius); + dp.Color = color ?? accessory.Data.DefaultSafeColor; + dp.DestoryAt = duration; + dp.ScaleMode = ScaleMode.None; + accessory.Method.SendDraw(DrawModeEnum.Default, DrawTypeEnum.Circle, dp); + } + + private void DrawCircleOnOwner(ScriptAccessory accessory, string name, uint ownerId, float radius, int duration, int delay = 0, Vector4? color = null, ScaleMode scaleMode = ScaleMode.ByTime) + { + var dp = accessory.Data.GetDefaultDrawProperties(); + dp.Name = name; + dp.Owner = ownerId; + dp.Scale = new Vector2(radius); + dp.Color = color ?? accessory.Data.DefaultDangerColor; + dp.Delay = delay; + dp.DestoryAt = duration; + dp.ScaleMode = scaleMode; + accessory.Method.SendDraw(DrawModeEnum.Default, DrawTypeEnum.Circle, dp); + } + + private void DrawRect(Event @event, ScriptAccessory accessory, string name, float width, float length, int duration, int delay = 0, Vector4? color = null, ScaleMode scaleMode = ScaleMode.YByTime) + { + DrawRect(accessory, name, @event.SourcePosition, @event.SourceRotation, width, length, duration, delay, color, scaleMode); + } + + private void DrawRect(ScriptAccessory accessory, string name, Vector3 position, float rotation, float width, float length, int duration, int delay = 0, Vector4? color = null, ScaleMode scaleMode = ScaleMode.YByTime) + { + var dp = accessory.Data.GetDefaultDrawProperties(); + dp.Name = name; + dp.Position = position; + dp.Rotation = rotation; + dp.Scale = new Vector2(width, length); + dp.Color = color ?? accessory.Data.DefaultDangerColor; + dp.Delay = delay; + dp.DestoryAt = duration; + dp.ScaleMode = scaleMode; + accessory.Method.SendDraw(DrawModeEnum.Default, DrawTypeEnum.Rect, dp); + } + + private void DrawFan(Event @event, ScriptAccessory accessory, string name, float radius, float radian, int duration, int delay = 0, Vector4? color = null, ScaleMode scaleMode = ScaleMode.ByTime) + { + DrawFan(accessory, name, @event.SourcePosition, @event.SourceRotation, radius, radian, duration, delay, color, scaleMode); + } + + private void DrawFan(ScriptAccessory accessory, string name, Vector3 position, float rotation, float radius, float radian, int duration, int delay = 0, Vector4? color = null, ScaleMode scaleMode = ScaleMode.ByTime) + { + var dp = accessory.Data.GetDefaultDrawProperties(); + dp.Name = name; + dp.Position = position; + dp.Rotation = rotation; + dp.Scale = new Vector2(radius); + dp.Radian = radian; + dp.Color = color ?? accessory.Data.DefaultDangerColor; + dp.Delay = delay; + dp.DestoryAt = duration; + dp.ScaleMode = scaleMode; + accessory.Method.SendDraw(DrawModeEnum.Default, DrawTypeEnum.Fan, dp); + } + + private void DrawLineToTarget(ScriptAccessory accessory, string name, uint sourceId, uint targetId, float width, int duration, int delay = 0, Vector4? color = null) + { + var dp = accessory.Data.GetDefaultDrawProperties(); + dp.Name = name; + dp.Owner = sourceId; + dp.TargetObject = targetId; + dp.Scale = new Vector2(width, 1.0f); + dp.Color = color ?? accessory.Data.DefaultDangerColor; + dp.Delay = delay; + dp.DestoryAt = duration; + dp.ScaleMode = ScaleMode.YByDistance; + accessory.Method.SendDraw(DrawModeEnum.Default, DrawTypeEnum.Rect, dp); + } + + private void DrawGuide(ScriptAccessory accessory, string name, Vector3 targetPosition, int duration, int delay = 0, Vector4? color = null) + { + var dp = accessory.Data.GetDefaultDrawProperties(); + dp.Name = name; + dp.Owner = GetMyId(accessory); + dp.TargetPosition = targetPosition; + dp.Scale = new Vector2(0.5f); + dp.ScaleMode = ScaleMode.YByDistance; + dp.Color = color ?? GuideColor.V4; + dp.Delay = delay; + dp.DestoryAt = duration; + accessory.Method.SendDraw(DrawModeEnum.Imgui, DrawTypeEnum.Displacement, dp); + } + + private void DrawGuideBetweenPositions(ScriptAccessory accessory, string name, Vector3 sourcePosition, Vector3 targetPosition, int duration, int delay = 0, Vector4? color = null) + { + var dp = accessory.Data.GetDefaultDrawProperties(); + dp.Name = name; + dp.Position = sourcePosition; + dp.TargetPosition = targetPosition; + dp.Scale = new Vector2(0.5f); + dp.ScaleMode = ScaleMode.YByDistance; + dp.Color = color ?? GuideColor.V4; + dp.Delay = delay; + dp.DestoryAt = duration; + accessory.Method.SendDraw(DrawModeEnum.Imgui, DrawTypeEnum.Displacement, dp); + } + + private void DrawInactiveGuideBetweenPositions(ScriptAccessory accessory, string name, Vector3 sourcePosition, Vector3 targetPosition, int duration, int delay = 0) + { + DrawGuideBetweenPositions(accessory, name, sourcePosition, targetPosition, duration, delay, SolidDangerRed); + } + + #endregion + + #region Mechanisms + + [ScriptMethod(name: "P4开始", eventType: EventTypeEnum.StartCasting, eventCondition: ["ActionId:49884"], userControl: false)] + public void P4_Start(Event @event, ScriptAccessory accessory) + { + _acc = accessory; + _phase = Phase.P4; + ResetMechanic(accessory); + } + + [ScriptMethod(name: "P4 2056真伪窗口", eventType: EventTypeEnum.StatusAdd, eventCondition: ["StatusID:2056"], userControl: false)] + public void P4_StatusTruthWindow(Event @event, ScriptAccessory accessory) + { + _acc = accessory; + if (_phase != Phase.P4) return; + + if (!int.TryParse(@event["Param"], NumberStyles.Integer, CultureInfo.InvariantCulture, out var param)) + return; + + if (!TryGetTargetDataId(@event, accessory, out var targetDataId)) + return; + + var now = NowMs(); + + if (targetDataId == P4XDataId && (param == 1121 || param == 1122)) + { + var targetPosition = ResolveEventTargetPosition(@event, accessory); + var newTwelveDirection = DefaultNorth; + var directionReady = IsLikelyArenaPosition(targetPosition) + && TryNormalizeFromCenter(targetPosition, out newTwelveDirection); + + lock (_p4Lock) + { + _p4XParam = param; + _p4XUpdatedAt = now; + _p4XExpiresAt = now + P4StatusWindowMs; + _p4XEventCount++; + + if (_p4XEventCount == 3) + { + _p4GroupSnapshotParam = param; + _p4GroupSnapshotAt = now; + } + + if (_p4XEventCount == 4) + { + _p4FourthXParam = param; + _p4FourthSnapshotAt = now; + _p4FourthTargetPosition = targetPosition; + _p4FourthNewTwelveDirection = directionReady ? newTwelveDirection : DefaultNorth; + _p4FourthDirectionReady = directionReady; + } + } + } + else if (targetDataId == P4CDataId && (param == 1119 || param == 1120)) + { + lock (_p4Lock) + { + _p4CParam = param; + _p4CUpdatedAt = now; + _p4CExpiresAt = now + P4StatusWindowMs; + } + } + + ScheduleP4LaterGroupsIfReady(accessory); + } + + [ScriptMethod(name: "P4 状态变量赋值", eventType: EventTypeEnum.StatusAdd, eventCondition: ["StatusID:regex:^(554[1-8]|454|1382|4887|4888|5464)$"], userControl: false)] + public void P4_TrackedStatusAdd(Event @event, ScriptAccessory accessory) + { + _acc = accessory; + if (_phase != Phase.P4) return; + + if (!TryGetStatusId(@event, out var rawStatusId)) + return; + + var directTrue = TryGetP4DirectTrueStatus(rawStatusId, out var directTrueStatusId); + var statusId = directTrue ? directTrueStatusId : rawStatusId; + + if (!TryGetP4TrackedStatusIndex(statusId, out var statusIndex)) + return; + + if (!TryGetTargetId(@event, out var targetId)) + return; + + var partyIndex = GetPlayerIndex(accessory, targetId); + if (partyIndex < 0 || partyIndex >= 8) + return; + + var now = NowMs(); + var hasDuration = TryGetDurationMs(@event, out var durationMs); + var assigned = false; + var assignedState = P4StateUnknown; + var xParam = 0; + var cParam = 0; + var xExpiresAt = 0L; + var cExpiresAt = 0L; + var xRemainingMs = 0; + var cRemainingMs = 0; + var xActive = false; + var cActive = false; + var usedWindow = "-"; + var usedParam = 0; + var usedWindowRemainingMs = 0; + var storedState = P4StateUnknown; + var storedRemainingMs = 0; + var chainRound = 0; + var chainStep = P4ChainStep.None; + + lock (_p4Lock) + { + if (hasDuration) + _p4StatusExpiresAtByPartyAndStatus[partyIndex, statusIndex] = now + durationMs; + + if (directTrue) + { + _p4StatusStateByPartyAndStatus[partyIndex, statusIndex] = P4StateTrue; + assigned = true; + assignedState = P4StateTrue; + usedWindow = "direct"; + } + else if (IsP4ChainDebugStatus(statusId) + && TryResolveP4LaterStateFromWindow(statusId, _p4XParam, _p4XExpiresAt, _p4CParam, _p4CExpiresAt, now, out var laterStateValue, out usedWindow, out usedParam, out usedWindowRemainingMs)) + { + _p4StatusStateByPartyAndStatus[partyIndex, statusIndex] = laterStateValue; + assigned = true; + assignedState = laterStateValue; + + if (hasDuration) + { + _p4BuffRecords.Add(new P4BuffRecord + { + PartyIndex = partyIndex, + StatusId = statusId, + State = laterStateValue, + DurationMs = durationMs, + ExpiresAt = now + durationMs, + }); + } + } + else if (!IsP4ChainDebugStatus(statusId) + && TryGetActiveP4StateValueLocked(now, out var stateValue, out usedWindow, out usedParam, out usedWindowRemainingMs)) + { + _p4StatusStateByPartyAndStatus[partyIndex, statusIndex] = stateValue; + assigned = true; + assignedState = stateValue; + } + + xParam = _p4XParam; + cParam = _p4CParam; + xExpiresAt = _p4XExpiresAt; + cExpiresAt = _p4CExpiresAt; + xRemainingMs = RemainingMs(_p4XExpiresAt, now); + cRemainingMs = RemainingMs(_p4CExpiresAt, now); + xActive = _p4XParam != 0 && xRemainingMs > 0; + cActive = _p4CParam != 0 && cRemainingMs > 0; + storedState = _p4StatusStateByPartyAndStatus[partyIndex, statusIndex]; + storedRemainingMs = RemainingMs(_p4StatusExpiresAtByPartyAndStatus[partyIndex, statusIndex], now); + chainRound = _p4ChainRound; + chainStep = _p4ChainStep; + } + + if (IsP4ChainDebugStatus(statusId)) + { + DebugEcho( + accessory, + $"P4BUFF+ r={chainRound}/{chainStep} {PartyPriorityLabel(partyIndex)} {P4BuffName(statusId)} id={statusId} set={(assigned ? assignedState.ToString(CultureInfo.InvariantCulture) : "-")} now={storedState}({FormatRemainingMs(storedRemainingMs)}) dur={(hasDuration ? FormatRemainingMs(durationMs) : "-")} use={FormatP4UsedWindowDebug(usedWindow, usedParam, usedWindowRemainingMs)} {FormatP4WindowDebug("X", xParam, xRemainingMs, xActive)} {FormatP4WindowDebug("C", cParam, cRemainingMs, cActive)}"); + ScheduleP4LaterGroupsIfReady(accessory); + } + } + + [ScriptMethod(name: "P4 50070后续循环开始", eventType: EventTypeEnum.ActionEffect, eventCondition: ["ActionId:50070"], userControl: false)] + public void P4_ChainStartActionEffect(Event @event, ScriptAccessory accessory) + { + _acc = accessory; + if (_phase != Phase.P4) return; + + accessory.Method.RemoveDraw($"{DrawPrefix}_P4_First_.*"); + ClearP4CommandMarksNow(accessory); + GreenMoveStopAndClear(accessory, "P4 chain start"); + DebugEcho(accessory, "P4 later timeline start by 50070"); + ScheduleP4LaterGroupsIfReady(accessory); + } + + [ScriptMethod(name: "P4 后续状态移除推进", eventType: EventTypeEnum.StatusRemove, eventCondition: ["StatusID:regex:^554[3-8]$"], userControl: false)] + public void P4_TrackedStatusRemove(Event @event, ScriptAccessory accessory) + { + _acc = accessory; + if (_phase != Phase.P4) return; + + if (!TryGetStatusId(@event, out var statusId)) + return; + + if (!TryGetP4TrackedStatusIndex(statusId, out var statusIndex)) + return; + + if (!TryGetTargetId(@event, out var targetId)) + return; + + var partyIndex = GetPlayerIndex(accessory, targetId); + if (partyIndex < 0 || partyIndex >= 8) + return; + } + + [ScriptMethod(name: "P4 后续危险范围捕获", eventType: EventTypeEnum.StartCasting, eventCondition: ["ActionId:regex:^(47777|47775|47768|47774)$"], userControl: false)] + public void P4_LaterDangerStartCasting(Event @event, ScriptAccessory accessory) + { + _acc = accessory; + if (_phase != Phase.P4) return; + + if (!TryGetActionId(@event, out var actionId)) + return; + + P4DangerRecord record; + if (actionId == 47777 || actionId == 47775) + { + record = new P4DangerRecord + { + ActionId = actionId, + Shape = P4DangerShape.Rect, + Position = @event.SourcePosition, + Rotation = @event.SourceRotation, + CapturedAt = NowMs(), + }; + } + else + { + record = new P4DangerRecord + { + ActionId = actionId, + Shape = P4DangerShape.Fan, + Position = P4ArenaCenter, + Rotation = @event.SourceRotation, + CapturedAt = NowMs(), + }; + } + + var shouldTryFinal = false; + lock (_p4Lock) + { + _p4DangerRecords.Add(record); + shouldTryFinal = _p4FGroupStartedAt > 0 && !_p4FGroupDangerDrawn; + } + + DebugEcho(accessory, $"P4DANGER+ id={actionId} shape={record.Shape} pos={FormatPosition(record.Position)} rot={record.Rotation:F3}"); + + if (shouldTryFinal) + TryDrawP4FinalSafe(accessory, _generation); + } + + [ScriptMethod(name: "P4 一运半场指路", eventType: EventTypeEnum.StartCasting, eventCondition: ["ActionId:regex:^(50068|50069)$"], userControl: false)] + public void P4_FirstMechanicGuide(Event @event, ScriptAccessory accessory) + { + _acc = accessory; + if (_phase != Phase.P4) return; + + if (!TryGetActionId(@event, out var actionId)) + return; + + var states = new int[8, 10]; + int fourthParam; + int fourthCount; + long fourthSnapshotAt; + Vector3 fourthTargetPosition; + Vector3 blueSourcePosition; + Vector3 purpleSourcePosition; + Vector3 newTwelveDirection; + Vector3 midpoint; + + lock (_p4Lock) + { + if (_p4HotpotResolved) + return; + + if (actionId == P4ResolveActionId) + { + _p4HotpotBlueSourcePosition = @event.SourcePosition; + _p4HotpotBlueSourceReady = true; + } + else if (actionId == P4PurpleSourceActionId) + { + _p4HotpotPurpleSourcePosition = @event.SourcePosition; + _p4HotpotPurpleSourceReady = true; + } + else + { + return; + } + + fourthParam = _p4FourthXParam; + fourthCount = _p4XEventCount; + fourthSnapshotAt = _p4FourthSnapshotAt; + fourthTargetPosition = _p4FourthTargetPosition; + blueSourcePosition = _p4HotpotBlueSourcePosition; + purpleSourcePosition = _p4HotpotPurpleSourcePosition; + + if (!_p4HotpotBlueSourceReady || !_p4HotpotPurpleSourceReady) + { + DebugEcho( + accessory, + $"P4HOTPOT WAIT action={actionId} blueReady={_p4HotpotBlueSourceReady} purpleReady={_p4HotpotPurpleSourceReady} blue50069={FormatPosition(_p4HotpotBlueSourcePosition)} purple50068={FormatPosition(_p4HotpotPurpleSourcePosition)}"); + return; + } + + if (fourthParam != 1121 && fourthParam != 1122) + { + DebugEcho(accessory, $"P4一运结算跳过:第四次19510/2056未就绪 param={fourthParam} target={FormatPosition(_p4FourthTargetPosition)}"); + return; + } + + midpoint = new Vector3( + (blueSourcePosition.X + purpleSourcePosition.X) * 0.5f, + (blueSourcePosition.Y + purpleSourcePosition.Y) * 0.5f, + (blueSourcePosition.Z + purpleSourcePosition.Z) * 0.5f); + + if (!TryNormalizeFromCenter(midpoint, out newTwelveDirection)) + { + DebugEcho(accessory, $"P4一运结算跳过:50068/50069中点无效 blue50069={FormatPosition(blueSourcePosition)} purple50068={FormatPosition(purpleSourcePosition)} mid={FormatPosition(midpoint)}"); + return; + } + + Array.Copy(_p4StatusStateByPartyAndStatus, states, _p4StatusStateByPartyAndStatus.Length); + _p4HotpotResolved = true; + } + + var rightVector = RightVectorFromNewTwelve(newTwelveDirection); + var l1 = P4ArenaCenter - rightVector * P4GuideOffset; + var r1 = P4ArenaCenter + rightVector * P4GuideOffset; + var blueSide = SideOfPosition(blueSourcePosition, rightVector); + var purpleSide = SideOfPosition(purpleSourcePosition, rightVector); + var blueDelta = new Vector3(blueSourcePosition.X - P4ArenaCenter.X, 0.0f, blueSourcePosition.Z - P4ArenaCenter.Z); + var purpleDelta = new Vector3(purpleSourcePosition.X - P4ArenaCenter.X, 0.0f, purpleSourcePosition.Z - P4ArenaCenter.Z); + var blueDot = blueDelta.X * rightVector.X + blueDelta.Z * rightVector.Z; + var purpleDot = purpleDelta.X * rightVector.X + purpleDelta.Z * rightVector.Z; + var leftColor = blueSide == P4HalfSide.Left + ? P4HalfColor.Blue + : purpleSide == P4HalfSide.Left + ? P4HalfColor.Purple + : P4HalfColor.Unknown; + var rightColor = blueSide == P4HalfSide.Right + ? P4HalfColor.Blue + : purpleSide == P4HalfSide.Right + ? P4HalfColor.Purple + : P4HalfColor.Unknown; + if (leftColor == P4HalfColor.Unknown && rightColor != P4HalfColor.Unknown) + leftColor = OppositeColor(rightColor); + if (rightColor == P4HalfColor.Unknown && leftColor != P4HalfColor.Unknown) + rightColor = OppositeColor(leftColor); + var fourthAgeMs = fourthSnapshotAt > 0 ? NowMs() - fourthSnapshotAt : -1; + + DebugEcho( + accessory, + $"P4HOTPOT DIAG xCount={fourthCount} fourthParam={fourthParam} fourthAge={fourthAgeMs}ms fourthTarget={FormatPosition(fourthTargetPosition)} blue50069={FormatPosition(blueSourcePosition)} purple50068={FormatPosition(purpleSourcePosition)} mid={FormatPosition(midpoint)} new12={FormatPosition(newTwelveDirection)} rightVec={FormatPosition(rightVector)} blueDot={blueDot:F3} purpleDot={purpleDot:F3} blueSide={FormatP4Side(blueSide)} purpleSide={FormatP4Side(purpleSide)} left={FormatP4Color(leftColor)} right={FormatP4Color(rightColor)}"); + + var sides = new P4HalfSide[8]; + var colors = new P4HalfColor[8]; + + for (var i = 0; i < 8; i++) + { + if (!TryResolveP4PlayerColor(states, i, out var color, out _, out _)) + { + colors[i] = P4HalfColor.Unknown; + sides[i] = P4HalfSide.Unknown; + continue; + } + + colors[i] = color; + sides[i] = SideForColor(color, leftColor, rightColor); + } + + var marks = BuildP4CommandMarks(sides); + + lock (_p4Lock) + { + Array.Copy(sides, _p4ResolvedSideByPartyIndex, sides.Length); + Array.Copy(colors, _p4ResolvedColorByPartyIndex, colors.Length); + Array.Copy(marks, _p4CommandMarkByPartyIndex, marks.Length); + } + + ApplyP4CommandMarks(accessory, marks); + + var myIndex = GetMyIndex(accessory); + if (myIndex >= 0 && myIndex < sides.Length) + { + var mySide = sides[myIndex]; + if (mySide == P4HalfSide.Left || mySide == P4HalfSide.Right) + { + var targetPosition = mySide == P4HalfSide.Right ? r1 : l1; + DrawGuide(accessory, $"{DrawPrefix}_P4_First_Guide", targetPosition, P4GuideDurationMs); + DrawStaticCircle(accessory, $"{DrawPrefix}_P4_First_Target", targetPosition, 0.25f, P4GuideDurationMs, GuideColor.V4); + DebugEcho(accessory, $"P4一运指路已画:my={PartyPriorityLabel(myIndex)} side={FormatP4Side(mySide)} color={FormatP4Color(colors[myIndex])} target={FormatPosition(targetPosition)} duration={P4GuideDurationMs}ms greenMove={EnableGreenMove}"); + GreenMoveToPoint(targetPosition, accessory, "P4 first mechanic"); + } + else + { + DebugEcho(accessory, $"P4一运指路未画:my={PartyPriorityLabel(myIndex)} side={FormatP4Side(mySide)} color={FormatP4Color(colors[myIndex])}"); + } + } + else + { + DebugEcho(accessory, $"P4一运指路未画:myIndex={myIndex} partyCount={accessory.Data.PartyList.Count}"); + } + + DebugP4Resolve(accessory, states, sides, colors, marks, fourthParam, blueSide, leftColor, rightColor, newTwelveDirection, l1, r1); + } + + #endregion + } +} diff --git "a/a16239438/(\345\274\202\351\227\273\345\225\206\346\227\205\345\245\207\350\260\255)Another Merchant's Tale.cs" "b/a16239438/(\345\274\202\351\227\273\345\225\206\346\227\205\345\245\207\350\260\255)Another Merchant's Tale.cs" new file mode 100644 index 0000000..bbb0788 --- /dev/null +++ "b/a16239438/(\345\274\202\351\227\273\345\225\206\346\227\205\345\245\207\350\260\255)Another Merchant's Tale.cs" @@ -0,0 +1,1073 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Numerics; +using System.Threading.Tasks; +using KodakkuAssist.Data; +using KodakkuAssist.Module.Draw; +using KodakkuAssist.Module.GameEvent; +using KodakkuAssist.Module.GameEvent.Struct; +using KodakkuAssist.Script; + +namespace RyougiMioScriptNamespace +{ + [ScriptType(name: "(异闻商旅奇谭)Another Merchant's Tale", territorys: [1317], guid: "41658b2f-191c-4d68-98c3-c26a10ddce67", version: "0.0.0.76", author: "RyougiMio", note: "Another Merchant's Tale test")] + public class Script1317 + { + #region Settings + [UserSetting("是否开启屏幕文字提示")] + public bool EnableText { get; set; } = true; + + [UserSetting("是否开启TTS语音提示")] + public bool EnableTTS { get; set; } = true; + + [UserSetting("常用危险色")] + public ScriptColor DangerColor { get; set; } = new ScriptColor() { V4 = new Vector4(1.0f, 0.0f, 0.0f, 0.01f) }; + + [UserSetting("常用安全色")] + public ScriptColor SafeColor { get; set; } = new ScriptColor() { V4 = new Vector4(0.0f, 1.0f, 0.0f, 0.01f) }; + + [UserSetting("指路/引导颜色")] + public ScriptColor GuideColor { get; set; } = new ScriptColor() { V4 = new Vector4(0.0f, 1.0f, 1.0f, 0.01f) }; + + [UserSetting("2161-2164调试输出")] + public bool Debug2161To2164 { get; set; } = true; + #endregion + + #region Variables + private ScriptAccessory _acc; + private int _phase = 0; + private long _lastMechanicTicks = 0; + private int _p3EnbuSide = 0; + private long _p3EnbuCastAt = 0; + private int _p3EnbuTetherOrder = 1; + private int _p3EnbuGroupType = 0; + private int _p3KaitenGenbuCast = 0; + private int _p3QuadEnbuCapture = 0; + private uint _p3QuadEnbuCastId = 0; + private readonly List _p3QuadEnbuIcons = new(); + private readonly List _p3QuadEnbuStacks = new(); + private int _p3QuadEnbuCurrentStep = 0; + private float _p3QuadEnbuCurrentRotation = 0f; + private ulong _p3QuadEnbuSourceId = 0; + private string _p3QuadEnbuCurrentDrawName = string.Empty; + private string _p3QuadEnbuCurrentCircleDrawName = string.Empty; + private long _p3QuadEnbuCurrentDrawAt = 0; + private int _p3Timeline4563State = 0; + private Vector3 _p3Timeline4563Pos = default; + private int _p3Action45504Count = 0; + private readonly List<(ulong SourceId, Vector3 Pos)> _p3Action45504Records = new(); + private ulong _p3Action45504TargetSourceId = 0; + private int _p3Action47153TargetCount = 0; + private readonly (string Name, Vector3 Position, float Rotation, Vector2 Scale)[] _icon0014RowPreset = + [ + ("Icon0014_Row_0", new Vector3(355f, -29.5f, 514f), MathF.PI * 0.5f, new Vector2(8f, 40f)), + ("Icon0014_Row_1", new Vector3(355f, -29.5f, 522f), MathF.PI * 0.5f, new Vector2(8f, 40f)), + ("Icon0014_Row_2", new Vector3(355f, -29.5f, 530f), MathF.PI * 0.5f, new Vector2(8f, 40f)), + ("Icon0014_Row_3", new Vector3(355f, -29.5f, 538f), MathF.PI * 0.5f, new Vector2(8f, 40f)), + ("Icon0014_Row_4", new Vector3(355f, -29.5f, 546f), MathF.PI * 0.5f, new Vector2(8f, 40f)), + ]; + private readonly (string Name, Vector3 Position, float Rotation, Vector2 Scale)[] _icon0014ColPreset = + [ + ("Icon0014_Col_0", new Vector3(359f, -29.5f, 510f), 0f, new Vector2(8f, 40f)), + ("Icon0014_Col_1", new Vector3(367f, -29.5f, 510f), 0f, new Vector2(8f, 40f)), + ("Icon0014_Col_2", new Vector3(375f, -29.5f, 510f), 0f, new Vector2(8f, 40f)), + ("Icon0014_Col_3", new Vector3(383f, -29.5f, 510f), 0f, new Vector2(8f, 40f)), + ("Icon0014_Col_4", new Vector3(391f, -29.5f, 510f), 0f, new Vector2(8f, 40f)), + ]; + #endregion + + #region Methods + private void QTTS(string text, int rate = 0) + { + if (!EnableTTS || _acc == null) return; + _acc.Method.TTS(text, rate); + } + + private void QText(string text, int duration, bool isWarning = false) + { + if (!EnableText || _acc == null) return; + _acc.Method.TextInfo(text, duration, isWarning); + } + #endregion + + #region Initialization + public void Init(ScriptAccessory accessory) + { + _acc = accessory; + _phase = 0; + _lastMechanicTicks = 0; + _p3EnbuSide = 0; + _p3EnbuCastAt = 0; + _p3EnbuTetherOrder = 1; + _p3EnbuGroupType = 0; + _p3KaitenGenbuCast = 0; + _p3QuadEnbuCapture = 0; + _p3QuadEnbuCastId = 0; + _p3QuadEnbuIcons.Clear(); + _p3QuadEnbuStacks.Clear(); + _p3QuadEnbuCurrentStep = 0; + _p3QuadEnbuCurrentRotation = 0f; + _p3QuadEnbuSourceId = 0; + _p3QuadEnbuCurrentDrawName = string.Empty; + _p3QuadEnbuCurrentCircleDrawName = string.Empty; + _p3QuadEnbuCurrentDrawAt = 0; + _p3Timeline4563State = 0; + _p3Timeline4563Pos = default; + _p3Action45504Count = 0; + _p3Action45504Records.Clear(); + _p3Action45504TargetSourceId = 0; + _p3Action47153TargetCount = 0; + + accessory.Method.RemoveDraw(".*"); + + accessory.Method.SendChat("/e Another Merchant's Tale Initialized."); + } + #endregion + + #region ScriptMethods + [ScriptMethod(name: "45870 AOE TTS", eventType: EventTypeEnum.StartCasting, eventCondition: ["ActionId:45870"])] + public void AOE_Alert_45870(Event @event, ScriptAccessory accessory) + { + _acc = accessory; + _lastMechanicTicks = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(); + + if (!uint.TryParse(@event["ActionId"], out var aid)) return; + if (aid != 45870) return; + + QTTS("AOE"); + QText("AOE", 3000, true); + } + + [ScriptMethod(name: "45517 AOE TTS", eventType: EventTypeEnum.StartCasting, eventCondition: ["ActionId:45517"])] + public void AOE_Alert_45517(Event @event, ScriptAccessory accessory) + { + _acc = accessory; + _lastMechanicTicks = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(); + + if (!uint.TryParse(@event["ActionId"], out var aid)) return; + if (aid != 45517) return; + + QTTS("AOE"); + QText("AOE", 3000, true); + } + + [ScriptMethod(name: "45839/45841/45840 危险渐变矩形", eventType: EventTypeEnum.StartCasting, eventCondition: ["ActionId:regex:^(45839|45841|45840)$"])] + public void DrawDangerRect_45839_45841(Event @event, ScriptAccessory accessory) + { + _acc = accessory; + _lastMechanicTicks = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(); + + if (!uint.TryParse(@event["ActionId"], out var aid)) return; + + int duration = 5000; + if (int.TryParse(@event["DurationMilliseconds"], out var dur) && dur > 0) + duration = dur; + + var dp = accessory.Data.GetDefaultDrawProperties(); + dp.Name = $"Rect_{aid}_{DateTime.Now.Ticks}"; + dp.Position = @event.SourcePosition; + dp.Rotation = @event.SourceRotation; + dp.Scale = new Vector2(8f, 40f); + dp.Color = accessory.Data.DefaultDangerColor; + dp.DestoryAt = duration; + dp.ScaleMode = ScaleMode.YByTime; + accessory.Method.SendDraw(DrawModeEnum.Default, DrawTypeEnum.Rect, dp); + } + + [ScriptMethod(name: "45843 危险渐变45度扇形", eventType: EventTypeEnum.StartCasting, eventCondition: ["ActionId:45843"])] + public void DrawDangerFan_45843(Event @event, ScriptAccessory accessory) + { + _acc = accessory; + _lastMechanicTicks = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(); + + int duration = 5000; + if (int.TryParse(@event["DurationMilliseconds"], out var dur) && dur > 0) + duration = dur; + + var dp = accessory.Data.GetDefaultDrawProperties(); + dp.Name = $"Fan45_45843_{DateTime.Now.Ticks}"; + dp.Position = @event.SourcePosition; + dp.Rotation = @event.SourceRotation; + dp.Scale = new Vector2(45f); + dp.Radian = float.Pi / 4; + dp.Color = accessory.Data.DefaultDangerColor; + dp.DestoryAt = duration; + dp.ScaleMode = ScaleMode.ByTime; + accessory.Method.SendDraw(DrawModeEnum.Default, DrawTypeEnum.Fan, dp); + } + + [ScriptMethod(name: "45842 危险渐变半圆", eventType: EventTypeEnum.StartCasting, eventCondition: ["ActionId:45842"])] + public void DrawDangerSemiCircle_45842(Event @event, ScriptAccessory accessory) + { + _acc = accessory; + _lastMechanicTicks = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(); + + int duration = 5000; + if (int.TryParse(@event["DurationMilliseconds"], out var dur) && dur > 0) + duration = dur; + + var dp = accessory.Data.GetDefaultDrawProperties(); + dp.Name = $"SemiCircle_45842_{DateTime.Now.Ticks}"; + dp.Position = @event.SourcePosition; + dp.Rotation = @event.SourceRotation; + dp.Scale = new Vector2(20f); + dp.Radian = float.Pi; + dp.Color = accessory.Data.DefaultDangerColor; + dp.DestoryAt = duration; + dp.ScaleMode = ScaleMode.ByTime; + accessory.Method.SendDraw(DrawModeEnum.Default, DrawTypeEnum.Fan, dp); + } + + [ScriptMethod(name: "45866 危险渐变90度扇形(最后3秒)", eventType: EventTypeEnum.StartCasting, eventCondition: ["ActionId:45866"])] + public void DrawDangerFanLast3s_45866(Event @event, ScriptAccessory accessory) + { + _acc = accessory; + _lastMechanicTicks = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(); + + int duration = 5000; + if (int.TryParse(@event["DurationMilliseconds"], out var dur) && dur > 0) + duration = dur; + + int showMs = Math.Min(3000, duration); + int delayMs = Math.Max(0, duration - 3000); + + float rotation = @event.SourceRotation; + if (float.TryParse(@event["TargetRotation"], NumberStyles.Float, CultureInfo.InvariantCulture, out var targetRot)) + rotation = targetRot; + + var dp = accessory.Data.GetDefaultDrawProperties(); + dp.Name = $"Fan90_45866_{DateTime.Now.Ticks}"; + dp.Position = @event.SourcePosition; + dp.Rotation = rotation; + dp.Scale = new Vector2(25f); + dp.Radian = float.Pi / 2; + dp.Color = accessory.Data.DefaultDangerColor; + dp.Delay = delayMs; + dp.DestoryAt = showMs; + dp.ScaleMode = ScaleMode.ByTime; + accessory.Method.SendDraw(DrawModeEnum.Default, DrawTypeEnum.Fan, dp); + } + + [ScriptMethod(name: "45863 中心矩形步进5次", eventType: EventTypeEnum.StartCasting, eventCondition: ["ActionId:45863"])] + public void DrawStepRect_45863(Event @event, ScriptAccessory accessory) + { + _acc = accessory; + _lastMechanicTicks = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(); + + float rot = @event.SourceRotation; + Vector3 src = @event.SourcePosition; + int firstDuration = 2000; + if (int.TryParse(@event["DurationMilliseconds"], out var dur) && dur > 0) + firstDuration = dur; + + for (int i = 0; i < 5; i++) + { + float forward = i * 8f - 4f; + Vector3 pos = new( + src.X + MathF.Sin(rot) * forward, + src.Y, + src.Z + MathF.Cos(rot) * forward + ); + + var dp = accessory.Data.GetDefaultDrawProperties(); + dp.Name = $"Rect_45863_{i}_{DateTime.Now.Ticks}"; + dp.Position = pos; + dp.Rotation = rot; + dp.Scale = new Vector2(40f, 8f); + dp.Color = accessory.Data.DefaultDangerColor; + dp.Delay = i == 0 ? 0 : firstDuration + (i - 1) * 2125; + dp.DestoryAt = i == 0 ? firstDuration : 2125; + dp.ScaleMode = ScaleMode.None; + accessory.Method.SendDraw(DrawModeEnum.Default, DrawTypeEnum.Rect, dp); + } + } + + [ScriptMethod(name: "2015003 危险圆(ObjectChanged Add/Remove)", eventType: EventTypeEnum.ObjectChanged, eventCondition: ["DataId:2015003"])] + public void ObjectChanged_2015003_Circle(Event @event, ScriptAccessory accessory) + { + string operate = @event["Operate"] ?? string.Empty; + + ulong sid = @event.SourceId; + if (sid == 0) + { + string rawSourceId = @event["SourceId"]; + if (!string.IsNullOrWhiteSpace(rawSourceId)) + { + if (!ulong.TryParse(rawSourceId, out sid)) + { + string normalized = rawSourceId.StartsWith("0x", StringComparison.OrdinalIgnoreCase) + ? rawSourceId.Substring(2) + : rawSourceId; + ulong.TryParse(normalized, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out sid); + } + } + } + if (sid == 0) return; + + string drawName = $"Circle_2015003_{sid}"; + if (operate.Equals("Add", StringComparison.OrdinalIgnoreCase)) + { + var dp = accessory.Data.GetDefaultDrawProperties(); + dp.Name = drawName; + dp.Owner = sid; + dp.Position = @event.SourcePosition; + dp.Scale = new Vector2(12f); + dp.Color = accessory.Data.DefaultDangerColor; + dp.DestoryAt = 3600000; + accessory.Method.SendDraw(DrawModeEnum.Default, DrawTypeEnum.Circle, dp); + return; + } + + if (operate.Equals("Remove", StringComparison.OrdinalIgnoreCase)) + { + accessory.Method.RemoveDraw(drawName); + } + } + + [ScriptMethod(name: "2015004/2015005 位置判定危险区(ObjectEffect16|32)", eventType: EventTypeEnum.ObjectEffect, eventCondition: ["Id1:16", "Id2:32"])] + public void ObjectEffect_2015004_2015005_ByPosition(Event @event, ScriptAccessory accessory) + { + Vector3 pos = @event.SourcePosition; + bool isSteel = (pos.X >= 370f && pos.X <= 380f) || (pos.Z >= 525f && pos.Z <= 535f); + + var dp = accessory.Data.GetDefaultDrawProperties(); + dp.Name = $"Obj_{(isSteel ? "Steel" : "Donut")}_{DateTime.Now.Ticks}"; + dp.Position = pos; + dp.Color = accessory.Data.DefaultDangerColor; + dp.DestoryAt = 10000; + dp.ScaleMode = ScaleMode.ByTime; + + if (isSteel) + { + dp.Scale = new Vector2(18f); + accessory.Method.SendDraw(DrawModeEnum.Default, DrawTypeEnum.Circle, dp); + } + else + { + dp.Scale = new Vector2(20f); + dp.InnerScale = new Vector2(4f); + dp.Radian = float.Pi * 2; + accessory.Method.SendDraw(DrawModeEnum.Default, DrawTypeEnum.Donut, dp); + } + } + + [ScriptMethod(name: "2161-2164 Buff绑身射线", eventType: EventTypeEnum.StatusAdd, eventCondition: ["StatusID:regex:^(2161|2162|2163|2164)$"])] + public void OnStatusAdd_2161_2164_BindLine(Event @event, ScriptAccessory accessory) + { + string status = @event["StatusID"]; + if (string.IsNullOrWhiteSpace(status)) status = @event["StatusId"]; + string rawTargetId = @event["TargetId"]; + if (string.IsNullOrWhiteSpace(rawTargetId)) rawTargetId = @event["TargetID"]; + if (string.IsNullOrWhiteSpace(rawTargetId)) return; + string targetIdHex = rawTargetId.StartsWith("0x", StringComparison.OrdinalIgnoreCase) ? rawTargetId.Substring(2) : rawTargetId; + targetIdHex = targetIdHex.PadLeft(16, '0'); + if (!ulong.TryParse(targetIdHex, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out var tid)) return; + + float radOffset = 0f; + if (status == "2161") radOffset = 0f; // 前 + else if (status == "2162") radOffset = MathF.PI; // 后 + else if (status == "2163") radOffset = 0.5f * MathF.PI; // 左 + else if (status == "2164") radOffset = -0.5f * MathF.PI; // 右 + + var dpLine = accessory.Data.GetDefaultDrawProperties(); + dpLine.Name = $"GuideLine_216x_{tid}_{DateTime.Now.Ticks}"; + dpLine.Owner = tid; + dpLine.Rotation = radOffset; + dpLine.Scale = new Vector2(0.5f, 16.5f); + dpLine.Color = accessory.Data.DefaultSafeColor; + dpLine.DestoryAt = 19000; + accessory.Method.SendDraw(DrawModeEnum.Default, DrawTypeEnum.Line, dpLine); + } + + #region P3 + [ScriptMethod(name: "P3 集火炎舞/火粉炎舞", eventType: EventTypeEnum.StartCasting, eventCondition: ["ActionId:regex:^(45437|45436|45435|45434)$"])] + public void P3_EnbuStoreAndTTS(Event @event, ScriptAccessory accessory) + { + _acc = accessory; + _lastMechanicTicks = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(); + + if (!uint.TryParse(@event["ActionId"], out var aid)) return; + + _p3EnbuCastAt = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(); + _p3EnbuTetherOrder = 1; + + if (aid == 45437) + { + _p3EnbuSide = 1; + _p3EnbuGroupType = 1; + QTTS("稍后月环分摊"); + return; + } + if (aid == 45436) + { + _p3EnbuSide = -1; + _p3EnbuGroupType = 1; + QTTS("稍后月环分摊"); + return; + } + if (aid == 45435) + { + _p3EnbuSide = 1; + _p3EnbuGroupType = 2; + QTTS("稍后月环分散 左"); + return; + } + if (aid == 45434) + { + _p3EnbuSide = -1; + _p3EnbuGroupType = 2; + QTTS("稍后月环分散 右"); + return; + } + } + + [ScriptMethod(name: "P3 集火炎舞/火粉炎舞-Tether", eventType: EventTypeEnum.Tether, eventCondition: ["Id:0163"])] + public void P3_EnbuTether(Event @event, ScriptAccessory accessory) + { + if (_p3EnbuSide == 0) return; + + long now = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(); + if (_p3EnbuCastAt == 0 || now - _p3EnbuCastAt > 30000) return; + + _acc = accessory; + _lastMechanicTicks = now; + + Vector3 sourcePos = @event.SourcePosition; + Vector3 targetPos = @event.TargetPosition; + float deltaX = targetPos.X - sourcePos.X; + float deltaZ = targetPos.Z - sourcePos.Z; + float rotation = MathF.Atan2(deltaX, deltaZ) + _p3EnbuSide * 0.5f * MathF.PI; + rotation %= 2 * MathF.PI; + if (rotation > MathF.PI) + rotation -= 2 * MathF.PI; + else if (rotation <= -MathF.PI) + rotation += 2 * MathF.PI; + + Vector3 pos = new( + (sourcePos.X + targetPos.X) * 0.5f, + (sourcePos.Y + targetPos.Y) * 0.5f, + (sourcePos.Z + targetPos.Z) * 0.5f + ); + + int delayMs = 0; + int durationMs = 11125; + if (_p3EnbuTetherOrder == 1) + { + _p3EnbuTetherOrder = 2; + } + else if (_p3EnbuTetherOrder == 2) + { + delayMs = 6125; + durationMs = 4000; + _p3EnbuTetherOrder = 3; + } + else + { + delayMs = 6125; + durationMs = 3125; + _p3EnbuTetherOrder = 1; + } + + var dp = accessory.Data.GetDefaultDrawProperties(); + dp.Name = $"P3_EnbuTether_{DateTime.Now.Ticks}"; + dp.Position = pos; + dp.Rotation = rotation; + dp.Scale = new Vector2(40f, 40f); + dp.Color = accessory.Data.DefaultDangerColor; + dp.Delay = delayMs; + dp.DestoryAt = durationMs; + dp.ScaleMode = ScaleMode.YByTime; + accessory.Method.SendDraw(DrawModeEnum.Default, DrawTypeEnum.Rect, dp); + } + + [ScriptMethod(name: "P3 45448 火环", eventType: EventTypeEnum.StartCasting, eventCondition: ["ActionId:45448"])] + public void P3_DrawDonut_45448(Event @event, ScriptAccessory accessory) + { + _acc = accessory; + _lastMechanicTicks = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(); + + int duration = 5000; + if (int.TryParse(@event["DurationMilliseconds"], out var dur) && dur > 0) + duration = dur; + + var dp = accessory.Data.GetDefaultDrawProperties(); + dp.Name = $"P3_Donut_45448_{DateTime.Now.Ticks}"; + dp.Position = @event.SourcePosition; + dp.Scale = new Vector2(50f); + dp.InnerScale = new Vector2(10f); + dp.Radian = float.Pi * 2; + dp.Color = accessory.Data.DefaultDangerColor; + dp.DestoryAt = duration; + dp.ScaleMode = ScaleMode.ByTime; + accessory.Method.SendDraw(DrawModeEnum.Default, DrawTypeEnum.Donut, dp); + + if (_p3EnbuGroupType == 1) + QTTS("分摊"); + else if (_p3EnbuGroupType == 2) + QTTS("分散"); + + _p3EnbuGroupType = 0; + _p3EnbuSide = 0; + } + + [ScriptMethod(name: "P3 回转幻舞-读条", eventType: EventTypeEnum.StartCasting, eventCondition: ["ActionId:regex:^(45479|45478)$"])] + public void P3_KaitenGenbu_Cast(Event @event, ScriptAccessory accessory) + { + _acc = accessory; + _lastMechanicTicks = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(); + + if (!int.TryParse(@event["ActionId"], out var aid)) return; + _p3KaitenGenbuCast = aid; + } + + [ScriptMethod(name: "P3 回转幻舞-TargetIcon", eventType: EventTypeEnum.TargetIcon, eventCondition: ["Id:regex:^(0274|0275)$"])] + public void P3_KaitenGenbu_TargetIcon(Event @event, ScriptAccessory accessory) + { + if (_p3KaitenGenbuCast != 45479 && _p3KaitenGenbuCast != 45478) return; + + string iconId = @event["Id"]; + string rawTargetId = @event["TargetId"]; + if (string.IsNullOrWhiteSpace(rawTargetId)) rawTargetId = @event["TargetID"]; + if (string.IsNullOrWhiteSpace(iconId) || string.IsNullOrWhiteSpace(rawTargetId)) return; + + string normalizedTargetId = rawTargetId.StartsWith("0x", StringComparison.OrdinalIgnoreCase) + ? rawTargetId.Substring(2) + : rawTargetId; + if (!ulong.TryParse(normalizedTargetId, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out var tid)) return; + + var obj = accessory.Data.Objects.SearchById(tid); + if (obj == null) return; + + float rotation = obj.Rotation; + if (_p3KaitenGenbuCast == 45479) + { + if (iconId == "0275") + rotation += MathF.PI; + else if (iconId != "0274") + return; + } + else + { + if (iconId == "0274") + rotation += MathF.PI; + else if (iconId != "0275") + return; + } + + var dp = accessory.Data.GetDefaultDrawProperties(); + dp.Name = $"P3_KaitenGenbu_{iconId}_{tid}_{DateTime.Now.Ticks}"; + dp.Position = obj.Position; + dp.Rotation = rotation; + dp.Scale = new Vector2(40f, 40f); + dp.Color = accessory.Data.DefaultDangerColor; + dp.DestoryAt = 11000; + dp.ScaleMode = ScaleMode.YByTime; + accessory.Method.SendDraw(DrawModeEnum.Default, DrawTypeEnum.Rect, dp); + } + + [ScriptMethod(name: "P3 45536 TTS", eventType: EventTypeEnum.StartCasting, eventCondition: ["ActionId:45536"])] + public void P3_TTS_45536(Event @event, ScriptAccessory accessory) + { + _acc = accessory; + _lastMechanicTicks = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(); + QTTS("分散"); + } + + [ScriptMethod(name: "P3 45538/45540 TTS", eventType: EventTypeEnum.StartCasting, eventCondition: ["ActionId:regex:^(45538|45540)$"])] + public void P3_TTS_45538_45540(Event @event, ScriptAccessory accessory) + { + _acc = accessory; + _lastMechanicTicks = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(); + QTTS("22分摊"); + } + + [ScriptMethod(name: "P3 45439/45438 半场刀", eventType: EventTypeEnum.StartCasting, eventCondition: ["ActionId:regex:^(45439|45438)$"])] + public void P3_DrawHalfCleave_45439_45438(Event @event, ScriptAccessory accessory) + { + _acc = accessory; + _lastMechanicTicks = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(); + + if (!uint.TryParse(@event["ActionId"], out var aid)) return; + + int duration = 3000; + if (int.TryParse(@event["DurationMilliseconds"], out var dur) && dur > 0) + duration += dur; + + float rotation = @event.SourceRotation; + if (aid == 45439) + rotation += 0.5f * MathF.PI; + else if (aid == 45438) + rotation -= 0.5f * MathF.PI; + else + return; + + var dp = accessory.Data.GetDefaultDrawProperties(); + dp.Name = $"P3_HalfCleave_{aid}_{DateTime.Now.Ticks}"; + dp.Position = @event.SourcePosition; + dp.Rotation = rotation; + dp.Scale = new Vector2(80f, 40f); + dp.Color = accessory.Data.DefaultDangerColor; + dp.DestoryAt = duration; + dp.ScaleMode = ScaleMode.YByTime; + accessory.Method.SendDraw(DrawModeEnum.Default, DrawTypeEnum.Rect, dp); + } + + [ScriptMethod(name: "P3 45548 四向矩形", eventType: EventTypeEnum.StartCasting, eventCondition: ["ActionId:45548"])] + public void P3_DrawCrossRect_45548(Event @event, ScriptAccessory accessory) + { + _acc = accessory; + _lastMechanicTicks = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(); + + int duration = 250; + if (int.TryParse(@event["DurationMilliseconds"], out var dur) && dur > 0) + duration += dur; + + float baseRotation = @event.SourceRotation; + for (int i = 0; i < 4; i++) + { + var dp = accessory.Data.GetDefaultDrawProperties(); + dp.Name = $"P3_CrossRect_45548_{i}_{DateTime.Now.Ticks}"; + dp.Position = @event.SourcePosition; + dp.Rotation = baseRotation + i * 0.5f * MathF.PI; + dp.Scale = new Vector2(10f, 40f); + dp.Color = accessory.Data.DefaultDangerColor; + dp.DestoryAt = duration; + dp.ScaleMode = ScaleMode.YByTime; + accessory.Method.SendDraw(DrawModeEnum.Default, DrawTypeEnum.Rect, dp); + } + } + + [ScriptMethod(name: "P3 4563 时间轴记录", eventType: EventTypeEnum.PlayActionTimeline, eventCondition: ["Id:4563"])] + public void P3_RecordPlayActionTimeline_4563(Event @event, ScriptAccessory accessory) + { + _acc = accessory; + _lastMechanicTicks = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(); + + if (_p3Timeline4563State != 0) return; + + _p3Timeline4563State = 1; + _p3Timeline4563Pos = @event.SourcePosition; + _p3Action45504Count = 0; + _p3Action45504Records.Clear(); + _p3Action45504TargetSourceId = 0; + _p3Action47153TargetCount = 0; + } + + [ScriptMethod(name: "P3 45504 三次记录", eventType: EventTypeEnum.ActionEffect, eventCondition: ["ActionId:45504"])] + public void P3_RecordActionEffect_45504(Event @event, ScriptAccessory accessory) + { + if (_p3Timeline4563State != 1) return; + if (_p3Action45504TargetSourceId != 0) return; + + _acc = accessory; + _lastMechanicTicks = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(); + + ulong sourceId = @event.SourceId; + if (sourceId == 0) return; + + _p3Action45504Count++; + _p3Action45504Records.Add((sourceId, @event.SourcePosition)); + + if (_p3Action45504Count < 3) return; + + var nearest = _p3Action45504Records + .OrderBy(x => Vector3.DistanceSquared(x.Pos, _p3Timeline4563Pos)) + .First(); + + _p3Action45504TargetSourceId = nearest.SourceId; + _p3Action47153TargetCount = 0; + _p3Action45504Count = 0; + _p3Action45504Records.Clear(); + } + + [ScriptMethod(name: "P3 47153 第三次淡蓝十字", eventType: EventTypeEnum.ActionEffect, eventCondition: ["ActionId:47153"])] + public void P3_ActionEffect_47153_DrawCross(Event @event, ScriptAccessory accessory) + { + if (_p3Timeline4563State != 1) return; + if (_p3Action45504TargetSourceId == 0) return; + if (@event.SourceId != _p3Action45504TargetSourceId) return; + + _acc = accessory; + _lastMechanicTicks = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(); + + _p3Action47153TargetCount++; + if (_p3Action47153TargetCount != 3) return; + + for (int i = 0; i < 4; i++) + { + var dp = accessory.Data.GetDefaultDrawProperties(); + dp.Name = $"P3_Cross47153_{i}_{DateTime.Now.Ticks}"; + dp.Position = @event.SourcePosition; + dp.Rotation = i * 0.5f * MathF.PI; + dp.Scale = new Vector2(10f, 40f); + dp.Color = GuideColor.V4; + dp.DestoryAt = 10000; + dp.ScaleMode = ScaleMode.None; + accessory.Method.SendDraw(DrawModeEnum.Default, DrawTypeEnum.Rect, dp); + } + + _p3Timeline4563State = 2; + _p3Action45504TargetSourceId = 0; + _p3Action47153TargetCount = 0; + _p3Action45504Count = 0; + _p3Action45504Records.Clear(); + } + + [ScriptMethod(name: "P3 四联炎舞-读条", eventType: EventTypeEnum.StartCasting, eventCondition: ["ActionId:regex:^(45468|45467)$"])] + public void P3_QuadEnbu_Cast(Event @event, ScriptAccessory accessory) + { + _acc = accessory; + _lastMechanicTicks = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(); + + if (!uint.TryParse(@event["ActionId"], out var aid)) return; + + if (!string.IsNullOrWhiteSpace(_p3QuadEnbuCurrentDrawName)) + accessory.Method.RemoveDraw(_p3QuadEnbuCurrentDrawName); + if (!string.IsNullOrWhiteSpace(_p3QuadEnbuCurrentCircleDrawName)) + accessory.Method.RemoveDraw(_p3QuadEnbuCurrentCircleDrawName); + + _p3QuadEnbuCapture = 1; + _p3QuadEnbuCastId = aid; + _p3QuadEnbuIcons.Clear(); + _p3QuadEnbuStacks.Clear(); + _p3QuadEnbuCurrentStep = 0; + _p3QuadEnbuCurrentRotation = 0f; + _p3QuadEnbuSourceId = 0; + _p3QuadEnbuCurrentDrawName = string.Empty; + _p3QuadEnbuCurrentCircleDrawName = string.Empty; + _p3QuadEnbuCurrentDrawAt = 0; + } + + [ScriptMethod(name: "P3 四联炎舞-TargetIcon记录", eventType: EventTypeEnum.TargetIcon, eventCondition: ["Id:regex:^(0270|0271|0284|0285)$"])] + public void P3_QuadEnbu_TargetIcon(Event @event, ScriptAccessory accessory) + { + if (_p3QuadEnbuCapture != 1) return; + if (_p3QuadEnbuIcons.Count >= 4) return; + + string iconId = @event["Id"]; + if (string.IsNullOrWhiteSpace(iconId)) return; + + _p3QuadEnbuIcons.Add(iconId); + P3_TryStartQuadEnbu(accessory); + } + + [ScriptMethod(name: "P3 四联炎舞-Status记录", eventType: EventTypeEnum.StatusAdd, eventCondition: ["StatusID:2970", "StackCount:regex:^(244|245)$"])] + public void P3_QuadEnbu_Status(Event @event, ScriptAccessory accessory) + { + if (_p3QuadEnbuCapture != 1) return; + if (_p3QuadEnbuStacks.Count >= 4) return; + + string stackCount = @event["StackCount"]; + if (string.IsNullOrWhiteSpace(stackCount)) return; + + _p3QuadEnbuStacks.Add(stackCount); + P3_TryStartQuadEnbu(accessory); + } + + [ScriptMethod(name: "P3 四联炎舞-ActionEffect推进", eventType: EventTypeEnum.ActionEffect, eventCondition: ["ActionId:regex:^(45469|45470|45471|45472)$"])] + public void P3_QuadEnbu_ActionEffect(Event @event, ScriptAccessory accessory) + { + if (string.IsNullOrWhiteSpace(_p3QuadEnbuCurrentDrawName) && string.IsNullOrWhiteSpace(_p3QuadEnbuCurrentCircleDrawName)) return; + + long now = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(); + if (_p3QuadEnbuCurrentDrawAt != 0 && now - _p3QuadEnbuCurrentDrawAt > 10000) + { + P3_ResetQuadEnbu(); + return; + } + + if (!string.IsNullOrWhiteSpace(_p3QuadEnbuCurrentDrawName)) + accessory.Method.RemoveDraw(_p3QuadEnbuCurrentDrawName); + if (!string.IsNullOrWhiteSpace(_p3QuadEnbuCurrentCircleDrawName)) + accessory.Method.RemoveDraw(_p3QuadEnbuCurrentCircleDrawName); + _p3QuadEnbuCurrentDrawName = string.Empty; + _p3QuadEnbuCurrentCircleDrawName = string.Empty; + _p3QuadEnbuCurrentDrawAt = 0; + + if (_p3QuadEnbuCurrentStep < 4) + { + P3_DrawQuadEnbuStep(accessory); + return; + } + + P3_ResetQuadEnbu(); + } + + private void P3_TryStartQuadEnbu(ScriptAccessory accessory) + { + if (_p3QuadEnbuCapture != 1) return; + if (_p3QuadEnbuIcons.Count < 4 || _p3QuadEnbuStacks.Count < 4) return; + + _p3QuadEnbuCapture = 0; + + var sourceObj = accessory.Data.Objects.FirstOrDefault(x => x.DataId == 19056); + if (sourceObj == null) + { + P3_ResetQuadEnbu(); + return; + } + + _p3QuadEnbuSourceId = sourceObj.EntityId; + _p3QuadEnbuCurrentRotation = sourceObj.Rotation; + _p3QuadEnbuCurrentStep = 0; + P3_DrawQuadEnbuStep(accessory); + } + + private void P3_DrawQuadEnbuStep(ScriptAccessory accessory) + { + if (_p3QuadEnbuCurrentStep >= 4) return; + if (_p3QuadEnbuSourceId == 0) return; + + string iconId = _p3QuadEnbuIcons[_p3QuadEnbuCurrentStep]; + string stackCount = _p3QuadEnbuStacks[_p3QuadEnbuCurrentStep]; + float drawRotation; + + if (_p3QuadEnbuCastId == 45468) + { + if (iconId == "0270") + { + drawRotation = _p3QuadEnbuCurrentRotation; + _p3QuadEnbuCurrentRotation -= MathF.PI; + } + else if (iconId == "0271") + { + drawRotation = _p3QuadEnbuCurrentRotation + MathF.PI; + _p3QuadEnbuCurrentRotation += MathF.PI; + } + else + { + P3_ResetQuadEnbu(); + return; + } + } + else if (_p3QuadEnbuCastId == 45467) + { + if (iconId == "0284") + { + drawRotation = _p3QuadEnbuCurrentRotation + MathF.PI; + _p3QuadEnbuCurrentRotation -= MathF.PI; + } + else if (iconId == "0285") + { + drawRotation = _p3QuadEnbuCurrentRotation; + _p3QuadEnbuCurrentRotation += MathF.PI; + } + else + { + P3_ResetQuadEnbu(); + return; + } + } + else + { + P3_ResetQuadEnbu(); + return; + } + + var sourceObj = accessory.Data.Objects.SearchById(_p3QuadEnbuSourceId); + if (sourceObj == null) + { + P3_ResetQuadEnbu(); + return; + } + + var dp = accessory.Data.GetDefaultDrawProperties(); + _p3QuadEnbuCurrentDrawName = $"P3_QuadEnbu_{_p3QuadEnbuCurrentStep}_{DateTime.Now.Ticks}"; + dp.Name = _p3QuadEnbuCurrentDrawName; + dp.Position = sourceObj.Position; + dp.Rotation = drawRotation; + dp.Scale = new Vector2(40f, 40f); + dp.Color = accessory.Data.DefaultDangerColor; + dp.DestoryAt = 10000; + dp.ScaleMode = ScaleMode.None; + accessory.Method.SendDraw(DrawModeEnum.Default, DrawTypeEnum.Rect, dp); + + var dpCircle = accessory.Data.GetDefaultDrawProperties(); + _p3QuadEnbuCurrentCircleDrawName = $"P3_QuadEnbuCircle_{_p3QuadEnbuCurrentStep}_{DateTime.Now.Ticks}"; + dpCircle.Name = _p3QuadEnbuCurrentCircleDrawName; + dpCircle.Owner = _p3QuadEnbuSourceId; + dpCircle.CentreResolvePattern = stackCount == "244" + ? PositionResolvePatternEnum.PlayerNearestOrder + : PositionResolvePatternEnum.PlayerFarestOrder; + dpCircle.CentreOrderIndex = 1; + dpCircle.Scale = new Vector2(3f); + dpCircle.Color = accessory.Data.DefaultDangerColor; + dpCircle.DestoryAt = 10000; + dpCircle.ScaleMode = ScaleMode.None; + accessory.Method.SendDraw(DrawModeEnum.Default, DrawTypeEnum.Circle, dpCircle); + + _p3QuadEnbuCurrentDrawAt = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(); + + _p3QuadEnbuCurrentStep++; + } + + private void P3_ResetQuadEnbu() + { + _p3QuadEnbuCapture = 0; + _p3QuadEnbuCastId = 0; + _p3QuadEnbuIcons.Clear(); + _p3QuadEnbuStacks.Clear(); + _p3QuadEnbuCurrentStep = 0; + _p3QuadEnbuCurrentRotation = 0f; + _p3QuadEnbuSourceId = 0; + _p3QuadEnbuCurrentDrawName = string.Empty; + _p3QuadEnbuCurrentCircleDrawName = string.Empty; + _p3QuadEnbuCurrentDrawAt = 0; + } + #endregion + + [ScriptMethod(name: "TargetIcon0014 同行同列 危险矩形 (异步)", eventType: EventTypeEnum.TargetIcon, eventCondition: ["Id:0014"])] + public void OnTargetIcon_0014_RowColDanger(Event @event, ScriptAccessory accessory) + { + string rawTargetId = @event["TargetId"]; + if (string.IsNullOrWhiteSpace(rawTargetId)) rawTargetId = @event["TargetID"]; + if (string.IsNullOrWhiteSpace(rawTargetId)) return; + string normalizedTargetId = rawTargetId.StartsWith("0x", StringComparison.OrdinalIgnoreCase) + ? rawTargetId.Substring(2) + : rawTargetId; + if (string.IsNullOrWhiteSpace(normalizedTargetId)) return; + + if (!ulong.TryParse(normalizedTargetId, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out var tid)) return; + ulong me = accessory.Data.Me; + bool isSelf = me != 0 && tid == me; + if (Debug2161To2164) + accessory.Method.SendChat($"/e [0014DBG] target={normalizedTargetId} tid={tid} me={me} self={isSelf}"); + if (isSelf) return; + + const float minX = 355f; + const float minZ = 510f; + const float cellSize = 8f; + const int totalMs = 10000; + Vector4 presetColor = new Vector4(1.0f, 1.0f, 0.0f, 0.5f); + int startRow = -1; + int startCol = -1; + var obj = accessory.Data.Objects.SearchById(tid); + if (obj != null) + { + Vector3 pos = obj.Position; + startCol = (int)MathF.Floor((pos.X - minX) / cellSize); + startRow = (int)MathF.Floor((pos.Z - minZ) / cellSize); + if (startCol < 0) startCol = 0; else if (startCol > 4) startCol = 4; + if (startRow < 0) startRow = 0; else if (startRow > 4) startRow = 4; + + var rowPreset = _icon0014RowPreset[startRow]; + var colPreset = _icon0014ColPreset[startCol]; + var dpRow = accessory.Data.GetDefaultDrawProperties(); + dpRow.Name = $"{rowPreset.Name}_{normalizedTargetId}"; + dpRow.Position = rowPreset.Position; + dpRow.Rotation = rowPreset.Rotation; + dpRow.Scale = rowPreset.Scale; + dpRow.Color = presetColor; + dpRow.DestoryAt = totalMs; + dpRow.ScaleMode = ScaleMode.None; + accessory.Method.SendDraw(DrawModeEnum.Default, DrawTypeEnum.Rect, dpRow); + + var dpCol = accessory.Data.GetDefaultDrawProperties(); + dpCol.Name = $"{colPreset.Name}_{normalizedTargetId}"; + dpCol.Position = colPreset.Position; + dpCol.Rotation = colPreset.Rotation; + dpCol.Scale = colPreset.Scale; + dpCol.Color = presetColor; + dpCol.DestoryAt = totalMs; + dpCol.ScaleMode = ScaleMode.None; + accessory.Method.SendDraw(DrawModeEnum.Default, DrawTypeEnum.Rect, dpCol); + } + + _ = DrawTargetIcon0014RowColDangerAsync(normalizedTargetId, startRow, startCol, accessory); + } + + private async Task DrawTargetIcon0014RowColDangerAsync(string targetIdHex, int startRow, int startCol, ScriptAccessory accessory) + { + if (!ulong.TryParse(targetIdHex, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out var tid)) return; + + const float minX = 355f; + const float minZ = 510f; + const float cellSize = 8f; + const int intervalMs = 50; + const int totalMs = 8000; + Vector4 presetColor = new Vector4(1.0f, 1.0f, 0.0f, 0.5f); + + var obj = accessory.Data.Objects.SearchById(tid); + int lastRow = startRow; + int lastCol = startCol; + string rowDrawName = startRow >= 0 ? $"{_icon0014RowPreset[startRow].Name}_{targetIdHex}" : string.Empty; + string colDrawName = startCol >= 0 ? $"{_icon0014ColPreset[startCol].Name}_{targetIdHex}" : string.Empty; + + int elapsed = 0; + while (elapsed < totalMs) + { + if (obj != null) + { + Vector3 pos = obj.Position; + + int col = (int)MathF.Floor((pos.X - minX) / cellSize); + int row = (int)MathF.Floor((pos.Z - minZ) / cellSize); + if (col < 0) col = 0; else if (col > 4) col = 4; + if (row < 0) row = 0; else if (row > 4) row = 4; + + if (col != lastCol || row != lastRow) + { + int remain = totalMs - elapsed; + if (remain < intervalMs) remain = intervalMs; + if (row != lastRow) + { + if (!string.IsNullOrWhiteSpace(rowDrawName)) accessory.Method.RemoveDraw(rowDrawName); + var rowPreset = _icon0014RowPreset[row]; + rowDrawName = $"{rowPreset.Name}_{targetIdHex}"; + var dpRow = accessory.Data.GetDefaultDrawProperties(); + dpRow.Name = rowDrawName; + dpRow.Position = rowPreset.Position; + dpRow.Rotation = rowPreset.Rotation; + dpRow.Scale = rowPreset.Scale; + dpRow.Color = presetColor; + dpRow.DestoryAt = remain; + dpRow.ScaleMode = ScaleMode.None; + accessory.Method.SendDraw(DrawModeEnum.Default, DrawTypeEnum.Rect, dpRow); + lastRow = row; + } + if (col != lastCol) + { + if (!string.IsNullOrWhiteSpace(colDrawName)) accessory.Method.RemoveDraw(colDrawName); + var colPreset = _icon0014ColPreset[col]; + colDrawName = $"{colPreset.Name}_{targetIdHex}"; + var dpCol = accessory.Data.GetDefaultDrawProperties(); + dpCol.Name = colDrawName; + dpCol.Position = colPreset.Position; + dpCol.Rotation = colPreset.Rotation; + dpCol.Scale = colPreset.Scale; + dpCol.Color = presetColor; + dpCol.DestoryAt = remain; + dpCol.ScaleMode = ScaleMode.None; + accessory.Method.SendDraw(DrawModeEnum.Default, DrawTypeEnum.Rect, dpCol); + lastCol = col; + } + } + } + + await Task.Delay(intervalMs); + elapsed += intervalMs; + obj = accessory.Data.Objects.SearchById(tid); + } + + if (!string.IsNullOrWhiteSpace(rowDrawName)) accessory.Method.RemoveDraw(rowDrawName); + if (!string.IsNullOrWhiteSpace(colDrawName)) accessory.Method.RemoveDraw(colDrawName); + } + #endregion + } +} + + + + + + + + + + + + + + diff --git "a/a16239438/\346\240\274\350\216\261\346\235\250\346\213\211\346\263\242\345\260\224\346\255\274\346\256\233\346\210\230 Hell on Rails(Extreme).cs" "b/a16239438/\346\240\274\350\216\261\346\235\250\346\213\211\346\263\242\345\260\224\346\255\274\346\256\233\346\210\230 Hell on Rails(Extreme).cs" new file mode 100644 index 0000000..49af0ca --- /dev/null +++ "b/a16239438/\346\240\274\350\216\261\346\235\250\346\213\211\346\263\242\345\260\224\346\255\274\346\256\233\346\210\230 Hell on Rails(Extreme).cs" @@ -0,0 +1,567 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Numerics; +using System.Threading.Tasks; +using KodakkuAssist.Script; +using KodakkuAssist.Module.GameEvent; +using KodakkuAssist.Module.Draw; +using KodakkuAssist.Module.GameEvent.Struct; +using Dalamud.Utility.Numerics; + +using KodakkuAssist.Data; +using KodakkuAssist.Extensions; + +namespace RyougiMioScriptNamespace +{ + [ScriptType(name: "格莱杨拉波尔歼殛战 Hell on Rails(Extreme)", territorys: [1308], guid: "b9dfc210-add4-d464-3030-e9e294b384ff", version: "0.0.0.2", author: "RyougiMio", note: "Bug fix")] + public class Script1308 + { + // ==================== 用户设置区域 ==================== + + [UserSetting("常用危险色")] + public ScriptColor DangerColor { get; set; } = new ScriptColor() { V4 = new Vector4(1.0f, 0.0f, 0.0f, 1.0f) }; + + [UserSetting("常用安全色")] + public ScriptColor SafeColor { get; set; } = new ScriptColor() { V4 = new Vector4(0.0f, 1.0f, 0.0f, 1.0f) }; + [UserSetting("当前阶段 (PhaseCount) - 仅调试用")] + public int phaseCount { get; set; } = 1; + + [UserSetting("技能计数 (SkillCount) - 仅调试用")] + public int skillCount { get; set; } = 0; + private int skillCount1 = 0; + + /// + /// + public void Init(ScriptAccessory accessory) + { + // 重置变量 + phaseCount = 1; + skillCount = 0; // 重置计数器 + skillCount1 = 0; + accessory.Method.RemoveDraw(".*"); + + accessory.Method.SendChat("/e HoR(EX) Initialized."); + } + + [ScriptMethod(name: "计数更新_45663/45664", eventType: EventTypeEnum.StartCasting, eventCondition: ["ActionId:regex:^(45663|45664)$"], userControl: false)] + public void UpdatePhaseBySkillCount(Event @event, ScriptAccessory accessory) + { + skillCount++; + + + + if (skillCount == 4) + { + this.phaseCount++; + + + } + if (skillCount >= 5) + { + this.phaseCount=6; + + + } + } + + [ScriptMethod(name: "雷光环(P1)", eventType: EventTypeEnum.AddCombatant, eventCondition: ["DataId:19000"], userControl: true)] + public void DrawLightningRect(Event @event, ScriptAccessory accessory) + { + // 过滤空中的雷光环 + if (@event.SourcePosition.Y > 4.0f || phaseCount >2) + { + return; + } + + + var dpGrow = accessory.Data.GetDefaultDrawProperties(); + dpGrow.Name = $"LightningRect_Grow_{@event.SourceId}"; + dpGrow.Owner = @event.SourceId; + dpGrow.Color = accessory.Data.DefaultDangerColor; + dpGrow.Offset = new Vector3(0, 0, -15); // Z轴偏移 -15 + dpGrow.DestoryAt = 7030; // 生长耗时 5.7秒 + + + dpGrow.ScaleMode = ScaleMode.XByTime; + dpGrow.Scale = new Vector2(5f, 30f); + + accessory.Method.SendDraw(DrawModeEnum.Default, DrawTypeEnum.Straight, dpGrow); + + + var dpStatic = accessory.Data.GetDefaultDrawProperties(); + dpStatic.Name = $"LightningRect_Static_{@event.SourceId}"; + dpStatic.Owner = @event.SourceId; + dpStatic.Color = accessory.Data.DefaultDangerColor; + dpStatic.Offset = new Vector3(0, 0, -15); // 保持相同的偏移 + + + dpStatic.ScaleMode = ScaleMode.None; + dpStatic.Scale = new Vector2(5f, 30f); // 保持第一阶段结束时的大小 + + // 时间控制: + dpStatic.Delay = 7030; // 等第一阶段结束(填满)后再显示 + dpStatic.DestoryAt = 970; // 持续显示 1秒 + + accessory.Method.SendDraw(DrawModeEnum.Default, DrawTypeEnum.Straight, dpStatic); + } + [ScriptMethod(name: "雷光环(P5)", eventType: EventTypeEnum.AddCombatant, eventCondition: ["DataId:19000"], userControl: true)] + public void DrawLightningRect1(Event @event, ScriptAccessory accessory) + { + // 过滤空中的雷光环 + if ((@event.SourcePosition.X > 95.0f&&@event.SourcePosition.X <100.0f )|| phaseCount <2) + { + return; + } + + + var dpGrow = accessory.Data.GetDefaultDrawProperties(); + dpGrow.Name = $"LightningRect_Grow_{@event.SourceId}"; + dpGrow.Owner = @event.SourceId; + dpGrow.Color = accessory.Data.DefaultDangerColor; + dpGrow.Offset = new Vector3(0, 0, -15); // Z轴偏移 -15 + dpGrow.DestoryAt = 7030; // 生长耗时 5.7秒 + + + dpGrow.ScaleMode = ScaleMode.XByTime; + dpGrow.Scale = new Vector2(5f, 30f); + + accessory.Method.SendDraw(DrawModeEnum.Default, DrawTypeEnum.Straight, dpGrow); + + + + var dpStatic = accessory.Data.GetDefaultDrawProperties(); + dpStatic.Name = $"LightningRect_Static_{@event.SourceId}"; + dpStatic.Owner = @event.SourceId; + dpStatic.Color = accessory.Data.DefaultDangerColor; + dpStatic.Offset = new Vector3(0, 0, -15); // 保持相同的偏移 + + + dpStatic.ScaleMode = ScaleMode.None; + dpStatic.Scale = new Vector2(5f, 30f); // 保持第一阶段结束时的大小 + + // 时间控制: + dpStatic.Delay = 7030; // 等第一阶段结束(填满)后再显示 + dpStatic.DestoryAt = 970; // 持续显示 1秒 + + accessory.Method.SendDraw(DrawModeEnum.Default, DrawTypeEnum.Straight, dpStatic); + } + + [ScriptMethod(name: "超增压急行", eventType: EventTypeEnum.StartCasting, eventCondition: ["ActionId:45670"], userControl: true)] + public void DrawBossArrow(Event @event, ScriptAccessory accessory) + { + + var dp = accessory.Data.GetDefaultDrawProperties(); + + dp.Name = $"Boss_Arrow_{@event.SourceId}"; + dp.Color = new Vector4(1.0f, 1.0f, 0.0f, 1.0f); // 使用默认危险色 + + // 固定中心位置 (100, 0, 100) + dp.Position = new Vector3(100, 0, 40+phaseCount*50); + + + dp.Rotation = 0; + + // 持续 6500 毫秒 + dp.DestoryAt = 7000; + + + dp.ScaleMode = ScaleMode.YByTime; + + dp.Scale = new Vector2(10f, 20f); + // ==================================================== + + accessory.Method.SendDraw(DrawModeEnum.Default, DrawTypeEnum.Arrow, dp); + } + [ScriptMethod(name: "超增压抽雾", eventType: EventTypeEnum.StartCasting, eventCondition: ["ActionId:45696"], userControl: true)] + public void DrawBossArrow_f(Event @event, ScriptAccessory accessory) + { + + var dp = accessory.Data.GetDefaultDrawProperties(); + + dp.Name = $"Boss_Arrow_{@event.SourceId}"; + dp.Color = new Vector4(1.0f, 1.0f, 0.0f, 1.0f); // 使用默认危险色 + + + dp.Position = new Vector3(100, 0, 60+phaseCount*50); + + + dp.Rotation = 3.14159f; + + // 持续 6500 毫秒 + dp.DestoryAt = 7000; + + dp.ScaleMode = ScaleMode.YByTime; + + // X = 箭头的宽度 = 10 + // Y = 增长速度 = 总长度(10) / 时间(6.5秒) + dp.Scale = new Vector2(10f, 20f); + // ==================================================== + + accessory.Method.SendDraw(DrawModeEnum.Default, DrawTypeEnum.Arrow, dp); + } + + [ScriptMethod(name: "超增压", eventType: EventTypeEnum.StartCasting, eventCondition: ["ActionId:regex:^(45663|45664)$"])] + public void DrawPhase1Circles(Event @event, ScriptAccessory accessory) + { + // 1. 增加计数 + skillCount1 = skillCount1+1; + long delay = 0; + long duration = 5000; + + + + // 第一阶段 + if (skillCount1 == 1||skillCount1 == 2) + { + delay = 12000; + } + // 第二阶段 (phaseCount > 1) + else + { + // 严格照搬你的数值要求 + if (skillCount1 == 3) + { + delay = 19500; + } + else if (skillCount1 == 4) + { + delay = 21500; + } + else if (skillCount1 == 5) + { + delay = 52500; + } + else if (skillCount1 == 6) + { + delay = 49500; + } + else + { + + return; + } + } + + + if (@event.ActionId == 45663) + { + foreach (var playerId in accessory.Data.PartyList) + { + var player = accessory.Data.Objects.SearchById(playerId); + if (player == null || player.IsDead) continue; + + var dp = accessory.Data.GetDefaultDrawProperties(); + dp.Name = $"Circle_45663_{playerId}_{@event.SourceId}"; + dp.Owner = playerId; + dp.Color = accessory.Data.DefaultDangerColor; // 红色 + + dp.Delay = delay; + dp.DestoryAt = duration; + + + dp.ScaleMode = ScaleMode.ByTime; + dp.Scale = new Vector2(5.0f, 5.0f); + + accessory.Method.SendDraw(DrawModeEnum.Default, DrawTypeEnum.Circle, dp); + } + } + + else if (@event.ActionId == 45664) + { + var dp = accessory.Data.GetDefaultDrawProperties(); + dp.Name = $"Circle_45664_{accessory.Data.Me}_{@event.SourceId}"; + dp.Owner = accessory.Data.Me; + dp.Color = accessory.Data.DefaultSafeColor; // 绿色 + + dp.Delay = delay; + dp.DestoryAt = duration; + + + dp.ScaleMode = ScaleMode.ByTime; + dp.Scale = new Vector2(5.0f, 5.0f); + + accessory.Method.SendDraw(DrawModeEnum.Default, DrawTypeEnum.Circle, dp); + } + } + [ScriptMethod(name: "护卫炮塔", eventType: EventTypeEnum.StartCasting, eventCondition: ["ActionId:regex:^(45681|45683|45686)$"], userControl: true)] + public void DrawTurretLaser(Event @event, ScriptAccessory accessory) + { + // 基础参数 + uint actionId = @event.ActionId; + Vector3 srcPos = @event.SourcePosition; + float duration = 7000f; // 7秒 + + + bool isLeftTurret = Math.Abs(srcPos.X - 85) < 2; + bool isRightTurret = Math.Abs(srcPos.X - 115) < 2; + + + + if (!isLeftTurret && !isRightTurret) return; + + // 绘图属性初始化 + var dp = accessory.Data.GetDefaultDrawProperties(); + dp.Name = $"Turret_{actionId}_{@event.SourceId}"; + dp.Color = accessory.Data.DefaultDangerColor; + dp.DestoryAt = (long)duration; + + + + float rotation = isLeftTurret ? -float.Pi / 2 : float.Pi / 2; + dp.Rotation = rotation; + dp.ScaleMode = ScaleMode.XByTime; // 开启 Y 轴(长度) 渐变 + + + + // 1. Action 45681: X 从 90 射到 110 (覆盖场中) + if (actionId == 45681) + { + + float length = 5f; + float width = 20f; + + + if(isLeftTurret){dp.Offset = new Vector3(0f, 0, -15); } + if(isRightTurret){dp.Offset = new Vector3(0, 0, -15); } + + dp.Scale = new Vector2(width, length); // Y = 速度 + dp.Owner = @event.SourceId; // 绑在炮塔上 + + accessory.Method.SendDraw(DrawModeEnum.Default, DrawTypeEnum.Straight, dp); + } + + else if (actionId == 45683) + { + + float length = 5f; + float width = 15f; + + + if(isLeftTurret){dp.Offset = new Vector3(0, 0, -12.5f); } + if(isRightTurret){dp.Offset = new Vector3(0, 0, -12.5f); } + + dp.Scale = new Vector2(width, length); + dp.Owner = @event.SourceId; + + accessory.Method.SendDraw(DrawModeEnum.Default, DrawTypeEnum.Straight, dp); + } + // 3. Action 45686: Z是他的sourceposition+-2.5 (不射出来) + + } + [ScriptMethod(name: "格莱杨拉波尔分身_标记", eventType: EventTypeEnum.AddCombatant, eventCondition: ["DataId:19329"], userControl: true)] + public void MarkClone(Event @event, ScriptAccessory accessory) + { + var dp = accessory.Data.GetDefaultDrawProperties(); + dp.Name = $"Clone_Mark_{@event.SourceId}"; + dp.Owner = @event.SourceId; // 绑定到分身上,怪动圈也动 + dp.Color = new Vector4(1.0f, 1.0f, 0.0f, 1.0f); + dp.Scale = new Vector2(2.0f); // 设置圆圈半径为 2米 + dp.DestoryAt = 120000; // + + accessory.Method.SendDraw(DrawModeEnum.Default, DrawTypeEnum.Circle, dp); + } + + [ScriptMethod(name: "格莱杨拉波尔分身_扇形", eventType: EventTypeEnum.AddCombatant, eventCondition: ["DataId:19329"], userControl: true)] + public void DrawCloneFanToTanks(Event @event, ScriptAccessory accessory) + { + // 1. 查找坦克 + var tankIds = accessory.Data.PartyList.Where(id => + { + var obj = accessory.Data.Objects.SearchById(id); + + + return obj is IBattleChara bc && bc.IsTank(); + }).ToList(); + + // 2. 绘制扇形 + foreach (var tankId in tankIds) + { + var dp = accessory.Data.GetDefaultDrawProperties(); + dp.Name = $"Clone_Fan_To_Tank_{tankId}_{@event.SourceId}"; + + dp.Owner = @event.SourceId; + dp.TargetObject = tankId; + + dp.Color = new Vector4(1.0f, 1.0f, 0.0f, 0.1f); // 红色半透明 + + dp.Radian = float.Pi / 5.143f; // 22.5度 + + dp.Scale = new Vector2(60.0f); + dp.DestoryAt = 120000; + + accessory.Method.SendDraw(DrawModeEnum.Default, DrawTypeEnum.Fan, dp); + } + } + [ScriptMethod(name: "格莱杨拉波尔分身_分散分摊", eventType: EventTypeEnum.TargetIcon, eventCondition: ["Id:regex:^(027D|027E)$"], userControl: true)] + public void DrawSpreadStackByIcon(Event @event, ScriptAccessory accessory) + { + // 基础参数 + string iconId = @event["Id"]; + long duration = 6500; // 6.5秒 + + + if (iconId == "027E") + { + foreach (var playerId in accessory.Data.PartyList) + { + var player = accessory.Data.Objects.SearchById(playerId); + if (player == null || player.IsDead) continue; + + var dp = accessory.Data.GetDefaultDrawProperties(); + dp.Name = $"Icon_Spread_{playerId}_{@event.SourceId}"; + dp.Owner = playerId; + dp.Color = accessory.Data.DefaultDangerColor; // 红色 + + dp.DestoryAt = duration; + dp.ScaleMode = ScaleMode.ByTime; + dp.Scale = new Vector2(5.0f, 5.0f); + + accessory.Method.SendDraw(DrawModeEnum.Default, DrawTypeEnum.Circle, dp); + } + } + // 2. 处理分摊 (027E) -> 仅奶妈绿圈 + else if (iconId == "027D") + { + foreach (var playerId in accessory.Data.PartyList) + { + var player = accessory.Data.Objects.SearchById(playerId); + + // 筛选条件:对象存在 且 是战斗角色 且 是治疗职业 + if (player is IBattleChara bc && bc.IsHealer()) + { + var dp = accessory.Data.GetDefaultDrawProperties(); + dp.Name = $"Icon_Stack_Healer_{playerId}_{@event.SourceId}"; + dp.Owner = playerId; + dp.Color = accessory.Data.DefaultSafeColor; // 绿色 + + dp.DestoryAt = duration; + dp.ScaleMode = ScaleMode.ByTime; + dp.Scale = new Vector2(5.0f, 5.0f); + + accessory.Method.SendDraw(DrawModeEnum.Default, DrawTypeEnum.Circle, dp); + } + } + } + } + [ScriptMethod(name: "雷光一闪", eventType: EventTypeEnum.StartCasting, eventCondition: ["ActionId:45666"], userControl: true)] + public void DrawLightningFlash(Event @event, ScriptAccessory accessory) + { + // 获取 EffectPosition + var pos = @event.EffectPosition; + + + var drawPos = new Vector3(pos.X, 0, pos.Z); + + var dp = accessory.Data.GetDefaultDrawProperties(); + dp.Name = $"LightningFlash_{@event.SourceId}_{@event.Id}"; + dp.Position = drawPos; + dp.Color = DangerColor.V4; // 使用用户设置的危险色 + dp.Scale = new Vector2(4.0f); // 半径 5 + + dp.Delay = 0; // 延时 3秒 + dp.DestoryAt = 3000; + + accessory.Method.SendDraw(DrawModeEnum.Default, DrawTypeEnum.Circle, dp); + } + [ScriptMethod(name: "前照光", eventType: EventTypeEnum.StartCasting, eventCondition: ["ActionId:45690"], userControl: true)] + public void DrawHeadlightRects(Event @event, ScriptAccessory accessory) + { + var srcPos = @event.SourcePosition; + var srcRot = @event.SourceRotation; + + // 矩形基础参数 + float width = 20f; + float length = 30f; + + + Vector3 rectOffset = new Vector3(0, 0, -10f); + + Vector4 blueColor = new Vector4(0.0f, 1.0f, 1.0f, 0.2f); + var dp1 = accessory.Data.GetDefaultDrawProperties(); + dp1.Name = $"Headlight_High_{@event.SourceId}"; + dp1.Position = new Vector3(srcPos.X, 5.0f, srcPos.Z); + dp1.Rotation = srcRot; + dp1.Color = accessory.Data.DefaultDangerColor; + + dp1.Offset = rectOffset; + dp1.DestoryAt = 6700; + + // --- 渐变设置 --- + dp1.ScaleMode = ScaleMode.YByTime; // 随时间增长长度 + + dp1.Scale = new Vector2(width, length); + + accessory.Method.SendDraw(DrawModeEnum.Default, DrawTypeEnum.Rect, dp1); + + + var dp2 = accessory.Data.GetDefaultDrawProperties(); + dp2.Name = $"Headlight_Low_{@event.SourceId}"; + dp2.Position = new Vector3(srcPos.X, 0.0f, srcPos.Z); + dp2.Rotation = srcRot; + dp2.Color = blueColor; + + dp2.Offset = rectOffset; + + dp2.Delay = 6700; // 等待第一阶段结束 + dp2.DestoryAt = 2700; + + // --- 静态设置 --- + dp2.ScaleMode = ScaleMode.YByTime; + + dp2.Scale = new Vector2(width, length); + + accessory.Method.SendDraw(DrawModeEnum.Default, DrawTypeEnum.Rect, dp2); + } + [ScriptMethod(name: "雷鸣", eventType: EventTypeEnum.StartCasting, eventCondition: ["ActionId:45687"], userControl: true)] + public void DrawHeadlightRects1(Event @event, ScriptAccessory accessory) + { + var srcPos = @event.SourcePosition; + var srcRot = @event.SourceRotation; + + // 矩形基础参数 + float width = 20f; + float length = 30f; + + // 偏移量 (保持你的设置) + Vector3 rectOffset = new Vector3(0, 0, -10f); + + Vector4 blueColor = new Vector4(0.0f, 1.0f, 1.0f, 0.2f); + var dp1 = accessory.Data.GetDefaultDrawProperties(); + dp1.Name = $"Headlight_High_{@event.SourceId}"; + dp1.Position = new Vector3(srcPos.X, 0.0f, srcPos.Z); + dp1.Rotation = srcRot; + dp1.Color = blueColor; + + dp1.Offset = rectOffset; + dp1.DestoryAt = 6700; + + // --- 渐变设置 --- + dp1.ScaleMode = ScaleMode.YByTime; // 随时间增长长度 + + dp1.Scale = new Vector2(width, length); + + accessory.Method.SendDraw(DrawModeEnum.Default, DrawTypeEnum.Rect, dp1); + + + var dp2 = accessory.Data.GetDefaultDrawProperties(); + dp2.Name = $"Headlight_Low_{@event.SourceId}"; + dp2.Position = new Vector3(srcPos.X, 5.0f, srcPos.Z); + dp2.Rotation = srcRot; + dp2.Color = accessory.Data.DefaultDangerColor; + + dp2.Offset = rectOffset; + + dp2.Delay = 6700; // 等待第一阶段结束 + dp2.DestoryAt = 2700; + + + dp2.ScaleMode = ScaleMode.YByTime; + + dp2.Scale = new Vector2(width, length); + + accessory.Method.SendDraw(DrawModeEnum.Default, DrawTypeEnum.Rect, dp2); + } + + } +} \ No newline at end of file