diff --git a/package-lock.json b/package-lock.json index 77595d1..a8a2b67 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,13 @@ { "name": "vcs-game-maker", - "version": "0.4.0", + "version": "0.5.0", "lockfileVersion": 2, "requires": true, "packages": { "": { - "version": "0.4.0", + "version": "0.5.0", "dependencies": { + "@curtishughes/pixel-editor": "^4.0.1", "@vue/composition-api": "^1.0.0-rc.13", "batari-basic": "^0.0.1", "blockly": "^6.20210701.0", @@ -1070,6 +1071,11 @@ "to-fast-properties": "^2.0.0" } }, + "node_modules/@curtishughes/pixel-editor": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@curtishughes/pixel-editor/-/pixel-editor-4.0.1.tgz", + "integrity": "sha512-y9Ntl+Z/FonWEFxdEAVXH1D+iU9QorH4AckM4ZuYfyWiKoxsLZf6E+dJwwuIAi6gEyKueFcYgDjlU1QAjJu9sA==" + }, "node_modules/@eslint/eslintrc": { "version": "0.4.2", "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-0.4.2.tgz", @@ -16010,6 +16016,11 @@ "to-fast-properties": "^2.0.0" } }, + "@curtishughes/pixel-editor": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@curtishughes/pixel-editor/-/pixel-editor-4.0.1.tgz", + "integrity": "sha512-y9Ntl+Z/FonWEFxdEAVXH1D+iU9QorH4AckM4ZuYfyWiKoxsLZf6E+dJwwuIAi6gEyKueFcYgDjlU1QAjJu9sA==" + }, "@eslint/eslintrc": { "version": "0.4.2", "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-0.4.2.tgz", diff --git a/package.json b/package.json index 22a62c4..3d654d1 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "vcs-game-maker", - "version": "0.5.0", + "version": "0.6.0", "private": true, "scripts": { "serve": "vue-cli-service serve", @@ -8,6 +8,7 @@ "lint": "vue-cli-service lint" }, "dependencies": { + "@curtishughes/pixel-editor": "^4.0.1", "@vue/composition-api": "^1.0.0-rc.13", "batari-basic": "^0.0.1", "blockly": "^6.20210701.0", diff --git a/src/components/PixelEditor.vue b/src/components/PixelEditor.vue new file mode 100644 index 0000000..2b8c9c2 --- /dev/null +++ b/src/components/PixelEditor.vue @@ -0,0 +1,140 @@ + + + diff --git a/src/generators/bbasic.bb.hbs b/src/generators/bbasic.bb.hbs index b3ec8f1..caa5cd7 100644 --- a/src/generators/bbasic.bb.hbs +++ b/src/generators/bbasic.bb.hbs @@ -5,17 +5,7 @@ rem - shape of the playfield and player sprites rem ************************************************************************** playfield: - XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX - X....X...................X....X - X.............................X - X.............................X - X.............................X - X.............................X - X.............................X - X.............................X - X.............................X - X....X...................X....X - XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX +{{ playField }} end player0: diff --git a/src/generators/bbasic.js b/src/generators/bbasic.js index 3f0050e..aae0d14 100644 --- a/src/generators/bbasic.js +++ b/src/generators/bbasic.js @@ -14,6 +14,9 @@ import Blockly from 'blockly/core'; import templateText from 'raw-loader!./bbasic.bb.hbs'; import Handlebars from 'handlebars'; +import {useBackgroundsStorage} from '../hooks/project'; +import {matrixToPlayfield} from '../utils/pixels'; + const handlebarsTemplate = Handlebars.compile(templateText); /** @@ -172,11 +175,13 @@ Blockly.BBasic.finish = function(code) { code = Object.getPrototypeOf(this).finish.call(this, code); code = code.replace(/^[\t ]*/gm, Blockly.BBasic.INDENT); + const playField = Blockly.BBasic.generateBackgrounds(); + this.isInitialized = false; this.nameDB_.reset(); const generatedBody = definitions.join('\n\n') + '\n\n\n' + code; - return handlebarsTemplate({generatedBody}); + return handlebarsTemplate({generatedBody, playField}); }; /** @@ -322,6 +327,37 @@ Blockly.BBasic.getAdjusted = function(block, atId, optDelta, optNegate, return at; }; +Blockly.BBasic.generateBackgrounds = function() { + const backgroundsStorage = useBackgroundsStorage(); + + let backgroundData = null; + try { + backgroundData = backgroundsStorage.value; + } catch (e) { + console.error('Failed to load backgrounds', e); + } + + const backgrounds = backgroundData && backgroundData.backgrounds; + let playField = + 'XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX\n' + + 'X....X...................X....X\n' + + 'X.............................X\n' + + 'X.............................X\n' + + 'X.............................X\n' + + 'X.............................X\n' + + 'X.............................X\n' + + 'X.............................X\n' + + 'X.............................X\n' + + 'X....X...................X....X\n' + + 'XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX'; + + if (backgrounds && backgrounds[0] && backgrounds[0].pixels) { + playField = matrixToPlayfield(backgrounds[0].pixels); + } + + return playField.split('\n').map((line) => ' ' + line).join('\n'); +}; + import collision from './bbasic/collision'; import colour from './bbasic/colour'; import input from './bbasic/input'; diff --git a/src/hooks/project.js b/src/hooks/project.js index 17ca53e..a65d765 100644 --- a/src/hooks/project.js +++ b/src/hooks/project.js @@ -1,4 +1,10 @@ -import {useLocalStorage} from '../hooks/storage'; +import {useJsonLocalStorage, useLocalStorage} from '../hooks/storage'; + +const keyOf = (type) =>`vcs-game-maker.${type}`; + +export const useProjectStorage = (type) => useLocalStorage(keyOf(type)); +export const useJsonProjectStorage = (type) => useJsonLocalStorage(keyOf(type)); -export const useProjectStorage = (type) => useLocalStorage(`vcs-game-maker.${type}`); export const useWorkspaceStorage = () => useProjectStorage('workspace'); +export const useBackgroundsStorage = () => useJsonProjectStorage('backgrounds'); + diff --git a/src/hooks/storage.js b/src/hooks/storage.js index 4e8864b..d828d42 100644 --- a/src/hooks/storage.js +++ b/src/hooks/storage.js @@ -11,3 +11,13 @@ export const useLocalStorage = (key) => computed({ localStorage.setItem(key, value); }, }); + +export const useJsonLocalStorage = (key) => computed({ + get() { + const jsonText = localStorage.getItem(key); + return jsonText ? JSON.parse(jsonText) : null; + }, + set(value) { + localStorage.setItem(key, JSON.stringify(value)); + }, +}); diff --git a/src/router/index.js b/src/router/index.js index 1b9ea4c..d3de110 100644 --- a/src/router/index.js +++ b/src/router/index.js @@ -19,6 +19,11 @@ const routes = [ component: () => import(/* webpackChunkName: "about" */ '../views/About.vue'), }, + { + path: '/background', + name: 'Background', + component: () => import('../views/BackgroundEditor.vue'), + }, { path: '/generated', name: 'Generated', diff --git a/src/utils/array.js b/src/utils/array.js new file mode 100644 index 0000000..d5f8bfe --- /dev/null +++ b/src/utils/array.js @@ -0,0 +1,25 @@ +// Based on https://stackoverflow.com/a/16436975/679240 +export const isArrayEqual = (a, b) => { + if (a === b) return true; + if (a == null || b == null) return false; + if (a.length !== b.length) return false; + + for (let i = 0; i < a.length; ++i) { + if (a[i] !== b[i]) return false; + } + return true; +}; + +export const isMatrixEqual = (a, b) => { + if (a === b) return true; + if (a == null || b == null) return false; + if (a.length !== b.length) return false; + + for (let i = 0; i < a.length; ++i) { + if (!isArrayEqual(a[i], b[i])) return false; + } + + return true; +}; + +export const copyMatrix = (original) => original.map((row) => row.slice()); diff --git a/src/utils/pixels.js b/src/utils/pixels.js new file mode 100644 index 0000000..91c52b6 --- /dev/null +++ b/src/utils/pixels.js @@ -0,0 +1,6 @@ +export const playfieldToMatrix = (text) => text.trim().split('\n') + .map((line) => line.trim().split('').map((ch) => ch === 'X' ? 1 : 0)); + +export const matrixToPlayfield = (matrix) => matrix + .map((line) => line.map((pixel) => pixel ? 'X' : '.').join('')) + .join('\n').trim(); diff --git a/src/views/BackgroundEditor.vue b/src/views/BackgroundEditor.vue new file mode 100644 index 0000000..c0ccc73 --- /dev/null +++ b/src/views/BackgroundEditor.vue @@ -0,0 +1,87 @@ + + + diff --git a/src/views/Project.vue b/src/views/Project.vue index e129bfc..209f511 100644 --- a/src/views/Project.vue +++ b/src/views/Project.vue @@ -24,7 +24,8 @@ import {defineComponent, reactive} from '@vue/composition-api'; import {saveAs} from 'file-saver'; import YAML from 'yaml'; -import {useWorkspaceStorage} from '../hooks/project'; +import {useBackgroundsStorage, useWorkspaceStorage} from '../hooks/project'; +import {matrixToPlayfield, playfieldToMatrix} from '../utils/pixels'; const FORMAT_TYPE = 'VCS Game Maker Project'; const FORMAT_VERSION = 1.0; @@ -33,16 +34,27 @@ export default defineComponent({ setup(props, context) { const data = reactive({fileToImport: null}); const router = context.root.$router; + + const backgroundsStorage = useBackgroundsStorage(); const workspaceStorage = useWorkspaceStorage(); - return {data, router, workspaceStorage}; + + return {data, router, backgroundsStorage, workspaceStorage}; }, methods: { handleSaveProject() { + const backgrounds = !this.backgroundsStorage ? null : + { + ...this.backgroundsStorage, + backgrounds: this.backgroundsStorage.backgrounds + .map((bkg) => ({...bkg, pixels: matrixToPlayfield(bkg.pixels)})), + }; + const projectYaml = YAML.stringify({ 'type': FORMAT_TYPE, 'format-version': FORMAT_VERSION, 'generation-time': new Date(), 'blockly-workspace': this.workspaceStorage, + backgrounds, }); const projectBlob = new Blob([projectYaml], {type: 'text/yaml'}); @@ -55,7 +67,6 @@ export default defineComponent({ return; } - console.info('Importing file', this.data.fileToImport); const reader = new FileReader(); reader.readAsText(this.data.fileToImport, 'UTF-8'); reader.onload = (evt) => { @@ -74,6 +85,15 @@ export default defineComponent({ this.workspaceStorage = project['blockly-workspace']; + if (project.backgrounds) { + const backgrounds = { + ...project.backgrounds, + backgrounds: project.backgrounds.backgrounds + .map((bkg) => ({...bkg, pixels: playfieldToMatrix(bkg.pixels)})), + }; + this.backgroundsStorage = backgrounds; + } + this.router.push('/'); }; reader.onerror = (evt) => console.error('Error while loading project', evt);