7a62067af1
Vite + React + PixiJS + TypeScript. Features: - 4-level campaign (King's Road → Obsidian Keep) - Isometric 2.5D grid with ley-line mechanics - ECS architecture (entities, components, systems) - 4 tower types, hero spellcaster, 10+ enemy types - Lich King boss with 3-phase AI - Meta-progression: essence, rune unlocks - Full UI redesign with fantasy design system Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
219 lines
8.1 KiB
TypeScript
219 lines
8.1 KiB
TypeScript
import { useGameStore } from '@/state/gameStore'
|
|
import { useMetaStore } from '@/state/metaStore'
|
|
|
|
interface Level {
|
|
id: string
|
|
name: string
|
|
nameRu: string
|
|
waves: number
|
|
difficulty: 'Новобранец' | 'Рыцарь' | 'Архимаг' | 'Босс'
|
|
unlockedBy: string | null
|
|
x: string
|
|
y: string
|
|
biomeColor: string
|
|
biomeGlow: string
|
|
icon: string
|
|
}
|
|
|
|
const LEVELS: Level[] = [
|
|
{
|
|
id: 'kings_road',
|
|
name: "King's Road",
|
|
nameRu: 'Королевский Тракт',
|
|
waves: 20,
|
|
difficulty: 'Новобранец',
|
|
unlockedBy: null,
|
|
x: '18%', y: '62%',
|
|
biomeColor: 'border-emerald-500/60',
|
|
biomeGlow: 'rgba(74,222,128,0.2)',
|
|
icon: '⚔',
|
|
},
|
|
{
|
|
id: 'whispering_woods',
|
|
name: 'Whispering Woods',
|
|
nameRu: 'Шепчущий Лес',
|
|
waves: 20,
|
|
difficulty: 'Рыцарь',
|
|
unlockedBy: 'kings_road',
|
|
x: '46%', y: '36%',
|
|
biomeColor: 'border-gold/60',
|
|
biomeGlow: 'rgba(201,161,74,0.2)',
|
|
icon: '🌲',
|
|
},
|
|
{
|
|
id: 'frostfall_pass',
|
|
name: 'Frostfall Pass',
|
|
nameRu: 'Морозный Перевал',
|
|
waves: 20,
|
|
difficulty: 'Архимаг',
|
|
unlockedBy: 'whispering_woods',
|
|
x: '70%', y: '22%',
|
|
biomeColor: 'border-frost/60',
|
|
biomeGlow: 'rgba(110,203,213,0.2)',
|
|
icon: '❄',
|
|
},
|
|
{
|
|
id: 'obsidian_keep',
|
|
name: 'Obsidian Keep',
|
|
nameRu: 'Обсидиановая Цитадель',
|
|
waves: 10,
|
|
difficulty: 'Босс',
|
|
unlockedBy: 'frostfall_pass',
|
|
x: '84%', y: '56%',
|
|
biomeColor: 'border-arcane/60',
|
|
biomeGlow: 'rgba(155,77,232,0.22)',
|
|
icon: '💀',
|
|
},
|
|
]
|
|
|
|
const difficultyStyle: Record<string, string> = {
|
|
'Новобранец': 'text-emerald-400',
|
|
'Рыцарь': 'text-gold',
|
|
'Архимаг': 'text-rose-400',
|
|
'Босс': 'text-arcane',
|
|
}
|
|
|
|
export function CampaignMap() {
|
|
const setScreen = useGameStore((s) => s.setScreen)
|
|
const resetGame = useGameStore((s) => s.resetGame)
|
|
const setCurrentLevelId = useGameStore((s) => s.setCurrentLevelId)
|
|
const completedLevels = useMetaStore((s) => s.completedLevels)
|
|
|
|
const isUnlocked = (level: Level) =>
|
|
level.unlockedBy === null || completedLevels.includes(level.unlockedBy)
|
|
|
|
const handlePlay = (levelId: string) => {
|
|
setCurrentLevelId(levelId)
|
|
resetGame()
|
|
setScreen('game')
|
|
}
|
|
|
|
return (
|
|
<div className="w-full h-full flex flex-col bg-midnight-deep overflow-hidden">
|
|
|
|
{/* Header */}
|
|
<div className="flex items-center justify-between px-8 py-4 border-b border-gold/15 bg-midnight/60 backdrop-blur-sm shrink-0">
|
|
<button
|
|
onClick={() => setScreen('menu')}
|
|
className="btn-ghost text-xs py-2 px-4"
|
|
>
|
|
← Назад
|
|
</button>
|
|
<div className="flex flex-col items-center">
|
|
<h2 className="text-title text-xl tracking-[0.3em]">Карта Кампании</h2>
|
|
<p className="font-garamond text-parchment/35 text-xs mt-0.5 tracking-wider">
|
|
{completedLevels.length} / {LEVELS.length} уровней завершено
|
|
</p>
|
|
</div>
|
|
<div className="w-24" />
|
|
</div>
|
|
|
|
{/* Map area */}
|
|
<div className="relative flex-1 w-full overflow-hidden">
|
|
|
|
{/* Background atmosphere */}
|
|
<div className="absolute inset-0"
|
|
style={{ background: 'radial-gradient(ellipse 70% 60% at 50% 40%, #111828 0%, #070b14 100%)' }} />
|
|
<div className="absolute inset-0 opacity-30"
|
|
style={{ backgroundImage: 'radial-gradient(circle at 20% 65%, rgba(74,222,128,0.08) 0%, transparent 35%), radial-gradient(circle at 47% 38%, rgba(201,161,74,0.07) 0%, transparent 30%), radial-gradient(circle at 70% 22%, rgba(110,203,213,0.07) 0%, transparent 25%), radial-gradient(circle at 84% 55%, rgba(155,77,232,0.1) 0%, transparent 30%)' }} />
|
|
|
|
{/* SVG connection lines */}
|
|
<svg className="absolute inset-0 w-full h-full pointer-events-none" aria-hidden>
|
|
<defs>
|
|
<filter id="glow-line">
|
|
<feGaussianBlur stdDeviation="2" result="blur" />
|
|
<feComposite in="SourceGraphic" in2="blur" operator="over" />
|
|
</filter>
|
|
</defs>
|
|
<line x1="18%" y1="62%" x2="46%" y2="36%" stroke="rgba(201,161,74,0.18)" strokeWidth="2" strokeDasharray="8 5" />
|
|
<line x1="46%" y1="36%" x2="70%" y2="22%" stroke="rgba(201,161,74,0.18)" strokeWidth="2" strokeDasharray="8 5" />
|
|
<line x1="70%" y1="22%" x2="84%" y2="56%" stroke="rgba(155,77,232,0.15)" strokeWidth="2" strokeDasharray="6 5" />
|
|
</svg>
|
|
|
|
{/* Level nodes */}
|
|
{LEVELS.map((level) => {
|
|
const completed = completedLevels.includes(level.id)
|
|
const unlocked = isUnlocked(level)
|
|
|
|
return (
|
|
<div
|
|
key={level.id}
|
|
className="absolute -translate-x-1/2 -translate-y-1/2"
|
|
style={{ left: level.x, top: level.y }}
|
|
>
|
|
<div className="flex flex-col items-center gap-2.5">
|
|
|
|
{/* Node button */}
|
|
<button
|
|
onClick={unlocked ? () => handlePlay(level.id) : undefined}
|
|
disabled={!unlocked}
|
|
className={`
|
|
relative w-16 h-16 rounded-full border-2 flex items-center justify-center
|
|
transition-all duration-300 select-none
|
|
${!unlocked
|
|
? 'border-parchment/15 bg-midnight/60 cursor-not-allowed'
|
|
: completed
|
|
? `${level.biomeColor} bg-midnight hover:scale-110`
|
|
: `${level.biomeColor} bg-midnight hover:scale-110`
|
|
}
|
|
`}
|
|
style={unlocked ? { boxShadow: `0 0 18px ${level.biomeGlow}, 0 0 40px ${level.biomeGlow}` } : undefined}
|
|
>
|
|
<span className="text-2xl">
|
|
{!unlocked ? '🔒' : completed ? '✦' : level.icon}
|
|
</span>
|
|
{unlocked && !completed && (
|
|
<span className="absolute -top-1 -right-1 w-3 h-3 bg-ember rounded-full animate-pulse" />
|
|
)}
|
|
{completed && (
|
|
<span
|
|
className="absolute inset-0 rounded-full"
|
|
style={{ boxShadow: `inset 0 0 16px ${level.biomeGlow}` }}
|
|
/>
|
|
)}
|
|
</button>
|
|
|
|
{/* Label card */}
|
|
<div
|
|
className={`panel-parchment px-3 py-2 text-center min-w-[148px] transition-all duration-300 ${
|
|
unlocked ? 'opacity-100' : 'opacity-45'
|
|
}`}
|
|
>
|
|
<p className="font-cinzel text-[11px] text-gold font-bold tracking-wide leading-none">
|
|
{level.name}
|
|
</p>
|
|
<p className="font-garamond text-[11px] text-parchment/60 mt-0.5 leading-none">
|
|
{level.nameRu}
|
|
</p>
|
|
<div className="rune-divider my-1.5" />
|
|
<div className="flex items-center justify-center gap-2">
|
|
<span className={`font-cinzel text-[10px] font-bold ${difficultyStyle[level.difficulty]}`}>
|
|
{level.difficulty}
|
|
</span>
|
|
<span className="text-parchment/25 text-[10px]">·</span>
|
|
<span className="font-garamond text-[11px] text-parchment/45">
|
|
{level.waves} волн
|
|
</span>
|
|
</div>
|
|
{completed && (
|
|
<p className="font-cinzel text-[9px] text-gold/60 mt-1 tracking-widest uppercase">
|
|
✦ Пройдено
|
|
</p>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)
|
|
})}
|
|
|
|
{/* Bottom lore */}
|
|
<div className="absolute bottom-4 left-1/2 -translate-x-1/2">
|
|
<p className="font-garamond text-parchment/20 text-xs italic tracking-wider text-center">
|
|
«Тьма не знает покоя — каждый рубеж должен устоять»
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|