Compare commits

...

7 Commits

Author SHA1 Message Date
Kei-Luna
7c41d84a08 Update LICENSE 2026-06-05 06:33:48 +09:00
Kei-Luna
1c9eb7ef14 License 2026-06-05 06:30:16 +09:00
Kei-Luna
3c045a79a9 Update version.txt 2026-05-31 05:42:02 +09:00
Kei-Luna
ecf2446598 Weapon_OneKeyToMax 2026-05-31 05:17:08 +09:00
Kei-Luna
75d840974b Item_Recycle 2026-05-31 05:02:04 +09:00
Kei-Luna
d6f57053dd Update README_jp.md 2026-05-30 17:52:15 +09:00
Kei-Luna
518c04fdb4 Fixed an issue where clicking on a feature not yet implemented on the server side would cause an infinite loading loop. 2026-05-27 07:12:31 +09:00
10 changed files with 587 additions and 32 deletions

View File

@@ -6,6 +6,7 @@ namespace MikuSB.Data.Excel;
public class RecycleExcel : ExcelResource
{
public int ID { get; set; }
public JToken? RecycleReward { get; set; }
public JToken? RecycleBase { get; set; }
public JToken? RecycleRatio { get; set; }

View File

@@ -13,6 +13,7 @@ public class SupportCardExcel : ExcelResource
public uint Icon { get; set; }
public uint ProvideExp { get; set; }
public uint Color { get; set; }
[JsonProperty("RecycleID")] public int RecycleID { get; set; }
[JsonProperty("LevelLimitID")] public int LevelLimitId { get; set; }
[JsonProperty("AffixPool")] public List<int> AffixPool { get; set; } = [];
[JsonProperty("AffixCost")] public JToken? AffixCostRaw { get; set; }

View File

@@ -8,6 +8,7 @@ public static class CallGSRouter
{
private static readonly Logger Logger = new("CallGS");
private static readonly Dictionary<string, ICallGSHandler> Handlers = [];
private const string UnavailableTipKey = "ui.TxtNotOpen";
public static void Init()
{
@@ -32,11 +33,13 @@ public static class CallGSRouter
catch (Exception e)
{
Logger.Error($"[{req.Api}] {e.Message}", e);
await SendUnavailableResponse(connection, req.Api);
}
return;
}
Logger.Error($"No handler for CallGS API: {req.Api}");
await SendUnavailableResponse(connection, req.Api);
}
public static async Task SendScript(Connection connection, string api, string arg, NtfSyncPlayer extra = null!)
@@ -44,4 +47,11 @@ public static class CallGSRouter
var rsp = new NtfCallScript { Api = api, Arg = arg, ExtraSync = extra };
await connection.SendPacket(CmdIds.NtfScript, rsp);
}
private static Task SendUnavailableResponse(Connection connection, string api)
{
// Many client Lua handlers treat sErr/sError as a recoverable failure path,
// which is preferable to leaving the request hanging forever.
return SendScript(connection, api, $$"""{"sErr":"{{UnavailableTipKey}}","sError":"{{UnavailableTipKey}}"}""");
}
}

View File

@@ -0,0 +1,316 @@
using MikuSB.Data;
using MikuSB.Data.Excel;
using MikuSB.Database;
using MikuSB.Database.Inventory;
using MikuSB.Enums.Item;
using MikuSB.Proto;
using Newtonsoft.Json.Linq;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace MikuSB.GameServer.Server.CallGS.Handlers.Inventory;
[CallGSApi("Item_Recycle")]
public class Item_Recycle : ICallGSHandler
{
public async Task Handle(Connection connection, string param, ushort seqNo)
{
var player = connection.Player!;
var req = JsonSerializer.Deserialize<ItemRecycleParam>(param);
if (req?.TbItems == null || req.TbItems.Count == 0)
{
await CallGSRouter.SendScript(connection, "Item_Recycle", "{\"sErr\":\"error.BadParam\"}");
return;
}
var config = RecycleConfig.Load();
var itemsToRecycle = new List<(BaseGameItemInfo Item, int RecycleId)>();
foreach (var uniqueId in req.TbItems)
{
BaseGameItemInfo? item = player.InventoryManager.GetWeaponItem((uint)uniqueId)
?? (BaseGameItemInfo?)player.InventoryManager.GetSupportCardItem((uint)uniqueId);
if (item == null)
{
await CallGSRouter.SendScript(connection, "Item_Recycle", "{\"sErr\":\"error.Recycle.ItemNotExists\"}");
return;
}
var recycleId = GetRecycleId(item);
if (recycleId <= 0 || !config.HasConfig(recycleId))
{
await CallGSRouter.SendScript(connection, "Item_Recycle", "{\"sErr\":\"error.Recycle.ItemCanNotRecycle\"}");
return;
}
itemsToRecycle.Add((item, recycleId));
}
var sync = new NtfSyncPlayer();
foreach (var (item, recycleId) in itemsToRecycle)
{
var rewards = config.CalcRewards(item, recycleId);
foreach (var reward in rewards)
await GrantRewardAsync(player, sync, reward);
RemoveItem(player.InventoryManager.InventoryData, item, sync);
}
DatabaseHelper.SaveDatabaseType(player.InventoryManager.InventoryData);
await CallGSRouter.SendScript(connection, "Item_Recycle", "{}", sync);
}
private static int GetRecycleId(BaseGameItemInfo item)
{
if (item.ItemType == ItemTypeEnum.TYPE_WEAPON)
{
var t = GameData.WeaponData.Values.FirstOrDefault(x =>
GameResourceTemplateId.FromGdpl(x.Genre, x.Detail, x.Particular, x.Level) == item.TemplateId);
return t?.RecycleID ?? 0;
}
if (item.ItemType == ItemTypeEnum.TYPE_SUPPORT)
{
var t = GameData.SupportCardData.FirstOrDefault(x => x.TemplateId == item.TemplateId);
return t?.RecycleID ?? 0;
}
return 0;
}
private static void RemoveItem(InventoryData inventory, BaseGameItemInfo item, NtfSyncPlayer sync)
{
var removed = item.ToProto();
removed.Count = 0;
sync.Items.Add(removed);
if (item.ItemType == ItemTypeEnum.TYPE_WEAPON)
inventory.Weapons.Remove(item.UniqueId);
else
inventory.SupportCards.Remove(item.UniqueId);
}
private static async Task GrantRewardAsync(GameServer.Game.Player.PlayerInstance player, NtfSyncPlayer sync, IReadOnlyList<uint> reward)
{
if (reward.Count < 5) return;
var itemType = (ItemTypeEnum)reward[0];
var detail = reward[1];
var particular = reward[2];
var level = reward[3];
var count = Math.Max(1u, reward[4]);
switch (itemType)
{
case ItemTypeEnum.TYPE_SUPPLIES:
{
var templateId = (uint)GameResourceTemplateId.FromGdpl(reward[0], detail, particular, level);
if (!GameData.SuppliesData.TryGetValue(templateId, out var supplies)) break;
var item = await player.InventoryManager.AddSuppliesItem(supplies, count, sendPacket: false);
if (item != null) sync.Items.Add(item.ToProto());
break;
}
}
}
}
internal sealed class RecycleConfig
{
private readonly Dictionary<int, RecycleEntry> _entries;
private readonly List<SupplyTemplate> _weaponSupplies;
private readonly List<SupplyTemplate> _supportSupplies;
private readonly Dictionary<int, ulong> _weaponLevelExp;
private readonly Dictionary<int, ulong> _supportLevelExp;
private readonly Dictionary<int, ulong> _weaponLevelExpSsr;
private readonly Dictionary<int, ulong> _supportLevelExpSsr;
private RecycleConfig(
Dictionary<int, RecycleEntry> entries,
List<SupplyTemplate> weaponSupplies,
List<SupplyTemplate> supportSupplies,
Dictionary<int, ulong> weaponLevelExp,
Dictionary<int, ulong> weaponLevelExpSsr,
Dictionary<int, ulong> supportLevelExp,
Dictionary<int, ulong> supportLevelExpSsr)
{
_entries = entries;
_weaponSupplies = weaponSupplies;
_supportSupplies = supportSupplies;
_weaponLevelExp = weaponLevelExp;
_weaponLevelExpSsr = weaponLevelExpSsr;
_supportLevelExp = supportLevelExp;
_supportLevelExpSsr = supportLevelExpSsr;
}
public static RecycleConfig Load()
{
var entries = new Dictionary<int, RecycleEntry>();
foreach (var row in GameData.RecycleData.Values)
{
var fixedRewards = ParseRewards(row.RecycleReward);
var recycleBase = GetUInt(row.RecycleBase);
var recycleRatio = GetDecimal(row.RecycleRatio);
entries[row.ID] = new RecycleEntry(fixedRewards, recycleBase, recycleRatio);
}
var weaponSupplies = new List<SupplyTemplate>();
var supportSupplies = new List<SupplyTemplate>();
foreach (var s in GameData.AllSuppliesData)
{
if (s.ProvideExp == 0) continue;
if (s.Genre == 5 && s.Detail == 2)
weaponSupplies.Add(new SupplyTemplate(s.Genre, s.Detail, s.Particular, s.Level, s.ProvideExp));
else if (s.Genre == 5 && s.Detail == 3)
supportSupplies.Add(new SupplyTemplate(s.Genre, s.Detail, s.Particular, s.Level, s.ProvideExp));
}
weaponSupplies.Sort((a, b) => b.ProvideExp.CompareTo(a.ProvideExp));
supportSupplies.Sort((a, b) => b.ProvideExp.CompareTo(a.ProvideExp));
var weaponLevelExp = BuildLevelExpTable(GameData.UpgradeExpData.Values.Select(x => (x.Lv, x.WeaponNeedExp)));
var weaponLevelExpSsr = BuildLevelExpTable(GameData.UpgradeExpData.Values.Select(x => (x.Lv, x.SSRWeaponNeedExp)));
var supportLevelExp = BuildLevelExpTable(GameData.UpgradeExpData.Values.Select(x => (x.Lv, x.SusNeedExp)));
var supportLevelExpSsr = BuildLevelExpTable(GameData.UpgradeExpData.Values.Select(x => (x.Lv, x.SSRSusNeedExp)));
return new RecycleConfig(entries, weaponSupplies, supportSupplies, weaponLevelExp, weaponLevelExpSsr, supportLevelExp, supportLevelExpSsr);
}
public bool HasConfig(int recycleId) => _entries.ContainsKey(recycleId);
public List<IReadOnlyList<uint>> CalcRewards(BaseGameItemInfo item, int recycleId)
{
if (!_entries.TryGetValue(recycleId, out var entry))
return [];
var rewards = new List<IReadOnlyList<uint>>(entry.FixedRewards);
var expRewards = CalcExpRewards(item, entry);
rewards.AddRange(expRewards);
return rewards;
}
private List<IReadOnlyList<uint>> CalcExpRewards(BaseGameItemInfo item, RecycleEntry entry)
{
if (entry.RecycleRatio == 0) return [];
List<SupplyTemplate> supplies;
Dictionary<int, ulong> levelExp;
if (item.ItemType == ItemTypeEnum.TYPE_WEAPON)
{
supplies = _weaponSupplies;
var color = GetItemColor(item);
levelExp = color == 5 ? _weaponLevelExpSsr : _weaponLevelExp;
}
else if (item.ItemType == ItemTypeEnum.TYPE_SUPPORT)
{
supplies = _supportSupplies;
var color = GetItemColor(item);
levelExp = color == 5 ? _supportLevelExpSsr : _supportLevelExp;
}
else
{
return [];
}
var baseExp = (ulong)entry.RecycleBase;
var levelAccum = levelExp.GetValueOrDefault((int)item.Level);
var totalExp = (ulong)Math.Floor((baseExp + levelAccum + item.Exp) * (double)entry.RecycleRatio);
if (totalExp == 0 || supplies.Count == 0) return [];
var rewards = new List<IReadOnlyList<uint>>();
var remaining = totalExp;
foreach (var supply in supplies)
{
if (remaining == 0) break;
var count = remaining / supply.ProvideExp;
if (count == 0) continue;
remaining -= count * supply.ProvideExp;
rewards.Add([supply.Genre, supply.Detail, supply.Particular, supply.Level, (uint)Math.Min(count, 99999)]);
}
return rewards;
}
private static List<IReadOnlyList<uint>> ParseRewards(JToken? token)
{
if (token == null) return [];
if (token is JArray outerArray)
{
var rewards = new List<IReadOnlyList<uint>>();
foreach (var element in outerArray)
{
if (element is JArray inner && inner.Count >= 4)
{
var reward = inner.Select(x => x.Value<uint>()).ToArray();
if (reward.Length < 5)
reward = [.. reward, 1];
rewards.Add(reward);
}
}
return rewards;
}
return [];
}
private static int GetItemColor(BaseGameItemInfo item)
{
if (item.ItemType == ItemTypeEnum.TYPE_WEAPON)
{
var t = GameData.WeaponData.Values.FirstOrDefault(x =>
GameResourceTemplateId.FromGdpl(x.Genre, x.Detail, x.Particular, x.Level) == item.TemplateId);
return t?.Color ?? 0;
}
if (item.ItemType == ItemTypeEnum.TYPE_SUPPORT)
{
var t = GameData.SupportCardData.FirstOrDefault(x => x.TemplateId == item.TemplateId);
return (int)(t?.Color ?? 0);
}
return 0;
}
private static Dictionary<int, ulong> BuildLevelExpTable(IEnumerable<(int Lv, uint NeedExp)> source)
{
var table = new Dictionary<int, ulong>();
ulong accumulated = 0;
foreach (var (lv, needExp) in source.OrderBy(x => x.Lv))
{
table[lv] = accumulated;
accumulated += needExp;
}
return table;
}
private static uint GetUInt(JToken? token) => token?.Type switch
{
JTokenType.Integer => token.Value<uint>(),
JTokenType.Float => (uint)Math.Max(0, token.Value<decimal>()),
JTokenType.String when uint.TryParse(token.Value<string>(), out var r) => r,
_ => 0
};
private static decimal GetDecimal(JToken? token) => token?.Type switch
{
JTokenType.Integer => token.Value<decimal>(),
JTokenType.Float => token.Value<decimal>(),
JTokenType.String when decimal.TryParse(token.Value<string>(), out var r) => r,
_ => 0m
};
}
internal readonly record struct RecycleEntry(
List<IReadOnlyList<uint>> FixedRewards,
uint RecycleBase,
decimal RecycleRatio);
internal readonly record struct SupplyTemplate(uint Genre, uint Detail, uint Particular, uint Level, uint ProvideExp);
internal sealed class ItemRecycleParam
{
[JsonPropertyName("tbItems")]
public List<int> TbItems { get; set; } = [];
}

View File

@@ -0,0 +1,183 @@
using MikuSB.Data;
using MikuSB.Database;
using MikuSB.Database.Inventory;
using MikuSB.Proto;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace MikuSB.GameServer.Server.CallGS.Handlers.Weapon;
[CallGSApi("Weapon_OneKeyToMax")]
public class Weapon_OneKeyToMax : ICallGSHandler
{
private const uint MaxBreak = 6;
public async Task Handle(Connection connection, string param, ushort seqNo)
{
var player = connection.Player!;
var req = JsonSerializer.Deserialize<OneKeyToMaxParam>(param);
if (req == null || req.Id <= 0 || req.TbBreakUpgradeMat == null || req.TbBreakUpgradeMat.Count == 0)
{
await CallGSRouter.SendScript(connection, "Weapon_OneKeyToMax", "{\"sErr\":\"error.BadParam\"}");
return;
}
var weapon = player.InventoryManager.GetWeaponItem((uint)req.Id);
if (weapon == null)
{
await CallGSRouter.SendScript(connection, "Weapon_OneKeyToMax", "{\"sErr\":\"error.BadParam\"}");
return;
}
var config = WeaponUpgradeConfig.Load();
if (!config.TryGetWeaponTemplate(weapon.TemplateId, out var targetTemplate))
{
await CallGSRouter.SendScript(connection, "Weapon_OneKeyToMax", "{\"sErr\":\"error.BadParam\"}");
return;
}
var inventory = player.InventoryManager.InventoryData;
var equippedWeaponIds = player.CharacterManager.CharacterData.Characters
.Select(x => x.WeaponUniqueId)
.Where(x => x != 0)
.ToHashSet();
// Validate all materials upfront before making any changes
foreach (var stage in req.TbBreakUpgradeMat)
{
if (stage == null || stage.Count < 3) continue;
var matList = stage[1].Deserialize<List<List<int>>>();
if (matList == null) continue;
foreach (var entry in matList)
{
if (entry == null || entry.Count < 2) continue;
var itemId = (uint)Math.Max(0, entry[0]);
var count = (uint)Math.Max(0, entry[1]);
if (itemId == 0 || count == 0) continue;
if (itemId == weapon.UniqueId)
{
await CallGSRouter.SendScript(connection, "Weapon_OneKeyToMax", "{\"sErr\":\"tip.material_not_enough\"}");
return;
}
var material = FindInventoryItem(inventory, itemId);
if (material == null || material.ItemCount < count)
{
await CallGSRouter.SendScript(connection, "Weapon_OneKeyToMax", "{\"sErr\":\"tip.material_not_enough\"}");
return;
}
if (material is GameWeaponInfo materialWeapon &&
(materialWeapon.EquipAvatarId != 0 || equippedWeaponIds.Contains(materialWeapon.UniqueId)))
{
await CallGSRouter.SendScript(connection, "Weapon_OneKeyToMax", "{\"sErr\":\"tip.material_not_enough\"}");
return;
}
}
}
var syncItems = new List<Item>();
var weaponLevel = weapon.Level == 0 ? 1u : weapon.Level;
weapon.Level = weaponLevel;
// Process each break stage
foreach (var stage in req.TbBreakUpgradeMat)
{
if (stage == null || stage.Count < 3) continue;
var matList = stage[1].Deserialize<List<List<int>>>();
var doBreak = stage[2].GetInt32() == 1;
if (matList != null && matList.Count > 0)
{
// Aggregate materials, skip duplicates
var materialsUsed = new Dictionary<uint, uint>();
foreach (var entry in matList)
{
if (entry == null || entry.Count < 2) continue;
var itemId = (uint)Math.Max(0, entry[0]);
var count = (uint)Math.Max(0, entry[1]);
if (itemId == 0 || count == 0) continue;
materialsUsed[itemId] = materialsUsed.GetValueOrDefault(itemId) + count;
}
ulong totalExp = 0;
foreach (var (itemId, count) in materialsUsed)
{
var material = FindInventoryItem(inventory, itemId)!;
if (config.TryGetMaterialGain(material, out var gainExp))
totalExp += gainExp * count;
}
// Consume materials
foreach (var (itemId, count) in materialsUsed)
{
var material = FindInventoryItem(inventory, itemId)!;
material.ItemCount -= count;
if (material.ItemCount == 0)
{
RemoveInventoryItem(inventory, itemId);
var proto = material.ToProto();
proto.Count = 0;
syncItems.Add(proto);
}
else
{
syncItems.Add(material.ToProto());
}
}
// Apply exp to weapon
if (totalExp > 0)
{
var maxLevel = config.GetWeaponMaxLevel(targetTemplate.BreakLimitId, weapon.Break);
var (newLevel, newExp) = config.ApplyWeaponExp(weapon.Level, weapon.Exp, totalExp, targetTemplate.Color, maxLevel);
weapon.Level = newLevel;
weapon.Exp = newExp;
}
}
// Perform break
if (doBreak && weapon.Break < MaxBreak)
weapon.Break++;
}
syncItems.Add(weapon.ToProto());
DatabaseHelper.SaveDatabaseType(inventory);
var finalMaxLevel = config.GetWeaponMaxLevel(targetTemplate.BreakLimitId, weapon.Break);
var bMaxUnlock = finalMaxLevel > 0 && weapon.Level >= finalMaxLevel;
var sync = new NtfSyncPlayer();
sync.Items.AddRange(syncItems);
await CallGSRouter.SendScript(connection, "Weapon_OneKeyToMax",
$"{{\"bMaxUnLock\":{(bMaxUnlock ? "true" : "false")}}}", sync);
}
private static BaseGameItemInfo? FindInventoryItem(InventoryData inventory, uint itemId)
{
if (inventory.Weapons.TryGetValue(itemId, out var weapon)) return weapon;
if (inventory.Skins.TryGetValue(itemId, out var skin)) return skin;
if (inventory.Items.TryGetValue(itemId, out var item)) return item;
return null;
}
private static void RemoveInventoryItem(InventoryData inventory, uint itemId)
{
inventory.Weapons.Remove(itemId);
inventory.Skins.Remove(itemId);
inventory.Items.Remove(itemId);
}
}
internal sealed class OneKeyToMaxParam
{
[JsonPropertyName("Id")]
public int Id { get; set; }
[JsonPropertyName("tbBreakUpgradeMat")]
public List<List<System.Text.Json.JsonElement>> TbBreakUpgradeMat { get; set; } = [];
}

View File

@@ -258,9 +258,9 @@ internal sealed class WeaponUpgradeConfig
var weaponTemplates = GameData.WeaponData.Values.ToDictionary(
x => GameResourceTemplateId.FromGdpl(x.Genre, x.Detail, x.Particular, x.Level),
x => new MaterialTemplate(x.Color, x.ProvideExp, x.ConsumeGold, x.RecycleID, x.BreakLimitID));
var suppliesTemplates = GameData.SuppliesData.Values.ToDictionary(
x => GameResourceTemplateId.FromGdpl(x.Genre, x.Detail, x.Particular, x.Level),
x => new MaterialTemplate(x.Color, x.ProvideExp, x.ConsumeGold, 0, 0));
var suppliesTemplates = GameData.AllSuppliesData
.GroupBy(x => GameResourceTemplateId.FromGdpl(x.Genre, x.Detail, x.Particular, x.Level))
.ToDictionary(g => g.Key, g => new MaterialTemplate(g.First().Color, g.First().ProvideExp, g.First().ConsumeGold, 0, 0));
return new WeaponUpgradeConfig(normalExp, ssrExp, breakLimits, recycleById, weaponTemplates, suppliesTemplates);
}
@@ -271,6 +271,24 @@ internal sealed class WeaponUpgradeConfig
public bool TryGetSuppliesTemplate(ulong templateId, out MaterialTemplate template) =>
_suppliesTemplates.TryGetValue(templateId, out template!);
public bool TryGetMaterialGain(BaseGameItemInfo item, out ulong exp)
{
exp = 0;
if (TryGetWeaponTemplate(item.TemplateId, out var weaponTemplate))
{
exp = weaponTemplate.ProvideExp;
if (item is GameWeaponInfo weapon && weapon.Level > 1)
exp += GetWeaponRecycleExp(weaponTemplate, weapon.Level);
return true;
}
if (TryGetSuppliesTemplate(item.TemplateId, out var suppliesTemplate))
{
exp = suppliesTemplate.ProvideExp;
return true;
}
return false;
}
public ulong GetWeaponRecycleExp(MaterialTemplate template, uint level)
{
if (template.RecycleId <= 0 || !_recycleById.TryGetValue(template.RecycleId, out var recycle))

21
LICENSE Normal file
View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2026 Kei-Luna, Naruse and contributors
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -87,6 +87,9 @@ dotnet build
- [Naruse](https://github.com/DevilProMT)
- [Kei-Luna](https://github.com/Kei-Luna)
## License
This repository is licensed under the [MIT License](LICENSE).
## Notes on use
This software is intended for research and testing purposes in a local environment.
It is not intended for unauthorized access to, interference with, or commercial use of official services.

View File

@@ -53,40 +53,42 @@ dotnet build
## 機能一覧
* [x] ログインと基本的なアカウント入場
* [x] プレイヤーデータの読み込み
* [x] 所持品の読み込み
* [x] キャラクターの読み込み
* [x] スキンの読み込み
* [x] 武器の読み込み
* [x] ロビー表示キャラクターの変更
* [x] キャラクタースキンの変更
* [x] キャラクタースキン形態の変更
* [x] 武器の付け替え
* [x] 武器の強化
* [x] プレイヤー名の変更
* [x] 現在対応済みロビー状態の基本保存
* [✓] メイン章のステージ入場と関連フロ
* [✓] デイリーのステージ入場と関連フロー
* [✓] 基本的なプレイヤー設定同期
* [✓] 基本的なプロフィール同期
* [✓] イベント関連リクエスト
* [✓] 実績関連リクエスト
* [✓] 編成関連リクエスト
* [✓] プレビュー関連リクエスト
* [✓] 一部のショップ関連リクエスト
* [ ] 完全な戦闘フロー
* [ ] ミッション / クエスト進行
* [ ] ガチャ / 募集システム
* [ ] 完全なショップ挙動
* [x] ログインシステム
* [x] インベントリ
* [x] 戦闘
* [x] キャラクター
* [x] GMメニュー
* [x] 武器
* [x] 後方支援
* [x] アポカリプス
* [x] アウクトゥス
* [x] 神格神経
* [x] キャラスキン
* [x] 武器スキン
* [x] ガチャ
* [x] メインストーリーチャプタ
* [x] 社員寮
* [x] ミニゲーム
* [x] 地下清掃
* [x] ニューロンシュミレーション
* [x] 戦術評価
* [x] エピソードストーリー
* [x] ルーチン作戦
* [x] 逆説迷宮
* [x] ロビー画面の編集
* [✓] 惑星開拓
* [✓] 夢の絵本
* [ ] ショップ
* [ ] マルチプレイシステム
* [ ] 基地 / 宿舎システム
* [ ] クライアント API 全体の対応
* [ ] 名誉紛争
## 貢献者
- [Naruse](https://github.com/DevilProMT)
- [Kei-Luna](https://github.com/Kei-Luna)
## ライセンス
このリポジトリは [MIT License](LICENSE) の下で公開されています。
## 利用上の注意
本ソフトウェアはローカル環境での研究・検証用途を想定しています。

View File

@@ -1 +1 @@
v=4.4
v=4.6