diff --git a/src/common/config.interface.ts b/src/common/config.interface.ts index 1ecb244..eb3f739 100644 --- a/src/common/config.interface.ts +++ b/src/common/config.interface.ts @@ -1,4 +1,5 @@ export type AppConfig = { dpeFolder: string; cfruFolder: string; + assetsFolder: string; }; diff --git a/src/common/convert-to-source.ts b/src/common/convert-to-source.ts index 0844e2c..567f4f2 100644 --- a/src/common/convert-to-source.ts +++ b/src/common/convert-to-source.ts @@ -45,6 +45,7 @@ function populateCoords( return { ...coords, species, + size: 0, }; } return undefined; diff --git a/src/common/ipc.interface.ts b/src/common/ipc.interface.ts index 5d09df1..c04e16c 100644 --- a/src/common/ipc.interface.ts +++ b/src/common/ipc.interface.ts @@ -6,6 +6,8 @@ export type IPCConfigs = { 'set-dpe-location': () => string; 'locate-cfru': () => void; 'set-cfru-location': () => string; + 'locate-assets': () => void; + 'set-assets-location': () => string; 'load-files': () => void; 'pokemon-source-data': (data: AllPokemonData) => AllPokemonData; 'data-saved': () => boolean; diff --git a/src/main/main.ts b/src/main/main.ts index 7f85568..0912e88 100644 --- a/src/main/main.ts +++ b/src/main/main.ts @@ -1,7 +1,7 @@ /* eslint-disable @typescript-eslint/no-var-requires */ /* eslint global-require: off, no-console: off, promise/always-return: off */ import 'core-js/stable'; -import { app, BrowserWindow, dialog, ipcMain, shell } from 'electron'; +import { app, BrowserWindow, dialog, ipcMain, protocol, shell } from 'electron'; import log from 'electron-log'; import ElectronStore from 'electron-store'; import { autoUpdater } from 'electron-updater'; @@ -146,6 +146,20 @@ ipcMain.on('locate-cfru', async (event) => { } }); +ipcMain.on('locate-assets', async (event) => { + const result = dialog.showOpenDialogSync(mainWindow!, { + properties: ['openDirectory'], + message: 'Select Assets Location', + title: 'Select Assets Folder', + defaultPath: store.get('assetsFolder'), + }); + if (result && result.length > 0) { + store.set('assetsFolder', result[0]); + event.reply('set-assets-location', result[0]); + await loadFiles(); + } +}); + if (process.env.NODE_ENV === 'production') { const sourceMapSupport = require('source-map-support'); sourceMapSupport.install(); @@ -184,6 +198,12 @@ const createWindow = async () => { return path.join(RESOURCES_PATH, ...paths); }; + protocol.registerFileProtocol('asset', (request, callback) => { + const relativePath = request.url.replace('asset://', ''); + const absolutePath = path.join(store.get('assetsFolder'), relativePath); + callback(absolutePath); + }); + mainWindow = new BrowserWindow({ show: false, width: 1024, diff --git a/src/renderer/SettingsDialog.tsx b/src/renderer/SettingsDialog.tsx index 0088075..e26f50d 100644 --- a/src/renderer/SettingsDialog.tsx +++ b/src/renderer/SettingsDialog.tsx @@ -37,6 +37,9 @@ const folderInputStyle: SxProps = { export const SettingsDialog = ({ open, onClose }: SettingsDialogProps) => { const [dpeFolder, setDPEFolder] = React.useState(Config.get('dpeFolder')); const [cfruFolder, setCFRUFolder] = React.useState(Config.get('cfruFolder')); + const [assetsFolder, setAssetsFolder] = React.useState( + Config.get('assetsFolder') + ); const handleDPESelect = () => { window.electron.ipcRenderer.send('locate-dpe'); @@ -44,6 +47,9 @@ export const SettingsDialog = ({ open, onClose }: SettingsDialogProps) => { const handleCFRUSelect = () => { window.electron.ipcRenderer.send('locate-cfru'); }; + const handleAssetsSelect = () => { + window.electron.ipcRenderer.send('locate-assets'); + }; useEffect(() => { const clearDpeListener = window.electron.ipcRenderer.on( @@ -54,9 +60,15 @@ export const SettingsDialog = ({ open, onClose }: SettingsDialogProps) => { 'set-cfru-location', setCFRUFolder ); + const clearAssetsListener = window.electron.ipcRenderer.on( + 'set-assets-location', + setAssetsFolder + ); + return () => { clearDpeListener(); clearCfruListener(); + clearAssetsListener(); }; }, [setDPEFolder, setCFRUFolder]); @@ -101,6 +113,16 @@ export const SettingsDialog = ({ open, onClose }: SettingsDialogProps) => { /> + + + + + diff --git a/src/renderer/images/battle_bg.png b/src/renderer/images/battle_bg.png new file mode 100644 index 0000000..2b38b55 Binary files /dev/null and b/src/renderer/images/battle_bg.png differ diff --git a/src/renderer/images/battle_bg_shadow.png b/src/renderer/images/battle_bg_shadow.png new file mode 100644 index 0000000..1f8c00f Binary files /dev/null and b/src/renderer/images/battle_bg_shadow.png differ diff --git a/src/renderer/pokemon-editor/tabs/graphics/BattlePreview.tsx b/src/renderer/pokemon-editor/tabs/graphics/BattlePreview.tsx new file mode 100644 index 0000000..5274fba --- /dev/null +++ b/src/renderer/pokemon-editor/tabs/graphics/BattlePreview.tsx @@ -0,0 +1,102 @@ +import { observer } from 'mobx-react-lite'; +import { useEffect, useRef } from 'react'; +import BgImage from '../../../images/battle_bg.png'; +import BgImageWithShadow from '../../../images/battle_bg_shadow.png'; +import { usePokemonStoreContext } from '../../pokemon.store'; + +export const BattlePreview = observer(() => { + const pokemonStore = usePokemonStoreContext(); + const species = pokemonStore.selectedSpecies; + const battleScreenCanvas = useRef(null); + + if (species) { + useEffect(() => { + function getSprite() { + return `asset://${species!.name.toLowerCase()}/sprites.png`; + } + + function getCroppedSpriteCanvas( + xOffset: number, + callback: (data: HTMLCanvasElement) => void + ) { + const spritesheet = new Image(); + spritesheet.src = getSprite(); + spritesheet.addEventListener('load', () => { + const canvas = document.createElement('canvas'); + const context = canvas.getContext('2d')!; + canvas.width = spritesheet.width; + canvas.height = spritesheet.height; + context.drawImage(spritesheet, xOffset, 0, 64, 64, 0, 0, 64, 64); + const imageData = context.getImageData(0, 0, 64, 64); + const bgRed = imageData.data[0]; + const bgGreen = imageData.data[1]; + const bgBlue = imageData.data[2]; + const bgAlpha = imageData.data[3]; + for (let i = 0; i < imageData.data.length; i += 4) { + // is this pixel tbg colored + if ( + imageData.data[i] === bgRed && + imageData.data[i + 1] === bgGreen && + imageData.data[i + 2] === bgBlue && + imageData.data[i + 3] === bgAlpha + ) { + // change to transparent + imageData.data[i] = 0; + imageData.data[i + 1] = 0; + imageData.data[i + 2] = 0; + imageData.data[i + 3] = 0; + } + } + context.clearRect(0, 0, canvas.width, canvas.height); + context.putImageData(imageData, 0, 0); + callback(canvas); + }); + } + + function getFrontSpriteData(callback: (data: HTMLCanvasElement) => void) { + return getCroppedSpriteCanvas(0, callback); + } + + function getBackSpriteData(callback: (data: HTMLCanvasElement) => void) { + return getCroppedSpriteCanvas(192, callback); + } + + const frontCanvas = battleScreenCanvas.current!; + const frontContext = frontCanvas.getContext('2d')!; + + const bgImage = new Image(); + bgImage.src = species.enemyElevation > 0 ? BgImageWithShadow : BgImage; + bgImage.addEventListener('load', () => { + frontContext.drawImage(bgImage, 0, 0); + getBackSpriteData((data) => { + frontContext.drawImage(data, 40, 48 + species.backCoords.y_offset); + }); + getFrontSpriteData((data) => { + frontContext.drawImage( + data, + 144, + 8 + species.frontCoords.y_offset - species.enemyElevation + ); + }); + }); + }, [ + species, + species.frontCoords.y_offset, + species.backCoords.y_offset, + species.enemyElevation, + ]); + return ( + + ); + } + return null; +}); diff --git a/src/renderer/pokemon-editor/tabs/graphics/GraphicsTab.tsx b/src/renderer/pokemon-editor/tabs/graphics/GraphicsTab.tsx index 79fbca0..13ba668 100644 --- a/src/renderer/pokemon-editor/tabs/graphics/GraphicsTab.tsx +++ b/src/renderer/pokemon-editor/tabs/graphics/GraphicsTab.tsx @@ -4,45 +4,51 @@ import { ObservableNumberField } from '../../../common/forms/ObservableNumberFie import { ObservableSwitch } from '../../../common/forms/ObservableSwitch'; import { ObservableTextField } from '../../../common/forms/ObservableTextField'; import { usePokemonStoreContext } from '../../pokemon.store'; +import { BattlePreview } from './BattlePreview'; export const GraphicsTab = observer(() => { const pokemonStore = usePokemonStoreContext(); const species = pokemonStore.selectedSpecies; + if (species) { return ( - - - - - - - + + + + + + + + + + + ); }