Skip to content
Merged
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
2 changes: 1 addition & 1 deletion Directory.Build.props
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<Project>
<PropertyGroup>
<Version>0.6.18</Version>
<Version>0.7.0</Version>
<Authors>Anton Makarevich</Authors>
<Company>Anton Makarevich</Company>
<PackageLicenseExpression>GPL-3.0-only</PackageLicenseExpression>
Expand Down
36 changes: 36 additions & 0 deletions src/MakaMek.MapEditor/Controls/HexInfoOverlay.axaml
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,42 @@
ToolTip.Tip="{extensions:Localize 'EditMap_IncreaseWaterDepth'}"
Classes="cardAction" Foreground="{StaticResource TextLightBrush}"/>
</Grid>
<StackPanel IsVisible="{Binding HexViewModel.HasRoadBridge}" Spacing="4">
<Grid ColumnDefinitions="*,Auto,Auto"
IsVisible="{Binding HexViewModel.IsBridge}">
<StackPanel Orientation="Horizontal" Spacing="5">
<TextBlock Text="{extensions:Localize 'EditMap_BridgeHeight'}"
Classes="bodySmall" Foreground="{StaticResource TextLightBrush}"/>
<TextBlock Text="{Binding HexViewModel.BridgeHeight}"
Classes="bodySmall" Foreground="{StaticResource TextLightBrush}"/>
</StackPanel>
<Button Grid.Column="1" Content="▲" FontSize="10" Padding="4,0"
Command="{Binding RaiseBridgeLevelCommand}"
ToolTip.Tip="{extensions:Localize 'EditMap_RaiseBridge'}"
Classes="cardAction" Foreground="{StaticResource TextLightBrush}"/>
<Button Grid.Column="2" Content="▼" FontSize="10" Padding="4,0"
Command="{Binding LowerBridgeLevelCommand}"
ToolTip.Tip="{extensions:Localize 'EditMap_LowerBridge'}"
Classes="cardAction" Foreground="{StaticResource TextLightBrush}"/>
</Grid>
<Grid ColumnDefinitions="*,Auto,Auto"
IsVisible="{Binding HexViewModel.IsBridge}">
<StackPanel Orientation="Horizontal" Spacing="5">
<TextBlock Text="{extensions:Localize 'EditMap_ConstructionFactor'}"
Classes="bodySmall" Foreground="{StaticResource TextLightBrush}"/>
<TextBlock Text="{Binding HexViewModel.ConstructionFactor}"
Classes="bodySmall" Foreground="{StaticResource TextLightBrush}"/>
</StackPanel>
<Button Grid.Column="1" Content="▲" FontSize="10" Padding="4,0"
Command="{Binding IncreaseConstructionFactorCommand}"
ToolTip.Tip="{extensions:Localize 'EditMap_IncreaseConstructionFactor'}"
Classes="cardAction" Foreground="{StaticResource TextLightBrush}"/>
<Button Grid.Column="2" Content="▼" FontSize="10" Padding="4,0"
Command="{Binding DecreaseConstructionFactorCommand}"
ToolTip.Tip="{extensions:Localize 'EditMap_DecreaseConstructionFactor'}"
Classes="cardAction" Foreground="{StaticResource TextLightBrush}"/>
</Grid>
</StackPanel>
</StackPanel>
</Grid>
</UserControl>
1 change: 1 addition & 0 deletions src/MakaMek.MapEditor/Models/ToolType.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ public enum ToolType
{
Terrain,
Cursor,
RoadBridge,
RaiseLevel,
LowerLevel,
IncreaseWaterDepth,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,13 @@ public MapEditorFakeLocalizationService()
Strings["EditMap_CloseConfirmTitle"] = "Close Map";
Strings["EditMap_CloseConfirmMessage"] = "Are you sure you want to close this map? Any unsaved changes will be lost.";
Strings["EditMap_CloseHexInfo"] = "Close";
Strings["EditMap_RoadBridge"] = "Road/Bridge";
Strings["EditMap_BridgeHeight"] = "Bridge Height:";
Strings["EditMap_ConstructionFactor"] = "Construction:";
Strings["EditMap_RaiseBridge"] = "▲ Raise Bridge";
Strings["EditMap_LowerBridge"] = "▼ Lower Bridge";
Strings["EditMap_IncreaseConstructionFactor"] = "▲ Increase CF";
Strings["EditMap_DecreaseConstructionFactor"] = "▼ Decrease CF";

Strings["Window_Title"] = "MakaMek Map Editor";

Expand Down
235 changes: 231 additions & 4 deletions src/MakaMek.MapEditor/ViewModels/EditMapViewModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -186,14 +186,150 @@ public ToolItem? SelectedTool
return Task.CompletedTask;
});

public IAsyncCommand RaiseBridgeLevelCommand => field ??= new AsyncCommand(() =>
{
if (_currentHex == null) return Task.CompletedTask;
if (Map == null) return Task.CompletedTask;

var bridge = _currentHex.GetTerrain(MakaMekTerrains.Bridge) as BridgeTerrain;
var road = _currentHex.GetTerrain(MakaMekTerrains.Road);

if (bridge == null && road == null) return Task.CompletedTask;

Hex updatedHex;
if (road != null)
{
updatedHex = new Hex(_currentHex.Coordinates, _currentHex.Level);
foreach (var terrain in _currentHex.GetTerrains())
{
if (terrain is RoadTerrain) continue;
updatedHex.AddTerrain(terrain);
}
updatedHex.AddTerrain(new BridgeTerrain(1, DefaultConstructionFactor));
}
else
{
updatedHex = new Hex(_currentHex.Coordinates, _currentHex.Level);
foreach (var terrain in _currentHex.GetTerrains())
{
if (terrain is BridgeTerrain)
updatedHex.AddTerrain(new BridgeTerrain(bridge!.Height + 1, bridge.ConstructionFactor));
else
updatedHex.AddTerrain(terrain);
}
}

Map.AddHex(updatedHex);
_currentHex = updatedHex;
HexViewModel?.UpdateFromHex(updatedHex);
HexUpdated?.Invoke(updatedHex);
return Task.CompletedTask;
});

public IAsyncCommand LowerBridgeLevelCommand => field ??= new AsyncCommand(() =>
{
if (_currentHex == null) return Task.CompletedTask;
if (Map == null) return Task.CompletedTask;

var bridge = _currentHex.GetTerrain(MakaMekTerrains.Bridge) as BridgeTerrain;
if (bridge == null) return Task.CompletedTask;

var newHeight = bridge.Height - 1;

Hex updatedHex;
if (newHeight <= 0 && !_currentHex.HasTerrain(MakaMekTerrains.Water))
{
updatedHex = new Hex(_currentHex.Coordinates, _currentHex.Level);
foreach (var terrain in _currentHex.GetTerrains())
{
if (terrain is BridgeTerrain) continue;
updatedHex.AddTerrain(terrain);
}
updatedHex.AddTerrain(new RoadTerrain());
}
else
{
updatedHex = new Hex(_currentHex.Coordinates, _currentHex.Level);
foreach (var terrain in _currentHex.GetTerrains())
{
if (terrain is BridgeTerrain)
updatedHex.AddTerrain(new BridgeTerrain(Math.Max(DefaultBridgeHeight, newHeight), bridge.ConstructionFactor));
else
updatedHex.AddTerrain(terrain);
}
}

Map.AddHex(updatedHex);
_currentHex = updatedHex;
HexViewModel?.UpdateFromHex(updatedHex);
HexUpdated?.Invoke(updatedHex);
return Task.CompletedTask;
});

public IAsyncCommand IncreaseConstructionFactorCommand => field ??= new AsyncCommand(() =>
{
if (_currentHex == null) return Task.CompletedTask;
if (Map == null) return Task.CompletedTask;

var bridge = _currentHex.GetTerrain(MakaMekTerrains.Bridge) as BridgeTerrain;
if (bridge == null) return Task.CompletedTask;

var newCf = Math.Min(MaxConstructionFactor, bridge.ConstructionFactor + ConstructionFactorStep);
var updatedHex = new Hex(_currentHex.Coordinates, _currentHex.Level);
foreach (var terrain in _currentHex.GetTerrains())
{
if (terrain is BridgeTerrain)
updatedHex.AddTerrain(new BridgeTerrain(bridge.Height, newCf));
else
updatedHex.AddTerrain(terrain);
}

Map.AddHex(updatedHex);
_currentHex = updatedHex;
HexViewModel?.UpdateFromHex(updatedHex);
HexUpdated?.Invoke(updatedHex);
return Task.CompletedTask;
});

public IAsyncCommand DecreaseConstructionFactorCommand => field ??= new AsyncCommand(() =>
{
if (_currentHex == null) return Task.CompletedTask;
if (Map == null) return Task.CompletedTask;

var bridge = _currentHex.GetTerrain(MakaMekTerrains.Bridge) as BridgeTerrain;
if (bridge == null) return Task.CompletedTask;

var newCf = Math.Max(MinConstructionFactor, bridge.ConstructionFactor - ConstructionFactorStep);
var updatedHex = new Hex(_currentHex.Coordinates, _currentHex.Level);
foreach (var terrain in _currentHex.GetTerrains())
{
if (terrain is BridgeTerrain)
updatedHex.AddTerrain(new BridgeTerrain(bridge.Height, newCf));
else
updatedHex.AddTerrain(terrain);
}

Map.AddHex(updatedHex);
_currentHex = updatedHex;
HexViewModel?.UpdateFromHex(updatedHex);
HexUpdated?.Invoke(updatedHex);
return Task.CompletedTask;
});

public virtual void Initialize(IBattleMap map)
{
Map = map;
LoadTerrains();
LoadTools();
}

// IMPORTANT: When adding new Terrain subclasses, manually add them here.
// This list replaces reflection-based discovery for WASM compatibility.
public const int DefaultBridgeHeight = 0;
public const int DefaultConstructionFactor = 60;
public const int MinConstructionFactor = 0;
public const int MaxConstructionFactor = 500;
public const int ConstructionFactorStep = 5;

private static readonly IReadOnlyList<Terrain> KnownTerrains =
[
new ClearTerrain(),
Expand All @@ -205,7 +341,7 @@ public virtual void Initialize(IBattleMap map)

private const string AssetBaseUri = "avares://Sanet.MakaMek.MapEditor/Assets";

private void LoadTerrains()
private void LoadTools()
{
AvailableTerrains.Clear();
AvailableTools.Clear();
Expand All @@ -220,6 +356,9 @@ private void LoadTerrains()
imagePath: $"{AssetBaseUri}/terrain/{terrain.Id.ToString().ToLowerInvariant()}.png"));
}

AvailableTools.Add(new ToolItem(LocalizationService.GetString("EditMap_RoadBridge"), ToolType.RoadBridge,
imagePath: $"{AssetBaseUri}/terrain/road.png"));

AvailableTools.Add(new ToolItem(LocalizationService.GetString("EditMap_RaiseLevel"), ToolType.RaiseLevel));
AvailableTools.Add(new ToolItem(LocalizationService.GetString("EditMap_LowerLevel"), ToolType.LowerLevel));
AvailableTools.Add(new ToolItem(LocalizationService.GetString("EditMap_IncreaseWaterDepth"), ToolType.IncreaseWaterDepth));
Expand All @@ -234,14 +373,18 @@ private void LoadTerrains()
/// In Terrain mode, replaces the hex's terrains and returns null.
/// In Cursor mode, populates the hex info popup and tracks the current hex.
/// </summary>
/// <summary>
/// Handles hex selection based on the current edit mode.
/// In Terrain mode, applies the selected terrain with correct layering rules.
/// In Cursor mode, populates the hex info popup and tracks the current hex.
/// </summary>
public Hex? HandleHexSelection(Hex hex)
{
switch (ActiveEditMode)
{
case ToolType.Terrain:
if (SelectedTerrain == null) return null;
hex.ReplaceTerrains([SelectedTerrain]);
return null;
return ApplyTerrainToHex(hex, SelectedTerrain);

case ToolType.Cursor:
if (HexViewModel == null)
Expand All @@ -264,11 +407,95 @@ private void LoadTerrains()
case ToolType.DecreaseWaterDepth:
return UpdateHexWithNewWaterDepth(hex, 1);

case ToolType.RoadBridge:
if (Map == null) return null;
if (hex.HasTerrain(MakaMekTerrains.Bridge)) return hex;
if (hex.HasTerrain(MakaMekTerrains.Road)) return hex;
return ApplyTerrainToHex(hex,
hex.HasTerrain(MakaMekTerrains.Water)
? new BridgeTerrain(DefaultBridgeHeight, DefaultConstructionFactor)
: new RoadTerrain());

default:
return null;
}
}

private static bool IsGroundTerrain(MakaMekTerrains id) =>
id is MakaMekTerrains.LightWoods
or MakaMekTerrains.HeavyWoods or MakaMekTerrains.Rough
or MakaMekTerrains.Pavement or MakaMekTerrains.Rubble;

/// <summary>
/// Applies a terrain to a hex with correct layering:
/// - Ground terrains (Clear/Woods/Rough/Pavement/Rubble) are mutually exclusive with each other
/// - Water coexists with ground and road layers; adding Water over Road converts Road to Bridge
/// - Road/Bridge sits on top and coexists with ground + water layers
/// </summary>
private static Hex ApplyTerrainToHex(Hex hex, Terrain terrain)
{
var newId = terrain.Id;

if (newId == MakaMekTerrains.Clear)
{
foreach (var t in hex.GetTerrains().ToList())
hex.RemoveTerrain(t.Id);
hex.AddTerrain(terrain);
return hex;
}

if (IsGroundTerrain(newId))
{

var existing = hex.GetTerrains().FirstOrDefault(t => IsGroundTerrain(t.Id));
if (existing != null)
hex.RemoveTerrain(existing.Id);
hex.AddTerrain(terrain);
return hex;
Comment thread
anton-makarevich marked this conversation as resolved.
}

if (newId == MakaMekTerrains.Water)
{
hex.RemoveTerrain(MakaMekTerrains.Water);
if (hex.HasTerrain(MakaMekTerrains.Road))
{
hex.RemoveTerrain(MakaMekTerrains.Road);
hex.AddTerrain(new BridgeTerrain(DefaultBridgeHeight, DefaultConstructionFactor));
}
hex.AddTerrain(terrain);
return hex;
}

if (newId == MakaMekTerrains.Road)
{
if (hex.HasTerrain(MakaMekTerrains.Bridge))
return hex;
hex.RemoveTerrain(MakaMekTerrains.Road);
if (hex.HasTerrain(MakaMekTerrains.Water))
hex.AddTerrain(new BridgeTerrain(DefaultBridgeHeight, DefaultConstructionFactor));
else
hex.AddTerrain(terrain);
return hex;
}

if (newId == MakaMekTerrains.Bridge)
{
hex.RemoveTerrain(MakaMekTerrains.Road);
hex.RemoveTerrain(MakaMekTerrains.Bridge);
if (hex.HasTerrain(MakaMekTerrains.Water) && terrain is BridgeTerrain bt)
hex.AddTerrain(new BridgeTerrain(bt.Height, bt.ConstructionFactor));
else
hex.AddTerrain(new RoadTerrain());
return hex;
}

var oldGround = hex.GetTerrains().FirstOrDefault(t => IsGroundTerrain(t.Id));
if (oldGround != null)
hex.RemoveTerrain(oldGround.Id);
hex.AddTerrain(terrain);
return hex;
}

private Hex? ReplaceHexWithNewLevel(Hex oldHex, int newLevel)
{
if (Map == null) return null;
Expand Down
Loading
Loading