Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
c5edba9
add T03Everkeep skeleton with Soul Overflow raidwide
Apr 17, 2026
fe94a7f
T03Everkeep: add Patricidal Pique tankbuster
Apr 17, 2026
9d9ae34
T03Everkeep: add Calamity's Edge raidwide
Apr 17, 2026
337139d
T03Everkeep: add Shadow of Tural Burst add-circle AoE
Apr 17, 2026
743f36a
T03Everkeep: add Vorpal Trail charge puddles
Apr 17, 2026
af87624
T03Everkeep: add Double-edged Swords half-arena cleaves
Apr 17, 2026
3034bd0
T03Everkeep: split into per-mechanic files and add P1/P2 state machine
Apr 17, 2026
5117b92
T03Everkeep: add P2 Dawn of an Age raidwide
Apr 17, 2026
a6f6dbd
T03Everkeep: add P2 Actualize raidwide
Apr 17, 2026
31d7e47
T03Everkeep: add P2 Chasm of Vollok 5x5 grid cells
Apr 17, 2026
fa9a031
T03Everkeep: catalog P2 Gateway AID (non-damaging chain trigger)
Apr 17, 2026
0e2f36b
T03Everkeep: catalog P2 Blade Warp / Forged Track AIDs (component def…
Apr 17, 2026
9f2718f
T03Everkeep: add P2 Duty's Edge line stack (ported from Ex2 with Norm…
Apr 17, 2026
0e5a929
T03Everkeep: add P2 Half Full half-arena cleave
Apr 17, 2026
fb37df2
T03Everkeep: add P2 Bitter Reaping paired tankbusters
Apr 17, 2026
baf35ef
T03Everkeep: add P2 Half Circuit rect+donut+circle combo
Apr 17, 2026
76e6a1d
T03Everkeep: add P2 Smiting Circuit donut+circle (distinct from Ex2 v…
Apr 17, 2026
8948b37
T03Everkeep: bump maturity to Contributed so module loads by default
Apr 17, 2026
4832345
T03Everkeep: fix arena bounds to rotated 40x40 square matching Ex2 pl…
Apr 17, 2026
74b7e2d
T03Everkeep: Vorpal Trail draws rect per fang sprint segment
Apr 23, 2026
c562921
T03Everkeep: shrink/restore arena bounds on Dawn of an Age / Actualize
Apr 23, 2026
a57e744
T03Everkeep: survive the mid-fight P2 boss actor swap
Apr 23, 2026
67311b5
T03Everkeep: add P2 Forged Track sword charges
Apr 24, 2026
79a217e
T03Everkeep: add P2 Fire III spread on every party member
Apr 24, 2026
93af8ea
T03Everkeep: clear Duty's Edge line stack after 4th hit
Apr 24, 2026
f60f5ea
T03Everkeep: shift Chasm of Vollok rects forward so corners reach are…
Apr 24, 2026
90632d4
T03Everkeep: label P2 phase as 'Enrage' in radar countdown
Apr 24, 2026
b8a69af
T03Everkeep: render Chasm of Vollok during 6.7s preview, not only the…
Apr 24, 2026
9b85cc8
T03Everkeep: derive Forged Track lane offset sign from Blade Warp dir…
Apr 24, 2026
f8306c6
T03Everkeep: widen Vorpal Trail pinwheel rects to 2.75 halfwidth to c…
Apr 24, 2026
2a38c7e
T03Everkeep: derive Forged Track lane offset from outer fang's lane p…
Apr 25, 2026
104a708
T03Everkeep: render Chasm of Vollok predicted damage cells during 6.7…
Apr 25, 2026
94da66a
T03Everkeep: keep Forged Track predicted rects painted through previe…
Apr 26, 2026
d5d06ab
T03Everkeep: bump Fire III spread radius to 7m for 2m AI buffer betwe…
Apr 26, 2026
00ff7e5
T03Everkeep: forbid Vorpal Trail arena center to push AI to perimeter…
Apr 26, 2026
478d3f9
T03Everkeep: forbid post-shrink out-of-bounds during Dawn of an Age cast
Apr 26, 2026
232e103
T03Everkeep: rename phase states to 'Enrage' to match other modules' …
Apr 26, 2026
7e2cae1
T03Everkeep: prune outdated and redundant comments
Apr 26, 2026
aba4ef7
T03Everkeep: attract AI to boss during Fire III spread to limit playe…
Apr 27, 2026
dac9bad
T03Everkeep: layered Vorpal Trail AI hints — predicted next sprint, c…
Apr 27, 2026
070d30c
T03Everkeep: paint collapsing diamond wings during Dawn of an Age cast
Apr 27, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions BossMod/Modules/Dawntrail/Trial/T03Everkeep/Actualize.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
namespace BossMod.Dawntrail.Trial.T03Everkeep;

class Actualize(BossModule module) : Components.RaidwideCast(module, AID.Actualize)
{
public override void OnEventCast(Actor caster, ActorCastEvent spell)
{
base.OnEventCast(caster, spell);
if ((AID)spell.Action.ID == AID.Actualize)
Module.Arena.Bounds = T03Everkeep.NormalBounds;
}
}
3 changes: 3 additions & 0 deletions BossMod/Modules/Dawntrail/Trial/T03Everkeep/BitterReaping.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
namespace BossMod.Dawntrail.Trial.T03Everkeep;

class BitterReaping(BossModule module) : Components.SingleTargetCast(module, AID.BitterReapingAOE, "Tank busters");
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For components that are a single line, feel free to just put them in the root module file (T03Everkeep), creating separate files for simple components is more trouble than it's worth I feel (I will merge this as-is though)

3 changes: 3 additions & 0 deletions BossMod/Modules/Dawntrail/Trial/T03Everkeep/Burst.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
namespace BossMod.Dawntrail.Trial.T03Everkeep;

class Burst(BossModule module) : Components.StandardAOEs(module, AID.Burst, new AOEShapeCircle(8));
3 changes: 3 additions & 0 deletions BossMod/Modules/Dawntrail/Trial/T03Everkeep/CalamitysEdge.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
namespace BossMod.Dawntrail.Trial.T03Everkeep;

class CalamitysEdge(BossModule module) : Components.RaidwideCast(module, AID.CalamitysEdge);
57 changes: 57 additions & 0 deletions BossMod/Modules/Dawntrail/Trial/T03Everkeep/ChasmOfVollok.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
namespace BossMod.Dawntrail.Trial.T03Everkeep;

// Two flavors of Chasm of Vollok exist:
// - Single-cast (Mode A): 37720 cells spawn inside the arena and ARE the damage; 37722 doesn't
// fire. We just render the 37720 cell with its 6.7s cast as the warning.
// - Telegraphed (Mode B): 37720 cells spawn on the OUTER ring (visual only, no damage), then
// 37722 fires 6s later on different cells inside the arena. The user only gets ~0.7s warning
// from 37722 alone, which the AI cannot react to in time.
//
// In Mode B the outer 37720 cell maps deterministically to its inner 37722 damage cell: shift
// ~21.21m (= 30/√2, three 5m cells along the rotated grid) toward the arena center along both
// world X and Z axes. We render the predicted damage cell with 6.7s warning so the AI has time
// to reposition. When 37722 actually fires, its rect also renders (final-moment reminder,
// redundant with the predicted one).
//
// Origin-at-back-corner shape (5f forward + 0 back): cast targets sit at the back corner of each
// 5×5 cell, not the center, so we extend the rect 5 forward from its origin.
class ChasmOfVollok(BossModule module) : Components.GenericAOEs(module)
{
private readonly Dictionary<ulong, AOEInstance> _previewAoes = [];
private readonly Dictionary<ulong, AOEInstance> _damageAoes = [];

private static readonly AOEShapeRect _shape = new(5f, 2.5f);
private const float ArenaRadius = 15f; // cells beyond this are outer-ring previews (Mode B)
private const float TranslateMagnitude = 21.21f; // 15√2; shift toward center per world axis

public override IEnumerable<AOEInstance> ActiveAOEs(int slot, Actor actor) =>
_previewAoes.Values.Concat(_damageAoes.Values);

public override void OnCastStarted(Actor caster, ActorCastInfo spell)
{
switch ((AID)spell.Action.ID)
{
case AID.ChasmOfVollokPreview:
var pos = spell.LocXZ;
var dx = pos.X - Module.Center.X;
var dz = pos.Z - Module.Center.Z;
if (dx * dx + dz * dz > ArenaRadius * ArenaRadius)
pos = new WPos(pos.X - TranslateMagnitude * MathF.Sign(dx),
pos.Z - TranslateMagnitude * MathF.Sign(dz));
_previewAoes[caster.InstanceID] = new AOEInstance(_shape, pos, spell.Rotation, Module.CastFinishAt(spell));
break;
case AID.ChasmOfVollokAOE:
_damageAoes[caster.InstanceID] = new AOEInstance(_shape, spell.LocXZ, spell.Rotation, Module.CastFinishAt(spell));
break;
}
}

public override void OnCastFinished(Actor caster, ActorCastInfo spell)
{
switch ((AID)spell.Action.ID)
{
case AID.ChasmOfVollokPreview: _previewAoes.Remove(caster.InstanceID); break;
case AID.ChasmOfVollokAOE: _damageAoes.Remove(caster.InstanceID); break;
}
}
}
50 changes: 50 additions & 0 deletions BossMod/Modules/Dawntrail/Trial/T03Everkeep/DawnOfAnAge.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
namespace BossMod.Dawntrail.Trial.T03Everkeep;

// Dawn of an Age is the raidwide that also shrinks the arena: when the cast resolves the platform
// collapses to a 20x20 center square for the Chasm of Vollok + Forged Track sequence. Actualize
// restores the full 40x40 arena at the end of that sequence.
class DawnOfAnAge(BossModule module) : Components.RaidwideCast(module, AID.DawnOfAnAge)
{
public override void OnEventCast(Actor caster, ActorCastEvent spell)
{
base.OnEventCast(caster, spell);
if ((AID)spell.Action.ID == AID.DawnOfAnAge)
Module.Arena.Bounds = T03Everkeep.SmallBounds;
}

public override void AddAIHints(int slot, Actor actor, PartyRolesConfig.Assignment assignment, AIHints hints)
{
base.AddAIHints(slot, actor, assignment, hints);
// While the cast is up, forbid everything outside the post-shrink diamond so ranged jobs
// pull in toward center before the platform collapses around them.
var center = Module.Center;
var bounds = T03Everkeep.SmallBounds;
foreach (var c in Casters)
hints.AddForbiddenZone(p => !bounds.Contains(p - center), Module.CastFinishAt(c.CastInfo));
}

// Cardinal extents of the rotated-45° diamonds: corners sit at half-extent × √2 from center.
private static readonly float OuterCardinal = 20f * MathF.Sqrt(2);
private static readonly float InnerCardinal = 10f * MathF.Sqrt(2);

public override void DrawArenaBackground(int pcSlot, Actor pc)
{
if (Casters.Count == 0)
return;
// Paint the four diamond wings that will collapse — the donut between SmallBounds and
// NormalBounds, drawn as four quadrilaterals between adjacent diamond cardinals.
var c = Module.Center;
var nO = new WPos(c.X, c.Z - OuterCardinal);
var eO = new WPos(c.X + OuterCardinal, c.Z);
var sO = new WPos(c.X, c.Z + OuterCardinal);
var wO = new WPos(c.X - OuterCardinal, c.Z);
var nI = new WPos(c.X, c.Z - InnerCardinal);
var eI = new WPos(c.X + InnerCardinal, c.Z);
var sI = new WPos(c.X, c.Z + InnerCardinal);
var wI = new WPos(c.X - InnerCardinal, c.Z);
Arena.ZonePoly("doaa-ne", new[] { nO, eO, eI, nI }, ArenaColor.AOE);
Arena.ZonePoly("doaa-se", new[] { eO, sO, sI, eI }, ArenaColor.AOE);
Arena.ZonePoly("doaa-sw", new[] { sO, wO, wI, sI }, ArenaColor.AOE);
Arena.ZonePoly("doaa-nw", new[] { wO, nO, nI, wI }, ArenaColor.AOE);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
namespace BossMod.Dawntrail.Trial.T03Everkeep;

class DoubleEdgedSwords(BossModule module) : Components.StandardAOEs(module, AID.DoubleEdgedSwordsAOE, new AOEShapeRect(60, 60));
26 changes: 26 additions & 0 deletions BossMod/Modules/Dawntrail/Trial/T03Everkeep/DutysEdge.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
namespace BossMod.Dawntrail.Trial.T03Everkeep;

// Duty's Edge fires 4 line-stack hits (37750) after a 4.6s visual (37748), ~2.1s apart. The target
// icon (35567) is picked once per instance. Clear Source after the 4th hit so the rendering goes
// away, and reset NumCasts on each new target so the mechanic works for the second instance.
class DutysEdge(BossModule module) : Components.GenericWildCharge(module, 4, AID.DutysEdgeAOE, 100)
{
private const int TotalHits = 4;

public override void OnEventCast(Actor caster, ActorCastEvent spell)
{
switch ((AID)spell.Action.ID)
{
case AID.DutysEdgeTarget:
Source = caster;
NumCasts = 0;
foreach (var (i, p) in Raid.WithSlot(true))
PlayerRoles[i] = p.InstanceID == spell.MainTargetID ? PlayerRole.Target : PlayerRole.Share;
break;
case AID.DutysEdgeAOE:
if (++NumCasts >= TotalHits)
Source = null;
break;
}
}
}
15 changes: 15 additions & 0 deletions BossMod/Modules/Dawntrail/Trial/T03Everkeep/FireIII.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
namespace BossMod.Dawntrail.Trial.T03Everkeep;

// Spread on icon 376; 8 helpers instant-cast Fire III (37752) ~5s later. Forbidden radius is 7m
// (2m wider than the 5m hit) so the AI doesn't clip a neighbor while routing.
class FireIII(BossModule module) : Components.SpreadFromIcon(module, 376, AID.FireIII, 7f, 5.1f)
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

GenericStackSpread already has an extra spread threshold for AI (in this case 1 unit) so the extra size here should be unnecessary

{
public override void AddAIHints(int slot, Actor actor, PartyRolesConfig.Assignment assignment, AIHints hints)
{
base.AddAIHints(slot, actor, assignment, hints);
// Duty Support NPCs always fan to the perimeter, so running onto the boss leaves the player
// alone in the center — only their own Fire III circle hits, no overlap from a neighbor's.
if (Spreads.Any(s => s.Target == actor))
hints.GoalZones.Add(hints.GoalSingleTarget(Module.PrimaryActor, 1f, 10f));
}
}
55 changes: 55 additions & 0 deletions BossMod/Modules/Dawntrail/Trial/T03Everkeep/ForgedTrack.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
namespace BossMod.Dawntrail.Trial.T03Everkeep;

// Forged Track: 4 intercardinal platforms spawn sword Fangs that telegraph narrow lane charges
// across the small arena. Each preview (37729, 11.6s, 0-hit) has an outer-platform caster; after
// the preview resolves, a separate inner Fang instant-casts 37730 for the actual damage along a
// parallel lane.
//
// The lane relationship between preview and damage:
// - NW-SE diagonal (rot +45° / -135°): damage lane coincides with preview lane (no offset).
// - NE-SW diagonal (rot -45° / +135°): damage lane is 5m perpendicular from preview lane. Offset
// direction is determined per-fang by which lane the outer fang stands on.
//
// The four valid NE-SW lanes within the arena lie at perpendicular offsets of -7.5, -2.5, +2.5,
// +7.5 from arena center (each 5m wide, the arena is 20m across the diamond). They form two
// interleaved pairs: outer fangs spawn on one pair, inner damage fangs on the other. So the
// offset sign alternates between adjacent lanes — `floor(perp / 5)` parity gives the right sign.
class ForgedTrack(BossModule module) : Components.GenericAOEs(module)
{
private readonly Dictionary<ulong, AOEInstance> _aoes = [];

public override IEnumerable<AOEInstance> ActiveAOEs(int slot, Actor actor)
{
// Preview entries linger past activation until the actual 37730 fires — auto-cull stale ones.
var cutoff = WorldState.CurrentTime.AddSeconds(-0.5);
foreach (var id in _aoes.Where(kv => kv.Value.Activation < cutoff).Select(kv => kv.Key).ToList())
_aoes.Remove(id);
return _aoes.Values;
}

private static readonly AOEShapeRect _shape = new(20f, 2.5f);
private const float ForwardOffset = 30f;
private const float PerpOffsetMagnitude = 5f;
// Activation = predicted damage moment so the rect stays forbidden through the preview→damage gap.
private const float DamageDelayAfterPreview = 1.4f;

public override void OnCastStarted(Actor caster, ActorCastInfo spell)
{
if ((AID)spell.Action.ID != AID.ForgedTrackPreview)
return;

var dir = spell.Rotation.ToDirection();
// NE-SW diagonal: dir.X and dir.Z have opposite signs; NW-SE: same signs (no offset).
var perpOffset = default(WDir);
if (dir.X * dir.Z < 0)
{
var orthoL = dir.OrthoL();
var perp = (caster.Position - Module.Center).Dot(orthoL);
var laneIndex = (int)MathF.Floor(perp / PerpOffsetMagnitude);
var perpSign = laneIndex % 2 == 0 ? 1f : -1f;
perpOffset = orthoL * (PerpOffsetMagnitude * perpSign);
}
var origin = caster.Position + dir * ForwardOffset + perpOffset;
_aoes[caster.InstanceID] = new AOEInstance(_shape, origin, spell.Rotation, Module.CastFinishAt(spell, DamageDelayAfterPreview));
}
}
5 changes: 5 additions & 0 deletions BossMod/Modules/Dawntrail/Trial/T03Everkeep/HalfCircuit.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
namespace BossMod.Dawntrail.Trial.T03Everkeep;

class HalfCircuitRect(BossModule module) : Components.StandardAOEs(module, AID.HalfCircuitRect, new AOEShapeRect(60, 60));
class HalfCircuitDonut(BossModule module) : Components.StandardAOEs(module, AID.HalfCircuitDonut, new AOEShapeDonut(10, 30));
class HalfCircuitCircle(BossModule module) : Components.StandardAOEs(module, AID.HalfCircuitCircle, new AOEShapeCircle(10));
3 changes: 3 additions & 0 deletions BossMod/Modules/Dawntrail/Trial/T03Everkeep/HalfFull.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
namespace BossMod.Dawntrail.Trial.T03Everkeep;

class HalfFull(BossModule module) : Components.StandardAOEs(module, AID.HalfFullAOE, new AOEShapeRect(60, 60));
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
namespace BossMod.Dawntrail.Trial.T03Everkeep;

class PatricidalPique(BossModule module) : Components.SingleTargetCast(module, AID.PatricidalPique);
4 changes: 4 additions & 0 deletions BossMod/Modules/Dawntrail/Trial/T03Everkeep/SmitingCircuit.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
namespace BossMod.Dawntrail.Trial.T03Everkeep;

class SmitingCircuitDonut(BossModule module) : Components.StandardAOEs(module, AID.SmitingCircuitDonutAOE, new AOEShapeDonut(10, 30));
class SmitingCircuitCircle(BossModule module) : Components.StandardAOEs(module, AID.SmitingCircuitCircleAOE, new AOEShapeCircle(10));
4 changes: 4 additions & 0 deletions BossMod/Modules/Dawntrail/Trial/T03Everkeep/SoulOverflow.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
namespace BossMod.Dawntrail.Trial.T03Everkeep;

class SoulOverflow(BossModule module) : Components.RaidwideCast(module, AID.SoulOverflow);
class SoulOverflowEnrage(BossModule module) : Components.RaidwideCast(module, AID.SoulOverflowEnrage);
21 changes: 21 additions & 0 deletions BossMod/Modules/Dawntrail/Trial/T03Everkeep/T03Everkeep.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
namespace BossMod.Dawntrail.Trial.T03Everkeep;

[ModuleInfo(BossModuleInfo.Maturity.Contributed, Contributors = "Gabriel Deleon", PrimaryActorOID = (uint)OID.Boss, GroupType = BossModuleInfo.GroupType.CFC, GroupID = 995, NameID = 12881)]
public class T03Everkeep(WorldState ws, Actor primary) : BossModule(ws, primary, new(100, 100), NormalBounds)
{
public static readonly ArenaBoundsRect NormalBounds = new(20, 20, 45.Degrees());
public static readonly ArenaBoundsRect SmallBounds = new(10, 10, 45.Degrees(), 20);

public Actor? BossP1() => PrimaryActor;

// The P2 boss actor is re-spawned mid-fight (the old instance is destroyed and a new one with
// the same OID is spawned ~0.4s later during the big arena transition), so we query dynamically
// and skip destroyed instances instead of caching the first one we see.
public Actor? BossP2() => Enemies(OID.BossP2).FirstOrDefault(a => !a.IsDestroyed);

protected override void DrawEnemies(int pcSlot, Actor pc)
{
Arena.Actor(PrimaryActor, ArenaColor.Enemy);
Arena.Actor(BossP2(), ArenaColor.Enemy);
}
}
Loading