Skip to content

Commit

Permalink
Merge pull request #353 from sourpuh/lyon6
Browse files Browse the repository at this point in the history
Add WIP Lyon duel 6 module
  • Loading branch information
awgil authored May 5, 2024
2 parents ae0b601 + c06c49f commit 23a140f
Show file tree
Hide file tree
Showing 5 changed files with 354 additions and 0 deletions.
42 changes: 42 additions & 0 deletions BossMod/Modules/Shadowbringers/Foray/Duel/Duel6Lyon/Duel6Lyon.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
namespace BossMod.Shadowbringers.Foray.Duel.Duel6Lyon;

class Duel6LyonStates : StateMachineBuilder
{
public Duel6LyonStates(BossModule module) : base(module)
{
TrivialPhase()
.ActivateOnEnter<OnFire>()
.ActivateOnEnter<WildfiresFury>()
.ActivateOnEnter<HeavenAndEarth>()
.ActivateOnEnter<HeartOfNatureConcentric>()
.ActivateOnEnter<TasteOfBloodAndDuelOrDie>()
.ActivateOnEnter<FlamesMeet>()
.ActivateOnEnter<WindsPeak>()
.ActivateOnEnter<WindsPeakKB>()
.ActivateOnEnter<SplittingRage>()
.ActivateOnEnter<NaturesBlood>()
.ActivateOnEnter<MoveMountains>()
.ActivateOnEnter<WildfireCrucible>();
}
}

[ModuleInfo(BossModuleInfo.Maturity.WIP, Contributors = "SourP", GroupType = BossModuleInfo.GroupType.BozjaDuel, GroupID = 778, NameID = 31)]
public class Duel6Lyon(WorldState ws, Actor primary) : BossModule(ws, primary, new(50f, -410f), new ArenaBoundsCircle(20))
{
protected override void DrawEnemies(int pcSlot, Actor pc)
{
var tasteOfBlood = FindComponent<TasteOfBloodAndDuelOrDie>();
if (tasteOfBlood?.Casters.Count > 0)
{
foreach (var caster in tasteOfBlood.Casters)
{
bool isDueler = tasteOfBlood.Duelers.Contains(caster);
Arena.Actor(caster, isDueler ? ArenaColor.Danger : ArenaColor.Enemy, true);
}
}
else
{
base.DrawEnemies(pcSlot, pc);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
namespace BossMod.Shadowbringers.Foray.Duel.Duel6Lyon;

public enum OID : uint
{
Boss = 0x31C1,
Helper = 0x233C,
VermillionFlame = 0x2E8F,
}

public enum AID : uint
{
AutoAttack = 6497, // Boss->player, no cast, single-target
WildfiresFury = 0x5D39, // Damage
HarnessFire = 0x5D38, // Boss->self, 3,0s cast, single-target
HeartOfNature = 0x5D24, // Boss->self, 3,0s cast, range 80 circle
CagedHeartOfNature = 0x5D1D, // Boss->self, 3,0s cast, range 10 circle
NaturesPulse1 = 0x5D25, // Helper->self, 4,0s cast, range 10 circle
NaturesPulse2 = 0x5D26, // Helper->self, 5,5s cast, range 10-20 donut
NaturesPulse3 = 0x5D27, // Helper->self, 7,0s cast, range 20-30 donut
TasteOfBlood = 0x5D23, // Boss->self, 4,0s cast, range 40 180-degree cone
SoulAflame = 0x5D2C,
FlamesMeet1 = 0x5D2D, // VermillionFlame->self, 6.5s cast; makes the orb light up
FlamesMeet2 = 0x5D2E, // VermillionFlame->self, 11s cast; actual AOE
HeavenAndEarthCW = 0x5D2F,
HeavenAndEarthCCW = 0x5FEA,
HeavenAndEarthRotate = 0x5D30, // Unused by module
HeavenAndEarthStart = 0x5D31,
HeavenAndEarthMove = 0x5D32,
MoveMountains1 = 0x5D33, // Boss->self, 5s cast, first attack
MoveMountains2 = 0x5D34, // Boss->self, no cast, second attack
MoveMountains3 = 0x5D35, // Helper->self, 5s cast, first line
MoveMountains4 = 0x5D36, // Helper->self, no cast, second line
WindsPeak1 = 0x5D2A, // Boss->self, 3,0s cast, range 5 circle
WindsPeak2 = 0x5D2B, // Helper->self, 4,0s cast, range 50 circle
NaturesBlood1 = 0x5D28, // Helper->self, 7,5s cast, range 4 circle
NaturesBlood2 = 0x5D29, // Helper->self, no cast, range 4 circle
SplittingRage = 0x5D37, // Boss->self, 3,0s cast, range 50 circle
DuelOrDie = 0x5D1C,
WildfireCrucible = 0x5D3B, //enrage, 25s cast time
}

public enum SID : uint
{
OnFire = 0x9F3, // Boss->Boss
TemporaryMisdirection = 1422, // Boss->player, extra=0x2D0
DuelOrDie = 0x9F1, // Boss/Helper->player
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
namespace BossMod.Shadowbringers.Foray.Duel.Duel6Lyon;

class OnFire(BossModule module) : BossComponent(module)
{
private bool _hasBuff;
private bool _isCasting;

public override void AddHints(int slot, Actor actor, TextHints hints)
{
if (_isCasting)
hints.Add("Applies On Fire to Lyon. Use Dispell to remove it");
if (_hasBuff)
hints.Add("Lyon has 'On Fire'. Use Dispell to remove it!");
}

public override void OnStatusGain(Actor actor, ActorStatus status)
{
if (actor == Module.PrimaryActor && (SID)status.ID == SID.OnFire)
_hasBuff = true;
}

public override void OnCastStarted(Actor caster, ActorCastInfo spell)
{
if ((AID)spell.Action.ID == AID.HarnessFire)
_isCasting = true;
}

public override void OnCastFinished(Actor caster, ActorCastInfo spell)
{
if ((AID)spell.Action.ID == AID.HarnessFire)
_isCasting = false;
}

public override void OnStatusLose(Actor actor, ActorStatus status)
{
if (actor == Module.PrimaryActor && (SID)status.ID == SID.OnFire)
_hasBuff = false;
}
}

class WildfiresFury(BossModule module) : Components.RaidwideCast(module, ActionID.MakeSpell(AID.WildfiresFury));

class HeavenAndEarth(BossModule module) : Components.GenericRotatingAOE(module)
{
private Angle _increment;

private static readonly AOEShapeCone _shape = new(20, 15.Degrees());

private int _index;

private void UpdateIncrement(Angle increment)
{
_increment = increment;
for (int i = 0; i < Sequences.Count; i++)
{
var sequence = Sequences[i];
sequence.Increment = _increment;
Sequences[i] = sequence;
}
}

public override void OnCastStarted(Actor caster, ActorCastInfo spell)
{
if ((AID)spell.Action.ID == AID.HeavenAndEarthCW)
UpdateIncrement(-30.Degrees());
else if ((AID)spell.Action.ID == AID.HeavenAndEarthCCW)
UpdateIncrement(30.Degrees());

if ((AID)spell.Action.ID == AID.HeavenAndEarthStart)
Sequences.Add(new(_shape, caster.Position, spell.Rotation, _increment, spell.NPCFinishAt, 1.2f, 4));
}

public override void OnEventCast(Actor caster, ActorCastEvent spell)
{
if ((AID)spell.Action.ID == AID.HeavenAndEarthMove && Sequences.Count > 0)
{
AdvanceSequence(_index++ % Sequences.Count, WorldState.CurrentTime);
}
}
}

class HeartOfNatureConcentric(BossModule module) : Components.ConcentricAOEs(module, _shapes)
{
private static readonly AOEShape[] _shapes = [new AOEShapeCircle(10), new AOEShapeDonut(10, 20), new AOEShapeDonut(20, 30)];

public override void OnCastStarted(Actor caster, ActorCastInfo spell)
{
if ((AID)spell.Action.ID == AID.NaturesPulse1)
AddSequence(caster.Position, spell.NPCFinishAt);
}

public override void OnEventCast(Actor caster, ActorCastEvent spell)
{
if (Sequences.Count > 0)
{
var order = (AID)spell.Action.ID switch
{
AID.NaturesPulse1 => 0,
AID.NaturesPulse2 => 1,
AID.NaturesPulse3 => 2,
_ => -1
};
AdvanceSequence(order, caster.Position);
}
}
}

class CagedHeartOfNature(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.CagedHeartOfNature), new AOEShapeCircle(6));

class WindsPeak(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.WindsPeak1), new AOEShapeCircle(5));

class WindsPeakKB(BossModule module) : Components.Knockback(module)
{
private DateTime _time;
private bool _watched;
private DateTime _activation;

public override IEnumerable<Source> Sources(int slot, Actor actor)
{
if (_watched && WorldState.CurrentTime < _time.AddSeconds(4.4f))
yield return new(Module.PrimaryActor.Position, 15, _activation);
}

public override void OnCastStarted(Actor caster, ActorCastInfo spell)
{
if ((AID)spell.Action.ID == AID.WindsPeak1)
{
_watched = true;
_time = WorldState.CurrentTime;
_activation = spell.NPCFinishAt;
}
}
}

class SplittingRage(BossModule module) : Components.CastHint(module, ActionID.MakeSpell(AID.SplittingRage), "Applies temporary misdirection");

class NaturesBlood(BossModule module) : Components.Exaflare(module, 4)
{
class LineWithActor : Line
{
public Actor Caster;

public LineWithActor(Actor caster)
{
Next = caster.Position;
Advance = 6 * caster.Rotation.ToDirection();
NextExplosion = caster.CastInfo!.NPCFinishAt;
TimeToMove = 1.1f;
ExplosionsLeft = 7;
MaxShownExplosions = 3;
Caster = caster;
}
}

public override void OnCastStarted(Actor caster, ActorCastInfo spell)
{
if ((AID)spell.Action.ID is AID.NaturesBlood1)
Lines.Add(new LineWithActor(caster));
}

public override void OnEventCast(Actor caster, ActorCastEvent spell)
{
if (Lines.Count > 0 && (AID)spell.Action.ID is AID.NaturesBlood1 or AID.NaturesBlood2)
{
int index = Lines.FindIndex(item => ((LineWithActor)item).Caster == caster);
AdvanceLine(Lines[index], caster.Position);
if (Lines[index].ExplosionsLeft == 0)
Lines.RemoveAt(index);
}
}
}

class MoveMountains(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.MoveMountains3), new AOEShapeRect(40, 3))
{
// TODO predict rotation
}

class WildfireCrucible(BossModule module) : Components.CastHint(module, ActionID.MakeSpell(AID.WildfireCrucible), "Enrage!", true);
33 changes: 33 additions & 0 deletions BossMod/Modules/Shadowbringers/Foray/Duel/Duel6Lyon/FlamesMeet.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@

namespace BossMod.Shadowbringers.Foray.Duel.Duel6Lyon;
class FlamesMeet(BossModule module) : Components.GenericAOEs(module)
{
private readonly List<AOEInstance> _aoes = [];
private static readonly AOEShapeCross _shape = new(40, 7);

public override IEnumerable<AOEInstance> ActiveAOEs(int slot, Actor actor)
{
for (int i = 0; i < _aoes.Count; i++)
{
AOEInstance aoe = _aoes[i];
if (i == 0)
aoe.Color = ArenaColor.Danger;
yield return aoe;
// Only show the first 2 so it's obvious which one to go to.
if (i == 1)
break;
}
}

public override void OnCastStarted(Actor caster, ActorCastInfo spell)
{
if ((AID)spell.Action.ID == AID.FlamesMeet2)
_aoes.Add(new(_shape, caster.Position));
}

public override void OnCastFinished(Actor caster, ActorCastInfo spell)
{
if ((AID)spell.Action.ID == AID.FlamesMeet2 && _aoes.Count > 0)
_aoes.RemoveAt(0);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
using BossMod.Components;

namespace BossMod.Shadowbringers.Foray.Duel.Duel6Lyon;

class TasteOfBloodAndDuelOrDie(BossModule module) : GenericAOEs(module)
{
private readonly AOEShape _tasteOfBloodShape = new AOEShapeCone(40, 90.Degrees());
public readonly List<Actor> Casters = [];
public readonly List<Actor> Duelers = [];

public override IEnumerable<AOEInstance> ActiveAOEs(int slot, Actor actor)
{
foreach (var caster in Casters)
{
// If the caster did Duel Or Die, the player must get hit by their attack.
// This is represented by pointing the AOE behind the caster so their front is safe.
Angle angle = Duelers.Contains(caster) ? caster.Rotation + 180.Degrees() : caster.Rotation;
yield return new AOEInstance(_tasteOfBloodShape, caster.Position, angle);
}
}

public override void OnCastStarted(Actor caster, ActorCastInfo spell)
{
if ((AID)spell.Action.ID == AID.TasteOfBlood)
Casters.Add(caster);

if ((AID)spell.Action.ID == AID.DuelOrDie)
Duelers.Add(caster);
}

public override void OnCastFinished(Actor caster, ActorCastInfo spell)
{
if ((AID)spell.Action.ID == AID.TasteOfBlood)
{
Casters.Remove(caster);
Duelers.Remove(caster);
}
}

public override void DrawArenaForeground(int pcSlot, Actor pc)
{
foreach (var caster in Casters)
{
bool isDueler = Duelers.Contains(caster);
Arena.Actor(caster, isDueler ? ArenaColor.Danger : ArenaColor.Enemy, true);
}
}

public override void AddHints(int slot, Actor actor, TextHints hints)
{
if (Duelers.Count > 0)
hints.Add($"Get hit by {Duelers.Count} Duel or Die Taste Of Blood");
}
}

0 comments on commit 23a140f

Please sign in to comment.