RPG world mod: tile maps, grid walking, map events, battles
Maps of square tiles in three layers plus a collision layer, mounted over the 2D shell when their scene is entered. WASD walking, E/F action events, touch teleports, story-flag conditions, random encounters, rpg.battle and rpg.teleport actions, position saving. Integration suite rpg_walk.
This commit is contained in:
commit
5ca225a2d7
43 files changed
+2066
No files matched your search
@@ -0,0 +1,20 @@
|
||||
namespace Cthangover.Mods.Rpg
|
||||
{
|
||||
/// <summary>
|
||||
/// Cell codes of the map's system collision layer. It is a per-cell override
|
||||
/// of the tiles' own <see cref="RpgTileInfo.Solid"/> flag: most cells stay
|
||||
/// <see cref="Auto"/>, and the author paints exceptions (an invisible wall at
|
||||
/// the cliff edge, a walkable gap in a hedge).
|
||||
/// </summary>
|
||||
public static class RpgCollision
|
||||
{
|
||||
/// <summary>Passability comes from the tiles.</summary>
|
||||
public const char Auto = '.';
|
||||
|
||||
/// <summary>Always blocked.</summary>
|
||||
public const char Block = '#';
|
||||
|
||||
/// <summary>Always walkable, whatever the tiles say.</summary>
|
||||
public const char Open = 'o';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Cthangover.Mods.Rpg
|
||||
{
|
||||
/// <summary>
|
||||
/// Facing of an actor on the grid. The order is the row order of an RPG Maker
|
||||
/// style charset (down, left, right, up), so the value is also the frame row
|
||||
/// of <see cref="RpgActor"/>. JSON spells it as a word ("down", "Left").
|
||||
/// </summary>
|
||||
[JsonConverter(typeof(JsonStringEnumConverter))]
|
||||
public enum RpgDirection
|
||||
{
|
||||
Down = 0,
|
||||
Left = 1,
|
||||
Right = 2,
|
||||
Up = 3,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
using Godot;
|
||||
|
||||
namespace Cthangover.Mods.Rpg
|
||||
{
|
||||
/// <summary>Grid arithmetic of <see cref="RpgDirection"/>: the cell step it makes and the facing that looks back.</summary>
|
||||
public static class RpgDirections
|
||||
{
|
||||
/// <summary>Cell offset of one step; y grows downwards like the map rows.</summary>
|
||||
public static Vector2I Delta(RpgDirection direction) => direction switch
|
||||
{
|
||||
RpgDirection.Left => new Vector2I(-1, 0),
|
||||
RpgDirection.Right => new Vector2I(1, 0),
|
||||
RpgDirection.Up => new Vector2I(0, -1),
|
||||
_ => new Vector2I(0, 1),
|
||||
};
|
||||
|
||||
/// <summary>The facing that looks at the one given: an NPC turns to the player with it.</summary>
|
||||
public static RpgDirection Opposite(RpgDirection direction) => direction switch
|
||||
{
|
||||
RpgDirection.Left => RpgDirection.Right,
|
||||
RpgDirection.Right => RpgDirection.Left,
|
||||
RpgDirection.Up => RpgDirection.Down,
|
||||
_ => RpgDirection.Up,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
using System;
|
||||
|
||||
namespace Cthangover.Mods.Rpg
|
||||
{
|
||||
/// <summary>
|
||||
/// The random-battle roll made after each step on a map with an
|
||||
/// <see cref="RpgEncounterTable"/>. Takes the random source from outside so a
|
||||
/// test can make it deterministic.
|
||||
/// </summary>
|
||||
public static class RpgEncounterRoller
|
||||
{
|
||||
/// <summary>
|
||||
/// Returns the enemy group of the battle this step starts, or null for no
|
||||
/// battle (no table, encounters off, no groups, or the roll missed).
|
||||
/// </summary>
|
||||
public static string Roll(RpgEncounterTable table, Random random)
|
||||
{
|
||||
if (table == null || table.Steps <= 0 || table.Groups == null || table.Groups.Count == 0 || random == null)
|
||||
return null;
|
||||
if (random.Next(table.Steps) != 0)
|
||||
return null;
|
||||
|
||||
var group = table.Groups[random.Next(table.Groups.Count)];
|
||||
return string.IsNullOrWhiteSpace(group) ? null : group;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Cthangover.Mods.Rpg
|
||||
{
|
||||
/// <summary>
|
||||
/// Random battles of a map: after every step the player takes on it, a battle
|
||||
/// starts with probability <c>1 / Steps</c> against one of the
|
||||
/// <see cref="Groups"/>. Groups are enemy lists in the <c>battle.init</c>
|
||||
/// format (<c>"slime,slime"</c>), so they name character templates.
|
||||
/// </summary>
|
||||
public class RpgEncounterTable
|
||||
{
|
||||
/// <summary>Average number of steps between battles; 0 or less turns encounters off.</summary>
|
||||
public int Steps { get; set; }
|
||||
|
||||
/// <summary>Enemy groups, one of them picked uniformly.</summary>
|
||||
public List<string> Groups { get; set; } = new();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Cthangover.Mods.Rpg
|
||||
{
|
||||
/// <summary>How the player sets off an <see cref="RpgMapEvent"/>.</summary>
|
||||
[JsonConverter(typeof(JsonStringEnumConverter))]
|
||||
public enum RpgEventTrigger
|
||||
{
|
||||
/// <summary>The action key (E or F) while facing the event, or standing on it.</summary>
|
||||
Action = 0,
|
||||
|
||||
/// <summary>Stepping onto the event's cell (doors, map exits, traps).</summary>
|
||||
Touch = 1,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
namespace Cthangover.Mods.Rpg
|
||||
{
|
||||
/// <summary>Drawn layers of a map, bottom to top; actors are drawn between <see cref="Main"/> and <see cref="Front"/>.</summary>
|
||||
public enum RpgLayer
|
||||
{
|
||||
Back = 0,
|
||||
Main = 1,
|
||||
Front = 2,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Text.Json.Serialization;
|
||||
using Cthangover.Core.Factories;
|
||||
|
||||
namespace Cthangover.Mods.Rpg
|
||||
{
|
||||
/// <summary>
|
||||
/// One tile map of the RPG world. The map id is also the name of the logical
|
||||
/// scene that shows it: entering scene <c>X</c> (any <c>switch_scene X</c>, a
|
||||
/// teleport, the return from a battle, loading a save) mounts map <c>X</c>
|
||||
/// over the shell. Because the scene stays an ordinary scene, everything the
|
||||
/// novel engine attaches to scenes works on a map unchanged — entry
|
||||
/// scenarios, ambience and music from <c>scenes/X.json</c>, the default
|
||||
/// background (which also becomes the backdrop of battles started here),
|
||||
/// saving rules.
|
||||
/// <para/>
|
||||
/// Files declare <c>"Kind": "rpg_map"</c>. Layers are parsed by
|
||||
/// <see cref="RpgMapGrid"/>; a dependent mod replaces a whole map by id, the
|
||||
/// usual entity override.
|
||||
/// </summary>
|
||||
[EntityKind("rpg_map")]
|
||||
public class RpgMap : IIdentifiable
|
||||
{
|
||||
/// <summary>Map id = logical scene name.</summary>
|
||||
[JsonPropertyName("Id")]
|
||||
public string ID { get; set; }
|
||||
|
||||
/// <summary>Locale key of the map's display name.</summary>
|
||||
public string Name { get; set; }
|
||||
|
||||
/// <summary>Id of the <see cref="RpgTileset"/> the indices refer to.</summary>
|
||||
public string Tileset { get; set; }
|
||||
|
||||
/// <summary>Columns.</summary>
|
||||
public int Width { get; set; }
|
||||
|
||||
/// <summary>Rows.</summary>
|
||||
public int Height { get; set; }
|
||||
|
||||
/// <summary>Screen pixels per tile pixel; pixel art is scaled by a whole number.</summary>
|
||||
public int Zoom { get; set; } = 2;
|
||||
|
||||
/// <summary>The three drawn layers.</summary>
|
||||
public RpgMapLayers Layers { get; set; } = new();
|
||||
|
||||
/// <summary>Rows of <see cref="RpgCollision"/> codes, one character per cell; may be empty.</summary>
|
||||
public List<string> Collision { get; set; } = new();
|
||||
|
||||
/// <summary>Where the player appears when entering the map without a teleport target.</summary>
|
||||
public RpgTeleport Spawn { get; set; } = new();
|
||||
|
||||
/// <summary>Events placed on the map.</summary>
|
||||
public List<RpgMapEvent> Events { get; set; } = new();
|
||||
|
||||
/// <summary>Random battles; null for a safe map.</summary>
|
||||
public RpgEncounterTable Encounters { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Cthangover.Mods.Rpg
|
||||
{
|
||||
/// <summary>
|
||||
/// Something placed on a map cell that reacts to the player, in the spirit of
|
||||
/// RPG Maker events: an NPC, a sign, a chest, a door. What it looks like and
|
||||
/// what it does are independent:
|
||||
/// <list type="bullet">
|
||||
/// <item>look — a charset <see cref="Sprite"/>, or one atlas <see cref="Tile"/>, or nothing (an invisible trigger);</item>
|
||||
/// <item>effect — a <see cref="Teleport"/>, handled natively, or a dialog built
|
||||
/// from a <see cref="Scenario"/> file followed by inline <see cref="Commands"/>
|
||||
/// (the full scenario DSL, so <c>action rpg.battle …</c>, <c>flag …</c>, quests).</item>
|
||||
/// </list>
|
||||
/// A <see cref="Condition"/> in the scenario condition language hides the
|
||||
/// event while false; it is re-checked whenever game state changes, so a
|
||||
/// defeated boss (<c>flag boss_dead 1</c>) disappears at once.
|
||||
/// </summary>
|
||||
public class RpgMapEvent
|
||||
{
|
||||
/// <summary>Id unique within the map.</summary>
|
||||
public string Id { get; set; }
|
||||
|
||||
/// <summary>Column.</summary>
|
||||
public int X { get; set; }
|
||||
|
||||
/// <summary>Row.</summary>
|
||||
public int Y { get; set; }
|
||||
|
||||
/// <summary>Charset image in the <c>charsets</c> group (3 frames × 4 facings).</summary>
|
||||
public string Sprite { get; set; }
|
||||
|
||||
/// <summary>Atlas index of the map's tileset drawn as the event when it has no sprite; -1 for none.</summary>
|
||||
public int Tile { get; set; } = -1;
|
||||
|
||||
/// <summary>Initial facing of a sprite.</summary>
|
||||
public RpgDirection Dir { get; set; } = RpgDirection.Down;
|
||||
|
||||
/// <summary>When the event fires.</summary>
|
||||
public RpgEventTrigger Trigger { get; set; } = RpgEventTrigger.Action;
|
||||
|
||||
/// <summary>The event occupies its cell. Null means "solid when it is visible" (a sprite or a tile).</summary>
|
||||
public bool? Solid { get; set; }
|
||||
|
||||
/// <summary>Scenario path inside the <c>scenarios</c> group (extension optional), played as a dialog.</summary>
|
||||
public string Scenario { get; set; }
|
||||
|
||||
/// <summary>Inline DSL lines appended after the scenario.</summary>
|
||||
public List<string> Commands { get; set; }
|
||||
|
||||
/// <summary>Moves the player instead of starting a dialog.</summary>
|
||||
public RpgTeleport Teleport { get; set; }
|
||||
|
||||
/// <summary>Scenario condition; the event exists only while it is true.</summary>
|
||||
public string Condition { get; set; }
|
||||
|
||||
/// <summary>Effective solidity: explicit value, else whether the event is drawn.</summary>
|
||||
public bool IsSolid => Solid ?? (!string.IsNullOrEmpty(Sprite) || Tile >= 0);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
using System;
|
||||
using Cthangover.Core.Factories;
|
||||
using Cthangover.Core.Mods.Resolvers;
|
||||
|
||||
namespace Cthangover.Mods.Rpg
|
||||
{
|
||||
/// <summary>
|
||||
/// Maps collected by the shared entity resolver (<c>"Kind": "rpg_map"</c>).
|
||||
/// <see cref="Publish"/> lets the map editor put a just-saved map into the
|
||||
/// resolver's cache, so the running world shows the edit without a mod
|
||||
/// reload; the next reload reads the same content back from the file.
|
||||
/// </summary>
|
||||
public class RpgMapFactory : FileFactory<RpgMap>
|
||||
{
|
||||
private static readonly Lazy<RpgMapFactory> instance = new(() => new RpgMapFactory());
|
||||
|
||||
/// <summary>Shared instance.</summary>
|
||||
public static RpgMapFactory Instance => instance.Value;
|
||||
|
||||
/// <summary>Replaces the cached definition of <paramref name="map"/>'s id.</summary>
|
||||
public void Publish(RpgMap map)
|
||||
{
|
||||
if (map != null && !string.IsNullOrEmpty(map.ID))
|
||||
Entities.Collect<RpgMap>()[map.ID] = map;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,176 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
|
||||
namespace Cthangover.Mods.Rpg
|
||||
{
|
||||
/// <summary>
|
||||
/// Parsed cells of an <see cref="RpgMap"/>: the three tile layers as atlas
|
||||
/// indices (-1 = empty) and the collision codes. The map file keeps rows as
|
||||
/// text (<see cref="RpgMapLayers"/>); this is the form the world and the
|
||||
/// editor work with, and <see cref="WriteTo"/> turns it back into rows.
|
||||
/// <para/>
|
||||
/// Parsing is forgiving, because maps are edited by hand: short rows and
|
||||
/// missing rows are empty cells, extra cells are cut off, an unreadable token
|
||||
/// is empty. The grid always has exactly the map's declared size.
|
||||
/// </summary>
|
||||
public class RpgMapGrid
|
||||
{
|
||||
private const string EmptyToken = ".";
|
||||
|
||||
private int[][] layers;
|
||||
private char[] collision;
|
||||
|
||||
/// <summary>Columns.</summary>
|
||||
public int Width { get; private set; }
|
||||
|
||||
/// <summary>Rows.</summary>
|
||||
public int Height { get; private set; }
|
||||
|
||||
/// <summary>Creates an empty grid: no tiles, collision from tiles everywhere.</summary>
|
||||
public RpgMapGrid(int width, int height)
|
||||
{
|
||||
Width = Math.Max(1, width);
|
||||
Height = Math.Max(1, height);
|
||||
layers = new int[3][];
|
||||
for (var i = 0; i < layers.Length; i++)
|
||||
layers[i] = Enumerable.Repeat(-1, Width * Height).ToArray();
|
||||
collision = Enumerable.Repeat(RpgCollision.Auto, Width * Height).ToArray();
|
||||
}
|
||||
|
||||
/// <summary>Reads the layers and the collision rows of <paramref name="map"/>.</summary>
|
||||
public static RpgMapGrid Parse(RpgMap map)
|
||||
{
|
||||
var grid = new RpgMapGrid(map?.Width ?? 1, map?.Height ?? 1);
|
||||
if (map == null)
|
||||
return grid;
|
||||
|
||||
grid.ReadLayer(RpgLayer.Back, map.Layers?.Back);
|
||||
grid.ReadLayer(RpgLayer.Main, map.Layers?.Main);
|
||||
grid.ReadLayer(RpgLayer.Front, map.Layers?.Front);
|
||||
|
||||
var rows = map.Collision ?? new List<string>();
|
||||
for (var y = 0; y < grid.Height && y < rows.Count; y++)
|
||||
{
|
||||
var row = rows[y] ?? string.Empty;
|
||||
for (var x = 0; x < grid.Width && x < row.Length; x++)
|
||||
grid.SetCollision(x, y, row[x]);
|
||||
}
|
||||
return grid;
|
||||
}
|
||||
|
||||
/// <summary>True when the cell lies on the map.</summary>
|
||||
public bool InBounds(int x, int y) => x >= 0 && y >= 0 && x < Width && y < Height;
|
||||
|
||||
/// <summary>Atlas index on a layer, -1 for empty or outside the map.</summary>
|
||||
public int GetTile(RpgLayer layer, int x, int y) => InBounds(x, y) ? layers[(int)layer][y * Width + x] : -1;
|
||||
|
||||
/// <summary>Puts an atlas index (-1 clears); outside the map it does nothing.</summary>
|
||||
public void SetTile(RpgLayer layer, int x, int y, int index)
|
||||
{
|
||||
if (InBounds(x, y))
|
||||
layers[(int)layer][y * Width + x] = index < 0 ? -1 : index;
|
||||
}
|
||||
|
||||
/// <summary>Collision code of a cell; outside the map it is <see cref="RpgCollision.Block"/>.</summary>
|
||||
public char GetCollision(int x, int y) => InBounds(x, y) ? collision[y * Width + x] : RpgCollision.Block;
|
||||
|
||||
/// <summary>Sets a collision code; an unknown code becomes <see cref="RpgCollision.Auto"/>.</summary>
|
||||
public void SetCollision(int x, int y, char code)
|
||||
{
|
||||
if (!InBounds(x, y))
|
||||
return;
|
||||
collision[y * Width + x] = code == RpgCollision.Block || code == RpgCollision.Open ? code : RpgCollision.Auto;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Changes the size keeping the top-left content; new cells are empty.
|
||||
/// Used by the editor; the map's events are not moved.
|
||||
/// </summary>
|
||||
public void Resize(int width, int height)
|
||||
{
|
||||
var resized = new RpgMapGrid(width, height);
|
||||
for (var y = 0; y < Math.Min(Height, resized.Height); y++)
|
||||
{
|
||||
for (var x = 0; x < Math.Min(Width, resized.Width); x++)
|
||||
{
|
||||
foreach (RpgLayer layer in Enum.GetValues(typeof(RpgLayer)))
|
||||
resized.SetTile(layer, x, y, GetTile(layer, x, y));
|
||||
resized.SetCollision(x, y, GetCollision(x, y));
|
||||
}
|
||||
}
|
||||
Width = resized.Width;
|
||||
Height = resized.Height;
|
||||
layers = resized.layers;
|
||||
collision = resized.collision;
|
||||
}
|
||||
|
||||
/// <summary>Copy of the grid, so editor undo steps never share cells.</summary>
|
||||
public RpgMapGrid Clone()
|
||||
{
|
||||
var copy = new RpgMapGrid(Width, Height);
|
||||
for (var i = 0; i < layers.Length; i++)
|
||||
Array.Copy(layers[i], copy.layers[i], layers[i].Length);
|
||||
Array.Copy(collision, copy.collision, collision.Length);
|
||||
return copy;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Writes size, layer rows and collision rows into <paramref name="map"/>.
|
||||
/// Indices are right-aligned to one width per layer, so columns line up in
|
||||
/// the file. An all-auto collision layer is written as no rows.
|
||||
/// </summary>
|
||||
public void WriteTo(RpgMap map)
|
||||
{
|
||||
map.Width = Width;
|
||||
map.Height = Height;
|
||||
map.Layers ??= new RpgMapLayers();
|
||||
map.Layers.Back = WriteLayer(RpgLayer.Back);
|
||||
map.Layers.Main = WriteLayer(RpgLayer.Main);
|
||||
map.Layers.Front = WriteLayer(RpgLayer.Front);
|
||||
|
||||
var rows = new List<string>();
|
||||
if (collision.Any(c => c != RpgCollision.Auto))
|
||||
{
|
||||
for (var y = 0; y < Height; y++)
|
||||
rows.Add(new string(collision, y * Width, Width));
|
||||
}
|
||||
map.Collision = rows;
|
||||
}
|
||||
|
||||
private void ReadLayer(RpgLayer layer, List<string> rows)
|
||||
{
|
||||
if (rows == null)
|
||||
return;
|
||||
for (var y = 0; y < Height && y < rows.Count; y++)
|
||||
{
|
||||
var tokens = (rows[y] ?? string.Empty).Split(' ', StringSplitOptions.RemoveEmptyEntries);
|
||||
for (var x = 0; x < Width && x < tokens.Length; x++)
|
||||
SetTile(layer, x, y, int.TryParse(tokens[x], out var index) ? index : -1);
|
||||
}
|
||||
}
|
||||
|
||||
private List<string> WriteLayer(RpgLayer layer)
|
||||
{
|
||||
var cells = layers[(int)layer];
|
||||
var width = cells.Select(Token).Max(t => t.Length);
|
||||
var rows = new List<string>();
|
||||
var line = new StringBuilder();
|
||||
for (var y = 0; y < Height; y++)
|
||||
{
|
||||
line.Clear();
|
||||
for (var x = 0; x < Width; x++)
|
||||
{
|
||||
if (x > 0)
|
||||
line.Append(' ');
|
||||
line.Append(Token(cells[y * Width + x]).PadLeft(width));
|
||||
}
|
||||
rows.Add(line.ToString());
|
||||
}
|
||||
return rows;
|
||||
}
|
||||
|
||||
private static string Token(int index) => index < 0 ? EmptyToken : index.ToString();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Cthangover.Mods.Rpg
|
||||
{
|
||||
/// <summary>
|
||||
/// The three drawn layers of a map, each a list of rows. A row is a string of
|
||||
/// space-separated atlas indices, <c>.</c> for an empty cell, so a map file
|
||||
/// stays readable and diffable by hand (see <see cref="RpgMapGrid"/>).
|
||||
/// <list type="bullet">
|
||||
/// <item><see cref="Back"/> — ground: grass, floors, water.</item>
|
||||
/// <item><see cref="Main"/> — objects at the actors' level: walls, trunks, fences.</item>
|
||||
/// <item><see cref="Front"/> — drawn over the actors: tree crowns, roofs' eaves, arches.</item>
|
||||
/// </list>
|
||||
/// </summary>
|
||||
public class RpgMapLayers
|
||||
{
|
||||
/// <summary>Ground layer rows.</summary>
|
||||
public List<string> Back { get; set; } = new();
|
||||
|
||||
/// <summary>Object layer rows.</summary>
|
||||
public List<string> Main { get; set; } = new();
|
||||
|
||||
/// <summary>Overhead layer rows.</summary>
|
||||
public List<string> Front { get; set; } = new();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
namespace Cthangover.Mods.Rpg
|
||||
{
|
||||
/// <summary>
|
||||
/// Whether the terrain of a cell can be walked on: the collision layer
|
||||
/// decides when painted, otherwise a solid tile on the back or the main layer
|
||||
/// blocks. Front-layer tiles never block — they are what the player walks
|
||||
/// under. Events are not terrain: the world adds their cells on top.
|
||||
/// </summary>
|
||||
public static class RpgPassability
|
||||
{
|
||||
/// <summary>True when the cell is inside the map and its terrain lets the player in.</summary>
|
||||
public static bool IsWalkable(RpgMapGrid grid, RpgTileset tileset, int x, int y)
|
||||
{
|
||||
if (grid == null || !grid.InBounds(x, y))
|
||||
return false;
|
||||
|
||||
var code = grid.GetCollision(x, y);
|
||||
if (code == RpgCollision.Block)
|
||||
return false;
|
||||
if (code == RpgCollision.Open)
|
||||
return true;
|
||||
|
||||
if (tileset == null)
|
||||
return true;
|
||||
return !tileset.IsSolid(grid.GetTile(RpgLayer.Back, x, y))
|
||||
&& !tileset.IsSolid(grid.GetTile(RpgLayer.Main, x, y));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
namespace Cthangover.Mods.Rpg
|
||||
{
|
||||
/// <summary>
|
||||
/// A cell on some map and the facing the player arrives with. Used for the
|
||||
/// map's spawn point and for teleport events (doors, map edges, stairs).
|
||||
/// </summary>
|
||||
public class RpgTeleport
|
||||
{
|
||||
/// <summary>Target map id; empty means the map the event is on.</summary>
|
||||
public string Map { get; set; }
|
||||
|
||||
/// <summary>Target column.</summary>
|
||||
public int X { get; set; }
|
||||
|
||||
/// <summary>Target row.</summary>
|
||||
public int Y { get; set; }
|
||||
|
||||
/// <summary>Facing after arrival.</summary>
|
||||
public RpgDirection Dir { get; set; } = RpgDirection.Down;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
namespace Cthangover.Mods.Rpg
|
||||
{
|
||||
/// <summary>
|
||||
/// Properties of one atlas cell of an <see cref="RpgTileset"/>. Only cells that
|
||||
/// differ from the default (walkable, unnamed) need an entry.
|
||||
/// </summary>
|
||||
public class RpgTileInfo
|
||||
{
|
||||
/// <summary>Row-major atlas index: <c>row * Columns + column</c>.</summary>
|
||||
public int Index { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The tile blocks walking when it lies on the back or the main layer
|
||||
/// (front-layer tiles never block: the player walks under them). The map's
|
||||
/// collision layer can override it per cell.
|
||||
/// </summary>
|
||||
public bool Solid { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Optional author-facing tag shown by the editor palette ("grass",
|
||||
/// "tree_crown"). Not a locale key: players never see it.
|
||||
/// </summary>
|
||||
public string Tag { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Text.Json.Serialization;
|
||||
using Cthangover.Core.Factories;
|
||||
|
||||
namespace Cthangover.Mods.Rpg
|
||||
{
|
||||
/// <summary>
|
||||
/// A square-tile atlas: one image cut into <see cref="TileSize"/> cells,
|
||||
/// <see cref="Columns"/> per row, addressed by a row-major index. Maps store
|
||||
/// only these indices, so a pack can redraw its tiles without touching maps.
|
||||
/// <para/>
|
||||
/// Files declare <c>"Kind": "rpg_tileset"</c> and live anywhere in a mod; the
|
||||
/// image is resolved in the <c>tilesets</c> file group (last mod wins), so a
|
||||
/// dependent mod can reskin a tileset by shipping an image of the same path.
|
||||
/// </summary>
|
||||
[EntityKind("rpg_tileset")]
|
||||
public class RpgTileset : IIdentifiable
|
||||
{
|
||||
private HashSet<int> solid;
|
||||
|
||||
/// <summary>Tileset id that maps reference.</summary>
|
||||
[JsonPropertyName("Id")]
|
||||
public string ID { get; set; }
|
||||
|
||||
/// <summary>Image path inside the <c>tilesets</c> group, extension optional.</summary>
|
||||
public string Image { get; set; }
|
||||
|
||||
/// <summary>Edge of one square tile in image pixels.</summary>
|
||||
public int TileSize { get; set; } = 32;
|
||||
|
||||
/// <summary>Atlas cells per row.</summary>
|
||||
public int Columns { get; set; } = 8;
|
||||
|
||||
/// <summary>Per-cell properties; cells without an entry are walkable.</summary>
|
||||
public List<RpgTileInfo> Tiles { get; set; } = new();
|
||||
|
||||
/// <summary>True when the atlas cell blocks walking. Built once from <see cref="Tiles"/>.</summary>
|
||||
public bool IsSolid(int index)
|
||||
{
|
||||
if (index < 0)
|
||||
return false;
|
||||
if (solid == null)
|
||||
{
|
||||
solid = new HashSet<int>();
|
||||
foreach (var tile in Tiles ?? new List<RpgTileInfo>())
|
||||
{
|
||||
if (tile.Solid)
|
||||
solid.Add(tile.Index);
|
||||
}
|
||||
}
|
||||
return solid.Contains(index);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
using System;
|
||||
using Cthangover.Core.Factories;
|
||||
|
||||
namespace Cthangover.Mods.Rpg
|
||||
{
|
||||
/// <summary>Tilesets collected by the shared entity resolver (<c>"Kind": "rpg_tileset"</c>).</summary>
|
||||
public class RpgTilesetFactory : FileFactory<RpgTileset>
|
||||
{
|
||||
private static readonly Lazy<RpgTilesetFactory> instance = new(() => new RpgTilesetFactory());
|
||||
|
||||
/// <summary>Shared instance.</summary>
|
||||
public static RpgTilesetFactory Instance => instance.Value;
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user