unholy-maiden/js/plugins/FullMapScreenshot.js
2025-12-22 03:23:15 -06:00

284 lines
No EOL
11 KiB
JavaScript

/*:
* @target MZ
* @plugindesc Full stitched map screenshot with optional exit labels/boxes and robust capture logic. Options toggle at bottom. Press O. NW.js only.
*
* @help
* Features:
* - Full map screenshot with multi-segment stitching
* - Optional exit labels and boxes
* - Robust recheck to ensure no segments are missing
* - Camera restored after capture
* - Saves as JPEG in "(FullMapScreenshot)"
* - Options toggle at bottom
*/
(() => {
const KEY_O = 79;
const FRAME_WAIT = 5;
const CROP_TOP_FRAC = 0.15;
const CROP_RIGHT_FRAC = 0.35;
const BOX_X_OFFSET = -4;
const BOX_Y_OFFSET = -2;
const MAX_CHARS_PER_LINE = 14;
const LINE_HEIGHT = 24;
let capturing = false;
// --- ConfigManager defaults ---
if (ConfigManager.mapScreenshotsEnabled === undefined) ConfigManager.mapScreenshotsEnabled = false;
if (ConfigManager.mapScreenshotsShowLabels === undefined) ConfigManager.mapScreenshotsShowLabels = true;
const _ConfigManager_makeData = ConfigManager.makeData;
ConfigManager.makeData = function() {
const config = _ConfigManager_makeData.call(this);
config.mapScreenshotsEnabled = this.mapScreenshotsEnabled;
config.mapScreenshotsShowLabels = this.mapScreenshotsShowLabels;
return config;
};
const _ConfigManager_applyData = ConfigManager.applyData;
ConfigManager.applyData = function(config) {
_ConfigManager_applyData.call(this, config);
this.mapScreenshotsEnabled = config.mapScreenshotsEnabled ?? false;
this.mapScreenshotsShowLabels = config.mapScreenshotsShowLabels ?? true;
};
// --- Options menu patch ---
const _Scene_Boot_start = Scene_Boot.prototype.start;
Scene_Boot.prototype.start = function() {
_Scene_Boot_start.call(this);
if (!Window_Options.prototype._mapScreenshotPatched) {
patchOptionsWindow();
Window_Options.prototype._mapScreenshotPatched = true;
}
};
function patchOptionsWindow() {
const _Window_Options_makeCommandList = Window_Options.prototype.makeCommandList;
Window_Options.prototype.makeCommandList = function() {
_Window_Options_makeCommandList.call(this);
if (!this._list.some(c => c.symbol === "mapScreenshotsEnabled")) {
this.addCommand("Map Screenshot Mod (O)", "mapScreenshotsEnabled");
}
if (!this._list.some(c => c.symbol === "mapScreenshotsShowLabels")) {
this.addCommand(" Show Labels/Boxes", "mapScreenshotsShowLabels");
}
};
}
// --- Hotkey ---
document.addEventListener("keydown", async e => {
if (
e.keyCode === KEY_O &&
ConfigManager.mapScreenshotsEnabled &&
!capturing &&
SceneManager._scene instanceof Scene_Map
) {
capturing = true;
try {
await captureFullMap();
} catch (err) {
console.error(err);
}
capturing = false;
}
});
// --- Capture logic ---
async function captureFullMap() {
const scene = SceneManager._scene;
const spriteset = scene._spriteset;
const mapW = $gameMap.width() * $gameMap.tileWidth();
const mapH = $gameMap.height() * $gameMap.tileHeight();
const screenW = Graphics.width;
const screenH = Graphics.height;
const cropTop = Math.floor(screenH * CROP_TOP_FRAC);
const cropRight = Math.floor(screenW * CROP_RIGHT_FRAC);
const effW = screenW - cropRight;
const effH = screenH - cropTop;
const cols = Math.ceil(mapW / effW);
const rows = Math.ceil(mapH / effH);
const canvas = document.createElement("canvas");
canvas.width = mapW;
canvas.height = mapH;
const ctx = canvas.getContext("2d");
const oldX = $gameMap.displayX();
const oldY = $gameMap.displayY();
const oldPlayerX = $gamePlayer.x;
const oldPlayerY = $gamePlayer.y;
const exitPoints = collectExitPoints();
const originalUpdate = Scene_Map.prototype.update;
Scene_Map.prototype.update = function () {};
const renderer = Graphics.app.renderer;
const segments = [];
for (let y = 0; y < rows; y++) {
for (let x = 0; x < cols; x++) {
const dx = (x * effW) / $gameMap.tileWidth();
const dy = (y * effH) / $gameMap.tileHeight();
$gameMap.setDisplayPos(dx, dy);
const tmpCanvas = await captureSegment(spriteset, scene, effW, effH, cropTop, FRAME_WAIT);
ctx.drawImage(tmpCanvas, x * effW, y * effH);
segments.push({ x, y, dx, dy, tmpCanvas });
}
}
await verifySegments(canvas, segments, effW, effH, spriteset, scene, cropTop);
if (ConfigManager.mapScreenshotsShowLabels) {
drawExitLabels(ctx, exitPoints);
}
Scene_Map.prototype.update = originalUpdate;
$gameMap.setDisplayPos(oldX, oldY);
$gamePlayer.center(oldPlayerX, oldPlayerY);
saveCanvas(canvas);
}
async function captureSegment(spriteset, scene, effW, effH, cropTop, frameWait) {
const renderer = Graphics.app.renderer;
for (let f = 0; f < frameWait; f++) {
scene.update();
spriteset.update();
Graphics.app.render();
await nextFrame();
}
const snapCanvas = renderer.extract.canvas(spriteset);
const tmp = document.createElement("canvas");
tmp.width = effW;
tmp.height = effH;
tmp.getContext("2d").drawImage(snapCanvas, 0, cropTop, effW, effH, 0, 0, effW, effH);
return tmp;
}
async function verifySegments(canvas, segments, effW, effH, spriteset, scene, cropTop) {
const ctx = canvas.getContext("2d");
for (const seg of segments) {
const data = ctx.getImageData(seg.x * effW, seg.y * effH, effW, effH).data;
let fullyDrawn = true;
for (let i = 0; i < data.length; i += 4) {
if (data[i + 3] === 0) {
fullyDrawn = false;
break;
}
}
if (!fullyDrawn) {
$gameMap.setDisplayPos(seg.dx, seg.dy);
const fixed = await captureSegment(spriteset, scene, effW, effH, cropTop, FRAME_WAIT);
ctx.drawImage(fixed, seg.x * effW, seg.y * effH);
}
}
}
function collectExitPoints() {
const exits = [];
const currentMapId = $gameMap.mapId();
for (const ev of $gameMap.events()) {
if (!ev) continue;
for (const page of ev.event().pages) {
for (const cmd of page.list) {
if (cmd.code === 201) {
const destMapId = cmd.parameters[1];
if (destMapId === currentMapId) continue;
const mapInfo = $dataMapInfos[destMapId];
if (mapInfo) exits.push({ name: mapInfo.name, x: ev.x, y: ev.y });
break;
}
}
}
}
return exits;
}
function drawExitLabels(ctx, exits) {
const tw = $gameMap.tileWidth();
const th = $gameMap.tileHeight();
ctx.font = "22px sans-serif";
ctx.textAlign = "center";
ctx.textBaseline = "bottom";
const visited = new Set();
const clusters = [];
for (let i = 0; i < exits.length; i++) {
if (visited.has(i)) continue;
const queue = [i];
const cluster = [];
while (queue.length) {
const idx = queue.shift();
if (visited.has(idx)) continue;
visited.add(idx);
cluster.push(exits[idx]);
for (let j = 0; j < exits.length; j++) {
if (visited.has(j)) continue;
if (exits[j].name !== exits[idx].name) continue;
const dx = Math.abs(exits[idx].x - exits[j].x);
const dy = Math.abs(exits[idx].y - exits[j].y);
if (Math.max(dx, dy) <= 1) queue.push(j);
}
}
clusters.push(cluster);
}
for (const cluster of clusters) {
cluster.sort((a, b) => (a.y - b.y) || (a.x - b.x));
const mid = cluster[Math.floor(cluster.length / 2)];
ctx.lineWidth = 2;
ctx.strokeStyle = "red";
for (const ex of cluster) {
const x = (ex.x + BOX_X_OFFSET) * tw;
const y = (ex.y + BOX_Y_OFFSET) * th;
ctx.strokeRect(x, y, tw, th);
}
const lines = wrapTextWords(mid.name, MAX_CHARS_PER_LINE);
const blockHeight = lines.length * LINE_HEIGHT;
let px = (mid.x + BOX_X_OFFSET + 0.5) * tw;
let baseY = (mid.y + BOX_Y_OFFSET) * th - 4;
const halfWidth = Math.max(...lines.map(l => ctx.measureText(l).width)) / 2;
px = Math.max(halfWidth, Math.min(ctx.canvas.width - halfWidth, px));
if (baseY < blockHeight) baseY = blockHeight;
for (let i = 0; i < lines.length; i++) {
const y = baseY - (blockHeight - i * LINE_HEIGHT);
ctx.lineWidth = 6;
ctx.strokeStyle = "black";
ctx.strokeText(lines[i], px, y);
ctx.lineWidth = 4;
ctx.strokeText(lines[i], px, y);
ctx.fillStyle = "#b862ffff";
ctx.fillText(lines[i], px, y);
}
}
}
function wrapTextWords(text, maxChars) {
const words = text.split(" ");
const lines = [];
let current = "";
for (const word of words) {
if ((current + (current ? " " : "") + word).length <= maxChars) {
current += (current ? " " : "") + word;
} else {
if (current) lines.push(current);
current = word;
}
}
if (current) lines.push(current);
return lines;
}
function nextFrame() {
return new Promise(r => requestAnimationFrame(r));
}
function saveCanvas(canvas) {
if (!Utils.isNwjs()) return;
const fs = require("fs");
const path = require("path");
const dir = path.join(process.cwd(), "(FullMapScreenshot)");
if (!fs.existsSync(dir)) fs.mkdirSync(dir);
const file = path.join(dir, `map_${$gameMap.mapId()}_${Date.now()}.jpg`);
const data = canvas.toDataURL("image/jpeg", 0.9).replace(/^data:image\/jpeg;base64,/, "");
fs.writeFileSync(file, Buffer.from(data, "base64"));
}
})();