Skip to content

Commit 5134160

Browse files
committed
Add scene registry and transition hooks
introduce keyed scene registration with factory loading, metadata, and lookup helpers to make scene flow configurable at runtime. emit scene changing/changed events with transition context so systems can react to lifecycle changes without tight coupling. track current scene keys and expose transition state for easier orchestration from higher-level modules. document the engine's optional-helper philosophy in the readme, clarifying that scene/map/hitbox tooling is convenience-first and replaceable.
1 parent dea116f commit 5134160

4 files changed

Lines changed: 531 additions & 1 deletion

File tree

KitsuneEngine.Core/MapSystem.cs

Lines changed: 270 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,270 @@
1+
using System.Numerics;
2+
3+
namespace KitsuneEngine.Core;
4+
5+
/// <summary>
6+
/// Generic map descriptor suitable for visual novel, RPG, and other 2D games.
7+
/// </summary>
8+
public sealed class MapData
9+
{
10+
public string Id { get; }
11+
public string Name { get; set; }
12+
public Vector2 Size { get; set; }
13+
public string? BackgroundAsset { get; set; }
14+
15+
/// <summary>
16+
/// Arbitrary map values (music id, weather, chapter, etc.)
17+
/// </summary>
18+
public Dictionary<string, string> Metadata { get; } = new(StringComparer.OrdinalIgnoreCase);
19+
20+
/// <summary>
21+
/// Spawn points and named anchors (player start, npc_home, camera_focus, etc.)
22+
/// </summary>
23+
public Dictionary<string, Vector2> Anchors { get; } = new(StringComparer.OrdinalIgnoreCase);
24+
25+
/// <summary>
26+
/// Connections to other maps for travel/warp/door systems.
27+
/// </summary>
28+
public List<MapConnection> Connections { get; } = new();
29+
30+
/// <summary>
31+
/// Optional tile map payload. Leave null if this map is not tile-based.
32+
/// </summary>
33+
public TileMapData? TileMap { get; set; }
34+
35+
public MapData(string id, string? name = null)
36+
{
37+
if (string.IsNullOrWhiteSpace(id))
38+
throw new ArgumentException("Map id cannot be null or empty.", nameof(id));
39+
40+
Id = id;
41+
Name = string.IsNullOrWhiteSpace(name) ? id : name;
42+
Size = Vector2.Zero;
43+
}
44+
}
45+
46+
/// <summary>
47+
/// Optional tile map data for grid-based 2D maps.
48+
/// Uses flattened tile indices per layer in row-major order.
49+
/// </summary>
50+
public sealed class TileMapData
51+
{
52+
public int Width { get; }
53+
public int Height { get; }
54+
public int TileWidth { get; }
55+
public int TileHeight { get; }
56+
public List<TileLayerData> Layers { get; } = new();
57+
58+
public TileMapData(int width, int height, int tileWidth, int tileHeight)
59+
{
60+
if (width <= 0)
61+
throw new ArgumentOutOfRangeException(nameof(width), "Width must be greater than zero.");
62+
if (height <= 0)
63+
throw new ArgumentOutOfRangeException(nameof(height), "Height must be greater than zero.");
64+
if (tileWidth <= 0)
65+
throw new ArgumentOutOfRangeException(nameof(tileWidth), "Tile width must be greater than zero.");
66+
if (tileHeight <= 0)
67+
throw new ArgumentOutOfRangeException(nameof(tileHeight), "Tile height must be greater than zero.");
68+
69+
Width = width;
70+
Height = height;
71+
TileWidth = tileWidth;
72+
TileHeight = tileHeight;
73+
}
74+
75+
public int GetIndex(int x, int y)
76+
{
77+
if (x < 0 || y < 0 || x >= Width || y >= Height)
78+
throw new ArgumentOutOfRangeException($"Tile coordinate ({x},{y}) is out of range.");
79+
80+
return y * Width + x;
81+
}
82+
83+
public Vector2 TileToWorld(int x, int y)
84+
{
85+
return new Vector2(x * TileWidth, y * TileHeight);
86+
}
87+
}
88+
89+
public sealed class TileLayerData
90+
{
91+
public string Name { get; }
92+
public int ZIndex { get; set; }
93+
public bool Visible { get; set; } = true;
94+
public int[] Tiles { get; }
95+
96+
public TileLayerData(string name, int tileCount)
97+
{
98+
if (string.IsNullOrWhiteSpace(name))
99+
throw new ArgumentException("Layer name cannot be null or empty.", nameof(name));
100+
if (tileCount <= 0)
101+
throw new ArgumentOutOfRangeException(nameof(tileCount), "Tile count must be greater than zero.");
102+
103+
Name = name;
104+
Tiles = new int[tileCount];
105+
106+
// -1 means empty cell
107+
Array.Fill(Tiles, -1);
108+
}
109+
110+
public int GetTile(int index) => Tiles[index];
111+
public void SetTile(int index, int tileId) => Tiles[index] = tileId;
112+
}
113+
114+
/// <summary>
115+
/// Optional helper builder for creating tile maps quickly.
116+
/// Developers can use this for convenience or implement their own map pipeline.
117+
/// </summary>
118+
public sealed class TileMapBuilder
119+
{
120+
private readonly TileMapData _map;
121+
private readonly Dictionary<string, TileLayerData> _layers = new(StringComparer.OrdinalIgnoreCase);
122+
123+
public TileMapBuilder(int width, int height, int tileWidth, int tileHeight)
124+
{
125+
_map = new TileMapData(width, height, tileWidth, tileHeight);
126+
}
127+
128+
public TileMapBuilder AddLayer(string name, int zIndex = 0)
129+
{
130+
var layer = new TileLayerData(name, _map.Width * _map.Height)
131+
{
132+
ZIndex = zIndex
133+
};
134+
135+
_layers[name] = layer;
136+
return this;
137+
}
138+
139+
public TileMapBuilder SetTile(string layerName, int x, int y, int tileId)
140+
{
141+
if (!_layers.TryGetValue(layerName, out var layer))
142+
throw new InvalidOperationException($"Layer '{layerName}' not found.");
143+
144+
int index = _map.GetIndex(x, y);
145+
layer.SetTile(index, tileId);
146+
return this;
147+
}
148+
149+
public TileMapBuilder FillRect(string layerName, int startX, int startY, int width, int height, int tileId)
150+
{
151+
if (width <= 0 || height <= 0)
152+
return this;
153+
154+
for (int y = startY; y < startY + height; y++)
155+
{
156+
for (int x = startX; x < startX + width; x++)
157+
{
158+
SetTile(layerName, x, y, tileId);
159+
}
160+
}
161+
162+
return this;
163+
}
164+
165+
public TileMapData Build()
166+
{
167+
_map.Layers.Clear();
168+
_map.Layers.AddRange(_layers.Values.OrderBy(l => l.ZIndex));
169+
return _map;
170+
}
171+
}
172+
173+
public sealed class MapConnection
174+
{
175+
public string ToMapId { get; }
176+
public string? ViaAnchor { get; set; }
177+
public string? RequiredFlag { get; set; }
178+
179+
public MapConnection(string toMapId)
180+
{
181+
if (string.IsNullOrWhiteSpace(toMapId))
182+
throw new ArgumentException("Destination map id cannot be null or empty.", nameof(toMapId));
183+
184+
ToMapId = toMapId;
185+
}
186+
}
187+
188+
/// <summary>
189+
/// Lightweight optional manager for map registry and map transitions.
190+
/// </summary>
191+
public sealed class MapManager
192+
{
193+
private readonly Dictionary<string, MapData> _maps = new(StringComparer.OrdinalIgnoreCase);
194+
195+
public MapData? CurrentMap { get; private set; }
196+
public string? CurrentMapId => CurrentMap?.Id;
197+
public IReadOnlyCollection<MapData> Maps => _maps.Values;
198+
199+
public event Action<MapTransitionContext>? MapChanging;
200+
public event Action<MapTransitionContext>? MapChanged;
201+
202+
public void RegisterMap(MapData map)
203+
{
204+
if (map == null)
205+
throw new ArgumentNullException(nameof(map));
206+
207+
_maps[map.Id] = map;
208+
}
209+
210+
public bool UnregisterMap(string mapId)
211+
{
212+
if (string.IsNullOrWhiteSpace(mapId))
213+
return false;
214+
215+
if (CurrentMap != null && string.Equals(CurrentMap.Id, mapId, StringComparison.OrdinalIgnoreCase))
216+
CurrentMap = null;
217+
218+
return _maps.Remove(mapId);
219+
}
220+
221+
public bool HasMap(string mapId) => _maps.ContainsKey(mapId);
222+
223+
public bool TryGetMap(string mapId, out MapData map) => _maps.TryGetValue(mapId, out map!);
224+
225+
public bool TrySetCurrentMap(string mapId)
226+
{
227+
if (!_maps.TryGetValue(mapId, out var next))
228+
return false;
229+
230+
SetCurrentMap(next);
231+
return true;
232+
}
233+
234+
public void SetCurrentMap(MapData map)
235+
{
236+
if (map == null)
237+
throw new ArgumentNullException(nameof(map));
238+
239+
var context = new MapTransitionContext(CurrentMap, map);
240+
MapChanging?.Invoke(context);
241+
CurrentMap = map;
242+
MapChanged?.Invoke(context);
243+
}
244+
245+
public bool TryTravel(string connectionToMapId)
246+
{
247+
if (CurrentMap == null)
248+
return false;
249+
250+
var connection = CurrentMap.Connections.FirstOrDefault(c =>
251+
string.Equals(c.ToMapId, connectionToMapId, StringComparison.OrdinalIgnoreCase));
252+
253+
if (connection == null)
254+
return false;
255+
256+
return TrySetCurrentMap(connection.ToMapId);
257+
}
258+
}
259+
260+
public sealed class MapTransitionContext
261+
{
262+
public MapData? FromMap { get; }
263+
public MapData? ToMap { get; }
264+
265+
public MapTransitionContext(MapData? fromMap, MapData? toMap)
266+
{
267+
FromMap = fromMap;
268+
ToMap = toMap;
269+
}
270+
}

0 commit comments

Comments
 (0)