copy from github

This commit is contained in:
Sebastian Bularca
2026-03-27 15:13:27 +01:00
parent 83532f396d
commit 823e146df0
44 changed files with 2999 additions and 2 deletions

View File

@@ -0,0 +1,14 @@
{
"name": "Jovian.ZoneSystem",
"rootNamespace": "Jovian.ZoneSystem",
"references": [],
"includePlatforms": [],
"excludePlatforms": [],
"allowUnsafeCode": false,
"overrideReferences": false,
"precompiledReferences": [],
"autoReferenced": true,
"defineConstraints": [],
"versionDefines": [],
"noEngineReferences": false
}

View File

@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 14a17a3524e6bed489ca921a325f8942
AssemblyDefinitionImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

43
Runtime/MapPlane.cs Normal file
View File

@@ -0,0 +1,43 @@
using UnityEngine;
namespace Jovian.ZoneSystem {
/// <summary>
/// Defines which two world axes the map (and zone polygons) lie on.
/// XY = flat sprite / UI map (Z is depth)
/// XZ = 3D world map (Y is up) ← standard Unity 3D
/// YZ = side-on map (X is depth)
/// </summary>
public enum MapPlane {
XY,
XZ,
YZ
}
public static class MapPlaneUtility {
/// <summary>
/// Projects a 3D world position onto the chosen map plane,
/// returning a 2D point suitable for polygon testing.
/// </summary>
public static Vector2 ProjectToPlane(Vector3 worldPos, MapPlane plane) {
switch(plane) {
case MapPlane.XY: return new Vector2(worldPos.x, worldPos.y);
case MapPlane.XZ: return new Vector2(worldPos.x, worldPos.z);
case MapPlane.YZ: return new Vector2(worldPos.y, worldPos.z);
default: return new Vector2(worldPos.x, worldPos.y);
}
}
/// <summary>
/// Reconstructs a 3D world position from a 2D polygon point on the chosen plane.
/// The depth value fills the axis not covered by the plane.
/// </summary>
public static Vector3 UnprojectFromPlane(Vector2 point, MapPlane plane, float depth = 0f) {
switch(plane) {
case MapPlane.XY: return new Vector3(point.x, point.y, depth);
case MapPlane.XZ: return new Vector3(point.x, depth, point.y);
case MapPlane.YZ: return new Vector3(depth, point.x, point.y);
default: return new Vector3(point.x, point.y, depth);
}
}
}
}

2
Runtime/MapPlane.cs.meta Normal file
View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: e9eeceb36a2fca741a5e4c3206a20d00

200
Runtime/PolygonUtils.cs Normal file
View File

@@ -0,0 +1,200 @@
using System.Collections.Generic;
using UnityEngine;
namespace Jovian.ZoneSystem {
public static class PolygonUtils {
/// <summary>
/// Ray-casting point-in-polygon test (Jordan curve theorem).
/// Works on any plane — caller projects the world position first via MapPlaneUtility.
/// Handles edge and vertex cases robustly.
/// </summary>
/// <param name="point">2D point already projected onto the polygon's plane.</param>
/// <param name="polygon">Polygon vertices in the same 2D space.</param>
public static bool PointInPolygon(Vector2 point, List<Vector2> polygon) {
if(polygon == null || polygon.Count < 3) {
return false;
}
float px = point.x;
float py = point.y;
bool inside = false;
int count = polygon.Count;
int j = count - 1;
for(int i = 0; i < count; i++) {
float xi = polygon[i].x, yi = polygon[i].y;
float xj = polygon[j].x, yj = polygon[j].y;
// Crossing test: does the edge (j→i) cross the horizontal ray from point?
bool crosses = (yi > py) != (yj > py) &&
px < ((xj - xi) * (py - yi) / (yj - yi)) + xi;
if(crosses) {
inside = !inside;
}
j = i;
}
return inside;
}
/// <summary>
/// Overload that accepts a world position and projects it onto the given plane
/// before testing — this is the primary API used by ZoneManager.
/// </summary>
public static bool PointInPolygon(Vector3 worldPos, List<Vector2> polygon, MapPlane plane) {
Vector2 projected = MapPlaneUtility.ProjectToPlane(worldPos, plane);
return PointInPolygon(projected, polygon);
}
/// <summary>
/// Returns the centroid of a polygon (for label placement in the editor).
/// </summary>
public static Vector2 Centroid(List<Vector2> polygon) {
if(polygon == null || polygon.Count == 0) {
return Vector2.zero;
}
Vector2 sum = Vector2.zero;
foreach(Vector2 pt in polygon) {
sum += pt;
}
return sum / polygon.Count;
}
/// <summary>
/// Returns the approximate axis-aligned bounding box of a polygon.
/// Useful for a cheap pre-check before running the full ray-cast test.
/// </summary>
public static (Vector2 min, Vector2 max) Bounds(List<Vector2> polygon) {
if(polygon == null || polygon.Count == 0) {
return (Vector2.zero, Vector2.zero);
}
Vector2 min = polygon[0], max = polygon[0];
foreach(Vector2 pt in polygon) {
if(pt.x < min.x) {
min.x = pt.x;
}
if(pt.y < min.y) {
min.y = pt.y;
}
if(pt.x > max.x) {
max.x = pt.x;
}
if(pt.y > max.y) {
max.y = pt.y;
}
}
return (min, max);
}
/// <summary>
/// Fast AABB pre-check. Call this before PointInPolygon to skip the
/// ray-cast for points clearly outside the bounding box.
/// </summary>
public static bool PointInBounds(Vector2 point, Vector2 min, Vector2 max) {
return point.x >= min.x && point.x <= max.x &&
point.y >= min.y && point.y <= max.y;
}
/// <summary>
/// Ear-clipping triangulation for simple (non-self-intersecting) polygons.
/// Returns a list of triangle index triplets into the original vertex list.
/// Supports both convex and concave polygons.
/// </summary>
public static List<int> Triangulate(List<Vector2> polygon) {
List<int> triangles = new List<int>();
int n = polygon.Count;
if(n < 3) {
return triangles;
}
// Build index list
List<int> indices = new List<int>(n);
bool clockwise = SignedArea(polygon) < 0f;
for(int i = 0; i < n; i++) {
indices.Add(clockwise ? i : n - 1 - i);
}
int remaining = n;
int failSafe = remaining * 2;
int v = remaining - 1;
while(remaining > 2) {
if(failSafe-- <= 0) {
break;
}
int u = v;
if(u >= remaining) {
u = 0;
}
v = u + 1;
if(v >= remaining) {
v = 0;
}
int w = v + 1;
if(w >= remaining) {
w = 0;
}
if(IsEar(polygon, indices, u, v, w, remaining)) {
triangles.Add(indices[u]);
triangles.Add(indices[v]);
triangles.Add(indices[w]);
indices.RemoveAt(v);
remaining--;
failSafe = remaining * 2;
}
}
return triangles;
}
private static float SignedArea(List<Vector2> polygon) {
float area = 0f;
int count = polygon.Count;
for(int i = 0; i < count; i++) {
Vector2 a = polygon[i];
Vector2 b = polygon[(i + 1) % count];
area += (b.x - a.x) * (b.y + a.y);
}
return area;
}
private static bool IsEar(List<Vector2> polygon, List<int> indices, int u, int v, int w, int remaining) {
Vector2 a = polygon[indices[u]];
Vector2 b = polygon[indices[v]];
Vector2 c = polygon[indices[w]];
// Must be convex (counter-clockwise winding after we've ensured CCW order)
float cross = (b.x - a.x) * (c.y - a.y) - (b.y - a.y) * (c.x - a.x);
if(cross <= 0f) {
return false;
}
// No other vertex must be inside this triangle
for(int p = 0; p < remaining; p++) {
if(p == u || p == v || p == w) {
continue;
}
if(PointInTriangle(polygon[indices[p]], a, b, c)) {
return false;
}
}
return true;
}
private static bool PointInTriangle(Vector2 p, Vector2 a, Vector2 b, Vector2 c) {
float d1 = (p.x - b.x) * (a.y - b.y) - (a.x - b.x) * (p.y - b.y);
float d2 = (p.x - c.x) * (b.y - c.y) - (b.x - c.x) * (p.y - c.y);
float d3 = (p.x - a.x) * (c.y - a.y) - (c.x - a.x) * (p.y - a.y);
bool hasNeg = (d1 < 0) || (d2 < 0) || (d3 < 0);
bool hasPos = (d1 > 0) || (d2 > 0) || (d3 > 0);
return !(hasNeg && hasPos);
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: c09971de26d1abf48ac379e5e8ac533c

49
Runtime/ShapeFactory.cs Normal file
View File

@@ -0,0 +1,49 @@
using System.Collections.Generic;
using UnityEngine;
namespace Jovian.ZoneSystem {
public static class ShapeFactory {
public const int CircleSegments = 24;
public const float DefaultRadius = 2f;
public const float DefaultSquareHalf = 2f;
public const float DefaultPolygonRadius = 3f;
public const int DefaultPolygonVertices = 12;
public static List<Vector2> CreateSquare(float halfSize = DefaultSquareHalf) {
return new List<Vector2> {
new(-halfSize, -halfSize),
new(-halfSize, halfSize),
new(halfSize, halfSize),
new(halfSize, -halfSize)
};
}
public static List<Vector2> CreateCircle(float radius = DefaultRadius, int segments = CircleSegments) {
List<Vector2> points = new List<Vector2>(segments);
float step = 2f * Mathf.PI / segments;
for(int i = 0; i < segments; i++) {
float angle = i * step;
points.Add(new Vector2(Mathf.Cos(angle) * radius, Mathf.Sin(angle) * radius));
}
return points;
}
public static List<Vector2> CreatePolygon(float radius = DefaultPolygonRadius, int vertices = DefaultPolygonVertices) {
return CreateCircle(radius, vertices);
}
public static List<Vector2> CreateDefault(ZoneShape shape) {
switch(shape) {
case ZoneShape.Square: return CreateSquare();
case ZoneShape.Circle: return CreateCircle();
case ZoneShape.Polygon: return CreatePolygon();
default: return CreateSquare();
}
}
public static void RegenerateCircle(ZoneData data) {
data.polygon.Clear();
data.polygon.AddRange(CreateCircle(data.circleRadius));
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 202475efe66c6304298b9073ef7627ea

62
Runtime/ZoneData.cs Normal file
View File

@@ -0,0 +1,62 @@
using System.Collections.Generic;
using UnityEngine;
namespace Jovian.ZoneSystem {
[CreateAssetMenu(fileName = "NewZone", menuName = "ZoneSystem/Zone Data")]
public class ZoneData : ScriptableObject {
[Header("Identity")]
public string zoneId;
public string zoneName;
public ZoneRole role = ZoneRole.Base;
public int priority = 1;
[Header("Visual (Editor Only)")]
public Color debugColor = new(1f, 0.5f, 0f, 0.25f);
// ── Base zone fields ────────────────────────────────────────────
[Header("Base Zone Settings")]
[Tooltip("Only used when Role = Base")]
public string encounterTableId;
[Tooltip("Only used when Role = Base")]
public DifficultyTier baseDifficultyTier = DifficultyTier.Mild;
[Tooltip("Base encounter chance per check (0..1). Only used when Role = Base")]
[Range(0f, 1f)]
public float baseEncounterChance = 0.2f;
// ── Modifier zone fields ─────────────────────────────────────────
[Header("Modifier Zone Settings")]
[Tooltip("Multiplied onto the base encounter chance. Only used when Role = Modifier")]
public float encounterChanceMultiplier = 1f;
[Tooltip("Added to the base difficulty tier (clamped). Only used when Role = Modifier")]
public int difficultyTierBonus;
// ── Override zone fields ─────────────────────────────────────────
[Header("Override Zone Settings")]
[Tooltip("If true, no encounters occur in this zone. Only used when Role = Override")]
public bool isSafeZone;
[Tooltip("Only used when Role = Override and isSafeZone = false")]
public string overrideEncounterTableId;
[Tooltip("Only used when Role = Override and isSafeZone = false")]
[Range(0f, 1f)]
public float overrideEncounterChance = 1f;
[Tooltip("Only used when Role = Override and isSafeZone = false")]
public DifficultyTier overrideDifficultyTier = DifficultyTier.Deadly;
// ── Shape ────────────────────────────────────────────────────────
[HideInInspector]
public ZoneShape shape = ZoneShape.Square;
[HideInInspector]
public float circleRadius = 2f;
[HideInInspector]
public List<Vector2> polygon = new();
}
}

2
Runtime/ZoneData.cs.meta Normal file
View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 8497d766078e5764a9c7c0dd5d671561

91
Runtime/ZoneExporter.cs Normal file
View File

@@ -0,0 +1,91 @@
using System;
using System.Collections.Generic;
using UnityEngine;
namespace Jovian.ZoneSystem {
/// <summary>
/// Serializable representations for JSON export.
/// Kept in Runtime so server-side or headless builds can also consume them.
/// </summary>
[Serializable]
public class ZoneExportEntry {
public string id;
public string name;
public string role;
public int priority;
// Base
public string encounterTableId;
public int baseDifficultyTier;
public float baseEncounterChance;
// Modifier
public float encounterChanceMultiplier;
public int difficultyTierBonus;
// Override
public bool isSafeZone;
public string overrideEncounterTableId;
public float overrideEncounterChance;
public int overrideDifficultyTier;
// Shape
public string shape;
public float circleRadius;
public float[] position;
public List<float[]> polygon;
}
[Serializable]
public class ZoneExportRoot {
public List<ZoneExportEntry> zones = new();
}
public static class ZoneExporter {
public static ZoneExportRoot BuildExport(ZoneInstance[] instances, MapPlane plane = MapPlane.XZ) {
ZoneExportRoot root = new ZoneExportRoot();
foreach(ZoneInstance inst in instances) {
if(inst.data == null) {
continue;
}
ZoneData d = inst.data;
Vector3 pos = inst.transform.position;
Vector2 origin = MapPlaneUtility.ProjectToPlane(pos, plane);
ZoneExportEntry entry = new ZoneExportEntry {
id = d.zoneId,
name = d.zoneName,
role = d.role.ToString(),
priority = d.priority,
shape = d.shape.ToString(),
circleRadius = d.circleRadius,
position = new[] { pos.x, pos.y, pos.z },
encounterTableId = d.encounterTableId,
baseDifficultyTier = (int)d.baseDifficultyTier,
baseEncounterChance = d.baseEncounterChance,
encounterChanceMultiplier = d.encounterChanceMultiplier,
difficultyTierBonus = d.difficultyTierBonus,
isSafeZone = d.isSafeZone,
overrideEncounterTableId = d.overrideEncounterTableId,
overrideEncounterChance = d.overrideEncounterChance,
overrideDifficultyTier = (int)d.overrideDifficultyTier,
polygon = new List<float[]>()
};
foreach(Vector2 pt in d.polygon) {
Vector2 worldPt = pt + origin;
entry.polygon.Add(new[] { worldPt.x, worldPt.y });
}
root.zones.Add(entry);
}
return root;
}
public static string ToJson(ZoneExportRoot root, bool pretty = true) {
return JsonUtility.ToJson(root, pretty);
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 321272ab8f26941488d472164a97c162

84
Runtime/ZoneInstance.cs Normal file
View File

@@ -0,0 +1,84 @@
using System.Collections.Generic;
using UnityEngine;
namespace Jovian.ZoneSystem {
public class ZoneInstance : MonoBehaviour {
public ZoneData data;
private Vector2 _boundsMax;
// Cached AABB for fast pre-rejection (rebuilt when data changes)
private Vector2 _boundsMin;
private bool _boundsValid;
private void Awake() {
RebuildBoundsCache();
}
#if UNITY_EDITOR
private void OnDrawGizmos() {
if(data == null || data.polygon == null || data.polygon.Count < 2) {
return;
}
MapPlane plane = MapPlane.XZ;
ZonesObjectHolder mgr = FindFirstObjectByType<ZonesObjectHolder>();
if(mgr != null) {
plane = mgr.mapPlane;
}
float depth = plane == MapPlane.XZ ? transform.position.y
: plane == MapPlane.YZ ? transform.position.x
: transform.position.z;
Vector2 origin = MapPlaneUtility.ProjectToPlane(transform.position, plane);
Gizmos.color = data.debugColor;
List<Vector2> pts = data.polygon;
for(int i = 0; i < pts.Count; i++) {
Vector3 a = MapPlaneUtility.UnprojectFromPlane(pts[i] + origin, plane, depth);
Vector3 b = MapPlaneUtility.UnprojectFromPlane(pts[(i + 1) % pts.Count] + origin, plane, depth);
Gizmos.DrawLine(a, b);
}
}
#endif
private void OnValidate() {
RebuildBoundsCache();
}
/// <summary>
/// Rebuilds the AABB cache from the current polygon data.
/// Called automatically on Awake/Validate; also call this in the
/// editor after modifying polygon points.
/// </summary>
public void RebuildBoundsCache() {
if(data == null || data.polygon == null || data.polygon.Count < 3) {
_boundsValid = false;
return;
}
(_boundsMin, _boundsMax) = PolygonUtils.Bounds(data.polygon);
_boundsValid = true;
}
/// <summary>
/// Returns true if the given world position is inside this zone's polygon.
/// Plane controls which two axes are used for the 2D projection.
/// </summary>
public bool Contains(Vector3 worldPos, MapPlane plane) {
if(data == null || data.polygon == null || data.polygon.Count < 3) {
return false;
}
Vector2 projected = MapPlaneUtility.ProjectToPlane(worldPos, plane);
Vector2 origin = MapPlaneUtility.ProjectToPlane(transform.position, plane);
Vector2 localPoint = projected - origin;
// Fast AABB reject before running the full ray-cast
if(_boundsValid && !PolygonUtils.PointInBounds(localPoint, _boundsMin, _boundsMax)) {
return false;
}
return PolygonUtils.PointInPolygon(localPoint, data.polygon);
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 95af4f7ff0649854598833eabd84f131

83
Runtime/ZoneResolver.cs Normal file
View File

@@ -0,0 +1,83 @@
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
namespace Jovian.ZoneSystem {
public static class ZoneResolver {
/// <summary>
/// Resolves a list of overlapping ZoneData into a single ZoneContext.
/// Resolution rules:
/// 1. If any Override zone is present → use highest-priority Override exclusively.
/// 2. Otherwise → find highest-priority Base zone, then stack all Modifier zones
/// multiplicatively on top.
/// </summary>
public static ZoneContext Resolve(List<ZoneData> overlapping) {
if(overlapping == null || overlapping.Count == 0) {
return SafeFallback(string.Empty);
}
// ── 1. Check for Override zones ──────────────────────────────
var overrides = overlapping
.Where(z => z.role == ZoneRole.Override)
.OrderByDescending(z => z.priority)
.ToList();
if(overrides.Count > 0) {
var ov = overrides[0];
return new ZoneContext {
resolvedZoneId = ov.zoneId,
isSafe = ov.isSafeZone,
encounterTableId = ov.overrideEncounterTableId,
finalEncounterChance = ov.overrideEncounterChance,
finalDifficultyTier = ov.overrideDifficultyTier
};
}
// ── 2. Find highest-priority Base zone ───────────────────────
var baseZone = overlapping
.Where(z => z.role == ZoneRole.Base)
.OrderByDescending(z => z.priority)
.FirstOrDefault();
if(!baseZone) {
return SafeFallback(string.Empty);
}
// ── 3. Collect all Modifier zones ────────────────────────────
var modifiers = overlapping
.Where(z => z.role == ZoneRole.Modifier)
.ToList();
var chance = baseZone.baseEncounterChance;
var tierOffset = 0;
foreach(var mod in modifiers) {
// Multiplicative stacking — each modifier is independent
chance *= mod.encounterChanceMultiplier;
tierOffset += mod.difficultyTierBonus;
}
chance = Mathf.Clamp01(chance);
var rawTier = (int)baseZone.baseDifficultyTier + tierOffset;
var clampedTier = Mathf.Clamp(rawTier, (int)DifficultyTier.Safe, (int)DifficultyTier.Deadly);
return new ZoneContext {
resolvedZoneId = baseZone.zoneId,
isSafe = false,
encounterTableId = baseZone.encounterTableId,
finalEncounterChance = chance,
finalDifficultyTier = (DifficultyTier)clampedTier
};
}
private static ZoneContext SafeFallback(string name) {
return new ZoneContext {
resolvedZoneId = name,
isSafe = true,
encounterTableId = string.Empty,
finalEncounterChance = 0f,
finalDifficultyTier = DifficultyTier.Safe
};
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 147813fb56be464458dc1c5be47057f4

90
Runtime/ZoneSystem.cs Normal file
View File

@@ -0,0 +1,90 @@
using System.Collections.Generic;
using UnityEngine;
namespace Jovian.ZoneSystem {
public class ZoneSystem {
private readonly ZonesObjectHolder zonesObjectHolder;
public ZoneSystem(ZonesObjectHolder zonesObjectHolder) {
this.zonesObjectHolder = zonesObjectHolder;
Refresh();
}
/// <summary>
/// Returns the resolved ZoneContext at the given world position.
/// This is the only call your encounter/travel system needs to make.
/// </summary>
public ZoneContext QueryZone(Vector3 worldPos) {
var overlapping = GetOverlappingZones(worldPos);
return ZoneResolver.Resolve(overlapping);
}
/// <summary>
/// Returns all ZoneData assets whose polygons contain worldPos.
/// Ordered by descending priority — useful if you need the raw list
/// before resolution (e.g. for debug UI).
/// </summary>
public List<ZoneData> GetOverlappingZones(Vector3 worldPos) {
var result = new List<ZoneData>();
foreach(var zone in zonesObjectHolder.Zones) {
if(zone == null || zone.data == null) {
continue;
}
if(zone.Contains(worldPos, zonesObjectHolder.mapPlane)) {
result.Add(zone.data);
}
}
result.Sort((a, b) => b.priority.CompareTo(a.priority));
return result;
}
/// <summary>
/// Returns true if worldPos is inside any zone that is an Override+Safe zone
/// (i.e. a town or safe area). Cheap shortcut before rolling encounters.
/// </summary>
public bool IsInSafeZone(Vector3 worldPos) {
foreach(var zone in zonesObjectHolder.Zones) {
if(zone == null || zone.data == null) {
continue;
}
if(zone.data.role == ZoneRole.Override &&
zone.data.isSafeZone &&
zone.Contains(worldPos, zonesObjectHolder.mapPlane)) {
return true;
}
}
return false;
}
/// <summary>
/// Registers a single ZoneInstance dynamically (e.g. spawned at runtime).
/// </summary>
internal void Register(ZoneInstance zone) {
if(!zonesObjectHolder.Zones.Contains(zone)) {
zone.RebuildBoundsCache();
zonesObjectHolder.Zones.Add(zone);
}
}
/// <summary>
/// Unregisters a ZoneInstance (e.g. before it is destroyed at runtime).
/// </summary>
public void Unregister(ZoneInstance zone) {
zonesObjectHolder.Zones.Remove(zone);
}
/// <summary>
/// Re-scans the scene for all ZoneInstances and rebuilds their bounds caches.
/// Call this if you add or remove zones at runtime.
/// </summary>
private void Refresh() {
zonesObjectHolder.Zones.Clear();
ZoneInstance[] found = Object.FindObjectsByType<ZoneInstance>(FindObjectsSortMode.None);
foreach(ZoneInstance z in found) {
z.RebuildBoundsCache();
zonesObjectHolder.Zones.Add(z);
}
}
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 366abba6f6364bbfb3d0564358ead42c
timeCreated: 1772985323

33
Runtime/ZoneTypes.cs Normal file
View File

@@ -0,0 +1,33 @@
namespace Jovian.ZoneSystem {
public enum ZoneRole {
Base, // Provides the encounter table and baseline difficulty
Modifier, // Mutates difficulty/chance on top of a base zone
Override // Completely replaces everything (safe towns, story events)
}
public enum ZoneShape {
Square,
Circle,
Polygon
}
public enum DifficultyTier {
Safe = 0,
Mild = 1,
Moderate = 2,
Dangerous = 3,
Deadly = 4
}
/// <summary>
/// The resolved result of overlapping zones at a world position.
/// This is what the encounter system consumes — it never needs to know about raw zones.
/// </summary>
public struct ZoneContext {
public string encounterTableId;
public float finalEncounterChance; // 0..1
public DifficultyTier finalDifficultyTier;
public bool isSafe;
public string resolvedZoneId;
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: ee375ef89c9f9574594736f1984be25f

View File

@@ -0,0 +1,35 @@
using System.Collections.Generic;
using UnityEngine;
namespace Jovian.ZoneSystem {
public class ZonesObjectHolder: MonoBehaviour {
internal List<ZoneInstance> Zones { get; } = new();
public MapPlane mapPlane;
public IReadOnlyList<ZoneInstance> AllZones => Zones;
private void Awake() {
Refresh();
}
#if UNITY_EDITOR
private void OnValidate() {
Refresh();
}
#endif
/// <summary>
/// Re-scans the scene for all ZoneInstances and rebuilds their bounds caches.
/// Call this if you add or remove zones at runtime.
/// </summary>
private void Refresh() {
Zones.Clear();
var found = FindObjectsByType<ZoneInstance>(FindObjectsSortMode.None);
foreach(var z in found) {
z.RebuildBoundsCache();
Zones.Add(z);
}
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 514ac2296ff6032459b84681867b26cd