feat: switch to LPC tileset with layered autotiling
Replace the 16x16 tileset with LPC Base Assets (32x32 tiles, 1.5x scale). Uses a 4-layer rendering approach: - Layer 0: solid grass base (grass.png) - Layer 1: water transitions (watergrass.png) with LPC autotile edges - Layer 2: dirt transitions (dirt.png) with LPC autotile edges - Layer 3: tree canopy decorations (treetop.png) LPC autotile format: 3x6 grid with inner/outer corners, edges, and fill tiles. Edge tiles are selected based on 8-neighbor terrain analysis. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Binary file not shown.
|
After Width: | Height: | Size: 8.4 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 8.3 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 11 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 9.7 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 12 KiB |
@@ -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 {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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++) {
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user