From 8adc66260f1c5421325e74992f62a583ec040cad Mon Sep 17 00:00:00 2001 From: krypek Date: Sun, 18 Aug 2024 15:07:57 +0200 Subject: [PATCH] Add map construction for Simple --- pnpm-lock.yaml | 8 +- src/area/area-json-creator.ts | 199 +++++++------- src/bun-run.ts | 9 +- src/dungeon/builder.ts | 41 ++- src/dungeon/paths.ts | 152 +++++++++++ src/library-providers.ts | 9 + src/map-arrange/map-arrange.ts | 10 +- src/map-construct/map-construct.ts | 7 + src/map-construct/theme.ts | 2 +- src/maps/simple-branch.ts | 12 +- src/maps/simple-tunnel.ts | 12 +- src/maps/simple.ts | 420 +++++++++++++++++++++++++++-- src/plugin.ts | 20 +- src/setup-test.ts | 1 + src/setup.ts | 2 + src/util/game-start.ts | 4 +- src/util/geometry.ts | 8 +- src/util/runtime-assets.ts | 2 + 18 files changed, 755 insertions(+), 163 deletions(-) create mode 100644 src/dungeon/paths.ts create mode 100644 src/library-providers.ts diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 015a48b..aa622e8 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -52,7 +52,7 @@ importers: version: 5.5.4 ultimate-crosscode-typedefs: specifier: github:krypciak/ultimate-crosscode-typedefs - version: https://codeload.github.com/krypciak/ultimate-crosscode-typedefs/tar.gz/f227aa0e8de791091c470b325007af7e2e7a90a6 + version: https://codeload.github.com/krypciak/ultimate-crosscode-typedefs/tar.gz/04f89469a0952b72164d4b99e30326de99c4c167 packages: @@ -877,8 +877,8 @@ packages: engines: {node: '>=14.17'} hasBin: true - ultimate-crosscode-typedefs@https://codeload.github.com/krypciak/ultimate-crosscode-typedefs/tar.gz/f227aa0e8de791091c470b325007af7e2e7a90a6: - resolution: {tarball: https://codeload.github.com/krypciak/ultimate-crosscode-typedefs/tar.gz/f227aa0e8de791091c470b325007af7e2e7a90a6} + ultimate-crosscode-typedefs@https://codeload.github.com/krypciak/ultimate-crosscode-typedefs/tar.gz/04f89469a0952b72164d4b99e30326de99c4c167: + resolution: {tarball: https://codeload.github.com/krypciak/ultimate-crosscode-typedefs/tar.gz/04f89469a0952b72164d4b99e30326de99c4c167} version: 0.0.1 engines: {node: '>=10.0.0'} @@ -1687,7 +1687,7 @@ snapshots: typescript@5.5.4: {} - ultimate-crosscode-typedefs@https://codeload.github.com/krypciak/ultimate-crosscode-typedefs/tar.gz/f227aa0e8de791091c470b325007af7e2e7a90a6: + ultimate-crosscode-typedefs@https://codeload.github.com/krypciak/ultimate-crosscode-typedefs/tar.gz/04f89469a0952b72164d4b99e30326de99c4c167: dependencies: '@types/jquery': 1.10.45 '@types/node': 11.15.54 diff --git a/src/area/area-json-creator.ts b/src/area/area-json-creator.ts index 386be77..e9bc6f5 100644 --- a/src/area/area-json-creator.ts +++ b/src/area/area-json-creator.ts @@ -1,93 +1,106 @@ -// import { BuildQueueAccesor } from '../build-queue/build-queue' -// import { copyMapArrange, MapArrange, MapArrangeData, offsetMapArrange } from '../map-arrange/map-arrange' -// import { Rect } from '../util/geometry' -// import { ObjectEntriesT } from '../util/modify-prototypes' -// import { assert } from '../util/util' -// -// export {} -// declare global { -// namespace sc { -// namespace AreaLoadable { -// interface Data { -// shaduungeonCustom?: boolean -// } -// /* SD - ShaDuungeon */ -// namespace SDCustom { -// // @ts-expect-error -// interface Data extends sc.AreaLoadable.Data { -// shaduungeonCustom: true -// floors: Floor[] -// } -// interface Floor { -// level: number -// name: ig.LangLabel.Data -// -// maps: Map[] -// connections: Connection[] -// icons: Icon[] -// landmarks: Landmark[] -// rooms?: sc.AreaRoomBounds[] -// } -// interface Map extends sc.AreaLoadable.Map {} -// } -// } -// } -// } -// -// export function createAreaJsonFromBuildQueue( -// accesor: BuildQueueAccesor, -// areaName: string, -// defaultFloor: number, -// chests: number, -// floorNames: Record, -// ): sc.AreaLoadable.SDCustom.Data { -// const maps: MapArrange[] = [] -// for (let id = 0; ; id++) { -// const res = accesor.tryGet(id) -// if (!res) break -// maps.push(copyMapArrange(res)) -// } -// -// const bounds: Rect = Rect.boundsOfArr(maps.flatMap(a => a.rects)) -// const offset = Vec2.mulC(bounds, -1) -// for (const map of maps) offsetMapArrange(map, offset) -// -// const mapsByFloor: Record = {} -// for (const map of maps) { -// ;(mapsByFloor[map.floor ?? 0] ??= []).push(map) -// } -// -// const floors: sc.AreaLoadable.SDCustom.Floor[] = [] -// let actualDefaultFloor!: number -// for (const [floor, maps] of ObjectEntriesT(mapsByFloor)) { -// if (floor == defaultFloor) actualDefaultFloor = floors.length -// -// const areaMaps: sc.AreaLoadable.SDCustom.Map[] = maps.map(map => { -// return map as any -// }) -// -// const name = floorNames[floor] -// assert(name) -// floors.push({ -// level: floor, -// name, -// -// maps: areaMaps, -// connections: [], -// icons: [], -// landmarks: [], -// }) -// } -// -// assert(actualDefaultFloor) -// return { -// DOCTYPE: 'AREAS_MAP', -// shaduungeonCustom: true, -// name: areaName, -// width: bounds.width, -// height: bounds.height, -// defaultFloor: actualDefaultFloor, -// chests, -// floors, -// } -// } +import { unique } from 'jquery' +import { AreaInfo, MapConstruct } from '../map-construct/map-construct' +import { Rect } from '../util/geometry' +import { ObjectEntriesT } from '../util/modify-prototypes' +import { allLangs, Array2d, assert } from '../util/util' + +export {} +declare global { + namespace sc { + namespace AreaLoadable { + interface Data { + shaduungeonCustom?: boolean + } + /* SD - ShaDuungeon */ + namespace SDCustom { + // @ts-expect-error + interface Data extends sc.AreaLoadable.Data { + shaduungeonCustom: true + floors: Floor[] + } + interface Floor { + level: number + name: ig.LangLabel.Data + + maps: Map[] + connections: Connection[] + icons: Icon[] + landmarks: Landmark[] + rooms?: sc.AreaRoomBounds[] + } + interface Map extends sc.AreaLoadable.Map {} + } + } + } +} + +export function createArea( + maps: MapConstruct[], + areaInfo: AreaInfo, + defaultFloor: number, + chests: number, + floorNames: Record +): sc.AreaLoadable.SDCustom.Data { + const { width, height }: Rect = Rect.boundsOfArr(maps.flatMap(a => a.rects)) + + const mapsByFloor: Record = {} + for (const map of maps) { + ;(mapsByFloor[map.floor ?? 0] ??= []).push(map) + } + + const floors: sc.AreaLoadable.SDCustom.Floor[] = [] + let actualDefaultFloor!: number + for (const [floor, maps] of ObjectEntriesT(mapsByFloor)) { + if (floor == defaultFloor) actualDefaultFloor = floors.length + + const areaMaps: sc.AreaLoadable.SDCustom.Map[] = maps.map(map => { + return { + path: map.constructed.name, + name: allLangs(map.title), + offset: { x: 0, y: 0 }, + dungeon: 'DUNGEON', + } + }) + + const name = floorNames[floor] + assert(name) + floors.push({ + level: floor, + name, + tiles: Array2d.empty({ x: width, y: height }, 0), + + maps: areaMaps, + connections: [], + icons: [], + landmarks: [], + }) + } + + assert(actualDefaultFloor !== undefined) + return { + DOCTYPE: 'AREAS_MAP', + shaduungeonCustom: true, + name: areaInfo.id, + width, + height, + defaultFloor: actualDefaultFloor, + chests, + floors, + } +} + +export function createAreaDbEntry(area: sc.AreaLoadable.SDCustom.Data, areaInfo: AreaInfo): sc.MapModel.Area { + return { + boosterItem: areaInfo.boosterItem.toString(), + landmarks: {}, + name: allLangs(areaInfo.title), + description: allLangs(areaInfo.description), + keyItem: areaInfo.keyItem.toString(), + masterKeyItem: areaInfo.masterKeyItem.toString(), + areaType: areaInfo.type, + order: 1001, + track: true, + chests: area.chests, + position: areaInfo.pos, + } +} diff --git a/src/bun-run.ts b/src/bun-run.ts index 16cee89..165bfed 100644 --- a/src/bun-run.ts +++ b/src/bun-run.ts @@ -1,7 +1,12 @@ import './setup-test' import { DungeonBuilder } from './dungeon/builder' -const b = new DungeonBuilder() -b.build('das') +import { initLibraries } from './library-providers' +;(async () => { + await initLibraries() + + const b = new DungeonBuilder() + b.build('das') +})() // const b = new Test_DungeonBuilder() // b.samePlaceFail() diff --git a/src/dungeon/builder.ts b/src/dungeon/builder.ts index a1e5bec..31e1692 100644 --- a/src/dungeon/builder.ts +++ b/src/dungeon/builder.ts @@ -1,15 +1,20 @@ +import { createArea, createAreaDbEntry } from '../area/area-json-creator' import { BuildQueue } from '../build-queue/build-queue' import { drawMapArrangeQueue } from '../map-arrange/drawer' import { MapArrange, MapArrangeData } from '../map-arrange/map-arrange' import { MapPicker, mapPickerConfigurable } from '../map-arrange/map-picker/configurable' -import { constructMapsFromMapsArrange } from '../map-construct/map-construct' +import { AreaInfo, constructMapsFromMapsArrange } from '../map-construct/map-construct' +import { Item } from '../util/items' import { setRandomSeed } from '../util/util' +import { DungeonPaths } from './paths' export class DungeonBuilder { - build(seed: string) { + async build(seed: string) { + const dungeonId = 'mydng' + const queue = new BuildQueue(true) const randomizeDirTryOrder = true - const roomSizeReg = { x: 9*16, y: 9*16 } + const roomSizeReg = { x: 9 * 16, y: 9 * 16 } // const tunnelSizeReg = { x: 1, y: 1 } // const roomSizeBranch = { x: 5, y: 5 } // const tunnelSizeBranch = { x: 1, y: 1 } @@ -54,7 +59,7 @@ export class DungeonBuilder { root: { type: 'Simple', size: roomSizeReg, - count: 1, + count: 6, randomizeDirTryOrder, // followedBy: branch( @@ -72,11 +77,31 @@ export class DungeonBuilder { console.log(drawMapArrangeQueue(queue, 16, false, undefined, false, true)) if (!mapsArrange) throw new Error('res null') - const mapsConstruct = constructMapsFromMapsArrange(mapsArrange, { - id: 'myarea', + const areaInfo: AreaInfo = { + id: `dnggen-${dungeonId}-myarea`, title: 'My area', description: '8th ring of hell', - }) - console.log(mapsConstruct) + pos: { x: 60, y: 60 }, + type: 'DUNGEON', + masterKeyItem: Item.FajroKeyMaster, + keyItem: Item.FajroKey, + boosterItem: 999, + } + const mapsConstruct = constructMapsFromMapsArrange(mapsArrange, areaInfo) + + const paths = new DungeonPaths(dungeonId) + await Promise.all(mapsConstruct.map(map => paths.saveMap(map.constructed))) + + const area = createArea(mapsConstruct, areaInfo, 0, 0, { 0: 'myfloor0' }) + const areaDb = createAreaDbEntry(area, areaInfo) + await paths.saveArea(areaInfo.id, area, areaDb) + + await paths.saveConfig() + + ig.game.teleport( + mapsConstruct[0].constructed.name, + ig.TeleportPosition.createFromJson({ marker: 'entrance_0', level: 0, baseZPos: 0, size: { x: 0, y: 0 } }) + ) + // console.dir(mapsConstruct, { depth: null }) } } diff --git a/src/dungeon/paths.ts b/src/dungeon/paths.ts new file mode 100644 index 0000000..93bcfca --- /dev/null +++ b/src/dungeon/paths.ts @@ -0,0 +1,152 @@ +import { fs } from '../library-providers' +import DngGen from '../plugin' +import { ObjectEntriesT } from '../util/modify-prototypes' +import { RuntimeResources } from '../util/runtime-assets' + +interface DungeonSaveConfig { + paths: Record + areaDbEntries: Record + // sels: Record +} + +export class DungeonPaths { + static paths: Record = {} + + static async getLoaded(dungeonId: string): Promise { + let entry = DungeonPaths.paths[dungeonId] + if (entry) return entry + + entry = new DungeonPaths(dungeonId) + if (!(await entry.loadConfig())) return false + return entry + } + + static registerAutoLoadDungeon() { + let ignore: boolean = false + ig.Game.inject({ + preloadLevel(mapName: string) { + if (ignore) { + ignore = false + return this.parent(mapName) + } + if (!mapName.startsWith('dnggen-')) return this.parent(mapName) + + const id = mapName.split('/')[0].split('-')[1] + DungeonPaths.getLoaded(id).then(paths => { + if (paths) { + paths.register() + ignore = true + ig.game.preloadLevel(mapName) + } else { + ig.game.preloadLevel('dnggen/limbo') + } + }) + }, + }) + } + + baseDir: string + + config: DungeonSaveConfig + configFile: string + configLoaded: boolean = false + + mapsDirGame: string = 'data/maps' + mapsDir: string + + areaDirGame: string = 'data/areas' + + selIndexes: Record = {} + + constructor(public dungeonId: string) { + DungeonPaths.paths[dungeonId] = this + + this.baseDir = `assets/mod-data/${DngGen.manifset.id}/saves/${dungeonId}` + + this.configFile = `${this.baseDir}/config.json` + + this.mapsDir = `${this.baseDir}/assets/${this.mapsDirGame}` + + this.config = { + paths: {}, + areaDbEntries: {}, + // sels: { + // puzzle: `${this.baseDir}/selPuzzle.json`, + // battle: `${this.baseDir}/selBattle.json`, + // }, + } + } + + async clearDir() { + await clear(this.baseDir) + async function clear(path: string) { + for (const file of await fs.promises.readdir(path)) { + const filePath = `${path}/${file}` + + if ((await fs.promises.lstat(filePath)).isDirectory()) { + await clear(filePath) + await fs.promises.rmdir(filePath) + } else { + await fs.promises.unlink(filePath) + } + } + } + } + + async saveMap(map: sc.MapModel.Map): Promise { + await fs.promises.mkdir(`${this.mapsDir}/${map.name.split('/')[0]}`, { recursive: true }) + + const path = `${this.mapsDir}/${map.name}.json` + const gamePath = `${this.mapsDirGame}/${map.name}.json` + + this.config.paths[gamePath] = path + + await fs.promises.writeFile(path, JSON.stringify(map)) + } + + async saveArea(areaId: string, area: sc.AreaLoadable.SDCustom.Data, dbEntry: sc.MapModel.Area) { + const fileName = `${areaId}.json` + const areaFileGame = `${this.areaDirGame}/${fileName}` + const areaDir = `${this.baseDir}/assets/${this.areaDirGame}` + const areaFile = `${areaDir}/${fileName}` + + await fs.promises.mkdir(areaDir, { recursive: true }) + + this.config.areaDbEntries[areaId] = dbEntry + + this.config.paths[areaFileGame] = areaFile + fs.promises.writeFile(areaFile, JSON.stringify(area)) + } + + async saveConfig() { + await fs.promises.writeFile(this.configFile, JSON.stringify(this.config)) + } + + async loadConfig(): Promise { + if (this.config) return true + + function exists(path: string): Promise { + return new Promise(resolve => { + fs.promises + .stat(path) + .then(() => resolve(true)) + .catch(_err => resolve(false)) + }) + } + if (!(await exists(this.configFile))) return false + const data = await fs.promises.readFile(this.configFile, 'utf8') + this.config = JSON.parse(data) + return true + } + + async register() { + for (const [k, v] of ObjectEntriesT(this.config.paths)) { + RuntimeResources.add(k, v) + } + RuntimeResources.reload() + + Object.entries(this.config.areaDbEntries).forEach(e => { + ig.database.data.areas[e[0]] = e[1] + }) + } +} diff --git a/src/library-providers.ts b/src/library-providers.ts new file mode 100644 index 0000000..5d2b753 --- /dev/null +++ b/src/library-providers.ts @@ -0,0 +1,9 @@ +export let fs: typeof import('fs') + +export async function initLibraries() { + if ('window' in global) { + fs = (0, eval)("require('fs')") + } else { + fs = await import('fs') + } +} diff --git a/src/map-arrange/map-arrange.ts b/src/map-arrange/map-arrange.ts index 9b2dba2..26214c3 100644 --- a/src/map-arrange/map-arrange.ts +++ b/src/map-arrange/map-arrange.ts @@ -15,7 +15,7 @@ export interface TprArrange extends TprArrange3d { export interface MapArrange { type: MapPicker.ConfigTypes id: Id - rects: Rect[] + rects: RoomArrange[] floor?: number /* 0 by default */ entranceTprs: TprArrange3d[] @@ -32,7 +32,7 @@ export function copyMapArrange(map: MapArrangeData): MapArrange { return { type: map.type!, id: map.id!, - rects: (map.rects ?? [])?.map(Rect.copy), + rects: (map.rects ?? [])?.map(a => ({ ...a })), floor: map.floor, entranceTprs: (map.entranceTprs ?? []).map(a => ({ ...a })), @@ -54,11 +54,13 @@ export function offsetMapArrange(map: MapArrange, vec: Vec2) { export type MapArrangeData = Partial -export interface RoomArrange extends Rect {} +export interface RoomArrange extends Rect { + walls: Record +} export function doesMapArrangeFit( accesor: BuildQueueAccesor, - mapToFit: Pick, + mapToFit: { rects: Rect[] } & Pick, id: Id ): boolean { for (let i = id - 1; i >= 0; i--) { diff --git a/src/map-construct/map-construct.ts b/src/map-construct/map-construct.ts index b39ebcc..f58dfc2 100644 --- a/src/map-construct/map-construct.ts +++ b/src/map-construct/map-construct.ts @@ -2,15 +2,22 @@ import { copyMapArrange, MapArrange } from '../map-arrange/map-arrange' import { MapPicker } from '../map-arrange/map-picker/configurable' import { Id } from '../build-queue/build-queue' import { assert } from '../util/util' +import { Item } from '../util/items' export interface MapConstruct extends MapArrange { constructed: sc.MapModel.Map + title: string } export interface AreaInfo { id: string title: string description: string + pos: Vec2 + boosterItem: Item + keyItem: Item + masterKeyItem: Item + type: sc.MapModel.Area['areaType'] } export type MapConstructFunc = ( diff --git a/src/map-construct/theme.ts b/src/map-construct/theme.ts index 76898df..e592c02 100644 --- a/src/map-construct/theme.ts +++ b/src/map-construct/theme.ts @@ -17,7 +17,7 @@ export type MapThemeConfig = { lightStep?: number } & ( | { - addShadows?: boolean + addShadows?: false } | { addShadows: true diff --git a/src/maps/simple-branch.ts b/src/maps/simple-branch.ts index ba64c07..c5e98ef 100644 --- a/src/maps/simple-branch.ts +++ b/src/maps/simple-branch.ts @@ -59,7 +59,9 @@ export function simpleMapBranchTunnelArrange({ let tunnelEntrance: RoomArrange { const rect = Rect.centeredRect(tunnelSize, tpr) - tunnelEntrance = { ...rect } + const walls: Record = [true, true, true, true] + walls[exitTpr.dir] = false + tunnelEntrance = { ...rect, walls } map.rects.push(tunnelEntrance) } let room: RoomArrange @@ -68,7 +70,7 @@ export function simpleMapBranchTunnelArrange({ ...Rect.middle(Rect.side(tunnelEntrance, exitTpr.dir)), dir: tpr.dir, }) - room = { ...rect } + room = { ...rect, walls: [true, true, true, true] } map.rects.push(room) } @@ -92,7 +94,7 @@ export function simpleMapBranchTunnelArrange({ const dirs = exitChoices[branch] const createNextBranch = (prevId: number): QueueEntry | null => { const mapOld = accesor.get(id) - const map = { rects: [] as Rect[], restTprs: [] as TprArrange3d[] } + const map = { rects: [] as RoomArrange[], restTprs: [] as TprArrange3d[] } const currentBranch = mapOld.restTprs!.length if (currentBranch == dirs.length) { @@ -115,7 +117,9 @@ export function simpleMapBranchTunnelArrange({ ...Rect.middle(Rect.side(room, dir)), dir: DirU.flip(dir), }) - tunnelExit = { ...rect } + const walls: Record = [true, true, true, true] + walls[dir] = false + tunnelExit = { ...rect, walls } map.rects.push(tunnelExit) } if (!doesMapArrangeFit(accesor, map, id)) return null diff --git a/src/maps/simple-tunnel.ts b/src/maps/simple-tunnel.ts index c087753..96306ca 100644 --- a/src/maps/simple-tunnel.ts +++ b/src/maps/simple-tunnel.ts @@ -72,7 +72,9 @@ export function simpleMapTunnelArrange({ let tunnelEntrance: RoomArrange { const rect = Rect.centeredRect(tunnelSize, tpr) - tunnelEntrance = { ...rect } + const walls: Record = [true, true, true, true] + walls[exitTpr.dir] = false + tunnelEntrance = { ...rect, walls } map.rects.push(tunnelEntrance) } let room: RoomArrange @@ -81,7 +83,7 @@ export function simpleMapTunnelArrange({ ...Rect.middle(Rect.side(tunnelEntrance, exitTpr.dir)), dir: tpr.dir, }) - room = { ...rect } + room = { ...rect, walls: [true, true, true, true] } map.rects.push(room) } @@ -102,7 +104,7 @@ export function simpleMapTunnelArrange({ finishedEntry: branchDone, nextQueueEntryGenerator: (_, branch, accesor) => { - const map = { rects: [] as Rect[], restTprs: [] as TprArrange3d[] } + const map = { rects: [] as RoomArrange[], restTprs: [] as TprArrange3d[] } const dir = dirChoices[branch] let tunnelExit: RoomArrange @@ -111,7 +113,9 @@ export function simpleMapTunnelArrange({ ...Rect.middle(Rect.side(room, dir)), dir: DirU.flip(dir), }) - tunnelExit = { ...rect } + const walls: Record = [true, true, true, true] + walls[dir] = false + tunnelExit = { ...rect, walls } map.rects.push(tunnelExit) } if (!doesMapArrangeFit(accesor, map, id)) return null diff --git a/src/maps/simple.ts b/src/maps/simple.ts index 5660b47..97940fe 100644 --- a/src/maps/simple.ts +++ b/src/maps/simple.ts @@ -7,12 +7,14 @@ import { RoomArrange, doesMapArrangeFit, offsetMapArrange, + TprArrange3d, } from '../map-arrange/map-arrange' import { MapPicker, registerMapPickerNodeConfig } from '../map-arrange/map-picker/configurable' import { Dir, DirU, Rect } from '../util/geometry' -import { shuffleArray } from '../util/util' +import { Array2d, assert, shuffleArray } from '../util/util' import { registerMapConstructor } from '../map-construct/map-construct' -import { MapTheme } from '../map-construct/theme' +import { MapTheme, MapThemeConfig } from '../map-construct/theme' +import { Coll } from '../util/map' declare global { export namespace MapPickerNodeConfigs { @@ -72,7 +74,7 @@ export function simpleMapArrange({ let room: RoomArrange { const rect = Rect.centeredRect(size, tpr) - room = { ...rect } + room = { ...rect, walls: [true, true, true, true] } map.rects.push(room) } @@ -124,37 +126,417 @@ export function simpleMapArrange({ } registerMapConstructor('Simple', (map, areaInfo, pathResolver, _mapsArranged, _mapsConstructed) => { - const bounds = Rect.boundsOfArr(map.rects) - const offset = Vec2.mulC(bounds, -1) + const boundsEntity = Rect.boundsOfArr(map.rects) + Rect.extend(boundsEntity, 8 * 16) + const offset = Vec2.mulC(boundsEntity, -1) offsetMapArrange(map, offset) + const bounds = Rect.div(Rect.copy(boundsEntity), 16) + const theme = MapTheme.default + const mapSize: Vec2 = Rect.toTwoVecSize(bounds)[1] - const c: sc.MapModel.Map = { + const mic: MapInConstruction = { name: pathResolver(map.id), - mapWidth: bounds.width, - mapHeight: bounds.height, - levels: [{ height: 0 }], + mapWidth: mapSize.x, + mapHeight: mapSize.y, masterLevel: 0, attributes: theme.getMapAttributes(areaInfo.id), screen: { x: 0, y: 0 }, entities: [], - layer: [], + + ...getEmptyLayers(mapSize, 3, theme.config), } - for (const tpr of [...map.entranceTprs, ...map.restTprs]) { - const entity = { - type: 'DOOR', - x: tpr.x, - y: tpr.y, - level: 0, - settings: {}, + function pushTprEntity(tpr: TprArrange3d, isEntrance: boolean, index: number) { + const name = getTprName(isEntrance, index) + const dir = DirU.flip(tpr.dir as Dir) + if (tpr.destId == -1) { + return mic.entities.push({ + type: 'Marker', + x: tpr.x, + y: tpr.y, + level: 0, + settings: { name, dir: DirU.toString(dir) }, + }) } - c.entities.push(entity) + + let x = tpr.x + let y = tpr.y + + if (tpr.dir != Dir.SOUTH) y -= 16 + if (tpr.dir != Dir.EAST) x -= 16 + + mic.entities.push({ + type: 'Door', + x, + y, + level: 0, + settings: { + name, + map: pathResolver(tpr.destId), + marker: getTprName(!isEntrance, tpr.destIndex ?? 0), + dir: DirU.toString(dir), + }, + }) + } + + map.entranceTprs.forEach((tpr, i) => pushTprEntity(tpr, true, i)) + map.restTprs.forEach((tpr, i) => pushTprEntity(tpr, false, i)) + + for (const room of map.rects) { + placeRoom(room, mic, theme.config, true) } + const constructed: sc.MapModel.Map = Object.assign(mic, { layers: undefined }) + return { ...map, - constructed: c, + constructed, + title: `map ${constructed.name}`, } }) + +function getTprName(isEntrance: boolean, index: number): string { + return `${isEntrance ? 'entrance' : 'rest'}_${index}` +} + +interface MapConstructionLayers { + background: number[][][] + shadow: number[][] + light: number[][] + coll: number[][][] + nav: number[][][] +} +interface MapInConstruction extends sc.MapModel.Map { + layers: MapConstructionLayers +} + +function emptyLayer( + size: Vec2, + fill: number, + rest: Pick +): sc.MapModel.MapLayer { + return { + visible: 1, + repeat: false, + distance: 1, + yDistance: 0, + tilesize: 16, + moveSpeed: { x: 0, y: 0 }, + lighter: false, + id: -10, + + data: Array2d.empty(size, fill), + width: size.x, + height: size.y, + ...rest, + } +} + +function getEmptyLayers( + size: Vec2, + levelCount: number, + theme: MapThemeConfig +): { layers: MapConstructionLayers; levels: sc.MapModel.Map['levels']; layer: sc.MapModel.MapLayer[] } { + const layer: sc.MapModel.MapLayer[] = [] + const levels: sc.MapModel.Map['levels'] = [] + + let background: number[][][] = [], + shadow: number[][] = [], + coll: number[][][] = [], + nav: number[][][] = [] + + for (let level = 0; level < levelCount; level++) { + levels.push({ height: level * 16 * 2 }) + + const backgroundLayer = emptyLayer(size, level == 0 ? theme.blackTile : 0, { + type: 'Background', + name: 'NEW_BACKGROUND', + tilesetName: theme.tileset, + level, + }) + + background.push(backgroundLayer.data) + layer.push(backgroundLayer) + + if (level == 0 && theme.addShadows) { + const shadowLayer = emptyLayer(size, 0, { + name: 'NEW_SHADOW', + type: 'Background', + tilesetName: theme.shadowTileset, + level, + }) + shadow = shadowLayer.data + layer.push(shadowLayer) + } + const collisionLayer = emptyLayer(size, Coll.Wall, { + name: 'NEW_COLLISION', + type: 'Collision', + tilesetName: 'media/map/collisiontiles-16x16.png', + level, + }) + coll.push(collisionLayer.data) + layer.push(collisionLayer) + + const navigationLayer = emptyLayer(size, 0, { + name: 'NEW_NAVIGATION', + type: 'Navigation', + tilesetName: 'media/map/pathmap-tiles.png', + level, + }) + nav.push(navigationLayer.data) + layer.push(navigationLayer) + } + + const lightLayer = emptyLayer(size, 0, { + name: 'NEW_LIGHT', + type: 'Light', + tilesetName: 'media/map/lightmap-tiles.png', + level: 'last', + }) + const light: number[][] = lightLayer.data + layer.push(lightLayer) + + return { + layers: { + background, + shadow, + light, + coll, + nav, + }, + levels, + layer, + } +} + +function placeRoom(room: RoomArrange, map: MapInConstruction, tc: MapThemeConfig, addNavMap: boolean) { + const rect = Rect.div(Rect.copy(room), 16) + const { x: rx, y: ry } = rect + const { x: rx2, y: ry2 } = Rect.x2y2(rect) + const background = map.layers.background[0] + const shadow = map.layers.shadow + const light = map.layers.light + const colls = map.layers.coll + const navs = map.layers.nav + + // draw floor + for (let y = ry; y < ry2; y++) { + for (let x = rx; x < rx2; x++) { + background[y][x] = tc.floorTile + if (tc.addShadows) { + shadow![y][x] = 0 + } + for (const coll of colls) { + coll[y][x] = 0 + } + light[y][x] = 0 + if (addNavMap) { + for (const nav of navs) { + nav[y][x] = 1 + } + } + } + } + + if (room.walls[Dir.NORTH]) { + for (let x = rx; x < rx2; x++) { + placeWall(map, tc, { x, y: ry }, Dir.NORTH) + } + } else if (tc.addShadows) { + Array2d.pasteInto(shadow, tc.edgeShadowBottomLeft!, rx, ry - 2) + Array2d.pasteInto(shadow, tc.edgeShadowBottomRight!, rx2 - 2, ry - 2) + for (let x = rx + 2; x < rx2 - 2; x++) { + for (let y = ry - 2; y < ry; y++) { + shadow![y][x] = 0 + } + } + } + + if (room.walls[Dir.EAST]) { + for (let y = ry; y < ry2; y++) { + placeWall(map, tc, { x: rx2, y }, Dir.EAST) + } + } else if (tc.addShadows) { + Array2d.pasteInto(shadow!, tc.edgeShadowTopLeft!, rx2, ry) + Array2d.pasteInto(shadow!, tc.edgeShadowBottomLeft!, rx2, ry2 - 2) + for (let y = ry + 2; y < ry2 - 2; y++) { + for (let x = rx2; x < rx2 + 2; x++) { + shadow![y][x] = 0 + } + } + } + + if (room.walls[Dir.SOUTH]) { + for (let x = rx; x < rx2; x++) { + placeWall(map, tc, { x, y: ry2 }, Dir.SOUTH) + } + } else if (tc.addShadows) { + Array2d.pasteInto(shadow!, tc.edgeShadowTopLeft!, rx, ry2) + Array2d.pasteInto(shadow!, tc.edgeShadowTopRight!, rx2 - 2, ry2) + for (let x = rx + 2; x < rx2 - 2; x++) { + for (let y = ry2; y < ry2 + 2; y++) { + shadow![y][x] = 0 + } + } + } + + if (room.walls[Dir.WEST]) { + for (let y = ry; y < ry2; y++) { + placeWall(map, tc, { x: rx, y }, Dir.WEST) + } + } else if (tc.addShadows) { + Array2d.pasteInto(shadow!, tc.edgeShadowTopRight!, rx - 2, ry) + Array2d.pasteInto(shadow!, tc.edgeShadowBottomRight!, rx - 2, ry2 - 2) + for (let y = ry + 2; y < ry2 - 2; y++) { + for (let x = rx - 2; x < rx; x++) { + shadow![y][x] = 0 + } + } + } + + if (tc.addShadows) { + // fix shadow corners + if (room.walls[Dir.NORTH] && room.walls[Dir.WEST]) { + Array2d.pasteInto(shadow!, tc.cornerShadowTopLeft!, rx, ry) + } + if (room.walls[Dir.NORTH] && room.walls[Dir.EAST]) { + Array2d.pasteInto(shadow!, tc.cornerShadowTopRight!, rx2 - 2, ry) + } + if (room.walls[Dir.SOUTH] && room.walls[Dir.WEST]) { + Array2d.pasteInto(shadow!, tc.cornerShadowBottomLeft!, rx, ry2 - 2) + } + if (room.walls[Dir.SOUTH] && room.walls[Dir.EAST]) { + Array2d.pasteInto(shadow!, tc.cornerShadowBottomRight!, rx2 - 2, ry2 - 2) + } + } + + if (tc.addLight) { + assert(tc.lightStep) + assert(tc.lightTile) + const distFromWall = 5 + const lx1 = rx + distFromWall - 1 + const ly1 = ry + distFromWall - 1 + const lx2 = rx2 - distFromWall + const ly2 = ry2 - distFromWall + + const mx = Math.floor(lx1 + (lx2 - lx1) / 2) + const my = Math.floor(ly1 + (ly2 - ly1) / 2) + light[my][mx] = tc.lightTile + + for (let x = lx1; x <= mx; x += tc.lightStep) { + for (let y = ly1; y <= my; y += tc.lightStep) { + light[y][x] = tc.lightTile + } + for (let y = ly2; y >= my; y -= tc.lightStep) { + light[y][x] = tc.lightTile + } + light[my][x] = tc.lightTile + } + for (let x = lx2; x >= mx; x -= tc.lightStep) { + for (let y = ly1; y <= my; y += tc.lightStep) { + light[y][x] = tc.lightTile + } + for (let y = ly2; y >= my; y -= tc.lightStep) { + light[y][x] = tc.lightTile + } + light[my][x] = tc.lightTile + } + + for (let y = ly1; y <= ly2; y += tc.lightStep) { + light[y][mx] = tc.lightTile + } + for (let y = ly2; y >= my; y -= tc.lightStep) { + light[y][mx] = tc.lightTile + } + } +} + +function placeWall(map: MapInConstruction, tc: MapThemeConfig, pos: Vec2, dir: Dir): void { + const background = map.layers.background[0] + const shadow = map.layers.shadow + const colls = map.layers.coll + + switch (dir) { + case Dir.NORTH: { + for (let i = 0; i < tc.wallUp.length; i++) { + const y = pos.y - i + 1 + if (tc.wallUp[i]) { + background[y][pos.x] = tc.wallUp[i] + } + if (tc.addShadows && tc.wallUpShadow![i]) { + shadow![y][pos.x] = tc.wallUpShadow![i] + } + } + for (let i = map.masterLevel; i < colls.length; i++) { + const ri = i - map.masterLevel + const coll: number[][] = colls[i] + for (let y = pos.y - 3; y <= pos.y; y++) { + coll[y - ri * 2][pos.x] = Coll.None + } + let y: number = pos.y - ri * 2 - 1 + coll[y][pos.x] = Coll.Wall + } + break + } + case Dir.EAST: { + for (let i = 0; i < tc.wallRight.length; i++) { + const x = pos.x - tc.wallRight.length + i + 1 + if (tc.wallRight[i]) { + if (!background[pos.y][x]) { + background[pos.y][x] = tc.wallRight[i] + } + + for (const coll of colls) { + coll[pos.y][x] = Coll.Wall + coll[pos.y][x + 1] = Coll.Wall + } + } + if (tc.addShadows && tc.wallRightShadow![i]) { + shadow![pos.y][x] = tc.wallRightShadow![i] + } + } + break + } + case Dir.SOUTH: { + for (let i = 0; i < tc.wallDown.length; i++) { + const y = pos.y - tc.wallDown.length + i + 1 + if (tc.wallDown[i]) { + background[y][pos.x] = tc.wallDown[i] + } + if (tc.addShadows && tc.wallDownShadow![i]) { + shadow![y][pos.x] = tc.wallDownShadow![i] + } + } + for (let i = map.masterLevel; i < colls.length; i++) { + const ri = i - map.masterLevel + const coll: number[][] = colls[i] + for (let y = pos.y; y >= pos.y - 3; y--) { + coll[y - ri * 2][pos.x] = Coll.None + } + const y: number = pos.y - ri * 2 + coll[y][pos.x] = Coll.Wall + } + break + } + case Dir.WEST: { + for (let i = 0; i < tc.wallLeft.length; i++) { + const x = pos.x + i - 1 + if (tc.wallLeft[i]) { + if (!background[pos.y][x]) { + background[pos.y][x] = tc.wallLeft[i] + } + for (const coll of colls) { + coll[pos.y][x] = Coll.Wall + coll[pos.y][x - 1] = Coll.Wall + } + } + if (tc.addShadows && tc.wallLeftShadow![i]) { + shadow![pos.y][x] = tc.wallLeftShadow![i] + } + } + break + } + } +} diff --git a/src/plugin.ts b/src/plugin.ts index 69ea5e9..40249c6 100644 --- a/src/plugin.ts +++ b/src/plugin.ts @@ -5,18 +5,7 @@ import ccmod from '../ccmod.json' import { RuntimeResources } from './util/runtime-assets' import { injectGameStarting } from './util/game-start' import * as _ from 'ultimate-crosscode-typedefs' - -declare global { - let dnggen: DngGen - interface Window { - dnggen: DngGen - } - namespace NodeJS { - interface Global { - dnggen: DngGen - } - } -} +import { DungeonPaths } from './dungeon/paths' export default class DngGen { static dir: string @@ -28,12 +17,6 @@ export default class DngGen { DngGen.mod = mod DngGen.mod.isCCL3 = mod.findAllAssets ? true : false DngGen.mod.isCCModPacked = mod.baseDirectory.endsWith('.ccmod/') - - if ('window' in global) { - window.dnggen = this - } else { - global.dnggen = this - } } async prestart() { @@ -41,6 +24,7 @@ export default class DngGen { import('./util/title-screen-button') injectGameStarting() import('./area/custom-area-container') + DungeonPaths.registerAutoLoadDungeon() } async poststart() { diff --git a/src/setup-test.ts b/src/setup-test.ts index dba618e..7518292 100644 --- a/src/setup-test.ts +++ b/src/setup-test.ts @@ -9,6 +9,7 @@ Math.seedrandomSeed = (seed: string) => { import { Mod1 } from 'cc-blitzkrieg/src/types' import DngGen from './plugin' + new DngGen({ baseDirectory: 'assets/mods/cc-shaduungeon', findAllAssets: true as any, diff --git a/src/setup.ts b/src/setup.ts index b369cf3..907be61 100644 --- a/src/setup.ts +++ b/src/setup.ts @@ -1,3 +1,5 @@ +import { initLibraries } from './library-providers' +initLibraries() import './util/modify-prototypes' import './map-arrange/map-picker/configurable' diff --git a/src/util/game-start.ts b/src/util/game-start.ts index c3ebd3a..6ed8c61 100644 --- a/src/util/game-start.ts +++ b/src/util/game-start.ts @@ -14,7 +14,7 @@ export async function startDnggenGame(titleGuiInstance?: sc.TitleScreenButtonGui ig.game.setPaused(false) const builder = new DungeonBuilder() - const res = builder.build('helo') + builder.build('helo') godmode() } @@ -26,7 +26,7 @@ export function injectGameStarting() { }, transitionEnded() { if (justStarted) { - ig.game.teleport('dnggen/limbo') + // ig.game.teleport('dnggen/limbo') // ig.game.teleport(DungeonBuilder.initialMap.path, new ig.TeleportPosition(DungeonBuilder.initialMap.entarenceMarker), 'NEW') justStarted = false } else { diff --git a/src/util/geometry.ts b/src/util/geometry.ts index 0381a2e..7da86d5 100644 --- a/src/util/geometry.ts +++ b/src/util/geometry.ts @@ -96,10 +96,10 @@ export namespace Rect { return rect } export function div(rect: Rect, div: number): Rect { - rect.x *= div - rect.y *= div - rect.width *= div - rect.height *= div + rect.x /= div + rect.y /= div + rect.width /= div + rect.height /= div return rect } export function x2(rect: Rect): number { diff --git a/src/util/runtime-assets.ts b/src/util/runtime-assets.ts index 9cf2cbd..9746e73 100644 --- a/src/util/runtime-assets.ts +++ b/src/util/runtime-assets.ts @@ -10,6 +10,8 @@ export class RuntimeResources { } static reload() { + if (!('window' in global)) return + if (DngGen.mod.isCCL3) { for (const asset of this.everAdded) { ccmod.resources.assetOverridesTable.delete(asset)