This commit is contained in:
2026-10-13 21:52:02 +08:00
parent aad11be43d
commit fcfa04137b
15 changed files with 945 additions and 63 deletions
@@ -814,7 +814,7 @@ namespace FishGameService.GameService
}
public async Task<long> KillFish(string cid, string roomId, string userId, string fishNumber, string fihsId, long gold, long timespan, string bulletId, long timestamp, string sign)
public async Task<(long gold, long gainExp, long exp, int level, long expMax)> KillFish(string cid, string roomId, string userId, string fishNumber, string fihsId, long gold, long timespan, string bulletId, long timestamp, string sign)
{
return await _distributedLock.TryExecuteAsync(
$"{RedisKey.DistributedLockPrefix}fish:gold:{roomId}:{userId}",
@@ -822,12 +822,12 @@ namespace FishGameService.GameService
retry: true);
}
private async Task<long> KillFishInternal(string cid, string roomId, string userId, string fishNumber, string fihsId, long gold, long timespan, string bulletId, long timestamp, string sign)
private async Task<(long gold, long gainExp, long exp, int level, long expMax)> KillFishInternal(string cid, string roomId, string userId, string fishNumber, string fihsId, long gold, long timespan, string bulletId, long timestamp, string sign)
{
if (!VerifySign(cid, $"{roomId}|{userId}|{fishNumber}|{fihsId}|{gold}|{timespan}|{bulletId}|{timestamp}", timestamp, sign))
{
_sendResult.ErrorMessage(cid, ErrorCodes.签名错误, "签名错误");
return 0;
return (0, 0, 0, 0, 0);
}
;
@@ -847,7 +847,7 @@ namespace FishGameService.GameService
if (!string.IsNullOrWhiteSpace(cid))
_sendResult.ErrorMessage(cid, ErrorCodes.鱼配置不存在, "鱼配置不存在");
return 0;
return (0, 0, 0, 0, 0);
}
var gameRoom = _redisService.GetGameRoom(roomId);
if (gameRoom == null)
@@ -855,7 +855,7 @@ namespace FishGameService.GameService
if (!string.IsNullOrWhiteSpace(cid))
_sendResult.ErrorMessage(cid, ErrorCodes.房间不存在, "房间不存在");
return 0;
return (0, 0, 0, 0, 0);
}
var player = gameRoom.Players.Where(c => c.UserId == userId).FirstOrDefault();
if (player == null)
@@ -863,31 +863,32 @@ namespace FishGameService.GameService
if (!string.IsNullOrWhiteSpace(cid))
_sendResult.ErrorMessage(cid, ErrorCodes.玩家不在房间, "玩家不在房间");
return 0;
return (0, 0, 0, 0, 0);
}
if (player.State == 2)
{
return player.Gold;
return (player.Gold, 0, player.Exp, player.Level, player.MaxExp);
}
if (!IsCallerPlayer(cid, gameRoom, player))
{
_hubGame.SendUserCId(cid, "KickToMainPage", new { userId });
return player.Gold;
return (player.Gold, 0, player.Exp, player.Level, player.MaxExp);
}
if (gold <= 0 || string.IsNullOrWhiteSpace(bulletId))
{
_sendResult.ErrorMessage(cid, ErrorCodes.子弹参数无效, "子弹参数无效");
return player.Gold;
return (player.Gold, 0, player.Exp, player.Level, player.MaxExp);
}
if (!TryConsumeBullet(roomId, userId, bulletId))
{
return player.Gold;
return (player.Gold, 0, player.Exp, player.Level, player.MaxExp);
}
var isKill = false;
long killExpGain = 0;
var factor = 0.0;
var playerFactor = await GetPlayerFactorAsync(roomId, userId);
@@ -897,7 +898,7 @@ namespace FishGameService.GameService
serverFishId = fishIdLong;
if (!_fishSpawnService.ValidateFishHit(roomId, fishIdLong))
{
return player.Gold;
return (player.Gold, 0, player.Exp, player.Level, player.MaxExp);
}
}
@@ -1006,7 +1007,7 @@ namespace FishGameService.GameService
catch (OverflowException)
{
_sendResult.ErrorMessage(cid, ErrorCodes.金币参数超出范围, "金币参数超出范围");
return player.Gold;
return (player.Gold, 0, player.Exp, player.Level, player.MaxExp);
}
(long gold, double beishu, object res) res;
if (isKill)
@@ -1062,7 +1063,39 @@ namespace FishGameService.GameService
var expGain = fishConfig.FishType is FishTypeEnum.Boss鱼 or FishTypeEnum.特殊鱼 or FishTypeEnum.特殊Boss
? 1000 : fishConfig.FishScore;
_redisService.AddFishingExpProgress(roomId, player.UserId, expGain);
if (expGain > 0)
{
var expSaved = false;
try
{
var expResult = await _userService.UpdateAssets(new UserAssetParam
{
UserId = player.UserId,
Exp = expGain,
ChangeType = UserAssetChangeTypeEnum.增加,
Source = RewardSource.击杀鱼,
Remark = "捕鱼经验",
RoomInfo = roomId,
IsAddRecord = false
});
expSaved = expResult.isok;
}
catch (Exception ex)
{
_logger.LogError(ex, "击杀经验实时入库异常 roomId={RoomId} userId={UserId} exp={Exp}", roomId, player.UserId, expGain);
}
if (expSaved)
{
ApplyFishingExp(player, expGain);
killExpGain = expGain;
}
else
{
_logger.LogWarning("击杀经验实时入库未成功,转为 Redis 累计待离房结算 roomId={RoomId} userId={UserId} exp={Exp}", roomId, player.UserId, expGain);
_redisService.AddFishingExpProgress(roomId, player.UserId, expGain);
}
}
if ((fishConfig.FishType == FishTypeEnum.Boss鱼 || fishConfig.FishType == FishTypeEnum.特殊Boss || fishConfig.FishType == FishTypeEnum.特殊鱼)
&& fishNumber != "12028"
@@ -1135,7 +1168,21 @@ namespace FishGameService.GameService
}
_redisService.AddSystemGoldDay(gold - (isKill ? skillGold : 0));
return player.Gold;
return (player.Gold, killExpGain, player.Exp, player.Level, player.MaxExp);
}
/// <summary>
/// 击杀经验实时入库成功后,同步房间内玩家的经验/等级/经验上限(与 UserEntity.LevelVerify 的升级判定保持一致)。
/// </summary>
private void ApplyFishingExp(RedisGamePlayer player, long gainExp)
{
player.Exp += gainExp;
var levelCfg = _gameConfig?.UserLevelConfigs?.FirstOrDefault(c => player.Exp >= c.ExpMin && player.Exp < c.ExpMax);
if (levelCfg != null)
{
player.Level = levelCfg.Level;
player.MaxExp = levelCfg.ExpMax;
}
}
private void AddBullets(string roomId, string userId, List<string> bulletIds)
@@ -1,4 +1,4 @@
using System;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
@@ -25,7 +25,7 @@ namespace FishGameService.GameService
Task<(List<string> bullets, long gold)> FireCannon(string cid, string roomId, string userId, long gold, int count, long timestamp, string sign, object extra = null);
Task<long> KillFish(string cid, string roomId, string userId, string fishNumber, string fihsId, long gold, long timespan, string bulletId, long timestamp, string sign);
Task<(long gold, long gainExp, long exp, int level, long expMax)> KillFish(string cid, string roomId, string userId, string fishNumber, string fihsId, long gold, long timespan, string bulletId, long timestamp, string sign);
Task LeaveRoom(string cid, string roomId, string userId);
@@ -313,8 +313,16 @@ namespace FishGameService.Hubs
{
string cid = Context.ConnectionId;
var currentGold = await _fishingService.KillFish(cid, roomId, userId, fishNumber, fihsId, gold, timespan, bulletId, timestamp, sign);
return new { gold = currentGold, timestamp = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds() };
var result = await _fishingService.KillFish(cid, roomId, userId, fishNumber, fihsId, gold, timespan, bulletId, timestamp, sign);
return new
{
gold = result.gold,
gainExp = result.gainExp,
exp = result.exp,
level = result.level,
expMax = result.expMax,
timestamp = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds()
};
}
public async Task LeaveRoom(string roomId, string userId)
@@ -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;