Files
Arcanum_TD/src/game/systems/WaveSystem.ts
T
Mareli 7a62067af1 Initial commit: Arcanum TD — medieval fantasy tower defense
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>
2026-04-19 12:31:49 +03:00

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
}
}