team setting and postgres constraint
This commit is contained in:
parent
9fdda7c52c
commit
8cd0f1af36
@ -102,7 +102,7 @@ nav h2 {
|
||||
|
||||
nav hr {
|
||||
margin: 1em 0;
|
||||
border-color: whitesmoke;
|
||||
border-color: #444;
|
||||
}
|
||||
|
||||
nav button {
|
||||
@ -120,12 +120,12 @@ nav button.active {
|
||||
}
|
||||
|
||||
nav button[disabled], nav button[disabled]:hover {
|
||||
color: #333333;
|
||||
color: #333;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
nav button:hover {
|
||||
color: #888;
|
||||
color: whitesmoke;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
|
||||
@ -31,9 +31,11 @@ export const setShowLog = value => ({ type: 'SET_SHOW_LOG', value });
|
||||
export const setShowNav = value => ({ type: 'SET_SHOW_NAV', value });
|
||||
export const setSkip = value => ({ type: 'SET_SKIP', value });
|
||||
export const setShop = value => ({ type: 'SET_SHOP', value });
|
||||
export const setTeam = value => ({ type: 'SET_SELECTED_CONSTRUCTS', value: Array.from(value) });
|
||||
export const setVboxHighlight = value => ({ type: 'SET_VBOX_HIGHLIGHT', value });
|
||||
|
||||
export const setTeam = value => ({ type: 'SET_TEAM', value: Array.from(value) });
|
||||
export const setTeamSelect = value => ({ type: 'SET_TEAM_SELECT', value: Array.from(value) });
|
||||
|
||||
export const setVboxHighlight = value => ({ type: 'SET_VBOX_HIGHLIGHT', value });
|
||||
export const setVboxSelected = value => ({ type: 'SET_VBOX_SELECTED', value });
|
||||
|
||||
export const setWs = value => ({ type: 'SET_WS', value });
|
||||
|
||||
@ -5,26 +5,29 @@ const actions = require('./../actions');
|
||||
|
||||
const addState = connect(
|
||||
function receiveState(state) {
|
||||
const { team, showNav } = state;
|
||||
const {
|
||||
teamSelect,
|
||||
showNav,
|
||||
ws,
|
||||
} = state;
|
||||
|
||||
function sendAccountSetTeam() {
|
||||
return ws.sendAccountSetTeam(teamSelect);
|
||||
}
|
||||
|
||||
return {
|
||||
team,
|
||||
sendAccountSetTeam,
|
||||
teamSelect,
|
||||
showNav,
|
||||
};
|
||||
},
|
||||
function receiveDispatch(dispatch) {
|
||||
function navToList() {
|
||||
dispatch(actions.setGame(null));
|
||||
dispatch(actions.setInstance(null));
|
||||
return dispatch(actions.setNav('list'));
|
||||
}
|
||||
|
||||
function setShowNav(v) {
|
||||
return dispatch(actions.setShowNav(v));
|
||||
}
|
||||
|
||||
return {
|
||||
navToList,
|
||||
setShowNav,
|
||||
};
|
||||
}
|
||||
@ -33,20 +36,18 @@ const addState = connect(
|
||||
function TeamFooter(args) {
|
||||
const {
|
||||
showNav,
|
||||
team,
|
||||
navToList,
|
||||
teamSelect,
|
||||
sendAccountSetTeam,
|
||||
setShowNav,
|
||||
} = args;
|
||||
|
||||
if (!team) return false;
|
||||
|
||||
return (
|
||||
<footer>
|
||||
<button id="nav-btn" onClick={() => setShowNav(!showNav)} >☰</button>
|
||||
<button
|
||||
disabled={team.some(c => !c)}
|
||||
onClick={() => navToList()}>
|
||||
Confirm
|
||||
disabled={teamSelect.some(c => !c)}
|
||||
onClick={sendAccountSetTeam}>
|
||||
Set Team
|
||||
</button>
|
||||
</footer>
|
||||
);
|
||||
|
||||
@ -8,11 +8,9 @@ const { stringSort } = require('./../utils');
|
||||
const SpawnButton = require('./spawn.button');
|
||||
const { ConstructAvatar } = require('./construct');
|
||||
|
||||
const idSort = stringSort('id');
|
||||
|
||||
const addState = connect(
|
||||
function receiveState(state) {
|
||||
const { ws, constructs, team } = state;
|
||||
const { ws, constructs, teamSelect } = state;
|
||||
|
||||
function sendConstructSpawn(name) {
|
||||
return ws.sendMtxConstructSpawn(name);
|
||||
@ -20,27 +18,26 @@ const addState = connect(
|
||||
|
||||
return {
|
||||
constructs,
|
||||
team,
|
||||
teamSelect,
|
||||
sendConstructSpawn,
|
||||
};
|
||||
},
|
||||
|
||||
function receiveDispatch(dispatch) {
|
||||
function setTeam(constructIds) {
|
||||
localStorage.setItem('team', JSON.stringify(constructIds));
|
||||
dispatch(actions.setTeam(constructIds));
|
||||
dispatch(actions.setTeamSelect(constructIds));
|
||||
}
|
||||
|
||||
return {
|
||||
setTeam,
|
||||
};
|
||||
}
|
||||
|
||||
);
|
||||
|
||||
function Team(args) {
|
||||
const {
|
||||
constructs,
|
||||
team,
|
||||
teamSelect,
|
||||
setTeam,
|
||||
sendConstructSpawn,
|
||||
} = args;
|
||||
@ -51,27 +48,25 @@ function Team(args) {
|
||||
// so much for dumb components
|
||||
function selectConstruct(id) {
|
||||
// remove
|
||||
const i = team.findIndex(sid => sid === id);
|
||||
const i = teamSelect.findIndex(sid => sid === id);
|
||||
if (i > -1) {
|
||||
team[i] = null;
|
||||
return setTeam(team);
|
||||
teamSelect[i] = null;
|
||||
return setTeam(teamSelect);
|
||||
}
|
||||
|
||||
// window insert
|
||||
const insert = team.findIndex(j => j === null);
|
||||
const insert = teamSelect.findIndex(j => j === null);
|
||||
if (insert === -1) return setTeam([id, null, null]);
|
||||
team[insert] = id;
|
||||
return setTeam(team);
|
||||
teamSelect[insert] = id;
|
||||
return setTeam(teamSelect);
|
||||
}
|
||||
|
||||
const constructPanels = constructs.map(construct => {
|
||||
const colour = team.indexOf(construct.id);
|
||||
const colour = teamSelect.indexOf(construct.id);
|
||||
const selected = colour > -1;
|
||||
|
||||
const borderColour = selected ? COLOURS[colour] : '#000000';
|
||||
|
||||
|
||||
// <button disabled={true} >↻</button>
|
||||
return (
|
||||
<div
|
||||
key={construct.id}
|
||||
|
||||
@ -43,7 +43,9 @@ module.exports = {
|
||||
resolution: createReducer(null, 'SET_RESOLUTION'),
|
||||
skip: createReducer(false, 'SET_SKIP'),
|
||||
shop: createReducer(false, 'SET_SHOP'),
|
||||
team: createReducer([null, null, null], 'SET_SELECTED_CONSTRUCTS'),
|
||||
|
||||
team: createReducer([], 'SET_TEAM'),
|
||||
teamSelect: createReducer([null, null, null], 'SET_TEAM_SELECT'),
|
||||
|
||||
vboxHighlight: createReducer([], 'SET_VBOX_HIGHLIGHT'),
|
||||
vboxSelected: createReducer([], 'SET_VBOX_SELECTED'),
|
||||
|
||||
@ -28,7 +28,7 @@ function createSocket(events) {
|
||||
// Outgoing
|
||||
// -------------
|
||||
function send(msg) {
|
||||
// if (msg[0] !== 'Ping') console.log('outgoing msg', msg);
|
||||
if (msg[0] !== 'Ping') console.log('outgoing msg', msg);
|
||||
ws.send(cbor.encode(msg));
|
||||
}
|
||||
|
||||
@ -46,6 +46,10 @@ function createSocket(events) {
|
||||
send(['AccountInstances', {}]);
|
||||
}
|
||||
|
||||
function sendAccountSetTeam(ids) {
|
||||
send(['AccountSetTeam', { ids }]);
|
||||
}
|
||||
|
||||
function sendGameState(id) {
|
||||
send(['GameState', { id }]);
|
||||
}
|
||||
@ -108,6 +112,11 @@ function createSocket(events) {
|
||||
send(['InstancePractice', {}]);
|
||||
}
|
||||
|
||||
function sendInstanceQueue() {
|
||||
send(['InstancePractice', {}]);
|
||||
}
|
||||
|
||||
|
||||
function sendInstanceReady(instanceId) {
|
||||
send(['InstanceReady', { instance_id: instanceId }]);
|
||||
}
|
||||
@ -184,6 +193,7 @@ function createSocket(events) {
|
||||
InstanceState: onInstanceState,
|
||||
ItemInfo: onItemInfo,
|
||||
Pong: onPong,
|
||||
Error: errHandler,
|
||||
};
|
||||
|
||||
function logout() {
|
||||
@ -208,7 +218,6 @@ function createSocket(events) {
|
||||
// decode binary msg from server
|
||||
const blob = new Uint8Array(event.data);
|
||||
const res = cbor.decode(blob);
|
||||
if (res.err) return errHandler(res.err);
|
||||
|
||||
const [msgType, params] = res;
|
||||
if (msgType !== 'Pong') console.log(res);
|
||||
@ -268,6 +277,7 @@ function createSocket(events) {
|
||||
return {
|
||||
sendAccountConstructs,
|
||||
sendAccountInstances,
|
||||
sendAccountSetTeam,
|
||||
|
||||
sendGameState,
|
||||
sendGameReady,
|
||||
@ -276,6 +286,7 @@ function createSocket(events) {
|
||||
|
||||
sendInstanceReady,
|
||||
sendInstancePractice,
|
||||
sendInstanceQueue,
|
||||
sendInstanceState,
|
||||
|
||||
sendVboxAccept,
|
||||
|
||||
@ -1,18 +1,56 @@
|
||||
const team_size_trigger = `
|
||||
CREATE OR REPLACE FUNCTION enforce_team_size() RETURNS trigger AS $$
|
||||
DECLARE
|
||||
team_size INTEGER := 3;
|
||||
team_count INTEGER := 0;
|
||||
BEGIN
|
||||
IF (TG_OP = 'UPDATE' OR TG_OP = 'INSERT') THEN
|
||||
SELECT INTO team_count COUNT(id)
|
||||
FROM constructs
|
||||
WHERE account = NEW.account
|
||||
AND team = true;
|
||||
ELSIF (TG_OP = 'DELETE') THEN
|
||||
SELECT INTO team_count COUNT(id)
|
||||
FROM constructs
|
||||
WHERE account = OLD.account
|
||||
AND team = true;
|
||||
END IF;
|
||||
|
||||
IF team_count != team_size THEN
|
||||
RAISE EXCEPTION 'You must have exactly % constructs in your team.', team_size;
|
||||
END IF;
|
||||
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
CREATE CONSTRAINT TRIGGER check_team_size
|
||||
AFTER INSERT OR UPDATE OR DELETE
|
||||
ON constructs
|
||||
DEFERRABLE INITIALLY DEFERRED
|
||||
FOR EACH ROW
|
||||
EXECUTE PROCEDURE enforce_team_size();
|
||||
`;
|
||||
|
||||
exports.up = async knex => {
|
||||
return knex.schema.createTable('constructs', table => {
|
||||
await knex.schema.createTable('constructs', table => {
|
||||
table.uuid('id').primary();
|
||||
table.timestamps(true, true);
|
||||
table.uuid('account').notNullable()
|
||||
table.foreign('account')
|
||||
.references('id')
|
||||
.inTable('accounts')
|
||||
.onDelete('CASCADE');
|
||||
.references('id')
|
||||
.inTable('accounts')
|
||||
.onDelete('CASCADE');
|
||||
table.binary('data').notNullable();
|
||||
|
||||
table.boolean('team')
|
||||
.notNullable()
|
||||
.defaultTo(false);
|
||||
.notNullable()
|
||||
.defaultTo(false);
|
||||
});
|
||||
|
||||
await knex.raw(team_size_trigger);
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
exports.down = async () => {};
|
||||
exports.down = async () => {};
|
||||
|
||||
@ -175,12 +175,12 @@ pub fn debit(tx: &mut Transaction, id: Uuid, debit: i64) -> Result<Account, Erro
|
||||
";
|
||||
|
||||
let result = tx
|
||||
.query(query, &[&debit, &id])?;
|
||||
.query(query, &[&debit, &id])
|
||||
.or(Err(err_msg("insufficient balance")))?;
|
||||
|
||||
let row = result.iter().next()
|
||||
.ok_or(format_err!("account not found {:?}", id))?;
|
||||
|
||||
|
||||
let name: String = row.get(1);
|
||||
let db_balance: i64 = row.get(2);
|
||||
let balance = u32::try_from(db_balance)
|
||||
@ -301,8 +301,7 @@ pub fn account_team(tx: &mut Transaction, account: &Account) -> Result<Vec<Const
|
||||
SELECT data
|
||||
FROM constructs
|
||||
WHERE account = $1
|
||||
AND team = true
|
||||
LIMIT 3;
|
||||
AND team = true;
|
||||
";
|
||||
|
||||
let result = tx
|
||||
@ -325,39 +324,32 @@ pub fn account_team(tx: &mut Transaction, account: &Account) -> Result<Vec<Const
|
||||
}
|
||||
|
||||
let mut constructs = constructs.unwrap();
|
||||
|
||||
if constructs.len() != 3 {
|
||||
return Err(format_err!("team not size 3 account={:?}", account));
|
||||
}
|
||||
|
||||
constructs.sort_by_key(|c| c.id);
|
||||
|
||||
return Ok(constructs);
|
||||
}
|
||||
|
||||
pub fn account_set_team(tx: &mut Transaction, account: &Account, ids: Vec<Uuid>) -> Result<Vec<Construct>, Error> {
|
||||
// there is a trigger constraint on the table that enforces
|
||||
// exactly 3 constructs in a team
|
||||
pub fn set_team(tx: &mut Transaction, account: &Account, ids: Vec<Uuid>) -> Result<Vec<Construct>, Error> {
|
||||
let query = "
|
||||
UPDATE constructs
|
||||
SET team = false
|
||||
SET team =
|
||||
CASE
|
||||
WHEN id = ANY($2) THEN true
|
||||
ELSE false
|
||||
END
|
||||
WHERE account = $1;
|
||||
";
|
||||
|
||||
let updated = tx
|
||||
let _updated = tx
|
||||
.execute(query, &[&account.id, &ids])?;
|
||||
|
||||
if updated > 3 {
|
||||
warn!("team members >3 account={:?} count={:?}", account, updated);
|
||||
}
|
||||
|
||||
let query = "
|
||||
UPDATE constructs
|
||||
SET team = true
|
||||
WHERE account = $1
|
||||
AND id in $2
|
||||
RETURNING data;
|
||||
";
|
||||
|
||||
let updated = tx
|
||||
.execute(query, &[&account.id, &ids])?;
|
||||
|
||||
if updated != 3 {
|
||||
return Err(format_err!("could not create team of 3 account={:?} updated={:?}", account, updated));
|
||||
}
|
||||
|
||||
account_team(tx, account)
|
||||
}
|
||||
|
||||
|
||||
@ -6,6 +6,7 @@ use uuid::Uuid;
|
||||
use failure::Error;
|
||||
use failure::err_msg;
|
||||
|
||||
use account;
|
||||
use pg::{Db};
|
||||
use construct::{Construct};
|
||||
use game::{Game, game_state, game_skill, game_ready};
|
||||
@ -54,6 +55,7 @@ enum RpcRequest {
|
||||
AccountState {},
|
||||
AccountShop {},
|
||||
AccountConstructs {},
|
||||
AccountSetTeam { ids: Vec<Uuid> },
|
||||
|
||||
InstancePvp {},
|
||||
InstancePractice {},
|
||||
@ -100,11 +102,12 @@ pub fn receive(data: Vec<u8>, db: &Db, begin: Instant, account: &Option<Account>
|
||||
RpcRequest::AccountConstructs {} =>
|
||||
Ok(RpcMessage::AccountConstructs(account_constructs(&mut tx, &account)?)),
|
||||
|
||||
RpcRequest::AccountSetTeam { ids } =>
|
||||
Ok(RpcMessage::AccountConstructs(account::set_team(&mut tx, &account, ids)?)),
|
||||
|
||||
// RpcRequest::AccountShop {} =>
|
||||
// Ok(RpcMessage::AccountShop(mtx::account_shop(&mut tx, &account)?)),
|
||||
|
||||
|
||||
|
||||
// RpcRequest::ConstructDelete" => handle_construct_delete(data, &mut tx, account),
|
||||
|
||||
RpcRequest::GameState { id } =>
|
||||
|
||||
@ -83,6 +83,8 @@ impl Warden {
|
||||
let db = self.pool.get()?;
|
||||
let tx = db.transaction()?;
|
||||
|
||||
info!("received pair={:?}", pair);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user