using System.Text.Json; namespace Roboto.Data; public record AreaNode( string Id, string Name, int Act, int Level, int Order, bool HasWaypoint, bool IsTown, List ConnectsTo); /// /// Graph of game areas loaded from data/poe2/areas.json. /// Supports progression ordering, adjacency queries, and BFS pathfinding. /// public sealed class AreaGraph { private readonly Dictionary _byId; private readonly Dictionary _byName; private readonly List _allByOrder; private AreaGraph(Dictionary byId, Dictionary byName, List allByOrder) { _byId = byId; _byName = byName; _allByOrder = allByOrder; } public AreaNode? GetById(string id) => _byId.GetValueOrDefault(id); public AreaNode? GetByName(string name) => _byName.GetValueOrDefault(name); public List GetConnections(string id) { if (!_byId.TryGetValue(id, out var node)) return []; var result = new List(node.ConnectsTo.Count); foreach (var cid in node.ConnectsTo) { if (_byId.TryGetValue(cid, out var connected)) result.Add(connected); } return result; } /// /// BFS from currentId to find the lowest-order unvisited reachable area. /// Returns the area ID to target next, or null if progression is complete. /// public string? FindNextTarget(string currentId, HashSet visited) { if (!_byId.ContainsKey(currentId)) return null; // BFS to find all reachable areas var reachable = new HashSet(); var queue = new Queue(); queue.Enqueue(currentId); reachable.Add(currentId); while (queue.Count > 0) { var id = queue.Dequeue(); if (!_byId.TryGetValue(id, out var node)) continue; foreach (var cid in node.ConnectsTo) { if (reachable.Add(cid)) queue.Enqueue(cid); } } // Find the lowest-order unvisited reachable area AreaNode? best = null; foreach (var rid in reachable) { if (visited.Contains(rid)) continue; if (!_byId.TryGetValue(rid, out var node)) continue; if (best is null || CompareOrder(node, best) < 0) best = node; } return best?.Id; } /// /// BFS shortest path from fromId to toId through the area graph. /// Returns the sequence of area IDs, or null if unreachable. /// public List? FindAreaPath(string fromId, string toId) { if (fromId == toId) return [fromId]; if (!_byId.ContainsKey(fromId) || !_byId.ContainsKey(toId)) return null; var prev = new Dictionary(); var queue = new Queue(); queue.Enqueue(fromId); prev[fromId] = fromId; while (queue.Count > 0) { var id = queue.Dequeue(); if (id == toId) { // Reconstruct path var path = new List(); var cur = toId; while (cur != fromId) { path.Add(cur); cur = prev[cur]; } path.Add(fromId); path.Reverse(); return path; } if (!_byId.TryGetValue(id, out var node)) continue; foreach (var cid in node.ConnectsTo) { if (!prev.ContainsKey(cid)) { prev[cid] = id; queue.Enqueue(cid); } } } return null; // unreachable } /// /// Compare two nodes by (act, order) for progression ordering. /// private static int CompareOrder(AreaNode a, AreaNode b) { var actCmp = a.Act.CompareTo(b.Act); return actCmp != 0 ? actCmp : a.Order.CompareTo(b.Order); } /// /// Returns all area IDs with order less than or equal to the given area's order (same act). /// Used to auto-mark earlier areas as visited so progression always moves forward. /// public List GetEarlierAreas(string areaId) { if (!_byId.TryGetValue(areaId, out var current)) return []; var result = new List(); foreach (var node in _allByOrder) { if (node.Act == current.Act && node.Order < current.Order) result.Add(node.Id); } return result; } public static AreaGraph Load() { var byId = new Dictionary(StringComparer.OrdinalIgnoreCase); var byName = new Dictionary(StringComparer.OrdinalIgnoreCase); var allByOrder = new List(); try { var path = Path.Combine("data", "poe2", "areas.json"); if (!File.Exists(path)) return new AreaGraph(byId, byName, allByOrder); var json = File.ReadAllText(path); using var doc = JsonDocument.Parse(json); foreach (var actElement in doc.RootElement.EnumerateArray()) { var act = actElement.GetProperty("act").GetInt32(); foreach (var area in actElement.GetProperty("areas").EnumerateArray()) { var id = area.GetProperty("id").GetString()!; var name = area.GetProperty("name").GetString()!; var level = area.GetProperty("level").GetInt32(); var order = area.GetProperty("order").GetInt32(); var wp = area.GetProperty("wp").GetBoolean(); var town = area.TryGetProperty("town", out var townProp) && townProp.GetBoolean(); var connects = new List(); foreach (var c in area.GetProperty("connects").EnumerateArray()) connects.Add(c.GetString()!); var node = new AreaNode(id, name, act, level, order, wp, town, connects); byId[id] = node; byName[name] = node; allByOrder.Add(node); } } } catch { /* non-critical */ } allByOrder.Sort((a, b) => CompareOrder(a, b)); return new AreaGraph(byId, byName, allByOrder); } }