Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
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
88 changes: 88 additions & 0 deletions PositionalGuide/ConfigWindow.cs
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,9 @@ public const sbyte
MinTetherLengthInner = -1,
MinTetherLengthOuter = -2,
MaxTetherLength = 32;
public const byte
MinFillOpacity = byte.MinValue,
MaxFillOpacity = byte.MaxValue;

private const ImGuiWindowFlags WindowFlags = ImGuiWindowFlags.None
//| ImGuiWindowFlags.NoScrollbar
Expand Down Expand Up @@ -105,6 +108,16 @@ public override void Draw() {
tetherLengthInner = this.conf.TetherLengthInner,
tetherLengthOuter = this.conf.TetherLengthOuter;

byte positionalFillOpacity = this.conf.PositionalFillOpacity;

bool highlightPositionals = this.conf.HighlightPositionals;
bool highlightDuringPerfectBalance = this.conf.HighlightDuringPerfectBalance;
bool positionalIncludeBoundaryLines = this.conf.PositionalIncludeBoundaryLines;
bool positionalColourCircles = this.conf.PositionalColourCircles;
bool fillPositionalZones = this.conf.FillPositionalZones;
Vector4 positionalGoodColour = this.conf.PositionalGoodColour;
Vector4 positionalBadColour = this.conf.PositionalBadColour;

bool active = this.conf.Enabled;
bool tether = this.conf.DrawTetherLine;
bool flattenTether = this.conf.FlattenTether;
Expand Down Expand Up @@ -230,6 +243,67 @@ public override void Draw() {

ImGui.PopStyleVar();

// live positional highlighting, driven by whichever weaponskill is coming up next
ImGui.TextUnformatted("");
changed |= ImGui.Checkbox("Highlight the positional your next attack wants?", ref highlightPositionals);
utils.Tooltip("While this is on and you're playing a job with positionals, the guides stop using the colours set"
+ " above and instead colour the correct side of your target with the 'correct' colour below, and every other"
+ " side with the 'incorrect' colour."
+ "\n"
+ "\nSupported jobs, and what each one watches:"
+ "\n Dragoon - the combo. Chaotic Spring and Wheeling Thrust want the rear, Fang and Claw wants a flank."
+ "\n Monk - coeurl form and your Coeurl's Fury. Demolish wants the rear, Pouncing Coeurl wants a flank."
+ "\n Ninja - the combo and your Kazematoi. Aeolian Edge wants the rear, Armor Crush wants a flank."
+ "\n Reaper - your Soul Reaver window. Gallows wants the rear, Gibbet wants a flank."
+ "\n Samurai - the combo. Gekko wants the rear, Kasha wants a flank."
+ "\n Viper - the combo. Swiftskin's Sting leads to the rear pair, Hunter's Sting to the flank pair."
+ "\n"
+ "\nAnywhere the next weaponskill has no positional - a different job, the wrong point in a combo, a reaper"
+ " with neither Gibbet nor Gallows enhanced - your normal colours above are used instead, so nothing is"
+ " highlighted when nothing needs to be.");

ImGui.PushStyleVar(ImGuiStyleVar.Alpha, highlightPositionals ? 1 : InactiveOptionAlpha);
ImGui.Indent();

changed |= ImGui.ColorEdit4("Colour for the correct side", ref positionalGoodColour, ImGuiColorEditFlags.NoInputs);
changed |= ImGui.ColorEdit4("Colour for the incorrect sides", ref positionalBadColour, ImGuiColorEditFlags.NoInputs);

changed |= ImGui.Checkbox("Monk: keep highlighting during Perfect Balance?", ref highlightDuringPerfectBalance);
utils.Tooltip("Perfect Balance hides your form and lets you use any of them, so there's no form to read."
+ " With this on, the rear is highlighted for the whole window, because Demolish is the coeurl-chakra filler"
+ " and nothing else monk presses under Perfect Balance has a positional at all, so the rear is never the"
+ " wrong place to be standing."
+ "\n"
+ "\nWith this off, highlighting simply switches off until the window ends.");

changed |= ImGui.Checkbox("Count the diagonal guidelines as part of the correct side?", ref positionalIncludeBoundaryLines);
utils.Tooltip("Each positional cone is 90 degrees wide, bounded by two diagonal guidelines and cut down the middle"
+ " by a cardinal one."
+ "\n"
+ "\nWith this on, a cone's two boundary lines are coloured as part of it, so you can see exactly how far it"
+ " extends. With this off, only the cardinal line through the middle of the cone is coloured.");

changed |= ImGui.Checkbox("Recolour the circles to match?", ref positionalColourCircles);
utils.Tooltip("While a positional is being highlighted, colour the target circle and outer circle by side as well,"
+ " overriding the circle colour options above.");

changed |= ImGui.Checkbox("Shade in the correct and incorrect sides?", ref fillPositionalZones);
utils.Tooltip("Fills the area enclosed by the guidelines, so the correct side reads as a solid wedge of colour"
+ " instead of something you have to work out from which lines changed.");

ImGui.PushStyleVar(ImGuiStyleVar.Alpha, highlightPositionals && fillPositionalZones ? 1 : InactiveOptionAlpha);
ImGui.PushItemWidth(300 * scale);
changed |= ImGui.SliderScalar("Shading opacity", ImGuiDataType.U8, ref positionalFillOpacity, MinFillOpacity, MaxFillOpacity, "%i", ImGuiSliderFlags.AlwaysClamp);
utils.Tooltip("How solid the shading is, from 0 (invisible) to 255 (completely opaque)."
+ "\n"
+ "\nLow values are recommended, since the shading covers your target.");
ImGui.PopItemWidth();
ImGui.PopStyleVar();

ImGui.Unindent();
ImGui.PopStyleVar();
ImGui.TextUnformatted("");

// sliders for numeric modifiers to the lines being drawn
ImGui.PushItemWidth(470 * scale);

Expand Down Expand Up @@ -298,6 +372,12 @@ public override void Draw() {
ImGui.TextUnformatted("");
ImGui.TextUnformatted("Additionally, you can use 'cardinal', 'cardinals', 'diagonal', 'diagonals', 'lines', and 'all' to affect multiple lines with a single command.");
ImGui.TextUnformatted("");
ImGui.TextUnformatted("Positional highlighting has its own targets:");
ImGui.Indent();
ImGui.TextUnformatted("For the highlighting itself: p, positional, positionals");
ImGui.TextUnformatted("For the shading: fill, shading");
ImGui.Unindent();
ImGui.TextUnformatted("");
ImGui.TextUnformatted("Finally, you can use the target 'tether' to toggle tether line rendering, and the target 'render' (the default) to toggle showing guides at all without losing your settings.");
ImGui.PopTextWrapPos();
}
Expand All @@ -314,6 +394,14 @@ public override void Draw() {
this.conf.AlwaysUseCircleColours = circleColours[0];
this.conf.AlwaysUseCircleColoursTarget = circleColours[1];
this.conf.AlwaysUseCircleColoursOuter = circleColours[2];
this.conf.HighlightPositionals = highlightPositionals;
this.conf.HighlightDuringPerfectBalance = highlightDuringPerfectBalance;
this.conf.PositionalIncludeBoundaryLines = positionalIncludeBoundaryLines;
this.conf.PositionalColourCircles = positionalColourCircles;
this.conf.FillPositionalZones = fillPositionalZones;
this.conf.PositionalFillOpacity = positionalFillOpacity;
this.conf.PositionalGoodColour = positionalGoodColour;
this.conf.PositionalBadColour = positionalBadColour;
this.conf.ExtraDrawRange = guideLengthMod;
this.conf.MinDrawRange = minGuideLength;
this.conf.MaxDrawRange = maxGuideLength;
Expand Down
10 changes: 10 additions & 0 deletions PositionalGuide/Configuration.cs
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,16 @@ public const int
public bool AlwaysUseCircleColoursTarget { get; set; } = false;
public bool AlwaysUseCircleColoursOuter { get; set; } = false;

public bool HighlightPositionals { get; set; } = false;
public bool HighlightDuringPerfectBalance { get; set; } = true;
public bool PositionalIncludeBoundaryLines { get; set; } = true;
public bool PositionalColourCircles { get; set; } = true;
public bool FillPositionalZones { get; set; } = true;
public byte PositionalFillOpacity { get; set; } = 48;

public Vector4 PositionalGoodColour { get; set; } = new(0, 1, 0, 1);
public Vector4 PositionalBadColour { get; set; } = new(1, 0, 0, 1);

/// <summary>
/// Starts at front then goes clockwise up to index=7, then circle at index=8
/// </summary>
Expand Down
119 changes: 113 additions & 6 deletions PositionalGuide/Plugin.cs
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ private enum CircleTypes { Target, Outer };
[PluginService] public static IPluginLog Log { get; private set; } = null!;
[PluginService] public static INotificationManager Notifications { get; private set; } = null!;
[PluginService] public static IObjectTable Objects { get; private set; } = null!;
[PluginService] public static IJobGauges Gauges { get; private set; } = null!;

public Configuration Config { get; private set; }

Expand Down Expand Up @@ -164,15 +165,31 @@ internal void Draw() {
if (!targetOnScreen && !limitEither && limitInner)
return;

// which side of the target does the next weaponskill want to be hit from, if any
RequiredPositional positional = this.Config.HighlightPositionals
? Positionals.Detect(player, this.Config)
: RequiredPositional.None;

// +X = east, -X = west
// +Z = south, -Z = north
Vector3 guidelineBasePoint2 = targetPos + new Vector3(0, 0, length);
Vector3 circleBasePoint = targetPos + new Vector3(0, 0, target.HitboxRadius);

bool anyLineActive = false;
for (int lineIndex = Configuration.IndexFront; lineIndex <= Configuration.IndexFrontLeft; ++lineIndex) {
if (this.Config.DrawGuides[lineIndex]) {
anyLineActive = true;
break;
}
}

// the shaded wedges go down first so that every line and circle draws on top of them
if (positional is not RequiredPositional.None && this.Config.FillPositionalZones && targetOnScreen)
this.fillPositionalZones(drawing, targetPos, centre, targetFacing, length, positional);

for (int lineIndex = Configuration.IndexFront; lineIndex <= Configuration.IndexFrontLeft; ++lineIndex) {
if (!this.Config.DrawGuides[lineIndex])
continue;
anyLineActive = true;

Vector3 rotated = rotatePoint(targetPos, guidelineBasePoint2, targetFacing + this.lineIndexToAngle[lineIndex]);
bool endpointOnScreen = Gui.WorldToScreen(rotated, out Vector2 coord);
Expand All @@ -183,14 +200,14 @@ internal void Draw() {
else if (limitOuter && !endpointOnScreen) {
continue;
}
drawing.AddLine(centre, coord, ImGui.GetColorU32(this.Config.LineColours[lineIndex]), this.Config.LineThickness);
drawing.AddLine(centre, coord, ImGui.GetColorU32(this.resolveLineColour(lineIndex, positional)), this.Config.LineThickness);
}

if (this.Config.DrawCircle)
this.drawCircle(drawing, targetPos, circleBasePoint, targetFacing, anyLineActive, CircleTypes.Target);
this.drawCircle(drawing, targetPos, circleBasePoint, targetFacing, anyLineActive, CircleTypes.Target, positional);

if (this.Config.DrawOuterCircle)
this.drawCircle(drawing, targetPos, circleBasePoint, targetFacing, anyLineActive, CircleTypes.Outer);
this.drawCircle(drawing, targetPos, circleBasePoint, targetFacing, anyLineActive, CircleTypes.Outer, positional);

// the tether line itself used to be pretty simple: from our position to the target's
// then I decided to allow controlling the maximum length of it and suddenly everything got Complicated
Expand Down Expand Up @@ -247,10 +264,89 @@ internal void Draw() {
ImGui.End();
}

private void drawCircle(ImDrawListPtr drawing, Vector3 targetPos, Vector3 basePoint, float targetFacing, bool anyLineActive, CircleTypes circleType) {
/// <summary>
/// Whether a guideline belongs to the zone the next weaponskill wants. Each 90° positional cone is
/// bounded by two diagonal guidelines and bisected by a cardinal one, so the boundaries can either
/// count as part of the cone they enclose or be left to the cone on the other side of them.
/// </summary>
private static bool lineIsInZone(int lineIndex, RequiredPositional positional, bool includeBoundaries) => positional switch {
RequiredPositional.Rear => lineIndex is Configuration.IndexBack
|| (includeBoundaries && lineIndex is Configuration.IndexBackRight or Configuration.IndexBackLeft),
RequiredPositional.Flank => lineIndex is Configuration.IndexRight or Configuration.IndexLeft
|| (includeBoundaries && lineIndex is Configuration.IndexFrontRight or Configuration.IndexBackRight
or Configuration.IndexBackLeft or Configuration.IndexFrontLeft),
_ => false,
};

/// <summary>
/// Whether a circle/fill segment falls inside the cone the next weaponskill wants. Segment 0 sits on
/// the target's front and they run clockwise, so eighths of the circle line up with the guidelines.
/// </summary>
private static bool segmentIsInZone(int segmentIndex, RequiredPositional positional) {
int eighth = segmentIndex * 8 / CircleSegmentCount;
return positional switch {
RequiredPositional.Rear => eighth is 3 or 4,
RequiredPositional.Flank => eighth is 1 or 2 or 5 or 6,
_ => false,
};
}

private Vector4 resolveLineColour(int lineIndex, RequiredPositional positional)
=> positional is RequiredPositional.None
? this.Config.LineColours[lineIndex]
: lineIsInZone(lineIndex, positional, this.Config.PositionalIncludeBoundaryLines)
? this.Config.PositionalGoodColour
: this.Config.PositionalBadColour;

private Vector4 resolveSegmentColour(int segmentIndex, RequiredPositional positional)
=> segmentIsInZone(segmentIndex, positional)
? this.Config.PositionalGoodColour
: this.Config.PositionalBadColour;

/// <summary>
/// Shades the area enclosed by the guidelines, so the correct cone reads as a solid wedge of colour
/// rather than something to be inferred from which lines changed.
/// </summary>
private void fillPositionalZones(ImDrawListPtr drawing, Vector3 targetPos, Vector2 centre, float targetFacing, float radius, RequiredPositional positional) {
if (this.Config.PositionalFillOpacity is 0)
return;

Vector3 startPoint = rotatePoint(targetPos, targetPos + new Vector3(0, 0, radius), targetFacing);
Vector3[] points = circlePoints(targetPos, startPoint, this.circleSegmentIdxToAngle).ToArray();

(Vector2 point, bool render)[] screenPoints = new (Vector2 point, bool render)[points.Length];
for (int i = 0; i < points.Length; ++i) {
bool render = Gui.WorldToScreen(points[i], out Vector2 screenPoint);
screenPoints[i] = (screenPoint, render);
}

float opacity = this.Config.PositionalFillOpacity / 255f;

// the fan's triangles share their edges exactly, so they tile seamlessly - but each one's antialiasing
// fringe would blend over its neighbour's and leave 128 spokes radiating out of the target, so the fill
// gets drawn unantialiased and the flag is put back immediately afterwards
ImDrawListFlags originalFlags = drawing.Flags;
drawing.Flags &= ~ImDrawListFlags.AntiAliasedFill;

for (int i = 0; i < screenPoints.Length; ++i) {
int nextIndex = (i + 1) % screenPoints.Length;
if (!screenPoints[i].render || !screenPoints[nextIndex].render)
continue;

Vector4 colour = this.resolveSegmentColour(i, positional);
colour.W *= opacity;

drawing.AddTriangleFilled(centre, screenPoints[i].point, screenPoints[nextIndex].point, ImGui.GetColorU32(colour));
}

drawing.Flags = originalFlags;
}

private void drawCircle(ImDrawListPtr drawing, Vector3 targetPos, Vector3 basePoint, float targetFacing, bool anyLineActive, CircleTypes circleType, RequiredPositional positional) {
Vector4 circleColour = new(1, 0, 0, 1);
Vector3 circleBasePoint = basePoint;
bool forceCircleColour = false;
bool usePositionalColours = positional is not RequiredPositional.None && this.Config.PositionalColourCircles;

switch (circleType) {
case CircleTypes.Target:
Expand Down Expand Up @@ -281,7 +377,9 @@ private void drawCircle(ImDrawListPtr drawing, Vector3 targetPos, Vector3 basePo
#pragma warning restore IDE0042 // Deconstruct variable declaration

if (curPoint.render && nextPoint.render) {
Vector4 colour = forceCircleColour ? circleColour : this.circleSegmentIdxToColour[i];
Vector4 colour = usePositionalColours
? this.resolveSegmentColour(i, positional)
: forceCircleColour ? circleColour : this.circleSegmentIdxToColour[i];
drawing.AddLine(curPoint.point, nextPoint.point, ImGui.GetColorU32(colour), this.Config.LineThickness + 2);
}
}
Expand Down Expand Up @@ -434,6 +532,15 @@ internal void OnPluginCommand(string command, string arguments) {
this.Config.DrawCircle = state ?? !this.Config.DrawCircle;
this.Config.DrawOuterCircle = state ?? !this.Config.DrawOuterCircle;
break;
case "p":
case "positional":
case "positionals":
this.Config.HighlightPositionals = state ?? !this.Config.HighlightPositionals;
break;
case "fill":
case "shading":
this.Config.FillPositionalZones = state ?? !this.Config.FillPositionalZones;
break;
case "tether":
this.Config.DrawTetherLine = state ?? !this.Config.DrawTetherLine;
//Notifications.AddNotification(new() {
Expand Down
Loading