This commit is contained in:
root
2026-04-26 12:44:06 +02:00
commit 9590629795
398 changed files with 26617 additions and 0 deletions

508
GameLogic.cs Normal file
View File

@@ -0,0 +1,508 @@
namespace GeoSus.Server;
using Microsoft.Extensions.Logging;
using System.Threading.Channels;
using System.Security.Cryptography;
// Herní logika - kill, meeting, voting, tasks
public class GameLogic
{
private readonly ServerConfig _config;
private readonly ILogger _logger;
private readonly OverpassService? _overpassService;
private readonly Random _random = new Random(); // Sdílená instance pro lepší náhodnost
public GameLogic(ServerConfig config, ILogger logger, OverpassService? overpassService = null)
{
_config = config;
_logger = logger;
_overpassService = overpassService;
}
#region Zabíjení
public (bool Success, string? Error, Body? Body) TryKill(
Player killer, Player victim, Dictionary<string, Player> players, List<Body> bodies)
{
// Validace - killer musí být impostor a alive
if (killer.Role != PlayerRole.Impostor)
return (false, "Nejsi impostor", null);
if (killer.State != PlayerState.Alive)
return (false, "Jsi mrtvý", null);
if (victim.State != PlayerState.Alive)
return (false, "Oběť je již mrtvá", null);
// Cooldown kontrola
if (killer.LastKillTime.HasValue)
{
var elapsed = (DateTime.UtcNow - killer.LastKillTime.Value).TotalMilliseconds;
if (elapsed < _config.KillCooldownMs)
{
var remaining = (_config.KillCooldownMs - elapsed) / 1000;
return (false, $"Cooldown: {remaining:F1}s", null);
}
}
// Vzdálenost
var distance = killer.Position.DistanceTo(victim.Position);
if (distance > _config.KillDistanceM)
return (false, $"Příliš daleko ({distance:F1}m)", null);
// Kill úspěšný
victim.State = PlayerState.Dead;
killer.LastKillTime = DateTime.UtcNow;
var body = new Body
{
BodyId = Guid.NewGuid().ToString("N")[..8],
VictimId = victim.ClientUuid,
KillerId = killer.ClientUuid,
Location = victim.Position,
KilledAt = DateTime.UtcNow
};
bodies.Add(body);
_logger.LogInformation("Kill: {Killer} zabil {Victim}, body {BodyId}",
killer.ClientUuid, victim.ClientUuid, body.BodyId);
return (true, null, body);
}
#endregion
#region Reporty a meetingy
public (bool Success, string? Error, Body? Body) TryReportBody(
Player reporter, string bodyId, List<Body> bodies)
{
if (reporter.State != PlayerState.Alive)
return (false, "Jsi mrtvý", null);
var body = bodies.FirstOrDefault(b => b.BodyId == bodyId);
if (body == null)
return (false, "Tělo neexistuje", null);
if (body.Reported)
return (false, "Tělo již bylo reportnuto", null);
// Vzdálenost
var distance = reporter.Position.DistanceTo(body.Location);
if (distance > _config.ReportDistanceM)
return (false, $"Příliš daleko ({distance:F1}m)", null);
body.Reported = true;
body.ReportedBy = reporter.ClientUuid;
_logger.LogInformation("Report: {Reporter} reportnul tělo {BodyId}",
reporter.ClientUuid, bodyId);
return (true, null, body);
}
public (bool Success, string? Error) TryCallEmergencyMeeting(
Player caller, int maxMeetings, int cooldownMs)
{
if (caller.State != PlayerState.Alive)
return (false, "Jsi mrtvý");
// Limit na počet emergency meetings
if (caller.EmergencyMeetingsUsed >= maxMeetings)
return (false, $"Vyčerpal jsi emergency meetingy ({maxMeetings})");
// Cooldown
if (caller.LastEmergencyMeetingTime.HasValue)
{
var elapsed = (DateTime.UtcNow - caller.LastEmergencyMeetingTime.Value).TotalMilliseconds;
if (elapsed < cooldownMs)
{
var remaining = (cooldownMs - elapsed) / 1000;
return (false, $"Cooldown: {remaining:F1}s");
}
}
caller.EmergencyMeetingsUsed++;
caller.LastEmergencyMeetingTime = DateTime.UtcNow;
_logger.LogInformation("Emergency meeting: {Caller}", caller.ClientUuid);
return (true, null);
}
public Meeting StartMeeting(
MeetingType type, string callerId, Position meetingLocation,
string? reportedBodyId, int arrivalBaseMs, int votingPhaseMs, int discussionPhaseMs)
{
var now = DateTime.UtcNow;
return new Meeting
{
MeetingId = Guid.NewGuid().ToString("N")[..8],
Type = type,
CallerId = callerId,
ReportedBodyId = reportedBodyId,
MeetingLocation = meetingLocation,
StartTime = now,
ArrivalDeadline = now.AddMilliseconds(arrivalBaseMs + _config.ArrivalSafetyMarginMs),
DiscussionEndTime = discussionPhaseMs > 0 ? now.AddMilliseconds(arrivalBaseMs + discussionPhaseMs) : null,
VotingEndTime = now.AddMilliseconds(arrivalBaseMs + discussionPhaseMs + votingPhaseMs)
};
}
public bool CheckPlayerArrival(Player player, Meeting meeting)
{
if (player.State != PlayerState.Alive)
return false;
var distance = player.Position.DistanceTo(meeting.MeetingLocation);
var now = DateTime.UtcNow;
// Grace period
var deadline = meeting.ArrivalDeadline.AddMilliseconds(_config.AllowedLateMs);
if (now <= deadline && distance <= _config.MeetingArrivalRadiusM)
{
meeting.ArrivedPlayers.Add(player.ClientUuid);
return true;
}
return false;
}
#endregion
#region Hlasování
public (bool Success, string? Error) TryCastVote(
Player voter, string? targetId, Meeting meeting, Dictionary<string, Player> players)
{
if (voter.State != PlayerState.Alive)
return (false, "Jsi mrtvý");
if (!meeting.ArrivedPlayers.Contains(voter.ClientUuid))
return (false, "Nedorazil jsi včas na meeting");
var now = DateTime.UtcNow;
// Kontrola fáze - musíme být ve voting fázi
if (meeting.DiscussionEndTime.HasValue && now < meeting.DiscussionEndTime.Value)
return (false, "Ještě probíhá diskuze");
if (now > meeting.VotingEndTime)
return (false, "Hlasování skončilo");
// Rate limit na změny hlasu
if (meeting.Votes.ContainsKey(voter.ClientUuid) && meeting.LastVoteChangeTime.HasValue)
{
var sinceLastChange = (now - meeting.LastVoteChangeTime.Value).TotalMilliseconds;
if (sinceLastChange < 2000)
return (false, "Příliš rychlá změna hlasu");
}
// Validace targetu
if (targetId != null)
{
if (!players.TryGetValue(targetId, out var target))
return (false, "Neplatný cíl hlasu");
if (target.State != PlayerState.Alive)
return (false, "Cíl je mrtvý");
}
meeting.Votes[voter.ClientUuid] = targetId;
meeting.LastVoteChangeTime = now;
return (true, null);
}
public (string? EjectedId, bool WasTie, Dictionary<string, int> VoteCounts) ResolveVoting(
Meeting meeting, Dictionary<string, Player> players, TiePolicy tiePolicy)
{
var voteCounts = new Dictionary<string, int>();
var skipCount = 0;
foreach (var (voterId, targetId) in meeting.Votes)
{
if (targetId == null)
{
skipCount++;
}
else
{
voteCounts.TryAdd(targetId, 0);
voteCounts[targetId]++;
}
}
voteCounts["__SKIP__"] = skipCount;
// Najdeme maximum
var maxVotes = voteCounts.Values.Max();
var topVoted = voteCounts.Where(kv => kv.Value == maxVotes).Select(kv => kv.Key).ToList();
// Kontrola remízy
if (topVoted.Count > 1 || topVoted.Contains("__SKIP__"))
{
// Remíza nebo skip vyhrál
switch (tiePolicy)
{
case TiePolicy.NoEject:
return (null, topVoted.Count > 1, voteCounts);
case TiePolicy.Random:
if (topVoted.Contains("__SKIP__"))
topVoted.Remove("__SKIP__");
if (topVoted.Count > 0)
{
var random = new Random();
var ejected = topVoted[random.Next(topVoted.Count)];
return (ejected, true, voteCounts);
}
return (null, true, voteCounts);
case TiePolicy.EjectLowestId:
if (topVoted.Contains("__SKIP__"))
topVoted.Remove("__SKIP__");
if (topVoted.Count > 0)
{
var ejected = topVoted.OrderBy(id => id).First();
return (ejected, true, voteCounts);
}
return (null, true, voteCounts);
default:
return (null, topVoted.Count > 1, voteCounts);
}
}
var winner = topVoted[0];
if (winner == "__SKIP__")
return (null, false, voteCounts);
return (winner, false, voteCounts);
}
public void EjectPlayer(Player player)
{
player.State = PlayerState.Dead;
_logger.LogInformation("Ejected: {Player}", player.ClientUuid);
}
#endregion
#region Tasky
/// <summary>
/// Pokusí se dokončit task. Kontroluje:
/// - Hráč je naživu
/// - Není impostor
/// - Task existuje a patří hráči
/// - Task ještě není dokončen
/// - Hráč je dostatečně blízko
/// Poznámka: Duchové (mrtví hráči) MOHOU plnit úkoly - to je důležité pro crew!
/// </summary>
public (bool Success, string? Error) TryCompleteTask(Player player, string taskId)
{
// Duchové mohou plnit úkoly - neblokujeme mrtvé hráče
// Ale musí být crew role
if (player.Role == PlayerRole.Impostor)
return (false, "Impostoři nemohou dělat tasky");
var task = player.Tasks.FirstOrDefault(t => t.TaskId == taskId);
if (task == null)
return (false, "Tento task ti nepatří");
if (player.CompletedTaskIds.Contains(taskId))
return (false, "Task již dokončen");
// Kontrola vzdálenosti
var distance = player.Position.DistanceTo(task.Location);
if (distance > _config.TaskStartDistanceM)
return (false, $"Příliš daleko ({distance:F1}m, max {_config.TaskStartDistanceM}m)");
// Dokončit task
player.CompletedTaskIds.Add(taskId);
_logger.LogInformation("Task completed: {Player} dokončil {Task} na pozici {Pos} (vzdálenost {Dist:F1}m)",
player.ClientUuid, task.Name, task.Location, distance);
return (true, null);
}
/// <summary>
/// Najde nejbližší nedokončený task hráče
/// </summary>
public GameTask? FindNearestTask(Player player, double maxDistance)
{
return player.Tasks
.Where(t => !player.CompletedTaskIds.Contains(t.TaskId))
.Where(t => player.Position.DistanceTo(t.Location) <= maxDistance)
.OrderBy(t => player.Position.DistanceTo(t.Location))
.FirstOrDefault();
}
#endregion
#region Win conditions
public (bool GameOver, string? WinningFaction, string? Reason) CheckWinConditions(
Dictionary<string, Player> players, List<GameTask> tasks)
{
var aliveCrew = players.Values.Count(p => p.State == PlayerState.Alive && p.Role == PlayerRole.Crew);
var aliveImpostors = players.Values.Count(p => p.State == PlayerState.Alive && p.Role == PlayerRole.Impostor);
// Impostoři vyhráli - mají většinu nebo rovnost
if (aliveImpostors >= aliveCrew && aliveCrew > 0)
{
return (true, "Impostor", "Impostoři mají převahu");
}
// Všichni impostoři mrtví
if (aliveImpostors == 0)
{
return (true, "Crew", "Všichni impostoři eliminováni");
}
// Všechny tasky hotové (počítáme pouze crew tasky)
var crewPlayers = players.Values.Where(p => p.Role == PlayerRole.Crew).ToList();
var totalTasks = crewPlayers.Sum(p => p.Tasks.Count);
var completedTasks = crewPlayers.Sum(p => p.CompletedTaskIds.Count);
if (totalTasks > 0 && completedTasks >= totalTasks)
{
return (true, "Crew", "Všechny tasky dokončeny");
}
return (false, null, null);
}
#endregion
#region Role assignment
public void AssignRoles(List<Player> players, int impostorCount)
{
if (players.Count < 2)
{
// Minimálně 2 hráči
foreach (var p in players)
p.Role = PlayerRole.Crew;
return;
}
// Omezení počtu impostorů
var maxImpostors = Math.Max(1, players.Count / 3);
impostorCount = Math.Min(impostorCount, maxImpostors);
impostorCount = Math.Min(impostorCount, players.Count - 1);
// Náhodně vybereme impostory
var shuffled = players.OrderBy(_ => RandomNumberGenerator.GetInt32(int.MaxValue)).ToList();
for (int i = 0; i < players.Count; i++)
{
shuffled[i].Role = i < impostorCount ? PlayerRole.Impostor : PlayerRole.Crew;
shuffled[i].State = PlayerState.Alive;
shuffled[i].EmergencyMeetingsUsed = 0;
shuffled[i].LastEmergencyMeetingTime = null;
shuffled[i].LastKillTime = null;
shuffled[i].CompletedTaskIds.Clear();
shuffled[i].CurrentTaskId = null;
}
_logger.LogInformation("Roles assigned: {ImpostorCount} impostorů z {TotalPlayers} hráčů",
impostorCount, players.Count);
}
public List<GameTask> GenerateTasks(int count, Position center, double radius, MapData? mapData = null, int startIndex = 0)
{
var tasks = new List<GameTask>();
var taskNames = new[] {
"Opravit kabel", "Zkalibrovat senzor", "Stáhnout data",
"Nabít baterii", "Vyčistit filtr", "Nastavit kompas",
"Opravit antenu", "Zkontrolovat zásoby", "Otestovat reaktor"
};
// Get task positions - use map data if available
List<Position> positions;
if (mapData != null && _overpassService != null && mapData.ReachablePositions.Count >= count)
{
positions = _overpassService.GetPOIBasedPositions(mapData, count, _random);
_logger.LogInformation("Generated {Count} positions for tasks using map data", positions.Count);
}
else
{
positions = GenerateRandomPositions(count, center, radius, _random);
_logger.LogDebug("Generated {Count} positions for tasks using random fallback", positions.Count);
}
for (int i = 0; i < count && i < positions.Count; i++)
{
string taskId = $"task_{startIndex + i}";
string taskName = taskNames[(startIndex + i) % taskNames.Length];
tasks.Add(new GameTask
{
TaskId = taskId,
Name = taskName,
Location = positions[i],
Type = TaskType.Instant
});
}
return tasks;
}
private List<Position> GenerateRandomPositions(int count, Position center, double radius, Random random)
{
var positions = new List<Position>();
for (int i = 0; i < count; i++)
{
// Náhodná pozice v kruhu
var angle = random.NextDouble() * 2 * Math.PI;
var distance = random.NextDouble() * radius * 0.8; // 80% radius aby nebyly na kraji
var lat = center.Lat + (distance / 111000) * Math.Cos(angle);
var lon = center.Lon + (distance / (111000 * Math.Cos(center.Lat * Math.PI / 180))) * Math.Sin(angle);
positions.Add(new Position(lat, lon));
}
return positions;
}
/// <summary>
/// Generate repair station positions using map data for reachability
/// </summary>
public List<Position> GenerateRepairStationPositions(int count, Position center, double radius, MapData? mapData = null)
{
if (mapData != null && _overpassService != null && mapData.ReachablePositions.Count >= count)
{
// Get well-distributed reachable positions
var positions = _overpassService.GetDistributedReachablePositions(
mapData, count, _random, _config.MinTaskSpacingMeters * 2);
if (positions.Count >= count)
{
_logger.LogInformation("Generated {Count} repair station positions using map data", positions.Count);
return positions;
}
}
// Fallback: opposite ends of play area
var result = new List<Position>();
for (int i = 0; i < count; i++)
{
var angle = (2 * Math.PI * i) / count;
var dist = radius * 0.7;
var lat = center.Lat + (dist * Math.Cos(angle)) / 111000.0;
var lon = center.Lon + (dist * Math.Sin(angle)) / (111000.0 * Math.Cos(center.Lat * Math.PI / 180));
result.Add(new Position(lat, lon));
}
return result;
}
#endregion
}