e-対戦 [e-typing]

e-typingに対戦機能を追加したい

// ==UserScript==
// @name         e-対戦 [e-typing]
// @namespace    http://tampermonkey.net/
// @version      6.2
// @description  e-typingに対戦機能を追加したい
// @author       Toshi
// @match        https://www.e-typing.ne.jp/app/jsa_std*trysc*
// @match        https://www.e-typing.ne.jp/app/jsa_kana*trysc*

// @exclude      https://www.e-typing.ne.jp/app/ad*
// @exclude      https://www.e-typing.ne.jp/app/*std.2*
// @icon         https://www.google.com/s2/favicons?sz=64&domain=e-typing.ne.jp
// @license MIT
// @require      https://www.gstatic.com/firebasejs/10.1.0/firebase-app-compat.js
// @require      https://www.gstatic.com/firebasejs/10.1.0/firebase-auth-compat.js
// @require      https://www.gstatic.com/firebasejs/10.1.0/firebase-database-compat.js

// @grant unsafeWindow


// ==/UserScript==

//localStorage内に'firebase:previous_websocket_failure'keyが存在しているとデータ取得できなくなるため、ロード時に削除。
localStorage.removeItem('firebase:previous_websocket_failure')

const firebaseConfig = {
	//テスト用データベース
/*  	apiKey: "AIzaSyARzYljiRuZCoABA32_wnuyMkjScpcZUN8",
	databaseURL: "https://e-typing-battle-fb330-default-rtdb.firebaseio.com/" */

	//本番用データベース
 	apiKey: "AIzaSyDsHiPII5dgN_AEGwOtMehyveucoF4Twvs",
	databaseURL: "https://e-typing-battle-default-rtdb.firebaseio.com"
};


class MyResult{

	constructor(){
		this.battlePlayerResultData = {}

		this.etypingPlus = ['score','level','time','typeCount','missCount','wpm','correct','latency','rkpm']
		this.normal = ['score','level','time','typeCount','missCount','wpm','correct','weakKey']
	}



	sendResult(){
		playerStateChange.update('result')

		const RESULT_DATA = document.getElementsByClassName("result_data")[0].firstElementChild.children
		const RESULT_ITEM = RESULT_DATA.length == 9 ? this.etypingPlus : this.normal
		let updates = {}
		let sendData = {}


		for(let i=0; i<RESULT_ITEM.length; i++){
			sendData[RESULT_ITEM[i]] = RESULT_DATA[i].lastElementChild.textContent
		}




		updates['/users/' + myID + '/result/'] = sendData
		firebaseDB.ref().update(updates)


		this.createBattlePlayerResultArea()
		//相手のリザルトを取得していた場合、相手のリザルトを表示
		const BATTLE_PLAYPER_RESULT = Object.keys(this.battlePlayerResultData)

		if(BATTLE_PLAYPER_RESULT.length){
				const RESULT_DATA = document.getElementsByClassName("result_data")[1].firstElementChild.children
				const RESULT_ITEM = RESULT_DATA.length == 9 ? myResult.etypingPlus : myResult.normal

				for(let i=0;i<BATTLE_PLAYPER_RESULT.length;i++){
					const INDEX = RESULT_ITEM.indexOf(BATTLE_PLAYPER_RESULT[i])

					if(INDEX >= 0){
						RESULT_DATA[INDEX].lastElementChild.textContent = this.battlePlayerResultData[BATTLE_PLAYPER_RESULT[i]]
					}
				}

			document.getElementById("prev").firstElementChild.textContent = battleUserData.data.name
			myResult.displayResultRivalInputMode()

		}

	}


	createBattlePlayerResultArea(){
		const RESULT_DATA = document.getElementsByClassName("result_data")[1].firstElementChild.children
		document.getElementById("prev").firstElementChild.textContent = '待機中'

		for(let i=0; i<RESULT_DATA.length; i++){
			RESULT_DATA[i].lastElementChild.textContent = ''
			RESULT_DATA[i].lastElementChild.style.color = '#23c21f'
		}

		if(battleArea.RTCLine.textContent == 'タイムアウトしました。'){
			battleUserData.resultTimeoutPlayer()
		}

	}


	onBattleResultDisplay(snapshot){
		const uid = snapshot._delegate.ref._path.pieces_[1];
		const Info = snapshot._delegate.ref._path.pieces_[3]
		const SnapShotValue = snapshot.val()


		myResult.battlePlayerResultData[Info] = SnapShotValue

		if(playerStateChange.prevState == 'result'){
		const RESULT_DATA = document.getElementsByClassName("result_data")[1].firstElementChild.children
		const RESULT_ITEM = RESULT_DATA.length == 9 ? myResult.etypingPlus : myResult.normal

			const INDEX = RESULT_ITEM.indexOf(Info)

			if(INDEX >= 0){
				RESULT_DATA[INDEX].lastElementChild.textContent = SnapShotValue
			}

			document.getElementById("prev").firstElementChild.textContent = battleUserData.data.name
			myResult.displayResultRivalInputMode()
		}

		battleUserData.removePlayerStateChangeEvent()
	}

		displayResultRivalInputMode(){
			const INPUT_MODE = document.getElementsByClassName("result_data")[1].getElementsByClassName("input-mode")[0]
			const MODE = battleUserData.data.mode == 'roma' ? 'ローマ字' : 'かな'

			if(INPUT_MODE){
				INPUT_MODE.lastElementChild.textContent = MODE
			}
		}

	reBuildResultAria(){

		//e-typing plus用
		document.getElementById("RTCGamePlayScene").style.marginTop = ''
		document.getElementById("RTCGamePlayScene").style.display = 'none'
		document.getElementById("battle-display-option").style.display = 'none'
		document.getElementById("comment").style.display = 'none'
		document.getElementById("result").querySelector('article').style.height = '441px'
		document.getElementById("current").style.height = '414px'
		document.getElementById("exampleList").style.height = '345px'
		document.getElementsByClassName("result_data")[0].style.height = '365px'
		document.getElementById("prev").style.height = '414px'
		document.getElementsByClassName("result_data")[1].style.height = '365px'

		for(let i=0;i<2;i++){
			document.getElementsByClassName("result_data")[i].firstElementChild.insertAdjacentHTML('beforeend',
`<li class='input-mode'><div class="data"></div></li><li class='battle-result'><div class="data"></div></li>`)
		}

		const INPUT_MODE = document.getElementsByClassName("result_data")[0].getElementsByClassName("input-mode")[0]
		const MODE = setUp.typingMode == 'roma' ? 'ローマ字' : 'かな'

		if(INPUT_MODE){
			INPUT_MODE.lastElementChild.textContent = MODE
		}
	}


	checkResultDisplay(){
		myResult.reBuildResultAria()
		myResult.sendResult()
	}



}
let myResult



class DeleteUser {

	constructor(){

	}

	userTimeoutCheck(DeleteTimeStamp){

		firebaseDB.ref('users').once('value').then(users => {

			const PLAYERS_KEY = Object.keys(users.val())
			let updates = {}

			for(let i=0;i<PLAYERS_KEY.length;i++){

				const checkID = PLAYERS_KEY[i]
				const TIMEOUT_TIME = DeleteTimeStamp - users.val()[checkID].deleteTimeStamp

				if(TIMEOUT_TIME >= 100000 || !users.val()[checkID].deleteTimeStamp){
					//50秒TimeStampの更新が無ければユーザーを削除する
					updates['/users/' + checkID] = null;
					updates['/usersState/' + checkID] = null;

				}else if(TIMEOUT_TIME >= 20000){
					//20秒TimeStampの更新が無ければタイムアウト状態にする
					updates['/usersState/' + checkID + '/state'] = "timeOut"
				}

			}

			firebaseDB.ref().update(updates);
		});
	}

}
let deleteUser = new DeleteUser()


class PlayerStateChange {

	constructor(){
		this.prevState = 'idle'
		this.scriptUpdateNotify = `<a href="https://gf.qytechs.cn/ja/scripts/471999-e-%E5%AF%BE%E6%88%A6-e-typing/versions" style='font-weight:bold;' target="_blank">こちら</a>から最新のスクリプトに更新をお願いします`

	}


	update(state){

		if(this.prevState == 'oldVersion'){return;}

		let updates = {}

		if(state != 'afk' && state != 'timeOut'){
			this.prevState = state
		}

		updates['/usersState/' + myID + '/state'] = state;


		switch(state){
			case "oldVersion":

				const RTCLine = document.getElementsByClassName("RTCLine")[0]

				if(RTCLine){
					RTCLine.innerHTML = playerStateChange.scriptUpdateNotify
				}
				break;

			case "move":

				break;

			case "idle":

				break;

			case "preStart":
				battleUserData.removePlayerStateChangeEvent()
				battleUserData.addPlayerStateChangeEvent()


				break;

			case "matching":
				battleArea.displayReadyButton()

				break;


			case "ready":

				break;

			case "play":

				break;

			case "soloPlay":
				battleUserData.removePlayerStateChangeEvent()


				break;

			case "result":

				break;

			case "afk":

				break;

			case "timeOut":

				break;
		}

		firebaseDB.ref().update(updates)

	}

}
let playerStateChange



class MyData {

	constructor(){
		this.locationDateTimeStamp
		this.localDateTimeStamp
		this.myName
		this.UserCheckTimeCount = 0
		this.lineInput = ''
		this.clearCount = 0
		this.updateTimeInterval
		this.AFK_TIMEOUT = 60000;


		myResult = new MyResult()
		deleteUser = new DeleteUser()
		playerStateChange = new PlayerStateChange()

		this.update()
		this.loggedIn()
		this.getLocationDate().then( () => myData.startingClockTime())
	}



	update(){
		var updates = {};
		const NAME = localStorage.getItem("battleName")
		this.myName = NAME ? NAME : 'Guest'

		//ユーザーネーム更新
		updates['/usersState/' + myID + '/name'] = this.myName;
		updates['/usersState/' + myID + '/mode'] = setUp.typingMode;
		updates['/users/' + myID + '/version'] = GM_info.script.version
		updates['/users/' + myID + '/status/' + '/lineInput'] = '';
		updates['/users/' + myID + '/status/' + '/clearCount'] = 0;

		firebaseDB.ref().update(updates)
		playerStateChange.update(playerStateChange.prevState)
	}

	loggedIn(){

		document.getElementById("start_btn").style.display = 'block'
		document.getElementsByClassName("loading")[0].style.display = 'none'

		window.addEventListener('beforeunload', e => {
			playerStateChange.update('move')
			myData.resetResult()
		});

		if(window.parent.document.getElementsByClassName("pp_close").length){

			window.parent.document.getElementsByClassName("pp_close")[0].addEventListener('click',e => {
			playerStateChange.update('move')
			myData.resetResult()
		});

		}

		window.addEventListener('focus', e => {
			myData.update()
			myData.startingClockTime()
			myData.resetResult()
		});

	}

	resetResult(){

		let updates = {};
		updates['/users/' + myID + '/result/'] = ''
		updates['/usersState/' + myID + '/matchPlayerKey'] = null
		firebaseDB.ref().update(updates)

	}

	updateTimeStamp(){
		const newDate = new Date().getTime()
		var updates = {};
		const deleteTimeStamp = myData.locationDateTimeStamp + (newDate - myData.localDateTimeStamp)
		updates['/users/' + myID + '/deleteTimeStamp'] = deleteTimeStamp

		//30秒に一度、ルーム内のユーザーの存在をチェックする
		if(newDate - myData.UserCheckTimeCount >= 30000){
			myData.UserCheckTimeCount = newDate
			deleteUser.userTimeoutCheck(deleteTimeStamp)
		}

		firebaseDB.ref().update(updates);
	}


	startingClockTime(){
		//ユーザー確認用タイムスタンプを更新
		this.updateTimeStamp()

		clearInterval(this.updateTimeInterval)
		this.updateTimeInterval = setInterval(this.updateTimeStamp,5000)
	}

	async getLocationDate(){
		const resp = await fetch(window.location.href)

		//サーバー時刻のタイムスタンプ
		this.locationDateTimeStamp = await new Date(resp.headers.get("date")).getTime()
		//ローカル時刻タイムスタンプ
		this.localDateTimeStamp = new Date().getTime()


		return true
	}


}
let myData






let myID
const firebaseApp = firebase.initializeApp(firebaseConfig,"firebaseApp");
const firebaseDB = firebaseApp.database();

class LoginFirebase {

	constructor(){
		firebase.initializeApp(firebaseConfig)
		this.roginAnon()
	}


	roginAnon(){

		firebase.auth().signInAnonymously().catch(function(error) {
			// Handle Errors here.
			var errorCode = error.code;
			var errorMessage = error.message;

			console.log(errorCode);
			console.log( errorMessage);
			alert("RealTimeCombatting:Firebaseのサインインに失敗しました。");
			return false;
			// ...
		});

		firebase.auth().onAuthStateChanged(function(user) {

			if (user) {
				// User is signed in.
				myID = "U"+user.uid
				console.log('!!!')

				var path = firebaseDB.ref('users/' + myID);

				path.transaction(function(currentData) {
					//ユーザー情報を更新
					myData = new MyData()
				}).then( () => loginFirebase.versionCheck());
			}
		});

	}


	versionCheck(){
		firebaseDB.ref('newVersion').once('value').then(version => {
			const newVersion = version.val()

			if(+GM_info.script.version < +newVersion){
				playerStateChange.update('oldVersion')

			}

		}).catch(error => console.log(error));
	}
}

let loginFirebase













const typingAppMod = () => {


	//タイピング画面に移動した。

		if(setUp.battleSwitch){

			battleArea = new BattleArea()
			keyJudge = new KeyJudge()


		}

}


class SetMutationObserver {

	constructor(){
		this.observer
		this.elem = document.getElementById("app")
		this.config = {
			childList: true//「子ノード(テキストノードも含む)」の変化
		};

		this.set()
		this.startObserve()
	}

	startObserve(){
		this.observer.observe(this.elem, this.config);
	}

	stopObserve(){
		this.observer.disconnect();
	}

	set(){
		this.observer = new MutationObserver(function(event){

			const add = event[0].addedNodes[0]
			const remove = event[0].removedNodes[0]

			if(remove && remove.id == 'hands'){

				if(playerStateChange.prevState == "play"){
					setMutationObserver.setResultObserver()
				}else{
					playerStateChange.update('result')
				}

				return;
			}

			if(add && add.id == 'example_container'){
				//console.log('battleAreaSetUp')

				if(!setUp.battleSwitch){
					setMutationObserver.stopObserve()
					return;
				}

				if(battleUserData && battleUserData.data){
					battleUserData.deleteBattlePlayerEvents()
				}

				myResult.battlePlayerResultData = {}
				typingAppMod()
				return;
			}
		});
	}


	setResultObserver(){
		this.result = document.getElementById("result")

		this.resultObserver = new MutationObserver(function(){
			console.log('resultDisplay')
			myResult.checkResultDisplay()
			setMutationObserver.resultObserver.disconnect();

		});

		this.resultObserver.observe(this.result, this.config);
	}

}
let setMutationObserver






class KeyJudge {

	constructor(){
		this.wordReload = false;
		this.clearLine = 0
		this.wordSendSwitch = 1
		if(keyJudge){
			keyJudge.removeEvent()
		}

		this.addEvent()
	}

	addEvent(){
		this.Event = this.wait.bind(this)
		this.playEvent = this.startSpaceKey.bind(this)
		window.addEventListener("keydown",this.Event)
		window.addEventListener("keydown",this.playEvent)
	}

	removeSpaceKeyEvent(){
		window.removeEventListener("keydown",this.playEvent)
	}

	removeEvent(){
		window.removeEventListener("keydown",this.Event)
	}


	wait(event){
		setTimeout(() => this.checkType(event))
	}

	startSpaceKey(event){
		if(event.code == 'Space' || event.code == 'Digit1' || event.code == 'Digit2' || event.code == 'Digit3' || event.code == 'Digit0' || (event.code == 'KeyL' && setUp.typingMode == "roma")){
			playerStateChange.update('soloPlay')
			this.removeSpaceKeyEvent()
			this.removeEvent()
			battleArea.displayKeyboard()


		}
	}


	judge(event , sentenceText){
		let result
		if(setUp.typingMode == "roma"){
			result = sentenceText.textContent.slice(-1).toLowerCase() == event.key ? true:false
		}else if(setUp.typingMode == "eng"){
			result = sentenceText.textContent.slice(-1).replace("␣", " ") == event.key ? true:false
		}else if(setUp.typingMode == "kana"){
			result = this.createKanaChar(event).includes(sentenceText.textContent.slice(-1))
		}
		return result;
	}

	checkType(event){
		const sentenceText = document.getElementsByClassName("entered")[setUp.enteredClass]
		let key

		if(sentenceText){
			key = this.judge(event , sentenceText)
		}

		if(!sentenceText && this.wordReload){
			this.sendWordData('')
			this.wordReload = true

			if(!sentenceText){
				this.wordReload = false
			}

		}else if(sentenceText && key){

			this.sendWordData(sentenceText.textContent)
			this.wordReload = true

			if(!sentenceText){
				this.wordReload = false
			}

		}
	}

	createKanaChar(event){
		let char = windows_keymap[event.code] ? windows_keymap[event.code] : kana_keymap[event.key];

		if(event.shiftKey){
			if(event.code == "KeyE"){char[0] = "ぃ";}
			if(event.code == "KeyZ"){char[0] = "っ";}
		}

		if(event.shiftKey && event.key === "0"){char = ["を"];}

		return char;
	}

	sendWordData(text) {

		var updates = {}
		if(this.wordSendSwitch == 1 || !text){
			this.wordSendSwitch = ''
		}else{
			this.wordSendSwitch = 1
		}

		updates['/users/' + myID + '/status/' + '/lineInput'] = text.slice(-1) + this.wordSendSwitch;

		if(!text){
			this.clearLine++
			updates['/users/' + myID + '/status/' + '/clearCount'] = this.clearLine

		}

		firebaseDB.ref().update(updates)
	}

}
let keyJudge








class BattleArea {

	constructor(){
		battleUserData = new BattleUserData()
		if(setUp.soundEffectSwitch){
			soundEffect = new SoundEffect()
		}
		playerStateChange.update('preStart')

		const RTC = document.getElementById("RTCGamePlayScene")
		if(RTC){
			RTC.remove()
			document.getElementById("battle-display-option").remove()
			document.getElementById("user-state-area").remove()
		}


		this.createArea()
		myData.resetResult()
		this.Lstart = false

		this.joinLength = 0
		this.RTCLine
		this.myDisplayOption

		this.stateName = {
			 "preStart":'対戦募集',
			 "matching":'マッチ済み',
			 "ready":'マッチ済み',
			 "play":'対戦中',
			 "result":'リザルト',
			 "oldVersion":'マッチ不可',
			 "soloPlay":'ソロプレイ',
			 "move":'離席中',
			 "idle":'離席中',
			 "afk":'離席中',
			 "timeOut":'タイムアウト'
		}
	}


	displayReadyButton(){

		const LstartButton = document.getElementById("l-ready-button")

		if(LstartButton){
			document.getElementById("l-ready-button").style.display = 'block'
		}

		document.getElementById("ready-button").style.display = 'block'
	}

	hideReadyButton(){
		const LstartButton = document.getElementById("l-ready-button")

		if(LstartButton){
			document.getElementById("l-ready-button").style.display = 'none'
		}

		document.getElementById("ready-button").style.display = 'none'
	}


	displayKeyboard(){
		document.getElementById('virtual_keyboard').style.display = 'block';
		document.getElementById('hands').style.display = 'block';
		document.getElementById('RTCGamePlayScene').style.display = 'none';
		document.getElementById("battle-display-option").style.display = 'none'
		document.getElementById("user-state-area").style.display = 'none'
	}

	hideKeyboard(){
		document.getElementById('virtual_keyboard').style.display = 'none';
		document.getElementById('hands').style.display = 'none';
		document.getElementById('RTCGamePlayScene').style.display = 'block';
		document.getElementById("battle-display-option").style.visibility = 'hidden'
		document.getElementById("user-state-area").style.display = 'none'
	}


	addTable(){
		document.getElementById('user-state-area').insertAdjacentHTML('beforeend',`<table class='user-state-table'><tbody><tr><td>現在の参加者</td></tr></tbody></table>`)
	}

	updateJoinLength(){
			const JOIN_LENGTH_ELEMENT = document.getElementById("join-length")
			if(JOIN_LENGTH_ELEMENT){
				JOIN_LENGTH_ELEMENT.textContent = battleArea.joinLength
			}
	}

	createActiveUserTable(){
		document.getElementById('RTCGamePlayScene').insertAdjacentHTML('afterend',`<div id='user-state-area'></div>`)

		this.addTable()

		firebaseDB.ref('usersState').once('value').then(usersState => {

			const USERS_STATE = usersState.val()
			const USERS_KEY = Object.keys(USERS_STATE)

			for(let i=0;i<USERS_KEY.length;i++){
				const tableClass = document.getElementsByClassName("user-state-table")
				const table = tableClass[tableClass.length-1].firstElementChild
				if(USERS_STATE[USERS_KEY[i]].name){
					if(table.children.length < 4){
						battleArea.joinLength ++
						table.insertAdjacentHTML('beforeend',`<tr id='${USERS_KEY[i]}' class='${USERS_KEY[i] == myID ? 'mine': ''}'><td class='state'>${battleArea.stateName[USERS_STATE[USERS_KEY[i]].state]}</td></tr>`)
					}else{
						battleArea.addTable()
						i--
					}
				}
			}

			battleArea.updateJoinLength()

		})

		setTimeout(() => {
			const tableClass = document.getElementsByClassName("user-state-table")
			if(tableClass[0].firstChild.children.length == 1){
				battleArea.RTCLine.innerHTML = 'データを取得できませんでした。<br>シークレットウィンドウや別のブラウザでお試しください。'
				battleArea.RTCLine.style.fontSize = 'larger'
			}
		},2000)


	}


	createArea(){
		document.getElementById('virtual_keyboard').style.display = 'none';
		document.getElementById('hands').style.display = 'none';

		document.getElementById('start_msg').insertAdjacentHTML("afterbegin" ,
`<div class="loading ready-btn" id='ready-button'>準備完了</div>
${setUp.typingMode == 'roma' ? `<div class="loading ready-btn" id='l-ready-button'>Lスタートで準備完了</div>` : ''}`)


		document.getElementById("ready-button").addEventListener('click', event => {
			event.target.style.display = 'none'
			const LstartButton = document.getElementById("l-ready-button")

			if(LstartButton){
				LstartButton.style.display = 'none'
			}

			battleArea.Lstart = false
			playerStateChange.update('ready')
			if(setUp.soundEffectSwitch){
				soundEffect.play('ready')
			}
		})

		const LstartButton = document.getElementById("l-ready-button")

		if(LstartButton){

			LstartButton.addEventListener('click', event => {
				event.target.style.display = 'none'
				document.getElementById("ready-button").style.display = 'none'
				battleArea.Lstart = true
				playerStateChange.update('ready')
				if(setUp.soundEffectSwitch){
					soundEffect.play('ready')
				}
			})

		}

		const SEARCH_PLAYER_GUIDE = playerStateChange.prevState != "oldVersion" ? `対戦相手を探しています (<span id='join-length'>0</span>人参加中)` : playerStateChange.scriptUpdateNotify;


		document.getElementById('example_container').insertAdjacentHTML('afterend',
`<div id='battle-display-option'>
<div class="display-head"><strong>表示方法</strong></div>
<div><label><input type="radio" name="layout" id="keyboard-display" ${localStorage.getItem('battle-display-option') == 'keyboard-display' || !localStorage.getItem('battle-display-option')? 'checked' : ''}>キーボード</label>
<label><input type="radio" name="layout" id="player-display" ${localStorage.getItem('battle-display-option') == 'player-display' ? 'checked' : ''}>対戦相手</label></div>
</div>
<div id="RTCGamePlayScene"><table class='user-table' rules="all" border="1"><tbody>
<tr>
<td class='user-name'></td>
<td><span  class="RTCLine" style='color:#7b7a7a;'>${SEARCH_PLAYER_GUIDE}</span></td>
<td class="InputMode"></td></tr></tbody></table></div>`)

		this.RTCLine = document.getElementsByClassName("RTCLine")[0]

		document.getElementById("battle-display-option").addEventListener('change', event => {
			localStorage.setItem('battle-display-option',event.target.id)
			battleArea.sendBattleDisplayOption(event.target.id)
		})
		this.myDisplayOption = document.querySelector("input[name='layout']:checked").id
		this.sendBattleDisplayOption(this.myDisplayOption)
		this.createActiveUserTable()
	}

	sendBattleDisplayOption(displayOption){
		let updates = {}
		updates['/usersState/' + myID + '/displayOption'] = displayOption
		this.myDisplayOption = displayOption
		firebaseDB.ref().update(updates)
	}



	addBattleStatusTable(userName,key,inputType){
		const INPUT_TYPE = inputType != 'kana' ? 'ローマ字' : 'かな'
		document.getElementsByClassName("user-name")[0].textContent = userName
		document.getElementsByClassName("InputMode")[0].textContent = INPUT_TYPE
		battleArea.RTCLine.textContent = '対戦相手の準備完了を待ってます'
		battleArea.RTCLine.insertAdjacentHTML('afterend',`<div id="battle-progress-bar"></div>`)

		document.getElementById("battle-progress-bar").style.width = (battleArea.RTCLine.parentElement.clientWidth-3) + 'px'

	}

	removeBattleStatusTable(){
		setTimeout( () => {
			if(playerStateChange.prevState == "preStart"){
				battleArea.RTCLine.textContent = '対戦相手を探しています'
			}
		},2000)
		document.getElementsByClassName("user-name")[0].textContent = ''
		document.getElementsByClassName("InputMode")[0].textContent = ''
		battleArea.RTCLine.style.color = '#7b7a7a'
		battleArea.RTCLine.textContent = '離脱しました'


		if(document.getElementById("battle-progress-bar") != null){
			document.getElementById("battle-progress-bar").remove()
		}

	}

}
let battleArea



class BattleUserData{

	constructor(){
		this.data
	}


	onUpdateUserStatus(snapshot){
		const uid = snapshot._delegate.ref._path.pieces_[1];
		const Update_Info = snapshot._delegate.ref._path.pieces_[3]
		const SnapShotValue = snapshot.val()

		switch(Update_Info){
			case "clearCount":
				document.getElementById("battle-progress-bar").style.transform = `scaleX(${(100 - (+SnapShotValue / 15 * 100))/100})`

				break;

			case "lineInput":
				if(SnapShotValue){
					battleArea.RTCLine.textContent = (battleArea.RTCLine.textContent + SnapShotValue.slice(0,1)).substr( -28, 28 );
				} else{
					battleArea.RTCLine.textContent = "";
				}
				break;
		}
	}

	findBattlePlayer(name,key,mode,state,displayOption){

		battleUserData.data = {
			name:name,
			key:key,
			mode:mode,
			display:displayOption,
			state:state
		}

		battleArea.addBattleStatusTable(battleUserData.data.name , battleUserData.data.key , battleUserData.data.mode)
		playerStateChange.update('matching')

		if(setUp.soundEffectSwitch){
			soundEffect.play('match')
		}

		let updates = {}
		updates['/usersState/' + myID + '/matchPlayerKey'] = battleUserData.data.key

		firebaseDB.ref().update(updates)

		document.getElementById("start_msg").getElementsByTagName('em')[0].textContent = 'スペースキーを押すと一人プレイで開始します'

		const userTable = document.getElementById(battleUserData.data.key)

		if(userTable){
			userTable.style.fontWeight = 'bold'
		}

	}

	addBattlePlayerEvents(){

		if(battleArea.myDisplayOption != 'keyboard-display'){
			firebaseDB.ref('users/' + battleUserData.data.key + '/status').on('child_changed', battleUserData.onUpdateUserStatus);
		}

		firebaseDB.ref('users/' + battleUserData.data.key + '/result').on('child_added', myResult.onBattleResultDisplay);
	}


	deleteBattlePlayerEvents(){
		firebaseDB.ref('users/' + battleUserData.data.key + '/status').off('child_changed');
		firebaseDB.ref('users/' + battleUserData.data.key + '/result').off('child_added');
	}

	addPlayerStateChangeEvent(){
		firebaseDB.ref('usersState/').on('child_changed', battleUserData.onChangeUserState);
		firebaseDB.ref('usersState/').on('child_removed', battleUserData.onRemoveUserState);
	}


	removePlayerStateChangeEvent(){
		firebaseDB.ref('usersState/').off('child_changed');
		firebaseDB.ref('usersState/').off('child_removed');
	}

	lostBattlePlayer(){
		this.deleteBattlePlayerEvents()
		const userTable = document.getElementById(battleUserData.data.key)

		if(userTable){
			userTable.style.fontWeight = ''
		}

		battleUserData.data = null
		battleArea.hideReadyButton()
		playerStateChange.update('preStart')
		document.getElementById("start_msg").getElementsByTagName('em')[0].textContent = 'スペースキーで開始'
		battleArea.removeBattleStatusTable()


	}


	resultTimeoutPlayer(){
		this.deleteBattlePlayerEvents()
		const RESULT_DATA = document.getElementsByClassName("result_data")[1].firstElementChild.children
		document.getElementById("prev").firstElementChild.textContent = 'タイムアウト'

		for(let i=0; i<RESULT_DATA.length; i++){
			RESULT_DATA[i].lastElementChild.textContent = '-'
			RESULT_DATA[i].lastElementChild.style.color = '#23c21f'
		}

	}

	updateUserTable(uid, name, state){
		const userTable = document.getElementById(uid)
		if(!name){return;}

		if(userTable){
			userTable.getElementsByClassName("state")[0].textContent = battleArea.stateName[state];
		}else{
			const tableClass = document.getElementsByClassName("user-state-table")
			const table = tableClass[tableClass.length-1].firstElementChild

			if(table.children.length < 4){
				battleArea.joinLength ++
				table.insertAdjacentHTML('beforeend',`<tr id='${uid}'><td class='state'>${battleArea.stateName[state]}</td></tr>`)
				battleArea.updateJoinLength()
			}else{
				battleArea.addTable()
				this.updateUserTable(uid, name, state)
			}

		}

	}

	removeUserTable(uid){
		const userTable = document.getElementById(uid)

		if(userTable){
			battleArea.joinLength --
			userTable.remove()
			battleArea.updateJoinLength()
		}
	}


	battleStart(){

		document.dispatchEvent( new KeyboardEvent("keydown",{
			// keyCode:32 スペースキー keyCode:76 Lキー
			// keyプロパティ & codeプロパティでは開始できませんでした。
			keyCode: (battleArea.Lstart ? 76 : 32)

		}))

		battleUserData.addBattlePlayerEvents()

		if(battleUserData.data.displayOption == 'keyboard-display'){
			keyJudge.removeEvent()
		}


		if(battleArea.myDisplayOption == 'keyboard-display'){
			battleArea.displayKeyboard()
		}else{
			battleArea.hideKeyboard()
		}

		battleArea.RTCLine.textContent = '';
		playerStateChange.update('play')
		keyJudge.removeSpaceKeyEvent()

	}

	rivalReady(){
		battleArea.RTCLine.style.color = ''
		battleArea.RTCLine.textContent = '準備完了';
		if(setUp.soundEffectSwitch){
			soundEffect.play('ready')
		}
	}

	onChangeUserState(snapshot){
		const uid = snapshot._delegate.ref._path.pieces_[1];
		const name = snapshot.val().name
		const state = snapshot.val().state
		const mode = snapshot.val().mode
		const displayOption = snapshot.val().displayOption
		const matchPlayerKey = snapshot.val().matchPlayerKey

		//対戦相手のstateを更新する
		if(battleUserData.data && uid == battleUserData.data.key){
			battleUserData.data.state = state;
		}

		//対戦相手のdisplayOptionを更新する
		if(battleUserData.data && uid == battleUserData.data.key){
			battleUserData.data.displayOption = displayOption;
		}

		//参加者のstateを更新する
		if(playerStateChange.prevState != "play"){
			battleUserData.updateUserTable(uid, name, state)
		}

		if(!battleUserData.data){

			//お互いに状態がpreStartのプレイヤーをマッチさせる
			if( playerStateChange.prevState == "preStart"){

				//先にマッチ画面で待機していたプレイヤー
				if(uid != myID && state == 'preStart'){
					battleUserData.findBattlePlayer(name , uid , mode, state , displayOption)
				}

				//後から入室したプレイヤー
				if(matchPlayerKey == myID && state == 'matching'){
					battleUserData.findBattlePlayer(name , uid , mode, state , displayOption)
				}

			}
		}


		if(battleUserData.data){

			if(battleUserData.data.state == 'ready' && battleUserData.data.key == uid){
				battleUserData.rivalReady()
			}

			//自分と相手が準備完了したら同時に開始。
			if( (battleUserData.data.key == uid || myID == uid) && (battleUserData.data.state == 'ready' && playerStateChange.prevState == "ready") ){
				battleUserData.battleStart()
			}

			//相手が離脱した。
			if(uid == battleUserData.data.key && (state == 'soloPlay' || state == 'idle' || state == 'timeOut' || state == 'move' || state == 'afk')){

				if(playerStateChange.prevState != "play" && playerStateChange.prevState != "result"){
					battleUserData.lostBattlePlayer()
				}else if(playerStateChange.prevState == "play"){
					battleArea.RTCLine.textContent = 'タイムアウトしました。';
					battleUserData.deleteBattlePlayerEvents()
				}else if(playerStateChange.prevState == "result"){
					battleUserData.resultTimeoutPlayer()
				}
				myData.resetResult()
			}


		}

	}

	onRemoveUserState(snapshot){
		const uid = snapshot._delegate.ref._path.pieces_[1];

		if( playerStateChange.prevState != "play"){
			battleUserData.removeUserTable(uid)
		}
	}


}
let battleUserData




////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////







const kana_keymap = {
	0: ["わ"],
	1: ["ぬ"],
	"!": ["ぬ"],
	2: ["ふ"],
	3: ["あ"],
	4: ["う"],
	5: ["え"],
	6: ["お"],
	7: ["や"],
	8: ["ゆ"],
	9: ["よ"],
	"-": ["ほ","-"],
	"q": ["た"],
	"Q": ["た"],
	"w": ["て"],
	"W": ["て"],
	"e": ["い"],
	"E": ["い"],
	"r": ["す"],
	"R": ["す"],
	"t": ["か"],
	"T": ["か"],
	"y": ["ん"],
	"Y": ["ん"],
	"u": ["な"],
	"U": ["な"],
	"i": ["に"],
	"I": ["に"],
	"o": ["ら"],
	"O": ["ら"],
	"p": ["せ"],
	"P": ["せ"],
	"a": ["ち"],
	"A": ["ち"],
	"s": ["と"],
	"S": ["と"],
	"d": ["し"],
	"D": ["し"],
	"f": ["は"],
	"F": ["は"],
	"g": ["き"],
	"G": ["き"],
	"h": ["く"],
	"H": ["く"],
	"j": ["ま"],
	"J": ["ま"],
	"k": ["の"],
	"K": ["の"],
	"l": ["り"],
	"L": ["り"],
	"z": ["つ"],
	"Z": ["つ"],
	"x": ["さ"],
	"X": ["さ"],
	"c": ["そ"],
	"C": ["そ"],
	"v": ["ひ"],
	"V": ["ひ"],
	"b": ["こ"],
	"B": ["こ"],
	"n": ["み"],
	"N": ["み"],
	"m": ["も"],
	"M": ["も"],
	",": ["ね",","],
	"<": ["、"],
	".": ["る","."],
	">": ["。"],
	"/": ["め","/"],
	"?": ["・"],
	"#": ["ぁ"],
	"$": ["ぅ"],
	"%": ["ぇ"],
	"'": ["ゃ","’","'"],
	"^": ["へ"],
	"~": ["へ"],
	"&": ["ぉ"],
	"(": ["ゅ"],
	")": ["ょ"],
	'|': ["ー"],
	"_": ["ろ"],
	"=": ["ほ"],
	"+": ["れ"],
	";": ["れ"],
	'"': ["ふ","”","“","\""],
	"@": ["゛"],
	'`': ["゛"],
	"[": ["゜"],
	']': ["む"],
	"{": ["「"],
	'}': ["」"],
	":": ["け"],
	"*": ["け"]
}

const windows_keymap = {
	'IntlYen': ["ー","¥","\\"],
	"IntlRo": ["ろ","¥","\\"],
	"Space": [" "],
	"Numpad1": [],
	"Numpad2": [],
	"Numpad3": [],
	"Numpad4": [],
	"Numpad5": [],
	"Numpad6": [],
	"Numpad7": [],
	"Numpad8": [],
	"Numpad9": [],
	"Numpad0": [],
	"NumpadDivide": [],
	"NumpadMultiply": [],
	"NumpadSubtract": [],
	"NumpadAdd": [],
	"NumpadDecimal": []
}



class SetUp {

	constructor(){

		this.typingMode = 'roma'
		this.enteredClass = 2
		this.battleSwitch = true
		this.soundEffectSwitch = true

		this.checkTypingMode()
		loginFirebase = localStorage.getItem("battle-option") != "false" ? new LoginFirebase() : null
	}


	checkDisplayMenu(){
		const config = {
			childList: true//「子ノード(テキストノードも含む)」の変化
		};

		const Observe = new MutationObserver(function(event){

			setTimeout( () => {
				setUp.createMenu()
				setMutationObserver = new SetMutationObserver()
			})

			Observe.disconnect();
		});

		Observe.observe(document.getElementById("app"), config);

	}


	checkTypingMode(){

		if(location.href.match(/kana\.1/)){
			this.typingMode = "kana"
			this.enteredClass = 1
		}else if(location.href.match(/std\.2/) || location.href.match(/lstn\.4/)){
			this.typingMode = "eng"
			this.enteredClass = 1
		}else{
			this.typingMode = "roma"
			this.enteredClass = 2
		}

		this.checkDisplayMenu()
	}

	createMenu(){

		const NAME = localStorage.getItem("battleName")
		this.battleSwitch = localStorage.getItem("battle-option") == "false" ? false : true;
		this.soundEffectSwitch = localStorage.getItem("sound-option") == "false" ? false : true;


		if(!localStorage.getItem("battle-option")){
			localStorage.setItem("battle-option",'true')
		}

		if(loginFirebase){
			document.getElementById("start_btn").style.display = this.battleSwitch == false ? '' : 'none'
			document.getElementById("start_btn").insertAdjacentHTML('afterend',`<div class='loading'>対戦データベースに接続中</div>`)
		}

		this.addCss()

		const FUNC_VIEW = document.getElementById("func_view")
		setTimeout( () => FUNC_VIEW.style.height = FUNC_VIEW.clientHeight + 30 + "px", 100)
		FUNC_VIEW.insertAdjacentHTML('beforeend' ,
									 `<div id='battle-option-container'>
<label><small>対戦機能</small>
<input id="battle-option" type="checkbox" style="display:none;" ${this.battleSwitch == false ? "" : "checked"}>
<div id="battle-effect-btn" style="margin-left:4px;margin-right: 18px;" class="switch_btn"><a class="on_btn btn show">ON</a>
<a class="off_btn btn" style="display:${this.battleSwitch == false ? "block" : ""};">OFF</a></div>
</label>

<label><small>マッチ音</small>
<input id="sound-option" type="checkbox" style="display:none;" ${this.soundEffectSwitch == false ? "" : "checked"}>
<div id="sound-effect-btn" style="margin-left:4px;margin-right: 18px;" class="switch_btn"><a class="on_btn btn show">ON</a>
<a class="off_btn btn" style="display:${this.soundEffectSwitch == false ? "block" : ""};">OFF</a></div>
</label>

<input type="button" id="battle-name" value="対戦ネーム変更">
</div>`)

		if(!NAME){
			localStorage.setItem("battleName" , 'Guest')
		}


		document.getElementById("battle-name").addEventListener("click", event => {
			const NAME = window.prompt("対戦時のユーザー名を入力してください", myData.myName)

			if(NAME){
				localStorage.setItem("battleName" , NAME)
				myData.update()
			}
		})

		document.getElementById("battle-option").addEventListener("change" , event => {
			localStorage.setItem("battle-option" , event.target.checked);

			if(event.target.checked){
				document.querySelector("#battle-effect-btn .off_btn").style.display = ""
				setUp.battleSwitch = true;

				if(!loginFirebase){
					loginFirebase = new LoginFirebase()
					document.getElementById("start_btn").style.display = setUp.battleSwitch == false ? '' : 'none'
					document.getElementById("start_btn").insertAdjacentHTML('afterend',`<div class='loading'>対戦データベースに接続中</div>`)
				}

			}else{
				document.querySelector("#battle-effect-btn .off_btn").style.display = "block"
				setUp.battleSwitch = false;
			}
		})

		document.getElementById("sound-option").addEventListener("change" , event => {
			localStorage.setItem("sound-option" , event.target.checked);

			if(event.target.checked){
				document.querySelector("#sound-effect-btn .off_btn").style.display = ""
				setUp.soundEffectSwitch = true;

			}else{
				document.querySelector("#sound-effect-btn .off_btn").style.display = "block"
				setUp.soundEffectSwitch = false;
			}
		})

	}

	addCss(){

		document.getElementById("app").insertAdjacentHTML('afterend',`<style>


.loading{
    color: #fff;
    font-size: 12px;
    font-weight: bold;
    background-color: #057fff;
    width: 160px;
    height: 45px;
    margin: 0 auto;
    text-align: center;
    line-height: 45px;
    overflow: hidden;
    border-radius: 3px;
}


#battle-option-container{
    display: flex;
    align-items: center;
    justify-content: center;
}

#battle-option-container label{
cursor: pointer;

}

#battle-name{
    width: 7rem;
    margin: 2px;
cursor: pointer;
}


.user-table{
width: 100%;
position: relative;
height: 68px;
left: 0;
right: 0;
margin: auto;
}

.user-table tr{
font-weight:bold;
height: 3rem;
}

.user-name{
    font-size: 1rem;
    width: 90px;
text-align: center;
}

.RTCLine{
max-width: 350px;
white-space: nowrap;
overflow:hidden;
width: 68%;
color:#ffd0a6;
font-size: 26px;
font-weight: normal;
}

.InputMode{
font-size: 1rem;
    width: 95px;
text-align: center;
}

.clear-line{
font-size: 1rem;
text-align: center;
}

#RTCGamePlayScene{
margin: 8px;
margin-top:1rem;
}

.display-head{
margin-left: 0.2rem;
font-size: 1rem;
margin-bottom: 0.3rem;
}

#battle-display-option{
margin-left: 4rem;
display: flex;
flex-direction: column;
}


#battle-progress-bar{
    position: absolute;
    bottom: 1px;
    background-color: #bcbcbc;
    height: 4px;
    transform-origin: left top;
}

#user-state-area{
    display: flex;
    justify-content: space-evenly;
    align-items: flex-start;
    margin-top: 1.3rem;
}


.user-state-table, .user-state-table td, .user-state-table th{
	border: 1px solid #595959;
	border-collapse: collapse;
    text-align: center;
}


.user-state-table td, .user-state-table th {
	padding: 3px;
	width: 100px;
	height: 25px;

}

.mine{
font-weight:bold;
}

.ready-btn{
cursor: pointer;
display: none;
position: absolute;
bottom: -23px;
width: 130px;
}

#ready-button{
left: 28px;
font-size: 1.1rem;
}

#l-ready-button{
right: 26px;
font-size: 0.8rem;
}
</style>`)

	}



}

const setUp = new SetUp()

const SoundURL = {
	'match':'https://dl.dropboxusercontent.com/scl/fi/14ovgqnxribt5yw0bcpzv/match.mp3?rlkey=6lsizg3oda06psbn9zoip1lyd&dl=0',
	'ready':'https://dl.dropboxusercontent.com/scl/fi/p5u2luqb36st0qt18qkfi/ready.mp3?rlkey=i1ibbnb2mh6q7fmrtjrqw2wqy&dl=0'


}
window.AudioContext = window.AudioContext || window.webkitAudioContext;
class SoundEffect {

	constructor(){
		this.match = new AudioContext();
		this.ready = new AudioContext();
		this.audioBuffer = {}

		this.loadSoundEffect('match')
		this.loadSoundEffect('ready')

	}

	loadSoundEffect(soundName){
		fetch(SoundURL[soundName]).then(function(response) {
			return response.arrayBuffer();
		}).then(function(arrayBuffer) {
			soundEffect[soundName].decodeAudioData(arrayBuffer, function(buffer) {
				soundEffect.audioBuffer[soundName] = buffer;
			});
		})
	}

	play(soundName){
		let playGain = this[soundName].createGain();
		let playSrc = this[soundName].createBufferSource();

		playSrc.buffer = this.audioBuffer[soundName];
		playSrc.connect(playGain);
		playGain.connect(this[soundName].destination);
		playGain.gain.value = 0.2
		playSrc.start(0);
	}


}
let soundEffect




QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址