├── .nvmrc ├── src ├── app │ ├── app.component.scss │ ├── app.component.html │ ├── pages │ │ ├── character-page │ │ │ ├── character-page.component.scss │ │ │ ├── character-moves │ │ │ │ ├── motions │ │ │ │ │ ├── motions.component.scss │ │ │ │ │ ├── motions.component.html │ │ │ │ │ └── motions.component.ts │ │ │ │ ├── character-moves.component.ts │ │ │ │ ├── character-moves.component.scss │ │ │ │ └── character-moves.component.html │ │ │ ├── character-page.component.html │ │ │ ├── character-page.resolver.ts │ │ │ ├── character-page.guard.ts │ │ │ ├── character-details │ │ │ │ ├── character-details.component.scss │ │ │ │ ├── character-details.component.ts │ │ │ │ └── character-details.component.html │ │ │ └── character-page.component.ts │ │ ├── characters-list-page │ │ │ ├── characters-list-page.component.scss │ │ │ ├── characters-list-page.component.html │ │ │ └── characters-list-page.component.ts │ │ └── country-page │ │ │ ├── country-page.resolver.ts │ │ │ ├── country-page.guard.ts │ │ │ ├── country-page.component.html │ │ │ ├── country-page.component.scss │ │ │ └── country-page.component.ts │ ├── shared │ │ ├── features │ │ │ └── update-app │ │ │ │ ├── update-app-indicator │ │ │ │ ├── update-app-indicator.component.scss │ │ │ │ ├── update-app-indicator.component.html │ │ │ │ └── update-app-indicator.component.ts │ │ │ │ ├── update-app-dialog │ │ │ │ ├── update-app-dialog.component.scss │ │ │ │ ├── update-app-dialog.component.ts │ │ │ │ └── update-app-dialog.component.html │ │ │ │ ├── check-for-update.service.ts │ │ │ │ └── prompt-update.service.ts │ │ └── offline │ │ │ └── connection.service.ts │ ├── layout │ │ └── nav │ │ │ ├── controls │ │ │ ├── controls.component.scss │ │ │ ├── controls.component.ts │ │ │ └── controls.component.html │ │ │ ├── nav.component.scss │ │ │ ├── nav.animations.ts │ │ │ ├── nav.component.spec.ts │ │ │ ├── nav.component.ts │ │ │ └── nav.component.html │ ├── app.component.ts │ ├── app-routing.module.ts │ ├── app.module.ts │ └── data.ts ├── favicon.ico ├── environments │ ├── environment.prod.ts │ └── environment.ts ├── assets │ ├── logo.png │ ├── motions │ │ ├── C.png │ │ ├── J.png │ │ ├── J↗.png │ │ ├── K.png │ │ ├── K1.png │ │ ├── K2.png │ │ ├── K3.png │ │ ├── M←.png │ │ ├── M↑.png │ │ ├── M→.png │ │ ├── M↓.png │ │ ├── M↘.png │ │ ├── P.png │ │ ├── P1.png │ │ ├── P2.png │ │ ├── P3.png │ │ ├── Kx2.png │ │ ├── Kx3.png │ │ ├── MC←→.png │ │ ├── MC↓↑.png │ │ ├── MC↙→.png │ │ ├── MC↙↘.png │ │ ├── M←→.png │ │ ├── M←↓↙.png │ │ ├── M→↓↘.png │ │ ├── M↓↘→.png │ │ ├── M↓↙←.png │ │ ├── M↙↗.png │ │ ├── Px2.png │ │ ├── Px3.png │ │ ├── M←↙↓↘→.png │ │ ├── M→↘↓↙←.png │ │ ├── M←↙↓↘→↗↑.png │ │ └── M→↓↘orM←↓↙.png │ ├── icons │ │ ├── icon-72x72.png │ │ ├── icon-96x96.png │ │ ├── icon-128x128.png │ │ ├── icon-144x144.png │ │ ├── icon-192x192.png │ │ └── icon-512x512.png │ ├── characters │ │ ├── abel_thumbnail.png │ │ ├── adon_thumbnail.png │ │ ├── cody_thumbnail.png │ │ ├── dan_thumbnail.png │ │ ├── gen_thumbnail.png │ │ ├── guy_thumbnail.png │ │ ├── juri_thumbnail.png │ │ ├── ken_thumbnail.png │ │ ├── oni_thumbnail.png │ │ ├── rose_thumbnail.png │ │ ├── ryu_thumbnail.png │ │ ├── seth_thumbnail.png │ │ ├── vega_thumbnail.png │ │ ├── yang_thumbnail.png │ │ ├── yun_thumbnail.png │ │ ├── akuma_thumbnail.png │ │ ├── balrog_thumbnail.png │ │ ├── blanka_thumbnail.png │ │ ├── cammy_thumbnail.png │ │ ├── dudley_thumbnail.png │ │ ├── gouken_thumbnail.png │ │ ├── guile_thumbnail.png │ │ ├── hakan_thumbnail.png │ │ ├── ibuki_thumbnail.png │ │ ├── makoto_thumbnail.png │ │ ├── rufus_thumbnail.png │ │ ├── sagat_thumbnail.png │ │ ├── sakura_thumbnail.png │ │ ├── t-hawk_thumbnail.png │ │ ├── c-viper_thumbnail.png │ │ ├── chun-li_thumbnail.png │ │ ├── dee-jay_thumbnail.png │ │ ├── dhalsim_thumbnail.png │ │ ├── e-honda_thumbnail.png │ │ ├── el-fuerte_thumbnail.png │ │ ├── evil-ryu_thumbnail.png │ │ ├── fei-long_thumbnail.png │ │ ├── m-bison_thumbnail.png │ │ └── zangief_thumbnail.png │ ├── countries │ │ ├── Japan.svg │ │ ├── France.svg │ │ ├── Thailand.svg │ │ ├── Italy.svg │ │ ├── Jamaica.svg │ │ ├── Turkey.svg │ │ ├── China.svg │ │ ├── UK.svg │ │ ├── South_Korea.svg │ │ ├── India.svg │ │ ├── USA.svg │ │ ├── Hong_Kong.svg │ │ ├── None.svg │ │ ├── USSR.svg │ │ └── Brazil.svg │ └── i18n │ │ ├── en.json │ │ └── fr.json ├── styles.scss ├── main.ts ├── manifest.webmanifest └── index.html ├── setup-jest.ts ├── .github ├── preview.png └── workflows │ ├── visual-review.yml │ └── build-deploy.yml ├── cypress ├── fixtures │ └── example.json ├── tsconfig.json ├── support │ ├── e2e.ts │ └── commands.ts └── e2e │ └── navigation.cy.ts ├── .prettierignore ├── .prettierrc ├── cypress.config.ts ├── tsconfig.spec.json ├── tsconfig.app.json ├── .editorconfig ├── ngsw-config.json ├── README.md ├── .gitignore ├── .eslintrc.json ├── tsconfig.json ├── server-config └── .htaccess ├── package.json ├── sitemap-urls.txt └── angular.json /.nvmrc: -------------------------------------------------------------------------------- 1 | 18 2 | -------------------------------------------------------------------------------- /src/app/app.component.scss: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/app/app.component.html: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /setup-jest.ts: -------------------------------------------------------------------------------- 1 | import 'jest-preset-angular/setup-jest'; 2 | -------------------------------------------------------------------------------- /src/app/pages/character-page/character-page.component.scss: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ben-barbier/street-fighter-moves/HEAD/src/favicon.ico -------------------------------------------------------------------------------- /src/environments/environment.prod.ts: -------------------------------------------------------------------------------- 1 | export const environment = { 2 | production: true, 3 | }; 4 | -------------------------------------------------------------------------------- /.github/preview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ben-barbier/street-fighter-moves/HEAD/.github/preview.png -------------------------------------------------------------------------------- /src/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ben-barbier/street-fighter-moves/HEAD/src/assets/logo.png -------------------------------------------------------------------------------- /src/assets/motions/C.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ben-barbier/street-fighter-moves/HEAD/src/assets/motions/C.png -------------------------------------------------------------------------------- /src/assets/motions/J.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ben-barbier/street-fighter-moves/HEAD/src/assets/motions/J.png -------------------------------------------------------------------------------- /src/assets/motions/J↗.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ben-barbier/street-fighter-moves/HEAD/src/assets/motions/J↗.png -------------------------------------------------------------------------------- /src/assets/motions/K.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ben-barbier/street-fighter-moves/HEAD/src/assets/motions/K.png -------------------------------------------------------------------------------- /src/assets/motions/K1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ben-barbier/street-fighter-moves/HEAD/src/assets/motions/K1.png -------------------------------------------------------------------------------- /src/assets/motions/K2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ben-barbier/street-fighter-moves/HEAD/src/assets/motions/K2.png -------------------------------------------------------------------------------- /src/assets/motions/K3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ben-barbier/street-fighter-moves/HEAD/src/assets/motions/K3.png -------------------------------------------------------------------------------- /src/assets/motions/M←.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ben-barbier/street-fighter-moves/HEAD/src/assets/motions/M←.png -------------------------------------------------------------------------------- /src/assets/motions/M↑.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ben-barbier/street-fighter-moves/HEAD/src/assets/motions/M↑.png -------------------------------------------------------------------------------- /src/assets/motions/M→.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ben-barbier/street-fighter-moves/HEAD/src/assets/motions/M→.png -------------------------------------------------------------------------------- /src/assets/motions/M↓.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ben-barbier/street-fighter-moves/HEAD/src/assets/motions/M↓.png -------------------------------------------------------------------------------- /src/assets/motions/M↘.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ben-barbier/street-fighter-moves/HEAD/src/assets/motions/M↘.png -------------------------------------------------------------------------------- /src/assets/motions/P.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ben-barbier/street-fighter-moves/HEAD/src/assets/motions/P.png -------------------------------------------------------------------------------- /src/assets/motions/P1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ben-barbier/street-fighter-moves/HEAD/src/assets/motions/P1.png -------------------------------------------------------------------------------- /src/assets/motions/P2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ben-barbier/street-fighter-moves/HEAD/src/assets/motions/P2.png -------------------------------------------------------------------------------- /src/assets/motions/P3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ben-barbier/street-fighter-moves/HEAD/src/assets/motions/P3.png -------------------------------------------------------------------------------- /src/assets/motions/Kx2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ben-barbier/street-fighter-moves/HEAD/src/assets/motions/Kx2.png -------------------------------------------------------------------------------- /src/assets/motions/Kx3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ben-barbier/street-fighter-moves/HEAD/src/assets/motions/Kx3.png -------------------------------------------------------------------------------- /src/assets/motions/MC←→.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ben-barbier/street-fighter-moves/HEAD/src/assets/motions/MC←→.png -------------------------------------------------------------------------------- /src/assets/motions/MC↓↑.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ben-barbier/street-fighter-moves/HEAD/src/assets/motions/MC↓↑.png -------------------------------------------------------------------------------- /src/assets/motions/MC↙→.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ben-barbier/street-fighter-moves/HEAD/src/assets/motions/MC↙→.png -------------------------------------------------------------------------------- /src/assets/motions/MC↙↘.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ben-barbier/street-fighter-moves/HEAD/src/assets/motions/MC↙↘.png -------------------------------------------------------------------------------- /src/assets/motions/M←→.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ben-barbier/street-fighter-moves/HEAD/src/assets/motions/M←→.png -------------------------------------------------------------------------------- /src/assets/motions/M←↓↙.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ben-barbier/street-fighter-moves/HEAD/src/assets/motions/M←↓↙.png -------------------------------------------------------------------------------- /src/assets/motions/M→↓↘.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ben-barbier/street-fighter-moves/HEAD/src/assets/motions/M→↓↘.png -------------------------------------------------------------------------------- /src/assets/motions/M↓↘→.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ben-barbier/street-fighter-moves/HEAD/src/assets/motions/M↓↘→.png -------------------------------------------------------------------------------- /src/assets/motions/M↓↙←.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ben-barbier/street-fighter-moves/HEAD/src/assets/motions/M↓↙←.png -------------------------------------------------------------------------------- /src/assets/motions/M↙↗.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ben-barbier/street-fighter-moves/HEAD/src/assets/motions/M↙↗.png -------------------------------------------------------------------------------- /src/assets/motions/Px2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ben-barbier/street-fighter-moves/HEAD/src/assets/motions/Px2.png -------------------------------------------------------------------------------- /src/assets/motions/Px3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ben-barbier/street-fighter-moves/HEAD/src/assets/motions/Px3.png -------------------------------------------------------------------------------- /cypress/fixtures/example.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Using fixtures to represent data", 3 | "email": "hello@cypress.io" 4 | } 5 | -------------------------------------------------------------------------------- /src/assets/motions/M←↙↓↘→.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ben-barbier/street-fighter-moves/HEAD/src/assets/motions/M←↙↓↘→.png -------------------------------------------------------------------------------- /src/assets/motions/M→↘↓↙←.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ben-barbier/street-fighter-moves/HEAD/src/assets/motions/M→↘↓↙←.png -------------------------------------------------------------------------------- /src/app/shared/features/update-app/update-app-indicator/update-app-indicator.component.scss: -------------------------------------------------------------------------------- 1 | :host { 2 | cursor: pointer; 3 | } 4 | -------------------------------------------------------------------------------- /src/assets/icons/icon-72x72.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ben-barbier/street-fighter-moves/HEAD/src/assets/icons/icon-72x72.png -------------------------------------------------------------------------------- /src/assets/icons/icon-96x96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ben-barbier/street-fighter-moves/HEAD/src/assets/icons/icon-96x96.png -------------------------------------------------------------------------------- /src/assets/motions/M←↙↓↘→↗↑.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ben-barbier/street-fighter-moves/HEAD/src/assets/motions/M←↙↓↘→↗↑.png -------------------------------------------------------------------------------- /src/assets/icons/icon-128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ben-barbier/street-fighter-moves/HEAD/src/assets/icons/icon-128x128.png -------------------------------------------------------------------------------- /src/assets/icons/icon-144x144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ben-barbier/street-fighter-moves/HEAD/src/assets/icons/icon-144x144.png -------------------------------------------------------------------------------- /src/assets/icons/icon-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ben-barbier/street-fighter-moves/HEAD/src/assets/icons/icon-192x192.png -------------------------------------------------------------------------------- /src/assets/icons/icon-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ben-barbier/street-fighter-moves/HEAD/src/assets/icons/icon-512x512.png -------------------------------------------------------------------------------- /src/assets/motions/M→↓↘orM←↓↙.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ben-barbier/street-fighter-moves/HEAD/src/assets/motions/M→↓↘orM←↓↙.png -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | /* 2 | !/src 3 | !/cypress 4 | 5 | *.ico 6 | *.png 7 | *.svg 8 | 9 | # System Files 10 | .DS_Store 11 | Thumbs.db 12 | -------------------------------------------------------------------------------- /src/assets/characters/abel_thumbnail.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ben-barbier/street-fighter-moves/HEAD/src/assets/characters/abel_thumbnail.png -------------------------------------------------------------------------------- /src/assets/characters/adon_thumbnail.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ben-barbier/street-fighter-moves/HEAD/src/assets/characters/adon_thumbnail.png -------------------------------------------------------------------------------- /src/assets/characters/cody_thumbnail.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ben-barbier/street-fighter-moves/HEAD/src/assets/characters/cody_thumbnail.png -------------------------------------------------------------------------------- /src/assets/characters/dan_thumbnail.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ben-barbier/street-fighter-moves/HEAD/src/assets/characters/dan_thumbnail.png -------------------------------------------------------------------------------- /src/assets/characters/gen_thumbnail.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ben-barbier/street-fighter-moves/HEAD/src/assets/characters/gen_thumbnail.png -------------------------------------------------------------------------------- /src/assets/characters/guy_thumbnail.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ben-barbier/street-fighter-moves/HEAD/src/assets/characters/guy_thumbnail.png -------------------------------------------------------------------------------- /src/assets/characters/juri_thumbnail.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ben-barbier/street-fighter-moves/HEAD/src/assets/characters/juri_thumbnail.png -------------------------------------------------------------------------------- /src/assets/characters/ken_thumbnail.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ben-barbier/street-fighter-moves/HEAD/src/assets/characters/ken_thumbnail.png -------------------------------------------------------------------------------- /src/assets/characters/oni_thumbnail.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ben-barbier/street-fighter-moves/HEAD/src/assets/characters/oni_thumbnail.png -------------------------------------------------------------------------------- /src/assets/characters/rose_thumbnail.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ben-barbier/street-fighter-moves/HEAD/src/assets/characters/rose_thumbnail.png -------------------------------------------------------------------------------- /src/assets/characters/ryu_thumbnail.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ben-barbier/street-fighter-moves/HEAD/src/assets/characters/ryu_thumbnail.png -------------------------------------------------------------------------------- /src/assets/characters/seth_thumbnail.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ben-barbier/street-fighter-moves/HEAD/src/assets/characters/seth_thumbnail.png -------------------------------------------------------------------------------- /src/assets/characters/vega_thumbnail.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ben-barbier/street-fighter-moves/HEAD/src/assets/characters/vega_thumbnail.png -------------------------------------------------------------------------------- /src/assets/characters/yang_thumbnail.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ben-barbier/street-fighter-moves/HEAD/src/assets/characters/yang_thumbnail.png -------------------------------------------------------------------------------- /src/assets/characters/yun_thumbnail.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ben-barbier/street-fighter-moves/HEAD/src/assets/characters/yun_thumbnail.png -------------------------------------------------------------------------------- /src/app/shared/features/update-app/update-app-dialog/update-app-dialog.component.scss: -------------------------------------------------------------------------------- 1 | [mat-dialog-actions] { 2 | justify-content: flex-end; 3 | } 4 | -------------------------------------------------------------------------------- /src/assets/characters/akuma_thumbnail.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ben-barbier/street-fighter-moves/HEAD/src/assets/characters/akuma_thumbnail.png -------------------------------------------------------------------------------- /src/assets/characters/balrog_thumbnail.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ben-barbier/street-fighter-moves/HEAD/src/assets/characters/balrog_thumbnail.png -------------------------------------------------------------------------------- /src/assets/characters/blanka_thumbnail.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ben-barbier/street-fighter-moves/HEAD/src/assets/characters/blanka_thumbnail.png -------------------------------------------------------------------------------- /src/assets/characters/cammy_thumbnail.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ben-barbier/street-fighter-moves/HEAD/src/assets/characters/cammy_thumbnail.png -------------------------------------------------------------------------------- /src/assets/characters/dudley_thumbnail.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ben-barbier/street-fighter-moves/HEAD/src/assets/characters/dudley_thumbnail.png -------------------------------------------------------------------------------- /src/assets/characters/gouken_thumbnail.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ben-barbier/street-fighter-moves/HEAD/src/assets/characters/gouken_thumbnail.png -------------------------------------------------------------------------------- /src/assets/characters/guile_thumbnail.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ben-barbier/street-fighter-moves/HEAD/src/assets/characters/guile_thumbnail.png -------------------------------------------------------------------------------- /src/assets/characters/hakan_thumbnail.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ben-barbier/street-fighter-moves/HEAD/src/assets/characters/hakan_thumbnail.png -------------------------------------------------------------------------------- /src/assets/characters/ibuki_thumbnail.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ben-barbier/street-fighter-moves/HEAD/src/assets/characters/ibuki_thumbnail.png -------------------------------------------------------------------------------- /src/assets/characters/makoto_thumbnail.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ben-barbier/street-fighter-moves/HEAD/src/assets/characters/makoto_thumbnail.png -------------------------------------------------------------------------------- /src/assets/characters/rufus_thumbnail.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ben-barbier/street-fighter-moves/HEAD/src/assets/characters/rufus_thumbnail.png -------------------------------------------------------------------------------- /src/assets/characters/sagat_thumbnail.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ben-barbier/street-fighter-moves/HEAD/src/assets/characters/sagat_thumbnail.png -------------------------------------------------------------------------------- /src/assets/characters/sakura_thumbnail.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ben-barbier/street-fighter-moves/HEAD/src/assets/characters/sakura_thumbnail.png -------------------------------------------------------------------------------- /src/assets/characters/t-hawk_thumbnail.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ben-barbier/street-fighter-moves/HEAD/src/assets/characters/t-hawk_thumbnail.png -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "printWidth": 140, 4 | "trailingComma": "all", 5 | "arrowParens": "avoid", 6 | "tabWidth": 2 7 | } 8 | -------------------------------------------------------------------------------- /src/assets/characters/c-viper_thumbnail.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ben-barbier/street-fighter-moves/HEAD/src/assets/characters/c-viper_thumbnail.png -------------------------------------------------------------------------------- /src/assets/characters/chun-li_thumbnail.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ben-barbier/street-fighter-moves/HEAD/src/assets/characters/chun-li_thumbnail.png -------------------------------------------------------------------------------- /src/assets/characters/dee-jay_thumbnail.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ben-barbier/street-fighter-moves/HEAD/src/assets/characters/dee-jay_thumbnail.png -------------------------------------------------------------------------------- /src/assets/characters/dhalsim_thumbnail.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ben-barbier/street-fighter-moves/HEAD/src/assets/characters/dhalsim_thumbnail.png -------------------------------------------------------------------------------- /src/assets/characters/e-honda_thumbnail.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ben-barbier/street-fighter-moves/HEAD/src/assets/characters/e-honda_thumbnail.png -------------------------------------------------------------------------------- /src/assets/characters/el-fuerte_thumbnail.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ben-barbier/street-fighter-moves/HEAD/src/assets/characters/el-fuerte_thumbnail.png -------------------------------------------------------------------------------- /src/assets/characters/evil-ryu_thumbnail.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ben-barbier/street-fighter-moves/HEAD/src/assets/characters/evil-ryu_thumbnail.png -------------------------------------------------------------------------------- /src/assets/characters/fei-long_thumbnail.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ben-barbier/street-fighter-moves/HEAD/src/assets/characters/fei-long_thumbnail.png -------------------------------------------------------------------------------- /src/assets/characters/m-bison_thumbnail.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ben-barbier/street-fighter-moves/HEAD/src/assets/characters/m-bison_thumbnail.png -------------------------------------------------------------------------------- /src/assets/characters/zangief_thumbnail.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ben-barbier/street-fighter-moves/HEAD/src/assets/characters/zangief_thumbnail.png -------------------------------------------------------------------------------- /cypress.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'cypress' 2 | 3 | export default defineConfig({ 4 | e2e: { 5 | 'baseUrl': 'http://localhost:4200', 6 | }, 7 | }) 8 | -------------------------------------------------------------------------------- /src/app/layout/nav/controls/controls.component.scss: -------------------------------------------------------------------------------- 1 | table { 2 | border-spacing: 12px 2px; 3 | } 4 | 5 | [mat-dialog-actions] { 6 | justify-content: center; 7 | } 8 | -------------------------------------------------------------------------------- /cypress/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "include": ["**/*.ts"], 4 | "compilerOptions": { 5 | "sourceMap": false, 6 | "types": ["cypress", "@percy/cypress"] 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /src/app/shared/features/update-app/update-app-indicator/update-app-indicator.component.html: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /src/assets/countries/Japan.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/app/pages/character-page/character-moves/motions/motions.component.scss: -------------------------------------------------------------------------------- 1 | :host { 2 | display: flex; 3 | align-items: center; 4 | 5 | img.move { 6 | margin-right: 3px; 7 | } 8 | 9 | span { 10 | font-size: 46px; 11 | margin-right: 6px; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/assets/countries/France.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/assets/countries/Thailand.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "extends": "./tsconfig.json", 4 | "compilerOptions": { 5 | "outDir": "./out-tsc/spec", 6 | "types": ["jest"] 7 | }, 8 | "include": [ 9 | "src/**/*.spec.ts", 10 | "src/**/*.d.ts" 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /src/assets/countries/Italy.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /src/app/pages/characters-list-page/characters-list-page.component.scss: -------------------------------------------------------------------------------- 1 | ul { 2 | display: flex; 3 | flex-wrap: wrap; 4 | list-style-type: none; 5 | justify-content: center; 6 | padding: 0; 7 | margin: 8px 0; 8 | 9 | li { 10 | padding: 1px 2px; 11 | 12 | img { 13 | width: 72px; 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/styles.scss: -------------------------------------------------------------------------------- 1 | /* You can add global styles to this file, and also import other style files */ 2 | html, 3 | body { 4 | height: 100%; 5 | } 6 | 7 | body { 8 | margin: 0; 9 | font-family: Roboto, 'Helvetica Neue', sans-serif; 10 | } 11 | 12 | button:enabled, 13 | input[type='file']:enabled { 14 | cursor: pointer; 15 | } 16 | -------------------------------------------------------------------------------- /src/app/pages/characters-list-page/characters-list-page.component.html: -------------------------------------------------------------------------------- 1 | 8 | -------------------------------------------------------------------------------- /tsconfig.app.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "extends": "./tsconfig.json", 4 | "compilerOptions": { 5 | "outDir": "./out-tsc/app", 6 | "types": [] 7 | }, 8 | "files": [ 9 | "src/main.ts" 10 | ], 11 | "include": [ 12 | "src/**/*.d.ts" 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /src/app/pages/character-page/character-moves/motions/motions.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | {{ motion }} 5 | 6 | 7 | -------------------------------------------------------------------------------- /.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 | [*.ts] 12 | quote_type = single 13 | 14 | [*.md] 15 | max_line_length = off 16 | trim_trailing_whitespace = false 17 | -------------------------------------------------------------------------------- /src/app/pages/character-page/character-page.component.html: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /src/assets/countries/Jamaica.svg: -------------------------------------------------------------------------------- 1 | 2 | Flag of Jamaica 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import { enableProdMode } from '@angular/core'; 2 | import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; 3 | import { AppModule } from './app/app.module'; 4 | import { environment } from './environments/environment'; 5 | 6 | if (environment.production) { 7 | enableProdMode(); 8 | } 9 | 10 | platformBrowserDynamic() 11 | .bootstrapModule(AppModule) 12 | .catch(err => console.error(err)); 13 | -------------------------------------------------------------------------------- /src/assets/countries/Turkey.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 8 | 9 | -------------------------------------------------------------------------------- /src/app/pages/country-page/country-page.resolver.ts: -------------------------------------------------------------------------------- 1 | import { ActivatedRouteSnapshot, ResolveFn, RouterStateSnapshot } from '@angular/router'; 2 | import { Observable, of } from 'rxjs'; 3 | import { Character, data } from '../../data'; 4 | 5 | export const countryPageResolver: ResolveFn = ( 6 | route: ActivatedRouteSnapshot, 7 | state: RouterStateSnapshot, 8 | ): Observable => { 9 | return of(data[0].characters.filter(c => c.country === route.params['country'])); 10 | }; 11 | -------------------------------------------------------------------------------- /src/app/pages/character-page/character-page.resolver.ts: -------------------------------------------------------------------------------- 1 | import { ActivatedRouteSnapshot, ResolveFn, RouterStateSnapshot } from '@angular/router'; 2 | import { Observable, of } from 'rxjs'; 3 | import { Character, data } from '../../data'; 4 | 5 | export const characterPageResolver: ResolveFn = ( 6 | route: ActivatedRouteSnapshot, 7 | state: RouterStateSnapshot, 8 | ): Observable => { 9 | return of(data[0].characters.find(c => c.id === route.params['characterId']) as Character); 10 | }; 11 | -------------------------------------------------------------------------------- /src/app/shared/offline/connection.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { BehaviorSubject } from 'rxjs'; 3 | 4 | @Injectable({ 5 | providedIn: 'root', 6 | }) 7 | export class ConnectionService { 8 | public isOnline$ = new BehaviorSubject(navigator.onLine); 9 | 10 | constructor() { 11 | window.addEventListener('offline', () => this.isOnline$.next(navigator.onLine)); 12 | window.addEventListener('online', () => this.isOnline$.next(navigator.onLine)); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/app/pages/country-page/country-page.guard.ts: -------------------------------------------------------------------------------- 1 | import { inject } from '@angular/core'; 2 | import { ActivatedRouteSnapshot, CanActivateFn, Router, RouterStateSnapshot, UrlTree } from '@angular/router'; 3 | import { data } from '../../data'; 4 | 5 | export const canActivateCountry: CanActivateFn = (route: ActivatedRouteSnapshot, state: RouterStateSnapshot): boolean | UrlTree => { 6 | const countryExists = data[0].characters.some(c => c.country === route.params['country']); 7 | return countryExists || inject(Router).createUrlTree(['/sf4']); 8 | }; 9 | -------------------------------------------------------------------------------- /src/app/pages/character-page/character-page.guard.ts: -------------------------------------------------------------------------------- 1 | import { inject } from '@angular/core'; 2 | import { ActivatedRouteSnapshot, CanActivateFn, Router, RouterStateSnapshot, UrlTree } from '@angular/router'; 3 | import { data } from '../../data'; 4 | 5 | export const canActivateCharacter: CanActivateFn = (route: ActivatedRouteSnapshot, state: RouterStateSnapshot): boolean | UrlTree => { 6 | const characterExists = data[0].characters.some(c => c.id === route.params['characterId']); 7 | return characterExists || inject(Router).createUrlTree(['/sf4']); 8 | }; 9 | -------------------------------------------------------------------------------- /src/app/pages/character-page/character-details/character-details.component.scss: -------------------------------------------------------------------------------- 1 | mat-card { 2 | display: flex; 3 | flex-direction: row; 4 | margin: 12px; 5 | padding: 16px; 6 | 7 | img.character-thumbnail { 8 | margin-right: 12px; 9 | } 10 | 11 | .character-infos { 12 | width: 100%; 13 | 14 | .title { 15 | display: inline-flex; 16 | margin: 0.5em 0; 17 | 18 | h1 { 19 | margin: 0; 20 | } 21 | 22 | .flag { 23 | margin-left: 1em; 24 | 25 | img { 26 | max-height: 2em; 27 | } 28 | } 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/app/layout/nav/controls/controls.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | import { MatButtonModule } from '@angular/material/button'; 3 | import { MatDialogModule } from '@angular/material/dialog'; 4 | import { MatIconModule } from '@angular/material/icon'; 5 | import { TranslateModule } from '@ngx-translate/core'; 6 | 7 | @Component({ 8 | selector: 'app-controls', 9 | templateUrl: './controls.component.html', 10 | styleUrls: ['./controls.component.scss'], 11 | standalone: true, 12 | imports: [MatDialogModule, TranslateModule, MatButtonModule, MatIconModule], 13 | }) 14 | export class ControlsComponent {} 15 | -------------------------------------------------------------------------------- /src/app/pages/character-page/character-moves/motions/motions.component.ts: -------------------------------------------------------------------------------- 1 | import { CommonModule } from '@angular/common'; 2 | import { ChangeDetectionStrategy, Component, Input } from '@angular/core'; 3 | 4 | @Component({ 5 | selector: 'app-motions', 6 | templateUrl: './motions.component.html', 7 | styleUrls: ['./motions.component.scss'], 8 | changeDetection: ChangeDetectionStrategy.OnPush, 9 | standalone: true, 10 | imports: [CommonModule], 11 | }) 12 | export class MotionsComponent { 13 | @Input() motions: string[] = []; 14 | 15 | public displayImage(motion: string): boolean { 16 | return ['[', ']'].every(noImage => noImage !== motion); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/app/shared/features/update-app/update-app-dialog/update-app-dialog.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | import { MatButtonModule } from '@angular/material/button'; 3 | import { MatDialogModule } from '@angular/material/dialog'; 4 | import { MatIconModule } from '@angular/material/icon'; 5 | import { TranslateModule } from '@ngx-translate/core'; 6 | 7 | @Component({ 8 | selector: 'app-update-app-dialog', 9 | templateUrl: './update-app-dialog.component.html', 10 | styleUrls: ['./update-app-dialog.component.scss'], 11 | standalone: true, 12 | imports: [MatIconModule, MatDialogModule, TranslateModule, MatButtonModule], 13 | }) 14 | export class UpdateAppDialogComponent {} 15 | -------------------------------------------------------------------------------- /src/app/pages/character-page/character-moves/character-moves.component.ts: -------------------------------------------------------------------------------- 1 | import { CommonModule } from '@angular/common'; 2 | import { Component, Input } from '@angular/core'; 3 | import { MatIconModule } from '@angular/material/icon'; 4 | import { Character } from '../../../data'; 5 | import { MotionsComponent } from './motions/motions.component'; 6 | 7 | @Component({ 8 | selector: 'app-character-moves', 9 | templateUrl: './character-moves.component.html', 10 | styleUrls: ['./character-moves.component.scss'], 11 | standalone: true, 12 | imports: [CommonModule, MotionsComponent, MatIconModule], 13 | }) 14 | export class CharacterMovesComponent { 15 | @Input() character: Character | undefined; 16 | } 17 | -------------------------------------------------------------------------------- /src/app/shared/features/update-app/update-app-dialog/update-app-dialog.component.html: -------------------------------------------------------------------------------- 1 |

{{ 'update-app-dialog.title' | translate }}

2 |
3 |

{{ 'update-app-dialog.texts.0' | translate }}

4 |

5 | {{ 'update-app-dialog.texts.1' | translate }} 6 | system_update 7 | {{ 'update-app-dialog.texts.2' | translate }} 8 |

9 |
10 |
11 | 12 | 13 |
14 | -------------------------------------------------------------------------------- /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/plugins/zone-error'; // Included with Angular CLI. 17 | -------------------------------------------------------------------------------- /ngsw-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "./node_modules/@angular/service-worker/config/schema.json", 3 | "index": "/index.html", 4 | "assetGroups": [ 5 | { 6 | "name": "app", 7 | "installMode": "prefetch", 8 | "resources": { 9 | "files": [ 10 | "/favicon.ico", 11 | "/index.html", 12 | "/manifest.webmanifest", 13 | "/*.css", 14 | "/*.js" 15 | ] 16 | } 17 | }, 18 | { 19 | "name": "assets", 20 | "installMode": "prefetch", 21 | "updateMode": "prefetch", 22 | "resources": { 23 | "files": [ 24 | "/assets/**", 25 | "/*.(eot|svg|cur|jpg|png|webp|gif|otf|ttf|woff|woff2|ani)" 26 | ] 27 | } 28 | } 29 | ] 30 | } 31 | -------------------------------------------------------------------------------- /cypress/support/e2e.ts: -------------------------------------------------------------------------------- 1 | // *********************************************************** 2 | // This example support/component.ts is processed and 3 | // loaded automatically before your test files. 4 | // 5 | // This is a great place to put global configuration and 6 | // behavior that modifies Cypress. 7 | // 8 | // You can change the location of this file or turn off 9 | // automatically serving support files with the 10 | // 'supportFile' configuration option. 11 | // 12 | // You can read more here: 13 | // https://on.cypress.io/configuration 14 | // *********************************************************** 15 | 16 | // When a command from ./commands is ready to use, import with `import './commands'` syntax 17 | // import './commands'; 18 | 19 | import '@percy/cypress'; 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Street Fighter Moves 2 | 3 | > A handset ready "Street Fighter" moves glossary 4 | 5 | ![StreetFighterMoves](https://github.com/ben-barbier/street-fighter-moves/blob/main/.github/preview.png?raw=true) 6 | 7 | [![This project is using Percy.io for visual regression testing.](https://percy.io/static/images/percy-badge.svg)](https://percy.io/9e068207/street-fighter-moves) 8 | [![code style: prettier](https://img.shields.io/badge/code_style-prettier-ff69b4.svg?style=flat-square)](https://github.com/prettier/prettier) 9 | [![Github Issues](https://img.shields.io/github/issues/ben-barbier/street-fighter-moves)]() 10 | [![Github Stars](https://img.shields.io/github/stars/ben-barbier/street-fighter-moves)]() 11 | 12 |
13 | 14 | [Website](https://street-fighter-moves.tech-homies.io/) 15 | -------------------------------------------------------------------------------- /src/assets/countries/China.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/assets/countries/UK.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /src/app/layout/nav/nav.component.scss: -------------------------------------------------------------------------------- 1 | .sidenav-container { 2 | height: 100%; 3 | } 4 | 5 | .characters-link { 6 | display: flex; 7 | margin: 12px; 8 | } 9 | 10 | .sidenav { 11 | width: 200px; 12 | } 13 | 14 | .sidenav .mat-toolbar { 15 | background: inherit; 16 | } 17 | 18 | .mat-toolbar.title-container { 19 | display: flex; 20 | justify-content: center; 21 | 22 | img { 23 | height: 100%; 24 | } 25 | } 26 | 27 | .mat-toolbar.mat-primary { 28 | position: sticky; 29 | top: 0; 30 | z-index: 1; 31 | overflow-x: auto; 32 | } 33 | 34 | .character-thumbnail { 35 | height: 44px; 36 | margin-right: 8px; 37 | } 38 | 39 | .spacer { 40 | flex: 1 1 auto; 41 | } 42 | 43 | .flagButton { 44 | justify-content: center; 45 | 46 | .flag { 47 | max-height: 36px; 48 | } 49 | } 50 | 51 | .search { 52 | max-width: 100%; 53 | } 54 | -------------------------------------------------------------------------------- /.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 | /.angular/cache 36 | /.sass-cache 37 | /connect.lock 38 | /coverage 39 | /libpeerconnection.log 40 | npm-debug.log 41 | yarn-error.log 42 | testem.log 43 | /typings 44 | /cypress/videos/* 45 | /cypress/screenshots/* 46 | 47 | # System Files 48 | .DS_Store 49 | Thumbs.db 50 | -------------------------------------------------------------------------------- /src/app/pages/country-page/country-page.component.html: -------------------------------------------------------------------------------- 1 |
5 |
6 | 7 |
8 | 9 | 10 | 11 |
12 |
13 |
14 | 15 |
16 | 17 | 18 | 19 |
20 | -------------------------------------------------------------------------------- /.github/workflows/visual-review.yml: -------------------------------------------------------------------------------- 1 | name: Visual Review fot non-main branches (https://percy.io/) 2 | 3 | on: 4 | push: 5 | branches: 6 | - '**' 7 | - '!main' 8 | 9 | jobs: 10 | visual-review: 11 | runs-on: ubuntu-latest 12 | env: 13 | PERCY_TOKEN: ${{ secrets.PERCY_TOKEN }} 14 | steps: 15 | - name: Log 16 | run: | 17 | echo GITHUB_ACTOR: ${GITHUB_ACTOR} 18 | echo GITHUB_REPOSITORY: ${GITHUB_REPOSITORY} 19 | echo REPOSITORY_PATH : ${REPOSITORY_PATH} 20 | - name: Checkout 21 | uses: actions/checkout@v3 22 | - uses: actions/setup-node@v3 #this installs node and npm for us 23 | with: 24 | node-version: 16 25 | - name: Percy 26 | run: | 27 | npm ci 28 | npm run percy:ci 29 | -------------------------------------------------------------------------------- /src/app/pages/character-page/character-details/character-details.component.ts: -------------------------------------------------------------------------------- 1 | import { CommonModule } from '@angular/common'; 2 | import { Component, Input } from '@angular/core'; 3 | import { MatCardModule } from '@angular/material/card'; 4 | import { MatProgressBarModule } from '@angular/material/progress-bar'; 5 | import { RouterModule } from '@angular/router'; 6 | import { TranslateModule } from '@ngx-translate/core'; 7 | import { Character } from '../../../data'; 8 | 9 | @Component({ 10 | selector: 'app-character-details', 11 | templateUrl: './character-details.component.html', 12 | styleUrls: ['./character-details.component.scss'], 13 | standalone: true, 14 | imports: [MatCardModule, MatProgressBarModule, RouterModule, TranslateModule, CommonModule], 15 | }) 16 | export class CharacterDetailsComponent { 17 | @Input() character: Character | undefined; 18 | @Input() maxStamina = 1; 19 | @Input() maxStun = 1; 20 | @Input() countryLinkActive = false; 21 | } 22 | -------------------------------------------------------------------------------- /src/app/pages/country-page/country-page.component.scss: -------------------------------------------------------------------------------- 1 | .country-flag .light { 2 | background: rgba(255, 255, 255, 0.7); 3 | min-height: 275px; 4 | display: flex; 5 | flex-direction: column; 6 | align-items: center; 7 | justify-content: space-between; 8 | 9 | & > img { 10 | width: 250px; 11 | margin-top: 12px; 12 | max-width: calc(100% - 24px); 13 | text-align: center; 14 | } 15 | 16 | .other-countries-flags { 17 | display: flex; 18 | flex-wrap: wrap; 19 | justify-content: center; 20 | padding: 12px 0; 21 | 22 | a img { 23 | max-height: 45px; 24 | max-width: 65px; 25 | margin: 3px 6px; 26 | } 27 | } 28 | } 29 | 30 | .characters { 31 | display: flex; 32 | flex-wrap: wrap; 33 | justify-content: center; 34 | 35 | a { 36 | text-decoration: none; 37 | 38 | app-character-details { 39 | display: block; 40 | width: 370px; 41 | max-width: 100%; 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/assets/countries/South_Korea.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | Flag of South Korea 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /src/app/shared/features/update-app/check-for-update.service.ts: -------------------------------------------------------------------------------- 1 | import { ApplicationRef, inject, Injectable } from '@angular/core'; 2 | import { SwUpdate } from '@angular/service-worker'; 3 | import { concat, interval } from 'rxjs'; 4 | import { first } from 'rxjs/operators'; 5 | 6 | @Injectable({ 7 | providedIn: 'root', 8 | }) 9 | export class CheckForUpdateService { 10 | private appRef = inject(ApplicationRef); 11 | private updates = inject(SwUpdate); 12 | 13 | constructor() { 14 | if (this.updates.isEnabled) { 15 | // Allow the app to stabilize first, before starting polling for updates with `interval()`. 16 | const appIsStable$ = this.appRef.isStable.pipe(first(isStable => isStable)); 17 | const everySixHours$ = interval(6 * 60 * 60 * 1000); 18 | const everySixHoursOnceAppIsStable$ = concat(appIsStable$, everySixHours$); 19 | 20 | everySixHoursOnceAppIsStable$.subscribe(() => this.updates.checkForUpdate()); 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "overrides": [ 4 | { 5 | "files": ["*.ts"], 6 | "extends": [ 7 | "plugin:@angular-eslint/recommended", 8 | "plugin:@angular-eslint/template/process-inline-templates", 9 | "plugin:prettier/recommended" 10 | ] 11 | }, 12 | // NOTE: WE ARE NOT APPLYING PRETTIER IN THIS OVERRIDE, ONLY @ANGULAR-ESLINT/TEMPLATE 13 | { 14 | "files": ["*.html"], 15 | "extends": ["plugin:@angular-eslint/template/recommended"], 16 | "rules": {} 17 | }, 18 | // NOTE: WE ARE NOT APPLYING @ANGULAR-ESLINT/TEMPLATE IN THIS OVERRIDE, ONLY PRETTIER 19 | { 20 | "files": ["*.html"], 21 | "excludedFiles": ["*inline-template-*.component.html"], 22 | "extends": ["plugin:prettier/recommended"], 23 | "rules": { 24 | // NOTE: WE ARE OVERRIDING THE DEFAULT CONFIG TO ALWAYS SET THE PARSER TO ANGULAR (SEE BELOW) 25 | "prettier/prettier": ["error", { "parser": "angular" }] 26 | } 27 | } 28 | ] 29 | } 30 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "compileOnSave": false, 4 | "compilerOptions": { 5 | "baseUrl": "./", 6 | "outDir": "./dist/out-tsc", 7 | "forceConsistentCasingInFileNames": true, 8 | "strict": true, 9 | "noImplicitOverride": true, 10 | "noPropertyAccessFromIndexSignature": true, 11 | "noImplicitReturns": true, 12 | "noFallthroughCasesInSwitch": true, 13 | "sourceMap": true, 14 | "declaration": false, 15 | "downlevelIteration": true, 16 | "experimentalDecorators": true, 17 | "moduleResolution": "node", 18 | "importHelpers": true, 19 | "resolveJsonModule": true, 20 | "allowSyntheticDefaultImports": true, 21 | "target": "ES2022", 22 | "module": "es2020", 23 | "lib": [ 24 | "es2020", 25 | "dom" 26 | ], 27 | "useDefineForClassFields": false 28 | }, 29 | "angularCompilerOptions": { 30 | "enableI18nLegacyMessageIdFormat": false, 31 | "strictInjectionParameters": true, 32 | "strictInputAccessModifiers": true, 33 | "strictTemplates": true 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/app/shared/features/update-app/update-app-indicator/update-app-indicator.component.ts: -------------------------------------------------------------------------------- 1 | import { CommonModule } from '@angular/common'; 2 | import { Component, inject } from '@angular/core'; 3 | import { MatButtonModule } from '@angular/material/button'; 4 | import { MatIconModule } from '@angular/material/icon'; 5 | import { combineLatest } from 'rxjs'; 6 | import { map } from 'rxjs/operators'; 7 | import { ConnectionService } from '../../../offline/connection.service'; 8 | import { PromptUpdateService } from '../prompt-update.service'; 9 | 10 | @Component({ 11 | selector: 'app-update-app-indicator', 12 | templateUrl: './update-app-indicator.component.html', 13 | styleUrls: ['./update-app-indicator.component.scss'], 14 | standalone: true, 15 | imports: [CommonModule, MatButtonModule, MatIconModule], 16 | }) 17 | export class UpdateAppIndicatorComponent { 18 | public update = inject(PromptUpdateService); 19 | private connection = inject(ConnectionService); 20 | 21 | public displayIndicator$ = combineLatest([this.connection.isOnline$, this.update.appHasUpdateAvailable$]).pipe( 22 | map(([isOnline, appHasUpdateAvailable]) => isOnline && appHasUpdateAvailable), 23 | ); 24 | } 25 | -------------------------------------------------------------------------------- /server-config/.htaccess: -------------------------------------------------------------------------------- 1 | # For instructions and new versions of this Gist go to: 2 | # https://gist.github.com/julianpoemp/bcf277cb56d2420cc53ec630a04a3566 3 | # Version 1.4.1 4 | 5 | 6 | RewriteEngine On 7 | 8 | # -- REDIRECTION to https (optional): 9 | # If you need this, uncomment the next two commands 10 | RewriteCond %{HTTPS} !on 11 | RewriteRule (.*) https://%{HTTP_HOST}%{REQUEST_URI} [L] 12 | # -- 13 | 14 | RewriteCond %{DOCUMENT_ROOT}%{REQUEST_URI} -f [OR] 15 | RewriteCond %{DOCUMENT_ROOT}%{REQUEST_URI} -d 16 | 17 | RewriteRule ^.*$ - [NC,L] 18 | RewriteRule ^(.*) index.html [NC,L] 19 | 20 | 21 | #------------ BROWSER CACHING (optional) 22 | # Disable browser caching for all files that don't get a hash string by Angular. 23 | # 24 | # 25 | # FileETag None 26 | # Header unset ETag 27 | # Header unset Pragma 28 | # Header unset Cache-Control 29 | # Header unset Last-Modified 30 | # Header set Pragma "no-cache" 31 | # Header set Cache-Control "max-age=0, no-cache, no-store, must-revalidate" 32 | # Header set Expires "Mon, 10 Apr 1972 00:00:00 GMT" 33 | # 34 | # 35 | #------------ 36 | -------------------------------------------------------------------------------- /src/assets/countries/India.svg: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /src/manifest.webmanifest: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Street Fighter Moves", 3 | "short_name": "SF Moves", 4 | "theme_color": "#1976d2", 5 | "background_color": "#fafafa", 6 | "display": "standalone", 7 | "scope": "./", 8 | "start_url": "./", 9 | "icons": [ 10 | { 11 | "src": "assets/icons/icon-72x72.png", 12 | "sizes": "72x72", 13 | "type": "image/png", 14 | "purpose": "maskable any" 15 | }, 16 | { 17 | "src": "assets/icons/icon-96x96.png", 18 | "sizes": "96x96", 19 | "type": "image/png", 20 | "purpose": "maskable any" 21 | }, 22 | { 23 | "src": "assets/icons/icon-128x128.png", 24 | "sizes": "128x128", 25 | "type": "image/png", 26 | "purpose": "maskable any" 27 | }, 28 | { 29 | "src": "assets/icons/icon-144x144.png", 30 | "sizes": "144x144", 31 | "type": "image/png", 32 | "purpose": "maskable any" 33 | }, 34 | { 35 | "src": "assets/icons/icon-192x192.png", 36 | "sizes": "192x192", 37 | "type": "image/png", 38 | "purpose": "maskable any" 39 | }, 40 | { 41 | "src": "assets/icons/icon-512x512.png", 42 | "sizes": "512x512", 43 | "type": "image/png", 44 | "purpose": "maskable any" 45 | } 46 | ] 47 | } 48 | -------------------------------------------------------------------------------- /src/app/pages/character-page/character-details/character-details.component.html: -------------------------------------------------------------------------------- 1 | 2 |
3 | 4 |
5 | 6 |
7 |
8 |

{{ character.name }}

9 | 10 | 11 | 12 | 13 | 14 | 15 |
16 |
17 | 🛡 {{ 'pages.character.details.stamina' | translate }} - {{ character.stamina }} 18 | 19 |
20 |
21 | 👊 {{ 'pages.character.details.stun' | translate }} - {{ character.stun }} 22 | 23 |
24 |
25 |
26 | -------------------------------------------------------------------------------- /src/assets/countries/USA.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /src/assets/countries/Hong_Kong.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 9 | 12 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /src/assets/i18n/en.json: -------------------------------------------------------------------------------- 1 | { 2 | "actions": { 3 | "update": "Update", 4 | "do-later": "Do Later", 5 | "ok": "OK, got it" 6 | }, 7 | "nav": { 8 | "title": "Street Fighter Moves", 9 | "list": "Characters", 10 | "no-results": "No results" 11 | }, 12 | "update-app-dialog": { 13 | "title": "Street Fighter Moves : new version available", 14 | "texts": [ 15 | "A new version of the 'Street Fighter Moves' application is available.", 16 | "You can choose to activate it now or activate it later by clicking on the", 17 | "icon on your navigation bar." 18 | ] 19 | }, 20 | "controls": { 21 | "title": "Controls", 22 | "manuals": "CAPCOM® Online Manuals", 23 | "punch": "Punch", 24 | "kick": "Kick", 25 | "light-punch": "Light kick", 26 | "medium-punch": "Medium kick", 27 | "heavy-punch": "Heavy kick", 28 | "triple-punch": "3 Punch buttons in same time", 29 | "light-kick": "Light kick", 30 | "medium-kick": "Medium kick", 31 | "heavy-kick": "Heavy kick", 32 | "triple-kick": "3 Kick buttons in same time" 33 | }, 34 | "pages": { 35 | "character": { 36 | "title": "Street Fighter Moves - {{character}}", 37 | "details": { 38 | "stun": "Stun", 39 | "stamina": "Stamina" 40 | } 41 | }, 42 | "characters-list": { 43 | "title": "Street Fighter Moves - Characters" 44 | }, 45 | "country": { 46 | "title": "Street Fighter Moves - Countries - {{country}}" 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /cypress/support/commands.ts: -------------------------------------------------------------------------------- 1 | // *********************************************** 2 | // This example namespace declaration will help 3 | // with Intellisense and code completion in your 4 | // IDE or Text Editor. 5 | // *********************************************** 6 | // declare namespace Cypress { 7 | // interface Chainable { 8 | // customCommand(param: any): typeof customCommand; 9 | // } 10 | // } 11 | // 12 | // function customCommand(param: any): void { 13 | // console.warn(param); 14 | // } 15 | // 16 | // NOTE: You can use it like so: 17 | // Cypress.Commands.add('customCommand', customCommand); 18 | // 19 | // *********************************************** 20 | // This example commands.js shows you how to 21 | // create various custom commands and overwrite 22 | // existing commands. 23 | // 24 | // For more comprehensive examples of custom 25 | // commands please read more here: 26 | // https://on.cypress.io/custom-commands 27 | // *********************************************** 28 | // 29 | // 30 | // -- This is a parent command -- 31 | // Cypress.Commands.add("login", (email, password) => { ... }) 32 | // 33 | // 34 | // -- This is a child command -- 35 | // Cypress.Commands.add("drag", { prevSubject: 'element'}, (subject, options) => { ... }) 36 | // 37 | // 38 | // -- This is a dual command -- 39 | // Cypress.Commands.add("dismiss", { prevSubject: 'optional'}, (subject, options) => { ... }) 40 | // 41 | // 42 | // -- This will overwrite an existing command -- 43 | // Cypress.Commands.overwrite("visit", (originalFn, url, options) => { ... }) 44 | -------------------------------------------------------------------------------- /src/app/app-routing.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { RouterModule, Routes } from '@angular/router'; 3 | import { CharacterPageComponent } from './pages/character-page/character-page.component'; 4 | import { canActivateCharacter } from './pages/character-page/character-page.guard'; 5 | import { characterPageResolver } from './pages/character-page/character-page.resolver'; 6 | import { CharactersListPageComponent } from './pages/characters-list-page/characters-list-page.component'; 7 | import { CountryPageComponent } from './pages/country-page/country-page.component'; 8 | import { canActivateCountry } from './pages/country-page/country-page.guard'; 9 | import { countryPageResolver } from './pages/country-page/country-page.resolver'; 10 | 11 | const routes: Routes = [ 12 | { 13 | path: 'sf4', 14 | component: CharactersListPageComponent, 15 | data: { animation: 'CharactersListPage' }, 16 | }, 17 | { 18 | path: 'sf4/characters/:characterId', 19 | component: CharacterPageComponent, 20 | canActivate: [canActivateCharacter], 21 | resolve: { character: characterPageResolver }, 22 | data: { animation: 'CharacterPage' }, 23 | }, 24 | { 25 | path: 'sf4/countries/:country', 26 | component: CountryPageComponent, 27 | canActivate: [canActivateCountry], 28 | resolve: { character: countryPageResolver }, 29 | data: { animation: 'CountryPage' }, 30 | }, 31 | { path: '**', redirectTo: '/sf4' }, 32 | ]; 33 | 34 | @NgModule({ 35 | imports: [RouterModule.forRoot(routes)], 36 | exports: [RouterModule], 37 | }) 38 | export class AppRoutingModule {} 39 | -------------------------------------------------------------------------------- /src/assets/i18n/fr.json: -------------------------------------------------------------------------------- 1 | { 2 | "actions": { 3 | "update": "Mettre à jour", 4 | "do-later": "Plus tard", 5 | "ok": "OK" 6 | }, 7 | "nav": { 8 | "title": "Street Fighter Moves", 9 | "list": "Personnages", 10 | "no-results": "Aucun résultat" 11 | }, 12 | "update-app-dialog": { 13 | "title": "Street Fighter Moves : nouvelle version disponible", 14 | "texts": [ 15 | "Une nouvelle version de l'application 'Street Fighter Moves' est disponible.", 16 | "Vous pouvez choisir de l'activer dès à présent ou de le faire ultérieurement en cliquant sur l'icone", 17 | "de votre barre de navigation." 18 | ] 19 | }, 20 | "controls": { 21 | "title": "Commandes", 22 | "manuals": "Manuels CAPCOM® en ligne", 23 | "punch": "Coup de poing", 24 | "kick": "Coup de pied", 25 | "light-punch": "Coup de poing léger", 26 | "medium-punch": "Coup de poing moyen", 27 | "heavy-punch": "Coup de poing lourd", 28 | "triple-punch": "3 boutons 'Coup de poing' en même temps", 29 | "light-kick": "Coup de pied léger", 30 | "medium-kick": "Coup de pied moyen", 31 | "heavy-kick": "Coup de pied lourd", 32 | "triple-kick": "3 boutons 'Coup de pied' en même temps" 33 | }, 34 | "pages": { 35 | "character": { 36 | "title": "Street Fighter Moves - {{character}}", 37 | "details": { 38 | "stun": "Étourdissement", 39 | "stamina": "Endurance" 40 | } 41 | }, 42 | "characters-list": { 43 | "title": "Street Fighter Moves - Personnages" 44 | }, 45 | "country": { 46 | "title": "Street Fighter Moves - Pays - {{country}}" 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /.github/workflows/build-deploy.yml: -------------------------------------------------------------------------------- 1 | name: Build and Deploy 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | jobs: 8 | build-and-deploy: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - name: Log 12 | run: | 13 | echo GITHUB_ACTOR: ${GITHUB_ACTOR} 14 | echo GITHUB_REPOSITORY: ${GITHUB_REPOSITORY} 15 | echo REPOSITORY_PATH : ${REPOSITORY_PATH} 16 | - name: Checkout 17 | uses: actions/checkout@v3 18 | - uses: actions/setup-node@v3 #this installs node and npm for us 19 | with: 20 | node-version: 16 21 | - name: Build 22 | run: | 23 | npm ci 24 | npm run lint 25 | npm run fmt:check 26 | npm run cy:ci 27 | npm run sitemap:generate 28 | npm run build 29 | - name: Add server config 30 | env: 31 | stagingDir: 'dist/street-fighter-moves' 32 | run: | 33 | cp server-config/.htaccess $stagingDir 34 | mv sitemap.xml $stagingDir 35 | - name: Setup 36 | run: sudo apt-get install lftp 37 | - name: Upload ftp 38 | env: 39 | localDir: './dist/street-fighter-moves' 40 | remoteDir: 'street-fighter-moves' 41 | run: 42 | lftp ${{ secrets.FTP_SERVER }} 43 | -u ${{ secrets.FTP_USERNAME }},${{ secrets.FTP_PASSWORD }} 44 | -e "rm -r $remoteDir; mirror -R $localDir $remoteDir" 45 | -------------------------------------------------------------------------------- /src/app/layout/nav/nav.animations.ts: -------------------------------------------------------------------------------- 1 | import { animate, animateChild, AnimationMetadata, group, query, style, transition, trigger } from '@angular/animations'; 2 | 3 | const swipeLeft: AnimationMetadata[] = [ 4 | style({ position: 'relative' }), 5 | query(':enter, :leave', [style({ position: 'absolute', top: 0, left: 0, width: '100%' })]), 6 | query(':enter', [style({ left: '100%' })]), 7 | query(':leave', animateChild()), 8 | group([ 9 | query(':leave', [animate('300ms ease-out', style({ left: '-100%' }))]), 10 | query(':enter', [animate('300ms ease-out', style({ left: '0%' }))]), 11 | ]), 12 | ]; 13 | 14 | const swipeRight: AnimationMetadata[] = [ 15 | style({ position: 'relative' }), 16 | query(':enter, :leave', [style({ position: 'absolute', top: 0, left: 0, width: '100%' })]), 17 | query(':enter', [style({ left: '-100%' })]), 18 | query(':leave', animateChild()), 19 | group([ 20 | query(':leave', [animate('300ms ease-out', style({ left: '100%' }))]), 21 | query(':enter', [animate('300ms ease-out', style({ left: '0%' }))]), 22 | ]), 23 | ]; 24 | 25 | const fadeIn: AnimationMetadata[] = [ 26 | style({ position: 'relative' }), 27 | query(':enter, :leave', [style({ position: 'absolute', top: 0, left: 0, width: '100%' })]), 28 | query(':enter', [style({ opacity: '0' })]), 29 | query(':leave', animateChild()), 30 | group([ 31 | query(':leave', [animate('300ms ease-out', style({ opacity: '0' }))]), 32 | query(':enter', [animate('300ms ease-out', style({ opacity: '1' }))]), 33 | ]), 34 | ]; 35 | 36 | export const slideInAnimation = trigger('routeAnimations', [ 37 | transition('CharactersListPage => CharacterPage', swipeLeft), 38 | transition('CharacterPage <=> CountryPage', fadeIn), 39 | transition('* => CharactersListPage', swipeRight), 40 | ]); 41 | -------------------------------------------------------------------------------- /src/app/shared/features/update-app/prompt-update.service.ts: -------------------------------------------------------------------------------- 1 | import { inject, Injectable } from '@angular/core'; 2 | import { MatDialog } from '@angular/material/dialog'; 3 | import { SwUpdate } from '@angular/service-worker'; 4 | import { BehaviorSubject } from 'rxjs'; 5 | import { filter } from 'rxjs/operators'; 6 | import { UpdateAppDialogComponent } from './update-app-dialog/update-app-dialog.component'; 7 | 8 | @Injectable({ 9 | providedIn: 'root', 10 | }) 11 | export class PromptUpdateService { 12 | private updates = inject(SwUpdate); 13 | private dialog = inject(MatDialog); 14 | 15 | public appHasUpdateAvailable$ = new BehaviorSubject(false); 16 | 17 | constructor() { 18 | if (this.updates.isEnabled) { 19 | this.updates.versionUpdates.subscribe(evt => { 20 | switch (evt.type) { 21 | case 'VERSION_DETECTED': 22 | console.log(`Downloading new app version: ${evt.version.hash}`); 23 | break; 24 | case 'VERSION_READY': 25 | console.log(`Current app version: ${evt.currentVersion.hash}`); 26 | console.log(`New app version ready for use: ${evt.latestVersion.hash}`); 27 | this.appHasUpdateAvailable$.next(true); 28 | this.promptUpdateDialog(); 29 | break; 30 | case 'VERSION_INSTALLATION_FAILED': 31 | console.log(`Failed to install app version '${evt.version.hash}': ${evt.error}`); 32 | break; 33 | } 34 | }); 35 | } 36 | } 37 | 38 | public promptUpdateDialog(): void { 39 | this.dialog 40 | .open(UpdateAppDialogComponent) 41 | .afterClosed() 42 | .pipe(filter(response => !!response)) 43 | .subscribe(() => { 44 | this.updates.activateUpdate().then(() => document.location.reload()); 45 | }); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Street Fighter Moves 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 31 | 32 | 33 | 34 |
35 | Street Fighter 36 |
37 |
38 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /src/app/layout/nav/controls/controls.component.html: -------------------------------------------------------------------------------- 1 |

{{ 'controls.title' | translate }}

2 |
3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 49 | 50 |
{{ 'controls.punch' | translate }}
{{ 'controls.light-punch' | translate }}
{{ 'controls.medium-punch' | translate }}
{{ 'controls.heavy-punch' | translate }}
{{ 'controls.triple-punch' | translate }}
{{ 'controls.kick' | translate }}
{{ 'controls.light-kick' | translate }}
{{ 'controls.medium-kick' | translate }}
{{ 'controls.heavy-kick' | translate }}
{{ 'controls.triple-kick' | translate }}
menu_book 47 | {{ 'controls.manuals' | translate }} 48 |
51 |
52 |
53 | 56 |
57 | -------------------------------------------------------------------------------- /src/app/pages/characters-list-page/characters-list-page.component.ts: -------------------------------------------------------------------------------- 1 | import { CommonModule, DOCUMENT } from '@angular/common'; 2 | import { Component, inject, OnDestroy, OnInit } from '@angular/core'; 3 | import { Meta, Title } from '@angular/platform-browser'; 4 | import { RouterLink } from '@angular/router'; 5 | import { TranslateService } from '@ngx-translate/core'; 6 | import { Character, data } from '../../data'; 7 | 8 | @Component({ 9 | selector: 'app-characters-list-page', 10 | templateUrl: './characters-list-page.component.html', 11 | styleUrls: ['./characters-list-page.component.scss'], 12 | standalone: true, 13 | imports: [CommonModule, RouterLink], 14 | }) 15 | export class CharactersListPageComponent implements OnInit, OnDestroy { 16 | private title = inject(Title); 17 | private meta = inject(Meta); 18 | private translate = inject(TranslateService); 19 | private document = inject(DOCUMENT); 20 | 21 | public characters: Character[] = data[0].characters; 22 | 23 | public trackById = (index: number, character: Character) => character.id; 24 | 25 | ngOnInit(): void { 26 | const titleText = this.translate.instant('pages.characters-list.title'); 27 | this.title.setTitle(titleText); 28 | 29 | const description = 'Street Fighter 4 Arcade Edition'; 30 | this.meta.updateTag({ name: 'description', content: description }); 31 | 32 | // 📖 : https://developers.facebook.com/docs/sharing/webmasters#markup 33 | this.meta.updateTag({ name: 'og:url', content: this.document.URL }); 34 | this.meta.updateTag({ name: 'og:type', content: 'website' }); 35 | this.meta.updateTag({ name: 'og:title', content: titleText }); 36 | this.meta.updateTag({ name: 'og:description', content: description }); 37 | this.meta.updateTag({ name: 'og:image', content: `${this.document.baseURI}assets/logo.png` }); 38 | } 39 | 40 | ngOnDestroy(): void { 41 | this.removeMetaTags(); 42 | } 43 | 44 | private removeMetaTags(): void { 45 | ['description', 'og:url', 'og:type', 'og:title', 'og:description', 'og:image'].forEach(tag => this.meta.removeTag(`name='${tag}'`)); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/app/pages/character-page/character-moves/character-moves.component.scss: -------------------------------------------------------------------------------- 1 | $special: #03a9f4; 2 | $super: #ffa000; 3 | $ultra: #d32f2f; 4 | 5 | :host { 6 | display: flex; 7 | flex-wrap: wrap; 8 | } 9 | 10 | table { 11 | margin: 0 6px; 12 | } 13 | 14 | .vertical-align { 15 | display: flex; 16 | flex-direction: column; 17 | justify-content: center; 18 | } 19 | 20 | .special, 21 | .super, 22 | .ultra { 23 | .type { 24 | font-weight: bold; 25 | writing-mode: tb-rl; 26 | width: 18px; 27 | text-align: center; 28 | margin-right: 2px; 29 | text-shadow: 0 0 3px black; 30 | } 31 | 32 | .title { 33 | &.combo { 34 | display: flex; 35 | padding: 4px 10px 4px 4px; 36 | } 37 | 38 | padding: 4px 10px; 39 | min-height: 64px; 40 | height: 100%; 41 | box-sizing: border-box; 42 | 43 | .name { 44 | font-size: 1.2em; 45 | font-weight: bold; 46 | } 47 | 48 | .original-name { 49 | font-size: 0.8em; 50 | } 51 | } 52 | 53 | .motion { 54 | display: flex; 55 | align-items: center; 56 | 57 | mat-icon { 58 | margin-right: 10px; 59 | font-size: 30px; 60 | } 61 | } 62 | } 63 | 64 | .special { 65 | .title { 66 | color: white; 67 | background-color: $special; 68 | 69 | .original-name { 70 | color: #b3e5fc; 71 | } 72 | } 73 | 74 | &.followup .title { 75 | background-color: lighten($special, 15%); 76 | } 77 | } 78 | 79 | .super { 80 | .type { 81 | color: $super; 82 | } 83 | .title { 84 | color: white; 85 | background-color: #ffc107; 86 | 87 | .original-name { 88 | color: #ffecb3; 89 | } 90 | } 91 | 92 | &.followup .title { 93 | background-color: lighten($super, 15%); 94 | } 95 | } 96 | 97 | .ultra { 98 | .type { 99 | color: $ultra; 100 | } 101 | .title { 102 | color: white; 103 | background-color: #f44336; 104 | 105 | .original-name { 106 | color: #ffcdd2; 107 | } 108 | } 109 | 110 | &.followup .title { 111 | background-color: lighten($ultra, 15%); 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /src/assets/countries/None.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 7 | 12 | 13 | -------------------------------------------------------------------------------- /src/app/layout/nav/nav.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { LayoutModule } from '@angular/cdk/layout'; 2 | import { HttpClientModule } from '@angular/common/http'; 3 | import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; 4 | import { FormsModule } from '@angular/forms'; 5 | import { MatButtonModule } from '@angular/material/button'; 6 | import { MatDialogModule } from '@angular/material/dialog'; 7 | import { MatFormFieldModule } from '@angular/material/form-field'; 8 | import { MatIconModule } from '@angular/material/icon'; 9 | import { MatInputModule } from '@angular/material/input'; 10 | import { MatListModule } from '@angular/material/list'; 11 | import { MatMenuModule } from '@angular/material/menu'; 12 | import { MatSidenavModule } from '@angular/material/sidenav'; 13 | import { MatToolbarModule } from '@angular/material/toolbar'; 14 | import { NoopAnimationsModule } from '@angular/platform-browser/animations'; 15 | import { RouterTestingModule } from '@angular/router/testing'; 16 | import { expect } from '@jest/globals'; 17 | import { TranslateModule } from '@ngx-translate/core'; 18 | import { GithubButtonModule } from 'ng-github-button'; 19 | import { PromptUpdateService } from '../../shared/features/update-app/prompt-update.service'; 20 | import { UpdateAppIndicatorComponent } from '../../shared/features/update-app/update-app-indicator/update-app-indicator.component'; 21 | import { NavComponent } from './nav.component'; 22 | 23 | describe('NavComponent', () => { 24 | let component: NavComponent; 25 | let fixture: ComponentFixture; 26 | 27 | beforeEach(waitForAsync(() => { 28 | TestBed.configureTestingModule({ 29 | imports: [ 30 | NavComponent, 31 | UpdateAppIndicatorComponent, 32 | NoopAnimationsModule, 33 | FormsModule, 34 | GithubButtonModule, 35 | HttpClientModule, 36 | LayoutModule, 37 | MatButtonModule, 38 | MatDialogModule, 39 | MatFormFieldModule, 40 | MatIconModule, 41 | MatInputModule, 42 | MatListModule, 43 | MatMenuModule, 44 | MatSidenavModule, 45 | MatToolbarModule, 46 | RouterTestingModule, 47 | TranslateModule.forRoot(), 48 | ], 49 | providers: [{ provide: PromptUpdateService, useValue: { promptUpdateDilog: jest.fn() } }], 50 | }).compileComponents(); 51 | })); 52 | 53 | beforeEach(() => { 54 | fixture = TestBed.createComponent(NavComponent); 55 | component = fixture.componentInstance; 56 | fixture.detectChanges(); 57 | }); 58 | 59 | it('should compile', () => { 60 | expect(component).toBeDefined(); 61 | }); 62 | }); 63 | -------------------------------------------------------------------------------- /src/app/pages/country-page/country-page.component.ts: -------------------------------------------------------------------------------- 1 | import { CommonModule } from '@angular/common'; 2 | import { Component, inject, OnDestroy, OnInit } from '@angular/core'; 3 | import { Meta, Title } from '@angular/platform-browser'; 4 | import { ActivatedRoute, RouterLink } from '@angular/router'; 5 | import { TranslateService } from '@ngx-translate/core'; 6 | import { map } from 'rxjs/operators'; 7 | import { Character, countries, maxStamina, maxStun } from '../../data'; 8 | import { CharacterDetailsComponent } from '../character-page/character-details/character-details.component'; 9 | 10 | @Component({ 11 | selector: 'app-country-page', 12 | templateUrl: './country-page.component.html', 13 | styleUrls: ['./country-page.component.scss'], 14 | standalone: true, 15 | imports: [CommonModule, RouterLink, CharacterDetailsComponent], 16 | }) 17 | export class CountryPageComponent implements OnInit, OnDestroy { 18 | private route = inject(ActivatedRoute); 19 | private title = inject(Title); 20 | private meta = inject(Meta); 21 | private translate = inject(TranslateService); 22 | 23 | public characters: Character[] = []; 24 | public maxStamina: number = maxStamina(1); 25 | public maxStun: number = maxStun(1); 26 | public country: string = ''; 27 | public otherCountries: string[] = []; 28 | 29 | ngOnInit(): void { 30 | this.route.data.pipe(map(resolveData => resolveData['character'])).subscribe(characters => { 31 | this.country = characters[0].country; 32 | this.characters = characters; 33 | this.otherCountries = [...new Set(countries(1).filter(c => c !== this.characters[0].country))]; 34 | 35 | const titleText = this.translate.instant('pages.country.title', { country: this.country }); 36 | this.title.setTitle(titleText); 37 | 38 | const description = `Street Fighter 4 Arcade Edition - ${this.country} characters`; 39 | this.meta.updateTag({ name: 'description', content: description }); 40 | 41 | // 📖 : https://developers.facebook.com/docs/sharing/webmasters#markup 42 | this.meta.updateTag({ name: 'og:url', content: document.URL }); 43 | this.meta.updateTag({ name: 'og:type', content: 'website' }); 44 | this.meta.updateTag({ name: 'og:title', content: titleText }); 45 | this.meta.updateTag({ name: 'og:description', content: description }); 46 | this.meta.updateTag({ name: 'og:image', content: `${document.baseURI}assets/countries/${this.country}.svg` }); 47 | }); 48 | } 49 | 50 | ngOnDestroy(): void { 51 | this.removeMetaTags(); 52 | } 53 | 54 | private removeMetaTags(): void { 55 | ['description', 'og:url', 'og:type', 'og:title', 'og:description', 'og:image'].forEach(tag => this.meta.removeTag(`name='${tag}'`)); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/app/app.module.ts: -------------------------------------------------------------------------------- 1 | import { LOCATION_INITIALIZED } from '@angular/common'; 2 | import { HttpClient, HttpClientModule } from '@angular/common/http'; 3 | import { APP_INITIALIZER, Injector, NgModule } from '@angular/core'; 4 | import { MatDialogModule } from '@angular/material/dialog'; 5 | import { BrowserModule } from '@angular/platform-browser'; 6 | import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; 7 | import { ServiceWorkerModule } from '@angular/service-worker'; 8 | import { TranslateLoader, TranslateModule, TranslateService } from '@ngx-translate/core'; 9 | import { TranslateHttpLoader } from '@ngx-translate/http-loader'; 10 | import { finalize, tap } from 'rxjs'; 11 | import { environment } from '../environments/environment'; 12 | import { AppRoutingModule } from './app-routing.module'; 13 | import { AppComponent } from './app.component'; 14 | import { NavComponent } from './layout/nav/nav.component'; 15 | 16 | const availableLanguages = ['fr', 'en']; 17 | 18 | const loadTranslations = 19 | (translate: TranslateService, injector: Injector): (() => Promise) => 20 | () => 21 | new Promise((resolve: any) => { 22 | const locationInitialized = injector.get(LOCATION_INITIALIZED, Promise.resolve(null)); 23 | locationInitialized.then(() => { 24 | const browserLanguage = navigator?.language?.split('-')[0]; 25 | const langToSet = availableLanguages.find(l => l === browserLanguage) || 'en'; 26 | translate.setDefaultLang('fr'); 27 | translate 28 | .use(langToSet) 29 | .pipe( 30 | tap({ 31 | next: () => console.log(`Successfully initialized '${langToSet}' language.`), 32 | error: () => console.error(`Problem with '${langToSet}' language initialization.`), 33 | }), 34 | finalize(() => resolve()), 35 | ) 36 | .subscribe(); 37 | }); 38 | }); 39 | 40 | @NgModule({ 41 | declarations: [AppComponent], 42 | imports: [ 43 | BrowserModule, 44 | BrowserAnimationsModule, 45 | AppRoutingModule, 46 | HttpClientModule, 47 | MatDialogModule, 48 | NavComponent, 49 | ServiceWorkerModule.register('ngsw-worker.js', { enabled: environment.production }), 50 | TranslateModule.forRoot({ 51 | loader: { 52 | provide: TranslateLoader, 53 | useFactory: (http: HttpClient) => new TranslateHttpLoader(http), 54 | deps: [HttpClient], 55 | }, 56 | }), 57 | ], 58 | providers: [ 59 | { 60 | provide: APP_INITIALIZER, 61 | useFactory: loadTranslations, 62 | deps: [TranslateService, Injector], 63 | multi: true, 64 | }, 65 | ], 66 | bootstrap: [AppComponent], 67 | }) 68 | export class AppModule {} 69 | -------------------------------------------------------------------------------- /src/app/pages/character-page/character-page.component.ts: -------------------------------------------------------------------------------- 1 | import { CommonModule, DOCUMENT } from '@angular/common'; 2 | import { Component, inject, OnDestroy, OnInit } from '@angular/core'; 3 | import { Meta, Title } from '@angular/platform-browser'; 4 | import { ActivatedRoute } from '@angular/router'; 5 | import { UntilDestroy, untilDestroyed } from '@ngneat/until-destroy'; 6 | import { TranslateService } from '@ngx-translate/core'; 7 | import { Observable } from 'rxjs'; 8 | import { map } from 'rxjs/operators'; 9 | import { Character, maxStamina, maxStun } from '../../data'; 10 | import { CharacterDetailsComponent } from './character-details/character-details.component'; 11 | import { CharacterMovesComponent } from './character-moves/character-moves.component'; 12 | 13 | @UntilDestroy() 14 | @Component({ 15 | selector: 'app-character-page', 16 | templateUrl: './character-page.component.html', 17 | styleUrls: ['./character-page.component.scss'], 18 | standalone: true, 19 | imports: [CommonModule, CharacterDetailsComponent, CharacterMovesComponent], 20 | }) 21 | export class CharacterPageComponent implements OnInit, OnDestroy { 22 | private route = inject(ActivatedRoute); 23 | private title = inject(Title); 24 | private meta = inject(Meta); 25 | private translate = inject(TranslateService); 26 | private document = inject(DOCUMENT); 27 | 28 | public character$: Observable = this.route.data.pipe(map(resolveData => resolveData['character'])); 29 | public maxStamina = maxStamina(1); 30 | public maxStun = maxStun(1); 31 | 32 | ngOnInit(): void { 33 | this.character$.pipe(untilDestroyed(this)).subscribe((character: Character) => { 34 | const titleText = this.translate.instant('pages.character.title', { character: character.name }); 35 | this.title.setTitle(titleText); 36 | 37 | const description = `Street Fighter 4 Arcade Edition - ${character.name} moves`; 38 | this.meta.updateTag({ name: 'description', content: description }); 39 | 40 | // 📖 : https://developers.facebook.com/docs/sharing/webmasters#markup 41 | this.meta.updateTag({ name: 'og:url', content: this.document.URL }); 42 | this.meta.updateTag({ name: 'og:type', content: 'website' }); 43 | this.meta.updateTag({ name: 'og:title', content: titleText }); 44 | this.meta.updateTag({ name: 'og:description', content: description }); 45 | this.meta.updateTag({ 46 | name: 'og:image', 47 | content: `${this.document.baseURI}assets/characters/${character.id}_thumbnail.png`, 48 | }); 49 | }); 50 | } 51 | 52 | ngOnDestroy(): void { 53 | this.removeMetaTags(); 54 | } 55 | 56 | private removeMetaTags(): void { 57 | ['description', 'og:url', 'og:type', 'og:title', 'og:description', 'og:image'].forEach(tag => this.meta.removeTag(`name='${tag}'`)); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "street-fighter-moves", 3 | "version": "1.2.3", 4 | "scripts": { 5 | "ng": "ng", 6 | "start": "ng serve", 7 | "build": "ng build", 8 | "test": "ng test", 9 | "lint": "ng lint", 10 | "cy:open": "cypress open", 11 | "cy:run": "cypress run", 12 | "cy:ci": "START_SERVER_AND_TEST_INSECURE=1 start-server-and-test start http-get://localhost:4200 cy:run", 13 | "percy:ci": "percy exec -- npm run cy:ci", 14 | "fmt": "prettier --write \"**\"", 15 | "fmt:check": "prettier --check \"**\"", 16 | "sitemap:generate": "sitemap < sitemap-urls.txt > sitemap.xml", 17 | "e2e": "ng e2e" 18 | }, 19 | "private": true, 20 | "dependencies": { 21 | "@angular/animations": "^15.2.4", 22 | "@angular/cdk": "^15.2.4", 23 | "@angular/common": "^15.2.4", 24 | "@angular/compiler": "^15.2.4", 25 | "@angular/core": "^15.2.4", 26 | "@angular/forms": "^15.2.4", 27 | "@angular/material": "^15.2.4", 28 | "@angular/platform-browser": "^15.2.4", 29 | "@angular/platform-browser-dynamic": "^15.2.4", 30 | "@angular/router": "^15.2.4", 31 | "@angular/service-worker": "^15.2.4", 32 | "@ngneat/until-destroy": "^9.2.3", 33 | "@ngx-translate/core": "^14.0.0", 34 | "@ngx-translate/http-loader": "^7.0.0", 35 | "ng-github-button": "^15.0.0", 36 | "rxjs": "~7.8.0", 37 | "sitemap": "^7.1.1", 38 | "tslib": "^2.4.0", 39 | "zone.js": "~0.13.0" 40 | }, 41 | "devDependencies": { 42 | "@angular-builders/jest": "^15.0.0", 43 | "@angular-devkit/build-angular": "^15.2.4", 44 | "@angular-eslint/builder": "^15.2.1", 45 | "@angular-eslint/eslint-plugin": "^15.2.1", 46 | "@angular-eslint/eslint-plugin-template": "^15.2.1", 47 | "@angular-eslint/schematics": "^15.2.1", 48 | "@angular-eslint/template-parser": "^15.2.1", 49 | "@angular/cli": "^15.2.4", 50 | "@angular/compiler-cli": "^15.2.4", 51 | "@cypress/schematic": "^2.5.0", 52 | "@percy/cli": "^1.21.0", 53 | "@percy/cypress": "^3.1.2", 54 | "@types/jest": "^29.5.0", 55 | "@types/node": "^16.18.21", 56 | "@typescript-eslint/eslint-plugin": "^5.57.0", 57 | "@typescript-eslint/parser": "^5.57.0", 58 | "cypress": "^12.8.1", 59 | "eslint": "^8.36.0", 60 | "eslint-config-prettier": "^8.8.0", 61 | "eslint-plugin-import": "^2.27.5", 62 | "eslint-plugin-jest": "^27.2.1", 63 | "eslint-plugin-jsdoc": "^40.1.0", 64 | "eslint-plugin-prefer-arrow": "^1.2.3", 65 | "eslint-plugin-prettier": "^4.2.1", 66 | "husky": "^8.0.3", 67 | "jest": "^29.5.0", 68 | "jest-preset-angular": "^13.0.1", 69 | "prettier": "^2.8.7", 70 | "prettier-eslint": "^15.0.1", 71 | "prettier-plugin-organize-imports": "^3.2.2", 72 | "pretty-quick": "^3.1.3", 73 | "start-server-and-test": "^2.0.0", 74 | "ts-node": "^10.9.1", 75 | "typescript": "~4.9.5" 76 | }, 77 | "husky": { 78 | "hooks": { 79 | "pre-commit": "pretty-quick --staged" 80 | } 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /cypress/e2e/navigation.cy.ts: -------------------------------------------------------------------------------- 1 | context('Navigation', () => { 2 | context('Characters List', () => { 3 | it(`Characters List page contains all characters`, () => { 4 | const numberOfCharacters = 37; 5 | cy.visit(`/sf4`).get('ul').find('li').should('have.length', numberOfCharacters); 6 | cy.percySnapshot('Characters List'); 7 | }); 8 | }); 9 | 10 | context('Characters', () => { 11 | const characters = [ 12 | { slug: 'abel', name: 'Abel' }, 13 | { slug: 'adon', name: 'Adon' }, 14 | { slug: 'akuma', name: 'Akuma' }, 15 | { slug: 'balrog', name: 'Balrog' }, 16 | { slug: 'blanka', name: 'Blanka' }, 17 | { slug: 'c-viper', name: 'Crimson Viper' }, 18 | { slug: 'cammy', name: 'Cammy' }, 19 | { slug: 'chun-li', name: 'Chun-Li' }, 20 | { slug: 'cody', name: 'Cody' }, 21 | { slug: 'dan', name: 'Dan' }, 22 | { slug: 'dee-jay', name: 'Dee Jay' }, 23 | { slug: 'dhalsim', name: 'Dhalsim' }, 24 | { slug: 'dudley', name: 'Dudley' }, 25 | { slug: 'e-honda', name: 'Edmond Honda' }, 26 | { slug: 'el-fuerte', name: 'El Fuerte' }, 27 | { slug: 'fei-long', name: 'Fei Long' }, 28 | { slug: 'gen', name: 'Gen' }, 29 | { slug: 'gouken', name: 'Gouken' }, 30 | { slug: 'guile', name: 'Guile' }, 31 | { slug: 'guy', name: 'Guy' }, 32 | { slug: 'hakan', name: 'Hakan' }, 33 | { slug: 'ibuki', name: 'Ibuki' }, 34 | { slug: 'juri', name: 'Juri' }, 35 | { slug: 'ken', name: 'Ken' }, 36 | { slug: 'm-bison', name: 'M. Bison' }, 37 | { slug: 'makoto', name: 'Makoto' }, 38 | { slug: 'rose', name: 'Rose' }, 39 | { slug: 'rufus', name: 'Rufus' }, 40 | { slug: 'ryu', name: 'Ryu' }, 41 | { slug: 'sagat', name: 'Sagat' }, 42 | { slug: 'sakura', name: 'Sakura' }, 43 | { slug: 'seth', name: 'Seth' }, 44 | { slug: 't-hawk', name: 'T. Hawk' }, 45 | { slug: 'vega', name: 'Vega' }, 46 | { slug: 'yang', name: 'Yang' }, 47 | { slug: 'yun', name: 'Yun' }, 48 | { slug: 'zangief', name: 'Zangief' }, 49 | ]; 50 | 51 | characters.forEach(character => { 52 | it(`${character.name} page exists`, () => { 53 | cy.visit(`/sf4/characters/${character.slug}`).get('h1').contains(character.name); 54 | cy.percySnapshot(character.name); 55 | }); 56 | }); 57 | }); 58 | 59 | context('Countries', () => { 60 | const countries = [ 61 | 'Brazil', 62 | 'China', 63 | 'France', 64 | 'Hong_Kong', 65 | 'India', 66 | 'Italy', 67 | 'Jamaica', 68 | 'Japan', 69 | 'Mexico', 70 | 'None', 71 | 'South_Korea', 72 | 'Spain', 73 | 'Thailand', 74 | 'Turkey', 75 | 'UK', 76 | 'USA', 77 | 'USSR', 78 | ]; 79 | 80 | countries.forEach(country => { 81 | it(`${country} page exists`, () => { 82 | cy.visit(`/sf4/countries/${country}`) 83 | .get('#flag') 84 | .should('have.attr', 'alt') 85 | .then(alt => expect(alt).eq(country)); 86 | cy.percySnapshot(country); 87 | }); 88 | }); 89 | }); 90 | }); 91 | -------------------------------------------------------------------------------- /src/app/layout/nav/nav.component.ts: -------------------------------------------------------------------------------- 1 | import { BreakpointObserver } from '@angular/cdk/layout'; 2 | import { CommonModule } from '@angular/common'; 3 | import { Component, inject, OnInit } from '@angular/core'; 4 | import { FormsModule } from '@angular/forms'; 5 | import { MatButtonModule } from '@angular/material/button'; 6 | import { MatDialog } from '@angular/material/dialog'; 7 | import { MatIconModule } from '@angular/material/icon'; 8 | import { MatInputModule } from '@angular/material/input'; 9 | import { MatListModule } from '@angular/material/list'; 10 | import { MatMenuModule } from '@angular/material/menu'; 11 | import { MatSidenavModule } from '@angular/material/sidenav'; 12 | import { MatToolbarModule } from '@angular/material/toolbar'; 13 | import { ChildrenOutletContexts, RouterLink, RouterOutlet } from '@angular/router'; 14 | import { TranslateModule, TranslateService } from '@ngx-translate/core'; 15 | import { GithubButtonComponent } from 'ng-github-button'; 16 | import { map } from 'rxjs/operators'; 17 | import pkg from '../../../../package.json'; 18 | import { data } from '../../data'; 19 | import { UpdateAppIndicatorComponent } from '../../shared/features/update-app/update-app-indicator/update-app-indicator.component'; 20 | import { ControlsComponent } from './controls/controls.component'; 21 | import { slideInAnimation } from './nav.animations'; 22 | 23 | @Component({ 24 | selector: 'app-nav', 25 | templateUrl: './nav.component.html', 26 | styleUrls: ['./nav.component.scss'], 27 | standalone: true, 28 | imports: [ 29 | CommonModule, 30 | MatSidenavModule, 31 | MatIconModule, 32 | RouterLink, 33 | FormsModule, 34 | MatListModule, 35 | MatButtonModule, 36 | MatInputModule, 37 | TranslateModule, 38 | GithubButtonComponent, 39 | MatToolbarModule, 40 | MatMenuModule, 41 | RouterOutlet, 42 | UpdateAppIndicatorComponent, 43 | ], 44 | animations: [slideInAnimation], 45 | }) 46 | export class NavComponent implements OnInit { 47 | private breakpointObserver = inject(BreakpointObserver); 48 | private dialog = inject(MatDialog); 49 | private translate = inject(TranslateService); 50 | private contexts = inject(ChildrenOutletContexts); 51 | 52 | public characters = data[0].characters; 53 | public filteredCharacters = [...this.characters]; 54 | public version: string = pkg?.version; 55 | public isHandset = false; 56 | public search = ''; 57 | 58 | ngOnInit(): void { 59 | this.breakpointObserver 60 | .observe('(max-width: 739px)') 61 | .pipe(map(result => result.matches)) 62 | .subscribe(isHandset => (this.isHandset = isHandset)); 63 | } 64 | 65 | public openControls(): void { 66 | this.dialog.open(ControlsComponent); 67 | } 68 | 69 | public setLang(lang: 'fr' | 'en'): void { 70 | this.translate.use(lang); 71 | } 72 | 73 | public filterCharacters(search: string): void { 74 | this.filteredCharacters = this.characters.filter(character => character.name.toLowerCase().includes(search.toLowerCase())); 75 | } 76 | 77 | public clearSearch(): void { 78 | this.search = ''; 79 | this.filterCharacters(''); 80 | } 81 | 82 | public getRouteAnimationData(): string | undefined { 83 | return this.contexts.getContext('primary')?.route?.snapshot?.data?.['animation']; 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/app/layout/nav/nav.component.html: -------------------------------------------------------------------------------- 1 | 2 | 10 | 11 | Menu 12 | 13 | 14 | 15 | view_module 16 | {{ 'nav.list' | translate }} 17 | 18 | 19 | 20 | 21 | person_search 22 | 23 | 24 | 27 | 28 | 29 | 35 | 36 | {{ character.name }} 37 | 38 | 39 | 40 | search_off 41 |
{{ 'nav.no-results' | translate }}
42 |
43 | 44 | v {{ version }} 45 | 46 | 47 | 48 |
49 |
50 | 51 | 52 | 55 | {{ 'nav.title' | translate }} 56 | 57 | 58 | 61 | 64 | 65 | 68 | 71 | 72 | 73 |
74 | 75 |
76 |
77 |
78 | -------------------------------------------------------------------------------- /src/assets/countries/USSR.svg: -------------------------------------------------------------------------------- 1 | 2 | 12 | 14 | 15 | 17 | image/svg+xml 18 | 20 | 21 | 22 | 23 | 24 | 26 | 32 | 36 | 40 | 44 | 48 | 49 | 50 | -------------------------------------------------------------------------------- /sitemap-urls.txt: -------------------------------------------------------------------------------- 1 | https://street-fighter-moves.tech-homies.io 2 | https://street-fighter-moves.tech-homies.io/sf4 3 | https://street-fighter-moves.tech-homies.io/sf4/characters/abel 4 | https://street-fighter-moves.tech-homies.io/sf4/characters/adon 5 | https://street-fighter-moves.tech-homies.io/sf4/characters/akuma 6 | https://street-fighter-moves.tech-homies.io/sf4/characters/balrog 7 | https://street-fighter-moves.tech-homies.io/sf4/characters/blanka 8 | https://street-fighter-moves.tech-homies.io/sf4/characters/c-viper 9 | https://street-fighter-moves.tech-homies.io/sf4/characters/cammy 10 | https://street-fighter-moves.tech-homies.io/sf4/characters/chun-li 11 | https://street-fighter-moves.tech-homies.io/sf4/characters/cody 12 | https://street-fighter-moves.tech-homies.io/sf4/characters/dan 13 | https://street-fighter-moves.tech-homies.io/sf4/characters/dee-jay 14 | https://street-fighter-moves.tech-homies.io/sf4/characters/dhalsim 15 | https://street-fighter-moves.tech-homies.io/sf4/characters/dudley 16 | https://street-fighter-moves.tech-homies.io/sf4/characters/e-honda 17 | https://street-fighter-moves.tech-homies.io/sf4/characters/el-fuerte 18 | https://street-fighter-moves.tech-homies.io/sf4/characters/fei-long 19 | https://street-fighter-moves.tech-homies.io/sf4/characters/gen 20 | https://street-fighter-moves.tech-homies.io/sf4/characters/gouken 21 | https://street-fighter-moves.tech-homies.io/sf4/characters/guile 22 | https://street-fighter-moves.tech-homies.io/sf4/characters/guy 23 | https://street-fighter-moves.tech-homies.io/sf4/characters/hakan 24 | https://street-fighter-moves.tech-homies.io/sf4/characters/ibuki 25 | https://street-fighter-moves.tech-homies.io/sf4/characters/juri 26 | https://street-fighter-moves.tech-homies.io/sf4/characters/ken 27 | https://street-fighter-moves.tech-homies.io/sf4/characters/m-bison 28 | https://street-fighter-moves.tech-homies.io/sf4/characters/makoto 29 | https://street-fighter-moves.tech-homies.io/sf4/characters/rose 30 | https://street-fighter-moves.tech-homies.io/sf4/characters/rufus 31 | https://street-fighter-moves.tech-homies.io/sf4/characters/ryu 32 | https://street-fighter-moves.tech-homies.io/sf4/characters/sagat 33 | https://street-fighter-moves.tech-homies.io/sf4/characters/sakura 34 | https://street-fighter-moves.tech-homies.io/sf4/characters/seth 35 | https://street-fighter-moves.tech-homies.io/sf4/characters/t-hawk 36 | https://street-fighter-moves.tech-homies.io/sf4/characters/vega 37 | https://street-fighter-moves.tech-homies.io/sf4/characters/yang 38 | https://street-fighter-moves.tech-homies.io/sf4/characters/yun 39 | https://street-fighter-moves.tech-homies.io/sf4/characters/zangief 40 | https://street-fighter-moves.tech-homies.io/sf4/countries/Brazil 41 | https://street-fighter-moves.tech-homies.io/sf4/countries/China 42 | https://street-fighter-moves.tech-homies.io/sf4/countries/France 43 | https://street-fighter-moves.tech-homies.io/sf4/countries/Hong_Kong 44 | https://street-fighter-moves.tech-homies.io/sf4/countries/India 45 | https://street-fighter-moves.tech-homies.io/sf4/countries/Italy 46 | https://street-fighter-moves.tech-homies.io/sf4/countries/Jamaica 47 | https://street-fighter-moves.tech-homies.io/sf4/countries/Japan 48 | https://street-fighter-moves.tech-homies.io/sf4/countries/Mexico 49 | https://street-fighter-moves.tech-homies.io/sf4/countries/None 50 | https://street-fighter-moves.tech-homies.io/sf4/countries/South_Korea 51 | https://street-fighter-moves.tech-homies.io/sf4/countries/Spain 52 | https://street-fighter-moves.tech-homies.io/sf4/countries/Thailand 53 | https://street-fighter-moves.tech-homies.io/sf4/countries/Turkey 54 | https://street-fighter-moves.tech-homies.io/sf4/countries/UK 55 | https://street-fighter-moves.tech-homies.io/sf4/countries/USA 56 | https://street-fighter-moves.tech-homies.io/sf4/countries/USSR 57 | -------------------------------------------------------------------------------- /angular.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "./node_modules/@angular/cli/lib/config/schema.json", 3 | "version": 1, 4 | "newProjectRoot": "projects", 5 | "projects": { 6 | "street-fighter-moves": { 7 | "projectType": "application", 8 | "schematics": { 9 | "@schematics/angular:component": { 10 | "style": "scss", 11 | "standalone": true 12 | } 13 | }, 14 | "root": "", 15 | "sourceRoot": "src", 16 | "prefix": "app", 17 | "architect": { 18 | "build": { 19 | "builder": "@angular-devkit/build-angular:browser-esbuild", 20 | "options": { 21 | "outputPath": "dist/street-fighter-moves", 22 | "index": "src/index.html", 23 | "main": "src/main.ts", 24 | "polyfills": [ 25 | "zone.js" 26 | ], 27 | "tsConfig": "tsconfig.app.json", 28 | "inlineStyleLanguage": "scss", 29 | "assets": [ 30 | "src/favicon.ico", 31 | "src/assets", 32 | "src/manifest.webmanifest" 33 | ], 34 | "styles": [ 35 | "./node_modules/@angular/material/prebuilt-themes/indigo-pink.css", 36 | "src/styles.scss" 37 | ], 38 | "scripts": [] 39 | }, 40 | "configurations": { 41 | "production": { 42 | "budgets": [ 43 | { 44 | "type": "initial", 45 | "maximumWarning": "500kb", 46 | "maximumError": "1mb" 47 | }, 48 | { 49 | "type": "anyComponentStyle", 50 | "maximumWarning": "2kb", 51 | "maximumError": "4kb" 52 | } 53 | ], 54 | "serviceWorker": true, 55 | "ngswConfigPath": "ngsw-config.json", 56 | "fileReplacements": [ 57 | { 58 | "replace": "src/environments/environment.ts", 59 | "with": "src/environments/environment.prod.ts" 60 | } 61 | ], 62 | "outputHashing": "all" 63 | }, 64 | "development": { 65 | "buildOptimizer": false, 66 | "optimization": false, 67 | "vendorChunk": true, 68 | "extractLicenses": false, 69 | "sourceMap": true, 70 | "namedChunks": true 71 | } 72 | }, 73 | "defaultConfiguration": "production" 74 | }, 75 | "serve": { 76 | "builder": "@angular-devkit/build-angular:dev-server", 77 | "configurations": { 78 | "production": { 79 | "browserTarget": "street-fighter-moves:build:production" 80 | }, 81 | "development": { 82 | "browserTarget": "street-fighter-moves:build:development" 83 | } 84 | }, 85 | "defaultConfiguration": "development" 86 | }, 87 | "extract-i18n": { 88 | "builder": "@angular-devkit/build-angular:extract-i18n", 89 | "options": { 90 | "browserTarget": "street-fighter-moves:build" 91 | } 92 | }, 93 | "test": { 94 | "builder": "@angular-builders/jest:run", 95 | "options": { 96 | "configPath": "src/test/javascript/jest.config.js", 97 | "tsConfig": "tsconfig.spec.json" 98 | } 99 | }, 100 | "lint": { 101 | "builder": "@angular-eslint/builder:lint", 102 | "options": { 103 | "lintFilePatterns": [ 104 | "src/**/*.ts", 105 | "src/**/*.html" 106 | ] 107 | } 108 | }, 109 | "cypress-run": { 110 | "builder": "@cypress/schematic:cypress", 111 | "options": { 112 | "devServerTarget": "street-fighter-moves:serve" 113 | }, 114 | "configurations": { 115 | "production": { 116 | "devServerTarget": "street-fighter-moves:serve:production" 117 | } 118 | } 119 | }, 120 | "cypress-open": { 121 | "builder": "@cypress/schematic:cypress", 122 | "options": { 123 | "watch": true, 124 | "headless": false 125 | } 126 | }, 127 | "e2e": { 128 | "builder": "@cypress/schematic:cypress", 129 | "options": { 130 | "devServerTarget": "street-fighter-moves:serve", 131 | "watch": true, 132 | "headless": false 133 | }, 134 | "configurations": { 135 | "production": { 136 | "devServerTarget": "street-fighter-moves:serve:production" 137 | } 138 | } 139 | } 140 | } 141 | } 142 | }, 143 | "cli": { 144 | "schematicCollections": [ 145 | "@angular-eslint/schematics" 146 | ], 147 | "analytics": "c2a420d1-77d4-4f6d-b861-b4b9265a849f" 148 | }, 149 | "schematics": { 150 | "@angular-eslint/schematics:application": { 151 | "setParserOptionsProject": true 152 | }, 153 | "@angular-eslint/schematics:library": { 154 | "setParserOptionsProject": true 155 | } 156 | } 157 | } 158 | -------------------------------------------------------------------------------- /src/app/pages/character-page/character-moves/character-moves.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 15 | 18 | 19 | 20 | 23 | 29 | 35 | 36 | 37 | 38 | 39 | 48 | 51 | 52 | 53 | 56 | 62 | 68 | 69 | 70 | 71 | 72 | 81 | 84 | 85 | 86 | 89 | 95 | 101 | 102 | 103 | 104 | 105 | 111 | 114 | 115 | 116 | 119 | 125 | 131 | 132 | 133 | 134 |
7 |
8 |
Super
9 |
10 |
{{ move.name }}
11 |
{{ move.originalName }}
12 |
13 |
14 |
16 | 17 |
21 | subdirectory_arrow_right 22 | 24 |
25 |
{{ followUp.name }}
26 |
{{ followUp.originalName }}
27 |
28 |
30 |
31 | control_point 32 | 33 |
34 |
40 |
41 |
Ultra 1
42 |
43 |
{{ move.name }}
44 |
{{ move.originalName }}
45 |
46 |
47 |
49 | 50 |
54 | subdirectory_arrow_right 55 | 57 |
58 |
{{ followUp.name }}
59 |
{{ followUp.originalName }}
60 |
61 |
63 |
64 | control_point 65 | 66 |
67 |
73 |
74 |
Ultra 2
75 |
76 |
{{ move.name }}
77 |
{{ move.originalName }}
78 |
79 |
80 |
82 | 83 |
87 | subdirectory_arrow_right 88 | 90 |
91 |
{{ followUp.name }}
92 |
{{ followUp.originalName }}
93 |
94 |
96 |
97 | control_point 98 | 99 |
100 |
106 |
107 |
{{ move.name }}
108 |
{{ move.originalName }}
109 |
110 |
112 | 113 |
117 | subdirectory_arrow_right 118 | 120 |
121 |
{{ followUp.name }}
122 |
{{ followUp.originalName }}
123 |
124 |
126 |
127 | control_point 128 | 129 |
130 |
135 |
136 | -------------------------------------------------------------------------------- /src/assets/countries/Brazil.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 8 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 21 | 24 | 27 | 28 | 29 | 30 | 31 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | -------------------------------------------------------------------------------- /src/app/data.ts: -------------------------------------------------------------------------------- 1 | export interface Game { 2 | id: number; 3 | name: string; 4 | characters: Character[]; 5 | } 6 | 7 | export interface Character { 8 | id: string; 9 | order: number; 10 | name: string; 11 | stamina: number; 12 | stun: number; 13 | country: string; 14 | moves: Move[]; 15 | } 16 | 17 | export interface Move { 18 | name: string; 19 | originalName?: string; 20 | type?: 'super' | 'ultra1' | 'ultra2'; 21 | motions?: string[]; 22 | followUpList?: Move[]; 23 | } 24 | 25 | export const data: Game[] = [ 26 | { 27 | id: 1, 28 | name: 'Street Fighter IV - Arcade Edition', 29 | characters: [ 30 | { 31 | id: 'ryu', 32 | order: 1, 33 | name: 'Ryu', 34 | stamina: 1000, 35 | stun: 1000, 36 | country: 'Japan', 37 | moves: [ 38 | { 39 | name: 'Fireball', 40 | originalName: 'Hadouken', 41 | motions: ['M↓↘→', 'P'], 42 | }, 43 | { 44 | name: 'Dragon Punch', 45 | originalName: 'Shoryuken', 46 | motions: ['M→↓↘', 'P'], 47 | }, 48 | { 49 | name: 'Hurricane Kick', 50 | originalName: 'Tatsumaki Senpuukyaku', 51 | motions: ['M↓↙←', 'K'], 52 | }, 53 | { 54 | name: 'Air Hurricane Kick', 55 | originalName: '', 56 | motions: ['J', 'M↓↙←', 'K'], 57 | }, 58 | { 59 | name: 'Fireball +', 60 | originalName: 'Shinkuu Hadouken', 61 | motions: ['M↓↘→', 'M↓↘→', 'P'], 62 | type: 'super', 63 | }, 64 | { 65 | name: 'Fireball ++', 66 | originalName: 'Metsu Hadouken', 67 | motions: ['M↓↘→', 'M↓↘→', 'Px3'], 68 | type: 'ultra1', 69 | }, 70 | { 71 | name: 'Dragon Punch ++', 72 | originalName: 'Metsu Shoryuken', 73 | motions: ['M↓↘→', 'M↓↘→', 'Kx3'], 74 | type: 'ultra2', 75 | }, 76 | ], 77 | }, 78 | { 79 | id: 'ken', 80 | order: 2, 81 | name: 'Ken', 82 | stamina: 1000, 83 | stun: 1000, 84 | country: 'USA', 85 | moves: [ 86 | { 87 | name: 'Fireball', 88 | originalName: 'Hadouken', 89 | motions: ['M↓↘→', 'P'], 90 | }, 91 | { 92 | name: 'Dragon Punch', 93 | originalName: 'Shoryuken', 94 | motions: ['M→↓↘', 'P'], 95 | }, 96 | { 97 | name: 'Hurricane Kick', 98 | originalName: 'Tatsumaki Senpuukyaku', 99 | motions: ['M↓↙←', 'K'], 100 | }, 101 | { 102 | name: 'Shoryu-Reppa', 103 | motions: ['M↓↘→', 'M↓↘→', 'P'], 104 | type: 'super', 105 | }, 106 | { 107 | name: 'Shinryuuken', 108 | motions: ['M↓↘→', 'M↓↘→', 'Px3'], 109 | type: 'ultra1', 110 | }, 111 | { 112 | name: 'Guren Senpukyaku', 113 | motions: ['M↓↘→', 'M↓↘→', 'Kx3'], 114 | type: 'ultra2', 115 | }, 116 | ], 117 | }, 118 | { 119 | id: 'e-honda', 120 | order: 3, 121 | name: 'Edmond Honda', 122 | stamina: 1050, 123 | stun: 1100, 124 | country: 'Japan', 125 | moves: [ 126 | { 127 | name: 'Hundred Hand Slap', 128 | motions: ['P', 'P', 'P', 'P'], 129 | }, 130 | { 131 | // *ARMOR BREAKING* 132 | name: 'Sumo Headbutt', 133 | motions: ['MC←→', 'P'], 134 | }, 135 | { 136 | name: 'Sumo Smash', 137 | originalName: 'Hyakkan Otoshi', 138 | motions: ['MC↓↑', 'K'], 139 | }, 140 | { 141 | name: 'Oicho Throw', 142 | originalName: 'Oicho Nage', 143 | motions: ['M→↘↓↙←', 'P'], 144 | }, 145 | { 146 | name: 'Super Killer Head Ram', 147 | originalName: 'oni musou', 148 | motions: ['MC←→', 'M←→', 'P'], 149 | type: 'super', 150 | }, 151 | { 152 | // *ARMOR BREAKING* 153 | name: 'Ultimate Killer Head Ram', 154 | originalName: 'Shin Oni Musou', 155 | motions: ['MC←→', 'M←→', 'Px3'], 156 | type: 'ultra1', 157 | }, 158 | { 159 | name: 'Orochi Breaker', 160 | motions: ['M→↘↓↙←', 'M→↘↓↙←', 'Px3'], 161 | type: 'ultra2', 162 | }, 163 | ], 164 | }, 165 | { 166 | id: 'ibuki', 167 | order: 4, 168 | name: 'Ibuki', 169 | stamina: 900, 170 | stun: 950, 171 | country: 'Japan', 172 | moves: [ 173 | { 174 | name: 'Kunai', 175 | motions: ['J', 'M↓↘→', 'P'], 176 | }, 177 | { 178 | name: 'Tsujigoe', 179 | motions: ['M→↓↘', 'P'], 180 | }, 181 | { 182 | name: 'Neck Breaker', 183 | motions: ['M←↙↓↘→', 'P'], 184 | }, 185 | { 186 | name: 'Raida', 187 | motions: ['M→↘↓↙←', 'P'], 188 | }, 189 | { 190 | name: 'Kasumi Gake', 191 | motions: ['M↓↘→', 'K'], 192 | }, 193 | { 194 | name: 'Kazegiri', 195 | motions: ['M→↓↘', 'K'], 196 | }, 197 | { 198 | name: 'Tsumuji', 199 | motions: ['M↓↙←', 'K'], 200 | }, 201 | { 202 | name: 'Hein', 203 | motions: ['M←↓↙', 'K'], 204 | }, 205 | { 206 | name: 'Kasumi Suzaku', 207 | motions: ['J', 'M↓↘→', 'M↓↘→', 'P'], 208 | type: 'super', 209 | }, 210 | { 211 | name: 'Yoroitoshi', 212 | motions: ['M→↘↓↙←', 'M→↘↓↙←', 'Px3'], 213 | type: 'ultra1', 214 | }, 215 | { 216 | name: 'Hashinsho', 217 | motions: ['M↓↘→', 'M↓↘→', 'Kx3'], 218 | type: 'ultra2', 219 | }, 220 | ], 221 | }, 222 | { 223 | id: 'makoto', 224 | order: 5, 225 | name: 'Makoto', 226 | stamina: 1000, 227 | stun: 1050, 228 | country: 'Japan', 229 | moves: [ 230 | { 231 | name: 'Fukiage', 232 | motions: ['M→↓↘', 'P'], 233 | }, 234 | { 235 | name: 'Hayate', 236 | motions: ['M↓↘→', 'P'], 237 | }, 238 | { 239 | name: 'Oroshi', 240 | motions: ['M↓↙←', 'P'], 241 | }, 242 | { 243 | name: 'Karakusa', 244 | motions: ['M→↘↓↙←', 'K'], 245 | }, 246 | { 247 | name: 'Tsurugi', 248 | motions: ['M↓↙←', 'K'], 249 | }, 250 | { 251 | name: 'Tanden Renki', 252 | motions: ['M↓↘→', 'M↓↘→', 'P'], 253 | type: 'super', 254 | }, 255 | { 256 | name: 'Seichusen Godanzaki', 257 | motions: ['M↓↘→', 'M↓↘→', 'Px3'], 258 | type: 'ultra1', 259 | }, 260 | { 261 | name: 'Abare Tosanami', 262 | motions: ['M↓↘→', 'M↓↘→', 'Kx3'], 263 | type: 'ultra2', 264 | }, 265 | ], 266 | }, 267 | { 268 | id: 'dudley', 269 | order: 6, 270 | name: 'Dudley', 271 | stamina: 1050, 272 | stun: 1050, 273 | country: 'UK', 274 | moves: [ 275 | { 276 | name: 'Jet Upper', 277 | motions: ['M→↓↘', 'P'], 278 | }, 279 | { 280 | name: 'Machine Gun Blow', 281 | motions: ['M←↙↓↘→', 'P'], 282 | }, 283 | { 284 | name: 'Cross Counter', 285 | motions: ['M→↘↓↙←', 'P'], 286 | }, 287 | { 288 | name: 'Short Swing Blow', 289 | motions: ['M→↘↓↙←', 'K'], 290 | }, 291 | { 292 | name: 'Duck', 293 | motions: ['M←↙↓↘→', 'K'], 294 | }, 295 | { 296 | name: 'Ducking Straight', 297 | motions: ['M←↙↓↘→', 'K', 'P'], 298 | }, 299 | { 300 | name: 'Ducking Upper', 301 | motions: ['M←↙↓↘→', 'K', 'K'], 302 | }, 303 | { 304 | name: 'Thunderbolt', 305 | motions: ['MC↓↑', 'K'], 306 | }, 307 | { 308 | name: 'Rocket Upper', 309 | motions: ['M↓↘→', 'M↓↘→', 'P'], 310 | type: 'super', 311 | }, 312 | { 313 | name: 'Rolling Thunder', 314 | motions: ['M↓↘→', 'M↓↘→', 'Kx3'], 315 | type: 'ultra1', 316 | }, 317 | { 318 | name: 'Corkscrew Cross', 319 | motions: ['M↓↘→', 'M↓↘→', 'Px3'], 320 | type: 'ultra2', 321 | }, 322 | ], 323 | }, 324 | { 325 | id: 'seth', 326 | order: 7, 327 | name: 'Seth', 328 | stamina: 800, 329 | stun: 900, 330 | country: 'None', 331 | moves: [ 332 | { 333 | name: 'Sonic Boom', 334 | motions: ['M↓↘→', 'P'], 335 | }, 336 | { 337 | name: 'Shoryuken', 338 | motions: ['M→↓↘', 'P'], 339 | }, 340 | { 341 | name: 'Hyakuretsukyaku', 342 | motions: ['M↓↙←', 'K'], 343 | }, 344 | { 345 | name: 'Tanden Engine', 346 | motions: ['M↓↙←', 'P'], 347 | }, 348 | { 349 | name: 'Spinning Piledriver', 350 | motions: ['M←↙↓↘→↗↑', 'P'], 351 | }, 352 | { 353 | name: 'Yoga Teleport', 354 | followUpList: [ 355 | { 356 | name: 'Approach (far)', 357 | motions: ['M→↓↘', 'Px3'], 358 | }, 359 | { 360 | name: 'Approach (short)', 361 | motions: ['M→↓↘', 'Kx3'], 362 | }, 363 | { 364 | name: 'Retreat (far)', 365 | motions: ['M←↓↙', 'Px3'], 366 | }, 367 | { 368 | name: 'Retreat (short)', 369 | motions: ['M←↓↙', 'Kx3'], 370 | }, 371 | ], 372 | }, 373 | { 374 | name: 'Tanden Storm', 375 | motions: ['M↓↘→', 'M↓↘→', 'P'], 376 | type: 'super', 377 | }, 378 | { 379 | name: 'Tanden Stream', 380 | motions: ['M↓↘→', 'M↓↘→', 'Px3'], 381 | type: 'ultra1', 382 | }, 383 | { 384 | name: 'Tanden Typhoon', 385 | motions: ['M↓↙←', 'M↓↙←', 'Px3'], 386 | type: 'ultra2', 387 | }, 388 | ], 389 | }, 390 | { 391 | id: 'gouken', 392 | order: 8, 393 | name: 'Gouken', 394 | stamina: 1000, 395 | stun: 1000, 396 | country: 'Japan', 397 | moves: [ 398 | { 399 | name: 'Gohadoken', 400 | // 💡 : Chargeable 401 | // 💡 : 1 P = 1 direction 402 | motions: ['M↓↘→', 'P'], 403 | }, 404 | { 405 | name: 'Senkugoshoha', 406 | motions: ['M→↓↘', 'P'], 407 | }, 408 | { 409 | name: 'Tatsumaki Gorasen', 410 | motions: ['M↓↙←', 'K'], 411 | }, 412 | { 413 | name: 'Hyakkishu', 414 | motions: ['M→↓↘', 'K'], 415 | }, 416 | { 417 | name: 'Kongoshin', 418 | // 💡 : K or P 419 | motions: ['M←↓↙', 'K'], 420 | }, 421 | { 422 | name: 'Forbidden Shoryuken', 423 | motions: ['M↓↘→', 'M↓↘→', 'P'], 424 | type: 'super', 425 | }, 426 | { 427 | name: 'Shin Shoryuken', 428 | motions: ['M↓↘→', 'M↓↘→', 'Px3'], 429 | type: 'ultra1', 430 | }, 431 | { 432 | // 💡 : inflict heavy stun instead of damage 433 | name: 'Denjin Hadouken', 434 | motions: ['M↓↘→', 'M↓↘→', 'Kx3'], 435 | type: 'ultra2', 436 | }, 437 | ], 438 | }, 439 | { 440 | id: 'akuma', 441 | order: 9, 442 | name: 'Akuma', 443 | stamina: 850, 444 | stun: 850, 445 | country: 'Japan', 446 | moves: [ 447 | { 448 | name: 'Fireball', 449 | originalName: 'Gou Hadouken', 450 | motions: ['M↓↘→', 'P'], 451 | }, 452 | { 453 | name: 'Air Fireball (↘)', 454 | originalName: 'Gou Hadouken', 455 | motions: ['J', 'M↓↘→', 'P'], 456 | }, 457 | { 458 | name: 'Multi-hit Fireball', 459 | originalName: 'Shakunetsu Hadouken', 460 | motions: ['M→↘↓↙←', 'P'], 461 | }, 462 | { 463 | name: 'Dragon Punch', 464 | originalName: 'Gou Shoryuken', 465 | motions: ['M→↓↘', 'P'], 466 | }, 467 | { 468 | name: 'Hurricane Kick', 469 | originalName: 'Tatsumaki Zankukyaku', 470 | motions: ['M↓↙←', 'K'], 471 | }, 472 | { 473 | name: 'Demon Flip', 474 | originalName: 'Hyakki Shu', 475 | motions: ['M→↓↘', 'K'], 476 | }, 477 | { 478 | name: 'Teleport', 479 | originalName: 'Ahura Senku', 480 | followUpList: [ 481 | { 482 | name: 'Approach (far)', 483 | motions: ['M→↓↘', 'Px3'], 484 | }, 485 | { 486 | name: 'Approach (short)', 487 | motions: ['M→↓↘', 'Kx3'], 488 | }, 489 | { 490 | name: 'Retreat (far)', 491 | motions: ['M←↓↙', 'Px3'], 492 | }, 493 | { 494 | name: 'Retreat (short)', 495 | motions: ['M←↓↙', 'Kx3'], 496 | }, 497 | ], 498 | }, 499 | { 500 | name: 'Raging Demon', 501 | originalName: 'Shun Goku Satsu', 502 | motions: ['P1', 'P1', 'M→', '[', 'K1', 'P3', ']'], 503 | type: 'super', 504 | }, 505 | { 506 | name: 'Wrath of the Raging Demon', 507 | originalName: 'Shin Shun Goku Satsu', 508 | motions: ['P1', 'P1', 'M←', '[', 'K1', 'P3', ']'], 509 | type: 'ultra1', 510 | }, 511 | { 512 | name: 'Demon Armageddon', 513 | motions: ['M↑', 'M↑', 'Kx3'], 514 | type: 'ultra2', 515 | }, 516 | ], 517 | }, 518 | { 519 | id: 'gen', 520 | order: 10, 521 | name: 'Gen', 522 | stamina: 900, 523 | stun: 900, 524 | country: 'China', 525 | // TODO: Gen has 2 styles of battle 526 | // TODO: need to change data structure 527 | moves: [ 528 | // 🥷 : Mantis moves 529 | { 530 | name: 'Mantis Style', 531 | motions: ['Px3'], 532 | }, 533 | { 534 | name: 'Gekiro', 535 | originalName: 'Mantis only', 536 | motions: ['M→↓↘', 'K'], 537 | }, 538 | { 539 | name: 'Rapid Hands', 540 | originalName: 'Mantis only', 541 | motions: ['P', 'P', 'P', 'P'], 542 | }, 543 | { 544 | name: `Zan 'Ei`, 545 | originalName: 'Mantis only', 546 | motions: ['M↓↘→', 'M↓↘→', 'P'], 547 | type: 'super', 548 | }, 549 | { 550 | name: 'Zetsuei', 551 | originalName: 'Mantis only', 552 | motions: ['M↓↘→', 'M↓↘→', 'Px3'], 553 | type: 'ultra1', 554 | }, 555 | { 556 | name: 'Shitkenketsu', 557 | originalName: 'Mantis only', 558 | motions: ['M↓↙←', 'M↓↙←', 'Px3'], 559 | type: 'ultra2', 560 | }, 561 | // 🥷 : Crane moves 562 | { 563 | name: 'Crane Style', 564 | motions: ['Kx3'], 565 | }, 566 | { 567 | name: 'Jyasen', 568 | originalName: 'Crane only', 569 | motions: ['MC←→', 'P'], 570 | }, 571 | { 572 | // OGA + BUTTON = Special attack 573 | // Sudden Stop: Back after Oga 574 | // Close Kick: Down after Oga 575 | // Far Kick: Down-Forward or Forward after Oga 576 | // Ceiling Jump: Up-Back or Up or Up-Forward after Oga 577 | // Falling Kick: Down After Ceiling Jump 578 | // Close Range Kick Right: Down-forward or Forward after Ceiling Jump 579 | // Close Range Kick Left: Down-Back or Back after Ceiling Jump 580 | name: 'Oga', 581 | originalName: 'Crane only', 582 | motions: ['MC↓↑', 'K'], 583 | }, 584 | { 585 | name: 'Jyakoha', 586 | originalName: 'Crane only', 587 | motions: ['M↓↘→', 'M↓↘→', 'K'], 588 | type: 'super', 589 | }, 590 | { 591 | name: 'Ryukoha', 592 | originalName: 'Crane only', 593 | motions: ['M↓↘→', 'M↓↘→', 'Kx3'], 594 | type: 'ultra1', 595 | }, 596 | { 597 | name: 'Teiga', 598 | originalName: 'Crane only', 599 | motions: ['J', 'M↓↘→', 'M↓↘→', 'Kx3'], 600 | type: 'ultra2', 601 | }, 602 | ], 603 | }, 604 | { 605 | id: 'dan', 606 | order: 11, 607 | name: 'Dan', 608 | stamina: 1000, 609 | stun: 900, 610 | country: 'Hong_Kong', 611 | moves: [ 612 | { 613 | name: 'Fireball', 614 | originalName: 'GADOKEN', 615 | motions: ['M↓↘→', 'P'], 616 | }, 617 | { 618 | name: 'Dragon Punch', 619 | originalName: 'KORYUKEN', 620 | motions: ['M→↓↘', 'P'], 621 | }, 622 | { 623 | // *ARMOR BREAKING* 624 | name: 'Dan Kicks', 625 | originalName: 'DANKUKYAKU', 626 | motions: ['M↓↙←', 'K'], 627 | }, 628 | { 629 | // *ARMOR BREAKING* 630 | name: 'Dan Kicks', 631 | originalName: 'DANKUKYAKU', 632 | motions: ['J', 'M↓↙←', 'K'], 633 | }, 634 | { 635 | name: 'HISSHO BURAIKEN', 636 | motions: ['M↓↘→', 'M↓↘→', 'P'], 637 | type: 'super', 638 | }, 639 | { 640 | name: 'LEGENDARY TAUNT', 641 | // TODO: P3 + K3 in same time 642 | motions: ['M↓↘→', 'M↓↘→', 'P3', 'K3'], 643 | type: 'super', 644 | }, 645 | { 646 | name: 'SHISSO BURAIKEN', 647 | motions: ['M↓↘→', 'M↓↘→', 'Px3'], 648 | type: 'ultra1', 649 | }, 650 | { 651 | name: 'HAOH GADOUKEN', 652 | motions: ['M↓↘→', 'M↓↘→', 'Kx3'], 653 | type: 'ultra2', 654 | }, 655 | ], 656 | }, 657 | { 658 | id: 'sakura', 659 | order: 12, 660 | name: 'Sakura', 661 | stamina: 950, 662 | stun: 1000, 663 | country: 'Japan', 664 | moves: [ 665 | { 666 | name: 'Fireball', 667 | originalName: 'Hadouken', 668 | motions: ['M↓↘→', 'P'], 669 | }, 670 | { 671 | name: 'Dragon Punch', 672 | originalName: 'Shouoken', 673 | motions: ['M→↓↘', 'P'], 674 | }, 675 | { 676 | name: 'Hurricane Kick', 677 | originalName: 'Shunpuu Kyaku', 678 | motions: ['M↓↙←', 'K'], 679 | }, 680 | { 681 | name: 'Sakura Otoshi', 682 | motions: ['M→↓↘', 'K'], 683 | }, 684 | { 685 | name: 'Haru Ichiban', 686 | motions: ['M↓↙←', 'M↓↙←', 'K'], 687 | type: 'super', 688 | }, 689 | { 690 | name: 'Haru Ranman', 691 | motions: ['M↓↙←', 'M↓↙←', 'Kx3'], 692 | type: 'ultra1', 693 | }, 694 | { 695 | name: 'Ultra 2', 696 | motions: ['M↓↘→', 'M↓↘→'], 697 | followUpList: [ 698 | { 699 | name: 'shinku hadouken (→)', 700 | motions: ['Px3'], 701 | }, 702 | { 703 | name: 'tengyo hadouken (↗)', 704 | motions: ['Kx3'], 705 | }, 706 | ], 707 | type: 'ultra2', 708 | }, 709 | ], 710 | }, 711 | { 712 | id: 'yun', 713 | order: 13, 714 | name: 'Yun', 715 | stamina: 900, 716 | stun: 1000, 717 | country: 'Hong_Kong', 718 | moves: [], 719 | }, 720 | { 721 | id: 'juri', 722 | order: 14, 723 | name: 'Juri', 724 | stamina: 950, 725 | stun: 950, 726 | country: 'South_Korea', 727 | moves: [], 728 | }, 729 | { 730 | id: 'chun-li', 731 | order: 15, 732 | name: 'Chun-Li', 733 | stamina: 900, 734 | stun: 1050, 735 | country: 'China', 736 | moves: [ 737 | { 738 | name: 'Kikkoken', 739 | motions: ['MC←→', 'P'], 740 | }, 741 | { 742 | name: 'Lightning Legs', 743 | originalName: 'Hyakuretsu Kyaku', 744 | motions: ['K', 'K', 'K', 'K'], 745 | }, 746 | { 747 | name: 'Spinning Bird Kick', 748 | motions: ['MC↓↑', 'K'], 749 | }, 750 | { 751 | name: 'Overhead Flip Kick', 752 | originalName: 'Hazan Shu', 753 | motions: ['M→↘↓↙←', 'K'], 754 | }, 755 | { 756 | name: 'Senretsukyaku', 757 | motions: ['MC←→', 'M←→', 'K'], 758 | type: 'super', 759 | }, 760 | { 761 | name: 'Housenka', 762 | motions: ['MC←→', 'M←→', 'Kx3'], 763 | type: 'ultra1', 764 | }, 765 | { 766 | name: 'Kikosho', 767 | motions: ['M↓↘→', 'M↓↘→', 'Px3'], 768 | type: 'ultra2', 769 | }, 770 | ], 771 | }, 772 | { 773 | id: 'dhalsim', 774 | order: 16, 775 | name: 'Dhalsim', 776 | stamina: 900, 777 | stun: 900, 778 | country: 'India', 779 | moves: [ 780 | { 781 | name: 'Yoga Inferno', 782 | motions: ['M↓↘→', 'M↓↘→', 'P'], 783 | type: 'super', 784 | }, 785 | { 786 | name: 'Yoga Catasrophe', 787 | motions: ['M↓↘→', 'M↓↘→', 'Px3'], 788 | type: 'ultra1', 789 | }, 790 | { 791 | name: 'Yoga Shangri-La', 792 | motions: ['J', 'M↓↘→', 'M↓↘→', 'Kx3'], 793 | type: 'ultra2', 794 | }, 795 | ], 796 | }, 797 | { 798 | id: 'abel', 799 | order: 17, 800 | name: 'Abel', 801 | stamina: 1050, 802 | stun: 1050, 803 | country: 'France', 804 | moves: [], 805 | }, 806 | { 807 | id: 'c-viper', 808 | order: 18, 809 | name: 'Crimson Viper', 810 | stamina: 900, 811 | stun: 950, 812 | country: 'USA', 813 | moves: [], 814 | }, 815 | { 816 | id: 'm-bison', 817 | order: 19, 818 | name: 'M. Bison', 819 | stamina: 1000, 820 | stun: 950, 821 | country: 'None', 822 | moves: [], 823 | }, 824 | { 825 | id: 'sagat', 826 | order: 20, 827 | name: 'Sagat', 828 | stamina: 1050, 829 | stun: 1000, 830 | country: 'Thailand', 831 | moves: [ 832 | { 833 | name: 'Tiger Uppercut', 834 | motions: ['M→↓↘', 'P'], 835 | }, 836 | { 837 | name: 'Tiger Knee', 838 | motions: ['M→↓↘', 'K'], 839 | }, 840 | { 841 | name: 'Tiger Shot (High)', 842 | motions: ['M↓↘→', 'P'], 843 | }, 844 | { 845 | name: 'Tiger Shot (Low)\n', 846 | motions: ['M↓↘→', 'K'], 847 | }, 848 | { 849 | name: 'Angry Scar', 850 | motions: ['M↓↙←', 'M↓↙←', 'K'], 851 | }, 852 | { 853 | name: 'Tiger Genocide', 854 | motions: ['M↓↘→', 'M↓↘→', 'K'], 855 | type: 'super', 856 | }, 857 | { 858 | name: 'Tiger Destruction', 859 | motions: ['M↓↘→', 'M↓↘→', 'Kx3'], 860 | type: 'ultra1', 861 | }, 862 | { 863 | name: 'Tiger Cannon', 864 | motions: ['M↓↘→', 'M↓↘→', 'Px3'], 865 | type: 'ultra2', 866 | }, 867 | ], 868 | }, 869 | { 870 | id: 'cammy', 871 | order: 21, 872 | name: 'Cammy', 873 | stamina: 950, 874 | stun: 950, 875 | country: 'UK', 876 | moves: [], 877 | }, 878 | { 879 | id: 'dee-jay', 880 | order: 22, 881 | name: 'Dee Jay', 882 | stamina: 1000, 883 | stun: 1000, 884 | country: 'Jamaica', 885 | moves: [ 886 | { 887 | name: 'Air Slasher', 888 | motions: ['MC←→', 'P'], 889 | }, 890 | { 891 | name: 'Double Rolling Sobat', 892 | motions: ['MC←→', 'K'], 893 | }, 894 | { 895 | name: 'Jackknife Maximum', 896 | motions: ['MC↓↑', 'K'], 897 | }, 898 | { 899 | name: 'Machinegun Upper', 900 | motions: ['MC↓↑', 'P'], 901 | }, 902 | { 903 | name: 'Sobat Carnival', 904 | motions: ['MC←→', 'M←→', 'K'], 905 | type: 'super', 906 | }, 907 | { 908 | name: 'Sobat Festival', 909 | motions: ['MC←→', 'M←→', 'Kx3'], 910 | type: 'ultra1', 911 | }, 912 | { 913 | name: 'Climax Beat', 914 | motions: ['MC↙↘', 'M↙↗', 'Px3'], 915 | type: 'ultra2', 916 | }, 917 | ], 918 | }, 919 | { 920 | id: 'cody', 921 | order: 23, 922 | name: 'Cody', 923 | stamina: 1000, 924 | stun: 1050, 925 | country: 'USA', 926 | moves: [], 927 | }, 928 | { 929 | id: 'guy', 930 | order: 24, 931 | name: 'Guy', 932 | stamina: 1000, 933 | stun: 950, 934 | country: 'USA', 935 | moves: [ 936 | { 937 | name: 'Bushin Senpukyaku', 938 | motions: ['M↓↙←', 'K'], 939 | }, 940 | { 941 | name: 'Bushin Izuna Otoshi', 942 | motions: ['M↓↘→', 'P'], 943 | }, 944 | { 945 | name: 'Hozanto', 946 | motions: ['M↓↙←', 'P'], 947 | }, 948 | { 949 | name: 'Run', 950 | motions: ['M↓↘→', 'K'], 951 | followUpList: [ 952 | { 953 | name: 'Sudden Stop', 954 | motions: ['K1'], 955 | }, 956 | { 957 | name: 'Slide', 958 | motions: ['K2'], 959 | }, 960 | { 961 | name: '2 Hits Attack', 962 | motions: ['K3'], 963 | }, 964 | ], 965 | }, 966 | { 967 | name: 'Bushin Hasoken', 968 | motions: ['M↓↘→', 'M↓↘→', 'P'], 969 | type: 'super', 970 | }, 971 | { 972 | name: 'Bushin Goraisenpujin', 973 | motions: ['M↓↘→', 'M↓↘→', 'Kx3'], 974 | type: 'ultra1', 975 | }, 976 | { 977 | name: 'Bushin Muso Renge', 978 | motions: ['M→↘↓↙←', 'M→↘↓↙←', 'Px3'], 979 | type: 'ultra2', 980 | }, 981 | ], 982 | }, 983 | { 984 | id: 'hakan', 985 | order: 25, 986 | name: 'Hakan', 987 | stamina: 1050, 988 | stun: 1100, 989 | country: 'Turkey', 990 | moves: [], 991 | }, 992 | { 993 | id: 'yang', 994 | order: 26, 995 | name: 'Yang', 996 | stamina: 900, 997 | stun: 1000, 998 | country: 'Hong_Kong', 999 | moves: [], 1000 | }, 1001 | { 1002 | id: 'guile', 1003 | order: 27, 1004 | name: 'Guile', 1005 | stamina: 1000, 1006 | stun: 900, 1007 | country: 'USA', 1008 | moves: [ 1009 | { 1010 | name: 'Sonic Boom', 1011 | motions: ['MC←→', 'P'], 1012 | }, 1013 | { 1014 | // *ARMOR BREAKING* 1015 | name: 'Flash Kick', 1016 | motions: ['MC↓↑', 'K'], 1017 | }, 1018 | { 1019 | name: 'Double Flashkick', 1020 | motions: ['MC↙↘', 'M↙↗', 'K'], 1021 | type: 'super', 1022 | }, 1023 | { 1024 | name: 'Flash Explosion', 1025 | motions: ['MC↙↘', 'M↙↗', 'Kx3'], 1026 | type: 'ultra1', 1027 | }, 1028 | { 1029 | name: 'Sonic Hurricane', 1030 | motions: ['MC←→', 'M←→', 'Px3'], 1031 | type: 'ultra2', 1032 | }, 1033 | ], 1034 | }, 1035 | { 1036 | id: 'blanka', 1037 | order: 28, 1038 | name: 'Blanka', 1039 | stamina: 1000, 1040 | stun: 950, 1041 | country: 'Brazil', 1042 | moves: [], 1043 | }, 1044 | { 1045 | id: 'zangief', 1046 | order: 29, 1047 | name: 'Zangief', 1048 | stamina: 1100, 1049 | stun: 1100, 1050 | country: 'USSR', 1051 | moves: [ 1052 | { 1053 | name: 'Spinning Pile Driver', 1054 | motions: ['M←↙↓↘→↗↑', 'P'], 1055 | }, 1056 | { 1057 | name: 'Banishing Flat', 1058 | motions: ['M→↓↘', 'P'], 1059 | }, 1060 | { 1061 | name: 'Spinning Lariat', 1062 | motions: ['Px3'], 1063 | }, 1064 | { 1065 | name: 'Quick Lariat', 1066 | motions: ['Kx3'], 1067 | }, 1068 | { 1069 | name: 'Final Atomic Buster', 1070 | motions: ['M←↙↓↘→↗↑', 'M←↙↓↘→↗↑', 'K'], 1071 | type: 'super', 1072 | }, 1073 | { 1074 | name: 'Ultra Atomic Buster', 1075 | motions: ['M←↙↓↘→↗↑', 'M←↙↓↘→↗↑', 'Px3'], 1076 | type: 'ultra1', 1077 | }, 1078 | { 1079 | name: 'Siberian Blizzard', 1080 | motions: ['M←↙↓↘→↗↑', 'M←↙↓↘→↗↑', 'Kx3'], 1081 | type: 'ultra2', 1082 | }, 1083 | ], 1084 | }, 1085 | { 1086 | id: 'rufus', 1087 | order: 30, 1088 | name: 'Rufus', 1089 | stamina: 1050, 1090 | stun: 950, 1091 | country: 'USA', 1092 | moves: [], 1093 | }, 1094 | { 1095 | id: 'el-fuerte', 1096 | order: 31, 1097 | name: 'El Fuerte', 1098 | stamina: 900, 1099 | stun: 1000, 1100 | country: 'Mexico', 1101 | moves: [], 1102 | }, 1103 | { 1104 | id: 'vega', 1105 | order: 32, 1106 | name: 'Vega', 1107 | stamina: 1000, 1108 | stun: 900, 1109 | country: 'Spain', 1110 | moves: [ 1111 | { 1112 | name: 'Rolling Crystal Flash', 1113 | motions: ['MC←→', 'P'], 1114 | }, 1115 | { 1116 | // *ARMOR BREAKING* 1117 | name: 'Scarlet Terror', 1118 | motions: ['MC↙→', 'K'], 1119 | }, 1120 | { 1121 | // *EX HAS ARMOR BREAKING* 1122 | name: 'Sky High Claw', 1123 | motions: ['MC↓↑', 'P'], 1124 | }, 1125 | { 1126 | name: 'Flying Barcelona Attack', 1127 | motions: ['MC↓↑', 'K'], 1128 | }, 1129 | { 1130 | name: 'Short Back Flip', 1131 | motions: ['Kx3'], 1132 | }, 1133 | { 1134 | name: 'Back Flip', 1135 | motions: ['Px3'], 1136 | }, 1137 | { 1138 | name: 'Flying Barcelona Special', 1139 | motions: ['MC↙↘', 'M↙↗', 'K'], 1140 | type: 'super', 1141 | }, 1142 | { 1143 | name: 'Bloody High Claw', 1144 | motions: ['MC↙↘', 'M↙↗', 'Kx3'], 1145 | type: 'ultra1', 1146 | }, 1147 | { 1148 | name: 'Splendid Claw', 1149 | motions: ['MC↙→', 'M←→', 'Kx3'], 1150 | type: 'ultra2', 1151 | }, 1152 | ], 1153 | }, 1154 | { 1155 | id: 'balrog', 1156 | order: 33, 1157 | name: 'Balrog', 1158 | stamina: 1050, 1159 | stun: 1000, 1160 | country: 'USA', 1161 | moves: [], 1162 | }, 1163 | { 1164 | id: 'fei-long', 1165 | order: 34, 1166 | name: 'Fei Long', 1167 | stamina: 1000, 1168 | stun: 1050, 1169 | country: 'Hong_Kong', 1170 | moves: [], 1171 | }, 1172 | { 1173 | id: 't-hawk', 1174 | order: 35, 1175 | name: 'T. Hawk', 1176 | stamina: 1100, 1177 | stun: 1100, 1178 | country: 'Mexico', 1179 | moves: [], 1180 | }, 1181 | { 1182 | id: 'adon', 1183 | order: 36, 1184 | name: 'Adon', 1185 | stamina: 950, 1186 | stun: 1000, 1187 | country: 'Thailand', 1188 | moves: [], 1189 | }, 1190 | { 1191 | id: 'rose', 1192 | order: 37, 1193 | name: 'Rose', 1194 | stamina: 950, 1195 | stun: 1000, 1196 | country: 'Italy', 1197 | moves: [], 1198 | }, 1199 | ], 1200 | }, 1201 | ]; 1202 | 1203 | export const maxStamina = (gameId: number): number => 1204 | data.find(g => g.id === gameId)?.characters.reduce((acc, c) => Math.max(acc, c.stamina), 0) ?? 0; 1205 | 1206 | export const maxStun = (gameId: number): number => 1207 | data.find(g => g.id === gameId)?.characters.reduce((acc, c) => Math.max(acc, c.stun), 0) ?? 0; 1208 | 1209 | export const countries = (gameId: number): string[] => data.find(g => g.id === gameId)?.characters.map(c => c.country) ?? []; 1210 | --------------------------------------------------------------------------------