TerrainTool.cs (15892B)
1 // 2 // Copyright 2020 Electronic Arts Inc. 3 // 4 // The Command & Conquer Map Editor and corresponding source code is free 5 // software: you can redistribute it and/or modify it under the terms of 6 // the GNU General Public License as published by the Free Software Foundation, 7 // either version 3 of the License, or (at your option) any later version. 8 9 // The Command & Conquer Map Editor and corresponding source code is distributed 10 // in the hope that it will be useful, but with permitted additional restrictions 11 // under Section 7 of the GPL. See the GNU General Public License in LICENSE.TXT 12 // distributed with this program. You should have received a copy of the 13 // GNU General Public License along with permitted additional restrictions 14 // with this program. If not, see https://github.com/electronicarts/CnC_Remastered_Collection 15 using MobiusEditor.Controls; 16 using MobiusEditor.Event; 17 using MobiusEditor.Interface; 18 using MobiusEditor.Model; 19 using MobiusEditor.Utility; 20 using MobiusEditor.Widgets; 21 using System; 22 using System.ComponentModel; 23 using System.Drawing; 24 using System.Linq; 25 using System.Windows.Forms; 26 27 namespace MobiusEditor.Tools 28 { 29 public class TerrainTool : ViewTool 30 { 31 private readonly TypeComboBox terrainTypeComboBox; 32 private readonly MapPanel terrainTypeMapPanel; 33 private readonly TerrainProperties terrainProperties; 34 35 private Map previewMap; 36 protected override Map RenderMap => previewMap; 37 38 private bool placementMode; 39 40 private readonly Terrain mockTerrain; 41 42 private Terrain selectedTerrain; 43 private Point selectedTerrainPivot; 44 45 private TerrainType selectedTerrainType; 46 private TerrainPropertiesPopup selectedTerrainProperties; 47 private TerrainType SelectedTerrainType 48 { 49 get => selectedTerrainType; 50 set 51 { 52 if (selectedTerrainType != value) 53 { 54 if (placementMode && (selectedTerrainType != null)) 55 { 56 mapPanel.Invalidate(map, new Rectangle(navigationWidget.MouseCell, selectedTerrainType.OverlapBounds.Size)); 57 } 58 59 selectedTerrainType = value; 60 terrainTypeComboBox.SelectedValue = selectedTerrainType; 61 62 if (placementMode && (selectedTerrainType != null)) 63 { 64 mapPanel.Invalidate(map, new Rectangle(navigationWidget.MouseCell, selectedTerrainType.OverlapBounds.Size)); 65 } 66 67 mockTerrain.Type = selectedTerrainType; 68 mockTerrain.Icon = selectedTerrainType.IsTransformable ? 22 : 0; 69 70 RefreshMapPanel(); 71 } 72 } 73 } 74 75 public TerrainTool(MapPanel mapPanel, MapLayerFlag layers, ToolStripStatusLabel statusLbl, TypeComboBox terrainTypeComboBox, MapPanel terrainTypeMapPanel, TerrainProperties terrainProperties, IGamePlugin plugin, UndoRedoList<UndoRedoEventArgs> url) 76 : base(mapPanel, layers, statusLbl, plugin, url) 77 { 78 previewMap = map; 79 80 mockTerrain = new Terrain(); 81 mockTerrain.PropertyChanged += MockTerrain_PropertyChanged; 82 83 this.mapPanel.MouseDown += MapPanel_MouseDown; 84 this.mapPanel.MouseMove += MapPanel_MouseMove; 85 this.mapPanel.MouseUp += MapPanel_MouseUp; 86 this.mapPanel.MouseDoubleClick += MapPanel_MouseDoubleClick; 87 (this.mapPanel as Control).KeyDown += TerrainTool_KeyDown; 88 (this.mapPanel as Control).KeyUp += TerrainTool_KeyUp; 89 90 this.terrainTypeComboBox = terrainTypeComboBox; 91 this.terrainTypeComboBox.SelectedIndexChanged += TerrainTypeCombo_SelectedIndexChanged; 92 93 this.terrainTypeMapPanel = terrainTypeMapPanel; 94 this.terrainTypeMapPanel.BackColor = Color.White; 95 this.terrainTypeMapPanel.MaxZoom = 1; 96 97 this.terrainProperties = terrainProperties; 98 this.terrainProperties.Terrain = mockTerrain; 99 this.terrainProperties.Visible = plugin.GameType == GameType.TiberianDawn; 100 101 navigationWidget.MouseCellChanged += MouseoverWidget_MouseCellChanged; 102 103 SelectedTerrainType = terrainTypeComboBox.Types.First() as TerrainType; 104 105 UpdateStatus(); 106 } 107 108 private void MapPanel_MouseDoubleClick(object sender, MouseEventArgs e) 109 { 110 if (Control.ModifierKeys != Keys.None) 111 { 112 return; 113 } 114 115 if (map.Metrics.GetCell(navigationWidget.MouseCell, out int cell)) 116 { 117 if (map.Technos[cell] is Terrain terrain) 118 { 119 selectedTerrain = null; 120 121 selectedTerrainProperties?.Close(); 122 selectedTerrainProperties = new TerrainPropertiesPopup(terrainProperties.Plugin, terrain); 123 selectedTerrainProperties.Closed += (cs, ce) => 124 { 125 navigationWidget.Refresh(); 126 }; 127 128 selectedTerrainProperties.Show(mapPanel, mapPanel.PointToClient(Control.MousePosition)); 129 130 UpdateStatus(); 131 } 132 } 133 } 134 135 private void MockTerrain_PropertyChanged(object sender, PropertyChangedEventArgs e) 136 { 137 RefreshMapPanel(); 138 } 139 140 private void TerrainTypeCombo_SelectedIndexChanged(object sender, EventArgs e) 141 { 142 SelectedTerrainType = terrainTypeComboBox.SelectedValue as TerrainType; 143 } 144 145 private void TerrainTool_KeyDown(object sender, KeyEventArgs e) 146 { 147 if (e.KeyCode == Keys.ShiftKey) 148 { 149 EnterPlacementMode(); 150 } 151 } 152 153 private void TerrainTool_KeyUp(object sender, KeyEventArgs e) 154 { 155 if (e.KeyCode == Keys.ShiftKey) 156 { 157 ExitPlacementMode(); 158 } 159 } 160 161 private void MapPanel_MouseDown(object sender, MouseEventArgs e) 162 { 163 if (placementMode) 164 { 165 if (e.Button == MouseButtons.Left) 166 { 167 AddTerrain(navigationWidget.MouseCell); 168 } 169 else if (e.Button == MouseButtons.Right) 170 { 171 RemoveTerrain(navigationWidget.MouseCell); 172 } 173 } 174 else if (e.Button == MouseButtons.Left) 175 { 176 SelectTerrain(navigationWidget.MouseCell); 177 } 178 else if (e.Button == MouseButtons.Right) 179 { 180 PickTerrain(navigationWidget.MouseCell); 181 } 182 } 183 184 private void MapPanel_MouseUp(object sender, MouseEventArgs e) 185 { 186 if (selectedTerrain != null) 187 { 188 selectedTerrain = null; 189 selectedTerrainPivot = Point.Empty; 190 191 UpdateStatus(); 192 } 193 } 194 195 private void MapPanel_MouseMove(object sender, MouseEventArgs e) 196 { 197 if (!placementMode && (Control.ModifierKeys == Keys.Shift)) 198 { 199 EnterPlacementMode(); 200 } 201 else if (placementMode && (Control.ModifierKeys == Keys.None)) 202 { 203 ExitPlacementMode(); 204 } 205 } 206 207 private void MouseoverWidget_MouseCellChanged(object sender, MouseCellChangedEventArgs e) 208 { 209 if (placementMode) 210 { 211 if (SelectedTerrainType != null) 212 { 213 mapPanel.Invalidate(map, new Rectangle(e.OldCell, SelectedTerrainType.OverlapBounds.Size)); 214 mapPanel.Invalidate(map, new Rectangle(e.NewCell, SelectedTerrainType.OverlapBounds.Size)); 215 } 216 } 217 else if (selectedTerrain != null) 218 { 219 var oldLocation = map.Technos[selectedTerrain].Value; 220 var newLocation = new Point(Math.Max(0, e.NewCell.X - selectedTerrainPivot.X), Math.Max(0, e.NewCell.Y - selectedTerrainPivot.Y)); 221 mapPanel.Invalidate(map, selectedTerrain); 222 map.Technos.Remove(selectedTerrain); 223 if (map.Technos.Add(newLocation, selectedTerrain)) 224 { 225 mapPanel.Invalidate(map, selectedTerrain); 226 } 227 else 228 { 229 map.Technos.Add(oldLocation, selectedTerrain); 230 } 231 } 232 } 233 234 private void AddTerrain(Point location) 235 { 236 if (!map.Metrics.Contains(location)) 237 { 238 return; 239 } 240 241 if (SelectedTerrainType != null) 242 { 243 var terrain = mockTerrain.Clone(); 244 if (map.Technos.Add(location, terrain)) 245 { 246 mapPanel.Invalidate(map, terrain); 247 248 void undoAction(UndoRedoEventArgs e) 249 { 250 e.MapPanel.Invalidate(e.Map, location); 251 e.Map.Technos.Remove(terrain); 252 } 253 254 void redoAction(UndoRedoEventArgs e) 255 { 256 e.Map.Technos.Add(location, terrain); 257 e.MapPanel.Invalidate(e.Map, location); 258 } 259 260 url.Track(undoAction, redoAction); 261 262 plugin.Dirty = true; 263 } 264 } 265 } 266 267 private void RemoveTerrain(Point location) 268 { 269 if (map.Technos[location] is Terrain terrain) 270 { 271 mapPanel.Invalidate(map, terrain); 272 map.Technos.Remove(location); 273 274 void undoAction(UndoRedoEventArgs e) 275 { 276 e.Map.Technos.Add(location, terrain); 277 e.MapPanel.Invalidate(e.Map, location); 278 } 279 280 void redoAction(UndoRedoEventArgs e) 281 { 282 e.MapPanel.Invalidate(e.Map, location); 283 e.Map.Technos.Remove(terrain); 284 } 285 286 url.Track(undoAction, redoAction); 287 288 plugin.Dirty = true; 289 } 290 } 291 292 private void EnterPlacementMode() 293 { 294 if (placementMode) 295 { 296 return; 297 } 298 299 placementMode = true; 300 301 navigationWidget.MouseoverSize = Size.Empty; 302 303 if (SelectedTerrainType != null) 304 { 305 mapPanel.Invalidate(map, new Rectangle(navigationWidget.MouseCell, selectedTerrainType.OverlapBounds.Size)); 306 } 307 308 UpdateStatus(); 309 } 310 311 private void ExitPlacementMode() 312 { 313 if (!placementMode) 314 { 315 return; 316 } 317 318 placementMode = false; 319 320 navigationWidget.MouseoverSize = new Size(1, 1); 321 322 if (SelectedTerrainType != null) 323 { 324 mapPanel.Invalidate(map, new Rectangle(navigationWidget.MouseCell, selectedTerrainType.OverlapBounds.Size)); 325 } 326 327 UpdateStatus(); 328 } 329 330 private void PickTerrain(Point location) 331 { 332 if (map.Metrics.GetCell(location, out int cell)) 333 { 334 if (map.Technos[cell] is Terrain terrain) 335 { 336 SelectedTerrainType = terrain.Type; 337 mockTerrain.Trigger = terrain.Trigger; 338 } 339 } 340 } 341 342 private void SelectTerrain(Point location) 343 { 344 if (map.Metrics.GetCell(location, out int cell)) 345 { 346 selectedTerrain = map.Technos[cell] as Terrain; 347 selectedTerrainPivot = (selectedTerrain != null) ? (location - (Size)map.Technos[selectedTerrain].Value) : Point.Empty; 348 } 349 350 UpdateStatus(); 351 } 352 353 private void RefreshMapPanel() 354 { 355 terrainTypeMapPanel.MapImage = mockTerrain.Type.Thumbnail; 356 } 357 358 private void UpdateStatus() 359 { 360 if (placementMode) 361 { 362 statusLbl.Text = "Left-Click to place terrain, Right-Click to remove terrain"; 363 } 364 else if (selectedTerrain != null) 365 { 366 statusLbl.Text = "Drag mouse to move terrain"; 367 } 368 else 369 { 370 statusLbl.Text = "Shift to enter placement mode, Left-Click drag to move terrain, Double-Click update terrain properties, Right-Click to pick terrain"; 371 } 372 } 373 374 protected override void PreRenderMap() 375 { 376 base.PreRenderMap(); 377 378 previewMap = map.Clone(); 379 if (placementMode) 380 { 381 var location = navigationWidget.MouseCell; 382 if (SelectedTerrainType != null) 383 { 384 if (previewMap.Metrics.Contains(location)) 385 { 386 var terrain = new Terrain 387 { 388 Type = SelectedTerrainType, 389 Icon = SelectedTerrainType.IsTransformable ? 22 : 0, 390 Tint = Color.FromArgb(128, Color.White) 391 }; 392 previewMap.Technos.Add(location, terrain); 393 } 394 } 395 } 396 } 397 398 protected override void PostRenderMap(Graphics graphics) 399 { 400 base.PostRenderMap(graphics); 401 402 var terrainPen = new Pen(Color.Green, 4.0f); 403 var occupyPen = new Pen(Color.Red, 2.0f); 404 foreach (var (topLeft, terrain) in previewMap.Technos.OfType<Terrain>()) 405 { 406 var bounds = new Rectangle(new Point(topLeft.X * Globals.TileWidth, topLeft.Y * Globals.TileHeight), terrain.Type.RenderSize); 407 graphics.DrawRectangle(terrainPen, bounds); 408 409 for (var y = 0; y < terrain.Type.OccupyMask.GetLength(0); ++y) 410 { 411 for (var x = 0; x < terrain.Type.OccupyMask.GetLength(1); ++x) 412 { 413 if (terrain.Type.OccupyMask[y, x]) 414 { 415 var occupyBounds = new Rectangle( 416 new Point((topLeft.X + x) * Globals.TileWidth, (topLeft.Y + y) * Globals.TileHeight), 417 Globals.TileSize 418 ); 419 graphics.DrawRectangle(occupyPen, occupyBounds); 420 } 421 } 422 } 423 } 424 } 425 426 #region IDisposable Support 427 private bool disposedValue = false; 428 429 protected override void Dispose(bool disposing) 430 { 431 if (!disposedValue) 432 { 433 if (disposing) 434 { 435 selectedTerrainProperties?.Close(); 436 437 mapPanel.MouseDown -= MapPanel_MouseDown; 438 mapPanel.MouseMove -= MapPanel_MouseMove; 439 mapPanel.MouseUp -= MapPanel_MouseUp; 440 mapPanel.MouseDoubleClick -= MapPanel_MouseDoubleClick; 441 (mapPanel as Control).KeyDown -= TerrainTool_KeyDown; 442 (mapPanel as Control).KeyUp -= TerrainTool_KeyUp; 443 444 terrainTypeComboBox.SelectedIndexChanged -= TerrainTypeCombo_SelectedIndexChanged; 445 446 navigationWidget.MouseCellChanged -= MouseoverWidget_MouseCellChanged; 447 } 448 disposedValue = true; 449 } 450 451 base.Dispose(disposing); 452 } 453 #endregion 454 } 455 }