game invites

This commit is contained in:
ntr 2019-09-15 15:42:47 +10:00
parent a22521b119
commit b85dade351
10 changed files with 250 additions and 73 deletions

View File

@ -53,7 +53,7 @@ aside {
border-color: forestgreen;
}
&:active, &:focus {
&:active, &:focus, &.enabled {
background: forestgreen;
color: black;
border-color: forestgreen;

View File

@ -286,3 +286,9 @@ li {
background-repeat: no-repeat;
background-position: center;
}
#clipboard {
width: 1px;
height: 1px;
padding: 0px;
}

View File

@ -19,6 +19,7 @@ export const setConstructRename = value => ({ type: 'SET_CONSTRUCT_RENAME', valu
export const setGame = value => ({ type: 'SET_GAME', value });
export const setInfo = value => ({ type: 'SET_INFO', value });
export const setEmail = value => ({ type: 'SET_EMAIL', value });
export const setInvite = value => ({ type: 'SET_INVITE', value });
export const setInstance = value => ({ type: 'SET_INSTANCE', value });
export const setInstances = value => ({ type: 'SET_INSTANCES', value });
export const setItemEquip = value => ({ type: 'SET_ITEM_EQUIP', value });

View File

@ -1,11 +1,14 @@
const preact = require('preact');
const { connect } = require('preact-redux');
const { errorToast, infoToast } = require('../utils');
const addState = connect(
function receiveState(state) {
const {
ws,
instances,
invite,
} = state;
function sendInstanceState(id) {
@ -20,12 +23,18 @@ const addState = connect(
ws.sendInstanceQueue();
}
function sendInstanceInvite() {
ws.sendInstanceInvite();
}
return {
instances,
invite,
sendInstanceState,
sendInstanceQueue,
sendInstancePractice,
sendInstanceInvite,
};
}
);
@ -33,10 +42,12 @@ const addState = connect(
function JoinButtons(args) {
const {
instances,
invite,
sendInstanceState,
sendInstanceQueue,
sendInstancePractice,
sendInstanceInvite,
} = args;
if (instances.length) {
@ -55,6 +66,48 @@ function JoinButtons(args) {
);
}
const inviteBtn = () => {
if (!invite) {
return (
<button
class='pvp ready'
onClick={() => sendInstanceInvite()}
type="submit">
Invite
</button>
);
}
function copyClick(e) {
const link = `${document.location.origin}#join=${invite}`;
const textArea = document.createElement('textarea', { id: '#clipboard' });
textArea.value = link;
document.body.appendChild(textArea);
textArea.focus();
textArea.select();
try {
document.execCommand('copy');
infoToast('Invite link copied.');
} catch (err) {
console.error('link copy error', err);
errorToast('Invite link copy error.');
}
document.body.removeChild(textArea);
return true;
}
return (
<button
class='pvp ready enabled'
onClick={copyClick}
type="submit">
Copy Link
</button>
);
};
return (
<aside class='play-ctrl'>
<div class="timer-container"></div>
@ -65,6 +118,7 @@ function JoinButtons(args) {
type="submit">
PVP
</button>
{inviteBtn()}
<button
class='practice ready'
onClick={() => sendInstancePractice()}

View File

@ -1,3 +1,5 @@
const querystring = require('query-string');
const eachSeries = require('async/eachSeries');
const sample = require('lodash/sample');
@ -168,9 +170,20 @@ function registerEvents(store) {
return store.dispatch(actions.setInstances(v));
}
function setInvite(code) {
if (!code) return store.dispatch(actions.setInvite(null));
navigator.clipboard.writeText(code).then(() => {
notify(`your invite code ${code} was copied to the clipboard.`);
}, () => {});
return store.dispatch(actions.setInvite(code));
}
function setInstance(v) {
const { account, instance, ws } = store.getState();
if (v) {
setInvite(null);
const player = v.players.find(p => p.id === account.id);
store.dispatch(actions.setPlayer(player));
@ -272,6 +285,16 @@ function registerEvents(store) {
} */
// setup / localstorage
function urlHashChange() {
const { ws } = store.getState();
const cmds = querystring.parse(location.hash);
if (cmds.join) ws.sendInstanceJoin(cmds.join);
return true;
}
window.addEventListener('hashchange', urlHashChange, false);
return {
clearCombiner,
clearConstructRename,
@ -289,12 +312,15 @@ function registerEvents(store) {
setEmail,
setInstance,
setItemInfo,
setInvite,
setPing,
setShop,
setTeam,
setSubscription,
setWs,
urlHashChange,
notify,
};
}

View File

@ -31,6 +31,7 @@ module.exports = {
constructRename: createReducer(null, 'SET_CONSTRUCT_RENAME'),
game: createReducer(null, 'SET_GAME'),
email: createReducer(null, 'SET_EMAIL'),
invite: createReducer(null, 'SET_INVITE'),
info: createReducer(null, 'SET_INFO'),
instance: createReducer(null, 'SET_INSTANCE'),
instances: createReducer([], 'SET_INSTANCES'),

View File

@ -1,5 +1,3 @@
const querystring = require('query-string');
const toast = require('izitoast');
const cbor = require('borc');
@ -128,6 +126,14 @@ function createSocket(events) {
send(['InstanceQueue', {}]);
}
function sendInstanceInvite() {
send(['InstanceInvite', {}]);
}
function sendInstanceJoin(code) {
send(['InstanceJoin', { code }]);
}
function sendInstanceReady(instanceId) {
send(['InstanceReady', { instance_id: instanceId }]);
}
@ -241,6 +247,9 @@ function createSocket(events) {
QueueRequested: () => events.notify('pvp queue request received'),
QueueJoined: () => events.notify('you have joined the pvp queue'),
InviteRequested: () => events.notify('pvp queue request received'),
Invite: code => events.setInvite(code),
Joining: () => events.notify('searching for instance...'),
Error: errHandler,
};
@ -286,6 +295,8 @@ function createSocket(events) {
sendPing();
sendItemInfo();
events.urlHashChange();
return true;
}
@ -312,8 +323,6 @@ function createSocket(events) {
ws = null;
}
console.log(querystring.parse(location.hash));
ws = new WebSocket(SOCKET_URL);
ws.binaryType = 'arraybuffer';
@ -322,6 +331,7 @@ function createSocket(events) {
ws.addEventListener('message', onMessage);
ws.addEventListener('error', onError);
ws.addEventListener('close', onClose);
return ws;
}
@ -342,6 +352,8 @@ function createSocket(events) {
sendInstancePractice,
sendInstanceQueue,
sendInstanceState,
sendInstanceInvite,
sendInstanceJoin,
sendVboxAccept,
sendVboxApply,

View File

@ -12,6 +12,7 @@ use account;
use account::Account;
use game;
use instance;
use names;
use pg::{Db, PgPool};
use rpc::RpcMessage;
@ -54,6 +55,9 @@ pub enum Event {
// client events
Queue(Id),
Invite(Id),
Join(Id, String),
Joined(Id),
}
struct WsClient {
@ -62,6 +66,7 @@ struct WsClient {
tx: Sender<RpcMessage>,
subs: HashSet<Uuid>,
pvp: bool,
invite: Option<String>,
}
impl Events {
@ -115,7 +120,7 @@ impl Events {
None => None,
};
let client = WsClient { id, tx, account: account_id, subs: HashSet::new(), pvp: false };
let client = WsClient { id, tx, account: account_id, subs: HashSet::new(), pvp: false, invite: None };
self.clients.insert(id, client);
info!("clients={:?}", self.clients.len());
@ -223,6 +228,59 @@ impl Events {
info!("joined game queue id={:?} account={:?}", requester.id, requester.account);
return Ok(());
},
Event::Invite(id) => {
// check whether request is valid
let c = self.clients.get_mut(&id)
.ok_or(format_err!("connection not found id={:?}", id))?;
if let None = c.account {
return Err(err_msg("cannot join pvp queue anonymously"));
}
let code = names::name().split_whitespace().collect::<Vec<&str>>().join("-");
info!("pvp invite request id={:?} account={:?} code={:?}", c.id, c.account, code);
c.invite = Some(code.clone());
c.tx.send(RpcMessage::Invite(code))?;
return Ok(());
},
Event::Join(id, code) => {
// check whether request is valid
let c = self.clients.get(&id)
.ok_or(format_err!("connection not found id={:?}", id))?;
if let None = c.account {
return Err(err_msg("cannot join pvp queue anonymously"));
}
info!("pvp join request id={:?} account={:?} code={:?}", c.id, c.account, code);
let inv = self.clients.iter()
.filter(|(_id, c)| c.invite.is_some())
.find(|(_id, c)| match c.invite {
Some(ref c) => *c == code,
None => false,
})
.map(|(_id, c)| PvpRequest { id: c.id, account: c.account.unwrap(), tx: c.tx.clone() })
.ok_or(format_err!("invite not found code={:?}", code))?;
let join = PvpRequest { id: c.id, account: c.account.unwrap(), tx: c.tx.clone() };
self.warden.send(GameEvent::Match((join, inv)))?;
return Ok(());
},
Event::Joined(id) => {
// check whether request is valid
let c = self.clients.get_mut(&id)
.ok_or(format_err!("connection not found id={:?}", id))?;
c.pvp = false;
c.invite = None;
return Ok(());
},
}
}
}

View File

@ -62,6 +62,10 @@ pub enum RpcMessage {
QueueJoined(()),
QueueCancelled(()),
InviteRequested(()),
Invite(String),
Joining(()),
Error(String),
}
@ -91,6 +95,8 @@ pub enum RpcRequest {
SubscriptionState {},
EmailState {},
InstanceInvite {},
InstanceJoin { code: String },
InstanceQueue {},
InstancePractice {},
InstanceAbandon { instance_id: Uuid },
@ -136,21 +142,27 @@ impl Connection {
None => return Err(err_msg("auth required")),
};
// evented but authorization required
match v {
RpcRequest::InstanceQueue {} => {
self.events.send(Event::Queue(self.id))?;
return Ok(RpcMessage::QueueRequested(()));
},
_ => (),
};
// all good, let's make a tx and process
let mut tx = db.transaction()?;
let request = v.clone();
let response = match v {
// evented but authorization required
RpcRequest::InstanceQueue {} => {
self.events.send(Event::Queue(self.id))?;
Ok(RpcMessage::QueueRequested(()))
},
RpcRequest::InstanceInvite {} => {
self.events.send(Event::Invite(self.id))?;
Ok(RpcMessage::InviteRequested(()))
},
RpcRequest::InstanceJoin { code } => {
self.events.send(Event::Join(self.id, code))?;
Ok(RpcMessage::Joining(()))
},
_ => {
// all good, let's make a tx and process
let mut tx = db.transaction()?;
let res = match v {
RpcRequest::AccountState {} =>
Ok(RpcMessage::AccountState(account.clone())),
RpcRequest::AccountConstructs {} =>
@ -228,6 +240,9 @@ impl Connection {
};
tx.commit()?;
res
}
};
info!("request={:?} account={:?} duration={:?}", request, account.name, begin.elapsed());

View File

@ -90,6 +90,10 @@ impl Warden {
fn on_match(&mut self, pair: Pair) -> Result<(), Error> {
info!("received pair={:?}", pair);
// clear pvp status
self.events.send(Event::Joined(pair.0.id))?;
self.events.send(Event::Joined(pair.1.id))?;
let db = self.pool.get()?;
let mut tx = db.transaction()?;