[e-typing] e-対戦

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

Ekde 2023/07/29. Vidu La ĝisdata versio.

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Userscripts to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==UserScript==
// @name         [e-typing] e-対戦
// @namespace    http://tampermonkey.net/
// @version      0.3
// @description  e-typingに対戦機能を追加したい
// @author       Toshi
// @match        https://www.e-typing.ne.jp/app/jsa_std*
// @exclude      https://www.e-typing.ne.jp/app/ad*
// @icon         https://www.google.com/s2/favicons?sz=64&domain=e-typing.ne.jp
// @grant        none
// @license MIT
// @require      https://www.gstatic.com/firebasejs/7.2.1/firebase-app.js
// @require      https://www.gstatic.com/firebasejs/7.2.1/firebase-auth.js
// @require      https://www.gstatic.com/firebasejs/7.2.1/firebase-database.js





// ==/UserScript==

const firebaseConfig = {
	apiKey: "AIzaSyDsHiPII5dgN_AEGwOtMehyveucoF4Twvs",
	databaseURL: "https://e-typing-battle-default-rtdb.firebaseio.com"
};


class MyResult{

	constructor(){
		this.score = 0
		this.time = 0
		this.typeCount = 0
		this.missCount = 0
		this.wpm = 0
		this.latency = 0
		this.rkpm = 0

	}


	sendResetMyResult(){
		let updates = {};

		//ユーザーネーム更新
		updates['/users/' + myID + '/result/' + '/score'] = 0;
		updates['/users/' + myID + '/result/' + '/time'] = 0;
		updates['/users/' + myID + '/result/' + '/typeCount'] = 0;
		updates['/users/' + myID + '/result/' + '/missCount'] = 0;
		updates['/users/' + myID + '/result/' + '/wpm'] = 0;
		updates['/users/' + myID + '/result/' + '/latency'] = 0;
		updates['/users/' + myID + '/result/' + '/rkpm'] = 0;

		firebase.database().ref().update(updates)
	}



}
let myResult


class MyStatus{

	constructor(){
		this.lineInput = ''
		this.clearCount = 0

	}


	sendResetMyStatus(){
		let updates = {};

		//ユーザーネーム更新
		updates['/users/' + myID + '/status/' + '/lineInput'] = '';
		updates['/users/' + myID + '/status/' + '/clearCount'] = 0;


		firebase.database().ref().update(updates)
	}



}
let myStatus


class MyData {

	constructor(){
		this.prevState
		this.locationDateTimeStamp
		this.localDateTimeStamp
		this.myName
		myResult = new MyResult()
		myStatus = new MyStatus()
	}

	update(){
		var updates = {};
		this.myName = localStorage.getItem("battleName")

		//ユーザーネーム更新
		updates['/usersState/' + myID + '/name'] = this.myName;
		updates['/usersState/' + myID + '/state'] = this.prevState = "idle";

		firebase.database().ref().update(updates)
		myResult.sendResetMyResult()
		myStatus.sendResetMyStatus()
	}

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


		//30秒に一度、ルーム内のユーザーの存在をチェックする
		/* 		if(isEnter && !playing && new_Date - RoomUserAfkWriteClock >= 30000){
			RoomUserAfkWriteClock = new_Date
			roomUserTimeoutCheck(deleteTimeStamp)
		} */

		firebase.database().ref().update(updates);
	}


	startingClockTime(){

		/* 		if(roomID == null && !wholeRoom){
			addWholeRoomsUpdateEvent()
			setTimeout(()=> {document.getElementById("noRoomMes").textContent = "現在ルームが存在しません。";},700)
		} */

		//ユーザー確認用タイムスタンプを更新
		this.updateTimeStamp()
		setInterval(this.updateTimeStamp,5000)
		document.getElementById("start_btn").visibility = ''
	}

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

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

		//LocationDateTimeStamp + (new Date().getTime() - LocalDateTimeStamp)
		//サーバー時刻のタイムスタンプ + (現在のローカル時刻 - ローカル時刻タイムスタンプ)
		//上記の計算で環境の違いでズレない時刻を取得

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

		//サーバー時刻取得後、現在ログインしていない部屋とユーザー情報を削除
		//deleteIdlePlayerAndRoom();
		return true
	}

}
let myData


let myID
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 = firebase.database().ref('users/' + myID);

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

	}







}

let loginFirebase = new LoginFirebase()





let typingAppModInterval
let createOptionInterval






const createOption = () => {
	const FUNC_VIEW = document.getElementById("func_view")

	//設定エリアが表示された
	if(FUNC_VIEW){
		setUpMenu(FUNC_VIEW)
		clearInterval(createOptionInterval)
	}
}


const typingAppMod = () => {

	const example_container = document.getElementById("example_container") //iframe内の要素を取得

	//タイピング画面に移動した。
	if(example_container){

		battleArea = new BattleArea()

		keyJudge = new KeyJudge()

		if(setUp.battleSwitch){
			keyJudge.addEvent()
		}

		clearInterval(typingAppModInterval)
		typingAppModInterval = null
	}

}




const createEventInTypingApp = (() => {

		typingAppModInterval = setInterval(typingAppMod , 50)
		createOptionInterval = setInterval(createOption , 100)

		/*
		//タイピング中にEscキーを押したらtypingAppModを実行
		window.addEventListener("keydown", event => {
			// document.getElementById("miss_type_screen") {タイピングワードが表示される要素}
			// document.getElementById("miss_type_screen") 要素が存在する場合のみ即時リトライを適用。(打ち切り時の結果画面等では無効化。)
			if(!typingAppModInterval && event.key == "Escape" && document.getElementById("exampleText") != null && document.getElementById("miss_type_screen") != null){
				typingAppModInterval = setInterval(typingAppMod , 50)
			}
			if(!typingAppModInterval && event.code == "KeyR" && document.getElementById("replay_btn") != null){
				typingAppModInterval = setInterval(typingAppMod , 50)
			}
		},true)

		//打ち切り時にやり直しボタン or ミスだけボタンをクリックしたらtypingAppModを実行
		window.addEventListener("click", event => {

			if(!typingAppModInterval && event.target.id == "replay_btn" || event.target.id == "miss_only_btn"){
				typingAppModInterval = setInterval(typingAppMod , 50)
			}
		},true)
*/

})()



class KeyJudge {

	constructor(){
		this.wordReload = false;
		this.clearLine = 0
	}

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

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

	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;
	}

	keyDown(event){

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

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

		if(event.key == "Escape"){
			this.wordReload = false
		}

		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 = {}
		updates['/users/' + myID + '/status/' + '/lineInput'] = text;

		const MY_LINE = document.getElementsByClassName("mine")[0].getElementsByClassName('RTCLine')[0]
		MY_LINE.textContent = text

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

			const MY_CLEAR_COUNT = document.getElementsByClassName("mine")[0].getElementsByClassName('clear-count')[0]
			MY_CLEAR_COUNT.textContent = this.clearLine
		}

		firebase.database().ref().update(updates)
	}

}
let keyJudge






class BattleArea {

	constructor(){
		this.battleUser
		this.updatePreStart()
		this.createArea()
		battleUserData = new BattleUserData()
	}

	searchPreStartPlayer(){
		firebase.database().ref('usersState/').once('value').then(users => {
			const USERS = users.val()
			const USERS_KEY = Object.keys(USERS)
			this.battleUser = null

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

				if(USERS_KEY[i] != myID && USERS[USERS_KEY[i]].state == 'preStart'){
					this.battleUser = {
						name:USERS[USERS_KEY[i]].name,
						key:USERS_KEY[i],
						mode:'roma'
					}
					this.createBattleTable()
					this.updatePlay(USERS_KEY[i])
					return
					break;
				}

			}

			if(!this.battleUser){
				this.createBattleTable('wait')
			}
		})
	}

	createBattleTable(wait){
		for(let i=0;i < 2 ;i++){
			if(!i){
				this.addbattleStatusTable(myData.myName ,myID, setUp.typingMode)
			}else if(!wait){
				this.addbattleStatusTable(this.battleUser.name , this.battleUser.key , this.battleUser.mode)
				firebase.database().ref('users/' + this.battleUser.key + '/status').on('child_changed', battleUserData.onUpdateUserStatus);
				this.updatePlay()
			}

		}

		if(wait){
			firebase.database().ref('usersState/').on('child_changed', battleUserData.onChangeUserState);
		}
	}


	updatePreStart(){
		var updates = {}
		updates['/usersState/' + myID + '/state'] = myData.prevState = "preStart";
		firebase.database().ref().update(updates)
		this.searchPreStartPlayer()
	}

	updatePlay(battleUserKey){
		var updates = {}
		updates['/usersState/' + myID + '/state'] = myData.prevState = "play";
		updates['/usersState/' + battleUserKey + '/state'] = "play";

		firebase.database().ref().update(updates)
	}

	createArea(){
		document.getElementById('virtual_keyboard').style.display = 'none';
		document.getElementById('app').style.overflowY = 'scroll';
		document.getElementById('hands').style.display = 'none';
		document.getElementById("ad_frame").style.display = 'none';

		document.getElementById('example_container').insertAdjacentHTML('afterend',
`<div id="RTCGamePlayScene" style="width:98.5%;margin: 8px;">
<div style="height:292px;padding:0;" id="RTCGamePlayWrapper">
</div></div>`)


	}



	addbattleStatusTable(userName,key,inputType){
		const INPUT_TYPE = inputType != 'kana' ? 'ローマ字' : 'かな'

		const TABLE = `<table style="width: 96%;position: relative;top:${document.getElementById("RTCGamePlayWrapper").children.length ? '45%':'20%'};left: 0;right: 0;margin: auto;" rules="all" border="1"><tbody><tr id="${key}" class="${key == myID ? 'mine' : ''}" style="font-weight:bold;height: 3rem;">
<td rowspan="2" style="width:11%;text-align: center;">${userName}</td>
<td class="RTCLine" colspan="7" style="max-width: 350px;white-space: nowrap;overflow:hidden;width: 68%;color:#ffd0a6;font-size: 26px;font-weight: normal;"></td>
<td class="InputMode" style="font-size: 0.9rem;text-align: center;">${INPUT_TYPE}</td>
<td class="clear-line" style="font-size: 1rem;text-align: center;"><span class="clear-count">0</span>/15</td></tr></tbody></table>`

		document.getElementById("RTCGamePlayWrapper").insertAdjacentHTML('beforeend',TABLE)

	}
}
let battleArea



class BattleUserData{

	constructor(){


	}


	onUpdateUserStatus(snapshot){
		const uid = snapshot.ref_.path.pieces_[1];
		const Update_Info = snapshot.ref_.path.pieces_[3]
		const SnapShotValue = snapshot.val()

		switch(Update_Info){
			case "clearCount":
				document.getElementById(uid).getElementsByClassName('clear-count')[0].textContent = SnapShotValue
				break;

			case "lineInput":
				if(SnapShotValue){
					document.getElementById(uid).getElementsByClassName('RTCLine')[0].textContent = (SnapShotValue).substr( -60, 60 );
				}else{
					document.getElementById(uid).getElementsByClassName('RTCLine')[0].textContent = "";
				}
				break;
		}
	}

	onChangeUserState(snapshot){
		const uid = snapshot.ref_.path.pieces_[1];
		const Update_Info = snapshot.ref_.path.pieces_[3]
		const SnapShotValue = snapshot.val()

		if(uid != myID && myData.prevState == "preStart" && SnapShotValue.state == 'preStart'){
			battleArea.addbattleStatusTable(SnapShotValue.name , uid)
			firebase.database().ref('users/' + uid + '/status').on('child_changed', battleUserData.onUpdateUserStatus);
			battleArea.updatePlay(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

	}

	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
		}

	}

}

const setUp = new SetUp()


function setUpMenu(FUNC_VIEW){

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

	document.getElementById("start_btn").visibility = setUp.battleSwitch == false ? '' : 'hidden'
	FUNC_VIEW.style.height = document.getElementById("func_view").clientHeight + 30 + "px"
	FUNC_VIEW.insertAdjacentHTML('beforeend' ,
`<div><div>
<label><small>対戦機能</small>
<input id="battle-option" type="checkbox" style="display:none;" ${setUp.battleSwitch == false ? "" : "checked"}>
<div id="sound-effect-btn" style="margin-left:4px;" class="switch_btn"><a class="on_btn btn show">ON</a>
<a class="off_btn btn" style="display:${setUp.battleSwitch == false ? "block" : ""};">OFF</a></div>
</label>
<input type="text" id="battle-name" placeholder="Name" value="${NAME ? NAME : 'Guest'}" maxlength="10" style="display:${setUp.battleSwitch == false ? "none" : "inline"}; position: absolute;width: 7rem;margin: 2px;right: 85px;">
</div></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("#sound-effect-btn .off_btn").style.display = ""
			document.getElementById("battle-name").style.display = "inline"
			setUp.battleSwitch = true;
		}else{
			document.querySelector("#sound-effect-btn .off_btn").style.display = "block"
			document.getElementById("battle-name").style.display = "none"
			setUp.battleSwitch = false;
		}

	})
}