diff --git a/app/javascripts/components/gather.js b/app/javascripts/components/gather.js index 7f9d877..17905e2 100644 --- a/app/javascripts/components/gather.js +++ b/app/javascripts/components/gather.js @@ -15,6 +15,7 @@ const SelectPlayerButton = React.createClass({ selectPlayer(e) { e.preventDefault(); this.props.socket.emit("gather:select", { + type: this.props.gather.type, player: parseInt(e.target.value, 10) }); }, @@ -257,12 +258,16 @@ const JoinGatherButton = React.createClass({ joinGather(e) { e.preventDefault(); - this.props.socket.emit("gather:join"); + this.props.socket.emit("gather:join", { + type: this.props.gather.type + }); }, leaveGather(e) { e.preventDefault(); - this.props.socket.emit("gather:leave"); + this.props.socket.emit("gather:leave", { + type: this.props.gather.type + }); }, cooldownTime() { @@ -324,6 +329,7 @@ const GatherActions = React.createClass({ voteRegather(e) { e.preventDefault(e); this.props.socket.emit("gather:vote", { + type: this.props.gather.type, regather: (e.target.value === "true") }); }, @@ -380,11 +386,13 @@ const VoteButton = React.createClass({ propTypes: { socket: React.PropTypes.object.isRequired, candidate: React.PropTypes.object.isRequired, - thisGatherer: React.PropTypes.object + thisGatherer: React.PropTypes.object, + gather: React.PropTypes.object.isRequired }, cancelVote(e) { this.props.socket.emit("gather:vote", { + type: this.props.gather.type, leader: { candidate: null } @@ -394,6 +402,7 @@ const VoteButton = React.createClass({ vote(e) { e.preventDefault(); this.props.socket.emit("gather:vote", { + type: this.props.gather.type, leader: { candidate: parseInt(e.target.value, 10) } @@ -441,6 +450,7 @@ const ServerVoting = React.createClass({ return e => { e.preventDefault(); this.props.socket.emit("gather:vote", { + type: this.props.gather.type, server: { id: serverId } @@ -465,7 +475,7 @@ const ServerVoting = React.createClass({ }).map(server => { let votes = self.votesForServer(server); let style = thisGatherer.serverVote.some(voteId => voteId === server.id) ? - "list-group-item list-group-item-default" : "list-group-item"; + "list-group-item list-group-item-success" : "list-group-item"; return ( { e.preventDefault(); this.props.socket.emit("gather:vote", { + type: this.props.gather.type, map: { id: mapId } @@ -674,6 +685,61 @@ const LifeformIcons = exports.LifeformIcons = React.createClass({ } }); +const GatherMenu = exports.GatherMenu = React.createClass({ + propTypes: { + gatherPool: React.PropTypes.object.isRequired, + currentGather: React.PropTypes.string.isRequired, + gatherSelectedCallback: React.PropTypes.func.isRequired + }, + + onClick(gather) { + return () => { + this.props.gatherSelectedCallback(gather.type); + } + }, + + itemClass(gather) { + let className = ["list-group-item", "pointer"]; + if (gather.type === this.props.currentGather) { + className.push("list-group-item-success"); + } + return className.join(" "); + }, + + gatherPoolArray() { + const gatherArray = []; + const gatherPool = this.props.gatherPool; + for (let attr in gatherPool) { + if (gatherPool.hasOwnProperty(attr)) { + gatherArray.push(gatherPool[attr]); + } + } + return gatherArray.sort((a, b) => a.name - b.name); + }, + + render() { + return ( +
+
Gather Menu
+ +
+ ); + } +}); + const GathererListItem = React.createClass({ propTypes: { user: React.PropTypes.object.isRequired, @@ -687,6 +753,7 @@ const GathererListItem = React.createClass({ bootGatherer(e) { e.preventDefault(); this.props.socket.emit("gather:leave", { + type: this.props.gather.type, gatherer: parseInt(e.target.value, 10) || null }); }, @@ -755,6 +822,7 @@ const GathererListItem = React.createClass({ {votes + " votes"} @@ -860,7 +928,9 @@ const Gatherers = React.createClass({ joinGather(e) { e.preventDefault(); - this.props.socket.emit("gather:join"); + this.props.socket.emit("gather:join", { + type: this.props.gather.type + }); }, render() { diff --git a/app/javascripts/components/gatherArchive.js b/app/javascripts/components/gatherArchive.js index 309ec32..b827128 100644 --- a/app/javascripts/components/gatherArchive.js +++ b/app/javascripts/components/gatherArchive.js @@ -23,7 +23,7 @@ const ArchivedGathers = exports.ArchivedGathers = React.createClass({ }); return ( -
+
Archived Gathers
{archive} diff --git a/app/javascripts/components/gatherMenu.js b/app/javascripts/components/gatherMenu.js deleted file mode 100644 index 929dbae..0000000 --- a/app/javascripts/components/gatherMenu.js +++ /dev/null @@ -1,47 +0,0 @@ -const React = require("react"); - -const GatherMenu = exports.GatherMenu = React.createClass({ - getInitialState() { - return { - open: false - }; - }, - - toggleOpen(e) { - e.preventDefault(); - this.setState({ open: !this.state.open }); - }, - - chevron() { - if (this.state.open) { - return ; - } else { - return ; - } - }, - - render() { - const open = this.state.open; - let componentClass = ["treeview"]; - let dropdown; - if (open) { - componentClass.push("active"); - dropdown = ( - - ); - } - - return ( -
  • - - Gathers - {this.chevron()} - - {dropdown} -
  • - ); - } -}); - diff --git a/app/javascripts/components/main.js b/app/javascripts/components/main.js index 4f0edb1..d93f7e8 100644 --- a/app/javascripts/components/main.js +++ b/app/javascripts/components/main.js @@ -1,11 +1,10 @@ import {News} from "javascripts/components/news"; import {Events} from "javascripts/components/event"; -import {Gather} from "javascripts/components/gather"; +import {Gather, GatherMenu} from "javascripts/components/gather"; import {InfoButton} from "javascripts/components/info"; import {AdminPanel} from "javascripts/components/admin"; import {Chatroom} from "javascripts/components/message"; import {SoundPanel} from "javascripts/components/sound"; -import {GatherMenu} from "javascripts/components/gatherMenu"; import {SettingsPanel} from "javascripts/components/settings"; import {ArchivedGathers} from "javascripts/components/gatherArchive"; import {CurrentUser, ProfileModal, UserMenu} from "javascripts/components/user"; @@ -155,9 +154,15 @@ const GatherPage = React.createClass({ return { modal: null, - gather: { - gatherers: [] + gatherPool: { + public: { + gatherers: [] + }, + skilled: { + gatherers: [] + } }, + currentGather: "public", users: [], messages: [], maps: [], @@ -175,6 +180,10 @@ const GatherPage = React.createClass({ }; }, + currentGather() { + return this.state.gatherPool[this.state.currentGather]; + }, + componentDidMount() { let self = this; let socket = this.props.socket; @@ -187,7 +196,7 @@ const GatherPage = React.createClass({ if (state.from === 'gathering' && state.to === 'election' - && this.thisGatherer()) { + && this.thisGatherer(data.type)) { soundController.playGatherMusic(); } @@ -228,11 +237,13 @@ const GatherPage = React.createClass({ }); socket.on("gather:refresh", (data) => { + const gatherPool = this.state.gatherPool; + const type = data.type; + gatherPool[type] = data.gather; self.setState({ - gather: data.gather, maps: data.maps, servers: data.servers, - previousGather: data.previousGather + gatherPool: gatherPool }); this.updateTitle(); }); @@ -267,7 +278,7 @@ const GatherPage = React.createClass({ }, updateTitle() { - let gather = this.state.gather; + let gather = this.currentGather(); if (gather && this.state.updateTitle) { document.title = `NSL Gathers (${gather.gatherers.length}/12)`; return; @@ -292,8 +303,9 @@ const GatherPage = React.createClass({ this.updateTitle(); }, - thisGatherer() { - let gather = this.state.gather; + thisGatherer(gatherType) { + if (gatherType === undefined) gatherType = this.state.currentGather; + let gather = this.state.gatherPool[gatherType]; let user = this.state.user; if (gather && user && gather.gatherers.length) { return gather.gatherers @@ -347,6 +359,14 @@ const GatherPage = React.createClass({ }); }, + onGatherSelected(gatherName) { + let gather = this.state.gatherPool[gatherName]; + if (gather === undefined) return; + this.setState({ + currentGather: gather.type + }); + }, + render() { const socket = this.props.socket; @@ -442,10 +462,6 @@ const GatherPage = React.createClass({ -
      -
    • Gathers
    • - -
    • Information
    • @@ -461,19 +477,22 @@ const GatherPage = React.createClass({ socket={socket} maps={this.state.maps} user={this.state.user} - gather={this.state.gather} + gather={this.currentGather()} servers={this.state.servers} thisGatherer={this.thisGatherer()} - previousGather={this.state.previousGather} soundController={this.state.soundController} />
    + {eventsPanel}

    -
    +
    diff --git a/app/stylesheets/app.css b/app/stylesheets/app.css index 9e938b9..1e3c805 100644 --- a/app/stylesheets/app.css +++ b/app/stylesheets/app.css @@ -71,6 +71,11 @@ html, body { /*Gather Styles*/ +.archived-gather-panel { + max-width: 1140px; + margin: 0 auto; +} + .vote-button { min-width: 60px; } diff --git a/config/routes.js b/config/routes.js index 09e2089..d47812a 100644 --- a/config/routes.js +++ b/config/routes.js @@ -3,7 +3,7 @@ const path = require("path"); const winston = require("winston"); const config = require("./config.js"); -const Gather = require("../lib/gather/gather_singleton"); +const Gather = require("../lib/gather/gather_pool").get("public"); const mongoose = require("mongoose"); const Message = mongoose.model("Message"); const cors = require("cors"); diff --git a/lib/gather/controller.js b/lib/gather/controller.js index 5ae7e0f..e68a651 100644 --- a/lib/gather/controller.js +++ b/lib/gather/controller.js @@ -17,28 +17,26 @@ * */ -var Map = require("./map"); -var Server = require("./server"); -var mongoose = require("mongoose"); -var Gather = require("./gather_singleton"); -var ArchivedGather = mongoose.model("ArchivedGather"); -var Event = mongoose.model("Event"); -var _ = require("lodash"); -var winston = require("winston"); +const Map = require("./map"); +const Server = require("./server"); +const mongoose = require("mongoose"); +const GatherPool = require("./gather_pool"); +const ArchivedGather = mongoose.model("ArchivedGather"); +const Event = mongoose.model("Event"); +const _ = require("lodash"); +const winston = require("winston"); + +const emitGather = (socket, gather) => { + socket.emit("gather:refresh", { + gather: gather ? gather.toJson() : null, + type: gather ? gather.type : null, + maps: Map.list, + servers: Server.list + }); +} module.exports = function (namespace) { - var refreshGather = _.debounce(function () { - namespace.emit("gather:refresh", { - gather: Gather.current ? Gather.current.toJson() : null, - maps: Map.list, - servers: Server.list - }); - }, 200, { - leading: true, - trailing: true - }); - - var refreshArchive = () => { + const refreshArchive = () => { ArchivedGather.recent((error, recentGathers) => { if (error) return winston.error(error); namespace.emit("gather:archive:refresh", { @@ -49,26 +47,62 @@ module.exports = function (namespace) { }); }; - Gather.registerCallback('onDone', refreshGather); - Gather.registerCallback('onEvent', refreshGather); - Gather.registerCallback('onEvent', (event, from, to) => { - if (from !== to) { - namespace.emit("stateChange", { - event: event, - state: { - from: from, - to: to - } - }); + const refreshGather = type => { + if (type === undefined) { + for (let attr in gatherRefreshers) { + gatherRefreshers[attr].call(); + } + } else { + const refresh = gatherRefreshers[type]; + if (refresh) refresh(); } + } + + const gatherRefreshers = {}; // Stores debounced procedures to refresh gathers + + GatherPool.forEach((gatherManager, type) => { + let config = gatherManager.config; + gatherManager.registerCallback('onDone', refreshGather.bind(null, type)); + gatherManager.registerCallback('onEvent', refreshGather.bind(null, type)); + gatherManager.registerCallback('onEvent', (event, from, to) => { + if (from !== to) { + namespace.emit("stateChange", { + type: type, + event: event, + state: { + from: from, + to: to + } + }); + } + }); + + gatherManager.onArchiveUpdate(refreshArchive); + + gatherRefreshers[type] = _.debounce(function () { + namespace.emit("gather:refresh", { + gather: gatherManager.current ? gatherManager.current.toJson() : null, + type: gatherManager.current ? gatherManager.current.type : null, + maps: Map.list, + servers: Server.list + }); + }, 200, { + leading: true, + trailing: true + }); + + gatherManager.restart(); }); - Gather.onArchiveUpdate(refreshArchive); - Gather.restart(); // ***** Generate Test Users ***** if (process.env.POPULATE_GATHER) { let helper = require("./helper"); - helper.createTestUsers({ gather: Gather.current }, refreshGather); + + GatherPool.forEach(gatherManager => { + helper.createTestUsers({ + gather: gatherManager.current + }, refreshGather()); + }); } namespace.on("connection", function (socket) { @@ -82,61 +116,76 @@ module.exports = function (namespace) { }); socket.on("gather:join", function (data) { - let gather = Gather.current; + if (!data) data = {}; + const gatherManager = GatherPool.get(data.type); + if (!gatherManager) return; + const gather = gatherManager.current; if (gather.can("addGatherer")) gather.addGatherer(socket._user); Event.joiner(socket._user); - refreshGather(); + refreshGather(data.type); }); - socket.on("gather:refresh", function () { - socket.emit("gather:refresh", { - gather: Gather.current ? Gather.current.toJson() : null, - maps: Map.list, - servers: Server.list - }); + socket.on("gather:refresh", function (data) { + // Refresh all gathers + if (!data) data = {}; + if (data.type === undefined) { + GatherPool.forEach(manager => emitGather(socket, manager.current)); + return; + } + // Otherwise refresh specified gather + const gatherManager = GatherPool.get(data.type); + if (gatherManager == undefined) return; + emitGather(socket, gatherManager.current) }); - let removeGatherer = user => { - let gather = Gather.current; + const removeGatherer = (gather, user) => { let gatherLeaver = gather.getGatherer(user); if (gather.can("removeGatherer")) { gather.removeGatherer(user); } if (user.cooldown) gather.applyCooldown(user); Event.leaver(gatherLeaver.user); - refreshGather(); + refreshGather(gather.type); } socket.on("gather:leave", function (data) { - if (data && data.gatherer) { + if (!data) data = {}; + const gatherManager = GatherPool.get(data.type); + if (!gatherManager) return; + const gather = gatherManager.current; + if (data.gatherer) { // Remove gatherer defined by ID (admins only) if (!socket._user.isGatherAdmin()) return; - removeGatherer({ id: data.gatherer, cooldown: true }); + removeGatherer(gather, { id: data.gatherer, cooldown: true }); } else { // Remove gatherer attached to socket - removeGatherer(socket._user); + removeGatherer(gather, socket._user); } }); socket.on("gather:select", function (data) { - let gather = Gather.current; + if (!data) data = {}; + const gatherManager = GatherPool.get(data.type); + if (!gatherManager) return; + const gather = gatherManager.current; + let playerId = data.player; // Check team & leader - let gatherLeader = gather.getGatherer(socket._user); + let gatherer = gatherPool.getGatherer(socket._user); // Cancel if not gatherer or leader - if (gatherLeader === null || gatherLeader.leader === false) { + if (gatherer === null || gatherer.leader === false) { return null; } // Cancel if id belongs to a leader - let selectedPlayer = gather.getGatherer({id: playerId}); + let selectedPlayer = gatherPool.getGatherer({id: playerId}); if (selectedPlayer === null || selectedPlayer.leader) { return null; } - let team = gatherLeader.team; + let team = gatherer.team; let method = (team === 'alien') ? gather.moveToAlien : gather.moveToMarine; method.call(gather, selectedPlayer.user, socket._user); @@ -154,7 +203,7 @@ module.exports = function (namespace) { Event.playerSelected(socket._user, data, gather); - refreshGather(); + refreshGather(data.type); }); socket.on("disconnect", function () { @@ -162,7 +211,11 @@ module.exports = function (namespace) { }); socket.on("gather:vote", function (data) { - let gather = Gather.current; + if (!data) data = {}; + const gatherManager = GatherPool.get(data.type); + if (!gatherManager) return; + const gather = gatherManager.current; + if (data.leader) { gather.selectLeader(socket._user, data.leader.candidate); Event.leaderVote(socket._user, data, gather); @@ -182,17 +235,23 @@ module.exports = function (namespace) { gather.regather(socket._user, data.regather); } - refreshGather(); + refreshGather(data.type); }); - socket.on("gather:reset", function () { + socket.on("gather:reset", function (data) { + if (!data) data = {}; + const gatherManager = GatherPool.get(data.type); + if (!gatherManager) return; if (socket._user.isGatherAdmin()) { - Gather.reset(); - refreshGather(); + GatherManager.reset(); + refreshGather(data.type); Event.adminRegather(socket._user); } }); - refreshGather(); + // Refresh gather + for (let attr in gatherRefreshers) { + gatherRefreshers[attr].call(); + } }); }; diff --git a/lib/gather/gather.js b/lib/gather/gather.js index 5dcb855..149ae76 100644 --- a/lib/gather/gather.js +++ b/lib/gather/gather.js @@ -317,6 +317,7 @@ Gather.prototype.toJson = function () { return { name: this.name, description: this.description, + type: this.type, gatherers: this.gatherers, state: this.current, pickingTurn: this.pickingTurn(), diff --git a/lib/gather/gather_pool.js b/lib/gather/gather_pool.js new file mode 100644 index 0000000..3b83f13 --- /dev/null +++ b/lib/gather/gather_pool.js @@ -0,0 +1,104 @@ +"use strict" + +/* + * Implements a pool of concurrent gathers + * (no longer a singleton class, should rename) + * + */ + +const _ = require("lodash"); +const Gather = require("./gather"); +const winston = require("winston"); +const mongoose = require("mongoose"); +const ArchivedGather = mongoose.model("ArchivedGather"); +let gatherCallbacks = {}; +let archiveUpdatedCallback = () => {}; + +const GatherPool = new Map(); +const GATHER_CONFIGS = [ + { + type: "public", + name: "Public Gather", + description: "No requirements, 6v6" + }, + { + type: "skilled", + name: "Competitive Gather", + description: "Hive Requirements, 6v6" + } +]; + +GATHER_CONFIGS.forEach(config => { + const gatherManager = { + type: config.type, + name: config.name, + registerCallback: function (type, method) { + if (this.gatherCallbacks[type]) { + this.gatherCallbacks[type].push(method); + } else { + this.gatherCallbacks[type] = [method]; + } + }, + onArchiveUpdate: function (callback) { + archiveUpdatedCallback = callback; + }, + restart: function () { + this.previousGather = undefined; + this.current = undefined; + return newGather(); + }, + reset: function () { + return newGather(); + }, + current: Gather(), + previous: undefined, + gatherCallbacks: {} + }; + + gatherManager.gatherCallbacks['onDone'] = [function () { + rotateGather(); + }]; + + const newGather = () => { + const newGatherConfig = _.clone(config); + + newGatherConfig.onEvent = function () { + gatherManager.gatherCallbacks['onEvent'].forEach(cb => { + cb.apply(this, [].slice.call(arguments)) + }); + }; + + newGatherConfig.onDone = function () { + gatherManager.gatherCallbacks['onDone'].forEach(cb => { + cb.apply(this, [].slice.call(arguments)) + }); + }; + + return gatherManager.current = Gather(newGatherConfig); + }; + + const archiveGather = gather => { + ArchivedGather.archive(gather, (error, result) => { + if (error) return winston.error(error); + if (archiveUpdatedCallback + && typeof archiveUpdatedCallback === 'function') { + archiveUpdatedCallback(); + } + }); + }; + + const rotateGather = () => { + if (gatherManager.current) { + gatherManager.previous = gatherManager.current; + archiveGather(gatherManager.previous); + } + return newGather(); + }; + + GatherPool.set(config.type, gatherManager) + +}); + +// Register initial callback to reset gather when state is `done` + +module.exports = GatherPool; diff --git a/lib/gather/gather_singleton.js b/lib/gather/gather_singleton.js deleted file mode 100644 index 63814a3..0000000 --- a/lib/gather/gather_singleton.js +++ /dev/null @@ -1,71 +0,0 @@ -"use strict" - -let Gather = require("./gather"); -let gatherCallbacks = {}; -let archiveUpdatedCallback = () => {}; -let winston = require("winston"); -let mongoose = require("mongoose"); -let ArchivedGather = mongoose.model("ArchivedGather"); - -// Register initial callback to reset gather when state is `done` -gatherCallbacks['onDone'] = [function () { - rotateGather(); -}]; - -let newGather = () => { - return SingletonClass.current = Gather({ - onEvent: function () { - gatherCallbacks['onEvent'].forEach(cb => { - cb.apply(this, [].slice.call(arguments)) - }); - }, - onDone: function () { - gatherCallbacks['onDone'].forEach(cb => { - cb.apply(this, [].slice.call(arguments)) - }); - } - }); -}; - -let archiveGather = gather => { - ArchivedGather.archive(gather, (error, result) => { - if (error) return winston.error(error); - if (archiveUpdatedCallback - && typeof archiveUpdatedCallback === 'function') { - archiveUpdatedCallback(); - } - }); -}; - -let rotateGather = () => { - if (SingletonClass.current) { - SingletonClass.previous = SingletonClass.current; - archiveGather(SingletonClass.previous); - } - return newGather(); -} - -let SingletonClass = { - registerCallback: function (type, method) { - if (gatherCallbacks[type]) { - gatherCallbacks[type].push(method); - } else { - gatherCallbacks[type] = [method]; - } - }, - onArchiveUpdate: function (callback) { - archiveUpdatedCallback = callback; - }, - restart: function () { - this.previousGather = undefined; - this.current = undefined; - return newGather(); - }, - reset: function () { - return newGather(); - }, - current: Gather(), - previous: undefined -}; - -module.exports = SingletonClass; diff --git a/lib/steam/bot.js b/lib/steam/bot.js index c1c2b22..97956e9 100644 --- a/lib/steam/bot.js +++ b/lib/steam/bot.js @@ -5,7 +5,7 @@ var steam = require("steam"); var winston = require("winston"); var password = process.env.GATHER_STEAM_PASSWORD; var account_name = process.env.GATHER_STEAM_ACCOUNT; -var Gather = require("../gather/gather_singleton"); +var Gather = require("../gather/gather_pool").get("public"); function SteamBot(config) { let self = this;