Files
Arcanum_TD/src/game/rendering/MapRenderer.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

182 lines
5.6 KiB
TypeScript

import { Container, Graphics, Text, TextStyle } from 'pixi.js'
import { GridMap, ISO_HALF_W, ISO_HALF_H, type TileType } from '@/game/map/GridMap'
import { getLeyLineSegments } from '@/game/map/LeyLines'
const TILE_TOP: Record<TileType, number> = {
buildable: 0x2d5a1e,
path: 0x6b4226,
blocked: 0x1a1a28,
ley_line: 0x1a3a5c,
nexus: 0x2a1060,
spawn: 0x601010,
}
const TILE_SIDE_L: Record<TileType, number> = {
buildable: 0x1e3e14,
path: 0x4a2e18,
blocked: 0x111120,
ley_line: 0x102840,
nexus: 0x1a0840,
spawn: 0x400808,
}
const TILE_SIDE_R: Record<TileType, number> = {
buildable: 0x163010,
path: 0x3a2010,
blocked: 0x0c0c18,
ley_line: 0x0a1e30,
nexus: 0x140630,
spawn: 0x300606,
}
const TILE_EDGE: Record<TileType, number> = {
buildable: 0x3a7a28,
path: 0x8a5a30,
blocked: 0x2a2a38,
ley_line: 0x2a5a8a,
nexus: 0x3020a0,
spawn: 0x801818,
}
const SIDE_DEPTH = 8 // px depth of 3D side faces
export class MapRenderer {
readonly container = new Container()
private leyGfx = new Graphics()
private debugContainer = new Container()
private map: GridMap | null = null
private _debugPath: { x: number; y: number }[] = []
constructor() {
this.container.addChild(this.leyGfx)
this.container.addChild(this.debugContainer)
}
load(map: GridMap): void {
this.map = map
this._drawTiles()
this._drawLeyLines()
}
showPath(path: { x: number; y: number }[]): void {
this._debugPath = path
this._drawDebug()
}
private _drawTiles(): void {
if (!this.map) return
const hw = ISO_HALF_W, hh = ISO_HALF_H, sd = SIDE_DEPTH
const sideLGfx = new Graphics()
const sideRGfx = new Graphics()
const topGfx = new Graphics()
const decoGfx = new Graphics()
// Draw in back-to-front order: sort tiles by (tx + ty) ascending
const sorted = [...this.map.allTiles()].sort((a, b) => (a.x + a.y) - (b.x + b.y))
for (const tile of sorted) {
const { x: cx, y: cy } = this.map.tileCenter(tile.x, tile.y)
// Left side face (south-west)
sideLGfx.poly([cx - hw, cy, cx, cy + hh, cx, cy + hh + sd, cx - hw, cy + sd])
sideLGfx.fill({ color: TILE_SIDE_L[tile.type] })
// Right side face (south-east)
sideRGfx.poly([cx, cy + hh, cx + hw, cy, cx + hw, cy + sd, cx, cy + hh + sd])
sideRGfx.fill({ color: TILE_SIDE_R[tile.type] })
// Top diamond face
topGfx.poly([cx, cy - hh, cx + hw, cy, cx, cy + hh, cx - hw, cy])
topGfx.fill({ color: TILE_TOP[tile.type] })
topGfx.poly([cx, cy - hh, cx + hw, cy, cx, cy + hh, cx - hw, cy])
topGfx.stroke({ color: TILE_EDGE[tile.type], width: 1, alpha: 0.5 })
// Decorations
if (tile.type === 'spawn') {
decoGfx.circle(cx, cy, 10)
decoGfx.fill({ color: 0xff4444, alpha: 0.7 })
decoGfx.circle(cx, cy, 10)
decoGfx.stroke({ color: 0xff8888, width: 1.5 })
}
if (tile.type === 'nexus') {
decoGfx.circle(cx, cy, 14)
decoGfx.fill({ color: 0x8844ff, alpha: 0.8 })
decoGfx.circle(cx, cy, 14)
decoGfx.stroke({ color: 0xbb88ff, width: 2 })
// Nexus gem
decoGfx.poly([cx, cy - 10, cx + 8, cy, cx, cy + 10, cx - 8, cy])
decoGfx.fill({ color: 0xcc88ff, alpha: 0.9 })
}
}
this.container.addChildAt(sideLGfx, 0)
this.container.addChildAt(sideRGfx, 1)
this.container.addChildAt(topGfx, 2)
this.container.addChildAt(decoGfx, 3)
}
private _drawLeyLines(): void {
if (!this.map) return
this.leyGfx.clear()
const segments = getLeyLineSegments(this.map)
for (const { x1, y1, x2, y2 } of segments) {
const { x: sx1, y: sy1 } = this.map.tileCenter(x1, y1)
const { x: sx2, y: sy2 } = this.map.tileCenter(x2, y2)
this.leyGfx.moveTo(sx1, sy1)
this.leyGfx.lineTo(sx2, sy2)
this.leyGfx.stroke({ color: 0x6ecbd5, width: 8, alpha: 0.12 })
this.leyGfx.moveTo(sx1, sy1)
this.leyGfx.lineTo(sx2, sy2)
this.leyGfx.stroke({ color: 0x6ecbd5, width: 3, alpha: 0.6 })
}
for (const tile of this.map.allTiles()) {
if (tile.type !== 'ley_line') continue
const { x: cx, y: cy } = this.map.tileCenter(tile.x, tile.y)
this.leyGfx.circle(cx, cy, 5)
this.leyGfx.fill({ color: 0x6ecbd5, alpha: 0.9 })
}
}
private _drawDebug(): void {
this.debugContainer.removeChildren()
if (this._debugPath.length === 0 || !this.map) return
const gfx = new Graphics()
const pts = this._debugPath
for (let i = 1; i < pts.length; i++) {
const { x: x1, y: y1 } = this.map.tileCenter(pts[i - 1].x, pts[i - 1].y)
const { x: x2, y: y2 } = this.map.tileCenter(pts[i].x, pts[i].y)
gfx.moveTo(x1, y1)
gfx.lineTo(x2, y2)
gfx.stroke({ color: 0xffdd44, width: 2, alpha: 0.7 })
}
for (let i = 0; i < pts.length; i++) {
const { x, y } = this.map.tileCenter(pts[i].x, pts[i].y)
const isEnd = i === 0 || i === pts.length - 1
gfx.circle(x, y, isEnd ? 8 : 4)
gfx.fill({
color: i === 0 ? 0xff4444 : i === pts.length - 1 ? 0x44ff88 : 0xffdd44,
alpha: 0.9,
})
}
for (let i = 0; i < pts.length; i += 4) {
const { x, y } = this.map.tileCenter(pts[i].x, pts[i].y)
const label = new Text({
text: String(i),
style: new TextStyle({ fill: 0xffffff, fontSize: 9, fontFamily: 'monospace' }),
})
label.x = x + 6
label.y = y - 6
this.debugContainer.addChild(label)
}
this.debugContainer.addChild(gfx)
}
destroy(): void {
this.container.destroy({ children: true })
}
}