diff --git a/src/ArcadeMaker.Core/IGame.Funcs.cs b/src/ArcadeMaker.Core/IGame.Funcs.cs
index 47e7512..e33f025 100644
--- a/src/ArcadeMaker.Core/IGame.Funcs.cs
+++ b/src/ArcadeMaker.Core/IGame.Funcs.cs
@@ -693,6 +693,22 @@ Exp.Void DrawLine(Exp.Instance? _, IValue?[] args)
[Param("thickness", ParamType.Number, "Outline thickness.", Optional = true)]
Exp.Void DrawEllipse(Exp.Instance? _, IValue?[] args);
+ ///
+ /// Draws a triangle.
+ ///
+ /// The calling EXP instance (unused).
+ /// (x1, y1, x2, y2, x3, y3, [outline], [thickness]).
+ [EngineFunc(6, 7, 8)]
+ [Param("x1", ParamType.Number, "The x value of the first point in the triangle.")]
+ [Param("y1", ParamType.Number, "The y value of the first point in the triangle.")]
+ [Param("x2", ParamType.Number, "The x value of the second point in the triangle.")]
+ [Param("y2", ParamType.Number, "The y value of the second point in the triangle.")]
+ [Param("x3", ParamType.Number, "The x value of the third point in the triangle.")]
+ [Param("y3", ParamType.Number, "The y value of the third point in the triangle.")]
+ [Param("outline", ParamType.Bool, "Whether to fill a triangle or only draw the outline.", Optional = true)]
+ [Param("thickness", ParamType.Number, "Outline thickness.", Optional = true)]
+ Exp.Void DrawTriangle(Exp.Instance? _, IValue?[] args);
+
///
/// Draws a path starting at the path's start position or an optional provided position.
///
diff --git a/src/ArcadeMaker.IDE/Debugging/FutileGame.cs b/src/ArcadeMaker.IDE/Debugging/FutileGame.cs
index 6b8decb..48f94cd 100644
--- a/src/ArcadeMaker.IDE/Debugging/FutileGame.cs
+++ b/src/ArcadeMaker.IDE/Debugging/FutileGame.cs
@@ -31,6 +31,7 @@ public void DrawBackgrounds() { }
public void DrawLine(double x1, double y1, double x2, double y2, double thickness) { }
public Exp.Void DrawRect(Exp.Instance? _, IValue?[] args) => null!;
public Exp.Void DrawEllipse(Exp.Instance? _, IValue?[] args) => null!;
+ public Exp.Void DrawTriangle(Exp.Instance? _, IValue?[] args) => null!;
public void SetWindowsSize(int w, int h) { }
public void SetCaption(string caption) { }
public Color BackColor { get; set; }
diff --git a/src/Engines/MonoGame/Core/ArcadeMakerMonoGame.cs b/src/Engines/MonoGame/Core/ArcadeMakerMonoGame.cs
index f84286c..ad5baa2 100644
--- a/src/Engines/MonoGame/Core/ArcadeMakerMonoGame.cs
+++ b/src/Engines/MonoGame/Core/ArcadeMakerMonoGame.cs
@@ -744,6 +744,65 @@ public Exp.Void DrawEllipse(Exp.Instance? _, IValue?[] args)
return Exp.Void.Return;
}
+ public Exp.Void DrawTriangle(Exp.Instance? _, IValue?[] args)
+ {
+ var x1 = (float)args[0].ThrowIfNull().Number;
+ var y1 = (float)args[1].ThrowIfNull().Number;
+ var x2 = (float)args[2].ThrowIfNull().Number;
+ var y2 = (float)args[3].ThrowIfNull().Number;
+ var x3 = (float)args[4].ThrowIfNull().Number;
+ var y3 = (float)args[5].ThrowIfNull().Number;
+ bool outline = args.Length < 7 || args[6].ThrowIfNull().Bool;
+ var thickness = args.Length >= 8 ? (float)args[7].ThrowIfNull().Number : 1f;
+
+ Vector2 p1 = new(x1, y1);
+ Vector2 p2 = new(x2, y2);
+ Vector2 p3 = new(x3, y3);
+
+ if (outline)
+ SpriteBatch.DrawPolygon(Vector2.Zero, [p1, p2, p3], drawColor, thickness);
+ else
+ FillTriangle(p1, p2, p3);
+
+ return Exp.Void.Return;
+ }
+
+ ///
+ /// Fills a triangle by drawing it as a series of horizontal scanlines.
+ /// MonoGame.Extended only exposes a solid-fill helper for rectangles, so the
+ /// triangle is rasterized one row at a time.
+ ///
+ private void FillTriangle(Vector2 p1, Vector2 p2, Vector2 p3)
+ {
+ int minY = (int)MathF.Floor(MathF.Min(p1.Y, MathF.Min(p2.Y, p3.Y)));
+ int maxY = (int)MathF.Ceiling(MathF.Max(p1.Y, MathF.Max(p2.Y, p3.Y)));
+
+ for (int y = minY; y <= maxY; y++)
+ {
+ // sample each row at its center, so rows the triangle barely covers are not filled
+ float rowCenter = y + 0.5f;
+ float left = float.MaxValue, right = float.MinValue;
+
+ ScanEdge(p1, p2, rowCenter, ref left, ref right);
+ ScanEdge(p2, p3, rowCenter, ref left, ref right);
+ ScanEdge(p3, p1, rowCenter, ref left, ref right);
+
+ if (left < right)
+ SpriteBatch.FillRectangle(left, y, right - left, 1f, drawColor);
+ }
+
+ // Widens [left, right] to cover where the given edge crosses the scanline at 'y'.
+ static void ScanEdge(Vector2 a, Vector2 b, float y, ref float left, ref float right)
+ {
+ if (a.Y == b.Y || y < MathF.Min(a.Y, b.Y) || y > MathF.Max(a.Y, b.Y))
+ return;
+
+ float x = a.X + (b.X - a.X) * (y - a.Y) / (b.Y - a.Y);
+ left = MathF.Min(left, x);
+ right = MathF.Max(right, x);
+ }
+ }
+
public (int x, int y) MousePositionInWindow
{
get