Add Void difficulty and publish changelogs
Some checks failed
Deploy MineGeo to Dokploy / deploy (push) Failing after 3m40s
Some checks failed
Deploy MineGeo to Dokploy / deploy (push) Failing after 3m40s
This commit is contained in:
51
shared/changelog.ts
Normal file
51
shared/changelog.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
export const RELEASE_NEWS_CHANNEL = "1558048830770122772";
|
||||
// A webhook ID is public; its secret URL/token must never be stored here.
|
||||
export const RELEASE_WEBHOOK_ID = "1561021045211660401";
|
||||
|
||||
export type ReleasePost = {
|
||||
id: string;
|
||||
channel_id: string;
|
||||
webhook_id?: string;
|
||||
content: string;
|
||||
timestamp: string;
|
||||
};
|
||||
export type ChangelogEntry = {
|
||||
id: string;
|
||||
title: string;
|
||||
version: string;
|
||||
kind: "Alpha" | "Beta" | "Production" | "Bug fix";
|
||||
body: string;
|
||||
publishedAt: string;
|
||||
};
|
||||
|
||||
export function parseReleasePost(
|
||||
post: ReleasePost,
|
||||
webhookId = RELEASE_WEBHOOK_ID,
|
||||
): ChangelogEntry | null {
|
||||
if (post.webhook_id !== webhookId || !/^\d{17,20}$/.test(post.id))
|
||||
return null;
|
||||
const clean = post.content
|
||||
.replace(/<@&\d+>/g, "")
|
||||
.replace(/(?:^|\n)\s*PLAY THE MINEGEO AT\s*:[\s\S]*$/i, "")
|
||||
.replace(/^[\s,.]+/, "")
|
||||
.trim();
|
||||
const header =
|
||||
/^#{1,3}[ \t]+MineGeo[ \t]+(\d+\.\d+\.\d+(?:\.\d+)?)[ \t]+(Alpha|Beta|Production|Bug[ -]fix)([ \t]+[—–:-][^\r\n]*)?[ \t]*\r?\n/i.exec(
|
||||
clean,
|
||||
);
|
||||
if (!header || !Number.isFinite(Date.parse(post.timestamp))) return null;
|
||||
const body = clean.slice(header[0].length).trim();
|
||||
if (!body) return null;
|
||||
const kind = /^bug/i.test(header[2])
|
||||
? "Bug fix"
|
||||
: ((header[2][0].toUpperCase() +
|
||||
header[2].slice(1).toLowerCase()) as ChangelogEntry["kind"]);
|
||||
return {
|
||||
id: post.id,
|
||||
title: `MineGeo ${header[1]} ${kind}${header[3]?.trimEnd() ?? ""}`,
|
||||
version: header[1],
|
||||
kind,
|
||||
body,
|
||||
publishedAt: post.timestamp,
|
||||
};
|
||||
}
|
||||
@@ -238,6 +238,8 @@ export type Database = {
|
||||
compass: boolean;
|
||||
coordinates: boolean;
|
||||
map_zoom: boolean;
|
||||
map_pan: boolean;
|
||||
timer_visible: boolean;
|
||||
scoring: "normal" | "strict" | "gentle";
|
||||
created_at: string;
|
||||
id: string;
|
||||
@@ -250,6 +252,8 @@ export type Database = {
|
||||
compass?: boolean;
|
||||
coordinates?: boolean;
|
||||
map_zoom?: boolean;
|
||||
map_pan?: boolean;
|
||||
timer_visible?: boolean;
|
||||
scoring?: "normal" | "strict" | "gentle";
|
||||
created_at?: string;
|
||||
id?: string;
|
||||
@@ -262,6 +266,8 @@ export type Database = {
|
||||
compass?: boolean;
|
||||
coordinates?: boolean;
|
||||
map_zoom?: boolean;
|
||||
map_pan?: boolean;
|
||||
timer_visible?: boolean;
|
||||
scoring?: "normal" | "strict" | "gentle";
|
||||
name?: string;
|
||||
seconds?: number;
|
||||
|
||||
@@ -190,7 +190,10 @@ export const developerOpenApi = {
|
||||
type: "object",
|
||||
properties: {
|
||||
id,
|
||||
difficulty: { type: "string", enum: ["easy", "normal", "hard"] },
|
||||
difficulty: {
|
||||
type: "string",
|
||||
enum: ["easy", "normal", "hard", "void"],
|
||||
},
|
||||
createdAt: date,
|
||||
currentRound: { type: "integer", minimum: 0, maximum: 5 },
|
||||
complete: { type: "boolean" },
|
||||
|
||||
@@ -3,6 +3,7 @@ export const leaderboardDifficulties = [
|
||||
"easy",
|
||||
"normal",
|
||||
"hard",
|
||||
"void",
|
||||
] as const;
|
||||
export type LeaderboardDifficulty = (typeof leaderboardDifficulties)[number];
|
||||
export type LeaderboardEntry =
|
||||
|
||||
@@ -1046,5 +1046,24 @@
|
||||
"Minecraft map. Click to place a guess, or use arrow keys to pan and Enter to pin the center.": "Карта Minecraft. Нажмите для метки или перемещайтесь стрелками и нажмите Enter для метки в центре.",
|
||||
"Strict scoring penalizes distance more; gentle scoring gives more credit for nearby guesses. Every game still has five rounds.": "Строгий подсчёт сильнее штрафует за расстояние, мягкий даёт больше очков за близкие метки. В игре по-прежнему пять раундов.",
|
||||
"Guess selected": "Метка выбрана",
|
||||
"Map": "Карта"
|
||||
"Map": "Карта",
|
||||
"Void": "Пустота",
|
||||
"Hardest · No compass · Fixed map": "Самая сложная · Без компаса · Фиксированная карта",
|
||||
"Allow guess-map panning": "Разрешить перемещение карты",
|
||||
"Show round countdown": "Показывать обратный отсчёт",
|
||||
"Minecraft map. Click to place a guess, or press Enter to pin the center.": "Карта Minecraft. Нажмите для метки или нажмите Enter, чтобы поставить метку в центре.",
|
||||
"Easy gives you 180 seconds, Normal 90, and Hard 30. Void is the hardest: just 10 seconds, a fixed panorama and map, no zoom, no compass or coordinates, and strict scoring. A timed-out round scores zero; you can still finish the expedition.": "Лёгкая сложность даёт 180 секунд, обычная — 90, высокая — 30. Пустота — самая сложная: 10 секунд, фиксированные панорама и карта, без масштабирования, компаса и координат, со строгим подсчётом очков. За истёкший раунд начисляется ноль очков; экспедицию можно продолжить.",
|
||||
"Meet MineGeo’s top explorers. Compare the best five-round scores across Easy, Normal, Hard, and Void Minecraft expeditions.": "Лучшие исследователи MineGeo. Сравнивайте результаты пяти раундов на лёгкой, обычной, высокой сложности и в Пустоте.",
|
||||
"Changelogs": "История обновлений",
|
||||
"WHAT'S NEW": "ЧТО НОВОГО",
|
||||
"New releases, improvements, and fixes from MineGeo.": "Новые версии, улучшения и исправления MineGeo.",
|
||||
"Latest releases": "Последние обновления",
|
||||
"Release notes are temporarily unavailable": "История обновлений временно недоступна",
|
||||
"Please check back soon.": "Пожалуйста, загляните позже.",
|
||||
"No releases on this page yet.": "На этой странице пока нет обновлений.",
|
||||
"Older posts": "Более ранние записи",
|
||||
"Alpha": "Альфа",
|
||||
"Beta": "Бета",
|
||||
"Production": "Релиз",
|
||||
"Bug fix": "Исправления ошибок"
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ export const createRoomSchema = z
|
||||
.object({
|
||||
datasetId: z.string().uuid(),
|
||||
difficulty: z.union([
|
||||
z.enum(["easy", "normal", "hard"]),
|
||||
z.enum(["easy", "normal", "hard", "void"]),
|
||||
z.string().uuid(),
|
||||
]),
|
||||
playerLimit: z.number().int().min(2).max(16),
|
||||
|
||||
@@ -65,7 +65,7 @@ export const metricDefinitions = {
|
||||
minegeo_games: {
|
||||
help: "Retained solo games by difficulty and completion state.",
|
||||
labels: {
|
||||
difficulty: ["easy", "normal", "hard"],
|
||||
difficulty: ["easy", "normal", "hard", "void"],
|
||||
status: ["incomplete", "complete"],
|
||||
},
|
||||
},
|
||||
|
||||
@@ -183,6 +183,18 @@ export const difficulties = {
|
||||
easy: { seconds: 180, zoom: true, label: "Easy" },
|
||||
normal: { seconds: 90, zoom: true, label: "Normal" },
|
||||
hard: { seconds: 30, zoom: false, label: "Hard" },
|
||||
void: {
|
||||
seconds: 10,
|
||||
zoom: false,
|
||||
label: "Void",
|
||||
pan: false,
|
||||
compass: false,
|
||||
coordinates: false,
|
||||
mapZoom: false,
|
||||
mapPan: false,
|
||||
timerVisible: true,
|
||||
scoring: "strict",
|
||||
},
|
||||
} as const;
|
||||
export type Difficulty = keyof typeof difficulties;
|
||||
export const difficultyRuleDefaults = {
|
||||
@@ -190,6 +202,8 @@ export const difficultyRuleDefaults = {
|
||||
compass: true,
|
||||
coordinates: true,
|
||||
mapZoom: true,
|
||||
mapPan: true,
|
||||
timerVisible: true,
|
||||
scoring: "normal" as const,
|
||||
};
|
||||
export type ScoringMode = "normal" | "strict" | "gentle";
|
||||
@@ -203,6 +217,8 @@ export type DifficultyPreset = {
|
||||
compass?: boolean;
|
||||
coordinates?: boolean;
|
||||
mapZoom?: boolean;
|
||||
mapPan?: boolean;
|
||||
timerVisible?: boolean;
|
||||
scoring?: ScoringMode;
|
||||
};
|
||||
export const customDifficultySchema = z
|
||||
@@ -222,14 +238,16 @@ export const customDifficultySchema = z
|
||||
seconds: z
|
||||
.number()
|
||||
.int()
|
||||
.min(15)
|
||||
.min(5)
|
||||
.max(600)
|
||||
.refine((seconds) => seconds % 15 === 0),
|
||||
.refine((seconds) => seconds % 5 === 0),
|
||||
zoom: z.boolean(),
|
||||
pan: z.boolean().default(true),
|
||||
compass: z.boolean().default(true),
|
||||
coordinates: z.boolean().default(true),
|
||||
mapZoom: z.boolean().default(true),
|
||||
mapPan: z.boolean().default(true),
|
||||
timerVisible: z.boolean().default(true),
|
||||
scoring: z.enum(["normal", "strict", "gentle"]).default("normal"),
|
||||
})
|
||||
.strict();
|
||||
|
||||
@@ -16,17 +16,24 @@ export const tournamentSettings = z
|
||||
name: safeName,
|
||||
datasetId: z.string().uuid(),
|
||||
difficulty: z.union([
|
||||
z.enum(["easy", "normal", "hard"]),
|
||||
z.enum(["easy", "normal", "hard", "void"]),
|
||||
z.string().uuid(),
|
||||
]),
|
||||
playerLimit: z.number().int().min(2).max(32),
|
||||
roundCount: z.number().int().min(1).max(10),
|
||||
roundTimeSeconds: z.number().int().min(15).max(3000).multipleOf(15),
|
||||
roundTimeSeconds: z.number().int().min(10).max(3000),
|
||||
timerMode: z.enum(["shared", "perMini"]).default("shared"),
|
||||
startsAt: z.string().datetime({ offset: true }),
|
||||
announceDiscord: z.boolean(),
|
||||
})
|
||||
.strict()
|
||||
.refine(
|
||||
(value) =>
|
||||
value.difficulty === "void"
|
||||
? value.timerMode === "perMini" && value.roundTimeSeconds === 10
|
||||
: value.roundTimeSeconds >= 15 && value.roundTimeSeconds % 15 === 0,
|
||||
"Void requires a separate 10-second timer; other tournament timers use 15-second steps",
|
||||
)
|
||||
.refine(
|
||||
(value) => value.timerMode !== "perMini" || value.roundTimeSeconds <= 600,
|
||||
"Mini-round timers allow at most 600 seconds",
|
||||
|
||||
Reference in New Issue
Block a user