Skip to content

Commit 6708bae

Browse files
authored
feat(mod): ranked threshold and comp threshold can be modified
1 parent cb80f68 commit 6708bae

File tree

8 files changed

+241
-18
lines changed

8 files changed

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

566662
module.exports = router;

routes/game.js

Lines changed: 15 additions & 10 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);
@@ -1298,4 +1303,4 @@ const settingsChecks = {
12981303
},
12991304
};
13001305

1301-
module.exports = router;
1306+
module.exports = router;

0 commit comments

Comments
 (0)