Files

448 lines
16 KiB
JavaScript

// gen_maps.js: Generate 10 Tiled-format JSON maps per PRD §10.
// Produces assets/maps/*.json
// Tile index reference (matches tools/gen_tileset.lua order):
// NOTE: Tiled uses global tile IDs where 0 = empty and localIndex N -> global N+1 (firstgid=1).
// We store raw LOCAL indices in T.* and convert to global IDs (+1, 0 stays 0=empty) on write.
const T = {
grass: 0, path: 1, water: 2, flowersY: 3, flowersP: 4, flowersG: 5, berry: 6, tree: 7,
birdTree: 8, bush: 9, wall: 10, wallTop: 11, roof: 12, roofEdge: 13, door: 14, window: 15,
floor: 16, floorRug: 17, bed: 18, counter: 19, oven: 20, shelf: 21, shelfFull: 22, table: 23,
chair: 24, stool: 25, well: 26, fog: 27, fogThick: 28, whiteWall: 29, whiteFloor: 30, whiteDoor: 31,
whiteBed: 32, whiteMonitor: 33, whiteTable: 34, ribbon: 35, ribbon2: 36, picnic: 37, bench: 38, sign: 39,
calendar: 40, paper: 41, chart: 42, fenceH: 43, fenceV: 44, lamp: 45, potPlant: 46, barrel: 47,
crate: 48, grassAlt: 49, pathH: 50, pathV: 51, pathCross: 52, waterEdge: 53, grassDark: 54, pathCorner: 55,
flowerMix: 56, berryEmpty: 57, fogBand: 58, whitePillar: 59, floorEdge: 60, wallTrim: 61, grassPath: 62, transparent: 63,
};
// EMPTY tile (global 0): use a sentinel; we map 63(transparent local) -> 0 empty on output, others +1.
const EMPTY = 63; // local index used for "transparent/empty"
const COLS = 8; // tileset columns
const fs = require('fs');
const path = require('path');
const outDir = 'assets/maps';
fs.mkdirSync(outDir, { recursive: true });
// Load the tileset descriptor to embed inline.
const tilesetJson = JSON.parse(fs.readFileSync('assets/tilesets/tileset.json', 'utf8'));
function makeMap(name, width, height, layers) {
// layers: [{name, type:'tilelayer', data:number[] (len w*h)} or {name,type:'objectgroup', objects:[]}]
// Find door objects and clear obstacle tiles at their positions so the player can walk through.
const objLayer = layers.find(l => l.type === 'objectgroup');
const doorTiles = new Set();
if (objLayer) {
for (const o of objLayer.objects) {
if (o.type === 'door' || (o.name && o.name.startsWith('door'))) {
const tx = Math.floor(o.x / 16), ty = Math.floor(o.y / 16);
doorTiles.add(ty * width + tx);
}
}
}
const tiledLayers = layers.map(l => {
if (l.type === 'tilelayer') {
// convert local indices to global tile IDs: local N -> N+1, EMPTY(63) -> 0
// also clear obstacle tiles at door positions
const globalData = l.data.map((v, idx) => {
if (l.name === 'obstacles' && doorTiles.has(idx)) return 0; // clear wall at door
return (v === EMPTY ? 0 : v + 1);
});
return {
name: l.name,
type: 'tilelayer',
x: 0, y: 0,
width, height,
visible: true,
opacity: 1,
offsetx: 0, offsety: 0,
draworder: 'right-down',
data: globalData,
};
} else {
return {
name: l.name,
type: 'objectgroup',
x: 0, y: 0,
visible: true,
opacity: 1,
offsetx: 0, offsety: 0,
draworder: 'index',
objects: l.objects,
};
}
});
return {
compressionlevel: -1,
height, width,
tilewidth: 16,
tileheight: 16,
type: 'map',
orientation: 'orthogonal',
renderorder: 'right-down',
tiledversion: '1.12.2',
// embed tileset inline so Phaser resolves it without external file
tilesets: [{
firstgid: 1,
name: tilesetJson.name,
columns: tilesetJson.columns,
image: '../tilesets/tileset.png',
imagewidth: tilesetJson.imagewidth,
imageheight: tilesetJson.imageheight,
margin: 0,
spacing: 0,
tilecount: tilesetJson.tilecount,
tilewidth: 16,
tileheight: 16,
tiles: tilesetJson.tiles,
}],
layers: tiledLayers,
};
}
// fill helpers
function fillGrid(w, h, val) { return new Array(w * h).fill(val); }
function set(g, w, x, y, v) { if (x>=0&&y>=0&&x<w&&y<g.length/w) g[y*w+x] = v; }
function rectFill(g, w, h, x0, y0, rw, rh, v) {
for (let y=y0; y<y0+rh && y<h; y++) for (let x=x0; x<x0+rw && x<w; x++) set(g, w, x, y, v);
}
function borderFill(g, w, h, x0, y0, rw, rh, v) {
for (let y=y0; y<y0+rh && y<h; y++) for (let x=x0; x<x0+rw && x<w; x++) {
if (y===y0||y===y0+rh-1||x===x0||x===x0+rw-1) set(g,w,x,y,v);
}
}
// object helpers (pixel coords: x,y = top-left in pixels, 16 per tile)
function obj(name, type, tx, ty, props={}) {
return { name, type, x: tx*16, y: ty*16, width: props._w||16, height: props._h||16,
properties: Object.entries(props).filter(([k])=>!k.startsWith('_')).map(([k,v])=>({name:k,type:'string',value:String(v)})) };
}
const maps = {};
// 1. House (12x8) interior
{
const w=12, h=8;
const ground = fillGrid(w,h,T.floor);
rectFill(ground,w,h,0,0,12,1,T.wallTop);
rectFill(ground,w,h,0,h-1,12,1,T.floorEdge);
const obs = fillGrid(w,h,63); // transparent
borderFill(obs,w,h,0,0,12,8,T.wall);
// bed bottom-left
rectFill(obs,w,h,1,5,3,2,T.bed);
// calendar on wall (C1) - object only
// door to plaza (south center) — widen the gap so the player can't get stuck
set(obs,w,5,7,63); set(obs,w,6,7,63); set(ground,w,5,7,T.door);
// objects
const objects = [
obj('door_plaza','door',5,7,{target:'plaza', spawn:'door_house'}),
obj('bed','interact',1,5,{clue:'none'}),
obj('calendar','clue',2,0,{clue:'C1'}),
obj('sleep_point','interact',6,5),
];
maps.house = makeMap('house', w, h, [
{name:'ground',type:'tilelayer',data:ground},
{name:'obstacles',type:'tilelayer',data:obs},
{name:'objects',type:'objectgroup',objects},
]);
}
// 2. Plaza (30x20) hub
{
const w=30, h=20;
const ground = fillGrid(w,h,T.grass);
// cross paths
rectFill(ground,w,h,0,9,30,2,T.path); // horizontal
rectFill(ground,w,h,14,0,2,20,T.path); // vertical
// flower beds
rectFill(ground,w,h,3,3,4,3,T.flowersY);
rectFill(ground,w,h,23,3,4,3,T.flowersP);
// twin bird tree
set(ground,w,15,3,T.birdTree);
// signs/decor
set(ground,w,2,14,T.lamp); set(ground,w,27,14,T.lamp);
// ribbons area for festival (decor placed day6)
const obs = fillGrid(w,h,63);
// border trees
for (let x=0;x<w;x++){ set(obs,w,x,0,T.tree); set(obs,w,x,1,T.tree);}
for (let x=0;x<w;x++){ set(obs,w,x,h-1,T.tree);}
for (let y=0;y<h;y++){ set(obs,w,0,y,T.tree); set(obs,w,29,y,T.tree);}
// carve door gaps in the border + place a door tile on the ground at each.
// Gaps are 3 tiles tall/wide so the player can't miss them when walking to an edge.
// bakery: left edge, rows 7-9
for (let y=7;y<=9;y++) set(obs,w,0,y,63); set(ground,w,0,8,T.door);
// lake: left edge, rows 11-13
for (let y=11;y<=13;y++) set(obs,w,0,y,63); set(ground,w,0,12,T.door);
// fog_boundary: left edge, rows 2-4
for (let y=2;y<=4;y++) set(obs,w,0,y,63); set(ground,w,0,3,T.door);
// shop: right edge, rows 7-9
for (let y=7;y<=9;y++) set(obs,w,29,y,63); set(ground,w,29,8,T.door);
// field: right edge, rows 11-13
for (let y=11;y<=13;y++) set(obs,w,29,y,63); set(ground,w,29,12,T.door);
// well_area: right edge, rows 5-6
for (let y=5;y<=6;y++) set(obs,w,29,y,63); set(ground,w,29,5,T.door);
// clinic: top edge, cols 13-15
for (let x=13;x<=15;x++){ set(obs,w,x,0,63); set(obs,w,x,1,63); } set(ground,w,14,0,T.door);
// house: bottom edge, cols 13-15
for (let x=13;x<=15;x++) set(obs,w,x,19,63); set(ground,w,14,19,T.door);
// announcement board (collides)
set(obs,w,7,7,T.sign); set(obs,w,8,7,T.sign);
// fountain-ish bush
set(obs,w,20,12,T.bush);
// objects: doors to all scenes — placed at the border gaps so they're easy to find
const objects = [
obj('door_house','door',14,19,{target:'house', spawn:'door_plaza'}),
obj('door_bakery','door',0,8,{target:'bakery', spawn:'door_plaza'}),
obj('door_shop','door',29,8,{target:'shop', spawn:'door_plaza'}),
obj('door_clinic','door',14,0,{target:'clinic', spawn:'door_plaza'}),
obj('door_lake','door',0,12,{target:'lake', spawn:'door_plaza'}),
obj('door_field','door',29,12,{target:'field', spawn:'door_plaza'}),
obj('door_well','door',29,5,{target:'well_area', spawn:'door_plaza'}),
obj('door_fog','door',0,3,{target:'fog_boundary', spawn:'door_plaza'}),
// NPC spawn points (tile coords of standing position)
obj('npc_afu','spawn',8,8), // dog mayor at board
obj('npc_bird','spawn',15,2), // twin birds on tree
// board interaction
obj('board','interact',7,7),
// flowerbed water points (T2)
obj('waterflower1','interact',4,4),
obj('waterflower2','interact',5,4),
obj('waterflower3','interact',4,5),
// greet points
obj('greet_point','interact',15,10),
];
maps.plaza = makeMap('plaza', w, h, [
{name:'ground',type:'tilelayer',data:ground},
{name:'obstacles',type:'tilelayer',data:obs},
{name:'objects',type:'objectgroup',objects},
]);
}
// 3. Bakery (15x10)
{
const w=15,h=10;
const ground = fillGrid(w,h,T.floor);
rectFill(ground,w,h,0,0,15,1,T.wallTop);
const obs = fillGrid(w,h,63);
borderFill(obs,w,h,0,0,15,10,T.wall);
// counter
rectFill(obs,w,h,2,5,4,1,T.counter);
// oven
rectFill(obs,w,h,11,2,2,2,T.oven);
// shelves
set(obs,w,11,6,T.shelfFull);
// door
set(ground,w,7,9,T.door);
const objects = [
obj('door_plaza','door',7,9,{target:'plaza',spawn:'door_bakery'}),
obj('npc_yuanyuan','spawn',3,7),
obj('recipe_paper','clue',11,5,{clue:'C2'}), // on shelf
obj('oven','interact',11,2),
obj('knead_point','interact',3,6), // T11 knead dough
];
maps.bakery = makeMap('bakery', w, h, [
{name:'ground',type:'tilelayer',data:ground},
{name:'obstacles',type:'tilelayer',data:obs},
{name:'objects',type:'objectgroup',objects},
]);
}
// 4. Shop (15x10)
{
const w=15,h=10;
const ground = fillGrid(w,h,T.floor);
rectFill(ground,w,h,0,0,15,1,T.wallTop);
const obs = fillGrid(w,h,63);
borderFill(obs,w,h,0,0,15,10,T.wall);
// shelves: same product repeated 9 squares (C5)
for (let y=2;y<5;y++) for (let x=2;x<5;x++) set(obs,w,x,y,T.shelfFull);
for (let y=2;y<5;y++) for (let x=10;x<13;x++) set(obs,w,x,y,T.shelfFull);
// counter
rectFill(obs,w,h,6,6,3,1,T.counter);
set(ground,w,7,9,T.door);
const objects = [
obj('door_plaza','door',7,9,{target:'plaza',spawn:'door_shop'}),
obj('npc_mimi','spawn',7,7),
obj('shelf_clue','clue',2,2,{clue:'C5'}),
obj('count_shelf','interact',2,2), // T5
];
maps.shop = makeMap('shop', w, h, [
{name:'ground',type:'tilelayer',data:ground},
{name:'obstacles',type:'tilelayer',data:obs},
{name:'objects',type:'objectgroup',objects},
]);
}
// 5. Clinic (12x8)
{
const w=12,h=8;
const ground = fillGrid(w,h,T.floor);
rectFill(ground,w,h,0,0,12,1,T.wallTop);
const obs = fillGrid(w,h,63);
borderFill(obs,w,h,0,0,12,8,T.wall);
// doctor table with chart (C8)
rectFill(obs,w,h,8,4,3,1,T.table);
set(obs,w,9,3,T.chart);
// bed
set(obs,w,1,4,T.bed);
set(ground,w,5,7,T.door);
const objects = [
obj('door_plaza','door',5,7,{target:'plaza',spawn:'door_clinic'}),
obj('npc_dr_fu','spawn',5,5),
obj('chart_clue','clue',9,3,{clue:'C8'}),
];
maps.clinic = makeMap('clinic', w, h, [
{name:'ground',type:'tilelayer',data:ground},
{name:'obstacles',type:'tilelayer',data:obs},
{name:'objects',type:'objectgroup',objects},
]);
}
// 6. Lake (24x16)
{
const w=24,h=16;
const ground = fillGrid(w,h,T.grass);
// lake water body
rectFill(ground,w,h,2,2,10,8,T.water);
// path along bottom
rectFill(ground,w,h,0,14,24,2,T.path);
rectFill(ground,w,h,22,0,2,16,T.path); // right exit
// bench (grandpa)
set(ground,w,12,10,T.bench);
// picnic spot
set(ground,w,16,11,T.picnic);
set(ground,w,17,11,T.picnic);
const obs = fillGrid(w,h,63);
// trees border
for (let x=0;x<w;x++){ set(obs,w,x,0,T.tree);}
for (let x=0;x<w;x++){ set(obs,w,x,h-1,T.tree);}
for (let y=0;y<h;y++){ set(obs,w,0,y,T.tree);}
const objects = [
obj('door_plaza','door',23,8,{target:'plaza',spawn:'door_lake'}),
obj('npc_grandpa','spawn',13,10),
obj('picnic_point','interact',16,11), // T7
obj('lake_reflection','interact',7,7), // reflection show
];
maps.lake = makeMap('lake', w, h, [
{name:'ground',type:'tilelayer',data:ground},
{name:'obstacles',type:'tilelayer',data:obs},
{name:'objects',type:'objectgroup',objects},
]);
}
// 7. Flower field (20x14)
{
const w=20,h=14;
const ground = fillGrid(w,h,T.grass);
// berry bushes (T6)
set(ground,w,4,4,T.berry); set(ground,w,8,3,T.berry); set(ground,w,12,5,T.berry);
set(ground,w,6,8,T.berry); set(ground,w,14,9,T.berry);
// flowers
for (let i=0;i<w;i+=2) set(ground,w,i,11,T.flowersY);
rectFill(ground,w,h,0,0,2,14,T.path);
rectFill(ground,w,h,18,0,2,14,T.path);
const obs = fillGrid(w,h,63);
for (let x=0;x<w;x++){ set(obs,w,x,0,T.tree); set(obs,w,x,h-1,T.tree);}
for (let y=0;y<h;y++){ set(obs,w,0,y,T.tree); set(obs,w,w-1,y,T.tree);}
// remove border where exits are
set(obs,w,0,7,63); set(obs,w,1,7,63);
set(obs,w,18,7,63); set(obs,w,19,7,63);
const objects = [
obj('door_plaza','door',0,7,{target:'plaza',spawn:'door_field'}),
obj('berry1','interact',4,4),
obj('berry2','interact',8,3),
obj('berry3','interact',12,5),
obj('berry4','interact',6,8),
obj('berry5','interact',14,9),
obj('water_point','interact',2,11), // T2 watering here
];
maps.field = makeMap('field', w, h, [
{name:'ground',type:'tilelayer',data:ground},
{name:'obstacles',type:'tilelayer',data:obs},
{name:'objects',type:'objectgroup',objects},
]);
}
// 8. Well area (14x10)
{
const w=14,h=10;
const ground = fillGrid(w,h,T.grass);
rectFill(ground,w,h,0,8,14,2,T.path);
// well center
set(ground,w,6,4,T.well);
set(ground,w,7,4,T.well);
const obs = fillGrid(w,h,63);
for (let x=0;x<w;x++){ set(obs,w,x,0,T.tree);}
for (let x=0;x<w;x++){ set(obs,w,x,h-1,T.tree);}
for (let y=0;y<h;y++){ set(obs,w,0,y,T.tree); set(obs,w,w-1,y,T.tree);}
set(obs,w,0,8,63);
const objects = [
obj('door_plaza','door',0,8,{target:'plaza',spawn:'door_well'}),
obj('well','interact',6,4,{clue:'C7'}),
];
maps.well_area = makeMap('well_area', w, h, [
{name:'ground',type:'tilelayer',data:ground},
{name:'obstacles',type:'tilelayer',data:obs},
{name:'objects',type:'objectgroup',objects},
]);
}
// 9. Fog boundary (20x8)
{
const w=20,h=8;
const ground = fillGrid(w,h,T.grass);
// fog wall
rectFill(ground,w,h,10,0,4,8,T.fogThick);
rectFill(ground,w,h,14,0,2,8,T.fog);
rectFill(ground,w,h,0,7,20,1,T.path);
const obs = fillGrid(w,h,63);
for (let x=0;x<w;x++){ set(obs,w,x,0,T.tree);}
for (let y=0;y<h;y++){ set(obs,w,0,y,T.tree); set(obs,w,w-1,y,T.tree);}
set(obs,w,0,7,63);
const objects = [
obj('door_plaza','door',0,6,{target:'plaza',spawn:'door_fog'}),
obj('fog_wall','interact',12,4,{clue:'fog'}),
obj('fog_exit','interact',13,4,{clue:'E3_exit'}),
];
maps.fog_boundary = makeMap('fog_boundary', w, h, [
{name:'ground',type:'tilelayer',data:ground},
{name:'obstacles',type:'tilelayer',data:obs},
{name:'objects',type:'objectgroup',objects},
]);
}
// 10. White layer (corridor 20x6 + ward 10x8) combined as one map 20x14
{
const w=20,h=14;
const ground = fillGrid(w,h,T.whiteFloor);
// corridor top, ward bottom
rectFill(ground,w,h,0,0,20,6,T.whiteFloor);
rectFill(ground,w,h,5,7,10,7,T.whiteFloor);
rectFill(ground,w,h,0,7,5,7,T.whiteWall);
rectFill(ground,w,h,15,7,5,7,T.whiteWall);
const obs = fillGrid(w,h,63);
// corridor walls
for (let x=0;x<w;x++){ set(obs,w,x,0,T.whiteWall); set(obs,w,x,5,T.whiteWall);}
// ward walls
borderFill(obs,w,h,5,7,10,7,T.whiteWall);
set(obs,w,9,13,63); // door gap
// bed + monitor in ward
set(obs,w,6,8,T.whiteBed);
set(obs,w,13,8,T.whiteMonitor);
set(obs,w,9,10,T.whiteTable);
const objects = [
obj('exit','door',9,12,{target:'ending'}),
obj('patient_chart','interact',9,10,{clue:'E3_chart'}),
];
maps.white_layer = makeMap('white_layer', w, h, [
{name:'ground',type:'tilelayer',data:ground},
{name:'obstacles',type:'tilelayer',data:obs},
{name:'objects',type:'objectgroup',objects},
]);
}
for (const [name, map] of Object.entries(maps)) {
fs.writeFileSync(path.join(outDir, name + '.json'), JSON.stringify(map, null, 0));
console.log('Wrote ' + name + '.json');
}
console.log('Done: ' + Object.keys(maps).length + ' maps');