diff --git a/TombEditor/Command.cs b/TombEditor/Command.cs index 55457428f..76a09aed1 100644 --- a/TombEditor/Command.cs +++ b/TombEditor/Command.cs @@ -1152,6 +1152,13 @@ static CommandHandler() args.Editor.Action = new EditorActionPlace(false, (l, r) => new FlybyCameraInstance(args.Editor.SelectedObject)); }); + AddCommand("AddWayPoint", "Add waypoint", CommandType.Objects, delegate (CommandArgs args) + { + if (!EditorActions.VersionCheck(args.Editor.Level.IsTombEngine, "WayPoint")) + return; + args.Editor.Action = new EditorActionPlace(false, (l, r) => new WayPointInstance(args.Editor.SelectedObject)); + }); + AddCommand("AddSink", "Add sink", CommandType.Objects, delegate (CommandArgs args) { args.Editor.Action = new EditorActionPlace(false, (l, r) => new SinkInstance()); diff --git a/TombEditor/Controls/ContextMenus/MaterialObjectContextMenu.cs b/TombEditor/Controls/ContextMenus/MaterialObjectContextMenu.cs index 0063c126c..d9a31c4e6 100644 --- a/TombEditor/Controls/ContextMenus/MaterialObjectContextMenu.cs +++ b/TombEditor/Controls/ContextMenus/MaterialObjectContextMenu.cs @@ -9,7 +9,7 @@ class MaterialObjectContextMenu : BaseContextMenu public MaterialObjectContextMenu(Editor editor, IWin32Window owner, ObjectInstance targetObject) : base(editor, owner) { - if (targetObject is IHasScriptID) + if (targetObject is IHasScriptID && !(targetObject is WayPointInstance)) { if (_editor.Level.IsNG && targetObject == editor.SelectedObject) { @@ -137,7 +137,7 @@ public MaterialObjectContextMenu(Editor editor, IWin32Window owner, ObjectInstan })); } - if (targetObject is PositionAndScriptBasedObjectInstance && _editor.Level.Settings.GameVersion == TRVersion.Game.TombEngine) + if (targetObject is PositionAndScriptBasedObjectInstance && !(targetObject is WayPointInstance) && _editor.Level.Settings.GameVersion == TRVersion.Game.TombEngine) { Items.Add(new ToolStripMenuItem("Copy Lua name to clipboard", null, (o, e) => { diff --git a/TombEditor/Controls/ContextMenus/SectorContextMenu.cs b/TombEditor/Controls/ContextMenus/SectorContextMenu.cs index 652b98726..29b6f8ffa 100644 --- a/TombEditor/Controls/ContextMenus/SectorContextMenu.cs +++ b/TombEditor/Controls/ContextMenus/SectorContextMenu.cs @@ -49,6 +49,12 @@ public SectorContextMenu(Editor editor, IWin32Window owner, Room targetRoom, Vec EditorActions.PlaceObject(targetRoom, targetSector, new FlybyCameraInstance(editor.SelectedObject)); })); + if (editor.Level.IsTombEngine) + Items.Add(new ToolStripMenuItem("Add waypoint", Properties.Resources.objects_movie_projector_16, (o, e) => + { + EditorActions.PlaceObject(targetRoom, targetSector, new WayPointInstance(editor.SelectedObject)); + })); + Items.Add(new ToolStripMenuItem("Add sink", Properties.Resources.objects_tornado_16, (o, e) => { EditorActions.PlaceObject(targetRoom, targetSector, new SinkInstance()); diff --git a/TombEditor/Controls/ContextMenus/SelectedGeometryContextMenu.cs b/TombEditor/Controls/ContextMenus/SelectedGeometryContextMenu.cs index abd8da4a9..d6eb36da0 100644 --- a/TombEditor/Controls/ContextMenus/SelectedGeometryContextMenu.cs +++ b/TombEditor/Controls/ContextMenus/SelectedGeometryContextMenu.cs @@ -56,6 +56,12 @@ public SelectedGeometryContextMenu(Editor editor, IWin32Window owner, Room targe EditorActions.PlaceObject(targetRoom, targetSector, new FlybyCameraInstance(editor.SelectedObject)); })); + if (editor.Level.IsTombEngine) + Items.Add(new ToolStripMenuItem("Add waypoint", Properties.Resources.objects_movie_projector_16, (o, e) => + { + EditorActions.PlaceObject(targetRoom, targetSector, new WayPointInstance(editor.SelectedObject)); + })); + Items.Add(new ToolStripMenuItem("Add sink", Properties.Resources.objects_tornado_16, (o, e) => { EditorActions.PlaceObject(targetRoom, targetSector, new SinkInstance()); diff --git a/TombEditor/Controls/Panel3D/Panel3D.cs b/TombEditor/Controls/Panel3D/Panel3D.cs index 61eca6e9f..11d0f60dd 100644 --- a/TombEditor/Controls/Panel3D/Panel3D.cs +++ b/TombEditor/Controls/Panel3D/Panel3D.cs @@ -134,6 +134,7 @@ public bool DisablePickingForHiddenRooms private bool _drawHeightLine; private Buffer _objectHeightLineVertexBuffer; private Buffer _flybyPathVertexBuffer; + private Buffer _wayPointPathVertexBuffer; private Buffer _ghostBlockVertexBuffer; private Buffer _boxVertexBuffer; @@ -217,6 +218,7 @@ protected override void Dispose(bool disposing) _rasterizerWireframe?.Dispose(); _objectHeightLineVertexBuffer?.Dispose(); _flybyPathVertexBuffer?.Dispose(); + _wayPointPathVertexBuffer?.Dispose(); _gizmo?.Dispose(); _sphere?.Dispose(); _cone?.Dispose(); diff --git a/TombEditor/Controls/Panel3D/Panel3DDraw.cs b/TombEditor/Controls/Panel3D/Panel3DDraw.cs index a29d32b1d..765d03fc3 100644 --- a/TombEditor/Controls/Panel3D/Panel3DDraw.cs +++ b/TombEditor/Controls/Panel3D/Panel3DDraw.cs @@ -196,6 +196,19 @@ private void DrawFlybyPath(Effect effect) effect.CurrentTechnique.Passes[0].Apply(); _legacyDevice.Draw(PrimitiveType.TriangleList, _flybyPathVertexBuffer.ElementCount); } + + // Add the path of waypoints + if (_editor.SelectedObject is WayPointInstance waypoint && + AddWayPointPath(waypoint.BaseName)) + { + _legacyDevice.SetRasterizerState(_legacyDevice.RasterizerStates.CullNone); + _legacyDevice.SetVertexBuffer(_wayPointPathVertexBuffer); + _legacyDevice.SetVertexInputLayout(VertexInputLayout.FromBuffer(0, _wayPointPathVertexBuffer)); + effect.Parameters["ModelViewProjection"].SetValue(_viewProjection.ToSharpDX()); + effect.Parameters["Color"].SetValue(new Vector4(1.0f, 0.5f, 0.0f, 1.0f)); // Orange for waypoints + effect.CurrentTechnique.Passes[0].Apply(); + _legacyDevice.Draw(PrimitiveType.TriangleList, _wayPointPathVertexBuffer.ElementCount); + } } private void DrawSectorSplitHighlights(Effect effect) @@ -1138,6 +1151,55 @@ private void DrawPlaceholders(Effect effect, Room[] roomsWhoseObjectsToDraw, Lis DrawOrQueueServiceObject(instance, _littleCube, color, effect, sprites); } + if (group.Key == typeof(WayPointInstance)) + foreach (WayPointInstance instance in group) + { + _legacyDevice.SetRasterizerState(_legacyDevice.RasterizerStates.CullBack); + + var color = new Vector4(1.0f, 0.5f, 0.0f, 1.0f); // Orange color for waypoints + + if (_editor.SelectedObject is WayPointInstance selectedWaypoint && selectedWaypoint.Name == instance.Name) + { + int nameHash = Math.Abs(instance.Name?.GetHashCode() ?? 1); + if (nameHash == 0) nameHash = 1; + color = MathC.GetRandomColorByIndex(nameHash, 32, 0.7f); + } + + if (_highlightedObjects.Contains(instance)) + { + color = _editor.Configuration.UI_ColorScheme.ColorSelection; + _legacyDevice.SetRasterizerState(_rasterizerWireframe); + + if (_editor.SelectedObject == instance) + { + // Add text message with format: Name (Number) for multi-point, just Name for singular + string label = instance.IsSingularType() ? + $"{instance.BaseName} " : + $"{instance.BaseName} ({instance.Number}) "; + + string rotationInfo = GetObjectRotationString(instance.Room, instance); + if (!string.IsNullOrEmpty(rotationInfo)) + rotationInfo = "\n" + rotationInfo; + + textToDraw.Add(CreateTextTagForObject( + instance.RotationPositionMatrix * _viewProjection, + label + + GetObjectPositionString(instance.Room, instance) + rotationInfo + GetObjectTriggerString(instance))); + + // Add the line height of the object + AddObjectHeightLine(instance.Room, instance.Position); + } + } + + DrawOrQueueServiceObject(instance, _littleCube, color, effect, sprites); + + // Draw shape for shape types (Circle, Ellipse, Square, Rectangle) + if (instance.RequiresRadius()) + { + DrawWayPointShape(instance, color); + } + } + if (group.Key == typeof(MemoInstance)) foreach (MemoInstance instance in group) { @@ -1424,6 +1486,150 @@ private void DrawOrQueueServiceObject(ISpatial instance, GeometricPrimitive prim _legacyDevice.DrawIndexed(PrimitiveType.TriangleList, primitive.IndexBuffer.ElementCount); } + private void DrawWayPointShape(WayPointInstance instance, Vector4 color) + { + // Get world position + Vector3 position = instance.Position + instance.Room.WorldPos; + + // Create transformation matrix for the shape orientation + // Apply rotations in order: X, Y, Z (Roll) + Matrix4x4 rotation = Matrix4x4.CreateRotationX(instance.RotationX * (float)Math.PI / 180.0f) * + Matrix4x4.CreateRotationY(instance.RotationY * (float)Math.PI / 180.0f) * + Matrix4x4.CreateRotationZ(instance.Roll * (float)Math.PI / 180.0f); + + // Number of segments for circles/ellipses + int segments = 32; + var points = new List(); + + switch (instance.Type) + { + case WayPointType.Circle: + // Draw circle with Radius1 + for (int i = 0; i <= segments; i++) + { + float angle = (i / (float)segments) * 2.0f * (float)Math.PI; + float x = (float)Math.Cos(angle) * instance.Radius1; + float z = (float)Math.Sin(angle) * instance.Radius1; + Vector3 point = Vector3.Transform(new Vector3(x, 0, z), rotation) + position; + points.Add(point); + } + break; + + case WayPointType.Ellipse: + // Draw ellipse with Radius1 and Radius2 + for (int i = 0; i <= segments; i++) + { + float angle = (i / (float)segments) * 2.0f * (float)Math.PI; + float x = (float)Math.Cos(angle) * instance.Radius1; + float z = (float)Math.Sin(angle) * instance.Radius2; + Vector3 point = Vector3.Transform(new Vector3(x, 0, z), rotation) + position; + points.Add(point); + } + break; + + case WayPointType.Square: + // Draw square with Radius1 + { + float r = instance.Radius1; + Vector3[] corners = new Vector3[] + { + new Vector3(-r, 0, -r), + new Vector3(r, 0, -r), + new Vector3(r, 0, r), + new Vector3(-r, 0, r), + new Vector3(-r, 0, -r) // Close the loop + }; + foreach (var corner in corners) + { + points.Add(Vector3.Transform(corner, rotation) + position); + } + } + break; + + case WayPointType.Rectangle: + // Draw rectangle with Radius1 and Radius2 + { + float r1 = instance.Radius1; + float r2 = instance.Radius2; + Vector3[] corners = new Vector3[] + { + new Vector3(-r1, 0, -r2), + new Vector3(r1, 0, -r2), + new Vector3(r1, 0, r2), + new Vector3(-r1, 0, r2), + new Vector3(-r1, 0, -r2) // Close the loop + }; + foreach (var corner in corners) + { + points.Add(Vector3.Transform(corner, rotation) + position); + } + } + break; + } + + // Convert points to line vertices using the same approach as flyby paths + if (points.Count > 1) + { + var vertices = new List(); + float th = 16.0f; // Line thickness (increased for better visibility) + + for (int i = 0; i < points.Count - 1; i++) + { + var linePoints = new List() + { + new Vector3[] + { + points[i], + new Vector3(points[i].X + th, points[i].Y + th, points[i].Z + th), + new Vector3(points[i].X - th, points[i].Y + th, points[i].Z + th) + }, + new Vector3[] + { + points[i + 1], + new Vector3(points[i + 1].X + th, points[i + 1].Y + th, points[i + 1].Z + th), + new Vector3(points[i + 1].X - th, points[i + 1].Y + th, points[i + 1].Z + th) + } + }; + + // Add triangles to form the line segment (both sides for double-sided rendering) + for (int k = 0; k < _flybyPathIndices.Count; k++) + { + var v = new SolidVertex(); + v.Position = linePoints[_flybyPathIndices[k].Y][_flybyPathIndices[k].X]; + v.Color = color; + vertices.Add(v); + } + + // Add reversed triangles for the back side + for (int k = _flybyPathIndices.Count - 1; k >= 0; k--) + { + var v = new SolidVertex(); + v.Position = linePoints[_flybyPathIndices[k].Y][_flybyPathIndices[k].X]; + v.Color = color; + vertices.Add(v); + } + } + + // Create temporary vertex buffer for this shape + if (vertices.Count > 0) + { + var shapeBuffer = SharpDX.Toolkit.Graphics.Buffer.Vertex.New(_legacyDevice, + vertices.ToArray(), SharpDX.Direct3D11.ResourceUsage.Dynamic); + + // Draw the shape + var effect = DeviceManager.DefaultDeviceManager.___LegacyEffects["Solid"]; + effect.Parameters["ModelViewProjection"].SetValue(_viewProjection.ToSharpDX()); + effect.Parameters["Color"].SetValue(color); + effect.Techniques[0].Passes[0].Apply(); + _legacyDevice.SetVertexBuffer(shapeBuffer); + _legacyDevice.SetVertexInputLayout(VertexInputLayout.FromBuffer(0, shapeBuffer)); + _legacyDevice.Draw(PrimitiveType.TriangleList, vertices.Count); + + shapeBuffer.Dispose(); + } + } + } + private void DrawCardinalDirections(List textToDraw) { string[] messages; diff --git a/TombEditor/Controls/Panel3D/Panel3DHelpers.cs b/TombEditor/Controls/Panel3D/Panel3DHelpers.cs index 4f61f9d47..b699dad9e 100644 --- a/TombEditor/Controls/Panel3D/Panel3DHelpers.cs +++ b/TombEditor/Controls/Panel3D/Panel3DHelpers.cs @@ -183,6 +183,102 @@ private bool AddFlybyPath(int sequence) return true; } + private bool AddWayPointPath(string name) + { + // Collect all waypoints + var wayPoints = new List(); + + foreach (var room in _editor.Level.ExistingRooms) + foreach (var instance in room.Objects.OfType()) + { + if (instance.BaseName == name) + wayPoints.Add(instance); + } + + // Filter to only multi-point types (Linear, Bezier) + wayPoints = wayPoints.Where(wp => wp.Type == WayPointType.Linear || wp.Type == WayPointType.Bezier).ToList(); + + // Is it actually necessary to show the path? + if (wayPoints.Count < 2) + return false; + + // Sort waypoints + wayPoints.Sort((x, y) => x.Number.CompareTo(y.Number)); + + // Initialize variables for vertex buffer preparation + var vertices = new List(); + // Use name hash for color selection (ensure positive and non-zero) + int nameHash = Math.Abs(name?.GetHashCode() ?? 1); + if (nameHash == 0) nameHash = 1; + var startColor = MathC.GetRandomColorByIndex(nameHash, 32, 0.7f); + var endColor = MathC.GetRandomColorByIndex(nameHash, 32, 0.3f); + + float th = _flybyPathThickness; + + // Determine the Type to use for the sequence (use the most common one) + var typeCounts = wayPoints.GroupBy(wp => wp.Type) + .OrderByDescending(g => g.Count()) + .ToList(); + var wpType = typeCounts.FirstOrDefault()?.Key ?? WayPointType.Linear; + + // Process waypoints to calculate paths + var pointList = new List(); + for (int i = 0; i < wayPoints.Count; i++) + { + var wp = wayPoints[i]; + pointList.Add(wp.Position + wp.Room.WorldPos); + } + + // Calculate the spline path based on Type + List interpolatedPoints; + if (wpType == WayPointType.Linear) + { + // For linear, just use the points as-is + interpolatedPoints = pointList; + } + else + { + // For Curved and Bezier, use spline interpolation + interpolatedPoints = Spline.Calculate(pointList, pointList.Count * _flybyPathSmoothness); + } + + // Add vertices for the path + for (int j = 0; j < interpolatedPoints.Count - 1; j++) + { + var color = Vector4.Lerp(startColor, endColor, j / (float)interpolatedPoints.Count); + var points = new List() + { + new Vector3[] + { + interpolatedPoints[j], + new Vector3(interpolatedPoints[j].X + th, interpolatedPoints[j].Y + th, interpolatedPoints[j].Z + th), + new Vector3(interpolatedPoints[j].X - th, interpolatedPoints[j].Y + th, interpolatedPoints[j].Z + th) + }, + new Vector3[] + { + interpolatedPoints[j + 1], + new Vector3(interpolatedPoints[j + 1].X + th, interpolatedPoints[j + 1].Y + th, interpolatedPoints[j + 1].Z + th), + new Vector3(interpolatedPoints[j + 1].X - th, interpolatedPoints[j + 1].Y + th, interpolatedPoints[j + 1].Z + th) + } + }; + + for (int k = 0; k < _flybyPathIndices.Count; k++) + { + var v = new SolidVertex(); + v.Position = points[_flybyPathIndices[k].Y][_flybyPathIndices[k].X]; + v.Color = color; + vertices.Add(v); + } + } + + // Prepare the Vertex Buffer + _wayPointPathVertexBuffer?.Dispose(); + _wayPointPathVertexBuffer = null; + _wayPointPathVertexBuffer = SharpDX.Toolkit.Graphics.Buffer.Vertex.New(_legacyDevice, vertices.ToArray(), SharpDX.Direct3D11.ResourceUsage.Dynamic); + + return true; + } + private class Comparer : IComparer, IComparer, IComparer { public int Compare(StaticInstance x, StaticInstance y) diff --git a/TombEditor/EditorActions.cs b/TombEditor/EditorActions.cs index b3ea287e3..7c54d769f 100644 --- a/TombEditor/EditorActions.cs +++ b/TombEditor/EditorActions.cs @@ -1129,6 +1129,13 @@ public static void EditObject(ObjectInstance instance, IWin32Window owner) return; _editor.ObjectChange(instance, ObjectChangeType.Change); } + else if (instance is WayPointInstance) + { + using (var formWayPoint = GetObjectSetupWindow((WayPointInstance)instance)) + if (formWayPoint.ShowDialog(owner) != DialogResult.OK) + return; + _editor.ObjectChange(instance, ObjectChangeType.Change); + } else if (instance is CameraInstance) { using (var formCamera = GetObjectSetupWindow((CameraInstance)instance)) diff --git a/TombEditor/EditorCommands.cs b/TombEditor/EditorCommands.cs index a2c624dc2..8635e6ac1 100644 --- a/TombEditor/EditorCommands.cs +++ b/TombEditor/EditorCommands.cs @@ -777,6 +777,13 @@ private void AddCommands() _editor.Action = new EditorActionPlace(false, (l, r) => new FlybyCameraInstance()); }); + AddCommand("AddWayPoint", "Add waypoint", CommandType.Objects, delegate () + { + if (!VersionCheck(_editor.Level.IsTombEngine, "WayPoint")) + return; + _editor.Action = new EditorActionPlace(false, (l, r) => new WayPointInstance()); + }); + AddCommand("AddSink", "Add sink", CommandType.Objects, delegate () { _editor.Action = new EditorActionPlace(false, (l, r) => new SinkInstance()); diff --git a/TombEditor/Forms/FormWayPoint.Designer.cs b/TombEditor/Forms/FormWayPoint.Designer.cs new file mode 100644 index 000000000..df187b449 --- /dev/null +++ b/TombEditor/Forms/FormWayPoint.Designer.cs @@ -0,0 +1,342 @@ +using DarkUI.Controls; + +namespace TombEditor.Forms +{ + partial class FormWayPoint + { + /// + /// Required designer variable. + /// + private System.ComponentModel.IContainer components = null; + + /// + /// Clean up any resources being used. + /// + /// true if managed resources should be disposed; otherwise, false. + protected override void Dispose(bool disposing) + { + if (disposing && (components != null)) + { + components.Dispose(); + } + base.Dispose(disposing); + } + + #region Windows Form Designer generated code + + /// + /// Required method for Designer support - do not modify + /// the contents of this method with the code editor. + /// + private void InitializeComponent() + { + butCancel = new DarkButton(); + butOK = new DarkButton(); + lblName = new DarkLabel(); + lblSequence = new DarkLabel(); + lblNumber = new DarkLabel(); + lblType = new DarkLabel(); + lblDimension1 = new DarkLabel(); + lblDimension2 = new DarkLabel(); + lblRotationX = new DarkLabel(); + lblRotationY = new DarkLabel(); + lblRoll = new DarkLabel(); + txtName = new DarkTextBox(); + numSequence = new DarkNumericUpDown(); + numNumber = new DarkNumericUpDown(); + cmbType = new DarkComboBox(); + numDimension1 = new DarkNumericUpDown(); + numDimension2 = new DarkNumericUpDown(); + numRotationX = new DarkNumericUpDown(); + numRotationY = new DarkNumericUpDown(); + numRoll = new DarkNumericUpDown(); + ((System.ComponentModel.ISupportInitialize)numSequence).BeginInit(); + ((System.ComponentModel.ISupportInitialize)numNumber).BeginInit(); + ((System.ComponentModel.ISupportInitialize)numDimension1).BeginInit(); + ((System.ComponentModel.ISupportInitialize)numDimension2).BeginInit(); + ((System.ComponentModel.ISupportInitialize)numRotationX).BeginInit(); + ((System.ComponentModel.ISupportInitialize)numRotationY).BeginInit(); + ((System.ComponentModel.ISupportInitialize)numRoll).BeginInit(); + SuspendLayout(); + // + // butCancel + // + butCancel.Anchor = System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Right; + butCancel.Checked = false; + butCancel.DialogResult = System.Windows.Forms.DialogResult.Cancel; + butCancel.Location = new System.Drawing.Point(239, 248); + butCancel.Name = "butCancel"; + butCancel.Size = new System.Drawing.Size(80, 23); + butCancel.TabIndex = 19; + butCancel.Text = "Cancel"; + butCancel.TextImageRelation = System.Windows.Forms.TextImageRelation.ImageBeforeText; + butCancel.Click += butCancel_Click; + // + // butOK + // + butOK.Anchor = System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Right; + butOK.Checked = false; + butOK.Location = new System.Drawing.Point(153, 248); + butOK.Name = "butOK"; + butOK.Size = new System.Drawing.Size(80, 23); + butOK.TabIndex = 18; + butOK.Text = "OK"; + butOK.TextImageRelation = System.Windows.Forms.TextImageRelation.ImageBeforeText; + butOK.Click += butOK_Click; + // + // lblName + // + lblName.AutoSize = true; + lblName.ForeColor = System.Drawing.Color.FromArgb(220, 220, 220); + lblName.Location = new System.Drawing.Point(12, 15); + lblName.Name = "lblName"; + lblName.Size = new System.Drawing.Size(39, 13); + lblName.TabIndex = 0; + lblName.Text = "Name:"; + // + // lblSequence + // + lblSequence.AutoSize = true; + lblSequence.ForeColor = System.Drawing.Color.FromArgb(220, 220, 220); + lblSequence.Location = new System.Drawing.Point(12, 41); + lblSequence.Name = "lblSequence"; + lblSequence.Size = new System.Drawing.Size(61, 13); + lblSequence.TabIndex = 2; + lblSequence.Text = "Sequence:"; + // + // lblNumber + // + lblNumber.AutoSize = true; + lblNumber.ForeColor = System.Drawing.Color.FromArgb(220, 220, 220); + lblNumber.Location = new System.Drawing.Point(12, 67); + lblNumber.Name = "lblNumber"; + lblNumber.Size = new System.Drawing.Size(51, 13); + lblNumber.TabIndex = 4; + lblNumber.Text = "Number:"; + // + // lblType + // + lblType.AutoSize = true; + lblType.ForeColor = System.Drawing.Color.FromArgb(220, 220, 220); + lblType.Location = new System.Drawing.Point(12, 93); + lblType.Name = "lblType"; + lblType.Size = new System.Drawing.Size(32, 13); + lblType.TabIndex = 6; + lblType.Text = "Type:"; + // + // lblDimension1 + // + lblDimension1.AutoSize = true; + lblDimension1.ForeColor = System.Drawing.Color.FromArgb(220, 220, 220); + lblDimension1.Location = new System.Drawing.Point(12, 119); + lblDimension1.Name = "lblDimension1"; + lblDimension1.Size = new System.Drawing.Size(74, 13); + lblDimension1.TabIndex = 8; + lblDimension1.Text = "Dimension 1:"; + // + // lblDimension2 + // + lblDimension2.AutoSize = true; + lblDimension2.ForeColor = System.Drawing.Color.FromArgb(220, 220, 220); + lblDimension2.Location = new System.Drawing.Point(12, 145); + lblDimension2.Name = "lblDimension2"; + lblDimension2.Size = new System.Drawing.Size(74, 13); + lblDimension2.TabIndex = 10; + lblDimension2.Text = "Dimension 2:"; + // + // lblRotationX + // + lblRotationX.AutoSize = true; + lblRotationX.ForeColor = System.Drawing.Color.FromArgb(220, 220, 220); + lblRotationX.Location = new System.Drawing.Point(12, 171); + lblRotationX.Name = "lblRotationX"; + lblRotationX.Size = new System.Drawing.Size(64, 13); + lblRotationX.TabIndex = 12; + lblRotationX.Text = "Rotation X:"; + // + // lblRotationY + // + lblRotationY.AutoSize = true; + lblRotationY.ForeColor = System.Drawing.Color.FromArgb(220, 220, 220); + lblRotationY.Location = new System.Drawing.Point(12, 197); + lblRotationY.Name = "lblRotationY"; + lblRotationY.Size = new System.Drawing.Size(63, 13); + lblRotationY.TabIndex = 14; + lblRotationY.Text = "Rotation Y:"; + // + // lblRoll + // + lblRoll.AutoSize = true; + lblRoll.ForeColor = System.Drawing.Color.FromArgb(220, 220, 220); + lblRoll.Location = new System.Drawing.Point(12, 223); + lblRoll.Name = "lblRoll"; + lblRoll.Size = new System.Drawing.Size(64, 13); + lblRoll.TabIndex = 16; + lblRoll.Text = "Rotation Z:"; + // + // txtName + // + txtName.Location = new System.Drawing.Point(92, 12); + txtName.Name = "txtName"; + txtName.Size = new System.Drawing.Size(227, 22); + txtName.TabIndex = 1; + // + // numSequence + // + numSequence.IncrementAlternate = new decimal(new int[] { 10, 0, 0, 65536 }); + numSequence.Location = new System.Drawing.Point(92, 38); + numSequence.LoopValues = false; + numSequence.Maximum = new decimal(new int[] { 65535, 0, 0, 0 }); + numSequence.Name = "numSequence"; + numSequence.Size = new System.Drawing.Size(227, 22); + numSequence.TabIndex = 3; + // + // numNumber + // + numNumber.IncrementAlternate = new decimal(new int[] { 10, 0, 0, 65536 }); + numNumber.Location = new System.Drawing.Point(92, 64); + numNumber.LoopValues = false; + numNumber.Maximum = new decimal(new int[] { 65535, 0, 0, 0 }); + numNumber.Name = "numNumber"; + numNumber.Size = new System.Drawing.Size(227, 22); + numNumber.TabIndex = 5; + // + // cmbType + // + cmbType.FormattingEnabled = true; + cmbType.Items.AddRange(new object[] { "Point", "Circle", "Ellipse", "Square", "Rectangle", "Linear", "Bezier" }); + cmbType.Location = new System.Drawing.Point(92, 90); + cmbType.Name = "cmbType"; + cmbType.Size = new System.Drawing.Size(227, 23); + cmbType.TabIndex = 7; + cmbType.SelectedIndexChanged += cmbType_SelectedIndexChanged; + // + // numDimension1 + // + numDimension1.DecimalPlaces = 2; + numDimension1.IncrementAlternate = new decimal(new int[] { 100, 0, 0, 0 }); + numDimension1.Location = new System.Drawing.Point(92, 116); + numDimension1.LoopValues = false; + numDimension1.Maximum = new decimal(new int[] { 100000, 0, 0, 0 }); + numDimension1.Name = "numDimension1"; + numDimension1.Size = new System.Drawing.Size(227, 22); + numDimension1.TabIndex = 9; + numDimension1.Value = new decimal(new int[] { 1024, 0, 0, 0 }); + // + // numDimension2 + // + numDimension2.DecimalPlaces = 2; + numDimension2.IncrementAlternate = new decimal(new int[] { 100, 0, 0, 0 }); + numDimension2.Location = new System.Drawing.Point(92, 142); + numDimension2.LoopValues = false; + numDimension2.Maximum = new decimal(new int[] { 100000, 0, 0, 0 }); + numDimension2.Name = "numDimension2"; + numDimension2.Size = new System.Drawing.Size(227, 22); + numDimension2.TabIndex = 11; + numDimension2.Value = new decimal(new int[] { 1024, 0, 0, 0 }); + // + // numRotationX + // + numRotationX.DecimalPlaces = 2; + numRotationX.IncrementAlternate = new decimal(new int[] { 10, 0, 0, 65536 }); + numRotationX.Location = new System.Drawing.Point(92, 168); + numRotationX.LoopValues = false; + numRotationX.Maximum = new decimal(new int[] { 90, 0, 0, 0 }); + numRotationX.Minimum = new decimal(new int[] { 90, 0, 0, int.MinValue }); + numRotationX.Name = "numRotationX"; + numRotationX.Size = new System.Drawing.Size(227, 22); + numRotationX.TabIndex = 13; + // + // numRotationY + // + numRotationY.DecimalPlaces = 2; + numRotationY.IncrementAlternate = new decimal(new int[] { 10, 0, 0, 65536 }); + numRotationY.Location = new System.Drawing.Point(92, 194); + numRotationY.LoopValues = false; + numRotationY.Maximum = new decimal(new int[] { 360, 0, 0, 0 }); + numRotationY.Name = "numRotationY"; + numRotationY.Size = new System.Drawing.Size(227, 22); + numRotationY.TabIndex = 15; + // + // numRoll + // + numRoll.DecimalPlaces = 2; + numRoll.IncrementAlternate = new decimal(new int[] { 10, 0, 0, 65536 }); + numRoll.Location = new System.Drawing.Point(92, 220); + numRoll.LoopValues = false; + numRoll.Maximum = new decimal(new int[] { 360, 0, 0, 0 }); + numRoll.Name = "numRoll"; + numRoll.Size = new System.Drawing.Size(227, 22); + numRoll.TabIndex = 17; + // + // FormWayPoint + // + AcceptButton = butOK; + AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F); + AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; + CancelButton = butCancel; + ClientSize = new System.Drawing.Size(331, 281); + Controls.Add(numRoll); + Controls.Add(numRotationY); + Controls.Add(numRotationX); + Controls.Add(numDimension2); + Controls.Add(numDimension1); + Controls.Add(cmbType); + Controls.Add(numNumber); + Controls.Add(numSequence); + Controls.Add(txtName); + Controls.Add(lblRoll); + Controls.Add(lblRotationY); + Controls.Add(lblRotationX); + Controls.Add(lblDimension2); + Controls.Add(lblDimension1); + Controls.Add(lblType); + Controls.Add(lblNumber); + Controls.Add(lblSequence); + Controls.Add(lblName); + Controls.Add(butCancel); + Controls.Add(butOK); + FormBorderStyle = System.Windows.Forms.FormBorderStyle.FixedDialog; + MaximizeBox = false; + MinimizeBox = false; + Name = "FormWayPoint"; + ShowIcon = false; + ShowInTaskbar = false; + StartPosition = System.Windows.Forms.FormStartPosition.CenterParent; + Text = "WayPoint"; + Load += FormWayPoint_Load; + ((System.ComponentModel.ISupportInitialize)numSequence).EndInit(); + ((System.ComponentModel.ISupportInitialize)numNumber).EndInit(); + ((System.ComponentModel.ISupportInitialize)numDimension1).EndInit(); + ((System.ComponentModel.ISupportInitialize)numDimension2).EndInit(); + ((System.ComponentModel.ISupportInitialize)numRotationX).EndInit(); + ((System.ComponentModel.ISupportInitialize)numRotationY).EndInit(); + ((System.ComponentModel.ISupportInitialize)numRoll).EndInit(); + ResumeLayout(false); + PerformLayout(); + } + + #endregion + + private DarkButton butOK; + private DarkButton butCancel; + private DarkLabel lblName; + private DarkLabel lblSequence; + private DarkLabel lblNumber; + private DarkLabel lblType; + private DarkLabel lblDimension1; + private DarkLabel lblDimension2; + private DarkLabel lblRotationX; + private DarkLabel lblRotationY; + private DarkLabel lblRoll; + private DarkTextBox txtName; + private DarkNumericUpDown numSequence; + private DarkNumericUpDown numNumber; + private DarkComboBox cmbType; + private DarkNumericUpDown numDimension1; + private DarkNumericUpDown numDimension2; + private DarkNumericUpDown numRotationX; + private DarkNumericUpDown numRotationY; + private DarkNumericUpDown numRoll; + } +} diff --git a/TombEditor/Forms/FormWayPoint.cs b/TombEditor/Forms/FormWayPoint.cs new file mode 100644 index 000000000..992329a77 --- /dev/null +++ b/TombEditor/Forms/FormWayPoint.cs @@ -0,0 +1,268 @@ +using System; +using System.Linq; +using System.Windows.Forms; +using DarkUI.Forms; +using TombLib.LevelData; + +namespace TombEditor.Forms +{ + public partial class FormWayPoint : DarkForm + { + public bool IsNew { get; set; } + + private readonly WayPointInstance _wayPoint; + private readonly Editor _editor; + + public FormWayPoint(WayPointInstance wayPoint) + { + _wayPoint = wayPoint; + _editor = Editor.Instance; + + InitializeComponent(); + } + + private void butCancel_Click(object sender, EventArgs e) + { + DialogResult = DialogResult.Cancel; + Close(); + } + + private void FormWayPoint_Load(object sender, EventArgs e) + { + // Extract base name from full name (remove _number suffix if present) + string baseName = _wayPoint.Name; + if (!_wayPoint.IsSingularType()) + { + int lastUnderscore = baseName.LastIndexOf('_'); + if (lastUnderscore >= 0) + { + string suffix = baseName.Substring(lastUnderscore + 1); + if (ushort.TryParse(suffix, out _)) + { + baseName = baseName.Substring(0, lastUnderscore); + } + } + } + + txtName.Text = baseName; + numSequence.Value = _wayPoint.Sequence; + numNumber.Value = _wayPoint.Number; + cmbType.SelectedIndex = (int)_wayPoint.Type; + numDimension1.Value = (decimal)_wayPoint.Radius1; + numDimension2.Value = (decimal)_wayPoint.Radius2; + numRotationX.Value = (decimal)_wayPoint.RotationX; + numRotationY.Value = (decimal)_wayPoint.RotationY; + numRoll.Value = (decimal)_wayPoint.Roll; + + UpdateFieldVisibility(); + } + + private void cmbType_SelectedIndexChanged(object sender, EventArgs e) + { + WayPointType newType = (WayPointType)cmbType.SelectedIndex; + WayPointType oldType = _wayPoint.Type; + + // If type changed to singular, reset number to 0 + bool newIsSingular = newType == WayPointType.Point || + newType == WayPointType.Circle || + newType == WayPointType.Ellipse || + newType == WayPointType.Square || + newType == WayPointType.Rectangle; + + bool oldIsSingular = oldType == WayPointType.Point || + oldType == WayPointType.Circle || + oldType == WayPointType.Ellipse || + oldType == WayPointType.Square || + oldType == WayPointType.Rectangle; + + // Reset number to 0 when changing to singular type from multi-point type + if (newIsSingular && !oldIsSingular) + { + numNumber.Value = 0; + } + + UpdateFieldVisibility(); + } + + private void UpdateFieldVisibility() + { + WayPointType type = (WayPointType)cmbType.SelectedIndex; + + // Check if this is a singular type + bool isSingular = type == WayPointType.Point || + type == WayPointType.Circle || + type == WayPointType.Ellipse || + type == WayPointType.Square || + type == WayPointType.Rectangle; + + // Number field only for multi-point types - disable instead of hide + numNumber.Enabled = !isSingular; + + // Dimension fields only for shape types - disable instead of hide + bool requiresDimension = type == WayPointType.Circle || + type == WayPointType.Ellipse || + type == WayPointType.Square || + type == WayPointType.Rectangle; + + numDimension1.Enabled = requiresDimension; + + // Dimension2 only for ellipse and rectangle - disable instead of hide + bool requiresTwoDimensions = type == WayPointType.Ellipse || + type == WayPointType.Rectangle; + + numDimension2.Enabled = requiresTwoDimensions; + } + + private void butOK_Click(object sender, EventArgs e) + { + // Validate name is not empty + string newName = txtName.Text.Trim(); + if (string.IsNullOrEmpty(newName)) + { + DarkMessageBox.Show(this, "Waypoint name cannot be empty.", "Validation Error", + MessageBoxButtons.OK, MessageBoxIcon.Error); + return; + } + + var oldType = _wayPoint.Type; + var newType = (WayPointType)cmbType.SelectedIndex; + + // Extract old base name for comparison + string oldBaseName = _wayPoint.Name; + if (!_wayPoint.IsSingularType()) + { + int lastUnderscore = oldBaseName.LastIndexOf('_'); + if (lastUnderscore >= 0) + { + string suffix = oldBaseName.Substring(lastUnderscore + 1); + if (ushort.TryParse(suffix, out _)) + { + oldBaseName = oldBaseName.Substring(0, lastUnderscore); + } + } + } + + // Check for changes + bool nameChanged = oldBaseName != newName; + ushort currentNumber = (ushort)numNumber.Value; + bool numberChanged = _wayPoint.Number != currentNumber; + ushort currentSequence = (ushort)numSequence.Value; + bool sequenceChanged = _wayPoint.Sequence != currentSequence; + + // Validation rules: + // 1. Same name + same sequence can only exist if numbers are different + // 2. Same name + different sequence is NOT allowed + // 3. Different name + same sequence is NOT allowed + // 4. Same sequence + same name + same number is NOT allowed + // Always validate if any of these changed (removed the condition check) + if (_editor?.Level != null) + { + foreach (var room in _editor.Level.ExistingRooms) + { + foreach (var obj in room.Objects.OfType()) + { + if (obj == _wayPoint) + continue; + + // Rule 1 & 4: Check for same name + same sequence + if (obj.BaseName == newName && obj.Sequence == currentSequence) + { + // Only allowed if numbers are different + if (obj.Number == currentNumber) + { + DarkMessageBox.Show(this, + $"A waypoint with name '{newName}', sequence {currentSequence}, and number {currentNumber} already exists.", + "Validation Error", + MessageBoxButtons.OK, MessageBoxIcon.Error); + return; + } + // Different numbers - this is allowed + } + // Rule 2: Check for same name + different sequence + else if (obj.BaseName == newName && obj.Sequence != currentSequence) + { + DarkMessageBox.Show(this, + $"A waypoint with name '{newName}' already exists with a different sequence ({obj.Sequence}). The same name cannot be used with different sequence numbers.", + "Validation Error", + MessageBoxButtons.OK, MessageBoxIcon.Error); + return; + } + // Rule 3: Check for different name + same sequence + else if (obj.BaseName != newName && obj.Sequence == currentSequence) + { + DarkMessageBox.Show(this, + $"A waypoint with sequence {currentSequence} already exists with a different name ('{obj.BaseName}'). The same sequence number cannot be used with different names.", + "Validation Error", + MessageBoxButtons.OK, MessageBoxIcon.Error); + return; + } + } + } + } + + // Batch type update: if type changed, update all waypoints with the same name/sequence pair + if (oldType != newType && _editor?.Level != null) + { + var oldSequence = _wayPoint.Sequence; + foreach (var room in _editor.Level.ExistingRooms) + { + foreach (var obj in room.Objects.OfType()) + { + // Update all waypoints that share the same base name (which implies same sequence) + if (obj != _wayPoint && obj.BaseName == oldBaseName) + { + obj.Type = newType; + } + } + } + } + + // Generate the new full name that will be used + string fullNewName = newName; + ushort newNumber = (ushort)numNumber.Value; + + bool newIsSingular = newType == WayPointType.Point || + newType == WayPointType.Circle || + newType == WayPointType.Ellipse || + newType == WayPointType.Square || + newType == WayPointType.Rectangle; + + if (!newIsSingular) + { + fullNewName = newName + "_" + newNumber; + } + + // Update waypoint properties + _wayPoint.Name = newName; + _wayPoint.Sequence = (ushort)numSequence.Value; + _wayPoint.Number = newNumber; + _wayPoint.Type = newType; + _wayPoint.Radius1 = (float)numDimension1.Value; + _wayPoint.Radius2 = (float)numDimension2.Value; + _wayPoint.RotationX = (float)numRotationX.Value; + _wayPoint.RotationY = (float)numRotationY.Value; + _wayPoint.Roll = (float)numRoll.Value; + + // Batch type update: if type changed, update all waypoints with either: + // 1. Same original base name OR + // 2. Same sequence number + if (oldType != newType && _editor?.Level != null) + { + var oldSequence = _wayPoint.Sequence; + foreach (var room in _editor.Level.ExistingRooms) + { + foreach (var obj in room.Objects.OfType()) + { + if (obj != _wayPoint && (obj.BaseName == oldBaseName || obj.Sequence == oldSequence)) + { + obj.Type = newType; + } + } + } + } + + DialogResult = DialogResult.OK; + Close(); + } + } +} diff --git a/TombEditor/Forms/FormWayPoint.resx b/TombEditor/Forms/FormWayPoint.resx new file mode 100644 index 000000000..8b2ff64a1 --- /dev/null +++ b/TombEditor/Forms/FormWayPoint.resx @@ -0,0 +1,120 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + \ No newline at end of file diff --git a/TombEditor/Properties/Resources.Designer.cs b/TombEditor/Properties/Resources.Designer.cs index f53d05977..3a7de26cd 100644 --- a/TombEditor/Properties/Resources.Designer.cs +++ b/TombEditor/Properties/Resources.Designer.cs @@ -860,6 +860,16 @@ internal static System.Drawing.Bitmap objects_volume_sphere_16 { } } + /// + /// Looks up a localized resource of type System.Drawing.Bitmap. + /// + internal static System.Drawing.Bitmap objects_WayPoint_16 { + get { + object obj = ResourceManager.GetObject("objects_WayPoint_16", resourceCulture); + return ((System.Drawing.Bitmap)(obj)); + } + } + /// /// Looks up a localized resource of type System.Drawing.Bitmap. /// diff --git a/TombEditor/Properties/Resources.resx b/TombEditor/Properties/Resources.resx index f4fa1b3d4..59793f457 100644 --- a/TombEditor/Properties/Resources.resx +++ b/TombEditor/Properties/Resources.resx @@ -247,6 +247,9 @@ ..\Resources\icons_objects\objects_volume-sphere-16.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a + + ..\Resources\icons_objects\objects_WayPoint-16.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a + ..\Resources\icons_general\general_animcommand-16.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a diff --git a/TombEditor/Resources/icons_objects/objects_WayPoint-16.png b/TombEditor/Resources/icons_objects/objects_WayPoint-16.png new file mode 100644 index 000000000..081f4aaef Binary files /dev/null and b/TombEditor/Resources/icons_objects/objects_WayPoint-16.png differ diff --git a/TombEditor/ToolWindows/MainView.Designer.cs b/TombEditor/ToolWindows/MainView.Designer.cs index 05032bdee..ce0d33a23 100644 --- a/TombEditor/ToolWindows/MainView.Designer.cs +++ b/TombEditor/ToolWindows/MainView.Designer.cs @@ -61,6 +61,7 @@ private void InitializeComponent() butAddCamera = new System.Windows.Forms.ToolStripButton(); butAddSprite = new System.Windows.Forms.ToolStripButton(); butAddFlybyCamera = new System.Windows.Forms.ToolStripButton(); + butAddWayPoint = new System.Windows.Forms.ToolStripButton(); butAddSink = new System.Windows.Forms.ToolStripButton(); butAddSoundSource = new System.Windows.Forms.ToolStripButton(); butAddImportedGeometry = new System.Windows.Forms.ToolStripButton(); @@ -97,7 +98,7 @@ private void InitializeComponent() toolStrip.BackColor = System.Drawing.Color.FromArgb(60, 63, 65); toolStrip.ForeColor = System.Drawing.Color.FromArgb(220, 220, 220); toolStrip.GripStyle = System.Windows.Forms.ToolStripGripStyle.Hidden; - toolStrip.Items.AddRange(new System.Windows.Forms.ToolStripItem[] { but2D, but3D, butFaceEdit, butLightingMode, butUndo, butRedo, butCenterCamera, butDrawPortals, butDrawAllRooms, butDrawHorizon, butDrawRoomNames, butDrawCardinalDirections, butDrawExtraBlendingModes, butHideTransparentFaces, butBilinearFilter, butDrawWhiteLighting, butDrawStaticTint, butDrawIllegalSlopes, butDrawSlideDirections, butDisableGeometryPicking, butDisableHiddenRoomPicking, butDrawObjects, butFlipMap, butCopy, butPaste, butStamp, butOpacityNone, butOpacitySolidFaces, butOpacityTraversableFaces, butMirror, butAddCamera, butAddSprite, butAddFlybyCamera, butAddSink, butAddSoundSource, butAddImportedGeometry, butAddGhostBlock, butAddMemo, butCompileLevel, butCompileLevelAndPlay, butCompileAndPlayPreview, butAddBoxVolume, butAddSphereVolume, butTextureFloor, butTextureCeiling, butTextureWalls, butEditLevelSettings, butToggleFlyMode, butSearch, butSearchAndReplaceObjects }); + toolStrip.Items.AddRange(new System.Windows.Forms.ToolStripItem[] { but2D, but3D, butFaceEdit, butLightingMode, butUndo, butRedo, butCenterCamera, butDrawPortals, butDrawAllRooms, butDrawHorizon, butDrawRoomNames, butDrawCardinalDirections, butDrawExtraBlendingModes, butHideTransparentFaces, butBilinearFilter, butDrawWhiteLighting, butDrawStaticTint, butDrawIllegalSlopes, butDrawSlideDirections, butDisableGeometryPicking, butDisableHiddenRoomPicking, butDrawObjects, butFlipMap, butCopy, butPaste, butStamp, butOpacityNone, butOpacitySolidFaces, butOpacityTraversableFaces, butMirror, butAddCamera, butAddSprite, butAddFlybyCamera, butAddWayPoint, butAddSink, butAddSoundSource, butAddImportedGeometry, butAddGhostBlock, butAddMemo, butCompileLevel, butCompileLevelAndPlay, butCompileAndPlayPreview, butAddBoxVolume, butAddSphereVolume, butTextureFloor, butTextureCeiling, butTextureWalls, butEditLevelSettings, butToggleFlyMode, butSearch, butSearchAndReplaceObjects }); toolStrip.Location = new System.Drawing.Point(0, 0); toolStrip.Name = "toolStrip"; toolStrip.Padding = new System.Windows.Forms.Padding(6, 0, 1, 0); @@ -567,6 +568,17 @@ private void InitializeComponent() butAddFlybyCamera.Size = new System.Drawing.Size(23, 29); butAddFlybyCamera.Tag = "AddFlybyCamera"; // + // butAddWayPoint + // + butAddWayPoint.BackColor = System.Drawing.Color.FromArgb(60, 63, 65); + butAddWayPoint.DisplayStyle = System.Windows.Forms.ToolStripItemDisplayStyle.Image; + butAddWayPoint.ForeColor = System.Drawing.Color.FromArgb(220, 220, 220); + butAddWayPoint.Image = Properties.Resources.objects_WayPoint_16; + butAddWayPoint.ImageTransparentColor = System.Drawing.Color.Magenta; + butAddWayPoint.Name = "butAddWayPoint"; + butAddWayPoint.Size = new System.Drawing.Size(23, 29); + butAddWayPoint.Tag = "AddWayPoint"; + // // butAddSink // butAddSink.BackColor = System.Drawing.Color.FromArgb(60, 63, 65); @@ -902,6 +914,7 @@ private void InitializeComponent() private System.Windows.Forms.ToolStripButton butOpacityTraversableFaces; private System.Windows.Forms.ToolStripButton butAddCamera; private System.Windows.Forms.ToolStripButton butAddFlybyCamera; + private System.Windows.Forms.ToolStripButton butAddWayPoint; private System.Windows.Forms.ToolStripButton butAddSoundSource; private System.Windows.Forms.ToolStripButton butAddSink; private System.Windows.Forms.ToolStripButton butAddGhostBlock; diff --git a/TombEditor/ToolWindows/MainView.cs b/TombEditor/ToolWindows/MainView.cs index 711e19edc..eac3e6375 100644 --- a/TombEditor/ToolWindows/MainView.cs +++ b/TombEditor/ToolWindows/MainView.cs @@ -190,6 +190,7 @@ obj is Editor.LevelChangedEvent || { butAddBoxVolume.Enabled = _editor.Level.IsTombEngine; butAddSphereVolume.Enabled = _editor.Level.IsTombEngine; + butAddWayPoint.Enabled = _editor.Level.IsTombEngine; butDrawVolumes.Enabled = _editor.Level.IsTombEngine; // We may safely hide it because it's not customizable butAddSprite.Enabled = _editor.Level.Settings.GameVersion.Native() <= TRVersion.Game.TR2; diff --git a/TombLib/TombLib.Rendering/Rendering/ServiceObjectTextures.cs b/TombLib/TombLib.Rendering/Rendering/ServiceObjectTextures.cs index a439c0665..c8829746e 100644 --- a/TombLib/TombLib.Rendering/Rendering/ServiceObjectTextures.cs +++ b/TombLib/TombLib.Rendering/Rendering/ServiceObjectTextures.cs @@ -14,6 +14,7 @@ public enum ServiceObjectTexture { camera, flyby_camera, + waypoint, imp_geo, sink, sound_source, @@ -134,6 +135,7 @@ public static ServiceObjectTexture GetType(ISpatial instance) else if (instance is VolumeInstance) type = ServiceObjectTexture.volume; else if (instance is GhostBlockInstance) type = ServiceObjectTexture.ghost_block; else if (instance is FlybyCameraInstance) type = ServiceObjectTexture.flyby_camera; + else if (instance is WayPointInstance) type = ServiceObjectTexture.waypoint; else if (instance is SoundSourceInstance) type = ServiceObjectTexture.sound_source; else if (instance is ImportedGeometryInstance) type = ServiceObjectTexture.imp_geo; else type = ServiceObjectTexture.unknown; diff --git a/TombLib/TombLib.Rendering/Rendering/ServiceObjectTextures/waypoint.png b/TombLib/TombLib.Rendering/Rendering/ServiceObjectTextures/waypoint.png new file mode 100644 index 000000000..879947ae5 Binary files /dev/null and b/TombLib/TombLib.Rendering/Rendering/ServiceObjectTextures/waypoint.png differ diff --git a/TombLib/TombLib.Rendering/TombLib.Rendering.csproj b/TombLib/TombLib.Rendering/TombLib.Rendering.csproj index eaaed83bd..d3631a1a8 100644 --- a/TombLib/TombLib.Rendering/TombLib.Rendering.csproj +++ b/TombLib/TombLib.Rendering/TombLib.Rendering.csproj @@ -151,6 +151,7 @@ Always + diff --git a/TombLib/TombLib.Test/WayPointInstanceTests.cs b/TombLib/TombLib.Test/WayPointInstanceTests.cs new file mode 100644 index 000000000..7f19491db --- /dev/null +++ b/TombLib/TombLib.Test/WayPointInstanceTests.cs @@ -0,0 +1,173 @@ +using TombLib.LevelData; + +namespace TombLib.Test +{ + [TestClass] + public class WayPointInstanceTests + { + [TestMethod] + public void WayPoint_DefaultType() + { + // Arrange & Act + var wayPoint = new WayPointInstance(); + + // Assert + Assert.AreEqual(WayPointType.Point, wayPoint.Type, "Default Type should be Point"); + } + + [TestMethod] + public void WayPoint_TypeCanBeSet() + { + // Arrange + var wayPoint = new WayPointInstance(); + + // Act + wayPoint.Type = WayPointType.Bezier; + + // Assert + Assert.AreEqual(WayPointType.Bezier, wayPoint.Type, "Type should be settable to Bezier"); + } + + [TestMethod] + public void WayPoint_AutoNaming_SingularType() + { + // Arrange & Act + var wayPoint = new WayPointInstance(); + wayPoint.Type = WayPointType.Circle; + wayPoint.Name = "Patrol"; + + // Assert + Assert.AreEqual("Patrol", wayPoint.Name, "Singular type should use base name only"); + } + + [TestMethod] + public void WayPoint_AutoNaming_MultiPointType() + { + // Arrange & Act + var wayPoint = new WayPointInstance(); + wayPoint.Type = WayPointType.Linear; + wayPoint.Name = "Path"; + wayPoint.Number = 5; + + // Assert + Assert.AreEqual("Path_5", wayPoint.Name, "Multi-point type should use Name_Number format"); + } + + [TestMethod] + public void WayPoint_AutoNaming_NumberChangeLinear() + { + // Arrange + var wayPoint = new WayPointInstance(); + wayPoint.Type = WayPointType.Linear; + wayPoint.Name = "Camera"; + wayPoint.Number = 3; + + // Act + wayPoint.Number = 7; + + // Assert + Assert.AreEqual("Camera_7", wayPoint.Name, "Name should update to 'Camera_7' when number changes to 7"); + } + + [TestMethod] + public void WayPoint_AutoNaming_TypeChangeToSingular() + { + // Arrange + var wayPoint = new WayPointInstance(); + wayPoint.Type = WayPointType.Bezier; + wayPoint.Name = "Target"; + wayPoint.Number = 3; + Assert.AreEqual("Target_3", wayPoint.Name, "Initially should be Target_3"); + + // Act + wayPoint.Type = WayPointType.Ellipse; + + // Assert + Assert.AreEqual("Target", wayPoint.Name, "After changing to singular type, name should be just 'Target'"); + } + + [TestMethod] + public void WayPoint_RequiresRadius_Circle() + { + // Arrange & Act + var wayPoint = new WayPointInstance(); + wayPoint.Type = WayPointType.Circle; + + // Assert + Assert.IsTrue(wayPoint.RequiresRadius(), "Circle should require radius"); + } + + [TestMethod] + public void WayPoint_RequiresTwoRadii_Ellipse() + { + // Arrange & Act + var wayPoint = new WayPointInstance(); + wayPoint.Type = WayPointType.Ellipse; + + // Assert + Assert.IsTrue(wayPoint.RequiresTwoRadii(), "Ellipse should require two radii"); + } + + [TestMethod] + public void WayPoint_IsSingularType_Point() + { + // Arrange & Act + var wayPoint = new WayPointInstance(); + wayPoint.Type = WayPointType.Point; + + // Assert + Assert.IsTrue(wayPoint.IsSingularType(), "Point should be a singular type"); + } + + [TestMethod] + public void WayPoint_IsSingularType_Linear() + { + // Arrange & Act + var wayPoint = new WayPointInstance(); + wayPoint.Type = WayPointType.Linear; + + // Assert + Assert.IsFalse(wayPoint.IsSingularType(), "Linear should not be a singular type"); + } + + [TestMethod] + public void WayPoint_RotationXClamping() + { + // Arrange + var wayPoint = new WayPointInstance(); + + // Act & Assert + wayPoint.RotationX = 100; + Assert.AreEqual(90, wayPoint.RotationX, "RotationX should be clamped to 90"); + + wayPoint.RotationX = -100; + Assert.AreEqual(-90, wayPoint.RotationX, "RotationX should be clamped to -90"); + } + + [TestMethod] + public void WayPoint_RotationYWrapping() + { + // Arrange + var wayPoint = new WayPointInstance(); + + // Act + wayPoint.RotationY = 400; + + // Assert + Assert.AreEqual(40, wayPoint.RotationY, 0.01, "RotationY should wrap around 360 degrees"); + } + + [TestMethod] + public void WayPoint_RollWrapping() + { + // Arrange + var wayPoint = new WayPointInstance(); + + // Act + wayPoint.Roll = 720; + + // Assert + Assert.AreEqual(0, wayPoint.Roll, 0.01, "Roll should wrap around 360 degrees"); + } + } +} diff --git a/TombLib/TombLib/LevelData/Compilers/TombEngine/LevelCompilerTombEngine.cs b/TombLib/TombLib/LevelData/Compilers/TombEngine/LevelCompilerTombEngine.cs index 1b5835b98..fb89588b7 100644 --- a/TombLib/TombLib/LevelData/Compilers/TombEngine/LevelCompilerTombEngine.cs +++ b/TombLib/TombLib/LevelData/Compilers/TombEngine/LevelCompilerTombEngine.cs @@ -56,6 +56,7 @@ public int Compare(TombEngineBucket x, TombEngineBucket y) private readonly List _cameras = new List(); private readonly List _sinks = new List(); private readonly List _flyByCameras = new List(); + private readonly List _wayPoints = new List(); private readonly List _soundSources = new List(); private List _boxes = new List(); private List _overlaps = new List(); @@ -74,6 +75,7 @@ public int Compare(TombEngineBucket x, TombEngineBucket y) private Dictionary _aiObjectsTable; private Dictionary _soundSourcesTable; private Dictionary _flybyTable; + private Dictionary _wayPointTable; // Collected game limits private Dictionary _limits; @@ -228,10 +230,12 @@ private void BuildCamerasAndSinks() int sinkID = 0; int camID = 0; int flybyID = 0; + int wayPointID = 0; _cameraTable = new Dictionary(new ReferenceEqualityComparer()); _sinkTable = new Dictionary(new ReferenceEqualityComparer()); _flybyTable = new Dictionary(new ReferenceEqualityComparer()); + _wayPointTable = new Dictionary(new ReferenceEqualityComparer()); foreach (var room in _level.ExistingRooms) { @@ -241,6 +245,8 @@ private void BuildCamerasAndSinks() _flybyTable.Add(obj, flybyID++); foreach (var obj in room.Objects.OfType()) _sinkTable.Add(obj, sinkID++); + foreach (var obj in room.Objects.OfType()) + _wayPointTable.Add(obj, wayPointID++); } } @@ -332,8 +338,110 @@ private void BuildCamerasAndSinks() lastIndex = _flyByCameras[i].Index; } + // Collect waypoints with name-based grouping and selective compilation logic + // Skip waypoints without a name + // If any waypoint with a name is singular type, only compile singular types with that name + var waypointsByName = new Dictionary>(); + foreach (var instance in _wayPointTable.Keys) + { + // Extract base name + string baseName = instance.Name; + if (string.IsNullOrEmpty(baseName)) + continue; // Skip waypoints without a name + + if (!instance.IsSingularType()) + { + int lastUnderscore = baseName.LastIndexOf('_'); + if (lastUnderscore >= 0) + { + string suffix = baseName.Substring(lastUnderscore + 1); + if (ushort.TryParse(suffix, out _)) + { + baseName = baseName.Substring(0, lastUnderscore); + } + } + } + + if (!waypointsByName.ContainsKey(baseName)) + waypointsByName[baseName] = new List(); + waypointsByName[baseName].Add(instance); + } + + foreach (var namePair in waypointsByName) + { + var waypoints = namePair.Value; + + // Check if any waypoint with this name is singular type + bool hasSingularType = waypoints.Any(wp => wp.IsSingularType()); + + // If there's a singular type, only compile singular type waypoints + // Otherwise, compile all waypoints + var waypointsToCompile = hasSingularType + ? waypoints.Where(wp => wp.IsSingularType()).ToList() + : waypoints; + + foreach (var instance in waypointsToCompile) + { + Vector3 position = instance.Room.WorldPos + instance.Position; + _wayPoints.Add(new TombEngineWayPoint + { + X = (int)Math.Round(position.X), + Y = (int)Math.Round(-position.Y), + Z = (int)Math.Round(position.Z), + Room = _roomRemapping[instance.Room], + RotationX = instance.RotationX, + RotationY = instance.RotationY, + Roll = instance.Roll, + Sequence = instance.Sequence, + Number = instance.Number, + Type = (int)instance.Type, + Radius1 = instance.Radius1, + Radius2 = instance.Radius2, + Name = instance.Name + }); + } + } + + // Sort by name, then number + _wayPoints.Sort((x, y) => + { + int nameCompare = string.Compare(x.Name, y.Name, StringComparison.Ordinal); + if (nameCompare != 0) return nameCompare; + return x.Number.CompareTo(y.Number); + }); + + // Check waypoint duplicates + string lastName = ""; + lastIndex = -1; + + for (int i = 0; i < _wayPoints.Count; i++) + { + // Extract base name for comparison + string baseName = _wayPoints[i].Name; + if (baseName.Contains("_")) + { + int lastUnderscore = baseName.LastIndexOf('_'); + string suffix = baseName.Substring(lastUnderscore + 1); + if (ushort.TryParse(suffix, out _)) + { + baseName = baseName.Substring(0, lastUnderscore); + } + } + + if (baseName != lastName) + { + lastName = baseName; + lastIndex = -1; + } + + if (_wayPoints[i].Number == lastIndex && baseName == lastName) + _progressReporter.ReportWarn($"Warning: waypoint '{baseName}' has duplicated waypoint with number {lastIndex}"); + lastIndex = _wayPoints[i].Number; + } + ReportProgress(47, " Number of cameras: " + _cameraTable.Count); ReportProgress(47, " Number of flyby cameras: " + _flyByCameras.Count); + ReportProgress(47, " Number of waypoints: " + _wayPointTable.Count); ReportProgress(47, " Number of sinks: " + _sinkTable.Count); } diff --git a/TombLib/TombLib/LevelData/Compilers/TombEngine/Structs.cs b/TombLib/TombLib/LevelData/Compilers/TombEngine/Structs.cs index a00161107..af681ed32 100644 --- a/TombLib/TombLib/LevelData/Compilers/TombEngine/Structs.cs +++ b/TombLib/TombLib/LevelData/Compilers/TombEngine/Structs.cs @@ -585,6 +585,24 @@ public struct TombEngineSink public string LuaName; } + [StructLayout(LayoutKind.Sequential, Pack = 1)] + public struct TombEngineWayPoint + { + public int X; + public int Y; + public int Z; + public int Room; + public float RotationX; + public float RotationY; + public float Roll; + public ushort Sequence; + public ushort Number; + public int Type; + public float Radius1; + public float Radius2; + public string Name; + } + [StructLayout(LayoutKind.Sequential, Pack = 1)] public struct TombEngineSoundSource { diff --git a/TombLib/TombLib/LevelData/Compilers/TombEngine/TombEngine.cs b/TombLib/TombLib/LevelData/Compilers/TombEngine/TombEngine.cs index 67650844a..b33e8461a 100644 --- a/TombLib/TombLib/LevelData/Compilers/TombEngine/TombEngine.cs +++ b/TombLib/TombLib/LevelData/Compilers/TombEngine/TombEngine.cs @@ -77,6 +77,24 @@ private void WriteLevelTombEngine() writer.Write((uint)_flyByCameras.Count); writer.WriteBlockArray(_flyByCameras); + writer.Write((uint)_wayPoints.Count); + foreach (var waypoint in _wayPoints) + { + writer.Write(waypoint.X); + writer.Write(waypoint.Y); + writer.Write(waypoint.Z); + writer.Write(waypoint.Room); + writer.Write(waypoint.RotationX); + writer.Write(waypoint.RotationY); + writer.Write(waypoint.Roll); + writer.Write(waypoint.Sequence); + writer.Write(waypoint.Number); + writer.Write(waypoint.Type); + writer.Write(waypoint.Radius1); + writer.Write(waypoint.Radius2); + writer.Write(waypoint.Name); + } + writer.Write((uint)_sinks.Count); foreach (var sink in _sinks) { diff --git a/TombLib/TombLib/LevelData/IO/Prj2Chunks.cs b/TombLib/TombLib/LevelData/IO/Prj2Chunks.cs index ba995a2cc..af88f211a 100644 --- a/TombLib/TombLib/LevelData/IO/Prj2Chunks.cs +++ b/TombLib/TombLib/LevelData/IO/Prj2Chunks.cs @@ -190,6 +190,7 @@ internal static class Prj2Chunks /**********/public static readonly ChunkId ObjectFlyBy = ChunkId.FromString("TeFly"); /**********/public static readonly ChunkId ObjectFlyBy2 = ChunkId.FromString("TeFly2"); /**********/public static readonly ChunkId ObjectFlyBy2LuaScript = ChunkId.FromString("TeFly2Lua"); + /**********/public static readonly ChunkId ObjectWayPoint = ChunkId.FromString("TeWayPt"); /**********/public static readonly ChunkId ObjectMemo = ChunkId.FromString("TeMemo"); /**********/public static readonly ChunkId ObjectMemo2 = ChunkId.FromString("TeMemo2"); /**********/public static readonly ChunkId ObjectSink = ChunkId.FromString("TeSin"); diff --git a/TombLib/TombLib/LevelData/IO/Prj2Loader.cs b/TombLib/TombLib/LevelData/IO/Prj2Loader.cs index 0613f0545..90ae3ab23 100644 --- a/TombLib/TombLib/LevelData/IO/Prj2Loader.cs +++ b/TombLib/TombLib/LevelData/IO/Prj2Loader.cs @@ -1402,6 +1402,25 @@ private static bool LoadObjects(ChunkReader chunkIO, ChunkId idOuter, LevelSetti addObject(instance); newObjects.TryAdd(objectID, instance); } + else if (id3 == Prj2Chunks.ObjectWayPoint) + { + var instance = new WayPointInstance(); + instance.Position = chunkIO.Raw.ReadVector3(); + instance.SetArbitaryRotationsYX(chunkIO.Raw.ReadSingle(), chunkIO.Raw.ReadSingle()); + instance.Roll = chunkIO.Raw.ReadSingle(); + string baseName = chunkIO.Raw.ReadStringUTF8(); + instance.Sequence = LEB128.ReadUShort(chunkIO.Raw); + instance.Number = LEB128.ReadUShort(chunkIO.Raw); + instance.Type = (WayPointType)LEB128.ReadInt(chunkIO.Raw); + instance.Radius1 = chunkIO.Raw.ReadSingle(); + instance.Radius2 = chunkIO.Raw.ReadSingle(); + + // Set the name (will auto-format with _number if multi-point type) + instance.Name = baseName; + + addObject(instance); + newObjects.TryAdd(objectID, instance); + } else if (id3 == Prj2Chunks.ObjectFlyBy2) // Obsolete; LuaScript is unused with new script concept. { var instance = new FlybyCameraInstance(); diff --git a/TombLib/TombLib/LevelData/IO/Prj2Writer.cs b/TombLib/TombLib/LevelData/IO/Prj2Writer.cs index 01ab4e49c..d0a4e6025 100644 --- a/TombLib/TombLib/LevelData/IO/Prj2Writer.cs +++ b/TombLib/TombLib/LevelData/IO/Prj2Writer.cs @@ -679,6 +679,38 @@ private static void WriteObjects(ChunkWriter chunkIO, IEnumerable + { + var instance = (WayPointInstance)o; + LEB128.Write(chunkIO.Raw, objectInstanceLookup.TryGetOrDefault(instance, -1)); + chunkIO.Raw.Write(instance.Position); + chunkIO.Raw.Write(instance.RotationY); + chunkIO.Raw.Write(instance.RotationX); + chunkIO.Raw.Write(instance.Roll); + + // Extract base name from full name + string baseName = instance.Name; + if (!instance.IsSingularType()) + { + int lastUnderscore = baseName.LastIndexOf('_'); + if (lastUnderscore >= 0) + { + string suffix = baseName.Substring(lastUnderscore + 1); + if (ushort.TryParse(suffix, out _)) + { + baseName = baseName.Substring(0, lastUnderscore); + } + } + } + + chunkIO.Raw.WriteStringUTF8(baseName); + LEB128.Write(chunkIO.Raw, instance.Sequence); + LEB128.Write(chunkIO.Raw, instance.Number); + LEB128.Write(chunkIO.Raw, (int)instance.Type); + chunkIO.Raw.Write(instance.Radius1); + chunkIO.Raw.Write(instance.Radius2); + }); else if (o is MemoInstance) using (var chunk = chunkIO.WriteChunk(Prj2Chunks.ObjectMemo2, LEB128.MaximumSize3Byte)) { diff --git a/TombLib/TombLib/LevelData/Instances/WayPointInstance.cs b/TombLib/TombLib/LevelData/Instances/WayPointInstance.cs new file mode 100644 index 000000000..624acd765 --- /dev/null +++ b/TombLib/TombLib/LevelData/Instances/WayPointInstance.cs @@ -0,0 +1,231 @@ +using System; +using System.Linq; +using System.Numerics; + +namespace TombLib.LevelData +{ + public enum WayPointType + { + Point, // Single point, no radius + Circle, // Single point with radius + Ellipse, // Single point with two radii + Square, // Single point with radius (rendered as square) + Rectangle, // Single point with two radii (rendered as rectangle) + Linear, // Multi-point linear path + Bezier // Multi-point bezier path + } + + public class WayPointInstance : PositionBasedObjectInstance, IRotateableYXRoll, ISizeable + { + private string _name = ""; + private ushort _sequence; + private ushort _number; + private WayPointType _type = WayPointType.Point; + + // Public property to access the base name (without number suffix) + public string BaseName + { + get { return _name; } + } + + public ushort Sequence + { + get { return _sequence; } + set { _sequence = value; } + } + + public string Name + { + get + { + // Singular types use name as-is + if (IsSingularType()) + return _name; + else + return _name + "_" + _number; + } + set + { + // When setting name, extract the base name (strip trailing _number if present for multi-point types) + if (!string.IsNullOrEmpty(value)) + { + int lastUnderscore = value.LastIndexOf('_'); + if (lastUnderscore >= 0 && !IsSingularType()) + { + string suffix = value.Substring(lastUnderscore + 1); + if (ushort.TryParse(suffix, out _)) + { + _name = value.Substring(0, lastUnderscore); + } + else + { + _name = value; + } + } + else + { + _name = value; + } + } + else + { + _name = ""; + } + } + } + + public ushort Number + { + get { return _number; } + set + { + _number = value; + } + } + + public WayPointType Type + { + get { return _type; } + set + { + _type = value; + } + } + + public float Radius1 { get; set; } = 1024.0f; // Default radius in units + public float Radius2 { get; set; } = 1024.0f; // Default radius in units + + // ISizeable implementation for scale gizmo support on X and Z axes + public Vector3 DefaultSize => new Vector3(Radius1 * 2, 0, Radius2 * 2); + + public Vector3 Size + { + get => new Vector3(Radius1 * 2, 0, Radius2 * 2); + set + { + // Only allow scaling on X and Z axes for shapes + if (RequiresRadius()) + { + Radius1 = Math.Max(0.01f, value.X / 2); + if (RequiresTwoRadii()) + Radius2 = Math.Max(0.01f, value.Z / 2); + else + Radius2 = Radius1; // Keep them synchronized for single-radius shapes + } + } + } + + private float _rotationX { get; set; } + private float _rotationY { get; set; } + private float _roll { get; set; } + + public bool IsSingularType() + { + return _type == WayPointType.Point || + _type == WayPointType.Circle || + _type == WayPointType.Ellipse || + _type == WayPointType.Square || + _type == WayPointType.Rectangle; + } + + public bool RequiresRadius() + { + return _type == WayPointType.Circle || + _type == WayPointType.Ellipse || + _type == WayPointType.Square || + _type == WayPointType.Rectangle; + } + + public bool RequiresTwoRadii() + { + return _type == WayPointType.Ellipse || + _type == WayPointType.Rectangle; + } + + public WayPointInstance(ObjectInstance selectedObject = null) + { + if (selectedObject is WayPointInstance prevWayPoint) + { + var currNum = (ushort)(prevWayPoint.Number + 1); + + // Only push forward if it's a multi-point type + if (!prevWayPoint.IsSingularType()) + { + // Push next waypoints with same name forward + var level = selectedObject.Room.Level; + var prevName = prevWayPoint._name; + foreach (var room in level.ExistingRooms) + foreach (var instance in room.Objects.OfType()) + if (instance._name == prevName && instance.Number >= currNum) + instance.Number++; + } + + Number = prevWayPoint.IsSingularType() ? (ushort)0 : currNum; + _name = prevWayPoint._name; + Type = prevWayPoint.Type; + Radius1 = prevWayPoint.Radius1; + Radius2 = prevWayPoint.Radius2; + + // Additionally copy last waypoint parameters + RotationX = prevWayPoint.RotationX; + RotationY = prevWayPoint.RotationY; + Roll = prevWayPoint.Roll; + + } + } + + public override ObjectInstance Clone() + { + var clone = (WayPointInstance)base.Clone(); + + // For singular types, clear the name so user must provide a new one + if (IsSingularType()) + { + clone._name = ""; + } + else + { + // For multi-point types, increment the number + clone.Number = (ushort)(Number + 1); + } + + return clone; + } + + /// Degrees in the range [-90, 90] + public float RotationX + { + get { return _rotationX; } + set { _rotationX = Math.Max(-90, Math.Min(90, value)); } + } + + /// Degrees in the range [0, 360) + public float RotationY + { + get { return _rotationY; } + set { _rotationY = (float)(value - Math.Floor(value / 360.0) * 360.0); } + } + + /// Degrees in the range [0, 360) + public float Roll + { + get { return _roll; } + set { _roll = (float)(value - Math.Floor(value / 360.0) * 360.0); } + } + + public override bool CopyToAlternateRooms => false; + + public override string ToString() + { + return "WayPoint " + + ", Name = " + Name + + (IsSingularType() ? "" : ", Number = " + Number) + + ", Type = " + Type + + " (" + (Room?.ToString() ?? "NULL") + ")" + + ", X = " + SectorPosition.X + + ", Z = " + SectorPosition.Y; + } + + public string ShortName() => "WayPoint " + Name + (IsSingularType() ? "" : " (" + Number + ")") + " " + " (" + (Room?.ToString() ?? "NULL") + ")"; + } +}