This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
- Run games-web (every public web tenant):
bun run dev --filter=games-web— openhttp://palia.localhost:3100/,http://www.localhost:3100/,http://app.localhost:3100/dashboardetc. in a browser to test as a specific tenant.*.localhostresolves to loopback per RFC 6761; no/etc/hostsentry needed. - Run an Overwolf app:
bun run dev:overwolforbun run dev:{game}(e.g.dev:palworldtargetspalworld-overwolf). - Local data-forge: open
http://palia-dev.localhost:3100/(any tenant +-devsuffix) to serve that tenant against a local data-forge dev server onlocalhost:33033instead of prod — per tab, no restart; plainpalia.localhost:3100keeps using prod data. Works via a dev-only same-origin proxy (seeFORGE_DEV_PROXYinpackages/lib/src/config.tsandapps/games-web/src/proxy.ts). For Overwolf apps (no host to vary), overrideNEXT_PUBLIC_DATA_FORGE_URL/NEXT_PUBLIC_DATA_FORGE_CDN_URL/NEXT_PUBLIC_TH_GL_URL/NEXT_PUBLIC_API_FORGE_URLin the environment instead (wired into Vite via@repo/lib/vite-define). - CLI host override:
curl -H "Host: palia.th.gl" http://localhost:3100/.
⚠️ Before finishing any change, runbun run verify— typecheck + lint across the monorepo (CI-parity; this is what GitHub Actions enforces). It is concurrency-capped + heap-bumped so it does NOT OOM the way a rawturbo run typecheck lintdoes (~12 paralleltscexhausts memory). Treat a cleanbun run verifyas the bar for "done".- Build:
bun run buildorturbo run build - Typecheck:
bun run typecheck - Lint:
bun run lint - Clean:
bun run clean - Update dependencies:
bun run update-deps - Git hooks (auto-installed via
prepare→core.hooksPath=.githooks):pre-commitformats staged files with Prettier;pre-pushrunsbun run verify. Bypass in a pinch with--no-verify. - Before committing, confirm
git branch --show-current— the checkout is shared across parallel Claude sessions; another session may have switched to a WIP branch between your turns (a 2026-07-27 commit landed on a stray branch this way).
No test framework is configured - the project relies on TypeScript, linting, and formatting for code quality.
This is a TurboRepo monorepo for The Hidden Gaming Lair, containing one multi-tenant Next.js container that serves every public web surface plus nine game-specific Overwolf overlay apps.
-
apps/games-web/— multi-tenant Next.js container. One Docker image, deployed on Bunny Magic Containers, serves every game site (palia.th.gl,avowed.th.gl,oncehuman.th.gl, etc.), the THGLApp WebView2 surface (app.th.gl), and the marketing site (www.th.gl). Middleware dispatches byHostheader → per-tenantAppConfiginsrc/configs/{slug}.ts. Seeapps/games-web/README.mdfor the full routing model. -
apps/{game}-overwolf/— Vite-based Overwolf overlay per game. Each ships independently (Overwolf store distribution requires its own .opk per game). -
Shared Packages (
packages/):@repo/lib: Core logic, types, utilities, game configurations@repo/ui: Shared React components using Radix UI + Tailwind- Component folders organized by usage context (see UI Package Structure below)
- Config packages:
config-eslint,config-typescript,config-tailwind
apps/games-web/src/configs/{slug}.ts— per-tenantAppConfig(registered insrc/configs/index.ts). Driven by the request's first hostname subdomain.apps/{game}-overwolf/src/config.ts— per-Overwolf-app config.- Game definitions live in
packages/lib/src/games.ts.
packages/lib/src/games.ts (games: Game[]) is canonical. The per-surface
configs only carry surface-specific fields plus a name that links back to
Game.id; the shared fields — title, domain, markerOptions — are not
re-declared there. They are derived from the linked Game by resolvers in
@repo/lib:
- Web config:
export const x = resolveAppConfig({ name: "<game-id>", ... })(omittitle/domain/markerOptions— they come from theGame). - Overwolf config:
export const APP_CONFIG = resolveOverwolfConfig({ name, gameClassId, appId, appUrl, discordApplicationId }). Overwolf store identifiers (appId/appUrl) stay in the overwolf config and are never hoisted to the publicGameregistry (some are private apps). domainis derived from theGame.websubdomain viagetAppDomain;markerOptionsviagetGameMarkerOptions(top-levelGame.markerOptions, falling back tocompanion.markerOptions). Helpers + resolvers + the strict output types (AppConfig/OverwolfAppConfig) and authoring input types (AppConfigInput/OverwolfAppConfigInput) all live inpackages/lib/src/config.ts.- Configs with no
Gameentry (e.g.thgl-web,thgl-app, in-development games) must supply their owntitle/domainin the config.
- Runtime: Bun (package manager and runtime)
- Frameworks: Next.js (games-web container), Vite (Overwolf apps)
- UI: React + TypeScript + Tailwind CSS + Radix UI
- State: Zustand
- Maps: Custom WebGL2 engine for interactive game maps
- All paths must be absolute, not relative
- Follow existing code patterns and conventions in the codebase
games-webauto-deploys to Bunny via GitHub Actions (games-web-deploy.yml). Overwolf apps require manual updates.- No direct pushes to main - all changes via PR
- Use
.env.examplefiles for environment variable templates (never commit.envfiles) - Repository is source-available but NOT open source - code cannot be reused for other projects
- Format code with Prettier, ensure ESLint passes before committing
The codebase uses reusable components to maintain consistency and reduce duplication:
- PageShell: Wrapper for page content with consistent spacing and max-width
- PageHeader: Standardized page headers with title and description
- ViewMoreLink: Consistent "view all" links with arrow icons
- InfoCard: General-purpose card for links with optional badges, icons, and descriptions
- Used by: PartnerCard, PlatformCard
- Supports: external links, custom badge variants, h2/h3 title sizes
- BenefitList: Icon + description lists with configurable styling
- Supports: emoji strings or Lucide icons, optional labels, size/spacing variants
- Used in: partner-program, advertise pages
- Check for existing reusable components before creating new ones
- Look for repeated patterns (3+ occurrences) that could be extracted into components
- Prefer composition over duplication
- Keep components flexible with optional props and sensible defaults
The @repo/ui package is organized into component folders by usage context to optimize tree-shaking and prevent circular dependencies:
- (overwolf): Overwolf-exclusive components (ads, app shell, hotkeys, resize borders)
- Only used by
{game-name}-overwolfapps - Import via
@repo/ui/overwolf
- Only used by
- (desktop): Shared desktop components used by both Overwolf apps and thgl-app
- Components: StreamingSender, MapContainer, QR
- Import via
@repo/ui/desktop
- (thgl-app): thgl-app-specific components (companion app for Windows)
- Import via
@repo/ui/thgl-app
- Import via
- (apps): Shared app-level components (RootLayout, MapPage, etc.)
- Used by web and desktop apps
- Import via
@repo/ui/apps
- (controls): UI controls and dialogs (Actions, Toaster, Settings, etc.)
- Import via
@repo/ui/controls - Note: MarkersSearch exported separately via
@repo/ui/markers-search
- Import via
- (interactive-map): Map components (InteractiveMap, Markers, LivePlayer, etc.)
- Import via
@repo/ui/interactive-map
- Import via
- (header): Header components (Header, Brand, Account, PlausibleTracker)
- Import via
@repo/ui/header
- Import via
- (providers): Context providers (I18NProvider, TooltipProvider, CoordinatesProvider)
- Import via
@repo/ui/providers
- Import via
- (content): Content display components (markdown, discord messages, etc.)
- Import via
@repo/ui/content
- Import via
- (data): Data display components (spawns lists, map guides, progress tracking)
- Import via
@repo/ui/data
- Import via
- (peer): P2P collaboration components (Whiteboard)
- Import via
@repo/ui/peer
- Import via
- (ads): Ad components for web apps
- Import via
@repo/ui/ads
- Import via
- Barrel files use named exports only - No wildcard
export *to enable proper tree-shaking - Avoid circular dependencies - Within
packages/ui, use direct relative imports (e.g.,../ui/button) instead of@repo/ui/*imports - Break dependency chains - Components should not import from unrelated feature folders
- Client components - "use client" components in barrel files can prevent tree-shaking in Next.js; isolated exports help (e.g.,
@repo/ui/markers-search) - Package.json exports - All component folders are explicitly exported in package.json for controlled access
When asked to analyze Overwolf log files, refer to .claude/overwolf-logs-analysis.md for detailed instructions.
Log Location: %LOCALAPPDATA%\Overwolf\Log
Quick Reference - Log Types:
| Log File | Pattern | Purpose |
|---|---|---|
| Trace | Trace_*.log |
Platform actions, system state, errors |
| Game HTML | <GameName>_*.Game.html |
Game integration, DLL injection, overlay rendering |
| OBS | OBS logs | Recording engine, encoder, audio/video devices |
| OverwolfPerf | OverwolfPerf logs | CPU/memory usage per process |
| DxDiag | DxDiag.txt | System hardware, drivers, DirectX info |
Analysis Priority:
- Start with Trace logs - search for
ERRORandWARNentries - Check Game HTML logs - for overlay/rendering issues
- Review OverwolfPerf - for performance complaints
- Check DxDiag - for hardware/driver compatibility
Hotkeys for desktop apps (Overwolf and THGL Companion App) require updates in multiple locations:
-
Overwolf Apps - Update ALL
manifest.jsonfiles inapps/*-overwolf/:"hotkeys": { "hotkey_name": { "title": "Human Readable Title", "action-type": "custom", "default": "Shift+F5" } }
-
THGL App & Settings Store - Update
packages/lib/src/settings.ts:// In DEFAULT_PROFILE_SETTINGS.hotkeys: hotkeys: { toggle_app: "F6", zoom_in_app: "F7", zoom_out_app: "F8", toggle_lock_app: "F9", discover_node: "F10", toggle_live_mode: "F5", toggle_overlay_fullscreen: "Shift+F9", show_labels: "Shift+F5", },
-
Hotkey Handler - Implement in
packages/ui/src/components/(overwolf)/map-hotkeys.tsxor equivalent
All 9 Overwolf apps need identical hotkey configurations:
apps/avowed-overwolf/manifest.jsonapps/diablo4-overwolf/manifest.jsonapps/hogwarts-legacy-overwolf/manifest.jsonapps/once-human-overwolf/manifest.jsonapps/palia-overwolf/manifest.jsonapps/palworld-overwolf/manifest.jsonapps/pax-dei-overwolf/manifest.jsonapps/satisfactory-overwolf/manifest.jsonapps/wuthering-waves-overwolf/manifest.json
Some features are gated behind Preview Release access for Elite Supporters.
// In component:
const hasPreviewAccess = useAccountStore(
(state) => state.perks.previewReleaseAccess,
);
// Conditionally render or enable feature:
if (hasPreviewAccess) {
// Show preview feature
}- Marker Labels (label mode per filter, text size, hotkey toggle)
When a preview feature is ready for all users:
- Remove
hasPreviewAccesschecks from the code - Update release notes to announce public availability
- Add new preview feature to maintain supporter value
The filter settings popover (packages/ui/src/components/(controls)/filter-settings-popover.tsx) provides per-filter configuration.
- Icon Size:
iconSizeByFilterin settings store - Audio Alert:
audioAlertByFilterin settings store - Label Mode:
labelModeByFilterin settings store (preview access)
-
Settings Store (
packages/lib/src/settings.ts):// Add to ProfileSettings type: newSettingByFilter: Record<string, ValueType>; // Add to DEFAULT_PROFILE_SETTINGS: newSettingByFilter: {}, // Add action: setNewSettingByFilter: (filterId: string, value: ValueType) => void;
-
Filter Settings Popover - Add UI control in the popover
-
Markers Component - Read setting and apply to markers
Some settings support group-level control (all filters in a group):
- Use
setNewSettingByFilters(filterIds[], value)for batch updates - Check for "mixed" state when group has different values per filter
For maps with thousands of markers and real-time updates:
Located in packages/ui/src/components/(interactive-map)/spatial-grid.ts
// O(k) queries instead of O(n) iteration
const spatialGrid = new SpatialGrid<CanvasMarker>(cellSize);
spatialGrid.add(marker, x, y);
spatialGrid.getNearby(playerX, playerY, maxDistance);Use when: Checking proximity for many markers (audio alerts, z-position, labels)
Located in packages/ui/src/components/(interactive-map)/canvas-marker.ts
- Cache rendered marker canvases by unique key
- Clear cache on zoom change (
clearCanvasCache()) - Include all visual properties in cache key (icon, size, color, colorBlind settings)
Instead of multiple O(n) passes over markers:
// BAD: 3 separate loops
markers.forEach(checkZPosition);
markers.forEach(checkAudioAlert);
markers.forEach(updateLabels);
// GOOD: Single consolidated loop
for (const marker of markers) {
// Check z-position
// Check audio alert
// Update labels
}// Memoize expensive calculations
const rotatedPlayer = useMemo(() => {
if (!player || !rotationCache) return null;
const rotated = rotationCache.getRotated(player.x, player.y);
return { x: rotated[0], y: rotated[1], z: player.z };
}, [player, rotationCache]);When manipulating canvas pixels:
// BAD: Multiple getImageData/putImageData cycles
applyFillColor(context); // getImageData + putImageData
applyColorBlind(context); // getImageData + putImageData
// GOOD: Single cycle for all transforms
const imageData = context.getImageData(0, 0, width, height);
applyFillColorToData(imageData.data);
applyColorBlindToData(imageData.data);
context.putImageData(imageData, 0, 0);- Implement feature/fix
- Run
bun run typecheckto verify - Commit with descriptive message
- Create PR for review (no direct pushes to main)
After merging:
-
Discord (
#app-updateschannel):- Use bold headers, no emojis
- Include role mentions for relevant games
- Add support links and app links at top
- Attach screenshots if applicable
-
Patreon (for significant updates):
- Similar format to Discord but more personal tone
- Start with "Hey everyone!"
- End with "— DevLeon"
- Highlight supporter-exclusive features
_To get pinged for future updates, claim the @{Game} role in <id:customize>_
You can [support me](https://www.th.gl/support-me) (no more ads) and by sharing this project on social media.
[{game}.th.gl](https://{game}.th.gl)
[THGL Companion App](https://www.th.gl/companion-app)
**Feature Title**
Description of the feature...
How it works
- Step 1
- Step 2
**Bug Fixes**
- Fix description