diff --git a/app/handlers/map.ts b/app/handlers/map.ts index 6bc390a..7ab7134 100644 --- a/app/handlers/map.ts +++ b/app/handlers/map.ts @@ -1,9 +1,103 @@ -export function ensureMap(name: string, mapName: string) {} +import * as childProcess from 'child_process'; +import * as fs from 'fs-extra'; +import { baseUrl } from '../helpers'; -export function newMap(name: string, creator: string) {} +export const fixTiledMapPaths = (map: any) => { + map.tilesets.forEach((tileset: any) => { + tileset.image = `../../__assets/spritesheets/${tileset.name.toLowerCase()}.png`; + }); +}; -export function renameMap(oldName: string, newName: string) {} +export function ensureMap(mapName: string, mapData: any) { + const path = `${baseUrl}/resources/maps/src/content/maps/custom/${mapName}.json`; + fs.writeFileSync(path, JSON.stringify(mapData)); +} -export function editMap(name: string) {} +export function newMap(mapName: string, mapAuthor: string) { + const fileName = mapName.replace(/[^a-zA-Z-]/g, ''); + const templatePath = `${baseUrl}/resources/maps/src/content/maps/custom/Template.json`; + const path = `${baseUrl}/resources/maps/src/content/maps/custom/${fileName}.json`; -export function editMapSpawnerNames(oldName: string, newName: string) {} + if (!fs.existsSync(templatePath)) { + throw new Error('Template is gone.'); + } + + if (fs.existsSync(path)) { + throw new Error('Map already exists'); + } + + const json = fs.readJSONSync(templatePath); + json.properties.creator = mapAuthor; + json.propertytypes.creator = 'string'; + + fs.writeJSONSync(path, json); + + editMap(fileName); + + return json; +} + +export function copyMap(mapName: string) { + const oldPath = `${baseUrl}/resources/maps/src/content/maps/custom/${mapName}.json`; + const newPath = `${baseUrl}/resources/maps/src/content/maps/custom/${mapName} (copy).json`; + + if (fs.existsSync(newPath)) { + throw new Error('A map by that name already exists.'); + } + + fs.copySync(oldPath, newPath); +} + +export function renameMap(oldName: string, newName: string) { + const oldPath = `${baseUrl}/resources/maps/src/content/maps/custom/${oldName}.json`; + const newPath = `${baseUrl}/resources/maps/src/content/maps/custom/${newName}.json`; + + if (fs.existsSync(newPath)) { + throw new Error('A map by that name already exists.'); + } + + fs.moveSync(oldPath, newPath); +} + +export function removeMap(mapName: string) { + const oldPath = `${baseUrl}/resources/maps/src/content/maps/custom/${mapName}.json`; + const newPath = `${baseUrl}/resources/maps/src/content/maps/custom/${mapName}.bak.json`; + + fs.moveSync(oldPath, newPath, { overwrite: true }); +} + +export function editMap(mapName: string) { + if (!fs.existsSync(`${baseUrl}/resources/Tiled`)) { + throw new Error('Tiled is not installed.'); + } + + const path = `${baseUrl}/resources/maps/src/content/maps/custom/${mapName}.json`; + + const map = fs.readJsonSync(path); + fixTiledMapPaths(map); + fs.writeJsonSync(path, map); + + childProcess.exec(`${baseUrl}/resources/Tiled/tiled.exe "${path}"`); +} + +export function editMapSpawnerNames(oldName: string, newName: string) { + fs.readdirSync(`${baseUrl}/resources/maps/src/content/maps/custom`).forEach( + (file) => { + const path = `${baseUrl}/resources/maps/src/content/maps/custom/${file}`; + const json = fs.readJSONSync(path); + + let didWrite = false; + + json.layers[10].objects.forEach((spawner: any) => { + if (spawner.tag !== oldName) return; + + spawner.tag = newName; + didWrite = true; + }); + + if (didWrite) { + fs.writeJSONSync(path, json); + } + } + ); +} diff --git a/app/ipc.ts b/app/ipc.ts index 61ea1dd..1a2f510 100644 --- a/app/ipc.ts +++ b/app/ipc.ts @@ -73,8 +73,38 @@ export function setupIPC(sendToUI: SendToUI) { }); ipcMain.on('RENAME_MAP', async (e: any, data: any) => { - handlers.renameMap(data.oldName, data.newName); - sendToUI('renamemap', data); + try { + handlers.renameMap(data.oldName, data.newName); + sendToUI('renamemap', data); + } catch (e) { + sendToUI('notify', { + type: 'error', + text: 'A map by that name already exists.', + }); + } + }); + + ipcMain.on('REMOVE_MAP', async (e: any, data: any) => { + try { + handlers.removeMap(data.mapName); + } catch (e) { + sendToUI('notify', { + type: 'error', + text: 'Could not fully delete map for some reason.', + }); + } + }); + + ipcMain.on('COPY_MAP', async (e: any, data: any) => { + try { + handlers.copyMap(data.mapName); + sendToUI('copymap', data); + } catch (e) { + sendToUI('notify', { + type: 'error', + text: 'A map by that name already exists.', + }); + } }); ipcMain.on('EDIT_MAP', async (e: any, data: any) => { diff --git a/src/app/app.module.ts b/src/app/app.module.ts index 534f726..3383e8d 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -13,6 +13,11 @@ import { HomeModule } from './home/home.module'; import { provideHotToastConfig } from '@ngxpert/hot-toast'; import { SweetAlert2Module } from '@sweetalert2/ngx-sweetalert2'; +import { + NgxFloatUiModule, + NgxFloatUiPlacements, + NgxFloatUiTriggers, +} from 'ngx-float-ui'; import { provideNgxWebstorage, withLocalStorage, @@ -29,6 +34,12 @@ import { AppComponent } from './app.component'; SharedModule, HomeModule, AppRoutingModule, + NgxFloatUiModule.forRoot({ + trigger: NgxFloatUiTriggers.hover, + showDelay: 500, + placement: NgxFloatUiPlacements.TOPEND, + appendTo: 'body', + }), SweetAlert2Module.forRoot({ provideSwal: () => import('sweetalert2/dist/sweetalert2.js'), }), diff --git a/src/app/home/home.module.ts b/src/app/home/home.module.ts index 06dabc8..00231d9 100644 --- a/src/app/home/home.module.ts +++ b/src/app/home/home.module.ts @@ -5,11 +5,7 @@ import { HomeRoutingModule } from './home-routing.module'; import { SweetAlert2Module } from '@sweetalert2/ngx-sweetalert2'; import { AgGridModule } from 'ag-grid-angular'; -import { - NgxFloatUiModule, - NgxFloatUiPlacements, - NgxFloatUiTriggers, -} from 'ngx-float-ui'; +import { NgxFloatUiModule } from 'ngx-float-ui'; import { SharedModule } from '../shared/shared.module'; import { DialogsComponent } from '../tabs/dialogs/dialogs.component'; import { DroptablesComponent } from '../tabs/droptables/droptables.component'; @@ -41,11 +37,7 @@ import { HomeComponent } from './home.component'; HomeRoutingModule, SweetAlert2Module, AgGridModule, - NgxFloatUiModule.forRoot({ - trigger: NgxFloatUiTriggers.hover, - showDelay: 500, - placement: NgxFloatUiPlacements.TOPEND, - }), + NgxFloatUiModule, ], }) export class HomeModule {} diff --git a/src/app/services/electron.service.ts b/src/app/services/electron.service.ts index 14ad37f..bb79cbb 100644 --- a/src/app/services/electron.service.ts +++ b/src/app/services/electron.service.ts @@ -44,7 +44,7 @@ export class ElectronService { }); window.api.receive('newmap', (mapData) => { - this.modService.addMap(mapData); + this.modService.addMap(mapData as { name: string; map: any }); }); window.api.receive('renamemap', (nameData) => { @@ -54,6 +54,10 @@ export class ElectronService { ); }); + window.api.receive('copymap', (nameData) => { + this.modService.copyMap(nameData.mapName as string); + }); + window.api.receive('json', (jsonData) => { this.modService.setJSON(jsonData.name as string, jsonData.data); }); diff --git a/src/app/services/mod.service.ts b/src/app/services/mod.service.ts index 016938c..8cbb8d9 100644 --- a/src/app/services/mod.service.ts +++ b/src/app/services/mod.service.ts @@ -44,6 +44,8 @@ export class ModService { const newModData = this.mod(); this.localStorage.store('mod', newModData); }); + + this.ensureMapsExist(); } // mod functions @@ -93,11 +95,43 @@ export class ModService { } // map functions - public addMap(map: any) { - if (map.name === 'Template') return; + private ensureMapsExist() { + this.mod().maps.forEach((map) => { + window.api.send('ENSURE_MAP', { ...map }); + }); + } + + public importMap(incomingMap: { name: string; map: any }) { + this.addMap(incomingMap); + window.api.send('ENSURE_MAP', { ...incomingMap }); + } + + public addMap(incomingMap: { name: string; map: any }) { + if (incomingMap.name === 'Template') return; const mod = this.mod(); - mod.maps.push(map); + if (!mod.meta.name) mod.meta.name = incomingMap.name; + + const existingMap = mod.maps.findIndex((x) => x.name === incomingMap.name); + if (existingMap !== -1) { + mod.maps.splice(existingMap, 1, incomingMap); + } else { + mod.maps.push(incomingMap); + } + + this.updateMod(mod); + } + + public copyMap(mapName: string) { + const mod = this.mod(); + + const existingMap = mod.maps.find((x) => x.name === mapName); + if (!existingMap) return; + + const newMap = structuredClone(existingMap); + newMap.name = `${mapName} (copy)`; + + mod.maps.push(newMap); this.updateMod(mod); } @@ -111,6 +145,28 @@ export class ModService { mapRef.name = newName; + this.updateMapNameAcrossMod(oldName, newName); + this.updateMod(mod); + } + + private updateMapNameAcrossMod(oldName: string, newName: string) { + const mod = this.mod(); + mod.drops.forEach((droptable) => { + if (droptable.mapName !== oldName) return; + + droptable.mapName = newName; + }); + + this.updateMod(mod); + } + + public removeMap(removeMap: { name: string; map: any }) { + const mod = this.mod(); + const existingMap = mod.maps.findIndex((x) => x.name === removeMap.name); + if (existingMap !== -1) { + mod.maps.splice(existingMap, 1); + } + this.updateMod(mod); } diff --git a/src/app/shared/components/cell-buttons/cell-buttons.component.html b/src/app/shared/components/cell-buttons/cell-buttons.component.html index 65ddde2..323a816 100644 --- a/src/app/shared/components/cell-buttons/cell-buttons.component.html +++ b/src/app/shared/components/cell-buttons/cell-buttons.component.html @@ -1,6 +1,6 @@
@if(params.showCopyButton) { - + + @if(showImport()) { + + } +
+ +} @else { +
+ +
+} diff --git a/src/app/shared/components/editor-view-table/editor-view-table.component.scss b/src/app/shared/components/editor-view-table/editor-view-table.component.scss new file mode 100644 index 0000000..e69de29 diff --git a/src/app/shared/components/editor-view-table/editor-view-table.component.ts b/src/app/shared/components/editor-view-table/editor-view-table.component.ts new file mode 100644 index 0000000..a2526bf --- /dev/null +++ b/src/app/shared/components/editor-view-table/editor-view-table.component.ts @@ -0,0 +1,17 @@ +import { Component, input, output } from '@angular/core'; +import { ColDef } from 'ag-grid-community'; + +@Component({ + selector: 'app-editor-view-table', + templateUrl: './editor-view-table.component.html', + styleUrl: './editor-view-table.component.scss', +}) +export class EditorViewTableComponent { + public tableItems = input([]); + public dataType = input(''); + public tableColumns = input([]); + public showImport = input(false); + + public create = output(); + public import = output(); +} diff --git a/src/app/shared/components/header-buttons/header-buttons.component.html b/src/app/shared/components/header-buttons/header-buttons.component.html index 59248a6..7e6d5b1 100644 --- a/src/app/shared/components/header-buttons/header-buttons.component.html +++ b/src/app/shared/components/header-buttons/header-buttons.component.html @@ -1,6 +1,16 @@
+ @if(params.showImportButton) { + + } + @if(params.showNewButton) { - -
- -} @else { -
- -
-} + } @else { diff --git a/src/app/tabs/items/items.component.ts b/src/app/tabs/items/items.component.ts index d63d4d0..1635ca0 100644 --- a/src/app/tabs/items/items.component.ts +++ b/src/app/tabs/items/items.component.ts @@ -1,11 +1,11 @@ -import { Component, computed, inject, signal } from '@angular/core'; +import { Component, computed } from '@angular/core'; import { ColDef } from 'ag-grid-community'; import { IItemDefinition } from '../../../interfaces'; import { defaultItem } from '../../helpers'; -import { ModService } from '../../services/mod.service'; import { CellButtonsComponent } from '../../shared/components/cell-buttons/cell-buttons.component'; import { CellSpriteComponent } from '../../shared/components/cell-sprite/cell-sprite.component'; +import { EditorBaseTableComponent } from '../../shared/components/editor-base-table/editor-base-table.component'; import { HeaderButtonsComponent } from '../../shared/components/header-buttons/header-buttons.component'; type EditingType = IItemDefinition; @@ -15,17 +15,10 @@ type EditingType = IItemDefinition; templateUrl: './items.component.html', styleUrl: './items.component.scss', }) -export class ItemsComponent { - private modService = inject(ModService); - - public readonly dataType = 'items'; - - public isEditing = signal(false); - public oldData = signal(undefined); - public editingData = signal(defaultItem()); +export class ItemsComponent extends EditorBaseTableComponent { + public defaultData = defaultItem; public tableItems = computed(() => this.modService.mod().items); - public tableColumns: ColDef[] = [ { field: 'sprite', @@ -88,23 +81,8 @@ export class ItemsComponent { }, ]; - public createNew() { - this.isEditing.set(true); - this.editingData.set(defaultItem()); - } - - public editExisting(data: EditingType) { - this.isEditing.set(true); - this.oldData.set(structuredClone(data)); - this.editingData.set(data); - } - - public cancelEditing() { - this.isEditing.set(false); - } - public saveNewData(data: EditingType) { - this.isEditing.set(false); + super.saveNewData(data); const oldItem = this.oldData(); if (oldItem) { @@ -117,6 +95,8 @@ export class ItemsComponent { } public deleteData(data: EditingType) { + super.deleteData(data); + this.modService.removeItem(data); } } diff --git a/src/app/tabs/maps/maps.component.html b/src/app/tabs/maps/maps.component.html index 1879a4a..58368bf 100644 --- a/src/app/tabs/maps/maps.component.html +++ b/src/app/tabs/maps/maps.component.html @@ -1 +1,10 @@ -

maps works!

+ + + + + + + diff --git a/src/app/tabs/maps/maps.component.ts b/src/app/tabs/maps/maps.component.ts index db9b7d7..dee005b 100644 --- a/src/app/tabs/maps/maps.component.ts +++ b/src/app/tabs/maps/maps.component.ts @@ -1,8 +1,192 @@ -import { Component } from '@angular/core'; +import { + Component, + computed, + ElementRef, + inject, + viewChild, +} from '@angular/core'; +import { SwalComponent } from '@sweetalert2/ngx-sweetalert2'; +import { ColDef } from 'ag-grid-community'; +import { ElectronService } from '../../services/electron.service'; +import { NotifyService } from '../../services/notify.service'; +import { CellButtonsComponent } from '../../shared/components/cell-buttons/cell-buttons.component'; +import { EditorBaseTableComponent } from '../../shared/components/editor-base-table/editor-base-table.component'; +import { HeaderButtonsComponent } from '../../shared/components/header-buttons/header-buttons.component'; + +type EditingType = { name: string; map: any }; @Component({ selector: 'app-maps', templateUrl: './maps.component.html', styleUrl: './maps.component.scss', }) -export class MapsComponent {} +export class MapsComponent extends EditorBaseTableComponent { + private notifyService = inject(NotifyService); + private electronService = inject(ElectronService); + + public newSwal = viewChild('newSwal'); + public renameSwal = viewChild('renameSwal'); + public importMapButton = viewChild>('mapUpload'); + + // eslint-disable-next-line @typescript-eslint/no-unsafe-return + public tableItems = computed(() => this.modService.mod().maps); + + public tableColumns: ColDef[] = [ + { + field: 'name', + flex: 1, + cellDataType: 'text', + filter: 'agTextColumnFilter', + sort: 'asc', + }, + { + field: 'map.width', + flex: 1, + cellDataType: 'number', + filter: 'agNumberColumnFilter', + }, + { + field: 'map.height', + flex: 1, + cellDataType: 'number', + filter: 'agNumberColumnFilter', + }, + { + field: 'map.properties.maxLevel', + headerName: 'Max Level', + flex: 1, + cellDataType: 'number', + filter: 'agNumberColumnFilter', + }, + { + field: 'map.properties.maxSkill', + headerName: 'Max Skill', + flex: 1, + cellDataType: 'number', + filter: 'agNumberColumnFilter', + }, + { + field: 'map.properties.region', + headerName: 'Region', + flex: 1, + cellDataType: 'text', + filter: 'agTextColumnFilter', + }, + { + field: 'map.properties.creator', + headerName: 'Creator', + flex: 1, + cellDataType: 'text', + filter: 'agTextColumnFilter', + }, + { + field: '', + width: 250, + sortable: false, + suppressMovable: true, + headerComponent: HeaderButtonsComponent, + headerComponentParams: { + showNewButton: true, + newCallback: () => this.createNewDialog(), + showImportButton: true, + importCallback: () => this.importDialog(), + }, + cellRenderer: CellButtonsComponent, + cellClass: 'no-adjust', + cellRendererParams: { + showCopyButton: true, + copyCallback: (item: EditingType) => this.copyMap(item.name), + showRenameButton: true, + renameCallback: (item: EditingType) => this.renameDialog(item), + showEditButton: true, + editCallback: (item: EditingType) => this.editExisting(item), + showDeleteButton: true, + deleteCallback: (item: EditingType) => this.deleteData(item), + }, + }, + ]; + + public async createNewDialog() { + const res = await this.newSwal()?.fire(); + const newMapName = res?.value as string; + + if (!newMapName) return; + + this.createMap(newMapName); + } + + public importDialog() { + this.importMapButton()?.nativeElement.click(); + } + + private async renameDialog(item: any) { + const oldName = item.name as string; + + const res = await this.renameSwal()?.fire(); + const newMapName = res?.value as string; + + if (!newMapName) return; + + this.renameMap(oldName, newMapName); + } + + public importMaps(event: any) { + const files = event.target.files; + + Array.from(files as ArrayLike).forEach((file: File) => { + if (!file || file.type !== 'application/json') return; + + const reader = new FileReader(); + reader.readAsText(file, 'UTF-8'); + + reader.onload = (evt: any) => { + try { + const map = JSON.parse(evt.target.result as string); + const mapName = file.name.split('.')[0]; + this.import({ map, name: mapName }); + } catch (e: any) { + this.notifyService.error({ + message: `Map upload error: ${e.message}`, + }); + } + }; + + reader.onerror = () => { + this.notifyService.error({ message: `Generic map upload error.` }); + }; + }); + } + + private import(mapData: EditingType) { + this.modService.importMap(mapData); + } + + private createMap(mapName: string) { + this.electronService.send('NEW_MAP', { + name: mapName, + creator: this.modService.mod().meta.author, + }); + } + + private renameMap(oldName: string, newName: string) { + this.electronService.send('RENAME_MAP', { + newName, + oldName, + }); + } + + private copyMap(mapName: string) { + this.electronService.send('COPY_MAP', { + mapName, + }); + } + + public editExisting(data: EditingType) { + this.electronService.send('EDIT_MAP', { name: data.name }); + } + + public deleteData(data: EditingType) { + this.modService.removeMap(data); + this.electronService.send('REMOVE_MAP', { mapName: data.name }); + } +}