From e9402750ea3d2fb65f92b221835b3adbf04ab6a7 Mon Sep 17 00:00:00 2001 From: slaguru666 <111923774+slaguru666@users.noreply.github.com> Date: Mon, 25 May 2026 17:40:41 +0100 Subject: [PATCH] Premium procedurals: audio atmos, loot tiles, hazard regions, v13 teleporters, styled BPN briefs --- module.json | 2 +- module/scifi-deckplan-generator.mjs | 347 +++++++++++++++++++++++++++- 2 files changed, 341 insertions(+), 8 deletions(-) diff --git a/module.json b/module.json index ffdba4b..41bc731 100644 --- a/module.json +++ b/module.json @@ -2,7 +2,7 @@ "id": "scifi-deckplan-generator", "title": "Sci-Fi & Fantasy Map Generator", "description": "Procedurally generates sci-fi deck plans, fantasy dungeons, horror buildings, wilderness encounter maps, and tactical battlemaps as Foundry VTT scenes.", - "version": "0.9.2", + "version": "0.9.3", "compatibility": { "minimum": "13", "verified": "13.350", diff --git a/module/scifi-deckplan-generator.mjs b/module/scifi-deckplan-generator.mjs index dc5b170..e68a226 100644 --- a/module/scifi-deckplan-generator.mjs +++ b/module/scifi-deckplan-generator.mjs @@ -1014,18 +1014,19 @@ async function deleteGeneratedDocuments(scene) { return; } - const { wallIds, drawingIds, tileIds, lightIds, tokenIds, noteIds } = collectGeneratedSceneIds(scene); + const ids = collectGeneratedSceneIds(scene); + const total = ids.wallIds.length + ids.drawingIds.length + ids.tileIds.length + ids.lightIds.length + ids.tokenIds.length + ids.noteIds.length + ids.soundIds.length + ids.regionIds.length; - if (!wallIds.length && !drawingIds.length && !tileIds.length && !lightIds.length && !tokenIds.length && !noteIds.length) { + if (total === 0) { ui.notifications.info("No generated elements found in this scene."); return; } await Dialog.confirm({ title: "Delete Generated Elements", - content: `
Remove ${wallIds.length} walls, ${drawingIds.length} drawings, ${tileIds.length} tiles, ${lightIds.length} lights, ${tokenIds.length} encounter tokens, and ${noteIds.length} generated notes from ${escapeXml(scene.name)}?
`, + content: `Remove ${ids.wallIds.length} walls, ${ids.drawingIds.length} drawings, ${ids.tileIds.length} tiles, ${ids.lightIds.length} lights, ${ids.tokenIds.length} encounter tokens, ${ids.noteIds.length} generated notes, ${ids.soundIds.length} ambient sounds, and ${ids.regionIds.length} regions from ${escapeXml(scene.name)}?
`, yes: async () => { - await deleteGeneratedDocumentsInternal(scene, { wallIds, drawingIds, tileIds, lightIds, tokenIds, noteIds }); + await deleteGeneratedDocumentsInternal(scene, ids); ui.notifications.info(`Deleted generated elements from ${scene.name}.`); } }); @@ -1094,7 +1095,9 @@ function collectGeneratedSceneIds(scene) { tileIds: scene.tiles.contents.filter((tile) => tile.flags?.[MODULE_ID]?.generatedTile).map((tile) => tile.id), lightIds: scene.lights.contents.filter((light) => light.flags?.[MODULE_ID]?.generatedLight).map((light) => light.id), tokenIds: scene.tokens.contents.filter((token) => token.flags?.[MODULE_ID]?.generatedToken).map((token) => token.id), - noteIds: scene.notes.contents.filter((note) => note.flags?.[MODULE_ID]?.generatedNote).map((note) => note.id) + noteIds: scene.notes.contents.filter((note) => note.flags?.[MODULE_ID]?.generatedNote).map((note) => note.id), + soundIds: scene.sounds.contents.filter((sound) => sound.flags?.[MODULE_ID]?.generatedSound).map((sound) => sound.id), + regionIds: scene.regions?.contents?.filter((region) => region.flags?.[MODULE_ID]?.generatedRegion)?.map((region) => region.id) ?? [] }; } @@ -1105,6 +1108,8 @@ async function deleteGeneratedDocumentsInternal(scene, ids = collectGeneratedSce if (ids.lightIds?.length) await scene.deleteEmbeddedDocuments("AmbientLight", ids.lightIds); if (ids.tokenIds?.length) await scene.deleteEmbeddedDocuments("Token", ids.tokenIds); if (ids.noteIds?.length) await scene.deleteEmbeddedDocuments("Note", ids.noteIds); + if (ids.soundIds?.length) await scene.deleteEmbeddedDocuments("AmbientSound", ids.soundIds); + if (ids.regionIds?.length) await scene.deleteEmbeddedDocuments("Region", ids.regionIds); } function normalizeFormState(raw = {}) { @@ -3197,6 +3202,11 @@ async function populateSceneWithDeck(scene, deckPlan, deck, formState, connector await scene.createEmbeddedDocuments("Tile", [withGeneratedTileFlag(gmOverlayTile)]); } + const lootTiles = buildLootTiles(deck, formState); + if (lootTiles.length) { + await scene.createEmbeddedDocuments("Tile", lootTiles.map((t) => withGeneratedTileFlag(t))); + } + const walls = deck.wallSegments.map((segment) => wallSegmentToDocument(segment, formState.gridSize)); if (walls.length) { await scene.createEmbeddedDocuments("Wall", walls.map((wall) => withGeneratedWallFlag(wall))); @@ -3222,6 +3232,11 @@ async function populateSceneWithDeck(scene, deckPlan, deck, formState, connector } } + const sounds = buildAmbientSounds(deck, formState); + if (sounds.length) { + await scene.createEmbeddedDocuments("AmbientSound", sounds.map((s) => withGeneratedSoundFlag(s))); + } + if (formState.seedEncounters) { const tokens = await buildEncounterTokens(deck, formState); if (tokens.length) { @@ -3238,7 +3253,10 @@ async function populateSceneWithDeck(scene, deckPlan, deck, formState, connector if (formState.placeRegions) { try { - const regions = buildRegionDocuments(deck, formState); + const regions = [ + ...buildRegionDocuments(deck, formState, deckPlan), + ...buildHazardRegions(deck, formState) + ]; if (regions.length) { await scene.createEmbeddedDocuments("Region", regions); } @@ -3262,7 +3280,7 @@ async function populateSceneWithDeck(scene, deckPlan, deck, formState, connector applyGmOverlayVisibility(scene); } -function buildRegionDocuments(deck, formState) { +function buildRegionDocuments(deck, formState, deckPlan = null) { const gs = formState.gridSize; const regions = []; for (const room of deck.rooms) { @@ -3285,6 +3303,64 @@ function buildRegionDocuments(deck, formState) { flags: { [MODULE_ID]: { generatedRegion: true, roomType: room.type } } }); } + + // Multi-Deck Elevator/Ladder/Stairs Teleporters (v13+) + if (deckPlan && deckPlan.decks.length > 1 && deck.deckConnectors?.length) { + for (const connector of deck.deckConnectors) { + const currentDeckIndex = deck.deckIndex; + let targetDeckIndex = null; + if (connector.type === "elevator") { + targetDeckIndex = currentDeckIndex < deckPlan.decks.length - 1 ? currentDeckIndex + 1 : currentDeckIndex - 1; + } else { + const labelUpper = String(connector.label || "").toUpperCase(); + if (labelUpper.includes("UP")) { + targetDeckIndex = currentDeckIndex + 1; + } else if (labelUpper.includes("DOWN")) { + targetDeckIndex = currentDeckIndex - 1; + } else { + targetDeckIndex = currentDeckIndex < deckPlan.decks.length - 1 ? currentDeckIndex + 1 : currentDeckIndex - 1; + } + } + + const targetDeck = deckPlan.decks[targetDeckIndex]; + if (targetDeck && targetDeck.sceneId) { + const targetConnector = targetDeck.deckConnectors?.find((tc) => tc.label === connector.label) ?? targetDeck.deckConnectors?.[0]; + if (targetConnector) { + const cx = connector.x * gs; + const cy = connector.y * gs; + const pts = [ + cx, cy, + cx + gs, cy, + cx + gs, cy + gs, + cx, cy + gs + ]; + + const targetX = Math.round(targetConnector.x * gs + gs / 2); + const targetY = Math.round(targetConnector.y * gs + gs / 2); + + regions.push({ + name: `Transit: ${connector.label}`, + color: "#00e5ff", + visibility: 0, + shapes: [{ type: "polygon", points: pts }], + behaviors: [{ + type: "teleportToken", + name: `Transit to ${targetDeck.deckName}`, + disabled: false, + destination: { + scene: targetDeck.sceneId, + x: targetX, + y: targetY, + elevation: 0 + } + }], + flags: { [MODULE_ID]: { generatedRegion: true, isTransit: true } } + }); + } + } + } + } + return regions; } @@ -5185,6 +5261,20 @@ function withGeneratedNoteFlag(note) { }; } +function withGeneratedSoundFlag(sound) { + return { + ...sound, + flags: { + ...(sound.flags ?? {}), + [MODULE_ID]: { + ...(sound.flags?.[MODULE_ID] ?? {}), + generatedSound: true + } + } + }; +} + + async function buildEncounterTokens(deck, formState) { const preset = resolveEncounterPreset(formState); const packActors = await getEncounterPackActors(formState.encounterPack); @@ -6531,6 +6621,82 @@ function buildMissionBriefContent(deckPlan, formState) { const intro = typeof introSource === "string" ? introSource : introSource?.[formState.theme] ?? "The following location has been mapped from the best data currently available. Treat all positions as approximate until confirmed on the ground."; + + if (chosenTemplate === "sla") { + const seedNum = hashSeed(deckPlan.seed); + const sectorCode = `SEC-${(seedNum % 900 + 100)}-${String.fromCharCode(65 + (seedNum % 26))}${String.fromCharCode(65 + ((seedNum >> 8) % 26))}`; + const threatLevel = { + light: { label: "GREEN", color: "#4cd964", desc: "MINIMAL RESISTANCE / LOW RISK" }, + standard: { label: "YELLOW", color: "#ffcc00", desc: "ACTIVE THREATS / CAUTION REQUIRED" }, + heavy: { label: "RED", color: "#ff3b30", desc: "WAR ZONE / IMMEDIATE DANGER" } + }[formState.encounterDensity] ?? { label: "AMBER", color: "#ff9500", desc: "UNSTABLE ENVIRONMENT" }; + + const payout = (15000 + (seedNum % 35000) + (deckPlan.decks.length * 10000)).toLocaleString(); + const deckLines = deckPlan.decks.map((deck) => ` ++ ${escapeXml(intro)} Operatives must secure the designated sectors and neutralize any flagged threats. +
+