diff --git a/src/area/area-json-creator.ts b/src/area/area-json-creator.ts index 0523997..aa2c7f3 100644 --- a/src/area/area-json-creator.ts +++ b/src/area/area-json-creator.ts @@ -91,6 +91,9 @@ export function createArea( const pos: Vec2 = Vec2.copy(tpr) Vec2.divC(pos, divider) Vec2.sub(pos, offsetDas) + + Vec2.divC(Vec2.floor(Vec2.mulC(pos, 8)), 8) + connections.push({ tx: pos.x, ty: pos.y, diff --git a/src/area/custom-area-container.ts b/src/area/custom-area-container.ts index 8dbefe7..884fa30 100644 --- a/src/area/custom-area-container.ts +++ b/src/area/custom-area-container.ts @@ -183,7 +183,7 @@ function drawConnection(v: Vec2, connection: sc.AreaLoadable.SDCustom.Connection x += 2 let h = 4 if (connection.dir == Dir.NORTH) { - inactiveColors.empty.draw(x, y, connection.size, h) + inactiveColors.empty.draw(x, y, connection.size, h + 2) inactiveColors.border.draw(x - 1, y + 1, 1, h - 2) inactiveColors.border.draw(x + connection.size, y + 1, 1, h - 2) } else if (connection.dir == Dir.EAST) { diff --git a/src/dungeon/builder.ts b/src/dungeon/builder.ts index ba69822..c049f16 100644 --- a/src/dungeon/builder.ts +++ b/src/dungeon/builder.ts @@ -4,6 +4,8 @@ import { drawMapArrangeQueue } from '../map-arrange/drawer' import { MapArrange, MapArrangeData } from '../map-arrange/map-arrange' import { MapPicker, mapPickerConfigurable } from '../map-arrange/map-picker/configurable' import { AreaInfo, constructMapsFromMapsArrange } from '../map-construct/map-construct' +import { initAllPuzzles } from '../maps/puzzle-data' +import { Dir } from '../util/geometry' import { Item } from '../util/items' import { setRandomSeed } from '../util/util' import { DungeonPaths } from './paths' @@ -15,7 +17,7 @@ export class DungeonBuilder { const queue = new BuildQueue(true) const randomizeDirTryOrder = true const roomSizeReg = { x: 13 * 16, y: 13 * 16 } - const tunnelSizeReg = { x: 5 * 16, y: 3 * 16 } + const tunnelSizeReg = { x: 5 * 16, y: 5 * 16 } const roomSizeBranch = { x: 17 * 16, y: 17 * 16 } const tunnelSizeBranch = { x: 5 * 16, y: 5 * 16 } @@ -57,21 +59,25 @@ export class DungeonBuilder { const mapPicker: MapPicker = mapPickerConfigurable({ root: { - type: 'Simple', - size: roomSizeReg, - count: 2, + type: 'DngPuzzleTunnel', + tunnelSize: tunnelSizeReg, + // size: roomSizeReg, + count: 20, randomizeDirTryOrder, - followedBy: branch( - 1, - () => 1, - () => 1, - () => 2 - ), + // followedBy: branch( + // 1, + // () => 1, + // () => 1, + // () => 2 + // ), }, }) setRandomSeed(seed) + + await initAllPuzzles() + const mapsArrange = queue.begin(mapPicker(-1, queue)) as MapArrange[] // console.dir(queue.queue, { depth: null }) console.log(drawMapArrangeQueue(queue, 16, false, undefined, false, true)) @@ -103,6 +109,6 @@ export class DungeonBuilder { mapsConstruct[0].constructed.name, ig.TeleportPosition.createFromJson({ marker: 'entrance_0', level: 0, baseZPos: 0, size: { x: 0, y: 0 } }) ) - // console.dir(mapsConstruct, { depth: null }) + console.dir(mapsConstruct, { depth: null }) } } diff --git a/src/map-arrange/drawer.ts b/src/map-arrange/drawer.ts index 4e0d858..4bc8fc2 100644 --- a/src/map-arrange/drawer.ts +++ b/src/map-arrange/drawer.ts @@ -5,7 +5,7 @@ import { MapArrangeData, MapArrange, offsetMapArrange, copyMapArrange } from '.. import { BuildQueueAccesor } from '../build-queue/build-queue' import 'colorts/lib/string' -const colorMap = ['red', 'green', 'yellow', 'blue', 'magenta', 'cyan'] as const +const colorMap = ['yellow', 'blue', 'magenta', 'cyan'] as const export function drawMapArrangeQueue( queue: BuildQueueAccesor, @@ -68,6 +68,19 @@ export function drawMapArrangeQueue( console.error(`error while trying to parse rect ${Rect.toString(rect)} of map ${map.id}`, e) } } + // for (const tpr of map.entranceTprs) { + // const { x, y } = Vec2.round(Vec2.divC(Vec2.copy(tpr), scale)) + // let char = (map.id % 10).toString() + // if (color) char = char.green.italic + // + // strmap[y][x] = char + // } + // for (const tpr of map.rects) { + // const { x, y } = Vec2.round(Vec2.divC(Vec2.copy(tpr), scale)) + // let char = (map.id % 10).toString() + // if (color) char = char.red.italic + // strmap[y][x] = char + // } } return strmap.map(arr => arr.join('')).join('\n') diff --git a/src/map-arrange/map-picker/configurable.ts b/src/map-arrange/map-picker/configurable.ts index 17ba7de..cada2f9 100644 --- a/src/map-arrange/map-picker/configurable.ts +++ b/src/map-arrange/map-picker/configurable.ts @@ -49,6 +49,7 @@ export namespace MapPicker { export interface Config { root: ConfigNode + startDir?: Dir } export type ConfigNodeBuildtime = ConfigNode & { @@ -113,7 +114,7 @@ export function mapPickerConfigurable(_config: MapPicker.Config): MapPicker { const lastTpr = last ? (last.restTprs.find(t => t.destId == newId)! as TprArrange) - : { x: 0, y: 0, dir: Dir.NORTH, destId: 0 } + : { x: 0, y: 0, dir: _config.startDir ?? Dir.NORTH, destId: 0 } const nodeId = nextConfig?.nodeId ?? last?.nodeId ?? 0 const nodeProgress = nextConfig ? 0 : (last?.nodeProgress ?? 0) diff --git a/src/map-construct/map-construct.ts b/src/map-construct/map-construct.ts index 35bca43..1ed8b80 100644 --- a/src/map-construct/map-construct.ts +++ b/src/map-construct/map-construct.ts @@ -8,6 +8,9 @@ import { MapConstructionLayers } from './layer' import { getEmptyLayers } from './layer' import { MapTheme } from './theme' +export type TprDoorLikeType = 'Door' | 'TeleportGround' +export type TprType = TprDoorLikeType | 'TeleportField' + export interface MapConstruct extends MapArrange { arrangeCopy: MapArrange constructed: sc.MapModel.Map @@ -67,7 +70,7 @@ export function baseMapConstruct( areaId: string, theme: MapTheme, extend: Record -): MapInConstruction { +): MapInConstruction { const boundsEntity = Rect.boundsOfArr(map.rects) for (let dir = 0 as Dir; dir < 4; dir++) { @@ -81,7 +84,7 @@ export function baseMapConstruct( const bounds = Rect.div(Rect.copy(boundsEntity), 16) - const mapSize: Vec2 = Rect.toTwoVecSize(bounds)[1] + const mapSize: Vec2 = { x: bounds.width, y: bounds.height } const mic: MapInConstruction = { name: mapName, diff --git a/src/map-construct/room.ts b/src/map-construct/room.ts index 5546f28..5763571 100644 --- a/src/map-construct/room.ts +++ b/src/map-construct/room.ts @@ -9,6 +9,10 @@ export function placeRoom(room: RoomArrange, map: MapInConstruction, tc: MapThem const rect = Rect.div(Rect.copy(room), 16) const { x: rx, y: ry } = rect const { x: rx2, y: ry2 } = Rect.x2y2(rect) + assert(rx % 1 == 0) + assert(ry % 1 == 0) + assert(rx2 % 1 == 0) + assert(ry2 % 1 == 0) const background = map.layers.background[0] const shadow = map.layers.shadow const light = map.layers.light diff --git a/src/maps/dng-puzzle-tunnel.ts b/src/maps/dng-puzzle-tunnel.ts new file mode 100644 index 0000000..97809a5 --- /dev/null +++ b/src/maps/dng-puzzle-tunnel.ts @@ -0,0 +1,146 @@ +import { Id, NextQueueEntryGenerator } from '../build-queue/build-queue' +import { + TprArrange, + MapArrangeData, + MapArrange, + RoomArrange, + doesMapArrangeFit, + TprArrange3d, +} from '../map-arrange/map-arrange' +import { MapPicker, registerMapPickerNodeConfig } from '../map-arrange/map-picker/configurable' +import { registerMapConstructor } from '../map-construct/map-construct' +import { Dir, DirU, Rect } from '../util/geometry' +import { shuffleArray } from '../util/util' +import { Vec2 } from '../util/vec2' +import { getPuzzleList } from './puzzle-data' +import { simpleMapConstructor } from './simple' + +declare global { + export namespace MapPickerNodeConfigs { + export interface All { + DngPuzzleTunnel: DngPuzzleTunnel + } + export interface DngPuzzleTunnel { + count: number + tunnelSize: Vec2 + randomizeDirTryOrder?: boolean + followedBy?: MapPicker.ConfigNode + } + } +} +registerMapPickerNodeConfig('DngPuzzleTunnel', (data, buildtimeData) => { + return das({ ...data, ...buildtimeData }) +}) +export function das({ + mapPicker, + exitTpr, + tunnelSize, + destId, + destIndex, + finishedWhole, + branchDone, + nodeId, + nodeProgress, +}: { + mapPicker: MapPicker + exitTpr: TprArrange + tunnelSize: Vec2 + destId: Id + destIndex: number + finishedWhole?: boolean + branchDone?: boolean + nodeId?: number + nodeProgress?: number +}): NextQueueEntryGenerator { + return (id, _, accesor) => { + const tpr: TprArrange = { + dir: DirU.flip(exitTpr.dir), + x: exitTpr.x, + y: exitTpr.y, + destId, + destIndex, + } + const map: MapArrange = { + type: 'DngPuzzleTunnel', + rects: [], + restTprs: [], + id, + entranceTprs: [tpr], + branchDone, + nodeId, + nodeProgress, + } + + let tunnelEntrance: RoomArrange + { + const rect = Rect.centered(tunnelSize, tpr) + const walls: Record = [true, true, true, true] + walls[exitTpr.dir] = false + tunnelEntrance = { ...rect, walls } + map.rects.push(tunnelEntrance) + } + if (!doesMapArrangeFit(accesor, map, id)) return null + + const puzzles = shuffleArray(getPuzzleList(tpr.dir)) + + return { + data: map, + id, + branch: 0, + branchCount: puzzles.length, + + nextQueueEntryGenerator: (_, branch, accesor) => { + const map = { rects: [] as RoomArrange[], restTprs: [] as TprArrange3d[] } + const puzzle = puzzles[branch] + + const bounds = Rect.boundsOfArr(puzzle.rects) + if (puzzle.sel.data.type == blitzkrieg.PuzzleRoomType.AddWalls) Rect.extend(bounds, 3 * 2 * 16) + const size = { x: bounds.width, y: bounds.height } + const offset = { x: 0, y: 0 } + if ((tunnelSize.x / 16) % 2 != (size.x / 16) % 2) offset.x += 16 + if ((tunnelSize.x / 16) % 2 != (size.y / 16) % 2) offset.y += 16 + + Vec2.add(size, offset) + + const rect = Rect.centered(size, { + ...Rect.middle(Rect.side(tunnelEntrance, exitTpr.dir)), + dir: tpr.dir, + }) + // const rect = Rect.centered(size, tpr) + + const room: RoomArrange = { + ...rect, + walls: [true, true, true, true], + } + + map.rects.push(room) + + if (!doesMapArrangeFit(accesor, map, id)) return null + + { + const pos: Vec2 = Rect.sideVec(room, Vec2.add(Vec2.copy(puzzle.exit.vec), room), puzzle.exit.dir) + Vec2.round(pos) + + map.restTprs.push({ + ...pos, + dir: puzzle.exit.dir, + destId: id + 1, + }) + } + + return { + data: map, + id, + finishedEntry: true, + finishedWhole, + + branch: 0, + branchCount: 1, + getNextQueueEntryGenerator: () => mapPicker(id, accesor), + } + }, + } + } +} + +registerMapConstructor('DngPuzzleTunnel', simpleMapConstructor) diff --git a/src/maps/puzzle-data.ts b/src/maps/puzzle-data.ts new file mode 100644 index 0000000..75a61c0 --- /dev/null +++ b/src/maps/puzzle-data.ts @@ -0,0 +1,107 @@ +import type { PuzzleRoomType, PuzzleSelection } from 'cc-blitzkrieg/types/puzzle-selection' +import { Rect, Dir } from '../util/geometry' +import { ObjectEntriesT } from '../util/modify-prototypes' +import type * as _ from 'cc-blitzkrieg/types/plugin' +import { TprType } from '../map-construct/map-construct' +import { assert } from '../util/util' +import { Vec2 } from '../util/vec2' + +export namespace PuzzleData { + export type Tpr = sc.MapModel.MapEntity +} + +export interface PuzzleData { + sel: PuzzleSelection + + map: string + rects: Rect[] + entrance: ReturnType + exit: ReturnType + exitTpr?: PuzzleData.Tpr + completionCondition?: { path: string; value: any } +} + +let allPuzzles!: PuzzleData[] +export async function initAllPuzzles() { + const puzzles: (PuzzleData | undefined)[] = [] + const promises: Promise[] = [] + + for (const [map, entry] of ObjectEntriesT(blitzkrieg.sels.puzzle.selMap)) { + for (const sel of entry.sels) { + if (sel.data.type == blitzkrieg.PuzzleRoomType.Dis) continue + + const promise = blitzkrieg.mapUtil.getMapObject(map) + promises.push(promise) + promise.then(mapData => puzzles.push(createPuzzleData(map, sel, mapData))) + } + } + + await Promise.all(promises) + allPuzzles = puzzles.filter(Boolean) as PuzzleData[] +} + +function createPuzzleData(map: string, sel: PuzzleSelection, mapData: sc.MapModel.Map): PuzzleData | undefined { + if (sel.data.completionType == blitzkrieg.PuzzleCompletionType.Item) { + console.warn(`sel on ${map} has UNIMPLEMENTED PuzzleCompletionType.Item`) + return + } + const selPos = Rect.mul(Rect.copy(sel.sizeRect), 16) + const rects = sel.bb.map(r => Vec2.sub(Rect.mul(Rect.copy(r), 16), selPos) as Rect) + + let exitTpr: PuzzleData.Tpr | undefined + exitTprIf: if ( + sel.data.completionType == blitzkrieg.PuzzleCompletionType.GetTo || + sel.data.completionType == blitzkrieg.PuzzleCompletionType.Normal + ) { + const { dist, entity } = mapData.entities.reduce( + (acc, entity) => { + if (entity.type != 'Door' && entity.type != 'TeleportGround' && entity.type != 'TeleportField') + return acc + + const dist = Vec2.distance(entity, sel.data.endPos) + if (dist < acc.dist) return { dist, entity } + return acc + }, + { dist: 100e3, entity: undefined } as { dist: number; entity: sc.MapModel.MapEntity | undefined } + ) + if (dist < 200) break exitTprIf + assert(entity) + + exitTpr = { ...entity } + Vec2.sub(exitTpr, selPos) + } else assert(false) + + let completionCondition: { path: string; value: any } | undefined + if (sel.data.completionType == blitzkrieg.PuzzleCompletionType.Normal) { + const res = blitzkrieg.PuzzleSelectionManager.getPuzzleSolveCondition(sel) + assert(res) + completionCondition = { path: res[0], value: res[1] } + } else if (sel.data.completionType == blitzkrieg.PuzzleCompletionType.GetTo) { + completionCondition = undefined + } else if (sel.data.completionType == blitzkrieg.PuzzleCompletionType.Item) { + assert(false, `sel on ${map} has UNIMPLEMENTED PuzzleCompletionType.Item`) + } else assert(false) + + const entrance = Rect.closestSideArr(rects, Vec2.sub(Vec2.copy(sel.data.startPos), selPos)) + const exit = Rect.closestSideArr(rects, Vec2.sub(Vec2.copy(sel.data.endPos), selPos)) + const res: PuzzleData = { + sel, + rects, + map, + entrance, + exit, + exitTpr, + completionCondition, + } + + if (res.entrance.dir == res.exit.dir) return + + assert(res.exit.distance < 16 * 16) + assert(res.entrance.distance < 16 * 16) + return res +} + +export function getPuzzleList(entranceDir: Dir, filter?: PuzzleRoomType): PuzzleData[] { + assert(allPuzzles) + return allPuzzles.filter(p => p.entrance.dir == entranceDir && (filter === undefined || filter == p.sel.data.type)) +} diff --git a/src/maps/simple-branch.ts b/src/maps/simple-branch.ts index 76fc7c1..04cafef 100644 --- a/src/maps/simple-branch.ts +++ b/src/maps/simple-branch.ts @@ -65,7 +65,7 @@ export function simpleMapBranchTunnelArrange({ let tunnelEntrance: RoomArrange { - const rect = Rect.centeredRect(tunnelSize, tpr) + const rect = Rect.centered(tunnelSize, tpr) const walls: Record = [true, true, true, true] walls[exitTpr.dir] = false tunnelEntrance = { ...rect, walls } @@ -73,7 +73,7 @@ export function simpleMapBranchTunnelArrange({ } let room: RoomArrange { - const rect = Rect.centeredRect(roomSize, { + const rect = Rect.centered(roomSize, { ...Rect.middle(Rect.side(tunnelEntrance, exitTpr.dir)), dir: tpr.dir, }) @@ -121,7 +121,7 @@ export function simpleMapBranchTunnelArrange({ let tunnelExit: RoomArrange { - const rect = Rect.centeredRect(tunnelSize, { + const rect = Rect.centered(tunnelSize, { ...Rect.middle(Rect.side(room, dir)), dir: DirU.flip(dir), }) diff --git a/src/maps/simple-tunnel.ts b/src/maps/simple-tunnel.ts index bad08a7..d1eb148 100644 --- a/src/maps/simple-tunnel.ts +++ b/src/maps/simple-tunnel.ts @@ -78,7 +78,7 @@ export function simpleMapTunnelArrange({ let tunnelEntrance: RoomArrange { - const rect = Rect.centeredRect(tunnelSize, tpr) + const rect = Rect.centered(tunnelSize, tpr) const walls: Record = [true, true, true, true] walls[exitTpr.dir] = false tunnelEntrance = { ...rect, walls } @@ -86,7 +86,7 @@ export function simpleMapTunnelArrange({ } let room: RoomArrange { - const rect = Rect.centeredRect(roomSize, { + const rect = Rect.centered(roomSize, { ...Rect.middle(Rect.side(tunnelEntrance, exitTpr.dir)), dir: tpr.dir, }) @@ -116,7 +116,7 @@ export function simpleMapTunnelArrange({ let tunnelExit: RoomArrange { - const rect = Rect.centeredRect(tunnelSize, { + const rect = Rect.centered(tunnelSize, { ...Rect.middle(Rect.side(room, dir)), dir: DirU.flip(dir), }) diff --git a/src/maps/simple.ts b/src/maps/simple.ts index f1dddda..71a71df 100644 --- a/src/maps/simple.ts +++ b/src/maps/simple.ts @@ -84,7 +84,7 @@ export function simpleMapArrange({ } let room: RoomArrange { - const rect = Rect.centeredRect(size, tpr) + const rect = Rect.centered(size, tpr) room = { ...rect, walls: [true, true, true, true] } map.rects.push(room) diff --git a/src/setup.ts b/src/setup.ts index 907be61..283b768 100644 --- a/src/setup.ts +++ b/src/setup.ts @@ -8,3 +8,4 @@ import './map-construct/map-construct' import './maps/simple' import './maps/simple-branch' import './maps/simple-tunnel' +import './maps/dng-puzzle-tunnel' diff --git a/src/util/geometry.ts b/src/util/geometry.ts index f952276..749d933 100644 --- a/src/util/geometry.ts +++ b/src/util/geometry.ts @@ -85,12 +85,6 @@ export namespace Rect { height: v2.y, } } - export function toTwoVecSize(rect: Rect): [Vec2, Vec2] { - return [ - { x: rect.x, y: rect.y }, - { x: rect.width, y: rect.height }, - ] - } export function mul(rect: Rect, mul: number): Rect { rect.x *= mul rect.y *= mul @@ -231,6 +225,34 @@ export namespace Rect { } return smallest } + export function closestSideArr(rects: Rect[], vec: Vec2): { distance: number; dir: Dir; vec: Vec2; index: number } { + let smallest: { distance: number; dir: Dir; vec: Vec2; index: number } = { + distance: 10000, + dir: Dir.NORTH, + vec: { x: 0, y: 0 }, + index: -1, + } + rects.forEach((rect, i) => { + for (let dir = Dir.NORTH; dir < 4; dir++) { + const v: Vec2 = Rect.side(rect, dir) + if (DirU.isVertical(dir)) { + v.x = vec.x + } else { + v.y = vec.y + } + const distance = Vec2.distance(vec, v) + if (distance < smallest.distance) { + smallest = { + distance, + dir, + vec: v, + index: i, + } + } + } + }) + return smallest + } export function isVecIn(rect: Rect, vec: Vec2): boolean { return vec.x >= rect.x && vec.x < rect.x + rect.width && vec.y >= rect.y && vec.y < rect.y + rect.height } @@ -260,13 +282,14 @@ export namespace Rect { height: y2 - y, } } - export function centeredRect(size: Vec2, tpr: Vec2Dir): Rect { + export function centered(size: Vec2, tpr: Vec2Dir): Rect { const pos: Vec2 = Vec2.copy(tpr) if (tpr.dir == Dir.SOUTH || tpr.dir == Dir.EAST) { Vec2.moveInDirection(pos, DirU.flip(tpr.dir), size.y) } const move = size.x / 2 Vec2.moveInDirection(pos, DirU.isVertical(tpr.dir) ? Dir.WEST : Dir.NORTH, move) - return Rect.fromTwoVecSize(pos, Vec2.flipSides(size, !DirU.isVertical(tpr.dir))) + const res = Rect.fromTwoVecSize(pos, Vec2.flipSides(size, !DirU.isVertical(tpr.dir))) + return res } }