- Isometric canvas renderer (depth-sorted, FOV/fog, additive lighting) - 3 classes x 20 skills, 4 acts x 4 floors + boss lairs, torment I-X - Diablo-style loot: rarities, affix tiers, 14 legendaries, vendor, stash - Rogue camp with 6 NPCs: Charsi/Akara/Kashya/Cain/Gheed/storage - NPC quest chain (accept -> hunt -> turn in) with rewards & gating - Procedural WebAudio SFX + generative music, EN/VI localization - Saves, settings, waypoints, hardcore mode, PWA manifest - 93-assertion headless suite + browser E2E via CDP
514 lines
18 KiB
JavaScript
514 lines
18 KiB
JavaScript
/* ============================================================
|
|
* Diablo2D — world.js : procedural dungeon & town generation
|
|
* ============================================================ */
|
|
'use strict';
|
|
window.D2 = window.D2 || {};
|
|
(function (D2) {
|
|
|
|
const T = { VOID: 0, FLOOR: 1, WALL: 2 };
|
|
|
|
const THEMES = {
|
|
cathedral: {
|
|
gen: 'rooms', floorsPerRoom: 0.9,
|
|
floorCols: ['#4a3f38', '#52453c', '#453b34', '#57493e'],
|
|
wallTop: '#2c2622', wallFace: '#3a322c',
|
|
accent: '#7a5a3a', torch: '#ff9a4a', fog: '#0a0806', ambient: 0.16,
|
|
music: 'crypt',
|
|
},
|
|
catacombs: {
|
|
gen: 'rooms',
|
|
floorCols: ['#3a4048', '#414750', '#363c44', '#464c55'],
|
|
wallTop: '#23282e', wallFace: '#30363d',
|
|
accent: '#4a6a8a', torch: '#7ab8ff', fog: '#07090c', ambient: 0.12,
|
|
music: 'crypt',
|
|
},
|
|
caves: {
|
|
gen: 'caves',
|
|
floorCols: ['#3e4a34', '#46523a', '#38442e', '#4d5940'],
|
|
wallTop: '#242c20', wallFace: '#323e2a',
|
|
accent: '#5a8a3a', torch: '#b8e86a', fog: '#080a06', ambient: 0.14,
|
|
music: 'cave',
|
|
},
|
|
hell: {
|
|
gen: 'caves',
|
|
floorCols: ['#48302a', '#50362e', '#422a24', '#583a30'],
|
|
wallTop: '#281512', wallFace: '#38201a',
|
|
accent: '#c83a1a', torch: '#ff5a2a', fog: '#100604', ambient: 0.18,
|
|
music: 'hell',
|
|
},
|
|
town: {
|
|
gen: 'town',
|
|
floorCols: ['#55503e', '#5c5644', '#4e4938', '#635d49'],
|
|
wallTop: '#33291e', wallFace: '#453829',
|
|
accent: '#8a7a4a', torch: '#ffb84a', fog: '#0c0a08', ambient: 0.32,
|
|
music: 'town',
|
|
},
|
|
};
|
|
|
|
/* ---------------- helpers ---------------- */
|
|
|
|
function idx(w, x, y) { return y * w + x; }
|
|
|
|
function inBounds(w, h, x, y) { return x >= 0 && y >= 0 && x < w && y < h; }
|
|
|
|
/* ---------------- rooms & corridors generator ---------------- */
|
|
|
|
function genRooms(rng, w, h, roomAttempts) {
|
|
const tiles = new Uint8Array(w * h);
|
|
const rooms = [];
|
|
for (let i = 0; i < roomAttempts && rooms.length < 14; i++) {
|
|
const rw = rng.int(6, 13), rh = rng.int(5, 10);
|
|
const rx = rng.int(2, w - rw - 3), ry = rng.int(2, h - rh - 3);
|
|
const room = { x: rx, y: ry, w: rw, h: rh, cx: (rx + rw / 2) | 0, cy: (ry + rh / 2) | 0 };
|
|
if (rooms.some(r => overlap(room, r, 2))) continue;
|
|
rooms.push(room);
|
|
carveRect(tiles, w, room);
|
|
}
|
|
/* L corridors between consecutive rooms */
|
|
for (let i = 1; i < rooms.length; i++) {
|
|
const a = rooms[i - 1], b = rooms[i];
|
|
carveL(tiles, w, a.cx, a.cy, b.cx, b.cy);
|
|
}
|
|
/* a couple of loops */
|
|
for (let k = 0; k < 3 && rooms.length > 4; k++) {
|
|
const a = rng.pick(rooms), b = rng.pick(rooms);
|
|
if (a !== b) carveL(tiles, w, a.cx, a.cy, b.cx, b.cy);
|
|
}
|
|
return { tiles, rooms };
|
|
}
|
|
|
|
function overlap(a, b, pad) {
|
|
return a.x - pad < b.x + b.w && a.x + a.w + pad > b.x &&
|
|
a.y - pad < b.y + b.h && a.y + a.h + pad > b.y;
|
|
}
|
|
|
|
function carveRect(tiles, w, r) {
|
|
for (let y = r.y; y < r.y + r.h; y++)
|
|
for (let x = r.x; x < r.x + r.w; x++)
|
|
tiles[idx(w, x, y)] = T.FLOOR;
|
|
}
|
|
|
|
function carveL(tiles, w, x0, y0, x1, y1) {
|
|
let x = x0, y = y0;
|
|
const horizFirst = Math.random() < 0.5;
|
|
const step = () => { tiles[idx(w, x, y)] = T.FLOOR; };
|
|
step();
|
|
if (horizFirst) {
|
|
while (x !== x1) { x += Math.sign(x1 - x); step(); }
|
|
while (y !== y1) { y += Math.sign(y1 - y); step(); }
|
|
} else {
|
|
while (y !== y1) { y += Math.sign(y1 - y); step(); }
|
|
while (x !== x1) { x += Math.sign(x1 - x); step(); }
|
|
}
|
|
}
|
|
|
|
/* ---------------- cellular cave generator ---------------- */
|
|
|
|
function genCaves(rng, w, h) {
|
|
const tiles = new Uint8Array(w * h);
|
|
const wallP = 0.44;
|
|
for (let y = 0; y < h; y++)
|
|
for (let x = 0; x < w; x++)
|
|
tiles[idx(w, x, y)] = (x === 0 || y === 0 || x === w - 1 || y === h - 1 || rng.next() < wallP) ? T.WALL : T.FLOOR;
|
|
|
|
for (let pass = 0; pass < 4; pass++) {
|
|
const next = Uint8Array.from(tiles);
|
|
for (let y = 1; y < h - 1; y++) {
|
|
for (let x = 1; x < w - 1; x++) {
|
|
let walls = 0;
|
|
for (let dy = -1; dy <= 1; dy++)
|
|
for (let dx = -1; dx <= 1; dx++) {
|
|
if (!dx && !dy) continue;
|
|
if (tiles[idx(w, x + dx, y + dy)] !== T.FLOOR) walls++;
|
|
}
|
|
next[idx(w, x, y)] = walls >= 5 ? T.WALL : T.FLOOR;
|
|
}
|
|
}
|
|
tiles.set(next);
|
|
}
|
|
|
|
/* keep only the largest connected region */
|
|
const seen = new Uint8Array(w * h);
|
|
let best = null;
|
|
for (let y = 0; y < h; y++) {
|
|
for (let x = 0; x < w; x++) {
|
|
const i0 = idx(w, x, y);
|
|
if (tiles[i0] !== T.FLOOR || seen[i0]) continue;
|
|
const region = [];
|
|
const stack = [[x, y]];
|
|
seen[i0] = 1;
|
|
while (stack.length) {
|
|
const [cx, cy] = stack.pop();
|
|
region.push([cx, cy]);
|
|
for (const [dx, dy] of [[1,0],[-1,0],[0,1],[0,-1]]) {
|
|
const nx = cx + dx, ny = cy + dy;
|
|
if (!inBounds(w, h, nx, ny)) continue;
|
|
const ni = idx(w, nx, ny);
|
|
if (!seen[ni] && tiles[ni] === T.FLOOR) { seen[ni] = 1; stack.push([nx, ny]); }
|
|
}
|
|
}
|
|
if (!best || region.length > best.length) best = region;
|
|
}
|
|
}
|
|
const keep = new Set(best.map(([x, y]) => idx(w, x, y)));
|
|
for (let i = 0; i < tiles.length; i++)
|
|
if (tiles[i] === T.FLOOR && !keep.has(i)) tiles[i] = T.VOID;
|
|
|
|
/* pseudo-rooms = flood-fill pockets for prop/spawn logic */
|
|
const rooms = [];
|
|
const grid = {};
|
|
for (const [x, y] of best) grid[x + ',' + y] = true;
|
|
// sample room anchors on a coarse grid where floor exists
|
|
for (let gy = 6; gy < h - 6; gy += 9) {
|
|
for (let gx = 6; gx < w - 6; gx += 9) {
|
|
if (grid[gx + ',' + gy]) rooms.push({ x: gx - 3, y: gy - 3, w: 7, h: 7, cx: gx, cy: gy });
|
|
}
|
|
}
|
|
return { tiles, rooms };
|
|
}
|
|
|
|
/* ---------------- walls around floors ---------------- */
|
|
|
|
function buildWalls(tiles, w, h) {
|
|
const out = Uint8Array.from(tiles);
|
|
for (let y = 0; y < h; y++) {
|
|
for (let x = 0; x < w; x++) {
|
|
if (tiles[idx(w, x, y)] !== T.VOID) continue;
|
|
let touchesFloor = false;
|
|
for (let dy = -1; dy <= 1 && !touchesFloor; dy++)
|
|
for (let dx = -1; dx <= 1; dx++) {
|
|
const nx = x + dx, ny = y + dy;
|
|
if (inBounds(w, h, nx, ny) && tiles[idx(w, nx, ny)] === T.FLOOR) { touchesFloor = true; break; }
|
|
}
|
|
if (touchesFloor) out[idx(w, x, y)] = T.WALL;
|
|
}
|
|
}
|
|
return out;
|
|
}
|
|
|
|
/* ---------------- floor assembly ---------------- */
|
|
|
|
/**
|
|
* opts: { act, floorIdx, seed, bossLair, torment }
|
|
* Returns the World object used by simulation & rendering.
|
|
*/
|
|
function generateFloor(opts) {
|
|
const act = opts.act | 0;
|
|
const floorIdx = opts.floorIdx | 0;
|
|
const themeId = D2.BAL.acts[act].theme;
|
|
const theme = THEMES[themeId];
|
|
const rng = new D2.util.RNG(opts.seed >>> 0);
|
|
const isBoss = !!opts.bossLair;
|
|
|
|
const w = isBoss ? 52 : 62, h = isBoss ? 38 : 62;
|
|
let tiles, rooms;
|
|
|
|
if (isBoss) {
|
|
tiles = new Uint8Array(w * h);
|
|
const m = 4;
|
|
const room = { x: m, y: m, w: w - m * 2, h: h - m * 2 };
|
|
carveRect(tiles, w, room);
|
|
rooms = [room];
|
|
/* pillars for cover */
|
|
for (let py = room.y + 4; py < room.y + room.h - 3; py += 6) {
|
|
for (let px = room.x + 5; px < room.x + room.w - 4; px += 8) {
|
|
tiles[idx(w, px, py)] = T.WALL;
|
|
tiles[idx(w, px + 1, py)] = T.WALL;
|
|
}
|
|
}
|
|
} else if (theme.gen === 'rooms') {
|
|
({ tiles, rooms } = genRooms(rng, w, h, 90));
|
|
} else {
|
|
({ tiles, rooms } = genCaves(rng, w, h));
|
|
}
|
|
|
|
const finalTiles = buildWalls(tiles, w, h);
|
|
|
|
/* variants for art */
|
|
const variant = new Uint8Array(w * h);
|
|
for (let i = 0; i < variant.length; i++) variant[i] = (rng.next() * 4) | 0;
|
|
|
|
const world = {
|
|
w, h,
|
|
tiles: finalTiles,
|
|
variant,
|
|
theme, themeId, act, floorIdx,
|
|
isBossLair: isBoss,
|
|
seed: opts.seed >>> 0,
|
|
torment: opts.torment | 0,
|
|
rooms,
|
|
props: [],
|
|
propMap: new Map(), // "x,y" -> blocking prop
|
|
spawns: [],
|
|
visible: new Uint8Array(w * h),
|
|
explored: new Uint8Array(w * h),
|
|
torches: [], // light sources {x,y,color,r,flicker}
|
|
spawnX: 0, spawnY: 0,
|
|
stairsDown: null, stairsUp: null,
|
|
mlvl: D2.BAL.monsterLevel(act, floorIdx, opts.torment | 0),
|
|
|
|
tileAt(x, y) { return inBounds(w, h, x, y) ? finalTiles[idx(w, x, y)] : T.VOID; },
|
|
isWalkable(x, y) {
|
|
if (this.tileAt(Math.floor(x), Math.floor(y)) !== T.FLOOR) return false;
|
|
return !this.propMap.has(Math.floor(x) + ',' + Math.floor(y));
|
|
},
|
|
transparent(x, y) { return this.tileAt(x, y) === T.FLOOR; },
|
|
randomFloorIn(room) {
|
|
for (let tries = 0; tries < 30; tries++) {
|
|
const x = rng.int(room.x, room.x + room.w - 1);
|
|
const y = rng.int(room.y, room.y + room.h - 1);
|
|
if (finalTiles[idx(w, x, y)] === T.FLOOR && !this.propMap.has(x + ',' + y)) return { x, y };
|
|
}
|
|
return null;
|
|
},
|
|
addProp(p) {
|
|
p.uid = D2.util.uid();
|
|
this.props.push(p);
|
|
if (p.blocking) this.propMap.set(p.x + ',' + p.y, p);
|
|
if (p.type === 'torch') this.torches.push({ x: p.x, y: p.y });
|
|
return p;
|
|
},
|
|
removeProp(p) {
|
|
const i = this.props.indexOf(p);
|
|
if (i >= 0) this.props.splice(i, 1);
|
|
this.propMap.delete(p.x + ',' + p.y);
|
|
},
|
|
};
|
|
|
|
/* ---- spawn point = first room center ---- */
|
|
const startRoom = rooms[0];
|
|
world.spawnX = startRoom.cx; world.spawnY = startRoom.cy;
|
|
|
|
/* ---- stairs ---- */
|
|
if (!isBoss) {
|
|
const lastRoom = farthestRoom(world, rooms, startRoom);
|
|
const sp = world.randomFloorIn(lastRoom) || { x: lastRoom.cx, y: lastRoom.cy };
|
|
world.stairsDown = { x: sp.x, y: sp.y };
|
|
world.addProp({ type: 'stairs_down', x: sp.x, y: sp.y, blocking: false });
|
|
} else {
|
|
world.stairsUp = { x: startRoom.cx, y: startRoom.cy };
|
|
world.addProp({ type: 'stairs_up', x: startRoom.cx, y: startRoom.cy, blocking: false });
|
|
}
|
|
|
|
populateProps(world, rng, theme);
|
|
populateMonsters(world, rng, act, isBoss);
|
|
ensureRouteToStairs(world);
|
|
|
|
return world;
|
|
}
|
|
|
|
/* Remove blocking props along one spawn→stairs route so caves
|
|
with 1-wide tunnels can never be sealed by barrels. */
|
|
function ensureRouteToStairs(world) {
|
|
if (!world.stairsDown || world.isTown) return;
|
|
const tilesPath = D2.path.find(
|
|
(x, y) => world.transparent(x, y),
|
|
world.w, world.h,
|
|
world.spawnX, world.spawnY,
|
|
world.stairsDown.x, world.stairsDown.y, 30000);
|
|
if (!tilesPath) return;
|
|
for (const n of tilesPath) {
|
|
const pr = world.propMap.get(n.x + ',' + n.y);
|
|
if (pr && pr.blocking) world.removeProp(pr);
|
|
}
|
|
}
|
|
|
|
function farthestRoom(world, rooms, from) {
|
|
let best = rooms[rooms.length - 1], bestD = -1;
|
|
for (const r of rooms) {
|
|
const d = (r.cx - from.cx) ** 2 + (r.cy - from.cy) ** 2;
|
|
if (d > bestD) { bestD = d; best = r; }
|
|
}
|
|
return best;
|
|
}
|
|
|
|
/* ---------------- props ---------------- */
|
|
|
|
function populateProps(world, rng, theme) {
|
|
const { w, h } = world;
|
|
const at = (x, y) => world.tileAt(x, y) === T.FLOOR && !world.propMap.has(x + ',' + y);
|
|
|
|
/* torches along walls */
|
|
const torchSpots = [];
|
|
for (let y = 1; y < h - 1; y++)
|
|
for (let x = 1; x < w - 1; x++)
|
|
if (world.tiles[idx(w, x, y)] === T.WALL &&
|
|
world.tileAt(x, y + 1) === T.FLOOR)
|
|
torchSpots.push([x, y]);
|
|
rng.shuffle(torchSpots);
|
|
const torchCount = Math.min(18, torchSpots.length);
|
|
for (let i = 0; i < torchCount; i++) {
|
|
const [x, y] = torchSpots[i];
|
|
world.addProp({ type: 'torch', x, y: y, blocking: false, light: true });
|
|
}
|
|
|
|
if (world.isBossLair) {
|
|
/* a chest behind the boss area */
|
|
return;
|
|
}
|
|
|
|
for (const room of world.rooms.slice(1)) {
|
|
/* barrels & urns */
|
|
const clusterN = rng.int(0, 3);
|
|
for (let c = 0; c < clusterN; c++) {
|
|
const bx = rng.int(room.x + 1, room.x + room.w - 2);
|
|
const by = rng.int(room.y + 1, room.y + room.h - 2);
|
|
const n = rng.int(1, 3);
|
|
for (let i = 0; i < n; i++) {
|
|
const x = Math.min(room.x + room.w - 1, bx + i % 2), y = by;
|
|
if (at(x, y)) world.addProp({
|
|
type: rng.chance(0.5) ? 'barrel' : 'urn',
|
|
x, y, blocking: true, hp: 1,
|
|
});
|
|
}
|
|
}
|
|
/* chest */
|
|
if (rng.chance(0.3)) {
|
|
const spot = world.randomFloorIn(room);
|
|
if (spot) world.addProp({ type: 'chest', x: spot.x, y: spot.y, blocking: true, opened: false });
|
|
}
|
|
/* shrine (rare) */
|
|
if (rng.chance(0.05)) {
|
|
const spot = world.randomFloorIn(room);
|
|
if (spot) world.addProp({
|
|
type: 'shrine', x: spot.x, y: spot.y, blocking: false, used: false,
|
|
buff: rng.pick(['dmg', 'speed', 'armor', 'xp', 'regen']),
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
/* ---------------- monsters ---------------- */
|
|
|
|
function populateMonsters(world, rng, act, isBoss) {
|
|
if (isBoss) {
|
|
const bd = D2.Monsters.BOSS_MAP[D2.BAL.acts[act].boss];
|
|
world.spawns.push({ boss: bd.id, x: world.w / 2, y: world.h * 0.68 });
|
|
/* honor guard */
|
|
const guard = D2.Monsters.rollSpeciesForAct(act);
|
|
for (let i = 0; i < 4; i++) {
|
|
world.spawns.push({ speciesId: guard.id, x: world.w / 2 + (rng.range(-5, 5)), y: world.h * 0.55 + rng.range(-2, 2), elite: false });
|
|
}
|
|
return;
|
|
}
|
|
|
|
const density = 0.55 + world.act * 0.12 + world.floorIdx * 0.05;
|
|
for (const room of world.rooms.slice(1)) {
|
|
if (world.stairsDown && room === nearestRoomToStairs(world)) continue;
|
|
const packs = Math.max(1, Math.round((room.w * room.h) / 55 * density));
|
|
for (let p = 0; p < packs; p++) {
|
|
const species = D2.Monsters.rollSpeciesForAct(act);
|
|
const packSize = species.ai === 'swarm' ? rng.int(3, 5) : rng.int(2, 4);
|
|
const packElite = rng.chance(0.13);
|
|
const base = world.randomFloorIn(room);
|
|
if (!base) continue;
|
|
for (let i = 0; i < packSize; i++) {
|
|
const sx = Math.max(1, Math.min(world.w - 2, base.x + ((rng.next() * 5) | 0) - 2));
|
|
const sy = Math.max(1, Math.min(world.h - 2, base.y + ((rng.next() * 5) | 0) - 2));
|
|
if (world.tileAt(sx, sy) === T.FLOOR)
|
|
world.spawns.push({ speciesId: species.id, x: sx, y: sy, elite: packElite });
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
function nearestRoomToStairs(world) {
|
|
if (!world.rooms.length || !world.stairsDown) return null;
|
|
let best = null, bd = Infinity;
|
|
for (const r of world.rooms) {
|
|
const d = (r.cx - world.stairsDown.x) ** 2 + (r.cy - world.stairsDown.y) ** 2;
|
|
if (d < bd) { bd = d; best = r; }
|
|
}
|
|
return best;
|
|
}
|
|
|
|
/* ---------------- town ---------------- */
|
|
|
|
function generateTown(seed) {
|
|
const w = 56, h = 40;
|
|
const rng = new D2.util.RNG((seed ^ 0x7A6E) >>> 0);
|
|
const theme = THEMES.town;
|
|
const tiles = new Uint8Array(w * h).fill(T.VOID);
|
|
|
|
const border = 2;
|
|
for (let y = border; y < h - border; y++)
|
|
for (let x = border; x < w - border; x++)
|
|
tiles[idx(w, x, y)] = T.FLOOR;
|
|
|
|
const world = {
|
|
w, h, tiles, variant: new Uint8Array(w * h),
|
|
theme, themeId: 'town', act: -1, floorIdx: -1,
|
|
isBossLair: false, isTown: true, seed: seed >>> 0, torment: 0,
|
|
rooms: [], props: [], propMap: new Map(),
|
|
spawns: [], visible: new Uint8Array(w * h), explored: new Uint8Array(w * h),
|
|
torches: [], spawnX: (w / 2) | 0, spawnY: (h * 0.62) | 0,
|
|
stairsDown: null, stairsUp: null, mlvl: 1,
|
|
|
|
tileAt(x, y) { return inBounds(w, h, x, y) ? tiles[idx(w, x, y)] : T.VOID; },
|
|
isWalkable(x, y) {
|
|
if (this.tileAt(Math.floor(x), Math.floor(y)) !== T.FLOOR) return false;
|
|
return !this.propMap.has(Math.floor(x) + ',' + Math.floor(y));
|
|
},
|
|
transparent(x, y) { return this.tileAt(x, y) === T.FLOOR; },
|
|
randomFloorIn(room) { return null; },
|
|
addProp(p) {
|
|
p.uid = D2.util.uid();
|
|
this.props.push(p);
|
|
if (p.blocking) this.propMap.set(p.x + ',' + p.y, p);
|
|
if (p.type === 'torch') this.torches.push({ x: p.x, y: p.y });
|
|
return p;
|
|
},
|
|
removeProp(p) {
|
|
const i = this.props.indexOf(p);
|
|
if (i >= 0) this.props.splice(i, 1);
|
|
this.propMap.delete(p.x + ',' + p.y);
|
|
},
|
|
};
|
|
|
|
/* Rogue-camp buildings along the top (charsi/akara/stash/kashya/cain/gheed) */
|
|
const bw = 7, bh = 6;
|
|
const buildings = [
|
|
{ x: 3, npc: 'charsi' }, // blacksmith
|
|
{ x: 11, npc: 'akara' }, // healer & magic
|
|
{ x: 19, npc: 'stash' }, // camp storage
|
|
{ x: 27, npc: 'kashya' }, // rogue captain — hunt quests
|
|
{ x: 35, npc: 'cain' }, // elder lore — main quests
|
|
{ x: 43, npc: 'gheed' }, // gambler
|
|
].map(b => ({ ...b, w: bw, h: bh, y: 4 }));
|
|
for (const b of buildings) {
|
|
for (let y = b.y; y < b.y + b.h; y++)
|
|
for (let x = b.x; x < b.x + b.w; x++)
|
|
tiles[idx(w, x, y)] = T.WALL;
|
|
/* doorway at bottom center */
|
|
const doorX = b.x + (b.w >> 1);
|
|
tiles[idx(w, doorX, b.y + b.h - 1)] = T.FLOOR;
|
|
tiles[idx(w, doorX, b.y + b.h - 2)] = T.FLOOR;
|
|
world.addProp({ type: 'npc_' + b.npc, x: doorX, y: b.y + b.h - 4, blocking: true });
|
|
world.addProp({ type: 'torch', x: b.x - 1, y: b.y + b.h, blocking: false, light: true });
|
|
world.addProp({ type: 'torch', x: b.x + b.w, y: b.y + b.h, blocking: false, light: true });
|
|
}
|
|
|
|
/* waypoint stone center */
|
|
world.addProp({ type: 'waypoint', x: (w / 2) | 0, y: (h * 0.38) | 0, blocking: true });
|
|
|
|
/* decorative props */
|
|
for (let i = 0; i < 10; i++) {
|
|
const x = rng.int(border + 1, w - border - 2), y = rng.int(h * 0.55 | 0, h - border - 2);
|
|
if (world.tileAt(x, y) === T.FLOOR && !world.propMap.has(x + ',' + y))
|
|
world.addProp({ type: rng.chance(0.5) ? 'barrel' : 'bones', x, y, blocking: rng.chance(0.5), hp: 1 });
|
|
}
|
|
/* fence posts around perimeter */
|
|
for (let x = border; x < w - border; x += 3) {
|
|
world.addProp({ type: 'bones', x, y: h - border, blocking: false });
|
|
world.addProp({ type: 'bones', x, y: border - 1 >= 0 ? border : border, blocking: false });
|
|
}
|
|
|
|
/* explored everywhere in town */
|
|
world.explored.fill(1);
|
|
|
|
return world;
|
|
}
|
|
|
|
D2.world = { T, THEMES, generateFloor, generateTown };
|
|
})(window.D2);
|