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>
120 lines
4.1 KiB
TypeScript
120 lines
4.1 KiB
TypeScript
import type { EntityManager } from '@/game/core/EntityManager'
|
|
import type { GridMap } from '@/game/map/GridMap'
|
|
import type { HealthComp, MovementComp } from '@/game/components'
|
|
import { createGoblin } from '@/game/entities/enemies/goblin'
|
|
import { createOrc } from '@/game/entities/enemies/orc'
|
|
import { createWarg } from '@/game/entities/enemies/warg'
|
|
import { createWraith } from '@/game/entities/enemies/wraith'
|
|
import { createTroll } from '@/game/entities/enemies/troll'
|
|
import { createGolem } from '@/game/entities/enemies/golem'
|
|
import { createNecromancer } from '@/game/entities/enemies/necromancer'
|
|
import { createDragon } from '@/game/entities/enemies/dragon'
|
|
import { createLichKing } from '@/game/entities/enemies/lich_king'
|
|
import { eventBus } from '@/game/core/EventBus'
|
|
import { getWaveDef, type EnemyType } from '@/data/waves'
|
|
import { useSettingsStore } from '@/state/settingsStore'
|
|
import { useGameStore } from '@/state/gameStore'
|
|
|
|
export interface WaveState {
|
|
active: boolean
|
|
waveIndex: number
|
|
spawnQueue: EnemyType[]
|
|
spawnTimer: number
|
|
spawnInterval: number
|
|
aliveCount: number
|
|
countdown: number
|
|
}
|
|
|
|
const BETWEEN_WAVE_TIME = 10
|
|
|
|
export function createWaveState(): WaveState {
|
|
return {
|
|
active: false,
|
|
waveIndex: 0,
|
|
spawnQueue: [],
|
|
spawnTimer: 0,
|
|
spawnInterval: 1.2,
|
|
aliveCount: 0,
|
|
countdown: 0,
|
|
}
|
|
}
|
|
|
|
export function startWave(state: WaveState, waveIndex: number): void {
|
|
if (state.active) return
|
|
const levelId = useGameStore.getState().currentLevelId
|
|
const def = getWaveDef(waveIndex, levelId)
|
|
state.active = true
|
|
state.waveIndex = waveIndex
|
|
state.spawnQueue = [...def.enemies]
|
|
state.spawnTimer = 0
|
|
state.spawnInterval = def.interval
|
|
state.aliveCount = state.spawnQueue.length
|
|
state.countdown = 0
|
|
eventBus.emit('wave:start', { waveIndex })
|
|
}
|
|
|
|
export function waveSystem(
|
|
state: WaveState,
|
|
entities: EntityManager,
|
|
_map: GridMap,
|
|
path: { x: number; y: number }[],
|
|
dt: number,
|
|
): void {
|
|
if (!state.active) return
|
|
|
|
const alive = entities.withTag('enemy').filter((e) => !e.tags.has('dead'))
|
|
state.aliveCount = alive.length + state.spawnQueue.length
|
|
|
|
if (state.spawnQueue.length > 0) {
|
|
state.spawnTimer -= dt
|
|
if (state.spawnTimer <= 0) {
|
|
const type = state.spawnQueue.shift()!
|
|
const entity = entities.create()
|
|
const spawn = path[0]
|
|
const sx = spawn.x + (Math.random() - 0.5) * 8
|
|
const sy = spawn.y + (Math.random() - 0.5) * 8
|
|
spawnEnemy(type, entity, path, sx, sy)
|
|
applyDifficulty(entity)
|
|
state.spawnTimer = state.spawnInterval
|
|
}
|
|
}
|
|
|
|
if (state.spawnQueue.length === 0 && alive.length === 0) {
|
|
state.active = false
|
|
state.countdown = BETWEEN_WAVE_TIME
|
|
eventBus.emit('wave:end', { waveIndex: state.waveIndex })
|
|
}
|
|
}
|
|
|
|
function applyDifficulty(entity: ReturnType<EntityManager['create']>): void {
|
|
const diff = useSettingsStore.getState().difficulty
|
|
if (diff === 'normal') return
|
|
const hpMult = diff === 'heroic' ? 1.35 : 1.75
|
|
const speedMult = diff === 'heroic' ? 1.1 : 1.25
|
|
const hp = entity.health as HealthComp | undefined
|
|
if (hp) { hp.current = Math.round(hp.current * hpMult); hp.max = Math.round(hp.max * hpMult) }
|
|
const mv = entity.movement as MovementComp | undefined
|
|
if (mv) mv.speed = Math.round(mv.speed * speedMult)
|
|
}
|
|
|
|
function spawnEnemy(
|
|
type: EnemyType,
|
|
entity: ReturnType<EntityManager['create']>,
|
|
path: { x: number; y: number }[],
|
|
sx: number,
|
|
sy: number,
|
|
): void {
|
|
const flyPath = [path[0], path[path.length - 1]]
|
|
switch (type) {
|
|
case 'orc': createOrc(entity, path, sx, sy); break
|
|
case 'warg': createWarg(entity, path, sx, sy); break
|
|
case 'wraith': createWraith(entity, path, sx, sy); break
|
|
case 'troll': createTroll(entity, path, sx, sy); break
|
|
case 'golem': createGolem(entity, path, sx, sy); break
|
|
case 'necromancer': createNecromancer(entity, path, sx, sy); break
|
|
case 'dragon': createDragon(entity, flyPath, sx, sy - 30); break
|
|
case 'lich_king': createLichKing(entity, path, sx, sy); break
|
|
default: createGoblin(entity, path, sx, sy); break
|
|
}
|
|
}
|