├── .gitignore
├── .vscode
└── extensions.json
├── LICENSE.md
├── README.md
├── README_zh.md
├── docs
├── .vitepress
│ ├── config.ts
│ ├── config
│ │ ├── en.ts
│ │ └── zh.ts
│ ├── theme
│ │ ├── code.css
│ │ └── index.ts
│ └── utils
│ │ └── index.ts
├── guide
│ ├── cheat.md
│ ├── controller.md
│ ├── directives.md
│ ├── events.md
│ ├── getting-started.md
│ ├── methods.md
│ ├── props.md
│ └── replay.md
├── index.md
├── public
│ ├── android-chrome-192x192.png
│ ├── android-chrome-512x512.png
│ ├── apple-touch-icon.png
│ ├── favicon-16x16.png
│ ├── favicon-32x32.png
│ ├── nes-vue.svg
│ └── site.webmanifest
└── zh
│ ├── guide
│ ├── cheat.md
│ ├── controller.md
│ ├── directives.md
│ ├── events.md
│ ├── getting-started.md
│ ├── methods.md
│ ├── props.md
│ └── replay.md
│ └── index.md
├── eslint.config.js
├── index.html
├── package.json
├── playground
├── .editorconfig
├── .eslintignore
├── .eslintrc
├── LICENSE.md
├── README.md
├── index.html
├── package.json
├── pnpm-lock.yaml
├── postcss.config.js
├── public
│ ├── Mighty Final Fight (USA).nes
│ ├── Mitsume ga Tooru (Japan).nes
│ ├── Super Mario Bros (JU).nes
│ ├── Super Mario Bros 3.nes
│ ├── happylee-supermariobros,warped.fm2
│ ├── icons
│ │ ├── android-chrome-192x192.png
│ │ ├── android-chrome-512x512.png
│ │ ├── apple-touch-icon.png
│ │ ├── browserconfig.xml
│ │ ├── favicon-16x16.png
│ │ ├── favicon-32x32.png
│ │ ├── favicon.ico
│ │ ├── mstile-150x150.png
│ │ ├── safari-pinned-tab.svg
│ │ └── site.webmanifest
│ ├── jy,aiqiyou-mitsumegatooru.fm2
│ ├── lordtom,maru,tompa-smb3-warps.fm2
│ ├── nes-vue.es.js
│ ├── vite.svg
│ └── xipov3-mightyfinalfight.fm2
├── src
│ ├── App.vue
│ ├── assets
│ │ └── vue.svg
│ ├── auto-imports.d.ts
│ ├── components.d.ts
│ ├── components
│ │ ├── Icon
│ │ │ ├── IconDownload.vue
│ │ │ ├── IconGitee.vue
│ │ │ ├── IconGithub.vue
│ │ │ ├── IconMoon.vue
│ │ │ └── IconSun.vue
│ │ └── MainHeader.vue
│ ├── css
│ │ └── app.scss
│ ├── download
│ │ ├── files.ts
│ │ ├── index.ts
│ │ └── roms.ts
│ ├── layouts
│ │ └── MainLayout.vue
│ ├── main.ts
│ ├── pages
│ │ ├── 404.vue
│ │ └── index.vue
│ ├── router
│ │ └── index.ts
│ ├── stores
│ │ ├── dark.ts
│ │ └── index.ts
│ ├── template.ts
│ └── vite-env.d.ts
├── tsconfig.json
├── tsconfig.node.json
└── vite.config.ts
├── pnpm-lock.yaml
├── public
├── Super Mario Bros (JU).nes
├── favicon.ico
├── happylee-supermariobros,warped.fm2
├── nintendo-svgrepo-com.svg
└── vite.svg
├── src
├── App.vue
├── animation
│ └── index.ts
├── audio
│ └── index.ts
├── components
│ └── NesVue.vue
├── composables
│ ├── use-controller.ts
│ └── use-instance.ts
├── db
│ └── index.ts
├── dev
│ └── NesDemo.vue
├── directives
│ └── v-gamepad.ts
├── env.d.ts
├── index.ts
├── main.ts
├── nes
│ └── index.ts
├── types.ts
└── utils
│ └── index.ts
├── tsconfig.json
├── tsconfig.node.json
└── vite.config.ts
/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 | pnpm-debug.log*
8 | lerna-debug.log*
9 |
10 | node_modules
11 | dist
12 | dist-ssr
13 | *.local
14 | /**/cache
15 |
16 | # Editor directories and files
17 | .vscode/*
18 | !.vscode/settings.json
19 | !.vscode/extensions.json
20 | .idea
21 | .DS_Store
22 | *.suo
23 | *.ntvs*
24 | *.njsproj
25 | *.sln
26 | *.sw?
27 |
--------------------------------------------------------------------------------
/.vscode/extensions.json:
--------------------------------------------------------------------------------
1 | {
2 | "recommendations": [
3 | "vue.volar",
4 | "dbaeumer.vscode-eslint"
5 | ]
6 | }
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2022 Taiyuuki
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sub license, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
6 |
7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
8 |
9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,FITNESS FOR A PARTICULAR PURPOSE AND NON INFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
nes-vue
2 |
3 |
4 | A NES (FC)🎮 emulator component for Vue 3.
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 | :cn:[中文](./README_zh.md)
13 |
14 | ## 🚀Playground
15 |
16 | [Vue SFC Playgournd](https://play.vuejs.org/#eNqtV9lu2zgU/RWOX+wAtpw0LVB40k7SQTuYImmDLH2J8iBLlMVEIjUi5dgI/O9zuEmy46xoH1zxrueuZO57R2UZzGvam/QOZFyxUhFJVV1+DjkrSlEpck8qmg5JLIqyVjQhK5JWoiB9KPU7Qj+o/FXThsupHFmJkMeCS0VmUUGPGT4+kauQExL2MqVKORmPVcSWdX3LghlTWT0NmBg79fF5XdKKnEQVE+RLJSQZfL/cCcANe8O3G9l/rYUTNsvUknxjPMrxiwMZXJ4fvRrJCVOyLihyQS6EqGqEE5URd3ZCfu2TlRbvurkyPy/0kUVlucwpHUkddqGjniLo4V1UlTQJYNkhJmRX/3dtDq/xkIsqUaIYwnY9xEcZjWQx3R9pD3LNwWjvbR4WrBTz/VFh0p7qrKf683eAv1kOI/YfW4oa5k01ZpHStdhivFMQ3b2XZ8coCKZh4Hv5avd6x0vAgePyOs8bssx1FR+QEyajaU4dS1U1bVhMnka1xKRZXhrlsmWWmnUcTWkOtp/JwWCHfPrcKAbzKMco/kX6p3m07JMJPjSjDyshT2seKyY4SamKs2/FOyjf63CbzoNl139XPtCA8YQufqYDhGPN298dJIjo0B0ResjSIAj0F9zpXAYqo9xBNI70Pxe/gwqHOkrNXEFrtYazothI/wCIB9q6M6yB8bMRfMeiiqTOA+OzTd66H6lEud2N5lgvm7B14R7YoTmN1ZkovCHXO43SRg4ft/ssdluyVmijZRpIJdgXR+cPI0OVtKoNbouzrfEhG48ZO28y9eZ6dGFrC94PS8lg3ahjdCHoQK3/FaGwuUXC2jQiW3H+sU4xqA7G9m7ErYiDogX8KIoTIQcJm5M4j6T8FPamYhH2DBkMt3F8x6MwkHCL3pImdZWD5vqjQ79jicrA+bCHldRQM6qXIMjvP+625ENZxzE17v1Ee+bYIhwD4hasigOKB2ubdh0r+rR1wwESNFzuLc2bWhM8jLOIz7RsMwie6ZzBnSh1gf2RkPkoFZU1Txhv3gqtVYRvyrEJAfRbutykNo4Iucf7RRTYFChZTAd9fdX2h6Tf32lpL7g2rApZmZ4xIYxtDD6BYxutP05rpdDBh3HO4lsNzy+xJuWEnGmaV7cKj6n73dTR1qP2MmXT8mEPg4t3Ac6muybvKRJGJm7zJKC7z44PJK9z4fjgn/Vnt82z1vUWIBAkv1hCxcsTYW3PRzITdyC1+2QjO0/a9lNxMO6MM44mSfgMMMt2e5RCMl3pCcYCcmxO/3QrWwc6Qb/mjNPRNBfxLThmiQV6uqw6XkkzxkfAMyEfyoWVwGOFWzBDd18MYQeX+ZpOpefdaHmz+jVhJGwNyd7urjeJDnTQe8OefZaPiqgMbqTgeN0brdAxMPgTvxrNTjKLCrT2CRUnHKoJzdm8wsyoMS8LPw2He8HHYA8JlCBbUoAFe+NWG8CsgEFJXE94t20g0I8WltPqpxmfdSRRnou774ambx73GINORuPbLfQbiX2rUZ/q+armiKHhKeQQ42XYX89/0AW+G2Yhklr34hNMTKfIa7OmjNiXmieA3ZEzaP81CUXzXcivC0W59EH5K3zl/jxAkv5+IvQW7n6w32Rx9T8laIlV)
17 |
18 | [NES Vue Playground](https://taiyuuki.github.io/nes-vue)
19 |
20 | ## Features
21 |
22 | - [x] Support for multiplayer.
23 | - [x] Support for gamepad.
24 | - [x] Support for turbo button.
25 | - [x] Support for saving and loading.
26 | - [x] Support for playing TAS recordings. (`*.fm2`)
27 | - [x] Support for cheat codes.
28 |
29 | ## Usage
30 |
31 | ### install
32 |
33 | ```shell
34 | npm install nes-vue --save
35 | ```
36 |
37 | And then
38 |
39 | ```vue
40 |
43 |
44 |
47 |
48 | ```
49 | Refer to [documentations](https://nes-vue-docs.netlify.app/) for more details.
--------------------------------------------------------------------------------
/README_zh.md:
--------------------------------------------------------------------------------
1 | nes-vue
2 |
3 |
4 | 用于 Vue 3 的 NES (FC)🎮 游戏模拟器组件。
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 | ## 🚀游乐场
13 |
14 | [Vue SFC Playgournd](https://play.vuejs.org/#eNqtV9lu2zgU/RWOX+wAtpw0LVB40k7SQTuYImmDLH2J8iBLlMVEIjUi5dgI/O9zuEmy46xoH1zxrueuZO57R2UZzGvam/QOZFyxUhFJVV1+DjkrSlEpck8qmg5JLIqyVjQhK5JWoiB9KPU7Qj+o/FXThsupHFmJkMeCS0VmUUGPGT4+kauQExL2MqVKORmPVcSWdX3LghlTWT0NmBg79fF5XdKKnEQVE+RLJSQZfL/cCcANe8O3G9l/rYUTNsvUknxjPMrxiwMZXJ4fvRrJCVOyLihyQS6EqGqEE5URd3ZCfu2TlRbvurkyPy/0kUVlucwpHUkddqGjniLo4V1UlTQJYNkhJmRX/3dtDq/xkIsqUaIYwnY9xEcZjWQx3R9pD3LNwWjvbR4WrBTz/VFh0p7qrKf683eAv1kOI/YfW4oa5k01ZpHStdhivFMQ3b2XZ8coCKZh4Hv5avd6x0vAgePyOs8bssx1FR+QEyajaU4dS1U1bVhMnka1xKRZXhrlsmWWmnUcTWkOtp/JwWCHfPrcKAbzKMco/kX6p3m07JMJPjSjDyshT2seKyY4SamKs2/FOyjf63CbzoNl139XPtCA8YQufqYDhGPN298dJIjo0B0ResjSIAj0F9zpXAYqo9xBNI70Pxe/gwqHOkrNXEFrtYazothI/wCIB9q6M6yB8bMRfMeiiqTOA+OzTd66H6lEud2N5lgvm7B14R7YoTmN1ZkovCHXO43SRg4ft/ssdluyVmijZRpIJdgXR+cPI0OVtKoNbouzrfEhG48ZO28y9eZ6dGFrC94PS8lg3ahjdCHoQK3/FaGwuUXC2jQiW3H+sU4xqA7G9m7ErYiDogX8KIoTIQcJm5M4j6T8FPamYhH2DBkMt3F8x6MwkHCL3pImdZWD5vqjQ79jicrA+bCHldRQM6qXIMjvP+625ENZxzE17v1Ee+bYIhwD4hasigOKB2ubdh0r+rR1wwESNFzuLc2bWhM8jLOIz7RsMwie6ZzBnSh1gf2RkPkoFZU1Txhv3gqtVYRvyrEJAfRbutykNo4Iucf7RRTYFChZTAd9fdX2h6Tf32lpL7g2rApZmZ4xIYxtDD6BYxutP05rpdDBh3HO4lsNzy+xJuWEnGmaV7cKj6n73dTR1qP2MmXT8mEPg4t3Ac6muybvKRJGJm7zJKC7z44PJK9z4fjgn/Vnt82z1vUWIBAkv1hCxcsTYW3PRzITdyC1+2QjO0/a9lNxMO6MM44mSfgMMMt2e5RCMl3pCcYCcmxO/3QrWwc6Qb/mjNPRNBfxLThmiQV6uqw6XkkzxkfAMyEfyoWVwGOFWzBDd18MYQeX+ZpOpefdaHmz+jVhJGwNyd7urjeJDnTQe8OefZaPiqgMbqTgeN0brdAxMPgTvxrNTjKLCrT2CRUnHKoJzdm8wsyoMS8LPw2He8HHYA8JlCBbUoAFe+NWG8CsgEFJXE94t20g0I8WltPqpxmfdSRRnou774ambx73GINORuPbLfQbiX2rUZ/q+armiKHhKeQQ42XYX89/0AW+G2Yhklr34hNMTKfIa7OmjNiXmieA3ZEzaP81CUXzXcivC0W59EH5K3zl/jxAkv5+IvQW7n6w32Rx9T8laIlV)
15 |
16 | [NES Vue Playground](https://taiyuuki.github.io/nes-vue)
17 |
18 | ## 功能
19 |
20 | - [x] 支持双人
21 | - [x] 支持手柄
22 | - [x] 支持连发键
23 | - [x] 支持保存、读取
24 | - [x] 支持回放TAS录像(*.fm2文件)
25 | - [x] 支持金手指(作弊码)
26 |
27 | ## 使用
28 |
29 | ### 安装
30 |
31 | ```shell
32 | npm install nes-vue --save
33 | ```
34 |
35 | 然后:
36 |
37 | ```vue
38 |
41 |
42 |
45 |
46 | ```
47 |
48 | 更多组件API请查看 [文档](https://nes-vue-docs.netlify.app/zh/)
--------------------------------------------------------------------------------
/docs/.vitepress/config.ts:
--------------------------------------------------------------------------------
1 | import { defineConfig } from 'vitepress'
2 | import root from './config/en'
3 | import zh from './config/zh'
4 | import { copyright } from './utils'
5 |
6 | const giteeSVG = ``
10 |
11 | const path = process.env.GITEE ? '/nes-vue-docs' : ''
12 |
13 | // https://vitepress.dev/reference/site-config
14 | export default defineConfig({
15 | base: process.env.GITEE ? '/nes-vue-docs/' : void 0,
16 | title: 'NES Vue',
17 | description: 'NES Vue documents',
18 |
19 | head: [
20 | ['link', { rel: 'icon', href: path + '/favicon-32x32.png' }],
21 | ['link', { rel: 'icon', href: path + '/favicon-16x16.png' }],
22 | ['link', { rel: 'apple-touch-icon', href: path + '/apple-touch-icon.png' }],
23 | ['link', { rel: 'manifest', href: path + '/site.webmanifest' }],
24 | ['meta', { name: 'msapplication-TileColor', content: '#da532c' }],
25 | ['meta', { name: 'theme-color', content: '#ffffff' }],
26 | ],
27 |
28 | locales: {
29 | root,
30 | zh,
31 | },
32 |
33 | markdown: {
34 | theme: {
35 | light: 'light-plus',
36 | dark: 'github-dark',
37 | },
38 | },
39 |
40 | themeConfig: {
41 | logo: '/nes-vue.svg',
42 | outline: {
43 | level: [2, 3],
44 | },
45 | footer: {
46 | message: 'Released under the MIT License.',
47 | copyright: copyright(),
48 | },
49 | socialLinks: [
50 | { icon: 'github', link: 'https://github.com/taiyuuki/nes-vue' },
51 | {
52 | icon: {
53 | svg: giteeSVG,
54 | },
55 | link: 'https://gitee.com/taiyuuki/nes-vue',
56 | },
57 | ],
58 | search: {
59 | provider: 'local',
60 | },
61 | },
62 | })
63 |
--------------------------------------------------------------------------------
/docs/.vitepress/config/en.ts:
--------------------------------------------------------------------------------
1 | export default {
2 | label: 'English',
3 | lang: 'en',
4 | themeConfig: {
5 | nav: [
6 | { text: 'Home', link: '/' },
7 | { text: 'API', link: '/guide/getting-started' },
8 | ],
9 |
10 | sidebar: [
11 | {
12 | text: 'Guide',
13 | items: [
14 | { text: 'Getting Started', link: '/guide/getting-started' },
15 | { text: 'Props', link: '/guide/props' },
16 | { text: 'Controller', link: '/guide/controller' },
17 | { text: 'Directives', link: '/guide/directives' },
18 | { text: 'Events', link: '/guide/events' },
19 | { text: 'Methods', link: '/guide/methods' },
20 | { text: 'TAS Video', link: '/guide/replay' },
21 | { text: 'Cheat Code', link: '/guide/cheat' },
22 | ],
23 | },
24 | ],
25 |
26 | outlineTitle: 'Outline',
27 | },
28 | }
29 |
--------------------------------------------------------------------------------
/docs/.vitepress/config/zh.ts:
--------------------------------------------------------------------------------
1 | export default {
2 | label: '简体中文',
3 | lang: 'zh-CN',
4 | themeConfig: {
5 | // https://vitepress.dev/reference/default-theme-config
6 | nav: [
7 | { text: '主页', link: '/zh/index' },
8 | { text: 'API', link: '/zh/guide/getting-started' },
9 | ],
10 |
11 | sidebar: [
12 | {
13 | text: '指南',
14 | items: [
15 | { text: '开始使用', link: '/zh/guide/getting-started' },
16 | { text: '属性', link: '/zh/guide/props' },
17 | { text: '控制器', link: '/zh/guide/controller' },
18 | { text: '指令', link: '/zh/guide/directives' },
19 | { text: '事件', link: '/zh/guide/events' },
20 | { text: '方法', link: '/zh/guide/methods' },
21 | { text: '播放录像', link: '/zh/guide/replay' },
22 | { text: '金手指', link: '/zh/guide/cheat' },
23 | ],
24 | },
25 | ],
26 |
27 | outlineTitle: '目录',
28 |
29 | },
30 | }
31 |
--------------------------------------------------------------------------------
/docs/.vitepress/theme/code.css:
--------------------------------------------------------------------------------
1 | :root {
2 | --vp-c-brand: #f03752;
3 | --vp-button-brand-bg: #f03752;
4 | --vp-button-brand-border: #f04b22;
5 | --vp-button-brand-hover-bg: #41b883;
6 | --vp-button-brand-hover-border: #41b349;
7 | --vp-button-brand-active-bg: #1e9eb3;
8 | --vp-button-brand-active-border: #2b73af;
9 |
10 | --code-base: #eef7f2;
11 | --code-gradient: #fffef9;
12 | --code-counter: #131124;
13 | --code-border: #41b349;
14 | }
15 |
16 | :root.dark {
17 | --code-base: #132c33;
18 | --code-gradient: #1c0d1a;
19 | --code-counter: #baccd9;
20 | }
21 |
22 | html {
23 | overflow-y: auto;
24 | }
25 |
26 | header.VPNav.VPNav {
27 | width: 100vw;
28 | }
29 |
30 | .vp-doc div[class*='language-'] {
31 | overflow-x: initial;
32 | }
33 |
34 | div:not(.language-shell)>pre.shiki {
35 | position: relative;
36 | margin: 0;
37 | padding: 10px 10px 10px 1.25em;
38 | overflow: auto;
39 | white-space: pre;
40 | text-align: left;
41 | word-break: break-all;
42 | background: var(--code-base);
43 | background-image:
44 | linear-gradient(410deg,
45 | var(--code-gradient) 25%,
46 | transparent 25%,
47 | transparent 75%,
48 | var(--code-gradient) 75%,
49 | var(--code-gradient)),
50 | linear-gradient(410deg,
51 | var(--code-gradient) 25%,
52 | transparent 25%,
53 | transparent 75%,
54 | var(--code-gradient) 75%,
55 | var(--code-gradient));
56 | background-position: 0 0, 2px 2px;
57 | background-size: 4px 4px;
58 | border-right: 5px solid var(--code-border);
59 | border-left: 5px solid var(--code-border);
60 | border-radius: 5px;
61 | outline: none;
62 | }
63 |
64 | div:not(.language-shell)>pre.shiki::before {
65 | position: absolute;
66 | top: 10px;
67 | bottom: 10px;
68 | left: 0.5em;
69 | padding-right: 0.5em;
70 | overflow: hidden;
71 | color: var(--code-counter);
72 | text-align: right;
73 | content: "01\A 02\A 03\A 04\A 05\A 06\A 07\A 08\A 09\A 10\A 11\A 12\A 13\A 14\A 15\A 16\A 17\A 18\A 19\A 20\A 21\A 22\A 23\A 24\A 25\A 26\A 27\A 28\A 29\A 30\A 31\A 32\A 33\A 34\A 35\A 36\A 37\A 38\A 39\A 40\A 41\A 42\A 43\A 44\A 45\A 46\A 47\A 48\A 49\A 50\A 51\A 52\A 53\A 54\A 55\A 56\A 57\A 58\A 59\A 60\A 61\A 62\A 63\A 64\A 65\A 66\A 67\A 68\A 69\A 70\A 71\A 72\A 73\A 74\A 75\A 76\A 77\A 78\A 79\A 80\A 81\A 82\A 83\A 84\A 85\A 86\A 87\A 88\A 89\A 90\A 91\A 92\A 93\A 94\A 95\A 96\A 97\A 98\A 99";
74 | }
75 |
76 | div:not(.language-shell)>pre.shiki::after {
77 | position: absolute;
78 | top: 0px;
79 | bottom: 0px;
80 | left: 1em;
81 | padding-right: 0.5em;
82 | border-right: 1px dashed var(--code-counter);
83 | content: ' \A';
84 | }
85 |
86 | .image-container img {
87 | max-width: 320px;
88 | max-height: 320px;
89 | }
--------------------------------------------------------------------------------
/docs/.vitepress/theme/index.ts:
--------------------------------------------------------------------------------
1 | // .vitepress/theme/index.js
2 | import DefaultTheme from 'vitepress/theme'
3 | import './code.css'
4 |
5 | export default DefaultTheme
6 |
--------------------------------------------------------------------------------
/docs/.vitepress/utils/index.ts:
--------------------------------------------------------------------------------
1 | function copyright() {
2 | const start = 2023
3 | const now = new Date().getFullYear()
4 | const year = now === start ? '2023' : `${start}-${now}`
5 | return `Copyright © ${year} Taiyuuki`
6 | }
7 |
8 | export { copyright }
9 |
--------------------------------------------------------------------------------
/docs/guide/cheat.md:
--------------------------------------------------------------------------------
1 | # Cheat Code
2 |
3 | Start with v1.8.0, nes-vue supports cheat code.
4 |
5 | ## Usage
6 |
7 | Just like the cheat code in VirtuaNES, The format of cheat code is `xxxx-yz-vv`, where `xxxx` is memory address, `y` is type, `z` is length and `vv` is value. for example, one cheat code of Super Mario Bros is `079F-01-01`:
8 |
9 | ::: code-group
10 | ```vue [vue-js]
11 |
32 |
33 |
34 |
38 |
39 |
40 |
41 |
42 | ```
43 |
44 | ```vue [vue-ts]
45 |
68 |
69 |
70 |
74 |
75 |
76 |
77 |
78 | ```
79 | :::
--------------------------------------------------------------------------------
/docs/guide/controller.md:
--------------------------------------------------------------------------------
1 | # Controller
2 |
3 | `p1` and `p2` properties can customize controller's corresponding keyboard key values as follows:
4 |
5 | ```js
6 | p1 = {
7 | UP: 'KeyW',
8 | DOWN: 'KeyS',
9 | LEFT: 'KeyA',
10 | RIGHT: 'KeyD',
11 | A: 'KeyK',
12 | B: 'KeyJ',
13 | C: 'KeyI',
14 | D: 'KeyU',
15 | SELECT: 'Digit2',
16 | START: 'Digit1'
17 | }
18 | p2 = {
19 | UP: 'ArrowUp',
20 | DOWN: 'ArrowDown',
21 | LEFT: 'ArrowLeft',
22 | RIGHT: 'ArrowRight',
23 | A: 'Numpad2',
24 | B: 'Numpad1',
25 | C: 'Numpad5',
26 | D: 'Numpad4'
27 | }
28 | ```
29 |
30 | The values are [KeyboardEvent.code](https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/code), Each field is optional, and if a field is missing, the default value will be used.
31 |
32 | ## Control the game in other ways
33 |
34 | Strongly recommended to use [v-gamepad](/zh/guide/directives#v-gamepad) directive, which can bind any HTML element to game controller.
35 |
36 | Otherwise, you can manually trigger `document`'s `keydown` and `keyup` events to control the game, but it is recommended to do this when `v-gamepad` cannot satisfy your needs, here is an example:
37 |
38 | ```vue
39 |
49 |
50 |
51 |
54 |
55 |
56 | ```
57 |
58 | Touching the `button` element will triggers the game controller's `RIGHT` button.
59 |
60 | Using `v-gamepad` directive can achieve the same effect, and the performance is relatively better:
61 |
62 | ```vue
63 |
66 |
67 |
68 |
71 |
72 |
73 | ```
74 |
75 | Detailed usage please refer to [v-gamepad](/guide/directives#v-gamepad).
76 |
77 | ## Gamepad
78 |
79 | The component has built-in support for the gamepad, which does not require additional configuration.
80 |
--------------------------------------------------------------------------------
/docs/guide/directives.md:
--------------------------------------------------------------------------------
1 | # Directives
2 |
3 | ## v-gamepad
4 |
5 | `nes-vue` provides a `v-gamepad` directive that binds HTML elements to game controllers. For example, click on the `button` element to trigger `B`.
6 |
7 | ```vue
8 |
11 |
12 |
13 |
16 |
17 |
18 | ```
19 |
20 | Attribute value can be `'UP'`、`'DOWN'`、`'LEFT'`、`'RIGHT'`、`'A'`、`'B'`、`'C'`、`'D'`、`'SELECT'` or `'START'`. **Note that there must be quotation marks when using literal values.**
21 |
22 | You can also bind multiple buttons on the same element. A typical example is to bind `A` and `B` buttons on the `button` element:
23 |
24 | ```vue
25 |
28 |
29 |
30 |
33 |
34 |
35 | ```
36 |
37 | ### Arguments and modifiers
38 |
39 | By default, `v-gamepad` is bound to the mouse event (`mousedown` and `mouseup`), which controls player 1.
40 |
41 | To bind the touch event, you can add the `touch` modifier (default is `mouse`):
42 |
43 | ```vue
44 |
47 |
48 |
49 |
52 |
53 |
54 | ```
55 |
56 | To bind the player 2, you can add the `p2` modifier (default is `p1`):
57 |
58 | ```vue
59 |
62 |
63 |
64 |
67 |
68 |
69 | ```
70 |
--------------------------------------------------------------------------------
/docs/guide/events.md:
--------------------------------------------------------------------------------
1 | # events
2 |
3 | ### @fps
4 |
5 | ```ts
6 | @fps -> function(fps: number)
7 | ```
8 |
9 | Emitted per second while the game is running.
10 |
11 | ```vue
12 |
17 |
18 |
19 |
23 |
24 | ```
25 |
26 | ### @success
27 |
28 | ```ts
29 | @success -> function()
30 | ```
31 |
32 | Emitted when the ROM is loaded successfully.
33 |
34 | ### @saved
35 |
36 | ```ts
37 | @saved -> function({id, message, target})
38 | ```
39 |
40 | Emitted when the state has been saved.
41 |
42 | ### @loaded
43 |
44 | ```ts
45 | @loaded -> function({id, message, target})
46 | ```
47 |
48 | Emitted when the state has been loaded.
49 |
50 | ### @removed
51 |
52 | ```ts
53 | @removed -> function(id)
54 | ```
55 |
56 | Emitted when the saved state has been removed.
57 |
58 | ### @error
59 |
60 | ```ts
61 | @error -> funciont({code, message})
62 | ```
63 |
64 | Emitted when error occurs, the code is a`number`:
65 |
66 | * 0:ROM load error.
67 | * 1:Save state error.
68 | * 2:Load state error.
69 | * 3:Play fm2 video error.
--------------------------------------------------------------------------------
/docs/guide/getting-started.md:
--------------------------------------------------------------------------------
1 | # Getting Started
2 |
3 | ## Installation
4 |
5 | ```shell
6 | npm i nes-vue
7 | ```
8 | ## Import Component
9 |
10 | ```vue
11 |
14 |
15 |
20 |
21 | ```
22 |
23 | ## Playground
24 |
25 | [Playground](https://taiyuuki.github.io/nes-vue)
--------------------------------------------------------------------------------
/docs/guide/methods.md:
--------------------------------------------------------------------------------
1 | # Methods
2 |
3 | All methods are mounted on component instances. If you use TypeScript, you can obtain the `NesVueInstance` from `nes-vue`.
4 |
5 | ```vue
6 |
13 |
14 |
15 |
19 |
20 | ```
21 |
22 | ## start
23 |
24 | Start NES game.
25 |
26 | ```ts
27 | start(url?: string) => void
28 | ```
29 |
30 |
31 | ::: tip
32 | In most cases, clicking on the text ([label](/guide/props#label)) in the middle of the screen will start the game without the `start` method.
33 | :::
34 |
35 | ::: warning
36 | If you have to switch the game via the `start` method, you must use the **v-model** directive to bind the url, so that nes-vue can update it.
37 | :::
38 |
39 | ## reset
40 |
41 | Restart the game.
42 |
43 | ```ts
44 | reset() => void
45 | ```
46 |
47 | ## stop
48 |
49 | Stop the game.
50 |
51 | ```ts
52 | stop() => void
53 | ```
54 |
55 | ## pause
56 |
57 | The game is paused and can be resumed.
58 |
59 | ```ts
60 | pause() => void
61 | ```
62 |
63 | ## play
64 |
65 | Resume the game.
66 |
67 | ```ts
68 | play() => void
69 | ```
70 |
71 | ## save
72 |
73 | Save game state.
74 |
75 | ::: warning
76 | Can only be loaded while the game is running, and ensure that the running game is consistent with the saved game.
77 | :::
78 |
79 | ```ts
80 | save(id: string) => void
81 | ```
82 |
83 | By default, the game state is saved in indexedDB, you can also save it in localStorage via [storage](/guide/props#storage) property.
84 |
85 | Each game state archive requires about 200KB, if you need to save more data, it’s recommended to use indexedDB.
86 |
87 | ## load
88 |
89 | Load game state.
90 |
91 | ```ts
92 | load(id: string) => void
93 | ```
94 |
95 | example:
96 |
97 | ::: code-group
98 | ```vue [vue-js]
99 |
116 |
117 |
118 |
124 |
125 |
126 |
127 | ```
128 | ```vue [vue-ts]
129 |
148 |
149 |
150 |
156 |
157 |
158 |
159 | ```
160 | :::
161 |
162 | ## remove
163 |
164 | Remove game state.
165 |
166 | ```ts
167 | remove(id: string) => void
168 | ```
169 |
170 | ## clear
171 |
172 | Clear all saved state.
173 |
174 | ```ts
175 | clear() => void
176 | ```
177 |
178 | ## screenshot
179 |
180 | ```ts
181 | screenshot(download?: boolean, imageName?: string) => HTMLImageElement
182 | ```
183 |
184 | `screenshot(true)` will start downloading the screenshot inside the browser.
185 |
186 | The return value is an image element of the screenshot.
187 |
188 | ## fm2URL
189 |
190 | ```ts
191 | fm2URL(url: string) => Promise
192 | ```
193 |
194 | Fetch the `*.fm2` file, refer to [Replay](/guide/replay).
195 |
196 | ## fm2Text
197 |
198 | ```ts
199 | fm2Text(text: string) => Promise
200 | ```
201 |
202 | Read the `*.fm2` file's text content, refer to [Replay](/guide/replay).
203 |
204 | ## fm2Play
205 |
206 | ```ts
207 | fm2Play() => void
208 | ```
209 |
210 | Play fm2 video, refer to [Replay](/guide/replay).
211 |
212 | ## fm2Stop
213 |
214 | ```ts
215 | fm2Stop() => void
216 | ```
217 |
218 | Stop fm2 video, refer to [Replay](/guide/replay).
219 |
220 | ## cheatCode
221 |
222 | ```ts
223 | cheatCode(code: string) => void
224 | ```
225 |
226 | Enable cheat code, refer to [Cheat Code](/guide/cheat).
227 |
228 | ## cancelCheatCode
229 |
230 | ```ts
231 | cancelCheatCode(code: string) => void
232 | ```
233 |
234 | Disable cheat code, refer to [Cheat Code](/guide/cheat).
235 |
236 | ## cancelCheatCodeAll
237 |
238 | ```ts
239 | cancelCheatCodeAll() => void
240 | ```
241 |
242 | Disable all cheat code, refer to [Cheat Code](/guide/cheat).
--------------------------------------------------------------------------------
/docs/guide/props.md:
--------------------------------------------------------------------------------
1 | # Props
2 |
3 | Except for the `url`, all other properties are optional.
4 |
5 | ```vue
6 |
7 |
10 |
11 | ```
12 |
13 | ## All properties
14 |
15 | ### url
16 |
17 | * Type `string`
18 |
19 | URL of the nes ROM. **Required!**
20 |
21 | If you want to switch to another game, just bind the `url` with a reactive value, and change it:
22 |
23 | ```vue
24 |
33 |
34 |
35 |
36 |
37 |
38 | ```
39 |
40 | ### width
41 |
42 | * Type `string | number`
43 | * Default `256`
44 |
45 | Game screen width, units can be included, default is px.
46 |
47 | ### height
48 |
49 | * Type `string | number`
50 | * Default `240`
51 |
52 | Game screen height, units can be included, default is px.
53 |
54 | ::: tip
55 | Maintain a ratio of 256×240 for width and height.
56 | :::
57 |
58 | ```vue
59 |
60 |
65 |
66 | ```
67 |
68 | ### label
69 |
70 | * Type `string`
71 | * Default `"Game Start"`
72 |
73 | Text of the game screen,click on it to start the game。
74 |
75 | ### gain
76 |
77 | * Type `number`
78 | * Default `100`
79 |
80 | The game volume between [0, 100].
81 |
82 | ### no-clip
83 |
84 | * Type `boolean`
85 | * Default `false`
86 |
87 | Background clipping, false=BG invisible in left 8-pixel column, true=No clipping.
88 |
89 | Clip mode can solve the problem that the edges of some game BG are not fully displayed, and the background will not have black edges, but it will also cause the edge materials of most game BG to flicker. Please use it as appropriate.
90 |
91 | ### auto-start
92 |
93 | * Type `boolean`
94 | * Default `false`
95 |
96 | Auto start when the component on mounted.
97 |
98 | :::warning
99 | nes-vue uses the AudioContext API for audio playback, and due to the browser's security policy, the game will only run after user interaction (such as mouse clicks), so use this property with caution.
100 | If you want to use this property, place nes-vue in a component that will not load until user's clicked.
101 | :::
102 |
103 | ### turbo
104 |
105 | * Type `number`
106 | * Default `16`
107 |
108 | Mashing speed per second, between [5, 20].
109 |
110 | ### storage
111 |
112 | * Type `boolean`
113 | * Default `false`
114 |
115 | Use `localStorage` to save the game state, default is indexedDB, see [Methods - save](/guide/methods#save).
116 |
117 | ### db-name
118 |
119 | * Type `string`
120 | * Default `"nes-vue"`
121 |
122 | The name of the object store for indexedDB.
123 |
124 | ### p1
125 |
126 | * Type `object`
127 | * Default see[Controller](/guide/controller)
128 |
129 | Player 1 controller.
130 |
131 | ### p2
132 |
133 | * Type `object`
134 | * Default see[Controller](/guide/controller)
135 |
136 | Player 2 controller.
137 |
138 | ### debugger
139 |
140 | * Type boolean
141 | * Default false
142 |
143 | The error message is output in the console.
144 |
--------------------------------------------------------------------------------
/docs/guide/replay.md:
--------------------------------------------------------------------------------
1 | # Replay recording video
2 |
3 | Starting from v1.5.0, the function of playing TAS videos (`*.fm2` files) has been added, which can be downloaded from [TASVideos](https://tasvideos.org/) .
4 |
5 | There are two ways to play the `*.fm2` here.
6 |
7 | ## Fetch fm2 file
8 |
9 | The first is to fetch `*.fm2` files through a URL.
10 |
11 | ::: code-group
12 | ```vue [vue-js]
13 |
27 |
28 |
29 |
35 |
36 |
37 | ```
38 |
39 | ```vue [vue-ts]
40 |
56 |
57 |
58 |
64 |
65 |
66 | ```
67 | :::
68 |
69 | ## Read the plain text string
70 |
71 | The second is to directly read the plain text string of the `*.fm2` file.
72 |
73 | ::: code-group
74 | ```vue [vue-js]
75 |
87 |
88 |
89 |
95 |
96 |
97 | ```
98 |
99 | ```vue [vue-ts]
100 |
114 |
115 |
116 |
122 |
123 |
124 | ```
125 |
126 | :::
127 |
128 | ## Note
129 |
130 | There are several points to note:
131 |
132 | * Please ensure that the game version used in the `*.fm2` is completely consistent with the game ROM version, including Japanese, American, European, modified, and translated versions.
133 |
134 | * Due to differences in the start frame, some game recording videos may require manual adjustment. A second parameter is provided to fine-tune the frame count.
135 |
136 | ```ts
137 | nes.value.fm2Play(-1) // 1 frame in advance
138 | nes.value.fm2Play(2) // Delay by 2 frames
139 | ```
140 |
141 | The specific number of frames that need to be adjusted can only be tested by yourself.
142 |
143 | * Even with identical game versions, the start frame is completely aligned, and as the game progresses, errors may occur due to differences in the implementation of the emulator. In this case, manual adjustment of the `*.fm2` file is the only way to correct it.
144 |
--------------------------------------------------------------------------------
/docs/index.md:
--------------------------------------------------------------------------------
1 | ---
2 | # https://vitepress.dev/reference/default-theme-home-page
3 | layout: home
4 |
5 | hero:
6 | name: "NES Vue"
7 | text: "A NES emulator for Vue 3"
8 | image:
9 | src: /nes-vue.svg
10 | alt: NESVue
11 | actions:
12 | - theme: alt
13 | text: Playground
14 | link: https://taiyuuki.github.io/nes-vue
15 | - theme: brand
16 | text: Getting Started
17 | link: /guide/getting-started
18 |
19 | features:
20 | - title: 💡Easy To Use
21 | details: Simply import and use it.
22 | - title: 💾Save
23 | details: Support saving and loading.
24 | - title: 📺TAS
25 | details: Supports playing TAS videos.
26 | - title: 🎮Gamepad
27 | details: Support gamepad.
28 | - title: 🔧Cheat Code
29 | details: Support cheat code.
30 | - title: 👯Multiplayer
31 | details: Support for dual players。
32 | ---
--------------------------------------------------------------------------------
/docs/public/android-chrome-192x192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/taiyuuki/nes-vue/2ecceb265b47867bb83fee28a430ff3ccc0b0312/docs/public/android-chrome-192x192.png
--------------------------------------------------------------------------------
/docs/public/android-chrome-512x512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/taiyuuki/nes-vue/2ecceb265b47867bb83fee28a430ff3ccc0b0312/docs/public/android-chrome-512x512.png
--------------------------------------------------------------------------------
/docs/public/apple-touch-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/taiyuuki/nes-vue/2ecceb265b47867bb83fee28a430ff3ccc0b0312/docs/public/apple-touch-icon.png
--------------------------------------------------------------------------------
/docs/public/favicon-16x16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/taiyuuki/nes-vue/2ecceb265b47867bb83fee28a430ff3ccc0b0312/docs/public/favicon-16x16.png
--------------------------------------------------------------------------------
/docs/public/favicon-32x32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/taiyuuki/nes-vue/2ecceb265b47867bb83fee28a430ff3ccc0b0312/docs/public/favicon-32x32.png
--------------------------------------------------------------------------------
/docs/public/nes-vue.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/docs/public/site.webmanifest:
--------------------------------------------------------------------------------
1 | {
2 | "name": "NES Vue",
3 | "short_name": "NES Vue",
4 | "icons": [
5 | {
6 | "src": "/android-chrome-192x192.png",
7 | "sizes": "192x192",
8 | "type": "image/png"
9 | },
10 | {
11 | "src": "/android-chrome-512x512.png",
12 | "sizes": "512x512",
13 | "type": "image/png"
14 | }
15 | ],
16 | "theme_color": "#ffffff",
17 | "background_color": "#ffffff",
18 | "display": "standalone"
19 | }
--------------------------------------------------------------------------------
/docs/zh/guide/cheat.md:
--------------------------------------------------------------------------------
1 | # 金手指
2 |
3 | v1.8.0开始,nes-vue支持金手指功能。
4 |
5 | ## 使用
6 |
7 | 金手指格式为`xxxx-yz-vv`,例如`079F-01-01`:
8 |
9 | ::: code-group
10 | ```vue [vue-js]
11 |
32 |
33 |
34 |
38 |
39 |
40 |
41 |
42 | ```
43 |
44 | ```vue [vue-ts]
45 |
68 |
69 |
70 |
74 |
75 |
76 |
77 |
78 | ```
79 | :::
80 |
81 | ## 金手指格式说明
82 |
83 | nes-vue采用的是兼容`VirtuaNES`的金手指格式,例如`079F-01-01`,其中`079F`表示内存地址,中间`01`的`0`表示修改类型,中间`01`的`1`表示数值长度,右侧的`01`表示数值。
84 |
85 | 修改类型取值范围是:0-3,其中`0`表示始终修改,`1`表示只修改一次,`2`表示保证值不大于,`3`表示保证值不小于。
86 |
87 | 更详细的信息请自行查阅`VirtuaNES`的金手指相关内容。
--------------------------------------------------------------------------------
/docs/zh/guide/controller.md:
--------------------------------------------------------------------------------
1 | # 控制器
2 |
3 | [`p1`](/zh/guide/props#p1)和[`p2`](/zh/guide/props#p2)属性可以自定义控制器对应的键盘按键,默认值如下:
4 |
5 | ```js
6 | p1 = {
7 | UP: 'KeyW',
8 | DOWN: 'KeyS',
9 | LEFT: 'KeyA',
10 | RIGHT: 'KeyD',
11 | A: 'KeyK',
12 | B: 'KeyJ',
13 | C: 'KeyI',
14 | D: 'KeyU',
15 | SELECT: 'Digit2',
16 | START: 'Digit1'
17 | }
18 | p2 = {
19 | UP: 'ArrowUp',
20 | DOWN: 'ArrowDown',
21 | LEFT: 'ArrowLeft',
22 | RIGHT: 'ArrowRight',
23 | A: 'Numpad2',
24 | B: 'Numpad1',
25 | C: 'Numpad5',
26 | D: 'Numpad4'
27 | }
28 | ```
29 |
30 | 属性值是 [KeyboardEvent.code](https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/code),每个字段都是可选的,如果缺省会使用默认值。
31 |
32 | ## 用其他方式操作游戏
33 |
34 | 强烈推荐使用 [v-gamepad](/zh/guide/directives#v-gamepad) 指令,它可以给任意HTML元素绑定游戏控制器。
35 |
36 | 除此之外,你还可以手动触发document上的`keydown`和`keyup`事件来操作游戏,但建议只在`v-gamepad`无法满足你的需求时这么做,这里提供一个示例:
37 |
38 | ```vue
39 |
49 |
50 |
51 |
54 |
55 |
56 | ```
57 |
58 | 触摸`button`元素即可触发游戏控制器的`RIGHT`按钮。
59 |
60 | 使用`v-gamepad`指令可以达到和上面同样的效果,且性能相对会更好:
61 |
62 | ```vue
63 |
66 |
67 |
68 |
71 |
72 |
73 | ```
74 |
75 | 详细用法请查看[v-gamepad](/zh/guide/directives#v-gamepad)。
76 |
77 | ## 手柄
78 |
79 | 组件内置了对手柄的支持,无需额外配置,即插即用。
--------------------------------------------------------------------------------
/docs/zh/guide/directives.md:
--------------------------------------------------------------------------------
1 | # 指令
2 |
3 | ## v-gamepad
4 |
5 | `nes-vue` 提供了 `v-gamepad` 指令,可以给HTML元素绑定游戏控制器。例如:
6 |
7 | ```vue
8 |
11 |
12 |
13 |
16 |
17 |
18 | ```
19 |
20 | 这样,点击`button`元素即可触发游戏控制器的`B`按钮。
21 |
22 | 属性值可以是`'UP'`、`'DOWN'`、`'LEFT'`、`'RIGHT'`、`'A'`、`'B'`、`'C'`、`'D'`、`'SELECT'`或`'START'`。**注意,如果使用的是字面值,必须添加引号**。
23 |
24 | 你也可以在同一个元素上绑定多个按键,典型的例子就是在`button`元素上绑定`A`和`B`按钮:
25 |
26 | ```vue
27 |
30 |
31 |
32 |
35 |
36 |
37 | ```
38 |
39 | ### 参数和修饰符
40 |
41 | 默认情况下,`v-gamepad` 绑定的是鼠标点击事件(`mousedown` 和 `mouseup`),控制玩家P1。
42 |
43 | 如果要绑定移动端的触摸事件,需要添加`touch`参数(默认是`mouse`):
44 |
45 | ```vue
46 |
49 |
50 |
51 |
54 |
55 |
56 | ```
57 |
58 | 如果要控制P2,需要添加`p2`修饰符(默认是`p1`):
59 |
60 | ```vue
61 |
64 |
65 |
66 |
69 |
70 |
71 | ```
--------------------------------------------------------------------------------
/docs/zh/guide/events.md:
--------------------------------------------------------------------------------
1 | # 事件
2 |
3 | ### @fps
4 |
5 | ```ts
6 | @fps -> function(fps: number)
7 | ```
8 |
9 | 获取游戏FPS,每秒触发一次。
10 |
11 | ```vue
12 |
17 |
18 |
19 |
23 |
24 | ```
25 |
26 | ### @success
27 |
28 | ```ts
29 | @success -> function()
30 | ```
31 |
32 | ROM加载成功时触发。
33 |
34 | ### @saved
35 |
36 | ```ts
37 | @saved -> function({id, message, target})
38 | ```
39 |
40 | 存档后触发。
41 |
42 | ### @loaded
43 |
44 | ```ts
45 | @loaded -> function({id, message, target})
46 | ```
47 |
48 | 读档后触发。
49 |
50 | ### @removed
51 |
52 | ```ts
53 | @removed -> function(id)
54 | ```
55 |
56 | 删除存档后触发。
57 |
58 | ### @error
59 |
60 | ```ts
61 | @error -> funciont({code, message})
62 | ```
63 |
64 | 发生错误时触发,其中code是一个`number`:
65 |
66 | * 0:表示ROM读取错误
67 | * 1:表示存档错误
68 | * 2:表示读档错误
69 | * 3:表示播放录像错误
--------------------------------------------------------------------------------
/docs/zh/guide/getting-started.md:
--------------------------------------------------------------------------------
1 | # 开始使用
2 |
3 | ## 安装
4 |
5 | ```shell
6 | npm i nes-vue
7 | ```
8 | ## 引入组件
9 |
10 | ```vue
11 |
14 |
15 |
21 |
22 | ```
23 |
24 | ## 游乐场
25 |
26 | 在线体验:[Playground](https://taiyuuki.github.io/nes-vue)
--------------------------------------------------------------------------------
/docs/zh/guide/methods.md:
--------------------------------------------------------------------------------
1 | # 方法
2 |
3 | 所有方法都挂载于组件实例上,如果你使用TS,可以通过`nes-vue`提供的`NesVueInstance`获取实例类型。
4 |
5 | ```vue
6 |
13 |
14 |
15 |
19 |
20 | ```
21 |
22 | ## start
23 |
24 | 开始游戏
25 |
26 | ```ts
27 | start(url?: string) => void
28 | ```
29 |
30 | `start`的主要作用是让停止的游戏开始运行,一般**不需要url参数** 。
31 |
32 | 如果携带参数,且参数与当前游戏的URL不一致,就会切换游戏,但强烈建议不要这么做,切换游戏建议用[props](/zh/guide/props#url),重启游戏建议用[reset](#reset)
33 |
34 | ::: tip
35 | 在大多数情况下,点击屏幕中间的文字(也就是[label](/zh/guide/props#label))即可开始游戏,不需要用`start`这个方法。
36 | :::
37 |
38 | ::: warning
39 | 如果你一定要用`start`来切换游戏, 那就必须使用 **v-model** 指令绑定url属性,这样在切换游戏后,nes-vue组件才会更新url的值。
40 | :::
41 |
42 | ## reset
43 |
44 | 重启游戏。
45 |
46 | ```ts
47 | reset() => void
48 | ```
49 |
50 | ## stop
51 |
52 | 停止游戏。
53 |
54 | ```ts
55 | stop() => void
56 | ```
57 |
58 | ## pause
59 |
60 | 游戏暂停,可恢复。
61 |
62 | ```ts
63 | pause() => void
64 | ```
65 |
66 | ## play
67 |
68 | 恢复游戏。
69 |
70 | ```ts
71 | play() => void
72 | ```
73 |
74 | ## save
75 |
76 | 存档
77 |
78 | ::: warning
79 | 只有在游戏运行时才能存档、读档,读档还需要保证存档与游戏的一致性。
80 | :::
81 |
82 | ```ts
83 | save(id: string) => void
84 | ```
85 |
86 | 默认情况下,存档是保存在 indexedDB,你可以设置[storage](/zh/guide/props#storage)属性让其保存在localStorage。
87 |
88 | 每个存档大约200kB,如果需要保存较多的数据,建议使用默认的 indexedDB。
89 |
90 | ## load
91 |
92 | 读档
93 |
94 | ```ts
95 | load(id: string) => void
96 | ```
97 |
98 | 存档、读档示例:
99 |
100 | ::: code-group
101 | ```vue [vue-js]
102 |
119 |
120 |
121 |
127 |
128 |
129 |
130 | ```
131 | ```vue [vue-ts]
132 |
151 |
152 |
153 |
159 |
160 |
161 |
162 | ```
163 | :::
164 |
165 | ## remove
166 |
167 | 删除存档。
168 |
169 | ```ts
170 | remove(id: string) => void
171 | ```
172 |
173 | ## clear
174 |
175 | 清空所有存档。
176 |
177 | ```ts
178 | clear() => void
179 | ```
180 |
181 | ## screenshot
182 |
183 | 截图
184 |
185 | ```ts
186 | screenshot(download?: boolean, imageName?: string) => HTMLImageElement
187 | ```
188 |
189 | 调用`screenshot(true)` 会在浏览器中开始下载游戏截图。
190 |
191 | 返回值是截图的image元素。
192 |
193 | ## fm2URL
194 |
195 | ```ts
196 | fm2URL(url: string) => Promise
197 | ```
198 |
199 | 读取`*.fm2`录像文件,见[播放录像](/zh/guide/replay)。
200 |
201 | ## fm2Text
202 |
203 | ```ts
204 | fm2Text(text: string) => Promise
205 | ```
206 |
207 | 读取`*.fm2`录像文件文本,见[播放录像](/zh/guide/replay)。
208 |
209 | ## fm2Play
210 |
211 | ```ts
212 | fm2Play() => void
213 | ```
214 |
215 | 开始播放录像,见[播放录像](/zh/guide/replay)。
216 |
217 | ## fm2Stop
218 |
219 | ```ts
220 | fm2Stop() => void
221 | ```
222 |
223 | 停止播放录像,见[播放录像](/zh/guide/replay)。
224 |
225 | ## cheatCode
226 |
227 | ```ts
228 | cheatCode(code: string) => void
229 | ```
230 |
231 | 添加金手指。见[金手指](/zh/guide/cheat)。
232 |
233 | ## cancelCheatCode
234 |
235 | ```ts
236 | cancelCheatCode(code: string) => void
237 | ```
238 |
239 | 取消添加的金手指。见[金手指](/zh/guide/cheat)。
240 |
241 | ## cancelCheatCodeAll
242 |
243 | ```ts
244 | cancelCheatCodeAll() => void
245 | ```
246 |
247 | 取消所有添加的金手指。见[金手指](/zh/guide/cheat)。
--------------------------------------------------------------------------------
/docs/zh/guide/props.md:
--------------------------------------------------------------------------------
1 | # 属性
2 |
3 | 除`url`外,其他属性都是可选属性。
4 |
5 | ```vue
6 |
7 |
10 |
11 | ```
12 |
13 | ## 全部属性
14 |
15 | ### url
16 |
17 | * Type `string`
18 |
19 | NES游戏的ROM地址,**必须!**
20 |
21 | 如果要切换游戏,只需用响应式数据绑定url,然后修改url的值即可:
22 |
23 | ```vue
24 |
33 |
34 |
35 |
36 |
37 |
38 | ```
39 |
40 | ### width
41 |
42 | * Type `string | number`
43 | * 默认值 `256`
44 |
45 | 游戏画面宽度,可以有单位,默认是px。
46 |
47 | ### height
48 |
49 | * Type `string | number`
50 | * 默认值 `240`
51 |
52 | 游戏画面高度,可以有单位,默认是px。
53 |
54 | ::: tip
55 | 注意保持width和height的比例为256×240。
56 | :::
57 |
58 | ```vue
59 |
60 |
65 |
66 | ```
67 |
68 | ### label
69 |
70 | * Type `string`
71 | * 默认值 `"Game Start"`
72 |
73 | 游戏运行前画面上的显示文字,点击文字开始游戏。
74 |
75 | ### gain
76 |
77 | * Type `number`
78 | * 默认值 `100`
79 |
80 | 游戏音量,介于[0, 100]之间。
81 |
82 | ### no-clip
83 |
84 | * Type `boolean`
85 | * 默认值 `false`
86 |
87 | 是否剪切画面边缘,false=游戏画面的边缘将剪切8像素,true=不剪切。
88 |
89 | 设置此属性可以解决部分游戏画面边缘显示不全的问题,且画面紧凑没有黑边,但也会造成很多游戏画面边缘材质闪烁,请酌情使用。
90 |
91 | ### auto-start
92 |
93 | * Type `boolean`
94 | * 默认值 `false`
95 |
96 | 组件挂载后自动开始游戏。
97 |
98 | :::warning
99 | nes-vue使用AudioContext API实现音频播放,由于浏览器的安全策略,游戏只会在用户发生交互(例如鼠标点击)后才会运行,所以请谨慎使用这个属性。
100 | 如果要使用这个属性,请将nes-vue置于用户点击后才会加载的组件中。
101 | :::
102 |
103 | ### turbo
104 |
105 | * Type `number`
106 | * 默认值 `16`
107 |
108 | 连发键每秒频率 介于[5, 25]之间。
109 |
110 | ### storage
111 |
112 | * Type `boolean`
113 | * 默认值 `false`
114 |
115 | 设置此属性,游戏存档会使用localStorage保存,默认是indexedDB, 详情见[方法 - save](/zh/guide/methods#save)。
116 |
117 | ### db-name
118 |
119 | * Type `string`
120 | * 默认值 `"nes-vue"`
121 |
122 | indexedDB数据库名称
123 |
124 | ### p1
125 |
126 | * Type `object`
127 | * 默认值 见[控制器](/zh/guide/controller)
128 |
129 | 玩家1控制器
130 |
131 | ### p2
132 |
133 | * Type `object`
134 | * 默认值 见[控制器](/zh/guide/controller)
135 |
136 | 玩家2控制器
137 |
138 | ### debugger
139 |
140 | * Type boolean
141 | * 默认值 false
142 |
143 | 错误信息输出到控制台。
144 |
145 |
--------------------------------------------------------------------------------
/docs/zh/guide/replay.md:
--------------------------------------------------------------------------------
1 | # 播放录像
2 |
3 | 从 v1.5.0 开始,新增播放 `*.fm2` 录像文件的功能,录像文件可以在 [TASVideos](https://tasvideos.org/) 下载。
4 |
5 | 这里提供两种方式来播放 `*.fm2` 文件。
6 |
7 | ## 请求fm2文件
8 |
9 | 第一种:通过URL读取*.fm2文件
10 |
11 | ::: code-group
12 | ```vue [vue-js]
13 |
27 |
28 |
29 |
35 |
36 |
37 | ```
38 |
39 | ```vue [vue-ts]
40 |
56 |
57 |
58 |
64 |
65 |
66 | ```
67 |
68 | :::
69 |
70 | ## 读取纯文本
71 |
72 | 第二种:直接读取 `*.fm2` 文件的纯文本形式的字符串
73 |
74 | ::: code-group
75 | ```vue [vue-js]
76 |
89 |
90 |
91 |
97 |
98 |
99 | ```
100 |
101 | ```vue [vue-ts]
102 |
117 |
118 |
119 |
125 |
126 |
127 | ```
128 | :::
129 |
130 | ## 注意事项
131 |
132 | 关于录像播放,这里有几点需要注意:
133 |
134 | * 请确保录像使用的游戏版本与游戏ROM的版本完全一致,日版、美版、欧版、修改版、翻译版,不能混同。
135 |
136 | * 不同的游戏录像由于开始帧的位置差异,可能需要手动调整,这里提供了第二个参数来微调帧数。
137 |
138 | ```ts
139 | nes.value.fm2Play(-1) // 提前1帧
140 | nes.value.fm2Play(2) // 延迟2帧
141 | ```
142 |
143 | 具体需要调整多少,只能靠自己测试。
144 |
145 | * 即便完全相同的游戏版本,开始帧也完全对齐,随着游戏的进行,也可能会出现差错,这是模拟器的实现差异造成的,在这种情况下,只能靠手动调整 `*.fm2` 文件来修正,没有其他办法。
146 |
--------------------------------------------------------------------------------
/docs/zh/index.md:
--------------------------------------------------------------------------------
1 | ---
2 | # https://vitepress.dev/reference/default-theme-home-page
3 | layout: home
4 |
5 | hero:
6 | name: "NES Vue"
7 | text: "用于Vue 3的NES模拟器组件"
8 | image:
9 | src: /nes-vue.svg
10 | alt: NESVue
11 | actions:
12 | - theme: alt
13 | text: 在线游乐场
14 | link: https://taiyuuki.github.io/nes-vue
15 | - theme: brand
16 | text: 开始使用
17 | link: /zh/guide/getting-started
18 |
19 | features:
20 | - title: 💡易用
21 | details: 简单引入就可以使用。
22 | - title: 💾存档
23 | details: 支持存档、读档。
24 | - title: 📺TAS
25 | details: 支持播放TAS录像。
26 | - title: 🎮手柄
27 | details: 支持手柄。
28 | - title: 🔧金手指
29 | details: 支持金手指。
30 | - title: 👯双人
31 | details: 支持双人玩家。
32 | ---
33 |
--------------------------------------------------------------------------------
/eslint.config.js:
--------------------------------------------------------------------------------
1 | import tyk_eslint from '@taiyuuki/eslint-config'
2 |
3 | export default tyk_eslint({
4 | ts: true,
5 | vue: true,
6 | ignores: [
7 | '**/docs',
8 | '**/playground',
9 | ],
10 | rules: { '@typescript-eslint/no-explicit-any': 'off' },
11 | })
12 |
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | nes-vue demo
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "nes-vue",
3 | "version": "1.9.0",
4 | "description": "A NES(FC) emulator component for Vue 3",
5 | "module": "./dist/nes-vue.es.js",
6 | "main": "./dist/nes-vue.umd.js",
7 | "type": "module",
8 | "license": "MIT",
9 | "files": [
10 | "dist"
11 | ],
12 | "exports": {
13 | ".": {
14 | "import": "./dist/nes-vue.es.js",
15 | "types": "./dist/index.d.ts"
16 | }
17 | },
18 | "keywords": [
19 | "fc",
20 | "nes",
21 | "jsnes",
22 | "vue",
23 | "nes emulator",
24 | "fc emulator"
25 | ],
26 | "repository": {
27 | "type": "git",
28 | "url": "https://github.com/taiyuuki/nes-vue.git"
29 | },
30 | "pnpm": {
31 | "peerDependencyRules": {
32 | "ignoreMissing": [
33 | "@algolia/client-search",
34 | "search-insights"
35 | ]
36 | }
37 | },
38 | "scripts": {
39 | "lint": "eslint --ext .js,.ts,.vue ./ --fix",
40 | "dev": "vite",
41 | "build": "vue-tsc --noEmit && vite build",
42 | "test": "vitest",
43 | "patch": "bump patch",
44 | "minor": "bump minor",
45 | "publish": "pnpm publish --registry https://registry.npmjs.org",
46 | "docs:dev": "vitepress dev docs",
47 | "docs:build": "vitepress build docs",
48 | "docs:build-gitee": "set GITEE=1 && pnpx vitepress build docs",
49 | "docs:preview": "vitepress preview docs"
50 | },
51 | "dependencies": {
52 | "@nesjs/core": "^1.0.9"
53 | },
54 | "devDependencies": {
55 | "@taiyuuki/eslint-config": "^1.4.18",
56 | "@taiyuuki/utils": "^0.5.3",
57 | "@types/node": "^22.14.1",
58 | "@vitejs/plugin-vue": "^5.2.3",
59 | "eslint": "^9.24.0",
60 | "nes-vue": "link:./dist/nes-vue.e.js",
61 | "rollup-plugin-delete": "^3.0.1",
62 | "typescript": "^5.8.3",
63 | "vite": "^6.2.6",
64 | "vite-plugin-dts": "^4.5.3",
65 | "vitepress": "1.0.0-beta.1",
66 | "vitest": "^3.1.1",
67 | "vue": "^3.5.13",
68 | "vue-tsc": "^2.2.8"
69 | }
70 | }
--------------------------------------------------------------------------------
/playground/.editorconfig:
--------------------------------------------------------------------------------
1 | root = true
2 |
3 | [*]
4 | charset = utf-8
5 | indent_style = space
6 | indent_size = 2
7 | end_of_line = lf
8 | insert_final_newline = true
9 | trim_trailing_whitespace = true
10 |
--------------------------------------------------------------------------------
/playground/.eslintignore:
--------------------------------------------------------------------------------
1 | /dist
2 | /node_modules
3 | /src-ssr
--------------------------------------------------------------------------------
/playground/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "root": true,
3 | "extends": "@taiyuuki/eslint-config-vue-unimport",
4 |
5 | "rules": {
6 | "import/no-unresolved":
7 | ["error", {
8 | "ignore": [
9 | "~pages",
10 | "virtual:generated-layouts",
11 | "virtual:generated-pages",
12 | "virtual:vue-component-preview",
13 | ""
14 | ]
15 | }]
16 | }
17 |
18 | }
--------------------------------------------------------------------------------
/playground/LICENSE.md:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2023 taiyuuki
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sub license, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
6 |
7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
8 |
9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,FITNESS FOR A PARTICULAR PURPOSE AND NON INFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
--------------------------------------------------------------------------------
/playground/README.md:
--------------------------------------------------------------------------------
1 | ### NesVue Playground
2 |
3 | ### Install
4 |
5 | ```shell
6 | pnpm install
7 | ```
8 |
9 | ### Run
10 |
11 | ```shell
12 | pnpm dev
13 | ```
14 |
15 |
--------------------------------------------------------------------------------
/playground/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 | NesVue Playground
14 |
15 |
16 |
17 |
18 |
19 |
20 |
--------------------------------------------------------------------------------
/playground/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "nes-vue-playground",
3 | "version": "0.0.1",
4 | "description": "nes-vue playground",
5 | "author": "taiyuuki ",
6 | "repository": {
7 | "type": "git",
8 | "url": "https://github.com/taiyuuki/nes-vue"
9 | },
10 | "bugs": "https://github.com/taiyuuki/nes-vue/issues",
11 | "homepage": "https://github.com/taiyuuki/nes-vue#readme",
12 | "private": true,
13 | "scripts": {
14 | "lint": "eslint --ext .js,.ts,.vue ./ --fix",
15 | "dev": "vite",
16 | "build": "vue-tsc && vite build",
17 | "preview": "vite preview"
18 | },
19 | "dependencies": {
20 | "@taiyuuki/utils": "^0.5.3",
21 | "@vue/repl": "^4.5.1",
22 | "jszip": "^3.10.1",
23 | "pinia": "^3.0.2",
24 | "pinia-plugin-persistedstate": "^4.2.0",
25 | "vue": "3.5.13",
26 | "vue-router": "^4.5.0"
27 | },
28 | "devDependencies": {
29 | "@taiyuuki/eslint-config-vue-unimport": "0.0.5",
30 | "@types/node": "^22.14.1",
31 | "@vitejs/plugin-vue": "^5.2.3",
32 | "autoprefixer": "^10.4.21",
33 | "eslint": "^9.24.0",
34 | "eslint-plugin-import": "^2.31.0",
35 | "postcss": "^8.5.3",
36 | "postcss-html": "^1.8.0",
37 | "sass": "^1.86.3",
38 | "typescript": "^5.8.3",
39 | "unplugin-auto-import": "^19.1.2",
40 | "unplugin-vue-components": "^28.5.0",
41 | "vite": "^6.2.6",
42 | "vite-plugin-pages": "^0.33.0",
43 | "vite-plugin-vue-layouts": "^0.11.0",
44 | "vue-tsc": "^2.2.8"
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/playground/postcss.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | plugins: [
3 | require('autoprefixer')({
4 | overrideBrowserslist: [
5 | 'last 4 Chrome versions',
6 | 'last 4 Firefox versions',
7 | 'last 4 Edge versions',
8 | 'last 4 Safari versions',
9 | 'last 4 Android versions',
10 | 'last 4 ChromeAndroid versions',
11 | 'last 4 FirefoxAndroid versions',
12 | 'last 4 iOS versions',
13 | ],
14 | }),
15 | ],
16 | }
17 |
--------------------------------------------------------------------------------
/playground/public/Mighty Final Fight (USA).nes:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/taiyuuki/nes-vue/2ecceb265b47867bb83fee28a430ff3ccc0b0312/playground/public/Mighty Final Fight (USA).nes
--------------------------------------------------------------------------------
/playground/public/Mitsume ga Tooru (Japan).nes:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/taiyuuki/nes-vue/2ecceb265b47867bb83fee28a430ff3ccc0b0312/playground/public/Mitsume ga Tooru (Japan).nes
--------------------------------------------------------------------------------
/playground/public/Super Mario Bros (JU).nes:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/taiyuuki/nes-vue/2ecceb265b47867bb83fee28a430ff3ccc0b0312/playground/public/Super Mario Bros (JU).nes
--------------------------------------------------------------------------------
/playground/public/Super Mario Bros 3.nes:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/taiyuuki/nes-vue/2ecceb265b47867bb83fee28a430ff3ccc0b0312/playground/public/Super Mario Bros 3.nes
--------------------------------------------------------------------------------
/playground/public/icons/android-chrome-192x192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/taiyuuki/nes-vue/2ecceb265b47867bb83fee28a430ff3ccc0b0312/playground/public/icons/android-chrome-192x192.png
--------------------------------------------------------------------------------
/playground/public/icons/android-chrome-512x512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/taiyuuki/nes-vue/2ecceb265b47867bb83fee28a430ff3ccc0b0312/playground/public/icons/android-chrome-512x512.png
--------------------------------------------------------------------------------
/playground/public/icons/apple-touch-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/taiyuuki/nes-vue/2ecceb265b47867bb83fee28a430ff3ccc0b0312/playground/public/icons/apple-touch-icon.png
--------------------------------------------------------------------------------
/playground/public/icons/browserconfig.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | #00aba9
7 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/playground/public/icons/favicon-16x16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/taiyuuki/nes-vue/2ecceb265b47867bb83fee28a430ff3ccc0b0312/playground/public/icons/favicon-16x16.png
--------------------------------------------------------------------------------
/playground/public/icons/favicon-32x32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/taiyuuki/nes-vue/2ecceb265b47867bb83fee28a430ff3ccc0b0312/playground/public/icons/favicon-32x32.png
--------------------------------------------------------------------------------
/playground/public/icons/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/taiyuuki/nes-vue/2ecceb265b47867bb83fee28a430ff3ccc0b0312/playground/public/icons/favicon.ico
--------------------------------------------------------------------------------
/playground/public/icons/mstile-150x150.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/taiyuuki/nes-vue/2ecceb265b47867bb83fee28a430ff3ccc0b0312/playground/public/icons/mstile-150x150.png
--------------------------------------------------------------------------------
/playground/public/icons/safari-pinned-tab.svg:
--------------------------------------------------------------------------------
1 |
2 |
4 |
89 |
--------------------------------------------------------------------------------
/playground/public/icons/site.webmanifest:
--------------------------------------------------------------------------------
1 | {
2 | "name": "",
3 | "short_name": "",
4 | "icons": [
5 | {
6 | "src": "./android-chrome-192x192.png",
7 | "sizes": "192x192",
8 | "type": "image/png"
9 | },
10 | {
11 | "src": "./android-chrome-512x512.png",
12 | "sizes": "512x512",
13 | "type": "image/png"
14 | }
15 | ],
16 | "theme_color": "#ffffff",
17 | "background_color": "#ffffff",
18 | "display": "standalone"
19 | }
--------------------------------------------------------------------------------
/playground/public/vite.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/playground/src/App.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/playground/src/assets/vue.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/playground/src/auto-imports.d.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable */
2 | /* prettier-ignore */
3 | // @ts-nocheck
4 | // noinspection JSUnusedGlobalSymbols
5 | // Generated by unplugin-auto-import
6 | // biome-ignore lint: disable
7 | export {}
8 | declare global {
9 | const $: typeof import('vue/macros')['$']
10 | const $$: typeof import('vue/macros')['$$']
11 | const $computed: typeof import('vue/macros')['$computed']
12 | const $customRef: typeof import('vue/macros')['$customRef']
13 | const $ref: typeof import('vue/macros')['$ref']
14 | const $shallowRef: typeof import('vue/macros')['$shallowRef']
15 | const $toRef: typeof import('vue/macros')['$toRef']
16 | const EffectScope: typeof import('vue')['EffectScope']
17 | const acceptHMRUpdate: typeof import('pinia')['acceptHMRUpdate']
18 | const computed: typeof import('vue')['computed']
19 | const createApp: typeof import('vue')['createApp']
20 | const createPinia: typeof import('pinia')['createPinia']
21 | const customRef: typeof import('vue')['customRef']
22 | const defineAsyncComponent: typeof import('vue')['defineAsyncComponent']
23 | const defineComponent: typeof import('vue')['defineComponent']
24 | const defineStore: typeof import('pinia')['defineStore']
25 | const effectScope: typeof import('vue')['effectScope']
26 | const getActivePinia: typeof import('pinia')['getActivePinia']
27 | const getCurrentInstance: typeof import('vue')['getCurrentInstance']
28 | const getCurrentScope: typeof import('vue')['getCurrentScope']
29 | const h: typeof import('vue')['h']
30 | const inject: typeof import('vue')['inject']
31 | const isProxy: typeof import('vue')['isProxy']
32 | const isReactive: typeof import('vue')['isReactive']
33 | const isReadonly: typeof import('vue')['isReadonly']
34 | const isRef: typeof import('vue')['isRef']
35 | const mapActions: typeof import('pinia')['mapActions']
36 | const mapGetters: typeof import('pinia')['mapGetters']
37 | const mapState: typeof import('pinia')['mapState']
38 | const mapStores: typeof import('pinia')['mapStores']
39 | const mapWritableState: typeof import('pinia')['mapWritableState']
40 | const markRaw: typeof import('vue')['markRaw']
41 | const nextTick: typeof import('vue')['nextTick']
42 | const onActivated: typeof import('vue')['onActivated']
43 | const onBeforeMount: typeof import('vue')['onBeforeMount']
44 | const onBeforeRouteLeave: typeof import('vue-router')['onBeforeRouteLeave']
45 | const onBeforeRouteUpdate: typeof import('vue-router')['onBeforeRouteUpdate']
46 | const onBeforeUnmount: typeof import('vue')['onBeforeUnmount']
47 | const onBeforeUpdate: typeof import('vue')['onBeforeUpdate']
48 | const onDeactivated: typeof import('vue')['onDeactivated']
49 | const onErrorCaptured: typeof import('vue')['onErrorCaptured']
50 | const onMounted: typeof import('vue')['onMounted']
51 | const onRenderTracked: typeof import('vue')['onRenderTracked']
52 | const onRenderTriggered: typeof import('vue')['onRenderTriggered']
53 | const onScopeDispose: typeof import('vue')['onScopeDispose']
54 | const onServerPrefetch: typeof import('vue')['onServerPrefetch']
55 | const onUnmounted: typeof import('vue')['onUnmounted']
56 | const onUpdated: typeof import('vue')['onUpdated']
57 | const onWatcherCleanup: typeof import('vue')['onWatcherCleanup']
58 | const provide: typeof import('vue')['provide']
59 | const reactive: typeof import('vue')['reactive']
60 | const readonly: typeof import('vue')['readonly']
61 | const ref: typeof import('vue')['ref']
62 | const resolveComponent: typeof import('vue')['resolveComponent']
63 | const resolveDirective: typeof import('vue')['resolveDirective']
64 | const setActivePinia: typeof import('pinia')['setActivePinia']
65 | const setMapStoreSuffix: typeof import('pinia')['setMapStoreSuffix']
66 | const shallowReactive: typeof import('vue')['shallowReactive']
67 | const shallowReadonly: typeof import('vue')['shallowReadonly']
68 | const shallowRef: typeof import('vue')['shallowRef']
69 | const storeToRefs: typeof import('pinia')['storeToRefs']
70 | const toRaw: typeof import('vue')['toRaw']
71 | const toRef: typeof import('vue')['toRef']
72 | const toRefs: typeof import('vue')['toRefs']
73 | const toValue: typeof import('vue')['toValue']
74 | const triggerRef: typeof import('vue')['triggerRef']
75 | const unref: typeof import('vue')['unref']
76 | const useAttrs: typeof import('vue')['useAttrs']
77 | const useCssModule: typeof import('vue')['useCssModule']
78 | const useCssVars: typeof import('vue')['useCssVars']
79 | const useId: typeof import('vue')['useId']
80 | const useLink: typeof import('vue-router')['useLink']
81 | const useModel: typeof import('vue')['useModel']
82 | const useRoute: typeof import('vue-router')['useRoute']
83 | const useRouter: typeof import('vue-router')['useRouter']
84 | const useSlots: typeof import('vue')['useSlots']
85 | const useTemplateRef: typeof import('vue')['useTemplateRef']
86 | const watch: typeof import('vue')['watch']
87 | const watchEffect: typeof import('vue')['watchEffect']
88 | const watchPostEffect: typeof import('vue')['watchPostEffect']
89 | const watchSyncEffect: typeof import('vue')['watchSyncEffect']
90 | }
91 | // for type re-export
92 | declare global {
93 | // @ts-ignore
94 | export type { Component, Slot, Slots, ComponentPublicInstance, ComputedRef, DirectiveBinding, ExtractDefaultPropTypes, ExtractPropTypes, ExtractPublicPropTypes, InjectionKey, PropType, Ref, MaybeRef, MaybeRefOrGetter, VNode, WritableComputedRef } from 'vue'
95 | import('vue')
96 | }
97 |
--------------------------------------------------------------------------------
/playground/src/components.d.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable */
2 | // @ts-nocheck
3 | // Generated by unplugin-vue-components
4 | // Read more: https://github.com/vuejs/core/pull/3399
5 | // biome-ignore lint: disable
6 | export {}
7 |
8 | /* prettier-ignore */
9 | declare module 'vue' {
10 | export interface GlobalComponents {
11 | IconDownload: typeof import('./components/Icon/IconDownload.vue')['default']
12 | IconGitee: typeof import('./components/Icon/IconGitee.vue')['default']
13 | IconGithub: typeof import('./components/Icon/IconGithub.vue')['default']
14 | IconMoon: typeof import('./components/Icon/IconMoon.vue')['default']
15 | IconSun: typeof import('./components/Icon/IconSun.vue')['default']
16 | MainHeader: typeof import('./components/MainHeader.vue')['default']
17 | RouterLink: typeof import('vue-router')['RouterLink']
18 | RouterView: typeof import('vue-router')['RouterView']
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/playground/src/components/Icon/IconDownload.vue:
--------------------------------------------------------------------------------
1 |
2 |
41 |
42 |
--------------------------------------------------------------------------------
/playground/src/components/Icon/IconGitee.vue:
--------------------------------------------------------------------------------
1 |
2 |
9 |
10 |
--------------------------------------------------------------------------------
/playground/src/components/Icon/IconGithub.vue:
--------------------------------------------------------------------------------
1 |
2 |
12 |
13 |
--------------------------------------------------------------------------------
/playground/src/components/Icon/IconMoon.vue:
--------------------------------------------------------------------------------
1 |
2 |
11 |
12 |
--------------------------------------------------------------------------------
/playground/src/components/Icon/IconSun.vue:
--------------------------------------------------------------------------------
1 |
2 |
43 |
44 |
--------------------------------------------------------------------------------
/playground/src/components/MainHeader.vue:
--------------------------------------------------------------------------------
1 |
33 |
34 |
35 |
64 |
65 |
66 |
98 |
--------------------------------------------------------------------------------
/playground/src/css/app.scss:
--------------------------------------------------------------------------------
1 | @font-face {
2 | font-family: Emoji;
3 | src:
4 | local("Apple Color Emojiji"),
5 | local("Segoe Emoji"),
6 | local("Noto Color Emoji");
7 | unicode-range: U+1F000-1F644, U+203C-3299;
8 | }
9 |
10 | body {
11 | margin: 0;
12 | padding: 0;
13 | font-family:
14 | system-ui,
15 | -apple-system,
16 | "Segoe UI",
17 | Roboto,
18 | Emoji,
19 | Helvetica,
20 | Arial,
21 | sans-serif;
22 | }
23 |
24 | .dark {
25 | color-scheme: dark;
26 | }
27 |
--------------------------------------------------------------------------------
/playground/src/download/files.ts:
--------------------------------------------------------------------------------
1 | import { repoURL } from 'src/template'
2 |
3 | export const html = `
4 |
5 |
6 |
7 |
8 |
9 | Vite App
10 |
11 |
12 |
13 |
14 |
15 |
16 | `
17 |
18 | export const pkg = `{
19 | "name": "nes-vue-demo",
20 | "version": "0.0.0",
21 | "scripts": {
22 | "dev": "vite",
23 | "build": "vite build",
24 | "serve": "vite preview"
25 | },
26 | "dependencies": {
27 | "nes-vue": "^1.5.0",
28 | "vue": "^3.3.0"
29 | },
30 | "devDependencies": {
31 | "@vitejs/plugin-vue": "^3.2.0",
32 | "@vue/compiler-sfc": "^3.2.0",
33 | "vite": "^3.2.7"
34 | }
35 | }`
36 |
37 | export const main = `import { createApp } from 'vue'
38 | import App from './App.vue'
39 |
40 | createApp(App).mount('#app')
41 | `
42 |
43 | export const viteConfig = `import { defineConfig } from 'vite'
44 | import vue from '@vitejs/plugin-vue'
45 |
46 | // https://vitejs.dev/config/
47 | export default defineConfig({
48 | plugins: [vue()]
49 | })
50 | `
51 |
52 | export const readme = `# NesVue Demo
53 |
54 | This is a [nes-vue](${repoURL}) project demo.
55 |
56 | To start:
57 |
58 | \`\`\`sh
59 | npm install
60 | npm run dev
61 |
62 | # if using yarn:
63 | yarn
64 | yarn dev
65 | \`\`\`
66 | `
67 |
--------------------------------------------------------------------------------
/playground/src/download/index.ts:
--------------------------------------------------------------------------------
1 | import { download_blob, url_to_blob } from '@taiyuuki/utils'
2 | import type { ReplStore } from '@vue/repl'
3 | import { fm2s, roms } from './roms'
4 | import { html, main, pkg, readme, viteConfig } from './files'
5 |
6 | export async function downloadProject(store: ReplStore) {
7 | if (!confirm('Download project files?')) {
8 | return
9 | }
10 | const JSzip = (await import('jszip')).default
11 |
12 | const zip = new JSzip()
13 | const srcFolder = zip.folder('src')!
14 | const publicFolder = zip.folder('public')!
15 |
16 | const files = store.getFiles()
17 |
18 | // base files
19 | zip.file('index.html', html)
20 | zip.file('package.json', pkg)
21 | zip.file('vite.config.js', viteConfig)
22 | zip.file('README.md', readme)
23 | srcFolder.file('main.js', main)
24 |
25 | // playground files
26 | for (const file in files) {
27 | srcFolder.file(file, files[file])
28 | }
29 |
30 | // .nes files
31 | for await (const rom of roms) {
32 | const romBlob = await url_to_blob(rom)
33 | publicFolder.file(rom, romBlob)
34 | }
35 |
36 | // .fm2 files
37 | for await (const fm2 of fm2s) {
38 | const fm2Blob = await url_to_blob(fm2[0])
39 | publicFolder.file(fm2[0], fm2Blob)
40 | }
41 |
42 | const project = await zip.generateAsync({ type: 'blob' })
43 | download_blob(project, 'nes-vue-demo')
44 | }
45 |
--------------------------------------------------------------------------------
/playground/src/download/roms.ts:
--------------------------------------------------------------------------------
1 | export const roms = [
2 | 'Super Mario Bros (JU).nes',
3 | 'Super Mario Bros 3.nes',
4 | 'Mighty Final Fight (USA).nes',
5 | 'Mitsume ga Tooru (Japan).nes',
6 | ]
7 |
8 | export const fm2s: [string, number][] = [
9 | ['happylee-supermariobros,warped.fm2', 0],
10 | ['lordtom,maru,tompa-smb3-warps.fm2', -1],
11 | ['xipov3-mightyfinalfight.fm2', 0],
12 | ['jy,aiqiyou-mitsumegatooru.fm2', 0],
13 | ]
14 |
--------------------------------------------------------------------------------
/playground/src/layouts/MainLayout.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/playground/src/main.ts:
--------------------------------------------------------------------------------
1 | import { createApp } from 'vue'
2 | import App from './App.vue'
3 | import route from './router'
4 | import pinia from './stores'
5 | import './css/app.scss'
6 |
7 | createApp(App).use(pinia).use(route).mount('#app')
8 |
--------------------------------------------------------------------------------
/playground/src/pages/404.vue:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
7 | 404
8 |
9 |
10 |
--------------------------------------------------------------------------------
/playground/src/pages/index.vue:
--------------------------------------------------------------------------------
1 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
26 |
--------------------------------------------------------------------------------
/playground/src/router/index.ts:
--------------------------------------------------------------------------------
1 | import {
2 | createMemoryHistory,
3 | createRouter,
4 | createWebHashHistory,
5 | } from 'vue-router'
6 |
7 | import { setupLayouts } from 'virtual:generated-layouts'
8 | import generatedRoutes from 'virtual:generated-pages'
9 |
10 | const routes = setupLayouts(generatedRoutes)
11 | routes.push({
12 | path: '/:catchAll(.*)*',
13 | component: () => import('pages/404.vue'),
14 | })
15 | // hash mode
16 | const createHistory = import.meta.env.SSR ? createMemoryHistory : createWebHashHistory
17 |
18 | const route = createRouter({
19 | scrollBehavior: () => ({ left: 0, top: 0 }),
20 | routes,
21 | history: createHistory(),
22 | })
23 | export default route
24 |
--------------------------------------------------------------------------------
/playground/src/stores/dark.ts:
--------------------------------------------------------------------------------
1 | import { defineStore } from 'pinia'
2 |
3 | export const useDark = defineStore('dark', {
4 | state: () => ({ on: false }),
5 | persist: true,
6 | })
7 |
--------------------------------------------------------------------------------
/playground/src/stores/index.ts:
--------------------------------------------------------------------------------
1 | import { createPinia } from 'pinia'
2 | import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'
3 |
4 | const pinia = createPinia()
5 | pinia.use(piniaPluginPersistedstate)
6 |
7 | export default pinia
8 |
--------------------------------------------------------------------------------
/playground/src/template.ts:
--------------------------------------------------------------------------------
1 | import { fm2s, roms } from './download/roms'
2 |
3 | const template = `
4 |
5 |
6 |
7 |
14 |
15 |
16 |
30 |
33 |
36 |
39 |
42 |
45 |
46 |
47 | `
48 |
49 | let script = `
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
21 |
--------------------------------------------------------------------------------
/src/animation/index.ts:
--------------------------------------------------------------------------------
1 |
2 | const WIDTH = 256
3 | const HEIGHT = 240
4 | let animationframeID: number
5 | let framebuffer_u8!: Uint8ClampedArray,
6 | framebuffer_u32!: Uint32Array
7 | let canvas_ctx!: CanvasRenderingContext2D
8 | const imageData = new ImageData(WIDTH, HEIGHT)
9 |
10 | // const playback = new Playback
11 |
12 | function onFrame(u32: number[]) {
13 | for (let i = 0; i < 256 * 240; i += 1) {
14 | framebuffer_u32[i] = 0xFF000000 | u32[i]
15 | }
16 | }
17 |
18 | function putImageData() {
19 | imageData.data.set(framebuffer_u8)
20 | canvas_ctx.putImageData(imageData, 0, 0)
21 | }
22 |
23 | function animationFrame(cvs: HTMLCanvasElement) {
24 | canvas_ctx = cvs.getContext('2d')!
25 | canvas_ctx.fillStyle = 'black'
26 | canvas_ctx.fillRect(0, 0, WIDTH, HEIGHT)
27 | const buffer = new ArrayBuffer(imageData.data.length)
28 | framebuffer_u8 = new Uint8ClampedArray(buffer)
29 | framebuffer_u32 = new Uint32Array(buffer)
30 |
31 | // playback.clearDB()
32 | animationframeID = requestAnimationFrame(onAnimationFrame)
33 |
34 | function onAnimationFrame() {
35 | cancelAnimationFrame(animationframeID)
36 | animationframeID = requestAnimationFrame(onAnimationFrame)
37 | putImageData()
38 | }
39 | }
40 |
41 | // function rewind() {
42 | // const frame = nes.frameCounter - 1
43 | // if (frame in playback.frameData) {
44 | // const frameData = decompressArray(playback.action(frame))
45 | // for (let i = 0; i < frameData.length; i++) {
46 | // framebuffer_u32[i] = frameData[i]
47 | // }
48 | // putImageData()
49 | // nes.frameCounter--
50 | // }
51 | // else {
52 | // playback.load((data) => {
53 | // loadNesData(data, () => {
54 | // console.error('Failed to load nes data')
55 | // })
56 |
57 | // rewind()
58 | // })
59 | // }
60 | // }
61 |
62 | // function forward() {
63 | // const frame = nes.frameCounter + 1
64 | // if (frame in playback.frameData) {
65 | // const frameData = decompressArray(playback.action(frame))
66 | // for (let i = 0; i < frameData.length; i++) {
67 | // framebuffer_u32[i] = frameData[i]
68 | // }
69 | // putImageData()
70 | // nes.frameCounter++
71 | // }
72 | // else {
73 | // nesFrame()
74 | // }
75 | // }
76 |
77 | // function frameAction() {
78 | // const frame = nes.frameCounter + 1
79 | // if (frame in playback.frameData) {
80 | // forward()
81 | // setTimeout(frameAction, 1000 / 60)
82 | // }
83 | // else {
84 | // resume()
85 | // }
86 | // }
87 |
88 | function fitInParent(cvs: HTMLCanvasElement) {
89 | const parent = cvs.parentNode as HTMLElement
90 | const parentWidth = parent.clientWidth
91 | const parentHeight = parent.clientHeight
92 | const parentRatio = parentWidth / parentHeight
93 | const desiredRatio = WIDTH / HEIGHT
94 | if (desiredRatio < parentRatio) {
95 | cvs.style.height = `${parentHeight}px`
96 | cvs.style.width = `${Math.round(parentHeight + desiredRatio)}px`
97 | }
98 | else {
99 | cvs.style.width = `${parentWidth}px`
100 | cvs.style.height = `${Math.round(parentWidth / desiredRatio)}px`
101 | }
102 | }
103 |
104 | function animationStop() {
105 | cancelAnimationFrame(animationframeID)
106 | }
107 |
108 | function cut(cvs: HTMLCanvasElement) {
109 | const image = new Image()
110 | image.src = cvs.toDataURL('image/png')
111 |
112 | return image
113 | }
114 |
115 | export {
116 | WIDTH,
117 | HEIGHT,
118 | onFrame,
119 | animationFrame,
120 | animationStop,
121 | fitInParent,
122 |
123 | // rewind,
124 | // forward,
125 | cut,
126 |
127 | // frameAction,
128 | }
129 |
--------------------------------------------------------------------------------
/src/audio/index.ts:
--------------------------------------------------------------------------------
1 | import { nesFrame } from 'src/nes'
2 | import { math_between } from '@taiyuuki/utils'
3 |
4 | let audio_ctx = new AudioContext()
5 | let script_processor: ScriptProcessorNode
6 | let gain = 1
7 | const AUDIO_BUFFERING = 512
8 | const SAMPLE_COUNT = 4 * 1024
9 | const SAMPLE_MASK = SAMPLE_COUNT - 1
10 | const audio_samples_L = new Float32Array(SAMPLE_COUNT)
11 | const audio_samples_R = new Float32Array(SAMPLE_COUNT)
12 | let audio_write_cursor = 0
13 | let audio_read_cursor = 0
14 |
15 | function audio_remain() {
16 | return audio_write_cursor - audio_read_cursor & SAMPLE_MASK
17 | }
18 |
19 | function onAudioSample(left: number, right: number) {
20 | audio_samples_L[audio_write_cursor] = left
21 | audio_samples_R[audio_write_cursor] = right
22 | audio_write_cursor = audio_write_cursor + 1 & SAMPLE_MASK
23 | }
24 |
25 | function getSampleRate() {
26 | if (!window.AudioContext) {
27 | return 44100
28 | }
29 | const myCtx = new window.AudioContext()
30 | const sampleRate = myCtx.sampleRate
31 | myCtx.close()
32 |
33 | return sampleRate
34 | }
35 |
36 | function audioFrame() {
37 | audio_ctx = new AudioContext()
38 | script_processor = audio_ctx.createScriptProcessor(AUDIO_BUFFERING, 0, 2)
39 | script_processor.onaudioprocess = (event: AudioProcessingEvent) => {
40 | const dst = event.outputBuffer
41 | const len = dst.length
42 | if (audio_remain() < AUDIO_BUFFERING) {
43 | nesFrame()
44 | }
45 | const dst_l = dst.getChannelData(0)
46 | const dst_r = dst.getChannelData(1)
47 | for (let i = 0; i < len; i++) {
48 | const src_idx = audio_read_cursor + i & SAMPLE_MASK
49 | dst_l[i] = audio_samples_L[src_idx] * gain
50 | dst_r[i] = audio_samples_R[src_idx] * gain
51 | }
52 |
53 | audio_read_cursor = audio_read_cursor + len & SAMPLE_MASK
54 | }
55 | script_processor.connect(audio_ctx.destination)
56 | }
57 |
58 | function audioStop() {
59 | script_processor.disconnect(audio_ctx.destination)
60 | script_processor.onaudioprocess = null
61 | script_processor = {} as ScriptProcessorNode
62 |
63 | if ('close' in audio_ctx) {
64 | audio_ctx.close()
65 | }
66 | }
67 |
68 | /**
69 | * 🎮: Pause
70 | */
71 | function suspend() {
72 | audio_ctx.suspend()
73 | }
74 |
75 | /**
76 | * 🎮: Play
77 | */
78 | function resume() {
79 | audio_ctx.resume()
80 | }
81 |
82 | function setGain(n: number) {
83 | gain = math_between(n, 0, 100) / 100
84 | }
85 |
86 | export {
87 | onAudioSample,
88 | nesFrame,
89 | audioFrame,
90 | audioStop,
91 | getSampleRate,
92 | setGain,
93 | suspend,
94 | resume,
95 | }
96 |
--------------------------------------------------------------------------------
/src/components/NesVue.vue:
--------------------------------------------------------------------------------
1 |
529 |
530 |
533 |
534 |
535 |
536 |
542 |
546 |
551 | {{ label }}
552 |
553 |
554 |
555 |
--------------------------------------------------------------------------------
/src/composables/use-controller.ts:
--------------------------------------------------------------------------------
1 | import { math_between, object_keys } from '@taiyuuki/utils'
2 | import type { NesVueProps, Player } from 'src/types'
3 | import { controllerState } from 'src/nes'
4 | import { fillFalse, gpFilter } from 'src/utils'
5 | import type { ComputedRef } from 'vue'
6 | import { computed, onBeforeUnmount, onMounted, watch } from 'vue'
7 |
8 | // threshold of level.
9 | const THRESHOLD = 0.3
10 |
11 | // buttonDown - 0x41
12 | // buttonUp - 0x40
13 | // controllerState - [A, B, SELECT, START, UP, DOWN, LEFT, RIGHT]
14 | // for example: [0x40, 0x40, 0x40, 0x41, 0x40, 0x40, 0x40, 0x40]
15 | // means: Start button is pressed.
16 |
17 | const KEYS_INDEX = {
18 | A: 0,
19 | B: 1,
20 | SELECT: 2,
21 | START: 3,
22 | UP: 4,
23 | DOWN: 5,
24 | LEFT: 6,
25 | RIGHT: 7,
26 | C: 8,
27 | D: 9,
28 | }
29 |
30 | export const P1_DEFAULT = {
31 | UP: 'KeyW',
32 | DOWN: 'KeyS',
33 | LEFT: 'KeyA',
34 | RIGHT: 'KeyD',
35 | A: 'KeyK',
36 | B: 'KeyJ',
37 | C: 'KeyI',
38 | D: 'KeyU',
39 | SELECT: 'Digit2',
40 | START: 'Digit1',
41 | }
42 |
43 | export const P2_DEFAULT = {
44 | UP: 'ArrowUp',
45 | DOWN: 'ArrowDown',
46 | LEFT: 'ArrowLeft',
47 | RIGHT: 'ArrowRight',
48 | A: 'Numpad2',
49 | B: 'Numpad1',
50 | C: 'Numpad5',
51 | D: 'Numpad4',
52 | SELECT: 'NumpadDecimal',
53 | START: 'NumpadEnter',
54 | }
55 |
56 | let interval = 1000 / (2 * 16)
57 |
58 | export function emitControllerState(eventCode: string, state: 0x40 | 0x41) {
59 | controllerState.emit(eventCode, state, interval)
60 | }
61 |
62 | class GamepadManager {
63 | animationFrame: number
64 | axesHolding: Record
65 | btnHolding: Record
66 | gamepad_btns: ComputedRef<{ p1: string[], p2: string[] }>
67 |
68 | constructor(gamepad_btns: ComputedRef<{ p1: string[], p2: string[] }>) {
69 | window.addEventListener('gamepadconnected', this.connectHandler.bind(this, true))
70 | window.addEventListener('gamepaddisconnected', this.connectHandler.bind(this, false))
71 | this.animationFrame = requestAnimationFrame(this.frame.bind(this))
72 | this.btnHolding = {
73 | p1: fillFalse(20),
74 | p2: fillFalse(20),
75 | }
76 | this.axesHolding = {
77 | p1: fillFalse(4),
78 | p2: fillFalse(4),
79 | }
80 | this.gamepad_btns = gamepad_btns
81 | }
82 |
83 | get gamepads() {
84 | return gpFilter(navigator.getGamepads())
85 | }
86 |
87 | connectHandler(state: boolean, e: GamepadEvent) {
88 | if (state) {
89 | this.gamepads[e.gamepad.index] = e.gamepad
90 | }
91 | else if (this.gamepads.length === 0) {
92 | this.close()
93 | }
94 | }
95 |
96 | axesHandler(player: Player, check: boolean, aindex: number, bindex: number) {
97 | const hold = this.axesHolding[player]?.[aindex]
98 | if (check) {
99 | if (!hold) {
100 | emitControllerState(this.gamepad_btns.value[player][bindex], 0x41)
101 | this.axesHolding[player][aindex] = true
102 | }
103 | }
104 | else if (hold) {
105 | emitControllerState(this.gamepad_btns.value[player][bindex], 0x40)
106 | this.axesHolding[player][aindex] = false
107 | }
108 | }
109 |
110 | btnHandler(player: Player, btn: GamepadButton, bindex: number) {
111 | const hold = this.btnHolding[player]?.[bindex]
112 | if (btn.pressed) {
113 | if (hold) {
114 | return
115 | }
116 | emitControllerState(this.gamepad_btns.value[player][bindex], 0x41)
117 | this.btnHolding[player][bindex] = true
118 | }
119 | else if (hold) {
120 | emitControllerState(this.gamepad_btns.value[player][bindex], 0x40)
121 | this.btnHolding[player][bindex] = false
122 | }
123 | }
124 |
125 | frame() {
126 | for (let gindex = 0; gindex < this.gamepads.length; gindex++) {
127 | if (gindex > 1) {
128 | break
129 | }
130 | const player = `p${gindex + 1}` as Player
131 | const gamepad = this.gamepads[gindex]
132 |
133 | gamepad.buttons.forEach(this.btnHandler.bind(this, player))
134 |
135 | const lr = gamepad.axes[0]
136 | const tb = gamepad.axes[1]
137 |
138 | this.axesHandler(player, lr > THRESHOLD, 0, 15)
139 | this.axesHandler(player, lr < -THRESHOLD, 1, 14)
140 | this.axesHandler(player, tb > THRESHOLD, 2, 13)
141 | this.axesHandler(player, tb < -THRESHOLD, 3, 12)
142 | }
143 | }
144 |
145 | run() {
146 | this.frame()
147 | cancelAnimationFrame(this.animationFrame)
148 | this.animationFrame = requestAnimationFrame(this.run.bind(this))
149 | }
150 |
151 | close() {
152 | this.btnHolding.p1.fill(false)
153 | this.btnHolding.p2.fill(false)
154 | this.axesHolding.p1.fill(false)
155 | this.axesHolding.p2.fill(false)
156 | cancelAnimationFrame(this.animationFrame)
157 | }
158 | }
159 |
160 | export function useController(props: NesVueProps): (eventCode: string, state: 0x40 | 0x41)=> void {
161 | const p1 = computed(() => Object.assign(P1_DEFAULT, props.p1))
162 | const p2 = computed(() => Object.assign(P2_DEFAULT, props.p2))
163 |
164 | function setTurboInterval() {
165 | interval = 1000 / (2 * math_between(props.turbo as number, 5, 20))
166 | }
167 |
168 | function setController() {
169 | controllerState.init()
170 | object_keys(KEYS_INDEX).forEach(key => {
171 | const index = KEYS_INDEX[key]
172 | controllerState.on(p1.value[key], {
173 | p: 1,
174 | index,
175 | })
176 | controllerState.on(p2.value[key], {
177 | p: key === 'SELECT' || key === 'START' ? 1 : 2,
178 | index,
179 | })
180 | })
181 | }
182 |
183 | setController()
184 | setTurboInterval()
185 |
186 | watch(() => props.p1, setController, { deep: true })
187 | watch(() => props.p2, setController, { deep: true })
188 | watch(() => props.turbo, setTurboInterval)
189 |
190 | const gamepad_btns = computed(() => {
191 | return {
192 | p1: [
193 | p1.value.A,
194 | p1.value.C,
195 | p1.value.B,
196 | p1.value.D,
197 | '',
198 | '',
199 | '',
200 | '',
201 | p1.value.SELECT,
202 | p1.value.START,
203 | '',
204 | '',
205 | p1.value.UP,
206 | p1.value.DOWN,
207 | p1.value.LEFT,
208 | p1.value.RIGHT,
209 | ],
210 | p2: [
211 | p2.value.A,
212 | p2.value.C,
213 | p2.value.B,
214 | p2.value.D,
215 | '',
216 | '',
217 | '',
218 | '',
219 | p1.value.SELECT,
220 | p1.value.START,
221 | '',
222 | '',
223 | p2.value.UP,
224 | p2.value.DOWN,
225 | p2.value.LEFT,
226 | p2.value.RIGHT,
227 | ],
228 | }
229 | })
230 |
231 | const gamepad = new GamepadManager(gamepad_btns)
232 |
233 | onMounted(() => {
234 | gamepad.run()
235 | })
236 |
237 | onBeforeUnmount(() => {
238 | gamepad.close()
239 | })
240 |
241 | return emitControllerState
242 | }
243 |
--------------------------------------------------------------------------------
/src/composables/use-instance.ts:
--------------------------------------------------------------------------------
1 | import type { Component, Ref } from 'vue'
2 | import { ref } from 'vue'
3 |
4 | export const useInstance = Component>() => ref() as Ref>
5 |
6 | export const useElement = () => ref() as Ref
7 |
--------------------------------------------------------------------------------
/src/db/index.ts:
--------------------------------------------------------------------------------
1 | import { DB } from '@taiyuuki/utils'
2 |
3 | function createDB(dbName: string, storeName: string): DB {
4 | return new DB(dbName, storeName)
5 | }
6 |
7 | type DBInstance = InstanceType>
8 |
9 | export type { DBInstance }
10 |
11 | export { createDB }
12 |
--------------------------------------------------------------------------------
/src/dev/NesDemo.vue:
--------------------------------------------------------------------------------
1 |
79 |
80 |
81 |
82 |
93 |
94 |
95 |
98 |
101 |
106 |
111 |
116 |
122 |
125 |
128 |
131 |
132 |
133 |
136 |
139 |
142 |
145 |
148 |
151 |
154 |
155 |
156 |
157 |
184 |
--------------------------------------------------------------------------------
/src/directives/v-gamepad.ts:
--------------------------------------------------------------------------------
1 | import type { Directive } from 'vue'
2 | import type { ControllerKey } from 'src/types'
3 | import { P1_DEFAULT, P2_DEFAULT, emitControllerState } from 'src/composables/use-controller'
4 | import { key_in, object_keys } from '@taiyuuki/utils'
5 |
6 | const _events: Record void)[]>> = {}
7 |
8 | function addEvent(el: HTMLElement, eventId: string, type: string, fn: (e: E)=> void, options?: AddEventListenerOptions | boolean) {
9 | el.addEventListener(type, fn, options)
10 | _events[eventId] = _events[eventId] || {}
11 | _events[eventId][type] = _events[eventId][type] || []
12 | _events[eventId][type].push(fn)
13 | }
14 |
15 | function removeEventAll(el: HTMLElement, eventId: string) {
16 | if (_events[eventId]) {
17 | object_keys(_events[eventId]).forEach(type => {
18 | _events[eventId][type].forEach(fn => {
19 | el.removeEventListener(type, fn)
20 | })
21 | delete _events[eventId][type]
22 | })
23 | delete _events[eventId]
24 | }
25 | }
26 |
27 | function resolveKeys(key: ControllerKey | ControllerKey[]) {
28 | if (typeof key === 'string') {
29 | key = [key]
30 | }
31 |
32 | return Array.from(new Set(key)).map(key => key.toUpperCase())
33 | .sort() as ControllerKey[]
34 | }
35 |
36 | /**
37 | * v-gamepad directive
38 | * @example
39 | * ```vue
40 | *
43 | *
44 | *
45 | *
48 | *
49 | *
50 | * ```
51 | */
52 | export const vGamepad: Directive = (target, binding) => {
53 | if (!binding.value) {
54 | throw '[nes-vue] v-gamepad value is required'
55 | }
56 | const arg = (binding.arg ?? '').toLowerCase()
57 | const checkPlayer = binding.modifiers.p2 || binding.modifiers.P2
58 | const player = checkPlayer ? P2_DEFAULT : P1_DEFAULT
59 | if (binding.oldValue) {
60 | const oldKeys = resolveKeys(binding.oldValue).filter(key => key_in(key, player))
61 | const oldEventId = `gamepad-${`${arg + (checkPlayer ? 'p2' : 'p1')}-${oldKeys.join('-')}`}`
62 | removeEventAll(target, oldEventId)
63 | }
64 | const keys = resolveKeys(binding.value).filter(key => key_in(key, player))
65 | const eventId = `gamepad-${`${arg + (checkPlayer ? 'p2' : 'p1')}-${keys.join('-')}`}`
66 | if (keys.length) {
67 | if (arg === 'touch') {
68 | addEvent(target, eventId, 'touchstart', () => {
69 | keys.forEach(key => {
70 | emitControllerState(player[key], 0x41)
71 | })
72 | })
73 | addEvent(target, eventId, 'touchend', () => {
74 | keys.forEach(key => {
75 | emitControllerState(player[key], 0x40)
76 | })
77 | })
78 | addEvent(target, eventId, 'touchcancel', () => {
79 | keys.forEach(key => {
80 | emitControllerState(player[key], 0x40)
81 | })
82 | })
83 | } else {
84 | addEvent(target, eventId, 'mousedown', () => {
85 | keys.forEach(key => {
86 | emitControllerState(player[key], 0x41)
87 | })
88 | })
89 | addEvent(target, eventId, 'mouseup', () => {
90 | keys.forEach(key => {
91 | emitControllerState(player[key], 0x40)
92 | })
93 | })
94 | addEvent(target, eventId, 'mouseleave', () => {
95 | keys.forEach(key => {
96 | emitControllerState(player[key], 0x40)
97 | })
98 | })
99 | if (arg && arg !== 'mouse') {
100 | console.warn('[nes-vue] argument should be mouse or touch, changed to default: mouse')
101 | }
102 | }
103 | }
104 | }
105 |
--------------------------------------------------------------------------------
/src/env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
3 | declare module '*.vue' {
4 | import type { DefineComponent } from 'vue'
5 |
6 | const component: DefineComponent
7 | export default component
8 | }
9 |
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
1 | import NesVue from 'src/components/NesVue.vue'
2 | import { nes } from 'src/nes'
3 | import { vGamepad } from 'src/directives/v-gamepad'
4 |
5 | export type NesVueInstance = InstanceType
6 | export type {
7 | Controller,
8 | NesVueProps,
9 | EmitErrorObj,
10 | SavedOrLoaded,
11 | } from 'src/types'
12 | export { NesVue, nes, vGamepad }
13 |
--------------------------------------------------------------------------------
/src/main.ts:
--------------------------------------------------------------------------------
1 | import { createApp } from 'vue'
2 | import App from './App.vue'
3 |
4 | createApp(App).mount('#app')
5 |
--------------------------------------------------------------------------------
/src/nes/index.ts:
--------------------------------------------------------------------------------
1 |
2 | import { NES } from '@nesjs/core'
3 | import { onFrame } from 'src/animation'
4 | import { getSampleRate, onAudioSample } from 'src/audio'
5 | import type { ControllerStateType, EmitErrorObj, SaveData } from 'src/types'
6 |
7 | const nes = new NES({
8 | onFrame,
9 | onAudioSample,
10 | sampleRate: getSampleRate(),
11 | })
12 |
13 | const rom = { buffer: null as Uint8Array | null }
14 |
15 | function nesFrame() {
16 | nes.frame()
17 | }
18 |
19 | function getNesData(url: string) {
20 | return {
21 | path: url,
22 | data: nes.toJSON(),
23 | }
24 | }
25 |
26 | function loadNesData(saveData: SaveData, emitError: (error: EmitErrorObj)=> void, url?: string) {
27 | if (url && saveData.path !== url) {
28 | return emitError({
29 | code: 2,
30 | message: `Load Error: The saved data is inconsistent with the current game, saved: ${saveData.path}, current: ${url}.`,
31 | })
32 | }
33 | if (!rom.buffer) {
34 | return emitError({
35 | code: 3,
36 | message: 'Load Error: NES ROM is not loaded.',
37 | })
38 | }
39 | try {
40 | nes.fromJSON(saveData.data)
41 | }
42 | catch (e) {
43 | console.error(e)
44 | emitError({
45 | code: 3,
46 | message: 'Load Error: The saved data is invalid.',
47 | })
48 | }
49 | }
50 |
51 | class ControllerState {
52 | _events: Record
53 | _auto: Record>
58 |
59 | constructor() {
60 | this._events = {}
61 | this._auto = {
62 | 1: {
63 | 8: {
64 | timeout: 0,
65 | beDown: false,
66 | once: true,
67 | },
68 | 9: {
69 | timeout: 0,
70 | beDown: false,
71 | once: true,
72 | },
73 | },
74 | 2: {
75 | 8: {
76 | timeout: 0,
77 | beDown: false,
78 | once: true,
79 | },
80 | 9: {
81 | timeout: 0,
82 | beDown: false,
83 | once: true,
84 | },
85 | },
86 | }
87 | }
88 |
89 | on(keyCode: string, state: ControllerStateType) {
90 | if (!this._events[keyCode]) {
91 | this._events[keyCode] = []
92 | }
93 | this._events[keyCode].push(state)
94 | }
95 |
96 | emit(keyboadCode: string, stateValue: 0x40 | 0x41, interval: number) {
97 | this._events[keyboadCode]?.forEach(event => {
98 | const p = event.p as 1 | 2
99 | const state = nes.controllers[p].state
100 | if (event.index <= 7) {
101 | state[event.index] = stateValue
102 | }
103 | else {
104 | const auto = this._auto[p][event.index]
105 | if (stateValue === 0x41) {
106 | if (auto.once) {
107 | state[event.index - 8] = 0x41
108 | auto.timeout = window.setInterval(() => {
109 | state[event.index - 8] = auto.beDown ? 0x41 : 0x40
110 | auto.beDown = !auto.beDown
111 | }, interval)
112 | auto.once = false
113 | }
114 | }
115 | else {
116 | clearInterval(auto.timeout)
117 | state[event.index - 8] = 0x40
118 | auto.once = true
119 | auto.beDown = false
120 | }
121 | }
122 | })
123 | }
124 |
125 | getState(keyCode: string) {
126 | return this._events[keyCode]
127 | }
128 |
129 | init() {
130 | this._events = {}
131 | }
132 | }
133 |
134 | const controllerState = new ControllerState()
135 |
136 | export {
137 | nes,
138 | nesFrame,
139 | getNesData,
140 | loadNesData,
141 | rom,
142 | controllerState,
143 | }
144 |
--------------------------------------------------------------------------------
/src/types.ts:
--------------------------------------------------------------------------------
1 | export interface EmitErrorObj {
2 | code: number
3 | message: string
4 | }
5 |
6 | export interface Controller {
7 | UP: string
8 | DOWN: string
9 | LEFT: string
10 | RIGHT: string
11 | A: string
12 | B: string
13 | C: string
14 | D: string
15 | SELECT: string
16 | START: string
17 | }
18 |
19 | export type ControllerKey = keyof Controller
20 |
21 | export interface ControllerStateType {
22 | p: number
23 | index: number
24 | }
25 |
26 | export interface SavedOrLoaded {
27 | id: string
28 | message: string
29 | target: 'indexedDB' | 'localStorage'
30 | }
31 |
32 | export interface NesVueProps {
33 | url: string
34 | autoStart?: boolean
35 | width?: number | string
36 | height?: number | string
37 | label?: string
38 | gain?: number
39 | clip?: boolean
40 | storage?: boolean
41 | debugger?: boolean
42 | turbo?: number
43 | p1?: Partial
44 | p2?: Partial
45 | }
46 |
47 | export interface NesVueEmits {
48 | (e: 'fps', fps: number): void
49 | (e: 'success'): void
50 | (e: 'error', error: EmitErrorObj): void
51 | (e: 'saved', saved: SavedOrLoaded): void
52 | (e: 'loaded', loaded: SavedOrLoaded): void
53 | (e: 'update:url', path: string): void
54 | (e: 'removed', id: string): void
55 | }
56 |
57 | export interface Automatic {
58 | timeout: number
59 | beDown: boolean
60 | once: boolean
61 | }
62 |
63 | export type Player = 'p1' | 'p2'
64 |
65 | export interface FrameData { [frame: number]: number[] }
66 |
67 | export interface SaveData {
68 | path: string
69 | data: {
70 | data: Uint8Array | string,
71 | compress: boolean
72 | }
73 | }
74 |
75 | export interface PlaybackData {
76 | length: number
77 | frameList: number[]
78 | frameData: FrameData
79 | nes: SaveData
80 | }
81 |
82 | export interface TasState {
83 | [frame: number]: {
84 | p1: number[]
85 | p2: number[]
86 | }
87 | }
88 |
89 | export type CheatCodeMap = Record
90 |
--------------------------------------------------------------------------------
/src/utils/index.ts:
--------------------------------------------------------------------------------
1 |
2 | export function fillFalse(num: number): boolean[] {
3 | return Array(num).fill(false)
4 | }
5 |
6 | export function gpFilter(arr: T[]): NonNullable[] {
7 | return arr.filter(Boolean) as NonNullable[]
8 | }
9 |
10 | export function toHexNumber(str: string) {
11 | return Number(`0x${str}`)
12 | }
13 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "esnext",
4 | "useDefineForClassFields": true,
5 | "module": "esnext",
6 | "moduleResolution": "node",
7 | "strict": true,
8 | "jsx": "preserve",
9 | "sourceMap": true,
10 | "resolveJsonModule": true,
11 | "isolatedModules": true,
12 | "esModuleInterop": true,
13 | "declaration": true,
14 | "lib": [
15 | "esnext",
16 | "dom"
17 | ],
18 | "skipLibCheck": true,
19 | "baseUrl": "./",
20 | "paths": {
21 | "src/*": [
22 | "src/*"
23 | ],
24 | }
25 | },
26 | "include": [
27 | "src/**/*.ts",
28 | "src/**/*.d.ts",
29 | "src/**/*.tsx",
30 | "src/**/*.vue"
31 | ],
32 | "references": [
33 | {
34 | "path": "./tsconfig.node.json"
35 | }
36 | ]
37 | }
--------------------------------------------------------------------------------
/tsconfig.node.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "composite": true,
4 | "module": "esnext",
5 | "moduleResolution": "node",
6 | "allowSyntheticDefaultImports": true
7 | },
8 | "include": [
9 | "vite.config.ts"
10 | ]
11 | }
--------------------------------------------------------------------------------
/vite.config.ts:
--------------------------------------------------------------------------------
1 | import path from 'node:path'
2 | import { defineConfig } from 'vite'
3 | import vue from '@vitejs/plugin-vue'
4 | import rollupDelete from 'rollup-plugin-delete'
5 | import dts from 'vite-plugin-dts'
6 |
7 | function resolve(dir: string) {
8 | return path.join(__dirname, dir)
9 | }
10 |
11 | // https://vitejs.dev/config/
12 | export default defineConfig({
13 | plugins: [
14 | dts({
15 | outDir: 'dist',
16 | staticImport: true,
17 | insertTypesEntry: true,
18 | rollupTypes: true,
19 | }),
20 | vue(),
21 | ],
22 | resolve: {
23 | alias: {
24 | '@': resolve('src'),
25 | 'src': resolve('src'),
26 | 'common': resolve('src/common'),
27 | 'components': resolve('src/components'),
28 | 'composables': resolve('src/composables'),
29 | },
30 | },
31 | build: {
32 | lib: {
33 | entry: resolve('src/index.ts'),
34 | name: 'NesVue',
35 | fileName: format => `nes-vue.${format}.js`,
36 | },
37 | rollupOptions: {
38 | external: ['vue'],
39 | output: {
40 | // 为外部依赖提供全局变量
41 | globals: { NesVue: 'NesVue' },
42 | },
43 | plugins: [
44 | rollupDelete({
45 | targets: ['dist/*.{ico,txt,svg,nes,NES,fm2}'],
46 | hook: 'generateBundle',
47 | }),
48 | ],
49 | },
50 | },
51 | })
52 |
--------------------------------------------------------------------------------