/* System Variable */ var time,timer; var MouseOn = false; var now = null; // to store whose turn is this. value: "player" or "enemy" var socketReady = false; var lock = true; var debugMode = false; var dialogDisplay = false; var playerChooseStatus = false; var curName = ""; var eneName = ""; var drawID = ""; var poisonDamage = ""; var tradeChoose = ""; var handCards = []; /* DOM Objects */ var cardContainer = $('.cards.container')[0]; var dialog = $('#modal'); var grid = $('#grid')[0]; var column = $('.column'); var log = $('#log')[0]; var gameStatus = $('#status'); var turn = $('#turn')[0]; var roomNum = $('.statistic > .value')[0]; var enemy = $('#enemy'); var myself = $('#myself'); /* text */ // cards var cards = {'1': '攻擊', '2': '防禦', '3': '治癒', '4': '補給', '5': '強奪', '6': '奇襲', '7': '交易', '8': '洞悉', '9': '妙策', '10': '掃射', '11': '加護', '12': '劇毒', '13': '詛咒', '14': '反制', '15': '狂亂', '16': '逆轉'}; var cardsDescription = {"1":"對敵方造成兩點傷害","2":"回復一點生命
被動:抵擋攻擊類卡片","3":"回復兩點生命","4":"抽取兩張手牌","5":"從敵方手牌中選擇一張加入自己的手牌","6":"對敵方造成一點傷害,並使其隨機損失一張手牌","7":"選取一張手牌與敵方交換","8":"抽取三張手牌
被動:抵擋攻擊類卡片,並抽取一張手牌、抵擋強奪的效果","9":"從牌庫中隨機挑出三張卡片,選擇一張加入手牌","10":"對敵方造成零~五點傷害","11":"回復三點生命,並解除中毒","12":"使敵方中毒:每個回合,玩家會損失一點生命","13":"使其損失四點生命,並隨機損失一張手牌","14":"使敵方生命減半
被動:抵擋攻擊類卡片,並反彈其傷害和效果","15":"回復三點生命,並對敵方造成三點傷害","16":"使自己與敵方的生命交換"} var characters = {'1': '安', '2': '圭月', '3': '梅', '4': '小兔', '5': '銀', '6': '正作', '7': 'W', '8': '桑德', '9': '海爾', '10': '雪村'}; // templates var cardTemplate = `
{{ name }}
{{ id }}
{{ description }}
`; var listCardTemplate = `
{{ name }}
{{ description }}
`; var logTemplate = `
{{ content }}
`; var logDivider = `
{{ player }} Turn {{ turn }}
`; // messages var messages = { "attack": "{} 攻擊 {}", "damaged": "{} 受到{}點傷害", "defended": "{} 防禦成功", "defend": "{} 沒什麼可以防禦的,回復一點生命", "heal": "{} 回復兩點生命", "supply": "{} 增加兩張手牌", "rob": "{} 正在對 {} 行搶", "cantRob": "{}沒有搶到任何東西", "robbed": "{} 搶到了 {}", "surprise": "{} 發動奇襲", "surprised": "{} 受到{}點傷害,而且掉了一張手牌", "surNoCard": "{} 受到{}點傷害", "trade": "{} 想與 {} 進行交易", "tradeChoose": "{} 選擇了 {}", "tradeNoCard": "{} 沒有卡片可以交易", "awared": "{} 洞悉了 {} 的{},並抽取了一張手牌", "aware": "{} 增加三張手牌", "plan": "{} 有個妙策", "sweep": "{} 對 {} 進行掃射,威力是 {}", "bless": "{} 獲得加護,身上的毒素一掃而空,並回復三點生命,還抽取了兩張手牌", "poison": "{} 在食物下毒,{}中毒了", "curse": "{} 詛咒了 {},使其損失四點生命,並掉了一張手牌", "curseNoCard": "{} 詛咒了 {},使其損失四點生命", "countered": "{} 反制了 {} 的攻擊,反彈了{}點傷害", "counteredSur": "{} 反制了 {} 的攻擊,反彈了{}點傷害,並使其掉了一張手牌", "counter": "{} 反制了敵手,使 {} 生命值減半了!", "chaos": "{} 進入狂亂模式,回復三點生命,並對 {} 造成三點傷害", "reverse": "{} 一口氣逆轉了情勢", "noCard": "{} 抽到了死神", "poisonDamaged": "{} 受到了劇毒的侵蝕,損失{}點生命", "surrender": "{}投降", "firstAttack": "{}先攻", "win": "{}獲勝", "draw": "{}抽到了{}", "drawEne": "{}抽了一張卡片", "use": "{}使用了{}", "eneDisconn": "因敵方斷線,所以{}獲勝" }; /* text end */ // handler function wsHandler(dataJson) { if (debugMode) { // debugging data rawLog(dataJson); } // define the current player and update player status if (dataJson.now === "player") { now = "player"; gameUpdate(dataJson); $('#skip').removeClass('disabled'); } else if (dataJson.now === "enemy") { now = "enemy"; gameUpdate(dataJson); $('#skip').addClass('disabled'); } /* special cases */ if (dataJson.toString().match(/^[0-9]{1,5}$/)) { // contains only number => room id id = dataJson.toString(); gameUpdate({ "room": id }); return null; } if (dataJson.room) { // game start curName = dataJson['cur']; eneName = dataJson['ene']; setPlayerName(curName); setEnemyName(eneName); $('#enemy .icon').css('opacity','1'); // show enemy avatar return null; } if (dataJson.action) { // time for users to do more action! // Received: {"msg": "rob", "data": ["W", "\u96ea\u6751"], "action": "toRob", "value": {"enemy_card": ["5", "5", "5", "5", "5", "5", "5", "5", "5", "5", "5", "5", "5", "5", "5"]}} statusInitialize(); lock = false; switch (dataJson.action) { case "toDefend": askGuard(dataJson.value); break; case "toRob": chooseRob(dataJson.value['enemy_card']); break; case "toBeRobbed": askDefendRob(); break; case "toTrade": if (dataJson.value) { chooseTrade(dataJson.value['hand']) } else { chooseTrade(handCards,tradeChoose); } break; case "toAdd": choosePlan(dataJson.value['cards']); break; default: break; } } // normal situation if (dataJson.msg) { switch (dataJson.msg) { case "draw": drawID = dataJson.data[1]; // card ID break; case "use": useID = dataJson.data[1]; // card ID dataJson.data[1] = cards[useID]; // edit data Log(dataJson); break; case "robbed": LogPlayerChoose("robbed",dataJson.data); break; case "tradeChoose": tradeChoose = dataJson.data[1] // set system variable, cardID LogPlayerChoose("tradeChoose",dataJson.data); break; case "poisonDamaged": poisonDamage = dataJson.data[1]; // posion level break; case "win": if (dataJson.data[0] === "player") { playerWin(); dataJson.data[0] = '你'; Log(dataJson); } else if (dataJson.data[0] === "enemy") { playerLose(); } break; case "eneDisconn": playerWin(); Log(dataJson); break; default: Log(dataJson); break; } } } function wsOnClose() { console.log("Disconnected"); ts('.snackbar').snackbar({ content: '已中斷連線', }); statusInitialize(); gameStatus.html('連線已中斷'); gameStatus.addClass('warning'); $('#skip').addClass('disabled'); $('#giveup').addClass('disabled'); unsetCardListener(); socketReady = false; } function wsOnOpen(status) { console.log("Connected! Status:" + status); ts('.snackbar').snackbar({ content: '連線成功!' }); socketReady = true; } function wsOnError(except) { ts('.snackbar').snackbar({ content: '連線失敗!', action: '重試', actionEmphasis: 'negative', onAction: () => { init(); } }); console.log(except); } // https://stackoverflow.com/questions/25779831/how-to-catch-websocket-connection-to-ws-xxxnn-failed-connection-closed-be // listener function setCardListener() { $('.cards.container a.ts.card').each((i,e) => { $(e).click(function() { useCard(this.dataset.id); }); }); } function unsetCardListener() { $('.cards.container a.ts.card').each((i,e) => { $(e).off("click"); }); } function setModalCardListListener() { $('#modal .ts.list .item').each((i,e) => { $(e).click(function() { useCard(this.dataset.id); modalClose(); }); }); } function setModalSkipButtonListener() { $('#modal #modalSkipBtn').click(function() { send(0); modalClose(); }); } $('#skip').click(function() { if (!$(this).hasClass('disabled')) { send(0); } }); $('#giveup').click(function() { dialog.children(".header").html('你放棄人生了,SAD'); dialog.children(".content").html('
'); dialog.children(".actions").html(''); timerInitialize(); // also remember to stop the timer modalOpen(); resultListener(); quit(); }); function resultListener() { $('#returnIndex').click(function() { location.href = './index.html'; }); $('#restart').click(function() { location.reload(); }); $('#close').click(function() { modalClose(); }); } // log log.addEventListener("mouseover", function() { MouseOn = true; }); // if mouse is over the div, don't scroll to bottom log.addEventListener("mouseleave", function() { MouseOn = false; }); function Log(msgJson) { var node = logTemplate; node = (now === "player") ? node.replace("{{ isSelf }}","right") : node.replace("{{ isSelf }}",""); // to distinguish if this is self log or enemy log node = node.replace("{{ content }}",messages[msgJson['msg']].format(msgJson['data'])); log.insertAdjacentHTML("beforeend",node); // insert to log if (!MouseOn) { // if mouse isn't over the div, scroll to bottom log.scrollTop = log.scrollHeight; // scroll to bottom } } function LogPlayerDraw() { var node = logTemplate.replace("{{ isSelf }}","right"); node = node.replace("{{ content }}",messages['draw'].format([curName,cards[drawID]])); // msgJson['data'][0] player name, msgJson['data'][1] card id log.insertAdjacentHTML("beforeend",node); // insert to log if (!MouseOn) { // if mouse isn't over the div, scroll to bottom log.scrollTop = log.scrollHeight; // scroll to bottom } drawID = ""; } function LogPlayerChoose(type,data) { var node = logTemplate; var name = data[0]; var chooseID = data[1]; if (playerChooseStatus || type === "robbed") { node = (now === "player") ? node.replace("{{ isSelf }}","right") : node.replace("{{ isSelf }}",""); // to distinguish if this is self log or enemy log } else { node = (now === "player") ? node.replace("{{ isSelf }}","") : node.replace("{{ isSelf }}","right"); // if this is the second one playerChoose message, then this is another one's choice } node = node.replace("{{ content }}",messages[type].format([name,cards[chooseID]])); // msgJson['data'][0] player name, msgJson['data'][1] card id log.insertAdjacentHTML("beforeend",node); // insert to log if (!MouseOn) { // if mouse isn't over the div, scroll to bottom log.scrollTop = log.scrollHeight; // scroll to bottom } playerChooseStatus = (playerChooseStatus && type !== "robbed") ? false : true; // to count this is first playerChoose message or second one in the turn } function LogPlayerPoisonDamaged() { var node = logTemplate; var tmpName = ""; if(now === "player") { // to distinguish if this is self log or enemy log node = node.replace("{{ isSelf }}","right"); tmpName = curName; } else if (now === "enemy") { node = node.replace("{{ isSelf }}",""); tmpName = eneName; } node = node.replace("{{ content }}",messages['poisonDamaged'].format([tmpName,poisonDamage])); // msgJson['data'][0] player name, msgJson['data'][1] card id log.insertAdjacentHTML("beforeend",node); // insert to log if (!MouseOn) { // if mouse isn't over the div, scroll to bottom log.scrollTop = log.scrollHeight; // scroll to bottom } poisonDamage = ""; } function rawLog(msg) { console.log(JSON.stringify(msg)); if (!MouseOn) { // if mouse isn't over the div, scroll to bottom log.scrollTop = log.scrollHeight; // scroll to bottom } } function logTurn(playerName,playerTurn) { var node = logDivider; node = node.replace("{{ player }}",playerName); node = node.replace("{{ turn }}",playerTurn); log.insertAdjacentHTML("beforeend",node); // insert to log if (!MouseOn) { // if mouse isn't over the div, scroll to bottom log.scrollTop = log.scrollHeight; // scroll to bottom } } // game status updater function gameUpdate(data) { if (data.room) { roomNum.innerHTML = data.room.padStart(5,'0'); } else if (data.now) { if (data.now === "player") { turn.innerHTML = curName+" Turn "+data.player.turn; logTurn(curName,data.player.turn); if (drawID !== "") { LogPlayerDraw(data); } statusInitialize(); lock = false; // unlock the cards gameStatus.addClass('primary'); gameStatus.html('輪到你出牌'); timerSetup(28); } else if (data.now === "enemy") { turn.innerHTML = eneName+" Turn "+data.enemy.turn; logTurn(eneName,data.enemy.turn); statusInitialize(); gameStatus.html('等待對手出牌'); } if (poisonDamage !== "") { LogPlayerPoisonDamaged(); } playerUpdate(data['player']); enemyUpdate(data['enemy']); modalClose(); } } function statusInitialize() { gameStatus.removeClass('info negative warning pulsing primary inverted'); // initailize the status timerInitialize(); } // player status updater function setPlayerName(name) { var selfNameHeader = $('#myself > .profile > .name > .header'); selfNameHeader.html(name); } function setEnemyName(name) { var eneNameHeader = $('#enemy > .profile > .name > .header'); eneNameHeader.html(name); } function playerUpdate(data) { handCards = data['hand']; setCard(data['hand']); var selfHand = $('#selfHand'); // 手牌數 var selfDeck = $('#selfDeck'); var selfLifeBar = $('#myself > .profile > .life.progress > .bar'); var selfLifeText = selfLifeBar.children(); var selfStatus = $('#myself > .profile > .status'); var barWidth = (parseInt(data['life'])/20)*100 // % selfHand.html(data['hand'].length); selfDeck.html(data['deck_left']); selfLifeBar.css('width',barWidth+'%'); selfLifeBar.attr('data-life',data['life']); selfLifeText.html(data['life']); if (parseInt(data['poison']) > 0){ selfStatus.html('

中毒 lv.'+data['poison']+'

'); selfLifeBar.addClass('poison'); } else if (parseInt(data['poison']) == 0) { if (selfLifeBar.hasClass('poison')) { selfLifeBar.removeClass('poison'); } selfStatus.html('清新'); } if (parseInt(data['life']) <= 0) { // dead if (selfLifeBar.hasClass('poison')) { selfLifeBar.removeClass('poison'); } selfLifeBar.addClass('negative'); } } function enemyUpdate(data) { var eneHand = $('#eneHand'); // 手牌數 var eneDeck = $('#eneDeck'); var eneLifeBar = $('#enemy > .profile > .life.progress > .bar'); var eneLifeText = eneLifeBar.children(); var eneStatus = $('#enemy > .profile > .status'); var barWidth = (parseInt(data['life'])/20)*100 // % eneHand.html(data['hand']); eneDeck.html(data['deck_left']); eneLifeBar.css('width',barWidth+'%'); eneLifeBar.attr('data-life',data['life']); eneLifeText.html(data['life']); if (parseInt(data['poison']) > 0){ eneStatus.html('

中毒 lv.'+data['poison']+'

'); eneLifeBar.addClass('poison'); } else if (parseInt(data['poison']) == 0) { if (eneLifeBar.hasClass('poison')) { eneLifeBar.removeClass('poison'); } eneStatus.html('清新'); } if (parseInt(data['life']) <= 0) { // dead if (eneLifeBar.hasClass('poison')) { eneLifeBar.removeClass('poison'); } eneLifeBar.addClass('negative'); } } // card function useCard(id) { if (!lock) { statusInitialize(); send(id); lock = true; } } function setCard(cardsArray) { $(cardContainer).empty(); var node = ""; cardsArray.forEach((id) => { var tmp = cardTemplate; tmp = tmp.replace(/{{ id }}/g,id).replace("{{ name }}",cards[id]).replace("{{ description }}",cardsDescription[id]); node += tmp; }); $(cardContainer).append(node); setCardListener(); resize(); // special case } // modals function chooseRob(data) { var list = `
`; data.forEach((id) => { var tmp = listCardTemplate; tmp = tmp.replace('disabled','').replace('{{ id }}',id).replace("{{ name }}",cards[id]).replace("{{ description }}",cardsDescription[id]); list+=tmp; }); list+='
'; dialog.children(".header").html('請問要搶哪張?'); dialog.children(".content").html(list); dialog.children(".actions").html(''); setModalCardListListener(); modalOpen(); } function chooseTrade(data,tradeID=null) { if (data.length == 0) { send(0); } if (tradeID) { var text = "

對手選擇了"+cards[tradeID]+"

"; } else { var text = "

選擇一張卡與對手交換

"; } var list = `
`; data.forEach((id) => { var tmp = listCardTemplate; tmp = tmp.replace('disabled','').replace('{{ id }}',id).replace("{{ name }}",cards[id]).replace("{{ description }}",cardsDescription[id]); list+=tmp; }); list+='
'; dialog.children(".header").html('交易') dialog.children(".content").html(text+list); dialog.children(".actions").html(''); setModalCardListListener(); modalOpen(); tradeChoose = ""; // reset value } function choosePlan(data) { var text = `

請從三張卡中選擇一張加入手牌

`; var list = `
`; data.forEach((id) => { var tmp = listCardTemplate; tmp = tmp.replace('disabled','').replace('{{ id }}',id).replace("{{ name }}",cards[id]).replace("{{ description }}",cardsDescription[id]); // not need to be disabled list+=tmp; }); list+='
'; dialog.children(".header").html('妙策'); dialog.children(".content").html(text+list); dialog.children(".actions").html(''); setModalCardListListener(); modalOpen(); } function askGuard(data) { var attackType = data['type']; var attackName = {"attack": "攻擊","surprise": "奇襲","sweep": "掃射"}; var damage = data['damage']; var text = `對手使用了 `+attackName[attackType]+`,傷害為 `+damage; var list = `
`; handCards.forEach((id) => { var tmp = listCardTemplate; tmp = tmp.replace('{{ id }}',id).replace("{{ name }}",cards[id]).replace("{{ description }}",cardsDescription[id]); list+=tmp; }); list+='
'; dialog.children(".header").html('防禦'); dialog.children(".content").html(text+list); dialog.children(".actions").html(''); setModalCardListListener(); setModalSkipButtonListener(); $('#modal .ts.list .item').each((i,e) => { if (e.dataset.id == "2" || e.dataset.id == "8" || e.dataset.id == "14") { $(e).removeClass('disabled'); } }); modalOpen(); } function askDefendRob() { var list = `
`; handCards.forEach((id) => { var tmp = listCardTemplate; tmp = tmp.replace('{{ id }}',id).replace("{{ name }}",cards[id]).replace("{{ description }}",cardsDescription[id]); list+=tmp; }); list+='
'; dialog.children(".header").html('防禦強奪'); dialog.children(".content").html(list); dialog.children(".actions").html(''); setModalCardListListener(); setModalSkipButtonListener(); $('#modal .ts.list .item').each((i,e) => { if (e.dataset.id == "8") { $(e).removeClass('disabled'); } }); modalOpen(); } function playerWin() { dialog.children(".header").html('你贏了!'); dialog.children(".content").html('

'+eneName+'

'+curName+'

'); dialog.children(".actions").html(''); timerInitialize(); // also remember to stop the timer modalOpen(); resultListener(); } function playerLose() { dialog.children(".header").html('你輸爆了,SAD'); dialog.children(".content").html('

'+eneName+'

'+curName+'

'); dialog.children(".actions").html(''); timerInitialize(); // also remember to stop the timer modalOpen(); resultListener(); } function modalClose() { if (dialogDisplay) { ts('#modal').modal('hide'); dialogDisplay = false; } } function modalOpen() { if (!dialogDisplay) { ts('#modal').modal('show'); dialogDisplay = true; } } // countdown timer for each turn function timing() { if (time == 0) { clearInterval(timer); // unset the timer gameStatus.removeClass('warning pulsing'); gameStatus.addClass('negative'); gameStatus.text('時間到!'); return null; } if (time <= 5) { if(!gameStatus.hasClass('pulsing')) { gameStatus.addClass('pulsing'); // pulsing animation } gameStatus.removeClass('info'); gameStatus.addClass('warning'); gameStatus.text('輪到你出牌 '+time); } else if (time <= 10) { if(!gameStatus.hasClass('pulsing')) { gameStatus.addClass('pulsing'); // pulsing animation } gameStatus.text('輪到你出牌 '+time); } else { gameStatus.removeClass('negative pulsing'); gameStatus.addClass('info'); gameStatus.text('輪到你出牌'); } time--; } function timerSetup(t) { time = t; timer = setInterval(timing,1000); } function timerInitialize() { clearInterval(timer); gameStatus.addClass('inverted'); gameStatus.html('等待中'); } // resize function resize() { var h = cardContainer.offsetHeight; grid.style.height='calc(100vh - ' + h + 'px)'; column.css('height','calc(100vh - ' + h + 'px)'); } resize(); window.addEventListener("resize",resize); // game initailize $(document).ready(() => { var roomID = localStorage.getItem('room'); var characterID = localStorage.getItem('character'); if(!roomID || !characterID){ roomID = "n"; characterID = Math.floor(Math.random()*10 + 1).toString(); // randomly choose a player } curName = characters[characterID]; setPlayerName(characters[characterID]); init(); var retry = setInterval(() => { if(socketReady) { // send the charactor id and room id until the connected send(characterID); send(roomID); clearInterval(retry); } }, 300); // try every 0.3s }); /* https://stackoverflow.com/questions/11700927/horizontal-scrolling-with-mouse-wheel-in-a-div */ function scrollHorizontally(e) { e = window.event || e; var delta = Math.max(-1, Math.min(1, (e.wheelDelta || -e.detail))); cardContainer.scrollLeft -= (delta*40); // Multiplied by 40 e.preventDefault(); } cardContainer.addEventListener("mousewheel", scrollHorizontally, false); cardContainer.addEventListener("DOMMouseScroll", scrollHorizontally, false); //Firefox