├── .editorconfig ├── .github ├── ISSUE_TEMPLATE │ └── bug_report.md └── workflows │ └── ci.yml ├── .gitignore ├── .gitmodules ├── .vscode ├── launch.json └── tasks.json ├── LICENSE ├── README.md ├── __tests__ ├── models │ ├── entrance.test.ts │ ├── fill.test.ts │ ├── item.test.ts │ ├── itemCollection.test.ts │ ├── itemOverrides.test.ts │ ├── items.test.ts │ ├── randomizer.test.ts │ ├── randomizerSettings.test.ts │ ├── tricks.test.ts │ └── world.test.ts └── utils.ts ├── _config.yml ├── appveyor.yml ├── binding.gyp ├── electron-builder.json ├── icon.png ├── jest.config.js ├── jest.config.ts ├── native └── randomprime.cpp ├── package-lock.json ├── package.json ├── postcss.config.js ├── postinstall-web.js ├── postinstall.js ├── src ├── client │ ├── .editorconfig │ ├── .gitignore │ ├── README.md │ ├── angular.json │ ├── browserslist │ ├── e2e │ │ ├── src │ │ │ ├── app.e2e-spec.ts │ │ │ └── app.po.ts │ │ └── tsconfig.json │ ├── package.json │ ├── src │ │ ├── app │ │ │ ├── app-routing.module.ts │ │ │ ├── app.component.html │ │ │ ├── app.component.scss │ │ │ ├── app.component.spec.ts │ │ │ ├── app.component.ts │ │ │ ├── app.module.ts │ │ │ ├── components │ │ │ │ └── common │ │ │ │ │ ├── modal.component.ts │ │ │ │ │ └── picklist-form.component.ts │ │ │ ├── directives │ │ │ │ ├── disable-control.directive.spec.ts │ │ │ │ └── disable-control.directive.ts │ │ │ ├── game-details │ │ │ │ ├── game-details.component.html │ │ │ │ ├── game-details.component.scss │ │ │ │ ├── game-details.component.spec.ts │ │ │ │ └── game-details.component.ts │ │ │ ├── generate-game │ │ │ │ ├── generate-game.component.html │ │ │ │ ├── generate-game.component.scss │ │ │ │ ├── generate-game.component.spec.ts │ │ │ │ └── generate-game.component.ts │ │ │ ├── help │ │ │ │ ├── help.component.html │ │ │ │ ├── help.component.scss │ │ │ │ ├── help.component.spec.ts │ │ │ │ └── help.component.ts │ │ │ ├── import-settings-modal │ │ │ │ ├── import-settings-modal.component.html │ │ │ │ ├── import-settings-modal.component.scss │ │ │ │ ├── import-settings-modal.component.spec.ts │ │ │ │ └── import-settings-modal.component.ts │ │ │ ├── item-overrides │ │ │ │ ├── item-overrides.component.html │ │ │ │ ├── item-overrides.component.scss │ │ │ │ ├── item-overrides.component.spec.ts │ │ │ │ └── item-overrides.component.ts │ │ │ ├── picklist │ │ │ │ ├── picklist.component.html │ │ │ │ ├── picklist.component.scss │ │ │ │ ├── picklist.component.spec.ts │ │ │ │ └── picklist.component.ts │ │ │ ├── prime-iso-diagnostics-modal │ │ │ │ ├── prime-iso-diagnostics-modal.component.html │ │ │ │ ├── prime-iso-diagnostics-modal.component.scss │ │ │ │ ├── prime-iso-diagnostics-modal.component.spec.ts │ │ │ │ └── prime-iso-diagnostics-modal.component.ts │ │ │ ├── progress-modal │ │ │ │ ├── progress-modal.component.html │ │ │ │ ├── progress-modal.component.scss │ │ │ │ ├── progress-modal.component.spec.ts │ │ │ │ └── progress-modal.component.ts │ │ │ ├── randomizer │ │ │ │ ├── randomizer.component.html │ │ │ │ ├── randomizer.component.scss │ │ │ │ ├── randomizer.component.spec.ts │ │ │ │ └── randomizer.component.ts │ │ │ ├── remove-preset-modal │ │ │ │ ├── remove-preset-modal.component.html │ │ │ │ ├── remove-preset-modal.component.scss │ │ │ │ ├── remove-preset-modal.component.spec.ts │ │ │ │ └── remove-preset-modal.component.ts │ │ │ ├── save-preset-modal │ │ │ │ ├── save-preset-modal.component.html │ │ │ │ ├── save-preset-modal.component.scss │ │ │ │ ├── save-preset-modal.component.spec.ts │ │ │ │ └── save-preset-modal.component.ts │ │ │ ├── seed-history │ │ │ │ ├── seed-history.component.html │ │ │ │ ├── seed-history.component.scss │ │ │ │ ├── seed-history.component.spec.ts │ │ │ │ └── seed-history.component.ts │ │ │ ├── services │ │ │ │ ├── diagnostics.service.spec.ts │ │ │ │ ├── diagnostics.service.ts │ │ │ │ ├── electron.service.spec.ts │ │ │ │ ├── electron.service.ts │ │ │ │ ├── generator.service.spec.ts │ │ │ │ ├── generator.service.ts │ │ │ │ ├── patcher.service.spec.ts │ │ │ │ ├── patcher.service.ts │ │ │ │ ├── presets.service.spec.ts │ │ │ │ ├── presets.service.ts │ │ │ │ ├── progress.service.spec.ts │ │ │ │ ├── progress.service.ts │ │ │ │ ├── randomizer.service.spec.ts │ │ │ │ ├── randomizer.service.ts │ │ │ │ ├── seed.service.spec.ts │ │ │ │ ├── seed.service.ts │ │ │ │ ├── settings.service.spec.ts │ │ │ │ ├── settings.service.ts │ │ │ │ ├── tab.service.spec.ts │ │ │ │ ├── tab.service.ts │ │ │ │ ├── update.service.spec.ts │ │ │ │ └── update.service.ts │ │ │ ├── settings │ │ │ │ ├── customize-settings-container │ │ │ │ │ ├── customize-settings-container.component.html │ │ │ │ │ ├── customize-settings-container.component.scss │ │ │ │ │ ├── customize-settings-container.component.spec.ts │ │ │ │ │ └── customize-settings-container.component.ts │ │ │ │ ├── exclude-locations │ │ │ │ │ ├── exclude-locations.component.html │ │ │ │ │ ├── exclude-locations.component.scss │ │ │ │ │ ├── exclude-locations.component.spec.ts │ │ │ │ │ └── exclude-locations.component.ts │ │ │ │ ├── read-only-settings-container │ │ │ │ │ ├── read-only-settings-container.component.html │ │ │ │ │ ├── read-only-settings-container.component.scss │ │ │ │ │ ├── read-only-settings-container.component.spec.ts │ │ │ │ │ └── read-only-settings-container.component.ts │ │ │ │ ├── rom-settings │ │ │ │ │ ├── rom-settings.component.html │ │ │ │ │ ├── rom-settings.component.scss │ │ │ │ │ ├── rom-settings.component.spec.ts │ │ │ │ │ └── rom-settings.component.ts │ │ │ │ ├── rules │ │ │ │ │ ├── rules.component.html │ │ │ │ │ ├── rules.component.scss │ │ │ │ │ ├── rules.component.spec.ts │ │ │ │ │ └── rules.component.ts │ │ │ │ ├── settings-section.ts │ │ │ │ └── tricks │ │ │ │ │ ├── tricks.component.html │ │ │ │ │ ├── tricks.component.scss │ │ │ │ │ ├── tricks.component.spec.ts │ │ │ │ │ └── tricks.component.ts │ │ │ ├── tricks-detail-modal │ │ │ │ ├── tricks-detail-modal.component.html │ │ │ │ ├── tricks-detail-modal.component.scss │ │ │ │ ├── tricks-detail-modal.component.spec.ts │ │ │ │ └── tricks-detail-modal.component.ts │ │ │ ├── utilities.ts │ │ │ └── welcome │ │ │ │ ├── welcome.component.html │ │ │ │ ├── welcome.component.scss │ │ │ │ ├── welcome.component.spec.ts │ │ │ │ └── welcome.component.ts │ │ ├── assets │ │ │ ├── .gitkeep │ │ │ ├── help-documents │ │ │ │ ├── differences.md │ │ │ │ ├── faq.md │ │ │ │ ├── gettingStarted.md │ │ │ │ ├── images │ │ │ │ │ ├── gameDetails.png │ │ │ │ │ └── presetAndCustomize.png │ │ │ │ ├── softlocks.md │ │ │ │ └── trackers.md │ │ │ └── scss │ │ │ │ └── _bulma_overrides.scss │ │ ├── environments │ │ │ ├── environment.prod.ts │ │ │ └── environment.ts │ │ ├── favicon.ico │ │ ├── index.html │ │ ├── main.ts │ │ ├── polyfills.ts │ │ ├── styles.scss │ │ ├── test.ts │ │ └── typings.d.ts │ ├── tsconfig.app.json │ ├── tsconfig.json │ ├── tsconfig.spec.json │ └── tslint.json ├── common │ ├── constants.ts │ ├── data │ │ └── settingsDetails.ts │ └── models │ │ ├── generatedSeed.ts │ │ ├── itemOverride.ts │ │ ├── patchForm.ts │ │ ├── patcherMessage.ts │ │ ├── presetObject.ts │ │ ├── progressBar.ts │ │ ├── randomStartingItems.ts │ │ ├── randomizerForm.ts │ │ ├── tab.ts │ │ └── trickItem.ts └── electron │ ├── controllers.ts │ ├── controllers │ ├── diagnosticsController.ts │ ├── generateSeedController.ts │ ├── patcherController.ts │ ├── presetsController.ts │ ├── seedHistoryController.ts │ ├── settingsController.ts │ └── spoilerController.ts │ ├── data │ ├── names.json │ ├── presetsDefault.json │ └── visiblePointsOfNoReturn.json │ ├── enums │ ├── elevator.ts │ ├── heatDamagePrevention.ts │ ├── optionType.ts │ ├── pointOfNoReturnItems.ts │ ├── primeItem.ts │ ├── primeLocation.ts │ └── primeRegion.ts │ ├── main.ts │ ├── menu.ts │ ├── mersenneTwister.ts │ ├── models │ ├── collection.ts │ ├── entrance.ts │ ├── fill.ts │ ├── item.ts │ ├── itemCollection.ts │ ├── location.ts │ ├── locationCollection.ts │ ├── option.ts │ ├── prime │ │ ├── entranceShuffle.ts │ │ ├── excludeLocations.ts │ │ ├── fill.ts │ │ ├── itemCollection.ts │ │ ├── itemOverrides.ts │ │ ├── itemPool.ts │ │ ├── items.ts │ │ ├── layoutString.ts │ │ ├── locations.ts │ │ ├── patcher.ts │ │ ├── randomizer.ts │ │ ├── randomizerSettings.ts │ │ ├── regions │ │ │ ├── chozoRuins.ts │ │ │ ├── magmoorCaverns.ts │ │ │ ├── phazonMines.ts │ │ │ ├── phendranaDrifts.ts │ │ │ ├── root.ts │ │ │ └── tallonOverworld.ts │ │ ├── rules.ts │ │ ├── seedHistory.ts │ │ ├── spoiler.ts │ │ ├── startingItems.ts │ │ ├── tricks.ts │ │ └── world.ts │ ├── randomizerSettings.ts │ ├── region.ts │ ├── regionCollection.ts │ ├── searchResults.ts │ ├── settingsFlags.ts │ └── world.ts │ ├── randomprime.ts │ └── utilities.ts ├── tsconfig.json ├── tsconfig.test.json ├── tslint.json ├── webpack.development.js └── webpack.production.js /.editorconfig: -------------------------------------------------------------------------------- 1 | # Editor configuration, see http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | indent_style = space 7 | indent_size = 2 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.md] 12 | max_line_length = off 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | 5 | --- 6 | 7 | Version: 8 | Permalink: 9 | 10 | Description of problem: 11 | 12 | Spoiler log (if available): 13 | 14 | Verify ISO file (if available): 15 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | on: push 4 | 5 | env: 6 | CARGO_TERM_COLOR: always 7 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 8 | 9 | jobs: 10 | linux: 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | - uses: actions/checkout@v2 15 | with: 16 | submodules: recursive 17 | 18 | - name: Add Salt 19 | run: sed -i "s/getNumericSeed()/getNumericSeed('${{ secrets.RELEASE_SALT }}')/" src/electron/models/prime/randomizer.ts 20 | - name: Enable Nightly Rust Toolchain 21 | run: rustup toolchain install nightly 22 | - name: Add PowerPC toolchain 23 | run: rustup target add --toolchain nightly powerpc-unknown-linux-gnu 24 | - name: NPM Install 25 | run: npm install 26 | - name: Build Linux App 27 | run: npm run electron:linux 28 | 29 | macos: 30 | runs-on: macos-latest 31 | 32 | steps: 33 | - uses: actions/checkout@v2 34 | with: 35 | submodules: recursive 36 | 37 | - name: Add Salt 38 | run: sed -i.bak "s/getNumericSeed()/getNumericSeed('${{ secrets.RELEASE_SALT }}')/" src/electron/models/prime/randomizer.ts 39 | - name: Enable Nightly Rust Toolchain 40 | run: rustup toolchain install nightly 41 | - name: Add PowerPC toolchain 42 | run: rustup target add --toolchain nightly powerpc-unknown-linux-gnu 43 | - name: NPM Install 44 | run: npm install 45 | - name: Build macOS App 46 | run: npm run electron:mac 47 | 48 | windows: 49 | runs-on: windows-latest 50 | 51 | steps: 52 | - uses: actions/checkout@v2 53 | with: 54 | submodules: recursive 55 | 56 | - name: Add Salt 57 | run: $x = get-content src/electron/models/prime/randomizer.ts | %{$_ -replace "getNumericSeed\(\)", "getNumericSeed('${{ secrets.RELEASE_SALT }}')" }; set-content src/electron/models/prime/randomizer.ts $x 58 | - name: Enable nightly Rust Toolchain 59 | run: rustup toolchain install nightly 60 | - name: Add PowerPC toolchain 61 | run: rustup target add --toolchain nightly powerpc-unknown-linux-gnu 62 | - name: NPM Install 63 | run: npm install 64 | - name: Build Windows App 65 | run: npm run electron:windows 66 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See http://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # compiled output 4 | /dist 5 | /tmp 6 | /out-tsc 7 | /app-builds 8 | /release 9 | main.js 10 | main.js.LICENSE.txt 11 | src/**/*.js 12 | *.js.map 13 | common/**/*.js 14 | /build 15 | /bin 16 | settings.json 17 | /coverage 18 | 19 | # dependencies 20 | /node_modules 21 | 22 | # IDEs and editors 23 | /.idea 24 | .project 25 | .classpath 26 | .c9/ 27 | *.launch 28 | .settings/ 29 | *.sublime-workspace 30 | 31 | # IDE - VSCode 32 | .vscode/* 33 | !.vscode/settings.json 34 | !.vscode/tasks.json 35 | !.vscode/launch.json 36 | !.vscode/extensions.json 37 | 38 | # misc 39 | /.sass-cache 40 | /connect.lock 41 | /coverage 42 | /libpeerconnection.log 43 | npm-debug.log 44 | testem.log 45 | /typings 46 | package-lock.json 47 | 48 | # e2e 49 | /e2e/*.js 50 | !/e2e/protractor.conf.js 51 | /e2e/*.map 52 | 53 | # System Files 54 | .DS_Store 55 | Thumbs.db 56 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "randomprime"] 2 | path = randomprime 3 | url = https://github.com/BashPrime/randomprime.git 4 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "type": "node", 6 | "request": "launch", 7 | "name": "Electron: Main", 8 | "protocol": "inspector", 9 | "runtimeExecutable": "${workspaceFolder}/node_modules/.bin/electron", 10 | "runtimeArgs": [ 11 | "--remote-debugging-port=9223", 12 | ".", 13 | "--serve" 14 | ], 15 | "windows": { 16 | "runtimeExecutable": "${workspaceFolder}/node_modules/.bin/electron.cmd" 17 | }, 18 | "sourceMaps": true, 19 | "console": "integratedTerminal", 20 | "preLaunchTask": "electron-build" 21 | }, 22 | { 23 | "type": "node", 24 | "request": "launch", 25 | "name": "Mocha All", 26 | "program": "${workspaceFolder}/node_modules/mocha/bin/_mocha", 27 | "args": [ 28 | "--require", 29 | "ts-node/register", 30 | "test/**/*.test.ts" 31 | ], 32 | "console": "integratedTerminal", 33 | "internalConsoleOptions": "neverOpen" 34 | }, 35 | ] 36 | } 37 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | // See https://go.microsoft.com/fwlink/?LinkId=733558 3 | // for the documentation about the tasks.json format 4 | "version": "2.0.0", 5 | "tasks": [ 6 | { 7 | "label": "electron-build", 8 | "type": "npm", 9 | "script": "electron:build" 10 | }, 11 | { 12 | "label": "electron-build-prod", 13 | "type": "npm", 14 | "script": "electron:build:prod" 15 | } 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018-2020 BashPrime, Syncathetic, and Pwootage 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /__tests__/models/entrance.test.ts: -------------------------------------------------------------------------------- 1 | import { setUpWorld } from '../utils'; 2 | import { PrimeRandomizerSettings } from '../../src/electron/models/prime/randomizerSettings'; 3 | import { ENTRANCE_SEPARATOR } from '../../src/common/constants'; 4 | 5 | describe('Entrance', () => { 6 | it('getOpposite() returns a valid entrance', () => { 7 | const world = setUpWorld(new PrimeRandomizerSettings()); 8 | const expected = world.getRegionByKey('Tallon Canyon').getExit('Tallon Canyon' + ENTRANCE_SEPARATOR + 'Landing Site'); 9 | const actual = world.getRegionByKey('Landing Site').getExit('Landing Site' + ENTRANCE_SEPARATOR + 'Tallon Canyon').getOpposite(); 10 | 11 | expect(actual).toEqual(expected); 12 | }); 13 | }); -------------------------------------------------------------------------------- /__tests__/models/item.test.ts: -------------------------------------------------------------------------------- 1 | import { Item } from '../../src/electron/models/item'; 2 | 3 | describe('Item', () => { 4 | it('constructs without error', () => { 5 | expect(() => { 6 | new Item('Test Item', 'test type', 0); 7 | }).not.toThrow(); 8 | }); 9 | 10 | it('should contain a name', () => { 11 | const itemName = 'e03dfd6b-5e97-40c6-b29a-4a7d400f75f0'; 12 | const item = new Item(itemName, 'TestType', 0); 13 | 14 | expect(item.getName()).toBe(itemName); 15 | }); 16 | 17 | it('should contain a patcher ID', () => { 18 | const patcherId = Number.MAX_SAFE_INTEGER; 19 | const item = new Item('test', 'TestType', patcherId); 20 | 21 | expect(item.getPatcherId()).toBe(patcherId); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /__tests__/models/itemCollection.test.ts: -------------------------------------------------------------------------------- 1 | import { ItemCollection } from '../../src/electron/models/itemCollection'; 2 | import { primeItems } from '../../src/electron/models/prime/items'; 3 | import { PrimeItem } from '../../src/electron/enums/primeItem'; 4 | 5 | describe('ItemCollection', () => { 6 | it('should handle if an item is in the collection', () => { 7 | const items = [primeItems[PrimeItem.MORPH_BALL]]; 8 | const collection = new ItemCollection(items); 9 | const hasResult = collection.has(primeItems[PrimeItem.MORPH_BALL].getName()); 10 | 11 | expect(hasResult).toBe(true); 12 | }); 13 | 14 | it('should handle if an item is NOT in the collection', () => { 15 | const items = [primeItems[PrimeItem.MORPH_BALL]]; 16 | const collection = new ItemCollection(items); 17 | const hasResult = collection.has(primeItems[PrimeItem.SUPER_MISSILE].getName()); 18 | 19 | expect(hasResult).toBe(false); 20 | }); 21 | 22 | it('should contain at least a certain amount of specific items', () => { 23 | const amount = 8; 24 | const items = Array(amount).fill(primeItems[PrimeItem.MISSILE_EXPANSION]); 25 | const hasCount = new ItemCollection(items).hasCount(primeItems[PrimeItem.MISSILE_EXPANSION].getName(), amount); 26 | 27 | expect(hasCount).toBe(true); 28 | }); 29 | 30 | it('should NOT not contain at least a certain amount of specific items', () => { 31 | const amount = 7; 32 | const count = 8; 33 | const items = Array(amount).fill(primeItems[PrimeItem.MISSILE_EXPANSION]); 34 | const hasCount = new ItemCollection(items).hasCount(primeItems[PrimeItem.MISSILE_EXPANSION].getName(), count); 35 | 36 | expect(hasCount).toBe(false); 37 | }); 38 | }); -------------------------------------------------------------------------------- /__tests__/models/itemOverrides.test.ts: -------------------------------------------------------------------------------- 1 | import { ItemOverrides } from '../../src/electron/models/prime/itemOverrides'; 2 | import { PrimeItem } from '../../src/electron/enums/primeItem'; 3 | 4 | describe('ItemOverrides', () => { 5 | it('constructs without error', () => { 6 | expect(() => { 7 | new ItemOverrides(); 8 | }).not.toThrow(); 9 | }); 10 | 11 | it('should handle scan visor vanilla state', () => { 12 | const overrides = new ItemOverrides([ 13 | { 14 | name: PrimeItem.SCAN_VISOR, 15 | state: ItemOverrides.STATES.vanilla, 16 | count: 0 17 | } 18 | ]); 19 | 20 | const scanVisorOverride = overrides[PrimeItem.SCAN_VISOR]; 21 | expect(scanVisorOverride).toBeDefined; 22 | expect(scanVisorOverride.state).not.toBe(ItemOverrides.STATES.vanilla); 23 | }); 24 | }); -------------------------------------------------------------------------------- /__tests__/models/items.test.ts: -------------------------------------------------------------------------------- 1 | import { primeItems } from '../../src/electron/models/prime/items'; 2 | 3 | describe('Prime Items', () => { 4 | it('should contain 38 entries', () => { 5 | const numEntries = 38; 6 | expect(Object.keys(primeItems).length).toBe(numEntries); 7 | }); 8 | }); -------------------------------------------------------------------------------- /__tests__/models/randomizer.test.ts: -------------------------------------------------------------------------------- 1 | import { generateWorld } from '../../src/electron/models/prime/randomizer'; 2 | import { PrimeRandomizerSettings } from '../../src/electron/models/prime/randomizerSettings'; 3 | 4 | describe('PrimeRandomizer', () => { 5 | it('should successfully generate a world', () => { 6 | const settings = new PrimeRandomizerSettings({ seed: 'N99OETYKV4' }); 7 | const world = generateWorld(settings); 8 | const placedItemLocations = world.getLocations().toArray().filter(location => location.hasItem()); 9 | 10 | expect(placedItemLocations.length).toBe(100); 11 | expect(world.getRandomprimePatcherLayoutString()).toBeDefined; 12 | expect(world.getLayoutHash()).toBeDefined; 13 | }); 14 | }); -------------------------------------------------------------------------------- /__tests__/models/randomizerSettings.test.ts: -------------------------------------------------------------------------------- 1 | import { PrimeRandomizerSettings } from '../../src/electron/models/prime/randomizerSettings'; 2 | import { discreteNumberSelection } from '../../src/electron/models/option'; 3 | 4 | describe('PrimeRandomizerSettings', () => { 5 | it('should return a settings instance', () => { 6 | const settings = new PrimeRandomizerSettings(); 7 | // VS Code is complaining, likely due to unsupported TypeScript, but works 8 | expect(settings).toBeInstanceOf(PrimeRandomizerSettings); 9 | }); 10 | 11 | it('should export settings to settings string', () => { 12 | const settings = new PrimeRandomizerSettings({ 13 | spoiler: true, 14 | skipHudPopups: false, 15 | hideItemModels: true, 16 | itemOverrides: [ 17 | { 18 | name: 'Morph Ball', 19 | state: 'starting-item', 20 | count: 0 21 | } 22 | ], 23 | excludeLocations: { 24 | ['Alcove']: true 25 | }, 26 | tricks: { 27 | alcoveEscapeWithoutSpaceJump: true, 28 | landingSiteScanDash: true 29 | } 30 | }); 31 | const expected = 'GY574LC-TXQWBYM6C68KHWGNTO8V9X4NX8G-UQ3UEU2I5SNC9D47UV4-YKDBPOTTIAI540X6O0'; 32 | const result = settings.toSettingsString(); 33 | 34 | expect(result).toBe(expected); 35 | }); 36 | 37 | it('should import settings from setting string successfully', () => { 38 | const expected = new PrimeRandomizerSettings({ spoiler: false, goal: 'all-bosses', excludeLocations: { ['Landing Site']: true } }); 39 | const settingsString = expected.toSettingsString(); 40 | const result = PrimeRandomizerSettings.fromSettingsString(settingsString); 41 | 42 | expect(result).toEqual(expected); 43 | expect(result.toSettingsString()).toBe(expected.toSettingsString()); 44 | }); 45 | 46 | it('discreteNumberSelection should return full range of given numbers', () => { 47 | const result = discreteNumberSelection(1, 5); 48 | const expected = [ 49 | { name: '1', value: 1 }, 50 | { name: '2', value: 2 }, 51 | { name: '3', value: 3 }, 52 | { name: '4', value: 4 }, 53 | { name: '5', value: 5 } 54 | ]; 55 | 56 | expect(result).toEqual(expected); 57 | }); 58 | }); 59 | -------------------------------------------------------------------------------- /__tests__/models/tricks.test.ts: -------------------------------------------------------------------------------- 1 | import { setUpWorld } from '../utils'; 2 | import { primeItems } from '../../src/electron/models/prime/items'; 3 | import { PrimeRandomizerSettings } from '../../src/electron/models/prime/randomizerSettings'; 4 | import { PrimeItemCollection } from '../../src/electron/models/prime/itemCollection'; 5 | import { PrimeLocation } from '../../src/electron/enums/primeLocation'; 6 | import { ENTRANCE_SEPARATOR } from '../../src/common/constants'; 7 | import { PrimeItem } from '../../src/electron/enums/primeItem'; 8 | 9 | describe('Tricks', () => { 10 | it('should handle Landing Site dash (no scan)', () => { 11 | const world = setUpWorld(new PrimeRandomizerSettings({ 12 | tricks: { 13 | landingSiteDashWithoutScanVisor: true 14 | } 15 | })); 16 | const items = new PrimeItemCollection([]); 17 | const exit = world.getRegionByKey('Landing Site').getExit('Landing Site' + ENTRANCE_SEPARATOR + 'Alcove'); 18 | 19 | expect(exit.accessRule(items, world.getSettings())).toBe(true); 20 | }); 21 | 22 | it('should handle Landing Site scan dash', () => { 23 | const world = setUpWorld(new PrimeRandomizerSettings({ 24 | tricks: { 25 | landingSiteScanDash: true 26 | } 27 | })); 28 | const items = new PrimeItemCollection([ 29 | primeItems['Scan Visor'] 30 | ]); 31 | const exit = world.getRegionByKey('Landing Site').getExit('Landing Site' + ENTRANCE_SEPARATOR + 'Alcove'); 32 | 33 | expect(exit.accessRule(items, world.getSettings())).toBe(true); 34 | }); 35 | 36 | it('should NOT allow Landing Site scan dash without scan', () => { 37 | const world = setUpWorld(new PrimeRandomizerSettings({ 38 | tricks: { 39 | landingSiteScanDash: true 40 | } 41 | })); 42 | const items = new PrimeItemCollection([]); 43 | const exit = world.getRegionByKey('Landing Site').getExit('Landing Site' + ENTRANCE_SEPARATOR + 'Alcove'); 44 | 45 | expect(exit.accessRule(items, world.getSettings())).toBe(false); 46 | }); 47 | 48 | it('should handle Arbor Chamber without Plasma Beam', () => { 49 | const world = setUpWorld(new PrimeRandomizerSettings({ 50 | tricks: { 51 | arborChamberWithoutPlasma: true 52 | } 53 | })); 54 | const items = new PrimeItemCollection([ 55 | primeItems[PrimeItem.MISSILE_LAUNCHER], 56 | primeItems[PrimeItem.MORPH_BALL], 57 | primeItems[PrimeItem.MORPH_BALL_BOMB], 58 | primeItems[PrimeItem.SPACE_JUMP_BOOTS], 59 | primeItems[PrimeItem.XRAY_VISOR], 60 | primeItems[PrimeItem.GRAPPLE_BEAM] 61 | ]); 62 | const location = world.getLocationByKey(PrimeLocation.ARBOR_CHAMBER); 63 | 64 | expect(location.itemRule(items, world.getSettings())).toBe(true); 65 | }); 66 | 67 | it('should handle Climb Frigate Crash Site', () => { 68 | const world = setUpWorld(new PrimeRandomizerSettings({ 69 | tricks: { 70 | climbFrigateCrashSite: true 71 | } 72 | })); 73 | const items = new PrimeItemCollection([ 74 | primeItems[PrimeItem.MORPH_BALL], 75 | primeItems[PrimeItem.SPACE_JUMP_BOOTS], 76 | primeItems[PrimeItem.ICE_BEAM] 77 | ]); 78 | const exit = world.getRegionByKey('Frigate Crash Site').getExit('Frigate Crash Site' + ENTRANCE_SEPARATOR + 'Overgrown Cavern'); 79 | 80 | expect(exit.accessRule(items, world.getSettings())).toBe(true); 81 | }); 82 | }); 83 | -------------------------------------------------------------------------------- /__tests__/models/world.test.ts: -------------------------------------------------------------------------------- 1 | import { PrimeWorld } from '../../src/electron/models/prime/world'; 2 | import { PrimeRandomizerSettings } from '../../src/electron/models/prime/randomizerSettings'; 3 | 4 | describe('PrimeWorld', () => { 5 | it('should return a PrimeWorld instance', () => { 6 | const settings = new PrimeRandomizerSettings(); 7 | const world = new PrimeWorld(settings); 8 | // VS Code is complaining, likely due to unsupported TypeScript, but works 9 | expect(world).toBeInstanceOf(PrimeWorld); 10 | }); 11 | }); -------------------------------------------------------------------------------- /__tests__/utils.ts: -------------------------------------------------------------------------------- 1 | import { PrimeRandomizerSettings } from '../src/electron/models/prime/randomizerSettings'; 2 | import { PrimeWorld } from '../src/electron/models/prime/world'; 3 | import { setEntrances } from '../src/electron/models/prime/entranceShuffle'; 4 | 5 | export function setUpWorld(settings: PrimeRandomizerSettings) { 6 | const world = new PrimeWorld(settings); 7 | world.loadRegions(); 8 | world.setRootRegion(world.getRegionByKey('Root')); 9 | setEntrances(world); 10 | return world; 11 | } -------------------------------------------------------------------------------- /_config.yml: -------------------------------------------------------------------------------- 1 | theme: jekyll-theme-architect -------------------------------------------------------------------------------- /appveyor.yml: -------------------------------------------------------------------------------- 1 | version: "{build}" 2 | 3 | image: Visual Studio 2017 4 | 5 | # Test against the latest version of this Node.js version 6 | environment: 7 | nodejs_version: "8" 8 | 9 | # Install scripts. (runs after repo cloning) 10 | install: 11 | # Get the latest stable version of Node.js or io.js 12 | # IMPORTANT: we MUST use 64-bit node or node-gyp will fail 13 | - ps: Install-Product node $env:nodejs_version x64 14 | # Use 64-bit Visual C++ toolset 15 | - call "C:\Program Files (x86)\Microsoft Visual Studio\2017\Community\VC\Auxiliary\Build\vcvars64.bat" 16 | # install modules 17 | - npm install -g node-gyp 18 | - npm config set python C:\Python27 19 | - npm config set msvs_version 2017 20 | - npm install 21 | 22 | # Post-install test scripts. 23 | test_script: 24 | # Output useful info for debugging. 25 | - node --version 26 | - npm --version 27 | # build application 28 | - npm run electron:windows 29 | 30 | # Don't actually build. 31 | build: off -------------------------------------------------------------------------------- /binding.gyp: -------------------------------------------------------------------------------- 1 | { 2 | "targets": [ 3 | { 4 | "target_name": "randomprime", 5 | "sources": [ "./native/randomprime.cpp" ], 6 | "conditions": [ 7 | ["OS=='win'", { 8 | "msvs_settings": { 9 | "VCCLCompilerTool": { 10 | "AdditionalOptions": [ 11 | "/MD" 12 | ] 13 | } 14 | }, 15 | "libraries": [ 16 | "-l<(module_root_dir)/randomprime/target/release/randomprime", 17 | "-lcredui.lib", 18 | "-lmsimg32.lib", 19 | "-lopengl32.lib", 20 | "-lsecur32.lib", 21 | "-lsetupapi.lib", 22 | "-lws2_32.lib", 23 | "-luserenv.lib", 24 | "-lmsvcrt.lib" 25 | ], 26 | "defines": [ 27 | "_HAS_EXCEPTIONS=1" 28 | ] 29 | }], 30 | ["OS=='mac'", { 31 | "libraries": [ 32 | "<(module_root_dir)/randomprime/target/release/librandomprime.a" 33 | ] 34 | }], 35 | ["OS=='linux'", { 36 | "libraries": [ 37 | "<(module_root_dir)/randomprime/target/release/librandomprime.a" 38 | ] 39 | }] 40 | ], 41 | 'include_dirs': [" 8 | void callWrapper(void *ptr, const char*data) { 9 | (*static_cast(ptr))(data); 10 | } 11 | 12 | void patchRandomizedGame(const Napi::CallbackInfo& info) 13 | { 14 | Napi::String json = info[0].As(); 15 | Napi::Function cb = info[1].As(); 16 | 17 | std::string jsonText = json.Utf8Value(); 18 | auto callback = std::make_shared(cb); 19 | 20 | std::thread([jsonText, callback] () { 21 | auto progressCb = [&callback] (const char *data) { 22 | // This call is async, so we need to make a copy of the message 23 | // that the lambda can then capture. 24 | std::string s(data); 25 | auto argConverter = [s] (napi_env env, std::vector &args) { 26 | args.push_back(Napi::String::New(env, s)); 27 | }; 28 | (*callback)(argConverter, nullptr); 29 | }; 30 | 31 | randomprime_patch_iso(jsonText.c_str(), &progressCb, 32 | callWrapper); 33 | }).detach(); 34 | } 35 | 36 | Napi::Object init(Napi::Env env, Napi::Object exports) 37 | { 38 | exports.Set(Napi::String::New(env, "patchRandomizedGame"), 39 | Napi::Function::New(env, patchRandomizedGame)); 40 | return exports; 41 | } 42 | 43 | NODE_API_MODULE(randomprime, init) 44 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = {}; -------------------------------------------------------------------------------- /postinstall-web.js: -------------------------------------------------------------------------------- 1 | // Allow angular using electron module (native node modules) 2 | const fs = require('fs'); 3 | const f_angular = 'node_modules/@angular-devkit/build-angular/src/angular-cli-files/models/webpack-configs/browser.js'; 4 | 5 | fs.readFile(f_angular, 'utf8', function (err, data) { 6 | if (err) { 7 | return console.log(err); 8 | } 9 | var result = data.replace(/target: "electron-renderer",/g, ''); 10 | var result = result.replace(/target: "web",/g, ''); 11 | var result = result.replace(/return \{/g, 'return {target: "web",'); 12 | 13 | fs.writeFile(f_angular, result, 'utf8', function (err) { 14 | if (err) return console.log(err); 15 | }); 16 | }); -------------------------------------------------------------------------------- /postinstall.js: -------------------------------------------------------------------------------- 1 | // Allow angular using electron module (native node modules) 2 | const fs = require('fs'); 3 | const f_angular = 'node_modules/@angular-devkit/build-angular/src/angular-cli-files/models/webpack-configs/browser.js'; 4 | 5 | fs.readFile(f_angular, 'utf8', function (err, data) { 6 | if (err) { 7 | return console.log(err); 8 | } 9 | var result = data.replace(/target: "electron-renderer",/g, ''); 10 | var result = result.replace(/target: "web",/g, ''); 11 | var result = result.replace(/return \{/g, 'return {target: "electron-renderer",'); 12 | 13 | fs.writeFile(f_angular, result, 'utf8', function (err) { 14 | if (err) return console.log(err); 15 | }); 16 | }); -------------------------------------------------------------------------------- /src/client/.editorconfig: -------------------------------------------------------------------------------- 1 | # Editor configuration, see https://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | indent_style = space 7 | indent_size = 2 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.md] 12 | max_line_length = off 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /src/client/.gitignore: -------------------------------------------------------------------------------- 1 | # See http://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # compiled output 4 | /dist 5 | /tmp 6 | /out-tsc 7 | # Only exists if Bazel was run 8 | /bazel-out 9 | 10 | # dependencies 11 | /node_modules 12 | 13 | # profiling files 14 | chrome-profiler-events*.json 15 | speed-measure-plugin*.json 16 | 17 | # IDEs and editors 18 | /.idea 19 | .project 20 | .classpath 21 | .c9/ 22 | *.launch 23 | .settings/ 24 | *.sublime-workspace 25 | 26 | # IDE - VSCode 27 | .vscode/* 28 | !.vscode/settings.json 29 | !.vscode/tasks.json 30 | !.vscode/launch.json 31 | !.vscode/extensions.json 32 | .history/* 33 | 34 | # misc 35 | /.sass-cache 36 | /connect.lock 37 | /coverage 38 | /libpeerconnection.log 39 | npm-debug.log 40 | yarn-error.log 41 | testem.log 42 | /typings 43 | 44 | # System Files 45 | .DS_Store 46 | Thumbs.db 47 | -------------------------------------------------------------------------------- /src/client/README.md: -------------------------------------------------------------------------------- 1 | # MetroidPrimeRandomizerClient 2 | 3 | This project was generated with [Angular CLI](https://github.com/angular/angular-cli) version 8.3.4. 4 | 5 | ## Development server 6 | 7 | Run `ng serve` for a dev server. Navigate to `http://localhost:4200/`. The app will automatically reload if you change any of the source files. 8 | 9 | ## Code scaffolding 10 | 11 | Run `ng generate component component-name` to generate a new component. You can also use `ng generate directive|pipe|service|class|guard|interface|enum|module`. 12 | 13 | ## Build 14 | 15 | Run `ng build` to build the project. The build artifacts will be stored in the `dist/` directory. Use the `--prod` flag for a production build. 16 | 17 | ## Running unit tests 18 | 19 | Run `ng test` to execute the unit tests via [Karma](https://karma-runner.github.io). 20 | 21 | ## Running end-to-end tests 22 | 23 | Run `ng e2e` to execute the end-to-end tests via [Protractor](http://www.protractortest.org/). 24 | 25 | ## Further help 26 | 27 | To get more help on the Angular CLI use `ng help` or go check out the [Angular CLI README](https://github.com/angular/angular-cli/blob/master/README.md). 28 | -------------------------------------------------------------------------------- /src/client/browserslist: -------------------------------------------------------------------------------- 1 | # This file is used by the build system to adjust CSS and JS output to support the specified browsers below. 2 | # For additional information regarding the format and rule options, please see: 3 | # https://github.com/browserslist/browserslist#queries 4 | 5 | # You can see what browsers were selected by your queries by running: 6 | # npx browserslist 7 | 8 | > 0.5% 9 | last 2 versions 10 | Firefox ESR 11 | not dead 12 | not IE 9-11 # For IE 9-11 support, remove 'not'. -------------------------------------------------------------------------------- /src/client/e2e/src/app.e2e-spec.ts: -------------------------------------------------------------------------------- 1 | import { AppPage } from './app.po'; 2 | import { browser, logging } from 'protractor'; 3 | 4 | describe('workspace-project App', () => { 5 | let page: AppPage; 6 | 7 | beforeEach(() => { 8 | page = new AppPage(); 9 | }); 10 | 11 | it('should display welcome message', () => { 12 | page.navigateTo(); 13 | expect(page.getTitleText()).toEqual('metroid-prime-randomizer-client app is running!'); 14 | }); 15 | 16 | afterEach(async () => { 17 | // Assert that there are no errors emitted from the browser 18 | const logs = await browser.manage().logs().get(logging.Type.BROWSER); 19 | expect(logs).not.toContain(jasmine.objectContaining({ 20 | level: logging.Level.SEVERE, 21 | } as logging.Entry)); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /src/client/e2e/src/app.po.ts: -------------------------------------------------------------------------------- 1 | import { browser, by, element } from 'protractor'; 2 | 3 | export class AppPage { 4 | navigateTo() { 5 | return browser.get(browser.baseUrl) as Promise; 6 | } 7 | 8 | getTitleText() { 9 | return element(by.css('app-root .content span')).getText() as Promise; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/client/e2e/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../out-tsc/e2e", 5 | "module": "commonjs", 6 | "target": "es5", 7 | "types": [ 8 | "jasmine", 9 | "jasminewd2", 10 | "node" 11 | ] 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/client/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "metroid-prime-randomizer-client", 3 | "scripts": { 4 | "ng": "ng", 5 | "ng:serve": "ng serve", 6 | "ng:build": "ng build", 7 | "ng:build:prod": "ng build --prod" 8 | }, 9 | "private": true 10 | } 11 | -------------------------------------------------------------------------------- /src/client/src/app/app-routing.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { Routes, RouterModule } from '@angular/router'; 3 | import { RandomizerComponent } from './randomizer/randomizer.component'; 4 | import { SeedHistoryComponent } from './seed-history/seed-history.component'; 5 | 6 | const routes: Routes = [ 7 | { 8 | path: '', 9 | component: RandomizerComponent 10 | }, 11 | { 12 | path: 'history', 13 | component: SeedHistoryComponent 14 | } 15 | ]; 16 | 17 | @NgModule({ 18 | imports: [RouterModule.forRoot(routes)], 19 | exports: [RouterModule] 20 | }) 21 | export class AppRoutingModule { } 22 | -------------------------------------------------------------------------------- /src/client/src/app/app.component.html: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/client/src/app/app.component.scss: -------------------------------------------------------------------------------- 1 | .columns { 2 | height: 100vh; 3 | margin: 0; 4 | } 5 | 6 | .column { 7 | padding: 0; 8 | } 9 | 10 | .column.is-menu-column { 11 | background-color: #000; 12 | } -------------------------------------------------------------------------------- /src/client/src/app/app.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed, async } from '@angular/core/testing'; 2 | import { RouterTestingModule } from '@angular/router/testing'; 3 | import { AppComponent } from './app.component'; 4 | 5 | describe('AppComponent', () => { 6 | beforeEach(async(() => { 7 | TestBed.configureTestingModule({ 8 | imports: [ 9 | RouterTestingModule 10 | ], 11 | declarations: [ 12 | AppComponent 13 | ], 14 | }).compileComponents(); 15 | })); 16 | 17 | it('should create the app', () => { 18 | const fixture = TestBed.createComponent(AppComponent); 19 | const app = fixture.debugElement.componentInstance; 20 | expect(app).toBeTruthy(); 21 | }); 22 | 23 | it(`should have as title 'metroid-prime-randomizer-client'`, () => { 24 | const fixture = TestBed.createComponent(AppComponent); 25 | const app = fixture.debugElement.componentInstance; 26 | expect(app.title).toEqual('metroid-prime-randomizer-client'); 27 | }); 28 | 29 | it('should render title', () => { 30 | const fixture = TestBed.createComponent(AppComponent); 31 | fixture.detectChanges(); 32 | const compiled = fixture.debugElement.nativeElement; 33 | expect(compiled.querySelector('.content span').textContent).toContain('metroid-prime-randomizer-client app is running!'); 34 | }); 35 | }); 36 | -------------------------------------------------------------------------------- /src/client/src/app/app.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-root', 5 | templateUrl: './app.component.html', 6 | styleUrls: ['./app.component.scss'] 7 | }) 8 | export class AppComponent { 9 | title = 'metroid-prime-randomizer-client'; 10 | } 11 | -------------------------------------------------------------------------------- /src/client/src/app/components/common/modal.component.ts: -------------------------------------------------------------------------------- 1 | export class ModalComponent { 2 | private open: boolean = false; 3 | 4 | constructor() { } 5 | 6 | getOpen(): boolean { 7 | return this.open; 8 | } 9 | 10 | setOpen(open: boolean) { 11 | this.open = open; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/client/src/app/components/common/picklist-form.component.ts: -------------------------------------------------------------------------------- 1 | import { FormArray, FormBuilder } from '@angular/forms'; 2 | import { SelectItem } from 'primeng/api'; 3 | 4 | import { SettingsSection } from 'src/app/settings/settings-section'; 5 | import { RandomizerService } from 'src/app/services/randomizer.service'; 6 | 7 | export abstract class PicklistFormComponent extends SettingsSection { 8 | items: PickList = { 9 | available: [], 10 | selected: [] 11 | }; 12 | protected abstract formArray: FormArray; 13 | private fb = new FormBuilder(); 14 | 15 | // Constants 16 | readonly GLOBAL_STYLE = { height: '100%' }; 17 | 18 | constructor(protected randomizerService: RandomizerService) { 19 | super(randomizerService); 20 | } 21 | 22 | ngOnInit() { 23 | this.initialize(); 24 | } 25 | 26 | addItems(event: any): void { 27 | const items = (event.items as SelectItem[]).map(item => item.value); 28 | 29 | for (let item of items) { 30 | this.formArray.push(this.fb.control(item)); 31 | } 32 | } 33 | 34 | removeItems(event: any): void { 35 | const items = (event.items as SelectItem[]).map(item => item.value); 36 | 37 | for (let item of items) { 38 | const formValue = this.formArray.value; 39 | this.formArray.removeAt(formValue.indexOf(item)); 40 | } 41 | } 42 | 43 | protected abstract initialize(): void; 44 | } 45 | 46 | export interface PickList { 47 | available: SelectItem[]; 48 | selected: SelectItem[]; 49 | } 50 | -------------------------------------------------------------------------------- /src/client/src/app/directives/disable-control.directive.spec.ts: -------------------------------------------------------------------------------- 1 | import { DisableControlDirective } from './disable-control.directive'; 2 | 3 | describe('DisableControlDirective', () => { 4 | it('should create an instance', () => { 5 | const directive = new DisableControlDirective(); 6 | expect(directive).toBeTruthy(); 7 | }); 8 | }); 9 | -------------------------------------------------------------------------------- /src/client/src/app/directives/disable-control.directive.ts: -------------------------------------------------------------------------------- 1 | import { Directive, Input } from '@angular/core'; 2 | import { NgControl } from '@angular/forms'; 3 | 4 | @Directive({ 5 | selector: '[disableControl]' 6 | }) 7 | export class DisableControlDirective { 8 | @Input() set disableControl(condition: boolean) { 9 | const action = condition ? 'disable' : 'enable'; 10 | this.ngControl.control[action](); 11 | } 12 | 13 | constructor(private ngControl: NgControl) { } 14 | } 15 | -------------------------------------------------------------------------------- /src/client/src/app/game-details/game-details.component.scss: -------------------------------------------------------------------------------- 1 | @import '~bulma/sass/utilities/mixins'; 2 | 3 | .seed-details { 4 | padding: 1rem; 5 | border-bottom: 1px solid #282f2f; 6 | } 7 | 8 | .seed-details:first-child { 9 | padding-top: 0; 10 | } 11 | 12 | .seed-details:last-child { 13 | border-bottom: none; 14 | padding-bottom: 0; 15 | } 16 | 17 | .box.settings-box { 18 | background-color: #101010; 19 | border: 2px solid #404040; 20 | } 21 | 22 | // Don't let permalink overflow container on size >= tablet 23 | .is-permalink { 24 | word-break: break-word; 25 | 26 | @include from ($tablet) { 27 | max-width: 80%; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/client/src/app/game-details/game-details.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { GameDetailsComponent } from './game-details.component'; 4 | 5 | describe('GameDetailsComponent', () => { 6 | let component: GameDetailsComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [ GameDetailsComponent ] 12 | }) 13 | .compileComponents(); 14 | })); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(GameDetailsComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/client/src/app/generate-game/generate-game.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 | 5 |
6 |
7 |
8 | 9 |
10 |
11 |
12 | 15 |
16 |
17 |
18 |
19 |
20 | 21 | 23 | 25 | 26 | 27 |
28 |
29 |
30 |
31 |
32 | 33 |
34 | 35 |
36 |

Minimum count is 1.

37 |

Maximum count is 99.

38 |

You must enter a number 39 | between 1 and 99.

40 |
41 |
42 |
43 |
44 |
45 | 46 | 47 | 48 | 49 |
50 |
51 | 52 |
53 |
54 |
55 |
56 |
57 | 63 | 64 | 65 | -------------------------------------------------------------------------------- /src/client/src/app/generate-game/generate-game.component.scss: -------------------------------------------------------------------------------- 1 | button.is-generate-button { 2 | min-width: 12em; 3 | 4 | &.is-spoiler { 5 | background-color: #900000; 6 | border-color: transparent; 7 | } 8 | } 9 | 10 | .field.is-generate-field { 11 | margin: 2em; 12 | } 13 | 14 | hr { 15 | margin: 0; 16 | } 17 | 18 | .section.randomizer-content { 19 | height: calc(100% - 3em); 20 | } 21 | 22 | form { 23 | height: 100%; 24 | display: flex; 25 | flex-direction: column; 26 | } 27 | 28 | app-customize-settings-container { 29 | height: 100%; 30 | display: flex; 31 | flex-direction: column; 32 | } 33 | -------------------------------------------------------------------------------- /src/client/src/app/generate-game/generate-game.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { GenerateGameComponent } from './generate-game.component'; 4 | 5 | describe('GenerateGameComponent', () => { 6 | let component: GenerateGameComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [ GenerateGameComponent ] 12 | }) 13 | .compileComponents(); 14 | })); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(GenerateGameComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/client/src/app/help/help.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
5 | 12 |
13 |
14 |
15 | 16 |
17 |
18 |
19 |
20 |
21 | -------------------------------------------------------------------------------- /src/client/src/app/help/help.component.scss: -------------------------------------------------------------------------------- 1 | :host { 2 | ::ng-deep img { 3 | border: 1px solid #404040; 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /src/client/src/app/help/help.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { HelpComponent } from './help.component'; 4 | 5 | describe('HelpComponent', () => { 6 | let component: HelpComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [ HelpComponent ] 12 | }) 13 | .compileComponents(); 14 | })); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(HelpComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/client/src/app/help/help.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit, HostListener } from '@angular/core'; 2 | import { MarkdownService } from 'ngx-markdown'; 3 | 4 | import { ElectronService } from '../services/electron.service'; 5 | 6 | interface MenuItem { 7 | name: string; 8 | file: string; 9 | } 10 | 11 | @Component({ 12 | selector: 'app-help', 13 | templateUrl: './help.component.html', 14 | styleUrls: ['./help.component.scss'] 15 | }) 16 | export class HelpComponent implements OnInit { 17 | readonly menuItems = [ 18 | { name: 'Getting Started', file: 'assets/help-documents/gettingStarted.md' }, 19 | { name: 'FAQ', file: 'assets/help-documents/faq.md' }, 20 | { name: 'Softlocks', file: 'assets/help-documents/softlocks.md' }, 21 | { name: 'Differences', file: 'assets/help-documents/differences.md' }, 22 | { name: 'Trackers', file: 'assets/help-documents/trackers.md' } 23 | ]; 24 | private selectedIndex: number; 25 | 26 | constructor(private markdownService: MarkdownService, private electronService: ElectronService) { } 27 | 28 | ngOnInit() { 29 | this.setSelectedIndex(0); 30 | 31 | this.markdownService.renderer.link = (href: string, title: string, text: string) => { 32 | return `${text}`; 33 | }; 34 | } 35 | 36 | getSelectedItem(): MenuItem { 37 | return this.menuItems[this.selectedIndex]; 38 | } 39 | 40 | getSelectedIndex(): number { 41 | return this.selectedIndex; 42 | } 43 | 44 | setSelectedIndex(index: number): void { 45 | this.selectedIndex = index; 46 | } 47 | 48 | @HostListener('document:click', ['$event']) 49 | interceptAnchorHref(event) { 50 | const element = event.target; 51 | 52 | if (element.tagName === 'A' && element.href) { 53 | event.preventDefault(); 54 | this.electronService.shell.openExternal(element.href); 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/client/src/app/import-settings-modal/import-settings-modal.component.html: -------------------------------------------------------------------------------- 1 | 27 | -------------------------------------------------------------------------------- /src/client/src/app/import-settings-modal/import-settings-modal.component.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BashPrime/metroid-prime-randomizer/a43b732aaa5530d2b8b8c3e57f8146f9f35d07ca/src/client/src/app/import-settings-modal/import-settings-modal.component.scss -------------------------------------------------------------------------------- /src/client/src/app/import-settings-modal/import-settings-modal.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { ImportSettingsModalComponent } from './import-settings-modal.component'; 4 | 5 | describe('ImportSettingsModalComponent', () => { 6 | let component: ImportSettingsModalComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [ ImportSettingsModalComponent ] 12 | }) 13 | .compileComponents(); 14 | })); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(ImportSettingsModalComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/client/src/app/import-settings-modal/import-settings-modal.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import { FormGroup, FormBuilder, Validators } from '@angular/forms'; 3 | import { Subject } from 'rxjs'; 4 | import { takeUntil } from 'rxjs/operators'; 5 | 6 | 7 | import { ModalComponent } from '../components/common/modal.component'; 8 | import { GeneratorService } from '../services/generator.service'; 9 | 10 | interface ImportForm { 11 | permalink: string; 12 | } 13 | 14 | @Component({ 15 | selector: 'app-import-settings-modal', 16 | templateUrl: './import-settings-modal.component.html', 17 | styleUrls: ['./import-settings-modal.component.scss'] 18 | }) 19 | export class ImportSettingsModalComponent extends ModalComponent implements OnInit { 20 | private formGroup: FormGroup; 21 | private submitted: boolean = false; 22 | private ngUnsubscribe: Subject = new Subject(); 23 | 24 | constructor(private generatorService: GeneratorService) { 25 | super(); 26 | } 27 | 28 | ngOnInit() { 29 | this.initForm(); 30 | this.generatorService._lastSettingsUsed 31 | .pipe(takeUntil(this.ngUnsubscribe)) 32 | .subscribe(() => this.setOpen(false)); 33 | } 34 | 35 | openModal(): void { 36 | this.submitted = false; 37 | this.initForm(); 38 | navigator.clipboard.readText().then(clipboardText => { 39 | this.formGroup.patchValue({ permalink: clipboardText }); 40 | }); 41 | this.setOpen(true); 42 | } 43 | 44 | getFormGroup(): FormGroup { 45 | return this.formGroup; 46 | } 47 | 48 | isSubmitted(): boolean { 49 | return this.submitted; 50 | } 51 | 52 | onSubmit(formValue: ImportForm) { 53 | this.submitted = true; 54 | 55 | if (this.formGroup.valid) { 56 | this.generatorService.importPermalink(formValue.permalink); 57 | } 58 | } 59 | 60 | get permalink() { 61 | return this.formGroup.get('permalink'); 62 | } 63 | 64 | private initForm(): void { 65 | const fb = new FormBuilder(); 66 | this.formGroup = fb.group({ 67 | permalink: ['', Validators.required] 68 | }); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/client/src/app/item-overrides/item-overrides.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
Item Pool: {{ getItemPoolSize() }} / 4 | {{ getItemPoolCapacity() }}
5 |
6 |
7 |
8 |
9 | 12 |
13 |
14 |
15 |
16 | 17 | 19 |
20 |
21 |
22 |
23 |
25 |
26 |
27 |

{{ override.value.name }}

28 |
29 |
30 |
31 |

EXPERIMENTAL - You are not guaranteed to finish the game if you change this 32 | item's behavior.

33 |
34 |
35 | 36 |
37 |
38 | 43 |
44 |
45 |
46 |
47 | 48 |
49 | 50 |
51 |
52 |
53 | 56 |
57 |
58 |
59 |
60 | -------------------------------------------------------------------------------- /src/client/src/app/item-overrides/item-overrides.component.scss: -------------------------------------------------------------------------------- 1 | .item-override { 2 | padding: 1rem; 3 | border-bottom: 1px solid #282f2f; 4 | } 5 | 6 | .item-override:last-child { 7 | border-bottom: initial; 8 | } 9 | 10 | // Dataview styling 11 | :host { 12 | ::ng-deep .ui-dataview .ui-dataview-content { 13 | // height: 100%; 14 | background-color: #101010; 15 | color: #efefef; 16 | border-color: #404040; 17 | } 18 | } 19 | 20 | -------------------------------------------------------------------------------- /src/client/src/app/item-overrides/item-overrides.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { ItemOverridesComponent } from './item-overrides.component'; 4 | 5 | describe('ItemOverridesComponent', () => { 6 | let component: ItemOverridesComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [ ItemOverridesComponent ] 12 | }) 13 | .compileComponents(); 14 | })); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(ItemOverridesComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/client/src/app/picklist/picklist.component.html: -------------------------------------------------------------------------------- 1 | 4 | 5 |
6 | {{ item.label }} 7 |
8 |
9 |
-------------------------------------------------------------------------------- /src/client/src/app/picklist/picklist.component.scss: -------------------------------------------------------------------------------- 1 | :host { 2 | ::ng-deep .ui-picklist .ui-picklist-list { 3 | background-color: #101010; 4 | color: #efefef; 5 | border-color: #404040; 6 | 7 | .ui-picklist-item { 8 | color: #efefef; 9 | } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/client/src/app/picklist/picklist.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { PicklistComponent } from './picklist.component'; 4 | 5 | describe('PicklistComponent', () => { 6 | let component: PicklistComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [ PicklistComponent ] 12 | }) 13 | .compileComponents(); 14 | })); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(PicklistComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/client/src/app/picklist/picklist.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit, Input } from '@angular/core'; 2 | import { SelectItem } from 'primeng/api'; 3 | 4 | 5 | /** 6 | * This is a common picklist component meant to be used for all picklists in the randomizer application. 7 | */ 8 | @Component({ 9 | selector: 'app-picklist', 10 | templateUrl: './picklist.component.html', 11 | styleUrls: ['./picklist.component.scss'] 12 | }) 13 | export class PicklistComponent implements OnInit { 14 | @Input() source: PicklistItem[]; 15 | @Input() target: PicklistItem[]; 16 | @Input() style: object; 17 | @Input() sourceStyle: object; 18 | @Input() targetStyle: object; 19 | @Input() sourceHeader: string; 20 | @Input() targetHeader: string; 21 | @Input() disabled: boolean; 22 | 23 | constructor() { } 24 | 25 | ngOnInit() { 26 | } 27 | } 28 | 29 | export interface PicklistItem extends SelectItem { 30 | tooltip?: string; 31 | } 32 | -------------------------------------------------------------------------------- /src/client/src/app/prime-iso-diagnostics-modal/prime-iso-diagnostics-modal.component.html: -------------------------------------------------------------------------------- 1 | 53 | -------------------------------------------------------------------------------- /src/client/src/app/prime-iso-diagnostics-modal/prime-iso-diagnostics-modal.component.scss: -------------------------------------------------------------------------------- 1 | .heading { 2 | color: #8c9b9d; 3 | } 4 | -------------------------------------------------------------------------------- /src/client/src/app/prime-iso-diagnostics-modal/prime-iso-diagnostics-modal.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { PrimeIsoDiagnosticsModalComponent } from './prime-iso-diagnostics-modal.component'; 4 | 5 | describe('PrimeIsoDiagnosticsModalComponent', () => { 6 | let component: PrimeIsoDiagnosticsModalComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [ PrimeIsoDiagnosticsModalComponent ] 12 | }) 13 | .compileComponents(); 14 | })); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(PrimeIsoDiagnosticsModalComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/client/src/app/prime-iso-diagnostics-modal/prime-iso-diagnostics-modal.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import { Subject } from 'rxjs'; 3 | import { takeUntil } from 'rxjs/operators'; 4 | import { faCheck, faTimes } from '@fortawesome/free-solid-svg-icons'; 5 | 6 | import { ModalComponent } from '../components/common/modal.component'; 7 | import { DiagnosticsService, IsoData } from '../services/diagnostics.service'; 8 | import { ElectronService } from '../services/electron.service'; 9 | @Component({ 10 | selector: 'app-prime-iso-diagnostics-modal', 11 | templateUrl: './prime-iso-diagnostics-modal.component.html', 12 | styleUrls: ['./prime-iso-diagnostics-modal.component.scss'] 13 | }) 14 | export class PrimeIsoDiagnosticsModalComponent extends ModalComponent implements OnInit { 15 | gameCode: string; 16 | revision: number; 17 | md5Hash: string; 18 | private loaded: boolean = false; 19 | private ngUnsubscribe: Subject = new Subject(); 20 | 21 | // Constants 22 | readonly ICONS = { 23 | valid: faCheck, 24 | invalid: faTimes 25 | } 26 | readonly VALID_PRIME_MD5_HASHES = { 27 | 0: 'eeacd0ced8e2bae491eca14f141a4b7c', 28 | 2: 'fdfc41b8414dd7d24834c800f567c0f8' 29 | }; 30 | readonly NTSC_METROID_PRIME_GAME_CODE = 'GM8E01'; 31 | 32 | constructor(private diagnosticsService: DiagnosticsService, private electronService: ElectronService) { 33 | super(); 34 | } 35 | 36 | ngOnInit() { 37 | this.diagnosticsService._isoData 38 | .pipe(takeUntil(this.ngUnsubscribe)) 39 | .subscribe(data => { 40 | if (data) { 41 | this.setIsoData(data); 42 | this.loaded = true; 43 | } 44 | }); 45 | 46 | this.diagnosticsService._errorParse 47 | .pipe(takeUntil(this.ngUnsubscribe)) 48 | .subscribe(() => { 49 | this.setOpen(false); 50 | }); 51 | } 52 | 53 | ngOnDestroy() { 54 | this.ngUnsubscribe.next(); 55 | this.ngUnsubscribe.complete(); 56 | } 57 | 58 | setIsoData(data: IsoData) { 59 | this.gameCode = data.gameCode; 60 | this.revision = data.revision; 61 | this.md5Hash = data.md5Hash; 62 | } 63 | 64 | openModal(): void { 65 | this.loaded = false; 66 | this.setOpen(true); 67 | } 68 | 69 | isLoaded(): boolean { 70 | return this.loaded; 71 | } 72 | 73 | hasValidGameCode(): boolean { 74 | return this.gameCode === this.NTSC_METROID_PRIME_GAME_CODE; 75 | } 76 | 77 | gameCodeIsMetroidPrime() { 78 | return this.gameCode.substr(1, 2) === this.NTSC_METROID_PRIME_GAME_CODE.substr(1, 2); 79 | } 80 | 81 | gameCodeIsNTSC() { 82 | return this.gameCode.substr(3) === this.NTSC_METROID_PRIME_GAME_CODE.substr(3); 83 | } 84 | 85 | hasValidMd5Hash(): boolean { 86 | const validHash = this.VALID_PRIME_MD5_HASHES[this.revision]; 87 | return validHash && this.md5Hash === validHash; 88 | } 89 | 90 | saveIsoData(): void { 91 | this.diagnosticsService.saveIsoData({ 92 | gameCode: this.gameCode, 93 | revision: this.revision, 94 | md5Hash: this.md5Hash 95 | }); 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /src/client/src/app/progress-modal/progress-modal.component.html: -------------------------------------------------------------------------------- 1 | 18 | -------------------------------------------------------------------------------- /src/client/src/app/progress-modal/progress-modal.component.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BashPrime/metroid-prime-randomizer/a43b732aaa5530d2b8b8c3e57f8146f9f35d07ca/src/client/src/app/progress-modal/progress-modal.component.scss -------------------------------------------------------------------------------- /src/client/src/app/progress-modal/progress-modal.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { ProgressModalComponent } from './progress-modal.component'; 4 | 5 | describe('ProgressModalComponent', () => { 6 | let component: ProgressModalComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [ ProgressModalComponent ] 12 | }) 13 | .compileComponents(); 14 | })); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(ProgressModalComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/client/src/app/progress-modal/progress-modal.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import { Subject } from 'rxjs'; 3 | import { takeUntil } from 'rxjs/operators'; 4 | 5 | import { ModalComponent } from '../components/common/modal.component'; 6 | import { ProgressBar } from '../../../../common/models/progressBar'; 7 | import { ProgressService } from '../services/progress.service'; 8 | 9 | @Component({ 10 | selector: 'app-progress-modal', 11 | templateUrl: './progress-modal.component.html', 12 | styleUrls: ['./progress-modal.component.scss'] 13 | }) 14 | export class ProgressModalComponent extends ModalComponent implements OnInit { 15 | OBJECT_KEYS = Object.keys; 16 | private progressBars: ProgressBar[]; 17 | private title: string; 18 | private message: string; 19 | private ngUnsubscribe: Subject = new Subject(); 20 | 21 | constructor(private progressService: ProgressService) { 22 | super(); 23 | } 24 | 25 | ngOnInit() { 26 | this.progressService._progressBars 27 | .pipe(takeUntil(this.ngUnsubscribe)) 28 | .subscribe(progressBars => this.setProgressBars(progressBars)); 29 | 30 | this.progressService._open 31 | .pipe(takeUntil(this.ngUnsubscribe)) 32 | .subscribe(open => this.setOpen(open)); 33 | 34 | this.progressService._title 35 | .pipe(takeUntil(this.ngUnsubscribe)) 36 | .subscribe(title => this.setTitle(title)); 37 | 38 | this.progressService._message 39 | .pipe(takeUntil(this.ngUnsubscribe)) 40 | .subscribe(message => this.setMessage(message)); 41 | } 42 | 43 | ngOnDestroy() { 44 | this.ngUnsubscribe.next(); 45 | this.ngUnsubscribe.complete(); 46 | } 47 | 48 | getProgressBars(): ProgressBar[] { 49 | return this.progressBars; 50 | } 51 | 52 | setProgressBars(progressBars: ProgressBar[]): void { 53 | this.progressBars = progressBars; 54 | } 55 | 56 | getTitle(): string { 57 | return this.title; 58 | } 59 | 60 | setTitle(title: string): void { 61 | this.title = title; 62 | } 63 | 64 | getMessage(): string { 65 | return this.message; 66 | } 67 | 68 | setMessage(message: string): void { 69 | this.message = message; 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/client/src/app/randomizer/randomizer.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 | 9 |
10 |
11 | 12 | 13 | 14 | 15 | 45 |
46 | 47 | -------------------------------------------------------------------------------- /src/client/src/app/randomizer/randomizer.component.scss: -------------------------------------------------------------------------------- 1 | .tabs a { 2 | min-width: 10em; 3 | } 4 | 5 | .button.is-link:active { 6 | background-color: #943100; 7 | } 8 | 9 | hr { 10 | margin: 0; 11 | } 12 | 13 | .fullheight-wrapper { 14 | display: flex; 15 | flex-direction: column; 16 | height: 100%; 17 | } 18 | 19 | footer { 20 | padding: 0.5rem 1.5rem; 21 | 22 | .field { 23 | margin-bottom: 0; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/client/src/app/randomizer/randomizer.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { RandomizerComponent } from './randomizer.component'; 4 | 5 | describe('RandomizerComponent', () => { 6 | let component: RandomizerComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [ RandomizerComponent ] 12 | }) 13 | .compileComponents(); 14 | })); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(RandomizerComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/client/src/app/remove-preset-modal/remove-preset-modal.component.html: -------------------------------------------------------------------------------- 1 | 17 | -------------------------------------------------------------------------------- /src/client/src/app/remove-preset-modal/remove-preset-modal.component.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BashPrime/metroid-prime-randomizer/a43b732aaa5530d2b8b8c3e57f8146f9f35d07ca/src/client/src/app/remove-preset-modal/remove-preset-modal.component.scss -------------------------------------------------------------------------------- /src/client/src/app/remove-preset-modal/remove-preset-modal.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { RemovePresetModalComponent } from './remove-preset-modal.component'; 4 | 5 | describe('RemovePresetModalComponent', () => { 6 | let component: RemovePresetModalComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [ RemovePresetModalComponent ] 12 | }) 13 | .compileComponents(); 14 | })); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(RemovePresetModalComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/client/src/app/remove-preset-modal/remove-preset-modal.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit, Output, EventEmitter } from '@angular/core'; 2 | 3 | import { ModalComponent } from '../components/common/modal.component'; 4 | 5 | @Component({ 6 | selector: 'app-remove-preset-modal', 7 | templateUrl: './remove-preset-modal.component.html', 8 | styleUrls: ['./remove-preset-modal.component.scss'] 9 | }) 10 | export class RemovePresetModalComponent extends ModalComponent implements OnInit { 11 | @Output() onRemove: EventEmitter = new EventEmitter(); 12 | private preset: string; 13 | 14 | constructor() { 15 | super(); 16 | } 17 | 18 | ngOnInit() { 19 | } 20 | 21 | getPreset(): string { 22 | return this.preset; 23 | } 24 | 25 | setPresetAndOpen(preset: string): void { 26 | this.preset = preset; 27 | this.setOpen(true); 28 | } 29 | 30 | onRemovePreset(): void { 31 | this.onRemove.emit(this.preset); 32 | this.setOpen(false); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/client/src/app/save-preset-modal/save-preset-modal.component.html: -------------------------------------------------------------------------------- 1 | 40 | -------------------------------------------------------------------------------- /src/client/src/app/save-preset-modal/save-preset-modal.component.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BashPrime/metroid-prime-randomizer/a43b732aaa5530d2b8b8c3e57f8146f9f35d07ca/src/client/src/app/save-preset-modal/save-preset-modal.component.scss -------------------------------------------------------------------------------- /src/client/src/app/save-preset-modal/save-preset-modal.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { SavePresetModalComponent } from './save-preset-modal.component'; 4 | 5 | describe('SavePresetModalComponent', () => { 6 | let component: SavePresetModalComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [ SavePresetModalComponent ] 12 | }) 13 | .compileComponents(); 14 | })); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(SavePresetModalComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/client/src/app/save-preset-modal/save-preset-modal.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit, Output, EventEmitter, Input } from '@angular/core'; 2 | import { FormGroup, FormBuilder, Validators } from '@angular/forms'; 3 | import { take } from 'rxjs/operators'; 4 | 5 | import { ModalComponent } from '../components/common/modal.component'; 6 | import { PresetObject } from '../../../../common/models/presetObject'; 7 | import { PresetsService } from '../services/presets.service'; 8 | 9 | @Component({ 10 | selector: 'app-save-preset-modal', 11 | templateUrl: './save-preset-modal.component.html', 12 | styleUrls: ['./save-preset-modal.component.scss'] 13 | }) 14 | export class SavePresetModalComponent extends ModalComponent implements OnInit { 15 | @Output() onSave: EventEmitter = new EventEmitter(); 16 | private presets: PresetObject = {}; 17 | private formGroup: FormGroup; 18 | private submitted: boolean = false; 19 | 20 | constructor(private presetsService: PresetsService) { 21 | super(); 22 | } 23 | 24 | ngOnInit() { 25 | this.initializeForm(); 26 | } 27 | 28 | openModal() { 29 | this.presetsService._userPresets 30 | .pipe(take(1)) 31 | .subscribe(presets => { 32 | this.presets = presets; 33 | this.submitted = false; 34 | this.initializeForm(); 35 | this.setOpen(true); 36 | }); 37 | } 38 | 39 | getFormGroup(): FormGroup { 40 | return this.formGroup; 41 | } 42 | 43 | getPresets(): PresetObject { 44 | return this.presets; 45 | } 46 | 47 | isSubmitted(): boolean { 48 | return this.submitted; 49 | } 50 | 51 | onSavePreset(formValue: SavePresetForm): void { 52 | this.submitted = true; 53 | 54 | if (this.formGroup.valid) { 55 | this.onSave.emit(formValue.preset); 56 | this.setOpen(false); 57 | } 58 | } 59 | 60 | getPresetsAsDropdown(): string[] { 61 | return ['', ...Object.keys(this.presets)]; 62 | } 63 | 64 | onPresetsDropdownChange(event) { 65 | this.formGroup.patchValue({ preset: event.target.value }); 66 | this.formGroup.get('preset').markAsDirty(); 67 | } 68 | 69 | get preset() { 70 | return this.formGroup.get('preset'); 71 | } 72 | 73 | private initializeForm(): void { 74 | const fb = new FormBuilder(); 75 | this.formGroup = fb.group({ 76 | preset: ['', [Validators.required]] 77 | }); 78 | } 79 | } 80 | 81 | interface SavePresetForm { 82 | preset: string; 83 | } 84 | -------------------------------------------------------------------------------- /src/client/src/app/seed-history/seed-history.component.html: -------------------------------------------------------------------------------- 1 | 19 | -------------------------------------------------------------------------------- /src/client/src/app/seed-history/seed-history.component.scss: -------------------------------------------------------------------------------- 1 | .seed { 2 | margin: 1rem; 3 | padding-bottom: 1rem; 4 | border-bottom: 1px solid #7f7f7f; 5 | 6 | .hashItem { 7 | margin-right: 0.5rem; 8 | } 9 | 10 | .hashItem:last-child { 11 | margin-right: 0; 12 | } 13 | } 14 | 15 | .seed:last-child { 16 | border-bottom: none; 17 | } 18 | -------------------------------------------------------------------------------- /src/client/src/app/seed-history/seed-history.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { SeedHistoryComponent } from './seed-history.component'; 4 | 5 | describe('SeedHistoryComponent', () => { 6 | let component: SeedHistoryComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [ SeedHistoryComponent ] 12 | }) 13 | .compileComponents(); 14 | })); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(SeedHistoryComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/client/src/app/seed-history/seed-history.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import { faChevronLeft } from '@fortawesome/free-solid-svg-icons'; 3 | import { Subject } from 'rxjs'; 4 | import { takeUntil } from 'rxjs/operators'; 5 | 6 | import { SeedService } from '../services/seed.service'; 7 | import { GeneratedSeed } from '../../../../common/models/generatedSeed'; 8 | 9 | @Component({ 10 | selector: 'app-seed-history', 11 | templateUrl: './seed-history.component.html', 12 | styleUrls: ['./seed-history.component.scss'] 13 | }) 14 | export class SeedHistoryComponent implements OnInit { 15 | isLoaded = false; 16 | seedHistory: GeneratedSeed[]; 17 | faChevronLeft = faChevronLeft; 18 | private ngUnsubscribe: Subject = new Subject(); 19 | 20 | constructor(private seedService: SeedService) { } 21 | 22 | ngOnInit() { 23 | this.seedService._seedHistory 24 | .pipe(takeUntil(this.ngUnsubscribe)) 25 | .subscribe(seedHistory => { 26 | this.seedHistory = seedHistory; 27 | this.isLoaded = true; 28 | }); 29 | } 30 | 31 | ngOnDestroy() { 32 | this.ngUnsubscribe.next(); 33 | this.ngUnsubscribe.complete(); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/client/src/app/services/diagnostics.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed } from '@angular/core/testing'; 2 | 3 | import { DiagnosticsService } from './diagnostics.service'; 4 | 5 | describe('DiagnosticsService', () => { 6 | beforeEach(() => TestBed.configureTestingModule({})); 7 | 8 | it('should be created', () => { 9 | const service: DiagnosticsService = TestBed.get(DiagnosticsService); 10 | expect(service).toBeTruthy(); 11 | }); 12 | }); 13 | -------------------------------------------------------------------------------- /src/client/src/app/services/diagnostics.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable, NgZone } from '@angular/core'; 2 | import { BehaviorSubject, Subject } from 'rxjs'; 3 | import { ToastrService } from 'ngx-toastr'; 4 | 5 | import { ElectronService } from './electron.service'; 6 | 7 | export interface IsoData { 8 | gameCode?: string; 9 | revision?: number; 10 | md5Hash?: string; 11 | } 12 | 13 | @Injectable({ 14 | providedIn: 'root' 15 | }) 16 | export class DiagnosticsService { 17 | private isoData$ = new BehaviorSubject(undefined); 18 | private errorParse$ = new Subject(); 19 | _isoData = this.isoData$.asObservable(); 20 | _errorParse = this.errorParse$.asObservable(); 21 | 22 | constructor(private ngZone: NgZone, private electronService: ElectronService, private toastrService: ToastrService) { 23 | this.electronService.ipcRenderer.on('parseIsoResponse', (event, data: IsoData) => { 24 | this.ngZone.run(() => { 25 | this.isoData$.next(data); 26 | }); 27 | }); 28 | 29 | this.electronService.ipcRenderer.on('parseIsoError', (event, errMsg: string) => { 30 | this.ngZone.run(() => { 31 | this.errorParse$.next(); 32 | this.toastrService.error('Verification on your ISO ran into an error: ' + errMsg, null, { 33 | disableTimeOut: true 34 | }); 35 | }); 36 | }); 37 | 38 | this.electronService.ipcRenderer.on('saveIsoDataResponse', (event, errMsg: string) => { 39 | this.ngZone.run(() => { 40 | if (errMsg) { 41 | this.toastrService.error('Failed to save diagnostics: ' + errMsg, null, { 42 | disableTimeOut: true 43 | }); 44 | } else { 45 | this.toastrService.success('Diagnostics saved.'); 46 | } 47 | }); 48 | }); 49 | } 50 | 51 | verifyIso(isoPath: string) { 52 | this.electronService.ipcRenderer.send('parseIso', isoPath); 53 | } 54 | 55 | setIsoData(data: IsoData) { 56 | this.isoData$.next(data); 57 | } 58 | 59 | saveIsoData(isoData: IsoData) { 60 | this.electronService.dialog.showSaveDialog({ 61 | title: 'Save ISO Verification Data', 62 | filters: [ 63 | { name: 'JSON Files', extensions: ['json'] } 64 | ] 65 | }).then(result => { 66 | if (!result.canceled && result.filePath) { 67 | this.electronService.ipcRenderer.send('saveIsoData', isoData, result.filePath); 68 | } 69 | }); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/client/src/app/services/electron.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed } from '@angular/core/testing'; 2 | 3 | import { ElectronService } from './electron.service'; 4 | 5 | describe('ElectronService', () => { 6 | beforeEach(() => TestBed.configureTestingModule({})); 7 | 8 | it('should be created', () => { 9 | const service: ElectronService = TestBed.get(ElectronService); 10 | expect(service).toBeTruthy(); 11 | }); 12 | }); 13 | -------------------------------------------------------------------------------- /src/client/src/app/services/electron.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | 3 | // If you import a module but never use any of the imported values other than as TypeScript types, 4 | // the resulting javascript file will look as if you never imported the module at all. 5 | import { ipcRenderer, webFrame, remote, shell } from 'electron'; 6 | import * as childProcess from 'child_process'; 7 | import * as fs from 'fs'; 8 | 9 | @Injectable({ 10 | providedIn: 'root' 11 | }) 12 | export class ElectronService { 13 | ipcRenderer: typeof ipcRenderer; 14 | webFrame: typeof webFrame; 15 | remote: typeof remote; 16 | childProcess: typeof childProcess; 17 | fs: typeof fs; 18 | dialog: typeof remote.dialog; 19 | shell: typeof shell; 20 | 21 | constructor() { 22 | // Conditional imports 23 | if (this.isElectron) { 24 | this.ipcRenderer = window.require('electron').ipcRenderer; 25 | this.webFrame = window.require('electron').webFrame; 26 | this.remote = window.require('electron').remote; 27 | this.shell = window.require('electron').shell; 28 | this.dialog = this.remote.dialog; 29 | 30 | this.childProcess = window.require('child_process'); 31 | this.fs = window.require('fs'); 32 | } 33 | } 34 | 35 | get isElectron() { 36 | return window && window.process && window.process.type; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/client/src/app/services/generator.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed } from '@angular/core/testing'; 2 | 3 | import { GeneratorService } from './generator.service'; 4 | 5 | describe('GeneratorService', () => { 6 | beforeEach(() => TestBed.configureTestingModule({})); 7 | 8 | it('should be created', () => { 9 | const service: GeneratorService = TestBed.get(GeneratorService); 10 | expect(service).toBeTruthy(); 11 | }); 12 | }); 13 | -------------------------------------------------------------------------------- /src/client/src/app/services/patcher.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed } from '@angular/core/testing'; 2 | 3 | import { PatcherService } from './patcher.service'; 4 | 5 | describe('PatcherService', () => { 6 | beforeEach(() => TestBed.configureTestingModule({})); 7 | 8 | it('should be created', () => { 9 | const service: PatcherService = TestBed.get(PatcherService); 10 | expect(service).toBeTruthy(); 11 | }); 12 | }); 13 | -------------------------------------------------------------------------------- /src/client/src/app/services/presets.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed } from '@angular/core/testing'; 2 | 3 | import { PresetsService } from './presets.service'; 4 | 5 | describe('PresetsService', () => { 6 | beforeEach(() => TestBed.configureTestingModule({})); 7 | 8 | it('should be created', () => { 9 | const service: PresetsService = TestBed.get(PresetsService); 10 | expect(service).toBeTruthy(); 11 | }); 12 | }); 13 | -------------------------------------------------------------------------------- /src/client/src/app/services/progress.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed } from '@angular/core/testing'; 2 | 3 | import { ProgressService } from './progress.service'; 4 | 5 | describe('ProgressService', () => { 6 | beforeEach(() => TestBed.configureTestingModule({})); 7 | 8 | it('should be created', () => { 9 | const service: ProgressService = TestBed.get(ProgressService); 10 | expect(service).toBeTruthy(); 11 | }); 12 | }); 13 | -------------------------------------------------------------------------------- /src/client/src/app/services/progress.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { BehaviorSubject } from 'rxjs'; 3 | 4 | import { ProgressBar } from '../../../../common/models/progressBar'; 5 | 6 | @Injectable({ 7 | providedIn: 'root' 8 | }) 9 | export class ProgressService { 10 | progressBars$ = new BehaviorSubject(undefined); 11 | open$ = new BehaviorSubject(false); 12 | title$ = new BehaviorSubject(undefined); 13 | message$ = new BehaviorSubject(undefined); 14 | _progressBars = this.progressBars$.asObservable(); 15 | _open = this.open$.asObservable(); 16 | _title = this.title$.asObservable(); 17 | _message = this.message$.asObservable(); 18 | 19 | constructor() { } 20 | 21 | setProgressBars(progressBars: ProgressBar[]): void { 22 | this.progressBars$.next(progressBars); 23 | } 24 | 25 | setOpen(open: boolean): void { 26 | this.open$.next(open); 27 | } 28 | 29 | setTitle(title: string): void { 30 | this.title$.next(title); 31 | } 32 | 33 | setMessage(message: string): void { 34 | this.message$.next(message); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/client/src/app/services/randomizer.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed } from '@angular/core/testing'; 2 | 3 | import { RandomizerService } from './randomizer.service'; 4 | 5 | describe('RandomizerService', () => { 6 | beforeEach(() => TestBed.configureTestingModule({})); 7 | 8 | it('should be created', () => { 9 | const service: RandomizerService = TestBed.get(RandomizerService); 10 | expect(service).toBeTruthy(); 11 | }); 12 | }); 13 | -------------------------------------------------------------------------------- /src/client/src/app/services/randomizer.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { FormGroup, FormBuilder, Validators } from '@angular/forms'; 3 | import { PrimeRandomizerSettings, settings } from '../../../../electron/models/prime/randomizerSettings'; 4 | 5 | import { version } from '../../../../../package.json'; 6 | import { RandomizerForm } from '../../../../common/models/randomizerForm'; 7 | 8 | @Injectable({ 9 | providedIn: 'root' 10 | }) 11 | export class RandomizerService { 12 | readonly DEFAULT_SETTINGS = new PrimeRandomizerSettings(); 13 | readonly SETTINGS = settings; 14 | readonly DEFAULT_PRESET = 'Default'; 15 | readonly APP_VERSION = version; 16 | 17 | constructor() { } 18 | 19 | /** 20 | * Constructs a FormGroup object representing the randomizer settings. 21 | * This object is meant to be as faithful in structure to the PrimeRandomizerSettings interface as possible. 22 | */ 23 | createForm(): FormGroup { 24 | const fb = new FormBuilder(); 25 | 26 | return fb.group({ 27 | preset: [this.DEFAULT_PRESET], 28 | generationCount: [1, [Validators.min(1), Validators.max(99), Validators.required]], 29 | romSettings: fb.group({ 30 | skipHudPopups: [this.DEFAULT_SETTINGS.skipHudPopups], 31 | hideItemModels: [this.DEFAULT_SETTINGS.hideItemModels], 32 | enableMainPlazaLedgeDoor: [this.DEFAULT_SETTINGS.enableMainPlazaLedgeDoor], 33 | skipImpactCrater: [this.DEFAULT_SETTINGS.skipImpactCrater] 34 | }), 35 | rules: fb.group({ 36 | goal: [this.DEFAULT_SETTINGS.goal], 37 | goalArtifacts: [this.DEFAULT_SETTINGS.goalArtifacts], 38 | artifactLocationHints: [this.DEFAULT_SETTINGS.artifactLocationHints], 39 | elevatorShuffle: [this.DEFAULT_SETTINGS.elevatorShuffle], 40 | heatProtection: [this.DEFAULT_SETTINGS.heatProtection], 41 | suitDamageReduction: [this.DEFAULT_SETTINGS.suitDamageReduction], 42 | startingArea: [this.DEFAULT_SETTINGS.startingArea], 43 | randomStartingItems: fb.group({ 44 | minimum: [this.DEFAULT_SETTINGS.randomStartingItems.minimum, [Validators.required, Validators.min(0), Validators.max(25)]], 45 | maximum: [this.DEFAULT_SETTINGS.randomStartingItems.maximum, [Validators.required, Validators.min(0), Validators.max(25)]] 46 | }), 47 | pointOfNoReturnItems: [this.DEFAULT_SETTINGS.pointOfNoReturnItems], 48 | junkItems: [this.DEFAULT_SETTINGS.junkItems], 49 | shuffleMode: [this.DEFAULT_SETTINGS.shuffleMode] 50 | }), 51 | itemOverrides: fb.array([]), 52 | excludeLocations: fb.array([]), 53 | tricks: fb.array([]) 54 | }); 55 | } 56 | 57 | getRandomizerFormGracefully(form: RandomizerForm): RandomizerForm { 58 | const newSettings = this.DEFAULT_SETTINGS.toRandomizerForm(); 59 | 60 | Object.assign(newSettings.romSettings, form.romSettings); 61 | Object.assign(newSettings.rules, form.rules); 62 | Object.assign(newSettings.itemOverrides, form.itemOverrides); 63 | Object.assign(newSettings.excludeLocations, form.excludeLocations); 64 | Object.assign(newSettings.tricks, form.tricks); 65 | 66 | return newSettings; 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/client/src/app/services/seed.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed } from '@angular/core/testing'; 2 | 3 | import { SeedService } from './seed.service'; 4 | 5 | describe('SeedService', () => { 6 | beforeEach(() => TestBed.configureTestingModule({})); 7 | 8 | it('should be created', () => { 9 | const service: SeedService = TestBed.get(SeedService); 10 | expect(service).toBeTruthy(); 11 | }); 12 | }); 13 | -------------------------------------------------------------------------------- /src/client/src/app/services/seed.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable, NgZone } from '@angular/core'; 2 | import { BehaviorSubject } from 'rxjs'; 3 | 4 | import { ElectronService } from './electron.service'; 5 | import { GeneratedSeed } from '../../../../common/models/generatedSeed'; 6 | 7 | @Injectable({ 8 | providedIn: 'root' 9 | }) 10 | export class SeedService { 11 | private seedHistory$ = new BehaviorSubject(undefined); 12 | _seedHistory = this.seedHistory$.asObservable(); 13 | 14 | constructor(private ngZone: NgZone, private electronService: ElectronService) { 15 | this.getSeedHistory(); 16 | 17 | this.electronService.ipcRenderer.on('getSeedHistoryResponse', (event, seedHistory: GeneratedSeed[]) => { 18 | this.ngZone.run(() => { 19 | this.seedHistory$.next(seedHistory); 20 | }); 21 | }); 22 | } 23 | 24 | getSeedHistory() { 25 | this.electronService.ipcRenderer.send('getSeedHistory'); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/client/src/app/services/settings.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed } from '@angular/core/testing'; 2 | 3 | import { SettingsService } from './settings.service'; 4 | 5 | describe('SettingsService', () => { 6 | beforeEach(() => TestBed.configureTestingModule({})); 7 | 8 | it('should be created', () => { 9 | const service: SettingsService = TestBed.get(SettingsService); 10 | expect(service).toBeTruthy(); 11 | }); 12 | }); 13 | -------------------------------------------------------------------------------- /src/client/src/app/services/settings.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable, NgZone } from '@angular/core'; 2 | import { BehaviorSubject } from 'rxjs'; 3 | 4 | import { ElectronService } from './electron.service'; 5 | import { RandomizerForm } from '../../../../common/models/randomizerForm'; 6 | import { PatchForm } from '../../../../common/models/patchForm'; 7 | 8 | @Injectable({ 9 | providedIn: 'root' 10 | }) 11 | export class SettingsService { 12 | private settings$ = new BehaviorSubject(undefined); 13 | private patchSettings$ = new BehaviorSubject(undefined); 14 | _settings = this.settings$.asObservable(); 15 | _patchSettings = this.patchSettings$.asObservable(); 16 | 17 | constructor(private ngZone: NgZone, private electronService: ElectronService) { 18 | this.electronService.ipcRenderer.on('getSettingsResponse', (event, settings: RandomizerForm) => { 19 | this.ngZone.run(() => { 20 | this.settings$.next(settings); 21 | }); 22 | }); 23 | 24 | this.electronService.ipcRenderer.on('getPatchSettingsResponse', (event, settings: PatchForm) => { 25 | this.ngZone.run(() => { 26 | this.patchSettings$.next(settings); 27 | }); 28 | }); 29 | } 30 | 31 | getSettings(): void { 32 | this.electronService.ipcRenderer.send('getSettings'); 33 | } 34 | 35 | applySettings(settings: RandomizerForm): void { 36 | this.electronService.ipcRenderer.send('applySettings', settings); 37 | } 38 | 39 | getPatchSettings(): void { 40 | this.electronService.ipcRenderer.send('getPatchSettings'); 41 | } 42 | 43 | applyPatchSettings(settings: PatchForm) { 44 | this.electronService.ipcRenderer.send('applyPatchSettings', settings); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/client/src/app/services/tab.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed } from '@angular/core/testing'; 2 | 3 | import { TabService } from './tab.service'; 4 | 5 | describe('TabService', () => { 6 | beforeEach(() => TestBed.configureTestingModule({})); 7 | 8 | it('should be created', () => { 9 | const service: TabService = TestBed.get(TabService); 10 | expect(service).toBeTruthy(); 11 | }); 12 | }); 13 | -------------------------------------------------------------------------------- /src/client/src/app/services/tab.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { Subject } from 'rxjs'; 3 | 4 | @Injectable({ 5 | providedIn: 'root' 6 | }) 7 | export class TabService { 8 | private selectedTabId$ = new Subject(); 9 | _selectedTabId = this.selectedTabId$.asObservable(); 10 | 11 | constructor() { } 12 | 13 | selectTab(tabId: number) { 14 | this.selectedTabId$.next(tabId); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/client/src/app/services/update.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed } from '@angular/core/testing'; 2 | 3 | import { UpdateService } from './update.service'; 4 | 5 | describe('UpdateService', () => { 6 | beforeEach(() => TestBed.configureTestingModule({})); 7 | 8 | it('should be created', () => { 9 | const service: UpdateService = TestBed.get(UpdateService); 10 | expect(service).toBeTruthy(); 11 | }); 12 | }); 13 | -------------------------------------------------------------------------------- /src/client/src/app/services/update.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { HttpClient } from '@angular/common/http'; 3 | import { take } from 'rxjs/operators'; 4 | import { ToastrService } from 'ngx-toastr'; 5 | import * as compareVersions from 'compare-versions'; 6 | 7 | import { version } from '../../../../../package.json'; 8 | import { ElectronService } from './electron.service.js'; 9 | 10 | @Injectable({ 11 | providedIn: 'root' 12 | }) 13 | export class UpdateService { 14 | constructor(private http: HttpClient, private toastrService: ToastrService, private electronService: ElectronService) { 15 | 16 | } 17 | 18 | checkForUpdates(): void { 19 | this.http.get('https://raw.githubusercontent.com/BashPrime/metroid-prime-randomizer/master/package.json') 20 | .subscribe((data: any) => { 21 | if (compareVersions(version, data.version) < 0) { 22 | this.toastrService.info('Click to go to the downloads page.', 'Update Available! (v' + data.version + ')', { 23 | disableTimeOut: true, 24 | positionClass: 'toast-bottom-left' 25 | }).onTap.pipe(take(1)).subscribe(() => this.electronService.shell.openExternal('https://github.com/BashPrime/metroid-prime-randomizer/releases/latest')) 26 | } 27 | }); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/client/src/app/settings/customize-settings-container/customize-settings-container.component.html: -------------------------------------------------------------------------------- 1 |
2 | 7 |
8 |
9 | 10 | 11 | 12 | 13 | 14 |
15 | 16 | -------------------------------------------------------------------------------- /src/client/src/app/settings/customize-settings-container/customize-settings-container.component.scss: -------------------------------------------------------------------------------- 1 | .tabs { 2 | flex: 0 0 auto; 3 | } 4 | 5 | .settings-container { 6 | flex: 1; 7 | } 8 | -------------------------------------------------------------------------------- /src/client/src/app/settings/customize-settings-container/customize-settings-container.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { CustomizeSettingsContainerComponent } from './customize-settings-container.component'; 4 | 5 | describe('CustomizeSettingsContainerComponent', () => { 6 | let component: CustomizeSettingsContainerComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [ CustomizeSettingsContainerComponent ] 12 | }) 13 | .compileComponents(); 14 | })); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(CustomizeSettingsContainerComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/client/src/app/settings/customize-settings-container/customize-settings-container.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit, Input } from '@angular/core'; 2 | import { FormGroup, ControlContainer } from '@angular/forms'; 3 | 4 | import { Tab } from '../../../../../common/models/tab'; 5 | 6 | @Component({ 7 | selector: 'app-customize-settings-container', 8 | templateUrl: './customize-settings-container.component.html', 9 | styleUrls: ['./customize-settings-container.component.scss'] 10 | }) 11 | export class CustomizeSettingsContainerComponent implements OnInit { 12 | @Input() disabled: boolean; 13 | 14 | readonly tabIds = { 15 | rom: 0, 16 | rules: 1, 17 | itemOverrides: 2, 18 | excludeLocations: 3, 19 | tricks: 4 20 | }; 21 | readonly tabs: Tab[] = [ 22 | { id: this.tabIds.rom, name: 'ROM Settings' }, 23 | { id: this.tabIds.rules, name: 'Rules' }, 24 | { id: this.tabIds.itemOverrides, name: 'Item Overrides' }, 25 | { id: this.tabIds.excludeLocations, name: 'Exclude Locations' }, 26 | { id: this.tabIds.tricks, name: 'Tricks' } 27 | ]; 28 | private selectedTabId = this.tabIds.rom; 29 | 30 | constructor(private controlContainer: ControlContainer) { } 31 | 32 | ngOnInit() { 33 | } 34 | 35 | getFormGroup(): FormGroup { 36 | return this.controlContainer.control as FormGroup; 37 | } 38 | 39 | setSelectedTabId(tabId: number) { 40 | this.selectedTabId = tabId; 41 | } 42 | 43 | isTabIdSelected(tabId: number): boolean { 44 | return tabId === this.selectedTabId; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/client/src/app/settings/exclude-locations/exclude-locations.component.html: -------------------------------------------------------------------------------- 1 | 5 | 6 |
7 | {{ item.label }} 8 |
9 |
10 |
11 | -------------------------------------------------------------------------------- /src/client/src/app/settings/exclude-locations/exclude-locations.component.scss: -------------------------------------------------------------------------------- 1 | // Picklist styling 2 | :host { 3 | ::ng-deep .ui-picklist { 4 | .ui-picklist-filter-container { 5 | background-color: inherit; 6 | color: inherit; 7 | 8 | input.ui-picklist-filter { 9 | background-color: #101010; 10 | color: #efefef; 11 | border-color: #404040; 12 | } 13 | } 14 | 15 | .ui-picklist-list { 16 | background-color: #101010; 17 | color: #efefef; 18 | border-color: #404040; 19 | 20 | .ui-picklist-item { 21 | color: #efefef; 22 | } 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/client/src/app/settings/exclude-locations/exclude-locations.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { ExcludeLocationsComponent } from './exclude-locations.component'; 4 | 5 | describe('ExcludeLocationsComponent', () => { 6 | let component: ExcludeLocationsComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [ ExcludeLocationsComponent ] 12 | }) 13 | .compileComponents(); 14 | })); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(ExcludeLocationsComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/client/src/app/settings/exclude-locations/exclude-locations.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import { FormArray, ControlContainer } from '@angular/forms'; 3 | 4 | import { PicklistFormComponent } from 'src/app/components/common/picklist-form.component'; 5 | import { RandomizerService } from 'src/app/services/randomizer.service'; 6 | 7 | @Component({ 8 | selector: 'app-exclude-locations', 9 | templateUrl: './exclude-locations.component.html', 10 | styleUrls: ['./exclude-locations.component.scss'] 11 | }) 12 | export class ExcludeLocationsComponent extends PicklistFormComponent implements OnInit { 13 | protected formArray: FormArray; 14 | 15 | // Constants 16 | readonly GLOBAL_STYLE = { height: 'calc(100% - 0.75rem)' }; 17 | 18 | constructor(private controlContainer: ControlContainer, protected randomizerService: RandomizerService) { 19 | super(randomizerService); 20 | } 21 | 22 | ngOnInit() { 23 | this.formArray = this.controlContainer.control.get('excludeLocations') as FormArray; 24 | this.initialize(); 25 | } 26 | 27 | protected initialize(): void { 28 | const settings = this.randomizerService.DEFAULT_SETTINGS; 29 | 30 | for (let key of settings.excludeLocations.getSettingsKeys()) { 31 | const location = { 32 | label: key, 33 | value: key 34 | }; 35 | 36 | // If form contains value on init, push to selected locations. Else, push to available locations. 37 | if (this.formArray.value.includes(location.value)) { 38 | this.items.selected.push(location); 39 | } else { 40 | this.items.available.push(location); 41 | } 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/client/src/app/settings/read-only-settings-container/read-only-settings-container.component.scss: -------------------------------------------------------------------------------- 1 | .title { 2 | font-size: 1rem; 3 | } 4 | 5 | .has-bottom-border { 6 | padding-bottom: 0.5rem; 7 | border-bottom: 1px solid #707070; 8 | } 9 | 10 | .heading { 11 | color: #8c9b9d; 12 | } 13 | 14 | ul { 15 | list-style: circle inside; 16 | } 17 | 18 | li { 19 | margin-bottom: 0.35rem; 20 | } 21 | -------------------------------------------------------------------------------- /src/client/src/app/settings/read-only-settings-container/read-only-settings-container.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { ReadOnlySettingsContainerComponent } from './read-only-settings-container.component'; 4 | 5 | describe('ReadOnlySettingsContainerComponent', () => { 6 | let component: ReadOnlySettingsContainerComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [ ReadOnlySettingsContainerComponent ] 12 | }) 13 | .compileComponents(); 14 | })); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(ReadOnlySettingsContainerComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/client/src/app/settings/rom-settings/rom-settings.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
5 | 6 | 8 |
9 |
10 |
11 |
12 | 13 | 15 |
16 |
17 |
18 |
19 | 20 | 22 |
23 |
24 |
25 |
26 | 27 | 29 |
30 |
31 |
32 |
33 | -------------------------------------------------------------------------------- /src/client/src/app/settings/rom-settings/rom-settings.component.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BashPrime/metroid-prime-randomizer/a43b732aaa5530d2b8b8c3e57f8146f9f35d07ca/src/client/src/app/settings/rom-settings/rom-settings.component.scss -------------------------------------------------------------------------------- /src/client/src/app/settings/rom-settings/rom-settings.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { RomSettingsComponent } from './rom-settings.component'; 4 | 5 | describe('RomSettingsComponent', () => { 6 | let component: RomSettingsComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [ RomSettingsComponent ] 12 | }) 13 | .compileComponents(); 14 | })); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(RomSettingsComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/client/src/app/settings/rom-settings/rom-settings.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit, Input } from '@angular/core'; 2 | import { FormGroup, ControlContainer } from '@angular/forms'; 3 | 4 | import { RandomizerService } from 'src/app/services/randomizer.service'; 5 | import { SettingsSection } from '../settings-section'; 6 | 7 | @Component({ 8 | selector: 'app-rom-settings', 9 | templateUrl: './rom-settings.component.html', 10 | styleUrls: ['./rom-settings.component.scss'] 11 | }) 12 | export class RomSettingsComponent extends SettingsSection implements OnInit { 13 | private formGroup: FormGroup; 14 | constructor(private controlContainer: ControlContainer, protected randomizerService: RandomizerService) { 15 | super(randomizerService); 16 | } 17 | 18 | ngOnInit() { 19 | this.formGroup = this.controlContainer.control.get('romSettings') as FormGroup; 20 | } 21 | 22 | getFormGroup(): FormGroup { 23 | return this.formGroup; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/client/src/app/settings/rules/rules.component.scss: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/client/src/app/settings/rules/rules.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { RulesComponent } from './rules.component'; 4 | 5 | describe('RulesComponent', () => { 6 | let component: RulesComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [ RulesComponent ] 12 | }) 13 | .compileComponents(); 14 | })); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(RulesComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/client/src/app/settings/rules/rules.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import { FormGroup, ControlContainer } from '@angular/forms'; 3 | 4 | import { RandomizerService } from 'src/app/services/randomizer.service'; 5 | import { SettingsSection } from '../settings-section'; 6 | 7 | @Component({ 8 | selector: 'app-rules', 9 | templateUrl: './rules.component.html', 10 | styleUrls: ['./rules.component.scss'] 11 | }) 12 | export class RulesComponent extends SettingsSection implements OnInit { 13 | private formGroup: FormGroup; 14 | 15 | // Constants 16 | private readonly ARTIFACT_COLLECTION = 'artifact-collection'; 17 | 18 | constructor(private controlContainer: ControlContainer, protected randomizerService: RandomizerService) { 19 | super(randomizerService); 20 | } 21 | 22 | ngOnInit() { 23 | this.formGroup = this.controlContainer.control.get('rules') as FormGroup; 24 | } 25 | 26 | getFormGroup(): FormGroup { 27 | return this.formGroup; 28 | } 29 | 30 | getRandomStartingItemsFormGroup(): FormGroup { 31 | return this.formGroup.controls.randomStartingItems as FormGroup; 32 | } 33 | 34 | isArtifactCollectionSelected(): boolean { 35 | return this.formGroup.get('goal').value === this.ARTIFACT_COLLECTION; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/client/src/app/settings/settings-section.ts: -------------------------------------------------------------------------------- 1 | import { RandomizerService } from '../services/randomizer.service'; 2 | import { details } from '../../../../common/data/settingsDetails'; 3 | 4 | export abstract class SettingsSection { 5 | readonly OBJECT_KEYS = Object.keys; 6 | readonly SETTINGS = this.randomizerService.SETTINGS; 7 | readonly DETAILS = details; 8 | 9 | constructor(protected randomizerService: RandomizerService) { } 10 | 11 | getSetting(name: string) { 12 | return this.SETTINGS.find(setting => setting.name === name); 13 | } 14 | 15 | getDetails(name: string) { 16 | return this.DETAILS[name]; 17 | } 18 | 19 | getDisplayName(name: string) { 20 | return this.getDetails(name).name; 21 | } 22 | 23 | getTooltip(name: string) { 24 | return this.getDetails(name).description; 25 | } 26 | 27 | getChoices(name: string) { 28 | return this.getSetting(name).choices; 29 | } 30 | 31 | getChoiceName(name: string, value: string) { 32 | // Using == because the value can be a number. 33 | return this.getChoices(name).find(choice => choice.value == value).name; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/client/src/app/settings/tricks/tricks.component.html: -------------------------------------------------------------------------------- 1 |
2 |
Trick Difficulty
3 |
4 |
5 |
6 | 9 |
10 |
11 |
12 | 13 | 14 |
15 |
16 |
17 | 21 | 22 |
23 | {{ item.label }} 24 |
25 |
26 |
27 | -------------------------------------------------------------------------------- /src/client/src/app/settings/tricks/tricks.component.scss: -------------------------------------------------------------------------------- 1 | // Picklist styling 2 | :host { 3 | ::ng-deep .ui-picklist { 4 | .ui-picklist-filter-container { 5 | background-color: inherit; 6 | color: inherit; 7 | 8 | input.ui-picklist-filter { 9 | background-color: #101010; 10 | color: #efefef; 11 | border-color: #404040; 12 | } 13 | } 14 | 15 | .ui-picklist-list { 16 | background-color: #101010; 17 | color: #efefef; 18 | border-color: #404040; 19 | 20 | .ui-picklist-item { 21 | color: #efefef; 22 | } 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/client/src/app/settings/tricks/tricks.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { TricksComponent } from './tricks.component'; 4 | 5 | describe('TricksComponent', () => { 6 | let component: TricksComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [ TricksComponent ] 12 | }) 13 | .compileComponents(); 14 | })); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(TricksComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/client/src/app/tricks-detail-modal/tricks-detail-modal.component.html: -------------------------------------------------------------------------------- 1 | 25 | -------------------------------------------------------------------------------- /src/client/src/app/tricks-detail-modal/tricks-detail-modal.component.scss: -------------------------------------------------------------------------------- 1 | ul { 2 | list-style: circle inside; 3 | } 4 | 5 | li { 6 | margin-bottom: 0.35rem; 7 | cursor: pointer; 8 | } 9 | -------------------------------------------------------------------------------- /src/client/src/app/tricks-detail-modal/tricks-detail-modal.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { TricksDetailModalComponent } from './tricks-detail-modal.component'; 4 | 5 | describe('TricksDetailModalComponent', () => { 6 | let component: TricksDetailModalComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [ TricksDetailModalComponent ] 12 | }) 13 | .compileComponents(); 14 | })); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(TricksDetailModalComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/client/src/app/tricks-detail-modal/tricks-detail-modal.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit, Input } from '@angular/core'; 2 | 3 | import { ModalComponent } from '../components/common/modal.component'; 4 | import { details, Difficulty } from '../../../../common/data/settingsDetails'; 5 | import { TrickItem } from '../../../../common/models/trickItem'; 6 | 7 | interface TrickDetailObject { 8 | [key: string]: TrickItem[]; 9 | } 10 | 11 | @Component({ 12 | selector: 'app-tricks-detail-modal', 13 | templateUrl: './tricks-detail-modal.component.html', 14 | styleUrls: ['./tricks-detail-modal.component.scss'] 15 | }) 16 | export class TricksDetailModalComponent extends ModalComponent implements OnInit { 17 | @Input() private tricks: string[]; 18 | 19 | readonly DETAILS = details; 20 | private detailedTricks: TrickDetailObject = {}; 21 | 22 | constructor() { 23 | super(); 24 | } 25 | 26 | ngOnInit() { 27 | } 28 | 29 | ngOnChanges() { 30 | this.setDetailedTricks(this.buildTricksObject()); 31 | } 32 | 33 | getDetailedTricks(): TrickDetailObject { 34 | return this.detailedTricks; 35 | } 36 | 37 | getDetailedTrickKeys(): string[] { 38 | return Object.keys(this.detailedTricks); 39 | } 40 | 41 | setDetailedTricks(detailedTricks: TrickDetailObject) { 42 | this.detailedTricks = detailedTricks; 43 | } 44 | 45 | private buildTricksObject(): TrickDetailObject { 46 | const detailedTricks: TrickDetailObject = { 47 | [Difficulty.TRIVIAL]: [], 48 | [Difficulty.EASY]: [], 49 | [Difficulty.NORMAL]: [], 50 | [Difficulty.HARD]: [], 51 | [Difficulty.INSANE]: [], 52 | [Difficulty.OOB]: [] 53 | }; 54 | 55 | for (let trick of this.tricks) { 56 | const trickItem = this.buildTrickItem(trick); 57 | detailedTricks[trickItem.difficulty].push(trickItem); 58 | } 59 | 60 | return Object.keys(detailedTricks) 61 | .filter(key => detailedTricks[key].length > 0) 62 | .reduce((obj, key) => { 63 | obj[key] = detailedTricks[key]; 64 | return obj; 65 | }, {}); 66 | } 67 | 68 | private buildTrickItem(trickName: string): TrickItem { 69 | const trickDetails = this.DETAILS[trickName]; 70 | 71 | return { 72 | label: trickDetails ? trickDetails.name : trickName, 73 | value: trickName, 74 | tooltip: trickDetails 75 | ? trickDetails.description 76 | : null, 77 | difficulty: trickDetails ? trickDetails.difficulty : null 78 | }; 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/client/src/app/utilities.ts: -------------------------------------------------------------------------------- 1 | import { PERMALINK_SEPARATOR } from '../../../common/constants'; 2 | 3 | export function filterProperties(obj: object, remove: string[]): object { 4 | const raw = JSON.parse(JSON.stringify(obj)); 5 | 6 | return Object.keys(raw) 7 | .filter(key => !remove.includes(key)) 8 | .reduce((obj, key) => { 9 | obj[key] = raw[key]; 10 | return obj; 11 | }, {}); 12 | } 13 | 14 | /** 15 | * Generates a base64 string from the given seed and permalink 16 | * @param seed The seed string used for setting the RNG seed. 17 | * @param settingsString The settings string representing the settings used. 18 | */ 19 | export function generatePermalink(seed: string, settingsString: string): string { 20 | if (seed && settingsString) { 21 | return btoa(seed + PERMALINK_SEPARATOR + settingsString); 22 | } 23 | 24 | return null; 25 | } 26 | 27 | /** 28 | * Parses a provided base64 permalink string into the seed and settings strings. 29 | * @param permalink The permalink to be parsed 30 | */ 31 | export function parsePermalink(permalink: string): { seed: string, settingsString: string } { 32 | let decodedString: string; 33 | const expectedEntries = 2; 34 | 35 | try { 36 | decodedString = atob(permalink); 37 | } catch (err) { 38 | throw new Error('The permalink is incorrectly encoded, and could not be parsed.'); 39 | } 40 | 41 | const decodedEntries = atob(permalink).split(PERMALINK_SEPARATOR); 42 | 43 | if (decodedEntries.length !== expectedEntries) { 44 | throw new Error(`Incorrect number of entries in the permalink. (expected: 2, actual: ${decodedEntries.length})`); 45 | } 46 | 47 | return { 48 | seed: decodedEntries[0], 49 | settingsString: decodedEntries[1] 50 | }; 51 | } 52 | -------------------------------------------------------------------------------- /src/client/src/app/welcome/welcome.component.html: -------------------------------------------------------------------------------- 1 |
2 |

Welcome

3 |
4 | 5 | 6 |
7 |
8 | 9 | 10 |
11 | 12 |
13 | -------------------------------------------------------------------------------- /src/client/src/app/welcome/welcome.component.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BashPrime/metroid-prime-randomizer/a43b732aaa5530d2b8b8c3e57f8146f9f35d07ca/src/client/src/app/welcome/welcome.component.scss -------------------------------------------------------------------------------- /src/client/src/app/welcome/welcome.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { WelcomeComponent } from './welcome.component'; 4 | 5 | describe('WelcomeComponent', () => { 6 | let component: WelcomeComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [ WelcomeComponent ] 12 | }) 13 | .compileComponents(); 14 | })); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(WelcomeComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/client/src/app/welcome/welcome.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit, ViewChild } from '@angular/core'; 2 | 3 | import { TabService } from '../services/tab.service'; 4 | import { ImportSettingsModalComponent } from '../import-settings-modal/import-settings-modal.component'; 5 | import { ElectronService } from '../services/electron.service'; 6 | 7 | @Component({ 8 | selector: 'app-welcome', 9 | templateUrl: './welcome.component.html', 10 | styleUrls: ['./welcome.component.scss'] 11 | }) 12 | export class WelcomeComponent implements OnInit { 13 | @ViewChild(ImportSettingsModalComponent, {static: false}) private importPermalinkModal: ImportSettingsModalComponent; 14 | 15 | // Constants 16 | private readonly generateGameTab = 1; 17 | private readonly helpTab = 3; 18 | 19 | constructor(private tabService: TabService, private electronService: ElectronService) { } 20 | 21 | ngOnInit() { 22 | } 23 | 24 | goToGenerateGame(): void { 25 | this.tabService.selectTab(this.generateGameTab); 26 | } 27 | 28 | goToHelp(): void { 29 | this.tabService.selectTab(this.helpTab); 30 | } 31 | 32 | openImportPermalinkModal(): void { 33 | this.importPermalinkModal.openModal(); 34 | } 35 | 36 | openSeedHistory(): void { 37 | this.electronService.ipcRenderer.send('openSeedHistoryFolder'); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/client/src/assets/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BashPrime/metroid-prime-randomizer/a43b732aaa5530d2b8b8c3e57f8146f9f35d07ca/src/client/src/assets/.gitkeep -------------------------------------------------------------------------------- /src/client/src/assets/help-documents/differences.md: -------------------------------------------------------------------------------- 1 | # Differences 2 | 3 | This randomizer uses Syncathetic's randomprime patcher, which makes some changes to Metroid Prime for quality of life reasons. Here are the changes that are always applied: 4 | 5 | * Missile and power bomb capacities are shown in the HUD. 6 | * The Phazon Elite in Elite Research can now be broken out of its container as soon as you can visit the room. 7 | * The scan point that disables the forcefield in Research Lab Hydra can now be scanned from either side of the forcefield. 8 | * The FMV that plays when you start a game from the main menu is no longer random. The patcher chooses one of the three FMVs to use depending on the seed, but it will be the same one for every player that uses the same seed. 9 | * The Hint System is turned off by default. 10 | * The cutscene in Temple Security Station has been removed. 11 | * The Ridley flyover cutscene in Phendrana Shorelines has been removed. 12 | * You can safely enter Sunchamber and fight Flaahgra from the Sun Tower side. To trigger the Sunchamber Chozo Ghost layer change, you will need to return to Sun Tower and touch the trigger near the top of the spider track after defeating Flaahgra. 13 | * Research Lab Aether's glass will be broken if you collect the Research Core item before visiting the room for the first time, allowing you access to Control Tower and the rest of the pirate labs. 14 | * Observatory's Boost Ball puzzle can now be done at any time, regardless if you collect the Research Core item beforehand (or not). 15 | * The door in Main Ventilation Shaft Section B is now powered on by default, allowing you to traverse through the entire crashed frigate in reverse on your first visit. 16 | * Virtually every obstacle added in NTSC 1.02 and PAL has been removed, or reverted back to their 1.00 behavior. 17 | * You will no longer be softlocked in Mine Security Station if you kill the pirate troopers before triggering the Security Access A door lock. 18 | * The Grapple point in Gravity Chamber will no longer disappear if you leave the room and return later. 19 | -------------------------------------------------------------------------------- /src/client/src/assets/help-documents/gettingStarted.md: -------------------------------------------------------------------------------- 1 | # Getting Started 2 | 3 | 1. Press the **Generate Game** button on the Welcome Page, or select the **Generate Game** tab at the top of this application. 4 | 5 | 2. Choose a preset, or configure the settings you wish to use by clicking the **Customize** button underneath the presets dropdown. 6 | 7 | ![The Presets dropdown, as well as the Customize button underneath it.](assets/help-documents/images/presetAndCustomize.png) 8 | 9 | 3. Click **Generate**. If you want the option to save a spoiler log, click **Generate with Spoilers**. When the seed is done generating, you will be taken to the Game Details page. 10 | 11 | * The randomizer will usually generate a seed without any issues, but with some settings it may fail to do so. If this happens, click one of the Generate buttons again. Unless you change the item overrides in a way that makes the game unable to be completed, the randomizer will generate a seed sooner or later. 12 | 13 | ![The Game Details page, showing the generated seed information as well as the file patching form.](assets/help-documents/images/gameDetails.png) 14 | 15 | 4. Select a Metroid Prime ISO file for your base ISO that the patcher will use. 16 | 17 | * You can only use a NTSC-USA Nintendo GameCube version of Metroid Prime. 18 | 19 | * You can click the **Verify** button to confirm what version of Metroid Prime you are using. If the Game Code and MD5 Hash fields have checkmarks, you're good to go! 20 | 21 | 5. (Optional) You can choose where to save your randomized seeds and spoiler logs. If left to default, they will be saved in your `Documents/Metroid Prime` folder. 22 | 23 | 6. (Optional) If you have a ISO of Metroid Prime Trilogy, the randomizer can use it to fix the Flaahgra boss music. 24 | 25 | 7. (Optional) You can change the output type and save your generated seed to a compressed ISO, or a compressed Gamecube Archive file. 26 | 27 | 8. Click **Save Spoiler** to save your spoiler log (or logs) to the output folder. 28 | 29 | 9. Click **Save ISO** to patch your randomized seeds and save them to the output folder. 30 | 31 | * Files are automatically saved using the name `Prime - ` to the output folder. 32 | -------------------------------------------------------------------------------- /src/client/src/assets/help-documents/images/gameDetails.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BashPrime/metroid-prime-randomizer/a43b732aaa5530d2b8b8c3e57f8146f9f35d07ca/src/client/src/assets/help-documents/images/gameDetails.png -------------------------------------------------------------------------------- /src/client/src/assets/help-documents/images/presetAndCustomize.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BashPrime/metroid-prime-randomizer/a43b732aaa5530d2b8b8c3e57f8146f9f35d07ca/src/client/src/assets/help-documents/images/presetAndCustomize.png -------------------------------------------------------------------------------- /src/client/src/assets/help-documents/softlocks.md: -------------------------------------------------------------------------------- 1 | # Softlocks 2 | 3 | Softlocks are situations where the game is usually still playable, but further progression is impossible. 4 | 5 | If you're not careful, you can accidentally find yourself in a room without the item needed to leave it, causing you to softlock. While the randomizer handles a lot of these cases, there are some that it does not and cannot account for. 6 | 7 | ## Ruined Shrine 8 | 9 | If you fall into the pit in Ruined Shrine, you will need either the Morph Ball or Space Jump Boots to escape. If you get stuck, you can slope jump and abuse the standable terrain on the large vine near where the Beetle Battle item is, then dash across to the half pipe, but this is never expected of players. 10 | 11 | ## Burn Dome 12 | 13 | If you do not have bombs, the Morph Ball tunnel leading to Burn Dome is a one-way trip. You will be stuck and softlocked if you do not find bombs there. 14 | 15 | ## Chapel of the Elders, Antechamber, and Plasma Processing 16 | 17 | These rooms use different door colors to enter and leave them, so you can get stuck if you enter the room without having the necessary beam to open the door from the inside. 18 | 19 | ## Warrior Shrine Tunnel 20 | 21 | If you enter the tunnel underneath Warrior Shrine without having bombs, you will get stuck. 22 | 23 | ## Shore Tunnel 24 | 25 | If you do not have Space Jump Boots, you will either need to double bomb jump or perform a precise slope jump to reach the broken tube from the pit underneath. 26 | 27 | ## Phendrana Canyon 28 | 29 | If you do not have Boost Ball or Space Jump Boots after entering the room, you will need to jump on the crates to leave. If the crates are destroyed, you will get stuck. 30 | 31 | ## Observatory 32 | 33 | The bottom door only locks when you get near it. If you enter Observatory from the top door and kill the Space Pirates before triggering the lock, it will stay locked while you are still in the room. If you cannot complete the puzzle to extend the platforms, or know how to climb the room with dashes, you will softlock. 34 | 35 | Make sure you trigger the door lock first, then kill the Space Pirates. 36 | 37 | ## Control Tower 38 | 39 | If you collapse the tower and enter the section where the item is without bombs, you will be unable to escape in a glitchless setting. 40 | 41 | ## Quarantine Cave 42 | 43 | You can get stuck in Quarantine Cave if you do not have Spider Ball, Grapple Beam, or know how to slope jump to escape the room. 44 | 45 | ## Lower Phazon Mines / Ventilation Shaft 46 | 47 | If you fall down into the half pipe section of Ventilation Shaft, you will either need Boost Ball to leave the way you entered, or the minimum items needed to traverse through lower Phazon Mines, defeat Omega Pirate, and reach Phazon Processing Center through Processing Center Access. If you cannot do either of these, you will be stuck. 48 | 49 | ## Elite Quarters 50 | 51 | If you start the Omega Pirate fight and don't have X-Ray Visor, you will softlock. Omega Pirate can only take damage while you have X-Ray Visor equipped, and the room will only unlock after defeating it. 52 | 53 | ## Metroid Prime Lair 54 | 55 | You can only use the Phazon Beam if you have the Phazon Suit, and Metroid Prime Essence can only take damage from the Phazon Beam. If you start the fight without the Phazon Suit, you will be unable to continue. 56 | -------------------------------------------------------------------------------- /src/client/src/assets/help-documents/trackers.md: -------------------------------------------------------------------------------- 1 | # Trackers 2 | 3 | Using an item or map tracker is a great way to stay organized, as well as to keep an eye on your progress during a randomizer playthrough. 4 | 5 | ## Web Trackers 6 | 7 | [Elias Thompson's Item Tracker](https://eliasthompson.github.io/simple-metroid-prime-rando-tracker) 8 | 9 | [pkmnfrk's Item Tracker](https://pkmnfrk.github.io/prime-item-tracker) 10 | 11 | ## Downloadable Trackers 12 | 13 | [EmoTracker](https://emotracker.net) (SauceRelic has an item and map tracker for it) 14 | 15 | [Pwootage's and Dyceron's Item Tracker](https://github.com/MetroidPrimeModding/PrimeRandomizerItemHelper) 16 | -------------------------------------------------------------------------------- /src/client/src/assets/scss/_bulma_overrides.scss: -------------------------------------------------------------------------------- 1 | // Metroid Prime Randomizer Bulma overrides 2 | $prime-menu-color: #c66320; 3 | $info: #0050a0; 4 | $link: $prime-menu-color; 5 | $link-hover: #f89552; 6 | $link-active: #943100; 7 | $success: #2ea071; 8 | $input-placeholder-color: #a0a0a0; 9 | -------------------------------------------------------------------------------- /src/client/src/environments/environment.prod.ts: -------------------------------------------------------------------------------- 1 | export const environment = { 2 | production: true 3 | }; 4 | -------------------------------------------------------------------------------- /src/client/src/environments/environment.ts: -------------------------------------------------------------------------------- 1 | // This file can be replaced during build by using the `fileReplacements` array. 2 | // `ng build --prod` replaces `environment.ts` with `environment.prod.ts`. 3 | // The list of file replacements can be found in `angular.json`. 4 | 5 | export const environment = { 6 | production: false 7 | }; 8 | 9 | /* 10 | * For easier debugging in development mode, you can import the following file 11 | * to ignore zone related error stack frames such as `zone.run`, `zoneDelegate.invokeTask`. 12 | * 13 | * This import should be commented out in production mode because it will have a negative impact 14 | * on performance if an error is thrown. 15 | */ 16 | // import 'zone.js/dist/zone-error'; // Included with Angular CLI. 17 | -------------------------------------------------------------------------------- /src/client/src/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BashPrime/metroid-prime-randomizer/a43b732aaa5530d2b8b8c3e57f8146f9f35d07ca/src/client/src/favicon.ico -------------------------------------------------------------------------------- /src/client/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /src/client/src/main.ts: -------------------------------------------------------------------------------- 1 | import { enableProdMode } from '@angular/core'; 2 | import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; 3 | 4 | import { AppModule } from './app/app.module'; 5 | import { environment } from './environments/environment'; 6 | 7 | if (environment.production) { 8 | enableProdMode(); 9 | } 10 | 11 | platformBrowserDynamic().bootstrapModule(AppModule) 12 | .catch(err => console.error(err)); 13 | -------------------------------------------------------------------------------- /src/client/src/polyfills.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This file includes polyfills needed by Angular and is loaded before the app. 3 | * You can add your own extra polyfills to this file. 4 | * 5 | * This file is divided into 2 sections: 6 | * 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers. 7 | * 2. Application imports. Files imported after ZoneJS that should be loaded before your main 8 | * file. 9 | * 10 | * The current setup is for so-called "evergreen" browsers; the last versions of browsers that 11 | * automatically update themselves. This includes Safari >= 10, Chrome >= 55 (including Opera), 12 | * Edge >= 13 on the desktop, and iOS 10 and Chrome on mobile. 13 | * 14 | * Learn more in https://angular.io/guide/browser-support 15 | */ 16 | 17 | /*************************************************************************************************** 18 | * BROWSER POLYFILLS 19 | */ 20 | 21 | /** IE10 and IE11 requires the following for NgClass support on SVG elements */ 22 | // import 'classlist.js'; // Run `npm install --save classlist.js`. 23 | 24 | /** 25 | * Web Animations `@angular/platform-browser/animations` 26 | * Only required if AnimationBuilder is used within the application and using IE/Edge or Safari. 27 | * Standard animation support in Angular DOES NOT require any polyfills (as of Angular 6.0). 28 | */ 29 | // import 'web-animations-js'; // Run `npm install --save web-animations-js`. 30 | 31 | /** 32 | * By default, zone.js will patch all possible macroTask and DomEvents 33 | * user can disable parts of macroTask/DomEvents patch by setting following flags 34 | * because those flags need to be set before `zone.js` being loaded, and webpack 35 | * will put import in the top of bundle, so user need to create a separate file 36 | * in this directory (for example: zone-flags.ts), and put the following flags 37 | * into that file, and then add the following code before importing zone.js. 38 | * import './zone-flags.ts'; 39 | * 40 | * The flags allowed in zone-flags.ts are listed here. 41 | * 42 | * The following flags will work for all browsers. 43 | * 44 | * (window as any).__Zone_disable_requestAnimationFrame = true; // disable patch requestAnimationFrame 45 | * (window as any).__Zone_disable_on_property = true; // disable patch onProperty such as onclick 46 | * (window as any).__zone_symbol__UNPATCHED_EVENTS = ['scroll', 'mousemove']; // disable patch specified eventNames 47 | * 48 | * in IE/Edge developer tools, the addEventListener will also be wrapped by zone.js 49 | * with the following flag, it will bypass `zone.js` patch for IE/Edge 50 | * 51 | * (window as any).__Zone_enable_cross_context_check = true; 52 | * 53 | */ 54 | 55 | /*************************************************************************************************** 56 | * Zone JS is required by default for Angular itself. 57 | */ 58 | import 'zone.js/dist/zone'; // Included with Angular CLI. 59 | 60 | 61 | /*************************************************************************************************** 62 | * APPLICATION IMPORTS 63 | */ 64 | -------------------------------------------------------------------------------- /src/client/src/styles.scss: -------------------------------------------------------------------------------- 1 | /* You can add global styles to this file, and also import other style files */ 2 | @import '~bulmaswatch/darkly/_variables'; 3 | @import './assets/scss/bulma_overrides'; 4 | @import '~bulma/bulma'; 5 | @import '~bulmaswatch/darkly/_overrides'; 6 | @import '~bulma-checkradio/dist/css/bulma-checkradio.min.css'; 7 | @import '~primeicons/primeicons.css'; 8 | @import '~primeng/resources/themes/nova-dark/theme.css'; 9 | @import '~primeng/resources/primeng.min.css'; 10 | @import '~ngx-toastr/toastr'; 11 | 12 | html { 13 | overflow: auto; 14 | } 15 | 16 | html, body { 17 | margin: 0; 18 | height: 100%; 19 | } 20 | 21 | .section { 22 | padding: 1.5rem; 23 | } 24 | 25 | .app-wrapper { 26 | height: 100%; 27 | display: flex; 28 | flex-direction: column; 29 | 30 | .app-wrapper-content { 31 | flex: 1; 32 | overflow: auto; 33 | } 34 | } 35 | 36 | .loading-indicator { 37 | position: relative; 38 | pointer-events: none; 39 | width: 100%; 40 | &:after { 41 | @include loader; 42 | margin: 0 auto; 43 | width: 5rem; 44 | height: 5rem; 45 | } 46 | } 47 | 48 | .select select, .textarea, .input { 49 | background-color: #101010; 50 | color: #efefef; 51 | border-color: #404040; 52 | } 53 | 54 | [disabled].input { 55 | background-color: #202020; 56 | color: #b0b0b0; 57 | border-color: #606060; 58 | } 59 | 60 | // Remove spinners from number inputs 61 | input::-webkit-outer-spin-button, 62 | input::-webkit-inner-spin-button { 63 | /* display: none; <- Crashes Chrome on hover */ 64 | -webkit-appearance: none; 65 | margin: 0; /* <-- Apparently some margin are still there even though it's hidden */ 66 | } 67 | 68 | input[type=number] { 69 | -moz-appearance:textfield; /* Firefox */ 70 | } 71 | 72 | .is-checkradio[type=checkbox] { 73 | position: relative; 74 | } 75 | 76 | .is-checkradio[type=checkbox]+label:first-of-type { 77 | margin-left: -1em; 78 | } 79 | 80 | .is-vertically-centered { 81 | display: flex; 82 | align-items: center; 83 | } 84 | 85 | // Toast notification overrides 86 | .toast-container .notification { 87 | position: relative; 88 | overflow: hidden; 89 | margin: 0 0 6px; 90 | padding: 15px 15px 15px 50px; 91 | width: 300px; 92 | pointer-events: auto; 93 | } 94 | 95 | .toast-container { 96 | bottom: 45px; 97 | } 98 | 99 | .label.is-required:after { 100 | content: ' *'; 101 | color: red; 102 | } 103 | 104 | .has-width-auto { 105 | width: auto; 106 | } 107 | 108 | // Horizontal spacing for inline-block fields 109 | .field.is-spaced-field { 110 | margin-right: 1rem; 111 | } 112 | 113 | .field.spaced-field:last-child { 114 | margin-right: initial; 115 | } 116 | 117 | .ui-tooltip.is-wide-tooltip { 118 | max-width: 25em; 119 | } 120 | 121 | .ui-tooltip.is-modal-tooltip .ui-tooltip-text { 122 | background-color: #202020; 123 | } 124 | -------------------------------------------------------------------------------- /src/client/src/test.ts: -------------------------------------------------------------------------------- 1 | // This file is required by karma.conf.js and loads recursively all the .spec and framework files 2 | 3 | import 'zone.js/dist/zone-testing'; 4 | import { getTestBed } from '@angular/core/testing'; 5 | import { 6 | BrowserDynamicTestingModule, 7 | platformBrowserDynamicTesting 8 | } from '@angular/platform-browser-dynamic/testing'; 9 | 10 | declare const require: any; 11 | 12 | // First, initialize the Angular testing environment. 13 | getTestBed().initTestEnvironment( 14 | BrowserDynamicTestingModule, 15 | platformBrowserDynamicTesting() 16 | ); 17 | // Then we find all the tests. 18 | const context = require.context('./', true, /\.spec\.ts$/); 19 | // And load the modules. 20 | context.keys().map(context); 21 | -------------------------------------------------------------------------------- /src/client/src/typings.d.ts: -------------------------------------------------------------------------------- 1 | /* SystemJS module definition */ 2 | declare var nodeModule: NodeModule; 3 | interface NodeModule { 4 | id: string; 5 | } 6 | 7 | declare var window: Window; 8 | interface Window { 9 | process: any; 10 | require: any; 11 | } -------------------------------------------------------------------------------- /src/client/tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "./out-tsc/app", 5 | "types": [] 6 | }, 7 | "files": [ 8 | "src/main.ts", 9 | "src/polyfills.ts" 10 | ], 11 | "include": [ 12 | "src/**/*.ts" 13 | ], 14 | "exclude": [ 15 | "src/test.ts", 16 | "src/**/*.spec.ts" 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /src/client/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": false, 3 | "compilerOptions": { 4 | "baseUrl": "./", 5 | "outDir": "./dist/out-tsc", 6 | "sourceMap": true, 7 | "declaration": false, 8 | "downlevelIteration": true, 9 | "experimentalDecorators": true, 10 | "module": "esnext", 11 | "moduleResolution": "node", 12 | "resolveJsonModule": true, 13 | "importHelpers": true, 14 | "target": "es5", 15 | "typeRoots": [ 16 | "node_modules/@types" 17 | ], 18 | "lib": [ 19 | "es2018", 20 | "dom" 21 | ] 22 | }, 23 | "angularCompilerOptions": { 24 | "fullTemplateTypeCheck": true, 25 | "strictInjectionParameters": true 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/client/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "./out-tsc/spec", 5 | "types": [ 6 | "jasmine", 7 | "node" 8 | ] 9 | }, 10 | "files": [ 11 | "src/test.ts", 12 | "src/polyfills.ts" 13 | ], 14 | "include": [ 15 | "src/**/*.spec.ts", 16 | "src/**/*.d.ts" 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /src/client/tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "tslint:recommended", 3 | "rules": { 4 | "array-type": false, 5 | "arrow-parens": false, 6 | "deprecation": { 7 | "severity": "warning" 8 | }, 9 | "component-class-suffix": true, 10 | "contextual-lifecycle": true, 11 | "directive-class-suffix": true, 12 | "directive-selector": [ 13 | true, 14 | "attribute", 15 | "app", 16 | "camelCase" 17 | ], 18 | "component-selector": [ 19 | true, 20 | "element", 21 | "app", 22 | "kebab-case" 23 | ], 24 | "import-blacklist": [ 25 | true, 26 | "rxjs/Rx" 27 | ], 28 | "interface-name": false, 29 | "max-classes-per-file": false, 30 | "max-line-length": [ 31 | true, 32 | 140 33 | ], 34 | "member-access": false, 35 | "member-ordering": [ 36 | true, 37 | { 38 | "order": [ 39 | "static-field", 40 | "instance-field", 41 | "static-method", 42 | "instance-method" 43 | ] 44 | } 45 | ], 46 | "no-consecutive-blank-lines": false, 47 | "no-console": [ 48 | true, 49 | "debug", 50 | "info", 51 | "time", 52 | "timeEnd", 53 | "trace" 54 | ], 55 | "no-empty": false, 56 | "no-inferrable-types": [ 57 | true, 58 | "ignore-params" 59 | ], 60 | "no-non-null-assertion": true, 61 | "no-redundant-jsdoc": true, 62 | "no-switch-case-fall-through": true, 63 | "no-use-before-declare": true, 64 | "no-var-requires": false, 65 | "object-literal-key-quotes": [ 66 | true, 67 | "as-needed" 68 | ], 69 | "object-literal-sort-keys": false, 70 | "ordered-imports": false, 71 | "quotemark": [ 72 | true, 73 | "single" 74 | ], 75 | "trailing-comma": false, 76 | "no-conflicting-lifecycle": true, 77 | "no-host-metadata-property": true, 78 | "no-input-rename": true, 79 | "no-inputs-metadata-property": true, 80 | "no-output-native": true, 81 | "no-output-on-prefix": true, 82 | "no-output-rename": true, 83 | "no-outputs-metadata-property": true, 84 | "template-banana-in-box": true, 85 | "template-no-negated-async": true, 86 | "use-lifecycle-interface": true, 87 | "use-pipe-transform-interface": true 88 | }, 89 | "rulesDirectory": [ 90 | "codelyzer" 91 | ] 92 | } -------------------------------------------------------------------------------- /src/common/constants.ts: -------------------------------------------------------------------------------- 1 | export const PERMALINK_SEPARATOR = ' '; 2 | export const ENTRANCE_SEPARATOR = ' --> '; 3 | export const SETTINGS_STRING_DELIMITER = '-'; 4 | export const STARTING_AREA_LANDING_SITE = 20; 5 | export const STARTING_AREA_RANDOM = -1; 6 | -------------------------------------------------------------------------------- /src/common/models/generatedSeed.ts: -------------------------------------------------------------------------------- 1 | export interface GeneratedSeed { 2 | id: string; 3 | seed: string; 4 | settingsString: string; 5 | seedHash: string[]; 6 | createdDate: Date; 7 | } 8 | -------------------------------------------------------------------------------- /src/common/models/itemOverride.ts: -------------------------------------------------------------------------------- 1 | export interface ItemOverride { 2 | name: string; 3 | state: string; 4 | count: number; 5 | isExpansion?: boolean; 6 | } 7 | -------------------------------------------------------------------------------- /src/common/models/patchForm.ts: -------------------------------------------------------------------------------- 1 | export interface PatchForm { 2 | baseIso: string; 3 | outputFolder?: string; 4 | trilogyIso?: string; 5 | outputType: string; 6 | } 7 | -------------------------------------------------------------------------------- /src/common/models/patcherMessage.ts: -------------------------------------------------------------------------------- 1 | export interface PatcherMessage { 2 | type: string; 3 | percent: number; 4 | msg: string; 5 | } 6 | -------------------------------------------------------------------------------- /src/common/models/presetObject.ts: -------------------------------------------------------------------------------- 1 | import { RandomizerForm } from './randomizerForm'; 2 | 3 | export interface PresetObject { 4 | [key: string]: RandomizerForm; 5 | } 6 | -------------------------------------------------------------------------------- /src/common/models/progressBar.ts: -------------------------------------------------------------------------------- 1 | export interface ProgressBar { 2 | total: number; 3 | value: number; 4 | label: string; 5 | class?: string; 6 | } 7 | -------------------------------------------------------------------------------- /src/common/models/randomStartingItems.ts: -------------------------------------------------------------------------------- 1 | export interface RandomStartingItems { 2 | minimum: number; 3 | maximum: number; 4 | } 5 | -------------------------------------------------------------------------------- /src/common/models/randomizerForm.ts: -------------------------------------------------------------------------------- 1 | import { ItemOverride } from './itemOverride'; 2 | import { RandomStartingItems } from './randomStartingItems'; 3 | 4 | export interface RandomizerForm { 5 | generationCount?: number; 6 | romSettings: { 7 | skipHudPopups: boolean; 8 | hideItemModels: boolean; 9 | enableMainPlazaLedgeDoor: boolean; 10 | skipImpactCrater: boolean; 11 | }; 12 | rules: { 13 | goal: string; 14 | goalArtifacts: number; 15 | artifactLocationHints: boolean; 16 | elevatorShuffle: boolean; 17 | heatProtection: string; 18 | suitDamageReduction: string; 19 | startingArea: number; 20 | randomStartingItems: RandomStartingItems; 21 | pointOfNoReturnItems: string; 22 | junkItems: string; 23 | shuffleMode: string; 24 | }; 25 | itemOverrides: ItemOverride[]; 26 | excludeLocations: string[]; 27 | tricks: string[]; 28 | } 29 | -------------------------------------------------------------------------------- /src/common/models/tab.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Basic tab interface for Angular application tabs 3 | */ 4 | export interface Tab { 5 | id: number; 6 | name: string; 7 | hidden?: boolean; 8 | } 9 | -------------------------------------------------------------------------------- /src/common/models/trickItem.ts: -------------------------------------------------------------------------------- 1 | export interface TrickItem { 2 | label: string; 3 | value: string; 4 | tooltip: string; 5 | difficulty: string; 6 | } 7 | -------------------------------------------------------------------------------- /src/electron/controllers.ts: -------------------------------------------------------------------------------- 1 | import * as diagnosticsController from './controllers/diagnosticsController'; 2 | import * as generateSeedController from './controllers/generateSeedController'; 3 | import * as settingsController from './controllers/settingsController'; 4 | import * as seedHistoryController from './controllers/seedHistoryController'; 5 | import * as presetsController from './controllers/presetsController'; 6 | import * as patcherController from './controllers/patcherController'; 7 | import * as spoilerController from './controllers/spoilerController'; 8 | 9 | /** 10 | * Initializes and maintains the controllers used for this application. 11 | */ 12 | export function defineControllers() { 13 | return { 14 | diagnosticsController: diagnosticsController.initialize(), 15 | generateSeed: generateSeedController.initialize(), 16 | settings: settingsController.initialize(), 17 | seedHistory: seedHistoryController.initialize(), 18 | presets: presetsController.initialize(), 19 | patcher: patcherController.initialize(), 20 | spoiler: spoilerController.initialize() 21 | }; 22 | } 23 | -------------------------------------------------------------------------------- /src/electron/controllers/diagnosticsController.ts: -------------------------------------------------------------------------------- 1 | import { app, ipcMain } from 'electron'; 2 | import * as fs from 'fs'; 3 | import * as crypto from 'crypto'; 4 | 5 | interface IsoData { 6 | gameCode?: string; 7 | revision?: number; 8 | md5Hash?: string; 9 | } 10 | 11 | export function initialize() { 12 | ipcMain.on('parseIso', (event, isoPath: string) => { 13 | let isoData: IsoData; 14 | const hash = crypto.createHash('md5'); 15 | let stream = fs.createReadStream(isoPath); 16 | 17 | stream.on('data', (chunk: Buffer) => { 18 | // get isoData values if this is the first chunk 19 | if (!isoData) { 20 | isoData = { 21 | gameCode: chunk.toString('utf8', 0, 6), 22 | revision: chunk[7] 23 | }; 24 | } 25 | 26 | // Update hash with chunk data 27 | hash.update(chunk); 28 | }); 29 | 30 | // Stream is finished, get hash digest and return 31 | stream.on('end', () => { 32 | isoData.md5Hash = hash.digest('hex'); 33 | event.sender.send('parseIsoResponse', isoData); 34 | }); 35 | 36 | // Error handling 37 | stream.on('error', (err: Error) => { 38 | event.sender.send('parseIsoError', err.message); 39 | }); 40 | }); 41 | 42 | ipcMain.on('saveIsoData', (event, isoData: IsoData, filePath: string) => { 43 | fs.writeFile(filePath, JSON.stringify(isoData, null, '\t'), 'utf8', (err) => { 44 | event.sender.send('saveIsoDataResponse', err ? err.code : null); 45 | }); 46 | }); 47 | } 48 | -------------------------------------------------------------------------------- /src/electron/controllers/generateSeedController.ts: -------------------------------------------------------------------------------- 1 | import { ipcMain } from 'electron'; 2 | 3 | import { seedHistory, writeSeedToHistory } from './seedHistoryController'; 4 | import { RandomizerForm } from '../../common/models/randomizerForm'; 5 | import { generateWorld } from '../models/prime/randomizer'; 6 | import { PrimeRandomizerSettings, PrimeRandomizerSettingsArgs } from '../models/prime/randomizerSettings'; 7 | import { SettingsFlagsArgs } from '../models/settingsFlags'; 8 | import { allPresets } from './presetsController'; 9 | 10 | export function initialize() { 11 | ipcMain.on('generateSeed', (event, form: RandomizerForm, spoiler: boolean) => { 12 | let args: PrimeRandomizerSettingsArgs; 13 | const preset = (form as any).preset; 14 | // If a preset was provided, don't use the form controls. Use the preset itself instead. 15 | if (preset !== 'Custom') { 16 | args = convertFormToArgs(allPresets[preset]); 17 | } else { 18 | args = convertFormToArgs(form); 19 | } 20 | 21 | args.spoiler = spoiler; 22 | 23 | // Generate the seed and add it to the seeds history 24 | const settings = new PrimeRandomizerSettings(args); 25 | 26 | try { 27 | const newSeedId = generateSeed(settings); 28 | 29 | // Save generated seed to history 30 | const seed = seedHistory.getSeedObject(newSeedId); 31 | writeSeedToHistory(seed.seed, seed.world); 32 | 33 | // Send client-friendly seed information back to the UI 34 | event.sender.send('generateSeedResponse', seedHistory.getSeedObject(newSeedId).seed); 35 | } catch (err) { 36 | event.sender.send('generateSeedError', err.message); 37 | } 38 | }); 39 | 40 | ipcMain.on('importSeed', (event, seed: string, settingsString: string) => { 41 | try { 42 | const settings = PrimeRandomizerSettings.fromSettingsString(settingsString); 43 | settings.seed = seed; 44 | const newSeedId = generateSeed(settings); 45 | 46 | // Send client-friendly seed information back to the UI 47 | event.sender.send('importSeedResponse', seedHistory.getSeedObject(newSeedId).seed, settings.spoiler); 48 | } catch (err) { 49 | event.sender.send('generateSeedError', 'Failed to import seed: ' + err.message); 50 | } 51 | }); 52 | } 53 | 54 | function generateSeed(settings: PrimeRandomizerSettings): string { 55 | const world = generateWorld(settings); 56 | return seedHistory.addSeedFromWorld(world); 57 | } 58 | 59 | function convertFormToArgs(form: RandomizerForm): PrimeRandomizerSettingsArgs { 60 | const args: PrimeRandomizerSettingsArgs = {}; 61 | Object.assign(args, form.romSettings); 62 | Object.assign(args, form.rules); 63 | Object.assign(args, { 64 | excludeLocations: processArrayControl(form.excludeLocations), 65 | tricks: processArrayControl(form.tricks) 66 | }); 67 | 68 | args.itemOverrides = form.itemOverrides; 69 | 70 | return args; 71 | } 72 | 73 | /** 74 | * Converts the given array to key-value object for the randomizer class to use. 75 | * @param control The array passed from the form 76 | */ 77 | function processArrayControl(control: string[]): SettingsFlagsArgs { 78 | const newControl = {}; 79 | 80 | for (let item of control) { 81 | newControl[item] = true; 82 | } 83 | 84 | return newControl; 85 | } 86 | -------------------------------------------------------------------------------- /src/electron/controllers/seedHistoryController.ts: -------------------------------------------------------------------------------- 1 | import { app, ipcMain, shell } from 'electron'; 2 | import { BehaviorSubject } from 'rxjs'; 3 | import { take } from 'rxjs/operators'; 4 | import * as fs from 'fs'; 5 | import * as path from 'path'; 6 | 7 | import { SeedHistory } from '../models/prime/seedHistory'; 8 | import { GeneratedSeed } from '../../common/models/generatedSeed'; 9 | import { PrimeWorld } from '../models/prime/world'; 10 | import { Spoiler } from '../models/prime/spoiler'; 11 | 12 | export const seedHistory: SeedHistory = new SeedHistory(); 13 | const seedHistoryDir: string = path.join(app.getPath('userData'), 'seed-history'); 14 | const historyFileRead$ = new BehaviorSubject(false); 15 | 16 | export function initialize() { 17 | // Create seed history folder if it does not exist 18 | fs.mkdirSync(seedHistoryDir, { recursive: true }); 19 | 20 | // Request from renderer to open the seed history folder 21 | ipcMain.on('openSeedHistoryFolder', (event) => { 22 | shell.openItem(seedHistoryDir); 23 | }); 24 | 25 | // Request from renderer to get the seed history 26 | ipcMain.on('getSeedHistory', (event) => { 27 | historyFileRead$.asObservable().pipe(take(1)).subscribe(fileRead => { 28 | if (fileRead) { 29 | event.sender.send('getSeedHistoryResponse', seedHistory.toGeneratedSeedArray()); 30 | } 31 | }); 32 | }); 33 | } 34 | 35 | export function writeSeedToHistory(seed: GeneratedSeed, world: PrimeWorld) { 36 | const seedJson = Spoiler.generateFromWorld(world).toJSON(!world.getSettings().spoiler); 37 | const fileName = getSeedHistoryDateString(seed.createdDate) + ' ' + seed.seedHash.toString().replace(/,+/g, ' '); 38 | const filePath = path.join(seedHistoryDir, fileName + '.json'); 39 | 40 | fs.writeFile(filePath, seedJson, 'utf8', err => { 41 | if (err) throw err; 42 | }); 43 | } 44 | 45 | function getSeedHistoryDateString(date: Date): string { 46 | const splitDateString = new Date(date.getTime() - (date.getTimezoneOffset() * 60000)) 47 | .toISOString() 48 | .split('T'); 49 | 50 | return splitDateString[0] + '-' + splitDateString[1].replace(/:/g, '-').replace(/\.\d{3}Z/g, ''); 51 | } 52 | -------------------------------------------------------------------------------- /src/electron/controllers/settingsController.ts: -------------------------------------------------------------------------------- 1 | import { app, ipcMain } from 'electron'; 2 | import * as fs from 'fs'; 3 | import * as path from 'path'; 4 | import { BehaviorSubject, Subject } from 'rxjs'; 5 | import { take } from 'rxjs/operators'; 6 | 7 | import { RandomizerForm } from '../../common/models/randomizerForm'; 8 | import { PatchForm } from '../../common/models/patchForm'; 9 | 10 | const settings$ = new BehaviorSubject(undefined); 11 | const patchSettings$ = new BehaviorSubject(undefined); 12 | 13 | const settingsPath = path.join(app.getPath('userData'), 'settings.json'); 14 | const patchSettingsPath = path.join(app.getPath('userData'), 'patch-settings.json'); 15 | 16 | export function initialize() { 17 | readFile(settingsPath, newSettings => { 18 | settings$.next(newSettings); 19 | }); 20 | 21 | readFile(patchSettingsPath, newPatchSettings => { 22 | patchSettings$.next(newPatchSettings); 23 | }); 24 | 25 | // Request from renderer to get settings file from main process 26 | ipcMain.on('getSettings', (event) => { 27 | settings$.asObservable().pipe(take(1)).subscribe(value => { 28 | if (value) { 29 | event.sender.send('getSettingsResponse', value); 30 | } 31 | }); 32 | }); 33 | 34 | ipcMain.on('getPatchSettings', (event) => { 35 | patchSettings$.asObservable().pipe(take(1)).subscribe(value => { 36 | if (value) { 37 | event.sender.send('getPatchSettingsResponse', value); 38 | } 39 | }); 40 | }); 41 | 42 | ipcMain.on('applySettings', (event, newSettings: RandomizerForm) => { 43 | settings$.next(newSettings); 44 | }); 45 | 46 | ipcMain.on('applyPatchSettings', (event, newPatchSettings: PatchForm) => { 47 | patchSettings$.next(newPatchSettings); 48 | }); 49 | } 50 | 51 | export function writeSettingsFiles() { 52 | // Write general settings if they exist 53 | if (settings$.getValue()) { 54 | fs.writeFile(settingsPath, JSON.stringify(settings$.getValue(), null, '\t'), 'utf8', err => { 55 | if (err) throw err; 56 | }); 57 | } 58 | 59 | // Write patch settings if they exist 60 | if (patchSettings$.getValue()) { 61 | fs.writeFile(patchSettingsPath, JSON.stringify(patchSettings$.getValue(), null, '\t'), 'utf8', err => { 62 | if (err) throw err; 63 | }); 64 | } 65 | } 66 | 67 | function readFile(path: string, callback: (settings: any) => void) { 68 | fs.readFile(path, 'utf8', (err, json) => { 69 | if (!err && json) { 70 | callback(JSON.parse(json)); 71 | } else { 72 | callback(null); 73 | } 74 | }); 75 | } 76 | -------------------------------------------------------------------------------- /src/electron/controllers/spoilerController.ts: -------------------------------------------------------------------------------- 1 | import { app, ipcMain } from 'electron'; 2 | import * as fs from 'fs'; 3 | import * as path from 'path'; 4 | 5 | import { getOutputFolder, getRandomizerFileNameNoExtension, getWorldFromSeedHistory } from './patcherController'; 6 | import { Spoiler } from '../models/prime/spoiler'; 7 | import { GeneratedSeed } from '../../common/models/generatedSeed'; 8 | import { PatchForm } from '../../common/models/patchForm'; 9 | 10 | export function initialize() { 11 | // Request from renderer to get settings file from main process 12 | ipcMain.on('saveSpoiler', (event, seed: GeneratedSeed, form: PatchForm) => { 13 | saveSpoiler(seed, form, err => { 14 | event.sender.send('saveSpoilerResponse', err); 15 | }); 16 | }); 17 | } 18 | 19 | function saveSpoiler(seed: GeneratedSeed, form: PatchForm, callback: (err: NodeJS.ErrnoException) => void) { 20 | const world = getWorldFromSeedHistory(seed.id); 21 | if (world.getSettings().spoiler) { 22 | // World was set with spoiler = true 23 | const spoiler = Spoiler.generateFromWorld(world); 24 | const outputFile = path.join(getOutputFolder(form), getRandomizerFileNameNoExtension(world) + ' - Spoiler.json'); 25 | 26 | fs.writeFile(outputFile, spoiler.toJSON(), 'utf8', err => { 27 | callback(err); 28 | }); 29 | } else { 30 | // Don't generate spoiler if the spoiler flag is false. Just callback immediately 31 | callback(null); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/electron/data/names.json: -------------------------------------------------------------------------------- 1 | [ 2 | "Samus", 3 | "Metroid", 4 | "Prime", 5 | "Varia", 6 | "Gravity", 7 | "Phazon", 8 | "Power", 9 | "Wave", 10 | "Ice", 11 | "Plasma", 12 | "Thermal", 13 | "X-Ray", 14 | "Tallon", 15 | "Chozo", 16 | "Magmoor", 17 | "Phendrana", 18 | "Mines", 19 | "Crater", 20 | "Frigate", 21 | "Truth", 22 | "Strength", 23 | "Elder", 24 | "Wild", 25 | "Lifegiver", 26 | "Warrior", 27 | "Nature", 28 | "Sun", 29 | "World", 30 | "Spirit", 31 | "Newborn", 32 | "Artifact", 33 | "Glider", 34 | "Zoomer", 35 | "Shriekbat", 36 | "Incinerator", 37 | "Mecha", 38 | "Flaahgra", 39 | "Thardus", 40 | "Omega", 41 | "Pirate", 42 | "Sheegoth", 43 | "Wasp", 44 | "Beetle", 45 | "Blastcap", 46 | "Bloodflower", 47 | "Ghost", 48 | "Drone", 49 | "Crystallite", 50 | "Elite", 51 | "Eyon", 52 | "Oculus", 53 | "Fission", 54 | "Hunter", 55 | "Geemer", 56 | "Grizby", 57 | "Hive", 58 | "Parasite", 59 | "Burrower", 60 | "Trooper", 61 | "Bombu", 62 | "Jelzap", 63 | "Lumigek", 64 | "Turret", 65 | "Ridley", 66 | "Exoskeleton", 67 | "Essence", 68 | "Puffer", 69 | "Reaper", 70 | "Scarab", 71 | "Shadow", 72 | "Triclops", 73 | "Access", 74 | "Transport", 75 | "Tunnel", 76 | "Radion", 77 | "Cordite", 78 | "Talloric", 79 | "Bendezium", 80 | "Station", 81 | "Quarantine", 82 | "Hydra", 83 | "Aether", 84 | "Shorelines", 85 | "Chapel", 86 | "Temple", 87 | "Subchamber", 88 | "Jump", 89 | "Morph", 90 | "Boost", 91 | "Spider", 92 | "Grapple", 93 | "Super", 94 | "Charge", 95 | "Wavebuster", 96 | "Spreader", 97 | "Core", 98 | "Flamethrower" 99 | ] 100 | -------------------------------------------------------------------------------- /src/electron/data/presetsDefault.json: -------------------------------------------------------------------------------- 1 | { 2 | "Default": { 3 | "excludeLocations": [], 4 | "itemOverrides": [], 5 | "romSettings": { 6 | "enableMainPlazaLedgeDoor": false, 7 | "hideItemModels": false, 8 | "skipHudPopups": true, 9 | "skipImpactCrater": false 10 | }, 11 | "rules": { 12 | "artifactLocationHints": true, 13 | "elevatorShuffle": false, 14 | "goal": "artifact-collection", 15 | "goalArtifacts": 12, 16 | "heatProtection": "any-suit", 17 | "junkItems": "Nothing", 18 | "pointOfNoReturnItems": "allow-all", 19 | "randomStartingItems": { 20 | "maximum": 0, 21 | "minimum": 0 22 | }, 23 | "shuffleMode": "full", 24 | "startingArea": 20, 25 | "suitDamageReduction": "default" 26 | }, 27 | "tricks": [] 28 | }, 29 | "Beginner Friendly": { 30 | "excludeLocations": [], 31 | "itemOverrides": [ 32 | { 33 | "count": 1, 34 | "name": "Charge Beam", 35 | "state": "starting-item" 36 | }, 37 | { 38 | "count": 1, 39 | "name": "Morph Ball", 40 | "state": "starting-item" 41 | }, 42 | { 43 | "count": 1, 44 | "name": "Morph Ball Bomb", 45 | "state": "starting-item" 46 | }, 47 | { 48 | "count": 1, 49 | "name": "Boost Ball", 50 | "state": "starting-item" 51 | }, 52 | { 53 | "count": 1, 54 | "name": "Spider Ball", 55 | "state": "starting-item" 56 | } 57 | ], 58 | "romSettings": { 59 | "enableMainPlazaLedgeDoor": false, 60 | "hideItemModels": false, 61 | "skipHudPopups": true, 62 | "skipImpactCrater": false 63 | }, 64 | "rules": { 65 | "artifactLocationHints": true, 66 | "elevatorShuffle": false, 67 | "goal": "artifact-collection", 68 | "goalArtifacts": 6, 69 | "heatProtection": "any-suit", 70 | "junkItems": "Energy Tank", 71 | "pointOfNoReturnItems": "do-not-allow", 72 | "randomStartingItems": { 73 | "maximum": 0, 74 | "minimum": 0 75 | }, 76 | "shuffleMode": "full", 77 | "startingArea": 20, 78 | "suitDamageReduction": "default" 79 | }, 80 | "tricks": [] 81 | }, 82 | "2019 Tournament": { 83 | "excludeLocations": [], 84 | "itemOverrides": [ 85 | { 86 | "count": 1, 87 | "name": "Charge Beam", 88 | "state": "vanilla" 89 | } 90 | ], 91 | "romSettings": { 92 | "enableMainPlazaLedgeDoor": false, 93 | "hideItemModels": false, 94 | "skipHudPopups": true, 95 | "skipImpactCrater": false 96 | }, 97 | "rules": { 98 | "artifactLocationHints": true, 99 | "elevatorShuffle": false, 100 | "goal": "artifact-collection", 101 | "goalArtifacts": 6, 102 | "heatProtection": "varia-only", 103 | "junkItems": "Missile Expansion", 104 | "pointOfNoReturnItems": "allow-visible", 105 | "randomStartingItems": { 106 | "maximum": 0, 107 | "minimum": 0 108 | }, 109 | "shuffleMode": "full", 110 | "startingArea": 20, 111 | "suitDamageReduction": "progressive" 112 | }, 113 | "tricks": [ 114 | "destroyBombCoversWithPowerBombs", 115 | "lateMagmoorNoHeatProtection" 116 | ] 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /src/electron/data/visiblePointsOfNoReturn.json: -------------------------------------------------------------------------------- 1 | { 2 | "Alcove": [ 3 | "Landing Site" 4 | ], 5 | "Ruined Shrine (Pit)": [ 6 | "Ruined Shrine (Outer)" 7 | ], 8 | "Antechamber": [ 9 | "Reflecting Pool" 10 | ], 11 | "Shore Tunnel (Lava Pit)": [ 12 | "Shore Tunnel" 13 | ], 14 | "Plasma Processing": [ 15 | "Geothermal Core", 16 | "Magmoor Workstation" 17 | ], 18 | "Chapel of the Elders": [ 19 | "Chozo Ice Temple" 20 | ], 21 | "Observatory": [ 22 | "Control Tower" 23 | ], 24 | "Gravity Chamber": [ 25 | "Hunter Cave" 26 | ] 27 | } -------------------------------------------------------------------------------- /src/electron/enums/elevator.ts: -------------------------------------------------------------------------------- 1 | export enum Elevator { 2 | TALLON_NORTH = 'Tallon Overworld North (Tallon Canyon)', 3 | TALLON_EAST = 'Tallon Overworld East (Frigate Crash Site)', 4 | TALLON_WEST = 'Tallon Overworld West (Root Cave)', 5 | TALLON_SOUTH_CHOZO = 'Tallon Overworld South (Great Tree Hall, Upper)', 6 | TALLON_SOUTH_MINES = 'Tallon Overworld South (Great Tree Hall, Lower)', 7 | CHOZO_WEST = 'Chozo Ruins West (Main Plaza)', 8 | CHOZO_NORTH = 'Chozo Ruins North (Sun Tower)', 9 | CHOZO_EAST = 'Chozo Ruins East (Reflecting Pool, Save Station)', 10 | CHOZO_SOUTH = 'Chozo Ruins South (Reflecting Pool, Far End)', 11 | MAGMOOR_NORTH = 'Magmoor Caverns North (Lava Lake)', 12 | MAGMOOR_WEST = 'Magmoor Caverns West (Monitor Station)', 13 | MAGMOOR_EAST = 'Magmoor Caverns East (Twin Fires)', 14 | MAGMOOR_SOUTH_MINES = 'Magmoor Caverns South (Magmoor Workstation, Debris)', 15 | MAGMOOR_SOUTH_PHENDRANA = 'Magmoor Caverns South (Magmoor Workstation, Save Station)', 16 | PHENDRANA_NORTH = 'Phendrana Drifts North (Phendrana Shorelines)', 17 | PHENDRANA_SOUTH = 'Phendrana Drifts South (Quarantine Cave)', 18 | MINES_EAST = 'Phazon Mines East (Main Quarry)', 19 | MINES_WEST = 'Phazon Mines West (Phazon Processing Center)', 20 | LANDING_SITE = 'Landing Site', 21 | ARTIFACT_TEMPLE = 'Artifact Temple', 22 | CRATER_ENTRY_POINT = 'Crater Entry Point' 23 | } 24 | -------------------------------------------------------------------------------- /src/electron/enums/heatDamagePrevention.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Enumerates the heat damage prevention setting to ease on development. 3 | */ 4 | export enum HeatDamagePrevention { 5 | ANY_SUIT = 'any-suit', 6 | VARIA_ONLY = 'varia-only' 7 | } 8 | -------------------------------------------------------------------------------- /src/electron/enums/optionType.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Represents the option types used for the randomizer settings. 3 | */ 4 | export enum OptionType { 5 | NUMBER, 6 | STRING, 7 | BOOLEAN, 8 | SELECT, 9 | OBJECT 10 | } 11 | -------------------------------------------------------------------------------- /src/electron/enums/pointOfNoReturnItems.ts: -------------------------------------------------------------------------------- 1 | export enum PointOfNoReturnItems { 2 | ALLOW_ALL = 'allow-all', 3 | ALLOW_VISIBLE = 'allow-visible', 4 | DO_NOT_ALLOW = 'do-not-allow' 5 | } -------------------------------------------------------------------------------- /src/electron/enums/primeItem.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Enumerates the Metroid Prime items to ease on development. 3 | */ 4 | export enum PrimeItem { 5 | MISSILE = 'Missile', // mainly for randomprime patcher 6 | MISSILE_EXPANSION = 'Missile Expansion', 7 | ENERGY_TANK = 'Energy Tank', 8 | POWER_BOMB_EXPANSION = 'Power Bomb Expansion', 9 | MISSILE_LAUNCHER = 'Missile Launcher', 10 | WAVE_BEAM = 'Wave Beam', 11 | ICE_BEAM = 'Ice Beam', 12 | PLASMA_BEAM = 'Plasma Beam', 13 | CHARGE_BEAM = 'Charge Beam', 14 | SPACE_JUMP_BOOTS = 'Space Jump Boots', 15 | SUPER_MISSILE = 'Super Missile', 16 | WAVEBUSTER = 'Wavebuster', 17 | ICE_SPREADER = 'Ice Spreader', 18 | FLAMETHROWER = 'Flamethrower', 19 | GRAPPLE_BEAM = 'Grapple Beam', 20 | MORPH_BALL = 'Morph Ball', 21 | BOOST_BALL = 'Boost Ball', 22 | SPIDER_BALL = 'Spider Ball', 23 | MORPH_BALL_BOMB = 'Morph Ball Bomb', 24 | POWER_BOMB = 'Power Bomb', 25 | VARIA_SUIT = 'Varia Suit', 26 | GRAVITY_SUIT = 'Gravity Suit', 27 | PHAZON_SUIT = 'Phazon Suit', 28 | SCAN_VISOR = 'Scan Visor', 29 | THERMAL_VISOR = 'Thermal Visor', 30 | XRAY_VISOR = 'X-Ray Visor', 31 | ARTIFACT_OF_TRUTH = 'Artifact of Truth', 32 | ARTIFACT_OF_STRENGTH = 'Artifact of Strength', 33 | ARTIFACT_OF_ELDER = 'Artifact of Elder', 34 | ARTIFACT_OF_WILD = 'Artifact of Wild', 35 | ARTIFACT_OF_LIFEGIVER = 'Artifact of Lifegiver', 36 | ARTIFACT_OF_WARRIOR = 'Artifact of Warrior', 37 | ARTIFACT_OF_CHOZO = 'Artifact of Chozo', 38 | ARTIFACT_OF_NATURE = 'Artifact of Nature', 39 | ARTIFACT_OF_SUN = 'Artifact of Sun', 40 | ARTIFACT_OF_WORLD = 'Artifact of World', 41 | ARTIFACT_OF_SPIRIT = 'Artifact of Spirit', 42 | ARTIFACT_OF_NEWBORN = 'Artifact of Newborn', 43 | NOTHING = 'Nothing' 44 | } 45 | -------------------------------------------------------------------------------- /src/electron/enums/primeRegion.ts: -------------------------------------------------------------------------------- 1 | export enum PrimeRegion { 2 | TALLON_OVERWORLD = 'Tallon Overworld', 3 | CHOZO_RUINS = 'Chozo Ruins', 4 | MAGMOOR_CAVERNS = 'Magmoor Caverns', 5 | PHENDRANA_DRIFTS = 'Phendrana Drifts', 6 | PHAZON_MINES = 'Phazon Mines' 7 | } -------------------------------------------------------------------------------- /src/electron/main.ts: -------------------------------------------------------------------------------- 1 | import { app, BrowserWindow, Menu } from 'electron'; 2 | import * as path from 'path'; 3 | import * as url from 'url'; 4 | 5 | import * as Utilities from './utilities'; 6 | import menuTemplate from './menu'; 7 | import { defineControllers } from './controllers'; 8 | import { writeSettingsFiles } from './controllers/settingsController'; 9 | 10 | let win: Electron.BrowserWindow; 11 | const serve = Utilities.isServe(); 12 | 13 | function createWindow() { 14 | // Check if we are on a non-serve build 15 | if (!serve) { 16 | // Create our menu entries so that we can use Mac shortcuts 17 | Menu.setApplicationMenu(Menu.buildFromTemplate(menuTemplate)); 18 | } 19 | 20 | // Create the browser window. 21 | win = new BrowserWindow({ 22 | width: 1024, 23 | height: 768, 24 | minWidth: 800, 25 | minHeight: 600, 26 | icon : path.join(__dirname, 'icon.png'), 27 | title: 'Metroid Prime Randomizer', 28 | webPreferences: { 29 | nodeIntegration: true 30 | } 31 | }); 32 | 33 | if (serve) { 34 | win.loadURL('http://localhost:4200'); 35 | win.webContents.openDevTools(); 36 | } else { 37 | win.loadURL(url.format({ 38 | pathname: path.join(__dirname, 'dist/index.html'), 39 | protocol: 'file:', 40 | slashes: true 41 | })); 42 | } 43 | 44 | // Emitted when the window is closed. 45 | win.on('closed', () => { 46 | // Dereference the window object, usually you would store window 47 | // in an array if your app supports multi windows, this is the time 48 | // when you should delete the corresponding element. 49 | 50 | // Write settings file, seed history file if changes have been made. 51 | writeSettingsFiles(); 52 | 53 | win = null; 54 | }); 55 | } 56 | 57 | try { 58 | // This method will be called when Electron has finished 59 | // initialization and is ready to create browser windows. 60 | // Some APIs can only be used after this event occurs. 61 | app.on('ready', createWindow); 62 | 63 | // Quit when all windows are closed. 64 | app.on('window-all-closed', () => { 65 | // On OS X it is common for applications and their menu bar 66 | // to stay active until the user quits explicitly with Cmd + Q 67 | if (process.platform !== 'darwin') { 68 | app.quit(); 69 | } 70 | }); 71 | 72 | app.on('activate', () => { 73 | // On OS X it's common to re-create a window in the app when the 74 | // dock icon is clicked and there are no other windows open. 75 | if (win === null) { 76 | createWindow(); 77 | } 78 | }); 79 | 80 | } catch (e) { 81 | // Catch Error 82 | // throw e; 83 | } 84 | 85 | // Initialize main process controllers 86 | defineControllers(); 87 | -------------------------------------------------------------------------------- /src/electron/menu.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Template of the menu items for the application toolbar. 3 | */ 4 | export const menuTemplate: Electron.MenuItemConstructorOptions[] = [ 5 | { 6 | label: 'File', 7 | submenu: [ 8 | { role: 'quit' } 9 | ] 10 | }, 11 | { 12 | label: 'Edit', 13 | submenu: [ 14 | { role: 'undo' }, 15 | { role: 'redo' }, 16 | { type: 'separator' }, 17 | { role: 'cut' }, 18 | { role: 'copy' }, 19 | { role: 'paste' }, 20 | { role: 'pasteAndMatchStyle' }, 21 | { role: 'delete' }, 22 | { role: 'selectAll' } 23 | ] 24 | } 25 | ]; 26 | 27 | export default menuTemplate; 28 | -------------------------------------------------------------------------------- /src/electron/models/collection.ts: -------------------------------------------------------------------------------- 1 | import { MersenneTwister } from '../mersenneTwister'; 2 | 3 | export abstract class Collection { 4 | protected abstract items: T[]; 5 | 6 | toArray(): T[] { 7 | return this.items; 8 | } 9 | 10 | size(): number { 11 | return this.items.length; 12 | } 13 | 14 | pop(): T { 15 | return this.items.shift(); 16 | } 17 | 18 | push(newItem: T) { 19 | this.items.push(newItem); 20 | } 21 | 22 | abstract remove(item: T): T; 23 | abstract shuffle(rng: MersenneTwister); 24 | abstract filter(fn); 25 | abstract has(key: string): boolean; 26 | abstract diff(otherItems); 27 | abstract merge(otherItems); 28 | } -------------------------------------------------------------------------------- /src/electron/models/item.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Generic representation of an item in the Metroid Prime series. 3 | */ 4 | export class Item { 5 | private name: string; 6 | private type: string; 7 | private priority: number; 8 | private patcherId: number; // internal number used by randomprime patcher 9 | 10 | constructor(name: string, type: string, patcherId: number) { 11 | this.name = name; 12 | this.patcherId = patcherId; 13 | this.type = type; 14 | } 15 | 16 | /** 17 | * Returns the item's name. 18 | */ 19 | getName(): string { 20 | return this.name; 21 | } 22 | 23 | /** 24 | * Returns the item ID used externally, such as through a game patcher. 25 | */ 26 | getPatcherId(): number { 27 | return this.patcherId; 28 | } 29 | 30 | /** 31 | * Returns the item's type. 32 | */ 33 | getType(): string { 34 | return this.type; 35 | } 36 | 37 | /** 38 | * Returns the item's priority (when being placed in the world) 39 | */ 40 | getPriority(): number { 41 | return this.priority; 42 | } 43 | 44 | /** 45 | * Sets the priority number for the item. 46 | * @param priority The priority being assigned. 47 | */ 48 | setPriority(priority: number) { 49 | this.priority = priority; 50 | } 51 | 52 | /** 53 | * Returns a deep-copied (separate) instance of this item. 54 | */ 55 | copy(): Item { 56 | return new Item(this.name, this.type, this.patcherId); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/electron/models/itemCollection.ts: -------------------------------------------------------------------------------- 1 | import { Item } from './item'; 2 | import { Collection } from './collection'; 3 | import { MersenneTwister } from '../mersenneTwister'; 4 | import { randomArray } from '../utilities'; 5 | 6 | export class ItemCollection extends Collection { 7 | protected items: Item[]; 8 | 9 | constructor(items: Item[]) { 10 | super(); 11 | this.items = items; 12 | } 13 | 14 | filter(fn): ItemCollection { 15 | return new ItemCollection(this.items.filter(fn)); 16 | } 17 | 18 | shuffle(rng: MersenneTwister): ItemCollection { 19 | return new ItemCollection(randomArray(this.items, this.items.length, rng)); 20 | } 21 | 22 | remove(element: Item): Item { 23 | const firstIndex = this.items.findIndex(item => item.getName() === element.getName()); 24 | 25 | if (firstIndex > -1) { 26 | this.items.splice(firstIndex, 1); 27 | return element; 28 | } 29 | 30 | return null; 31 | } 32 | 33 | has(key: string): boolean { 34 | return this.items.map(item => item.getName()).includes(key); 35 | } 36 | 37 | hasCount(key: string, count: number): boolean { 38 | return this.items.filter(item => item.getName() === key).length >= count; 39 | } 40 | 41 | diff(otherItems: ItemCollection): ItemCollection { 42 | return this.filter(item => !otherItems.has(item.getName())); 43 | } 44 | 45 | merge(otherItems: ItemCollection): ItemCollection { 46 | return new ItemCollection(this.items.concat(otherItems.toArray())); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/electron/models/location.ts: -------------------------------------------------------------------------------- 1 | import { Item } from './item'; 2 | import { Region } from './region'; 3 | import { ItemCollection } from './itemCollection'; 4 | import { RandomizerSettings } from './randomizerSettings'; 5 | 6 | export interface LocationObject { 7 | [key: string]: (items?: ItemCollection, settings?: RandomizerSettings) => boolean; 8 | }; 9 | 10 | export class Location { 11 | private name: string; 12 | private parentRegion: Region; 13 | private item: Item; 14 | private excluded: boolean = false; 15 | private locked: boolean = false; 16 | 17 | itemRule: (items: ItemCollection, settings: RandomizerSettings) => boolean; 18 | 19 | constructor(name: string) { 20 | this.name = name; 21 | } 22 | 23 | getName(): string { 24 | return this.name; 25 | } 26 | 27 | setName(name: string) { 28 | this.name = name; 29 | } 30 | 31 | getParentRegion(): Region { 32 | return this.parentRegion; 33 | } 34 | 35 | setParentRegion(parentRegion: Region) { 36 | this.parentRegion = parentRegion; 37 | } 38 | 39 | getItem(): Item { 40 | return this.item; 41 | } 42 | 43 | setItem(item: Item): void { 44 | this.item = item; 45 | } 46 | 47 | hasItem(): boolean { 48 | return this.item ? true : false; 49 | } 50 | 51 | isExcluded(): boolean { 52 | return this.excluded; 53 | } 54 | 55 | setExcluded(excluded: boolean): void { 56 | this.excluded = excluded; 57 | } 58 | 59 | isLocked(): boolean { 60 | return this.locked; 61 | } 62 | 63 | setLocked(locked: boolean): void { 64 | this.locked = locked; 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/electron/models/locationCollection.ts: -------------------------------------------------------------------------------- 1 | import { Location } from './location'; 2 | import { ItemCollection } from './itemCollection'; 3 | import { Collection } from './collection'; 4 | import { MersenneTwister } from '../mersenneTwister'; 5 | import { randomArray } from '../utilities'; 6 | 7 | export class LocationCollection extends Collection { 8 | protected items: Location[] = []; 9 | 10 | constructor(locations: Location[]) { 11 | super(); 12 | this.items = locations; 13 | } 14 | 15 | filter(fn): LocationCollection { 16 | return new LocationCollection(this.items.filter(fn)); 17 | } 18 | 19 | shuffle(rng: MersenneTwister): LocationCollection { 20 | return new LocationCollection(randomArray(this.items, this.items.length, rng)); 21 | } 22 | 23 | remove(element: Location): Location { 24 | const firstIndex = this.items.findIndex(item => item.getName() === element.getName()); 25 | 26 | if (firstIndex > -1) { 27 | this.items.splice(firstIndex, 1); 28 | return element; 29 | } 30 | 31 | return null; 32 | } 33 | 34 | getLocationByKey(key: string): Location { 35 | return this.items.find(location => location.getName() === key); 36 | } 37 | 38 | getEmptyLocations(): Location[] { 39 | return this.items.filter(location => !location.hasItem()); 40 | } 41 | 42 | has(key: string): boolean { 43 | return this.items.map(location => location.getName()).includes(key); 44 | } 45 | 46 | diff(otherLocations: LocationCollection): LocationCollection { 47 | return this.filter(item => !otherLocations.has(item.getName())); 48 | } 49 | 50 | merge(otherLocations: LocationCollection): LocationCollection { 51 | return new LocationCollection(this.items.concat(otherLocations.toArray())); 52 | } 53 | 54 | getItems(): ItemCollection { 55 | return new ItemCollection( 56 | this.items.filter(location => location.hasItem()) 57 | .map(location => location.getItem()) 58 | ); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/electron/models/prime/fill.ts: -------------------------------------------------------------------------------- 1 | import { fillRestrictive, fillFast } from '../fill'; 2 | import { PrimeWorld } from './world'; 3 | import { Item } from '../item'; 4 | import { ItemPriority } from './items'; 5 | import { Location } from '../location'; 6 | 7 | /** 8 | * Parent function for distributing an item pool across the Metroid Prime world. 9 | * @param world The Metroid Prime world instance being used. 10 | */ 11 | export function distributeItemsRestrictive(world: PrimeWorld): void { 12 | // Get whole item pool, and shuffle it 13 | const itemPool = world.getItemPool().shuffle(world.getRng()); 14 | 15 | // Progression items and artifacts go in the same pool. 16 | const progressionItemPool = itemPool.filter((item: Item) => item.getPriority() === ItemPriority.PROGRESSION || item.getPriority() === ItemPriority.ARTIFACTS); 17 | const extrasItemPool = itemPool.filter((item: Item) => item.getPriority() === ItemPriority.EXTRA); 18 | 19 | // Logically fill progressive items to ensure the game can be completed. 20 | fillRestrictive(world, progressionItemPool); 21 | 22 | // Filter out filled locations 23 | let fillLocations = world.getLocations().filter((location: Location) => !location.hasItem()); 24 | 25 | // Fill extras/remaining junk items last. No logic needed as the progression items are placed by now. 26 | fillFast(world, fillLocations, extrasItemPool, true); 27 | } 28 | -------------------------------------------------------------------------------- /src/electron/models/prime/itemCollection.ts: -------------------------------------------------------------------------------- 1 | import { ItemCollection } from '../itemCollection'; 2 | import { PrimeItem } from '../../enums/primeItem'; 3 | import { HeatDamagePrevention } from '../../enums/heatDamagePrevention'; 4 | import { PrimeRandomizerSettings } from './randomizerSettings'; 5 | import { MersenneTwister } from '../../mersenneTwister'; 6 | import { randomArray } from '../../utilities'; 7 | 8 | export class PrimeItemCollection extends ItemCollection { 9 | filter(fn): PrimeItemCollection { 10 | return new PrimeItemCollection(this.items.filter(fn)); 11 | } 12 | 13 | shuffle(rng: MersenneTwister): PrimeItemCollection { 14 | return new PrimeItemCollection(randomArray(this.items, this.items.length, rng)); 15 | } 16 | 17 | diff(otherItems: PrimeItemCollection): PrimeItemCollection { 18 | return this.filter(item => !otherItems.has(item.getName())); 19 | } 20 | 21 | merge(otherItems: PrimeItemCollection): PrimeItemCollection { 22 | return new PrimeItemCollection(this.items.concat(otherItems.toArray())); 23 | } 24 | 25 | hasMissiles(): boolean { 26 | return this.has(PrimeItem.MISSILE_LAUNCHER) || this.has(PrimeItem.MISSILE_EXPANSION); 27 | } 28 | 29 | hasMissileCount(count: number) { 30 | return this.filter(item => { 31 | return item.getName() === PrimeItem.MISSILE_EXPANSION || item.getName() === PrimeItem.MISSILE_LAUNCHER 32 | }).size() >= count; 33 | } 34 | 35 | canLayBombs(): boolean { 36 | return this.has(PrimeItem.MORPH_BALL) && this.has(PrimeItem.MORPH_BALL_BOMB); 37 | } 38 | 39 | canLayPowerBombs(): boolean { 40 | return this.has(PrimeItem.MORPH_BALL) && (this.has(PrimeItem.POWER_BOMB) || this.has(PrimeItem.POWER_BOMB_EXPANSION)); 41 | } 42 | 43 | canBoost(): boolean { 44 | return this.has(PrimeItem.MORPH_BALL) && this.has(PrimeItem.BOOST_BALL); 45 | } 46 | 47 | canSpider(): boolean { 48 | return this.has(PrimeItem.MORPH_BALL) && this.has(PrimeItem.SPIDER_BALL); 49 | } 50 | 51 | hasSuit(settings: PrimeRandomizerSettings): boolean { 52 | if (settings.heatProtection === HeatDamagePrevention.VARIA_ONLY) { 53 | return this.has(PrimeItem.VARIA_SUIT); 54 | } 55 | 56 | return this.has(PrimeItem.VARIA_SUIT) || this.has(PrimeItem.GRAVITY_SUIT) || this.has(PrimeItem.PHAZON_SUIT); 57 | } 58 | 59 | canLayBombsOrPowerBombs(): boolean { 60 | return this.canLayBombs() || this.canLayPowerBombs(); 61 | } 62 | 63 | canFireSuperMissiles(): boolean { 64 | return this.hasMissiles() && this.has(PrimeItem.CHARGE_BEAM) && this.has(PrimeItem.SUPER_MISSILE); 65 | } 66 | 67 | canInfiniteSpeed(): boolean { 68 | return this.canLayBombs() && this.canBoost(); 69 | } 70 | 71 | canWallcrawl(settings: PrimeRandomizerSettings): boolean { 72 | const bombReqs = settings.tricks.outOfBoundsWithoutMorphBall || this.canLayBombs(); 73 | return bombReqs && this.has(PrimeItem.SPACE_JUMP_BOOTS); 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/electron/models/prime/patcher.ts: -------------------------------------------------------------------------------- 1 | import randomPrimePatcher from '../../randomprime'; 2 | 3 | export interface PatcherConfiguration { 4 | input_iso: string; 5 | output_iso: string; 6 | layout_string: string; 7 | iso_format: string; 8 | skip_frigate: boolean; 9 | skip_hudmenus: boolean; 10 | nonvaria_heat_damage: boolean; 11 | staggered_suit_damage: boolean; 12 | obfuscate_items: boolean; 13 | artifact_hint_behavior: string; 14 | trilogy_disc_path: string; 15 | starting_items: number; 16 | random_starting_items: number; 17 | comment: string; 18 | main_menu_message: string; 19 | auto_enabled_elevators: boolean; 20 | enable_vault_ledge_door: boolean; 21 | skip_impact_crater: boolean; 22 | } 23 | 24 | export function runRandomprimePatcher(config: PatcherConfiguration, callback: (message: string) => void): void { 25 | randomPrimePatcher.patchRandomizedGame(JSON.stringify(config), message => { 26 | callback(message); 27 | }); 28 | } 29 | 30 | -------------------------------------------------------------------------------- /src/electron/models/prime/randomizer.ts: -------------------------------------------------------------------------------- 1 | import { PrimeRandomizerSettings } from './randomizerSettings'; 2 | import { PrimeWorld } from './world'; 3 | import { generateItemPool } from './itemPool'; 4 | import { setRandomStartingItems, setStartingItems } from './startingItems'; 5 | import { setEntrances } from './entranceShuffle'; 6 | import { setRules } from './rules'; 7 | import { distributeItemsRestrictive } from './fill'; 8 | import { MersenneTwister } from '../../mersenneTwister'; 9 | import { generateAlphanumericString } from '../../utilities'; 10 | 11 | /** 12 | * Generates a Metroid Prime world with logically shuffled items. 13 | * @param settings Configuration object for the world generation 14 | */ 15 | export function generateWorld(settings: PrimeRandomizerSettings): PrimeWorld { 16 | let success = false; 17 | let world: PrimeWorld; 18 | let currentTry = 0; 19 | const maxTries = 100; 20 | 21 | // If no seed is supplied in the settings, generate a random alphanumeric seed. 22 | if (!settings.seed) { 23 | settings.seed = generateAlphanumericString(); 24 | } 25 | 26 | // Initialize rng based on hashed seed, and re-use in case the item distribution fails. 27 | const rng = new MersenneTwister(settings.getNumericSeed()); 28 | let lastErrorMessage: string; 29 | 30 | while (!success && currentTry < maxTries) { 31 | try { 32 | world = new PrimeWorld(settings); 33 | 34 | // Initialize rng based on hashed seed 35 | world.setRng(rng); 36 | 37 | // Set up Prime world regions 38 | world.loadRegions(); 39 | 40 | // Set the root node of the world graph 41 | world.setRootRegion(world.getRegionByKey('Root')); 42 | 43 | // Set starting items for the world 44 | setStartingItems(world); 45 | 46 | // Set random starting items for the world 47 | setRandomStartingItems(world); 48 | 49 | // Generate item pool based on settings, and add the item pool to the world instance 50 | generateItemPool(world); 51 | 52 | // Pass world into entrance shuffle class, using settings to determine entrance shuffle 53 | setEntrances(world); 54 | 55 | // Set core game rules 56 | setRules(world); 57 | 58 | // Fill the world locations using the item pool. 59 | distributeItemsRestrictive(world); 60 | 61 | // If we get here, the item fill succeeded (no exception thrown)! Flag as successful. 62 | success = true; 63 | } catch (Error) { 64 | // Handle exception gracefully and try again. 65 | lastErrorMessage = Error.message; 66 | currentTry++; 67 | } 68 | } 69 | 70 | if (!success) { 71 | throw new Error('Failed to generate a world after ' + maxTries + ' attempts. Last error thrown: ' + lastErrorMessage); 72 | } 73 | 74 | return world; 75 | } 76 | -------------------------------------------------------------------------------- /src/electron/models/prime/regions/root.ts: -------------------------------------------------------------------------------- 1 | import { RegionObject } from '../../region'; 2 | 3 | export function root() { 4 | const regions: RegionObject[] = [ 5 | { 6 | name: 'Root', 7 | exits: { 8 | 'Landing Site': () => true 9 | } 10 | } 11 | ]; 12 | 13 | return regions; 14 | }; 15 | -------------------------------------------------------------------------------- /src/electron/models/prime/rules.ts: -------------------------------------------------------------------------------- 1 | import { PrimeWorld } from './world'; 2 | import { PrimeLocation } from '../../enums/primeLocation'; 3 | 4 | export function setRules(world: PrimeWorld): void { 5 | const locations = world.getLocations(); 6 | 7 | // Set excluded locations 8 | for (let key of world.getSettings().excludeLocations.toArray()) { 9 | locations.getLocationByKey(key).setExcluded(true); 10 | } 11 | 12 | // Automatically artifact temple from being in progression if goal is always open 13 | if (world.getSettings().goal === 'always-open') { 14 | locations.getLocationByKey(PrimeLocation.ARTIFACT_TEMPLE).setExcluded(true); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/electron/models/randomizerSettings.ts: -------------------------------------------------------------------------------- 1 | import { Checkbox, SelectOption } from './option'; 2 | 3 | export interface RandomizerSettingsArgs { 4 | seed?: string; 5 | spoiler?: boolean; 6 | excludeLocations?: object; 7 | tricks?: object; 8 | } 9 | 10 | export class RandomizerSettings { 11 | seed: string; 12 | spoiler: boolean; 13 | excludeLocations: object; 14 | tricks: object; 15 | 16 | constructor() { } 17 | } 18 | -------------------------------------------------------------------------------- /src/electron/models/region.ts: -------------------------------------------------------------------------------- 1 | import { World } from './world'; 2 | import { LocationObject } from './location'; 3 | import { Entrance, EntranceObject } from './entrance'; 4 | import { LocationCollection } from './locationCollection'; 5 | import { ItemCollection } from './itemCollection'; 6 | import { RandomizerSettings } from './randomizerSettings'; 7 | 8 | export interface RegionObject { 9 | name: string; 10 | locations?: LocationObject; 11 | exits?: EntranceObject; 12 | } 13 | 14 | /** 15 | * A region that has a name descriptor, item locations, and elevators (with their prerequisite items) required to access. 16 | */ 17 | export class Region { 18 | private name: string; 19 | private world: World; 20 | private locations: LocationCollection = new LocationCollection([]); 21 | private entrances: Entrance[] = []; 22 | private exits: Entrance[] = []; 23 | 24 | constructor(name: string) { 25 | this.name = name; 26 | } 27 | 28 | /** 29 | * Returns the name of the region. 30 | */ 31 | getName(): string { 32 | return this.name; 33 | } 34 | 35 | /** 36 | * Sets the region's name. 37 | * @param name The region name being set. 38 | */ 39 | setName(name: string) { 40 | this.name = name; 41 | } 42 | 43 | /** 44 | * Returns all item locations belonging to this region. 45 | */ 46 | getLocations(): LocationCollection { 47 | return this.locations; 48 | } 49 | 50 | /** 51 | * Sets the collection of item locations belonging to this region. 52 | * @param locations The location collection being assigned. 53 | */ 54 | setLocations(locations: LocationCollection) { 55 | this.locations = locations; 56 | } 57 | 58 | /** 59 | * Returns all of this region's exits to other regions. 60 | */ 61 | getExits(): Entrance[] { 62 | return this.exits; 63 | } 64 | 65 | getExit(key: string): Entrance { 66 | return this.exits.find(exit => exit.getName() === key); 67 | } 68 | 69 | /** 70 | * Sets this region's available exits to other regions. 71 | * @param exits The collection of exits being assigned. 72 | */ 73 | setExits(exits: Entrance[]) { 74 | this.exits = exits; 75 | } 76 | 77 | /** 78 | * Returns all of this region's entrances from other regions. 79 | */ 80 | getEntrances(): Entrance[] { 81 | return this.entrances; 82 | } 83 | 84 | getEntrance(key: string): Entrance { 85 | return this.entrances.find(exit => exit.getName() === key); 86 | } 87 | 88 | /** 89 | * Sets this region's available entrances from other regions. 90 | * @param entrances The collection of entrances being assigned. 91 | */ 92 | setEntrances(entrances: Entrance[]) { 93 | this.entrances = entrances; 94 | } 95 | 96 | /** 97 | * Returns the parent world that this region belongs to. 98 | */ 99 | getWorld(): World { 100 | return this.world; 101 | } 102 | 103 | /** 104 | * Sets the parent world that this region belongs to. 105 | * @param world The game world being assigned. 106 | */ 107 | setWorld(world: World) { 108 | this.world = world; 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /src/electron/models/regionCollection.ts: -------------------------------------------------------------------------------- 1 | import { Region } from './region'; 2 | import { Collection } from './collection'; 3 | import { MersenneTwister } from '../mersenneTwister'; 4 | import { randomArray } from '../utilities'; 5 | 6 | export class RegionCollection extends Collection { 7 | protected items: Region[]; 8 | 9 | constructor(regions: Region[]) { 10 | super(); 11 | this.items = regions; 12 | } 13 | 14 | getRegionByKey(key: string): Region { 15 | return this.items.find(region => region.getName() === key); 16 | } 17 | 18 | filter(fn): RegionCollection { 19 | return new RegionCollection(this.items.filter(fn)); 20 | } 21 | 22 | shuffle(rng: MersenneTwister): RegionCollection { 23 | return new RegionCollection(randomArray(this.items, this.items.length, rng)); 24 | } 25 | 26 | remove(element: Region): Region { 27 | const firstIndex = this.items.findIndex(item => item.getName() === element.getName()); 28 | 29 | if (firstIndex > -1) { 30 | this.items.splice(firstIndex, 1); 31 | return element; 32 | } 33 | 34 | return null; 35 | } 36 | 37 | has(key: string): boolean { 38 | return this.items.map(region => region.getName()).includes(key); 39 | } 40 | 41 | diff(otherRegions: RegionCollection): RegionCollection { 42 | return new RegionCollection(this.items.filter(item => !otherRegions.has(item.getName()))); 43 | } 44 | 45 | merge(otherRegions: RegionCollection): RegionCollection { 46 | return new RegionCollection(this.items.concat(otherRegions.toArray())); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/electron/models/searchResults.ts: -------------------------------------------------------------------------------- 1 | import { Region } from './region'; 2 | import { Entrance } from './entrance'; 3 | import { ItemCollection } from './itemCollection'; 4 | import { Location } from './location'; 5 | 6 | export class SearchResults { 7 | private visitedRegions: VisitedRegionWrapper[] = []; 8 | private items: ItemCollection = new ItemCollection([]); 9 | 10 | constructor(args?: SearchArgs) { 11 | if (args) { 12 | Object.assign(this, args); 13 | } 14 | } 15 | 16 | getLocations(): Location[] { 17 | let locations: Location[] = []; 18 | 19 | for (let visitedRegion of this.visitedRegions) { 20 | const region = visitedRegion.region; 21 | if (region.getLocations().size()) { 22 | locations = locations.concat(region.getLocations().toArray()); 23 | } 24 | } 25 | 26 | return locations; 27 | } 28 | 29 | getVisitedRegions(): VisitedRegionWrapper[] { 30 | return this.visitedRegions; 31 | } 32 | 33 | setVisitedRegions(visited: VisitedRegionWrapper[]): void { 34 | this.visitedRegions = visited; 35 | } 36 | 37 | getVisitedRegion(region: Region): VisitedRegionWrapper { 38 | return this.visitedRegions.find(visitedItem => visitedItem.region.getName() === region.getName()); 39 | } 40 | 41 | getFirstVisitedRegion(): VisitedRegionWrapper { 42 | return this.visitedRegions.length ? this.visitedRegions[0] : null; 43 | } 44 | 45 | getLastVisitedRegion(): VisitedRegionWrapper { 46 | return this.visitedRegions.length ? this.visitedRegions[this.visitedRegions.length - 1] : null; 47 | } 48 | 49 | getItems(): ItemCollection { 50 | return this.items; 51 | } 52 | 53 | setItems(items: ItemCollection): void { 54 | this.items = items; 55 | } 56 | } 57 | 58 | export interface SearchArgs { 59 | visitedRegions: VisitedRegionWrapper[]; 60 | items: ItemCollection; 61 | } 62 | 63 | export interface VisitedRegionWrapper { 64 | region: Region; 65 | entryPoint: Entrance; 66 | } 67 | 68 | -------------------------------------------------------------------------------- /src/electron/models/settingsFlags.ts: -------------------------------------------------------------------------------- 1 | import * as bigInt from 'big-integer'; 2 | 3 | export interface SettingsFlagsArgs { 4 | [key: string]: boolean; 5 | } 6 | 7 | export abstract class SettingsFlags { 8 | constructor() { } 9 | 10 | getSettingsKeys() { 11 | return Object.keys(this).filter(key => typeof this[key] === 'boolean'); 12 | } 13 | 14 | abstract setSettings(args: SettingsFlagsArgs): void; 15 | 16 | toSettingsString(): string { 17 | let bits = ''; 18 | for (let key of this.getSettingsKeys()) { 19 | bits += this[key] ? '1' : '0'; 20 | } 21 | 22 | return bigInt(bits, 2).toString(36).toUpperCase(); 23 | } 24 | 25 | toArray(): string[] { 26 | const array = []; 27 | 28 | for (const key of this.getSettingsKeys()) { 29 | if (this[key] === true) { 30 | array.push(key) 31 | } 32 | } 33 | 34 | return array; 35 | } 36 | } 37 | 38 | 39 | -------------------------------------------------------------------------------- /src/electron/randomprime.ts: -------------------------------------------------------------------------------- 1 | // Defines randomprime interface 2 | export interface RandomPrime { 3 | patchRandomizedGame: (json: string, callback: any) => void; 4 | } 5 | 6 | // Use __non_webpack_require__ to resolve randomprime addon at runtime 7 | // The path is intentionally because this will ultimately be called in the compiled main.js in the app root. 8 | const randomprime: RandomPrime = __non_webpack_require__('./build/Release/randomprime'); 9 | export default randomprime; 10 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": false, 3 | "compilerOptions": { 4 | "allowJs": true, 5 | "allowSyntheticDefaultImports": true, 6 | "lib": [ 7 | "es2018" 8 | ], 9 | "moduleResolution": "node", 10 | "newLine": "LF", 11 | "pretty": true, 12 | "resolveJsonModule": true, 13 | "skipLibCheck": true, 14 | "sourceMap": true, 15 | "target": "es5", 16 | "types": [ 17 | "webpack-env" 18 | ] 19 | }, 20 | "include": [ 21 | "src/electron/**/*.ts" 22 | ] 23 | } 24 | -------------------------------------------------------------------------------- /tsconfig.test.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "types": [ 5 | "webpack-cli", 6 | "jest" 7 | ] 8 | }, 9 | "include": [ 10 | "src/electron/**/*.ts", 11 | "test/**/*.test.ts" 12 | ] 13 | } -------------------------------------------------------------------------------- /webpack.development.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | module.exports = { 4 | mode: 'development', 5 | target: 'electron-main', 6 | entry: './src/electron/main.ts', 7 | devtool: 'source-map', 8 | output: { 9 | path: path.resolve(__dirname), 10 | filename: 'main.js', 11 | }, 12 | resolve: { 13 | extensions: ['.ts', '.js'] 14 | }, 15 | module: { 16 | rules: [ 17 | // Handle all .ts or .tsx files with ts-loader 18 | { 19 | test: /\.tsx?$/, 20 | use: 'ts-loader', 21 | exclude: /node_modules/ 22 | }, 23 | ] 24 | }, 25 | node: { 26 | __dirname: false 27 | } 28 | }; 29 | -------------------------------------------------------------------------------- /webpack.production.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const TerserPlugin = require('terser-webpack-plugin'); 3 | 4 | module.exports = { 5 | mode: 'production', 6 | target: 'electron-main', 7 | entry: './src/electron/main.ts', 8 | optimization: { 9 | minimize: true, 10 | minimizer: [new TerserPlugin()], 11 | }, 12 | output: { 13 | path: path.resolve(__dirname), 14 | filename: 'main.js', 15 | }, 16 | resolve: { 17 | extensions: ['.ts', '.js'] 18 | }, 19 | module: { 20 | rules: [ 21 | // Handle all .ts or .tsx files with ts-loader 22 | { 23 | test: /\.tsx?$/, 24 | use: 'ts-loader', 25 | exclude: /node_modules/ 26 | } 27 | ] 28 | }, 29 | node: { 30 | __dirname: false 31 | } 32 | }; 33 | --------------------------------------------------------------------------------