284 lines
No EOL
11 KiB
JavaScript
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"));
|
|
}
|
|
|
|
})(); |