Exact source used by this build. SHA-256: bb66c709994b27c810092c97b575d8fd7c1379d5c80e02f2135040eadbc33005.

1 import type { Area, TilePosition, WorldState } from "../../types";
2 import { createRng, type SeededRng } from "../../rng";
3 import { getMines, mineAreaId, mineDay, type MineFloor, type MineMonster, type MineRock, type MinesContent } from "./model";
4 export function mineFloorSeed(worldSeed: number, floor: number, day: string): number { let hash = worldSeed >>> 0; for (const char of `${floor}:${day}`) hash = Math.imul(hash ^ char.charCodeAt(0), 16777619) >>> 0; return hash; }
5 const between = (rng: SeededRng, min: number, max: number) => min + rng.nextInt(max - min + 1);
6 /** Rooms and connecting corridors are generated, never loaded from authored maps. */
7 export function generateMineFloor(state: WorldState, number: number, data: MinesContent): Area {
8   const config = data.config, width = config.width, height = config.height, day = mineDay(state), seed = mineFloorSeed(getMines(state).worldSeed ?? state.rngState, number, day), rng = createRng(seed);
9   const band = data.bands.find((entry) => number >= entry.minFloor && number <= entry.maxFloor) ?? data.bands[data.bands.length - 1]!;
10   const collision = Array<boolean>(width * height).fill(true), carve = (x: number, y: number) => { if (x > 0 && y > 0 && x < width - 1 && y < height - 1) collision[y * width + x] = false; };
11   const rooms: { x: number; y: number; width: number; height: number; center: TilePosition }[] = [];
12   const roomCount = between(rng, config.rooms.min, config.rooms.max);
13   for (let room = 0; room < roomCount; room++) {
14     const rw = Math.min(width - 2, between(rng, config.rooms.minSize, config.rooms.maxSize)), rh = Math.min(height - 2, between(rng, config.rooms.minSize, config.rooms.maxSize));
15     const x = between(rng, 1, width - rw - 1), y = between(rng, 1, height - rh - 1), center = { x: x + Math.floor(rw / 2), y: y + Math.floor(rh / 2) };
16     for (let py = y; py < y + rh; py++) for (let px = x; px < x + rw; px++) carve(px, py);
17     const previous = rooms[room - 1]?.center;
18     if (previous) { for (let px = Math.min(previous.x, center.x); px <= Math.max(previous.x, center.x); px++) { carve(px, previous.y); carve(px, previous.y + 1); } for (let py = Math.min(previous.y, center.y); py <= Math.max(previous.y, center.y); py++) { carve(center.x, py); carve(center.x + 1, py); } }
19     rooms.push({ x, y, width: rw, height: rh, center });
20   }
21   const spawn = rooms[0]!.center, roll = rng.next(), treasure = number % config.treasureEvery === 0;
22   const kind: MineFloor["kind"] = treasure ? "treasure" : roll < config.infestedChance ? "infested" : roll < config.infestedChance + config.mushroomChance ? "mushroom" : "normal";
23   const available: TilePosition[] = [];
24   for (let y = 1; y < height - 1; y++) for (let x = 1; x < width - 1; x++) if (!collision[y * width + x] && Math.hypot(x - spawn.x, y - spawn.y) > 2) available.push({ x, y });
25   for (let index = available.length - 1; index > 0; index--) { const other = rng.nextInt(index + 1); [available[index], available[other]] = [available[other]!, available[index]!]; }
26   const rocks: Record<string, MineRock> = {}, monsters: Record<string, MineMonster> = {}, mushrooms: MineFloor["mushrooms"] = {};
27   if (!treasure) {
28     const count = kind === "infested" ? 0 : between(rng, config.rocks.min, config.rocks.max);
29     for (let index = 0; index < count && available.length; index++) {
30       const position = available.pop()!, roll = rng.next(), weights = config.nodeWeights;
31       const nodeId = roll < weights.rock ? band.rockNodeId : roll < weights.rock + weights.ore ? band.oreNodeId : roll < weights.rock + weights.ore + weights.gem ? band.gemNodeIds[rng.nextInt(band.gemNodeIds.length)]! : band.geodeNodeId;
32       const id = `rock_${index + 1}`; rocks[id] = { id, nodeId, ...position, hits: 0 };
33     }
34     const countMonsters = between(rng, config.monsters.min, config.monsters.max);
35     for (let index = 0; index < countMonsters && available.length; index++) {
36       const position = available.pop()!, monsterId = band.monsterIds[rng.nextInt(band.monsterIds.length)]!, definition = data.monsters.find((entry) => entry.id === monsterId)!;
37       const id = `monster_${index + 1}`; monsters[id] = { id, monsterId, ...position, health: definition.health, attackReadyAtMs: 0, hopReadyAtMs: 0, stunnedUntilMs: 0, facingX: 0, facingY: 1 };
38     }
39     if (kind === "mushroom") for (let index = 0, count = between(rng, data.mushrooms.min, data.mushrooms.max); index < count && available.length; index++) mushrooms[`mushroom_${index + 1}`] = { ...available.pop()!, itemId: data.mushrooms.itemIds[rng.nextInt(data.mushrooms.itemIds.length)]! };
40   }
41   const chestPosition = available.pop() ?? { x: spawn.x + 1, y: spawn.y }, ladderPosition = available.pop() ?? { x: spawn.x, y: spawn.y + 1 };
42   const floor: MineFloor = { number, day, seed, rngState: rng.state, band: band.id, kind, rocks, monsters, mushrooms, ladder: treasure && number < config.maxFloor ? ladderPosition : null, chest: treasure ? { ...chestPosition, opened: getMines(state).claimedTreasureFloors.includes(number) } : null, projectiles: [], nextProjectileId: 1 };
43   const tiles = collision.map(() => ({ ground: band.id, tillable: false, tilled: false, watered: false, objectId: null as string | null, crop: null }));
44   for (const rock of Object.values(rocks)) tiles[rock.y * width + rock.x]!.objectId = rock.id;
45   return { id: mineAreaId(number), name: "TBD", width, height, tiles, collision, groundTiles: collision.map((wall) => wall ? 0 : 1), decorTiles: Array<number>(width * height).fill(0), spawn, landmarks: [], warps: [], clockRateMsPerGameMinute: config.clockMsPerGameMinute, outdoor: false, locked: false, systems: { mines: floor } };
46 }
47 export function ladderChance(data: MinesContent, remainingRocks: number, remainingMonsters: number, dailyLuck = 0, luckLevel = 0): number { const config = data.config.ladder; return Math.min(1, Math.max(0, config.baseChance + config.remainingRockNumerator / Math.max(1, remainingRocks) + dailyLuck / config.luckDivisor + luckLevel * config.luckLevelMultiplier + (remainingMonsters === 0 ? config.emptyMonsterBonus : 0))); }
48