36 KiB
Frontend Recreation TODO
This document outlines all the necessary information to recreate the SvelteKit frontend for the Openplace/Wplace application.
Overview
Openplace is a collaborative real-time pixel art canvas layered over a world map (similar to r/place). Users can paint pixels, join alliances, view leaderboards, and moderate content. The frontend is built with SvelteKit and integrates with the backend API documented below.
Tech Stack
- Framework: SvelteKit (based on
index.htmlmodule imports) - Language: TypeScript (inferred from backend consistency)
- Fonts:
- PixelifySans (pixel art style font)
- Geist (modern sans-serif)
- NotoColorEmoji (for flag emojis)
- Map Library: Likely Leaflet or Mapbox (for world map integration)
- PWA: Progressive Web App with service worker
- Build Tool: Vite (standard for SvelteKit)
Project Structure
Based on compiled output, the frontend likely has these routes:
/ - Main canvas view
/admin - Admin panel
/admin/* - Various admin sub-pages
/moderation - Moderation panel
/maps - Map-related pages
404.html - 404 error page
API Endpoints Reference
Authentication (/auth)
POST /login
- Body:
{ username: string, password: string } - Response:
{ success: boolean } - Cookie: Sets
jcookie (JWT token, HttpOnly, 30 day expiry) - Note: Auto-registers users if username doesn't exist
POST /auth/logout
- Headers: Requires JWT cookie
- Response:
{ success: boolean } - Cookie: Clears
jcookie
Pixel Operations (/pixel)
GET /:season/tile/random
- Purpose: Get random tile coordinates to start viewing
- Response:
{ tileX: number, tileY: number }
GET /:season/pixel/:tileX/:tileY?x=X&y=Y
- Purpose: Get information about a specific pixel
- Query Params:
x(0-999): pixel X within tiley(0-999): pixel Y within tile
- Response:
{
colorId: number,
paintedBy: number,
paintedAt: string,
user: {
id: number,
name: string,
level: number,
equippedFlag: number
}
}
GET /files/:season/tiles/:tileX/:tileY.png
- Purpose: Get rendered tile image (1000x1000 pixels)
- Response: PNG image
- Cache: 5 minutes (
Cache-Control: public, max-age=300)
POST /:season/pixel/:tileX/:tileY
- Purpose: Paint one or more pixels
- Headers: Requires JWT cookie
- Body:
{
colors: number[], // Array of colorIds (0-63)
coords: number[][] // Array of [x, y] coordinates within tile
}
- Response:
{
success: boolean,
currentCharges: number,
maxCharges: number,
chargesLastUpdatedAt: string,
pixelsPainted: number,
level: number
}
- Errors:
- 400: Invalid coordinates or colors
- 403: Not enough charges, color not unlocked, or timed out
User Profile (/me)
GET /me
- Headers: Requires JWT cookie
- Response:
{
id: number,
name: string,
discord: string | null,
country: string,
droplets: number,
currentCharges: number,
maxCharges: number,
chargesCooldownMs: number,
chargesLastUpdatedAt: string,
pixelsPainted: number,
level: number,
equippedFlag: number,
extraColorsBitmap: string, // base64 encoded
flagsBitmap: string | null, // base64 encoded
showLastPixel: boolean,
picture: string | null,
allianceId: number | null,
allianceRole: string,
alliance: {
id: number,
name: string,
description: string,
pixelsPainted: number
} | null
}
POST /me/update
- Headers: Requires JWT cookie
- Body:
{
name?: string, // 3-20 chars
showLastPixel?: boolean,
discord?: string // Optional Discord username
}
- Response: Updated user profile (same as GET /me)
GET /me/profile-pictures
- Headers: Requires JWT cookie
- Response:
{
pictures: Array<{
id: number,
url: string
}>
}
Alliance System (/alliance)
GET /alliance
- Headers: Requires JWT cookie
- Response: User's current alliance details or error if not in alliance
POST /alliance
- Purpose: Create new alliance
- Headers: Requires JWT cookie
- Body:
{ name: string }// 3-30 chars, unique - Response: Alliance details
POST /alliance/update-description
- Headers: Requires JWT cookie (must be alliance admin)
- Body:
{ description: string }// Max 500 chars - Response: Updated alliance
GET /alliance/invites
- Headers: Requires JWT cookie (must be alliance admin)
- Response:
{
invites: Array<{
id: string, // UUID
createdAt: string
}>
}
GET /alliance/join/:invite
- Headers: Requires JWT cookie
- Response: Joins alliance via invite code
POST /alliance/update-headquarters
- Headers: Requires JWT cookie (must be alliance admin)
- Body:
{ latitude: number, longitude: number } - Response: Updated alliance
GET /alliance/members/:page
- Headers: Requires JWT cookie
- Params:
page(0-indexed) - Response:
{
members: Array<{
id: number,
name: string,
pixelsPainted: number,
level: number,
role: string,
equippedFlag: number
}>,
total: number,
page: number,
pageSize: number
}
GET /alliance/members/banned/:page
- Headers: Requires JWT cookie (must be alliance admin)
- Response: Paginated list of banned users
POST /alliance/give-admin
- Headers: Requires JWT cookie (must be alliance owner)
- Body:
{ promotedUserId: number } - Response: 200 OK
POST /alliance/ban
- Headers: Requires JWT cookie (must be alliance admin)
- Body:
{ bannedUserId: number } - Response: Updated alliance
POST /alliance/unban
- Headers: Requires JWT cookie (must be alliance admin)
- Body:
{ unbannedUserId: number } - Response: Updated alliance
GET /alliance/leaderboard/:mode
- Headers: Requires JWT cookie
- Params:
mode- "today" | "week" | "month" | "all-time" - Response: Top 50 alliances with pixel counts
Leaderboards (/leaderboard)
GET /leaderboard/player/:mode
- Params:
mode- "today" | "week" | "month" | "all-time" - Response:
Array<{
id: number,
name: string,
allianceId: number,
allianceName: string,
equippedFlag: number,
pixelsPainted: number,
picture?: string,
discord: string
}>
GET /leaderboard/alliance/:mode
- Params:
mode- "today" | "week" | "month" | "all-time" - Response:
Array<{
id: number,
name: string,
pixelsPainted: number
}>
GET /leaderboard/country/:mode
- Params:
mode- "today" | "week" | "month" | "all-time" - Response: Array of countries with pixel counts
- Note: Currently returns mock data, needs implementation
GET /leaderboard/region/:mode/:country
- Params:
mode- "today" | "week" | "month" | "all-time"country- Country ID number
- Response: Array of regions with pixel counts
- Note: Currently returns mock data, needs implementation
GET /leaderboard/region/players/:city/:mode
- Params:
city- City IDmode- "today" | "week" | "month" | "all-time"
- Response: Top 50 players in region
- Note: City parameter currently unused
GET /leaderboard/region/alliances/:city/:mode
- Params:
city- City IDmode- "today" | "week" | "month" | "all-time"
- Response: Top 50 alliances in region
- Note: City parameter currently unused
Store (/store)
POST /purchase
- Headers: Requires JWT cookie
- Body:
{
product: {
id: 70 | 80 | 100 | 110,
amount?: number, // Quantity (default 1)
variant?: number // For colors (32-63) or flags (1-251)
}
}
- Product IDs:
- 70: +5 Max Charges (500 droplets)
- 80: +30 Paint Charges (500 droplets)
- 100: Unlock Paid Color (2000 droplets) - requires
variant(32-63) - 110: Unlock Flag (20,000 droplets) - requires
variant(1-251)
- Response:
{ success: boolean } - Errors: 403 if insufficient droplets
POST /flag/equip/:id
- Headers: Requires JWT cookie
- Params:
id- Flag ID (1-251) - Response:
{ success: boolean } - Errors: 403 if flag not unlocked
Admin Panel (/admin/*)
All admin endpoints require:
- JWT cookie authentication
- User role = "admin"
- Returns 403 Forbidden otherwise
GET /admin/users?id=USER_ID
- Query:
id- User ID - Response:
{
id: number,
name: string,
droplets: number,
picture: string | null,
role: string,
timeout_until: string,
ban_reason: null, // TODO: Not implemented
reported_times: 0, // TODO: Not implemented
timeouts_count: 0, // TODO: Not implemented
same_ip_accounts: 0, // TODO: Not implemented
alliance_id: number | null,
alliance_name: string | null,
pixels_painted: number,
phone_validated: false, // TODO: Not implemented
discord: string | null
}
GET /admin/users/notes?userId=USER_ID
- Query:
userId- User ID - Response:
{
notes: Array<{
id: number,
author: {
role: string,
id: number,
name: string
},
note: string,
createdAt: string
}>
}
POST /admin/users/notes
- Body:
{ userId: number, note: string } - Response:
{}
GET /admin/users/tickets?id=USER_ID
- Query:
id- User ID - Response:
{}// TODO: Not implemented
GET /admin/users/purchases?userId=USER_ID
- Query:
userId- User ID - Response:
{}// TODO: Not implemented
POST /admin/users/set-user-droplets
- Body:
{ userId: number, droplets: number }// Adds droplets (can be negative) - Response:
{ success: boolean }
GET /admin/tickets
- Response: Open tickets grouped by reported user
{
tickets: Array<{
id: number, // Reported user ID
reportedUser: {
id: number,
name: string,
discord: string,
country: string,
banned: boolean
},
createdAt: string,
reports: Array<{
id: string, // Ticket ID (UUID)
latitude: number,
longitude: number,
zoom: number,
reason: string,
notes: string,
image: string,
createdAt: string
}>
}>,
status: 200
}
GET /admin/closed-tickets
- Response: Same as
/admin/ticketsbut for resolved tickets
GET /admin/open-tickets-count
- Response:
{ tickets: number }
POST /admin/severe-open-tickets-count
- Response:
{ tickets: number }
POST /admin/assign-new-tickets
- Response:
{ newTicketsIds: [] }// TODO: Not implemented
GET /admin/count-all-tickets
- Response:
{
doxxing: number,
inappropriate_content: number,
hate_speech: number,
bot: number,
other: number,
griefing: number,
total_open_tickets: number
}
GET /admin/count-all-reports
- Response: Same as
/admin/count-all-tickets// TODO: Uses same data
GET /admin/alliances/:id
- Params:
id- Alliance ID - Response:
{
id: number,
name: string,
pixelsPainted: number
}
GET /admin/alliances/:id/full
- Params:
id- Alliance ID - Response: Full alliance details including members, bans, etc.
GET /admin/alliances/search?q=QUERY
- Query:
q- Search by name or ID - Response:
{ results: Alliance[] }// Top 20 results
Moderation Panel (/moderator/*)
All moderator endpoints require:
- JWT cookie authentication
- User role = "moderator" or "admin"
- Returns 403 Forbidden otherwise
GET /moderator/tickets
- Response: Same format as
/admin/tickets
GET /moderator/users/tickets?userId=USER_ID
- Query:
userId- User ID - Response: All tickets for a specific user
GET /moderator/open-tickets-count
- Response:
{ tickets: number }
POST /moderator/severe-open-tickets-count
- Response:
{ tickets: number }
POST /moderator/assign-new-tickets
- Response:
{ newTicketsIds: [] }// TODO: Not implemented
GET /moderator/count-my-tickets
- Response:
0// TODO: Not implemented
Core Frontend Features to Implement
1. Authentication System
Components:
- Login form (see
LoginFormCSS asset) - Registration flow (combined with login)
- Session management using JWT cookie
- Auto-redirect to login if unauthorized
Key Implementation Details:
- Cookie name:
j - Cookie is HttpOnly (not accessible via JavaScript)
- 30-day expiration
- Auto-create account on first login with username/password
2. Main Canvas View
Components:
- Interactive world map (Leaflet/Mapbox)
- Tile-based pixel rendering system
- Zoom controls
- Color picker palette (32 free colors + 32 paid colors)
- Brush/paint tool
- Pixel info tooltip on hover/click
- Charge indicator (shows current/max charges)
- Level display
Technical Requirements:
- Tiles are 1000x1000 pixels
- Fetch tiles as PNG images:
/files/:season/tiles/:tileX/:tileY.png - Cache tiles appropriately (5 min cache header)
- Calculate global coordinates:
globalX = tileX * 1000 + x,globalY = tileY * 1000 + y - Map global coordinates to lat/lng for world map overlay
- Handle painting multiple pixels in one request
- Show charge regeneration countdown (default: 1 charge per 30 seconds)
- Disable paid colors unless unlocked (check
extraColorsBitmap)
Charge System:
- Default: 20 max charges
- Regenerates 1 charge every 30 seconds (configurable per user)
- Painting consumes charges
- Must calculate current charges:
currentCharges + floor((now - lastUpdate) / cooldownMs) - 10% discount when painting in equipped flag's region (TODO: region system not implemented)
Color Palette (0-63):
// Colors 0-31: Free
// Colors 32-63: Paid (require purchase)
// Color 0: Transparent
// Check if color unlocked: extraColorsBitmap & (1 << (colorId - 32))
Full color palette available in backend: src/utils/colors.ts
3. User Profile Page
Components:
- Profile avatar with level indicator (see
ProfileAvatarWithLevelCSS asset) - Username (editable)
- Discord username (editable)
- Show last pixel toggle
- Droplets balance
- Charges indicator
- Pixels painted count
- Level display
- Equipped flag display
- Alliance affiliation
Features:
- Edit profile settings
- View unlocked colors
- View unlocked flags
- View alliance info
- View favorite locations (TODO: not implemented in backend)
4. Alliance System
Components:
- Alliance creation dialog
- Alliance info panel
- Member list (paginated, 50 per page)
- Admin controls (for alliance admins)
- Invite system
- Ban management
- Headquarters map marker
Features:
- Create alliance (requires no current alliance)
- Join alliance via invite link
- Leave alliance
- Update description (admins only)
- Set headquarters location on map (admins only)
- Promote members to admin (owner only)
- Ban/unban members (admins only)
- View alliance leaderboard
5. Leaderboards
Views:
- Player leaderboard (top 50)
- Alliance leaderboard (top 50)
- Country leaderboard
- Region leaderboard
- Regional player leaderboard
- Regional alliance leaderboard
Time Filters:
- Today
- Week (last 7 days)
- Month (current month)
- All-time
Display Fields:
- Rank (1-50)
- Player name / Alliance name
- Equipped flag icon
- Pixels painted
- Alliance affiliation (for players)
6. Store System
Products:
-
+5 Max Charges (500 droplets)
- Increases maxCharges by 5
- Can purchase multiple
-
+30 Paint Charges (500 droplets)
- Adds 30 to currentCharges (up to max)
- Can purchase multiple
-
Unlock Paid Color (2000 droplets each)
- Unlocks one of colors 32-63
- Must select color variant
- Updates
extraColorsBitmap
-
Unlock Flag (20,000 droplets each)
- Unlocks one of 251 country flags
- Must select flag variant (1-251)
- Updates
flagsBitmap
Implementation:
- Display droplet balance
- Show which colors/flags are already unlocked
- Disable purchase if insufficient droplets
- Confirmation dialog before purchase
- Update UI after successful purchase
Flag Equipping:
- Separate endpoint to equip owned flag
- Can only equip flags that are unlocked
- Equipped flag shown on profile and leaderboards
7. Admin Panel
Pages:
- User management
- Ticket management (reports)
- Alliance management
- Statistics dashboard
User Management:
- Search users by ID
- View user details
- View user notes
- Add moderator notes
- Set droplets (add/subtract)
- View user tickets
- View purchase history (TODO)
Ticket Management:
- View open tickets
- View closed tickets
- Tickets grouped by reported user
- Show ticket details (location, reason, image evidence)
- Assign tickets to moderators (TODO)
- Count tickets by reason
Alliance Management:
- Search alliances
- View alliance details
- View full alliance info (members, bans)
8. Moderation Panel
Features:
- View assigned tickets
- View all open tickets
- View user ticket history
- Count severe tickets
- Count my assigned tickets (TODO)
Ticket Types:
- Doxxing
- Inappropriate Content
- Hate Speech
- Bot
- Griefing
- Other
Ticket Details:
- Reporter info
- Reported user info
- Canvas location (lat/lng, zoom)
- Reason
- Notes
- Evidence image
- Timestamp
Data Models
User
{
id: number
name: string
discord: string | null
country: string
email: string | null
banned: boolean
timeoutUntil: Date
role: "user" | "moderator" | "admin"
pixelsPainted: number
droplets: number
maxCharges: number
currentCharges: number
chargesCooldownMs: number
chargesLastUpdatedAt: Date
extraColorsBitmap: number // Bitmask for unlocked paid colors
flagsBitmap: Bytes | null // Bitmap for unlocked flags
equippedFlag: number // Currently equipped flag (0 = none)
showLastPixel: boolean
picture: string | null
level: number // floor(sqrt(pixelsPainted / 100)) + 1
allianceId: number | null
allianceRole: "member" | "admin" | "owner"
}
Alliance
{
id: number
name: string // Unique, 3-30 chars
description: string | null // Max 500 chars
hqLatitude: number | null
hqLongitude: number | null
pixelsPainted: number
members: User[]
bannedUsers: BannedUser[]
invites: AllianceInvite[]
}
Pixel
{
id: number
tileX: number
tileY: number
x: number // 0-999
y: number // 0-999
colorId: number // 0-63
paintedBy: number // User ID
paintedAt: Date
}
Tile
{
id: number
x: number // Tile X coordinate
y: number // Tile Y coordinate
imageData: Bytes | null // Cached PNG (if applicable)
pixels: Pixel[]
}
Ticket (Report)
{
id: string // UUID
userId: number // Reporter
reportedUserId: number // Reported user
latitude: number // Canvas location
longitude: number
zoom: number
reason: "doxxing" | "inappropriate_content" | "hate_speech" | "bot" | "griefing" | "other"
notes: string
image: string // Evidence image URL/path
resolved: boolean
severe: boolean
createdAt: Date
}
Region
{
id: number
cityId: number
name: string
number: number
countryId: number
flagId: number
}
Constants and Configuration
Season
- Default:
"s1"(Season 1) - Used in pixel API endpoints:
/:season/pixel/...
Color Palette
- 64 total colors (0-63)
- 0-31: Free colors
- 32-63: Paid colors (2000 droplets each)
- Color 0: Transparent/eraser
Flags
- 251 total country flags (1-251)
- 20,000 droplets each
- Stored as bitmap in
flagsBitmap
Charge System
- Default max charges: 20
- Default cooldown: 30,000ms (30 seconds)
- Formula:
floor((now - lastUpdate) / cooldownMs)charges regenerated
Level Calculation
level = floor(sqrt(pixelsPainted / 100)) + 1
Pagination
- Default page size: 50
- Pages are 0-indexed
Validation Rules
- Username: 3-20 characters
- Alliance name: 3-30 characters, unique
- Alliance description: Max 500 characters
- Coordinates: x, y must be 0-999 within tile
- Color ID: 0-63
State Management
Client-side state to manage:
-
User State
- Current user profile
- Authentication status
- Charge count (auto-update based on time)
- Droplets balance
- Unlocked colors/flags
-
Canvas State
- Current map position (lat/lng)
- Zoom level
- Visible tiles
- Selected color
- Brush mode
- Cached tile images
-
Alliance State
- Current alliance
- Member list
- Invites (if admin)
- Leaderboard
-
UI State
- Active modal/dialog
- Sidebar open/closed
- Selected leaderboard mode
- Selected leaderboard time filter
Real-time considerations:
- Pixel updates from other users (consider WebSocket/polling)
- Charge regeneration countdown
- Leaderboard updates
UI/UX Guidelines
Theme
- Light theme only (from meta tag:
color-scheme: light only) - Theme color:
#f8f4f0(from webmanifest) - Background:
#ffffff
Fonts
- PixelifySans: Use for headings, canvas UI elements, retro aesthetic
- Geist: Use for body text, modern UI
- NotoColorEmoji: Use for flag rendering
Responsive Design
- Mobile-first approach
- PWA optimized
- Touch-friendly controls for canvas
- Separate mobile/desktop layouts for complex pages (admin panel)
Key Interactions
- Hover over pixel: Show tooltip with painter info
- Click pixel: Show detailed pixel info modal
- Click map: Pan to location
- Click color: Select for painting
- Click canvas: Paint pixel(s) with selected color
- Right-click/long-press: Color picker (pick color from canvas)
Assets Required
Images
- Favicon (multiple sizes)
- App icons (192x192, 512x512)
- PWA screenshots
- Flag sprite sheet (flags.webp, flags@2x.webp @ 2x resolution)
- OG image for social sharing
Audio
notification.mp3- For notification sounds
Existing Assets (in /frontend folder)
/img/*- Various images/maps/*- Map-related assets/download.png,/download.svg- Download iconsPixelifySans-latin.vdc2vUDH.woff2- Font filecss2.css- Likely Google Fonts CSS
Service Worker & PWA
Features to implement:
- Offline canvas viewing (cache tiles)
- Background sync for painted pixels
- Push notifications for alliance updates
- Install prompt handling (see
window.pwaInstallPromptin index.html) - Cache strategy for static assets
- Network-first for API calls
- Cache-first for tile images
Service Worker Registration:
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('/service-worker.js');
}
WebSocket / Real-time Updates (Recommended)
While not currently implemented in the backend, the frontend should be designed to support real-time updates:
Potential WebSocket events:
pixel:painted- Another user painted a pixeltile:updated- Tile has new pixelsalliance:member_joined- New alliance membercharge:regenerated- Charge regenerated (client-side timer is fine too)leaderboard:updated- Leaderboard changed
Implementation approach:
- Start with polling (GET tile images every 5 seconds for visible tiles)
- Design component architecture to easily swap in WebSocket later
- Use event emitter pattern for pixel updates
Routing Structure (SvelteKit)
src/routes/
├── +page.svelte # Main canvas view
├── +layout.svelte # Root layout (auth check, header, etc.)
├── admin/
│ ├── +page.svelte # Admin dashboard
│ ├── users/
│ │ └── +page.svelte # User management
│ ├── tickets/
│ │ ├── +page.svelte # Open tickets
│ │ └── closed/+page.svelte # Closed tickets
│ └── alliances/
│ └── +page.svelte # Alliance management
├── moderation/
│ ├── +page.svelte # Moderator dashboard
│ └── tickets/+page.svelte # Assigned tickets
├── leaderboard/
│ └── +page.svelte # Leaderboard with tabs
├── profile/
│ └── +page.svelte # User profile
├── alliance/
│ ├── +page.svelte # Alliance view/create
│ └── [inviteId]/+page.svelte # Join alliance via invite
└── store/
└── +page.svelte # Store page
Component Architecture (Suggested)
Shared Components
src/lib/components/
├── auth/
│ ├── LoginForm.svelte
│ └── AuthGuard.svelte
├── canvas/
│ ├── MapCanvas.svelte
│ ├── TileLayer.svelte
│ ├── ColorPicker.svelte
│ ├── BrushTool.svelte
│ ├── PixelInfo.svelte
│ └── ChargeIndicator.svelte
├── user/
│ ├── ProfileAvatar.svelte
│ ├── ProfileAvatarWithLevel.svelte # Existing CSS asset
│ ├── UserCard.svelte
│ └── UserStats.svelte
├── alliance/
│ ├── AllianceCard.svelte
│ ├── AllianceMembers.svelte
│ ├── AllianceInvite.svelte
│ └── CreateAlliance.svelte
├── leaderboard/
│ ├── LeaderboardTable.svelte
│ ├── LeaderboardFilters.svelte
│ └── LeaderboardEntry.svelte
├── store/
│ ├── StoreItem.svelte
│ ├── ColorUnlockGrid.svelte
│ └── FlagSelector.svelte
├── admin/
│ ├── UserSearch.svelte
│ ├── UserDetails.svelte
│ ├── TicketList.svelte
│ ├── TicketDetails.svelte
│ └── AllianceSearch.svelte
└── common/
├── Button.svelte
├── Modal.svelte
├── Pagination.svelte
├── Toast.svelte
└── Tooltip.svelte
Store (Svelte Stores)
// src/lib/stores/auth.ts
export const currentUser = writable<User | null>(null);
export const isAuthenticated = derived(currentUser, $user => !!$user);
// src/lib/stores/canvas.ts
export const selectedColor = writable<number>(1);
export const currentCharges = writable<number>(20);
export const canvasPosition = writable<{lat: number, lng: number, zoom: number}>();
export const visibleTiles = writable<Set<string>>(); // "x,y" tile keys
// src/lib/stores/alliance.ts
export const currentAlliance = writable<Alliance | null>(null);
// src/lib/stores/ui.ts
export const activeModal = writable<string | null>(null);
export const sidebarOpen = writable<boolean>(false);
API Client
Create a typed API client for all backend endpoints:
// src/lib/api/client.ts
export class ApiClient {
private baseUrl = ''; // Same origin
// Auth
async login(username: string, password: string) { ... }
async logout() { ... }
// Pixels
async getRandomTile() { ... }
async getPixelInfo(tileX, tileY, x, y) { ... }
async paintPixels(tileX, tileY, colors, coords) { ... }
getTileImageUrl(tileX, tileY): string { ... }
// User
async getProfile() { ... }
async updateProfile(data) { ... }
// Alliance
async getAlliance() { ... }
async createAlliance(name) { ... }
// ... etc
// Leaderboards
async getPlayerLeaderboard(mode) { ... }
// ... etc
// Store
async purchase(productId, amount, variant?) { ... }
async equipFlag(flagId) { ... }
// Admin (requires admin role)
async getUser(userId) { ... }
// ... etc
}
export const api = new ApiClient();
Bitmap Utilities (Client-side)
Implement bitmap helper for colors and flags:
// src/lib/utils/bitmap.ts
export class WplaceBitmap {
private bytes: Uint8Array;
constructor(base64?: string) {
if (base64) {
this.bytes = Uint8Array.from(atob(base64), c => c.charCodeAt(0));
} else {
this.bytes = new Uint8Array(0);
}
}
get(index: number): boolean {
const byteIndex = Math.floor(index / 8);
const bitIndex = index % 8;
if (byteIndex >= this.bytes.length) return false;
const realIndex = this.bytes.length - 1 - byteIndex;
return (this.bytes[realIndex] & (1 << bitIndex)) !== 0;
}
toBase64(): string {
return btoa(String.fromCharCode(...this.bytes));
}
}
export function isColorUnlocked(colorId: number, extraColorsBitmap: number): boolean {
if (colorId < 32) return true;
const mask = 1 << (colorId - 32);
return (extraColorsBitmap & mask) !== 0;
}
Charge Calculation (Client-side)
// src/lib/utils/charges.ts
export function calculateCurrentCharges(
currentCharges: number,
maxCharges: number,
lastUpdate: Date,
cooldownMs: number
): number {
if (currentCharges >= maxCharges) return currentCharges;
const timeSinceLastUpdate = Date.now() - lastUpdate.getTime();
const chargesGenerated = Math.floor(timeSinceLastUpdate / cooldownMs);
return Math.min(maxCharges, currentCharges + chargesGenerated);
}
export function getNextChargeTime(
currentCharges: number,
maxCharges: number,
lastUpdate: Date,
cooldownMs: number
): Date | null {
if (currentCharges >= maxCharges) return null;
const timeSinceLastUpdate = Date.now() - lastUpdate.getTime();
const timeUntilNextCharge = cooldownMs - (timeSinceLastUpdate % cooldownMs);
return new Date(Date.now() + timeUntilNextCharge);
}
Level Calculation (Client-side)
// src/lib/utils/level.ts
export function calculateLevel(pixelsPainted: number): number {
return Math.floor(Math.sqrt(pixelsPainted / 100)) + 1;
}
export function getPixelsForNextLevel(currentLevel: number): number {
return ((currentLevel + 1 - 1) ** 2) * 100;
}
export function getLevelProgress(pixelsPainted: number): number {
const currentLevel = calculateLevel(pixelsPainted);
const pixelsForCurrentLevel = ((currentLevel - 1) ** 2) * 100;
const pixelsForNextLevel = (currentLevel ** 2) * 100;
const pixelsInCurrentLevel = pixelsPainted - pixelsForCurrentLevel;
const pixelsNeededForLevel = pixelsForNextLevel - pixelsForCurrentLevel;
return pixelsInCurrentLevel / pixelsNeededForLevel;
}
Color Palette (Client-side)
// src/lib/constants/colors.ts
export interface Color {
rgb: [number, number, number];
paid: boolean;
}
export const COLOR_PALETTE: Record<number, Color> = {
0: { rgb: [0, 0, 0], paid: false }, // Transparent
1: { rgb: [0, 0, 0], paid: false },
2: { rgb: [60, 60, 60], paid: false },
// ... (copy from backend src/utils/colors.ts)
63: { rgb: [205, 197, 158], paid: true }
};
export function getColorHex(colorId: number): string {
const color = COLOR_PALETTE[colorId];
if (!color) return '#000000';
const [r, g, b] = color.rgb;
return `#${r.toString(16).padStart(2, '0')}${g.toString(16).padStart(2, '0')}${b.toString(16).padStart(2, '0')}`;
}
Testing Checklist
Authentication
- Login with existing account
- Register new account (auto-create on login)
- Logout
- Session persistence across page reloads
- Redirect to login on 401
Canvas
- Load random tile on first visit
- Pan and zoom map
- Render tile images correctly
- Paint single pixel
- Paint multiple pixels
- Color picker selection
- Charge deduction after painting
- Charge regeneration countdown
- Hover tooltip with pixel info
- Cannot paint without charges
- Cannot paint with locked color
Profile
- View own profile
- Edit username
- Edit discord
- Toggle show last pixel
- View unlocked colors
- View unlocked flags
- Display correct level
Alliance
- Create alliance
- Join alliance via invite
- Leave alliance
- Update description (admin)
- Set HQ location (admin)
- View members list (paginated)
- Promote member (owner only)
- Ban member (admin)
- Unban member (admin)
- View alliance leaderboard
Leaderboards
- Player leaderboard (all time modes)
- Alliance leaderboard (all time modes)
- Correct sorting by pixels painted
- Display alliance affiliation for players
- Display equipped flags
Store
- Purchase max charges
- Purchase paint charges
- Purchase color unlock
- Purchase flag unlock
- Equip purchased flag
- Cannot purchase without droplets
- Cannot equip non-owned flag
Admin Panel
- Search user by ID
- View user details
- Add user note
- Set user droplets
- View open tickets
- View closed tickets
- Count tickets by reason
- Search alliances
- View alliance details
Moderation Panel
- View assigned tickets
- View all open tickets
- Count severe tickets
- View user ticket history
Known Limitations / TODOs
Backend TODOs (frontend should account for):
- Region system returns placeholder data
- Country/region leaderboards not fully implemented
- Ticket assignment system not implemented
- Purchase history not tracked
- User ban/timeout system incomplete
- Phone verification not implemented
- Same IP account detection not implemented
- Report counts not implemented
Frontend recommendations:
- Add WebSocket support for real-time pixel updates
- Implement efficient tile caching strategy
- Add undo/redo for painting
- Add eyedropper tool (pick color from canvas)
- Add minimap for navigation
- Add search functionality for map locations
- Add notification system for alliance events
- Add dark mode toggle (update meta tag)
Development Setup
-
Initialize SvelteKit project:
npm create svelte@latest frontend cd frontend npm install -
Install dependencies:
npm install -D @sveltejs/adapter-static npm install leaflet # or mapbox-gl npm install @types/leaflet -D -
Configure for static build: Update
svelte.config.jsto useadapter-static -
Environment variables: Create
.env:PUBLIC_API_URL=http://localhost:3000 PUBLIC_SEASON=s1 -
Development:
npm run dev -
Build:
npm run buildOutput to
build/directory, copy to backend'sfrontend/folder
API Response Error Handling
All endpoints follow consistent error format:
{
error: string, // Error message
status: number // HTTP status code
}
Common status codes:
- 400: Bad Request (validation error)
- 401: Unauthorized (not logged in)
- 403: Forbidden (insufficient permissions, banned, timed out, or not enough resources)
- 404: Not Found
- 500: Internal Server Error
Frontend should handle:
- Display error messages from
errorfield - Redirect to login on 401
- Show appropriate UI feedback for 403 (e.g., "You don't have permission")
- Retry on 500 with exponential backoff
Final Notes
This TODO document provides a comprehensive reference for recreating the frontend. The backend API is fully functional and documented here. The frontend should be built as a SvelteKit static site that communicates with this backend via the documented API endpoints.
Key priorities:
- Authentication and session management
- Main canvas view with painting functionality
- User profile and settings
- Alliance system
- Leaderboards
- Store
- Admin/moderation panels
The compiled frontend in the current frontend/ folder can serve as a reference for styling and UX patterns, but the source code needs to be recreated from scratch based on this documentation.