Skip to content

Commit

Permalink
Merge pull request #497 from FFXIV-CombatReborn/mergeWIP
Browse files Browse the repository at this point in the history
Daen Ose The Avaricious Ultros module
  • Loading branch information
CarnifexOptimus authored Dec 12, 2024
2 parents 11c1ab8 + ee6a361 commit 4efe0fb
Show file tree
Hide file tree
Showing 11 changed files with 225 additions and 56 deletions.
4 changes: 4 additions & 0 deletions BossMod/BossModReborn.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -103,4 +103,8 @@
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
</ItemGroup>

<ItemGroup Condition="'$(Configuration)' == 'Release'">
<EmbeddedResource Include="Pathfinding\ObstacleMaps\*" />
</ItemGroup>
</Project>
12 changes: 7 additions & 5 deletions BossMod/Debug/DebugObstacles.cs
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,8 @@ protected override void DrawSidebar()
ImGui.SameLine();
if (ImGui.Button("Reload"))
{
CheckpointNoClone(new(owner.Obstacles.RootPath + e.Filename));
using var stream = File.OpenRead(owner.Obstacles.RootPath + e.Filename);
CheckpointNoClone(new(stream));
}

ImGui.SetNextItemWidth(100);
Expand Down Expand Up @@ -162,6 +163,7 @@ public void Draw()
private void DrawEntries(List<ObstacleMapDatabase.Entry> entries)
{
Action? modifications = null;
using var disableScope = ImRaii.Disabled(!Obstacles.CanEditDatabase());
for (int i = 0; i < entries.Count; ++i)
{
using var id = ImRaii.PushId(i);
Expand All @@ -174,9 +176,8 @@ private void DrawEntries(List<ObstacleMapDatabase.Entry> entries)
if (ImGui.Button("Move down"))
modifications += () => (entries[index], entries[index + 1]) = (entries[index + 1], entries[index]);
ImGui.SameLine();
using (ImRaii.Disabled(!Obstacles.CanEditDatabase()))
if (ImGui.Button("Delete"))
modifications += () => DeleteMap(entries, index);
if (ImGui.Button("Delete"))
modifications += () => DeleteMap(entries, index);
ImGui.SameLine();
if (ImGui.Button("Edit"))
OpenEditor(entries[index]);
Expand Down Expand Up @@ -230,7 +231,8 @@ private string GenerateMapName()

private void OpenEditor(ObstacleMapDatabase.Entry entry)
{
var editor = new Editor(this, new(Obstacles.RootPath + entry.Filename), entry);
using var stream = File.OpenRead(Obstacles.RootPath + entry.Filename);
var editor = new Editor(this, new(stream), entry);
_ = new UISimpleWindow($"Obstacle map {entry.Filename}", editor.Draw, true, new(1000, 1000));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,24 @@ class Ceras(BossModule module) : Components.SingleTargetCast(module, ActionID.Ma

class WaveOfTurmoil(BossModule module) : Components.KnockbackFromCastTarget(module, ActionID.MakeSpell(AID.WaveOfTurmoil), 20, stopAtWall: true)
{
public override bool DestinationUnsafe(int slot, Actor actor, WPos pos) => Module.FindComponent<Hydrobomb>()?.ActiveAOEs(slot, actor).Any(z => z.Shape.Check(pos, z.Origin, z.Rotation)) ?? false;
private readonly Hydrobomb _aoe = module.FindComponent<Hydrobomb>()!;

public override bool DestinationUnsafe(int slot, Actor actor, WPos pos) => _aoe?.ActiveAOEs(slot, actor).Any(z => z.Shape.Check(pos, z.Origin, z.Rotation)) ?? false;

public override void AddAIHints(int slot, Actor actor, PartyRolesConfig.Assignment assignment, AIHints hints)
{
var forbidden = new List<Func<WPos, float>>();
var source = Sources(slot, actor).FirstOrDefault();
if (source != default)
{
foreach (var c in _aoe.ActiveAOEs(slot, actor))
{
forbidden.Add(ShapeDistance.Cone(Arena.Center, 20, Angle.FromDirection(c.Origin - Module.Center), 30.Degrees()));
}
if (forbidden.Count > 0)
hints.AddForbiddenZone(p => forbidden.Min(f => f(p)), source.Activation);
}
}
}

class Hydrobomb(BossModule module) : Components.LocationTargetedAOEs(module, ActionID.MakeSpell(AID.Hydrobomb), 10);
Expand All @@ -70,8 +87,8 @@ public GymnasiouMeganereisStates(BossModule module) : base(module)
{
TrivialPhase()
.ActivateOnEnter<Ceras>()
.ActivateOnEnter<WaveOfTurmoil>()
.ActivateOnEnter<Hydrobomb>()
.ActivateOnEnter<WaveOfTurmoil>()
.ActivateOnEnter<Waterspout>()
.ActivateOnEnter<Hydrocannon>()
.ActivateOnEnter<Hydrocannon2>()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -161,7 +161,7 @@ public DaenOseTheAvariciousTyphonStates(BossModule module) : base(module)
}
}

[ModuleInfo(BossModuleInfo.Maturity.Verified, Contributors = "The Combat Reborn Team (Malediktus)", GroupType = BossModuleInfo.GroupType.CFC, GroupID = 745, NameID = 9808)]
[ModuleInfo(BossModuleInfo.Maturity.Verified, Contributors = "The Combat Reborn Team (Malediktus)", GroupType = BossModuleInfo.GroupType.CFC, GroupID = 745, NameID = 9808, SortOrder = 1)]
public class DaenOseTheAvariciousTyphon(WorldState ws, Actor primary) : THTemplate(ws, primary)
{
private static readonly uint[] bonusAdds = [(uint)OID.SecretEgg, (uint)OID.SecretGarlic, (uint)OID.SecretOnion, (uint)OID.SecretTomato,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
namespace BossMod.Shadowbringers.TreasureHunt.ShiftingOubliettesOfLyheGhiah.DaenOseTheAvariciousUltros;

public enum OID : uint
{
Boss = 0x3030, // R0.75-5.1
StylishTentacle = 0x3031, // R7.2
SecretQueen = 0x3021, // R0.84, icon 5, needs to be killed in order from 1 to 5 for maximum rewards
SecretGarlic = 0x301F, // R0.84, icon 3, needs to be killed in order from 1 to 5 for maximum rewards
SecretTomato = 0x3020, // R0.84, icon 4, needs to be killed in order from 1 to 5 for maximum rewards
SecretOnion = 0x301D, // R0.84, icon 1, needs to be killed in order from 1 to 5 for maximum rewards
SecretEgg = 0x301E, // R0.84, icon 2, needs to be killed in order from 1 to 5 for maximum rewards
Helper = 0x233C
}

public enum AID : uint
{
AutoAttack = 872, // Boss/SecretTomato/SecretQueen->player, no cast, single-target
Change = 21741, // Boss->self, 6.0s cast, single-target, boss morphs into Ultros

TentacleVisual = 21753, // Boss->self, no cast, single-target
Tentacle = 21754, // StylishTentacle->self, 3.0s cast, range 8 circle
Megavolt = 21752, // Boss->self, 3.5s cast, range 11 circle
Wallop = 21755, // StylishTentacle->self, 5.0s cast, range 20 width 10 rect
ThunderIII = 21743, // Boss->player, 4.0s cast, single-target, tankbuster

WaveOfTurmoilVisual = 21748, // Boss->self, 5.0s cast, single-target
WaveOfTurmoil = 21749, // Helper->self, 5.0s cast, range 40 circle, knockback 20, away from source

SoakingSplatter = 21750, // Helper->location, 6.5s cast, range 10 circle
AquaBreath = 21751, // Boss->self, 3.0s cast, range 13 90-degree cone

FallingWaterVisual = 21746, // Boss->self, 5.0s cast, single-target
FallingWater = 21747, // Helper->player, 5.0s cast, range 8 circle, spread

WaterspoutVisual = 21744, // Boss->self, 3.0s cast, single-target
Waterspout = 21745, // Helper->location, 3.0s cast, range 4 circle

Pollen = 6452, // SecretQueen->self, 3.5s cast, range 6+R circle
TearyTwirl = 6448, // SecretOnion->self, 3.5s cast, range 6+R circle
HeirloomScream = 6451, // SecretTomato->self, 3.5s cast, range 6+R circle
PluckAndPrune = 6449, // SecretEgg->self, 3.5s cast, range 6+R circle
PungentPirouette = 6450, // SecretGarlic->self, 3.5s cast, range 6+R circle
Telega = 9630 // Mandragoras->self, no cast, single-target, bonus adds disappear
}

class AquaBreath(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.AquaBreath), new AOEShapeCone(13, 45.Degrees()));
class Tentacle(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.Tentacle), new AOEShapeCircle(8));
class Wallop(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.Wallop), new AOEShapeRect(20, 5));
class Megavolt(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.Megavolt), new AOEShapeCircle(11));
class Waterspout(BossModule module) : Components.LocationTargetedAOEs(module, ActionID.MakeSpell(AID.Waterspout), 4);
class SoakingSplatter(BossModule module) : Components.LocationTargetedAOEs(module, ActionID.MakeSpell(AID.SoakingSplatter), 10);
class FallingWater(BossModule module) : Components.SpreadFromCastTargets(module, ActionID.MakeSpell(AID.FallingWater), 8);
class ThunderIII(BossModule module) : Components.SingleTargetCast(module, ActionID.MakeSpell(AID.ThunderIII));

class WaveOfTurmoil(BossModule module) : Components.KnockbackFromCastTarget(module, ActionID.MakeSpell(AID.WaveOfTurmoil), 20, stopAtWall: true)
{
private readonly SoakingSplatter _aoe = module.FindComponent<SoakingSplatter>()!;

public override bool DestinationUnsafe(int slot, Actor actor, WPos pos) => _aoe?.ActiveAOEs(slot, actor).Any(z => z.Shape.Check(pos, z.Origin, z.Rotation)) ?? false;

public override void AddAIHints(int slot, Actor actor, PartyRolesConfig.Assignment assignment, AIHints hints)
{
var forbidden = new List<Func<WPos, float>>();
var source = Sources(slot, actor).FirstOrDefault();
if (source != default)
{
foreach (var c in _aoe.ActiveAOEs(slot, actor))
{
forbidden.Add(ShapeDistance.Cone(Arena.Center, 20, Angle.FromDirection(c.Origin - Module.Center), 30.Degrees()));
}
if (forbidden.Count > 0)
hints.AddForbiddenZone(p => forbidden.Min(f => f(p)), source.Activation);
}
}
}

abstract class Mandragoras(BossModule module, AID aid) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(aid), new AOEShapeCircle(6.84f));
class PluckAndPrune(BossModule module) : Mandragoras(module, AID.PluckAndPrune);
class TearyTwirl(BossModule module) : Mandragoras(module, AID.TearyTwirl);
class HeirloomScream(BossModule module) : Mandragoras(module, AID.HeirloomScream);
class PungentPirouette(BossModule module) : Mandragoras(module, AID.PungentPirouette);
class Pollen(BossModule module) : Mandragoras(module, AID.Pollen);

class DaenOseTheAvariciousUltrosStates : StateMachineBuilder
{
public DaenOseTheAvariciousUltrosStates(BossModule module) : base(module)
{
TrivialPhase()
.ActivateOnEnter<AquaBreath>()
.ActivateOnEnter<Tentacle>()
.ActivateOnEnter<Wallop>()
.ActivateOnEnter<Megavolt>()
.ActivateOnEnter<Waterspout>()
.ActivateOnEnter<SoakingSplatter>()
.ActivateOnEnter<FallingWater>()
.ActivateOnEnter<ThunderIII>()
.ActivateOnEnter<WaveOfTurmoil>()
.ActivateOnEnter<PluckAndPrune>()
.ActivateOnEnter<TearyTwirl>()
.ActivateOnEnter<HeirloomScream>()
.ActivateOnEnter<PungentPirouette>()
.ActivateOnEnter<Pollen>()
.Raw.Update = () => module.Enemies(DaenOseTheAvariciousUltros.All).All(x => x.IsDeadOrDestroyed);
}
}

[ModuleInfo(BossModuleInfo.Maturity.Verified, Contributors = "The Combat Reborn Team (Malediktus)", GroupType = BossModuleInfo.GroupType.CFC, GroupID = 745, NameID = 9808, SortOrder = 2)]
public class DaenOseTheAvariciousUltros(WorldState ws, Actor primary) : THTemplate(ws, primary)
{
private static readonly uint[] bonusAdds = [(uint)OID.SecretEgg, (uint)OID.SecretGarlic, (uint)OID.SecretOnion, (uint)OID.SecretTomato,
(uint)OID.SecretQueen];
public static readonly uint[] All = [(uint)OID.Boss, .. bonusAdds];

protected override void DrawEnemies(int pcSlot, Actor pc)
{
Arena.Actor(PrimaryActor);
Arena.Actors(Enemies(bonusAdds), Colors.Vulnerable);
}

protected override void CalculateModuleAIHints(int slot, Actor actor, PartyRolesConfig.Assignment assignment, AIHints hints)
{
for (var i = 0; i < hints.PotentialTargets.Count; ++i)
{
var e = hints.PotentialTargets[i];
e.Priority = (OID)e.Actor.OID switch
{
OID.SecretOnion => 5,
OID.SecretEgg => 4,
OID.SecretGarlic => 3,
OID.SecretTomato => 2,
OID.SecretQueen => 1,
_ => 0
};
}
}
}
39 changes: 16 additions & 23 deletions BossMod/Pathfinding/ObstacleMapDatabase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,35 +19,28 @@ public sealed record class Entry(Vector3 MinBounds, Vector3 MaxBounds, WPos Orig

public readonly Dictionary<uint, List<Entry>> Entries = [];

public void Load(string listPath)
public void Load(Stream stream)
{
Entries.Clear();
try
using var json = Serialization.ReadJson(stream);
foreach (var jentries in json.RootElement.EnumerateObject())
{
using var json = Serialization.ReadJson(listPath);
foreach (var jentries in json.RootElement.EnumerateObject())
var sep = jentries.Name.IndexOf('.', StringComparison.Ordinal);
var zone = sep >= 0 ? uint.Parse(jentries.Name.AsSpan()[..sep]) : uint.Parse(jentries.Name);
var cfc = sep >= 0 ? uint.Parse(jentries.Name.AsSpan()[(sep + 1)..]) : 0;
var entries = Entries[(zone << 16) | cfc] = [];
foreach (var jentry in jentries.Value.EnumerateArray())
{
var sep = jentries.Name.IndexOf('.', StringComparison.Ordinal);
var zone = sep >= 0 ? uint.Parse(jentries.Name.AsSpan()[..sep]) : uint.Parse(jentries.Name);
var cfc = sep >= 0 ? uint.Parse(jentries.Name.AsSpan()[(sep + 1)..]) : 0;
var entries = Entries[(zone << 16) | cfc] = [];
foreach (var jentry in jentries.Value.EnumerateArray())
{
entries.Add(new(
ReadVec3(jentry, nameof(Entry.MinBounds)),
ReadVec3(jentry, nameof(Entry.MaxBounds)),
ReadWPos(jentry, nameof(Entry.Origin)),
jentry.GetProperty(nameof(Entry.ViewWidth)).GetInt32(),
jentry.GetProperty(nameof(Entry.ViewHeight)).GetInt32(),
jentry.GetProperty(nameof(Entry.Filename)).GetString() ?? ""
));
}
entries.Add(new(
ReadVec3(jentry, nameof(Entry.MinBounds)),
ReadVec3(jentry, nameof(Entry.MaxBounds)),
ReadWPos(jentry, nameof(Entry.Origin)),
jentry.GetProperty(nameof(Entry.ViewWidth)).GetInt32(),
jentry.GetProperty(nameof(Entry.ViewHeight)).GetInt32(),
jentry.GetProperty(nameof(Entry.Filename)).GetString() ?? ""
));
}
}
catch (Exception ex)
{
Service.Log($"Failed to load obstacle map database '{listPath}': {ex}");
}
}

public void Save(string listPath)
Expand Down
33 changes: 25 additions & 8 deletions BossMod/Pathfinding/ObstacleMapManager.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
namespace BossMod.Pathfinding;
using System.IO;
using System.Reflection;

namespace BossMod.Pathfinding;

[ConfigDisplay(Name = "Obstacle map development", Order = 8)]
public sealed class ObstacleMapConfig : ConfigNode
Expand All @@ -19,6 +22,8 @@ public sealed class ObstacleMapManager : IDisposable
private readonly EventSubscriptions _subscriptions;
private readonly List<(ObstacleMapDatabase.Entry entry, Bitmap data)> _entries = [];

public bool LoadFromSource => _config.LoadFromSource;

public ObstacleMapManager(WorldState ws)
{
World = ws;
Expand All @@ -41,10 +46,20 @@ public void Dispose()

public void ReloadDatabase()
{
var dbPath = _config.LoadFromSource ? _config.SourcePath : ""; // TODO: load from near assembly instead
Service.Log($"Loading obstacle database from '{dbPath}'");
Database.Load(dbPath);
RootPath = dbPath[..(dbPath.LastIndexOfAny(['\\', '/']) + 1)];
Service.Log($"Loading obstacle database from {(_config.LoadFromSource ? _config.SourcePath : "<embedded>")}");
RootPath = _config.LoadFromSource ? _config.SourcePath[..(_config.SourcePath.LastIndexOfAny(['\\', '/']) + 1)] : "";

try
{
using var dbStream = _config.LoadFromSource ? File.OpenRead(_config.SourcePath) : GetEmbeddedResource("maplist.json");
Database.Load(dbStream);
}
catch (Exception ex)
{
Service.Log($"Failed to load obstacle database: {ex}");
Database.Entries.Clear();
}

LoadMaps(World.CurrentZone, World.CurrentCFCID);
}

Expand All @@ -63,17 +78,19 @@ private void LoadMaps(ushort zoneId, ushort cfcId)
{
foreach (var e in entries)
{
var filename = RootPath + e.Filename;
try
{
var bitmap = new Bitmap(filename);
using var eStream = _config.LoadFromSource ? File.OpenRead(RootPath + e.Filename) : GetEmbeddedResource(e.Filename);
var bitmap = new Bitmap(eStream);
_entries.Add((e, bitmap));
}
catch (Exception ex)
{
Service.Log($"Failed to load map {filename} for {zoneId}.{cfcId}: {ex}");
Service.Log($"Failed to load map {e.Filename} from {(_config.LoadFromSource ? RootPath : "<embedded>")} for {zoneId}.{cfcId}: {ex}");
}
}
}
}

private Stream GetEmbeddedResource(string name) => Assembly.GetExecutingAssembly().GetManifestResourceStream($"BossMod.Pathfinding.ObstacleMaps.{name}") ?? throw new InvalidDataException($"Missing embedded resource {name}");
}
25 changes: 12 additions & 13 deletions BossMod/Util/Bitmap.cs
Original file line number Diff line number Diff line change
Expand Up @@ -150,29 +150,28 @@ public Bitmap(int width, int height, Color color0, Color color1, int resolution
Pixels = new byte[height * BytesPerRow];
}

public Bitmap(string filename)
public Bitmap(Stream stream)
{
using var fstream = File.OpenRead(filename);
using var reader = new BinaryReader(fstream);
var fileHeader = fstream.ReadStruct<FileHeader>();
using var reader = new BinaryReader(stream);
var fileHeader = stream.ReadStruct<FileHeader>();
if (fileHeader.Type != Magic)
throw new ArgumentException($"File '{filename}' is not a bitmap: magic is {fileHeader.Type:X4}");
throw new ArgumentException($"Not a bitmap: magic is {fileHeader.Type:X4}");

var header = fstream.ReadStruct<BitmapInfoHeader>();
var header = stream.ReadStruct<BitmapInfoHeader>();
if (header.SizeInBytes != Marshal.SizeOf<BitmapInfoHeader>())
throw new ArgumentException($"Bitmap '{filename}' has unsupported header size {header.SizeInBytes}");
throw new ArgumentException($"Bitmap has unsupported header size {header.SizeInBytes}");
if (header.Width <= 0)
throw new ArgumentException($"Bitmap '{filename}' has non-positive width {header.Width}");
throw new ArgumentException($"Bitmap has non-positive width {header.Width}");
if (header.Height >= 0)
throw new ArgumentException($"Bitmap '{filename}' is not top-down (height={header.Height})");
throw new ArgumentException($"Bitmap is not top-down (height={header.Height})");
if (header.BitCount != 1)
throw new ArgumentException($"Bitmap '{filename}' is not 1bpp (bitcount={header.BitCount})");
throw new ArgumentException($"Bitmap is not 1bpp (bitcount={header.BitCount})");
if (header.Compression != 0)
throw new ArgumentException($"Bitmap '{filename}' has unsupported compression method {header.Compression:X8}");
throw new ArgumentException($"Bitmap has unsupported compression method {header.Compression:X8}");
if (header.XPixelsPerMeter != header.YPixelsPerMeter || header.XPixelsPerMeter <= 0)
throw new ArgumentException($"Bitmap '{filename}' has inconsistent or non-positive resolution {header.XPixelsPerMeter}x{header.YPixelsPerMeter}");
throw new ArgumentException($"Bitmap has inconsistent or non-positive resolution {header.XPixelsPerMeter}x{header.YPixelsPerMeter}");
if (header.ColorUsedCount is not 0 or 2)
throw new ArgumentException($"Bitmap '{filename}' has wrong palette size {header.ColorUsedCount}");
throw new ArgumentException($"Bitmap has wrong palette size {header.ColorUsedCount}");

Width = header.Width;
Height = -header.Height;
Expand Down
Loading

0 comments on commit 4efe0fb

Please sign in to comment.