Skill upgrade, more commands, fixes (#21)

* add BuyWallpaper

* added partial stage clear info handling, no longer fails get check

the creation of the clear info still has imperfections, so I've left it disabled for now. no point in letting it clutter the DB until the info is at least accurate in the UI.

* unfinished CP calculation stuff

still need to impl other tables (equipment), need a better way to capture outputs and compare them to retail, not really worth the excess work currently since CP calc isn't used anywhere critical right now

* AddItem command

* AddCharacter command

* finishalltutorials command

* addallmaterials command

also load the material items table

* skill upgrade impl

* update README

* potential fix for bodies not being removed properly on limit breaks and core upgrades

the client seems to be smart enough to deal with zero count item entries, so there's no real reason to remove zero count item entries from the DB

* check to make sure we have the character before adding a body

* use CreateWholeUserDataFromDbUser instead of doing a manual copy
This commit is contained in:
Kyle873
2024-12-20 14:39:21 -05:00
committed by GitHub
parent 6c58b3e137
commit b022eb688c
15 changed files with 947 additions and 440 deletions

View File

@@ -183,6 +183,7 @@ namespace EpinelPS.Database
public NetWallpaperJukeboxFavorite[] WallpaperFavoriteList = []; public NetWallpaperJukeboxFavorite[] WallpaperFavoriteList = [];
public NetWallpaperPlaylist[] WallpaperPlaylistList = []; public NetWallpaperPlaylist[] WallpaperPlaylistList = [];
public NetWallpaperJukebox[] WallpaperJukeboxList = []; public NetWallpaperJukebox[] WallpaperJukeboxList = [];
public List<int> LobbyDecoBackgroundList = [];
public Dictionary<int, NetUserTeamData> UserTeams = new Dictionary<int, NetUserTeamData>(); public Dictionary<int, NetUserTeamData> UserTeams = new Dictionary<int, NetUserTeamData>();
@@ -208,6 +209,8 @@ namespace EpinelPS.Database
public OutpostBuffs OutpostBuffs = new(); public OutpostBuffs OutpostBuffs = new();
public Dictionary<int, UnlockData> ContentsOpenUnlocked = new(); public Dictionary<int, UnlockData> ContentsOpenUnlocked = new();
public List<NetStageClearInfo> StageClearHistorys = [];
// Event data // Event data
public Dictionary<int, EventData> EventInfo = new(); public Dictionary<int, EventData> EventInfo = new();
public MogMinigameInfo MogInfo = new(); public MogMinigameInfo MogInfo = new();
@@ -382,24 +385,12 @@ namespace EpinelPS.Database
if (item.Isn == isn) if (item.Isn == isn)
{ {
if (item.Count == 1) removed++;
item.Count -= count;
if (item.Count < 0)
{ {
Items.Remove(item); item.Count = 0;
count--;
}
else
{
// TODO test this
if (item.Count >= count)
{
removed++;
item.Count -= count;
}
else
{
removed += item.Count;
Items.Remove(item);
}
} }
} }
} }

View File

@@ -32,8 +32,9 @@ namespace EpinelPS.StaticInfo
private Dictionary<int, CampaignChapterRecord> chapterCampaignData; private Dictionary<int, CampaignChapterRecord> chapterCampaignData;
private JArray characterCostumeTable; private JArray characterCostumeTable;
public Dictionary<int, CharacterRecord> characterTable; public Dictionary<int, CharacterRecord> characterTable;
private Dictionary<int, ClearedTutorialData> tutorialTable; public Dictionary<int, ClearedTutorialData> tutorialTable;
private Dictionary<int, ItemEquipRecord> itemEquipTable; public Dictionary<int, ItemEquipRecord> itemEquipTable;
public Dictionary<int, ItemMaterialRecord> itemMaterialTable;
private Dictionary<string, JArray> FieldMapData = new Dictionary<string, JArray>(); // Fixed initialization private Dictionary<string, JArray> FieldMapData = new Dictionary<string, JArray>(); // Fixed initialization
private Dictionary<int, CharacterLevelData> LevelData = new Dictionary<int, CharacterLevelData>(); // Fixed initialization private Dictionary<int, CharacterLevelData> LevelData = new Dictionary<int, CharacterLevelData>(); // Fixed initialization
private Dictionary<int, TacticAcademyLessonRecord> TacticAcademyLessons = new Dictionary<int, TacticAcademyLessonRecord>(); // Fixed initialization private Dictionary<int, TacticAcademyLessonRecord> TacticAcademyLessons = new Dictionary<int, TacticAcademyLessonRecord>(); // Fixed initialization
@@ -54,6 +55,9 @@ namespace EpinelPS.StaticInfo
public Dictionary<int, ArchiveEventDungeonStageRecord> archiveEventDungeonStageRecords = new Dictionary<int, ArchiveEventDungeonStageRecord>(); public Dictionary<int, ArchiveEventDungeonStageRecord> archiveEventDungeonStageRecords = new Dictionary<int, ArchiveEventDungeonStageRecord>();
public Dictionary<int, UserTitleRecord> userTitleRecords = new Dictionary<int, UserTitleRecord>(); public Dictionary<int, UserTitleRecord> userTitleRecords = new Dictionary<int, UserTitleRecord>();
public Dictionary<int, ArchiveMessengerConditionRecord> archiveMessengerConditionRecords; public Dictionary<int, ArchiveMessengerConditionRecord> archiveMessengerConditionRecords;
public Dictionary<int, CharacterStatRecord> characterStatTable;
public Dictionary<int, SkillInfoRecord> skillInfoTable;
public Dictionary<int, CostRecord> costTable;
@@ -90,6 +94,10 @@ namespace EpinelPS.StaticInfo
ZipStream = new(); ZipStream = new();
tutorialTable = new(); tutorialTable = new();
itemEquipTable = new(); itemEquipTable = new();
itemMaterialTable = new();
characterStatTable = new();
skillInfoTable = new();
costTable = new();
// Initialize Jukebox data dictionaries // Initialize Jukebox data dictionaries
jukeboxListDataRecords = new Dictionary<int, JukeboxListRecord>(); jukeboxListDataRecords = new Dictionary<int, JukeboxListRecord>();
@@ -320,6 +328,12 @@ namespace EpinelPS.StaticInfo
this.itemEquipTable.Add(obj.id, obj); this.itemEquipTable.Add(obj.id, obj);
} }
var itemMaterialTable = await LoadZip<ItemMaterialTable>("ItemMaterialTable.json", progress);
foreach (var obj in itemMaterialTable.records)
{
this.itemMaterialTable.Add(obj.id, obj);
}
var characterLevelTable = await LoadZip("CharacterLevelTable.json", progress); var characterLevelTable = await LoadZip("CharacterLevelTable.json", progress);
foreach (JToken item in characterLevelTable) foreach (JToken item in characterLevelTable)
@@ -460,6 +474,24 @@ namespace EpinelPS.StaticInfo
// Load Jukebox data // Load Jukebox data
await LoadJukeboxListData(progress); await LoadJukeboxListData(progress);
await LoadJukeboxThemeData(progress); await LoadJukeboxThemeData(progress);
var characterStatTable = await LoadZip<CharacterStatTable>("CharacterStatTable.json", progress);
foreach (var obj in characterStatTable.records)
{
this.characterStatTable.Add(obj.id, obj);
}
var skillinfoTable = await LoadZip<SkillInfoTable>("SkillInfoTable.json", progress);
foreach (var obj in skillinfoTable.records)
{
this.skillInfoTable.Add(obj.id, obj);
}
var costTable = await LoadZip<CostTable>("CostTable.json", progress);
foreach (var obj in costTable.records)
{
this.costTable.Add(obj.id, obj);
}
} }
public async Task LoadJukeboxListData(ProgressBar bar) public async Task LoadJukeboxListData(ProgressBar bar)

View File

@@ -131,11 +131,38 @@
{ {
public int id; public int id;
public int piece_id; public int piece_id;
public string original_rare; public string original_rare;
public string corporation; public string corporation;
public int grade_core_id; public int grade_core_id;
public int name_code; public int name_code;
public int grow_grade; public int grow_grade;
public int stat_enhance_id;
public string character_class;
public List<int> element_id;
public int critical_ratio;
public int critical_damage;
public int shot_id;
public int bonusrange_min;
public int bonusrange_max;
public string use_burst_skill;
public string change_burst_step;
public int burst_apply_delay;
public int burst_duration;
public int ulti_skill_id;
public int skill1_id;
public string skill1_table;
public int skill2_id;
public string skill2_table;
public string eff_category_type;
public int eff_category_value;
public string category_type_1;
public string category_type_2;
public string category_type_3;
public string cv_localkey;
public string squad;
public bool is_visible;
public bool prism_is_active;
public bool is_detail_close;
} }
public class CharacterTable public class CharacterTable
{ {
@@ -427,4 +454,82 @@
public List<ArchiveMessengerConditionRecord> records; public List<ArchiveMessengerConditionRecord> records;
} }
public class CharacterStatRecord
{
public int id;
public int group;
public int level;
public int level_hp;
public int level_attack;
public int level_defence;
public int level_energy_resist;
public int level_bio_resist;
}
public class CharacterStatTable
{
public List<CharacterStatRecord> records;
}
public class ItemMaterialRecord
{
public int id;
public string name_localkey;
public string description_localkey;
public string resource_id;
public string item_type;
public string item_sub_type;
public string item_rare;
public int item_value;
public string material_type;
public int material_value;
public int stack_max;
}
public class ItemMaterialTable
{
public List<ItemMaterialRecord> records;
}
public class SkillInfoRecord
{
public int id;
public int group_id;
public int skill_level;
public int next_level_id;
public int level_up_cost_id;
public string icon;
public string name_localkey;
public string description_localkey;
public string info_description_localkey;
public List<DescriptionValue> description_value_list;
}
public class DescriptionValue
{
public string description_value;
}
public class SkillInfoTable
{
public List<SkillInfoRecord> records;
}
public class CostRecord
{
public int id;
public List<CostData> costs;
}
public class CostData
{
public string item_type;
public int item_id;
public int item_value;
}
public class CostTable
{
public List<CostRecord> records;
}
} }

View File

@@ -55,12 +55,9 @@ namespace EpinelPS.LobbyServer.Msgs.Character
}; };
// remove spare body item // remove spare body item
var bodyItem = user.Items.FirstOrDefault(i => i.Isn == req.Isn);
user.RemoveItemBySerialNumber(req.Isn, 1); user.RemoveItemBySerialNumber(req.Isn, 1);
response.Items.Add(NetUtils.ToNet(bodyItem));
foreach (var item in user.Items)
{
response.Items.Add(NetUtils.ToNet(item));
}
// replace any reference to the old character to the new TID // replace any reference to the old character to the new TID
// Check if RepresentationTeamData exists and has slots // Check if RepresentationTeamData exists and has slots

View File

@@ -0,0 +1,87 @@
using EpinelPS.Database;
using EpinelPS.StaticInfo;
using EpinelPS.Utils;
namespace EpinelPS.LobbyServer.Msgs.Character
{
[PacketPath("/character/skill/levelup")]
public class SkillLevelUp : LobbyMsgHandler
{
protected override async Task HandleAsync()
{
var req = await ReadData<ReqCharacterSkillLevelUp>();
var user = GetUser();
var response = new ResCharacterSkillLevelUp();
var character = user.Characters.FirstOrDefault(c => c.Csn == req.Csn);
var charRecord = GameData.Instance.characterTable.Values.FirstOrDefault(c => c.id == character.Tid);
var skillIdMap = new Dictionary<int, int>
{
{ 1, charRecord.ulti_skill_id },
{ 2, charRecord.skill1_id },
{ 3, charRecord.skill2_id }
};
var skillLevelMap = new Dictionary<int, int>
{
{ 1, character.UltimateLevel },
{ 2, character.Skill1Lvl },
{ 3, character.Skill2Lvl }
};
var skillRecord = GameData.Instance.skillInfoTable.Values.FirstOrDefault(s => s.id == skillIdMap[req.Category] + (skillLevelMap[req.Category] - 1));
var costRecord = GameData.Instance.costTable.Values.FirstOrDefault(c => c.id == skillRecord.level_up_cost_id);
foreach (var cost in costRecord.costs.Where(i => i.item_type != "None"))
{
var item = user.Items.FirstOrDefault(i => i.ItemType == cost.item_id);
item.Count -= cost.item_value;
response.Items.Add(new NetUserItemData
{
Isn = item.Isn,
Tid = cost.item_id,
Count = item.Count,
Csn = item.Csn,
Corporation = item.Corp,
Level = item.Level,
Exp = item.Exp,
Position = item.Position
});
}
var newChar = new NetUserCharacterDefaultData
{
CostumeId = character.CostumeId,
Csn = character.Csn,
Level = character.Level,
Grade = character.Grade,
Tid = character.Tid,
DispatchTid = character.Tid,
Skill1Lv = character.Skill1Lvl,
Skill2Lv = character.Skill2Lvl,
UltiSkillLv = character.UltimateLevel,
};
if (req.Category == 1)
{
character.UltimateLevel++;
newChar.UltiSkillLv++;
}
else if (req.Category == 2)
{
character.Skill1Lvl++;
newChar.Skill1Lv++;
}
else if (req.Category == 3)
{
character.Skill2Lvl++;
newChar.Skill2Lv++;
}
response.Character = newChar;
JsonDb.Save();
await WriteDataAsync(response);
}
}
}

View File

@@ -56,12 +56,9 @@ namespace EpinelPS.LobbyServer.Msgs.Character
}; };
// remove spare body item // remove spare body item
var bodyItem = user.Items.FirstOrDefault(i => i.Isn == req.Isn);
user.RemoveItemBySerialNumber(req.Isn, 1); user.RemoveItemBySerialNumber(req.Isn, 1);
response.Items.Add(NetUtils.ToNet(bodyItem));
foreach (var item in user.Items)
{
response.Items.Add(NetUtils.ToNet(item));
}
// replace any reference to the old character to the new TID // replace any reference to the old character to the new TID
// Check if RepresentationTeamData exists and has slots // Check if RepresentationTeamData exists and has slots

View File

@@ -64,55 +64,58 @@ namespace EpinelPS.LobbyServer.Msgs.Gacha
// Add each character's item to user.Items if the character exists in user.Characters // Add each character's item to user.Items if the character exists in user.Characters
foreach (var characterData in selectedCharacters) foreach (var characterData in selectedCharacters)
{ {
// Check if the item for this character already exists in user.Items based on ItemType if (user.HasCharacter(characterData.id))
var existingItem = user.Items.FirstOrDefault(item => item.ItemType == characterData.piece_id); {
// Check if the item for this character already exists in user.Items based on ItemType
var existingItem = user.Items.FirstOrDefault(item => item.ItemType == characterData.piece_id);
if (existingItem != null) if (existingItem != null)
{ {
// If the item exists, increment the count // If the item exists, increment the count
existingItem.Count += 1; existingItem.Count += 1;
// Send the updated item in the response // Send the updated item in the response
response.Items.Add(new NetUserItemData() response.Items.Add(new NetUserItemData()
{ {
Tid = existingItem.ItemType, Tid = existingItem.ItemType,
Csn = existingItem.Csn, Csn = existingItem.Csn,
Count = existingItem.Count, Count = existingItem.Count,
Level = existingItem.Level, Level = existingItem.Level,
Exp = existingItem.Exp, Exp = existingItem.Exp,
Position = existingItem.Position, Position = existingItem.Position,
Isn = existingItem.Isn Isn = existingItem.Isn
}); });
} }
else else
{ {
// If the item does not exist, create a new item entry // If the item does not exist, create a new item entry
var newItem = new ItemData() var newItem = new ItemData()
{ {
ItemType = characterData.piece_id, ItemType = characterData.piece_id,
Csn = 0, Csn = 0,
Count = 1, // or any relevant count Count = 1, // or any relevant count
Level = 0, Level = 0,
Exp = 0, Exp = 0,
Position = 0, Position = 0,
Corp = 0, Corp = 0,
Isn = user.GenerateUniqueItemId() Isn = user.GenerateUniqueItemId()
}; };
user.Items.Add(newItem); user.Items.Add(newItem);
// Add the new item to response // Add the new item to response
response.Items.Add(new NetUserItemData() response.Items.Add(new NetUserItemData()
{ {
Tid = newItem.ItemType, Tid = newItem.ItemType,
Csn = newItem.Csn, Csn = newItem.Csn,
Count = newItem.Count, Count = newItem.Count,
Level = newItem.Level, Level = newItem.Level,
Exp = newItem.Exp, Exp = newItem.Exp,
Position = newItem.Position, Position = newItem.Position,
Isn = newItem.Isn Isn = newItem.Isn
}); });
} }
} }
}
// Populate the 2D array with characterId and pieceId for each selected character // Populate the 2D array with characterId and pieceId for each selected character
foreach (var characterData in selectedCharacters) foreach (var characterData in selectedCharacters)

View File

@@ -104,6 +104,8 @@ namespace EpinelPS.LobbyServer.Msgs.Stage
} }
} }
// CreateClearInfo(user);
var key = (clearedStage.chapter_id - 1) + "_" + clearedStage.chapter_mod; var key = (clearedStage.chapter_id - 1) + "_" + clearedStage.chapter_mod;
if (!user.FieldInfoNew.ContainsKey(key)) if (!user.FieldInfoNew.ContainsKey(key))
user.FieldInfoNew.Add(key, new FieldInfoNew()); user.FieldInfoNew.Add(key, new FieldInfoNew());
@@ -309,5 +311,29 @@ namespace EpinelPS.LobbyServer.Msgs.Stage
} }
// TODO: add neon // TODO: add neon
} }
private static void CreateClearInfo(Database.User user)
{
NetStageClearInfo clearInfo = new NetStageClearInfo
{
User = LobbyHandler.CreateWholeUserDataFromDbUser(user),
TeamCombat = user.RepresentationTeamData.TeamCombat,
ClearedAt = DateTimeOffset.UtcNow.Ticks
};
foreach (var character in user.RepresentationTeamData.Slots)
{
clearInfo.Slots.Add(new NetStageClearInfoTeam()
{
Slot = character.Slot,
Tid = character.Tid,
Level = character.Level,
Combat = FormulaUtils.CalculateCP(user, character.Csn),
CharacterType = StageClearInfoTeamCharacterType.StageClearInfoTeamCharacterTypeOwnedCharacter // TODO: how do we get this?
});
}
user.StageClearHistorys.Add(clearInfo);
}
} }
} }

View File

@@ -0,0 +1,19 @@
using EpinelPS.Utils;
namespace EpinelPS.LobbyServer.Msgs.Stage
{
[PacketPath("/stageclearinfo/get")]
public class GetStageClearInfo : LobbyMsgHandler
{
protected override async Task HandleAsync()
{
var req = await ReadData<ReqGetStageClearInfo>();
var response = new ResGetStageClearInfo();
var user = GetUser();
response.Historys.AddRange(user.StageClearHistorys);
await WriteDataAsync(response);
}
}
}

View File

@@ -0,0 +1,24 @@
using EpinelPS.Database;
using EpinelPS.Utils;
namespace EpinelPS.LobbyServer.Msgs.User
{
[PacketPath("/user/wallpaper/buy")]
public class BuyWallpaper : LobbyMsgHandler
{
protected override async Task HandleAsync()
{
var req = await ReadData<ReqBuyLobbyDecoBackground>();
var response = new ResBuyLobbyDecoBackground();
var user = GetUser();
user.LobbyDecoBackgroundList.Add(req.LobbyDecoBackgroundId);
response.OwnedLobbyDecoBackgroundIdList.Add(user.LobbyDecoBackgroundList);
JsonDb.Save();
await WriteDataAsync(response);
}
}
}

View File

@@ -85,6 +85,8 @@ namespace EpinelPS.LobbyServer.Msgs.User
response.LastClearedNormalMainStageId = user.LastNormalStageCleared; response.LastClearedNormalMainStageId = user.LastNormalStageCleared;
response.TimeRewardBuffs.AddRange(NetUtils.GetOutpostTimeReward(user)); response.TimeRewardBuffs.AddRange(NetUtils.GetOutpostTimeReward(user));
response.OwnedLobbyDecoBackgroundIdList.AddRange(user.LobbyDecoBackgroundList);
await WriteDataAsync(response); await WriteDataAsync(response);
} }
} }

View File

@@ -17,6 +17,7 @@ namespace EpinelPS.LobbyServer.Msgs.User
response.WallpaperJukeboxList.AddRange(user.WallpaperJukeboxList); response.WallpaperJukeboxList.AddRange(user.WallpaperJukeboxList);
response.WallpaperBackgroundList.AddRange(user.WallpaperBackground); response.WallpaperBackgroundList.AddRange(user.WallpaperBackground);
response.WallpaperFavoriteList.AddRange(user.WallpaperFavoriteList); response.WallpaperFavoriteList.AddRange(user.WallpaperFavoriteList);
response.OwnedLobbyDecoBackgroundIdList.AddRange(user.LobbyDecoBackgroundList);
// TODO: JukeboxIdList // TODO: JukeboxIdList

View File

@@ -217,13 +217,16 @@ namespace EpinelPS
Console.WriteLine(" unban - unban selected user from game"); Console.WriteLine(" unban - unban selected user from game");
Console.WriteLine(" exit - exit server application"); Console.WriteLine(" exit - exit server application");
Console.WriteLine(" completestage (chapter num)-(stage number) - complete selected stage and get rewards (and all previous ones). Example completestage 15-1. Note that the exact stage number cleared may not be exact."); Console.WriteLine(" completestage (chapter num)-(stage number) - complete selected stage and get rewards (and all previous ones). Example completestage 15-1. Note that the exact stage number cleared may not be exact.");
Console.WriteLine(" sickpulls (requires selecting user first) allows for all characters to have equal chances of getting pulled"); Console.WriteLine(" sickpulls (requires selecting user first) allows for all characters to have equal chances of getting pulled");
Console.WriteLine(" SetLevel (level) - Set all characters' level (between 1 and 999 takes effect on game and server restart)"); Console.WriteLine(" SetLevel (level) - Set all characters' level (between 1 and 999 takes effect on game and server restart)");
Console.WriteLine(" SetSkillLevel (level) - Set all characters' skill levels between 1 and 10 (takes effect on game and server restart)"); Console.WriteLine(" SetSkillLevel (level) - Set all characters' skill levels between 1 and 10 (takes effect on game and server restart)");
Console.WriteLine(" addallcharacters - Add all missing characters to the selected user with default levels and skills (takes effect on game and server restart)"); Console.WriteLine(" addallcharacters - Add all missing characters to the selected user with default levels and skills (takes effect on game and server restart)");
Console.WriteLine(" addallmaterials (amount) - Add all materials to the selected user with default levels and skills (takes effect on game and server restart)");
Console.WriteLine(" finishalltutorials - finish all tutorials for the selected user (takes effect on game and server restart)");
Console.WriteLine(" SetCoreLevel (core level / 0-3 sets stars) - Set all characters' grades based on the input (from 0 to 11)"); Console.WriteLine(" SetCoreLevel (core level / 0-3 sets stars) - Set all characters' grades based on the input (from 0 to 11)");
Console.WriteLine(" AddItem (id) (amount) - Adds an item to the selected user (takes effect on game and server restart)");
} Console.WriteLine(" AddCharacter (id) - Adds a character to the selected user (takes effect on game and server restart)");
}
else if (input == "ls /users") else if (input == "ls /users")
{ {
Console.WriteLine("Id,Username,Nickname"); Console.WriteLine("Id,Username,Nickname");
@@ -263,299 +266,378 @@ namespace EpinelPS
Console.WriteLine("Usage: chroot (user id)"); Console.WriteLine("Usage: chroot (user id)");
} }
} }
else if (input == "addallcharacters") else if (input == "addallcharacters")
{ {
if (selectedUser == 0) if (selectedUser == 0)
{ {
Console.WriteLine("No user selected"); Console.WriteLine("No user selected");
} }
else else
{ {
var user = JsonDb.Instance.Users.FirstOrDefault(x => x.ID == selectedUser); var user = JsonDb.Instance.Users.FirstOrDefault(x => x.ID == selectedUser);
if (user == null) if (user == null)
{ {
Console.WriteLine("Selected user does not exist"); Console.WriteLine("Selected user does not exist");
selectedUser = 0; selectedUser = 0;
prompt = "# "; prompt = "# ";
} }
else else
{ {
// Group characters by name_code and always add those with grade_core_id == 11, 103, and include grade_core_id == 201 // Group characters by name_code and always add those with grade_core_id == 11, 103, and include grade_core_id == 201
var allCharacters = GameData.Instance.characterTable.Values var allCharacters = GameData.Instance.characterTable.Values
.GroupBy(c => c.name_code) // Group by name_code to treat same name_code as one character 3999 = marian .GroupBy(c => c.name_code) // Group by name_code to treat same name_code as one character 3999 = marian
.SelectMany(g => g.Where(c => c.grade_core_id == 1 || c.grade_core_id == 101 || c.grade_core_id == 201 || c.name_code == 3999)) // Always add characters with grade_core_id == 11 and 103 .SelectMany(g => g.Where(c => c.grade_core_id == 1 || c.grade_core_id == 101 || c.grade_core_id == 201 || c.name_code == 3999)) // Always add characters with grade_core_id == 11 and 103
.ToList(); .ToList();
foreach (var character in allCharacters) foreach (var character in allCharacters)
{ {
if (!user.HasCharacter(character.id)) if (!user.HasCharacter(character.id))
{ {
user.Characters.Add(new Database.Character() user.Characters.Add(new Database.Character()
{ {
CostumeId = 0, CostumeId = 0,
Csn = user.GenerateUniqueCharacterId(), Csn = user.GenerateUniqueCharacterId(),
Grade = 0, Grade = 0,
Level = 1, Level = 1,
Skill1Lvl = 1, Skill1Lvl = 1,
Skill2Lvl = 1, Skill2Lvl = 1,
Tid = character.id, // Tid is the character ID Tid = character.id, // Tid is the character ID
UltimateLevel = 1 UltimateLevel = 1
}); });
} }
} }
Console.WriteLine("Added all missing characters to user " + user.Username); Console.WriteLine("Added all missing characters to user " + user.Username);
JsonDb.Save(); JsonDb.Save();
} }
} }
} }
else if (input.StartsWith("SetCoreLevel")) else if (input == "addallmaterials")
{ {
if (selectedUser == 0) if (selectedUser == 0)
{ {
Console.WriteLine("No user selected"); Console.WriteLine("No user selected");
} }
else else
{ {
var user = JsonDb.Instance.Users.FirstOrDefault(x => x.ID == selectedUser); var user = JsonDb.Instance.Users.FirstOrDefault(x => x.ID == selectedUser);
if (user == null) if (user == null)
{ {
Console.WriteLine("Selected user does not exist"); Console.WriteLine("Selected user does not exist");
selectedUser = 0; selectedUser = 0;
prompt = "# "; prompt = "# ";
} }
else if (args.Length == 2 && int.TryParse(args[1], out int inputGrade) && inputGrade >= 0 && inputGrade <= 11) else
{ {
foreach (var character in user.Characters) int amount = 1000000;
{ if (args.Length >= 2)
// Get current character's Tid {
int tid = character.Tid; int.TryParse(args[1], out amount);
}
// Get the character data from the character table foreach (var tableItem in GameData.Instance.itemMaterialTable.Values)
if (!GameData.Instance.characterTable.TryGetValue(tid, out var charData)) {
{ ItemData? item = user.Items.FirstOrDefault(i => i.ItemType == tableItem.id);
Console.WriteLine($"Character data not found for Tid {tid}");
continue;
}
int currentGradeCoreId = charData.grade_core_id; if (item == null)
int nameCode = charData.name_code; {
string originalRare = charData.original_rare; user.Items.Add(new ItemData
{
Isn = user.GenerateUniqueItemId(),
ItemType = tableItem.id,
Level = 1,
Exp = 1,
Count = amount
});
}
else
{
item.Count += amount;
}
}
// Skip characters with original_rare == "R" Console.WriteLine($"Added {amount} of all materials to user " + user.Username);
if (originalRare == "R" || nameCode == 3999) JsonDb.Save();
{ }
continue; }
} }
else if (input == "finishalltutorials")
{
if (selectedUser == 0)
{
Console.WriteLine("No user selected");
}
else
{
var user = JsonDb.Instance.Users.FirstOrDefault(x => x.ID == selectedUser);
if (user == null)
{
Console.WriteLine("Selected user does not exist");
selectedUser = 0;
prompt = "# ";
}
else
{
foreach (var tutorial in GameData.Instance.tutorialTable.Values)
{
if (!user.ClearedTutorialData.ContainsKey(tutorial.id))
{
user.ClearedTutorialData.Add(tutorial.id, tutorial);
}
}
// Now handle normal SR and SSR characters Console.WriteLine("Finished all tutorials for user " + user.Username);
int maxGradeCoreId = 0; JsonDb.Save();
}
}
}
else if (input.StartsWith("SetCoreLevel"))
{
if (selectedUser == 0)
{
Console.WriteLine("No user selected");
}
else
{
var user = JsonDb.Instance.Users.FirstOrDefault(x => x.ID == selectedUser);
if (user == null)
{
Console.WriteLine("Selected user does not exist");
selectedUser = 0;
prompt = "# ";
}
else if (args.Length == 2 && int.TryParse(args[1], out int inputGrade) && inputGrade >= 0 && inputGrade <= 11)
{
foreach (var character in user.Characters)
{
// Get current character's Tid
int tid = character.Tid;
// If the character is "SSR", it can have a grade_core_id from 1 to 11 // Get the character data from the character table
if (originalRare == "SSR") if (!GameData.Instance.characterTable.TryGetValue(tid, out var charData))
{ {
maxGradeCoreId = 11; // SSR characters can go from 1 to 11 Console.WriteLine($"Character data not found for Tid {tid}");
continue;
}
// Calculate the new grade_core_id within the bounds int currentGradeCoreId = charData.grade_core_id;
int newGradeCoreId = Math.Min(inputGrade + 1, maxGradeCoreId); // +1 because inputGrade starts from 0 for SSRs int nameCode = charData.name_code;
string originalRare = charData.original_rare;
// Find the character with the same name_code and new grade_core_id // Skip characters with original_rare == "R"
var newCharData = GameData.Instance.characterTable.Values.FirstOrDefault(c => if (originalRare == "R" || nameCode == 3999)
c.name_code == nameCode && c.grade_core_id == newGradeCoreId); {
continue;
}
if (newCharData != null) // Now handle normal SR and SSR characters
{ int maxGradeCoreId = 0;
// Update the character's Tid and Grade
character.Tid = newCharData.id;
character.Grade = inputGrade;
}
} // If the character is "SSR", it can have a grade_core_id from 1 to 11
if (originalRare == "SSR")
{
maxGradeCoreId = 11; // SSR characters can go from 1 to 11
// If the character is "SR", it can have a grade_core_id from 101 to 103 // Calculate the new grade_core_id within the bounds
else if (originalRare == "SR") int newGradeCoreId = Math.Min(inputGrade + 1, maxGradeCoreId); // +1 because inputGrade starts from 0 for SSRs
{
maxGradeCoreId = 103; // SR characters can go from 101 to 103
// Start from 101 and increment based on inputGrade (inputGrade 0 -> grade_core_id 101) // Find the character with the same name_code and new grade_core_id
int newGradeCoreId = Math.Min(101 + inputGrade, maxGradeCoreId); // Starts at 101 var newCharData = GameData.Instance.characterTable.Values.FirstOrDefault(c =>
c.name_code == nameCode && c.grade_core_id == newGradeCoreId);
// Find the character with the same name_code and new grade_core_id if (newCharData != null)
var newCharData = GameData.Instance.characterTable.Values.FirstOrDefault(c => {
c.name_code == nameCode && c.grade_core_id == newGradeCoreId); // Update the character's Tid and Grade
character.Tid = newCharData.id;
character.Grade = inputGrade;
}
if (newCharData != null) }
{
// Update the character's Tid and Grade
character.Tid = newCharData.id;
character.Grade = inputGrade;
}
} // If the character is "SR", it can have a grade_core_id from 101 to 103
else if (originalRare == "SR")
{
maxGradeCoreId = 103; // SR characters can go from 101 to 103
} // Start from 101 and increment based on inputGrade (inputGrade 0 -> grade_core_id 101)
Console.WriteLine($"Core level of all characters have been set to {inputGrade}"); int newGradeCoreId = Math.Min(101 + inputGrade, maxGradeCoreId); // Starts at 101
JsonDb.Save();
}
else
{
Console.WriteLine("Invalid argument. Core level must be between 0 and 11.");
}
}
//code above WILL change tids in user.characters so this will update them in representation team
foreach (var user in JsonDb.Instance.Users)
{
// Check if RepresentationTeamData exists and has slots
if (user.RepresentationTeamData != null && user.RepresentationTeamData.Slots != null)
{
// Iterate through RepresentationTeamData slots
foreach (var slot in user.RepresentationTeamData.Slots)
{
// Find the character in user's character list that matches the slot's Csn
var correspondingCharacter = user.Characters.FirstOrDefault(c => c.Csn == slot.Csn);
if (correspondingCharacter != null) // Find the character with the same name_code and new grade_core_id
{ var newCharData = GameData.Instance.characterTable.Values.FirstOrDefault(c =>
// Update the Tid value if it differs c.name_code == nameCode && c.grade_core_id == newGradeCoreId);
if (slot.Tid != correspondingCharacter.Tid)
{
slot.Tid = correspondingCharacter.Tid;
}
}
}
}
}
// Save the updated data if (newCharData != null)
JsonDb.Save(); {
// Update the character's Tid and Grade
character.Tid = newCharData.id;
character.Grade = inputGrade;
}
} }
}
Console.WriteLine($"Core level of all characters have been set to {inputGrade}");
JsonDb.Save();
}
else
{
Console.WriteLine("Invalid argument. Core level must be between 0 and 11.");
}
}
//code above WILL change tids in user.characters so this will update them in representation team
foreach (var user in JsonDb.Instance.Users)
{
// Check if RepresentationTeamData exists and has slots
if (user.RepresentationTeamData != null && user.RepresentationTeamData.Slots != null)
{
// Iterate through RepresentationTeamData slots
foreach (var slot in user.RepresentationTeamData.Slots)
{
// Find the character in user's character list that matches the slot's Csn
var correspondingCharacter = user.Characters.FirstOrDefault(c => c.Csn == slot.Csn);
if (correspondingCharacter != null)
{
// Update the Tid value if it differs
if (slot.Tid != correspondingCharacter.Tid)
{
slot.Tid = correspondingCharacter.Tid;
}
}
}
}
}
// Save the updated data
JsonDb.Save();
}
else if (input == "sickpulls") else if (input == "sickpulls")
{ {
if (selectedUser == 0) if (selectedUser == 0)
{ {
Console.WriteLine("No user selected"); Console.WriteLine("No user selected");
} }
else else
{ {
var user = JsonDb.Instance.Users.FirstOrDefault(x => x.ID == selectedUser); var user = JsonDb.Instance.Users.FirstOrDefault(x => x.ID == selectedUser);
if (user == null) if (user == null)
{ {
Console.WriteLine("Selected user does not exist"); Console.WriteLine("Selected user does not exist");
selectedUser = 0; selectedUser = 0;
prompt = "# "; prompt = "# ";
} }
else else
{ {
// Check current value of sickpulls and toggle it // Check current value of sickpulls and toggle it
bool currentSickPulls = EpinelPS.Database.JsonDb.IsSickPulls(user); bool currentSickPulls = EpinelPS.Database.JsonDb.IsSickPulls(user);
if (currentSickPulls) if (currentSickPulls)
{ {
user.sickpulls = false; user.sickpulls = false;
Console.WriteLine("sickpulls is now set to false for user " + user.Username); Console.WriteLine("sickpulls is now set to false for user " + user.Username);
} }
else else
{ {
user.sickpulls = true; user.sickpulls = true;
Console.WriteLine("sickpulls is now set to true for user " + user.Username); Console.WriteLine("sickpulls is now set to true for user " + user.Username);
} }
// Save the changes to the database // Save the changes to the database
JsonDb.Save(); JsonDb.Save();
} }
} }
} }
else if (input.StartsWith("SetLevel")) else if (input.StartsWith("SetLevel"))
{ {
if (selectedUser == 0) if (selectedUser == 0)
{ {
Console.WriteLine("No user selected"); Console.WriteLine("No user selected");
} }
else else
{ {
var user = JsonDb.Instance.Users.FirstOrDefault(x => x.ID == selectedUser); var user = JsonDb.Instance.Users.FirstOrDefault(x => x.ID == selectedUser);
if (user == null) if (user == null)
{ {
Console.WriteLine("Selected user does not exist"); Console.WriteLine("Selected user does not exist");
selectedUser = 0; selectedUser = 0;
prompt = "# "; prompt = "# ";
} }
else if (args.Length == 2 && int.TryParse(args[1], out int level) && level >= 1 && level <= 999) else if (args.Length == 2 && int.TryParse(args[1], out int level) && level >= 1 && level <= 999)
{ {
foreach (var character in user.Characters) foreach (var character in user.Characters)
{ {
character.Level = level; character.Level = level;
} }
Console.WriteLine("Set all characters' level to " + level); Console.WriteLine("Set all characters' level to " + level);
JsonDb.Save(); JsonDb.Save();
} }
else else
{ {
Console.WriteLine("Invalid argument. Level must be between 1 and 999."); Console.WriteLine("Invalid argument. Level must be between 1 and 999.");
} }
} }
//code above WILL change levels in user.characters so this will update them in representation team //code above WILL change levels in user.characters so this will update them in representation team
foreach (var user in JsonDb.Instance.Users) foreach (var user in JsonDb.Instance.Users)
{ {
// Check if RepresentationTeamData exists and has slots // Check if RepresentationTeamData exists and has slots
if (user.RepresentationTeamData != null && user.RepresentationTeamData.Slots != null) if (user.RepresentationTeamData != null && user.RepresentationTeamData.Slots != null)
{ {
// Iterate through RepresentationTeamData slots // Iterate through RepresentationTeamData slots
foreach (var slot in user.RepresentationTeamData.Slots) foreach (var slot in user.RepresentationTeamData.Slots)
{ {
// Find the character in user's character list that matches the slot's Csn // Find the character in user's character list that matches the slot's Csn
var correspondingCharacter = user.Characters.FirstOrDefault(c => c.Csn == slot.Csn); var correspondingCharacter = user.Characters.FirstOrDefault(c => c.Csn == slot.Csn);
if (correspondingCharacter != null) if (correspondingCharacter != null)
{ {
// Update the Level value if it differs // Update the Level value if it differs
if (slot.Level != correspondingCharacter.Level) if (slot.Level != correspondingCharacter.Level)
{ {
slot.Level = correspondingCharacter.Level; slot.Level = correspondingCharacter.Level;
} }
} }
} }
} }
} }
// Save the updated data // Save the updated data
JsonDb.Save(); JsonDb.Save();
} }
else if (input.StartsWith("SetSkillLevel")) else if (input.StartsWith("SetSkillLevel"))
{ {
if (selectedUser == 0) if (selectedUser == 0)
{ {
Console.WriteLine("No user selected"); Console.WriteLine("No user selected");
} }
else else
{ {
var user = JsonDb.Instance.Users.FirstOrDefault(x => x.ID == selectedUser); var user = JsonDb.Instance.Users.FirstOrDefault(x => x.ID == selectedUser);
if (user == null) if (user == null)
{ {
Console.WriteLine("Selected user does not exist"); Console.WriteLine("Selected user does not exist");
selectedUser = 0; selectedUser = 0;
prompt = "# "; prompt = "# ";
} }
else if (args.Length == 2 && int.TryParse(args[1], out int skillLevel) && skillLevel >= 1 && skillLevel <= 10) else if (args.Length == 2 && int.TryParse(args[1], out int skillLevel) && skillLevel >= 1 && skillLevel <= 10)
{ {
foreach (var character in user.Characters) foreach (var character in user.Characters)
{ {
character.UltimateLevel = skillLevel; character.UltimateLevel = skillLevel;
character.Skill1Lvl = skillLevel; character.Skill1Lvl = skillLevel;
character.Skill2Lvl = skillLevel; character.Skill2Lvl = skillLevel;
} }
Console.WriteLine("Set all characters' skill levels to " + skillLevel); Console.WriteLine("Set all characters' skill levels to " + skillLevel);
JsonDb.Save(); JsonDb.Save();
} }
else else
{ {
Console.WriteLine("Invalid argument. Skill level must be between 1 and 10."); Console.WriteLine("Invalid argument. Skill level must be between 1 and 10.");
} }
} }
} }
else if (input.StartsWith("rmuser")) else if (input.StartsWith("rmuser"))
{ {
@@ -591,108 +673,218 @@ namespace EpinelPS
} }
} }
} }
else if (input.StartsWith("completestage")) else if (input.StartsWith("completestage"))
{ {
if (selectedUser == 0) if (selectedUser == 0)
{ {
Console.WriteLine("No user selected"); Console.WriteLine("No user selected");
} }
else else
{ {
var user = JsonDb.Instance.Users.FirstOrDefault(x => x.ID == selectedUser); var user = JsonDb.Instance.Users.FirstOrDefault(x => x.ID == selectedUser);
if (user == null) if (user == null)
{ {
Console.WriteLine("Selected user does not exist"); Console.WriteLine("Selected user does not exist");
selectedUser = 0; selectedUser = 0;
prompt = "# "; prompt = "# ";
} }
else else
{ {
if (args.Length == 2) if (args.Length == 2)
{ {
var input2 = args[1]; var input2 = args[1];
try try
{ {
var chapterParsed = int.TryParse(input2.Split('-')[0], out int chapterNumber); var chapterParsed = int.TryParse(input2.Split('-')[0], out int chapterNumber);
var stageParsed = int.TryParse(input2.Split('-')[1], out int stageNumber); var stageParsed = int.TryParse(input2.Split('-')[1], out int stageNumber);
if (chapterParsed && stageParsed) if (chapterParsed && stageParsed)
{ {
Console.WriteLine($"Chapter number: {chapterNumber}, Stage number: {stageNumber}"); Console.WriteLine($"Chapter number: {chapterNumber}, Stage number: {stageNumber}");
// Complete main stages // Complete main stages
for (int i = 0; i <= chapterNumber; i++) for (int i = 0; i <= chapterNumber; i++)
{ {
var stages = GameData.Instance.GetStageIdsForChapter(i, true); var stages = GameData.Instance.GetStageIdsForChapter(i, true);
int target = 1; int target = 1;
foreach (var item in stages) foreach (var item in stages)
{ {
if (!user.IsStageCompleted(item, true)) if (!user.IsStageCompleted(item, true))
{ {
Console.WriteLine("Completing stage " + item); Console.WriteLine("Completing stage " + item);
ClearStage.CompleteStage(user, item, true); ClearStage.CompleteStage(user, item, true);
} }
if (i == chapterNumber && target == stageNumber) if (i == chapterNumber && target == stageNumber)
{ {
break; break;
} }
target++; target++;
} }
} }
// Process scenario and regular stages // Process scenario and regular stages
Console.WriteLine($"Processing stages for chapters 0 to {chapterNumber}"); Console.WriteLine($"Processing stages for chapters 0 to {chapterNumber}");
for (int chapter = 0; chapter <= chapterNumber; chapter++) for (int chapter = 0; chapter <= chapterNumber; chapter++)
{ {
Console.WriteLine($"Processing chapter: {chapter}"); Console.WriteLine($"Processing chapter: {chapter}");
var stages = GameData.Instance.GetScenarioStageIdsForChapter(chapter) var stages = GameData.Instance.GetScenarioStageIdsForChapter(chapter)
.Where(stageId => GameData.Instance.IsValidScenarioStage(stageId, chapterNumber, stageNumber)) .Where(stageId => GameData.Instance.IsValidScenarioStage(stageId, chapterNumber, stageNumber))
.ToList(); .ToList();
Console.WriteLine($"Found {stages.Count} stages for chapter {chapter}"); Console.WriteLine($"Found {stages.Count} stages for chapter {chapter}");
foreach (var stage in stages) foreach (var stage in stages)
{ {
if (!user.CompletedScenarios.Contains(stage)) if (!user.CompletedScenarios.Contains(stage))
{ {
user.CompletedScenarios.Add(stage); user.CompletedScenarios.Add(stage);
Console.WriteLine($"Added stage {stage} to CompletedScenarios"); Console.WriteLine($"Added stage {stage} to CompletedScenarios");
} }
else else
{ {
Console.WriteLine($"Stage {stage} is already completed"); Console.WriteLine($"Stage {stage} is already completed");
} }
} }
} }
// Save changes to user data // Save changes to user data
JsonDb.Save(); JsonDb.Save();
} }
else else
{ {
Console.WriteLine("Chapter and stage number must be valid integers"); Console.WriteLine("Chapter and stage number must be valid integers");
} }
} }
catch (Exception ex) catch (Exception ex)
{ {
Console.WriteLine("Exception: " + ex.ToString()); Console.WriteLine("Exception: " + ex.ToString());
} }
} }
else else
{ {
Console.WriteLine("Invalid argument length, must be 1"); Console.WriteLine("Invalid argument length, must be 1");
} }
} }
} }
} }
else if (input.StartsWith("AddItem"))
{
if (selectedUser == 0)
{
Console.WriteLine("No user selected");
}
else
{
var user = JsonDb.Instance.Users.FirstOrDefault(x => x.ID == selectedUser);
if (user == null)
{
Console.WriteLine("Selected user does not exist");
selectedUser = 0;
prompt = "# ";
}
else
{
if (args.Length == 3)
{
if (int.TryParse(args[1], out int itemId) && int.TryParse(args[2], out int amount))
{
ItemData? item = user.Items.FirstOrDefault(i => i.ItemType == itemId);
if (item == null)
{
user.Items.Add(new ItemData
{
Isn = user.GenerateUniqueItemId(),
ItemType = itemId,
Level = 1,
Exp = 1,
Count = amount
});
}
else
{
item.Count += amount;
if (item.Count < 0)
{
item.Count = 0;
}
}
Console.WriteLine($"Added {amount} of item {itemId} to user {user.Username}");
JsonDb.Save();
}
else
{
Console.WriteLine("Invalid item ID or amount");
}
}
else
{
Console.WriteLine("Invalid argument length, must be 2");
}
}
}
}
else if (input.StartsWith("AddCharacter"))
{
if (selectedUser == 0)
{
Console.WriteLine("No user selected");
}
else
{
var user = JsonDb.Instance.Users.FirstOrDefault(x => x.ID == selectedUser);
if (user == null)
{
Console.WriteLine("Selected user does not exist");
selectedUser = 0;
prompt = "# ";
}
else
{
if (args.Length == 2)
{
if (int.TryParse(args[1], out int characterId))
{
if (!user.HasCharacter(characterId))
{
user.Characters.Add(new Database.Character()
{
CostumeId = 0,
Csn = user.GenerateUniqueCharacterId(),
Grade = 0,
Level = 1,
Skill1Lvl = 1,
Skill2Lvl = 1,
Tid = characterId,
UltimateLevel = 1
});
Console.WriteLine($"Added character {characterId} to user {user.Username}");
JsonDb.Save();
}
else
{
Console.WriteLine($"User {user.Username} already has character {characterId}");
}
}
else
{
Console.WriteLine("Invalid character ID");
}
}
else
{
Console.WriteLine("Invalid argument length, must be 1");
}
}
}
}
else if (input == "exit") else if (input == "exit")
{ {
Environment.Exit(0); Environment.Exit(0);

View File

@@ -0,0 +1,32 @@
using EpinelPS.StaticInfo;
namespace EpinelPS.Utils
{
public class FormulaUtils
{
public static int CalculateCP(Database.User user, long csn)
{
var character = user.Characters.FirstOrDefault(c => c.Csn == csn);
var charRecord = GameData.Instance.characterTable.Values.FirstOrDefault(c => c.id == character.Tid);
var statRecord = GameData.Instance.characterStatTable.Values.FirstOrDefault(s => charRecord.stat_enhance_id == s.group + (character.Level - 1));
float coreMult = 1f + character.Grade * 0.02f;
float hp = statRecord.level_hp * coreMult;
float atk = statRecord.level_attack * coreMult;
float def = statRecord.level_defence * coreMult;
float critRate = charRecord.critical_ratio;
float critDamage = charRecord.critical_damage;
float skill1Level = character.Skill1Lvl;
float skill2Level = character.Skill2Lvl;
float ultSkillLevel = character.UltimateLevel;
float critResult = 1 + ((critRate / 10000f) * (critDamage / 10000f - 1));
float effHealthResult = (def * 100) + hp;
float skillResult = (skill1Level * 0.01f) + (skill2Level * 0.01f) + (ultSkillLevel * 0.02f);
float bondResult = 0f; // TODO
float equipResult = 0f; // TOD
float overloadResult = 0; // TODO
float finalResult = (((critResult * atk * 18) + (effHealthResult * 0.7f)) * (1.3f + skillResult) + bondResult + equipResult + overloadResult) / 100f;
return (int)Math.Round(finalResult);
}
}
}

View File

@@ -45,7 +45,7 @@ To skip stages, a basic command line interface is implemented.
- [X] Outpost Rewards - [X] Outpost Rewards
- [ ] Admin Panel - [ ] Admin Panel
- [ ] Simulation Room - [ ] Simulation Room
- [ ] Skill level up - [X] Skill level up
- [ ] Outpost jukebox - [ ] Outpost jukebox
- [ ] Event system - [ ] Event system
- [ ] Download all game assets ahead of time - [ ] Download all game assets ahead of time
@@ -57,7 +57,6 @@ To skip stages, a basic command line interface is implemented.
## What is not working: ## What is not working:
- Events - Events
- Skill upgrade
- Mission reward, daily/weekly missions - Mission reward, daily/weekly missions
- Side quests - Side quests
- Lots of things in the outpost - Lots of things in the outpost