From c8964aa859e2f2630acac6f4c15c358949d8ac04 Mon Sep 17 00:00:00 2001 From: onms Date: Mon, 22 Dec 2025 03:23:15 -0600 Subject: [PATCH 1/3] Map Screenshotting mod --- .../(FullMapScreenshot Mod Information).txt | 6 + js/plugins.js | 6 + js/plugins/FullMapScreenshot.js | 284 ++++++++++++++++++ 3 files changed, 296 insertions(+) create mode 100644 (FullMapScreenshot)/(FullMapScreenshot Mod Information).txt create mode 100644 js/plugins/FullMapScreenshot.js diff --git a/(FullMapScreenshot)/(FullMapScreenshot Mod Information).txt b/(FullMapScreenshot)/(FullMapScreenshot Mod Information).txt new file mode 100644 index 0000000..955909a --- /dev/null +++ b/(FullMapScreenshot)/(FullMapScreenshot Mod Information).txt @@ -0,0 +1,6 @@ +This is the folder that any images you take using the FullMapScreenshot mod will go. + +The hotkey to activate this mod is "O". After hitting "O", this mod will render multiple pictures of the map you are currently on, stitch them together, save them to this folder as a ".jpg", and then relocate the camera back onto the character you are controlling. + +By default, this mod will be disabled. If you want to use it, you will first need to toggle it on in the game's options (this will be memorized for future game launches). +There is also an additional feature which displays connected locations. If you want to take a clean screenshot of the map, you can toggle this feature off. \ No newline at end of file diff --git a/js/plugins.js b/js/plugins.js index 5b07ba7..7ba8928 100644 --- a/js/plugins.js +++ b/js/plugins.js @@ -2551,4 +2551,10 @@ var $plugins = [ "Toggle Key": "77", }, }, + { + "name": "FullMapScreenshot", + "status": true, + description: "Press O to take a screenshot of the current map, which will be saved to (FMS Mod Screenshots)", + "parameters": {}, + }, ]; diff --git a/js/plugins/FullMapScreenshot.js b/js/plugins/FullMapScreenshot.js new file mode 100644 index 0000000..93cc6b1 --- /dev/null +++ b/js/plugins/FullMapScreenshot.js @@ -0,0 +1,284 @@ +/*: + * @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")); + } + +})(); \ No newline at end of file From 95160e140aaad25ebb29e4fe0eb033c064502c4d Mon Sep 17 00:00:00 2001 From: onms Date: Mon, 22 Dec 2025 03:41:53 -0600 Subject: [PATCH 2/3] plugins description adjustment --- js/plugins.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/js/plugins.js b/js/plugins.js index 7ba8928..041a224 100644 --- a/js/plugins.js +++ b/js/plugins.js @@ -2554,7 +2554,7 @@ var $plugins = [ { "name": "FullMapScreenshot", "status": true, - description: "Press O to take a screenshot of the current map, which will be saved to (FMS Mod Screenshots)", + description: "Press O to take a screenshot of the current map, which will be saved to (FullMapScreenshot)", "parameters": {}, }, ]; From dd8df185aecbd287755200e0ec52879047c7ea2e Mon Sep 17 00:00:00 2001 From: onms Date: Mon, 22 Dec 2025 17:27:36 -0600 Subject: [PATCH 3/3] fixed a few bad lines --- data/CommonEvents.json | 4 ++-- data/Items.json | 2 +- data/Map040.json | 16 ++++++++-------- data/Map150.json | 2 +- 4 files changed, 12 insertions(+), 12 deletions(-) diff --git a/data/CommonEvents.json b/data/CommonEvents.json index 346c75a..5110c55 100644 --- a/data/CommonEvents.json +++ b/data/CommonEvents.json @@ -64607,7 +64607,7 @@ 105, 0, 4, - "`Pinned down by a goblin aroused by \\n[1]'s scent.`" + "`Pinned down by a goblin aroused by \\\\n[1]'s scent.`" ] }, { @@ -64673,7 +64673,7 @@ 20, 0, 4, - "`Her virginity is taken in missionary as he sucks on her nipples.`" + "`Her virginity was taken in\\nmissionary as he sucked\non her nipples.`" ] }, { diff --git a/data/Items.json b/data/Items.json index b12c7e2..6f30c39 100644 --- a/data/Items.json +++ b/data/Items.json @@ -10010,7 +10010,7 @@ "iconIndex": 94, "itypeId": 3, "name": "Beneath the Prison...", - "note": "\n\n\n", + "note": "\n\n\n", "occasion": 2, "price": 30, "repeats": 1, diff --git a/data/Map040.json b/data/Map040.json index 7920db3..0f514b6 100644 --- a/data/Map040.json +++ b/data/Map040.json @@ -69283,7 +69283,7 @@ 105, 0, 4, - "`Caught in a teleport trap, ass stuck in the wall, violated by a goblin.`" + "`Caught in a teleport trap, ass stuck in the wall,\\n violated by goblins.`" ] }, { @@ -69323,7 +69323,7 @@ 20, 0, 4, - "`Trapped by a teleport trap, violated while ass is stuck in the wall.`" + "`Trapped by a teleport\\ntrap, violated while ass\\nwas stuck in the wall.`" ] }, { @@ -70867,7 +70867,7 @@ 105, 0, 4, - "`Caught in a teleport trap, ass stuck in the wall, violated by a goblin.`" + "`Caught in a teleport trap, ass stuck in the wall,\\n violated by goblins.`" ] }, { @@ -70907,7 +70907,7 @@ 20, 0, 4, - "`Trapped by a teleport trap, violated while ass is stuck in the wall.`" + "`Trapped by a teleport\\ntrap, violated while ass\\nwas stuck in the wall.`" ] }, { @@ -72170,7 +72170,7 @@ 105, 0, 4, - "`Caught in a teleport trap, ass stuck in the wall, violated by a goblin.`" + "`Caught in a teleport trap, ass stuck in the wall,\\n violated by goblins.`" ] }, { @@ -72210,7 +72210,7 @@ 20, 0, 4, - "`Trapped by a teleport trap, violated while ass is stuck in the wall.`" + "`Trapped by a teleport\\ntrap, violated while ass\\nwas stuck in the wall.`" ] }, { @@ -73548,7 +73548,7 @@ 105, 0, 4, - "`Caught in a teleport trap, ass stuck in the wall, violated by a goblin.`" + "`Caught in a teleport trap, ass stuck in the wall,\\n violated by goblins.`" ] }, { @@ -73588,7 +73588,7 @@ 20, 0, 4, - "`Trapped by a teleport trap, violated while ass is stuck in the wall.`" + "`Trapped by a teleport\\ntrap, violated while ass\\nwas stuck in the wall.`" ] }, { diff --git a/data/Map150.json b/data/Map150.json index 07eaf5f..3449a91 100644 --- a/data/Map150.json +++ b/data/Map150.json @@ -10019,7 +10019,7 @@ 105, 0, 4, - "`Take a babysitting job for noble child Pipin in the upper residential district.`" + "`Take a babysitting job for noble child Pipin in the upper\\nresidential district.`" ] }, {