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>
This commit is contained in:
@@ -0,0 +1,119 @@
|
||||
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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user