mirror of
https://github.com/AsgardEternal/squad-js-map-vote.git
synced 2025-01-23 20:43:52 -06:00
initial commit
This commit is contained in:
commit
9754bc8416
352
config_tool.html
Normal file
352
config_tool.html
Normal file
@ -0,0 +1,352 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Map vote plugin configuration generator</title>
|
||||||
|
<style>
|
||||||
|
*
|
||||||
|
{
|
||||||
|
background-color: lightgray;
|
||||||
|
}
|
||||||
|
|
||||||
|
div#main
|
||||||
|
{
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-around;
|
||||||
|
margin: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.pannel
|
||||||
|
{
|
||||||
|
border: black;
|
||||||
|
border-radius: 15px;
|
||||||
|
border-style: solid;
|
||||||
|
padding: 10px;
|
||||||
|
padding-bottom: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
li.rule
|
||||||
|
{
|
||||||
|
border: 1px solid black;
|
||||||
|
border-radius: 10px;
|
||||||
|
padding: 5px;
|
||||||
|
margin: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
ul.rules
|
||||||
|
{
|
||||||
|
margin: 5px;
|
||||||
|
padding: 2px;
|
||||||
|
overflow: clip;
|
||||||
|
overflow-y: scroll;
|
||||||
|
width: 82ch;
|
||||||
|
height: 100ch;
|
||||||
|
border: solid black;
|
||||||
|
}
|
||||||
|
|
||||||
|
ul.nomination_list
|
||||||
|
{
|
||||||
|
list-style-type: none;
|
||||||
|
padding: 2px;
|
||||||
|
margin-top: 2px;
|
||||||
|
margin-bottom: 2px;
|
||||||
|
width: 75ch;
|
||||||
|
min-height: 5ch;
|
||||||
|
border: solid black;
|
||||||
|
}
|
||||||
|
|
||||||
|
ul.border_list
|
||||||
|
{
|
||||||
|
list-style-type: none;
|
||||||
|
margin: 5px;
|
||||||
|
padding: 2px;
|
||||||
|
overflow: clip;
|
||||||
|
overflow-y: scroll;
|
||||||
|
width: 75ch;
|
||||||
|
height: 10ch;
|
||||||
|
border: solid black;
|
||||||
|
}
|
||||||
|
|
||||||
|
li.selected
|
||||||
|
{
|
||||||
|
background-color: cadetblue;
|
||||||
|
}
|
||||||
|
|
||||||
|
div#info
|
||||||
|
{
|
||||||
|
margin: 2%;
|
||||||
|
}
|
||||||
|
|
||||||
|
li.nomination
|
||||||
|
{
|
||||||
|
border: 1px solid black;
|
||||||
|
margin: 5px;
|
||||||
|
padding: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
input.short_input
|
||||||
|
{
|
||||||
|
width: 10ch;
|
||||||
|
}
|
||||||
|
|
||||||
|
input.rule_input
|
||||||
|
{
|
||||||
|
width: 30ch;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="main">
|
||||||
|
<div id="left" class="pannel">
|
||||||
|
<div id="controls">
|
||||||
|
<button id="save">save</button>
|
||||||
|
<input type="file" id="load" accept=".json">
|
||||||
|
</div>
|
||||||
|
<div id="rules_pannel">
|
||||||
|
<h3>vote rules:</h3>
|
||||||
|
<button id="add_rule">add rule</button>
|
||||||
|
<ul id="rules_list" class="rules"></ul>
|
||||||
|
</div>
|
||||||
|
<div id="modes">
|
||||||
|
<h3>mode strings (case sensitive):</h3>
|
||||||
|
<ul id="modes_list" class="border_list"></ul>
|
||||||
|
<input type="text" id="mode_input">
|
||||||
|
<button id="add_mode">add</button>
|
||||||
|
<button id="remove_mode">remove</button>
|
||||||
|
</div>
|
||||||
|
<div id="addons">
|
||||||
|
<h3>addon layer strings:</h3>
|
||||||
|
(not currently implemented)
|
||||||
|
<ul id="addons_list"></ul>
|
||||||
|
<input type="text">
|
||||||
|
<button>add</button>
|
||||||
|
<button>remove</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div id="info">
|
||||||
|
<h3>info:</h3>
|
||||||
|
tool by maskedmonkyman <br>
|
||||||
|
version 1.0 <br>
|
||||||
|
<h4>warning:</h4>
|
||||||
|
<p>
|
||||||
|
this tool is a thin wrapper around
|
||||||
|
the Squad JS pluggin config schemea. This tool
|
||||||
|
does <b>NO INPUT VALIDATION</b> and is just a graphical utility.
|
||||||
|
It is very possible to create broken configs using
|
||||||
|
this tool. Be sure to check your inputs.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<h4>notes:</h4>
|
||||||
|
<ol>
|
||||||
|
<li>
|
||||||
|
be sure to always have a "defualt" vote rule.
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
the Any option for modes, will select modes from the
|
||||||
|
mode strings section so be sure to fill it out.
|
||||||
|
</li>
|
||||||
|
</ol>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<script>
|
||||||
|
"use strict";
|
||||||
|
//TODO addon layers
|
||||||
|
const save_button = document.getElementById("save");
|
||||||
|
const load_button = document.getElementById("load");
|
||||||
|
//rules
|
||||||
|
const add_rule_button = document.getElementById("add_rule");
|
||||||
|
const remove_rule_button = document.getElementById("remove_rule");
|
||||||
|
const rules_list = document.getElementById("rules_list");
|
||||||
|
let selected_rule = null;
|
||||||
|
//mode
|
||||||
|
const add_mode_button = document.getElementById("add_mode");
|
||||||
|
const remove_mode_button = document.getElementById("remove_mode");
|
||||||
|
const mode_input = document.getElementById("mode_input");
|
||||||
|
const modes_list = document.getElementById("modes_list");
|
||||||
|
let selected_mode = null;
|
||||||
|
|
||||||
|
add_rule_button.onclick = ()=>{
|
||||||
|
let rule = build_rule_element();
|
||||||
|
rules_list.appendChild(rule);
|
||||||
|
};
|
||||||
|
|
||||||
|
add_mode_button.onclick = () => {
|
||||||
|
if (mode_input.value == "")
|
||||||
|
return;
|
||||||
|
add_mode(mode_input.value);
|
||||||
|
};
|
||||||
|
|
||||||
|
function add_mode(mode_string)
|
||||||
|
{
|
||||||
|
let mode = document.createElement("li");
|
||||||
|
mode.innerHTML = mode_string;
|
||||||
|
modes_list.appendChild(mode);
|
||||||
|
mode.onclick = () => {
|
||||||
|
if (selected_mode)
|
||||||
|
{
|
||||||
|
selected_mode.classList.remove("selected");
|
||||||
|
}
|
||||||
|
selected_mode = mode;
|
||||||
|
selected_mode.classList.add("selected");
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
remove_mode_button.onclick = () => {
|
||||||
|
if (selected_mode)
|
||||||
|
{
|
||||||
|
selected_mode.parentNode.removeChild(selected_mode);
|
||||||
|
selected_mode = null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
save_button.onclick = () => {
|
||||||
|
let vote_rules = {
|
||||||
|
"modes": [],
|
||||||
|
"rules": {},
|
||||||
|
"addon_map_layer_strings": []
|
||||||
|
};
|
||||||
|
|
||||||
|
for(let mode of modes_list.children)
|
||||||
|
{
|
||||||
|
vote_rules.modes.push(mode.innerHTML.trim());
|
||||||
|
}
|
||||||
|
|
||||||
|
for(let rule_element of rules_list.children)
|
||||||
|
{
|
||||||
|
let rule = {
|
||||||
|
"name": rule_element.rule_string_input.value,
|
||||||
|
"nominations": []
|
||||||
|
};
|
||||||
|
for (let nomination of rule_element.nomination_list.children)
|
||||||
|
{
|
||||||
|
rule.nominations.push({
|
||||||
|
"map": nomination.map_input.value.trim(),
|
||||||
|
"modes": nomination.mode_input.value.trim().split(',').map(e => e.trim()),
|
||||||
|
"versions": nomination.verison_input.value.trim().split(',').map(e => e.trim())
|
||||||
|
});
|
||||||
|
}
|
||||||
|
vote_rules.rules[rule.name] = rule.nominations;
|
||||||
|
}
|
||||||
|
var a = document.createElement("a");
|
||||||
|
a.href = window.URL.createObjectURL(new Blob([JSON.stringify(vote_rules)], {type: "text/plain"}));
|
||||||
|
a.download = "vote_rules.json";
|
||||||
|
a.click();
|
||||||
|
};
|
||||||
|
|
||||||
|
load_button.onchange = () => {
|
||||||
|
let reader = new FileReader();
|
||||||
|
reader.addEventListener("load", (event) => {
|
||||||
|
let vote_rules = JSON.parse(event.target.result);
|
||||||
|
for (let mode of vote_rules.modes)
|
||||||
|
add_mode(mode);
|
||||||
|
for (let rule in vote_rules.rules)
|
||||||
|
{
|
||||||
|
let rule_element = build_rule_element();
|
||||||
|
rule_element.rule_string_input.value = rule;
|
||||||
|
rules_list.appendChild(rule_element);
|
||||||
|
for(let nomination of vote_rules.rules[rule])
|
||||||
|
{
|
||||||
|
let nomination_element = build_nomination_element(nomination.map, nomination.modes, nomination.versions);
|
||||||
|
rule_element.nomination_list.appendChild(nomination_element);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
reader.readAsText(load_button.files[0]);
|
||||||
|
};
|
||||||
|
|
||||||
|
function build_rule_element()
|
||||||
|
{
|
||||||
|
let rule = document.createElement("li");
|
||||||
|
rule.classList.add("rule");
|
||||||
|
let label = document.createElement("label");
|
||||||
|
label.innerHTML = "Rule string: ";
|
||||||
|
let input = document.createElement("input");
|
||||||
|
input.setAttribute("type", "text");
|
||||||
|
input.classList.add("rule_input");
|
||||||
|
label.appendChild(input);
|
||||||
|
rule.appendChild(label);
|
||||||
|
rule["rule_string_input"] = input;
|
||||||
|
|
||||||
|
let add_nomination_button = document.createElement("button");
|
||||||
|
add_nomination_button.innerHTML = "add nomination";
|
||||||
|
rule.appendChild(add_nomination_button);
|
||||||
|
|
||||||
|
let nominations_list = document.createElement("ul");
|
||||||
|
nominations_list.classList.add("nomination_list");
|
||||||
|
rule.appendChild(nominations_list);
|
||||||
|
rule["nomination_list"] = nominations_list;
|
||||||
|
|
||||||
|
let delete_button = document.createElement("button");
|
||||||
|
delete_button.innerHTML = "delete";
|
||||||
|
rule.appendChild(delete_button);
|
||||||
|
|
||||||
|
add_nomination_button.onclick = () => {
|
||||||
|
nominations_list.appendChild(build_nomination_element());
|
||||||
|
};
|
||||||
|
|
||||||
|
delete_button.onclick = () => {
|
||||||
|
rule.parentNode.removeChild(rule);
|
||||||
|
}
|
||||||
|
return rule;
|
||||||
|
}
|
||||||
|
|
||||||
|
function build_nomination_element(map_string = "", mode_string = "Any", version_string = "Any")
|
||||||
|
{
|
||||||
|
let nomination = document.createElement("li");
|
||||||
|
nomination.classList.add("nomination");
|
||||||
|
|
||||||
|
//map field
|
||||||
|
let map_label = document.createElement("label");
|
||||||
|
map_label.innerHTML = "Map:";
|
||||||
|
nomination.appendChild(map_label);
|
||||||
|
let map_input = document.createElement("input");
|
||||||
|
map_input.setAttribute("type", "text");
|
||||||
|
map_input.value = map_string;
|
||||||
|
map_label.appendChild(map_input);
|
||||||
|
nomination["map_input"] = map_input;
|
||||||
|
|
||||||
|
//mode field
|
||||||
|
let mode_label = document.createElement("label");
|
||||||
|
mode_label.innerHTML = "Mode(s):";
|
||||||
|
nomination.appendChild(mode_label);
|
||||||
|
let mode_input = document.createElement("input");
|
||||||
|
mode_input.setAttribute("type", "text");
|
||||||
|
mode_input.classList.add("short_input");
|
||||||
|
mode_input.value = mode_string;
|
||||||
|
mode_label.appendChild(mode_input);
|
||||||
|
nomination["mode_input"] = mode_input;
|
||||||
|
|
||||||
|
//version feild
|
||||||
|
let version = document.createElement("label");
|
||||||
|
version.innerHTML = "Version(s):";
|
||||||
|
nomination.appendChild(version);
|
||||||
|
let verison_input = document.createElement("input");
|
||||||
|
verison_input.setAttribute("type", "text");
|
||||||
|
verison_input.classList.add("short_input");
|
||||||
|
verison_input.value = version_string;
|
||||||
|
version.appendChild(verison_input);
|
||||||
|
nomination["verison_input"] = verison_input;
|
||||||
|
|
||||||
|
//delete button
|
||||||
|
let delete_button = document.createElement("button");
|
||||||
|
delete_button.innerHTML = "delete";
|
||||||
|
delete_button.onclick = () => {
|
||||||
|
nomination.parentNode.removeChild(nomination);
|
||||||
|
};
|
||||||
|
nomination.appendChild(delete_button);
|
||||||
|
|
||||||
|
return nomination;
|
||||||
|
}
|
||||||
|
|
||||||
|
function main()
|
||||||
|
{
|
||||||
|
console.log("hello world");
|
||||||
|
}
|
||||||
|
|
||||||
|
document.addEventListener("DOMContentLoaded", main);
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
442
mapvote.js
Normal file
442
mapvote.js
Normal file
@ -0,0 +1,442 @@
|
|||||||
|
//Plugin by MaskedMonkeyMan
|
||||||
|
|
||||||
|
import BasePlugin from "./base-plugin.js";
|
||||||
|
|
||||||
|
import fs from "fs";
|
||||||
|
import { Layers } from "../layers/index.js"
|
||||||
|
|
||||||
|
function randomElement(array)
|
||||||
|
{
|
||||||
|
return array[Math.floor(Math.random() * array.length)];
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatChoice(choiceIndex, mapString, currentVotes)
|
||||||
|
{
|
||||||
|
return `type !vote ${choiceIndex + 1} : ${mapString} (${currentVotes} votes)`
|
||||||
|
}
|
||||||
|
|
||||||
|
function toMils(min)
|
||||||
|
{
|
||||||
|
return min * 60 * 1000;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default class MapVote extends BasePlugin
|
||||||
|
{
|
||||||
|
static get description()
|
||||||
|
{
|
||||||
|
return "Map Voting plugin";
|
||||||
|
}
|
||||||
|
|
||||||
|
static get defaultEnabled()
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
static get optionsSpecification()
|
||||||
|
{
|
||||||
|
return {
|
||||||
|
commandPrefix:
|
||||||
|
{
|
||||||
|
required: false,
|
||||||
|
description: "command name to use in chat",
|
||||||
|
default: "!vote"
|
||||||
|
},
|
||||||
|
voteRulesPath:
|
||||||
|
{
|
||||||
|
required: false,
|
||||||
|
description: 'the path to the layersConfig file',
|
||||||
|
default: ''
|
||||||
|
},
|
||||||
|
minPlayersForVote:
|
||||||
|
{
|
||||||
|
required: false,
|
||||||
|
description: 'number of players needed on the server for a vote to start',
|
||||||
|
default: 50
|
||||||
|
},
|
||||||
|
voteWaitTimeFromMatchStart:
|
||||||
|
{
|
||||||
|
required: false,
|
||||||
|
description: 'time in mils from the start of a round to the start of a new map vote',
|
||||||
|
default: 20
|
||||||
|
},
|
||||||
|
voteBroadcastInterval:
|
||||||
|
{
|
||||||
|
required: false,
|
||||||
|
description: 'broadcast interval for vote notification in mils',
|
||||||
|
default: 15
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(server, options, connectors)
|
||||||
|
{
|
||||||
|
super(server, options, connectors);
|
||||||
|
|
||||||
|
this.voteRules = {}; //data object holding vote configs
|
||||||
|
this.nominations = []; //layer strings for the current vote choices
|
||||||
|
this.trackedVotes = {}; //player votes, keyed by steam id
|
||||||
|
this.tallies = []; //votes per layer, parellel with nominations
|
||||||
|
this.votingEnabled = false;
|
||||||
|
this.onConnectBound = false;
|
||||||
|
this.broadcastIntervalTask = null;
|
||||||
|
|
||||||
|
this.onNewGame = this.onNewGame.bind(this);
|
||||||
|
this.onPlayerDisconnected = this.onPlayerDisconnected.bind(this);
|
||||||
|
this.onChatMessage = this.onChatMessage.bind(this);
|
||||||
|
this.broadcastNominations = this.broadcastNominations.bind(this);
|
||||||
|
this.beginVoting = this.beginVoting.bind(this);
|
||||||
|
|
||||||
|
this.msgBroadcast = (msg) => {this.server.rcon.broadcast(msg);};
|
||||||
|
this.msgDirect = (steamid, msg) => {this.server.rcon.warn(steamid, msg);};
|
||||||
|
|
||||||
|
//load voteRules with options from source file
|
||||||
|
this.loadLayersConfig();
|
||||||
|
}
|
||||||
|
|
||||||
|
async mount()
|
||||||
|
{
|
||||||
|
this.server.on('NEW_GAME', this.onNewGame);
|
||||||
|
this.server.on('CHAT_MESSAGE', this.onChatMessage);
|
||||||
|
this.server.on('PLAYER_DISCONNECTED', this.onPlayerDisconnected);
|
||||||
|
this.verbose(1, 'Map vote was mounted.');
|
||||||
|
}
|
||||||
|
|
||||||
|
async unmount()
|
||||||
|
{
|
||||||
|
this.server.removeEventListener('NEW_GAME', this.onNewGame);
|
||||||
|
this.server.removeEventListener('CHAT_MESSAGE', this.onChatMessage);
|
||||||
|
this.server.removeEventListener('PLAYER_DISCONNECTED', this.onPlayerDisconnected);
|
||||||
|
clearInterval(this.broadcastIntervalTask);
|
||||||
|
this.verbose(1, 'Map vote was un-mounted.');
|
||||||
|
}
|
||||||
|
|
||||||
|
//loads layer configs from disk into plugin memory
|
||||||
|
loadLayersConfig()
|
||||||
|
{
|
||||||
|
this.verbose(1, `Fetching Map Voting Lists...`);
|
||||||
|
|
||||||
|
let layersConfigString = '';
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (!fs.existsSync(this.options.voteRulesPath))
|
||||||
|
throw new Error(`Could not find Map Vote List at ${this.options.voteRulesPath}`);
|
||||||
|
layersConfigString = fs.readFileSync(this.options.voteRulesPath, 'utf8');
|
||||||
|
}
|
||||||
|
catch (error)
|
||||||
|
{
|
||||||
|
this.verbose('SquadServer', 1, `Error fetching mapvoting list: ${options.voteRulesPath}`, error);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.voteRules = JSON.parse(layersConfigString);
|
||||||
|
}
|
||||||
|
|
||||||
|
async onNewGame()
|
||||||
|
{
|
||||||
|
//wait to start voting
|
||||||
|
this.endVoting();
|
||||||
|
this.trackedVotes = {};
|
||||||
|
this.tallies = [];
|
||||||
|
this.nominations = [];
|
||||||
|
setTimeout(this.beginVoting, toMils(this.options.voteWaitTimeFromMatchStart));
|
||||||
|
}
|
||||||
|
|
||||||
|
async onPlayerDisconnected()
|
||||||
|
{
|
||||||
|
await this.server.updatePlayerList();
|
||||||
|
this.clearVote();
|
||||||
|
this.updateNextMap();
|
||||||
|
}
|
||||||
|
|
||||||
|
async onChatMessage(info)
|
||||||
|
{
|
||||||
|
const {steamID, name: playerName} = info;
|
||||||
|
const message = info.message.toLowerCase();
|
||||||
|
//check to see if this message has a command prefix
|
||||||
|
if (!message.startsWith(this.options.commandPrefix))
|
||||||
|
return;
|
||||||
|
|
||||||
|
const subCommand = message.substring(this.options.commandPrefix.length).trim();
|
||||||
|
if(!isNaN(subCommand)) // if this succeeds player is voting for a map
|
||||||
|
{
|
||||||
|
const mapNumber = parseInt(subCommand); //try to get a vote number
|
||||||
|
if (!this.votingEnabled)
|
||||||
|
{
|
||||||
|
await this.msgDirect(steamID, "There is no vote running right now");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await this.registerVote(steamID, mapNumber, playerName);
|
||||||
|
this.updateNextMap();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const isAdmin = info.chat === "ChatAdmin";
|
||||||
|
switch(subCommand) // select the sub command
|
||||||
|
{
|
||||||
|
case "choices": //sends choices to player in the from of a warning
|
||||||
|
if (!this.votingEnabled)
|
||||||
|
{
|
||||||
|
await this.msgDirect(steamID, "There is no vote running right now");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.directMsgNominations(steamID);
|
||||||
|
return;
|
||||||
|
case "results": //sends player the results in a warning
|
||||||
|
if (!this.votingEnabled)
|
||||||
|
{
|
||||||
|
await this.msgDirect(steamID, "There is no vote running right now");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.directMsgNominations(steamID);
|
||||||
|
return;
|
||||||
|
case "restart": //starts the vote again if it was canceled
|
||||||
|
if(!isAdmin) return;
|
||||||
|
|
||||||
|
if(this.votingEnabled)
|
||||||
|
{
|
||||||
|
await this.msgDirect(steamID, "Voting is already enabled");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.beginVoting(true);
|
||||||
|
return;
|
||||||
|
case "cancel": //cancels the current vote and wont set next map to current winnner
|
||||||
|
if(!isAdmin) return;
|
||||||
|
|
||||||
|
if(!this.votingEnabled)
|
||||||
|
{
|
||||||
|
await this.msgDirect(steamID, "Voting is already disabled");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.endVoting();
|
||||||
|
await this.msgDirect(steamID, "ending current vote");
|
||||||
|
return;
|
||||||
|
case "reload": //allows for config hot reloads
|
||||||
|
if(!isAdmin) return;
|
||||||
|
|
||||||
|
this.loadLayersConfig();
|
||||||
|
await this.msgDirect(steamID, "Reloaded map vote layers configuration")
|
||||||
|
return;
|
||||||
|
case "help": //displays available commands
|
||||||
|
await this.msgDirect(steamID, `!vote <choices|number|results>`);
|
||||||
|
if(!isAdmin) return;
|
||||||
|
|
||||||
|
await this.msgDirect(steamID, `!vote <restart|cancel|reload> (admin only)`);
|
||||||
|
return;
|
||||||
|
default:
|
||||||
|
//give them an error
|
||||||
|
await this.msgDirect(steamID, `Unknown vote subcommand: ${subCommand}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
updateNextMap() //sets next map to current mapvote winner, if there is a tie will pick at random
|
||||||
|
{
|
||||||
|
const nextMap = randomElement(this.currentWinners);
|
||||||
|
this.server.rcon.execute(`AdminSetNextLayer ${nextMap}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
//TODO: right now if version is set to "Any" no caf layers will be selected
|
||||||
|
populateNominations() //gets nomination strings from layer options
|
||||||
|
{
|
||||||
|
if (!this.server.currentLayer)
|
||||||
|
{
|
||||||
|
this.verbose(1, "Error: unknown currentLayer");
|
||||||
|
endVoting();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const splitName = name => name.substring(0, name.lastIndexOf("_"));
|
||||||
|
const removeCAF = name => name.replace("CAF_", "");
|
||||||
|
|
||||||
|
this.nominations = [];
|
||||||
|
const rulesList = this.voteRules.rules;
|
||||||
|
let layerString = this.server.currentLayer.layerid;
|
||||||
|
let nominationsList = rulesList.default;
|
||||||
|
|
||||||
|
const matchLayers = builtString => Layers.layers.filter((element) => element.layerid.startsWith(builtString));
|
||||||
|
|
||||||
|
while(layerString.length > 0)
|
||||||
|
{
|
||||||
|
if(layerString in rulesList)
|
||||||
|
{
|
||||||
|
nominationsList = rulesList[layerString];
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
layerString = removeCAF(layerString);
|
||||||
|
layerString = splitName(layerString);
|
||||||
|
}
|
||||||
|
|
||||||
|
for(const nomination of nominationsList)
|
||||||
|
{
|
||||||
|
const mapName = nomination.map;
|
||||||
|
let mode = randomElement(nomination.modes);
|
||||||
|
let version = randomElement(nomination.versions);
|
||||||
|
let cafPrefix = "";
|
||||||
|
|
||||||
|
if (version.includes("CAF_"))
|
||||||
|
{
|
||||||
|
cafPrefix = "CAF_";
|
||||||
|
version = removeCAF(version);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mode === "Any")
|
||||||
|
{
|
||||||
|
let modes = this.voteRules.modes;
|
||||||
|
while (modes.length > 0)
|
||||||
|
{
|
||||||
|
mode = randomElement(this.voteRules.modes);
|
||||||
|
modes = modes.filter((elem) => elem !== mode);
|
||||||
|
if (matchLayers(`${cafPrefix}${mapName}_${mode}`).length > 0)
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let builtLayerString = `${cafPrefix}${mapName}_${mode}_${version}`;
|
||||||
|
if (version === "Any")
|
||||||
|
{
|
||||||
|
let versions = matchLayers(`${cafPrefix}${mapName}_${mode}`);
|
||||||
|
if (versions.length == 0)
|
||||||
|
{
|
||||||
|
this.verbose(1, `error: could not find layer for ${builtLayerString} from vote rule \"${layerString}\"`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
versions = versions.map(l => l.layerid);
|
||||||
|
version = randomElement(versions);
|
||||||
|
version = version.substring(version.lastIndexOf("_") + 1, version.length);
|
||||||
|
builtLayerString = `${cafPrefix}${mapName}_${mode}_${version}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!Layers.getLayerByCondition((layer) => layer.layerid === builtLayerString))
|
||||||
|
{
|
||||||
|
this.verbose(1, `error: could not find layer for ${builtLayerString} from vote rule \"${layerString}\"`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
this.nominations.push(builtLayerString);
|
||||||
|
this.tallies.push(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//checks if there are enough players to start voting, if not binds itself to player connected
|
||||||
|
//when there are enough players it clears old votes, sets up new nominations, and starts broadcast
|
||||||
|
beginVoting(force = false)
|
||||||
|
{
|
||||||
|
const playerCount = this.server.players.length;
|
||||||
|
const minPlayers = this.options.minPlayersForVote;
|
||||||
|
if (playerCount < minPlayers && !force)
|
||||||
|
{
|
||||||
|
if (this.onConnectBound == false)
|
||||||
|
{
|
||||||
|
this.server.on("PLAYER_CONNECTED", this.beginVoting)
|
||||||
|
this.onConnectBound = true;
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if(this.onConnectBound)
|
||||||
|
{
|
||||||
|
this.server.removeEventListener("PLAYER_CONNECTED", this.beginVoting);
|
||||||
|
this.onConnectBound = false;
|
||||||
|
}
|
||||||
|
// these need to be reset after reenabling voting
|
||||||
|
this.trackedVotes = {};
|
||||||
|
this.tallies = [];
|
||||||
|
|
||||||
|
this.populateNominations();
|
||||||
|
|
||||||
|
this.votingEnabled = true;
|
||||||
|
this.broadcastNominations();
|
||||||
|
this.broadcastIntervalTask = setInterval(this.broadcastNominations, toMils(this.options.voteBroadcastInterval));
|
||||||
|
}
|
||||||
|
|
||||||
|
endVoting()
|
||||||
|
{
|
||||||
|
this.votingEnabled = false;
|
||||||
|
clearInterval(this.broadcastIntervalTask);
|
||||||
|
this.broadcastIntervalTask = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
//sends a message about nominations through a broadcast
|
||||||
|
//NOTE: max squad broadcast message length appears to be 485 characters
|
||||||
|
//Note: broadcast strings with multi lines are very strange
|
||||||
|
async broadcastNominations()
|
||||||
|
{
|
||||||
|
await this.msgBroadcast("Type !vote <map number> in chat to cast your vote, Candidates:\n");
|
||||||
|
let nominationStrings = [];
|
||||||
|
for(let choice in this.nominations)
|
||||||
|
{
|
||||||
|
choice = Number(choice);
|
||||||
|
nominationStrings.push(formatChoice(choice, this.nominations[choice], this.tallies[choice]));
|
||||||
|
}
|
||||||
|
await this.msgBroadcast(nominationStrings.join("\n"));
|
||||||
|
//const winners = this.currentWinners;
|
||||||
|
//await this.msgBroadcast(`Current winner${winners.length > 1 ? "s" : ""}: ${winners.join(", ")}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async directMsgNominations(steamID)
|
||||||
|
{
|
||||||
|
for(let choice in this.nominations)
|
||||||
|
{
|
||||||
|
choice = Number(choice);
|
||||||
|
await this.msgDirect(steamID, formatChoice(choice, this.nominations[choice], this.tallies[choice]));
|
||||||
|
}
|
||||||
|
|
||||||
|
const winners = this.currentWinners;
|
||||||
|
await this.msgDirect(steamID, `Current winner${winners.length > 1 ? "s" : ""}: ${winners.join(", ")}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
//counts a vote from a player and adds it to tallies
|
||||||
|
async registerVote(steamID, nominationIndex, playerName)
|
||||||
|
{
|
||||||
|
nominationIndex -= 1; // shift indices from display range
|
||||||
|
if(nominationIndex < 0 || nominationIndex > this.nominations.length)
|
||||||
|
{
|
||||||
|
await this.msgDirect(steamID, `[Map Vote] ${playerName}: invalid map number, typ !vote results to see map numbers`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const previousVote = this.trackedVotes[steamID];
|
||||||
|
this.trackedVotes[steamID] = nominationIndex;
|
||||||
|
|
||||||
|
this.tallies[nominationIndex] += 1;
|
||||||
|
if(previousVote !== undefined)
|
||||||
|
this.tallies[previousVote] -= 1;
|
||||||
|
await this.msgDirect(steamID, `you voted for ${this.nominations[nominationIndex]}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
//removes a players vote if they disconnect from the sever
|
||||||
|
clearVote()
|
||||||
|
{
|
||||||
|
const currentPlayers = this.server.players.map((p) => p.steamID);
|
||||||
|
for (const steamID in this.trackedVotes)
|
||||||
|
{
|
||||||
|
if (!(currentPlayers.includes(steamID)))
|
||||||
|
{
|
||||||
|
const vote = this.trackedVotes[steamID];
|
||||||
|
this.tallies[vote] -= 1;
|
||||||
|
delete this.trackedVotes[steamID];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//calculates the current winner(s) of the vote and returns thier strings in an array
|
||||||
|
get currentWinners()
|
||||||
|
{
|
||||||
|
const ties = [];
|
||||||
|
|
||||||
|
let highestScore = -Infinity;
|
||||||
|
for(let choice in this.tallies)
|
||||||
|
{
|
||||||
|
const score = this.tallies[choice];
|
||||||
|
if(score < highestScore)
|
||||||
|
continue;
|
||||||
|
else if(score > highestScore)
|
||||||
|
{
|
||||||
|
highestScore = score;
|
||||||
|
ties.length = 0;
|
||||||
|
ties.push(choice);
|
||||||
|
}
|
||||||
|
else // equal
|
||||||
|
ties.push(choice);
|
||||||
|
}
|
||||||
|
|
||||||
|
return ties.map(i => this.nominations[i]);
|
||||||
|
}
|
||||||
|
}
|
42
vote_rules_example.json
Normal file
42
vote_rules_example.json
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
{
|
||||||
|
"modes":
|
||||||
|
[
|
||||||
|
"AAS",
|
||||||
|
"RAAS",
|
||||||
|
"Invasion"
|
||||||
|
],
|
||||||
|
|
||||||
|
"rules":
|
||||||
|
{
|
||||||
|
"Gorodok":
|
||||||
|
[
|
||||||
|
{"map": "Yehorivka", "modes": ["Any"], "versions": ["Any"]},
|
||||||
|
{"map": "Gorodok", "modes": ["Any"], "versions": ["Any"]}
|
||||||
|
],
|
||||||
|
|
||||||
|
"Gorodok_Invasion":
|
||||||
|
[
|
||||||
|
{"map": "Yehorivka", "modes": ["Any"], "versions": ["Any"]}
|
||||||
|
],
|
||||||
|
|
||||||
|
"Yehorivka_RAAS_v1":
|
||||||
|
[
|
||||||
|
{"map": "Gorodok", "modes": ["AAS"], "versions": ["Any"]}
|
||||||
|
],
|
||||||
|
|
||||||
|
"default":
|
||||||
|
[
|
||||||
|
{"map": "Yehorivka", "modes": ["Any"], "versions": ["Any"]},
|
||||||
|
{"map": "Gorodok", "modes": ["Any"], "versions": ["Any"]}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
|
||||||
|
"addon_map_layer_strings":
|
||||||
|
{
|
||||||
|
"example":
|
||||||
|
[
|
||||||
|
"string1",
|
||||||
|
"string2"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user