-
-
-
+
{entry.player_id === playerId && (
{t("You")}
)}
@@ -164,15 +158,9 @@ export function WinnersPodium({ leaders }: { leaders: LeaderboardEntry[] }) {
-
-
-
+
{formatNumber(entry.best_score)} / 25,000
diff --git a/components/MultiplayerGame.tsx b/components/MultiplayerGame.tsx
index d1ea298..67ada09 100644
--- a/components/MultiplayerGame.tsx
+++ b/components/MultiplayerGame.tsx
@@ -142,7 +142,6 @@ export function MultiplayerGame({
diff --git a/components/MultiplayerRoom.tsx b/components/MultiplayerRoom.tsx
index fe2ea73..00ae8ef 100644
--- a/components/MultiplayerRoom.tsx
+++ b/components/MultiplayerRoom.tsx
@@ -334,7 +334,6 @@ export function MultiplayerRoom({ id }: { id: string }) {
{player.id === room.hostId && (
@@ -442,7 +441,6 @@ export function MultiplayerRoom({ id }: { id: string }) {
value === effect);
if (avatarUrl)
return (
-
-
-
+
);
const seed = [...id].reduce(
(value, char) => (value * 31 + char.charCodeAt(0)) >>> 0,
@@ -59,28 +53,26 @@ export function PlayerAvatar({
const skin = ["#c99a77", "#a67b60", "#dab28d", "#875f4d"][(seed >>> 3) % 4];
const shirt = ["#68876a", "#697e91", "#a68a50", "#8b6e7f"][(seed >>> 5) % 4];
return (
-
-
-
+
);
}
diff --git a/lib/account-api.ts b/lib/account-api.ts
index ec4efef..3d92be3 100644
--- a/lib/account-api.ts
+++ b/lib/account-api.ts
@@ -181,7 +181,7 @@ export async function accountApi(
.update({ avatar_path: avatarPath })
.eq("id", account.id)
.select(
- "id,display_name,bio,avatar_path,profile_effect,role,name_glow,glow_color,bug_finder,custom_prefix,custom_prefix_icon",
+ "id,display_name,avatar_path,role,name_glow,glow_color,bug_finder,custom_prefix,custom_prefix_icon",
)
.single(),
);
@@ -269,11 +269,10 @@ export async function accountApi(
...(body.avatarPath !== undefined
? { avatar_path: body.avatarPath }
: {}),
- ...(body.bio !== undefined ? { bio: body.bio } : {}),
})
.eq("id", account.id)
.select(
- "id,display_name,bio,avatar_path,profile_effect,role,name_glow,glow_color,bug_finder,custom_prefix,custom_prefix_icon",
+ "id,display_name,avatar_path,role,name_glow,glow_color,bug_finder,custom_prefix,custom_prefix_icon",
)
.single(),
);
diff --git a/lib/api.ts b/lib/api.ts
index ac81179..0e46c73 100644
--- a/lib/api.ts
+++ b/lib/api.ts
@@ -18,7 +18,6 @@ import { requestOrigin, verifyOrigin } from "./request-origin";
import { browserAccount } from "./request-security";
import { jsonBody } from "./json-body";
import { accountApi } from "./account-api";
-import { cosmeticsApi } from "./cosmetics-api";
import { multiplayerApi } from "./multiplayer-api";
import { bugReportApi } from "./bug-report-api";
import { moderatorUsersApi } from "./moderator-users-api";
@@ -132,8 +131,6 @@ export async function handle(request: NextRequest, path: string[]) {
return await accountControlsApi(request, path, { jsonBody, limit });
if (path[0] === "auth" || path[0] === "profile")
return await accountApi(request, path, { jsonBody, limit });
- if (path[0] === "cosmetics")
- return await cosmeticsApi(request, path, { jsonBody, limit });
// Check signed-in identities before any guest-cookie fallback (games/rooms).
if (path[0] !== "health") await user();
if (path[0] === "rooms")
diff --git a/lib/catalog.ts b/lib/catalog.ts
index d60d2e0..3d30200 100644
--- a/lib/catalog.ts
+++ b/lib/catalog.ts
@@ -64,7 +64,7 @@ export async function catalog() {
await client
.from("profiles")
.select(
- "id,display_name,avatar_path,profile_effect,name_glow,glow_color,role,bug_finder,custom_prefix,custom_prefix_icon",
+ "id,display_name,avatar_path,name_glow,glow_color,role,bug_finder,custom_prefix,custom_prefix_icon",
)
.in("id", [
...new Set(
@@ -106,7 +106,7 @@ export async function getMap(id: string) {
? admin()
.from("profiles")
.select(
- "display_name,avatar_path,profile_effect,name_glow,glow_color,role,bug_finder,custom_prefix,custom_prefix_icon",
+ "display_name,avatar_path,name_glow,glow_color,role,bug_finder,custom_prefix,custom_prefix_icon",
)
.eq("id", map.owner_id)
.maybeSingle()
diff --git a/lib/cosmetics-api.ts b/lib/cosmetics-api.ts
deleted file mode 100644
index 795de3c..0000000
--- a/lib/cosmetics-api.ts
+++ /dev/null
@@ -1,52 +0,0 @@
-import "server-only";
-import { NextRequest, NextResponse } from "next/server";
-import { profileCosmeticActionSchema } from "@/shared/profile-cosmetics";
-import { admin, required, user } from "./supabase";
-import { browserAccount } from "./request-security";
-import { profileCosmeticsState } from "./profile-cosmetics";
-
-type Context = {
- jsonBody: (request: NextRequest) => Promise;
- limit: (key: string, count: number, seconds: number) => Promise;
-};
-
-const json = (body: unknown, status = 200) =>
- NextResponse.json(body, { status, headers: { "Cache-Control": "no-store" } });
-
-export async function cosmeticsApi(
- request: NextRequest,
- path: string[],
- context: Context,
-) {
- if (path.length !== 1) return json({ error: "Not found" }, 404);
- const account = await browserAccount(request, user);
- if (request.method === "GET")
- return json({ shop: await profileCosmeticsState(account.id) });
- if (request.method !== "POST")
- return json({ error: "Method not allowed" }, 405);
-
- const body = profileCosmeticActionSchema.parse(
- await context.jsonBody(request),
- );
- if (body.action === "purchase") {
- await context.limit("cosmetic-purchase:" + account.id, 8, 60);
- return json({
- shop: required(
- await admin().rpc("buy_profile_cosmetic", {
- p_user: account.id,
- p_cosmetic_id: body.cosmeticId,
- }),
- ),
- });
- }
-
- await context.limit("cosmetic-equip:" + account.id, 30, 60);
- return json({
- shop: required(
- await admin().rpc("equip_profile_cosmetic", {
- p_user: account.id,
- p_cosmetic_id: body.cosmeticId,
- }),
- ),
- });
-}
diff --git a/lib/leaderboard.ts b/lib/leaderboard.ts
index 8d5ae58..59b2be5 100644
--- a/lib/leaderboard.ts
+++ b/lib/leaderboard.ts
@@ -35,7 +35,7 @@ export async function leaderboard({
await admin()
.from("profiles")
.select(
- "id,avatar_path,profile_effect,name_glow,glow_color,role,bug_finder,custom_prefix,custom_prefix_icon",
+ "id,avatar_path,name_glow,glow_color,role,bug_finder,custom_prefix,custom_prefix_icon",
)
.in("id", ids),
)
diff --git a/lib/multiplayer-appearance.ts b/lib/multiplayer-appearance.ts
index 41c1f91..d8d413e 100644
--- a/lib/multiplayer-appearance.ts
+++ b/lib/multiplayer-appearance.ts
@@ -25,7 +25,7 @@ export async function multiplayerAppearance(room: string, ids: string[]) {
await db
.from("profiles")
.select(
- "id,display_name,avatar_path,profile_effect,name_glow,glow_color,role,bug_finder,custom_prefix,custom_prefix_icon",
+ "id,display_name,avatar_path,name_glow,glow_color,role,bug_finder,custom_prefix,custom_prefix_icon",
)
.in("id", accounts),
);
diff --git a/lib/profile-cosmetics.ts b/lib/profile-cosmetics.ts
deleted file mode 100644
index 1855076..0000000
--- a/lib/profile-cosmetics.ts
+++ /dev/null
@@ -1,15 +0,0 @@
-import "server-only";
-import { admin, required } from "./supabase";
-import {
- cosmeticShopStateSchema,
- type CosmeticShopState,
-} from "@/shared/profile-cosmetics";
-
-export async function profileCosmeticsState(
- userId: string,
-): Promise {
- const result = required(
- await admin().rpc("cosmetics_shop_state", { p_user: userId }),
- );
- return cosmeticShopStateSchema.parse(result);
-}
diff --git a/lib/profile.ts b/lib/profile.ts
index e138703..95363cf 100644
--- a/lib/profile.ts
+++ b/lib/profile.ts
@@ -21,7 +21,7 @@ export async function accountProfile(account: User) {
await admin()
.from("profiles")
.select(
- "id,display_name,bio,avatar_path,profile_effect,role,name_glow,glow_color,bug_finder,custom_prefix,custom_prefix_icon,account_status,account_reason,account_changed_at",
+ "id,display_name,avatar_path,role,name_glow,glow_color,bug_finder,custom_prefix,custom_prefix_icon,account_status,account_reason,account_changed_at",
)
.eq("id", account.id)
.single(),
diff --git a/lib/public-profile.ts b/lib/public-profile.ts
deleted file mode 100644
index 327b184..0000000
--- a/lib/public-profile.ts
+++ /dev/null
@@ -1,41 +0,0 @@
-import "server-only";
-import { z } from "zod";
-import { admin } from "./supabase";
-
-const uuidSchema = z.string().uuid();
-
-export async function publicProfile(userId: string) {
- const validId = uuidSchema.safeParse(userId);
- if (!validId.success) return null;
-
- const result = await admin()
- .from("profiles")
- .select(
- "id,display_name,bio,avatar_path,profile_effect,name_glow,glow_color,role,bug_finder,custom_prefix,custom_prefix_icon,created_at",
- )
- .eq("id", validId.data)
- .eq("account_status", "active")
- .maybeSingle();
- if (result.error) throw new Error(result.error.message);
- if (!result.data) return null;
-
- const [games, maps] = await Promise.all([
- admin()
- .from("game_sessions")
- .select("id", { count: "exact", head: true })
- .eq("owner_id", validId.data)
- .eq("current_round", 5),
- admin()
- .from("datasets")
- .select("id", { count: "exact", head: true })
- .eq("owner_id", validId.data)
- .eq("state", "published"),
- ]);
- if (games.error) throw new Error(games.error.message);
- if (maps.error) throw new Error(maps.error.message);
- return {
- ...result.data,
- completedGames: games.count ?? 0,
- publishedMaps: maps.count ?? 0,
- };
-}
diff --git a/shared/account.ts b/shared/account.ts
index 8f099c2..c971fd7 100644
--- a/shared/account.ts
+++ b/shared/account.ts
@@ -43,26 +43,13 @@ export const profileUpdateSchema = z
displayName: displayNameSchema.optional(),
glowColor: glowColorSchema.optional(),
avatarPath: z.string().max(128).nullable().optional(),
- bio: z
- .string()
- .trim()
- .max(280, "Use at most 280 characters")
- .refine(
- (value) =>
- !/[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f-\u009f\u202a-\u202e\u2066-\u2069<>]/u.test(
- value,
- ),
- "Use plain text without hidden controls or markup",
- )
- .optional(),
})
.strict()
.refine(
(value) =>
value.displayName !== undefined ||
value.glowColor !== undefined ||
- value.avatarPath !== undefined ||
- value.bio !== undefined,
+ value.avatarPath !== undefined,
"Choose a profile change",
);
export const initialPasswordSchema = z
diff --git a/shared/database.ts b/shared/database.ts
index 0208b31..47fb03c 100644
--- a/shared/database.ts
+++ b/shared/database.ts
@@ -568,8 +568,6 @@ export type Database = {
profiles: {
Row: AccountStatusFields & {
avatar_path: string | null;
- bio: string;
- profile_effect: string | null;
glow_color: string;
bug_finder: boolean;
custom_prefix: string | null;
@@ -582,8 +580,6 @@ export type Database = {
};
Insert: Partial & {
avatar_path?: string | null;
- bio?: string;
- profile_effect?: string | null;
glow_color?: string;
bug_finder?: boolean;
custom_prefix?: string | null;
@@ -596,8 +592,6 @@ export type Database = {
};
Update: Partial & {
avatar_path?: string | null;
- bio?: string;
- profile_effect?: string | null;
glow_color?: string;
bug_finder?: boolean;
custom_prefix?: string | null;
@@ -788,18 +782,6 @@ export type Database = {
[_ in never]: never;
};
Functions: {
- cosmetics_shop_state: {
- Args: { p_user: string };
- Returns: Json;
- };
- buy_profile_cosmetic: {
- Args: { p_user: string; p_cosmetic_id: string };
- Returns: Json;
- };
- equip_profile_cosmetic: {
- Args: { p_user: string; p_cosmetic_id: string | null };
- Returns: Json;
- };
admin_account_action: {
Args: {
p_reviewer: string;
diff --git a/shared/locales/ru.json b/shared/locales/ru.json
index 4036143..100c4f3 100644
--- a/shared/locales/ru.json
+++ b/shared/locales/ru.json
@@ -10,55 +10,6 @@
"Guide": "Как играть",
"Docs": "Документация",
"My account": "Мой аккаунт",
- "Profile cosmetics": "Косметика профиля",
- "Browse profile cosmetics": "Открыть магазин косметики",
- "View public profile": "Открыть публичный профиль",
- "About you": "О себе",
- "Tell other explorers a little about yourself.": "Расскажите другим исследователям немного о себе.",
- "Shown on your public profile · Maximum 280 characters.": "Отображается в публичном профиле · Не более 280 символов.",
- "Save public profile": "Сохранить публичный профиль",
- "Public profile saved.": "Публичный профиль сохранён.",
- "A name and description for other explorers to see.": "Имя и описание, которые увидят другие исследователи.",
- "Back to leaderboard": "Назад к рейтингу",
- "MINEGEO EXPLORER": "ИССЛЕДОВАТЕЛЬ MINEGEO",
- "Exploring since {date}": "Исследует миры с {date}",
- "About this explorer": "Об исследователе",
- "This explorer hasn’t added a description yet.": "Исследователь пока не добавил описание.",
- "Profile stats": "Статистика профиля",
- "Completed journeys": "Завершённые экспедиции",
- "Published maps": "Опубликованные карты",
- "Only public account details and published maps appear here.": "Здесь отображаются только публичные данные аккаунта и опубликованные карты.",
- "THE EXPLORER’S WARDROBE": "ГАРДЕРОБ ИССЛЕДОВАТЕЛЯ",
- "Leave a little sparkle.": "Добавь немного сияния.",
- "Earn GeoCoins in solo expeditions and dress up your profile portrait.": "Зарабатывай GeoCoins в одиночных экспедициях и укрась портрет профиля.",
- "GeoCoins balance": "Баланс GeoCoins",
- "YOUR BALANCE": "ТВОЙ БАЛАНС",
- "GeoCoins": "GeoCoins",
- "Complete a public solo game to earn 10–20 GeoCoins. Rewards are capped at 100 every 24 hours.": "Заверши одиночную игру на публичной карте и получи 10–20 GeoCoins. Максимум — 100 за каждые 24 часа.",
- "common": "обычный",
- "rare": "редкий",
- "epic": "эпический",
- "legendary": "легендарный",
- "Equipped": "Надето",
- "Cosmetic purchased and equipped.": "Эффект куплен и надет.",
- "Cosmetic equipped.": "Эффект надет.",
- "Profile effect removed.": "Эффект профиля снят.",
- "Not enough GeoCoins": "Недостаточно GeoCoins.",
- "Cosmetic is not available": "Эта косметика сейчас недоступна.",
- "Cosmetic not owned": "Сначала купи этот эффект.",
- "Buy · {price} GeoCoins": "Купить · {price} GeoCoins",
- "Unequip effect": "Снять эффект",
- "Equip effect": "Надеть эффект",
- "Buying…": "Покупка…",
- "Cosmetics use GeoCoins for now. Payment options may be added later.": "Пока косметика покупается за GeoCoins. Позже могут появиться другие способы оплаты.",
- "Redstone Pulse": "Импульс редстоуна",
- "A redstone signal ripples around your portrait.": "Вокруг портрета пробегает импульс редстоуна.",
- "Ender Orbit": "Орбита Края",
- "Two ender sparks circle your portrait.": "Вокруг портрета вращаются две искры Края.",
- "Nether Flame": "Пламя Незера",
- "A warm nether flame flickers along the frame.": "По рамке мерцает жаркое пламя Незера.",
- "Aurora Crown": "Корона сияния",
- "A living aurora sweeps around your portrait.": "Вокруг портрета переливается северное сияние.",
"Log in": "Войти",
"Tools & downloads": "Инструменты",
"Moderation": "Модерация",
diff --git a/shared/profile-appearance.ts b/shared/profile-appearance.ts
index f8079b3..ff4b718 100644
--- a/shared/profile-appearance.ts
+++ b/shared/profile-appearance.ts
@@ -36,7 +36,6 @@ export const prefixUpdateSchema = z
.strict();
export type ProfileAppearance = {
avatar_path?: string | null;
- profile_effect?: string | null;
name_glow?: boolean;
glow_color?: string;
role?: string;
@@ -45,7 +44,7 @@ export type ProfileAppearance = {
custom_prefix_icon?: string;
};
export const appearanceColumns =
- "avatar_path,profile_effect,name_glow,glow_color,role,bug_finder,custom_prefix,custom_prefix_icon" as const;
+ "avatar_path,name_glow,glow_color,role,bug_finder,custom_prefix,custom_prefix_icon" as const;
export type BadgeUser = ProfileAppearance & {
id: string;
display_name: string;
diff --git a/shared/profile-cosmetic-ids.ts b/shared/profile-cosmetic-ids.ts
deleted file mode 100644
index 70d13ae..0000000
--- a/shared/profile-cosmetic-ids.ts
+++ /dev/null
@@ -1,8 +0,0 @@
-export const profileCosmeticIds = [
- "redstone-pulse",
- "ender-orbit",
- "nether-flame",
- "aurora-crown",
-] as const;
-
-export type ProfileCosmeticId = (typeof profileCosmeticIds)[number];
diff --git a/shared/profile-cosmetics.ts b/shared/profile-cosmetics.ts
deleted file mode 100644
index 0490b79..0000000
--- a/shared/profile-cosmetics.ts
+++ /dev/null
@@ -1,41 +0,0 @@
-import { z } from "zod";
-import { profileCosmeticIds } from "./profile-cosmetic-ids";
-
-export const profileCosmeticIdSchema = z.enum(profileCosmeticIds);
-export type { ProfileCosmeticId } from "./profile-cosmetic-ids";
-
-export const profileCosmeticActionSchema = z.discriminatedUnion("action", [
- z
- .object({
- action: z.literal("purchase"),
- cosmeticId: profileCosmeticIdSchema,
- })
- .strict(),
- z
- .object({
- action: z.literal("equip"),
- cosmeticId: profileCosmeticIdSchema.nullable(),
- })
- .strict(),
-]);
-
-export const cosmeticShopStateSchema = z
- .object({
- balance: z.number().int().nonnegative(),
- owned: z.array(profileCosmeticIdSchema),
- equipped: profileCosmeticIdSchema.nullable(),
- catalog: z.array(
- z
- .object({
- id: profileCosmeticIdSchema,
- name: z.string().min(1).max(48),
- description: z.string().min(1).max(160),
- price: z.number().int().positive(),
- rarity: z.enum(["common", "rare", "epic", "legendary"]),
- })
- .strict(),
- ),
- })
- .strict();
-
-export type CosmeticShopState = z.infer;
diff --git a/supabase/migrations/20261015132835_revert_profile_cosmetics_and_currency.sql b/supabase/migrations/20261015132835_revert_profile_cosmetics_and_currency.sql
new file mode 100644
index 0000000..63acfb1
--- /dev/null
+++ b/supabase/migrations/20261015132835_revert_profile_cosmetics_and_currency.sql
@@ -0,0 +1,15 @@
+-- Roll back the profile cosmetics shop and GeoCoin currency.
+drop trigger if exists game_rounds_reward_cosmetic_currency on public.game_rounds;
+drop function if exists public.reward_completed_game_cosmetic_currency();
+drop function if exists public.equip_profile_cosmetic(uuid, text);
+drop function if exists public.buy_profile_cosmetic(uuid, text);
+drop function if exists public.cosmetics_shop_state(uuid);
+
+alter table public.profiles
+ drop column if exists profile_effect,
+ drop column if exists bio;
+
+drop table if exists public.game_wallet_transactions;
+drop table if exists public.profile_cosmetic_ownership;
+drop table if exists public.game_wallets;
+drop table if exists public.profile_cosmetics;