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>
182 lines
5.6 KiB
TypeScript
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 })
|
|
}
|
|
}
|