Skip to content

Commit 9390b99

Browse files
authored
Merge pull request #9 from Diddyy/feature/ltd-raffle-system
feat(catalog): implement LTD raffle system
2 parents 0282c90 + cc932d6 commit 9390b99

38 files changed

Lines changed: 4531 additions & 57 deletions

.config/dotnet-tools.json

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,13 @@
88
"csharpier"
99
],
1010
"rollForward": false
11+
},
12+
"dotnet-ef": {
13+
"version": "9.0.8",
14+
"commands": [
15+
"dotnet-ef"
16+
],
17+
"rollForward": false
1118
}
1219
}
13-
}
20+
}

Turbo.Catalog/CatalogService.cs

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
using System;
2+
using System.Linq;
3+
using System.Threading;
4+
using System.Threading.Tasks;
5+
using Microsoft.EntityFrameworkCore;
26
using Microsoft.Extensions.Logging;
7+
using Turbo.Database.Context;
38
using Turbo.Primitives.Catalog;
49
using Turbo.Primitives.Catalog.Enums;
510
using Turbo.Primitives.Catalog.Providers;
@@ -10,12 +15,14 @@ namespace Turbo.Catalog;
1015

1116
public sealed class CatalogService(
1217
ILogger<ICatalogService> logger,
13-
ICatalogSnapshotProvider<NormalCatalog> normalCatalogProvider
18+
ICatalogSnapshotProvider<NormalCatalog> normalCatalogProvider,
19+
IDbContextFactory<TurboDbContext> dbCtxFactory
1420
) : ICatalogService
1521
{
1622
private readonly ILogger<ICatalogService> _logger = logger;
1723
private readonly ICatalogSnapshotProvider<NormalCatalog> _normalCatalogProvider =
1824
normalCatalogProvider;
25+
private readonly IDbContextFactory<TurboDbContext> _dbCtxFactory = dbCtxFactory;
1926

2027
public CatalogSnapshot GetCatalogSnapshot(CatalogType catalogType)
2128
{
@@ -28,4 +35,40 @@ public CatalogSnapshot GetCatalogSnapshot(CatalogType catalogType)
2835
_ => throw new NotSupportedException($"Catalog type {catalogType} is not supported."),
2936
};
3037
}
38+
39+
public async Task<UpcomingLtdSnapshot?> GetUpcomingLtdAsync(CancellationToken ct)
40+
{
41+
await using var dbCtx = await _dbCtxFactory.CreateDbContextAsync(ct);
42+
43+
// Find the nearest upcoming active LTD drop
44+
var now = DateTime.UtcNow;
45+
var nextSeries = await dbCtx
46+
.LtdSeries.AsNoTracking()
47+
.Where(s => s.IsActive && s.StartsAt > now)
48+
.OrderBy(s => s.StartsAt)
49+
.FirstOrDefaultAsync(ct);
50+
51+
if (nextSeries == null)
52+
return null;
53+
54+
var catalogSnap = GetCatalogSnapshot(CatalogType.Normal);
55+
var product = catalogSnap.ProductsById.Values.FirstOrDefault(p =>
56+
p.LtdSeriesId == nextSeries.Id
57+
);
58+
59+
if (product == null)
60+
return null;
61+
62+
// Resolve PageId from Offer
63+
if (!catalogSnap.OffersById.TryGetValue(product.OfferId, out var offer))
64+
return null;
65+
66+
return new UpcomingLtdSnapshot
67+
{
68+
SecondsUntil = (int)(nextSeries.StartsAt!.Value - now).TotalSeconds,
69+
PageId = offer.PageId,
70+
OfferId = offer.Id,
71+
ClassName = product.ClassName,
72+
};
73+
}
3174
}
Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,164 @@
1+
using System;
2+
13
namespace Turbo.Catalog.Configuration;
24

35
public class CatalogConfig
46
{
57
public const string SECTION_NAME = "Turbo:Catalog";
8+
9+
/// <summary>
10+
/// Configuration for LTD raffle weighting criteria.
11+
/// </summary>
12+
public LtdRaffleWeightConfig LtdRaffle { get; set; } = new();
13+
}
14+
15+
/// <summary>
16+
/// Configuration for LTD raffle weighting criteria.
17+
/// Hotel owners can tune these values to define their own fairness criteria.
18+
/// </summary>
19+
public class LtdRaffleWeightConfig
20+
{
21+
/// <summary>
22+
/// Base weight given to all participants (ensures everyone has a chance).
23+
/// </summary>
24+
public double BaseWeight { get; set; } = 1.0;
25+
26+
/// <summary>
27+
/// Default buffer window in seconds.
28+
/// </summary>
29+
public int DefaultBufferSeconds { get; set; } = 20;
30+
31+
/// <summary>
32+
/// If true, uses pure random selection (equal chance for all).
33+
/// Set to true to disable all weighting.
34+
/// </summary>
35+
public bool UsePureRandom { get; set; } = false;
36+
37+
/// <summary>
38+
/// If true, serial numbers are assigned randomly (Habbo style).
39+
/// If false, they are assigned sequentially (1, 2, 3...).
40+
/// </summary>
41+
public bool RandomizeSerials { get; set; } = true;
42+
43+
/// <summary>
44+
/// If true, each player can only win one item per LTD series.
45+
/// If false, players can buy as many as they want (if stock permits).
46+
/// </summary>
47+
public bool LimitOnePerCustomer { get; set; } = true;
48+
49+
/// <summary>
50+
/// Maximum number of entries accepted per raffle batch.
51+
/// Prevents unbounded memory growth during the buffer window.
52+
/// </summary>
53+
public int MaxEntriesPerBatch { get; set; } = 5000;
54+
55+
/// <summary>
56+
/// Badge count weighting configuration.
57+
/// </summary>
58+
public WeightCriterion BadgeCount { get; set; } =
59+
new()
60+
{
61+
Enabled = true,
62+
BonusPerUnit = 0.02,
63+
MaxBonus = 1.0,
64+
};
65+
66+
/// <summary>
67+
/// Account age (days) weighting configuration.
68+
/// </summary>
69+
public WeightCriterion AccountAgeDays { get; set; } =
70+
new()
71+
{
72+
Enabled = true,
73+
BonusPerUnit = 0.00137, // ~0.5 per year
74+
MaxBonus = 0.5,
75+
};
76+
77+
/// <summary>
78+
/// Online time (minutes) weighting configuration.
79+
/// Note: Requires online time tracking to be implemented.
80+
/// </summary>
81+
public WeightCriterion OnlineTimeMinutes { get; set; } =
82+
new()
83+
{
84+
Enabled = false,
85+
BonusPerUnit = 0.00005,
86+
MaxBonus = 0.5,
87+
};
88+
89+
/// <summary>
90+
/// Room count weighting configuration.
91+
/// </summary>
92+
public WeightCriterion RoomCount { get; set; } =
93+
new()
94+
{
95+
Enabled = false,
96+
BonusPerUnit = 0.05,
97+
MaxBonus = 0.5,
98+
};
99+
100+
/// <summary>
101+
/// Furniture count weighting configuration.
102+
/// </summary>
103+
public WeightCriterion FurnitureCount { get; set; } =
104+
new()
105+
{
106+
Enabled = false,
107+
BonusPerUnit = 0.001,
108+
MaxBonus = 0.5,
109+
};
110+
111+
/// <summary>
112+
/// Friend count weighting configuration.
113+
/// </summary>
114+
public WeightCriterion FriendCount { get; set; } =
115+
new()
116+
{
117+
Enabled = false,
118+
BonusPerUnit = 0.01,
119+
MaxBonus = 0.5,
120+
};
121+
122+
/// <summary>
123+
/// Respects earned (from other users) weighting configuration.
124+
/// </summary>
125+
public WeightCriterion RespectsReceived { get; set; } =
126+
new()
127+
{
128+
Enabled = false,
129+
BonusPerUnit = 0.005,
130+
MaxBonus = 0.5,
131+
};
132+
133+
/// <summary>
134+
/// Achievement score weighting configuration.
135+
/// </summary>
136+
public WeightCriterion AchievementScore { get; set; } =
137+
new()
138+
{
139+
Enabled = false,
140+
BonusPerUnit = 0.0001,
141+
MaxBonus = 0.5,
142+
};
143+
}
144+
145+
/// <summary>
146+
/// Configuration for a single weighting criterion.
147+
/// </summary>
148+
public class WeightCriterion
149+
{
150+
/// <summary>
151+
/// Whether this criterion is used in weight calculation.
152+
/// </summary>
153+
public bool Enabled { get; set; }
154+
155+
/// <summary>
156+
/// Bonus weight added per unit of this criterion.
157+
/// </summary>
158+
public double BonusPerUnit { get; set; }
159+
160+
/// <summary>
161+
/// Maximum bonus that can be gained from this criterion.
162+
/// </summary>
163+
public double MaxBonus { get; set; }
6164
}

0 commit comments

Comments
 (0)