Merge branch 'main' into 'main'
Map Screenshotting mod See merge request dazed-translations/unholy-maiden!11
This commit is contained in:
commit
72e3854a06
7 changed files with 308 additions and 12 deletions
|
|
@ -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.
|
||||
|
|
@ -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.`"
|
||||
]
|
||||
},
|
||||
{
|
||||
|
|
|
|||
|
|
@ -10010,7 +10010,7 @@
|
|||
"iconIndex": 94,
|
||||
"itypeId": 3,
|
||||
"name": "Beneath the Prison...",
|
||||
"note": "<SG説明:Those who have committed especially\nheinous crimes are imprisoned here.\nOr so they say, but in reality, it's often used\nto throw in anyone who gets in the way of those\nin power, without any procedure at all.\nIf someone close to you suddenly disappears,\nthe first place you should suspect is the\nprison.>\n\n<SGピクチャ:system_event_window16>\n<SGテキスト位置:bottom>",
|
||||
"note": "<SG説明:Those who have committed especially\nheinous crimes are imprisoned here.\nOr so they say, but in reality, it's often used\nto throw in anyone who gets in the way of\nthose in power, without any procedure at all.\nIf someone close to you suddenly disappears,\nthe first place you should suspect is the\nprison.>\n\n<SGピクチャ:system_event_window16>\n<SGテキスト位置:bottom>",
|
||||
"occasion": 2,
|
||||
"price": 30,
|
||||
"repeats": 1,
|
||||
|
|
|
|||
|
|
@ -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.`"
|
||||
]
|
||||
},
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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.`"
|
||||
]
|
||||
},
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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 (FullMapScreenshot)",
|
||||
"parameters": {},
|
||||
},
|
||||
];
|
||||
|
|
|
|||
284
js/plugins/FullMapScreenshot.js
Normal file
284
js/plugins/FullMapScreenshot.js
Normal file
|
|
@ -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"));
|
||||
}
|
||||
|
||||
})();
|
||||
Loading…
Reference in a new issue