366 lines
14 KiB
TypeScript
366 lines
14 KiB
TypeScript
|
|
import Phaser from 'phaser'
|
|||
|
|
import { TILE_SIZE, WORLD_TILES } from '../config'
|
|||
|
|
import { TileType } from '../types'
|
|||
|
|
import { generateTerrain, findSpawn } from '../utils/noise'
|
|||
|
|
import { stateManager } from '../StateManager'
|
|||
|
|
|
|||
|
|
const TOTAL_TILES = 11 // 0-10 in the tileset strip
|
|||
|
|
|
|||
|
|
export class BootScene extends Phaser.Scene {
|
|||
|
|
constructor() {
|
|||
|
|
super({ key: 'Boot' })
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
preload(): void {
|
|||
|
|
this.createLoadingBar()
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
create(): void {
|
|||
|
|
this.buildTileset()
|
|||
|
|
this.buildResourceTextures()
|
|||
|
|
this.buildPlayerTexture()
|
|||
|
|
this.buildCropTextures()
|
|||
|
|
this.buildUITextures()
|
|||
|
|
this.buildVillagerAndBuildingTextures()
|
|||
|
|
this.generateWorldIfNeeded()
|
|||
|
|
this.scene.start('Game')
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ─── Loading bar ──────────────────────────────────────────────────────────
|
|||
|
|
|
|||
|
|
private createLoadingBar(): void {
|
|||
|
|
const { width, height } = this.scale
|
|||
|
|
const barW = 400, barH = 20
|
|||
|
|
const x = width / 2 - barW / 2
|
|||
|
|
const y = height / 2
|
|||
|
|
|
|||
|
|
const border = this.add.graphics()
|
|||
|
|
border.lineStyle(2, 0xffffff)
|
|||
|
|
border.strokeRect(x - 2, y - 2, barW + 4, barH + 4)
|
|||
|
|
|
|||
|
|
const bar = this.add.graphics()
|
|||
|
|
this.load.on('progress', (v: number) => {
|
|||
|
|
bar.clear()
|
|||
|
|
bar.fillStyle(0x4CAF50)
|
|||
|
|
bar.fillRect(x, y, barW * v, barH)
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
this.add.text(width / 2, y - 40, 'Loading...', {
|
|||
|
|
fontSize: '20px', color: '#ffffff', fontFamily: 'monospace'
|
|||
|
|
}).setOrigin(0.5)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ─── Tileset (TOTAL_TILES × TILE_SIZE wide, TILE_SIZE tall) ──────────────
|
|||
|
|
|
|||
|
|
private buildTileset(): void {
|
|||
|
|
const T = TILE_SIZE
|
|||
|
|
const g = this.add.graphics()
|
|||
|
|
|
|||
|
|
const drawTile = (idx: number, cb: (g: Phaser.GameObjects.Graphics) => void) => {
|
|||
|
|
g.save(); g.translateCanvas(idx * T, 0); cb(g); g.restore()
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 0 – Deep water
|
|||
|
|
drawTile(TileType.DEEP_WATER, g => {
|
|||
|
|
g.fillStyle(0x1565C0); g.fillRect(0, 0, T, T)
|
|||
|
|
g.fillStyle(0x1976D2, 0.6)
|
|||
|
|
for (let i = 0; i < 3; i++) g.fillRect(4 + i * 10, 8 + i * 8, 14, 3)
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
// 1 – Shallow water
|
|||
|
|
drawTile(TileType.SHALLOW_WATER, g => {
|
|||
|
|
g.fillStyle(0x42A5F5); g.fillRect(0, 0, T, T)
|
|||
|
|
g.fillStyle(0x64B5F6, 0.7)
|
|||
|
|
g.fillRect(5, 12, 22, 3); g.fillRect(8, 20, 16, 3)
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
// 2 – Sand
|
|||
|
|
drawTile(TileType.SAND, g => {
|
|||
|
|
g.fillStyle(0xF5DEB3); g.fillRect(0, 0, T, T)
|
|||
|
|
g.fillStyle(0xDEB887, 0.5)
|
|||
|
|
g.fillRect(4, 4, 6, 6); g.fillRect(18, 14, 8, 8); g.fillRect(10, 22, 5, 5)
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
// 3 – Grass
|
|||
|
|
drawTile(TileType.GRASS, g => {
|
|||
|
|
g.fillStyle(0x66BB6A); g.fillRect(0, 0, T, T)
|
|||
|
|
g.fillStyle(0x4CAF50, 0.6)
|
|||
|
|
g.fillRect(3, 8, 4, 6); g.fillRect(14, 4, 4, 8); g.fillRect(24, 16, 3, 7)
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
// 4 – Dark grass
|
|||
|
|
drawTile(TileType.DARK_GRASS, g => {
|
|||
|
|
g.fillStyle(0x43A047); g.fillRect(0, 0, T, T)
|
|||
|
|
g.fillStyle(0x388E3C, 0.6)
|
|||
|
|
g.fillRect(2, 6, 5, 8); g.fillRect(16, 3, 5, 10); g.fillRect(22, 18, 4, 8)
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
// 5 – Forest floor (under trees)
|
|||
|
|
drawTile(TileType.FOREST, g => {
|
|||
|
|
g.fillStyle(0x33691E); g.fillRect(0, 0, T, T)
|
|||
|
|
g.fillStyle(0x2E7D32, 0.5); g.fillRect(0, 0, T, T)
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
// 6 – Rock ground
|
|||
|
|
drawTile(TileType.ROCK, g => {
|
|||
|
|
g.fillStyle(0x616161); g.fillRect(0, 0, T, T)
|
|||
|
|
g.fillStyle(0x757575, 0.6)
|
|||
|
|
g.fillRect(4, 4, 10, 10); g.fillRect(18, 16, 8, 8)
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
// 7 – Built floor (wood plank)
|
|||
|
|
drawTile(TileType.FLOOR, g => {
|
|||
|
|
g.fillStyle(0xD2A679); g.fillRect(0, 0, T, T)
|
|||
|
|
g.lineStyle(1, 0xB8895A)
|
|||
|
|
g.strokeRect(1, 1, T - 2, T - 2)
|
|||
|
|
g.strokeRect(1, T / 2, T - 2, 1)
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
// 8 – Built wall
|
|||
|
|
drawTile(TileType.WALL, g => {
|
|||
|
|
g.fillStyle(0x9E9E9E); g.fillRect(0, 0, T, T)
|
|||
|
|
g.fillStyle(0x757575)
|
|||
|
|
for (let row = 0; row < 4; row++) {
|
|||
|
|
for (let col = 0; col < 2; col++) {
|
|||
|
|
const ox = col * 16 + (row % 2 === 0 ? 0 : 8)
|
|||
|
|
g.fillRect(ox + 1, row * 8 + 1, 14, 6)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
// 9 – Tilled soil
|
|||
|
|
drawTile(TileType.TILLED_SOIL, g => {
|
|||
|
|
g.fillStyle(0x5D3A1A); g.fillRect(0, 0, T, T)
|
|||
|
|
g.fillStyle(0x4A2E0E)
|
|||
|
|
for (let row = 0; row < 4; row++) {
|
|||
|
|
g.fillRect(2, 4 + row * 8, T - 4, 2)
|
|||
|
|
}
|
|||
|
|
g.fillStyle(0x7B4F2A, 0.5)
|
|||
|
|
g.fillRect(3, 5, T - 6, 1)
|
|||
|
|
g.fillRect(3, 13, T - 6, 1)
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
// 10 – Watered soil
|
|||
|
|
drawTile(TileType.WATERED_SOIL, g => {
|
|||
|
|
g.fillStyle(0x3E2208); g.fillRect(0, 0, T, T)
|
|||
|
|
g.fillStyle(0x2C5F8A, 0.25); g.fillRect(0, 0, T, T)
|
|||
|
|
g.fillStyle(0x2A1505)
|
|||
|
|
for (let row = 0; row < 4; row++) {
|
|||
|
|
g.fillRect(2, 4 + row * 8, T - 4, 2)
|
|||
|
|
}
|
|||
|
|
g.fillStyle(0x4A90D9, 0.3)
|
|||
|
|
g.fillRect(3, 5, T - 6, 1)
|
|||
|
|
g.fillRect(3, 13, T - 6, 1)
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
g.generateTexture('tiles', TOTAL_TILES * T, T)
|
|||
|
|
g.destroy()
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ─── Tree and rock textures ───────────────────────────────────────────────
|
|||
|
|
|
|||
|
|
private buildResourceTextures(): void {
|
|||
|
|
// Tree (32 × 52)
|
|||
|
|
const tg = this.add.graphics()
|
|||
|
|
tg.fillStyle(0x000000, 0.18); tg.fillEllipse(16, 44, 22, 8)
|
|||
|
|
tg.fillStyle(0x6D4C41); tg.fillRect(11, 28, 10, 18)
|
|||
|
|
tg.fillStyle(0x2E7D32); tg.fillCircle(16, 20, 15)
|
|||
|
|
tg.fillStyle(0x388E3C); tg.fillCircle(10, 25, 11); tg.fillCircle(22, 25, 11)
|
|||
|
|
tg.fillStyle(0x43A047); tg.fillCircle(16, 14, 10)
|
|||
|
|
tg.generateTexture('tree', 32, 52)
|
|||
|
|
tg.destroy()
|
|||
|
|
|
|||
|
|
// Rock (40 × 34)
|
|||
|
|
const rg = this.add.graphics()
|
|||
|
|
rg.fillStyle(0x000000, 0.18); rg.fillEllipse(20, 30, 36, 8)
|
|||
|
|
rg.fillStyle(0x78909C); rg.fillEllipse(20, 20, 36, 26)
|
|||
|
|
rg.fillStyle(0x90A4AE); rg.fillEllipse(14, 14, 20, 16)
|
|||
|
|
rg.fillStyle(0xB0BEC5, 0.6); rg.fillEllipse(12, 10, 10, 8)
|
|||
|
|
rg.generateTexture('rock', 40, 34)
|
|||
|
|
rg.destroy()
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ─── Player texture (placeholder – no player body in game) ───────────────
|
|||
|
|
|
|||
|
|
private buildPlayerTexture(): void {
|
|||
|
|
const T = TILE_SIZE
|
|||
|
|
const dirs = ['down', 'up', 'left', 'right'] as const
|
|||
|
|
dirs.forEach(dir => {
|
|||
|
|
const g = this.add.graphics()
|
|||
|
|
g.fillStyle(0x000000, 0.2); g.fillEllipse(T / 2, T - 4, 20, 8)
|
|||
|
|
g.fillStyle(0xE53935); g.fillCircle(T / 2, T / 2 - 2, 11)
|
|||
|
|
g.fillStyle(0x3949AB)
|
|||
|
|
g.fillRect(T / 2 - 8, T / 2 + 5, 7, 10)
|
|||
|
|
g.fillRect(T / 2 + 1, T / 2 + 5, 7, 10)
|
|||
|
|
g.fillStyle(0xFFFFFF)
|
|||
|
|
if (dir === 'down') g.fillTriangle(T/2, T/2+2, T/2-5, T/2-4, T/2+5, T/2-4)
|
|||
|
|
if (dir === 'up') g.fillTriangle(T/2, T/2-5, T/2-5, T/2+3, T/2+5, T/2+3)
|
|||
|
|
if (dir === 'left') g.fillTriangle(T/2-5, T/2, T/2+3, T/2-5, T/2+3, T/2+5)
|
|||
|
|
if (dir === 'right') g.fillTriangle(T/2+5, T/2, T/2-3, T/2-5, T/2-3, T/2+5)
|
|||
|
|
g.generateTexture(`player_${dir}`, T, T)
|
|||
|
|
g.destroy()
|
|||
|
|
})
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ─── Crop textures ────────────────────────────────────────────────────────
|
|||
|
|
|
|||
|
|
private buildCropTextures(): void {
|
|||
|
|
this.buildWheatTextures()
|
|||
|
|
this.buildCarrotTextures()
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
private buildWheatTextures(): void {
|
|||
|
|
const W = 32, H = 40
|
|||
|
|
|
|||
|
|
const g0 = this.add.graphics()
|
|||
|
|
g0.fillStyle(0x8BC34A); g0.fillRect(14, 26, 4, 12)
|
|||
|
|
g0.fillStyle(0x9CCC65); g0.fillEllipse(16, 24, 10, 8)
|
|||
|
|
g0.generateTexture('crop_wheat_0', W, H); g0.destroy()
|
|||
|
|
|
|||
|
|
const g1 = this.add.graphics()
|
|||
|
|
g1.fillStyle(0x7CB342)
|
|||
|
|
for (let i = 0; i < 3; i++) {
|
|||
|
|
g1.fillRect(8 + i * 8, 16 + i * 2, 3, 22 - i * 2)
|
|||
|
|
}
|
|||
|
|
g1.fillStyle(0x9CCC65)
|
|||
|
|
g1.fillEllipse(10, 14, 8, 6); g1.fillEllipse(18, 12, 8, 6); g1.fillEllipse(26, 16, 8, 6)
|
|||
|
|
g1.generateTexture('crop_wheat_1', W, H); g1.destroy()
|
|||
|
|
|
|||
|
|
const g2 = this.add.graphics()
|
|||
|
|
g2.fillStyle(0x558B2F)
|
|||
|
|
for (let i = 0; i < 4; i++) {
|
|||
|
|
g2.fillRect(5 + i * 7, 8 + (i % 2) * 4, 3, 30 - (i % 2) * 4)
|
|||
|
|
}
|
|||
|
|
g2.fillStyle(0x689F38)
|
|||
|
|
g2.fillEllipse(7, 6, 7, 5); g2.fillEllipse(14, 4, 7, 5)
|
|||
|
|
g2.fillEllipse(21, 7, 7, 5); g2.fillEllipse(28, 5, 7, 5)
|
|||
|
|
g2.generateTexture('crop_wheat_2', W, H); g2.destroy()
|
|||
|
|
|
|||
|
|
const g3 = this.add.graphics()
|
|||
|
|
g3.fillStyle(0x795548)
|
|||
|
|
for (let i = 0; i < 5; i++) {
|
|||
|
|
g3.fillRect(3 + i * 6, 14 + (i % 2) * 2, 2, 24)
|
|||
|
|
}
|
|||
|
|
g3.fillStyle(0xFDD835)
|
|||
|
|
for (let i = 0; i < 5; i++) {
|
|||
|
|
g3.fillEllipse(4 + i * 6, 10 + (i % 2) * 2, 6, 12)
|
|||
|
|
}
|
|||
|
|
g3.fillStyle(0xF9A825, 0.7)
|
|||
|
|
for (let i = 0; i < 5; i++) {
|
|||
|
|
g3.fillRect(3 + i * 6, 4 + (i % 2) * 2, 2, 8)
|
|||
|
|
}
|
|||
|
|
g3.generateTexture('crop_wheat_3', W, H); g3.destroy()
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
private buildCarrotTextures(): void {
|
|||
|
|
const W = 32, H = 40
|
|||
|
|
|
|||
|
|
const g0 = this.add.graphics()
|
|||
|
|
g0.fillStyle(0x4CAF50); g0.fillRect(14, 26, 4, 12)
|
|||
|
|
g0.fillStyle(0x66BB6A); g0.fillEllipse(16, 24, 10, 8)
|
|||
|
|
g0.generateTexture('crop_carrot_0', W, H); g0.destroy()
|
|||
|
|
|
|||
|
|
const g1 = this.add.graphics()
|
|||
|
|
g1.fillStyle(0x388E3C)
|
|||
|
|
g1.fillRect(14, 22, 4, 16)
|
|||
|
|
g1.fillEllipse(11, 18, 10, 8); g1.fillEllipse(21, 18, 10, 8)
|
|||
|
|
g1.fillEllipse(16, 14, 8, 10)
|
|||
|
|
g1.generateTexture('crop_carrot_1', W, H); g1.destroy()
|
|||
|
|
|
|||
|
|
const g2 = this.add.graphics()
|
|||
|
|
g2.fillStyle(0x2E7D32)
|
|||
|
|
g2.fillRect(14, 18, 4, 20)
|
|||
|
|
g2.fillEllipse(9, 14, 12, 10); g2.fillEllipse(23, 14, 12, 10)
|
|||
|
|
g2.fillEllipse(16, 8, 10, 12); g2.fillEllipse(13, 10, 8, 8); g2.fillEllipse(19, 10, 8, 8)
|
|||
|
|
g2.fillStyle(0xE65100, 0.6); g2.fillEllipse(16, 36, 8, 6)
|
|||
|
|
g2.generateTexture('crop_carrot_2', W, H); g2.destroy()
|
|||
|
|
|
|||
|
|
const g3 = this.add.graphics()
|
|||
|
|
g3.fillStyle(0x1B5E20)
|
|||
|
|
g3.fillRect(14, 14, 4, 16)
|
|||
|
|
g3.fillEllipse(8, 10, 12, 10); g3.fillEllipse(24, 10, 12, 10)
|
|||
|
|
g3.fillEllipse(16, 4, 10, 12); g3.fillEllipse(13, 6, 8, 8); g3.fillEllipse(19, 6, 8, 8)
|
|||
|
|
g3.fillStyle(0xFF6F00); g3.fillEllipse(16, 30, 12, 10)
|
|||
|
|
g3.fillStyle(0xFF8F00)
|
|||
|
|
g3.fillTriangle(10, 28, 22, 28, 16, 40)
|
|||
|
|
g3.fillStyle(0xFFCC02, 0.4); g3.fillEllipse(13, 27, 5, 4)
|
|||
|
|
g3.generateTexture('crop_carrot_3', W, H); g3.destroy()
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ─── UI panel texture ─────────────────────────────────────────────────────
|
|||
|
|
|
|||
|
|
private buildUITextures(): void {
|
|||
|
|
const pg = this.add.graphics()
|
|||
|
|
pg.fillStyle(0x000000, 0.65)
|
|||
|
|
pg.fillRoundedRect(0, 0, 200, 100, 8)
|
|||
|
|
pg.generateTexture('panel', 200, 100)
|
|||
|
|
pg.destroy()
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ─── Villager + building object textures ──────────────────────────────────
|
|||
|
|
|
|||
|
|
private buildVillagerAndBuildingTextures(): void {
|
|||
|
|
// ── Villager gnome (24 × 28) ──────────────────────────────────────────
|
|||
|
|
const vg = this.add.graphics()
|
|||
|
|
vg.fillStyle(0x000000, 0.15); vg.fillEllipse(12, 26, 18, 6)
|
|||
|
|
vg.fillStyle(0x1A237E); vg.fillRect(7, 16, 5, 9)
|
|||
|
|
vg.fillRect(12, 16, 5, 9)
|
|||
|
|
vg.fillStyle(0x3949AB); vg.fillRect(5, 9, 14, 10)
|
|||
|
|
vg.fillStyle(0xFFCC80); vg.fillCircle(12, 7, 6)
|
|||
|
|
vg.fillStyle(0x4E342E); vg.fillCircle(10, 6, 1); vg.fillCircle(14, 6, 1)
|
|||
|
|
vg.generateTexture('villager', 24, 28)
|
|||
|
|
vg.destroy()
|
|||
|
|
|
|||
|
|
// ── Bed (32 × 32) ─────────────────────────────────────────────────────
|
|||
|
|
const bg = this.add.graphics()
|
|||
|
|
bg.fillStyle(0x6D4C41); bg.fillRect(1, 1, 30, 30)
|
|||
|
|
bg.fillStyle(0xEFEBE9); bg.fillRect(3, 3, 26, 10)
|
|||
|
|
bg.fillStyle(0x5C6BC0); bg.fillRect(3, 15, 26, 14)
|
|||
|
|
bg.lineStyle(1, 0x4E342E, 0.8); bg.strokeRect(1, 1, 30, 30)
|
|||
|
|
bg.fillStyle(0xBCAAA4); bg.fillRect(3, 13, 26, 3)
|
|||
|
|
bg.generateTexture('bed_obj', 32, 32)
|
|||
|
|
bg.destroy()
|
|||
|
|
|
|||
|
|
// ── Stockpile zone (32 × 32) ──────────────────────────────────────────
|
|||
|
|
const sg = this.add.graphics()
|
|||
|
|
sg.fillStyle(0xFFF9C4, 0.5); sg.fillRect(0, 0, 32, 32)
|
|||
|
|
sg.lineStyle(2, 0xF9A825, 0.9)
|
|||
|
|
for (let i = 0; i < 32; i += 6) { sg.strokeRect(i, 0, 6, 32) }
|
|||
|
|
sg.fillStyle(0x8D6E63); sg.fillRect(6, 10, 9, 9); sg.fillRect(17, 10, 9, 9)
|
|||
|
|
sg.fillRect(6, 20, 9, 9); sg.fillRect(17, 20, 9, 9)
|
|||
|
|
sg.lineStyle(1, 0x5D4037)
|
|||
|
|
sg.strokeRect(6, 10, 9, 9); sg.strokeRect(17, 10, 9, 9)
|
|||
|
|
sg.strokeRect(6, 20, 9, 9); sg.strokeRect(17, 20, 9, 9)
|
|||
|
|
sg.generateTexture('stockpile_obj', 32, 32)
|
|||
|
|
sg.destroy()
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ─── Terrain generation ───────────────────────────────────────────────────
|
|||
|
|
|
|||
|
|
private generateWorldIfNeeded(): void {
|
|||
|
|
const state = stateManager.getState()
|
|||
|
|
if (Object.keys(state.world.resources).length === 0) {
|
|||
|
|
const tiles = generateTerrain(state.world.seed)
|
|||
|
|
const mutableTiles = state.world.tiles as number[]
|
|||
|
|
for (let i = 0; i < tiles.length; i++) mutableTiles[i] = tiles[i]
|
|||
|
|
|
|||
|
|
for (let y = 0; y < WORLD_TILES; y++) {
|
|||
|
|
for (let x = 0; x < WORLD_TILES; x++) {
|
|||
|
|
const tile = tiles[y * WORLD_TILES + x]
|
|||
|
|
if (tile === TileType.FOREST && Math.random() < 0.7) {
|
|||
|
|
const id = `tree_${x}_${y}`
|
|||
|
|
state.world.resources[id] = { id, tileX: x, tileY: y, kind: 'tree', hp: 3 }
|
|||
|
|
} else if (tile === TileType.ROCK && Math.random() < 0.5) {
|
|||
|
|
const id = `rock_${x}_${y}`
|
|||
|
|
state.world.resources[id] = { id, tileX: x, tileY: y, kind: 'rock', hp: 5 }
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const spawn = findSpawn(tiles)
|
|||
|
|
;(state.player as { x: number; y: number }).x = (spawn.tileX + 0.5) * 32
|
|||
|
|
;(state.player as { x: number; y: number }).y = (spawn.tileY + 0.5) * 32
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|