Skip to content

Commit 07f0b0f

Browse files
committed
feat(mod): control over mingame threshold + point threshold
1 parent cb80f68 commit 07f0b0f

File tree

8 files changed

+241
-16
lines changed

8 files changed

+241
-16
lines changed

data/constants.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -468,6 +468,7 @@ module.exports = {
468468
editAnySetup: true,
469469
createPoll: true,
470470
manageCompetitive: true,
471+
adjustMinGames: true,
471472
awardTrophy: true,
472473
deleteStrategy: true,
473474
seeModPanel: true,

db/schemas.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -733,6 +733,7 @@ var schemas = {
733733
dateCompleted: { type: String }, // YYYY-MM-DD (inclusive)
734734
remainingOpenDays: { type: Number, required: true },
735735
remainingReviewDays: { type: Number, required: true },
736+
minimumPoints: { type: Number, default: 150 },
736737
}),
737738
CompetitiveGameCompletion: new mongoose.Schema({
738739
userId: { type: String },

modules/redis.js

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1471,6 +1471,20 @@ async function getDeletedVanityUrlUserId(vanityUrl) {
14711471
return await client.getAsync(key);
14721472
}
14731473

1474+
async function getMinimumGamesForRanked() {
1475+
const key = "setting:minimumGamesForRanked";
1476+
const value = await client.getAsync(key);
1477+
if (value === null) {
1478+
return constants.minimumGamesForRanked;
1479+
}
1480+
return parseInt(value, 10);
1481+
}
1482+
1483+
async function setMinimumGamesForRanked(value) {
1484+
const key = "setting:minimumGamesForRanked";
1485+
await client.setAsync(key, value);
1486+
}
1487+
14741488
module.exports = {
14751489
client,
14761490
getUserDbId,
@@ -1547,4 +1561,6 @@ module.exports = {
15471561
rateLimit,
15481562
cacheDeletedVanityUrl,
15491563
getDeletedVanityUrlUserId,
1564+
getMinimumGamesForRanked,
1565+
setMinimumGamesForRanked,
15501566
};

react_main/src/pages/Policy/Moderation/CompetitiveManagement.jsx

Lines changed: 50 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,11 @@ import Setup from "components/Setup";
1919

2020
import { COMMAND_COLOR } from "./commands";
2121

22+
const DEFAULT_MINIMUM_POINTS = 150;
23+
2224
export default function CompetitiveManagement() {
2325
const [seasonData, setSeasonData] = useState(null);
26+
const [roundSettings, setRoundSettings] = useState({});
2427
const [loading, setLoading] = useState(false);
2528
const [saving, setSaving] = useState(false);
2629
const [addSetupDialogOpen, setAddSetupDialogOpen] = useState(false);
@@ -53,6 +56,7 @@ export default function CompetitiveManagement() {
5356
return;
5457
}
5558
setSeasonData(response.data);
59+
setRoundSettings(response.data.roundSettings || {});
5660
setLoading(false);
5761
} else {
5862
errorAlert("Invalid season data received.");
@@ -100,20 +104,42 @@ export default function CompetitiveManagement() {
100104
}
101105
};
102106

107+
const handleMinimumPointsChange = (roundNumber, value) => {
108+
const numValue = parseInt(value, 10);
109+
setRoundSettings((prev) => ({
110+
...prev,
111+
[roundNumber]: {
112+
...prev[roundNumber],
113+
minimumPoints: isNaN(numValue) ? DEFAULT_MINIMUM_POINTS : numValue,
114+
},
115+
}));
116+
};
117+
103118
const handleSave = () => {
104119
if (!seasonData) return;
105120

106121
setSaving(true);
107-
axios
108-
.post("/api/competitive/updateSetupOrder", {
109-
setupOrder: seasonData.setupOrder,
110-
})
122+
123+
const setupOrderPromise = axios.post("/api/competitive/updateSetupOrder", {
124+
setupOrder: seasonData.setupOrder,
125+
});
126+
127+
const roundSettingsPromise = axios.post(
128+
"/api/competitive/updateRoundSettings",
129+
{
130+
roundSettings: roundSettings,
131+
}
132+
);
133+
134+
Promise.all([setupOrderPromise, roundSettingsPromise])
111135
.then(() => {
112-
siteInfo.showAlert("Setup order updated successfully.", "success");
136+
siteInfo.showAlert("Changes saved successfully.", "success");
113137
setSaving(false);
114138
})
115139
.catch((error) => {
116-
errorAlert("Failed to update setup order.");
140+
errorAlert(
141+
error.response?.data || "Failed to save changes."
142+
);
117143
setSaving(false);
118144
});
119145
};
@@ -151,6 +177,7 @@ export default function CompetitiveManagement() {
151177
response.data.setupOrder
152178
) {
153179
setSeasonData(response.data);
180+
setRoundSettings(response.data.roundSettings || {});
154181
setAddSetupDialogOpen(false);
155182
setSetupIdToAdd("");
156183
setAddingSetup(false);
@@ -239,6 +266,23 @@ export default function CompetitiveManagement() {
239266
<i className="fas fa-plus" />
240267
</IconButton>
241268
</Stack>
269+
<TextField
270+
label="Minimum Points"
271+
type="number"
272+
size="small"
273+
value={
274+
roundSettings[roundIndex + 1]?.minimumPoints ??
275+
DEFAULT_MINIMUM_POINTS
276+
}
277+
onChange={(e) =>
278+
handleMinimumPointsChange(
279+
roundIndex + 1,
280+
e.target.value
281+
)
282+
}
283+
inputProps={{ min: 0 }}
284+
sx={{ mb: 1, maxWidth: 200 }}
285+
/>
242286
<Stack direction="column" spacing={1}>
243287
{roundSetups && roundSetups.length > 0 ? (
244288
roundSetups.map((setupNumber, setupIndex) => {

react_main/src/pages/Policy/Moderation/commands.js

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -914,6 +914,29 @@ export function useModCommands(argValues, commandRan, setResults) {
914914
.catch(errorAlert);
915915
},
916916
},
917+
"Adjust Minimum Games Threshold": {
918+
perm: "adjustMinGames",
919+
category: "Site Management",
920+
args: [
921+
{
922+
label: "Minimum Games for Ranked",
923+
name: "value",
924+
type: "number",
925+
},
926+
],
927+
run: function () {
928+
axios
929+
.post("/api/mod/minGamesThreshold", argValues)
930+
.then((res) => {
931+
siteInfo.showAlert(
932+
`Minimum games for ranked set to ${res.data.minimumGamesForRanked}.`,
933+
"success"
934+
);
935+
commandRan();
936+
})
937+
.catch(errorAlert);
938+
},
939+
},
917940
"Toggle Ranked Setup": {
918941
perm: "approveRanked",
919942
category: "Setup Management",

routes/competitive.js

Lines changed: 98 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -314,7 +314,7 @@ router.get("/current", async function (req, res) {
314314
completed: false,
315315
})
316316
.sort({ number: -1 })
317-
.select("setups setupOrder number")
317+
.select("setups setupOrder number numRounds")
318318
.populate([
319319
{
320320
path: "setups",
@@ -341,10 +341,29 @@ router.get("/current", async function (req, res) {
341341
return;
342342
}
343343

344+
// Fetch round settings for all rounds (filter out round 0 if it exists)
345+
const rounds = await models.CompetitiveRound.find({
346+
season: currentSeason.number,
347+
number: { $gt: 0 },
348+
})
349+
.select("number minimumPoints")
350+
.sort({ number: 1 })
351+
.lean();
352+
353+
// Build roundSettings object keyed by round number (1-indexed)
354+
const roundSettings = {};
355+
for (const round of rounds) {
356+
roundSettings[round.number] = {
357+
minimumPoints: round.minimumPoints ?? constants.minimumPointsForCompetitive,
358+
};
359+
}
360+
344361
res.json({
345362
seasonNumber: currentSeason.number,
346363
setups: currentSeason.setups,
347364
setupOrder: currentSeason.setupOrder,
365+
numRounds: currentSeason.numRounds,
366+
roundSettings: roundSettings,
348367
});
349368
} catch (e) {
350369
logger.error(e);
@@ -563,4 +582,82 @@ router.post("/updateSetupOrder", async function (req, res) {
563582
}
564583
});
565584

585+
// Update round settings (minimumPoints) for the current season
586+
router.post("/updateRoundSettings", async function (req, res) {
587+
try {
588+
var userId = await routeUtils.verifyLoggedIn(req);
589+
590+
if (!(await routeUtils.verifyPermission(res, userId, "manageCompetitive")))
591+
return;
592+
593+
const roundSettings = req.body.roundSettings;
594+
595+
if (!roundSettings || typeof roundSettings !== "object") {
596+
res.status(400);
597+
res.send("roundSettings must be an object.");
598+
return;
599+
}
600+
601+
const currentSeason = await models.CompetitiveSeason.findOne({
602+
completed: false,
603+
})
604+
.sort({ number: -1 })
605+
.select("number numRounds")
606+
.lean();
607+
608+
if (!currentSeason) {
609+
res.status(404);
610+
res.send("No season in progress.");
611+
return;
612+
}
613+
614+
// Get existing rounds for this season
615+
const existingRounds = await models.CompetitiveRound.find({
616+
season: currentSeason.number,
617+
})
618+
.select("number")
619+
.lean();
620+
const existingRoundNumbers = new Set(existingRounds.map((r) => r.number));
621+
622+
// Validate and update each round's settings
623+
for (const [roundNumberStr, settings] of Object.entries(roundSettings)) {
624+
const roundNumber = Number.parseInt(roundNumberStr);
625+
626+
if (isNaN(roundNumber) || roundNumber < 1 || roundNumber > currentSeason.numRounds) {
627+
res.status(400);
628+
res.send(`Invalid round number: ${roundNumberStr}`);
629+
return;
630+
}
631+
632+
// Skip rounds that haven't been created yet
633+
if (!existingRoundNumbers.has(roundNumber)) {
634+
continue;
635+
}
636+
637+
if (settings.minimumPoints !== undefined) {
638+
const minPoints = Number.parseInt(settings.minimumPoints);
639+
if (isNaN(minPoints) || minPoints < 0) {
640+
res.status(400);
641+
res.send(`Invalid minimumPoints for round ${roundNumber}: must be a non-negative number.`);
642+
return;
643+
}
644+
645+
// Update the round document
646+
await models.CompetitiveRound.updateOne(
647+
{ season: currentSeason.number, number: roundNumber },
648+
{ $set: { minimumPoints: minPoints } }
649+
);
650+
}
651+
}
652+
653+
routeUtils.createModAction(userId, "Update Competitive Round Settings", []);
654+
655+
res.sendStatus(200);
656+
} catch (e) {
657+
logger.error(e);
658+
res.status(500);
659+
res.send("Error updating round settings.");
660+
}
661+
});
662+
566663
module.exports = router;

routes/game.js

Lines changed: 14 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,11 @@ const logger = require("../modules/logging")(".");
99
const router = express.Router();
1010
const axios = require("axios");
1111

12-
async function userCanPlayCompetitive(userId) {
12+
async function userCanPlayCompetitive(userId, minimumPoints = constants.minimumPointsForCompetitive) {
1313
const user = await redis.getUserInfo(userId);
1414

15-
if (!user || user.points < constants.minimumPointsForCompetitive) {
16-
return `You cannot play competitive games until you've earned ${constants.minimumPointsForCompetitive} fortune.`;
15+
if (!user || user.points < minimumPoints) {
16+
return `You cannot play competitive games until you've earned ${minimumPoints} fortune.`;
1717
}
1818

1919
if (!user || user.goldHearts <= 0) {
@@ -252,11 +252,12 @@ router.get("/:id/connect", async function (req, res) {
252252
// Ranked checks
253253
if (userId && game.settings.ranked && !isSpectating) {
254254
const user = await redis.getUserInfo(userId);
255+
const minGamesForRanked = await redis.getMinimumGamesForRanked();
255256

256-
if (!user || user.gamesPlayed < constants.minimumGamesForRanked) {
257+
if (!user || user.gamesPlayed < minGamesForRanked) {
257258
res.status(400);
258259
res.send(
259-
`You cannot play ranked games until you've played ${constants.minimumGamesForRanked} games.`
260+
`You cannot play ranked games until you've played ${minGamesForRanked} games.`
260261
);
261262
return;
262263
}
@@ -272,7 +273,9 @@ router.get("/:id/connect", async function (req, res) {
272273

273274
// Competitive checks
274275
if (userId && game.settings.competitive && !isSpectating) {
275-
const failureReason = await userCanPlayCompetitive(userId);
276+
const roundInfo = await redis.getCompRoundInfo();
277+
const minimumPoints = roundInfo?.round?.minimumPoints ?? constants.minimumPointsForCompetitive;
278+
const failureReason = await userCanPlayCompetitive(userId, minimumPoints);
276279
if (failureReason) {
277280
res.status(400);
278281
res.send(failureReason);
@@ -704,10 +707,11 @@ router.post("/host", async function (req, res) {
704707

705708
const user = await redis.getUserInfo(userId);
706709
if (req.body.ranked) {
707-
if (user && user.gamesPlayed < constants.minimumGamesForRanked) {
710+
const minGamesForRanked = await redis.getMinimumGamesForRanked();
711+
if (user && user.gamesPlayed < minGamesForRanked) {
708712
res.status(400);
709713
res.send(
710-
`You cannot play ranked games until you've played ${constants.minimumGamesForRanked} games.`
714+
`You cannot play ranked games until you've played ${minGamesForRanked} games.`
711715
);
712716
return;
713717
}
@@ -722,7 +726,8 @@ router.post("/host", async function (req, res) {
722726
}
723727

724728
if (userId && req.body.competitive) {
725-
const failureReason = await userCanPlayCompetitive(userId);
729+
const minimumPoints = roundInfo?.round?.minimumPoints ?? constants.minimumPointsForCompetitive;
730+
const failureReason = await userCanPlayCompetitive(userId, minimumPoints);
726731
if (failureReason) {
727732
res.status(400);
728733
res.send(failureReason);

0 commit comments

Comments
 (0)