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,114 @@
|
||||
import { Application, Text, TextStyle } from 'pixi.js'
|
||||
import { Time } from './Time'
|
||||
import { EntityManager } from './EntityManager'
|
||||
import { eventBus } from './EventBus'
|
||||
import { getApp, layers } from '@/game/rendering/PixiRoot'
|
||||
import { LevelScene } from '@/game/LevelScene'
|
||||
import type { LevelDef } from '@/data/levels'
|
||||
|
||||
export class GameEngine {
|
||||
private readonly _app: Application
|
||||
readonly entities = new EntityManager()
|
||||
private _running = false
|
||||
private _lastTime = 0
|
||||
private _frameCount = 0
|
||||
private _fpsTime = 0
|
||||
private _fpsText: Text | null = null
|
||||
private _showFps = false
|
||||
private _scene: LevelScene | null = null
|
||||
private _canvas: HTMLElement | null = null
|
||||
|
||||
constructor(app: Application) {
|
||||
this._app = app
|
||||
}
|
||||
|
||||
loadLevel(def: LevelDef, canvas: HTMLElement): void {
|
||||
this._scene?.destroy()
|
||||
this._canvas = canvas
|
||||
this._scene = new LevelScene(def)
|
||||
this._scene.attachInput(canvas)
|
||||
}
|
||||
|
||||
getScene(): LevelScene | null {
|
||||
return this._scene
|
||||
}
|
||||
|
||||
start(): void {
|
||||
this._running = true
|
||||
this._lastTime = performance.now()
|
||||
this._fpsTime = performance.now()
|
||||
this._app.ticker.add(this._tick, this)
|
||||
this._setupFpsCounter()
|
||||
}
|
||||
|
||||
stop(): void {
|
||||
this._running = false
|
||||
this._app.ticker.remove(this._tick, this)
|
||||
}
|
||||
|
||||
setShowFps(show: boolean): void {
|
||||
this._showFps = show
|
||||
if (this._fpsText) this._fpsText.visible = show
|
||||
}
|
||||
|
||||
private _tick(): void {
|
||||
if (!this._running) return
|
||||
|
||||
const now = performance.now()
|
||||
Time.delta = Math.min((now - this._lastTime) / 1000, 0.05)
|
||||
Time.elapsed += Time.delta
|
||||
this._lastTime = now
|
||||
|
||||
this._scene?.update(Time.delta)
|
||||
this._updateFps(now)
|
||||
}
|
||||
|
||||
private _setupFpsCounter(): void {
|
||||
const style = new TextStyle({
|
||||
fill: 'rgba(201, 161, 74, 0.7)',
|
||||
fontSize: 12,
|
||||
fontFamily: 'monospace',
|
||||
})
|
||||
this._fpsText = new Text({ text: 'FPS: --', style })
|
||||
this._fpsText.visible = this._showFps
|
||||
this._fpsText.x = 8
|
||||
this._fpsText.y = 8
|
||||
this._fpsText.zIndex = 9999
|
||||
layers?.ui.addChild(this._fpsText)
|
||||
}
|
||||
|
||||
private _updateFps(now: number): void {
|
||||
this._frameCount++
|
||||
if (now - this._fpsTime >= 500) {
|
||||
const fps = Math.round((this._frameCount * 1000) / (now - this._fpsTime))
|
||||
if (this._fpsText) this._fpsText.text = `FPS: ${fps}`
|
||||
this._frameCount = 0
|
||||
this._fpsTime = now
|
||||
}
|
||||
}
|
||||
|
||||
destroy(): void {
|
||||
if (this._canvas && this._scene) {
|
||||
this._scene.detachInput(this._canvas)
|
||||
}
|
||||
this._scene?.destroy()
|
||||
this._scene = null
|
||||
this.stop()
|
||||
this.entities.clear()
|
||||
eventBus.clear()
|
||||
this._fpsText?.destroy()
|
||||
this._fpsText = null
|
||||
}
|
||||
}
|
||||
|
||||
let _engine: GameEngine | null = null
|
||||
|
||||
export function createEngine(): GameEngine {
|
||||
_engine?.destroy()
|
||||
_engine = new GameEngine(getApp())
|
||||
return _engine
|
||||
}
|
||||
|
||||
export function getEngine(): GameEngine | null {
|
||||
return _engine
|
||||
}
|
||||
Reference in New Issue
Block a user