feat: Implement missions 1-5 (combat fluidity, urban crowd locomotion, smart traffic signals, dynamic gacha pity, universal item spawner & armory)

This commit is contained in:
CAtlas0784 committed 2026-09-18 19:05:47 +07:00
1 parent 5e333d7678
commit ce1f471eab
27 files changed
+321970 -135

No files matched your search

@@ -13,6 +13,7 @@ public sealed class CityTrafficEngine
private readonly ConcurrentDictionary<ulong, SimulatedVehicle> _simulatedVehicles = new();
private RoadNetworkFile? _roadNetwork;
private TrafficSignalFile? _signalData;
private readonly Dictionary<int, RoadLaneData> _laneLookup = new();
private CancellationTokenSource? _cts;
@@ -69,30 +70,46 @@ public sealed class CityTrafficEngine
_sessionProvider = sessionProvider;
var roadPath = Path.Combine(clientDataPath, "World", "RoadNetwork.json");
if (!File.Exists(roadPath))
if (File.Exists(roadPath))
{
Console.WriteLine($"[TRAFFIC] RoadNetwork.json not found at: {roadPath}");
return;
}
try
{
Console.WriteLine($"[TRAFFIC] Loading road network from: {roadPath}...");
var json = File.ReadAllText(roadPath);
_roadNetwork = JsonSerializer.Deserialize<RoadNetworkFile>(json);
if (_roadNetwork is not null)
try
{
foreach (var lane in _roadNetwork.Lanes)
Console.WriteLine($"[TRAFFIC] Loading road network from: {roadPath}...");
var json = File.ReadAllText(roadPath);
_roadNetwork = JsonSerializer.Deserialize<RoadNetworkFile>(json);
if (_roadNetwork is not null)
{
lane.Precompute();
_laneLookup[lane.Id] = lane;
foreach (var lane in _roadNetwork.Lanes)
{
lane.Precompute();
_laneLookup[lane.Id] = lane;
}
Console.WriteLine($"[TRAFFIC] Road network loaded: {_roadNetwork.Lanes.Count} lanes precomputed across {_roadNetwork.SpatialGrid.Count} spatial cells.");
}
Console.WriteLine($"[TRAFFIC] Road network loaded: {_roadNetwork.Lanes.Count} lanes precomputed across {_roadNetwork.SpatialGrid.Count} spatial cells.");
}
catch (Exception ex)
{
Console.WriteLine($"[TRAFFIC] Failed to load road network: {ex.Message}");
}
}
catch (Exception ex)
else
{
Console.WriteLine($"[TRAFFIC] Failed to load road network: {ex.Message}");
Console.WriteLine($"[TRAFFIC] RoadNetwork.json not found at: {roadPath}");
}
var signalPath = Path.Combine(clientDataPath, "World", "TrafficSignals.json");
if (File.Exists(signalPath))
{
try
{
var sJson = File.ReadAllText(signalPath);
_signalData = JsonSerializer.Deserialize<TrafficSignalFile>(sJson);
Console.WriteLine($"[TRAFFIC] Loaded {_signalData?.Lights.Count ?? 0} traffic signals from {signalPath}.");
}
catch (Exception ex)
{
Console.WriteLine($"[TRAFFIC] Error loading traffic signals: {ex.Message}");
}
}
Start();
@@ -194,7 +211,48 @@ public sealed class CityTrafficEngine
continue;
}
v.DistanceOnLane += v.Speed * SpeedScale * dt;
// 1. Car-Following: Check if there is another vehicle ahead on the same lane
var effectiveSpeed = v.Speed;
SimulatedVehicle? leadingVehicle = null;
var minGap = float.MaxValue;
foreach (var other in vehicleEntries)
{
if (other.EntityId == v.EntityId || other.HijackedByPlayer)
continue;
if (other.CurrentLaneId == v.CurrentLaneId && other.DistanceOnLane > v.DistanceOnLane)
{
var gap = other.DistanceOnLane - v.DistanceOnLane;
if (gap < minGap)
{
minGap = gap;
leadingVehicle = other;
}
}
}
if (leadingVehicle is not null)
{
if (minGap < 5f)
{
// Emergency stop to prevent rear-end collision
effectiveSpeed = 0f;
}
else if (minGap < 14f)
{
// Smooth deceleration matching leader
effectiveSpeed = MathF.Min(effectiveSpeed, leadingVehicle.Speed * ((minGap - 4f) / 10f));
}
}
// 2. Traffic Light check: if approaching a red signal, decelerate/stop
if (effectiveSpeed > 0f && IsTrafficLightRedForVehicle(v.X, v.Y, v.Z, v.Yaw))
{
effectiveSpeed = MathF.Max(0f, effectiveSpeed - 6f * dt);
}
v.DistanceOnLane += effectiveSpeed * SpeedScale * dt;
// Check if vehicle reached the end of the lane
if (v.DistanceOnLane >= currentLane.TotalLength)
@@ -228,7 +286,7 @@ public sealed class CityTrafficEngine
}
// Calculate new spline position & velocity
var (nx, ny, nz, nyaw, vx, vz) = currentLane.Evaluate(v.DistanceOnLane, v.Speed * SpeedScale);
var (nx, ny, nz, nyaw, vx, vz) = currentLane.Evaluate(v.DistanceOnLane, effectiveSpeed * SpeedScale);
v.X = nx;
v.Y = ny;
v.Z = nz;
@@ -364,6 +422,34 @@ public sealed class CityTrafficEngine
Console.WriteLine($"[TRAFFIC] Cleared {ids.Count} traffic vehicles.");
}
private bool IsTrafficLightRedForVehicle(float vx, float vy, float vz, float vyaw)
{
if (_signalData is null || _signalData.Lights.Count == 0)
return false;
var cycleSecond = (DateTime.UtcNow.Ticks / TimeSpan.TicksPerSecond) % 30; // 30s cycle
foreach (var light in _signalData.Lights)
{
if (light.Pos.Count < 3) continue;
var ldx = light.Pos[0] - vx;
var ldz = light.Pos[2] - vz;
var distSq = ldx * ldx + ldz * ldz;
// Approaching intersection light within 6m to 24m
if (distSq >= 6f * 6f && distSq <= 24f * 24f)
{
var isPhaseA = (light.Zbr % 2) == 0;
var isRed = isPhaseA ? (cycleSecond >= 14) : (cycleSecond < 15 || cycleSecond == 29);
if (isRed)
return true;
}
}
return false;
}
public TrafficStatus GetStatus() => new()
{
Enabled = Enabled,
@@ -4,6 +4,30 @@ using Ananta.Server.RpcTypes.Client4229938;
namespace Ananta.Server.Gameplay.Traffic;
public sealed class TrafficSignalFile
{
[JsonPropertyName("lights")]
public List<TrafficLightEntry> Lights { get; set; } = [];
}
public sealed class TrafficLightEntry
{
[JsonPropertyName("handle")]
public long Handle { get; set; }
[JsonPropertyName("inter")]
public int Inter { get; set; }
[JsonPropertyName("zbr")]
public int Zbr { get; set; }
[JsonPropertyName("pos")]
public List<float> Pos { get; set; } = [];
[JsonPropertyName("fwd")]
public List<float> Fwd { get; set; } = [];
}
public sealed class RoadNetworkFile
{
[JsonPropertyName("metadata")]