2 Commits

Author SHA1 Message Date
Vi-brance
735060b16e fix character core upgrade with multiple body items (#72) 2025-12-20 15:30:02 -05:00
qmengz
da51e6ba3e feat: Implement soloraid - 实现单人突袭 (#71)
- Added new handlers for closing, entering, and opening solo raids and trials, including:
  - Close
  - ClosePractice
  - CloseTrial
  - Enter
  - EnterTrial
  - Open
  - OpenPractice
  - OpenTrial
- Introduced FastBattle handler for quick battle functionality.
- Implemented methods for getting level and badge data, including:
  - GetLevel
  - GetLevelPractice
  - GetLevelTrial
- Implemented methods for setting damage, including:
  - SetDamage
  - SetDamagePractice
  - SetDamageTrial
- Added logging and error handling for various operations.
- Created SoloRaidHelper class to manage solo raid logic, including opening, closing, and setting damage.
- Updated database models to include solo raid data structures.
- Enhanced user model to store solo raid information.
2025-12-15 21:33:49 -05:00
26 changed files with 1109 additions and 18 deletions

View File

@@ -351,6 +351,23 @@ namespace EpinelPS.Data
public readonly Dictionary<int, EventAZXAppleGameSkillRecord_Raw> EventAZXAppleGameSkillTable = [];
[LoadRecord("EventAZXAppleGameCutSceneTable.json", "Id")]
public readonly Dictionary<int, EventAZXAppleGameCutSceneRecord_Raw> EventAZXAppleGameCutSceneTable = [];
// solo raid data Table
[LoadRecord("SoloRaidManagerTable.json", "Id")]
public readonly Dictionary<int, SoloRaidManagerRecord> SoloRaidManagerTable = [];
[LoadRecord("SoloRaidPresetTable.json", "Id")]
public readonly Dictionary<int, SoloRaidPresetRecord> SoloRaidPresetTable = [];
// Monster data Table
[LoadRecord("MonsterTable.json", "Id")]
public readonly Dictionary<long, MonsterRecord> MonsterTable = [];
[LoadRecord("MonsterModelTable.json", "Id")]
public readonly Dictionary<int, MonsterModelRecord> MonsterModelTable = [];
[LoadRecord("MonsterStatEnhanceTable.json", "Id")]
public readonly Dictionary<int, MonsterStatEnhanceRecord> MonsterStatEnhanceTable = [];
[LoadRecord("WaveDataTable.wave_Intercept_001.json", "StageId")]
public readonly Dictionary<int, WaveDataRecord> WaveIntercept001Table = [];
static async Task<GameData> BuildAsync()
{

View File

@@ -31,15 +31,15 @@ namespace EpinelPS.LobbyServer.Character
return;
}
// Find a new CSN based on the `NameCode` of the current character and `GradeCoreId + 1`
// Find a new CSN based on the `NameCode` of the current character and `GradeCoreId + req.Count`
// For some reason, there is a seperate character for each limit/core break value.
CharacterRecord? newCharacter = fullchardata.FirstOrDefault(c => c.NameCode == currentCharacter.NameCode && c.GradeCoreId == currentCharacter.GradeCoreId + 1);
CharacterRecord? newCharacter = fullchardata.FirstOrDefault(c => c.NameCode == currentCharacter.NameCode && c.GradeCoreId == currentCharacter.GradeCoreId + req.Count);
if (newCharacter != null)
{
// replace character in DB with new character
targetCharacter.Grade++;
targetCharacter.Grade += req.Count;
targetCharacter.Tid = newCharacter.Id;
response.Character = new NetUserCharacterDefaultData()
@@ -47,7 +47,7 @@ namespace EpinelPS.LobbyServer.Character
Csn = req.Csn,
CostumeId = targetCharacter.CostumeId,
Grade = targetCharacter.Grade,
Lv = user.GetSynchroLevel(),
Lv = targetCharacter.Level,
Skill1Lv = targetCharacter.Skill1Lvl,
Skill2Lv = targetCharacter.Skill2Lvl,
Tid = targetCharacter.Tid,
@@ -56,7 +56,7 @@ namespace EpinelPS.LobbyServer.Character
// remove spare body item
ItemData bodyItem = user.Items.FirstOrDefault(i => i.Isn == req.Isn) ?? throw new NullReferenceException();
user.RemoveItemBySerialNumber(req.Isn, 1);
user.RemoveItemBySerialNumber(req.Isn, req.Count);
response.Items.Add(NetUtils.ToNet(bodyItem));
JsonDb.Save();

View File

@@ -32,15 +32,15 @@ namespace EpinelPS.LobbyServer.Character
return;
}
// Find a new CSN based on the `NameCode` of the current character and `GradeCoreId + 1`
// Find a new CSN based on the `NameCode` of the current character and `GradeCoreId + req.Count`
// For some reason, there is a seperate character for each limit/core break value.
CharacterRecord? newCharacter = fullchardata.FirstOrDefault(c => c.NameCode == currentCharacter.NameCode && c.GradeCoreId == currentCharacter.GradeCoreId + 1);
CharacterRecord? newCharacter = fullchardata.FirstOrDefault(c => c.NameCode == currentCharacter.NameCode && c.GradeCoreId == currentCharacter.GradeCoreId + req.Count);
if (newCharacter != null)
{
// replace character in DB with new character
targetCharacter.Grade++;
targetCharacter.Grade += req.Count;
targetCharacter.Tid = newCharacter.Id;
response.Character = new NetUserCharacterDefaultData()
@@ -48,7 +48,7 @@ namespace EpinelPS.LobbyServer.Character
Csn = req.Csn,
CostumeId = targetCharacter.CostumeId,
Grade = targetCharacter.Grade,
Lv = user.GetSynchroLevel(),
Lv = targetCharacter.Level,
Skill1Lv = targetCharacter.Skill1Lvl,
Skill2Lv = targetCharacter.Skill2Lvl,
Tid = targetCharacter.Tid,
@@ -57,7 +57,7 @@ namespace EpinelPS.LobbyServer.Character
// remove spare body item
ItemData bodyItem = user.Items.FirstOrDefault(i => i.Isn == req.Isn) ?? throw new NullReferenceException();
user.RemoveItemBySerialNumber(req.Isn, 1);
user.RemoveItemBySerialNumber(req.Isn, req.Count);
response.Items.Add(NetUtils.ToNet(bodyItem));
if (newCharacter.GradeCoreId == 103 || newCharacter.GradeCoreId == 11 || newCharacter.GradeCoreId == 201)

View File

@@ -0,0 +1,28 @@
using EpinelPS.Utils;
namespace EpinelPS.LobbyServer.Soloraid;
[PacketPath("/soloraid/close")]
public class Close : LobbyMsgHandler
{
protected override async Task HandleAsync()
{
// int RaidId, int RaidLevel
var req = await ReadData<ReqCloseSoloRaid>();
var user = GetUser();
ResCloseSoloRaid response = new()
{
PeriodResult = SoloRaidPeriodResult.Success
};
try
{
SoloRaidHelper.CloseSoloRaid(user, req.RaidId, req.RaidLevel, SoloRaidType.Normal);
}catch(Exception ex)
{
Logging.WriteLine($"CloseSoloRaid Error: {ex.Message}", LogType.Error);
}
await WriteDataAsync(response);
}
}

View File

@@ -0,0 +1,28 @@
using EpinelPS.Utils;
namespace EpinelPS.LobbyServer.Soloraid;
[PacketPath("/soloraid/practice/close")]
public class ClosePractice : LobbyMsgHandler
{
protected override async Task HandleAsync()
{
// int RaidId, int RaidLevel
var req = await ReadData<ReqCloseSoloRaidPractice>();
var user = GetUser();
ResCloseSoloRaidPractice response = new()
{
PeriodResult = SoloRaidPeriodResult.Success
};
try
{
SoloRaidHelper.CloseSoloRaid(user, req.RaidId, req.RaidLevel, SoloRaidType.Practice);
}catch(Exception ex)
{
Logging.WriteLine($"CloseSoloRaidPractice Error: {ex.Message}", LogType.Error);
}
await WriteDataAsync(response);
}
}

View File

@@ -0,0 +1,28 @@
using EpinelPS.Utils;
namespace EpinelPS.LobbyServer.Soloraid;
[PacketPath("/soloraid/trial/close")]
public class CloseTrial : LobbyMsgHandler
{
protected override async Task HandleAsync()
{
// int RaidId, int RaidLevel
var req = await ReadData<ReqCloseSoloRaidTrial>();
var user = GetUser();
ResCloseSoloRaidTrial response = new()
{
PeriodResult = SoloRaidPeriodResult.Success
};
try
{
SoloRaidHelper.CloseSoloRaid(user, req.RaidId, req.RaidLevel, SoloRaidType.Trial);
}catch(Exception ex)
{
Logging.WriteLine($"CloseSoloRaidTrial Error: {ex.Message}", LogType.Error);
}
await WriteDataAsync(response);
}
}

View File

@@ -0,0 +1,21 @@
using EpinelPS.Utils;
namespace EpinelPS.LobbyServer.Soloraid;
[PacketPath("/soloraid/enter")]
public class Enter : LobbyMsgHandler
{
protected override async Task HandleAsync()
{
var req = await ReadData<ReqEnterSoloRaid>();
Logging.WriteLine($"Entering solo raid {req.RaidId} at level {req.RaidLevel} for user {GetUser().ID} team {req.Team} members");
ResEnterSoloRaid response = new()
{
PeriodResult = SoloRaidPeriodResult.Success,
};
await WriteDataAsync(response);
}
}

View File

@@ -0,0 +1,21 @@
using EpinelPS.Utils;
namespace EpinelPS.LobbyServer.Soloraid;
[PacketPath("/soloraid/trial/enter")]
public class EnterTrial : LobbyMsgHandler
{
protected override async Task HandleAsync()
{
var req = await ReadData<ReqEnterSoloRaidTrial>();
Logging.WriteLine($"Entering solo raid {req.RaidId} at level {req.RaidLevel} for user {GetUser().ID} team {req.Team} members");
ResEnterSoloRaidTrial response = new()
{
PeriodResult = SoloRaidPeriodResult.Success,
};
await WriteDataAsync(response);
}
}

View File

@@ -0,0 +1,25 @@
using EpinelPS.Utils;
namespace EpinelPS.LobbyServer.Soloraid;
[PacketPath("/soloraid/fastbattle")]
public class FastBattle : LobbyMsgHandler
{
protected override async Task HandleAsync()
{
var req = await ReadData<ReqFastBattleSoloRaid>();
var user = GetUser();
ResFastBattleSoloRaid response = new();
try
{
SoloRaidHelper.FastBattle(user, ref response, req.RaidId, req.RaidLevel, req.ClearCount);
}
catch (Exception e)
{
Logging.WriteLine($"FastBattle Error {e.Message}");
}
await WriteDataAsync(response);
}
}

View File

@@ -1,6 +1,6 @@
using EpinelPS.Utils;
namespace EpinelPS.LobbyServer.SoloraId;
namespace EpinelPS.LobbyServer.Soloraid;
[PacketPath("/soloraidmuseum/get/reddotdata")]
public class GetBadgeData : LobbyMsgHandler

View File

@@ -0,0 +1,35 @@
using EpinelPS.Utils;
namespace EpinelPS.LobbyServer.Soloraid;
[PacketPath("/soloraid/get")]
public class GetInfo : LobbyMsgHandler
{
protected override async Task HandleAsync()
{
await ReadData<ReqGetSoloRaidInfo>();
User user = GetUser();
// ResGetSoloRaidInfo Fields
// NetUserSoloRaidInfo Info
// SoloRaidPeriodResult PeriodResult
// SoloRaidBanResult BanResult
ResGetSoloRaidInfo response = new()
{
Info = new NetUserSoloRaidInfo(),
PeriodResult = SoloRaidPeriodResult.Success,
BanResult = SoloRaidBanResult.Success
};
try
{
response.Info = SoloRaidHelper.GetUserSoloRaidInfo(user);
}
catch (Exception ex)
{
Logging.WriteLine($"GetSoloRaidInfo error: {ex.Message}", LogType.Error);
}
await WriteDataAsync(response);
}
}

View File

@@ -0,0 +1,25 @@
using EpinelPS.Utils;
namespace EpinelPS.LobbyServer.Soloraid;
[PacketPath("/soloraid/getlevel")]
public class GetLevel : LobbyMsgHandler
{
protected override async Task HandleAsync()
{
var req = await ReadData<ReqGetLevelSoloRaid>();
User user = GetUser();
ResGetLevelSoloRaid response = new();
try
{
SoloRaidHelper.GetLevelInfo(user, ref response, req.RaidLevel);
}
catch (Exception ex)
{
Logging.WriteLine($"GetLevelSoloRaid Error: {ex.Message}", LogType.Error);
}
await WriteDataAsync(response);
}
}

View File

@@ -0,0 +1,25 @@
using EpinelPS.Utils;
namespace EpinelPS.LobbyServer.Soloraid;
[PacketPath("/soloraid/practice/getlevel")]
public class GetLevelPractice : LobbyMsgHandler
{
protected override async Task HandleAsync()
{
var req = await ReadData<ReqGetLevelPracticeSoloRaid>();
var user = GetUser();
ResGetLevelPracticeSoloRaid response = new();
try
{
SoloRaidHelper.GetLevelPracticeInfo(user, ref response, req.RaidLevel);
}
catch (Exception ex)
{
Logging.WriteLine($"GetLevelPractice Error: {ex.Message}", LogType.Error);
}
await WriteDataAsync(response);
}
}

View File

@@ -0,0 +1,26 @@
using EpinelPS.Utils;
namespace EpinelPS.LobbyServer.Soloraid;
[PacketPath("/soloraid/trial/getlevel")]
public class GetLevelTrial : LobbyMsgHandler
{
protected override async Task HandleAsync()
{
// int RaidLevel
var req = await ReadData<ReqGetLevelTrialSoloRaid>();
User user = GetUser();
ResGetLevelTrialSoloRaid response = new();
try
{
SoloRaidHelper.GetLevelTrialInfo(user, ref response, req.RaidLevel);
}
catch (Exception ex)
{
Logging.WriteLine($"GetLevelTrial Error: {ex.Message}", LogType.Error);
}
await WriteDataAsync(response);
}
}

View File

@@ -0,0 +1,27 @@
using EpinelPS.Utils;
namespace EpinelPS.LobbyServer.Soloraid
{
[PacketPath("/soloraid/getlogs")]
public class GetLogs : LobbyMsgHandler
{
protected override async Task HandleAsync()
{
// int RaidId, int RaidLevel
var req = await ReadData<ReqGetSoloRaidLogs>();
var user = GetUser();
ResGetSoloRaidLogs response = new();
try
{
SoloRaidHelper.GetSoloRaidLog(user, ref response, req.RaidId, req.RaidLevel);
}
catch (Exception ex)
{
Logging.WriteLine($"GetLogs Error: {ex.Message}", LogType.Error);
}
await WriteDataAsync(response);
}
}
}

View File

@@ -1,20 +1,17 @@
using EpinelPS.Utils;
namespace EpinelPS.LobbyServer.SoloraId
namespace EpinelPS.LobbyServer.Soloraid
{
[PacketPath("/soloraid/getperiod")]
public class GetSoloraidPeriod : LobbyMsgHandler
public class GetPeriod : LobbyMsgHandler
{
protected override async Task HandleAsync()
{
ReqGetSoloRaidPeriod req = await ReadData<ReqGetSoloRaidPeriod>();
await ReadData<ReqGetSoloRaidPeriod>();
ResGetSoloRaidPeriod response = new()
{
Period = new NetSoloRaidPeriodData
{
}
Period = SoloRaidHelper.GetSoloRaidPeriod()
};
// TODO
await WriteDataAsync(response);

View File

@@ -0,0 +1,18 @@
using EpinelPS.Utils;
namespace EpinelPS.LobbyServer.Soloraid;
[PacketPath("/soloraid/getranking")]
public class GetRanking : LobbyMsgHandler
{
protected override async Task HandleAsync()
{
await ReadData<ReqGetSoloRaidRanking>();
ResGetSoloRaidRanking response = new();
// TODO
await WriteDataAsync(response);
}
}

View File

@@ -0,0 +1,32 @@
using EpinelPS.Database;
using EpinelPS.Utils;
namespace EpinelPS.LobbyServer.Soloraid;
[PacketPath("/soloraid/open")]
public class Open : LobbyMsgHandler
{
protected override async Task HandleAsync()
{
// { "raidId": 1000030, "raidLevel": 1 }
var req = await ReadData<ReqOpenSoloRaid>();
User user = GetUser();
ResOpenSoloRaid response = new()
{
PeriodResult = SoloRaidPeriodResult.Success,
};
try
{
int openCount = SoloRaidHelper.OpenSoloRaid(user, req.RaidId, req.RaidLevel);
response.RaidOpenCount = openCount;
}
catch (Exception ex)
{
Logging.WriteLine($"OpenSoloRaid error: {ex.Message}", LogType.Error);
}
JsonDb.Save();
await WriteDataAsync(response);
}
}

View File

@@ -0,0 +1,33 @@
using EpinelPS.Database;
using EpinelPS.Utils;
namespace EpinelPS.LobbyServer.Soloraid;
[PacketPath("/soloraid/practice/open")]
public class OpenPractice : LobbyMsgHandler
{
protected override async Task HandleAsync()
{
// ReqOpenSoloRaidPractice Fields:
// int RaidLevel
// SoloRaidDifficultyType DifficultyType
var req = await ReadData<ReqOpenSoloRaidPractice>();
User user = GetUser();
ResOpenSoloRaidPractice response = new()
{
PeriodResult = SoloRaidPeriodResult.Success,
};
try
{
SoloRaidHelper.OpenSoloRaid(user, 0, req.RaidLevel, type: SoloRaidType.Practice);
}
catch (Exception ex)
{
Logging.WriteLine($"OpenSoloRaid error: {ex.Message}", LogType.Error);
}
JsonDb.Save();
await WriteDataAsync(response);
}
}

View File

@@ -0,0 +1,32 @@
using EpinelPS.Database;
using EpinelPS.Utils;
namespace EpinelPS.LobbyServer.Soloraid;
[PacketPath("/soloraid/trial/open")]
public class OpenTrial : LobbyMsgHandler
{
protected override async Task HandleAsync()
{
// { "raidLevel": 8 }
var req = await ReadData<ReqOpenSoloRaidTrial>();
User user = GetUser();
ResOpenSoloRaidTrial response = new()
{
PeriodResult = SoloRaidPeriodResult.Success,
RaidOpenCount = 1,
};
try
{
response.RaidOpenCount = SoloRaidHelper.OpenSoloRaid(user, 0, req.RaidLevel, type: SoloRaidType.Trial);
}
catch (Exception ex)
{
Logging.WriteLine($"OpenSoloRaid error: {ex.Message}", LogType.Error);
}
JsonDb.Save();
await WriteDataAsync(response);
}
}

View File

@@ -0,0 +1,29 @@
using EpinelPS.Database;
using EpinelPS.Utils;
namespace EpinelPS.LobbyServer.Soloraid;
[PacketPath("/soloraid/setdamage")]
public class SetDamage : LobbyMsgHandler
{
protected override async Task HandleAsync()
{
var req = await ReadData<ReqSetSoloRaidDamage>();
User user = GetUser();
ResSetSoloRaidDamage response = new();
try
{
SoloRaidHelper.SetDamage(user, ref response, req);
}
catch (Exception ex)
{
Logging.WriteLine($"SetDamage Error: {ex.Message}", LogType.Error);
}
JsonDb.Save();
await WriteDataAsync(response);
}
}

View File

@@ -0,0 +1,27 @@
using EpinelPS.Database;
using EpinelPS.Utils;
namespace EpinelPS.LobbyServer.Soloraid;
[PacketPath("/soloraid/practice/setdamage")]
public class SetDamagePractice : LobbyMsgHandler
{
protected override async Task HandleAsync()
{
var req = await ReadData<ReqSetSoloRaidPracticeDamage>();
var user = GetUser();
ResSetSoloRaidPracticeDamage response = new();
try
{
SoloRaidHelper.SetDamagePractice(user, ref response, req);
}
catch (Exception ex)
{
Logging.WriteLine($"SetDamagePractice Error: {ex.Message}", LogType.Error);
}
JsonDb.Save();
await WriteDataAsync(response);
}
}

View File

@@ -0,0 +1,29 @@
using EpinelPS.Database;
using EpinelPS.Utils;
namespace EpinelPS.LobbyServer.Soloraid;
[PacketPath("/soloraid/trial/setdamage")]
public class SetDamageTrial : LobbyMsgHandler
{
protected override async Task HandleAsync()
{
var req = await ReadData<ReqSetSoloRaidTrialDamage>();
User user = GetUser();
ResSetSoloRaidTrialDamage response = new();
try
{
SoloRaidHelper.SetDamageTrial(user, ref response, req);
}
catch (Exception ex)
{
Logging.WriteLine($"SetDamageTrial Error: {ex.Message}", LogType.Error);
}
JsonDb.Save();
await WriteDataAsync(response);
}
}

View File

@@ -0,0 +1,527 @@
using EpinelPS.Data;
using EpinelPS.Database;
using EpinelPS.Utils;
using Google.Protobuf.Collections;
using log4net;
using Newtonsoft.Json;
namespace EpinelPS.LobbyServer.Soloraid
{
public class SoloRaidHelper
{
private static readonly ILog log = LogManager.GetLogger(typeof(SoloRaidHelper));
/// <summary>
/// Open a solo raid
/// </summary>
public static int OpenSoloRaid(User user, int raidId, int raidLevel, SoloRaidType type = SoloRaidType.Normal)
{
if (raidId == 0) raidId = GetRaidId();
// Check if the raid manager exists, if not, exit
if (!GameData.Instance.SoloRaidManagerTable.TryGetValue(raidId, out var manager)) return 0;
log.Debug($"Fond SoloRaidManager: RaidId: {raidId}, SoloRaidManager: {JsonConvert.SerializeObject(manager)}");
// Get the preset for the raid level
var preset = GameData.Instance.SoloRaidPresetTable.Values.FirstOrDefault(r =>
r.PresetGroupId == manager.MonsterPreset && r.WaveOrder == raidLevel);
if (preset is null) return 0; // If the preset is null, exit
log.Debug($"Fond SoloRaidPreset: PresetGroupId: {manager.MonsterPreset}, WaveOrder: {raidLevel}, SoloRaidPreset: {JsonConvert.SerializeObject(preset)}");
var statEnhanceId = GetStatEnhanceIdByWave(preset.Wave); // Get the stat enhance id for the wave
if (statEnhanceId == 0) return 0; // If the stat enhance id is 0, exit
// Get the stat enhance data for the raid level
var statEnhance = GameData.Instance.MonsterStatEnhanceTable.Values.Where(m => m.Lv == preset.MonsterStageLv && m.GroupId == statEnhanceId);
log.Debug($"Fond MonsterStatEnhance: Lv: {preset.MonsterStageLv}, GroupId: {statEnhanceId}, MonsterStatEnhance: {JsonConvert.SerializeObject(statEnhance)}");
var levelHp = statEnhance.Sum(m => m.LevelHp); // Monster level hp = statEnhance.Sum(m => m.LevelHp)
var raid = GetSoloRaidData(user, raidId, isAdd: true);
switch (type)
{
case SoloRaidType.Normal:
raid.RaidOpenCount++; // If the raid is a normal raid, increment the raid open count
break;
case SoloRaidType.Trial:
raid.TrialCount++; // If the raid is a trial raid, increment the trial count
break;
case SoloRaidType.Practice:
break;
default:
Logging.Warn($"Unknown SoloRaidType: {type}");
break;
}
var level = GetSoloRaidLevelData(raid, raidLevel, type, isOpen: true);
// Reset the level data
if (level != null)
{
raid.SoloRaidLevels.Remove(level);
}
level = new SoloRaidLevelData
{
RaidLevel = raidLevel,
Hp = levelHp,
Type = type,
Status = SoloRaidStatus.Alive,
IsClear = false,
IsOpen = true,
};
raid.SoloRaidLevels.Add(level);
user.SoloRaidData[raidId] = raid;
return type == SoloRaidType.Trial ? raid.TrialCount : raid.RaidOpenCount;
}
public static void CloseSoloRaid(User user, int raidId, int raidLevel, SoloRaidType type = SoloRaidType.Normal)
{
Logging.WriteLine($"CloseSoloRaid: raidId: {raidId}, raidLevel: {raidLevel}, type: {type}");
if (raidId == 0) raidId = GetRaidId();
var raid = GetSoloRaidData(user, raidId, isAdd: false);
// Reset the raid data
if (type == SoloRaidType.Normal) raid.RaidOpenCount--; // If the raid is a normal raid, decrement the raid open count
if (raid.RaidOpenCount < 0) raid.RaidOpenCount = 0; // If the raid open count is less than 0, set it to 0
if (type == SoloRaidType.Trial) raid.TrialCount--;
if (raid.TrialCount < 0) raid.TrialCount = 0;
var level = GetSoloRaidLevelData(raid, raidLevel, type, isOpen: true);
if (level is not null) raid.SoloRaidLevels.Remove(level); // If the level is not null, remove it from the raid data
if (type == SoloRaidType.Trial && level is not null)
{
// If the raid is a trial raid and the level is not null, check if the level is the highest damage level
// If the current level's damage is higher than the previous level, update the status and remove the old level
level.Status = SoloRaidStatus.Kill;
level.IsClear = true;
level.IsOpen = false;
var oldLevel = GetSoloRaidLevelData(raid, raidLevel, SoloRaidType.Trial, isOpen: false);
if (oldLevel is null)
{
raid.SoloRaidLevels.Add(level);
}
else if (level.TotalDamage > oldLevel.TotalDamage)
{
raid.SoloRaidLevels.Remove(oldLevel);
raid.SoloRaidLevels.Add(level);
}
}
user.SoloRaidData[raidId] = raid;
JsonDb.Save();
}
/// <summary>
/// Gets the user solo raid info
/// </summary>
public static NetUserSoloRaidInfo GetUserSoloRaidInfo(User user)
{
int SoloRaidManagerTid = GetRaidId();
NetUserSoloRaidInfo info = new()
{
SoloRaidManagerTid = SoloRaidManagerTid,
LastClearLevel = 0, // set the last clear level
RaidOpenCount = 0, // set the raid open count
Period = GetSoloRaidPeriod(),
};
var raidData = GetSoloRaidData(user, SoloRaidManagerTid);
if (raidData is null) return info;
info.RaidOpenCount = raidData.RaidOpenCount;
int lastClearLevel = GetLastClearLevelId(user, SoloRaidManagerTid);
info.LastClearLevel = lastClearLevel;
var openLevelData = raidData.SoloRaidLevels.Where(r => r.IsOpen).OrderBy(x => x.RaidLevel).FirstOrDefault();
if (openLevelData is not null) info.LastOpenRaid = new NetSoloRaid { Level = openLevelData.RaidLevel, Type = openLevelData.Type };
var trialLevelData = GetSoloRaidLevelData(raidData, 8, SoloRaidType.Trial);
if (trialLevelData is null) return info;
info.TrialCount = raidData.TrialCount;
info.TrialDamage = trialLevelData.TotalDamage;
return info;
}
public static void SetDamage(User user, ref ResSetSoloRaidDamage response, ReqSetSoloRaidDamage req)
{
(int rewardId, int FirstRewardId, var levelData) =
SetDamage(user, req.RaidLevel, req.Damage, req.AntiCheatBattleData.WaveId, req.AntiCheatBattleData.Characters);
if (rewardId > 0) response.Reward = RewardUtils.RegisterRewardsForUser(user, rewardId);
if (FirstRewardId > 0) response.FirstClearReward = RewardUtils.RegisterRewardsForUser(user, FirstRewardId);
response.Info = new NetNormalSoloRaid
{
Level = req.RaidLevel,
Hp = levelData.Hp - levelData.TotalDamage,
};
response.RaidJoinCount = levelData.RaidJoinCount;
response.Status = levelData.Status;
response.PeriodResult = SoloRaidPeriodResult.Success;
response.JoinData = GetJoinData(levelData);
}
public static void SetDamagePractice(User user, ref ResSetSoloRaidPracticeDamage response, ReqSetSoloRaidPracticeDamage req)
{
(_, _, var levelData) =
SetDamage(user, req.RaidLevel, req.Damage, req.AntiCheatBattleData.WaveId, req.AntiCheatBattleData.Characters, SoloRaidType.Practice);
response.Info = new NetPracticeSoloRaid
{
Level = req.RaidLevel,
Hp = levelData.Hp - levelData.TotalDamage,
};
response.RaidJoinCount = levelData.RaidJoinCount;
response.Status = levelData.Status;
response.PeriodResult = SoloRaidPeriodResult.Success;
response.JoinData = GetJoinData(levelData);
}
public static void SetDamageTrial(User user, ref ResSetSoloRaidTrialDamage response, ReqSetSoloRaidTrialDamage req)
{
(_, _, var levelData) =
SetDamage(user, req.RaidLevel, req.Damage, req.AntiCheatBattleData.WaveId, req.AntiCheatBattleData.Characters, SoloRaidType.Trial);
response.Info = new NetTrialSoloRaid
{
Level = req.RaidLevel,
Damage = levelData.TotalDamage,
};
response.RaidJoinCount = levelData.RaidJoinCount;
response.Status = levelData.Status;
response.PeriodResult = SoloRaidPeriodResult.Success;
response.JoinData = GetJoinData(levelData);
}
public static void GetLevelInfo(User user, ref ResGetLevelSoloRaid response, int raidLevel)
{
int raidId = GetRaidId();
response.PeriodResult = SoloRaidPeriodResult.Success;
var raidData = GetSoloRaidData(user, raidId, isAdd: false);
var levelData = GetSoloRaidLevelData(raidData, raidLevel, SoloRaidType.Normal, true);
if (levelData is null) return;
response.Raid = new NetNormalSoloRaid
{
Level = raidLevel,
Hp = levelData.Hp - levelData.TotalDamage,
};
response.RaidJoinCount = levelData.RaidJoinCount;
response.JoinData = GetJoinData(levelData);
}
public static void GetLevelPracticeInfo(User user, ref ResGetLevelPracticeSoloRaid response, int raidLevel)
{
int raidId = GetRaidId();
response.PeriodResult = SoloRaidPeriodResult.Success;
var raidData = GetSoloRaidData(user, raidId, isAdd: false);
var levelData = GetSoloRaidLevelData(raidData, raidLevel, SoloRaidType.Practice, true);
if (levelData is null) return;
response.Raid = new NetPracticeSoloRaid
{
Level = raidLevel,
Hp = levelData.Hp - levelData.TotalDamage,
};
response.RaidJoinCount = levelData.RaidJoinCount;
response.PeriodResult = SoloRaidPeriodResult.Success;
response.JoinData = GetJoinData(levelData);
}
public static void GetLevelTrialInfo(User user, ref ResGetLevelTrialSoloRaid response, int raidLevel)
{
int raidId = GetRaidId();
response.PeriodResult = SoloRaidPeriodResult.Success;
var raidData = GetSoloRaidData(user, raidId, isAdd: false);
var levelData = GetSoloRaidLevelData(raidData, raidLevel, SoloRaidType.Trial, true);
if (levelData is null) return;
response.Raid = new NetTrialSoloRaid
{
Level = raidLevel,
Damage = levelData.TotalDamage,
};
response.RaidJoinCount = levelData.RaidJoinCount;
response.PeriodResult = SoloRaidPeriodResult.Success;
response.JoinData = GetJoinData(levelData);
}
public static void GetSoloRaidLog(User user, ref ResGetSoloRaidLogs response, int raidId, int raidLevel)
{
// ResGetSoloRaidLogs Fields:
// RepeatedField<NetSoloRaidLog> Logs
// SoloRaidBanResult BanResult
// RepeatedField<NetSoloRaidLog> PracticeLogs
// NetSoloRaidLog Fields:
// long Damage
// RepeatedField<NetSoloRaidTeamCharacter> Team
// bool Kill
// NetSoloRaidTeamCharacter Fields:
// int Slot
// int Tid
// int Lv
// int Combat
// int CostumeId
if (raidId == 0) raidId = GetRaidId();
var raidData = GetSoloRaidData(user, raidId);
var levelData = GetSoloRaidLevelData(raidData, raidLevel, SoloRaidType.Normal);
if (levelData is not null && levelData.Logs.Count > 0)
{
response.Logs.AddRange(levelData.Logs.Select(x => x.ToNet()));
}
levelData = GetSoloRaidLevelData(raidData, raidLevel, SoloRaidType.Normal, true);
if (levelData is not null && levelData.Logs.Count > 0)
{
response.Logs.AddRange(levelData.Logs.Select(x => x.ToNet()));
}
var practiceLevelData = GetSoloRaidLevelData(raidData, raidLevel, type: SoloRaidType.Practice);
if (practiceLevelData is not null && practiceLevelData.Logs.Count > 0)
{
response.PracticeLogs.AddRange(practiceLevelData.Logs.Select(x => x.ToNet()));
}
practiceLevelData = GetSoloRaidLevelData(raidData, raidLevel, type: SoloRaidType.Practice, true);
if (practiceLevelData is not null && practiceLevelData.Logs.Count > 0)
{
response.PracticeLogs.AddRange(practiceLevelData.Logs.Select(x => x.ToNet()));
}
}
public static void GetSoloRaidRanking(User user, ref ResGetSoloRaidRanking response)
{
// ResGetSoloRaidRanking Fields:
// RepeatedField<NetSoloRaidRankingData> Rankings
// NetSoloRaidRankingData User
// long TotalUserCount
// SoloRaidBanResult BanResult
// NetSoloRaidRankingData Fields:
// long Ranking
// long Damage
// NetWholeUserData User
// long ReportBattleId
// Google.Protobuf.WellKnownTypes.Timestamp ReportBattleDate
//
}
public static void FastBattle(User user, ref ResFastBattleSoloRaid response, int raidId, int raidLevel, int clearCount)
{
var raidData = GetSoloRaidData(user, raidId, isAdd: true);
raidData.RaidOpenCount += clearCount;
user.SoloRaidData[raidId] = raidData;
response.PeriodResult = SoloRaidPeriodResult.Success;
if (GameData.Instance.SoloRaidManagerTable.TryGetValue(raidId, out var manager))
{
var presetData = GameData.Instance.SoloRaidPresetTable.Values.FirstOrDefault(r =>
r.PresetGroupId == manager.MonsterPreset && r.WaveOrder == raidLevel);
if (presetData is not null)
{
NetRewardData reward = new();
for (int i = 0; i < clearCount; i++)
{
var newReward = RewardUtils.RegisterRewardsForUser(user, presetData.RewardId);
reward.MergeFrom(newReward);
}
response.Reward = reward;
}
}
response.RaidOpenCount = raidData.RaidOpenCount;
JsonDb.Save();
}
public static (int rewardId, int FirstRewardId, SoloRaidLevelData levelData)
SetDamage(User user, int raidLevel, long damage, int waveId,
RepeatedField<NetAntiCheatCharacter> characters, SoloRaidType type = SoloRaidType.Normal)
{
int rewardId = 0;
int FirstRewardId = 0;
int raidId = GetRaidId();
var raidData = GetSoloRaidData(user, raidId, isAdd: true);
var levelData = GetSoloRaidLevelData(raidData, raidLevel, type, isOpen: true);
var oldLevel = GetSoloRaidLevelData(raidData, raidLevel, type, isOpen: false);
// Remove existing level data
if (levelData is not null)
raidData.SoloRaidLevels.Remove(levelData);
else
levelData = new SoloRaidLevelData() { RaidLevel = raidLevel, Type = type, IsOpen = true };
levelData.TotalDamage += damage;
levelData.RaidJoinCount++;
if (type == SoloRaidType.Trial || raidLevel == 8)
{
if (levelData.RaidJoinCount == 5)
{
levelData.Status = SoloRaidStatus.Kill;
levelData.IsClear = true;
levelData.IsOpen = false;
}
else
{
levelData.Status = SoloRaidStatus.Alive;
}
}
else if (levelData.TotalDamage >= levelData.Hp)
{
levelData.TotalDamage = levelData.Hp;
levelData.Status = SoloRaidStatus.Kill;
if (type == SoloRaidType.Normal)
{
var presetData = GameData.Instance.SoloRaidPresetTable.Values.FirstOrDefault(r =>
r.Wave == waveId && r.WaveOrder == raidLevel);
if (presetData is not null)
{
bool isFirstClear = oldLevel is null;
rewardId = presetData.RewardId;
FirstRewardId = !isFirstClear ? 0 : presetData.FirstClearRewardId;
}
}
levelData.IsClear = true;
levelData.IsOpen = false;
}
else
{
levelData.Status = SoloRaidStatus.Alive;
}
SoloRaidLogData logData = new()
{
Damage = damage,
Kill = levelData.Status == SoloRaidStatus.Kill,
};
foreach (var item in characters)
{
int costumeId = user.Characters.FirstOrDefault(c => c.Tid == item.Tid)?.CostumeId ?? 0;
logData.Team.Add(new TeamCharacterData
{
Slot = item.Slot,
Tid = item.Tid,
Csn = item.Csn,
Lv = item.CharacterSpec.Level,
Combat = (int)item.CharacterSpec.Combat,
CostumeId = costumeId,
});
}
levelData.Logs.Add(logData);
// If the level is not open, remove the old level data
if (!levelData.IsOpen && oldLevel is not null)
{
raidData.SoloRaidLevels.Remove(oldLevel);
}
// If the level is not open and join count is 5, do not add to levels
if (!(levelData.RaidJoinCount == 5 && levelData.IsOpen))
{
raidData.SoloRaidLevels.Add(levelData);
}
user.SoloRaidData[raidId] = raidData;
return (rewardId, FirstRewardId, levelData);
}
public static NetSoloRaidJoinData GetJoinData(SoloRaidLevelData? levelData)
{
var joinData = new NetSoloRaidJoinData();
if (levelData is null || levelData.Logs.Count <= 0) return joinData;
foreach (var item in levelData.Logs)
{
joinData.CsnList.AddRange(item.Team.Select(l => l.Csn));
}
return joinData;
}
/// <summary>
/// Gets the solo raid data for the user
/// </summary>
public static SoloRaidInfo? GetSoloRaidData(User user, int raidId, bool isAdd = false)
{
// Get the solo raid data for the raidId, if not found, isAdd is true, create a new one
if (!user.SoloRaidData.TryGetValue(raidId, out var raidData))
{
raidData = new() { RaidId = raidId, LastDateDay = user.GetDateDay() };
if (isAdd) user.SoloRaidData.TryAdd(raidId, raidData);
return isAdd ? raidData : null;
}
ResetOpenCount(user, ref raidData);
return raidData;
}
/// <summary>
/// Gets the last clear level id
/// </summary>
public static int GetLastClearLevelId(User user, int raidId)
{
var raidData = GetSoloRaidData(user, raidId, isAdd: false);
if (raidData is null || !raidData.SoloRaidLevels.Where(r => r.IsClear && r.Type == SoloRaidType.Normal).Any()) return 0;
return raidData.SoloRaidLevels.Where(r => r.IsClear && r.Type == SoloRaidType.Normal).Max(r => r.RaidLevel);
}
public static SoloRaidLevelData? GetSoloRaidLevelData(SoloRaidInfo raidData, int raidLevel, SoloRaidType type, bool isOpen = false)
{
return raidData.SoloRaidLevels.FirstOrDefault(r => r.RaidLevel == raidLevel && r.Type == type && r.IsOpen == isOpen);
}
public static void ResetOpenCount(User user, ref SoloRaidInfo? raidData)
{
if (raidData is null) return;
int newDateDay = user.GetDateDay();
if (newDateDay <= raidData.LastDateDay) return;
Logging.WriteLine($"Reset OpenCount: LastDateDay: {raidData.LastDateDay}, NewDateDay: {newDateDay}, Old: {raidData.RaidOpenCount}, New: 0 ", LogType.Warning);
raidData.LastDateDay = newDateDay;
raidData.RaidOpenCount = 0;
raidData.TrialCount = 0;
}
/// <summary>
/// Gets the solo raid period data
/// </summary>
/// <returns></returns>
public static NetSoloRaidPeriodData GetSoloRaidPeriod()
{
DateTime utcNow = DateTime.UtcNow.Date;
return new NetSoloRaidPeriodData
{
VisibleDate = utcNow.AddDays(-10).Ticks,
StartDate = utcNow.AddDays(-5).Ticks,
EndDate = utcNow.AddDays(5).Ticks,
DisableDate = utcNow.AddDays(10).Ticks,
SettleDate = utcNow.AddDays(15).Ticks,
};
}
private static int GetStatEnhanceIdByWave(int wave)
{
// Get the intercept data for the wave, if not found, return 0
if (!GameData.Instance.WaveIntercept001Table.TryGetValue(wave, out var intercept)) return 0;
log.Debug($"Fond WaveIntercept001: Wave: {wave}, WaveIntercept: {JsonConvert.SerializeObject(intercept)}");
if (intercept.TargetList!.Count == 0) return 0;
var monsterId = intercept.TargetList[0]; // monsterId is the first element of the TargetList
// Get the monster data for the monsterId, if not found, return 0
if (!GameData.Instance.MonsterTable.TryGetValue(monsterId, out var monster)) return 0;
monster.SkillData = [];
log.Debug($"Fond Monster: MonsterId: {monsterId}, Monster: {JsonConvert.SerializeObject(monster)}");
return monster.StatenhanceId;
}
public static int GetRaidId()
{
return GameData.Instance.SoloRaidManagerTable.Keys.Max();
}
}
}

View File

@@ -440,4 +440,63 @@ namespace EpinelPS.Models
public int CutSceneId { get; set; }
public bool IsNew { get; set; }
}
// Solo Raid Data
public class SoloRaidInfo
{
public int RaidId { get; set; }
public int RaidOpenCount { get; set; }
public int TrialCount { get; set; }
public int LastDateDay { get; set; }
public List<SoloRaidLevelData> SoloRaidLevels { get; set; } = []; // key: raidLevel
}
public class SoloRaidLevelData
{
public int RaidLevel { get; set; }
public int RaidJoinCount { get; set; }
public long Hp { get; set; }
public long TotalDamage { get; set; }
public bool IsClear { get; set; }
public SoloRaidStatus Status { get; set; }
public SoloRaidType Type { get; set; }
public bool IsOpen { get; set; }
public List<SoloRaidLogData> Logs { get; set; } = [];
}
public class SoloRaidLogData
{
public long Damage { get; set; }
public bool Kill { get; set; }
public List<TeamCharacterData> Team { get; set; } = [];
public NetSoloRaidLog ToNet()
{
return new NetSoloRaidLog()
{
Damage = Damage,
Kill = Kill,
Team = { Team.Select(x => x.ToNet()).ToList() },
};
}
}
public class TeamCharacterData
{
public int Slot { get; set; }
public long Csn { get; set; }
public int Tid { get; set; }
public int Lv { get; set; }
public int Combat { get; set; }
public int CostumeId { get; set; }
public NetSoloRaidTeamCharacter ToNet()
{
return new NetSoloRaidTeamCharacter()
{
Slot = Slot,
Tid = Tid,
Lv = Lv,
Combat = Combat,
CostumeId = CostumeId,
};
}
}
}

View File

@@ -126,6 +126,8 @@ public class User
public List<int> LobbyPrivateBannerIds = [];
public Dictionary<int, MiniGameAzxData> MiniGameAzxInfo = [];
// solo raid data
public Dictionary<int, SoloRaidInfo> SoloRaidData = []; // key: raidId
public TriggerModel AddTrigger(Trigger type, int value, int conditionId = 0)
{