Sourcemined's port of SCP - Containment Breach's facility generator and SCP-294 lookup. Licence: CC BY-SA 3.0 (https://creativecommons.org/licenses/by-sa/3.0/), like the original source (https://github.com/Regalis11/scpcb) by Undertow Games, the SCP-CB Community Preservation Project and contributors. // ===== packages/scpcb/src/gen/blitz.ts ===== /*! SCP - Containment Breach facility generator for Sourcemined (sourcemined.com/scpcb/seed-map/). * A port of SCP - Containment Breach's source code (MapSystem.bb, Main.bb, Menu.bb) by Joonas Rikkonen * ("Regalis"), Undertow Games, the SCP-CB Community Preservation Project and contributors: * https://github.com/Regalis11/scpcb and https://store.steampowered.com/app/2178380/ * The game is based on the SCP Foundation wiki (https://scp-wiki.wikidot.com/). * Original and port licensed CC BY-SA 3.0: https://creativecommons.org/licenses/by-sa/3.0/ * This port (packages/scpcb/src/gen and src/scp294.ts) is available under the same licence. */ // Blitz3D runtime behaviour that SCP - Containment Breach's map generator depends on, written from // the game's own runtime (the Blitz3D TSS runtime inside the Steam build's exe, read statically): // // rnd(): state = 48271 * state mod (2^31 - 1) (a 0 result becomes 3399), // value = ((state & 0xFFFF) * 2 + 1) / 131072, an exact float // Rnd(a, b) = rnd() * (b - a) + a, stored as a 32-bit float // Rand(a, b)= trunc(float32(rnd() * float32(hi - lo + 1))) + lo, with lo/hi the smaller/larger bound // SeedRnd s: state = s & 0x7FFFFFFF, or 1 when that is 0 // // Blitz's one-argument forms are Rand(n) = Rand(1, n) and Rnd(x) = Rnd(x, 0) (= x - x * rnd()). // Float-to-int assignment rounds to nearest (ties to even), as the x87 FISTP the compiler emits. const M = 2147483647; const A = 48271; const f32 = Math.fround; export class BlitzRng { state = 1; calls = 0; seed(s: number): void { const v = (s | 0) & 0x7fffffff; this.state = v === 0 ? 1 : v; } /** The runtime's rnd(): one step of the Lehmer generator. */ next(): number { // The runtime computes state*A - floor(state/44488)*M in 32-bit arithmetic; the result is // the exact (state * A) mod M, which fits a double exactly. let s = (this.state * A) % M; if (s === 0) s = 3399; this.state = s; this.calls++; return ((s & 0xffff) * 2 + 1) / 131072; } /** Rand(a, b); Rand(n) is Rand(1, n). */ rand(a: number, b = 1): number { const lo = Math.min(a, b); const hi = Math.max(a, b); const r = this.next(); return Math.trunc(f32(r * f32(hi - lo + 1))) + lo; } /** Rnd(a, b); Rnd(x) is Rnd(x, 0). */ rnd(a: number, b = 0): number { const r = this.next(); return f32(r * (f32(b) - f32(a)) + f32(a)); } } /** Blitz float -> int conversion (assignment to an int variable): round half to even. */ export function toInt(v: number): number { const f = Math.floor(v); const d = v - f; if (d > 0.5) return f + 1; if (d < 0.5) return f; return f % 2 === 0 ? f : f + 1; } /** GenerateSeedNumber: how a text seed becomes the number given to SeedRnd. */ export function seedNumber(seed: string): number { let temp = 0; let shift = 0; for (let i = 0; i < seed.length; i++) { temp = (temp ^ (seed.charCodeAt(i) << shift)) | 0; shift = (shift + 1) % 24; } return temp; } export { f32 }; // ===== packages/scpcb/src/gen/types.ts ===== /*! SCP - Containment Breach facility generator for Sourcemined (sourcemined.com/scpcb/seed-map/). * A port of SCP - Containment Breach's source code (MapSystem.bb, Main.bb, Menu.bb) by Joonas Rikkonen * ("Regalis"), Undertow Games, the SCP-CB Community Preservation Project and contributors: * https://github.com/Regalis11/scpcb and https://store.steampowered.com/app/2178380/ * The game is based on the SCP Foundation wiki (https://scp-wiki.wikidot.com/). * Original and port licensed CC BY-SA 3.0: https://creativecommons.org/licenses/by-sa/3.0/ * This port (packages/scpcb/src/gen and src/scp294.ts) is available under the same licence. */ // Data the map generator needs, all read from the game's files by the extractor // (data/scpcb//gen.json), and the shape of what it produces. export const ROOM1 = 1, ROOM2 = 2, ROOM2C = 3, ROOM3 = 4, ROOM4 = 5; export interface GenTemplate { /** Creation id (order of first appearance in rooms.ini), as saves store it. */ id: number; name: string; shape: number; zone: [number, number, number]; commonness: number; large: number; setRoom: number; priority: number; disableDecals: boolean; lightCones: boolean; disableOverlap: boolean; /** Drawn-mesh bounds of the room file, in its own units; null when the room has no mesh file. */ bounds: { min: [number, number, number]; max: [number, number, number] } | null; /** Point lights the room file places (each costs two random numbers while the room has light slots left). */ lights: number; /** Waypoints the room file places (only counted for loading-screen draws). */ waypoints: number; } export interface GenData { build: string; map: { width: number; height: number; forceRoom1: number; forceRoom2c: number; forceRoom4: number }; templates: GenTemplate[]; /** Number of loading screens and the 1-based id of the CWM one (it draws random text while loading). */ loadingScreens: { count: number; cwm: number }; /** Red channel of the forest heightmaps by tile shape (ROOM1..ROOM4), row-major from the top. */ forest: Record; } export type Difficulty = "safe" | "euclid" | "keter" | "esoteric"; export interface GenOptions { difficulty?: Difficulty; /** Esoteric only: the "aggressive NPCs" setting. */ aggressiveNpcs?: boolean; intro?: boolean; /** The game adds a birthday cake on 15 April (local date), which changes the rest of the map. */ birthday?: boolean; /** "Numeric seeds" option: the seed is used as a number instead of hashed text. */ numeric?: boolean; } export interface GenItem { template: string; x: number; y: number; z: number; room: number; yaw: number } export interface GenRoom { index: number; template: string; shape: number; zone: number; x: number; y: number; z: number; angle: number; gx: number; gz: number; } export interface GenDoor { x: number; y: number; z: number; angle: number; room: number; open: boolean; big: number; keycard: number; code: string } export interface GenDecal { site?: string; id: number; x: number; y: number; z: number; size: number; room: number } export interface GenEvent { name: string; room: number } export interface GenResult { seed: string | number; seedNumber: number; accessCode: number; /** 0-based loading screen id the game picked (it decides whether loading draws random text). */ loadingScreen: number; rooms: GenRoom[]; grid: number[][]; doors: GenDoor[]; items: GenItem[]; decals: GenDecal[]; events: GenEvent[]; forest: number[] | null; /** Frames (70 per second) until SCP-106 first comes for the player. */ scp106Timer: number; rngCalls: number; } // ===== packages/scpcb/src/gen/generate.ts ===== /*! SCP - Containment Breach facility generator for Sourcemined (sourcemined.com/scpcb/seed-map/). * A port of SCP - Containment Breach's source code (MapSystem.bb, Main.bb, Menu.bb) by Joonas Rikkonen * ("Regalis"), Undertow Games, the SCP-CB Community Preservation Project and contributors: * https://github.com/Regalis11/scpcb and https://store.steampowered.com/app/2178380/ * The game is based on the SCP Foundation wiki (https://scp-wiki.wikidot.com/). * Original and port licensed CC BY-SA 3.0: https://creativecommons.org/licenses/by-sa/3.0/ * This port (packages/scpcb/src/gen and src/scp294.ts) is available under the same licence. */ // SCP - Containment Breach facility generator, ported from the game's source (MapSystem.bb // CreateMap and the functions it calls, Main.bb InitNewGame/InitEvents, Menu.bb DrawLoading) for the // Steam build. Every call that draws a random number is reproduced in the game's order, including // the ones whose result nothing here uses, so the sequence stays in step with the game. import { BlitzRng, f32, seedNumber, toInt } from "./blitz.ts"; import { fillRoom } from "./fillroom.ts"; import { ROOM1, ROOM2, ROOM2C, ROOM3, ROOM4, type GenData, type GenOptions, type GenResult, type GenTemplate } from "./types.ts"; export const ROOM_SCALE = 8 / 2048; export const HARPCODE = 7816; const ZONEAMOUNT = 3; const MAX_ROOM_LIGHTS = 32; /** How the game's compiled code orders the evaluation of a call's arguments, read from saves of the * real game (decal positions, sizes and textures): arguments that are larger expressions around a * random call go first, left to right; plain random calls follow, right to left. Call sites with * several random arguments carry a tag with their measured order (argument indices, first to last). */ export const ARG_ORDER: "ltr" | "rtl" = (globalThis as any).process?.env?.SCPCB_ARGS === "ltr" ? "ltr" : "rtl"; const SITE_ORDER: Record = { LR2: "13240", // lockroom2: CreateDecal(Rand, x+Rnd, y+Rnd, z+Rnd, 90, Rnd(360), 0) ING1: "13240", // InitNewGame: CreateDecal(Rand(2,3), x+Rnd, 0.003, z+Rnd, 90, Rand(360), 0) ING2: "012", // InitNewGame: CreateDecal(0, x+Rnd, 0.003, z+Rnd, 90, Rand(360), 0) I173: "13420", // intro room: CreateDecal(Rand(4,6), x+Rnd, Rnd, z+Rnd, 90, Rnd(360), 0) (by the rule above) }; /** Whether Blitz's And evaluates every operand (no short-circuit). */ export const AND_EVALUATES_ALL = (globalThis as any).process?.env?.SCPCB_AND !== "short"; export interface Room { index: number; t: GenTemplate; zone: number; x: number; y: number; z: number; angle: number; lights: number; minX: number; minY: number; minZ: number; maxX: number; maxY: number; maxZ: number; /** Position the room had while it was filled (children keep their offset from it). */ fx: number; fy: number; fz: number; forest: number[] | null; } export interface Door { x: number; y: number; z: number; angle: number; room: Room | null; open: boolean; big: number; keycard: number; code: string; noWaypoint: boolean; autoClose: boolean; world?: boolean } export interface Item { name: string; x: number; y: number; z: number; room: Room | null; yaw: number } export interface Decal { id: number; x: number; y: number; z: number; size: number; room: Room | null; site?: string } export class Gen { rng = new BlitzRng(); data: GenData; opts: Required; aggressive: boolean; W: number; H: number; transition: [number, number] = [0, 0]; mapTemp: number[][] = []; mapName: string[][] = []; mapRoom: string[][] = []; mapRoomId: number[] = []; roomAmounts: number[][] = []; minPos: number[][] = []; maxPos: number[][] = []; templates: GenTemplate[]; rooms: Room[] = []; doors: Door[] = []; items: Item[] = []; decals: Decal[] = []; events: { name: string; room: Room }[] = []; waypoints = 0; accessCode = 0; loadingScreen = 0; cwm = false; room2gwBroken = false; room2gwX = 0; room2gwZ = 0; scp106Timer = 0; constructor(data: GenData, opts: GenOptions = {}) { this.data = data; this.opts = { difficulty: "safe", aggressiveNpcs: false, intro: false, birthday: false, numeric: false, ...opts }; const d = this.opts.difficulty; this.aggressive = d === "keter" || (d === "esoteric" && this.opts.aggressiveNpcs); this.W = data.map.width; this.H = data.map.height; this.templates = sortTemplates(data.templates); } /** Evaluate a call's arguments in the game's order and return them in parameter order. */ args(...fns: { [K in keyof T]: () => T[K] }): T { return this.argsAt("", ...fns); } argsAt(tag: string, ...fns: { [K in keyof T]: () => T[K] }): T { const out = new Array(fns.length); const perm = ((globalThis as any).process?.env?.["SCPCB_PERM_" + tag] as string | undefined) ?? SITE_ORDER[tag]; if (perm && perm.length === fns.length) { for (const ch of perm) { const i = Number(ch); out[i] = fns[i](); } return out as T; } if (ARG_ORDER === "ltr") for (let i = 0; i < fns.length; i++) out[i] = fns[i](); else for (let i = fns.length - 1; i >= 0; i--) out[i] = fns[i](); return out as T; } // --- the game's helpers ------------------------------------------------------------------- mt(x: number, y: number): number { if (x < 0 || y < 0 || x > this.W + 1 || y > this.H + 1) throw new Error(`MapTemp(${x}, ${y}) out of bounds`); return this.mapTemp[x][y]; } mtSafe(x: number, y: number): number { if (x < 0 || y < 0 || x > this.W || y > this.H) return 0; return this.mapTemp[x][y]; } getZone(y: number): number { return Math.min(Math.floor(f32(f32(f32(this.H - y) / this.H) * ZONEAMOUNT)), ZONEAMOUNT - 1); } createDoor(room: Room | null, x: number, y: number, z: number, angle: number, open: number | boolean = false, big: number | boolean = 0, keycard: number | boolean = 0, code = ""): Door { const o = typeof open === "boolean" ? (open ? 1 : 0) : open; const b = typeof big === "boolean" ? (big ? 1 : 0) : big; const d: Door = { x, y, z, angle, room, open: o !== 0, big: b, keycard: typeof keycard === "boolean" ? (keycard ? 1 : 0) : keycard, code, noWaypoint: false, autoClose: false }; // If d\open And big = False And Rand(8) = 1 Then d\AutoClose = True if (AND_EVALUATES_ALL) { const r8 = this.rng.rand(8); if (o !== 0 && b === 0 && r8 === 1) d.autoClose = true; } else if (o !== 0 && b === 0) { if (this.rng.rand(8) === 1) d.autoClose = true; } this.doors.push(d); return d; } createItem(name: string, x: number, y: number, z: number, room: Room | null): Item { const yaw = this.rng.rand(360); const it: Item = { name, x, y, z, room, yaw }; this.items.push(it); return it; } decal(id: number, x: number, y: number, z: number, room: Room | null, size = 1): Decal { const d: Decal = { id, x, y, z, size, room }; this.decals.push(d); return d; } addLight(r: Room): void { if (r.lights >= MAX_ROOM_LIGHTS) return; this.rng.rand(360); this.rng.rand(1, 10); r.lights++; } /** DrawLoading(percent): only the CWM loading screen draws random numbers. */ drawLoading(percent: number): void { const rng = this.rng; if (percent === 0) { this.loadingScreen = rng.rand(1, this.data.loadingScreens.count); this.cwm = this.loadingScreen === this.data.loadingScreens.cwm; if ((globalThis as any).process?.env?.SCPCB_NOCWM) this.cwm = false; } if (!this.cwm) return; const n = rng.rand(2, 9); for (let i = 0; i <= n; i++) rng.rand(48, 122); // RandomDefaultWidthChar if (percent === 0) { if (rng.rand(5) === 1) rng.rand(2); else rng.rand(13); } rng.rand(5); // the To expression is evaluated on every pass of the loop for (let i = 0; i <= rng.rand(10, 15); i++) { // Replace(..., Mid(txt, Rand(1, Len(strtemp) - 1), 1), RandomDefaultWidthChar(130, 250, "?")) this.args(() => rng.rand(1, 2), () => rng.rand(130, 250)); } } // --- CreateMap ------------------------------------------------------------------------------ createMapLayout(): void { const W = this.W, H = this.H, rng = this.rng; let x = Math.floor(W / 2); let y = H - 2; for (let i = y; i <= H - 1; i++) this.mapTemp[x][i] = 1; let temp = 0; let tempheight = 0; do { let width = rng.rand(Math.floor(f32(W * f32(0.6))), Math.floor(f32(W * f32(0.85)))); if (x > f32(W * f32(0.6))) width = -width; else if (x > f32(W * f32(0.4))) x = x - Math.trunc(width / 2); if (x + width > W - 3) width = W - 3 - x; else if (x + width < 2) width = -x + 2; x = Math.min(x, x + width); width = Math.abs(width); for (let i = x; i <= x + width; i++) this.mapTemp[Math.min(i, W)][y] = 1; let height = rng.rand(3, 4); if (y - height < 1) height = y - 1; const yhallways = rng.rand(4, 5); if (this.getZone(y - height) !== this.getZone(y - height + 1)) height = height - 1; for (let i = 1; i <= yhallways; i++) { let x2 = toInt(Math.max(Math.min(rng.rand(x, x + width - 1), W - 2), 2)); while (this.mt(x2, y - 1) || this.mt(x2 - 1, y - 1) || this.mt(x2 + 1, y - 1)) x2 = x2 + 1; if (x2 < x + width) { if (i === 1) { tempheight = height; if (rng.rand(2) === 1) x2 = x; else x2 = x + width; } else { tempheight = rng.rand(1, height); } for (let y2 = y - tempheight; y2 <= y; y2++) { if (this.getZone(y2) !== this.getZone(y2 + 1)) this.mapTemp[x2][y2] = 255; else this.mapTemp[x2][y2] = 1; } if (tempheight === height) temp = x2; } } x = temp; y = y - height; } while (!(y < 2)); } countRooms(): void { const W = this.W, H = this.H; this.roomAmounts = Array.from({ length: ROOM4 + 2 }, () => new Array(ZONEAMOUNT + 1).fill(0)); const ra = this.roomAmounts; const m1 = (v: number) => Math.min(v, 1); for (let y = 1; y <= H - 1; y++) { const zone = this.getZone(y); for (let x = 1; x <= W - 1; x++) { if (this.mt(x, y) > 0) { let temp = m1(this.mt(x + 1, y)) + m1(this.mt(x - 1, y)); temp = temp + m1(this.mt(x, y + 1)) + m1(this.mt(x, y - 1)); if (this.mt(x, y) < 255) this.mapTemp[x][y] = temp; switch (this.mt(x, y)) { case 1: ra[ROOM1][zone]++; break; case 2: if (m1(this.mt(x + 1, y)) + m1(this.mt(x - 1, y)) === 2) ra[ROOM2][zone]++; else if (m1(this.mt(x, y + 1)) + m1(this.mt(x, y - 1)) === 2) ra[ROOM2][zone]++; else ra[ROOM2C][zone]++; break; case 3: ra[ROOM3][zone]++; break; case 4: ra[ROOM4][zone]++; break; } } } } } forceRooms(): void { const W = this.W, H = this.H, ra = this.roomAmounts, mt = this.mapTemp; const m1 = (v: number) => Math.min(v, 1); const forceRoom1 = this.data.map.forceRoom1; let x2 = 0, y2 = 0; for (let i = 0; i <= 2; i++) { let temp = -ra[ROOM1][i] + forceRoom1; if (temp > 0) { const yMin = i === 2 ? 1 : this.transition[i]; const yMax = i === 0 ? H - 2 : this.transition[i - 1] - 1; const xMin = 1, xMax = W - 2; outer: for (let y = yMin; y <= yMax; y++) { for (let x = xMin; x <= xMax; x++) { if (this.mt(x, y) === 0) { if (m1(this.mt(x + 1, y)) + m1(this.mt(x - 1, y)) + m1(this.mt(x, y + 1)) + m1(this.mt(x, y - 1)) === 1) { if (this.mt(x + 1, y)) { x2 = x + 1; y2 = y; } else if (this.mt(x - 1, y)) { x2 = x - 1; y2 = y; } else if (this.mt(x, y + 1)) { x2 = x; y2 = y + 1; } else if (this.mt(x, y - 1)) { x2 = x; y2 = y - 1; } let placed = false; if (this.mt(x2, y2) > 1 && this.mt(x2, y2) < 4 && (y < yMax || y2 < y || i === 0)) { switch (this.mt(x2, y2)) { case 2: if (m1(this.mt(x2 + 1, y2)) + m1(this.mt(x2 - 1, y2)) === 2) { ra[ROOM2][i]--; ra[ROOM3][i]++; placed = true; } else if (m1(this.mt(x2, y2 + 1)) + m1(this.mt(x2, y2 - 1)) === 2) { ra[ROOM2][i]--; ra[ROOM3][i]++; placed = true; } break; case 3: ra[ROOM3][i]--; ra[ROOM4][i]++; placed = true; break; } if (placed) { mt[x2][y2] = mt[x2][y2] + 1; mt[x][y] = 1; ra[ROOM1][i]++; temp = temp - 1; } } } } if (temp === 0) break outer; } } } } const forceRoom4 = this.data.map.forceRoom4; const forceRoom2c = this.data.map.forceRoom2c; for (let i = 0; i <= 2; i++) { const yMin = i === 2 ? 1 : this.transition[i]; const yMax = i === 0 ? H - 2 : this.transition[i - 1] - 2; const xMin = 1, xMax = W - 2; let temp = -ra[ROOM4][i] + forceRoom4; if (temp > 0) { temp = 0; outer4: for (let y = yMin; y <= yMax; y++) { for (let x = xMin; x <= xMax; x++) { if (this.mt(x, y) === 3) { let placed = false; // Select 1: the first case whose condition is true if (x !== xMax && this.mt(x + 1, y) + this.mt(x + 1, y + 1) + this.mt(x + 1, y - 1) + this.mt(x + 2, y) === 0) { mt[x + 1][y] = 1; placed = true; } else if (x !== xMin && this.mt(x - 1, y) + this.mt(x - 1, y + 1) + this.mt(x - 1, y - 1) + this.mt(x - 2, y) === 0) { mt[x - 1][y] = 1; placed = true; } else if ((i !== 0 || y !== yMax) && this.mt(x, y + 1) + this.mt(x + 1, y + 1) + this.mt(x - 1, y + 1) + this.mt(x, y + 2) === 0) { mt[x][y + 1] = 1; placed = true; } else if ((i === 2 || y !== yMin) && this.mt(x, y - 1) + this.mt(x + 1, y - 1) + this.mt(x - 1, y - 1) + this.mt(x, y - 2) === 0) { mt[x][y - 1] = 1; placed = true; } if (placed) { mt[x][y] = 4; ra[ROOM4][i]++; ra[ROOM3][i]--; ra[ROOM1][i]++; temp = temp - 1; } } if (temp === 0) break outer4; } } } temp = -ra[ROOM2C][i] + forceRoom2c; if (temp > 0) { outer2c: for (let y = yMax; y >= yMin; y--) { for (let x = xMin; x <= xMax; x++) { if (this.mt(x, y) === 1) { let placed = false; const mts = (a: number, b: number) => this.mtSafe(a, b); const m = (a: number, b: number) => this.mt(a, b); // Select True: the first case whose condition is true; "Lor" short-circuits if (m(x - 1, y) > 0) { if (m(x + 1, y - 1) + m(x + 1, y + 1) + m(x + 2, y) === 0 && x < xMax) { if (mts(x + 1, y - 2) + m(x + 2, y - 1) === 0 && (y > yMin || i === 2)) { mt[x][y] = 2; mt[x + 1][y] = 2; mt[x + 1][y - 1] = 1; placed = true; } else if (m(x + 1, y + 2) + m(x + 2, y + 1) === 0 && (y < yMax || i > 0)) { mt[x][y] = 2; mt[x + 1][y] = 2; mt[x + 1][y + 1] = 1; placed = true; } } } else if (m(x + 1, y) > 0) { if ((x - 2 < 0 || m(x - 2, y) === 0) && m(x - 1, y - 1) + m(x - 1, y + 1) === 0 && x > xMin) { if ((x - 2 < 0 || m(x - 2, y - 1) === 0) && mts(x - 1, y - 2) === 0 && (y > yMin || i === 2)) { mt[x][y] = 2; mt[x - 1][y] = 2; mt[x - 1][y - 1] = 1; placed = true; } else if ((x - 2 < 0 || m(x - 2, y + 1) === 0) && m(x - 1, y + 2) === 0 && (y < yMax || i > 0)) { mt[x][y] = 2; mt[x - 1][y] = 2; mt[x - 1][y + 1] = 1; placed = true; } } } else if (m(x, y - 1) > 0) { if (m(x - 1, y + 1) + m(x + 1, y + 1) + m(x, y + 2) === 0 && (y < yMax || i > 0)) { if ((x - 2 < 0 || m(x - 2, y + 1) === 0) && m(x - 1, y + 2) === 0 && x > xMin) { mt[x][y] = 2; mt[x][y + 1] = 2; mt[x - 1][y + 1] = 1; placed = true; } else if (m(x + 2, y + 1) + m(x + 1, y + 2) === 0 && x < xMax) { mt[x][y] = 2; mt[x][y + 1] = 2; mt[x + 1][y + 1] = 1; placed = true; } } } else if (m(x, y + 1) > 0) { if (m(x - 1, y - 1) + m(x + 1, y - 1) + mts(x, y - 2) === 0 && (y > yMin || i === 2)) { if ((x - 2 < 0 || m(x - 2, y - 1) === 0) && mts(x - 1, y - 2) === 0 && x > xMin) { mt[x][y] = 2; mt[x][y - 1] = 2; mt[x - 1][y - 1] = 1; placed = true; } else if (m(x + 2, y - 1) + mts(x + 1, y - 2) === 0 && x < xMax) { mt[x][y] = 2; mt[x][y - 1] = 2; mt[x + 1][y - 1] = 1; placed = true; } } } if (placed) { ra[ROOM2C][i]++; ra[ROOM2][i]++; temp = temp - 1; } } if (temp === 0) break outer2c; } } } } } initializeRoomRanges(): void { const ra = this.roomAmounts; let maxRooms = Math.trunc((55 * this.W) / 20); for (let rs = ROOM1; rs <= ROOM4; rs++) maxRooms = Math.max(maxRooms, ra[rs][0] + ra[rs][1] + ra[rs][2] + 1); this.mapRoom = Array.from({ length: ROOM4 + 2 }, () => new Array(maxRooms + 1).fill("")); this.minPos = Array.from({ length: ROOM4 + 2 }, () => new Array(ZONEAMOUNT + 1).fill(0)); this.maxPos = Array.from({ length: ROOM4 + 2 }, () => new Array(ZONEAMOUNT + 1).fill(0)); for (let rs = ROOM1; rs <= ROOM4; rs++) { for (let z = 0; z < ZONEAMOUNT; z++) { this.minPos[rs][z] = 0; this.maxPos[rs][z] = ra[rs][0]; if (z > 0) { this.minPos[rs][z] += ra[rs][0]; this.maxPos[rs][z] += ra[rs][1]; } if (z > 1) { this.minPos[rs][z] += ra[rs][1]; this.maxPos[rs][z] += ra[rs][2]; } } } } setRoom(name: string, type: number, pos: number, minPos: number, maxPos: number): boolean { if (maxPos <= minPos) return false; const upper = (p: number): boolean => { if (p >= maxPos) return false; while (this.mapRoom[type][p] !== "") { p++; if (p >= maxPos) return false; } this.mapRoom[type][p] = name; return true; }; const lower = (p: number): boolean => { if (p < minPos) return false; while (this.mapRoom[type][p] !== "") { p--; if (p < minPos) return false; } this.mapRoom[type][p] = name; return true; }; // Lor short-circuits: the second placement is only tried when the first fails if (pos >= Math.trunc((minPos + maxPos) / 2)) return upper(pos) || lower(pos - 1); return lower(pos) || upper(pos + 1); } setRooms(): void { this.mapRoom[ROOM1][0] = "start"; this.mapRoom[ROOM1][this.maxPos[ROOM1][2] - 2] = "exit1"; this.mapRoom[ROOM1][this.maxPos[ROOM1][2] - 1] = "gateaentrance"; for (const rt of this.templates) { if (rt.setRoom >= 0) { for (let i = 0; i < ZONEAMOUNT; i++) { let zone = rt.zone[i]; if (zone !== 0) { zone = zone - 1; const start = this.minPos[rt.shape][zone]; const pos = start + Math.floor(f32(f32(rt.setRoom) * this.roomAmounts[rt.shape][zone])); this.setRoom(rt.name, rt.shape, pos, start, this.maxPos[rt.shape][zone]); } } } } } template(name: string): GenTemplate { const t = this.templates.find((t) => t.name === name); if (!t) throw new Error("Room " + name + " not found"); return t; } createRoom(zone: number, shape: number, x: number, y: number, z: number, angle: number, name: string): Room { let t: GenTemplate | undefined; if (name !== "") { t = this.template(name.toLowerCase()); } else { let temp = 0; for (const rt of this.templates) { for (let i = 1; i <= ZONEAMOUNT; i++) if (rt.zone[i - 1] === zone && rt.shape === shape) { temp += rt.commonness; break; } } const pick = this.rng.rand(temp); temp = 0; outer: for (const rt of this.templates) { for (let i = 1; i <= ZONEAMOUNT; i++) { if (rt.zone[i - 1] === zone && rt.shape === shape) { temp += rt.commonness; if (pick > temp - rt.commonness && pick <= temp) { t = rt; break outer; } } } } if (!t) throw new Error(`no room template for zone ${zone} shape ${shape}`); } const r: Room = { index: this.rooms.length, t, zone, x, y, z, angle: 0, lights: 0, minX: 0, minY: 0, minZ: 0, maxX: 0, maxY: 0, maxZ: 0, fx: x, fy: y, fz: z, forest: null }; this.rooms.push(r); fillRoom(this, r); r.angle = angle; this.calculateRoomExtents(r); return r; } createRooms(): void { const W = this.W, H = this.H; for (let y = H - 1; y >= 0; y--) { this.drawLoading(toInt(50 + f32(f32(H - 1 - y) / (H - 1)) * 19)); let zone: number; if (y < Math.trunc(H / 3) + 1) zone = 3; else if (y < f32(H * f32(2 / 3))) zone = 2; else zone = 1; for (let x = 1; x <= W - 2; x++) { const v = this.mt(x, y); if (v === 255) { if (y > Math.trunc(H / 2)) this.createRoom(zone, ROOM2, x * 8, 0, y * 8, 0, "checkpoint1"); else this.createRoom(zone, ROOM2, x * 8, 0, y * 8, 0, "checkpoint2"); } else if (v > 0) { let angle = 0; const m1 = (a: number) => Math.min(a, 1); const temp = m1(this.mt(x + 1, y)) + m1(this.mt(x - 1, y)) + m1(this.mt(x, y + 1)) + m1(this.mtSafe(x, y - 1)); const pick = (shape: number) => { if (this.mapName[x][y] === "" && this.mapRoom[shape][this.mapRoomId[shape]] !== "") this.mapName[x][y] = this.mapRoom[shape][this.mapRoomId[shape]]; }; switch (temp) { case 1: pick(ROOM1); if (this.mt(x, y + 1)) angle = 180; else if (this.mt(x - 1, y)) angle = 270; else if (this.mt(x + 1, y)) angle = 90; else angle = 0; this.createRoom(zone, ROOM1, x * 8, 0, y * 8, angle, this.mapName[x][y]); this.mapRoomId[ROOM1]++; break; case 2: if (this.mt(x - 1, y) > 0 && this.mt(x + 1, y) > 0) { pick(ROOM2); angle = this.rng.rand(2) === 1 ? 90 : 270; this.createRoom(zone, ROOM2, x * 8, 0, y * 8, angle, this.mapName[x][y]); this.mapRoomId[ROOM2]++; } else if (this.mtSafe(x, y - 1) > 0 && this.mt(x, y + 1) > 0) { pick(ROOM2); angle = this.rng.rand(2) === 1 ? 180 : 0; this.createRoom(zone, ROOM2, x * 8, 0, y * 8, angle, this.mapName[x][y]); this.mapRoomId[ROOM2]++; } else { pick(ROOM2C); if (this.mt(x - 1, y) > 0 && this.mt(x, y + 1) > 0) angle = 180; else if (this.mt(x + 1, y) > 0 && this.mt(x, y + 1) > 0) angle = 90; else if (this.mt(x - 1, y) > 0 && this.mtSafe(x, y - 1) > 0) angle = 270; else angle = 0; this.createRoom(zone, ROOM2C, x * 8, 0, y * 8, angle, this.mapName[x][y]); this.mapRoomId[ROOM2C]++; } break; case 3: pick(ROOM3); if (!this.mtSafe(x, y - 1)) angle = 180; else if (!this.mt(x - 1, y)) angle = 90; else if (!this.mt(x + 1, y)) angle = 270; else angle = 0; this.createRoom(zone, ROOM3, x * 8, 0, y * 8, angle, this.mapName[x][y]); this.mapRoomId[ROOM3]++; break; case 4: pick(ROOM4); this.createRoom(zone, ROOM4, x * 8, 0, y * 8, 0, this.mapName[x][y]); this.mapRoomId[ROOM4]++; break; } } } } this.createRoom(0, ROOM1, (W - 1) * 8, 500, 8, 0, "gatea"); this.mapRoomId[ROOM1]++; this.createRoom(0, ROOM1, (W - 1) * 8, 0, (H - 1) * 8, 0, "pocketdimension"); this.mapRoomId[ROOM1]++; if (this.opts.intro) { this.createRoom(0, ROOM1, 8, 0, (H - 1) * 8, 0, "173"); this.mapRoomId[ROOM1]++; } this.createRoom(0, ROOM1, 8, 800, 0, 0, "dimension1499"); this.mapRoomId[ROOM1]++; } // --- overlap resolver (OverlapResolver.bb) -------------------------------------------------- calculateRoomExtents(r: Room): void { if (r.t.disableOverlap || !r.t.bounds) return; const b = r.t.bounds; const [ax, ay, az] = tformVector(b.min[0], b.min[1], b.min[2], r.angle); const [bx, by, bz] = tformVector(b.max[0], b.max[1], b.max[2], r.angle); r.minX = f32(ax + r.x); r.minY = ay; r.minZ = f32(az + r.z); r.maxX = f32(bx + r.x); r.maxY = by; r.maxZ = f32(bz + r.z); if (r.minX > r.maxX) { const t = r.maxX; r.maxX = r.minX; r.minX = t; } if (r.minZ > r.maxZ) { const t = r.maxZ; r.maxZ = r.minZ; r.minZ = t; } const s = f32(0.05); r.minX = f32(r.minX + s); r.maxX = f32(r.maxX - s); r.minY = f32(r.minY + s); r.maxY = f32(r.maxY - s); r.minZ = f32(r.minZ + s); r.maxZ = f32(r.maxZ - s); } overlapArea(a: Room, b: Room): number { if (a.maxX <= b.minX || a.maxY <= b.minY || a.maxZ <= b.minZ) return 0; if (a.minX >= b.maxX || a.minY >= b.maxY || a.minZ >= b.maxZ) return 0; return f32(f32(Math.min(a.maxX, b.maxX) - Math.max(a.minX, b.minX)) * f32(Math.min(a.maxZ, b.maxZ) - Math.max(a.minZ, b.minZ))); } allOverlaps(r: Room): number { let area = 0; for (const r2 of this.rooms) if (!r2.t.disableOverlap && r2 !== r) area = f32(area + this.overlapArea(r, r2)); return area; } skipCandidate(r: Room): boolean { return r.t.disableOverlap || r.t.name === "checkpoint1" || r.t.name === "checkpoint2" || r.t.name === "start"; } preventRoomOverlap(r: Room): void { if (r.t.disableOverlap) return; if (this.skipCandidate(r)) return; let before1 = this.allOverlaps(r); if (before1 === 0) return; if (r.t.shape === ROOM2) { r.angle += 180; this.calculateRoomExtents(r); const after1 = this.allOverlaps(r); if (after1 === 0) return; if (before1 > after1) before1 = after1; else { r.angle -= 180; this.calculateRoomExtents(r); } } for (const r2 of this.rooms) { if (r.t.shape === r2.t.shape && r.zone === r2.zone && r !== r2 && !this.skipCandidate(r2)) { const before2 = this.allOverlaps(r2); const candBefore = this.overlapArea(r, r2); const o1 = { x: r.x, z: r.z, a: r.angle }; const o2 = { x: r2.x, z: r2.z, a: r2.angle }; r.x = o2.x; r.z = o2.z; r.angle = o2.a; this.calculateRoomExtents(r); r2.x = o1.x; r2.z = o1.z; r2.angle = o1.a; this.calculateRoomExtents(r2); let after1 = 0, after2 = 0; const candAfter = this.overlapArea(r, r2); for (const r3 of this.rooms) { if (!r3.t.disableOverlap) { if (r3 !== r) after1 = f32(after1 + this.overlapArea(r, r3)); if (r3 !== r2) after2 = f32(after2 + this.overlapArea(r2, r3)); } } if (after1 === 0 && after2 === 0) return; if (f32(f32(after1 + after2) - candAfter) < f32(f32(before1 + before2) - candBefore)) { before1 = after1; } else { r.x = o1.x; r.z = o1.z; r.angle = o1.a; this.calculateRoomExtents(r); r2.x = o2.x; r2.z = o2.z; r2.angle = o2.a; this.calculateRoomExtents(r2); } } } } preventRoomOverlaps(): void { for (const r of this.rooms) this.preventRoomOverlap(r); let total = 0; for (const r of this.rooms) total = f32(total + this.allOverlaps(r)); if (total > 16) for (const r of this.rooms) this.preventRoomOverlap(r); } // --- doors between rooms ---------------------------------------------------------------------- createDoors(): void { const W = this.W, H = this.H, spacing = 8; for (let y = H; y >= 0; y--) { let zone: number; if (y < this.transition[1] - 1) zone = 3; else if (y >= this.transition[1] - 1 && y < this.transition[0] - 1) zone = 2; else zone = 1; for (let x = W; x >= 0; x--) { if (this.mt(x, y) > 0) { const temp = zone === 2 ? 2 : 0; for (const r of this.rooms) { r.angle = wrapAngle(r.angle); if (Math.trunc(f32(r.x / 8)) === x && Math.trunc(f32(r.z / 8)) === y) { let spawn = false; switch (r.t.shape) { case ROOM1: spawn = r.angle === 90; break; case ROOM2: spawn = r.angle === 90 || r.angle === 270; break; case ROOM2C: spawn = r.angle === 0 || r.angle === 90; break; case ROOM3: spawn = r.angle === 0 || r.angle === 180 || r.angle === 90; break; default: spawn = true; } if (spawn && x + 1 < W + 1 && this.mt(x + 1, y) > 0) { const open = Math.max(this.rng.rand(-3, 1), 0); this.createDoor(r, x * spacing + spacing / 2, 0, y * spacing, 90, open, temp).world = true; } spawn = false; switch (r.t.shape) { case ROOM1: spawn = r.angle === 180; break; case ROOM2: spawn = r.angle === 0 || r.angle === 180; break; case ROOM2C: spawn = r.angle === 180 || r.angle === 90; break; case ROOM3: spawn = r.angle === 180 || r.angle === 90 || r.angle === 270; break; default: spawn = true; } if (spawn && y + 1 < H + 1 && this.mt(x, y + 1) > 0) { const open = Math.max(this.rng.rand(-3, 1), 0); this.createDoor(r, x * spacing, 0, y * spacing + spacing / 2, 0, open, temp).world = true; } break; } } } } } } createMap(): void { const W = this.W, H = this.H; this.transition = [Math.floor(f32(H * f32(2 / 3))) + 1, Math.floor(f32(H * f32(1 / 3))) + 1]; this.rng.seed(this.seedValue); this.mapTemp = Array.from({ length: W + 2 }, () => new Array(H + 2).fill(0)); this.mapName = Array.from({ length: W + 1 }, () => new Array(H + 1).fill("")); this.mapRoomId = new Array(ROOM4 + 2).fill(0); this.createMapLayout(); this.countRooms(); this.forceRooms(); this.initializeRoomRanges(); this.setRooms(); this.createRooms(); this.preventRoomOverlaps(); for (let y = 0; y <= H; y++) for (let x = 0; x <= W; x++) this.mapTemp[x][y] = Math.min(this.mapTemp[x][y], 1); this.createDoors(); } seedValue = 0; // --- InitEvents (Main.bb) ------------------------------------------------------------------- createEvent(name: string, roomName: string, id: number, prob = 0): void { let i = 0; if (prob === 0) { for (const r of this.rooms) { if (roomName === "" || roomName === r.t.name) { const taken = this.events.some((e) => e.room === r); i++; if (i >= id && !taken) { this.events.push({ name, room: r }); return; } } } } else { for (const r of this.rooms) { if (roomName === "" || roomName === r.t.name) { const taken = this.events.some((e) => e.room === r); if (this.rng.rnd(0, 1) < f32(prob) && !taken) this.events.push({ name, room: r }); } } } } initEvents(): void { const rng = this.rng, ag = this.aggressive ? 1 : 0; const p = (a: number, b: number) => f32(f32(a) + f32(f32(b) * ag)); const ce = (n: string, r: string, id: number, prob = 0) => this.createEvent(n, r, id, prob); ce("173", "173", 0); ce("alarm", "start", 0); ce("pocketdimension", "pocketdimension", 0); ce("tunnel106", "tunnel", 0, p(0.07, 0.1)); if (rng.rand(3) < 3) ce("lockroom173", "lockroom", 0); ce("lockroom173", "lockroom", 0, p(0.3, 0.5)); ce("lockroom173", "lockroom_ez", 0, p(0.3, 0.5)); ce("room2trick", "room2", 0, 0.15); ce("1048a", "room2", 0, 1.0); ce("room2storage", "room2storage", 0); ce("lockroom096", "lockroom2", 0); ce("endroom106", "endroom", rng.rand(0, 1)); ce("room2poffices2", "room2poffices2", 0); ce("room2fan", "room2_2", 0, 1.0); ce("room2elevator2", "room2elevator", 0); ce("room2elevator", "room2elevator", rng.rand(1, 2)); ce("room3storage", "room3storage", 0, 0); ce("tunnel2smoke", "tunnel2", 0, 0.2); ce("tunnel2", "tunnel2", rng.rand(0, 2), 0); ce("tunnel2", "tunnel2", 0, f32(0.2 * ag)); ce("room2doors173", "room2doors", 0, p(0.5, 0.4)); ce("room2offices2", "room2offices2", 0, 0.7); ce("room2closets", "room2closets", 0); ce("room2cafeteria", "room2cafeteria", 0); ce("room3pitduck", "room3pit", 0); ce("room3pit1048", "room3pit", 1); ce("room2offices3", "room2offices3", 0, 1.0); ce("room2servers", "room2servers", 0); ce("room3servers", "room3servers", 0); ce("room3servers", "room3servers2", 0); ce("room3tunnel", "room3tunnel", 0, 0.08); ce("room4", "room4", 0); if (rng.rand(5) < 5) { switch (rng.rand(3)) { case 1: ce("682roar", "tunnel", rng.rand(0, 2), 0); break; case 2: ce("682roar", "room3pit", rng.rand(0, 2), 0); break; case 3: ce("682roar", "room2z3", 0, 0); break; } } ce("testroom173", "room2testroom2", 0, 1.0); ce("room2tesla", "room2tesla", 0, 0.9); ce("room2nuke", "room2nuke", 0, 0); if (rng.rand(5) < 5) ce("coffin106", "coffin", 0, 0); else ce("coffin", "coffin", 0, 0); ce("checkpoint", "checkpoint1", 0, 1.0); ce("checkpoint", "checkpoint2", 0, 1.0); ce("room3door", "room3", 0, 0.1); ce("room3door", "room3tunnel", 0, 0.1); if (rng.rand(2) === 1) { ce("106victim", "room3", rng.rand(1, 2)); ce("106sinkhole", "room3_2", rng.rand(2, 3)); } else { ce("106victim", "room3_2", rng.rand(1, 2)); ce("106sinkhole", "room3", rng.rand(2, 3)); } ce("106sinkhole", "room4", rng.rand(1, 2)); ce("room079", "room079", 0, 0); ce("room049", "room049", 0, 0); ce("room012", "room012", 0, 0); ce("room035", "room035", 0, 0); ce("008", "008", 0, 0); ce("room106", "room106", 0, 0); ce("pj", "roompj", 0, 0); ce("914", "914", 0, 0); ce("buttghost", "room2toilets", 0, 0); ce("toiletguard", "room2toilets", 1, 0); ce("room2pipes106", "room2pipes", rng.rand(0, 3)); ce("room2pit", "room2pit", 0, p(0.4, 0.4)); ce("testroom", "testroom", 0); ce("room2tunnel", "room2tunnel", 0); ce("room2ccont", "room2ccont", 0); ce("gateaentrance", "gateaentrance", 0); ce("gatea", "gatea", 0); ce("exit1", "exit1", 0); ce("room205", "room205", 0); ce("room860", "room860", 0); ce("room966", "room966", 0); ce("room1123", "room1123", 0, 0); ce("room2tesla", "room2tesla_lcz", 0, 0.9); ce("room2tesla", "room2tesla_hcz", 0, 0.9); ce("room4tunnels", "room4tunnels", 0); ce("room_gw", "room2gw", 0, 1.0); ce("dimension1499", "dimension1499", 0); ce("room1162", "room1162", 0); ce("room2scps2", "room2scps2", 0); ce("room_gw", "room3gw", 0, 1.0); ce("room2sl", "room2sl", 0); ce("medibay", "medibay", 0); ce("room2shaft", "room2shaft", 0); ce("room1lifts", "room1lifts", 0); ce("room2gw_b", "room2gw_b", rng.rand(0, 1)); ce("096spawn", "room4pit", 0, p(0.6, 0.2)); ce("096spawn", "room3pit", 0, p(0.6, 0.2)); ce("096spawn", "room2pipes", 0, p(0.4, 0.2)); ce("096spawn", "room2pit", 0, p(0.5, 0.2)); ce("096spawn", "room3tunnel", 0, p(0.6, 0.2)); ce("096spawn", "room4tunnels", 0, p(0.7, 0.2)); ce("096spawn", "tunnel", 0, p(0.6, 0.2)); ce("096spawn", "tunnel2", 0, p(0.4, 0.2)); ce("096spawn", "room3z2", 0, p(0.7, 0.2)); ce("room2pit", "room2_4", 0, p(0.4, 0.4)); ce("room2offices035", "room2offices", 0); ce("room2pit106", "room2pit", 0, p(0.07, 0.1)); ce("room1archive", "room1archive", 0, 1.0); } // --- New game (Menu.bb + Main.bb InitNewGame) ------------------------------------------------- run(seed: string | number): GenResult { this.seedValue = this.opts.numeric ? (Number(seed) | 0) : seedNumber(String(seed)); const rng = this.rng; // Menu: SeedRnd, then LoadEntities draws the first loading screen and the loading bar rng.seed(this.seedValue); for (const pct of [0, 10, 15, 20, 24, 25, 35, 37, 40]) this.drawLoading(pct); // InitNewGame this.drawLoading(45); let code = 0; for (let i = 0; i <= 3; i++) code = code + toInt(rng.rand(1, 9) * 10 ** i); if (code === HARPCODE) code = code + 1; this.accessCode = code; this.createMap(); this.drawLoading(70); // InitWayPoints: one waypoint per door that allows one, after the rooms' own for (const d of this.doors) if (!d.noWaypoint) this.waypoints++; for (let n = 20; n <= this.waypoints; n += 20) this.drawLoading(71 + Math.floor(f32(f32(9 / this.waypoints) * n))); this.drawLoading(79); this.scp106Timer = 70 * 60 * rng.rand(12, 17); this.drawLoading(81); for (const r of this.rooms) { if (!r.t.disableDecals) { if (rng.rand(4) === 1) { const [id, x, , z] = this.argsAt("ING1", () => rng.rand(2, 3), () => f32(r.x + rng.rnd(-2, 2)), () => 0.003, () => f32(r.z + rng.rnd(-2, 2)), () => rng.rand(360)); const d = this.decal(id, x, 0.003, z, null); d.site = "ING1"; d.size = rng.rnd(0.1, 0.4); rng.rnd(0.85, 0.95); } if (rng.rand(4) === 1) { const [x, z] = this.argsAt("ING2", () => f32(r.x + rng.rnd(-2, 2)), () => f32(r.z + rng.rnd(-2, 2)), () => rng.rand(360)) as unknown as [number, number]; // CreateDecal(0, ...) and then de\ID = 1 const d = this.decal(1, x, 0.003, z, null); d.site = "ING2"; d.size = rng.rnd(0.5, 0.7); rng.rnd(0.7, 0.85); } } if (r.t.name === "start" && !this.opts.intro) { this.createItem("docORI", 1, 1, 1, null); this.createItem("doc173", 1, 1, 1, null); } } rng.rand(160, 200); this.initEvents(); return this.result(seed); } result(seed: string | number): GenResult { const pos = (room: Room | null, x: number, y: number, z: number) => { if (!room) return { x, y, z }; const dx = x - room.fx, dz = z - room.fz; const a = (room.angle * Math.PI) / 180; const c = Math.cos(a), s = Math.sin(a); return { x: room.x + c * dx - s * dz, y, z: room.z + s * dx + c * dz }; }; const grid: number[][] = []; for (let y = 0; y <= this.H; y++) { const row: number[] = []; for (let x = 0; x <= this.W; x++) row.push(this.mapTemp[x][y]); grid.push(row); } return { seed, seedNumber: this.seedValue, accessCode: this.accessCode, loadingScreen: this.loadingScreen, rooms: this.rooms.map((r) => ({ index: r.index, template: r.t.name, shape: r.t.shape, zone: r.zone, x: r.x, y: r.y, z: r.z, angle: wrapAngle(r.angle), gx: Math.round(r.x / 8), gz: Math.round(r.z / 8) })), grid, doors: this.doors.map((d) => ({ ...pos(d.world ? null : d.room, d.x, d.y, d.z), angle: d.angle + (d.room && !d.world ? d.room.angle : 0), room: d.room ? d.room.index : -1, open: d.open, big: d.big, keycard: d.keycard, code: d.code })), items: this.items.map((it) => ({ template: it.name, ...pos(it.room, it.x, it.y, it.z), room: it.room ? it.room.index : -1, yaw: it.yaw })), decals: this.decals.map((d) => ({ site: d.site ?? (d.room ? d.room.t.name : "-"), id: d.id, ...pos(d.room, d.x, d.y, d.z), size: d.size, room: d.room ? d.room.index : -1 })), events: this.events.map((e) => ({ name: e.name, room: e.room.index })), forest: this.rooms.find((r) => r.forest)?.forest ?? null, scp106Timer: this.scp106Timer, rngCalls: this.rng.calls, }; } } export function wrapAngle(a: number): number { while (a < 0) a += 360; while (a >= 360) a -= 360; return a; } /** TFormVector of a point through a room's transform (scale RoomScale, yaw `angle`). */ function tformVector(x: number, y: number, z: number, angle: number): [number, number, number] { const a = (wrapAngle(angle) * Math.PI) / 180; // multiples of 90 degrees: exact const c = Math.round(Math.cos(a)), s = Math.round(Math.sin(a)); const sx = f32(x * ROOM_SCALE), sy = f32(y * ROOM_SCALE), sz = f32(z * ROOM_SCALE); return [f32(c * sx - s * sz), sy, f32(s * sx + c * sz)]; } /** InitRoomTemplates' ordering pass: templates with a higher "set room priority" move ahead. */ export function sortTemplates(list: GenTemplate[]): GenTemplate[] { type Node = { t: GenTemplate; prev: Node | null; next: Node | null }; let head: Node | null = null, tail: Node | null = null; for (const t of list) { const n: Node = { t, prev: tail, next: null }; if (tail) tail.next = n; else head = n; tail = n; } const remove = (n: Node) => { if (n.prev) n.prev.next = n.next; else head = n.next; if (n.next) n.next.prev = n.prev; else tail = n.prev; n.prev = n.next = null; }; const insertBefore = (n: Node, at: Node) => { remove(n); n.prev = at.prev; n.next = at; if (at.prev) at.prev.next = n; else head = n; at.prev = n; }; let sorted: boolean; do { sorted = true; let rt: Node | null = head; while (rt) { let rtt: Node | null = rt.next; while (rtt && rtt.t.priority > rt.t.priority) { sorted = false; insertBefore(rtt, rt); rtt = rt.next; } rt = rt.next; } } while (!sorted); const out: GenTemplate[] = []; for (let n: Node | null = head; n; n = n.next) out.push(n.t); return out; } export function generate(data: GenData, seed: string | number, opts: GenOptions = {}): GenResult { return new Gen(data, opts).run(seed); } export { ROOM1, ROOM2, ROOM2C, ROOM3, ROOM4, ROOM_SCALE as RoomScale }; // ===== packages/scpcb/src/gen/fillroom.ts ===== /*! SCP - Containment Breach facility generator for Sourcemined (sourcemined.com/scpcb/seed-map/). * A port of SCP - Containment Breach's source code (MapSystem.bb, Main.bb, Menu.bb) by Joonas Rikkonen * ("Regalis"), Undertow Games, the SCP-CB Community Preservation Project and contributors: * https://github.com/Regalis11/scpcb and https://store.steampowered.com/app/2178380/ * The game is based on the SCP Foundation wiki (https://scp-wiki.wikidot.com/). * Original and port licensed CC BY-SA 3.0: https://creativecommons.org/licenses/by-sa/3.0/ * This port (packages/scpcb/src/gen and src/scp294.ts) is available under the same licence. */ // FillRoom (MapSystem.bb): what each room adds when it is created, in the game's order. Only what // draws random numbers or that the seed map shows is kept: doors, items, decals, lights, and the // SCP-860-1 forest. Positions are the game's, before the room is turned. import { f32 } from "./blitz.ts"; import type { Door, Gen, Room } from "./generate.ts"; import { genForestGrid, placeForest } from "./forest.ts"; const RS = 8 / 2048; export function fillRoom(g: Gen, r: Room): void { const rng = g.rng; const X = (v: number) => f32(r.x + f32(v * RS)); const Y = (v: number) => f32(r.y + f32(v * RS)); const Z = (v: number) => f32(r.z + f32(v * RS)); const A = (v: number) => f32(v * RS); const door = (x: number, y: number, z: number, angle: number, open: number | boolean = false, big: number | boolean = 0, keycard: number | boolean = 0, code = "", lvl = r.zone): Door => { void lvl; return g.createDoor(r, x, y, z, angle, open, big, keycard, code); }; const item = (name: string, x: number, y: number, z: number, parent = true) => g.createItem(name, x, y, z, parent ? r : null); /** CreateDecal(...) whose only random argument is its yaw. */ const decalYaw = (id: number, x: number, y: number, z: number, randYaw: "rand" | "rnd", parent = true) => { if (randYaw === "rand") rng.rand(360); else rng.rnd(360); return g.decal(id, x, y, z, parent ? r : null); }; switch (r.t.name) { case "room860": { door(X(928), 0, Z(640), 0, false, false, false, "ABCD"); const d = door(X(928), 0, Z(-640), 0, true, false, false, "ABCD"); d.noWaypoint = true; door(X(416), 0, Z(-640), 0, false, false, 1); door(X(416), 0, Z(640), 0, false, false, 1); const grid = genForestGrid(rng); r.forest = grid; placeForest(g, grid, r.x, f32(r.y + 30), r.z); item("doc8601", X(672), Y(176), Z(335)); item("doc860", X(1152), Y(176), Z(-384)); break; } case "lockroom": case "lockroom_ez": door(X(-736), 0, Z(-104), 0, true); door(X(104), 0, Z(736), 270, true); break; case "lockroom2": for (let i = 0; i <= 5; i++) { let [id, x, y, z] = g.argsAt("LR2", () => rng.rand(2, 3), () => f32(r.x + f32(rng.rnd(-392, 520) * RS)), () => f32(A(3) + rng.rnd(0, 0.001)), () => f32(r.z + f32(rng.rnd(-392, 520) * RS)), () => rng.rnd(360)); const de = g.decal(id, x, y, z, null); de.size = rng.rnd(0.3, 0.6); [id, x, y, z] = g.argsAt("LR2", () => rng.rand(15, 16), () => f32(r.x + f32(rng.rnd(-392, 520) * RS)), () => f32(A(3) + rng.rnd(0, 0.001)), () => f32(r.z + f32(rng.rnd(-392, 520) * RS)), () => rng.rnd(360)); g.decal(id, x, y, z, null); de.size = rng.rnd(0.1, 0.6); [id, x, y, z] = g.argsAt("LR2", () => rng.rand(15, 16), () => f32(r.x + rng.rnd(-0.5, 0.5)), () => f32(A(3) + rng.rnd(0, 0.001)), () => f32(r.z + rng.rnd(-0.5, 0.5)), () => rng.rnd(360)); g.decal(id, x, y, z, null); de.size = rng.rnd(0.1, 0.6); } break; case "gatea": { door(X(-4064), Y(-1280), Z(3952), 0, false); door(r.x, r.y, Z(-1024), 0, false); door(X(-1440), Y(-480), Z(2328), 0, false, false, 2); door(X(-1440), Y(-480), Z(4352), 0, false, false, 2); for (const r2 of g.rooms) if (r2.t.name === "gateaentrance") door(X(1544), r.y, Z(-64), 90, false, 3, 0, "", 0); break; } case "gateaentrance": door(X(744), 0, Z(512), 90, true, 3, 0, "", 0); door(r.x, 0, Z(-360), 0, false, true, 5); break; case "exit1": door(r.x, 0, Z(-320), 0, false, true, 5); door(X(720), 0, Z(1432), 0, true, 3, 0, "", 0); door(X(-5424), A(10784), Z(-1380), 0, false, 3, 0, "", 0); door(X(4352), A(10784), Z(-492), 0, false, 0, 0, "", 0); door(X(4352), A(10784), Z(500), 0, false, 0, 0, "", 0); door(X(3248), A(9856), Z(6400), 0, false, false, 0, "ABCD", 0); door(X(3072), A(9856), Z(5800), 90, false, false, 3, "", 0); break; case "roompj": item("doc372", X(800), Y(176), Z(1108)); item("radio", X(800), Y(112), Z(944)); door(r.x, r.y, Z(-368), 0, true, true, 2); break; case "room079": door(r.x, A(-448), Z(1136), 0, false, true, 4); door(X(1456), A(-448), Z(976), 0, false, true, 3); door(X(1144), A(-448), Z(704), 90, false, false, -1); decalYaw(3, X(1184), f32(A(-448) + 0.01), Z(1792), "rnd").size = 0.5; break; case "checkpoint1": door(X(48), 0, Z(-128), 0, false, false, 3, "", 0); door(X(-352), 0, Z(-128), 0, false, false, 3, "", 0); if (g.mt(Math.floor(r.x / 8), Math.floor(r.z / 8) - 1) === 0) door(r.x, 0, f32(r.z - 4), 0, 0, 2, 0, "GEAR"); break; case "checkpoint2": door(X(-48), 0, Z(128), 0, false, false, 5, "", 0); door(X(352), 0, Z(128), 0, false, false, 5, "", 0); if (g.mt(Math.floor(r.x / 8), Math.floor(r.z / 8) - 1) === 0) door(r.x, 0, f32(r.z - 4), 0, 0, false, 0, "GEAR"); break; case "room2testroom2": door(X(-240), 0, Z(640), 90, false, false, 1); door(X(-512), 0, Z(384), 0, false, false); item("key2", X(-914), Y(137), Z(61)); item("snav300", X(-312), Y(264), Z(176)); break; case "room2storage": for (const dx of [-1288, -760, -264, 264, 760, 1288]) door(X(dx), 0, r.z, 270); item("doc939", X(352), Y(176), Z(256)); item("bat", X(352), Y(112), Z(448)); item("emptycup", X(-672), A(240), Z(288)); item("key1", X(-672), Y(240), Z(224)); break; case "room2sroom": door(X(1440), A(224), Z(32), 90, false, false, 4); item("scp420j", X(1776), Y(400), Z(427)); item("scp420j", X(1808), Y(400), Z(435)); item("key5", X(2232), Y(392), Z(387)); item("docNDP", X(2248), Y(440), Z(372)); item("radio", X(2240), Y(320), Z(128)); break; case "room2shaft": door(X(1552), r.y, Z(552), 0, false, false); door(X(256), r.y, Z(744), 90, false, false, 2); item("key3", X(1119), Y(233), Z(494)); item("firstaid", X(1035), Y(145), Z(56)); item("bat", X(1930), Y(97), Z(256)); item("bat", X(1061), Y(161), Z(494)); item("eyedrops", X(1930), Y(225), Z(128)); decalYaw(3, X(1334), f32(A(-796) + 0.01), Z(-220), "rnd").size = 0.25; break; case "room2poffices": door(X(240), 0, Z(448), 90, false, false, 0, String(g.accessCode)); door(X(-496), 0, r.z, 90, false, false, 0, "ABCD"); door(X(240), 0, Z(-576), 90, false, false, 0, "7816"); item("mysteriousnote", X(736), Y(224), Z(544)); item("vest", X(608), Y(112), Z(32)); item("docIR106", X(704), Y(183), Z(-576)); item("docGonzales", X(912), Y(176), Z(-160)); item("firstaid", X(912), Y(112), Z(-336)); break; case "room2poffices2": door(X(240), 0, Z(48), 270, false, false, 3); if (g.opts.birthday) { decalYaw(22, X(600), 0.005, Z(232), "rand"); item("bdc", X(600), Y(125), Z(232)); } door(X(-432), 0, r.z, 90, false, false, 0, "1234"); decalYaw(0, X(-808), 0.005, Z(-72), "rand"); decalYaw(2, X(-808), 0.01, Z(-72), "rand").size = 0.3; decalYaw(0, X(-432), 0.01, r.z, "rand"); item("docL4", X(-688), 1.0, Z(-16)); item("docL5", X(-808), 1.0, Z(-72)); item("docMSP", X(622), Y(125), Z(-73)); break; case "room2elevator": door(X(448), 0, r.z, 90, false, 3); break; case "room2cafeteria": item("cup", X(-508), A(-187), Z(284)); item("cup", X(1412), A(-187), Z(-716)); item("emptycup", X(-540), A(-187), Z(124)); item("25ct", X(-447), Y(-334), Z(36)); item("25ct", X(1409), Y(-334), Z(-732)); break; case "room2nuke": door(X(576), 0, Z(152), 90, false, false, 5); door(X(-544), A(1504), Z(738), 90, false, false, 5); door(X(1192), 0, r.z, 90, true, 3); door(X(680), A(1504), r.z, 90, false, 3); item("docNDP", X(-768), Y(1684), Z(-768)); item("vest", X(-944), Y(1652), Z(-656)); break; case "room2tunnel": { door(X(264), 0, Z(656), 90, true, 3); door(X(-264), 0, Z(-656), 90, true, 3); let temp = (Math.trunc(g.accessCode) * 3) % 10000; if (temp < 1000) temp = temp + 1000; door(r.x, r.y, r.z, 0, false, true, false, String(temp), 0); decalYaw(0, X(64), 0.005, Z(144), "rand"); item("docL6", X(64), Y(144), Z(-384)); break; } case "008": door(X(296), 0, Z(-672), 180, true, 0, 4); door(X(296), 0, Z(-144), 0, false); door(X(-384), 0, Z(-672), 0, false, 0, 4); item("hazmatsuit", X(-76), 0.5, Z(-396)); item("doc008", X(-245), Y(192), Z(368)); break; case "room035": door(X(-296), 0, Z(-672), 180, true, 0, 5); door(X(-296), 0, Z(-144), 0, false); door(X(384), 0, Z(-672), 180, false, 0, 5); door(X(768), 0, Z(512), 90, false, 0, 0, "5731", 0); item("doc035ad", X(248), Y(220), Z(576)); item("radio", X(-544), 0.5, Z(704)); item("scp500", X(1168), A(224), Z(576)); item("scp148", X(-360), 0.5, Z(644)); item("doc035", X(1168), A(104), Z(608)); break; case "room513": door(X(-704), 0, Z(304), 0, false, 0, 2); item("scp513", X(-32), Y(196), Z(688)); item("docL3", X(736), 1.0, Z(48)); item("doc513", X(-480), A(104), Z(-176)); break; case "room966": door(X(-400), 0, r.z, -90, false, false, 3); door(r.x, 0, Z(-480), 180, false, false, 3); item("nvgoggles", X(320), 0.5, Z(704)); break; case "room3storage": { door(r.x, 0, Z(448), 0, true, 3); door(X(5840), A(-5632), Z(1048), 0, false, 3); door(X(608), 0, Z(-312), 0, true, 3); door(X(-456), A(-5632), Z(-824), 0, false, 3); let x = 0, z = 0; switch (rng.rand(3)) { case 1: x = 2312; z = -952; break; case 2: x = 3032; z = 1288; break; case 3: x = 2824; z = 2808; break; } item("hand2", X(x), f32(A(-5596) + 1), Z(z)); item("nvgoggles", X(1936), Y(-5496), Z(-944)); decalYaw(3, X(x), f32(A(-5632) + 0.01), Z(z), "rnd").size = 0.5; door(X(56), Y(-5632), Z(6344), 90, false, 2); door(X(1157), Y(-5632), Z(660), 0, false, 2); door(X(234), Y(-5632), Z(5239), 90, false, 2); door(X(3446), Y(-5632), Z(6369), 90, false, 2); break; } case "room049": { door(X(330), 0, Z(656), 90, true, 3); door(X(2898), A(-3520), Z(1824), 90, false, 3); door(X(-672), 0, Z(-408), 0, true, 3); door(X(-2766), A(-3520), Z(-1592), 0, false, 3); door(X(272), A(-3552), Z(104), 90, false); door(X(264), A(-3520), Z(-1824), 90, false); door(X(-264), A(-3520), Z(1824), 90, false); door(r.x, 0, r.z, 0, false, 2, -2, "", 0); item("doc049", X(-608), Y(-3332), Z(876)); item("key4", X(-512), Y(-3412), Z(864)); item("firstaid", X(385), Y(-3412), Z(271)); door(X(-272), Y(-3552), Z(98), 90, true, true); door(X(-2990), Y(-3520), Z(-1824), 90, false, 2).noWaypoint = true; door(X(-896), r.y, Z(-640), 90, false, 2).noWaypoint = true; break; } case "room012": door(X(264), 0, Z(672), 270, false, false, 3); door(X(-512), A(-768), Z(-336), 0, false, false); item("doc012", X(-56), Y(-576), Z(-408)); item("hand", X(-784), f32(A(-576) + 0.3), Z(640)); decalYaw(3, X(-784), f32(A(-768) + 0.01), Z(640), "rnd").size = 0.5; break; case "room2servers": door(r.x, 0, r.z, 0, false, 2, false, "", 0); door(X(-208), 0, Z(-736), 90, true, false, false, ""); door(X(-208), 0, Z(736), 90, true, false, false, ""); door(X(-672), 0, Z(-1024), 0, false, false, false, "GEAR").noWaypoint = true; break; case "room3servers": item("bat", X(-132), Y(-368), Z(-648)); if (rng.rand(2) === 1) item("bat", X(-76), Y(-368), Z(-648)); if (rng.rand(2) === 1) item("bat", X(-196), Y(-368), Z(-648)); item("snav300", X(124), Y(-368), Z(-648)); break; case "room3servers2": item("doc970", X(960), Y(-448), Z(251)); item("gasmask", X(954), Y(-504), Z(235)); break; case "testroom": door(X(720), 0, r.z, 0, false, 2, -1, "", 0); door(X(-624), A(-1280), r.z, 90, true, 0, 0, "", 0); item("doc682", X(656), Y(-1200), Z(-16)); break; case "room2closets": { item("doc1048", X(736), Y(176), Z(736)); item("gasmask", X(736), Y(176), Z(544)); item("bat", X(736), Y(176), Z(-448)); if (rng.rand(2) === 1) item("bat", X(730), Y(176), Z(-496)); if (rng.rand(2) === 1) item("bat", X(740), Y(176), Z(-560)); item("key1", X(736), Y(240), Z(752)); item("clipboard", X(736), Y(224), Z(-720)); // the clipboard holds this document; it stays in the item list g.createItem("doc1048a", X(736), Y(224), Z(-720), null); door(X(-240), 0, r.z, 90, false, 0, 0, "", 0); break; } case "room2offices": item("doc106", X(404), Y(145), Z(559)); item("key2", X(-156), Y(151), Z(72)); item("snav300", X(305), Y(153), Z(944)); item("docRAND1", X(-137), Y(153), Z(464)); g.waypoints += 2; break; case "room2offices2": item("key1", X(-368), Y(-48), Z(80)); item("doc895", X(-800), Y(-48), Z(368)); if (rng.rand(2) === 1) item("doc860", X(-800), Y(-48), Z(-464)); else item("doc093rm", X(-800), Y(-48), Z(-464)); item("snav300", X(-336), Y(-48), Z(-480)); rng.rand(1, 4); break; case "room2offices3": { if (rng.rand(2) === 1) item("docMTF", X(744), Y(240), Z(944)); else item("docSC", X(680), Y(240), Z(944)); item("docOBJC", X(160), Y(240), Z(568)); item("docRAND3", X(-1440), Y(624), Z(152)); item("radio", X(-1184), Y(480), Z(-800)); // Blitz evaluates a For loop's To expression on every pass, so Rand(0, 1) is drawn again each time for (let i = 0; i <= rng.rand(0, 1); i++) item("eyedrops", X(-1529), Y(563), f32(Z(-572) + i * 0.05)); item("bat", X(-1545), Y(603), Z(-372)); if (rng.rand(2) === 1) item("bat", X(-1540), Y(603), Z(-340)); if (rng.rand(2) === 1) item("bat", X(-1529), Y(603), Z(-308)); door(X(-1056), A(384), Z(290), 90, true); break; } case "start": door(X(4000), A(384), Z(1696), 90, true, true); door(X(2704), A(384), Z(624), 90, false); door(X(1392), A(384), Z(64), 90, true); door(X(-640), A(384), Z(64), 90, false); door(X(1280), A(384), Z(312), 180, true); door(r.x, 0, Z(1184), 0, false); decalYaw(0, X(272), 0.005, Z(262), "rand"); decalYaw(0, X(456), 0.005, Z(135), "rand"); break; case "room2scps": { door(X(264), 0, r.z, 90, true, false, 3); door(X(-264), 0, r.z, 270, true, false, 3); door(X(-560), 0, Z(-272), 0, true, false, 3); door(X(560), 0, Z(-272), 180, true, false, 3); door(X(560), 0, Z(272), 180, true, false, 3); door(X(-560), 0, Z(272), 0, true, false, 3); item("scp714", X(-552), Y(220), Z(-760)); item("scp1025", X(552), Y(224), Z(-758)); item("scp860", X(568), Y(178), Z(760)); item("doc714", X(-728), Y(288), Z(-360)); item("doc427", X(-608), Y(66), Z(636)); const spots: [number, number][] = [[-64, -516], [-96, -388], [-128, -292], [-128, -132], [-160, -36], [-192, 28], [-384, 28], [-448, 92], [-480, 124], [-512, 156], [-544, 220], [-544, 380], [-544, 476], [-544, 572], [-544, 636]]; spots.forEach(([dx, dz], i) => { const [id] = g.argsAt("SCPS", () => rng.rand(15, 16), () => rng.rand(360)); const de = g.decal(id, X(dx), 0.005, Z(dz), r); de.size = i > 10 ? rng.rnd(0.2, 0.25) : rng.rnd(0.1, 0.17); }); break; } case "room205": door(X(128), 0, Z(640), 90, true, false, 3); door(X(-1392), A(-128), Z(-384), 0, true, false, 3, ""); break; case "endroom": door(r.x, 0, Z(1136), 0, false, true, 6); break; case "endroomc": door(X(1024), 0, r.z, 0, false, 2, false, ""); break; case "coffin": door(r.x, 0, Z(-448), 0, false, true, 2); item("doc895", X(-688), Y(133), Z(-304)); item("key3", X(240), Y(-1456), Z(2064)); item("nvgoggles", X(280), Y(-1456), Z(2164)); break; case "room2doors": door(r.x, 0, Z(528), 0, true); door(r.x, 0, Z(-528), 180, true); break; case "914": door(r.x, 0, Z(-368), 0, false, true, 2); door(X(-624), 0, Z(528), 180, true); door(X(816), 0, Z(528), 180, true); item("docRAND2", X(954), Y(228), Z(127)); item("firstaid", X(960), Y(112), Z(-40)); item("docL1", X(-928), A(160), Z(-160)); break; case "173": { door(X(288), 0, Z(384), 90, false, true); { const [id] = g.args(() => rng.rand(4, 5), () => rng.rnd(360)); g.decal(id, X(1472), 0.002, Z(912), null).size = 1.2; } for (let xt = 0; xt <= 1; xt++) { for (let zt = 0; zt <= 1; zt++) { const [id, x, y, z] = g.argsAt("I173", () => rng.rand(4, 6), () => f32(f32(r.x + A(700) + xt * A(700)) + rng.rnd(-0.5, 0.5)), () => rng.rnd(0.001, 0.0018), () => f32(f32(r.z + A(600 * zt)) + rng.rnd(-0.5, 0.5)), () => rng.rnd(360)); const de = g.decal(id, x, y, z, null); de.size = rng.rnd(0.5, 0.8); rng.rnd(0.8, 1.0); } } door(X(-1008), 0, Z(-688), 90, true, false, false, ""); door(X(-2320), 0, Z(-1248), 90, true); door(X(-4352), 0, Z(-1248), 90, true); door(X(-3712), A(-385), Z(-128), 0, true); door(X(-3712), A(-385), Z(-2336), 0, false).noWaypoint = true; door(X(-6864), 0, Z(-1248), 90, true); door(X(-5856), 0, Z(-1504), 0, false).noWaypoint = true; door(X(-2432), 0, Z(-1000), 0, false).noWaypoint = true; for (let zt = 0; zt <= 1; zt++) { door(X(-5760), 0, Z(320 + 896 * zt), 0, false).noWaypoint = true; const d = door(X(-8288), 0, Z(320 + 896 * zt), 0, false); if (zt !== 0) d.noWaypoint = true; for (let xt = 0; xt <= 2; xt++) door(X(-(7424 - 512 * xt)), 0, Z(1008 - 480 * zt), 180 * (zt === 0 ? 1 : 0), false).noWaypoint = true; for (let xt = 0; xt <= 4; xt++) door(X(-(5120 - 512 * xt)), 0, Z(1008 - 480 * zt), 180 * (zt === 0 ? 1 : 0), false).noWaypoint = true; } item("docORI", X(-(2914 + 1024)), A(170), Z(40), false); break; } case "room2ccont": door(X(64), 0, Z(368), 180, false, false, 2); item("docDan", X(-400), A(1040), Z(115)); break; case "room106": item("key5", X(-752), Y(-592), Z(3026)); item("doc106_2", X(-416), Y(-576), Z(2492)); item("docRP", X(268), Y(-576), Z(2593)); door(X(-968), A(-764), Z(1392), 0, false, false, 4); door(r.x, 0, Z(-464), 0, false, false, 4); door(X(-624), A(-1280), r.z, 90, false, false, 4); break; case "room1archive": { for (let xt = 0; xt <= 1; xt++) { for (let yt = 0; yt <= 2; yt++) { for (let zt = 0; zt <= 2; zt++) { let name = "bat"; const chance = rng.rand(-10, 100); if (chance < 0) break; // Exit leaves the innermost For if (chance < 40) { name = "doc" + ["1123", "1048", "939", "682", "079", "096", "966"][rng.rand(1, 7) - 1]; } else if (chance < 45) name = "key" + rng.rand(1, 2); else if (chance < 50) name = "firstaid"; else if (chance < 60) name = "bat"; else if (chance < 70) name = "snav300"; else if (chance < 85) name = "radio"; else if (chance < 95) name = "clipboard"; else name = ["playingcard", "mastercard", "origami"][rng.rand(1, 3) - 1]; const x = f32(f32(-672 + 864 * xt) * RS); const y = f32(f32(96 + 96 * yt) * RS); const z = f32(f32(f32(480 - 352 * zt) + rng.rnd(-96, 96)) * RS); item(name, f32(r.x + x), y, f32(r.z + z)); } } } door(r.x, r.y, Z(-528), 0, false, false, 6); break; } case "room1123": item("doc1123", X(511), Y(125), Z(-936)); item("scp1123", X(832), Y(166), Z(784)); item("leaflet", X(-816), Y(704), Z(888)); item("gasmask", X(457), Y(150), Z(960)); door(X(832), 0, Z(367), 0, false, false, 3); door(X(280), 0, Z(-607), 90, false, false); door(X(280), A(512), Z(-607), 90, false, false); break; case "pocketdimension": item("burntnote", r.x, 0.5, f32(r.z + 3.5), false); door(r.x, A(2048), f32(f32(r.z + 32) - A(1024)), 0, false, 0, 0, "", 0); door(r.x, A(2048), f32(f32(r.z + 32) + A(1024)), 180, false, 0, 0, "", 0); rng.rnd(0.8, 0.8); for (let i = 1; i <= 8; i++) if (i < 6) rng.rnd(0.5, 0.5); break; case "room2_3": case "room3_3": g.waypoints += 1; break; case "room1lifts": g.waypoints += 1; break; case "room2tesla": case "room2tesla_lcz": case "room2tesla_hcz": g.waypoints += 2; break; case "room2servers2": door(X(264), 0, Z(672), 270, false, false, 3); door(X(-512), A(-768), Z(-336), 0, false, false, 3); door(X(-509), A(-768), Z(-1037), 0, false, false, 3).noWaypoint = true; item("nvgoggles", X(56.0154), Y(-648), Z(749.638)); rng.rand(245); break; case "room2gw": case "room2gw_b": { door(X(336), 0, Z(-382), 0, false, false); door(X(336), 0, Z(462), 180, false, false); if (r.t.name === "room2gw") { const bd = g.room2gwBroken && g.room2gwX === r.x && g.room2gwZ === r.z; // (room2gw_brokendoor = 0 And Rand(1,2)=1) Or bd_temp const broken = (!g.room2gwBroken && rng.rand(1, 2) === 1) || bd; if (broken) { g.room2gwBroken = true; g.room2gwX = r.x; g.room2gwZ = r.z; } } break; } case "room3gw": door(X(-728), 0, Z(-458), 0, false, false, 3); door(X(-223), 0, Z(-736), -90, false, false, 3); door(X(-459), 0, Z(339), 90, false, false); door(X(385), 0, Z(339), 270, false, false); break; case "room1162": door(X(248), 0, Z(-736), 90, false, false, 2); item("doc1162", X(863.227), Y(152), Z(-953.231)); break; case "room2scps2": door(X(288), r.y, Z(576), 90, false, false, 3); door(X(777), r.y, Z(671), 90, false, false, 4); door(X(556), r.y, Z(296), 0, false, false, 3); item("scp1499", X(600), Y(176), Z(-228)); item("doc1499", X(840), Y(260), Z(224)); item("doc500", X(1152), Y(224), Z(336)); item("badge", X(364), Y(5), Z(716)); break; case "room3offices": door(X(736), 0, Z(240), 0, false, false, 3); break; case "room2offices4": door(X(-240), 0, r.z, 90, false, 0, 0, "", 0); item("note682", X(-991), Y(-242), Z(904)); break; case "room2sl": door(X(480), r.y, Z(-640), 90, false, false, 3); door(X(544), Y(480), Z(256), 270, false, false, 3); door(X(1504), Y(480), Z(960), 0); break; case "lockroom3": door(X(-736), 0, Z(-104), 0, true); door(X(104), 0, Z(736), 270, true); break; case "medibay": item("firstaid", X(-506), Y(192), Z(-322)); item("syringe", X(-333), Y(100), Z(97.3)); item("syringe", X(-340), Y(100), Z(52.3)); door(X(-264), Y(-0), Z(640), 90, false, false, 3); break; case "room2cpit": { const d = door(X(-256), 0, Z(-752), 90, false, 2, 3); d.noWaypoint = true; item("docL2", X(-160), A(32), Z(-353)); break; } } for (let i = 0; i < r.t.lights; i++) g.addLight(r); g.waypoints += r.t.waypoints; } // ===== packages/scpcb/src/gen/forest.ts ===== /*! SCP - Containment Breach facility generator for Sourcemined (sourcemined.com/scpcb/seed-map/). * A port of SCP - Containment Breach's source code (MapSystem.bb, Main.bb, Menu.bb) by Joonas Rikkonen * ("Regalis"), Undertow Games, the SCP-CB Community Preservation Project and contributors: * https://github.com/Regalis11/scpcb and https://store.steampowered.com/app/2178380/ * The game is based on the SCP Foundation wiki (https://scp-wiki.wikidot.com/). * Original and port licensed CC BY-SA 3.0: https://creativecommons.org/licenses/by-sa/3.0/ * This port (packages/scpcb/src/gen and src/scp294.ts) is available under the same licence. */ // The SCP-860-1 forest (MapSystem.bb GenForestGrid and PlaceForest). The path grid is drawn from // the seed while the map is generated; placing it scatters trees and rocks over each tile's // heightmap, which draws more random numbers, and leaves the documents ("logs") on the path. import { f32, type BlitzRng } from "./blitz.ts"; import type { Gen } from "./generate.ts"; import { ROOM1, ROOM2, ROOM2C, ROOM3, ROOM4 } from "./types.ts"; export const GRIDSIZE = 10; const DEVIATION_CHANCE = 40, BRANCH_CHANCE = 65, BRANCH_MAX_LIFE = 4, BRANCH_DIE_CHANCE = 18; const MAX_DEVIATION = 3, RETURN_CHANCE = 27, CENTER = 5, MIN_DOOR = 3, MAX_DOOR = 7; /** Whether a Blitz `Local arr[n]` inside a loop is cleared each time it runs (the game re-declares * `itemPlaced` for every tile). */ export const LOCAL_ARRAY_RESETS = (globalThis as any).process?.env?.SCPCB_LOCALRESET === "1"; /** Whether Blitz's Or evaluates its right side even when the left is true. */ export const OR_EVALUATES_BOTH = (globalThis as any).process?.env?.SCPCB_OR !== "short"; function moveForward(dir: number, px: number, py: number, retval = 0): number { if (dir === 1) return retval === 0 ? px : py + 1; return retval === 0 ? px - 1 + dir : py; } function turnIfDeviating(maxDev: number, px: number, center: number, dir: number, retval = 0): number { const cur = center - px; let deviated = 0; if ((dir === 0 && cur >= maxDev) || (dir === 2 && cur <= -maxDev)) { dir = (dir + 2) % 4; deviated = 1; } return retval === 0 ? dir : deviated; } export function genForestGrid(rng: BlitzRng): number[] { const G = GRIDSIZE; const chance = (c: number) => rng.rand(0, 100) <= c; const grid = new Array(G * G + 11).fill(0); const door1 = rng.rand(MIN_DOOR, MAX_DOOR); const door2 = rng.rand(MIN_DOOR, MAX_DOOR); grid[door1] = 3; grid[(G - 1) * G + door2] = 3; let px = door2, py = 1, dir = 1; grid[(G - 1 - py) * G + px] = 1; while (py < G - 4) { if (dir === 1) { if (chance(DEVIATION_CHANCE)) { dir = 2 * rng.rand(0, 1); dir = turnIfDeviating(MAX_DEVIATION, px, CENTER, dir); const deviated = turnIfDeviating(MAX_DEVIATION, px, CENTER, dir, 1); if (deviated) grid[(G - 1 - py) * G + px] = 1; px = moveForward(dir, px, py); py = moveForward(dir, px, py, 1); } } else { dir = turnIfDeviating(MAX_DEVIATION, px, CENTER, dir); const deviated = turnIfDeviating(MAX_DEVIATION, px, CENTER, dir, 1); // Blitz's Or evaluates both sides: the chance is drawn even when already deviated if (OR_EVALUATES_BOTH) { const c = chance(RETURN_CHANCE); if (deviated || c) dir = 1; } else if (deviated || chance(RETURN_CHANCE)) dir = 1; px = moveForward(dir, px, py); py = moveForward(dir, px, py, 1); if (dir === 1) { grid[(G - 1 - py) * G + px] = 1; px = moveForward(dir, px, py); py = moveForward(dir, px, py, 1); } } grid[(G - 1 - py) * G + px] = 1; } dir = 1; while (py < G - 2) { px = moveForward(dir, px, py); py = moveForward(dir, px, py, 1); grid[(G - 1 - py) * G + px] = 1; } if (px !== door1) { dir = door1 > px ? 2 : 0; while (px !== door1) { px = moveForward(dir, px, py); py = moveForward(dir, px, py, 1); grid[(G - 1 - py) * G + px] = 1; } } let newY = -3; while (newY < G - 6) { newY += 4; let tempY = newY; let newX = 0; if (chance(BRANCH_CHANCE)) { const branchPos = 2 * rng.rand(0, 1); let leftmost = G - 1, rightmost = 0; for (let i = 0; i < G; i++) if (grid[(G - 1 - newY) * G + i] === 1) { if (i < leftmost) leftmost = i; if (i > rightmost) rightmost = i; } newX = branchPos === 0 ? leftmost - 1 : rightmost + 1; // And is not short-circuit in Blitz, but these reads have no side effects if (newX >= 0 && newX < G && grid[(G - 1 - tempY - 1) * G + newX] !== 1 && grid[(G - 1 - tempY + 1) * G + newX] !== 1) { grid[(G - 1 - tempY) * G + newX] = -1; newX = branchPos === 0 ? leftmost - 2 : rightmost + 2; if (newX >= 0 && newX < G) { grid[(G - 1 - tempY) * G + newX] = -1; let i = 2; while (i < BRANCH_MAX_LIFE) { i++; if (chance(BRANCH_DIE_CHANCE)) break; if (rng.rand(0, 3) === 0) newX += branchPos === 0 ? -1 : 1; else tempY++; if (newX < 0 || newX >= G || grid[(G - 1 - tempY - 1) * G + newX] === 1) break; grid[(G - 1 - tempY) * G + newX] = -1; if (tempY >= G - 2) break; } } } } } for (let i = 1; i <= G - 2; i++) for (let j = 0; j < G; j++) if (grid[i * G + j] === -1) grid[i * G + j] = 1; return grid.slice(0, G * G); } /** PlaceForest: tiles, scattered details (random numbers only) and the log documents. */ export function placeForest(g: Gen, grid: number[], x: number, y: number, z: number): void { const rng = g.rng, G = GRIDSIZE, tileSize = 12; const placedRows = new Set(); const at = (tx: number, ty: number) => grid[ty * G + tx] ?? 0; for (let tx = 0; tx < G; tx++) { for (let ty = 1; ty <= G - 2; ty++) { if (at(tx, ty) !== 1) continue; let n = 0; if (tx + 1 < G) n = at(tx + 1, ty) > 0 ? 1 : 0; if (tx - 1 >= 0) n += at(tx - 1, ty) > 0 ? 1 : 0; if (ty + 1 < G) n += at(tx, ty + 1) > 0 ? 1 : 0; if (ty - 1 >= 0) n += at(tx, ty - 1) > 0 ? 1 : 0; let angle = 0; let shape = 0; switch (n) { case 1: if (at(tx, ty + 1) > 0) angle = 180; else if (at(tx - 1, ty) > 0) angle = 270; else if (at(tx + 1, ty) > 0) angle = 90; shape = ROOM1; break; case 2: if (at(tx, ty - 1) > 0 && at(tx, ty + 1) > 0) shape = ROOM2; else if (at(tx + 1, ty) > 0 && at(tx - 1, ty) > 0) { angle = 90; shape = ROOM2; } else { if (at(tx - 1, ty) > 0 && at(tx, ty + 1) > 0) angle = 180; else if (at(tx + 1, ty) > 0 && at(tx, ty - 1) > 0) angle = 0; else if (at(tx - 1, ty) > 0 && at(tx, ty - 1) > 0) angle = 270; else angle = 90; shape = ROOM2C; } break; case 3: if (at(tx, ty - 1) === 0) angle = 180; else if (at(tx - 1, ty) === 0) angle = 90; else if (at(tx + 1, ty) === 0) angle = 270; shape = ROOM3; break; case 4: shape = ROOM4; break; } if (shape === 0) continue; const hm = g.data.forest[String(shape)]; const w = hm.width; for (let lx = 3; lx <= w - 2; lx++) { for (let ly = 3; ly <= w - 2; ly++) { const red = hm.red[(w - ly) * w + lx] ?? 0; if (red > rng.rand(100, 260)) { const k = rng.rand(0, 7); if (k <= 6) { rng.rnd(0.25, 0.4); for (let i = 0; i <= 3; i++) rng.rnd(-20, 20); rng.rnd(3.0, 3.2); g.args(() => rng.rnd(-5, 5), () => rng.rnd(360)); } else { rng.rnd(0.01, 0.012); rng.rnd(360); } } } } // logs: one on the path in rows 2, 5 and 8 if (ty % 3 === 2 && (LOCAL_ARRAY_RESETS || !placedRows.has(Math.floor(ty / 3)))) { placedRows.add(Math.floor(ty / 3)); const n3 = Math.floor(ty / 3) + 1; const local = shape === ROOM1 ? [0.4, -0] : shape === ROOM2C ? [1.7, -0.4] : [0, 0]; const a = (angle * Math.PI) / 180, c = Math.round(Math.cos(a)), s = Math.round(Math.sin(a)); const wx = f32(x + tx * tileSize + c * local[0] - s * local[1]); const wz = f32(z + ty * tileSize + s * local[0] + c * local[1]); g.createItem("log" + n3, wx, f32(y + 0.2), wz, null); } } } } export { ROOM2, ROOM4 }; // ===== packages/scpcb/src/scp294.ts ===== /*! SCP - Containment Breach facility generator for Sourcemined (sourcemined.com/scpcb/seed-map/). * A port of SCP - Containment Breach's source code (MapSystem.bb, Main.bb, Menu.bb) by Joonas Rikkonen * ("Regalis"), Undertow Games, the SCP-CB Community Preservation Project and contributors: * https://github.com/Regalis11/scpcb and https://store.steampowered.com/app/2178380/ * The game is based on the SCP Foundation wiki (https://scp-wiki.wikidot.com/). * Original and port licensed CC BY-SA 3.0: https://creativecommons.org/licenses/by-sa/3.0/ * This port (packages/scpcb/src/gen and src/scp294.ts) is available under the same licence. */ // SCP-294's drink lookup and the effects of drinking, as the game's code applies them // (Main.bb: the 294 keypad's dispense branch, GetINISectionLocation, and the "cup" item use). /** Blitz Int(string): leading sign and digits only ("-0.2" is 0, "2.5" is 2), like C's atoi. */ export function blitzInt(s: string | null | undefined): number { const m = /^\s*([+-]?\d+)/.exec(s ?? ""); return m ? parseInt(m[1], 10) | 0 : 0; } /** Blitz Float(string): like C's atof. */ export function blitzFloat(s: string | null | undefined): number { const m = /^\s*([+-]?(\d+\.?\d*|\.\d+)([eE][+-]?\d+)?)/.exec(s ?? ""); return m ? Math.fround(parseFloat(m[1])) : 0; } /** What the keypad input becomes before the lookup: trimmed, lower case, "cup of " / "a cup of " removed. */ export function normalizeInput(input: string, prefixes: string[] = ["cup of", "a cup of"]): string { let s = input.toLowerCase().trim(); for (const p of prefixes) { const prefix = p + " "; if (s.slice(0, Math.min(prefix.length, s.length)) === prefix) s = s.slice(prefix.length); } return s; } /** * GetINISectionLocation: the first section line whose name list contains the text as a whole * entry ("[" or "|" before it, "]" or "|" after). Returns the index into `sections` or -1. * The search steps past each partial hit by its length + 1, exactly as the game does. */ export function findSection(sectionLines: string[], text: string): number { if (text === "") return -1; const section = text.toLowerCase(); for (let n = 0; n < sectionLines.length; n++) { const line = sectionLines[n].toLowerCase(); // Blitz Instr is 1-based; Mid(s, 0, 1) is "" (so a hit at the very start never counts) const instr = (from: number) => { const i = line.indexOf(section, from - 1); return i < 0 ? 0 : i + 1; }; const mid = (p: number) => (p >= 1 && p <= line.length ? line[p - 1] : ""); let temp = instr(1); while (temp > 0) { const before = mid(temp - 1), after = mid(temp + section.length); if ((before === "[" || before === "|") && (after === "]" || after === "|")) return n; temp = instr(temp + section.length + 1); } } return -1; } export interface DrinkRaw { section: string; keys: Record } export interface DrinkEffect { refused: boolean; message: string | null; lethal: boolean; deathMessage: string | null; explosion: boolean; /** Seconds of blur added (the game reads a whole number). */ blur: number; cameraShake: number; vomit: number; /** Seconds until death, 0 = none. */ deathTimer: number; injuries: number; bloodLoss: number; stomachache: boolean; blinkEffect: number | null; blinkEffectTimer: number | null; staminaEffect: number | null; staminaEffectTimer: number | null; } /** GetINIString2: keys match without regard to case; the first one in the section wins. */ export function key(d: DrinkRaw, name: string): string { const k = Object.keys(d.keys).find((x) => x.toLowerCase() === name.toLowerCase()); return k === undefined ? "" : d.keys[k]; } /** The effects of drinking one cup straight from the dispenser (cup state 1). */ export function drinkEffect(d: DrinkRaw): DrinkEffect { const s = (k: string) => key(d, k); // GetINIInt2: "true" is 1, "false" is 0, anything else Int(text) const int2 = (k: string) => { const v = s(k).toLowerCase(); return v === "true" ? 1 : v === "false" ? 0 : blitzInt(v); }; const opt = (k: string) => (s(k) !== "" ? blitzFloat(s(k)) : null); return { refused: s("refusemessage") !== "", message: s("refusemessage") || s("message") || null, lethal: int2("lethal") !== 0, deathMessage: s("deathmessage") || null, explosion: int2("explosion") === 1, blur: Math.max(int2("blur"), 0), cameraShake: Math.max(blitzFloat(s("camerashake")), 0), vomit: Math.max(int2("vomit"), 0), deathTimer: Math.max(int2("deathtimer"), 0), injuries: int2("damage") + int2("injuries"), bloodLoss: int2("blood loss"), stomachache: int2("stomachache") !== 0, blinkEffect: opt("blink effect"), blinkEffectTimer: opt("blink effect timer"), staminaEffect: opt("stamina effect"), staminaEffectTimer: opt("stamina effect timer"), }; }