e-対戦 [e-typing]

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

Per 11-08-2023. Zie de nieuwste versie.

Voor het installeren van scripts heb je een extensie nodig, zoals Tampermonkey, Greasemonkey of Violentmonkey.

Voor het installeren van scripts heb je een extensie nodig, zoals Tampermonkey of Violentmonkey.

Voor het installeren van scripts heb je een extensie nodig, zoals Tampermonkey of Violentmonkey.

Voor het installeren van scripts heb je een extensie nodig, zoals Tampermonkey of Userscripts.

Voor het installeren van scripts heb je een extensie nodig, zoals {tampermonkey_link:Tampermonkey}.

Voor het installeren van scripts heb je een gebruikersscriptbeheerder nodig.

(Ik heb al een user script manager, laat me het downloaden!)

Voor het installeren van gebruikersstijlen heb je een extensie nodig, zoals {stylus_link:Stylus}.

Voor het installeren van gebruikersstijlen heb je een extensie nodig, zoals {stylus_link:Stylus}.

Voor het installeren van gebruikersstijlen heb je een extensie nodig, zoals {stylus_link:Stylus}.

Voor het installeren van gebruikersstijlen heb je een gebruikersstijlbeheerder nodig.

Voor het installeren van gebruikersstijlen heb je een gebruikersstijlbeheerder nodig.

Voor het installeren van gebruikersstijlen heb je een gebruikersstijlbeheerder nodig.

(Ik heb al een beheerder - laat me doorgaan met de installatie!)

// ==UserScript==
// @name         e-対戦 [e-typing]
// @namespace    http://tampermonkey.net/
// @version      5.5
// @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==


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
		}

		battleUserData.removePlayerStateChangeEvent()
		myResult.displayResultRivalInputMode()
	}

		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.sendResult()
		myResult.reBuildResultAria()
	}



}
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://greatest.deepsurf.us/ja/scripts/471999-e-%E5%AF%BE%E6%88%A6-e-typing/versions" style='font-weight:bold;'>こちら</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.timeStampForAFK
		this.isWrittenAFKState
		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 = {};
		this.myName = localStorage.getItem("battleName")

		//ユーザーネーム更新
		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
		myData.afkStateChange(newDate)

		//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()

		this.timeStampForAFK = new Date().getTime();

		this.afkEvent = this.afkReturnStateChange.bind(this)
		document.body.addEventListener("keydown",this.afkEvent);
		document.body.addEventListener("mousedown",this.afkEvent);
		document.body.addEventListener("mousemove",this.afkEvent);

		return true
	}


	afkReturnStateChange(){
		this.timeStampForAFK = new Date().getTime();

		if(this.isWrittenAFKState){
			this.isWrittenAFKState = false;
			playerStateChange.update(playerStateChange.prevState)
			this.updateTimeStamp()
		}

	}

	afkStateChange(newDate){
		if(newDate > this.AFK_TIMEOUT + this.timeStampForAFK && (playerStateChange.prevState == "matching" || playerStateChange.prevState == "preStart")){

			if(!this.isWrittenAFKState){
				this.isWrittenAFKState = true;
				playerStateChange.update('afk')
			}

		}


	}

}
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")
		FUNC_VIEW.style.height = (FUNC_VIEW.clientHeight ? FUNC_VIEW.clientHeight : 26) + 30 + "px"
		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="text" id="battle-name" placeholder="Name" value="${NAME ? NAME : 'Guest'}" maxlength="11">
</div>`)

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


		document.getElementById("battle-name").addEventListener("change", event => {
			localStorage.setItem("battleName" , event.target.value)
			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-name{
    width: 7rem;
    margin: 2px;
}


.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