2026-03-20 08:11:31 +00:00
|
|
|
import Phaser from 'phaser'
|
|
|
|
|
import { TILE_SIZE, WORLD_TILES } from '../config'
|
|
|
|
|
import { TileType, IMPASSABLE } from '../types'
|
|
|
|
|
import { stateManager } from '../StateManager'
|
|
|
|
|
|
|
|
|
|
const BIOME_COLORS: Record<number, string> = {
|
|
|
|
|
0: '#1565C0', // DEEP_WATER
|
|
|
|
|
1: '#42A5F5', // SHALLOW_WATER
|
|
|
|
|
2: '#F5DEB3', // SAND
|
|
|
|
|
3: '#66BB6A', // GRASS
|
|
|
|
|
4: '#43A047', // DARK_GRASS
|
|
|
|
|
5: '#33691E', // FOREST
|
|
|
|
|
6: '#616161', // ROCK
|
|
|
|
|
// Built types: show grass below
|
|
|
|
|
7: '#66BB6A', 8: '#66BB6A', 9: '#66BB6A', 10: '#66BB6A',
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export class WorldSystem {
|
|
|
|
|
private scene: Phaser.Scene
|
|
|
|
|
private map!: Phaser.Tilemaps.Tilemap
|
|
|
|
|
private tileset!: Phaser.Tilemaps.Tileset
|
|
|
|
|
private bgImage!: Phaser.GameObjects.Image
|
|
|
|
|
private builtLayer!: Phaser.Tilemaps.TilemapLayer
|
2026-03-21 16:15:21 +00:00
|
|
|
private bgCanvasTexture!: Phaser.Textures.CanvasTexture
|
2026-03-20 08:11:31 +00:00
|
|
|
|
2026-03-21 12:11:54 +00:00
|
|
|
/** @param scene - The Phaser scene this system belongs to */
|
2026-03-20 08:11:31 +00:00
|
|
|
constructor(scene: Phaser.Scene) {
|
|
|
|
|
this.scene = scene
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-21 12:11:54 +00:00
|
|
|
/**
|
|
|
|
|
* Generates the terrain background canvas from saved tile data,
|
|
|
|
|
* creates the built-tile tilemap layer, and sets camera bounds.
|
|
|
|
|
*/
|
2026-03-20 08:11:31 +00:00
|
|
|
create(): void {
|
|
|
|
|
const state = stateManager.getState()
|
|
|
|
|
|
|
|
|
|
// --- Canvas background (1px per tile, scaled up, LINEAR filtered) ---
|
2026-03-21 16:15:21 +00:00
|
|
|
const canvasTexture = this.scene.textures.createCanvas('terrain_bg', WORLD_TILES, WORLD_TILES) as Phaser.Textures.CanvasTexture
|
|
|
|
|
const ctx = canvasTexture.context
|
2026-03-20 08:11:31 +00:00
|
|
|
|
|
|
|
|
for (let y = 0; y < WORLD_TILES; y++) {
|
|
|
|
|
for (let x = 0; x < WORLD_TILES; x++) {
|
|
|
|
|
const tile = state.world.tiles[y * WORLD_TILES + x]
|
|
|
|
|
ctx.fillStyle = BIOME_COLORS[tile] ?? '#0a2210'
|
|
|
|
|
ctx.fillRect(x, y, 1, 1)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-21 16:15:21 +00:00
|
|
|
canvasTexture.refresh()
|
|
|
|
|
this.bgCanvasTexture = canvasTexture
|
|
|
|
|
|
2026-03-20 08:11:31 +00:00
|
|
|
this.bgImage = this.scene.add.image(0, 0, 'terrain_bg')
|
|
|
|
|
.setOrigin(0, 0)
|
|
|
|
|
.setScale(TILE_SIZE)
|
|
|
|
|
.setDepth(0)
|
2026-03-21 16:15:21 +00:00
|
|
|
canvasTexture.setFilter(Phaser.Textures.FilterMode.LINEAR)
|
2026-03-20 08:11:31 +00:00
|
|
|
|
|
|
|
|
// --- Built tile layer (sparse — only FLOOR, WALL, TILLED_SOIL, WATERED_SOIL) ---
|
|
|
|
|
this.map = this.scene.make.tilemap({
|
|
|
|
|
tileWidth: TILE_SIZE,
|
|
|
|
|
tileHeight: TILE_SIZE,
|
|
|
|
|
width: WORLD_TILES,
|
|
|
|
|
height: WORLD_TILES,
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
const ts = this.map.addTilesetImage('tiles', 'tiles', TILE_SIZE, TILE_SIZE, 0, 0, 0)
|
|
|
|
|
if (!ts) throw new Error('Failed to add tileset')
|
|
|
|
|
this.tileset = ts
|
|
|
|
|
|
|
|
|
|
const layer = this.map.createBlankLayer('built', this.tileset, 0, 0)
|
|
|
|
|
if (!layer) throw new Error('Failed to create built layer')
|
|
|
|
|
this.builtLayer = layer
|
|
|
|
|
this.builtLayer.setDepth(1)
|
|
|
|
|
|
|
|
|
|
const BUILT_TILES = new Set([7, 8, 9, 10]) // FLOOR, WALL, TILLED_SOIL, WATERED_SOIL
|
|
|
|
|
for (let y = 0; y < WORLD_TILES; y++) {
|
|
|
|
|
for (let x = 0; x < WORLD_TILES; x++) {
|
|
|
|
|
const t = state.world.tiles[y * WORLD_TILES + x]
|
|
|
|
|
if (BUILT_TILES.has(t)) {
|
|
|
|
|
this.builtLayer.putTileAt(t, x, y)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Camera bounds
|
|
|
|
|
this.scene.cameras.main.setBounds(0, 0, WORLD_TILES * TILE_SIZE, WORLD_TILES * TILE_SIZE)
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-21 12:11:54 +00:00
|
|
|
/** Returns the built-tile tilemap layer (floor, wall, soil). */
|
2026-03-20 08:11:31 +00:00
|
|
|
getLayer(): Phaser.Tilemaps.TilemapLayer {
|
|
|
|
|
return this.builtLayer
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-21 12:11:54 +00:00
|
|
|
/**
|
|
|
|
|
* Places or removes a tile on the built layer.
|
|
|
|
|
* Built tile types are added; natural types remove the built-layer entry.
|
|
|
|
|
* @param tileX - Tile column
|
|
|
|
|
* @param tileY - Tile row
|
|
|
|
|
* @param type - New tile type to apply
|
|
|
|
|
*/
|
2026-03-20 08:11:31 +00:00
|
|
|
setTile(tileX: number, tileY: number, type: TileType): void {
|
|
|
|
|
const BUILT_TILES = new Set([TileType.FLOOR, TileType.WALL, TileType.TILLED_SOIL, TileType.WATERED_SOIL])
|
|
|
|
|
if (BUILT_TILES.has(type)) {
|
|
|
|
|
this.builtLayer.putTileAt(type, tileX, tileY)
|
|
|
|
|
} else {
|
|
|
|
|
// Reverting to natural: remove from built layer
|
|
|
|
|
this.builtLayer.removeTileAt(tileX, tileY)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-21 12:11:54 +00:00
|
|
|
/**
|
|
|
|
|
* Returns whether the tile at the given coordinates can be walked on.
|
|
|
|
|
* Out-of-bounds tiles are treated as impassable.
|
|
|
|
|
* @param tileX - Tile column
|
|
|
|
|
* @param tileY - Tile row
|
|
|
|
|
*/
|
2026-03-20 08:11:31 +00:00
|
|
|
isPassable(tileX: number, tileY: number): boolean {
|
|
|
|
|
if (tileX < 0 || tileY < 0 || tileX >= WORLD_TILES || tileY >= WORLD_TILES) return false
|
|
|
|
|
const state = stateManager.getState()
|
|
|
|
|
const tile = state.world.tiles[tileY * WORLD_TILES + tileX]
|
|
|
|
|
return !IMPASSABLE.has(tile)
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-21 12:11:54 +00:00
|
|
|
/**
|
|
|
|
|
* Converts world pixel coordinates to tile coordinates.
|
|
|
|
|
* @param worldX - World X in pixels
|
|
|
|
|
* @param worldY - World Y in pixels
|
|
|
|
|
* @returns Integer tile position
|
|
|
|
|
*/
|
2026-03-20 08:11:31 +00:00
|
|
|
worldToTile(worldX: number, worldY: number): { tileX: number; tileY: number } {
|
|
|
|
|
return {
|
|
|
|
|
tileX: Math.floor(worldX / TILE_SIZE),
|
|
|
|
|
tileY: Math.floor(worldY / TILE_SIZE),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-21 12:11:54 +00:00
|
|
|
/**
|
|
|
|
|
* Converts tile coordinates to the world pixel center of that tile.
|
|
|
|
|
* @param tileX - Tile column
|
|
|
|
|
* @param tileY - Tile row
|
|
|
|
|
* @returns World pixel center position
|
|
|
|
|
*/
|
2026-03-20 08:11:31 +00:00
|
|
|
tileToWorld(tileX: number, tileY: number): { x: number; y: number } {
|
|
|
|
|
return {
|
|
|
|
|
x: tileX * TILE_SIZE + TILE_SIZE / 2,
|
|
|
|
|
y: tileY * TILE_SIZE + TILE_SIZE / 2,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-21 12:11:54 +00:00
|
|
|
/**
|
|
|
|
|
* Returns the tile type at the given tile coordinates from saved state.
|
|
|
|
|
* @param tileX - Tile column
|
|
|
|
|
* @param tileY - Tile row
|
|
|
|
|
*/
|
2026-03-20 08:11:31 +00:00
|
|
|
getTileType(tileX: number, tileY: number): TileType {
|
|
|
|
|
const state = stateManager.getState()
|
|
|
|
|
return state.world.tiles[tileY * WORLD_TILES + tileX] as TileType
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-21 16:15:21 +00:00
|
|
|
/**
|
|
|
|
|
* Updates a single tile's pixel on the background canvas and refreshes the GPU texture.
|
|
|
|
|
* Used when a natural tile changes at runtime (e.g. DARK_GRASS → GRASS after recovery,
|
|
|
|
|
* or GRASS → FOREST when a seedling matures).
|
|
|
|
|
* @param tileX - Tile column
|
|
|
|
|
* @param tileY - Tile row
|
|
|
|
|
* @param type - New tile type to reflect visually
|
|
|
|
|
*/
|
|
|
|
|
refreshTerrainTile(tileX: number, tileY: number, type: TileType): void {
|
|
|
|
|
const color = BIOME_COLORS[type] ?? '#0a2210'
|
|
|
|
|
this.bgCanvasTexture.context.fillStyle = color
|
|
|
|
|
this.bgCanvasTexture.context.fillRect(tileX, tileY, 1, 1)
|
|
|
|
|
this.bgCanvasTexture.refresh()
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-21 12:11:54 +00:00
|
|
|
/** Destroys the tilemap and background image. */
|
2026-03-20 08:11:31 +00:00
|
|
|
destroy(): void {
|
|
|
|
|
this.map.destroy()
|
|
|
|
|
this.bgImage.destroy()
|
|
|
|
|
}
|
|
|
|
|
}
|