├── .prettierrc.json ├── src ├── resources │ ├── js │ │ ├── update_history │ │ │ └── main_update_history.js │ │ ├── quick_match │ │ │ ├── quick_match_server_url.template.js │ │ │ ├── match_group.js │ │ │ └── quick_match.js │ │ ├── utils │ │ │ ├── hash_code.js │ │ │ ├── mod.js │ │ │ ├── input_conversion.js │ │ │ ├── is_local_storage_available.js │ │ │ └── generate_pushid.js │ │ ├── replay │ │ │ ├── main_replay.js │ │ │ ├── replay_saver.js │ │ │ ├── pikavolley_replay.js │ │ │ ├── replay_player.js │ │ │ └── ui_replay.js │ │ ├── data_channel │ │ │ ├── firebase_config.template.js │ │ │ ├── rtc_configuration.js │ │ │ ├── parse_candidate.js │ │ │ └── network_test.js │ │ ├── offline_version_js │ │ │ ├── utils │ │ │ │ ├── local_storage_wrapper.js │ │ │ │ ├── is_embedded_in_other_website.js │ │ │ │ └── dark_color_scheme.js │ │ │ ├── rand.js │ │ │ ├── assets_path.js │ │ │ ├── cloud_and_wave.js │ │ │ ├── audio.js │ │ │ ├── keyboard.js │ │ │ └── main.js │ │ ├── nickname_display.js │ │ ├── bad_words_filtering │ │ │ ├── chat_filter.js │ │ │ ├── bad_word_list.js │ │ │ └── ui.js │ │ ├── block_other_players │ │ │ ├── blocked_ip_list.js │ │ │ └── ui.js │ │ ├── chat_display.js │ │ ├── pikavolley_online.js │ │ ├── main_online.js │ │ └── keyboard_online.js │ └── assets │ │ ├── sounds │ │ ├── bgm.mp3 │ │ ├── WAVE140_1.wav │ │ ├── WAVE141_1.wav │ │ ├── WAVE142_1.wav │ │ ├── WAVE143_1.wav │ │ ├── WAVE144_1.wav │ │ ├── WAVE145_1.wav │ │ └── WAVE146_1.wav │ │ └── images │ │ ├── screenshot.png │ │ ├── IDI_PIKAICON-0.png │ │ ├── sprite_sheet.png │ │ ├── controls_online.png │ │ ├── controls_online_ko.png │ │ ├── IDI_PIKAICON-1_gap_filled.png │ │ ├── IDI_PIKAICON-1_gap_filled_192.png │ │ └── IDI_PIKAICON-1_gap_filled_512.png ├── index.html ├── ko │ ├── ko.js │ ├── replay │ │ └── index.html │ └── update-history │ │ └── index.html └── en │ ├── replay │ └── index.html │ └── update-history │ └── index.html ├── .gitignore ├── jsconfig.json ├── webpack.prod.js ├── webpack.dev.js ├── .vscode └── settings.json ├── .eslintrc.json ├── package.json ├── README.ko.md ├── README.md └── webpack.common.js /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true 3 | } 4 | -------------------------------------------------------------------------------- /src/resources/js/update_history/main_update_history.js: -------------------------------------------------------------------------------- 1 | import '../../style.css'; 2 | -------------------------------------------------------------------------------- /src/resources/assets/sounds/bgm.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gorisanson/pikachu-volleyball-p2p-online/HEAD/src/resources/assets/sounds/bgm.mp3 -------------------------------------------------------------------------------- /src/resources/assets/sounds/WAVE140_1.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gorisanson/pikachu-volleyball-p2p-online/HEAD/src/resources/assets/sounds/WAVE140_1.wav -------------------------------------------------------------------------------- /src/resources/assets/sounds/WAVE141_1.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gorisanson/pikachu-volleyball-p2p-online/HEAD/src/resources/assets/sounds/WAVE141_1.wav -------------------------------------------------------------------------------- /src/resources/assets/sounds/WAVE142_1.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gorisanson/pikachu-volleyball-p2p-online/HEAD/src/resources/assets/sounds/WAVE142_1.wav -------------------------------------------------------------------------------- /src/resources/assets/sounds/WAVE143_1.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gorisanson/pikachu-volleyball-p2p-online/HEAD/src/resources/assets/sounds/WAVE143_1.wav -------------------------------------------------------------------------------- /src/resources/assets/sounds/WAVE144_1.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gorisanson/pikachu-volleyball-p2p-online/HEAD/src/resources/assets/sounds/WAVE144_1.wav -------------------------------------------------------------------------------- /src/resources/assets/sounds/WAVE145_1.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gorisanson/pikachu-volleyball-p2p-online/HEAD/src/resources/assets/sounds/WAVE145_1.wav -------------------------------------------------------------------------------- /src/resources/assets/sounds/WAVE146_1.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gorisanson/pikachu-volleyball-p2p-online/HEAD/src/resources/assets/sounds/WAVE146_1.wav -------------------------------------------------------------------------------- /src/resources/assets/images/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gorisanson/pikachu-volleyball-p2p-online/HEAD/src/resources/assets/images/screenshot.png -------------------------------------------------------------------------------- /src/resources/assets/images/IDI_PIKAICON-0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gorisanson/pikachu-volleyball-p2p-online/HEAD/src/resources/assets/images/IDI_PIKAICON-0.png -------------------------------------------------------------------------------- /src/resources/assets/images/sprite_sheet.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gorisanson/pikachu-volleyball-p2p-online/HEAD/src/resources/assets/images/sprite_sheet.png -------------------------------------------------------------------------------- /src/resources/assets/images/controls_online.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gorisanson/pikachu-volleyball-p2p-online/HEAD/src/resources/assets/images/controls_online.png -------------------------------------------------------------------------------- /src/resources/assets/images/controls_online_ko.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gorisanson/pikachu-volleyball-p2p-online/HEAD/src/resources/assets/images/controls_online_ko.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | dist/ 2 | node_modules/ 3 | firebase_config.js 4 | quick_match_server_url.js 5 | analytics.html 6 | .firebaserc 7 | firebase.json 8 | .firebase/ 9 | yarn.lock 10 | 11 | -------------------------------------------------------------------------------- /src/resources/assets/images/IDI_PIKAICON-1_gap_filled.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gorisanson/pikachu-volleyball-p2p-online/HEAD/src/resources/assets/images/IDI_PIKAICON-1_gap_filled.png -------------------------------------------------------------------------------- /src/resources/assets/images/IDI_PIKAICON-1_gap_filled_192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gorisanson/pikachu-volleyball-p2p-online/HEAD/src/resources/assets/images/IDI_PIKAICON-1_gap_filled_192.png -------------------------------------------------------------------------------- /src/resources/assets/images/IDI_PIKAICON-1_gap_filled_512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gorisanson/pikachu-volleyball-p2p-online/HEAD/src/resources/assets/images/IDI_PIKAICON-1_gap_filled_512.png -------------------------------------------------------------------------------- /jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "es6", 4 | "target": "es6", 5 | "moduleResolution": "node", 6 | "checkJs": true 7 | }, 8 | "include": ["src/**/*"] 9 | } 10 | -------------------------------------------------------------------------------- /webpack.prod.js: -------------------------------------------------------------------------------- 1 | const { merge } = require('webpack-merge'); 2 | const common = require('./webpack.common.js'); 3 | 4 | module.exports = merge(common, { 5 | mode: 'production', 6 | devtool: 'source-map', 7 | }); 8 | -------------------------------------------------------------------------------- /src/resources/js/quick_match/quick_match_server_url.template.js: -------------------------------------------------------------------------------- 1 | // Set the following to proper server URL, 2 | // and rename this file to "quick_match_server_url.js". 3 | export const serverURL = 'http://localhost:3000'; 4 | -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
12 |
13 | ## 구조
14 |
15 | - 오프라인 버전: 오프라인 웹 버전의 소스 코드 파일이 모두 [`src/resources/js/offline_version_js/`](src/resources/js/offline_version_js)에 담겨 있습니다. https://github.com/gorisanson/pikachu-volleyball/tree/main/src/resources/js 에 있는 소스 코드 파일과 동일한 것입니다. 이를 기반으로 온라인 버전을 만들었습니다.
16 |
17 | - WebRTC data channels: WebRTC data channels를 이용한 P2P 온라인 핵심 기능들이 [`src/resources/js/data_channel/data_channel.js`](src/resources/js/data_channel/data_channel.js)에 담겨 있습니다. (WebRTC로 P2P 연결을 맺기 위한 매개 수단으로 [Firebase Cloud Firestore](https://firebase.google.com/docs/firestore)를 사용합니다. 방장이 방장의 친구에게 보내는 방 ID가 서로 공유하는 Cloud Firestore document의 ID입니다. [Firebase + WebRTC Codelab](https://webrtc.org/getting-started/firebase-rtc-codelab) 및 [https://github.com/webrtc/FirebaseRTC](https://github.com/webrtc/FirebaseRTC)에서 사용한 방식을 거의 그대로 이용한 것입니다.)
18 |
19 | - 퀵 매치: 퀵 매치 서버와 통신하는 기능이 [`src/resources/js/quick_match/quick_match.js`](src/resources/js/quick_match/quick_match.js)에 담겨 있습니다. (퀵 매치 서버로는 [Google App Engine](https://cloud.google.com/appengine)을 사용합니다. 퀵 매치 서버는 퀵 매치를 위해 현재 대기하고 있는 사람의 방 ID를 새로 들어온 사람에게 보내주는 역할을 합니다.)
20 |
21 | 게임에서 사용되는 RNG (random number generator) 부분만을 제외하면 게임 상태는 오로지 사용자의 (키보드) 입력에 의해 결정됩니다. 따라서 네트워크 양편에 있는 두 사용자가 사용하는 RNG가 같다면, 사용자의 입력을 서로 주고 받는 것만으로도 두 사용자의 게임 상태를 동일하게 유지할 수 있습니다. 이 P2P 온라인 버전은 data channel open event가 발생할 때 이 두 사용자의 RNG를 같게 만들고 그 후 사용자의 입력을 네트워크를 통해 서로 주고 받습니다.
22 |
23 | 더 자세한 사항은 [`src/resources/js/main_online.js`](src/resources/js/main_online.js) 파일에 있는 주석에서 볼 수 있습니다.
24 |
--------------------------------------------------------------------------------
/src/resources/js/offline_version_js/assets_path.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Manages the paths (file locations) of the game assets.
3 | */
4 | 'use strict';
5 |
6 | export const ASSETS_PATH = {
7 | SPRITE_SHEET: '../resources/assets/images/sprite_sheet.json',
8 | TEXTURES: {},
9 | SOUNDS: {},
10 | };
11 |
12 | const TEXTURES = ASSETS_PATH.TEXTURES;
13 | TEXTURES.PIKACHU = (i, j) => `pikachu/pikachu_${i}_${j}.png`;
14 | TEXTURES.BALL = (s) => `ball/ball_${s}.png`;
15 | TEXTURES.NUMBER = (n) => `number/number_${n}.png`;
16 |
17 | TEXTURES.SKY_BLUE = 'objects/sky_blue.png';
18 | TEXTURES.MOUNTAIN = 'objects/mountain.png';
19 | TEXTURES.GROUND_RED = 'objects/ground_red.png';
20 | TEXTURES.GROUND_LINE = 'objects/ground_line.png';
21 | TEXTURES.GROUND_LINE_LEFT_MOST = 'objects/ground_line_leftmost.png';
22 | TEXTURES.GROUND_LINE_RIGHT_MOST = 'objects/ground_line_rightmost.png';
23 | TEXTURES.GROUND_YELLOW = 'objects/ground_yellow.png';
24 | TEXTURES.NET_PILLAR_TOP = 'objects/net_pillar_top.png';
25 | TEXTURES.NET_PILLAR = 'objects/net_pillar.png';
26 | TEXTURES.SHADOW = 'objects/shadow.png';
27 | TEXTURES.BALL_HYPER = 'ball/ball_hyper.png';
28 | TEXTURES.BALL_TRAIL = 'ball/ball_trail.png';
29 | TEXTURES.BALL_PUNCH = 'ball/ball_punch.png';
30 | TEXTURES.CLOUD = 'objects/cloud.png';
31 | TEXTURES.WAVE = 'objects/wave.png';
32 | TEXTURES.BLACK = 'objects/black.png';
33 |
34 | TEXTURES.SACHISOFT = 'messages/common/sachisoft.png';
35 | TEXTURES.READY = 'messages/common/ready.png';
36 | TEXTURES.GAME_END = 'messages/common/game_end.png';
37 |
38 | TEXTURES.MARK = 'messages/ja/mark.png';
39 | TEXTURES.POKEMON = 'messages/ja/pokemon.png';
40 | TEXTURES.PIKACHU_VOLLEYBALL = 'messages/ja/pikachu_volleyball.png';
41 | TEXTURES.FIGHT = 'messages/ja/fight.png';
42 | TEXTURES.WITH_COMPUTER = 'messages/ja/with_computer.png';
43 | TEXTURES.WITH_FRIEND = 'messages/ja/with_friend.png';
44 | TEXTURES.GAME_START = 'messages/ja/game_start.png';
45 |
46 | TEXTURES.SITTING_PIKACHU = 'sitting_pikachu.png';
47 |
48 | const SOUNDS = ASSETS_PATH.SOUNDS;
49 | SOUNDS.BGM = '../resources/assets/sounds/bgm.mp3';
50 | SOUNDS.PIPIKACHU = '../resources/assets/sounds/WAVE140_1.wav';
51 | SOUNDS.PIKA = '../resources/assets/sounds/WAVE141_1.wav';
52 | SOUNDS.CHU = '../resources/assets/sounds/WAVE142_1.wav';
53 | SOUNDS.PI = '../resources/assets/sounds/WAVE143_1.wav';
54 | SOUNDS.PIKACHU = '../resources/assets/sounds/WAVE144_1.wav';
55 | SOUNDS.POWERHIT = '../resources/assets/sounds/WAVE145_1.wav';
56 | SOUNDS.BALLTOUCHESGROUND = '../resources/assets/sounds/WAVE146_1.wav';
57 |
--------------------------------------------------------------------------------
/src/resources/js/nickname_display.js:
--------------------------------------------------------------------------------
1 | import { channel } from './data_channel/data_channel';
2 | import { filterBadWords } from './bad_words_filtering/chat_filter.js';
3 |
4 | /**
5 | * Display nickname for the player
6 | * @param {boolean} isForPlayer2
7 | * @param {string} nickname
8 | */
9 | export function displayNicknameFor(nickname, isForPlayer2) {
10 | let nicknameElm = null;
11 | if (!isForPlayer2) {
12 | nicknameElm = document.getElementById('player1-nickname');
13 | } else {
14 | nicknameElm = document.getElementById('player2-nickname');
15 | }
16 | nicknameElm.textContent = nickname;
17 | }
18 |
19 | /**
20 | * Display peer's nickname for the player
21 | * Added for filtering nickname
22 | * @param {boolean} isForPlayer2
23 | * @param {string} nickname
24 | */
25 | export function displayPeerNicknameFor(nickname, isForPlayer2) {
26 | let nicknameElm = null;
27 | if (!isForPlayer2) {
28 | nicknameElm = document.getElementById('player1-nickname');
29 | } else {
30 | nicknameElm = document.getElementById('player2-nickname');
31 | }
32 | nicknameElm.textContent = filterBadWords(nickname);
33 | }
34 |
35 | /**
36 | * Display partial ip for the player
37 | * @param {boolean} isForPlayer2
38 | * @param {string} partialIP
39 | */
40 | export function displayPartialIPFor(partialIP, isForPlayer2) {
41 | let partialIPElm = null;
42 | if (!isForPlayer2) {
43 | partialIPElm = document.getElementById('player1-partial-ip');
44 | } else {
45 | partialIPElm = document.getElementById('player2-partial-ip');
46 | }
47 | partialIPElm.textContent = partialIP;
48 | }
49 |
50 | export function displayMyAndPeerNicknameShownOrHidden() {
51 | const elem1 = document.getElementById('player1-hiding-peer-nickname');
52 | const elem2 = document.getElementById('player2-hiding-peer-nickname');
53 | const displayShown = (isNicknameShown, elem) => {
54 | if (isNicknameShown) {
55 | elem.classList.add('hidden');
56 | } else {
57 | elem.classList.remove('hidden');
58 | }
59 | };
60 |
61 | if (channel.amIPlayer2 === null) {
62 | if (channel.amICreatedRoom) {
63 | displayShown(channel.myIsPeerNicknameVisible, elem1);
64 | displayShown(channel.peerIsPeerNicknameVisible, elem2);
65 | } else {
66 | displayShown(channel.myIsPeerNicknameVisible, elem2);
67 | displayShown(channel.peerIsPeerNicknameVisible, elem1);
68 | }
69 | } else if (channel.amIPlayer2 === false) {
70 | displayShown(channel.myIsPeerNicknameVisible, elem1);
71 | displayShown(channel.peerIsPeerNicknameVisible, elem2);
72 | } else if (channel.amIPlayer2 === true) {
73 | displayShown(channel.myIsPeerNicknameVisible, elem2);
74 | displayShown(channel.peerIsPeerNicknameVisible, elem1);
75 | }
76 | }
77 |
--------------------------------------------------------------------------------
/src/resources/js/offline_version_js/cloud_and_wave.js:
--------------------------------------------------------------------------------
1 | /**
2 | * This module takes charge of the physical motion of clouds (on the sky) and wave (on the bottom of game screen) in the game.
3 | * It is also a Model in MVC pattern and also rendered by "view.js".
4 | *
5 | * It is gained by reverse engineering the original game.
6 | * The address of the original function is in the comment.
7 | * ex) FUN_00404770 means the function at the address 00404770 in the machine code.
8 | */
9 | 'use strict';
10 | import { rand } from './rand.js';
11 |
12 | /**
13 | * Class represents a cloud
14 | */
15 | export class Cloud {
16 | constructor() {
17 | this.topLeftPointX = -68 + (rand() % (432 + 68));
18 | this.topLeftPointY = rand() % 152;
19 | this.topLeftPointXVelocity = 1 + (rand() % 2);
20 | this.sizeDiffTurnNumber = rand() % 11;
21 | }
22 |
23 | get sizeDiff() {
24 | // this same as return [0, 1, 2, 3, 4, 5, 4, 3, 2, 1, 0][this.sizeDiffTurnNumber]
25 | return 5 - Math.abs(this.sizeDiffTurnNumber - 5);
26 | }
27 |
28 | get spriteTopLeftPointX() {
29 | return this.topLeftPointX - this.sizeDiff;
30 | }
31 |
32 | get spriteTopLeftPointY() {
33 | return this.topLeftPointY - this.sizeDiff;
34 | }
35 |
36 | get spriteWidth() {
37 | return 48 + 2 * this.sizeDiff;
38 | }
39 |
40 | get spriteHeight() {
41 | return 24 + 2 * this.sizeDiff;
42 | }
43 | }
44 |
45 | /**
46 | * Class representing wave
47 | */
48 | export class Wave {
49 | constructor() {
50 | this.verticalCoord = 0;
51 | this.verticalCoordVelocity = 2;
52 | this.yCoords = [];
53 | for (let i = 0; i < 432 / 16; i++) {
54 | this.yCoords.push(314);
55 | }
56 | }
57 | }
58 |
59 | /**
60 | * FUN_00404770
61 | * Move clouds and wave
62 | * @param {Cloud[]} cloudArray
63 | * @param {Wave} wave
64 | */
65 | export function cloudAndWaveEngine(cloudArray, wave) {
66 | for (let i = 0; i < 10; i++) {
67 | cloudArray[i].topLeftPointX += cloudArray[i].topLeftPointXVelocity;
68 | if (cloudArray[i].topLeftPointX > 432) {
69 | cloudArray[i].topLeftPointX = -68;
70 | cloudArray[i].topLeftPointY = rand() % 152;
71 | cloudArray[i].topLeftPointXVelocity = 1 + (rand() % 2);
72 | }
73 | cloudArray[i].sizeDiffTurnNumber =
74 | (cloudArray[i].sizeDiffTurnNumber + 1) % 11;
75 | }
76 |
77 | wave.verticalCoord += wave.verticalCoordVelocity;
78 | if (wave.verticalCoord > 32) {
79 | wave.verticalCoord = 32;
80 | wave.verticalCoordVelocity = -1;
81 | } else if (wave.verticalCoord < 0 && wave.verticalCoordVelocity < 0) {
82 | wave.verticalCoordVelocity = 2;
83 | wave.verticalCoord = -(rand() % 40);
84 | }
85 |
86 | for (let i = 0; i < 432 / 16; i++) {
87 | wave.yCoords[i] = 314 - wave.verticalCoord + (rand() % 3);
88 | }
89 | }
90 |
--------------------------------------------------------------------------------
/src/resources/js/offline_version_js/utils/dark_color_scheme.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 | import { localStorageWrapper } from './local_storage_wrapper';
3 | const THEME_COLOR_LIGHT = '#FFFFFF';
4 | const THEME_COLOR_DARK = '#202124';
5 |
6 | setUpDarkColorSchemeCheckbox();
7 |
8 | /**
9 | * Set up dark color scheme checkbox
10 | */
11 | function setUpDarkColorSchemeCheckbox() {
12 | const darkColorSchemeCheckboxElements = Array.from(
13 | document.getElementsByClassName('dark-color-scheme-checkbox')
14 | );
15 | const colorScheme = localStorageWrapper.get('colorScheme');
16 | if (colorScheme === 'dark' || colorScheme === 'light') {
17 | darkColorSchemeCheckboxElements.forEach((elem) => {
18 | // @ts-ignore
19 | elem.checked = colorScheme === 'dark';
20 | });
21 | applyColorScheme(colorScheme);
22 | } else {
23 | const doesPreferDarkColorScheme = window.matchMedia(
24 | '(prefers-color-scheme: dark)'
25 | ).matches;
26 | // The following line is not for "document.documentElement.dataset.colorScheme = colorScheme;".
27 | // document.documentElement.dataset.colorScheme is not needed to be set for displaying dark color scheme,
28 | // since style.css has media query "@media (prefers-color-scheme: dark)" which deals with it without JavaScript.
29 | // The following line is for setting theme color and etc...
30 | applyColorScheme(doesPreferDarkColorScheme ? 'dark' : 'light');
31 | darkColorSchemeCheckboxElements.forEach((elem) => {
32 | // @ts-ignore
33 | elem.checked = doesPreferDarkColorScheme;
34 | });
35 | }
36 | darkColorSchemeCheckboxElements.forEach((elem) => {
37 | elem.addEventListener('change', () => {
38 | // @ts-ignore
39 | const colorScheme = elem.checked ? 'dark' : 'light';
40 | applyColorScheme(colorScheme);
41 | localStorageWrapper.set('colorScheme', colorScheme);
42 | // For syncing states of other checkbox elements
43 | darkColorSchemeCheckboxElements.forEach((element) => {
44 | if (element !== elem) {
45 | // @ts-ignore
46 | element.checked = elem.checked;
47 | }
48 | });
49 | });
50 | });
51 | }
52 |
53 | /**
54 | * Apply color scheme. (Display color scheme to the screen.)
55 | * @param {string} colorScheme 'dark' or 'light'
56 | */
57 | function applyColorScheme(colorScheme) {
58 | document.documentElement.dataset.colorScheme = colorScheme;
59 | const themeColorMetaElement = document.querySelector(
60 | 'meta[name="theme-color"]'
61 | );
62 | if (themeColorMetaElement !== null) {
63 | // The line below is for the status bar color, which is set by theme-color
64 | // meta tag content, of PWA in Apple devices.
65 | themeColorMetaElement.setAttribute(
66 | 'content',
67 | colorScheme === 'dark' ? THEME_COLOR_DARK : THEME_COLOR_LIGHT
68 | );
69 | }
70 | }
71 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Pikachu Volleyball P2P Online
2 |
3 | _✓_ _English_ | [_Korean(한국어)_](README.ko.md)
4 |
5 | Pikachu Volleyball (対戦ぴかちゅ~ ビーチバレー編) is an old Windows game which was developed by "(C) SACHI SOFT / SAWAYAKAN Programmers" and "(C) Satoshi Takenouchi" in 1997.
6 |
7 | Pikachu Volleyball P2P Online is an peer-to-peer online version of the game. You can play with other person through the internet. It is developed by integrating [WebRTC](https://webrtc.org/) [data channels](https://webrtc.org/getting-started/data-channels) to [the offline web version of Pikachu Volleyball](https://github.com/gorisanson/pikachu-volleyball) which is made by reverse engineering the original game.
8 |
9 | You can play Pikachu Volleyball P2P online on the website: https://gorisanson.github.io/pikachu-volleyball-p2p-online/en/
10 |
11 |
12 |
13 | ## Structure
14 |
15 | - Offline version: All the offline web version source code files is in the directory [`src/resources/js/offline_version_js/`](src/resources/js/offline_version_js). These are the same as the source code files in https://github.com/gorisanson/pikachu-volleyball/tree/main/src/resources/js. The online version is developed base on these.
16 |
17 | - WebRTC data channels: The peer-to-peer online functions utilizing WebRTC data channels are contained in [`src/resources/js/data_channel/data_channel.js`](src/resources/js/data_channel/data_channel.js). ([Firebase Cloud Firestore](https://firebase.google.com/docs/firestore) is used as a mediator for establishing a peer-to-peer communication via WebRTC. The room ID which the room creator sends to the joiner is the ID of a Cloud Firestore document which is shared between them. This method is originally from [Firebase + WebRTC Codelab](https://webrtc.org/getting-started/firebase-rtc-codelab) and [https://github.com/webrtc/FirebaseRTC](https://github.com/webrtc/FirebaseRTC).)
18 |
19 | - Quick Match: The communication with the quick match server is contained in [`src/resources/js/quick_match/quick_match.js`](src/resources/js/quick_match/quick_match.js). ([Google App Engine](https://cloud.google.com/appengine) is used as the quick match server. The quick match server sends the ID of the room, which is created by a person waiting for a quick match, to the new one who comes in.)
20 |
21 | The game state is deterministic on the user (keyboard) inputs except the RNG (random number generator) used in the game. So if the RNG is the same on both peers, only the user inputs need to be communicated to maintain the same game state between the peers. In this p2p online version, the RNG is set to the same thing on both peers at the data channel open event, then the user inputs are communicated via the data channel.
22 |
23 | Refer comments on [`src/resources/js/main_online.js`](src/resources/js/main_online.js) for other details.
24 |
--------------------------------------------------------------------------------
/src/resources/js/bad_words_filtering/chat_filter.js:
--------------------------------------------------------------------------------
1 | import { badWordList } from './bad_word_list';
2 |
3 | /**
4 | * @param {string} message
5 | * @returns {string}
6 | */
7 | export function filterBadWords(message) {
8 | const badWords = badWordList.createWordArray(); // Sum of basic bad words and additional bad words
9 | const filteredBadWords = [...new Set(badWords)].filter(
10 | (word) => word.length > 0
11 | );
12 |
13 | let resultChars = Array.from(message); // Original message not yet filtered
14 | let hasChanges = true; // Variable for detecting 'Is filtering ended?'
15 |
16 | const pattern = new RegExp(filteredBadWords.join('|'), 'gi');
17 |
18 | const characterViews = [
19 | { name: 'Letter+Emoji', regex: /\p{L}|\p{Emoji}/u }, // Only letter and emoji
20 | { name: 'Letter', regex: /\p{L}/u }, // Only letter
21 | { name: 'Korean', regex: /\p{Script=Hangul}/u }, // Only korean
22 | { name: 'English', regex: /\p{Script=Latin}/u }, // Only English
23 | { name: 'Kanji', regex: /\p{Script=Han}/u }, // Only kanji
24 | { name: 'Emoji', regex: /\p{Emoji}/u }, // Only Emoji
25 | ];
26 |
27 | while (hasChanges) {
28 | // Repeat filtering while target doesn't exist
29 | hasChanges = false;
30 | let currentResult = Array.from(resultChars);
31 |
32 | for (const characterView of characterViews) {
33 | const { cleaned, mapToOriginal } = cleanMessage(
34 | currentResult.join(''),
35 | characterView.regex
36 | );
37 | const matches = [...cleaned.matchAll(pattern)];
38 |
39 | for (const m of matches) {
40 | const matchLength = Array.from(m[0]).length; // Count emojis as length 1
41 | const prefix = cleaned.slice(0, m.index);
42 | const arrayIndex = Array.from(prefix).length;
43 |
44 | // Substitute bad-words to '*' by index
45 | for (let i = 0; i < matchLength; i++) {
46 | const cleanedIndex = arrayIndex + i;
47 | const origIndex = mapToOriginal[cleanedIndex];
48 |
49 | // Prevent re-filtering
50 | if (currentResult[origIndex] !== '*') {
51 | currentResult[origIndex] = '*';
52 | hasChanges = true;
53 | }
54 | }
55 | }
56 | }
57 |
58 | if (hasChanges) {
59 | resultChars = currentResult;
60 | }
61 | } // End of while loop
62 | return resultChars.join('');
63 | }
64 |
65 | /**
66 | * Remove characters not matching 'filterRegex'
67 | * @param {string} message
68 | * @param {RegExp} filterRegex - character which is wanted to remain
69 | * @returns {{cleaned: string, mapToOriginal: number[]}}
70 | */
71 | function cleanMessage(message, filterRegex) {
72 | const cleanedChars = [];
73 | const mapToOriginal = [];
74 | const messageChars = Array.from(message);
75 |
76 | for (let i = 0; i < messageChars.length; i++) {
77 | const ch = messageChars[i];
78 |
79 | if (filterRegex.test(ch)) {
80 | mapToOriginal.push(i);
81 | cleanedChars.push(ch.toLowerCase());
82 | }
83 | }
84 |
85 | return {
86 | cleaned: cleanedChars.join(''),
87 | mapToOriginal: mapToOriginal,
88 | };
89 | }
90 |
--------------------------------------------------------------------------------
/src/resources/js/bad_words_filtering/bad_word_list.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Class representing a custom bad word added by the user.
3 | */
4 | class CustomBadWord {
5 | /**
6 | * Create a CustomBadWord object
7 | * @param {string} word
8 | * @param {number} [addedTime]
9 | */
10 | constructor(word, addedTime = Date.now()) {
11 | this.word = word.toLowerCase().trim();
12 | this.addedTime = addedTime;
13 | }
14 | }
15 |
16 | /**
17 | * Class representing a list of custom bad words
18 | */
19 | class BadWordList {
20 | /**
21 | * Create a BadWordList object
22 | * @param {number} maxLength
23 | */
24 | constructor(maxLength) {
25 | this._badWords = [];
26 | this.maxLength = maxLength;
27 | this.willUseBasicBadWords = false;
28 | this.basicBadWords = [
29 | 'fuck',
30 | 'fuckyou',
31 | 'shit',
32 | 'bitch',
33 | 'asshole',
34 | 'nigger',
35 | 'faggot',
36 | '개새',
37 | '느금',
38 | 'ㄴㄱㅁ',
39 | 'ㄴ금마',
40 | '니애미',
41 | 'ㄴㅇㅁ',
42 | '느그',
43 | '병신',
44 | '병ㅅ',
45 | 'ㅂㅅ',
46 | 'ㅂ신',
47 | 'ㅅㅂ',
48 | '새끼',
49 | 'ㅅㄲ',
50 | '시발',
51 | '씨발',
52 | 'ㅅ발',
53 | '애미',
54 | '애비',
55 | '어머니',
56 | '엄마',
57 | '아버지',
58 | '좆',
59 | 'ㅈ까',
60 | 'ㅈ밥',
61 | 'ㅈㅂ',
62 | 'ㅈ이',
63 | 'ㅄ',
64 | '씹',
65 | ];
66 | }
67 |
68 | get length() {
69 | return this._badWords.length;
70 | }
71 |
72 | /**
73 | * Return if the list is full
74 | * @returns {boolean}
75 | */
76 | isFull() {
77 | return this.length >= this.maxLength;
78 | }
79 |
80 | /**
81 | * Add the custom bad words into custom bad words list
82 | * @param {string} bad_Words
83 | */
84 | AddBadWords(bad_Words) {
85 | if (this.isFull() || this._badWords.some((bw) => bw.word === bad_Words)) {
86 | return;
87 | }
88 | this._badWords.push(new CustomBadWord(bad_Words));
89 | }
90 |
91 | /**
92 | * Remove a bad word at index from the list
93 | * @param {number} index
94 | */
95 | removeAt(index) {
96 | this._badWords.splice(index, 1);
97 | }
98 |
99 | /**
100 | * Create a read-only 2D array [word, addedTime].
101 | * @returns {[string, number][]}
102 | */
103 | createArrayView() {
104 | return this._badWords.map((badWord) => [badWord.word, badWord.addedTime]);
105 | }
106 |
107 | /**
108 | * Read a 2D array and update this._badWords from it.
109 | * @param {[string, number][]} arrayView
110 | */
111 | readArrayViewAndUpdate(arrayView) {
112 | this._badWords = [];
113 | arrayView = arrayView.slice(0, this.maxLength);
114 | this._badWords = arrayView.map(
115 | (value) => new CustomBadWord(value[0], value[1])
116 | );
117 | }
118 |
119 | /**
120 | * Create a read-only 1D array of words.
121 | * @returns {string[]}
122 | */
123 | createWordArray() {
124 | if (this.willUseBasicBadWords) {
125 | return this.basicBadWords.concat(
126 | this._badWords.map((badWord) => badWord.word)
127 | );
128 | } else {
129 | return this._badWords.map((badWord) => badWord.word);
130 | }
131 | }
132 | }
133 |
134 | export const badWordList = new BadWordList(50); // Limit of the number of bad words
135 |
--------------------------------------------------------------------------------
/src/resources/js/utils/generate_pushid.js:
--------------------------------------------------------------------------------
1 | /**
2 | * This code is originated from a gist https://gist.github.com/mikelehen/3596a30bd69384624c11
3 | * I found the gist link at https://firebase.googleblog.com/2015/02/the-2120-ways-to-ensure-unique_68.html
4 | *
5 | * Modified the original code somewhat so that the generated id can be easily distinguishable by human eye
6 | * and Web Crypto API is used instead of Math.random if available.
7 | */
8 | 'use strict';
9 |
10 | /**
11 | * Fancy ID generator that creates 20-character string identifiers with the following properties:
12 | *
13 | * 1. They're based on timestamp so that they sort *after* any existing ids.
14 | * 2. They contain 50-bits of random data after the timestamp so that IDs won't collide with other clients' IDs.
15 | * 3. They sort *lexicographically* (so the timestamp is converted to characters that will sort properly).
16 | * 4. They're monotonically increasing. Even if you generate more than one in the same timestamp, the
17 | * latter ones will sort after the former ones. We do this by using the previous random bits
18 | * but "incrementing" them by 1 (only in the case of a timestamp collision).
19 | */
20 | export const generatePushID = (function () {
21 | // Modeled after base32 web-safe chars, but ordered by ASCII.
22 | const PUSH_CHARS = '23456789abcdefghijkmnpqrstuvwxyz';
23 |
24 | // Timestamp of last push, used to prevent local collisions if you push twice in one ms.
25 | let lastPushTime = 0;
26 |
27 | // We generate 50-bits of randomness which get turned into 10 characters (since 32 === 2^5, (2^5)^10 === 2^50 )
28 | // and appended to the timestamp to prevent collisions with other clients. We store the last characters we
29 | // generated because in the event of a collision, we'll use those same characters except
30 | // "incremented" by one.
31 | const lastRandChars = [];
32 |
33 | return function () {
34 | let now = Date.now();
35 | const duplicateTime = now === lastPushTime;
36 | lastPushTime = now;
37 |
38 | const timeStampChars = new Array(10);
39 | for (let i = 9; i >= 0; i--) {
40 | timeStampChars[i] = PUSH_CHARS.charAt(now % 32);
41 | // NOTE: Can't use << here because javascript will convert to int and lose the upper bits.
42 | now = Math.floor(now / 32);
43 | }
44 | if (now !== 0)
45 | throw new Error('We should have converted the entire timestamp.');
46 |
47 | let id = timeStampChars.join('');
48 | if (!duplicateTime) {
49 | let array;
50 | if (
51 | typeof window.crypto !== 'undefined' &&
52 | window.crypto.getRandomValues
53 | ) {
54 | array = new Uint32Array(10);
55 | window.crypto.getRandomValues(array);
56 | }
57 | for (let i = 0; i < 10; i++) {
58 | lastRandChars[i] = array
59 | ? array[i] % 32
60 | : Math.floor(Math.random() * 32);
61 | }
62 | } else {
63 | // If the timestamp hasn't changed since last push, use the same random number, except incremented by 1.
64 | let i;
65 | for (i = 9; i >= 0 && lastRandChars[i] === 31; i--) {
66 | lastRandChars[i] = 0;
67 | }
68 | lastRandChars[i]++;
69 | }
70 | for (let i = 0; i < 10; i++) {
71 | id += PUSH_CHARS.charAt(lastRandChars[i]);
72 | }
73 | if (id.length !== 20) throw new Error('Length should be 20.');
74 |
75 | return id;
76 | };
77 | })();
78 |
--------------------------------------------------------------------------------
/src/resources/js/block_other_players/blocked_ip_list.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Class representing a blocked IP
3 | */
4 | class BlockedIP {
5 | /**
6 | * Create a BlockedIP object
7 | * @param {string} hashedIP
8 | * @param {string} partialIP
9 | * @param {number} [blockedTime]
10 | * @param {string} [remark]
11 | */
12 | constructor(hashedIP, partialIP, blockedTime = Date.now(), remark = '') {
13 | this.hashedIP = hashedIP;
14 | this.partialIP = partialIP;
15 | this.blockedTime = blockedTime;
16 | this.remark = remark;
17 | }
18 | }
19 |
20 | /**
21 | * Class representing a list of blocked IPs
22 | */
23 | class BlockedIPList {
24 | /**
25 | * Create a BlockedIPLIst object
26 | * @param {number} maxLength
27 | */
28 | constructor(maxLength) {
29 | this._blockedIPs = [];
30 | this._peerHashedIP = null;
31 | this.maxLength = maxLength;
32 | }
33 |
34 | get length() {
35 | return this._blockedIPs.length;
36 | }
37 |
38 | /**
39 | * Return if the list is full
40 | * @returns {boolean}
41 | */
42 | isFull() {
43 | return this.length >= this.maxLength;
44 | }
45 |
46 | /**
47 | * Stage the hashed IP of the peer
48 | * @param {string} peerHashedIP
49 | */
50 | stagePeerHashedIP(peerHashedIP) {
51 | this._peerHashedIP = peerHashedIP;
52 | }
53 |
54 | /**
55 | * Return if a peer hashed IP is staged
56 | * @returns {boolean}
57 | */
58 | isPeerHashedIPStaged() {
59 | return this._peerHashedIP !== null;
60 | }
61 |
62 | /**
63 | * Add the staged peer hashed IP to blocked IP list
64 | * @param {string} peerPartialIP
65 | */
66 | AddStagedPeerHashedIPWithPeerPartialIP(peerPartialIP) {
67 | if (!this.isPeerHashedIPStaged() || this.isFull()) {
68 | return;
69 | }
70 | this._blockedIPs.push(new BlockedIP(this._peerHashedIP, peerPartialIP));
71 | }
72 |
73 | /**
74 | * Remove a blocked IP at index from the list
75 | * @param {number} index
76 | */
77 | removeAt(index) {
78 | this._blockedIPs.splice(index, 1);
79 | }
80 |
81 | /**
82 | * Edit remark of a BlockedIP object at index
83 | * @param {number} index
84 | * @param {string} newRemark
85 | */
86 | editRemarkAt(index, newRemark) {
87 | this._blockedIPs[index].remark = newRemark;
88 | }
89 |
90 | /**
91 | * Create a read-only 2D array whose elements have a structure of [ip, blockedTime, remark].
92 | * And the elements have same indices as in the this._blockedIPs.
93 | * @returns {[string, string, number, string][]}
94 | */
95 | createArrayView() {
96 | return this._blockedIPs.map((blockedIP) => [
97 | blockedIP.hashedIP,
98 | blockedIP.partialIP,
99 | blockedIP.blockedTime,
100 | blockedIP.remark,
101 | ]);
102 | }
103 |
104 | /**
105 | * Read a 2D array which has the same structure of an array created by {@link createArrayView}
106 | * and update this._blockedIPs from it.
107 | * @param {[string, string, number, string][]} arrayView
108 | */
109 | readArrayViewAndUpdate(arrayView) {
110 | arrayView = arrayView.slice(0, this.maxLength);
111 | this._blockedIPs = arrayView.map(
112 | (value) => new BlockedIP(value[0], value[1], value[2], value[3])
113 | );
114 | }
115 |
116 | /**
117 | * Create a read-only 1D array whose elements are blocked hashed IP addresses.
118 | * @returns {[string]}
119 | */
120 | createHashedIPArray() {
121 | // @ts-ignore
122 | return this._blockedIPs.map((blockedIP) => blockedIP.hashedIP);
123 | }
124 | }
125 |
126 | export const blockedIPList = new BlockedIPList(50);
127 |
--------------------------------------------------------------------------------
/webpack.common.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 | const { CleanWebpackPlugin } = require('clean-webpack-plugin');
3 | const CopyPlugin = require('copy-webpack-plugin');
4 | const HtmlWebpackPlugin = require('html-webpack-plugin');
5 | const MiniCssExtractPlugin = require('mini-css-extract-plugin');
6 |
7 | module.exports = {
8 | entry: {
9 | main: './src/resources/js/main_online.js',
10 | ko: './src/ko/ko.js',
11 | main_replay: './src/resources/js/replay/main_replay.js',
12 | main_update_history:
13 | './src/resources/js/update_history/main_update_history.js',
14 | dark_color_scheme:
15 | './src/resources/js/offline_version_js/utils/dark_color_scheme.js',
16 | is_embedded_in_other_website:
17 | './src/resources/js/offline_version_js/utils/is_embedded_in_other_website.js',
18 | },
19 | output: {
20 | filename: '[name].[contenthash].js',
21 | path: path.resolve(__dirname, 'dist'),
22 | },
23 | optimization: {
24 | runtimeChunk: { name: 'runtime' }, // this is for code-sharing between "main_online.js" and "ko.js"
25 | splitChunks: {
26 | chunks: 'all',
27 | },
28 | },
29 | module: {
30 | rules: [
31 | {
32 | test: /\.css$/i,
33 | use: [MiniCssExtractPlugin.loader, 'css-loader'],
34 | },
35 | ],
36 | },
37 | plugins: [
38 | new CleanWebpackPlugin(),
39 | new CopyPlugin({
40 | patterns: [
41 | {
42 | context: 'src/',
43 | from: 'resources/assets/**/*.+(json|png|mp3|wav)',
44 | },
45 | { from: 'src/index.html', to: 'index.html' },
46 | ],
47 | }),
48 | new MiniCssExtractPlugin({
49 | filename: '[name].[contenthash].css',
50 | chunkFilename: '[id].[contenthash].css',
51 | }),
52 | new HtmlWebpackPlugin({
53 | template: 'src/en/index.html',
54 | filename: 'en/index.html',
55 | chunks: [
56 | 'runtime',
57 | 'main',
58 | 'dark_color_scheme',
59 | 'is_embedded_in_other_website',
60 | ],
61 | chunksSortMode: 'manual',
62 | minify: {
63 | collapseWhitespace: true,
64 | removeComments: true,
65 | },
66 | }),
67 | new HtmlWebpackPlugin({
68 | template: 'src/ko/index.html',
69 | filename: 'ko/index.html',
70 | chunks: [
71 | 'runtime',
72 | 'ko',
73 | 'main',
74 | 'dark_color_scheme',
75 | 'is_embedded_in_other_website',
76 | ],
77 | chunksSortMode: 'manual',
78 | minify: {
79 | collapseWhitespace: true,
80 | removeComments: true,
81 | },
82 | }),
83 | new HtmlWebpackPlugin({
84 | template: 'src/en/replay/index.html',
85 | filename: 'en/replay/index.html',
86 | chunks: ['runtime', 'main_replay', 'dark_color_scheme'],
87 | chunksSortMode: 'manual',
88 | minify: {
89 | collapseWhitespace: true,
90 | removeComments: true,
91 | },
92 | }),
93 | new HtmlWebpackPlugin({
94 | template: 'src/ko/replay/index.html',
95 | filename: 'ko/replay/index.html',
96 | chunks: ['runtime', 'ko', 'main_replay', 'dark_color_scheme'],
97 | chunksSortMode: 'manual',
98 | minify: {
99 | collapseWhitespace: true,
100 | removeComments: true,
101 | },
102 | }),
103 | new HtmlWebpackPlugin({
104 | template: 'src/en/update-history/index.html',
105 | filename: 'en/update-history/index.html',
106 | chunks: ['main_update_history', 'dark_color_scheme'],
107 | chunksSortMode: 'manual',
108 | minify: {
109 | collapseWhitespace: true,
110 | removeComments: true,
111 | },
112 | }),
113 | new HtmlWebpackPlugin({
114 | template: 'src/ko/update-history/index.html',
115 | filename: 'ko/update-history/index.html',
116 | chunks: ['main_update_history', 'dark_color_scheme'],
117 | chunksSortMode: 'manual',
118 | minify: {
119 | collapseWhitespace: true,
120 | removeComments: true,
121 | },
122 | }),
123 | ],
124 | };
125 |
--------------------------------------------------------------------------------
/src/resources/js/data_channel/parse_candidate.js:
--------------------------------------------------------------------------------
1 | /**
2 | * This function is from https://github.com/otalk/sdp
3 | * (permalink: https://github.com/otalk/sdp/blob/3a8d369a9c159a691c5ee67d6a5f26b4887d26dc/sdp.js#L48)
4 | * (I see this function first from https://webrtchacks.com/symmetric-nat/)
5 | * The license of the code this function is below (MIT License)
6 | *
7 | ****************************************
8 | * Copyright (c) 2017 Philipp Hancke
9 | *
10 | * Permission is hereby granted, free of charge, to any person obtaining a copy
11 | * of this software and associated documentation files (the "Software"), to deal
12 | * in the Software without restriction, including without limitation the rights
13 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
14 | * copies of the Software, and to permit persons to whom the Software is
15 | * furnished to do so, subject to the following conditions:
16 | *
17 | * The above copyright notice and this permission notice shall be included in all
18 | * copies or substantial portions of the Software.
19 | *
20 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
21 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
22 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
23 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
24 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
25 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
26 | * SOFTWARE.
27 | *********************************************
28 | */
29 | export function parseCandidate(line) {
30 | let parts;
31 | // Parse both variants.
32 | if (line.indexOf('a=candidate:') === 0) {
33 | parts = line.substring(12).split(' ');
34 | } else {
35 | parts = line.substring(10).split(' ');
36 | }
37 |
38 | const candidate = {
39 | foundation: parts[0],
40 | component: { 1: 'rtp', 2: 'rtcp' }[parts[1]],
41 | protocol: parts[2].toLowerCase(),
42 | priority: parseInt(parts[3], 10),
43 | ip: parts[4],
44 | address: parts[4], // address is an alias for ip.
45 | port: parseInt(parts[5], 10),
46 | // skip parts[6] == 'typ'
47 | type: parts[7],
48 | };
49 |
50 | for (let i = 8; i < parts.length; i += 2) {
51 | switch (parts[i]) {
52 | case 'raddr':
53 | candidate.relatedAddress = parts[i + 1];
54 | break;
55 | case 'rport':
56 | candidate.relatedPort = parseInt(parts[i + 1], 10);
57 | break;
58 | case 'tcptype':
59 | candidate.tcpType = parts[i + 1];
60 | break;
61 | case 'ufrag':
62 | candidate.ufrag = parts[i + 1]; // for backward compatibility.
63 | candidate.usernameFragment = parts[i + 1];
64 | break;
65 | default:
66 | // extension handling, in particular ufrag
67 | candidate[parts[i]] = parts[i + 1];
68 | break;
69 | }
70 | }
71 | return candidate;
72 | }
73 |
74 | /**
75 | * Return public IP address extracted from the candidate.
76 | * If the candidate does not contain public IP address, return null.
77 | * @param {Object} candidate
78 | */
79 | export function parsePublicIPFromCandidate(candidate) {
80 | // Parse the candidate
81 | const cand = parseCandidate(candidate);
82 | // Try to get and return the peer's public IP
83 | if (cand.type === 'srflx') {
84 | return cand.ip;
85 | } else if (cand.type === 'host') {
86 | if (!cand.ip.endsWith('.local')) {
87 | const privateIPReg = RegExp(
88 | '(^127.)|(^10.)|(^172.1[6-9].)|(^172.2[0-9].)|(^172.3[0-1].)|(^192.168.)'
89 | );
90 | if (!privateIPReg.test(cand.ip)) {
91 | return cand.ip;
92 | }
93 | }
94 | }
95 | return null;
96 | }
97 |
98 | /**
99 | * Get partial public IP, for example, 123.222.*.*
100 | * @param {string} ip ip address, for example, 123.222.111.123
101 | */
102 | export function getPartialIP(ip) {
103 | const index = ip.indexOf('.', ip.indexOf('.') + 1);
104 | if (index === -1) {
105 | // if ip is IPv6 address
106 | return ip.slice(0, 7);
107 | } else {
108 | // if ip is IPv4 address
109 | return `${ip.slice(0, index)}.*.*`;
110 | }
111 | }
112 |
--------------------------------------------------------------------------------
/src/resources/js/offline_version_js/audio.js:
--------------------------------------------------------------------------------
1 | /**
2 | * This module takes charge of the game audio (or sounds)
3 | */
4 | 'use strict';
5 | import { sound, Sound, filters } from '@pixi/sound';
6 | import { ASSETS_PATH } from './assets_path.js';
7 |
8 | const SOUNDS = ASSETS_PATH.SOUNDS;
9 |
10 | /** @typedef {import('@pixi/loaders').LoaderResource} LoaderResource */
11 |
12 | /**
13 | * Class representing audio
14 | */
15 | export class PikaAudio {
16 | /**
17 | * Create a PikaAudio object
18 | * @param {Object.47 | "파일 열기" 버튼을 누르거나 여기에 리플레이 파일을 끌어다 놓으세요 48 |
49 |Use "Open File" button or drag a replay file here
50 |34 | English 35 | | ✓ Korean(한국어) 36 |
37 |P2P 온라인 버전으로 돌아가기
38 |주요 업데이트만 기록합니다.
39 |2020-04-08 공개
43 |44 | 2020-04-19 오프라인 웹 버전 변경 반영: 45 | 피카츄 다이빙 방향 오류 수정 (@djsjukr님이 수정함) 51 |
52 |2020-04-29 퀵 매치 도입
53 |54 | 2020-05-03 오프라인 웹 버전 변경 반영: 55 | 왼편 키보드 매핑을 원조 피카츄 배구 게임과 동일하도록 수정 (@repeat_c님의 제보) 61 |
62 |63 | 2020-05-08 오프라인 웹 버전에서처럼 게임 64 | 속도와 다른 설정을 변경할 수 있도록 함 (@repeat_c님의 제안) 70 |
71 |72 | 2020-05-17 맨 처음 연결 후 기다리는 동안에 73 | 게임 속도 및 다른 설정을 변경할 수 있도록 함 (@e6p77Bi8CW7zRBg님의 제안) 79 |
80 |81 | 2020-05-23 "채팅 끄기" 버튼 추가 ("채팅" 82 | 버튼 오른편에 있음) (@막내님의 제안) 88 |
89 |90 | 2020-05-23 팝업 텍스트 박스가 뜰 때, 91 | 내용을 더 편하게 볼 수 있도록, 배경을 블러 처리함. (@KNHui님의 제안) 97 |
98 |99 | 2020-05-25 이 업데이트 기록 페이지를 만듦 100 | (@e6p77Bi8CW7zRBg님의 제안) 106 |
107 |108 | 2020-05-26 닉네임과 공인 IP 앞부분 표시 109 | (@e6p77Bi8CW7zRBg님의 제안 115 | 및 116 | @repeat_c님의 제안) 122 |
123 |124 | 2020-05-28 연결에 성공하거나 실패 시 125 | "피카츄" 소리로 알림 (@repeat_c님의 제안) 131 |
132 |133 | 2020-06-02 리플레이 저장 기능 추가 (@e6p77Bi8CW7zRBg님의 제안) 139 |
140 |141 | 2020-06-03 리플레이 재생기에서 키보드 142 | 입력을 볼 수 있음 (@repeat_c님의 제안) 148 |
149 |150 | 2020-06-07 게임 속도 "빠르게"로 변경 신청 151 | 자동으로 할 수 있는 옵션 만듦. 배경음악 및 효과음 설정 변경 유지되게 152 | 함. 153 |
154 |155 | 2020-06-17 "나가기" 버튼 추가. 리플레이에 156 | 관한 설명 추가. 157 |
158 |159 | 2020-06-18 리플레이 자동 저장 옵션 만듦. 160 | (@e6p77Bi8CW7zRBg님의 제안) 166 |
167 |168 | 2020-06-23 메인 페이지에서 "엔터" 키를 169 | 누르면 퀵 매치 진입 (@repeat_c님의 제안 참고함) 175 |
176 |177 | 2020-06-28 방 ID를 클립보드에 편리하게 178 | 복사할 수 있도록 "복사" 버튼 추가 (@samslow님이 구현함) 184 |
185 |186 | 2021-12-01 퀵 매치 차단 기능 추가. 퀵 매치 187 | 차단 목록에 등록된 IP로 접속하는 상대방과는 퀵 매치 상에서 매치되지 188 | 않음. 189 |
190 |2022-01-17 다크 모드 도입
191 |192 | 2023-02-19 퀵 매치에 매치 그룹 추가. 193 | 동일한 매치 그룹을 선택한 플레이어끼리 매치가 성사됩니다.(@DuckLL_tw님의 제안) 199 |
200 |201 | 2023-02-26 네트워크 검사 버그 수정. 이 202 | 버그 수정이 적용되기 전에는, 실제로는 그렇지 않은데도 네트워크가 203 | symmetric NAT 하에 있다고 검사 결과가 잘못 나올 수 있었습니다. (@소츄님의 제보 덕분에 버그를 발견함) 209 |
210 |211 | 2023-10-02 그래픽 옵션 ("예리하게", 212 | "부드럽게") 추가 (@DuckLL_tw님의 제안) 218 |
219 |220 | 2025-03-03 상대방 닉네임 가리기 기능 추가 221 | (@훈령병님의 제안) 222 |
223 |224 | 2025-11-23 채팅 및 닉네임 욕설 필터 추가 225 | (@uzaramen108님이 구현함) 231 |
232 |
233 | 2025-11-25 Ctrl 키와 Shift 키를 사용하여
234 | "퀵 매치 차단 목록" 및 "추가 금지어 목록"에서 여러 개의 항목을
235 | 선택할 수 있습니다. (@눈알님의 제안)
236 | 퀵 매치 취소 버튼을 상대방과 연결 시도 후 일정 시간이 지나면 다시
237 | 활성화합니다. (@마조리카님의 제안)
238 |
37 | ✓ English | 38 | Korean(한국어) 39 |
40 |41 | Back to 42 | the peer-to-peer online version 43 |
44 |Only major updates are recorded here.
45 |2020-04-08 Release
49 |50 | 2020-04-19 From the offline web version: 51 | fix Pikachu's diving direction. (Fixed by @djsjukr) 57 |
58 |2020-04-29 Introduce Quick Match
59 |60 | 2020-05-03 From the offline web version: 61 | fix left side keyboard mapping to the same as the original Pikachu 62 | Volleyball game. (Thanks to @repeat_c) 68 |
69 |70 | 2020-05-08 Enable changing the game speed 71 | and other options like in the offline web version. (Suggested by @repeat_c) 77 |
78 |79 | 2020-05-17 Enable changing game speed and 80 | other options on the initial waiting time. (Suggested by @e6p77Bi8CW7zRBg) 86 |
87 |88 | 2020-05-23 Add "disable chat" button, 89 | which is on the right side of "chat" button. (Suggested by @막내) 95 |
96 |97 | 2020-05-23 Add background blurring effect 98 | to the pop-up contents so that the text contents would be read 99 | easier. (Suggested by @KNHui) 105 |
106 |107 | 2020-05-25 Create this update history 108 | page. (Suggested by @e6p77Bi8CW7zRBg) 114 |
115 |116 | 2020-05-26 Show nickname and partial 117 | public IP address. (Suggested by @e6p77Bi8CW7zRBg 123 | and 124 | Suggested by @repeat_c) 130 |
131 |132 | 2020-05-28 Notify by "Pikachu" sound when 133 | the connection is established or failed. (Suggested by @repeat_c) 139 |
140 |141 | 2020-06-02 Enable saving replay (Suggested by @e6p77Bi8CW7zRBg) 147 |
148 |149 | 2020-06-03 Users can see keyboard inputs 150 | in replay viewer (Suggested by @repeat_c) 156 |
157 |158 | 2020-06-07 Create an option that 159 | automatically asks the peer about changing the game speed to "fast". 160 | Let BGM and SFX options change be kept. 161 |
162 |163 | 2020-06-17 Add "Exit" button. Add 164 | explanation about replay. 165 |
166 |167 | 2020-06-18 Create an option that 168 | automatically saves replay. (Suggested by @e6p77Bi8CW7zRBg) 174 |
175 |176 | 2020-06-23 Enable starting Quick Match by 177 | pressing "Enter" key in main page (Referred a suggestion of 182 | @repeat_c) 184 |
185 |186 | 2020-06-28 Add "Copy" button which can be 187 | used to copy the room ID to the clipboard. (Implemented by @samslow) 193 |
194 |195 | 2021-12-01 Create quick match block list. 196 | In the quick match, you are not matched against the players whose IP 197 | addresses are in the list. 198 |
199 |2022-01-17 Introduce dark mode.
200 |201 | 2023-02-19 Create match groups for the 202 | quick match. Players who have selected the same match group will be 203 | matched. (Suggested by 204 | @DuckLL_tw) 210 |
211 |212 | 2023-02-26 Fix bug in the network test. 213 | Before this bug fix, the test result could falsely say that the 214 | network is behind a symmetric NAT. (Found this bug thanks to a 215 | report by 216 | @소츄) 222 |
223 |224 | 2023-10-02 Options for graphic — "sharp" 225 | and "soft" — are added. (Suggested by @DuckLL_tw) 231 |
232 |233 | 2025-03-03 Add feature to hide peer's 234 | nickname. (Suggested by @훈령병) 235 |
236 |237 | 2025-11-23 Add chat and nickname profanity 238 | filter. (Implemented by @uzaramen108) 244 |
245 |
246 | 2025-11-25 Support Ctrl key and Shift key
247 | to select multiple entries in "Quick Match Block List" and "Quick
248 | Match Block List". (Suggested by
249 | @눈알)
250 | Re-enable cancel quick match button a short time after initiating
251 | the connection attempt. (Suggested by
252 | @마조리카)
253 |