diff --git a/client/public/assets/lpc/dirt.png b/client/public/assets/lpc/dirt.png new file mode 100644 index 0000000..6143264 Binary files /dev/null and b/client/public/assets/lpc/dirt.png differ diff --git a/client/public/assets/lpc/grass.png b/client/public/assets/lpc/grass.png new file mode 100644 index 0000000..bf7098b Binary files /dev/null and b/client/public/assets/lpc/grass.png differ diff --git a/client/public/assets/lpc/treetop.png b/client/public/assets/lpc/treetop.png new file mode 100644 index 0000000..5a79014 Binary files /dev/null and b/client/public/assets/lpc/treetop.png differ diff --git a/client/public/assets/lpc/watergrass.png b/client/public/assets/lpc/watergrass.png new file mode 100644 index 0000000..93fee6b Binary files /dev/null and b/client/public/assets/lpc/watergrass.png differ diff --git a/client/public/assets/tileset_16x16.png b/client/public/assets/tileset_16x16.png deleted file mode 100644 index 13ccc5e..0000000 Binary files a/client/public/assets/tileset_16x16.png and /dev/null differ diff --git a/client/src/scenes/BootScene.ts b/client/src/scenes/BootScene.ts index 76f7aa9..722d25a 100644 --- a/client/src/scenes/BootScene.ts +++ b/client/src/scenes/BootScene.ts @@ -10,7 +10,10 @@ export class BootScene extends Phaser.Scene { } preload(): void { - this.load.image('tileset_16x16', 'assets/tileset_16x16.png'); + this.load.image('lpc_grass', 'assets/lpc/grass.png'); + this.load.image('lpc_watergrass', 'assets/lpc/watergrass.png'); + this.load.image('lpc_dirt', 'assets/lpc/dirt.png'); + this.load.image('lpc_treetop', 'assets/lpc/treetop.png'); } create(): void { diff --git a/client/src/scenes/GameScene.ts b/client/src/scenes/GameScene.ts index 6ea4d6d..3de3ea5 100644 --- a/client/src/scenes/GameScene.ts +++ b/client/src/scenes/GameScene.ts @@ -6,7 +6,7 @@ import { NpcInfoPanel } from '../ui/NpcInfoPanel.js'; import { CommandPanel } from '../ui/CommandPanel.js'; import { InteractionEmojiManager } from '../ui/InteractionEmoji.js'; import type { WorldState, StateUpdate, EntityState, Appearance } from '@dflike/shared'; -import { TILE_SIZE, SPRITE_FRAME_WIDTH, SPRITE_FRAME_HEIGHT, SPRITE_COLS, Direction, Terrain, TILESET_TILE_SIZE, TILESET_COLS, TILESET_SCALE } from '@dflike/shared'; +import { TILE_SIZE, SPRITE_FRAME_WIDTH, SPRITE_FRAME_HEIGHT, SPRITE_COLS, Direction, Terrain, TILESET_TILE_SIZE, TILESET_SCALE } from '@dflike/shared'; interface EntitySprite { sprite: Phaser.GameObjects.Sprite; @@ -145,108 +145,127 @@ export class GameScene extends Phaser.Scene { private drawWorld(): void { const { worldWidth, worldHeight, terrain, decorations, pointsOfInterest } = this.worldState; - // --- Tile index mapping --- - // Tileset: 17 cols x 19 rows of 16x16 tiles (272x304px) - // Autotile blocks are 5 cols x 3 rows on the left side: - // Grass: rows 0-2 Water: rows 3-5 Dirt: rows 6-8 - // Layout per block: - // [NW-outer] [N-edge ] [NE-outer] [inner-NW] [inner-NE] - // [W-edge ] [center ] [E-edge ] [inner-SW] [inner-SE] - // [SW-outer] [S-edge ] [SE-outer] [variant ] [variant ] + // --- LPC autotile layout (3 cols x 6 rows per tileset, 32x32 tiles) --- + // Row 0: [inner-SE=0] [inner-SW=1] [??=2] + // Row 1: [inner-NE=3] [inner-NW=4] [??=5] + // Row 2: [outer-NW=6] [N-edge=7] [outer-NE=8] + // Row 3: [W-edge=9] [center=10] [E-edge=11] + // Row 4: [outer-SW=12][S-edge=13] [outer-SE=14] + // Row 5: [fill-1=15] [fill-2=16] [fill-3=17] + // + // "outer" = convex corner of the terrain island + // "inner" = concave notch (diagonal-only neighbor is the other terrain) - const C = TILESET_COLS; // 17 - - interface AutotileSet { - nw: number; n: number; ne: number; - w: number; c: number; e: number; - sw: number; s: number; se: number; - inner_nw: number; inner_ne: number; - inner_sw: number; inner_se: number; - } - - const makeAutotile = (baseRow: number): AutotileSet => ({ - nw: baseRow * C + 0, n: baseRow * C + 1, ne: baseRow * C + 2, - w: (baseRow + 1) * C + 0, c: (baseRow + 1) * C + 1, e: (baseRow + 1) * C + 2, - sw: (baseRow + 2) * C + 0, s: (baseRow + 2) * C + 1, se: (baseRow + 2) * C + 2, - inner_nw: baseRow * C + 3, inner_ne: baseRow * C + 4, - inner_sw: (baseRow + 1) * C + 3, inner_se: (baseRow + 1) * C + 4, - }); - - const GRASS = makeAutotile(0); - const WATER = makeAutotile(3); - const DIRT = makeAutotile(6); + const FILL = 16; // solid fill tile (row 5, col 1) const getTerr = (x: number, y: number): number => { if (x < 0 || y < 0 || x >= worldWidth || y >= worldHeight) return Terrain.GRASS; return terrain[y * worldWidth + x]; }; - // Pick autotile based on 8-neighbor context - const pickOverlayTile = (x: number, y: number, terrainType: number): number => { - const tiles = terrainType === Terrain.WATER ? WATER : DIRT; - const same = (dx: number, dy: number) => getTerr(x + dx, y + dy) === terrainType; - const n = same(0, -1), s = same(0, 1), w = same(-1, 0), e = same(1, 0); - const nw = same(-1, -1), ne = same(1, -1), sw = same(-1, 1), se = same(1, 1); + // For a GRASS tile, pick the watergrass/dirt transition tile based on + // which neighbors are the "other" terrain type. + // The LPC edge tiles are drawn FROM the grass perspective (grass is the terrain, + // water/dirt is the background bleeding through at edges). + const pickEdgeTile = (x: number, y: number, otherTerrain: number): number => { + const isOther = (dx: number, dy: number) => getTerr(x + dx, y + dy) === otherTerrain; + const n = isOther(0, -1), s = isOther(0, 1), w = isOther(-1, 0), e = isOther(1, 0); + const nw = isOther(-1, -1), ne = isOther(1, -1), sw = isOther(-1, 1), se = isOther(1, 1); - // Outer corners - if (!n && !w) return tiles.nw; - if (!n && !e) return tiles.ne; - if (!s && !w) return tiles.sw; - if (!s && !e) return tiles.se; - // Edges - if (!n) return tiles.n; - if (!s) return tiles.s; - if (!w) return tiles.w; - if (!e) return tiles.e; - // Inner corners (cardinal matches but diagonal doesn't) - if (!nw) return tiles.inner_nw; - if (!ne) return tiles.inner_ne; - if (!sw) return tiles.inner_sw; - if (!se) return tiles.inner_se; - // Fully surrounded - return tiles.c; + // Outer corners (two cardinal sides are the other terrain) + if (n && w) return 6; // outer-NW: grass only in SE, other terrain in NW + if (n && e) return 8; // outer-NE + if (s && w) return 12; // outer-SW + if (s && e) return 14; // outer-SE + // Edges (one cardinal side is the other terrain) + if (n) return 7; // N-edge: other terrain bleeds from north + if (s) return 13; // S-edge + if (w) return 9; // W-edge + if (e) return 11; // E-edge + // Inner corners (all cardinal = grass, but a diagonal is the other terrain) + if (se) return 0; // inner-SE: other terrain peeks in SE corner + if (sw) return 1; // inner-SW + if (ne) return 3; // inner-NE + if (nw) return 4; // inner-NW + // No adjacent other-terrain at all + return -1; }; - // Build tile data arrays - const grassData: number[][] = []; // base layer: all grass - const overlayData: number[][] = []; // water/dirt with autotile edges - const decoData: number[][] = []; // tree/bush decorations + // Build tile data arrays for each layer + const grassData: number[][] = []; // base: solid grass everywhere + const waterData: number[][] = []; // watergrass transitions + solid water + const dirtData: number[][] = []; // dirt transitions + solid dirt + const decoData: number[][] = []; // tree decorations for (let y = 0; y < worldHeight; y++) { const grassRow: number[] = []; - const overlayRow: number[] = []; + const waterRow: number[] = []; + const dirtRow: number[] = []; const decoRow: number[] = []; + for (let x = 0; x < worldWidth; x++) { - grassRow.push(GRASS.c); const t = terrain[y * worldWidth + x]; - overlayRow.push(t !== Terrain.GRASS ? pickOverlayTile(x, y, t) : -1); + + // Base: always solid grass + grassRow.push(FILL); + + // Water layer + if (t === Terrain.WATER) { + waterRow.push(FILL); // solid water + } else if (t === Terrain.GRASS) { + waterRow.push(pickEdgeTile(x, y, Terrain.WATER)); + } else { + waterRow.push(-1); + } + + // Dirt layer + if (t === Terrain.DIRT) { + dirtRow.push(FILL); // solid dirt + } else if (t === Terrain.GRASS) { + dirtRow.push(pickEdgeTile(x, y, Terrain.DIRT)); + } else { + dirtRow.push(-1); + } + + // Decorations const deco = decorations[y * worldWidth + x]; decoRow.push(deco >= 0 ? deco : -1); } + grassData.push(grassRow); - overlayData.push(overlayRow); + waterData.push(waterRow); + dirtData.push(dirtRow); decoData.push(decoRow); } - // Helper to create a tilemap layer from a 2D data array - const createTileLayer = (data: number[][], name: string, depth: number) => { + // Helper to create a scaled tilemap layer + const createLayer = ( + data: number[][], + tilesetName: string, + textureKey: string, + depth: number, + ) => { const tm = this.make.tilemap({ data, tileWidth: TILESET_TILE_SIZE, tileHeight: TILESET_TILE_SIZE, }); - const ts = tm.addTilesetImage(name, 'tileset_16x16', TILESET_TILE_SIZE, TILESET_TILE_SIZE, 0, 0)!; + const ts = tm.addTilesetImage( + tilesetName, textureKey, + TILESET_TILE_SIZE, TILESET_TILE_SIZE, 0, 0, + )!; const layer = tm.createLayer(0, ts, 0, 0)!; layer.setScale(TILESET_SCALE); layer.setDepth(depth); return layer; }; - createTileLayer(grassData, 'grass', -3); - createTileLayer(overlayData, 'overlay', -2); - createTileLayer(decoData, 'deco', -1); + createLayer(grassData, 'grass', 'lpc_grass', -4); + createLayer(waterData, 'water', 'lpc_watergrass', -3); + createLayer(dirtData, 'dirt', 'lpc_dirt', -2); + createLayer(decoData, 'trees', 'lpc_treetop', -1); - // Points of interest (rendered as colored markers on top of terrain) + // Points of interest markers const graphics = this.add.graphics(); graphics.setDepth(0); for (const poi of pointsOfInterest) { diff --git a/server/src/map/mapGenerator.ts b/server/src/map/mapGenerator.ts index 4d784e9..9ad00b4 100644 --- a/server/src/map/mapGenerator.ts +++ b/server/src/map/mapGenerator.ts @@ -108,12 +108,11 @@ export function generateMap( } // --- Trees (decorations on grass tiles) --- - // Tree tile indices in the tileset (17 cols wide): - // Deciduous tree top: row 2, col 9 = 2*17+9 = 43 (but multi-tile) - // Small bush: row 7, col 9 = 7*17+9 = 128 - // Pine tree: row 3, col 15 = 3*17+15 = 66 - // We'll use a few different tree/bush decoration tiles - const TREE_TILES = [128, 129, 130, 145, 146]; // small plants/bushes from right side of tileset + // Tree tile indices reference treetop.png (6 cols x 7 rows) + // Pick recognizable canopy center tiles from each of the 4 trees: + // Deciduous 1 center: (1,1) = 7 Deciduous 2 center: (4,1) = 10 + // Conifer 1 center: (1,5) = 31 Conifer 2 center: (4,5) = 34 + const TREE_TILES = [7, 10, 31, 34]; for (let y = 1; y < height - 1; y++) { for (let x = 1; x < width - 1; x++) { diff --git a/shared/src/constants.ts b/shared/src/constants.ts index a5721da..75e7f27 100644 --- a/shared/src/constants.ts +++ b/shared/src/constants.ts @@ -72,7 +72,8 @@ export const Terrain = { } as const; export type Terrain = (typeof Terrain)[keyof typeof Terrain]; -// Tileset constants (16x16 tileset scaled 3x to match TILE_SIZE=48) -export const TILESET_TILE_SIZE = 16; -export const TILESET_COLS = 17; // 272px / 16px -export const TILESET_SCALE = TILE_SIZE / TILESET_TILE_SIZE; // 3 +// LPC tileset constants (32x32 tiles scaled 1.5x to match TILE_SIZE=48) +export const TILESET_TILE_SIZE = 32; +export const LPC_COLS = 3; // terrain tilesets are 3 cols wide (96px / 32px) +export const TILESET_SCALE = TILE_SIZE / TILESET_TILE_SIZE; // 1.5 +export const TREETOP_COLS = 6; // treetop.png is 6 cols wide (192px / 32px)