1
This commit is contained in:
@@ -96,6 +96,7 @@ namespace FishGameService.Services
|
||||
RoomLevel = roomLevel,
|
||||
Config = config,
|
||||
};
|
||||
ResetBossSpawnState(state);
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
lock (state.Lock)
|
||||
@@ -132,6 +133,7 @@ namespace FishGameService.Services
|
||||
|
||||
state.Config = newConfig;
|
||||
state.NextSpawnByType.Clear();
|
||||
ResetBossSpawnState(state);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -893,6 +895,12 @@ namespace FishGameService.Services
|
||||
return spawned;
|
||||
}
|
||||
|
||||
if (prob.FishType == (int)CommonDomain.Enum.FishTypeEnum.Boss鱼)
|
||||
return SpawnBossFishByQueue(state, candidates, spawnCount, now, random, isFirst);
|
||||
|
||||
if (prob.FishType == (int)CommonDomain.Enum.FishTypeEnum.特殊Boss)
|
||||
return SpawnSpecialBossByCycle(state, candidates, spawnCount, now, random, isFirst);
|
||||
|
||||
var maxAttempts = Math.Max(20, spawnCount * 10);
|
||||
for (int attempt = 0; spawned.Count < spawnCount && attempt < maxAttempts; attempt++)
|
||||
{
|
||||
@@ -921,7 +929,6 @@ namespace FishGameService.Services
|
||||
now < nextAllowedAt)
|
||||
continue;
|
||||
|
||||
var fishId = NextFishId();
|
||||
var routeId = PickRoute(fishCfg, random, requestedOdd);
|
||||
|
||||
if (alternateBySide && ((routeId & 1) == 1) != requestedOdd)
|
||||
@@ -962,49 +969,7 @@ namespace FishGameService.Services
|
||||
preferOdd = !preferOdd;
|
||||
}
|
||||
|
||||
var times = fishCfg.Times > 0 ? fishCfg.Times : 8;
|
||||
|
||||
DateTime createdAt;
|
||||
if (isFirst)
|
||||
{
|
||||
var pastOffset = times * (0.2 + random.NextDouble() * 0.6);
|
||||
createdAt = now.AddSeconds(-pastOffset);
|
||||
}
|
||||
else
|
||||
{
|
||||
createdAt = now;
|
||||
}
|
||||
|
||||
var activeFish = new ActiveFish
|
||||
{
|
||||
FishId = fishId,
|
||||
FishNumber = fishCfg.FishNumber,
|
||||
FishType = fishCfg.FishType,
|
||||
RouteId = routeId,
|
||||
CreatedAt = createdAt,
|
||||
ExpireAt = createdAt.AddSeconds(times),
|
||||
IsKilled = false
|
||||
};
|
||||
|
||||
state.ActiveFishes[fishId] = activeFish;
|
||||
if (fishCfg.FishType == 2 || fishCfg.FishType == 5)
|
||||
{
|
||||
state.NextAllowedSpawnByFishNumber[fishCfg.FishNumber] =
|
||||
activeFish.ExpireAt.AddSeconds(RestrictedFishRespawnBufferSeconds);
|
||||
}
|
||||
state.FishCountByType.TryGetValue(fishCfg.FishType, out var fc);
|
||||
state.FishCountByType[fishCfg.FishType] = fc + 1;
|
||||
|
||||
spawned.Add(new SpawnFishItem
|
||||
{
|
||||
FishId = fishId,
|
||||
FishIds = new List<long> { fishId },
|
||||
FishNumber = fishCfg.FishNumber,
|
||||
FishType = fishCfg.FishType,
|
||||
RouteId = routeId,
|
||||
CreatedAtMs = new DateTimeOffset(createdAt).ToUnixTimeMilliseconds(),
|
||||
ExpireAtMs = new DateTimeOffset(activeFish.ExpireAt).ToUnixTimeMilliseconds()
|
||||
});
|
||||
spawned.Add(AddActiveFish(state, fishCfg, routeId, isFirst, now, random));
|
||||
}
|
||||
}
|
||||
else if (prob.Type == 2 && config.FishArrays != null)
|
||||
@@ -1139,6 +1104,221 @@ namespace FishGameService.Services
|
||||
return spawned;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 重置 Boss 排队状态:Boss鱼(FishType=2)队列、回合计数、特殊Boss(FishType=5)轮询列表
|
||||
/// </summary>
|
||||
private static void ResetBossSpawnState(RoomFishState state)
|
||||
{
|
||||
var fishes = state.Config?.Fishes ?? new List<FishSpawnFish>();
|
||||
|
||||
state.BossQueue = fishes
|
||||
.Where(f => f.FishType == (int)CommonDomain.Enum.FishTypeEnum.Boss鱼 &&
|
||||
f.FishWeight > 0 && !string.IsNullOrWhiteSpace(f.FishNumber))
|
||||
.Select(f => f.FishNumber)
|
||||
.Distinct()
|
||||
.OrderBy(_ => Random.Shared.Next())
|
||||
.ToList();
|
||||
state.BossQueueTotal = state.BossQueue.Count;
|
||||
state.BossSpawnedThisRound = new List<string>();
|
||||
state.BossRoundBatchCount = 0;
|
||||
|
||||
state.SpecialBossCycle = fishes
|
||||
.Where(f => f.FishType == (int)CommonDomain.Enum.FishTypeEnum.特殊Boss &&
|
||||
f.FishWeight > 0 && !string.IsNullOrWhiteSpace(f.FishNumber))
|
||||
.Select(f => f.FishNumber)
|
||||
.Distinct()
|
||||
.OrderBy(_ => Random.Shared.Next())
|
||||
.ToList();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Boss鱼排队出鱼:按队列依次轮流出、不重复;每 maxBosscount/2 轮(批次)为一个回合,
|
||||
/// 回合结束后本回合出过的鱼重新添加到随机出 Boss 的列表中。
|
||||
/// </summary>
|
||||
private List<SpawnFishItem> SpawnBossFishByQueue(RoomFishState state, List<FishSpawnFish> candidates,
|
||||
int spawnCount, DateTime now, Random random, bool isFirst)
|
||||
{
|
||||
var spawned = new List<SpawnFishItem>();
|
||||
if (state.BossQueueTotal == 0 && state.BossQueue.Count == 0 && state.BossSpawnedThisRound.Count == 0)
|
||||
ResetBossSpawnState(state);
|
||||
|
||||
while (spawned.Count < spawnCount)
|
||||
{
|
||||
var requestedOdd = state.PreferOddRestrictedRoute;
|
||||
var fishCfg = TakeNextEligibleBoss(state, candidates, requestedOdd, now);
|
||||
if (fishCfg == null)
|
||||
break;
|
||||
|
||||
var routeId = PickRoute(fishCfg, random, requestedOdd);
|
||||
state.PreferOddRestrictedRoute = (routeId & 1) == 0;
|
||||
spawned.Add(AddActiveFish(state, fishCfg, routeId, isFirst, now, random));
|
||||
}
|
||||
|
||||
state.BossRoundBatchCount++;
|
||||
var roundLength = Math.Max(1, state.BossQueueTotal / 2);
|
||||
if (state.BossRoundBatchCount >= roundLength)
|
||||
{
|
||||
if (state.BossSpawnedThisRound.Count > 0)
|
||||
state.BossQueue.AddRange(state.BossSpawnedThisRound);
|
||||
state.BossSpawnedThisRound.Clear();
|
||||
state.BossRoundBatchCount = 0;
|
||||
}
|
||||
|
||||
return spawned;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 特殊Boss轮询出鱼:每轮按随机顺序把每个特殊Boss各出一次,出完一轮重新随机排序。
|
||||
/// </summary>
|
||||
private List<SpawnFishItem> SpawnSpecialBossByCycle(RoomFishState state, List<FishSpawnFish> candidates,
|
||||
int spawnCount, DateTime now, Random random, bool isFirst)
|
||||
{
|
||||
var spawned = new List<SpawnFishItem>();
|
||||
|
||||
while (spawned.Count < spawnCount)
|
||||
{
|
||||
var requestedOdd = state.PreferOddRestrictedRoute;
|
||||
var fishCfg = TakeNextEligibleSpecialBoss(state, candidates, requestedOdd, now);
|
||||
if (fishCfg == null)
|
||||
break;
|
||||
|
||||
var routeId = PickRoute(fishCfg, random, requestedOdd);
|
||||
state.PreferOddRestrictedRoute = (routeId & 1) == 0;
|
||||
spawned.Add(AddActiveFish(state, fishCfg, routeId, isFirst, now, random));
|
||||
}
|
||||
|
||||
return spawned;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 从 Boss 队列依次取鱼:取队头,仍活跃或处于冷却期的鱼暂缓到队尾,取出的鱼记录到本回合已出列表。
|
||||
/// </summary>
|
||||
private FishSpawnFish? TakeNextEligibleBoss(RoomFishState state, List<FishSpawnFish> candidates,
|
||||
bool requestedOdd, DateTime now)
|
||||
{
|
||||
var scanLimit = state.BossQueue.Count;
|
||||
for (var i = 0; i < scanLimit && state.BossQueue.Count > 0; i++)
|
||||
{
|
||||
var fishNumber = state.BossQueue[0];
|
||||
state.BossQueue.RemoveAt(0);
|
||||
|
||||
var fishCfg = candidates.FirstOrDefault(f =>
|
||||
string.Equals(f.FishNumber, fishNumber, StringComparison.OrdinalIgnoreCase));
|
||||
if (fishCfg == null)
|
||||
continue;
|
||||
|
||||
if (!HasRouteForSide(fishCfg, requestedOdd) || IsFishNumberActiveOrCooling(state, fishNumber, now))
|
||||
{
|
||||
state.BossQueue.Add(fishNumber);
|
||||
continue;
|
||||
}
|
||||
|
||||
state.BossSpawnedThisRound.Add(fishNumber);
|
||||
return fishCfg;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 从特殊Boss轮询列表依次取鱼:取队头,仍活跃或处于冷却期的鱼暂缓到队尾;列表取空后重新随机排序。
|
||||
/// </summary>
|
||||
private FishSpawnFish? TakeNextEligibleSpecialBoss(RoomFishState state, List<FishSpawnFish> candidates,
|
||||
bool requestedOdd, DateTime now)
|
||||
{
|
||||
if (state.SpecialBossCycle.Count == 0)
|
||||
{
|
||||
state.SpecialBossCycle = candidates
|
||||
.Select(f => f.FishNumber)
|
||||
.Distinct()
|
||||
.OrderBy(_ => Random.Shared.Next())
|
||||
.ToList();
|
||||
}
|
||||
|
||||
var scanLimit = state.SpecialBossCycle.Count;
|
||||
for (var i = 0; i < scanLimit && state.SpecialBossCycle.Count > 0; i++)
|
||||
{
|
||||
var fishNumber = state.SpecialBossCycle[0];
|
||||
state.SpecialBossCycle.RemoveAt(0);
|
||||
|
||||
var fishCfg = candidates.FirstOrDefault(f =>
|
||||
string.Equals(f.FishNumber, fishNumber, StringComparison.OrdinalIgnoreCase));
|
||||
if (fishCfg == null)
|
||||
continue;
|
||||
|
||||
if (!HasRouteForSide(fishCfg, requestedOdd) || IsFishNumberActiveOrCooling(state, fishNumber, now))
|
||||
{
|
||||
state.SpecialBossCycle.Add(fishNumber);
|
||||
continue;
|
||||
}
|
||||
|
||||
return fishCfg;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static bool IsFishNumberActiveOrCooling(RoomFishState state, string fishNumber, DateTime now)
|
||||
{
|
||||
if (state.ActiveFishes.Values.Any(f => !f.IsKilled && f.ExpireAt > now &&
|
||||
string.Equals(f.FishNumber, fishNumber, StringComparison.Ordinal)))
|
||||
return true;
|
||||
|
||||
return state.NextAllowedSpawnByFishNumber.TryGetValue(fishNumber, out var nextAllowedAt) && now < nextAllowedAt;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 将鱼写入房间活跃鱼状态并返回出鱼项(首批鱼向前偏移创建时间,模拟已游动一段时间)。
|
||||
/// </summary>
|
||||
private SpawnFishItem AddActiveFish(RoomFishState state, FishSpawnFish fishCfg, int routeId,
|
||||
bool isFirst, DateTime now, Random random)
|
||||
{
|
||||
var times = fishCfg.Times > 0 ? fishCfg.Times : 8;
|
||||
|
||||
DateTime createdAt;
|
||||
if (isFirst)
|
||||
{
|
||||
var pastOffset = times * (0.2 + random.NextDouble() * 0.6);
|
||||
createdAt = now.AddSeconds(-pastOffset);
|
||||
}
|
||||
else
|
||||
{
|
||||
createdAt = now;
|
||||
}
|
||||
|
||||
var fishId = NextFishId();
|
||||
var activeFish = new ActiveFish
|
||||
{
|
||||
FishId = fishId,
|
||||
FishNumber = fishCfg.FishNumber,
|
||||
FishType = fishCfg.FishType,
|
||||
RouteId = routeId,
|
||||
CreatedAt = createdAt,
|
||||
ExpireAt = createdAt.AddSeconds(times),
|
||||
IsKilled = false
|
||||
};
|
||||
|
||||
state.ActiveFishes[fishId] = activeFish;
|
||||
if (fishCfg.FishType == 2 || fishCfg.FishType == 5)
|
||||
{
|
||||
state.NextAllowedSpawnByFishNumber[fishCfg.FishNumber] =
|
||||
activeFish.ExpireAt.AddSeconds(RestrictedFishRespawnBufferSeconds);
|
||||
}
|
||||
state.FishCountByType.TryGetValue(fishCfg.FishType, out var fc);
|
||||
state.FishCountByType[fishCfg.FishType] = fc + 1;
|
||||
|
||||
return new SpawnFishItem
|
||||
{
|
||||
FishId = fishId,
|
||||
FishIds = new List<long> { fishId },
|
||||
FishNumber = fishCfg.FishNumber,
|
||||
FishType = fishCfg.FishType,
|
||||
RouteId = routeId,
|
||||
CreatedAtMs = new DateTimeOffset(createdAt).ToUnixTimeMilliseconds(),
|
||||
ExpireAtMs = new DateTimeOffset(activeFish.ExpireAt).ToUnixTimeMilliseconds()
|
||||
};
|
||||
}
|
||||
|
||||
private static string GetSpawnLogTypeName(int fishType)
|
||||
{
|
||||
return fishType switch
|
||||
@@ -1271,6 +1451,16 @@ namespace FishGameService.Services
|
||||
|
||||
public Dictionary<string, int> ActiveArrayFishCount { get; set; } = new Dictionary<string, int>();
|
||||
|
||||
public List<string> BossQueue { get; set; } = new List<string>();
|
||||
|
||||
public List<string> BossSpawnedThisRound { get; set; } = new List<string>();
|
||||
|
||||
public int BossQueueTotal { get; set; }
|
||||
|
||||
public int BossRoundBatchCount { get; set; }
|
||||
|
||||
public List<string> SpecialBossCycle { get; set; } = new List<string>();
|
||||
|
||||
public bool PreferOddArrayNumber { get; set; } = true;
|
||||
|
||||
public bool PreferOddRestrictedRoute { get; set; } = true;
|
||||
|
||||
Reference in New Issue
Block a user