UNPKG

arx-level-generator

Version:
478 lines (477 loc) 18.8 kB
import fs from 'node:fs/promises'; import path from 'node:path'; import { AMB } from 'arx-convert'; import { getCellCoords, MAP_DEPTH_IN_CELLS, MAP_WIDTH_IN_CELLS } from 'arx-convert/utils'; import { Audio } from './Audio.js'; import { Entities } from './Entities.js'; import { Entity } from './Entity.js'; import { Fog } from './Fog.js'; import { Fogs } from './Fogs.js'; import { HUD } from './HUD.js'; import { LevelLoader } from './LevelLoader.js'; import { Light } from './Light.js'; import { Lights } from './Lights.js'; import { Manifest } from './Manifest.js'; import { generateMetadata } from './MetaData.js'; import { Path } from './Path.js'; import { Paths } from './Paths.js'; import { Player } from './Player.js'; import { Polygon } from './Polygon.js'; import { Polygons } from './Polygons.js'; import { Portal } from './Portal.js'; import { Rotation } from './Rotation.js'; import { Script } from './Script.js'; import { $ } from './Selection.js'; import { Translations } from './Translations.js'; import { UI } from './UI.js'; import { Vector3 } from './Vector3.js'; import { Zone } from './Zone.js'; import { Zones } from './Zones.js'; import { compile } from './compile.js'; import { MapFinalizedError, MapNotFinalizedError } from './errors.js'; import { times, uniq } from './faux-ramda.js'; import { getGeneratorPackageJSON, latin9ToLatin1 } from './helpers.js'; export class ArxMap { polygons = new Polygons(); lights = new Lights(); fogs = new Fogs(); entities = new Entities(); zones = new Zones(); paths = new Paths(); player = new Player(); portals = []; // TODO: create Portals class config = { isFinalized: false, offset: new Vector3(0, 0, 0), }; hud = new HUD(); ui = new UI(); i18n = new Translations(); todo = { uniqueHeaders: [], cells: times(() => ({}), MAP_DEPTH_IN_CELLS * MAP_WIDTH_IN_CELLS), anchors: [], rooms: [ { portals: [], polygons: [] }, { portals: [], polygons: [] }, ], roomDistances: [ { distance: -1, startPosition: { x: 0, y: 0, z: 0 }, endPosition: { x: 1, y: 0, z: 0 }, }, { distance: -1, startPosition: { x: 0, y: 0, z: 0 }, endPosition: { x: 0, y: 1, z: 0 }, }, { distance: -1, startPosition: { x: 0.984375, y: 0.984375, z: 0 }, endPosition: { x: 0, y: 0, z: 0 }, }, { distance: -1, startPosition: { x: 0, y: 0, z: 0 }, endPosition: { x: 0, y: 0, z: 0 }, }, ], }; constructor(dlf, fts, llf, areNormalsCalculated = false) { if (typeof dlf === 'undefined' || typeof fts === 'undefined' || typeof llf === 'undefined') { return; } this.player.orientation = Rotation.fromArxRotation(dlf.header.angleEdit); this.player.position = Vector3.fromArxVector3(dlf.header.posEdit); dlf.interactiveObjects.forEach((entity) => { this.entities.push(Entity.fromArxInteractiveObject(entity)); }); dlf.fogs.forEach((fog) => { this.fogs.push(Fog.fromArxFog(fog)); }); dlf.zones.forEach((zone) => { this.zones.push(Zone.fromArxZone(zone)); }); dlf.paths.forEach((path) => { this.paths.push(Path.fromArxPath(path)); }); fts.polygons.forEach((polygon) => { this.polygons.push(Polygon.fromArxPolygon(polygon, llf.colors, fts.textureContainers, areNormalsCalculated)); }); this.portals = fts.portals.map(Portal.fromArxPortal); llf.lights.forEach((light) => { this.lights.push(Light.fromArxLight(light)); }); // TODO: deal with these stuff later this.todo.uniqueHeaders = fts.uniqueHeaders; this.config.offset = Vector3.fromArxVector3(fts.sceneHeader.mScenePosition); this.todo.cells = fts.cells; this.todo.anchors = fts.anchors; this.todo.rooms = fts.rooms; this.todo.roomDistances = fts.roomDistances; } async toArxData(settings) { const now = Math.floor(Date.now() / 1000); const generatorId = await ArxMap.getGeneratorId(); const dlf = { header: { lastUser: generatorId, time: now, posEdit: this.player.position.toArxVector3(), angleEdit: this.player.orientation.toArxRotation(), numberOfBackgroundPolygons: this.polygons.length, }, scene: { levelIdx: settings.levelIdx, }, ...this.fogs.toArxData(), ...this.paths.toArxData(), ...this.zones.toArxData(), ...this.entities.toArxData(), }; const fts = { header: { levelIdx: settings.levelIdx, }, uniqueHeaders: this.todo.uniqueHeaders, sceneHeader: { mScenePosition: this.config.offset.toArxVector3(), }, cells: this.todo.cells, anchors: this.todo.anchors, portals: this.portals.map((portal) => portal.toArxPortal()), rooms: this.todo.rooms, roomDistances: this.todo.roomDistances, ...(await this.polygons.toArxData()), }; const llf = { header: { lastUser: generatorId, time: now, numberOfBackgroundPolygons: this.polygons.length, }, colors: this.polygons.getVertexColors(), ...this.lights.toArxData(), }; return { dlf, fts, llf, }; } /** * Loads one of the levels found in the original game * * Requires the pkware-test-files repo * @see https://github.com/meszaros-lajos-gyorgy/pkware-test-files */ static async fromOriginalLevel(levelIdx, settings) { const loader = new LevelLoader(levelIdx, settings); const dlf = await loader.readDlf(); const fts = await loader.readFts(); const llf = await loader.readLlf(); return new ArxMap(dlf, fts, llf, true); } static fromThreeJsMesh(threeJsObj, meshImportProps) { const map = new ArxMap(); map.polygons.addThreeJsMesh(threeJsObj, meshImportProps); return map; } static async getGeneratorId() { const generator = await getGeneratorPackageJSON(); return `${generator.name} - v.${generator.version}`; } finalize() { if (this.config.isFinalized) { throw new MapFinalizedError(); } const removedPolygons = $(this.polygons).clearSelection().selectOutOfBounds().delete(); if (removedPolygons.length > 0) { console.warn(`[warning] ArxMap: Removed ${removedPolygons.length} polygons what are outside the 0..16000 boundary on the X or Z axis`); } this.polygons.forEach((polygon) => { polygon.calculateNormals(); polygon.calculateArea(); }); this.calculateRoomData(); this.config.isFinalized = true; } removePortals() { if (this.config.isFinalized) { throw new MapFinalizedError(); } this.portals = []; this.todo.rooms.forEach((room) => { room.portals = []; }); this.todo.roomDistances = [ { distance: -1, startPosition: { x: 0, y: 0, z: 0 }, endPosition: { x: 1, y: 0, z: 0 }, }, { distance: -1, startPosition: { x: 0, y: 0, z: 0 }, endPosition: { x: 0, y: 1, z: 0 }, }, { distance: -1, startPosition: { x: 0.984375, y: 0.984375, z: 0 }, endPosition: { x: 0, y: 0, z: 0 }, }, { distance: -1, startPosition: { x: 0, y: 0, z: 0 }, endPosition: { x: 0, y: 0, z: 0 }, }, ]; this.movePolygonsToSameRoom(); } movePolygonsToSameRoom() { $(this.polygons).selectAll().moveToRoom1(); this.todo.rooms = this.todo.rooms.slice(0, 2); this.todo.roomDistances = [ { distance: -1, startPosition: { x: 0, y: 0, z: 0 }, endPosition: { x: 1, y: 0, z: 0 }, }, { distance: -1, startPosition: { x: 0, y: 0, z: 0 }, endPosition: { x: 0, y: 1, z: 0 }, }, { distance: -1, startPosition: { x: 0.984375, y: 0.984375, z: 0 }, endPosition: { x: 0, y: 0, z: 0 }, }, { distance: -1, startPosition: { x: 0, y: 0, z: 0 }, endPosition: { x: 0, y: 0, z: 0 }, }, ]; } calculateRoomData = () => { this.todo.rooms.forEach((room) => { room.polygons = []; }); const polygonsPerCellCounter = {}; this.polygons.forEach((polygon) => { if (polygon.room < 1) { return; } const vertices = polygon.vertices.map((vertex) => vertex.toArxVertex()); const [cellX, cellY] = getCellCoords(vertices); const key = `${cellX}|${cellY}`; if (key in polygonsPerCellCounter) { polygonsPerCellCounter[key] += 1; } else { polygonsPerCellCounter[key] = 0; } this.todo.rooms[polygon.room].polygons.push({ cellX, cellY, polygonIdx: polygonsPerCellCounter[key] }); }); }; async saveToDisk(settings, exportJsonFiles = false, prettify = false) { if (!this.config.isFinalized) { throw new MapNotFinalizedError(); } console.log(`[info] ArxMap: seed = "${settings.seed}"`); console.log(`[info] ArxMap: output directory = "${settings.outputDir}"`); await Manifest.uninstall(settings); const meta = await generateMetadata(settings); // ------------------------ let textures = await this.polygons.exportTextures(settings); const hudElements = this.hud.exportSourcesAndTargets(settings); const uiElements = this.ui.exportSourcesAndTargets(settings); const ambienceTracks = this.zones.reduce((acc, zone) => { if (!zone.hasAmbience() || zone.ambience.isNative) { return acc; } zone.ambience.exportSourcesAndTargets(settings).forEach(([source, target]) => { acc[target] = source; }); return acc; }, {}); const customAmbiences = this.zones.reduce((acc, zone) => { if (!zone.hasAmbience() || zone.ambience.isNative) { return acc; } return { ...acc, ...zone.ambience.toArxData(settings), }; }, {}); const scripts = {}; const models = {}; const otherDependencies = {}; for (const entity of this.entities) { if (entity.hasScript()) { scripts[entity.exportScriptTarget(settings)] = entity.script.toArxData(); const texturesToExport = await entity.script.exportTextures(settings); for (const target in texturesToExport) { textures[target] = texturesToExport[target]; } } if (entity.hasInventoryIcon()) { const texturesToExport = await entity.exportInventoryIcon(settings); for (const target in texturesToExport) { textures[target] = texturesToExport[target]; } } if (entity.hasModel()) { const modelToExport = await entity.model.exportSourceAndTarget(settings, entity.src, exportJsonFiles, prettify); for (const target in modelToExport) { models[target] = modelToExport[target]; } } const dependenciesToExport = await entity.exportOtherDependencies(settings); for (const target in dependenciesToExport) { otherDependencies[target] = dependenciesToExport[target]; } } const sounds = Audio.exportReplacements(settings); // removing root entities while also making sure the entities land in an Entities object and not in an array this.entities = this.entities .filter((entity) => { return !entity.hasScript() || !entity.script.isRoot; }) .reduce((acc, entity) => { acc.push(entity); return acc; }, new Entities()); if (this.player.hasScript()) { scripts[this.player.exportTarget(settings)] = this.player.script.toArxData(); textures = { ...textures, ...(await this.player.script.exportTextures(settings)), }; } const translations = this.i18n.exportSourcesAndTargets(settings); const files = { dlf: path.resolve(settings.outputDir, `graph/levels/level${settings.levelIdx}/level${settings.levelIdx}.dlf.json`), fts: path.resolve(settings.outputDir, `game/graph/levels/level${settings.levelIdx}/fast.fts.json`), llf: path.resolve(settings.outputDir, `graph/levels/level${settings.levelIdx}/level${settings.levelIdx}.llf.json`), }; const pathsOfTheFiles = [ ...Object.keys(textures), ...Object.keys(models), ...Object.keys(otherDependencies), ...Object.keys(sounds), ...Object.keys(hudElements), ...Object.keys(uiElements), ...Object.keys(ambienceTracks), ...Object.keys(customAmbiences).map((filename) => filename.replace(/\.json$/, '')), ...Object.keys(scripts), ...Object.keys(translations), ...Object.values(files).map((filename) => filename.replace(/\.json$/, '')), ]; const dirnames = uniq(pathsOfTheFiles.map(path.dirname.bind(path))); for (const dirname of dirnames) { await fs.mkdir(dirname, { recursive: true }); } // ------------------------ const filesToCopy = [ ...Object.entries(textures), ...Object.entries(hudElements), ...Object.entries(uiElements), ...Object.entries(ambienceTracks), ...Object.entries(models), ...Object.entries(otherDependencies), ...Object.entries(sounds), ]; for (const [target, source] of filesToCopy) { await fs.copyFile(source, target); } // ------------------------ for (const [target, script] of Object.entries(scripts)) { const latin1Script = latin9ToLatin1(script.replace(/\n/g, Script.EOL)); await fs.writeFile(target, latin1Script, 'latin1'); } const generatorId = await ArxMap.getGeneratorId(); for (const [filename, translation] of Object.entries(translations)) { await fs.writeFile(filename, `// ${meta.name} v.${meta.version} - ${generatorId} ${translation}`, 'utf8'); } // ------------------------ const { dlf, fts, llf } = await this.toArxData(settings); if (exportJsonFiles) { const stringifiedDlf = prettify ? JSON.stringify(dlf, null, 2) : JSON.stringify(dlf); await fs.writeFile(files.dlf, stringifiedDlf); const stringifiedFts = prettify ? JSON.stringify(fts, null, 2) : JSON.stringify(fts); await fs.writeFile(files.fts, stringifiedFts); const stringifiedLlf = prettify ? JSON.stringify(llf, null, 2) : JSON.stringify(llf); await fs.writeFile(files.llf, stringifiedLlf); for (const [target, amb] of Object.entries(customAmbiences)) { const stringifiedAmb = prettify ? JSON.stringify(amb, null, 2) : JSON.stringify(amb); await fs.writeFile(target, stringifiedAmb); } pathsOfTheFiles.push(...Object.keys(customAmbiences)); pathsOfTheFiles.push(...Object.values(files)); } for (const [target, amb] of Object.entries(customAmbiences)) { const compiledAmb = AMB.save(amb); await fs.writeFile(target.replace(/\.json$/, ''), compiledAmb); } await compile(settings, { dlf, fts, llf }); // ------------------------ await Manifest.write(settings, pathsOfTheFiles); } adjustOffsetTo(map) { const offsetDifference = map.config.offset.clone().sub(this.config.offset); $(this.polygons).selectAll().move(offsetDifference); } move(offset) { if (this.config.isFinalized) { throw new MapFinalizedError(); } $(this.polygons).selectAll().move(offset); $(this.entities).selectAll().move(offset); $(this.lights).selectAll().move(offset); $(this.fogs).selectAll().move(offset); $(this.paths).selectAll().move(offset); $(this.zones).selectAll().move(offset); // anchors this.todo.anchors.forEach((anchor) => { anchor.data.pos.x += offset.x; anchor.data.pos.y += offset.y; anchor.data.pos.z += offset.z; }); } add(map, alignPolygons = false) { if (this.config.isFinalized) { throw new MapFinalizedError(); } if (alignPolygons) { map.adjustOffsetTo(this); } map.polygons.forEach((polygon) => { this.polygons.push(polygon); }); map.lights.forEach((light) => { this.lights.push(light); }); map.fogs.forEach((fog) => { this.fogs.push(fog); }); map.entities.forEach((entity) => { this.entities.push(entity); }); map.paths.forEach((path) => { this.paths.push(path); }); map.zones.forEach((zone) => { this.zones.push(zone); }); // map.anchors.forEach((anchor) => { // this.anchors.push(anchor) // }) // TODO: adjust fts anchor linked anchor indices // TODO: adjust fts polygon texture container ids } } //# sourceMappingURL=ArxMap.js.map