UNPKG

board-game

Version:

an online board game engine

901 lines (870 loc) 35.5 kB
import 'babel-polyfill'; import $ from 'jquery'; import Vue from 'vue'; import io from 'socket.io-client'; import utilTools from './utils'; import qrcode from 'qrcode'; const keyNames = [ '', '', '', '', '', '', '', '', '', '', //000-009 '', '', '', 'enter', '', '', 'shift', 'ctrl', 'alt', '', //010-019 '', '', '', '', '', '', '', '', '', '', //020-029 '', '', 'space', '', '', '', '', 'left', 'up', 'right', //030-039 'down', '', '', '', '', '', '', '', '', '', //040-049 '', '', '', '', '', '', '', '', '', '', //050-059 '', '', '', '', '', 'a', 'b', 'c', 'd', 'e', //060-069 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', //070-079 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', //080-089 'z', '', '', '', '', '', '', '', '', '', //090-099 '', '', '', '', '', '', '', '', '', '', //100-109 '', '', '', '', '', '', '', '', '', '', //110-119 '', '', '', '', '', '', '', '', '', '', //120-129 '', '', '', '', '', '', '', '', '', '', //130-139 '', '', '', '', '', '', '', '', '', '', //140-149 '', '', '', '', '', '', '', '', '', '', //150-159 '', '', '', '', '', '', '', '', '', '', //160-169 '', '', '', '', '', '', '', '', '', '', //170-179 '', '', '', '', '', '', '', '', '', '', //180-189 '', '', '', '', '', '', '', '', '', '', //190-199 ]; const keyStates = keyNames.map(v => false); const getEventName = (type, name) => `${ type }.${ name }`; const clone = obj => { return JSON.parse(JSON.stringify(obj)); }; const clear = value => { if (typeof value === 'object' && value !== null) { for (let [k, v] of Object.entries(value)) { if (!k.startsWith('~')) { value[k] = clear(v); } } for (let [k, v] of Object.entries(value)) { if (k.startsWith('~') && v === true) { delete value[k]; } } } return value; }; const setProp = (obj, prop, value) => { if (Array.isArray(obj)) { obj.splice(prop, 1, clear(value)); } else { Vue.set(obj, prop, clear(value)); } }; const set = (obj, prop, value) => { if (typeof obj === 'undefined') return; if (prop.startsWith('~')) { if (value === true) { Vue.delete(obj, prop.substring(1)); } } else { let old = obj[prop]; if (typeof old === 'undefined') { setProp(obj, prop, value); } else { if (old !== value) { if (typeof value === 'object' && value !== null) { if (typeof old !== 'object') { setProp(obj, prop, value); } else { if (Array.isArray(old) && Array.isArray(value)) { old.splice(value.length); } for (let name in value) { if (value.hasOwnProperty(name)) { set(old, name, value[name]); } } } } else { setProp(obj, prop, value); } } } } }; const append = (array, items) => { items.forEach(item => { if (item === '~') { array.splice(array.length - 1, 1); } else if (item === '*') { array.splice(0); } else { array.push(item); } }); }; const getTouch = (event, target, index) => { let filter = touch => $(target).closest(touch.target).length > 0 || $(touch.target).closest(target).length > 0; if (event.targetTouches) { let touches = [...event.targetTouches].filter(filter); if (typeof index === 'function') { let touch = touches.find(index); if (touch) return touch; } else if (touches.length > index) { return touches[index]; } } if (event.changedTouches) { let touches = [...event.changedTouches].filter(filter); if (typeof index === 'function') { let touch = touches.find(index); if (touch) return touch; } else if (touches.length > index) { return touches[index]; } } return event; }; const audioContext = new (window.AudioContext || window.webkitAudioContext || window.mozAudioContext || window.oAudioContext || window.msAudioContext)(); // const Base64Binary = { // _keyStr : "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=", // // /* will return a Uint8Array type */ // decodeArrayBuffer: function(input) { // let bytes = (input.length/4) * 3; // let ab = new ArrayBuffer(bytes); // this.decode(input, ab); // // return ab; // }, // // removePaddingChars: function(input){ // let lkey = this._keyStr.indexOf(input.charAt(input.length - 1)); // if(lkey === 64){ // return input.substring(0,input.length - 1); // } // return input; // }, // // decode: function (input, arrayBuffer) { // //get last chars to see if are valid // input = this.removePaddingChars(input); // input = this.removePaddingChars(input); // // let bytes = parseInt((input.length / 4) * 3, 10); // // let uarray; // let chr1, chr2, chr3; // let enc1, enc2, enc3, enc4; // let i = 0; // let j = 0; // // if (arrayBuffer) // uarray = new Uint8Array(arrayBuffer); // else // uarray = new Uint8Array(bytes); // // input = input.replace(/[^A-Za-z0-9\+\/\=]/g, ""); // // for (i=0; i<bytes; i+=3) { // //get the 3 octects in 4 ascii chars // enc1 = this._keyStr.indexOf(input.charAt(j++)); // enc2 = this._keyStr.indexOf(input.charAt(j++)); // enc3 = this._keyStr.indexOf(input.charAt(j++)); // enc4 = this._keyStr.indexOf(input.charAt(j++)); // // chr1 = (enc1 << 2) | (enc2 >> 4); // chr2 = ((enc2 & 15) << 4) | (enc3 >> 2); // chr3 = ((enc3 & 3) << 6) | enc4; // // uarray[i] = chr1; // if (enc3 !== 64) uarray[i+1] = chr2; // if (enc4 !== 64) uarray[i+2] = chr3; // } // // return uarray; // } // } const audioMap = new Map(); class AudioPlayer { constructor(mp3, ogg) { if (audioMap.has(mp3)) { this.audioBuffer = audioMap.get(mp3); } else { let load = (url, onerror) => { let request = new XMLHttpRequest(); request.open("GET", url, true); request.responseType = "arraybuffer"; request.onload = () => { return audioContext.decodeAudioData(request.response, buffer => { audioMap.set(mp3, buffer); this.audioBuffer = buffer; }, e => { onerror && onerror(e) }); }; request.onerror = e => { onerror && onerror(e) }; request.send(); }; load(mp3, e => ogg ? load(ogg, e => console.log(e)) : console.log(e)); } this.complete = []; this.paused = true; this.startTime = 0; this.volume = 1; this.timer = false; } get duration() { return this.audioBuffer ? this.audioBuffer.duration : 0; } get currentTime() { return this.startTime === 0 ? 0 : ((Date.now() - this.startTime) / 1000); } pause() { if (this.audioSource && !this.paused) { this.audioSource.stop(); this.paused = true; if (this.timer) { clearTimeout(this.timer); this.timer = false; } } } play(second) { if (!this.audioBuffer) return; if (typeof second === 'undefined' && !this.paused) return; second = second || 0; this.pause(); let audioSource = audioContext.createBufferSource(); audioSource.buffer = this.audioBuffer; if (this.volume === 1) { audioSource.connect(audioContext.destination); } else { let gainNode = audioContext.createGain(); gainNode.gain.value = this.volume; audioSource.connect(gainNode); gainNode.connect(audioContext.destination); } this.audioSource = audioSource; this.audioSource.start(0, second); this.paused = false; this.startTime = Date.now() - Math.floor(second * 1000); if (this.duration !== 0) { this.timer = setTimeout(() => { this.complete.forEach(complete => complete()); this.paused = true; }, Math.ceil((this.duration - second) * 1000)); } } onComplete(callback) { this.complete.push(callback); } offComplete(callback) { this.complete = callback ? this.complete.filter(c => c !== callback) : []; } } const utils = { getAudio(mp3, ogg) { return new AudioPlayer(mp3, ogg); }, get touchable() { return navigator.maxTouchPoints > 0 || /(iPhone|iPad|iPod|iOS)/i.test(navigator.userAgent); }, get vw() { return parseInt($(window).width()); }, get vh() { return parseInt($(window).height()); }, get vmin() { return Math.min(this.vw, this.vh); }, get vmax() { return Math.max(this.vw, this.vh); }, isPortrait() { return this.vw < this.vh; }, isLandscape() { return this.vw >= this.vh; }, point(event, target, index = 0) { if (!isNaN(target)) { index = target; target = $(event.target); } else { target = $(target || event.target); } let touch = getTouch(event, target, index); let result = { clientX: touch.clientX, clientY: touch.clientY, pageX: touch.pageX, pageY: touch.pageY, identifier: touch.identifier || 0 }; let offset = target.offset(); result.x = result.pageX - offset.left; result.y = result.pageY - offset.top; return result; }, units: { rad: 'rad', turn: 'turn', deg: 'deg', grad: 'grad', px: 'px', pt: 'pt', in: 'in', cm: 'cm', mm: 'mm', vw: 'vw', vh: 'vh', vmin: 'vmin', vmax: 'vmax', s: 's', ms: 'ms' }, asPx(value, unit = 'px') { unit = (/(px|pt|in|cm|mm|vw|vh|vmin|vmax)$/i.exec(value) || [ unit ])[0]; value = parseFloat(value); switch (unit.toLowerCase()) { case 'px': return value; case 'pt': return value * 4 / 3; case 'in': return value * 96; case 'cm': return value * 48 / 1.27; case 'mm': return value * 48 / 12.7; case 'vw': return value * this.vw / 100; case 'vh': return value * this.vh / 100; case 'vmin': return value * this.vmin / 100; case 'vmax': return value * this.vmax / 100; } }, asPt(value, unit = 'px') { return this.asPx(value, unit) * 3 / 4; }, asIn(value, unit = 'px') { return this.asPx(value, unit) / 96; }, asCm(value, unit = 'px') { return this.asPx(value, unit) * 1.27 / 48; }, asMm(value, unit = 'px') { return this.asPx(value, unit) * 12.7 / 48; }, asVm(value, unit = 'px') { return this.asPx(value, unit) * 100 / this.vm; }, asVh(value, unit = 'px') { return this.asPx(value, unit) * 100 / this.vh; }, asVmin(value, unit = 'px') { return this.asPx(value, unit) * 100 / this.vmin; }, asVmax(value, unit = 'px') { return this.asPx(value, unit) * 100 / this.vmax; }, asSecond(value, unit = 's') { unit = (/(s|ms)$/i.exec(value) || [ unit ])[0]; value = parseFloat(value); switch (unit.toLowerCase()) { case 's' : return value; case 'ms' : return value / 1000; } }, asMillisecond(value, unit = 's') { return this.asSecond(value, unit) * 1000; }, asRad(value, unit = 'rad') { unit = (/(deg|turn|grad|rad)$/i.exec(value) || [ unit ])[0]; value = parseFloat(value); switch (unit.toLowerCase()) { case 'rad': return value; case 'turn': return value * (2 * Math.PI); case 'deg': return value / 180 * Math.PI; case 'grad': return value / 200 * Math.PI; } return value; }, asDeg(value, unit = 'rad') { return this.asRad(value, unit) / Math.PI * 180; }, asTurn(value, unit = 'rad') { return this.asRad(value, unit) / (Math.PI * 2); }, asGrad(value, unit = 'rad') { return this.asRad(value, unit) / Math.PI * 200; }, ...utilTools }; const mixin = adapter && adapter.mixin || { }; const getKeyEventNames = type => { let result = { }; for (let name of keyNames) { if (name) result[name] = getEventName(type, name); } return new Proxy(result, { get(target, property, receiver) { return Reflect.get(target, property, receiver) || `${ type }.${ property }`; } }); }; const getControlEventNames = type => { let result = { }; for (let name of [ 'a', 'b', 'x', 'y' ]) { if (name) result[name] = getEventName(type, name); } return new Proxy(result, { get(target, property, receiver) { return Reflect.get(target, property, receiver) || `${ type }.${ property }`; } }); }; const events = { key: { start: getKeyEventNames('key.start'), end: getKeyEventNames('key.end'), press: getKeyEventNames('key.press'), }, control: { start: getControlEventNames('control.start'), end: getControlEventNames('control.end'), press: getControlEventNames('control.press') } }; export { mixin, utils, events, Vue }; export default (application, listeners, callback) => { const inBrowser = typeof window !== 'undefined'; const UA = inBrowser && window.navigator.userAgent.toLowerCase(); const isMobile = (UA && /iphone|ipad|ipod|ios|android/.test(UA));//是否为手机环境 if (config.dev) { if (isMobile) { require(['@whinc/web-console'], WebConsole => { new WebConsole(config.webconsole || {}); }); } else { Vue.config.devtools = config.devtools; } } try { document.domain = (/(\d{1,2}|[01]\d{2}|2[0-4]\d{2}|25[0-5])(\.(\d{1,2}|[01]\d{2}|2[0-4]\d{2}|25[0-5])){3}/ig.test(location.hostname) ? location.hostname : (location.hostname.match(/[^.]+\.[^.]+$/ig) || [location.hostname])[0]); } catch (e) { } document.title = config.name; $(() => { let fullScreenTimer; function listenCallBack(){ if(isMobile && utils.isLandscape()) { $('#scrollTipMask').css({ display: 'block' }); $('html, body').css({ overflow: 'auto', 'touch-action': 'auto' }); if( window.innerHeight === document.documentElement.clientHeight ){ //全屏并且加载完毕,则不显示div $('#scrollTipMask').css({ display: 'none' }); $('html, body').css({ overflow: 'hidden', 'touch-action': 'none' }); } else { //未全屏显示则把div显示出来 $('html, body').css({ overflow: 'auto', 'touch-action': 'auto' }); clearTimeout(fullScreenTimer); fullScreenTimer = setTimeout(() => $(document).scrollTop(0), 40); } } } $(window).on('resize', listenCallBack).on('scroll', listenCallBack); listenCallBack(); $(document).on('gesturestart', e => { if (e.cancelable && !e.isDefaultPrevented()) e.preventDefault(); }).on('dblclick', e => { if (e.cancelable && !e.isDefaultPrevented()) e.preventDefault(); }).on('touchmove', e => { if (!$(e.target).parents('.modal,.scrollable,.web-console,#scrollTipMask').length && e.cancelable && !e.isDefaultPrevented()) e.preventDefault(); }).on('touchstart', e => { if (!$(e.target).parents('.modal,.scrollable,.web-console,#scrollTipMask').length && !$(e.target).is('input,select,textarea') && e.cancelable && !e.isDefaultPrevented()) e.preventDefault(); }).on('touchend', e => { if (!$(e.target).parents('.modal,.scrollable,.web-console,#scrollTipMask').length && !$(e.target).is('input,select,textarea') && e.cancelable && !e.isDefaultPrevented()) e.preventDefault(); }).on('hold', e => { if (e.cancelable && !e.isDefaultPrevented()) e.preventDefault(); }).on('contextmenu', e => { if (e.cancelable && !e.isDefaultPrevented()) e.preventDefault(); }).on('selectstart', e => { if (e.cancelable && !e.isDefaultPrevented()) e.preventDefault(); }); const storage = new Proxy({ }, { get(target, property, receiver) { if (property in target || typeof property === 'symbol') { return Reflect.get(target, property, receiver); } else { let value = localStorage.getItem(property); return value && JSON.parse(value); } }, set(target, property, value, receiver) { if (property in target || typeof property === 'symbol') { Reflect.set(target, property, value, receiver); } else { localStorage.setItem(property, JSON.stringify(value)); } return true; } }); const lazy = { queue: [], timer: false, exec(executor) { if (typeof executor !== 'undefined') { this.queue.push(executor); } if (this.queue.length) { clearTimeout(this.timer); this.timer = setTimeout(() => this.execute(), 0); } }, execute() { while (this.queue.length) { this.queue.shift().call(); } } }; const socket = new Proxy(io(location.protocol + '//' + location.hostname + ':' + location.port, { reconnection: false, transports: ['websocket', 'polling'] }), { get(target, property, receiver) { if (property === 'on') { let on = Reflect.get(target, property, receiver); return arg => { if (typeof arg === 'object' && arg !== null) { for (let key in arg) { if (arg.hasOwnProperty(key)) { on.call(target, key, data => lazy.exec(arg[key].bind(target, data))); } } } else { on.apply(target, arguments); } } } else { return Reflect.get(target, property, receiver); } } }); const queue = { queue: [], timer: false, exec(executor) { if (typeof executor !== 'undefined') { this.queue.push(executor); } this.doExecute(); }, doExecute() { clearTimeout(this.timer); if (this.queue.length) { this.timer = setTimeout(() => { this.queue.shift().call(); this.doExecute(); }, 0); } } }; const emit = new Proxy({ }, { get(target, property, receiver) { if (property in target) { return Reflect.get(target, property, receiver); } else { return data => queue.exec(() => socket.emit(property.replace(/^[_$]/i, ''), data)); } } }); const game = { data: { loading: true, player: { }, rooms: { loading: false, page: 0, list: [], status: { waiting: '等待中', gaming: '游戏中', sorting: '准备中' } }, dialog: { alert: false, settings: false, join: false, player: false, login: false }, tip: { more: false }, modal: { introduction: false, qr: false }, config: clone(config) }, computed: { qr: function() { let url = window.location.origin + window.location.pathname + (this.player && this.player.room ? '#' + this.player.room.id : ''); let qr = qrcode(0, 'L'); qr.addData(url); qr.make(); return qr.createDataURL(10, 15); // return 'http://pan.baidu.com/share/qrcode?w=150&h=150&url=' + encodeURIComponent(window.location.origin + window.location.pathname + '#' + this.player.room.id) }, dialogs: function() { return Object.values(this.dialog).some(dialog => !!dialog); } }, mounted() { if (this.config.login) { this.loading = false; this.dialog.login = { id: storage.player && storage.player.id || '', password: '' } } else { this.loading = true; if (storage.player) { this.player = storage.player; setTimeout(() => { emit.login(this.player); }, 0); } else { $.get(['id']).then(data => { this.player.id = data; emit.login(this.player); }).catch(() => { setTimeout(() => window.location.reload(), 500); }); } } this.getRooms(); }, methods: { showControl() { this.$control().show(); }, hideControl() { this.$control().hide(); }, random(callback) { let name = this.dialog.player.name; $.get(['random']).then(data => { if (name === this.dialog.player.name) { this.dialog.player.name = data; callback && typeof callback === 'function' && callback(); } }); }, doLogin() { if (!this.dialog.login.id) this.alert('请输入用户名'); else if (!this.dialog.login.password) this.alert('请输入密码'); else emit.login(this.player = this.dialog.login); }, help() { this.modal.introduction = true; }, mine() { this.dialog.player = { id: this.player.id, name: this.player.name } }, share() { this.modal.qr = true; }, settings() { this.dialog.settings = this.room.settings; }, more() { this.tip.more = !this.tip.more; }, home() { window.location.href = this.config.home; }, getRooms: function() { if (!this.rooms.loading) { this.rooms.loading = true; emit.rooms(); } }, alert(message) { this.dialog.alert = message; }, prepare() { this.loading = true; emit.settings(); }, create() { this.loading = true; emit.create(this.dialog.settings); this.dialog.settings = false; }, preJoin(roomId) { this.dialog.join = { roomId: roomId && !isNaN(roomId) ? roomId : '' }; }, join() { if (this.dialog.join && this.dialog.join.roomId && !isNaN(this.dialog.join.roomId)) { this.loading = true; emit.join(this.dialog.join); this.dialog.join = false; } }, joinTo(roomId) { this.preJoin(roomId); this.join(); }, exit() { this.loading = true; emit.exit(); }, start() { if (this.player.room.canStart && this.player.room.creator.id === this.player.id) { this.loading = true; emit.start(); } }, move(role, team, index) { let data = { team, index }; if (role === true) { this.loading = true; if (this.creator) { emit.disable(data); } else { emit.move(data); } } else if (role === false) { if (this.creator) { this.loading = true; emit.enable(data); } } else if (role.player.id !== this.player.id) { if (this.creator) { if(confirm('是否踢出玩家 ' + role.player.name)) { this.loading = true; emit.kick(data); } } } }, confirm() { this.loading = true; emit.confirm(); }, savePlayer() { this.loading = true; if (!this.dialog.player.name) { this.random(() => this.savePlayer()); } else { emit.info(this.dialog.player); this.dialog.player = false; } }, onControl(control) { this.$emit('control', control); }, $game: function() { return this.$refs.game; }, $control: function() { return this.$refs.control; } } }; if (!application) application = () => ({ }); if ($.isFunction(application)) application = application({ socket, emit, storage, set, append }) || { }; if (!adapter || !adapter.create) throw '无法初始化'; const app = adapter.create(application, game); app.socket = socket; app.emit = emit; app.storage = storage; socket.on({ player(player) { set(app, 'player', player); let roomId = parseInt((window.location.hash || '').replace(/^.*?#/ig, '').replace(/[^\d].+/ig, '')); if (roomId && !isNaN(roomId)) { if (app.room) { window.location.href = window.location.origin + window.location.pathname; } else { app.joinTo(roomId.toString()); } } if (!app.room) app.getRooms(); app.loading = false; storage.player = { id: app.player.id, name: app.player.name }; app.dialog.login = false; if (!app.player.name) { app.dialog.player = { id: app.player.id, name: app.player.name }; } }, rooms(rooms) { app.rooms.list = rooms.page === 0 ? rooms.list : app.rooms.list.concat(rooms.list); app.rooms.page = rooms.page; app.rooms.loading = false; if (app.rooms.list.length === 0) { setTimeout(() => app.getRooms(), 1000); } }, role(role) { set(app.player, 'role', role); app.loading = false; }, room(room) { set(app.player, 'room', room); if (app.game) emit.ready(); app.loading = false; }, game(game) { app.room && set(app.room, 'game', game); app.loading = false; }, group(summary) { app.game && app.game.groups && set(app.game.groups, summary.index.toString(), summary); app.loading = false; }, board(board) { app.game && set(app.game, 'board', board); }, message(message) { if (app.game) { append(app.game.messages, message); } else if (app.room) { append(app.room.messages, message); } }, settings(settings) { app.dialog.settings = settings; app.loading = false; }, err(err) { err ? app ? app.alert(err) : alert(err) : window.location.reload(); app && (app.loading = false); }, disconnect() { app.loading = true; let retryTimes = 0; let retry = function() { retryTimes++; $.get(['id']).then(function() { window.location.reload(); }).catch(function() { setTimeout(retry, Math.min(retryTimes, 10) * 500); }); }; retry(); } }); if (!listeners) listeners = () => ({ }); if ($.isFunction(listeners)) listeners = listeners({ socket, emit, app, storage, set, append }) || { }; socket.on(listeners); emit.connection(); window.alert = function(message) { if (app.view) { app.view.alert = message; } else if (app.dialog) { app.dialog.alert = message; } }; $(document).on('keypress', event => app.$emit('key.press', { code: event.keyCode, name: keyNames[event.keyCode] || event.key || '', shift: event.shiftKey, alt: event.altKey, ctrl: event.ctrlKey })); $(document).on('keydown', event => { app.$emit('key.start.repeat', { code: event.keyCode, name: keyNames[event.keyCode] || event.key || '', shift: event.shiftKey, alt: event.altKey, ctrl: event.ctrlKey }); if (!keyStates[event.keyCode]) { keyStates[event.keyCode] = true; app.$emit('key.start', { code: event.keyCode, name: keyNames[event.keyCode] || event.key || '', shift: event.shiftKey, alt: event.altKey, ctrl: event.ctrlKey }); } }); $(document).on('keyup', event => { keyStates[event.keyCode] = false; app.$emit('key.end', { code: event.keyCode, name: keyNames[event.keyCode] || event.key || '', shift: event.shiftKey, alt: event.altKey, ctrl: event.ctrlKey }); }); $(window).on('resize', event => app.$emit('resize')); callback && callback(); }); };