feat: Implement missions 1-5 (combat fluidity, urban crowd locomotion, smart traffic signals, dynamic gacha pity, universal item spawner & armory)
This commit is contained in:
1 parent
5e333d7678
commit
ce1f471eab
27 files changed
+321970
-135
No files matched your search
@@ -1,97 +1,220 @@
|
||||
using Ananta.SDK.Network;
|
||||
using Ananta.SDK.Rpc;
|
||||
using Ananta.Server.ClientData.Client4229938;
|
||||
using Ananta.Server.Protocol.Client4229938;
|
||||
using Ananta.Server.RpcTypes.Client4229938.Auto;
|
||||
using Ananta.Server.RpcTypes.Client4229938.Methods.Game;
|
||||
using GameMethods = Ananta.Server.RpcTypes.Client4229938.Methods.Game;
|
||||
|
||||
namespace Ananta.Server.Handlers.Game;
|
||||
|
||||
/// <summary>
|
||||
/// Gacha draws for build 4229938 (GachaManager.DoDrawGacha -> AskDrawGacha(poolId, drawCount, isAutoExchange)).
|
||||
///
|
||||
/// Odds follow GachaPoolTierRuleConfig: RuleType 1 pools ramp the grand-prize chance from
|
||||
/// BaseProbability by ProbabilityIncrement per draw past RampStartDraw and hand out a guaranteed
|
||||
/// grand prize at PityThreshold; these pools also pick the prize row itself by relative weight within
|
||||
/// the "grand" or "ordinary" group. RuleType 0 pools have no pity ramp and simply pick by weight.
|
||||
/// Duplicate grand prizes are converted to the row's duplicateReturn drop, which is also how the
|
||||
/// client labels the result (IsConverted).
|
||||
/// </summary>
|
||||
internal sealed partial class GameRouter
|
||||
{
|
||||
private const int GachaProbabilityScale4229938 = 100000;
|
||||
|
||||
[Handler(MethodId.AskDrawGacha, HandlerPacketKind.Invoke)]
|
||||
private async Task AskDrawGacha(Connection conn, UxRpcMessage msg)
|
||||
{
|
||||
var args = msg.GetArgs<AskDrawGachaArgs>();
|
||||
uint poolId = args.gachaPoolId == 0 ? 1001u : args.gachaPoolId;
|
||||
uint count = args.drawCount == 0 ? 1u : args.drawCount;
|
||||
var state = GetWorldState(msg.Context);
|
||||
var request = ParseEconomyRequest4229938(msg.Body);
|
||||
var drawCount = Math.Clamp(request.Count == 0 ? 1u : request.Count, 1u, 10u);
|
||||
|
||||
conn.Session.Log.Info($"[GACHA] AskDrawGacha: poolId={poolId} count={count} autoEx={args.isAutoExchange}");
|
||||
|
||||
var fiveStarSpirits = new uint[] { 15020992, 15020991, 15020997, 15021024, 15021025 };
|
||||
var fourStarSpirits = new uint[] { 15020989, 15020990, 15021016, 15021017, 15021020, 15021021, 15021022, 15021023 };
|
||||
var topWeapons = new uint[] { 98003001, 98003023, 98003131, 98003184, 98003185 };
|
||||
|
||||
var sync = new SyncGachaDrawInfo
|
||||
if (!EconomyConfigRepository.TryPool(request.Id, out var pool))
|
||||
{
|
||||
isGrandPrizeWithAllFillers = false,
|
||||
drawDetails = new List<GachaDrawDetail>()
|
||||
};
|
||||
conn.Log.Warn($"[GACHA] unknown pool {request.Id} (body={Convert.ToHexString(msg.Body)})");
|
||||
await ReplyDrawGacha4229938Async(conn, msg);
|
||||
return;
|
||||
}
|
||||
|
||||
var records = new List<GachaDrawRecord>();
|
||||
var rng = Random.Shared;
|
||||
|
||||
for (uint i = 0; i < count; i++)
|
||||
var contents = EconomyConfigRepository.PoolContents(pool.Id);
|
||||
if (contents.Count == 0)
|
||||
{
|
||||
bool isGuaranteedFiveStar = (count >= 10 && i == count - 1) || (rng.Next(100) < 15);
|
||||
bool isFourStar = !isGuaranteedFiveStar && (rng.Next(100) < 45);
|
||||
conn.Log.Warn($"[GACHA] pool {pool.Id} has no contents, nothing can be drawn");
|
||||
await ReplyDrawGacha4229938Async(conn, msg);
|
||||
return;
|
||||
}
|
||||
|
||||
uint prizeId;
|
||||
if (isGuaranteedFiveStar)
|
||||
EconomyConfigRepository.GachaTierRule? tierRule = null;
|
||||
if (pool.GrandTierRuleId != 0 && EconomyConfigRepository.TryTierRule(pool.GrandTierRuleId, out var foundRule))
|
||||
tierRule = foundRule;
|
||||
|
||||
var changes = new EconomyChanges4229938();
|
||||
ChargeCurrency4229938(
|
||||
state, changes, pool.MoneyId, pool.CostFor(drawCount), conn.Log, $"gacha {pool.Id}");
|
||||
|
||||
var details = new List<GameMethods.GachaDrawDetail4229938>((int)drawCount);
|
||||
var grandCount = 0;
|
||||
var skipped = 0;
|
||||
|
||||
for (var draw = 0u; draw < drawCount; draw++)
|
||||
{
|
||||
uint drawsSinceGrand;
|
||||
lock (state.SyncRoot)
|
||||
{
|
||||
prizeId = fiveStarSpirits[rng.Next(fiveStarSpirits.Length)];
|
||||
state.Progress.GachaDrawsSinceGrand.TryGetValue(pool.Id, out var previous);
|
||||
drawsSinceGrand = previous + 1;
|
||||
}
|
||||
else if (isFourStar)
|
||||
|
||||
var isGrand = RollGrandPrize4229938(tierRule, drawsSinceGrand);
|
||||
var content = PickGachaContent4229938(contents, isGrand)
|
||||
?? PickGachaContent4229938(contents, grandOnly: null);
|
||||
|
||||
if (content is null)
|
||||
{
|
||||
prizeId = fourStarSpirits[rng.Next(fourStarSpirits.Length)];
|
||||
skipped++;
|
||||
lock (state.SyncRoot)
|
||||
state.Progress.GachaDrawsSinceGrand[pool.Id] = drawsSinceGrand;
|
||||
continue;
|
||||
}
|
||||
|
||||
isGrand = content.IsGrandPrize;
|
||||
lock (state.SyncRoot)
|
||||
state.Progress.GachaDrawsSinceGrand[pool.Id] = isGrand ? 0u : drawsSinceGrand;
|
||||
|
||||
if (isGrand)
|
||||
grandCount++;
|
||||
|
||||
var converted = false;
|
||||
var isNew = false;
|
||||
var dropId = content.DropId;
|
||||
var rolls = 1u;
|
||||
|
||||
if (TryPrizeTemplateId4229938(content.DropId, out var prizeTemplateId))
|
||||
{
|
||||
var owned = IsItemOwned4229938(state, prizeTemplateId);
|
||||
isNew = !owned;
|
||||
|
||||
if (owned && content.DuplicateReturnDropId != 0 && content.DuplicateReturnCount != 0)
|
||||
{
|
||||
converted = true;
|
||||
dropId = content.DuplicateReturnDropId;
|
||||
rolls = content.DuplicateReturnCount;
|
||||
}
|
||||
}
|
||||
|
||||
if (dropId != 0 && EconomyConfigRepository.TryDrop(dropId, out var drop) && !drop.IsEmpty)
|
||||
GrantDrop4229938(changes, drop, rolls, $"gacha {pool.Id} content {content.Id}", conn.Log);
|
||||
else
|
||||
{
|
||||
prizeId = topWeapons[rng.Next(topWeapons.Length)];
|
||||
}
|
||||
conn.Log.Warn($"[GACHA] content {content.Id} grants nothing (drop={dropId})");
|
||||
|
||||
sync.drawDetails.Add(new GachaDrawDetail
|
||||
details.Add(new GameMethods.GachaDrawDetail4229938
|
||||
{
|
||||
PoolContentId = prizeId,
|
||||
IsGrandPrize = isGuaranteedFiveStar,
|
||||
IsConverted = false,
|
||||
IsNew = true
|
||||
});
|
||||
|
||||
records.Add(new GachaDrawRecord
|
||||
{
|
||||
GachaId = poolId,
|
||||
PoolContentId = prizeId,
|
||||
DropId = prizeId,
|
||||
DrawTimeUnix = DateTimeOffset.UtcNow.ToUnixTimeSeconds()
|
||||
PoolContentId = content.Id,
|
||||
IsGrandPrize = isGrand,
|
||||
IsConverted = converted,
|
||||
IsNew = isNew,
|
||||
});
|
||||
}
|
||||
|
||||
await conn.NotifyAsync(MethodId.SyncGachaDrawInfo, sync);
|
||||
var summary = await ApplyEconomy4229938Async(conn, state, changes, $"gacha {pool.Id}");
|
||||
|
||||
var gachaInfos = new PlayerGachaInfos();
|
||||
var poolInfo = new PlayerGachaPoolInfo
|
||||
if (details.Count > 0)
|
||||
{
|
||||
DrawCount = count,
|
||||
DrawRecords = records
|
||||
};
|
||||
gachaInfos.PoolInfos[poolId] = poolInfo;
|
||||
gachaInfos.GroupInfos[poolId] = new PlayerGachaGroupInfo
|
||||
{
|
||||
TotalDrawCount = count
|
||||
};
|
||||
gachaInfos.PityInfos[poolId] = new PlayerGachaPityInfo
|
||||
{
|
||||
TotalDrawCount = count,
|
||||
DrawsSinceLastReset = count % 80
|
||||
};
|
||||
await conn.NotifyAsync(MethodId.IGameToClient_SyncGachaDrawInfo, new GameMethods.SyncGachaDrawInfo4229938
|
||||
{
|
||||
drawDetails = details,
|
||||
isGrandPrizeWithAllFillers = false,
|
||||
});
|
||||
}
|
||||
|
||||
await conn.ReturnAsync(msg, gachaInfos);
|
||||
conn.Log.Info(
|
||||
$"[GACHA] pool={pool.Id} system={pool.MoneyId} count={drawCount} cost={pool.CostFor(drawCount)} " +
|
||||
$"grand={grandCount} skipped={skipped} rule={tierRule?.Id ?? 0} ruleType={tierRule?.RuleType ?? 0} " +
|
||||
$"-> {summary}");
|
||||
|
||||
await ReplyDrawGacha4229938Async(conn, msg);
|
||||
}
|
||||
|
||||
private static bool RollGrandPrize4229938(EconomyConfigRepository.GachaTierRule? rule, uint drawsSinceGrand)
|
||||
{
|
||||
if (rule is null)
|
||||
return false;
|
||||
|
||||
if (rule.PityThreshold > 0 && drawsSinceGrand >= rule.PityThreshold)
|
||||
return true;
|
||||
|
||||
if (rule.RuleType != 1)
|
||||
return false;
|
||||
|
||||
var probability = (double)rule.BaseProbability;
|
||||
if (rule.RampStartDraw > 0 && drawsSinceGrand > rule.RampStartDraw)
|
||||
probability += (double)(drawsSinceGrand - rule.RampStartDraw) * rule.ProbabilityIncrement;
|
||||
|
||||
if (probability <= 0)
|
||||
return false;
|
||||
if (probability >= GachaProbabilityScale4229938)
|
||||
return true;
|
||||
|
||||
return Random.Shared.Next(GachaProbabilityScale4229938) < probability;
|
||||
}
|
||||
|
||||
private static EconomyConfigRepository.GachaContent? PickGachaContent4229938(
|
||||
IReadOnlyList<EconomyConfigRepository.GachaContent> contents,
|
||||
bool? grandOnly)
|
||||
{
|
||||
var candidates = new List<EconomyConfigRepository.GachaContent>();
|
||||
foreach (var content in contents)
|
||||
{
|
||||
if (content.Weight <= 0)
|
||||
continue;
|
||||
if (grandOnly is { } wanted && content.IsGrandPrize != wanted)
|
||||
continue;
|
||||
|
||||
candidates.Add(content);
|
||||
}
|
||||
|
||||
if (candidates.Count == 0)
|
||||
return null;
|
||||
|
||||
var index = PickWeightedIndex4229938(candidates.Count, i => candidates[i].Weight);
|
||||
return index < 0 ? null : candidates[index];
|
||||
}
|
||||
|
||||
private static bool TryPrizeTemplateId4229938(uint dropId, out uint templateId)
|
||||
{
|
||||
templateId = 0;
|
||||
if (dropId == 0 || !EconomyConfigRepository.TryDrop(dropId, out var drop))
|
||||
return false;
|
||||
|
||||
if (drop.Guaranteed.Count > 0)
|
||||
templateId = drop.Guaranteed[0].Id;
|
||||
else if (drop.Ranged.Count > 0)
|
||||
templateId = drop.Ranged[0].Id;
|
||||
else if (drop.Weighted.Count > 0)
|
||||
templateId = drop.Weighted[0].Id;
|
||||
|
||||
return templateId != 0;
|
||||
}
|
||||
|
||||
private static bool IsItemOwned4229938(WorldEntryState state, uint templateId)
|
||||
{
|
||||
lock (state.SyncRoot)
|
||||
return state.GmBackpackItems.TryGetValue(templateId, out var stack) && stack.Count > 0;
|
||||
}
|
||||
|
||||
private static Task ReplyDrawGacha4229938Async(Connection conn, UxRpcMessage msg)
|
||||
{
|
||||
if (DefaultReturnCatalog4229938.TryGet(msg.MethodId, out var body, out var shape))
|
||||
{
|
||||
conn.Log.Info($"[GACHA] reply typed-default {shape} {body.Length}b");
|
||||
return msg.Context.ReturnAsync(body);
|
||||
}
|
||||
|
||||
return conn.ReturnEmptyOkAsync(msg);
|
||||
}
|
||||
|
||||
[Handler(MethodId.AskClaimGachaMilestone, HandlerPacketKind.Invoke)]
|
||||
private Task AskClaimGachaMilestone(Connection conn, UxRpcMessage msg)
|
||||
{
|
||||
var args = msg.GetArgs<AskClaimGachaMilestoneArgs>();
|
||||
var args = msg.GetArgs<GameMethods.AskClaimGachaMilestoneArgs>();
|
||||
conn.Session.Log.Info($"[GACHA] AskClaimGachaMilestone: groupId={args.groupId} count={args.milestoneCount}");
|
||||
return conn.ReturnEmptyOkAsync(msg);
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user