Implement overlay sprite rendering and update API endpoints for overlays

This commit is contained in:
Marco 2026-03-27 13:12:45 +01:00
commit 06e67d8341
6 changed files with 145 additions and 82 deletions

View file

@ -29,6 +29,7 @@ function ensureDir(dirPath) {
}
const DOWNLOAD_CACHE_ROOT = path.join(TILE_CACHE_ROOT, "downloads");
const RENDER_CACHE_VERSION = "v2-overlays-as-sprites";
sharp.cache(false);
function normalizeBuildOptions(options = {}) {
@ -39,7 +40,7 @@ function normalizeBuildOptions(options = {}) {
}
function buildOptionSuffix(options) {
return `editor-${options.includeEditor ? "on" : "off"}_roofs-${options.includeRoofs ? "on" : "off"}`;
return `${RENDER_CACHE_VERSION}_editor-${options.includeEditor ? "on" : "off"}_roofs-${options.includeRoofs ? "on" : "off"}`;
}
function toHex(value, width = 4) {
@ -189,6 +190,7 @@ function serializeOverlayItem(node, minLeft, minTop, index) {
return {
id: `${index}:${item.source}:${item.shape}:${item.frame}:${item.x}:${item.y}:${item.z}`,
index,
kind,
label: overlayLabel(kind),
family: info.family,
@ -238,6 +240,9 @@ function serializeOverlayItem(node, minLeft, minTop, index) {
invitem: info.isInvitem,
animType: info.animType
},
presentation: {
opacity: kind === "helper" ? 0.5 : info.isTranslucent ? 0.7 : 1
},
notes: overlayNotes(item, info)
};
}
@ -410,6 +415,8 @@ export class BuildManager {
assets,
prepared: [],
overlays: [],
overlayNodes: [],
overlayNodeById: new Map(),
minLeft: 0,
minTop: 0,
width: TILE_SIZE,
@ -462,6 +469,8 @@ export class BuildManager {
assets,
prepared: [],
overlays: [],
overlayNodes: [],
overlayNodeById: new Map(),
minLeft: 0,
minTop: 0,
width: TILE_SIZE,
@ -503,6 +512,10 @@ export class BuildManager {
const overlays = overlayProjection.projected.map((node, index) =>
serializeOverlayItem(node, bounds.minLeft, bounds.minTop, index)
);
const overlayNodeById = new Map();
for (let index = 0; index < overlays.length; index += 1) {
overlayNodeById.set(overlays[index].id, overlayProjection.projected[index]);
}
const invalidItemCount = sorted.invalidItemCount + overlayProjection.invalidItemCount;
const invalidItems = [...sorted.invalidItems, ...overlayProjection.invalidItems].slice(0, 20);
@ -551,6 +564,8 @@ export class BuildManager {
assets,
prepared: sorted.prepared,
overlays,
overlayNodes: overlayProjection.projected,
overlayNodeById,
minLeft: bounds.minLeft,
minTop: bounds.minTop,
width: bounds.width,
@ -623,6 +638,63 @@ export class BuildManager {
};
}
async renderOverlaySprite(jobId, gameId, mapId, overlayId, format = "webp") {
const job = this.requireReadyJob(jobId, gameId, mapId);
const overlay = job.build.overlays.find((item) => item.id === overlayId);
const node = job.build.overlayNodeById.get(overlayId);
if (!overlay || !node) {
throw new Error("Unknown overlay id");
}
const extension = format === "png" ? "png" : "webp";
const spritePath = path.join(
TILE_CACHE_ROOT,
gameId,
`map-${mapId}`,
buildOptionSuffix(job.options),
"overlays",
`${overlay.index}.${extension}`
);
if (fs.existsSync(spritePath)) {
return fs.readFileSync(spritePath);
}
const spriteWidth = Math.max(1, overlay.screen.width);
const spriteHeight = Math.max(1, overlay.screen.height);
const buffer = rgbaBuffer(spriteWidth, spriteHeight, [0, 0, 0, 0]);
blitFrame(
buffer,
spriteWidth,
spriteHeight,
0,
0,
node.frame,
node.pixels,
job.build.assets.palette,
Boolean(node.item.flags & FLAG_FLIPPED)
);
let output;
if (format === "png") {
output = encodePng(spriteWidth, spriteHeight, buffer);
} else {
output = await sharp(buffer, {
raw: {
width: spriteWidth,
height: spriteHeight,
channels: 4
},
limitInputPixels: false
})
.webp({ lossless: true, effort: 4 })
.toBuffer();
}
ensureDir(path.dirname(spritePath));
fs.writeFileSync(spritePath, output);
return output;
}
async renderFullMap(jobId, gameId, mapId) {
const job = this.requireReadyJob(jobId, gameId, mapId);
const outputPath = path.join(