feat(inventory): read-side methods on IInventoryService + tx

EffectiveBalance/OwnsCard/OwnsCosmetic on the tx are freeplay-aware
against the live viewer. EffectiveOwnedCardsAsync/EffectiveCosmeticsAsync
on the service mirror today's ViewerEntitlements projections (used by
/load/index).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
gamer147
2026-05-31 16:05:35 -04:00
parent ea340cde21
commit 91909c5755
3 changed files with 225 additions and 11 deletions

View File

@@ -60,13 +60,71 @@ public sealed class InventoryService : IInventoryService
return new InventoryTransaction(_db, dbTx, viewer, freeplay, _log);
}
// Stubs for later tasks.
public Task<IReadOnlyList<OwnedCardEntry>> EffectiveOwnedCardsAsync(Viewer viewer, CancellationToken ct = default)
=> throw new NotImplementedException();
public Task<EffectiveCosmetics> EffectiveCosmeticsAsync(Viewer viewer, CancellationToken ct = default)
=> throw new NotImplementedException();
public long EffectiveBalance(Viewer viewer, SpendCurrency currency)
=> throw new NotImplementedException();
{
var cfg = _config.Get<FreeplayConfig>();
if (cfg.Enabled && currency != SpendCurrency.SpotPoint)
return checked((long)cfg.CurrencyAmount);
return currency switch
{
SpendCurrency.Crystal => (long)viewer.Currency.Crystals,
SpendCurrency.Rupee => (long)viewer.Currency.Rupees,
SpendCurrency.RedEther => (long)viewer.Currency.RedEther,
SpendCurrency.SpotPoint => (long)viewer.Currency.SpotPoints,
_ => throw new ArgumentOutOfRangeException(nameof(currency)),
};
}
public async Task<IReadOnlyList<OwnedCardEntry>> EffectiveOwnedCardsAsync(
Viewer viewer, CancellationToken ct = default)
{
var defaults = await _cards.GetDefaultCards();
var defaultIds = defaults.Select(c => c.Id).ToHashSet();
var cfg = _config.Get<FreeplayConfig>();
if (cfg.Enabled)
{
var all = await _cards.GetAll(onlyCollectible: true);
return all
.Select(c => new OwnedCardEntry
{
Card = c,
Count = cfg.CardCopies,
IsProtected = defaultIds.Contains(c.Id),
})
.ToList();
}
var owned = viewer.Cards.Where(c => c.Count > 0 && !defaultIds.Contains(c.Card.Id));
return owned
.Concat(defaults.Select(bc => new OwnedCardEntry { Card = bc, Count = 3, IsProtected = true }))
.ToList();
}
public async Task<EffectiveCosmetics> EffectiveCosmeticsAsync(
Viewer viewer, CancellationToken ct = default)
{
var allSkins = await _collection.GetLeaderSkins();
var cfg = _config.Get<FreeplayConfig>();
if (cfg.Enabled)
{
return new EffectiveCosmetics(
await _collection.GetAllSleeveIds(),
await _collection.GetAllEmblemIds(),
await _collection.GetAllDegreeIds(),
await _collection.GetAllMyPageBackgroundIds(),
allSkins,
allSkins.Select(s => s.Id).ToHashSet());
}
return new EffectiveCosmetics(
viewer.Sleeves.Select(s => s.Id).ToList(),
viewer.Emblems.Select(e => e.Id).ToList(),
viewer.Degrees.Select(d => d.Id).ToList(),
viewer.MyPageBackgrounds.Select(m => m.Id).ToList(),
allSkins,
viewer.LeaderSkins.Select(s => s.Id).ToHashSet());
}
}

View File

@@ -237,9 +237,37 @@ internal sealed class InventoryTransaction : IInventoryTransaction
_ => false,
};
public long EffectiveBalance(SpendCurrency currency) => throw new NotImplementedException();
public bool OwnsCard(long cardId) => throw new NotImplementedException();
public bool OwnsCosmetic(CosmeticType type, int id) => throw new NotImplementedException();
public long EffectiveBalance(SpendCurrency currency)
{
if (_freeplay.Enabled && currency != SpendCurrency.SpotPoint)
return checked((long)_freeplay.CurrencyAmount);
return currency switch
{
SpendCurrency.Crystal => (long)Viewer.Currency.Crystals,
SpendCurrency.Rupee => (long)Viewer.Currency.Rupees,
SpendCurrency.RedEther => (long)Viewer.Currency.RedEther,
SpendCurrency.SpotPoint => (long)Viewer.Currency.SpotPoints,
_ => throw new ArgumentOutOfRangeException(nameof(currency)),
};
}
public bool OwnsCard(long cardId)
=> _freeplay.Enabled || Viewer.Cards.Any(c => c.Card.Id == cardId && c.Count > 0);
public bool OwnsCosmetic(CosmeticType type, int id)
{
if (_freeplay.Enabled) return true;
return type switch
{
CosmeticType.Sleeve => Viewer.Sleeves.Any(s => s.Id == id),
CosmeticType.Emblem => Viewer.Emblems.Any(e => e.Id == id),
CosmeticType.Degree => Viewer.Degrees.Any(d => d.Id == id),
CosmeticType.Skin => Viewer.LeaderSkins.Any(s => s.Id == id),
CosmeticType.MyPageBG => Viewer.MyPageBackgrounds.Any(m => m.Id == id),
_ => false,
};
}
public async Task<InventoryCommitResult> CommitAsync(CancellationToken ct = default)
{