diff --git a/src/Renderer/components/Manager/Data/Scene/Map/Main.vue b/src/Renderer/components/Manager/Data/Scene/Map/Main.vue index ef326af..643f9a7 100644 --- a/src/Renderer/components/Manager/Data/Scene/Map/Main.vue +++ b/src/Renderer/components/Manager/Data/Scene/Map/Main.vue @@ -120,6 +120,15 @@ + + + 저장 중... + @@ -317,14 +326,6 @@ - - - 저장 - - 맵은 자동으로 저장됩니다. - - - @@ -394,6 +395,7 @@ export default class SceneMapEditor extends Vue { @NonReactivity(null) private game!: Phaser.Game|null @NonReactivity(null) private scene!: PreviewScene|null + private isSaving: boolean = false private isBuilding: boolean = false private isBuiltFail: boolean = false private isTooltipOpen: boolean = false @@ -436,6 +438,12 @@ export default class SceneMapEditor extends Vue { click: (): void => { this.openMapResizer() } + }, + { + text: '저장하기', + click: (): void => { + this.save() + } } ] }, @@ -535,7 +543,7 @@ export default class SceneMapEditor extends Vue { private async createGame(): Promise { const [ width, height ] = this.projectConfig.gameDisplaySize - const previewScene: PreviewScene = new PreviewScene(this.projectDirectory, this.storageKey, this.filePath) + const previewScene: PreviewScene = new PreviewScene(this.projectDirectory, this.storageKey) const config = createConfig(width, height, [ previewScene ], this.canvasParent) this.game = new Phaser.Game(config) @@ -551,6 +559,14 @@ export default class SceneMapEditor extends Vue { .on('load-map-success', (map: Engine.GameProject.SceneMap): void => { this.mapSceneSide = map.side }) + .on('save-map-fail', (message: string): void => { + this.isSaving = false + this.$store.dispatch('snackbar', message) + }) + .on('save-map-success', (): void => { + this.isSaving = false + this.$store.dispatch('snackbar', '저장되었습니다!') + }) this.resizeCanvas() } @@ -666,7 +682,7 @@ export default class SceneMapEditor extends Vue { return } - this.scene.transfer.emit('receive-properties', { + this.scene.transfer.emit('receive-wall-properties', { alias: this.propertyAlias, scale: this.propertyScale, isSensor: this.propertyIsSensor @@ -709,6 +725,17 @@ export default class SceneMapEditor extends Vue { this.setDisposeBrush(null) } + private save(): void { + if (!this.scene) { + return + } + if (this.isSaving) { + return + } + + this.isSaving = true + this.scene.transfer.emit('receive-save-request') + } private checkKeyExists(): boolean { if (!this.storageKey) { diff --git a/src/Renderer/components/Manager/Data/Scene/Map/Phaser/PreviewScene.ts b/src/Renderer/components/Manager/Data/Scene/Map/Phaser/PreviewScene.ts index 8c1f249..12ee54f 100644 --- a/src/Renderer/components/Manager/Data/Scene/Map/Phaser/PreviewScene.ts +++ b/src/Renderer/components/Manager/Data/Scene/Map/Phaser/PreviewScene.ts @@ -6,7 +6,6 @@ import { Plugin as DialoguePlugin } from '@eriengine/plugin-dialogue' import { Plugin as FogOfWarPlugin } from '@eriengine/plugin-fog-of-war' import { Plugin as IsometricScenePlugin } from '@eriengine/plugin-isometric-scene' import { PointerPlugin as IsometricCursorPlugin, SelectPlugin as IsometricSelectPlugin } from '@eriengine/plugin-isometric-cursor' -import { FileWatcher } from '@/Utils/FileWatcher' import * as Types from './Vars/Types' import { SceneDataTransfer } from './SceneDataTransfer' @@ -28,10 +27,8 @@ export default class PreviewScene extends Phaser.Scene { readonly transfer: SceneDataTransfer = new SceneDataTransfer readonly mapData: SceneMapManager = new SceneMapManager({ side: 2000, walls: [], floors: [] }) - private watcher: FileWatcher|null = null private projectDirectory: string = '' private storageKey: string = '' - private mapFilePath: string = '' private cameraControl: Phaser.Cameras.Controls.SmoothedKeyControl|null = null private requireImages: Types.PaletteImage[] = [] @@ -46,12 +43,11 @@ export default class PreviewScene extends Phaser.Scene { private disposeBrush: Types.PaletteImage|Types.PaletteSprite|null = null - constructor(projectDirectory: string, storageKey: string, filePath: string) { + constructor(projectDirectory: string, storageKey: string) { super({ key: '__preview-scene__', active: false }) this.projectDirectory = projectDirectory this.storageKey = storageKey - this.mapFilePath = filePath this.transfer .on('receive-image-list', (list): void => { @@ -79,13 +75,6 @@ export default class PreviewScene extends Phaser.Scene { return true } - private get isAnimationPalette(): boolean { - if (!this.isDisposeEnable) { - return false - } - return Object.prototype.hasOwnProperty.call(this.disposeBrush, 'frameWidth') - } - private get cursorSide(): number { if (!this.isDisposeEnable) { return 0 @@ -94,7 +83,7 @@ export default class PreviewScene extends Phaser.Scene { let width: number let height: number - if (this.isAnimationPalette) { + if (this.isAnimationPalette(this.disposeBrush!.key)) { const brush: Types.PaletteSprite = this.disposeBrush as Types.PaletteSprite width = brush.frameWidth height = brush.frameHeight @@ -115,6 +104,31 @@ export default class PreviewScene extends Phaser.Scene { return this.getIsometricSideFromWidth(width / 2) } + private getPaletteFromKey(key: string): Types.PaletteImage|Types.PaletteSprite|null { + const map: Map = new Map + + for (const image of this.requireImages) { + map.set(image.key, image) + } + for (const sprite of this.requireSprites) { + map.set(sprite.key, sprite) + } + + if (!map.has(key)) { + return null + } + + return map.get(key)! + } + + private isAnimationPalette(key: string): boolean { + const palette: Types.PaletteImage|Types.PaletteSprite|null = this.getPaletteFromKey(key) + if (!palette) { + return false + } + return Object.prototype.hasOwnProperty.call(this.getPaletteFromKey(key)!, 'frameWidth') + } + private setCameraMoving(): void { const camera = this.cameras.main const acceleration: number = 0.05 @@ -232,108 +246,91 @@ export default class PreviewScene extends Phaser.Scene { this.selectionTiles.clear() } - private setItemProperties({ alias, scale, isSensor }: Types.PaletteProperties): void { - for (const wall of this.selectionWalls) { - scale = Number(scale) - isSensor = Boolean(isSensor) + private setWallProperties(wall: Phaser.Physics.Matter.Sprite, { alias, scale, isSensor }: Types.PaletteProperties): void { + scale = Number(scale) + isSensor = Boolean(isSensor) - // 올바르지 않은 값이 넘어왔을 경우 객체를 삭제하고 데이터에서도 제거함 - if (isNaN(scale) || typeof scale !== 'number') { - wall.destroy() - this.selectionWalls.delete(wall) - continue - } + // 올바르지 않은 값이 넘어왔을 경우 객체를 삭제하고 데이터에서도 제거함 + if (isNaN(scale) || typeof scale !== 'number') { + this.mapData.dropWallData(wall) + wall.destroy() + return + } - wall.setScale(scale) - wall.setSensor(isSensor) - wall.data.set('alias', alias) + wall.setScale(scale) + wall.setSensor(isSensor) + wall.data.set('alias', alias) - this.mapData.modifyWallData(wall) - } + this.mapData.modifyWallData(wall) } - private getCoordKey(x: number, y: number): string { - return `${x},${y}` - } + private calcStraightDisposeOffset(x: number, y: number): Types.Point2 { + const startOffset: Point2 = this.cursor.calcCursorOffset(this.dragStartOffset) + const distanceX: number = x - startOffset.x + const distanceY: number = y - startOffset.y + + let deg: number + const distance: number = this.getDiagonal(distanceX, distanceY) - private dispose(e: Phaser.Input.Pointer): void { - if (!this.isDisposeEnable) { - return + // ↗ + if (distanceX > 0 && distanceY < 0) { + deg = -26.57 + } + // ↘ + else if (distanceX > 0 && distanceY > 0) { + deg = 26.57 + } + // ↙ + else if (distanceX < 0 && distanceY > 0) { + deg = 180 - 26.57 + } + // ↖ + else { + deg = 180 + 26.57 } - // shift키를 누른 상태로 작업했을 시, 직선으로 계산함 - let x: number - let y: number - if (e.event.shiftKey) { - const startOffset: Point2 = this.cursor.calcCursorOffset(this.dragStartOffset) - const distanceX: number = e.worldX - startOffset.x - const distanceY: number = e.worldY - startOffset.y - - // 정확히 상하/좌우로 이동하거나, 이동하지 않았을 경우 - if (distanceX === 0 || distanceY === 0) { - x = this.cursor.pointerX - y = this.cursor.pointerY - } - else { - let deg: number - const distance: number = this.getDiagonal(distanceX, distanceY) + const rad: number = Phaser.Math.DegToRad(deg) + const offset: Point2 = this.cursor.calcCursorOffset({ + x: Math.cos(rad) * distance, + y: Math.sin(rad) * distance + }) - // ↗ - if (distanceX > 0 && distanceY < 0) { - deg = -26.57 - } - // ↘ - else if (distanceX > 0 && distanceY > 0) { - deg = 26.57 - } - // ↙ - else if (distanceX < 0 && distanceY > 0) { - deg = 180 - 26.57 - } - // ↖ - else { - deg = 180 + 26.57 - } + x = startOffset.x + offset.x + y = startOffset.y + offset.y - const rad: number = Phaser.Math.DegToRad(deg) - const offset: Point2 = this.cursor.calcCursorOffset({ - x: Math.cos(rad) * distance, - y: Math.sin(rad) * distance - }) + return { x, y } + } - x = startOffset.x + offset.x - y = startOffset.y + offset.y - } - } - else { - x = this.cursor.pointerX - y = this.cursor.pointerY + private dispose(x: number, y: number, type: number, brushKey: string): Phaser.GameObjects.GameObject|null { + if (!this.getPaletteFromKey(brushKey)) { + return null } let animsKey: string|undefined = undefined - if (this.isAnimationPalette) { - const brush: Types.PaletteSprite = this.disposeBrush as Types.PaletteSprite - animsKey = brush.key + if (this.isAnimationPalette(brushKey)) { + animsKey = brushKey } - switch (this.selectionType) { + switch (type) { case 1: break case 2: { - const wall = this.isometric.setWalltile(x, y, this.disposeBrush!.key, undefined, animsKey) + const wall = this.isometric.setWalltile(x, y, brushKey, undefined, animsKey) wall.setDataEnabled() this.mapData.insertWallData(wall) - break + return wall } case 3: { - const floor = this.isometric.setFloortile(x, y, this.disposeBrush!.key, undefined, animsKey) + const floor = this.isometric.setFloortile(x, y, brushKey, undefined, animsKey) this.mapData.insertFloorData(floor) - break + return floor } } + + return null } private updateCamera(delta: number): void { @@ -348,11 +345,6 @@ export default class PreviewScene extends Phaser.Scene { this.cameraControl?.destroy() } - private generateWatcher(): void { - this.destroyWatcher() - this.watcher = new FileWatcher(this.mapFilePath, false).update(this.onMapDataChange.bind(this)).start().emit() - } - private generateAnimation(): void { for (const anims of this.requireSprites) { const { key, frameRate, start, end } = anims @@ -368,30 +360,40 @@ export default class PreviewScene extends Phaser.Scene { } } - private async onMapDataChange(): Promise { - await this.generateMapData() - } - - private destroyWatcher(): void { - this.watcher?.destroy() - this.watcher = null - } - private updateDragStartOffset({ worldX, worldY }: Phaser.Input.Pointer): void { this.dragStartOffset = { x: worldX, y: worldY } } private onMouseLeftDown(e: Phaser.Input.Pointer): void { this.updateDragStartOffset(e) - this.dispose(e) + // dispose brush + if (this.disposeBrush) { + let { x, y } = this.cursor.calcCursorOffset({ x: e.worldX, y: e.worldY }) + if (e.event.shiftKey) { + const offset = this.calcStraightDisposeOffset(x, y) + x = offset.x + y = offset.y + } + this.dispose(x, y, this.selectionType, this.disposeBrush.key) + } if (!e.event.shiftKey) { this.unselectObjects() } } private onMouseLeftDrag(e: Phaser.Input.Pointer): void { - this.dispose(e) + + // dispose brush + if (this.disposeBrush) { + let { x, y } = this.cursor.calcCursorOffset({ x: e.worldX, y: e.worldY }) + if (e.event.shiftKey) { + const offset = this.calcStraightDisposeOffset(x, y) + x = offset.x + y = offset.y + } + this.dispose(x, y, this.selectionType, this.disposeBrush.key) + } } private onMouseLeftUp(e: Phaser.Input.Pointer): void { @@ -411,6 +413,20 @@ export default class PreviewScene extends Phaser.Scene { } this.mapData.setData(sceneMapRead.content) this.transfer.emit('load-map-success', this.mapData) + + this.setWorldSize(this.mapData.side) + + for (const props of this.mapData.walls) { + const wall: Phaser.GameObjects.GameObject|null = this.dispose(props.x, props.y, 2, props.key) + if (!wall) { + continue + } + this.setWallProperties(wall as Phaser.Physics.Matter.Sprite, props) + } + for (const { key, x, y } of this.mapData.floors) { + this.dispose(x, y, 3, key) + } + return true } @@ -457,8 +473,7 @@ export default class PreviewScene extends Phaser.Scene { // 데이터 송수신 인스턴스 이벤트 할당 this.transfer .on('receive-map-side', (side: number): void => { - this.mapData.modifySide(side) - this.isometric.setWorldSize(this.mapData.side) + this.setWorldSize(side) }) .on('receive-selection-type', (type: number): void => { this.setSelectionType(type) @@ -472,9 +487,33 @@ export default class PreviewScene extends Phaser.Scene { .on('receive-delete-selection', (): void => { this.deleteSelectionObjects() }) - .on('receive-properties', (properties: Types.PaletteProperties): void => { - this.setItemProperties(properties) + .on('receive-wall-properties', (properties: Types.PaletteProperties): void => { + for (const wall of this.selectionWalls) { + this.setWallProperties(wall, properties) + } }) + .on('receive-save-request', (): void => { + this.save() + }) + } + + private setWorldSize(side: number): void { + this.mapData.modifySide(side) + this.isometric.setWorldSize(side) + } + + private async save(): Promise { + if (!this.mapData) { + this.transfer.emit('save-map-fail', '데이터 인스턴스가 없습니다') + return + } + const mapData: Engine.GameProject.SceneMap = this.mapData.data + const mapWrite: Engine.GameProject.WriteSceneMapSuccess|Engine.GameProject.WriteSceneMapFail = await ipcRenderer.invoke('write-scene-map', this.projectDirectory, this.storageKey, mapData) + if (!mapWrite.success) { + this.transfer.emit('save-map-fail', mapWrite.message) + return + } + this.transfer.emit('save-map-success', mapData) } preload(): void { @@ -494,7 +533,7 @@ export default class PreviewScene extends Phaser.Scene { } // 맵 파일 감지 시작 - this.generateWatcher() + this.generateMapData() this.generateAnimation() // 씬 기능 시작 @@ -508,7 +547,6 @@ export default class PreviewScene extends Phaser.Scene { this.attachTransferEvent() // 플러그인 설정 - this.isometric.setWorldSize(this.mapData.side) this.cursor.enableCoordinate(true) this.select.enable(false) @@ -524,6 +562,5 @@ export default class PreviewScene extends Phaser.Scene { private onDestroy(): void { this.destroyCamera() - this.destroyWatcher() } } \ No newline at end of file diff --git a/src/Renderer/components/Manager/Data/Scene/Map/Phaser/SceneDataTransfer.ts b/src/Renderer/components/Manager/Data/Scene/Map/Phaser/SceneDataTransfer.ts index 830a54d..e144964 100644 --- a/src/Renderer/components/Manager/Data/Scene/Map/Phaser/SceneDataTransfer.ts +++ b/src/Renderer/components/Manager/Data/Scene/Map/Phaser/SceneDataTransfer.ts @@ -4,6 +4,8 @@ import * as Types from './Vars/Types' interface SceneDataTransferEvents { 'load-map-fail': (message: string) => void 'load-map-success': (map: Engine.GameProject.SceneMap) => void + 'save-map-fail': (message: string) => void + 'save-map-success': (map: Engine.GameProject.SceneMap) => void 'receive-map-side': (side: number) => void 'receive-selection-type': (type: number) => void 'receive-dispose-mode': (activity: boolean) => void @@ -12,7 +14,8 @@ interface SceneDataTransferEvents { 'receive-sprite-list': (list: Types.PaletteSprite[]) => void 'receive-open-properties': () => void 'receive-delete-selection': () => void - 'receive-properties': (properties: Types.PaletteProperties) => void + 'receive-wall-properties': (properties: Types.PaletteProperties) => void + 'receive-save-request': () => void } export class SceneDataTransfer extends TypedEmitter {} \ No newline at end of file diff --git a/src/Renderer/components/Manager/Data/Scene/Map/Phaser/SceneMapManager.ts b/src/Renderer/components/Manager/Data/Scene/Map/Phaser/SceneMapManager.ts index 95f8edb..1b11ebd 100644 --- a/src/Renderer/components/Manager/Data/Scene/Map/Phaser/SceneMapManager.ts +++ b/src/Renderer/components/Manager/Data/Scene/Map/Phaser/SceneMapManager.ts @@ -11,8 +11,17 @@ export class SceneMapManager implements Engine.GameProject.SceneMap { this.setData(sceneMap) } + get data(): Engine.GameProject.SceneMap { + const { side, walls, floors } = this + return { + side, + walls, + floors + } + } + private getItemKey(x: number, y: number): string { - return `${x},${y}` + return `${x}:${y}` } setData(sceneMap: Engine.GameProject.SceneMap): this { diff --git a/src/Template/Game/BASE_SCENE.txt b/src/Template/Game/BASE_SCENE.txt index ca1884c..238deb3 100644 --- a/src/Template/Game/BASE_SCENE.txt +++ b/src/Template/Game/BASE_SCENE.txt @@ -17,8 +17,8 @@ interface Point2 { interface SceneMap { side: number - walls: { name: string, alias: string, x: number, y: number, scale: number, isSensor: boolean }[] - floors: { name: string, x: number, y: number }[] + walls: { key: string, alias: string, x: number, y: number, scale: number, isSensor: boolean }[] + floors: { key: string, x: number, y: number }[] } interface SceneStorage { @@ -179,6 +179,29 @@ export default class BaseScene extends Phaser.Scene { } } + protected createMap(): void { + this.isometric.setWorldSize(this.__map.side) + + for (const { key, alias, x, y, scale, isSensor } of this.__map.walls) { + let animsKey: string|undefined + if (this.anims.exists(key)) { + animsKey = key + } + const wall = this.isometric.setWalltile(x, y, key, undefined, animsKey) + wall.setScale(scale) + wall.setSensor(isSensor) + wall.setDataEnabled() + wall.data.set('alias', alias) + } + for (const { key, x, y } of this.__map.floors) { + let animsKey: string|undefined + if (this.anims.exists(key)) { + animsKey = key + } + this.isometric.setFloortile(x, y, key, undefined, animsKey) + } + } + private getSaveKey(key: string): string { return `${BaseScene.ApplicationId}-save-${key}` } diff --git a/src/Template/Game/SCENE.txt b/src/Template/Game/SCENE.txt index 91964c7..72c7000 100644 --- a/src/Template/Game/SCENE.txt +++ b/src/Template/Game/SCENE.txt @@ -60,6 +60,7 @@ export default class NewScene extends BaseScene { create(): void { this.createAnimation() + this.createMap() this.runScript(this.__scripts.get('onSceneCreate')) // 이 아래에 작성하세요