Files
nissefolk/src/scenes/BootScene.ts
tekki mariani 18c8ccb644 implement unified tile system (Issue #14)
- Tree seedlings: plant tree_seed on grass via farming tool; two-stage
  growth (sprout → sapling → young tree, ~1 min/stage); matures into
  a harvestable FOREST resource tile
- Tile recovery: Nisse chops start a 5-min DARK_GRASS→GRASS timer;
  terrain canvas updated live via WorldSystem.refreshTerrainTile()
- New TreeSeedlingSystem manages sprites, growth ticking, maturation
- BootScene generates seedling_0/1/2 textures procedurally
- FarmingSystem adds tree_seed to tool cycle (F key)
- Stockpile panel shows tree_seed (default: 5); panel height adjusted
- StateManager v5: treeSeedlings + tileRecovery in WorldState
- WorldSystem uses CanvasTexture for live single-pixel updates

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-21 16:15:21 +00:00

401 lines
15 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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.buildSeedlingTextures()
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()
}
// ─── Tree seedling textures (3 growth stages) ────────────────────────────
/**
* Generates textures for the three tree-seedling growth stages:
* seedling_0 small sprout
* seedling_1 sapling with leaves
* seedling_2 young tree (about to mature into a FOREST tile)
*/
private buildSeedlingTextures(): void {
// Stage 0: tiny sprout
const g0 = this.add.graphics()
g0.fillStyle(0x6D4C41); g0.fillRect(10, 20, 4, 10)
g0.fillStyle(0x66BB6A); g0.fillEllipse(12, 16, 12, 8)
g0.fillStyle(0x4CAF50); g0.fillEllipse(12, 13, 8, 6)
g0.generateTexture('seedling_0', 24, 32); g0.destroy()
// Stage 1: sapling
const g1 = this.add.graphics()
g1.fillStyle(0x6D4C41); g1.fillRect(9, 15, 5, 16)
g1.fillStyle(0x4CAF50); g1.fillCircle(12, 12, 8)
g1.fillStyle(0x66BB6A, 0.7); g1.fillCircle(7, 16, 5); g1.fillCircle(17, 16, 5)
g1.fillStyle(0x81C784); g1.fillCircle(12, 8, 5)
g1.generateTexture('seedling_1', 24, 32); g1.destroy()
// Stage 2: young tree (mature, ready to become a resource)
const g2 = this.add.graphics()
g2.fillStyle(0x000000, 0.15); g2.fillEllipse(12, 28, 16, 6)
g2.fillStyle(0x6D4C41); g2.fillRect(9, 14, 6, 14)
g2.fillStyle(0x2E7D32); g2.fillCircle(12, 9, 10)
g2.fillStyle(0x388E3C); g2.fillCircle(7, 13, 7); g2.fillCircle(17, 13, 7)
g2.fillStyle(0x43A047); g2.fillCircle(12, 6, 7)
g2.generateTexture('seedling_2', 24, 32); g2.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
}
}
}