diff --git a/Codaaaaaa/DoomTrainScripts.cs b/Codaaaaaa/DoomTrainScripts.cs new file mode 100644 index 0000000..541747b --- /dev/null +++ b/Codaaaaaa/DoomTrainScripts.cs @@ -0,0 +1,812 @@ +using System; +using System.Linq; +using System.Numerics; +using System.Collections.Generic; +using System.Threading.Tasks; +using Newtonsoft.Json; +using KodakkuAssist.Script; +using KodakkuAssist.Module.GameEvent; +using KodakkuAssist.Module.Draw; +using KodakkuAssist.Module.Draw.Manager; +using KodakkuAssist.Data; +using KodakkuAssist.Extensions; + +namespace Codaaaaaa.DoomTrainScripts; + +[ScriptType(guid: "3f8c6b2e-91c4-4a87-bd63-0b7a5f0d7e42", name: "格莱杨拉波尔歼殛战 指路+TTS", territorys: [1308], version: "0.0.0.9", author: "Codaaaaaa", note: "画图+指路+TTS。做个测试,使用前请务必调整小队顺序")] +public class NewRaid4P +{ + #region 用户设置 + [UserSetting("是否开启TTS")] public static bool TTSOpen { get; set; } = true; + #endregion + + private static readonly Vector3 Center = new(100, 0, 100); + + // ========================= + // 共享机制:每个P都要独立存一次“分摊/分散” + // ========================= + private enum 分摊分散 + { + None = 0, + Stack = 1, // 分摊 + Spread = 2, // 分散 + } + + private uint _phase = 1; + private 分摊分散 超增压 = 分摊分散.None; + + // 异世界 + private static readonly Vector2 Origin = new(-400, -400); + private const uint TargetBNpcId = 19329; + + private const int SampleCount = 7; + private const int SampleIntervalMs = 500; + private const float PredictStopT = 7.6f; + private bool 以太晶球最大近战距离 = false; + + // 其他 + // private bool 最后平台飞箱 = false; + + public void Init(ScriptAccessory sa) + { + ResetAll(); + sa.Method.RemoveDraw(".*"); + } + + private void ResetAll() + { + _phase = 1; + 超增压 = 分摊分散.None; + 以太晶球最大近战距离 = false; + } + + [ScriptMethod(name: "clear draw", eventType: EventTypeEnum.Chat, eventCondition: ["Type:Echo", "Message:KASCLEAR"], userControl: false)] + public void ClearDraw(Event evt, ScriptAccessory sa) => sa.Method.RemoveDraw(".*"); + + // ========================= + // 手动切P口令测试 + // ========================= + [ScriptMethod(name: "Set Phase 1", eventType: EventTypeEnum.Chat, eventCondition: ["Type:Echo", "Message:KASP1"], userControl: false)] + public void SetP1(Event evt, ScriptAccessory sa) => _phase = 1; + + [ScriptMethod(name: "Set Phase 2", eventType: EventTypeEnum.Chat, eventCondition: ["Type:Echo", "Message:KASP2"], userControl: false)] + public void SetP2(Event evt, ScriptAccessory sa) => _phase = 2; + + [ScriptMethod(name: "Set Phase 3", eventType: EventTypeEnum.Chat, eventCondition: ["Type:Echo", "Message:KASP3"], userControl: false)] + public void SetP3(Event evt, ScriptAccessory sa) => _phase = 3; + + [ScriptMethod(name: "Set Phase 4", eventType: EventTypeEnum.Chat, eventCondition: ["Type:Echo", "Message:KASP4"], userControl: false)] + public void SetP4(Event evt, ScriptAccessory sa) => _phase = 4; + + [ScriptMethod(name: "Set Phase 5", eventType: EventTypeEnum.Chat, eventCondition: ["Type:Echo", "Message:KASP5"], userControl: false)] + public void SetP5(Event evt, ScriptAccessory sa) => _phase = 5; + + [ScriptMethod(name: "Set Phase 6", eventType: EventTypeEnum.Chat, eventCondition: ["Type:Echo", "Message:KASP6"], userControl: false)] + public void SetP6(Event evt, ScriptAccessory sa) => _phase = 6; + + [ScriptMethod(name: "Show Phase", eventType: EventTypeEnum.Chat, eventCondition: ["Type:Echo", "Message:phase"], userControl: false)] + public void ShowPhase(Event evt, ScriptAccessory sa) => sa.Method.SendChat($"/e Current Phase: {_phase}"); + + [ScriptMethod( + name: "换p", + eventType: EventTypeEnum.StartCasting, + eventCondition: ["ActionId:regex:^(45680|45709|46489|46490)$"], + userControl: false)] + public async void 换p(Event evt, ScriptAccessory sa) + { + var actionId = evt.ActionId(); + if (actionId == 45709) _phase = 4; + else if (actionId == 46489) _phase = 5; + else if (actionId == 46490) _phase = 6; + else _phase++; + + 超增压 = 分摊分散.None; + sa.Method.TextInfo($"快跑, 前往第{_phase}平台", 5500, true); + } + + [ScriptMethod( + name: "Shared-Store-Stack", + eventType: EventTypeEnum.StartCasting, + eventCondition: ["ActionId:45664"], + userControl: false)] + public void SharedStoreStack(Event evt, ScriptAccessory sa) + { + 超增压 = 分摊分散.Stack; + // sa.Method.TextInfo($"P{_phase} 共享机制:分摊(已记录)", 2500, true); + // 你也可以 /e 广播 + // sa.Method.SendChat($"/e P{_phase} share = STACK"); + } + + [ScriptMethod( + name: "Shared-Store-Spread", + eventType: EventTypeEnum.StartCasting, + eventCondition: ["ActionId:45663"], + userControl: false)] + public void SharedStoreSpread(Event evt, ScriptAccessory sa) + { + 超增压 = 分摊分散.Spread; + if (TTSOpen) + sa.Method.EdgeTTS("待会儿分散"); + // sa.Method.SendChat($"/e P{_phase} share = SPREAD"); + } + + [ScriptMethod( + name: "雷光环", + eventType: EventTypeEnum.AddCombatant, + eventCondition: ["DataId:19000"])] + public void P1LightningRingSpawn(Event evt, ScriptAccessory sa) + { + + // sa.Method.SendChat("/e P1 机制:雷光环出现"); + var pos = evt.SourcePosition(); + + // 平台1/2 只处理 Y:0.00 + if (_phase == 1 || _phase == 2 || _phase == 6) + { + if (MathF.Abs(pos.Y - 0.0f) > 0.01f) return; + } + + + // 画矩形危险区 + if (_phase == 1 || _phase == 6){ + var dp = sa.FastDp("雷光环矩形危险区", pos, 7000, new Vector2(5, 30), safe: false); + sa.Method.SendDraw(DrawModeEnum.Default, DrawTypeEnum.Rect, dp); + } + else if (_phase == 2){ + var dp1 = sa.FastDp("雷光环矩形危险区-半透明", pos, 4000, new Vector2(5, 30), safe: false); + dp1.Color = new Vector4(dp1.Color.X, dp1.Color.Y, dp1.Color.Z, 0.2f); + sa.Method.SendDraw(DrawModeEnum.Default, DrawTypeEnum.Rect, dp1); + + var dp2 = sa.FastDp("雷光环矩形危险区-不透明", pos, 3000, new Vector2(5, 30), safe: false); + dp2.Delay = 4000; // 延后 4 秒出现 + dp2.Color = new Vector4(dp2.Color.X, dp2.Color.Y, dp2.Color.Z, 1f); + sa.Method.SendDraw(DrawModeEnum.Default, DrawTypeEnum.Rect, dp2); + } + } + + [ScriptMethod( + name: "超增压急行", + eventType: EventTypeEnum.StartCasting, + eventCondition: ["ActionId:45670"])] + public async void 超增压急行(Event evt, ScriptAccessory sa) + { + // if (_phase != 1) return; + var dp = sa.Data.GetDefaultDrawProperties(); + dp.Name = $"超增压急行"; + dp.Owner = sa.Data.Me; + dp.FixRotation = true; + dp.Rotation = 0f; + dp.Color = new Vector4(1f, 1f, 0f, 1f); // 黄色 + // dp.Color = sa.Data.DefaultSafeColor; + dp.DestoryAt = 7000; + dp.Scale = new Vector2(0.7f, 15f); + + if (TTSOpen) + sa.Method.EdgeTTS("击退"); + sa.Method.SendDraw(DrawModeEnum.Default, DrawTypeEnum.Displacement, dp); + + await Task.Delay(5500); + // TTS + switch (超增压) + { + case 分摊分散.Stack: + if (TTSOpen) + sa.Method.EdgeTTS("分摊"); + sa.Method.TextInfo("分摊", 4000, false); + break; + case 分摊分散.Spread: + if (TTSOpen) + sa.Method.EdgeTTS("分散"); + sa.Method.TextInfo("分散", 4000, false); + break; + default: + sa.Method.TextInfo("未记录分摊/分散!", 4000, true); + break; + } + + await Task.Delay(500); + + // 分散集合画图 + switch (超增压) + { + case 分摊分散.Stack: + sa.DrawStackPairLines10s(); + break; + case 分摊分散.Spread: + sa.DrawSpreadCircles10s(); + break; + } + } + + [ScriptMethod( + name: "超增压抽雾", + eventType: EventTypeEnum.StartCasting, + eventCondition: ["ActionId:45677"])] + public async void 超增压抽雾(Event evt, ScriptAccessory sa) + { + // if (_phase != 1) return; + var dp = sa.Data.GetDefaultDrawProperties(); + dp.Name = $"超增压抽雾"; + dp.Owner = sa.Data.Me; + dp.FixRotation = true; + dp.Rotation = MathF.PI; // 180 度 + dp.Color = new Vector4(1f, 1f, 0f, 1f); // 黄色 + // dp.Color = sa.Data.DefaultSafeColor; + dp.DestoryAt = 7000; + dp.Scale = new Vector2(0.7f, 15f); + + if (TTSOpen) + sa.Method.EdgeTTS("吸引"); + sa.Method.SendDraw(DrawModeEnum.Default, DrawTypeEnum.Displacement, dp); + + await Task.Delay(5500); + // TTS + switch (超增压) + { + case 分摊分散.Stack: + if (TTSOpen) + sa.Method.EdgeTTS("分摊"); + sa.Method.TextInfo("分摊", 4000, false); + break; + case 分摊分散.Spread: + if (TTSOpen) + sa.Method.EdgeTTS("分散"); + sa.Method.TextInfo("分散", 4000, false); + break; + default: + sa.Method.TextInfo("未记录分摊/分散!", 4000, true); + break; + } + + await Task.Delay(500); + + // 分散集合画图 + switch (超增压) + { + case 分摊分散.Stack: + sa.DrawStackPairLines10s(); + break; + case 分摊分散.Spread: + sa.DrawSpreadCircles10s(); + break; + } + } + + // 第二平台 + [ScriptMethod( + name: "雷转质射线", + eventType: EventTypeEnum.StartCasting, + eventCondition: ["ActionId:regex:^(45681|45683)$"])] + public void 雷转质射线(Event evt, ScriptAccessory sa) + { + var actionId = evt.ActionId(); + var length = 0f; + if(_phase == 2){ + length = 30f; + } + else{ + length = actionId == 45683 ? 20f : 5f; + } + + // if (actionId == 45681 && 最后平台飞箱 == true) { + if (actionId == 45681) { + length = 40f; + } + + var dp = sa.Data.GetDefaultDrawProperties(); + dp.Name = $"雷转质射线-{actionId}-{evt.SourceId():X}"; + dp.Owner = evt.SourceId(); + dp.Color = sa.Data.DefaultDangerColor; + dp.DestoryAt = 7000; + dp.Scale = new Vector2(5, length); + + dp.FixRotation = false; + + sa.Method.SendDraw(DrawModeEnum.Default, DrawTypeEnum.Rect, dp); + } + + // 最后平台 + [ScriptMethod( + name: "雷转质射线2", + eventType: EventTypeEnum.StartCasting, + eventCondition: ["ActionId:45682"])] + public void 雷转质射线2(Event evt, ScriptAccessory sa) + { + if(_phase != 6) return; + var actionId = evt.ActionId(); + + var dp = sa.Data.GetDefaultDrawProperties(); + dp.Name = $"雷转质射线2-{actionId}-{evt.SourceId():X}"; + dp.Owner = evt.SourceId(); + dp.Color = sa.Data.DefaultDangerColor; + dp.DestoryAt = 7000; + dp.Scale = new Vector2(5, 10); + + dp.FixRotation = false; + + sa.Method.SendDraw(DrawModeEnum.Default, DrawTypeEnum.Rect, dp); + // 最后平台飞箱 = true; + } + + private float Theta(Vector2 origin, Vector2 target) => MathF.Atan2(target.Y - origin.Y, target.X - origin.X); + + private float RelTheta(float baseTheta, float t) + { + var diff = t - baseTheta; + while (diff < -MathF.PI) diff += 2 * MathF.PI; + while (diff > MathF.PI) diff -= 2 * MathF.PI; + return diff; + } + + // 异世界 + [ScriptMethod( + name: "以太晶球最大近战距离", + eventType: EventTypeEnum.AddCombatant, + eventCondition: ["DataId:9020"])] + public void P2AetherSphereSpawn(Event evt, ScriptAccessory sa) + { + // 不知道为什么每次会同时生成六个以太晶球... + // sa.Method.RemoveDraw("以太晶球最大近战距离"); + + // 检测有没有以太晶球最大近战距离,有就不画了 + // 笨办法,DrawList从哪获得啊 + if (以太晶球最大近战距离) return; + 以太晶球最大近战距离 = true; + // sa.Method.SendChat("/e 以太晶球出现,画最大近战距离圈"); + + var pos = evt.SourcePosition(); + var dp = sa.Data.GetDefaultDrawProperties(); + dp.Name = $"以太晶球最大近战距离"; + dp.Position = new Vector3(-400f, -900f, -400f); + dp.Color = new Vector4(sa.Data.DefaultSafeColor.X, sa.Data.DefaultSafeColor.Y, sa.Data.DefaultSafeColor.Z, 0.01f); + dp.DestoryAt = 100000; + dp.Scale = new Vector2(5f); // 半径4米 + + sa.Method.SendDraw(DrawModeEnum.Imgui, DrawTypeEnum.Circle, dp); + } + + [ScriptMethod( + name: "异世界-双T&D3D4鸣笛指路", + eventType: EventTypeEnum.TargetIcon, + eventCondition: ["Id:027F"] + )] + public async void OnHorn(Event evt, ScriptAccessory sa) + { + // sa.Method.SendChat("/e 异世界鸣笛 预测开始"); + // 扇形范围:只针对当前队伍的坦克 + var tankTargets = new List(sa.Data.PartyList.Count); + foreach (var memberId in sa.Data.PartyList) + { + var obj = sa.Data.Objects.SearchById(memberId); + if (obj is IBattleChara battleChara && battleChara.IsTank()) + { + tankTargets.Add(memberId); + } + } + + foreach (var tankId in tankTargets) + { + var dp2 = sa.Data.GetDefaultDrawProperties(); + dp2.Name = $"HornCone_{tankId:X}_{evt.SourceId():X}"; + dp2.Owner = sa.Data.Objects.FirstOrDefault(x => x.DataId == TargetBNpcId).EntityId; + dp2.TargetObject = tankId; + dp2.Color = sa.Data.DefaultDangerColor; + dp2.Radian = MathF.PI / 5.143f; // 22.5° + dp2.Scale = new Vector2(60f); + dp2.DestoryAt = 9000; + + sa.Method.SendDraw(DrawModeEnum.Default, DrawTypeEnum.Fan, dp2); + } + + // 前置 + var myIdx = sa.MyIndex(); + if (new List() { 2, 3, 4, 5 }.Contains(myIdx)) return; + + // 1) 采样 7 个角度 + var thetas = new List(SampleCount); + + Vector2? TryGetEntityPos2D(uint id) + { + var obj = sa.Data.Objects.FirstOrDefault(x => x.DataId == id); + return obj == default ? null : new Vector2(obj.Position.X, obj.Position.Z); + } + + var p0 = TryGetEntityPos2D(TargetBNpcId); + if (p0 == null) return; + + float theta0 = Theta(Origin, p0.Value); + thetas.Add(theta0); + + for (int i = 1; i < SampleCount; i++) + { + await Task.Delay(SampleIntervalMs); + + var p = TryGetEntityPos2D(TargetBNpcId); + if (p == null) return; + + thetas.Add(Theta(Origin, p.Value)); + } + + // 2) relθ:相对初始角度,避免跨 π 跳变 + var rel = thetas.Select(t => RelTheta(theta0, t)).ToList(); + + // 3) ω 拟合 + float omega = ( + -2f * rel[1] + -1f * rel[2] + +1f * rel[4] + +2f * rel[5] + +3f * rel[6] + ) / 14f; + + // 4) θerr + float avg = rel.Sum() / SampleCount; + float thetaErr = avg - 1.5f * omega; + + // 5) θ停 + float thetaStop = theta0 + PredictStopT * omega + thetaErr; + + // 6) 画图 + float facingIn = thetaStop + MathF.PI; + float offset = 35f * MathF.PI / 180f; + + float leftDir = facingIn + offset; + float rightDir = facingIn - offset; + Vector2 dirIn = new Vector2(MathF.Cos(facingIn), MathF.Sin(facingIn)); + + static Vector2 Rotate(Vector2 v, float rad) + { + float c = MathF.Cos(rad); + float s = MathF.Sin(rad); + return new Vector2(v.X * c - v.Y * s, v.X * s + v.Y * c); + } + + Vector2 local = new(25, 0); + Vector2 stopPos = Origin + Rotate(local, thetaStop); + // sa.Method.SendChat($"/e 停止位置: {stopPos.X:F2}, {stopPos.Y:F2}"); + + Vector2 mtPoint = stopPos + new Vector2(MathF.Cos(leftDir), MathF.Sin(leftDir)) * 19f; + Vector2 stPoint = stopPos + new Vector2(MathF.Cos(rightDir), MathF.Sin(rightDir)) * 19f; + Vector2 d3Point = stopPos + dirIn * 11f; + Vector2 d4Point = stopPos + dirIn * 39f; + + // 使用 + Vector3 ToWaypoint(Vector2 source) => new Vector3(source.X, -900, source.Y); + + var wpos = myIdx switch + { + 0 => ToWaypoint(mtPoint), + 1 => ToWaypoint(stPoint), + 6 => ToWaypoint(d3Point), + 7 => ToWaypoint(d4Point), + _ => Vector3.Zero + }; + + var dp = sa.WaypointDp(wpos, 5500); + sa.Method.SendDraw(DrawModeEnum.Imgui, DrawTypeEnum.Displacement, dp); + } + + [ScriptMethod( + name: "异世界-分摊分散", + eventType: EventTypeEnum.TargetIcon, + eventCondition: ["Id:regex:^(027D|027E)$"] + )] + public async void p3StackSpread(Event evt, ScriptAccessory sa){ + var idStr = (evt["Id"] ?? "").Replace("0x", "", StringComparison.OrdinalIgnoreCase) + .ToUpperInvariant(); + + const uint duration = 8000; + + // 分散 + if (idStr == "027E") + { + if (TTSOpen) + sa.Method.EdgeTTS("分散"); + sa.Method.TextInfo("分散", 4000, false); + foreach (var pid in sa.Data.PartyList) + { + var obj = sa.Data.Objects.SearchById(pid); + if (obj is IBattleChara bc && bc.IsTank()) + continue; // 不给坦克画圈 + + var dp = sa.Data.GetDefaultDrawProperties(); + dp.Name = $"P3-SpreadCircle-{pid:X}"; + dp.Owner = pid; + dp.Color = sa.Data.DefaultDangerColor; + dp.DestoryAt = duration; + dp.Scale = new Vector2(5); + + sa.Method.SendDraw(DrawModeEnum.Default, DrawTypeEnum.Circle, dp); + } + return; + } + + // 分摊 + if (idStr == "027D") + { + if (TTSOpen) + sa.Method.EdgeTTS("分摊"); + sa.Method.TextInfo("分摊", 4000, false); + if (sa.Data.PartyList.Count < 4) return; + + var h1 = sa.Data.PartyList[2]; + var h2 = sa.Data.PartyList[3]; + var d1 = sa.Data.PartyList[4]; + var d2 = sa.Data.PartyList[5]; + + void DrawYellowCircle(uint pid) + { + var dp = sa.Data.GetDefaultDrawProperties(); + dp.Name = $"P3-StackCircle-{pid:X}"; + dp.Owner = pid; + dp.Color = new Vector4(1f, 1f, 0f, 0.1f); + dp.DestoryAt = duration; + dp.Scale = new Vector2(5); + + sa.Method.SendDraw(DrawModeEnum.Imgui, DrawTypeEnum.Circle, dp); + } + + DrawYellowCircle(h1); + DrawYellowCircle(h2); + + var dp = sa.Data.GetDefaultDrawProperties(); + dp.Name = $"P2-StackLine-h1"; + dp.Owner = h1; // 线起点跟随 from + dp.TargetObject = d1; // 线终点跟随 to + dp.ScaleMode = ScaleMode.YByDistance; + dp.Scale = new Vector2(5); // 线宽 + dp.Color = sa.Data.DefaultDangerColor; + dp.DestoryAt = duration; + + sa.Method.SendDraw(DrawModeEnum.Default, DrawTypeEnum.Line, dp); + + var dp2 = sa.Data.GetDefaultDrawProperties(); + dp2.Name = $"P2-StackLine-h2"; + dp2.Owner = h2; // 线起点跟随 from + dp2.TargetObject = d2; // 线终点跟随 to + dp2.ScaleMode = ScaleMode.YByDistance; + dp2.Scale = new Vector2(5); // 线宽 + dp2.Color = sa.Data.DefaultDangerColor; + dp2.DestoryAt = duration; + + sa.Method.SendDraw(DrawModeEnum.Default, DrawTypeEnum.Line, dp2); + } + + } + + [ScriptMethod( + name: "雷光一闪(拍拍手)", + eventType: EventTypeEnum.StartCasting, + eventCondition: ["ActionId:45666"] + )] + public void OnClap(Event evt, ScriptAccessory sa) + { + var pos = evt.EffectPosition(); + var dp = sa.FastDp("雷光一闪", pos, 3500, new Vector2(4), safe: false); + sa.Method.SendDraw(DrawModeEnum.Default, DrawTypeEnum.Circle, dp); + } + + [ScriptMethod( + name: "前照光", + eventType: EventTypeEnum.StartCasting, + eventCondition: ["ActionId:regex:^(45690|45687)$"] + )] + public async void OnHeadlight(Event evt, ScriptAccessory sa) + { + var actionId = evt.ActionId(); + // 先打上后打下 + if (actionId == 45690){ + // sa.Method.SendChat($"/e phase: {_phase}"); + if (TTSOpen) + sa.Method.EdgeTTS("留在下面"); + sa.Method.TextInfo("留在下面", 3000, false); + + if (_phase == 4){ + var dp1 = sa.FastDp("前照光矩形危险区-上面1", new Vector3(107.5f, 5f, 245f), 6500, new Vector2(5, 10), safe: false); + var dp2 = sa.FastDp("前照光矩形危险区-上面2", new Vector3(92.5f, 5f, 250f), 6500, new Vector2(5, 15), safe: false); + sa.Method.SendDraw(DrawModeEnum.Default, DrawTypeEnum.Rect, dp1); + sa.Method.SendDraw(DrawModeEnum.Default, DrawTypeEnum.Rect, dp2); + } + else + { + var dp1 = sa.FastDp("前照光矩形危险区-上面1", new Vector3(92.5f, 5f, 345f), 6500, new Vector2(5, 20), safe: false); + sa.Method.SendDraw(DrawModeEnum.Default, DrawTypeEnum.Rect, dp1); + } + + await Task.Delay(6500); + if (TTSOpen) + sa.Method.EdgeTTS("上去"); + sa.Method.TextInfo("上去", 3000, false); + + if (_phase == 4){ + var dp3 = sa.FastDp("前照光矩形危险区-下面1", new Vector3(97.5f, 0f, 235f), 3000, new Vector2(15, 15), safe: false); + var dp5 = sa.FastDp("前照光矩形危险区-下面3", new Vector3(107.5f, 0f, 235f), 3000, new Vector2(5, 10), safe: false); + var dp6 = sa.FastDp("前照光矩形危险区-下面4", new Vector3(102.5f, 0f, 255f), 3000, new Vector2(15, 10), safe: false); + sa.Method.SendDraw(DrawModeEnum.Default, DrawTypeEnum.Rect, dp3); + sa.Method.SendDraw(DrawModeEnum.Default, DrawTypeEnum.Rect, dp5); + sa.Method.SendDraw(DrawModeEnum.Default, DrawTypeEnum.Rect, dp6); + } + else{ + var dp3 = sa.FastDp("前照光矩形危险区-下面1", new Vector3(92.5f, 0f, 335f), 3000, new Vector2(5, 10), safe: false); + var dp4 = sa.FastDp("前照光矩形危险区-下面4", new Vector3(102.5f, 0f, 335f), 3000, new Vector2(15, 30), safe: false); + sa.Method.SendDraw(DrawModeEnum.Default, DrawTypeEnum.Rect, dp3); + sa.Method.SendDraw(DrawModeEnum.Default, DrawTypeEnum.Rect, dp4); + } + } + else{ + // sa.Method.SendChat($"/e phase: {_phase}"); + if (TTSOpen) + sa.Method.EdgeTTS("上去"); + sa.Method.TextInfo("上去", 3000, false); + + if (_phase == 4){ + var dp1 = sa.FastDp("前照光矩形危险区-下面1", new Vector3(97.5f, 0f, 235f), 6500, new Vector2(15, 15), safe: false); + var dp5 = sa.FastDp("前照光矩形危险区-下面3", new Vector3(107.5f, 0f, 235f), 6500, new Vector2(5, 10), safe: false); + var dp6 = sa.FastDp("前照光矩形危险区-下面4", new Vector3(102.5f, 0f, 255f), 6500, new Vector2(15, 10), safe: false); + sa.Method.SendDraw(DrawModeEnum.Default, DrawTypeEnum.Rect, dp1); + sa.Method.SendDraw(DrawModeEnum.Default, DrawTypeEnum.Rect, dp5); + sa.Method.SendDraw(DrawModeEnum.Default, DrawTypeEnum.Rect, dp6); + } + else{ + var dp1 = sa.FastDp("前照光矩形危险区-下面1", new Vector3(92.5f, 0f, 335f), 6500, new Vector2(5, 10), safe: false); + var dp2 = sa.FastDp("前照光矩形危险区-下面4", new Vector3(102.5f, 0f, 335f), 6500, new Vector2(15, 30), safe: false); + sa.Method.SendDraw(DrawModeEnum.Default, DrawTypeEnum.Rect, dp1); + sa.Method.SendDraw(DrawModeEnum.Default, DrawTypeEnum.Rect, dp2); + } + + await Task.Delay(6500); + if (TTSOpen) + sa.Method.EdgeTTS("下去"); + sa.Method.TextInfo("下去", 3000, false); + + if (_phase == 4){ + var dp2 = sa.FastDp("前照光矩形危险区-上面1", new Vector3(107.5f, 5f, 245f), 3000, new Vector2(5, 10), safe: false); + var dp4 = sa.FastDp("前照光矩形危险区-上面2", new Vector3(92.5f, 5f, 250f), 3000, new Vector2(5, 15), safe: false); + sa.Method.SendDraw(DrawModeEnum.Default, DrawTypeEnum.Rect, dp2); + sa.Method.SendDraw(DrawModeEnum.Default, DrawTypeEnum.Rect, dp4); + } + else{ + var dp2 = sa.FastDp("前照光矩形危险区-上面1", new Vector3(92.5f, 5f, 345f), 3000, new Vector2(5, 20), safe: false); + sa.Method.SendDraw(DrawModeEnum.Default, DrawTypeEnum.Rect, dp2); + } + } + } +} + +#region Helpers + +public static class EventExtensions +{ + private static bool ParseHexId(string? idStr, out uint id) + { + id = 0; + if (string.IsNullOrEmpty(idStr)) return false; + try + { + var idStr2 = idStr.Replace("0x", ""); + id = uint.Parse(idStr2, System.Globalization.NumberStyles.HexNumber); + return true; + } + catch + { + return false; + } + } + + public static uint ActionId(this Event evt) => JsonConvert.DeserializeObject(evt["ActionId"]); + public static uint SourceId(this Event evt) => ParseHexId(evt["SourceId"], out var id) ? id : 0; + public static uint TargetId(this Event evt) => ParseHexId(evt["TargetId"], out var id) ? id : 0; + public static Vector3 SourcePosition(this Event evt) => JsonConvert.DeserializeObject(evt["SourcePosition"]); + public static Vector3 EffectPosition(this Event evt) => JsonConvert.DeserializeObject(evt["EffectPosition"]); + + public static uint DirectorId(this Event evt) => ParseHexId(evt["DirectorId"], out var id) ? id : 0; +} + +public static class ScriptAccessoryExtensions +{ + private static readonly string[] DefaultRoleByIndex = + [ + "MT", "ST", "H1", "H2", "D1", "D2", "D3", "D4" + ]; + + public static int MyIndex(this ScriptAccessory sa) + { + return sa.Data.PartyList.IndexOf(sa.Data.Me); + } + + public static DrawPropertiesEdit FastDp(this ScriptAccessory sa, string name, Vector3 pos, uint duration, Vector2 scale, bool safe = false) + { + var dp = sa.Data.GetDefaultDrawProperties(); + dp.Name = name; + dp.Color = safe ? sa.Data.DefaultSafeColor : sa.Data.DefaultDangerColor; + dp.Position = pos; + dp.DestoryAt = duration; + dp.Scale = scale; + return dp; + } + + public static DrawPropertiesEdit WaypointDp(this ScriptAccessory sa, Vector3 pos, uint duration, uint delay = 0, string name = "Waypoint") + { + var dp = sa.Data.GetDefaultDrawProperties(); + dp.Name = name; + dp.Color = sa.Data.DefaultSafeColor; + dp.Owner = sa.Data.Me; + dp.TargetPosition = pos; + dp.DestoryAt = duration; + dp.Delay = delay; + dp.Scale = new Vector2(2); + dp.ScaleMode = ScaleMode.YByDistance; + return dp; + } + + public static void DrawSpreadCircles10s(this ScriptAccessory sa) + { + // sa.Method.RemoveDraw("P1-SpreadCircle.*"); + + foreach (var pid in sa.Data.PartyList) + { + var dp = sa.Data.GetDefaultDrawProperties(); + dp.Name = $"P1-SpreadCircle-{pid:X}"; + dp.Owner = pid; + dp.Color = sa.Data.DefaultDangerColor; + dp.DestoryAt = 6000; + dp.Scale = new Vector2(5); + + sa.Method.SendDraw(DrawModeEnum.Default, DrawTypeEnum.Circle, dp); + } + } + + public static void DrawStackPairLines10s(this ScriptAccessory sa) + { + // sa.Method.RemoveDraw("P1-Stack.*"); + + void DrawLine(uint from, uint to, string tag) + { + var dp = sa.Data.GetDefaultDrawProperties(); + dp.Name = $"P1-StackLine-{from:X}"; + dp.Owner = from; // 线起点跟随 from + dp.TargetObject = to; // 线终点跟随 to + dp.ScaleMode = ScaleMode.YByDistance; + dp.Scale = new Vector2(5); // 线宽 + dp.Color = sa.Data.DefaultDangerColor; + dp.DestoryAt = 6000; + + sa.Method.SendDraw(DrawModeEnum.Default, DrawTypeEnum.Line, dp); + } + void DrawCircle(uint ply, string tag) + { + var dp = sa.Data.GetDefaultDrawProperties(); + dp.Name = $"P1-StackCircle-{ply:X}"; + dp.Owner = ply; + dp.Color = new Vector4(1f, 1f, 0f, 0.1f); // 黄色 + dp.DestoryAt = 6000; + dp.Scale = new Vector2(5); + + sa.Method.SendDraw(DrawModeEnum.Imgui, DrawTypeEnum.Circle, dp); + } + + // 防止队伍不足 8 人时崩 + if (sa.Data.PartyList.Count < 8) return; + + var mt = sa.Data.PartyList[0]; + var st = sa.Data.PartyList[1]; + var h1 = sa.Data.PartyList[2]; + var h2 = sa.Data.PartyList[3]; + var d1 = sa.Data.PartyList[4]; + var d2 = sa.Data.PartyList[5]; + var d3 = sa.Data.PartyList[6]; + var d4 = sa.Data.PartyList[7]; + + DrawLine(mt, d1, "MT-D1"); + DrawLine(st, d2, "ST-D2"); + DrawLine(d3, h1, "D3-H1"); + DrawLine(d4, h2, "D4-H2"); + + DrawCircle(mt, "MT"); + DrawCircle(st, "ST"); + DrawCircle(h1, "H1"); + DrawCircle(h2, "H2"); + } +} + +#endregion diff --git a/Codaaaaaa/Enuo.cs b/Codaaaaaa/Enuo.cs new file mode 100644 index 0000000..c31dc3f --- /dev/null +++ b/Codaaaaaa/Enuo.cs @@ -0,0 +1,1832 @@ +using System; +using System.Linq; +using System.Numerics; +using System.Threading; +using System.Collections.Generic; +using System.Threading.Tasks; +using Newtonsoft.Json; +using KodakkuAssist.Script; +using KodakkuAssist.Module.GameEvent; +using KodakkuAssist.Module.Draw; +using KodakkuAssist.Module.Draw.Manager; +using KodakkuAssist.Data; +using KodakkuAssist.Extensions; +using System.Runtime.CompilerServices; +using System.Security.Cryptography.X509Certificates; +using System.Reflection; + +namespace Codaaaaaa.Enuo; + +[ScriptType( + guid: "8c4a9f2d-6b31-4e0a-9f27-1d7c5b8a3e46", + name: "恩欧歼殛战画图", + territorys: [1362], + version: "0.0.0.7", + author: "Codaaaaaa", + updateInfo: "0.0.0.7\n更新了时间轴,修复了黑球范围不随双奶移动的问题", + note: "mmw文档+NOCCHH")] +public class Enuo +{ + #region 用户设置 + // [UserSetting("是否开启TTS")] public static bool TTSOpen { get; set; } = true; + [UserSetting("")] public static 热病提示enum 热病冷却提示 { get; set; } = 热病提示enum.横幅; + [UserSetting("")] public static P2打法法enum P2打法 { get; set; } = P2打法法enum.NOCCHH; + #endregion + + #region enum + public enum 热病提示enum + { + 横幅, + 默语, + TTS + } + + public enum P2打法法enum + { + NOCCHH, + MMW + } + #endregion + + #region 变量和初始化 + private readonly object _commonMechanicLock = new(); + private readonly List<(uint OrbId, uint PlayerId, DateTime Time)> _blackBallTethers = new(); + + // 混沌激流 + private readonly List<(Vector3 vector3, uint sourceId)> 混沌激流黑球生成位置 = new(); + private readonly List<(int GlobalOrder, Vector3 Pos, uint SourceId)> 混沌激流_0196tethers = new(); + private readonly List<(int GlobalOrder, Vector3 Pos, uint SourceId)> 混沌激流_0197tethers = new(); + private bool 混沌激流_0196已分配 = false; + private bool 混沌激流_0197已分配 = false; + private int 混沌激流_AddCombatantCount = 0; + private DateTime 撞球易伤上次提示时间 = DateTime.MinValue; + + private readonly object P1深度冻结锁 = new(); + private bool P1深度冻结冷却已提醒 = false; + + // P2 无之涡流 + private readonly object P2无之涡流锁 = new(); + private readonly List P2无之涡流点名玩家 = new(); + private readonly bool[] P2无之涡流塔出现 = new bool[8]; + private readonly Vector3[] P2无之涡流塔位置 = new Vector3[8]; + private int P2无之涡流塔数量 = 0; + private bool P2无之涡流已分配 = false; + private static readonly Vector3[] P2_无之涡流固定点位 = + [ + new Vector3(109.54f, -0.02f, 76.89f), // 0 + new Vector3(123.09f, -0.02f, 90.41f), // 1 + new Vector3(123.09f, -0.02f, 109.54f), // 2 + new Vector3(109.54f, -0.02f, 123.09f), // 3 + new Vector3(90.41f, -0.02f, 123.09f), // 4 + new Vector3(76.89f, -0.02f, 109.54f), // 5 + new Vector3(76.89f, -0.02f, 90.41f), // 6 + new Vector3(90.41f, -0.02f, 76.89f), // 7 + + ]; + private uint P2无之涡流BossId = 0; + + private int P2_获取无之涡流塔Index(Vector3 pos) + { + var bestIndex = -1; + var bestDist = float.MaxValue; + + for (int i = 0; i < P2_无之涡流固定点位.Length; i++) + { + var dist = Vector3.Distance(pos, P2_无之涡流固定点位[i]); + if (dist < bestDist) + { + bestDist = dist; + bestIndex = i; + } + } + + return bestDist <= 5f ? bestIndex : -1; + } + + public void Init(ScriptAccessory sa) + { + _phase = 1; + lock (_commonMechanicLock) + { + _blackBallTethers.Clear(); + } + + lock (P1深度冻结锁) + { + P1深度冻结冷却已提醒 = false; + } + + 混沌激流黑球生成位置.Clear(); + 混沌激流_0196tethers.Clear(); + 混沌激流_0197tethers.Clear(); + 混沌激流_0196已分配 = false; + 混沌激流_0197已分配 = false; + 混沌激流_AddCombatantCount = 0; + 撞球易伤上次提示时间 = DateTime.MinValue; + + P2_重置无之涡流(); + + lock (_p2EyeLock) + { + _p2EyeTargets.Clear(); + _p2EyeRefreshing = false; + } + + sa.Method.RemoveDraw(".*"); + } + + #endregion + + #region 阶段 + private double _phase = 1; + + [ScriptMethod(name: "Set Phase 1", eventType: EventTypeEnum.Chat, eventCondition: ["Type:Echo", "Message:KASP1"], userControl: false)] + public void SetP1(Event evt, ScriptAccessory sa) => _phase = 1; + + [ScriptMethod(name: "Set Phase 2", eventType: EventTypeEnum.Chat, eventCondition: ["Type:Echo", "Message:KASP2"], userControl: false)] + public void SetP2(Event evt, ScriptAccessory sa) => _phase = 2; + + [ScriptMethod(name: "Set Phase 3", eventType: EventTypeEnum.Chat, eventCondition: ["Type:Echo", "Message:KASP3"], userControl: false)] + public void SetP3(Event evt, ScriptAccessory sa) => _phase = 3; + #endregion + + #region 通用机制 + + [ScriptMethod(name: "通用机制-无之膨胀", eventType: EventTypeEnum.StartCasting, eventCondition: ["ActionId:regex:^(49977|49978|49979|49980)$"])] + public async void 通用机制_无之膨胀(Event evt, ScriptAccessory sa) + { + var sourceId = evt.SourceId(); + if (sourceId == 0) return; + + const uint duration = 7700; + var actionId = evt.ActionId(); + + if (actionId == 49977) + { + // 49977 钢铁:半径 40 + DrawCircleOwner(sa, "通用机制-无之膨胀-钢铁", sourceId, 40f, duration, sa.Data.DefaultDangerColor); + } + else if (actionId == 49978) + { + // 49978 月环:外圈 60,内圈 10 + DrawDonutOwner(sa, "通用机制-无之膨胀-月环", sourceId, 60f, 40f, duration, sa.Data.DefaultDangerColor); + } + else if (actionId == 49979) + { + // 49979 钢铁:半径 20 + DrawCircleOwner(sa, "通用机制-无之膨胀-本体-钢铁", sourceId, 12f, duration, sa.Data.DefaultDangerColor); + } + else if (actionId == 49980) + { + // 49978 月环:外圈 60,内圈 10 + DrawDonutOwner(sa, "通用机制-无之膨胀-本体-月环", sourceId, 40f, 6f, duration, sa.Data.DefaultDangerColor); + } + } + + [ScriptMethod(name: "通用机制-回归重波动(单奶妈黑球)", eventType: EventTypeEnum.TargetIcon, eventCondition: ["Id:regex:^(02BE)$"])] + public async void 通用机制_回归重波动(Event evt, ScriptAccessory sa) + { + if (!TryGetTetherOrbAndPlayer(evt, sa, out var orbId, out var playerId, out _)) return; + + RecordBlackBallTether(orbId, playerId); + + var targetId = evt.TargetId(); + // 单奶黑球:被点名的人连线矩形固定 safeColor。 + var dp = sa.Data.GetDefaultDrawProperties(); + dp.Name = $"通用机制-回归重波动-{orbId:X}-{playerId:X}"; + dp.Owner = orbId; + dp.Scale = new Vector2(6, 1); + dp.Color = sa.Data.DefaultSafeColor; + dp.DestoryAt = 9500; + dp.ScaleMode = ScaleMode.YByDistance; + + // 终点 = 被点名的人沿「场中→人」方向再往外 7m,每帧按人当前位置重算 + var center = new Vector3(100f, 0f, 100f); + Vector3? OutPoint() + { + if (sa.Data.Objects.SearchById(targetId) is not { } obj) return null; + var dir = obj.Position - center; + dir.Y = 0; + return dir.LengthSquared() < 0.01f ? obj.Position : obj.Position + Vector3.Normalize(dir) * 7f; + } + dp.TargetPosition = OutPoint(); + sa.Method.SendDraw(DrawModeEnum.Default, DrawTypeEnum.Rect, dp, d => d.TargetPosition = OutPoint() ?? d.TargetPosition); + + } + + [ScriptMethod(name: "通用机制-回归波动(双奶妈黑球)", eventType: EventTypeEnum.TargetIcon, eventCondition: ["Id:regex:^(02BD)$"])] + public async void 通用机制_回归波动(Event evt, ScriptAccessory sa) + { + var myIdx = sa.MyIndex(); + var targetId = evt.TargetId(); + var targetIdx = sa.Data.PartyList.IndexOf(targetId); + var orbId = evt.SourceId(); + + if (orbId == 0) return; + if (!IsValidPartyIndex(targetIdx)) return; + + // 按你的 comment:被点名 index 为奇数时 1/3/5/7 safe;为偶数时 0/2/4/6 safe。 + var isSafeForMe = (myIdx % 2) == (targetIdx % 2); + var dp = sa.Data.GetDefaultDrawProperties(); + dp.Name = $"通用机制-回归波动-{targetIdx}"; + dp.Owner = orbId; + dp.Scale = new Vector2(6, 1); + dp.Color = isSafeForMe ? sa.Data.DefaultSafeColor : sa.Data.DefaultDangerColor; + dp.DestoryAt = 9500; + dp.ScaleMode = ScaleMode.YByDistance; + + // 终点 = 被点名的人沿「场中→人」方向再往外 7m,每帧按人当前位置重算 + var center = new Vector3(100f, 0f, 100f); + Vector3? OutPoint() + { + if (sa.Data.Objects.SearchById(targetId) is not { } obj) return null; + var dir = obj.Position - center; + dir.Y = 0; + return dir.LengthSquared() < 0.01f ? obj.Position : obj.Position + Vector3.Normalize(dir) * 7f; + } + dp.TargetPosition = OutPoint(); + sa.Method.SendDraw(DrawModeEnum.Default, DrawTypeEnum.Rect, dp, d => d.TargetPosition = OutPoint() ?? d.TargetPosition); + } + + [ScriptMethod(name: "通用机制-集束波动(双奶扇形分摊)", eventType: EventTypeEnum.StartCasting, eventCondition: ["ActionId:regex:^(50033)$"])] + public async void 通用机制_集束波动(Event evt, ScriptAccessory sa) + { + var sourceId = evt.SourceId(); + if (sourceId == 0) return; + + var myIdx = sa.MyIndex(); + if (!IsValidPartyIndex(myIdx)) return; + + const uint duration = 4800; + const float range = 60f; + const float radian = MathF.PI * 10f / 18f; // 110° + + // index 2 左边,index 3 右边。偶数组 0/2/4/6 吃 index 2,奇数组 1/3/5/7 吃 index 3。 + DrawFanFromOwnerToPartyIndex(sa, "通用机制-集束波动-index2", sourceId, 2, range, radian, duration, + IsEvenGroup(myIdx) ? sa.Data.DefaultSafeColor : sa.Data.DefaultDangerColor); + DrawFanFromOwnerToPartyIndex(sa, "通用机制-集束波动-index3", sourceId, 3, range, radian, duration, + IsOddGroup(myIdx) ? sa.Data.DefaultSafeColor : sa.Data.DefaultDangerColor); + + if (myIdx == 2) + { + var dp = sa.WaypointDp(new Vector3(88f, 0f, 100f), duration, 0, "集束波动-index2-左边"); + sa.Method.SendDraw(DrawModeEnum.Imgui, DrawTypeEnum.Displacement, dp); + } + else if (myIdx == 3) + { + var dp = sa.WaypointDp(new Vector3(112f, 0f, 100f), duration, 0, "集束波动-index3-右边"); + sa.Method.SendDraw(DrawModeEnum.Imgui, DrawTypeEnum.Displacement, dp); + } + } + + [ScriptMethod(name: "通用机制-扩散波动(两两扇形分摊)", eventType: EventTypeEnum.StartCasting, eventCondition: ["ActionId:regex:^(50032)$"])] + public async void 通用机制_扩散波动(Event evt, ScriptAccessory sa) + { + var sourceId = evt.SourceId(); + if (sourceId == 0) return; + + var myIdx = sa.MyIndex(); + if (!IsValidPartyIndex(myIdx)) return; + + const uint duration = 4800; + const float range = 60f; + const float radian = MathF.PI / 3f; + + var safeFanIndex = myIdx switch + { + 0 or 6 => 0, + 1 or 5 => 1, + 2 or 4 => 2, + 3 or 7 => 3, + _ => -1 + }; + + // 06 左上,15 右下,24 左下,37 右上 + var myGuidePos = myIdx switch + { + 0 or 6 => new Vector3(95f, 0f, 95f), // 左上 + 1 or 5 => new Vector3(105f, 0f, 105f), // 右下 + 2 or 4 => new Vector3(95f, 0f, 105f), // 左下 + 3 or 7 => new Vector3(105f, 0f, 95f), // 右上 + _ => new Vector3(100f, 0f, 100f) + }; + + // 指路 + var guideDp = sa.WaypointDp( + myGuidePos, + duration, + 0, + $"通用机制-扩散波动-指路-{myIdx}" + ); + sa.Method.SendDraw(DrawModeEnum.Imgui, DrawTypeEnum.Displacement, guideDp); + + for (var targetIdx = 0; targetIdx < 4; targetIdx++) + { + DrawFanFromOwnerToPartyIndex(sa, + name: $"通用机制-扩散波动-index{targetIdx}", + ownerId: sourceId, + targetPartyIndex: targetIdx, + range: range, + radian: radian, + duration: duration, + color: targetIdx == safeFanIndex + ? sa.Data.DefaultSafeColor + : sa.Data.DefaultDangerColor); + } + } + + [ScriptMethod(name: "通用机制-混沌激流(黑球转转乐)", eventType: EventTypeEnum.AddCombatant, eventCondition: ["DataId:regex:^(19909|19910)$"])] + public async void 通用机制_混沌激流(Event evt, ScriptAccessory sa) + { + var pos = evt.SourcePosition(); + var sourceId = evt.SourceId(); + var dataId = evt.DataId(); + + bool firstOfRound = false; + bool shouldDraw = true; + + lock (_commonMechanicLock) + { + // 新一轮开始:第一个 19909 出现时初始化 + if (dataId == 19909 && 混沌激流_AddCombatantCount == 0) + { + 混沌激流黑球生成位置.Clear(); + + 混沌激流_0196tethers.Clear(); + 混沌激流_0197tethers.Clear(); + 混沌激流_0196已分配 = false; + 混沌激流_0197已分配 = false; + 撞球易伤上次提示时间 = DateTime.MinValue; + + firstOfRound = true; + } + + 混沌激流_AddCombatantCount++; + + if (dataId == 19909) + { + // 正常前 8 个黑球 + if (混沌激流_AddCombatantCount <= 8) + { + 混沌激流黑球生成位置.Add((pos, sourceId)); + } + else + { + // 理论上不该发生,防止后续异常污染 list + shouldDraw = false; + sa.Method.SendChat($"/e [混沌激流] 异常:第 {混沌激流_AddCombatantCount} 次仍然收到 19909"); + } + } + else if (dataId == 19910) + { + int replaceIndex = 混沌激流_AddCombatantCount switch + { + 9 => 0, + 10 => 1, + _ => -1 + }; + + if (replaceIndex >= 0 && replaceIndex < 混沌激流黑球生成位置.Count) + { + // 第 9 次覆盖 index 0,第 10 次覆盖 index 1 + 混沌激流黑球生成位置[replaceIndex] = (pos, sourceId); + } + else + { + shouldDraw = false; + sa.Method.SendChat($"/e [混沌激流] 异常:第 {混沌激流_AddCombatantCount} 次 19910 无法覆盖 index {replaceIndex},当前数量={混沌激流黑球生成位置.Count}"); + } + } + } + + if (firstOfRound) + { + _ = Task.Run(async () => + { + await Task.Delay(30000); + + lock (_commonMechanicLock) + { + 混沌激流黑球生成位置.Clear(); + + 混沌激流_0196tethers.Clear(); + 混沌激流_0197tethers.Clear(); + 混沌激流_0196已分配 = false; + 混沌激流_0197已分配 = false; + 撞球易伤上次提示时间 = DateTime.MinValue; + + 混沌激流_AddCombatantCount = 0; + } + }); + } + + if (!shouldDraw) return; + + DrawFanFromCenterToPosition( + sa, + name: $"通用机制-混沌激流-黑球扇形-{sourceId:X}", + center: new Vector3(100f, 0f, 100f), + targetPos: pos, + degree: 45f, + radius: 30f, + duration: 7000, + color: sa.Data.DefaultDangerColor); + } + + [ScriptMethod(name: "通用机制-混沌激流-撞球", eventType: EventTypeEnum.Tether, eventCondition: ["Id:regex:^(0196|0197)$"])] + public async void 通用机制_混沌激流_撞球(Event evt, ScriptAccessory sa) + { + var myIdx = sa.MyIndex(); + var srcId = evt.SourceId(); + var tgtId = evt.TargetId(); + var sourcePos = evt.SourcePosition(); + var rawId = (evt["Id"] ?? string.Empty); + + if (!IsValidPartyIndex(myIdx)) + { + return; + } + + bool is0196; + var cleanId = rawId.Replace("0x", "").Trim(); + if (cleanId.Equals("0196", StringComparison.OrdinalIgnoreCase)) is0196 = true; + else if (cleanId.Equals("0197", StringComparison.OrdinalIgnoreCase)) is0196 = false; + else + { + return; + } + + Vector3 myTargetPos = default; + uint myTargetObjectId = 0; + bool shouldDraw = false; + int myTypeOrder = myIdx / 2; + int globalOrder = -1; + float minDist = float.MaxValue; + int listCountAfterAdd = 0; + bool deduped = false; + bool alreadyAssigned = false; + + lock (_commonMechanicLock) + { + if (混沌激流黑球生成位置.Count == 0) + { + return; + } + + // 打印当前黑球list + var ballListStr = string.Join(" | ", 混沌激流黑球生成位置.Select((p, i) => $"[{i}]({p.vector3.X:F1},{p.vector3.Z:F1})")); + + for (int i = 0; i < 混沌激流黑球生成位置.Count; i++) + { + var dist = Vector3.Distance(sourcePos, 混沌激流黑球生成位置[i].vector3); + if (dist < minDist) + { + minDist = dist; + globalOrder = i; + } + } + + if (globalOrder < 0) return; + var matchedBall = 混沌激流黑球生成位置[globalOrder]; + var matchedPos = matchedBall.vector3; + var matchedSourceId = matchedBall.sourceId; + + var list = is0196 ? 混沌激流_0196tethers : 混沌激流_0197tethers; + + if (list.Any(t => t.GlobalOrder == globalOrder)) + { + deduped = true; + return; + } + + list.Add((globalOrder, matchedPos, matchedSourceId)); + listCountAfterAdd = list.Count; + + var listStr = string.Join(",", list.Select(t => t.GlobalOrder)); + + if (list.Count < 4) + { + return; + } + + alreadyAssigned = is0196 ? 混沌激流_0196已分配 : 混沌激流_0197已分配; + if (alreadyAssigned) + { + return; + } + + var 标准顺序 = 获取混沌激流标准顺时针顺序(sa); + + var rankMap = 标准顺序 + .Select((globalOrder, rank) => new { globalOrder, rank }) + .ToDictionary(x => x.globalOrder, x => x.rank); + + var sorted = list + .OrderBy(t => rankMap.TryGetValue(t.GlobalOrder, out var rank) ? rank : 999) + .ToList(); + + var sortedStr = string.Join(",", sorted.Select((t, i) => + $"#{i}=global{t.GlobalOrder}({t.Pos.X:F1},{t.Pos.Z:F1})")); + + if (myTypeOrder >= sorted.Count) + { + return; + } + var myTarget = sorted[myTypeOrder]; + + myTargetPos = myTarget.Pos; + myTargetObjectId = myTarget.SourceId; + + shouldDraw = myTargetObjectId != 0; + + if (is0196) 混沌激流_0196已分配 = true; + else 混沌激流_0197已分配 = true; + } + + if (!shouldDraw) return; + + uint delay = is0196 ? 6000u : 1000u; + uint duration = is0196 ? 6000u : 5000u; + + var dp = sa.WaypointToObjectDp( + myTargetObjectId, + duration, + delay, + $"混沌激流-撞球-{(is0196 ? "0196" : "0197")}-{myTypeOrder}-{myTargetObjectId:X}" + ); + + sa.Method.SendDraw(DrawModeEnum.Imgui, DrawTypeEnum.Displacement, dp); + } + + [ScriptMethod(name: "通用机制-奔流", eventType: EventTypeEnum.StartCasting, eventCondition: ["ActionId:regex:^(49995|49996|49997)$"])] + public async void 通用机制_奔流(Event evt, ScriptAccessory sa) + { + var pos = evt.EffectPosition(); + + var dp = sa.Data.GetDefaultDrawProperties(); + dp.Name = $"通用机制-奔流-{pos.X:F1}-{pos.Z:F1}-{evt.SourceId():X}"; + dp.Position = pos; + dp.Scale = new Vector2(7f); + dp.Color = sa.Data.DefaultDangerColor; + dp.DestoryAt = 6000; + dp.ScaleMode = ScaleMode.ByTime; + sa.Method.SendDraw(DrawModeEnum.Default, DrawTypeEnum.Circle, dp); + } + + [ScriptMethod(name: "通用机制-撞球易伤提示", eventType: EventTypeEnum.StatusAdd, eventCondition: ["StatusID:regex:^(2941)$"])] + public async void 通用机制_撞球易伤提示(Event evt, ScriptAccessory sa) + { + int durationMilliseconds = 4450; + var targetId = evt.TargetId(); + if (targetId !=sa.Data.Me) return; + + if ((DateTime.UtcNow - 撞球易伤上次提示时间).TotalSeconds < 30) + { + return; + } + + var sourceId = evt.SourceId(); + if (sourceId == 0) return; + + var sourceObj = sa.Data.Objects.SearchByEntityId(sourceId); + if (sourceObj == null) return; + + var dataId = sourceObj.DataId; + + // 只处理混沌激流黑球 + if (dataId != 19909 && dataId != 19910) + { + return; + } + + 撞球易伤上次提示时间 = DateTime.UtcNow; + + if (热病冷却提示 == 热病提示enum.横幅) + { + sa.Method.TextInfo("等这行提示消失再去撞球", durationMilliseconds, true); + } + else if (热病冷却提示 == 热病提示enum.默语) + { + sa.Method.SendChat("/e 等提示消失再去撞球"); + } + else if (热病冷却提示 == 热病提示enum.TTS) + { + sa.Method.TTS("等提示消失再去撞球"); + } + } + + #region 通用机制 helpers + private static bool IsValidPartyIndex(int idx) => idx >= 0 && idx <= 7; + private static bool IsEvenGroup(int idx) => idx is 0 or 2 or 4 or 6; + private static bool IsOddGroup(int idx) => idx is 1 or 3 or 5 or 7; + + private void DrawCircleOwner(ScriptAccessory sa, string name, uint ownerId, float radius, uint duration, Vector4 color, uint delay = 0) + { + var dp = sa.Data.GetDefaultDrawProperties(); + dp.Name = name; + dp.Owner = ownerId; + dp.Scale = new Vector2(radius); + dp.Color = color; + dp.Delay = delay; + dp.DestoryAt = duration; + dp.ScaleMode = ScaleMode.ByTime; + sa.Method.SendDraw(DrawModeEnum.Default, DrawTypeEnum.Circle, dp); + } + + private void DrawDonutOwner(ScriptAccessory sa, string name, uint ownerId, float outerRadius, float innerRadius, uint duration, Vector4 color, uint delay = 0) + { + var dp = sa.Data.GetDefaultDrawProperties(); + dp.Name = name; + dp.Owner = ownerId; + dp.Scale = new Vector2(outerRadius); + dp.InnerScale = new Vector2(innerRadius); + dp.Radian = MathF.PI * 2f; + dp.Color = color; + dp.Delay = delay; + dp.DestoryAt = duration; + // dp.ScaleMode = ScaleMode.ByTime; + sa.Method.SendDraw(DrawModeEnum.Default, DrawTypeEnum.Donut, dp); + } + + private void DrawRectFromOwnerToTarget(ScriptAccessory sa, string name, uint ownerId, uint targetId, float width, float length, uint duration, Vector4 color, uint delay = 0) + { + var dp = sa.Data.GetDefaultDrawProperties(); + dp.Name = name; + dp.Owner = ownerId; + dp.Scale = new Vector2(width, length); + dp.Color = color; + dp.Delay = delay; + dp.DestoryAt = duration; + dp.ScaleMode = ScaleMode.ByTime; + SetDrawTargetObject(dp, targetId); + sa.Method.SendDraw(DrawModeEnum.Default, DrawTypeEnum.Rect, dp); + } + + private void DrawFanFromOwnerToPartyIndex(ScriptAccessory sa, string name, uint ownerId, int targetPartyIndex, float range, float radian, uint duration, Vector4 color, uint delay = 0) + { + if (!IsValidPartyIndex(targetPartyIndex)) return; + var targetId = sa.Data.PartyList[targetPartyIndex]; + if (targetId == 0) return; + + var dp = sa.Data.GetDefaultDrawProperties(); + dp.Name = name; + dp.Owner = ownerId; + dp.Scale = new Vector2(range); + dp.Radian = radian; + dp.Color = color; + dp.Delay = delay; + dp.DestoryAt = duration; + dp.ScaleMode = ScaleMode.ByTime; + SetDrawTargetObject(dp, targetId); + sa.Method.SendDraw(DrawModeEnum.Default, DrawTypeEnum.Fan, dp); + } + + private void DrawFanFromCenterToPosition( + ScriptAccessory sa, + string name, + Vector3 center, + Vector3 targetPos, + float degree, + float radius, + uint duration, + Vector4 color) + { + var dp = sa.Data.GetDefaultDrawProperties(); + + dp.Name = name; + dp.Owner = 0; + dp.Position = center; + dp.Rotation = GetRadian(center, targetPos); + dp.Radian = degree * MathF.PI / 180f; + dp.Scale = new Vector2(radius); + dp.Color = color; + dp.DestoryAt = duration; + dp.ScaleMode = ScaleMode.ByTime; + + sa.Method.SendDraw(DrawModeEnum.Default, DrawTypeEnum.Fan, dp); + } + + private void DrawFanFromPositionToTarget( + ScriptAccessory sa, + string name, + Vector3 fromPos, + uint targetId, + float range, + float radian, + uint duration, + Vector4 color, + uint delay = 0) + { + if (targetId == 0) return; + + var dp = sa.Data.GetDefaultDrawProperties(); + dp.Name = name; + dp.Owner = 0; + dp.Position = fromPos; + dp.Scale = new Vector2(range); + dp.Radian = radian; + dp.Color = color; + dp.Delay = delay; + dp.DestoryAt = duration; + dp.ScaleMode = ScaleMode.ByTime; + + SetDrawTargetObject(dp, targetId); + + sa.Method.SendDraw(DrawModeEnum.Default, DrawTypeEnum.Fan, dp); + } + + private float GetRadian(Vector3 from, Vector3 to) + { + var dx = to.X - from.X; + var dz = to.Z - from.Z; + + return MathF.Atan2(dx, dz); + } + + private bool TryGetTetherOrbAndPlayer(Event evt, ScriptAccessory sa, out uint orbId, out uint playerId, out int playerIndex) + { + var sourceId = evt.SourceId(); + var targetId = evt.TargetId(); + var sourceIndex = sa.Data.PartyList.IndexOf(sourceId); + var targetIndex = sa.Data.PartyList.IndexOf(targetId); + + if (IsValidPartyIndex(sourceIndex)) + { + playerId = sourceId; + playerIndex = sourceIndex; + orbId = targetId; + return orbId != 0 && playerId != 0; + } + + if (IsValidPartyIndex(targetIndex)) + { + playerId = targetId; + playerIndex = targetIndex; + orbId = sourceId; + return orbId != 0 && playerId != 0; + } + + orbId = sourceId; + playerId = targetId; + playerIndex = targetIndex; + return orbId != 0 && playerId != 0; + } + + private void RecordBlackBallTether(uint orbId, uint playerId) + { + lock (_commonMechanicLock) + { + var now = DateTime.UtcNow; + _blackBallTethers.RemoveAll(x => (now - x.Time).TotalMilliseconds > 15000 || x.PlayerId == playerId); + _blackBallTethers.Add((orbId, playerId, now)); + } + } + + private static void SetDrawTargetObject(DrawPropertiesEdit dp, uint targetId) + { + if (TrySetDrawProperty(dp, "TargetObject", targetId)) return; + if (TrySetDrawProperty(dp, "TargetId", targetId)) return; + TrySetDrawProperty(dp, "TargetID", targetId); + } + + private static bool TrySetDrawProperty(DrawPropertiesEdit dp, string propertyName, T value) + { + try + { + var prop = typeof(DrawPropertiesEdit).GetProperty(propertyName, BindingFlags.Public | BindingFlags.Instance); + if (prop == null || !prop.CanWrite) return false; + + var targetType = Nullable.GetUnderlyingType(prop.PropertyType) ?? prop.PropertyType; + object converted = value; + if (targetType != typeof(T)) + { + converted = Convert.ChangeType(value, targetType); + } + prop.SetValue(dp, converted); + return true; + } + catch + { + return false; + } + } + + private List 获取混沌激流标准顺时针顺序(ScriptAccessory sa) + { + // 默认 AddCombatant 顺序 + var normal = Enumerable.Range(0, 混沌激流黑球生成位置.Count).ToList(); + + if (混沌激流黑球生成位置.Count < 8) + { + return normal; + } + + var center = new Vector3(100f, 0f, 100f); + float totalDelta = 0f; + + for (int i = 0; i < 8; i++) + { + var p1 = 混沌激流黑球生成位置[i].vector3; + var p2 = 混沌激流黑球生成位置[(i + 1) % 8].vector3; + + var a1 = MathF.Atan2(p1.Z - center.Z, p1.X - center.X); + var a2 = MathF.Atan2(p2.Z - center.Z, p2.X - center.X); + + var delta = NormalizeRadian(a2 - a1); + totalDelta += delta; + } + + var isClockwise = totalDelta > 0f; + + if (isClockwise) + { + // 0 1 2 3 4 5 6 7 + return new List { 0, 1, 2, 3, 4, 5, 6, 7 }; + } + + // 0 1 2 3 4 5 6 7 -> 1 0 7 6 5 4 3 2 + return new List { 1, 0, 7, 6, 5, 4, 3, 2 }; + } + + private static float NormalizeRadian(float rad) + { + while (rad > MathF.PI) rad -= MathF.PI * 2f; + while (rad < -MathF.PI) rad += MathF.PI * 2f; + return rad; + } + private void P2_尝试分配无之涡流(ScriptAccessory sa) + { + var myIdx = sa.MyIndex(); + if (!IsValidPartyIndex(myIdx)) return; + + Vector3 targetPos = default; + bool shouldDraw = false; + bool isMyMarked = false; + + lock (P2无之涡流锁) + { + if (P2无之涡流已分配) return; + if (P2无之涡流点名玩家.Count < 4) return; + if (P2无之涡流塔数量 < 4) return; + + var towers = Enumerable.Range(0, 8) + .Where(i => P2无之涡流塔出现[i]) + .OrderBy(i => i) + .ToList(); + + var empties = Enumerable.Range(0, 8) + .Where(i => !P2无之涡流塔出现[i]) + .OrderBy(i => i) + .ToList(); + + var marked = P2无之涡流点名玩家 + .Distinct() + .OrderBy(i => i) + .ToList(); + + var unmarked = Enumerable.Range(0, 8) + .Where(i => !marked.Contains(i)) + .OrderBy(i => i) + .ToList(); + + isMyMarked = marked.Contains(myIdx); + + int assignedSpot = -1; + + if (P2打法 == P2打法法enum.MMW) + { + assignedSpot = P2_无之涡流_MMW分配(myIdx, marked, unmarked, towers, empties); + } + else + { + assignedSpot = P2_无之涡流_NOCCHH分配(myIdx, marked, unmarked, towers, empties); + } + + if (assignedSpot < 0 || assignedSpot > 7) return; + + targetPos = P2_无之涡流固定点位[assignedSpot]; + + if (P2无之涡流塔出现[assignedSpot]) + { + targetPos = P2无之涡流塔位置[assignedSpot]; + } + + P2无之涡流已分配 = true; + shouldDraw = true; + } + + if (!shouldDraw) return; + + var drawTargetPos = targetPos; + + if (isMyMarked) + { + var center = new Vector3(100f, 0f, 100f); + var dirToCenter = center - targetPos; + + if (dirToCenter.LengthSquared() > 0.001f) + { + drawTargetPos = targetPos + Vector3.Normalize(dirToCenter) * 10f; + } + } + + var dp = sa.WaypointDp( + drawTargetPos, + duration: 7000, + delay: 0, + name: $"P2-无之涡流-指路-{myIdx}" + ); + sa.Method.SendDraw(DrawModeEnum.Imgui, DrawTypeEnum.Displacement, dp); + + if (!isMyMarked) + { + var circle = sa.FastDp( + name: $"P2-无之涡流-目标点-{myIdx}", + pos: drawTargetPos, + duration: 7000, + scale: new Vector2(6f), + safe: true + ); + sa.Method.SendDraw(DrawModeEnum.Default, DrawTypeEnum.Circle, circle); + } + + _ = Task.Run(async () => + { + await Task.Delay(10000); + P2_重置无之涡流(); + }); + } + + private int P2_无之涡流_MMW分配( + int myIdx, + List marked, + List unmarked, + List towers, + List empties) + { + // 没点名的人去塔 + if (unmarked.Contains(myIdx)) + { + var rank = unmarked.IndexOf(myIdx); + if (rank < 0 || rank >= towers.Count) return -1; + return towers[rank]; + } + + // 被点名的人去空地 + if (marked.Contains(myIdx)) + { + var rank = marked.IndexOf(myIdx); + if (rank < 0 || rank >= empties.Count) return -1; + return empties[rank]; + } + + return -1; + } + + private int P2_无之涡流_NOCCHH分配( + int myIdx, + List marked, + List unmarked, + List towers, + List empties) + { + var isMarked = marked.Contains(myIdx); + var isUnmarked = unmarked.Contains(myIdx); + if (!isMarked && !isUnmarked) return -1; + + var isEven = IsEvenGroup(myIdx); + + var playerGroup = isMarked + ? marked.Where(i => IsEvenGroup(i) == isEven).OrderBy(i => i).ToList() + : unmarked.Where(i => IsEvenGroup(i) == isEven).OrderBy(i => i).ToList(); + + var rank = playerGroup.IndexOf(myIdx); + if (rank < 0) return -1; + + var sourceSpots = isMarked ? empties : towers; + + List targetSpots; + + if (isEven) + { + // 0/2/4/6 去左半场:index 4-7,按 index 从大到小 + targetSpots = sourceSpots + .Where(i => i >= 4 && i <= 7) + .OrderByDescending(i => i) + .ToList(); + } + else + { + // 1/3/5/7 去右半场:index 0-3,按 index 从小到大 + targetSpots = sourceSpots + .Where(i => i >= 0 && i <= 3) + .OrderBy(i => i) + .ToList(); + } + + if (rank >= targetSpots.Count) + { + return P2_无之涡流_MMW分配(myIdx, marked, unmarked, towers, empties); + } + + return targetSpots[rank]; + } + + private void P2_重置无之涡流() + { + lock (P2无之涡流锁) + { + P2无之涡流点名玩家.Clear(); + + for (int i = 0; i < 8; i++) + { + P2无之涡流塔出现[i] = false; + P2无之涡流塔位置[i] = default; + } + + P2无之涡流塔数量 = 0; + P2无之涡流已分配 = false; + } + + P2无之涡流BossId = 0; + } + + #endregion + + #endregion + + #region P1 + [ScriptMethod(name: "P1-核心熔毁集合分散", eventType: EventTypeEnum.StartCasting, eventCondition: ["ActionId:regex:^(50040)$"])] + public async void P1_核心熔毁(Event evt, ScriptAccessory sa) + { + var targetId = evt.TargetId(); + // if (targetId !=sa.Data.Me) return; + // sa.Method.SendChat($"/e {targetId}"); + // sa.Method.SendChat($"/e {sa.Data.Me}"); + + uint durationMilliseconds = 5200; + + if (durationMilliseconds<=0||durationMilliseconds>=7200000) + { + sa.Method.SendChat("/e DurationMilliseconds2"); + return; + } + + await Task.Delay(5000); + P1_热病buff提醒(evt, sa); + + + // 八方分散指路+画图+时间 + + await Task.Delay(2500); + for (int i = 0; i < sa.Data.PartyList.Count; i++) + { + var playerId = sa.Data.PartyList[i]; + + var dpFoot = sa.Data.GetDefaultDrawProperties(); + dpFoot.Name = $"核心熔毁-脚下黄圈-{i}-{playerId:X}"; + dpFoot.Owner = playerId; + dpFoot.Color = sa.Data.DefaultDangerColor; + dpFoot.DestoryAt = 3300; + dpFoot.ScaleMode = ScaleMode.ByTime; + dpFoot.Scale = new Vector2(5f); + + sa.Method.SendDraw(DrawModeEnum.Default, DrawTypeEnum.Circle, dpFoot); + } + for (int i = 0; i < sa.Data.PartyList.Count; i++) + { + var playerId = sa.Data.PartyList[i]; + + var obj = sa.Data.Objects.SearchById(playerId); + if (obj == null) continue; + + var pos = obj.Position; + + var dpFoot = sa.Data.GetDefaultDrawProperties(); + dpFoot.Name = $"核心熔毁-脚下定点黄圈-{i}-{playerId:X}"; + dpFoot.Position = pos; // 定点位置 + dpFoot.Color = sa.Data.DefaultDangerColor; + dpFoot.DestoryAt = 2300; + dpFoot.ScaleMode = ScaleMode.ByTime; + dpFoot.Scale = new Vector2(5f); + + sa.Method.SendDraw(DrawModeEnum.Default, DrawTypeEnum.Circle, dpFoot); + } + var dp = sa.Data.GetDefaultDrawProperties(); + dp.Name = $"核心熔毁-八方分散-{targetId:X}"; + dp.Owner = targetId; + dp.Color = sa.Data.DefaultDangerColor; + dp.DestoryAt = 3300; + dp.ScaleMode = ScaleMode.ByTime; + dp.Scale = new Vector2(5); + + sa.Method.SendDraw(DrawModeEnum.Default, DrawTypeEnum.Circle, dp); + + var myIdx = sa.MyIndex(); + if (myIdx < 0 || myIdx > 7) return; + + Vector3 wpos2 = myIdx switch + { + 0 => new Vector3(100f, 0.00f, 88f), + 1 => new Vector3(100f, 0.00f, 112f), + 2 => new Vector3(88f, 0.00f, 100f), + 3 => new Vector3(112f, 0.00f, 100f), + 4 => new Vector3(91.515f, 0.00f, 108.485f), // 左下 + 5 => new Vector3(108.485f, 0.00f, 108.485f), // 右下 + 6 => new Vector3(91.515f, 0.00f, 91.515f), // 左上 + 7 => new Vector3(108.485f, 0.00f, 91.515f), // 右上 + }; + + var dp2 = sa.WaypointDp(wpos2, 3300, 0, $"八方分散指路-{myIdx}"); + sa.Method.SendDraw(DrawModeEnum.Imgui, DrawTypeEnum.Displacement, dp2); + + + } + + private async void P1_热病buff提醒(Event evt, ScriptAccessory sa) + { + int durationMilliseconds = 1509; + + if (durationMilliseconds<=0||durationMilliseconds>=7200000) + { + return; + } + + if (热病冷却提示 == 热病提示enum.横幅) + { + sa.Method.TextInfo("停止移动,直到这行提示消失", durationMilliseconds,true); + } + else if (热病冷却提示 == 热病提示enum.默语) + { + sa.Method.SendChat("/e 停止移动"); + } + else if (热病冷却提示 == 热病提示enum.TTS) + { + sa.Method.TTS("停止移动"); + } + } + + [ScriptMethod(name: "P1-深度冻结(双T核爆)", eventType: EventTypeEnum.StartCasting, eventCondition: ["ActionId:regex:^(50044)$"])] + public async void P1_深度冻结(Event evt, ScriptAccessory sa) + { + if (_phase != 1) return; + + var myIdx = sa.MyIndex(); + if (!IsValidPartyIndex(myIdx)) return; + + const uint duration = 6000; + + if (myIdx == 0) + { + var dp = sa.WaypointDp(new Vector3(88f, 0f, 88f), duration, 0, "深度冻结-MT-左上"); + sa.Method.SendDraw(DrawModeEnum.Imgui, DrawTypeEnum.Displacement, dp); + } + else if (myIdx == 1) + { + var dp = sa.WaypointDp(new Vector3(112f, 0f, 88f), duration, 0, "深度冻结-ST-右上"); + sa.Method.SendDraw(DrawModeEnum.Imgui, DrawTypeEnum.Displacement, dp); + } + + await Task.Delay(5000); + + if (!P1_尝试设置深度冻结提醒()) + return; + + P1_冷却buff提醒(sa); + + await Task.Delay(10000); + P1_重置深度冻结提醒(); + } + private bool P1_尝试设置深度冻结提醒() + { + lock (P1深度冻结锁) + { + if (P1深度冻结冷却已提醒) return false; + + P1深度冻结冷却已提醒 = true; + return true; + } + } + + private void P1_重置深度冻结提醒() + { + lock (P1深度冻结锁) + { + P1深度冻结冷却已提醒 = false; + } + } + private void P1_冷却buff提醒(ScriptAccessory sa) + { + int durationMilliseconds = 2509; + + if (热病冷却提示 == 热病提示enum.横幅) + { + sa.Method.TextInfo("持续移动,直到这行提示消失", durationMilliseconds, true); + } + else if (热病冷却提示 == 热病提示enum.默语) + { + sa.Method.SendChat("/e 持续移动"); + } + else if (热病冷却提示 == 热病提示enum.TTS) + { + sa.Method.TTS("持续移动"); + } + } + + #endregion + + #region P2 + [ScriptMethod(name: "P2-无之涡流-Boss记录", eventType: EventTypeEnum.AddCombatant, eventCondition: ["DataId:regex:^(19911)$"], userControl: false)] + public void P2_无之涡流_Boss记录(Event evt, ScriptAccessory sa) + { + if (_phase != 2) return; + + var sourceId = evt.SourceId(); + if (sourceId == 0) return; + + P2无之涡流BossId = sourceId; + } + [ScriptMethod(name: "P2转阶段", eventType: EventTypeEnum.StartCasting, eventCondition: ["ActionId:regex:^(50010)$"], userControl: false)] + public async void P2转阶段(Event evt, ScriptAccessory sa) + { + // 无之领域 + _phase = 2; + } + + [ScriptMethod(name: "P2-虚无大冲击自动防击退", eventType: EventTypeEnum.StartCasting, eventCondition: ["ActionId:regex:^(49369)$"])] + public async void 虚无大冲击自动防击退(Event evt, ScriptAccessory sa) + { + await Task.Delay(1000); + + sa.Method.SendChat("/ac 沉稳咏唱"); + sa.Method.SendChat("/ac 亲疏自行"); + } + + [ScriptMethod(name: "P2-无之涡流-点名记录", eventType: EventTypeEnum.TargetIcon, eventCondition: ["Id:regex:^(02D1)$"])] + public async void P2_无之涡流_点名记录(Event evt, ScriptAccessory sa) + { + if (_phase != 2) return; + + var targetId = evt.TargetId(); // 被点名玩家 + var targetIdx = sa.Data.PartyList.IndexOf(targetId); + if (!IsValidPartyIndex(targetIdx)) return; + + lock (P2无之涡流锁) + { + if (!P2无之涡流点名玩家.Contains(targetIdx)) + { + P2无之涡流点名玩家.Add(targetIdx); + } + } + + // 给所有被点名的人画一个 60° 危险扇形(从 boss 到人) + // 60° = PI / 3 + DrawFanFromPositionToTarget( + sa, + name: $"P2-无之涡流-点名扇形-{targetIdx}", + fromPos: new Vector3(100f, 0f, 100f), + targetId: targetId, + range: 60f, + radian: MathF.PI / 3f, + duration: 8000, + color: sa.Data.DefaultDangerColor + ); + + P2_尝试分配无之涡流(sa); + } + + [ScriptMethod(name: "P2-无之涡流(扇形踩塔)", eventType: EventTypeEnum.StartCasting, eventCondition: ["ActionId:regex:^(50013)$"])] + public async void P2_无之涡流(Event evt, ScriptAccessory sa) + { + if (_phase != 2) return; + + await Task.Delay(200); + + var pos = evt.SourcePosition(); + var towerIndex = P2_获取无之涡流塔Index(pos); + if (towerIndex < 0) return; + + lock (P2无之涡流锁) + { + if (P2无之涡流塔出现[towerIndex]) + { + return; + } + + P2无之涡流塔出现[towerIndex] = true; + P2无之涡流塔位置[towerIndex] = pos; + P2无之涡流塔数量++; + } + + P2_尝试分配无之涡流(sa); + } + + private readonly object _p2EyeLock = new(); + private readonly HashSet _p2EyeTargets = new(); + private bool _p2EyeRefreshing = false; + + [ScriptMethod( + name: "P2-恶魔之瞳(背对)", + eventType: EventTypeEnum.StartCasting, + eventCondition: ["ActionId:regex:^(50022)$"] + )] + public async void P2_恶魔之瞳(Event evt, ScriptAccessory sa) + { + if (_phase != 2) return; + + var sourceId = evt.SourceId(); + if (sourceId == 0) return; + + // 画一个20m的imgui环。 + var dp2 = sa.Data.GetDefaultDrawProperties(); + dp2.Name = $"P2 恶魔之瞳-圈-{sourceId:X}"; + dp2.Owner = sourceId; + dp2.Scale = new Vector2(20f); + dp2.Color = new Vector4(1f, 1f, 0f, 0.1f); + dp2.DestoryAt = 4700; + // dp2.ScaleMode = ScaleMode.ByTime; + + sa.Method.SendDraw(DrawModeEnum.Imgui, DrawTypeEnum.Circle, dp2); + + lock (_p2EyeLock) + { + _p2EyeTargets.Add(sourceId); + + if (_p2EyeRefreshing) + return; + + _p2EyeRefreshing = true; + } + + const uint refreshDuration = 300; + const int refreshInterval = 250; + const int totalDuration = 4700; + const float validRange = 20f; + + try + { + var startTime = DateTime.Now; + + while ((DateTime.Now - startTime).TotalMilliseconds < totalDuration) + { + if (_phase != 2) + break; + + List targets; + + lock (_p2EyeLock) + { + targets = _p2EyeTargets.ToList(); + } + + var myObj = sa.Data.Objects.SearchById(sa.Data.Me); + if (myObj == null) + { + await Task.Delay(refreshInterval); + continue; + } + + var myPos = myObj.Position; + + foreach (var targetId in targets) + { + var targetObj = sa.Data.Objects.SearchById(targetId); + if (targetObj == null) + continue; + + var targetPos = targetObj.Position; + var distance = Vector3.Distance(myPos, targetPos); + + // 只有目标在 validRange 内,才参与 SightAvoid + if (distance > validRange) + continue; + + // sa.Method.SendChat($"/e sourceid: {sourceId} distance: {distance}"); + var dp = sa.Data.GetDefaultDrawProperties(); + + dp.Name = $"P2 恶魔之瞳-{targetId:X}"; + dp.Color = sa.Data.DefaultDangerColor; + dp.Owner = sa.Data.Me; + dp.TargetObject = targetId; + dp.DestoryAt = refreshDuration; + + sa.Method.SendDraw( + DrawModeEnum.Default, + DrawTypeEnum.SightAvoid, + dp + ); + } + + await Task.Delay(refreshInterval); + } + } + finally + { + lock (_p2EyeLock) + { + _p2EyeTargets.Clear(); + _p2EyeRefreshing = false; + } + + sa.Method.RemoveDraw("P2 恶魔之瞳-.*"); + } + } + + #endregion + + #region P3 + [ScriptMethod(name: "P3转场", eventType: EventTypeEnum.StartCasting, eventCondition: ["ActionId:regex:^(50029)$"], userControl: false)] + public async void P3转场(Event evt, ScriptAccessory sa) + { + // 无光的世界 + _phase = 3; + } + + [ScriptMethod(name: "P3-聚能波动", eventType: EventTypeEnum.StartCasting, eventCondition: ["ActionId:regex:^(49985|49986)$"], userControl: false)] + public async void P3_聚能波动(Event evt, ScriptAccessory sa) + { + if (_phase != 3) return; + + var actionId = evt.ActionId(); + var sourceId = evt.SourceId(); + if (sourceId == 0) return; + + var pos = evt.SourcePosition(); + var rot = evt.SourceRotation(); + + if (actionId == 49985) + { + // 49985 矩形 + var dp = sa.Data.GetDefaultDrawProperties(); + dp.Name = $"P3-聚能波动-矩形-{sourceId:X}"; + dp.Owner = 0; + dp.Position = pos; + dp.Rotation = rot; + dp.Scale = new Vector2(16f, 80f); + dp.Color = sa.Data.DefaultDangerColor; + dp.DestoryAt = 6300; + dp.ScaleMode = ScaleMode.ByTime; + + sa.Method.SendDraw(DrawModeEnum.Default, DrawTypeEnum.Rect, dp); + } + else if (actionId == 49986) + { + // 49986 辣翅 + var dp = sa.Data.GetDefaultDrawProperties(); + dp.Name = $"P3-聚能波动-辣翅-{sourceId:X}"; + dp.Owner = sourceId; + dp.Scale = new Vector2(16f, 80f); + dp.Offset = new Vector3(-16f, 0f, 0f); + dp.Color = sa.Data.DefaultDangerColor; + dp.DestoryAt = 5700; + dp.ScaleMode = ScaleMode.ByTime; + + sa.Method.SendDraw(DrawModeEnum.Default, DrawTypeEnum.Rect, dp); + + var dp2 = sa.Data.GetDefaultDrawProperties(); + dp2.Name = $"P3-聚能波动-辣翅-{sourceId:X}"; + dp2.Owner = sourceId; + dp2.Scale = new Vector2(16f, 800f); + dp2.Offset = new Vector3(16f, 0f, 0f); + dp2.Color = sa.Data.DefaultDangerColor; + dp2.DestoryAt = 5700; + dp2.ScaleMode = ScaleMode.ByTime; + + sa.Method.SendDraw(DrawModeEnum.Default, DrawTypeEnum.Rect, dp2); + } + } + + [ScriptMethod(name: "P3-暗影神圣(双奶分摊)", eventType: EventTypeEnum.StartCasting, eventCondition: ["ActionId:regex:^(50045)$"], userControl: false)] + public async void P3_暗影神圣(Event evt, ScriptAccessory sa) + { + if (_phase != 3) return; + + var myIdx = sa.MyIndex(); + if (!IsValidPartyIndex(myIdx)) return; + + const uint duration = 6000; + const float range = 6f; + + var index2Id = sa.Data.PartyList[2]; + var index3Id = sa.Data.PartyList[3]; + if (index2Id == 0 || index3Id == 0) return; + + // 偶数组安全,奇数组危险 + var dp2 = sa.Data.GetDefaultDrawProperties(); + dp2.Name = "P3-暗影神圣-index2"; + dp2.Owner = index2Id; + dp2.Scale = new Vector2(range); + dp2.Color = IsEvenGroup(myIdx) ? sa.Data.DefaultSafeColor : sa.Data.DefaultDangerColor; + dp2.DestoryAt = duration; + dp2.ScaleMode = ScaleMode.ByTime; + sa.Method.SendDraw(DrawModeEnum.Default, DrawTypeEnum.Circle, dp2); + + // 奇数组安全,偶数组危险 + var dp3 = sa.Data.GetDefaultDrawProperties(); + dp3.Name = "P3-暗影神圣-index3"; + dp3.Owner = index3Id; + dp3.Scale = new Vector2(range); + dp3.Color = IsOddGroup(myIdx) ? sa.Data.DefaultSafeColor : sa.Data.DefaultDangerColor; + dp3.DestoryAt = duration; + dp3.ScaleMode = ScaleMode.ByTime; + sa.Method.SendDraw(DrawModeEnum.Default, DrawTypeEnum.Circle, dp3); + } + + #endregion + + #region 时间轴 + [ScriptTimeline("恩欧歼殛战")] + const string EnuoTimeline = """ + # info 在技能生效(ActionEffect)的时刻,sync 在读条开始 + # 无之膨胀(钢铁/月环)、奔流(扇形大小)有随机变体,sync 用 Boss 本体读条或整组 regex + # P1 + 10.20 sync eventName=StartCasting ActionId=50049 window=10.2 timeout=10.2 + 15.20 info text=流星雨(AOE) + 22.59 sync eventName=StartCasting ActionId=regex:^(49975|49976)$ window=12.4 timeout=12.4 + 31.37 info level=important text=无之膨胀(单/双奶分摊) + 40.77 sync eventName=StartCasting ActionId=50040 + 45.24 info level=important text=核心熔毁(热病) + 53.37 sync eventName=StartCasting ActionId=regex:^(50032|50033) + 58.37 info level=important text=扩散/集束波动(分摊) + 60.75 sync eventName=StartCasting ActionId=regex:^(49975|49976)$ + 69.57 info level=important text=无之膨胀(单/双奶分摊) + 78.14 sync eventName=StartCasting ActionId=50002 window=17.4 timeout=17.4 + 112.49 sync eventName=StartCasting ActionId=regex:^(49995|49996|49997)$ + 116.49 info text=奔流 + 117.04 sync eventName=StartCasting ActionId=50001 + 118.50 info text=无之漩涡 + 118.67 sync eventName=StartCasting ActionId=regex:^(50032|50033) + 123.72 info level=important text=扩散/集束波动(分摊) + 128.87 sync eventName=StartCasting ActionId=50044 window=10.2 timeout=10.2 + 133.82 info level=important text=深度冻结(冰冻持续移动) + 137.02 sync eventName=StartCasting ActionId=50049 + 141.97 info text=流星雨(AOE) + # P2 无之深渊 + 150.64 sync eventName=StartCasting ActionId=50010 window=13.6 timeout=13.6 + 155.66 info text=无之领域(P2) + 169.94 sync eventName=StartCasting ActionId=49369 window=19.3 timeout=19.3 + 175.94 info level=important text=虚无大冲击(击退) + # P3 + 254.55 sync eventName=StartCasting ActionId=50029 window=9.7 timeout=9.7 + 265.54 info text=无光的世界(持续AOE) + 274.72 sync eventName=StartCasting ActionId=49972 window=20.2 timeout=20.2 + 279.75 info text=至高无上(AOE) + 285.47 sync eventName=StartCasting ActionId=regex:^(49975|49976)$ window=10.8 timeout=10.8 + 294.29 info level=important text=无之膨胀(单/双奶分摊) + 299.59 sync eventName=StartCasting ActionId=49973 window=14.1 timeout=14.1 + 303.69 sync eventName=StartCasting ActionId=49986 + 309.64 info text=聚能波动 + 313.95 sync eventName=StartCasting ActionId=49985 window=10.3 timeout=10.3 + 314.99 sync eventName=StartCasting ActionId=49986 + 320.95 info text=聚能波动 + 321.04 sync eventName=StartCasting ActionId=50046 + 327.00 info level=important text=暗影神圣(双奶分摊) + 334.49 sync eventName=StartCasting ActionId=regex:^(49975|49976)$ window=13.4 timeout=13.4 + 343.25 info level=important text=无之膨胀(单/双奶分摊) + 348.57 sync eventName=StartCasting ActionId=50047 window=14.1 timeout=14.1 + 353.92 info level=important text=零次元(持续分摊) + 364.44 sync eventName=StartCasting ActionId=49994 window=15.9 timeout=15.9 + 366.39 info text=无之漩涡 + 368.55 sync eventName=StartCasting ActionId=regex:^(49995|49996|49997)$ + 368.55 sync eventName=StartCasting ActionId=50040 + 372.60 info text=奔流 + 373.12 sync eventName=StartCasting ActionId=50001 + 373.47 info level=important text=核心熔毁(热病) + 374.59 info text=无之漩涡 + 379.12 info text=核心熔毁 + 385.77 sync eventName=StartCasting ActionId=50002 window=12.2 timeout=12.2 + 419.99 sync eventName=StartCasting ActionId=49972 window=30 timeout=30 + 424.99 info text=至高无上(AOE) + 433.37 sync eventName=StartCasting ActionId=49973 window=13.4 timeout=13.4 + 435.29 info text=无之活性 + 437.52 sync eventName=StartCasting ActionId=49992 + 438.50 sync eventName=StartCasting ActionId=49986 + 438.59 sync eventName=StartCasting ActionId=48475 + 443.46 info level=important text=无之追踪 + 458.80 sync eventName=StartCasting ActionId=49985 window=20.2 timeout=20.2 + 465.84 info text=聚能波动 + 466.80 sync eventName=StartCasting ActionId=regex:^(50032|50033) + 471.86 info level=important text=扩散/集束波动(分摊) + 477.14 sync eventName=StartCasting ActionId=49992 window=10.3 timeout=10.3 + 478.12 sync eventName=StartCasting ActionId=49986 + 478.20 sync eventName=StartCasting ActionId=48475 + 483.07 info level=important text=无之追踪 + 498.35 sync eventName=StartCasting ActionId=49985 window=20.2 timeout=20.2 + 505.39 info text=聚能波动 + 506.39 sync eventName=StartCasting ActionId=regex:^(50032|50033) + 511.39 info level=important text=扩散/集束波动(分摊) + 515.54 sync eventName=StartCasting ActionId=50047 + 520.87 info level=important text=零次元(持续分摊) + """; + + [ScriptTimelineI18n(ScriptTimelineLang.ChineseSimplified)] + static readonly Dictionary EnuoTimelineI18n = new() + { + ["流星雨(AOE)"] = "流星雨(AOE)", + ["无之膨胀(单/双奶分摊)"] = "无之膨胀(单/双奶分摊)", + ["核心熔毁(热病)"] = "核心熔毁(热病)", + ["扩散/集束波动(分摊)"] = "扩散/集束波动(分摊)", + ["奔流"] = "奔流", + ["无之漩涡"] = "无之漩涡", + ["深度冻结(冰冻持续移动)"] = "深度冻结(冰冻持续移动)", + ["无之领域(P2)"] = "无之领域(P2)", + ["虚无大冲击(击退)"] = "虚无大冲击(击退)", + ["无光的世界(持续AOE)"] = "无光的世界(持续AOE)", + ["至高无上(AOE)"] = "至高无上(AOE)", + ["聚能波动"] = "聚能波动", + ["暗影神圣(双奶分摊)"] = "暗影神圣(双奶分摊)", + ["零次元(持续分摊)"] = "零次元(持续分摊)", + ["核心熔毁"] = "核心熔毁", + ["无之活性"] = "无之活性", + ["无之追踪"] = "无之追踪", + }; + + [ScriptTimelineI18n(ScriptTimelineLang.English)] + static readonly Dictionary EnuoTimelineI18n_English = new() + { + ["流星雨(AOE)"] = "Meteorain (AOE)", + ["无之膨胀(单/双奶分摊)"] = "Naught Grows (Party/Light Party Stack)", + ["核心熔毁(热病)"] = "Meltdown (Pyretic)", + ["扩散/集束波动(分摊)"] = "Airy/Dense Emptiness (Stack)", + ["奔流"] = "Silent Torrent", + ["无之漩涡"] = "Vacuum", + ["深度冻结(冰冻持续移动)"] = "Deep Freeze (Keep Moving)", + ["无之领域(P2)"] = "All for Naught (P2)", + ["虚无大冲击(击退)"] = "Looming Emptiness (Knockback)", + ["无光的世界(持续AOE)"] = "Lightless World (Multi-hit AOE)", + ["至高无上(AOE)"] = "Almagest (AOE)", + ["聚能波动"] = "Passage of Naught", + ["暗影神圣(双奶分摊)"] = "Shrouded Holy (Healer Stacks)", + ["零次元(持续分摊)"] = "Dimension Zero (Multi-hit Stack)", + ["核心熔毁"] = "Meltdown", + ["无之活性"] = "Naught Wakes", + ["无之追踪"] = "Naught Hunts", + }; + #endregion +} + +#region Helpers + +public static class EventExtensions +{ + public static float SourceRotation(this Event evt) + => JsonConvert.DeserializeObject(evt["SourceRotation"]); + private static bool ParseHexId(string? idStr, out uint id) + { + id = 0; + if (string.IsNullOrEmpty(idStr)) return false; + try + { + var idStr2 = idStr.Replace("0x", ""); + id = uint.Parse(idStr2, System.Globalization.NumberStyles.HexNumber); + return true; + } + catch + { + return false; + } + } + + public static uint ActionId(this Event evt) => JsonConvert.DeserializeObject(evt["ActionId"]); + public static uint DataId(this Event evt) => JsonConvert.DeserializeObject(evt["DataId"]); + public static uint SourceId(this Event evt) => ParseHexId(evt["SourceId"], out var id) ? id : 0; + public static uint TargetId(this Event evt) => ParseHexId(evt["TargetId"], out var id) ? id : 0; + public static Vector3 SourcePosition(this Event evt) => ParseVector3(evt["SourcePosition"]); + public static Vector3 EffectPosition(this Event evt) => ParseVector3(evt["EffectPosition"]); + public static uint DirectorId(this Event evt) => ParseHexId(evt["DirectorId"], out var id) ? id : 0; + + private static Vector3 ParseVector3(string? raw) + { + if (string.IsNullOrWhiteSpace(raw)) + return Vector3.Zero; + + raw = raw.Trim(); + + // 1. 先尝试标准 JSON + try + { + return JsonConvert.DeserializeObject(raw); + } + catch + { + } + + // 非标准格式 + try + { + float x = ExtractFloat(raw, "X"); + float y = ExtractFloat(raw, "Y"); + float z = ExtractFloat(raw, "Z"); + return new Vector3(x, y, z); + } + catch + { + return Vector3.Zero; + } + } + + private static float ExtractFloat(string raw, string key) + { + var pattern = $@"{key}\s*[:=]\s*(-?\d+(?:[\.,]\d+)?)"; + var match = System.Text.RegularExpressions.Regex.Match( + raw, + pattern, + System.Text.RegularExpressions.RegexOptions.IgnoreCase + ); + + if (!match.Success) + return 0f; + + // 兼容小数点是 comma 的情况,比如 100,25 + var value = match.Groups[1].Value.Replace(',', '.'); + + return float.Parse( + value, + System.Globalization.CultureInfo.InvariantCulture + ); + } +} + +public static class ScriptAccessoryExtensions +{ + public static int MyIndex(this ScriptAccessory sa) => sa.Data.PartyList.IndexOf(sa.Data.Me); + + public static DrawPropertiesEdit FastDp(this ScriptAccessory sa, string name, Vector3 pos, uint duration, Vector2 scale, bool safe = false) + { + var dp = sa.Data.GetDefaultDrawProperties(); + dp.Name = name; + dp.Color = safe ? sa.Data.DefaultSafeColor : sa.Data.DefaultDangerColor; + dp.Position = pos; + dp.DestoryAt = duration; + dp.Scale = scale; + return dp; + } + + public static DrawPropertiesEdit WaypointToObjectDp( + this ScriptAccessory sa, + uint targetObjectId, + uint duration, + uint delay = 0, + string name = "WaypointToObject", + Vector4? color = null) + { + var dp = sa.Data.GetDefaultDrawProperties(); + dp.Name = name; + dp.Color = color ?? sa.Data.DefaultSafeColor; + dp.Owner = sa.Data.Me; + dp.DestoryAt = duration; + dp.Delay = delay; + dp.Scale = new Vector2(2); + dp.ScaleMode = ScaleMode.YByDistance; + + var prop = + typeof(DrawPropertiesEdit).GetProperty("TargetObject") ?? + typeof(DrawPropertiesEdit).GetProperty("TargetId") ?? + typeof(DrawPropertiesEdit).GetProperty("TargetID"); + + if (prop != null && prop.CanWrite) + { + var targetType = Nullable.GetUnderlyingType(prop.PropertyType) ?? prop.PropertyType; + object value = targetObjectId; + + if (targetType != typeof(uint)) + { + value = Convert.ChangeType(targetObjectId, targetType); + } + + prop.SetValue(dp, value); + } + + return dp; + } + public static DrawPropertiesEdit WaypointDp(this ScriptAccessory sa, Vector3 target, uint duration, uint delay = 0, string name = "Waypoint", Vector4? color = null) + { + var dp = sa.Data.GetDefaultDrawProperties(); + dp.Name = name; + dp.Color = color ?? sa.Data.DefaultSafeColor; + dp.Owner = sa.Data.Me; // 仍然绑定自己 + dp.TargetPosition = target; + dp.DestoryAt = duration; + dp.Delay = delay; + dp.Scale = new Vector2(2); + dp.ScaleMode = ScaleMode.YByDistance; + return dp; + } + + public static DrawPropertiesEdit WaypointFromToDp(this ScriptAccessory sa, Vector3 from, Vector3 to, uint duration, uint delay = 0, string name = "WaypointFromTo", Vector4? color = null) + { + var dp = sa.Data.GetDefaultDrawProperties(); + dp.Name = name; + dp.Color = color ?? sa.Data.DefaultSafeColor; + dp.Owner = 0; // 不绑定任何物体,防止跟着人跑 + dp.Position = from; // 起点 + dp.TargetPosition = to; // 终点 + dp.DestoryAt = duration; + dp.Delay = delay; + dp.Scale = new Vector2(2); + dp.ScaleMode = ScaleMode.YByDistance; + return dp; + } +} + +#endregion \ No newline at end of file diff --git a/Codaaaaaa/G18.cs b/Codaaaaaa/G18.cs new file mode 100644 index 0000000..7aaccb8 --- /dev/null +++ b/Codaaaaaa/G18.cs @@ -0,0 +1,274 @@ +using System; +using System.Linq; +using System.Numerics; +using System.Collections.Generic; +using Newtonsoft.Json; +using KodakkuAssist.Script; +using KodakkuAssist.Module.GameEvent; +using KodakkuAssist.Module.Draw; +using KodakkuAssist.Module.Draw.Manager; +using KodakkuAssist.Data; +using KodakkuAssist.Extensions; + +namespace Codaaaaaa.G18; + +[ScriptType( + guid: "8f2a1d6d-4d3b-4c61-8b9a-6a4b8d0c8b2a", + name: "g18神秘剧透小脚本", + territorys: [1279], + version: "0.0.0.3", + author: "Codaaaaaa", + note: "剧透")] +public class G18 +{ + #region User Settings + [UserSetting("Debug")] + public bool ChatOutput { get; set; } = false; + #endregion + + private readonly List _historyRound = new(); + + private readonly List _roundBuf = new(); + + private static readonly TimeSpan RoundResetGap = TimeSpan.FromSeconds(3); + + private long _seq = 0; // 用于稳定排序(同index时按到达顺序) + + private sealed class RoundItem + { + public int Index { get; } + public string Text { get; } + public long Seq { get; } + + public RoundItem(int index, string text, long seq) + { + Index = index; + Text = text; + Seq = seq; + } + } + + public void Init(ScriptAccessory sa) + { + sa.Method.RemoveDraw(".*"); + _historyRound.Clear(); + // _roundBuf.Clear(); + // _lastRecordTime = DateTime.MinValue; + _seq = 0; + } + + [ScriptMethod(name: "记录", eventType: EventTypeEnum.EnvControl, eventCondition: ["Flag:regex:^(1|2)$"], userControl: false)] + public void 记录(Event evt, ScriptAccessory sa) + { + if (!TryGetInt(evt, "Index", out var index)) return; + + // 允许:0-20 + 27-31 + if (!IsValidIndex(index)) return; + + // 隔太久就清空,认为新一轮 + // var now = DateTime.UtcNow; + // if (_lastRecordTime != DateTime.MinValue && (now - _lastRecordTime) > RoundResetGap) + // { + // DebugPrint(sa, $"[G18] Round reset by gap. buf cleared. gap={(now - _lastRecordTime).TotalMilliseconds:0}ms"); + // _roundBuf.Clear(); + // } + // _lastRecordTime = now; + + var txt = MapIndexToText(index); + + _roundBuf.Add(new RoundItem(index, txt, ++_seq)); + DebugPrint(sa, $"[G18] Record index={index} => {txt}, bufCount={_roundBuf.Count}"); + + // 收满3个就结算(按index排序) + if (_roundBuf.Count >= 3) + { + var ordered = _roundBuf + .OrderBy(x => x.Index) // index 越大越靠后 + .ThenBy(x => x.Seq) // 同index时按到达顺序 + .Take(3) + .Select(x => x.Text) + .ToList(); + + var line = string.Join(" / ", ordered); + // _historyRound.Add(line); + + DebugPrint(sa, $"[G18] Round sealed => {line}"); + + // _roundBuf.Clear(); + } + } + + [ScriptMethod(name: "变动播报", eventType: EventTypeEnum.EnvControl, eventCondition: ["Flag:regex:^(64|128|256|512|1024|2048|4096|8192|16384)$"])] + public void 变动播报(Event evt, ScriptAccessory sa) + { + if (!TryGetInt(evt, "Index", out var index)) return; + if (index >= 21 && index <= 26) return; + + DebugPrint(sa, $"变动"); + sa.Method.TTS("变动"); + sa.Method.TextInfo("变动", 5500, true); + } + + [ScriptMethod(name: "剧透报", eventType: EventTypeEnum.ObjectEffect, eventCondition: ["SourceName:潜网巡梦"])] + public void 剧透报(Event evt, ScriptAccessory sa) + { + if (TryGetInt(evt, "Id1", out var id1) && id1 != 1) return; + + DebugPrint(sa, "[G18] 1."); + // 1) 优先播报当前缓冲(即使没满3个也可以发) + if (_roundBuf.Count > 0) + { + var ordered = _roundBuf + .OrderBy(x => x.Index) + .ThenBy(x => x.Seq) + .Select(x => x.Text) + .ToList(); + + var msg = $"[G18]:{string.Join(" / ", ordered)}"; + Output(sa, msg); + _roundBuf.Clear(); + return; + } + + // 2) 否则播报最后一条封存历史 + // if (_historyRound.Count > 0) + // { + // var last = _historyRound.Last(); + // Output(sa, $"[G18]:{last}"); + // return; + // } + + DebugPrint(sa, "[G18] No records yet."); + } + + // ---------------- Core Mapping ---------------- + + private static bool IsValidIndex(int index) + => (index >= 0 && index <= 20) || (index >= 27 && index <= 31); + + private static string MapIndexToText(int index) + { + // 27-31:固定映射 + if (index >= 27 && index <= 31) + { + return index switch + { + 27 => "心碎", + 28 => "终钻", + 29 => "三钻", + 30 => "终钻", + 31 => "宝箱", + _ => "?" + }; + } + + // 0-20:循环规律 + var m = index % 7; + return m switch + { + 0 => "心碎", + 1 => "小钻", + 2 => "三钻", + 3 => "终钻", + 4 => "宝箱", + 5 => "彩", + 6 => "小钻", + _ => "?" + }; + } + + // ---------------- Output Helpers ---------------- + + private void Output(ScriptAccessory sa, string msg) + { + if (ChatOutput) + DebugPrint(sa, msg); + + sa.Method.TextInfo(msg, 3000, false); + } + + private void DebugPrint(ScriptAccessory sa, string msg) + { + if (ChatOutput) + sa.Method.SendChat($"/e {msg}"); + } + + private static bool TryGetInt(Event evt, string key, out int value) + { + value = 0; + try + { + var s = evt[key]; + if (string.IsNullOrEmpty(s)) return false; + + if (s.StartsWith("0x", StringComparison.OrdinalIgnoreCase)) + return int.TryParse(s.Substring(2), System.Globalization.NumberStyles.HexNumber, null, out value); + + return int.TryParse(s, out value); + } + catch + { + return false; + } + } +} + +#region Helpers + +public static class EventExtensions +{ + private static bool ParseHexId(string? idStr, out uint id) + { + id = 0; + if (string.IsNullOrEmpty(idStr)) return false; + try + { + var idStr2 = idStr.Replace("0x", ""); + id = uint.Parse(idStr2, System.Globalization.NumberStyles.HexNumber); + return true; + } + catch + { + return false; + } + } + + public static uint ActionId(this Event evt) => JsonConvert.DeserializeObject(evt["ActionId"]); + public static uint SourceId(this Event evt) => ParseHexId(evt["SourceId"], out var id) ? id : 0; + public static uint TargetId(this Event evt) => ParseHexId(evt["TargetId"], out var id) ? id : 0; + public static Vector3 SourcePosition(this Event evt) => JsonConvert.DeserializeObject(evt["SourcePosition"]); + public static Vector3 EffectPosition(this Event evt) => JsonConvert.DeserializeObject(evt["EffectPosition"]); + public static uint DirectorId(this Event evt) => ParseHexId(evt["DirectorId"], out var id) ? id : 0; +} + +public static class ScriptAccessoryExtensions +{ + public static int MyIndex(this ScriptAccessory sa) => sa.Data.PartyList.IndexOf(sa.Data.Me); + + public static DrawPropertiesEdit FastDp(this ScriptAccessory sa, string name, Vector3 pos, uint duration, Vector2 scale, bool safe = false) + { + var dp = sa.Data.GetDefaultDrawProperties(); + dp.Name = name; + dp.Color = safe ? sa.Data.DefaultSafeColor : sa.Data.DefaultDangerColor; + dp.Position = pos; + dp.DestoryAt = duration; + dp.Scale = scale; + return dp; + } + + public static DrawPropertiesEdit WaypointDp(this ScriptAccessory sa, Vector3 pos, uint duration, uint delay = 0, string name = "Waypoint") + { + var dp = sa.Data.GetDefaultDrawProperties(); + dp.Name = name; + dp.Color = sa.Data.DefaultSafeColor; + dp.Owner = sa.Data.Me; + dp.TargetPosition = pos; + dp.DestoryAt = duration; + dp.Delay = delay; + dp.Scale = new Vector2(2); + dp.ScaleMode = ScaleMode.YByDistance; + return dp; + } +} + +#endregion diff --git a/Codaaaaaa/Kefka.cs b/Codaaaaaa/Kefka.cs new file mode 100644 index 0000000..4528751 --- /dev/null +++ b/Codaaaaaa/Kefka.cs @@ -0,0 +1,1920 @@ +using System; +using System.Linq; +using System.Numerics; +using System.Collections.Generic; +using System.Threading.Tasks; +using Newtonsoft.Json; +using KodakkuAssist.Script; +using KodakkuAssist.Module.GameEvent; +using KodakkuAssist.Module.Draw; +using KodakkuAssist.Module.Draw.Manager; +using KodakkuAssist.Data; +using KodakkuAssist.Extensions; + +namespace Codaaaaaa.Kefka; + +[ScriptType( + guid: "cc2c6d88-abe5-40be-89da-5f231b9d21d8", + name: "绝凯夫卡P1指路先行版", + territorys: [1363], + version: "0.0.1.5", + author: "Codaaaaaa", + note: "自用拼好挂。请支持K佬&灵视佬")] +public class Kefka +{ + #region 用户设置 + + internal static bool DebugEnabled; + + [UserSetting("调试:输出调试日志")] + public bool DebugLog + { + get => DebugEnabled; + set => DebugEnabled = value; + } + + public enum BigIceSealGuideMode + { + 斜角黑泥, + _1A四人分摊_TH上半场, + _1A四人分摊_MT组上半场, + _1A四人分摊_T远北奶近南, + Yan式八人全分摊, + } + + public enum BigIceSealMtUpperHalfTankMode + { + 不交换分组_MT上半场, + 交换分组_ST上半场, + } + + public enum Statue3ArrowGuideMode + { + _1A, + _1A改, + 方逆, + 方顺, + } + + public enum Statue3TetherGuideMode + { + 魅惑在外, + 魅惑在内, + 全固定, + } + + private enum Statue3TetherType + { + Unknown, + 睡眠, + 魅惑, + } + + public enum XuanHuHuGuideMode + { + 固定半场, + 盗火烬, + } + + [UserSetting("P1_神像1攻略")] + public XuanHuHuGuideMode XuanHuHuMode { get; set; } = XuanHuHuGuideMode.盗火烬; + + [UserSetting("P1_神像2攻略")] + public BigIceSealGuideMode BigIceSealMode { get; set; } = BigIceSealGuideMode.斜角黑泥; + + [UserSetting("P1_神像2指路_双T是否交换位置 只影响'斜角黑泥'和'_1A四人分摊_MT组上半场'")] + public BigIceSealMtUpperHalfTankMode BigIceSealMtUpperHalfTank { get; set; } = BigIceSealMtUpperHalfTankMode.不交换分组_MT上半场; + + [UserSetting("P1_神像3箭头攻略")] + public Statue3ArrowGuideMode Statue3ArrowMode { get; set; } = Statue3ArrowGuideMode.方逆; + + [UserSetting("P1_神像3_睡眠魅惑攻略")] + public Statue3TetherGuideMode Statue3TetherMode { get; set; } = Statue3TetherGuideMode.魅惑在外; + + #endregion + + #region 全局变量与初始化 + + // P1 子阶段: + // 1.0 = P1 开场/未记录神像 + // 1.1 = P1 神像1 + // 1.2 = P1 神像2 + // 1.3 = P1 神像3 + // 2.0 = P2 + // 3.0 = P3 + private double _phase = 1.0; + private int _p1StatueCount = 0; + + public void Init(ScriptAccessory sa) + { + _phase = 1.0; + _p1StatueCount = 0; + + lock (_iceFireLock) + { + _iceIsReal = null; + _iceDiagonalIsLeftUpRightDown = null; + _actualStack = null; + _iceFireTargets.Clear(); + _waveCannonTargets.Clear(); + _action47784Targets.Clear(); + _action47784GuideSent = false; + _action47784Started = false; + } + + lock (_bigIceSealLock) + { + _bigIceSealSafeIsLeftUpRightDown = null; + _lastBigIceSealGuidePos = null; + _bigIceSealTetherTargets.Clear(); + _bigIceSealActive = false; + _bigIceSealGuideStarted = false; + _bigIceSealSecondGuideDone = false; + _bigIceSealTetherDetectionEnabled = false; + _bigIceSealTetherDetectionWindowStarted = false; + } + + ResetStatue3ArrowState(); + + sa.Method.RemoveDraw(".*"); + } + + #endregion + + #region 阶段控制 + + [ScriptMethod(name: "Set Phase 1", eventType: EventTypeEnum.Chat, eventCondition: ["Type:Echo", "Message:KASP1"], userControl: false)] + public void SetP1(Event evt, ScriptAccessory sa) + { + _phase = 1.1; + _p1StatueCount = 1; + ResetStatue3ArrowState(); + } + + [ScriptMethod(name: "Set Phase 2", eventType: EventTypeEnum.Chat, eventCondition: ["Type:Echo", "Message:KASP2"], userControl: false)] + public void SetP2(Event evt, ScriptAccessory sa) => _phase = 2.0; + + [ScriptMethod(name: "Set Phase 3", eventType: EventTypeEnum.Chat, eventCondition: ["Type:Echo", "Message:KASP3"], userControl: false)] + public void SetP3(Event evt, ScriptAccessory sa) => _phase = 3.0; + + [ScriptMethod(name: "Show Phase", eventType: EventTypeEnum.Chat, eventCondition: ["Type:Echo", "Message:phase"], userControl: false)] + public void ShowPhase(Event evt, ScriptAccessory sa) => sa.Debug($"当前 Phase={_phase}, P1StatueCount={_p1StatueCount}"); + + [ScriptMethod( + name: "P1_众神之像_阶段控制", + eventType: EventTypeEnum.StartCasting, + eventCondition: ["ActionId:48370"], + userControl: false)] + public void P1_众神之像_阶段控制(Event evt, ScriptAccessory sa) + { + if (_phase >= 2.0) + return; + + _p1StatueCount++; + + _phase = _p1StatueCount switch + { + 1 => 1.1, + 2 => 1.2, + 3 => 1.3, + _ => _phase, + }; + + if (IsPhase(_phase, 1.3)) + ResetStatue3ArrowState(); + + sa.Debug($"P1众神之像阶段控制:Count={_p1StatueCount}, Phase={_phase}"); + } + + [ScriptMethod( + name: "P1结束_切换到P2", + eventType: EventTypeEnum.StartCasting, + eventCondition: ["ActionId:47804"], + userControl: false)] + public void P1结束_切换到P2(Event evt, ScriptAccessory sa) + { + if (!IsPhase(_phase, 1.3)) + return; + + _phase = 2.0; + + sa.Debug($"P1结束检测到 47804,切换到 P2。Phase={_phase}"); + } + + #endregion + + #region P1 神像1(冰爆) + + #region 神像1 变量 + + private readonly object _iceFireLock = new(); + + // true = 真冰,false = 假冰 + private bool? _iceIsReal = null; + + // true = 左上右下,false = 左下右上 + private bool? _iceDiagonalIsLeftUpRightDown = null; + + // true = 实际分摊,false = 实际散开 + private bool? _actualStack = null; + + private readonly HashSet _iceFireTargets = new(); + private readonly HashSet _waveCannonTargets = new(); + + private readonly Dictionary _action47784Targets = new(); + private bool _action47784GuideSent = false; + private bool _action47784Started = false; + + private static readonly Vector3 UpperLeftPoint = new(93.90f, 0.00f, 93.94f); + private static readonly Vector3 UpperRightPoint = new(106.09f, 0.00f, 93.93f); + + private static readonly Vector3 UpperLeftAreaUpperPoint = new(94.67f, 0.00f, 88.49f); + private static readonly Vector3 UpperRightAreaUpperPoint = new(108.29f, 0.00f, 88.06f); + + private static readonly Vector3 LowerLeftPoint = new(93.90f, 0.00f, 106.06f); + private static readonly Vector3 LowerRightPoint = new(106.09f, 0.00f, 106.07f); + + private const uint RealIceAction = 47768; + private const uint StackIcon = 0x0080; + private const uint SpreadIcon = 0x007F; + + #endregion + + [ScriptMethod( + name: "P1_冰爆_记录冰真假与方向", + eventType: EventTypeEnum.StartCasting, + eventCondition: ["ActionId:regex:^(47768|47771)$"], + userControl: false)] + public void P1_冰爆_记录冰真假与方向(Event evt, ScriptAccessory sa) + { + if (!IsPhase(_phase, 1.1)) + return; + + var actionId = evt.ActionId(); + + bool iceIsReal = actionId == RealIceAction; + bool rawDiagonalIsLeftUpRightDown = IsLeftUpRightDownByRotation(evt.SourceRotation()); + + // 47768 真冰:实际覆盖方向 = SourceRotation 判断出来的方向 + // 47771 假冰:实际覆盖方向 = 反方向 + bool actualDiagonalIsLeftUpRightDown = iceIsReal + ? rawDiagonalIsLeftUpRightDown + : !rawDiagonalIsLeftUpRightDown; + + lock (_iceFireLock) + { + _iceIsReal = iceIsReal; + _iceDiagonalIsLeftUpRightDown = actualDiagonalIsLeftUpRightDown; + } + + sa.Debug($""" + Ice cast: + Phase={_phase} + ActionId={actionId} + SourceRotation={evt.SourceRotation()} + IceIsReal={iceIsReal} + RawDiagonal={(rawDiagonalIsLeftUpRightDown ? "左上右下" : "左下右上")} + ActualDiagonal={(actualDiagonalIsLeftUpRightDown ? "左上右下" : "左下右上")} + UpperCoveredPoint={(actualDiagonalIsLeftUpRightDown ? "左上点" : "右上点")} + """); + } + + [ScriptMethod( + name: "P1_冰爆_记录分摊散开", + eventType: EventTypeEnum.TargetIcon, + eventCondition: ["Id:regex:^(0080|007F)$"], + userControl: false)] + public void P1_冰爆_记录分摊散开(Event evt, ScriptAccessory sa) + { + if (!IsPhase(_phase, 1.1)) + return; + + uint iconId = evt.IconId(); + uint targetId = evt.TargetId(); + + if (targetId == 0) + return; + + lock (_iceFireLock) + { + _iceFireTargets.Add(targetId); + + // 2 个 0080:认为实际是分摊 + if (_iceFireTargets.Count == 2 && iconId == StackIcon) + _actualStack = true; + + // 8 个 007F:认为实际是散开 + if (_iceFireTargets.Count == 8 && iconId == SpreadIcon) + _actualStack = false; + } + + // sa.Debug($""" + // Fire icon: + // Phase={_phase} + // IconId=0x{iconId:X4} + // TargetId=0x{targetId:X} + // TargetCount={_iceFireTargets.Count} + // ActualStack={_actualStack} + // """); + } + + [ScriptMethod( + name: "P1_冰爆_记录002D点名", + eventType: EventTypeEnum.Tether, + eventCondition: ["Id:002D"], + userControl: false)] + public void P1_冰爆_记录002D点名(Event evt, ScriptAccessory sa) + { + uint targetId = evt.TargetId(); + uint sourceId = evt.SourceId(); + + uint tetherPlayerId = sa.Data.PartyList.Contains(targetId) + ? targetId + : sa.Data.PartyList.Contains(sourceId) + ? sourceId + : targetId; + + if (tetherPlayerId == 0) + return; + + // 神像1:只记录玄乎乎魔法需要的 002D。 + if (IsPhase(_phase, 1.1)) + { + lock (_iceFireLock) + { + _waveCannonTargets.Add(tetherPlayerId); + } + + sa.Debug($"神像1 002D记录:Player=0x{tetherPlayerId:X}, Source=0x{sourceId:X}, Target=0x{targetId:X}, IsMe={tetherPlayerId == sa.Data.Me}, Phase={_phase}"); + return; + } + + // 神像2:只在扩大大冰封结算后 5 秒检测窗打开时,才接受 002D 并触发第二段。 + if (IsPhase(_phase, 1.2)) + { + bool shouldSendBigIceSealSecondGuide = false; + bool bigIceSealDetectionEnabledSnapshot = false; + BigIceSealGuideMode modeSnapshot = BigIceSealMode; + double phaseSnapshot = _phase; + Vector3? firstGuidePosSnapshot = null; + + lock (_bigIceSealLock) + { + bigIceSealDetectionEnabledSnapshot = _bigIceSealTetherDetectionEnabled; + + if (_bigIceSealActive + && _bigIceSealTetherDetectionEnabled + && !_bigIceSealSecondGuideDone) + { + _bigIceSealTetherTargets.Add(tetherPlayerId); + + if (tetherPlayerId == sa.Data.Me) + { + modeSnapshot = BigIceSealMode; + phaseSnapshot = _phase; + firstGuidePosSnapshot = _lastBigIceSealGuidePos; + + _bigIceSealSecondGuideDone = true; + _bigIceSealActive = false; + _bigIceSealTetherDetectionEnabled = false; + shouldSendBigIceSealSecondGuide = true; + } + } + } + + sa.Debug($"神像2 002D:Player=0x{tetherPlayerId:X}, Source=0x{sourceId:X}, Target=0x{targetId:X}, IsMe={tetherPlayerId == sa.Data.Me}, DetectionEnabled={bigIceSealDetectionEnabledSnapshot}, Phase={_phase}"); + + if (shouldSendBigIceSealSecondGuide) + SendBigIceSeal002DSecondGuide(sa, modeSnapshot, phaseSnapshot, firstGuidePosSnapshot); + + return; + } + + // 神像3 002D 由 P1_神像3_002D指路 单独处理。 + sa.Debug($"002D忽略:当前 Phase={_phase}, Player=0x{tetherPlayerId:X}, Source=0x{sourceId:X}, Target=0x{targetId:X}"); + } + + [ScriptMethod( + name: "P1_冰爆_玄乎乎魔法指路", + eventType: EventTypeEnum.StartCasting, + eventCondition: ["ActionId:47764"])] + public void P1_冰爆_玄乎乎魔法指路(Event evt, ScriptAccessory sa) + { + if (!IsPhase(_phase, 1.1)) + return; + + Task.Run(async () => + { + await Task.Delay(100); + + bool? iceDiagonalIsLeftUpRightDown; + bool? iceIsReal; + bool? actualStack; + HashSet waveCannonTargetsSnapshot; + + lock (_iceFireLock) + { + iceDiagonalIsLeftUpRightDown = _iceDiagonalIsLeftUpRightDown; + iceIsReal = _iceIsReal; + actualStack = _actualStack; + waveCannonTargetsSnapshot = new HashSet(_waveCannonTargets); + } + + if (iceDiagonalIsLeftUpRightDown == null) + { + sa.Debug("玄乎乎魔法指路失败:没有记录到冰方向。"); + return; + } + + var party = sa.Data.PartyList; + int myIdx = sa.MyIndex(); + + if (myIdx < 0 || myIdx > 7) + { + sa.Debug($"玄乎乎魔法指路失败:自己的 index 异常。MyIndex={myIdx}"); + return; + } + + bool meHas002D = waveCannonTargetsSnapshot.Contains(sa.Data.Me); + + Vector3 coveredUpperPoint = iceDiagonalIsLeftUpRightDown.Value + ? UpperLeftPoint + : UpperRightPoint; + + Vector3 uncoveredUpperPoint = iceDiagonalIsLeftUpRightDown.Value + ? UpperRightPoint + : UpperLeftPoint; + + Vector3 GetFirstGuidePos(uint playerId) + { + bool has002D = waveCannonTargetsSnapshot.Contains(playerId); + int playerIdx = PartyIndex(party, playerId); + + if (XuanHuHuMode == XuanHuHuGuideMode.固定半场) + { + // 固定半场:0/1/2/3 固定左半场,4/5/6/7 固定右半场。 + bool isLeftHalf = playerIdx <= 3; + bool upperLeftHasIce = iceDiagonalIsLeftUpRightDown.Value; + return GetFixedHalfPos(isLeftHalf, has002D, upperLeftHasIce); + } + + // 盗火烬:002D 去上半场有冰点,其他人去上半场无冰点。 + return has002D + ? coveredUpperPoint + : uncoveredUpperPoint; + } + + bool IsRightHalfForSecondGuide(uint playerId) + { + int playerIdx = PartyIndex(party, playerId); + + if (XuanHuHuMode == XuanHuHuGuideMode.固定半场) + { + // 固定半场只有第一段半场判定不同;第二段仍按对应半场分左右点。 + return playerIdx >= 4; + } + + // 盗火烬按第一段实际去到的点决定左右半场。 + return GetFirstGuidePos(playerId).X > 100.0f; + } + + Vector3 myFirstGuidePos = GetFirstGuidePos(sa.Data.Me); + + var firstGuideDp = sa.WaypointDp( + myFirstGuidePos, + 3000, + 0, + $"P1_冰爆_玄乎乎魔法指路_第一段_{XuanHuHuMode}_{myIdx}" + ); + + sa.Method.SendDraw(DrawModeEnum.Imgui, DrawTypeEnum.Displacement, firstGuideDp); + + sa.Debug($""" + 玄乎乎魔法第一段指路: + Phase={_phase} + Mode={XuanHuHuMode} + IceIsReal={iceIsReal} + ActualStack={actualStack} + ActualDiagonal={(iceDiagonalIsLeftUpRightDown.Value ? "左上右下" : "左下右上")} + MyIndex={myIdx} + MeHas002D={meHas002D} + FirstSide={(IsRightHalfForSecondGuide(sa.Data.Me) ? "右半场" : "左半场")} + TargetPoint={myFirstGuidePos} + DrawName=P1_冰爆_玄乎乎魔法指路_第一段_{XuanHuHuMode}_{myIdx} + """); + + await Task.Delay(5000); + + var rightSidePoints = new List + { + new(102.25f, 0.00f, 100.00f), + new(106.81f, 0.00f, 100.00f), + new(111.45f, 0.00f, 100.00f), + new(116.25f, 0.00f, 100.00f), + }; + + var leftSidePoints = new List + { + new(97.75f, 0.00f, 100.00f), + new(93.19f, 0.00f, 100.00f), + new(88.55f, 0.00f, 100.00f), + new(83.75f, 0.00f, 100.00f), + }; + + var rightSideMembers = party + .Where(IsRightHalfForSecondGuide) + .OrderBy(id => PartyIndex(party, id)) + .ToList(); + + var leftSideMembers = party + .Where(id => !IsRightHalfForSecondGuide(id)) + .OrderBy(id => PartyIndex(party, id)) + .ToList(); + + Vector3? mySecondGuidePos = null; + + int myRightOrder = rightSideMembers.IndexOf(sa.Data.Me); + if (myRightOrder >= 0 && myRightOrder < rightSidePoints.Count) + mySecondGuidePos = rightSidePoints[myRightOrder]; + + int myLeftOrder = leftSideMembers.IndexOf(sa.Data.Me); + if (myLeftOrder >= 0 && myLeftOrder < leftSidePoints.Count) + mySecondGuidePos = leftSidePoints[myLeftOrder]; + + if (mySecondGuidePos == null) + { + sa.Debug("玄乎乎魔法第二段指路失败:没有找到自己的左右半场排序。"); + return; + } + + var secondGuideDp = sa.WaypointDp( + mySecondGuidePos.Value, + 5000, + 0, + $"P1_冰爆_玄乎乎魔法指路_第二段_{XuanHuHuMode}_{myIdx}" + ); + + sa.Method.SendDraw(DrawModeEnum.Imgui, DrawTypeEnum.Displacement, secondGuideDp); + + string side = myRightOrder >= 0 ? "右半场" : "左半场"; + int order = myRightOrder >= 0 ? myRightOrder : myLeftOrder; + + sa.Debug($""" + 玄乎乎魔法第二段指路: + Phase={_phase} + Mode={XuanHuHuMode} + Side={side} + Order={order} + MyIndex={myIdx} + TargetPoint={mySecondGuidePos.Value} + DrawName=P1_冰爆_玄乎乎魔法指路_第二段_{XuanHuHuMode}_{myIdx} + RightMembers={string.Join(",", rightSideMembers.Select(x => PartyIndex(party, x)))} + LeftMembers={string.Join(",", leftSideMembers.Select(x => PartyIndex(party, x)))} + """); + }); + } + + [ScriptMethod( + name: "P1_冰爆_47784换位指路", + eventType: EventTypeEnum.ActionEffect, + eventCondition: ["ActionId:47784"], + userControl: false)] + public void P1_冰爆_47784换位指路(Event evt, ScriptAccessory sa) + { + if (!IsPhase(_phase, 1.1)) + return; + + uint targetId = evt.TargetId(); + Vector3 effectPosition = evt.TargetPosition(); + + if (targetId == 0) + return; + + Dictionary? snapshot = null; + + lock (_iceFireLock) + { + // 47784 已经完成过一次指路,后面的重复 ActionEffect 直接丢掉 + if (_action47784GuideSent) + return; + + _action47784Started = true; + _action47784Targets[targetId] = effectPosition; + + sa.Debug($""" + 47784 ActionEffect: + Phase={_phase} + Started={_action47784Started} + GuideSent={_action47784GuideSent} + TargetId=0x{targetId:X} + TargetIndex={sa.Data.PartyList.IndexOf(targetId)} + TargetPosition={effectPosition} + Count={_action47784Targets.Count} + """); + + if (_action47784Targets.Count < 4) + return; + + _phase = 1.2; + _action47784GuideSent = true; + snapshot = new Dictionary(_action47784Targets); + } + + Task.Run(async () => + { + await Task.Delay(100); + + var party = sa.Data.PartyList; + int myIdx = sa.MyIndex(); + + if (myIdx < 0 || myIdx > 7) + { + sa.Debug($"47784换位指路失败:自己的 index 异常。MyIndex={myIdx}"); + return; + } + + var lowGroup = Enumerable.Range(0, 4) + .Where(i => i < party.Count) + .Select(i => party[i]) + .ToList(); + + var highGroup = Enumerable.Range(4, 4) + .Where(i => i < party.Count) + .Select(i => party[i]) + .ToList(); + + Vector3? myGuidePos = Get47784GuidePosForGroup(sa.Data.Me, lowGroup, party, snapshot); + myGuidePos ??= Get47784GuidePosForGroup(sa.Data.Me, highGroup, party, snapshot); + + if (myGuidePos == null) + { + sa.Debug($""" + 47784换位指路:自己不需要指路或没有匹配到。 + MyIndex={myIdx} + MeWasTargeted={snapshot.ContainsKey(sa.Data.Me)} + Targets={string.Join(",", snapshot.Keys.Select(x => party.IndexOf(x)).OrderBy(x => x))} + """); + return; + } + + var guideDp = sa.WaypointDp( + myGuidePos.Value, + 4500, + 0, + $"P1_冰爆_47784换位指路_{myIdx}" + ); + + sa.Method.SendDraw(DrawModeEnum.Imgui, DrawTypeEnum.Displacement, guideDp); + + sa.Debug($""" + 47784换位指路: + Phase={_phase} + MyIndex={myIdx} + TargetPoint={myGuidePos.Value} + DrawName=P1_冰爆_47784换位指路_{myIdx} + All47784Targets={string.Join(",", snapshot.Keys.Select(x => party.IndexOf(x)).OrderBy(x => x))} + LowGroupTargets={string.Join(",", snapshot.Keys.Where(x => party.IndexOf(x) >= 0 && party.IndexOf(x) <= 3).Select(x => party.IndexOf(x)).OrderBy(x => x))} + HighGroupTargets={string.Join(",", snapshot.Keys.Where(x => party.IndexOf(x) >= 4 && party.IndexOf(x) <= 7).Select(x => party.IndexOf(x)).OrderBy(x => x))} + """); + }); + } + + #endregion + + #region P1 神像2(扩大大冰封) + + #region 神像2 变量 + + private readonly object _bigIceSealLock = new(); + + private static readonly Vector3 ArenaCenter = new(100.00f, 0.00f, 100.00f); + + // true = 左上右下是安全区,false = 左下右上是安全区 + private bool? _bigIceSealSafeIsLeftUpRightDown = null; + private Vector3? _lastBigIceSealGuidePos = null; + private readonly HashSet _bigIceSealTetherTargets = new(); + private bool _bigIceSealActive = false; + private bool _bigIceSealGuideStarted = false; + private bool _bigIceSealSecondGuideDone = false; + + // 只有扩大大冰封 ActionEffect 结算后再等 5 秒,才开启神像2第二段 002D 检测。 + private bool _bigIceSealTetherDetectionEnabled = false; + private bool _bigIceSealTetherDetectionWindowStarted = false; + + #endregion + + [ScriptMethod( + name: "P1_神像2_记录47774安全对角", + eventType: EventTypeEnum.StartCasting, + eventCondition: ["ActionId:regex:^(47774|47768)$"], + userControl: false)] + public void P1_神像2_记录47774安全对角(Event evt, ScriptAccessory sa) + { + if (!IsPhase(_phase, 1.2)) + return; + + bool hitIsLeftUpRightDown = IsLeftUpRightDownByRotation(evt.SourceRotation()); + bool safeIsLeftUpRightDown = !hitIsLeftUpRightDown; + + lock (_bigIceSealLock) + { + _bigIceSealSafeIsLeftUpRightDown = safeIsLeftUpRightDown; + } + + sa.Debug($"神像2安全对角: Phase={_phase}, ActionId={evt.ActionId()}, Rot={evt.SourceRotation()}, Hit={(hitIsLeftUpRightDown ? "左上右下" : "左下右上")}, Safe={(safeIsLeftUpRightDown ? "左上右下" : "左下右上")}"); + } + + [ScriptMethod( + name: "P1_神像2指路", + eventType: EventTypeEnum.StartCasting, + eventCondition: ["ActionId:47765"])] + public void P1_第二次扩大大冰封指路(Event evt, ScriptAccessory sa) + { + if (!IsPhase(_phase, 1.2)) + return; + + _ = Task.Run(async () => + { + await Task.Delay(100); + + if (!IsPhase(_phase, 1.2)) + return; + + int myIdx = sa.MyIndex(); + + if (myIdx < 0 || myIdx > 7) + { + sa.Debug($"扩大大冰封指路失败:自己的 index 异常。MyIndex={myIdx}"); + return; + } + + bool safeIsLeftUpRightDown; + + lock (_bigIceSealLock) + { + if (_bigIceSealGuideStarted) + { + sa.Debug($"扩大大冰封指路跳过:本次 init 后已经触发过。Mode={BigIceSealMode}, MyIndex={myIdx}"); + return; + } + + if (_bigIceSealSafeIsLeftUpRightDown == null) + { + sa.Debug($"扩大大冰封指路失败:没有记录到 47774 安全对角。Mode={BigIceSealMode}, MyIndex={myIdx}"); + return; + } + + _bigIceSealGuideStarted = true; + safeIsLeftUpRightDown = _bigIceSealSafeIsLeftUpRightDown.Value; + } + + Vector3? myGuidePos = GetBigIceSealInitialPos( + BigIceSealMode, + myIdx, + safeIsLeftUpRightDown, + BigIceSealMtUpperHalfTank + ); + + if (myGuidePos == null) + { + sa.Debug($"扩大大冰封指路失败:没有匹配到点位。Mode={BigIceSealMode}, MyIndex={myIdx}, SafeDiagonal={(safeIsLeftUpRightDown ? "左上右下" : "左下右上")}"); + return; + } + + lock (_bigIceSealLock) + { + _lastBigIceSealGuidePos = myGuidePos.Value; + _bigIceSealTetherTargets.Clear(); + _bigIceSealActive = true; + _bigIceSealSecondGuideDone = false; + _bigIceSealTetherDetectionEnabled = false; + _bigIceSealTetherDetectionWindowStarted = false; + } + + bool useTankGroupSwapSetting = BigIceSealMode is + BigIceSealGuideMode.斜角黑泥 or + BigIceSealGuideMode._1A四人分摊_MT组上半场; + + string initialDrawName = useTankGroupSwapSetting + ? $"P1_第二次扩大大冰封指路_{BigIceSealMode}_{BigIceSealMtUpperHalfTank}_{myIdx}" + : $"P1_第二次扩大大冰封指路_{BigIceSealMode}_{myIdx}"; + + var guideDp = sa.WaypointDp( + myGuidePos.Value, + 5000, + 0, + initialDrawName + ); + + sa.Method.SendDraw(DrawModeEnum.Imgui, DrawTypeEnum.Displacement, guideDp); + + sa.Debug($""" + 第二次扩大大冰封指路: + Phase={_phase} + Mode={BigIceSealMode} + TankGroupSwapSetting={BigIceSealMtUpperHalfTank} + MyIndex={myIdx} + SafeDiagonal={(safeIsLeftUpRightDown ? "左上右下" : "左下右上")} + TargetPoint={myGuidePos.Value} + DrawName={initialDrawName} + """); + }); + } + + [ScriptMethod( + name: "P1_神像2指路第二段", + eventType: EventTypeEnum.ActionEffect, + eventCondition: ["ActionId:47765"], + userControl: false)] + public void P1_神像2_扩大大冰封结算后开启002D检测(Event evt, ScriptAccessory sa) + { + if (!IsPhase(_phase, 1.2)) + return; + + bool shouldStartWindowTask = false; + + lock (_bigIceSealLock) + { + if (_bigIceSealActive + && _bigIceSealGuideStarted + && !_bigIceSealSecondGuideDone + && !_bigIceSealTetherDetectionWindowStarted) + { + _bigIceSealTetherDetectionWindowStarted = true; + _bigIceSealTetherDetectionEnabled = false; + shouldStartWindowTask = true; + } + } + + if (!shouldStartWindowTask) + return; + + sa.Debug("神像2扩大大冰封已结算:5秒后开启002D检测。首个结算 ActionEffect=47765"); + + _ = Task.Run(async () => + { + await Task.Delay(5000); + + lock (_bigIceSealLock) + { + if (!IsPhase(_phase, 1.2) || !_bigIceSealActive || _bigIceSealSecondGuideDone) + return; + + _bigIceSealTetherDetectionEnabled = true; + } + + sa.Debug("神像2第二段002D检测已开启:现在收到自己的002D才会指路。"); + }); + } + + private void SendBigIceSeal002DSecondGuide( + ScriptAccessory sa, + BigIceSealGuideMode modeSnapshot, + double phaseSnapshot, + Vector3? firstGuidePosSnapshot) + { + int myIdx = sa.MyIndex(); + + if (myIdx < 0 || myIdx > 7) + { + sa.Debug($"扩大大冰封002D第二段失败:自己的 index 异常。MyIndex={myIdx}"); + return; + } + + Vector3? myGuidePos2 = null; + + if (modeSnapshot == BigIceSealGuideMode.Yan式八人全分摊) + { + myGuidePos2 = new Vector3(100.00f, 0.00f, 88.00f); + } + else if (firstGuidePosSnapshot != null) + { + myGuidePos2 = MoveTowards(firstGuidePosSnapshot.Value, ArenaCenter, 6.50f); + } + + if (myGuidePos2 == null) + { + sa.Debug($"扩大大冰封002D第二段失败:没有第一段安全点。Mode={modeSnapshot}, MyIndex={myIdx}"); + return; + } + + Vector3 finalGuidePos = myGuidePos2.Value; + string drawName = $"P1_第二次扩大大冰封指路_002D第二段_{modeSnapshot}_{myIdx}"; + + sa.Debug($"第二次扩大大冰封002D第二段已触发:4.5秒后显示指路。Mode={modeSnapshot}, MyIndex={myIdx}, TargetPoint={finalGuidePos}"); + + _ = Task.Run(async () => + { + await Task.Delay(4500); + + if (!IsPhase(_phase, 1.2)) + { + sa.Debug($"扩大大冰封002D第二段跳过:延迟后 Phase={_phase},已经不在神像2。"); + return; + } + + var guideDp2 = sa.WaypointDp( + finalGuidePos, + 5000, + 0, + drawName + ); + + sa.Method.SendDraw(DrawModeEnum.Imgui, DrawTypeEnum.Displacement, guideDp2); + + sa.Debug($""" + 第二次扩大大冰封002D第二段指路: + Trigger=Self002D_Delay + Phase={phaseSnapshot} + CurrentPhase={_phase} + Mode={modeSnapshot} + MyIndex={myIdx} + FirstPoint={firstGuidePosSnapshot} + TargetPoint={finalGuidePos} + DrawName={drawName} + """); + }); + } + + #endregion + + #region P1 神像3(箭头 + 002D) + + #region 神像3 变量 + + private readonly object _statue3ArrowLock = new(); + + private int _statue3ArrowCount = 0; + private int? _statue3SmallDir4 = null; + private int? _statue3LargeDir4 = null; + private bool? _statue3SmallIsShort = null; + private bool _statue3ArrowGuideSent = false; + private bool _statue3TetherGuideDone = false; + + private const uint Statue3SmallArrowMinStatus = 0x130C; + private const uint Statue3SmallArrowMaxStatus = 0x130F; + + private static readonly Vector3 Statue3SleepSourcePos = new(107.00f, 8.50f, 43.00f); + + private static readonly Vector3 Statue3CharmSourcePos = new(95f, 27f, 25f); + + #endregion + + [ScriptMethod( + name: "P1_神像3_箭头指路", + eventType: EventTypeEnum.StatusAdd, + eventCondition: ["StatusID:regex:^(4876|4877|4878|4879|5079|5080|5081|5082|130C|130D|130E|130F|13D7|13D8|13D9|13DA)$"])] + public void P1_神像3_箭头指路(Event evt, ScriptAccessory sa) + { + if (_phase >= 2.0) + return; + + // 神像3箭头 buff 是神像3专属;如果阶段控制 48370 漏了,用它兜底切到 1.3。 + if (!IsPhase(_phase, 1.3)) + { + _phase = 1.3; + _p1StatueCount = Math.Max(_p1StatueCount, 3); + sa.Debug($"神像3箭头触发,兜底修正 Phase=1.3。Old status event entered with Phase={_phase}"); + } + + uint targetId = evt.TargetId(); + if (targetId != 0 && targetId != sa.Data.Me) + return; + + uint statusId = evt.StatusId(); + sa.Debug($"神像3statusID: StatusID={statusId}"); + int? dir4 = GetStatue3ArrowDir4(statusId); + if (dir4 == null) + { + sa.Debug($"神像3箭头记录失败:未知 StatusID={statusId}"); + return; + } + + uint sourceId = evt.SourceId(); + Vector3 sourcePos = evt.SourcePosition(); + double duration = evt.Duration(); + + if (sourceId == 0 || sourcePos == Vector3.Zero || duration > 100) + { + sa.Debug($""" + 神像3箭头忽略旧/无效StatusAdd: + StatusID=0x{statusId:X} + SourceId=0x{sourceId:X} + SourcePos={sourcePos} + Duration={duration} + Phase={_phase} + """); + return; + } + + bool isSmallId = statusId >= Statue3SmallArrowMinStatus && statusId <= Statue3SmallArrowMaxStatus; + + bool shouldSendGuide = false; + bool sameBuff; + bool smallIsShort; + int smallDir4; + int largeDir4; + + lock (_statue3ArrowLock) + { + if (_statue3ArrowGuideSent) + return; + + _statue3ArrowCount++; + + if (isSmallId) + { + _statue3SmallDir4 = dir4.Value; + _statue3SmallIsShort = duration < 8.0; + } + else + { + _statue3LargeDir4 = dir4.Value; + } + + if (_statue3ArrowCount < 2) + { + sa.Debug($"神像3箭头记录:Count={_statue3ArrowCount}, StatusID=0x{statusId:X}, IsSmall={isSmallId}, Dir4={dir4.Value}, Duration={duration}, Phase={_phase}"); + return; + } + + + sameBuff = !_statue3SmallDir4.HasValue || !_statue3LargeDir4.HasValue; + smallIsShort = _statue3SmallIsShort ?? false; + smallDir4 = _statue3SmallDir4 ?? 0; + largeDir4 = _statue3LargeDir4 ?? 0; + + sa.Debug($"神像3箭头记录完毕:Count={_statue3ArrowCount}, StatusID=0x{statusId:X}, IsSmall={isSmallId}, Dir4={dir4.Value}, Duration={duration}, sameBuff={sameBuff}, smallIsShort={smallIsShort}, "); + + _statue3ArrowGuideSent = true; + shouldSendGuide = true; + } + + if (!shouldSendGuide) + return; + + var plan = GetStatue3ArrowPlan(Statue3ArrowMode, sameBuff, smallIsShort, smallDir4, largeDir4); + Vector3 firstPos = RotateStatue3RelativePoint(plan.Dir4, plan.FirstRelative); + Vector3 secondPos = RotateStatue3RelativePoint(plan.Dir4, plan.SecondRelative); + + var firstDp = sa.WaypointDp( + firstPos, + 7500, + 0, + $"P1_神像3_箭头指路_第一段_{Statue3ArrowMode}", + new Vector4(0, 1, 0, 1) + ); + + var secondDp = sa.WaypointDp( + secondPos, + 3000, + 7500, + $"P1_神像3_箭头指路_第二段_{Statue3ArrowMode}", + new Vector4(0, 1, 0, 1) + ); + + var firstToSecondDp = sa.WaypointDp( + secondPos, + 7500, + 0, + $"P1_神像3_箭头指路_第一段到第二段提前观察_{Statue3ArrowMode}", + new Vector4(1, 1, 1, 1) + ); + firstToSecondDp.Position = firstPos; + firstToSecondDp.Owner = 0; + + sa.Method.SendDraw(DrawModeEnum.Imgui, DrawTypeEnum.Displacement, firstDp); + sa.Method.SendDraw(DrawModeEnum.Imgui, DrawTypeEnum.Displacement, secondDp); + sa.Method.SendDraw(DrawModeEnum.Imgui, DrawTypeEnum.Displacement, firstToSecondDp); + + sa.Debug($""" + 神像3箭头指路: + Phase={_phase} + Mode={Statue3ArrowMode} + SameBuff={sameBuff} + SmallIsShort={smallIsShort} + SmallDir4={smallDir4} + LargeDir4={largeDir4} + FinalDir4={plan.Dir4} + FirstRelative={plan.FirstRelative} + SecondRelative={plan.SecondRelative} + FirstPoint={firstPos} + SecondPoint={secondPos} + Callout={plan.Callout} + """); + } + + [ScriptMethod( + name: "P1_神像3_魅惑睡眠指路", + eventType: EventTypeEnum.Tether, + eventCondition: ["Id:002D"])] + public void P1_神像3_魅惑睡眠指路(Event evt, ScriptAccessory sa) + { + if (!IsPhase(_phase, 1.3)) + return; + + uint sourceId = evt.SourceId(); // 众神之像 + uint targetId = evt.TargetId(); // 被连线玩家 + + if (targetId != sa.Data.Me) + return; + + int myIdx = sa.MyIndex(); + + if (myIdx < 0 || myIdx > 7) + { + sa.Debug($"神像3 002D指路失败:自己的 index 异常。MyIndex={myIdx}"); + return; + } + + Vector3 statueSourcePos = evt.SourcePosition(); + + Statue3TetherType tetherType = GetStatue3TetherType(statueSourcePos); + Vector3? wpos = GetStatue3TetherGuidePos(myIdx, tetherType, Statue3TetherMode); + + if (wpos == null) + { + sa.Debug($""" + 神像3 002D指路失败:没有匹配到点位或未识别睡眠/魅惑。 + Phase={_phase} + Mode={Statue3TetherMode} + TetherType={tetherType} + MyIndex={myIdx} + SourceId=0x{sourceId:X} + TargetId=0x{targetId:X} + StatueSourcePos={statueSourcePos} + """); + return; + } + + lock (_statue3ArrowLock) + { + if (_statue3TetherGuideDone) + return; + + _statue3TetherGuideDone = true; + } + + var dp = sa.WaypointDp( + wpos.Value, + 5000, + 5000, + $"P1_神像3_002D指路_{Statue3TetherMode}_{tetherType}_{myIdx}" + ); + + sa.Method.SendDraw(DrawModeEnum.Imgui, DrawTypeEnum.Displacement, dp); + + sa.Debug($""" + 神像3 002D指路: + Phase={_phase} + Mode={Statue3TetherMode} + TetherType={tetherType} + MyIndex={myIdx} + SourceId=0x{sourceId:X} + TargetId=0x{targetId:X} + StatueSourcePos={statueSourcePos} + TargetPoint={wpos.Value} + """); + } + + private void ResetStatue3ArrowState() + { + lock (_statue3ArrowLock) + { + _statue3ArrowCount = 0; + _statue3SmallDir4 = null; + _statue3LargeDir4 = null; + _statue3SmallIsShort = null; + _statue3ArrowGuideSent = false; + _statue3TetherGuideDone = false; + } + } + + #endregion + + #region 工具方法 + + private readonly struct Statue3ArrowPlan + { + public Statue3ArrowPlan(int dir4, Vector2 firstRelative, Vector2 secondRelative, string callout) + { + Dir4 = dir4; + FirstRelative = firstRelative; + SecondRelative = secondRelative; + Callout = callout; + } + + public int Dir4 { get; } + public Vector2 FirstRelative { get; } + public Vector2 SecondRelative { get; } + public string Callout { get; } + } + + private static int? GetStatue3ArrowDir4(uint statusId) + { + return statusId switch + { + 0x130C or 0x13D7 => 0, + 0x130D or 0x13D8 => 2, + 0x130E or 0x13D9 => 3, + 0x130F or 0x13DA => 1, + _ => null, + }; + } + + private static Statue3ArrowPlan GetStatue3ArrowPlan( + Statue3ArrowGuideMode mode, + bool sameBuff, + bool smallIsShort, + int smallDir4, + int largeDir4) + { + int dir4; + Vector2 first; + Vector2 second; + string callout; + + switch (mode) + { + case Statue3ArrowGuideMode._1A: + dir4 = sameBuff ? (Math.Max(smallDir4, largeDir4) + 3) % 4 : smallDir4; + first = sameBuff + ? new Vector2(0, -14) + : smallIsShort ? new Vector2(6, -9) : new Vector2(0, -8); + second = sameBuff + ? new Vector2(6, -14) + : smallIsShort ? new Vector2(0, -8) : new Vector2(6, -9); + callout = sameBuff + ? $"{CardinalDirText(dir4)}外侧放两次" + : $"{CardinalDirText(dir4)}内侧,{(smallIsShort ? "先斜后正" : "先正后斜")}"; + break; + + case Statue3ArrowGuideMode._1A改: + dir4 = sameBuff ? (Math.Max(smallDir4, largeDir4) + 2) % 4 : smallDir4; + first = sameBuff + ? new Vector2(6, -9) + : smallIsShort ? new Vector2(0, -8) : new Vector2(0, -14); + second = sameBuff + ? new Vector2(6, -14) + : smallIsShort ? new Vector2(0, -14) : new Vector2(0, -8); + callout = sameBuff + ? $"{CardinalDirText(dir4)}斜方向,放两次" + : $"{CardinalDirText(dir4)}正方向,{(smallIsShort ? "先近后远" : "先远后近")}"; + break; + + case Statue3ArrowGuideMode.方顺: + dir4 = sameBuff ? (Math.Max(smallDir4, largeDir4) + 1) % 4 : (largeDir4 + 2) % 4; + first = sameBuff + ? new Vector2(0, -12) + : smallIsShort ? new Vector2(6, -12) : new Vector2(12, -12); + second = sameBuff + ? new Vector2(-6, -12) + : smallIsShort ? new Vector2(12, -12) : new Vector2(6, -12); + callout = sameBuff + ? $"{CardinalDirText(dir4)}正点放两次" + : $"{DiagonalClockwiseDirText(dir4)},{(smallIsShort ? "先角后边" : "先边后角")}"; + break; + + case Statue3ArrowGuideMode.方逆: + default: + dir4 = sameBuff ? (Math.Max(smallDir4, largeDir4) + 3) % 4 : (smallDir4 + 2) % 4; + first = sameBuff + ? new Vector2(0, -12) + : smallIsShort ? new Vector2(-12, -12) : new Vector2(-6, -12); + second = sameBuff + ? new Vector2(6, -12) + : smallIsShort ? new Vector2(-6, -12) : new Vector2(-12, -12); + callout = sameBuff + ? $"{CardinalDirText(dir4)}正点放两次" + : $"{DiagonalCounterClockwiseDirText(dir4)},{(smallIsShort ? "先角后边" : "先边后角")}"; + break; + } + + return new Statue3ArrowPlan(dir4, first, second, callout); + } + + private static Vector3 RotateStatue3RelativePoint(int dir4, Vector2 relative) + { + float x = relative.X; + float z = relative.Y; + + (float rotatedX, float rotatedZ) = ((dir4 % 4 + 4) % 4) switch + { + 0 => (x, z), + 1 => (z, -x), + 2 => (-x, -z), + 3 => (-z, x), + _ => (x, z), + }; + + return new Vector3(100.00f + rotatedX, 0.00f, 100.00f + rotatedZ); + } + + private static string CardinalDirText(int dir4) + { + return ((dir4 % 4 + 4) % 4) switch + { + 0 => "上北", + 1 => "左西", + 2 => "下南", + 3 => "右东", + _ => "未知", + }; + } + + private static string DiagonalCounterClockwiseDirText(int dir4) + { + return ((dir4 % 4 + 4) % 4) switch + { + 0 => "左上", + 1 => "左下", + 2 => "右下", + 3 => "右上", + _ => "未知", + }; + } + + private static string DiagonalClockwiseDirText(int dir4) + { + return ((dir4 % 4 + 4) % 4) switch + { + 0 => "右上", + 1 => "左上", + 2 => "左下", + 3 => "右下", + _ => "未知", + }; + } + + private static Statue3TetherType GetStatue3TetherType(Vector3 statueSourcePos) + { + const float deviation = 1.0f; + + if (Vector3.Distance(statueSourcePos, Statue3SleepSourcePos) < deviation) + return Statue3TetherType.睡眠; + + if (Vector3.Distance(statueSourcePos, Statue3CharmSourcePos) < deviation) + return Statue3TetherType.魅惑; + + return Statue3TetherType.Unknown; + } + + private static Vector3? GetStatue3TetherGuidePos( + int myIdx, + Statue3TetherType tetherType, + Statue3TetherGuideMode mode) + { + if (myIdx < 0 || myIdx > 7) + return null; + + if (mode == Statue3TetherGuideMode.全固定) + { + return myIdx switch + { + 0 => new Vector3(100f, 0.00f, 91f), + 1 => new Vector3(109f, 0.00f, 100f), + 2 => new Vector3(85f, 0.00f, 100f), + 3 => new Vector3(100f, 0.00f, 115f), + 4 => new Vector3(91f, 0.00f, 100f), + 5 => new Vector3(100f, 0.00f, 109f), + 6 => new Vector3(100f, 0.00f, 85f), + 7 => new Vector3(115f, 0.00f, 100f), + _ => null, + }; + } + + return tetherType switch + { + // 魅惑在外:睡眠走内侧,魅惑走外侧。 + // 魅惑在内:睡眠/魅惑点位反过来。 + Statue3TetherType.睡眠 => mode == Statue3TetherGuideMode.魅惑在内 + ? GetStatue3TetherOuterPos(myIdx) + : GetStatue3TetherInnerPos(myIdx), + + Statue3TetherType.魅惑 => mode == Statue3TetherGuideMode.魅惑在内 + ? GetStatue3TetherInnerPos(myIdx) + : GetStatue3TetherOuterPos(myIdx), + + _ => null, + }; + } + + private static Vector3? GetStatue3TetherInnerPos(int myIdx) + { + return myIdx switch + { + 0 or 6 => new Vector3(100f, 0.00f, 91f), + 1 or 7 => new Vector3(109f, 0.00f, 100f), + 2 or 4 => new Vector3(91f, 0.00f, 100f), + 3 or 5 => new Vector3(100f, 0.00f, 109f), + _ => null, + }; + } + + private static Vector3? GetStatue3TetherOuterPos(int myIdx) + { + return myIdx switch + { + 0 or 6 => new Vector3(100f, 0.00f, 85f), + 1 or 7 => new Vector3(115f, 0.00f, 100f), + 2 or 4 => new Vector3(85f, 0.00f, 100f), + 3 or 5 => new Vector3(100f, 0.00f, 115f), + _ => null, + }; + } + + private static Vector3? GetBigIceSealInitialPos( + BigIceSealGuideMode mode, + int index, + bool safeIsLeftUpRightDown, + BigIceSealMtUpperHalfTankMode mtUpperHalfTankMode) + { + return mode switch + { + BigIceSealGuideMode.斜角黑泥 => GetBigIceSealDiagonalBlackMudPos(index, safeIsLeftUpRightDown, mtUpperHalfTankMode), + BigIceSealGuideMode._1A四人分摊_TH上半场 => GetBigIceSeal1AThUpperHalfPos(index, safeIsLeftUpRightDown), + BigIceSealGuideMode._1A四人分摊_MT组上半场 => GetBigIceSeal1AMtUpperHalfPos(index, safeIsLeftUpRightDown, mtUpperHalfTankMode), + BigIceSealGuideMode._1A四人分摊_T远北奶近南 => GetBigIceSeal1ATFarNorthHealerNearSouthPos(index, safeIsLeftUpRightDown), + BigIceSealGuideMode.Yan式八人全分摊 => GetBigIceSealYanEightStackPos(safeIsLeftUpRightDown), + _ => null, + }; + } + + private static Vector3? GetBigIceSealDiagonalBlackMudPos( + int index, + bool safeIsLeftUpRightDown, + BigIceSealMtUpperHalfTankMode mtUpperHalfTankMode) + { + int groupIndex = GetBigIceSealTankGroupIndex(index, mtUpperHalfTankMode); + + return groupIndex switch + { + 2 or 4 => safeIsLeftUpRightDown + ? new Vector3(84.39f, 0.00f, 90.18f) + : new Vector3(84.30f, 0.00f, 109.84f), + + 0 or 6 => safeIsLeftUpRightDown + ? new Vector3(90.25f, 0.00f, 83.63f) + : new Vector3(109.65f, 0.00f, 84.57f), + + 3 or 5 => safeIsLeftUpRightDown + ? new Vector3(110.09f, 0.00f, 115.08f) + : new Vector3(89.38f, 0.00f, 115.14f), + + 1 or 7 => safeIsLeftUpRightDown + ? new Vector3(115.83f, 0.00f, 109.33f) + : new Vector3(115.92f, 0.00f, 90.07f), + + _ => null, + }; + } + + private static Vector3? GetBigIceSeal1AThUpperHalfPos(int index, bool safeIsLeftUpRightDown) + { + return index switch + { + 0 or 1 or 2 or 3 => safeIsLeftUpRightDown + ? new Vector3(99.50f, 0.00f, 81.00f) + : new Vector3(100.50f, 0.00f, 81.00f), + + 4 or 5 or 6 or 7 => safeIsLeftUpRightDown + ? new Vector3(100.00f, 0.00f, 119.50f) + : new Vector3(100.00f, 0.00f, 118.50f), + + _ => null, + }; + } + + private static Vector3? GetBigIceSeal1ATFarNorthHealerNearSouthPos(int index, bool safeIsLeftUpRightDown) + { + // T远北奶近南:固定 0/1/6/7 去北,2/3/4/5 去南。 + return index switch + { + 0 or 1 or 6 or 7 => safeIsLeftUpRightDown + ? new Vector3(99.50f, 0.00f, 81.00f) + : new Vector3(100.50f, 0.00f, 81.00f), + + 2 or 3 or 4 or 5 => safeIsLeftUpRightDown + ? new Vector3(100.50f, 0.00f, 119f) + : new Vector3(100.00f, 0.00f, 118.50f), + + _ => null, + }; + } + + private static Vector3? GetBigIceSeal1AMtUpperHalfPos( + int index, + bool safeIsLeftUpRightDown, + BigIceSealMtUpperHalfTankMode mtUpperHalfTankMode) + { + int groupIndex = GetBigIceSealTankGroupIndex(index, mtUpperHalfTankMode); + + return groupIndex switch + { + 0 or 2 or 4 or 6 => safeIsLeftUpRightDown + ? new Vector3(99.50f, 0.00f, 81.00f) + : new Vector3(100.50f, 0.00f, 81.00f), + + 1 or 3 or 5 or 7 => safeIsLeftUpRightDown + ? new Vector3(100.50f, 0.00f, 119.00f) + : new Vector3(99.50f, 0.00f, 119.00f), + + _ => null, + }; + } + + private static int GetBigIceSealTankGroupIndex(int index, BigIceSealMtUpperHalfTankMode mtUpperHalfTankMode) + { + if (mtUpperHalfTankMode == BigIceSealMtUpperHalfTankMode.交换分组_ST上半场) + { + return index switch + { + 0 => 1, + 1 => 0, + _ => index, + }; + } + + return index; + } + + private static Vector3? GetBigIceSealYanEightStackPos(bool safeIsLeftUpRightDown) + { + return safeIsLeftUpRightDown + ? new Vector3(100.50f, 0.00f, 112.00f) + : new Vector3(99.50f, 0.00f, 112.00f); + } + + private static Vector3 MoveTowards(Vector3 from, Vector3 to, float distance) + { + Vector3 dir = to - from; + dir.Y = 0; + + if (dir.LengthSquared() <= 0.0001f) + return from; + + dir = Vector3.Normalize(dir); + return from + dir * distance; + } + + private static Vector3? Get47784GuidePosForGroup( + uint me, + List groupMembers, + IReadOnlyList party, + Dictionary targetPositions) + { + if (!groupMembers.Contains(me)) + return null; + + if (targetPositions.ContainsKey(me)) + return null; + + var targetedMembers = groupMembers + .Where(id => targetPositions.ContainsKey(id)) + .OrderBy(id => PartyIndex(party, id)) + .ToList(); + + var untargetedMembers = groupMembers + .Where(id => !targetPositions.ContainsKey(id)) + .OrderBy(id => PartyIndex(party, id)) + .ToList(); + + if (targetedMembers.Count != 2 || untargetedMembers.Count != 2) + return null; + + int myOrder = untargetedMembers.IndexOf(me); + + if (myOrder < 0 || myOrder >= targetedMembers.Count) + return null; + + uint targetToGo = targetedMembers[myOrder]; + + return targetPositions[targetToGo]; + } + + private static Vector3 GetFixedHalfPos(bool isLeftHalf, bool has002D, bool upperLeftHasIce) + { + if (isLeftHalf) + { + // 0/1/2/3:固定左半场 + if (has002D) + { + return upperLeftHasIce + ? UpperLeftPoint + : UpperLeftAreaUpperPoint; + } + + // 没002D + return upperLeftHasIce + ? LowerLeftPoint + : UpperLeftPoint; + } + + // 4/5/6/7:固定右半场 + if (has002D) + { + return upperLeftHasIce + ? UpperRightAreaUpperPoint + : UpperRightPoint; + } + + // 没002D + return upperLeftHasIce + ? UpperRightPoint + : LowerRightPoint; + } + + private static int PartyIndex(IReadOnlyList party, uint playerId) + { + for (int i = 0; i < party.Count; i++) + { + if (party[i] == playerId) + return i; + } + + return -1; + } + + private static bool IsPhase(double current, double target) + { + return Math.Abs(current - target) < 0.0001; + } + + private static bool IsLeftUpRightDownByRotation(float rotation) + { + double degree = rotation * 180.0 / Math.PI; + degree = (degree % 360.0 + 360.0) % 360.0; + + double distanceTo45Or225 = Math.Min( + AngleDistance(degree, 45.0), + AngleDistance(degree, 225.0) + ); + + double distanceTo135Or315 = Math.Min( + AngleDistance(degree, 135.0), + AngleDistance(degree, 315.0) + ); + + return distanceTo45Or225 <= distanceTo135Or315; + } + + private static double AngleDistance(double a, double b) + { + double diff = Math.Abs(a - b) % 360.0; + return diff > 180.0 ? 360.0 - diff : diff; + } + + #endregion +} + +#region Helpers + +public static class EventExtensions +{ + public static float SourceRotation(this Event evt) + => JsonConvert.DeserializeObject(evt["SourceRotation"]); + + private static bool ParseHexId(string? idStr, out uint id) + { + id = 0; + + if (string.IsNullOrEmpty(idStr)) + return false; + + try + { + var idStr2 = idStr.Replace("0x", ""); + id = uint.Parse(idStr2, System.Globalization.NumberStyles.HexNumber); + return true; + } + catch + { + return false; + } + } + + private static bool TryParseUIntAny(string? raw, out uint id) + { + id = 0; + + if (string.IsNullOrWhiteSpace(raw)) + return false; + + raw = raw.Trim(); + + if (raw.StartsWith("0x", StringComparison.OrdinalIgnoreCase)) + { + return uint.TryParse( + raw[2..], + System.Globalization.NumberStyles.HexNumber, + System.Globalization.CultureInfo.InvariantCulture, + out id + ); + } + + if (raw.Any(c => c is >= 'A' and <= 'F' or >= 'a' and <= 'f')) + { + return uint.TryParse( + raw, + System.Globalization.NumberStyles.HexNumber, + System.Globalization.CultureInfo.InvariantCulture, + out id + ); + } + + return uint.TryParse( + raw, + System.Globalization.NumberStyles.Integer, + System.Globalization.CultureInfo.InvariantCulture, + out id + ); + } + + public static uint ActionId(this Event evt) + => JsonConvert.DeserializeObject(evt["ActionId"]); + + public static uint DataId(this Event evt) + => JsonConvert.DeserializeObject(evt["DataId"]); + + public static uint StatusId(this Event evt) + { + if (TryParseUIntAny(evt["StatusID"], out var id)) + return id; + + if (TryParseUIntAny(evt["StatusId"], out id)) + return id; + + if (TryParseUIntAny(evt["ActionId"], out id)) + return id; + + return 0; + } + + public static double Duration(this Event evt) + { + return double.TryParse( + evt["Duration"], + System.Globalization.NumberStyles.Float, + System.Globalization.CultureInfo.InvariantCulture, + out var duration + ) + ? duration + : 0.0; + } + + public static uint SourceId(this Event evt) + => ParseHexId(evt["SourceId"], out var id) ? id : 0; + + public static uint TargetId(this Event evt) + => ParseHexId(evt["TargetId"], out var id) ? id : 0; + + public static uint IconId(this Event evt) + { + var raw = evt["Id"]; + + if (string.IsNullOrWhiteSpace(raw)) + return 0; + + raw = raw.Trim(); + + if (raw.StartsWith("0x", StringComparison.OrdinalIgnoreCase)) + raw = raw[2..]; + + return uint.TryParse( + raw, + System.Globalization.NumberStyles.HexNumber, + null, + out var result + ) + ? result + : 0; + } + + public static Vector3 SourcePosition(this Event evt) + => ParseVector3(evt["SourcePosition"]); + + public static Vector3 EffectPosition(this Event evt) + => ParseVector3(evt["EffectPosition"]); + + public static Vector3 TargetPosition(this Event evt) + => ParseVector3(evt["TargetPosition"]); + + public static uint DirectorId(this Event evt) + => ParseHexId(evt["DirectorId"], out var id) ? id : 0; + + private static Vector3 ParseVector3(string? raw) + { + if (string.IsNullOrWhiteSpace(raw)) + return Vector3.Zero; + + raw = raw.Trim(); + + try + { + return JsonConvert.DeserializeObject(raw); + } + catch + { + } + + try + { + float x = ExtractFloat(raw, "X"); + float y = ExtractFloat(raw, "Y"); + float z = ExtractFloat(raw, "Z"); + return new Vector3(x, y, z); + } + catch + { + return Vector3.Zero; + } + } + + private static float ExtractFloat(string raw, string key) + { + var pattern = $@"{key}\s*[:=]\s*(-?\d+(?:[\.,]\d+)?)"; + var match = System.Text.RegularExpressions.Regex.Match( + raw, + pattern, + System.Text.RegularExpressions.RegexOptions.IgnoreCase + ); + + if (!match.Success) + return 0f; + + var value = match.Groups[1].Value.Replace(',', '.'); + + return float.Parse( + value, + System.Globalization.CultureInfo.InvariantCulture + ); + } +} + +public static class ScriptAccessoryExtensions +{ + public static int MyIndex(this ScriptAccessory sa) => sa.Data.PartyList.IndexOf(sa.Data.Me); + + public static DrawPropertiesEdit FastDp(this ScriptAccessory sa, string name, Vector3 pos, uint duration, Vector2 scale, bool safe = false) + { + var dp = sa.Data.GetDefaultDrawProperties(); + dp.Name = name; + dp.Color = safe ? sa.Data.DefaultSafeColor : sa.Data.DefaultDangerColor; + dp.Position = pos; + dp.DestoryAt = duration; + dp.Scale = scale; + return dp; + } + + public static DrawPropertiesEdit WaypointDp( + this ScriptAccessory sa, + Vector3 target, + uint duration, + uint delay = 0, + string name = "Waypoint", + Vector4? color = null) + { + var dp = sa.Data.GetDefaultDrawProperties(); + dp.Name = name; + dp.Color = color ?? sa.Data.DefaultSafeColor; + dp.Owner = sa.Data.Me; + dp.TargetPosition = target; + dp.DestoryAt = duration; + dp.Delay = delay; + dp.Scale = new Vector2(2); + dp.ScaleMode = ScaleMode.YByDistance; + return dp; + } + + public static DrawPropertiesEdit WaypointFromToDp( + this ScriptAccessory sa, + Vector3 from, + Vector3 to, + uint duration, + uint delay = 0, + string name = "WaypointFromTo", + Vector4? color = null) + { + var dp = sa.Data.GetDefaultDrawProperties(); + dp.Name = name; + dp.Color = color ?? sa.Data.DefaultSafeColor; + dp.Position = from; + dp.Owner = 0; + dp.TargetPosition = to; + dp.DestoryAt = duration; + dp.Delay = delay; + dp.Scale = new Vector2(2); + dp.ScaleMode = ScaleMode.YByDistance; + return dp; + } + + public static void Debug(this ScriptAccessory sa, object message) + { + if (!Kefka.DebugEnabled) + return; + + sa.Method.SendChat($"/e [Kefka] {message}"); + } +} + +#endregion diff --git a/Codaaaaaa/M10S.cs b/Codaaaaaa/M10S.cs new file mode 100644 index 0000000..96aec4b --- /dev/null +++ b/Codaaaaaa/M10S.cs @@ -0,0 +1,2503 @@ +using System; +using System.Linq; +using System.Numerics; +using System.Collections.Generic; +using System.Threading.Tasks; +using Newtonsoft.Json; +using KodakkuAssist.Script; +using KodakkuAssist.Module.GameEvent; +using KodakkuAssist.Module.Draw; +using KodakkuAssist.Module.Draw.Manager; +using KodakkuAssist.Data; +using KodakkuAssist.Extensions; + +using System.Diagnostics; +using System.Runtime.Intrinsics.Arm; + +namespace Codaaaaaa.M9S; + +[ScriptType( + guid: "2c8b7d4a-6e91-4f3c-a5d2-9b7e1f6c8a03", + name: "阿卡狄亚零式登天斗技场M10S 指路", + territorys: [1323], + version: "0.0.1.0", + author: "Codaaaaaa", + note: "大部分的机制都做了指路,使用之前请务必调整可达鸭内位置和选择打法。由于版本初拿不到arr用来测试,有较大概率会被电...如果电了可以在频道反馈。感谢灵视佬提供的arr\n目前支持\nP2 第一轮打法\n * 水波\n * 镜像水波\n\nP2 第二三轮打法\n * 近战优化\n * 美野\n\n进水牢方式\n * 坦克\n * 近战\n * 治疗\n\n水牢打法\n * 无脑\n * MMW\n")] +public class M10S +{ + #region 用户设置 + // [UserSetting("是否开启TTS")] public static bool TTSOpen { get; set; } = true; + + // 水波或者镜像水波 + [ UserSetting("P2第一轮打法")] public P2第一轮打法 p2第一轮打法 { get; set; } = P2第一轮打法.水波; + // 近战优化或者美式野 + [ UserSetting("P2第二三轮打法")] public P2第二三轮打法 p2第二三轮打法 { get; set; } = P2第二三轮打法.近战优化; + // 进水牢方式 + [ UserSetting("进水牢方式")] public 进水牢 进水牢方式 { get; set; } = 进水牢.近战进水牢; + [ UserSetting("水牢打法")] public 水牢打法 水牢打法选择 { get; set; } = 水牢打法.无脑; + [ UserSetting("P5换边指路")] public bool p5换边指路 { get; set; } = true; + #endregion + public enum P2第一轮打法 + { + 水波, + 镜像水波 + } + + public enum P2第二三轮打法 + { + 近战优化, + 美式野 + } + public enum 进水牢 + { + 坦克进水牢, + 近战进水牢, + 治疗进水牢 + } + + public enum 水牢打法 + { + 无脑, + MMW + } + + public enum P5火水蛇 + { + 无, + 火, + 水 + } + private static readonly Vector3 Center = new(100f, 0f, 100f); + + private uint _phase = 1; + private uint 炽红 = 19287u; + private uint 深蓝 = 19288u; + + // ========================= + // 空中旋火:射线记录 & 点位动态下移 + // ========================= + private bool _airSpinRayActive = false; + private Vector3 _airSpinRayBossPos; + private Vector3 _airSpinRayTargetPos; + private Vector2 _airSpinRayDirXZ; // (dx, dz) + private readonly Dictionary _airSpinP1Override = new(); + private bool _bossBasisReady = false; + private Vector3 _bossBasisPos; // 火冲开始时boss位置 + private Vector2 _bossFwdXZ; // 火冲开始时boss forward(朝场内) + private Vector2 _bossRightXZ; // 火冲开始时boss right + + private static readonly Dictionary AirSpinRowZ103_57 = new() + { + { 4, new Vector3(118.01f, 0f, 103.57f) }, + { 5, new Vector3(111.8f, 0f, 103.57f) }, + { 1, new Vector3(105.59f, 0f, 103.57f) }, + { 3, new Vector3(99.38f, 0f, 103.57f) }, + { 7, new Vector3(93.17f, 0f, 103.57f) }, + }; + + // ========================= + // 分摊分散 + private int _分摊分散 = 0; + private Vector3 _浪花位置 = Vector3.Zero; + public void Init(ScriptAccessory sa) + { + ResetAll(); + sa.Method.RemoveDraw(".*"); + } + + private void ResetAll() + { + _phase = 1; + _airSpinRayActive = false; + _airSpinP1Override.Clear(); + _分摊分散 = 0; + _P2左右 = 1; + _P2火圈次数 = 0; + _P3水牢次数 = 0; + _P3水牢深蓝初始位置 = 0; + _浪花位置 = Vector3.Zero; + _P5火水蛇 = P5火水蛇.无; + _P5狂浪次数 = 0; + } + + // P2左右 + private int _P2左右 = 1; // 1=左 2=右 + private int _P2火圈次数 = 0; + + // P3 + private int _P3水牢次数 = 0; + private int _P3水牢深蓝初始位置 = 0; // 1 或者 4 + + // P5 + private P5火水蛇 _P5火水蛇 = P5火水蛇.无; + private int _P5狂浪次数 = 0; // 0 奶 1 近 2 远 + + // Debug + [ScriptMethod(name: "clear draw", eventType: EventTypeEnum.Chat, eventCondition: ["Type:Echo", "Message:KASCLEAR"], userControl: false)] + public void ClearDraw(Event evt, ScriptAccessory sa) => sa.Method.RemoveDraw(".*"); + + [ScriptMethod(name: "Set Phase 1", eventType: EventTypeEnum.Chat, eventCondition: ["Type:Echo", "Message:KASP1"], userControl: false)] + public void SetP1(Event evt, ScriptAccessory sa) => _phase = 1; + + [ScriptMethod(name: "Set Phase 2", eventType: EventTypeEnum.Chat, eventCondition: ["Type:Echo", "Message:KASP2"], userControl: false)] + public void SetP2(Event evt, ScriptAccessory sa) => _phase = 2; + + [ScriptMethod(name: "Set Phase 3", eventType: EventTypeEnum.Chat, eventCondition: ["Type:Echo", "Message:KASP3"], userControl: false)] + public void SetP3(Event evt, ScriptAccessory sa) => _phase = 3; + + [ScriptMethod(name: "Set Phase 4", eventType: EventTypeEnum.Chat, eventCondition: ["Type:Echo", "Message:KASP4"], userControl: false)] + public void SetP4(Event evt, ScriptAccessory sa) => _phase = 4; + + [ScriptMethod(name: "Set Phase 5", eventType: EventTypeEnum.Chat, eventCondition: ["Type:Echo", "Message:KASP5"], userControl: false)] + public void SetP5(Event evt, ScriptAccessory sa) => _phase = 5; + + [ScriptMethod(name: "Show Phase", eventType: EventTypeEnum.Chat, eventCondition: ["Type:Echo", "Message:phase"], userControl: false)] + public void ShowPhase(Event evt, ScriptAccessory sa) => sa.Method.SendChat($"/e Current Phase: {_phase}"); + + [ScriptMethod(name: "Show Debug", eventType: EventTypeEnum.Chat, eventCondition: ["Type:Echo", "Message:debug"], userControl: false)] + public void ShowDebug(Event evt, ScriptAccessory sa) => sa.Method.SendChat($"/e Current Param: {_P2火圈次数}, 左右: {_P2左右}"); + + [ScriptMethod( + name: "分摊死刑", + eventType: EventTypeEnum.StartCasting, + eventCondition: ["ActionId:46518"])] + public void 分摊死刑(Event evt, ScriptAccessory sa) + { + var myIdx = sa.MyIndex(); + var color = myIdx switch + { + 0 => sa.Data.DefaultSafeColor, + 1 => sa.Data.DefaultSafeColor, + _ => sa.Data.DefaultDangerColor + }; + var targetId = evt.TargetId(); + + var dp = sa.Data.GetDefaultDrawProperties(); + dp.Name = $"死刑圈-{targetId:X}"; + dp.Owner = targetId; + dp.Color = color; + dp.DestoryAt = 5500; + dp.ScaleMode = ScaleMode.ByTime; + dp.Scale = new Vector2(6); + + sa.Method.SendDraw(DrawModeEnum.Default, DrawTypeEnum.Circle, dp); + } + + // 开场火冲 + [ScriptMethod( + name: "开场火冲麻将", + eventType: EventTypeEnum.StatusAdd, + eventCondition: ["StatusID:regex:^(3004|3005|3006|3451)$"] + )] + public void 开场火冲麻将(Event evt, ScriptAccessory sa) + { + var targetId = evt.TargetId(); + var myId = sa.Data.Me; + + if (!RayHelpers.TryGetBossBasis(sa, 炽红, Center, out var bossPos, out var fwd, out var right)) + { + sa.Method.SendChat("/e 找不到Boss,无法缓存火冲基底"); + return; + } + + _bossBasisReady = true; + _bossBasisPos = new Vector3(bossPos.X, 0f, bossPos.Z); + _bossFwdXZ = fwd; + _bossRightXZ = right; + + // sa.Method.SendChat($"/e 缓存Boss基底 pos=({_bossBasisPos.X:F1},{_bossBasisPos.Z:F1}) fwd=({_bossFwdXZ.X:F2},{_bossFwdXZ.Y:F2}) right=({_bossRightXZ.X:F2},{_bossRightXZ.Y:F2})"); + // 只画自己身上的 + if (targetId == 0 || myId == 0 || targetId != myId) + return; + + // 取 boss 作为锚点 + 计算朝向 + // 后面全部用缓存 + var bossPos0 = _bossBasisPos; + var fwd0 = _bossFwdXZ; + var right0 = _bossRightXZ; + + var wpos = evt["StatusID"] switch + { + "3004" => RayHelpers.BossRelativePoint(bossPos0, fwd0, right0, 19f, 1f), + "3005" => RayHelpers.BossRelativePoint(bossPos0, fwd0, right0, -4f, 1f), + "3006" => RayHelpers.BossRelativePoint(bossPos0, fwd0, right0, 19f, 6f), + "3451" => RayHelpers.BossRelativePoint(bossPos0, fwd0, right0, -4f, 6f), + _ => Center + }; + + + sa.Method.SendChat($"/e 动态麻将点 sid={evt["StatusID"]} boss=({bossPos0.X:F1},{bossPos0.Z:F1}) => wpos=({wpos.X:F1},{wpos.Z:F1})"); + + var dp = sa.WaypointDp(wpos, 20000, 0, "开场火冲麻将指路"); + sa.Method.SendDraw(DrawModeEnum.Imgui, DrawTypeEnum.Displacement, dp); + } + + + [ScriptMethod( + name: "开场火冲麻将删除画图空中旋火射线记录", + eventType: EventTypeEnum.StatusRemove, + eventCondition: ["StatusID:regex:^(3004|3005|3006|3451)$"], + userControl: false + )] + public void 开场火冲麻将删除画图空中旋火射线记录(Event evt, ScriptAccessory sa) + { + var targetId = evt.TargetId(); + var myId = sa.Data.Me; + var statusId = evt["StatusID"]; + + // 只删自己身上的 + if (statusId != "3451") + { + if (targetId == 0 || myId == 0 || targetId != myId) + return; + } + + sa.Method.RemoveDraw("开场火冲麻将指路"); + + // 记录 + if (statusId != "3451") + return; + sa.Method.SendChat("/e 记录"); + if (targetId == 0) return; + + var targetObj = sa.Data.Objects.FirstOrDefault(o => o.GameObjectId == targetId); + if (targetObj == null) { + // sa.Method.SendChat($"/e 目标对象未找到,TargetId={targetId:X}"); + return; + } + + var bossObj = sa.Data.Objects.FirstOrDefault(o => o.DataId == 炽红); + if (bossObj == null) { + // sa.Method.SendChat($"/e Boss对象未找到,DataId={炽红:X}"); + return; + } + + var bossPos = bossObj.Position; + var targetPos = targetObj.Position; + + // 方向(XZ) + var d = new Vector2(targetPos.X - bossPos.X, targetPos.Z - bossPos.Z); + // sa.Method.SendChat($"/e 方向d=({d.X},{d.Y})"); + if (d.LengthSquared() < 0.0001f) return; + + _airSpinRayActive = true; + _airSpinRayBossPos = bossPos; + _airSpinRayTargetPos = targetPos; + _airSpinRayDirXZ = d; + + if (!_bossBasisReady) + { + // 说明火冲没触发过或者缓存丢了,没法按你的逻辑做 + return; + } + var bpos = _bossBasisPos; + var fwd = _bossFwdXZ; + var right= _bossRightXZ; + + var baseMap = new Dictionary + { + // Row points you described + { 4, RayHelpers.BossRelativePoint(bpos, fwd, right, 19f, 8f) }, + { 5, RayHelpers.BossRelativePoint(bpos, fwd, right, 9.5f, 8f) }, + { 1, RayHelpers.BossRelativePoint(bpos, fwd, right, 0f, 8f) }, + { 3, RayHelpers.BossRelativePoint(bpos, fwd, right, -9.5f, 8f) }, + { 7, RayHelpers.BossRelativePoint(bpos, fwd, right, -16f, 8f) }, + }; + + _airSpinP1Override.Clear(); + + // Move along boss forward until not covered + foreach (var kv in baseMap) + { + var idx = kv.Key; + var p0 = kv.Value; + + var pNew = RayHelpers.AdjustPointAlongDirUntilNotCoveredXZ( + p0, + _airSpinRayBossPos, + _airSpinRayDirXZ, + moveDirXZ: fwd, // IMPORTANT: move along boss forward towards center + halfWidth: 5f, + epsilon: 0.05f, + maxMove: 40f + ); + + pNew = RayHelpers.ClampXZ(pNew, 80f, 120f); + _airSpinP1Override[idx] = pNew; + } + + var baseRight19 = RayHelpers.BossRelativePoint(bpos, fwd, right, 19f, 8f); + // 先拿 right=19, forward=0 的最近不覆盖点当锚点 + var anchor = RayHelpers.AdjustPointAlongDirUntilNotCoveredXZ( + baseRight19, _airSpinRayBossPos, _airSpinRayDirXZ, + moveDirXZ: fwd, + halfWidth: 5f, epsilon: 0.05f, maxMove: 40f + ); + + static Vector3 AddForward(Vector3 p, Vector2 fwd, float dist) + => new Vector3(p.X + fwd.X * dist, 0f, p.Z + fwd.Y * dist); + + // 再从 anchor 加 9/18/27 + (int idx, float dist)[] col = { (0, 9f), (2, 18f), (6, 27f) }; + + foreach (var (idx, dist) in col) + { + var p = AddForward(anchor, fwd, dist); + + var pNew = RayHelpers.AdjustPointAlongDirUntilNotCoveredXZ( + p, _airSpinRayBossPos, _airSpinRayDirXZ, + moveDirXZ: fwd, + halfWidth: 5f, epsilon: 0.05f, maxMove: 40f + ); + + + pNew = RayHelpers.ClampXZ(pNew, 80f, 120f); + _airSpinP1Override[idx] = pNew; + } + + sa.Method.SendChat($"/e Ray boss={bossObj.GameObjectId:X} target={targetObj.GameObjectId:X} dir=({_airSpinRayDirXZ.X:F2},{_airSpinRayDirXZ.Y:F2}), adjusted {_airSpinP1Override[7]}."); + } + // 转场换P + [ScriptMethod( + name: "转场换P2", + eventType: EventTypeEnum.StartCasting, + eventCondition: ["ActionId:46553"], + userControl: false + )] + public void 转场换P2(Event evt, ScriptAccessory sa) + { + _phase = 2; + sa.Method.SendChat("/e 转场换P,当前阶段 P2"); + } + // 转场换P + [ScriptMethod( + name: "转场换P3", + eventType: EventTypeEnum.StartCasting, + eventCondition: ["ActionId:46563"], + userControl: false + )] + public void 转场换P3(Event evt, ScriptAccessory sa) + { + _phase = 3; + sa.Method.SendChat("/e 转场换P,当前阶段 P3"); + } + + // 空中旋火 + [ScriptMethod( + name: "空中旋火指路P1", + eventType: EventTypeEnum.StartCasting, + eventCondition: ["ActionId:46529"] + )] + public void 空中旋火指路P1(Event evt, ScriptAccessory sa) + { + var myIdx = sa.MyIndex(); + var targetId = evt.TargetId(); + var myId = sa.Data.Me; + if (_phase == 1) + { + // P1 + // 只画自己身上的 + if (targetId == 0 || myId == 0 || targetId != myId) + return; + + Vector3 wpos; + if (_airSpinRayActive && _airSpinP1Override.TryGetValue(myIdx, out var ov)) + { + wpos = ov; + } + else + { + wpos = myIdx switch + { + _ => new Vector3(0f, 0f, 0f) + }; + } + + var dp = sa.WaypointDp(wpos, 6000, 0, "空中旋火指路P1"); + sa.Method.SendDraw(DrawModeEnum.Imgui, DrawTypeEnum.Displacement, dp); + } + + if (_phase == 2 && _P2火圈次数 <= 3) + { + _P2火圈次数++; + // sa.Method.SendChat($"/e P2火圈次数 {_P2火圈次数}"); + + const uint BuffId = 4974; + var spots = new Vector3[4]; + if (p2第二三轮打法 == P2第二三轮打法.近战优化) + { + // 四个落点(y 固定 0) + // 如果P2左右==1 左边点,否则右边点 + if (_P2左右 == 1) + { + spots = new[] + { + new Vector3(118.91f, 0.00f, 87.64f), + new Vector3(119.09f, 0.00f, 95.83f), + new Vector3(118.96f, 0.00f, 103.66f), + new Vector3(119.39f, 0.00f, 110.78f), + }; + } + else + { + spots = new[] + { + new Vector3(118.91f, 0.00f, 87.64f), + new Vector3(119.09f, 0.00f, 95.83f), + new Vector3(118.96f, 0.00f, 103.66f), + new Vector3(119.39f, 0.00f, 110.78f), + }; + } + } + else + { + // 美 + if (_P2左右 == 1) + { + spots = new[] + { + new Vector3(118.09f, 0.00f, 96.88f), // 6 7 + new Vector3(112.2f, 0.00f, 93.43f), // 4 5 + new Vector3(112.2f, 0.00f, 103.74f), // 0 1 + new Vector3(117.98f, 0.00f, 102.98f), // 2 3 + }; + } + else + { + spots = new[] + { + new Vector3(81.72f, 0.00f, 96.88f), // 6 7 + new Vector3(87.8f, 0.00f, 93.43f), // 4 5 + new Vector3(87.8f, 0.00f, 103.74f), // 0 1 + new Vector3(81.72f, 0.00f, 102.98f), // 2 3 + }; + } + } + // 优先级:6 7 4 5 0 1 2 3 + var prio = new[] { 6, 7, 4, 5, 0, 1, 2, 3 }; + + // 1) 找到所有带 4974 的人(用 GetParty + HasStatus) + var buffed = sa.GetParty() + .Where(p => p != null && p.EntityId != 0 && p.HasStatus(BuffId)) + .ToList(); + + if (buffed.Count == 0) return; + + // 2) 按 prio 排序(按 PartyList 的 index 排) + var ordered = buffed + .Select(p => new { Player = p, Idx = sa.Data.PartyList.IndexOf(p.EntityId) }) + .Where(x => x.Idx >= 0) // 找不到就丢掉 + .OrderBy(x => Array.IndexOf(prio, x.Idx)) // prio 里越靠前越优先 + .ToList(); + + // prio 里没有的(Array.IndexOf 返回 -1)会排到最前面,所以要过滤掉 + ordered = ordered.Where(x => Array.IndexOf(prio, x.Idx) >= 0).ToList(); + + if (ordered.Count == 0) return; + + // 3) 只给自己画:我必须在前4个里 + var mySlot = -1; + for (int i = 0; i < Math.Min(4, ordered.Count); i++) + { + if (ordered[i].Player.EntityId == sa.Data.Me) + { + mySlot = i; + break; + } + } + if (mySlot < 0) return; + + var wpos = spots[mySlot]; + var dp = sa.WaypointDp(wpos, 6000, 0, "空中旋火指路P2"); + sa.Method.SendDraw(DrawModeEnum.Imgui, DrawTypeEnum.Displacement, dp); + + } + else if (_phase == 2 && _P2火圈次数 <= 7) + { + _P2火圈次数++; + // sa.Method.SendChat($"/e P2火圈次数 {_P2火圈次数}"); + + } + + if (_phase == 3) + { + // 其实是P4 + Vector3 wpos = myIdx switch + { + 0 => new Vector3(81.27f, 0.00f, 87.70f), + 1 => new Vector3(118.99f, 0.00f, 87.11f), + 2 => new Vector3(80.93f, 0.00f, 118.95f), + 3 => new Vector3(118.66f, 0.00f, 118.24f), + 4 => new Vector3(80.90f, 0.00f, 110.95f), + 5 => new Vector3(119.21f, 0.00f, 109.14f), + 6 => new Vector3(81.11f, 0.00f, 81.26f), + 7 => new Vector3(119.21f, 0.00f, 80.52f), + _ => Center + }; + var dp = sa.WaypointDp(wpos, 6000, 0, "空中旋火指路P3"); + sa.Method.SendDraw(DrawModeEnum.Imgui, DrawTypeEnum.Displacement, dp); + } + + + + } + + // 破势乘浪 + [ScriptMethod( + name: "破势乘浪", + eventType: EventTypeEnum.StartCasting, + eventCondition: ["ActionId:46542"] + )] + public async void 破势乘浪(Event evt, ScriptAccessory sa) + { + // await Task.Delay(3000); + // 获得危险区位置 + var effectPos = evt.EffectPosition(); + var sourceRotation = evt.SourceRotation; + // 画危险区 + var dpDanger = sa.Data.GetDefaultDrawProperties(); + dpDanger.Name = "破势乘浪-危险区"; + dpDanger.Position = effectPos; + dpDanger.Color = sa.Data.DefaultDangerColor; + dpDanger.DestoryAt = 8000; + dpDanger.Scale = new Vector2(15f, 50f); + dpDanger.Rotation = sourceRotation; + dpDanger.FixRotation = true; + dpDanger.ScaleMode = ScaleMode.None; + sa.Method.SendDraw(DrawModeEnum.Default, DrawTypeEnum.Rect, dpDanger); + + // 获得 DataId 19290的对象的位置和朝向 + var obj = sa.Data.Objects.FirstOrDefault(o => o.DataId == 19290); + if (obj == null) return; + + var myIdx = sa.MyIndex(); + + // 以 obj 朝向构建局部坐标(y 固定 0) + var objPos = new Vector3(obj.Position.X, 0f, obj.Position.Z); + var rot = obj.Rotation; + + // forward: 朝向;left/right: 垂直于朝向 + var forward = new Vector3((float)Math.Sin(rot), 0f, (float)Math.Cos(rot)); + if (forward.LengthSquared() < 1e-6f) forward = new Vector3(0f, 0f, 1f); + forward = Vector3.Normalize(forward); + + var right = new Vector3(-forward.Z, 0f, forward.X); + right = Vector3.Normalize(right); + var left = -right; + var back = -forward; + + Vector3 wpos; + + if (_分摊分散 == 1) + { + sa.Method.EdgeTTS("分摊"); + // 双奶画分摊圈 + if (_phase == 1 || _phase == 3) + { + for (int i = 2; i < 4; i++) + { + var oid = sa.Data.PartyList[i]; + if (oid == 0) continue; + + var dpCircle = sa.Data.GetDefaultDrawProperties(); + dpCircle.Name = $"分摊圈_{oid:X}"; + dpCircle.Owner = oid; + dpCircle.Color = sa.Data.DefaultSafeColor; + dpCircle.DestoryAt = 10000; + dpCircle.Scale = new Vector2(5f); + dpCircle.ScaleMode = ScaleMode.ByTime; + + sa.Method.SendDraw(DrawModeEnum.Default, DrawTypeEnum.Circle, dpCircle); + } + } + else if (_phase == 2) + { + // P2 分摊只画有BUFF的治疗 + var buffId = 4975u; + for (int i = 2; i < 4; i++) + { + var oid = sa.Data.PartyList[i]; + var p = sa.GetParty().FirstOrDefault(x => x != null && x.EntityId == oid); + if (p == null || oid == 0 || !p.HasStatus(buffId)) + continue; + + var dpCircle = sa.Data.GetDefaultDrawProperties(); + dpCircle.Name = $"分摊圈_{oid:X}"; + dpCircle.Owner = oid; + dpCircle.Color = sa.Data.DefaultSafeColor; + dpCircle.DestoryAt = 10000; + dpCircle.Scale = new Vector2(5f); + dpCircle.ScaleMode = ScaleMode.ByTime; + + sa.Method.SendDraw(DrawModeEnum.Default, DrawTypeEnum.Circle, dpCircle); + } + } + + var dangerCenter = new Vector3(effectPos.X, 0f, effectPos.Z); + float dangerYaw = sourceRotation; + float dangerW = 15f; + float dangerL = 50f; + + // 分摊用的“车道距离”:左(-12) / 中(0) / 右(+12) + float lane = 15f; + + // 分摊只需要判断“后12”这一排会不会被危险区盖住 + float[] fbSamples = { -15f }; // forward * (-12) 等价于 back * 12 + + // 选出给偶数组(0/2/4/6)与奇数组(1/3/5/7)的车道 + RayHelpers.PickSafeLanesByDangerRect( + objPos, + right, // obj 的 right + forward, // obj 的 forward + dangerCenter, + dangerYaw, + dangerW, + dangerL, + lane, + fbSamples, + out var evenLane, + out var oddLane + ); + + bool isOdd = (myIdx % 2) == 1; + float laneOffset = isOdd ? oddLane : evenLane; + if (_phase == 1) + { + // 分摊固定:后15 + float fbDist = -15f; + + wpos = objPos + right * laneOffset + forward * fbDist; + wpos = new Vector3(wpos.X, 0f, wpos.Z); + + var dp = sa.WaypointDp(wpos, 7000, 0, "破势乘浪-分摊指路"); + sa.Method.SendDraw(DrawModeEnum.Imgui, DrawTypeEnum.Displacement, dp); + } + + // sa.Method.SendChat($"/e 分摊 idx={myIdx} evenLane={evenLane:F2} oddLane={oddLane:F2}"); + } + else + { + sa.Method.EdgeTTS("分散"); + // 所有人画 5m 的危险圈 + if (_phase == 1 || _phase == 3) + { + foreach (var oid in sa.Data.PartyList) // 这里的 oid 就是 uint + { + if (oid == 0) continue; + + var dpCircle = sa.Data.GetDefaultDrawProperties(); + dpCircle.Name = $"分散圈_{oid:X}"; + dpCircle.Owner = oid; + dpCircle.Color = sa.Data.DefaultDangerColor; + dpCircle.DestoryAt = 10000; + dpCircle.Scale = new Vector2(5f); + dpCircle.ScaleMode = ScaleMode.ByTime; + + sa.Method.SendDraw(DrawModeEnum.Default, DrawTypeEnum.Circle, dpCircle); + } + } + else if (_phase == 2) + { + // P2 分散只画有BUFF的人 + var buffId = 4975u; + foreach (var p in sa.GetParty()) + { + if (p == null || p.EntityId == 0 || !p.HasStatus(buffId)) + continue; + + var dpCircle = sa.Data.GetDefaultDrawProperties(); + dpCircle.Name = $"分散圈_{p.EntityId:X}"; + dpCircle.Owner = p.EntityId; + dpCircle.Color = sa.Data.DefaultDangerColor; + dpCircle.DestoryAt = 10000; + dpCircle.Scale = new Vector2(5f); + dpCircle.ScaleMode = ScaleMode.ByTime; + + sa.Method.SendDraw(DrawModeEnum.Default, DrawTypeEnum.Circle, dpCircle); + } + } + + + // 分散:按 idx 映射 + // 3/5/1/7: 左15,前后:+8 / -2 / -12 / -18 + // 2/4/0/6: 右15,前后:+8 / -2 / -12 / -18 + // 这里把 “前” 记为 +forward,“后” 记为 -forward + float lane = 15f; + float[] fbSamples = { +8f, -2f, -12f, -18f }; + + var dangerCenter = new Vector3(effectPos.X, 0f, effectPos.Z); + float dangerYaw = sourceRotation; + float dangerW = 15f; + float dangerL = 50f; + + RayHelpers.PickSafeLanesByDangerRect( + objPos, + right, // 注意:这里用 obj 的 right(你上面算出来的 Vector3 right) + forward, // 用 obj 的 forward + dangerCenter, + dangerYaw, + dangerW, + dangerL, + lane, + fbSamples, + out var evenLane, + out var oddLane + ); + // ====== 每个人前后位置还是按 idx 映射 ====== + float fbDist; + if (myIdx == 3) fbDist = +8f; + else if (myIdx == 5) fbDist = -2f; + else if (myIdx == 1) fbDist = -12f; + else if (myIdx == 7) fbDist = -18f; + else if (myIdx == 2) fbDist = +8f; + else if (myIdx == 4) fbDist = -2f; + else if (myIdx == 0) fbDist = -12f; + else if (myIdx == 6) fbDist = -18f; + else fbDist = -12f; + + bool isOdd = (myIdx % 2) == 1; + float laneOffset = isOdd ? oddLane : evenLane; + + if (_phase == 1) + { + wpos = objPos + right * laneOffset + forward * fbDist; + wpos = new Vector3(wpos.X, 0f, wpos.Z); + + var dp = sa.WaypointDp(wpos, 7000, 0, "破势乘浪-分散指路"); + sa.Method.SendDraw(DrawModeEnum.Imgui, DrawTypeEnum.Displacement, dp); + } + + // sa.Method.SendChat($"/e 分散 idx={myIdx} evenLane={evenLane:F2} oddLane={oddLane:F2}"); + } + } + + // 水基佬分摊分散记录 + [ScriptMethod( + name: "水基佬分摊分散记录", + eventType: EventTypeEnum.StatusAdd, + eventCondition: ["StatusID:2056"], + userControl: false + ) + ] + public void 水基佬分摊分散记录(Event evt, ScriptAccessory sa) + { + if (!int.TryParse(evt["Param"], out var param)) return; + _分摊分散 = param switch + { + 1005 => 1, // 分摊 + 1006 => 2, // 分散 + 1007 => 1, // 分摊(P3) + 1008 => 2, // 分散(P3) + }; + sa.Method.EdgeTTS(_分摊分散 == 1 ? "待会儿分摊" : "待会儿分散"); + } + + // 击退 + [ScriptMethod( + name: "惊涛击退画图", + eventType: EventTypeEnum.SetObjPos, + eventCondition: ["SourceDataId:19290"] + )] + public async void 惊涛击退(Event evt, ScriptAccessory sa) + { + var duration = 19000; + var rotation = evt.SourceRotation; + + var dp = sa.Data.GetDefaultDrawProperties(); + dp.Name = "击退方向"; + dp.Owner = sa.Data.Me; + dp.Color = new Vector4 (1f, 1f, 0, 1f); // yellow + dp.DestoryAt = duration; + dp.Scale = new Vector2(1f, 10f); + dp.Rotation = rotation; + dp.FixRotation = true; + dp.ScaleMode = ScaleMode.None; + + if (_phase == 1) + sa.Method.SendDraw(DrawModeEnum.Default, DrawTypeEnum.Displacement, dp); + + if (_phase == 2) + { + await Task.Delay(30000); + dp.DestoryAt = 10000; + sa.Method.SendDraw(DrawModeEnum.Default, DrawTypeEnum.Displacement, dp); + } + + + } + + // 火基佬四连跳画图 + [ScriptMethod( + name: "火基佬四连跳画图", + eventType: EventTypeEnum.StartCasting, + eventCondition: ["ActionId:46532"] + )] + public void 火基佬四连跳(Event evt, ScriptAccessory sa) + { + const uint FireBuffId = 4974; // 火buff + const uint Duration = 11000; + // 只有自己有buff的才画 + var buffed = sa.GetParty() + .Where(p => p != null && p.EntityId != 0 && p.HasStatus(FireBuffId)) + .ToList(); + if (!sa.GetParty().Any(p => p != null && p.EntityId == sa.Data.Me && p.HasStatus(FireBuffId))) + return; + + // 3) 美式野:火buff组指路(你给的固定坐标) + if (_phase >= 2 && p2第二三轮打法 == P2第二三轮打法.美式野) + { + var myIdx = sa.MyIndex(); + if (myIdx < 0 || myIdx > 7) return; + + Vector3 wpos = myIdx switch + { + 0 or 1 => new Vector3(88.26f, 0f, 88.20f), + 2 or 3 => new Vector3(112.14f, 0f, 88.24f), + 4 or 5 => new Vector3(112.22f, 0f, 111.80f), + _ => new Vector3(88.18f, 0f, 111.87f), + }; + + var dpWp = sa.WaypointDp(wpos, Duration, 0, "火基佬四连跳指路"); + sa.Method.SendDraw(DrawModeEnum.Imgui, DrawTypeEnum.Displacement, dpWp); + } + } + + + // 清除火基佬四连跳画图 + [ScriptMethod( + name: "火基佬四连跳二连指路", + eventType: EventTypeEnum.ActionEffect, + eventCondition: ["ActionId:regex:^(47390|47391|47392|47393)$"], + userControl: false + )] + public void 火基佬四连跳二连指路(Event evt, ScriptAccessory sa) + { + var targetId = evt.TargetId(); + if (targetId == 0) return; + + // 清除指路/连线/圈 + if (targetId == sa.Data.Me) + { + sa.Method.RemoveDraw("火基佬四连跳指路"); + // sa.Method.SendChat("/e 火基佬四连跳指路2"); + + // var dp = sa.WaypointDp(Center, 4000, 0, "火基佬四连跳指路2"); + // sa.Method.SendDraw(DrawModeEnum.Imgui, DrawTypeEnum.Displacement, dp); + + // 指勾八,自己去场中吧 + } + } + + // 深海冲击 + [ScriptMethod( + name: "深海冲击", + eventType: EventTypeEnum.StartCasting, + eventCondition: ["ActionId:46519"] + )] + public void 深海冲击(Event evt, ScriptAccessory sa) + { + sa.Method.EdgeTTS("坦克远离人群,注意死刑"); + sa.Method.TextInfo("T远离人群,注意死刑", 4500, true); + } + + + + // ===== 水波P2指路补完:角色/角度工具 ===== + private enum WaterRole + { + Tank, + Healer, + Melee, + Ranged + } + + private static WaterRole GetRoleByPartyIndex(int idx) + { + // 默认按你常用的小队排序: + // 0/1 = T, 2/3 = H, 4/5 = 近战, 6/7 = 远程 + if (idx == 0 || idx == 1) return WaterRole.Tank; + if (idx == 2 || idx == 3) return WaterRole.Healer; + if (idx == 4 || idx == 5) return WaterRole.Melee; + return WaterRole.Ranged; + } + + private static Vector3 DirFromYaw(float yaw) + { + // 与你脚本其它地方保持一致:forward = (sin(yaw), cos(yaw)) + return new Vector3(MathF.Sin(yaw), 0f, MathF.Cos(yaw)); + } + + private static float YawFromForward(Vector3 fwd) + { + // 与你 YawFromDir 同一套:yaw = atan2(x, z) + var d = new Vector2(fwd.X, fwd.Z); + if (d.LengthSquared() < 1e-6f) return 0f; + d = Vector2.Normalize(d); + return MathF.Atan2(d.X, d.Y); + } + + private static float DegToRad(float deg) => deg * (MathF.PI / 180f); + + // 约定:0=南,90=东,180=北,270=西 + // 北偏东 = 180 - t;南偏东 = 0 + t + private static float YawDeg_N_By_E(float t) => 180f - t; + private static float YawDeg_S_By_E(float t) => 0f + t; + + // 北偏西 = 180 + t;南偏西 = 360 - t + private static float YawDeg_N_By_W(float t) => 180f + t; + private static float YawDeg_S_By_W(float t) => 360f - t; + + private static float Normalize180(float deg) + { + deg %= 360f; + if (deg > 180f) deg -= 360f; + if (deg < -180f) deg += 360f; + return deg; + } + private float GetWaterOffsetRad(P2第一轮打法 strat, bool zHigh, WaterRole role, int 左右) + { + float yawDeg = 90f; // 默认给个值 + float baseDeg = (左右 == 1) ? 90f : 270f; // 左=东基准;右=西基准 + + if (左右 == 1) + { + // ===== 你原来的“左(东基准)”逻辑不动 ===== + if (strat == P2第一轮打法.水波) + { + if (zHigh) + { + yawDeg = role switch + { + WaterRole.Tank => YawDeg_S_By_E(5f), + WaterRole.Melee => YawDeg_N_By_E(5f), + WaterRole.Ranged => YawDeg_S_By_E(45f), + WaterRole.Healer => YawDeg_S_By_E(30f), + _ => 90f + }; + } + else + { + yawDeg = role switch + { + WaterRole.Melee => YawDeg_N_By_E(5f), + WaterRole.Tank => YawDeg_S_By_E(5f), + WaterRole.Healer => YawDeg_N_By_E(45f), + WaterRole.Ranged => YawDeg_N_By_E(30f), + _ => 90f + }; + } + } + else // 镜像水波 + { + if (zHigh) + { + yawDeg = role switch + { + WaterRole.Tank => YawDeg_N_By_E(5f), + WaterRole.Melee => YawDeg_S_By_E(5f), + WaterRole.Ranged => YawDeg_S_By_E(35f), + WaterRole.Healer => YawDeg_S_By_E(45f), + _ => 90f + }; + } + else + { + yawDeg = role switch + { + WaterRole.Tank => YawDeg_S_By_E(5f), + WaterRole.Melee => YawDeg_N_By_E(5f), + WaterRole.Ranged => YawDeg_N_By_E(30f), + WaterRole.Healer => YawDeg_N_By_E(45f), + _ => 90f + }; + } + } + } + else if (左右 == 2) + { + // ===== 右(西基准):把 E 换成 W,其它结构保持一致 ===== + if (strat == P2第一轮打法.水波) + { + if (zHigh) + { + yawDeg = role switch + { + WaterRole.Tank => YawDeg_S_By_W(5f), + WaterRole.Melee => YawDeg_N_By_W(5f), + WaterRole.Ranged => YawDeg_S_By_W(45f), + WaterRole.Healer => YawDeg_S_By_W(30f), + _ => 270f + }; + } + else + { + yawDeg = role switch + { + WaterRole.Melee => YawDeg_N_By_W(5f), + WaterRole.Tank => YawDeg_S_By_W(5f), + WaterRole.Healer => YawDeg_N_By_W(45f), + WaterRole.Ranged => YawDeg_N_By_W(30f), + _ => 270f + }; + } + } + else // 镜像水波 + { + if (zHigh) + { + yawDeg = role switch + { + WaterRole.Tank => YawDeg_N_By_W(5f), + WaterRole.Melee => YawDeg_S_By_W(5f), + WaterRole.Ranged => YawDeg_S_By_W(35f), + WaterRole.Healer => YawDeg_S_By_W(45f), + _ => 270f + }; + } + else + { + yawDeg = role switch + { + WaterRole.Tank => YawDeg_S_By_W(5f), + WaterRole.Melee => YawDeg_N_By_W(5f), + WaterRole.Ranged => YawDeg_N_By_W(30f), + WaterRole.Healer => YawDeg_N_By_W(45f), + _ => 270f + }; + } + } + } + + // 最终偏移角:左用东(90),右用西(270) + float offDeg = Normalize180(yawDeg - baseDeg); + return DegToRad(offDeg); + } + + // 水基佬水波 + [ScriptMethod( + name: "水波画图", + eventType: EventTypeEnum.StartCasting, + eventCondition: ["ActionId:regex:^(46557|46560)$"] + )] + public async void 水波画图(Event evt, ScriptAccessory sa) + { + var actionId = evt["ActionId"]; + + // 给队里所有人画扇形 from boss to oid + if (!int.TryParse(evt["DurationMilliseconds"], out var duration)) return; + if (_phase == 1 || _phase == 3 || _phase == 5) + { + foreach (var oid in sa.Data.PartyList) + { + var sourceId = evt.SourceId(); + if (oid == 0) continue; + + var dp = sa.Data.GetDefaultDrawProperties(); + dp.Name = $"水波扇形_{oid:X}"; + dp.Owner = evt.SourceId(); + dp.TargetObject = oid; + dp.Color = sa.Data.DefaultDangerColor; + dp.DestoryAt = (uint)duration; + dp.Scale = new Vector2(60f, 60f); // radius, length + dp.ScaleMode = ScaleMode.ByTime; + dp.Radian = float.Pi / 6f; // 30 degrees + + sa.Method.SendDraw(DrawModeEnum.Default, DrawTypeEnum.Fan, dp); + } + } + else if (_phase == 2) + { + // 只在身上有statusid 4975的人画扇形 + const uint BuffId = 4975; + var buffed = sa.GetParty() + .Where(p => p != null && p.EntityId != 0 && p.HasStatus(BuffId)) + .ToList(); + + if (buffed.Count == 0) return; + foreach (var p in buffed) + { + var oid = p.EntityId; + + var dp = sa.Data.GetDefaultDrawProperties(); + dp.Name = $"水波扇形_{oid:X}"; + dp.Owner = evt.SourceId(); + dp.TargetObject = oid; + dp.Color = sa.Data.DefaultDangerColor; + dp.DestoryAt = (uint)duration; + dp.Scale = new Vector2(60f, 60f); // radius, length + dp.ScaleMode = ScaleMode.ByTime; + dp.Radian = float.Pi / 6f; // 30 degrees + + sa.Method.SendDraw(DrawModeEnum.Default, DrawTypeEnum.Fan, dp); + } + + } + + var myIdx = sa.MyIndex(); + var bossId = evt.SourceId(); + var bossObj = sa.Data.Objects.FirstOrDefault(o => o.GameObjectId == bossId); + // boss 位置(y 固定 0) + var bossPos = new Vector3(bossObj.Position.X, 0f, bossObj.Position.Z); + + if (bossObj == null) return; + // 画图,0去boss正前面,1去boss正后,2去boss左侧,3去boss右侧,4去boss左后,5去boss右后,6去boss左前,7去boss右前。去的位置都是以boss为中心,距离4m处 + if (_phase == 1) + { + // boss 朝向 -> forward/right(y 固定 0) + var rot = bossObj.Rotation; + var fwd = new Vector3((float)Math.Sin(rot), 0f, (float)Math.Cos(rot)); + if (fwd.LengthSquared() < 1e-6f) fwd = new Vector3(0f, 0f, 1f); + fwd = Vector3.Normalize(fwd); + + var right = new Vector3(-fwd.Z, 0f, fwd.X); + right = Vector3.Normalize(right); + + var left = -right; + var back = -fwd; + + float r = 4f; // 距离 4m + + Vector3 wpos = myIdx switch + { + 0 => bossPos + fwd * r, // 正前 + 1 => bossPos + back * r, // 正后 + 2 => bossPos + left * r, // 左 + 3 => bossPos + right * r, // 右 + 4 => bossPos + Vector3.Normalize(left + back) * r, // 左后 + 5 => bossPos + Vector3.Normalize(right + back) * r, // 右后 + 6 => bossPos + Vector3.Normalize(left + fwd) * r, // 左前 + 7 => bossPos + Vector3.Normalize(right + fwd) * r, // 右前 + _ => bossPos + }; + + wpos = new Vector3(wpos.X, 0f, wpos.Z); + + // 指路(持续时间用同一个 duration) + var dpWp = sa.WaypointDp(wpos, (uint)duration, 0, "水波站位指路"); + sa.Method.SendDraw(DrawModeEnum.Imgui, DrawTypeEnum.Displacement, dpWp); + } + else if (_phase == 2) + { + const uint BuffId = 4975; + float r = 4f; + // 判断boss在左半场和右半场:x<100左以东基准,x>100右以西基准 + _P2左右 = bossPos.X < 100f ? 1 : 2; + // sa.Method.SendChat($"/e P2左右"); + // sa.Method.SendChat($"/e P2左右={_P2左右}"); + // 只在身上有 4975 的人参与分配 + var buffed = sa.GetParty() + .Where(p => p != null && p.EntityId != 0 && p.HasStatus(BuffId)) + .ToList(); + + if (buffed.Count == 0) return; + + // 只给自己画(你之前其它指路也是这个风格) + if (!buffed.Any(p => p.EntityId == sa.Data.Me)) return; + + // 判断 boss 在上半场还是下半场:z>100 + bool zHigh = bossPos.Z > 100f; + + var myIdx2 = sa.MyIndex(); + if (myIdx2 < 0) return; + + var role = GetRoleByPartyIndex(myIdx2); + float baseYaw; + if (_P2左右 == 1) + { + // 左半场,以东为基准 + baseYaw = MathF.PI / 2f; + } + else + { + // 右半场,以西为基准 + baseYaw = -MathF.PI / 2f; + } + // float baseYaw = MathF.PI / 2f; + + float offRad = GetWaterOffsetRad(p2第一轮打法, zHigh, role, _P2左右); + float yaw = baseYaw + offRad; + + var dir = DirFromYaw(yaw); // (sin(yaw), 0, cos(yaw)) + var wpos2 = bossPos + dir * r; + wpos2 = new Vector3(wpos2.X, 0f, wpos2.Z); + + var dpWp2 = sa.WaypointDp(wpos2, (uint)duration, 0, "水波站位指路P2"); + sa.Method.SendDraw(DrawModeEnum.Imgui, DrawTypeEnum.Displacement, dpWp2); + } + + + + await Task.Delay(duration); + // 记录每个玩家位置 + // 然后// --- 情况 A: 46558 (保持原方向 往每个玩家位置喷30度) --- // --- 情况 B: 46560 (左右偏转 24度,各画一个 15度扇形) --- + var snapPos = new Dictionary(); + foreach (var oid in sa.Data.PartyList) + { + if (oid == 0) continue; + + var obj = sa.Data.Objects.FirstOrDefault(o => o.GameObjectId == oid); + if (obj == null) continue; + + snapPos[oid] = new Vector3(obj.Position.X, 0f, obj.Position.Z); + } + sa.Method.SendChat($"/e 水波喷射记录完毕"); + + // 重新抓一次 boss(防止 cast 期间移动) + bossObj = sa.Data.Objects.FirstOrDefault(o => o.GameObjectId == bossId); + if (bossObj == null) return; + + bossPos = new Vector3(bossObj.Position.X, 0f, bossObj.Position.Z); + + // ============= 4) 根据 actionId 画后续喷射 ============= + const uint afterMs = 3500; // 后续扇形显示时间,你想短点/长点都行 + const float radius = 60f; + const float length = 60f; + + static float YawFromDir(Vector2 d) + { + // 与你 sin/cos 的 forward 定义一致:yaw=atan2(x,z) + if (d.LengthSquared() < 1e-6f) return 0f; + d = Vector2.Normalize(d); + return MathF.Atan2(d.X, d.Y); + } + + void DrawFanFixed(string name, float yaw, float radian) + { + var dp = sa.Data.GetDefaultDrawProperties(); + dp.Name = name; + dp.Position = bossPos; // 固定从 boss 位置发射 + dp.Rotation = yaw; + dp.FixRotation = true; + dp.Color = sa.Data.DefaultDangerColor; + dp.DestoryAt = afterMs; + dp.Scale = new Vector2(radius, length); + dp.ScaleMode = ScaleMode.ByTime; + dp.Radian = radian; + sa.Method.SendDraw(DrawModeEnum.Default, DrawTypeEnum.Fan, dp); + } + + if (actionId == "46557") + { + // --- 情况 A: 保持原方向,往每个玩家当时位置喷 30度 --- + if (_phase == 1 || _phase == 3 || _phase == 5) + { + foreach (var kv in snapPos) + { + var oid = kv.Key; + var p = kv.Value; + + var dir = new Vector2(p.X - bossPos.X, p.Z - bossPos.Z); + var yaw = YawFromDir(dir); + + DrawFanFixed($"水波喷射A_{oid:X}", yaw, float.Pi / 6f); // 30° + } + } + else if (_phase == 2) + { + const uint BuffId = 4975; + var buffed = sa.GetParty() + .Where(p => p != null && p.EntityId != 0 && p.HasStatus(BuffId)) + .ToList(); + if (buffed.Count == 0) return; + foreach (var p in buffed) + { + var oid = p.EntityId; + if (!snapPos.TryGetValue(oid, out var pos)) continue; + + var dir = new Vector2(pos.X - bossPos.X, pos.Z - bossPos.Z); + var yaw = YawFromDir(dir); + + DrawFanFixed($"水波喷射A_{oid:X}", yaw, float.Pi / 6f); // 30° + } + } + } + else if (actionId == "46560") + { + // --- 情况 B: 左右偏转 24度,各画一个 15度扇形 --- + float off = 24f * (float)Math.PI / 180f; // 24° + float rad = 15f * (float)Math.PI / 180f; // 15° + if (_phase == 1 || _phase == 3 || _phase == 5) + { + foreach (var kv in snapPos) + { + var oid = kv.Key; + var p = kv.Value; + + var dir = new Vector2(p.X - bossPos.X, p.Z - bossPos.Z); + var baseYaw = YawFromDir(dir); + + DrawFanFixed($"水波喷射B_L_{oid:X}", baseYaw - off, rad); + DrawFanFixed($"水波喷射B_R_{oid:X}", baseYaw + off, rad); + } + } + else if (_phase == 2) + { + const uint BuffId = 4975; + var buffed = sa.GetParty() + .Where(p => p != null && p.EntityId != 0 && p.HasStatus(BuffId)) + .ToList(); + if (buffed.Count == 0) return; + foreach (var p in buffed) + { + var oid = p.EntityId; + if (!snapPos.TryGetValue(oid, out var pos)) continue; + + var dir = new Vector2(pos.X - bossPos.X, pos.Z - bossPos.Z); + var baseYaw = YawFromDir(dir); + + DrawFanFixed($"水波喷射B_L_{oid:X}", baseYaw - off, rad); + DrawFanFixed($"水波喷射B_R_{oid:X}", baseYaw + off, rad); + } + } + } + } + + [ScriptMethod(name: "狂浪腾空", eventType: EventTypeEnum.EnvControl, eventCondition: ["Index:regex:^\\d+$"])] + public void 狂浪腾空(Event evt, ScriptAccessory sa) + { + // 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), + if (!EnvHelpers.TryGetGridIndexFlag(evt, out var index, out var flag)) return; + if (!EnvHelpers.TryGetGridPos(index, out var gridPos)) return; + + var baseName = $"九宫格_{index}"; + + // 仅 flag=4/8 时清除并退出(保持原逻辑) + if (EnvHelpers.IsClearFlag(flag)) + { + EnvHelpers.ClearGridDraw(sa, baseName); + return; + } + + var drawColor = sa.Data.DefaultDangerColor; + + switch (EnvHelpers.GetMechanic(flag)) + { + case EnvHelpers.GridMechanic.Tankbuster: + EnvHelpers.DrawTB(sa, baseName, gridPos, drawColor); + break; + case EnvHelpers.GridMechanic.Stack: + EnvHelpers.DrawStack(sa, baseName, gridPos, sa.Data.DefaultSafeColor); + break; + case EnvHelpers.GridMechanic.Spread: + EnvHelpers.DrawSpread(sa, baseName, gridPos, drawColor); + break; + default: + break; + } + + if (_phase != 5) return; + // P5 + bool isWaterSnake = EnvHelpers.IsWater(flag); // 2/32/128 + bool isFireSnake = (flag == 512 || flag == 2048 || flag == 8192); + + if (!isWaterSnake && !isFireSnake) return; + + var myIdx = sa.MyIndex(); + if (myIdx < 0 || myIdx > 7) return; + + // 当前格子对应的蛇类型 + var snakeFromFlag = isWaterSnake ? P5火水蛇.水 : P5火水蛇.火; + + // 我自己的蛇形BUFF是否与该格子“相同” + bool sameSnake = (_P5火水蛇 != P5火水蛇.无) && (_P5火水蛇 == snakeFromFlag); + + // 发指路的小函数:只给自己发 + void DrawWpIfNeeded(string name) + { + var wpos = new Vector3(gridPos.X, 0f, gridPos.Z); + var dpWp = sa.WaypointDp(wpos, 6000, 0, name); + if (p5换边指路) + { + sa.Method.SendDraw(DrawModeEnum.Imgui, DrawTypeEnum.Displacement, dpWp); + } + } + + // 1) 死刑(TB):flag=128 或 8192 + // TextInfo 提示换T,并且只给 T(0/1) 指路:如果自己的蛇形 != 格子的蛇形 才去 + if (flag == 128 || flag == 8192) + { + sa.Method.TextInfo("死刑:换T", 2500, false); + + if (myIdx == 0 || myIdx == 1) + { + if (!sameSnake) + DrawWpIfNeeded("狂浪腾空-换T指路"); + } + return; // 死刑格子不参与下面的“轮换奶/近/远” + } + + // 2) 分摊/分散:flag=2/32/512/2048 + bool isSpreadOrStack = (flag == 2 || flag == 32 || flag == 512 || flag == 2048); + if (!isSpreadOrStack) return; + + // 轮换提示:0奶 -> 1近 -> 2远 + if (_P5狂浪次数 < 2) sa.Method.TextInfo("狂浪:换奶", 2500, false); + else if (_P5狂浪次数 < 4) sa.Method.TextInfo("狂浪:换近战", 2500, false); + else sa.Method.TextInfo("狂浪:换远程", 2500, false); + + // 决定这一轮谁要去“对蛇格子”(且:自己的蛇形 != 格子的蛇形 才去) + bool shouldHandle = + (_P5狂浪次数 < 2 && (myIdx == 2 || myIdx == 3)) || // 奶 + (_P5狂浪次数 >= 2 && _P5狂浪次数 < 4 && (myIdx == 4 || myIdx == 5)) || // 近战 + (_P5狂浪次数 >= 4 && _P5狂浪次数 < 6 && (myIdx == 6 || myIdx == 7)); // 远程 + + // bool shouldHandleHeal = (_P5狂浪次数 < 2 && (myIdx == 2 || myIdx == 3)); + // bool shouldHandleMelee = (_P5狂浪次数 >= 2 && _P5狂浪次数 < 4 && (myIdx == 4 || myIdx == 5)); + // bool shouldHandleRanged = (_P5狂浪次数 >= 4 && _P5狂浪次数 < 6 && (myIdx == 6 || myIdx == 7)); + // sa.Method.SendChat($"/e 狂浪 shouldHandle={shouldHandle} 奶={shouldHandleHeal} 近={shouldHandleMelee} 远={shouldHandleRanged} sameSnake={sameSnake}"); + if (shouldHandle) + { + if (!sameSnake) + DrawWpIfNeeded("狂浪腾空-对蛇指路"); + } + + // 次数推进:0->1->2->0 循环 + // if (!isWaterSnake) return; + _P5狂浪次数 = _P5狂浪次数 + 1; + sa.Method.SendChat($"/e P5狂浪次数={_P5狂浪次数}"); + } + + [ScriptMethod( + name: "浪尖转体分摊分散", + eventType: EventTypeEnum.EnvControl, + eventCondition: ["Index:regex:^\\d+$"] + )] + public async void 浪尖转体分摊分散(Event evt, ScriptAccessory sa) + { + if (!int.TryParse(evt["Index"], out var index)) return; + if (!int.TryParse(evt["Flag"], out var flag)) return; + + if (index != 4) + return; + + if (flag == 2048) + { + sa.Method.EdgeTTS("待会儿分散"); + + await Task.Delay(5000); + // 有BUFF的人画圈 + const uint BuffId = 4975; + var buffed = sa.GetParty() + .Where(p => p != null && p.EntityId != 0 && p.HasStatus(BuffId)) + .ToList(); + if (buffed.Count == 0) return; + foreach (var p in buffed) + { + var oid = p.EntityId; + + var dpCircle = sa.Data.GetDefaultDrawProperties(); + dpCircle.Name = $"浪尖转体分散圈_{oid:X}"; + dpCircle.Owner = oid; + dpCircle.Color = sa.Data.DefaultDangerColor; + dpCircle.DestoryAt = 5000; + dpCircle.Scale = new Vector2(5f); + dpCircle.ScaleMode = ScaleMode.ByTime; + + sa.Method.SendDraw(DrawModeEnum.Default, DrawTypeEnum.Circle, dpCircle); + } + } + else if (flag == 128) + { + sa.Method.EdgeTTS("待会儿分摊"); + + await Task.Delay(5000); + // 有BUFF的人画圈 + // P2 分摊只画有BUFF的治疗 + var buffId = 4975u; + for (int i = 2; i < 4; i++) + { + var oid = sa.Data.PartyList[i]; + var p = sa.GetParty().FirstOrDefault(x => x != null && x.EntityId == oid); + if (p == null || oid == 0 || !p.HasStatus(buffId)) + continue; + + var dpCircle = sa.Data.GetDefaultDrawProperties(); + dpCircle.Name = $"分摊圈_{oid:X}"; + dpCircle.Owner = oid; + dpCircle.Color = sa.Data.DefaultSafeColor; + dpCircle.DestoryAt = 5000; + dpCircle.Scale = new Vector2(5f); + dpCircle.ScaleMode = ScaleMode.ByTime; + + sa.Method.SendDraw(DrawModeEnum.Default, DrawTypeEnum.Circle, dpCircle); + } + } + } + + // 浪尖转体 + [ScriptMethod( + name: "浪尖转体", + eventType: EventTypeEnum.StartCasting, + eventCondition: ["ActionId:regex:^(46547|46488)$"] + )] + public void 浪尖转体(Event evt, ScriptAccessory sa) + { + var sourcePos0 = evt.SourcePosition(); + var sourceRot = evt.SourceRotation; + + // y 固定 0 + var sourcePos = new Vector3(sourcePos0.X, 0f, sourcePos0.Z); + + var dp = sa.Data.GetDefaultDrawProperties(); + dp.Name = "浪尖转体-扇形"; + dp.Position = sourcePos; + dp.Rotation = sourceRot; + dp.FixRotation = true; + dp.Color = sa.Data.DefaultDangerColor; + dp.DestoryAt = 6500; + dp.Radian = (float)Math.PI * 2f / 3f; // 120 degrees + dp.Scale = new Vector2(50f); + dp.ScaleMode = ScaleMode.ByTime; + sa.Method.SendDraw(DrawModeEnum.Default, DrawTypeEnum.Fan, dp); + + _浪花位置 = sourcePos; + + // ===== Buff 判定 ===== + const uint WaterBuffId = 4975; + const uint FireBuffId = 4974; + + bool hasWater = sa.GetParty().Any(p => p != null && p.EntityId == sa.Data.Me && p.HasStatus(WaterBuffId)); + bool hasFire = sa.GetParty().Any(p => p != null && p.EntityId == sa.Data.Me && p.HasStatus(FireBuffId)); + + if (!hasWater && !hasFire) return; + + var myIdx = sa.MyIndex(); + if (myIdx < 0 || myIdx > 7) return; + + bool zHigh = sourcePos.Z > 100f; // 上/下 + + // 分组:0/1, 2/3, 4/5, 6/7 + int pairGroup = myIdx switch + { + 0 or 1 => 0, + 2 or 3 => 1, + 4 or 5 => 2, + _ => 3, // 6 or 7 + }; + + void DrawWp(Vector3 wpos, string name) + { + wpos = new Vector3(wpos.X, 0f, wpos.Z); + var dpWp = sa.WaypointDp(wpos, 6000, 0, name); + sa.Method.SendDraw(DrawModeEnum.Imgui, DrawTypeEnum.Displacement, dpWp); + } + + // ========================================= + // P1:你原来的兜底(没写 P1 美野就也走这个) + // ========================================= + if (_phase < 2) + { + var forward = new Vector3(MathF.Sin(sourceRot), 0f, MathF.Cos(sourceRot)); + if (forward.LengthSquared() < 1e-6f) forward = new Vector3(0f, 0f, 1f); + forward = Vector3.Normalize(forward); + + var wpos = sourcePos + forward * 10f; + DrawWp(wpos, "浪尖转体-指路"); + return; + } + + // ========================================= + // P2:按打法分流 + // ========================================= + if (p2第二三轮打法 == P2第二三轮打法.近战优化) + { + // ------------------------- + // 水 buff:所有水去固定点(你注释的 4 点) + // ------------------------- + if (hasWater) + { + Vector3 wposWater; + if (_P2左右 == 1) + { + // 左(东侧基准) + // wposWater = zHigh ? new Vector3(89f, 0f, 119f) : new Vector3(89f, 0f, 81f); + + wposWater = myIdx switch + { + 0 or 1 or 2 or 3 => zHigh ? new Vector3(80.65f, 0f, 113.99f) : new Vector3(80.65f, 0f, 86.01f), + 4 or 5 or 6 or 7 => zHigh ? new Vector3(87.49f, 0f, 114.21f): new Vector3(87.49f, 0f, 85.79f), + _ => zHigh ? new Vector3(89f, 0f, 119f) : new Vector3(89f, 0f, 81f), + }; + } + else + { + // 右(西侧基准) + wposWater = zHigh ? new Vector3(111f, 0f, 119f) : new Vector3(111f, 0f, 81f); + } + + DrawWp(wposWater, "浪尖转体-指路P2(水-近战优化)"); + } + + // ------------------------- + // 火 buff:按优先级 0 1 4 5 2 3 6 7 分 4 组去 4 个点 + // 你原来 xs 顺序写反了,这里按注释修正 + // ------------------------- + if (hasFire) + { + // z + float z = zHigh ? 119.5f : 80.5f; + + // _P2左右==1: 组0/1/2/3 => 116.83, 109.62, 90.4, 82.50 + // _P2左右==2: 组0/1/2/3 => 82.50, 90.4, 109.62, 116.83 + float[] xs = _P2左右 == 1 + ? new float[] { 80.50f, 90.40f, 104.62f, 111f } + : new float[] { 111f, 104.62f, 90.40f, 80.50f }; + + // 分组: (0,1)->0, (4,5)->1, (2,3)->2, (6,7)->3 (按你注释) + int fireGroup = myIdx switch + { + 0 or 1 => 0, + 4 or 5 => 1, + 2 or 3 => 2, + _ => 3, // 6 or 7 + }; + + var wposFire = new Vector3(xs[fireGroup], 0f, z); + DrawWp(wposFire, "浪尖转体-指路P2(火-近战优化)"); + } + + return; + } + + // ========================================= + // 美式野:严格按你给的表(火 buff 表 / 水 buff 表) + // ========================================= + // 你给的点位是“固定坐标表”,这里不再用 _P2左右 + // 只要有火就走火表,否则走水表(避免同一人画两次) + bool useFireTable = hasFire; + + Vector3 wposUS; + if (!zHigh) + { + // sourcePos.Z < 100 + if (useFireTable) + { + wposUS = pairGroup switch + { + 0 => new Vector3(103.22f, 0.00f, 80.61f), // 0/1 + 1 => new Vector3(119.57f, 0.00f, 80.61f), // 2/3 + 2 => new Vector3(109.78f, 0.00f, 80.61f), // 4/5 + _ => new Vector3(118.76f, 0.00f, 88.66f), // 6/7 + }; + } + else + { + wposUS = pairGroup switch + { + 0 => new Vector3(96.78f, 0.00f, 80.61f), // 100-3.22 + 1 => new Vector3(80.43f, 0.00f, 80.61f), + 2 => new Vector3(90.22f, 0.00f, 80.61f), + _ => new Vector3(81.24f, 0.00f, 88.66f), + }; + } + } + else + { + // sourcePos.Z > 100 + if (useFireTable) + { + wposUS = pairGroup switch + { + 0 => new Vector3(103.22f, 0.00f, 119.39f), // 0/1 + 1 => new Vector3(119.57f, 0.00f, 119.55f), // 2/3 + 2 => new Vector3(109.78f, 0.00f, 119.35f), // 4/5 + _ => new Vector3(118.76f, 0.00f, 113.26f), // 6/7 + }; + } + else + { + wposUS = pairGroup switch + { + 0 => new Vector3(96.78f, 0.00f, 119.39f), // 100-3.22 + 1 => new Vector3(80.43f, 0.00f, 119.55f), + 2 => new Vector3(90.22f, 0.00f, 119.35f), + _ => new Vector3(81.24f, 0.00f, 113.26f), + }; + } + } + + DrawWp(wposUS, useFireTable ? "浪尖转体-指路P2(火-美式野)" : "浪尖转体-指路P2(水-美式野)"); + } + + // P3 水牢 + [ScriptMethod( + name: "进牢指路", + eventType: EventTypeEnum.StartCasting, + eventCondition: ["ActionId:46563"] + )] + public void 进牢指路(Event evt, ScriptAccessory sa) + { + // 看进水牢方式的选择,指路(100,0,100).如果选择是T就是只有0 1画,治疗是2 3画,近战是4 5画.但是你还是要mydex来查看自己的index,如果不是就不画 + var myIdx = sa.MyIndex(); + if (myIdx < 0 || myIdx > 5) return; + if (进水牢方式 == 进水牢.坦克进水牢 && myIdx > 1) return; + if (进水牢方式 == 进水牢.治疗进水牢 && (myIdx < 2 || myIdx > 3)) return; + if (进水牢方式 == 进水牢.近战进水牢 && (myIdx < 4 || myIdx > 5)) return; + + Vector3 wpos = new Vector3(100f, 0f, 100f); + var dpWp = sa.WaypointDp(wpos, 8000, 0, "进牢指路"); + sa.Method.SendDraw(DrawModeEnum.Imgui, DrawTypeEnum.Displacement, dpWp); + + } + + // 水牢画图指路-火 + [ScriptMethod( + name: "水牢画图指路-火", + eventType: EventTypeEnum.TargetIcon, + eventCondition: ["Id:regex:027C"] + )] + public void 水牢画图指路火(Event evt, ScriptAccessory sa) + { + var sourceId = evt.SourceId(); + // 查找BOSS炽红 DATABID对应的位置 + var bossObj = sa.Data.Objects.FirstOrDefault(o => o.DataId == 炽红); + if (bossObj == null) return; + var bossPos = new Vector3(bossObj.Position.X, 0f, bossObj.Position.Z); + // 画图 + var dp = sa.Data.GetDefaultDrawProperties(); + dp.Name = "水牢画图指路-火"; + dp.Position = bossPos; + dp.TargetObject = sourceId; + dp.Color = new Vector4(0.8863f, 0.5451f, 0.1569f, 1f); + dp.DestoryAt = 6000; + dp.Scale = new Vector2(8f, 50f); + dp.ScaleMode = ScaleMode.ByTime; + sa.Method.SendDraw(DrawModeEnum.Default, DrawTypeEnum.Rect, dp); + + + if (sourceId != sa.Data.Me) return; + if (水牢打法选择 == 水牢打法.MMW) + { + // 指路 一共有四个点 + var p_NE = new Vector3(113.10f, 0.00f, 113.21f); // 上半场 + 左半场 -> 去右上 + var p_NW = new Vector3(86.37f, 0.00f, 112.98f); // 上半场 + 右半场 -> 去左上 + var p_SW = new Vector3(86.90f, 0.00f, 86.44f); // 下半场 + 右半场 -> 去左下 + var p_SE = new Vector3(113.38f, 0.00f, 86.16f); // 下半场 + 左半场 -> 去右下 + + bool topHalf = bossPos.Z < 100f; // 你注释里写的:Z<100 上半场 + bool rightHalf = bossPos.X > 100f; // X>100 右半场(否则左半场) + + Vector3 wpos; + if (topHalf) + { + // 上半场 + wpos = rightHalf ? p_NW : p_NE; + } + else + { + // 下半场 + wpos = rightHalf ? p_SW : p_SE; + } + + // 发一个位移指路 + var dpWp = sa.WaypointDp(wpos, 6000, 0, "水牢指路-火"); + sa.Method.SendDraw(DrawModeEnum.Imgui, DrawTypeEnum.Displacement, dpWp); + } + else + { + // 无脑 + // 如果boss上半场,指路 (95.39f, -0.00f, 118.40f) + // 如果boss下半场,指路 {96.97, 0.00, 81.30} + if (_P3水牢次数 == 0) + { + Vector3 wpos = bossPos.Z < 100f + ? new Vector3(97.97f, 0.00f, 119.09f) + : new Vector3(96.97f, 0.00f, 80.73f); + var dpWp = sa.WaypointDp(wpos, 6000, 0, "水牢指路-火"); + sa.Method.SendDraw(DrawModeEnum.Imgui, DrawTypeEnum.Displacement, dpWp); + } + else + { + if (_P3水牢深蓝初始位置 == 1) + { + Vector3 wpos = bossPos.Z < 100f + ? new Vector3(97.97f, 0.00f, 119.09f) + : new Vector3(110.36f, 0.00f, 83.66f); + var dpWp = sa.WaypointDp(wpos, 6000, 0, "水牢指路-火"); + sa.Method.SendChat($"/e 深蓝初始位置1, 水牢>1. wpos:{wpos}"); + sa.Method.SendDraw(DrawModeEnum.Imgui, DrawTypeEnum.Displacement, dpWp); + } + else + { + Vector3 wpos = bossPos.Z < 100f + ? new Vector3(108.41f, 0.00f, 117.58f) + : new Vector3(96.97f, 0.00f, 80.73f); + sa.Method.SendChat($"/e 深蓝初始位置{_P3水牢深蓝初始位置}, 水牢>1. wpos:{wpos}"); + var dpWp = sa.WaypointDp(wpos, 6000, 0, "水牢指路-火"); + sa.Method.SendDraw(DrawModeEnum.Imgui, DrawTypeEnum.Displacement, dpWp); + } + } + + } + } + + // // 水牢画图指路-水 + [ScriptMethod( + name: "水牢画图指路-水", + eventType: EventTypeEnum.TargetIcon, + eventCondition: ["Id:regex:027B"] + )] + public void 水牢画图指路水(Event evt, ScriptAccessory sa) + { + var sourceId = evt.SourceId(); + + var bossObj = sa.Data.Objects.FirstOrDefault(o => o.DataId == 深蓝); + if (bossObj == null) return; + var bossPos = new Vector3(bossObj.Position.X, 0f, bossObj.Position.Z); + // 画图 + var dp = sa.Data.GetDefaultDrawProperties(); + dp.Name = "水牢画图指路-水"; + dp.Position = bossPos; + dp.TargetObject = sourceId; + dp.Color = new Vector4(0.1569f, 0.5451f, 0.8863f, 1f); + dp.DestoryAt = 6000; + dp.Scale = new Vector2(8f, 50f); + dp.ScaleMode = ScaleMode.ByTime; + sa.Method.SendDraw(DrawModeEnum.Default, DrawTypeEnum.Rect, dp); + + // 指路 + // 首先判断p3水牢方法 + Vector3 wpos = default; + if (水牢打法选择 == 水牢打法.MMW) + { + if (_P3水牢次数 == 0) + { + // 如果BossPos.Z > 100 BOSS下半场,_P3水牢深蓝初始位置 = 4 去 {95.67, 0.00, 81.59} + // 如果bossPos.Z < 100 上半场,_P3水牢深蓝初始位置 = 1 去 {104.04, 0.00, 119.04} + + if (bossPos.Z > 100f) + { + _P3水牢深蓝初始位置 = 4; + sa.Method.SendChat($"/e _P3水牢深蓝初始位置设为4"); + wpos = new Vector3(95.67f, 0.00f, 81.59f); + } + else + { + sa.Method.SendChat($"/e _P3水牢深蓝初始位置设为4"); + _P3水牢深蓝初始位置 = 1; + wpos = new Vector3(104.04f, 0.00f, 119.04f); + } + } + else + { + // sa.Method.SendChat($"/e >1次水牢"); + if (_P3水牢深蓝初始位置 == 4) + { + // 初始 4: + // bossPos.Z > 93 -> {118.13, 0.00, 105.24} + // bossPos.Z < 93 -> {95.67, 0.00, 81.59} + wpos = bossPos.Z < 93f + ? new Vector3(118.13f, 0.00f, 105.24f) + : new Vector3(95.67f, 0.00f, 81.59f); + } + else // 初始 1(默认按 1 处理) + { + // 初始 1: + // bossPos.X > 93 -> {81.76, 0.00, 94.95} + // bossPos.X < 93 -> {104.04, 0.00, 119.04} + wpos = bossPos.X > 93f + ? new Vector3(81.76f, 0.00f, 94.95f) + : new Vector3(104.04f, 0.00f, 119.04f); + } + } + } + else + { + // 无脑 + // 如果boss上半场,指路 {88.57, 0.00, 115.93} + // 如果boss下半场,指路 {88.79, -0.00, 84.46} + if (_P3水牢次数 == 0) + { + if (bossPos.Z > 100f) + { + _P3水牢深蓝初始位置 = 4; + sa.Method.SendChat($"/e _P3水牢深蓝初始位置设为4"); + wpos = new Vector3(95.67f, 0.00f, 81.59f); + } + else + { + sa.Method.SendChat($"/e _P3水牢深蓝初始位置设为4"); + _P3水牢深蓝初始位置 = 1; + wpos = new Vector3(104.04f, 0.00f, 119.04f); + } + } + + wpos = bossPos.Z < 100f + ? new Vector3(88.57f, 0.00f, 115.93f) + : new Vector3(88.79f, -0.00f, 84.46f); + + } + var wp = sa.WaypointDp(wpos, 6000, 0, $"水牢指路-水_{_P3水牢次数 + 1}"); + if (sourceId == sa.Data.Me) + { + sa.Method.SendDraw(DrawModeEnum.Imgui, DrawTypeEnum.Displacement, wp); + } + // sa.Method.SendChat($"/e P3水牢次数={_P3水牢次数} wpos={wpos.X:F2},{wpos.Z:F2} bossPos={bossPos.X:F2},{bossPos.Z:F2}"); + _P3水牢次数++; + } + + // P4 + // 浪顶炽火 + [ScriptMethod( + name: "浪顶炽火", + eventType: EventTypeEnum.StartCasting, + eventCondition: ["ActionId:46548"] + )] + public void 浪顶炽火(Event evt, ScriptAccessory sa) + { + var myIdx = sa.MyIndex(); + if (myIdx < 0 || myIdx > 7) return; + // 指路 + Vector3 wpos = myIdx switch + { + 0 or 2 or 4 or 6 => new Vector3(93.23f, 0.00f, 95.37f), + 1 or 3 or 5 or 7 => new Vector3(106.34f, -0.00f, 96.78f), + _ => Center, + }; + var dpWp = sa.WaypointDp(wpos, 5000, 0, "浪顶炽火指路"); + sa.Method.SendDraw(DrawModeEnum.Imgui, DrawTypeEnum.Displacement, dpWp); + } + + // 异常旋转巨火 + [ScriptMethod( + name: "异常旋转巨火", + eventType: EventTypeEnum.StartCasting, + eventCondition: ["ActionId:46486"] + )] + public void 异常旋转巨火(Event evt, ScriptAccessory sa) + { + var myIdx = sa.MyIndex(); + if (myIdx < 0 || myIdx > 7) return; + // 指路 + Vector3 wpos = myIdx switch + { + 0 or 6 => new Vector3(90.37f, 0.00f, 82.63f), + 1 or 7 => new Vector3(109.58f, 0.00f, 82.10f), + 2 or 4 => new Vector3(89.81f, 0.00f, 117.30f), + 3 or 5 => new Vector3(108.98f, 0.00f, 117.27f), + _ => Center, + }; + var dpWp = sa.WaypointDp(wpos, 5000, 0, "异常旋转巨火指路"); + sa.Method.SendDraw(DrawModeEnum.Imgui, DrawTypeEnum.Displacement, dpWp); + + if (_phase == 5) return; + _phase = 5; + sa.Method.SendChat("/e 马上进入P5阶段"); + } + + // 获得buff + [ScriptMethod( + name: "P5火水蛇形BUFF判定", + eventType: EventTypeEnum.StatusAdd, + eventCondition: ["StatusID:regex:^(4827|4828)$"], + userControl: false + )] + public void P5火水蛇形BUFF判定(Event evt, ScriptAccessory sa) + { + var targetId = evt.TargetId(); + if (targetId != sa.Data.Me) return; + + var statusId = evt["StatusID"]; + if (statusId == "4827") + { + _P5火水蛇 = P5火水蛇.火; + sa.Method.SendChat("/e 获得火蛇形BUFF"); + } + else if (statusId == "4828") + { + _P5火水蛇 = P5火水蛇.水; + sa.Method.SendChat("/e 获得水蛇形BUFF"); + } + } +} + +#region Helpers + +public static class EventExtensions +{ + private static bool ParseHexId(string? idStr, out uint id) + { + id = 0; + if (string.IsNullOrEmpty(idStr)) return false; + try + { + var idStr2 = idStr.Replace("0x", ""); + id = uint.Parse(idStr2, System.Globalization.NumberStyles.HexNumber); + return true; + } + catch + { + return false; + } + } + + public static uint ActionId(this Event evt) => JsonConvert.DeserializeObject(evt["ActionId"]); + public static uint SourceId(this Event evt) => ParseHexId(evt["SourceId"], out var id) ? id : 0; + public static uint TargetId(this Event evt) => ParseHexId(evt["TargetId"], out var id) ? id : 0; + public static Vector3 SourcePosition(this Event evt) => JsonConvert.DeserializeObject(evt["SourcePosition"]); + public static Vector3 EffectPosition(this Event evt) => JsonConvert.DeserializeObject(evt["EffectPosition"]); + public static uint DirectorId(this Event evt) => ParseHexId(evt["DirectorId"], out var id) ? id : 0; +} + +public static class ScriptAccessoryExtensions +{ + public static int MyIndex(this ScriptAccessory sa) => sa.Data.PartyList.IndexOf(sa.Data.Me); + + public static DrawPropertiesEdit FastDpWithPosition(this ScriptAccessory sa, string name, Vector3 pos, uint duration, Vector2 scale, bool safe = false) + { + var dp = sa.Data.GetDefaultDrawProperties(); + dp.Name = name; + dp.Color = safe ? sa.Data.DefaultSafeColor : sa.Data.DefaultDangerColor; + dp.Position = pos; + dp.DestoryAt = duration; + dp.Scale = scale; + return dp; + } + + public static DrawPropertiesEdit FastDpWithTarget(this ScriptAccessory sa, string name, uint targetId, uint duration, Vector2 scale, bool safe = false) + { + var dp = sa.Data.GetDefaultDrawProperties(); + dp.Name = name; + dp.Color = safe ? sa.Data.DefaultSafeColor : sa.Data.DefaultDangerColor; + dp.Owner = targetId; + dp.DestoryAt = duration; + dp.Scale = scale; + return dp; + } + + public static DrawPropertiesEdit WaypointDp(this ScriptAccessory sa, Vector3 pos, uint duration, uint delay = 0, string name = "Waypoint") + { + var dp = sa.Data.GetDefaultDrawProperties(); + dp.Name = name; + dp.Color = sa.Data.DefaultSafeColor; + dp.Owner = sa.Data.Me; + dp.TargetPosition = pos; + dp.DestoryAt = duration; + dp.Delay = delay; + dp.Scale = new Vector2(2); + dp.ScaleMode = ScaleMode.YByDistance; + return dp; + } + public static IEnumerable GetParty(this ScriptAccessory sa) + { + foreach (var pid in sa.Data.PartyList) + { + var obj = sa.Data.Objects.SearchByEntityId(pid); + if (obj is IPlayerCharacter character) yield return character; + } + } +} + +internal static class RayHelpers +{ + private static readonly Vector3 Center = new(100f, 0f, 100f); + private const uint 炽红 = 12345; + public static bool IsCoveredByRayXZ(Vector3 point, Vector3 bossPos, Vector2 dirXZ, float halfWidth) + { + var d2 = dirXZ.LengthSquared(); + if (d2 < 0.0001f) return false; + + var vx = point.X - bossPos.X; + var vz = point.Z - bossPos.Z; + + var t = (vx * dirXZ.X + vz * dirXZ.Y) / d2; + if (t < 0f) return false; + + var cross = dirXZ.X * vz - dirXZ.Y * vx; + var dist = MathF.Abs(cross) / MathF.Sqrt(d2); + + return dist <= halfWidth; + } + + public static Vector3 AdjustPointDownUntilNotCoveredXZ( + Vector3 p0, Vector3 bossPos, Vector2 dirXZ, + float halfWidth, float epsilon, float maxDown) + { + if (!IsCoveredByRayXZ(p0, bossPos, dirXZ, halfWidth)) + return p0; + + var dx = dirXZ.X; + var dz = dirXZ.Y; + var dLen = MathF.Sqrt(dx * dx + dz * dz); + if (dLen < 0.0001f) return p0; + + if (MathF.Abs(dx) < 1e-4f) + { + var z = p0.Z; + var minZ = p0.Z - maxDown; + while (z > minZ) + { + z -= 0.1f; + var p = new Vector3(p0.X, p0.Y, z); + if (!IsCoveredByRayXZ(p, bossPos, dirXZ, halfWidth)) + return p; + } + return new Vector3(p0.X, p0.Y, minZ); + } + + var x = p0.X; + var bz = bossPos.Z; + var bx = bossPos.X; + + var C = -dx * bz - dz * (x - bx); + var K = halfWidth * dLen; + + var z1 = ( K - C) / dx; + var z2 = (-K - C) / dx; + + var low = MathF.Min(z1, z2); + var high = MathF.Max(z1, z2); + + var z0 = p0.Z; + if (z0 >= low && z0 <= high) + { + var zNew = low - epsilon; + var pTry = new Vector3(x, p0.Y, zNew); + if (!IsCoveredByRayXZ(pTry, bossPos, dirXZ, halfWidth)) + return pTry; + + zNew -= 0.2f; + return new Vector3(x, p0.Y, zNew); + } + + var zFallback = z0; + var minZFb = z0 - maxDown; + while (zFallback > minZFb) + { + zFallback -= 0.1f; + var p = new Vector3(x, p0.Y, zFallback); + if (!IsCoveredByRayXZ(p, bossPos, dirXZ, halfWidth)) + return p; + } + return new Vector3(x, p0.Y, minZFb); + } + + public static bool TryGetBossBasis( + ScriptAccessory sa, + uint bossDataId, + Vector3 center, + out Vector3 bossPos, + out Vector2 forwardXZ, + out Vector2 rightXZ) + { + bossPos = default; + forwardXZ = default; + rightXZ = default; + + var bossObj = sa.Data.Objects.FirstOrDefault(o => o.DataId == bossDataId); + if (bossObj == null) return false; + + bossPos = bossObj.Position; + + // forward = 指向中心(面向场内) + var f = new Vector2(center.X - bossPos.X, center.Z - bossPos.Z); + if (f.LengthSquared() < 0.0001f) return false; + f = Vector2.Normalize(f); + + // right = forward 的右侧(确保和你给的例子匹配) + var r = new Vector2(-f.Y, f.X); + + forwardXZ = f; + rightXZ = r; + return true; + } + + public static Vector3 BossRelativePoint( + Vector3 bossPos, + Vector2 forwardXZ, + Vector2 rightXZ, + float rightDist, + float upDist) + { + var x = bossPos.X + rightXZ.X * rightDist + forwardXZ.X * upDist; + var z = bossPos.Z + rightXZ.Y * rightDist + forwardXZ.Y * upDist; + return new Vector3(x, 0f, z); + } + + public static Vector3 AdjustPointAlongDirUntilNotCoveredXZ( + Vector3 p0, + Vector3 rayBossPos, + Vector2 rayDirXZ, + Vector2 moveDirXZ, // IMPORTANT: normalized direction you want to move along + float halfWidth, + float epsilon, + float maxMove) + { + if (!IsCoveredByRayXZ(p0, rayBossPos, rayDirXZ, halfWidth)) + return p0; + + var r2 = rayDirXZ.LengthSquared(); + if (r2 < 1e-6f) return p0; + + if (moveDirXZ.LengthSquared() < 1e-6f) return p0; + var m = Vector2.Normalize(moveDirXZ); + + // v0 = p0 - boss + var v0 = new Vector2(p0.X - rayBossPos.X, p0.Z - rayBossPos.Z); + + // cross(r, v) = rx*vz - rz*vx + float Cross(Vector2 a, Vector2 b) => a.X * b.Y - a.Y * b.X; + + var r = rayDirXZ; + var cross0 = Cross(r, v0); + var rLen = MathF.Sqrt(r2); + var K = halfWidth * rLen; + + // If moving direction doesn't change cross, stepping won't escape by distance -> fallback stepping + var crossRM = Cross(r, m); + + // Also consider escaping by t<0 (behind boss on the ray direction) + var dot0 = Vector2.Dot(v0, r); + var dotMR = Vector2.Dot(m, r); + + float bestS = float.PositiveInfinity; + + // Candidate 1: escape by distance boundary |cross| > K + if (MathF.Abs(crossRM) > 1e-6f) + { + // Solve cross(s) = ±K, where cross(s) = cross0 + crossRM * s + var sA = ( K - cross0) / crossRM; + var sB = (-K - cross0) / crossRM; + + void TryPick(float s) + { + if (s < 0) return; + var s2 = s + epsilon; // step slightly past boundary + if (s2 > maxMove) return; + + var px = p0.X + m.X * s2; + var pz = p0.Z + m.Y * s2; + var pTry = new Vector3(px, p0.Y, pz); + if (!IsCoveredByRayXZ(pTry, rayBossPos, rayDirXZ, halfWidth)) + bestS = MathF.Min(bestS, s2); + } + + TryPick(sA); + TryPick(sB); + } + + // Candidate 2: escape by t<0 (move behind ray origin so t<0 => not covered) + // t(s) = dot(v0 + m*s, r) / |r|^2 + // Need dot(v0 + m*s, r) < 0 => dot0 + dotMR*s < 0 + if (dot0 >= 0f && dotMR < -1e-6f) + { + var sT = (-dot0 / dotMR) + epsilon; + if (sT >= 0f && sT <= maxMove) + { + var px = p0.X + m.X * sT; + var pz = p0.Z + m.Y * sT; + var pTry = new Vector3(px, p0.Y, pz); + if (!IsCoveredByRayXZ(pTry, rayBossPos, rayDirXZ, halfWidth)) + bestS = MathF.Min(bestS, sT); + } + } + + // If analytic fails, do small-step fallback + if (float.IsInfinity(bestS)) + { + float step = 0.1f; + for (float s = step; s <= maxMove; s += step) + { + var px = p0.X + m.X * s; + var pz = p0.Z + m.Y * s; + var pTry = new Vector3(px, p0.Y, pz); + if (!IsCoveredByRayXZ(pTry, rayBossPos, rayDirXZ, halfWidth)) + return pTry; + } + // worst-case: clamp to maxMove + return new Vector3(p0.X + m.X * maxMove, p0.Y, p0.Z + m.Y * maxMove); + } + + return new Vector3(p0.X + m.X * bestS, p0.Y, p0.Z + m.Y * bestS); + } + + public static Vector3 ClampXZ(Vector3 p, float min, float max) + { + var x = MathF.Max(min, MathF.Min(max, p.X)); + var z = MathF.Max(min, MathF.Min(max, p.Z)); + return new Vector3(x, 0f, z); + } + + public static bool IsInsideRotRectXZ(Vector3 p, Vector3 rectCenter, float yaw, float width, float length) + { + // width/length 这里按“全宽/全长”理解,所以半宽半长要 /2 + float hx = width * 0.5f; + float hz = length * 0.5f; + + // forward / right in XZ + var f = new Vector2(MathF.Sin(yaw), MathF.Cos(yaw)); // forward + var r = new Vector2(-f.Y, f.X); // right + + var v = new Vector2(p.X - rectCenter.X, p.Z - rectCenter.Z); + float localX = v.X * r.X + v.Y * r.Y; // right axis + float localZ = v.X * f.X + v.Y * f.Y; // forward axis + + return MathF.Abs(localX) <= hx && MathF.Abs(localZ) <= hz; + } + + /// + /// 根据危险区(旋转矩形)判断三条车道:左(-lane), 中(0), 右(+lane) 哪些被覆盖。 + /// 返回给偶数组(0/2/4/6)与奇数组(1/3/5/7)的车道偏移值(沿 objRight 方向)。 + /// 优先级:左 -> 中 -> 右,从没覆盖的里面选两条不同的。 + /// + public static void PickSafeLanesByDangerRect( + Vector3 objPos, + Vector3 objRight, + Vector3 objForward, + Vector3 dangerCenter, + float dangerYaw, + float dangerWidth, + float dangerLength, + float laneOffset, // 13.86 + IReadOnlyList fbSamples, // {+8,-2,-12,-22} + out float evenLane, + out float oddLane) + { + // lane candidates in priority order: left, center, right + float[] lanes = { +laneOffset, 0f, -laneOffset }; + + bool LaneCovered(float lane) + { + foreach (var fb in fbSamples) + { + var p = objPos + objRight * lane + objForward * fb; + p = new Vector3(p.X, 0f, p.Z); + if (IsInsideRotRectXZ(p, dangerCenter, dangerYaw, dangerWidth, dangerLength)) + return true; + } + return false; + } + + var safe = lanes.Where(l => !LaneCovered(l)).ToList(); + if (safe.Count == 0) safe.AddRange(lanes); // 兜底:全覆盖/算法异常时至少不崩 + + evenLane = safe[0]; + oddLane = (safe.Count >= 2) ? safe[1] : safe[0]; + } + +} + +internal static class EnvHelpers +{ + public enum GridMechanic + { + None = 0, + Tankbuster = 1, + Stack = 2, + Spread = 3, + } + + public static bool TryGetGridIndexFlag(Event evt, out int index, out int flag) + { + index = 0; + flag = 0; + + // 你原来的范围是 14~22(方法名写 15-21 但逻辑是 14-22,保持原样) + if (!int.TryParse(evt["Index"], out index)) return false; + if (index < 14 || index > 22) return false; + + if (!int.TryParse(evt["Flag"], out flag)) return false; + return true; + } + + public static bool TryGetGridPos(int index, out Vector3 pos) + { + // 14~22 = 3x3 grid: + // 14 15 16 -> z=87, x=87,100,113 + // 17 18 19 -> z=100, x=87,100,113 + // 20 21 22 -> z=113, x=87,100,113 + pos = Vector3.Zero; + if (index < 14 || index > 22) return false; + + int i = index - 14; + int row = i / 3; // 0..2 + int col = i % 3; // 0..2 + + float x = 87f + 13f * col; + float z = 87f + 13f * row; + pos = new Vector3(x, 0f, z); + return true; + } + + public static bool IsClearFlag(int flag) => flag == 4 || flag == 8; + + public static bool IsWater(int flag) => flag == 2 || flag == 32 || flag == 128; + + public static GridMechanic GetMechanic(int flag) + { + // 保持原判定集合完全一致 + if (flag == 128 || flag == 8192) return GridMechanic.Tankbuster; + if (flag == 32 || flag == 2048) return GridMechanic.Stack; + if (flag == 2 || flag == 512) return GridMechanic.Spread; + return GridMechanic.None; + } + + public static void ClearGridDraw(ScriptAccessory sa, string baseName) + { + sa.Method.RemoveDraw($"{baseName}_TB"); + sa.Method.RemoveDraw($"{baseName}_Stack"); + for (int i = 1; i <= 4; i++) + sa.Method.RemoveDraw($"{baseName}_Spread_{i}"); + } + + public static void SetupCommon(DrawPropertiesEdit dp, string name, Vector3 pos, Vector4 color) + { + dp.Name = name; + dp.Position = pos; + dp.Color = color; + dp.DestoryAt = 9999999; + dp.ScaleMode = ScaleMode.None; + } + + public static void DrawTB(ScriptAccessory sa, string baseName, Vector3 pos, Vector4 color) + { + var dp = sa.Data.GetDefaultDrawProperties(); + SetupCommon(dp, $"{baseName}_TB", pos, color); + + dp.Scale = new Vector2(6f); + dp.CentreResolvePattern = PositionResolvePatternEnum.PlayerNearestOrder; + dp.CentreOrderIndex = 1; + + sa.Method.SendDraw(DrawModeEnum.Default, DrawTypeEnum.Circle, dp); + } + + public static void DrawStack(ScriptAccessory sa, string baseName, Vector3 pos, Vector4 color) + { + var dp = sa.Data.GetDefaultDrawProperties(); + SetupCommon(dp, $"{baseName}_Stack", pos, color); + + dp.Scale = new Vector2(60f); + dp.Radian = float.Pi / 4; + dp.TargetResolvePattern = PositionResolvePatternEnum.PlayerNearestOrder; + dp.TargetOrderIndex = 1; + + sa.Method.SendDraw(DrawModeEnum.Default, DrawTypeEnum.Fan, dp); + } + + public static void DrawSpread(ScriptAccessory sa, string baseName, Vector3 pos, Vector4 color) + { + for (uint i = 1; i <= 4; i++) + { + var dp = sa.Data.GetDefaultDrawProperties(); + SetupCommon(dp, $"{baseName}_Spread_{i}", pos, color); + + dp.Scale = new Vector2(60f); + dp.Radian = float.Pi / 4; + dp.TargetResolvePattern = PositionResolvePatternEnum.PlayerNearestOrder; + dp.TargetOrderIndex = i; + + sa.Method.SendDraw(DrawModeEnum.Default, DrawTypeEnum.Fan, dp); + } + } +} + + +#endregion diff --git a/Codaaaaaa/M11S.cs b/Codaaaaaa/M11S.cs new file mode 100644 index 0000000..c9da02c --- /dev/null +++ b/Codaaaaaa/M11S.cs @@ -0,0 +1,1682 @@ +using System; +using System.Linq; +using System.Numerics; +using System.Threading; +using System.Collections.Generic; +using System.Threading.Tasks; +using Newtonsoft.Json; +using KodakkuAssist.Script; +using KodakkuAssist.Module.GameEvent; +using KodakkuAssist.Module.Draw; +using KodakkuAssist.Module.Draw.Manager; +using KodakkuAssist.Data; +using KodakkuAssist.Extensions; +using System.Runtime.CompilerServices; +using System.Security.Cryptography.X509Certificates; +using System.Reflection; + +namespace Codaaaaaa.M11S; + +[ScriptType( + guid: "6f3d1b82-9d44-4c5a-8277-3a8f5c0f2b1e", + name: "M11S补充画图", + territorys: [1325], + version: "0.0.1.3", + author: "Codaaaaaa", + note: "设置里面改打法,但目前支持的不是很多有很大概率被电。\n- 该脚本只对RyougiMio佬的画图更新前做指路补充,需要配合使用。\n- 谢谢灵视佬和7dsa1wd1s佬提供的arr")] +public class M11S +{ + #region 用户设置 + // [UserSetting("是否开启TTS")] public static bool TTSOpen { get; set; } = true; + [UserSetting("铸兵之令统治打法")] public 铸兵之令统治打法 铸兵之令统治打法选择 { get; set; } = 铸兵之令统治打法.近战固定法; + [UserSetting("王者陨石L改踩塔打法")] public 王者陨石踩塔打法 王者陨石踩塔打法选择 { get; set; } = 王者陨石踩塔打法.tndd; + [UserSetting("王者陨石L改踩塔击飞打法")] public 王者陨石击飞打法 王者陨石踩塔击飞打法选择 { get; set; } = 王者陨石击飞打法.同平台; + [UserSetting("陨石狂奔打法")] public 陨石狂奔打法 陨石狂奔打法选择 { get; set; } = 陨石狂奔打法.十引导; + // [UserSetting("流星雨打法")] public 流星雨打法 流星雨打法选择 { get; set; } = 流星雨打法.奶远近; + [UserSetting("六连风圈高亮颜色")] public ScriptColor 六连风圈高亮颜色 { get; set; } = new ScriptColor() { V4 = new Vector4(0.9803922f, 0.4313726f, 0.1960784f, 0.35f) }; // 默认淡蓝 + + #endregion + + public enum 流星雨打法 + { + 奶远近 + } + + public enum 铸兵之令统治打法 + { + 近战固定法, + } + public enum 王者陨石踩塔打法 + { + 近近远远, + tndd + } + public enum 王者陨石击飞打法 + { + 同平台, + 闲人斜飞 + } + public enum 陨石狂奔打法 + { + 十引导, + X引导, + } + + private enum Corner + { + 左上, + 右上, + 左下, + 右下, + 未设定 + } + + + private static readonly Vector3[] DomPoints = new[] + { + new Vector3(85f, 0f, 100f), // 点0 + new Vector3(115f, 0f, 100f), // 点1 + new Vector3(100f, 0f, 115f), // 点2 + new Vector3(100f, 0f, 85f), // 点3 + }; + + private static readonly Vector3[][] SafeByMissing近固 = new[] + { + // missing = 0 (DomPoints[0] 没出现) + new[] + { + new Vector3(100.14f, 0f, 91.23f), // group0: myIdx 0/4 + new Vector3(99.89f, 0f, 108.96f), // group1: myIdx 1/5 + new Vector3(85.21f, 0f, 94.08f), // group2: myIdx 2/6 + new Vector3(85.49f, 0f, 106.01f), // group3: myIdx 3/7 + }, + + // missing = 1 (DomPoints[1] 没出现) + new[] + { + new Vector3(100.14f, 0f, 91.23f), // group0: myIdx 0/4 + new Vector3(99.89f, 0f, 108.96f), // group1: myIdx 1/5 + new Vector3(114.51f, 0f, 106.01f), // group2: myIdx 2/6 + new Vector3(114.51f, 0f, 94.08f), // group3: myIdx 3/7 + }, + + // missing = 2 (DomPoints[2] 没出现) + new[] + { + new Vector3(91.25f, 0f, 100.08f), + new Vector3(108.63f, 0f, 99.68f), + new Vector3(94.15f, 0f, 114.20f), + new Vector3(105.70f, 0f, 114.08f), + }, + + // missing = 3 (DomPoints[3] 没出现) + new[] + { + new Vector3(91.25f, 0f, 100.08f), + new Vector3(108.63f, 0f, 99.68f), + new Vector3(105.70f, 0f, 85.92f), + new Vector3(94.15f, 0f, 85.92f), + }, + }; + + private static readonly IReadOnlyDictionary 王者陨石塔位置 + = new Dictionary + { + [Corner.左上] = new[] + { + new Vector3(86.10f, 0f, 88.73f), // 拉线踩塔 + new Vector3(83.94f, 0f, 91.43f), // 闲人踩塔同平台 + new Vector3(86.94f, 0f, 90.25f), // 闲人踩塔斜飞 + }, + [Corner.右上] = new[] + { + new Vector3(113.05f, 0f, 88.90f), + new Vector3(116.02f, 0f, 91.73f), + new Vector3(113.02f, 0f, 90.22f), + }, + [Corner.左下] = new[] + { + new Vector3(86.74f, 0f, 110.78f), + new Vector3(84.40f, 0f, 108.26f), + new Vector3(86.94f, 0f, 109.79f), + }, + [Corner.右下] = new[] + { + new Vector3(113.40f, 0f, 111.06f), + new Vector3(116.06f, 0f, 108.47f), + new Vector3(113.05f, 0f, 109.78f), + }, + }; + private static readonly IReadOnlyDictionary 王者陨石火圈引导美 + = new Dictionary + { + [Corner.左上] = new[] + { + new Vector3(92.86f, 0f, 97.76f), + new Vector3(92.47f, 0f, 91.70f), + new Vector3(92.41f, 0f, 85.55f), + }, + [Corner.右上] = new[] + { + new Vector3(107.14f, 0f, 97.76f), + new Vector3(107.53f, 0f, 91.70f), + new Vector3(107.59f, 0f, 85.55f), + }, + [Corner.左下] = new[] + { + new Vector3(92.86f, 0f, 102.24f), + new Vector3(92.47f, 0f, 108.30f), + new Vector3(92.41f, 0f, 114.45f), + }, + [Corner.右下] = new[] + { + new Vector3(107.14f, 0f, 102.24f), + new Vector3(107.53f, 0f, 108.30f), + new Vector3(107.59f, 0f, 114.45f), + }, + }; + + // 匹配固定点的容差(只看 XZ) + private const float DomPointEps = 1.2f; + + // 收集机制触发的状态(线程安全) + private readonly object _domLock = new(); + private int _domMask = 0; // 4bit:出现过哪些点 + private int _domCount = 0; // 已收到几次(目标 6 次) + private int _domSeq = 0; // 每轮机制序号(防止上一轮 Task 影响下一轮) + private long _domLastTick = 0; + + // 王者陨石拉线记录 + private bool 王者陨石是否有拉线Buff = false; + private Vector3 王者陨石陨石Pos = Vector3.Zero; + private Corner 王者陨石下一次Corner = Corner.未设定; + + private readonly object _meteorLock = new(); + private CancellationTokenSource _meteorCts = new(); + private int _meteorSeq = 0; + + + // 换P + private int _phase = 1; + + // 陨石狂奔:拉线只触发一次 + private bool _runMeteorTetherTriggered = false; + private readonly object _runMeteorLock = new(); + private readonly List _runMeteorFireTargets = new(); // 每轮 2 个火圈 TargetId + private readonly List _runMeteorPositions = new(); // 每轮 2 个陨石位置 + + private const float RunMeteorPosEps = 0.35f; + + [ScriptMethod(name: "Set Phase 1", eventType: EventTypeEnum.Chat, eventCondition: ["Type:Echo", "Message:KASP1"], userControl: false)] + public void SetP1(Event evt, ScriptAccessory sa) => _phase = 1; + + [ScriptMethod(name: "Set Phase 2", eventType: EventTypeEnum.Chat, eventCondition: ["Type:Echo", "Message:KASP2"], userControl: false)] + public void SetP2(Event evt, ScriptAccessory sa) => _phase = 2; + + #region StarTrack + + // ==================== [Star Track] Config ==================== + private const int StarTrackActionId = 46131; + private bool _stGuideIssued = false; // 本轮是否已经提前出过指路 + + private const int TotalCasts = 8; // 8 次炮线 + private const int PairCount = 4; // 两两一组 -> 4 个交点(标 1..4) + + // 4x4 网格映射 + private const float GridMinXZ = 80f; // NW 角最小 X/Z + private const float CellSize = 10f; // 每格 10 + + // 去重参数 + private const int DuplicateWindowMs = 300; + private const float DuplicatePosTolerance = 0.2f; + private const float DuplicateRotTolerance = 0.05f; + + private static readonly Vector3 PtSW = new(98.52f, 0f, 100.68f); // 左下 + private static readonly Vector3 PtNW = new(98.54f, 0f, 99.25f); // 左上 + private static readonly Vector3 PtNE = new(101.55f, 0f, 99.35f); // 右上 + private static readonly Vector3 PtSE = new(101.28f, 0f, 100.61f); // 右下 + + // 半透明黄色 + private static readonly Vector4 Yellow50 = new(1f, 1f, 0f, 0.5f); + + // ==================== [Star Track] State ==================== + private readonly object _stLock = new(); + + private readonly record struct StarCast(long TimeMs, uint SourceId, Vector3 Origin, float Rot); + private readonly record struct DedupInfo(long TimeMs, Vector3 Pos, float Rot); + + private int _stCastSeq = 0; // 1..8 + private readonly List _stCasts = new(TotalCasts); + private readonly List _stIntersections = new(PairCount); + private readonly int[,] _stGrid = new int[4, 4]; // 4x4 结果网格(0/1..4) + private readonly Dictionary _stLastBySource = new(); + + private readonly int[,] _stLastSnapshot = new int[4, 4]; + + // ==================== [Beastflame/Tail] Gate ==================== + // 兽焰连尾击:记录面向,用来 gate 星轨链指路 + private readonly object _beastLock = new(); + private bool _beastFacingAC = true; + private float _beastLastRot = 0f; + private long _beastLastMs = 0; + + // 容忍角度:0.35rad ~ 20° + private const float BeastFacingTol = 0.35f; + + // 仅在最近 N ms 内记录有效(避免上一轮的“坏面向”影响下一轮) + private const int BeastFacingValidMs = 15000; + + // 默认:C = +Z (rot ~ 0),A = -Z (rot ~ PI) + private const float AngleC = 0f; + private const float AngleA = MathF.PI; + private bool BeastflameAllowsStarTrack(long nowMs) + { + lock (_beastLock) + { + if (_beastLastMs == 0) return true; // 没记录到就不拦 + if (nowMs - _beastLastMs > BeastFacingValidMs) return true; // 记录过旧就不拦 + return _beastFacingAC; + } + } + + private static bool IsFacingAngle(float rot, float target, float tol) + => MathF.Abs(NormalizeAngle(rot - target)) <= tol; + + // ==================== [Star Track] Utils ==================== + private static float NormalizeAngle(float a) + { + float twoPi = MathF.PI * 2f; + while (a > MathF.PI) a -= twoPi; + while (a < -MathF.PI) a += twoPi; + return a; + } + + // FFXIV 常见旋转:rot=0 朝 +Z + private static Vector2 DirXZ(float rot) => new(MathF.Sin(rot), MathF.Cos(rot)); + private static float Cross(Vector2 a, Vector2 b) => a.X * b.Y - a.Y * b.X; + + private static bool TryIntersectXZ(Vector3 o1, float r1, Vector3 o2, float r2, out Vector3 hit) + { + Vector2 p1 = new(o1.X, o1.Z); + Vector2 p2 = new(o2.X, o2.Z); + Vector2 d1 = DirXZ(r1); + Vector2 d2 = DirXZ(r2); + + float denom = Cross(d1, d2); + if (MathF.Abs(denom) < 1e-4f) + { + hit = default; + return false; + } + + Vector2 diff = p2 - p1; + float t = Cross(diff, d2) / denom; + + Vector2 ip = p1 + d1 * t; + hit = new Vector3(ip.X, 0f, ip.Y); + return true; + } + + // World(XZ) -> Grid(row,col); row=0 是“上/北”(Z小), col=0 是“左/西”(X小) + private static (int row, int col) WorldToGrid(Vector3 p) + { + int col = (int)MathF.Floor((p.X - GridMinXZ) / CellSize); + int row = (int)MathF.Floor((p.Z - GridMinXZ) / CellSize); + col = Math.Clamp(col, 0, 3); + row = Math.Clamp(row, 0, 3); + return (row, col); + } + + private static void ClearGrid(int[,] grid) + { + for (int r = 0; r < 4; r++) + for (int c = 0; c < 4; c++) + grid[r, c] = 0; + } + + private static bool TryFindValue(int[,] grid, int value, out int row, out int col) + { + for (int r = 0; r < 4; r++) + for (int c = 0; c < 4; c++) + { + if (grid[r, c] == value) + { + row = r; + col = c; + return true; + } + } + row = col = -1; + return false; + } + + private enum STCorner { NW, NE, SW, SE } + + private static STCorner Opposite(STCorner c) => c switch + { + STCorner.NW => STCorner.SE, + STCorner.NE => STCorner.SW, + STCorner.SW => STCorner.NE, + STCorner.SE => STCorner.NW, + _ => STCorner.NW + }; + + private static Vector3 CornerPoint(STCorner c) => c switch + { + STCorner.NW => PtNW, + STCorner.NE => PtNE, + STCorner.SW => PtSW, + STCorner.SE => PtSE, + _ => PtNW + }; + + // 2 在中心 2x2 的哪个格 -> 对应哪个角 + // [1][1]=左上,[1][2]=右上,[2][1]=左下,[2][2]=右下 + private static STCorner? Inner2x2ToCorner(int row, int col) + { + if (row == 1 && col == 1) return STCorner.NW; + if (row == 1 && col == 2) return STCorner.NE; + if (row == 2 && col == 1) return STCorner.SW; + if (row == 2 && col == 2) return STCorner.SE; + return null; + } + + private static bool IsCornerCell(int r, int c) + => (r == 0 || r == 3) && (c == 0 || c == 3); + + private static bool IsEdgeCell(int r, int c) + => (r == 0 || r == 3 || c == 0 || c == 3) && !IsCornerCell(r, c); + + // ==================== [Star Track] Reset & Snapshot ==================== + private void ResetStarTrackState() + { + lock (_stLock) + { + _stCastSeq = 0; + _stCasts.Clear(); + _stIntersections.Clear(); + _stLastBySource.Clear(); + ClearGrid(_stGrid); + _stGuideIssued = false; + } + } + + private void SnapshotGrid() + { + lock (_stLock) + { + for (int r = 0; r < 4; r++) + for (int c = 0; c < 4; c++) + _stLastSnapshot[r, c] = _stGrid[r, c]; + } + } + + // ==================== [Star Track] Draw wrappers(全用 Displacement) ==================== + private void DrawWpToMe(ScriptAccessory sa, string name, Vector3 target, Vector4 color, int delayMs, int durMs) + { + var dp = sa.WaypointDp(target, (uint)durMs, (uint)delayMs, $"{name}_{Environment.TickCount64}", color); + sa.Method.SendDraw(DrawModeEnum.Imgui, DrawTypeEnum.Displacement, dp); + } + + private void DrawWpPosToPos(ScriptAccessory sa, string name, Vector3 from, Vector3 to, Vector4 color, int delayMs, int durMs) + { + var dp = sa.WaypointFromToDp(from, to, (uint)durMs, (uint)delayMs, $"{name}_{Environment.TickCount64}", color); + sa.Method.SendDraw(DrawModeEnum.Imgui, DrawTypeEnum.Displacement, dp); + } + + // ==================== [Star Track] Main ==================== + [ScriptMethod(name: "星轨链近战无损指路", eventType: EventTypeEnum.StartCasting, eventCondition: ["ActionId:46131"])] + public void OnStarTrackCast(Event evt, ScriptAccessory sa) + { + long nowMs = Environment.TickCount64; + + // ========= Gate:兽焰连尾击不面向 A/C 则禁用星轨链本轮指路 ========= + if (!BeastflameAllowsStarTrack(nowMs)) + { + // 清空星轨链状态,防止残留数据导致后续误触发 + lock (_stLock) + { + _stCastSeq = 0; + _stCasts.Clear(); + _stIntersections.Clear(); + _stLastBySource.Clear(); + ClearGrid(_stGrid); + } + return; + } + + uint sourceId = evt.SourceId(); + Vector3 pos = evt.SourcePosition(); + float rot = evt.SourceRotation(); // 下面我在 EventExtensions 里补了这个方法 + + // 颜色:正常用 DefaultSafeColor + var normal = sa.Data.DefaultSafeColor; + + lock (_stLock) + { + // ---- 去重:同一 SourceId,短时间 + 位置/角度几乎一致 => 重复 + if (_stLastBySource.TryGetValue(sourceId, out var last)) + { + bool within = (nowMs - last.TimeMs) <= DuplicateWindowMs; + bool samePos = Vector3.Distance(last.Pos, pos) <= DuplicatePosTolerance; + bool sameRot = MathF.Abs(NormalizeAngle(rot - last.Rot)) <= DuplicateRotTolerance; + if (within && samePos && sameRot) return; + } + _stLastBySource[sourceId] = new DedupInfo(nowMs, pos, rot); + + // ---- 收集 cast + _stCastSeq++; + _stCasts.Add(new StarCast(nowMs, sourceId, pos, rot)); + + // ---- 每两条算一组交点(标号 1..4) + if (_stCastSeq % 2 == 0 && _stCasts.Count >= 2 && _stIntersections.Count < PairCount) + { + var a = _stCasts[_stCasts.Count - 2]; + var b = _stCasts[_stCasts.Count - 1]; + + if (TryIntersectXZ(a.Origin, a.Rot, b.Origin, b.Rot, out var hit)) + { + _stIntersections.Add(hit); + int idx = _stIntersections.Count; // 1..4 + var (row, col) = WorldToGrid(hit); + _stGrid[row, col] = idx; + + if (!_stGuideIssued && _stIntersections.Count >= 2) + { + _stGuideIssued = true; + + var gridCopyEarly = new int[4, 4]; + for (int rr = 0; rr < 4; rr++) + for (int cc = 0; cc < 4; cc++) + gridCopyEarly[rr, cc] = _stGrid[rr, cc]; + + sa.Method.SendChat("/e 记录两个 指路"); + Task.Run(() => ResolveStarTrackGuide(sa, gridCopyEarly, normal, 3000)); + } + } + } + + // ---- 8 次齐了:做指路逻辑 + snapshot + reset + if (_stCastSeq >= TotalCasts) + { + int[,]? gridCopyEnd = null; + + if (!_stGuideIssued) + { + gridCopyEnd = new int[4, 4]; + for (int r = 0; r < 4; r++) + for (int c = 0; c < 4; c++) + gridCopyEnd[r, c] = _stGrid[r, c]; + } + + SnapshotGrid(); + ResetStarTrackState(); + + if (gridCopyEnd != null) + { + sa.Method.SendChat("/e 保底 指路"); + Task.Run(() => ResolveStarTrackGuide(sa, gridCopyEnd, normal)); + } + } + } + } + + // ==================== [Star Track] 3 Cases ==================== + private void ResolveStarTrackGuide(ScriptAccessory sa, int[,] grid, Vector4 normalColor, int delay = 0) + { + if (!TryFindValue(grid, 1, out int r1, out int c1)) return; + if (!TryFindValue(grid, 2, out int r2, out int c2)) return; + + // -------------------- + // Case 1: 1 在 [2][1] 或 [1][2] + // -------------------- + if ((r1 == 2 && c1 == 1) || (r1 == 1 && c1 == 2)) + { + // 1在[2][1] -> 起跑点=右上(NE) -> 黄线 NE->SW(4s) -> 4s后指路到SW + // 1在[1][2] -> 起跑点=左下(SW) -> 黄线 SW->NE(4s) -> 4s后指路到NE + STCorner startCorner = (r1 == 2 && c1 == 1) ? STCorner.NE : STCorner.SW; + STCorner endCorner = Opposite(startCorner); + + Vector3 start = CornerPoint(startCorner); + Vector3 end = CornerPoint(endCorner); + + DrawWpToMe(sa, "ST_C1_Start", start, normalColor, 0, 1500+delay); + DrawWpPosToPos(sa, "ST_C1_Preview", start, end, Yellow50, 0, 1500+delay); + DrawWpToMe(sa, "ST_C1_End", end, normalColor, 1500+delay, 1500); + return; + } + + // -------------------- + // Case 2: 1 在四角 + // -------------------- + if (IsCornerCell(r1, c1)) + { + var targetOpt = Inner2x2ToCorner(r2, c2); + if (!targetOpt.HasValue) return; + + STCorner target = targetOpt.Value; // 2 对应的“目标角” + STCorner start = Opposite(target); // 起跑点=目标角对角 + + Vector3 startPos = CornerPoint(start); + Vector3 targetPos = CornerPoint(target); + + DrawWpToMe(sa, "ST_C2_Start", startPos, normalColor, 0, 3000+delay); + DrawWpPosToPos(sa, "ST_C2_Preview", startPos, targetPos, Yellow50, 0, 3000+delay); + DrawWpToMe(sa, "ST_C2_Target", targetPos, normalColor, 3000+delay, 1500); + return; + } + + // -------------------- + // Case 3: 1 在四边(非四角) + // -------------------- + if (IsEdgeCell(r1, c1)) + { + // 0-4s 指路去起跑点 + // 0-4s 起跑->对角 黄线预告 + // 4-12s 对角->起跑 黄线预告(持续8s) + // 4-8s 4s后指路去对角 + // 8-12s 再4s后指路回起跑 + var startCornerOpt = Inner2x2ToCorner(r2, c2); + if (startCornerOpt == null) return; + + STCorner startCorner = startCornerOpt.Value; + STCorner endCorner = Opposite(startCorner); + + Vector3 start = CornerPoint(startCorner); + Vector3 end = CornerPoint(endCorner); + + DrawWpToMe(sa, "ST_C3_Start", start, normalColor, 0, 1500+delay); + + DrawWpPosToPos(sa, "ST_C3_Preview_1", start, end, Yellow50, 0, 1500+delay); + DrawWpPosToPos(sa, "ST_C3_Preview_2", end, start, Yellow50, 1500+delay, 1500); + + DrawWpToMe(sa, "ST_C3_GoEnd", end, normalColor, 1500+delay, 1500); + DrawWpToMe(sa, "ST_C3_GoBack", start, normalColor, 3000+delay, 1500); + return; + } + } + + #endregion + + [ScriptMethod( + name: "兽焰连尾击", + eventType: EventTypeEnum.StartCasting, + eventCondition: ["ActionId:regex:^(46072|46128|46073|46129)$"], + userControl: false)] + public void 兽焰连尾击记录面向(Event evt, ScriptAccessory sa) + { + float rot; + try + { + rot = evt.SourceRotation(); + } + catch + { + return; + } + + long nowMs = Environment.TickCount64; + + // 面向 C(南,+Z) 或 A(北,-Z) + bool ok = IsFacingAngle(rot, AngleC, BeastFacingTol) || IsFacingAngle(rot, AngleA, BeastFacingTol); + + lock (_beastLock) + { + _beastLastRot = rot; + _beastLastMs = nowMs; + _beastFacingAC = ok; + } + + sa.Method.SendChat($"/e [兽焰连尾击] rot={rot:0.00} => {(ok ? "面向A/C(允许星轨)" : "非A/C(禁用星轨)")} "); + } + + + private (int seq, CancellationToken token) GetMeteorToken() + { + lock (_meteorLock) + return (_meteorSeq, _meteorCts.Token); + } + + private bool IsMeteorSeqValid(int seq) + { + lock (_meteorLock) + return seq == _meteorSeq; + } + + private void CancelMeteorTasks() + { + lock (_meteorLock) + { + _meteorSeq++; + try { _meteorCts.Cancel(); } catch { } + try { _meteorCts.Dispose(); } catch { } + _meteorCts = new CancellationTokenSource(); + } + } + public void Init(ScriptAccessory sa) + { + ResetAll(); + sa.Method.RemoveDraw(".*"); + } + + private void ResetAll() + { + ResetStarTrackState(); + CancelMeteorTasks(); + + _phase = 1; + _runMeteorTetherTriggered = false; + 王者陨石是否有拉线Buff = false; + 王者陨石陨石Pos = Vector3.Zero; + 王者陨石下一次Corner = Corner.未设定; + + lock (_domLock) + { + _domMask = 0; + _domCount = 0; + _domSeq++; + _domLastTick = DateTime.UtcNow.Ticks; + } + lock (_runMeteorLock) + { + _runMeteorFireTargets.Clear(); + _runMeteorPositions.Clear(); + } + lock (_beastLock) + { + _beastFacingAC = true; + _beastLastRot = 0f; + _beastLastMs = 0; + } + } + + // ========================= + // 统治:6次点名 -> 4点缺1点 -> 指路安全区 + // ========================= + [ScriptMethod(name: "铸兵之令统治指路", eventType: EventTypeEnum.StartCasting, eventCondition: ["ActionId:46112"])] + public void 铸兵之令统治指路(Event evt, ScriptAccessory sa) + { + if (!int.TryParse(evt["DurationMilliseconds"], out var dur)) return; + + var targetPos = evt.TargetPosition; + + int seqLocal; + bool startJudgeTask = false; + + lock (_domLock) + { + // 超时保护:如果距离上一次 > 4秒,认为新一轮开始 + var nowTicks = DateTime.UtcNow.Ticks; + var dtMs = (nowTicks - _domLastTick) / TimeSpan.TicksPerMillisecond; + if (dtMs > 4000) + { + _domMask = 0; + _domCount = 0; + _domSeq++; + } + _domLastTick = nowTicks; + + // 第一发进来就开一个延迟判定任务 + if (_domCount == 0) + startJudgeTask = true; + + var idx = MatchDomPointIndex(targetPos); + if (idx >= 0) + _domMask |= (1 << idx); + + _domCount++; + seqLocal = _domSeq; + } + + if (startJudgeTask) + { + _ = Task.Run(async () => + { + // 等一小会,给 6 次事件进来 + await Task.Delay(250); + + int mask, cnt, seqNow; + lock (_domLock) + { + mask = _domMask; + cnt = _domCount; + seqNow = _domSeq; + } + + // 如果期间 reset/新一轮了,直接丢弃 + if (seqNow != seqLocal) return; + + if (cnt < 6) return; + + var missing = FindMissingIndexFromMask(mask); + if (missing < 0 || missing > 3) return; + + var myIdx = sa.MyIndex(); + if (myIdx < 0 || myIdx > 7) return; + + var group = myIdx % 4; + var safe = SafeByMissing近固[missing][group]; + + DrawWaypointToMe(sa, safe, dur, "Dom_Waypoint"); + + // 本轮收尾:清空(也可以不清,让超时逻辑去清) + lock (_domLock) + { + _domMask = 0; + _domCount = 0; + _domSeq++; + } + }); + } + } + + [ScriptMethod(name: "王者陨石指路-开场", eventType: EventTypeEnum.StartCasting, eventCondition: ["ActionId:47760"])] + public void 王者陨石指路开场(Event evt, ScriptAccessory sa) + { + if (!int.TryParse(evt["DurationMilliseconds"], out var dur)) return; + + var myIdx = sa.MyIndex(); + if (myIdx < 0 || myIdx > 7) return; + + // myIdx 奇数去左边,偶数去右边 + var targetPos = myIdx % 2 == 0 ? new Vector3(98.05f, 0f, 99.29f) : new Vector3(102.47f, 0f, 100.09f); + + DrawWaypointToMe(sa, targetPos, dur, "Meteor_Waypoint"); + } + + [ScriptMethod(name: "王者陨石指路-拉线记录buff", eventType: EventTypeEnum.Tether, eventCondition: ["Id:0039"], userControl: false)] + public void 王者陨石指路拉线记录buff(Event evt, ScriptAccessory sa) + { + var myIdx = sa.MyIndex(); + if (myIdx < 0 || myIdx > 7) return; + + var targetId = evt.TargetId(); + var myId = sa.Data.Me; + if (targetId != myId) return; + + if (_phase == 1) + { + sa.Method.SendChat("/e 记录王者陨石拉线Buff和位置"); + 王者陨石是否有拉线Buff = true; + 王者陨石陨石Pos = evt.SourcePosition(); + } + } + + [ScriptMethod(name: "王者陨石指路-拉线", eventType: EventTypeEnum.ActionEffect, eventCondition: ["ActionId:46144"])] + public async void 王者陨石指路拉线(Event evt, ScriptAccessory sa) + { + var (seq, token) = GetMeteorToken(); + + _ = Task.Run(async () => + { + try + { + await Task.Delay(2000, token); // 等buff先触发 + if (!IsMeteorSeqValid(seq)) return; + + var myIdx = sa.MyIndex(); + if (myIdx < 0 || myIdx > 7) return; + + // 先确定第一次踩哪里的塔 + if (王者陨石踩塔打法选择 == 王者陨石踩塔打法.近近远远) + { + 王者陨石下一次Corner = myIdx switch + { + 0 or 4 => Corner.左上, + 1 or 5 => Corner.右上, + 2 or 6 => Corner.左下, + 3 or 7 => Corner.右下, + _ => Corner.未设定, + }; + } + else if (王者陨石踩塔打法选择 == 王者陨石踩塔打法.tndd) + { + 王者陨石下一次Corner = myIdx switch + { + 0 or 2 => Corner.左上, + 1 or 3 => Corner.右上, + 4 or 6 => Corner.左下, + 5 or 7 => Corner.右下, + _ => Corner.未设定, + }; + } + + // 第一次踩塔循环 + await RunMeteorCycleAsync(sa, seq, token); + + await Task.Delay(6500, token); // 视情况调小/调大 + sa.Method.SendChat("/e 第二次踩塔,出现buff"); + + // 你可以按你自己的机制点,在这里等 buff / tetherPos 更新完成 + // (如果你已经在别的事件里更新了 王者陨石是否有拉线Buff / 王者陨石陨石Pos,就只需要等一小会) + + + // 第二次踩塔循环(此时王者陨石是否有拉线Buff 通常变 true) + await RunMeteorCycleAsync(sa, seq, token); + } + catch (OperationCanceledException) + { + // 任务被取消,安全退出 + sa.Method.SendChat("/e 王者陨石指路任务已取消"); + } + catch (Exception ex) + { + sa.Method.SendChat($"/e [M11S] Meteor task crashed: {ex.GetType().Name}: {ex.Message}"); + } + }); + } + + [ScriptMethod(name: "六连风圈指路", eventType: EventTypeEnum.AddCombatant, eventCondition: ["DataId:19183"])] + public async void 六连风圈指路(Event evt, ScriptAccessory sa) + { + + var myIdx = sa.MyIndex(); + if (myIdx < 0 || myIdx > 7) return; + + var sourcePos = evt.SourcePosition(); + if (MathF.Abs(sourcePos.X - 100f) > 0.1f || MathF.Abs(sourcePos.Z - 87.97f) > 0.1f) + return; + + var wPos = myIdx switch + { + 0 => new Vector3(103.35f, 0.00f, 83.27f), + 1 => new Vector3(93.43f, 0.00f, 116.62f), + 2 => new Vector3(85.91f, 0.00f, 96.18f), + 3 => new Vector3(115.15f, 0.00f, 103.49f), + 4 => new Vector3(95.72f, 0.00f, 83.80f), + 5 => new Vector3(101.60f, 0.00f, 117.08f), + 6 => new Vector3(86.04f, 0.00f, 102.22f), + 7 => new Vector3(116.16f, 0.00f, 96.73f), + _ => default + }; + + DrawWaypointToMe(sa, wPos, 6000, "六连风圈指路"); + } + + [ScriptMethod(name: "六连风圈高亮", eventType: EventTypeEnum.AddCombatant, eventCondition: ["DataId:19183"])] + public void 六连风圈高亮(Event evt, ScriptAccessory sa) + { + var sourcePos = evt.SourcePosition(); + + var dp = sa.Data.GetDefaultDrawProperties(); + dp.Name = $"六连风圈高亮_{Environment.TickCount64}"; + dp.Position = sourcePos; + dp.Rotation = 0f; + dp.DestoryAt = 20000; + + dp.Scale = new Vector2(4f); + dp.ScaleMode = ScaleMode.None; + + dp.Color = 六连风圈高亮颜色.V4; + + sa.Method.SendDraw(DrawModeEnum.Imgui, DrawTypeEnum.Circle, dp); + } + + [ScriptMethod(name: "六连风圈删除", eventType: EventTypeEnum.StartCasting, eventCondition: ["ActionId:46120"], userControl: false)] + public void 六连风圈删除(Event evt, ScriptAccessory sa) + { + sa.Method.RemoveDraw("^六连风圈高亮_.*$"); + } + + [ScriptMethod(name: "陨石狂奔-远程组AC火圈BUFF记录", eventType: EventTypeEnum.TargetIcon, eventCondition: ["Id:001E"], userControl: false)] + public void 陨石狂奔远程组AC火圈BUFF记录(Event evt, ScriptAccessory sa) + { + if (_phase != 2) return; + + var tid = evt.TargetId(); + if (tid == 0) return; + + lock (_runMeteorLock) + { + if (_runMeteorFireTargets.Count >= 2) return; + if (_runMeteorFireTargets.Contains(tid)) return; + + _runMeteorFireTargets.Add(tid); + sa.Method.SendChat($"/e [P2] 记录火圈点名 {_runMeteorFireTargets.Count}/2 => 0x{tid:X}"); + } + } + + + [ScriptMethod(name: "陨石狂奔-陨石位置记录", eventType: EventTypeEnum.StartCasting, eventCondition: ["ActionId:46163"], userControl: false)] + public void 陨石狂奔陨石位置记录(Event evt, ScriptAccessory sa) + { + if (_phase != 2) return; + + var pos = evt.SourcePosition(); + if (pos == Vector3.Zero) return; + + lock (_runMeteorLock) + { + if (_runMeteorPositions.Count >= 2) return; + + foreach (var p in _runMeteorPositions) + { + if (MathF.Abs(p.X - pos.X) < RunMeteorPosEps && MathF.Abs(p.Z - pos.Z) < RunMeteorPosEps) + return; // 同点重复触发 + } + + _runMeteorPositions.Add(pos); + sa.Method.SendChat($"/e [P2] 记录陨石位置 {_runMeteorPositions.Count}/2 => X={pos.X:0.00}, Z={pos.Z:0.00}"); + } + } + + [ScriptMethod(name: "陨石狂奔指路-远程组AC火圈", eventType: EventTypeEnum.ActionEffect, eventCondition: ["ActionId:46162"])] + public void 陨石狂奔指路远程组AC火圈(Event evt, ScriptAccessory sa) + { + if (_phase != 2) return; + + _ = Task.Run(async () => + { + await Task.Delay(1500); + + List targets; + List meteors; + + lock (_runMeteorLock) + { + targets = _runMeteorFireTargets.ToList(); + meteors = _runMeteorPositions.ToList(); + } + + if (targets.Count < 2) return; + if (meteors.Count < 2) return; + + var myId = sa.Data.Me; + if (!targets.Contains(myId)) return; // 只给被点的两个人画 + + // 优先级:H1 D3 D4 H2 -> partyIdx: 2 6 7 3 + int[] prio = { 2, 6, 7, 3 }; + int PartyIdxOf(uint id) => sa.Data.PartyList.IndexOf(id); + + var ordered = targets + .Select(id => new { id, pidx = PartyIdxOf(id) }) + .Where(x => x.pidx >= 0) + .OrderBy(x => + { + var k = Array.IndexOf(prio, x.pidx); + return k < 0 ? 999 : k; + }) + .ToList(); + + if (ordered.Count < 2) return; + + var highId = ordered[0].id; + var lowId = ordered[1].id; + + bool sameXZ = + MathF.Abs(meteors[0].X - meteors[0].Z) < RunMeteorPosEps && + MathF.Abs(meteors[1].X - meteors[1].Z) < RunMeteorPosEps; + + Vector3 highPos, lowPos; + if (sameXZ) + { + highPos = new Vector3(110.04f, 0.00f, 81.11f); + lowPos = new Vector3(89.66f, 0.00f, 119.10f); + } + else + { + highPos = new Vector3(89.36f, 0.00f, 82.47f); + lowPos = new Vector3(108.91f, 0.00f, 119.13f); + } + + var go = (myId == highId) ? highPos : lowPos; + DrawWaypointToMe(sa, go, 6500, "陨石狂奔_AC火圈_Waypoint"); + }); + } + + + + [ScriptMethod(name: "陨石狂奔指路-拉线", eventType: EventTypeEnum.Tether, eventCondition: ["Id:0039"])] + public void 陨石狂奔指路拉线(Event evt, ScriptAccessory sa) + { + var myIdx = sa.MyIndex(); + if (myIdx < 0 || myIdx > 7) return; + + var targetId = evt.TargetId(); + var myId = sa.Data.Me; + if (targetId != myId) return; + + if (_phase != 2) return; + + // 只能触发一次 + if (_runMeteorTetherTriggered) return; + _runMeteorTetherTriggered = true; + + var sourcePos = evt.SourcePosition(); + sa.Method.SendChat($"/e [P2] 记录陨石狂奔Buff和位置并指路 (srcX={sourcePos.X:0.00}, srcZ={sourcePos.Z:0.00})"); + + var center = new Vector3(100f, 0f, 100f); + + var forward = center - new Vector3(sourcePos.X, 0f, sourcePos.Z); + var len = forward.Length(); + if (len < 0.001f) + { + // 极端情况:sourcePos 就在中心附近,给个兜底朝向 + forward = new Vector3(0f, 0f, -1f); + } + else + { + forward /= len; + } + + // 左方向:在 XZ 平面把 forward 逆时针转 90°(x,z)->(-z,x) + var left = new Vector3(forward.Z, 0f, -forward.X); + + // 从中心:前 40 + 左 20 + var targetPos = center + forward * 19f + left * 19f; + + DrawWaypointToMe(sa, targetPos, 7500, "陨石狂奔_Waypoint"); + sa.Method.SendChat($"/e [P2] 陨石狂奔目标点 => X={targetPos.X:0.00}, Z={targetPos.Z:0.00}"); + } + + [ScriptMethod(name: "陨石狂奔指路-二向四向火", eventType: EventTypeEnum.StartCasting, eventCondition: ["ActionId:regex:^(46170|47037)$"])] + public void 陨石狂奔指路二向四向火(Event evt, ScriptAccessory sa) + { + if (_phase != 2) return; + + // 46170 = 四向火, 47037 = 二向火 + var actionId = evt.ActionId(); + bool isFour = actionId == 46170; + bool isTwo = !isFour; + + // 读一下 cast 时长(拿不到就给个默认) + int dur = 0; + _ = int.TryParse(evt["DurationMilliseconds"], out dur); + + var myIdx = sa.MyIndex(); + if (myIdx < 0 || myIdx > 7) return; + + _ = Task.Run(async () => + { + // 给 TargetIcon / StartCasting 事件一点时间进来 + await Task.Delay(200); + + List fireTargets; + lock (_runMeteorLock) + { + fireTargets = _runMeteorFireTargets.ToList(); + } + if (fireTargets.Count < 2) return; + + var myId = sa.Data.Me; + + bool iAmFire = fireTargets.Contains(myId); + bool iAm45 = (myIdx == 4 || myIdx == 5); + bool isTen = (陨石狂奔打法选择 == 陨石狂奔打法.十引导); + bool isX = (陨石狂奔打法选择 == 陨石狂奔打法.X引导); + + // 22X 的二向火 + bool iAm01ForXTwo = isTwo && isX && (myIdx == 0 || myIdx == 1); + + // 只有相关人画:两火圈 + idx4/5 + (22X 二向火的 idx0/1) + if (!iAmFire && !iAm45 && !iAm01ForXTwo) return; + + var center = new Vector3(100f, 0f, 100f); + + // Step 1: 先集合到中点 (100,0,100),4秒 + const int gatherMs = 3500; + DrawWaypointToMe(sa, center, gatherMs, isFour ? "四向火_集合" : "二向火_集合"); + await Task.Delay(gatherMs); + + // 计算两名火圈点名的高/低优先 + int[] prio = { 2, 6, 7, 3 }; // H1 D3 D4 H2 -> partyIdx: 2 6 7 3 + int PartyIdxOf(uint id) => sa.Data.PartyList.IndexOf(id); + + var ordered = fireTargets + .Select(id => new { id, pidx = PartyIdxOf(id) }) + .Where(x => x.pidx >= 0) + .OrderBy(x => + { + var k = Array.IndexOf(prio, x.pidx); + return k < 0 ? 999 : k; + }) + .ToList(); + + if (ordered.Count < 2) return; + + var highId = ordered[0].id; + var lowId = ordered[1].id; + + // Step 2: 4秒后去最终点 + Vector3 go = Vector3.Zero; + + if (isFour) + { + if (isTen) + { + // 22十 四向火 + if (iAmFire) + go = (myId == highId) ? new Vector3(100f, 0.00f, 95f) : new Vector3(100f, 0.00f, 105f); + else if (myIdx == 4) + go = new Vector3(95f, 0.00f, 100f); + else if (myIdx == 5) + go = new Vector3(105f, 0.00f, 100f); + } + else + { + // 22X 四向火 + if (iAmFire) + go = (myId == highId) ? new Vector3(103.90f, 0.00f, 96.83f) : new Vector3(96.08f, 0.00f, 103.24f); + else if (myIdx == 4) + go = new Vector3(96.82f, 0.00f, 96.11f); + else if (myIdx == 5) + go = new Vector3(103.17f, 0.00f, 103.92f); + } + } + else + { + // 二向火 + if (isTen) + { + // 22十 二向火 + if (iAmFire) + go = (myId == highId) ? new Vector3(100f, 0.00f, 95f) : new Vector3(100f, 0.00f, 105f); + else if (myIdx == 4) + go = new Vector3(100f, 0.00f, 93f); + else if (myIdx == 5) + go = new Vector3(100f, 0.00f, 107f); + } + else + { + // 22X 二向火:火圈两人 + idx4/5 同 22十;idx0/1 选离自己近的(92.5/107.5) + if (iAmFire) + go = (myId == highId) ? new Vector3(100f, 0.00f, 95f) : new Vector3(100f, 0.00f, 105f); + else if (myIdx == 4) + go = new Vector3(100f, 0.00f, 93f); + else if (myIdx == 5) + go = new Vector3(100f, 0.00f, 107f); + else if (myIdx == 0 || myIdx == 1) + { + // 选更近的点 + var mePos = sa.Data.Objects.FirstOrDefault(o => o.GameObjectId == myId)?.Position ?? Vector3.Zero; + var a = new Vector3(100f, 0.00f, 92.5f); + var b = new Vector3(100f, 0.00f, 107.5f); + + float d2a = (mePos.X - a.X) * (mePos.X - a.X) + (mePos.Z - a.Z) * (mePos.Z - a.Z); + float d2b = (mePos.X - b.X) * (mePos.X - b.X) + (mePos.Z - b.Z) * (mePos.Z - b.Z); + go = (d2a <= d2b) ? a : b; + } + } + } + + if (go == Vector3.Zero) return; + + // 第二段持续时间:尽量用剩余 cast time;拿不到就默认 6.5s + int remainMs = dur > 0 ? Math.Max(2000, dur - gatherMs) : 6500; + DrawWaypointToMe(sa, go, remainMs, isFour ? "四向火_最终点" : "二向火_最终点"); + + // 用完再清 + lock (_runMeteorLock) + { + _runMeteorFireTargets.Clear(); + _runMeteorPositions.Clear(); + } + }); + } + + + + // 换P + [ScriptMethod(name: "陨石狂奔换P", eventType: EventTypeEnum.StartCasting, eventCondition: ["ActionId:46162"], userControl: false)] + public void 陨石狂奔换P(Event evt, ScriptAccessory sa) + { + _phase = 2; + _runMeteorTetherTriggered = false; + lock (_runMeteorLock) + { + _runMeteorFireTargets.Clear(); + _runMeteorPositions.Clear(); + } + } + + + [ScriptMethod( + name: "王者陨石掀地板指路", + eventType: EventTypeEnum.StartCasting, + eventCondition: ["ActionId:regex:^(46155|46157|46159|46161)$"] + )] + public async void 王者陨石掀地板指路(Event evt, ScriptAccessory sa) + { + + var (seq, token) = GetMeteorToken(); + + uint actionId = evt.ActionId(); + var corner = 王者陨石下一次Corner; + if (corner == Corner.未设定) return; + + // ---- 1) 先确定“踩塔位置索引” ---- + int towerIdx = actionId switch + { + 46159u => corner switch + { + Corner.右上 => 2, + Corner.右下 => 0, + Corner.左上 => 1, + Corner.左下 => 1, + _ => -1, + }, + 46161u => corner switch + { + Corner.右上 => 0, + Corner.右下 => 2, + Corner.左上 => 1, + Corner.左下 => 1, + _ => -1, + }, + 46157u => corner switch + { + Corner.右上 => 1, + Corner.右下 => 1, + Corner.左上 => 2, + Corner.左下 => 0, + _ => -1, + }, + 46155u => corner switch + { + Corner.右上 => 1, + Corner.右下 => 1, + Corner.左上 => 0, + Corner.左下 => 2, + _ => -1, + }, + _ => -1 + }; + + if (towerIdx < 0) return; + if (!王者陨石塔位置.TryGetValue(corner, out var towerArr)) return; + if (towerIdx >= towerArr.Length) return; + + var towerPos = towerArr[towerIdx]; + + // ---- 2) 再确定“掀地板后的安全点” ---- + var safePos = actionId switch + { + // 46159 面右(安全区4) + 46159u => new Vector3(83.84f, 0.00f, 117.36f), + // 46161 面右(安全区1) + 46161u => new Vector3(83.98f, 0.00f, 82.67f), + // 46157 面左(安全区3) + 46157u => new Vector3(115.73f, 0.00f, 117.35f), + // 46155 面左(安全区2) + 46155u => new Vector3(116.10f, 0.00f, 82.45f), + _ => Vector3.Zero + }; + if (safePos == Vector3.Zero) return; + + // ---- 3) 画图:先踩塔 7s;7s 后再去安全点 7s ---- + const int firstMs = 7000; + const int safeMs = 7000; + + sa.Method.SendChat($"/e [掀地板] action={actionId} corner={corner} towerIdx={towerIdx}"); + + DrawWaypointToMe(sa, towerPos, firstMs, $"掀地板_踩塔_{actionId}"); + + try + { + await Task.Delay(firstMs, token); + } + catch (OperationCanceledException) + { + return; + } + + if (!IsMeteorSeqValid(seq)) return; + + DrawWaypointToMe(sa, safePos, safeMs, $"掀地板_安全区_{actionId}"); + } + + + private static bool IsLeftCorner(Corner c) => c is Corner.左上 or Corner.左下; + private static bool IsRightCorner(Corner c) => c is Corner.右上 or Corner.右下; + + private async Task RunMeteorCycleAsync(ScriptAccessory sa, int seq, CancellationToken token) + { + token.ThrowIfCancellationRequested(); + if (!IsMeteorSeqValid(seq)) return; + + if (王者陨石下一次Corner == Corner.未设定) return; + + // 1) 先引导去踩塔:idx 逻辑更新 + int idx; + if (王者陨石是否有拉线Buff) + { + idx = 0; + + var meteorPos = 王者陨石陨石Pos; + bool hasMeteorPos = meteorPos != Vector3.Zero; + + if (hasMeteorPos) + { + bool cornerRight = IsRightCorner(王者陨石下一次Corner); + bool cornerLeft = IsLeftCorner(王者陨石下一次Corner); + + // - 陨石.X < 100:如果 corner 在右边 => idx=1,否则 idx=0 + // - 陨石.X > 100:如果 corner 在左边 => idx=1,否则 idx=0 + if (meteorPos.X < 100f) + { + idx = cornerRight ? 1 : 0; + } + else if (meteorPos.X > 100f) + { + idx = cornerLeft ? 1 : 0; + } + // X == 100:保持 idx=0 + } + } + else + { + // 没拉线 buff:保持原逻辑 + idx = (王者陨石踩塔击飞打法选择 == 王者陨石击飞打法.同平台 ? 1 : 2); + } + + sa.Method.SendChat($"/e 王者陨石下一次Corner: {王者陨石下一次Corner},idx: {idx}"); + var wPos = 王者陨石塔位置[王者陨石下一次Corner][idx]; + DrawWaypointToMe(sa, wPos, 11000, "Meteor_Tower_Waypoint"); + await Task.Delay(11000, token); + if (!IsMeteorSeqValid(seq)) return; + + sa.Method.SendChat("/e 空中飞人+判断下一次踩塔位置"); + + // 2) 空中飞人后,计算下一次 Corner + UpdateCornerAfterKnockback(); + + await Task.Delay(2500, token); + if (!IsMeteorSeqValid(seq)) return; + sa.Method.SendChat("/e 引导到待定位置"); + + // 3) 待定 + var wPos2 = 王者陨石下一次Corner switch + { + Corner.左上 => 王者陨石是否有拉线Buff ? new Vector3(88.15f, 0f, 100f) : new Vector3(93.08f, 0f, 100.38f), + Corner.右上 => 王者陨石是否有拉线Buff ? new Vector3(111.85f, 0f, 100f) : new Vector3(106.92f, 0f, 100.38f), + Corner.左下 => 王者陨石是否有拉线Buff ? new Vector3(88.15f, 0f, 100f) : new Vector3(93.08f, 0f, 100.38f), + Corner.右下 => 王者陨石是否有拉线Buff ? new Vector3(111.85f, 0f, 100f) : new Vector3(106.92f, 0f, 100.38f), + _ => Vector3.Zero, + }; + DrawWaypointToMe(sa, wPos2, 4000, "待定位置_Waypoint"); + await Task.Delay(3500, token); + if (!IsMeteorSeqValid(seq)) return; + // TODO: 这里再加一个根据同边同组没有buff的人的位置,修改corner。就近原则。 + await Task.Delay(500, token); + if (!IsMeteorSeqValid(seq)) return; + + sa.Method.SendChat("/e 引导火圈"); + + // 4) 火圈引导 + // foreach (var pos in 王者陨石火圈引导美[王者陨石下一次Corner]) + // { + // DrawWaypointToMe(sa, pos, 2000, "火圈_Waypoint"); + // await Task.Delay(2000); + // sa.Method.SendChat("/e 下一个火圈"); + // } + var fireList = 王者陨石火圈引导美[王者陨石下一次Corner]; + + for (int i = 0; i < fireList.Length; i++) + { + // sa.Method.SendChat($"/e i={i}"); + if (i == 2 && !王者陨石是否有拉线Buff) + { + // 在引导第二个火圈时重算一次 Corner + UpdateCornerByMyPosition(sa); + + // Corner 可能变了,重新取火圈路径 + fireList = 王者陨石火圈引导美[王者陨石下一次Corner]; + } + + var pos = fireList[i]; + if (王者陨石是否有拉线Buff) + DrawWaypointToMe(sa, pos, 2000, "火圈_Waypoint"); + await Task.Delay(2000, token); + if (!IsMeteorSeqValid(seq)) return; + // sa.Method.SendChat("/e 下一个火圈"); + } + + // 5) 最终位 + sa.Method.SendChat("/e 最终位置_Waypoint"); + var wPos3 = 王者陨石下一次Corner switch + { + Corner.左上 => 王者陨石是否有拉线Buff ? new Vector3(84.01f, 0f, 80.59f) : new Vector3(83.92f, 0f, 88.78f), + Corner.右上 => 王者陨石是否有拉线Buff ? new Vector3(116.01f, 0f, 80.91f) : new Vector3(115.98f, 0f, 88.78f), + Corner.左下 => 王者陨石是否有拉线Buff ? new Vector3(84.01f, 0f, 119.21f) : new Vector3(83.87f, 0f, 111.25f), + Corner.右下 => 王者陨石是否有拉线Buff ? new Vector3(115.96f, 0f, 119.02f) : new Vector3(116.01f, 0f, 111.13f), + _ => Vector3.Zero, + }; + DrawWaypointToMe(sa, wPos3, 2000, "最终位置_Waypoint"); + await Task.Delay(2000, token); + if (!IsMeteorSeqValid(seq)) return; + + // 初始化 + 王者陨石是否有拉线Buff = false; + 王者陨石陨石Pos = Vector3.Zero; + } + + private void UpdateCornerAfterKnockback() + { + if (王者陨石是否有拉线Buff) + { + var meteorPos = 王者陨石陨石Pos; + + bool isUpper = true; + bool hasMeteorPos = meteorPos != Vector3.Zero; + if (hasMeteorPos) + { + isUpper = meteorPos.Z > 100f; + } + + // 陨石.X < 100 => corner 一定是右 + // 陨石.X > 100 => corner 一定是左 + // X==100 或没抓到位置:保留原 corner 的左右 + bool isLeft; + if (hasMeteorPos && meteorPos.X < 100f) + isLeft = false; // 右 + else if (hasMeteorPos && meteorPos.X > 100f) + isLeft = true; // 左 + else + isLeft = IsLeftCorner(王者陨石下一次Corner); // fallback + + 王者陨石下一次Corner = (isLeft, isUpper) switch + { + (true, true) => Corner.左上, + (true, false) => Corner.左下, + (false, true) => Corner.右上, + (false, false) => Corner.右下, + }; + + return; + } + + // 没有拉线buff: + // - 同平台:只换上下(左上<->左下, 右上<->右下) + // - 斜飞:只换左右(左上<->右上, 左下<->右下) + if (王者陨石踩塔击飞打法选择 == 王者陨石击飞打法.同平台) + { + 王者陨石下一次Corner = 王者陨石下一次Corner switch + { + Corner.左上 => Corner.左下, + Corner.左下 => Corner.左上, + Corner.右上 => Corner.右下, + Corner.右下 => Corner.右上, + _ => 王者陨石下一次Corner, + }; + } + else + { + 王者陨石下一次Corner = 王者陨石下一次Corner switch + { + Corner.左上 => Corner.右上, + Corner.右上 => Corner.左上, + Corner.左下 => Corner.右下, + Corner.右下 => Corner.左下, + _ => 王者陨石下一次Corner, + }; + } + } + private void UpdateCornerByMyPosition(ScriptAccessory sa) + { + // 仅用于“没有拉线 buff”的情况 + if (王者陨石是否有拉线Buff) return; + + // 这里用 sa.Data.MePosition + var meId = sa.Data.Me; + // 通过id获得自己的坐标 + var p = sa.Data.Objects.FirstOrDefault(o => o.GameObjectId == meId)?.Position ?? Vector3.Zero; + if (p == Vector3.Zero) return; + + bool isLeft = IsLeftCorner(王者陨石下一次Corner); + bool isUpper = 王者陨石下一次Corner is Corner.左上 or Corner.右上; + + // X<100 左 / X>100 右;X==100 保持 + if (p.X < 100f) isLeft = true; + else if (p.X > 100f) isLeft = false; + + // Z<100 上 / Z>100 下;Z==100 保持 + if (p.Z < 100f) isUpper = true; + else if (p.Z > 100f) isUpper = false; + + 王者陨石下一次Corner = (isLeft, isUpper) switch + { + (true, true) => Corner.左上, + (true, false) => Corner.左下, + (false, true) => Corner.右上, + (false, false) => Corner.右下, + }; + + sa.Method.SendChat($"/e [无拉线] 按站位重算Corner => {王者陨石下一次Corner} (X={p.X:0.00}, Z={p.Z:0.00})"); + } + + + private static int MatchDomPointIndex(Vector3 p) + { + int best = -1; + float bestD2 = float.MaxValue; + + for (int i = 0; i < DomPoints.Length; i++) + { + var d = p - DomPoints[i]; + var d2 = d.X * d.X + d.Z * d.Z; + if (d2 < bestD2) + { + bestD2 = d2; + best = i; + } + } + + return bestD2 <= DomPointEps * DomPointEps ? best : -1; + } + private static int FindMissingIndexFromMask(int mask) + { + for (int i = 0; i < 4; i++) + { + if ((mask & (1 << i)) == 0) return i; + } + return -1; + } + + private void DrawWaypointToMe(ScriptAccessory sa, Vector3 wpos, int durMs, string name) + { + var dpWp = sa.WaypointDp(wpos, (uint)durMs, 0, name); + sa.Method.SendDraw(DrawModeEnum.Imgui, DrawTypeEnum.Displacement, dpWp); + } + + private void DrawSafeCircle(ScriptAccessory sa, Vector3 pos, int durMs, string name) + { + var dp = sa.Data.GetDefaultDrawProperties(); + dp.Name = name; + dp.Position = pos; + dp.DestoryAt = durMs; + dp.Color = sa.Data.DefaultSafeColor; + dp.Scale = new Vector2(6f); + dp.ScaleMode = ScaleMode.None; + sa.Method.SendDraw(DrawModeEnum.Default, DrawTypeEnum.Circle, dp); + } +} + +#region Helpers + +public static class EventExtensions +{ + public static float SourceRotation(this Event evt) + => JsonConvert.DeserializeObject(evt["SourceRotation"]); + private static bool ParseHexId(string? idStr, out uint id) + { + id = 0; + if (string.IsNullOrEmpty(idStr)) return false; + try + { + var idStr2 = idStr.Replace("0x", ""); + id = uint.Parse(idStr2, System.Globalization.NumberStyles.HexNumber); + return true; + } + catch + { + return false; + } + } + + public static uint ActionId(this Event evt) => JsonConvert.DeserializeObject(evt["ActionId"]); + public static uint SourceId(this Event evt) => ParseHexId(evt["SourceId"], out var id) ? id : 0; + public static uint TargetId(this Event evt) => ParseHexId(evt["TargetId"], out var id) ? id : 0; + public static Vector3 SourcePosition(this Event evt) => JsonConvert.DeserializeObject(evt["SourcePosition"]); + public static Vector3 EffectPosition(this Event evt) => JsonConvert.DeserializeObject(evt["EffectPosition"]); + public static uint DirectorId(this Event evt) => ParseHexId(evt["DirectorId"], out var id) ? id : 0; +} + +public static class ScriptAccessoryExtensions +{ + public static int MyIndex(this ScriptAccessory sa) => sa.Data.PartyList.IndexOf(sa.Data.Me); + + public static DrawPropertiesEdit FastDp(this ScriptAccessory sa, string name, Vector3 pos, uint duration, Vector2 scale, bool safe = false) + { + var dp = sa.Data.GetDefaultDrawProperties(); + dp.Name = name; + dp.Color = safe ? sa.Data.DefaultSafeColor : sa.Data.DefaultDangerColor; + dp.Position = pos; + dp.DestoryAt = duration; + dp.Scale = scale; + return dp; + } + + // public static DrawPropertiesEdit WaypointDp(this ScriptAccessory sa, Vector3 pos, uint duration, uint delay = 0, string name = "Waypoint") + // { + // var dp = sa.Data.GetDefaultDrawProperties(); + // dp.Name = name; + // dp.Color = sa.Data.DefaultSafeColor; + // dp.Owner = sa.Data.Me; + // dp.TargetPosition = pos; + // dp.DestoryAt = duration; + // dp.Delay = delay; + // dp.Scale = new Vector2(2); + // dp.ScaleMode = ScaleMode.YByDistance; + // return dp; + // } + public static DrawPropertiesEdit WaypointDp(this ScriptAccessory sa, Vector3 target, uint duration, uint delay = 0, string name = "Waypoint", Vector4? color = null) + { + var dp = sa.Data.GetDefaultDrawProperties(); + dp.Name = name; + dp.Color = color ?? sa.Data.DefaultSafeColor; + dp.Owner = sa.Data.Me; // 仍然绑定自己 + dp.TargetPosition = target; + dp.DestoryAt = duration; + dp.Delay = delay; + dp.Scale = new Vector2(2); + dp.ScaleMode = ScaleMode.YByDistance; + return dp; + } + + public static DrawPropertiesEdit WaypointFromToDp(this ScriptAccessory sa, Vector3 from, Vector3 to, uint duration, uint delay = 0, string name = "WaypointFromTo", Vector4? color = null) + { + var dp = sa.Data.GetDefaultDrawProperties(); + dp.Name = name; + dp.Color = color ?? sa.Data.DefaultSafeColor; + dp.Owner = 0; // 不绑定任何物体,防止跟着人跑 + dp.Position = from; // 起点 + dp.TargetPosition = to; // 终点 + dp.DestoryAt = duration; + dp.Delay = delay; + dp.Scale = new Vector2(2); + dp.ScaleMode = ScaleMode.YByDistance; + return dp; + } +} + +#endregion diff --git a/Codaaaaaa/MerchantsTale.cs b/Codaaaaaa/MerchantsTale.cs new file mode 100644 index 0000000..6b5df65 --- /dev/null +++ b/Codaaaaaa/MerchantsTale.cs @@ -0,0 +1,2185 @@ +using System; +using System.Linq; +using System.Numerics; +using System.Threading; +using System.Collections.Generic; +using System.Threading.Tasks; +using Newtonsoft.Json; +using KodakkuAssist.Script; +using KodakkuAssist.Module.GameEvent; +using KodakkuAssist.Module.Draw; +using KodakkuAssist.Module.Draw.Manager; +using KodakkuAssist.Data; +using KodakkuAssist.Extensions; +using System.Runtime.CompilerServices; +using System.Security.Cryptography.X509Certificates; +using System.Reflection; + +namespace Codaaaaaa.MerchantsTale; + +[ScriptType( + guid: "a2c7f9d1-4b6e-4f3a-9c18-7d2e6a5b8c41", + name: "多变迷宫 异闻商客奇谭 指路+画图", + territorys: [1317], + version: "0.0.0.4", + author: "Codaaaaaa", + note: "目前只完成了剑术大师,老一老三可以用Ryou佬的画图\n感谢Tou_uTou佬和Angelways佬的arr\n攻略使用的是mmw: https://mmw-ffxiv.feishu.cn/wiki/KvdJwQqfziIab3kPBAAcbCYvn5r")] +public class MerchantsTale +{ + #region 用户设置 + [UserSetting("是否开启TTS")] public static bool TTSOpen { get; set; } = true; + [UserSetting("TTS播报方式")] public static TTS播报方式 TTSMode { get; set; } = TTS播报方式.原生TTS; + // [UserSetting("剑术大师-P3打法")] public static 剑术大师P3打法 剑术大师_P3打法 { get; set; } = 剑术大师P3打法.MMW; + [UserSetting("Debug")] public static bool 是否展示调试信息 { get; set; } = false; + #endregion + + #region 设置enum + public enum TTS播报方式 + { + 原生TTS, + EdgeTTS, + DrTTS + } + // public enum 剑术大师P3打法 + // { + // MMW, + // 美式 + // } + #endregion + + // 换P + private int _phase = 1; + + #region 剑术大师param + // ====== 天界交叉斩:点名记录 ====== + private readonly object _tjLock = new(); + private uint _tjArrowTarget = 0; // 028C + private uint _tjMj1Target = 0; // 014C + private uint _tjMj2Target = 0; // 014D + + private static readonly Vector3 TJ_ArrowPos = new(170.04f, -16.00f, -822.50f); + + // 单:没点名 + private static readonly Vector3 TJ_NoBuff_Single = new(162.39f, -16.00f, -814.90f); + // 双:没点名 + private static readonly Vector3 TJ_NoBuff_Double = new(170.04f, -16.00f, -822.50f); + + // 圆/环:麻将站位 + private static readonly Vector3 TJ_Pos_A = new(170.02f, -16.00f, -807.20f); + private static readonly Vector3 TJ_Pos_B = new(170.00f, -16.00f, -814.48f); + + private readonly object _xzLock = new(); + + // 4785 / 4786 -> 同buff的人(一般4人/组,但我们只需要找“同组另一个人”) + private readonly Dictionary> _xzStatusGroups = new() + { + [4785u] = new List(), + [4786u] = new List(), + [4779u] = new List(), + [4783u] = new List() + + }; + + // 灵击波点名的两个人(满足过滤条件后才记录) + private readonly List _xzLingjiTargets = new(); + + // 画图名,用于 remove + private const string XZCircleNamePrefix = "XZ_LinkCircle_"; + + // 2m 绿色圈 + private static readonly Vector4 XZGreen = new(0f, 1f, 0f, 0.55f); + private readonly object _p3O3Lock = new(); + private P3O3Dir? _p3O3FourthDanger = null; + private readonly Dictionary _p3O3WavePos = new(); + private long _p3O3LastExecMs = 0; + private bool _p3O3TaskScheduled = false; + private long _p3O3FourthTicks = long.MinValue; + + private const float P3O3MatchEps = 0.6f; + + private bool _四方凶兆3收尾 = false; + + // 四向分类 + private enum P3O3Dir { West, East, South, North } + private enum P3O3Quadrant { Unknown, NW, NE, SW, SE } + + private static bool Near(float v, float target, float eps) => MathF.Abs(v - target) <= eps; + + private readonly object _p4RockLock = new(); + + // 记录两颗陨石(DataId=19229)的落点(XZ) + private readonly List _p4RockPositions = new(); + + // 去重容差 + private const float P4RockEps = 0.6f; + + private static readonly Vector3 P4RockRef = new(175.50f, -16.00f, -809.50f); + + // 集合点 + private static readonly Vector3 P4Gather = new(170.00f, -16.00f, -815.00f); + + // 分组指路点 + private static readonly Vector3 P4_A_02 = new(183.18f, -16.00f, -828.14f); + private static readonly Vector3 P4_A_13 = new(156.93f, -16.00f, -801.79f); + + private static readonly Vector3 P4_B_02 = new(156.93f, -16.00f, -828.14f); + private static readonly Vector3 P4_B_13 = new(183.18f, -16.00f, -801.79f); + private readonly object _p4O4Lock = new(); + private readonly Dictionary _p4O4WavePos = new(); + private bool _p4O4TaskScheduled = false; + + private const float P4O4MatchEps = 0.8f; + + #endregion + + // ---------------------------- + // Phase helper + // ---------------------------- + [ScriptMethod(name: "Set Phase 1", eventType: EventTypeEnum.Chat, eventCondition: ["Type:Echo", "Message:KASP1"], userControl: false)] + public void SetP1(Event evt, ScriptAccessory sa) => _phase = 1; + + [ScriptMethod(name: "Set Phase 2", eventType: EventTypeEnum.Chat, eventCondition: ["Type:Echo", "Message:KASP2"], userControl: false)] + public void SetP2(Event evt, ScriptAccessory sa) => _phase = 2; + + [ScriptMethod(name: "Set Phase 3", eventType: EventTypeEnum.Chat, eventCondition: ["Type:Echo", "Message:KASP3"], userControl: false)] + public void SetP3(Event evt, ScriptAccessory sa) => _phase = 3; + [ScriptMethod(name: "Set Phase 4", eventType: EventTypeEnum.Chat, eventCondition: ["Type:Echo", "Message:KASP4"], userControl: false)] + public void SetP4(Event evt, ScriptAccessory sa) => _phase = 4; + [ScriptMethod(name: "Set Phase 5", eventType: EventTypeEnum.Chat, eventCondition: ["Type:Echo", "Message:KASP5"], userControl: false)] + public void SetP5(Event evt, ScriptAccessory sa) => _phase = 5; + + [ScriptMethod(name: "Show Phase", eventType: EventTypeEnum.Chat, eventCondition: ["Type:Echo", "Message:phase"], userControl: false)] + public void ShowPhase(Event evt, ScriptAccessory sa) => sa.Method.SendChat($"/e Current Phase: {_phase}"); + + [ScriptMethod(name: "初始化", eventType: EventTypeEnum.Chat, eventCondition: ["Type:NPCDialogueAnnouncements", "Message:regex:.*放马过来吧.*"], userControl: false)] + public void 初始化(Event evt, ScriptAccessory sa) + { + sa.Method.RemoveDraw(".*"); + // sa.Method.SendChat($"/e Current Phase: {_phase}"); + ResetAll(); + } + + [ScriptMethod(name: "初始化兜底", eventType: EventTypeEnum.Chat, eventCondition: ["Type:SystemMessage", "Message:战斗开始!"], userControl: false)] + public void 初始化兜底(Event evt, ScriptAccessory sa) + { + sa.Method.RemoveDraw(".*"); + // sa.Method.SendChat($"/e Current Phase: {_phase}"); + ResetAll(); + } + + public void Init(ScriptAccessory sa) + { + ResetAll(); + sa.Method.RemoveDraw(".*"); + } + + private void ResetAll() + { + _phase = 1; + + lock (_tjLock) + { + _tjArrowTarget = 0; + _tjMj1Target = 0; + _tjMj2Target = 0; + } + lock (_xzLock) + { + _xzStatusGroups[4785u].Clear(); + _xzStatusGroups[4786u].Clear(); + _xzStatusGroups[4779u].Clear(); + _xzStatusGroups[4783u].Clear(); + _xzLingjiTargets.Clear(); + } + ResetP3(); + ResetP4Rock(); + ResetP4O4(); + } + private readonly object _p3Lock = new(); + + // 记录凶兆分组 + private readonly List _p3SafeEW = new(); // 4784 东西安全 + private readonly List _p3SafeNS = new(); // 4775 南北安全 + + // 记录灵击波 tether 边(source -> target) + private readonly List _p3Tethers = new(); + + private bool _p3Resolved = false; + private bool _p3GuideIssued = false; + private int _p3Seq = 0; + private long _p3LastMs = 0; + + private readonly Dictionary _p3MemberToBottom = new(); // memberId -> bottom info + + // 固定四点 + private static readonly Vector3 P3_Right = new(190.00f, -16.00f, -810.00f); + private static readonly Vector3 P3_Left = new(150.00f, -16.00f, -820.00f); + private static readonly Vector3 P3_Down = new(165.00f, -16.00f, -795.00f); + private static readonly Vector3 P3_Up = new(175.00f, -16.00f, -835.00f); + + private const float P3PointEps = 6.0f; + private const float P3AssignEps = 5.0f; + + private enum P3BottomKey { Unknown, Right, Left, Down, Up } + + private readonly record struct P3TetherEdge( + uint SourceId, + uint TargetId, + Vector3 SourcePos, + Vector3 TargetPos, + long TimeMs + ); + + private readonly record struct P3MemberLink( + uint MemberId, + uint BottomOrbId, + Vector3 BottomPos, + P3BottomKey Key + ); + + private void ResetP3() + { + lock (_p3Lock) + { + _p3Seq++; + _p3LastMs = 0; + + _p3SafeEW.Clear(); + _p3SafeNS.Clear(); + + _p3Tethers.Clear(); + _p3MemberToBottom.Clear(); + + _p3Resolved = false; + _p3GuideIssued = false; + } + + _四方凶兆3收尾 = false; + lock (_p3O3Lock) + { + _p3O3FourthDanger = null; + _p3O3FourthTicks = long.MinValue; + } + } + + private static float DistXZ2(Vector3 a, Vector3 b) + { + float dx = a.X - b.X; + float dz = a.Z - b.Z; + return dx * dx + dz * dz; + } + + private static P3BottomKey MatchBottomKey(Vector3 p) + { + // 只看 XZ + float eps2 = P3PointEps * P3PointEps; + if (DistXZ2(p, P3_Right) <= eps2) return P3BottomKey.Right; + if (DistXZ2(p, P3_Left) <= eps2) return P3BottomKey.Left; + if (DistXZ2(p, P3_Down) <= eps2) return P3BottomKey.Down; + if (DistXZ2(p, P3_Up) <= eps2) return P3BottomKey.Up; + return P3BottomKey.Unknown; + } + + private static bool IsPartyMember(ScriptAccessory sa, uint id) + => id != 0 && sa.Data.PartyList.Contains(id); + + private static Vector3 GetObjPos(ScriptAccessory sa, uint id) + => sa.Data.Objects.FirstOrDefault(o => o.GameObjectId == id)?.Position ?? Vector3.Zero; + + private static uint FindAnother(List list, uint me) + => list.FirstOrDefault(x => x != 0 && x != me); + private static bool TryClassifyWaveDir(Vector3 p, out P3O3Dir dir) + { + // X 150左右=西,190左右=东;Z -795左右=南,-835左右=北 + if (Near(p.X, 150f, P3O3MatchEps)) { dir = P3O3Dir.West; return true; } + if (Near(p.X, 190f, P3O3MatchEps)) { dir = P3O3Dir.East; return true; } + if (Near(p.Z, -795f, P3O3MatchEps)) { dir = P3O3Dir.South; return true; } + if (Near(p.Z, -835f, P3O3MatchEps)) { dir = P3O3Dir.North; return true; } + + dir = default; + return false; + } + + private static Vector3 ComposeCenter(Vector3 ewPos, Vector3 nsPos) + { + return new Vector3(nsPos.X, -16f, ewPos.Z); + } + + private static IEnumerable AllDirs() + { + yield return P3O3Dir.West; + yield return P3O3Dir.East; + yield return P3O3Dir.North; + yield return P3O3Dir.South; + } + + // 象限危险 -> 危险方向集合 + private static void AddQuadrantDanger(HashSet danger, bool se, bool sw, bool ne, bool nw) + { + if (se) { danger.Add(P3O3Dir.East); danger.Add(P3O3Dir.South); } + if (sw) { danger.Add(P3O3Dir.West); danger.Add(P3O3Dir.South); } + if (ne) { danger.Add(P3O3Dir.East); danger.Add(P3O3Dir.North); } + if (nw) { danger.Add(P3O3Dir.West); danger.Add(P3O3Dir.North); } + } + private void ClearP3O3() + { + lock (_p3O3Lock) + { + _p3O3WavePos.Clear(); + _p3O3TaskScheduled = false; + } + } + private static float DistXZ2P4(Vector3 a, Vector3 b) + { + float dx = a.X - b.X; + float dz = a.Z - b.Z; + return dx * dx + dz * dz; + } + + private static bool NearXZ(Vector3 a, Vector3 b, float eps) + => DistXZ2P4(a, b) <= eps * eps; + + private void ResetP4Rock() + { + lock (_p4RockLock) + _p4RockPositions.Clear(); + } + private void ResetP4O4() + { + lock (_p4O4Lock) + { + _p4O4WavePos.Clear(); + _p4O4TaskScheduled = false; + } + } + private static bool TryClassifyP4O4Dir(Vector3 p, out P3O3Dir dir) + { + if (Near(p.X, 150f, P4O4MatchEps)) { dir = P3O3Dir.West; return true; } + if (Near(p.X, 190f, P4O4MatchEps)) { dir = P3O3Dir.East; return true; } + if (Near(p.Z, -835f, P4O4MatchEps)) { dir = P3O3Dir.North; return true; } + if (Near(p.Z, -795f, P4O4MatchEps)) { dir = P3O3Dir.South; return true; } + + dir = default; + return false; + } + + // 4786: 东安全 / 4779: 北安全 / 4785: 西安全 / 4783: 南安全 + private static bool TryGetP4O4SafeDir(List myStatuses, out P3O3Dir safe) + { + if (myStatuses.Contains(4786u)) { safe = P3O3Dir.East; return true; } + if (myStatuses.Contains(4779u)) { safe = P3O3Dir.North; return true; } + if (myStatuses.Contains(4785u)) { safe = P3O3Dir.West; return true; } + if (myStatuses.Contains(4783u)) { safe = P3O3Dir.South; return true; } + + safe = default; + return false; + } + + private static float Abs(float x) => x < 0 ? -x : x; + + // 在两颗落石里选“指定轴更接近 innerCoord”的那一颗 + private static Vector3 PickRockByAxis(List rocks, bool compareZ, float innerCoord) + { + if (rocks.Count == 0) return Vector3.Zero; + + Vector3 best = Vector3.Zero; + float bestD = float.MaxValue; + + foreach (var r in rocks) + { + float v = compareZ ? r.Z : r.X; + float d = Abs(v - innerCoord); + if (d < bestD) + { + bestD = d; + best = r; + } + } + return best; + } + + #region 通用 + [ScriptMethod(name: "aoe播报", eventType: EventTypeEnum.StartCasting, eventCondition: ["ActionId:regex:^(45870|46686)$"])] + public void BossAoe(Event evt, ScriptAccessory sa) + { + sa.tts("超大AOE", TTSMode, TTSOpen); + } + private uint GetDataIdByObjectId(ScriptAccessory sa, uint objectId) + { + if (objectId == 0) return 0; + var obj = sa.Data.Objects.FirstOrDefault(o => o.GameObjectId == objectId); + if (obj == null) return 0; + + return obj.DataId; + } + private void DrawWaypointToMe(ScriptAccessory sa, Vector3 wpos, int durMs, string name) + { + var dpWp = sa.WaypointDp(wpos, (uint)durMs, 0, name); + sa.Method.SendDraw(DrawModeEnum.Imgui, DrawTypeEnum.Displacement, dpWp); + } + #endregion + + #region 剑术大师 + + // 47566: 圆 单 猜的 + // 47567: 环 单 + // 47568: 圆 双 + // 47569: 环 双 猜的 + [ScriptMethod(name: "剑术大师-P1-天界交叉斩", eventType: EventTypeEnum.StartCasting, eventCondition: ["ActionId:regex:^(47566|47567|47568|47569)$"])] + public void 剑术大师_P1_天界交叉斩(Event evt, ScriptAccessory sa) + { + uint actionId = evt.ActionId(); + + bool isCircle = actionId is 47566u or 47568u; + bool isDouble = actionId is 47568u or 47569u; + + int durMs = 0; + _ = int.TryParse(evt["DurationMilliseconds"], out durMs); + if (durMs <= 0) durMs = 6000; + + _ = Task.Run(async () => + { + await Task.Delay(600); + + uint myId = sa.Data.Me; + // sa.Method.SendChat($"/e Myid: {myId}"); + + uint arrow, mj1, mj2; + lock (_tjLock) + { + arrow = _tjArrowTarget; + mj1 = _tjMj1Target; + mj2 = _tjMj2Target; + } + uint arrowIdLocal = arrow; + uint mj1IdLocal = mj1; + uint mj2IdLocal = mj2; + + Vector3 mj1Target = isCircle ? TJ_Pos_A : TJ_Pos_B; + Vector3 mj2Target = isCircle ? TJ_Pos_B : TJ_Pos_A; + + bool IsCirclePos(Vector3 p) => p == TJ_Pos_A; + DrawTypeEnum TypeForMj(Vector3 mjTarget) => IsCirclePos(mjTarget) ? DrawTypeEnum.Circle : DrawTypeEnum.Donut; + + + // 画图 + void DrawOnTarget(uint targetId, string namePrefix, DrawTypeEnum type, float radius, Vector4 color, int dur) + { + if (targetId == 0) return; + var dp = sa.Data.GetDefaultDrawProperties(); + dp.Name = $"{namePrefix}_{targetId:X}_{Environment.TickCount64}"; + dp.Owner = targetId; + dp.DestoryAt = (uint)Math.Max(500, dur); + dp.Color = color; + dp.ScaleMode = ScaleMode.ByTime; + if (type == DrawTypeEnum.Circle) + { + dp.Scale = new Vector2(radius); + } + else + { + dp.Scale = new Vector2(60f); + dp.InnerScale = new Vector2(radius); + dp.Radian = float.Pi * 2; + } + sa.Method.SendDraw(DrawModeEnum.Default, type, dp); + } + // 1) 箭头:circle + if (arrowIdLocal != 0) + { + DrawOnTarget(arrowIdLocal, "TJ_Arrow", DrawTypeEnum.Circle, 5f, sa.Data.DefaultSafeColor, durMs+2000); + } + // 2) 麻将1 + if (mj1IdLocal != 0) + { + var t = TypeForMj(mj1Target); + var durMs_mj1 = isCircle ? durMs : durMs+2000; + DrawOnTarget(mj1IdLocal, "TJ_MJ1", t, 8f, sa.Data.DefaultDangerColor, durMs_mj1); + } + + // 3) 麻将2 + if (mj2IdLocal != 0) + { + var t = TypeForMj(mj2Target); + var durMs_mj2 = isCircle ? durMs+2000 : durMs; + DrawOnTarget(mj2IdLocal, "TJ_MJ2", t, 8f, sa.Data.DefaultDangerColor, durMs_mj2); + } + + // 指路 + Vector3 target = Vector3.Zero; + string label = ""; + + if (myId == arrow && arrow != 0) + { + target = TJ_ArrowPos; + label = "天界交叉斩_箭头"; + sa.tts("箭头位 去A", TTSMode, TTSOpen); + } + else if (myId == mj1 && mj1 != 0) + { + // 圆:014C->A, 014D->B + // 环:014C->B, 014D->A + target = isCircle ? TJ_Pos_A : TJ_Pos_B; + label = "天界交叉斩_麻将1"; + sa.tts(isCircle ? "麻将一 去C" : "麻将一 去中间", TTSMode, TTSOpen); + } + else if (myId == mj2 && mj2 != 0) + { + target = isCircle ? TJ_Pos_B : TJ_Pos_A; + label = "天界交叉斩_麻将2"; + sa.tts(isCircle ? "麻将二 去中间" : "麻将二 去C", TTSMode, TTSOpen); + } + else + { + // 没点名的人 + target = isDouble ? TJ_NoBuff_Double : TJ_NoBuff_Single; + label = isDouble ? "天界交叉斩_无点名_双" : "天界交叉斩_无点名_单"; + sa.tts(isDouble ? "无点名 去A分摊" : "无点名 靠边", TTSMode, TTSOpen); + } + + if (target == Vector3.Zero) return; + + var dp = sa.WaypointDp( + target: target, + duration: (uint)Math.Max(1000, durMs), + delay: 0, + name: $"{label}_{Environment.TickCount64}", + color: sa.Data.DefaultSafeColor + ); + sa.Method.SendDraw(DrawModeEnum.Imgui, DrawTypeEnum.Displacement, dp); + }); + } + + [ScriptMethod(name: "剑术大师-P1-天界交叉斩-麻将记录", eventType: EventTypeEnum.TargetIcon, eventCondition: ["Id:regex:^(028C|014C|014D)$"], userControl: false)] + public void 剑术大师_P1_天界交叉斩_麻将记录(Event evt, ScriptAccessory sa) + { + uint tid = evt.TargetId(); + if (tid == 0) return; + + string iconId = evt["Id"] ?? ""; + + lock (_tjLock) + { + if (iconId.Equals("028C", StringComparison.OrdinalIgnoreCase)) + { + _tjArrowTarget = tid; + } + else if (iconId.Equals("014C", StringComparison.OrdinalIgnoreCase)) + { + _tjMj1Target = tid; + } + else if (iconId.Equals("014D", StringComparison.OrdinalIgnoreCase)) + { + _tjMj2Target = tid; + } + } + } + + [ScriptMethod(name: "剑术大师-P1-四方凶兆1-凶兆记录", eventType: EventTypeEnum.StatusAdd, eventCondition: ["StatusID:regex:^(4785|4786|4783|4779)$"], userControl: false)] + public void 剑术大师_P1_四方凶兆1_凶兆记录(Event evt, ScriptAccessory sa) + { + // 4783 东西北 + // 4779 东西南 + uint tid = evt.TargetId(); + if (tid == 0) return; + + var sid = evt.StatusId; + // sa.Method.SendChat($"/e StatusId{sid}"); + + lock (_xzLock) + { + if (!_xzStatusGroups.TryGetValue(sid, out var list)) return; + if (!list.Contains(tid)) + list.Add(tid); + } + } + + [ScriptMethod(name: "剑术大师-P1-四方凶兆1-灵击波记录", eventType: EventTypeEnum.Tether, eventCondition: ["Id:regex:^(0173)$"], userControl: false)] + public void 剑术大师_P1_四方凶兆1_灵击波记录(Event evt, ScriptAccessory sa) + { + + uint tid = evt.TargetId(); + if (tid == 0) return; + // sa.Method.SendChat($"/e tid{tid}"); + uint dataId = GetDataIdByObjectId(sa, tid); + // sa.Method.SendChat($"/e dataId{dataId}"); + + // 过滤:DataId 是 19230 或 19228 的不记录 + if (dataId == 19230u || dataId == 19228u) return; + + lock (_xzLock) + { + if (_xzLingjiTargets.Count >= 2) return; + if (_xzLingjiTargets.Contains(tid)) return; + + _xzLingjiTargets.Add(tid); + // sa.Method.SendChat($"/e [四方凶兆1] 灵击波点名 {_xzLingjiTargets.Count}/2 => tid=0x{tid:X} dataId={dataId}"); + } + } + + [ScriptMethod(name: "剑术大师-P1-四方凶兆1-接线", eventType: EventTypeEnum.StartCasting, eventCondition: ["ActionId:regex:^(46698)$"])] + public async void 剑术大师_P1_四方凶兆1_接线(Event evt, ScriptAccessory sa) + { + if (_phase == 1) + { + await Task.Delay(1000); + uint myId = sa.Data.Me; + // sa.Method.SendChat($"/e myId{myId}"); + // sa.Method.SendChat($"/e _xzLingjiTargets{_xzLingjiTargets}"); + + uint partnerId = 0; + bool iAmLingji = false; + + lock (_xzLock) + { + iAmLingji = _xzLingjiTargets.Contains(myId); + // sa.Method.SendChat($"/e iAmLingji{iAmLingji}"); + if (iAmLingji) return; // 自己被灵击波点了,不接线,不画 + + // 找自己属于哪个凶兆组,然后找同组另一个人 + foreach (var kv in _xzStatusGroups) + { + var list = kv.Value; + if (!list.Contains(myId)) continue; + + partnerId = list.FirstOrDefault(x => x != myId); + break; + } + } + + if (partnerId == 0) + { + sa.tts("未找到搭档ID", TTSMode, TTSOpen); + return; + } + + + // 画 2m 绿圈(绑定对方脚下) + sa.tts("接同组的线", TTSMode, TTSOpen); + + var dp = sa.Data.GetDefaultDrawProperties(); + dp.Name = $"{XZCircleNamePrefix}{partnerId:X}_{Environment.TickCount64}"; + dp.Owner = partnerId; + dp.DestoryAt = 8000; // 给个够用时长,后面 ActionEffect 再 remove + dp.Scale = new Vector2(2f); + dp.ScaleMode = ScaleMode.None; + dp.Color = XZGreen; + + sa.Method.SendDraw(DrawModeEnum.Imgui, DrawTypeEnum.Circle, dp); + } + + if (_phase == 4) + { + _四方凶兆3收尾 = true; + } + } + + [ScriptMethod(name: "剑术大师-P1-天界交叉斩-清除绘图", eventType: EventTypeEnum.ActionEffect, eventCondition: ["ActionId:regex:^(46693)$"], userControl:false)] + public void 剑术大师_P1_天界交叉斩_清除绘图(Event evt, ScriptAccessory sa) + { + sa.Method.RemoveDraw(".*"); + } + + [ScriptMethod(name: "剑术大师-P1-四方凶兆1-接线清除绘图", eventType: EventTypeEnum.ActionEffect, eventCondition: ["ActionId:regex:^(46698)$"], userControl:false)] + public void 剑术大师_P1_四方凶兆1_接线清除绘图(Event evt, ScriptAccessory sa) + { + sa.Method.RemoveDraw($"^{XZCircleNamePrefix}.*$"); + + lock (_xzLock) + { + _xzStatusGroups[4785u].Clear(); + _xzStatusGroups[4786u].Clear(); + _xzStatusGroups[4779u].Clear(); + _xzStatusGroups[4783u].Clear(); + _xzLingjiTargets.Clear(); + } + } + + [ScriptMethod(name: "剑术大师-P2-八叶转轮残响回响组合技", eventType: EventTypeEnum.StartCasting, eventCondition: ["ActionId:regex:^(46707|46704)$"])] + public void 剑术大师_P2_八叶转轮残响回响组合技(Event evt, ScriptAccessory sa) + { + + uint actionId = evt.ActionId(); + + int durMs = 0; + _ = int.TryParse(evt["DurationMilliseconds"], out durMs); + if (durMs <= 0) durMs = 6000; + + if (_phase == 1) + { + sa.tts("场中集合", TTSMode, TTSOpen); + var dpWp = sa.WaypointDp( + target: new Vector3(170f, -16f, -815f), + duration: (uint)Math.Max(1000, durMs), + delay: 0, + name: $"P2-八叶转轮-场中集合_{actionId}", + color: sa.Data.DefaultSafeColor + ); + sa.Method.SendDraw(DrawModeEnum.Imgui, DrawTypeEnum.Displacement, dpWp); + } + + int delayMs = Math.Max(0, durMs); + + // 46707 用 8向 rect + float[] rots = + { + MathF.PI, // N + 3f * MathF.PI / 4f, // NE + MathF.PI / 2f, // E + MathF.PI / 4f, // SE + 0f, // S + -MathF.PI / 4f, // SW + -MathF.PI / 2f, // W + -3f * MathF.PI / 4f, // NW + }; + string[] dirNames = { "N","NE","E","SE","S","SW","W","NW" }; + + _ = Task.Run(async () => + { + try + { + await Task.Delay(delayMs); + + var partyIds = sa.Data.PartyList.ToList(); + if (partyIds.Count == 0) return; + + var posById = new List<(uint id, Vector3 pos)>(partyIds.Count); + foreach (var pid in partyIds) + { + if (pid == 0) continue; + var obj = sa.Data.Objects.FirstOrDefault(o => o.GameObjectId == pid); + if (obj == null) continue; + posById.Add((pid, obj.Position)); + } + if (posById.Count == 0) return; + + if (actionId == 46707u) + { + // 冰花 + foreach (var (pid, basePos) in posById) + { + for (int i = 0; i < 8; i++) + { + var dp = sa.Data.GetDefaultDrawProperties(); + dp.Name = $"P2-八叶转轮_Rect_{pid:X}_{dirNames[i]}_{Environment.TickCount64}"; + dp.Owner = 0; + dp.Position = basePos; + dp.Rotation = rots[i]; + dp.Color = sa.Data.DefaultDangerColor; + dp.DestoryAt = 7000; + + dp.Scale = new Vector2(8f, 35f); + dp.ScaleMode = ScaleMode.ByTime; + + sa.Method.SendDraw(DrawModeEnum.Default, DrawTypeEnum.Rect, dp); + } + } + } + else + { + // 月环 + foreach (var (pid, basePos) in posById) + { + var dp = sa.Data.GetDefaultDrawProperties(); + dp.Name = $"P2-八叶转轮_Donut_{pid:X}_{Environment.TickCount64}"; + dp.Owner = 0; + dp.Position = basePos; + dp.Rotation = 0f; + dp.Color = sa.Data.DefaultDangerColor; + dp.DestoryAt = 7000; + + dp.Scale = new Vector2(60f); + dp.InnerScale = new Vector2(8f); + dp.Radian = float.Pi * 2f; + dp.ScaleMode = ScaleMode.None; + + sa.Method.SendDraw(DrawModeEnum.Default, DrawTypeEnum.Donut, dp); + } + } + } + catch + { + } + }); + } + + private readonly object _eightLock = new(); + private int _eightSeq = 0; + private long _eightLastMs = 0; + + private readonly List _eightR1 = new(3); + private readonly List _eightR2 = new(3); + private readonly List _eightR3 = new(3); + + private bool _eightR2TaskScheduled = false; + private bool _eightR3GuideIssued = false; + + private static readonly Vector3[] EightCenters = new[] + { + new Vector3(157.00f, -16.01f, -828.00f), + new Vector3(169.97f, -16.01f, -828.00f), + new Vector3(182.97f, -16.01f, -828.00f), + + new Vector3(157.00f, -16.01f, -815.03f), + new Vector3(169.97f, -16.01f, -815.03f), + new Vector3(182.97f, -16.01f, -815.03f), + + new Vector3(157.00f, -16.01f, -802.03f), + new Vector3(169.97f, -16.01f, -802.03f), + new Vector3(182.97f, -16.01f, -802.03f), + }; + + private const float EightMatchEps = 0.6f; + + private void ResetEight() + { + lock (_eightLock) + { + _eightSeq++; + _eightLastMs = 0; + + _eightR1.Clear(); + _eightR2.Clear(); + _eightR3.Clear(); + + _eightR2TaskScheduled = false; + _eightR3GuideIssued = false; + } + } + + private static int MatchEightIndex(Vector3 p) + { + int best = -1; + float bestD2 = float.MaxValue; + + for (int i = 0; i < EightCenters.Length; i++) + { + var d = p - EightCenters[i]; + float d2 = d.X * d.X + d.Z * d.Z; + if (d2 < bestD2) { bestD2 = d2; best = i; } + } + return bestD2 <= EightMatchEps * EightMatchEps ? best : -1; + } + + private static Vector3 GetMePos(ScriptAccessory sa) + { + var meId = sa.Data.Me; + return sa.Data.Objects.FirstOrDefault(o => o.GameObjectId == meId)?.Position ?? Vector3.Zero; + } + + private static Vector3 PickNearest(Vector3 me, IEnumerable indices) + { + float bestD2 = float.MaxValue; + Vector3 best = Vector3.Zero; + + foreach (var idx in indices) + { + var c = EightCenters[idx]; + float d2 = (me.X - c.X) * (me.X - c.X) + (me.Z - c.Z) * (me.Z - c.Z); + if (d2 < bestD2) { bestD2 = d2; best = c; } + } + return best; + } + + private static void BuildR3FromR1R2(List r1, List r2, List r3) + { + r3.Clear(); + var used = new HashSet(r1); + foreach (var x in r2) used.Add(x); + + for (int i = 0; i < 9; i++) + if (!used.Contains(i)) + r3.Add(i); + } + + private void AddIdxToRounds_NoAdvance(int idx) + { + if (idx < 0) return; + + if (_eightR1.Count < 3) + { + if (!_eightR1.Contains(idx)) _eightR1.Add(idx); + return; + } + if (_eightR2.Count < 3) + { + if (!_eightR2.Contains(idx)) _eightR2.Add(idx); + return; + } + + if (_eightR3.Count < 3 && !_eightR3.Contains(idx)) + _eightR3.Add(idx); + } + + [ScriptMethod( + name: "剑术大师-P2-时差剑波", + eventType: EventTypeEnum.StartCasting, + eventCondition: ["ActionId:46714"], + userControl: false)] + public void 剑术大师_P2_时差剑波(Event evt, ScriptAccessory sa) + { + var pos = evt.EffectPosition; + int idx = MatchEightIndex(pos); + if (idx < 0) return; + + long now = Environment.TickCount64; + int seqLocal; + + bool r2Start = false; // 第二轮开始:R2 第一个圈 + bool r3Start = false; // 第三轮开始:R3 第一个圈 + + lock (_eightLock) + { + // 超时重置 + if (_eightLastMs != 0 && now - _eightLastMs > 5000) + ResetEight(); + + _eightLastMs = now; + + int r2Before = _eightR2.Count; + int r3Before = _eightR3.Count; + + AddIdxToRounds_NoAdvance(idx); + + if (r2Before == 0 && _eightR2.Count == 1) r2Start = true; + if (r3Before == 0 && _eightR3.Count == 1) r3Start = true; + + seqLocal = _eightSeq; + } + + if (r2Start) + { + bool canSchedule = false; + lock (_eightLock) + { + if (!_eightR2TaskScheduled) + { + _eightR2TaskScheduled = true; + canSchedule = true; + } + } + + if (canSchedule) + { + _ = Task.Run(async () => + { + await Task.Delay(250); + + List r1, r2, r3; + int seqNow; + + lock (_eightLock) + { + seqNow = _eightSeq; + r1 = _eightR1.ToList(); + r2 = _eightR2.ToList(); + r3 = _eightR3.ToList(); + } + + if (seqNow != seqLocal) return; + if (r1.Count < 3 || r2.Count < 3) return; + + // 推导第三轮 + var r3Calc = new List(3); + BuildR3FromR1R2(r1, r2, r3Calc); + + var me = GetMePos(sa); + if (me == Vector3.Zero) return; + + var target = PickNearest(me, r3Calc); + + var dpWp = sa.WaypointDp(target, (uint)3700, 0, "八叶R2->去R3最近圈"); + sa.Method.SendDraw(DrawModeEnum.Imgui, DrawTypeEnum.Displacement, dpWp); + }); + } + } + + if (r3Start) + { + bool need; + lock (_eightLock) + { + need = !_eightR3GuideIssued && _eightR1.Count == 3; + if (need) _eightR3GuideIssued = true; + } + + if (need) + { + var me = GetMePos(sa); + if (me == Vector3.Zero) return; + + List r1Copy; + lock (_eightLock) r1Copy = _eightR1.ToList(); + + var target = PickNearest(me, r1Copy); + + var dpWp = sa.WaypointDp(target, (uint)2000, 3000, "八叶R3->去R1最近圈"); + sa.Method.SendDraw(DrawModeEnum.Imgui, DrawTypeEnum.Displacement, dpWp); + } + } + } + + [ScriptMethod(name: "剑术大师-P2-八叶回响-分散圈", eventType: EventTypeEnum.StartCasting, eventCondition: ["ActionId:regex:^(46711)$"])] + public void 剑术大师_P2_八叶回响_分散圈(Event evt, ScriptAccessory sa) + { + var myIdx = sa.MyIndex(); + sa.tts("去标点放圈", TTSMode, TTSOpen); + + var wPos = myIdx switch + { + 0 => new Vector3(182.97f, -16.00f, -828.07f), + 1 => new Vector3(157.00f, -16.00f, -828.12f), + 2 => new Vector3(156.99f, -16.00f, -801.99f), + 3 => new Vector3(182.97f, -16.00f, -801.92f), + _ => default + }; + + var dpWp = sa.WaypointDp(wPos, (uint)5000, 4000, "剑术大师-P2-八叶回响-分散冰花指路"); + sa.Method.SendDraw(DrawModeEnum.Imgui, DrawTypeEnum.Displacement, dpWp); + } + + [ScriptMethod(name: "剑术大师-P2-八叶回响-分散圈-第二段指路", eventType: EventTypeEnum.ActionEffect, eventCondition: ["ActionId:regex:^(46712)$"])] + public void 剑术大师_P2_八叶回响_分散圈_第二段指路(Event evt, ScriptAccessory sa) + { + if(_phase != 1) return; + + var myId = sa.Data.Me; + var targetId = evt.TargetId(); + // sa.Method.SendChat($"/e {myId} {targetId}"); + if (targetId == 0 || targetId != myId) return; + + var myIdx = sa.MyIndex(); + sa.tts("快跑", TTSMode, TTSOpen); + + var wPos = myIdx switch + { + 0 => new Vector3(170.00f, -16.00f, -822.58f), + 1 => new Vector3(170.00f, -16.00f, -822.58f), + 2 => new Vector3(162.40f, -16.00f, -815.06f), + 3 => new Vector3(169.87f, -16.00f, -807.40f), + _ => default + }; + + var dpWp = sa.WaypointDp(wPos, (uint)3000, 0, "剑术大师-P2-八叶回响-分散冰花指路"); + sa.Method.SendDraw(DrawModeEnum.Imgui, DrawTypeEnum.Displacement, dpWp); + } + + [ScriptMethod(name: "剑术大师-P2-半场刀", eventType: EventTypeEnum.StartCasting, eventCondition: ["ActionId:regex:^(46716)$"])] + public void 剑术大师_P2_半场刀(Event evt, ScriptAccessory sa) + { + float rot; + try + { + rot = evt.SourceRotation; + } + catch + { + return; + } + + var pos = evt.SourcePosition(); + if (pos == Vector3.Zero) return; + + int durMs = 0; + _ = int.TryParse(evt["DurationMilliseconds"], out durMs); + if (durMs <= 0) durMs = 6000; + + const float outer = 60f; + + sa.tts("半场刀", TTSMode, TTSOpen); + var dp = sa.Data.GetDefaultDrawProperties(); + dp.Name = $"P2_半场刀_{Environment.TickCount64}"; + dp.Position = pos; + dp.Rotation = rot; + dp.Color = sa.Data.DefaultDangerColor; + dp.DestoryAt = (uint)(durMs); + dp.ScaleMode = ScaleMode.ByTime; + + dp.Scale = new Vector2(outer); + dp.Radian = MathF.PI; // 180° + + sa.Method.SendDraw(DrawModeEnum.Default, DrawTypeEnum.Fan, dp); + } + + [ScriptMethod(name: "剑术大师-P2-冥界重光波-三连分摊TTS", eventType: EventTypeEnum.StartCasting, eventCondition: ["ActionId:regex:^(46717)$"])] + public void 剑术大师_P2_冥界重光波_三连分摊TTS(Event evt, ScriptAccessory sa) + { + sa.tts("三连分摊", TTSMode, TTSOpen); + // 换p进入p3 + if (_phase > 3) return; + _phase = 3; + } + + [ScriptMethod(name: "剑术大师-P3-四方凶兆2-凶兆记录", eventType: EventTypeEnum.StatusAdd, eventCondition: ["StatusID:regex:^(4784|4775)$"], userControl: false)] + public void 剑术大师_P3_四方凶兆2_凶兆记录(Event evt, ScriptAccessory sa) + { + if (_phase != 3) return; + + uint tid = evt.TargetId(); + if (tid == 0) return; + + long now = Environment.TickCount64; + + lock (_p3Lock) + { + if (_p3LastMs != 0 && now - _p3LastMs > 12000) + ResetP3(); + + _p3LastMs = now; + + uint sid = evt.StatusId; + if (sid == 4784u) + { + if (!_p3SafeEW.Contains(tid)) _p3SafeEW.Add(tid); + } + else if (sid == 4775u) + { + if (!_p3SafeNS.Contains(tid)) _p3SafeNS.Add(tid); + } + } + } + + [ScriptMethod(name: "剑术大师-P3-四方凶兆2-灵击波记录1", eventType: EventTypeEnum.Tether, eventCondition: ["Id:regex:^(0173)$"], userControl: false)] + public void 剑术大师_P3_四方凶兆2_灵击波记录1(Event evt, ScriptAccessory sa) + { + if (_phase != 3) return; + + uint sid = evt.SourceId(); + uint tid = evt.TargetId(); + if (sid == 0 || tid == 0) return; + + long now = Environment.TickCount64; + + Vector3 sPos = evt.SourcePosition(); + Vector3 tPos = evt.TargetPosition(); + + lock (_p3Lock) + { + // 超时保护 + if (_p3LastMs != 0 && now - _p3LastMs > 12000) + ResetP3(); + _p3LastMs = now; + + // 去重 + const int windowMs = 300; + for (int i = _p3Tethers.Count - 1; i >= 0; i--) + { + var e = _p3Tethers[i]; + if (now - e.TimeMs > windowMs) break; + if (e.SourceId == sid && e.TargetId == tid) return; + } + + _p3Tethers.Add(new P3TetherEdge(sid, tid, sPos, tPos, now)); + } + } + + [ScriptMethod(name: "剑术大师-P3-四方凶兆2-灵击波记录2", + eventType: EventTypeEnum.StartCasting, + eventCondition: ["ActionId:regex:^(48653)$"], + userControl: false)] + public void 剑术大师_P3_四方凶兆2_灵击波记录2(Event evt, ScriptAccessory sa) + { + if (_phase != 3) return; + + int seqLocal; + lock (_p3Lock) + { + if (_p3Resolved) return; + _p3Resolved = true; + seqLocal = _p3Seq; + } + + _ = Task.Run(async () => + { + await Task.Delay(500); + + List edges; + List party; + Dictionary partyPos = new(); + + lock (_p3Lock) + { + if (seqLocal != _p3Seq) return; + edges = _p3Tethers.ToList(); + } + + party = sa.Data.PartyList.Where(x => x != 0).ToList(); + foreach (var pid in party) + partyPos[pid] = GetObjPos(sa, pid); + + if (edges.Count == 0 || party.Count == 0) return; + + var outMap = new Dictionary(); + var posOf = new Dictionary(); + + foreach (var e in edges) + { + outMap[e.SourceId] = e.TargetId; + + if (e.SourcePos != Vector3.Zero) posOf[e.SourceId] = e.SourcePos; + if (e.TargetPos != Vector3.Zero && !posOf.ContainsKey(e.TargetId)) posOf[e.TargetId] = e.TargetPos; + } + + var inMap = new Dictionary>(); + foreach (var e in edges) + { + if (!inMap.TryGetValue(e.TargetId, out var list)) + inMap[e.TargetId] = list = new List(); + if (!list.Contains(e.SourceId)) list.Add(e.SourceId); + } + + var memberToBottom = new Dictionary(); + + // ---- BEGIN INSERT ---- + var reservedOrbs = new HashSet(); + var usedBottoms = new HashSet(); + + uint GetBottom(uint start) + { + uint cur = start; + int guard = 0; + while (cur != 0 && guard++ < 16) + { + if (!inMap.TryGetValue(cur, out var parents) || parents.Count == 0) break; + uint p = parents.FirstOrDefault(x => !partyPos.ContainsKey(x)); + if (p == 0) break; + cur = p; + } + return cur; + } + + void MarkChainReserved(uint startOrb) + { + uint cur = startOrb; + int guard = 0; + while (cur != 0 && guard++ < 16) + { + reservedOrbs.Add(cur); + + if (!inMap.TryGetValue(cur, out var parents) || parents.Count == 0) break; + uint p = parents.FirstOrDefault(x => !partyPos.ContainsKey(x)); + if (p == 0) break; + cur = p; + } + } + + // 1) 正常匹配:orb -> player + foreach (var m in party) + { + var incoming = edges.Where(x => x.TargetId == m).Select(x => x.SourceId).Distinct().ToList(); + if (incoming.Count == 0) continue; + + uint orb = incoming.FirstOrDefault(id => posOf.ContainsKey(id)); + if (orb == 0) orb = incoming[0]; + + uint bottom = GetBottom(orb); + Vector3 bpos = posOf.TryGetValue(bottom, out var bp) ? bp : Vector3.Zero; + if (bpos == Vector3.Zero && posOf.TryGetValue(orb, out var op)) bpos = op; + + var key = MatchBottomKey(bpos); + memberToBottom[m] = new P3MemberLink(m, bottom, bpos, key); + + MarkChainReserved(orb); + MarkChainReserved(bottom); + usedBottoms.Add(bottom); + } + + // 2) orphan 兜底:只在未占用的 orb 节点里找 + var orphanOrbs = posOf + .Where(kv => !partyPos.ContainsKey(kv.Key)) + .Where(kv => kv.Value != Vector3.Zero) + .Where(kv => !reservedOrbs.Contains(kv.Key)) + .Select(kv => (id: kv.Key, pos: kv.Value)) + .ToList(); + + foreach (var m in party) + { + if (memberToBottom.ContainsKey(m)) continue; + if (!partyPos.TryGetValue(m, out var mp) || mp == Vector3.Zero) continue; + + var nearest = orphanOrbs + .Select(o => (o.id, o.pos, d2: DistXZ2(mp, o.pos))) + .OrderBy(x => x.d2) + .Take(6) + .ToList(); + + foreach (var cand in nearest) + { + uint bottom = GetBottom(cand.id); + if (bottom != 0 && usedBottoms.Contains(bottom)) + continue; + + Vector3 bpos = posOf.TryGetValue(bottom, out var bp) ? bp : cand.pos; + var key = MatchBottomKey(bpos); + + memberToBottom[m] = new P3MemberLink(m, bottom, bpos, key); + + MarkChainReserved(cand.id); + MarkChainReserved(bottom); + usedBottoms.Add(bottom); + break; + } + } + // ---- END INSERT ---- + + lock (_p3Lock) + { + if (seqLocal != _p3Seq) return; + + _p3MemberToBottom.Clear(); + foreach (var kv in memberToBottom) + _p3MemberToBottom[kv.Key] = kv.Value; + } + }); + } + + [ScriptMethod(name: "剑术大师-P3-四方凶兆2", eventType: EventTypeEnum.StartCasting, eventCondition: ["ActionId:regex:^(46725)$"])] + public void 剑术大师_P3_四方凶兆2(Event evt, ScriptAccessory sa) + { + if (_phase != 3) return; + + int seqLocal; + lock (_p3Lock) + { + if (_p3GuideIssued) return; + _p3GuideIssued = true; + seqLocal = _p3Seq; + } + + _ = Task.Run(async () => + { + await Task.Delay(1000); + + uint me = sa.Data.Me; + if (me == 0) return; + + List ew, ns; + Dictionary map; + + lock (_p3Lock) + { + if (seqLocal != _p3Seq) return; + ew = _p3SafeEW.ToList(); + ns = _p3SafeNS.ToList(); + map = _p3MemberToBottom.ToDictionary(k => k.Key, v => v.Value); + } + + bool iAmEW = ew.Contains(me); + bool iAmNS = ns.Contains(me); + + if (!iAmEW && !iAmNS) + { + sa.tts("未记录凶兆分组", TTSMode, TTSOpen); + return; + } + + uint partner = iAmEW ? FindAnother(ew, me) : FindAnother(ns, me); + if (partner == 0) + { + sa.tts("未找到同组队友", TTSMode, TTSOpen); + return; + } + + map.TryGetValue(me, out var myLink); + map.TryGetValue(partner, out var ptLink); + + Vector3 EW_A = new(188.24f, -16.00f, -819.50f); + Vector3 EW_B = new(151.26f, -16.00f, -810.56f); + + Vector3 NS_A = new(174.39f, -16.00f, -796.65f); + Vector3 NS_B = new(165.24f, -16.00f, -833.78f); + + Vector3 go = Vector3.Zero; + + if (iAmEW) + { + // - 优先看谁连 LEFT:连 LEFT 的去 EW_A,另一个去 EW_B + // - 如果两个人都不连 LEFT,则看谁连 RIGHT:连 RIGHT 的去 EW_B,另一个去 EW_A + bool meLeft = myLink.Key == P3BottomKey.Left; + bool ptLeft = ptLink.Key == P3BottomKey.Left; + + if (meLeft || ptLeft) + { + go = meLeft ? EW_A : EW_B; + } + else + { + bool meRight = myLink.Key == P3BottomKey.Right; + bool ptRight = ptLink.Key == P3BottomKey.Right; + + if (meRight || ptRight) + go = meRight ? EW_B : EW_A; + else + { + // 兜底:按站位近的分配 + sa.Method.SendChat("/e 四方凶兆2发生错误"); + sa.Method.SendChat($"/e [O2ERR] me=0x{me:X} iAmEW={iAmEW} iAmNS={iAmNS} partner=0x{partner:X} " + + $"myKey={myLink.Key} myBottom=0x{myLink.BottomOrbId:X} myPos=({myLink.BottomPos.X:0.0},{myLink.BottomPos.Z:0.0}) " + + $"ptKey={ptLink.Key} ptBottom=0x{ptLink.BottomOrbId:X} ptPos=({ptLink.BottomPos.X:0.0},{ptLink.BottomPos.Z:0.0}) " + + $"EW=[{string.Join(",", ew.Select(x=>x.ToString("X")))}] NS=[{string.Join(",", ns.Select(x=>x.ToString("X")))}] mapHasMe={map.ContainsKey(me)} mapHasPt={map.ContainsKey(partner)}"); + var mePos = GetObjPos(sa, me); + if (mePos != Vector3.Zero) + { + float d2a = DistXZ2(mePos, EW_A); + float d2b = DistXZ2(mePos, EW_B); + go = d2a <= d2b ? EW_A : EW_B; + } + } + } + + sa.tts("东西安全 去点位", TTSMode, TTSOpen); + } + else + { + // - 优先看谁连 UP:连 UP 的去 NS_A,另一个去 NS_B + // - 如果两个人都不连 UP,则看谁连 DOWN:连 DOWN 的去 NS_B,另一个去 NS_A + bool meUp = myLink.Key == P3BottomKey.Up; + bool ptUp = ptLink.Key == P3BottomKey.Up; + + if (meUp || ptUp) + { + go = meUp ? NS_A : NS_B; + } + else + { + bool meDown = myLink.Key == P3BottomKey.Down; + bool ptDown = ptLink.Key == P3BottomKey.Down; + + if (meDown || ptDown) + go = meDown ? NS_B : NS_A; + else + { + sa.Method.SendChat($"/e [O2ERR] me=0x{me:X} iAmEW={iAmEW} iAmNS={iAmNS} partner=0x{partner:X} " + + $"myKey={myLink.Key} myBottom=0x{myLink.BottomOrbId:X} myPos=({myLink.BottomPos.X:0.0},{myLink.BottomPos.Z:0.0}) " + + $"ptKey={ptLink.Key} ptBottom=0x{ptLink.BottomOrbId:X} ptPos=({ptLink.BottomPos.X:0.0},{ptLink.BottomPos.Z:0.0}) " + + $"EW=[{string.Join(",", ew.Select(x=>x.ToString("X")))}] NS=[{string.Join(",", ns.Select(x=>x.ToString("X")))}] mapHasMe={map.ContainsKey(me)} mapHasPt={map.ContainsKey(partner)}"); + // ===== dump map (member -> bottom) ===== + try + { + var lines = map + .OrderBy(k => k.Key) + .Select(kv => + $"m=0x{kv.Key:X} key={kv.Value.Key} bottom=0x{kv.Value.BottomOrbId:X} " + + $"pos=({kv.Value.BottomPos.X:0.0},{kv.Value.BottomPos.Z:0.0})" + ) + .ToList(); + + // sa.Method.SendChat($"/e [O2MAP] count={lines.Count}"); + // foreach (var s in lines) + // sa.Method.SendChat($"/e [O2MAP] {s}"); + } + catch { } + var mePos = GetObjPos(sa, me); + if (mePos != Vector3.Zero) + { + float d2a = DistXZ2(mePos, NS_A); + float d2b = DistXZ2(mePos, NS_B); + go = d2a <= d2b ? NS_A : NS_B; + } + } + } + + sa.tts("南北安全 去点位", TTSMode, TTSOpen); + } + + if (go == Vector3.Zero) return; + + DrawWaypointToMe(sa, go, 6500, "P3_四方凶兆2_指路"); + + await Task.Delay(5000); + _phase = 4; + sa.tts("天界交叉斩准备", TTSMode, TTSOpen); + }); + } + + [ScriptMethod( + name: "剑术大师-P3-四方凶兆3-灵击波记录", + eventType: EventTypeEnum.StartCasting, + eventCondition: ["ActionId:regex:^(46749)$"], + userControl: false)] + public void 剑术大师_P3_四方凶兆3_灵击波记录(Event evt, ScriptAccessory sa) + { + if (_phase != 4) return; + + var pos = evt.SourcePosition(); + if (pos == Vector3.Zero) return; + + long now = Environment.TickCount64; + + // 1) 先尽量记录四个方向的 SourcePosition + if (TryClassifyWaveDir(pos, out var dir)) + { + lock (_p3O3Lock) + { + _p3O3WavePos[dir] = pos; + } + } + + // 2) 3秒内只允许调度一次 + bool shouldSchedule = false; + lock (_p3O3Lock) + { + if (now - _p3O3LastExecMs < 3000) return; + + if (!_p3O3TaskScheduled) + { + _p3O3TaskScheduled = true; + shouldSchedule = true; + } + } + if (!shouldSchedule) return; + // if (_四方凶兆3收尾) return; + + _ = Task.Run(async () => + { + try + { + const uint SE = 4777u; // 东南危险 + const uint SW = 4778u; // 西南危险 + const uint NE = 4781u; // 东北危险 + const uint NW = 4782u; // 西北危险 + + var myStatuses = GetMyStatusIdsSafe(sa); + bool se = myStatuses.Contains(SE); + bool sw = myStatuses.Contains(SW); + bool ne = myStatuses.Contains(NE); + bool nw = myStatuses.Contains(NW); + + // 没有四个buff之一 -> 不画 + if (!se && !sw && !ne && !nw) + { + ClearP3O3(); + return; + } + + // 4) 等 200ms,让其它 SourcePosition 都进来 + await Task.Delay(200); + + Dictionary waveCopy; + lock (_p3O3Lock) + { + waveCopy = _p3O3WavePos.ToDictionary(k => k.Key, v => v.Value); + } + + // SE危险 -> 安全NW -> West + North + // SW危险 -> 安全NE -> East + North + // NE危险 -> 安全SW -> West + South + // NW危险 -> 安全SE -> East + South + P3O3Dir ewNeed, nsNeed; + if (se) { ewNeed = P3O3Dir.West; nsNeed = P3O3Dir.North; } + else if (sw) { ewNeed = P3O3Dir.East; nsNeed = P3O3Dir.North; } + else if (ne) { ewNeed = P3O3Dir.West; nsNeed = P3O3Dir.South; } + else /*nw*/ { ewNeed = P3O3Dir.East; nsNeed = P3O3Dir.South; } + + if (!waveCopy.TryGetValue(ewNeed, out var ewPos) || !waveCopy.TryGetValue(nsNeed, out var nsPos)) + { + // 没收齐坐标就不画,避免误导 + ClearP3O3(); + return; + } + + // 读取收尾标记 + 第四轮额外危险 + bool tail; + P3O3Dir? fourthDanger; + lock (_p3O3Lock) + { + tail = _四方凶兆3收尾; + fourthDanger = _p3O3FourthDanger; + } + + // ===================== + // 收尾逻辑:第四轮固定只有一个安全方向 + // ===================== + if (tail) + { + sa.tts("去边上", TTSMode, TTSOpen); + // 1) 把“象限危险”拆成四向危险 + var dangerSet = new HashSet(); + AddQuadrantDanger(dangerSet, se, sw, ne, nw); + + // 2) 叠加第四轮额外危险(North/South) + if (fourthDanger.HasValue) + dangerSet.Add(fourthDanger.Value); + + // sa.Method.SendChat($"/e [P3O3DBG] dangerSet=[{string.Join(",", dangerSet.Select(d => d.ToString()))}] " + + // $"se={se} sw={sw} ne={ne} nw={nw} fourth={fourthDanger?.ToString() ?? "null"} p303={_p3O3FourthDanger}"); + sa.Debug($"[P3O3DBG] dangerSet=[{string.Join(",", dangerSet.Select(d => d.ToString()))}] " + + $"se={se} sw={sw} ne={ne} nw={nw} fourth={fourthDanger?.ToString() ?? "null"} p303={_p3O3FourthDanger}", 是否展示调试信息); + + // 3) 找唯一安全方向 + var safeDirs = AllDirs().Where(d => !dangerSet.Contains(d)).ToList(); + if (safeDirs.Count != 1) + { + // 不符合“唯一安全”的预期 -> 兜底:回退到原本的“去安全角”逻辑 + } + else + { + var safeDir = safeDirs[0]; + + // 先找 safeDir 那一边的灵击波 + if (!waveCopy.TryGetValue(safeDir, out var safeWavePos)) + { + sa.Debug($"[P3O3DBG] missing safeDir wave: {safeDir}", 是否展示调试信息); + ClearP3O3(); + return; + } + + // 四个固定安全点 + var candidatePoints = new Dictionary + { + [P3O3Dir.East] = new Vector3(155.00f, -16.00f, -810.00f), + [P3O3Dir.West] = new Vector3(185.00f, -16.00f, -820.00f), + [P3O3Dir.North] = new Vector3(175.00f, -16.00f, -800.00f), + [P3O3Dir.South] = new Vector3(165.00f, -16.00f, -830.00f), + }; + + // safeDir 是东西时,看 Z;safeDir 是南北时,看 X + bool safeIsEW = (safeDir == P3O3Dir.East || safeDir == P3O3Dir.West); + float otherCoord = safeIsEW ? safeWavePos.Z : safeWavePos.X; + + // 只在同轴的两个候选点里选“另一轴最接近”的那个 + List> candidates; + if (safeDir == P3O3Dir.East || safeDir == P3O3Dir.West) + { + candidates = candidatePoints + .Where(kv => kv.Key == P3O3Dir.East || kv.Key == P3O3Dir.West) + .ToList(); + } + else + { + candidates = candidatePoints + .Where(kv => kv.Key == P3O3Dir.North || kv.Key == P3O3Dir.South) + .ToList(); + } + + float bestDelta = float.MaxValue; + Vector3 safePos = Vector3.Zero; + P3O3Dir finalSafeDir = safeDir; + + foreach (var kv in candidates) + { + float candidateOtherCoord = safeIsEW ? kv.Value.Z : kv.Value.X; + float delta = MathF.Abs(candidateOtherCoord - otherCoord); + if (delta < bestDelta) + { + bestDelta = delta; + safePos = kv.Value; + finalSafeDir = kv.Key; + } + } + + if (safePos == Vector3.Zero) + { + sa.Debug($"[P3O3DBG] failed to match safe point. safeDir={safeDir} wave=({safeWavePos.X:0.0},{safeWavePos.Z:0.0})", 是否展示调试信息); + ClearP3O3(); + return; + } + + sa.Debug( + $"safeDir={safeDir} finalSafeDir={finalSafeDir} safeWave=({safeWavePos.X:0.0},{safeWavePos.Z:0.0}) otherCoord={otherCoord:0.0} safePos=({safePos.X:0.0},{safePos.Z:0.0})", + 是否展示调试信息 + ); + + // 画固定安全点 rect + var dp2 = sa.Data.GetDefaultDrawProperties(); + dp2.Name = $"P3_凶兆3_第四轮安全Rect_{Environment.TickCount64}"; + dp2.Owner = 0; + dp2.Position = safePos - new Vector3(5f,0f,0f); + dp2.Rotation = MathF.PI / 2f; + dp2.DestoryAt = 3500; + dp2.Color = sa.Data.DefaultSafeColor; + dp2.ScaleMode = ScaleMode.None; + dp2.Scale = new Vector2(10f, 10f); + + sa.Method.SendDraw(DrawModeEnum.Imgui, DrawTypeEnum.Rect, dp2); + } + return; + } + + var center = ComposeCenter(ewPos, nsPos) - new Vector3(0, 0, 5f); + + var dp = sa.Data.GetDefaultDrawProperties(); + dp.Name = $"P3_凶兆3_安全Rect_{Environment.TickCount64}"; + dp.Owner = 0; + dp.Position = center; + dp.Rotation = 0f; + dp.DestoryAt = 4000; + dp.Color = sa.Data.DefaultSafeColor; + dp.ScaleMode = ScaleMode.None; + dp.Scale = new Vector2(10f, 10f); + + sa.Method.SendDraw(DrawModeEnum.Imgui, DrawTypeEnum.Rect, dp); + sa.tts("去安全角", TTSMode, TTSOpen); + + // 收尾本轮 + lock (_p3O3Lock) + { + _p3O3LastExecMs = Environment.TickCount64; + _p3O3TaskScheduled = false; + _p3O3WavePos.Clear(); + } + } + catch + { + lock (_p3O3Lock) + { + _p3O3TaskScheduled = false; + } + } + }); + } + + private List GetMyStatusIdsSafe(ScriptAccessory sa) + { + try + { + uint me = sa.Data.Me; + var obj = sa.Data.Objects.FirstOrDefault(o => o.GameObjectId == me); + if (obj == null) return new List(); + + // 常见状态列表属性名 + var t = obj.GetType(); + object? statusListObj = + t.GetProperty("StatusList")?.GetValue(obj) ?? + t.GetProperty("Statuses")?.GetValue(obj) ?? + t.GetProperty("Status")?.GetValue(obj); + + if (statusListObj is not System.Collections.IEnumerable enumerable) + return new List(); + + var result = new List(); + + foreach (var s in enumerable) + { + if (s == null) continue; + var st = s.GetType(); + + object? idObj = + st.GetProperty("StatusId")?.GetValue(s) ?? + st.GetProperty("StatusID")?.GetValue(s) ?? + st.GetProperty("Id")?.GetValue(s) ?? + st.GetProperty("ID")?.GetValue(s); + + if (idObj == null) continue; + + try + { + result.Add(Convert.ToUInt32(idObj)); + } + catch + { + // ignore + } + } + + return result; + } + catch + { + return new List(); + } + } + + [ScriptMethod( + name: "剑术大师-P3-四方凶兆3-清除绘图", + eventType: EventTypeEnum.ActionEffect, + eventCondition: ["ActionId:regex:^(46749)$"], + userControl: false)] + public void 剑术大师_P3_四方凶兆3_清除绘图(Event evt, ScriptAccessory sa) + { + if (_phase != 4) return; + + sa.Method.RemoveDraw($"^P3_凶兆3_安全Rect_.*$"); + ClearP3O3(); + } + + [ScriptMethod( + name: "剑术大师-P3-四方凶兆3-第四轮", + eventType: EventTypeEnum.Tether, + eventCondition: ["Id:regex:^(0165|0166|0167|0168)$"])] + public void 剑术大师_P3_四方凶兆3_第四轮(Event evt, ScriptAccessory sa) + { + // sa.Method.SendChat($"/e [O3-4] id={evt["Id"]} src=0x{evt.SourceId():X} tgt=0x{evt.TargetId():X} ticks={evt.DateTime.Ticks} ms={evt.DateTime.Millisecond}"); + if (_phase != 4) return; + + uint me = sa.Data.Me; + if (me == 0) return; + + uint tid = evt.SourceId(); + if (tid == 0 || tid != me) return; + string idStr = evt["Id"] ?? ""; + + P3O3Dir? danger = null; + if (idStr.Equals("0165", StringComparison.OrdinalIgnoreCase)) + danger = P3O3Dir.North; // 北凶 + else if (idStr.Equals("0168", StringComparison.OrdinalIgnoreCase)) + danger = P3O3Dir.South; // 南凶 + else if (idStr.Equals("0166", StringComparison.OrdinalIgnoreCase)) + danger = P3O3Dir.East; // 东凶 + else if (idStr.Equals("0167", StringComparison.OrdinalIgnoreCase)) + danger = P3O3Dir.West; // 西凶 + else + return; + + lock (_p3O3Lock) + { + long t = evt.DateTime.Ticks; + + // 只接受更晚的那条 + if (t >= _p3O3FourthTicks) + { + _p3O3FourthTicks = t; + _p3O3FourthDanger = danger; + // sa.Method.SendChat($"/e [O3-4] ticks={t} 已更新危险区danger={_p3O3FourthDanger}"); + sa.Debug($"{danger}", 是否展示调试信息); + } + } + } + + [ScriptMethod( + name: "剑术大师-P4-落石运动会-集合", + eventType: EventTypeEnum.TargetIcon, + eventCondition: ["Id:regex:^(028D)$"])] + public void 剑术大师_P4_落石运动会_集合(Event evt, ScriptAccessory sa) + { + if (_phase < 4) return; + + uint me = sa.Data.Me; + if (me == 0) return; + + uint tid = evt.TargetId(); + if (tid == 0 || tid != me) return; // 只对自己被点名时提示 + + sa.tts("疾跑 中间集合", TTSMode, TTSOpen); + DrawWaypointToMe(sa, P4Gather, 3500, $"P4_落石_集合_{Environment.TickCount64}"); + } + + [ScriptMethod( + name: "剑术大师-P4-落石运动会-拉线", + eventType: EventTypeEnum.Tether, + eventCondition: ["Id:regex:^(00A3)$"])] + public void 剑术大师_P4_落石运动会_拉线(Event evt, ScriptAccessory sa) + { + if (_phase < 4) return; + + uint me = sa.Data.Me; + if (me == 0) return; + + uint sid = evt.SourceId(); + uint tid = evt.TargetId(); + if (sid == 0 || tid == 0) return; + + // 只处理与自己有关的 tether + if (sid != me && tid != me) return; + + // 找到对方 + uint other = (sid == me) ? tid : sid; + + sa.tts("拉线", TTSMode, TTSOpen); + + _phase = 5; + ResetP4O4(); + + int myIdx = sa.MyIndex(); + int otherIdx = sa.Data.PartyList.IndexOf(other); + + if (myIdx < 0 || otherIdx < 0) return; + + _ = Task.Run(async () => + { + await Task.Delay(200); + + // 读取两颗陨石落点,判断 A/B + List rocks; + lock (_p4RockLock) + rocks = _p4RockPositions.ToList(); + + bool nearRef = false; + foreach (var p in rocks) + { + if (NearXZ(p, P4RockRef, 1.0f)) + { + nearRef = true; + break; + } + } + + Vector3 p02 = nearRef ? P4_A_02 : P4_B_02; + Vector3 p13 = nearRef ? P4_A_13 : P4_B_13; + + bool otherHigher = otherIdx > myIdx; + Vector3 go = otherHigher ? p02 : p13; + + DrawWaypointToMe( + sa, + go, + 5500, + $"P4_落石_拉线指路_{(nearRef ? "A" : "B")}_{(otherHigher ? "ME13" : "ME02")}_{Environment.TickCount64}" + ); + }); + } + + [ScriptMethod( + name: "剑术大师-P4-落石运动会-记录陨石位置", + eventType: EventTypeEnum.SetObjPos, + eventCondition: ["SourceDataId:19229"], + userControl: false)] + public void 剑术大师_P4_落石运动会_记录陨石位置(Event evt, ScriptAccessory sa) + { + if (_phase < 4) return; + var pos = evt.SourcePosition(); + if (pos == Vector3.Zero) return; + lock (_p4RockLock) + { + foreach (var p in _p4RockPositions) + if (NearXZ(p, pos, P4RockEps)) + return; + + if (_p4RockPositions.Count >= 2) return; + _p4RockPositions.Add(pos); + } + } + + + [ScriptMethod( + name: "剑术大师-P4-四方凶兆4-灵击波(找落石挡箭头)", + eventType: EventTypeEnum.StartCasting, + eventCondition: ["ActionId:47763"])] + public void 剑术大师_P4_四方凶兆4_灵击波(Event evt, ScriptAccessory sa) + { + if (_phase != 5) return; + + var pos = evt.SourcePosition(); + if (pos == Vector3.Zero) return; + + // 1) 分类到 East/West/North/South + if (!TryClassifyP4O4Dir(pos, out var dir)) + return; + + // 2) 写入 wavePos + bool shouldSchedule = false; + lock (_p4O4Lock) + { + _p4O4WavePos[dir] = pos; + + if (!_p4O4TaskScheduled) + { + _p4O4TaskScheduled = true; + shouldSchedule = true; + } + } + if (!shouldSchedule) return; + + _ = Task.Run(async () => + { + try + { + await Task.Delay(500); + var myStatuses = GetMyStatusIdsSafe(sa); + if (!TryGetP4O4SafeDir(myStatuses, out var safeDir)) + { + sa.tts("没读到安全方向buff", TTSMode, TTSOpen); + lock (_p4O4Lock) { _p4O4TaskScheduled = false; } + return; + } + + bool safeIsEW = (safeDir == P3O3Dir.East || safeDir == P3O3Dir.West); + + // ===== B) 等齐需要的方向===== + Dictionary wave = new(); + for (int i = 0; i < 12; i++) // 12*100=1200ms + { + lock (_p4O4Lock) + wave = _p4O4WavePos.ToDictionary(k => k.Key, v => v.Value); + + bool hasSafe = wave.ContainsKey(safeDir); + bool hasNS = wave.ContainsKey(P3O3Dir.North) && wave.ContainsKey(P3O3Dir.South); + bool hasEW = wave.ContainsKey(P3O3Dir.East) && wave.ContainsKey(P3O3Dir.West); + + if (hasSafe && (safeIsEW ? hasNS : hasEW)) + break; + + await Task.Delay(100); + } + + // ===== C) copy rocks ===== + List rocks; + lock (_p4RockLock) + rocks = _p4RockPositions.ToList(); + + if (rocks.Count < 2) + { + sa.tts("没记录到两颗落石", TTSMode, TTSOpen); + lock (_p4O4Lock) { _p4O4TaskScheduled = false; } + return; + } + + if (!wave.TryGetValue(safeDir, out var safeWavePos)) + { + sa.tts("没抓到安全边灵击波", TTSMode, TTSOpen); + lock (_p4O4Lock) { _p4O4TaskScheduled = false; } + return; + } + + // 安全东/西(靠 X 边) -> 取 Z + // 安全南/北(靠 Z 边) -> 取 X + float innerCoord = safeIsEW ? safeWavePos.Z : safeWavePos.X; + + // ===== E) 选出更匹配的落石 ===== + var chosenRock = PickRockByAxis(rocks, compareZ: safeIsEW, innerCoord: innerCoord); + if (chosenRock == Vector3.Zero) + { + lock (_p4O4Lock) { _p4O4TaskScheduled = false; } + return; + } + + // ===== F) 用另一组边判断靠哪边,偏移 3 ===== + Vector3 go = chosenRock; + + if (safeIsEW) + { + // 需要 North/South + if (!wave.TryGetValue(P3O3Dir.North, out var nPos) || + !wave.TryGetValue(P3O3Dir.South, out var sPos)) + { + sa.tts("没抓到南北灵击波", TTSMode, TTSOpen); + // sa.Method.SendChat($"/e [P4O4] missing NS, keys=[{string.Join(",", wave.Keys)}]"); + lock (_p4O4Lock) { _p4O4TaskScheduled = false; } + return; + } + + float dN = MathF.Abs(chosenRock.X - nPos.X); + float dS = MathF.Abs(chosenRock.X - sPos.X); + + // 南更近 -> 往北 5(Z 更小) + // 北更近 -> 往南 5(Z 更大) + go = (dS <= dN) ? chosenRock + new Vector3(0, 0, -4f) + : chosenRock + new Vector3(0, 0, +4f); + } + else + { + // 需要 East/West + if (!wave.TryGetValue(P3O3Dir.East, out var ePos) || + !wave.TryGetValue(P3O3Dir.West, out var wPos)) + { + sa.tts("没抓到东西灵击波", TTSMode, TTSOpen); + // sa.Method.SendChat($"/e [P4O4] missing EW, keys=[{string.Join(",", wave.Keys)}]"); + lock (_p4O4Lock) { _p4O4TaskScheduled = false; } + return; + } + + float dE = MathF.Abs(chosenRock.Z - ePos.Z); + float dW = MathF.Abs(chosenRock.Z - wPos.Z); + + go = (dE <= dW) ? chosenRock + new Vector3(-4f, 0, 0) + : chosenRock + new Vector3(+4f, 0, 0); + } + + // ===== G) 指路 ===== + sa.tts("去落石挡灵击波", TTSMode, TTSOpen); + DrawWaypointToMe(sa, go, 4500, $"P4_凶兆4_落石指路"); + + lock (_p4O4Lock) { _p4O4TaskScheduled = false; } + } + catch (Exception ex) + { + sa.Method.SendChat($"/e [P4凶兆4] EX={ex.GetType().Name}: {ex.Message}"); + lock (_p4O4Lock) { _p4O4TaskScheduled = false; } + } + }); + } + #endregion +} + +#region Helpers + +public static class EventExtensions +{ + private static bool ParseHexId(string? idStr, out uint id) + { + id = 0; + if (string.IsNullOrEmpty(idStr)) return false; + try + { + var s = idStr.Replace("0x", ""); + id = uint.Parse(s, System.Globalization.NumberStyles.HexNumber); + return true; + } + catch { return false; } + } + + public static uint ActionId(this Event evt) => JsonConvert.DeserializeObject(evt["ActionId"]); + public static uint SourceId(this Event evt) => ParseHexId(evt["SourceId"], out var id) ? id : 0; + public static uint TargetId(this Event evt) => ParseHexId(evt["TargetId"], out var id) ? id : 0; + public static Vector3 SourcePosition(this Event evt) => JsonConvert.DeserializeObject(evt["SourcePosition"]); + public static Vector3 TargetPosition(this Event evt) => JsonConvert.DeserializeObject(evt["TargetPosition"]); +} + +public static class ScriptAccessoryExtensions +{ + public static int MyIndex(this ScriptAccessory sa) => sa.Data.PartyList.IndexOf(sa.Data.Me); + + public static void tts(this ScriptAccessory sa, string text, MerchantsTale.TTS播报方式 mode, bool open) + { + if (!open) return; + switch (mode) + { + case MerchantsTale.TTS播报方式.原生TTS: + sa.Method.TTS(text); + break; + case MerchantsTale.TTS播报方式.EdgeTTS: + sa.Method.EdgeTTS(text); + break; + case MerchantsTale.TTS播报方式.DrTTS: + sa.Method.SendChat($"/pdr tts {text}"); + break; + } + } + public static void Debug(this ScriptAccessory sa, string text, bool debugOpen) + { + if (!debugOpen) return; + sa.Method.SendChat($"/e {text}"); + } + + public static DrawPropertiesEdit FastDp(this ScriptAccessory sa, string name, Vector3 pos, uint duration, Vector2 scale, bool safe = false) + { + var dp = sa.Data.GetDefaultDrawProperties(); + dp.Name = name; + dp.Color = safe ? sa.Data.DefaultSafeColor : sa.Data.DefaultDangerColor; + dp.Position = pos; + dp.DestoryAt = duration; + dp.Scale = scale; + return dp; + } + + public static DrawPropertiesEdit WaypointDp(this ScriptAccessory sa, Vector3 target, uint duration, uint delay = 0, string name = "Waypoint", Vector4? color = null) + { + var dp = sa.Data.GetDefaultDrawProperties(); + dp.Name = name; + dp.Color = color ?? sa.Data.DefaultSafeColor; + dp.Owner = sa.Data.Me; // 绑定自己 + dp.TargetPosition = target; + dp.DestoryAt = duration; + dp.Delay = delay; + dp.Scale = new Vector2(2); + dp.ScaleMode = ScaleMode.YByDistance; + return dp; + } + + public static DrawPropertiesEdit WaypointFromToDp(this ScriptAccessory sa, Vector3 from, Vector3 to, uint duration, uint delay = 0, string name = "WaypointFromTo", Vector4? color = null) + { + var dp = sa.Data.GetDefaultDrawProperties(); + dp.Name = name; + dp.Color = color ?? sa.Data.DefaultSafeColor; + dp.Owner = 0; // 不绑定任何物体,防止跟着人跑 + dp.Position = from; // 起点 + dp.TargetPosition = to; // 终点 + dp.DestoryAt = duration; + dp.Delay = delay; + dp.Scale = new Vector2(2); + dp.ScaleMode = ScaleMode.YByDistance; + return dp; + } +} + +#endregion \ No newline at end of file diff --git a/Codaaaaaa/TheForkedTowerMagic.cs b/Codaaaaaa/TheForkedTowerMagic.cs new file mode 100644 index 0000000..2806533 --- /dev/null +++ b/Codaaaaaa/TheForkedTowerMagic.cs @@ -0,0 +1,3591 @@ +using System; +using System.Linq; +using System.Numerics; +using System.Threading; +using System.Collections.Generic; +using System.Threading.Tasks; +using Newtonsoft.Json; +using KodakkuAssist.Script; +using KodakkuAssist.Module.GameEvent; +using KodakkuAssist.Module.Draw; +using KodakkuAssist.Module.Draw.Manager; +using KodakkuAssist.Data; +using KodakkuAssist.Extensions; +using Dalamud.Utility.Numerics; +using System.Runtime.CompilerServices; +using System.Security.Cryptography.X509Certificates; +using System.Reflection; +using FFXIVClientStructs.FFXIV.Client.Game.Object; + +namespace Codaaaaaa.TheForkedTowerMagic; + +[ScriptType( + guid: "45819e25-cb2d-4d84-a508-f110dc6a381a", + name: "魔之塔画图", + territorys: [1346], + version: "0.0.1.11", + author: "Codaaaaaa", + note: "写完喽,还有电的可以在频道里圈我\n\n感谢铁虎老大的帮助\n感谢Yatel老大和洋葱炒米老大的arr")] +public class TheForkedTowerMagic +{ + #region 用户设置 + [UserSetting("双头决战:只能选中自己Buff对应的头")] public static bool DualHeadTargetLock { get; set; } = false; + [UserSetting("排雷:显示塔内地雷点位(进图自动显示,/e 新月排雷 手动开关)")] public static bool 排雷显示 { get; set; } = true; + [UserSetting("钟灵时钟:播报要打的钟灵 + 拉怪错误提醒")] public static bool 钟灵时钟启用 { get; set; } = false; + [UserSetting("测试")] public static bool Debug输出 { get; set; } = false; + // [UserSetting("是否开启TTS")] public static bool TTSOpen { get; set; } = true; + #endregion + + private void Dbg(ScriptAccessory sa, string msg) + { + if (!Debug输出) return; + sa.Method.SendChat($"/e [魔之塔] {msg}"); + } + // 超魔自身会有Status 4228,普通没有 + private const uint 超魔Buff = 4228; + private static bool Is超魔(ScriptAccessory sa) + => sa.Data.Objects.Any(o => o is IBattleChara bc && bc.HasStatus(超魔Buff)); + // Map: 1136 魔之塔下层 Boss1 + // Map: 1178 魔之塔下层 小怪+Boss2 + // 换P + private double _phase = 1.0; + + [ScriptMethod(name: "Set Phase 1", eventType: EventTypeEnum.Chat, eventCondition: ["Type:Echo", "Message:KASP1"], userControl: false)] + public void SetP1(Event evt, ScriptAccessory sa) => _phase = 1; + + [ScriptMethod(name: "Set Phase 2", eventType: EventTypeEnum.Chat, eventCondition: ["Type:Echo", "Message:KASP2"], userControl: false)] + public void SetP2(Event evt, ScriptAccessory sa) => _phase = 2; + + [ScriptMethod(name: "Show Phase", eventType: EventTypeEnum.Chat, eventCondition: ["Type:Echo", "Message:KASP2"], userControl: false)] + public void ShowPhase(Event evt, ScriptAccessory sa) => sa.Method.SendChat($"/e Phase: {_phase}"); + + // 测试 + [ScriptMethod(name: "Debug - 19842状态查询", eventType: EventTypeEnum.Chat, eventCondition: ["Type:Echo", "Message:test"], userControl: false)] + public void Debug19842状态(Event evt, ScriptAccessory sa) + { + var found = false; + foreach (var obj in sa.Data.Objects) + { + if (obj is not IBattleChara bc || bc.DataId != 19307) continue; + found = true; + var statuses = bc.StatusList + .Where(s => s.StatusId != 0) + .Select(s => $"{s.StatusId}x{s.Param}({s.RemainingTime:F1}s)") + .ToList(); + sa.Method.SendChat($"/e [19842] {bc.Name} {bc.EntityId:X8} pos {bc.Position:F1}: {(statuses.Count == 0 ? "无状态" : string.Join(", ", statuses))}"); + } + if (!found) sa.Method.SendChat("/e [19842] 场上未找到该对象"); + } + + // 普魔 + #region Boss 1 双头 + + private const uint 绿头Buff = 4192; // 只能打绿头 + private const uint 蓝头Buff = 4194; // 只能打蓝头 + private static readonly uint[] 绿头DataIds = [19474, 19481]; // 普魔 / 超魔 + private static readonly uint[] 蓝头DataIds = [19475, 19482]; // 普魔 / 超魔 + + private bool _dualHeadLockOn = false; + private int _dualHeadLockState = 0; + + private readonly object _ballLock = new(); + private readonly List _thunderBalls = []; // 19478 雷球 + private readonly List _iceBalls = []; // 19479 冰球 + + // 特殊功能:决战只能选择对应buff的怪物。我抄我抄 + [ScriptMethod(name: "BOSS1 - 决战Buff目标限制", eventType: EventTypeEnum.StatusAdd, eventCondition: ["StatusID:regex:^(419[24])$"], userControl: true)] + public void 双头决战Buff目标限制(Event evt, ScriptAccessory sa) + { + if (!DualHeadTargetLock) return; + if (_dualHeadLockOn) return; + _dualHeadLockOn = true; + _dualHeadLockState = 0; + sa.Method.RegistFrameworkUpdateAction(Action); + + void Action() + { + const int 打绿头 = 1; + const int 打蓝头 = 2; + const int 无限制 = 3; + + var myObject = sa.Data.MyObject; + if (myObject is null) return; + + int currentState = (myObject.HasStatus(绿头Buff), myObject.HasStatus(蓝头Buff)) switch + { + (true, false) => 打绿头, + (false, true) => 打蓝头, + _ => 无限制 + }; + if (currentState == _dualHeadLockState) return; + + var green = sa.FindByDataId(绿头DataIds); + var blue = sa.FindByDataId(蓝头DataIds); + if (green is null || blue is null) return; // 找不到目标则下帧重试 + _dualHeadLockState = currentState; + + switch (currentState) + { + case 打绿头: + sa.SetTargetable(green, true); + sa.SetTargetable(blue, false); + break; + case 打蓝头: + sa.SetTargetable(green, false); + sa.SetTargetable(blue, true); + break; + default: + sa.SetTargetable(green, true); + sa.SetTargetable(blue, true); + break; + } + } + } + + // 47617 蓝头剧毒吐息:8s,EffectPosition 18m危险圈 + [ScriptMethod(name: "BOSS1 - 蓝头剧毒吐息", eventType: EventTypeEnum.StartCasting, eventCondition: ["ActionId:47617"])] + public void 蓝头剧毒吐息(Event evt, ScriptAccessory sa) + { + var dp = sa.FastDp("蓝头剧毒吐息", evt.EffectPosition(), 8000, new Vector2(18f)); + dp.ScaleMode = ScaleMode.ByTime; + sa.Method.SendDraw(DrawModeEnum.Default, DrawTypeEnum.Circle, dp); + } + + // 47616 绿头风暴吐息:8s,EffectPosition 上 CreateOmen 386 + [ScriptMethod(name: "BOSS1 - 绿头风暴吐息", eventType: EventTypeEnum.StartCasting, eventCondition: ["ActionId:47616"])] + public void 绿头风暴吐息(Event evt, ScriptAccessory sa) + { + sa.Method.VfxMethod.CreateOmen(530, new Vector3(30f), + evt.EffectPosition(), evt.SourceRotation(), new Vector4(1f,1f,1f,0.2f), 8000); + } + + // 50658 双头恐惧:TargetPosition为起点,按SourceRotation画6s矩形,长40宽10 + [ScriptMethod(name: "BOSS1 - 双头恐惧", eventType: EventTypeEnum.StartCasting, eventCondition: ["ActionId:50658"])] + public void 双头恐惧(Event evt, ScriptAccessory sa) + { + var dp = sa.FastDp("双头恐惧", evt.TargetPosition, 6000, new Vector2(10f, 40f)); + dp.Rotation = evt.SourceRotation(); + // dp.ScaleMode = ScaleMode.ByTime; + sa.Method.SendDraw(DrawModeEnum.Default, DrawTypeEnum.Rect, dp); + } + + // 5403 定时诅咒-东风:击退朝向(-1,0,0),13s;5404 定时诅咒-西风:击退朝向(1,0,0),13s + [ScriptMethod(name: "BOSS1 - 定时诅咒击退", eventType: EventTypeEnum.StatusAdd, eventCondition: ["StatusID:regex:^(540[34])$"])] + public void 定时诅咒击退(Event evt, ScriptAccessory sa) + { + if (evt.TargetId() != sa.Data.Me) return; + var dir = evt.StatusId == 5403 ? new Vector3(-1, 0, 0) : new Vector3(1, 0, 0); + + var dp = sa.Data.GetDefaultDrawProperties(); + dp.Name = $"定时诅咒击退-{evt.StatusId}"; + dp.Owner = sa.Data.Me; + dp.FixRotation = true; + dp.Rotation = MathF.Atan2(dir.X, dir.Z); + dp.Color = new Vector4(1f, 1f, 0f, 1f); + dp.DestoryAt = 13000; + dp.Scale = new Vector2(0.7f, 15f); + sa.Method.SendDraw(DrawModeEnum.Default, DrawTypeEnum.Displacement, dp); + } + + // 19478 雷球出现,记录位置 + [ScriptMethod(name: "BOSS1 - 雷球位置记录", eventType: EventTypeEnum.AddCombatant, eventCondition: ["DataId:19478"], userControl: false)] + public void 雷球位置记录(Event evt, ScriptAccessory sa) + { + var pos = evt.SourcePosition(); + lock (_ballLock) + { + if (_thunderBalls.All(p => DistXZ(p, pos) > 1f)) + _thunderBalls.Add(pos); + } + } + + // 19479 冰球出现,记录位置 + [ScriptMethod(name: "BOSS1 - 冰球位置记录", eventType: EventTypeEnum.AddCombatant, eventCondition: ["DataId:19479"], userControl: false)] + public void 冰球位置记录(Event evt, ScriptAccessory sa) + { + var pos = evt.SourcePosition(); + lock (_ballLock) + { + if (_iceBalls.All(p => DistXZ(p, pos) > 1f)) + _iceBalls.Add(pos); + } + } + + // 50698 冰簇:EffectPosition 15m内的冰球位置画15m危险圈8s,并从list删除 + [ScriptMethod(name: "BOSS1 - 冰簇", eventType: EventTypeEnum.StartCasting, eventCondition: ["ActionId:50698"])] + public void 冰簇(Event evt, ScriptAccessory sa) + => TriggerBallExplosion(sa, _iceBalls, evt.EffectPosition(), "冰簇爆炸"); + + // 50697 雷簇:EffectPosition 15m内的雷球位置画15m危险圈8s,并从list删除 + [ScriptMethod(name: "BOSS1 - 雷簇", eventType: EventTypeEnum.StartCasting, eventCondition: ["ActionId:50697"])] + public void 雷簇(Event evt, ScriptAccessory sa) + => TriggerBallExplosion(sa, _thunderBalls, evt.EffectPosition(), "雷簇爆炸"); + + private void TriggerBallExplosion(ScriptAccessory sa, List balls, Vector3 effectPos, string name) + { + List triggered = []; + lock (_ballLock) + { + triggered.AddRange(balls.Where(p => DistXZ(p, effectPos) <= 15f)); + balls.RemoveAll(p => DistXZ(p, effectPos) <= 15f); + } + for (var i = 0; i < triggered.Count; i++) + { + var dp = sa.FastDp($"{name}-{i}", triggered[i], 10000, new Vector2(15f)); + dp.ScaleMode = ScaleMode.ByTime; + sa.Method.SendDraw(DrawModeEnum.Default, DrawTypeEnum.Circle, dp); + } + } + + // 47735 雷霜暴风雨(绿头19474/超魔19481):所有剩余雷球/冰球位置画15m危险圈5s,然后清空两个list + [ScriptMethod(name: "BOSS1 - 雷霜暴风雨", eventType: EventTypeEnum.StartCasting, eventCondition: ["ActionId:47735", "SourceDataId:regex:^(19474|19481)$"])] + public void 雷霜暴风雨(Event evt, ScriptAccessory sa) + { + List remains = []; + lock (_ballLock) + { + remains.AddRange(_thunderBalls); + remains.AddRange(_iceBalls); + _thunderBalls.Clear(); + _iceBalls.Clear(); + } + for (var i = 0; i < remains.Count; i++) + { + var dp = sa.FastDp($"雷霜暴风雨-{i}", remains[i], 7000, new Vector2(15f)); + dp.ScaleMode = ScaleMode.ByTime; + sa.Method.SendDraw(DrawModeEnum.Default, DrawTypeEnum.Circle, dp); + } + } + + // 50703 冰焰凝环:先去EffectPosition朝me方向6m外(6s),6s后tts"穿",指路4m内(2s) + [ScriptMethod(name: "BOSS1 - 冰焰凝环", eventType: EventTypeEnum.StartCasting, eventCondition: ["ActionId:regex:^(50703|50704)$"])] + public async void 冰焰凝环(Event evt, ScriptAccessory sa) + { + var center = evt.EffectPosition(); + + var outPos = center + DirToMe(sa, center) * 6f; + sa.Method.SendDraw(DrawModeEnum.Imgui, DrawTypeEnum.Displacement, + sa.WaypointDp(outPos, 6000, 0, "冰焰凝环-外")); + + await Task.Delay(6000); + + sa.Method.TTS("穿", 3); + var inPos = center + DirToMe(sa, center) * 4f; + sa.Method.SendDraw(DrawModeEnum.Imgui, DrawTypeEnum.Displacement, + sa.WaypointDp(inPos, 2000, 0, "冰焰凝环-内")); + } + // 50705 冰焰凝环:先去EffectPosition朝me方向6m外(6s),6s后tts"穿",指路4m内(2s) + [ScriptMethod(name: "BOSS1 - 连续冰焰凝环", eventType: EventTypeEnum.StartCasting, eventCondition: ["ActionId:regex:^(50705)$"])] + public async void 冰焰凝环2(Event evt, ScriptAccessory sa) + { + var center = evt.EffectPosition(); + + var outPos = center + DirToMe(sa, center) * 6f; + sa.Method.SendDraw(DrawModeEnum.Imgui, DrawTypeEnum.Displacement, + sa.WaypointDp(outPos, 2000, 4000, "冰焰凝环-外")); + + await Task.Delay(6000); + + sa.Method.TTS("穿", 3); + var inPos = center + DirToMe(sa, center) * 4f; + sa.Method.SendDraw(DrawModeEnum.Imgui, DrawTypeEnum.Displacement, + sa.WaypointDp(inPos, 2000, 0, "冰焰凝环-内")); + } + + // 从center指向me的水平单位向量 + private static Vector3 DirToMe(ScriptAccessory sa, Vector3 center) + { + var mePos = sa.Data.MyObject?.Position ?? center; + var dir = new Vector3(mePos.X - center.X, 0, mePos.Z - center.Z); + return dir.Length() < 0.01f ? new Vector3(0, 0, 1) : Vector3.Normalize(dir); + } + + private static float DistXZ(Vector3 a, Vector3 b) + => new Vector2(a.X - b.X, a.Z - b.Z).Length(); + + #endregion + + #region BOSS2 剑舞者 + private static readonly Vector3 Boss2场中 = new(600f, -674f, 704f); + private readonly object _leapLock = new(); + private readonly List _leapPositions = []; // 49594 跃进步法落点,按触发顺序 + + private readonly object _bladeRectLock = new(); + private readonly List<(Vector3 Pos, float Rot)> _bladeRects = []; // 2015283 剑刃矩形,按触发顺序 + + // 49585 半圆:SourcePosition为圆心,面对SourceRotation + [ScriptMethod(name: "BOSS2 - 秘法剑", eventType: EventTypeEnum.StartCasting, eventCondition: ["ActionId:49585"])] + public void 半圆斩(Event evt, ScriptAccessory sa) + { + Dbg(sa, $"半圆斩(49585):src {evt.SourceId():X8} pos {evt.SourcePosition():F1} rot {evt.SourceRotation():F2}"); + var dp = sa.FastDp($"半圆斩-{evt.SourceId()}", evt.SourcePosition(), 5500, new Vector2(96f)); + dp.Rotation = evt.SourceRotation(); + dp.Radian = MathF.PI; + sa.Method.SendDraw(DrawModeEnum.Default, DrawTypeEnum.Fan, dp); + } + + [ScriptMethod(name: "BOSS2 - 剑舞", eventType: EventTypeEnum.ObjectEffect, eventCondition: ["Id1:1", "Id2:2"], userControl: false)] + public void 剑刃矩形(Event evt, ScriptAccessory sa) + { + if (Is超魔(sa)) return; // 该机制仅普通触发,超魔由舞动之剑预判处理 + var obj = sa.Data.Objects.SearchById(evt.SourceId()); + if (obj is null || obj.DataId != 2015283) return; + + var pos = evt.SourcePosition(); + var rot = evt.SourceRotation(); + + List<(Vector3 Pos, float Rot)> rects; + lock (_bladeRectLock) + { + if (_bladeRects.Any(r => DistXZ(r.Pos, pos) < 1f && MathF.Abs(WrapPi(r.Rot - rot)) < 0.1f)) return; + _bladeRects.Add((pos, rot)); + Dbg(sa, $"剑刃矩形记录 #{_bladeRects.Count}:pos {pos:F1} rot {rot:F2}"); + if (_bladeRects.Count < 4) return; + + rects = [.. _bladeRects]; + _bladeRects.Clear(); // 用完清空以便下次使用 + } + + for (var i = 0; i < rects.Count; i++) + { + var delay = (uint)(i == 0 ? 0 : 6000 + (i - 1) * 2500); + var duration = (uint)(i == 0 ? 6000 : 2500); + foreach (var (extraRot, tag) in new[] { (0f, "正"), (MathF.PI, "反") }) + { + var dp = sa.FastDp($"剑刃矩形-{i + 1}-{tag}", rects[i].Pos, duration, new Vector2(20f, 60f)); + dp.Rotation = rects[i].Rot + extraRot; + dp.Delay = delay; + dp.ScaleMode = ScaleMode.ByTime; + sa.Method.SendDraw(DrawModeEnum.Default, DrawTypeEnum.Rect, dp); + } + } + } + + // 49594 跃进步法:TargetDataId不是19830时,按触发顺序记录EffectPosition,排除(0,0,0) + [ScriptMethod(name: "BOSS2 - 跃进步法记录", eventType: EventTypeEnum.ActionEffect, eventCondition: ["ActionId:regex:^(49596|49597)$"], userControl: false)] + public void 跃进步法记录(Event evt, ScriptAccessory sa) + { + var pos = evt.EffectPosition(); + if (pos.Length() < 0.01f) return; // 排除(0,0,0) + + lock (_leapLock) + { + if (_leapPositions.All(p => DistXZ(p, pos) > 1f)) + { + _leapPositions.Add(pos); + Dbg(sa, $"跃进步法记录 #{_leapPositions.Count}:{pos:F1}"); + } + } + } + + // 49685 剑技爆发:按顺序指路各落点 + [ScriptMethod(name: "BOSS2 - 剑技爆发指路", eventType: EventTypeEnum.StartCasting, eventCondition: ["ActionId:49685"])] + public void 剑技爆发指路(Event evt, ScriptAccessory sa) + { + List points; + lock (_leapLock) + { + points = _leapPositions.Select(p => 朝场中偏移(p, 9f)).ToList(); + _leapPositions.Clear(); // 用完清空 + } + if (points.Count == 0) + { + Dbg(sa, $"剑技爆发(49685):落点list为空,跳过指路"); + return; + } + Dbg(sa, $"剑技爆发(49685):共 {points.Count} 个落点,偏移后 [{string.Join(" | ", points.Select(p => p.ToString("F1")))}]"); + + var green = new Vector4(0f, 1f, 0f, 1f); + var white = new Vector4(1f, 1f, 1f, 1f); + + for (var k = 0; k < points.Count; k++) + { + // 第一段显示5s,之后每段3s + var delay = (uint)(k == 0 ? 0 : 5000 + (k - 1) * 2500); + var duration = (uint)(k == 0 ? 5000 : 2500); + sa.Method.SendDraw(DrawModeEnum.Imgui, DrawTypeEnum.Displacement, + sa.WaypointDp(points[k], duration, delay, $"剑技爆发-me到{k + 1}", green)); + for (var j = k; j < points.Count - 1; j++) + sa.Method.SendDraw(DrawModeEnum.Imgui, DrawTypeEnum.Displacement, + sa.WaypointFromToDp(points[j], points[j + 1], duration, delay, $"剑技爆发-{j + 1}到{j + 2}-阶段{k + 1}", white)); + } + } + + // 将pos沿水平方向朝场中移动dist米 + private static Vector3 朝场中偏移(Vector3 pos, float dist) + { + var dir = new Vector3(Boss2场中.X - pos.X, 0, Boss2场中.Z - pos.Z); + return dir.Length() < 0.01f ? pos : pos + Vector3.Normalize(dir) * dist; + } + + // 将pos绕场中水平旋转rad弧度 + private static Vector3 绕场中旋转(Vector3 pos, float rad) + { + var dx = pos.X - Boss2场中.X; + var dz = pos.Z - Boss2场中.Z; + var cos = MathF.Cos(rad); + var sin = MathF.Sin(rad); + return new Vector3(Boss2场中.X + dx * cos - dz * sin, pos.Y, Boss2场中.Z + dx * sin + dz * cos); + } + + // 将pos绕场中旋转deg度,方向取远离awayFrom的一侧 + private static Vector3 绕场中旋转远离(Vector3 pos, Vector3 awayFrom, float deg = 10f) + { + var rad = deg * MathF.PI / 180f; + var a = 绕场中旋转(pos, rad); + var b = 绕场中旋转(pos, -rad); + return DistXZ(a, awayFrom) >= DistXZ(b, awayFrom) ? a : b; + } + + // 49616 突进:30m长6m宽矩形,4s + [ScriptMethod(name: "BOSS2 - 突进", eventType: EventTypeEnum.StartCasting, eventCondition: ["ActionId:49616"])] + public void 突进(Event evt, ScriptAccessory sa) + { + Dbg(sa, $"突进(49616):pos {evt.SourcePosition():F1} rot {evt.SourceRotation():F2}"); + var dp = sa.FastDp("突进", evt.SourcePosition(), 4000, new Vector2(6f, 30f)); + dp.Rotation = evt.SourceRotation(); + dp.ScaleMode = ScaleMode.ByTime; + sa.Method.SendDraw(DrawModeEnum.Default, DrawTypeEnum.Rect, dp); + } + + private const float 剑月环内半径 = 15f; + private const float 剑月环外半径 = 40f; + private long _b2SwordTtsAt; + + private void B2SwordTts(ScriptAccessory sa, string word) + { + var now = Environment.TickCount64; + if (now - Interlocked.Exchange(ref _b2SwordTtsAt, now) < 800) return; // 双剑同触发时只报一次 + sa.Method.TTS(word, 3); + } + + private static unsafe int B2GetModelState(ScriptAccessory sa, uint entityId) + { + var obj = sa.Data.Objects.SearchById(entityId); + if (obj is null || !obj.IsValid()) return -1; + var c = (FFXIVClientStructs.FFXIV.Client.Game.Character.Character*)obj.Address; + return c->Timeline.ModelState; + } + + private void B2DrawSwordAoe(ScriptAccessory sa, uint sid, Vector3 pos, bool isDonut, uint duration, uint delay=0) + { + if (isDonut) + { + var dp = sa.FastDp($"剑月环-{sid}-{delay}", pos, duration, new Vector2(剑月环外半径)); + dp.InnerScale = new Vector2(剑月环内半径); + dp.Radian = float.Pi * 2; + dp.Delay = delay; + sa.Method.SendDraw(DrawModeEnum.Default, DrawTypeEnum.Donut, dp); + } + else + { + var dp = sa.FastDp($"剑钢铁-{sid}-{delay}", pos, duration, new Vector2(15f)); + dp.ScaleMode = ScaleMode.ByTime; + dp.Delay = delay; + sa.Method.SendDraw(DrawModeEnum.Default, DrawTypeEnum.Circle, dp); + } + } + + [ScriptMethod(name: "BOSS2 - 舞动之剑预判", eventType: EventTypeEnum.PlayActionTimeline, eventCondition: ["Id:9710", "SourceDataId:19833"])] + public void 舞动之剑预判(Event evt, ScriptAccessory sa) + { + var sid = evt.SourceId(); + var pose = B2GetModelState(sa, sid); + Dbg(sa, $"舞动之剑9710:src {sid:X8} 姿势 {pose}"); + switch (pose) + { + case 0: // → 小月环(内10外40) + { + var donut = sa.FastDp($"剑月环-{sid}-0", evt.SourcePosition(), 9000, new Vector2(40f)); + donut.InnerScale = new Vector2(10f); + donut.Radian = float.Pi * 2; + sa.Method.SendDraw(DrawModeEnum.Default, DrawTypeEnum.Donut, donut); + break; + } + case 4: // idle_sp_1 → 月环 + B2DrawSwordAoe(sa, sid, evt.SourcePosition(), true, 9000); + break; + case 5: // idle_sp_2 → 大月环(内20外40) + { + var donut = sa.FastDp($"剑月环-{sid}-0", evt.SourcePosition(), 9000, new Vector2(40f)); + donut.InnerScale = new Vector2(20f); + donut.Radian = float.Pi * 2; + sa.Method.SendDraw(DrawModeEnum.Default, DrawTypeEnum.Donut, donut); + break; + } + case 6: // idle_sp_3 → 小钢铁(10) + { + var circle = sa.FastDp($"剑钢铁-{sid}-0", evt.SourcePosition(), 9000, new Vector2(10f)); + circle.ScaleMode = ScaleMode.ByTime; + sa.Method.SendDraw(DrawModeEnum.Default, DrawTypeEnum.Circle, circle); + break; + } + case 7: // idle_sp_4 → 钢铁 + B2DrawSwordAoe(sa, sid, evt.SourcePosition(), false, 9000); + break; + case 31: // → 大钢铁(20) + { + var circle = sa.FastDp($"剑钢铁-{sid}-0", evt.SourcePosition(), 9000, new Vector2(20f)); + circle.ScaleMode = ScaleMode.ByTime; + sa.Method.SendDraw(DrawModeEnum.Default, DrawTypeEnum.Circle, circle); + break; + } + default: // 未知姿势 + Dbg(sa, $"舞动之剑9710:未知姿势{pose},不绘图"); + break; + } + } + + #endregion + + #region BOSS3 惧死者 + // 47465 魔具联动-爆炎 / 47468 古代爆炎:5.5s,施法者上18m危险圈 + [ScriptMethod(name: "BOSS3 - 爆炎", eventType: EventTypeEnum.StartCasting, eventCondition: ["ActionId:regex:^(47465|47468)$"])] + public void 爆炎(Event evt, ScriptAccessory sa) + { + Dbg(sa, $"爆炎({evt.ActionId()}):src {evt.SourceId():X8}"); + var dp = sa.Data.GetDefaultDrawProperties(); + dp.Name = $"爆炎-{evt.SourceId()}"; + dp.Color = sa.Data.DefaultDangerColor; + dp.Owner = evt.SourceId(); + dp.DestoryAt = 5500; + dp.Scale = new Vector2(18f); + dp.ScaleMode = ScaleMode.ByTime; + sa.Method.SendDraw(DrawModeEnum.Default, DrawTypeEnum.Circle, dp); + } + + // 47466 魔具联动-冰封 / 47469 古代冰封:5.5s,以施法者rotation为正面,前后左右各45长15宽的rect十字 + [ScriptMethod(name: "BOSS3 - 冰封十字", eventType: EventTypeEnum.StartCasting, eventCondition: ["ActionId:regex:^(47466|47469)$"])] + public void 冰封十字(Event evt, ScriptAccessory sa) + { + Dbg(sa, $"冰封十字({evt.ActionId()}):src {evt.SourceId():X8}"); + List<(float Rot, string Tag)> dirs = [(0f, "前"), (MathF.PI / 2, "左"), (MathF.PI, "后"), (-MathF.PI / 2, "右")]; + foreach (var (rot, tag) in dirs) + { + var dp = sa.Data.GetDefaultDrawProperties(); + dp.Name = $"冰封十字-{evt.SourceId()}-{tag}"; + dp.Color = sa.Data.DefaultDangerColor; + dp.Owner = evt.SourceId(); + dp.Rotation = rot; + dp.DestoryAt = 5500; + dp.Scale = new Vector2(15f, 45f); + // dp.ScaleMode = ScaleMode.ByTime; + sa.Method.SendDraw(DrawModeEnum.Default, DrawTypeEnum.Rect, dp); + } + } + + // 47471 古代暴雷:5.5s,SourcePosition上以SourceRotation为正面的45度60m扇形 + [ScriptMethod(name: "BOSS3 - 古代暴雷", eventType: EventTypeEnum.StartCasting, eventCondition: ["ActionId:47471"])] + public void 古代暴雷(Event evt, ScriptAccessory sa) + { + Dbg(sa, $"古代暴雷(47479):pos {evt.SourcePosition():F1} rot {evt.SourceRotation():F2}"); + var dp = sa.FastDp($"古代暴雷-{evt.SourceId()}", evt.SourcePosition(), 5500, new Vector2(60f)); + dp.Rotation = evt.SourceRotation(); + dp.Radian = 45f * MathF.PI / 180f; + sa.Method.SendDraw(DrawModeEnum.Default, DrawTypeEnum.Fan, dp); + } + + [ScriptMethod(name: "BOSS3 - 灭亡射线", eventType: EventTypeEnum.StartCasting, eventCondition: ["ActionId:47475"])] + public async void 骷髅头47475(Event evt, ScriptAccessory sa) + { + var srcId = evt.SourceId(); + var pos = evt.SourcePosition(); + + await Task.Delay(1000); + + // 0.5s后取施法者当前实时朝向,取不到则回退用事件快照 + var srcObj = sa.Data.Objects.SearchById(srcId); + var rot = srcObj?.Rotation ?? evt.SourceRotation(); + Dbg(sa, $"灭亡射线(47475):pos {pos:F1} rot {rot:F2}(实时:{srcObj != null})"); + + var dp = sa.FastDp($"灭亡射线-{srcId}", pos, 4000, new Vector2(6f, 30f)); + dp.Rotation = rot; + // dp.ScaleMode = ScaleMode.ByTime; + sa.Method.SendDraw(DrawModeEnum.Default, DrawTypeEnum.Rect, dp); + } + + // 47477 黑暗奔流:EffectPosition 沿 SourceRotation 的 60长10宽矩形,CreateOmen 689(rect omen:X=半宽 Z=全长) + [ScriptMethod(name: "BOSS3 - 黑暗奔流", eventType: EventTypeEnum.StartCasting, eventCondition: ["ActionId:47477"])] + public void B3黑暗奔流(Event evt, ScriptAccessory sa) + { + Dbg(sa, $"黑暗奔流(47477):pos {evt.EffectPosition():F1} rot {evt.SourceRotation():F2}"); + sa.Method.VfxMethod.CreateOmen(689, new Vector3(5f, 10f, 60f), + evt.EffectPosition(), evt.SourceRotation(), null, 4000); + } + + // 47477 读条开始后:左右两侧步进地火,60长10宽矩形,每轮向外步进10m,每轮画3.5s紧接下一轮 + [ScriptMethod(name: "BOSS3 - 黑暗奔流地火", eventType: EventTypeEnum.StartCasting, eventCondition: ["ActionId:47477"])] + public void B3黑暗奔流地火(Event evt, ScriptAccessory sa) + { + var pos = evt.EffectPosition(); + var rot = evt.SourceRotation(); + Dbg(sa, $"黑暗奔流地火(47477):pos {pos:F1} rot {rot:F2}"); + for (var i = 0; i < 2; i++) + { + foreach (var (sign, tag) in new[] { (1f, "左"), (-1f, "右") }) + { + var side = rot + sign * MathF.PI / 2; + var center = pos + new Vector3(MathF.Sin(side), 0f, MathF.Cos(side)) * (10f * (i + 1)); + var dp = sa.FastDp($"黑暗奔流地火-{tag}-{i}", center, 4500, new Vector2(10f, 60f)); + dp.Rotation = rot; + dp.Delay = (uint)(3500 + i * 2000); + sa.Method.SendDraw(DrawModeEnum.Default, DrawTypeEnum.Rect, dp); + } + } + } + + #endregion + + #region BOSS4 目录 + + // 48385 封印武器-竖琴:7s,15m危险圈;48387 封印武器-弓:7s,11m危险圈 + [ScriptMethod(name: "BOSS4 - 封印武器", eventType: EventTypeEnum.StartCasting, eventCondition: ["ActionId:regex:^(48385|48387)$"])] + public void 封印武器(Event evt, ScriptAccessory sa) + { + var isHarp = evt.ActionId() == 48385; + var radius = isHarp ? 15f : 11f; + Dbg(sa, $"封印武器-{(isHarp ? "竖琴" : "弓")}({evt.ActionId()}):pos {evt.EffectPosition():F1} 半径 {radius}"); + + var dp = sa.FastDp($"封印武器-{evt.SourceId()}", evt.EffectPosition(), 7000, new Vector2(radius)); + dp.ScaleMode = ScaleMode.ByTime; + sa.Method.SendDraw(DrawModeEnum.Default, DrawTypeEnum.Circle, dp); + } + + private static readonly Vector3 Boss4中心 = new(0f, -684f, -628f); + + private readonly object _zoneLock = new(); + private float? _fireZoneRot; // 2015240 火区朝向(上下对称) + private float? _waterZoneRot; // 2015241 水区朝向(上下对称) + private float? _thunderZoneRot; // 2015242 雷区朝向(上下对称) + private readonly List _fireballPositions = []; // 19301 火球,每波两个 + private readonly List _iceballPositions = []; // 19300 冰球,每波两个 + private readonly List _thunderballPositions = []; // 19302 雷球,每波两个 + + // 2015240 火区 / 2015241 水区 / 2015242 雷区:记录SourceRotation,rotation及其对面就是对应元素区 + [ScriptMethod(name: "BOSS4 - 火区记录", eventType: EventTypeEnum.ObjectChanged, eventCondition: ["Operate:Add", "DataId:2015240"], userControl: false)] + public void 火区记录(Event evt, ScriptAccessory sa) + { + lock (_zoneLock) _fireZoneRot = evt.SourceRotation(); + Dbg(sa, $"火区记录:rot {evt.SourceRotation():F2}"); + } + + [ScriptMethod(name: "BOSS4 - 水区记录", eventType: EventTypeEnum.ObjectChanged, eventCondition: ["Operate:Add", "DataId:2015241"], userControl: false)] + public void 水区记录(Event evt, ScriptAccessory sa) + { + lock (_zoneLock) _waterZoneRot = evt.SourceRotation(); + Dbg(sa, $"水区记录:rot {evt.SourceRotation():F2}"); + } + + [ScriptMethod(name: "BOSS4 - 雷区记录", eventType: EventTypeEnum.ObjectChanged, eventCondition: ["Operate:Add", "DataId:2015242"], userControl: false)] + public void 雷区记录(Event evt, ScriptAccessory sa) + { + lock (_zoneLock) _thunderZoneRot = evt.SourceRotation(); + Dbg(sa, $"雷区记录:rot {evt.SourceRotation():F2}"); + } + + // 元素球每波两个:收齐后按中心→球朝向顺时针转到对应区的角度算duration, + // 在上下对应区各画一个60度30m扇形;duration多于3s时只在最后3s显示 + [ScriptMethod(name: "BOSS4 - 火球扇形", eventType: EventTypeEnum.AddCombatant, eventCondition: ["DataId:19301"], userControl: false)] + public void 火球扇形(Event evt, ScriptAccessory sa) + => 元素球扇形(sa, _fireballPositions, () => _fireZoneRot, evt.SourceId(), evt.SourcePosition(), "火"); + + [ScriptMethod(name: "BOSS4 - 冰球扇形", eventType: EventTypeEnum.AddCombatant, eventCondition: ["DataId:19300"], userControl: false)] + public void 冰球扇形(Event evt, ScriptAccessory sa) + => 元素球扇形(sa, _iceballPositions, () => _waterZoneRot, evt.SourceId(), evt.SourcePosition(), "冰"); + + [ScriptMethod(name: "BOSS4 - 雷球扇形", eventType: EventTypeEnum.AddCombatant, eventCondition: ["DataId:19302"], userControl: false)] + public void 雷球扇形(Event evt, ScriptAccessory sa) + => 元素球扇形(sa, _thunderballPositions, () => _thunderZoneRot, evt.SourceId(), evt.SourcePosition(), "雷"); + + private readonly Dictionary _ballSeenAt = []; + + private void 元素球扇形(ScriptAccessory sa, List balls, Func getZoneRot, ulong sourceId, Vector3 pos, string tag) + { + float zoneRot; + lock (_zoneLock) + { + var now = DateTime.Now; + if (_ballSeenAt.TryGetValue(sourceId, out var seen) && (now - seen).TotalSeconds < 20) + { + Dbg(sa, $"{tag}球 {sourceId:X8} 为{(now - seen).TotalSeconds:F1}s内复读事件,忽略"); + return; + } + _ballSeenAt[sourceId] = now; + + if (balls.Any(p => DistXZ(p, pos) <= 1f)) return; + balls.Add(pos); + Dbg(sa, $"{tag}球记录 #{balls.Count}:{pos:F1}"); + if (balls.Count < 2) return; + + pos = balls[0]; + balls.Clear(); // 收齐一波即清空,等下一波 + + var rot = getZoneRot(); + if (rot is null) + { + Dbg(sa, $"{tag}球已收齐但{tag}区未记录,跳过"); + return; + } + zoneRot = rot.Value; + } + + // 中心→球的朝向;该角度约定逆时针增加,顺时针转动即角度减小;区域上下对称,对180度取模 + var ballRad = MathF.Atan2(pos.X - Boss4中心.X, pos.Z - Boss4中心.Z); + var deltaRad = ((ballRad - zoneRot) % MathF.PI + MathF.PI) % MathF.PI; + var deltaDeg = deltaRad * 180f / MathF.PI; + var duration = (uint)(7000 + deltaDeg / 30f * 1000f); + // duration多于3s时加延迟,只在最后3s显示扇形 + var delay = duration > 3000 ? duration - 3000 : 0; + var show = duration - delay; + Dbg(sa, $"{tag}球→{tag}区顺时针 {deltaDeg:F1}度,duration {duration}ms(延迟{delay}ms后显示{show}ms)"); + + foreach (var (rot, dirTag) in new[] { (zoneRot, "上"), (zoneRot + MathF.PI, "下") }) + { + var dp = sa.FastDp($"{tag}区扇形-{dirTag}", Boss4中心, show, new Vector2(30f)); + dp.Delay = delay; + dp.Rotation = rot; + dp.Radian = 60f * MathF.PI / 180f; + dp.ScaleMode = ScaleMode.ByTime; + sa.Method.SendDraw(DrawModeEnum.Imgui, DrawTypeEnum.Fan, dp); + } + } + // 圆环 2015243火 / 2015244水(视为冰属性) / 2015245雷:出现7s后AOE。 + [ScriptMethod(name: "BOSS4 - 元素圆环换区", eventType: EventTypeEnum.ObjectChanged, eventCondition: ["Operate:Add", "DataId:regex:^(201524[345])$"], suppress: 1000)] + public void 元素圆环换区(Event evt, ScriptAccessory sa) + { + if (Is超魔(sa)) return; // 普魔专用,超魔由下方方法处理(间隔更短) + if (元素创造中()) return; // 元素创造期间由安全区指路接管 + _ = 元素圆环换区核心(evt, sa, 5000); + } + + private async Task 元素圆环换区核心(Event evt, ScriptAccessory sa, int waitMs) + { + if (!uint.TryParse(evt["DataId"], out var dataId)) return; + var ringElement = dataId switch + { + 2015243u => "火", + 2015244u => "冰", + _ => "雷" + }; + Dbg(sa, $"圆环出现:{ringElement}({dataId}),7s后AOE"); + + await Task.Delay(waitMs); + + var sectors = 元素扇区中心(); + if (sectors is null) + { + Dbg(sa, $"圆环换区:元素区未记录齐,无法判断"); + return; + } + + var mePos = sa.Data.MyObject?.Position ?? Boss4中心; + var meRad = MathF.Atan2(mePos.X - Boss4中心.X, mePos.Z - Boss4中心.Z); + var mine = sectors.OrderBy(s => MathF.Abs(WrapPi(s.Rot - meRad))).First(); + if (mine.Element != ringElement) + { + Dbg(sa, $"圆环{ringElement},自己站在{mine.Element}区,忽略"); + return; + } + + // 自己区左右两条交界取离自己近的一条,向邻区偏10度,中心往外9m + var neighbors = sectors + .Where(s => s.Element != mine.Element) + .Select(s => (s.Element, Delta: WrapPi(s.Rot - mine.Rot))) + .ToList(); + var pick = WrapPi(meRad - mine.Rot) >= 0 + ? neighbors.Where(n => n.Delta > 0).OrderBy(n => n.Delta).First() + : neighbors.Where(n => n.Delta < 0).OrderByDescending(n => n.Delta).First(); + var boundary = mine.Rot + pick.Delta / 2f; + var guide = boundary + 10f * MathF.PI / 180f * MathF.Sign(pick.Delta); + var guidePos = Boss4中心 + new Vector3(MathF.Sin(guide), 0, MathF.Cos(guide)) * 9f; + + var showMs = (uint)Math.Max(7000 - waitMs, 1000); + Dbg(sa, $"圆环{ringElement}命中所在区,指路穿到{pick.Element}区交界:{guidePos:F1}"); + sa.Method.SendDraw(DrawModeEnum.Imgui, DrawTypeEnum.Displacement, + sa.WaypointDp(guidePos, showMs, 0, "元素圆环穿区指路")); + + sa.Method.TTS("穿", 3); + sa.Method.TextInfo("穿", (int)showMs, false); + } + + // 6个扇区中心朝向(每元素上下各一个);任一元素区未记录则返回null + private List<(float Rot, string Element)>? 元素扇区中心() + { + lock (_zoneLock) + { + if (_fireZoneRot is null || _waterZoneRot is null || _thunderZoneRot is null) return null; + List<(float Rot, string Element)> sectors = []; + foreach (var (rot, elem) in new[] { (_fireZoneRot.Value, "火"), (_waterZoneRot.Value, "冰"), (_thunderZoneRot.Value, "雷") }) + { + sectors.Add((rot, elem)); + sectors.Add((rot + MathF.PI, elem)); + } + return sectors; + } + } + + private static float WrapPi(float rad) + { + rad %= 2f * MathF.PI; + if (rad > MathF.PI) rad -= 2f * MathF.PI; + if (rad < -MathF.PI) rad += 2f * MathF.PI; + return rad; + } + + #endregion + + // 超魔 + #region 超魔Boss 1 双头 + + // 47639 蓝头剧毒吐息:9s,EffectPosition 18m危险圈 + [ScriptMethod(name: "超魔BOSS1 - 蓝头剧毒吐息", eventType: EventTypeEnum.StartCasting, eventCondition: ["ActionId:47639"])] + public void 超魔蓝头剧毒吐息(Event evt, ScriptAccessory sa) + { + var dp = sa.FastDp("超魔蓝头剧毒吐息", evt.EffectPosition(), 9000, new Vector2(18f)); + dp.ScaleMode = ScaleMode.ByTime; + sa.Method.SendDraw(DrawModeEnum.Default, DrawTypeEnum.Circle, dp); + } + + // 47638 绿头风暴吐息:9s,EffectPosition 上 CreateOmen 386 + [ScriptMethod(name: "超魔BOSS1 - 绿头风暴吐息", eventType: EventTypeEnum.StartCasting, eventCondition: ["ActionId:47638"])] + public void 超魔绿头风暴吐息(Event evt, ScriptAccessory sa) + { + sa.Method.VfxMethod.CreateOmen(530, new Vector3(30f), + evt.EffectPosition(), evt.SourceRotation(), new Vector4(1f,1f,1f,0.2f), 8000); + } + + // 47640 绿头雷电赋格:9s,EffectPosition 月环 内15外60 + [ScriptMethod(name: "超魔BOSS1 - 绿头雷电赋格", eventType: EventTypeEnum.StartCasting, eventCondition: ["ActionId:47640"])] + public void 超魔绿头雷电赋格(Event evt, ScriptAccessory sa) + { + var dp = sa.FastDp("超魔绿头雷电赋格", evt.EffectPosition(), 9000, new Vector2(60f)); + dp.InnerScale = new Vector2(18f); + dp.Radian = float.Pi * 2; + sa.Method.SendDraw(DrawModeEnum.Default, DrawTypeEnum.Donut, dp); + } + + // 47641 蓝头冰柱赋格:9s,EffectPosition 20m危险圈,Imgui + [ScriptMethod(name: "超魔BOSS1 - 蓝头冰柱赋格", eventType: EventTypeEnum.StartCasting, eventCondition: ["ActionId:47641"])] + public void 超魔蓝头冰柱赋格(Event evt, ScriptAccessory sa) + { + var dp = sa.FastDp("超魔蓝头冰柱赋格", evt.EffectPosition(), 9000, new Vector2(20f)); + dp.Color = new Vector4(1f, 0f, 0f, 1f); + dp.ScaleMode = ScaleMode.ByTime; + sa.Method.SendDraw(DrawModeEnum.Imgui, DrawTypeEnum.Circle, dp); + } + + // 47685 冰焰交错:2s,SourcePosition上以SourceRotation为正面,前后左右各35长10宽的rect十字 + [ScriptMethod(name: "超魔BOSS1 - 冰焰交错", eventType: EventTypeEnum.StartCasting, eventCondition: ["ActionId:47685"])] + public void 冰焰交错(Event evt, ScriptAccessory sa) + { + Dbg(sa, $"冰焰交错(47685):pos {evt.SourcePosition():F1} rot {evt.SourceRotation():F2}"); + List<(float Rot, string Tag)> dirs = [(0f, "前"), (MathF.PI / 2, "左"), (MathF.PI, "后"), (-MathF.PI / 2, "右")]; + foreach (var (rot, tag) in dirs) + { + var dp = sa.FastDp($"冰焰交错-{tag}", evt.SourcePosition(), 2000, new Vector2(10f, 35f)); + dp.Rotation = evt.SourceRotation() + rot; + // dp.ScaleMode = ScaleMode.ByTime; + sa.Method.SendDraw(DrawModeEnum.Default, DrawTypeEnum.Rect, dp); + } + } + + // 47686 冰焰凝环:2s,SourcePosition 月环 内10外60 + [ScriptMethod(name: "超魔BOSS1 - 冰焰凝环", eventType: EventTypeEnum.StartCasting, eventCondition: ["ActionId:47686"])] + public void 超魔冰焰凝环(Event evt, ScriptAccessory sa) + { + var dp = sa.FastDp("超魔冰焰凝环", evt.SourcePosition(), 2000, new Vector2(60f)); + dp.InnerScale = new Vector2(5f); + dp.Radian = float.Pi * 2; + sa.Method.SendDraw(DrawModeEnum.Default, DrawTypeEnum.Donut, dp); + } + + private const uint 小绿头DataId = 19490; + private const uint 小蓝头DataId = 19491; + private readonly object _smallHeadLock = new(); + private readonly List<(uint SourceId, uint DataId)> _smallHeads = []; // 19491蓝 / 19490绿 小双头 + + // AddCombatant DataId 19491(蓝) and 19490 (绿),记录SourceId and DataId list + [ScriptMethod(name: "超魔BOSS1 - 魔法阵记录", eventType: EventTypeEnum.AddCombatant, eventCondition: ["DataId:regex:^(1949[01])$"], userControl: false)] + public void 超魔小双头记录(Event evt, ScriptAccessory sa) + { + if (!uint.TryParse(evt["DataId"], out var dataId)) return; + var sid = evt.SourceId(); + lock (_smallHeadLock) + { + if (_smallHeads.Any(h => h.SourceId == sid)) return; + _smallHeads.Add((sid, dataId)); + Dbg(sa, $"小双头记录 #{_smallHeads.Count}:{(dataId == 小蓝头DataId ? "蓝" : "绿")} {sid:X8}"); + } + } + + [ScriptMethod(name: "超魔BOSS1 - 小双头恐惧", eventType: EventTypeEnum.StartCasting, eventCondition: ["ActionId:47703"])] + public void 超魔小双头恐惧(Event evt, ScriptAccessory sa) + { + var dp = sa.FastDp("双头恐惧", evt.TargetPosition, 7000, new Vector2(10f, 40f)); + dp.Rotation = evt.SourceRotation(); + // dp.ScaleMode = ScaleMode.ByTime; + sa.Method.SendDraw(DrawModeEnum.Default, DrawTypeEnum.Rect, dp); + } + + // 47702 大双头恐惧:SourceName判色(Green/绿 或 Blue/蓝), + // 在已记录的对应颜色小双头身上以其rotation为正,画正反两个rect 60长5宽 6s + [ScriptMethod(name: "超魔BOSS1 - 大双头恐惧", eventType: EventTypeEnum.StartCasting, eventCondition: ["ActionId:47702"])] + public void 超魔大双头恐惧(Event evt, ScriptAccessory sa) + { + var dp = sa.FastDp("双头恐惧", evt.TargetPosition, 7000, new Vector2(20f, 40f)); + dp.Rotation = evt.SourceRotation(); + // dp.ScaleMode = ScaleMode.ByTime; + sa.Method.SendDraw(DrawModeEnum.Default, DrawTypeEnum.Rect, dp); + + var srcName = evt["SourceName"] ?? ""; + uint? headDataId = null; + if (srcName.Contains("Green", StringComparison.OrdinalIgnoreCase) || srcName.Contains('绿')) + headDataId = 小绿头DataId; + else if (srcName.Contains("Blue", StringComparison.OrdinalIgnoreCase) || srcName.Contains('蓝')) + headDataId = 小蓝头DataId; + if (headDataId is null) + { + Dbg(sa, $"大双头恐惧(47702):SourceName [{srcName}] 无法判色,跳过小双头直线"); + return; + } + + List heads; + lock (_smallHeadLock) + { + heads = _smallHeads.Where(h => h.DataId == headDataId).Select(h => h.SourceId).ToList(); + _smallHeads.Clear(); // 用完清空以便下一波重新记录 + } + Dbg(sa, $"大双头恐惧(47702):{(headDataId == 小蓝头DataId ? "蓝" : "绿")}色小双头 {heads.Count} 个"); + + foreach (var sid in heads) + { + foreach (var (extraRot, tag) in new[] { (0f, "正"), (MathF.PI, "反") }) + { + var lineDp = sa.Data.GetDefaultDrawProperties(); + lineDp.Name = $"大双头恐惧直线-{sid:X8}-{tag}"; + lineDp.Color = sa.Data.DefaultDangerColor; + lineDp.Owner = sid; + lineDp.Rotation = extraRot; + lineDp.DestoryAt = 7000; + lineDp.Scale = new Vector2(5f, 60f); + // lineDp.ScaleMode = ScaleMode.ByTime; + sa.Method.SendDraw(DrawModeEnum.Default, DrawTypeEnum.Rect, lineDp); + } + } + } + + private const uint 大绿头DataId = 19483; // 绿=雷 + private const uint 大蓝头DataId = 19484; // 蓝=冰 + private readonly object _conduitLock = new(); + private readonly List<(bool IsThunder, Vector3 Pos, float Rot)> _conduitBalls = []; // 19487导流雷球 / 19488导流冰球 + private readonly Dictionary _mahjongTethers = []; // Tether 019B:TargetId → 是否绿头(雷) + private readonly List<(bool IsGreen, Vector3 Pos, int Index, long At)> _mahjongMarks = []; // 麻将点名记录:头颜色/点名者位置/麻将几 + private readonly List<(Vector3 Pos, float Rot, long At)> _mahjongR1Fans = []; // 麻将1轮命中的导流球扇形,用于四点安全区判断 + private static readonly Vector3 麻将中心 = new(-900f, -980f, 700f); // 麻将四点指路:中心±5四角 + + // AddCombatant 19487导流雷球 / 19488导流冰球:记录属性、SourcePos、SourceRotation + [ScriptMethod(name: "超魔BOSS1 - 导流球记录", eventType: EventTypeEnum.AddCombatant, eventCondition: ["DataId:regex:^(1948[78])$"], userControl: false)] + public void 超魔导流球记录(Event evt, ScriptAccessory sa) + { + if (!uint.TryParse(evt["DataId"], out var dataId)) return; + var isThunder = dataId == 19487u; + var pos = evt.SourcePosition(); + var rot = evt.SourceRotation(); + lock (_conduitLock) + { + if (_conduitBalls.Any(b => DistXZ(b.Pos, pos) < 1f)) return; + _conduitBalls.Add((isThunder, pos, rot)); + Dbg(sa, $"导流{(isThunder ? "雷" : "冰")}球记录 #{_conduitBalls.Count}:pos {pos:F1} rot {rot:F2}"); + } + } + + // Tether 019B:SourceId是大蓝头19484或大绿头19483,记录TargetId对应的头颜色。一次机制两条线,一蓝一绿 + [ScriptMethod(name: "超魔BOSS1 - 麻将连线记录", eventType: EventTypeEnum.Tether, eventCondition: ["Id:019B"], userControl: false)] + public void 超魔麻将连线记录(Event evt, ScriptAccessory sa) + { + var srcObj = sa.Data.Objects.SearchById(evt.SourceId()); + if (srcObj is null || (srcObj.DataId != 大绿头DataId && srcObj.DataId != 大蓝头DataId)) + { + Dbg(sa, $"麻将连线(019B):SourceId {evt.SourceId():X8} 不是大双头,忽略"); + return; + } + var isGreen = srcObj.DataId == 大绿头DataId; + lock (_conduitLock) _mahjongTethers[evt.TargetId()] = isGreen; + Dbg(sa, $"麻将连线(019B):{(isGreen ? "绿" : "蓝")}头 → {evt.TargetId():X8}"); + } + + // TargetIcon 02D2~02D5 麻将1~4,各触发两次(一蓝一绿): + // 麻将1立即画15s;麻将2/3/4等15s后画4s。 + // 点名者位置画15m危险圈;15m内同属性导流球(绿头=雷19487/蓝头=冰19488)画60度60m扇形并从list删除 + [ScriptMethod(name: "超魔BOSS1 - 麻将1", eventType: EventTypeEnum.TargetIcon, eventCondition: ["Id:02D2"])] + public void 超魔麻将1(Event evt, ScriptAccessory sa) => 麻将处理(evt, sa, 1, 0, 17000); + + [ScriptMethod(name: "超魔BOSS1 - 麻将2", eventType: EventTypeEnum.TargetIcon, eventCondition: ["Id:02D3"])] + public void 超魔麻将2(Event evt, ScriptAccessory sa) => 麻将处理(evt, sa, 2, 12500, 4000); + + [ScriptMethod(name: "超魔BOSS1 - 麻将3", eventType: EventTypeEnum.TargetIcon, eventCondition: ["Id:02D4"])] + public void 超魔麻将3(Event evt, ScriptAccessory sa) => 麻将处理(evt, sa, 3, 12000, 4000); + + [ScriptMethod(name: "超魔BOSS1 - 麻将4", eventType: EventTypeEnum.TargetIcon, eventCondition: ["Id:02D5"])] + public void 超魔麻将4(Event evt, ScriptAccessory sa) => 麻将处理(evt, sa, 4, 11500, 4000); + + private async void 麻将处理(Event evt, ScriptAccessory sa, int index, int delayMs, uint duration) + { + var sid = evt.SourceId(); + bool isGreen; + bool found; + lock (_conduitLock) found = _mahjongTethers.TryGetValue(sid, out isGreen); + if (!found) + { + Dbg(sa, $"麻将{index}:{sid:X8} 没有对应的连线记录,跳过"); + return; + } + + var obj = sa.Data.Objects.SearchById(sid); + var pos = obj?.Position ?? evt.SourcePosition(); + lock (_conduitLock) + { + _mahjongMarks.RemoveAll(m => Environment.TickCount64 - m.At > 60000); + _mahjongMarks.Add((isGreen, pos, index, Environment.TickCount64)); + } + Dbg(sa, $"麻将{index}记录:{(isGreen ? "绿(雷)" : "蓝(冰)")} {sid:X8} pos {pos:F1}"); + + if (delayMs > 0) await Task.Delay(delayMs); + + var dp = sa.FastDp($"麻将{index}圈-{sid:X8}", pos, duration, new Vector2(15f)); + // dp.ScaleMode = ScaleMode.ByTime; + sa.Method.SendDraw(DrawModeEnum.Default, DrawTypeEnum.Circle, dp); + + // 15m内同属性导流球画扇形并删除 + List<(bool IsThunder, Vector3 Pos, float Rot)> hit; + lock (_conduitLock) + { + hit = _conduitBalls.Where(b => b.IsThunder == isGreen && DistXZ(b.Pos, pos) <= 15f).ToList(); + _conduitBalls.RemoveAll(b => b.IsThunder == isGreen && DistXZ(b.Pos, pos) <= 15f); + } + Dbg(sa, $"麻将{index}:命中导流{(isGreen ? "雷" : "冰")}球 {hit.Count} 个"); + + if (index == 1) + lock (_conduitLock) + { + _mahjongR1Fans.RemoveAll(f => Environment.TickCount64 - f.At > 60000); + _mahjongR1Fans.AddRange(hit.Select(h => (h.Pos, h.Rot, Environment.TickCount64))); + } + + for (var i = 0; i < hit.Count; i++) + { + var fan = sa.FastDp($"麻将{index}扇形-{i}", hit[i].Pos, duration, new Vector2(60f)); + fan.Rotation = hit[i].Rot; + fan.Radian = 50f * MathF.PI / 180f; + // fan.ScaleMode = ScaleMode.ByTime; + sa.Method.SendDraw(DrawModeEnum.Default, DrawTypeEnum.Fan, fan); + } + } + + // —— 麻将四点指路 —— + [ScriptMethod(name: "超魔BOSS1 - 麻将指路", eventType: EventTypeEnum.TargetIcon, eventCondition: ["Id:02D3"], userControl: false, suppress: 5000)] + public async void 超魔麻将指路(Event evt, ScriptAccessory sa) + { + List<(bool IsGreen, Vector3 Pos, int Index, long At)> marks = []; + for (var i = 0; i < 30 && marks.Count == 0; i++) + { + lock (_conduitLock) + { + var now = Environment.TickCount64; + var fresh = _mahjongMarks.Where(m => now - m.At < 30000).ToList(); + if (fresh.Count(m => m.Index == 1) >= 2 && fresh.Count(m => m.Index == 2) >= 2) marks = fresh; + } + if (marks.Count == 0) await Task.Delay(100); + } + if (marks.Count == 0) + { + Dbg(sa, "麻将指路:3s内麻将1/2点名记录不齐,跳过"); + return; + } + + List<(Vector3 Pos, float Rot)> fans; + lock (_conduitLock) + fans = _mahjongR1Fans.Where(f => Environment.TickCount64 - f.At < 30000) + .Select(f => (f.Pos, f.Rot)).ToList(); + + var r1 = marks.Where(m => m.Index == 1).ToList(); + var r2 = marks.Where(m => m.Index == 2).ToList(); + + // 首轮安全角 = 不被麻将1的15m圈覆盖、也不在其扇形(60m/50°)内的那个角 + Vector2[] cornerVecs = [new(-5, -5), new(5, -5), new(-5, 5), new(5, 5)]; + Vector3 角点(Vector2 v) => 麻将中心 + new Vector3(v.X, 0, v.Y); + string 角名(Vector2 v) => (v.X < 0 ? "左" : "右") + (v.Y < 0 ? "上" : "下"); + bool Covered(Vector3 c) => + r1.Any(m => DistXZ(m.Pos, c) <= 15f) || + fans.Any(f => DistXZ(f.Pos, c) <= 60f && + MathF.Abs(WrapPi(MathF.Atan2(c.X - f.Pos.X, c.Z - f.Pos.Z) - f.Rot)) <= 25f * MathF.PI / 180f); + var safeVecs = cornerVecs.Where(v => !Covered(角点(v))).ToList(); + if (safeVecs.Count == 0) + { + Dbg(sa, $"麻将指路:四角全被覆盖(圈{r1.Count} 扇{fans.Count}),跳过"); + return; + } + if (safeVecs.Count > 1) + Dbg(sa, $"麻将指路:安全角不唯一({string.Join("/", safeVecs.Select(角名))}),取第一个"); + var v1 = safeVecs[0]; + + // 同色点名左右是否换边;两色判定不一致时以绿头为准 + bool? 换边(bool green) + { + var a = r1.Where(m => m.IsGreen == green).ToList(); + var b = r2.Where(m => m.IsGreen == green).ToList(); + if (a.Count == 0 || b.Count == 0) return null; + return (a[^1].Pos.X < 麻将中心.X) != (b[^1].Pos.X < 麻将中心.X); + } + var 绿换 = 换边(true); + var 蓝换 = 换边(false); + if (绿换 is null && 蓝换 is null) + { + Dbg(sa, "麻将指路:两色点名记录不全,无法判断交叉/平行,跳过"); + return; + } + if (绿换 is not null && 蓝换 is not null && 绿换 != 蓝换) + Dbg(sa, $"麻将指路:两色换边判定不一致 绿{绿换} 蓝{蓝换},以绿头为准"); + var 交叉 = 绿换 ?? 蓝换!.Value; + + // 交叉→左右翻,平行→前后翻;叉积定顺逆,之后按同向每轮转90° + var v2 = 交叉 ? new Vector2(-v1.X, v1.Y) : new Vector2(v1.X, -v1.Y); + var cw = v1.X * v2.Y - v1.Y * v2.X > 0; + Vector2 下一角(Vector2 v) => cw ? new Vector2(-v.Y, v.X) : new Vector2(v.Y, -v.X); + var v3 = 下一角(v2); + var v4 = 下一角(v3); + + Dbg(sa, $"麻将指路:{(交叉 ? "交叉" : "平行")},{角名(v1)}→{角名(v2)}→{角名(v3)}→{角名(v4)},{(cw ? "顺" : "逆")}时针"); + sa.Method.TextInfo($"初始安全区{角名(v1)},{(cw ? "顺" : "逆")}时针跑", 15000, false); + + (Vector2 V, uint Dur, uint Delay)[] wps = [(v1, 12500, 0), (v2, 4000, 12500), (v3, 4000, 16500), (v4, 4000, 20500)]; + for (var i = 0; i < wps.Length; i++) + sa.Method.SendDraw(DrawModeEnum.Imgui, DrawTypeEnum.Displacement, + sa.WaypointDp(角点(wps[i].V), wps[i].Dur, wps[i].Delay, $"麻将指路-{i + 1}")); + } + + // 赋格触发对应属性魔法阵(19490绿=雷/19491蓝=冰):正反两个rect 60长5宽,只清除被影响颜色的记录 + private void 触发魔法阵直线(ScriptAccessory sa, uint headDataId, string name, uint delay, uint duration) + { + List heads; + lock (_smallHeadLock) + { + heads = _smallHeads.Where(h => h.DataId == headDataId).Select(h => h.SourceId).ToList(); + _smallHeads.RemoveAll(h => h.DataId == headDataId); // 只清除对应颜色 + } + + // 兜底:AddCombatant大量同帧刷新时事件可能被丢,扫一遍对象表补上漏记的 + var missed = sa.Data.Objects + .Where(o => o != null && o.DataId == headDataId && !heads.Contains(o.EntityId)) + .Select(o => o.EntityId) + .ToList(); + if (missed.Count > 0) + { + heads.AddRange(missed); + Dbg(sa, $"{name}:对象表补漏 {missed.Count} 个 [{string.Join(" ", missed.Select(m => m.ToString("X8")))}]"); + } + Dbg(sa, $"{name}:{(headDataId == 小蓝头DataId ? "蓝" : "绿")}色魔法阵 {heads.Count} 个"); + + foreach (var sid in heads) + { + foreach (var (extraRot, tag) in new[] { (0f, "正"), (MathF.PI, "反") }) + { + var dp = sa.Data.GetDefaultDrawProperties(); + dp.Name = $"{name}直线-{sid:X8}-{tag}"; + dp.Color = sa.Data.DefaultDangerColor; + dp.Owner = sid; + dp.Rotation = extraRot; + dp.Delay = delay; + dp.DestoryAt = duration; + dp.Scale = new Vector2(5f, 60f); + // dp.ScaleMode = ScaleMode.ByTime; + sa.Method.SendDraw(DrawModeEnum.Default, DrawTypeEnum.Rect, dp); + } + } + } + + // 50727 前雷电赋格:11s,EffectPosition 月环 内15外60 + 绿色魔法阵直线11s + [ScriptMethod(name: "超魔BOSS1 - 前雷电赋格", eventType: EventTypeEnum.StartCasting, eventCondition: ["ActionId:50727"])] + public void 超魔前雷电赋格(Event evt, ScriptAccessory sa) + { + var dp = sa.FastDp("超魔前雷电赋格", evt.EffectPosition(), 11000, new Vector2(60f)); + dp.InnerScale = new Vector2(18f); + dp.Radian = float.Pi * 2; + sa.Method.SendDraw(DrawModeEnum.Default, DrawTypeEnum.Donut, dp); + + 触发魔法阵直线(sa, 小绿头DataId, "前雷电赋格", 0, 11000); + } + + // 50728 前冰柱赋格:11s,EffectPosition 钢铁 20m + 蓝色魔法阵直线11s + [ScriptMethod(name: "超魔BOSS1 - 前冰柱赋格", eventType: EventTypeEnum.StartCasting, eventCondition: ["ActionId:50728"])] + public void 超魔前冰柱赋格(Event evt, ScriptAccessory sa) + { + var dp = sa.FastDp("超魔前冰柱赋格", evt.EffectPosition(), 11000, new Vector2(20f)); + dp.ScaleMode = ScaleMode.ByTime; + sa.Method.SendDraw(DrawModeEnum.Default, DrawTypeEnum.Circle, dp); + + 触发魔法阵直线(sa, 小蓝头DataId, "前冰柱赋格", 0, 11000); + } + + // 47629 后雷电赋格:延迟11s后显示4s,EffectPosition 月环 内15外60 + 绿色魔法阵直线(同延迟) + [ScriptMethod(name: "超魔BOSS1 - 后雷电赋格", eventType: EventTypeEnum.StartCasting, eventCondition: ["ActionId:47629"])] + public void 超魔后雷电赋格(Event evt, ScriptAccessory sa) + { + var dp = sa.FastDp("超魔后雷电赋格", evt.EffectPosition(), 4000, new Vector2(60f)); + dp.Delay = 7000; + dp.InnerScale = new Vector2(18f); + dp.Radian = float.Pi * 2; + sa.Method.SendDraw(DrawModeEnum.Default, DrawTypeEnum.Donut, dp); + + 触发魔法阵直线(sa, 小绿头DataId, "后雷电赋格", 7000, 4000); + } + + // 47630 后冰柱赋格:延迟11s后显示4s,EffectPosition 钢铁 20m + 蓝色魔法阵直线(同延迟) + [ScriptMethod(name: "超魔BOSS1 - 后冰柱赋格", eventType: EventTypeEnum.StartCasting, eventCondition: ["ActionId:47630"])] + public void 超魔后冰柱赋格(Event evt, ScriptAccessory sa) + { + var dp = sa.FastDp("超魔后冰柱赋格", evt.EffectPosition(), 4000, new Vector2(20f)); + dp.Delay = 7000; + dp.ScaleMode = ScaleMode.ByTime; + sa.Method.SendDraw(DrawModeEnum.Default, DrawTypeEnum.Circle, dp); + + 触发魔法阵直线(sa, 小蓝头DataId, "后冰柱赋格", 7000, 4000); + } + + // —— 位移吐息:自己身上的status决定方向 —— + private static int B1位移吐息时长(uint actionId) => actionId == 50708 ? 6000 : 11000; + + // SourceName判色(Green/绿 或 Blue/蓝);统一只画最后3s,只有delay随读条长短不同 + [ScriptMethod(name: "超魔BOSS1 - 位移吐息", eventType: EventTypeEnum.StartCasting, eventCondition: ["ActionId:regex:^5070[6-8]$"])] + public async void 超魔B1位移吐息(Event evt, ScriptAccessory sa) + { + var actionId = evt.ActionId(); + const int duration = 3000; + await Task.Delay(B1位移吐息时长(actionId) - duration); + + var me = sa.Data.MyObject; + if (me is null) return; + + var srcName = evt["SourceName"] ?? ""; + var isGreen = srcName.Contains("Green", StringComparison.OrdinalIgnoreCase) || srcName.Contains('绿'); + var isBlue = srcName.Contains("Blue", StringComparison.OrdinalIgnoreCase) || srcName.Contains('蓝'); + + uint statusId = 0; + if (isBlue) statusId = me.HasStatus(5054) ? 5054u : me.HasStatus(5055) ? 5055u : 0u; + else if (isGreen) statusId = me.HasStatus(5052) ? 5052u : me.HasStatus(5053) ? 5053u : 0u; + if (statusId == 0) + { + Dbg(sa, $"位移吐息({actionId}):SourceName [{srcName}] 蓝{isBlue} 绿{isGreen},无对应status,跳过"); + return; + } + + var isLeft = statusId is 5054 or 5052; + var pos = isLeft ? new Vector3(-880f, -980f, 700f) : new Vector3(-920f, -980f, 700f); + var rot = isLeft ? -MathF.PI / 2 : MathF.PI / 2; + Dbg(sa, $"位移吐息({actionId}+{statusId}):{(isBlue ? "蓝头" : "绿头")}向{(isLeft ? "左" : "右")}位移"); + sa.Method.VfxMethod.CreateOmen(314, new Vector3(20f, 10f, 40f), + pos, rot, new Vector4(1f, 1f, 1f, 0.3f), duration); + } + + // —— 十字/月环连招预告 —— + // 绿头47671~47674 / 蓝头47675~47678,各自含两轮:两次十字/两次月环/十字月环/月环十字。 + // 先读条的头占第1、3轮,后读条的头占第2、4轮,两头都到齐后TextInfo汇总四轮。 + // 自己带哪个头的位移吐息buff(绿5052/5053、蓝5054/5055),该头的轮次前加[击退] + private readonly object _b1ComboLock = new(); + private (bool IsGreen, string[] Skills, long At)? _b1ComboFirst; + + [ScriptMethod(name: "超魔BOSS1 - 十字月环连招预告", eventType: EventTypeEnum.StartCasting, eventCondition: ["ActionId:regex:^4767[1-8]$"])] + public void 超魔B1十字月环预告(Event evt, ScriptAccessory sa) + { + var actionId = evt.ActionId(); + var isGreen = actionId <= 47674; + string[] skills = (actionId - (isGreen ? 47671u : 47675u)) switch + { + 0 => ["十字", "十字"], + 1 => ["月环", "月环"], + 2 => ["十字", "月环"], + _ => ["月环", "十字"], + }; + + (bool IsGreen, string[] Skills, long At) first; + lock (_b1ComboLock) + { + var now = Environment.TickCount64; + // 没有先手记录、记录过期、或同一头重复读条:当作本波先手,等另一头 + if (_b1ComboFirst is not { } f || now - f.At > 30000 || f.IsGreen == isGreen) + { + _b1ComboFirst = (isGreen, skills, now); + Dbg(sa, $"十字月环({actionId}):{(isGreen ? "绿" : "蓝")}头先手 {skills[0]}+{skills[1]},等另一头"); + return; + } + first = f; + _b1ComboFirst = null; + } + + var me = sa.Data.MyObject; + var 绿击退 = me is not null && (me.HasStatus(5052) || me.HasStatus(5053)); + var 蓝击退 = me is not null && (me.HasStatus(5054) || me.HasStatus(5055)); + string Mark(bool green, string skill) => (green ? 绿击退 : 蓝击退) ? $"[击退]{skill}" : skill; + + var text = string.Join("→", new[] + { + Mark(first.IsGreen, first.Skills[0]), + Mark(isGreen, skills[0]), + Mark(first.IsGreen, first.Skills[1]), + Mark(isGreen, skills[1]), + }); + Dbg(sa, $"十字月环({actionId}):{(isGreen ? "绿" : "蓝")}头后手,汇总 {text}"); + sa.Method.TextInfo(text, 20000, false); + } + #endregion + + #region 超魔BOSS2 + + private readonly object _b2ChargeLock = new(); + private readonly List<(uint Sid, Vector3 Pos, float Rot, long At)> _b2剑Casts = []; // 49645秘法剑读条记录,用于突进配对 + + private static float AngleDiff(float a, float b) => MathF.Abs(MathF.Atan2(MathF.Sin(a - b), MathF.Cos(a - b))); + + // 49622:3.5s。若有面向刚好相对的秘法剑(49645)读条者,长度=到它的距离-4,否则48 + [ScriptMethod(name: "超魔BOSS2 - 突进", eventType: EventTypeEnum.StartCasting, eventCondition: ["ActionId:49622"])] + public async void 超魔B2突进(Event evt, ScriptAccessory sa) + { + var sid = evt.SourceId(); + var pos = evt.SourcePosition(); + var rot = evt.SourceRotation(); + + void Draw(float length, uint duration) + { + foreach (var (extraRot, tag) in new[] { (0f, "正"), (MathF.PI, "反") }) + { + var dp = sa.FastDp($"超魔B2突进-{sid}-{tag}", pos, duration, new Vector2(7f, length)); + dp.Rotation = rot + extraRot; + sa.Method.SendDraw(DrawModeEnum.Default, DrawTypeEnum.Rect, dp); + } + } + + await Task.Delay(300); + + var length = 48f; + lock (_b2ChargeLock) + { + var now = Environment.TickCount64; + _b2剑Casts.RemoveAll(c => now - c.At > 8000); + var idx = _b2剑Casts.FindIndex(c => AngleDiff(c.Rot, rot + MathF.PI) < 0.3f); + if (idx >= 0) + { + length = DistXZ(pos, _b2剑Casts[idx].Pos) - 5; + Dbg(sa, $"突进(49622):{sid:X8} 与秘法剑 {_b2剑Casts[idx].Sid:X8} 面向相对,长度 {length:F1}"); + _b2剑Casts.RemoveAt(idx); + } + else Dbg(sa, $"突进(49622):{sid:X8} 没有面向相对的秘法剑,长度48"); + } + Draw(length, 3200); + } + + // 49635:3.5s + [ScriptMethod(name: "超魔BOSS2 - 小回旋", eventType: EventTypeEnum.StartCasting, eventCondition: ["ActionId:49635"])] + public void 超魔B2小回旋(Event evt, ScriptAccessory sa) + { + var dp = sa.FastDp($"超魔B2小回旋-{evt.SourceId()}", evt.SourcePosition(), 3500, new Vector2(14f)); + dp.InnerScale = new Vector2(9f); + dp.Rotation = evt.SourceRotation() - MathF.PI / 4; + dp.Radian = float.Pi / 2; + sa.Method.SendDraw(DrawModeEnum.Default, DrawTypeEnum.Donut, dp); + } + + [ScriptMethod(name: "超魔BOSS2 - 中回旋", eventType: EventTypeEnum.StartCasting, eventCondition: ["ActionId:49636"])] + public void 超魔B2中回旋(Event evt, ScriptAccessory sa) + { + var dp = sa.FastDp($"超魔B2中回旋-{evt.SourceId()}", evt.SourcePosition(), 3500, new Vector2(19f)); + dp.InnerScale = new Vector2(14f); + dp.Rotation = evt.SourceRotation() - MathF.PI / 4; + dp.Radian = float.Pi / 2; + sa.Method.SendDraw(DrawModeEnum.Default, DrawTypeEnum.Donut, dp); + } + + // 49637:3.5s + [ScriptMethod(name: "超魔BOSS2 - 大回旋", eventType: EventTypeEnum.StartCasting, eventCondition: ["ActionId:49637"])] + public void 超魔B2大回旋(Event evt, ScriptAccessory sa) + { + var dp = sa.FastDp($"超魔B2大回旋-{evt.SourceId()}", evt.SourcePosition(), 3500, new Vector2(24f)); + dp.InnerScale = new Vector2(19f); + dp.Rotation = evt.SourceRotation() - MathF.PI / 4; + dp.Radian = float.Pi / 2; + sa.Method.SendDraw(DrawModeEnum.Default, DrawTypeEnum.Donut, dp); + } + + // 49616 突进:30m长6m宽矩形,6s + [ScriptMethod(name: "超魔BOSS2 - 突进", eventType: EventTypeEnum.StartCasting, eventCondition: ["ActionId:49674"])] + public void 超魔B2突进2(Event evt, ScriptAccessory sa) + { + Dbg(sa, $"突进(49616):pos {evt.SourcePosition():F1} rot {evt.SourceRotation():F2}"); + var dp = sa.FastDp("突进", evt.SourcePosition(), 6000, new Vector2(6f, 30f)); + dp.Rotation = evt.SourceRotation(); + // dp.ScaleMode = ScaleMode.ByTime; + sa.Method.SendDraw(DrawModeEnum.Default, DrawTypeEnum.Rect, dp); + } + + // 49585 半圆:SourcePosition为圆心,面对SourceRotation + [ScriptMethod(name: "超魔BOSS2 - 秘法剑", eventType: EventTypeEnum.StartCasting, eventCondition: ["ActionId:49645"])] + public void 超魔B2秘法剑(Event evt, ScriptAccessory sa) + { + Dbg(sa, $"半圆斩(49645):src {evt.SourceId():X8} pos {evt.SourcePosition():F1} rot {evt.SourceRotation():F2}"); + lock (_b2ChargeLock) + _b2剑Casts.Add((evt.SourceId(), evt.SourcePosition(), evt.SourceRotation(), Environment.TickCount64)); + var dp = sa.FastDp($"半圆斩-{evt.SourceId()}", evt.SourcePosition(), 5500, new Vector2(96f)); + dp.Rotation = evt.SourceRotation(); + dp.Radian = MathF.PI; + sa.Method.SendDraw(DrawModeEnum.Default, DrawTypeEnum.Fan, dp); + } + + private readonly object _swordPredictLock = new(); + private readonly List<(uint Sid, Vector3 Pos, int Pose, long At)> _swordPredicts = []; + private const long 同环阈值 = 1500; // 同一波金环的9710最大间隔 + + // pose→AOE形状:0=小月环(内10外40)接小钢铁10,4=月环(内15外40)接钢铁15,5=大月环(内20外40)接大钢铁20, + // 6=小钢铁10接小月环(内10外40),7=钢铁15接月环(内15外40),31=大钢铁20接大月环(内20外40) + private void B2DrawSwordShape(ScriptAccessory sa, uint sid, Vector3 pos, int pose, bool second, uint duration, uint delay) + { + var (donutFirst, r) = pose switch + { + 0 => (true, 10f), + 4 => (true, 15f), + 5 => (true, 20f), + 6 => (false, 10f), + 7 => (false, 15f), + 31 => (false, 20f), + _ => (false, 0f), + }; + if (r == 0f) return; + if (donutFirst ^ second) + { + var dp = sa.FastDp($"剑月环-{sid}-{delay}", pos, duration, new Vector2(40f)); + dp.InnerScale = new Vector2(r); + dp.Radian = float.Pi * 2; + dp.Delay = delay; + sa.Method.SendDraw(DrawModeEnum.Default, DrawTypeEnum.Donut, dp); + } + else + { + var dp = sa.FastDp($"剑钢铁-{sid}-{delay}", pos, duration, new Vector2(r)); + dp.Delay = delay; + sa.Method.SendDraw(DrawModeEnum.Default, DrawTypeEnum.Circle, dp); + } + } + + [ScriptMethod(name: "超魔BOSS2 - 舞动之剑预判", eventType: EventTypeEnum.PlayActionTimeline, eventCondition: ["Id:9710"])] + public void 超魔B2舞动之剑预判(Event evt, ScriptAccessory sa) + { + var sid = evt.SourceId(); + var obj = sa.Data.Objects.SearchById(sid); + if (obj is null || obj.DataId == 19833) return; // 19833 由普通BOSS2方法处理 + + var pose = B2GetModelState(sa, sid); + var pos = evt.SourcePosition(); + Dbg(sa, $"超魔舞动之剑9710:src {sid:X8} DataId {obj.DataId} 姿势 {pose}"); + if (pose is not (0 or 4 or 5 or 6 or 7 or 31)) + { + Dbg(sa, $"超魔舞动之剑9710:未知姿势{pose},不绘图"); + return; + } + var now = Environment.TickCount64; + bool firstWave; + lock (_swordPredictLock) + { + _swordPredicts.RemoveAll(s => now - s.At > 30000); // 清团灭残留 + // 首波(定时未激活且无更早出现的剑)立即绘制第1段;后续波由定时按判定链排程显示 + firstWave = now - _b2SwordTimingAt > 20000 && _swordPredicts.All(s => now - s.At < 同环阈值); + _swordPredicts.Add((sid, pos, pose, now)); + } + if (firstWave) B2DrawSwordShape(sa, sid, pos, pose, false, 20000, 0); + else Dbg(sa, $"超魔舞动之剑9710:{sid:X8} 非首波,待定时排程"); + } + + private long _b2SwordTimingAt; + + // 49647 定时:金环按9710出现顺序分波判定(约3s一波),首波第1段读条开始5s后判定; + // 显示跟随判定链:开始只显示首波第1段,首波判定时出现首波第2段+第2波第1段, + // 第2波判定时出现第2波第2段+第3波第1段,依此类推;晚于读条出现的金环通过轮询追加 + [ScriptMethod(name: "超魔BOSS2 - 舞动之剑定时", eventType: EventTypeEnum.StartCasting, eventCondition: ["ActionId:49647"], userControl: false)] + public async void 超魔B2舞动之剑定时(Event evt, ScriptAccessory sa) + { + List<(uint Sid, Vector3 Pos, int Pose, long At)> swords; + var castAt = Environment.TickCount64; + lock (_swordPredictLock) + { + if (_swordPredicts.Count == 0) return; + if (castAt - _b2SwordTimingAt < 15000) return; // 双剑各读一次时只按首个定时 + _b2SwordTimingAt = castAt; + swords = [.. _swordPredicts.OrderBy(s => s.At)]; + _swordPredicts.Clear(); + } + Dbg(sa, $"超魔舞动之剑49647:定时开始,已出现 {swords.Count} 把剑"); + + // 判定节奏固定:9710出现时间只用于分波排序,不决定判定时间 + const uint 首环判定 = 4000; // 首波第1段判定 = 读条开始后 + const uint 波判定间隔 = 4000; // 相邻两波判定的间隔(前一波第2段与后一波第1段同判) + var waveAt = swords[0].At; // 当前波首剑出现时间 + var wave = 0; + var next = 0; + while (true) + { + for (; next < swords.Count; next++) + { + var (sid, pos, pose, at) = swords[next]; + if (at - waveAt > 同环阈值) + { + wave++; + waveAt = at; + } + var curJ = 首环判定 + (uint)wave * 波判定间隔; // 本波第1段判定点(相对读条开始) + var elapsed = (uint)(Environment.TickCount64 - castAt); + Dbg(sa, $"超魔舞动之剑49647:{sid:X8} 姿势{pose} 第{wave + 1}波 判定+{curJ}ms"); + if (wave == 0) + { + // 首波:第1段已在预判时绘制,判定时移除 + _ = B2SwordPredictRemove(sa, sid, curJ > elapsed ? curJ - elapsed : 0); + } + else + { + // 后续波第1段:上一波判定时出现,显示到本波判定 + var prevJ = curJ - 波判定间隔; + var start = Math.Max(prevJ, elapsed); + if (curJ > start) + B2DrawSwordShape(sa, sid, pos, pose, false, curJ - start, prevJ > elapsed ? prevJ - elapsed : 0); + } + // 第2段:本波判定时出现,显示到下一波判定 + B2DrawSwordShape(sa, sid, pos, pose, true, 波判定间隔, curJ > elapsed ? curJ - elapsed : 0); + } + if (Environment.TickCount64 - castAt > 12000) break; + await Task.Delay(300); + lock (_swordPredictLock) + { + if (_swordPredicts.Count > 0) + { + swords.AddRange(_swordPredicts.OrderBy(s => s.At)); + _swordPredicts.Clear(); + } + } + } + } + + private static async Task B2SwordPredictRemove(ScriptAccessory sa, uint sid, uint delay) + { + if (delay > 0) await Task.Delay((int)delay); + sa.Method.RemoveDraw($"^剑(月环|钢铁)-{sid}-0$"); + } + + [ScriptMethod(name: "超魔BOSS2 - 剑舞", eventType: EventTypeEnum.ObjectEffect, eventCondition: ["Id1:1", "Id2:2"], userControl: false)] + public void 超魔剑刃矩形(Event evt, ScriptAccessory sa) + { + if (!Is超魔(sa)) return; // 该机制仅超魔触发 + var obj = sa.Data.Objects.SearchById(evt.SourceId()); + if (obj is null || obj.DataId != 2015283) return; + + var pos = evt.SourcePosition(); + var rot = evt.SourceRotation(); + + List<(Vector3 Pos, float Rot)> rects; + lock (_bladeRectLock) + { + if (_bladeRects.Any(r => DistXZ(r.Pos, pos) < 1f && MathF.Abs(WrapPi(r.Rot - rot)) < 0.1f)) return; + _bladeRects.Add((pos, rot)); + Dbg(sa, $"剑刃矩形记录 #{_bladeRects.Count}:pos {pos:F1} rot {rot:F2}"); + if (_bladeRects.Count < 4) return; + + rects = [.. _bladeRects]; + _bladeRects.Clear(); + } + + for (var i = 0; i < rects.Count; i++) + { + var delay = (uint)(i == 0 ? 0 : 5000 + (i - 1) * 1500); + var duration = (uint)(i == 0 ? 6500 : 3000); + foreach (var (extraRot, tag) in new[] { (0f, "正"), (MathF.PI, "反") }) + { + var dp = sa.FastDp($"剑刃矩形-{i + 1}-{tag}", rects[i].Pos, duration, new Vector2(20f, 60f)); + dp.Rotation = rects[i].Rot + extraRot; + dp.Delay = delay; + // dp.ScaleMode = ScaleMode.ByTime; + sa.Method.SendDraw(DrawModeEnum.Default, DrawTypeEnum.Rect, dp); + } + } + } + + [ScriptMethod(name: "超魔BOSS2 - 跃进步法记录", eventType: EventTypeEnum.ActionEffect, eventCondition: ["ActionId:regex:^(49656|49657)$"], userControl: false)] + public void 超魔跃进步法记录(Event evt, ScriptAccessory sa) + { + var pos = evt.EffectPosition(); + if (pos.Length() < 0.01f) return; // 排除(0,0,0) + + lock (_leapLock) + { + if (_leapPositions.All(p => DistXZ(p, pos) > 1f)) + { + _leapPositions.Add(pos); + Dbg(sa, $"跃进步法记录 #{_leapPositions.Count}:{pos:F1}"); + } + } + } + + // 49687 剑技爆发:按顺序指路各落点 + [ScriptMethod(name: "超魔BOSS2 - 剑技爆发指路", eventType: EventTypeEnum.StartCasting, eventCondition: ["ActionId:49687"])] + public void 超魔剑技爆发指路(Event evt, ScriptAccessory sa) + { + List rawPoints; + lock (_leapLock) + { + rawPoints = [.. _leapPositions]; + _leapPositions.Clear(); // 用完清空 + } + if (rawPoints.Count == 0) + { + Dbg(sa, $"剑技爆发(49687):落点list为空,跳过指路"); + return; + } + + // 检测场上19842中带 Status 2056 且 Param 1173 的剑,按位置匹配落点轮次 + var steelSwordPos = sa.Data.Objects + .Where(o => o is IBattleChara bc && bc.DataId == 19842 + && bc.StatusList.Any(s => s.StatusId == 2056 && s.Param == 1173)) + .Select(o => o.Position) + .ToList(); + var isSteel = rawPoints.Select(p => steelSwordPos.Any(sp => DistXZ(sp, p) < 3f)).ToList(); + Dbg(sa, $"剑技爆发(49687):共 {rawPoints.Count} 个落点,1173钢铁轮 [{string.Join(",", isSteel.Select((s, i) => (s, i)).Where(t => t.s).Select(t => t.i + 1))}]," + + $"1173剑位 [{string.Join(" | ", steelSwordPos.Select(p => p.ToString("F1")))}]"); + + var points = rawPoints.Select(p => 朝场中偏移(p, 5f)).ToList(); + var n = points.Count; + + // 钢铁轮前一轮的指路点:绕场中朝远离下一轮钢铁的方向旋转10° + for (var k = 0; k + 1 < n; k++) + if (isSteel[k + 1]) + points[k] = 绕场中旋转远离(points[k], rawPoints[k + 1]); + + var delays = new uint[n]; + var durations = new uint[n]; + var steelCount = 0; + for (var k = 0; k < n; k++) + { + if (isSteel[k]) steelCount++; + delays[k] = (uint)((k == 0 ? 0 : 5000 + (k - 1) * 2500) + steelCount * 2500); + durations[k] = (uint)(k == 0 ? 5000 : 2500); + } + + var green = new Vector4(0f, 1f, 0f, 1f); + var white = new Vector4(1f, 1f, 1f, 1f); + + for (var k = 0; k < n; k++) + { + var delay = delays[k]; + var duration = durations[k]; + + if (isSteel[k]) + { + // 钢铁圈与前一轮指路同时出现,显示4s + var dp = sa.FastDp($"剑技爆发-钢铁-{k + 1}", rawPoints[k], 4000, new Vector2(15f)); + dp.Delay = k == 0 ? 0 : delays[k - 1]; + dp.ScaleMode = ScaleMode.ByTime; + sa.Method.SendDraw(DrawModeEnum.Default, DrawTypeEnum.Circle, dp); + + // 钢铁时段同步显示本轮指路,全白色 + sa.Method.SendDraw(DrawModeEnum.Imgui, DrawTypeEnum.Displacement, + sa.WaypointDp(points[k], 2500, delay - 2500, $"剑技爆发-钢铁轮-me到{k + 1}", white)); + for (var j = k; j < n - 1; j++) + sa.Method.SendDraw(DrawModeEnum.Imgui, DrawTypeEnum.Displacement, + sa.WaypointFromToDp(points[j], points[j + 1], 2500, delay - 2500, $"剑技爆发-{j + 1}到{j + 2}-钢铁轮{k + 1}", white)); + } + + sa.Method.SendDraw(DrawModeEnum.Imgui, DrawTypeEnum.Displacement, + sa.WaypointDp(points[k], duration, delay, $"剑技爆发-me到{k + 1}", green)); + for (var j = k; j < n - 1; j++) + sa.Method.SendDraw(DrawModeEnum.Imgui, DrawTypeEnum.Displacement, + sa.WaypointFromToDp(points[j], points[j + 1], duration, delay, $"剑技爆发-{j + 1}到{j + 2}-阶段{k + 1}", white)); + } + } + #endregion + + #region 2.5 + // 48848:12s,SourcePosition 20m危险圈 + [ScriptMethod(name: "超魔2.5 - 不可见钢铁", eventType: EventTypeEnum.StartCasting, eventCondition: ["ActionId:48848"])] + public void 超魔25大圈(Event evt, ScriptAccessory sa) + { + Dbg(sa, $"2.5大圈(48848):pos {evt.SourcePosition():F1}"); + var dp = sa.FastDp($"2.5大圈-{evt.SourceId()}", evt.SourcePosition(), 12000, new Vector2(20f)); + dp.ScaleMode = ScaleMode.ByTime; + sa.Method.SendDraw(DrawModeEnum.Default, DrawTypeEnum.Circle, dp); + } + #endregion + + #region 超魔BOSS3 + // 超魔boss 3 + [ScriptMethod(name: "超魔BOSS3 - 爆炎", eventType: EventTypeEnum.StartCasting, eventCondition: ["ActionId:regex:^(47490|47494)$"])] + public void 超魔爆炎(Event evt, ScriptAccessory sa) + { + Dbg(sa, $"爆炎({evt.ActionId()}):src {evt.SourceId():X8}"); + var dp = sa.Data.GetDefaultDrawProperties(); + dp.Name = $"爆炎-{evt.SourceId()}"; + dp.Color = sa.Data.DefaultDangerColor; + dp.Owner = evt.SourceId(); + dp.DestoryAt = 5000; + dp.Scale = new Vector2(18f); + dp.ScaleMode = ScaleMode.ByTime; + sa.Method.SendDraw(DrawModeEnum.Default, DrawTypeEnum.Circle, dp); + } + + // 47466 魔具联动-冰封 / 47469 古代冰封:5.5s,以施法者rotation为正面,前后左右各45长15宽的rect十字 + [ScriptMethod(name: "超魔BOSS3 - 冰封十字", eventType: EventTypeEnum.StartCasting, eventCondition: ["ActionId:regex:^(47495|47491)$"])] + public void 超魔冰封十字(Event evt, ScriptAccessory sa) + { + Dbg(sa, $"冰封十字({evt.ActionId()}):src {evt.SourceId():X8}"); + List<(float Rot, string Tag)> dirs = [(0f, "前"), (MathF.PI / 2, "左"), (MathF.PI, "后"), (-MathF.PI / 2, "右")]; + foreach (var (rot, tag) in dirs) + { + var dp = sa.Data.GetDefaultDrawProperties(); + dp.Name = $"冰封十字-{evt.SourceId()}-{tag}"; + dp.Color = sa.Data.DefaultDangerColor; + dp.Owner = evt.SourceId(); + dp.Rotation = rot; + dp.DestoryAt = 5500; + dp.Scale = new Vector2(15f, 45f); + // dp.ScaleMode = ScaleMode.ByTime; + sa.Method.SendDraw(DrawModeEnum.Default, DrawTypeEnum.Rect, dp); + } + } + + // 47471 古代暴雷:5.5s,SourcePosition上以SourceRotation为正面的45度60m扇形 + [ScriptMethod(name: "超魔BOSS3 - 古代暴雷", eventType: EventTypeEnum.StartCasting, eventCondition: ["ActionId:regex:^(47497|50358)"])] + public void 超魔古代暴雷(Event evt, ScriptAccessory sa) + { + Dbg(sa, $"古代暴雷(47479):pos {evt.SourcePosition():F1} rot {evt.SourceRotation():F2}"); + var dp = sa.FastDp($"古代暴雷-{evt.SourceId()}", evt.SourcePosition(), 5500, new Vector2(60f)); + dp.Rotation = evt.SourceRotation(); + dp.Radian = 45f * MathF.PI / 180f; + sa.Method.SendDraw(DrawModeEnum.Default, DrawTypeEnum.Fan, dp); + } + + [ScriptMethod(name: "超魔BOSS3 - 灭亡射线", eventType: EventTypeEnum.StartCasting, eventCondition: ["ActionId:47504"])] + public async void 超魔灭亡射线(Event evt, ScriptAccessory sa) + { + var srcId = evt.SourceId(); + var pos = evt.SourcePosition(); + + await Task.Delay(1000); + + // 0.5s后取施法者当前实时朝向,取不到则回退用事件快照 + var srcObj = sa.Data.Objects.SearchById(srcId); + var rot = srcObj?.Rotation ?? evt.SourceRotation(); + Dbg(sa, $"灭亡射线(47475):pos {pos:F1} rot {rot:F2}(实时:{srcObj != null})"); + + var dp = sa.FastDp($"灭亡射线-{srcId}", pos, 3000, new Vector2(6f, 30f)); + dp.Rotation = rot; + // dp.ScaleMode = ScaleMode.ByTime; + sa.Method.SendDraw(DrawModeEnum.Default, DrawTypeEnum.Rect, dp); + } + + // 47500:5.5s,EffectPosition 沿 SourceRotation 的 60长10宽矩形,CreateOmen 689(rect omen:X=半宽 Z=全长) + [ScriptMethod(name: "超魔BOSS3 - 黑暗奔涌", eventType: EventTypeEnum.StartCasting, eventCondition: ["ActionId:47500"])] + public void 超魔B3直线47500(Event evt, ScriptAccessory sa) + { + Dbg(sa, $"直线(47500):pos {evt.EffectPosition():F1} rot {evt.SourceRotation():F2}"); + sa.Method.VfxMethod.CreateOmen(689, new Vector3(5f, 10f, 60f), + evt.EffectPosition(), evt.SourceRotation(), null, 4000); + } + + // 47500 读条开始4s后:左右两侧步进地火,60长10宽矩形,每轮向外步进10m,每轮画3.5s紧接下一轮 + [ScriptMethod(name: "超魔BOSS3 - 黑暗奔流地火", eventType: EventTypeEnum.StartCasting, eventCondition: ["ActionId:47500"])] + public void 超魔B3黑暗奔流地火(Event evt, ScriptAccessory sa) + { + var pos = evt.EffectPosition(); + var rot = evt.SourceRotation(); + Dbg(sa, $"黑暗奔流地火(47500):pos {pos:F1} rot {rot:F2}"); + for (var i = 0; i < 2; i++) + { + foreach (var (sign, tag) in new[] { (1f, "左"), (-1f, "右") }) + { + var side = rot + sign * MathF.PI / 2; + var center = pos + new Vector3(MathF.Sin(side), 0f, MathF.Cos(side)) * (10f * (i + 1)); + var dp = sa.FastDp($"黑暗奔流地火-{tag}-{i}", center, 3500, new Vector2(10f, 60f)); + dp.Rotation = rot; + dp.Delay = (uint)(2500 + i * 2000); + sa.Method.SendDraw(DrawModeEnum.Default, DrawTypeEnum.Rect, dp); + } + } + } + + // 47502 真空波:等1s让boss完成转向后,SourcePosition 上以实时朝向为正面的180度30m扇形 + [ScriptMethod(name: "超魔BOSS3 - 真空波", eventType: EventTypeEnum.StartCasting, eventCondition: ["ActionId:47502"])] + public async void 超魔B3真空波(Event evt, ScriptAccessory sa) + { + var srcId = evt.SourceId(); + var pos = evt.SourcePosition(); + + await Task.Delay(1000); + + // 取施法者当前实时朝向,取不到则回退用事件快照 + var srcObj = sa.Data.Objects.SearchById(srcId); + var rot = srcObj?.Rotation ?? evt.SourceRotation(); + Dbg(sa, $"真空波(47502):pos {pos:F1} rot {rot:F2}(实时:{srcObj != null})"); + + var dp = sa.FastDp($"真空波-{srcId}", pos, 3000, new Vector2(30f)); + dp.Rotation = rot; + dp.Radian = MathF.PI; + sa.Method.SendDraw(DrawModeEnum.Default, DrawTypeEnum.Fan, dp); + } + // —— 魔具联动:黑暗奔流 三连 —— + private readonly object _b3联动Lock = new(); + private readonly Dictionary _b3屏障头属性 = []; // 屏障头Id → 属性Param(1114火/1115冰/1116雷) + private readonly List _b3奔流顺序 = []; // 三波2552属性的接收顺序,集齐后播报 + private int _b3奔流波次; // 已收到的2552个数(只取前三) + private bool _b3联动就绪; // 47507读条开始→true,期间的2552才触发绘制 + + private static string B3属性名(uint param) => param switch { 1114 => "火", 1115 => "冰", 1116 => "雷", _ => $"?{param}" }; + + [ScriptMethod(name: "超魔BOSS3 - 屏障头注能记录", eventType: EventTypeEnum.Tether, eventCondition: ["Id:regex:^019[0-2]$"], userControl: false)] + public void 超魔B3屏障头注能记录(Event evt, ScriptAccessory sa) + { + var elem = evt["Id"] switch { "0190" => 1114u, "0191" => 1115u, "0192" => 1116u, _ => 0u }; + if (elem == 0) return; + lock (_b3联动Lock) _b3屏障头属性[evt.SourceId()] = elem; + Dbg(sa, $"屏障头注能:{evt.SourceId():X8} → {B3属性名(elem)}"); + } + + [ScriptMethod(name: "超魔BOSS3 - 魔具联动黑暗奔流", eventType: EventTypeEnum.StatusAdd, eventCondition: ["StatusID:2552", "Param:regex:^111[4-6]$"])] + public async void 超魔B3魔具联动黑暗奔流(Event evt, ScriptAccessory sa) + { + if (!Is超魔(sa)) return; + if (!uint.TryParse(evt["Param"], out var elem)) return; + + int wave; + List heads; + string? order = null; + lock (_b3联动Lock) + { + if (!_b3联动就绪) return; // 非联动期间的2552(如47490/47491/47492读条伴随),忽略 + if (_b3奔流波次 >= 3) return; // 每波结算时的重复Add,忽略 + wave = _b3奔流波次++; + _b3奔流顺序.Add(elem); + if (_b3奔流波次 == 3) order = string.Join("→", _b3奔流顺序.Select(B3属性名)); + heads = _b3屏障头属性.Where(kv => kv.Value == elem).Select(kv => kv.Key).ToList(); + } + + // 集齐三个属性后播报结算顺序(如"冰→雷→火"),持续到第三波结算 + if (order != null) sa.Method.TextInfo(order, 18000, false); + + uint[] durations = [8900, 13600, 18100]; // 自己的2552到本波结算 + uint[] delays = [0, 8900, 13600]; // 第2/3波延后开画 + var duration = durations[wave]; + var delay = delays[wave]; + var visible = duration - delay; + var rot = MathF.PI - wave * 2f * MathF.PI / 3f; // 第1波C→A(π),每波直线逆时针转60° + var center = new Vector3(100f, -723.96f, 800f); + var anchor = center - new Vector3(MathF.Sin(rot), 0f, MathF.Cos(rot)) * 30f; + Dbg(sa, $"魔具联动第{wave + 1}波:{B3属性名(elem)},{heads.Count}个屏障头,{delay}ms后开画,{duration}ms后结算"); + + // 步进地火:直线结算-1s起,左右±10、±20两轮,每轮3.5s + DrawPropertiesEdit dp; + for (var i = 0; i < 2; i++) + { + foreach (var (sign, tag) in new[] { (1f, "左"), (-1f, "右") }) + { + var side = rot + sign * MathF.PI / 2; + var pos = anchor + new Vector3(MathF.Sin(side), 0f, MathF.Cos(side)) * (10f + i * 10f); + dp = sa.FastDp($"B3联动地火-{wave}-{tag}-{i}", pos, 3500, new Vector2(10f, 60f)); + dp.Rotation = rot; + dp.Delay = (uint)(duration - 1000 + i * 2100); + sa.Method.SendDraw(DrawModeEnum.Default, DrawTypeEnum.Rect, dp); + } + } + + // 该属性两个屏障头的AoE,与直线同时结算 + foreach (var head in heads) + { + switch (elem) + { + case 1114: // 火:18m圆 + dp = sa.Data.GetDefaultDrawProperties(); + dp.Name = $"B3联动火圆-{wave}-{head:X8}"; + dp.Color = sa.Data.DefaultDangerColor; + dp.Owner = head; + dp.Delay = delay; + dp.DestoryAt = visible; + dp.Scale = new Vector2(18f); + sa.Method.SendDraw(DrawModeEnum.Default, DrawTypeEnum.Circle, dp); + break; + case 1115: // 冰:以头朝向前后左右15x45十字 + foreach (var (r, tag) in new[] { (0f, "前"), (MathF.PI / 2, "左"), (MathF.PI, "后"), (-MathF.PI / 2, "右") }) + { + dp = sa.Data.GetDefaultDrawProperties(); + dp.Name = $"B3联动冰十字-{wave}-{head:X8}-{tag}"; + dp.Color = sa.Data.DefaultDangerColor; + dp.Owner = head; + dp.Rotation = r; + dp.Delay = delay; + dp.DestoryAt = visible; + dp.Scale = new Vector2(15f, 45f); + sa.Method.SendDraw(DrawModeEnum.Default, DrawTypeEnum.Rect, dp); + } + break; + case 1116: // 雷:以头朝向左上左下右上右下四个45°60m扇形 + foreach (var (r, tag) in new[] { (MathF.PI / 4, "左上"), (3 * MathF.PI / 4, "左下"), (-MathF.PI / 4, "右上"), (-3 * MathF.PI / 4, "右下") }) + { + dp = sa.Data.GetDefaultDrawProperties(); + dp.Name = $"B3联动雷扇-{wave}-{head:X8}-{tag}"; + dp.Color = sa.Data.DefaultDangerColor; + dp.Owner = head; + dp.Rotation = r; + dp.Radian = 45f * MathF.PI / 180f; + dp.Delay = delay; + dp.DestoryAt = visible; + dp.Scale = new Vector2(60f); + sa.Method.SendDraw(DrawModeEnum.Default, DrawTypeEnum.Fan, dp); + } + break; + } + } + + // 直线:Omen 689(X=半宽 Z=全长),从场边锚点沿rot穿过场中心,等上一波结算后再出现 + if (delay > 0) await Task.Delay((int)delay); + sa.Method.VfxMethod.CreateOmen(689, new Vector3(5f, 10f, 60f), anchor, rot, null, (int)visible); + } + + // 47507 魔具联动:黑暗奔流读条开始:开闸,期间的2552触发绘制 + [ScriptMethod(name: "超魔BOSS3 - 联动开始", eventType: EventTypeEnum.StartCasting, eventCondition: ["ActionId:47507"], userControl: false)] + public void 超魔B3联动开始(Event evt, ScriptAccessory sa) + { + lock (_b3联动Lock) + { + _b3联动就绪 = true; + _b3奔流波次 = 0; + _b3奔流顺序.Clear(); + } + } + + // 47490爆炎/47491冰封/47492暴雷 读条时也会Add 2552,不触发联动:关闸并清空计数 + [ScriptMethod(name: "超魔BOSS3 - 联动误触发拦截", eventType: EventTypeEnum.StartCasting, eventCondition: ["ActionId:regex:^4749[0-2]$"], userControl: false)] + public void 超魔B3联动误触发拦截(Event evt, ScriptAccessory sa) + { + lock (_b3联动Lock) + { + _b3联动就绪 = false; + _b3奔流波次 = 0; + _b3奔流顺序.Clear(); + } + } + + // 魔力注入开始(新一轮注能前) / 真空波(本轮联动收尾):清空记录 + [ScriptMethod(name: "超魔BOSS3 - 联动记录清空", eventType: EventTypeEnum.StartCasting, eventCondition: ["ActionId:regex:^(47486|47502)$"], userControl: false)] + public void 超魔B3联动记录清空(Event evt, ScriptAccessory sa) + { + lock (_b3联动Lock) + { + _b3联动就绪 = false; + _b3屏障头属性.Clear(); + _b3奔流波次 = 0; + _b3奔流顺序.Clear(); + } + } + + // 47521 古代爆炎:5s,施法者上18m危险圈 + [ScriptMethod(name: "超魔BOSS3 - 古代爆炎", eventType: EventTypeEnum.StartCasting, eventCondition: ["ActionId:47521"])] + public void 超魔B3古代爆炎(Event evt, ScriptAccessory sa) + { + Dbg(sa, $"古代爆炎(47521):src {evt.SourceId():X8}"); + var dp = sa.Data.GetDefaultDrawProperties(); + dp.Name = $"古代爆炎-{evt.SourceId()}"; + dp.Color = sa.Data.DefaultDangerColor; + dp.Owner = evt.SourceId(); + dp.DestoryAt = 5000; + dp.Scale = new Vector2(18f); + dp.ScaleMode = ScaleMode.ByTime; + sa.Method.SendDraw(DrawModeEnum.Imgui, DrawTypeEnum.Circle, dp); + } + + // 47522 古代冰封:5s,以施法者rotation为正面,前后左右各45长15宽的rect十字 + [ScriptMethod(name: "超魔BOSS3 - 古代冰封", eventType: EventTypeEnum.StartCasting, eventCondition: ["ActionId:47522"])] + public void 超魔B3古代冰封(Event evt, ScriptAccessory sa) + { + Dbg(sa, $"古代冰封(47522):src {evt.SourceId():X8}"); + List<(float Rot, string Tag)> dirs = [(0f, "前"), (MathF.PI / 2, "左"), (MathF.PI, "后"), (-MathF.PI / 2, "右")]; + foreach (var (rot, tag) in dirs) + { + var dp = sa.Data.GetDefaultDrawProperties(); + dp.Name = $"古代冰封-{evt.SourceId()}-{tag}"; + dp.Color = sa.Data.DefaultDangerColor; + dp.Owner = evt.SourceId(); + dp.Rotation = rot; + dp.DestoryAt = 5000; + dp.Scale = new Vector2(15f, 45f); + sa.Method.SendDraw(DrawModeEnum.Imgui, DrawTypeEnum.Rect, dp); + } + } + + // 47493 分身古代暴雷:5s,SourcePosition上以SourceRotation为正面的45度60m扇形 + [ScriptMethod(name: "超魔BOSS3 - 分身古代暴雷", eventType: EventTypeEnum.StartCasting, eventCondition: ["ActionId:47493"])] + public void 超魔B3分身古代暴雷(Event evt, ScriptAccessory sa) + { + Dbg(sa, $"分身古代暴雷(47493):pos {evt.SourcePosition():F1} rot {evt.SourceRotation():F2}"); + var dp = sa.FastDp($"分身古代暴雷-{evt.SourceId()}", evt.SourcePosition(), 5000, new Vector2(60f)); + dp.Rotation = evt.SourceRotation(); + dp.Radian = 45f * MathF.PI / 180f; + sa.Method.SendDraw(DrawModeEnum.Imgui, DrawTypeEnum.Fan, dp); + } + + // —— 多产的土壤(鸳鸯锅)—— + private static readonly Vector3 Boss3场中 = new(100f, -723.96f, 800f); + private readonly object _b3鸳鸯锅Lock = new(); + private readonly HashSet _b3鸳鸯锅Heads = []; // 本轮已记录的屏障头,防重复Add + private int _b3鸳鸯锅序号; + + // 47514 多产的土壤读条:重置鸳鸯锅计数 + [ScriptMethod(name: "超魔BOSS3 - 鸳鸯锅重置", eventType: EventTypeEnum.StartCasting, eventCondition: ["ActionId:47514"], userControl: false)] + public void 超魔B3鸳鸯锅重置(Event evt, ScriptAccessory sa) + { + lock (_b3鸳鸯锅Lock) + { + _b3鸳鸯锅序号 = 0; + _b3鸳鸯锅Heads.Clear(); + } + } + + [ScriptMethod(name: "超魔BOSS3 - 鸳鸯锅指路", eventType: EventTypeEnum.StatusAdd, eventCondition: ["StatusID:2552", "Param:regex:^111[78]$"])] + public async void 超魔B3鸳鸯锅指路(Event evt, ScriptAccessory sa) + { + if (!Is超魔(sa)) return; + if (!uint.TryParse(evt["Param"], out var param)) return; + + var headId = evt.TargetId(); + int idx; + lock (_b3鸳鸯锅Lock) + { + if (!_b3鸳鸯锅Heads.Add(headId)) return; // 重复Add,忽略 + idx = _b3鸳鸯锅序号++; + } + + // StatusAdd时刻头可能还没转完,等1s后再取实时朝向 + await Task.Delay(1000); + var head = sa.Data.Objects.SearchById(headId); + if (head is null) + { + Dbg(sa, $"鸳鸯锅#{idx + 1}:找不到屏障头 {headId:X8},跳过"); + return; + } + var rot = head.Rotation; + Dbg(sa, $"鸳鸯锅#{idx + 1}:头 {headId:X8} {(param == 1117 ? "左蓝右红" : "左红右蓝")} rot {rot:F2}"); + + // 本锅结算(半场AoE命中)≈自己StatusAdd后 13270+4800*idx+500ms; + // 吃锅后自己的5136/5137会交换颜色,交换包在上一锅命中后约1s到达: + // 首锅从现在开始显示,其余显示结算前5s(比窗口开始晚1s判定,确保读到交换后的颜色) + var settle = 13770 + 4800 * idx - 1000; // 已等1s取朝向 + var showDur = (uint)(idx == 0 ? settle : 5050); + if (settle > showDur) await Task.Delay(settle - (int)showDur); + + var me = sa.Data.MyObject; + var meBlue = me?.HasStatus(5136) ?? false; // 蓝→吃红 + var meRed = me?.HasStatus(5137) ?? false; // 红→吃蓝 + if (!meBlue && !meRed) + { + Dbg(sa, $"鸳鸯锅#{idx + 1}:自己无5136/5137,跳过指路"); + return; + } + + // 1117=左蓝右红,1118=左红右蓝 蓝吃红去红半场,红吃蓝去蓝半场 + var 红在左 = param == 1118; + var 去左 = meBlue == 红在左; + var dp = sa.FastDp($"鸳鸯锅安全半场-{idx + 1}", Boss3场中, showDur, new Vector2(24f), safe: true); + dp.Rotation = rot + (去左 ? MathF.PI / 2 : -MathF.PI / 2); + dp.Radian = MathF.PI; + sa.Method.SendDraw(DrawModeEnum.Default, DrawTypeEnum.Fan, dp); + Dbg(sa, $"鸳鸯锅#{idx + 1}:自己{(meBlue ? "蓝吃红" : "红吃蓝")},安全半场在头{(去左 ? "左" : "右")}侧,显示{showDur}ms"); + } + #endregion + + #region 超魔BOSS4 + + // —— 四连召唤·封印武器(48907/48909,13.2s读条)—— + private readonly object _b4WeaponLock = new(); + private readonly List _b4Weapons = []; // 按StatusAdd顺序记录的武器statusId + private uint _b4BossId; + + private static string B4武器名(uint statusId) => statusId switch { 5534 => "弓[月环]", 5533 => "刀[正刀]", 5535 => "琴[钢铁]", _ => "铃铛[斜刀]" }; + + // TextInfo横幅用去的位置:弓=内(月环贴脸)、刀=斜(正刀躲斜角)、铃铛=正(斜刀躲正点)、琴=外(钢铁远离) + private static string B4武器位置(uint statusId) => statusId switch { 5534 => "靠近", 5533 => "去斜", 5535 => "远离", _ => "去正" }; + + // 弓的钢铁不在boss/场中,而是三个固定点(盯准施法者位置) + private static readonly Vector3[] 盯准固定点 = + [ + new(0.00f, -684.00f, -607.50f), + new(17.75f, -684.00f, -638.25f), + new(-17.75f, -684.00f, -638.25f), + ]; + + // 弓:三个固定点上各画一个11m钢铁,显示窗口相对StatusAdd为[delay, delay+duration] + private void 超魔B4画弓钢铁(ScriptAccessory sa, int k, uint delay, uint duration) + { + for (var i = 0; i < 盯准固定点.Length; i++) + { + var dp = sa.FastDp($"四连武器-{k + 1}-弓-{i}", 盯准固定点[i], duration, new Vector2(11f)); + dp.Delay = delay; + sa.Method.SendDraw(DrawModeEnum.Default, DrawTypeEnum.Circle, dp); + } + } + + [ScriptMethod(name: "超魔BOSS4 - 四连召唤开始", eventType: EventTypeEnum.StartCasting, eventCondition: ["ActionId:regex:^(48906|48907|48908|48909)$"], userControl: false)] + public void 超魔B4四连召唤开始(Event evt, ScriptAccessory sa) + { + lock (_b4WeaponLock) + { + _b4Weapons.Clear(); + _b4BossId = evt.SourceId(); + } + Dbg(sa, $"四连召唤({evt.ActionId()}):boss {_b4BossId:X8},开始记录武器顺序"); + } + + [ScriptMethod(name: "超魔BOSS4 - 四连召唤武器", eventType: EventTypeEnum.StatusAdd, eventCondition: ["StatusID:regex:^(553[2-5])$"])] + public void 超魔B4四连召唤武器(Event evt, ScriptAccessory sa) + { + uint bossId; + int k; + string? order = null; + lock (_b4WeaponLock) + { + bossId = _b4BossId; + if (bossId == 0 || evt.TargetId() != bossId) return; // 只认四连召唤读条中boss身上的status + if (_b4Weapons.Contains(evt.StatusId)) return; + if (_b4Weapons.Count >= 4) return; + k = _b4Weapons.Count; + _b4Weapons.Add(evt.StatusId); + if (_b4Weapons.Count == 4) order = string.Join("→", _b4Weapons.Select(B4武器位置)); + } + + // 每个武器现身时/e即时播报武器名,四个集齐后TextInfo汇总去的位置(如"内→斜→外→正"), + // 横幅持续到第四把结算(第4个StatusAdd + 14400+300*3) + var name = B4武器名(evt.StatusId); + sa.Method.SendChat($"/e [魔之塔] 四连武器{k + 1}:{name}"); + if (order != null) sa.Method.TextInfo(order, 15300, false); + + // 第k个武器:结算 = StatusAdd + 14400+300k ms; + // 显示窗口 = [上一个武器结算, 自己结算](第一个武器获得status时立即显示) + var delay = (uint)(k == 0 ? 0 : 11100 + 300 * k); + var duration = (uint)(k == 0 ? 14400 : 3300); + Dbg(sa, $"四连武器#{k + 1}:{name}({evt.StatusId}),{delay}ms后显示{duration}ms"); + + switch (evt.StatusId) + { + case 5534: // 弓:三个固定点上各一个11m钢铁 + 超魔B4画弓钢铁(sa, k, delay, duration); + break; + case 5535: // 琴:15m钢铁(同普魔琴) + { + var dp = sa.Data.GetDefaultDrawProperties(); + dp.Name = $"四连武器-{k + 1}-{name}"; + dp.Color = sa.Data.DefaultDangerColor; + dp.Owner = bossId; + dp.Delay = delay; + dp.DestoryAt = duration; + dp.Scale = new Vector2(15f); + sa.Method.SendDraw(DrawModeEnum.Default, DrawTypeEnum.Circle, dp); + break; + } + case 5532: // 铃铛:正后/右前/左前 60度扇形(左前右前之间隔正面60度空隙) + case 5533: // 刀:正面/左后/右后 60度扇形(左后右后之间隔正后60度空隙) + { + var dirs = evt.StatusId == 5532 + ? new[] { (MathF.PI, "正后"), (-MathF.PI / 3, "右前"), (MathF.PI / 3, "左前") } + : new[] { (0f, "正面"), (2 * MathF.PI / 3, "左后"), (-2 * MathF.PI / 3, "右后") }; + foreach (var (rot, tag) in dirs) + { + var dp = sa.Data.GetDefaultDrawProperties(); + dp.Name = $"四连武器-{k + 1}-{name}-{tag}"; + dp.Color = sa.Data.DefaultDangerColor; + dp.Owner = bossId; + dp.Rotation = rot; + dp.Radian = 60f * MathF.PI / 180f; + dp.Delay = delay; + dp.DestoryAt = duration; + dp.Scale = new Vector2(30f); + sa.Method.SendDraw(DrawModeEnum.Default, DrawTypeEnum.Fan, dp); + } + break; + } + } + } + + // —— 元素展开(48399)—— + // 圆环机制同普魔(出现约7s后AOE),但相邻圆环间隔缩短为约1.2~1.6s(普魔更长), + // 判定提前量从剩2s改为剩1.5s。普魔/超魔各自只触发自己的方法。 + [ScriptMethod(name: "超魔BOSS4 - 元素圆环换区", eventType: EventTypeEnum.ObjectChanged, eventCondition: ["Operate:Add", "DataId:regex:^(201524[345])$"], suppress: 1000)] + public void 超魔B4元素圆环换区(Event evt, ScriptAccessory sa) + { + if (!Is超魔(sa)) return; // 超魔专用 + if (元素创造中()) return; // 元素创造期间由安全区指路接管 + _ = 元素圆环换区核心(evt, sa, 5500); + } + + // —— 封印武器(单发,48384琴/48386弓)—— + [ScriptMethod(name: "超魔BOSS4 - 封印武器", eventType: EventTypeEnum.StartCasting, eventCondition: ["ActionId:regex:^(4842[23])$"])] + public void 超魔B4封印武器(Event evt, ScriptAccessory sa) + { + var isHarp = evt.ActionId() == 48422; + var radius = isHarp ? 15f : 11f; + var pos = isHarp ? evt.EffectPosition() : evt.SourcePosition(); + if (pos.Length() < 0.01f) pos = evt.SourcePosition(); + Dbg(sa, $"封印武器-{(isHarp ? "琴" : "弓")}({evt.ActionId()}):pos {pos:F1} 半径 {radius}"); + + var dp = sa.FastDp($"超魔封印武器-{evt.SourceId()}", pos, 7000, new Vector2(radius)); + dp.ScaleMode = ScaleMode.ByTime; + sa.Method.SendDraw(DrawModeEnum.Default, DrawTypeEnum.Circle, dp); + } + + [ScriptMethod(name: "BOSS4 - 预言现象", eventType: EventTypeEnum.AddCombatant, eventCondition: ["DataId:19299"])] + public void 普魔B4预言现象(Event evt, ScriptAccessory sa) => _ = B4预言现象核心(evt, sa); + + // —— 元素整合(48434,19.7s读条)—— + // 读条期间分三轮每轮3个球AddCombatant(19971冰/19972火/19973雷,间隔约3.2s,出现约6s后爆炸)。 + // 只在第一轮生成时:从场中朝每个球的方向画绿色60度60m扇形,持续6s。 + private readonly object _b4IntegrateLock = new(); + private bool _b4IntegrateArmed; + private readonly List _b4IntegrateBalls = []; + + [ScriptMethod(name: "超魔BOSS4 - 元素整合开始", eventType: EventTypeEnum.StartCasting, eventCondition: ["ActionId:48434"], userControl: false)] + public void 超魔B4元素整合开始(Event evt, ScriptAccessory sa) + { + lock (_b4IntegrateLock) + { + _b4IntegrateArmed = true; + _b4IntegrateBalls.Clear(); + } + Dbg(sa, "元素整合(48434):等待第一轮元素球"); + } + + [ScriptMethod(name: "超魔BOSS4 - 元素整合首轮标记", eventType: EventTypeEnum.AddCombatant, eventCondition: ["DataId:regex:^(1997[1-3])$"])] + public void 超魔B4元素整合首轮标记(Event evt, ScriptAccessory sa) + { + var pos = evt.SourcePosition(); + int idx; + lock (_b4IntegrateLock) + { + if (!_b4IntegrateArmed) return; // 只标记第一轮 + if (_b4IntegrateBalls.Any(p => DistXZ(p, pos) <= 1f)) return; // 复读事件去重 + idx = _b4IntegrateBalls.Count; + _b4IntegrateBalls.Add(pos); + if (_b4IntegrateBalls.Count >= 3) _b4IntegrateArmed = false; + } + + var rot = MathF.Atan2(pos.X - Boss4中心.X, pos.Z - Boss4中心.Z); + Dbg(sa, $"元素整合首轮球#{idx + 1}:DataId {evt["DataId"]} pos {pos:F1} rot {rot:F2}"); + + var dp = sa.FastDp($"元素整合首轮-{idx + 1}", Boss4中心, 6000, new Vector2(60f)); + dp.Color = new Vector4(0f, 1f, 0f, 1f); + dp.Rotation = rot; + dp.Radian = 60f * MathF.PI / 180f; + sa.Method.SendDraw(DrawModeEnum.Default, DrawTypeEnum.Fan, dp); + } + + // —— 魔法剑·石化(48444本体/48445分身,4.7s读条)—— + // 第0~3跳沿读条朝向直线前进、每跳6m;第3~5跳绕场中转弯、每跳40°(第4跳半径9、第5跳半径同第3跳); + // 第5~8跳沿(朝向∓60°)直线离场、每跳6m。 + // 转弯方向由起点车道相对场中的偏移决定:朝向×(场中-起点)叉积为负则+40°/-60°,为正则镜像。 + // 读条开始即可排程全部9跳,每跳显示结算前4s。 + private const float 石化地火半径 = 6f; + private const int 石化跳间隔 = 2000; + private const int 石化起爆时间 = 5000; // 读条开始到第0跳起爆 + + [ScriptMethod(name: "超魔BOSS4 - 石化地火", eventType: EventTypeEnum.StartCasting, eventCondition: ["ActionId:48445"])] + public void 超魔B4石化地火(Event evt, ScriptAccessory sa) + { + var sid = evt.SourceId(); + var p0 = evt.SourcePosition(); + var rot = evt.SourceRotation(); + var v = new Vector3(MathF.Sin(rot), 0, MathF.Cos(rot)); + + // 场中在行进方向的哪一侧决定转弯方向 + var wx = Boss4中心.X - p0.X; + var wz = Boss4中心.Z - p0.Z; + var dir = v.X * wz - v.Z * wx < 0 ? 1 : -1; + + var nodes = new Vector3[9]; + for (var k = 0; k <= 3; k++) + nodes[k] = p0 + v * (6f * k); + + var a3 = MathF.Atan2(nodes[3].X - Boss4中心.X, nodes[3].Z - Boss4中心.Z); + var r3 = DistXZ(nodes[3], Boss4中心); + var turn = 40f * MathF.PI / 180f * dir; + nodes[4] = Boss4中心 + new Vector3(MathF.Sin(a3 + turn), 0, MathF.Cos(a3 + turn)) * 9f; + nodes[5] = Boss4中心 + new Vector3(MathF.Sin(a3 + 2 * turn), 0, MathF.Cos(a3 + 2 * turn)) * r3; + + var outHeading = rot - dir * 60f * MathF.PI / 180f; + var v2 = new Vector3(MathF.Sin(outHeading), 0, MathF.Cos(outHeading)); + for (var k = 6; k <= 8; k++) + nodes[k] = nodes[5] + v2 * (6f * (k - 5)); + + Dbg(sa, $"石化地火(48445):src {sid:X8} 起点 {p0:F1} rot {rot:F2} 转向{(dir > 0 ? "+" : "-")}," + + $"终点 {nodes[8]:F1}"); + + for (var k = 0; k < nodes.Length; k++) + { + nodes[k].Y = p0.Y; + var resolve = 石化起爆时间 + k * 石化跳间隔; + var delay = (uint)Math.Max(resolve - 4000, 0); + var dp = sa.FastDp($"石化地火-{sid}-{k}", nodes[k], (uint)(resolve - delay), new Vector2(石化地火半径)); + dp.Delay = delay; + sa.Method.SendDraw(DrawModeEnum.Default, DrawTypeEnum.Circle, dp); + } + } + + // —— 全斩(48455,8.7s读条,12个小目录分三批释放)—— + private readonly object _b4ZenzanLock = new(); + private long _b4ZenzanFirstAt; + + [ScriptMethod(name: "超魔BOSS4 - 全斩", eventType: EventTypeEnum.StartCasting, eventCondition: ["ActionId:48455"])] + public void 超魔B4全斩(Event evt, ScriptAccessory sa) + { + var now = Environment.TickCount64; + long offset; + lock (_b4ZenzanLock) + { + if (now - _b4ZenzanFirstAt > 15000) _b4ZenzanFirstAt = now; // 新一轮机制 + offset = now - _b4ZenzanFirstAt; + } + var delay = (uint)(offset < 1000 ? 0 : Math.Max(7950 - offset, 0)); + var duration = (uint)(8950 - delay); + + var rot = float.TryParse(evt["TargetRotation"], out var tr) ? tr : evt.SourceRotation(); + Dbg(sa, $"全斩(48455):src {evt.SourceId():X8} offset {offset}ms,{delay}ms后显示{duration}ms"); + + var dp = sa.FastDp($"全斩-{evt.SourceId()}", evt.EffectPosition(), duration, new Vector2(8f, 15f)); + dp.Rotation = rot; + dp.Delay = delay; + sa.Method.SendDraw(DrawModeEnum.Default, DrawTypeEnum.Rect, dp); + } + + // VfxEvent 671去火 / 670去雷 / 672去水 + [ScriptMethod(name: "超魔BOSS4 - 属性点名指路", eventType: EventTypeEnum.VfxEvent, eventCondition: ["Id:regex:^(67[0-2])$"])] + public void 超魔B4属性点名指路(Event evt, ScriptAccessory sa) + { + if (evt.SourceId() != sa.Data.Me) return; + var element = evt["Id"] switch { "671" => "火", "670" => "雷", _ => "冰" }; + var word = element == "冰" ? "去水" : $"去{element}"; + Dbg(sa, $"属性点名(VfxEvent {evt["Id"]}):{word}"); + + sa.Method.TTS(word, 3); + sa.Method.TextInfo(word, 5000, false); + + var sectors = 元素扇区中心(); + if (sectors is null) + { + Dbg(sa, $"属性点名:元素区未记录齐,只播报不指路"); + return; + } + + // 上下两个同属性扇区取离自己近的一个,指路到该扇区中心方向往外9m + var mePos = sa.Data.MyObject?.Position ?? Boss4中心; + var meRad = MathF.Atan2(mePos.X - Boss4中心.X, mePos.Z - Boss4中心.Z); + var target = sectors + .Where(s => s.Element == element) + .OrderBy(s => MathF.Abs(WrapPi(s.Rot - meRad))) + .First(); + var guidePos = Boss4中心 + new Vector3(MathF.Sin(target.Rot), 0, MathF.Cos(target.Rot)) * 9f; + + Dbg(sa, $"属性点名:{word},指路 {guidePos:F1}"); + sa.Method.SendDraw(DrawModeEnum.Imgui, DrawTypeEnum.Displacement, + sa.WaypointDp(guidePos, 5000, 3000, "属性点名指路")); + } + + // —— 元素创造(48400)—— + private readonly object _b4CreationLock = new(); + private long _b4CreationUntil; + private readonly Dictionary _b4CreationLineAxis = []; // 元素→直线轴角[0,π) + private List? _b4CreationLineOrder; // 本波直线结算顺序 + private List? _b4CreationPredictedSafe; // 首轮推出的三轮安全元素 + private int _b4CreationRound; + private float? _b4CreationPrevSafeRot; + + private bool 元素创造中() + { + lock (_b4CreationLock) return Environment.TickCount64 < _b4CreationUntil; + } + + [ScriptMethod(name: "超魔BOSS4 - 元素创造开始", eventType: EventTypeEnum.StartCasting, eventCondition: ["ActionId:48400"], userControl: false)] + public void 超魔B4元素创造开始(Event evt, ScriptAccessory sa) + { + if (!Is超魔(sa)) return; + lock (_b4CreationLock) + { + _b4CreationUntil = Environment.TickCount64 + 60000; + _b4CreationLineAxis.Clear(); + _b4CreationLineOrder = null; + _b4CreationPredictedSafe = null; + _b4CreationRound = 0; + _b4CreationPrevSafeRot = null; + } + Dbg(sa, "元素创造(48400):进入元素创造模式60s"); + } + + [ScriptMethod(name: "超魔BOSS4 - 元素创造直线记录", eventType: EventTypeEnum.AddCombatant, eventCondition: ["DataId:regex:^(1930[89]|19310)$"], userControl: false)] + public void 超魔B4元素创造直线记录(Event evt, ScriptAccessory sa) + { + if (!元素创造中()) return; + if (!uint.TryParse(evt["DataId"], out var dataId)) return; + var elem = dataId switch { 19308u => "冰", 19309u => "火", _ => "雷" }; + var pos = evt.SourcePosition(); + var axis = MathF.Atan2(pos.X - Boss4中心.X, pos.Z - Boss4中心.Z); + axis = (axis % MathF.PI + MathF.PI) % MathF.PI; // 直线上下对称,取[0,π) + + lock (_b4CreationLock) + { + // 上一波3轮已用完,收到新直线说明第二波开始:清空重记 + if (_b4CreationRound >= 3) + { + _b4CreationLineAxis.Clear(); + _b4CreationLineOrder = null; + _b4CreationPredictedSafe = null; + _b4CreationRound = 0; + } + if (!_b4CreationLineAxis.TryAdd(elem, axis)) return; // 同元素成对出现,只记第一个 + } + Dbg(sa, $"元素创造直线:{elem} 轴角 {axis * 180f / MathF.PI:F1}°"); + } + + [ScriptMethod(name: "超魔BOSS4 - 元素创造安全区", eventType: EventTypeEnum.ObjectChanged, eventCondition: ["Operate:Add", "DataId:regex:^(201524[345])$"], suppress: 1000)] + public void 超魔B4元素创造安全区(Event evt, ScriptAccessory sa) + { + if (!元素创造中()) return; + if (!uint.TryParse(evt["DataId"], out var dataId)) return; + var ringElem = dataId switch { 2015243u => "火", 2015244u => "冰", _ => "雷" }; + + Dictionary zoneRots; + lock (_zoneLock) + { + if (_fireZoneRot is null || _waterZoneRot is null || _thunderZoneRot is null) + { + Dbg(sa, "元素创造安全区:元素区未记录齐,跳过"); + return; + } + zoneRots = new Dictionary + { + ["火"] = _fireZoneRot.Value, + ["冰"] = _waterZoneRot.Value, + ["雷"] = _thunderZoneRot.Value, + }; + } + + string lineElem; + int round; + lock (_b4CreationLock) + { + if (_b4CreationLineOrder is null) + { + if (_b4CreationLineAxis.Count < 3) + { + Dbg(sa, "元素创造安全区:直线未记录齐,跳过"); + return; + } + // 顺时针旋转 = 轴角减小,转到同属性区的角度 = (直线轴角-区轴角) mod π,小者先结算 + _b4CreationLineOrder = _b4CreationLineAxis + .OrderBy(kv => ((kv.Value - zoneRots[kv.Key]) % MathF.PI + MathF.PI) % MathF.PI) + .Select(kv => kv.Key) + .ToList(); + Dbg(sa, $"元素创造直线结算顺序:{string.Join("→", _b4CreationLineOrder)}"); + } + if (_b4CreationRound >= 3) return; // 本波已结束,等下一波直线重置 + round = _b4CreationRound++; + lineElem = _b4CreationLineOrder[round]; + } + + var safeElem = new[] { "火", "冰", "雷" }.First(e => e != ringElem && e != lineElem); + + // 三轮的环也是火冰雷各一次且环≠线:首轮环一出现即可唯一推出后两轮,汇总显示三轮安全区 + if (round == 0) + { + List lines; + lock (_b4CreationLock) lines = _b4CreationLineOrder!.ToList(); + var rest = new[] { "火", "冰", "雷" }.Where(e => e != ringElem).ToList(); + string ring1, ring2; + if (lines[1] == ringElem) + { + ring2 = rest.First(e => e != lines[2]); + ring1 = rest.First(e => e != ring2); + } + else + { + ring1 = rest.First(e => e != lines[1]); + ring2 = rest.First(e => e != ring1); + } + var safe1 = new[] { "火", "冰", "雷" }.First(e => e != ring1 && e != lines[1]); + var safe2 = new[] { "火", "冰", "雷" }.First(e => e != ring2 && e != lines[2]); + lock (_b4CreationLock) _b4CreationPredictedSafe = [safeElem, safe1, safe2]; + Dbg(sa, $"元素创造预测:环序{ringElem}→{ring1}→{ring2},安全{safeElem}→{safe1}→{safe2}"); + sa.Method.TextInfo($"去{safeElem}→{safe1}→{safe2}", 15000, false); + } + else + { + lock (_b4CreationLock) + { + if (_b4CreationPredictedSafe is { } pred && pred[round] != safeElem) + Dbg(sa, $"元素创造预测失误:第{round + 1}轮实际安全{safeElem}≠预测{pred[round]}"); + } + } + + // 安全元素上下两块扇区:首轮取离自己近的,之后取上一块逆时针(角度增加)方向最近的 + var zr = zoneRots[safeElem]; + float[] candidates = [zr, zr + MathF.PI]; + float safeRot; + lock (_b4CreationLock) + { + if (_b4CreationPrevSafeRot is null) + { + var mePos = sa.Data.MyObject?.Position ?? Boss4中心; + var meRad = MathF.Atan2(mePos.X - Boss4中心.X, mePos.Z - Boss4中心.Z); + safeRot = candidates.OrderBy(c => MathF.Abs(WrapPi(c - meRad))).First(); + } + else + { + var prev = _b4CreationPrevSafeRot.Value; + safeRot = candidates + .OrderBy(c => ((c - prev) % (2 * MathF.PI) + 2 * MathF.PI) % (2 * MathF.PI)) + .First(); + } + _b4CreationPrevSafeRot = safeRot; + } + + // 本轮环出现时上一轮还有约2.4s判定(6.8-4.4):非首轮延迟到上一轮判定后再显示 + var delay = (uint)(round == 0 ? 0 : 2400); + var duration = 6800u - delay; + Dbg(sa, $"元素创造第{round + 1}轮:环{ringElem}+线{lineElem},安全={safeElem}," + + $"扇区角 {safeRot * 180f / MathF.PI:F0}°,{delay}ms后显示{duration}ms"); + + var dp = sa.FastDp($"元素创造安全区-{round}", Boss4中心, duration, new Vector2(40f), safe: true); + dp.Rotation = safeRot; + dp.Radian = 60f * MathF.PI / 180f; + dp.Delay = delay; + sa.Method.SendDraw(DrawModeEnum.Default, DrawTypeEnum.Fan, dp); + + var guidePos = Boss4中心 + new Vector3(MathF.Sin(safeRot), 0, MathF.Cos(safeRot)) * 9f; + sa.Method.SendDraw(DrawModeEnum.Imgui, DrawTypeEnum.Displacement, + sa.WaypointDp(guidePos, duration, delay, $"元素创造安全区指路-{round}")); + } + + // —— 预言(48412)—— + private const uint 幻影DataId = 19311; + private const int 预言结算时间 = 9700; // 预言现象出现到结算 + + [ScriptMethod(name: "超魔BOSS4 - 预言现象", eventType: EventTypeEnum.AddCombatant, eventCondition: ["DataId:19307"])] + public void 超魔B4预言现象(Event evt, ScriptAccessory sa) => _ = B4预言现象核心(evt, sa); + + private async Task B4预言现象核心(Event evt, ScriptAccessory sa) + { + var oid = evt.SourceId(); + var spawnPos = evt.SourcePosition(); + var startAt = Environment.TickCount64; + + // 等状态附加后读球上的2552:1101钢铁 / 1100月环 + var param = 0; + while (param == 0 && Environment.TickCount64 - startAt < 4000) + { + await Task.Delay(300); + if (sa.Data.Objects.SearchById(oid) is not IBattleChara orb) return; + foreach (var s in orb.StatusList) + { + if (s.StatusId != 2552 || (s.Param != 1100 && s.Param != 1101)) continue; + param = s.Param; + break; + } + } + if (param == 0) + { + Dbg(sa, $"预言现象 {oid:X8}:未读到2552(1100/1101),跳过"); + return; + } + var isDonut = param == 1100; + Dbg(sa, $"预言现象 {oid:X8}:{(isDonut ? "月环" : "钢铁")},等待飘移判向"); + + // 5s后开始飘移:6s起每0.3s取实时位置,飘出0.7m即可判定目标幻影 + var wait = 6000 - (int)(Environment.TickCount64 - startAt); + if (wait > 0) await Task.Delay(wait); + + while (Environment.TickCount64 - startAt < 预言结算时间 - 700) + { + if (sa.Data.Objects.SearchById(oid) is not { } orbObj) return; + var moved = orbObj.Position - spawnPos; + moved.Y = 0; + if (moved.Length() >= 0.7f) + { + var phantom = sa.Data.Objects + .Where(o => o != null && o.DataId == 幻影DataId) + .OrderByDescending(o => + { + var to = o.Position - spawnPos; + to.Y = 0; + return to.Length() < 0.1f ? float.MinValue + : Vector3.Dot(Vector3.Normalize(moved), Vector3.Normalize(to)); + }) + .FirstOrDefault(); + if (phantom is null) + { + Dbg(sa, $"预言现象 {oid:X8}:场上找不到幻影(19311),跳过"); + return; + } + + var remain = (uint)Math.Max(预言结算时间 - (Environment.TickCount64 - startAt), 1000); + Dbg(sa, $"预言现象 {oid:X8}:目标幻影 {phantom.EntityId:X8} pos {phantom.Position:F1}," + + $"{(isDonut ? "月环" : "钢铁")}显示{remain}ms"); + + if (isDonut) + { + var dp = sa.FastDp($"预言月环-{oid:X8}", phantom.Position, remain, new Vector2(15f)); + dp.InnerScale = new Vector2(4f); + dp.Radian = float.Pi * 2; + sa.Method.SendDraw(DrawModeEnum.Default, DrawTypeEnum.Donut, dp); + } + else + { + var dp = sa.FastDp($"预言钢铁-{oid:X8}", phantom.Position, remain, new Vector2(10f)); + sa.Method.SendDraw(DrawModeEnum.Default, DrawTypeEnum.Circle, dp); + } + return; + } + await Task.Delay(300); + } + Dbg(sa, $"预言现象 {oid:X8}:到结算前仍未观测到飘移,跳过"); + } + + #endregion + + #region 钟灵时钟 + // 进入 MapId 1187 开始检测,收到 EnvControl Index 17 flag 8 结束(离开该图也结束)。 + // flag 给出本轮要求把钟推到的目标时间,钟面固定从 12:00 起走,指针只前进。 + // 钟灵推进指针:19770最大=时针+3格 / 19771次大=时针+1格 / 19772次小=分针+3格(15分) / 19773最小=分针+1格(5分)。 + // 全部走首击判定:谁第一下打到这只钟灵,就按 EntityId 计一次格数(被秒了也算),同时判开怪的是不是 T。 + private const uint 钟灵MapId = 1187; + private const int 时钟结束Flag = 8; + private static readonly Dictionary 时钟目标表 = new() + { + [2] = (8, 45), + [16] = (9, 40), + [32] = (10, 45), + [64] = (11, 30), + [128] = (5, 55), + [256] = (8, 50), + [512] = (10, 50), + [1024] = (11, 35), + [2048] = (11, 45), + }; + + // DataId → 该钟灵推进的格数与名称 + private static readonly Dictionary 钟灵表 = new() + { + [19770] = (3, 0, "最大"), + [19771] = (1, 0, "次大"), + [19772] = (0, 3, "次小"), + [19773] = (0, 1, "最小"), + }; + + private readonly object _钟灵Lock = new(); + private bool _钟灵检测中; + private (int 时, int 分)? _钟灵目标; + private (int 时格, int 分格) _钟灵已投入; + private readonly HashSet _钟灵首击 = []; // 已处理过首击的 EntityId,同一只只算一次 + + // 进 1187 开检测、离开就关;ChangeMap 会重复触发,状态没变就不输出 + [ScriptMethod(name: "钟灵时钟 - 检测开关", eventType: EventTypeEnum.ChangeMap, userControl: false)] + public void 钟灵检测开关(Event evt, ScriptAccessory sa) + { + var 在钟灵图 = 钟灵时钟启用 && uint.TryParse(evt["MapId"], out var mapId) && mapId == 钟灵MapId; + lock (_钟灵Lock) + { + if (_钟灵检测中 == 在钟灵图) return; + _钟灵检测中 = 在钟灵图; + 重置钟灵进度(); + } + if (!在钟灵图) 结束钟灵检测(sa); + Dbg(sa, 在钟灵图 ? $"钟灵时钟:进入 {钟灵MapId},开始检测" : "钟灵时钟:离开该图,停止检测"); + } + + // 调用方必须持有 _钟灵Lock + private void 重置钟灵进度() + { + _钟灵目标 = null; + _钟灵已投入 = (0, 0); + _钟灵首击.Clear(); + } + + private void 结束钟灵检测(ScriptAccessory sa) + { + lock (_钟灵Lock) + { + _钟灵检测中 = false; + 重置钟灵进度(); + } + sa.Method.RemoveDraw("钟灵拉错-.*"); + } + + [ScriptMethod(name: "钟灵时钟 - 目标时间播报", eventType: EventTypeEnum.EnvControl, eventCondition: ["Index:17"])] + public void 钟灵时钟播报(Event evt, ScriptAccessory sa) + { + if (!钟灵时钟启用) { 结束钟灵检测(sa); return; } // 运行中关掉设置:顺手收干净 + + bool 检测中; + lock (_钟灵Lock) 检测中 = _钟灵检测中; + if (!检测中) + { + Dbg(sa, $"钟灵时钟:不在检测窗口内,忽略 flag {evt["Flag"]}"); + return; + } + if (!int.TryParse(evt["Flag"], out var flag)) + { + Dbg(sa, $"钟灵时钟:Flag 解析失败(原始 {evt["Flag"]})"); + return; + } + if (flag == 时钟结束Flag) + { + 结束钟灵检测(sa); + Dbg(sa, "钟灵时钟:flag 8,结束检测"); + return; + } + if (!时钟目标表.TryGetValue(flag, out var 目标)) + { + Dbg(sa, $"钟灵时钟:未知 flag {flag},未收录"); + return; + } + + // 新一轮:钟面回到 12:00,上一轮的首击计数作废 + lock (_钟灵Lock) + { + 重置钟灵进度(); + _钟灵目标 = 目标; + } + sa.Method.RemoveDraw("钟灵拉错-.*"); + + var 需求 = 拆解格数(目标.时 % 12, 目标.分 / 5 % 12); + Dbg(sa, $"钟灵时钟:flag {flag} → 目标 {目标.时}:{目标.分:00},需 最大{需求.最大} 次大{需求.次大} 次小{需求.次小} 最小{需求.最小}"); + 播报钟灵需求(sa); + } + + // 全量 EnvControl 日志,用来补收未收录的 flag(仅 Debug 输出开启时可见) + // [ScriptMethod(name: "钟灵时钟 - EnvControl全量日志", eventType: EventTypeEnum.EnvControl, userControl: false)] + // public void 钟灵EnvControl日志(Event evt, ScriptAccessory sa) + // { + // if (!钟灵时钟启用) return; + // Dbg(sa, $"EnvControl Index={evt["Index"]} Flag={evt["Flag"]} DirectorId={evt["DirectorId"]} Id={evt["Id"]}"); + // } + + // 首击 = 这只钟灵被打的第一下:计一次格数并重播需求,同时判开怪的是不是 T,一只只处理一次。 + // 这里不加 eventCondition:TargetDataId 键在 ActionEffect 上没实测过,改用 TargetId 查对象判 DataId; + [ScriptMethod(name: "钟灵时钟 - 首击计数与非T提醒", eventType: EventTypeEnum.ActionEffect)] + public void 钟灵首击检查(Event evt, ScriptAccessory sa) + { + if (!钟灵时钟启用) return; + lock (_钟灵Lock) if (!_钟灵检测中) return; + + var 目标Id = evt.TargetId(); + if (目标Id == 0) return; + var 钟灵 = sa.Data.Objects.SearchById(目标Id); + if (钟灵 is null || !钟灵表.TryGetValue(钟灵.DataId, out var 格)) return; + + var 计入 = false; + lock (_钟灵Lock) + { + if (!_钟灵首击.Add(目标Id)) return; // 只认第一下 + if (_钟灵目标 is not null) + { + _钟灵已投入 = (_钟灵已投入.时格 + 格.时格, _钟灵已投入.分格 + 格.分格); + 计入 = true; + Dbg(sa, $"钟灵首击:{格.名} {目标Id:X8},累计 时针{_钟灵已投入.时格}格 分针{_钟灵已投入.分格}格"); + } + } + if (计入) 播报钟灵需求(sa); + + // 宠物/召唤兽先蹭到第一下时算到主人头上 + var 来源 = sa.Data.Objects.SearchById(evt.SourceId()); + var 打手 = 来源 as IPlayerCharacter + ?? (来源 is { OwnerId: not 0 } ? sa.Data.Objects.SearchById(来源.OwnerId) as IPlayerCharacter : null); + if (打手 is null) return; + if (打手.IsTank()) + { + Dbg(sa, $"钟灵首击:{格.名} {目标Id:X8} 由 {打手.Name} 开打(T)"); + return; + } + + var dp = sa.Data.GetDefaultDrawProperties(); + dp.Name = $"钟灵拉错-{目标Id}"; + dp.Color = new Vector4(1, 0, 0, 1); + dp.Owner = 目标Id; + dp.Scale = new Vector2(钟灵.HitboxRadius); + dp.InnerScale = new Vector2(钟灵.HitboxRadius + 1f); + dp.Radian = float.Pi * 2; + dp.DestoryAt = 60000; + sa.Method.SendDraw(DrawModeEnum.Imgui, DrawTypeEnum.Circle, dp); + + sa.Method.TextInfo($"钟灵首击异常:{格.名} 由 {打手.Name} 开怪,检查是否拉错", 8000, true); + sa.Method.TTS($"{格.名}钟灵首击异常", 3); + Dbg(sa, $"钟灵首击异常:{格.名} {目标Id:X8} 由 {打手.Name} 开怪"); + } + + private void 播报钟灵需求(ScriptAccessory sa) + { + (int 时, int 分) 目标; + (int 时格, int 分格) 已投入; + lock (_钟灵Lock) + { + if (!_钟灵检测中 || _钟灵目标 is null) return; + 目标 = _钟灵目标.Value; + 已投入 = _钟灵已投入; + } + + var 剩余时格 = 目标.时 % 12 - 已投入.时格; + var 剩余分格 = 目标.分 / 5 % 12 - 已投入.分格; + + List 超出 = []; + if (剩余时格 < 0) 超出.Add($"时针多{-剩余时格}格"); + if (剩余分格 < 0) 超出.Add($"分针多{-剩余分格}格"); + + var 需求 = 拆解格数(Math.Max(剩余时格, 0), Math.Max(剩余分格, 0)); + List<(int 数量, string 名)> 项 = [(需求.最大, "最大"), (需求.次大, "次大"), (需求.次小, "次小"), (需求.最小, "最小")]; + 项.RemoveAll(x => x.数量 <= 0); + + var 还需 = 项.Count == 0 ? "" : $"还需 {string.Join(" ", 项.Select(x => $"{x.名}×{x.数量}"))}"; + var 头 = $"目标 {目标.时}:{目标.分:00}"; + + if (超出.Count > 0) + { + var 超文 = string.Join(",", 超出); + // sa.Method.TextInfo($"{头} 打多了:{超文}{(还需 == "" ? "" : $"/{还需}")}", 1500, true); + // sa.Method.TTS("打多了", 3); + sa.Method.SendChat($"/e {头} 打多了:{超文}{(还需 == "" ? "" : $"/{还需}")} "); + return; + } + + if (项.Count == 0) + { + // sa.Method.TextInfo($"{头} 已凑齐", 4000, false); + sa.Method.TTS("齐了", 3); + return; + } + + // sa.Method.TextInfo($"{头} → {还需}", 1500, false); + // sa.Method.TTS(string.Join("", 项.Select(x => $"{x.名}{x.数量}")), 3); + sa.Method.SendChat($"/e {头} → {还需} "); + } + + // 时针每格1小时、分针每格5分钟:先用大格(3格)再用小格(1格),即击杀数最少的组合 + private static (int 最大, int 次大, int 次小, int 最小) 拆解格数(int 时格, int 分格) + => (时格 / 3, 时格 % 3, 分格 / 3, 分格 % 3); + + #endregion + + #region 排雷 + // 塔内地雷点位表见文件末尾 地雷数据库,按 MapId 分类。进入有雷的地图自动画出全部点位, + // 盗贼扫雷 / 猎人排雷 / 雷实体生成 / 雷爆炸 / 有人踩过而没炸 这五种情况都会把对应点位抹掉。 + private const uint 大雷DataId = 2014585; + private const uint 小雷DataId = 2014584; + private const long 雷点显示时长 = 1800000; + private const float 雷点匹配半径 = 1.5f; // 事件坐标 → 点位表的匹配容差 + private const float 踩点判定半径 = 1.5f; // 比雷的触发范围保守,避免擦边路过就误消 + private const float 踩点高度容差 = 3f; // 塔内各区域 Y 不同,防止上下层坐标重叠误消 + private const long 踩点扫描间隔 = 100; // ms,不必每帧全量比对 + + private bool _雷点已显示; + private uint _当前MapId; + private readonly object _mineLock = new(); + private readonly HashSet _踩点已排除 = []; // 已踩过的点,避免每帧重复 RemoveDraw + private string? _踩点扫描Guid; + private long _上次踩点扫描; + + private static string 雷点名(int g, int m) => $"FTM_Mine_G{g}_M{m}"; + private static string 爆点名(int g, int m) => $"FTM_Boom_G{g}_M{m}"; + + // 切图 / 团灭重置钩子 + public void Init(ScriptAccessory sa) + { + lock (_mineLock) + { + _雷点已显示 = false; + _当前MapId = 0; + _踩点已排除.Clear(); + } + 结束钟灵检测(sa); + sa.Method.RemoveDraw("FTM_(Mine|Boom)_.*"); + 注册踩点扫描(sa); + } + + [ScriptMethod(name: "排雷 - 手动开关地雷显示", eventType: EventTypeEnum.Chat, eventCondition: ["Type:Echo", "Message:新月排雷"])] + public void 手动开关排雷(Event evt, ScriptAccessory sa) + { + if (!排雷显示) { sa.Method.TextInfo("排雷功能已在设置中关闭。", 2000); return; } + lock (_mineLock) + { + if (!地雷数据库.MinesByMap.ContainsKey(_当前MapId)) + { + sa.Method.TextInfo("当前地图无地雷数据。", 2000); + return; + } + + _雷点已显示 = !_雷点已显示; + if (_雷点已显示) + { + 画出地雷点(sa, _当前MapId); + sa.Method.TextInfo("显示地雷位置", 2000); + } + else + { + sa.Method.RemoveDraw("FTM_Mine_.*"); + sa.Method.TextInfo("隐藏地雷位置", 2000); + } + } + } + + #region 排雷 - 进图 + [ScriptMethod(name: "排雷 - 切图处理", eventType: EventTypeEnum.ChangeMap, userControl: false)] + public void 排雷切图(Event evt, ScriptAccessory sa) + { + if (!uint.TryParse(evt["MapId"], out var mapId)) return; + if (mapId is 1183 or 1184) 沿用上一张图标记(mapId, sa); + else if (地雷数据库.MinesByMap.ContainsKey(mapId)) 进入雷区(mapId, sa); + else 离开雷区(mapId, sa); + } + + // 无雷区域:收掉上一张图残留的标记并复位状态,避免踩点扫描继续按老图点位表误消点。 + private void 离开雷区(uint mapId, ScriptAccessory sa) + { + lock (_mineLock) + { + if (mapId == _当前MapId) return; // ChangeMap 会重复触发 + _当前MapId = mapId; + _雷点已显示 = false; + _踩点已排除.Clear(); + sa.Method.RemoveDraw("FTM_(Mine|Boom)_.*"); + } + Dbg(sa, $"进入无雷区域 ({mapId}),已清除地雷标记。"); + } + + // 1182 → 1183 → 1184 是同一片区域随进度换的 MapId,点位表是 1182 的子集。 + // 换到 1183/1184 时不动任何东西:既不清图也不重画,继续沿用上一张图已经排掉一部分的标记 + // (重画会把已经确认过的点又变回来),_当前MapId 也保持不变,扫雷/爆炸/踩点仍按原图的点位索引对应。 + private void 沿用上一张图标记(uint mapId, ScriptAccessory sa) + => Dbg(sa, $"进入 {mapId},沿用上一张图({_当前MapId})的地雷标记。"); + + private async void 进入雷区(uint mapId, ScriptAccessory sa) + { + if (!排雷显示) return; + if (mapId == _当前MapId) return; // ChangeMap 会重复触发 + + uint newMapId; + lock (_mineLock) + { + _当前MapId = mapId; + newMapId = _当前MapId; + sa.Method.RemoveDraw("FTM_(Mine|Boom)_.*"); + } + await Task.Delay(50); + lock (_mineLock) + { + if (_当前MapId != newMapId) return; + _雷点已显示 = true; + 画出地雷点(sa, newMapId); + Dbg(sa, $"进入地雷区域 ({newMapId}),已自动显示标记。"); + } + } + #endregion + + private void 画出地雷点(ScriptAccessory sa, uint mapId) + { + if (!地雷数据库.MinesByMap.TryGetValue(mapId, out var mineGroups)) return; + _踩点已排除.Clear(); + 注册踩点扫描(sa); + + var 小雷色 = new Vector4(1.0f, 0.65f, 0.0f, 2.0f); + var 大雷色 = new Vector4(0.86f, 0.08f, 0.23f, 2.0f); + + for (var g = 0; g < mineGroups.Count; g++) + { + for (var m = 0; m < mineGroups[g].Mines.Count; m++) + { + var mine = mineGroups[g].Mines[m]; + var dp = sa.Data.GetDefaultDrawProperties(); + dp.Name = 雷点名(g, m); + dp.Position = mine.Position; + dp.DestoryAt = 雷点显示时长; + dp.Color = mine.IsLarge ? 大雷色 : 小雷色; + dp.Scale = new Vector2(4f); + sa.Method.SendDraw(DrawModeEnum.Imgui, DrawTypeEnum.Circle, dp); + } + } + } + + // 命中点位表就回调 (组号, 点号),radius 内全部命中;stopAtFirst 只处理第一个 + private void 遍历命中雷点(Vector3 pos, float radius, bool stopAtFirst, Action onHit) + { + if (!地雷数据库.MinesByMap.TryGetValue(_当前MapId, out var mineGroups)) return; + for (var g = 0; g < mineGroups.Count; g++) + { + for (var m = 0; m < mineGroups[g].Mines.Count; m++) + { + if (Vector3.Distance(mineGroups[g].Mines[m].Position, pos) > radius) continue; + onHit(g, m); + if (stopAtFirst) return; + } + } + } + + // 41648 盗贼扫雷:以自身为心 15m 内的雷位全部确认 + [ScriptMethod(name: "排雷 - 盗贼扫雷", eventType: EventTypeEnum.ActionEffect, eventCondition: ["ActionId:41648"])] + public void 盗贼扫雷(Event evt, ScriptAccessory sa) + { + if (!排雷显示) return; + 遍历命中雷点(evt.SourcePosition(), 15f, false, (g, m) => sa.Method.RemoveDraw(雷点名(g, m))); + } + + // 41601 猎人排雷:落点 9m 内的雷位全部确认 + [ScriptMethod(name: "排雷 - 猎人排雷", eventType: EventTypeEnum.ActionEffect, eventCondition: ["ActionId:41601"])] + public void 猎人排雷(Event evt, ScriptAccessory sa) + { + if (!排雷显示) return; + 遍历命中雷点(evt.EffectPosition(), 9f, false, (g, m) => sa.Method.RemoveDraw(雷点名(g, m))); + } + + [ScriptMethod(name: "排雷 - 大雷生成", eventType: EventTypeEnum.ObjectChanged, eventCondition: ["Operate:Add", "DataId:2014585"], userControl: false)] + public void 大雷生成(Event evt, ScriptAccessory sa) => 雷实体生成(evt.SourcePosition(), true, sa); + + [ScriptMethod(name: "排雷 - 小雷生成", eventType: EventTypeEnum.ObjectChanged, eventCondition: ["Operate:Add", "DataId:2014584"], userControl: false)] + public void 小雷生成(Event evt, ScriptAccessory sa) => 雷实体生成(evt.SourcePosition(), false, sa); + + // 雷实体现形 = 这一组已经确定,抹掉整组点位,改画实际爆炸范围(大雷 30m / 小雷 7m) + private async void 雷实体生成(Vector3 pos, bool isLarge, ScriptAccessory sa) + { + if (!排雷显示) return; + if (!地雷数据库.MinesByMap.TryGetValue(_当前MapId, out var mineGroups)) return; + + var hitG = -1; + var hitM = -1; + 遍历命中雷点(pos, 雷点匹配半径, true, (g, m) => { hitG = g; hitM = m; }); + if (hitG < 0) return; + + for (var m = 0; m < mineGroups[hitG].Mines.Count; m++) + sa.Method.RemoveDraw(雷点名(hitG, m)); + + await Task.Delay(50); // 等 Remove 处理完再画,避免同帧被顺带清掉 + + var dp = sa.Data.GetDefaultDrawProperties(); + dp.Name = 爆点名(hitG, hitM); + dp.Position = pos; + dp.Color = new Vector4(1.0f, 0.0f, 0.0f, 0.6f); + dp.DestoryAt = 1000000; + dp.Scale = isLarge ? new Vector2(30f) : new Vector2(7f); + sa.Method.SendDraw(DrawModeEnum.Imgui, DrawTypeEnum.Circle, dp); + } + + // 42050 / 42051 雷爆炸:炸完了,点位和爆炸圈一起收掉 + [ScriptMethod(name: "排雷 - 雷爆炸", eventType: EventTypeEnum.ActionEffect, eventCondition: ["ActionId:regex:^(42050|42051)$"], userControl: false)] + public void 雷爆炸(Event evt, ScriptAccessory sa) + { + 遍历命中雷点(evt.SourcePosition(), 雷点匹配半径, true, (g, m) => + { + sa.Method.RemoveDraw(雷点名(g, m)); + sa.Method.RemoveDraw(爆点名(g, m)); + }); + } + + private void 注册踩点扫描(ScriptAccessory sa) + { + if (_踩点扫描Guid != null) return; + _踩点扫描Guid = sa.Method.RegistFrameworkUpdateAction(() => 踩点扫描(sa)); + } + + // 有玩家(含非小队玩家)走进某雷位而没炸,说明这点没雷,抹掉标记 + private void 踩点扫描(ScriptAccessory sa) + { + var now = Environment.TickCount64; + if (now - _上次踩点扫描 < 踩点扫描间隔) return; + _上次踩点扫描 = now; + + lock (_mineLock) + { + if (!排雷显示) + { + // 运行中关掉设置:立刻收掉已有绘图 + if (!_雷点已显示) return; + _雷点已显示 = false; + sa.Method.RemoveDraw("FTM_(Mine|Boom)_.*"); + return; + } + if (!_雷点已显示) return; + if (!地雷数据库.MinesByMap.TryGetValue(_当前MapId, out var mineGroups)) return; + + var players = sa.Data.Objects + .OfType() + .Where(p => !p.IsDead) + .Select(p => p.Position) + .ToList(); + if (players.Count == 0) return; + + for (var g = 0; g < mineGroups.Count; g++) + { + for (var m = 0; m < mineGroups[g].Mines.Count; m++) + { + var name = 雷点名(g, m); + if (_踩点已排除.Contains(name)) continue; + var minePos = mineGroups[g].Mines[m].Position; + if (!players.Any(p => MathF.Abs(p.Y - minePos.Y) <= 踩点高度容差 + && DistXZ(p, minePos) <= 踩点判定半径)) continue; + + _踩点已排除.Add(name); + sa.Method.RemoveDraw(name); + } + } + } + } + #endregion +} + +#region Helpers + +public static class EventExtensions +{ + public static float SourceRotation(this Event evt) + => JsonConvert.DeserializeObject(evt["SourceRotation"]); + private static bool ParseHexId(string? idStr, out uint id) + { + id = 0; + if (string.IsNullOrEmpty(idStr)) return false; + try + { + var idStr2 = idStr.Replace("0x", ""); + id = uint.Parse(idStr2, System.Globalization.NumberStyles.HexNumber); + return true; + } + catch + { + return false; + } + } + + public static uint ActionId(this Event evt) => JsonConvert.DeserializeObject(evt["ActionId"]); + public static uint SourceId(this Event evt) => ParseHexId(evt["SourceId"], out var id) ? id : 0; + public static uint TargetId(this Event evt) => ParseHexId(evt["TargetId"], out var id) ? id : 0; + public static Vector3 SourcePosition(this Event evt) => JsonConvert.DeserializeObject(evt["SourcePosition"]); + public static Vector3 EffectPosition(this Event evt) => JsonConvert.DeserializeObject(evt["EffectPosition"]); + public static uint DirectorId(this Event evt) => ParseHexId(evt["DirectorId"], out var id) ? id : 0; +} + +public static class ScriptAccessoryExtensions +{ + public static int MyIndex(this ScriptAccessory sa) => sa.Data.PartyList.IndexOf(sa.Data.Me); + + public static IGameObject? FindByDataId(this ScriptAccessory sa, params uint[] dataIds) + => sa.Data.Objects.FirstOrDefault(x => x != null && dataIds.Contains(x.DataId)); + + // 修改物体可选中状态 + public static void SetTargetable(this ScriptAccessory sa, IGameObject? obj, bool targetable) + { + if (obj == null || !obj.IsValid()) + { + sa.Log.Error($"传入的IGameObject不合法。"); + return; + } + unsafe + { + GameObject* charaStruct = (GameObject*)obj.Address; + if (targetable) + { + if (obj.IsDead || obj.IsTargetable) return; + charaStruct->TargetableStatus |= ObjectTargetableFlags.IsTargetable; + } + else + { + if (!obj.IsTargetable) return; + charaStruct->TargetableStatus &= ~ObjectTargetableFlags.IsTargetable; + } + } + sa.Log.Debug($"SetTargetable {targetable} => {obj.Name} {obj}"); + } + + public static DrawPropertiesEdit FastDp(this ScriptAccessory sa, string name, Vector3 pos, uint duration, Vector2 scale, bool safe = false) + { + var dp = sa.Data.GetDefaultDrawProperties(); + dp.Name = name; + dp.Color = safe ? sa.Data.DefaultSafeColor : sa.Data.DefaultDangerColor; + dp.Position = pos; + dp.DestoryAt = duration; + dp.Scale = scale; + return dp; + } + + // public static DrawPropertiesEdit WaypointDp(this ScriptAccessory sa, Vector3 pos, uint duration, uint delay = 0, string name = "Waypoint") + // { + // var dp = sa.Data.GetDefaultDrawProperties(); + // dp.Name = name; + // dp.Color = sa.Data.DefaultSafeColor; + // dp.Owner = sa.Data.Me; + // dp.TargetPosition = pos; + // dp.DestoryAt = duration; + // dp.Delay = delay; + // dp.Scale = new Vector2(2); + // dp.ScaleMode = ScaleMode.YByDistance; + // return dp; + // } + public static DrawPropertiesEdit WaypointDp(this ScriptAccessory sa, Vector3 target, uint duration, uint delay = 0, string name = "Waypoint", Vector4? color = null) + { + var dp = sa.Data.GetDefaultDrawProperties(); + dp.Name = name; + dp.Color = color ?? sa.Data.DefaultSafeColor; + dp.Owner = sa.Data.Me; // 仍然绑定自己 + dp.TargetPosition = target; + dp.DestoryAt = duration; + dp.Delay = delay; + dp.Scale = new Vector2(2); + dp.ScaleMode = ScaleMode.YByDistance; + return dp; + } + + public static DrawPropertiesEdit WaypointFromToDp(this ScriptAccessory sa, Vector3 from, Vector3 to, uint duration, uint delay = 0, string name = "WaypointFromTo", Vector4? color = null) + { + var dp = sa.Data.GetDefaultDrawProperties(); + dp.Name = name; + dp.Color = color ?? sa.Data.DefaultSafeColor; + dp.Owner = 0; // 不绑定任何物体,防止跟着人跑 + dp.Position = from; // 起点 + dp.TargetPosition = to; // 终点 + dp.DestoryAt = duration; + dp.Delay = delay; + dp.Scale = new Vector2(2); + dp.ScaleMode = ScaleMode.YByDistance; + return dp; + } +} + +#endregion + +#region 排雷点位表 + +internal static class 地雷数据库 +{ + public struct Mine + { + public Vector3 Position; + public bool IsLarge; + } + + public class MineGroup + { + public List Mines = new List(); + } + + // 数据已按地图ID分类 + public static readonly Dictionary> MinesByMap = new Dictionary> + { + // --- Map ID: 1178 Data --- (3 组 / 18 雷) + [1178] = new List + { + // 区域 26: x[554.5~568.5] z[922.5~929.5] + new MineGroup { Mines = { + new Mine { Position = new Vector3(554.5f, -699.901f, 922.5f), IsLarge = false }, new Mine { Position = new Vector3(554.5f, -699.9f, 929.5f), IsLarge = false }, + new Mine { Position = new Vector3(561.5f, -700f, 922.5f), IsLarge = false }, new Mine { Position = new Vector3(561.5f, -700.001f, 929.5f), IsLarge = false }, + new Mine { Position = new Vector3(568.5f, -699.941f, 922.5f), IsLarge = false }, new Mine { Position = new Vector3(568.5f, -699.941f, 929.5f), IsLarge = false }, + }}, + // 区域 27: x[631.5~645.5] z[922.5~929.5] + new MineGroup { Mines = { + new Mine { Position = new Vector3(631.5f, -699.941f, 922.5f), IsLarge = false }, new Mine { Position = new Vector3(631.5f, -699.941f, 929.5f), IsLarge = false }, + new Mine { Position = new Vector3(638.5f, -700.001f, 922.5f), IsLarge = false }, new Mine { Position = new Vector3(638.5f, -700.001f, 929.5f), IsLarge = false }, + new Mine { Position = new Vector3(645.5f, -699.901f, 922.5f), IsLarge = false }, new Mine { Position = new Vector3(645.5f, -699.901f, 929.5f), IsLarge = false }, + }}, + // 区域 28: x[596.5~603.5] z[943~957] + new MineGroup { Mines = { + new Mine { Position = new Vector3(596.5f, -700f, 943f), IsLarge = false }, new Mine { Position = new Vector3(596.5f, -699.94f, 950f), IsLarge = false }, + new Mine { Position = new Vector3(596.5f, -700f, 957f), IsLarge = false }, new Mine { Position = new Vector3(603.5f, -700f, 943f), IsLarge = false }, + new Mine { Position = new Vector3(603.5f, -699.94f, 950f), IsLarge = false }, new Mine { Position = new Vector3(603.5f, -700f, 957f), IsLarge = false }, + }}, + }, + // --- Map ID: 1179 Data --- (12 组 / 40 雷) + [1179] = new List + { + // 区域 11: x[463.5~482.5] z[728.5~735.5] + new MineGroup { Mines = { + new Mine { Position = new Vector3(463.5f, -680f, 732f), IsLarge = false }, new Mine { Position = new Vector3(469.5f, -680f, 728.5f), IsLarge = false }, + new Mine { Position = new Vector3(469.5f, -680f, 735.5f), IsLarge = false }, new Mine { Position = new Vector3(476.5f, -680f, 735.5f), IsLarge = false }, + new Mine { Position = new Vector3(482.5f, -680f, 732f), IsLarge = false }, + }}, + // 区域 13: x[365~375] z[758.5~765.5] + new MineGroup { Mines = { + new Mine { Position = new Vector3(365f, -698f, 765.5f), IsLarge = false }, new Mine { Position = new Vector3(375f, -698f, 758.5f), IsLarge = false }, + }}, + // 区域 15: x[386~400] z[772~792] + new MineGroup { Mines = { + new Mine { Position = new Vector3(386f, -700f, 772f), IsLarge = true }, new Mine { Position = new Vector3(386f, -699.94f, 782f), IsLarge = true }, + new Mine { Position = new Vector3(386f, -700f, 792f), IsLarge = true }, new Mine { Position = new Vector3(393f, -700f, 772f), IsLarge = true }, + new Mine { Position = new Vector3(393f, -700f, 782f), IsLarge = true }, new Mine { Position = new Vector3(393f, -700f, 792f), IsLarge = true }, + new Mine { Position = new Vector3(400f, -700f, 772f), IsLarge = true }, new Mine { Position = new Vector3(400f, -699.94f, 782f), IsLarge = true }, + new Mine { Position = new Vector3(400f, -700f, 792f), IsLarge = true }, + }}, + // 区域 17: x[596.5~603.5] z[776~776] + new MineGroup { Mines = { + new Mine { Position = new Vector3(596.5f, -684f, 776f), IsLarge = true }, new Mine { Position = new Vector3(603.5f, -684f, 776f), IsLarge = false }, + }}, + // 区域 18: x[463.5~482.5] z[780.5~787.5] + new MineGroup { Mines = { + new Mine { Position = new Vector3(463.5f, -680f, 784f), IsLarge = true }, new Mine { Position = new Vector3(469.5f, -680f, 780.5f), IsLarge = true }, + new Mine { Position = new Vector3(469.5f, -680f, 787.5f), IsLarge = true }, new Mine { Position = new Vector3(476.5f, -680f, 780.5f), IsLarge = true }, + new Mine { Position = new Vector3(476.5f, -680f, 787.5f), IsLarge = true }, new Mine { Position = new Vector3(482.5f, -680f, 784f), IsLarge = true }, + }}, + // 区域 20: x[365~375] z[798.5~805.5] + new MineGroup { Mines = { + new Mine { Position = new Vector3(365f, -698f, 798.5f), IsLarge = false }, new Mine { Position = new Vector3(365f, -698f, 805.5f), IsLarge = false }, + new Mine { Position = new Vector3(375f, -698f, 805.5f), IsLarge = false }, + }}, + // 区域 22: x[561~561] z[825.5~832.5] + new MineGroup { Mines = { + new Mine { Position = new Vector3(561f, -680f, 825.5f), IsLarge = false }, new Mine { Position = new Vector3(561f, -680f, 832.5f), IsLarge = false }, + }}, + // 区域 23: x[639~639] z[825.5~825.5] + new MineGroup { Mines = { + new Mine { Position = new Vector3(639f, -680f, 825.5f), IsLarge = false }, + }}, + // 区域 24: x[514.5~521.5] z[849~861] + new MineGroup { Mines = { + new Mine { Position = new Vector3(514.5f, -680f, 849f), IsLarge = false }, new Mine { Position = new Vector3(514.5f, -680f, 861f), IsLarge = false }, + new Mine { Position = new Vector3(521.5f, -680f, 849f), IsLarge = false }, new Mine { Position = new Vector3(521.5f, -680f, 861f), IsLarge = false }, + }}, + // 区域 25: x[678.5~685.5] z[849~861] + new MineGroup { Mines = { + new Mine { Position = new Vector3(678.5f, -680f, 849f), IsLarge = false }, new Mine { Position = new Vector3(685.5f, -680f, 861f), IsLarge = false }, + }}, + // 区域 26: x[561.5~568.5] z[922.5~922.5] + new MineGroup { Mines = { + new Mine { Position = new Vector3(561.5f, -700f, 922.5f), IsLarge = false }, new Mine { Position = new Vector3(568.5f, -699.941f, 922.5f), IsLarge = false }, + }}, + // 区域 27: x[638.5~645.5] z[922.5~922.5] + new MineGroup { Mines = { + new Mine { Position = new Vector3(638.5f, -700.001f, 922.5f), IsLarge = false }, new Mine { Position = new Vector3(645.5f, -699.901f, 922.5f), IsLarge = false }, + }}, + }, + // --- Map ID: 1180 Data --- (14 组 / 58 雷) + [1180] = new List + { + // 区域 10: x[763.5~770.5] z[660~672] + new MineGroup { Mines = { + new Mine { Position = new Vector3(763.5f, -690f, 660f), IsLarge = false }, new Mine { Position = new Vector3(763.5f, -690f, 672f), IsLarge = false }, + new Mine { Position = new Vector3(770.5f, -690f, 660f), IsLarge = false }, new Mine { Position = new Vector3(770.5f, -690f, 672f), IsLarge = false }, + }}, + // 区域 12: x[717.5~736.5] z[728.5~735.5] + new MineGroup { Mines = { + new Mine { Position = new Vector3(717.5f, -680f, 732f), IsLarge = false }, new Mine { Position = new Vector3(723.5f, -680f, 728.5f), IsLarge = false }, + new Mine { Position = new Vector3(723.5f, -680f, 735.5f), IsLarge = false }, new Mine { Position = new Vector3(730.5f, -680f, 728.5f), IsLarge = false }, + new Mine { Position = new Vector3(730.5f, -680f, 735.5f), IsLarge = false }, new Mine { Position = new Vector3(736.5f, -680f, 732f), IsLarge = false }, + }}, + // 区域 14: x[825~835] z[758.5~765.5] + new MineGroup { Mines = { + new Mine { Position = new Vector3(825f, -698f, 758.5f), IsLarge = false }, new Mine { Position = new Vector3(825f, -698f, 765.5f), IsLarge = false }, + new Mine { Position = new Vector3(835f, -698f, 758.5f), IsLarge = false }, new Mine { Position = new Vector3(835f, -698f, 765.5f), IsLarge = false }, + }}, + // 区域 16: x[800~814] z[772~792] + new MineGroup { Mines = { + new Mine { Position = new Vector3(800f, -700f, 772f), IsLarge = true }, new Mine { Position = new Vector3(800f, -699.94f, 782f), IsLarge = true }, + new Mine { Position = new Vector3(800f, -700f, 792f), IsLarge = true }, new Mine { Position = new Vector3(807f, -700f, 772f), IsLarge = true }, + new Mine { Position = new Vector3(807f, -700f, 782f), IsLarge = true }, new Mine { Position = new Vector3(807f, -700f, 792f), IsLarge = true }, + new Mine { Position = new Vector3(814f, -700f, 772f), IsLarge = true }, new Mine { Position = new Vector3(814f, -699.94f, 782f), IsLarge = true }, + new Mine { Position = new Vector3(814f, -700f, 792f), IsLarge = true }, + }}, + // 区域 17: x[596.5~603.5] z[776~776] + new MineGroup { Mines = { + new Mine { Position = new Vector3(596.5f, -684f, 776f), IsLarge = false }, new Mine { Position = new Vector3(596.5f, -684f, 776f), IsLarge = true }, + new Mine { Position = new Vector3(603.5f, -684f, 776f), IsLarge = false }, new Mine { Position = new Vector3(603.5f, -684f, 776f), IsLarge = true }, + }}, + // 区域 19: x[717.5~736.5] z[780.5~787.5] + new MineGroup { Mines = { + new Mine { Position = new Vector3(717.5f, -680f, 784f), IsLarge = true }, new Mine { Position = new Vector3(723.5f, -680f, 780.5f), IsLarge = true }, + new Mine { Position = new Vector3(723.5f, -680f, 787.5f), IsLarge = true }, new Mine { Position = new Vector3(730.5f, -680f, 780.5f), IsLarge = true }, + new Mine { Position = new Vector3(730.5f, -680f, 787.5f), IsLarge = true }, new Mine { Position = new Vector3(736.5f, -680f, 784f), IsLarge = true }, + }}, + // 区域 21: x[825~835] z[798.5~805.5] + new MineGroup { Mines = { + new Mine { Position = new Vector3(825f, -698f, 798.5f), IsLarge = false }, new Mine { Position = new Vector3(825f, -698f, 805.5f), IsLarge = false }, + new Mine { Position = new Vector3(835f, -698f, 798.5f), IsLarge = false }, new Mine { Position = new Vector3(835f, -698f, 805.5f), IsLarge = false }, + }}, + // 区域 22: x[561~561] z[825.5~832.5] + new MineGroup { Mines = { + new Mine { Position = new Vector3(561f, -680f, 825.5f), IsLarge = false }, new Mine { Position = new Vector3(561f, -680f, 832.5f), IsLarge = false }, + }}, + // 区域 23: x[639~639] z[825.5~832.5] + new MineGroup { Mines = { + new Mine { Position = new Vector3(639f, -680f, 825.5f), IsLarge = false }, new Mine { Position = new Vector3(639f, -680f, 832.5f), IsLarge = false }, + }}, + // 区域 24: x[514.5~521.5] z[849~861] + new MineGroup { Mines = { + new Mine { Position = new Vector3(514.5f, -680f, 849f), IsLarge = false }, new Mine { Position = new Vector3(514.5f, -680f, 861f), IsLarge = false }, + new Mine { Position = new Vector3(521.5f, -680f, 849f), IsLarge = false }, new Mine { Position = new Vector3(521.5f, -680f, 861f), IsLarge = false }, + }}, + // 区域 25: x[678.5~685.5] z[849~861] + new MineGroup { Mines = { + new Mine { Position = new Vector3(678.5f, -680f, 849f), IsLarge = false }, new Mine { Position = new Vector3(678.5f, -680f, 861f), IsLarge = false }, + new Mine { Position = new Vector3(685.5f, -680f, 849f), IsLarge = false }, new Mine { Position = new Vector3(685.5f, -680f, 861f), IsLarge = false }, + }}, + // 区域 26: x[561.5~568.5] z[922.5~929.5] + new MineGroup { Mines = { + new Mine { Position = new Vector3(561.5f, -700.001f, 929.5f), IsLarge = false }, new Mine { Position = new Vector3(568.5f, -699.941f, 922.5f), IsLarge = false }, + }}, + // 区域 27: x[631.5~645.5] z[922.5~929.5] + new MineGroup { Mines = { + new Mine { Position = new Vector3(631.5f, -699.941f, 922.5f), IsLarge = false }, new Mine { Position = new Vector3(631.5f, -699.941f, 929.5f), IsLarge = false }, + new Mine { Position = new Vector3(638.5f, -700.001f, 922.5f), IsLarge = false }, new Mine { Position = new Vector3(645.5f, -699.901f, 922.5f), IsLarge = false }, + new Mine { Position = new Vector3(645.5f, -699.901f, 929.5f), IsLarge = false }, + }}, + // 区域 28: x[596.5~603.5] z[943~957] + new MineGroup { Mines = { + new Mine { Position = new Vector3(596.5f, -700f, 957f), IsLarge = false }, new Mine { Position = new Vector3(603.5f, -700f, 943f), IsLarge = false }, + }}, + }, + // --- Map ID: 1181 Data --- (10 组 / 31 雷) + [1181] = new List + { + // 区域 17: x[596.5~603.5] z[776~776] + new MineGroup { Mines = { + new Mine { Position = new Vector3(596.5f, -684f, 776f), IsLarge = false }, new Mine { Position = new Vector3(596.5f, -684f, 776f), IsLarge = true }, + new Mine { Position = new Vector3(603.5f, -684f, 776f), IsLarge = false }, new Mine { Position = new Vector3(603.5f, -684f, 776f), IsLarge = true }, + }}, + }, + // --- Map ID: 1182 Data --- (8 组 / 53 雷) + [1182] = new List + { + // 区域 2: x[530.5~537.5] z[81.5~88.5] + new MineGroup { Mines = { + new Mine { Position = new Vector3(530.5f, -700f, 81.5f), IsLarge = false }, new Mine { Position = new Vector3(530.5f, -700f, 88.5f), IsLarge = false }, + new Mine { Position = new Vector3(537.5f, -700f, 81.5f), IsLarge = false }, new Mine { Position = new Vector3(537.5f, -700f, 88.5f), IsLarge = false }, + }}, + // 区域 3: x[582~618] z[107~141] + new MineGroup { Mines = { + new Mine { Position = new Vector3(582f, -700f, 124f), IsLarge = false }, new Mine { Position = new Vector3(585f, -700f, 117f), IsLarge = false }, + new Mine { Position = new Vector3(585f, -700f, 131f), IsLarge = false }, new Mine { Position = new Vector3(589f, -699.95f, 124f), IsLarge = false }, + new Mine { Position = new Vector3(592f, -699.95f, 116f), IsLarge = false }, new Mine { Position = new Vector3(592f, -699.95f, 132f), IsLarge = false }, + new Mine { Position = new Vector3(593f, -700f, 109f), IsLarge = false }, new Mine { Position = new Vector3(593f, -700f, 139f), IsLarge = false }, + new Mine { Position = new Vector3(600f, -699.956f, 107f), IsLarge = false }, new Mine { Position = new Vector3(600f, -699.95f, 113f), IsLarge = false }, + new Mine { Position = new Vector3(600f, -699.95f, 135f), IsLarge = false }, new Mine { Position = new Vector3(600f, -699.956f, 141f), IsLarge = false }, + new Mine { Position = new Vector3(607f, -700f, 109f), IsLarge = false }, new Mine { Position = new Vector3(607f, -700f, 139f), IsLarge = false }, + new Mine { Position = new Vector3(608f, -699.95f, 116f), IsLarge = false }, new Mine { Position = new Vector3(608f, -699.95f, 132f), IsLarge = false }, + new Mine { Position = new Vector3(611f, -699.95f, 124f), IsLarge = false }, new Mine { Position = new Vector3(615f, -700f, 117f), IsLarge = false }, + new Mine { Position = new Vector3(615f, -700f, 131f), IsLarge = false }, new Mine { Position = new Vector3(618f, -700f, 124f), IsLarge = false }, + }}, + // 区域 4: x[634.5~642.5] z[117~131] + new MineGroup { Mines = { + new Mine { Position = new Vector3(634.5f, -700f, 117f), IsLarge = true }, new Mine { Position = new Vector3(634.5f, -700f, 124f), IsLarge = true }, + new Mine { Position = new Vector3(634.5f, -700f, 131f), IsLarge = true }, new Mine { Position = new Vector3(642.5f, -700f, 117f), IsLarge = true }, + new Mine { Position = new Vector3(642.5f, -700f, 124f), IsLarge = true }, new Mine { Position = new Vector3(642.5f, -700f, 131f), IsLarge = true }, + }}, + // 区域 5: x[669.5~677.5] z[117~131] + new MineGroup { Mines = { + new Mine { Position = new Vector3(669.5f, -700f, 117f), IsLarge = true }, new Mine { Position = new Vector3(669.5f, -700f, 124f), IsLarge = true }, + new Mine { Position = new Vector3(669.5f, -700f, 131f), IsLarge = true }, new Mine { Position = new Vector3(677.5f, -700f, 117f), IsLarge = true }, + new Mine { Position = new Vector3(677.5f, -700f, 124f), IsLarge = true }, new Mine { Position = new Vector3(677.5f, -700f, 131f), IsLarge = true }, + }}, + // 区域 6: x[528.343~539.657] z[118.343~129.657] + new MineGroup { Mines = { + new Mine { Position = new Vector3(528.343f, -700f, 118.343f), IsLarge = true }, new Mine { Position = new Vector3(528.343f, -700f, 129.657f), IsLarge = true }, + new Mine { Position = new Vector3(534f, -700f, 124f), IsLarge = true }, new Mine { Position = new Vector3(539.657f, -700f, 118.343f), IsLarge = true }, + new Mine { Position = new Vector3(539.657f, -700f, 129.657f), IsLarge = true }, + }}, + // 区域 7: x[491.5~498.5] z[120.5~127.5] + new MineGroup { Mines = { + new Mine { Position = new Vector3(491.5f, -700f, 120.5f), IsLarge = false }, new Mine { Position = new Vector3(491.5f, -700f, 127.5f), IsLarge = false }, + new Mine { Position = new Vector3(498.5f, -700f, 120.5f), IsLarge = false }, new Mine { Position = new Vector3(498.5f, -700f, 127.5f), IsLarge = false }, + }}, + // 区域 8: x[560~568] z[120.5~127.5] + new MineGroup { Mines = { + new Mine { Position = new Vector3(560f, -700f, 120.5f), IsLarge = false }, new Mine { Position = new Vector3(560f, -700f, 127.5f), IsLarge = false }, + new Mine { Position = new Vector3(568f, -700f, 120.5f), IsLarge = false }, new Mine { Position = new Vector3(568f, -700f, 127.5f), IsLarge = false }, + }}, + // 区域 9: x[530.5~537.5] z[159.5~166.5] + new MineGroup { Mines = { + new Mine { Position = new Vector3(530.5f, -700f, 159.5f), IsLarge = false }, new Mine { Position = new Vector3(530.5f, -700f, 166.5f), IsLarge = false }, + new Mine { Position = new Vector3(537.5f, -700f, 159.5f), IsLarge = false }, new Mine { Position = new Vector3(537.5f, -700f, 166.5f), IsLarge = false }, + }}, + }, + // // --- Map ID: 1183 Data --- (8 组 / 25 雷) + // [1183] = new List + // { + // // 区域 2: x[537.5~537.5] z[81.5~88.5] + // new MineGroup { Mines = { + // new Mine { Position = new Vector3(537.5f, -700f, 81.5f), IsLarge = false }, new Mine { Position = new Vector3(537.5f, -700f, 88.5f), IsLarge = false }, + // }}, + // // 区域 3: x[582~618] z[109~139] + // new MineGroup { Mines = { + // new Mine { Position = new Vector3(582f, -700f, 124f), IsLarge = false }, new Mine { Position = new Vector3(585f, -700f, 131f), IsLarge = false }, + // new Mine { Position = new Vector3(589f, -699.95f, 124f), IsLarge = false }, new Mine { Position = new Vector3(592f, -699.95f, 132f), IsLarge = false }, + // new Mine { Position = new Vector3(593f, -700f, 109f), IsLarge = false }, new Mine { Position = new Vector3(593f, -700f, 139f), IsLarge = false }, + // new Mine { Position = new Vector3(607f, -700f, 109f), IsLarge = false }, new Mine { Position = new Vector3(607f, -700f, 139f), IsLarge = false }, + // new Mine { Position = new Vector3(608f, -699.95f, 116f), IsLarge = false }, new Mine { Position = new Vector3(608f, -699.95f, 132f), IsLarge = false }, + // new Mine { Position = new Vector3(618f, -700f, 124f), IsLarge = false }, + // }}, + // // 区域 4: x[634.5~642.5] z[117~131] + // new MineGroup { Mines = { + // new Mine { Position = new Vector3(634.5f, -700f, 131f), IsLarge = true }, new Mine { Position = new Vector3(642.5f, -700f, 117f), IsLarge = true }, + // new Mine { Position = new Vector3(642.5f, -700f, 124f), IsLarge = true }, + // }}, + // // 区域 5: x[669.5~669.5] z[117~117] + // new MineGroup { Mines = { + // new Mine { Position = new Vector3(669.5f, -700f, 117f), IsLarge = true }, + // }}, + // // 区域 6: x[528.343~539.657] z[118.343~129.657] + // new MineGroup { Mines = { + // new Mine { Position = new Vector3(528.343f, -700f, 129.657f), IsLarge = true }, new Mine { Position = new Vector3(534f, -700f, 124f), IsLarge = true }, + // new Mine { Position = new Vector3(539.657f, -700f, 118.343f), IsLarge = true }, + // }}, + // // 区域 7: x[498.5~498.5] z[127.5~127.5] + // new MineGroup { Mines = { + // new Mine { Position = new Vector3(498.5f, -700f, 127.5f), IsLarge = false }, + // }}, + // // 区域 8: x[568~568] z[120.5~127.5] + // new MineGroup { Mines = { + // new Mine { Position = new Vector3(568f, -700f, 120.5f), IsLarge = false }, new Mine { Position = new Vector3(568f, -700f, 127.5f), IsLarge = false }, + // }}, + // // 区域 9: x[530.5~537.5] z[159.5~166.5] + // new MineGroup { Mines = { + // new Mine { Position = new Vector3(530.5f, -700f, 159.5f), IsLarge = false }, new Mine { Position = new Vector3(537.5f, -700f, 166.5f), IsLarge = false }, + // }}, + // }, + // --- Map ID: 1184 Data --- (5 组 / 8 雷) + // [1184] = new List + // { + // // 区域 2: x[530.5~530.5] z[88.5~88.5] + // new MineGroup { Mines = { + // new Mine { Position = new Vector3(530.5f, -700f, 88.5f), IsLarge = false }, + // }}, + // // 区域 3: x[618~618] z[124~124] + // new MineGroup { Mines = { + // new Mine { Position = new Vector3(618f, -700f, 124f), IsLarge = false }, + // }}, + // // 区域 4: x[634.5~642.5] z[117~124] + // new MineGroup { Mines = { + // new Mine { Position = new Vector3(634.5f, -700f, 117f), IsLarge = true }, new Mine { Position = new Vector3(642.5f, -700f, 124f), IsLarge = true }, + // }}, + // // 区域 7: x[491.5~498.5] z[120.5~120.5] + // new MineGroup { Mines = { + // new Mine { Position = new Vector3(491.5f, -700f, 120.5f), IsLarge = false }, new Mine { Position = new Vector3(498.5f, -700f, 120.5f), IsLarge = false }, + // }}, + // // 区域 9: x[537.5~537.5] z[159.5~166.5] + // new MineGroup { Mines = { + // new Mine { Position = new Vector3(537.5f, -700f, 159.5f), IsLarge = false }, new Mine { Position = new Vector3(537.5f, -700f, 166.5f), IsLarge = false }, + // }}, + // }, + // --- Map ID: 1189 Data --- (2 组 / 15 雷) + [1189] = new List + { + // 区域 0: x[-9~10] z[-433~-421] + new MineGroup { Mines = { + new Mine { Position = new Vector3(-9f, -707.95f, -430f), IsLarge = false }, new Mine { Position = new Vector3(-4f, -707.95f, -430f), IsLarge = false }, + new Mine { Position = new Vector3(0f, -707.95f, -433f), IsLarge = false }, new Mine { Position = new Vector3(0f, -707.95f, -427f), IsLarge = false }, + new Mine { Position = new Vector3(0f, -707.95f, -421f), IsLarge = false }, new Mine { Position = new Vector3(4f, -707.95f, -430f), IsLarge = false }, + new Mine { Position = new Vector3(10f, -708f, -430f), IsLarge = false }, + }}, + // 区域 1: x[27~46] z[-403~-385] + new MineGroup { Mines = { + new Mine { Position = new Vector3(27f, -715.95f, -394f), IsLarge = false }, new Mine { Position = new Vector3(32f, -715.95f, -394f), IsLarge = false }, + new Mine { Position = new Vector3(36f, -715.95f, -403f), IsLarge = false }, new Mine { Position = new Vector3(36f, -715.95f, -397f), IsLarge = false }, + new Mine { Position = new Vector3(36f, -715.95f, -391f), IsLarge = false }, new Mine { Position = new Vector3(36f, -715.95f, -385f), IsLarge = false }, + new Mine { Position = new Vector3(40f, -715.95f, -394f), IsLarge = false }, new Mine { Position = new Vector3(46f, -716f, -394f), IsLarge = false }, + }}, + }, + }; +} + +#endregion