tiny-army / web /mapSandbox.js
polats's picture
Game: hero creation + selection flow; pin the Space
3ef6bd6
// ../auto-battler/src/render/chunkedMap.js
function createChunkedMap(pixi, host, config) {
const { Application, Assets, Sprite, Container, Texture, Rectangle, RenderTexture } = pixi;
const TILE6 = config.tile ?? 8;
const CHUNK5 = config.chunk ?? 32;
const CHUNKPX = CHUNK5 * TILE6;
const BG = config.background ?? "#69636f";
const Z_DEFAULT = config.zoomDefault ?? 1;
const Z_MIN = config.zoomMin ?? 1 / 32;
const Z_MAX = config.zoomMax ?? 6;
const Z_STEP = config.zoomStep ?? 1.15;
const Z_DETAIL = config.zDetail ?? 0.9;
const Z_MACRO = config.zMacro ?? 0.5;
const MCHUNK = config.macroChunkTexels ?? 32;
const MACRO_TEXEL_PX = config.macroTexelPx ?? 2.5;
const MACRO_LEVEL_MAX = config.macroLevelMax ?? 6;
const DETAIL_BUDGET = config.detailBudget ?? 1;
const MACRO_BUDGET = config.macroBudget ?? 8;
const bounds = config.bounds ? {
x0: config.bounds.x0 * TILE6,
y0: config.bounds.y0 * TILE6,
x1: config.bounds.x1 * TILE6,
y1: config.bounds.y1 * TILE6,
tx0: config.bounds.x0,
ty0: config.bounds.y0,
tx1: config.bounds.x1,
ty1: config.bounds.y1
} : null;
let app = null, root = null, genRoot = null, macroRoot = null, shadowLayer = null, propLayer = null;
let ctx = null;
let alive = true;
let enabled = true;
let seed = config.seed ?? 1;
let zoom = Z_DEFAULT;
let cameraDirty = true, genPending = false, lastEmitTile = null;
let flyAnim = null;
const camera = { x: config.initialCamera?.x ?? CHUNKPX / 2, y: config.initialCamera?.y ?? CHUNKPX / 2 };
const chunks = /* @__PURE__ */ new Map();
const macroChunks = /* @__PURE__ */ new Map();
const fading = /* @__PURE__ */ new Set();
const keys = /* @__PURE__ */ new Set();
const texCache = /* @__PURE__ */ new Map();
const listeners = /* @__PURE__ */ new Set();
const tickHooks = /* @__PURE__ */ new Set();
const drag = { on: false, px: 0, py: 0 };
const pointers = /* @__PURE__ */ new Map();
const pinch = { on: false, dist: 0 };
const handlers = {};
const emit = () => listeners.forEach((fn) => fn(getSnapshot()));
const getSnapshot = () => ({ seed, zoom, cx: Math.round(camera.x / TILE6), cy: Math.round(camera.y / TILE6), chunks: chunks.size });
function tex(source, c, r) {
const k = source.uid + ":" + c + "," + r;
let t = texCache.get(k);
if (!t) {
t = new Texture({ source, frame: new Rectangle(c * TILE6, r * TILE6, TILE6, TILE6) });
texCache.set(k, t);
}
return t;
}
function texFrame(source, x, y, w, h2) {
const k = source.uid + ":f" + x + "," + y + "," + w + "," + h2;
let t = texCache.get(k);
if (!t) {
t = new Texture({ source, frame: new Rectangle(x, y, w, h2) });
texCache.set(k, t);
}
return t;
}
function makeChunk(cx, cy) {
const x0 = cx * CHUNK5, y0 = cy * CHUNK5;
const tmp = new Container();
const add = (source, c, r, tx, ty) => {
const sp = new Sprite(tex(source, c, r));
sp.x = tx * TILE6;
sp.y = ty * TILE6;
tmp.addChild(sp);
return sp;
};
const res = config.bake({ cx, cy, x0, y0, seed, chunk: CHUNK5, tile: TILE6, ctx, tmp, app, Sprite, Texture, Rectangle, tex, texFrame, add }) || {};
const rt = RenderTexture.create({ width: CHUNKPX, height: CHUNKPX, autoGenerateMipmaps: true, scaleMode: "nearest" });
rt.source.minFilter = "linear";
rt.source.mipmapFilter = "linear";
app.renderer.render({ container: tmp, target: rt });
rt.source.updateMipmaps();
tmp.destroy({ children: true });
const sprite = new Sprite(rt);
sprite.x = x0 * TILE6;
sprite.y = y0 * TILE6;
return { sprite, rt, meta: res.meta ?? null, live: res.live ?? null };
}
function chooseMacroLevel(z) {
return Math.max(0, Math.min(MACRO_LEVEL_MAX, Math.round(Math.log2(MACRO_TEXEL_PX / (TILE6 * z)))));
}
function makeMacroChunk(L, mcx, mcy) {
const step = 1 << L, t0x = mcx * MCHUNK * step, t0y = mcy * MCHUNK * step;
const cv = document.createElement("canvas");
cv.width = MCHUNK;
cv.height = MCHUNK;
const g = cv.getContext("2d");
const img = g.createImageData(MCHUNK, MCHUNK), d = img.data;
for (let j = 0; j < MCHUNK; j++) for (let i = 0; i < MCHUNK; i++) {
const [r, gg, b] = config.macroColor(seed, t0x + i * step + (step >> 1), t0y + j * step + (step >> 1));
const o = (j * MCHUNK + i) * 4;
d[o] = r;
d[o + 1] = gg;
d[o + 2] = b;
d[o + 3] = 255;
}
g.putImageData(img, 0, 0);
const t = Texture.from(cv);
t.source.scaleMode = "linear";
const sprite = new Sprite(t);
sprite.x = t0x * TILE6;
sprite.y = t0y * TILE6;
sprite.width = sprite.height = MCHUNK * step * TILE6;
return { sprite, tex: t };
}
function coverZoom() {
if (!bounds || !app) return Z_MIN;
return Math.max(app.screen.width / (bounds.x1 - bounds.x0), app.screen.height / (bounds.y1 - bounds.y0));
}
function clampCamera() {
if (!bounds || !app) return;
const hw = app.screen.width / 2 / zoom, hh = app.screen.height / 2 / zoom;
const loX = bounds.x0 + hw, hiX = bounds.x1 - hw, loY = bounds.y0 + hh, hiY = bounds.y1 - hh;
camera.x = loX <= hiX ? Math.min(hiX, Math.max(loX, camera.x)) : (bounds.x0 + bounds.x1) / 2;
camera.y = loY <= hiY ? Math.min(hiY, Math.max(loY, camera.y)) : (bounds.y0 + bounds.y1) / 2;
}
function reconcile() {
if (!genRoot || !ctx || !app) return;
if (bounds) zoom = Math.max(zoom, coverZoom());
clampCamera();
const sx = Math.round(app.screen.width / 2 - camera.x * zoom);
const sy = Math.round(app.screen.height / 2 - camera.y * zoom);
for (const L of [macroRoot, genRoot, shadowLayer, propLayer]) {
L.scale.set(zoom);
L.x = sx;
L.y = sy;
}
const detailActive = zoom > Z_MACRO;
const t = Math.max(0, Math.min(1, (zoom - Z_MACRO) / (Z_DETAIL - Z_MACRO)));
genRoot.visible = shadowLayer.visible = propLayer.visible = detailActive;
genRoot.alpha = shadowLayer.alpha = propLayer.alpha = t;
macroRoot.visible = true;
macroRoot.alpha = 1;
let pending = false;
if (detailActive) pending = reconcileDetail() || pending;
else clearChunks();
pending = reconcileMacro(chooseMacroLevel(zoom)) || pending;
genPending = pending;
}
function evictChunk(key, ch) {
fading.delete(ch.sprite);
genRoot.removeChild(ch.sprite);
ch.sprite.destroy();
ch.rt.destroy(true);
if (ch.live) for (const l of ch.live) {
propLayer.removeChild(l.sprite);
l.sprite.destroy();
if (l.shadow) {
shadowLayer.removeChild(l.shadow);
l.shadow.destroy();
}
}
chunks.delete(key);
}
function reconcileDetail() {
const halfW = app.screen.width / 2 / zoom, halfH = app.screen.height / 2 / zoom;
let c0 = Math.floor((camera.x - halfW) / CHUNKPX) - 1, c1 = Math.floor((camera.x + halfW) / CHUNKPX) + 1;
let r0 = Math.floor((camera.y - halfH) / CHUNKPX) - 1, r1 = Math.floor((camera.y + halfH) / CHUNKPX) + 1;
if (bounds) {
c0 = Math.max(c0, Math.floor(bounds.tx0 / CHUNK5));
c1 = Math.min(c1, Math.floor((bounds.tx1 - 1) / CHUNK5));
r0 = Math.max(r0, Math.floor(bounds.ty0 / CHUNK5));
r1 = Math.min(r1, Math.floor((bounds.ty1 - 1) / CHUNK5));
}
for (const [key, ch] of chunks) {
const [cx, cy] = key.split(",").map(Number);
if (cx < c0 - 1 || cx > c1 + 1 || cy < r0 - 1 || cy > r1 + 1) evictChunk(key, ch);
}
const ccx = camera.x / CHUNKPX, ccy = camera.y / CHUNKPX, missing = [];
for (let cy = r0; cy <= r1; cy++) for (let cx = c0; cx <= c1; cx++) {
const key = cx + "," + cy;
if (!chunks.has(key)) missing.push({ cx, cy, key, d: (cx + 0.5 - ccx) ** 2 + (cy + 0.5 - ccy) ** 2 });
}
missing.sort((a, b) => a.d - b.d);
for (let i = 0; i < missing.length && i < DETAIL_BUDGET; i++) {
const { cx, cy, key } = missing[i];
const ch = makeChunk(cx, cy);
chunks.set(key, ch);
genRoot.addChild(ch.sprite);
ch.sprite.alpha = 0;
fading.add(ch.sprite);
if (ch.live) for (const l of ch.live) {
if (l.shadow) shadowLayer.addChild(l.shadow);
propLayer.addChild(l.sprite);
}
}
return missing.length > DETAIL_BUDGET;
}
function reconcileMacro(L) {
const px = MCHUNK * (1 << L) * TILE6;
const halfW = app.screen.width / 2 / zoom, halfH = app.screen.height / 2 / zoom;
let c0 = Math.floor((camera.x - halfW) / px) - 1, c1 = Math.floor((camera.x + halfW) / px) + 1;
let r0 = Math.floor((camera.y - halfH) / px) - 1, r1 = Math.floor((camera.y + halfH) / px) + 1;
if (bounds) {
const mt = MCHUNK * (1 << L);
c0 = Math.max(c0, Math.floor(bounds.tx0 / mt));
c1 = Math.min(c1, Math.floor((bounds.tx1 - 1) / mt));
r0 = Math.max(r0, Math.floor(bounds.ty0 / mt));
r1 = Math.min(r1, Math.floor((bounds.ty1 - 1) / mt));
}
for (const [key, mc] of macroChunks) {
const [ml, mx, my] = key.split(",").map(Number);
if (ml !== L || mx < c0 - 1 || mx > c1 + 1 || my < r0 - 1 || my > r1 + 1) {
macroRoot.removeChild(mc.sprite);
mc.sprite.destroy();
mc.tex.destroy(true);
macroChunks.delete(key);
}
}
const ccx = camera.x / px, ccy = camera.y / px, missing = [];
for (let my = r0; my <= r1; my++) for (let mx = c0; mx <= c1; mx++) {
const key = L + "," + mx + "," + my;
if (!macroChunks.has(key)) missing.push({ mx, my, key, d: (mx + 0.5 - ccx) ** 2 + (my + 0.5 - ccy) ** 2 });
}
missing.sort((a, b) => a.d - b.d);
for (let i = 0; i < missing.length && i < MACRO_BUDGET; i++) {
const { mx, my, key } = missing[i];
const mc = makeMacroChunk(L, mx, my);
macroChunks.set(key, mc);
macroRoot.addChild(mc.sprite);
}
return missing.length > MACRO_BUDGET;
}
function clearChunks() {
for (const [key, ch] of chunks) evictChunk(key, ch);
}
function clearMacro() {
for (const [, mc] of macroChunks) {
macroRoot?.removeChild(mc.sprite);
mc.sprite.destroy();
mc.tex.destroy(true);
}
macroChunks.clear();
}
function zoomAt(factor, lx, ly) {
if (!app) return;
const nz = Math.min(Z_MAX, Math.max(bounds ? coverZoom() : Z_MIN, zoom * factor));
if (nz === zoom) return;
const sw = app.screen.width, sh = app.screen.height;
const wx = camera.x + (lx - sw / 2) / zoom, wy = camera.y + (ly - sh / 2) / zoom;
zoom = nz;
camera.x = wx - (lx - sw / 2) / zoom;
camera.y = wy - (ly - sh / 2) / zoom;
cameraDirty = true;
}
function zoomBy(factor) {
if (app) zoomAt(factor, app.screen.width / 2, app.screen.height / 2);
}
function flyTo(wx, wy, z, ms = 800) {
return new Promise((resolve) => {
if (!app) {
resolve();
return;
}
if (flyAnim) {
const r = flyAnim.resolve;
flyAnim = null;
r && r();
}
const toZ = Math.min(Z_MAX, Math.max(bounds ? coverZoom() : Z_MIN, z));
flyAnim = { fromX: camera.x, fromY: camera.y, fromZ: zoom, toX: wx, toY: wy, toZ, t: 0, dur: Math.max(1, ms), resolve };
});
}
function bindInput() {
const canvas = app.canvas;
const local = (cx, cy) => {
const r = canvas.getBoundingClientRect();
return [cx - r.left, cy - r.top];
};
const two = () => {
const it = pointers.values();
return [it.next().value, it.next().value];
};
handlers.down = (e) => {
if (!enabled || flyAnim) return;
pointers.set(e.pointerId, { x: e.clientX, y: e.clientY });
canvas.setPointerCapture?.(e.pointerId);
if (pointers.size === 1) {
drag.on = true;
drag.px = e.clientX;
drag.py = e.clientY;
} else if (pointers.size === 2) {
const [a, b] = two();
pinch.on = true;
pinch.dist = Math.hypot(a.x - b.x, a.y - b.y);
drag.on = false;
}
};
handlers.move = (e) => {
if (!pointers.has(e.pointerId)) return;
pointers.set(e.pointerId, { x: e.clientX, y: e.clientY });
if (pinch.on && pointers.size >= 2) {
const [a, b] = two(), d = Math.hypot(a.x - b.x, a.y - b.y);
if (pinch.dist > 0) zoomAt(d / pinch.dist, ...local((a.x + b.x) / 2, (a.y + b.y) / 2));
pinch.dist = d;
} else if (drag.on) {
camera.x -= (e.clientX - drag.px) / zoom;
camera.y -= (e.clientY - drag.py) / zoom;
drag.px = e.clientX;
drag.py = e.clientY;
cameraDirty = true;
}
};
handlers.up = (e) => {
pointers.delete(e.pointerId);
if (pointers.size < 2) pinch.on = false;
if (pointers.size === 1) {
const p = pointers.values().next().value;
drag.on = true;
drag.px = p.x;
drag.py = p.y;
} else if (pointers.size === 0) drag.on = false;
};
handlers.wheel = (e) => {
if (!enabled || flyAnim) return;
e.preventDefault();
zoomAt(e.deltaY < 0 ? Z_STEP : 1 / Z_STEP, ...local(e.clientX, e.clientY));
};
handlers.key = (down) => (e) => {
if (!enabled) return;
const tag = document.activeElement?.tagName;
if (tag === "INPUT" || tag === "TEXTAREA") return;
if (["ArrowLeft", "ArrowRight", "ArrowUp", "ArrowDown"].includes(e.key) || "wasd".includes(e.key.toLowerCase())) {
e.preventDefault();
if (down) keys.add(e.key);
else keys.delete(e.key);
}
};
handlers.keydown = handlers.key(true);
handlers.keyup = handlers.key(false);
canvas.style.touchAction = "none";
canvas.addEventListener("pointerdown", handlers.down);
canvas.addEventListener("pointermove", handlers.move);
window.addEventListener("pointerup", handlers.up);
canvas.addEventListener("wheel", handlers.wheel, { passive: false });
if (config.keyboardPan !== false) {
window.addEventListener("keydown", handlers.keydown);
window.addEventListener("keyup", handlers.keyup);
}
}
function unbindInput() {
const canvas = app?.canvas;
canvas?.removeEventListener("pointerdown", handlers.down);
canvas?.removeEventListener("pointermove", handlers.move);
window.removeEventListener("pointerup", handlers.up);
canvas?.removeEventListener("wheel", handlers.wheel);
window.removeEventListener("keydown", handlers.keydown);
window.removeEventListener("keyup", handlers.keyup);
}
const PAN_SPEED = 6;
function tick(ticker) {
for (const fn of tickHooks) fn(ticker);
if (flyAnim) {
flyAnim.t += ticker.deltaMS;
const p = Math.min(1, flyAnim.t / flyAnim.dur);
const e = p < 0.5 ? 2 * p * p : 1 - Math.pow(-2 * p + 2, 2) / 2;
camera.x = flyAnim.fromX + (flyAnim.toX - flyAnim.fromX) * e;
camera.y = flyAnim.fromY + (flyAnim.toY - flyAnim.fromY) * e;
zoom = flyAnim.fromZ + (flyAnim.toZ - flyAnim.fromZ) * e;
cameraDirty = true;
if (p >= 1) {
const r = flyAnim.resolve;
flyAnim = null;
r && r();
}
reconcile();
cameraDirty = false;
return;
}
if (!enabled) return;
let dx = 0, dy = 0;
for (const k of keys) {
if (k === "ArrowLeft" || k === "a" || k === "A") dx -= 1;
else if (k === "ArrowRight" || k === "d" || k === "D") dx += 1;
else if (k === "ArrowUp" || k === "w" || k === "W") dy -= 1;
else if (k === "ArrowDown" || k === "s" || k === "S") dy += 1;
}
if (dx || dy) {
const sp = PAN_SPEED * ticker.deltaTime / zoom;
camera.x += dx * sp;
camera.y += dy * sp;
cameraDirty = true;
}
if (fading.size) {
const step = 0.12 * ticker.deltaTime;
for (const sp of fading) {
sp.alpha = Math.min(1, sp.alpha + step);
if (sp.alpha >= 1) fading.delete(sp);
}
}
if (cameraDirty || genPending) {
reconcile();
cameraDirty = false;
const tk = Math.round(camera.x / TILE6) + "," + Math.round(camera.y / TILE6) + "," + zoom.toFixed(3);
if (tk !== lastEmitTile) {
lastEmitTile = tk;
emit();
}
}
}
const ready = (async () => {
const a = new Application();
await a.init({ background: BG, antialias: false, resizeTo: host });
if (!alive) {
a.destroy(true);
return;
}
app = a;
host.appendChild(app.canvas);
root = new Container();
app.stage.addChild(root);
macroRoot = new Container();
root.addChild(macroRoot);
genRoot = new Container();
root.addChild(genRoot);
shadowLayer = new Container();
root.addChild(shadowLayer);
propLayer = new Container();
propLayer.sortableChildren = true;
root.addChild(propLayer);
ctx = await config.load({ Assets, Texture, Rectangle });
if (!alive) return;
bindInput();
app.ticker.add(tick);
app.renderer.on("resize", () => {
cameraDirty = true;
});
cameraDirty = true;
reconcile();
emit();
})();
function getCamera() {
return { x: camera.x, y: camera.y, zoom };
}
function onTick(fn) {
tickHooks.add(fn);
return () => tickHooks.delete(fn);
}
function getEntityLayer() {
return propLayer;
}
function getApp() {
return app;
}
function screenToWorld(sx, sy) {
if (!app) return { x: 0, y: 0 };
return { x: camera.x + (sx - app.screen.width / 2) / zoom, y: camera.y + (sy - app.screen.height / 2) / zoom };
}
function worldToScreen(wx, wy) {
if (!app) return { x: 0, y: 0 };
return { x: app.screen.width / 2 + (wx - camera.x) * zoom, y: app.screen.height / 2 + (wy - camera.y) * zoom };
}
function tileIndexAt(wx, wy) {
if (!config.tileIndexAt) return null;
const cx = Math.floor(wx / CHUNK5), cy = Math.floor(wy / CHUNK5);
const ch = chunks.get(cx + "," + cy);
if (!ch) return null;
return config.tileIndexAt(wx, wy, ch.meta);
}
function biomeAt(wx, wy) {
return config.biomeAt ? config.biomeAt(seed, wx, wy) : null;
}
function getBounds() {
return bounds ? { x0: bounds.x0, y0: bounds.y0, x1: bounds.x1, y1: bounds.y1 } : null;
}
function setEnabled(v) {
enabled = v;
if (v) cameraDirty = true;
}
function regenerate(nextSeed) {
seed = nextSeed >>> 0;
clearChunks();
clearMacro();
cameraDirty = true;
reconcile();
emit();
}
function onChange(fn) {
listeners.add(fn);
return () => listeners.delete(fn);
}
function destroy() {
alive = false;
try {
unbindInput();
} catch {
}
try {
app?.ticker.remove(tick);
} catch {
}
try {
clearChunks();
clearMacro();
} catch {
}
try {
app?.canvas?.remove();
} catch {
}
try {
app?.destroy(true, { children: true });
} catch {
}
app = null;
root = null;
genRoot = null;
macroRoot = null;
shadowLayer = null;
propLayer = null;
ctx = null;
texCache.clear();
}
return { ready, regenerate, destroy, onChange, getSnapshot, getCamera, tileIndexAt, biomeAt, getBounds, zoomBy, flyTo, setEnabled, onTick, getEntityLayer, getApp, screenToWorld, worldToScreen, tile: TILE6 };
}
// ../auto-battler/src/engine/rng.js
function makeGenRng(seed) {
let s = Math.imul(seed >>> 0 ^ 2654435769, 2654435761) >>> 0 || 1;
s = (s ^ s >>> 15) >>> 0;
const rnd = () => {
s = Math.imul(s, 1664525) + 1013904223 >>> 0;
return s / 4294967296;
};
const ri = (a, b) => a + Math.floor(rnd() * (b - a + 1));
return { rnd, ri };
}
// ../auto-battler/src/engine/worldgen.js
function hash2(seed, x, y) {
let h2 = Math.imul(x | 0, 374761393) + Math.imul(y | 0, 668265263) + Math.imul(seed | 0, 2654435761);
h2 = Math.imul(h2 ^ h2 >>> 13, 1274126177);
h2 ^= h2 >>> 16;
return (h2 >>> 0) / 4294967296;
}
var smooth = (t) => t * t * (3 - 2 * t);
var lerp = (a, b, t) => a + (b - a) * t;
function valueNoise(seed, x, y) {
const xi = Math.floor(x), yi = Math.floor(y);
const xf = x - xi, yf = y - yi;
const v00 = hash2(seed, xi, yi), v10 = hash2(seed, xi + 1, yi);
const v01 = hash2(seed, xi, yi + 1), v11 = hash2(seed, xi + 1, yi + 1);
const u = smooth(xf), v = smooth(yf);
return lerp(lerp(v00, v10, u), lerp(v01, v11, u), v);
}
function fbm(seed, x, y, octaves = 5, lacunarity = 2, gain = 0.5) {
let amp = 1, freq = 1, sum = 0, norm = 0;
for (let o = 0; o < octaves; o++) {
sum += amp * valueNoise(seed + o * 1013, x * freq, y * freq);
norm += amp;
amp *= gain;
freq *= lacunarity;
}
return sum / norm;
}
// ../auto-battler/src/engine/biomeMap.js
var sub = (seed, salt) => (Math.imul(seed | 0, 2654435761) ^ (salt | 0)) >>> 0;
var SCALE = 5e-3;
var REGION_OCTAVES = 3;
var WARP_SCALE = 0.012;
var WARP_AMP = 40;
var BIOMES = [
{ id: "forgottenPlains", salt: 985505 },
// green meadow
{ id: "orc", salt: 11281 },
// dirt / grass plains
{ id: "necropolis", salt: 966869 }
// corrupted swamp
];
var OVERWORLD_BIOMES = BIOMES.map((b) => b.id);
function weights(seed, x, y, out) {
const wx = x + WARP_AMP * (fbm(sub(seed, 1441), x * WARP_SCALE, y * WARP_SCALE) - 0.5);
const wy = y + WARP_AMP * (fbm(sub(seed, 1442), x * WARP_SCALE, y * WARP_SCALE) - 0.5);
for (let i = 0; i < BIOMES.length; i++) out[i] = fbm(sub(seed, BIOMES[i].salt), wx * SCALE, wy * SCALE, REGION_OCTAVES);
return out;
}
var _w = new Array(BIOMES.length);
function biomeRegion(seed, x, y) {
const w = weights(seed, x, y, _w);
let i1 = 0;
for (let i = 1; i < w.length; i++) if (w[i] > w[i1]) i1 = i;
let i2 = -1;
for (let i = 0; i < w.length; i++) {
if (i === i1) continue;
if (i2 < 0 || w[i] > w[i2]) i2 = i;
}
return { id: BIOMES[i1].id, id2: BIOMES[i2]?.id ?? BIOMES[i1].id, edge: w[i1] - w[i2] };
}
// ../auto-battler/src/engine/transitionStencils.js
var TILE = 8;
var MID = 3.5;
var BAND = 2.6;
var WAVE_AMP = 1.35;
var WAVE_FREQ = 0.45;
var R_OUTER = 4.6;
var R_CONCAVE = 2.3;
var R_TIP = 2.6;
var BAYER4 = [
0.5 / 16,
8.5 / 16,
2.5 / 16,
10.5 / 16,
12.5 / 16,
4.5 / 16,
14.5 / 16,
6.5 / 16,
3.5 / 16,
11.5 / 16,
1.5 / 16,
9.5 / 16,
15.5 / 16,
7.5 / 16,
13.5 / 16,
5.5 / 16
];
var bayer = (x, y) => BAYER4[(y & 3) * 4 + (x & 3)];
function wave1d(t, salt) {
const i = Math.floor(t * WAVE_FREQ), f = t * WAVE_FREQ - i;
const a = hash2(salt, i, 0), b = hash2(salt, i + 1, 0);
const u = f * f * (3 - 2 * f);
return (a + (b - a) * u) * 2 - 1;
}
var CASES = {
// cardinal edges — wavy line; fg on the side away from the foreign neighbour.
N: (x, y, s) => y - (MID + WAVE_AMP * wave1d(x, s)),
S: (x, y, s) => MID + WAVE_AMP * wave1d(x, s) - y,
E: (x, y, s) => MID + WAVE_AMP * wave1d(y, s) - x,
W: (x, y, s) => x - (MID + WAVE_AMP * wave1d(y, s)),
// outer corners — bg is a quarter-disc in the corner (2 adjacent foreign cardinals).
oNE: (x, y, s) => dist(x, y, 7, 0) - (R_OUTER + WAVE_AMP * wave1d(x + y, s)),
oNW: (x, y, s) => dist(x, y, 0, 0) - (R_OUTER + WAVE_AMP * wave1d(x + y, s)),
oSE: (x, y, s) => dist(x, y, 7, 7) - (R_OUTER + WAVE_AMP * wave1d(x + y, s)),
oSW: (x, y, s) => dist(x, y, 0, 7) - (R_OUTER + WAVE_AMP * wave1d(x + y, s)),
// concave corners — small bg notch in the corner (1 foreign diagonal only).
cNE: (x, y, s) => dist(x, y, 7, 0) - (R_CONCAVE + 0.6 * wave1d(x + y, s)),
cNW: (x, y, s) => dist(x, y, 0, 0) - (R_CONCAVE + 0.6 * wave1d(x + y, s)),
cSE: (x, y, s) => dist(x, y, 7, 7) - (R_CONCAVE + 0.6 * wave1d(x + y, s)),
cSW: (x, y, s) => dist(x, y, 0, 7) - (R_CONCAVE + 0.6 * wave1d(x + y, s)),
// peninsula — fg is a small blob on the one open side (3 foreign cardinals).
tipN: (x, y, s) => R_TIP + 0.6 * wave1d(x, s) - dist(x, y, 3.5, 0),
tipS: (x, y, s) => R_TIP + 0.6 * wave1d(x, s) - dist(x, y, 3.5, 7),
tipE: (x, y, s) => R_TIP + 0.6 * wave1d(y, s) - dist(x, y, 7, 3.5),
tipW: (x, y, s) => R_TIP + 0.6 * wave1d(y, s) - dist(x, y, 0, 3.5),
// island — fg is a small blob in the centre (foreign on all 4 cardinals).
island: (x, y, s) => R_TIP + 0.6 * wave1d(x - y, s) - dist(x, y, 3.5, 3.5)
};
function dist(x, y, cx, cy) {
const dx = x - cx, dy = y - cy;
return Math.sqrt(dx * dx + dy * dy);
}
var STENCIL_CASES = Object.keys(CASES);
function boundaryCase(N, E, S, W, NW, NE, SW, SE) {
const card = N + E + S + W;
if (card === 0) {
const diag = NW + NE + SW + SE;
if (diag === 0) return null;
if (NW) return "cNW";
if (NE) return "cNE";
if (SW) return "cSW";
return "cSE";
}
if (card === 1) return N ? "N" : E ? "E" : S ? "S" : "W";
if (card === 2) {
if (N && E) return "oNE";
if (N && W) return "oNW";
if (S && E) return "oSE";
if (S && W) return "oSW";
return N ? "N" : "E";
}
if (card === 3) return !N ? "tipN" : !E ? "tipE" : !S ? "tipS" : "tipW";
return "island";
}
function makeStencil(caseKey, variant = 0) {
const sd = CASES[caseKey];
if (!sd) throw new Error(`unknown stencil case "${caseKey}"`);
const salt = (hashStr(caseKey) ^ variant * 40503) >>> 0;
const mask = new Uint8Array(TILE * TILE);
for (let y = 0; y < TILE; y++) for (let x = 0; x < TILE; x++) {
const d = sd(x, y, salt);
let fg;
if (d > BAND / 2) fg = 1;
else if (d < -BAND / 2) fg = 0;
else fg = (d + BAND / 2) / BAND > bayer(x, y) ? 1 : 0;
mask[y * TILE + x] = fg;
}
return mask;
}
function hashStr(s) {
let h2 = 2166136261;
for (let i = 0; i < s.length; i++) {
h2 ^= s.charCodeAt(i);
h2 = Math.imul(h2, 16777619);
}
return h2 >>> 0;
}
// ../auto-battler/src/engine/orcGen.js
var sub2 = (seed, salt) => (Math.imul(seed | 0, 2654435761) ^ (salt | 0)) >>> 0;
var DDIRT_SCALE = 0.045;
var DDIRT_THRESHOLD = 0.55;
function isDarkerDirt(seed, x, y) {
return fbm(sub2(seed, 56599), x * DDIRT_SCALE, y * DDIRT_SCALE) > DDIRT_THRESHOLD;
}
var GRASS_SCALE = 0.04;
var GRASS_THRESHOLD = 0.58;
function isGrass(seed, x, y) {
return fbm(sub2(seed, 27221), x * GRASS_SCALE, y * GRASS_SCALE) > GRASS_THRESHOLD;
}
var ROCK_SCALE = 0.12;
var ROCK_THRESHOLD = 0.64;
function isRock(seed, x, y) {
return fbm(sub2(seed, 16588), x * ROCK_SCALE, y * ROCK_SCALE) > ROCK_THRESHOLD;
}
// ../auto-battler/src/render/tileAutotile.js
var rect = (c, r, w, h2) => {
const a = [];
for (let j = 0; j < h2; j++) for (let i = 0; i < w; i++) a.push([c + i, r + j]);
return a;
};
var offset = (t, col) => [t[0] + col, t[1]];
var rhash = (x, y, salt, seed) => Math.imul(x * 73856093 ^ y * 19349663 ^ seed + (salt | 0), 2654435761) >>> 0;
function hashU32(a, b, c) {
let h2 = Math.imul((a | 0) ^ 2654435769, 2654435761);
h2 = Math.imul(h2 ^ (b | 0) ^ 2246822507, 2246822519);
h2 = Math.imul(h2 ^ (c | 0) ^ 3266489909, 3266489917);
h2 ^= h2 >>> 15;
return h2 >>> 0;
}
var sparse = (base, vars, x, y, salt, rate, seed) => {
if (!vars.length) return base;
const h2 = rhash(x, y, salt, seed);
return h2 % rate === 0 ? vars[(h2 >>> 5) % vars.length] : base;
};
var lerp3 = (a, b, t) => [a[0] + (b[0] - a[0]) * t, a[1] + (b[1] - a[1]) * t, a[2] + (b[2] - a[2]) * t];
function nineSlice(set, top, bot, left, right) {
if (top) return left ? set.nw : right ? set.ne : set.n;
if (bot) return left ? set.sw : right ? set.se : set.s;
if (left) return set.w;
if (right) return set.e;
return set.c;
}
function autotile(set, N, E, S, W, NW, NE, SW, SE) {
const card = N + E + S + W;
if (card === 1) return N ? set.N : S ? set.S : E ? set.E : set.W;
if (card === 2) {
if (N && W) return set.vNW;
if (N && E) return set.vNE;
if (S && W) return set.vSW;
if (S && E) return set.vSE;
return N ? set.N : set.E;
}
if (card >= 3) return N ? set.N : S ? set.S : E ? set.E : set.W;
const diag = NW + NE + SW + SE;
if (diag === 0) return null;
if (diag === 1) return NW ? set.cNW : NE ? set.cNE : SW ? set.cSW : set.cSE;
if (NW && SE && !NE && !SW) return set.dNWSE;
if (NE && SW && !NW && !SE) return set.dSWNE;
return null;
}
function cleanField(raw, M, SZ) {
const idx = (i, j) => j * SZ + i;
let cur = new Uint8Array(SZ * SZ);
for (let j = 0; j < SZ; j++) for (let i = 0; i < SZ; i++) cur[idx(i, j)] = raw(i - M, j - M) ? 1 : 0;
let nxt = new Uint8Array(SZ * SZ);
for (let j = 1; j < SZ - 1; j++) for (let i = 1; i < SZ - 1; i++) {
let c = 0;
for (let dj = -1; dj <= 1; dj++) for (let di = -1; di <= 1; di++) c += cur[idx(i + di, j + dj)];
nxt[idx(i, j)] = c >= 5 ? 1 : 0;
}
cur = nxt;
for (let p = 0; p < 2; p++) {
nxt = new Uint8Array(SZ * SZ);
for (let j = 1; j < SZ - 1; j++) for (let i = 1; i < SZ - 1; i++) {
const card = cur[idx(i, j - 1)] + cur[idx(i + 1, j)] + cur[idx(i, j + 1)] + cur[idx(i - 1, j)];
nxt[idx(i, j)] = cur[idx(i, j)] ? card >= 2 ? 1 : 0 : card >= 3 ? 1 : 0;
}
cur = nxt;
}
return cur;
}
function blockTile([c0, r0], N, E, S, W) {
const cx = W && !E ? 0 : E && !W ? 2 : 1;
const cy = N && !S ? 0 : S && !N ? 2 : 1;
return [c0 + cx, r0 + cy];
}
function centerTile([c0, r0], dNW, dNE, dSW, dSE) {
if (!dNW && !dNE && !dSW && !dSE) return [c0 + 1, r0 + 1];
if (dNW && dSE && !dNE && !dSW) return [c0 + 2, r0 + 3];
if (dNE && dSW && !dNW && !dSE) return [c0 + 2, r0 + 4];
if (dNW) return [c0, r0 + 3];
if (dNE) return [c0 + 1, r0 + 3];
if (dSW) return [c0, r0 + 4];
return [c0 + 1, r0 + 4];
}
function bakeCliffs(cfg, { chunk, x0, y0, seed, raised, place, accept = () => true, stairAt = null, stairAtV = null }) {
const ST = cfg.STAIR && (stairAt || stairAtV) ? cfg.STAIR : null;
const useNS = ST && stairAt, useWE = ST && stairAtV;
const isWide = (ax, ay, room) => room && rhash(ax, ay, ST.WIDE_SALT, seed) % 2 === 0;
const sAnchor = (x, y) => raised(x, y) && !raised(x, y + 1) && raised(x - 1, y) && raised(x + 1, y) && stairAt(x, y);
const nAnchor = (x, y) => raised(x, y) && !raised(x, y - 1) && raised(x - 1, y) && raised(x + 1, y) && stairAt(x, y);
const wAnchor = (x, y) => raised(x, y) && !raised(x - 1, y) && raised(x + 1, y) && raised(x, y - 1) && raised(x, y + 1) && !raised(x - 1, y + 1) && stairAtV(x, y);
const eAnchor = (x, y) => raised(x, y) && !raised(x + 1, y) && raised(x - 1, y) && raised(x, y - 1) && raised(x, y + 1) && !raised(x + 1, y + 1) && stairAtV(x, y);
const sRoom = (x, y) => !raised(x + 1, y + 1) && raised(x + 2, y) && !raised(x + 2, y + 1) && raised(x + 3, y);
const nRoom = (x, y) => !raised(x + 1, y - 1) && raised(x + 2, y) && !raised(x + 2, y - 1) && raised(x + 3, y);
const wRoom = (x, y) => raised(x, y + 2) && !raised(x - 1, y + 2);
const eRoom = (x, y) => raised(x, y + 2) && !raised(x + 1, y + 2);
const W_OFF = [[0, 0], [0, 1], [-1, 0], [-1, 1], [-1, 2]];
const E_OFF = [[0, 0], [0, 1], [1, 0], [1, 1], [1, 2]];
const sDepth = useNS ? Math.max(ST.S.NARROW.length, ST.S.WIDE.length) : 0;
function stairTileAt(wx, wy) {
if (useNS) {
for (let drow = 0; drow < sDepth; drow++) for (let dcol = 0; dcol <= 2; dcol++) {
const ax = wx - dcol, ay = wy - drow;
if (!sAnchor(ax, ay)) continue;
if (isWide(ax, ay, sRoom(ax, ay))) {
if (drow < ST.S.WIDE.length) return ST.S.WIDE[drow][dcol];
} else if (dcol === 0 && drow < ST.S.NARROW.length) return ST.S.NARROW[drow];
}
for (let dcol = 0; dcol <= 2; dcol++) {
if (nAnchor(wx - dcol, wy)) {
if (isWide(wx - dcol, wy, nRoom(wx - dcol, wy))) return ST.N.WIDE.LIP[dcol];
if (dcol === 0) return ST.N.NARROW.LIP;
}
if (nAnchor(wx - dcol, wy + 1)) {
if (isWide(wx - dcol, wy + 1, nRoom(wx - dcol, wy + 1))) return ST.N.WIDE.CAP[dcol];
if (dcol === 0) return ST.N.NARROW.CAP;
}
}
}
if (useWE) {
for (const [dx, dy] of W_OFF) {
const ax = wx - dx, ay = wy - dy;
if (!wAnchor(ax, ay)) continue;
const e = (isWide(ax, ay, wRoom(ax, ay)) ? ST.W.WIDE : ST.W.NARROW).find((o) => o.dx === dx && o.dy === dy);
if (e) return e.t;
}
for (const [dx, dy] of E_OFF) {
const ax = wx - dx, ay = wy - dy;
if (!eAnchor(ax, ay)) continue;
const e = (isWide(ax, ay, eRoom(ax, ay)) ? ST.E.WIDE : ST.E.NARROW).find((o) => o.dx === dx && o.dy === dy);
if (e) return e.t;
}
}
return null;
}
for (let ty = 0; ty < chunk; ty++) for (let tx = 0; tx < chunk; tx++) {
if (!accept(tx, ty)) continue;
const wx = x0 + tx, wy = y0 + ty;
if (ST) {
const st = stairTileAt(wx, wy);
if (st) {
place(st, tx, ty);
continue;
}
}
if (raised(wx, wy)) {
const bN = !raised(wx, wy - 1), bE = !raised(wx + 1, wy), bS = !raised(wx, wy + 1), bW = !raised(wx - 1, wy);
if (bN && bW) place(cfg.LIP_TL, tx, ty);
else if (bN && bE) place(cfg.LIP_TR, tx, ty);
else if (bS && bW) place(cfg.LIP_SW, tx, ty);
else if (bS && bE) place(cfg.LIP_SE, tx, ty);
else if (bN) place(sparse(cfg.LIP_T, cfg.LIP_T_VAR, wx, wy, 1, cfg.RATE, seed), tx, ty);
else if (bS) place(sparse(cfg.LIP_S, cfg.LIP_S_VAR, wx, wy, 2, cfg.RATE, seed), tx, ty);
else if (bW) place(sparse(cfg.WALL_L, cfg.WALL_L_VAR, wx, wy, 3, cfg.RATE, seed), tx, ty);
else if (bE) place(sparse(cfg.WALL_R, cfg.WALL_R_VAR, wx, wy, 4, cfg.RATE, seed), tx, ty);
} else {
for (let k = 1; k <= cfg.FACE_H; k++) {
if (raised(wx, wy - k)) {
const leftEnd = !raised(wx - 1, wy - k), rightEnd = !raised(wx + 1, wy - k);
place(leftEnd ? cfg.FACE_L[k - 1] : rightEnd ? cfg.FACE_R[k - 1] : sparse(cfg.FACE[k - 1], cfg.FACE_VAR[k - 1], wx, wy, 5 + k, cfg.RATE, seed), tx, ty);
break;
}
}
}
}
}
// ../auto-battler/src/render/orcKingdom.js
var ORC = "/assets/minifantasy/Minifantasy_Orc_Kingdom_v1.0/Minifantasy_Orc_Kingdom_Assets";
var TILES = `${ORC}/Tileset/Tiles.png`;
var ORC_MAP_ASSETS = [TILES];
var TILE2 = 8;
var CHUNK = 32;
var DIRT_BASE = [7, 51];
var DIRT_VARS = rect(13, 51, 2, 6);
var DIRT_RATE = 4;
var GRASS = {
fill: [3, 51],
vars: [],
N: [3, 54],
S: [3, 52],
E: [2, 53],
W: [4, 53],
cNW: [4, 54],
cNE: [2, 54],
cSW: [4, 52],
cSE: [2, 52],
vNW: [3, 56],
vNE: [2, 56],
vSW: [3, 55],
vSE: [2, 55],
dNWSE: [4, 55],
dSWNE: [4, 56]
};
var DDIRT = {
fill: [7, 53],
vars: rect(10, 51, 2, 6),
N: [7, 52],
S: [7, 54],
E: [8, 53],
W: [6, 53],
vNW: [6, 52],
vNE: [8, 52],
vSW: [6, 54],
vSE: [8, 54],
cNW: [6, 55],
cNE: [7, 55],
cSW: [6, 56],
cSE: [7, 56],
dNWSE: [8, 55],
dSWNE: [8, 56]
};
var OVERLAY_RATE = 5;
var ROCK_COLOR_OFF = [0, 6, 12];
var STONE_SIZES = [[16, 51], [16, 52], [16, 53], [16, 54], [16, 55], [16, 56]];
var ROCK_FOLIAGE = [[17, 51], [18, 51], [19, 51], [20, 51]];
var ROCK_DENSE = [[18, 53], [19, 53], [18, 54], [19, 54], [18, 55], [19, 55]];
var ROCK_SPARSE = [
[17, 52],
[18, 52],
[19, 52],
[20, 52],
[17, 53],
[20, 53],
[17, 54],
[20, 54],
[17, 55],
[20, 55],
[17, 56],
[18, 56],
[19, 56],
[20, 56]
];
var ROCK_STONE_RATE = 28;
var ROCK_FOLIAGE_RATE = 12;
var COL_DIRT = [150, 128, 95];
var COL_DDIRT = [106, 90, 79];
var COL_GRASS = [96, 132, 58];
function chunkFields(seed, x0, y0) {
const M = 6, SZ = CHUNK + 2 * M;
const dd = cleanField((lx, ly) => isDarkerDirt(seed, x0 + lx, y0 + ly), M, SZ);
const grRaw = cleanField((lx, ly) => isGrass(seed, x0 + lx, y0 + ly), M, SZ);
const gr = new Uint8Array(grRaw.length);
for (let k = 0; k < gr.length; k++) gr[k] = grRaw[k] && !dd[k] ? 1 : 0;
const rockRaw = cleanField((lx, ly) => isRock(seed, x0 + lx, y0 + ly), M, SZ);
const rock = new Uint8Array(rockRaw.length);
for (let k = 0; k < rock.length; k++) rock[k] = rockRaw[k] && !dd[k] && !gr[k] ? 1 : 0;
return { dd, gr, rock, M, SZ, x0, y0 };
}
var ORC_GROUND_FILLS = [
{ key: "dirt", url: TILES, tile: DIRT_BASE },
{ key: "ddirt", url: TILES, tile: DDIRT.fill },
{ key: "grass", url: TILES, tile: GRASS.fill }
];
function orcGroundFillKey(seed, x, y) {
if (isDarkerDirt(seed, x, y)) return "ddirt";
if (isGrass(seed, x, y)) return "grass";
return "dirt";
}
var orcConfig = (seed) => ({
seed,
tile: TILE2,
chunk: CHUNK,
background: "#69636f",
async load({ Assets }) {
const t = await Assets.load(TILES);
t.source.scaleMode = "nearest";
return { tiles: t.source };
},
// `accept(tx,ty)` (chunk-local) gates which tiles this biome paints — defaults to all, so the
// standalone map is unchanged; the multi-biome overworld passes a per-biome dither mask.
bake({ x0, y0, seed: seed2, ctx, add, accept = () => true }) {
const f = chunkFields(seed2, x0, y0);
const { dd, gr, rock, M, SZ } = f;
const idx = (i, j) => j * SZ + i;
const at = (fld, lx, ly) => fld[idx(lx + M, ly + M)] === 1;
const atRock = (lx, ly) => rock[idx(lx + M, ly + M)] === 1;
const place = (coord, tx, ty) => add(ctx.tiles, coord[0], coord[1], tx, ty);
const drawLayer = (field, set, tx, ty) => {
if (!at(field, tx, ty)) return;
const N = !at(field, tx, ty - 1), E = !at(field, tx + 1, ty), S = !at(field, tx, ty + 1), W = !at(field, tx - 1, ty);
const NW = !at(field, tx - 1, ty - 1), NE = !at(field, tx + 1, ty - 1), SW = !at(field, tx - 1, ty + 1), SE = !at(field, tx + 1, ty + 1);
const t = autotile(set, N, E, S, W, NW, NE, SW, SE);
place(t || sparse(set.fill, set.vars, x0 + tx, y0 + ty, set === GRASS ? 1 : 2, OVERLAY_RATE, seed2), tx, ty);
};
for (let ty = 0; ty < CHUNK; ty++) {
for (let tx = 0; tx < CHUNK; tx++) {
if (!accept(tx, ty)) continue;
place(sparse(DIRT_BASE, DIRT_VARS, x0 + tx, y0 + ty, 0, DIRT_RATE, seed2), tx, ty);
drawLayer(dd, DDIRT, tx, ty);
drawLayer(gr, GRASS, tx, ty);
if (atRock(tx, ty)) {
const h2 = rhash(x0 + tx, y0 + ty, 3, seed2);
const interior = atRock(tx, ty - 1) && atRock(tx + 1, ty) && atRock(tx, ty + 1) && atRock(tx - 1, ty);
const pool = interior ? ROCK_DENSE : ROCK_SPARSE;
place(offset(pool[(h2 >>> 2) % pool.length], ROCK_COLOR_OFF[h2 % 3]), tx, ty);
} else if (!at(dd, tx, ty) && !at(gr, tx, ty)) {
const h2 = rhash(x0 + tx, y0 + ty, 7, seed2);
if (h2 % ROCK_STONE_RATE === 0) place(offset(STONE_SIZES[(h2 >>> 3) % STONE_SIZES.length], ROCK_COLOR_OFF[(h2 >>> 6) % 3]), tx, ty);
else if ((h2 >>> 8) % ROCK_FOLIAGE_RATE === 0) place(offset(ROCK_FOLIAGE[(h2 >>> 11) % ROCK_FOLIAGE.length], ROCK_COLOR_OFF[(h2 >>> 14) % 3]), tx, ty);
}
}
}
return { meta: f };
},
macroColor(seed2, tx, ty) {
if (isDarkerDirt(seed2, tx, ty)) return COL_DDIRT;
if (isGrass(seed2, tx, ty)) return COL_GRASS;
return COL_DIRT;
},
// Top-most ground tile [c,r] at world (wx,wy) for the grid overlay (rocks omitted).
tileIndexAt(wx, wy, meta) {
if (!meta) return null;
const { dd, gr, M, SZ, x0, y0 } = meta;
const lx = wx - x0, ly = wy - y0;
if (lx < 0 || ly < 0 || lx >= CHUNK || ly >= CHUNK) return null;
const at = (f, x, y) => f[(y + M) * SZ + (x + M)] === 1;
for (const [field, set] of [[gr, GRASS], [dd, DDIRT]]) {
if (!at(field, lx, ly)) continue;
const N = !at(field, lx, ly - 1), E = !at(field, lx + 1, ly), S = !at(field, lx, ly + 1), W = !at(field, lx - 1, ly);
const NW = !at(field, lx - 1, ly - 1), NE = !at(field, lx + 1, ly - 1), SW = !at(field, lx - 1, ly + 1), SE = !at(field, lx + 1, ly + 1);
return autotile(set, N, E, S, W, NW, NE, SW, SE) || set.fill;
}
return DIRT_BASE;
}
});
function createOrcMap(pixi, host, opts = {}) {
return createChunkedMap(pixi, host, orcConfig(opts.seed ?? 1));
}
// ../auto-battler/src/engine/fpGen.js
var sub3 = (seed, salt) => (Math.imul(seed | 0, 2654435761) ^ (salt | 0)) >>> 0;
var DIRT_SCALE = 0.04;
var DIRT_THRESHOLD = 0.56;
function isDirt(seed, x, y) {
return fbm(sub3(seed, 53543), x * DIRT_SCALE, y * DIRT_SCALE) > DIRT_THRESHOLD;
}
var STONE_SCALE = 0.085;
var STONE_THRESHOLD = 0.66;
function isStone(seed, x, y) {
return fbm(sub3(seed, 22286), x * STONE_SCALE, y * STONE_SCALE) > STONE_THRESHOLD;
}
var FOREST_SCALE = 0.05;
function forestField(seed, x, y) {
return fbm(sub3(seed, 15740503), x * FOREST_SCALE, y * FOREST_SCALE);
}
var STONE_CLUMP_SCALE = 0.11;
function stoneClumpField(seed, x, y) {
return fbm(sub3(seed, 5702849), x * STONE_CLUMP_SCALE, y * STONE_CLUMP_SCALE);
}
var ELEV_SCALE = 0.025;
var ELEV_THRESHOLD = 0.6;
function isRaised(seed, x, y) {
return fbm(sub3(seed, 57836), x * ELEV_SCALE, y * ELEV_SCALE) > ELEV_THRESHOLD;
}
var RIVER_SCALE = 0.022;
var RIVER_WIDTH = 0.035;
var WARP_SCALE2 = 0.03;
var WARP_AMP2 = 22;
function isRiver(seed, x, y) {
const wx = x + WARP_AMP2 * (fbm(sub3(seed, 1297), x * WARP_SCALE2, y * WARP_SCALE2) - 0.5);
const wy = y + WARP_AMP2 * (fbm(sub3(seed, 1298), x * WARP_SCALE2, y * WARP_SCALE2) - 0.5);
return Math.abs(fbm(sub3(seed, 8654), wx * RIVER_SCALE, wy * RIVER_SCALE) - 0.5) < RIVER_WIDTH;
}
// ../auto-battler/src/render/forgottenPlains.js
var FP = "/assets/minifantasy/Minifantasy_ForgottenPlains_v3.6_Commercial_Version/Minifantasy_ForgottenPlains_Assets";
var TILES2 = `${FP}/Tileset/Minifantasy_ForgottenPlainsTiles.png`;
var SHADOW = `${FP}/Tileset/Minifantasy_ForgottenPlainsTilesShadows.png`;
var PROPS = `${FP}/props/Minifantasy_ForgottenPlainsProps.png`;
var PROP_SHADOW = `${FP}/props/Minifantasy_ForgottenPlainsPropsShadows.png`;
var FP_TILES_URL = TILES2;
var FP_PROPS_URL = PROPS;
var FP_MOCKUP_URL = `${FP}/Minifantasy_ForgottenPlainsMockup.png`;
var FP_MAP_ASSETS = [TILES2, SHADOW, PROPS, PROP_SHADOW];
var TILE3 = 8;
var CHUNK2 = 32;
var GRASS_BASE = [37, 11];
var GRASS_VARS = [...rect(1, 1, 4, 1), ...rect(2, 3, 3, 5)];
var GRASS_RATE = 9;
var DIRT_BLOCK = [7, 3];
var DIRT_FILL = [8, 4];
var DIRT_VARS2 = [[7, 1], [8, 1], [9, 1]];
var STONE_BLOCK = [12, 3];
var STONE_FILL = [13, 4];
var STONE_VARS = [[12, 1], [13, 1], [14, 1]];
var WATER_BLOCK = [25, 3];
var WATER_FILL = [26, 4];
var WATER_VARS = [[26, 4]];
var FILL_RATE = 6;
var FP_CLIFF = {
LIP_T: [25, 16],
LIP_T_VAR: [],
LIP_TL: [24, 16],
LIP_TR: [26, 16],
WALL_L: [24, 17],
WALL_L_VAR: [],
WALL_R: [26, 17],
WALL_R_VAR: [],
LIP_S: [25, 18],
LIP_S_VAR: [],
LIP_SW: [24, 18],
LIP_SE: [26, 18],
FACE_H: 2,
FACE: [[25, 19], [25, 20]],
FACE_VAR: [[], []],
FACE_L: [[24, 19], [24, 20]],
FACE_R: [[26, 19], [26, 20]],
RATE: 4,
// Stair exits cut into straight cliff edges. Two sheet variants, one tile wide (cross at cols 2–6)
// and two tiles wide (cross at cols 8–14); bakeCliffs picks per-exit by hash. Every tile below is
// a real non-empty sheet cell — the crosses have transparent corners, so placing those would bleed
// the grass base through (the bug the NARROW arms used to hit). Layout per direction:
// • S — NARROW: one column, [lip, step, step, step(onto ground)]. WIDE: 3 columns × those 4 rows.
// • N — LIP on the raised back edge (grass+steps) + CAP one tile north on the ground; ×3 cols wide.
// • W/E — a descent off the side wall. Each tile carries its {dx,dy} cell-offset from the wall
// anchor. NARROW: 3-tile L. WIDE: 2-tall entry on the wall + a 3-tall stone descent beside it.
STAIR: {
WIDE_SALT: 359697,
// hash salt deciding narrow-vs-wide at each exit (~50/50)
S: {
NARROW: [[4, 18], [4, 19], [4, 20], [4, 20]],
WIDE: [
[[10, 19], [11, 19], [12, 19]],
[[10, 20], [11, 20], [12, 20]],
[[10, 21], [11, 21], [12, 21]],
[[10, 21], [11, 21], [12, 21]]
]
},
N: {
NARROW: { LIP: [4, 16], CAP: [4, 15] },
WIDE: { LIP: [[10, 16], [11, 16], [12, 16]], CAP: [[10, 15], [11, 15], [12, 15]] }
},
W: {
NARROW: [{ dx: 0, dy: 0, t: [3, 17] }, { dx: -1, dy: 0, t: [2, 17] }, { dx: -1, dy: 1, t: [2, 18] }],
WIDE: [
{ dx: 0, dy: 0, t: [9, 17] },
{ dx: 0, dy: 1, t: [9, 18] },
{ dx: -1, dy: 0, t: [8, 17] },
{ dx: -1, dy: 1, t: [8, 18] },
{ dx: -1, dy: 2, t: [8, 19] }
]
},
E: {
NARROW: [{ dx: 0, dy: 0, t: [5, 17] }, { dx: 1, dy: 0, t: [6, 17] }, { dx: 1, dy: 1, t: [6, 18] }],
WIDE: [
{ dx: 0, dy: 0, t: [13, 17] },
{ dx: 0, dy: 1, t: [13, 18] },
{ dx: 1, dy: 0, t: [14, 17] },
{ dx: 1, dy: 1, t: [14, 18] },
{ dx: 1, dy: 2, t: [14, 19] }
]
}
}
};
var STAIR_SPACING = 14;
var COL_CLIFF = [150, 138, 112];
var FOLIAGE = [
{ weight: 5, tiles: [[9, 6], [10, 6], [11, 6]] },
// grass tufts
{ weight: 3, tiles: [[12, 6], [13, 6]] },
// small reeds
{ weight: 3, tiles: [[9, 9], [10, 9], [11, 9], [12, 9], [13, 9]] },
// flowers (red/purple/daisy) + small grass
{ weight: 1, sprite: { c: 14, r: 6, w: 1, h: 3 } },
// tall reed 1×3
{ weight: 1, sprite: { c: 15, r: 6, w: 2, h: 3 } },
// wide reed 2×3
{ weight: 1, sprite: { c: 17, r: 6, w: 2, h: 3 } },
// wide reed 2×3
{ weight: 1, sprite: { c: 14, r: 9, w: 1, h: 2 } }
// cattail 1×2
];
var FOLIAGE_WEIGHT = FOLIAGE.reduce((s, g) => s + g.weight, 0);
var FOLIAGE_RATE = 11;
var WATER_FOLIAGE = { c: 15, r: 9, w: 1, h: 2 };
var WATER_FOLIAGE_RATE = 16;
var TREES = [
{ frame: [155, 3, 21, 25], ax: 0.48, ay: 0.96 },
// tree 1 (plain)
{ frame: [155, 35, 21, 25], ax: 0.48, ay: 0.96 },
// tree 2 (fruited)
{ frame: [152, 32, 24, 32], ax: 0.5, ay: 0.94 }
// tree 3 — full 3×4 fruited (19,4)
];
var TREE_SCALE_BASE = 1;
var TREE_SCALE_JITTER = 0.14;
var TREE_FLIP_RATE = 0.5;
var TREE_JITTER = 2;
var FOREST_BLOCK_X = 3;
var FOREST_BLOCK_Y = 2;
var FOREST_THRESHOLD = 0.6;
var FOREST_MIN_NB = 2;
var FOREST_FILL = 0.5;
var STONES = [
{ c: 8, r: 3, w: 2, h: 2, weight: 3 },
// small rock
{ c: 10, r: 3, w: 2, h: 2, weight: 3 },
// small rock
{ c: 12, r: 3, w: 2, h: 2, weight: 3 },
// small flat rock
{ c: 14, r: 3, w: 2, h: 2, weight: 3 },
// small rock
{ c: 16, r: 3, w: 2, h: 2, weight: 3 },
// small rock
{ c: 6, r: 3, w: 2, h: 3, weight: 2 },
// medium boulder
{ c: 0, r: 3, w: 2, h: 4, weight: 1 },
// big boulder
{ c: 2, r: 3, w: 2, h: 4, weight: 1 },
// big boulder
{ c: 4, r: 3, w: 2, h: 4, weight: 1 }
// big knobbly boulder
];
var STONE_WEIGHT = STONES.reduce((s, g) => s + g.weight, 0);
var STONE_BLOCK_X = 2;
var STONE_BLOCK_Y = 2;
var STONE_CLUMP_THRESHOLD = 0.7;
var STONE_CLUMP_MIN_NB = 3;
var STONE_CLUMP_FILL = 0.7;
var COL_GRASS2 = [97, 150, 55];
var COL_DIRT2 = [118, 80, 38];
var COL_STONE = [120, 120, 122];
var COL_WATER = [74, 116, 196];
function loadImg(url) {
return new Promise((resolve, reject) => {
const img = new Image();
img.onload = () => resolve(img);
img.onerror = reject;
img.src = url;
});
}
async function buildShadowMask(url) {
const img = await loadImg(url);
const cv = document.createElement("canvas");
cv.width = img.naturalWidth;
cv.height = img.naturalHeight;
const g = cv.getContext("2d", { willReadFrequently: true });
g.drawImage(img, 0, 0);
const cols = img.naturalWidth / TILE3 | 0, rows = img.naturalHeight / TILE3 | 0, set = /* @__PURE__ */ new Set();
for (let r = 0; r < rows; r++) for (let c = 0; c < cols; c++) {
const d = g.getImageData(c * TILE3, r * TILE3, TILE3, TILE3).data;
for (let i = 3; i < d.length; i += 4) {
if (d[i] > 8) {
set.add(c + "," + r);
break;
}
}
}
return set;
}
function chunkFields2(seed, x0, y0) {
const WATER_BUFFER = 2;
const M = WATER_BUFFER + 4, SZ = CHUNK2 + 2 * M;
const idx = (i, j) => j * SZ + i;
const clean = (pred) => {
let cur = new Uint8Array(SZ * SZ);
for (let j = 0; j < SZ; j++) for (let i = 0; i < SZ; i++) cur[idx(i, j)] = pred(x0 + i - M, y0 + j - M) ? 1 : 0;
let nxt = new Uint8Array(SZ * SZ);
for (let j = 1; j < SZ - 1; j++) for (let i = 1; i < SZ - 1; i++) {
let c = 0;
for (let dj = -1; dj <= 1; dj++) for (let di = -1; di <= 1; di++) c += cur[idx(i + di, j + dj)];
nxt[idx(i, j)] = c >= 5 ? 1 : 0;
}
cur = nxt;
for (let p = 0; p < 2; p++) {
nxt = new Uint8Array(SZ * SZ);
for (let j = 1; j < SZ - 1; j++) for (let i = 1; i < SZ - 1; i++) {
const card = cur[idx(i, j - 1)] + cur[idx(i + 1, j)] + cur[idx(i, j + 1)] + cur[idx(i - 1, j)];
nxt[idx(i, j)] = cur[idx(i, j)] ? card >= 2 ? 1 : 0 : card >= 3 ? 1 : 0;
}
cur = nxt;
}
return cur;
};
const waterF = clean((x, y) => isRiver(seed, x, y));
const raisedField = clean((x, y) => isRaised(seed, x, y));
for (let k = 0; k < waterF.length; k++) if (raisedField[k]) waterF[k] = 0;
const dirtRaw = clean((x, y) => isDirt(seed, x, y));
const stoneRaw = clean((x, y) => isStone(seed, x, y));
const fraw = new Uint8Array(SZ * SZ);
for (let j = 0; j < SZ; j++) for (let i = 0; i < SZ; i++) fraw[idx(i, j)] = forestField(seed, x0 + i - M, y0 + j - M) > FOREST_THRESHOLD ? 1 : 0;
const forestRegion = new Uint8Array(SZ * SZ);
for (let j = 1; j < SZ - 1; j++) for (let i = 1; i < SZ - 1; i++) {
if (!fraw[idx(i, j)]) continue;
let c = 0;
for (let dj = -1; dj <= 1; dj++) for (let di = -1; di <= 1; di++) if ((di || dj) && fraw[idx(i + di, j + dj)]) c++;
forestRegion[idx(i, j)] = c >= FOREST_MIN_NB ? 1 : 0;
}
const sraw = new Uint8Array(SZ * SZ);
for (let j = 0; j < SZ; j++) for (let i = 0; i < SZ; i++) sraw[idx(i, j)] = stoneClumpField(seed, x0 + i - M, y0 + j - M) > STONE_CLUMP_THRESHOLD ? 1 : 0;
const stoneClumpRegion = new Uint8Array(SZ * SZ);
for (let j = 1; j < SZ - 1; j++) for (let i = 1; i < SZ - 1; i++) {
if (!sraw[idx(i, j)]) continue;
let c = 0;
for (let dj = -1; dj <= 1; dj++) for (let di = -1; di <= 1; di++) if ((di || dj) && sraw[idx(i + di, j + dj)]) c++;
stoneClumpRegion[idx(i, j)] = c >= STONE_CLUMP_MIN_NB ? 1 : 0;
}
const dirt = new Uint8Array(SZ * SZ), stone = new Uint8Array(SZ * SZ);
for (let j = WATER_BUFFER; j < SZ - WATER_BUFFER; j++) for (let i = WATER_BUFFER; i < SZ - WATER_BUFFER; i++) {
let nearW = 0;
for (let dj = -WATER_BUFFER; dj <= WATER_BUFFER && !nearW; dj++) for (let di = -WATER_BUFFER; di <= WATER_BUFFER; di++) if (waterF[idx(i + di, j + dj)]) {
nearW = 1;
break;
}
const rais = raisedField[idx(i, j)];
dirt[idx(i, j)] = dirtRaw[idx(i, j)] && !nearW && !rais ? 1 : 0;
stone[idx(i, j)] = stoneRaw[idx(i, j)] && !dirtRaw[idx(i, j)] && !nearW && !rais ? 1 : 0;
}
return { waterF, dirt, stone, raisedField, forestRegion, stoneClumpRegion, M, SZ, x0, y0 };
}
var FP_GROUND_FILLS = [
{ key: "grass", url: TILES2, tile: GRASS_BASE },
{ key: "dirt", url: TILES2, tile: DIRT_FILL },
{ key: "stone", url: TILES2, tile: STONE_FILL }
];
function fpGroundFillKey(seed, x, y) {
if (isStone(seed, x, y) && !isDirt(seed, x, y)) return "stone";
if (isDirt(seed, x, y)) return "dirt";
return "grass";
}
var fpConfig = (seed) => ({
seed,
tile: TILE3,
chunk: CHUNK2,
background: "#5a7b3a",
async load({ Assets }) {
const ctx = { tiles: null, shadow: null, shadowSet: null, props: null, propShadow: null };
const t = await Assets.load(TILES2);
t.source.scaleMode = "nearest";
ctx.tiles = t.source;
try {
const s = await Assets.load(SHADOW);
s.source.scaleMode = "nearest";
ctx.shadow = s.source;
} catch {
}
try {
ctx.shadowSet = await buildShadowMask(SHADOW);
} catch {
ctx.shadowSet = null;
}
try {
const p = await Assets.load(PROPS);
p.source.scaleMode = "nearest";
ctx.props = p.source;
} catch {
}
try {
const p = await Assets.load(PROP_SHADOW);
p.source.scaleMode = "nearest";
ctx.propShadow = p.source;
} catch {
}
return ctx;
},
// `accept(tx,ty)` (chunk-local) gates which tiles this biome paints — defaults to all, so the
// standalone map is unchanged; the multi-biome overworld passes a per-biome dither mask.
bake({ x0, y0, seed: seed2, ctx, tmp, Sprite, tex, texFrame, add, accept = () => true }) {
const f = chunkFields2(seed2, x0, y0);
const { waterF, dirt, stone, raisedField, forestRegion, stoneClumpRegion, M, SZ } = f;
const idx = (i, j) => j * SZ + i;
const atW = (wx, wy) => waterF[idx(wx - x0 + M, wy - y0 + M)] === 1;
const atD = (wx, wy) => dirt[idx(wx - x0 + M, wy - y0 + M)] === 1;
const atS = (wx, wy) => stone[idx(wx - x0 + M, wy - y0 + M)] === 1;
const isRaisedAt = (wx, wy) => raisedField[idx(wx - x0 + M, wy - y0 + M)] === 1;
const place = (coord, tx, ty) => {
add(ctx.tiles, coord[0], coord[1], tx, ty);
if (ctx.shadow && (!ctx.shadowSet || ctx.shadowSet.has(coord[0] + "," + coord[1]))) {
const sh = new Sprite(tex(ctx.shadow, coord[0], coord[1]));
sh.x = tx * TILE3;
sh.y = ty * TILE3;
tmp.addChild(sh);
}
};
const blob = (atFn, BLOCK, fillBase, fillVars, salt, wx, wy, tx, ty) => {
const N = !atFn(wx, wy - 1), E = !atFn(wx + 1, wy), S = !atFn(wx, wy + 1), W = !atFn(wx - 1, wy);
if (N && E && S && W) {
place(sparse(fillBase, fillVars, wx, wy, salt, FILL_RATE, seed2), tx, ty);
return;
}
let cr;
if (!N && !E && !S && !W) {
const dNW = !atFn(wx - 1, wy - 1), dNE = !atFn(wx + 1, wy - 1), dSW = !atFn(wx - 1, wy + 1), dSE = !atFn(wx + 1, wy + 1);
if (!dNW && !dNE && !dSW && !dSE) {
place(sparse(fillBase, fillVars, wx, wy, salt, FILL_RATE, seed2), tx, ty);
return;
}
cr = centerTile(BLOCK, dNW, dNE, dSW, dSE);
} else cr = blockTile(BLOCK, N, E, S, W);
place(cr, tx, ty);
};
for (let ty = 0; ty < CHUNK2; ty++) for (let tx = 0; tx < CHUNK2; tx++) {
if (!accept(tx, ty)) continue;
const wx = x0 + tx, wy = y0 + ty;
place(sparse(GRASS_BASE, GRASS_VARS, wx, wy, 0, GRASS_RATE, seed2), tx, ty);
if (atD(wx, wy)) blob(atD, DIRT_BLOCK, DIRT_FILL, DIRT_VARS2, 1, wx, wy, tx, ty);
if (atS(wx, wy)) blob(atS, STONE_BLOCK, STONE_FILL, STONE_VARS, 2, wx, wy, tx, ty);
if (atW(wx, wy)) blob(atW, WATER_BLOCK, WATER_FILL, WATER_VARS, 3, wx, wy, tx, ty);
}
const live = [];
const propSprite = (spec, wx, wy) => {
const sp = new Sprite(texFrame(ctx.props, spec.c * TILE3, (spec.r - spec.h + 1) * TILE3, spec.w * TILE3, spec.h * TILE3));
sp.anchor.set(0.5, 1);
sp.x = wx * TILE3 + TILE3 / 2;
sp.y = (wy + 1) * TILE3;
sp.zIndex = (wy + 1) * TILE3;
return sp;
};
if (ctx.props) for (let ty = 0; ty < CHUNK2; ty++) for (let tx = 0; tx < CHUNK2; tx++) {
if (!accept(tx, ty)) continue;
const wx = x0 + tx, wy = y0 + ty;
if (atW(wx, wy)) {
if (hashU32(seed2 ^ 24301, wx, wy) % WATER_FOLIAGE_RATE === 0) live.push({ sprite: propSprite(WATER_FOLIAGE, wx, wy), shadow: null });
continue;
}
if (atD(wx, wy) || atS(wx, wy)) continue;
const h2 = hashU32(seed2 ^ 15733114, wx, wy);
if (h2 % FOLIAGE_RATE !== 0) continue;
let pick = (h2 >>> 8) % FOLIAGE_WEIGHT, g = FOLIAGE[0];
for (const grp of FOLIAGE) {
if (pick < grp.weight) {
g = grp;
break;
}
pick -= grp.weight;
}
if (g.sprite) {
live.push({ sprite: propSprite(g.sprite, wx, wy), shadow: null });
continue;
}
const [c, r] = g.tiles[(h2 >>> 16) % g.tiles.length];
const sp = new Sprite(texFrame(ctx.props, c * TILE3, r * TILE3, TILE3, TILE3));
sp.x = tx * TILE3;
sp.y = ty * TILE3;
tmp.addChild(sp);
}
const stairAt = (wx, wy) => {
const off = hashU32(seed2 ^ 358936, Math.floor(wy / STAIR_SPACING), 0) % STAIR_SPACING;
return ((wx - off) % STAIR_SPACING + STAIR_SPACING) % STAIR_SPACING === 0;
};
const stairAtV = (wx, wy) => {
const off = hashU32(seed2 ^ 358940, Math.floor(wx / STAIR_SPACING), 0) % STAIR_SPACING;
return ((wy - off) % STAIR_SPACING + STAIR_SPACING) % STAIR_SPACING === 0;
};
bakeCliffs(FP_CLIFF, { chunk: CHUNK2, x0, y0, seed: seed2, raised: isRaisedAt, place, accept, stairAt, stairAtV });
if (ctx.props) {
const inGrove = (wx, wy) => forestRegion[idx(wx - x0 + M, wy - y0 + M)] === 1;
const bx0 = Math.floor(x0 / FOREST_BLOCK_X), bx1 = Math.floor((x0 + CHUNK2 - 1) / FOREST_BLOCK_X);
const by0 = Math.floor(y0 / FOREST_BLOCK_Y), by1 = Math.floor((y0 + CHUNK2 - 1) / FOREST_BLOCK_Y);
for (let by = by0; by <= by1; by++) for (let bx = bx0; bx <= bx1; bx++) {
const h2 = hashU32(seed2 ^ 15748695, bx, by);
const wx = bx * FOREST_BLOCK_X + h2 % FOREST_BLOCK_X, wy = by * FOREST_BLOCK_Y + (h2 >>> 4) % FOREST_BLOCK_Y;
if (wx < x0 || wx >= x0 + CHUNK2 || wy < y0 || wy >= y0 + CHUNK2) continue;
if (!accept(wx - x0, wy - y0)) continue;
if (!inGrove(wx, wy)) continue;
if ((h2 >>> 8 & 65535) / 65536 >= FOREST_FILL) continue;
if (atW(wx, wy) || atS(wx, wy)) continue;
if (!isRaisedAt(wx, wy)) {
let onFace = false;
for (let k = 1; k <= FP_CLIFF.FACE_H; k++) if (isRaisedAt(wx, wy - k)) {
onFace = true;
break;
}
if (onFace) continue;
}
const T = TREES[(h2 >>> 24) % TREES.length];
const flip = (h2 >>> 20 & 255) / 256 < TREE_FLIP_RATE;
const sc = TREE_SCALE_BASE * (1 + ((h2 >>> 12 & 255) / 256 - 0.5) * 2 * TREE_SCALE_JITTER);
const px = wx * TILE3 + TILE3 / 2 + ((h2 >>> 16 & 15) / 16 - 0.5) * 2 * TREE_JITTER;
const py = wy * TILE3 + TILE3 / 2 + ((h2 >>> 28 & 15) / 16 - 0.5) * 2 * TREE_JITTER;
const tr = new Sprite(texFrame(ctx.props, ...T.frame));
tr.anchor.set(T.ax, T.ay);
tr.scale.set(flip ? -sc : sc, sc);
tr.x = px;
tr.y = py;
tr.zIndex = py;
let shadow = null;
if (ctx.propShadow) {
shadow = new Sprite(texFrame(ctx.propShadow, ...T.frame));
shadow.anchor.set(T.ax, T.ay);
shadow.scale.set(flip ? -sc : sc, sc);
shadow.x = px;
shadow.y = py;
}
live.push({ sprite: tr, shadow });
}
}
if (ctx.props) {
const inClump = (wx, wy) => stoneClumpRegion[idx(wx - x0 + M, wy - y0 + M)] === 1;
const sbx0 = Math.floor(x0 / STONE_BLOCK_X), sbx1 = Math.floor((x0 + CHUNK2 - 1) / STONE_BLOCK_X);
const sby0 = Math.floor(y0 / STONE_BLOCK_Y), sby1 = Math.floor((y0 + CHUNK2 - 1) / STONE_BLOCK_Y);
for (let by = sby0; by <= sby1; by++) for (let bx = sbx0; bx <= sbx1; bx++) {
const h2 = hashU32(seed2 ^ 5702885, bx, by);
const wx = bx * STONE_BLOCK_X + h2 % STONE_BLOCK_X, wy = by * STONE_BLOCK_Y + (h2 >>> 4) % STONE_BLOCK_Y;
if (wx < x0 || wx >= x0 + CHUNK2 || wy < y0 || wy >= y0 + CHUNK2) continue;
if (!accept(wx - x0, wy - y0)) continue;
if (!inClump(wx, wy)) continue;
if ((h2 >>> 8 & 65535) / 65536 >= STONE_CLUMP_FILL) continue;
if (atW(wx, wy)) continue;
if (!isRaisedAt(wx, wy)) {
let onFace = false;
for (let k = 1; k <= FP_CLIFF.FACE_H; k++) if (isRaisedAt(wx, wy - k)) {
onFace = true;
break;
}
if (onFace) continue;
}
let pick = (h2 >>> 20) % STONE_WEIGHT, st = STONES[0];
for (const s of STONES) {
if (pick < s.weight) {
st = s;
break;
}
pick -= s.weight;
}
live.push({ sprite: propSprite(st, wx, wy), shadow: null });
}
}
return { meta: f, live };
},
macroColor(seed2, tx, ty) {
const raised = isRaised(seed2, tx, ty);
if (!raised && isRiver(seed2, tx, ty)) return COL_WATER;
let c = isStone(seed2, tx, ty) && !isDirt(seed2, tx, ty) ? COL_STONE : isDirt(seed2, tx, ty) ? COL_DIRT2 : COL_GRASS2;
if (raised) c = lerp3(c, COL_CLIFF, 0.25);
return c;
},
tileIndexAt(wx, wy, meta) {
if (!meta) return null;
const { waterF, dirt, stone, M, SZ, x0, y0 } = meta;
if (wx - x0 < 0 || wy - y0 < 0 || wx - x0 >= CHUNK2 || wy - y0 >= CHUNK2) return null;
const idx = (i, j) => j * SZ + i;
const pick = (fld, BLOCK, fill) => {
const at = (x, y) => fld[idx(x - x0 + M, y - y0 + M)] === 1;
const N = !at(wx, wy - 1), E = !at(wx + 1, wy), S = !at(wx, wy + 1), W = !at(wx - 1, wy);
if (N && E && S && W) return fill;
if (!N && !E && !S && !W) {
const dNW = !at(wx - 1, wy - 1), dNE = !at(wx + 1, wy - 1), dSW = !at(wx - 1, wy + 1), dSE = !at(wx + 1, wy + 1);
if (!dNW && !dNE && !dSW && !dSE) return fill;
return centerTile(BLOCK, dNW, dNE, dSW, dSE);
}
return blockTile(BLOCK, N, E, S, W);
};
if (waterF[idx(wx - x0 + M, wy - y0 + M)] === 1) return pick(waterF, WATER_BLOCK, WATER_FILL);
if (stone[idx(wx - x0 + M, wy - y0 + M)] === 1) return pick(stone, STONE_BLOCK, STONE_FILL);
if (dirt[idx(wx - x0 + M, wy - y0 + M)] === 1) return pick(dirt, DIRT_BLOCK, DIRT_FILL);
return GRASS_BASE;
}
});
function createForgottenPlainsMap(pixi, host, opts = {}) {
return createChunkedMap(pixi, host, { ...fpConfig(opts.seed ?? 1), keyboardPan: opts.keyboardPan });
}
// ../auto-battler/src/engine/necropolisGen.js
var sub4 = (seed, salt) => (Math.imul(seed | 0, 2654435761) ^ (salt | 0)) >>> 0;
var RIVER_SCALE2 = 0.045;
var RIVER_WIDTH2 = 0.05;
var WARP_SCALE3 = 0.03;
var WARP_AMP3 = 16;
var DARK_SCALE = 0.06;
var DARK_THRESHOLD = 0.6;
function isDark(seed, x, y) {
return fbm(sub4(seed, 55852), x * DARK_SCALE, y * DARK_SCALE) > DARK_THRESHOLD;
}
var ELEV_SCALE2 = 0.025;
var ELEV_THRESHOLD2 = 0.6;
function isRaised2(seed, x, y) {
return fbm(sub4(seed, 57836), x * ELEV_SCALE2, y * ELEV_SCALE2) > ELEV_THRESHOLD2;
}
var BONE_SCALE = 0.055;
var BONE_THRESHOLD = 0.62;
function isBone(seed, x, y) {
return fbm(sub4(seed, 45134), x * BONE_SCALE, y * BONE_SCALE) > BONE_THRESHOLD;
}
var FOREST_SCALE2 = 0.05;
function forestField2(seed, x, y) {
return fbm(sub4(seed, 15740503), x * FOREST_SCALE2, y * FOREST_SCALE2);
}
function isRiver2(seed, x, y) {
const wx = x + WARP_AMP3 * (fbm(sub4(seed, 1297), x * WARP_SCALE3, y * WARP_SCALE3) - 0.5);
const wy = y + WARP_AMP3 * (fbm(sub4(seed, 1298), x * WARP_SCALE3, y * WARP_SCALE3) - 0.5);
return Math.abs(fbm(sub4(seed, 8654), wx * RIVER_SCALE2, wy * RIVER_SCALE2) - 0.5) < RIVER_WIDTH2;
}
// ../auto-battler/src/render/necropolis.js
var NECRO = "/assets/minifantasy/Minifantasy_Necropolis_v1.0/Minifantasy_Necropolis_Assets";
var EXT_DIR = `${NECRO}/PremadeScenes/Exterior/SeparateLayers`;
var BIOME = `${NECRO}/Tileset/Biome/CorruptedBiome.png`;
var SHADOW2 = `${NECRO}/Tileset/Biome/CorruptedBiomeShadows.png`;
var PROPS2 = `${NECRO}/Props/Props.png`;
var PROP_SHADOW2 = `${NECRO}/Props/PropShadows.png`;
var NECRO_MAP_ASSETS = [BIOME, SHADOW2, PROPS2, PROP_SHADOW2];
var TILE4 = 8;
var CHUNK3 = 32;
var TREES2 = [
{ frame: [9, 15, 14, 14], shadow: [8, 24, 3, 5], ax: 0.04, ay: 0.82 },
// 1,1 (2×3)
{ frame: [28, 11, 17, 19], shadow: [25, 24, 6, 6], ax: 0, ay: 0.84 },
// 3,1 (3×3)
{ frame: [51, 13, 18, 16], shadow: [55, 24, 7, 5], ax: 0.42, ay: 0.84 },
// 6,1 (3×3)
{ frame: [75, 14, 10, 15], shadow: [72, 25, 7, 4], ax: 0.05, ay: 0.87 }
// 9,1 (2×3)
];
var TREE_SCALE_BASE2 = 1;
var TREE_SCALE_JITTER2 = 0.15;
var TREE_FLIP_RATE2 = 0.5;
var TREE_JITTER2 = 2;
var FOREST_BLOCK_X2 = 2;
var FOREST_BLOCK_Y2 = 1;
var FOREST_THRESHOLD2 = 0.62;
var FOREST_MIN_NB2 = 2;
var FOREST_FILL2 = 0.55;
var TREE_WATER_BUFFER = 1;
var FOLIAGE2 = [
{ tiles: [[1, 5], [2, 5], [1, 6], [2, 6]], shadow: false, weight: 3 },
// grass tufts (most common)
{ tiles: [[6, 5], [7, 5], [6, 6], [7, 6]], shadow: true, weight: 1 },
// species A
{ tiles: [[9, 5], [10, 5], [9, 6], [10, 6]], shadow: true, weight: 1 }
// species B
];
var FOLIAGE_WEIGHT2 = FOLIAGE2.reduce((s, g) => s + g.weight, 0);
var FOLIAGE_RATE2 = 12;
var LIP_T = [2, 10];
var LIP_T_VAR = [[3, 10], [4, 10], [5, 10]];
var LIP_TL = [1, 10];
var LIP_TR = [6, 10];
var WALL_L = [1, 14];
var WALL_L_VAR = [[1, 11], [1, 12], [1, 13]];
var WALL_R = [6, 14];
var WALL_R_VAR = [[6, 11], [6, 12], [6, 13]];
var LIP_S = [2, 15];
var LIP_S_VAR = [[3, 15], [4, 15], [5, 15]];
var LIP_SW = [1, 15];
var LIP_SE = [6, 15];
var FACE_H = 2;
var FACE = [[3, 16], [3, 17]];
var FACE_VAR = [[[2, 16], [4, 16], [5, 16]], [[2, 17], [4, 17], [5, 17]]];
var FACE_L = [[1, 16], [1, 17]];
var FACE_R = [[6, 16], [6, 17]];
var CLIFF_SPARSE_RATE = 2;
var NECRO_CLIFF = {
LIP_T,
LIP_T_VAR,
LIP_TL,
LIP_TR,
WALL_L,
WALL_L_VAR,
WALL_R,
WALL_R_VAR,
LIP_S,
LIP_S_VAR,
LIP_SW,
LIP_SE,
FACE_H,
FACE,
FACE_VAR,
FACE_L,
FACE_R,
RATE: CLIFF_SPARSE_RATE
};
var CORRUPT_LIGHT = [1, 1];
var CORRUPT_DARK = [2, 1];
var NECRO_GROUND_FILLS = [
{ key: "light", url: BIOME, tile: CORRUPT_LIGHT },
{ key: "dark", url: BIOME, tile: CORRUPT_DARK }
];
function necroGroundFillKey(seed, x, y) {
return isDark(seed, x, y) ? "dark" : "light";
}
var LIGHT_VARIANTS = [[1, 2], [2, 2], [1, 3], [2, 3], [1, 4], [2, 4], [1, 5], [2, 5]];
var LIGHT_SPARSE_RATE = 10;
var WATER_BLOCK2 = [4, 2];
var BONE_BLOCK = [13, 2];
var BONE_FILL = [[12, 1], [13, 1], [14, 1], [15, 1]];
var EDGE_N = [18, 4];
var EDGE_S = [18, 2];
var EDGE_E = [17, 3];
var EDGE_W = [19, 3];
var CONV_NW = [18, 6];
var CONV_NE = [17, 6];
var CONV_SW = [18, 5];
var CONV_SE = [17, 5];
var CONC_NW = [19, 4];
var CONC_NE = [17, 4];
var CONC_SW = [19, 2];
var CONC_SE = [17, 2];
var CONC_NWSE = [19, 6];
var CONC_NESW = [19, 5];
var COL_LIGHT = [130, 119, 136];
var COL_DARK = [105, 99, 113];
var COL_WATER2 = [74, 142, 48];
var COL_BONE = [180, 156, 126];
var COL_CLIFF2 = [175, 146, 109];
var COL_FOREST = [96, 101, 70];
var NECRO_LAYERS = [
["m-bg", "Background", "ground"],
["l-corruptedland", "Corrupted land", "ground"],
["k-coruptewater", "Corrupted water", "ground"],
["j-bonepiles", "Bone piles", "scatter"],
["i-perimeter", "Perimeter", "structure"],
["h-path2", "Path (under)", "paths"],
["g-path", "Path", "paths"],
["f-ziggurat", "Ziggurat", "structure"],
["e-walls", "Walls", "structure"],
["d-cliff", "Cliff", "structure"],
["c-props", "Props", "scatter"],
["b-shadows", "Shadows", "lighting"],
["a-undeadflames", "Undead flames", "lighting"]
];
var NECRO_EXT_DIR = EXT_DIR;
var NECRO_SCENE = 360;
function corruptTile(N, E, S, W, NW, NE, SW, SE) {
const card = (N ? 1 : 0) + (E ? 1 : 0) + (S ? 1 : 0) + (W ? 1 : 0);
if (card === 1) return N ? EDGE_N : S ? EDGE_S : E ? EDGE_E : EDGE_W;
if (card === 2) {
if (N && W) return CONV_NW;
if (N && E) return CONV_NE;
if (S && W) return CONV_SW;
if (S && E) return CONV_SE;
return N ? EDGE_N : EDGE_E;
}
if (card >= 3) return N ? EDGE_N : S ? EDGE_S : E ? EDGE_E : EDGE_W;
const diag = (NW ? 1 : 0) + (NE ? 1 : 0) + (SW ? 1 : 0) + (SE ? 1 : 0);
if (diag === 0) return null;
if (diag === 1) return NW ? CONC_NW : NE ? CONC_NE : SW ? CONC_SW : CONC_SE;
if (NW && SE && !NE && !SW) return CONC_NWSE;
if (NE && SW && !NW && !SE) return CONC_NESW;
return null;
}
function boneCenterTile([c0, r0], dNW, dNE, dSW, dSE) {
const n = dNW + dNE + dSW + dSE;
if (n === 1) {
if (dNW) return [c0, r0 + 3];
if (dNE) return [c0 + 1, r0 + 3];
if (dSW) return [c0, r0 + 4];
return [c0 + 1, r0 + 4];
}
if (dNE && dSW && !dNW && !dSE) return [c0 + 2, r0 + 4];
if (dNW && dSE && !dNE && !dSW) return [c0 + 2, r0 + 3];
return [c0 + 1, r0 + 1];
}
function biomeColor(seed, tx, ty) {
const raised = isRaised2(seed, tx, ty);
if (!raised && isRiver2(seed, tx, ty)) return COL_WATER2;
if (isBone(seed, tx, ty)) return COL_BONE;
let c = isDark(seed, tx, ty) ? COL_DARK : COL_LIGHT;
if (forestField2(seed, tx, ty) > FOREST_THRESHOLD2) c = lerp3(c, COL_FOREST, 0.5);
if (raised) c = lerp3(c, COL_CLIFF2, 0.22);
return c;
}
function loadImg2(url) {
return new Promise((resolve, reject) => {
const img = new Image();
img.onload = () => resolve(img);
img.onerror = reject;
img.src = url;
});
}
async function buildShadowMask2(url) {
const img = await loadImg2(url);
const cv = document.createElement("canvas");
cv.width = img.naturalWidth;
cv.height = img.naturalHeight;
const g = cv.getContext("2d", { willReadFrequently: true });
g.drawImage(img, 0, 0);
const cols = img.naturalWidth / TILE4 | 0, rows = img.naturalHeight / TILE4 | 0, set = /* @__PURE__ */ new Set();
for (let r = 0; r < rows; r++) for (let c = 0; c < cols; c++) {
const d = g.getImageData(c * TILE4, r * TILE4, TILE4, TILE4).data;
for (let i = 3; i < d.length; i += 4) {
if (d[i] > 8) {
set.add(c + "," + r);
break;
}
}
}
return set;
}
function chunkFields3(seed, x0, y0) {
const WATER_BUFFER = 2;
const ELEV_BUFFER = 3;
const M = Math.max(WATER_BUFFER, ELEV_BUFFER) + 4, SZ = CHUNK3 + 2 * M;
const idx = (i, j) => j * SZ + i;
const clean = (pred) => {
let cur = new Uint8Array(SZ * SZ);
for (let j = 0; j < SZ; j++) for (let i = 0; i < SZ; i++) cur[idx(i, j)] = pred(x0 + i - M, y0 + j - M) ? 1 : 0;
let nxt = new Uint8Array(SZ * SZ);
for (let j = 1; j < SZ - 1; j++) for (let i = 1; i < SZ - 1; i++) {
let c = 0;
for (let dj = -1; dj <= 1; dj++) for (let di = -1; di <= 1; di++) c += cur[idx(i + di, j + dj)];
nxt[idx(i, j)] = c >= 5 ? 1 : 0;
}
cur = nxt;
for (let p = 0; p < 2; p++) {
nxt = new Uint8Array(SZ * SZ);
for (let j = 1; j < SZ - 1; j++) for (let i = 1; i < SZ - 1; i++) {
const card = cur[idx(i, j - 1)] + cur[idx(i + 1, j)] + cur[idx(i, j + 1)] + cur[idx(i - 1, j)];
nxt[idx(i, j)] = cur[idx(i, j)] ? card >= 2 ? 1 : 0 : card >= 3 ? 1 : 0;
}
cur = nxt;
}
return cur;
};
const darkRaw = clean((x, y) => isDark(seed, x, y));
const waterField = clean((x, y) => isRiver2(seed, x, y));
const raisedField = clean((x, y) => isRaised2(seed, x, y));
const boneField = clean((x, y) => isBone(seed, x, y));
const fraw = new Uint8Array(SZ * SZ);
for (let j = 0; j < SZ; j++) for (let i = 0; i < SZ; i++) fraw[idx(i, j)] = forestField2(seed, x0 + i - M, y0 + j - M) > FOREST_THRESHOLD2 ? 1 : 0;
const forestRegion = new Uint8Array(SZ * SZ);
for (let j = 1; j < SZ - 1; j++) for (let i = 1; i < SZ - 1; i++) {
if (!fraw[idx(i, j)]) continue;
let c = 0;
for (let dj = -1; dj <= 1; dj++) for (let di = -1; di <= 1; di++) if ((di || dj) && fraw[idx(i + di, j + dj)]) c++;
forestRegion[idx(i, j)] = c >= FOREST_MIN_NB2 ? 1 : 0;
}
const field = new Uint8Array(SZ * SZ);
const boneMask = new Uint8Array(SZ * SZ);
const B = Math.max(WATER_BUFFER, ELEV_BUFFER);
for (let j = B; j < SZ - B; j++) for (let i = B; i < SZ - B; i++) {
let nearW = 0;
for (let dj = -WATER_BUFFER; dj <= WATER_BUFFER && !nearW; dj++) for (let di = -WATER_BUFFER; di <= WATER_BUFFER; di++) if (waterField[idx(i + di, j + dj)]) {
nearW = 1;
break;
}
let nearCliff = 0;
{
const me = raisedField[idx(i, j)];
for (let dj = -ELEV_BUFFER; dj <= ELEV_BUFFER && !nearCliff; dj++) for (let di = -ELEV_BUFFER; di <= ELEV_BUFFER; di++) if (raisedField[idx(i + di, j + dj)] !== me) {
nearCliff = 1;
break;
}
}
const water = waterField[idx(i, j)];
field[idx(i, j)] = darkRaw[idx(i, j)] && !nearW && !water && !nearCliff ? 0 : 1;
boneMask[idx(i, j)] = boneField[idx(i, j)] && !water && !nearW && !nearCliff ? 1 : 0;
}
for (let pass = 0; pass < 3; pass++) {
const prev = boneMask.slice();
let changed = false;
for (let j = B + 1; j < SZ - B - 1; j++) for (let i = B + 1; i < SZ - B - 1; i++) {
if (!prev[idx(i, j)]) continue;
const up = prev[idx(i, j - 1)], down = prev[idx(i, j + 1)], left = prev[idx(i - 1, j)], right = prev[idx(i + 1, j)];
if (!up && !down || !left && !right) {
boneMask[idx(i, j)] = 0;
changed = true;
}
}
if (!changed) break;
}
return { field, boneMask, waterField, raisedField, forestRegion, M, SZ, x0, y0 };
}
var necropolisConfig = (seed) => ({
seed,
tile: TILE4,
chunk: CHUNK3,
background: "#69636f",
async load({ Assets }) {
const ctx = { biome: null, shadow: null, shadowSet: null, props: null, propShadow: null };
const b = await Assets.load(BIOME);
b.source.scaleMode = "nearest";
ctx.biome = b.source;
try {
const s = await Assets.load(SHADOW2);
s.source.scaleMode = "nearest";
ctx.shadow = s.source;
} catch {
}
try {
ctx.shadowSet = await buildShadowMask2(SHADOW2);
} catch {
ctx.shadowSet = null;
}
try {
const p = await Assets.load(PROPS2);
p.source.scaleMode = "nearest";
ctx.props = p.source;
} catch {
}
try {
const p = await Assets.load(PROP_SHADOW2);
p.source.scaleMode = "nearest";
ctx.propShadow = p.source;
} catch {
}
return ctx;
},
// `accept(tx,ty)` (chunk-local) gates which tiles this biome paints — defaults to all, so the
// standalone map is unchanged; the multi-biome overworld passes a per-biome dither mask.
bake({ x0, y0, seed: seed2, ctx, tmp, Sprite, tex, texFrame, add, accept = () => true }) {
const f = chunkFields3(seed2, x0, y0);
const { field, boneMask, waterField, raisedField, forestRegion, M, SZ } = f;
const idx = (i, j) => j * SZ + i;
const isLight = (wx, wy) => field[idx(wx - x0 + M, wy - y0 + M)] === 1;
const isRaisedAt = (wx, wy) => raisedField[idx(wx - x0 + M, wy - y0 + M)] === 1;
const isWater = (wx, wy) => waterField[idx(wx - x0 + M, wy - y0 + M)] === 1 && !isRaisedAt(wx, wy);
const boneAt = (wx, wy) => boneMask[idx(wx - x0 + M, wy - y0 + M)] === 1;
const place = (coord, tx, ty) => {
add(ctx.biome, coord[0], coord[1], tx, ty);
if (ctx.shadow && (!ctx.shadowSet || ctx.shadowSet.has(coord[0] + "," + coord[1]))) {
const sh = new Sprite(tex(ctx.shadow, coord[0], coord[1]));
sh.x = tx * TILE4;
sh.y = ty * TILE4;
tmp.addChild(sh);
}
};
for (let ty = 0; ty < CHUNK3; ty++) for (let tx = 0; tx < CHUNK3; tx++) {
if (!accept(tx, ty)) continue;
const wx = x0 + tx, wy = y0 + ty;
if (!isLight(wx, wy)) {
place(CORRUPT_DARK, tx, ty);
continue;
}
const N = !isLight(wx, wy - 1), E = !isLight(wx + 1, wy), S = !isLight(wx, wy + 1), Wf = !isLight(wx - 1, wy);
const NW = !isLight(wx - 1, wy - 1), NE = !isLight(wx + 1, wy - 1), SW = !isLight(wx - 1, wy + 1), SE = !isLight(wx + 1, wy + 1);
const t = corruptTile(N, E, S, Wf, NW, NE, SW, SE);
if (!t) {
place(sparse(CORRUPT_LIGHT, LIGHT_VARIANTS, wx, wy, 0, LIGHT_SPARSE_RATE, seed2), tx, ty);
continue;
}
place(CORRUPT_DARK, tx, ty);
place(t, tx, ty);
}
for (let ty = 0; ty < CHUNK3; ty++) for (let tx = 0; tx < CHUNK3; tx++) {
if (!accept(tx, ty)) continue;
const wx = x0 + tx, wy = y0 + ty;
if (!isWater(wx, wy)) continue;
const N = !isWater(wx, wy - 1), E = !isWater(wx + 1, wy), S = !isWater(wx, wy + 1), Wf = !isWater(wx - 1, wy);
if (N && E && S && Wf) continue;
let cr;
if (!N && !E && !S && !Wf) cr = centerTile(WATER_BLOCK2, !isWater(wx - 1, wy - 1), !isWater(wx + 1, wy - 1), !isWater(wx - 1, wy + 1), !isWater(wx + 1, wy + 1));
else cr = blockTile(WATER_BLOCK2, N, E, S, Wf);
place(cr, tx, ty);
}
for (let ty = 0; ty < CHUNK3; ty++) for (let tx = 0; tx < CHUNK3; tx++) {
if (!accept(tx, ty)) continue;
const wx = x0 + tx, wy = y0 + ty;
if (!boneAt(wx, wy)) continue;
const N = !boneAt(wx, wy - 1), E = !boneAt(wx + 1, wy), S = !boneAt(wx, wy + 1), Wf = !boneAt(wx - 1, wy);
if (N && E && S && Wf) {
place(sparse(BONE_FILL[0], BONE_FILL, wx, wy, 11, 1, seed2), tx, ty);
continue;
}
let cr;
if (!N && !E && !S && !Wf) {
const dNW = !boneAt(wx - 1, wy - 1), dNE = !boneAt(wx + 1, wy - 1), dSW = !boneAt(wx - 1, wy + 1), dSE = !boneAt(wx + 1, wy + 1);
if (dNW || dNE || dSW || dSE) cr = boneCenterTile(BONE_BLOCK, dNW, dNE, dSW, dSE);
else {
place(sparse(BONE_FILL[0], BONE_FILL, wx, wy, 11, 1, seed2), tx, ty);
continue;
}
} else cr = blockTile(BONE_BLOCK, N, E, S, Wf);
place(cr, tx, ty);
}
if (ctx.props) for (let ty = 0; ty < CHUNK3; ty++) for (let tx = 0; tx < CHUNK3; tx++) {
if (!accept(tx, ty)) continue;
const wx = x0 + tx, wy = y0 + ty;
if (isWater(wx, wy) || boneAt(wx, wy)) continue;
const h2 = hashU32(seed2 ^ 15733114, wx, wy);
if (h2 % FOLIAGE_RATE2 !== 0) continue;
let pick = (h2 >>> 8) % FOLIAGE_WEIGHT2, g = FOLIAGE2[0];
for (const grp of FOLIAGE2) {
if (pick < grp.weight) {
g = grp;
break;
}
pick -= grp.weight;
}
const [c, r] = g.tiles[(h2 >>> 16) % g.tiles.length];
if (g.shadow && ctx.propShadow) {
const sh = new Sprite(texFrame(ctx.propShadow, c * TILE4, r * TILE4, TILE4, TILE4));
sh.x = tx * TILE4;
sh.y = ty * TILE4;
tmp.addChild(sh);
}
const sp = new Sprite(texFrame(ctx.props, c * TILE4, r * TILE4, TILE4, TILE4));
sp.x = tx * TILE4;
sp.y = ty * TILE4;
tmp.addChild(sp);
}
bakeCliffs(NECRO_CLIFF, { chunk: CHUNK3, x0, y0, seed: seed2, raised: isRaisedAt, place, accept });
const live = [];
if (ctx.props) {
const nearWater = (wx, wy) => {
for (let dj = -TREE_WATER_BUFFER; dj <= TREE_WATER_BUFFER; dj++) for (let di = -TREE_WATER_BUFFER; di <= TREE_WATER_BUFFER; di++) if (waterField[idx(wx + di - x0 + M, wy + dj - y0 + M)]) return true;
return false;
};
const nearCliff = (wx, wy) => {
const me = isRaisedAt(wx, wy);
for (let dj = -3; dj <= 3; dj++) for (let di = -3; di <= 3; di++) if (isRaisedAt(wx + di, wy + dj) !== me) return true;
return false;
};
const bx0 = Math.floor(x0 / FOREST_BLOCK_X2), bx1 = Math.floor((x0 + CHUNK3 - 1) / FOREST_BLOCK_X2);
const by0 = Math.floor(y0 / FOREST_BLOCK_Y2), by1 = Math.floor((y0 + CHUNK3 - 1) / FOREST_BLOCK_Y2);
for (let by = by0; by <= by1; by++) for (let bx = bx0; bx <= bx1; bx++) {
const h2 = hashU32(seed2 ^ 15748695, bx, by);
const wx = bx * FOREST_BLOCK_X2 + h2 % FOREST_BLOCK_X2, wy = by * FOREST_BLOCK_Y2 + (h2 >>> 4) % FOREST_BLOCK_Y2;
if (wx < x0 || wx >= x0 + CHUNK3 || wy < y0 || wy >= y0 + CHUNK3) continue;
if (!accept(wx - x0, wy - y0)) continue;
if (forestRegion[idx(wx - x0 + M, wy - y0 + M)] !== 1) continue;
if ((h2 >>> 8 & 65535) / 65536 >= FOREST_FILL2) continue;
if (nearWater(wx, wy) || boneAt(wx, wy) || nearCliff(wx, wy)) continue;
const T = TREES2[(h2 >>> 24) % TREES2.length];
const flip = (h2 >>> 20 & 255) / 256 < TREE_FLIP_RATE2;
const sc = TREE_SCALE_BASE2 * (1 + ((h2 >>> 12 & 255) / 256 - 0.5) * 2 * TREE_SCALE_JITTER2);
const px = wx * TILE4 + TILE4 / 2 + ((h2 >>> 16 & 15) / 16 - 0.5) * 2 * TREE_JITTER2;
const py = wy * TILE4 + TILE4 / 2 + ((h2 >>> 28 & 15) / 16 - 0.5) * 2 * TREE_JITTER2;
const tr = new Sprite(texFrame(ctx.props, ...T.frame));
tr.anchor.set(T.ax, T.ay);
tr.scale.set(flip ? -sc : sc, sc);
tr.x = px;
tr.y = py;
tr.zIndex = py;
let shadow = null;
if (ctx.propShadow) {
shadow = new Sprite(texFrame(ctx.propShadow, ...T.shadow));
shadow.anchor.set(0.5, 0.5);
shadow.scale.set(sc, sc);
shadow.x = px;
shadow.y = py;
}
live.push({ sprite: tr, shadow });
}
}
return { meta: f, live };
},
macroColor: biomeColor,
// The ground tile [c,r] the renderer placed at world (wx,wy) — for the debug grid overlay.
tileIndexAt(wx, wy, meta) {
if (!meta) return null;
const { field, M, SZ, x0, y0 } = meta;
const fl = (x, y) => field[(y - y0 + M) * SZ + (x - x0 + M)] === 1;
if (wx - x0 < 0 || wy - y0 < 0 || wx - x0 >= CHUNK3 || wy - y0 >= CHUNK3) return null;
if (!fl(wx, wy)) return CORRUPT_DARK;
const t = corruptTile(
!fl(wx, wy - 1),
!fl(wx + 1, wy),
!fl(wx, wy + 1),
!fl(wx - 1, wy),
!fl(wx - 1, wy - 1),
!fl(wx + 1, wy - 1),
!fl(wx - 1, wy + 1),
!fl(wx + 1, wy + 1)
);
return t || CORRUPT_LIGHT;
}
});
function createNecropolisMap(pixi, host, opts = {}) {
return createChunkedMap(pixi, host, necropolisConfig(opts.seed ?? 1));
}
// ../auto-battler/src/render/biomeRegistry.js
var BIOME_FACTORIES = {
forgottenPlains: fpConfig,
orc: orcConfig,
necropolis: necropolisConfig
};
var BIOME_GROUNDS = {
forgottenPlains: { fills: FP_GROUND_FILLS, fillKey: fpGroundFillKey },
orc: { fills: ORC_GROUND_FILLS, fillKey: orcGroundFillKey },
necropolis: { fills: NECRO_GROUND_FILLS, fillKey: necroGroundFillKey }
};
// ../auto-battler/src/render/transitionAtlas.js
var VARIANTS = 3;
function loadImg3(url) {
return new Promise((resolve, reject) => {
const img = new Image();
img.onload = () => resolve(img);
img.onerror = reject;
img.src = url;
});
}
function tilePixels(img, c, r) {
const cv = document.createElement("canvas");
cv.width = TILE;
cv.height = TILE;
const g = cv.getContext("2d", { willReadFrequently: true });
g.imageSmoothingEnabled = false;
g.drawImage(img, c * TILE, r * TILE, TILE, TILE, 0, 0, TILE, TILE);
return g.getImageData(0, 0, TILE, TILE).data;
}
async function buildTransitionAtlas({ grounds, ids }) {
const imgCache = /* @__PURE__ */ new Map(), pxCache = /* @__PURE__ */ new Map();
const getPx = async (url, tile) => {
const k = url + "|" + tile[0] + "," + tile[1];
if (pxCache.has(k)) return pxCache.get(k);
let img = imgCache.get(url);
if (!img) {
img = await loadImg3(url);
imgCache.set(url, img);
}
const p = tilePixels(img, tile[0], tile[1]);
pxCache.set(k, p);
return p;
};
const combos = [];
for (const A of ids) for (const B of ids) {
if (A === B) continue;
for (const fa of grounds[A].fills) for (const fb of grounds[B].fills) combos.push({ A, B, fa, fb });
}
for (const c of combos) {
c.fp = await getPx(c.fa.url, c.fa.tile);
c.bp = await getPx(c.fb.url, c.fb.tile);
}
const rowOf = /* @__PURE__ */ new Map();
combos.forEach((c, i) => rowOf.set(c.A + "|" + c.fa.key + "|" + c.B + "|" + c.fb.key, i));
const caseCol = /* @__PURE__ */ new Map();
STENCIL_CASES.forEach((k, i) => caseCol.set(k, i));
const canvas = document.createElement("canvas");
canvas.width = STENCIL_CASES.length * VARIANTS * TILE;
canvas.height = combos.length * TILE;
const ctx = canvas.getContext("2d");
ctx.imageSmoothingEnabled = false;
for (let r = 0; r < combos.length; r++) {
const { fp, bp } = combos[r];
for (let ci = 0; ci < STENCIL_CASES.length; ci++) {
for (let v = 0; v < VARIANTS; v++) {
const mask = makeStencil(STENCIL_CASES[ci], v);
const img = ctx.createImageData(TILE, TILE);
for (let p = 0; p < TILE * TILE; p++) {
const src = mask[p] ? fp : bp, o = p * 4;
img.data[o] = src[o];
img.data[o + 1] = src[o + 1];
img.data[o + 2] = src[o + 2];
img.data[o + 3] = src[o + 3];
}
ctx.putImageData(img, (ci * VARIANTS + v) * TILE, r * TILE);
}
}
}
const tileAt = (fgId, fgKey, bgId, bgKey, caseKey, variant) => {
const row = rowOf.get(fgId + "|" + fgKey + "|" + bgId + "|" + bgKey);
const ci = caseCol.get(caseKey);
if (row === void 0 || ci === void 0) return null;
return [ci * VARIANTS + (variant % VARIANTS + VARIANTS) % VARIANTS, row];
};
return { canvas, tileAt, variants: VARIANTS };
}
// ../auto-battler/src/render/overworld.js
var TILE5 = 8;
var CHUNK4 = 32;
var MACRO_BLEND = 0.06;
var REGION_TINT = {
forgottenPlains: [97, 150, 55],
// meadow green
orc: [150, 128, 95],
// dirt tan
necropolis: [120, 110, 128]
// corrupted grey-purple
};
var TINT_MIX = 0.4;
var RANK = {};
OVERWORLD_BIOMES.forEach((id, i) => {
RANK[id] = i;
});
var overworldConfig = (seed, opts = {}) => {
const region = opts.regionFn ?? biomeRegion;
const biomes = {};
for (const id of OVERWORLD_BIOMES) {
const factory = BIOME_FACTORIES[id];
if (!factory) throw new Error(`overworld: no biome config registered for "${id}"`);
biomes[id] = factory(seed);
}
return {
seed,
tile: TILE5,
chunk: CHUNK4,
background: "#2b2f3a",
bounds: opts.bounds,
initialCamera: opts.initialCamera,
// Each biome loads its own sheets into its own ctx; then build the cross-biome transition atlas
// from their base grounds. Key everything for bake to route to.
async load(api) {
const ctxById = {};
for (const id of OVERWORLD_BIOMES) ctxById[id] = await biomes[id].load(api);
const atlas = await buildTransitionAtlas({ grounds: BIOME_GROUNDS, ids: OVERWORLD_BIOMES });
const texture = api.Texture.from(atlas.canvas);
texture.source.scaleMode = "nearest";
return { ctxById, trans: { source: texture.source, tileAt: atlas.tileAt, variants: atlas.variants } };
},
// Bake a chunk: assign each tile to its (crisp) region winner, let each present biome paint its
// region, then overpaint the 1-tile border ring with generated transition tiles so neighbouring
// biomes blend cleanly. Fast path: a single-biome chunk = one bake call, no mask, no seams.
bake(api) {
const { x0, y0, seed: seed2, ctx, add } = api;
const GW = CHUNK4 + 2;
const winner = new Array(GW * GW);
const widx = (lx, ly) => (ly + 1) * GW + (lx + 1);
for (let ly = -1; ly <= CHUNK4; ly++) for (let lx = -1; lx <= CHUNK4; lx++) {
winner[widx(lx, ly)] = region(seed2, x0 + lx, y0 + ly).id;
}
const at = (lx, ly) => winner[widx(lx, ly)];
const present = /* @__PURE__ */ new Set();
for (let ty = 0; ty < CHUNK4; ty++) for (let tx = 0; tx < CHUNK4; tx++) present.add(at(tx, ty));
const single = present.size === 1;
const live = [];
const metaById = {};
for (const id of OVERWORLD_BIOMES) {
if (!present.has(id)) continue;
const accept = single ? void 0 : (tx, ty) => at(tx, ty) === id;
const res = biomes[id].bake({ ...api, ctx: ctx.ctxById[id], accept }) || {};
if (res.live) for (const l of res.live) live.push(l);
if (res.meta) metaById[id] = res.meta;
}
const trans = ctx.trans;
if (trans && !single) {
for (let ty = 0; ty < CHUNK4; ty++) for (let tx = 0; tx < CHUNK4; tx++) {
const A = at(tx, ty), rankA = RANK[A];
const below = (lx, ly) => {
const b = at(lx, ly);
return b !== A && RANK[b] < rankA;
};
const key = boundaryCase(
below(tx, ty - 1),
below(tx + 1, ty),
below(tx, ty + 1),
below(tx - 1, ty),
below(tx - 1, ty - 1),
below(tx + 1, ty - 1),
below(tx - 1, ty + 1),
below(tx + 1, ty + 1)
);
if (!key) continue;
const B = dominantForeign(at, tx, ty, A, rankA);
if (!B) continue;
const wx = x0 + tx, wy = y0 + ty;
const fgKey = BIOME_GROUNDS[A].fillKey(seed2, wx, wy);
const bgKey = BIOME_GROUNDS[B].fillKey(seed2, wx, wy);
const variant = hashU32(seed2 ^ 1957, wx, wy) % trans.variants;
const cr = trans.tileAt(A, fgKey, B, bgKey, key, variant);
if (cr) add(trans.source, cr[0], cr[1], tx, ty);
}
}
return { live, meta: { winner, GW, metaById } };
},
// Zoomed-out world map: a flat region tint (so lands stay legible at any zoom) modulated by a
// little of the biome's internal terrain colour, cross-fading into the runner-up region across
// a thin border band so regions read as soft-edged lands, not hard cells.
macroColor(seed2, tx, ty) {
const { id, id2, edge } = region(seed2, tx, ty);
let tint = REGION_TINT[id];
if (edge < MACRO_BLEND && id !== id2) tint = lerp3(tint, REGION_TINT[id2], 0.5 * (1 - edge / MACRO_BLEND));
return lerp3(tint, biomes[id].macroColor(seed2, tx, ty), TINT_MIX);
},
// Which biome owns a world tile (no chunk meta needed) — for callers that key behaviour to the
// land the player is on (free-roam enemy rosters, per-biome walkability).
biomeAt(seed2, wx, wy) {
return region(seed2, wx, wy).id;
},
// Debug grid overlay: dispatch to whichever biome owns the tile, with that biome's chunk meta.
tileIndexAt(wx, wy, meta) {
if (!meta) return null;
const tx = (wx % CHUNK4 + CHUNK4) % CHUNK4, ty = (wy % CHUNK4 + CHUNK4) % CHUNK4;
const id = meta.winner[(ty + 1) * meta.GW + (tx + 1)];
const bm = meta.metaById[id];
return bm ? biomes[id].tileIndexAt(wx, wy, bm) : null;
}
};
};
function dominantForeign(at, tx, ty, A, rankA) {
const tally = /* @__PURE__ */ new Map();
for (let dy = -1; dy <= 1; dy++) for (let dx = -1; dx <= 1; dx++) {
if (!dx && !dy) continue;
const b = at(tx + dx, ty + dy);
if (b !== A && RANK[b] < rankA) tally.set(b, (tally.get(b) || 0) + 1);
}
let best = null, bestN = 0;
for (const [b, n] of tally) if (n > bestN) {
bestN = n;
best = b;
}
return best;
}
function createOverworldMap(pixi, host, opts = {}) {
return createChunkedMap(pixi, host, overworldConfig(opts.seed ?? 1));
}
// ../auto-battler/src/render/sceneViewer.js
function createSceneViewer(pixi, host, { layers, dir, sceneW, sceneH, background = "#15121b", pulse = null }) {
const { Application, Assets, Sprite, Container } = pixi;
let app = null;
let root = null;
let alive = true;
let pulseSprite = null;
let pulsePhase = 0;
const layerMap = /* @__PURE__ */ new Map();
const listeners = /* @__PURE__ */ new Set();
const emit = () => listeners.forEach((fn) => fn(getSnapshot()));
const getSnapshot = () => ({
layers: layers.map(([key, label2, group]) => ({ key, label: label2, group, visible: layerMap.get(key)?.visible ?? false }))
});
function fit3() {
if (!root || !app) return;
const sw = app.screen.width, sh = app.screen.height;
const s = Math.max(1, Math.floor(Math.min(sw / sceneW, sh / sceneH)));
root.scale.set(s);
root.x = Math.round((sw - sceneW * s) / 2);
root.y = Math.round((sh - sceneH * s) / 2);
}
const ready = (async () => {
const a = new Application();
await a.init({ background, antialias: false, resizeTo: host });
if (!alive) {
a.destroy(true);
return;
}
app = a;
host.appendChild(app.canvas);
root = new Container();
app.stage.addChild(root);
for (const [key, label2, group] of layers) {
const container = new Container();
root.addChild(container);
layerMap.set(key, { container, label: label2, group, visible: true });
try {
const t = await Assets.load(`${dir}/Premade_${key}.png`);
if (!alive) return;
t.source.scaleMode = "nearest";
const sp = new Sprite(t);
container.addChild(sp);
if (pulse && key === pulse.key) pulseSprite = sp;
} catch {
}
}
fit3();
app.renderer.on("resize", fit3);
if (pulse) app.ticker.add(tickPulse);
emit();
})();
function tickPulse(ticker) {
if (!pulseSprite) return;
pulsePhase += (pulse.speed ?? 0.06) * ticker.deltaTime;
pulseSprite.alpha = (pulse.base ?? 0.72) + (pulse.amp ?? 0.28) * (0.5 + 0.5 * Math.sin(pulsePhase));
}
function setLayer(key, visible) {
const L = layerMap.get(key);
if (!L) return;
L.visible = visible;
L.container.visible = visible;
emit();
}
function onChange(fn) {
listeners.add(fn);
return () => listeners.delete(fn);
}
function destroy() {
alive = false;
try {
app?.ticker?.remove(tickPulse);
} catch {
}
try {
app?.renderer?.off("resize", fit3);
} catch {
}
try {
app?.canvas?.remove();
} catch {
}
try {
app?.destroy(true, { children: true });
} catch {
}
app = null;
root = null;
layerMap.clear();
}
return { ready, setLayer, onChange, destroy, getSnapshot };
}
// ../auto-battler/src/engine/roomGen.js
function generateRooms(seed, W = 52, H = 36) {
const { rnd, ri } = makeGenRng(seed);
const floor = new Uint8Array(W * H);
const at = (x, y) => x >= 0 && y >= 0 && x < W && y < H;
const rooms = [];
const target = ri(4, 7);
for (let t = 0; t < 120 && rooms.length < target; t++) {
const big = rnd() < 0.25;
const w = big ? ri(9, 13) : ri(5, 8), h2 = big ? ri(6, 9) : ri(4, 6);
const x = ri(1, W - w - 2), y = ri(1, H - h2 - 2);
let ok = true;
for (const r of rooms) if (x < r.x + r.w + 2 && x + w + 2 > r.x && y < r.y + r.h + 2 && y + h2 + 2 > r.y) {
ok = false;
break;
}
if (!ok) continue;
rooms.push({ x, y, w, h: h2 });
for (let j = y; j < y + h2; j++) for (let i = x; i < x + w; i++) floor[j * W + i] = 1;
}
const set = (x, y) => {
if (at(x, y)) floor[y * W + x] = 1;
};
const carveH = (x02, x12, y) => {
for (let x = Math.min(x02, x12); x <= Math.max(x02, x12); x++) {
set(x, y);
set(x, y + 1);
}
};
const carveV = (y02, y12, x) => {
for (let y = Math.min(y02, y12); y <= Math.max(y02, y12); y++) {
set(x, y);
set(x + 1, y);
}
};
const connect = (a, b) => {
const ax = a.x + (a.w >> 1), ay = a.y + (a.h >> 1), bx = b.x + (b.w >> 1), by = b.y + (b.h >> 1);
if (rnd() < 0.5) {
carveH(ax, bx, ay);
carveV(ay, by, bx);
} else {
carveV(ay, by, ax);
carveH(ax, bx, by);
}
};
for (let k = 1; k < rooms.length; k++) connect(rooms[k - 1], rooms[k]);
for (let e = 0, extra = ri(1, 2); e < extra && rooms.length > 2; e++) {
const a = rooms[ri(0, rooms.length - 1)], b = rooms[ri(0, rooms.length - 1)];
if (a !== b) connect(a, b);
}
let x0 = W, y0 = H, x1 = 0, y1 = 0;
for (let y = 0; y < H; y++) for (let x = 0; x < W; x++) if (floor[y * W + x]) {
if (x < x0) x0 = x;
if (y < y0) y0 = y;
if (x > x1) x1 = x;
if (y > y1) y1 = y;
}
return { W, H, floor, rooms, bounds: { x0, y0, x1, y1 } };
}
// ../auto-battler/src/render/interiorSkins.js
var ORC2 = "/assets/minifantasy/Minifantasy_Orc_Kingdom_v1.0/Minifantasy_Orc_Kingdom_Assets/Tileset/Tiles.png";
var NECRO2 = "/assets/minifantasy/Minifantasy_Necropolis_v1.0/Minifantasy_Necropolis_Assets/Tileset/Buildings/TileasetAndPremadeZiggurats.png";
var TEMPLE = "/assets/minifantasy/Minifantasy_Temple_Of_The_Snake_God_v1.0_Commercial_Version/Temple_Of_The_Snake_God/Tileset/Tileset.png";
var ORC_PROPS = "/assets/minifantasy/Minifantasy_Orc_Kingdom_v1.0/Minifantasy_Orc_Kingdom_Assets/Props/Props.png";
var ORC_FIRE = "/assets/minifantasy/Minifantasy_Orc_Kingdom_v1.0/Minifantasy_Orc_Kingdom_Assets/Props/Animated%20Props/Fires.png";
var TEMPLE_CANDLE = "/assets/minifantasy/Minifantasy_Temple_Of_The_Snake_God_v1.0_Commercial_Version/Temple_Of_The_Snake_God/Props/_Animated_Props/M_candles/M_candles__.png";
var NECRO_PROPS = "/assets/minifantasy/Minifantasy_Necropolis_v1.0/Minifantasy_Necropolis_Assets/Props/Props.png";
var NECRO_FLAME = "/assets/minifantasy/Minifantasy_Necropolis_v1.0/Minifantasy_Necropolis_Assets/Props/UndeadFlame/UndeadFlame.png";
var TEMPLE_PROPS = "/assets/minifantasy/Minifantasy_Temple_Of_The_Snake_God_v1.0_Commercial_Version/Temple_Of_The_Snake_God/Props/Props.png";
var flat = (b) => ({ nw: b, n: b, ne: b, w: b, c: b, e: b, sw: b, s: b, se: b });
var INTERIOR_SKINS = {
orc: {
label: "Orc Kingdom",
url: ORC2,
tile: 8,
bg: "#15110d",
floor: {
nw: [42, 49],
n: [43, 49],
ne: [52, 49],
w: [42, 50],
c: [43, 50],
e: [52, 50],
sw: [42, 59],
s: [43, 59],
se: [52, 59],
vars: [[44, 51], [46, 52], [48, 55], [45, 57]]
},
// Full 2.5D wall enclosure with TWO interchangeable materials — a wooden palisade and a
// grey stone wall (same room-template layout, stone shifted +22 rows on the sheet). Each
// room picks one. `directional` keeps height on every edge: N = face+cap, S = top+fringe,
// E/W = a face column, with oriented corners. Some N walls sprout a window.
// Each material: cols [left, mid, right] of its 7-wide room template + the template's rows.
walls: {
style: "directional",
sets: [
{
// wooden palisade (room template cols 22–28, rows 11–21)
cols: [22, 24, 28],
rows: { nCap: 11, nFace: 12, body: 15, sTop: 19, sFringe: 20 },
window: { top: [58, 13], bot: [58, 14] }
},
{
// grey stone (palisade layout + 22 rows)
cols: [22, 24, 28],
rows: { nCap: 33, nFace: 34, body: 37, sTop: 41, sFringe: 42 },
window: { top: [58, 35], bot: [58, 36] }
}
]
},
// Fur pelts scattered as rugs + a skull-totem centrepiece, from the Orc Props sheet.
propsUrl: ORC_PROPS,
props: [
{ c0: 1, r0: 10, w: 2, h: 2 },
{ c0: 5, r0: 10, w: 2, h: 2 },
{ c0: 8, r0: 10, w: 2, h: 2 },
// fur pelts
{ c0: 2, r0: 1, w: 2, h: 2 },
{ c0: 7, r0: 7, w: 2, h: 2 }
// cot bed + table
],
features: [{ c0: 7, r0: 16, w: 3, h: 3 }, { c0: 9, r0: 25, w: 3, h: 4 }],
// skull totem / bone war-standard
torch: { url: ORC_FIRE, frame: { c0: 1, r0: 1, w: 1, h: 2 }, frames: 8, stride: 2, glow: 16750899 }
// warm fire
},
necropolis: {
label: "Necropolis",
url: NECRO2,
tile: 8,
bg: "#0f0c14",
floor: { ...flat([41, 1]), vars: [[42, 1], [43, 1], [41, 2], [42, 2], [42, 5], [41, 4], [43, 6]] },
// Full wall enclosure (purple-brick autotile, ziggurat sheet cols 47–50/rows 23–33).
// `north` is the tall front-facing wall (crenellated cap + brick body, listed top→bottom)
// that gives the 2.5D height; the other sides are thin autotile edges + corner posts.
walls: {
cap: [48, 31],
// cream crenellated cap (continuous top trim)
body: [48, 32],
// brick body
bodyVars: [[49, 32], [50, 33], [49, 25]],
// hole/worn brick variants sprinkled in
height: 3,
bot: [48, 27],
left: [47, 25],
right: [50, 25],
post: [47, 23]
// rounded post at corners / wall ends
},
// Scattered bones/remains from the Necropolis Props sheet (2×2 bone curls), plus a
// hanging rune banner centrepiece in the largest room.
propsUrl: NECRO_PROPS,
props: [{ c0: 1, r0: 2, w: 2, h: 2 }, { c0: 4, r0: 2, w: 2, h: 2 }, { c0: 7, r0: 2, w: 2, h: 2 }, { c0: 9, r0: 5, w: 2, h: 2 }],
features: [{ c0: 12, r0: 1, w: 2, h: 4 }, { c0: 22, r0: 3, w: 2, h: 4 }],
// blue / red rune banner
// Undead-flame wall torches: a flame frame from the animation strip + an eerie green glow.
torch: { url: NECRO_FLAME, frame: { c0: 1, r0: 1, w: 2, h: 2 }, frames: 8, stride: 3, glow: 4841632 }
},
temple: {
label: "Temple",
url: TEMPLE,
tile: 8,
bg: "#080c08",
floor: { ...flat([61, 36]), vars: [[63, 36], [64, 36], [61, 38], [61, 39]] },
// Full enclosure matching the premade scene: a tall (3-tile) green-diamond north wall with
// a dark top cap, plus thin dark side/bottom walls (the temple's side walls are a single
// narrow edge, distinct from the thick back wall) and corner posts.
walls: {
cap: [7, 3],
// dark upper band (top trim)
body: [7, 4],
// green diamond wall face (col 7 = clean fill, off the window-frame highlight cols)
bodyVars: [[8, 4], [9, 5], [6, 5]],
// texture variants sprinkled in
height: 3,
bot: [7, 4],
left: [5, 4],
right: [11, 4],
post: [5, 3]
// thin sides + corner posts
},
// Small coiled snakes from the Temple Props sheet (2×2 each), plus a snake-idol head as
// a centrepiece feature placed in the largest room when it fits.
propsUrl: TEMPLE_PROPS,
props: [{ c0: 15, r0: 21, w: 2, h: 2 }, { c0: 17, r0: 21, w: 2, h: 2 }, { c0: 19, r0: 21, w: 2, h: 2 }],
features: [{ c0: 9, r0: 4, w: 3, h: 4 }, { c0: 2, r0: 3, w: 4, h: 4 }],
// front cobra idol / side-profile head
torch: { url: TEMPLE_CANDLE, frame: { c0: 1, r0: 2, w: 2, h: 2 }, frames: 6, stride: 3, glow: 16764006 }
// candle
}
};
// ../auto-battler/src/render/tileViewer.js
function createTileViewer(pixi, host, cfg) {
const { skins, defaultSkin, sourcesFor, render: render3, fit: fit3 } = cfg;
const { Application, Assets, Sprite, Container, Texture, Rectangle, Graphics } = pixi;
let app = null, root = null, alive = true;
let skinKey = defaultSkin;
let seed = cfg.seed ?? 1;
const sources = /* @__PURE__ */ new Map();
const texCache = /* @__PURE__ */ new Map();
const listeners = /* @__PURE__ */ new Set();
let lastBounds = null, lastTile = 8, lastSnapshot = {}, tick = null;
function tex(source, c, r, tile) {
const k = source.uid + ":" + c + "," + r;
let t = texCache.get(k);
if (!t) {
t = new Texture({ source, frame: new Rectangle(c * tile, r * tile, tile, tile) });
texCache.set(k, t);
}
return t;
}
async function ensureSource(url) {
if (sources.has(url)) return sources.get(url);
const a = await Assets.load(url);
a.source.scaleMode = "nearest";
sources.set(url, a.source);
return a.source;
}
async function loadSources(skin) {
for (const url of sourcesFor(skin)) {
await ensureSource(url);
if (!alive) return false;
}
return true;
}
const getSnapshot = () => ({ skin: skinKey, seed, ...lastSnapshot });
const emit = () => listeners.forEach((fn) => fn(getSnapshot()));
function redraw() {
if (!app || !alive) return;
if (tick) {
try {
app.ticker.remove(tick);
} catch {
}
tick = null;
}
if (root) {
root.destroy({ children: true });
root = null;
}
root = new Container();
app.stage.addChild(root);
const skin = skins[skinKey];
const TILE6 = skin.tile || 8;
const res = render3({ app, root, skin, seed, TILE: TILE6, tex, sources, Sprite, Graphics, Container }) || {};
lastBounds = res.bounds || null;
lastTile = res.tile ?? TILE6;
lastSnapshot = res.snapshot || {};
if (res.tick) {
tick = res.tick;
app.ticker.add(tick);
}
if (lastBounds) fit3(app, root, lastBounds, lastTile);
}
const ready = (async () => {
const a = new Application();
await a.init({ background: skins[skinKey].bg, antialias: false, resizeTo: host });
if (!alive) {
a.destroy(true);
return;
}
app = a;
host.appendChild(app.canvas);
if (!await loadSources(skins[skinKey])) return;
redraw();
emit();
app.renderer.on("resize", () => {
if (lastBounds) fit3(app, root, lastBounds, lastTile);
});
})();
async function setSkin(k) {
if (!skins[k] || k === skinKey) return;
skinKey = k;
if (!await loadSources(skins[k])) return;
if (app) app.renderer.background.color = skins[k].bg;
redraw();
emit();
}
function regenerate(nextSeed) {
seed = nextSeed >>> 0;
redraw();
emit();
}
function onChange(fn) {
listeners.add(fn);
return () => listeners.delete(fn);
}
function destroy() {
alive = false;
try {
if (tick) app?.ticker?.remove(tick);
} catch {
}
try {
app?.canvas?.remove();
} catch {
}
try {
app?.destroy(true, { children: true });
} catch {
}
app = null;
root = null;
sources.clear();
texCache.clear();
}
return { ready, regenerate, setSkin, onChange, destroy, getSnapshot };
}
// ../auto-battler/src/render/interior.js
function createInteriorViewer(pixi, host, opts = {}) {
return createTileViewer(pixi, host, {
skins: INTERIOR_SKINS,
defaultSkin: opts.skin || "orc",
seed: opts.seed ?? 1,
sourcesFor: (skin) => [skin.url, skin.propsUrl, skin.torch?.url].filter(Boolean),
render,
fit
});
}
function render({ root, skin, seed, TILE: TILE6, tex, sources, Sprite, Graphics }) {
const src = sources.get(skin.url);
const { W, H, floor, rooms, bounds } = generateRooms(seed);
const isF = (x, y) => x >= 0 && y >= 0 && x < W && y < H && floor[y * W + x] === 1;
const addS = (source, coord, x, y) => {
const sp = new Sprite(tex(source, coord[0], coord[1], TILE6));
sp.x = x * TILE6;
sp.y = y * TILE6;
root.addChild(sp);
return sp;
};
const add = (coord, x, y) => addS(src, coord, x, y);
const c = { skin, TILE: TILE6, W, H, rooms, seed, isF, add, addS, root, sources, Graphics };
drawWalls(c);
drawFloor(c);
drawShadows(c);
drawProps(c);
const torch = drawTorches(c);
let animPhase = 0;
const tick = torch.flames.length && torch.flameSrc ? (ticker) => {
animPhase += 0.14 * ticker.deltaTime;
const fi = Math.floor(animPhase) % torch.flameFrames;
for (const fl of torch.flames) fl.sp.texture = tex(torch.flameSrc, fl.col0 + torch.flameStride * fi, fl.row, TILE6);
if (torch.glowG) torch.glowG.alpha = 0.78 + 0.22 * Math.sin(animPhase * 0.9);
} : null;
return { bounds, tile: TILE6, snapshot: { rooms: rooms.length }, tick };
}
function fit(app, root, b, TILE6) {
const sw = app.screen.width, sh = app.screen.height;
const cw = (b.x1 - b.x0 + 3) * TILE6, ch = (b.y1 - b.y0 + 4) * TILE6;
const s = Math.max(1, Math.floor(Math.min(sw / cw, sh / ch)));
root.scale.set(s);
root.x = Math.round((sw - cw * s) / 2 - (b.x0 - 1) * TILE6 * s);
root.y = Math.round((sh - ch * s) / 2 - (b.y0 - 2) * TILE6 * s);
}
function drawWalls(c) {
if (c.skin.wall && !c.skin.walls) drawBackWall(c);
if (c.skin.walls) drawEnclosure(c);
}
function drawBackWall(c) {
const { skin, W, H, isF, add } = c;
for (let y = 0; y < H; y++) for (let x = 0; x < W; x++) {
if (!isF(x, y) || isF(x, y - 1)) continue;
if (skin.wall.face) add(skin.wall.face, x, y - 1);
if (skin.wall.cap) add(skin.wall.cap, x, y - 2);
}
}
function drawEnclosure(c) {
const { skin, W, H, rooms, seed, isF, add } = c;
const WS = skin.walls;
const Hh = WS.height || 3;
const sets = WS.sets || [WS];
const roomMat = rooms.map((rm) => sets.length > 1 ? hashU32(seed ^ 15386, rm.x, rm.y) % sets.length : 0);
const matAt = (fx, fy) => {
for (let i = 0; i < rooms.length; i++) {
const r = rooms[i];
if (fx >= r.x && fx < r.x + r.w && fy >= r.y && fy < r.y + r.h) return roomMat[i];
}
return 0;
};
const brick = (S, bx, by) => {
if (!S.bodyVars || !S.bodyVars.length) return S.body;
const h2 = hashU32(seed ^ 23057, bx, by);
return h2 % 4 === 0 ? S.bodyVars[(h2 >>> 5) % S.bodyVars.length] : S.body;
};
if (WS.style === "directional") {
const t = (S, col, row) => [S.cols[col], row];
const winRoll = (x, y) => hashU32(seed ^ 30627, x, y) % 5 === 0;
const northCol = (S, x, y, col) => {
if (S.window && col === 1 && winRoll(x, y) && !isF(x, y - 1)) {
add(S.window.bot, x, y);
add(S.window.top, x, y - 1);
return;
}
add(t(S, col, S.rows.nFace), x, y);
if (!isF(x, y - 1)) add(t(S, col, S.rows.nCap), x, y - 1);
};
const southCol = (S, x, y, col) => {
add(t(S, col, S.rows.sTop), x, y);
if (!isF(x, y + 1)) add(t(S, col, S.rows.sFringe), x, y + 1);
};
for (let y = 0; y < H; y++) for (let x = 0; x < W; x++) {
if (isF(x, y)) continue;
const fN = isF(x, y - 1), fE = isF(x + 1, y), fS = isF(x, y + 1), fW = isF(x - 1, y);
if (fS) northCol(sets[matAt(x, y + 1)], x, y, 1);
else if (fN) southCol(sets[matAt(x, y - 1)], x, y, 1);
else if (fE) add(t(sets[matAt(x + 1, y)], 0, sets[matAt(x + 1, y)].rows.body), x, y);
else if (fW) add(t(sets[matAt(x - 1, y)], 2, sets[matAt(x - 1, y)].rows.body), x, y);
else if (isF(x + 1, y + 1)) northCol(sets[matAt(x + 1, y + 1)], x, y, 0);
else if (isF(x - 1, y + 1)) northCol(sets[matAt(x - 1, y + 1)], x, y, 2);
else if (isF(x + 1, y - 1)) southCol(sets[matAt(x + 1, y - 1)], x, y, 0);
else if (isF(x - 1, y - 1)) southCol(sets[matAt(x - 1, y - 1)], x, y, 2);
}
} else {
const tall = (S, x, yBase, capTile, bodyTile, allowWindow) => {
const win = allowWindow && S.window && Hh >= 2 && hashU32(seed ^ 30627, x, yBase) % 5 === 0 && !isF(x, yBase - (Hh - 1));
for (let k = 0; k < Hh; k++) {
const yy = yBase - k;
if (yy < 0 || isF(x, yy)) break;
let tt;
if (win && k === Hh - 1) tt = S.window.top;
else if (win && k === Hh - 2) tt = S.window.bot;
else tt = k === Hh - 1 ? capTile : bodyTile || brick(S, x, yy);
add(tt, x, yy);
}
};
for (let y = 0; y < H; y++) for (let x = 0; x < W; x++) {
if (isF(x, y)) continue;
const fN = isF(x, y - 1), fE = isF(x + 1, y), fS = isF(x, y + 1), fW = isF(x - 1, y);
if (fS) tall(sets[matAt(x, y + 1)], x, y, sets[matAt(x, y + 1)].cap, null, true);
else if (WS.backOnly) continue;
else if (fN) add(sets[matAt(x, y - 1)].bot, x, y);
else if (fE) add(sets[matAt(x + 1, y)].left, x, y);
else if (fW) add(sets[matAt(x - 1, y)].right, x, y);
else if (isF(x + 1, y + 1)) {
const S = sets[matAt(x + 1, y + 1)];
tall(S, x, y, S.post, S.left);
} else if (isF(x - 1, y + 1)) {
const S = sets[matAt(x - 1, y + 1)];
tall(S, x, y, S.post, S.right);
} else if (isF(x + 1, y - 1)) add(sets[matAt(x + 1, y - 1)].post, x, y);
else if (isF(x - 1, y - 1)) add(sets[matAt(x - 1, y - 1)].post, x, y);
}
}
}
function drawFloor(c) {
const { skin, W, H, seed, isF, add } = c;
const F = skin.floor;
for (let y = 0; y < H; y++) for (let x = 0; x < W; x++) {
if (!isF(x, y)) continue;
const n = !isF(x, y - 1), e = !isF(x + 1, y), s = !isF(x, y + 1), w = !isF(x - 1, y);
let t;
if (n || e || s || w) t = nineSlice(F, n, s, w, e);
else {
const h2 = hashU32(seed ^ 496, x, y);
t = F.vars && F.vars.length && h2 % 7 === 0 ? F.vars[(h2 >>> 5) % F.vars.length] : F.c;
}
add(t, x, y);
}
}
function drawShadows(c) {
const { W, H, TILE: TILE6, isF, root, Graphics } = c;
const shade = new Graphics();
for (let y = 0; y < H; y++) for (let x = 0; x < W; x++) {
if (!isF(x, y)) continue;
if (!isF(x, y - 1)) {
shade.rect(x * TILE6, y * TILE6, TILE6, TILE6).fill({ color: 0, alpha: 0.3 });
if (isF(x, y + 1)) shade.rect(x * TILE6, (y + 1) * TILE6, TILE6, TILE6).fill({ color: 0, alpha: 0.12 });
} else if (!isF(x - 1, y) || !isF(x + 1, y)) {
shade.rect(x * TILE6, y * TILE6, TILE6, TILE6).fill({ color: 0, alpha: 0.12 });
}
}
root.addChild(shade);
}
function drawProps(c) {
const { skin, seed, rooms, sources, addS } = c;
const propSrc = sources.get(skin.propsUrl || skin.url);
if (!(skin.props && skin.props.length && propSrc)) return;
for (const rm of rooms) {
const ring = [];
for (let x = rm.x + 1; x < rm.x + rm.w - 1; x++) {
ring.push([x, rm.y + 1]);
ring.push([x, rm.y + rm.h - 2]);
}
for (let y = rm.y + 2; y < rm.y + rm.h - 2; y++) {
ring.push([rm.x + 1, y]);
ring.push([rm.x + rm.w - 2, y]);
}
let placed = 0;
for (const [x, y] of ring) {
if (placed >= 4) break;
const hh = hashU32(seed ^ 39692, x, y);
if (hh % 6 !== 0) continue;
const pr = skin.props[(hh >>> 5) % skin.props.length];
const pw = pr.w || 1, ph = pr.h || 1;
if (x + pw > rm.x + rm.w - 1 || y + ph > rm.y + rm.h - 1) continue;
for (let ry = 0; ry < ph; ry++) for (let rx = 0; rx < pw; rx++) addS(propSrc, [pr.c0 + rx, pr.r0 + ry], x + rx, y + ry);
placed++;
}
}
const feats = skin.features || [];
if (feats.length && rooms.length) {
const byArea = [...rooms].sort((a, b) => b.w * b.h - a.w * a.h);
let fi = 0;
for (const big of byArea) {
if (fi >= feats.length) break;
const f = feats[(seed + fi) % feats.length];
if (f.w <= big.w - 2 && f.h <= big.h - 2) {
const fx = big.x + (big.w - f.w >> 1), fy = big.y + 1;
for (let ry = 0; ry < f.h; ry++) for (let rx = 0; rx < f.w; rx++) addS(propSrc, [f.c0 + rx, f.r0 + ry], fx + rx, fy + ry);
fi++;
}
}
}
}
function drawTorches(c) {
const { skin, W, H, TILE: TILE6, isF, sources, root, Graphics, addS } = c;
const torch = skin.torch, torchSrc = torch && sources.get(torch.url);
if (!(torch && torchSrc)) return { flames: [], glowG: null, flameSrc: null, flameStride: 1, flameFrames: 1 };
const flames = [];
const glow = new Graphics();
const f = torch.frame, spots = [];
for (let y = 0; y < H; y++) for (let x = 0; x < W; x++) {
if (!isF(x, y) || isF(x, y - 1)) continue;
if ((x * 7 + y * 13) % 5 !== 0) continue;
const cx = (x + f.w / 2) * TILE6, cy = (y - 1) * TILE6;
for (const [r, a] of [[3.2, 0.05], [2.1, 0.08], [1.2, 0.13]]) glow.circle(cx, cy, r * TILE6).fill({ color: torch.glow, alpha: a });
spots.push([x, y]);
}
root.addChild(glow);
for (const [x, y] of spots) for (let ry = 0; ry < f.h; ry++) for (let rx = 0; rx < f.w; rx++) {
const sp = addS(torchSrc, [f.c0 + rx, f.r0 + ry], x + rx, y - 2 + ry);
flames.push({ sp, col0: f.c0 + rx, row: f.r0 + ry });
}
return { flames, glowG: glow, flameSrc: torchSrc, flameStride: torch.stride || 1, flameFrames: torch.frames || 1 };
}
// ../auto-battler/src/engine/towerGen.js
function generateTower(seed, opts = {}) {
const { rnd, ri } = makeGenRng(seed);
let roof = opts.roof;
if (!roof) {
const rr = rnd();
roof = rr < 0.15 ? "none" : rr < 0.45 ? "battlement" : "pyramid";
}
const baseW = opts.w ?? opts.baseW ?? 4;
const spriteRoof = roof === "pyramid" || roof === "battlement" && !opts.hasParapet;
const w = spriteRoof ? baseW : Math.max(3, baseW + ri(-1, 2));
const h2 = opts.h ?? (roof === "none" ? ri(3, 6) : ri(6, 12));
const hasDoor = rnd() < (roof === "none" ? 0.4 : 0.9);
const winH = opts.winH ?? 2, doorH = opts.doorH ?? 2;
const windows = [];
for (let y = 1; y + winH <= h2 - doorH - 1; y += winH + 1) if (rnd() < 0.6) windows.push(y);
return { w, h: h2, roof, windows, hasDoor };
}
// ../auto-battler/src/render/towerSkins.js
var ROOT = "/assets/minifantasy/Minifantasy_Towers_v1.0/Towers";
var MEADOW = `${ROOT}/Meadow_Towers/Tileset.png`;
var DESERT = `${ROOT}/Desert_Towers/Tileset.png`;
var SWAMP = `${ROOT}/Swamp_Towers/Tileset.png`;
var WALL = {
nw: [6, 40],
n: [7, 40],
ne: [8, 40],
w: [6, 41],
c: [7, 41],
e: [8, 41],
sw: [6, 42],
s: [7, 42],
se: [8, 42]
};
var DESERT_WALL = {
nw: [16, 32],
n: [17, 32],
ne: [18, 32],
w: [16, 34],
c: [17, 34],
e: [18, 34],
sw: [16, 37],
s: [17, 37],
se: [18, 37]
};
var TOWER_SKINS = {
meadow: {
label: "Meadow",
url: MEADOW,
tile: 8,
bg: "#16210f",
wall: WALL,
roof: { c0: 4, r0: 2, w: 8, h: 8 },
window: { c0: 17, r0: 45, w: 2, h: 2 },
door: { c0: 17, r0: 49, w: 3, h: 2 },
// Assembled rooftop crown (matches the premade tower): a 5-wide shaft topped by a
// wooden balustrade railing, with the blue crystal pyramid roof for the 'pyramid'
// variant. `dy` = each piece's top row relative to the shaft top (y=0, negative = up).
// Pieces are fixed-size cross/roof art from the tileset, blitted centred on the shaft.
crown: {
shaftW: 5,
platform: { c0: 4, r0: 27, w: 7, h: 8, dy: -8 },
// grey stone rooftop floor (cross)
rail: { c0: 13, r0: 12, w: 7, h: 8, dy: -8 },
// wooden balustrade on the platform edge
roof: { c0: 3, r0: 1, w: 9, h: 11, dy: -16 }
// blue crystal pyramid above
},
// Alternate brick fills sprinkled into the shaft interior for weathering (Meadow only;
// these coords are different art on the other variant sheets).
fillVars: [[5, 49], [9, 49]]
},
desert: {
label: "Desert",
url: DESERT,
tile: 8,
bg: "#241d10",
wall: DESERT_WALL,
// Teal dome (sphere + orange lip = cols 3–9 / rows 2–9; rows 10+ are the cylinder drum,
// not the dome). Cylindrical-tower door/window openings.
roof: { c0: 3, r0: 2, w: 7, h: 8 },
window: { c0: 14, r0: 23, w: 3, h: 3 },
door: { c0: 18, r0: 26, w: 2, h: 2 },
// Dome crown: the dome seats directly on the shaft top (no cross platform — desert towers
// are cylindrical, the dome caps the drum). dy nudges it down to rest on the cornice.
crown: {
shaftW: 5,
roof: { c0: 3, r0: 2, w: 7, h: 8, dy: -6 }
}
},
swamp: {
label: "Swamp",
url: SWAMP,
tile: 8,
bg: "#0f1410",
wall: WALL,
roof: { c0: 9, r0: 1, w: 6, h: 8 },
window: { c0: 17, r0: 20, w: 3, h: 3 },
door: { c0: 17, r0: 24, w: 3, h: 3 },
// Blue crenellated battlement row (reuses the window's merlon top edge).
parapet: { left: [17, 20], mid: [18, 20], right: [19, 20] }
}
};
// ../auto-battler/src/render/towerViewer.js
function createTowerViewer(pixi, host, opts = {}) {
return createTileViewer(pixi, host, {
skins: TOWER_SKINS,
defaultSkin: opts.skin || "meadow",
seed: opts.seed ?? 1,
sourcesFor: (skin) => [skin.url],
render: render2,
fit: fit2
});
}
function render2({ root, skin, seed, TILE: TILE6, tex, sources, Sprite, Graphics }) {
const src = sources.get(skin.url);
const shaftW = skin.crown ? skin.crown.shaftW : (skin.roof?.w ?? 7) - 2;
const winH = skin.window?.h ?? 2, doorH = skin.door?.h ?? 2;
const W = skin.wall;
const N = 3, GAP = 3;
const ROOFS = ["pyramid", "battlement", "none"];
let spec0 = null;
let ox = 0, x0 = Infinity, x1 = -Infinity, y0 = Infinity;
for (let i = 0; i < N; i++) {
const spec = generateTower(seed + i, { w: shaftW, hasParapet: !!(skin.parapet || skin.crown), winH, doorH, roof: ROOFS[i] });
if (i === 0) spec0 = spec;
const { w, h: h2 } = spec;
const PLINTH = 2, wb = w + 2;
const corn = spec.roof === "none" ? 0 : 1;
const dy = -(h2 + PLINTH - 1);
const add = (coord, x, y) => {
const sp = new Sprite(tex(src, coord[0], coord[1], TILE6));
sp.x = (x + ox) * TILE6;
sp.y = (y + dy) * TILE6;
root.addChild(sp);
return sp;
};
const shadow = new Graphics();
shadow.ellipse((ox + w / 2) * TILE6, TILE6 - TILE6 * 0.3, wb / 2 * TILE6 + TILE6 * 0.5, TILE6 * 0.7).fill({ color: 0, alpha: 0.22 });
root.addChild(shadow);
for (let y = 0; y < h2; y++) for (let x = 0; x < w; x++) {
const top = y === 0, bot = y === h2 - 1, left = x === 0, right = x === w - 1;
const t = top || bot || left || right ? nineSlice(W, top, bot, left, right) : skin.fillVars && rhash(ox + x, y, 0, seed) % 6 === 0 ? skin.fillVars[rhash(ox + x, y, 99, seed) % skin.fillVars.length] : W.c;
add(t, x, y);
}
for (let py = 0; py < PLINTH; py++) for (let px = 0; px < wb; px++) {
add(nineSlice(W, py === 0, py === PLINTH - 1, px === 0, px === wb - 1), px - 1, h2 + py);
}
for (let cy = 0; cy < corn; cy++) for (let cx = 0; cx < wb; cx++) {
add(nineSlice(W, true, false, cx === 0, cx === wb - 1), cx - 1, -1 - cy);
}
const belt = corn && h2 >= 8 ? Math.round(h2 / 2) : -1;
if (belt >= 0) for (let bx = 0; bx < wb; bx++) add(nineSlice(W, true, false, bx === 0, bx === wb - 1), bx - 1, belt);
let topY;
const blitCrown = (p) => {
const sx = Math.floor((w - p.w) / 2);
for (let ry = 0; ry < p.h; ry++) for (let rx = 0; rx < p.w; rx++) add([p.c0 + rx, p.r0 + ry], sx + rx, p.dy + ry);
x0 = Math.min(x0, ox + sx);
x1 = Math.max(x1, ox + sx + p.w);
return p.dy;
};
if (skin.crown && spec.roof !== "none") {
const cr = skin.crown;
let tY = 0;
if (cr.platform) tY = Math.min(tY, blitCrown(cr.platform));
if (cr.rail) tY = Math.min(tY, blitCrown(cr.rail));
if (spec.roof === "pyramid" && cr.roof) tY = Math.min(tY, blitCrown(cr.roof));
topY = tY + dy;
} else if (spec.roof === "none") {
topY = dy;
x0 = Math.min(x0, ox);
x1 = Math.max(x1, ox + w);
} else if (spec.roof === "battlement" && skin.parapet) {
const pp = skin.parapet;
for (let x = 0; x < w; x++) add(x === 0 ? pp.left : x === w - 1 ? pp.right : pp.mid, x, -1 - corn);
topY = -1 - corn + dy;
x0 = Math.min(x0, ox);
x1 = Math.max(x1, ox + w);
} else {
const rk = skin.roof;
const roofX0 = (w - rk.w) / 2, roofY0 = -(rk.h - 1) - corn;
for (let ry = 0; ry < rk.h; ry++) for (let rx = 0; rx < rk.w; rx++) add([rk.c0 + rx, rk.r0 + ry], roofX0 + rx, roofY0 + ry);
topY = roofY0 + dy;
x0 = Math.min(x0, ox + Math.floor(roofX0));
x1 = Math.max(x1, ox + Math.ceil(roofX0 + rk.w));
}
y0 = Math.min(y0, topY);
x0 = Math.min(x0, ox - 1);
x1 = Math.max(x1, ox + w + 1);
const blit = (rect2, bx, by) => {
for (let ry = 0; ry < rect2.h; ry++) for (let rx = 0; rx < rect2.w; rx++) add([rect2.c0 + rx, rect2.r0 + ry], bx + rx, by + ry);
};
if (skin.window) {
const wx = Math.floor((w - skin.window.w) / 2);
for (const wy of spec.windows) blit(skin.window, wx, wy);
}
if (skin.door && spec.hasDoor) blit(skin.door, Math.floor((w - skin.door.w) / 2), h2 - skin.door.h);
ox += w + GAP;
}
const bounds = { x0: Math.min(x0, 0), y0, x1: Math.max(x1, ox - GAP), y1: 2 };
return { bounds, tile: TILE6, snapshot: { w: spec0?.w ?? 0, h: spec0?.h ?? 0, roof: spec0?.roof } };
}
function fit2(app, root, b, TILE6) {
const sw = app.screen.width, sh = app.screen.height;
const cw = (b.x1 - b.x0) * TILE6, ch = (b.y1 - b.y0) * TILE6;
const pad = 4 * TILE6;
const s = Math.max(1, Math.floor(Math.min(sw / (cw + pad), sh / (ch + pad))));
root.scale.set(s);
root.x = Math.round((sw - cw * s) / 2 - b.x0 * TILE6 * s);
root.y = Math.round((sh - ch * s) / 2 - b.y0 * TILE6 * s);
}
// ../auto-battler/src/render/mapConfigs.js
var ASSET = "/assets/minifantasy";
var ORC3 = `${ASSET}/Minifantasy_Orc_Kingdom_v1.0/Minifantasy_Orc_Kingdom_Assets`;
var NECRO3 = `${ASSET}/Minifantasy_Necropolis_v1.0/Minifantasy_Necropolis_Assets`;
var ORC_LAYER_DIR = `${ORC3}/_Premade%20Scene/Separate%20Layers`;
var ORC_SCENE = { w: 648, h: 488 };
var ORC_LAYERS = [
["k-ground", "Ground", "ground"],
["j-walls", "Walls", "structure"],
["i-doors", "Doors", "structure"],
["h-characters", "Characters", "scatter"],
["g-wall_shadows", "Wall shadows", "lighting"],
["f-prop_shadows", "Prop shadows", "lighting"],
["e-roof_posts", "Roof posts", "structure"],
["d-props", "Props", "scatter"],
["c-fire", "Fire", "lighting"],
["b-fire_light", "Fire light", "lighting"],
["a-roofs", "Roofs", "structure"]
];
var NECRO_SHEETS = [
{ key: "biome", label: "CorruptedBiome", url: `${NECRO3}/Tileset/Biome/CorruptedBiome.png` },
{ key: "props", label: "Props", url: `${NECRO3}/Props/Props.png` }
];
var ORC_SHEETS = [
{ key: "tiles", label: "Tiles", url: `${ORC3}/Tileset/Tiles.png` },
{ key: "props", label: "Props", url: `${ORC3}/Props/Props.png` },
{ key: "roofs", label: "Roofs & Posts", url: `${ORC3}/Tileset/Roofs/Roofs_And_Posts.png` }
];
var FP_SHEETS = [
{ key: "tiles", label: "Tiles", url: FP_TILES_URL },
{ key: "props", label: "Props", url: FP_PROPS_URL }
];
var INTERIOR_REF = {
orc: {
dir: `${ORC3}/_Premade%20Scene/Separate%20Layers`,
w: 648,
h: 488,
bg: "#15110d",
layers: [
["k-ground", "Ground", "ground"],
["j-walls", "Walls", "structure"],
["i-doors", "Doors", "structure"],
["h-characters", "Characters", "scatter"],
["g-wall_shadows", "Wall shadows", "lighting"],
["f-prop_shadows", "Prop shadows", "lighting"],
["e-roof_posts", "Roof posts", "structure"],
["d-props", "Props", "scatter"],
["c-fire", "Fire", "lighting"],
["b-fire_light", "Fire light", "lighting"],
["a-roofs", "Roofs", "structure"]
]
},
necropolis: {
dir: `${NECRO3}/PremadeScenes/Interior/SeparateLayers`,
w: 256,
h: 336,
bg: "#0f0c14",
pulse: { key: "a-undeadflame", base: 0.55, amp: 0.45, speed: 0.08 },
layers: [
["k-bg", "Background", "ground"],
["j-floor", "Floor", "ground"],
["i-corruption", "Corruption", "ground"],
["h-bones", "Bones", "scatter"],
["g-wall", "Wall", "structure"],
["f-wallcorruption", "Wall corruption", "structure"],
["e-doors", "Doors", "structure"],
["d-props", "Props", "scatter"],
["c-propshadows", "Prop shadows", "lighting"],
["b-wallshadows", "Wall shadows", "lighting"],
["a-undeadflame", "Undead flame", "lighting"]
]
},
temple: {
dir: `${ASSET}/Minifantasy_Temple_Of_The_Snake_God_v1.0_Commercial_Version/Temple_Of_The_Snake_God/Premade/_Separate_Layers`,
w: 608,
h: 368,
bg: "#080c08",
pulse: { key: "c-candles", base: 0.6, amp: 0.4, speed: 0.07 },
layers: [
["j-bg", "Background", "ground"],
["i-floor", "Floor", "ground"],
["h-walls", "Walls", "structure"],
["g-sculpture_base", "Sculpture base", "structure"],
["f-snake_sculptures", "Snake sculptures", "scatter"],
["f.2-snake_sculptures", "Snake sculptures 2", "scatter"],
["e-props", "Props", "scatter"],
["d-shadows", "Shadows", "lighting"],
["c-candles", "Candles", "lighting"],
["b-light_effect_1", "Light effect 1", "lighting"],
["a-light_effect_2", "Light effect 2", "lighting"]
]
}
};
var TROOT = `${ASSET}/Minifantasy_Towers_v1.0/Towers`;
var TOWER_REF = {
meadow: {
dir: `${TROOT}/Meadow_Towers/_Premades/Premade_1`,
w: 104,
h: 352,
bg: "#16210f",
layers: [
["1_n-bg", "Background", "ground"],
["1_m-ashlars", "Ashlars", "structure"],
["1_l-platform", "Platform", "structure"],
["1_k-tower_walls", "Tower walls", "structure"],
["1_j-cornice", "Cornice", "structure"],
["1_i-doors", "Doors", "structure"],
["1_h-windows", "Windows", "structure"],
["1_g-top", "Top", "structure"],
["1_f-stairs", "Stairs", "structure"],
["1_e-railing", "Railing", "structure"],
["1_d-roof_structure", "Roof structure", "structure"],
["1_c-shadows", "Shadows", "lighting"],
["1_b-roof", "Roof", "structure"],
["1_a-banners", "Banners", "scatter"]
]
},
desert: {
dir: `${TROOT}/Desert_Towers/_Premades/Premade_1`,
w: 104,
h: 264,
bg: "#241d10",
layers: [
["1_m-bg", "Background", "ground"],
["1_l-ashlars", "Ashlars", "structure"],
["1_k-platform", "Platform", "structure"],
["1_j-tower_walls", "Tower walls", "structure"],
["1_i-cornice", "Cornice", "structure"],
["1_h-top", "Top", "structure"],
["1_g-decoration_b", "Decoration (back)", "scatter"],
["1_f-cilinder", "Cylinder", "structure"],
["1_e-dome", "Dome", "structure"],
["1_d-doors", "Doors", "structure"],
["1_c-windows", "Windows", "structure"],
["1_b-decoration_f", "Decoration (front)", "scatter"],
["1_a-shadows", "Shadows", "lighting"]
]
},
swamp: {
dir: `${TROOT}/Swamp_Towers/_Premades/Premade_1`,
w: 88,
h: 248,
bg: "#0f1410",
layers: [
["1_k-bg", "Background", "ground"],
["1_j-foundations", "Foundations", "structure"],
["1_i-shadows_1", "Shadows 1", "lighting"],
["1_h-structure_1", "Structure 1", "structure"],
["1_g-platform_1", "Platform", "structure"],
["1_f-ladder_2", "Ladder", "structure"],
["1_e-structure_2", "Structure 2", "structure"],
["1_d-floor", "Floor", "ground"],
["1_c-walls", "Walls", "structure"],
["1_b-shadows_2", "Shadows 2", "lighting"],
["1_a-roof", "Roof", "structure"]
]
}
};
var sceneUrls = (dir, layers) => layers.map(([key]) => `${dir}/Premade_${key}.png`);
var skinUrls = (skins) => Object.values(skins).flatMap((s) => [s.url, s.propsUrl, s.torch?.url].filter(Boolean));
var MAP_ASSET_URLS = [.../* @__PURE__ */ new Set([
// Generated biome maps (the World Map overworld composites all three).
...FP_MAP_ASSETS,
...ORC_MAP_ASSETS,
...NECRO_MAP_ASSETS,
FP_MOCKUP_URL,
// Tilesheet inspector sheets.
...NECRO_SHEETS.map((s) => s.url),
...ORC_SHEETS.map((s) => s.url),
...FP_SHEETS.map((s) => s.url),
// Premade-scene reference layers.
...sceneUrls(NECRO_EXT_DIR, NECRO_LAYERS),
...sceneUrls(ORC_LAYER_DIR, ORC_LAYERS),
...Object.values(INTERIOR_REF).flatMap((r) => sceneUrls(r.dir, r.layers)),
...Object.values(TOWER_REF).flatMap((r) => sceneUrls(r.dir, r.layers)),
// Generated interiors/towers skin sheets (+ props/torch).
...skinUrls(INTERIOR_SKINS),
...skinUrls(TOWER_SKINS)
])];
// ../auto-battler/src/render/mapSandbox.js
function h(tag, attrs, ...kids) {
const e = document.createElement(tag);
if (attrs) for (const [k, v] of Object.entries(attrs)) {
if (v == null || v === false) continue;
if (k === "class") e.className = v;
else if (k === "style") e.style.cssText = v;
else if (k.startsWith("on") && typeof v === "function") e.addEventListener(k.slice(2).toLowerCase(), v);
else e.setAttribute(k, v === true ? "" : v);
}
for (const kid of kids.flat()) if (kid != null && kid !== false) e.append(kid.nodeType ? kid : document.createTextNode(String(kid)));
return e;
}
var randomSeed = () => Math.floor(Date.now() % 1e6 + Math.random() * 1e6) >>> 0;
var infiniteMeta = (s) => s?.zoom != null ? `\u221E \xB7 seed ${s.seed} \xB7 (${s.cx}, ${s.cy}) \xB7 ${s.zoom.toFixed(2)}\xD7 \xB7 drag / WASD / scroll` : "loading\u2026";
function makeGridOverlay() {
const TILE6 = 8;
const cv = h("canvas", { class: "necro-grid-overlay" });
cv.style.display = "none";
let raf = 0, ctrl = null;
function draw() {
const host = cv.parentElement;
const cam = ctrl?.getCamera?.();
if (host && cam) {
const W = host.clientWidth, H = host.clientHeight;
if (cv.width !== W) cv.width = W;
if (cv.height !== H) cv.height = H;
const ctx = cv.getContext("2d");
ctx.clearRect(0, 0, W, H);
const z = cam.zoom, cell = TILE6 * z;
if (cell >= 6) {
const sx = (wx) => (wx * TILE6 - cam.x) * z + W / 2;
const sy = (wy) => (wy * TILE6 - cam.y) * z + H / 2;
const x0 = Math.floor((cam.x - W / 2 / z) / TILE6), x1 = Math.ceil((cam.x + W / 2 / z) / TILE6);
const y0 = Math.floor((cam.y - H / 2 / z) / TILE6), y1 = Math.ceil((cam.y + H / 2 / z) / TILE6);
ctx.strokeStyle = "rgba(255,70,70,0.45)";
ctx.lineWidth = 1;
ctx.beginPath();
for (let cx = x0; cx <= x1; cx++) {
const X = Math.round(sx(cx));
ctx.moveTo(X, 0);
ctx.lineTo(X, H);
}
for (let cy = y0; cy <= y1; cy++) {
const Y = Math.round(sy(cy));
ctx.moveTo(0, Y);
ctx.lineTo(W, Y);
}
ctx.stroke();
if (cell >= 22 && ctrl.tileIndexAt) {
ctx.font = `${Math.min(10, Math.floor(cell / 3))}px monospace`;
ctx.textBaseline = "top";
ctx.lineWidth = 2;
ctx.strokeStyle = "#000";
ctx.fillStyle = "#ffe000";
for (let cy = y0; cy < y1; cy++) for (let cx = x0; cx < x1; cx++) {
const idx = ctrl.tileIndexAt(cx, cy);
if (!idx) continue;
const s = `${idx[0]},${idx[1]}`, X = sx(cx) + 1.5, Y = sy(cy) + 1.5;
ctx.strokeText(s, X, Y);
ctx.fillText(s, X, Y);
}
}
}
}
raf = requestAnimationFrame(draw);
}
return {
canvas: cv,
setCtrl(c) {
ctrl = c;
},
setActive(on) {
cv.style.display = on ? "block" : "none";
if (on && !raf) draw();
if (!on && raf) {
cancelAnimationFrame(raf);
raf = 0;
}
},
destroy() {
if (raf) cancelAnimationFrame(raf);
raf = 0;
ctrl = null;
}
};
}
var SVG_NS = "http://www.w3.org/2000/svg";
function makeTilesheet(sheets, tile = 8) {
const z = 6;
let key = sheets[0]?.key;
const dims = h("span", { class: "necro-sheet-dims" }, "loading\u2026");
const bar = h("div", { class: "necro-sheet-bar" });
const stage = h("div", { class: "necro-sheet-stage" });
const img = h("img", { class: "checker necro-sheet-img", alt: key });
const scroll = h("div", { class: "necro-sheet-scroll" }, stage);
const root = h("div", { class: "necro-tilesheet" }, bar, scroll);
stage.append(img);
const btns = sheets.map((s) => {
const b = h("button", { class: "necro-chip", "data-testid": `sheet-${s.key}`, onClick: () => select(s.key) }, s.label);
bar.append(b);
return [s.key, b];
});
bar.append(dims);
let grid = null;
const urlOf = () => (sheets.find((s) => s.key === key) ?? sheets[0]).url;
function select(k) {
key = k;
btns.forEach(([kk, b]) => b.classList.toggle("on", kk === k));
dims.textContent = "loading\u2026";
if (grid) {
grid.remove();
grid = null;
}
img.style.width = img.style.height = stage.style.width = stage.style.height = "auto";
img.alt = k;
img.src = urlOf();
}
img.addEventListener("load", () => {
const nw = img.naturalWidth, nh = img.naturalHeight;
const W = nw * z, H = nh * z, cols = Math.ceil(nw / tile), rows = Math.ceil(nh / tile), cell = tile * z;
img.style.width = stage.style.width = `${W}px`;
img.style.height = stage.style.height = `${H}px`;
dims.textContent = `${cols}\xD7${rows} tiles \xB7 ${nw}\xD7${nh}px`;
if (grid) grid.remove();
grid = document.createElementNS(SVG_NS, "svg");
grid.setAttribute("class", "necro-sheet-grid");
grid.setAttribute("width", W);
grid.setAttribute("height", H);
grid.setAttribute("viewBox", `0 0 ${W} ${H}`);
for (let i = 0; i <= cols; i++) line(grid, i * cell, 0, i * cell, H);
for (let j = 0; j <= rows; j++) line(grid, 0, j * cell, W, j * cell);
for (let j = 0; j < rows; j++) for (let i = 0; i < cols; i++) label(grid, i * cell + 2, j * cell + 11, `${i},${j}`);
stage.append(grid);
});
select(key);
return root;
}
function line(svg, x1, y1, x2, y2) {
const l = document.createElementNS(SVG_NS, "line");
l.setAttribute("x1", x1);
l.setAttribute("y1", y1);
l.setAttribute("x2", x2);
l.setAttribute("y2", y2);
l.setAttribute("stroke", "#ff3b3b");
l.setAttribute("stroke-width", "1");
l.setAttribute("opacity", "0.5");
svg.append(l);
}
function label(svg, x, y, text) {
const t = document.createElementNS(SVG_NS, "text");
t.setAttribute("x", x);
t.setAttribute("y", y);
t.setAttribute("font-size", "10");
t.setAttribute("font-family", "monospace");
t.setAttribute("fill", "#ffe000");
t.setAttribute("stroke", "#000");
t.setAttribute("stroke-width", "0.5");
t.setAttribute("paint-order", "stroke");
t.textContent = text;
svg.append(t);
}
function makePage(pixi, body, cfg) {
const hasModes = cfg.modes.length > 1;
let view = cfg.modes[0].key;
let skin = cfg.skins ? cfg.skins[0].key : null;
let seed = cfg.initialSeed != null && cfg.initialSeed !== "" ? +cfg.initialSeed >>> 0 : 1;
let showGrid = false;
let ctrl = null, off = null, grid = null;
const titleSmall = h("small", { class: "muted" });
const modePills = h("div", { class: "mapsandbox-pills necro-modes" });
const skinPills = h("div", { class: "mapsandbox-pills necro-modes" });
const seedInput = h("input", { type: "number", value: seed });
const gridToggleInput = h("input", { type: "checkbox" });
const meta = h("span", { class: "worldmap-meta muted" });
const layersEl = h("div", { class: "necro-layers" });
const stage = h("div", { class: "necro-stage" });
const modeBtns = cfg.modes.map((m) => {
const b = h("button", { onClick: () => setView(m.key) }, m.label);
modePills.append(b);
return [m.key, b];
});
const skinBtns = (cfg.skins || []).map((s) => {
const b = h("button", { onClick: () => setSkin(s.key) }, s.label);
skinPills.append(b);
return [s.key, b];
});
seedInput.addEventListener("input", (e) => {
seed = e.target.value === "" ? "" : +e.target.value;
});
seedInput.addEventListener("keydown", (e) => {
if (e.key === "Enter") regen((+seed || 0) >>> 0);
});
const seedLabel = h("label", { class: "worldmap-seed" }, "seed", seedInput);
const regenBtn = h("button", { class: "worldmap-btn", onClick: () => regen((+seed || 0) >>> 0) }, "Regenerate");
const randBtn = h("button", { class: "worldmap-btn", onClick: () => {
const v = randomSeed();
seed = v;
seedInput.value = v;
regen(v);
} }, "Random");
gridToggleInput.addEventListener("change", (e) => {
showGrid = e.target.checked;
grid?.setActive(showGrid && view === "generated");
});
const gridToggle = h("label", { class: "necro-grid-toggle" }, gridToggleInput, " Grid + indices");
const genControls = h("span", {}, seedLabel, regenBtn, randBtn, cfg.grid ? gridToggle : null);
const allBtn = h("button", { class: "worldmap-btn", onClick: () => setAllLayers(true) }, "All");
const noneBtn = h("button", { class: "worldmap-btn", onClick: () => setAllLayers(false) }, "None");
const refControls = h("span", {}, allBtn, noneBtn);
const bar = h(
"div",
{ class: "worldmap-bar" },
h("span", { class: "worldmap-title" }, `${cfg.title} `, titleSmall),
hasModes ? modePills : null,
cfg.skins ? skinPills : null,
genControls,
refControls,
meta
);
const rootEl = h("div", { class: "necro" }, bar, layersEl, stage);
body.append(rootEl);
function regen(v) {
seed = v;
seedInput.value = v;
ctrl?.regenerate?.(v);
}
function setView(k) {
view = k;
render3();
}
function setSkin(k) {
skin = k;
render3();
}
function setLayer(key, vis) {
ctrl?.setLayer?.(key, vis);
}
function setAllLayers(vis) {
(lastLayers || []).forEach((l) => ctrl?.setLayer?.(l.key, vis));
}
let lastLayers = null;
const skinLabel = () => cfg.skins ? cfg.skins.find((s) => s.key === skin)?.label : "";
function onSnap(snap) {
if (view === "tilesheet") {
meta.textContent = "8px tiles \xB7 select a sheet";
return;
}
if (view === "reference") {
if (cfg.mockup) {
meta.textContent = "pack mockup";
return;
}
const layers = snap?.layers ?? [];
lastLayers = layers;
const suffix = cfg.skins ? ` \xB7 ${skinLabel()}` : "";
meta.textContent = layers.length ? `${layers.filter((l) => l.visible).length}/${layers.length} layers${suffix}` : "loading\u2026";
renderLayerChips(layers);
return;
}
meta.textContent = cfg.generatedMeta(snap, skinLabel());
}
function renderLayerChips(layers) {
layersEl.innerHTML = "";
for (const l of [...layers].reverse()) {
const cb = h("input", { type: "checkbox" });
cb.checked = l.visible;
cb.addEventListener("change", (e) => setLayer(l.key, e.target.checked));
layersEl.append(h("label", { class: `necro-chip group-${l.group} ${l.visible ? "on" : ""}` }, cb, ` ${l.label}`));
}
}
function teardown() {
off?.();
off = null;
try {
ctrl?.destroy?.();
} catch {
}
ctrl = null;
grid?.destroy();
grid = null;
lastLayers = null;
}
function render3() {
teardown();
modeBtns.forEach(([k, b]) => b.classList.toggle("active", k === view));
skinBtns.forEach(([k, b]) => b.classList.toggle("active", k === skin));
titleSmall.textContent = `\xB7 ${cfg.subtitle ? cfg.subtitle(view) : view}`;
const isScene = view === "reference" && !cfg.mockup;
const isImage = view === "reference" && !!cfg.mockup;
const hostVisible = view === "generated" || isScene;
genControls.style.display = view === "generated" ? "" : "none";
refControls.style.display = isScene ? "" : "none";
layersEl.style.display = isScene ? "" : "none";
layersEl.innerHTML = "";
stage.innerHTML = "";
const host = h("div", { class: "worldmap-host" });
host.hidden = !hostVisible;
stage.append(host);
if (cfg.grid) {
grid = makeGridOverlay();
stage.append(grid.canvas);
if (view === "generated") {
stage.append(h(
"div",
{ class: "necro-zoom" },
h("button", { "aria-label": "Zoom in", onClick: () => ctrl?.zoomBy?.(1.4) }, "+"),
h("button", { "aria-label": "Zoom out", onClick: () => ctrl?.zoomBy?.(1 / 1.4) }, "\u2212")
));
}
}
if (view === "tilesheet") {
stage.append(makeTilesheet(cfg.sheets(skin)));
onSnap(null);
return;
}
if (isImage) {
stage.append(h(
"div",
{ class: "necro-sheet-scroll" },
h("img", { src: cfg.mockup, alt: `${cfg.title} mockup`, style: "image-rendering:pixelated;display:block" })
));
onSnap(null);
return;
}
ctrl = isScene ? cfg.makeScene(pixi, host, skin) : cfg.makeGenerated(pixi, host, (+seed || 0) >>> 0, skin);
off = ctrl.onChange(onSnap);
ctrl.ready?.catch?.((e) => console.error(`${cfg.title} ${view} init failed`, e));
if (cfg.grid && view === "generated") {
grid.setCtrl(ctrl);
grid.setActive(showGrid);
}
onSnap(null);
}
render3();
return { destroy() {
teardown();
rootEl.remove();
} };
}
var GEN_MODES = [{ key: "generated", label: "Generated" }, { key: "tilesheet", label: "Tilesheet" }, { key: "reference", label: "Reference" }];
var SKIN_MODES = [{ key: "generated", label: "Generated" }, { key: "reference", label: "Reference" }, { key: "tilesheet", label: "Tilesheet" }];
var skinList = (skins) => Object.entries(skins).map(([key, s]) => ({ key, label: s.label }));
var sceneFromRef = (pixi, host, ref) => createSceneViewer(pixi, host, { layers: ref.layers, dir: ref.dir, sceneW: ref.w, sceneH: ref.h, background: ref.bg, pulse: ref.pulse });
var PAGES = {
world: (pixi, body, opts) => makePage(pixi, body, {
title: "World Map",
subtitle: () => "overworld",
modes: [{ key: "generated", label: "Generated" }],
grid: true,
initialSeed: opts?.seed,
makeGenerated: (p, host, seed) => createOverworldMap(p, host, { seed }),
generatedMeta: infiniteMeta
}),
necropolis: (pixi, body) => makePage(pixi, body, {
title: "Necropolis",
modes: GEN_MODES,
grid: true,
makeGenerated: (p, host, seed) => createNecropolisMap(p, host, { seed }),
makeScene: (p, host) => createSceneViewer(p, host, { layers: NECRO_LAYERS, dir: NECRO_EXT_DIR, sceneW: NECRO_SCENE, sceneH: NECRO_SCENE, pulse: { key: "a-undeadflames" } }),
sheets: () => NECRO_SHEETS,
generatedMeta: infiniteMeta
}),
orc: (pixi, body) => makePage(pixi, body, {
title: "Orc Kingdom",
modes: GEN_MODES,
grid: true,
makeGenerated: (p, host, seed) => createOrcMap(p, host, { seed }),
makeScene: (p, host) => createSceneViewer(p, host, { layers: ORC_LAYERS, dir: ORC_LAYER_DIR, sceneW: ORC_SCENE.w, sceneH: ORC_SCENE.h }),
sheets: () => ORC_SHEETS,
generatedMeta: infiniteMeta
}),
plains: (pixi, body) => makePage(pixi, body, {
title: "Forgotten Plains",
modes: GEN_MODES,
grid: true,
mockup: FP_MOCKUP_URL,
makeGenerated: (p, host, seed) => createForgottenPlainsMap(p, host, { seed }),
sheets: () => FP_SHEETS,
generatedMeta: infiniteMeta
}),
interiors: (pixi, body) => makePage(pixi, body, {
title: "Interiors",
modes: SKIN_MODES,
skins: skinList(INTERIOR_SKINS),
grid: false,
makeGenerated: (p, host, seed, skin) => createInteriorViewer(p, host, { skin, seed }),
makeScene: (p, host, skin) => sceneFromRef(p, host, INTERIOR_REF[skin]),
sheets: () => skinList(INTERIOR_SKINS).map((s) => ({ key: s.key, label: s.label, url: INTERIOR_SKINS[s.key].url })),
generatedMeta: (snap, label2) => snap ? `${snap.rooms} rooms \xB7 ${label2} \xB7 seed ${snap.seed}` : "loading\u2026"
}),
towers: (pixi, body) => makePage(pixi, body, {
title: "Towers",
modes: SKIN_MODES,
skins: skinList(TOWER_SKINS),
grid: false,
makeGenerated: (p, host, seed, skin) => createTowerViewer(p, host, { skin, seed }),
makeScene: (p, host, skin) => sceneFromRef(p, host, TOWER_REF[skin]),
sheets: () => skinList(TOWER_SKINS).map((s) => ({ key: s.key, label: s.label, url: TOWER_SKINS[s.key].url })),
generatedMeta: (snap, label2) => snap ? `pyramid \xB7 battlement \xB7 ruin \xB7 ${label2} \xB7 seed ${snap.seed}` : "loading\u2026"
})
};
var PAGE_LIST = [
["world", "World Map"],
["necropolis", "Necropolis"],
["orc", "Orc Kingdom"],
["plains", "Forgotten Plains"],
["interiors", "Interiors"],
["towers", "Towers"]
];
function mountMapSandbox(pixi, host, opts = {}) {
host.innerHTML = "";
const pills = h("div", { class: "mapsandbox-pills" });
const body = h("div", { class: "mapsandbox-body" });
const root = h(
"div",
{ class: "mapsandbox" },
h("header", { class: "mapsandbox-header" }, h("h1", {}, "Map"), pills),
body
);
host.append(root);
let current = null;
const btns = PAGE_LIST.map(([key, label2]) => {
const b = h("button", { "data-testid": `map-page-${key}`, onClick: () => select(key) }, label2);
pills.append(b);
return [key, b];
});
function select(key) {
try {
current?.destroy?.();
} catch {
}
body.innerHTML = "";
btns.forEach(([k, b]) => b.classList.toggle("active", k === key));
current = PAGES[key](pixi, body, opts);
}
select(PAGES[opts.page] ? opts.page : "world");
return { destroy() {
try {
current?.destroy?.();
} catch {
}
current = null;
host.innerHTML = "";
} };
}
export {
mountMapSandbox
};