Skip to content

Commit 40a002b

Browse files
committed
feat(catalog): implement production-grade LTD raffle system
- Added weighted selection based on badges, account age, achievement score, friend count, room count, and furniture count - Implemented 20s buffer window with automatic drawing logic - Added support for both randomized and sequential serial numbers - Implemented Landing View countdown timer (Header 2178) - Added 'One-Per-Customer' anti-hoarding policy - Refactored to lean, high-performance C# 12 patterns (Primary Constructors, Collection Expressions)
1 parent 608006e commit 40a002b

40 files changed

Lines changed: 7029 additions & 1658 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: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,158 @@
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+
/// Badge count weighting configuration.
51+
/// </summary>
52+
public WeightCriterion BadgeCount { get; set; } =
53+
new()
54+
{
55+
Enabled = true,
56+
BonusPerUnit = 0.02,
57+
MaxBonus = 1.0,
58+
};
59+
60+
/// <summary>
61+
/// Account age (days) weighting configuration.
62+
/// </summary>
63+
public WeightCriterion AccountAgeDays { get; set; } =
64+
new()
65+
{
66+
Enabled = true,
67+
BonusPerUnit = 0.00137, // ~0.5 per year
68+
MaxBonus = 0.5,
69+
};
70+
71+
/// <summary>
72+
/// Online time (minutes) weighting configuration.
73+
/// Note: Requires online time tracking to be implemented.
74+
/// </summary>
75+
public WeightCriterion OnlineTimeMinutes { get; set; } =
76+
new()
77+
{
78+
Enabled = false,
79+
BonusPerUnit = 0.00005,
80+
MaxBonus = 0.5,
81+
};
82+
83+
/// <summary>
84+
/// Room count weighting configuration.
85+
/// </summary>
86+
public WeightCriterion RoomCount { get; set; } =
87+
new()
88+
{
89+
Enabled = false,
90+
BonusPerUnit = 0.05,
91+
MaxBonus = 0.5,
92+
};
93+
94+
/// <summary>
95+
/// Furniture count weighting configuration.
96+
/// </summary>
97+
public WeightCriterion FurnitureCount { get; set; } =
98+
new()
99+
{
100+
Enabled = false,
101+
BonusPerUnit = 0.001,
102+
MaxBonus = 0.5,
103+
};
104+
105+
/// <summary>
106+
/// Friend count weighting configuration.
107+
/// </summary>
108+
public WeightCriterion FriendCount { get; set; } =
109+
new()
110+
{
111+
Enabled = false,
112+
BonusPerUnit = 0.01,
113+
MaxBonus = 0.5,
114+
};
115+
116+
/// <summary>
117+
/// Respects earned (from other users) weighting configuration.
118+
/// </summary>
119+
public WeightCriterion RespectsReceived { get; set; } =
120+
new()
121+
{
122+
Enabled = false,
123+
BonusPerUnit = 0.005,
124+
MaxBonus = 0.5,
125+
};
126+
127+
/// <summary>
128+
/// Achievement score weighting configuration.
129+
/// </summary>
130+
public WeightCriterion AchievementScore { get; set; } =
131+
new()
132+
{
133+
Enabled = false,
134+
BonusPerUnit = 0.0001,
135+
MaxBonus = 0.5,
136+
};
137+
}
138+
139+
/// <summary>
140+
/// Configuration for a single weighting criterion.
141+
/// </summary>
142+
public class WeightCriterion
143+
{
144+
/// <summary>
145+
/// Whether this criterion is used in weight calculation.
146+
/// </summary>
147+
public bool Enabled { get; set; }
148+
149+
/// <summary>
150+
/// Bonus weight added per unit of this criterion.
151+
/// </summary>
152+
public double BonusPerUnit { get; set; }
153+
154+
/// <summary>
155+
/// Maximum bonus that can be gained from this criterion.
156+
/// </summary>
157+
public double MaxBonus { get; set; }
6158
}

0 commit comments

Comments
 (0)