// ==UserScript==
// @name e-対戦 [e-typing]
// @namespace http://tampermonkey.net/
// @version 0.7
// @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.interval
}
sendResetResult(){
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)
}
sendResult(){
if(battleUserData.data){
firebase.database().ref('users/' + battleUserData.data.key + '/result').on('child_changed', myResult.onBattleResultDisplay);
}
firebase.database().ref('users/' + myID + '/result').on('child_changed', myResult.onBattleResultDisplay);
const RESULT_DATA = document.getElementsByClassName("result_data")[0].firstElementChild.children
const score = RESULT_DATA[0].getElementsByClassName("data")[0].textContent
const time = RESULT_DATA[2].getElementsByClassName("data")[0].textContent
const typeCount = RESULT_DATA[3].getElementsByClassName("data")[0].textContent
const missCount = RESULT_DATA[4].getElementsByClassName("data")[0].textContent
const wpm = RESULT_DATA[5].getElementsByClassName("data")[0].textContent
const latency = RESULT_DATA[7].getElementsByClassName("data")[0].textContent
const rkpm = RESULT_DATA[8].getElementsByClassName("data")[0].textContent
let updates = {};
//ユーザーネーム更新
updates['/users/' + myID + '/result/' + '/score'] = score;
updates['/users/' + myID + '/result/' + '/time'] = time;
updates['/users/' + myID + '/result/' + '/typeCount'] = typeCount;
updates['/users/' + myID + '/result/' + '/missCount'] = missCount;
updates['/users/' + myID + '/result/' + '/wpm'] = wpm;
updates['/users/' + myID + '/result/' + '/latency'] = latency;
updates['/users/' + myID + '/result/' + '/rkpm'] = rkpm;
firebase.database().ref().update(updates)
}
onBattleResultDisplay(){
const uid = snapshot.ref_.path.pieces_[1];
const Update_Info = snapshot.ref_.path.pieces_[3]
const SnapShotValue = snapshot.val()
}
checkResultDisplay(){
const Result = document.getElementsByClassName("result_data")[0].firstElementChild.children[8].getElementsByClassName("data")[0]
//リザルトが表示された
if(Result){
myResult.sendResult()
clearInterval(myResult.interval)
}
}
}
let myResult
class MyStatus{
constructor(){
this.lineInput = ''
this.clearCount = 0
}
sendResetStatus(){
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.sendResetResult()
myStatus.sendResetStatus()
document.getElementById("start_btn").style.display = 'block'
document.getElementsByClassName("loading")[0].style.display = 'none'
}
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)
}
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){
if(setUp.battleSwitch){
battleArea = new BattleArea()
keyJudge = new KeyJudge()
keyJudge.addEvent()
setMutationObserver = new SetMutationObserver()
}
clearInterval(typingAppModInterval)
typingAppModInterval = null
}
}
class SetMutationObserver {
constructor(){
this.set()
}
set(){
var observer = new MutationObserver(function(){
console.log('???')
const RTC = document.getElementById("RTCGamePlayScene")
if(RTC){
myResult.interval = setInterval(myResult.checkResultDisplay,50)
}
});
const elem = document.getElementById("app")
const config = {
childList: true//「子ノード(テキストノードも含む)」の変化
};
observer.observe(elem, config);
}
}
let setMutationObserver
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)
this.playEvent = this.startSpaceKey.bind(this)
window.addEventListener("keydown",this.Event)
window.addEventListener("keydown",this.playEvent)
}
wait(event){
setTimeout(() => this.keyDown(event))
}
startSpaceKey(event){
if(event.code == 'Space'){
battleArea.updatePlay()
window.removeEventListener('keyDown',this.playEvent)
document.getElementById('virtual_keyboard').style.display = 'block';
document.getElementById('hands').style.display = 'block';
document.getElementById('RTCGamePlayScene').style.display = 'none';
}
}
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.substr( -24, 24 );
if(!text){
this.clearLine++
updates['/users/' + myID + '/status/' + '/clearCount'] = this.clearLine
}
firebase.database().ref().update(updates)
}
}
let keyJudge
class BattleArea {
constructor(){
battleUserData = new BattleUserData()
this.updatePreStart()
this.createArea()
}
searchPreStartPlayer(){
firebase.database().ref('usersState/').once('value').then(users => {
const USERS = users.val()
const USERS_KEY = Object.keys(USERS)
battleUserData.data = null
for(let i=0;i<USERS_KEY.length;i++){
if(USERS_KEY[i] != myID && USERS[USERS_KEY[i]].state == 'preStart'){
battleUserData.data = {
name:USERS[USERS_KEY[i]].name,
key:USERS_KEY[i],
mode:'roma'
}
this.createBattleTable()
this.updatePlay(USERS_KEY[i])
return
break;
}
}
if(!battleUserData.data){
this.createBattleTable('wait')
}
})
}
createBattleTable(wait){
if(!wait){
this.addbattleStatusTable(battleUserData.data.name , battleUserData.data.key , battleUserData.data.mode)
firebase.database().ref('users/' + battleUserData.data.key + '/status').on('child_changed', battleUserData.onUpdateUserStatus);
this.updatePlay()
}
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()
}
displayReadyButton(){
document.getElementById("l-ready-button").style.display = 'block'
document.getElementById("ready-button").style.display = 'block'
}
updateReadyState(){
var updates = {}
updates['/usersState/' + myID + '/state'] = myData.prevState = "ready";
updates['/users/' + myID + '/status/' + '/lineInput'] = '準備完了';
firebase.database().ref().update(updates)
}
updatePlay(battleUserKey){
var updates = {}
updates['/usersState/' + myID + '/state'] = myData.prevState = "play";
if(battleUserKey){
updates['/usersState/' + battleUserKey + '/state'] = "play";
}
firebase.database().ref().update(updates)
this.displayReadyButton()
}
createArea(){
document.getElementById('virtual_keyboard').style.display = 'none';
document.getElementById('hands').style.display = 'none';
document.getElementById('app').style.overflowY = 'scroll';
document.getElementById("ad_frame").style.display = 'none';
document.getElementById('start_msg').insertAdjacentHTML("afterbegin" ,
`<div style="cursor: pointer; display: none;position: absolute;bottom: -23px;left: 28px;width: 130px;font-size: 1.1rem;" class="loading" id='ready-button'>準備完了</div>
<div style="cursor: pointer;display: none;position: absolute;bottom: -23px;width: 130px;right: 26px;font-size: 0.8rem;" class="loading" id='l-ready-button'>Lスタートで準備完了</div>`)
document.getElementById("ready-button").addEventListener('click', event => {
event.target.style.display = 'none'
document.getElementById("l-ready-button").style.display = 'none'
battleArea.updateReadyState()
})
document.getElementById("l-ready-button").addEventListener('click', event => {
event.target.style.display = 'none'
document.getElementById("ready-button").style.display = 'none'
battleArea.updateReadyState()
})
document.getElementById('example_container').insertAdjacentHTML('afterend',
`<div id="RTCGamePlayScene">
<div id="RTCGamePlayWrapper">
</div></div>`)
}
addbattleStatusTable(userName,key,inputType){
const INPUT_TYPE = inputType != 'kana' ? 'ローマ字' : 'かな'
const TABLE = `<table class='user-table' style="top:${document.getElementById("RTCGamePlayWrapper").children.length ? '45%':'20%'};" rules="all" border="1"><tbody>
<tr id="${key}" class="${key == myID ? 'mine' : ''}">
<td rowspan="2" class='user-name'>${userName}</td>
<td class="RTCLine" colspan="7"></td>
<td class="InputMode">${INPUT_TYPE}</td>
<td class="clear-line"><span class="clear-count">0</span>/15</td></tr></tbody></table>`
document.getElementById("RTCGamePlayWrapper").insertAdjacentHTML('beforeend',TABLE)
}
}
let battleArea
class BattleUserData{
constructor(){
this.data
}
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;
}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(battleUserData.data && uid == battleUserData.data.key){
battleUserData.data.state = SnapShotValue.state;
}
if(uid != myID && myData.prevState == "preStart" && SnapShotValue.state == 'preStart'){
battleUserData.data = {
name:SnapShotValue.name,
key:uid,
mode:'roma',
state:SnapShotValue.state
}
battleArea.addbattleStatusTable(battleUserData.data.name , battleUserData.data.key , battleUserData.data.mode)
firebase.database().ref('users/' + uid + '/status').on('child_changed', battleUserData.onUpdateUserStatus);
battleArea.updatePlay(uid)
}
if(battleUserData.data && (battleUserData.data.key == uid || myID == uid) && battleUserData.data.state == 'ready' && myData.prevState == "ready"){
document.dispatchEvent( new KeyboardEvent("keydown",{keyCode:32}))
}
}
}
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;
addCss()
document.getElementById("start_btn").style.display = setUp.battleSwitch == false ? '' : 'none'
document.getElementById("start_btn").insertAdjacentHTML('afterend',`<div class='loading'>対戦データベースに接続中</div>`)
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;
}
})
}
function 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;
}
.user-table{
width: 96%;
position: relative;
left: 0;
right: 0;
margin: auto;
}
.user-table tr{
font-weight:bold;
height: 3rem;
}
.user-name{
width:11%;
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: 0.9rem;
text-align: center;
}
.clear-line{
font-size: 1rem;
text-align: center;
}
#RTCGamePlayScene{
width:98.5%;
margin: 8px;
}
#RTCGamePlayWrapper{
height:292px;
padding:0;
}
</style>`)
}