Palette picks a whole object on a click; mod documentation
This commit is contained in:
1 parent
c585960c61
commit
bbdfe95de3
41 files changed
+1114
-179
No files matched your search
@@ -0,0 +1,41 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using Cthangover.Core.Mods;
|
||||
using Cthangover.Core.Mods.Resolvers;
|
||||
using Cthangover.Mods.Rpg;
|
||||
|
||||
namespace Cthangover.Tools.RpgEditor
|
||||
{
|
||||
/// <summary>
|
||||
/// All map charsets supplied by the loaded mods, including NPC artwork without
|
||||
/// a character entity. Paths stay distinct even when two mods use the same file name.
|
||||
/// These entries describe appearance only and must never be used as battle enemy ids.
|
||||
/// </summary>
|
||||
public static class RpgAppearanceCatalog
|
||||
{
|
||||
/// <summary>Reads charset images anew and builds standing-frame previews for the picker.</summary>
|
||||
public static List<RpgPickerEntry> Load(RpgTextures textures)
|
||||
{
|
||||
var files = Files.Collect(RpgTextures.CharsetGroup);
|
||||
return (files?.Keys.AsEnumerable() ?? Enumerable.Empty<string>())
|
||||
.Where(IsImage)
|
||||
.Select(file => Path.ChangeExtension(file, null))
|
||||
.Distinct(StringComparer.OrdinalIgnoreCase)
|
||||
.OrderBy(path => path, StringComparer.OrdinalIgnoreCase)
|
||||
.Select(path => new RpgPickerEntry
|
||||
{
|
||||
Id = path,
|
||||
Name = Path.GetFileName(path),
|
||||
Look = path,
|
||||
Icon = RpgCharacterCatalog.FrontFrame(textures.Charset(path)),
|
||||
OwnsIcon = true
|
||||
})
|
||||
.ToList();
|
||||
}
|
||||
|
||||
private static bool IsImage(string path)
|
||||
=> ModConfig.Instance.GetTextureExtensionSet().Contains(Path.GetExtension(path));
|
||||
}
|
||||
}
|
||||
@@ -40,7 +40,8 @@ namespace Cthangover.Tools.RpgEditor
|
||||
Id = info.ID,
|
||||
Name = string.IsNullOrEmpty(info.Name) ? info.ID : TranslationServer.Translate(info.Name).ToString(),
|
||||
Look = charset,
|
||||
Icon = FrontFrame(charset == null ? null : textures.Charset(charset))
|
||||
Icon = FrontFrame(charset == null ? null : textures.Charset(charset)),
|
||||
OwnsIcon = true
|
||||
};
|
||||
|
||||
private static string CharsetOf(CharacterInfo info, HashSet<string> charsets)
|
||||
@@ -54,7 +55,7 @@ namespace Cthangover.Tools.RpgEditor
|
||||
return candidates.FirstOrDefault(path => !string.IsNullOrEmpty(path) && charsets.Contains(path));
|
||||
}
|
||||
|
||||
private static Texture2D FrontFrame(Texture2D image)
|
||||
internal static Texture2D FrontFrame(Texture2D image)
|
||||
{
|
||||
if (image == null)
|
||||
return null;
|
||||
|
||||
+22
-4
@@ -84,13 +84,14 @@ namespace Cthangover.Tools.RpgEditor
|
||||
/// <summary>Reads the entries again and reapplies the search.</summary>
|
||||
public void Reload()
|
||||
{
|
||||
var selectedId = Selected?.Id;
|
||||
ReleaseEntries();
|
||||
all = Source?.Invoke() ?? new List<RpgPickerEntry>();
|
||||
Filter();
|
||||
Filter(selectedId);
|
||||
}
|
||||
|
||||
private void Filter()
|
||||
private void Filter(string selectedId)
|
||||
{
|
||||
var selectedId = Selected?.Id;
|
||||
var query = search.Text.Trim();
|
||||
shown.Clear();
|
||||
shown.AddRange(all.Where(entry => query.Length == 0
|
||||
@@ -108,6 +109,23 @@ namespace Cthangover.Tools.RpgEditor
|
||||
UpdateButtons();
|
||||
}
|
||||
|
||||
/// <summary>Drop the list's native references before disposing entry-owned previews; source images remain cache-owned.</summary>
|
||||
public override void _ExitTree()
|
||||
{
|
||||
ReleaseEntries();
|
||||
Source = null;
|
||||
}
|
||||
|
||||
private void ReleaseEntries()
|
||||
{
|
||||
list.Clear();
|
||||
shown.Clear();
|
||||
var owned = new HashSet<Texture2D>(all.Where(e => e.OwnsIcon && e.Icon != null).Select(e => e.Icon));
|
||||
all.Clear();
|
||||
foreach (var icon in owned)
|
||||
icon.Dispose();
|
||||
}
|
||||
|
||||
private void Run(Action<RpgPickerEntry> action)
|
||||
{
|
||||
if (Selected != null)
|
||||
@@ -120,7 +138,7 @@ namespace Cthangover.Tools.RpgEditor
|
||||
button.Disabled = Selected == null;
|
||||
}
|
||||
|
||||
private void OnSearchChanged(string text) => Filter();
|
||||
private void OnSearchChanged(string text) => Filter(Selected?.Id);
|
||||
|
||||
private void OnItemSelected(long index) => UpdateButtons();
|
||||
}
|
||||
|
||||
@@ -146,6 +146,22 @@ namespace Cthangover.Tools.RpgEditor
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>Places a rectangular selection, skipping padding and clipping at map edges; one history step is recorded by the caller.</summary>
|
||||
public int Stamp(RpgEditLayer layer, int x, int y, RpgTileStamp stamp)
|
||||
{
|
||||
if (layer > RpgEditLayer.Front || stamp == null)
|
||||
return 0;
|
||||
var changed = 0;
|
||||
for (var sy = 0; sy < stamp.Height; sy++)
|
||||
for (var sx = 0; sx < stamp.Width; sx++)
|
||||
{
|
||||
var tile = stamp.TileAt(sx, sy);
|
||||
if (tile >= 0 && Paint(layer, x + sx, y + sy, tile, RpgCollision.Auto))
|
||||
changed++;
|
||||
}
|
||||
return changed;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Flood-fills (4-neighbourhood) the area of cells equal to the start
|
||||
/// cell with a tile or a collision code. Returns the number of cells changed.
|
||||
|
||||
+171
-140
@@ -1,10 +1,12 @@
|
||||
using System;
|
||||
using Cthangover.Core.UI.Theming;
|
||||
using Cthangover.Mods.Rpg;
|
||||
using Godot;
|
||||
|
||||
namespace Cthangover.Tools.RpgEditor
|
||||
{
|
||||
using System;
|
||||
using Cthangover.Core.UI.Theming;
|
||||
using Cthangover.Mods.Rpg;
|
||||
using Cthangover.Mods.TileRender;
|
||||
using Cthangover.Core.Events;
|
||||
using Godot;
|
||||
|
||||
/// <summary>
|
||||
/// The map as the editor shows it: back, main and front tiles, the events
|
||||
/// (their charset frame or tile, else a marker), the spawn point, the
|
||||
@@ -37,6 +39,13 @@ namespace Cthangover.Tools.RpgEditor
|
||||
private static readonly Color LockedModulate = new(1f, 1f, 1f, 0.55f);
|
||||
|
||||
private readonly RpgTextures textures = new();
|
||||
private ITileWorldPresentation preview;
|
||||
private RpgRenderSceneBuilder builder;
|
||||
private TileRenderScene previewScene;
|
||||
private ITileProjection projection;
|
||||
private RpgMapEditorOverlay overlay;
|
||||
public string RendererId { get; private set; } = "topdown";
|
||||
public ITileWorldPresentation Preview => preview;
|
||||
private Vector2I? hover;
|
||||
private bool stroking;
|
||||
private RpgMapEvent dragged;
|
||||
@@ -63,6 +72,9 @@ namespace Cthangover.Tools.RpgEditor
|
||||
/// <summary>Atlas index the pencil and the fill put down.</summary>
|
||||
public int SelectedTile { get; set; }
|
||||
|
||||
/// <summary>Multi-cell brush selected in the object atlas; null uses SelectedTile.</summary>
|
||||
public RpgTileStamp SelectedStamp { get; set; }
|
||||
|
||||
/// <summary>Collision code the pencil and the fill put down.</summary>
|
||||
public char SelectedCollision { get; set; } = RpgCollision.Block;
|
||||
|
||||
@@ -99,11 +111,16 @@ namespace Cthangover.Tools.RpgEditor
|
||||
/// <summary>Creates the canvas; input comes through the GuiInput event.</summary>
|
||||
public RpgMapEditorCanvas()
|
||||
{
|
||||
RendererId = Cthangover.Core.Games.ActiveGame.GetSetting("tile.renderer") ?? (TileRendererRegistry.Instance.Ids.Count == 1 ? TileRendererRegistry.Instance.Ids[0] : "topdown");
|
||||
foreach (var arg in OS.GetCmdlineUserArgs())
|
||||
if (arg.StartsWith("--editor-renderer=")) RendererId = arg.Substring(18);
|
||||
ClipContents = true;
|
||||
FocusMode = FocusModeEnum.Click;
|
||||
TextureFilter = TextureFilterEnum.Nearest;
|
||||
GuiInput += OnGuiInput;
|
||||
MouseExited += OnMouseExited;
|
||||
overlay = new RpgMapEditorOverlay(this);
|
||||
AddChild(overlay);
|
||||
}
|
||||
|
||||
private float CellSize => (Tileset?.TileSize ?? 32) * Zoom;
|
||||
@@ -115,160 +132,168 @@ namespace Cthangover.Tools.RpgEditor
|
||||
Refresh();
|
||||
}
|
||||
|
||||
/// <summary>Resizes to the map and redraws; call after any outside change.</summary>
|
||||
public void SetRenderer(string id)
|
||||
{
|
||||
TileRendererRegistry.Instance.Resolve(id);
|
||||
if (RendererId != id) { ReleasePreview(); projection = null; }
|
||||
RendererId = id;
|
||||
Refresh();
|
||||
}
|
||||
|
||||
public override void _EnterTree()
|
||||
{
|
||||
TileRendererRegistry.Instance.Removing += OnRendererRemoving;
|
||||
ModEventBus.Subscribe<ModSetReloadedEvent>(OnModsReloaded);
|
||||
}
|
||||
public override void _ExitTree()
|
||||
{
|
||||
TileRendererRegistry.Instance.Removing -= OnRendererRemoving;
|
||||
ModEventBus.Unsubscribe<ModSetReloadedEvent>(OnModsReloaded);
|
||||
ReleasePreview(); projection = null; builder?.Dispose(); textures.Dispose();
|
||||
}
|
||||
|
||||
private void OnRendererRemoving(string id)
|
||||
{
|
||||
if (RendererId != id) return;
|
||||
ReleasePreview(); projection = null;
|
||||
}
|
||||
private void OnModsReloaded(ModSetReloadedEvent evt)
|
||||
{
|
||||
if (!evt.Applied || Document == null) return;
|
||||
ReleasePreview(); builder?.Dispose(); textures.Dispose();
|
||||
Tileset = RpgTilesetFactory.Instance.Get(Document.Map.Tileset);
|
||||
Refresh();
|
||||
}
|
||||
|
||||
private void ReleasePreview()
|
||||
{
|
||||
preview?.Dispose();
|
||||
preview = null;
|
||||
previewScene?.Release();
|
||||
previewScene = null;
|
||||
}
|
||||
|
||||
/// <summary>The same renderer as runtime, with a static frame and authoring overlays.</summary>
|
||||
public void Refresh()
|
||||
{
|
||||
var grid = Document?.Grid;
|
||||
CustomMinimumSize = grid == null ? Vector2.Zero : new Vector2(grid.Width, grid.Height) * CellSize;
|
||||
if (Document == null || Tileset == null) { ReleasePreview(); CustomMinimumSize = Vector2.Zero; QueueRedraw(); return; }
|
||||
var factory = TileRendererRegistry.Instance.Resolve(RendererId);
|
||||
projection = factory.Projection(Tileset.TileSize);
|
||||
builder ??= new RpgRenderSceneBuilder(textures);
|
||||
var previous = previewScene;
|
||||
previewScene = builder.Build(Document.Map, Document.Grid, Tileset);
|
||||
previewScene.Zoom = Zoom;
|
||||
foreach (var visual in previewScene.Visuals)
|
||||
visual.Tint = new Color(1, 1, 1, ActiveLayer <= RpgEditLayer.Front && visual.Layer != (int)ActiveLayer ? 0.45f : 1f);
|
||||
if (preview == null)
|
||||
{
|
||||
preview = factory.Create();
|
||||
AddChild(preview.Root);
|
||||
MoveChild(preview.Root, 0);
|
||||
preview.Mount(previewScene);
|
||||
}
|
||||
else preview.Apply(new TileSceneChanges { BaseRevision = previous.Revision, Scene = previewScene });
|
||||
previous?.Release();
|
||||
CustomMinimumSize = preview.ContentBounds.Size * Zoom;
|
||||
preview.Resize(CustomMinimumSize);
|
||||
var frame = new TileFrame { Ambient = Colors.White, Focus = new Vector2(Document.Grid.Width, Document.Grid.Height) / 2 };
|
||||
foreach (var mapEvent in Document.LockedEvents) AddPreviewEvent(frame, builder, mapEvent, true);
|
||||
foreach (var mapEvent in Document.Events) AddPreviewEvent(frame, builder, mapEvent, false);
|
||||
preview.Present(frame, 0);
|
||||
overlay.Position = preview.Root.Position;
|
||||
overlay.Scale = preview.Root.Scale;
|
||||
QueueRedraw();
|
||||
}
|
||||
|
||||
/// <summary>Draws the map.</summary>
|
||||
public override void _Draw()
|
||||
private void AddPreviewEvent(TileFrame frame, RpgRenderSceneBuilder builder, RpgMapEvent mapEvent, bool locked)
|
||||
{
|
||||
DrawRect(new Rect2(Vector2.Zero, Size), UiTokens.Surface0);
|
||||
var grid = Document?.Grid;
|
||||
if (grid == null)
|
||||
return;
|
||||
frame.Actors.Add(new TileActorPose { Id = (locked ? "locked/" : "own/") + mapEvent.Id,
|
||||
Position = new Vector2(mapEvent.X, mapEvent.Y), Visible = true, Look = builder.EventLook(mapEvent, Tileset),
|
||||
Facing = (int)mapEvent.Dir, Charset = !string.IsNullOrEmpty(mapEvent.Sprite), Tint = locked ? LockedModulate : Colors.White });
|
||||
}
|
||||
|
||||
DrawRect(new Rect2(Vector2.Zero, new Vector2(grid.Width, grid.Height) * CellSize), Colors.Black);
|
||||
DrawLayer(RpgLayer.Back);
|
||||
DrawLayer(RpgLayer.Main);
|
||||
DrawEvents();
|
||||
DrawLayer(RpgLayer.Front);
|
||||
DrawSpawn();
|
||||
if (ActiveLayer == RpgEditLayer.Collision)
|
||||
DrawCollision();
|
||||
public new void QueueRedraw() { base.QueueRedraw(); overlay?.QueueRedraw(); }
|
||||
|
||||
public override void _Draw() => DrawRect(new Rect2(Vector2.Zero, Size), UiTokens.Surface0);
|
||||
|
||||
/// <summary>Mouse and automation coordinates use the real presentation transform, including negative iso X.</summary>
|
||||
public Vector2 ProjectCell(Vector2 cell) => preview?.ProjectToView(new Vector3(cell.X, cell.Y, 0)) ?? cell * CellSize;
|
||||
|
||||
public void DrawOverlays(CanvasItem target)
|
||||
{
|
||||
if (Document == null || projection == null) return;
|
||||
var grid = Document.Grid;
|
||||
var unit = projection.Unit;
|
||||
if (ShowGrid)
|
||||
DrawGrid();
|
||||
if (hover.HasValue && grid.InBounds(hover.Value.X, hover.Value.Y))
|
||||
DrawRect(CellRect(hover.Value.X, hover.Value.Y), UiTokens.AccentBright, false, 2f);
|
||||
}
|
||||
|
||||
private void DrawLayer(RpgLayer layer)
|
||||
{
|
||||
if (Atlas == null || Tileset == null)
|
||||
return;
|
||||
var dim = ActiveLayer <= RpgEditLayer.Front && (int)ActiveLayer != (int)layer;
|
||||
var modulate = new Color(1f, 1f, 1f, dim ? 0.45f : 1f);
|
||||
var grid = Document.Grid;
|
||||
for (var y = 0; y < grid.Height; y++)
|
||||
{
|
||||
for (var x = 0; x < grid.Width; x++)
|
||||
{
|
||||
var index = grid.GetTile(layer, x, y);
|
||||
if (index < 0)
|
||||
continue;
|
||||
// Autotiles are assembled from their neighbours exactly like in the game.
|
||||
var autotile = Tileset.AutotileOf(index);
|
||||
if (autotile == null)
|
||||
DrawTextureRectRegion(Atlas, CellRect(x, y), TileRegion(index), modulate);
|
||||
else if (textures.Tileset(autotile.Autotile) is { } block)
|
||||
RpgAutotiles.Draw(this, block, CellRect(x, y), RpgAutotiles.QuartersAt(grid, layer, x, y, index), Tileset.TileSize, modulate);
|
||||
}
|
||||
for (var x = 0; x <= grid.Width; x++) target.DrawLine(P(x, 0), P(x, grid.Height), GridColor);
|
||||
for (var y = 0; y <= grid.Height; y++) target.DrawLine(P(0, y), P(grid.Width, y), GridColor);
|
||||
}
|
||||
}
|
||||
|
||||
private void DrawEvents()
|
||||
{
|
||||
foreach (var mapEvent in Document.LockedEvents)
|
||||
DrawEvent(mapEvent, true);
|
||||
foreach (var mapEvent in Document.Events)
|
||||
DrawEvent(mapEvent, false);
|
||||
}
|
||||
|
||||
/// <summary>One event: its look, then on the events layer a frame (dashed grey for a locked one).</summary>
|
||||
private void DrawEvent(RpgMapEvent mapEvent, bool locked)
|
||||
{
|
||||
var modulate = locked ? LockedModulate : Colors.White;
|
||||
var rect = CellRect(mapEvent.X, mapEvent.Y);
|
||||
var charset = string.IsNullOrEmpty(mapEvent.Sprite) ? null : textures.Charset(mapEvent.Sprite);
|
||||
if (charset != null)
|
||||
{
|
||||
var frame = new Vector2(charset.GetWidth() / 3f, charset.GetHeight() / 4f);
|
||||
var source = new Rect2(new Vector2(frame.X, frame.Y * (int)mapEvent.Dir), frame);
|
||||
var size = frame * Zoom;
|
||||
var target = new Rect2(rect.Position + new Vector2((rect.Size.X - size.X) / 2f, rect.Size.Y - size.Y), size);
|
||||
DrawTextureRectRegion(charset, target, source, modulate);
|
||||
}
|
||||
else if (mapEvent.Tile >= 0 && Atlas != null && Tileset != null)
|
||||
{
|
||||
DrawTextureRectRegion(Atlas, rect, TileRegion(mapEvent.Tile), modulate);
|
||||
}
|
||||
else if (!string.IsNullOrEmpty(mapEvent.Icon) && textures.Icon(mapEvent.Icon) is { } icon)
|
||||
{
|
||||
// Fitted like RpgActor.UseIcon: three quarters of the cell, centred.
|
||||
var fit = CellSize * 0.75f / Mathf.Max(1, Mathf.Max(icon.GetWidth(), icon.GetHeight()));
|
||||
var size = icon.GetSize() * fit;
|
||||
DrawTextureRect(icon, new Rect2(rect.GetCenter() - size / 2f, size), false, modulate);
|
||||
}
|
||||
else
|
||||
{
|
||||
DrawRect(rect.Grow(-CellSize * 0.15f), MarkerColor * modulate);
|
||||
}
|
||||
|
||||
if (ActiveLayer == RpgEditLayer.Events && locked)
|
||||
DrawDashedRect(rect.Grow(-1f));
|
||||
else if (ActiveLayer == RpgEditLayer.Events)
|
||||
DrawRect(rect, mapEvent == SelectedEvent ? UiTokens.AccentBright : UiTokens.Alpha(UiTokens.White, 0.5f), false, mapEvent == SelectedEvent ? 3f : 1f);
|
||||
}
|
||||
|
||||
private void DrawDashedRect(Rect2 rect)
|
||||
{
|
||||
var color = UiTokens.Alpha(UiTokens.White, 0.6f);
|
||||
DrawDashedLine(rect.Position, new Vector2(rect.End.X, rect.Position.Y), color, 1f, 4f);
|
||||
DrawDashedLine(new Vector2(rect.End.X, rect.Position.Y), rect.End, color, 1f, 4f);
|
||||
DrawDashedLine(rect.End, new Vector2(rect.Position.X, rect.End.Y), color, 1f, 4f);
|
||||
DrawDashedLine(new Vector2(rect.Position.X, rect.End.Y), rect.Position, color, 1f, 4f);
|
||||
}
|
||||
|
||||
private void DrawSpawn()
|
||||
{
|
||||
var spawn = Document.Map.Spawn;
|
||||
if (spawn == null)
|
||||
return;
|
||||
var rect = CellRect(spawn.X, spawn.Y);
|
||||
DrawCircle(rect.GetCenter(), CellSize * 0.18f, UiTokens.Ok);
|
||||
DrawArc(rect.GetCenter(), CellSize * 0.18f, 0f, Mathf.Tau, 24, Colors.Black, 1.5f);
|
||||
}
|
||||
|
||||
private void DrawCollision()
|
||||
{
|
||||
var grid = Document.Grid;
|
||||
for (var y = 0; y < grid.Height; y++)
|
||||
{
|
||||
if (ActiveLayer == RpgEditLayer.Collision)
|
||||
for (var y = 0; y < grid.Height; y++)
|
||||
for (var x = 0; x < grid.Width; x++)
|
||||
{
|
||||
var code = grid.GetCollision(x, y);
|
||||
if (code == RpgCollision.Block)
|
||||
DrawRect(CellRect(x, y), BlockColor);
|
||||
else if (code == RpgCollision.Open)
|
||||
DrawRect(CellRect(x, y), OpenColor);
|
||||
else if (!RpgPassability.IsWalkable(grid, Tileset, x, y))
|
||||
DrawRect(CellRect(x, y).Grow(-CellSize * 0.35f), BlockColor);
|
||||
if (code == RpgCollision.Block || code == RpgCollision.Open || !RpgPassability.IsWalkable(grid, Tileset, x, y))
|
||||
target.DrawColoredPolygon(Polygon(x, y, 1, 1), code == RpgCollision.Open ? OpenColor : BlockColor);
|
||||
}
|
||||
foreach (var mapEvent in Document.LockedEvents) DrawEventMarker(target, mapEvent, true);
|
||||
foreach (var mapEvent in Document.Events) DrawEventMarker(target, mapEvent, false);
|
||||
if (Document.Map.Spawn is { } spawn)
|
||||
{
|
||||
var center = P(spawn.X + 0.5f, spawn.Y + 0.5f);
|
||||
target.DrawCircle(center, unit * 0.18f, UiTokens.Ok);
|
||||
target.DrawArc(center, unit * 0.18f, 0, Mathf.Tau, 24, Colors.Black);
|
||||
}
|
||||
if (!hover.HasValue || !grid.InBounds(hover.Value.X, hover.Value.Y)) return;
|
||||
var cell = hover.Value;
|
||||
var stamp = SelectedStamp ?? RpgTileStamp.Single(SelectedTile);
|
||||
if (!Placing && Tool == RpgEditTool.Pencil && ActiveLayer <= RpgEditLayer.Front && Atlas != null)
|
||||
for (var y = 0; y < stamp.Height; y++)
|
||||
for (var x = 0; x < stamp.Width; x++)
|
||||
{
|
||||
var id = stamp.TileAt(x, y);
|
||||
if (id < 0) continue;
|
||||
var info = Tileset.TileOf(id);
|
||||
var texture = Atlas;
|
||||
var source = RpgRenderSceneBuilder.TileRegion(Tileset, id);
|
||||
if (info?.Surface is { } patch)
|
||||
{
|
||||
texture = textures.Tileset(Tileset.SurfaceImage);
|
||||
source = new Rect2(patch * Tileset.SurfaceSize + (cell.X + x) * Tileset.TileSize % Tileset.SurfaceSize,
|
||||
(cell.Y + y) * Tileset.TileSize % Tileset.SurfaceSize, Tileset.TileSize, Tileset.TileSize);
|
||||
}
|
||||
else if (!string.IsNullOrEmpty(info?.Autotile))
|
||||
{
|
||||
texture = textures.Autotile(info.Autotile, Tileset.TileSize, cell.X + x, cell.Y + y);
|
||||
source = RpgAutotiles.PreviewRect(Tileset.TileSize);
|
||||
}
|
||||
TileDrawing.Draw(target, new TileVisual { Texture = texture, Source = source,
|
||||
Bounds = new Rect2(cell.X + x, cell.Y + y, 1, 1) }, projection, new Color(1, 1, 1, 0.5f));
|
||||
}
|
||||
Outline(target, Polygon(cell.X, cell.Y, ActiveLayer <= RpgEditLayer.Front ? stamp.Width : 1,
|
||||
ActiveLayer <= RpgEditLayer.Front ? stamp.Height : 1), UiTokens.AccentBright, false);
|
||||
}
|
||||
|
||||
private void DrawGrid()
|
||||
private void DrawEventMarker(CanvasItem target, RpgMapEvent mapEvent, bool locked)
|
||||
{
|
||||
var grid = Document.Grid;
|
||||
var size = new Vector2(grid.Width, grid.Height) * CellSize;
|
||||
for (var x = 0; x <= grid.Width; x++)
|
||||
DrawLine(new Vector2(x * CellSize, 0f), new Vector2(x * CellSize, size.Y), GridColor);
|
||||
for (var y = 0; y <= grid.Height; y++)
|
||||
DrawLine(new Vector2(0f, y * CellSize), new Vector2(size.X, y * CellSize), GridColor);
|
||||
var polygon = Polygon(mapEvent.X, mapEvent.Y, 1, 1);
|
||||
if (string.IsNullOrEmpty(mapEvent.Sprite) && mapEvent.Tile < 0 && string.IsNullOrEmpty(mapEvent.Icon))
|
||||
target.DrawColoredPolygon(polygon, MarkerColor);
|
||||
if (ActiveLayer != RpgEditLayer.Events) return;
|
||||
Outline(target, polygon, mapEvent == SelectedEvent ? UiTokens.AccentBright : UiTokens.Alpha(UiTokens.White, 0.6f), locked);
|
||||
}
|
||||
|
||||
private Rect2 CellRect(int x, int y) => new(new Vector2(x, y) * CellSize, new Vector2(CellSize, CellSize));
|
||||
|
||||
private Rect2 TileRegion(int index)
|
||||
private static void Outline(CanvasItem target, Vector2[] polygon, Color color, bool dashed)
|
||||
{
|
||||
var coords = RpgTileSetBuilder.AtlasCoords(Tileset, index);
|
||||
return new Rect2(coords * Tileset.TileSize, new Vector2(Tileset.TileSize, Tileset.TileSize));
|
||||
for (var i = 0; i < polygon.Length; i++)
|
||||
if (dashed) target.DrawDashedLine(polygon[i], polygon[(i + 1) % polygon.Length], color, 1, 4);
|
||||
else target.DrawLine(polygon[i], polygon[(i + 1) % polygon.Length], color, 1.5f);
|
||||
}
|
||||
|
||||
private Vector2I CellAt(Vector2 position) => new((int)Mathf.Floor(position.X / CellSize), (int)Mathf.Floor(position.Y / CellSize));
|
||||
private Vector2 P(float x, float y) => TileDrawing.Project(projection, new Vector2(x, y));
|
||||
private Vector2[] Polygon(int x, int y, int width, int height)
|
||||
=> TileDrawing.CellPolygon(projection, new Rect2(x, y, width, height));
|
||||
private Vector2I CellAt(Vector2 position)
|
||||
=> preview != null && preview.TryPickGround(position, out var cell) ? cell : new Vector2I(-1, -1);
|
||||
|
||||
private void OnGuiInput(InputEvent @event)
|
||||
{
|
||||
@@ -391,8 +416,13 @@ namespace Cthangover.Tools.RpgEditor
|
||||
{
|
||||
var tile = erase ? -1 : SelectedTile;
|
||||
var collision = erase ? RpgCollision.Auto : SelectedCollision;
|
||||
if (Document.Paint(ActiveLayer, cell.X, cell.Y, tile, collision))
|
||||
QueueRedraw();
|
||||
var changed = !erase && ActiveLayer <= RpgEditLayer.Front && SelectedStamp != null
|
||||
? Document.Stamp(ActiveLayer, cell.X, cell.Y, SelectedStamp) > 0
|
||||
: Document.Paint(ActiveLayer, cell.X, cell.Y, tile, collision);
|
||||
if (changed)
|
||||
{
|
||||
Refresh();
|
||||
}
|
||||
}
|
||||
|
||||
private void Drag(Vector2I cell)
|
||||
@@ -406,6 +436,7 @@ namespace Cthangover.Tools.RpgEditor
|
||||
}
|
||||
dragged.X = cell.X;
|
||||
dragged.Y = cell.Y;
|
||||
Refresh();
|
||||
EventSelected?.Invoke(dragged);
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
namespace Cthangover.Tools.RpgEditor
|
||||
{
|
||||
using Godot;
|
||||
/// <summary>Authoring marks render above the shared world, without intercepting canvas input.</summary>
|
||||
public partial class RpgMapEditorOverlay : Node2D
|
||||
{
|
||||
private readonly RpgMapEditorCanvas canvas;
|
||||
public RpgMapEditorOverlay(RpgMapEditorCanvas canvas) { this.canvas = canvas; ZIndex = 10; }
|
||||
public override void _Draw() => canvas.DrawOverlays(this);
|
||||
}
|
||||
}
|
||||
@@ -10,6 +10,7 @@ using Cthangover.Core.Utils;
|
||||
using Cthangover.Mods.Interface.Theming;
|
||||
using Cthangover.Mods.Interface.Tool;
|
||||
using Cthangover.Mods.Rpg;
|
||||
using Cthangover.Mods.TileRender;
|
||||
using Cthangover.Tools.Common;
|
||||
using Cthangover.Tools.Services;
|
||||
using Godot;
|
||||
@@ -19,7 +20,7 @@ namespace Cthangover.Tools.RpgEditor
|
||||
/// <summary>
|
||||
/// Editor of RPG tile maps (<c>maps/*.json</c>, kind <c>rpg_map</c>).
|
||||
/// Layout: toolbar (file, save, layer, brush, grid, zoom, undo/redo, play),
|
||||
/// the tile palette, the game's characters and items as tabs on the left, the
|
||||
/// the tile palette, the game's characters, items and all map appearances as tabs on the left, the
|
||||
/// map in the middle, the map's and the selected event's properties on the right.
|
||||
/// A character is put on the map as an NPC or an enemy, an item as a pickup:
|
||||
/// pick it, press the button, click a cell (<see cref="RpgMapDocument.AddCharacter"/>,
|
||||
@@ -53,6 +54,7 @@ namespace Cthangover.Tools.RpgEditor
|
||||
private ModFileSelector fileSelector;
|
||||
private TabContainer sideTabs;
|
||||
private RpgTilePalette palette;
|
||||
private OptionButton rendererPicker;
|
||||
private SpinBox itemCount;
|
||||
private Func<Vector2I, RpgMapEvent> placeAt;
|
||||
private string placedKey;
|
||||
@@ -79,6 +81,14 @@ namespace Cthangover.Tools.RpgEditor
|
||||
fileSelector.Refresh();
|
||||
}
|
||||
|
||||
/// <summary>Child pickers release their preview regions on exit before the window releases the shared source images.</summary>
|
||||
protected override void Cleanup()
|
||||
{
|
||||
placeAt = null;
|
||||
textures.Dispose();
|
||||
base.Cleanup();
|
||||
}
|
||||
|
||||
// ── layout ─────────────────────────────────────────────────
|
||||
|
||||
private void BuildUi()
|
||||
@@ -98,6 +108,7 @@ namespace Cthangover.Tools.RpgEditor
|
||||
paletteColumn.AddChild(ToolUi.Section("tools/rpg_editor/section_palette"));
|
||||
palette = new RpgTilePalette();
|
||||
palette.TileSelected += OnPaletteTile;
|
||||
palette.StampSelected += OnPaletteStamp;
|
||||
palette.CollisionSelected += OnPaletteCollision;
|
||||
paletteColumn.AddChild(palette);
|
||||
sideTabs.AddChild(paletteColumn);
|
||||
@@ -120,6 +131,12 @@ namespace Cthangover.Tools.RpgEditor
|
||||
items.AddAction("tools/rpg_editor/place_item", "tools/rpg_editor/place_item_tip", PlaceItem);
|
||||
sideTabs.AddChild(items);
|
||||
sideTabs.SetTabTitle(2, ToolUi.Tr("tools/rpg_editor/tab_items"));
|
||||
|
||||
var appearances = new RpgEntityPicker("tools/rpg_editor/appearance_search", "tools/rpg_editor/appearances_hint") { Name = "Appearances" };
|
||||
appearances.Source = LoadAppearances;
|
||||
appearances.AddAction("tools/rpg_editor/place_appearance", "tools/rpg_editor/place_appearance_tip", PlaceAppearance);
|
||||
sideTabs.AddChild(appearances);
|
||||
sideTabs.SetTabTitle(3, ToolUi.Tr("tools/rpg_editor/tab_appearances"));
|
||||
sideTabs.TabChanged += OnSideTabChanged;
|
||||
main.AddChild(CreateSidebar(leftColumn, 400));
|
||||
|
||||
@@ -170,6 +187,24 @@ namespace Cthangover.Tools.RpgEditor
|
||||
AddToolButton(toolbar, RpgEditTool.Fill, "tools/rpg_editor/tool_fill");
|
||||
toolbar.AddChild(ToolUi.Divider());
|
||||
|
||||
rendererPicker = new OptionButton { Name = "RendererPicker" };
|
||||
var wantedRenderer = Cthangover.Core.Games.ActiveGame.GetSetting("tile.renderer") ?? "topdown";
|
||||
foreach (var arg in OS.GetCmdlineUserArgs()) if (arg.StartsWith("--editor-renderer=")) wantedRenderer = arg.Substring(18);
|
||||
foreach (var id in TileRendererRegistry.Instance.Ids)
|
||||
{
|
||||
var index = rendererPicker.ItemCount;
|
||||
var label = id switch
|
||||
{
|
||||
"topdown" => ToolUi.Tr("tools/rpg_editor/view_topdown"),
|
||||
"isometric" => ToolUi.Tr("tools/rpg_editor/view_isometric"),
|
||||
_ => id
|
||||
};
|
||||
rendererPicker.AddItem(label);
|
||||
rendererPicker.SetItemMetadata(index, id);
|
||||
if (id == wantedRenderer) rendererPicker.Select(index);
|
||||
}
|
||||
rendererPicker.ItemSelected += OnRendererSelected;
|
||||
toolbar.AddChild(rendererPicker);
|
||||
gridCheck = new CheckBox { Text = ToolUi.Tr("tools/rpg_editor/grid"), ButtonPressed = true };
|
||||
gridCheck.Toggled += OnGridToggled;
|
||||
toolbar.AddChild(gridCheck);
|
||||
@@ -432,10 +467,17 @@ namespace Cthangover.Tools.RpgEditor
|
||||
private void OnPaletteTile(int index)
|
||||
{
|
||||
canvas.SelectedTile = index;
|
||||
canvas.SelectedStamp = RpgTileStamp.Single(index);
|
||||
if (canvas.ActiveLayer > RpgEditLayer.Front)
|
||||
SelectLayer(RpgEditLayer.Main);
|
||||
}
|
||||
|
||||
private void OnPaletteStamp(RpgTileStamp stamp)
|
||||
{
|
||||
canvas.SelectedStamp = stamp;
|
||||
canvas.QueueRedraw();
|
||||
}
|
||||
|
||||
private void OnPaletteCollision(char code) => canvas.SelectedCollision = code;
|
||||
|
||||
private void OnSideTabChanged(long tab)
|
||||
@@ -448,6 +490,14 @@ namespace Cthangover.Tools.RpgEditor
|
||||
|
||||
private List<RpgPickerEntry> LoadItems() => RpgItemCatalog.Load(textures);
|
||||
|
||||
private List<RpgPickerEntry> LoadAppearances() => RpgAppearanceCatalog.Load(textures);
|
||||
|
||||
private void PlaceAppearance(RpgPickerEntry entry)
|
||||
{
|
||||
var id = "npc_" + new string(entry.Look.Select(c => char.IsLetterOrDigit(c) || c == '_' ? c : '_').ToArray());
|
||||
BeginPlacing(entry, "tools/rpg_editor/placed_npc", cell => document.AddCharacter(cell.X, cell.Y, id, entry.Look, false));
|
||||
}
|
||||
|
||||
private void PlaceNpc(RpgPickerEntry entry)
|
||||
=> BeginPlacing(entry, "tools/rpg_editor/placed_npc", cell => document.AddCharacter(cell.X, cell.Y, entry.Id, entry.Look, false));
|
||||
|
||||
@@ -492,6 +542,7 @@ namespace Cthangover.Tools.RpgEditor
|
||||
private void OnTilePicked(int index)
|
||||
{
|
||||
canvas.SelectedTile = index;
|
||||
canvas.SelectedStamp = RpgTileStamp.Single(index);
|
||||
palette.Select(index);
|
||||
}
|
||||
|
||||
@@ -505,7 +556,7 @@ namespace Cthangover.Tools.RpgEditor
|
||||
palette.SetCollisionMode(layer == RpgEditLayer.Collision);
|
||||
foreach (var button in toolButtons.Values)
|
||||
button.Disabled = layer == RpgEditLayer.Events;
|
||||
canvas.QueueRedraw();
|
||||
canvas.Refresh();
|
||||
}
|
||||
|
||||
private void SelectTool(RpgEditTool tool)
|
||||
@@ -515,6 +566,12 @@ namespace Cthangover.Tools.RpgEditor
|
||||
pair.Value.SetPressedNoSignal(pair.Key == tool);
|
||||
}
|
||||
|
||||
private void OnRendererSelected(long index)
|
||||
{
|
||||
canvas.SetRenderer(rendererPicker.GetItemMetadata((int)index).AsString());
|
||||
UpdateZoomLabel();
|
||||
}
|
||||
|
||||
private void OnGridToggled(bool on)
|
||||
{
|
||||
canvas.ShowGrid = on;
|
||||
|
||||
@@ -2,10 +2,10 @@ using Godot;
|
||||
|
||||
namespace Cthangover.Tools.RpgEditor
|
||||
{
|
||||
/// <summary>One row of an <see cref="RpgEntityPicker"/>: a character or an item of the game.</summary>
|
||||
/// <summary>One row of an <see cref="RpgEntityPicker"/>: a character, item or map appearance.</summary>
|
||||
public class RpgPickerEntry
|
||||
{
|
||||
/// <summary>Entity id (what <c>rpg.battle enemies=</c> or <c>item.add id=</c> takes).</summary>
|
||||
/// <summary>Entity id for character/item entries; a charset path for appearance-only entries.</summary>
|
||||
public string Id { get; init; }
|
||||
|
||||
/// <summary>Display name in the current language (the id when the entity has none).</summary>
|
||||
@@ -16,5 +16,8 @@ namespace Cthangover.Tools.RpgEditor
|
||||
|
||||
/// <summary>Picture of the row, or null.</summary>
|
||||
public Texture2D Icon { get; init; }
|
||||
|
||||
/// <summary>True for an entry-owned atlas preview; false for a texture borrowed from the window cache.</summary>
|
||||
public bool OwnsIcon { get; init; }
|
||||
}
|
||||
}
|
||||
+90
-7
@@ -17,7 +17,8 @@ namespace Cthangover.Tools.RpgEditor
|
||||
/// </summary>
|
||||
public partial class RpgTilePalette : Control
|
||||
{
|
||||
private const float Scale = 1.5f;
|
||||
// Keep wide architectural blocks fully visible in the standard sidebar.
|
||||
private float Scale => tileset == null ? 1.5f : Math.Min(1.5f, 384f / Math.Max(1, tileset.Columns * tileset.TileSize));
|
||||
private const float CodeHeight = 44f;
|
||||
private const float AutotileGap = 8f;
|
||||
|
||||
@@ -28,6 +29,15 @@ namespace Cthangover.Tools.RpgEditor
|
||||
private RpgTileset tileset;
|
||||
private Texture2D atlas;
|
||||
private bool collisionMode;
|
||||
private Vector2I? selectionStart;
|
||||
private Rect2I? selection;
|
||||
private bool dragged;
|
||||
|
||||
/// <summary>The current rectangular brush in logical map ids.</summary>
|
||||
public RpgTileStamp SelectedStamp { get; private set; } = RpgTileStamp.Single(0);
|
||||
|
||||
/// <summary>A dragged rectangle changed the whole brush.</summary>
|
||||
public event Action<RpgTileStamp> StampSelected;
|
||||
|
||||
/// <summary>Selected atlas index.</summary>
|
||||
public int SelectedTile { get; private set; }
|
||||
@@ -54,15 +64,18 @@ namespace Cthangover.Tools.RpgEditor
|
||||
{
|
||||
tileset = value;
|
||||
atlas = image;
|
||||
selection = null;
|
||||
selectionStart = null;
|
||||
autotiles.Clear();
|
||||
if (value != null)
|
||||
autotiles.AddRange(value.Tiles.Where(t => !string.IsNullOrEmpty(t.Autotile)));
|
||||
autotiles.AddRange(value.Tiles.Where(t => !string.IsNullOrEmpty(t.Autotile) || t.Surface != null));
|
||||
Relayout();
|
||||
}
|
||||
|
||||
/// <summary>Switches between the tile atlas and the collision codes.</summary>
|
||||
public void SetCollisionMode(bool value)
|
||||
{
|
||||
selectionStart = null;
|
||||
collisionMode = value;
|
||||
Relayout();
|
||||
}
|
||||
@@ -71,6 +84,9 @@ namespace Cthangover.Tools.RpgEditor
|
||||
public void Select(int index)
|
||||
{
|
||||
SelectedTile = index;
|
||||
SelectedStamp = RpgTileStamp.Single(index);
|
||||
selection = null;
|
||||
selectionStart = null;
|
||||
QueueRedraw();
|
||||
}
|
||||
|
||||
@@ -88,18 +104,24 @@ namespace Cthangover.Tools.RpgEditor
|
||||
|
||||
var cell = tileset.TileSize * Scale;
|
||||
DrawTextureRect(atlas, new Rect2(Vector2.Zero, atlas.GetSize() * Scale), false);
|
||||
foreach (var tile in tileset.Tiles.Where(t => t.Solid && string.IsNullOrEmpty(t.Autotile)))
|
||||
foreach (var tile in tileset.Tiles.Where(t => t.Solid && string.IsNullOrEmpty(t.Autotile) && t.Surface == null))
|
||||
DrawSolidMark((Vector2)RpgTileSetBuilder.AtlasCoords(tileset, tile.Index) * cell, cell);
|
||||
for (var i = 0; i < autotiles.Count; i++)
|
||||
{
|
||||
if (textures.Tileset(autotiles[i].Autotile) is { } block)
|
||||
if (autotiles[i].Surface is { } patch && textures.Tileset(tileset.SurfaceImage) is { } surface)
|
||||
DrawTextureRectRegion(surface, new Rect2(Slot(i), new Vector2(cell, cell)),
|
||||
new Rect2(patch * tileset.SurfaceSize, 0, tileset.TileSize, tileset.TileSize));
|
||||
else if (textures.Tileset(autotiles[i].Autotile) is { } block)
|
||||
DrawTextureRectRegion(block, new Rect2(Slot(i), new Vector2(cell, cell)), RpgAutotiles.PreviewRect(tileset.TileSize));
|
||||
if (autotiles[i].Solid)
|
||||
DrawSolidMark(Slot(i), cell);
|
||||
}
|
||||
var slot = autotiles.FindIndex(t => t.Index == SelectedTile);
|
||||
var selected = slot >= 0 ? Slot(slot) : (Vector2)RpgTileSetBuilder.AtlasCoords(tileset, SelectedTile) * cell;
|
||||
DrawRect(new Rect2(selected, new Vector2(cell, cell)), UiTokens.AccentBright, false, 3f);
|
||||
var highlight = selection is { } rectangle
|
||||
? new Rect2((Vector2)rectangle.Position * cell, (Vector2)rectangle.Size * cell)
|
||||
: new Rect2(selected, new Vector2(cell, cell));
|
||||
DrawRect(highlight, UiTokens.AccentBright, false, 3f);
|
||||
}
|
||||
|
||||
private void DrawSolidMark(Vector2 position, float cell)
|
||||
@@ -135,9 +157,19 @@ namespace Cthangover.Tools.RpgEditor
|
||||
{
|
||||
if (@event is InputEventMouseMotion motion && !collisionMode && tileset != null)
|
||||
{
|
||||
if (selectionStart.HasValue && (dragged || AtlasCellAt(motion.Position) != selectionStart.Value))
|
||||
{
|
||||
dragged = true;
|
||||
SelectRectangle(motion.Position);
|
||||
}
|
||||
TooltipText = Describe(IndexAt(motion.Position));
|
||||
return;
|
||||
}
|
||||
if (@event is InputEventMouseButton { Pressed: false, ButtonIndex: MouseButton.Left })
|
||||
{
|
||||
selectionStart = null;
|
||||
return;
|
||||
}
|
||||
if (@event is not InputEventMouseButton { Pressed: true, ButtonIndex: MouseButton.Left } button)
|
||||
return;
|
||||
|
||||
@@ -153,16 +185,63 @@ namespace Cthangover.Tools.RpgEditor
|
||||
else if (tileset != null && atlas != null)
|
||||
{
|
||||
var index = IndexAt(button.Position);
|
||||
if (index >= 0 && (index < RpgTileSetBuilder.TileCount(tileset, atlas) || tileset.AutotileOf(index) != null))
|
||||
if (index >= 0 && (index < RpgTileSetBuilder.TileCount(tileset, atlas) || tileset.TileOf(index) != null))
|
||||
{
|
||||
SelectedTile = index;
|
||||
SelectedStamp = RpgTileStamp.Single(index);
|
||||
selection = null;
|
||||
selectionStart = button.Position.Y < atlas.GetHeight() * Scale
|
||||
? AtlasCellAt(button.Position) : null;
|
||||
dragged = false;
|
||||
SelectGroupOf(index);
|
||||
TileSelected?.Invoke(index);
|
||||
StampSelected?.Invoke(SelectedStamp);
|
||||
}
|
||||
}
|
||||
QueueRedraw();
|
||||
AcceptEvent();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A click on any cell of a visual group picks the whole object, so it is stamped complete: a partial
|
||||
/// pattern does not match the group and the isometric view falls back to separate per-cell sprites.
|
||||
/// </summary>
|
||||
private void SelectGroupOf(int index)
|
||||
{
|
||||
var group = tileset.VisualGroups?.Find(g => g.Pattern != null && Array.IndexOf(g.Pattern, index) >= 0);
|
||||
if (group == null || RpgVisualGroupValidator.Error(group) != null)
|
||||
return;
|
||||
SelectedStamp = new RpgTileStamp(group.Width, group.Height, group.Pattern);
|
||||
SelectedTile = group.Pattern[0];
|
||||
var origin = RpgTileSetBuilder.AtlasCoords(tileset, group.Pattern[0]);
|
||||
var packed = true;
|
||||
for (var y = 0; y < group.Height; y++)
|
||||
for (var x = 0; x < group.Width; x++)
|
||||
packed &= RpgTileSetBuilder.AtlasCoords(tileset, group.Pattern[y * group.Width + x]) == origin + new Vector2I(x, y);
|
||||
selection = packed ? new Rect2I(origin, group.Width, group.Height) : null;
|
||||
}
|
||||
|
||||
private Vector2I AtlasCellAt(Vector2 position)
|
||||
{
|
||||
var cell = tileset.TileSize * Scale;
|
||||
return new Vector2I(Math.Clamp((int)(position.X / cell), 0, tileset.Columns - 1),
|
||||
Math.Clamp((int)(position.Y / cell), 0, atlas.GetHeight() / tileset.TileSize - 1));
|
||||
}
|
||||
|
||||
private void SelectRectangle(Vector2 position)
|
||||
{
|
||||
var end = AtlasCellAt(position);
|
||||
var start = selectionStart.Value;
|
||||
var left = Math.Min(start.X, end.X);
|
||||
var top = Math.Min(start.Y, end.Y);
|
||||
selection = new Rect2I(left, top, Math.Abs(start.X - end.X) + 1, Math.Abs(start.Y - end.Y) + 1);
|
||||
SelectedStamp = RpgTileStamp.FromAtlas(tileset, selection.Value);
|
||||
SelectedTile = SelectedStamp.TileAt(0, 0);
|
||||
TileSelected?.Invoke(SelectedTile);
|
||||
StampSelected?.Invoke(SelectedStamp);
|
||||
QueueRedraw();
|
||||
}
|
||||
|
||||
private float AutotileTop => atlas == null ? 0f : atlas.GetHeight() * Scale + (autotiles.Count > 0 ? AutotileGap : 0f);
|
||||
|
||||
private int AutotileRows => (autotiles.Count + Math.Max(1, tileset.Columns) - 1) / Math.Max(1, tileset.Columns);
|
||||
@@ -184,7 +263,11 @@ namespace Cthangover.Tools.RpgEditor
|
||||
}
|
||||
var column = (int)(position.X / cell);
|
||||
var row = (int)(position.Y / cell);
|
||||
return column < 0 || column >= tileset.Columns || row < 0 ? -1 : row * tileset.Columns + column;
|
||||
if (column < 0 || column >= tileset.Columns || row < 0 || position.Y >= atlas.GetHeight() * Scale)
|
||||
return -1;
|
||||
var physical = row * tileset.Columns + column;
|
||||
return tileset.Tiles.FirstOrDefault(t => t.AtlasIndex == physical)?.Index
|
||||
?? (tileset.Tiles.Any(t => t.AtlasIndex != null) ? -1 : physical);
|
||||
}
|
||||
|
||||
private string Describe(int index)
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
using System;
|
||||
using Cthangover.Mods.Rpg;
|
||||
using Godot;
|
||||
|
||||
namespace Cthangover.Tools.RpgEditor
|
||||
{
|
||||
/// <summary>A rectangular palette selection in logical map ids; empty atlas slots are skipped when stamping.</summary>
|
||||
public class RpgTileStamp
|
||||
{
|
||||
private readonly int[] tiles;
|
||||
|
||||
/// <summary>Selection width in cells.</summary>
|
||||
public int Width { get; }
|
||||
|
||||
/// <summary>Selection height in cells.</summary>
|
||||
public int Height { get; }
|
||||
|
||||
/// <summary>Copies a rectangular set of logical tile ids; negative ids denote gaps.</summary>
|
||||
public RpgTileStamp(int width, int height, int[] values)
|
||||
{
|
||||
if (width < 1 || height < 1 || values == null || values.Length != width * height)
|
||||
throw new ArgumentException("A stamp must contain exactly width times height cells.");
|
||||
Width = width;
|
||||
Height = height;
|
||||
tiles = (int[])values.Clone();
|
||||
}
|
||||
|
||||
/// <summary>Logical id at the given offset, or -1 outside the selection.</summary>
|
||||
public int TileAt(int x, int y) => x >= 0 && y >= 0 && x < Width && y < Height ? tiles[y * Width + x] : -1;
|
||||
|
||||
/// <summary>Single-tile brush, also used for surface materials and the pipette.</summary>
|
||||
public static RpgTileStamp Single(int index) => new(1, 1, new[] { index });
|
||||
|
||||
/// <summary>Converts physical object-atlas coordinates into logical ids without exposing padding cells.</summary>
|
||||
public static RpgTileStamp FromAtlas(RpgTileset tileset, Rect2I rectangle)
|
||||
{
|
||||
var values = new int[rectangle.Size.X * rectangle.Size.Y];
|
||||
var packed = tileset.Tiles.Exists(t => t.AtlasIndex != null);
|
||||
for (var y = 0; y < rectangle.Size.Y; y++)
|
||||
for (var x = 0; x < rectangle.Size.X; x++)
|
||||
{
|
||||
var physical = (rectangle.Position.Y + y) * tileset.Columns + rectangle.Position.X + x;
|
||||
values[y * rectangle.Size.X + x] = packed
|
||||
? tileset.Tiles.Find(t => t.AtlasIndex == physical)?.Index ?? -1 : physical;
|
||||
}
|
||||
return new RpgTileStamp(rectangle.Size.X, rectangle.Size.Y, values);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user