feat: add continent filtering to Know Your World game
Add continent-based filtering for the world map to make the game more playable by reducing visual clutter: Continent Selection: - Add continent selector to setup screen (only for world map) - 7 continents + "All" option: Africa, Asia, Europe, North America, South America, Oceania, Antarctica - Each continent button shows emoji and name Map Filtering: - Filter world map regions by selected continent using ISO 3166-1 country codes - Calculate bounding box for filtered regions - Automatically crop and scale viewBox to show only selected continent - Add 10% padding around continent bounding box for better visibility Technical Implementation: - Create continents.ts with comprehensive country-to-continent mappings (256 countries) - Add getFilteredMapData() function to filter regions and adjust viewBox - Add calculateBoundingBox() to compute min/max coordinates from SVG paths - Add selectedContinent field to game state and config (persisted to database) - Add SET_CONTINENT move type and validator - Update all map rendering components (StudyPhase, PlayingPhase, MapRenderer) Benefits: - Solves "too many small countries" problem on world map - Allows focused study of specific geographic regions - Dynamic viewBox adjustment provides optimal zoom level - Maintains full world map option for comprehensive play 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
25e24a7cbc
commit
7bb03b8409
|
|
@ -30,6 +30,7 @@ interface KnowYourWorldContextValue {
|
||||||
setMode: (mode: 'cooperative' | 'race' | 'turn-based') => void
|
setMode: (mode: 'cooperative' | 'race' | 'turn-based') => void
|
||||||
setDifficulty: (difficulty: 'easy' | 'hard') => void
|
setDifficulty: (difficulty: 'easy' | 'hard') => void
|
||||||
setStudyDuration: (duration: 0 | 30 | 60 | 120) => void
|
setStudyDuration: (duration: 0 | 30 | 60 | 120) => void
|
||||||
|
setContinent: (continent: import('./continents').ContinentId | 'all') => void
|
||||||
}
|
}
|
||||||
|
|
||||||
const KnowYourWorldContext = createContext<KnowYourWorldContextValue | null>(null)
|
const KnowYourWorldContext = createContext<KnowYourWorldContextValue | null>(null)
|
||||||
|
|
@ -59,12 +60,30 @@ export function KnowYourWorldProvider({ children }: { children: React.ReactNode
|
||||||
const studyDuration: 0 | 30 | 60 | 120 =
|
const studyDuration: 0 | 30 | 60 | 120 =
|
||||||
rawDuration === 30 || rawDuration === 60 || rawDuration === 120 ? rawDuration : 0
|
rawDuration === 30 || rawDuration === 60 || rawDuration === 120 ? rawDuration : 0
|
||||||
|
|
||||||
|
// Validate selectedContinent
|
||||||
|
const rawContinent = gameConfig?.selectedContinent
|
||||||
|
const validContinents = [
|
||||||
|
'all',
|
||||||
|
'africa',
|
||||||
|
'asia',
|
||||||
|
'europe',
|
||||||
|
'north-america',
|
||||||
|
'south-america',
|
||||||
|
'oceania',
|
||||||
|
'antarctica',
|
||||||
|
]
|
||||||
|
const selectedContinent: import('./continents').ContinentId | 'all' =
|
||||||
|
typeof rawContinent === 'string' && validContinents.includes(rawContinent)
|
||||||
|
? (rawContinent as any)
|
||||||
|
: 'all'
|
||||||
|
|
||||||
return {
|
return {
|
||||||
gamePhase: 'setup' as const,
|
gamePhase: 'setup' as const,
|
||||||
selectedMap: (gameConfig?.selectedMap as 'world' | 'usa') || 'world',
|
selectedMap: (gameConfig?.selectedMap as 'world' | 'usa') || 'world',
|
||||||
gameMode: (gameConfig?.gameMode as 'cooperative' | 'race' | 'turn-based') || 'cooperative',
|
gameMode: (gameConfig?.gameMode as 'cooperative' | 'race' | 'turn-based') || 'cooperative',
|
||||||
difficulty: (gameConfig?.difficulty as 'easy' | 'hard') || 'easy',
|
difficulty: (gameConfig?.difficulty as 'easy' | 'hard') || 'easy',
|
||||||
studyDuration,
|
studyDuration,
|
||||||
|
selectedContinent,
|
||||||
studyTimeRemaining: 0,
|
studyTimeRemaining: 0,
|
||||||
studyStartTime: 0,
|
studyStartTime: 0,
|
||||||
currentPrompt: null,
|
currentPrompt: null,
|
||||||
|
|
@ -277,6 +296,36 @@ export function KnowYourWorldProvider({ children }: { children: React.ReactNode
|
||||||
})
|
})
|
||||||
}, [viewerId, sendMove])
|
}, [viewerId, sendMove])
|
||||||
|
|
||||||
|
// Setup Action: Set Continent
|
||||||
|
const setContinent = useCallback(
|
||||||
|
(selectedContinent: import('./continents').ContinentId | 'all') => {
|
||||||
|
sendMove({
|
||||||
|
type: 'SET_CONTINENT',
|
||||||
|
playerId: viewerId || 'player-1',
|
||||||
|
userId: viewerId || '',
|
||||||
|
data: { selectedContinent },
|
||||||
|
})
|
||||||
|
|
||||||
|
// Persist to database
|
||||||
|
if (roomData?.id) {
|
||||||
|
const currentGameConfig = (roomData.gameConfig as Record<string, any>) || {}
|
||||||
|
const currentConfig = (currentGameConfig['know-your-world'] as Record<string, any>) || {}
|
||||||
|
|
||||||
|
updateGameConfig({
|
||||||
|
roomId: roomData.id,
|
||||||
|
gameConfig: {
|
||||||
|
...currentGameConfig,
|
||||||
|
'know-your-world': {
|
||||||
|
...currentConfig,
|
||||||
|
selectedContinent,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[viewerId, sendMove, roomData, updateGameConfig]
|
||||||
|
)
|
||||||
|
|
||||||
// Action: Return to Setup
|
// Action: Return to Setup
|
||||||
const returnToSetup = useCallback(() => {
|
const returnToSetup = useCallback(() => {
|
||||||
sendMove({
|
sendMove({
|
||||||
|
|
@ -304,6 +353,7 @@ export function KnowYourWorldProvider({ children }: { children: React.ReactNode
|
||||||
setMode,
|
setMode,
|
||||||
setDifficulty,
|
setDifficulty,
|
||||||
setStudyDuration,
|
setStudyDuration,
|
||||||
|
setContinent,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ import type {
|
||||||
KnowYourWorldState,
|
KnowYourWorldState,
|
||||||
GuessRecord,
|
GuessRecord,
|
||||||
} from './types'
|
} from './types'
|
||||||
import { getMapData } from './maps'
|
import { getMapData, getFilteredMapData } from './maps'
|
||||||
|
|
||||||
export class KnowYourWorldValidator
|
export class KnowYourWorldValidator
|
||||||
implements GameValidator<KnowYourWorldState, KnowYourWorldMove>
|
implements GameValidator<KnowYourWorldState, KnowYourWorldMove>
|
||||||
|
|
@ -32,6 +32,8 @@ export class KnowYourWorldValidator
|
||||||
return this.validateSetDifficulty(state, move.data.difficulty)
|
return this.validateSetDifficulty(state, move.data.difficulty)
|
||||||
case 'SET_STUDY_DURATION':
|
case 'SET_STUDY_DURATION':
|
||||||
return this.validateSetStudyDuration(state, move.data.studyDuration)
|
return this.validateSetStudyDuration(state, move.data.studyDuration)
|
||||||
|
case 'SET_CONTINENT':
|
||||||
|
return this.validateSetContinent(state, move.data.selectedContinent)
|
||||||
default:
|
default:
|
||||||
return { valid: false, error: 'Unknown move type' }
|
return { valid: false, error: 'Unknown move type' }
|
||||||
}
|
}
|
||||||
|
|
@ -56,10 +58,11 @@ export class KnowYourWorldValidator
|
||||||
return { valid: false, error: 'Need at least 1 player' }
|
return { valid: false, error: 'Need at least 1 player' }
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get map data and shuffle regions
|
// Get map data and shuffle regions (with continent filter if applicable)
|
||||||
const mapData = getMapData(selectedMap)
|
const mapData = getFilteredMapData(selectedMap, state.selectedContinent)
|
||||||
console.log('[KnowYourWorld Validator] Map data loaded:', {
|
console.log('[KnowYourWorld Validator] Map data loaded:', {
|
||||||
map: mapData.id,
|
map: mapData.id,
|
||||||
|
continent: state.selectedContinent,
|
||||||
regionsCount: mapData.regions.length,
|
regionsCount: mapData.regions.length,
|
||||||
})
|
})
|
||||||
const regionIds = mapData.regions.map((r) => r.id)
|
const regionIds = mapData.regions.map((r) => r.id)
|
||||||
|
|
@ -220,8 +223,8 @@ export class KnowYourWorldValidator
|
||||||
return { valid: false, error: 'Can only start next round from results' }
|
return { valid: false, error: 'Can only start next round from results' }
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get map data and shuffle regions
|
// Get map data and shuffle regions (with continent filter if applicable)
|
||||||
const mapData = getMapData(state.selectedMap)
|
const mapData = getFilteredMapData(state.selectedMap, state.selectedContinent)
|
||||||
const regionIds = mapData.regions.map((r) => r.id)
|
const regionIds = mapData.regions.map((r) => r.id)
|
||||||
const shuffledRegions = this.shuffleArray([...regionIds])
|
const shuffledRegions = this.shuffleArray([...regionIds])
|
||||||
|
|
||||||
|
|
@ -330,6 +333,22 @@ export class KnowYourWorldValidator
|
||||||
return { valid: true, newState }
|
return { valid: true, newState }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private validateSetContinent(
|
||||||
|
state: KnowYourWorldState,
|
||||||
|
selectedContinent: import('./continents').ContinentId | 'all'
|
||||||
|
): ValidationResult {
|
||||||
|
if (state.gamePhase !== 'setup') {
|
||||||
|
return { valid: false, error: 'Can only change continent during setup' }
|
||||||
|
}
|
||||||
|
|
||||||
|
const newState: KnowYourWorldState = {
|
||||||
|
...state,
|
||||||
|
selectedContinent,
|
||||||
|
}
|
||||||
|
|
||||||
|
return { valid: true, newState }
|
||||||
|
}
|
||||||
|
|
||||||
private validateEndStudy(state: KnowYourWorldState): ValidationResult {
|
private validateEndStudy(state: KnowYourWorldState): ValidationResult {
|
||||||
if (state.gamePhase !== 'studying') {
|
if (state.gamePhase !== 'studying') {
|
||||||
return { valid: false, error: 'Can only end study during studying phase' }
|
return { valid: false, error: 'Can only end study during studying phase' }
|
||||||
|
|
@ -389,6 +408,7 @@ export class KnowYourWorldValidator
|
||||||
gameMode: typedConfig?.gameMode || 'cooperative',
|
gameMode: typedConfig?.gameMode || 'cooperative',
|
||||||
difficulty: typedConfig?.difficulty || 'easy',
|
difficulty: typedConfig?.difficulty || 'easy',
|
||||||
studyDuration: typedConfig?.studyDuration || 0,
|
studyDuration: typedConfig?.studyDuration || 0,
|
||||||
|
selectedContinent: typedConfig?.selectedContinent || 'all',
|
||||||
studyTimeRemaining: 0,
|
studyTimeRemaining: 0,
|
||||||
studyStartTime: 0,
|
studyStartTime: 0,
|
||||||
currentPrompt: null,
|
currentPrompt: null,
|
||||||
|
|
|
||||||
|
|
@ -81,7 +81,7 @@ function isSmallRegion(bbox: BoundingBox, viewBox: string): boolean {
|
||||||
// Thresholds (relative to map size)
|
// Thresholds (relative to map size)
|
||||||
const minWidth = mapWidth * 0.025 // 2.5% of map width
|
const minWidth = mapWidth * 0.025 // 2.5% of map width
|
||||||
const minHeight = mapHeight * 0.025 // 2.5% of map height
|
const minHeight = mapHeight * 0.025 // 2.5% of map height
|
||||||
const minArea = (mapWidth * mapHeight) * 0.001 // 0.1% of total map area
|
const minArea = mapWidth * mapHeight * 0.001 // 0.1% of total map area
|
||||||
|
|
||||||
return bbox.width < minWidth || bbox.height < minHeight || bbox.area < minArea
|
return bbox.width < minWidth || bbox.height < minHeight || bbox.area < minArea
|
||||||
}
|
}
|
||||||
|
|
@ -116,10 +116,7 @@ export function MapRenderer({
|
||||||
regionId: region.id,
|
regionId: region.id,
|
||||||
regionName: region.name,
|
regionName: region.name,
|
||||||
regionCenter: region.center,
|
regionCenter: region.center,
|
||||||
labelPosition: [
|
labelPosition: [region.center[0] + offsetX, region.center[1] + offsetY],
|
||||||
region.center[0] + offsetX,
|
|
||||||
region.center[1] + offsetY,
|
|
||||||
],
|
|
||||||
isFound: regionsFound.includes(region.id),
|
isFound: regionsFound.includes(region.id),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
@ -229,8 +226,10 @@ export function MapRenderer({
|
||||||
y={label.labelPosition[1] - 12}
|
y={label.labelPosition[1] - 12}
|
||||||
width={label.regionName.length * 6 + 10}
|
width={label.regionName.length * 6 + 10}
|
||||||
height={20}
|
height={20}
|
||||||
fill={label.isFound ? (isDark ? '#22c55e' : '#86efac') : (isDark ? '#1f2937' : '#ffffff')}
|
fill={
|
||||||
stroke={label.isFound ? '#16a34a' : (isDark ? '#60a5fa' : '#3b82f6')}
|
label.isFound ? (isDark ? '#22c55e' : '#86efac') : isDark ? '#1f2937' : '#ffffff'
|
||||||
|
}
|
||||||
|
stroke={label.isFound ? '#16a34a' : isDark ? '#60a5fa' : '#3b82f6'}
|
||||||
strokeWidth={2}
|
strokeWidth={2}
|
||||||
rx={4}
|
rx={4}
|
||||||
style={{
|
style={{
|
||||||
|
|
@ -269,22 +268,11 @@ export function MapRenderer({
|
||||||
|
|
||||||
{/* Arrow marker definition */}
|
{/* Arrow marker definition */}
|
||||||
<defs>
|
<defs>
|
||||||
<marker
|
<marker id="arrowhead" markerWidth="10" markerHeight="10" refX="8" refY="3" orient="auto">
|
||||||
id="arrowhead"
|
<polygon points="0 0, 10 3, 0 6" fill={isDark ? '#60a5fa' : '#3b82f6'} />
|
||||||
markerWidth="10"
|
|
||||||
markerHeight="10"
|
|
||||||
refX="8"
|
|
||||||
refY="3"
|
|
||||||
orient="auto"
|
|
||||||
>
|
|
||||||
<polygon
|
|
||||||
points="0 0, 10 3, 0 6"
|
|
||||||
fill={isDark ? '#60a5fa' : '#3b82f6'}
|
|
||||||
/>
|
|
||||||
</marker>
|
</marker>
|
||||||
</defs>
|
</defs>
|
||||||
</svg>
|
</svg>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ import { useEffect } from 'react'
|
||||||
import { css } from '@styled/css'
|
import { css } from '@styled/css'
|
||||||
import { useTheme } from '@/contexts/ThemeContext'
|
import { useTheme } from '@/contexts/ThemeContext'
|
||||||
import { useKnowYourWorld } from '../Provider'
|
import { useKnowYourWorld } from '../Provider'
|
||||||
import { getMapData } from '../maps'
|
import { getFilteredMapData } from '../maps'
|
||||||
import { MapRenderer } from './MapRenderer'
|
import { MapRenderer } from './MapRenderer'
|
||||||
|
|
||||||
export function PlayingPhase() {
|
export function PlayingPhase() {
|
||||||
|
|
@ -12,7 +12,7 @@ export function PlayingPhase() {
|
||||||
const isDark = resolvedTheme === 'dark'
|
const isDark = resolvedTheme === 'dark'
|
||||||
const { state, clickRegion, lastError, clearError } = useKnowYourWorld()
|
const { state, clickRegion, lastError, clearError } = useKnowYourWorld()
|
||||||
|
|
||||||
const mapData = getMapData(state.selectedMap)
|
const mapData = getFilteredMapData(state.selectedMap, state.selectedContinent)
|
||||||
const totalRegions = mapData.regions.length
|
const totalRegions = mapData.regions.length
|
||||||
const foundCount = state.regionsFound.length
|
const foundCount = state.regionsFound.length
|
||||||
const progress = (foundCount / totalRegions) * 100
|
const progress = (foundCount / totalRegions) * 100
|
||||||
|
|
|
||||||
|
|
@ -3,11 +3,13 @@
|
||||||
import { css } from '@styled/css'
|
import { css } from '@styled/css'
|
||||||
import { useTheme } from '@/contexts/ThemeContext'
|
import { useTheme } from '@/contexts/ThemeContext'
|
||||||
import { useKnowYourWorld } from '../Provider'
|
import { useKnowYourWorld } from '../Provider'
|
||||||
|
import { CONTINENTS } from '../continents'
|
||||||
|
|
||||||
export function SetupPhase() {
|
export function SetupPhase() {
|
||||||
const { resolvedTheme } = useTheme()
|
const { resolvedTheme } = useTheme()
|
||||||
const isDark = resolvedTheme === 'dark'
|
const isDark = resolvedTheme === 'dark'
|
||||||
const { state, startGame, setMap, setMode, setDifficulty, setStudyDuration } = useKnowYourWorld()
|
const { state, startGame, setMap, setMode, setDifficulty, setStudyDuration, setContinent } =
|
||||||
|
useKnowYourWorld()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
|
|
@ -108,6 +110,94 @@ export function SetupPhase() {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Continent Selection (only for World map) */}
|
||||||
|
{state.selectedMap === 'world' && (
|
||||||
|
<div data-section="continent-selection">
|
||||||
|
<h2
|
||||||
|
className={css({
|
||||||
|
fontSize: '2xl',
|
||||||
|
fontWeight: 'bold',
|
||||||
|
marginBottom: '4',
|
||||||
|
color: isDark ? 'gray.100' : 'gray.900',
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
Focus on Continent 🌐
|
||||||
|
</h2>
|
||||||
|
<div
|
||||||
|
className={css({
|
||||||
|
display: 'grid',
|
||||||
|
gridTemplateColumns: 'repeat(4, 1fr)',
|
||||||
|
gap: '3',
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
{/* All continents option */}
|
||||||
|
<button
|
||||||
|
data-action="select-all-continents"
|
||||||
|
onClick={() => setContinent('all')}
|
||||||
|
className={css({
|
||||||
|
padding: '3',
|
||||||
|
rounded: 'lg',
|
||||||
|
border: '2px solid',
|
||||||
|
borderColor: state.selectedContinent === 'all' ? 'blue.500' : 'transparent',
|
||||||
|
bg:
|
||||||
|
state.selectedContinent === 'all'
|
||||||
|
? isDark
|
||||||
|
? 'blue.900'
|
||||||
|
: 'blue.50'
|
||||||
|
: isDark
|
||||||
|
? 'gray.800'
|
||||||
|
: 'gray.100',
|
||||||
|
color: isDark ? 'gray.100' : 'gray.900',
|
||||||
|
cursor: 'pointer',
|
||||||
|
transition: 'all 0.2s',
|
||||||
|
_hover: {
|
||||||
|
borderColor: 'blue.400',
|
||||||
|
},
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<div className={css({ fontSize: '2xl', marginBottom: '1' })}>🌍</div>
|
||||||
|
<div className={css({ fontSize: 'sm', fontWeight: 'bold' })}>All</div>
|
||||||
|
<div className={css({ fontSize: '2xs', color: isDark ? 'gray.400' : 'gray.600' })}>
|
||||||
|
Whole world
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{/* Individual continents */}
|
||||||
|
{CONTINENTS.map((continent) => (
|
||||||
|
<button
|
||||||
|
key={continent.id}
|
||||||
|
data-action={`select-${continent.id}-continent`}
|
||||||
|
onClick={() => setContinent(continent.id)}
|
||||||
|
className={css({
|
||||||
|
padding: '3',
|
||||||
|
rounded: 'lg',
|
||||||
|
border: '2px solid',
|
||||||
|
borderColor:
|
||||||
|
state.selectedContinent === continent.id ? 'blue.500' : 'transparent',
|
||||||
|
bg:
|
||||||
|
state.selectedContinent === continent.id
|
||||||
|
? isDark
|
||||||
|
? 'blue.900'
|
||||||
|
: 'blue.50'
|
||||||
|
: isDark
|
||||||
|
? 'gray.800'
|
||||||
|
: 'gray.100',
|
||||||
|
color: isDark ? 'gray.100' : 'gray.900',
|
||||||
|
cursor: 'pointer',
|
||||||
|
transition: 'all 0.2s',
|
||||||
|
_hover: {
|
||||||
|
borderColor: 'blue.400',
|
||||||
|
},
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<div className={css({ fontSize: '2xl', marginBottom: '1' })}>{continent.emoji}</div>
|
||||||
|
<div className={css({ fontSize: 'sm', fontWeight: 'bold' })}>{continent.name}</div>
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Mode Selection */}
|
{/* Mode Selection */}
|
||||||
<div data-section="mode-selection">
|
<div data-section="mode-selection">
|
||||||
<h2
|
<h2
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ import { useEffect, useState } from 'react'
|
||||||
import { css } from '@styled/css'
|
import { css } from '@styled/css'
|
||||||
import { useTheme } from '@/contexts/ThemeContext'
|
import { useTheme } from '@/contexts/ThemeContext'
|
||||||
import { useKnowYourWorld } from '../Provider'
|
import { useKnowYourWorld } from '../Provider'
|
||||||
import { getMapData } from '../maps'
|
import { getFilteredMapData } from '../maps'
|
||||||
import type { MapRegion } from '../types'
|
import type { MapRegion } from '../types'
|
||||||
import { getRegionColor, getLabelTextColor, getLabelTextShadow } from '../mapColors'
|
import { getRegionColor, getLabelTextColor, getLabelTextShadow } from '../mapColors'
|
||||||
|
|
||||||
|
|
@ -15,7 +15,7 @@ export function StudyPhase() {
|
||||||
|
|
||||||
const [timeRemaining, setTimeRemaining] = useState(state.studyTimeRemaining)
|
const [timeRemaining, setTimeRemaining] = useState(state.studyTimeRemaining)
|
||||||
|
|
||||||
const mapData = getMapData(state.selectedMap)
|
const mapData = getFilteredMapData(state.selectedMap, state.selectedContinent)
|
||||||
|
|
||||||
// Countdown timer
|
// Countdown timer
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
@ -102,7 +102,14 @@ export function StudyPhase() {
|
||||||
className={css({
|
className={css({
|
||||||
fontSize: '4xl',
|
fontSize: '4xl',
|
||||||
fontWeight: 'bold',
|
fontWeight: 'bold',
|
||||||
color: timeRemaining <= 10 ? (isDark ? 'red.400' : 'red.600') : isDark ? 'blue.200' : 'blue.800',
|
color:
|
||||||
|
timeRemaining <= 10
|
||||||
|
? isDark
|
||||||
|
? 'red.400'
|
||||||
|
: 'red.600'
|
||||||
|
: isDark
|
||||||
|
? 'blue.200'
|
||||||
|
: 'blue.800',
|
||||||
fontFeatureSettings: '"tnum"',
|
fontFeatureSettings: '"tnum"',
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
|
|
@ -200,7 +207,15 @@ export function StudyPhase() {
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
<p className={css({ fontWeight: 'semibold', marginBottom: '2' })}>💡 Study Tips:</p>
|
<p className={css({ fontWeight: 'semibold', marginBottom: '2' })}>💡 Study Tips:</p>
|
||||||
<ul className={css({ listStyle: 'disc', paddingLeft: '5', display: 'flex', flexDirection: 'column', gap: '1' })}>
|
<ul
|
||||||
|
className={css({
|
||||||
|
listStyle: 'disc',
|
||||||
|
paddingLeft: '5',
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
gap: '1',
|
||||||
|
})}
|
||||||
|
>
|
||||||
<li>Look for patterns - neighboring regions, shapes, sizes</li>
|
<li>Look for patterns - neighboring regions, shapes, sizes</li>
|
||||||
<li>Group regions mentally by area (e.g., Northeast, Southwest)</li>
|
<li>Group regions mentally by area (e.g., Northeast, Southwest)</li>
|
||||||
<li>Focus on the tricky small ones that are hard to see</li>
|
<li>Focus on the tricky small ones that are hard to see</li>
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,309 @@
|
||||||
|
/**
|
||||||
|
* Continent mappings for world countries
|
||||||
|
* Maps ISO 3166-1 alpha-2 country codes to continents
|
||||||
|
*/
|
||||||
|
|
||||||
|
export type ContinentId =
|
||||||
|
| 'africa'
|
||||||
|
| 'asia'
|
||||||
|
| 'europe'
|
||||||
|
| 'north-america'
|
||||||
|
| 'south-america'
|
||||||
|
| 'oceania'
|
||||||
|
| 'antarctica'
|
||||||
|
|
||||||
|
export interface Continent {
|
||||||
|
id: ContinentId
|
||||||
|
name: string
|
||||||
|
emoji: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export const CONTINENTS: Continent[] = [
|
||||||
|
{ id: 'africa', name: 'Africa', emoji: '🌍' },
|
||||||
|
{ id: 'asia', name: 'Asia', emoji: '🌏' },
|
||||||
|
{ id: 'europe', name: 'Europe', emoji: '🇪🇺' },
|
||||||
|
{ id: 'north-america', name: 'North America', emoji: '🌎' },
|
||||||
|
{ id: 'south-america', name: 'South America', emoji: '🌎' },
|
||||||
|
{ id: 'oceania', name: 'Oceania', emoji: '🌏' },
|
||||||
|
{ id: 'antarctica', name: 'Antarctica', emoji: '🇦🇶' },
|
||||||
|
]
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Map of country codes to continents
|
||||||
|
* Based on ISO 3166-1 alpha-2 codes from @svg-maps/world
|
||||||
|
*/
|
||||||
|
export const COUNTRY_TO_CONTINENT: Record<string, ContinentId> = {
|
||||||
|
// Africa
|
||||||
|
dz: 'africa', // Algeria
|
||||||
|
ao: 'africa', // Angola
|
||||||
|
bj: 'africa', // Benin
|
||||||
|
bw: 'africa', // Botswana
|
||||||
|
bf: 'africa', // Burkina Faso
|
||||||
|
bi: 'africa', // Burundi
|
||||||
|
cm: 'africa', // Cameroon
|
||||||
|
cv: 'africa', // Cape Verde
|
||||||
|
cf: 'africa', // Central African Republic
|
||||||
|
td: 'africa', // Chad
|
||||||
|
km: 'africa', // Comoros
|
||||||
|
cg: 'africa', // Congo (Brazzaville)
|
||||||
|
cd: 'africa', // Congo (Kinshasa)
|
||||||
|
ci: 'africa', // Côte d'Ivoire
|
||||||
|
dj: 'africa', // Djibouti
|
||||||
|
eg: 'africa', // Egypt
|
||||||
|
gq: 'africa', // Equatorial Guinea
|
||||||
|
er: 'africa', // Eritrea
|
||||||
|
sz: 'africa', // Eswatini
|
||||||
|
et: 'africa', // Ethiopia
|
||||||
|
ga: 'africa', // Gabon
|
||||||
|
gm: 'africa', // Gambia
|
||||||
|
gh: 'africa', // Ghana
|
||||||
|
gn: 'africa', // Guinea
|
||||||
|
gw: 'africa', // Guinea-Bissau
|
||||||
|
ke: 'africa', // Kenya
|
||||||
|
ls: 'africa', // Lesotho
|
||||||
|
lr: 'africa', // Liberia
|
||||||
|
ly: 'africa', // Libya
|
||||||
|
mg: 'africa', // Madagascar
|
||||||
|
mw: 'africa', // Malawi
|
||||||
|
ml: 'africa', // Mali
|
||||||
|
mr: 'africa', // Mauritania
|
||||||
|
mu: 'africa', // Mauritius
|
||||||
|
yt: 'africa', // Mayotte
|
||||||
|
ma: 'africa', // Morocco
|
||||||
|
mz: 'africa', // Mozambique
|
||||||
|
na: 'africa', // Namibia
|
||||||
|
ne: 'africa', // Niger
|
||||||
|
ng: 'africa', // Nigeria
|
||||||
|
re: 'africa', // Réunion
|
||||||
|
rw: 'africa', // Rwanda
|
||||||
|
st: 'africa', // São Tomé and Príncipe
|
||||||
|
sn: 'africa', // Senegal
|
||||||
|
sc: 'africa', // Seychelles
|
||||||
|
sl: 'africa', // Sierra Leone
|
||||||
|
so: 'africa', // Somalia
|
||||||
|
za: 'africa', // South Africa
|
||||||
|
ss: 'africa', // South Sudan
|
||||||
|
sd: 'africa', // Sudan
|
||||||
|
tz: 'africa', // Tanzania
|
||||||
|
tg: 'africa', // Togo
|
||||||
|
tn: 'africa', // Tunisia
|
||||||
|
ug: 'africa', // Uganda
|
||||||
|
eh: 'africa', // Western Sahara
|
||||||
|
zm: 'africa', // Zambia
|
||||||
|
zw: 'africa', // Zimbabwe
|
||||||
|
|
||||||
|
// Asia
|
||||||
|
af: 'asia', // Afghanistan
|
||||||
|
am: 'asia', // Armenia
|
||||||
|
az: 'asia', // Azerbaijan
|
||||||
|
bh: 'asia', // Bahrain
|
||||||
|
bd: 'asia', // Bangladesh
|
||||||
|
bt: 'asia', // Bhutan
|
||||||
|
bn: 'asia', // Brunei
|
||||||
|
kh: 'asia', // Cambodia
|
||||||
|
cn: 'asia', // China
|
||||||
|
cx: 'asia', // Christmas Island
|
||||||
|
cc: 'asia', // Cocos Islands
|
||||||
|
ge: 'asia', // Georgia
|
||||||
|
hk: 'asia', // Hong Kong
|
||||||
|
in: 'asia', // India
|
||||||
|
id: 'asia', // Indonesia
|
||||||
|
ir: 'asia', // Iran
|
||||||
|
iq: 'asia', // Iraq
|
||||||
|
il: 'asia', // Israel
|
||||||
|
jp: 'asia', // Japan
|
||||||
|
jo: 'asia', // Jordan
|
||||||
|
kz: 'asia', // Kazakhstan
|
||||||
|
kp: 'asia', // North Korea
|
||||||
|
kr: 'asia', // South Korea
|
||||||
|
kw: 'asia', // Kuwait
|
||||||
|
kg: 'asia', // Kyrgyzstan
|
||||||
|
la: 'asia', // Laos
|
||||||
|
lb: 'asia', // Lebanon
|
||||||
|
mo: 'asia', // Macau
|
||||||
|
my: 'asia', // Malaysia
|
||||||
|
mv: 'asia', // Maldives
|
||||||
|
mn: 'asia', // Mongolia
|
||||||
|
mm: 'asia', // Myanmar
|
||||||
|
np: 'asia', // Nepal
|
||||||
|
om: 'asia', // Oman
|
||||||
|
pk: 'asia', // Pakistan
|
||||||
|
ps: 'asia', // Palestine
|
||||||
|
ph: 'asia', // Philippines
|
||||||
|
qa: 'asia', // Qatar
|
||||||
|
sa: 'asia', // Saudi Arabia
|
||||||
|
sg: 'asia', // Singapore
|
||||||
|
lk: 'asia', // Sri Lanka
|
||||||
|
sy: 'asia', // Syria
|
||||||
|
tw: 'asia', // Taiwan
|
||||||
|
tj: 'asia', // Tajikistan
|
||||||
|
th: 'asia', // Thailand
|
||||||
|
tl: 'asia', // Timor-Leste
|
||||||
|
tr: 'asia', // Turkey (transcontinental, but primarily Asian)
|
||||||
|
tm: 'asia', // Turkmenistan
|
||||||
|
ae: 'asia', // United Arab Emirates
|
||||||
|
uz: 'asia', // Uzbekistan
|
||||||
|
vn: 'asia', // Vietnam
|
||||||
|
ye: 'asia', // Yemen
|
||||||
|
|
||||||
|
// Europe
|
||||||
|
ax: 'europe', // Åland Islands
|
||||||
|
al: 'europe', // Albania
|
||||||
|
ad: 'europe', // Andorra
|
||||||
|
at: 'europe', // Austria
|
||||||
|
by: 'europe', // Belarus
|
||||||
|
be: 'europe', // Belgium
|
||||||
|
ba: 'europe', // Bosnia and Herzegovina
|
||||||
|
bg: 'europe', // Bulgaria
|
||||||
|
hr: 'europe', // Croatia
|
||||||
|
cy: 'europe', // Cyprus
|
||||||
|
cz: 'europe', // Czech Republic
|
||||||
|
dk: 'europe', // Denmark
|
||||||
|
ee: 'europe', // Estonia
|
||||||
|
fo: 'europe', // Faroe Islands
|
||||||
|
fi: 'europe', // Finland
|
||||||
|
fr: 'europe', // France
|
||||||
|
de: 'europe', // Germany
|
||||||
|
gi: 'europe', // Gibraltar
|
||||||
|
gr: 'europe', // Greece
|
||||||
|
gg: 'europe', // Guernsey
|
||||||
|
hu: 'europe', // Hungary
|
||||||
|
is: 'europe', // Iceland
|
||||||
|
ie: 'europe', // Ireland
|
||||||
|
im: 'europe', // Isle of Man
|
||||||
|
it: 'europe', // Italy
|
||||||
|
je: 'europe', // Jersey
|
||||||
|
xk: 'europe', // Kosovo
|
||||||
|
lv: 'europe', // Latvia
|
||||||
|
li: 'europe', // Liechtenstein
|
||||||
|
lt: 'europe', // Lithuania
|
||||||
|
lu: 'europe', // Luxembourg
|
||||||
|
mk: 'europe', // North Macedonia
|
||||||
|
mt: 'europe', // Malta
|
||||||
|
md: 'europe', // Moldova
|
||||||
|
mc: 'europe', // Monaco
|
||||||
|
me: 'europe', // Montenegro
|
||||||
|
nl: 'europe', // Netherlands
|
||||||
|
no: 'europe', // Norway
|
||||||
|
pl: 'europe', // Poland
|
||||||
|
pt: 'europe', // Portugal
|
||||||
|
ro: 'europe', // Romania
|
||||||
|
ru: 'europe', // Russia (transcontinental, but primarily European for map purposes)
|
||||||
|
sm: 'europe', // San Marino
|
||||||
|
rs: 'europe', // Serbia
|
||||||
|
sk: 'europe', // Slovakia
|
||||||
|
si: 'europe', // Slovenia
|
||||||
|
es: 'europe', // Spain
|
||||||
|
sj: 'europe', // Svalbard and Jan Mayen
|
||||||
|
se: 'europe', // Sweden
|
||||||
|
ch: 'europe', // Switzerland
|
||||||
|
ua: 'europe', // Ukraine
|
||||||
|
gb: 'europe', // United Kingdom
|
||||||
|
va: 'europe', // Vatican City
|
||||||
|
|
||||||
|
// North America
|
||||||
|
ai: 'north-america', // Anguilla
|
||||||
|
ag: 'north-america', // Antigua and Barbuda
|
||||||
|
aw: 'north-america', // Aruba
|
||||||
|
bs: 'north-america', // Bahamas
|
||||||
|
bb: 'north-america', // Barbados
|
||||||
|
bz: 'north-america', // Belize
|
||||||
|
bm: 'north-america', // Bermuda
|
||||||
|
bq: 'north-america', // Caribbean Netherlands
|
||||||
|
vg: 'north-america', // British Virgin Islands
|
||||||
|
ca: 'north-america', // Canada
|
||||||
|
ky: 'north-america', // Cayman Islands
|
||||||
|
cr: 'north-america', // Costa Rica
|
||||||
|
cu: 'north-america', // Cuba
|
||||||
|
cw: 'north-america', // Curaçao
|
||||||
|
dm: 'north-america', // Dominica
|
||||||
|
do: 'north-america', // Dominican Republic
|
||||||
|
sv: 'north-america', // El Salvador
|
||||||
|
gl: 'north-america', // Greenland
|
||||||
|
gd: 'north-america', // Grenada
|
||||||
|
gp: 'north-america', // Guadeloupe
|
||||||
|
gt: 'north-america', // Guatemala
|
||||||
|
ht: 'north-america', // Haiti
|
||||||
|
hn: 'north-america', // Honduras
|
||||||
|
jm: 'north-america', // Jamaica
|
||||||
|
mq: 'north-america', // Martinique
|
||||||
|
mx: 'north-america', // Mexico
|
||||||
|
ms: 'north-america', // Montserrat
|
||||||
|
ni: 'north-america', // Nicaragua
|
||||||
|
pa: 'north-america', // Panama
|
||||||
|
pm: 'north-america', // Saint Pierre and Miquelon
|
||||||
|
kn: 'north-america', // Saint Kitts and Nevis
|
||||||
|
lc: 'north-america', // Saint Lucia
|
||||||
|
vc: 'north-america', // Saint Vincent and the Grenadines
|
||||||
|
sx: 'north-america', // Sint Maarten
|
||||||
|
tt: 'north-america', // Trinidad and Tobago
|
||||||
|
tc: 'north-america', // Turks and Caicos Islands
|
||||||
|
us: 'north-america', // United States
|
||||||
|
vi: 'north-america', // U.S. Virgin Islands
|
||||||
|
|
||||||
|
// South America
|
||||||
|
ar: 'south-america', // Argentina
|
||||||
|
bo: 'south-america', // Bolivia
|
||||||
|
br: 'south-america', // Brazil
|
||||||
|
cl: 'south-america', // Chile
|
||||||
|
co: 'south-america', // Colombia
|
||||||
|
ec: 'south-america', // Ecuador
|
||||||
|
fk: 'south-america', // Falkland Islands
|
||||||
|
gf: 'south-america', // French Guiana
|
||||||
|
gy: 'south-america', // Guyana
|
||||||
|
py: 'south-america', // Paraguay
|
||||||
|
pe: 'south-america', // Peru
|
||||||
|
sr: 'south-america', // Suriname
|
||||||
|
uy: 'south-america', // Uruguay
|
||||||
|
ve: 'south-america', // Venezuela
|
||||||
|
|
||||||
|
// Oceania
|
||||||
|
as: 'oceania', // American Samoa
|
||||||
|
au: 'oceania', // Australia
|
||||||
|
ck: 'oceania', // Cook Islands
|
||||||
|
fj: 'oceania', // Fiji
|
||||||
|
pf: 'oceania', // French Polynesia
|
||||||
|
gu: 'oceania', // Guam
|
||||||
|
ki: 'oceania', // Kiribati
|
||||||
|
mh: 'oceania', // Marshall Islands
|
||||||
|
fm: 'oceania', // Micronesia
|
||||||
|
nr: 'oceania', // Nauru
|
||||||
|
nc: 'oceania', // New Caledonia
|
||||||
|
nz: 'oceania', // New Zealand
|
||||||
|
nu: 'oceania', // Niue
|
||||||
|
nf: 'oceania', // Norfolk Island
|
||||||
|
mp: 'oceania', // Northern Mariana Islands
|
||||||
|
pw: 'oceania', // Palau
|
||||||
|
pg: 'oceania', // Papua New Guinea
|
||||||
|
pn: 'oceania', // Pitcairn Islands
|
||||||
|
ws: 'oceania', // Samoa
|
||||||
|
sb: 'oceania', // Solomon Islands
|
||||||
|
tk: 'oceania', // Tokelau
|
||||||
|
to: 'oceania', // Tonga
|
||||||
|
tv: 'oceania', // Tuvalu
|
||||||
|
vu: 'oceania', // Vanuatu
|
||||||
|
wf: 'oceania', // Wallis and Futuna
|
||||||
|
|
||||||
|
// Antarctica
|
||||||
|
aq: 'antarctica', // Antarctica
|
||||||
|
bv: 'antarctica', // Bouvet Island
|
||||||
|
tf: 'antarctica', // French Southern Territories
|
||||||
|
hm: 'antarctica', // Heard Island and McDonald Islands
|
||||||
|
gs: 'antarctica', // South Georgia and the South Sandwich Islands
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get continent for a country code
|
||||||
|
*/
|
||||||
|
export function getContinentForCountry(countryCode: string): ContinentId | null {
|
||||||
|
return COUNTRY_TO_CONTINENT[countryCode.toLowerCase()] || null
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all country codes for a continent
|
||||||
|
*/
|
||||||
|
export function getCountriesInContinent(continentId: ContinentId): string[] {
|
||||||
|
return Object.entries(COUNTRY_TO_CONTINENT)
|
||||||
|
.filter(([_, continent]) => continent === continentId)
|
||||||
|
.map(([countryCode]) => countryCode)
|
||||||
|
}
|
||||||
|
|
@ -32,9 +32,21 @@ const defaultConfig: KnowYourWorldConfig = {
|
||||||
gameMode: 'cooperative',
|
gameMode: 'cooperative',
|
||||||
difficulty: 'easy',
|
difficulty: 'easy',
|
||||||
studyDuration: 0,
|
studyDuration: 0,
|
||||||
|
selectedContinent: 'all',
|
||||||
}
|
}
|
||||||
|
|
||||||
function validateKnowYourWorldConfig(config: unknown): config is KnowYourWorldConfig {
|
function validateKnowYourWorldConfig(config: unknown): config is KnowYourWorldConfig {
|
||||||
|
const validContinents = [
|
||||||
|
'all',
|
||||||
|
'africa',
|
||||||
|
'asia',
|
||||||
|
'europe',
|
||||||
|
'north-america',
|
||||||
|
'south-america',
|
||||||
|
'oceania',
|
||||||
|
'antarctica',
|
||||||
|
]
|
||||||
|
|
||||||
return (
|
return (
|
||||||
typeof config === 'object' &&
|
typeof config === 'object' &&
|
||||||
config !== null &&
|
config !== null &&
|
||||||
|
|
@ -42,6 +54,7 @@ function validateKnowYourWorldConfig(config: unknown): config is KnowYourWorldCo
|
||||||
'gameMode' in config &&
|
'gameMode' in config &&
|
||||||
'difficulty' in config &&
|
'difficulty' in config &&
|
||||||
'studyDuration' in config &&
|
'studyDuration' in config &&
|
||||||
|
'selectedContinent' in config &&
|
||||||
(config.selectedMap === 'world' || config.selectedMap === 'usa') &&
|
(config.selectedMap === 'world' || config.selectedMap === 'usa') &&
|
||||||
(config.gameMode === 'cooperative' ||
|
(config.gameMode === 'cooperative' ||
|
||||||
config.gameMode === 'race' ||
|
config.gameMode === 'race' ||
|
||||||
|
|
@ -50,7 +63,9 @@ function validateKnowYourWorldConfig(config: unknown): config is KnowYourWorldCo
|
||||||
(config.studyDuration === 0 ||
|
(config.studyDuration === 0 ||
|
||||||
config.studyDuration === 30 ||
|
config.studyDuration === 30 ||
|
||||||
config.studyDuration === 60 ||
|
config.studyDuration === 60 ||
|
||||||
config.studyDuration === 120)
|
config.studyDuration === 120) &&
|
||||||
|
typeof config.selectedContinent === 'string' &&
|
||||||
|
validContinents.includes(config.selectedContinent)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
// @ts-ignore - ESM/CommonJS compatibility
|
// @ts-expect-error - ESM/CommonJS compatibility
|
||||||
import World from '@svg-maps/world'
|
import World from '@svg-maps/world'
|
||||||
// @ts-ignore - ESM/CommonJS compatibility
|
// @ts-expect-error - ESM/CommonJS compatibility
|
||||||
import USA from '@svg-maps/usa'
|
import USA from '@svg-maps/usa'
|
||||||
import type { MapData, MapRegion } from './types'
|
import type { MapData, MapRegion } from './types'
|
||||||
|
|
||||||
|
|
@ -20,7 +20,11 @@ function calculatePathCenter(pathString: string): [number, number] {
|
||||||
|
|
||||||
while ((match = commandRegex.exec(pathString)) !== null) {
|
while ((match = commandRegex.exec(pathString)) !== null) {
|
||||||
const command = match[1]
|
const command = match[1]
|
||||||
const params = match[2].trim().match(/-?\d+\.?\d*/g)?.map(Number) || []
|
const params =
|
||||||
|
match[2]
|
||||||
|
.trim()
|
||||||
|
.match(/-?\d+\.?\d*/g)
|
||||||
|
?.map(Number) || []
|
||||||
|
|
||||||
switch (command) {
|
switch (command) {
|
||||||
case 'M': // Move to (absolute)
|
case 'M': // Move to (absolute)
|
||||||
|
|
@ -240,3 +244,124 @@ export function getRegionById(mapId: 'world' | 'usa', regionId: string) {
|
||||||
const mapData = getMapData(mapId)
|
const mapData = getMapData(mapId)
|
||||||
return mapData.regions.find((r) => r.id === regionId)
|
return mapData.regions.find((r) => r.id === regionId)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate bounding box for a set of SVG paths
|
||||||
|
*/
|
||||||
|
export interface BoundingBox {
|
||||||
|
minX: number
|
||||||
|
maxX: number
|
||||||
|
minY: number
|
||||||
|
maxY: number
|
||||||
|
width: number
|
||||||
|
height: number
|
||||||
|
}
|
||||||
|
|
||||||
|
function calculateBoundingBox(paths: string[]): BoundingBox {
|
||||||
|
let minX = Infinity
|
||||||
|
let maxX = -Infinity
|
||||||
|
let minY = Infinity
|
||||||
|
let maxY = -Infinity
|
||||||
|
|
||||||
|
for (const path of paths) {
|
||||||
|
// Extract all numbers from path string
|
||||||
|
const numbers = path.match(/-?\d+\.?\d*/g)?.map(Number) || []
|
||||||
|
|
||||||
|
// Assume pairs of x,y coordinates
|
||||||
|
for (let i = 0; i < numbers.length - 1; i += 2) {
|
||||||
|
const x = numbers[i]
|
||||||
|
const y = numbers[i + 1]
|
||||||
|
|
||||||
|
minX = Math.min(minX, x)
|
||||||
|
maxX = Math.max(maxX, x)
|
||||||
|
minY = Math.min(minY, y)
|
||||||
|
maxY = Math.max(maxY, y)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
minX,
|
||||||
|
maxX,
|
||||||
|
minY,
|
||||||
|
maxY,
|
||||||
|
width: maxX - minX,
|
||||||
|
height: maxY - minY,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Filter world map regions by continent
|
||||||
|
*/
|
||||||
|
import { getContinentForCountry, type ContinentId } from './continents'
|
||||||
|
|
||||||
|
export function filterRegionsByContinent(
|
||||||
|
regions: MapRegion[],
|
||||||
|
continentId: ContinentId | 'all'
|
||||||
|
): MapRegion[] {
|
||||||
|
if (continentId === 'all') {
|
||||||
|
return regions
|
||||||
|
}
|
||||||
|
|
||||||
|
return regions.filter((region) => {
|
||||||
|
const continent = getContinentForCountry(region.id)
|
||||||
|
return continent === continentId
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate adjusted viewBox for a continent
|
||||||
|
* Adds padding around the bounding box
|
||||||
|
*/
|
||||||
|
export function calculateContinentViewBox(
|
||||||
|
regions: MapRegion[],
|
||||||
|
continentId: ContinentId | 'all',
|
||||||
|
originalViewBox: string
|
||||||
|
): string {
|
||||||
|
if (continentId === 'all') {
|
||||||
|
return originalViewBox
|
||||||
|
}
|
||||||
|
|
||||||
|
const filteredRegions = filterRegionsByContinent(regions, continentId)
|
||||||
|
|
||||||
|
if (filteredRegions.length === 0) {
|
||||||
|
return originalViewBox
|
||||||
|
}
|
||||||
|
|
||||||
|
const paths = filteredRegions.map((r) => r.path)
|
||||||
|
const bbox = calculateBoundingBox(paths)
|
||||||
|
|
||||||
|
// Add 10% padding on each side
|
||||||
|
const paddingX = bbox.width * 0.1
|
||||||
|
const paddingY = bbox.height * 0.1
|
||||||
|
|
||||||
|
const newMinX = bbox.minX - paddingX
|
||||||
|
const newMinY = bbox.minY - paddingY
|
||||||
|
const newWidth = bbox.width + 2 * paddingX
|
||||||
|
const newHeight = bbox.height + 2 * paddingY
|
||||||
|
|
||||||
|
return `${newMinX} ${newMinY} ${newWidth} ${newHeight}`
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get filtered map data for a continent
|
||||||
|
*/
|
||||||
|
export function getFilteredMapData(
|
||||||
|
mapId: 'world' | 'usa',
|
||||||
|
continentId: ContinentId | 'all'
|
||||||
|
): MapData {
|
||||||
|
const mapData = getMapData(mapId)
|
||||||
|
|
||||||
|
// Continent filtering only applies to world map
|
||||||
|
if (mapId !== 'world' || continentId === 'all') {
|
||||||
|
return mapData
|
||||||
|
}
|
||||||
|
|
||||||
|
const filteredRegions = filterRegionsByContinent(mapData.regions, continentId)
|
||||||
|
const adjustedViewBox = calculateContinentViewBox(mapData.regions, continentId, mapData.viewBox)
|
||||||
|
|
||||||
|
return {
|
||||||
|
...mapData,
|
||||||
|
regions: filteredRegions,
|
||||||
|
viewBox: adjustedViewBox,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import type { GameConfig, GameMove, GameState } from '@/lib/arcade/game-sdk'
|
import type { GameConfig, GameMove, GameState } from '@/lib/arcade/game-sdk'
|
||||||
|
import type { ContinentId } from './continents'
|
||||||
|
|
||||||
// Game configuration (persisted to database)
|
// Game configuration (persisted to database)
|
||||||
export interface KnowYourWorldConfig extends GameConfig {
|
export interface KnowYourWorldConfig extends GameConfig {
|
||||||
|
|
@ -6,6 +7,7 @@ export interface KnowYourWorldConfig extends GameConfig {
|
||||||
gameMode: 'cooperative' | 'race' | 'turn-based'
|
gameMode: 'cooperative' | 'race' | 'turn-based'
|
||||||
difficulty: 'easy' | 'hard'
|
difficulty: 'easy' | 'hard'
|
||||||
studyDuration: 0 | 30 | 60 | 120 // seconds (0 = skip study mode)
|
studyDuration: 0 | 30 | 60 | 120 // seconds (0 = skip study mode)
|
||||||
|
selectedContinent: ContinentId | 'all' // continent filter for world map ('all' = no filter)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Map data structures
|
// Map data structures
|
||||||
|
|
@ -42,6 +44,7 @@ export interface KnowYourWorldState extends GameState {
|
||||||
gameMode: 'cooperative' | 'race' | 'turn-based'
|
gameMode: 'cooperative' | 'race' | 'turn-based'
|
||||||
difficulty: 'easy' | 'hard'
|
difficulty: 'easy' | 'hard'
|
||||||
studyDuration: 0 | 30 | 60 | 120 // seconds (0 = skip study mode)
|
studyDuration: 0 | 30 | 60 | 120 // seconds (0 = skip study mode)
|
||||||
|
selectedContinent: ContinentId | 'all' // continent filter for world map ('all' = no filter)
|
||||||
|
|
||||||
// Study phase
|
// Study phase
|
||||||
studyTimeRemaining: number // seconds remaining in study phase
|
studyTimeRemaining: number // seconds remaining in study phase
|
||||||
|
|
@ -156,3 +159,12 @@ export type KnowYourWorldMove =
|
||||||
timestamp: number
|
timestamp: number
|
||||||
data: {}
|
data: {}
|
||||||
}
|
}
|
||||||
|
| {
|
||||||
|
type: 'SET_CONTINENT'
|
||||||
|
playerId: string
|
||||||
|
userId: string
|
||||||
|
timestamp: number
|
||||||
|
data: {
|
||||||
|
selectedContinent: ContinentId | 'all'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue