Initial commit.

This commit is contained in:
Ian Gulliver
2014-11-17 20:59:03 -08:00
commit 8f287a7184
29 changed files with 4297 additions and 0 deletions

21
app.yaml Normal file
View File

@@ -0,0 +1,21 @@
runtime: python27
version: 1
api_version: 1
application: strife-drafter
threadsafe: true
handlers:
- url: /
static_files: index.html
upload: index.html
secure: always
- url: /static
static_dir: static
secure: always
includes:
- cosmopolite
inbound_services:
- channel_presence

51
index.html Normal file
View File

@@ -0,0 +1,51 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Strife Drafter</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link href="https://fonts.googleapis.com/css?family=Audiowide" rel="stylesheet" type="text/css">
<link href="https://fonts.googleapis.com/css?family=Oswald:400,700" rel="stylesheet" type="text/css">
<link rel="stylesheet" href="/static/style.css">
<script src="/cosmopolite/static/cosmopolite.js" charset="UTF-8"></script>
<script src="/static/draft.js" charset="UTF-8"></script>
</head>
<body>
<div class="container">
<div class="team-column" style="order: 1;">
<div>
<button id="glory">Glory</button>
</div>
<div id="glory-cont"></div>
</div>
<div class="team-column" style="order: 3;">
<div>
<button id="valor">Valor</button>
</div>
<div id="valor-cont"></div>
</div>
<div class="hero-column" style="order: 2;">
<div class="title-container">
<div class="timer">
<div id="glory-step"></div>
<div id="glory-extra"></div>
</div>
<div id="game-title"></div>
<div class="timer">
<div id="valor-step"></div>
<div id="valor-extra"></div>
</div>
</div>
<div id="heroes">
<div id="countdown"></div>
<div id="slide"></div>
</div>
</div>
</div>
</body>
</html>

BIN
static/ban.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

532
static/draft.js Normal file
View File

@@ -0,0 +1,532 @@
var HEROES = [
'Ray',
'Fetterstone',
'Claudessa',
'Caprice',
'Lady Tinder',
'Shank',
'Vermillion',
'Malady',
'Bo',
'Carter',
'Ace',
'Moxie',
'Bastion',
'Hale',
'Minerva',
'Rook',
'Vex',
'Trixie',
'Blazer',
'Harrower',
'Gokong',
'Jin She',
'Nikolai',
];
var steps = [
{
'type': 'seating',
},
{
'type': 'countdown',
'seconds': 10,
},
{
'type': 'ban',
'side': 'glory',
'seconds': 60,
'slots': 1,
},
{
'type': 'ban',
'side': 'valor',
'seconds': 60,
'slots': 1,
},
{
'type': 'pick',
'side': 'glory',
'seconds': 60,
'slots': 1,
},
{
'type': 'pick',
'side': 'valor',
'seconds': 60,
'slots': 2,
},
{
'type': 'pick',
'side': 'glory',
'seconds': 60,
'slots': 2,
},
{
'type': 'pick',
'side': 'valor',
'seconds': 60,
'slots': 2,
},
{
'type': 'pick',
'side': 'glory',
'seconds': 60,
'slots': 2,
},
{
'type': 'pick',
'side': 'valor',
'seconds': 60,
'slots': 1,
},
{
'type': 'end',
}
];
var next_step = 0;
var client_last_step_time = 0;
var server_last_step_time = 0;
var cosmo, game_id;
var sides = {
'global': {
'bans': [
'Gokong',
],
},
'glory': {
'bans': [],
'picks': [],
'team_name': '???',
'extra_seconds': 60,
},
'valor': {
'bans': [],
'picks': [],
'team_name': '???',
'extra_seconds': 60,
},
};
var getTime = function() {
return Math.floor(new Date().getTime() / 1000);
}
var setTeamName = function() {
localStorage['strife-drafter:team_name'] = prompt(
'Enter your team name, or Cancel to observe.') || '';
return localStorage['strife-drafter:team_name'];
};
var onSideClick = function(sidename) {
if (!localStorage['strife-drafter:team_name']) {
while (confirm(
'You must set a team name before participating in the draft.' +
'Would you like to set a team name now?')) {
if (setTeamName()) {
break;
};
}
if (!localStorage['strife-drafter:team_name']) {
return;
}
}
cosmo.sendMessage(game_id, {
'type': 'sideclick',
'side': sidename,
'team_name': localStorage['strife-drafter:team_name'],
});
};
var onHeroClick = function(heroname) {
if (!everyoneSeated()) {
alert('Everyone must be seated before picks & bans begin.');
return;
}
cosmo.sendMessage(game_id, {
'type': 'heroclick',
'hero': heroname,
});
};
var sideBySender = function(sender) {
for (var side in sides) {
if (sides[side].sender == sender) {
return side;
}
}
return null;
};
var everyoneSeated = function(sender) {
for (var side in sides) {
if (!sides[side].team_name) {
continue;
}
if (!sides[side].sender) {
return false;
}
}
return true;
};
var setGameTitle = function() {
var title = document.getElementById('game-title');
var text = sides['glory'].team_name + ' vs. ' + sides['valor'].team_name;
title.textContent = text;
};
var onMessage = function(msg) {
var app_msg = msg.message;
var sender_side = sideBySender(msg.sender);
if (steps[next_step].seconds) {
var server_seconds = msg.created - server_last_step_time;
var allowed_seconds = steps[next_step].seconds;
if (steps[next_step].side) {
allowed_seconds += sides[steps[next_step].side].extra_seconds;
}
if (server_seconds > allowed_seconds) {
// Server timestamps rule, and this message is too late.
nextStep(msg);
}
}
switch (app_msg.type) {
case 'sideclick':
if (!(app_msg.side in sides) || !sides[app_msg.side].team_name) {
console.log('sideclick invalid side:', msg);
return;
}
if (sides[app_msg.side].sender) {
console.log('sideclick duplicate side:', msg);
return;
}
if (sender_side) {
console.log('sideclick multiple sides per sender:', msg);
return;
}
sides[app_msg.side].sender = msg.sender;
sides[app_msg.side].team_name = app_msg.team_name;
setGameTitle();
if (everyoneSeated()) {
nextStep(msg);
}
break;
case 'heroclick':
if (!everyoneSeated()) {
console.log('heroclick before everyone seated:', msg);
return;
}
if (!sender_side) {
console.log('heroclick from unknown source:', msg);
return;
}
if (!steps[next_step]) {
console.log('heroclick on invalid step:', msg);
return;
}
if (steps[next_step].side != sender_side) {
console.log('heroclick from wrong side:', msg);
return;
}
if (HEROES.indexOf(app_msg.hero) == -1) {
console.log('heroclick for unknown hero:', msg);
return;
}
if (unavailableHeroes().indexOf(app_msg.hero) != -1) {
console.log('heroclick for unavailable hero:', msg);
return;
}
addHero(app_msg.hero);
if (!steps[next_step].slots_remaining) {
nextStep(msg);
}
break;
case 'timeout':
// Work already done above.
break;
default:
console.log('Unknown message type:', app_msg);
break;
}
};
var addHero = function(hero_name) {
var hero = buildHero(hero_name);
var step = steps[next_step];
var side = sides[step.side];
if (step.type == 'ban') {
side.bans.push(hero_name);
} else if (step.type == 'pick') {
side.picks.push(hero_name);
}
var container_index = step.slots - step.slots_remaining;
step.containers[container_index].appendChild(hero);
step.slots_remaining--;
updateHeroes();
};
var nextStep = function(msg) {
var old_step = steps[next_step];
switch (old_step.type) {
case 'seating':
if (!sideBySender(cosmo.currentProfile())) {
document.body.className = 'observer';
}
break;
case 'countdown':
server_last_step_time += old_step.seconds;
client_last_step_time = getTime();
document.getElementById('countdown').className = null;
break;
case 'pick':
case 'ban':
for (var i = 0; i < old_step.containers.length; i++) {
var container = old_step.containers[i];
container.className = container.className.split(' ')[0];
}
// Measure real time impact from server timestamps.
var server_seconds = msg.created - server_last_step_time;
server_seconds -= old_step.seconds;
if (server_seconds > 0) {
sides[old_step.side].extra_seconds -= server_seconds;
if (sides[old_step.side].extra_seconds < 0) {
sides[old_step.side].extra_seconds = 0;
var random_value = msg.random_value;
while (old_step.slots_remaining) {
var available = availableHeroes();
var hero = available[random_value % available.length];
console.log('Random hero choice:', hero);
addHero(hero);
random_value >>>= 8;
}
}
}
updateTimers();
break;
}
next_step++;
var new_step = steps[next_step];
switch (new_step.type) {
case 'countdown':
document.getElementById('countdown').className = 'active';
break;
case 'pick':
case 'ban':
new_step.slots_remaining = new_step.slots;
for (var i = 0; i < new_step.containers.length; i++) {
new_step.containers[i].className += ' next-step';
}
break;
case 'end':
document.getElementById('slide').className = 'active';
break;
}
if (msg) {
server_last_step_time = msg.created;
client_last_step_time = getTime();
}
};
var unavailableHeroes = function() {
var ret = [];
for (var side in sides) {
ret.push.apply(ret, sides[side].bans);
ret.push.apply(ret, sides[side].picks);
}
return ret;
};
var availableHeroes = function() {
var unavailable = unavailableHeroes();
var ret = [];
for (var i = 0; i < HEROES.length; i++) {
var hero = HEROES[i];
if (unavailable.indexOf(hero) == -1) {
ret.push(hero);
}
}
return ret;
};
var updateHeroes = function() {
var unavailable = unavailableHeroes();
for (var i = 0; i < unavailable.length; i++) {
var hero = unavailable[i];
var container = document.getElementById('hero-' + hero);
container.className = 'hero-overlay hero-unavailable';
}
};
var updateTimers = function() {
for (var side in sides) {
if (!sides[side].team_name) {
continue;
}
sides[side].extra_seconds_cont.textContent = sides[side].extra_seconds;
for (var i = next_step; i < steps.length; i++) {
var step = steps[i];
if (step.side == side) {
sides[step.side].step_seconds_cont.textContent = step.seconds;
break;
}
}
}
};
var populateSides = function() {
for (var side in sides) {
if (!sides[side].team_name) {
continue;
}
sides[side].container = document.getElementById(side + '-cont');
document.getElementById(side).addEventListener('click',
onSideClick.bind(null, side));
sides[side].extra_seconds_cont = document.getElementById(side + '-extra');
sides[side].step_seconds_cont = document.getElementById(side + '-step');
}
for (var i = 0; i < steps.length; i++) {
var step = steps[i];
switch (step.type) {
case 'pick':
case 'ban':
step.containers = [];
for (var j = 0; j < step.slots; j++) {
var div = document.createElement('div');
if (step.type == 'ban') {
div.className = 'heroban-cont';
} else if (step.type == 'pick') {
div.className = 'hero-cont';
}
sides[step.side].container.appendChild(div);
step.containers.push(div);
}
break;
}
}
};
var buildHero = function(hero) {
var container = document.createElement('div');
container.className = 'hero-overlay';
var img = document.createElement('img');
img.src = 'static/heroes/' + hero.toLowerCase().replace(' ', '') + '.png';
img.className = 'hero-medium';
var text = document.createElement('div');
text.className = 'text-overlay';
text.appendChild(document.createTextNode(hero));
container.appendChild(img);
container.appendChild(text);
return container;
};
var tick = function() {
var step = steps[next_step];
if (!step.seconds) {
// Not a lot for a timer to do.
return;
}
var side = step.side;
var client_allowed_seconds = steps[next_step].seconds;
if (side) {
client_allowed_seconds += sides[step.side].extra_seconds;
}
var client_actual_seconds = getTime() - client_last_step_time;
var seconds_left = client_allowed_seconds - client_actual_seconds;
if (seconds_left < 0) {
cosmo.sendMessage(game_id, {
'type': 'timeout',
});
return;
}
switch (step.type) {
case 'countdown':
document.getElementById('countdown').textContent = seconds_left;
break;
case 'pick':
case 'ban':
var step_seconds = step.seconds;
var extra_seconds = sides[step.side].extra_seconds;
step_seconds -= client_actual_seconds;
if (step_seconds >= 0) {
sides[side].step_seconds_cont.textContent = step_seconds;
} else {
extra_seconds += step_seconds;
sides[side].extra_seconds_cont.textContent = extra_seconds;
}
break;
}
};
document.addEventListener('DOMContentLoaded', function() {
// Instantiate cosmo instance.
var callbacks = {
'onMessage': onMessage,
};
cosmo = new Cosmopolite(callbacks, null, 'strife-drafter');
// Determine or generate game ID
if (!window.location.hash) {
var binary_id = [];
for (var i = 0; i < 9; i++) {
binary_id.push(String.fromCharCode(Math.random() * 256));
}
window.location.hash =
btoa(binary_id.join('')).replace('+', '-').replace('/', '_');
}
game_id = window.location.hash.slice(1);
window.addEventListener('hashchange', function() {
window.location.reload();
});
// Prompt for team name if necessary.
if (localStorage['strife-drafter:team_name'] == undefined) {
setTeamName();
}
// Start pulling event stream.
cosmo.subscribe(game_id, -1);
// Add hero objects and callbacks.
var heroes_container = document.getElementById('heroes');
for (var i = 0; i < HEROES.length; i++) {
var hero = HEROES[i];
var container = buildHero(hero);
container.id = 'hero-' + hero;
container.addEventListener('click', onHeroClick.bind(null, hero));
heroes_container.appendChild(container);
}
updateHeroes();
populateSides();
setGameTitle();
updateTimers();
setInterval(tick, 250);
});

3466
static/fctv.svg Normal file

File diff suppressed because it is too large Load Diff

After

Width:  |  Height:  |  Size: 309 KiB

BIN
static/heroes/ace.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

BIN
static/heroes/bastion.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

BIN
static/heroes/blazer.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

BIN
static/heroes/bo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

BIN
static/heroes/caprice.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

BIN
static/heroes/carter.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

BIN
static/heroes/claudessa.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

BIN
static/heroes/gokong.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

BIN
static/heroes/hale.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

BIN
static/heroes/harrower.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

BIN
static/heroes/jinshe.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

BIN
static/heroes/malady.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

BIN
static/heroes/minerva.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

BIN
static/heroes/moxie.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

BIN
static/heroes/nikolai.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

BIN
static/heroes/ray.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

BIN
static/heroes/rook.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

BIN
static/heroes/shank.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

BIN
static/heroes/trixie.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

BIN
static/heroes/vex.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

227
static/style.css Normal file
View File

@@ -0,0 +1,227 @@
@-webkit-keyframes flipIn {
0% {
transform: rotateY(180deg);
}
100% {
transform: rotateY(0);
}
}
.observer {
cursor: none;
}
.observer > * {
pointer-events: none;
}
div.observer {
pointer-events: none;
}
button {
display: inline-block;
padding: 6px 12px;
margin-bottom: 0;
font-family: Oswald, sans-serif;
font-size: 14px;
text-align: center;
white-space: nowrap;
vertical-align: middle;
cursor: pointer;
border: 1px solid transparent;
border-radius: 4px;
color: #fff;
background-color: #337ab7;
border-color: #2e6da4;
}
button:hover {
color: #fff;
background-color: #286090;
border-color: #204d74;
}
button:focus {
outline: 0;
}
.hero-medium {
width: 96px;
height: 96px;
margin-top: 2px;
border-radius: 10px;
}
.hero-overlay {
display: inline-block;
position: relative;
width: 96px;
height: 96px;
margin: 5px;
cursor: pointer;
transition: opacity 0.25s;
}
.hero-overlay:hover {
opacity: 0.7;
}
.hero-unavailable {
transition: transform 0.5s;
transform: rotateY(180deg);
backface-visibility: hidden;
cursor: default;
}
.hero-unavailable:hover {
opacity: 0.3;
}
.hero-cont {
background-color: grey;
width: 100px;
height: 100px;
margin-top: 5px;
margin-bottom: 5px;
border-radius: 10px;
transition: box-shadow 0.5s;
}
.heroban-cont {
background: url("ban.png") no-repeat 0 0, grey;
background-position: 0px 0px;
background-size: 100% 100%;
width: 100px;
height: 100px;
margin-top: 5px;
margin-bottom: 5px;
border-radius: 10px;
transition: box-shadow 0.5s;
}
.hero-cont .hero-overlay {
margin: 0;
cursor: default;
-webkit-animation-name: flipIn;
-webkit-animation-iteration-count: once;
-webkit-animation-timing-function: ease-in-out;
-webkit-animation-duration: 0.5s;
backface-visibility: hidden;
}
.hero-cont .hero-overlay:hover {
opacity: 1.0;
}
.heroban-cont .hero-overlay {
margin: 0;
opacity: 0.3;
cursor: default;
-webkit-animation-name: flipIn;
-webkit-animation-iteration-count: once;
-webkit-animation-timing-function: ease-in-out;
-webkit-animation-duration: 0.5s;
backface-visibility: hidden;
}
.heroban-cont .hero-overlay:hover {
opacity: 0.3;
}
.next-step {
box-shadow: 0 0 10px red;
}
.text-overlay {
position: absolute;
bottom: 0px;
left: 0px;
width: 100%;
text-align: center;
color: #fff;
font-family: Oswald, sans-serif;
font-size: 15px;
text-shadow: 0 0 1px black;
}
.container {
display: flex;
justify-content: center;
}
.team-column {
flex-shrink: 0;
text-align: center;
}
.hero-column {
text-align: center;
margin: 10px;
max-width: 800px;
}
.title-container {
display: flex;
}
.timer {
font-family: Audiowide, sans-serif;
min-width: 5em;
}
#game-title {
font-family: Oswald, sans-serif;
font-size: 20px;
flex-grow: 1;
}
#heroes {
position: relative;
}
#countdown {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
z-index: -1;
font-size: 200px;
line-height: 200%;
color: black;
text-shadow: 0 0 15px white;
background-color: rgba(255, 255, 255, 0.5);
cursor: default;
opacity: 0.0;
transition: opacity 0.5s, z-index 0.5s;
font-family: Audiowide, sans-serif;
}
#countdown.active {
z-index: 100;
opacity: 1.0;
}
#slide {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
z-index: -1;
font-size: 200px;
line-height: 200%;
color: black;
cursor: default;
opacity: 0.0;
transition: opacity 2s, z-index 2s;
font-family: Oswald, sans-serif;
background: white url("fctv.svg") no-repeat top;
background-size: 100% 100%;
}
#slide.active {
z-index: 100;
opacity: 1.0;
}