@sonatel-os/strapi-plugin-tcg 0.3.0 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -0
- package/dist/_chunks/{App-B-WZGNkc.js → App-C3L5FTNZ.js} +12 -5
- package/dist/_chunks/{App-CS_cVYEI.mjs → App-DvkOruFZ.mjs} +12 -5
- package/dist/_chunks/{SettingsPage-DNWXuY8j.js → SettingsPage-BxmcGJ5s.js} +2 -2
- package/dist/_chunks/{SettingsPage-fGiEcX6l.mjs → SettingsPage-SjgoOH4S.mjs} +2 -2
- package/dist/_chunks/{api-hTExOv_J.js → api-BqmNHByN.js} +1 -1
- package/dist/_chunks/{api-BgR-fpDp.mjs → api-Clm2RMWV.mjs} +1 -1
- package/dist/_chunks/{index-ClDgxnnH.mjs → index-CwyVbzVg.mjs} +2 -2
- package/dist/_chunks/{index-CRbzJppw.js → index-DQmN1LMy.js} +2 -2
- package/dist/admin/index.js +1 -1
- package/dist/admin/index.mjs +1 -1
- package/dist/server/index.js +206 -43
- package/dist/server/index.mjs +206 -43
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -16,6 +16,7 @@ It was built to move **Jambaar** (the Dakar 2026 Youth Olympics card game) off i
|
|
|
16
16
|
| **Live** | **Boosts**: multiply the odds of any rarity for a time window, season-wide or on one pack; presets like "Golden hour". Open/sold-out drops, rewards running low. | `GET /live` |
|
|
17
17
|
| **Pulls** | Dashboard rarity split vs target, audit log with seed, roll, **odds snapshot**, **pity** flag and **mint number**. | `POST /pulls/scan`, `GET /pulls/history`, `GET /pulls/cooldowns` |
|
|
18
18
|
| **Collection** | Per-player view with completion, rank, sets, badges, mint numbers. | `GET /collection`, `GET /collection/stats` |
|
|
19
|
+
| **Score & rank** | Rarity-weighted, not a card count: every distinct card is worth its own `score` (admin, inside its rarity's band) or the midpoint of the band (defaults: common 20 · rare 45 · epic 70 · legendary 92); every completed set adds `limits.setCompletionBonus` (50). Standings sort by score, then distinct cards, then total cards. `GET /collection/stats` returns `globalRank`, `score`, `scoreBreakdown`; cards carry `points`. | `GET /collection/stats`, admin `GET /seasons/:season/leaderboard` |
|
|
19
20
|
| **Trades** | A handshake both players confirm: the initiator's offer (QR) → a receiver joins → proposes cards → the initiator confirms (atomic swap) or declines. Both screens poll `GET /trades/offer/:id` (`status`, `role`, both sides' cards). Counts on the dashboard. | `POST /trades/offer`, `POST /trades/scan`, `GET …/offer/:id`, `…/join`, `…/propose`, `…/confirm`, `…/decline`, `…/reject`, `…/cancel`, `GET /trades/active`, `GET /trades/history` |
|
|
20
21
|
| **Rewards** | **Catalog** with tiers, stock, per-player limit, booth, availability window and a **cost builder**: N cards of a rarity (or at least a rarity), a specific card, a complete set (owned or handed in). | `GET /rewards`, `GET /rewards/eligibility` |
|
|
21
22
|
| **Booth** | Type or scan a redemption code, see the cards and the reward, hand it over or cancel and give the cards back (stock released). | `POST /redemptions`, `GET /redemptions/:id`, `GET /redemptions/history` |
|
|
@@ -7,9 +7,9 @@ const admin = require("@strapi/strapi/admin");
|
|
|
7
7
|
const reactRouterDom = require("react-router-dom");
|
|
8
8
|
const reactIntl = require("react-intl");
|
|
9
9
|
const styledComponents = require("styled-components");
|
|
10
|
-
const api = require("./api-
|
|
10
|
+
const api = require("./api-BqmNHByN.js");
|
|
11
11
|
const react = require("react");
|
|
12
|
-
const index = require("./index-
|
|
12
|
+
const index = require("./index-DQmN1LMy.js");
|
|
13
13
|
const QR = require("qrcode");
|
|
14
14
|
const _interopDefault = (e) => e && e.__esModule ? e : { default: e };
|
|
15
15
|
const QR__default = /* @__PURE__ */ _interopDefault(QR);
|
|
@@ -422,10 +422,17 @@ const Dashboard = () => {
|
|
|
422
422
|
/* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { variant: "pi", textColor: "neutral600", children: e.player.code })
|
|
423
423
|
] }),
|
|
424
424
|
/* @__PURE__ */ jsxRuntime.jsxs(designSystem.Typography, { variant: "pi", children: [
|
|
425
|
+
/* @__PURE__ */ jsxRuntime.jsxs(designSystem.Typography, { fontWeight: "bold", children: [
|
|
426
|
+
e.score,
|
|
427
|
+
" pts"
|
|
428
|
+
] }),
|
|
429
|
+
" · ",
|
|
425
430
|
e.uniqueCards,
|
|
426
|
-
"
|
|
431
|
+
" ",
|
|
432
|
+
"unique · ",
|
|
427
433
|
e.totalCards,
|
|
428
|
-
" total"
|
|
434
|
+
" total",
|
|
435
|
+
e.setsCompleted ? ` · ${e.setsCompleted} set${e.setsCompleted > 1 ? "s" : ""}` : ""
|
|
429
436
|
] })
|
|
430
437
|
] }, e.player.id)) })
|
|
431
438
|
}
|
|
@@ -2533,7 +2540,7 @@ const PlayerDetailPage = () => {
|
|
|
2533
2540
|
{
|
|
2534
2541
|
label: "Cards",
|
|
2535
2542
|
value: stats?.totalCards ?? player.totalCards,
|
|
2536
|
-
hint: stats ? `${stats.uniqueCards} unique of ${stats.catalogTotal}` : void 0
|
|
2543
|
+
hint: stats ? `${stats.uniqueCards} unique of ${stats.catalogTotal} · ${stats.score ?? 0} pts` : void 0
|
|
2537
2544
|
}
|
|
2538
2545
|
) }),
|
|
2539
2546
|
/* @__PURE__ */ jsxRuntime.jsx(designSystem.Grid.Item, { col: 3, s: 6, alignItems: "stretch", children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
@@ -5,9 +5,9 @@ import { Page, useRBAC, useNotification } from "@strapi/strapi/admin";
|
|
|
5
5
|
import { useNavigate, useParams, Routes, Route, NavLink } from "react-router-dom";
|
|
6
6
|
import { useIntl } from "react-intl";
|
|
7
7
|
import { styled } from "styled-components";
|
|
8
|
-
import { u as useGetSeasonsQuery, E as Eyebrow, I as Intro, S as SPRING$1, a as useGetDashboardQuery, b as useGetLeaderboardQuery, c as useGetRewardsQuery, d as useGetSetsQuery, e as useGetBadgesQuery, f as Empty, g as Section, h as SetupSteps, i as StatTile, j as Sparkline, k as fmtNumber, R as RarityBadge, B as Bar, t as timeAgo, C as Card, P as Pill, l as StatusBadge, m as fmtDate, n as useGetSeasonDefaultsQuery, o as useGetSeasonQuery, p as useCreateSeasonMutation, q as useUpdateSeasonMutation, r as useDeleteSeasonMutation, s as errorMessage, v as useGetCardsQuery, w as useCreateCardMutation, x as useUpdateCardMutation, y as useDeleteCardMutation, z as rarityOf, A as useGetPacksQuery, D as useCreatePackMutation, F as useGetPackQuery, G as useUpdatePackMutation, H as useSetPackCardsMutation, J as useDeletePackMutation, K as useCreateDropMutation, L as useLazySimulatePackQuery, M as useGetPlayersQuery, N as fmtDateTime, O as useGetPlayerQuery, Q as useSetPlayerActiveMutation, T as useGrantCardMutation, U as useLazyGetRedemptionQuery, V as useFulfillRedemptionMutation, W as useCancelRedemptionMutation, X as useGetRedemptionsQuery, Y as Mono, Z as useGetPullsQuery, _ as useGetDropQuery, $ as useUpdateDropMutation, a0 as useSetDropCardsMutation, a1 as useDeleteDropMutation, a2 as useRotateDropSecretMutation, a3 as useDropQRMutation, a4 as useGetBoostsQuery, a5 as useCreateBoostMutation, a6 as useUpdateBoostMutation, a7 as useDeleteBoostMutation, a8 as useCreateRewardMutation, a9 as useUpdateRewardMutation, aa as useDeleteRewardMutation, ab as useCreateSetMutation, ac as useUpdateSetMutation, ad as useDeleteSetMutation, ae as useCreateBadgeMutation, af as useUpdateBadgeMutation, ag as useDeleteBadgeMutation, ah as useSeedBadgesMutation } from "./api-
|
|
8
|
+
import { u as useGetSeasonsQuery, E as Eyebrow, I as Intro, S as SPRING$1, a as useGetDashboardQuery, b as useGetLeaderboardQuery, c as useGetRewardsQuery, d as useGetSetsQuery, e as useGetBadgesQuery, f as Empty, g as Section, h as SetupSteps, i as StatTile, j as Sparkline, k as fmtNumber, R as RarityBadge, B as Bar, t as timeAgo, C as Card, P as Pill, l as StatusBadge, m as fmtDate, n as useGetSeasonDefaultsQuery, o as useGetSeasonQuery, p as useCreateSeasonMutation, q as useUpdateSeasonMutation, r as useDeleteSeasonMutation, s as errorMessage, v as useGetCardsQuery, w as useCreateCardMutation, x as useUpdateCardMutation, y as useDeleteCardMutation, z as rarityOf, A as useGetPacksQuery, D as useCreatePackMutation, F as useGetPackQuery, G as useUpdatePackMutation, H as useSetPackCardsMutation, J as useDeletePackMutation, K as useCreateDropMutation, L as useLazySimulatePackQuery, M as useGetPlayersQuery, N as fmtDateTime, O as useGetPlayerQuery, Q as useSetPlayerActiveMutation, T as useGrantCardMutation, U as useLazyGetRedemptionQuery, V as useFulfillRedemptionMutation, W as useCancelRedemptionMutation, X as useGetRedemptionsQuery, Y as Mono, Z as useGetPullsQuery, _ as useGetDropQuery, $ as useUpdateDropMutation, a0 as useSetDropCardsMutation, a1 as useDeleteDropMutation, a2 as useRotateDropSecretMutation, a3 as useDropQRMutation, a4 as useGetBoostsQuery, a5 as useCreateBoostMutation, a6 as useUpdateBoostMutation, a7 as useDeleteBoostMutation, a8 as useCreateRewardMutation, a9 as useUpdateRewardMutation, aa as useDeleteRewardMutation, ab as useCreateSetMutation, ac as useUpdateSetMutation, ad as useDeleteSetMutation, ae as useCreateBadgeMutation, af as useUpdateBadgeMutation, ag as useDeleteBadgeMutation, ah as useSeedBadgesMutation } from "./api-Clm2RMWV.mjs";
|
|
9
9
|
import { useState, useEffect, useMemo, createContext, useContext, useRef } from "react";
|
|
10
|
-
import { P as PLUGIN_ID, a as PERMISSIONS } from "./index-
|
|
10
|
+
import { P as PLUGIN_ID, a as PERMISSIONS } from "./index-CwyVbzVg.mjs";
|
|
11
11
|
import QR from "qrcode";
|
|
12
12
|
const KEY = "tcg.season";
|
|
13
13
|
const SeasonContext = createContext({
|
|
@@ -418,10 +418,17 @@ const Dashboard = () => {
|
|
|
418
418
|
/* @__PURE__ */ jsx(Typography, { variant: "pi", textColor: "neutral600", children: e.player.code })
|
|
419
419
|
] }),
|
|
420
420
|
/* @__PURE__ */ jsxs(Typography, { variant: "pi", children: [
|
|
421
|
+
/* @__PURE__ */ jsxs(Typography, { fontWeight: "bold", children: [
|
|
422
|
+
e.score,
|
|
423
|
+
" pts"
|
|
424
|
+
] }),
|
|
425
|
+
" · ",
|
|
421
426
|
e.uniqueCards,
|
|
422
|
-
"
|
|
427
|
+
" ",
|
|
428
|
+
"unique · ",
|
|
423
429
|
e.totalCards,
|
|
424
|
-
" total"
|
|
430
|
+
" total",
|
|
431
|
+
e.setsCompleted ? ` · ${e.setsCompleted} set${e.setsCompleted > 1 ? "s" : ""}` : ""
|
|
425
432
|
] })
|
|
426
433
|
] }, e.player.id)) })
|
|
427
434
|
}
|
|
@@ -2529,7 +2536,7 @@ const PlayerDetailPage = () => {
|
|
|
2529
2536
|
{
|
|
2530
2537
|
label: "Cards",
|
|
2531
2538
|
value: stats?.totalCards ?? player.totalCards,
|
|
2532
|
-
hint: stats ? `${stats.uniqueCards} unique of ${stats.catalogTotal}` : void 0
|
|
2539
|
+
hint: stats ? `${stats.uniqueCards} unique of ${stats.catalogTotal} · ${stats.score ?? 0} pts` : void 0
|
|
2533
2540
|
}
|
|
2534
2541
|
) }),
|
|
2535
2542
|
/* @__PURE__ */ jsx(Grid.Item, { col: 3, s: 6, alignItems: "stretch", children: /* @__PURE__ */ jsx(
|
|
@@ -3,8 +3,8 @@ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
|
3
3
|
const jsxRuntime = require("react/jsx-runtime");
|
|
4
4
|
const designSystem = require("@strapi/design-system");
|
|
5
5
|
const admin = require("@strapi/strapi/admin");
|
|
6
|
-
const api = require("./api-
|
|
7
|
-
const index = require("./index-
|
|
6
|
+
const api = require("./api-BqmNHByN.js");
|
|
7
|
+
const index = require("./index-DQmN1LMy.js");
|
|
8
8
|
const Row = ({ k, v, hint, tone }) => /* @__PURE__ */ jsxRuntime.jsxs(
|
|
9
9
|
designSystem.Flex,
|
|
10
10
|
{
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { Grid, Box, Flex, Typography, Badge } from "@strapi/design-system";
|
|
3
3
|
import { Page, Layouts } from "@strapi/strapi/admin";
|
|
4
|
-
import { ai as useGetSettingsQuery, I as Intro, g as Section, Y as Mono } from "./api-
|
|
5
|
-
import { a as PERMISSIONS } from "./index-
|
|
4
|
+
import { ai as useGetSettingsQuery, I as Intro, g as Section, Y as Mono } from "./api-Clm2RMWV.mjs";
|
|
5
|
+
import { a as PERMISSIONS } from "./index-CwyVbzVg.mjs";
|
|
6
6
|
const Row = ({ k, v, hint, tone }) => /* @__PURE__ */ jsxs(
|
|
7
7
|
Flex,
|
|
8
8
|
{
|
|
@@ -7,7 +7,7 @@ const symbols = require("@strapi/icons/symbols");
|
|
|
7
7
|
const reactRouterDom = require("react-router-dom");
|
|
8
8
|
const styledComponents = require("styled-components");
|
|
9
9
|
const admin = require("@strapi/strapi/admin");
|
|
10
|
-
const index = require("./index-
|
|
10
|
+
const index = require("./index-DQmN1LMy.js");
|
|
11
11
|
const fmtDateTime = (value, locale) => {
|
|
12
12
|
if (!value) return "—";
|
|
13
13
|
try {
|
|
@@ -6,7 +6,7 @@ import { EmptyDocuments } from "@strapi/icons/symbols";
|
|
|
6
6
|
import { useNavigate } from "react-router-dom";
|
|
7
7
|
import { styled, css, keyframes } from "styled-components";
|
|
8
8
|
import { adminApi } from "@strapi/strapi/admin";
|
|
9
|
-
import { P as PLUGIN_ID } from "./index-
|
|
9
|
+
import { P as PLUGIN_ID } from "./index-CwyVbzVg.mjs";
|
|
10
10
|
const fmtDateTime = (value, locale) => {
|
|
11
11
|
if (!value) return "—";
|
|
12
12
|
try {
|
|
@@ -62,7 +62,7 @@ const index = {
|
|
|
62
62
|
intlLabel: { id: `${PLUGIN_ID}.menu.label`, defaultMessage: "TCG" },
|
|
63
63
|
permissions: PERMISSIONS.access,
|
|
64
64
|
Component: async () => {
|
|
65
|
-
const { App } = await import("./App-
|
|
65
|
+
const { App } = await import("./App-DvkOruFZ.mjs");
|
|
66
66
|
return App;
|
|
67
67
|
}
|
|
68
68
|
});
|
|
@@ -75,7 +75,7 @@ const index = {
|
|
|
75
75
|
intlLabel: { id: `${PLUGIN_ID}.settings.link`, defaultMessage: "Configuration" },
|
|
76
76
|
permissions: PERMISSIONS.settingsRead,
|
|
77
77
|
Component: async () => {
|
|
78
|
-
const { SettingsPage } = await import("./SettingsPage-
|
|
78
|
+
const { SettingsPage } = await import("./SettingsPage-SjgoOH4S.mjs");
|
|
79
79
|
return SettingsPage;
|
|
80
80
|
}
|
|
81
81
|
}
|
|
@@ -63,7 +63,7 @@ const index = {
|
|
|
63
63
|
intlLabel: { id: `${PLUGIN_ID}.menu.label`, defaultMessage: "TCG" },
|
|
64
64
|
permissions: PERMISSIONS.access,
|
|
65
65
|
Component: async () => {
|
|
66
|
-
const { App } = await Promise.resolve().then(() => require("./App-
|
|
66
|
+
const { App } = await Promise.resolve().then(() => require("./App-C3L5FTNZ.js"));
|
|
67
67
|
return App;
|
|
68
68
|
}
|
|
69
69
|
});
|
|
@@ -76,7 +76,7 @@ const index = {
|
|
|
76
76
|
intlLabel: { id: `${PLUGIN_ID}.settings.link`, defaultMessage: "Configuration" },
|
|
77
77
|
permissions: PERMISSIONS.settingsRead,
|
|
78
78
|
Component: async () => {
|
|
79
|
-
const { SettingsPage } = await Promise.resolve().then(() => require("./SettingsPage-
|
|
79
|
+
const { SettingsPage } = await Promise.resolve().then(() => require("./SettingsPage-BxmcGJ5s.js"));
|
|
80
80
|
return SettingsPage;
|
|
81
81
|
}
|
|
82
82
|
}
|
package/dist/admin/index.js
CHANGED
package/dist/admin/index.mjs
CHANGED
package/dist/server/index.js
CHANGED
|
@@ -158,7 +158,9 @@ const DEFAULT_LIMITS = Object.freeze({
|
|
|
158
158
|
qrTokenTtlMs: 5 * 60 * 1e3,
|
|
159
159
|
staticScanCooldownMs: 24 * 60 * 60 * 1e3,
|
|
160
160
|
pityRareAfter: 0,
|
|
161
|
-
pityLegendaryAfter: 0
|
|
161
|
+
pityLegendaryAfter: 0,
|
|
162
|
+
/** Points a completed set adds to the collector's score (utils/score.js). */
|
|
163
|
+
setCompletionBonus: 50
|
|
162
164
|
});
|
|
163
165
|
const DEFAULT_RANK_TIERS = Object.freeze([
|
|
164
166
|
{ key: "legend", label: "Legend", minPercent: 100 },
|
|
@@ -2703,6 +2705,39 @@ const qr = ({ strapi }) => ({
|
|
|
2703
2705
|
return count ?? 0;
|
|
2704
2706
|
}
|
|
2705
2707
|
});
|
|
2708
|
+
const rarityPoints = (season2, rarityKey) => {
|
|
2709
|
+
const table = season2?.rarities?.length ? season2.rarities : DEFAULT_RARITIES;
|
|
2710
|
+
const idx = table.findIndex((r) => r.key === rarityKey);
|
|
2711
|
+
const meta = table[idx] ?? DEFAULT_RARITIES.find((r) => r.key === rarityKey) ?? null;
|
|
2712
|
+
const min = Number(meta?.scoreMin);
|
|
2713
|
+
const max = Number(meta?.scoreMax);
|
|
2714
|
+
if (Number.isFinite(min) && Number.isFinite(max) && max >= min)
|
|
2715
|
+
return Math.round((min + max) / 2);
|
|
2716
|
+
return 10 + Math.max(0, idx) * 20;
|
|
2717
|
+
};
|
|
2718
|
+
const cardPoints = (card2, season2) => {
|
|
2719
|
+
const own = Number(card2?.score);
|
|
2720
|
+
if (Number.isFinite(own) && own > 0) return Math.round(own);
|
|
2721
|
+
return rarityPoints(season2, card2?.rarity);
|
|
2722
|
+
};
|
|
2723
|
+
const standingOf = (distinctCards, totalCards, sets, season2, limits = {}) => {
|
|
2724
|
+
const ownedIds = new Set(distinctCards.map((c) => String(c.id)));
|
|
2725
|
+
const cardScore = distinctCards.reduce((sum, c) => sum + cardPoints(c, season2), 0);
|
|
2726
|
+
const setsCompleted = (sets ?? []).filter(
|
|
2727
|
+
(s) => (s.cards ?? []).length > 0 && s.cards.every((c) => ownedIds.has(String(c.id)))
|
|
2728
|
+
).length;
|
|
2729
|
+
const bonus = Math.max(0, Number(limits.setCompletionBonus) || 0);
|
|
2730
|
+
return {
|
|
2731
|
+
uniqueCards: distinctCards.length,
|
|
2732
|
+
totalCards,
|
|
2733
|
+
cardScore,
|
|
2734
|
+
setsCompleted,
|
|
2735
|
+
setBonus: setsCompleted * bonus,
|
|
2736
|
+
score: cardScore + setsCompleted * bonus
|
|
2737
|
+
};
|
|
2738
|
+
};
|
|
2739
|
+
const compareStandings = (a, b) => b.score - a.score || b.uniqueCards - a.uniqueCards || b.totalCards - a.totalCards;
|
|
2740
|
+
const rankAmong = (mine, all) => 1 + all.filter((s) => compareStandings(s, mine) < 0).length;
|
|
2706
2741
|
const mediaUrl = (media) => media && typeof media === "object" ? media.url ?? null : null;
|
|
2707
2742
|
const format = () => ({
|
|
2708
2743
|
rarityMeta(season2, key) {
|
|
@@ -2721,6 +2756,7 @@ const format = () => ({
|
|
|
2721
2756
|
rarityColor: rarity.color,
|
|
2722
2757
|
edition: card2.edition ?? "standard",
|
|
2723
2758
|
score: card2.score ?? 0,
|
|
2759
|
+
points: cardPoints(card2, season2),
|
|
2724
2760
|
imageUrl: mediaUrl(card2.image),
|
|
2725
2761
|
attributes: card2.attributes ?? {},
|
|
2726
2762
|
lore: card2.lore ?? null,
|
|
@@ -2782,7 +2818,13 @@ const format = () => ({
|
|
|
2782
2818
|
},
|
|
2783
2819
|
/** Admin-only view: includes msisdn. */
|
|
2784
2820
|
playerAdmin(player2) {
|
|
2785
|
-
return player2 ? {
|
|
2821
|
+
return player2 ? {
|
|
2822
|
+
...this.player(player2),
|
|
2823
|
+
externalId: player2.externalId ?? null,
|
|
2824
|
+
msisdn: player2.msisdn ?? null,
|
|
2825
|
+
isActive: player2.isActive,
|
|
2826
|
+
meta: player2.meta ?? {}
|
|
2827
|
+
} : null;
|
|
2786
2828
|
},
|
|
2787
2829
|
playerCard(pc, season2) {
|
|
2788
2830
|
if (!pc) return null;
|
|
@@ -2810,7 +2852,16 @@ const format = () => ({
|
|
|
2810
2852
|
note: r.note ?? null,
|
|
2811
2853
|
player: r.player ? this.player(r.player) : void 0,
|
|
2812
2854
|
season: r.season ? { id: r.season.documentId, name: r.season.name, slug: r.season.slug } : void 0,
|
|
2813
|
-
reward: r.reward ? {
|
|
2855
|
+
reward: r.reward ? {
|
|
2856
|
+
id: r.reward.documentId,
|
|
2857
|
+
name: r.reward.name,
|
|
2858
|
+
slug: r.reward.slug,
|
|
2859
|
+
tier: r.reward.tier ?? null,
|
|
2860
|
+
booth: r.reward.booth ?? null,
|
|
2861
|
+
imageUrl: mediaUrl(r.reward.image),
|
|
2862
|
+
stock: r.reward.stock ?? null,
|
|
2863
|
+
claimed: r.reward.claimed ?? 0
|
|
2864
|
+
} : r.rewardName ? { name: r.rewardName } : null
|
|
2814
2865
|
};
|
|
2815
2866
|
}
|
|
2816
2867
|
});
|
|
@@ -3522,22 +3573,11 @@ const collection = ({ strapi }) => ({
|
|
|
3522
3573
|
return [...new Set(rows.map((r) => r.card?.id).filter(Boolean))];
|
|
3523
3574
|
},
|
|
3524
3575
|
/**
|
|
3525
|
-
* Global rank = 1 + number of
|
|
3526
|
-
*
|
|
3576
|
+
* Global rank = 1 + the number of collectors with a better standing: rarity-weighted score, then distinct
|
|
3577
|
+
* cards, then total cards (utils/score.js). Null before the first card.
|
|
3527
3578
|
*/
|
|
3528
|
-
async rank(player2, season2
|
|
3529
|
-
|
|
3530
|
-
const knex = strapi.db.connection;
|
|
3531
|
-
const md = strapi.db.metadata.get(UID.PLAYER_CARD);
|
|
3532
|
-
const pcTable = md.tableName;
|
|
3533
|
-
const playerLnk = md.attributes.player.joinTable;
|
|
3534
|
-
const cardLnk = md.attributes.card.joinTable;
|
|
3535
|
-
const cardMd = strapi.db.metadata.get(UID.CARD);
|
|
3536
|
-
const seasonLnk = cardMd.attributes.season.joinTable;
|
|
3537
|
-
const seasonId = season2.id;
|
|
3538
|
-
const sub = knex(`${pcTable} as pc`).join(`${playerLnk.name} as pl`, "pl." + playerLnk.joinColumn.name, "pc.id").join(`${cardLnk.name} as cl`, "cl." + cardLnk.joinColumn.name, "pc.id").join(`${seasonLnk.name} as sl`, "sl." + seasonLnk.joinColumn.name, "cl." + cardLnk.inverseJoinColumn.name).where("sl." + seasonLnk.inverseJoinColumn.name, seasonId).groupBy("pl." + playerLnk.inverseJoinColumn.name).select("pl." + playerLnk.inverseJoinColumn.name + " as player_id").countDistinct("cl." + cardLnk.inverseJoinColumn.name + " as unique_cards");
|
|
3539
|
-
const row = await knex.from(sub.as("ranked")).where("ranked.unique_cards", ">", myUnique).count("* as better").first();
|
|
3540
|
-
return Number(row?.better ?? 0) + 1;
|
|
3579
|
+
async rank(player2, season2) {
|
|
3580
|
+
return (await strapi.plugin("tcg").service("stats").standingFor(player2, season2)).rank;
|
|
3541
3581
|
},
|
|
3542
3582
|
async stats(player2, season2) {
|
|
3543
3583
|
const plugin = strapi.plugin("tcg");
|
|
@@ -3573,16 +3613,26 @@ const collection = ({ strapi }) => ({
|
|
|
3573
3613
|
populate: { pack: { select: ["id"] }, card: { select: ["id"] } }
|
|
3574
3614
|
});
|
|
3575
3615
|
const byPack = packs.map((p) => {
|
|
3576
|
-
const cardIds = [
|
|
3616
|
+
const cardIds = [
|
|
3617
|
+
...new Set(
|
|
3618
|
+
packRows.filter((r) => r.pack?.id === p.id).map((r) => r.card?.id).filter(Boolean)
|
|
3619
|
+
)
|
|
3620
|
+
];
|
|
3577
3621
|
const collected = cardIds.filter((id) => ownedIds.has(id)).length;
|
|
3578
|
-
return {
|
|
3622
|
+
return {
|
|
3623
|
+
pack: format2.pack(p),
|
|
3624
|
+
collected,
|
|
3625
|
+
total: cardIds.length,
|
|
3626
|
+
isComplete: cardIds.length > 0 && collected === cardIds.length
|
|
3627
|
+
};
|
|
3579
3628
|
});
|
|
3580
3629
|
const tier = plugin.service("season").tierFor(season2, completion);
|
|
3581
|
-
const [sets, badges, metrics, live] = await Promise.all([
|
|
3630
|
+
const [sets, badges, metrics, live, standing] = await Promise.all([
|
|
3582
3631
|
plugin.service("set").list(season2, { includeInactive: false }),
|
|
3583
3632
|
plugin.service("badge").forPlayer(player2, season2),
|
|
3584
3633
|
plugin.service("badge").metrics(player2, season2),
|
|
3585
|
-
plugin.service("boost").liveNow(season2)
|
|
3634
|
+
plugin.service("boost").liveNow(season2),
|
|
3635
|
+
plugin.service("stats").standingFor(player2, season2)
|
|
3586
3636
|
]);
|
|
3587
3637
|
return {
|
|
3588
3638
|
sets: plugin.service("set").progress(sets, ownedIds, season2),
|
|
@@ -3593,7 +3643,13 @@ const collection = ({ strapi }) => ({
|
|
|
3593
3643
|
uniqueCards: ownedIds.size,
|
|
3594
3644
|
catalogTotal,
|
|
3595
3645
|
completionPercentage: completion,
|
|
3596
|
-
globalRank:
|
|
3646
|
+
globalRank: standing.rank,
|
|
3647
|
+
score: standing.score,
|
|
3648
|
+
scoreBreakdown: {
|
|
3649
|
+
cards: standing.cardScore,
|
|
3650
|
+
setBonus: standing.setBonus,
|
|
3651
|
+
setsCompleted: standing.setsCompleted
|
|
3652
|
+
},
|
|
3597
3653
|
rankTier: { key: tier.key, label: tier.label },
|
|
3598
3654
|
season: format2.season(season2),
|
|
3599
3655
|
byRarity,
|
|
@@ -3607,7 +3663,14 @@ const collection = ({ strapi }) => ({
|
|
|
3607
3663
|
const mintNumber = await strapi.plugin("tcg").service("pull").claimSlot(UID.CARD, card2.id, "minted", null);
|
|
3608
3664
|
const pc = await strapi.db.transaction(async () => {
|
|
3609
3665
|
const row = await strapi.db.query(UID.PLAYER_CARD).create({
|
|
3610
|
-
data: {
|
|
3666
|
+
data: {
|
|
3667
|
+
player: player2.id,
|
|
3668
|
+
card: card2.id,
|
|
3669
|
+
pack: pack2?.id ?? null,
|
|
3670
|
+
acquiredFrom: "grant",
|
|
3671
|
+
acquiredAt: now,
|
|
3672
|
+
mintNumber
|
|
3673
|
+
}
|
|
3611
3674
|
});
|
|
3612
3675
|
await strapi.db.query(UID.PLAYER).update({ where: { id: player2.id }, data: { totalCards: (player2.totalCards ?? 0) + 1 } });
|
|
3613
3676
|
return row;
|
|
@@ -4122,6 +4185,8 @@ const redemption = ({ strapi }) => ({
|
|
|
4122
4185
|
return { items: items.map((r) => format2.redemption(r)), total };
|
|
4123
4186
|
}
|
|
4124
4187
|
});
|
|
4188
|
+
const STANDINGS_TTL_MS = 10 * 1e3;
|
|
4189
|
+
const STANDINGS_CACHE = /* @__PURE__ */ new Map();
|
|
4125
4190
|
const startOfToday = () => {
|
|
4126
4191
|
const d = /* @__PURE__ */ new Date();
|
|
4127
4192
|
d.setHours(0, 0, 0, 0);
|
|
@@ -4156,15 +4221,31 @@ const stats = ({ strapi }) => ({
|
|
|
4156
4221
|
strapi.db.query(UID.PULL).count({ where: { ...seasonWhere, pulledAt: { $gte: since } } }),
|
|
4157
4222
|
strapi.db.query(UID.PULL).count({ where: seasonWhere }),
|
|
4158
4223
|
strapi.db.query(UID.TRADE).count({ where: { ...seasonWhere, completedAt: { $gte: since } } }),
|
|
4159
|
-
strapi.db.query(UID.TRADE_OFFER).count({
|
|
4224
|
+
strapi.db.query(UID.TRADE_OFFER).count({
|
|
4225
|
+
where: { ...seasonWhere, status: TRADE_STATUS.PENDING, expiresAt: { $gt: /* @__PURE__ */ new Date() } }
|
|
4226
|
+
}),
|
|
4160
4227
|
strapi.db.query(UID.REDEMPTION).count({ where: { ...seasonWhere, status: REDEMPTION_STATUS.PENDING } }),
|
|
4161
|
-
strapi.db.query(UID.REDEMPTION).count({
|
|
4228
|
+
strapi.db.query(UID.REDEMPTION).count({
|
|
4229
|
+
where: {
|
|
4230
|
+
...seasonWhere,
|
|
4231
|
+
status: REDEMPTION_STATUS.FULFILLED,
|
|
4232
|
+
fulfilledAt: { $gte: since }
|
|
4233
|
+
}
|
|
4234
|
+
}),
|
|
4162
4235
|
season2 ? strapi.db.query(UID.CARD).count({ where: { season: season2.id, isActive: true } }) : Promise.resolve(0),
|
|
4163
4236
|
season2 ? strapi.db.query(UID.PACK).count({ where: { season: season2.id, isActive: true } }) : Promise.resolve(0),
|
|
4164
|
-
strapi.db.query(UID.PULL).findMany({
|
|
4237
|
+
strapi.db.query(UID.PULL).findMany({
|
|
4238
|
+
where: { ...seasonWhere, pulledAt: { $gte: week } },
|
|
4239
|
+
select: ["pulledAt", "rarity"]
|
|
4240
|
+
}),
|
|
4165
4241
|
strapi.db.query(UID.PULL).findMany({
|
|
4166
4242
|
where: seasonWhere,
|
|
4167
|
-
populate: {
|
|
4243
|
+
populate: {
|
|
4244
|
+
player: { select: ["code", "displayName"] },
|
|
4245
|
+
card: { select: ["name", "rarity"] },
|
|
4246
|
+
pack: { select: ["name"] },
|
|
4247
|
+
drop: { select: ["name"] }
|
|
4248
|
+
},
|
|
4168
4249
|
orderBy: { pulledAt: "desc" },
|
|
4169
4250
|
limit: 8
|
|
4170
4251
|
})
|
|
@@ -4173,24 +4254,47 @@ const stats = ({ strapi }) => ({
|
|
|
4173
4254
|
const day = daysAgo(6 - i);
|
|
4174
4255
|
const next = new Date(day);
|
|
4175
4256
|
next.setDate(day.getDate() + 1);
|
|
4176
|
-
return {
|
|
4257
|
+
return {
|
|
4258
|
+
day: day.toISOString().slice(0, 10),
|
|
4259
|
+
pulls: weekPulls.filter((p) => new Date(p.pulledAt) >= day && new Date(p.pulledAt) < next).length
|
|
4260
|
+
};
|
|
4177
4261
|
});
|
|
4178
4262
|
const rarities = season2?.rarities ?? [];
|
|
4179
4263
|
const plugin = strapi.plugin("tcg");
|
|
4180
4264
|
const [liveBoosts, rewards, drops] = season2 ? await Promise.all([
|
|
4181
4265
|
plugin.service("boost").liveNow(season2),
|
|
4182
4266
|
plugin.service("reward").list(season2, { includeInactive: false }),
|
|
4183
|
-
strapi.db.query(UID.DROP).findMany({
|
|
4267
|
+
strapi.db.query(UID.DROP).findMany({
|
|
4268
|
+
where: { pack: { season: season2.id }, isActive: true },
|
|
4269
|
+
populate: { pack: { select: ["name"] } }
|
|
4270
|
+
})
|
|
4184
4271
|
]) : [[], [], []];
|
|
4185
|
-
const lowStock = rewards.filter((r) => r.stock != null).map((r) => ({
|
|
4186
|
-
|
|
4272
|
+
const lowStock = rewards.filter((r) => r.stock != null).map((r) => ({
|
|
4273
|
+
id: r.documentId,
|
|
4274
|
+
name: r.name,
|
|
4275
|
+
remaining: Math.max(0, r.stock - (r.claimed ?? 0)),
|
|
4276
|
+
stock: r.stock
|
|
4277
|
+
})).filter((r) => r.remaining <= Math.max(3, Math.ceil(r.stock * 0.1))).sort((a, b) => a.remaining - b.remaining);
|
|
4278
|
+
const dropStates = drops.map((d) => ({
|
|
4279
|
+
id: d.documentId,
|
|
4280
|
+
name: `${d.pack?.name ?? ""} · ${d.name}`,
|
|
4281
|
+
mode: d.mode,
|
|
4282
|
+
scans: d.scanCount ?? 0,
|
|
4283
|
+
cap: d.maxScansTotal ?? null,
|
|
4284
|
+
isOpen: plugin.service("drop").isOpen(d)
|
|
4285
|
+
}));
|
|
4187
4286
|
return {
|
|
4188
4287
|
liveBoosts,
|
|
4189
4288
|
lowStock,
|
|
4190
4289
|
drops: dropStates,
|
|
4191
4290
|
season: season2 ? { id: season2.documentId, name: season2.name, slug: season2.slug, status: season2.status } : null,
|
|
4192
4291
|
players: { total: players, today: playersToday },
|
|
4193
|
-
pulls: {
|
|
4292
|
+
pulls: {
|
|
4293
|
+
today: pullsToday,
|
|
4294
|
+
total: pullsTotal,
|
|
4295
|
+
sparkline,
|
|
4296
|
+
distribution: distribution(weekPulls, rarities)
|
|
4297
|
+
},
|
|
4194
4298
|
trades: { today: tradesToday, pendingOffers },
|
|
4195
4299
|
redemptions: { pending: pendingRedemptions, fulfilledToday },
|
|
4196
4300
|
catalog: { cards, packs },
|
|
@@ -4207,21 +4311,75 @@ const stats = ({ strapi }) => ({
|
|
|
4207
4311
|
}))
|
|
4208
4312
|
};
|
|
4209
4313
|
},
|
|
4210
|
-
/**
|
|
4211
|
-
|
|
4212
|
-
|
|
4213
|
-
|
|
4214
|
-
|
|
4215
|
-
|
|
4314
|
+
/**
|
|
4315
|
+
* Every collector's standing in a season (utils/score.js), sorted. Computed from every player card of
|
|
4316
|
+
* the season and cached a few seconds: the rank is asked on every stats screen.
|
|
4317
|
+
* @returns {Promise<Array<{player: Object, uniqueCards: number, totalCards: number, cardScore: number, setsCompleted: number, setBonus: number, score: number}>>}
|
|
4318
|
+
*/
|
|
4319
|
+
async standings(season2) {
|
|
4320
|
+
const cached = STANDINGS_CACHE.get(season2.id);
|
|
4321
|
+
if (cached && Date.now() - cached.at < STANDINGS_TTL_MS) return cached.value;
|
|
4322
|
+
const plugin = strapi.plugin("tcg");
|
|
4323
|
+
const [rows, sets] = await Promise.all([
|
|
4324
|
+
strapi.db.query(UID.PLAYER_CARD).findMany({
|
|
4325
|
+
where: { card: { season: season2.id } },
|
|
4326
|
+
populate: {
|
|
4327
|
+
player: { select: ["id", "documentId", "code", "displayName"] },
|
|
4328
|
+
card: { select: ["id", "rarity", "score"] }
|
|
4329
|
+
}
|
|
4330
|
+
}),
|
|
4331
|
+
plugin.service("set").list(season2, { includeInactive: false })
|
|
4332
|
+
]);
|
|
4333
|
+
const limits = plugin.service("season").limitsOf(season2);
|
|
4216
4334
|
const byPlayer = /* @__PURE__ */ new Map();
|
|
4217
4335
|
for (const r of rows) {
|
|
4218
4336
|
if (!r.player || !r.card) continue;
|
|
4219
|
-
const entry = byPlayer.get(r.player.id) ?? { player: r.player,
|
|
4220
|
-
entry.
|
|
4337
|
+
const entry = byPlayer.get(r.player.id) ?? { player: r.player, cards: /* @__PURE__ */ new Map(), total: 0 };
|
|
4338
|
+
entry.cards.set(r.card.id, r.card);
|
|
4221
4339
|
entry.total += 1;
|
|
4222
4340
|
byPlayer.set(r.player.id, entry);
|
|
4223
4341
|
}
|
|
4224
|
-
|
|
4342
|
+
const value = [...byPlayer.values()].map((e) => ({
|
|
4343
|
+
player: e.player,
|
|
4344
|
+
...standingOf([...e.cards.values()], e.total, sets, season2, limits)
|
|
4345
|
+
})).sort(compareStandings);
|
|
4346
|
+
STANDINGS_CACHE.set(season2.id, { at: Date.now(), value });
|
|
4347
|
+
return value;
|
|
4348
|
+
},
|
|
4349
|
+
/** Forget the cached standings (after a grant, a trade…); they rebuild on the next read. */
|
|
4350
|
+
invalidateStandings(season2) {
|
|
4351
|
+
if (season2) STANDINGS_CACHE.delete(season2.id);
|
|
4352
|
+
else STANDINGS_CACHE.clear();
|
|
4353
|
+
},
|
|
4354
|
+
/** Where one player stands: rank among every collector, and the score's parts. Null rank with no card. */
|
|
4355
|
+
async standingFor(player2, season2) {
|
|
4356
|
+
const all = await this.standings(season2);
|
|
4357
|
+
const mine = all.find((s) => s.player.id === player2.id);
|
|
4358
|
+
if (!mine)
|
|
4359
|
+
return {
|
|
4360
|
+
rank: null,
|
|
4361
|
+
uniqueCards: 0,
|
|
4362
|
+
totalCards: 0,
|
|
4363
|
+
cardScore: 0,
|
|
4364
|
+
setsCompleted: 0,
|
|
4365
|
+
setBonus: 0,
|
|
4366
|
+
score: 0
|
|
4367
|
+
};
|
|
4368
|
+
return { rank: rankAmong(mine, all), ...mine, player: void 0 };
|
|
4369
|
+
},
|
|
4370
|
+
/** Top collectors by score (admin leaderboard). */
|
|
4371
|
+
async leaderboard(season2, limit = 10) {
|
|
4372
|
+
const all = await this.standings(season2);
|
|
4373
|
+
return all.slice(0, limit).map((e, i) => ({
|
|
4374
|
+
rank: i + 1,
|
|
4375
|
+
player: { id: e.player.documentId, code: e.player.code, displayName: e.player.displayName },
|
|
4376
|
+
score: e.score,
|
|
4377
|
+
cardScore: e.cardScore,
|
|
4378
|
+
setBonus: e.setBonus,
|
|
4379
|
+
setsCompleted: e.setsCompleted,
|
|
4380
|
+
uniqueCards: e.uniqueCards,
|
|
4381
|
+
totalCards: e.totalCards
|
|
4382
|
+
}));
|
|
4225
4383
|
},
|
|
4226
4384
|
/** Recent pulls for the audit screen. */
|
|
4227
4385
|
async pulls(season2, { playerCode, rarity }, page) {
|
|
@@ -4231,7 +4389,12 @@ const stats = ({ strapi }) => ({
|
|
|
4231
4389
|
const [items, total] = await Promise.all([
|
|
4232
4390
|
strapi.db.query(UID.PULL).findMany({
|
|
4233
4391
|
where,
|
|
4234
|
-
populate: {
|
|
4392
|
+
populate: {
|
|
4393
|
+
player: { select: ["code", "displayName"] },
|
|
4394
|
+
card: { select: ["name", "rarity", "number"] },
|
|
4395
|
+
pack: { select: ["name", "slug"] },
|
|
4396
|
+
drop: { select: ["name", "slug"] }
|
|
4397
|
+
},
|
|
4235
4398
|
orderBy: { pulledAt: "desc" },
|
|
4236
4399
|
offset: page.offset,
|
|
4237
4400
|
limit: page.limit
|
package/dist/server/index.mjs
CHANGED
|
@@ -157,7 +157,9 @@ const DEFAULT_LIMITS = Object.freeze({
|
|
|
157
157
|
qrTokenTtlMs: 5 * 60 * 1e3,
|
|
158
158
|
staticScanCooldownMs: 24 * 60 * 60 * 1e3,
|
|
159
159
|
pityRareAfter: 0,
|
|
160
|
-
pityLegendaryAfter: 0
|
|
160
|
+
pityLegendaryAfter: 0,
|
|
161
|
+
/** Points a completed set adds to the collector's score (utils/score.js). */
|
|
162
|
+
setCompletionBonus: 50
|
|
161
163
|
});
|
|
162
164
|
const DEFAULT_RANK_TIERS = Object.freeze([
|
|
163
165
|
{ key: "legend", label: "Legend", minPercent: 100 },
|
|
@@ -2702,6 +2704,39 @@ const qr = ({ strapi }) => ({
|
|
|
2702
2704
|
return count ?? 0;
|
|
2703
2705
|
}
|
|
2704
2706
|
});
|
|
2707
|
+
const rarityPoints = (season2, rarityKey) => {
|
|
2708
|
+
const table = season2?.rarities?.length ? season2.rarities : DEFAULT_RARITIES;
|
|
2709
|
+
const idx = table.findIndex((r) => r.key === rarityKey);
|
|
2710
|
+
const meta = table[idx] ?? DEFAULT_RARITIES.find((r) => r.key === rarityKey) ?? null;
|
|
2711
|
+
const min = Number(meta?.scoreMin);
|
|
2712
|
+
const max = Number(meta?.scoreMax);
|
|
2713
|
+
if (Number.isFinite(min) && Number.isFinite(max) && max >= min)
|
|
2714
|
+
return Math.round((min + max) / 2);
|
|
2715
|
+
return 10 + Math.max(0, idx) * 20;
|
|
2716
|
+
};
|
|
2717
|
+
const cardPoints = (card2, season2) => {
|
|
2718
|
+
const own = Number(card2?.score);
|
|
2719
|
+
if (Number.isFinite(own) && own > 0) return Math.round(own);
|
|
2720
|
+
return rarityPoints(season2, card2?.rarity);
|
|
2721
|
+
};
|
|
2722
|
+
const standingOf = (distinctCards, totalCards, sets, season2, limits = {}) => {
|
|
2723
|
+
const ownedIds = new Set(distinctCards.map((c) => String(c.id)));
|
|
2724
|
+
const cardScore = distinctCards.reduce((sum, c) => sum + cardPoints(c, season2), 0);
|
|
2725
|
+
const setsCompleted = (sets ?? []).filter(
|
|
2726
|
+
(s) => (s.cards ?? []).length > 0 && s.cards.every((c) => ownedIds.has(String(c.id)))
|
|
2727
|
+
).length;
|
|
2728
|
+
const bonus = Math.max(0, Number(limits.setCompletionBonus) || 0);
|
|
2729
|
+
return {
|
|
2730
|
+
uniqueCards: distinctCards.length,
|
|
2731
|
+
totalCards,
|
|
2732
|
+
cardScore,
|
|
2733
|
+
setsCompleted,
|
|
2734
|
+
setBonus: setsCompleted * bonus,
|
|
2735
|
+
score: cardScore + setsCompleted * bonus
|
|
2736
|
+
};
|
|
2737
|
+
};
|
|
2738
|
+
const compareStandings = (a, b) => b.score - a.score || b.uniqueCards - a.uniqueCards || b.totalCards - a.totalCards;
|
|
2739
|
+
const rankAmong = (mine, all) => 1 + all.filter((s) => compareStandings(s, mine) < 0).length;
|
|
2705
2740
|
const mediaUrl = (media) => media && typeof media === "object" ? media.url ?? null : null;
|
|
2706
2741
|
const format = () => ({
|
|
2707
2742
|
rarityMeta(season2, key) {
|
|
@@ -2720,6 +2755,7 @@ const format = () => ({
|
|
|
2720
2755
|
rarityColor: rarity.color,
|
|
2721
2756
|
edition: card2.edition ?? "standard",
|
|
2722
2757
|
score: card2.score ?? 0,
|
|
2758
|
+
points: cardPoints(card2, season2),
|
|
2723
2759
|
imageUrl: mediaUrl(card2.image),
|
|
2724
2760
|
attributes: card2.attributes ?? {},
|
|
2725
2761
|
lore: card2.lore ?? null,
|
|
@@ -2781,7 +2817,13 @@ const format = () => ({
|
|
|
2781
2817
|
},
|
|
2782
2818
|
/** Admin-only view: includes msisdn. */
|
|
2783
2819
|
playerAdmin(player2) {
|
|
2784
|
-
return player2 ? {
|
|
2820
|
+
return player2 ? {
|
|
2821
|
+
...this.player(player2),
|
|
2822
|
+
externalId: player2.externalId ?? null,
|
|
2823
|
+
msisdn: player2.msisdn ?? null,
|
|
2824
|
+
isActive: player2.isActive,
|
|
2825
|
+
meta: player2.meta ?? {}
|
|
2826
|
+
} : null;
|
|
2785
2827
|
},
|
|
2786
2828
|
playerCard(pc, season2) {
|
|
2787
2829
|
if (!pc) return null;
|
|
@@ -2809,7 +2851,16 @@ const format = () => ({
|
|
|
2809
2851
|
note: r.note ?? null,
|
|
2810
2852
|
player: r.player ? this.player(r.player) : void 0,
|
|
2811
2853
|
season: r.season ? { id: r.season.documentId, name: r.season.name, slug: r.season.slug } : void 0,
|
|
2812
|
-
reward: r.reward ? {
|
|
2854
|
+
reward: r.reward ? {
|
|
2855
|
+
id: r.reward.documentId,
|
|
2856
|
+
name: r.reward.name,
|
|
2857
|
+
slug: r.reward.slug,
|
|
2858
|
+
tier: r.reward.tier ?? null,
|
|
2859
|
+
booth: r.reward.booth ?? null,
|
|
2860
|
+
imageUrl: mediaUrl(r.reward.image),
|
|
2861
|
+
stock: r.reward.stock ?? null,
|
|
2862
|
+
claimed: r.reward.claimed ?? 0
|
|
2863
|
+
} : r.rewardName ? { name: r.rewardName } : null
|
|
2813
2864
|
};
|
|
2814
2865
|
}
|
|
2815
2866
|
});
|
|
@@ -3521,22 +3572,11 @@ const collection = ({ strapi }) => ({
|
|
|
3521
3572
|
return [...new Set(rows.map((r) => r.card?.id).filter(Boolean))];
|
|
3522
3573
|
},
|
|
3523
3574
|
/**
|
|
3524
|
-
* Global rank = 1 + number of
|
|
3525
|
-
*
|
|
3575
|
+
* Global rank = 1 + the number of collectors with a better standing: rarity-weighted score, then distinct
|
|
3576
|
+
* cards, then total cards (utils/score.js). Null before the first card.
|
|
3526
3577
|
*/
|
|
3527
|
-
async rank(player2, season2
|
|
3528
|
-
|
|
3529
|
-
const knex = strapi.db.connection;
|
|
3530
|
-
const md = strapi.db.metadata.get(UID.PLAYER_CARD);
|
|
3531
|
-
const pcTable = md.tableName;
|
|
3532
|
-
const playerLnk = md.attributes.player.joinTable;
|
|
3533
|
-
const cardLnk = md.attributes.card.joinTable;
|
|
3534
|
-
const cardMd = strapi.db.metadata.get(UID.CARD);
|
|
3535
|
-
const seasonLnk = cardMd.attributes.season.joinTable;
|
|
3536
|
-
const seasonId = season2.id;
|
|
3537
|
-
const sub = knex(`${pcTable} as pc`).join(`${playerLnk.name} as pl`, "pl." + playerLnk.joinColumn.name, "pc.id").join(`${cardLnk.name} as cl`, "cl." + cardLnk.joinColumn.name, "pc.id").join(`${seasonLnk.name} as sl`, "sl." + seasonLnk.joinColumn.name, "cl." + cardLnk.inverseJoinColumn.name).where("sl." + seasonLnk.inverseJoinColumn.name, seasonId).groupBy("pl." + playerLnk.inverseJoinColumn.name).select("pl." + playerLnk.inverseJoinColumn.name + " as player_id").countDistinct("cl." + cardLnk.inverseJoinColumn.name + " as unique_cards");
|
|
3538
|
-
const row = await knex.from(sub.as("ranked")).where("ranked.unique_cards", ">", myUnique).count("* as better").first();
|
|
3539
|
-
return Number(row?.better ?? 0) + 1;
|
|
3578
|
+
async rank(player2, season2) {
|
|
3579
|
+
return (await strapi.plugin("tcg").service("stats").standingFor(player2, season2)).rank;
|
|
3540
3580
|
},
|
|
3541
3581
|
async stats(player2, season2) {
|
|
3542
3582
|
const plugin = strapi.plugin("tcg");
|
|
@@ -3572,16 +3612,26 @@ const collection = ({ strapi }) => ({
|
|
|
3572
3612
|
populate: { pack: { select: ["id"] }, card: { select: ["id"] } }
|
|
3573
3613
|
});
|
|
3574
3614
|
const byPack = packs.map((p) => {
|
|
3575
|
-
const cardIds = [
|
|
3615
|
+
const cardIds = [
|
|
3616
|
+
...new Set(
|
|
3617
|
+
packRows.filter((r) => r.pack?.id === p.id).map((r) => r.card?.id).filter(Boolean)
|
|
3618
|
+
)
|
|
3619
|
+
];
|
|
3576
3620
|
const collected = cardIds.filter((id) => ownedIds.has(id)).length;
|
|
3577
|
-
return {
|
|
3621
|
+
return {
|
|
3622
|
+
pack: format2.pack(p),
|
|
3623
|
+
collected,
|
|
3624
|
+
total: cardIds.length,
|
|
3625
|
+
isComplete: cardIds.length > 0 && collected === cardIds.length
|
|
3626
|
+
};
|
|
3578
3627
|
});
|
|
3579
3628
|
const tier = plugin.service("season").tierFor(season2, completion);
|
|
3580
|
-
const [sets, badges, metrics, live] = await Promise.all([
|
|
3629
|
+
const [sets, badges, metrics, live, standing] = await Promise.all([
|
|
3581
3630
|
plugin.service("set").list(season2, { includeInactive: false }),
|
|
3582
3631
|
plugin.service("badge").forPlayer(player2, season2),
|
|
3583
3632
|
plugin.service("badge").metrics(player2, season2),
|
|
3584
|
-
plugin.service("boost").liveNow(season2)
|
|
3633
|
+
plugin.service("boost").liveNow(season2),
|
|
3634
|
+
plugin.service("stats").standingFor(player2, season2)
|
|
3585
3635
|
]);
|
|
3586
3636
|
return {
|
|
3587
3637
|
sets: plugin.service("set").progress(sets, ownedIds, season2),
|
|
@@ -3592,7 +3642,13 @@ const collection = ({ strapi }) => ({
|
|
|
3592
3642
|
uniqueCards: ownedIds.size,
|
|
3593
3643
|
catalogTotal,
|
|
3594
3644
|
completionPercentage: completion,
|
|
3595
|
-
globalRank:
|
|
3645
|
+
globalRank: standing.rank,
|
|
3646
|
+
score: standing.score,
|
|
3647
|
+
scoreBreakdown: {
|
|
3648
|
+
cards: standing.cardScore,
|
|
3649
|
+
setBonus: standing.setBonus,
|
|
3650
|
+
setsCompleted: standing.setsCompleted
|
|
3651
|
+
},
|
|
3596
3652
|
rankTier: { key: tier.key, label: tier.label },
|
|
3597
3653
|
season: format2.season(season2),
|
|
3598
3654
|
byRarity,
|
|
@@ -3606,7 +3662,14 @@ const collection = ({ strapi }) => ({
|
|
|
3606
3662
|
const mintNumber = await strapi.plugin("tcg").service("pull").claimSlot(UID.CARD, card2.id, "minted", null);
|
|
3607
3663
|
const pc = await strapi.db.transaction(async () => {
|
|
3608
3664
|
const row = await strapi.db.query(UID.PLAYER_CARD).create({
|
|
3609
|
-
data: {
|
|
3665
|
+
data: {
|
|
3666
|
+
player: player2.id,
|
|
3667
|
+
card: card2.id,
|
|
3668
|
+
pack: pack2?.id ?? null,
|
|
3669
|
+
acquiredFrom: "grant",
|
|
3670
|
+
acquiredAt: now,
|
|
3671
|
+
mintNumber
|
|
3672
|
+
}
|
|
3610
3673
|
});
|
|
3611
3674
|
await strapi.db.query(UID.PLAYER).update({ where: { id: player2.id }, data: { totalCards: (player2.totalCards ?? 0) + 1 } });
|
|
3612
3675
|
return row;
|
|
@@ -4121,6 +4184,8 @@ const redemption = ({ strapi }) => ({
|
|
|
4121
4184
|
return { items: items.map((r) => format2.redemption(r)), total };
|
|
4122
4185
|
}
|
|
4123
4186
|
});
|
|
4187
|
+
const STANDINGS_TTL_MS = 10 * 1e3;
|
|
4188
|
+
const STANDINGS_CACHE = /* @__PURE__ */ new Map();
|
|
4124
4189
|
const startOfToday = () => {
|
|
4125
4190
|
const d = /* @__PURE__ */ new Date();
|
|
4126
4191
|
d.setHours(0, 0, 0, 0);
|
|
@@ -4155,15 +4220,31 @@ const stats = ({ strapi }) => ({
|
|
|
4155
4220
|
strapi.db.query(UID.PULL).count({ where: { ...seasonWhere, pulledAt: { $gte: since } } }),
|
|
4156
4221
|
strapi.db.query(UID.PULL).count({ where: seasonWhere }),
|
|
4157
4222
|
strapi.db.query(UID.TRADE).count({ where: { ...seasonWhere, completedAt: { $gte: since } } }),
|
|
4158
|
-
strapi.db.query(UID.TRADE_OFFER).count({
|
|
4223
|
+
strapi.db.query(UID.TRADE_OFFER).count({
|
|
4224
|
+
where: { ...seasonWhere, status: TRADE_STATUS.PENDING, expiresAt: { $gt: /* @__PURE__ */ new Date() } }
|
|
4225
|
+
}),
|
|
4159
4226
|
strapi.db.query(UID.REDEMPTION).count({ where: { ...seasonWhere, status: REDEMPTION_STATUS.PENDING } }),
|
|
4160
|
-
strapi.db.query(UID.REDEMPTION).count({
|
|
4227
|
+
strapi.db.query(UID.REDEMPTION).count({
|
|
4228
|
+
where: {
|
|
4229
|
+
...seasonWhere,
|
|
4230
|
+
status: REDEMPTION_STATUS.FULFILLED,
|
|
4231
|
+
fulfilledAt: { $gte: since }
|
|
4232
|
+
}
|
|
4233
|
+
}),
|
|
4161
4234
|
season2 ? strapi.db.query(UID.CARD).count({ where: { season: season2.id, isActive: true } }) : Promise.resolve(0),
|
|
4162
4235
|
season2 ? strapi.db.query(UID.PACK).count({ where: { season: season2.id, isActive: true } }) : Promise.resolve(0),
|
|
4163
|
-
strapi.db.query(UID.PULL).findMany({
|
|
4236
|
+
strapi.db.query(UID.PULL).findMany({
|
|
4237
|
+
where: { ...seasonWhere, pulledAt: { $gte: week } },
|
|
4238
|
+
select: ["pulledAt", "rarity"]
|
|
4239
|
+
}),
|
|
4164
4240
|
strapi.db.query(UID.PULL).findMany({
|
|
4165
4241
|
where: seasonWhere,
|
|
4166
|
-
populate: {
|
|
4242
|
+
populate: {
|
|
4243
|
+
player: { select: ["code", "displayName"] },
|
|
4244
|
+
card: { select: ["name", "rarity"] },
|
|
4245
|
+
pack: { select: ["name"] },
|
|
4246
|
+
drop: { select: ["name"] }
|
|
4247
|
+
},
|
|
4167
4248
|
orderBy: { pulledAt: "desc" },
|
|
4168
4249
|
limit: 8
|
|
4169
4250
|
})
|
|
@@ -4172,24 +4253,47 @@ const stats = ({ strapi }) => ({
|
|
|
4172
4253
|
const day = daysAgo(6 - i);
|
|
4173
4254
|
const next = new Date(day);
|
|
4174
4255
|
next.setDate(day.getDate() + 1);
|
|
4175
|
-
return {
|
|
4256
|
+
return {
|
|
4257
|
+
day: day.toISOString().slice(0, 10),
|
|
4258
|
+
pulls: weekPulls.filter((p) => new Date(p.pulledAt) >= day && new Date(p.pulledAt) < next).length
|
|
4259
|
+
};
|
|
4176
4260
|
});
|
|
4177
4261
|
const rarities = season2?.rarities ?? [];
|
|
4178
4262
|
const plugin = strapi.plugin("tcg");
|
|
4179
4263
|
const [liveBoosts, rewards, drops] = season2 ? await Promise.all([
|
|
4180
4264
|
plugin.service("boost").liveNow(season2),
|
|
4181
4265
|
plugin.service("reward").list(season2, { includeInactive: false }),
|
|
4182
|
-
strapi.db.query(UID.DROP).findMany({
|
|
4266
|
+
strapi.db.query(UID.DROP).findMany({
|
|
4267
|
+
where: { pack: { season: season2.id }, isActive: true },
|
|
4268
|
+
populate: { pack: { select: ["name"] } }
|
|
4269
|
+
})
|
|
4183
4270
|
]) : [[], [], []];
|
|
4184
|
-
const lowStock = rewards.filter((r) => r.stock != null).map((r) => ({
|
|
4185
|
-
|
|
4271
|
+
const lowStock = rewards.filter((r) => r.stock != null).map((r) => ({
|
|
4272
|
+
id: r.documentId,
|
|
4273
|
+
name: r.name,
|
|
4274
|
+
remaining: Math.max(0, r.stock - (r.claimed ?? 0)),
|
|
4275
|
+
stock: r.stock
|
|
4276
|
+
})).filter((r) => r.remaining <= Math.max(3, Math.ceil(r.stock * 0.1))).sort((a, b) => a.remaining - b.remaining);
|
|
4277
|
+
const dropStates = drops.map((d) => ({
|
|
4278
|
+
id: d.documentId,
|
|
4279
|
+
name: `${d.pack?.name ?? ""} · ${d.name}`,
|
|
4280
|
+
mode: d.mode,
|
|
4281
|
+
scans: d.scanCount ?? 0,
|
|
4282
|
+
cap: d.maxScansTotal ?? null,
|
|
4283
|
+
isOpen: plugin.service("drop").isOpen(d)
|
|
4284
|
+
}));
|
|
4186
4285
|
return {
|
|
4187
4286
|
liveBoosts,
|
|
4188
4287
|
lowStock,
|
|
4189
4288
|
drops: dropStates,
|
|
4190
4289
|
season: season2 ? { id: season2.documentId, name: season2.name, slug: season2.slug, status: season2.status } : null,
|
|
4191
4290
|
players: { total: players, today: playersToday },
|
|
4192
|
-
pulls: {
|
|
4291
|
+
pulls: {
|
|
4292
|
+
today: pullsToday,
|
|
4293
|
+
total: pullsTotal,
|
|
4294
|
+
sparkline,
|
|
4295
|
+
distribution: distribution(weekPulls, rarities)
|
|
4296
|
+
},
|
|
4193
4297
|
trades: { today: tradesToday, pendingOffers },
|
|
4194
4298
|
redemptions: { pending: pendingRedemptions, fulfilledToday },
|
|
4195
4299
|
catalog: { cards, packs },
|
|
@@ -4206,21 +4310,75 @@ const stats = ({ strapi }) => ({
|
|
|
4206
4310
|
}))
|
|
4207
4311
|
};
|
|
4208
4312
|
},
|
|
4209
|
-
/**
|
|
4210
|
-
|
|
4211
|
-
|
|
4212
|
-
|
|
4213
|
-
|
|
4214
|
-
|
|
4313
|
+
/**
|
|
4314
|
+
* Every collector's standing in a season (utils/score.js), sorted. Computed from every player card of
|
|
4315
|
+
* the season and cached a few seconds: the rank is asked on every stats screen.
|
|
4316
|
+
* @returns {Promise<Array<{player: Object, uniqueCards: number, totalCards: number, cardScore: number, setsCompleted: number, setBonus: number, score: number}>>}
|
|
4317
|
+
*/
|
|
4318
|
+
async standings(season2) {
|
|
4319
|
+
const cached = STANDINGS_CACHE.get(season2.id);
|
|
4320
|
+
if (cached && Date.now() - cached.at < STANDINGS_TTL_MS) return cached.value;
|
|
4321
|
+
const plugin = strapi.plugin("tcg");
|
|
4322
|
+
const [rows, sets] = await Promise.all([
|
|
4323
|
+
strapi.db.query(UID.PLAYER_CARD).findMany({
|
|
4324
|
+
where: { card: { season: season2.id } },
|
|
4325
|
+
populate: {
|
|
4326
|
+
player: { select: ["id", "documentId", "code", "displayName"] },
|
|
4327
|
+
card: { select: ["id", "rarity", "score"] }
|
|
4328
|
+
}
|
|
4329
|
+
}),
|
|
4330
|
+
plugin.service("set").list(season2, { includeInactive: false })
|
|
4331
|
+
]);
|
|
4332
|
+
const limits = plugin.service("season").limitsOf(season2);
|
|
4215
4333
|
const byPlayer = /* @__PURE__ */ new Map();
|
|
4216
4334
|
for (const r of rows) {
|
|
4217
4335
|
if (!r.player || !r.card) continue;
|
|
4218
|
-
const entry = byPlayer.get(r.player.id) ?? { player: r.player,
|
|
4219
|
-
entry.
|
|
4336
|
+
const entry = byPlayer.get(r.player.id) ?? { player: r.player, cards: /* @__PURE__ */ new Map(), total: 0 };
|
|
4337
|
+
entry.cards.set(r.card.id, r.card);
|
|
4220
4338
|
entry.total += 1;
|
|
4221
4339
|
byPlayer.set(r.player.id, entry);
|
|
4222
4340
|
}
|
|
4223
|
-
|
|
4341
|
+
const value = [...byPlayer.values()].map((e) => ({
|
|
4342
|
+
player: e.player,
|
|
4343
|
+
...standingOf([...e.cards.values()], e.total, sets, season2, limits)
|
|
4344
|
+
})).sort(compareStandings);
|
|
4345
|
+
STANDINGS_CACHE.set(season2.id, { at: Date.now(), value });
|
|
4346
|
+
return value;
|
|
4347
|
+
},
|
|
4348
|
+
/** Forget the cached standings (after a grant, a trade…); they rebuild on the next read. */
|
|
4349
|
+
invalidateStandings(season2) {
|
|
4350
|
+
if (season2) STANDINGS_CACHE.delete(season2.id);
|
|
4351
|
+
else STANDINGS_CACHE.clear();
|
|
4352
|
+
},
|
|
4353
|
+
/** Where one player stands: rank among every collector, and the score's parts. Null rank with no card. */
|
|
4354
|
+
async standingFor(player2, season2) {
|
|
4355
|
+
const all = await this.standings(season2);
|
|
4356
|
+
const mine = all.find((s) => s.player.id === player2.id);
|
|
4357
|
+
if (!mine)
|
|
4358
|
+
return {
|
|
4359
|
+
rank: null,
|
|
4360
|
+
uniqueCards: 0,
|
|
4361
|
+
totalCards: 0,
|
|
4362
|
+
cardScore: 0,
|
|
4363
|
+
setsCompleted: 0,
|
|
4364
|
+
setBonus: 0,
|
|
4365
|
+
score: 0
|
|
4366
|
+
};
|
|
4367
|
+
return { rank: rankAmong(mine, all), ...mine, player: void 0 };
|
|
4368
|
+
},
|
|
4369
|
+
/** Top collectors by score (admin leaderboard). */
|
|
4370
|
+
async leaderboard(season2, limit = 10) {
|
|
4371
|
+
const all = await this.standings(season2);
|
|
4372
|
+
return all.slice(0, limit).map((e, i) => ({
|
|
4373
|
+
rank: i + 1,
|
|
4374
|
+
player: { id: e.player.documentId, code: e.player.code, displayName: e.player.displayName },
|
|
4375
|
+
score: e.score,
|
|
4376
|
+
cardScore: e.cardScore,
|
|
4377
|
+
setBonus: e.setBonus,
|
|
4378
|
+
setsCompleted: e.setsCompleted,
|
|
4379
|
+
uniqueCards: e.uniqueCards,
|
|
4380
|
+
totalCards: e.totalCards
|
|
4381
|
+
}));
|
|
4224
4382
|
},
|
|
4225
4383
|
/** Recent pulls for the audit screen. */
|
|
4226
4384
|
async pulls(season2, { playerCode, rarity }, page) {
|
|
@@ -4230,7 +4388,12 @@ const stats = ({ strapi }) => ({
|
|
|
4230
4388
|
const [items, total] = await Promise.all([
|
|
4231
4389
|
strapi.db.query(UID.PULL).findMany({
|
|
4232
4390
|
where,
|
|
4233
|
-
populate: {
|
|
4391
|
+
populate: {
|
|
4392
|
+
player: { select: ["code", "displayName"] },
|
|
4393
|
+
card: { select: ["name", "rarity", "number"] },
|
|
4394
|
+
pack: { select: ["name", "slug"] },
|
|
4395
|
+
drop: { select: ["name", "slug"] }
|
|
4396
|
+
},
|
|
4234
4397
|
orderBy: { pulledAt: "desc" },
|
|
4235
4398
|
offset: page.offset,
|
|
4236
4399
|
limit: page.limit
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sonatel-os/strapi-plugin-tcg",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.1",
|
|
4
4
|
"description": "A full trading-card-game module for Strapi v5: seasons, packs, cards, pulls, trades, redemptions — with admin screens and RBAC.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "LPIX-11 <mohamed.johnson@orange-sonatel.com>",
|