diff --git a/.gitignore b/.gitignore index 1e31f171..b163494a 100644 --- a/.gitignore +++ b/.gitignore @@ -44,4 +44,4 @@ yarn-error.log Thumbs.db # scripts -*.bat \ No newline at end of file +*.bat diff --git a/CHANGELOG.md b/CHANGELOG.md index 826a7208..6454d2cd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,11 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). ## [Unreleased] +### Added +- Added overrideable JSON configs, see [#327](https://github.com/CCDirectLink/crosscode-map-editor/pull/327) for more details +- Added most definitions to autotiles.json +- Added new autotile type 4x4 + ## [1.7.1] 2024-07-21 ### Fixed - Fixed Event editor not working diff --git a/backend/src/server.ts b/backend/src/server.ts index cea88214..ff871986 100644 --- a/backend/src/server.ts +++ b/backend/src/server.ts @@ -27,6 +27,7 @@ app.get('/api/allTilesets', async (_, res) => res.json(await api.getAllTilesets( app.get('/api/allMaps', async (req, res) => res.json(await api.getAllMaps(config.pathToCrosscode, req.query['includeVanillaMaps'] == 'true'))); app.get('/api/allFilesInFolder', async (req, res) => res.json(await api.getAllFilesInFolder(config.pathToCrosscode, req.query['folder'] as string, req.query['extension'] as string))); app.get('/api/allMods', async (_, res) => res.json(await api.getAllMods(config.pathToCrosscode))); +app.get('/api/allModMapEditorConfigs', async (_, res) => res.json(await api.getAllModMapEditorConfigs(config.pathToCrosscode))); app.post('/api/get', async (req, res) => { res.json(await api.get(config.pathToCrosscode, req.body.path)); }); diff --git a/common/src/controllers/api.ts b/common/src/controllers/api.ts index f40eaafc..969ce831 100644 --- a/common/src/controllers/api.ts +++ b/common/src/controllers/api.ts @@ -1,6 +1,12 @@ import { fsPromise, pathPromise } from '../require.js'; import { saveFile as save } from './saveFile.js'; +export interface ModEditorConfig { + filename: string; + mod: string; + file: string; +} + const mods: string[] = []; let packagesCache: Record }>; @@ -8,7 +14,7 @@ async function listAllFiles(dir: string, filelist: string[], ending: string, roo if (root === undefined) { root = dir; } - + const files = await tryReadDir(dir); const promises: Promise[] = []; for (const file of files) { @@ -48,7 +54,7 @@ async function searchFile(file: string, dir: string, filelist: string[], ending: .resolve(dir, file) .split(path.normalize(root))[1] .replace(/\\/g, '/'); - + const result = normalized.startsWith('/') ? normalized.substr(1) : normalized; if (!filelist.includes(result)) { filelist.push(result); @@ -84,12 +90,12 @@ function selectMod(name: string, packages: Record path.basename(path.dirname(file)))); - + const promises: Promise<[string, Buffer]>[] = []; for (const file of files) { const folderName = path.basename(path.dirname(file)); @@ -146,7 +152,7 @@ async function readMods(dir: string) { } const rawPackages = await Promise.all(promises); const packages: Record }> = {}; - + for (const [name, pkg] of rawPackages) { try { const parsed = JSON.parse(pkg as unknown as string); @@ -159,13 +165,13 @@ async function readMods(dir: string) { console.error('Invalid json data in package.json of mod: ' + name, err); } } - + const promisesCCMod: Promise<[string, Buffer]>[] = []; for (const file of filesCCMod) { promisesCCMod.push((async (): Promise<[string, Buffer]> => [path.basename(path.dirname(file)), await fs.promises.readFile(file)])()); } const rawCCMods = await Promise.all(promisesCCMod); - + for (const [name, pkg] of rawCCMods) { try { const parsed = JSON.parse(pkg as unknown as string); @@ -178,7 +184,7 @@ async function readMods(dir: string) { console.error('Invalid json data in ccmod.json of mod: ' + name, err); } } - + packagesCache = packages; return packages; } @@ -187,35 +193,35 @@ export async function getAllFiles(dir: string) { const path = await pathPromise; const images = await listAllFiles(path.resolve(dir, 'media/'), [], 'png', path.resolve(dir)); const data = await listAllFiles(path.resolve(dir, 'data/'), [], 'json', path.resolve(dir)); - + for (const mod of mods) { const modDir = path.join(dir, 'mods', mod, 'assets'); await listAllFiles(path.resolve(modDir, 'media/'), images, 'png', path.resolve(modDir)); await listAllFiles(path.resolve(modDir, 'data/'), data, 'json', path.resolve(modDir)); } - + images.sort(); data.sort(); - - return { images, data }; + + return {images, data}; } export async function getAllTilesets(dir: string) { const path = await pathPromise; const result = await listAllFiles(path.resolve(dir, 'media/map/'), [], 'png', path.resolve(dir)); - + for (const mod of mods) { const modDir = path.join(dir, 'mods', mod, 'assets'); await listAllFiles(path.resolve(modDir, 'media/map/'), result, 'png', path.resolve(modDir)); } - + return result.sort(); } export async function getAllMaps(dir: string, includeVanillaMaps: boolean) { const path = await pathPromise; const paths: string[] = []; - + if (mods.length === 0 || includeVanillaMaps) { await listAllFiles(path.resolve(dir, 'data/maps/'), paths, 'json', path.resolve(dir)); } @@ -223,7 +229,7 @@ export async function getAllMaps(dir: string, includeVanillaMaps: boolean) { const modDir = path.join(dir, 'mods', mods[0], 'assets'); await listAllFiles(path.resolve(modDir, 'data/maps/'), paths, 'json', path.resolve(modDir)); } - + return paths .sort() .map(p => p.substring('data/maps/'.length, p.length - '.json'.length)) @@ -233,12 +239,12 @@ export async function getAllMaps(dir: string, includeVanillaMaps: boolean) { export async function getAllFilesInFolder(dir: string, folder: string, extension: string) { const path = await pathPromise; const result = await listAllFiles(path.resolve(dir, folder), [], extension, path.resolve(dir)); - + for (const mod of mods) { const modDir = path.join(dir, 'mods', mod, 'assets'); await listAllFiles(path.resolve(modDir, folder), result, extension, path.resolve(modDir)); } - + return result.sort() .map(p => p.substring(folder.length, p.length - `.${extension}`.length)); } @@ -246,10 +252,34 @@ export async function getAllFilesInFolder(dir: string, folder: string, extension export async function getAllMods(dir: string) { const packages = await readMods(dir); return Object.entries(packages) - .map(([id, pkg]) => ({ id, displayName: pkg.displayName as string })) + .map(([id, pkg]) => ({id, displayName: pkg.displayName as string})) .sort((a, b) => a.displayName.localeCompare(b.displayName)); } +export async function getAllModMapEditorConfigs(dir: string): Promise { + const packages = await readMods(dir); + + const fs = await fsPromise; + const path = await pathPromise; + + const configs: ModEditorConfig[] = []; + + for (const mod of Object.values(packages)) { + const modName = mod.folderName; + const mapEditorPath = path.join(dir, 'mods', modName, 'map-editor'); + const files = await listAllFiles(mapEditorPath, [], 'json', path.resolve(dir)); + for (const filename of files) { + const file = await fs.promises.readFile(path.join(dir, filename), 'utf-8'); + configs.push({ + filename: path.basename(filename), + mod: modName, + file: file + }); + } + } + return configs; +} + export async function selectedMod(dir: string, modName: string) { const packages = await readMods(dir); mods.splice(0); // Clear array @@ -264,7 +294,7 @@ export async function get(dir: string, file: string): Promise { promises.push(getAsync(modFile)); } promises.push(getAsync(path.join(dir, file))); - + const results = await Promise.all(promises); for (const result of results) { if (result) { @@ -282,7 +312,7 @@ export async function resolve(dir: string, file: string): Promise { promises.push(resolveAsync(modFile)); } promises.push(resolveAsync(path.join(dir, file))); - + const results = await Promise.all(promises); for (const result of results) { if (result) { diff --git a/install_all.sh b/install_all.sh index cf718cd1..df02d47b 100644 --- a/install_all.sh +++ b/install_all.sh @@ -1,9 +1,9 @@ #!/bin/bash cd common -npm install +npm ci npm run build cd ../backend -npm install +npm ci cd ../webapp -npm install \ No newline at end of file +npm ci diff --git a/webapp/src/app/app.component.ts b/webapp/src/app/app.component.ts index fc5a9237..1c6fa16c 100644 --- a/webapp/src/app/app.component.ts +++ b/webapp/src/app/app.component.ts @@ -13,16 +13,12 @@ export class AppComponent { constructor( private readonly eventsService: GlobalEventsService, private readonly overlayService: OverlayService, - private readonly router: Router - ) { - this.router.events.subscribe(event => { - console.log(event.constructor.name, event); - }); + ) { } - + @HostListener('window:beforeunload', ['$event']) onUnload($event: any) { - if(this.eventsService.hasUnsavedChanges.getValue()) { + if (this.eventsService.hasUnsavedChanges.getValue()) { $event.returnValue = 'Are you sure you want to discard your changes?'; const dialogRef = this.overlayService.open(ConfirmCloseComponent, { diff --git a/webapp/src/app/components/captions/captions.component.html b/webapp/src/app/components/captions/captions.component.html index 0b8ff06f..d67c55aa 100644 --- a/webapp/src/app/components/captions/captions.component.html +++ b/webapp/src/app/components/captions/captions.component.html @@ -1,8 +1,8 @@ -

{{ version }}

-

+

{{ version }}

+

- - {{el.text}} - + + {{ el.text }} +

diff --git a/webapp/src/app/components/captions/captions.component.scss b/webapp/src/app/components/captions/captions.component.scss index 8c76c985..873e0c79 100644 --- a/webapp/src/app/components/captions/captions.component.scss +++ b/webapp/src/app/components/captions/captions.component.scss @@ -1,12 +1,16 @@ @use '@angular/material' as mat; -.caption { +.bottom-container { bottom: 0; - padding: 5px; - color: white; + z-index: 9999; position: absolute; - pointer-events: none; + //pointer-events: none; +} + +.caption { + padding: 5px; + color: white; } .version { @@ -14,9 +18,14 @@ } .bottom-elements { - background-color: #0005; + display: flex; + gap: 8px; &:empty { display: none; } + + & .caption { + background-color: #0005; + } } diff --git a/webapp/src/app/components/captions/captions.component.ts b/webapp/src/app/components/captions/captions.component.ts index 39f901aa..28787bc5 100644 --- a/webapp/src/app/components/captions/captions.component.ts +++ b/webapp/src/app/components/captions/captions.component.ts @@ -16,10 +16,14 @@ export class CaptionsComponent implements OnInit { version = environment.version; coords: BottomUiElement = {}; selectionSize: BottomUiElement = {}; + autotile: BottomUiElement = { + text: 'Autotile' + }; uiElements: BottomUiElement[] = [ this.coords, - this.selectionSize + this.selectionSize, + this.autotile, ]; ngOnInit(): void { @@ -32,5 +36,9 @@ export class CaptionsComponent implements OnInit { this.selectionSize.text = `${size?.x}x${size?.y}`; this.selectionSize.active = !!size; }); + + Globals.globalEventsService.isAutotile.subscribe(show => { + this.autotile.active = show; + }); } } diff --git a/webapp/src/app/components/dialogs/floating-window/floating-window.component.scss b/webapp/src/app/components/dialogs/floating-window/floating-window.component.scss index 34d97a38..c62dfc8f 100644 --- a/webapp/src/app/components/dialogs/floating-window/floating-window.component.scss +++ b/webapp/src/app/components/dialogs/floating-window/floating-window.component.scss @@ -10,6 +10,7 @@ $toolbarHeight: 32px; .container { min-width: 100px; position: absolute; + z-index: 100; } .ng-draggable { diff --git a/webapp/src/app/components/dialogs/floating-window/tile-selector/tile-selector.scene.ts b/webapp/src/app/components/dialogs/floating-window/tile-selector/tile-selector.scene.ts index 4589a4c2..3de8984b 100644 --- a/webapp/src/app/components/dialogs/floating-window/tile-selector/tile-selector.scene.ts +++ b/webapp/src/app/components/dialogs/floating-window/tile-selector/tile-selector.scene.ts @@ -1,214 +1,123 @@ import * as Phaser from 'phaser'; import { Subscription } from 'rxjs'; - -import { Point } from '../../../../models/cross-code-map'; -import { SelectedTile } from '../../../../models/tile-selector'; import { Globals } from '../../../../services/globals'; import { Helper } from '../../../../services/phaser/helper'; import { MapPan } from '../../../../services/phaser/map-pan'; import { CCMapLayer } from '../../../../services/phaser/tilemap/cc-map-layer'; -import { Vec2 } from '../../../../services/phaser/vec2'; import { customPutTilesAt } from '../../../../services/phaser/tilemap/layer-helper'; +import { BaseTileDrawer } from '../../../../services/phaser/BaseTileDrawer'; export class TileSelectorScene extends Phaser.Scene { - private tileMap?: Phaser.Tilemaps.Tilemap; - private selecting = false; - private rect?: Phaser.GameObjects.Rectangle; - private sub?: Subscription; - - private tilesetRendered = false; + private tileMap!: Phaser.Tilemaps.Tilemap; + private subs: Subscription[] = []; - // TODO: copypaste - same is in tileDrawer, move somewhere else - private selectedTiles: SelectedTile[] = []; - private rightClickStart?: Point; - private rightClickEnd?: Point; + private baseDrawer!: BaseTileDrawer; + private tilesetWidth = 0; - private keyBindings: { event: string, fun: Function }[] = []; - private tilesetSize: Point = {x: 0, y: 0}; + private tilemapLayer!: CCMapLayer; constructor() { super({key: 'main'}); } - create() { + async create() { + + this.baseDrawer = new BaseTileDrawer(this, true); + this.baseDrawer.resetSelectedTiles(); + this.add.existing(this.baseDrawer); + this.cameras.main.setBackgroundColor('#616161'); this.game.canvas.oncontextmenu = function (e) { e.preventDefault(); }; - this.sub = Globals.mapLoaderService.selectedLayer.subscribe(layer => { - if (layer) { - this.drawTileset(layer); + this.subs.push(Globals.mapLoaderService.selectedLayer.subscribe(async layer => { + const success = await this.drawTileset(layer); + if (!success) { + this.tileMap.removeAllLayers(); + await this.baseDrawer.setLayer(); } - }); + })); + + this.subs.push(Globals.phaserEventsService.changeSelectedTiles.subscribe(tiles => { + this.baseDrawer.drawRect(0, 0); + if (tiles.length === 0) { + return; + } + const baseTile = tiles[0]; + + if (baseTile.id === 0) { + return; + } + let width = 0; + let height = 0; + + // If the selection is a continuous rectangle in the tile selector, highlight it + for (const tile of tiles) { + const id = tile.id - tile.offset.x - tile.offset.y * this.tilesetWidth; + if (baseTile.id !== id) { + return; + } + width = Math.max(width, tile.offset.x); + height = Math.max(height, tile.offset.y); + } + + const start = Helper.indexToPoint(baseTile.id, this.tilesetWidth); + this.baseDrawer.drawRect(width + 1, height + 1, start.x * Globals.TILE_SIZE, start.y * Globals.TILE_SIZE); + })); const pan = new MapPan(this, 'mapPan'); this.add.existing(pan); this.tileMap = this.add.tilemap(undefined, Globals.TILE_SIZE, Globals.TILE_SIZE); - - this.keyBindings = []; - const pointerDown = (pointer: Phaser.Input.Pointer) => { - if (pointer.rightButtonDown() || pointer.leftButtonDown()) { - this.onMouseDown(); - } - }; - this.keyBindings.push({event: 'pointerdown', fun: pointerDown}); - - const pointerUp = (pointer: Phaser.Input.Pointer) => { - if (pointer.rightButtonReleased() || pointer.leftButtonReleased()) { - this.onMouseUp(); - } - }; - this.keyBindings.push({event: 'pointerup', fun: pointerUp}); - this.keyBindings.push({event: 'pointerupoutside', fun: pointerUp}); - - this.keyBindings.forEach(binding => { - this.input.addListener(binding.event, binding.fun); + this.tilemapLayer = new CCMapLayer(this.tileMap); + + await this.tilemapLayer.init({ + type: 'Background', + name: 'fromPhaser', + level: 0, + width: 1, + height: 1, + visible: 1, + tilesetName: '', + repeat: false, + distance: 0, + tilesize: Globals.TILE_SIZE, + moveSpeed: {x: 0, y: 0}, + data: [] }); } - private onMouseDown() { - if (!this.tilesetRendered) { - return; - } - - // only start tile copy when cursor in bounds - const pointer = this.input.activePointer; - const p = Helper.worldToTile(pointer.worldX, pointer.worldY); - if (!Helper.isInBoundsP(this.tilesetSize, p)) { - return; - } - - this.rightClickStart = p; - } - public resize() { const size = this.scale.gameSize; this.game.scale.resize(size.width, size.height); } - private onMouseUp() { - if (!this.tilesetRendered) { - return; - } - this.selectedTiles = []; - - // cancel current selection when out of bounds - if (!this.rightClickStart || !this.rightClickEnd) { - this.drawRect(1, 1); - return; - } - - // select tiles - const start = this.rightClickStart; - const end = this.rightClickEnd; - - const smaller = { - x: Math.min(start.x, end.x), - y: Math.min(start.y, end.y) - }; - - const bigger = { - x: Math.max(start.x, end.x), - y: Math.max(start.y, end.y) - }; - - const width = bigger.x - smaller.x + 1; - const height = bigger.y - smaller.y + 1; - - - const tilesWithin = this.tileMap!.getTilesWithin(smaller.x, smaller.y, width, height) ?? []; - - tilesWithin.forEach((tile: Phaser.Tilemaps.Tile) => { - this.selectedTiles.push({ - id: tile.index, - offset: Vec2.sub(tile, smaller, true) - }); - }); - - this.drawRect(width, height, smaller.x, smaller.y); - - this.rightClickStart = undefined; - this.rightClickEnd = undefined; - - Globals.phaserEventsService.changeSelectedTiles.next(this.selectedTiles); - } - destroy() { - if (this.sub) { - this.sub.unsubscribe(); - } - this.keyBindings.forEach(binding => { - this.input.removeListener(binding.event, binding.fun); - }); - this.keyBindings = []; - } - - - override update(time: number, delta: number): void { - const pointer = this.input.activePointer; - const p = Helper.worldToTile(pointer.worldX, pointer.worldY); - - // render selection border - if (this.rightClickStart) { - p.x = Helper.clamp(p.x, 0, this.tilesetSize.x - 1); - p.y = Helper.clamp(p.y, 0, this.tilesetSize.y - 1); - - if (this.rightClickEnd && this.rightClickEnd.x === p.x && this.rightClickEnd.y === p.y) { - // shortcut to avoid redrawing rectangle every frame - return; - } - - this.rightClickEnd = p; - const diff = Vec2.sub(p, this.rightClickStart, true); - const start = {x: this.rightClickStart.x, y: this.rightClickStart.y}; - if (diff.x >= 0) { - diff.x++; - } else { - start.x += 1; - diff.x--; - } - if (diff.y >= 0) { - diff.y++; - } else { - start.y += 1; - diff.y--; - } - - this.drawRect(diff.x, diff.y, start.x, start.y); - return; + for (const sub of this.subs) { + sub.unsubscribe(); } + this.subs = []; } - private async drawTileset(selectedLayer: CCMapLayer) { - this.tilesetRendered = false; - this.drawRect(0, 0); - - if (!selectedLayer.details.tilesetName) { - if (this.tileMap) { - this.tileMap.removeAllLayers(); - } - return; - } - - if (!this.tileMap) { - return; + private async drawTileset(selectedLayer?: CCMapLayer): Promise { + if (!selectedLayer?.details.tilesetName) { + return false; } const exists = await Helper.loadTexture(selectedLayer.details.tilesetName, this); if (!exists) { - return; + return false; } const tilesetSize = Helper.getTilesetSize(this, selectedLayer.details.tilesetName); - this.tilesetSize = tilesetSize; + this.tilesetWidth = tilesetSize.x; this.tileMap.removeAllLayers(); - const tileset = this.tileMap.addTilesetImage('tileset', selectedLayer.details.tilesetName, Globals.TILE_SIZE, Globals.TILE_SIZE); + const tileset = this.tileMap.addTilesetImage(selectedLayer.details.tilesetName); if (!tileset) { - return; + return false; } tileset.firstgid = 1; const layer = this.tileMap.createBlankLayer('first', tileset, 0, 0, tilesetSize.x, tilesetSize.y)!; @@ -224,23 +133,9 @@ export class TileSelectorScene extends Phaser.Scene { } customPutTilesAt(data, layer); + this.tilemapLayer.setPhaserLayer(layer); + await this.baseDrawer.setLayer(this.tilemapLayer); - this.tilesetRendered = true; - } - - private drawRect(width: number, height: number, x = 0, y = 0) { - if (this.rect) { - this.rect.destroy(); - } - if (!this.tilesetRendered) { - return; - } - this.rect = this.add.rectangle(x * Globals.TILE_SIZE, y * Globals.TILE_SIZE, width * Globals.TILE_SIZE, height * Globals.TILE_SIZE); - this.rect.setOrigin(0, 0); - if (Globals.settingsService.getSettings().selectionBoxDark) { - this.rect.setStrokeStyle(2, 0x333333, 0.9); - } else { - this.rect.setStrokeStyle(2, 0xffffff, 0.6); - } + return true; } } diff --git a/webapp/src/app/components/dialogs/map-settings/map-settings.component.ts b/webapp/src/app/components/dialogs/map-settings/map-settings.component.ts index 1e669b64..829db54d 100644 --- a/webapp/src/app/components/dialogs/map-settings/map-settings.component.ts +++ b/webapp/src/app/components/dialogs/map-settings/map-settings.component.ts @@ -63,6 +63,8 @@ export class MapSettingsComponent { y: settings.mapHeight }); + this.events.updateEntities.next(true); + this.ref.close(); } } diff --git a/webapp/src/app/components/editor/editor.component.ts b/webapp/src/app/components/editor/editor.component.ts index 8f6377a6..524854b2 100644 --- a/webapp/src/app/components/editor/editor.component.ts +++ b/webapp/src/app/components/editor/editor.component.ts @@ -3,6 +3,7 @@ import { MatSidenav } from '@angular/material/sidenav'; import { AddEntityMenuService } from '../../services/add-entity-menu.service'; import { LoadMapComponent } from '../dialogs/load-map/load-map.component'; +import { JsonLoaderService } from '../../services/json-loader.service'; @Component({ selector: 'app-editor', @@ -16,8 +17,17 @@ export class EditorComponent { @ViewChild('sidenavLoadMap', {static: true}) sidenavLoadMap!: MatSidenav; - constructor(addEntity: AddEntityMenuService) { + constructor( + addEntity: AddEntityMenuService, + jsonLoader: JsonLoaderService + ) { addEntity.init(); + + // makes sure they are synchronously available + jsonLoader.loadJsonMerged('actions.json'); + jsonLoader.loadJsonMerged('events.json'); + jsonLoader.loadJsonMerged('map-styles.json'); + } loadMapClicked() { diff --git a/webapp/src/app/components/phaser/phaser.component.ts b/webapp/src/app/components/phaser/phaser.component.ts index b98a69ef..9faa1f1d 100644 --- a/webapp/src/app/components/phaser/phaser.component.ts +++ b/webapp/src/app/components/phaser/phaser.component.ts @@ -13,6 +13,7 @@ import { MainScene } from '../../services/phaser/main-scene'; import { PhaserEventsService } from '../../services/phaser/phaser-events.service'; import { SettingsService } from '../../services/settings.service'; import { StateHistoryService } from '../dialogs/floating-window/history/state-history.service'; +import { JsonLoaderService } from '../../services/json-loader.service'; @Component({ selector: 'app-phaser', @@ -34,7 +35,8 @@ export class PhaserComponent implements AfterViewInit { snackbar: MatSnackBar, registry: EntityRegistryService, autotile: AutotileService, - settingsService: SettingsService + settingsService: SettingsService, + jsonLoader: JsonLoaderService, ) { Globals.stateHistoryService = stateHistory; Globals.mapLoaderService = mapLoader; @@ -45,6 +47,7 @@ export class PhaserComponent implements AfterViewInit { Globals.httpService = http; Globals.snackbar = snackbar; Globals.settingsService = settingsService; + Globals.jsonLoader = jsonLoader; } diff --git a/webapp/src/app/components/widgets/custom-des-type-widget/custom-des-type-widget.component.ts b/webapp/src/app/components/widgets/custom-des-type-widget/custom-des-type-widget.component.ts index c34296a4..3baae6e8 100644 --- a/webapp/src/app/components/widgets/custom-des-type-widget/custom-des-type-widget.component.ts +++ b/webapp/src/app/components/widgets/custom-des-type-widget/custom-des-type-widget.component.ts @@ -85,7 +85,7 @@ export class CustomDesTypeWidgetComponent extends OverlayWidget('destructibles.json'); destructibles = Object.keys(json).map(v => ({ name: v, desType: v diff --git a/webapp/src/app/components/widgets/event-widget/event-editor/add/add-event.service.ts b/webapp/src/app/components/widgets/event-widget/event-editor/add/add-event.service.ts index 994d0624..1a0d1f60 100644 --- a/webapp/src/app/components/widgets/event-widget/event-editor/add/add-event.service.ts +++ b/webapp/src/app/components/widgets/event-widget/event-editor/add/add-event.service.ts @@ -3,15 +3,13 @@ import { Injectable } from '@angular/core'; import { DomSanitizer } from '@angular/platform-browser'; import { Observable, Subject } from 'rxjs'; -import actions from '../../../../../../assets/actions.json'; -import events from '../../../../../../assets/events.json'; -import { - ListSearchOverlayComponent -} from '../../../../dialogs/list-search-overlay/list-search-overlay.component'; +import { ListSearchOverlayComponent } from '../../../../dialogs/list-search-overlay/list-search-overlay.component'; import { OverlayRefControl } from '../../../../dialogs/overlay/overlay-ref-control'; import { OverlayService } from '../../../../dialogs/overlay/overlay.service'; import { AbstractEvent } from '../../event-registry/abstract-event'; import { EventRegistryService } from '../../event-registry/event-registry.service'; +import { JsonLoaderService } from '../../../../../services/json-loader.service'; +import { ActionsJson, EventsJson } from '../../event-registry/default-event'; @Injectable({ providedIn: 'root' @@ -20,8 +18,8 @@ export class AddEventService { private selectedEvent = new Subject>(); private actionStep = false; - events: string[]; - actions: string[]; + events: string[] = []; + actions: string[] = []; private ref?: OverlayRefControl; @@ -29,11 +27,18 @@ export class AddEventService { private eventRegistry: EventRegistryService, private overlayService: OverlayService, private overlay: Overlay, - private domSanitizer: DomSanitizer + private domSanitizer: DomSanitizer, + private jsonLoader: JsonLoaderService, ) { - const registry = Object.keys(this.eventRegistry.getAll()); - const eventNames = Object.keys(events); + this.init(); + } + + private async init() { + const events = await this.jsonLoader.loadJsonMerged('events.json'); + const actions = await this.jsonLoader.loadJsonMerged('actions.json'); + const eventNames = Object.keys(events); + const registry = Object.keys(this.eventRegistry.getAll()); const eventSet = new Set([...registry, ...eventNames]); this.events = Array.from(eventSet); diff --git a/webapp/src/app/components/widgets/event-widget/event-editor/event-helper.service.ts b/webapp/src/app/components/widgets/event-widget/event-editor/event-helper.service.ts index 8133437c..c36bab1f 100644 --- a/webapp/src/app/components/widgets/event-widget/event-editor/event-helper.service.ts +++ b/webapp/src/app/components/widgets/event-widget/event-editor/event-helper.service.ts @@ -9,11 +9,11 @@ import { EventRegistryService } from '../event-registry/event-registry.service'; }) export class EventHelperService { - selectedEvent: BehaviorSubject | null> = new BehaviorSubject | null> (null); + selectedEvent: BehaviorSubject | null> = new BehaviorSubject | null>(null); constructor( private eventRegistry: EventRegistryService, - private domSanitizer: DomSanitizer + private domSanitizer: DomSanitizer, ) { } diff --git a/webapp/src/app/components/widgets/event-widget/event-registry/default-event.ts b/webapp/src/app/components/widgets/event-widget/event-registry/default-event.ts index dc17298e..c504c7a2 100644 --- a/webapp/src/app/components/widgets/event-widget/event-registry/default-event.ts +++ b/webapp/src/app/components/widgets/event-widget/event-registry/default-event.ts @@ -1,8 +1,15 @@ import { DomSanitizer } from '@angular/platform-browser'; -import actions from '../../../../../assets/actions.json'; -import events from '../../../../../assets/events.json'; import { AttributeValue, EntityAttributes } from '../../../../services/phaser/entities/cc-entity'; import { AbstractEvent, EventType } from './abstract-event'; +import { Globals } from '../../../../services/globals'; + +export interface ActionsJson { + [key: string]: JsonEventType; +} + +export interface EventsJson { + [key: string]: JsonEventType; +} interface DefaultEventData extends EventType { [key: string]: any; @@ -20,13 +27,14 @@ export class DefaultEvent extends Abstra constructor( domSanitizer: DomSanitizer, data: T, - actionStep = false + actionStep = false, ) { super(domSanitizer, data, actionStep); + const jsonLoader = Globals.jsonLoader; if (actionStep) { - this.type = actions[this.data.type]; + this.type = jsonLoader.loadJsonMergedSync('actions.json')[this.data.type]; } else { - this.type = events[this.data.type]; + this.type = jsonLoader.loadJsonMergedSync('events.json')[this.data.type]; } } diff --git a/webapp/src/app/components/widgets/event-widget/event-registry/event-registry.service.ts b/webapp/src/app/components/widgets/event-widget/event-registry/event-registry.service.ts index 0a244c28..49a816af 100644 --- a/webapp/src/app/components/widgets/event-widget/event-registry/event-registry.service.ts +++ b/webapp/src/app/components/widgets/event-widget/event-registry/event-registry.service.ts @@ -24,7 +24,11 @@ import { ShowSideMsg } from './show-side-msg'; import { ShowModalChoice } from './show-modal-choice'; import { SetMsgExpression } from './set-msg-expression'; -type EventConstructor = new (domSanitizer: DomSanitizer, data: T, actionStep: boolean) => AbstractEvent; +type EventConstructor = new ( + domSanitizer: DomSanitizer, + data: T, + actionStep: boolean +) => AbstractEvent; @Injectable({ providedIn: 'root' diff --git a/webapp/src/app/components/widgets/person-expression-widget/custom-expression-widget/custom-expression-widget.component.ts b/webapp/src/app/components/widgets/person-expression-widget/custom-expression-widget/custom-expression-widget.component.ts index d5de39ef..258cff2a 100644 --- a/webapp/src/app/components/widgets/person-expression-widget/custom-expression-widget/custom-expression-widget.component.ts +++ b/webapp/src/app/components/widgets/person-expression-widget/custom-expression-widget/custom-expression-widget.component.ts @@ -9,10 +9,10 @@ import { Helper } from '../../../../services/phaser/helper'; import { CharacterSettings, Face } from '../../../../services/phaser/entities/registry/npc'; import { Person } from '../../../../models/events'; import { prepareSheet } from '../../../../services/phaser/sheet-parser'; -import AbstractFaces from '../../../../../assets/abstract-faces.json'; import { getNPCTemplates } from '../../../../services/phaser/entities/registry/npc-templates'; import { ExpressionRendererEntity, ExpressionRendererSettings } from './expression-renderer-entity'; import { Globals } from '../../../../services/globals'; +import { JsonLoaderService } from '../../../../services/json-loader.service'; @Component({ selector: 'app-custom-expression-widget', @@ -27,6 +27,7 @@ export class CustomExpressionWidgetComponent extends OverlayWidget imple constructor( private http: HttpClientService, private changeDetectorRef: ChangeDetectorRef, + private jsonLoader: JsonLoaderService, overlayService: OverlayService, overlay: Overlay, ) { @@ -109,7 +110,8 @@ export class CustomExpressionWidgetComponent extends OverlayWidget imple let face: Face = sheet.face ?? {}; if (typeof sheet.face === 'object' && sheet.face?.ABSTRACT) { - face = AbstractFaces[sheet.face.ABSTRACT as string]; + const abstractFaces = await this.jsonLoader.loadJsonMerged>('abstract-faces.json'); + face = abstractFaces[sheet.face.ABSTRACT as string]; } return face; diff --git a/webapp/src/app/models/map-styles.ts b/webapp/src/app/models/map-styles.ts index 6bbfd0fc..b48a1d30 100644 --- a/webapp/src/app/models/map-styles.ts +++ b/webapp/src/app/models/map-styles.ts @@ -1,21 +1,13 @@ -export type MapStyles = { - walls: WallColors; -} & { - [key: string]: MapStyle; -}; +export interface MapStyles { + default: MapStyleType; + + [key: string]: MapStyleType | undefined; +} -export type MapStyle = { - sheet: string; - hasDoorMat?: boolean; -} & { - [key: string]: any; -}; +export interface MapStyleType { + [key: string]: MapStyle | undefined; +} -export interface WallColors { - blockFront: string; - blockTop: string; - pBlockFront: string; - pBlockTop: string; - npBlockFront: string; - npBlockTop: string; +export interface MapStyle { + sheet?: string; } diff --git a/webapp/src/app/services/add-entity-menu.service.ts b/webapp/src/app/services/add-entity-menu.service.ts index 63f46129..4da63465 100644 --- a/webapp/src/app/services/add-entity-menu.service.ts +++ b/webapp/src/app/services/add-entity-menu.service.ts @@ -1,7 +1,6 @@ import { Overlay } from '@angular/cdk/overlay'; import { Injectable } from '@angular/core'; -import entities from '../../assets/entities.json'; import { ListSearchOverlayComponent } from '../components/dialogs/list-search-overlay/list-search-overlay.component'; import { OverlayRefControl } from '../components/dialogs/overlay/overlay-ref-control'; import { OverlayService } from '../components/dialogs/overlay/overlay.service'; @@ -9,6 +8,8 @@ import { MapEntity, Point } from '../models/cross-code-map'; import { GlobalEventsService } from './global-events.service'; import { EntityRegistryService } from './phaser/entities/registry/entity-registry.service'; import { Vec2 } from './phaser/vec2'; +import { JsonLoaderService } from './json-loader.service'; +import { EntitiesJson } from './phaser/entities/registry/default-entity'; @Injectable({ providedIn: 'root' @@ -26,11 +27,13 @@ export class AddEntityMenuService { private events: GlobalEventsService, private overlayService: OverlayService, private overlay: Overlay, - private entityRegistry: EntityRegistryService + private entityRegistry: EntityRegistryService, + private jsonLoader: JsonLoaderService, ) { } - public init() { + public async init() { + const entities = await this.jsonLoader.loadJsonMerged('entities.json'); const registry = Object.keys(this.entityRegistry.getAll()); const entityNames = Object.keys(entities); diff --git a/webapp/src/app/services/autotile/autotile.constants.ts b/webapp/src/app/services/autotile/autotile.constants.ts index 0753f94f..30248d57 100644 --- a/webapp/src/app/services/autotile/autotile.constants.ts +++ b/webapp/src/app/services/autotile/autotile.constants.ts @@ -1,13 +1,8 @@ import { Point } from '../../models/cross-code-map'; +import { Helper } from '../phaser/helper'; -/** value is width, height is always 2 */ -export enum AutotileType { - SMALL = 4, - DEFAULT = 8, - LARGE = 10, - SUPER_LARGE = 12, - MEGA_LARGE = 14 -} +/** Known autotile sizes */ +export type AutotileType = '4x4' | '8x2' | '10x2' | '12x2' | '14x2'; export interface AutotileConfig { tileCountX: number; @@ -23,6 +18,12 @@ export interface AutotileConfig { * X means tile, O means border. * * E.g. XXXX is the filled tile, OXXO is the left border + * + * XX OX + * XX OX + * + * + * 4x4 uses a different layout: top, right, bottom, left. */ export interface FillType { XXXX: Point[]; @@ -43,10 +44,6 @@ export interface FillType { XOOO: Point[]; } -export const FILL_TYPE: { - [key in AutotileType]: FillType -} = {}; - const empty: FillType = { XXXX: [], OXXX: [], @@ -67,7 +64,7 @@ const empty: FillType = { }; -const fillTypeDefault: FillType = { +const fillType8x2: FillType = { XXXX: [{x: 0, y: 0}], OXXX: [{x: 1, y: 0}], XOXX: [{x: 2, y: 0}], @@ -85,43 +82,62 @@ const fillTypeDefault: FillType = { OXOO: [{x: 6, y: 1}], XOOO: [{x: 7, y: 1}], }; -FILL_TYPE[AutotileType.DEFAULT] = fillTypeDefault; - -const fillTypeLarge: FillType = JSON.parse(JSON.stringify(fillTypeDefault)); -fillTypeLarge.OOXX.push({x: 8, y: 0}); -fillTypeLarge.OXXO.push({x: 9, y: 0}); -fillTypeLarge.XXOO.push({x: 8, y: 1}); -fillTypeLarge.XOOX.push({x: 9, y: 1}); -FILL_TYPE[AutotileType.LARGE] = fillTypeLarge; - -const fillTypeSuperLarge: FillType = JSON.parse(JSON.stringify(fillTypeLarge)); -fillTypeSuperLarge.OOXO.push({x: 10, y: 0}); -fillTypeSuperLarge.OOOX.push({x: 11, y: 0}); -fillTypeSuperLarge.OXOO.push({x: 10, y: 1}); -fillTypeSuperLarge.XOOO.push({x: 11, y: 1}); -FILL_TYPE[AutotileType.SUPER_LARGE] = fillTypeSuperLarge; - -const fillTypeMegaLarge: FillType = JSON.parse(JSON.stringify(fillTypeSuperLarge)); -fillTypeMegaLarge.OOXX.push({x: 12, y: 0}); -fillTypeMegaLarge.OXXO.push({x: 13, y: 0}); -fillTypeMegaLarge.XXOO.push({x: 12, y: 1}); -fillTypeMegaLarge.XOOX.push({x: 13, y: 1}); -FILL_TYPE[AutotileType.MEGA_LARGE] = fillTypeMegaLarge; - -const fillTypeSmall: FillType = JSON.parse(JSON.stringify(empty)); -for (const value of Object.values(fillTypeSmall) as Point[][]) { - value.push({x: 0, y: 0}); - value.push({x: 0, y: 0}); - value.push({x: 0, y: 0}); - value.push({x: 1, y: 0}); - value.push({x: 2, y: 0}); - value.push({x: 3, y: 0}); -} -FILL_TYPE[AutotileType.SMALL] = fillTypeSmall; +const fillType10x2 = Helper.copy(fillType8x2); +fillType10x2.OOXX.push({x: 8, y: 0}); +fillType10x2.OXXO.push({x: 9, y: 0}); +fillType10x2.XXOO.push({x: 8, y: 1}); +fillType10x2.XOOX.push({x: 9, y: 1}); + +const fillType12x2 = Helper.copy(fillType10x2); +fillType12x2.OOXO.push({x: 10, y: 0}); +fillType12x2.OOOX.push({x: 11, y: 0}); +fillType12x2.OXOO.push({x: 10, y: 1}); +fillType12x2.XOOO.push({x: 11, y: 1}); + +const fillType14x2 = Helper.copy(fillType12x2); +fillType14x2.OOXX.push({x: 12, y: 0}); +fillType14x2.OXXO.push({x: 13, y: 0}); +fillType14x2.XXOO.push({x: 12, y: 1}); +fillType14x2.XOOX.push({x: 13, y: 1}); + +const fillType4x4: FillType = { + // order is important for same offset. Last one is used for reverse mapping + OOOO: [{x: 2, y: 2}], + XXXX: [{x: 2, y: 2}], + + OXXX: [{x: 2, y: 1}], + XOXX: [{x: 3, y: 2}], + XXOX: [{x: 2, y: 3}], + XXXO: [{x: 1, y: 2}], + + XXOO: [{x: 1, y: 3}], + OXXO: [{x: 1, y: 1}], + OOXX: [{x: 3, y: 1}], + XOOX: [{x: 3, y: 3}], + + OXOX: [{x: 1, y: 0}], + XOXO: [{x: 0, y: 2}], + + XOOO: [{x: 0, y: 3}], + OXOO: [{x: 0, y: 0}], + OOXO: [{x: 0, y: 1}], + OOOX: [{x: 2, y: 0}], +}; + + +export const FILL_TYPE: { + [key in AutotileType]: FillType +} = { + '4x4': fillType4x4, + '8x2': fillType8x2, + '10x2': fillType10x2, + '12x2': fillType12x2, + '14x2': fillType14x2 +}; -export const FILL_TYPE_CLIFF_BORDER: FillType = JSON.parse(JSON.stringify(empty)); +export const FILL_TYPE_CLIFF_BORDER = Helper.copy(empty); FILL_TYPE_CLIFF_BORDER.XXXX = [ {x: 1, y: 0}, @@ -182,7 +198,7 @@ FILL_TYPE_CLIFF_BORDER.XOOX = [ {x: 3, y: 6} ]; -export const FILL_TYPE_CLIFF: FillType = JSON.parse(JSON.stringify(empty)); +export const FILL_TYPE_CLIFF: FillType = Helper.copy(empty); FILL_TYPE_CLIFF.XXXX = [ {x: 0, y: 3}, @@ -191,7 +207,7 @@ FILL_TYPE_CLIFF.XXXX = [ {x: 5, y: 3}, ]; -export const FILL_TYPE_CLIFF_ALT: FillType = JSON.parse(JSON.stringify(empty)); +export const FILL_TYPE_CLIFF_ALT: FillType = Helper.copy(empty); FILL_TYPE_CLIFF_ALT.XXXX = [ {x: 0, y: 0}, diff --git a/webapp/src/app/services/autotile/autotile.service.ts b/webapp/src/app/services/autotile/autotile.service.ts index 21a23da0..7b410c74 100644 --- a/webapp/src/app/services/autotile/autotile.service.ts +++ b/webapp/src/app/services/autotile/autotile.service.ts @@ -1,10 +1,15 @@ import { Injectable } from '@angular/core'; import { Point } from '../../models/cross-code-map'; -import { CheckDir, CHECK_DIR, CHECK_ITERATE } from '../height-map/heightmap.constants'; +import { CHECK_DIR, CHECK_ITERATE, CheckDir } from '../height-map/heightmap.constants'; import { CCMapLayer } from '../phaser/tilemap/cc-map-layer'; import { AutotileConfig, FillType } from './autotile.constants'; import { GfxMapper } from './gfx-mapper'; import { customPutTileAt } from '../phaser/tilemap/layer-helper'; +import { PhaserEventsService } from '../phaser/phaser-events.service'; +import { combineLatest } from 'rxjs'; +import { MapLoaderService } from '../map-loader.service'; +import { GlobalEventsService } from '../global-events.service'; +import { JsonLoaderService } from '../json-loader.service'; interface TileData { pos: Point; @@ -17,9 +22,33 @@ interface TileData { }) export class AutotileService { - private gfxMapper = new GfxMapper(); + private gfxMapper: GfxMapper; - constructor() { + constructor( + phaserEvents: PhaserEventsService, + mapLoader: MapLoaderService, + events: GlobalEventsService, + jsonLoader: JsonLoaderService, + ) { + this.gfxMapper = new GfxMapper(jsonLoader); + combineLatest([ + phaserEvents.changeSelectedTiles.asObservable(), + mapLoader.selectedLayer.asObservable() + ]).subscribe(([tiles, layer]) => { + if (!layer) { + events.isAutotile.next(false); + return; + } + let autotile = false; + for (const tile of tiles) { + const config = this.gfxMapper.getAutotileConfig(layer.details.tilesetName, tile.id, false); + if (config) { + autotile = true; + break; + } + } + events.isAutotile.next(autotile); + }); } public drawTile(layer: CCMapLayer, x: number, y: number, tile: number, checkCliff = true) { @@ -67,29 +96,54 @@ export class AutotileService { let fillType = ''; - if (this.checkAt(w, 1) && this.checkAt(nw, 2) && this.checkAt(n, 3)) { - fillType += 'X'; - } else { - fillType += 'O'; - } - - if (this.checkAt(n, 2) && this.checkAt(ne, 3) && this.checkAt(e, 0)) { - fillType += 'X'; - } else { - fillType += 'O'; - } - - if (this.checkAt(e, 3) && this.checkAt(se, 0) && this.checkAt(s, 1)) { - fillType += 'X'; + // 4x4 needs special handling, corners are ignored + if (config.type === '4x4') { + if (this.checkAt(n, 2)) { + fillType += 'X'; + } else { + fillType += 'O'; + } + if (this.checkAt(e, 3)) { + fillType += 'X'; + } else { + fillType += 'O'; + } + if (this.checkAt(s, 0)) { + fillType += 'X'; + } else { + fillType += 'O'; + } + if (this.checkAt(w, 1)) { + fillType += 'X'; + } else { + fillType += 'O'; + } } else { - fillType += 'O'; + if (this.checkAt(w, 1) && this.checkAt(nw, 2) && this.checkAt(n, 3)) { + fillType += 'X'; + } else { + fillType += 'O'; + } + + if (this.checkAt(n, 2) && this.checkAt(ne, 3) && this.checkAt(e, 0)) { + fillType += 'X'; + } else { + fillType += 'O'; + } + + if (this.checkAt(e, 3) && this.checkAt(se, 0) && this.checkAt(s, 1)) { + fillType += 'X'; + } else { + fillType += 'O'; + } + + if (this.checkAt(s, 0) && this.checkAt(sw, 1) && this.checkAt(w, 2)) { + fillType += 'X'; + } else { + fillType += 'O'; + } } - if (this.checkAt(s, 0) && this.checkAt(sw, 1) && this.checkAt(w, 2)) { - fillType += 'X'; - } else { - fillType += 'O'; - } tile.fill = fillType as keyof FillType; this.drawSingleTile(layer, config, tile); } diff --git a/webapp/src/app/services/autotile/gfx-mapper.ts b/webapp/src/app/services/autotile/gfx-mapper.ts index d4b429f3..ca30a971 100644 --- a/webapp/src/app/services/autotile/gfx-mapper.ts +++ b/webapp/src/app/services/autotile/gfx-mapper.ts @@ -2,28 +2,17 @@ import { Point } from '../../models/cross-code-map'; import { ChipsetConfig } from '../height-map/gfx-mapper/gfx-mapper.constants'; import { Helper } from '../phaser/helper'; import { Vec2 } from '../phaser/vec2'; -import { - AutotileConfig, - AutotileType, FillType, FILL_TYPE, - FILL_TYPE_CLIFF, - FILL_TYPE_CLIFF_ALT, - FILL_TYPE_CLIFF_BORDER -} from './autotile.constants'; - -import autotilesJson from '../../../assets/autotiles.json'; - -import tilesets from '../../../assets/tilesets.json'; - -const TILESET_CONFIG: { [key: string]: ChipsetConfig } = tilesets; +import { AutotileConfig, AutotileType, FILL_TYPE, FILL_TYPE_CLIFF, FILL_TYPE_CLIFF_ALT, FILL_TYPE_CLIFF_BORDER, FillType } from './autotile.constants'; +import { JsonLoaderService } from '../json-loader.service'; interface JsonType { map: string; tileCountX: number; autotiles: { - type: keyof typeof AutotileType; + size: Point; + cliff?: Point | null | false; mergeWithEmpty?: boolean; base: Point; - cliff: Point; }[]; } @@ -33,17 +22,25 @@ export class GfxMapper { [key: string]: AutotileConfig[] | undefined; } = {}; + + private TILESET_CONFIG: Record = {}; + private mapping: { [key in AutotileType]: Map } = {}; private cliffBorderMapping = new Map(); private cliffMapping = new Map(); private cliffAltMapping = new Map(); - constructor() { - this.generateAutotileConfig(); - - const enumVals = Object.values(AutotileType).filter(v => !isNaN(Number(v))); + constructor( + private jsonLoader: JsonLoaderService + ) { + this.init(); + } + + private async init() { + this.TILESET_CONFIG = await this.jsonLoader.loadJsonMerged('tilesets.json'); + await this.generateAutotileConfig(); - for (const type of enumVals as AutotileType[]) { + for (const type of Helper.typedKeys(FILL_TYPE)) { const map = new Map(); this.mapping[type] = map; this.generateMapping(map, FILL_TYPE[type]); @@ -52,24 +49,44 @@ export class GfxMapper { this.generateMapping(this.cliffBorderMapping, FILL_TYPE_CLIFF_BORDER); this.generateMapping(this.cliffMapping, FILL_TYPE_CLIFF); this.generateMapping(this.cliffAltMapping, FILL_TYPE_CLIFF_ALT); - } - private generateAutotileConfig() { - console.log(autotilesJson); - for (const config of autotilesJson as JsonType[]) { - const arr: AutotileConfig[] = []; + private async generateAutotileConfig() { + const jsons = await this.jsonLoader.loadJson('autotiles.json'); + const autotilesJson = jsons.flat(); + for (const config of autotilesJson) { + let arr: AutotileConfig[] = []; + const prevArr = this.AUTOTILE_CONFIG[config.map]; + if (prevArr) { + arr = prevArr; + } this.AUTOTILE_CONFIG[config.map] = arr; for (const autotile of config.autotiles) { - const type = AutotileType[autotile.type]; + const generatedType: AutotileType = `${autotile.size.x}x${autotile.size.y}` as AutotileType; + + const tileset = this.TILESET_CONFIG[config.map]; + const terrains = tileset?.terrains ?? []; + if (tileset) { + terrains.push(tileset.base); + } + let cliff = autotile.cliff; + if (cliff === undefined) { + for (const terrain of terrains) { + if (terrain.ground.x === autotile.base.x && terrain.ground.y === autotile.base.y) { + cliff = terrain.cliff; + break; + } + } + } + const newConfig: AutotileConfig = { key: config.map, tileCountX: config.tileCountX, - type: type, - mergeWithEmpty: autotile.mergeWithEmpty === undefined ? true : autotile.mergeWithEmpty, + type: generatedType, + mergeWithEmpty: !!autotile.mergeWithEmpty, base: autotile.base, - cliff: autotile.cliff + cliff: cliff ? cliff : undefined }; arr.push(newConfig); @@ -108,7 +125,7 @@ export class GfxMapper { if (!cliff) { return this.getFill(pos, config.base, this.mapping[config.type]); } - const tilesetConfig = TILESET_CONFIG[config.key]; + const tilesetConfig = this.TILESET_CONFIG[config.key]; let tilesetBase; if (tilesetConfig && tilesetConfig.base) { tilesetBase = tilesetConfig.base; @@ -146,7 +163,7 @@ export class GfxMapper { } private getMappingKey(p: Point) { - return p.y * 2000 + p.x; + return p.y * 1000 + p.x; } } diff --git a/webapp/src/app/services/global-events.service.ts b/webapp/src/app/services/global-events.service.ts index 4c9814ac..704cbcdf 100644 --- a/webapp/src/app/services/global-events.service.ts +++ b/webapp/src/app/services/global-events.service.ts @@ -22,9 +22,11 @@ export class GlobalEventsService { offsetEntities = new Subject(); toggleVisibility = new Subject(); showAddEntityMenu = new Subject(); + updateEntities = new Subject(); updateCoords = new Subject(); updateTileSelectionSize = new Subject(); + isAutotile = new BehaviorSubject(false); showIngamePreview = new BehaviorSubject(false); hasUnsavedChanges = new BehaviorSubject(false); gridSettings = new BehaviorSubject(Globals.gridSettings()); diff --git a/webapp/src/app/services/globals.ts b/webapp/src/app/services/globals.ts index 73393a92..bbaa9dd0 100644 --- a/webapp/src/app/services/globals.ts +++ b/webapp/src/app/services/globals.ts @@ -10,6 +10,7 @@ import { MatSnackBar } from '@angular/material/snack-bar'; import { SettingsService } from './settings.service'; import { signal } from '@angular/core'; import { GridSettings } from '../components/toolbar/grid-menu/grid-menu.component'; +import { JsonLoaderService } from './json-loader.service'; export class Globals { static isElectron = false; @@ -27,7 +28,6 @@ export class Globals { }); static disablePhaserInput = new Set(); - // TODO: remove them from global state static stateHistoryService: StateHistoryService; static mapLoaderService: MapLoaderService; static globalEventsService: GlobalEventsService; @@ -37,4 +37,5 @@ export class Globals { static httpService: HttpClientService; static settingsService: SettingsService; static snackbar: MatSnackBar; + static jsonLoader: JsonLoaderService; } diff --git a/webapp/src/app/services/height-map/height-map.service.ts b/webapp/src/app/services/height-map/height-map.service.ts index 8cec2161..4b198b6b 100644 --- a/webapp/src/app/services/height-map/height-map.service.ts +++ b/webapp/src/app/services/height-map/height-map.service.ts @@ -1,7 +1,6 @@ import { Injectable } from '@angular/core'; import { EventManager } from '@angular/platform-browser'; -import tilesets from '../../../assets/tilesets.json'; import { StateHistoryService } from '../../components/dialogs/floating-window/history/state-history.service'; import { AutotileService } from '../autotile/autotile.service'; import { GlobalEventsService } from '../global-events.service'; @@ -29,8 +28,11 @@ import { WallLink } from './heightmap.constants'; import { customPutTileAt } from '../phaser/tilemap/layer-helper'; +import { JsonLoaderService } from '../json-loader.service'; -const TILESET_CONFIG: { [key: string]: ChipsetConfig } = tilesets; +interface TilesetJson { + [key: string]: ChipsetConfig; +} interface TileData { level: number; @@ -53,6 +55,7 @@ export class HeightMapService { private maxLevel = 0; private width = 0; private height = 0; + private tilesetConfig: TilesetJson = {}; private c_wallProps = {start: 0, end: 0}; @@ -61,6 +64,7 @@ export class HeightMapService { private mapLoader: MapLoaderService, private stateHistory: StateHistoryService, private autotile: AutotileService, + private jsonLoader: JsonLoaderService, eventManager: EventManager ) { @@ -75,10 +79,12 @@ export class HeightMapService { }); } - public init() { + public async init() { this.events.generateHeights.subscribe(forceAll => this.generateHeights(forceAll)); this.mapLoader.tileMap.subscribe(map => this.onMapLoad(map)); + this.tilesetConfig = await this.jsonLoader.loadJsonMerged('tilesets.json'); + // TODO: add shortcuts for generation } @@ -202,7 +208,7 @@ export class HeightMapService { if (details.distance !== 1) { continue; } - if (details.type === 'Background' && TILESET_CONFIG[details.tilesetName] && lastLevel !== details.level) { + if (details.type === 'Background' && this.tilesetConfig[details.tilesetName] && lastLevel !== details.level) { lastLevel = details.level; this.applyOnBackground(layer, forceAll); } else if (details.type === 'Collision') { @@ -229,7 +235,7 @@ export class HeightMapService { } private applyOnBackground(layer: CCMapLayer, forceAll: boolean) { - const config = TILESET_CONFIG[layer.details.tilesetName]; + const config = this.tilesetConfig[layer.details.tilesetName]; if (!config) { return; } diff --git a/webapp/src/app/services/http-client.service.ts b/webapp/src/app/services/http-client.service.ts index 22d7142b..c063bfc3 100644 --- a/webapp/src/app/services/http-client.service.ts +++ b/webapp/src/app/services/http-client.service.ts @@ -1,7 +1,7 @@ import { HttpClient } from '@angular/common/http'; import { Injectable } from '@angular/core'; import { api } from 'cc-map-editor-common'; -import { Observable } from 'rxjs'; +import { lastValueFrom, Observable } from 'rxjs'; import { FileInfos } from '../models/file-infos'; import { ElectronService } from './electron.service'; import { Globals } from './globals'; @@ -61,6 +61,10 @@ export class HttpClientService { getMods(): Observable<{ id: string, displayName: string }[]> { return this.request('api/allMods', api.getAllMods); } + + getModMapEditorConfigs() { + return lastValueFrom(this.request('api/allModMapEditorConfigs', api.getAllModMapEditorConfigs)); + } getAssetsFile(path: string): Observable { if (!Globals.isElectron) { diff --git a/webapp/src/app/services/json-loader.service.ts b/webapp/src/app/services/json-loader.service.ts new file mode 100644 index 00000000..05335ac4 --- /dev/null +++ b/webapp/src/app/services/json-loader.service.ts @@ -0,0 +1,77 @@ +import { Injectable } from '@angular/core'; +import { HttpClientService } from './http-client.service'; +import { HttpClient } from '@angular/common/http'; +import { lastValueFrom } from 'rxjs'; +import { MatSnackBar } from '@angular/material/snack-bar'; + +@Injectable({ + providedIn: 'root' +}) +export class JsonLoaderService { + + private readonly initialized?: Promise; + private configs = new Map; + + private cache: Record = {}; + + constructor( + private http: HttpClientService, + private angularHttp: HttpClient, + private snackbar: MatSnackBar, + ) { + this.initialized = this.init(); + } + + async init() { + const configs = await this.http.getModMapEditorConfigs(); + + for (const config of configs) { + let parsedFile: unknown; + try { + parsedFile = JSON.parse(config.file); + } catch (e) { + console.error(e); + this.snackbar.open( + `Failed to parse mod config: ${config.mod}/map-editor/${config.filename}`, + 'close', + {panelClass: 'snackbar-error'} + ); + continue; + } + const configs = this.configs.get(config.filename) ?? []; + this.configs.set(config.filename, configs); + configs.push(parsedFile); + } + } + + async loadJson(file: string): Promise { + await this.initialized; + const json = await lastValueFrom(this.angularHttp.get(`./assets/${file}`)); + const modJson = (this.configs.get(file) ?? []) as T[]; + + return [ + json as T, + ...modJson + ]; + } + + async loadJsonMerged(file: string): Promise { + const cached = this.cache[file] as T | undefined; + if (cached) { + return cached; + } + const jsons = await this.loadJson(file); + const base = {} as T; + Object.assign(base as any, ...jsons); + this.cache[file] = base; + return base; + } + + loadJsonMergedSync(file: string): T { + const cached = this.cache[file] as T | undefined; + if (cached) { + return cached; + } + throw new Error('Tried to get json synchronous, but its not loaded'); + } +} diff --git a/webapp/src/app/services/map-loader.service.ts b/webapp/src/app/services/map-loader.service.ts index 1e9f8c9b..989face2 100644 --- a/webapp/src/app/services/map-loader.service.ts +++ b/webapp/src/app/services/map-loader.service.ts @@ -49,7 +49,8 @@ export class MapLoaderService { } catch (e: any) { console.error(e); this.snackBar.open('Error: ' + e.message, undefined, { - duration: 2500 + duration: 2500, + panelClass: 'snackbar-error' }); return; } diff --git a/webapp/src/app/services/phaser/BaseTileDrawer.ts b/webapp/src/app/services/phaser/BaseTileDrawer.ts new file mode 100644 index 00000000..c4fa8429 --- /dev/null +++ b/webapp/src/app/services/phaser/BaseTileDrawer.ts @@ -0,0 +1,315 @@ +import { SelectedTile } from '../../models/tile-selector'; +import { Point } from '../../models/cross-code-map'; +import { Helper } from './helper'; +import { BaseObject } from './base-object'; +import * as Phaser from 'phaser'; +import { CCMapLayer } from './tilemap/cc-map-layer'; +import { Globals } from '../globals'; +import { Vec2 } from './vec2'; +import { customPutTileAt } from './tilemap/layer-helper'; + +export class BaseTileDrawer extends BaseObject { + private selectedTiles: SelectedTile[] = []; + private rightPointerDown = false; + private rightClickStart?: Point; + private rightClickEnd?: Point; + + private selection?: Phaser.GameObjects.Container; + + private previewTileMap!: Phaser.Tilemaps.Tilemap; + private previewLayer?: Phaser.Tilemaps.TilemapLayer; + + private layer?: CCMapLayer; + + /** + * @param scene + * @param leftClick + * @param container container is used to follow mouse movements and render preview + */ + constructor( + scene: Phaser.Scene, + private leftClick?: boolean, + private container?: Phaser.GameObjects.Container + ) { + super(scene, 'baseTileDrawer'); + } + + protected override init(): void { + this.previewTileMap = this.scene.add.tilemap(undefined, Globals.TILE_SIZE, Globals.TILE_SIZE); + } + + preUpdate(): void { + if (!this.layer) { + return; + } + const pointer = this.scene.input.activePointer; + const p = Helper.worldToTile(pointer.worldX - this.layer.x, pointer.worldY - this.layer.y); + + // render selection border + if (this.rightClickStart) { + Helper.clampToBounds(this.layer, p); + + if (this.rightClickEnd && this.rightClickEnd.x === p.x && this.rightClickEnd.y === p.y) { + // shortcut to avoid redrawing rectangle every frame + return; + } + + this.rightClickEnd = p; + const diff = Vec2.sub(p, this.rightClickStart, true); + const start = {x: 0, y: 0}; + if (diff.x >= 0) { + diff.x++; + } else { + start.x = 1; + diff.x--; + } + if (diff.y >= 0) { + diff.y++; + } else { + start.y = 1; + diff.y--; + } + + if (!this.container) { + Vec2.add(start, this.rightClickStart); + } + this.drawRect(diff.x, diff.y, start.x * Globals.TILE_SIZE, start.y * Globals.TILE_SIZE, true); + return; + } + + + // position tile drawer border to cursor + if (this.container) { + const container = this.container; + container.x = pointer.worldX; + container.y = pointer.worldY; + + if (container.x < this.layer.x) { + container.x -= Globals.TILE_SIZE; + } + if (container.y < this.layer.y) { + container.y -= Globals.TILE_SIZE; + } + + container.x -= (container.x - this.layer.x) % Globals.TILE_SIZE; + container.y -= (container.y - this.layer.y) % Globals.TILE_SIZE; + + if (this.previewLayer) { + Vec2.assign(this.previewLayer, container); + } + } + } + + protected override activate(): void { + this.setVisibility(true); + const pointerDown = (pointer: Phaser.Input.Pointer) => { + if (pointer.rightButtonDown() || this.leftClick && pointer.leftButtonDown()) { + this.onMouseRightDown(); + } + }; + + const pointerUp = (pointer: Phaser.Input.Pointer) => { + if ((pointer.rightButtonReleased() || this.leftClick && pointer.leftButtonReleased()) && this.rightPointerDown) { + this.onMouseRightUp(); + } + }; + this.addKeybinding({event: 'pointerdown', fun: pointerDown, emitter: this.scene.input}); + this.addKeybinding({event: 'pointerup', fun: pointerUp, emitter: this.scene.input}); + this.addKeybinding({event: 'pointerupoutside', fun: pointerUp, emitter: this.scene.input}); + + this.addSubscription(Globals.phaserEventsService.changeSelectedTiles.subscribe(tiles => this.updateSelectedTiles(tiles))); + } + + protected override deactivate(): void { + this.setVisibility(false); + } + + private setVisibility(selection: boolean, preview = selection) { + this.selection?.setVisible(selection && !!this.layer); + this.previewLayer?.setVisible(preview && !!this.layer); + } + + public async setLayer(layer?: CCMapLayer) { + this.layer = layer; + if (!layer) { + this.setVisibility(false); + return; + } + + const exists = await Helper.loadTexture(layer.details.tilesetName, this.scene); + if (!exists) { + this.setVisibility(true, false); + return; + } + this.setVisibility(true); + + const tileset = this.previewTileMap.addTilesetImage('only', layer.details.tilesetName); + if (tileset) { + tileset.firstgid = 1; + } + + } + + public resetSelectedTiles() { + Globals.phaserEventsService.changeSelectedTiles.next([{id: 0, offset: {x: 0, y: 0}}]); + } + + + private onMouseRightDown() { + this.rightPointerDown = true; + if (!this.layer) { + return; + } + + // only start tile copy when cursor in bounds + const pointer = this.scene.input.activePointer; + const p = Helper.worldToTile(pointer.worldX - this.layer.x, pointer.worldY - this.layer.y); + if (!Helper.isInBounds(this.layer, p)) { + return; + } + + this.resetSelectedTiles(); + this.rightClickStart = p; + } + + private onMouseRightUp() { + this.rightPointerDown = false; + if (!this.layer) { + return; + } + + // cancel current selection when out of bounds + const phaserLayer = this.layer.getPhaserLayer(); + if (!this.rightClickStart || !this.rightClickEnd || !phaserLayer) { + this.resetSelectedTiles(); + return; + } + + // select tiles + const start = this.rightClickStart; + const end = this.rightClickEnd; + + const smaller = { + x: Math.min(start.x, end.x), + y: Math.min(start.y, end.y) + }; + + const bigger = { + x: Math.max(start.x, end.x), + y: Math.max(start.y, end.y) + }; + + const width = bigger.x - smaller.x + 1; + const height = bigger.y - smaller.y + 1; + + const tilesWithin = phaserLayer.getTilesWithin(smaller.x, smaller.y, width, height); + + const tiles: SelectedTile[] = tilesWithin.map(tile => ({ + id: tile.index, + offset: Vec2.sub(tile, smaller, true) + })); + Globals.phaserEventsService.changeSelectedTiles.next(tiles); + + this.rightClickStart = undefined; + this.rightClickEnd = undefined; + } + + private updateSelectedTiles(selected: SelectedTile[]) { + this.selectedTiles = selected; + this.renderPreview(); + + let x = 0; + let y = 0; + selected.forEach(tile => { + const o = tile.offset; + if (o.x > x) { + x = o.x; + } + if (o.y > y) { + y = o.y; + } + }); + + if (this.container) { + this.drawRect(x + 1, y + 1, 0, 0); + return; + } + } + + public drawRect(width: number, height: number, x = 0, y = 0, renderSize = false) { + if (this.selection) { + this.selection.destroy(); + } + + let textColor = 'rgba(0,0,0,0.6)'; + let backgroundColor = 0xffffff; + if (Globals.settingsService.getSettings().selectionBoxDark) { + textColor = 'rgba(255,255,255,0.9)'; + backgroundColor = 0x333333; + } + + this.selection = this.scene.add.container(x, y); + this.selection.setDepth(10); + + const rect = this.scene.add.rectangle(0, 0, width * Globals.TILE_SIZE, height * Globals.TILE_SIZE); + rect.setOrigin(0, 0); + rect.setStrokeStyle(1, backgroundColor, this.container ? 0.6 : 0.9); + + this.selection.add(rect); + this.container?.add(this.selection); + + if (!renderSize) { + Globals.globalEventsService.updateTileSelectionSize.next(undefined); + return; + } + + const makeText = (pos: Point, val: number) => { + const text = this.scene.add.text(pos.x, pos.y, Math.abs(val) + '', { + font: '400 10px Roboto', + color: textColor, + resolution: window.devicePixelRatio * 3, + }); + text.setOrigin(0.5, 0); + const background = this.scene.add.rectangle(pos.x, pos.y + 2, 14, 10, backgroundColor, 0.6); + background.setOrigin(0.5, 0); + + this.selection?.add(background); + this.selection?.add(text); + }; + + if (Math.abs(width) >= 3) { + makeText({ + x: width * Globals.TILE_SIZE / 2, + y: (height > 0 ? 0 : height * Globals.TILE_SIZE) - 1 + }, width); + } + + if (Math.abs(height) >= 3) { + makeText({ + x: Globals.TILE_SIZE / 2 + (width > 0 ? 0 : width * Globals.TILE_SIZE), + y: (height - 1) * Globals.TILE_SIZE / 2, + }, height); + } + + Globals.globalEventsService.updateTileSelectionSize.next({ + x: Math.abs(width), + y: Math.abs(height) + }); + } + + private renderPreview() { + if (!this.container) { + return; + } + this.previewTileMap.removeAllLayers(); + const layer = this.previewTileMap.createBlankLayer('layer', 'only', 0, 0, 40, 40)!; + + this.selectedTiles.forEach(tile => { + customPutTileAt(tile.id, tile.offset.x, tile.offset.y, layer.layer); + }); + + this.previewLayer = layer; + this.previewLayer.depth = this.container.depth - 1; + this.previewLayer.alpha = 0.6; + } +} diff --git a/webapp/src/app/services/phaser/entities/entity-manager.ts b/webapp/src/app/services/phaser/entities/entity-manager.ts index d3d3384d..56108751 100644 --- a/webapp/src/app/services/phaser/entities/entity-manager.ts +++ b/webapp/src/app/services/phaser/entities/entity-manager.ts @@ -83,6 +83,8 @@ export class EntityManager extends BaseObject { this.visibilityKey = keyboard!.addKey(keyCodes.R, false); this.selectionBox = new SelectionBox(this.scene); + + Globals.globalEventsService.updateEntities.subscribe(() => this.resetEntities()); } @@ -400,7 +402,10 @@ export class EntityManager extends BaseObject { } entities = (parsed as any[]).filter(v => this.isMapEntity(v)); } catch (e) { - Globals.snackbar.open('could not parse entities from clipboard', undefined, {duration: 2000}); + Globals.snackbar.open('could not parse entities from clipboard', undefined, { + duration: 2000, + panelClass: 'snackbar-error' + }); return; } if (entities.length === 0) { @@ -429,6 +434,19 @@ export class EntityManager extends BaseObject { } + private async resetEntities() { + if (!this.map) { + return; + } + const exportedMap = this.map.exportMap(); + await this.initialize(exportedMap, this.map); + if (this.active) { + for (const entity of this.entities) { + entity.setActive(true); + } + } + } + deleteSelectedEntities() { this.skipEdit = true; const saveHistory = this.selectedEntities.length > 0; diff --git a/webapp/src/app/services/phaser/entities/registry/default-entity.ts b/webapp/src/app/services/phaser/entities/registry/default-entity.ts index 70a0b3e7..3a3f597d 100644 --- a/webapp/src/app/services/phaser/entities/registry/default-entity.ts +++ b/webapp/src/app/services/phaser/entities/registry/default-entity.ts @@ -1,6 +1,10 @@ -import entities from '../../../../../assets/entities.json'; import { CCMap } from '../../tilemap/cc-map'; import { CCEntity, EntityAttributes, ScaleSettings } from '../cc-entity'; +import { Globals } from '../../../globals'; + +export interface EntitiesJson { + [key: string]: JsonEntityType; +} interface JsonEntityType { attributes: EntityAttributes; @@ -37,8 +41,15 @@ export class DefaultEntity extends CCEntity { } }; - constructor(scene: Phaser.Scene, map: CCMap, x: number, y: number, private typeName: string) { + constructor( + scene: Phaser.Scene, + map: CCMap, + x: number, + y: number, + private typeName: string + ) { super(scene, map, x, y, typeName); + const entities = Globals.jsonLoader.loadJsonMergedSync('entities.json'); this.typeDef = entities[typeName]; } diff --git a/webapp/src/app/services/phaser/entities/registry/destructible.ts b/webapp/src/app/services/phaser/entities/registry/destructible.ts index 8c2defc5..600551de 100644 --- a/webapp/src/app/services/phaser/entities/registry/destructible.ts +++ b/webapp/src/app/services/phaser/entities/registry/destructible.ts @@ -47,7 +47,7 @@ export class Destructible extends CCEntity { } protected async setupType(settings: any): Promise { - const types = this.scene.cache.json.get('destructible-types.json') as DestructibleTypes; + const types = await Globals.jsonLoader.loadJsonMerged('destructible-types.json'); this.attributes['desType'].options = {}; for (const name of Object.keys(types)) { @@ -105,7 +105,7 @@ export class Destructible extends CCEntity { const mapStyle = Helper.getMapStyle(Globals.map, 'destruct'); for (const sheet of sheets) { if (!sheet.gfx) { - sheet.gfx = mapStyle['sheet']; + sheet.gfx = mapStyle?.sheet; } const exists = await Helper.loadTexture(sheet.gfx, this.scene); if (!exists) { diff --git a/webapp/src/app/services/phaser/entities/registry/item-destruct.ts b/webapp/src/app/services/phaser/entities/registry/item-destruct.ts index 9aeff56a..0c52b2d7 100644 --- a/webapp/src/app/services/phaser/entities/registry/item-destruct.ts +++ b/webapp/src/app/services/phaser/entities/registry/item-destruct.ts @@ -2,10 +2,11 @@ import { Helper } from '../../helper'; import { Anims, AnimSheet } from '../../sheet-parser'; import { DefaultEntity } from './default-entity'; import { Point3 } from '../../../../models/cross-code-map'; -import { AttributeValue, EntityAttributes } from '../cc-entity'; +import { EntityAttributes } from '../cc-entity'; import { EnemyInfo } from './enemy'; import { SheetReference } from './destructible'; import { GlobalSettings } from '../../global-settings'; +import { Globals } from '../../../globals'; export interface ItemDestructTypes { [name: string]: ItemDestructType; @@ -58,7 +59,7 @@ export class ItemDestruct extends DefaultEntity { desType = config.desType; } } - const destructibles = this.scene.cache.json.get('destructibles.json') as ItemDestructTypes; + const destructibles = await Globals.jsonLoader.loadJsonMerged('destructibles.json'); const type = destructibles[desType]; if (!type) { this.generateNoImageType(0xFF0000, 1); diff --git a/webapp/src/app/services/phaser/helper.ts b/webapp/src/app/services/phaser/helper.ts index 1b51d43f..4de08ad6 100644 --- a/webapp/src/app/services/phaser/helper.ts +++ b/webapp/src/app/services/phaser/helper.ts @@ -1,6 +1,5 @@ -import mapStyles from '../../../../../webapp/src/assets/map-styles.json'; import { Point } from '../../models/cross-code-map'; -import { MapStyles } from '../../models/map-styles'; +import { MapStyle, MapStyles } from '../../models/map-styles'; import { Globals } from '../globals'; import { CCMap } from './tilemap/cc-map'; import { CCMapLayer } from './tilemap/cc-map-layer'; @@ -72,6 +71,10 @@ export class Helper { return JSON.parse(JSON.stringify(obj)); } + public static typedKeys(obj: T): (keyof T)[] { + return Object.keys(obj) as (keyof T)[]; + } + public static getJson(key: string, callback: (json: any) => void) { const scene = Globals.scene; @@ -146,13 +149,11 @@ export class Helper { return tag === 'input' || tag === 'textarea'; } - public static getMapStyle(map: CCMap, type: string): MapStyles { + public static getMapStyle(map: CCMap, type: keyof MapStyles): MapStyle | undefined { + const mapStyles = Globals.jsonLoader.loadJsonMergedSync('map-styles.json'); const mapStyleName = map.attributes.mapStyle || 'default'; const mapStyle = mapStyles[mapStyleName]; - if (mapStyle && mapStyle[type]) { - return mapStyle[type]; - } - return mapStyles.default[type]; + return mapStyle?.[type] ?? mapStyles.default[type]; } public static async asyncFilter(arr: T[], predicate: (v: T) => Promise) { diff --git a/webapp/src/app/services/phaser/main-scene.ts b/webapp/src/app/services/phaser/main-scene.ts index 106db6ca..a703ac2b 100644 --- a/webapp/src/app/services/phaser/main-scene.ts +++ b/webapp/src/app/services/phaser/main-scene.ts @@ -24,8 +24,6 @@ export class MainScene extends Phaser.Scene { this.load.image('pixel', 'assets/pixel.png'); this.load.image('ingame', 'assets/ingame.png'); - this.load.json('destructibles.json', 'assets/destructibles.json'); - this.load.json('destructible-types.json', 'assets/destructible-types.json'); this.load.crossOrigin = 'anonymous'; // this.load.on('progress', (val: number) => console.log(val)); diff --git a/webapp/src/app/services/phaser/tilemap/cc-map-layer.ts b/webapp/src/app/services/phaser/tilemap/cc-map-layer.ts index ba3dcea3..1b885e6b 100644 --- a/webapp/src/app/services/phaser/tilemap/cc-map-layer.ts +++ b/webapp/src/app/services/phaser/tilemap/cc-map-layer.ts @@ -33,7 +33,7 @@ export class CCMapLayer { this.container.depth = 999; this.makeLayer('stub'); this.updateBorder(); - if (details.data) { + if (details.data && details.data.length > 0) { customPutTilesAt(details.data, this.layer); } await this.updateTileset(details.tilesetName!); @@ -244,11 +244,26 @@ export class CCMapLayer { } private extractLayerData(layer: MapLayer): void { - this.layer.getTilesWithin().forEach(tile => { + for (const tile of this.layer.getTilesWithin()) { if (!layer.data[tile.y]) { layer.data[tile.y] = []; } layer.data[tile.y][tile.x] = tile.index; - }); + } + } + + setPhaserLayer(layer: Phaser.Tilemaps.TilemapLayer) { + + const oldLayer = this.layer as typeof this.layer | undefined; + + this.layer = layer; + this.layer.alpha = oldLayer?.alpha ?? 1; + this.details.width = this.layer.width / Globals.TILE_SIZE; + this.details.height = this.layer.height / Globals.TILE_SIZE; + this.setOffset(this.container.x, this.container.y); + this.updateLevel(); + if (oldLayer) { + oldLayer.destroy(true); + } } } diff --git a/webapp/src/app/services/phaser/tilemap/tile-drawer.ts b/webapp/src/app/services/phaser/tilemap/tile-drawer.ts index b11dee12..f53e5b60 100644 --- a/webapp/src/app/services/phaser/tilemap/tile-drawer.ts +++ b/webapp/src/app/services/phaser/tilemap/tile-drawer.ts @@ -9,22 +9,16 @@ import { CCMapLayer } from './cc-map-layer'; import { Filler } from './fill'; import { pointsInLine } from './points-in-line'; import { customPutTileAt } from './layer-helper'; +import { BaseTileDrawer } from '../BaseTileDrawer'; export class TileDrawer extends BaseObject { - private layer?: CCMapLayer; private selectedTiles: SelectedTile[] = []; + private layer?: CCMapLayer; - private selection?: Phaser.GameObjects.Container; - - private previewTileMap!: Phaser.Tilemaps.Tilemap; - private previewLayer?: Phaser.Tilemaps.TilemapLayer; + private baseDrawer!: BaseTileDrawer; - private rightClickStart?: Point; - private rightClickEnd?: Point; private renderLayersTransparent = false; - private rightPointerDown = false; - private container!: Phaser.GameObjects.Container; @@ -40,7 +34,9 @@ export class TileDrawer extends BaseObject { super(scene, 'tileDrawer'); } - protected init() { + + protected override init() { + this.fillKey = this.scene.input.keyboard!.addKey(Phaser.Input.Keyboard.KeyCodes.F, false); this.transparentKey = this.scene.input.keyboard!.addKey(Phaser.Input.Keyboard.KeyCodes.R, false); this.visibilityKey = this.scene.input.keyboard!.addKey(Phaser.Input.Keyboard.KeyCodes.V, false); @@ -49,15 +45,14 @@ export class TileDrawer extends BaseObject { this.container = this.scene.add.container(0, 0); this.container.depth = 10000; - this.drawRect(1, 1); - - this.resetSelectedTiles(); - - this.previewTileMap = this.scene.add.tilemap(undefined, Globals.TILE_SIZE, Globals.TILE_SIZE); + this.baseDrawer = new BaseTileDrawer(this.scene, false, this.container); + this.baseDrawer.resetSelectedTiles(); + this.scene.add.existing(this.baseDrawer); } - private selectLayer(selectedLayer?: CCMapLayer) { + private async selectLayer(selectedLayer?: CCMapLayer) { this.layer = selectedLayer; + await this.baseDrawer.setLayer(selectedLayer); this.setLayerAlpha(); @@ -66,32 +61,8 @@ export class TileDrawer extends BaseObject { return; } this.container.visible = true; - const tileset = this.previewTileMap.addTilesetImage('only', selectedLayer.details.tilesetName); - if (tileset) { - tileset.firstgid = 1; - } - } - - private updateSelectedTiles(selected: SelectedTile[]) { - this.selectedTiles = selected; - this.renderPreview(); - - let x = 0; - let y = 0; - selected.forEach(tile => { - const o = tile.offset; - if (o.x > x) { - x = o.x; - } - if (o.y > y) { - y = o.y; - } - }); - - this.drawRect(x + 1, y + 1, 0, 0); } - preUpdate(): void { // hide cursor when no map loaded if (!this.layer) { @@ -101,54 +72,6 @@ export class TileDrawer extends BaseObject { const pointer = this.scene.input.activePointer; const p = Helper.worldToTile(pointer.worldX - this.layer.x, pointer.worldY - this.layer.y); - // render selection border - if (this.rightClickStart) { - Helper.clampToBounds(this.layer, p); - - if (this.rightClickEnd && this.rightClickEnd.x === p.x && this.rightClickEnd.y === p.y) { - // shortcut to avoid redrawing rectangle every frame - return; - } - - this.rightClickEnd = p; - const diff = Vec2.sub(p, this.rightClickStart, true); - const start = {x: 0, y: 0}; - if (diff.x >= 0) { - diff.x++; - } else { - start.x = Globals.TILE_SIZE; - diff.x--; - } - if (diff.y >= 0) { - diff.y++; - } else { - start.y = Globals.TILE_SIZE; - diff.y--; - } - - this.drawRect(diff.x, diff.y, start.x, start.y, true); - return; - } - - // position tile drawer border to cursor - const container = this.container; - container.x = pointer.worldX; - container.y = pointer.worldY; - - if (container.x < this.layer.x) { - container.x -= Globals.TILE_SIZE; - } - if (container.y < this.layer.y) { - container.y -= Globals.TILE_SIZE; - } - - container.x -= (container.x - this.layer.x) % Globals.TILE_SIZE; - container.y -= (container.y - this.layer.y) % Globals.TILE_SIZE; - - if (this.previewLayer) { - Vec2.assign(this.previewLayer, container); - } - // draw tiles // trigger only when mouse is over canvas element (the renderer), avoids triggering when interacting with ui if (pointer.leftButtonDown() && pointer.downElement?.nodeName === 'CANVAS' && this.layer) { @@ -197,36 +120,13 @@ export class TileDrawer extends BaseObject { protected deactivate() { this.container.visible = false; - if (this.previewLayer) { - this.previewLayer.visible = false; - } + this.baseDrawer.setActive(false); } protected activate() { - if (this.previewLayer) { - this.previewLayer.visible = true; - } - const sub = Globals.mapLoaderService.selectedLayer.subscribe(layer => this.selectLayer(layer)); - this.addSubscription(sub); - - const sub2 = Globals.phaserEventsService.changeSelectedTiles.subscribe(tiles => this.updateSelectedTiles(tiles)); - this.addSubscription(sub2); - - const pointerDown = (pointer: Phaser.Input.Pointer) => { - if (pointer.rightButtonDown()) { - this.onMouseRightDown(); - } - }; - this.addKeybinding({event: 'pointerdown', fun: pointerDown, emitter: this.scene.input}); - - const pointerUp = (pointer: Phaser.Input.Pointer) => { - if (pointer.rightButtonReleased() && this.rightPointerDown) { - this.onMouseRightUp(); - } - }; - this.addKeybinding({event: 'pointerup', fun: pointerUp, emitter: this.scene.input}); - this.addKeybinding({event: 'pointerupoutside', fun: pointerUp, emitter: this.scene.input}); - + this.addSubscription(Globals.mapLoaderService.selectedLayer.subscribe(layer => this.selectLayer(layer))); + this.addSubscription(Globals.phaserEventsService.changeSelectedTiles.subscribe(tiles => this.selectedTiles = tiles)); + this.baseDrawer.setActive(true); const fill = () => { if (!Helper.isInputFocused()) { @@ -288,159 +188,6 @@ export class TileDrawer extends BaseObject { } } - private onMouseRightDown() { - this.rightPointerDown = true; - if (!this.layer) { - return; - } - - - // only start tile copy when cursor in bounds - const pointer = this.scene.input.activePointer; - const p = Helper.worldToTile(pointer.worldX - this.layer.x, pointer.worldY - this.layer.y); - if (!Helper.isInBounds(this.layer, p)) { - return; - } - - this.resetSelectedTiles(); - this.renderPreview(); - this.rightClickStart = p; - } - - private drawRect(width: number, height: number, x = 0, y = 0, renderSize = false) { - if (this.selection) { - this.selection.destroy(); - } - - let textColor = 'rgba(0,0,0,0.6)'; - let backgroundColor = 0xffffff; - if (Globals.settingsService.getSettings().selectionBoxDark) { - textColor = 'rgba(255,255,255,0.9)'; - backgroundColor = 0x333333; - } - - this.selection = this.scene.add.container(x, y); - - const rect = this.scene.add.rectangle(0, 0, width * Globals.TILE_SIZE, height * Globals.TILE_SIZE); - rect.setOrigin(0, 0); - rect.setStrokeStyle(1, backgroundColor, 0.6); - - this.selection.add(rect); - this.container.add(this.selection); - - if (!renderSize) { - Globals.globalEventsService.updateTileSelectionSize.next(undefined); - return; - } - - const makeText = (pos: Point, val: number) => { - const text = this.scene.add.text(pos.x, pos.y, Math.abs(val) + '', { - font: '400 10px Roboto', - color: textColor, - resolution: window.devicePixelRatio * 3, - }); - text.setOrigin(0.5, 0); - const background = this.scene.add.rectangle(pos.x, pos.y + 2, 14, 10, backgroundColor, 0.6); - background.setOrigin(0.5, 0); - - this.selection?.add(background); - this.selection?.add(text); - }; - - if (Math.abs(width) >= 3) { - makeText({ - x: width * Globals.TILE_SIZE / 2, - y: (height > 0 ? 0 : height * Globals.TILE_SIZE) - 1 - }, width); - } - - if (Math.abs(height) >= 3) { - makeText({ - x: Globals.TILE_SIZE / 2 + (width > 0 ? 0 : width * Globals.TILE_SIZE), - y: (height - 1) * Globals.TILE_SIZE / 2, - }, height); - } - - Globals.globalEventsService.updateTileSelectionSize.next({ - x: Math.abs(width), - y: Math.abs(height) - }); - } - - private onMouseRightUp() { - this.rightPointerDown = false; - if (!this.layer) { - return; - } - this.selectedTiles = []; - - // cancel current selection when out of bounds - const phaserLayer = this.layer.getPhaserLayer(); - if (!this.rightClickStart || !this.rightClickEnd || !phaserLayer) { - this.drawRect(1, 1); - this.resetSelectedTiles(); - this.renderPreview(); - return; - } - - // select tiles - const start = this.rightClickStart; - const end = this.rightClickEnd; - - const smaller = { - x: Math.min(start.x, end.x), - y: Math.min(start.y, end.y) - }; - - const bigger = { - x: Math.max(start.x, end.x), - y: Math.max(start.y, end.y) - }; - - const width = bigger.x - smaller.x + 1; - const height = bigger.y - smaller.y + 1; - - const tilesWithin = phaserLayer.getTilesWithin(smaller.x, smaller.y, width, height); - - tilesWithin.forEach((tile: Phaser.Tilemaps.Tile) => { - this.selectedTiles.push({ - id: tile.index, - offset: Vec2.sub(tile, smaller, true) - }); - }); - - this.renderPreview(); - - this.drawRect(width, height); - - - this.rightClickStart = undefined; - this.rightClickEnd = undefined; - } - - private renderPreview() { - - // reset last draw when selected tiles change - this.lastDraw.x = -1; - this.previewTileMap.removeAllLayers(); - const layer = this.previewTileMap.createBlankLayer('layer', 'only', 0, 0, 40, 40)!; - - this.selectedTiles.forEach(tile => { - customPutTileAt(tile.id, tile.offset.x, tile.offset.y, layer.layer); - }); - - this.previewLayer = layer; - this.previewLayer.depth = this.container.depth - 1; - this.previewLayer.alpha = 0.6; - - // TODO: phaser bug fix, see https://github.com/photonstorm/phaser/issues/4642 - this.previewTileMap.layers = [this.previewLayer.layer]; - } - - private resetSelectedTiles() { - this.selectedTiles = [{id: 0, offset: {x: 0, y: 0}}]; - } - private fill() { if (!this.layer) { return; diff --git a/webapp/src/app/services/save.service.ts b/webapp/src/app/services/save.service.ts index e376f73e..c8dbd03b 100644 --- a/webapp/src/app/services/save.service.ts +++ b/webapp/src/app/services/save.service.ts @@ -51,7 +51,7 @@ export class SaveService { this.snackbar.open('successfully saved map', 'ok', { duration: 3000 }); }, error: err => { console.error(err); - this.snackbar.open('failed to save map', 'ok'); + this.snackbar.open('failed to save map', 'ok', {panelClass: 'snackbar-error'}); } }); } diff --git a/webapp/src/assets/autotiles.json b/webapp/src/assets/autotiles.json index fe3b0343..5a589257 100644 --- a/webapp/src/assets/autotiles.json +++ b/webapp/src/assets/autotiles.json @@ -1,395 +1,2579 @@ [ + { + "map": "media/map/arid.png", + "tileCountX": 32, + "autotiles": [ + { + "base": { + "x": 1, + "y": 0 + }, + "size": { + "x": 10, + "y": 2 + } + }, + { + "base": { + "x": 11, + "y": 0 + }, + "size": { + "x": 8, + "y": 2 + } + }, + { + "base": { + "x": 13, + "y": 13 + }, + "size": { + "x": 4, + "y": 4 + } + } + ] + }, + { + "map": "media/map/arid-interior.png", + "tileCountX": 32, + "autotiles": [ + { + "base": { + "x": 1, + "y": 0 + }, + "size": { + "x": 10, + "y": 2 + } + }, + { + "base": { + "x": 11, + "y": 0 + }, + "size": { + "x": 10, + "y": 2 + } + }, + { + "base": { + "x": 24, + "y": 19 + }, + "size": { + "x": 4, + "y": 4 + } + }, + { + "base": { + "x": 24, + "y": 27 + }, + "size": { + "x": 8, + "y": 2 + } + }, + { + "base": { + "x": 24, + "y": 29 + }, + "size": { + "x": 8, + "y": 2 + } + }, + { + "base": { + "x": 24, + "y": 31 + }, + "size": { + "x": 8, + "y": 2 + } + }, + { + "base": { + "x": 24, + "y": 33 + }, + "size": { + "x": 8, + "y": 2 + } + } + ] + }, { "map": "media/map/autumn-outside.png", "tileCountX": 32, "autotiles": [ { - "type": "MEGA_LARGE", "base": { - "x": 0, - "y": 1 + "x": 0, + "y": 1 + }, + "size": { + "x": 14, + "y": 2 + }, + "cliff": false + }, + { + "base": { + "x": 12, + "y": 16 + }, + "size": { + "x": 4, + "y": 4 + } + }, + { + "base": { + "x": 8, + "y": 34 + }, + "size": { + "x": 8, + "y": 2 + } + }, + { + "base": { + "x": 8, + "y": 36 + }, + "size": { + "x": 8, + "y": 2 + } + } + ] + }, + { + "map": "media/map/beach.png", + "tileCountX": 32, + "autotiles": [ + { + "base": { + "x": 0, + "y": 0 + }, + "size": { + "x": 12, + "y": 2 + } + }, + { + "base": { + "x": 12, + "y": 0 + }, + "size": { + "x": 10, + "y": 2 + } + }, + { + "base": { + "x": 10, + "y": 2 + }, + "size": { + "x": 8, + "y": 2 + } + }, + { + "base": { + "x": 0, + "y": 2 + }, + "size": { + "x": 10, + "y": 2 + } + }, + { + "base": { + "x": 4, + "y": 17 + }, + "size": { + "x": 10, + "y": 2 + } + }, + { + "base": { + "x": 2, + "y": 19 + }, + "size": { + "x": 10, + "y": 2 + } + }, + { + "base": { + "x": 2, + "y": 21 + }, + "size": { + "x": 8, + "y": 2 + } + }, + { + "base": { + "x": 1, + "y": 25 + }, + "size": { + "x": 8, + "y": 2 + } + }, + { + "base": { + "x": 6, + "y": 31 + }, + "size": { + "x": 4, + "y": 4 + } + }, + { + "base": { + "x": 0, + "y": 27 + }, + "size": { + "x": 8, + "y": 2 + } + }, + { + "base": { + "x": 0, + "y": 29 + }, + "size": { + "x": 8, + "y": 2 + } + }, + { + "base": { + "x": 8, + "y": 29 + }, + "size": { + "x": 8, + "y": 2 + } + } + ] + }, + { + "map": "media/map/bergen-trail.png", + "tileCountX": 32, + "autotiles": [ + { + "base": { + "x": 1, + "y": 0 + }, + "size": { + "x": 10, + "y": 2 + } + }, + { + "base": { + "x": 1, + "y": 2 + }, + "size": { + "x": 8, + "y": 2 + } + }, + { + "base": { + "x": 11, + "y": 0 + }, + "size": { + "x": 8, + "y": 2 + } + }, + { + "base": { + "x": 22, + "y": 0 + }, + "size": { + "x": 10, + "y": 2 + } + }, + { + "base": { + "x": 23, + "y": 5 + }, + "size": { + "x": 8, + "y": 2 + } + }, + { + "base": { + "x": 23, + "y": 7 + }, + "size": { + "x": 8, + "y": 2 + } + }, + { + "base": { + "x": 22, + "y": 9 + }, + "size": { + "x": 10, + "y": 2 + } + }, + { + "base": { + "x": 13, + "y": 13 + }, + "size": { + "x": 4, + "y": 4 + } + }, + { + "base": { + "x": 0, + "y": 40 + }, + "size": { + "x": 8, + "y": 2 + } + }, + { + "base": { + "x": 0, + "y": 42 + }, + "size": { + "x": 8, + "y": 2 + } + }, + { + "base": { + "x": 18, + "y": 36 + }, + "size": { + "x": 12, + "y": 2 + } + }, + { + "base": { + "x": 18, + "y": 38 + }, + "size": { + "x": 12, + "y": 2 + } + }, + { + "base": { + "x": 0, + "y": 56 + }, + "size": { + "x": 10, + "y": 2 + } + } + ] + }, + { + "map": "media/map/cave.png", + "tileCountX": 32, + "autotiles": [ + { + "base": { + "x": 0, + "y": 1 + }, + "size": { + "x": 8, + "y": 2 + } + }, + { + "base": { + "x": 8, + "y": 1 + }, + "size": { + "x": 10, + "y": 2 + } + }, + { + "base": { + "x": 18, + "y": 1 + }, + "size": { + "x": 10, + "y": 2 + } + }, + { + "base": { + "x": 16, + "y": 19 + }, + "size": { + "x": 4, + "y": 4 + } + }, + { + "base": { + "x": 0, + "y": 32 + }, + "size": { + "x": 8, + "y": 2 + } + }, + { + "base": { + "x": 0, + "y": 34 + }, + "size": { + "x": 8, + "y": 2 + } + } + ] + }, + { + "map": "media/map/cold-dng.png", + "tileCountX": 32, + "autotiles": [ + { + "base": { + "x": 0, + "y": 1 + }, + "size": { + "x": 8, + "y": 2 + } + }, + { + "base": { + "x": 12, + "y": 0 + }, + "size": { + "x": 8, + "y": 2 + } + }, + { + "base": { + "x": 12, + "y": 2 + }, + "size": { + "x": 8, + "y": 2 + } + }, + { + "base": { + "x": 20, + "y": 0 + }, + "size": { + "x": 8, + "y": 2 + } + }, + { + "base": { + "x": 20, + "y": 2 + }, + "size": { + "x": 8, + "y": 2 + } + }, + { + "base": { + "x": 17, + "y": 19 + }, + "size": { + "x": 4, + "y": 4 + } + }, + { + "base": { + "x": 17, + "y": 23 + }, + "size": { + "x": 4, + "y": 4 + } + }, + { + "base": { + "x": 20, + "y": 27 + }, + "size": { + "x": 12, + "y": 2 + } + }, + { + "base": { + "x": 21, + "y": 40 + }, + "size": { + "x": 8, + "y": 2 + } + }, + { + "base": { + "x": 24, + "y": 47 + }, + "size": { + "x": 8, + "y": 2 + } + } + ] + }, + { + "map": "media/map/cold-dng-shadow.png", + "tileCountX": 16, + "autotiles": [ + { + "base": { + "x": 3, + "y": 8 + }, + "size": { + "x": 8, + "y": 2 + } + }, + { + "base": { + "x": 3, + "y": 10 + }, + "size": { + "x": 8, + "y": 2 + } + }, + { + "base": { + "x": 3, + "y": 12 + }, + "size": { + "x": 8, + "y": 2 + } + }, + { + "base": { + "x": 3, + "y": 14 + }, + "size": { + "x": 8, + "y": 2 + } + } + ] + }, + { + "map": "media/map/dungeon-shadow.png", + "tileCountX": 16, + "autotiles": [ + { + "base": { + "x": 3, + "y": 8 + }, + "size": { + "x": 8, + "y": 2 + } + }, + { + "base": { + "x": 3, + "y": 10 + }, + "size": { + "x": 8, + "y": 2 + } + }, + { + "base": { + "x": 3, + "y": 12 + }, + "size": { + "x": 8, + "y": 2 + } + }, + { + "base": { + "x": 3, + "y": 14 + }, + "size": { + "x": 8, + "y": 2 + } + } + ] + }, + { + "map": "media/map/evo-village.png", + "tileCountX": 32, + "autotiles": [ + { + "base": { + "x": 0, + "y": 1 + }, + "size": { + "x": 14, + "y": 2 + } + }, + { + "base": { + "x": 0, + "y": 3 + }, + "size": { + "x": 12, + "y": 2 + } + }, + { + "base": { + "x": 18, + "y": 3 + }, + "size": { + "x": 4, + "y": 4 + } + }, + { + "base": { + "x": 0, + "y": 16 + }, + "size": { + "x": 12, + "y": 2 + } + }, + { + "base": { + "x": 0, + "y": 22 + }, + "size": { + "x": 8, + "y": 2 + } + } + ] + }, + { + "map": "media/map/evo-village-interior.png", + "tileCountX": 32, + "autotiles": [ + { + "base": { + "x": 0, + "y": 5 + }, + "size": { + "x": 4, + "y": 4 + } + } + ] + }, + { + "map": "media/map/final-dungeon-inner.png", + "tileCountX": 32, + "autotiles": [ + { + "base": { + "x": 1, + "y": 0 + }, + "size": { + "x": 8, + "y": 2 + } + }, + { + "base": { + "x": 9, + "y": 0 + }, + "size": { + "x": 8, + "y": 2 + } + }, + { + "base": { + "x": 10, + "y": 11 + }, + "size": { + "x": 4, + "y": 4 + } + }, + { + "base": { + "x": 15, + "y": 11 + }, + "size": { + "x": 4, + "y": 4 + } + } + ] + }, + { + "map": "media/map/final-dungeon-outer.png", + "tileCountX": 32, + "autotiles": [ + { + "base": { + "x": 1, + "y": 0 + }, + "size": { + "x": 10, + "y": 2 + } + }, + { + "base": { + "x": 1, + "y": 2 + }, + "size": { + "x": 10, + "y": 2 + } + }, + { + "base": { + "x": 12, + "y": 0 + }, + "size": { + "x": 8, + "y": 2 + } + }, + { + "base": { + "x": 12, + "y": 2 + }, + "size": { + "x": 8, + "y": 2 + } + }, + { + "base": { + "x": 20, + "y": 0 + }, + "size": { + "x": 8, + "y": 2 + } + }, + { + "base": { + "x": 20, + "y": 2 + }, + "size": { + "x": 8, + "y": 2 + } + }, + { + "base": { + "x": 20, + "y": 4 + }, + "size": { + "x": 8, + "y": 2 + } + }, + { + "base": { + "x": 12, + "y": 4 + }, + "size": { + "x": 8, + "y": 2 + } + }, + { + "base": { + "x": 16, + "y": 9 + }, + "size": { + "x": 4, + "y": 4 + } + }, + { + "base": { + "x": 17, + "y": 26 + }, + "size": { + "x": 12, + "y": 2 + } + } + ] + }, + { + "map": "media/map/forest.png", + "tileCountX": 32, + "autotiles": [ + { + "base": { + "x": 1, + "y": 0 + }, + "size": { + "x": 10, + "y": 2 + } + }, + { + "base": { + "x": 11, + "y": 0 + }, + "size": { + "x": 10, + "y": 2 + } + }, + { + "base": { + "x": 21, + "y": 0 + }, + "size": { + "x": 8, + "y": 2 + } + }, + { + "base": { + "x": 9, + "y": 2 + }, + "size": { + "x": 10, + "y": 2 + } + }, + { + "base": { + "x": 0, + "y": 50 + }, + "size": { + "x": 8, + "y": 2 + } + }, + { + "base": { + "x": 0, + "y": 52 + }, + "size": { + "x": 8, + "y": 2 + } + }, + { + "base": { + "x": 4, + "y": 54 + }, + "size": { + "x": 4, + "y": 4 + } + }, + { + "base": { + "x": 0, + "y": 62 + }, + "size": { + "x": 8, + "y": 2 + } + } + ] + }, + { + "map": "media/map/forest-dng.png", + "tileCountX": 32, + "autotiles": [ + { + "base": { + "x": 10, + "y": 11 + }, + "size": { + "x": 4, + "y": 4 + } + }, + { + "base": { + "x": 15, + "y": 11 + }, + "size": { + "x": 4, + "y": 4 + } + }, + { + "base": { + "x": 1, + "y": 0 + }, + "size": { + "x": 8, + "y": 2 + } + }, + { + "base": { + "x": 9, + "y": 0 + }, + "size": { + "x": 8, + "y": 2 + } + }, + { + "base": { + "x": 3, + "y": 2 + }, + "size": { + "x": 8, + "y": 2 + } + } + ] + }, + { + "map": "media/map/glass-parallax.png", + "tileCountX": 32, + "autotiles": [ + { + "base": { + "x": 20, + "y": 4 + }, + "size": { + "x": 12, + "y": 2 + } + }, + { + "base": { + "x": 20, + "y": 6 + }, + "size": { + "x": 12, + "y": 2 + } + }, + { + "base": { + "x": 20, + "y": 8 + }, + "size": { + "x": 12, + "y": 2 + } + }, + { + "base": { + "x": 20, + "y": 10 + }, + "size": { + "x": 12, + "y": 2 + } + }, + { + "base": { + "x": 0, + "y": 8 + }, + "size": { + "x": 12, + "y": 2 + } + }, + { + "base": { + "x": 0, + "y": 10 + }, + "size": { + "x": 12, + "y": 2 + } + }, + { + "base": { + "x": 0, + "y": 12 + }, + "size": { + "x": 12, + "y": 2 + } + }, + { + "base": { + "x": 0, + "y": 14 + }, + "size": { + "x": 12, + "y": 2 + } + }, + { + "base": { + "x": 12, + "y": 12 + }, + "size": { + "x": 12, + "y": 2 + } + }, + { + "base": { + "x": 12, + "y": 14 + }, + "size": { + "x": 12, + "y": 2 + } + } + ] + }, + { + "map": "media/map/heat-area.png", + "tileCountX": 32, + "autotiles": [ + { + "base": { + "x": 1, + "y": 0 + }, + "size": { + "x": 10, + "y": 2 + } + }, + { + "base": { + "x": 1, + "y": 2 + }, + "size": { + "x": 8, + "y": 2 + } + }, + { + "base": { + "x": 11, + "y": 0 + }, + "size": { + "x": 10, + "y": 2 + } + }, + { + "base": { + "x": 11, + "y": 2 + }, + "size": { + "x": 8, + "y": 2 + } + }, + { + "base": { + "x": 21, + "y": 0 + }, + "size": { + "x": 10, + "y": 2 + } + }, + { + "base": { + "x": 21, + "y": 2 + }, + "size": { + "x": 10, + "y": 2 + } + }, + { + "base": { + "x": 24, + "y": 8 + }, + "size": { + "x": 8, + "y": 2 + } + }, + { + "base": { + "x": 24, + "y": 36 + }, + "size": { + "x": 8, + "y": 2 + } + }, + { + "base": { + "x": 24, + "y": 38 + }, + "size": { + "x": 8, + "y": 2 + } + }, + { + "base": { + "x": 6, + "y": 38 + }, + "size": { + "x": 8, + "y": 2 + } + }, + { + "base": { + "x": 6, + "y": 40 + }, + "size": { + "x": 8, + "y": 2 + } + }, + { + "base": { + "x": 6, + "y": 42 + }, + "size": { + "x": 4, + "y": 4 + } + }, + { + "base": { + "x": 2, + "y": 46 + }, + "size": { + "x": 10, + "y": 2 + } + }, + { + "base": { + "x": 0, + "y": 50 + }, + "size": { + "x": 8, + "y": 2 + } + } + ] + }, + { + "map": "media/map/heat-area-hax.png", + "tileCountX": 32, + "autotiles": [ + { + "base": { + "x": 0, + "y": 3 + }, + "size": { + "x": 8, + "y": 2 + } + } + ] + }, + { + "map": "media/map/heat-dng.png", + "tileCountX": 32, + "autotiles": [ + { + "base": { + "x": 1, + "y": 0 + }, + "size": { + "x": 8, + "y": 2 + } + }, + { + "base": { + "x": 17, + "y": 0 + }, + "size": { + "x": 8, + "y": 2 + } + }, + { + "base": { + "x": 1, + "y": 2 + }, + "size": { + "x": 10, + "y": 2 + } + }, + { + "base": { + "x": 9, + "y": 0 + }, + "size": { + "x": 8, + "y": 2 + } + }, + { + "base": { + "x": 11, + "y": 2 + }, + "size": { + "x": 8, + "y": 2 + } + }, + { + "base": { + "x": 19, + "y": 2 + }, + "size": { + "x": 8, + "y": 2 + } + }, + { + "base": { + "x": 10, + "y": 11 + }, + "size": { + "x": 4, + "y": 4 + } + }, + { + "base": { + "x": 15, + "y": 11 + }, + "size": { + "x": 4, + "y": 4 + } + }, + { + "base": { + "x": 6, + "y": 30 + }, + "size": { + "x": 10, + "y": 2 + } + }, + { + "base": { + "x": 0, + "y": 40 + }, + "size": { + "x": 8, + "y": 2 + } + } + ] + }, + { + "map": "media/map/heat-interior.png", + "tileCountX": 32, + "autotiles": [ + { + "base": { + "x": 11, + "y": 4 + }, + "size": { + "x": 8, + "y": 2 + } + }, + { + "base": { + "x": 6, + "y": 5 + }, + "size": { + "x": 4, + "y": 4 + } + } + ] + }, + { + "map": "media/map/heat-interior-shadow.png", + "tileCountX": 16, + "autotiles": [ + { + "base": { + "x": 3, + "y": 8 + }, + "size": { + "x": 8, + "y": 2 + } + }, + { + "base": { + "x": 3, + "y": 10 + }, + "size": { + "x": 8, + "y": 2 + } + } + ] + }, + { + "map": "media/map/jungle.png", + "tileCountX": 32, + "autotiles": [ + { + "base": { + "x": 6, + "y": 0 + }, + "size": { + "x": 14, + "y": 2 + } + }, + { + "base": { + "x": 20, + "y": 0 + }, + "size": { + "x": 12, + "y": 2 + } + }, + { + "base": { + "x": 20, + "y": 11 + }, + "size": { + "x": 12, + "y": 2 + } + }, + { + "base": { + "x": 20, + "y": 13 + }, + "size": { + "x": 12, + "y": 2 + } + }, + { + "base": { + "x": 20, + "y": 15 + }, + "size": { + "x": 12, + "y": 2 + } + }, + { + "base": { + "x": 20, + "y": 17 + }, + "size": { + "x": 12, + "y": 2 + } + }, + { + "base": { + "x": 20, + "y": 19 + }, + "size": { + "x": 12, + "y": 2 + } + }, + { + "base": { + "x": 20, + "y": 21 + }, + "size": { + "x": 12, + "y": 2 + } + }, + { + "base": { + "x": 20, + "y": 23 + }, + "size": { + "x": 12, + "y": 2 + } + }, + { + "base": { + "x": 20, + "y": 25 + }, + "size": { + "x": 12, + "y": 2 + } + }, + { + "base": { + "x": 7, + "y": 25 + }, + "size": { + "x": 4, + "y": 4 + } + }, + { + "base": { + "x": 11, + "y": 32 + }, + "size": { + "x": 8, + "y": 2 + } + }, + { + "base": { + "x": 16, + "y": 47 + }, + "size": { + "x": 8, + "y": 2 + } + }, + { + "base": { + "x": 24, + "y": 47 + }, + "size": { + "x": 8, + "y": 2 + } + }, + { + "base": { + "x": 9, + "y": 49 + }, + "size": { + "x": 8, + "y": 2 + } + }, + { + "base": { + "x": 24, + "y": 49 + }, + "size": { + "x": 8, + "y": 2 + } + }, + { + "base": { + "x": 24, + "y": 49 + }, + "size": { + "x": 8, + "y": 2 + } + }, + { + "base": { + "x": 28, + "y": 51 + }, + "size": { + "x": 4, + "y": 4 + } + } + ] + }, + { + "map": "media/map/jungle-boss.png", + "tileCountX": 32, + "autotiles": [ + { + "base": { + "x": 0, + "y": 0 + }, + "size": { + "x": 12, + "y": 2 + } + }, + { + "base": { + "x": 0, + "y": 2 + }, + "size": { + "x": 12, + "y": 2 + } + }, + { + "base": { + "x": 0, + "y": 4 + }, + "size": { + "x": 12, + "y": 2 + } + }, + { + "base": { + "x": 0, + "y": 6 + }, + "size": { + "x": 12, + "y": 2 + } + } + ] + }, + { + "map": "media/map/jungle-interior.png", + "tileCountX": 32, + "autotiles": [ + { + "base": { + "x": 3, + "y": 5 + }, + "size": { + "x": 8, + "y": 2 + } + } + ] + }, + { + "map": "media/map/jungle-interior-shadow.png", + "tileCountX": 16, + "autotiles": [ + { + "base": { + "x": 3, + "y": 8 + }, + "size": { + "x": 8, + "y": 2 + } + }, + { + "base": { + "x": 3, + "y": 10 + }, + "size": { + "x": 8, + "y": 2 + } + } + ] + }, + { + "map": "media/map/jungle-shadow.png", + "tileCountX": 16, + "autotiles": [ + { + "base": { + "x": 0, + "y": 0 + }, + "size": { + "x": 12, + "y": 2 + } + }, + { + "base": { + "x": 0, + "y": 4 + }, + "size": { + "x": 8, + "y": 2 + } + }, + { + "base": { + "x": 0, + "y": 6 + }, + "size": { + "x": 8, + "y": 2 + } + }, + { + "base": { + "x": 0, + "y": 8 + }, + "size": { + "x": 8, + "y": 2 + } + }, + { + "base": { + "x": 0, + "y": 10 + }, + "size": { + "x": 8, + "y": 2 + } + } + ] + }, + { + "map": "media/map/lab.png", + "tileCountX": 32, + "autotiles": [ + { + "base": { + "x": 0, + "y": 1 + }, + "size": { + "x": 8, + "y": 2 + } + }, + { + "base": { + "x": 12, + "y": 0 + }, + "size": { + "x": 8, + "y": 2 + } + }, + { + "base": { + "x": 12, + "y": 2 + }, + "size": { + "x": 8, + "y": 2 + } + }, + { + "base": { + "x": 20, + "y": 0 + }, + "size": { + "x": 8, + "y": 2 + } + }, + { + "base": { + "x": 20, + "y": 2 + }, + "size": { + "x": 8, + "y": 2 + } + }, + { + "base": { + "x": 0, + "y": 17 + }, + "size": { + "x": 4, + "y": 4 + } + }, + { + "base": { + "x": 11, + "y": 23 + }, + "size": { + "x": 8, + "y": 2 + } + }, + { + "base": { + "x": 11, + "y": 25 + }, + "size": { + "x": 8, + "y": 2 + } + }, + { + "base": { + "x": 11, + "y": 27 + }, + "size": { + "x": 8, + "y": 2 + } + }, + { + "base": { + "x": 0, + "y": 34 + }, + "size": { + "x": 14, + "y": 2 + } + }, + { + "base": { + "x": 20, + "y": 45 + }, + "size": { + "x": 12, + "y": 2 + } + } + ] + }, + { + "map": "media/map/observatory-inner.png", + "tileCountX": 32, + "autotiles": [ + { + "base": { + "x": 1, + "y": 0 + }, + "size": { + "x": 8, + "y": 2 + } + }, + { + "base": { + "x": 9, + "y": 0 + }, + "size": { + "x": 8, + "y": 2 + } + }, + { + "base": { + "x": 17, + "y": 0 + }, + "size": { + "x": 8, + "y": 2 + } + }, + { + "base": { + "x": 1, + "y": 2 + }, + "size": { + "x": 10, + "y": 2 + } + }, + { + "base": { + "x": 11, + "y": 2 + }, + "size": { + "x": 8, + "y": 2 + } + }, + { + "base": { + "x": 19, + "y": 2 + }, + "size": { + "x": 8, + "y": 2 + } + }, + { + "base": { + "x": 10, + "y": 11 + }, + "size": { + "x": 4, + "y": 4 + } + }, + { + "base": { + "x": 15, + "y": 11 + }, + "size": { + "x": 4, + "y": 4 + } + }, + { + "base": { + "x": 6, + "y": 30 + }, + "size": { + "x": 10, + "y": 2 + } + }, + { + "base": { + "x": 0, + "y": 40 + }, + "size": { + "x": 8, + "y": 2 + } + } + ] + }, + { + "map": "media/map/old-hideout.png", + "tileCountX": 32, + "autotiles": [ + { + "base": { + "x": 1, + "y": 0 + }, + "size": { + "x": 10, + "y": 2 + } + }, + { + "base": { + "x": 11, + "y": 0 + }, + "size": { + "x": 10, + "y": 2 + } + }, + { + "base": { + "x": 21, + "y": 0 + }, + "size": { + "x": 8, + "y": 2 + } + }, + { + "base": { + "x": 9, + "y": 2 + }, + "size": { + "x": 10, + "y": 2 + } + } + ] + }, + { + "map": "media/map/old-hideout-shadows.png", + "tileCountX": 16, + "autotiles": [ + { + "base": { + "x": 3, + "y": 8 + }, + "size": { + "x": 8, + "y": 2 + } + }, + { + "base": { + "x": 3, + "y": 10 + }, + "size": { + "x": 8, + "y": 2 + } + }, + { + "base": { + "x": 3, + "y": 12 + }, + "size": { + "x": 8, + "y": 2 + } + }, + { + "base": { + "x": 3, + "y": 14 + }, + "size": { + "x": 8, + "y": 2 + } + } + ] + }, + { + "map": "media/map/old-hideout-shadows2.png", + "tileCountX": 16, + "autotiles": [ + { + "base": { + "x": 3, + "y": 8 + }, + "size": { + "x": 8, + "y": 2 + } + }, + { + "base": { + "x": 3, + "y": 10 + }, + "size": { + "x": 8, + "y": 2 + } + } + ] + }, + { + "map": "media/map/raid-boss.png", + "tileCountX": 32, + "autotiles": [ + { + "base": { + "x": 5, + "y": 5 + }, + "size": { + "x": 8, + "y": 2 + } + }, + { + "base": { + "x": 5, + "y": 7 + }, + "size": { + "x": 8, + "y": 2 + } + } + ] + }, + { + "map": "media/map/rh-interior.png", + "tileCountX": 32, + "autotiles": [ + { + "base": { + "x": 6, + "y": 5 + }, + "size": { + "x": 4, + "y": 4 + } + }, + { + "base": { + "x": 11, + "y": 4 + }, + "size": { + "x": 8, + "y": 2 + } + } + ] + }, + { + "map": "media/map/rh-interior-shadow.png", + "tileCountX": 16, + "autotiles": [ + { + "base": { + "x": 3, + "y": 8 + }, + "size": { + "x": 8, + "y": 2 + } + }, + { + "base": { + "x": 3, + "y": 10 + }, + "size": { + "x": 8, + "y": 2 } } ] }, { - "map": "media/map/bergen-trail.png", + "map": "media/map/rhombus-dungeon.png", "tileCountX": 32, "autotiles": [ { - "type": "LARGE", "base": { - "x": 1, + "x": 17, "y": 0 }, - "cliff": { - "x": 6, - "y": 4 + "size": { + "x": 8, + "y": 2 } }, { - "type": "DEFAULT", "base": { - "x": 1, + "x": 17, "y": 2 }, - "cliff": { - "x": 12, - "y": 4 + "size": { + "x": 8, + "y": 2 } } ] }, { - "map": "media/map/jungle.png", + "map": "media/map/rhombus-outside.png", "tileCountX": 32, "autotiles": [ { - "type": "SMALL", - "base": { - "x": 0, - "y": 0 - } - }, - { - "type": "SMALL", "base": { "x": 0, - "y": 1 - } - }, - { - "type": "MEGA_LARGE", - "base": { - "x": 6, "y": 0 }, - "cliff": { - "x": 18, - "y": 4 + "size": { + "x": 12, + "y": 2 } }, { - "type": "SUPER_LARGE", "base": { - "x": 20, - "y": 0 + "x": 13, + "y": 15 }, - "cliff": { - "x": 18, + "size": { + "x": 4, "y": 4 } }, { - "type": "SUPER_LARGE", - "base": { - "x": 20, - "y": 11 - }, - "mergeWithEmpty": false - }, - { - "type": "SUPER_LARGE", - "base": { - "x": 20, - "y": 13 - }, - "mergeWithEmpty": false - }, - { - "type": "SUPER_LARGE", - "base": { - "x": 20, - "y": 15 - }, - "mergeWithEmpty": false - }, - { - "type": "SUPER_LARGE", - "base": { - "x": 20, - "y": 17 - }, - "mergeWithEmpty": false - }, - { - "type": "SUPER_LARGE", "base": { - "x": 20, + "x": 0, "y": 19 }, - "mergeWithEmpty": false - }, - { - "type": "SUPER_LARGE", - "base": { - "x": 20, - "y": 21 - }, - "mergeWithEmpty": false + "size": { + "x": 10, + "y": 2 + } }, { - "type": "SUPER_LARGE", "base": { - "x": 20, - "y": 23 + "x": 6, + "y": 32 }, - "mergeWithEmpty": false + "size": { + "x": 8, + "y": 2 + } }, { - "type": "SUPER_LARGE", "base": { - "x": 20, - "y": 25 + "x": 0, + "y": 40 }, - "mergeWithEmpty": false + "size": { + "x": 8, + "y": 2 + } } ] }, { - "map": "media/map/arid.png", - "tileCountX": 32, + "map": "media/map/rombus-shadows.png", + "tileCountX": 16, "autotiles": [ { - "type": "LARGE", "base": { - "x": 1, + "x": 0, "y": 0 }, - "cliff": { - "x": 6, - "y": 4 - } - }, - { - "type": "DEFAULT", - "base": { - "x": 11, - "y": 0 + "size": { + "x": 10, + "y": 2 } } ] }, { - "map": "media/map/arid-interior.png", + "map": "media/map/rookie-harbor.png", "tileCountX": 32, "autotiles": [ { - "type": "LARGE", "base": { "x": 1, "y": 0 + }, + "size": { + "x": 8, + "y": 2 } }, { - "type": "LARGE", "base": { - "x": 11, + "x": 13, "y": 0 - } - } - ] - }, - { - "map": "media/map/cave.png", - "tileCountX": 32, - "autotiles": [ - { - "type": "DEFAULT", - "base": { - "x": 0, - "y": 1 - } - }, - { - "type": "LARGE", - "base": { - "x": 8, - "y": 1 }, - "cliff": { - "x": 18, + "size": { + "x": 4, "y": 4 } }, { - "type": "LARGE", - "base": { - "x": 18, - "y": 1 - } - } - ] - }, - { - "map": "media/map/forest.png", - "tileCountX": 32, - "autotiles": [ - { - "type": "LARGE", "base": { - "x": 1, + "x": 17, "y": 0 + }, + "size": { + "x": 4, + "y": 4 } }, { - "type": "LARGE", "base": { - "x": 11, - "y": 0 + "x": 1, + "y": 2 + }, + "size": { + "x": 8, + "y": 2 } }, { - "type": "DEFAULT", "base": { - "x": 21, - "y": 0 + "x": 14, + "y": 18 + }, + "size": { + "x": 4, + "y": 4 } }, { - "type": "LARGE", "base": { - "x": 9, + "x": 11, + "y": 45 + }, + "size": { + "x": 12, "y": 2 } } ] }, { - "map": "media/map/heat-area.png", + "map": "media/map/shockwave-dng.png", "tileCountX": 32, "autotiles": [ { - "type": "LARGE", "base": { "x": 1, "y": 0 }, - "cliff": { - "x": 12, - "y": 4 + "size": { + "x": 8, + "y": 2 } }, { - "type": "DEFAULT", "base": { - "x": 1, - "y": 2 + "x": 9, + "y": 0 }, - "cliff": { - "x": 18, - "y": 4 + "size": { + "x": 8, + "y": 2 } }, { - "type": "LARGE", "base": { - "x": 11, - "y": 0 + "x": 3, + "y": 2 }, - "cliff": { - "x": 6, - "y": 4 + "size": { + "x": 8, + "y": 2 } }, { - "type": "DEFAULT", "base": { - "x": 11, - "y": 2 + "x": 10, + "y": 11 }, - "cliff": { - "x": 24, + "size": { + "x": 4, "y": 4 } }, { - "type": "LARGE", "base": { - "x": 21, - "y": 0 + "x": 15, + "y": 11 + }, + "size": { + "x": 4, + "y": 4 } }, { - "type": "LARGE", "base": { - "x": 21, + "x": 10, + "y": 42 + }, + "size": { + "x": 8, "y": 2 } } ] }, { - "map": "media/map/heat-dng.png", + "map": "media/map/tree-expo-space.png", "tileCountX": 32, "autotiles": [ { - "type": "DEFAULT", "base": { - "x": 1, + "x": 0, "y": 0 + }, + "size": { + "x": 8, + "y": 2 } - }, + } + ] + }, + { + "map": "media/map/tree-inner.png", + "tileCountX": 32, + "autotiles": [ { - "type": "LARGE", "base": { "x": 1, + "y": 0 + }, + "size": { + "x": 8, "y": 2 } }, { - "type": "DEFAULT", "base": { "x": 9, "y": 0 + }, + "size": { + "x": 8, + "y": 2 } }, { - "type": "DEFAULT", "base": { - "x": 17, - "y": 0 + "x": 3, + "y": 2 + }, + "size": { + "x": 8, + "y": 2 + } + }, + { + "base": { + "x": 10, + "y": 11 + }, + "size": { + "x": 4, + "y": 4 + } + }, + { + "base": { + "x": 15, + "y": 11 + }, + "size": { + "x": 4, + "y": 4 } } ] }, { - "map": "media/map/cold-dng.png", + "map": "media/map/underwater.png", "tileCountX": 32, "autotiles": [ { - "type": "DEFAULT", "base": { "x": 0, - "y": 1 - } - }, - { - "type": "DEFAULT", - "base": { + "y": 10 + }, + "size": { "x": 12, - "y": 0 + "y": 2 } }, { - "type": "DEFAULT", "base": { + "x": 0, + "y": 12 + }, + "size": { "x": 12, "y": 2 } }, { - "type": "DEFAULT", "base": { - "x": 20, - "y": 0 + "x": 0, + "y": 14 + }, + "size": { + "x": 12, + "y": 2 } - }, + } + ] + }, + { + "map": "media/map/unknown-interior.png", + "tileCountX": 32, + "autotiles": [ { - "type": "DEFAULT", "base": { - "x": 20, - "y": 2 + "x": 0, + "y": 12 + }, + "size": { + "x": 4, + "y": 4 } } ] diff --git a/webapp/src/styles.scss b/webapp/src/styles.scss index 94b9a875..dc526705 100644 --- a/webapp/src/styles.scss +++ b/webapp/src/styles.scss @@ -192,3 +192,17 @@ body { .newline-tooltip { white-space: pre-line; } + +.snackbar-error { + > * { + background: mat.get-color-from-palette(mat.$red-palette, 700) !important; + } + + button { + color: white !important; + } + + .mat-mdc-snack-bar-label { + color: white !important; + } +}