Skip to content

Commit baa3ce0

Browse files
codexclaude
andcommitted
feat(collectibles): a real shop, and the two fields that decided which furniture the client drew
The shop tab was answering an empty list with a confident comment about needing a blockchain. That was wrong -- an offer is a furniture classname, a price in emeralds and two flags, none of which needs a chain, exactly like collections. It was unbuilt, not impossible, and there was no way for an admin to fill it. Built end to end: an nft_store_offers table, an NftStoreGrain that both serves the offers and performs the sale, the purchase message (3196) with its result (448), and a Shop tab in the dashboard with the offer list and its editor. The sale runs inside the single-threaded grain so the last copy of a limited edition cannot be sold twice, refunds the emeralds if handing the furniture over fails, and refuses outright if the offer names furniture that does not exist. The picture bug, which was the same bug in two places. The client draws a collectible by reading itemTypeId with parseInt and looking that number up in one of its own furniture tables, chosen by productTypeId (0 wall, 1 floor). Both were being stored or defaulted rather than derived: itemTypeId fell back to the classname, so parseInt("02_dragonlamp_skream") gave 2, and sprite 2 is the post-it -- a dragon lamp came out as a post-it. Collections were worse: ToPrizeItem hardcoded productTypeId 0 and the classname, so every bonus and reward in every collection was looked up in the wrong table with a nonsense id. Both now derive from the furniture definition through one shared CollectibleProductIdentity, and two tests pin sprite-id-not-classname. Silver and emeralds are pushed at login beside the credits. The client asks for them once, while its inventory component starts up, and keeps the answer for the whole session -- so opening the Collectors Guild first left both at zero over a wallet holding thousands, and the shop's own affordability check then refused every purchase before sending anything. A collection's status now means something. The client parses the field and never reads it, so an admin picking a value was picking noise; the server withholds anything that is not Visible, which makes Draft a collection you can build over several sittings without a half-finished set reaching anyone. Dashboard: the collections editor asked for a bare status number, free-text rarity and hand-typed prize classnames. Status and rarity are lists now -- rarity from the client's own colour table -- both prizes are picked from the catalogue with their image, and the page is split into Collections / Shop / Collectors tabs instead of stacking three jobs vertically. Also: the purchase handler now names all six of its outcomes in the log. Only two were logged, so the three most likely refusals left no trace anywhere and looked exactly like a packet that never arrived. Committed with --no-verify at the user's explicit request: the hook cannot run while Vortex.Main holds the build outputs. The gate's contents were run by hand instead -- csharpier clean, every touched project builds with no warnings, and Players 297 / Revisions 209 / Dashboard 20 / Database 124 all pass, plus web lint and build. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Ko6eik295nh81LAxwGayMD
1 parent 39c917b commit baa3ce0

98 files changed

Lines changed: 11737 additions & 107 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

Vortex.Dashboard.API/Api/DashboardApiService.Collectibles.cs

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,66 @@ await db
112112
})
113113
.ToList();
114114

115+
// The shop is checked the same way a collection item is: an offer naming furniture
116+
// that does not exist would take the buyer's emeralds and hand over nothing, so the
117+
// grain refuses the sale — and this is the only place that would say so beforehand.
118+
var offerRows = await db
119+
.NftStoreOffers.AsNoTracking()
120+
.OrderBy(o => o.SortOrder)
121+
.ThenBy(o => o.Id)
122+
.Select(o => new
123+
{
124+
o.Id,
125+
o.ProductCode,
126+
o.EmeraldPrice,
127+
o.IsFeatured,
128+
o.IsLimited,
129+
o.MintLimit,
130+
o.SoldCount,
131+
o.ItemTypeId,
132+
o.ProductTypeId,
133+
o.Score,
134+
o.Rarity,
135+
o.Enabled,
136+
o.SortOrder,
137+
})
138+
.ToListAsync(ct)
139+
.ConfigureAwait(false);
140+
141+
List<string> offerCodes = offerRows.Select(o => o.ProductCode).Distinct().ToList();
142+
143+
HashSet<string> knownOfferFurniture = new(
144+
await db
145+
.FurnitureDefinitions.AsNoTracking()
146+
.Where(f => offerCodes.Contains(f.Name))
147+
.Select(f => f.Name)
148+
.ToListAsync(ct)
149+
.ConfigureAwait(false),
150+
StringComparer.Ordinal
151+
);
152+
153+
var storeOffers = offerRows
154+
.Select(o => new
155+
{
156+
o.Id,
157+
o.ProductCode,
158+
o.EmeraldPrice,
159+
o.IsFeatured,
160+
o.IsLimited,
161+
o.MintLimit,
162+
o.SoldCount,
163+
o.ItemTypeId,
164+
o.ProductTypeId,
165+
o.Score,
166+
o.Rarity,
167+
o.Enabled,
168+
o.SortOrder,
169+
resolved = knownOfferFurniture.Contains(o.ProductCode),
170+
soldOut = o.MintLimit > 0 && o.SoldCount >= o.MintLimit,
171+
iconUrl = BuildFurniIconUrl(o.ProductCode),
172+
})
173+
.ToList();
174+
115175
var collectorRows = await db
116176
.PlayerCollectorStats.AsNoTracking()
117177
.OrderByDescending(s => s.HighestScore)
@@ -141,8 +201,13 @@ await db
141201
unresolvedItems = collectionItems.Sum(c => c.unresolvedItems),
142202
completableCollections = collectionItems.Count(c => c.completable),
143203
trackedPlayers,
204+
storeOffers = storeOffers.Count,
205+
storeOffersOnSale = storeOffers.Count(o =>
206+
o.Enabled && !o.soldOut && o.resolved
207+
),
144208
},
145209
collections = collectionItems,
210+
storeOffers,
146211
topCollectors = collectorRows
147212
.Select(c => new
148213
{

Vortex.Dashboard.API/Assets/AccessDeniedNotice-DlEcD0d5.js renamed to Vortex.Dashboard.API/Assets/AccessDeniedNotice-CeFsinJd.js

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Vortex.Dashboard.API/Assets/AchievementResolutionsPage-DS70I-rm.js renamed to Vortex.Dashboard.API/Assets/AchievementResolutionsPage-C3e-nq5h.js

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Vortex.Dashboard.API/Assets/AchievementsPage-AxBbVGkW.js renamed to Vortex.Dashboard.API/Assets/AchievementsPage-BrEiiftR.js

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Vortex.Dashboard.API/Assets/ApiExplorerPage-BUDeCbFw.js renamed to Vortex.Dashboard.API/Assets/ApiExplorerPage-B4JAwewY.js

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Vortex.Dashboard.API/Assets/AuditPage-QkQT23H9.js renamed to Vortex.Dashboard.API/Assets/AuditPage-DZBvJvdD.js

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Vortex.Dashboard.API/Assets/BotsPage-DOavqhYR.js renamed to Vortex.Dashboard.API/Assets/BotsPage-Co6Y36xB.js

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Vortex.Dashboard.API/Assets/CatalogPage-uEk8cT4O.js renamed to Vortex.Dashboard.API/Assets/CatalogPage-FTOZpHoe.js

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)