// ==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