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; border-color: forestgreen;
} }
&:active, &:focus { &:active, &:focus, &.enabled {
background: forestgreen; background: forestgreen;
color: black; color: black;
border-color: forestgreen; border-color: forestgreen;

View File

@ -286,3 +286,9 @@ li {
background-repeat: no-repeat; background-repeat: no-repeat;
background-position: center; 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 setGame = value => ({ type: 'SET_GAME', value });
export const setInfo = value => ({ type: 'SET_INFO', value }); export const setInfo = value => ({ type: 'SET_INFO', value });
export const setEmail = value => ({ type: 'SET_EMAIL', 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 setInstance = value => ({ type: 'SET_INSTANCE', value });
export const setInstances = value => ({ type: 'SET_INSTANCES', value }); export const setInstances = value => ({ type: 'SET_INSTANCES', value });
export const setItemEquip = value => ({ type: 'SET_ITEM_EQUIP', value }); export const setItemEquip = value => ({ type: 'SET_ITEM_EQUIP', value });

View File

@ -1,11 +1,14 @@
const preact = require('preact'); const preact = require('preact');
const { connect } = require('preact-redux'); const { connect } = require('preact-redux');
const { errorToast, infoToast } = require('../utils');
const addState = connect( const addState = connect(
function receiveState(state) { function receiveState(state) {
const { const {
ws, ws,
instances, instances,
invite,
} = state; } = state;
function sendInstanceState(id) { function sendInstanceState(id) {
@ -20,12 +23,18 @@ const addState = connect(
ws.sendInstanceQueue(); ws.sendInstanceQueue();
} }
function sendInstanceInvite() {
ws.sendInstanceInvite();
}
return { return {
instances, instances,
invite,
sendInstanceState, sendInstanceState,
sendInstanceQueue, sendInstanceQueue,
sendInstancePractice, sendInstancePractice,
sendInstanceInvite,
}; };
} }
); );
@ -33,10 +42,12 @@ const addState = connect(
function JoinButtons(args) { function JoinButtons(args) {
const { const {
instances, instances,
invite,
sendInstanceState, sendInstanceState,
sendInstanceQueue, sendInstanceQueue,
sendInstancePractice, sendInstancePractice,
sendInstanceInvite,
} = args; } = args;
if (instances.length) { 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 ( return (
<aside class='play-ctrl'> <aside class='play-ctrl'>
<div class="timer-container"></div> <div class="timer-container"></div>
@ -65,6 +118,7 @@ function JoinButtons(args) {
type="submit"> type="submit">
PVP PVP
</button> </button>
{inviteBtn()}
<button <button
class='practice ready' class='practice ready'
onClick={() => sendInstancePractice()} onClick={() => sendInstancePractice()}

View File

@ -1,3 +1,5 @@
const querystring = require('query-string');
const eachSeries = require('async/eachSeries'); const eachSeries = require('async/eachSeries');
const sample = require('lodash/sample'); const sample = require('lodash/sample');
@ -168,9 +170,20 @@ function registerEvents(store) {
return store.dispatch(actions.setInstances(v)); 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) { function setInstance(v) {
const { account, instance, ws } = store.getState(); const { account, instance, ws } = store.getState();
if (v) { if (v) {
setInvite(null);
const player = v.players.find(p => p.id === account.id); const player = v.players.find(p => p.id === account.id);
store.dispatch(actions.setPlayer(player)); store.dispatch(actions.setPlayer(player));
@ -272,6 +285,16 @@ function registerEvents(store) {
} */ } */
// setup / localstorage // 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 { return {
clearCombiner, clearCombiner,
clearConstructRename, clearConstructRename,
@ -289,12 +312,15 @@ function registerEvents(store) {
setEmail, setEmail,
setInstance, setInstance,
setItemInfo, setItemInfo,
setInvite,
setPing, setPing,
setShop, setShop,
setTeam, setTeam,
setSubscription, setSubscription,
setWs, setWs,
urlHashChange,
notify, notify,
}; };
} }

View File

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

View File

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

View File

@ -12,6 +12,7 @@ use account;
use account::Account; use account::Account;
use game; use game;
use instance; use instance;
use names;
use pg::{Db, PgPool}; use pg::{Db, PgPool};
use rpc::RpcMessage; use rpc::RpcMessage;
@ -54,6 +55,9 @@ pub enum Event {
// client events // client events
Queue(Id), Queue(Id),
Invite(Id),
Join(Id, String),
Joined(Id),
} }
struct WsClient { struct WsClient {
@ -62,6 +66,7 @@ struct WsClient {
tx: Sender<RpcMessage>, tx: Sender<RpcMessage>,
subs: HashSet<Uuid>, subs: HashSet<Uuid>,
pvp: bool, pvp: bool,
invite: Option<String>,
} }
impl Events { impl Events {
@ -115,7 +120,7 @@ impl Events {
None => None, 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); self.clients.insert(id, client);
info!("clients={:?}", self.clients.len()); info!("clients={:?}", self.clients.len());
@ -223,6 +228,59 @@ impl Events {
info!("joined game queue id={:?} account={:?}", requester.id, requester.account); info!("joined game queue id={:?} account={:?}", requester.id, requester.account);
return Ok(()); 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(()), QueueJoined(()),
QueueCancelled(()), QueueCancelled(()),
InviteRequested(()),
Invite(String),
Joining(()),
Error(String), Error(String),
} }
@ -91,6 +95,8 @@ pub enum RpcRequest {
SubscriptionState {}, SubscriptionState {},
EmailState {}, EmailState {},
InstanceInvite {},
InstanceJoin { code: String },
InstanceQueue {}, InstanceQueue {},
InstancePractice {}, InstancePractice {},
InstanceAbandon { instance_id: Uuid }, InstanceAbandon { instance_id: Uuid },
@ -136,99 +142,108 @@ impl Connection {
None => return Err(err_msg("auth required")), 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 request = v.clone();
let response = match v { let response = match v {
RpcRequest::AccountState {} => // evented but authorization required
Ok(RpcMessage::AccountState(account.clone())), RpcRequest::InstanceQueue {} => {
RpcRequest::AccountConstructs {} => self.events.send(Event::Queue(self.id))?;
Ok(RpcMessage::AccountConstructs(account::constructs(&mut tx, &account)?)), Ok(RpcMessage::QueueRequested(()))
RpcRequest::AccountInstances {} => },
Ok(RpcMessage::AccountInstances(account::account_instances(&mut tx, account)?)), RpcRequest::InstanceInvite {} => {
RpcRequest::AccountSetTeam { ids } => self.events.send(Event::Invite(self.id))?;
Ok(RpcMessage::AccountTeam(account::set_team(&mut tx, &account, ids)?)), 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()?;
RpcRequest::EmailState {} => let res = match v {
Ok(RpcMessage::EmailState(mail::select_account(&db, account.id)?)), RpcRequest::AccountState {} =>
Ok(RpcMessage::AccountState(account.clone())),
RpcRequest::AccountConstructs {} =>
Ok(RpcMessage::AccountConstructs(account::constructs(&mut tx, &account)?)),
RpcRequest::AccountInstances {} =>
Ok(RpcMessage::AccountInstances(account::account_instances(&mut tx, account)?)),
RpcRequest::AccountSetTeam { ids } =>
Ok(RpcMessage::AccountTeam(account::set_team(&mut tx, &account, ids)?)),
RpcRequest::SubscriptionState {} => RpcRequest::EmailState {} =>
Ok(RpcMessage::SubscriptionState(payments::account_subscription(&db, &self.stripe, &account)?)), Ok(RpcMessage::EmailState(mail::select_account(&db, account.id)?)),
// RpcRequest::AccountShop {} => RpcRequest::SubscriptionState {} =>
// Ok(RpcMessage::AccountShop(mtx::account_shop(&mut tx, &account)?)), Ok(RpcMessage::SubscriptionState(payments::account_subscription(&db, &self.stripe, &account)?)),
// RpcRequest::ConstructDelete" => handle_construct_delete(data, &mut tx, account), // RpcRequest::AccountShop {} =>
// Ok(RpcMessage::AccountShop(mtx::account_shop(&mut tx, &account)?)),
RpcRequest::GameState { id } => // RpcRequest::ConstructDelete" => handle_construct_delete(data, &mut tx, account),
Ok(RpcMessage::GameState(game_state(&mut tx, account, id)?)),
RpcRequest::GameSkill { game_id, construct_id, target_construct_id, skill } => RpcRequest::GameState { id } =>
Ok(RpcMessage::GameState(game_skill(&mut tx, account, game_id, construct_id, target_construct_id, skill)?)), Ok(RpcMessage::GameState(game_state(&mut tx, account, id)?)),
RpcRequest::GameSkillClear { game_id } => RpcRequest::GameSkill { game_id, construct_id, target_construct_id, skill } =>
Ok(RpcMessage::GameState(game_skill_clear(&mut tx, account, game_id)?)), Ok(RpcMessage::GameState(game_skill(&mut tx, account, game_id, construct_id, target_construct_id, skill)?)),
RpcRequest::GameReady { id } => RpcRequest::GameSkillClear { game_id } =>
Ok(RpcMessage::GameState(game_ready(&mut tx, account, id)?)), Ok(RpcMessage::GameState(game_skill_clear(&mut tx, account, game_id)?)),
RpcRequest::InstancePractice {} => RpcRequest::GameReady { id } =>
Ok(RpcMessage::InstanceState(instance_practice(&mut tx, account)?)), Ok(RpcMessage::GameState(game_ready(&mut tx, account, id)?)),
// these two can return GameState or InstanceState RpcRequest::InstancePractice {} =>
RpcRequest::InstanceReady { instance_id } => Ok(RpcMessage::InstanceState(instance_practice(&mut tx, account)?)),
Ok(instance_ready(&mut tx, account, instance_id)?),
RpcRequest::InstanceState { instance_id } =>
Ok(instance_state(&mut tx, instance_id)?),
RpcRequest::InstanceAbandon { instance_id } =>
Ok(instance_abandon(&mut tx, account, instance_id)?),
RpcRequest::VboxAccept { instance_id, group, index } => // these two can return GameState or InstanceState
Ok(RpcMessage::InstanceState(vbox_accept(&mut tx, account, instance_id, group, index)?)), RpcRequest::InstanceReady { instance_id } =>
Ok(instance_ready(&mut tx, account, instance_id)?),
RpcRequest::InstanceState { instance_id } =>
Ok(instance_state(&mut tx, instance_id)?),
RpcRequest::InstanceAbandon { instance_id } =>
Ok(instance_abandon(&mut tx, account, instance_id)?),
RpcRequest::VboxApply { instance_id, construct_id, index } => RpcRequest::VboxAccept { instance_id, group, index } =>
Ok(RpcMessage::InstanceState(vbox_apply(&mut tx, account, instance_id, construct_id, index)?)), Ok(RpcMessage::InstanceState(vbox_accept(&mut tx, account, instance_id, group, index)?)),
RpcRequest::VboxCombine { instance_id, indices } => RpcRequest::VboxApply { instance_id, construct_id, index } =>
Ok(RpcMessage::InstanceState(vbox_combine(&mut tx, account, instance_id, indices)?)), Ok(RpcMessage::InstanceState(vbox_apply(&mut tx, account, instance_id, construct_id, index)?)),
RpcRequest::VboxDiscard { instance_id } => RpcRequest::VboxCombine { instance_id, indices } =>
Ok(RpcMessage::InstanceState(vbox_discard(&mut tx, account, instance_id)?)), Ok(RpcMessage::InstanceState(vbox_combine(&mut tx, account, instance_id, indices)?)),
RpcRequest::VboxReclaim { instance_id, index } => RpcRequest::VboxDiscard { instance_id } =>
Ok(RpcMessage::InstanceState(vbox_reclaim(&mut tx, account, instance_id, index)?)), Ok(RpcMessage::InstanceState(vbox_discard(&mut tx, account, instance_id)?)),
RpcRequest::VboxUnequip { instance_id, construct_id, target } => RpcRequest::VboxReclaim { instance_id, index } =>
Ok(RpcMessage::InstanceState(vbox_unequip(&mut tx, account, instance_id, construct_id, target)?)), Ok(RpcMessage::InstanceState(vbox_reclaim(&mut tx, account, instance_id, index)?)),
RpcRequest::MtxConstructSpawn {} => RpcRequest::VboxUnequip { instance_id, construct_id, target } =>
Ok(RpcMessage::ConstructSpawn(mtx::new_construct(&mut tx, account)?)), Ok(RpcMessage::InstanceState(vbox_unequip(&mut tx, account, instance_id, construct_id, target)?)),
RpcRequest::MtxConstructApply { mtx, construct_id, name } => RpcRequest::MtxConstructSpawn {} =>
Ok(RpcMessage::AccountTeam(mtx::apply(&mut tx, account, mtx, construct_id, name)?)), Ok(RpcMessage::ConstructSpawn(mtx::new_construct(&mut tx, account)?)),
RpcRequest::MtxBuy { mtx } => RpcRequest::MtxConstructApply { mtx, construct_id, name } =>
Ok(RpcMessage::AccountShop(mtx::buy(&mut tx, account, mtx)?)), Ok(RpcMessage::AccountTeam(mtx::apply(&mut tx, account, mtx, construct_id, name)?)),
RpcRequest::SubscriptionEnding { ending } => RpcRequest::MtxBuy { mtx } =>
Ok(RpcMessage::SubscriptionState(payments::subscription_ending(&mut tx, &self.stripe, account, ending)?)), Ok(RpcMessage::AccountShop(mtx::buy(&mut tx, account, mtx)?)),
_ => Err(format_err!("unknown request request={:?}", request)), RpcRequest::SubscriptionEnding { ending } =>
Ok(RpcMessage::SubscriptionState(payments::subscription_ending(&mut tx, &self.stripe, account, ending)?)),
_ => Err(format_err!("unknown request request={:?}", request)),
};
tx.commit()?;
res
}
}; };
tx.commit()?;
info!("request={:?} account={:?} duration={:?}", request, account.name, begin.elapsed()); info!("request={:?} account={:?} duration={:?}", request, account.name, begin.elapsed());
return response; return response;

View File

@ -90,6 +90,10 @@ impl Warden {
fn on_match(&mut self, pair: Pair) -> Result<(), Error> { fn on_match(&mut self, pair: Pair) -> Result<(), Error> {
info!("received pair={:?}", pair); 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 db = self.pool.get()?;
let mut tx = db.transaction()?; let mut tx = db.transaction()?;