Lots of stuff

This commit is contained in:
Deukhoofd 2016-02-01 23:19:30 +01:00
commit d7316d5799
6681 changed files with 527969 additions and 0 deletions

3
.gitignore vendored Normal file
View File

@ -0,0 +1,3 @@
/server/config.coffee
/node_modules/
/knexfile.coffee

17
.nodemonignore Normal file
View File

@ -0,0 +1,17 @@
# Generated by grunt-nodemon
.DS_Store
.git/
pokebattle-db
test/
scrapers/*
client/*
public/*
Gruntfile*
package.json
*.md
*.txt
Capfile
config/*
Gemfile
Gemfile.lock
dump.rdb

2
Capfile Normal file
View File

@ -0,0 +1,2 @@
load 'deploy'
load 'config/deploy' # remove this line to skip loading any of the default tasks

3
Gemfile Normal file
View File

@ -0,0 +1,3 @@
source 'https://rubygems.org'
gem "capistrano", '~>2.0'
gem "capistrano-node-deploy", :git => 'https://github.com/loopj/capistrano-node-deploy.git', :ref => '1c2279'

35
Gemfile.lock Normal file
View File

@ -0,0 +1,35 @@
GIT
remote: https://github.com/loopj/capistrano-node-deploy.git
revision: 1c22792d9163591ba4e941887598aed654190a4c
ref: 1c2279
specs:
capistrano-node-deploy (1.2.14)
multi_json (~> 1.3.6)
railsless-deploy (>= 1.1.0)
GEM
remote: https://rubygems.org/
specs:
capistrano (2.15.5)
highline
net-scp (>= 1.0.0)
net-sftp (>= 2.0.0)
net-ssh (>= 2.0.14)
net-ssh-gateway (>= 1.1.0)
highline (1.6.20)
multi_json (1.3.7)
net-scp (1.1.2)
net-ssh (>= 2.6.5)
net-sftp (2.1.2)
net-ssh (>= 2.6.5)
net-ssh (2.7.0)
net-ssh-gateway (1.2.0)
net-ssh (>= 2.6.5)
railsless-deploy (1.1.3)
PLATFORMS
ruby
DEPENDENCIES
capistrano (~> 2.0)
capistrano-node-deploy!

221
Gruntfile.coffee Normal file
View File

@ -0,0 +1,221 @@
{exec} = require('child_process')
path = require('path')
assets = require('./assets')
# asset paths (note: without public/ in front)
assetPaths = '''
js/data.js
js/vendor.js
js/templates.js
js/replays.js
js/app.js
css/main.css
css/vendor.css
'''.trim().split(/\s+/)
# Transform them using proper slashes
assetPaths = assetPaths.map (assetPath) -> assetPath.split('/').join(path.sep)
module.exports = (grunt) ->
awsConfigPath = 'aws_config.json'
if !grunt.file.exists(awsConfigPath)
grunt.file.copy("#{awsConfigPath}.example", awsConfigPath)
grunt.initConfig
pkg: grunt.file.readJSON('package.json')
concurrent:
compile: ["jade", "stylus", "coffee", "concat", "cssmin", "compile:json"]
server:
tasks: ["nodemon", "watch"]
options:
logConcurrentOutput: true
jade:
compile:
options:
client: true
compileDebug: false
processName: (fileName) ->
templatePath = 'client/views/'
index = fileName.lastIndexOf(templatePath) + templatePath.length
fileName = fileName.substr(index)
fileName.substr(0, fileName.indexOf('.'))
files:
"public/js/templates.js": "client/views/**/*.jade"
stylus:
compile:
use: [ require('nib') ]
files:
"public/css/main.css": "client/app/css/main.styl"
coffee:
compile:
files:
'public/js/app.js': [
"client/app/js/initializers/index.coffee"
"client/app/js/initializers/**/*.coffee"
"shared/**/*.coffee"
"client/app/js/mixins/index.coffee"
"client/app/js/mixins/**/*.coffee"
"client/app/js/models/battles/pokemon.coffee"
"client/app/js/models/battles/team.coffee"
"client/app/js/models/battles/**/*.coffee"
"client/app/js/models/chats/**/*.coffee"
"client/app/js/collections/battles/**/*.coffee"
"client/app/js/collections/chats/**/*.coffee"
"client/app/js/views/battles/**/*.coffee"
"client/app/js/views/teambuilder/**/*.coffee"
"client/app/js/views/*.coffee"
"client/app/js/client.coffee"
"client/app/js/helpers/**/*.coffee"
"client/app/js/concerns/**/*.coffee"
]
# The replay scripts are typically scoped to a battles/ folder
'public/js/replays.js': [
"client/app/js/initializers/index.coffee"
"client/app/js/initializers/**/*.coffee"
"shared/**/*.coffee"
"client/app/js/mixins/index.coffee"
"client/app/js/mixins/battles/**/*.coffee"
"client/app/js/models/battles/pokemon.coffee"
"client/app/js/models/battles/team.coffee"
"client/app/js/models/battles/**/*.coffee"
"client/app/js/models/replays/**/*.coffee"
"client/app/js/collections/replays/**/*.coffee"
"client/app/js/views/battles/**/*.coffee"
"client/app/js/views/replays/**/*.coffee"
"client/app/js/helpers/**/*.coffee"
]
uglify:
options:
compress: true
warn: false
vendor:
files:
'public/js/vendor.js': 'public/js/vendor.js'
coffee:
files:
'public/js/app.js': 'public/js/app.js'
jade:
files:
"public/js/templates.js": "public/js/templates.js"
json:
files:
'public/js/data.js': 'public/js/data.js'
cssmin:
combine:
files:
'public/css/vendor.css' : [
'client/vendor/css/**/*.css'
]
concat:
dist:
dest: 'public/js/vendor.js'
src: [
"client/vendor/js/jquery.js"
"client/vendor/js/underscore.js"
"client/vendor/js/backbone.js"
"client/vendor/js/*.js"
]
external_daemon:
cmd: "redis-server"
exec:
capistrano:
cmd: 'bundle && bundle exec cap deploy'
scrape:
cmd: ". ./venv/bin/activate && cd ./scrapers/bw && python pokemon.py"
watch:
templates:
files: ['client/views/**/*.jade']
tasks: 'jade'
css:
files: ['client/**/*.styl']
tasks: 'stylus'
js:
files: ['client/app/**/*.coffee', 'shared/**/*.coffee']
tasks: 'coffee'
vendor:
files: ['client/vendor/js/**/*.js']
tasks: 'concat'
vendor_css:
files: ['client/vendor/css/**/*.css']
tasks: 'cssmin'
json:
files: [
'**/*.json'
'!**/node_modules/**'
'server/generations'
'server/commands'
]
tasks: 'compile:json'
nodemon:
development:
options:
file: "start.js"
ignoredFiles: [
'.DS_Store'
'.git/'
'pokebattle-db'
'test/'
'scrapers/*'
'client/*'
'public/*'
'Gruntfile*'
'package.json'
'*.md'
'*.txt'
'Capfile'
'config/*'
'Gemfile'
'Gemfile.lock'
'dump.rdb'
]
aws: grunt.file.readJSON(awsConfigPath)
s3:
options:
accessKeyId: "<%= aws.accessKeyId %>"
secretAccessKey: "<%= aws.secretAccessKey %>"
bucket: "s3.pokebattle.com"
region: 'us-west-2'
build:
cwd: "public/"
expand: true
src: assetPaths
dest: assets.S3_ASSET_PREFIX
rename: (dest, src) ->
assets.get(src)
grunt.loadNpmTasks('grunt-contrib-jade')
grunt.loadNpmTasks('grunt-contrib-stylus')
grunt.loadNpmTasks('grunt-contrib-coffee')
grunt.loadNpmTasks('grunt-contrib-concat')
grunt.loadNpmTasks('grunt-contrib-cssmin')
grunt.loadNpmTasks('grunt-contrib-watch')
grunt.loadNpmTasks('grunt-contrib-uglify')
grunt.loadNpmTasks('grunt-nodemon')
grunt.loadNpmTasks('grunt-concurrent')
grunt.loadNpmTasks('grunt-external-daemon')
grunt.loadNpmTasks('grunt-aws')
grunt.loadNpmTasks('grunt-exec')
grunt.registerTask('compile', ['concurrent:compile', 'uglify'])
grunt.registerTask('heroku:production', 'compile')
grunt.registerTask('heroku:development', 'compile')
grunt.registerTask('default', ['concurrent:compile', 'concurrent:server'])
grunt.registerTask('scrape:pokemon', 'exec:scrape')
grunt.registerTask 'compile:json', 'Compile all data JSON into one file', ->
{GenerationJSON} = require './server/generations'
EventPokemon = require './shared/event_pokemon.json'
{HelpDescriptions} = require './server/commands'
contents = """var Generations = #{JSON.stringify(GenerationJSON)},
EventPokemon = #{JSON.stringify(EventPokemon)},
HelpDescriptions = #{JSON.stringify(HelpDescriptions)};"""
grunt.file.write('./public/js/data.js', contents)
grunt.registerTask 'deploy:assets', 'Compiles and uploads all assets', ->
grunt.task.run(['compile', 's3:build'])
grunt.registerTask('deploy:server', 'exec:capistrano')
grunt.registerTask('deploy', ['deploy:assets', 'deploy:server'])

7
LICENSE Normal file
View File

@ -0,0 +1,7 @@
Copyright (c) 2012 David Peter
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

18
Makefile Normal file
View File

@ -0,0 +1,18 @@
HTMLDOCS = $(DOCS:.md=.html)
REPORTER = spec
test:
NODE_ENV=test ./node_modules/.bin/mocha \
--reporter $(REPORTER) --require should --compilers coffee:coffee-script/register
test-cov: lib-cov
NUGGETBRIDGE_COV=1 $(MAKE) test REPORTER=html-cov > coverage.html
lib-cov:
rm -rf server-cov
coffee -c server
jscoverage server server-cov
rm server/*.js
.PHONY: test

115
README.md Normal file
View File

@ -0,0 +1,115 @@
# pokebattle-sim [![Build Status](https://secure.travis-ci.org/sarenji/pokebattle-sim.png?branch=master)](http://travis-ci.org/sarenji/pokebattle-sim)
A competitive Pokemon battle simulator playable in the browser.
## Set up
### Installation
```bash
git clone git://github.com/sarenji/pokebattle-sim.git
cd pokebattle-sim
npm install
```
Next, you need to install two dependencies: redis and PostgreSQL 9.1.
### Redis
On Mac OS X with homebrew, you can do:
```bash
brew install redis
```
On Windows, there is a Redis port that works fairly well: https://github.com/rgl/redis/downloads
### PostgreSQL
PostgreSQL has installable versions for every major OS. In particular, for Mac OS X, there is Postgres.app.
When you install PostgreSQL, you should create a database for pokebattle, called `pokebattle_sim`. You can do this two ways:
```bash
# command-line:
$ createdb pokebattle_sim
# or via SQL client:
CREATE DATABASE pokebattle_sim;
```
Next, you must migrate the database. Simply run:
```bash
npm install -g knex
knex migrate:latest
```
If you get an error complaining that the `postgres` role doesn't exist, run this: `createuser -s -r postgres`.
## Run server
We use [Grunt](http://gruntjs.com/) to handle our development. First, you must `npm install -g grunt-cli` to get the grunt runner. Then you can type
```bash
grunt
```
to automatically compile all client-side files and run `nodemon` for you.
We also [support Vagrant](https://github.com/sarenji/pokebattle-sim/wiki/Running-via-Vagrant) if you are on a Windows machine and so desire.
## Run tests
```bash
npm test
# or
npm install -g mocha
mocha
```
Or if you're in the Vagrant VM, you can just run
```bash
mocha
```
## Deployment
First, you must get SSH access to the server. Then, to deploy:
```bash
cap staging deploy
# test on staging
cap production deploy
```
## Guide
pokebattle-sim is a one-page app. The server serves the client.
```
api/ Hosts the code for the API that we host.
client/ Main client code. Contains JS and CSS.
config/ For Capistrano and deployment.
public/ Public-facing dir. Generated files, fonts, images.
server/ Server, battle, move, Pokemon logic, etc.
shared/ Files shared between server and client.
test/ Automated tests for server and client.
views/ All views that are rendered server-side go here.
Gruntfile.coffee Contains all tasks for pokebattle-sim, like compiling.
start.js The main entry point of pokebattle-sim.
```
## Contributing
All contributions to the simulator logic must come with tests. If a
contribution does not come with a test that fails before your contribution and
passes after, your contribution will be rejected.
Other contributions (e.g. to the client) are much less strict!
## Issues
Report issues in GitHub's [issue
tracker](https://github.com/sarenji/pokebattle-sim/issues).

120
Vagrantfile vendored Normal file
View File

@ -0,0 +1,120 @@
# -*- mode: ruby -*-
# vi: set ft=ruby :
# Vagrantfile API/syntax version. Don't touch unless you know what you're doing!
VAGRANTFILE_API_VERSION = "2"
Vagrant.configure(VAGRANTFILE_API_VERSION) do |config|
# All Vagrant configuration is done here. The most common configuration
# options are documented and commented below. For a complete reference,
# please see the online documentation at vagrantup.com.
# Every Vagrant virtual environment requires a box to build off of.
config.vm.box = "precise32"
# The url from where the 'config.vm.box' box will be fetched if it
# doesn't already exist on the user's system.
config.vm.box_url = "http://files.vagrantup.com/precise32.box"
config.vm.provision :shell, :path => "bootstrap.sh"
# Create a forwarded port mapping which allows access to a specific port
# within the machine from a port on the host machine. In the example below,
# accessing "localhost:8080" will access port 80 on the guest machine.
config.vm.network :forwarded_port, guest: 8000, host: 8000
# Create a private network, which allows host-only access to the machine
# using a specific IP.
# config.vm.network :private_network, ip: "192.168.33.10"
# Create a public network, which generally matched to bridged network.
# Bridged networks make the machine appear as another physical device on
# your network.
# config.vm.network :public_network
# If true, then any SSH connections made will enable agent forwarding.
# Default value: false
# config.ssh.forward_agent = true
# Share an additional folder to the guest VM. The first argument is
# the path on the host to the actual folder. The second argument is
# the path on the guest to mount the folder. And the optional third
# argument is a set of non-required options.
# config.vm.synced_folder "../data", "/vagrant_data"
# Provider-specific configuration so you can fine-tune various
# backing providers for Vagrant. These expose provider-specific options.
# Example for VirtualBox:
#
# config.vm.provider :virtualbox do |vb|
# # Don't boot with headless mode
# vb.gui = true
#
# # Use VBoxManage to customize the VM. For example to change memory:
# vb.customize ["modifyvm", :id, "--memory", "1024"]
# end
#
# View the documentation for the provider you're using for more
# information on available options.
# Enable provisioning with Puppet stand alone. Puppet manifests
# are contained in a directory path relative to this Vagrantfile.
# You will need to create the manifests directory and a manifest in
# the file precise32.pp in the manifests_path directory.
#
# An example Puppet manifest to provision the message of the day:
#
# # group { "puppet":
# # ensure => "present",
# # }
# #
# # File { owner => 0, group => 0, mode => 0644 }
# #
# # file { '/etc/motd':
# # content => "Welcome to your Vagrant-built virtual machine!
# # Managed by Puppet.\n"
# # }
#
# config.vm.provision :puppet do |puppet|
# puppet.manifests_path = "manifests"
# puppet.manifest_file = "init.pp"
# end
# Enable provisioning with chef solo, specifying a cookbooks path, roles
# path, and data_bags path (all relative to this Vagrantfile), and adding
# some recipes and/or roles.
#
# config.vm.provision :chef_solo do |chef|
# chef.cookbooks_path = "../my-recipes/cookbooks"
# chef.roles_path = "../my-recipes/roles"
# chef.data_bags_path = "../my-recipes/data_bags"
# chef.add_recipe "mysql"
# chef.add_role "web"
#
# # You may also specify custom JSON attributes:
# chef.json = { :mysql_password => "foo" }
# end
# Enable provisioning with chef server, specifying the chef server URL,
# and the path to the validation key (relative to this Vagrantfile).
#
# The Opscode Platform uses HTTPS. Substitute your organization for
# ORGNAME in the URL and validation key.
#
# If you have your own Chef Server, use the appropriate URL, which may be
# HTTP instead of HTTPS depending on your configuration. Also change the
# validation key to validation.pem.
#
# config.vm.provision :chef_client do |chef|
# chef.chef_server_url = "https://api.opscode.com/organizations/ORGNAME"
# chef.validation_key_path = "ORGNAME-validator.pem"
# end
#
# If you're using the Opscode platform, your validator client is
# ORGNAME-validator, replacing ORGNAME with your organization name.
#
# If you have your own Chef Server, the default validation client name is
# chef-validator, unless you changed the configuration.
#
# chef.validation_client_name = "ORGNAME-validator"
end

179
api/index.coffee Normal file
View File

@ -0,0 +1,179 @@
restify = require('restify')
generations = require('../server/generations')
learnsets = require('../shared/learnsets')
{makeBiasedRng} = require("../shared/bias_rng")
GenerationJSON = generations.GenerationJSON
getName = (name) ->
require('./name_map.json')[name]
slugify = (str) ->
str.toLowerCase().replace(/[^a-z0-9]/g, '-').replace(/\-{2,}/g, '-')
slugifyArray = (array) ->
hash = {}
for element in array
hash[slugify(element)] = element
hash
attachAPIEndpoints = (server) ->
for gen in generations.ALL_GENERATIONS
do (gen) ->
json = GenerationJSON[gen.toUpperCase()]
GenMoves = slugifyArray(json.MoveList)
GenAbilities = slugifyArray(json.AbilityList)
GenTypes = slugifyArray(json.TypeList)
try
# Preload Battle
{Battle} = require("../server/#{gen}/battle")
catch
# TODO: There is no Battle object for this gen
intGeneration = generations.GENERATION_TO_INT[gen]
server.get "#{gen}/moves", (req, res, next) ->
res.send(json.MoveData)
return next()
server.get "#{gen}/pokemon", (req, res, next) ->
res.send(json.FormeData)
return next()
server.get "#{gen}/pokemon/:species", (req, res, next) ->
species = getName(req.params.species)
return next(new restify.ResourceNotFoundError("Could not find Pokemon: #{req.params.species}")) if !species
pokemon = json.FormeData[species]
res.send(pokemon)
return next()
server.get "#{gen}/items", (req, res, next) ->
res.send(items: json.ItemList)
return next()
server.get "#{gen}/moves", (req, res, next) ->
res.send(moves: json.MoveList)
return next()
server.get "#{gen}/moves/:name", (req, res, next) ->
move = GenMoves[req.params.name]
return next(new restify.ResourceNotFoundError("Could not find Move: #{req.params.name}")) if !move
res.send(pokemon: json.MoveMap[move])
return next()
server.get "#{gen}/abilities", (req, res, next) ->
res.send(abilities: json.AbilityList)
return next()
server.get "#{gen}/abilities/:name", (req, res, next) ->
ability = GenAbilities[req.params.name]
return next(new restify.ResourceNotFoundError("Could not find Ability: #{req.params.name}")) if !ability
res.send(pokemon: json.AbilityMap[ability])
return next()
server.get "#{gen}/types", (req, res, next) ->
res.send(types: json.TypeList)
return next()
server.get "#{gen}/types/:name", (req, res, next) ->
type = GenTypes[req.params.name]
return next(new restify.ResourceNotFoundError("Could not find Type: #{req.params.name}")) if !type
res.send(pokemon: json.TypeMap[type])
return next()
server.get "#{gen}/pokemon/:species/moves", (req, res, next) ->
species = getName(req.params.species)
pokemon = {species: species}
moves = learnsets.learnableMoves(GenerationJSON, pokemon, intGeneration)
return next(new restify.ResourceNotFoundError("Could not find moves for Pokemon: #{req.params.species}")) if !moves || moves.length == 0
res.send(moves: moves)
return next()
server.get "#{gen}/pokemon/:species/:forme/moves", (req, res, next) ->
species = getName(req.params.species)
pokemon = {species: species, forme: req.params.forme}
moves = learnsets.learnableMoves(GenerationJSON, pokemon, intGeneration)
return next(new restify.ResourceNotFoundError("Could not find moves for Pokemon: #{req.params.species}")) if !moves || moves.length == 0
res.send(moves: moves)
return next()
checkMoveset = (req, res, next) ->
species = getName(req.params.species)
return next(new restify.ResourceNotFoundError("Could not find Pokemon: #{req.params.species}")) if !species
pokemon = {species: species}
pokemon.forme = req.params.forme if req.params.forme
moveset = req.query.moves?.split(/,/) || []
valid = learnsets.checkMoveset(GenerationJSON, pokemon, intGeneration, moveset)
errors = []
errors.push("Invalid moveset") if !valid
res.send(errors: errors)
return next()
server.get "#{gen}/pokemon/:species/check", checkMoveset
server.get "#{gen}/pokemon/:species/:forme/check", checkMoveset
server.put "#{gen}/damagecalc", (req, res, next) ->
# todo: catch any invalid data.
moveName = req.params.move
attacker = req.params.attacker
defender = req.params.defender
players = [
{id: "0", name: "0", team: [attacker]}
{id: "1", name: "1", team: [defender]}
]
battle = new Battle('id', players, numActive: 1)
move = battle.getMove(moveName)
if not move
return next(new restify.BadRequest("Invalid move #{moveName}"))
battle.begin()
attackerPokemon = battle.getTeam("0").at(0)
defenderPokemon = battle.getTeam("1").at(0)
# bias the RNG to remove randmomness like critical hits
makeBiasedRng(battle)
battle.rng.bias("next", "ch", 1)
battle.rng.bias("randInt", "miss", 0)
battle.rng.bias("next", "secondary effect", 0)
battle.rng.bias("randInt", "flinch", 100)
# calculate min damage
battle.rng.bias("randInt", "damage roll", 15)
minDamage = move.calculateDamage(battle, attackerPokemon, defenderPokemon)
# calculate max damage
battle.rng.bias("randInt", "damage roll", 0)
maxDamage = move.calculateDamage(battle, attackerPokemon, defenderPokemon)
# TODO: Add remaining HP or anything else that's requested
res.send(
moveType: move.getType(battle, attackerPokemon, defenderPokemon)
basePower: move.basePower(battle, attackerPokemon, defenderPokemon)
minDamage: minDamage
maxDamage: maxDamage
defenderMaxHP: defenderPokemon.stat('hp')
)
return next()
@createServer = (port, done) ->
server = restify.createServer
name: 'pokebattle-api'
version: '0.0.0'
server.pre(restify.pre.sanitizePath())
server.use(restify.acceptParser(server.acceptable))
server.use(restify.queryParser())
server.use(restify.bodyParser())
server.use(restify.gzipResponse())
server.use (req, res, next) ->
res.charSet('utf8')
return next()
attachAPIEndpoints(server)
server.listen port, ->
console.log('%s listening at %s', server.name, server.url)
done?()
server

1
api/name_map.json Normal file

File diff suppressed because one or more lines are too long

48
assets.coffee Normal file
View File

@ -0,0 +1,48 @@
crypto = require 'crypto'
path = require 'path'
fs = require 'fs'
config = require('./server/config')
cachedVersion = null
cachedAssetHash = null
cachedFingerprints = {}
S3_ASSET_PREFIX = 'sim/'
get = (src, options = {}) ->
return src if config.IS_LOCAL
hash = options.fingerprint || getFingerprint(src)
extName = path.extname(src)
"#{S3_ASSET_PREFIX}#{src[0...-extName.length]}-#{hash}#{extName}"
getFingerprint = (src) ->
return cachedFingerprints[src] if cachedFingerprints[src]
contents = fs.readFileSync("public/#{src}")
fingerprint = crypto.createHash('md5').update(contents).digest('hex')
cachedFingerprints[src] = fingerprint
fingerprint
getAbsolute = (src, options = {}) ->
prefix = (if config.IS_LOCAL then "" else "//media.pokebattle.com")
"#{prefix}/#{get(src, options)}"
# Returns a MD5 hash representing the version of the assets.
getVersion = ->
return cachedVersion if cachedVersion
hash = crypto.createHash('md5')
for jsPath in fs.readdirSync('public/js')
hash = hash.update(fs.readFileSync("public/js/#{jsPath}"))
cachedVersion = hash.digest('hex')
cachedVersion
# Returns a hash of asset hashes, keyed by filename
asHash = ->
return cachedAssetHash if cachedAssetHash
cachedAssetHash = {}
for jsPath in fs.readdirSync('public/js')
jsPath = "js/#{jsPath}"
cachedAssetHash[jsPath] = getFingerprint(jsPath)
cachedAssetHash
module.exports = {S3_ASSET_PREFIX, get, asHash, getAbsolute, getVersion}

6
aws_config.json Normal file
View File

@ -0,0 +1,6 @@
{
"accessKeyId": "akid",
"secretAccessKey": "secret",
"region": "us-west-2",
"sslEnabled": true
}

6
aws_config.json.example Normal file
View File

@ -0,0 +1,6 @@
{
"accessKeyId": "akid",
"secretAccessKey": "secret",
"region": "us-west-2",
"sslEnabled": true
}

21
bootstrap.sh Normal file
View File

@ -0,0 +1,21 @@
#!/usr/bin/env bash
# Set the USER and HOME variables to be the vagrant user. By default, the script is run as root
export USER='vagrant'
export HOME='/home/vagrant'
# Install some necessary packages
sudo apt-get install -y redis-server curl git make g++
# Install NVM. Use source ~/.profile to "restart" the shell
curl https://raw.github.com/creationix/nvm/master/install.sh | sh
source $HOME/.profile
nvm install 0.10
nvm alias default 0.10
npm install -g coffee-script grunt-cli mocha
# Rebuild
cd /vagrant
npm install --no-bin-links

538
client/app/css/battle.styl Normal file
View File

@ -0,0 +1,538 @@
outline-text(clr = #000)
text-shadow 0 1px 0 clr, 0 -1px 0 clr,
-1px 0 0 clr, 1px 0 0 clr
divide(a, b)
a / b
create-projectile(clr)
trans-clr = rgba(black, 0)
background radial-gradient(ellipse at center, lighten(clr, 50%) 0%, clr 50%, trans-clr 51%, trans-clr 100%)
$padding = 10px
$battle-width = 600px
$battle-height = 300px
$pane-width = $battle-width + $padding + $padding
$screen-width = 400px
$side-width = ($battle-width - $screen-width) / 2
.battle_window
.chat
left $pane-width
.battle
position absolute
top 0
left 0
bottom 0
width $pane-width
box-sizing border-box
padding $padding
h2
font-size 0.6875em
text-transform uppercase
letter-spacing 1px
border-bottom 1px solid rgba(0, 0, 0, .10)
line-height normal
.battle_container
position relative
width $battle-width
height $battle-height
narrow-font()
&.battle_bg_0
background-image url("//91.121.152.74:8000/Sprites/battle/backgrounds/battle_bg.png")
&.battle_bg_1
background-image url("//91.121.152.74:8000/Sprites/battle/backgrounds/battle_bg_1.png")
&.battle_bg_2
background-image url("//91.121.152.74:8000/Sprites/battle/backgrounds/battle_bg_2.png")
&.battle_bg_3
background-image url("//91.121.152.74:8000/Sprites/battle/backgrounds/battle_bg_3.png")
&.battle_bg_4
background-image url("//91.121.152.74:8000/Sprites/battle/backgrounds/battle_bg_4.png")
&.battle_bg_5
background-image url("//91.121.152.74:8000/Sprites/battle/backgrounds/battle_bg_5.png")
.battle_user_info
.pokemon_icons
margin 0 auto
width 80px
.icon_wrapper
width 40px
float left
&:nth-child(even)
position relative
top 6px
.fill-left, .fill-right
width $side-width
.left
bottom 20px
left 0
.right
top 20px
right 0
.left, .right
position absolute
width $side-width
color #fff
text-align center
outline-text()
font-size 13px
.icon_wrapper
position relative
&:hover
border-radius 5px
background-color rgba(187, 184, 248, .5)
.pokemon_hp_background
position absolute
right 6px
bottom 2px
width 4px
height 3px
border 1px solid rgba(32, 32, 32, .4)
background rgba(32, 32, 32, .4)
pointer-events none
.pokemon_hp
absolute bottom left
right 0
&.green
background #0f0
&.yellow
background #ff0
&.red
background #f00
.battle-timer
font-size 2em
&.battle-timer-low
color #f33
&.battle-timer-small
font-size 1em
&:before
content: '('
&:after
content: ')'
.battle_overlays
position absolute
top 0
left 0
right 0
bottom 0
pointer-events none
overflow hidden
.battle_overlay
position absolute
top 0
left 0
right 0
bottom 0
&.weather
&.rain
background rgba(0, 48, 196, .3)
&.hail
background rgba(128, 255, 255, .3)
&.sand
background linear-gradient(top, rgba(128, 64, 0, .3), rgba(64, 32, 48, .5))
&.sun
background rgba(255, 196, 64, .3)
.battle_pane
position absolute
width $screen-width
height $battle-height
top 0
left $side-width
.pokemon
&.bottom
.pokemon-info
bottom 35%
left 200px
.sprite
&.fade
position absolute
left -20px
opacity .3
&.top
.pokemon-info
top 20%
right 238px
.sprite
&.fade
position absolute
left 20px
opacity .3
.pokemon-info
position absolute
width 144px
height 6px
border 1px solid #fff
border-radius 2px
box-shadow 0 0 0 1px #000
background rgba(0, 8, 32, .5)
.hp, .hp-gradient
position absolute
top 0
left 0
bottom 0
width 100%
transition width .4s cubic-bezier(0, 1, .5, 1)
.hp-gradient
background linear-gradient(top, transparent 40%, rgba(0,32,64,0.3) 60%)
.hp
background #0f0
.hp-text
position absolute
top -1px
bottom @top
left 100%
background #333
color #fff
font-size 8px
line-height @font-size
padding 0 4px
border-radius 0 2px 2px 0
box-shadow 0 0 0 1px #222
font-family Verdana, sans-serif
.pokemon-meta
position absolute
left 0
bottom 100%
color #fff
pointer-events visible
outline-text()
.pokemon-name
font-weight 700
color #fff
.pokemon-level, .gender
font-size 75%
.pokemon-level-text
color #EFA532
.pokemon-effects
position absolute
top 9px
color #fff
normal-font()
.pokemon-effect
font-size .625em
float left
border-radius 3px
background #333
color #fff
padding 0 4px
border 1px solid rgba(0, 0, 0, .5)
&.sleep
background #535
&.burn
background #c33
&.freeze
background #38b
&.paralyze
background #b93
&.poison, &.toxic
background #b3b
&.boost
background #5a3
&.negative
background #a53
.sprite
position absolute
top 0
left 0
pointer-events visible
transition left 1s ease
img
position absolute
max-width none
.pokemon
position absolute
width 100%
height 100%
top 0
left 0
pointer-events none
overflow hidden
.pokeball
position absolute
background url("//media.pokebattle.com/img/battle/pokeballs.gif") -133px -10px
transition opacity .25s ease-in
.percentage
font-family "Helvetica Neue", sans-serif
font-weight 700
padding 4px
border-radius 3px
background #4e4
transition left 5s linear
color #fff
border 1px solid rgba(0, 0, 0, .3)
box-shadow 2px 2px 2px rgba(0, 0, 0, .3)
&.red
background #e44
.team-stealth-rock
position absolute
background url("//media.pokebattle.com/img/battle/stealth_rock.png")
width 28px
height 35px
pointer-events none
.team-toxic-spikes
position absolute
background url("//media.pokebattle.com/img/battle/toxic_spikes.png")
width 21px
height 21px
pointer-events none
.team-spikes
position absolute
background url("//media.pokebattle.com/img/battle/spikes.png")
width 21px
height 21px
pointer-events none
.team-sticky-web
position absolute
background url("//media.pokebattle.com/img/battle/sticky_web.png")
width 150px
height 151px
pointer-events none
.team-screen
position absolute
opacity .5
width 1px
height @width
pointer-events none
&.rounded
border-radius 100%
for $pair in ('blue' $water) ('yellow' $electric) ('pink' $fairy)
&.{$pair[0]}
border 1px solid darken($pair[1], 20%)
background-color $pair[1]
.projectile
position absolute
width 64px
height @width
pointer-events none
for $array in $types
&.{$array[0]}
create-projectile($array[1])
.battle_summary
position absolute
bottom 0
left $side-width
right @left
background rgba(black, .5)
color white
display none
text-shadow none
padding 4px 10px
font-size .875em
p
margin 0
font-size .8125em
&.big
font-size 1em
margin-top 4px
&:first-child
margin-top 0
.ability_activation
position absolute
width 150px
background rgba(black, .5)
text-align center
opacity 0
color white
outline-text()
pointer-events none
$radius = 4px
strong
font-size 1.2em
display block
&.front
border-radius $radius 0 0 $radius
top 64px
right 0
&.back
border-radius 0 $radius $radius 0
bottom 64px
left 0
.moves
.button
display block
float none
margin 0 0 5px
&.hidden
display none
.switches
.button
display block
float none
&.disabled
.pokemon_icon
opacity .5
.pokemon_icon
display block
.mega-evolve
text-align center
font-size .8em
.gender
&.gender_female
color #EA597A
&.gender_male
color #62CBB0
.team_icons
width 6 * 40px
.pokemon_icon
position relative
width 32px
height 32px
display inline-block
margin 0 auto
background url("//media.pokebattle.com/img/pokemon_icons_40px.png") top left
vertical-align middle
&.fainted
opacity 0.5
.battle_teams
position absolute
left $side-width
right $side-width
text-align center
margin 0 auto
width 300px
normal-font()
li
position relative
.gender
position absolute
top 0
right 2px
font-size .75em
text-shadow 0 1px 0 #333
.level
position absolute
bottom 0
right 2px
font-size .75em
line-height @font-size
outline-text(#fff)
narrow-font()
.battle_team_preview
background #eee
padding 10px
border-radius 4px
margin 10px 0
border 1px solid #aaa
.px_40, .sortable-placeholder
width 40px
margin 0 2px
padding-left 0
padding-right 0
float left
.submit_arrangement
margin 10px 0 0
.team_pokemon
position relative
width 40px
height 32px
margin 0 auto
.arrange_team
user-select none
li
cursor pointer
border-radius 4px
&.img-polaroid.active
background-color #ccc
.lead_text
font-size .8125em
min-height 0
.pokemon_icon
top 0
left 0
.sortable-placeholder
border 1px dashed #ccc
background none
height 40px
.unstyled
margin 0
.well-battle-actions
font-size 1.5em
height 3em
line-height @height
text-align center
.save-log
display block
text-align center
.drop
background linear-gradient(top, rgba($water, .7), rgba(255, 255, 255, 0.3))
width 1px
height 89px
position absolute
top 0
transform rotate(-20deg)
animation fall .63s linear infinite
@keyframes fall
to
margin-top 2 * $battle-height
margin-left 2 * $battle-height * .36
.ray
position absolute
background rgba(230, 192, 70, .5)
width ($battle-width / 10)
height 100%
transition all 1000ms ease
transform rotate(360deg) skewX(30deg)
animation fade-in-out 3s ease infinite
@keyframes fade-in-out
0%, 100%
opacity 0
50%
opacity .3
.sand_overlay
absolute top left
background url("//media.pokebattle.com/img/battle/weather_sand.png")
width 600px
height 600px
opacity .5
animation sandstorm .75s linear infinite
@keyframes sandstorm
0%
transform translate(0, 0)
100%
transform translate(200%, 100%)
.hailstone
position absolute
border-radius 4px
width 10px
height @width
background rgba(230, 200, 255, .5)
animation fall .4s linear infinite

View File

@ -0,0 +1,59 @@
.battle-list
position absolute
top 0
bottom 0
left 0
right 0
overflow auto
h2
display block
margin-left 10px
.battle-list-collection
padding-right 10px
.empty
margin-left 10px
.battle-list-item-wrapper
float left
width 100%
padding-left 10px
padding-bottom 10px
box-sizing border-box
.battle-list-item
cursor pointer
border 1px solid #DDD
background #F5F5F5
box-shadow 1px 2px 0 rgba(0, 0, 0, 0.2)
padding 10px
&:hover
background: #FFF
.vs
font-weight bold
margin 0 5px
// Normally, our media queries use max-width to scale down, but for this page we want to scale up
@media screen and (min-width: 1000px)
.battle-list-item-wrapper
width 50%
@media screen and (min-width: 1400px)
.battle-list-item-wrapper
width 33%
@media screen and (min-width: 1800px)
.battle-list-item-wrapper
width 25%
@media screen and (min-width: 2200px)
.battle-list-item-wrapper
width 20%
@media screen and (min-width: 2600px)
.battle-list-item-wrapper
width 16.6%

100
client/app/css/buttons.styl Normal file
View File

@ -0,0 +1,100 @@
button-bg(clr)
clr = lighten(clr, 50%)
light = lighten(clr, 30%)
dark-clr = darken(clr, 20%)
darker-clr = darken(clr, 50%)
darkest-clr = darken(darker-clr, 20%)
btn-clr = darken(darkest-clr, 30%)
background linear-gradient(top, clr, dark-clr)
border 1px solid darker-clr
border-top-color rgba(0, 0, 0, .1)
border-left-color rgba(0, 0, 0, .2)
border-right-color @border-left-color
border-bottom-color rgba(0, 0, 0, .3)
color btn-clr
&:hover
background linear-gradient(top, light, clr)
color lighten(btn-clr, 20%)
text-shadow 0 -1px 0 rgba(255, 255, 255, .10)
&:active, &.pressed
color darken(btn-clr, 50%)
text-shadow none
border-color darkest-clr
&.pressed
background darker-clr
&:active
background darkest-clr
.button
clearfix()
button-bg(#eee)
display inline-block
height 32px
line-height @height
border-radius 4px
margin-bottom 6px
padding 0 10px
cursor pointer
color #333
text-shadow 0 -1px 0 rgba(255, 255, 255, .25)
box-shadow inset 0 1px 0 rgba(255,255,255,.25), 0 1px 2px rgba(0,0,0,.25)
position relative
&.block
display block
&:hover
box-shadow inset 0 1px 0 rgba(255,255,255,.125), 0 1px 2px rgba(0,0,0,.125)
.main_text
narrow-font()
font-weight 700
float left
.meta_info
font-size 12px
float right
font-family "Helvetica Neue", sans-serif
&.big
font-size 1.5em
height 3em
line-height @height
text-align center
.main_text
float none
.meta_info
position absolute
top 0
right 1em
&.button_blue
background linear-gradient(top, #0066cc, #0044cc)
text-shadow 0 -1px rgba(0, 0, 0, .3)
color #fff
&:hover
background linear-gradient(top, #0088cc, #0066cc)
&.button_red
background linear-gradient(top, #cc6600, #cc4400)
text-shadow 0 -1px rgba(0, 0, 0, .3)
color #fff
&:hover
background linear-gradient(top, #cc8800, #cc6600)
for $array in $types
&.{$array[0]}
button-bg($array[1])
&.disabled, &.disabled:hover
background #333
border-color #000
cursor default
color #666
text-shadow none
box-shadow none
.hover_info_moves
.hover_info
width 196px
display block
margin 0
&:first-child
border-radius 4px 4px 0 0
&:last-child
border-radius 0 0 4px 4px
border-top none
&:not(:first-child):not(:last-child)
border-radius 0
border-top none

100
client/app/css/chat.styl Normal file
View File

@ -0,0 +1,100 @@
.chat_window .chat
left 350px
.chat
position absolute
top 0
right 0
bottom 0
&.without_spectators
.message_pane
right 0
.user_list
display none
&.without_chat_input
.messages
bottom 0
.chat_input_pane
display none
.user_list
position absolute
top 0
right 0
bottom 0
width 25%
overflow-y auto
p
padding 2px 6px
margin 0
ul
margin 0
padding 0
list-style none
li
padding 2px 6px
&:hover
background #fb7
.message_pane
position absolute
top 0
left 0
right 25%
bottom 0
box-shadow 0 0 5px rgba(0, 8, 16, .2)
.messages
overflow-y auto
position absolute
top 0
left 0
right 0
bottom 30px
border-left 1px solid rgba(0, 0, 0, .2)
border-right @border-left
background white
word-wrap break-word
h2, h3, p
padding 0 16px 2px
p
font-size .875em
text-rendering optimizeLegibility
&.move_message
font-size 1em
margin-top 10px
a.pokemon-link
color inherit
.alert
margin 0.75em 16px
.chat_input_pane
position absolute
width 100%
bottom 0
height 30px
.chat_input_wrapper
overflow: hidden // Make it take the remainder of the space not used by the button
.chat_input
box-sizing border-box
border 2px solid #384035
padding 2px 6px
margin 0
width 100%
height 30px
border-radius 2px
&:hover
border-color #5D736B
&:focus
border-color #F27405
outline none
.chat_input_send
box-sizing border-box
float right
margin 0
height 30px

View File

@ -0,0 +1,54 @@
$body-color = rgba(223, 231, 232, .5)
$background-color = #f3f3f3
$positive-color = #38AB46
$negative-color = #DC4E48
$off-white = #f5f5f5
$grey = #999
$yellow = #b92
$normal = #A9A57A
$fire = #E28544
$water = #6A97ED
$electric = #F0C944
$grass = #8FC052
$ice = #A2CFD0
$fighting = #B04137
$poison = #9253A0
$ground = #DCBE6F
$flying = #A09AEE
$psychic = #E0698B
$bug = #AEB22D
$rock = #B69D42
$ghost = #6A5F97
$dragon = #5C59F1
$dark = #685447
$steel = #B7BACF
$fairy = #F7B5F7
$types = ('normal' $normal) ('fire' $fire) ('water' $water) ('electric' $electric) ('grass' $grass) ('ice' $ice) ('fighting' $fighting) ('poison' $poison) ('ground' $ground) ('flying' $flying) ('psychic' $psychic) ('bug' $bug) ('rock' $rock) ('ghost' $ghost) ('dragon' $dragon) ('dark' $dark) ('steel' $steel) ('fairy' $fairy)
.bg-blue
background-color lighten($ice, 50%)
.bg-faded-white
background rgba(white, .5)
.bg-off-white
background $off-white
.hover-bg-white
&:hover
background white
.bg-faded-blue
background rgba(0, 20, 64, .5)
.red
color $negative-color
.yellow
color $yellow
.grey
color $grey

131
client/app/css/core.styl Normal file
View File

@ -0,0 +1,131 @@
narrow-font()
font-family "PT Sans Narrow", "Helvetica Neue", sans-serif
normal-font()
font-family 'proxima-nova', 'Helvetica Neue', Calibri, 'Droid Sans', Helvetica, Arial, sans-serif
$body_wrapper = 30px
$nav_size = 150px
$header_size = 52px
html, body
color #333332
padding 0
margin 0
font-size 100%
normal-font()
background $background-color url("//media.pokebattle.com/img/bg.png")
line-height normal
ul, ol
list-style none
ul, ol, p
margin 0
padding 0
#content
position absolute
top 0
left $nav_size
right 0
bottom 0
box-shadow 0 0 10px #000
.no-sidebar
#content
left 0
#navigation
display none
#main-section
position absolute
top $header_size
left 0
right 0
bottom 0
.window
position absolute
top 0
left 0
right 0
bottom 0
a, .fake_link
color #F27405
text-decoration none
cursor pointer
&:hover
color #BD4F00
.nav
clearfix()
list-style none
padding 0
margin 0
li
float left
border-radius 4px 4px 0 0
margin-left 2px
margin-right @margin-left
&:hover
background-color #d9d7d7
&.active
border 1px solid #d9d7d7
border-bottom none
background white
a, .fake_link, &.fake_link
color #333
text-decoration none
cursor default
a, .fake_link
padding 5px 15px
.popup-absolute
position absolute
top 20px
left 10px
right @left
text-align center
z-index 100
.popover
normal-font()
.popover-title
font-size 16px
.popover-content
font-size 14px
.loading-container
position absolute
top 0
left 0
right 0
bottom 0
background rgba(0, 0, 0, .7)
color white
font-size 4em
text-align center
z-index 99
display table
width 100%
height 100%
.loading-message
display table-cell
vertical-align middle
.team-pbv
font-size 0.75em
// Typography
.monospace
font-family "Monaco", monospace
abbr
border-bottom 1px dotted #333
.italic
font-style italic

103
client/app/css/header.styl Normal file
View File

@ -0,0 +1,103 @@
$header_size = 52px
#header
height $header_size
line-height @height
width 100%
background #38342B
z-index 10
position absolute
ul
margin 0
.icon
position relative
top 3px
font-size 20px
#main-nav
padding 0 18px
#main-nav, #header li
line-height 42px
#logo
float left
width 120px
margin-right 15px
#logo img
position relative
z-index: 100
top 3px
#sections
margin-left 30px
margin-right 120px
visibility visible
#leftmenu
display none
#leftmenu a
color #fff
padding 5px
margin-left 15px
&:hover
border-radius 5px
background #D94E47
text-decoration none
#rightmenu {
margin-left: -40px;
}
nav ul {
list-style: none;
font-size: .95em;
}
nav li {
display: block;
float: left;
text-align: left;
}
nav a {
display: block;
padding-left: .75em;
padding-right: .75em;
color: #fff;
text-decoration: none;
}
nav a:hover, nav li:hover {
background: #D94E47;
color: #6C2724;
text-decoration none
}
#sub-nav {
height: 10px;
background: #D94E47;
z-index: 6;
position absolute
bottom 0
left 0
right @left
}
@keyframes anim-rotate {
0% {
transform: rotate(0);
}
100% {
transform: rotate(360deg);
}
}
.spinner-anim {
display: inline-block;
animation: anim-rotate 1s infinite linear;
}
.spinner-anim.hidden {
display: none;
}

105
client/app/css/layout.styl Normal file
View File

@ -0,0 +1,105 @@
for $i in (1 2 3)
.z{$i}
z-index $i
for $i in (1 2 3 4)
.m{$i}
margin $i * 10px
.p{$i}
padding $i * 10px
for $i in (1 2 3 4)
// margin
.ml{$i}
margin-left $i * 10px
.mr{$i}
margin-right $i * 10px
.mt{$i}
margin-top $i * 10px
.mb{$i}
margin-bottom $i * 10px
// padding
.pl{$i}
padding-left $i * 10px
.pr{$i}
padding-right $i * 10px
.pt{$i}
padding-top $i * 10px
.pb{$i}
padding-bottom $i * 10px
.mt-header
margin-top $header_size
.left
float left
.right
float right
.center
text-align center
.align-left
text-align left
.align-right
text-align right
.flex-center
display flex
align-items center
justify-content center
.flex-column
flex-direction column
.hidden
display none
.absolute
position absolute
.fill
absolute top left
bottom 0
right 0
.fill-left
absolute top left
bottom 0
.fill-right
absolute top right
bottom 0
.relative
position relative
.rounded
border-radius 3px
.border
border 1px solid #ddd
.block
display block
.inline-block
display inline-block
.inline
display inline
.box-shadow
box-shadow 1px 2px 0 rgba(0, 0, 0, .2)
.tiny-type
font-size .75em
.clickable-box
@extend .border
@extend .bg-off-white
@extend .hover-bg-white
@extend .rounded
@extend .box-shadow

15
client/app/css/main.styl Normal file
View File

@ -0,0 +1,15 @@
@import 'nib'
@import 'colors'
@import 'core'
@import 'layout'
@import 'header'
@import 'buttons'
@import 'navigation'
@import 'chat'
@import 'main_buttons'
@import 'battle'
@import 'teambuilder'
@import 'battle_list'
@import 'messages'
@import 'miscellaneous'
@import 'responsive'

View File

@ -0,0 +1,34 @@
.main_buttons
position absolute
top 0
left 0
bottom 0
box-sizing border-box
padding 30px 30px 0 30px
width 350px
overflow auto
.button
display block
float none
text-align center
margin 0 auto
p
margin 1em 0 0 0
.section
width 100%
box-sizing border-box
background rgba(64, 64, 64, .15)
margin-bottom 15px
padding 8px
border-radius 8px
.select-team
.team-pbv
margin-top -2px
.no-alt-label
margin-left 5px
color #aaa

View File

@ -0,0 +1,110 @@
#messages
position absolute
bottom 0
right 0
width 100%
font-size .875em
.popup
position fixed
bottom 0
float right
width 270px
margin-left 10px
box-shadow 0 2px 6px rgba(0, 0, 0, .2)
&.new_message
.title
background #d94e47
.popup_messages
background white
height 270px
border-bottom 1px solid #ccc
border-left 1px solid #ccc
border-right @border-left
overflow-y scroll
line-height 1.3em
&.small
height 150px
p
margin 0 6px
overflow hidden
word-wrap break-word
.popup_menu
background linear-gradient(top, #f0f0f0, #eee)
border-left 1px solid #ccc
border-right @border-left
border-bottom @border-left
.popup_menu_button
border-right 1px solid #ddd
float left
padding 8px
cursor pointer
&:hover
background #ddd
.chat_input_pane
position relative
background #eee
height 36px
.chat_input_wrapper
padding 3px
border-left 1px solid #ccc
border-right @border-left
.title
background #333
border-radius 3px 3px 0 0
padding 8px 0 8px 12px
color white
&.new
background #d94e47
.title_buttons
position absolute
top 0
right 2px
.title_button
display inline-block
cursor pointer
padding 5px
margin 4px
&:hover
background #706666
.icon-minus
position relative
top 4px
.challenge
background #f0f0f0
border-right 1px solid #ccc
border-left @border-right
border-bottom @border-right
transition bottom .25s ease-out
.challenge_data
padding 8px
.challenge_clauses
height 50px
overflow-y auto
p
margin 1em 0 0
&:first-child
margin-top 0
.challenge_buttons
padding 8px
border-top 1px solid #ccc
.button
margin-right 8px

View File

@ -0,0 +1,93 @@
.clearfix:before, .clearfix:after
content ""
display table
line-height 0
.clearfix:after
clear both
::selection
background #F27405
.select
cursor pointer
border 1px solid #aaa
border-radius 3px
background #f0f0f0
padding 10px
&:hover
background #fff
border-color #bbb
&.disabled
background #ccc
cursor auto
.alt-input
$button-size = 65px
$num-buttons = 2
$alt-input-height = 38px
width 100%
padding-right ($button-size * $num-buttons)
box-sizing border-box
.input-wrapper
width 100%
float left
input
width 100%
box-sizing border-box
height $alt-input-height
border-radius 4px 0 0 4px
margin-bottom 0
.buttons-wrapper
margin-right -($button-size * $num-buttons)
float left
.button
float left
width $button-size
box-sizing border-box
height $alt-input-height
padding 0
border-radius 0
margin-left -1px
margin-bottom 0
.button:last-child
border-radius 0 4px 4px 0
.challenge_clauses
list-style none
margin 0
input
vertical-align top
margin-top 3px
label
display block
margin-bottom 0
&.disabled
cursor not-allowed
color #bbb
.achievement
img
float left
width 75px
h2
margin-top 0
.achievement-info
padding-left 90px
#achievements-modal
border none
animation: achievement-glow 2s infinite
$animation-glow-color = #4EABDA
@keyframes achievement-glow
0%
box-shadow 1px 1px 30px 2px $animation-glow-color
50%
box-shadow 1px 1px 45px 2px lighten($animation-glow-color, 20%)
100%
box-shadow 1px 1px 30px 2px $animation-glow-color

View File

@ -0,0 +1,113 @@
h1
narrow-font()
font-size 2em
font-weight 700
line-height 50px
margin 0
#header h1
text-transform lowercase
margin 0 $body_wrapper
#navigation
$clr = #44484e
position absolute
top 0
left 0
bottom 0
width $nav_size
background $clr
font-size .875em
overflow hidden
.logo
display block
margin 0
font-size 32px
color rgba(255, 255, 255, .8)
text-shadow 0 1px 0 #333
text-align center
height $header_size
line-height $header_size
.logo:hover
cursor pointer
.nav
margin-bottom 1em
a, .fake_link
display block
padding 4px 16px
line-height 1.5em
color #ccd
border none
background none
float none
margin 0
border-radius 0
&:hover
background lighten($clr, 10%)
text-decoration none
color #fff
h2
text-transform uppercase
line-height normal
margin 0
font-size .929em
padding 4px 16px
letter-spacing 1px
color lighten($clr, 60%)
background darken($clr, 20%)
text-shadow 0 1px 0 rgba(0, 0, 0, .3)
color #777
border-bottom 1px solid #555
border-top 1px solid #111
.nav_item
position relative
&.active, &.active:hover
background #d94e47
color #fff
cursor default
.nav_meta
position absolute
right 0
font-size .714em
.notifications, .close
height 1.3em * (1em / .714em) // 1.3 height adjusted by the nav_meta font size
width @height
line-height @height
text-align center
margin-top 0.1em // the container is 1.5em and the contents are 1.3em
margin-right 5px
float left
opacity 1
font-size 1em
text-shadow none
.notifications
border-radius 4px
background #a30
color #fff
&:hover .notifications
background #b41
.close
background #333
border-radius 1em
color #fff
cursor pointer
&:hover
background #555
.user-info
float right
margin 0 16px
.team-dropdown
max-height 300px
overflow-y auto
.team-pbv
margin-top -5px
.dropdown
li
border-bottom 1px solid #aaa
li:last-of-type
border none !important

View File

@ -0,0 +1,122 @@
@media screen and (max-width: 319px)
#logo
visibility hidden
@media screen and (max-width: 480px)
.modal
top 10px
left 10px
right 10px
.modal-header .close
padding 10px
margin -10px
@media screen and (max-width: 640px)
.popover
display none !important
#main-nav
padding 0
#logo
float none
margin 0 auto!important
#sections
visibility hidden
#leftmenu
display block
width 0
#navigation
display none
top $header_size
z-index 100
#navigation .logo
display none
#navigation.active
display block
animation slidenav 0.3s
@keyframes slidenav
0%
left -100%
100%
left 0
#content
left 0
@media screen and (max-width: 767px)
.chat_window
.main_buttons
width 100%
padding 10px
.chat
display none
// Modals
.modal
position fixed
top 20px
left 20px
right 20px
width auto
margin 0
&.fade
top -100px
&.fade.in
top 20px
// Shrink battle window
.battle_window
.battle
width 420px
margin 0 auto
right 0
padding 10px 0 0
.battle_summary
bottom 50px
left 0
right 0
.battle_container
width 400px
height 400px
margin 0 auto
.battle_pane
left 0
top 50px
.battle_teams
left 50px
right @left
top 50px
.battle_user_info
.left
bottom 0
.right
top 0
.left, .right
width 100%
left 0
.battle-timer
display inline-block
margin-right 10px
.pokemon_icons
display inline-block
vertical-align bottom
width auto
position relative
top -4px
.icon_wrapper:nth-child(even)
top 0
.moves.span8, .switches.span4
width 100%
margin-left 0
@media screen and (max-width: 980px)
.battle_window
.chat
display none

View File

@ -0,0 +1,457 @@
$header = 50px
$pokemon-list-height = 50px
// TODO: Make this a global style
.teambuilder input
.teambuilder select
box-sizing border-box
height 28px
.teambuilder .selectize-input input
height 18px
.teambuilder
position absolute
top 0
bottom 0
left 0
right 0
select, input
margin-bottom 0!important
width 100%
.teambuilder .meta-info
max-width 1368px
.teambuilder .meta-info .left-side, .teambuilder .meta-info .right-side
float left
box-sizing border-box
.teambuilder .navigation
position absolute
top $header
bottom 0
background-color $body-color
width: 150px
ul
list-style-type none
padding 0
margin 0
li, .nav-button
display block
box-sizing border-box
padding-left 1em
height $pokemon-list-height
line-height $pokemon-list-height
font-size 14px
li:hover, .nav-button:hover
cursor pointer
background-color rgba(0, 0, 0, 0.1)
li.active
background-color rgba(200, 210, 255, 0.5)
.pokemon_icon
float none
display inline-block
margin 0
vertical-align middle
.pokemon-pbv
margin-top 7px
font-size 0.75em
.pokemon-middle
line-height 50%
display inline-block
vertical-align middle
.teambuilder .meta-info .left-side
padding-left 180px
width 45%
.teambuilder .meta-info .right-side
width 55%
padding-left 20px
.teambuilder .species
float left
width 180px
margin-left -185px
.species_list
width 100%
.pbv
float right
color #888
.teambuilder .shiny-switch
display inline-block
width 14px
height 14px
margin-right 10px
margin-left -(@margin-right + @width)
vertical-align middle
cursor pointer
background-image URL('http://91.121.152.74:8000/Sprites/images/noshiny.png')
background-size cover
&.selected
background-image URL('http://91.121.152.74:8000/Sprites/images/shiny.png')
.teambuilder .happiness-switch
display inline-block
position relative
width 14px
height 14px
margin-left 10px
margin-right -(@margin-left + @width)
cursor pointer
vertical-align middle
$happy = #FF3C3C
$unhappy = #333
&:before, &:after
position absolute
content ""
left 8px
top 0
width 8px
height 13px
background $happy
border-radius 7px 7px 0 0
transform rotate(-45deg)
transform-origin 0 100%
&:after
left 0
transform rotate(45deg)
transform-origin 100% 100%
&:hover:before, &:hover:after
background darken($happy, 30%)
&.selected:before, &.selected:after
background $unhappy
&.selected:hover:before, &.selected:hover:after
background lighten($unhappy, 30%)
.teambuilder .team_meta
position absolute
top 0
left 0
right 0
height $header
line-height @height
.team_name
font-weight 700
font-size 1.5em
height $header
line-height @height
float left
margin 0
width auto
padding 0 (1em/@font-size)
box-sizing border-box
.team_name:hover:not(:focus)
text-decoration underline
cursor pointer
.team_meta_buttons
float right
margin-top 7px
.button
float left
margin-right 10px
.teambuilder .meta-info .non-stats
display table
width 100%
.teambuilder_row, .pbv-row
display table-row
line-height 1.5em
.teambuilder_col
display table-cell
box-sizing border-box
height 2em
margin 0
&:first-child
text-align right
padding-right 10px
.pbv-row .left, .pbv-row .right
box-sizing border-box
height 2em
.non-stat-label
width 1px // minimum possible size
.teambuilder .stats
width 100%
margin-bottom 1.5em
td, th
text-align left
padding-right 10px
white-space nowrap
line-height 1.5em
height 1.5em
.stat-label, .stat-total, .base-stat
width 1px // minimum possible size
td:first-child
text-align right
input
width 100%
text-align right
th.ev-cell, th.iv-cell
text-align right
padding-right 17px // match with value in textfield (10px from the main cell + the input padding)
td.ev-cell, td.iv-cell
width 50px
.plus-nature
color $positive-color
font-weight bold
.minus-nature
color $negative-color
.remaining-evs
float left
.hidden-power
float right
.ev-lock
float left
margin-right 5px
cursor pointer
.icon-lock
color #444
.icon-unlocked
color #888
.remaining-evs-amount.over-limit
color $negative-color
font-weight bold
.teambuilder select.select-hidden-power
width auto
.teambuilder .pokemon_edit
position absolute
overflow auto
left 150px
top $header
bottom 0
right 0
background white
padding 15px
overflow-y scroll
.teambuilder .forme-sprite-box
height 120px
line-height @height
text-align center
.forme-sprite
display inline-block
.teambuilder .species-types
text-align center
margin-bottom 5px
margin-top 50px
img
margin 0 2px
.teambuilder .selected_moves
.header
margin-bottom 8px
.moves-label
font-weight bold
font-size 2em
line-height 1em
a
line-height 2em
.move-button, input
display block
margin 0
box-sizing border-box
height 32px
.close
opacity .4
line-height 30px
width 20px
text-align center
float right
.close:hover, .close:focus
opacity .8
font-weight bold
.table-moves
border-collapse separate
border solid 1px #DDD
thead th
background whitesmoke
border-bottom solid 1px #DDD
tbody tr
cursor pointer
td
border 1px solid transparent
&.active td
background #f0f0f9
&:first-child
text-decoration underline
&.selected td
background #f0f0f9
&:first-child
font-weight bold
img
margin-bottom 3px
.name
white-space nowrap
.teambuilder .table-moves
margin-top 8px
.teambuilder .description
width 60%
.teambuilder .display_teams
position absolute
top 0
left 0
bottom 0
right 0
overflow auto
h2
margin-left 10px
margin-bottom 0
.select-team
h2
text-overflow ellipsis
white-space nowrap
overflow hidden
width 192px
margin 0
font-size 1.5em
.add-new-team
margin 10px
.team-meta
font-size .75em
text-align right
margin-top 10px
.team-pbv
margin-top -5px
textarea.textarea_modal
box-sizing border-box
width 100%
height 300px
@media screen and (max-width: 1268px)
// stack non-stats and stats
.teambuilder .meta-info .left-side, .teambuilder .meta-info .right-side
display block
width 100%
// stack non-stats and stats
.teambuilder .non-stats
margin 0!important
// stack non-stats and stats
.teambuilder .meta-info .right-side
padding-left 0
// add some separation between stats and non-stats
.teambuilder .stats
margin-top 1.5em
@media screen and (max-width: 1000px)
// Hide move descriptions
.table-moves .description
display none
// Make selected moves take up the entire row
.teambuilder .selected_moves .span3
width 100%
margin-left 0
@media screen and (max-width: 800px)
// Move side navigation to above
.teambuilder .navigation li, teambuilder .navigation .nav-button
float left
width (100/6)%
font-size 0 // Hide the text
text-align center
// Move side navigation to above
.teambuilder .pokemon_edit
left 0
top $header + $pokemon-list-height
// Move side navigation to above
.teambuilder .navigation
width 100%
@media screen and (max-width: 600px)
// Scroll at the teambuilder level instead of the pokemon_edit level
// TODO: This should kick in on tablet landscape modes,
// with large widths but small heights (ex: nexus 7)
.teambuilder
overflow auto
.team_meta
position static
height auto
.navigation
position static
.pokemon_edit
position static
overflow visible
textarea.textarea_modal
height auto
@media screen and (max-width: 480px)
// split species and non-stats
.teambuilder .meta-info .left-side
padding-left 0
// split species and non-stats
.teambuilder .species, .teambuilder .non-stats
margin-left 0
width 100%
margin-bottom 1.5em
// hide ev-bars
.teambuilder .ev-range-cell
display none
// Hide pp and acc cells
.table-moves
.pp, .acc
display none

View File

@ -0,0 +1,46 @@
return if PokeBattle.autoConnect == false
PokeBattle.primus = Primus.connect()
PokeBattle.primus.on 'listChatroom', (id, users) ->
if room = PokeBattle.rooms.get(id: id)
room.get('users').reset(users)
else
room = PokeBattle.rooms.add(id: id, users: users)
new ChatView(model: room, el: $('#chat-section .chat')).render()
PokeBattle.primus.on 'userMessage', (id, username, data) ->
room = PokeBattle.rooms.get(id)
room.userMessage(username, data)
PokeBattle.primus.on 'rawMessage', (id, message) ->
room = PokeBattle.rooms.get(id)
room.rawMessage(message)
PokeBattle.primus.on 'announce', (id, klass, message) ->
room = PokeBattle.rooms.get(id)
room.announce(klass, message)
PokeBattle.primus.on 'joinChatroom', (id, user) ->
room = PokeBattle.rooms.get(id)
room.get('users').add(user)
PokeBattle.primus.on 'leaveChatroom', (id, user) ->
room = PokeBattle.rooms.get(id)
room.get('users').remove(user)
PokeBattle.primus.on 'topic', (topic) ->
# TODO: Hardcoded
room = PokeBattle.rooms.get("Lobby")
room.setTopic(topic)
PokeBattle.userList = new UserList()
PokeBattle.battles = new BattleCollection([])
PokeBattle.messages = new PrivateMessages([])
PokeBattle.rooms = new Rooms([])
$ ->
PokeBattle.navigation = new SidebarView(el: $('#navigation'))
PokeBattle.teambuilder = new TeambuilderView(el: $("#teambuilder-section"))
PokeBattle.battleList = new BattleListView(el: $("#battle-list-section"))
new PrivateMessagesView(el: $("#messages"), collection: PokeBattle.messages)

View File

@ -0,0 +1,8 @@
class @BattleCollection extends Backbone.Collection
model: Battle
isPlaying: ->
@find((battle) -> battle.isPlaying())?
playingBattles: ->
@filter((battle) -> battle.isPlaying())

View File

@ -0,0 +1,2 @@
class @PrivateMessages extends Backbone.Collection
model: PrivateMessage

View File

@ -0,0 +1,9 @@
class @Rooms extends Backbone.Collection
model: Room
# Delegate room events to every single room in this collection.
for eventName in Room::EVENTS
do (eventName) =>
this::[eventName] = (args...) ->
@each (room) ->
room[eventName].apply(room, args)

View File

@ -0,0 +1,15 @@
class @UserList extends Backbone.Collection
model: User
comparator: (a, b) =>
aAuthority = a.get('authority')
bAuthority = b.get('authority')
aName = "#{a.id}".toLowerCase()
bName = "#{b.id}".toLowerCase()
if aAuthority < bAuthority then 1
else if aAuthority > bAuthority then -1
else if aName < bName then -1
else if aName > bName then 1
else 0
initialize: =>

View File

@ -0,0 +1,2 @@
class @Replays extends Backbone.Collection
model: Replay

View File

@ -0,0 +1,5 @@
$currentModal = null
PokeBattle.primus.on 'achievementsEarned', (achievements) ->
# TODO: Add achievements to the current modal if one is already open
$currentModal = PokeBattle.modal('modals/achievements', window: window, achievements: achievements)

View File

@ -0,0 +1,10 @@
PokeBattle.primus.on 'altList', (list) ->
PokeBattle.alts.list = list
PokeBattle.primus.on 'altCreated', (altName) ->
PokeBattle.alts.list.push(altName)
PokeBattle.alts =
list: []
createAlt: (altName) ->
PokeBattle.primus.send('createAlt', altName)

View File

@ -0,0 +1,52 @@
# Send primus event when leaving battles.
PokeBattle.battles.on 'remove', (battle) ->
PokeBattle.primus.send('leaveChatroom', battle.id)
# Event listeners
PokeBattle.primus.on 'updateBattle', (id, queue) ->
battle = PokeBattle.battles.get(id)
if !battle
console.log "Received events for #{id}, but no longer in battle!"
return
battle.update(queue)
# Create a BattleView when spectating a battle
PokeBattle.primus.on 'spectateBattle', (id, format, numActive, index, playerIds, log) ->
if PokeBattle.battles.get(id)
console.log "Already spectating battle #{id}!"
return
battle = new Battle({id, format, numActive, index, playerIds})
# Create BattleView
$battle = $(JST['battle_window']({battle, window}))
$('#main-section').append($battle)
battle.view = new BattleView(el: $battle, model: battle)
battle.view.skip = 0
battle.view.$('.battle_pane').hide()
# Add to collection
PokeBattle.battles.add(battle)
# Update log
battle.update(log)
PokeBattle.primus.on 'updateTimers', (id, timers) ->
battle = PokeBattle.battles.get(id)
if !battle
console.log "Received events for #{id}, but no longer in battle!"
return
battle.view.updateTimers(timers)
PokeBattle.primus.on 'resumeTimer', (id, player) ->
battle = PokeBattle.battles.get(id)
if !battle
console.log "Received events for #{id}, but no longer in battle!"
return
battle.view.resumeTimer(player)
PokeBattle.primus.on 'pauseTimer', (id, player, timeSinceLastAction) ->
battle = PokeBattle.battles.get(id)
if !battle
console.log "Received events for #{id}, but no longer in battle!"
return
battle.view.pauseTimer(player, timeSinceLastAction)

View File

@ -0,0 +1,2 @@
PokeBattle.primus.on 'battleList', (battles) ->
PokeBattle.battleList.refreshListComplete(battles)

View File

@ -0,0 +1,290 @@
PokeBattle.commands ?= {}
Commands = {}
desc = (description) ->
desc.lastDescription = description
makeCommand = (commandNames..., func) ->
for commandName in commandNames
Commands[commandName] = func
# Generate description
description = ""
if commandNames.length > 1
aliases = commandNames[1...].map((n) -> "/#{n}").join(', ')
description += " <i>Also #{aliases}. </i>"
description += desc.lastDescription
# TODO: Hardcoded user level
HelpDescriptions['1'][commandNames[0]] = description
delete desc.lastDescription
parseCommand = (line) ->
[ commandName, args... ] = line.split(/\s+/)
if commandName[0] == '/'
# It's a command. Remove leading slash
commandName = commandName[1...]
args = args.join(' ').split(/,/g)
return [commandName, args]
return null
PokeBattle.commands.execute = (room, line) ->
result = parseCommand(line)
return false if !result
[commandName, args] = result
command = Commands[commandName]
if !command
# Fall-through to server.
return false
command(room, args...)
return true
desc 'Displays a list of all commands.'
makeCommand "commands", "help", "h", (room) ->
user = room.get('users').get(PokeBattle.username)
for level, descriptions of HelpDescriptions
level = Number(level)
continue if user.get('authority') < level
message = []
# TODO: Hardcoded levels
authLevels = {1: "USER", 2: "DRIVER", 3: "MOD", 4: "ADMIN", 5: "OWNER"}
humanLevel = authLevels[level]
message.push("<b>#{humanLevel} COMMANDS:</b>")
for name, description of descriptions
message.push("<b>/#{name}:</b> #{description}")
message = message.join("<br>")
room.announce('success', message)
true
desc 'Opens the challenge for a specific user. Usage: /challenge username'
makeCommand "challenge", "chall", "c", (room, username) ->
if !username
PokeBattle.events.trigger("errorMessage", "Usage: /challenge username")
return
message = PokeBattle.messages.add(id: username)
message.openChallenge(username)
desc 'Private messages a certain user. Usage: /message username, message'
makeCommand "message", "msg", "pm", "whisper", "w", (room, username, messages...) ->
username = username?.trim()
if !username
PokeBattle.events.trigger("errorMessage", "Usage: /message username, msg")
return
message = PokeBattle.messages.add(id: username)
if messages.length > 0
text = messages.join(',')
PokeBattle.primus.send('privateMessage', message.id, text)
else
# The PM is opened without a message.
message.trigger('open', message)
desc 'Clears the chat.'
makeCommand "clear", (room) ->
room.clear()
desc 'Displays a Pokemon\'s PokeBattle value, or displays all Pokemon at or under a particular PBV. Usage: /pbv pkmn1, pkmn2, OR /pbv number'
makeCommand "pbv", (room, pokemon...) ->
pbv = Number(pokemon[0])
if !isNaN(pbv)
messages = findPokemonAtPBV(pbv)
else
messages = findTotalPBV(pokemon)
if messages.length == 0
room.announce('error', "<b>PBV error:</b> Enter valid Pokemon or PBV.")
else
room.announce('success', "<b>PBV:</b> #{messages.join('; ')}")
findPokemonAtPBV = (pbv) ->
messages = []
counter = 0
for speciesName, formes of window.Generations.XY.FormeData
for formeName, formeData of formes
if formeData.pokeBattleValue <= pbv
counter += 1
dexEntry = "pokemon/#{slugify(speciesName)}/#{slugify(formeName)}"
icon = pokemonIcon(speciesName, formeName)
formattedName = formatName(speciesName, formeName)
messages.push("#{linkToDex(dexEntry, icon + formattedName)}:
#{formeData.pokeBattleValue}")
if messages.length > 10
messages = _.sample(messages, 10)
messages.push(linkToDex("pokemon/?pbv=<#{pbv + 1}",
"See more Pokemon &raquo;"))
if messages.length > 0
plural = if messages.length == 1 then "is" else "are"
messages.unshift("There #{plural} #{counter} Pokemon with a PBV of
#{pbv} or less")
messages
findTotalPBV = (pokemon) ->
pokemon = _(pokemon).map(findPokemon)
messages = []
total = 0
for array in pokemon
continue unless array
[speciesName, formeName] = array
pbv = PokeBattle.PBV.determinePBV(window.Generations.XY,
species: speciesName, forme: formeName)
total += pbv
dexEntry = "pokemon/#{slugify(speciesName)}/#{slugify(formeName)}"
icon = pokemonIcon(speciesName, formeName)
formattedName = formatName(speciesName, formeName)
messages.push("#{linkToDex(dexEntry, icon + formattedName)}: #{pbv}")
messages.push("Total: #{total}") if messages.length > 1
messages
desc 'Looks up information about a Pokemon, move, item, or ability.'
makeCommand "data", "dex", (room, query) ->
if (pokemon = findPokemon(query))
message = dataPokemon(pokemon)
else if (item = findItem(query))
message = dataItem(item)
else if (move = findMove(query))
message = dataMove(move)
else if (ability = findAbility(query))
message = dataAbility(ability)
else
room.announce("error", "<b>Data error:</b> Enter a valid Pokemon, item,
move, or ability.</div>")
return
room.announce('success', message)
dataPokemon = (pokemon) ->
[speciesName, formeName] = pokemon
[speciesSlug, formeSlug] = [slugify(speciesName), slugify(formeName)]
forme = window.Generations.XY.FormeData[speciesName][formeName]
{types, abilities, hiddenAbility, stats, pokeBattleValue} = forme
# Format abilities
abilities = _.clone(abilities)
abilities.push(hiddenAbility) if hiddenAbility?
abilities = _(abilities).map((a) -> linkToDex("abilities/#{slugify(a)}", a))
abilities = abilities.join('/')
abilities += " (H)" if hiddenAbility?
# Format types, stats, and icon
types = _(types).map (t) ->
linkToDex("types/#{slugify(t)}",
"<img src='#{window.TypeSprite(t)}' alt='#{t}'/>")
statNames = [ 'HP', 'Attack', 'Defense', 'Sp.Attack', 'Sp.Defense', 'Speed']
stats = [ stats.hp, stats.attack, stats.defense,
stats.specialAttack, stats.specialDefense, stats.speed ]
statsText = _.map(_.zip(statNames, stats), (a) -> a.join(': ')).join(' / ')
# Build data
message = """#{pokemonIcon(speciesName, formeName, "left")}
<p class="ml3">
<b>#{formatName(speciesName, formeName)}:</b> #{types.join('')} |
#{abilities}<br />#{statsText} |
#{_(stats).reduce((a, b) -> a + b)} <abbr title="Base Stat Total">BST</abbr>
| <abbr title="PokeBattle Value">PBV</abbr>: #{pokeBattleValue}
#{linkToDex("pokemon/#{speciesSlug}/#{formeSlug}", "See dex entry &raquo;")}
</p>
"""
message
dataItem = (itemName) ->
item = window.Generations.XY.ItemData[itemName]
message = "<b>#{itemName}:</b> #{item.description}"
message += " Natural Gift is #{item.naturalGift.type} type
and has #{item.naturalGift.power} base power." if item.naturalGift
message += " Fling has #{item.flingPower} base power." if item.flingPower
message += " Currently unreleased in Gen 6." if item.unreleased
message
dataMove = (moveName) ->
move = window.Generations.XY.MoveData[moveName]
type = linkToDex("types/#{slugify(move.type)}",
"<img src='#{window.TypeSprite(move.type)}' alt='#{move.type}'/>")
category = """<img src="#{CategorySprite(move.damage)}"
alt="#{move.damage}"/>"""
target = """<img src="#{TargetSprite(move)}"
alt="#{move.target}"/>"""
power = move.power || "&mdash;"
acc = move.accuracy || "&mdash;"
maxpp = Math.floor(move.pp * 8/5)
if move.priority > 0
priority = "+#{move.priority}"
else if move.priority < 0
priority = move.priority
message = """<b>#{moveName}:</b> #{type} #{category} #{target} """
message += "<b>Power:</b> #{power} <b>Acc:</b> #{acc} <b>PP:</b> #{move.pp} (max #{maxpp})"
message += "<br />"
message += "Priority #{priority}. " if priority
message += move.description
message += " "
message += linkToDex("moves/#{slugify(moveName)}",
"See who learns this move &raquo;")
message
dataAbility = (abilityName) ->
ability = window.Generations.XY.AbilityData[abilityName]
message = """<b>#{abilityName}:</b> #{ability.description}
#{linkToDex("abilities/#{slugify(abilityName)}",
"See who obtains this ability &raquo;")}"""
message
# Finds the most lenient match possible.
findPokemon = (pokemonName) ->
pokemonName = normalize(pokemonName)
for speciesName, speciesData of window.Generations.XY.FormeData
for formeName of speciesData
name = speciesName
name += formeName unless formeName == 'default'
name = normalize(name)
name += name
return [speciesName, formeName] if name.indexOf(pokemonName) != -1
# Return blank match
null
# Finds the most lenient match possible.
findItem = (itemName) ->
normalized = normalize(itemName)
for name of window.Generations.XY.ItemData
return name if normalized == normalize(name)
# Return blank match
null
# Finds the most lenient match possible.
findMove = (moveName) ->
normalized = normalize(moveName)
for name of window.Generations.XY.MoveData
return name if normalized == normalize(name)
# Return blank match
null
# Finds the most lenient match possible.
findAbility = (abilityName) ->
normalized = normalize(abilityName)
for name of window.Generations.XY.AbilityData
return name if normalized == normalize(name)
# Return blank match
null
slugify = (str) ->
str.toLowerCase().replace(/[^a-z0-9]/g, '-').replace(/\-{2,}/g, '-')
normalize = (str) ->
str.trim().toLowerCase().replace(/[^a-zA-Z0-9]+/g, '')
formatName = (speciesName, formeName) ->
if formeName == 'default'
pokemonName = speciesName
else
pokemonName = speciesName
pokemonName += ' '
pokemonName += formeName.split('-')
.map((n) -> n[0].toUpperCase() + n[1...])
.join('-')
return pokemonName
linkToDex = (slug, text) ->
"<a href='//pokebattle.com/dex/#{slug}' target='_blank'>#{text}</a>"
pokemonIcon = (speciesName, formeName, classes="") ->
style = window.PokemonIconBackground(speciesName, formeName)
"""<span class="pokemon_icon #{classes}" style="#{style}"></span>"""

View File

@ -0,0 +1,39 @@
$body = $("body")
$popup = $('#popup')
PokeBattle.primus.on 'open', ->
$popup.hide()
PokeBattle.rooms.rawMessage("Connected to the server!", class: "yellow italic")
PokeBattle.primus.on 'reconnecting', ->
PokeBattle.rooms.rawMessage("Lost connection to the server...", class: "red italic")
PokeBattle.primus.on 'end', ->
PokeBattle.rooms.rawMessage("Connection terminated!", class: "red italic")
PokeBattle.primus.on 'reconnecting', (opts) ->
seconds = Math.floor(opts.timeout / 1000)
if $popup.length == 0
$popup = $('<div id="popup" class="reconnect popup-absolute"/>').hide()
$div = $('<span/>')
.addClass('alert')
.html("<strong>You lost your connection to PokeBattle!</strong>
<span class='reconnect-text'></span>")
.appendTo($popup)
$popup.appendTo($body)
$popup.find('.reconnect-text').html("""Reconnecting in
<span class='reconnect-timer'>#{seconds}</span> seconds...""")
$popup.fadeIn(250)
$body.data('seconds', seconds)
reconnectTimer($popup)
reconnectTimer = ($popup) ->
seconds = $body.data('seconds') ? 1
if seconds == 0
$popup.find('.reconnect-text').text("Reconnecting...")
else
time = "#{Math.floor(seconds / 60)}:"
time += "00#{seconds % 60}".substr(-2)
$popup.find('.reconnect-timer').text(time)
$body.data('seconds', seconds - 1)
setTimeout(reconnectTimer.bind(this, $popup), 1000)

View File

@ -0,0 +1,57 @@
PokeBattle.primus.on 'errorMessage', (args...) ->
PokeBattle.events.trigger('errorMessage', args...)
PokeBattle.events.on "errorMessage", (type, args...) ->
e = PokeBattle.errors
switch type
when e.INVALID_SESSION
$('#errors-modal').remove() if $('#errors-modal').length > 0
options =
title: "Your login timed out!"
body: """To access the simulator, you need to
<a href="http://91.121.152.74/">login again</a>."""
$modal = PokeBattle.modal('modals/errors', options)
$modal.find('.modal-footer button').first().focus()
PokeBattle.primus.end()
when e.BANNED
$('#errors-modal').remove() if $('#errors-modal').length > 0
[reason, length] = args
if length < 0
length = "is permanent"
else
length = "lasts for #{Math.round(length / 60)} minute(s)"
body = "This ban #{length}."
if reason
body += "You were banned for the following reason: #{reason}"
options =
title: "You have been banned!"
body: body
$modal = PokeBattle.modal('modals/errors', options)
$modal.find('.modal-footer button').first().focus()
PokeBattle.primus.end()
when e.FIND_BATTLE
PokeBattle.events.trigger("findBattleCanceled")
# Show errors
[errors] = args
alert(errors)
when e.BATTLE_DNE
[battleId] = args
message = 'This battle no longer exists.'
PokeBattle.rooms.get(battleId)?.announce('error', message)
when e.COMMAND_ERROR
[ roomId, message ] = args
PokeBattle.rooms.get(roomId).announce('error', message)
when e.PRIVATE_MESSAGE
[ toUser, messageText ] = args
message = PokeBattle.messages.get(toUser)
message.add(toUser, messageText, type: "error")
when e.INVALID_ALT_NAME
[ messageText ] = args
alert(messageText)
PokeBattle.events.trigger("invalidAltName")
else
console.log("Received error: #{type}")
console.log(" with content: #{args}")

View File

@ -0,0 +1,7 @@
# Prevents a clean escape while you're in a battle.
$(window).on 'beforeunload', ->
if PokeBattle.battles.isPlaying()
"You are currently in a battle."
else
# Do not prompt
return

View File

@ -0,0 +1,4 @@
PokeBattle.primus.on 'data', (args...) ->
try
console.log(args...) if window.localStorage.debug == 'true'
catch

View File

@ -0,0 +1,50 @@
$ ->
$mainButtons = $('.main_buttons')
$mainButtons.on 'click', '.teambuilder_button', (e) ->
PokeBattle.navigation.showTeambuilder()
createChallengePane
eventName: "findBattle"
populate: $mainButtons.find('.find_battle_select_team')
button: $mainButtons.find('.find_battle')
defaultClauses: [
Conditions.SLEEP_CLAUSE
Conditions.EVASION_CLAUSE
Conditions.SPECIES_CLAUSE
Conditions.OHKO_CLAUSE
Conditions.PRANKSTER_SWAGGER_CLAUSE
Conditions.UNRELEASED_BAN
Conditions.RATED_BATTLE
Conditions.TIMED_BATTLE
]
blockedClauses: true
$mainButtons.find('.find_battle').on 'challenge', ->
$this = $(this)
$this.find('.find-icon')
.addClass('icon-spinner spinner-anim')
.removeClass("icon-earth")
$mainButtons.find('.display_credits').click ->
$modal = PokeBattle.modal('modals/credits')
$modal.find('.modal-footer button').first().focus()
# Depresss Find Battle once one is found
depressFindBattle = ->
$mainButtons = $('.main_buttons')
$button = $mainButtons.find('.find_battle')
$button.removeClass("disabled")
$button.find('.find-icon')
.removeClass("icon-spinner spinner-anim")
.addClass("icon-earth")
$mainButtons.find('.find_battle_select_team .select').removeClass('disabled')
$(window).load ->
$mainButtons = $('.main_buttons')
PokeBattle.battles.on 'add', (battle) ->
if !battle.get('spectating')
depressFindBattle()
PokeBattle.primus.on 'findBattleCanceled', depressFindBattle
PokeBattle.events.on 'findBattleCanceled', depressFindBattle

View File

@ -0,0 +1,31 @@
# Clicking a person's name will open a private message session with that person.
$(document).on 'click', '.open_pm', ->
$this = $(this)
message = PokeBattle.messages.add(id: $this.data('user-id'))
message.trigger('open', message)
message.trigger('focus', message)
# Receive private message events
PokeBattle.primus.on 'privateMessage', (messageId, fromUserId, messageText) ->
message = PokeBattle.messages.add(id: messageId)
message.add(fromUserId, messageText)
# Challenges
PokeBattle.primus.on 'challenge', (fromUserId, generation, conditions) ->
message = PokeBattle.messages.add(id: fromUserId)
message.add(fromUserId, "You have been challenged!", type: "alert")
message.openChallenge(fromUserId, generation, conditions)
PokeBattle.primus.on 'cancelChallenge', (fromUserId) ->
message = PokeBattle.messages.add(id: fromUserId)
message.add(fromUserId, "The challenge was canceled!", type: "alert")
message.closeChallenge(fromUserId)
PokeBattle.primus.on 'rejectChallenge', (fromUserId) ->
message = PokeBattle.messages.add(id: fromUserId)
message.add(fromUserId, "The challenge was rejected!", type: "alert")
message.closeChallenge(fromUserId)
PokeBattle.primus.on 'challengeSuccess', (fromUserId) ->
message = PokeBattle.messages.add(id: fromUserId)
message.closeChallenge(fromUserId)

View File

@ -0,0 +1,21 @@
class PokeBattleRouter extends Backbone.Router
routes:
"" : "main"
"battles/:id" : "spectateBattle"
main: =>
$navigation = $('#navigation')
$navigation.find('.nav_item').first().click()
spectateBattle: (id) =>
if PokeBattle.battles.get(id)
PokeBattle.navigation.changeWindowToBattle(id)
else
PokeBattle.primus.send('spectateBattle', id)
PokeBattle.router = new PokeBattleRouter()
PokeBattle.primus.once "loginSuccess", ->
return if Backbone.History.started
PokeBattle.events.trigger("ready")
routed = Backbone.history.start(pushState: true)

View File

@ -0,0 +1,4 @@
$(document).on 'click', '.spectate', ->
battleId = $(this).data('battle-id')
PokeBattle.router.navigate("battles/#{battleId}", trigger: true)
return false

View File

@ -0,0 +1,6 @@
PokeBattle.primus.on 'version', (version) ->
if version != PokeBattle.CLIENT_VERSION
$modal = PokeBattle.modal 'modals/new_client', ($modal) ->
$modal.on 'click', '.button_refresh', ->
window.location.reload(true)
$modal.find('button').first().focus()

View File

@ -0,0 +1,57 @@
DEFAULT_INTERVAL = 1000
original = document.title
timeoutId = undefined
$.flashTitle = (newMsg, interval) ->
if newMsg == false
# stop flashing and reset title
clearTimeout(timeoutId)
document.title = original
else
# loop flashing
interval = interval || DEFAULT_INTERVAL
timeoutId = setTimeout( ->
clearTimeout(timeoutId)
document.title = if (document.title == original) then newMsg else original
timeoutId = setTimeout(arguments.callee, interval)
, interval)
PokeBattle.NotificationTypes =
PRIVATE_MESSAGE:
showDesktop: false
BATTLE_STARTED:
showDesktop: true
prefix: "bs"
title: "Battle Started"
body: "Your battle has started!"
ACTION_REQUESTED:
showDesktop: true
prefix: "ar"
title: "Battle Action Requested"
body: "A battle is ready for your input!"
notifications = []
# Currently called in concerns/find_battle.coffee
PokeBattle.requestNotifyPermission = =>
if notify.permissionLevel() == notify.PERMISSION_DEFAULT
notify.requestPermission()
# TODO: Count the notifications by unique type/identifier combos
PokeBattle.notifyUser = (type, identifier) =>
return if document.hasFocus()
$.flashTitle "You have new notification(s)"
if type.showDesktop
notification = notify.createNotification type.title,
icon: "//media.pokebattle.com/logo/pb_red.png"
body: type.body
tag: "PokeBattle_#{type.prefix}_#{identifier}"
notifications.push notification
$(window).focus ->
$.flashTitle(false)
notification.close() for notification in notifications
notifications = []

View File

@ -0,0 +1,70 @@
class TeamStore extends Backbone.Collection
model: Team
initialize: ->
@on('add remove reset remoteSync', @saveLocally)
@loadLocally()
# Only locally save teams without an id or is trying to save.
unsavedTeams: =>
@filter((team) -> !team.id || team.get('saving'))
saveLocally: =>
teams = @unsavedTeams()
json = _.map(teams, (team) -> team.toJSON())
try
window.localStorage.setItem('local_teams', JSON.stringify(json))
catch
console.error("Failed to save teams locally.")
loadLocally: =>
try
json = window.localStorage.getItem('local_teams')
return unless json
teams = JSON.parse(json)
@add(teams) if teams.length > 0
catch
console.error("Failed to load teams locally.")
saveRemotely: =>
teams = @unsavedTeams()
team.save() for team in teams
PokeBattle.TeamStore = new TeamStore()
PokeBattle.primus.on 'receiveTeams', (remoteTeams) ->
remoteTeams = remoteTeams.map (team) ->
team.teambuilder = true
new Team(team)
# First, find teams that are already saved locally -- these exclude deleted
# teams on either side. The remote copy of the team is checked against the
# local copy of the team. If they differ, display a modal asking whether to
# override or keep the local changes.
ids = PokeBattle.TeamStore.pluck('id')
for remoteTeam in remoteTeams when remoteTeam.id in ids
remoteJSON = remoteTeam.toJSON()
localTeam = PokeBattle.TeamStore.get(remoteTeam.id)
unsavedTeam = localTeam.clone()
unsavedTeam.set(localTeam.previousAttributes(), silent: true)
localJSON = unsavedTeam.toJSON()
if !_.isEqual(remoteJSON, localJSON)
# Whoa! Versions are different! Let's ask the user what to do.
teamText = PokeBattle.exportTeam(remoteJSON.pokemon)
domId = "teams-differ-#{remoteTeam.id}"
$modal = PokeBattle.modal('modals/teams_differ', domId, {teamText})
$modal.find('textarea').first().focus()
do (localTeam, remoteJSON) ->
# We want to override the current version with the one on the server.
# This is extremely hacky due to hidden state and clones everywhere on
# the teambuilder.
$modal.find('.button_override').one 'click', ->
localTeam.set(remoteJSON, silent: true)
localTeam.trigger('render', localTeam)
$modal.modal('hide')
# Now, add teams we haven't seen yet to the store.
PokeBattle.TeamStore.add(remoteTeams)
PokeBattle.primus.on 'loginSuccess', ->
PokeBattle.primus.send('requestTeams')

View File

@ -0,0 +1,9 @@
# Used to handle the show navigation button in the header, which is visible when the browser
# window becomes small enough.
$ ->
$navigation = $("#navigation")
$(".show-navigation").click =>
active = $navigation.hasClass('active')
$navigation.toggleClass('active', !active)
$navigation.on 'click', '.nav_item', -> $navigation.removeClass("active")

View File

@ -0,0 +1,199 @@
# eventName should be one of "challenge" or "find battle"
# opts may include whether to enable clauses, for example
@createChallengePane = (opts) ->
$wrapper = opts.populate
$button = opts.button
$accept = opts.acceptButton || $()
$reject = opts.rejectButton || $()
$buttons = $button.add($accept).add($reject)
eventName = opts.eventName
capitalizedEventName = "#{eventName[0].toUpperCase()}#{eventName.substr(1)}"
acceptEventName = "accept#{capitalizedEventName}"
rejectEventName = "reject#{capitalizedEventName}"
cancelEventName = "cancel#{capitalizedEventName}"
generation = opts.generation
personId = opts.personId
defaultClauses = opts.defaultClauses || []
blockedClauses = opts.blockedClauses ? false
selectedTeamId = null
selectedAlt = null
getSelectedTeam = ->
PokeBattle.TeamStore.get(selectedTeamId) || PokeBattle.TeamStore.at(0)
renderCurrentTeam = ($context) ->
$selectTeam = $context.find('.select-team')
if PokeBattle.TeamStore.length > 0
currentTeam = getSelectedTeam()
html = JST['team_dropdown'](window: window, team: currentTeam)
$selectTeam.html(html)
else
$selectTeam.html("You have no teams!")
cancelChallenge = ->
enableButtons()
if personId
PokeBattle.primus.send(cancelEventName, personId)
else
format = $selectFormat.data('format')
PokeBattle.primus.send(cancelEventName, format)
$button.trigger('cancelChallenge')
disableButtons = ->
$wrapper.find('.select').addClass('disabled')
$buttons.addClass('disabled')
# Enable buttons
enableButtons = ->
$buttons.removeClass('disabled')
toggleAltInput = (visible) ->
$wrapper.find('.alt-input').toggleClass("hidden", !visible)
$wrapper.find('.alt-dropdown-section').toggleClass("hidden", visible)
$wrapper.find('.alt-input input').focus() if visible
isAttachedToDom = ->
$.contains(document, $wrapper.get(0))
altCreatedEvent = ->
return PokeBattle.primus.off('altCreated', altCreatedEvent) unless isAttachedToDom()
$wrapper.find('.alt-input input').val("")
toggleAltInput(false)
PokeBattle.primus.on 'altCreated', altCreatedEvent
enableButtons()
$wrapper.html(JST['new_battle']({window, defaultClauses}))
$selectFormat = $wrapper.find(".select-format")
# Implement finding battle/challenging
$button.on 'click.challenge', ->
# Start requesting for notify permission here
PokeBattle.requestNotifyPermission()
format = $selectFormat.data('format')
# Toggle state when you press the button.
if !$button.hasClass('disabled')
team = getSelectedTeam()
unless team
alert("You need to create a team using the Teambuilder before you can battle.")
PokeBattle.navigation.showTeambuilder()
return
disableButtons()
teamJSON = team.toNonNullJSON().pokemon
# Send the event
if personId
$clauses = $wrapper.find('input:checked[type="checkbox"]')
clauses = []
$clauses.each(-> clauses.push(parseInt($(this).val(), 10)))
PokeBattle.primus.send(eventName, personId, format, teamJSON, clauses, selectedAlt)
else
PokeBattle.primus.send(eventName, format, teamJSON, selectedAlt)
$button.addClass('disabled').trigger('challenge')
else
cancelChallenge()
# Implement accept/reject buttons.
$accept.on 'click.challenge', ->
return if $(this).hasClass('disabled')
team = getSelectedTeam()
unless team
alert("You need to create a team using the Teambuilder before you can battle.")
PokeBattle.navigation.showTeambuilder()
return
disableButtons()
teamJSON = team.toNonNullJSON().pokemon
PokeBattle.primus.send(acceptEventName, personId, teamJSON, selectedAlt)
$reject.on 'click.challenge', ->
return if $(this).hasClass('disabled')
disableButtons()
PokeBattle.primus.send(rejectEventName, personId)
# Clicking the alts dropdown brings down an alt selection dropdown menu
$wrapper.find('.select-alt').click (e) ->
html = JST['alt_dropdown'](alts: PokeBattle.alts.list, username: PokeBattle.username)
$wrapper.find('.alt-dropdown').html(html)
# Selecting an alt from the dropdown
$wrapper.find('.alt-dropdown').on 'click', '.select-alt-dropdown-item', (e) ->
selectedAlt = $(this).data('alt-name')
$wrapper.find('.select-alt').html($(this).html())
# When add alt is clicked, show the alt input form
$wrapper.find('.alt-dropdown').on 'click', '.add-alt-dropdown-item', (e) ->
toggleAltInput(true)
# Clicking the Add Alt Button
$wrapper.find('.alt-input .add-button').click (e) ->
altName = $wrapper.find('.alt-input input').val().trim()
PokeBattle.alts.createAlt(altName)
# Clicking the Cancel Add Alt Button
$wrapper.find('.alt-input .cancel-button').click (e) ->
toggleAltInput(false)
# Clicking the team dropdown brings down a team selection menu.
# Also updates the allTeams collection
$wrapper.find('.select-team').click (e) ->
allTeams = PokeBattle.TeamStore.models || []
html = JST['team_dropdown'](window: window, teams: allTeams)
$wrapper.find('.team-dropdown').html(html)
# Selecting a team from the menu
$wrapper.find('.team-dropdown').on 'click', '.select-team-dropdown-item', (e) ->
slot = $(e.currentTarget).data('slot')
selectedTeamId = PokeBattle.TeamStore.at(slot).id
renderCurrentTeam($wrapper)
# Selecting build team from the menu
$wrapper.find('.team-dropdown').on 'click', '.build-team-option', (e) ->
PokeBattle.navigation.showTeambuilder()
# Selecting the format changes the dropdown.
$wrapper.find('.format-dropdown').on 'click', '.select-format-dropdown-item', (e) ->
$target = $(e.currentTarget)
format = $target.data('format')
$selectFormat.text($target.text())
$selectFormat.data('format', format)
# Select non-alt option
$wrapper.find('.select-alt').html(JST['alt_dropdown'](alt: null, username: PokeBattle.username))
# Auto-select format.
if generation
# If a generation is passed, auto-select it.
$format = $wrapper.find(".format-dropdown a[data-format='#{generation}']")
$format.first().click()
$wrapper.find('.select-format').addClass('disabled')
else
# Auto-select first available format.
$wrapper.find('.format-dropdown a').first().click()
if blockedClauses
$checkboxes = $wrapper.find('input[type="checkbox"]')
if blockedClauses != true
$checkboxes = $checkboxes.filter ->
clause = Number($(this).data('clause'))
clause in blockedClauses
$checkboxes.prop('disabled', true)
$checkboxes.closest('label').addClass('disabled')
renderCurrentTeam($wrapper)
# Called when a team has been updated
teamUpdated = ->
# If this challenge panel no longer exists, remove the callback
if not isAttachedToDom()
PokeBattle.TeamStore.off 'add remove reset saved', teamUpdated
return
# Rerender the current team
renderCurrentTeam($wrapper)
# Start listening for team updated events
PokeBattle.TeamStore.on 'add remove reset saved', teamUpdated

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,15 @@
# domId is optional:
#
# PokeBattle.modal(modalPath, [domId], options, initialize)
PokeBattle.modal = (modalPath, domId, options, initialize) ->
[domId, options, initialize] = [null, domId, options] if !_.isString(domId)
[options, initialize] = [{}, options] if _.isFunction(options)
$modal = $(JST[modalPath](options))
id = '#' + (domId || $modal.prop('id'))
if $(id).length == 0
$modal.appendTo($('body'))
initialize?($modal)
else
$modal = $(id).last()
$modal.modal('show')
return $modal

View File

@ -0,0 +1,262 @@
HiddenPower = (if module? then require('../../../../shared/hidden_power') else window.HiddenPower ?= {})
@PokeBattle ?= {}
@PokeBattle.parseTeam = (teamString) ->
text = teamString.split('\n')
team = []
pokemonRegex = /^(.*?)\s*(\(M\)|\(F\)|)?(?:\s*@\s*(.*))?$/
pokemon = null
for line in text
line = line.trim()
if line.length == 0
pokemon = null
else if !pokemon
[ all, pokemonLine, gender, item ] = line.match(pokemonRegex)
pokemon = {}
team.push(pokemon)
if pokemonLine.match(/(.*?)\s*\((.*)\)/)
pokemon.name = RegExp.$1
pokemonLine = RegExp.$2
convertNameToSpeciesAndForme(pokemon, pokemonLine.trim())
pokemon.gender = gender[1] if gender # (M) and (F)
pokemon.item = item if item
for olditem, newitem of Aliases.items
if olditem is pokemon.item
pokemon.item = newitem
else if line.match(/^(?:Trait|Ability):\s+(.*)$/i)
pokemon.ability = RegExp.$1
for oldability, newability of Aliases.abilities
if pokemon.ability is oldability
pokemon.ability = newability
else if line.match(/^Level:\s+(.*)$/i)
pokemon.level = Number(RegExp.$1) || 100
else if line.match(/^Happiness:\s+(.*)$/i)
pokemon.happiness = Number(RegExp.$1) || 0
else if line.match(/^Shiny: Yes$/i)
pokemon.shiny = true
else if line.match(/^EVs: (.*)$/i)
evs = RegExp.$1.split(/\//g)
pokemon.evs = {}
for ev in evs
ev = ev.trim()
[ numberString, rawStat ] = ev.split(/\s+/)
pokemon.evs[statsHash[rawStat]] = Number(numberString) || 0
else if line.match(/^IVs: (.*)$/i)
ivs = RegExp.$1.split(/\//g)
pokemon.ivs = {}
for iv in ivs
iv = iv.trim()
[ numberString, rawStat ] = iv.split(/\s+/)
pokemon.ivs[statsHash[rawStat]] = Number(numberString) || 0
else if line.match(/^([A-Za-z]+) nature/i)
pokemon.nature = RegExp.$1
else if line.match(/^[\-\~]\s*(.*)/)
moveName = RegExp.$1
for oldmove, newmove of Aliases.moves
if moveName is oldmove
moveName = newmove
if /Hidden Power /.test(moveName)
if !pokemon.ivs
moveName.match(/Hidden Power (.*)/i)
hiddenPowerType = RegExp.$1.trim().toLowerCase().replace(/\W+/g, '')
pokemon.ivs = HiddenPower.BW.ivs[hiddenPowerType] || {}
moveName = 'Hidden Power'
pokemon.moves ?= []
pokemon.moves.push(moveName)
return team
@PokeBattle.exportTeam = (json) ->
s = []
for pokemon in json
s.push("")
species = pokemon.species
if pokemon.forme && pokemon.forme != "default"
species += "-#{pokemon.forme[0].toUpperCase()}"
mainLine = []
if pokemon.name
mainLine.push(pokemon.name)
mainLine.push("(#{species})")
else
mainLine.push(species)
mainLine.push("(#{pokemon.gender})") if pokemon.gender
mainLine.push("@ #{pokemon.item}") if pokemon.item
s.push(mainLine.join(' '))
# Ability
s.push("Ability: #{pokemon.ability}") if pokemon.ability
# EVs
if pokemon.evs
evArray = for stat, amount of pokemon.evs when amount > 0
"#{amount} #{reverseStatsHash[stat]}"
s.push("EVs: #{evArray.join(" / ")}") if evArray.length > 0
# IVs
if pokemon.ivs
ivArray = for stat, amount of pokemon.ivs when amount < 31
"#{amount} #{reverseStatsHash[stat]}"
s.push("IVs: #{ivArray.join(" / ")}") if ivArray.length > 0
# Nature
s.push("#{pokemon.nature} nature") if pokemon.nature
# Level
s.push("Level: #{pokemon.level}") if pokemon.level && pokemon.level != 100
# Shiny
s.push("Shiny: Yes") if pokemon.shiny
# Happiness
if pokemon.happiness && pokemon.happiness != 100
s.push("Happiness: #{pokemon.happiness}")
# Moves
if pokemon.moves
s.push("- #{moveName}") for moveName in pokemon.moves
s.push("\n") # Trailing newlines, just in case.
s.join("\n")
Aliases =
moves:
"Ancient Power" : "AncientPower"
"Bubble Beam" : "BubbleBeam"
"Double Slap" : "DoubleSlap"
"Dragon Breath" : "DragonBreath"
"Dynamic Punch" : "DynamicPunch"
"Extreme Speed" : "ExtremeSpeed"
"Feint Attack" : "Faint Attack"
"Feather Dance" : "FeatherDance"
"Grass Whistle" : "GrassWhistle"
"High Jump Kick" : "Hi Jump Kick"
"Poison Powder" : "PoisonPowder"
"Sand Attack" : "Sand-Attack"
"Self-Destruct" : "Selfdestruct"
"Smelling Salts" : "SmellingSalt"
"Smokescreen" : "SmokeScreen"
"Soft-Boiled" : "Softboiled"
"Solar Beam" : "SolarBeam"
"Sonic Boom" : "SonicBoom"
"Thunder Punch" : "ThunderPunch"
"Thunder Shock" : "ThunderShock"
"Vice Grip" : "ViceGrip"
abilities:
"Compound Eyes" : "Compoundeyes"
"Lightning Rod" : "Lightningrod"
items:
"Balm Mushroom" : "BalmMushroom"
"Black Glasses" : "BlackGlasses"
"Bright Powder" : "BrightPowder"
"Deep Sea Scale" : "DeepSeaScale"
"Deep Sea Tooth" : "DeepSeaTooth"
"Energy Powder" : "EnergyPowder"
"Never-Melt Ice" : "NeverMeltIce"
"Paralyze Heal" : "Parlyz Heal"
"Rage Candy Bar" : "RageCandyBar"
"Silver Powder" : "SilverPowder"
"Thunder Stone" : "Thunderstone"
"Tiny Mushroom" : "TinyMushroom"
"Twisted Spoon" : "TwistedSpoon"
"X Defense" : "X Defend"
"X Sp. Atk" : "X Special"
statsHash =
'hp' : 'hp'
'Hp' : 'hp'
'HP' : 'hp'
'Atk' : 'attack'
'Def' : 'defense'
'SAtk' : 'specialAttack'
'SpA' : 'specialAttack'
'SDef' : 'specialDefense'
'SpD' : 'specialDefense'
'Spe' : 'speed'
'Spd' : 'speed'
reverseStatsHash =
'hp' : 'HP'
'attack' : 'Atk'
'defense' : 'Def'
'specialAttack' : 'SAtk'
'specialDefense' : 'SDef'
'speed' : 'Spe'
convertNameToSpeciesAndForme = (pokemon, species) ->
if species.match(/(Thundurus|Landorus|Tornadus)\-T(herian)?/i)
pokemon.species = RegExp.$1
pokemon.forme = 'therian'
else if species.match(/Shaymin-S(ky)?/i)
pokemon.species = "Shaymin"
pokemon.forme = 'sky'
else if species.match(/Giratina-O(rigin)?/i)
pokemon.species = "Giratina"
pokemon.forme = 'origin'
else if species.match(/Arceus(\-.*)?/)
pokemon.species = "Arceus"
else if species.match(/Kyurem-B(lack)?/)
pokemon.species = "Kyurem"
pokemon.forme = "black"
else if species.match(/Kyurem-W(hite)?/)
pokemon.species = "Kyurem"
pokemon.forme = "white"
else if species.match(/Rotom-W|Rotom-Wash/)
pokemon.species = "Rotom"
pokemon.forme = "wash"
else if species.match(/Rotom-S|Rotom-Fan/)
pokemon.species = "Rotom"
pokemon.forme = "fan"
else if species.match(/Rotom-H|Rotom-Heat/)
pokemon.species = "Rotom"
pokemon.forme = "heat"
else if species.match(/Rotom-F|Rotom-Frost/)
pokemon.species = "Rotom"
pokemon.forme = "frost"
else if species.match(/Rotom-C|Rotom-Mow/)
pokemon.species = "Rotom"
pokemon.forme = "mow"
else if species.match(/Deoxys-A|Deoxys-Attack/)
pokemon.species = "Deoxys"
pokemon.forme = "attack"
else if species.match(/Deoxys-D|Deoxys-Defense/)
pokemon.species = "Deoxys"
pokemon.forme = "defense"
else if species.match(/Deoxys-S|Deoxys-Speed/)
pokemon.species = "Deoxys"
pokemon.forme = "speed"
else if species.match(/Basculin-Blue-Striped|Basculin-A/)
pokemon.species = "Basculin"
pokemon.forme = "blue-striped"
else if species.match(/Keldeo-Resolute|Keldeo-R/)
pokemon.species = "Keldeo"
pokemon.forme = "resolute"
else if species.match(/Shellos-East/)
pokemon.species = "Shellos"
# TODO: Read east forme
pokemon.forme = "default"
else if species.match(/Gastrodon-East/)
pokemon.species = "Gastrodon"
# TODO: Read east forme
pokemon.forme = "default"
else if species.match(/Wormadam-Sandy|Wormadam-G/)
pokemon.species = "Wormadam"
pokemon.forme = "sandy"
else if species.match(/Wormadam-Trash|Wormadam-S/)
pokemon.species = "Wormadam"
pokemon.forme = "trash"
else if species.match(/Deerling-.*/)
pokemon.species = "Deerling"
# TODO: Read other formes
pokemon.forme = null
else if species.match(/Sawsbuck-.*/)
pokemon.species = "Sawsbuck"
# TODO: Read other formes
pokemon.forme = null
else if species.match(/Unown-.*/)
pokemon.species = "Unown"
# TODO: Read other formes
pokemon.forme = null
else
pokemon.species = species

View File

@ -0,0 +1,17 @@
# TODO: Move more timer functionality into here
# TODO: Encapsulate this better. Maybe put humanizeTime in a time object with more functions?
PokeBattle.humanizeTime = (unixTime) =>
unixTime = 0 if !unixTime? || unixTime < 0
seconds = Math.floor(unixTime / 1000) % 60
minutes = Math.floor(unixTime / 1000 / 60)
seconds = String(seconds)
return minutes + ":" + "00".substr(seconds.length) + seconds
$ ->
window.setInterval( ->
$(".elapsed-time").each ->
$el = $(this)
elapsedTime = Date.now() - $el.data("time-start")
$el.text(PokeBattle.humanizeTime(elapsedTime))
, 500)

View File

@ -0,0 +1,4 @@
@PokeBattle ?= {}
PokeBattle.battles = null
PokeBattle.events = {}
_.extend(PokeBattle.events, Backbone.Events)

View File

@ -0,0 +1,3 @@
PokeBattle.events.once "ready", ->
$loading = $(".loading-container")
$loading.fadeOut(-> $loading.remove())

View File

@ -0,0 +1,3 @@
# This is turned off by default, due to performance reasons.
# But we'd like to listen to this event for the teambuilder.
Backbone.Associations.EVENTS_NC = true

View File

@ -0,0 +1,210 @@
@PokeBattle.mixins.BattleProtocolParser =
update: (actions) ->
return if actions.length == 0
@notify()
hadStuff = (@updateQueue.length > 0)
@updateQueue.push(actions...)
@_update() unless hadStuff
_update: (wasAtBottom) ->
view = @view
queue = @updateQueue
return if !queue # closed battle in the middle of getting updates
if queue.length == 0
view.renderUserInfo()
view.resetPopovers()
if wasAtBottom || view.skip? then view.chatView.scrollToBottom()
if view.skip?
delete view.skip
view.$('.battle_pane').show()
return
wasAtBottom ||= view.chatView.isAtBottom()
action = queue.shift()
[ type, rest... ] = action
protocol = (key for key, value of Protocol when value == type)[0]
try
if window.localStorage.debug == 'true'
console.log "Received protocol: #{protocol} with args: #{rest}"
catch
done = () =>
return if done.called
done.called = true
if view.skip?
@_update.call(this, wasAtBottom)
else
# setTimeout 0 lets the browser breathe.
setTimeout(@_update.bind(this, wasAtBottom), 0)
doneTimeout = ->
setTimeout(done, 0)
doneSpeedTimeout = () =>
if view.skip? || view.speed <= 1
done()
else
setTimeout(done, (view.speed - 1) * 1000)
try
switch type
when Protocol.CHANGE_HP
[player, slot, newPercent] = rest
pokemon = @getPokemon(player, slot)
pokemon.set('percent', newPercent)
if view.skip? then done() else setTimeout(done, 500)
when Protocol.CHANGE_EXACT_HP
[player, slot, newHP] = rest
pokemon = @getPokemon(player, slot)
pokemon.set('hp', newHP)
done()
when Protocol.SWITCH_OUT
[player, slot] = rest
view.switchOut(player, slot, done)
when Protocol.SWITCH_IN
# TODO: Get Pokemon data, infer which Pokemon it is.
# Currently, it cheats with `fromSlot`.
[player, toSlot, fromSlot] = rest
team = @getTeam(player).get('pokemon').models
[team[toSlot], team[fromSlot]] = [team[fromSlot], team[toSlot]]
# TODO: Again, automatic.
view.switchIn(player, toSlot, fromSlot, doneSpeedTimeout)
when Protocol.CHANGE_PP
[player, slot, moveIndex, newPP] = rest
pokemon = @getPokemon(player, slot)
pokemon.setPP(moveIndex, newPP)
done()
when Protocol.REQUEST_ACTIONS
[validActions] = rest
view.enableButtons(validActions)
PokeBattle.notifyUser(PokeBattle.NotificationTypes.ACTION_REQUESTED, @id + "_" + @get('turn'))
done()
when Protocol.START_TURN
[turn] = rest
view.beginTurn(turn, doneTimeout)
when Protocol.CONTINUE_TURN
view.continueTurn(doneTimeout)
when Protocol.RAW_MESSAGE
[message] = rest
view.addLog("#{message}<br>")
done()
when Protocol.FAINT
[player, slot] = rest
view.faint(player, slot, done)
when Protocol.MAKE_MOVE
# TODO: Send move id instead
[player, slot, moveName] = rest
view.logMove(player, slot, moveName, done)
when Protocol.END_BATTLE
[winner] = rest
view.announceWinner(winner, done)
when Protocol.FORFEIT_BATTLE
[forfeiter] = rest
view.announceForfeit(forfeiter, done)
when Protocol.TIMER_WIN
[winner] = rest
view.announceTimer(winner, done)
when Protocol.BATTLE_EXPIRED
view.announceExpiration(done)
when Protocol.MOVE_SUCCESS
[player, slot, targetSlots, moveName] = rest
view.moveSuccess(player, slot, targetSlots, moveName, done)
when Protocol.CANNED_TEXT
cannedInteger = rest.splice(0, 1)
view.parseCannedText(cannedInteger, rest, done)
when Protocol.EFFECT_END
[player, slot, effect] = rest
view.endEffect(player, slot, effect, done)
when Protocol.POKEMON_ATTACH
[player, slot, attachment] = rest
view.attachPokemon(player, slot, attachment, done)
when Protocol.TEAM_ATTACH
[player, attachment] = rest
view.attachTeam(player, attachment, done)
when Protocol.BATTLE_ATTACH
[attachment] = rest
view.attachBattle(attachment, done)
when Protocol.POKEMON_UNATTACH
[player, slot, attachment] = rest
view.unattachPokemon(player, slot, attachment, done)
when Protocol.TEAM_UNATTACH
[player, attachment] = rest
view.unattachTeam(player, attachment, done)
when Protocol.BATTLE_UNATTACH
[attachment] = rest
view.unattachBattle(attachment, done)
when Protocol.INITIALIZE
# TODO: Handle non-team-preview
[teams] = rest
@receiveTeams(teams)
view.preloadImages()
if !@get('spectating')
PokeBattle.notifyUser(PokeBattle.NotificationTypes.BATTLE_STARTED, @id)
done()
when Protocol.START_BATTLE
view.removeTeamPreview()
view.renderBattle()
done()
when Protocol.REARRANGE_TEAMS
arrangements = rest
@get('teams').forEach (team, i) ->
team.rearrange(arrangements[i])
done()
when Protocol.RECEIVE_TEAM
[team] = rest
@receiveTeam(team)
done()
when Protocol.SPRITE_CHANGE
[player, slot, newSpecies, newForme] = rest
pokemon = @getPokemon(player, slot)
pokemon.set('species', newSpecies)
pokemon.set('forme', newForme)
view.changeSprite(player, slot, newSpecies, newForme, done)
when Protocol.NAME_CHANGE
[player, slot, newName] = rest
pokemon = @getPokemon(player, slot)
view.changeName(player, slot, newName, done)
when Protocol.BOOSTS
[player, slot, deltaBoosts] = rest
view.boost(player, slot, deltaBoosts, floatText: true)
done()
when Protocol.SET_BOOSTS
[player, slot, boosts] = rest
view.setBoosts(player, slot, boosts)
done()
when Protocol.RESET_BOOSTS
[player, slot] = rest
view.resetBoosts(player, slot)
done()
when Protocol.MOVESET_UPDATE
[player, slot, movesetJSON] = rest
pokemon = @getPokemon(player, slot)
pokemon.set(movesetJSON)
done()
when Protocol.WEATHER_CHANGE
[newWeather] = rest
view.changeWeather(newWeather, done)
when Protocol.TEAM_PREVIEW
view.renderTeamPreview()
done()
when Protocol.ACTIVATE_ABILITY
[player, slot, ability] = rest
pokemon = @getPokemon(player, slot)
pokemon.set('ability', ability)
view.activateAbility(player, slot, ability, done)
when Protocol.END_MOVE
doneSpeedTimeout()
when Protocol.ILLUSION_CHANGE
[player, slot, change] = rest
pokemon = @getPokemon(player, slot)
pokemon.setIllu(change)
done()
when Protocol.VISIBLE_TEAM
view.setVisibleTeam()
else
done()
catch e
console.error(e)
console.error(e.stack)
done()
if wasAtBottom && !view.chatView.isAtBottom()
view.chatView.scrollToBottom()

View File

@ -0,0 +1,2 @@
@PokeBattle ?= {}
@PokeBattle.mixins ?= {}

View File

@ -0,0 +1,85 @@
class Teams extends Backbone.Collection
model: Team
class @Battle extends Backbone.AssociatedModel
relations: [
type: Backbone.Many
key: 'teams'
relatedModel: Team
collectionType: Teams
]
defaults:
spectating: true
finished: false
_.extend(this.prototype, PokeBattle.mixins.BattleProtocolParser)
initialize: (attributes) =>
@updateQueue = []
{@numActive, spectators} = attributes
@spectators = new UserList(spectators) unless !spectators
@set('generation', Formats[@get('format')].generation)
@set('notifications', 0)
@set('turn', 0)
@set('teams', [{hidden: true}, {hidden: true}])
@set('spectating', !@has('index'))
@set('index', Math.floor(2 * Math.random())) unless @has('index')
receiveTeams: (receivedTeams) =>
teams = @get('teams')
for receivedTeam, i in receivedTeams
receivedTeam.hidden = true
team = teams.at(i)
team.set(receivedTeam) if team.get('hidden')
receiveTeam: (team) =>
teams = @get('teams')
teams.at(@get('index')).unset('hidden', silent: true).set(team)
makeMove: (moveName, forSlot, callback) =>
pokemon = @getPokemon(@get('index'), forSlot)
options = {}
options['megaEvolve'] = pokemon.get('megaEvolve') if pokemon.get('megaEvolve')
PokeBattle.primus.send(
'sendMove', @id, moveName, forSlot,
@get('turn'), options, callback,
)
makeSwitch: (toSlot, forSlot, callback) =>
PokeBattle.primus.send(
'sendSwitch', @id, toSlot, forSlot, @get('turn'), callback
)
makeCancel: =>
PokeBattle.primus.send 'sendCancelAction', @id, @get('turn')
arrangeTeam: (arrangement) =>
PokeBattle.primus.send 'arrangeTeam', @id, arrangement
switch: (fromIndex, toIndex) =>
you = @getTeam().pokemon
[you[fromIndex], you[toIndex]] = [you[toIndex], you[fromIndex]]
getTeam: (playerIndex = @get('index')) =>
@get("teams").at(playerIndex)
getOpponentTeam: (playerIndex = @get('index')) =>
@get("teams").at(1 - playerIndex)
getPokemon: (playerIndex, slot = 0) =>
team = @getTeam(playerIndex)
team.at(slot)
isPlaying: =>
!@get('finished') && !@get('spectating')
forfeit: =>
PokeBattle.primus.send('forfeit', @id)
# TODO: Opponent switch. Use some logic to determine whether the switch is
# to a previously seen Pokemon or a new Pokemon. In the latter case, we
# should reveal a previously unknown Pokeball if it's not a Wi-Fi battle.
notify: =>
@set('notifications', @get('notifications') + 1)

View File

@ -0,0 +1,350 @@
class @Pokemon extends Backbone.Model
defaults: =>
moves: []
percent: 100
ivs:
hp: 31
attack: 31
defense: 31
specialAttack: 31
specialDefense: 31
speed: 31
evs:
hp: 0
attack: 0
defense: 0
specialAttack: 0
specialDefense: 0
speed: 0
initialize: (attributes={}) ->
# History lesson: We stored species under `name`. Now that we support
# nicknames, we need the `name` freed up. However, teams are saved to the
# server using the old scheme. Therefore we need to do a simple check for
# the existence of `species`; if it exists, do nothing. If not, use `name`.
@set('species', @get('name')) if !@has('species') && @has('name')
@set('forme', 'default') unless @has('forme')
@set('illu', false)
@normalizeStats(@get('ivs'), 31)
@normalizeStats(@get('evs'), 0)
@resetBoosts()
@isNull = false
# Skip teambuilder-specific properties.
return if @get('teambuilder') != true
@on 'change:ivs', (model, ivs)=>
type = HiddenPower.BW.type(ivs).toLowerCase()
@set("hiddenPowerType", type, silent: true)
@on 'change:hiddenPowerType', (model, type) =>
hpIVs = HiddenPower.BW.ivs[type.toLowerCase()]
ivs = @get('ivs')
for stat, iv of ivs
ivs[stat] = hpIVs[stat] || 31
@set('ivs', ivs, silent: true)
@set('ability', @getAbilities()[0]) unless attributes.ability
@set('level', 100) unless attributes.level
@set('happiness', 100) if isNaN(attributes.happiness)
@set('nature', 'Hardy') unless attributes.nature
hiddenPowerType = HiddenPower.BW.type(@get('ivs')).toLowerCase()
@set('hiddenPowerType', hiddenPowerType, silent: true)
# If there is no gender set and only one possiblity, set the gender
unless @has('gender')
genders = @getGenders()
@set('gender', genders[0], silent: true) if genders.length == 1
resetBoosts: ->
@set 'stages',
hp: 0
attack: 0
defense: 0
specialAttack: 0
specialDefense: 0
speed: 0
accuracy: 0
evasion: 0
normalizeStats: (hash, defaultValue) ->
stats = [ "hp", "attack", "defense", "specialAttack",
"specialDefense", "speed"]
for stat in stats
hash[stat] ?= defaultValue
getName: ->
sanitizedName = $('<div/>').text(@get('name')).html()
sanitizedName = sanitizedName.replace(
/[\u0300-\u036F\u20D0-\u20FF\uFE20-\uFE2F]/g, '')
sanitizedName
getGeneration: (generation) ->
gen = generation || @collection?.generation || DEFAULT_GENERATION
gen = gen.toUpperCase()
window.Generations[gen]
getSpecies: ->
@getGeneration().SpeciesData[@get('species')]
getItem: ->
@getGeneration().ItemData[@get('item')]
getForme: (forme, generation) ->
forme ||= @get('forme')
@getGeneration(generation).FormeData[@get('species')]?[forme]
getFormes: ->
(forme for forme of @getGeneration().FormeData[@get('species')])
# Returns all non-battle only formes
getSelectableFormes: ->
_(@getFormes()).reject((forme) => @getForme(forme).isBattleOnly)
# Returns all mega formes
getMegaFormes: ->
_(@getFormes()).reject((forme) => forme.indexOf('mega') != 0)
getAbilities: ->
forme = @getForme()
abilities = _.clone(forme.abilities)
abilities.push(forme.hiddenAbility) if forme.hiddenAbility
_.unique(abilities)
getGenders: ->
species = @getSpecies()
genders = []
switch species.genderRatio
when -1
genders.push("Genderless")
when 0
genders.push("M")
when 8
genders.push("F")
else
genders.push("M", "F")
genders
hasSelectedMove: (moveName) ->
moveName && moveName in @moves
getMovepool: ->
{SpeciesData, MoveData} = @getGeneration()
generation = GENERATION_TO_INT[@collection?.generation || DEFAULT_GENERATION]
learnset = learnableMoves(window.Generations, @attributes, generation)
# Map each move name to a move object
return _(learnset).map (moveName) ->
console.log(moveName)
move = _(MoveData[moveName]).clone()
move['name'] = moveName
move
getTotalEVs: (options = {}) ->
total = 0
for stat, value of @get("evs")
total += value if stat != options.exclude
total
getTeam: =>
@collection?.parents[0]
setIv: (stat, value) ->
ivs = _.clone(@get("ivs"))
ivs[stat] = value
@set("ivs", ivs) # trigger change event
setEv: (stat, value) ->
evs = _.clone(@get("evs"))
value = value - (value % 4)
evs[stat] = value
@set("evs", evs) # trigger change event
value
iv: (stat) ->
@get("ivs")[stat] ? 31
ev: (stat) ->
@get("evs")[stat] ? 0
natureBoost: (stat) ->
nature = @get('nature')?.toLowerCase()
if nature of natures
natures[nature][stat] || 1
else
1
base: (key) ->
forme = @getForme()
base = forme["stats"][key]
stat: (key) ->
base = @base(key)
return 1 if base == 1 # For Shedinja. key doesn't have to be hp.
level = @get('level') || 100
iv = @iv(key)
ev = Math.floor(@ev(key) / 4)
total = if key == 'hp'
Math.floor((2 * base + iv + ev) * (level / 100) + level + 10)
else
Math.floor(((2 * base + iv + ev) * (level / 100) + 5) * @natureBoost(key))
# Returns the natures that this pokemon can use
# The natures are returned as a list of [id, value] values
# to populate a dropdown field.
# TODO: Should this be needed in more places, return Nature objects instead
getNatures: ->
natureResults = []
for nature, stats of natures
name = nature[0].toUpperCase() + nature.substr(1)
invertedStats = _(stats).invert()
label = name
if invertedStats[PLUS]
# This nature has an effect, so update the label
plusStat = statAbbreviations[invertedStats[PLUS]]
minusStat = statAbbreviations[invertedStats[MINUS]]
label = "#{name} (+#{plusStat}, -#{minusStat})"
natureResults.push [name, label]
return natureResults
getPBV: ->
gen = @getGeneration()
PokeBattle.PBV.determinePBV(gen, @attributes)
setPP: (moveIndex, newPP) ->
array = _.clone(@get('pp'))
array[moveIndex] = newPP
@set('pp', array)
getPercentHP: ->
Math.max(@get('percent'), 0)
getHPColor: ->
percent = @getPercentHP()
switch
when percent <= 20 then 'red'
when percent <= 50 then 'yellow'
else 'green'
isFainted: ->
@get('percent') <= 0
getStatus: ->
status = @get('status')
if status
"#{status[0].toUpperCase()}#{status.substr(1)}"
else
"Healthy"
canMegaEvolve: ->
# TODO: Refactor this to use getPossibleMegaForme()
# I didn't feel like making the change and testing it while implementing getPossibleMegaForme()
item = @getItem()
return false if item.type != 'megastone'
[ species, forme ] = item.mega
return false if @get('species') != species || @get('forme') != 'default'
return true
getPossibleMegaForme: ->
item = @getItem()
return null if item?.type != 'megastone'
[ species, forme ] = item.mega
return forme if @get('species') == species && @get('forme') == 'default'
return null
# Returns the complete web address to the pokedex link for this pokemon.
# For this project, this leads to our website at http://www.pokebattle.com,
# but if you want it to lead somewhere else, edit this function.
getPokedexUrl: ->
# todo: move this function to /shared, or use an actual slugify library
slugify = (str) ->
str.toLowerCase().replace(/[^a-z0-9]/g, '-').replace(/\-{2,}/g, '-')
slugSpecies = slugify(@get('species'))
slugForme = slugify(@get('forme'))
"//pokebattle.com/dex/pokemon/#{slugSpecies}/#{slugForme}"
getIllu: ->
@get('illu')
setIllu: (y) ->
@set('illu', y)
toJSON: ->
attributes = _.clone(@attributes)
delete attributes.gender if attributes.gender == 'Genderless'
delete attributes.hiddenPowerType
delete attributes.teambuilder
attributes
# TODO: These shortenings really should be stored somewhere else.
statAbbreviations =
'hp' : 'HP'
'attack' : 'Atk'
'defense' : 'Def'
'specialAttack' : 'SAtk'
'specialDefense' : 'SDef'
'speed' : 'Spe'
# A hash that keys a nature with the stats that it boosts.
# Neutral natures are ignored.
# TODO: .yml-ify these.
PLUS = 1.1
MINUS = 0.9
natures =
lonely: {attack: PLUS, defense: MINUS}
brave: {attack: PLUS, speed: MINUS}
adamant: {attack: PLUS, specialAttack: MINUS}
naughty: {attack: PLUS, specialDefense: MINUS}
bold: {defense: PLUS, attack: MINUS}
relaxed: {defense: PLUS, speed: MINUS}
impish: {defense: PLUS, specialAttack: MINUS}
lax: {defense: PLUS, specialDefense: MINUS}
timid: {speed: PLUS, attack: MINUS}
hasty: {speed: PLUS, defense: MINUS}
jolly: {speed: PLUS, specialAttack: MINUS}
naive: {speed: PLUS, specialDefense: MINUS}
modest: {specialAttack: PLUS, attack: MINUS}
mild: {specialAttack: PLUS, defense: MINUS}
quiet: {specialAttack: PLUS, speed: MINUS}
rash: {specialAttack: PLUS, specialDefense: MINUS}
calm: {specialDefense: PLUS, attack: MINUS}
gentle: {specialDefense: PLUS, defense: MINUS}
sassy: {specialDefense: PLUS, speed: MINUS}
careful: {specialDefense: PLUS, specialAttack: MINUS}
hardy: {}
docile: {}
serious: {}
bashful: {}
quirky: {}
class @NullPokemon extends Pokemon
initialize: ->
@set('species', null)
@set('forme', 'default')
@isNull = true
getNatures: -> []
getPBV: -> 0
base: -> 0
stat: -> null
iv: -> null
ev: -> null
getSpecies: ->
id: 0
genderRatio: -1
generation: 1
getForme: ->
@getFormes()['default']
getFormes: ->
default:
abilities: []
hiddenAbility: null
isBattleOnly: false
learnset: {}
types: []

View File

@ -0,0 +1,128 @@
class PokemonCollection extends Backbone.Collection
model: (attrs, options) =>
# History lesson: We stored species under `name`. Now that we support
# nicknames, we need the `name` freed up. However, teams are saved to the
# server using the old scheme. Therefore we need to do a simple check for
# the existence of `species`; if it exists, do nothing. If not, use `name`.
if attrs.name || attrs.species
attrs.teambuilder = @parents[0].get('teambuilder')
return new Pokemon(attrs, options)
else
return new NullPokemon()
class @Team extends Backbone.AssociatedModel
relations: [
type: Backbone.Many
key: 'pokemon'
collectionType: PokemonCollection
]
initialize: (attrs={}, options={}) =>
@owner = attrs.owner
@set('generation', DEFAULT_GENERATION) unless attrs.generation
@set('teambuilder', true) if options.teambuilder
@set('pokemon', []) unless attrs.pokemon
getName: =>
@get('name') || "Untitled team"
toJSON: =>
json = {}
json['id'] = @id if @id
json['name'] = @get('name')
json['generation'] = @get('generation')
json['pokemon'] = @get('pokemon').toJSON()
json
# Returns the pokemon at a particular index. Delegates to the internal pokemon collection
at: (idx) => @get('pokemon').at(idx)
# Returns which index the pokemon is in
indexOf: (idx) => @get('pokemon').indexOf(idx)
# Replace a pokemon at a particular index for another
replace: (idx, newPokemon) =>
@get('pokemon').remove(@get('pokemon').at(idx))
@get('pokemon').add(newPokemon, at: idx)
# Equivalent to toJSON, but omits NullPokemon
toNonNullJSON: =>
id: @id
name: @get('name')
generation: @get('generation')
pokemon: @get('pokemon')
.reject((pokemon) -> pokemon.isNull)
.map((pokemon) -> pokemon.toJSON())
clone: =>
attrs = _(@attributes).clone()
attrs.pokemon = @get('pokemon').toJSON()
new Team(attrs)
rearrange: (arrangement) ->
pokemon = @get('pokemon')
pokemon.reset((pokemon.models[index] for index in arrangement))
return true
getFormat: =>
format = @get('generation') # TODO: Migrate to format
format = DEFAULT_FORMAT if format not of Formats
Formats[format]
getGeneration: (generation) ->
gen = generation || @getFormat().generation
gen = gen.toUpperCase()
window.Generations[gen]
getPBV: =>
gen = @getGeneration()
pokemon = @get('pokemon').toJSON()
PokeBattle.PBV.determinePBV(gen, pokemon)
getMaxPBV: =>
{conditions} = @getFormat()
if Conditions.PBV_1000 in conditions
1000
else if Conditions.PBV_500 in conditions
500
else
0
hasPBV: =>
@getMaxPBV() > 0
getNonNullPokemon: =>
@get('pokemon').where(isNull: false)
hasNonNullPokemon: =>
@get('pokemon').some((pokemon) -> not pokemon.isNull)
sync: (method) =>
switch method
when 'create', 'patch', 'update'
@set('saving', true, silent: true)
@trigger('remoteSync', this)
PokeBattle.primus.send 'saveTeam', @toJSON(), (id) =>
# Note: If this model is saved multiple times, then this won't
# tell you if some of the saves failed.
@set('id', id)
@set('saving', false, silent: true)
@trigger('remoteSync', this)
when 'delete'
PokeBattle.primus.send('destroyTeam', @id) if @id
getRandomOrder: =>
order = @get('randomorder')
if typeof order is 'undefined'
@setRandomOrder()
order = @get('randomorder')
order
else
order = @get('randomorder')
order
setRandomOrder: =>
#random order for the icons with team preview when in battle
team = @get('pokemon').toJSON()
teamshuffled = _.shuffle(team)
@set('randomorder', teamshuffled)

View File

@ -0,0 +1,50 @@
MAX_LOG_LENGTH = 50
class @PrivateMessage extends Backbone.Model
initialize: =>
@loadLog()
@set('notifications', 0)
add: (username, message, opts = {}) =>
@set('notifications', @get('notifications') + 1) if username == @id
@trigger("receive", this, @id, username, message, opts)
log = @get('log')
log.push({username, message, opts})
# Trim the log size. Use 2x log length to reduce how often this happens
if log.length > (2 * MAX_LOG_LENGTH)
log.splice(0, log.length - MAX_LOG_LENGTH)
@saveLog()
openChallenge: (args...) =>
@trigger("openChallenge", args...)
cancelChallenge: (args...) =>
@trigger("cancelChallenge", args...)
closeChallenge: (args...) =>
@trigger("closeChallenge", args...)
getLog: =>
log = @get('log')
if log.length > 50
log.splice(0, log.length - 50)
return log
loadLog: =>
try
log = JSON.parse(window.localStorage.getItem(@logKey())) || []
@set('log', log)
catch
@set('log', [])
saveLog: =>
try
window.localStorage.setItem(@logKey(), JSON.stringify(@getLog()))
logKey: =>
key = [ @id, PokeBattle.username ]
key.sort()
key.join(':')

View File

@ -0,0 +1,20 @@
class @Room extends Backbone.AssociatedModel
relations: [
type: Backbone.Many
key: 'users'
relatedModel: 'User'
collectionType: 'UserList'
]
EVENTS: "userMessage rawMessage announce clear setTopic".split(/\s+/)
for eventName in this::EVENTS
do (eventName) =>
this::[eventName] = (args...) ->
@trigger(eventName, args...)
sendChat: (message) ->
return false unless message?.replace(/\s+$/).length > 0
if !PokeBattle.commands.execute(this, message)
PokeBattle.primus.send('sendChat', @id, message)
return true

View File

@ -0,0 +1,16 @@
AuthorityMap =
"1": ""
"2": "+"
"3": "%"
"4": "@"
"5": "~"
class @User extends Backbone.Model
initialize: (attributes) =>
getDisplayName: =>
authorityString = AuthorityMap[@get('authority')] ? ""
"#{authorityString}#{@get('id')}"
isAlt: =>
@get('isAlt')

View File

@ -0,0 +1,19 @@
class @Replay extends Backbone.Model
urlRoot: '/replays'
getFormat: ->
window.Formats[@get('format')].humanName
getCreatedAt: ->
date = new Date(@get('created_at'))
day = date.getDate()
month = date.getMonth() + 1
year = date.getFullYear()
timeOfDay = 'a.m.'
hours = date.getHours()
minutes = "00#{date.getMinutes()}"[-2...]
seconds = "00#{date.getSeconds()}"[-2...]
if hours > 12
hours = hours % 12
timeOfDay = 'p.m.'
"#{year}/#{month}/#{day} #{hours}:#{minutes}:#{seconds} #{timeOfDay}"

View File

@ -0,0 +1,14 @@
class @BattleListView extends Backbone.View
template: JST['battle_list']
initialize: (attributes) =>
@battles = []
@render()
refreshList: =>
PokeBattle.primus.send "getBattleList", (battles) =>
@battles = _(battles).sortBy((battle) => battle[3])
@render()
render: =>
@$el.html @template(battles: @battles)

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,244 @@
class @ChatView extends Backbone.View
template: JST['chat']
userListTemplate: JST['user_list']
events:
'click': 'focusChat'
'keydown .chat_input': 'handleKeys'
'click .chat_input_send': 'sendChat'
'scroll_to_bottom': 'scrollToBottom'
MAX_USERNAME_HISTORY = 10
MAX_MESSAGES_LENGTH = 500
# Takes a `model`, which is a Room instance.
initialize: (options) =>
{@noisy} = options
if @model
@listenTo(@model.get('users'), 'add remove reset', @renderUserList)
if @noisy
@listenTo(@model.get('users'), 'add', @userJoin)
@listenTo(@model.get('users'), 'remove', @userLeave)
for eventName in Room::EVENTS
callback = this[eventName] || throw new Error("ChatView must implement #{eventName}.")
@listenTo(@model, eventName, callback)
@chatHistory = []
@mostRecentNames = []
@tabCompleteIndex = -1
@tabCompleteNames = []
# Sets the channel topic
setTopic: (topic) =>
topic = @sanitize(topic)
@rawMessage("<div class='alert alert-info'><b>Topic:</b> #{topic}</div>")
render: =>
@$el.html @template()
if @model
@$el.removeClass('without_spectators')
@$el.removeClass('without_chat_input')
@renderUserList()
this
renderUserList: =>
@$('.user_count').text "Users (#{@model.get('users').length})"
@$('.users').html @userListTemplate(userList: @model.get('users').models)
this
getSelectedText: =>
text = ""
if window.getSelection
text = window.getSelection().toString()
else if document.selection && document.selection.type != "Control"
text = document.selection.createRange().text
return text
focusChat: =>
selectedText = @getSelectedText()
@$('.chat_input').focus() if selectedText.length == 0
sendChat: =>
$this = @$('.chat_input')
message = $this.val()
if @model.sendChat(message)
@chatHistory.push(message)
delete @chatHistoryIndex
$this.val('')
tabComplete: ($input, options = {}) =>
cursorIndex = $input.prop('selectionStart')
text = $input.val()
if @tabCompleteNames.length > 0 && @tabCompleteCursorIndex == cursorIndex
if options.reverse
@tabCompleteIndex -= 1
if @tabCompleteIndex < 0
@tabCompleteIndex = @tabCompleteNames.length - 1
else
@tabCompleteIndex = (@tabCompleteIndex + 1) % @tabCompleteNames.length
else
delete @tabCompleteCursorIndex
pieces = text[0...cursorIndex].split(' ')
possibleName = pieces.pop()
rest = pieces.join(' ')
rest += ' ' if pieces.length > 0 # Append a space if a word exists
length = possibleName.length
return if length == 0
candidates = _.union(@mostRecentNames, @model.get('users').pluck('id'))
candidates = candidates.filter (name) ->
name[...length].toLowerCase() == possibleName.toLowerCase()
return if candidates.length == 0
if options.reverse
@tabCompleteIndex = candidates.length - 1
else
@tabCompleteIndex = 0
@tabCompleteNames = candidates
@tabCompletePrefix = rest
@tabCompleteCursorIndex = cursorIndex
tabbedName = @tabCompleteNames[@tabCompleteIndex]
newPrefix = @tabCompletePrefix + tabbedName
newPrefixLength = newPrefix.length
$input.val(newPrefix + text[cursorIndex...])
$input[0].setSelectionRange(newPrefixLength, newPrefixLength)
@tabCompleteCursorIndex = newPrefixLength
handleKeys: (e) =>
$input = $(e.currentTarget)
switch e.which
when 13 # [Enter]
e.preventDefault()
@sendChat()
when 9 # [Tab]
e.preventDefault()
@tabComplete($input, reverse: e.shiftKey)
when 38 # [Up arrow]
e.preventDefault()
return if @chatHistory.length == 0
if !@chatHistoryIndex?
@chatHistoryIndex = @chatHistory.length
@chatHistoryText = $input.val()
if @chatHistoryIndex > 0
@chatHistoryIndex -= 1
$input.val(@chatHistory[@chatHistoryIndex])
when 40 # [Down arrow]
e.preventDefault()
return unless @chatHistoryIndex?
@chatHistoryIndex += 1
if @chatHistoryIndex == @chatHistory.length
$input.val(@chatHistoryText)
delete @chatHistoryIndex
else
$input.val(@chatHistory[@chatHistoryIndex])
userMessage: (username, message) =>
user = @model.get('users').get(username)
displayName = user?.getDisplayName() || username
yourName = PokeBattle.username
highlight = (new RegExp("\\b#{yourName}\\b", 'i').test(message))
# Render the chat message
u = "<b class='open_pm fake_link' data-user-id='#{username}'
style='color: #{@userColor(username)}'>#{displayName}:</b>"
@rawMessage("#{@timestamp()} #{u} #{@sanitize(message)}", {highlight})
# We might want to run something based on the message, e.g. !pbv from a mod.
@handleMessage(user, message)
# Record last few usernames who chatted
index = @mostRecentNames.indexOf(username)
@mostRecentNames.splice(index, 1) if index != -1
@mostRecentNames.push(username)
@mostRecentNames.shift() if @mostRecentNames.length > MAX_USERNAME_HISTORY
userColor: (username) =>
# Same hashing algorithm as in Java
hash = 0
for c, i in username
chr = username.charCodeAt(i)
hash = ((hash << 5) - hash) + chr
hash |= 0
h = hash % 360
hash /= 360
s = (hash % 25) + 75
l = 50
"hsl(#{h}, #{s}%, #{l}%)"
handleMessage: (user, message) =>
authority = user?.get('authority')
printableCommands = ['/pbv', '/data']
# TODO: no magic constants. '1' is a regular user.
if authority > 1 && message.split(/\s/, 1)[0] in printableCommands
PokeBattle.commands.execute(@model, message)
userJoin: (user) =>
@rawMessage("#{@timestamp()} #{user.id} joined!")
userLeave: (user) =>
@rawMessage("#{@timestamp()} #{user.id} left!")
rawMessage: (message, options = {}) =>
wasAtBottom = @isAtBottom()
klass = []
klass.push('bg-blue') if options.highlight
klass.push(options.class) if options.class
@print("<p class='chat_message #{klass.join(' ')}'>#{message}</p>")
@cleanChat()
if wasAtBottom then @scrollToBottom()
cleanChat: =>
$messages = @$('.chat_message')
numToRemove = ($messages.length - MAX_MESSAGES_LENGTH)
if numToRemove > 0
$messages.slice(0, numToRemove).remove()
announce: (klass, message) =>
wasAtBottom = @isAtBottom()
message = @linkify(message)
@print("<div class='alert alert-#{klass} clearfix'>#{message}</div>")
if wasAtBottom then @scrollToBottom()
print: (message) =>
@$('.messages').append(message)
clear: =>
@$('.messages').empty()
timestamp: =>
date = new Date()
hours = date.getHours()
minutes = date.getMinutes()
seconds = date.getSeconds()
minutes = "00#{minutes}".substr(-2)
seconds = "00#{seconds}".substr(-2)
"<span class='monospace'>[#{hours}:#{minutes}:#{seconds}]</span>"
# Escapes all HTML, but also converts links to clickable links.
sanitize: (message) =>
sanitizedMessage = $('<div/>').text(message).html()
@linkify(sanitizedMessage)
linkify: (message) =>
message = URI.withinString message, (url) ->
uri = URI(url)
[host, path] = [uri.host(), uri.path()]
battleRegex = /^\/battles\/([a-fA-F0-9]+)$/i
$a = $("<a/>").prop('href', url).prop('target', '_blank').text(url)
if host == URI(window.location.href).host() && battleRegex.test(path)
battleId = path.match(battleRegex)[1]
$a.addClass('spectate').attr('data-battle-id', battleId)
return $a.wrap("<div/>").parent().html()
message
# Returns true if the chat is scrolled to the bottom of the screen.
# This also returns true if the messages are hidden.
isAtBottom: =>
$el = @$('.messages')
($el[0].scrollHeight - $el.scrollTop() <= $el.outerHeight())
scrollToBottom: =>
messages = @$('.messages')[0]
messages.scrollTop = messages.scrollHeight
false

View File

@ -0,0 +1,259 @@
class @PrivateMessagesView extends Backbone.View
messageTemplate: JST['private_message']
events:
"keypress .chat_input" : "keyPressEvent"
"keyup .chat_input" : "keyUpEvent"
"click .challenge_button, .cancel_challenge" : "toggleChallengeEvent"
"click .popup_messages" : "focusChatEvent"
"click .title_minimize" : "minimizePopupEvent"
"click .title_close" : "closePopupEvent"
"challenge .popup" : "sendChallengeEvent"
"cancelChallenge .popup" : "challengeCanceledEvent"
"focus .popup" : "focusPopupEvent"
initialize: =>
@listenTo(@collection, 'open', @createPopup)
@listenTo(@collection, 'focus', @focusPopup)
@listenTo(@collection, 'receive', @receiveMessage)
@listenTo(@collection, 'close', @closePopup)
@listenTo(@collection, 'minimize', @minimizePopup)
@listenTo(@collection, 'show', @showPopup)
@listenTo(@collection, 'openChallenge', @openChallenge)
@listenTo(@collection, 'cancelChallenge', @cancelChallenge)
@listenTo(@collection, 'closeChallenge', @closeChallenge)
@listenTo(@collection, 'focus show', @resetNotifications)
# @listenTo(PokeBattle.userList, 'add', @notifyJoin)
# @listenTo(PokeBattle.userList, 'remove', @notifyLeave)
createPopup: (message) =>
title = id = message.id
$html = @$findPopup(id)
if @$findPopup(id).length == 0
$html = $(@messageTemplate({window, id, title}))
@$el.append($html)
@positionPopup($html, @$(".popup:visible").length - 1)
@addLogMessages($html, message.getLog())
$html
focusPopup: (message) =>
id = message.id
$popup = @$findPopup(id)
$popup.find('.chat_input').focus()
closePopup: (message) =>
username = message.id
@$findPopup(username).remove()
@repositionPopups()
minimizePopup: (message) =>
username = message.id
$popup = @$findPopup(username)
$popup.addClass('hidden')
@repositionPopups()
showPopup: (message) =>
username = message.id
$popup = @$findPopup(username)
@$el.append($popup)
$popup.removeClass('hidden')
@scrollToBottom($popup)
@repositionPopups()
addMessage: ($popup, message) =>
$messages = $popup.find('.popup_messages')
$messages.append(message)
# todo: make this and receiveMessage construct messages from a common source
addLogMessages: ($popup, log) =>
messageHtml = ""
for {username, message, opts} in log
message = _.escape(message)
username = "Me" if username == PokeBattle.username
if opts.type in [ 'error', 'alert' ]
messageHtml += "<p class='grey'>#{message}</p>"
else
messageHtml += "<p class='grey'><strong>#{username}:</strong> #{message}</p>"
@addMessage($popup, messageHtml)
@scrollToBottom($popup)
# todo: make this and addLogMessages construct messages from a common source
receiveMessage: (messageModel, messageId, username, message, options) =>
message = _.escape(message)
$popup = @$findOrCreatePopup(messageId)
wasAtBottom = @isAtBottom($popup)
username = "Me" if username == PokeBattle.username
if options.type == 'error'
@addMessage($popup, "<p class='red italic'>#{message}</p>")
else if options.type == 'alert'
@addMessage($popup, "<p class='yellow italic'>#{message}</p>")
else
if username != "Me" && !$popup.find('.chat_input').is(":focus")
$popup.addClass('new_message')
PokeBattle.notifyUser(PokeBattle.NotificationTypes.PRIVATE_MESSAGE, username)
else
@resetNotifications(messageModel)
@addMessage($popup, "<p><strong>#{username}:</strong> #{message}</p>")
if wasAtBottom then @scrollToBottom($popup)
openChallenge: (messageId, generation, conditions) =>
$popup = @$findOrCreatePopup(messageId)
$challenge = @createChallenge($popup, generation, conditions)
if generation
$challenge.find('.is_not_challenger').addClass('hidden')
$challenge.find('.is_challenger').removeClass('hidden')
cancelChallenge: (messageId) =>
$popup = @$findOrCreatePopup(messageId)
$challenge = $popup.find('.challenge')
$challenge.find('.icon-spinner').addClass('hidden')
$challenge.find('.send_challenge, .select').removeClass('disabled')
$challenge.find('.challenge_text').text("Challenge")
$challenge.find(".cancel_challenge").text('Close')
closeChallenge: (messageId) =>
$popup = @$findOrCreatePopup(messageId)
$challenge = $popup.find('.challenge')
$challenge.addClass('hidden')
$popup.find('.popup_messages').removeClass('small')
resetNotifications: (message) =>
message.set('notifications', 0)
notifyJoin: (user) =>
message = @collection.get(user.id)
return unless @isOpen(message)
message?.add(user.id, "#{user.id} is now online!", type: "alert")
notifyLeave: (user) =>
message = @collection.get(user.id)
return unless @isOpen(message)
message?.add(user.id, "#{user.id} is now offline.", type: "alert")
isOpen: (message) =>
message && @$findPopup(message.id).length > 0
# Returns true if the chat is scrolled to the bottom of the screen.
# This also returns true if the messages are hidden.
isAtBottom: ($popup) =>
$el = $popup.find('.popup_messages')
($el[0].scrollHeight - $el.scrollTop() <= $el.outerHeight())
scrollToBottom: ($popup) =>
messages = $popup.find('.popup_messages')[0]
return unless messages
messages.scrollTop = messages.scrollHeight
false
positionPopup: ($popup, index) =>
leftOffset = $('#content').position().left
$popup.css(left: leftOffset + index * $popup.outerWidth(true))
repositionPopups: =>
@$(".popup:visible").each (index, self) =>
@positionPopup($(self), index)
$findPopup: (id) =>
@$(".popup[data-user-id='#{id}']")
$findOrCreatePopup: (messageId) =>
$popup = @$findPopup(messageId)
$popup = @createPopup(@collection.get(messageId)) if $popup.length == 0
$popup
$closestPopup: (target) =>
$target = $(target)
return $target if $target.hasClass("popup")
return $target.closest(".popup")
messageFromPopup: (target) =>
$popup = @$closestPopup(target)
message = @collection.get($popup.data('user-id'))
return message
createChallenge: ($popup, generation, conditions) =>
$challenge = $popup.find('.challenge')
$challenge.html(JST['challenge']())
createChallengePane
eventName: "challenge"
button: $popup.find('.send_challenge')
acceptButton: $popup.find('.accept_challenge')
rejectButton: $popup.find('.reject_challenge')
populate: $popup.find(".challenge_data")
generation: generation
personId: $popup.data('user-id')
defaultClauses: conditions || [
Conditions.TEAM_PREVIEW
Conditions.PBV_1000
Conditions.SLEEP_CLAUSE
Conditions.EVASION_CLAUSE
Conditions.SPECIES_CLAUSE
Conditions.OHKO_CLAUSE
Conditions.PRANKSTER_SWAGGER_CLAUSE
Conditions.UNRELEASED_BAN
]
blockedClauses: conditions? || [Conditions.RATED_BATTLE]
$popup.find('.popup_messages').addClass('small')
$challenge.removeClass('hidden')
$challenge
##########
# EVENTS #
##########
keyPressEvent: (e) =>
switch e.which
when 13 # [ Enter ]
$input = $(e.currentTarget)
message = @messageFromPopup(e.currentTarget)
text = $input.val()
return if text.length == 0
PokeBattle.primus.send('privateMessage', message.id, text)
$input.val('')
keyUpEvent: (e) =>
switch e.which
when 27 # [ Esc ]
@closePopupEvent(e)
minimizePopupEvent: (e) =>
message = @messageFromPopup(e.currentTarget)
message.trigger('minimize', message)
closePopupEvent: (e) =>
message = @messageFromPopup(e.currentTarget)
message.trigger('close', message)
focusChatEvent: (e) =>
@$closestPopup(e.currentTarget).find('input').focus()
toggleChallengeEvent: (e) =>
$popup = @$closestPopup(e.currentTarget)
$challenge = $popup.find('.challenge')
wasAtBottom = @isAtBottom($popup)
if $challenge.hasClass("hidden")
@createChallenge($popup)
else if $challenge.find('.cancel_challenge').text() == 'Cancel'
$popup.find('.send_challenge').click()
else
@closeChallenge(@messageFromPopup($popup))
if wasAtBottom then @scrollToBottom($popup)
sendChallengeEvent: (e) =>
$popup = @$closestPopup(e.currentTarget)
$challenge = $popup.find('.challenge')
$challenge.find(".icon-spinner").removeClass('hidden')
$challenge.find(".challenge_text").text('Challenging...')
$challenge.find(".cancel_challenge").text('Cancel')
challengeCanceledEvent: (e) =>
message = @messageFromPopup(e.currentTarget)
message.trigger('cancelChallenge', message.id)
focusPopupEvent: (e) =>
$popup = @$closestPopup(e.currentTarget)
$popup.removeClass('new_message')
@resetNotifications(@collection.get($popup.data('user-id')))

View File

@ -0,0 +1,34 @@
class @ReplayView extends Backbone.View
replayTemplate: JST['replay']
events:
'click .delete-replay': 'deleteReplay'
render: =>
@$el.empty()
templates = @collection.map((replay) => @replayTemplate({window, replay}))
groups = for x in [0...templates.length] by 3
_.compact([ templates[x], templates[x + 1], templates[x + 2] ])
for groupHTML in groups
$row = $('<div/>').addClass('row-fluid')
$row.append(groupHTML)
$row.appendTo(@$el)
if @collection.length == 0
@$el.append($("<p/>").text("You have not saved any replays."))
this
deleteReplay: (e) =>
return unless confirm("Do you really want to delete this replay?")
$target = $(e.currentTarget)
$spinner = $target.closest('.clickable-box').find('.show_spinner')
$spinner.removeClass('hidden')
cid = $target.data('cid')
replay = @collection.get(cid)
replay
.destroy()
.complete(@render)

View File

@ -0,0 +1,188 @@
class @SidebarView extends Backbone.View
template: JST['navigation']
events:
"click .logo" : "focusLobbyEvent"
"click .nav_rooms li" : 'focusRoomEvent'
"click .nav_battles li" : 'focusBattleEvent'
"click .nav_messages li": 'focusMessageEvent'
"click .nav_battles .close" : 'leaveRoomEvent'
"click .nav_messages .close" : 'closeMessageEvent'
"click .nav_teambuilder": 'showTeambuilder'
"click .nav_battle_list": 'showBattleList'
initialize: (attributes) =>
@currentWindow = null
@listenTo(PokeBattle.battles, 'add', @addBattle)
@listenTo(PokeBattle.battles, 'remove', @removeBattle)
@listenTo(PokeBattle.battles, 'reset', @resetBattles)
@listenTo(PokeBattle.battles, 'change:notifications', @renderNotifications)
@listenTo(PokeBattle.messages, 'open receive', @addMessage)
@listenTo(PokeBattle.messages, 'close', @removeMessage)
@listenTo(PokeBattle.messages, 'reset', @resetMessages)
@listenTo(PokeBattle.messages, 'change:notifications', @renderMessageNotifications)
@render()
showTeambuilder: =>
@changeWindowTo($("#teambuilder-section"), $(".nav_teambuilder"))
showBattleList: =>
@changeWindowTo($("#battle-list-section"), $(".nav_battle_list"))
PokeBattle.battleList.refreshList()
render: =>
@$el.html @template(battles: PokeBattle.battles)
renderNotifications: (battle) =>
$notifications = @$("[data-battle-id='#{battle.id}'] .notifications")
# We don't want to display notifications if this window is already focused.
if @currentWindow.data('battle-id') == battle.id
battle.set('notifications', 0, silent: true)
$notifications.addClass('hidden')
return
# Show notification count.
notificationCount = battle.get('notifications')
if notificationCount > 0
$notifications.text(notificationCount)
$notifications.removeClass('hidden')
else
$notifications.addClass('hidden')
addBattle: (battle) =>
@$(".header_battles, .nav_battles").removeClass("hidden")
$li = $("""<li class="nav_item fake_link" data-battle-id="#{battle.id}">
<div class="nav_meta">
<div class="notifications hidden">0</div>
<div class="close">&times;</div>
</div>#{battle.get('playerIds').join(' VS ')}</li>""")
$li.appendTo(@$('.nav_battles'))
$li.click()
removeBattle: (battle) =>
$navItems = @$(".nav_item")
$battle = @$(".nav_item[data-battle-id='#{battle.id}']")
index = $navItems.index($battle)
$battle.remove()
if PokeBattle.battles.size() == 0
@$(".header_battles, .nav_battles").addClass('hidden')
PokeBattle.navigation.focusLobby()
else
$next = $navItems.eq(index).add($navItems.eq(index - 1))
$next.first().click()
resetBattles: (battles) =>
for battle in battles
@addBattle(battle)
addMessage: (message) =>
# This event can trigger on already opened messages, so we need to verify
return if @$(".nav_item[data-message-id='#{message.id}']").length
@$(".header_messages, .nav_messages").removeClass("hidden")
$li = $("""<li class="nav_item fake_link" data-message-id="#{message.id}">
<div class="nav_meta">
<div class="notifications hidden">0</div>
<div class="close">&times;</div>
</div>#{message.id}</li>""")
$li.appendTo(@$('.nav_messages'))
@renderMessageNotifications(message)
removeMessage: (message) =>
@$(".nav_item[data-message-id='#{message.id}']").remove()
# If there are no messages, remove the header
# Note: We can't check the collection directly since messages are never actually removed from it
if @$('.nav_messages li').length == 0
@$(".header_messages").addClass("hidden")
resetMessages: (messages) =>
@addMessage(message) for message in messages
renderMessageNotifications: (message) =>
$notifications = @$("[data-message-id='#{message.id}'] .notifications")
notificationCount = message.get('notifications')
if notificationCount > 0
$notifications.text(notificationCount)
$notifications.removeClass('hidden')
else
$notifications.addClass('hidden')
focusLobby: =>
# TODO: Clean this up once rooms are implemented
# right now it duplicates part of focusRoom()
$lobbyLink = @$(".nav_rooms li").first()
@resetNotifications($lobbyLink)
$room = $('.chat_window')
@changeWindowTo($room, $lobbyLink)
PokeBattle.router.navigate("")
leaveRoomEvent: (e) =>
$navItem = $(e.currentTarget).closest('.nav_item')
battleId = $navItem.data('battle-id')
battle = PokeBattle.battles.get(battleId)
if battle.isPlaying()
return if !confirm("Are you sure you want to forfeit this battle?")
battle.forfeit()
PokeBattle.battles.remove(battle)
false
closeMessageEvent: (e) =>
$navItem = $(e.currentTarget).closest('.nav_item')
messageId = $navItem.data('message-id')
message = PokeBattle.messages.get(messageId)
message.trigger('close', message)
focusBattleEvent: (e) =>
$this = $(e.currentTarget)
@resetNotifications($this)
battleId = $this.data('battle-id')
@changeWindowToBattle(battleId)
focusLobbyEvent: (e) =>
@focusLobby()
focusRoomEvent: (e) =>
$this = $(e.currentTarget)
@resetNotifications($this)
# TODO: Remove hardcoding once rooms are implemented
$room = $('.chat_window')
@changeWindowTo($room, $this)
PokeBattle.router.navigate("")
focusMessageEvent: (e) =>
$navItem = $(e.currentTarget).closest('.nav_item')
messageId = $navItem.data('message-id')
message = PokeBattle.messages.get(messageId)
message.trigger('show', message)
message.trigger('focus', message)
changeWindowTo: ($toSelector, $navItem) =>
# Show window, hide others
$mainContent = $('#main-section')
$mainContent.children().addClass("hidden")
@currentWindow = $toSelector.first()
@currentWindow.removeClass("hidden")
@currentWindow.find('.chat').trigger('scroll_to_bottom')
# Add .active to navigation, remove from others
@$('.nav_item').removeClass('active')
$navItem.addClass('active')
changeWindowToBattle: (battleId) =>
$battle = $(""".battle_window[data-battle-id='#{battleId}']""")
$navItem = @$("[data-battle-id='#{battleId}']")
@changeWindowTo($battle, $navItem)
PokeBattle.router.navigate("battles/#{battleId}")
resetNotifications: ($link) =>
$link = $link.first()
$link = $link.closest('li') if $link[0].tagName != 'li'
if battleId = $link.data('battle-id')
battle = PokeBattle.battles.get(battleId)
battle.set('notifications', 0)

View File

@ -0,0 +1,484 @@
isMobileOrAndroid = ->
return true if /Mobile/i.test(window.navigator.userAgent)
return true if /Android/i.test(window.navigator.userAgent)
return false
# helper which attaches selectize
attachSelectize = ($element, options) ->
# Block selectize on mobile and all android operating systems (All androids are blocked due to a bug)
return if isMobileOrAndroid()
$element.selectize(options)
setSelectizeValue = ($element, value) ->
if isMobileOrAndroid()
$element.val(value)
else
$element.each ->
@selectize?.setValue(value)
setSelectizeDisabled = ($element, disabled) ->
$element.filter(".selectized").each ->
return unless @selectize
if disabled then @selectize.disable() else @selectize.enable()
class @PokemonEditView extends Backbone.View
editTemplate: JST['teambuilder/pokemon']
speciesTemplate: JST['teambuilder/species']
nonStatsTemplate: JST['teambuilder/non_stats']
movesTemplate: JST['teambuilder/moves']
events:
'change .sortSpecies': 'changeSort'
'change .species_list': 'changeSpecies'
'change .selected_nickname': 'changeNickname'
'click .selected_shininess': 'changeShiny'
'click .selected_happiness': 'changeHappiness'
'change .selected-forme': 'changeForme'
'change .selected_nature': 'changeNature'
'change .selected_ability': 'changeAbility'
'change .selected_item': 'changeItem'
'change .selected_gender': 'changeGender'
'change .selected_level': 'changeLevel'
'change .iv-entry': 'changeIv'
'focus .ev-entry': 'focusEv'
'blur .ev-entry': 'changeEv'
'change .ev-entry': 'changeEv'
'input .ev-entry[type=range]': 'changeEv' # fix for firefox
'change .select-hidden-power': 'changeHiddenPower'
'keydown .selected_moves input': 'keydownMoves'
'blur .selected_moves input': 'blurMoves'
'click .table-moves tbody tr': 'clickMoveName'
'mousedown .table-moves': 'preventBlurMoves'
'click .move-button': 'clickSelectedMove'
'click .move-button .close': 'removeSelectedMove'
initialize: (attributes={}) =>
@onPokemonChange = attributes.onPokemonChange
setFormat: (format) =>
format = Formats[format] || Formats[DEFAULT_FORMAT]
@setGeneration(format.generation)
# TODO: Set PBV limit based on conditions
changeSort:(e) =>
sort = $(e.currentTarget).val()
console.log(sort)
if sort =="Default Sort"
@sortSpecieslist("Default")
else if sort == "Sort by Dexnumber"
@sortSpecieslist("id", false)
else if sort == "Invert by Dexnumber"
@sortSpecieslist("id", true)
else if sort == "Sort Alphabetically"
@sortSpecieslist("pokename", false)
else if sort == "Invert Alphabetically"
@sortSpecieslist("pokename", true)
sortSpecieslist: (option, reverse) =>
{MoveData, SpeciesData, ItemData} = @generation
if option == "Default"
sortedlist = @getSpecies
else
sortedlist = @sortObject(SpeciesData, option, reverse)
@speciesList = (species for species, data of sortedlist)
@render()
sortObject: (data, option, reverse) ->
arr = []
for key, val of data
val.pokename = key
arr.push(val)
arr = _.sortBy(arr, option)
if reverse == true
arr.reverse()
newobj = {}
for thing in arr
newobj[thing.pokename] = thing
finished = newobj
setGeneration: (generation) =>
@generation = window.Generations[generation.toUpperCase()]
{MoveData, SpeciesData, ItemData} = @generation
@moveData = MoveData
@speciesList = (species for species, data of SpeciesData)
# TODO: filter irrelevant items
@itemList = (_(itemName for itemName, data of ItemData).sort())
@render()
setPokemon: (pokemon) =>
# Stop listening for change events on the previously set pokemon
@stopListening(@pokemon) if @pokemon
@pokemon = pokemon
@listenTo(pokemon, 'change:level', @renderStats)
@listenTo(pokemon, 'change:ivs', @renderStats)
@listenTo(pokemon, 'change:evs', @renderStats)
@listenTo(pokemon, 'change:nature', @renderStats)
@listenTo(pokemon, 'change:hiddenPowerType', @renderStats)
@listenTo(pokemon, 'change:shiny', @renderSpecies)
@renderPokemon()
setTeamPBV: (pbv) =>
@teamPBV = pbv
changeSpecies: (e) =>
return if not @onPokemonChange
species = $(e.currentTarget).val()
@pokemon = if species
new Pokemon(teambuilder: true, species: species)
else
new NullPokemon()
@onPokemonChange(@pokemon)
changeNickname: (e) =>
$input = $(e.currentTarget)
@pokemon.set("name", $input.val())
changeShiny: (e) =>
$switch = $(e.currentTarget).toggleClass("selected")
@pokemon.set("shiny", $switch.is(".selected"))
changeHappiness: (e) =>
$switch = $(e.currentTarget).toggleClass("selected")
happiness = if $switch.is(".selected") then 0 else 100
@pokemon.set("happiness", happiness)
changeForme: (e) =>
$forme = $(e.currentTarget)
@pokemon.set('forme', $forme.val())
# Forme changes may have different abilities, so we have to change this.
@pokemon.set('ability', @pokemon.getAbilities()[0])
changeNature: (e) =>
$list = $(e.currentTarget)
@pokemon.set("nature", $list.val())
changeAbility: (e) =>
$list = $(e.currentTarget)
@pokemon.set("ability", $list.val())
changeItem: (e) =>
$list = $(e.currentTarget)
@pokemon.set("item", $list.val())
changeGender: (e) =>
$list = $(e.currentTarget)
@pokemon.set("gender", $list.val())
changeLevel: (e) =>
$input = $(e.currentTarget)
value = parseInt($input.val(), 10)
value = 1120 if isNaN(value) || value > 120
value = 1 if value < 1
$input.val(value)
@pokemon.set("level", value)
changeIv: (e) =>
# todo: make changeIv and changeEv DRY
$input = $(e.currentTarget)
stat = $input.data("stat")
value = parseInt($input.val(), 10)
if isNaN(value) || value > 31 || value < 0
value = 31
@pokemon.setIv(stat, value)
focusEv: (e) =>
$input = $(e.currentTarget)
return if $input.is("[type=range]")
value = parseInt($input.val(), 10)
$input.val("") if value == 0
changeEv: (e) =>
# todo: make changeIv and changeEv DRY
$input = $(e.currentTarget)
stat = $input.data("stat")
value = parseInt($input.val(), 10)
value = 252 if value > 252
value = 0 if isNaN(value) || value < 0
value = @pokemon.setEv(stat, value)
$input.val(value) if not $input.is("[type=range]")
changeHiddenPower: (e) =>
$input = $(e.currentTarget)
type = $input.val()
@pokemon.set('hiddenPowerType', type.toLowerCase())
# Prevents the blurMoves event from activating for the duration of
# the remaining javascript events. This allows the click event to not fire
# the blur event.
preventBlurMoves: (e) =>
@_preventBlur = true
_.defer =>
@_preventBlur = false
blurMoves: (e) =>
$input = $(e.currentTarget)
if @_preventBlur
previousScrollPosition = @$el.scrollTop()
$input.focus()
e.preventDefault()
@$el.scrollTop(previousScrollPosition) # prevent scroll from refocus
return
$selectedMove = @$selectedMove()
moveName = $selectedMove.data('move-id')
# Remove filtering and row selection
@filterMovesBy("")
$(".table-moves .active").removeClass("active")
if $input.val().length == 0
@recordMoves()
else
@insertMove($input, moveName)
clickMoveName: (e) =>
$this = $(e.currentTarget)
moveName = $this.data('move-id')
$moves = @$el.find('.selected_moves')
$input = $moves.find('input:focus').first()
$input = $moves.find('input').first() if $input.length == 0
return if $input.length == 0
@insertMove($input, moveName)
insertMove: ($input, moveName) =>
currentScrollPosition = @$el.scrollTop()
@preventBlurMoves()
return if !@buttonify($input, moveName)
$moves = @$el.find('.selected_moves')
$firstInput = $moves.find('input').first()
if $firstInput.length > 0
$firstInput.focus()
@$el.scrollTop(currentScrollPosition)
else
@$el.scrollTop(0)
@recordMoves()
recordMoves: =>
movesArray = []
$moves = @$el.find('.selected_moves')
$moves.find('.move-button').each ->
moveName = $(this).find("span").text().trim()
if moveName != ""
movesArray.push(moveName)
@pokemon.set("moves", movesArray)
$selectedMove: =>
$table = @$el.find('.table-moves')
$allMoves = $table.find('tbody tr')
$allMoves.filter('.active').first()
clickSelectedMove: (e) =>
$this = $(e.currentTarget)
moveName = $this.find('span').text()
$input = $("<input type='text' value='#{moveName}'/>")
$this.replaceWith($input)
$input.focus().select()
# Set the current move row to active
$(".table-moves tr[data-move-id='#{moveName}']").addClass("active")
removeSelectedMove: (e) =>
$this = $(e.currentTarget).parent()
$input = $("<input type='text'/>")
$this.replaceWith($input)
$input.focus()
e.stopPropagation()
buttonify: ($input, moveName) =>
return false if moveName not of @moveData
# The blur event may have been cancelled, so when removing the input also
# remove the filter
if $input.is(":focus")
@filterMovesBy("")
$(".table-moves .active").removeClass("active")
type = @moveData[moveName].type.toLowerCase()
$input.replaceWith("""
<div class="button move-button #{type}"><span>#{moveName}</span><div class='close'>&times;</div></div>
""")
return true
keydownMoves: (e) =>
$input = $(e.currentTarget)
$table = @$el.find('.table-moves')
$allMoves = $table.find('tbody tr')
switch e.which
when 13 # [Enter]; we're selecting the active move.
$activeMove = @$selectedMove()
$activeMove.click()
when 38 # [Up arrow]; selects move above
$activeMove = $allMoves.filter('.active').first()
$prevMove = $activeMove.prevAll(":visible").first()
if $prevMove.length > 0
$activeMove.removeClass('active')
$prevMove.addClass('active')
when 40 # [Down arrow]; selects move below
$activeMove = $allMoves.filter('.active').first()
$nextMove = $activeMove.nextAll(":visible").first()
if $nextMove.length > 0
$activeMove.removeClass('active')
$nextMove.addClass('active')
else
# Otherwise we're filtering moves
# We defer since $input may not have updated yet
_.defer =>
return unless $input.is(":focus")
moveName = $input.val()
@filterMovesBy(moveName)
filterMovesBy: (moveName) =>
moveName = moveName.replace(/\s+|-/g, "")
$table = @$el.find('.table-moves')
$allMoves = $table.find('tbody tr')
moveRegex = new RegExp(moveName, "i")
$moves = $allMoves.filter ->
$move = $(this)
moveName = $move.data('move-search-id')
moveRegex.test(moveName)
$table.addClass('hidden')
$moves.removeClass('hidden')
$allMoves.not($moves).addClass('hidden')
$allMoves.removeClass('active')
$moves.first().addClass('active')
$table.removeClass('hidden')
disableEventsAndExecute: (callback) =>
isOutermost = !@_eventsDisabled
@_eventsDisabled = true
@undelegateEvents() if isOutermost # disable events
callback()
@delegateEvents() if isOutermost
@_eventsDisabled = false if isOutermost
render: =>
@$el.html @editTemplate(window: window, speciesList: @speciesList, itemList: @itemList)
attachSelectize(@$el.find(".species_list"),
render:
option: (item, escape) =>
pbv = PokeBattle.PBV.determinePBV(@generation, species: item.value)
return "<div class='clearfix'>#{item.text}<div class='pbv'>#{pbv}</div></div>"
)
attachSelectize(@$el.find(".selected_item"))
return this
renderPokemon: =>
@renderSpecies()
@renderNonStats()
@renderStats()
@renderMoves()
@renderPBV()
# Disable entering values if this is a NullPokemon
$elements = @$el.find("input, select").not(".species input, .species select")
$elements.prop("disabled", @pokemon.isNull)
setSelectizeDisabled($elements, @pokemon.isNull)
return this
renderPBV: =>
individualPBV = @pokemon.getPBV()
@$(".individual-pbv").text(individualPBV)
team = @pokemon.getTeam()
if team && team.hasPBV()
pbv = team.getPBV()
maxPBV = team.getMaxPBV()
@$(".total-pbv").text(pbv).toggleClass("red", pbv > maxPBV)
@$(".max-pbv").text(maxPBV)
renderSpecies: =>
@disableEventsAndExecute =>
setSelectizeValue(@$(".species_list"), @pokemon.get("species"))
html = if @pokemon.isNull then "" else @speciesTemplate(window: window, pokemon: @pokemon)
@$(".species-info").html(html)
@$(".selected_shininess").toggleClass("selected", @pokemon.get('shiny') == true)
@$(".selected_happiness").toggleClass("selected", @pokemon.get("happiness") == 0)
renderNonStats: =>
$nonStats = @$el.find(".non-stats")
populateSelect = (searchStr, valueTextPairs, selectedValue) ->
$select = $nonStats.find(searchStr).empty()
for pair in valueTextPairs
value = text = pair
if pair instanceof Array
value = pair[0]
text = pair[1]
$select.append($("<option>").attr("value", value).text(text))
$select.val(selectedValue)
displayedGenders =
F: "Female"
M: "Male"
Genderless: "Genderless"
@disableEventsAndExecute =>
genders = ([g, displayedGenders[g]] for g in @pokemon.getGenders())
$nonStats.find(".selected_nickname").val(@pokemon.get("name"))
populateSelect ".selected_ability", @pokemon.getAbilities(), @pokemon.get("ability")
populateSelect ".selected_nature", @pokemon.getNatures(), @pokemon.get("nature")
setSelectizeValue(@$(".selected_item"), @pokemon.get("item"))
populateSelect ".selected_gender", genders, @pokemon.get("gender")
$nonStats.find(".selected_level").val(@pokemon.get("level"))
renderStats: =>
pokemon = @pokemon
@$(".iv-entry").each ->
$input = $(this)
stat = $input.data("stat")
$input.val(pokemon.iv(stat))
@$(".ev-entry").each ->
return if $(this).is(":focus")
$input = $(this)
stat = $input.data("stat")
$input.val(pokemon.ev(stat))
@$('.base-stat').each ->
$this = $(this)
stat = $this.data("stat")
$this.text(pokemon.base(stat))
@$('.stat-total').each ->
$this = $(this)
stat = $this.data("stat")
$this.text(pokemon.stat(stat))
$this.removeClass('plus-nature minus-nature')
if pokemon.natureBoost(stat) > 1
$this.addClass('plus-nature')
$this.text($this.text() + '+')
if pokemon.natureBoost(stat) < 1
$this.addClass('minus-nature')
$this.text($this.text() + '-')
remainingEvs = 508 - @pokemon.getTotalEVs()
@$('.remaining-evs-amount')
.text(remainingEvs)
.toggleClass("over-limit", remainingEvs < 0)
@$('.select-hidden-power').val(@pokemon.get('hiddenPowerType'))
renderMoves: =>
# TODO: Cache the resultant html
$moveSection = @$el.find(".moves-section")
if @pokemon.isNull
$moveSection.html ""
return
$moveSection.html @movesTemplate(window: window, pokemon: @pokemon)
$moveSection.find('.selected_moves input').each (i, el) =>
$this = $(el)
moveName = $this.val()
@buttonify($this, moveName)

View File

@ -0,0 +1,301 @@
class @TeambuilderView extends Backbone.View
template: JST['teambuilder/main']
teamTemplate: JST['teambuilder/team']
teamsTemplate: JST['teambuilder/teams']
pokemonListTemplate: JST['teambuilder/pokemon_list']
events:
# Team view
'click .add-new-team': 'addNewTeamEvent'
'click .export-team': 'exportTeam'
'click .clone-team': 'cloneTeam'
'click .delete-team': 'deleteTeamEvent'
'click .go-to-team': 'clickTeam'
'click .import-team': 'renderImportTeamModal'
# Teambuild view
'click .change-format-dropdown a': 'changeTeamFormat'
'blur .team_name': 'blurTeamName'
'keypress .team_name': 'keypressTeamName'
'click .go_back': 'goBackToOverview'
'click .pokemon_list li': 'clickPokemon'
'click .add_pokemon': 'addNewPokemonEvent'
'click .save_team': 'saveTeam'
initialize: (attributes) =>
@selectedPokemon = 0
@selectedTeam = null
@render()
@listenTo(PokeBattle.TeamStore, 'reset', @resetTeams)
@listenTo(PokeBattle.TeamStore, 'add', @addNewTeam)
@listenTo(PokeBattle.TeamStore, 'remove', @deleteTeam)
@listenTo(PokeBattle.TeamStore, 'change:id', @changeTeamId)
@listenTo(PokeBattle.TeamStore, 'reset', @renderTeams)
@listenTo(PokeBattle.TeamStore, 'saving', @renderSaving)
@listenTo(PokeBattle.TeamStore, 'saved', @renderSaved)
@listenTo PokeBattle.TeamStore, 'render', (team) =>
@renderTeams()
if @getSelectedTeam() && team.id == @getSelectedTeam().id
@setSelectedTeam(team)
@pokemonEditView = new PokemonEditView(
el: @$('.pokemon_edit')
onPokemonChange: (newPokemon) =>
team = @getSelectedTeam()
team.replace(@selectedPokemon, newPokemon)
@renderPBV()
)
clickTeam: (e) =>
$team = $(e.currentTarget).closest('.select-team')
team = PokeBattle.TeamStore.get($team.data('cid'))
@setSelectedTeam(team)
clickPokemon: (e) =>
$listItem = $(e.currentTarget)
index = @$('.pokemon_list li').index($listItem)
@setSelectedPokemonIndex(index)
attachEventsToTeam: (team) =>
return if team.attachedTeambuildEvents
@listenTo(team, 'add:pokemon', @renderPokemon)
# Todo: Make this perform better
@listenTo(team, 'change:pokemon[*].species change:pokemon[*].forme', (pokemon) =>
@renderPokemonList()
@renderPokemon(pokemon)
)
@listenTo(team, 'add:pokemon remove:pokemon', @renderPokemonList)
@listenTo(team, 'reset:pokemon', (=> @changeTeam(team)))
@listenTo(team, 'change nested-change reset:pokemon add:pokemon remove:pokemon', @dirty)
@listenTo(team, 'change:pokemon[*] reset:pokemon add:pokemon remove:pokemon', @renderPBV)
# A temporary flag to attach until the teambuilder view is refactored
team.attachedTeambuildEvents = true
addEmptyPokemon: (team) =>
team.get('pokemon').add(new NullPokemon())
addNewTeamEvent: (e) =>
team = new Team()
PokeBattle.TeamStore.add(team)
team.save()
addNewTeam: (team) =>
@addEmptyPokemon(team) while team.get('pokemon').length < 6
@$('.teambuilder_teams').append @teamTemplate({team, window})
@attachEventsToTeam(team)
resetTeams: (teamStore) =>
teamStore.forEach (team) =>
@attachEventsToTeam(team)
cloneTeam: (e) =>
$team = $(e.currentTarget).closest('.select-team')
cid = $team.data('cid')
clone = @getTeam(cid).clone().set("id", null)
PokeBattle.TeamStore.add(clone)
clone.save()
return false
deleteTeamEvent: (e) =>
return false if !confirm("Do you really want to delete this team?")
$team = $(e.currentTarget).closest('.select-team')
team = @getTeam($team.data('cid'))
PokeBattle.TeamStore.remove(team)
team.destroy()
return false
deleteTeam: (team) =>
@$(".select-team[data-cid=#{team.cid}]").remove()
changeTeam: (team) =>
html = $(@teamTemplate({team, window})).html()
@$(".select-team[data-cid=#{team.cid}]").html(html)
changeTeamId: (team) =>
@$(".select-team[data-cid=#{team.cid}]").attr('data-id', team.id)
exportTeam: (e) =>
$team = $(e.currentTarget).closest('.select-team')
id = $team.data('id')
if not @getTeam(id).hasNonNullPokemon()
alert("You cannot export empty teams. Please add some pokemon first.")
return false
teamJSON = @getTeam(id).toNonNullJSON()
teamString = PokeBattle.exportTeam(teamJSON.pokemon)
$modal = PokeBattle.modal('modals/export_team')
$modal.find('.exported-team').val(teamString)
$modal.find('textarea, input').first().focus().select()
return false
addNewPokemonEvent: =>
@addNewPokemon(@getSelectedTeam())
addNewPokemon: (team) =>
@addEmptyPokemon(team)
@$('.pokemon_list li').last().click()
saveTeam: =>
clone = @getSelectedTeam()
team = PokeBattle.TeamStore.get(clone.id)
team.save(clone.toJSON(), silent: true)
@resetHeaderButtons()
changeTeamFormat: (e) =>
$link = $(e.currentTarget)
format = $link.data('format')
team = @getSelectedTeam()
if format != team.get('generation')
team.set('generation', format)
@renderTeam()
@dirty() # renderTeam() removes dirty, so call it again
setSelectedPokemonIndex: (index) =>
pokemon = @getSelectedTeam().at(index)
@selectedPokemon = index
# Render the pokemon
@pokemonEditView.setPokemon(pokemon)
@renderPokemon(pokemon)
# Set the correct list item to active
@$(".navigation li").removeClass("active")
@$(".navigation li").eq(index).addClass("active")
getSelectedPokemon: =>
@getSelectedTeam().at(@selectedPokemon)
setSelectedTeam: (team) =>
# Duplicate the team, so that changes don't stick until saved
@selectedTeam = team.clone()
@selectedTeam.id = team.id
@selectedTeam.cid = team.cid
@selectedPokemon = 0
@attachEventsToTeam(@selectedTeam)
@renderTeam()
getAllTeams: =>
PokeBattle.TeamStore.models
getSelectedTeam: =>
@selectedTeam
getTeam: (idx) =>
PokeBattle.TeamStore.get(idx)
blurTeamName: =>
teamName = @$('.team_name').text()
@getSelectedTeam().set('name', teamName)
keypressTeamName: (e) =>
if e.which == 13 # [Enter]
@$('.team_name').blur()
goBackToOverview: =>
@renderTeams()
dirty: =>
@$('.go_back').text('Discard changes')
@$('.save_team').removeClass('disabled')
resetHeaderButtons: =>
@$('.go_back').text('Back')
@$('.save_team').addClass('disabled')
render: =>
@$el.html @template(pokemon: @getSelectedTeam(), selected: @selectedPokemon)
@renderTeams()
renderTeams: =>
@$('.display_teams').html @teamsTemplate(teams: @getAllTeams(), window: window)
@$('.display_teams').removeClass('hidden')
@$('.display_pokemon').addClass('hidden')
this
renderTeam: =>
team = @getSelectedTeam()
@pokemonEditView.setFormat(team.get('generation') || DEFAULT_FORMAT)
@resetHeaderButtons()
@renderFormat()
@renderPokemonList()
@setSelectedPokemonIndex(@selectedPokemon)
@$('.team_name').text(team.getName())
@$('.display_teams').addClass('hidden')
@$('.display_pokemon').removeClass('hidden')
renderPokemonList: =>
team = @getSelectedTeam()
$pokemon_list = @$(".pokemon_list").empty()
$pokemon_list.html @pokemonListTemplate(window: window, pokemonList: team.get('pokemon').models)
$pokemon_list.find("li[data-pokemon-index=#{@selectedPokemon}]").addClass("active")
# NOTE: this isn't be used, and just amounts to hiding the button, however
# we may re-enable this functionality in the future
# Hide add pokemon if there's 6 pokemon
if team.length < 6
@$(".add_pokemon").show()
else
@$(".add_pokemon").hide()
renderPokemon: (pokemon) =>
@pokemonEditView.setPokemon(pokemon)
renderPBV: (pokemon) =>
if pokemon
individualPBV = pokemon.getPBV()
$listItem = @$(".pokemon_list li[data-pokemon-cid=#{pokemon.cid}]")
$listItem.find(".pbv-value").text(individualPBV)
totalPBV = @getSelectedTeam().getPBV()
@pokemonEditView.setTeamPBV(totalPBV)
@pokemonEditView.renderPBV()
renderFormat: =>
format = @getSelectedTeam().get("generation")
format = DEFAULT_FORMAT if format not of Formats
text = @$(".change-format-dropdown a[data-format='#{format}']").text()
@$(".current-format").text(text)
renderImportTeamModal: =>
$modal = PokeBattle.modal 'modals/import_team', ($modal) =>
$modal.on 'click', '.import-team-submit', (e) =>
teamString = $modal.find('.imported-team').val()
pokemonJSON = PokeBattle.parseTeam(teamString)
errors = @validateImportedTeam(pokemonJSON)
if errors.length > 0
listErrors = errors.map((e) -> "<li>#{e}</li>").join('')
$errors = $modal.find('.form-errors')
$errors.html("<ul>#{listErrors}</ul>").removeClass('hidden')
else
team = new Team(pokemon: pokemonJSON, teambuilder: true)
PokeBattle.TeamStore.add(team)
team.save()
$modal.find('.imported-team').val("")
$modal.modal('hide')
return false
$modal.find('.imported-team').first().focus()
validateImportedTeam: (json) =>
errors = []
pokemonSpecies = (pokemon.species for pokemon in json)
{SpeciesData} = window.Generations[DEFAULT_GENERATION.toUpperCase()]
pokemonSpecies = pokemonSpecies.filter((s) -> s not of SpeciesData)
if pokemonSpecies.length > 0
errors.push(pokemonSpecies.map((n) -> "#{n} is not a valid Pokemon.")...)
return errors
return errors
renderSaving: (team) =>
$team = $(".select-team[data-cid='#{team.cid}']")
$team.find('.show_spinner').removeClass('hidden')
renderSaved: (team) =>
$team = $(".select-team[data-cid='#{team.cid}']")
$team.find('.show_spinner').addClass('hidden')

View File

@ -0,0 +1,49 @@
extends layout
block content
#main-section.clearfix
#chat-section.window.chat_window.hidden
.chat
.main_buttons
.section
.button.big.find_battle
span.find-icon.icon-earth
| Find battle
.find_battle_select_team
.section
.button.teambuilder_button
span.icon-pencil
| Teambuilder
.section
.button.display_credits Credits
#teambuilder-section.window.hidden
#battle-list-section.window.hidden
#messages.z2
block footer
.loading-container
.loading-message Loading...
script(src=asset_path('js/data.js'))
script(src=asset_path('js/vendor.js'))
script(src=asset_path('js/templates.js'))
script(src=asset_path('js/app.js'))
script.
// Autogenerated client version
PokeBattle.CLIENT_VERSION = "#{CLIENT_VERSION}";
// quicker click response on mobile devices
$(document).ready(function() {
FastClick.attach(document.body);
});
//- Authentication
(function() {
var TOKEN = "#{user.token}";
var ID = #{user.id};
var USERNAME = "#{user.name}"
PokeBattle.userId = ID;
PokeBattle.username = USERNAME;
PokeBattle.primus.on('open', function() {
PokeBattle.primus.send('login', ID, TOKEN);
});
}).call(this);

View File

@ -0,0 +1,31 @@
!!!
html
head
meta(http-equiv="X-UA-Compatible", content="IE=Edge")
meta(name="viewport", content="width=device-width, initial-scale=1.0, user-scalable=0")
title PokeBattle
link(rel="icon", type="image/png", href="//media.pokebattle.com/img/favicon.png")
link(rel="stylesheet", href=asset_path("css/vendor.css"))
link(rel="stylesheet", href=asset_path("css/main.css"))
body(class = bodyClass)
#navigation
#content
#header
#main-nav
#leftmenu.left
a(href="#").show-navigation
span.icon.icon-menu
#logo
img(id="logoimg", src="../Sprites/logo.png", height="120px", alt="pokebattle.com")
nav#sections
ul
li
a(href="../") Simulator
li
a(href="//91.121.152.74/", target="_blank") Forums
li
a(href="../leaderboard", target="_blank") Leaderboard
#sub-nav
block content
block footer
script.

View File

@ -0,0 +1,58 @@
style
body {
font-family: Verdana;
font-size: 13px;
background: #eee;
}
td {
padding: 10px;
}
tr {
background-color: #d3d3d3;
}
table {
border: 2px solid gray;
border-radius: 10px;
text-align: center;
margin-left: auto;
margin-right: auto;
border-collapse: collapse;
border-style: hidden;
box-shadow: 0 0 0 1px #666;
}
tr:nth-child(even) {
background-color: #c3c3c3;
}
th{
width: 200px;
border-bottom: 1px solid gray;
}
table tr:first-child th:first-child,
table.Info tr:first-child td:first-child {
border-top-left-radius: 10px;
}
table tr:first-child th:last-child,
table.Info tr:first-child td:last-child {
border-top-right-radius: 10px;
}
table tr:last-child td:first-child {
border-bottom-left-radius: 10px;
}
table tr:last-child td:last-child {
border-bottom-right-radius: 10px;
}
div
table
thead
tr
th Rank
th Player
th Score
tbody
each member, i in players
tr
td #{i + 1}
td #{member.username}
td #{member.score}

View File

@ -0,0 +1 @@
a(href="../modify/formes") Change Pokemon Data

View File

@ -0,0 +1,138 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>Advanced JSON Editor Example</title>
<script src="https://raw.githubusercontent.com/jdorn/json-editor/master/dist/jsoneditor.min.js"></script>
</head>
<body>
<h1>Advanced JSON Editor Example</h1>
<p>This example demonstrates the following:</p>
<ul>
<li>Loading external schemas via ajax (using $ref)</li>
<li>Setting the editor's value from javascript (try the Restore to Default button)</li>
<li>Validating the editor's contents (try setting name to an empty string)</li>
<li>Macro templates (try changing the city or state fields and watch the citystate field update automatically)</li>
<li>Enabling and disabling editor fields</li>
</ul>
<button id='submit'>Submit (console.log)</button>
<button id='restore'>Restore to Default</button>
<button id='enable_disable'>Disable/Enable Form</button>
<span id='valid_indicator'></span>
<div id='editor_holder'></div>
<script>
// This is the starting value for the editor
// We will use this to seed the initial editor
// and to provide a "Restore to Default" button.
var starting_value = [
{
name: "John Smith",
age: 35,
gender: "male",
location: {
city: "San Francisco",
state: "California",
citystate: ""
},
pets: [
{
name: "Spot",
type: "dog",
fixed: true
},
{
name: "Whiskers",
type: "cat",
fixed: false
}
]
}
];
// Initialize the editor
var editor = new JSONEditor(document.getElementById('editor_holder'),{
// Enable fetching schemas via ajax
ajax: true,
// The schema for the editor
schema: {
type: "array",
title: "People",
format: "tabs",
items: {
title: "Person",
headerTemplate: "{{i}} - {{self.name}}",
oneOf: [
{
$ref: "basic_person.json",
title: "Basic Person"
},
{
$ref: "person.json",
title: "Complex Person"
}
]
}
},
// Seed the form with a starting value
startval: starting_value,
// Disable additional properties
no_additional_properties: true,
// Require all properties by default
required_by_default: true
});
// Hook up the submit button to log to the console
document.getElementById('submit').addEventListener('click',function() {
// Get the value from the editor
console.log(editor.getValue());
});
// Hook up the Restore to Default button
document.getElementById('restore').addEventListener('click',function() {
editor.setValue(starting_value);
});
// Hook up the enable/disable button
document.getElementById('enable_disable').addEventListener('click',function() {
// Enable form
if(!editor.isEnabled()) {
editor.enable();
}
// Disable form
else {
editor.disable();
}
});
// Hook up the validation indicator to update its
// status whenever the editor changes
editor.on('change',function() {
// Get an array of errors from the validator
var errors = editor.validate();
var indicator = document.getElementById('valid_indicator');
// Not valid
if(errors.length) {
indicator.style.color = 'red';
indicator.textContent = "not valid";
}
// Valid
else {
indicator.style.color = 'green';
indicator.textContent = "valid";
}
});
</script>
</body>
</html>

View File

@ -0,0 +1,17 @@
doctype html
html
head
meta(charset="utf-8")
title Advanced JSON Editor Example
body
script(src="../js/jsoneditor.js")
button#submit Submit (console.log)
button#restore Restore to Default
button#enable_disable Disable/Enable Form
span#valid_indicator
#editor_holder
script(type='text/javascript').
var pokedata = !{JSON.stringify(data)};
script(src="../js/modify.js").

View File

@ -0,0 +1,15 @@
extends ../layout
block content
.m4.mt-header.pt1
h2 Your replays
#all-replays.row-fluid
block footer
script(src=asset_path('js/data.js'))
script(src=asset_path('js/vendor.js'))
script(src=asset_path('js/templates.js'))
script(src=asset_path('js/replays.js'))
script.
var replays = new Replays(!{JSON.stringify(replays)});
new ReplayView({el: '#all-replays', collection: replays}).render();

View File

@ -0,0 +1,39 @@
extends ../layout
block content
.fill.flex-center.flex-column
unless replay
p No replay found.
else
h1.mt-header.mb1
= replay.getFormat()
= ": " + replay.getName()
#replay.relative(style = 'width: 100%; height: 100%;')
block footer
if replay
script(src=asset_path('js/data.js', {fingerprint: replay.version('js/data.js')}))
script(src=asset_path('js/vendor.js', {fingerprint: replay.version('js/vendor.js')}))
script(src=asset_path('js/templates.js', {fingerprint: replay.version('js/templates.js')}))
script(src=asset_path('js/replays.js', {fingerprint: replay.version('js/replays.js')}))
script.
var replay = !{JSON.stringify(replay.toJSON())};
var battle = new Battle({
id: replay.id,
format: replay.format,
numActive: replay.numActive,
playerIds: replay.players
});
var $battleWindow = $(window.JST['battle_window']({
battle: battle,
window: window
}));
$('#replay').html($battleWindow);
battle.view = new BattleView({
el: $battleWindow,
model: battle
});
battle.update(replay.contents);

1930
client/vendor/css/bootstrap.css vendored Normal file

File diff suppressed because it is too large Load Diff

80
client/vendor/css/icomoon.css vendored Normal file
View File

@ -0,0 +1,80 @@
@font-face {
font-family: 'icomoon';
src:url('../fonts/icomoon.eot?-lv7lfm');
src:url('../fonts/icomoon.eot?#iefix-lv7lfm') format('embedded-opentype'),
url('../fonts/icomoon.woff?-lv7lfm') format('woff'),
url('../fonts/icomoon.ttf?-lv7lfm') format('truetype'),
url('../fonts/icomoon.svg?-lv7lfm#icomoon') format('svg');
font-weight: normal;
font-style: normal;
}
[class^="icon-"], [class*=" icon-"] {
font-family: 'icomoon';
speak: none;
font-style: normal;
font-weight: normal;
font-variant: normal;
text-transform: none;
line-height: 1;
font-size: 0.9em;
/* Better Font Rendering =========== */
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
.icon-lock:before {
content: "\e60e";
}
.icon-unlocked:before {
content: "\e60f";
}
.icon-home:before {
content: "\e600";
}
.icon-pencil:before {
content: "\e601";
}
.icon-copy:before {
content: "\e607";
}
.icon-upload:before {
content: "\e608";
}
.icon-spinner:before {
content: "\e60c";
}
.icon-search:before {
content: "\e609";
}
.icon-remove:before {
content: "\e60a";
}
.icon-menu:before {
content: "\e602";
}
.icon-earth:before {
content: "\e603";
}
.icon-eye-blocked:before {
content: "\e604";
}
.icon-heart:before {
content: "\e605";
}
.icon-heart-broken:before {
content: "\e606";
}
.icon-notification:before {
content: "\e60b";
}
.icon-checkmark-circle:before {
content: "\e60d";
}
.icon-close:before {
content: "\e6fd";
}
.icon-minus:before {
content: "\e701";
}

383
client/vendor/css/selectize.default.css vendored Normal file
View File

@ -0,0 +1,383 @@
/**
* selectize.default.css (v0.8.5) - Default Theme
* Copyright (c) 2013 Brian Reavis & contributors
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this
* file except in compliance with the License. You may obtain a copy of the License at:
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under
* the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF
* ANY KIND, either express or implied. See the License for the specific language
* governing permissions and limitations under the License.
*
* @author Brian Reavis <brian@thirdroute.com>
*/
/* MODIFIED FOR PB */
.selectize-control.plugin-drag_drop.multi > .selectize-input > div.ui-sortable-placeholder {
visibility: visible !important;
background: #f2f2f2 !important;
background: rgba(0, 0, 0, 0.06) !important;
border: 0 none !important;
-webkit-box-shadow: inset 0 0 12px 4px #ffffff;
box-shadow: inset 0 0 12px 4px #ffffff;
}
.selectize-control.plugin-drag_drop .ui-sortable-placeholder::after {
content: '!';
visibility: hidden;
}
.selectize-control.plugin-drag_drop .ui-sortable-helper {
-webkit-box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2);
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2);
}
.selectize-dropdown-header {
position: relative;
padding: 5px 8px;
border-bottom: 1px solid #d0d0d0;
background: #f8f8f8;
-webkit-border-radius: 3px 3px 0 0;
-moz-border-radius: 3px 3px 0 0;
border-radius: 3px 3px 0 0;
}
.selectize-dropdown-header-close {
position: absolute;
right: 8px;
top: 50%;
color: #303030;
opacity: 0.4;
margin-top: -12px;
line-height: 18px;
font-size: 20px !important;
}
.selectize-dropdown-header-close:hover {
color: #000000;
}
.selectize-dropdown.plugin-optgroup_columns .optgroup {
border-right: 1px solid #f2f2f2;
border-top: 0 none;
float: left;
-webkit-box-sizing: border-box;
-moz-box-sizing: border-box;
box-sizing: border-box;
}
.selectize-dropdown.plugin-optgroup_columns .optgroup:last-child {
border-right: 0 none;
}
.selectize-dropdown.plugin-optgroup_columns .optgroup:before {
display: none;
}
.selectize-dropdown.plugin-optgroup_columns .optgroup-header {
border-top: 0 none;
}
.selectize-control.plugin-remove_button [data-value] {
position: relative;
padding-right: 24px !important;
}
.selectize-control.plugin-remove_button [data-value] .remove {
position: absolute;
top: 0;
right: 0;
bottom: 0;
width: 17px;
text-align: center;
font-weight: bold;
font-size: 12px;
color: inherit;
text-decoration: none;
vertical-align: middle;
display: inline-block;
padding: 2px 0 0 0;
border-left: 1px solid #0073bb;
-webkit-border-radius: 0 2px 2px 0;
-moz-border-radius: 0 2px 2px 0;
border-radius: 0 2px 2px 0;
-webkit-box-sizing: border-box;
-moz-box-sizing: border-box;
box-sizing: border-box;
}
.selectize-control.plugin-remove_button [data-value] .remove:hover {
background: rgba(0, 0, 0, 0.05);
}
.selectize-control.plugin-remove_button [data-value].active .remove {
border-left-color: #00578d;
}
.selectize-control.plugin-remove_button .disabled [data-value] .remove:hover {
background: none;
}
.selectize-control.plugin-remove_button .disabled [data-value] .remove {
border-left-color: #aaaaaa;
}
.selectize-control {
position: relative;
}
.selectize-dropdown,
.selectize-input,
.selectize-input input {
color: #555;
font-family: inherit;
/*font-size: 13px;*/ /* overriden for pb */
line-height: 18px;
-webkit-font-smoothing: inherit;
}
.selectize-input,
.selectize-control.single .selectize-input.input-active {
background: #ffffff;
cursor: text;
display: inline-block;
}
.selectize-input {
border: 1px solid #d0d0d0;
padding: 4px 6px;
display: inline-block;
width: 100%;
/* overflow: hidden; */ /* removing this seemed to fix sizing issues for pb */
height: 28px;
position: relative;
z-index: 1;
-webkit-box-sizing: border-box;
-moz-box-sizing: border-box;
box-sizing: border-box;
-webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.1);
box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.1);
-webkit-border-radius: 3px;
-moz-border-radius: 3px;
border-radius: 3px;
}
.selectize-control.multi .selectize-input.has-items {
padding: 5px 8px 2px;
}
.selectize-input.full {
background-color: #ffffff;
}
.selectize-input.disabled,
.selectize-input.disabled * {
cursor: default !important;
}
.selectize-input.focus {
-webkit-box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.15);
box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.15);
}
.selectize-input.dropdown-active {
-webkit-border-radius: 3px 3px 0 0;
-moz-border-radius: 3px 3px 0 0;
border-radius: 3px 3px 0 0;
}
.selectize-input > * {
vertical-align: baseline;
display: -moz-inline-stack;
display: inline-block;
zoom: 1;
*display: inline;
}
.selectize-control.multi .selectize-input > div {
cursor: pointer;
margin: 0 3px 3px 0;
padding: 2px 6px;
background: #1da7ee;
color: #ffffff;
border: 1px solid #0073bb;
}
.selectize-control.multi .selectize-input > div.active {
background: #92c836;
color: #ffffff;
border: 1px solid #00578d;
}
.selectize-control.multi .selectize-input.disabled > div,
.selectize-control.multi .selectize-input.disabled > div.active {
color: #ffffff;
background: #d2d2d2;
border: 1px solid #aaaaaa;
}
.selectize-input > input {
padding: 0 !important;
min-height: 0 !important;
max-height: none !important;
max-width: 100% !important;
margin: 0 1px !important;
text-indent: 0 !important;
border: 0 none !important;
background: none !important;
line-height: inherit !important;
-webkit-user-select: auto !important;
-webkit-box-shadow: none !important;
box-shadow: none !important;
}
.selectize-input > input:focus {
outline: none !important;
}
.selectize-input::after {
content: ' ';
display: block;
clear: left;
}
.selectize-input.dropdown-active::before {
content: ' ';
display: block;
position: absolute;
background: #f0f0f0;
height: 1px;
bottom: 0;
left: 0;
right: 0;
}
.selectize-dropdown {
position: absolute;
z-index: 10;
border: 1px solid #d0d0d0;
background: #ffffff;
margin: -1px 0 0 0;
border-top: 0 none;
-webkit-box-sizing: border-box;
-moz-box-sizing: border-box;
box-sizing: border-box;
-webkit-box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
-webkit-border-radius: 0 0 3px 3px;
-moz-border-radius: 0 0 3px 3px;
border-radius: 0 0 3px 3px;
}
.selectize-dropdown [data-selectable] {
cursor: pointer;
overflow: hidden;
}
.selectize-dropdown [data-selectable] .highlight {
background: rgba(125, 168, 208, 0.2);
-webkit-border-radius: 1px;
-moz-border-radius: 1px;
border-radius: 1px;
}
.selectize-dropdown [data-selectable],
.selectize-dropdown .optgroup-header {
padding: 5px 8px;
}
.selectize-dropdown .optgroup:first-child .optgroup-header {
border-top: 0 none;
}
.selectize-dropdown .optgroup-header {
color: #303030;
background: #ffffff;
cursor: default;
}
.selectize-dropdown .active {
background-color: #e5eaed;
color: #495c68;
}
.selectize-dropdown .active.create {
color: #495c68;
}
.selectize-dropdown .create {
color: rgba(48, 48, 48, 0.5);
}
.selectize-dropdown-content {
overflow-y: auto;
overflow-x: hidden;
max-height: 200px;
}
.selectize-control.single .selectize-input,
.selectize-control.single .selectize-input input {
cursor: pointer;
}
.selectize-control.single .selectize-input.input-active,
.selectize-control.single .selectize-input.input-active input {
cursor: text;
}
.selectize-control.single .selectize-input:after {
content: ' ';
display: block;
position: absolute;
top: 50%;
right: 6px;
margin-top: -3px;
width: 0;
height: 0;
border-style: solid;
border-width: 6px 4px 0 4px;
border-color: #333 transparent transparent transparent;
}
.selectize-control.single .selectize-input.dropdown-active:after {
margin-top: -4px;
border-width: 0 4px 6px 4px;
border-color: transparent transparent #333 transparent;
}
.selectize-control.rtl.single .selectize-input:after {
left: 15px;
right: auto;
}
.selectize-control.rtl .selectize-input > input {
margin: 0 4px 0 -2px !important;
}
.selectize-control .selectize-input.disabled {
opacity: 0.5;
background-color: #fafafa;
}
.selectize-control.multi .selectize-input.has-items {
padding-left: 5px;
padding-right: 5px;
}
.selectize-control.multi .selectize-input.disabled [data-value] {
color: #999;
text-shadow: none;
background: none;
-webkit-box-shadow: none;
box-shadow: none;
}
.selectize-control.multi .selectize-input.disabled [data-value],
.selectize-control.multi .selectize-input.disabled [data-value] .remove {
border-color: #e6e6e6;
}
.selectize-control.multi .selectize-input.disabled [data-value] .remove {
background: none;
}
.selectize-control.multi .selectize-input [data-value] {
text-shadow: 0 1px 0 rgba(0, 51, 83, 0.3);
-webkit-border-radius: 3px;
-moz-border-radius: 3px;
border-radius: 3px;
background-color: #1b9dec;
background-image: -moz-linear-gradient(top, #1da7ee, #178ee9);
background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#1da7ee), to(#178ee9));
background-image: -webkit-linear-gradient(top, #1da7ee, #178ee9);
background-image: -o-linear-gradient(top, #1da7ee, #178ee9);
background-image: linear-gradient(to bottom, #1da7ee, #178ee9);
background-repeat: repeat-x;
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff1da7ee', endColorstr='#ff178ee9', GradientType=0);
-webkit-box-shadow: 0 1px 0 rgba(0,0,0,0.2),inset 0 1px rgba(255,255,255,0.03);
box-shadow: 0 1px 0 rgba(0,0,0,0.2),inset 0 1px rgba(255,255,255,0.03);
}
.selectize-control.multi .selectize-input [data-value].active {
background-color: #0085d4;
background-image: -moz-linear-gradient(top, #008fd8, #0075cf);
background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#008fd8), to(#0075cf));
background-image: -webkit-linear-gradient(top, #008fd8, #0075cf);
background-image: -o-linear-gradient(top, #008fd8, #0075cf);
background-image: linear-gradient(to bottom, #008fd8, #0075cf);
background-repeat: repeat-x;
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff008fd8', endColorstr='#ff0075cf', GradientType=0);
}
.selectize-control.single .selectize-input {
-webkit-box-shadow: 0 1px 0 rgba(0,0,0,0.05), inset 0 1px 0 rgba(255,255,255,0.8);
box-shadow: 0 1px 0 rgba(0,0,0,0.05), inset 0 1px 0 rgba(255,255,255,0.8);
background-color: #f9f9f9;
background-image: -moz-linear-gradient(top, #fefefe, #f2f2f2);
background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#fefefe), to(#f2f2f2));
background-image: -webkit-linear-gradient(top, #fefefe, #f2f2f2);
background-image: -o-linear-gradient(top, #fefefe, #f2f2f2);
background-image: linear-gradient(to bottom, #fefefe, #f2f2f2);
background-repeat: repeat-x;
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fffefefe', endColorstr='#fff2f2f2', GradientType=0);
}
.selectize-control.single .selectize-input,
.selectize-dropdown.single {
border-color: #b8b8b8;
}
.selectize-dropdown .optgroup-header {
padding-top: 7px;
font-weight: bold;
font-size: 0.85em;
}
.selectize-dropdown .optgroup {
border-top: 1px solid #f0f0f0;
}
.selectize-dropdown .optgroup:first-child {
border-top: 0 none;
}

112
client/vendor/css/spinner.css vendored Normal file
View File

@ -0,0 +1,112 @@
.spinner {
margin-left: auto;
margin-right: auto;
width: 20px;
height: 20px;
position: relative;
}
.container1 > div, .container2 > div, .container3 > div {
width: 6px;
height: 6px;
background-color: #333;
border-radius: 100%;
position: absolute;
-webkit-animation: bouncedelay 1.2s infinite ease-in-out;
animation: bouncedelay 1.2s infinite ease-in-out;
/* Prevent first frame from flickering when animation starts */
-webkit-animation-fill-mode: both;
animation-fill-mode: both;
}
.spinner .spinner-container {
position: absolute;
width: 100%;
height: 100%;
}
.container2 {
-webkit-transform: rotateZ(45deg);
transform: rotateZ(45deg);
}
.container3 {
-webkit-transform: rotateZ(90deg);
transform: rotateZ(90deg);
}
.circle1 { top: 0; left: 0; }
.circle2 { top: 0; right: 0; }
.circle3 { right: 0; bottom: 0; }
.circle4 { left: 0; bottom: 0; }
.container2 .circle1 {
-webkit-animation-delay: -1.1s;
animation-delay: -1.1s;
}
.container3 .circle1 {
-webkit-animation-delay: -1.0s;
animation-delay: -1.0s;
}
.container1 .circle2 {
-webkit-animation-delay: -0.9s;
animation-delay: -0.9s;
}
.container2 .circle2 {
-webkit-animation-delay: -0.8s;
animation-delay: -0.8s;
}
.container3 .circle2 {
-webkit-animation-delay: -0.7s;
animation-delay: -0.7s;
}
.container1 .circle3 {
-webkit-animation-delay: -0.6s;
animation-delay: -0.6s;
}
.container2 .circle3 {
-webkit-animation-delay: -0.5s;
animation-delay: -0.5s;
}
.container3 .circle3 {
-webkit-animation-delay: -0.4s;
animation-delay: -0.4s;
}
.container1 .circle4 {
-webkit-animation-delay: -0.3s;
animation-delay: -0.3s;
}
.container2 .circle4 {
-webkit-animation-delay: -0.2s;
animation-delay: -0.2s;
}
.container3 .circle4 {
-webkit-animation-delay: -0.1s;
animation-delay: -0.1s;
}
@-webkit-keyframes bouncedelay {
0%, 80%, 100% { -webkit-transform: scale(0.0) }
40% { -webkit-transform: scale(1.0) }
}
@keyframes bouncedelay {
0%, 80%, 100% {
transform: scale(0.0);
-webkit-transform: scale(0.0);
} 40% {
transform: scale(1.0);
-webkit-transform: scale(1.0);
}
}

166
client/vendor/js/Blob.js vendored Normal file
View File

@ -0,0 +1,166 @@
/* Blob.js
* A Blob implementation.
* 2013-06-20
*
* By Eli Grey, http://eligrey.com
* By Devin Samarin, https://github.com/eboyjr
* License: X11/MIT
* See LICENSE.md
*/
/*global self, unescape */
/*jslint bitwise: true, regexp: true, confusion: true, es5: true, vars: true, white: true,
plusplus: true */
/*! @source http://purl.eligrey.com/github/Blob.js/blob/master/Blob.js */
if (!(typeof Blob === "function" || typeof Blob === "object") || typeof URL === "undefined")
if ((typeof Blob === "function" || typeof Blob === "object") && typeof webkitURL !== "undefined") self.URL = webkitURL;
else var Blob = (function (view) {
"use strict";
var BlobBuilder = view.BlobBuilder || view.WebKitBlobBuilder || view.MozBlobBuilder || view.MSBlobBuilder || (function(view) {
var
get_class = function(object) {
return Object.prototype.toString.call(object).match(/^\[object\s(.*)\]$/)[1];
}
, FakeBlobBuilder = function BlobBuilder() {
this.data = [];
}
, FakeBlob = function Blob(data, type, encoding) {
this.data = data;
this.size = data.length;
this.type = type;
this.encoding = encoding;
}
, FBB_proto = FakeBlobBuilder.prototype
, FB_proto = FakeBlob.prototype
, FileReaderSync = view.FileReaderSync
, FileException = function(type) {
this.code = this[this.name = type];
}
, file_ex_codes = (
"NOT_FOUND_ERR SECURITY_ERR ABORT_ERR NOT_READABLE_ERR ENCODING_ERR "
+ "NO_MODIFICATION_ALLOWED_ERR INVALID_STATE_ERR SYNTAX_ERR"
).split(" ")
, file_ex_code = file_ex_codes.length
, real_URL = view.URL || view.webkitURL || view
, real_create_object_URL = real_URL.createObjectURL
, real_revoke_object_URL = real_URL.revokeObjectURL
, URL = real_URL
, btoa = view.btoa
, atob = view.atob
, ArrayBuffer = view.ArrayBuffer
, Uint8Array = view.Uint8Array
;
FakeBlob.fake = FB_proto.fake = true;
while (file_ex_code--) {
FileException.prototype[file_ex_codes[file_ex_code]] = file_ex_code + 1;
}
if (!real_URL.createObjectURL) {
URL = view.URL = {};
}
URL.createObjectURL = function(blob) {
var
type = blob.type
, data_URI_header
;
if (type === null) {
type = "application/octet-stream";
}
if (blob instanceof FakeBlob) {
data_URI_header = "data:" + type;
if (blob.encoding === "base64") {
return data_URI_header + ";base64," + blob.data;
} else if (blob.encoding === "URI") {
return data_URI_header + "," + decodeURIComponent(blob.data);
} if (btoa) {
return data_URI_header + ";base64," + btoa(blob.data);
} else {
return data_URI_header + "," + encodeURIComponent(blob.data);
}
} else if (real_create_object_URL) {
return real_create_object_URL.call(real_URL, blob);
}
};
URL.revokeObjectURL = function(object_URL) {
if (object_URL.substring(0, 5) !== "data:" && real_revoke_object_URL) {
real_revoke_object_URL.call(real_URL, object_URL);
}
};
FBB_proto.append = function(data/*, endings*/) {
var bb = this.data;
// decode data to a binary string
if (Uint8Array && (data instanceof ArrayBuffer || data instanceof Uint8Array)) {
var
str = ""
, buf = new Uint8Array(data)
, i = 0
, buf_len = buf.length
;
for (; i < buf_len; i++) {
str += String.fromCharCode(buf[i]);
}
bb.push(str);
} else if (get_class(data) === "Blob" || get_class(data) === "File") {
if (FileReaderSync) {
var fr = new FileReaderSync;
bb.push(fr.readAsBinaryString(data));
} else {
// async FileReader won't work as BlobBuilder is sync
throw new FileException("NOT_READABLE_ERR");
}
} else if (data instanceof FakeBlob) {
if (data.encoding === "base64" && atob) {
bb.push(atob(data.data));
} else if (data.encoding === "URI") {
bb.push(decodeURIComponent(data.data));
} else if (data.encoding === "raw") {
bb.push(data.data);
}
} else {
if (typeof data !== "string") {
data += ""; // convert unsupported types to strings
}
// decode UTF-16 to binary string
bb.push(unescape(encodeURIComponent(data)));
}
};
FBB_proto.getBlob = function(type) {
if (!arguments.length) {
type = null;
}
return new FakeBlob(this.data.join(""), type, "raw");
};
FBB_proto.toString = function() {
return "[object BlobBuilder]";
};
FB_proto.slice = function(start, end, type) {
var args = arguments.length;
if (args < 3) {
type = null;
}
return new FakeBlob(
this.data.slice(start, args > 1 ? end : this.data.length)
, type
, this.encoding
);
};
FB_proto.toString = function() {
return "[object Blob]";
};
return FakeBlobBuilder;
}(view));
return function Blob(blobParts, options) {
var type = options ? (options.type || "") : "";
var builder = new BlobBuilder();
if (blobParts) {
for (var i = 0, len = blobParts.length; i < len; i++) {
builder.append(blobParts[i]);
}
}
return builder.getBlob(type);
};
}(self));

218
client/vendor/js/FileSaver.js vendored Normal file
View File

@ -0,0 +1,218 @@
/* FileSaver.js
* A saveAs() FileSaver implementation.
* 2013-01-23
*
* By Eli Grey, http://eligrey.com
* License: X11/MIT
* See LICENSE.md
*/
/*global self */
/*jslint bitwise: true, regexp: true, confusion: true, es5: true, vars: true, white: true,
plusplus: true */
/*! @source http://purl.eligrey.com/github/FileSaver.js/blob/master/FileSaver.js */
var saveAs = saveAs
|| (navigator.msSaveOrOpenBlob && navigator.msSaveOrOpenBlob.bind(navigator))
|| (function(view) {
"use strict";
var
doc = view.document
// only get URL when necessary in case BlobBuilder.js hasn't overridden it yet
, get_URL = function() {
return view.URL || view.webkitURL || view;
}
, URL = view.URL || view.webkitURL || view
, save_link = doc.createElementNS("http://www.w3.org/1999/xhtml", "a")
, can_use_save_link = !view.externalHost && "download" in save_link
, click = function(node) {
var event = doc.createEvent("MouseEvents");
event.initMouseEvent(
"click", true, false, view, 0, 0, 0, 0, 0
, false, false, false, false, 0, null
);
node.dispatchEvent(event);
}
, webkit_req_fs = view.webkitRequestFileSystem
, req_fs = view.requestFileSystem || webkit_req_fs || view.mozRequestFileSystem
, throw_outside = function (ex) {
(view.setImmediate || view.setTimeout)(function() {
throw ex;
}, 0);
}
, force_saveable_type = "application/octet-stream"
, fs_min_size = 0
, deletion_queue = []
, process_deletion_queue = function() {
var i = deletion_queue.length;
while (i--) {
var file = deletion_queue[i];
if (typeof file === "string") { // file is an object URL
URL.revokeObjectURL(file);
} else { // file is a File
file.remove();
}
}
deletion_queue.length = 0; // clear queue
}
, dispatch = function(filesaver, event_types, event) {
event_types = [].concat(event_types);
var i = event_types.length;
while (i--) {
var listener = filesaver["on" + event_types[i]];
if (typeof listener === "function") {
try {
listener.call(filesaver, event || filesaver);
} catch (ex) {
throw_outside(ex);
}
}
}
}
, FileSaver = function(blob, name) {
// First try a.download, then web filesystem, then object URLs
var
filesaver = this
, type = blob.type
, blob_changed = false
, object_url
, target_view
, get_object_url = function() {
var object_url = get_URL().createObjectURL(blob);
deletion_queue.push(object_url);
return object_url;
}
, dispatch_all = function() {
dispatch(filesaver, "writestart progress write writeend".split(" "));
}
// on any filesys errors revert to saving with object URLs
, fs_error = function() {
// don't create more object URLs than needed
if (blob_changed || !object_url) {
object_url = get_object_url(blob);
}
if (target_view) {
target_view.location.href = object_url;
} else {
window.open(object_url, "_blank");
}
filesaver.readyState = filesaver.DONE;
dispatch_all();
}
, abortable = function(func) {
return function() {
if (filesaver.readyState !== filesaver.DONE) {
return func.apply(this, arguments);
}
};
}
, create_if_not_found = {create: true, exclusive: false}
, slice
;
filesaver.readyState = filesaver.INIT;
if (!name) {
name = "download";
}
if (can_use_save_link) {
object_url = get_object_url(blob);
save_link.href = object_url;
save_link.download = name;
click(save_link);
filesaver.readyState = filesaver.DONE;
dispatch_all();
return;
}
// Object and web filesystem URLs have a problem saving in Google Chrome when
// viewed in a tab, so I force save with application/octet-stream
// http://code.google.com/p/chromium/issues/detail?id=91158
if (view.chrome && type && type !== force_saveable_type) {
slice = blob.slice || blob.webkitSlice;
blob = slice.call(blob, 0, blob.size, force_saveable_type);
blob_changed = true;
}
// Since I can't be sure that the guessed media type will trigger a download
// in WebKit, I append .download to the filename.
// https://bugs.webkit.org/show_bug.cgi?id=65440
if (webkit_req_fs && name !== "download") {
name += ".download";
}
if (type === force_saveable_type || webkit_req_fs) {
target_view = view;
}
if (!req_fs) {
fs_error();
return;
}
fs_min_size += blob.size;
req_fs(view.TEMPORARY, fs_min_size, abortable(function(fs) {
fs.root.getDirectory("saved", create_if_not_found, abortable(function(dir) {
var save = function() {
dir.getFile(name, create_if_not_found, abortable(function(file) {
file.createWriter(abortable(function(writer) {
writer.onwriteend = function(event) {
target_view.location.href = file.toURL();
deletion_queue.push(file);
filesaver.readyState = filesaver.DONE;
dispatch(filesaver, "writeend", event);
};
writer.onerror = function() {
var error = writer.error;
if (error.code !== error.ABORT_ERR) {
fs_error();
}
};
"writestart progress write abort".split(" ").forEach(function(event) {
writer["on" + event] = filesaver["on" + event];
});
writer.write(blob);
filesaver.abort = function() {
writer.abort();
filesaver.readyState = filesaver.DONE;
};
filesaver.readyState = filesaver.WRITING;
}), fs_error);
}), fs_error);
};
dir.getFile(name, {create: false}, abortable(function(file) {
// delete file if it already exists
file.remove();
save();
}), abortable(function(ex) {
if (ex.code === ex.NOT_FOUND_ERR) {
save();
} else {
fs_error();
}
}));
}), fs_error);
}), fs_error);
}
, FS_proto = FileSaver.prototype
, saveAs = function(blob, name) {
return new FileSaver(blob, name);
}
;
FS_proto.abort = function() {
var filesaver = this;
filesaver.readyState = filesaver.DONE;
dispatch(filesaver, "abort");
};
FS_proto.readyState = FS_proto.INIT = 0;
FS_proto.WRITING = 1;
FS_proto.DONE = 2;
FS_proto.error =
FS_proto.onwritestart =
FS_proto.onprogress =
FS_proto.onwrite =
FS_proto.onabort =
FS_proto.onerror =
FS_proto.onwriteend =
null;
view.addEventListener("unload", process_deletion_queue, false);
return saveAs;
}(self));
if (typeof module !== 'undefined') module.exports = saveAs;

View File

@ -0,0 +1,820 @@
//
// Backbone-associations.js 0.6.1
//
// (c) 2013 Dhruva Ray, Jaynti Kanani, Persistent Systems Ltd.
// Backbone-associations may be freely distributed under the MIT license.
// For all details and documentation:
// https://github.com/dhruvaray/backbone-associations/
//
(function(root, factory) {
// Set up Backbone-associations appropriately for the environment. Start with AMD.
if (typeof define === 'function' && define.amd) {
define(['underscore', 'backbone'], function(_, Backbone) {
// Export global even in AMD case in case this script is loaded with
// others that may still expect a global Backbone.
return factory(root, Backbone, _);
});
// Next for Node.js or CommonJS.
} else if (typeof exports !== 'undefined') {
var _ = require('underscore'),
Backbone = require('backbone');
factory(root, Backbone, _);
if (typeof module !== 'undefined' && module.exports) {
module.exports = Backbone;
}
exports = Backbone;
// Finally, as a browser global.
} else {
factory(root, root.Backbone, root._);
}
}(this, function(root, Backbone, _) {
"use strict";
// Initial Setup
// --------------
// The top-level namespace. All public Backbone classes and modules will be attached to this.
// Exported for the browser and CommonJS.
var BackboneModel, BackboneCollection, ModelProto, BackboneEvent,
CollectionProto, AssociatedModel, pathChecker,
delimiters, pathSeparator, sourceModel, sourceKey, endPoints = {};
// Create local reference `Model` prototype.
BackboneModel = Backbone.Model;
BackboneCollection = Backbone.Collection;
ModelProto = BackboneModel.prototype;
CollectionProto = BackboneCollection.prototype;
BackboneEvent = Backbone.Events;
Backbone.Associations = {
VERSION: "0.6.1"
};
// Alternative scopes other than root
Backbone.Associations.scopes = [];
// Define `getter` and `setter` for `separator`
var getSeparator = function() {
return pathSeparator;
};
// Define `setSeperator`
var setSeparator = function(value) {
if (!_.isString(value) || _.size(value) < 1) {
value = ".";
}
// set private properties
pathSeparator = value;
pathChecker = new RegExp("[\\" + pathSeparator + "\\[\\]]+", "g");
delimiters = new RegExp("[^\\" + pathSeparator + "\\[\\]]+", "g");
};
try {
// Define `SEPERATOR` property to Backbone.Associations
Object.defineProperty(Backbone.Associations, 'SEPARATOR', {
enumerable: true,
get: getSeparator,
set: setSeparator
});
} catch (e) {}
// Backbone.AssociatedModel
// --------------
//Add `Many` and `One` relations to Backbone Object.
Backbone.Associations.Many = Backbone.Many = "Many";
Backbone.Associations.One = Backbone.One = "One";
Backbone.Associations.Self = Backbone.Self = "Self";
// Set default separator
Backbone.Associations.SEPARATOR = ".";
Backbone.Associations.getSeparator = getSeparator;
Backbone.Associations.setSeparator = setSeparator;
Backbone.Associations.EVENTS_BUBBLE = true;
Backbone.Associations.EVENTS_WILDCARD = true;
Backbone.Associations.EVENTS_NC = false;
setSeparator();
// Define `AssociatedModel` (Extends Backbone.Model).
AssociatedModel = Backbone.AssociatedModel = Backbone.Associations.AssociatedModel = BackboneModel.extend({
// Define relations with Associated Model.
relations:undefined,
// Define `Model` property which can keep track of already fired `events`,
// and prevent redundant event to be triggered in case of cyclic model graphs.
_proxyCalls:undefined,
// Override constructor to set parents
constructor: function (attributes, options) {
// Set parent's opportunistically.
options && options.__parents__ && (this.parents = [options.__parents__]);
BackboneModel.apply(this, arguments);
},
on: function (name, callback, context) {
var result = BackboneEvent.on.apply(this, arguments);
// No optimization possible if nested-events is wanted by the application
if (Backbone.Associations.EVENTS_NC) return result;
// Regular expression used to split event strings.
var eventSplitter = /\s+/;
// Handle atomic event names only
if (_.isString(name) && name && (!eventSplitter.test(name)) && callback) {
var endPoint = getPathEndPoint(name);
if (endPoint) {
//Increment end point counter. Represents # of nodes which listen to this end point
endPoints[endPoint] = (typeof endPoints[endPoint] === 'undefined') ? 1 : (endPoints[endPoint] + 1);
}
}
return result;
},
off: function (name, callback, context) {
// No optimization possible if nested-events is wanted by the application
if (Backbone.Associations.EVENTS_NC) return BackboneEvent.off.apply(this, arguments);
var eventSplitter = /\s+/,
events = this._events,
listeners = {},
names = events ? _.keys(events) : [],
all = (!name && !callback && !context),
atomic_event = (_.isString(name) && (!eventSplitter.test(name)));
if (all || atomic_event) {
for (var i = 0, l = names.length; i < l; i++) {
// Store the # of callbacks listening to the event name prior to the `off` call
listeners[names[i]] = events[names[i]] ? events[names[i]].length : 0;
}
}
// Call Backbone off implementation
var result = BackboneEvent.off.apply(this, arguments);
if (all || atomic_event) {
for (i = 0, l = names.length; i < l; i++) {
var endPoint = getPathEndPoint(names[i]);
if (endPoint) {
if (events[names[i]]) {
// Some listeners wiped out for this name for this object
endPoints[endPoint] -= (listeners[names[i]] - events[names[i]].length);
} else {
// All listeners wiped out for this name for this object
endPoints[endPoint] -= listeners[names[i]];
}
}
}
}
return result;
},
// Get the value of an attribute.
get:function (attr) {
var cache = this.__attributes__,
val = ModelProto.get.call(this, attr),
obj = cache ? val || cache[attr] : val;
return obj ? obj : this._getAttr.apply(this, arguments);
},
// Set a hash of model attributes on the Backbone Model.
set:function (key, value, options) {
var attributes, result;
// Duplicate backbone's behavior to allow separate key/value parameters,
// instead of a single 'attributes' object.
if (_.isObject(key) || key == null) {
attributes = key;
options = value;
} else {
attributes = {};
attributes[key] = value;
}
result = this._set(attributes, options);
// Trigger events which have been blocked until the entire object graph is updated.
this._processPendingEvents();
return result;
},
// Works with an attribute hash and options + fully qualified paths
_set:function (attributes, options) {
var attr, modelMap, modelId, obj, result = this;
if (!attributes) return this;
// temp cache of attributes
this.__attributes__ = attributes;
for (attr in attributes) {
//Create a map for each unique object whose attributes we want to set
modelMap || (modelMap = {});
if (attr.match(pathChecker)) {
var pathTokens = getPathArray(attr), initials = _.initial(pathTokens),
last = pathTokens[pathTokens.length - 1],
parentModel = this.get(initials);
if (parentModel instanceof BackboneModel) {
obj = modelMap[parentModel.cid] ||
(modelMap[parentModel.cid] = {'model': parentModel, 'data': {}});
obj.data[last] = attributes[attr];
}
} else {
obj = modelMap[this.cid] || (modelMap[this.cid] = {'model':this, 'data':{}});
obj.data[attr] = attributes[attr];
}
}
if (modelMap) {
for (modelId in modelMap) {
obj = modelMap[modelId];
this._setAttr.call(obj.model, obj.data, options) || (result = false);
}
} else {
result = this._setAttr.call(this, attributes, options);
}
delete this.__attributes__;
return result;
},
// Set a hash of model attributes on the object,
// fire Backbone `event` with options.
// It maintains relations between models during the set operation.
// It also bubbles up child events to the parent.
_setAttr:function (attributes, options) {
var attr;
// Extract attributes and options.
options || (options = {});
if (options.unset) for (attr in attributes) attributes[attr] = void 0;
this.parents = this.parents || [];
if (this.relations) {
// Iterate over `this.relations` and `set` model and collection values
// if `relations` are available.
_.each(this.relations, function (relation) {
var relationKey = relation.key,
activationContext = relation.scope || root,
relatedModel = this._transformRelatedModel(relation, attributes),
collectionType = this._transformCollectionType(relation, relatedModel, attributes),
map = _.isString(relation.map) ? map2Scope(relation.map, activationContext) : relation.map,
currVal = this.attributes[relationKey],
idKey = currVal && currVal.idAttribute,
val, relationOptions, data, relationValue, newCtx = false;
// Merge in `options` specific to this relation.
relationOptions = relation.options ? _.extend({}, relation.options, options) : options;
if (attributes[relationKey]) {
// Get value of attribute with relation key in `val`.
val = _.result(attributes, relationKey);
// Map `val` if a transformation function is provided.
val = map ? map.call(this, val, collectionType ? collectionType : relatedModel) : val;
if(!val) {
attributes[relationKey] = val;
return;
}
// If `relation.type` is `Backbone.Many`,
// Create `Backbone.Collection` with passed data and perform Backbone `set`.
if (relation.type === Backbone.Many) {
if (currVal) {
// Setting this flag will prevent events from firing immediately. That way clients
// will not get events until the entire object graph is updated.
currVal._deferEvents = true;
// Use Backbone.Collection's `reset` or smart `set` method
currVal[relationOptions.reset ? 'reset' : 'set'](
val instanceof BackboneCollection ? val.models : val, relationOptions);
data = currVal;
} else {
newCtx = true;
if (val instanceof BackboneCollection) {
data = val;
} else {
data = this._createCollection(
collectionType || BackboneCollection,
relation.collectionOptions || (relatedModel ? {model: relatedModel} : {})
);
data[relationOptions.reset ? 'reset' : 'set'](val, relationOptions);
}
}
} else if (relation.type === Backbone.One) {
var hasOwnProperty = (val instanceof BackboneModel) ?
val.attributes.hasOwnProperty(idKey) :
val.hasOwnProperty(idKey);
var newIdKey = (val instanceof BackboneModel) ?
val.attributes[idKey] :
val[idKey];
//Is the passed in data for the same key?
if (currVal && hasOwnProperty &&
currVal.attributes[idKey] === newIdKey) {
// Setting this flag will prevent events from firing immediately. That way clients
// will not get events until the entire object graph is updated.
currVal._deferEvents = true;
// Perform the traditional `set` operation
currVal._set(val instanceof BackboneModel ? val.attributes : val, relationOptions);
data = currVal;
} else {
newCtx = true;
if (val instanceof BackboneModel) {
data = val;
} else {
relationOptions.__parents__ = this;
data = new relatedModel(val, relationOptions);
delete relationOptions.__parents__;
}
}
} else {
throw new Error('type attribute must be specified and ' +
'have the values Backbone.One or Backbone.Many');
}
attributes[relationKey] = data;
relationValue = data;
// Add proxy events to respective parents.
// Only add callback if not defined or new Ctx has been identified.
if (newCtx || (relationValue && !relationValue._proxyCallback)) {
if(!relationValue._proxyCallback) {
relationValue._proxyCallback = function () {
return Backbone.Associations.EVENTS_BUBBLE &&
this._bubbleEvent.call(this, relationKey, relationValue, arguments);
};
}
relationValue.on("all", relationValue._proxyCallback, this);
}
}
//Distinguish between the value of undefined versus a set no-op
if (attributes.hasOwnProperty(relationKey))
this._setupParents(attributes[relationKey], this.attributes[relationKey]);
}, this);
}
// Return results for `BackboneModel.set`.
return ModelProto.set.call(this, attributes, options);
},
// Bubble-up event to `parent` Model
_bubbleEvent:function (relationKey, relationValue, eventArguments) {
var args = eventArguments,
opt = args[0].split(":"),
eventType = opt[0],
catch_all = args[0] == "nested-change",
isChangeEvent = eventType === "change",
eventObject = args[1],
indexEventObject = -1,
_proxyCalls = relationValue._proxyCalls,
cargs,
eventPath = opt[1],
eSrc = !eventPath || (eventPath.indexOf(pathSeparator) == -1),
basecolEventPath;
// Short circuit the listen in to the nested-graph event
if (catch_all) return;
// Record the source of the event
if (eSrc) sourceKey = (getPathEndPoint(args[0]) || relationKey);
// Short circuit the event bubbling as there are no listeners for this end point
if (!Backbone.Associations.EVENTS_NC && !endPoints[sourceKey]) return;
// Short circuit the listen in to the wild-card event
if (Backbone.Associations.EVENTS_WILDCARD) {
if (/\[\*\]/g.test(eventPath)) return this;
}
if (relationValue instanceof BackboneCollection && (isChangeEvent || eventPath)) {
// O(n) search :(
indexEventObject = relationValue.indexOf(sourceModel || eventObject);
}
if (this instanceof BackboneModel) {
// A quicker way to identify the model which caused an update inside the collection (while bubbling up)
sourceModel = this;
}
// Manipulate `eventPath`.
eventPath = relationKey + ((indexEventObject !== -1 && (isChangeEvent || eventPath)) ?
"[" + indexEventObject + "]" : "") + (eventPath ? pathSeparator + eventPath : "");
// Short circuit collection * events
if (Backbone.Associations.EVENTS_WILDCARD) {
basecolEventPath = eventPath.replace(/\[\d+\]/g, '[*]');
}
cargs = [];
cargs.push.apply(cargs, args);
cargs[0] = eventType + ":" + eventPath;
// Create a collection modified event with wild-card
if (Backbone.Associations.EVENTS_WILDCARD && eventPath !== basecolEventPath) {
cargs[0] = cargs[0] + " " + eventType + ":" + basecolEventPath;
}
// If event has been already triggered as result of same source `eventPath`,
// no need to re-trigger event to prevent cycle.
_proxyCalls = relationValue._proxyCalls = (_proxyCalls || {});
if (this._isEventAvailable.call(this, _proxyCalls, eventPath)) return this;
// Add `eventPath` in `_proxyCalls` to keep track of already triggered `event`.
_proxyCalls[eventPath] = true;
// Set up previous attributes correctly.
if (isChangeEvent) {
this._previousAttributes[relationKey] = relationValue._previousAttributes;
this.changed[relationKey] = relationValue;
}
// Bubble up event to parent `model` with new changed arguments.
this.trigger.apply(this, cargs);
//Only fire for change. Not change:attribute
if (Backbone.Associations.EVENTS_NC && isChangeEvent && this.get(eventPath) != args[2]) {
var ncargs = ["nested-change", eventPath, args[1]];
args[2] && ncargs.push(args[2]); //args[2] will be options if present
this.trigger.apply(this, ncargs);
}
// Remove `eventPath` from `_proxyCalls`,
// if `eventPath` and `_proxyCalls` are available,
// which allow event to be triggered on for next operation of `set`.
if (_proxyCalls && eventPath) delete _proxyCalls[eventPath];
sourceModel = undefined;
return this;
},
// Has event been fired from this source. Used to prevent event recursion in cyclic graphs
_isEventAvailable:function (_proxyCalls, path) {
return _.find(_proxyCalls, function (value, eventKey) {
return path.indexOf(eventKey, path.length - eventKey.length) !== -1;
});
},
//Maintain reverse pointers - a.k.a parents
_setupParents: function (updated, original) {
// Set new parent for updated
if (updated) {
updated.parents = updated.parents || [];
(_.indexOf(updated.parents, this) == -1) && updated.parents.push(this);
}
// Remove `this` from the earlier set value's parents (if the new value is different).
if (original && original.parents.length > 0 && original != updated) {
original.parents = _.difference(original.parents, [this]);
// Don't bubble to this parent anymore
original._proxyCallback && original.off("all", original._proxyCallback, this);
}
},
// Returns new `collection` (or derivatives) of type `options.model`.
_createCollection: function (type, options) {
options = _.defaults(options, {model: type.model});
var c = new type([], _.isFunction(options) ? options.call(this) : options);
c.parents = [this];
return c;
},
// Process all pending events after the entire object graph has been updated
_processPendingEvents:function () {
if (!this._processedEvents) {
this._processedEvents = true;
this._deferEvents = false;
// Trigger all pending events
_.each(this._pendingEvents, function (e) {
e.c.trigger.apply(e.c, e.a);
});
this._pendingEvents = [];
// Traverse down the object graph and call process pending events on sub-trees
_.each(this.relations, function (relation) {
var val = this.attributes[relation.key];
val && val._processPendingEvents && val._processPendingEvents();
}, this);
delete this._processedEvents;
}
},
// Process the raw `relatedModel` value set in a relation
_transformRelatedModel: function (relation, attributes) {
var relatedModel = relation.relatedModel;
var activationContext = relation.scope || root;
// Call function if relatedModel is implemented as a function
if (relatedModel && !(relatedModel.prototype instanceof BackboneModel))
relatedModel = _.isFunction(relatedModel) ?
relatedModel.call(this, relation, attributes) :
relatedModel;
// Get class if relation and map is stored as a string.
if (relatedModel && _.isString(relatedModel)) {
relatedModel = (relatedModel === Backbone.Self) ?
this.constructor :
map2Scope(relatedModel, activationContext);
}
// Error checking
if (relation.type === Backbone.One) {
if (!relatedModel)
throw new Error('specify a relatedModel for Backbone.One type');
if (!(relatedModel.prototype instanceof Backbone.Model))
throw new Error('specify an AssociatedModel or Backbone.Model for Backbone.One type');
}
return relatedModel;
},
// Process the raw `collectionType` value set in a relation
_transformCollectionType: function (relation, relatedModel, attributes) {
var collectionType = relation.collectionType;
var activationContext = relation.scope || root;
if (collectionType && _.isFunction(collectionType) &&
(collectionType.prototype instanceof BackboneModel))
throw new Error('type is of Backbone.Model. Specify derivatives of Backbone.Collection');
// Call function if collectionType is implemented as a function
if (collectionType && !(collectionType.prototype instanceof BackboneCollection))
collectionType = _.isFunction(collectionType) ?
collectionType.call(this, relation, attributes) : collectionType;
collectionType && _.isString(collectionType) &&
(collectionType = map2Scope(collectionType, activationContext));
// `collectionType` of defined `relation` should be instance of `Backbone.Collection`.
if (collectionType && !collectionType.prototype instanceof BackboneCollection) {
throw new Error('collectionType must inherit from Backbone.Collection');
}
// Error checking
if (relation.type === Backbone.Many) {
if ((!relatedModel) && (!collectionType))
throw new Error('specify either a relatedModel or collectionType');
}
return collectionType;
},
// Override trigger to defer events in the object graph.
trigger:function (name) {
// Defer event processing
if (this._deferEvents) {
this._pendingEvents = this._pendingEvents || [];
// Maintain a queue of pending events to trigger after the entire object graph is updated.
this._pendingEvents.push({c:this, a:arguments});
} else {
ModelProto.trigger.apply(this, arguments);
}
},
// The JSON representation of the model.
toJSON:function (options) {
var json = {}, aJson;
json[this.idAttribute] = this.id;
if (!this.visited) {
this.visited = true;
// Get json representation from `BackboneModel.toJSON`.
json = ModelProto.toJSON.apply(this, arguments);
// Pick up only the keys you want to serialize
if (options && options.serialize_keys) {
json = _.pick(json, options.serialize_keys);
}
// If `this.relations` is defined, iterate through each `relation`
// and added it's json representation to parents' json representation.
if (this.relations) {
_.each(this.relations, function (relation) {
var key = relation.key,
remoteKey = relation.remoteKey,
attr = this.attributes[key],
serialize = !relation.isTransient,
serialize_keys = relation.serialize || [],
_options = _.clone(options);
// Remove default Backbone serialization for associations.
delete json[key];
//Assign to remoteKey if specified. Otherwise use the default key.
//Only for non-transient relationships
if (serialize) {
// Pass the keys to serialize as options to the toJSON method.
if (serialize_keys.length) {
_options ?
(_options.serialize_keys = serialize_keys) :
(_options = {serialize_keys: serialize_keys})
}
aJson = attr && attr.toJSON ? attr.toJSON(_options) : attr;
json[remoteKey || key] = _.isArray(aJson) ? _.compact(aJson) : aJson;
}
}, this);
}
delete this.visited;
}
return json;
},
// Create a new model with identical attributes to this one.
clone: function (options) {
return new this.constructor(this.toJSON(options));
},
// Call this if you want to set an `AssociatedModel` to a falsy value like undefined/null directly.
// Not calling this will leak memory and have wrong parents.
// See test case "parent relations"
cleanup:function (options) {
options = options || {};
_.each(this.relations, function (relation) {
var val = this.attributes[relation.key];
if(val) {
val._proxyCallback && val.off("all", val._proxyCallback, this);
val.parents = _.difference(val.parents, [this]);
}
}, this);
(!options.listen) && this.off();
},
// Override destroy to perform house-keeping on `parents` collection
destroy: function(options) {
options = options ? _.clone(options) : {};
options = _.defaults(options, {remove_references: true, listen: true});
var model = this;
if(options.remove_references && options.wait) {
// Proxy success implementation
var success = options.success;
// Substitute with an implementation which will remove references to `model`
options.success = function (resp) {
if (success) success(model, resp, options);
model.cleanup(options);
}
}
// Call the base implementation
var xhr = ModelProto.destroy.apply(this, [options]);
if(options.remove_references && !options.wait) {
model.cleanup(options);
}
return xhr;
},
// Navigate the path to the leaf object in the path to query for the attribute value
_getAttr:function (path) {
var result = this,
cache = this.__attributes__,
//Tokenize the path
attrs = getPathArray(path),
key,
i;
if (_.size(attrs) < 1) return;
for (i = 0; i < attrs.length; i++) {
key = attrs[i];
if (!result) break;
//Navigate the path to get to the result
result = result instanceof BackboneCollection
? (isNaN(key) ? undefined : result.at(key))
: (cache ? result.attributes[key] || cache[key] : result.attributes[key]);
}
return result;
}
});
// Tokenize the fully qualified event path
var getPathArray = function (path) {
if (path === '') return [''];
return _.isString(path) ? (path.match(delimiters)) : path || [];
};
// Get the end point of the path.
var getPathEndPoint = function (path) {
if (!path) return path;
// event_type:<path>
var tokens = path.split(":");
if (tokens.length > 1) {
path = tokens[tokens.length - 1];
tokens = path.split(pathSeparator);
return tokens.length > 1 ? tokens[tokens.length - 1].split('[')[0] : tokens[0].split('[')[0];
} else {
//path of 0 depth
return "";
}
};
var map2Scope = function (path, context) {
var target,
scopes = [context];
// Check global scopes after passed-in context
scopes.push.apply(scopes, Backbone.Associations.scopes);
for (var ctx, i = 0, l = scopes.length; i < l; ++i) {
if (ctx = scopes[i]) {
target = _.reduce(path.split(pathSeparator), function (memo, elem) {
return memo[elem];
}, ctx);
if (target) break;
}
}
return target;
};
// Infer the relation from the collection's parents and find the appropriate map for the passed in `models`
var map2models = function (parents, target, models) {
var relation, surrogate;
//Iterate over collection's parents
_.find(parents, function (parent) {
//Iterate over relations
relation = _.find(parent.relations, function (rel) {
return parent.get(rel.key) === target;
}, this);
if (relation) {
surrogate = parent;//surrogate for transformation
return true;//break;
}
}, this);
//If we found a relation and it has a mapping function
if (relation && relation.map) {
return relation.map.call(surrogate, models, target);
}
return models;
};
var proxies = {};
// Proxy Backbone collection methods
_.each(['set', 'remove', 'reset'], function (method) {
proxies[method] = BackboneCollection.prototype[method];
CollectionProto[method] = function (models, options) {
//Short-circuit if this collection doesn't hold `AssociatedModels`
if (this.model.prototype instanceof AssociatedModel && this.parents) {
//Find a map function if available and perform a transformation
arguments[0] = map2models(this.parents, this, models);
}
return proxies[method].apply(this, arguments);
}
});
// Override trigger to defer events in the object graph.
proxies['trigger'] = CollectionProto['trigger'];
CollectionProto['trigger'] = function (name) {
if (this._deferEvents) {
this._pendingEvents = this._pendingEvents || [];
// Maintain a queue of pending events to trigger after the entire object graph is updated.
this._pendingEvents.push({c:this, a:arguments});
} else {
proxies['trigger'].apply(this, arguments);
}
};
// Attach process pending event functionality on collections as well. Re-use from `AssociatedModel`
CollectionProto._processPendingEvents = AssociatedModel.prototype._processPendingEvents;
CollectionProto.on = AssociatedModel.prototype.on;
CollectionProto.off = AssociatedModel.prototype.off;
return Backbone;
}));

1608
client/vendor/js/backbone.js vendored Normal file

File diff suppressed because it is too large Load Diff

2291
client/vendor/js/bootstrap.js vendored Normal file

File diff suppressed because it is too large Load Diff

18
client/vendor/js/desktop-notify-min.js vendored Normal file
View File

@ -0,0 +1,18 @@
/**
* Copyright 2012 Tsvetan Tsvetkov
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* Author: Tsvetan Tsvetkov (tsekach@gmail.com)
*/
(function(e){function m(t,n){var r;return e.Notification?r=new e.Notification(t,{icon:c(n.icon)?n.icon:n.icon.x32,body:n.body||u,tag:n.tag||u}):e.webkitNotifications?(r=e.webkitNotifications.createNotification(n.icon,t,n.body),r.show()):navigator.mozNotification?(r=navigator.mozNotification.createNotification(t,n.body,n.icon),r.show()):e.external&&e.external.msIsSiteMode()&&(e.external.msSiteModeClearIconOverlay(),e.external.msSiteModeSetIconOverlay(c(n.icon)?n.icon:n.icon.x16,t),e.external.msSiteModeActivate(),r={ieVerification:f+1}),r}function g(t){return{close:function(){t&&(t.close?t.close():e.external&&e.external.msIsSiteMode()&&t.ieVerification===f&&e.external.msSiteModeClearIconOverlay())}}}function y(t){if(!a)return;var n=l(t)?t:d;e.webkitNotifications&&e.webkitNotifications.checkPermission?e.webkitNotifications.requestPermission(n):e.Notification&&e.Notification.requestPermission&&e.Notification.requestPermission(n)}function b(){var r;if(!a)return;return e.Notification&&e.Notification.permissionLevel?r=e.Notification.permissionLevel():e.webkitNotifications&&e.webkitNotifications.checkPermission?r=i[e.webkitNotifications.checkPermission()]:navigator.mozNotification?r=n:e.Notification&&e.Notification.permission?r=e.Notification.permission:e.external&&e.external.msIsSiteMode()!==undefined&&(r=e.external.msIsSiteMode()?n:t),r}function w(e){return e&&h(e)&&p(v,e),v}function E(){return v.pageVisibility?document.hidden||document.msHidden||document.mozHidden||document.webkitHidden:!0}function S(t,r){var i,s;return a&&E()&&c(t)&&r&&(c(r.icon)||h(r.icon))&&b()===n&&(i=m(t,r)),s=g(i),v.autoClose&&i&&!i.ieVerification&&i.addEventListener&&i.addEventListener("show",function(){var t=s;e.setTimeout(function(){t.close()},v.autoClose)}),s}var t="default",n="granted",r="denied",i=[n,t,r],s={pageVisibility:!1,autoClose:0},o={},u="",a=function(){var t=!1;try{t=!!(e.Notification||e.webkitNotifications||navigator.mozNotification||e.external&&e.external.msIsSiteMode()!==undefined)}catch(n){}return t}(),f=Math.floor(Math.random()*10+1),l=function(e){return e&&e.constructor===Function},c=function(e){return e&&e.constructor===String},h=function(e){return e&&e.constructor===Object},p=function(e,t){var n,r;for(n in t){r=t[n];if(!(n in e)||e[n]!==r&&(!(n in o)||o[n]!==r))e[n]=r}return e},d=function(){},v=s;e.notify={PERMISSION_DEFAULT:t,PERMISSION_GRANTED:n,PERMISSION_DENIED:r,isSupported:a,config:w,createNotification:S,permissionLevel:b,requestPermission:y},l(Object.seal)&&Object.seal(e.notify)})(window);

788
client/vendor/js/fastclick.js vendored Normal file
View File

@ -0,0 +1,788 @@
/**
* @preserve FastClick: polyfill to remove click delays on browsers with touch UIs.
*
* @version 0.6.11
* @codingstandard ftlabs-jsv2
* @copyright The Financial Times Limited [All Rights Reserved]
* @license MIT License (see LICENSE.txt)
*/
/*jslint browser:true, node:true*/
/*global define, Event, Node*/
/**
* Instantiate fast-clicking listeners on the specificed layer.
*
* @constructor
* @param {Element} layer The layer to listen on
*/
function FastClick(layer) {
'use strict';
var oldOnClick, self = this;
/**
* Whether a click is currently being tracked.
*
* @type boolean
*/
this.trackingClick = false;
/**
* Timestamp for when when click tracking started.
*
* @type number
*/
this.trackingClickStart = 0;
/**
* The element being tracked for a click.
*
* @type EventTarget
*/
this.targetElement = null;
/**
* X-coordinate of touch start event.
*
* @type number
*/
this.touchStartX = 0;
/**
* Y-coordinate of touch start event.
*
* @type number
*/
this.touchStartY = 0;
/**
* ID of the last touch, retrieved from Touch.identifier.
*
* @type number
*/
this.lastTouchIdentifier = 0;
/**
* Touchmove boundary, beyond which a click will be cancelled.
*
* @type number
*/
this.touchBoundary = 10;
/**
* The FastClick layer.
*
* @type Element
*/
this.layer = layer;
if (!layer || !layer.nodeType) {
throw new TypeError('Layer must be a document node');
}
/** @type function() */
this.onClick = function() { return FastClick.prototype.onClick.apply(self, arguments); };
/** @type function() */
this.onMouse = function() { return FastClick.prototype.onMouse.apply(self, arguments); };
/** @type function() */
this.onTouchStart = function() { return FastClick.prototype.onTouchStart.apply(self, arguments); };
/** @type function() */
this.onTouchMove = function() { return FastClick.prototype.onTouchMove.apply(self, arguments); };
/** @type function() */
this.onTouchEnd = function() { return FastClick.prototype.onTouchEnd.apply(self, arguments); };
/** @type function() */
this.onTouchCancel = function() { return FastClick.prototype.onTouchCancel.apply(self, arguments); };
if (FastClick.notNeeded(layer)) {
return;
}
// Set up event handlers as required
if (this.deviceIsAndroid) {
layer.addEventListener('mouseover', this.onMouse, true);
layer.addEventListener('mousedown', this.onMouse, true);
layer.addEventListener('mouseup', this.onMouse, true);
}
layer.addEventListener('click', this.onClick, true);
layer.addEventListener('touchstart', this.onTouchStart, false);
layer.addEventListener('touchmove', this.onTouchMove, false);
layer.addEventListener('touchend', this.onTouchEnd, false);
layer.addEventListener('touchcancel', this.onTouchCancel, false);
// Hack is required for browsers that don't support Event#stopImmediatePropagation (e.g. Android 2)
// which is how FastClick normally stops click events bubbling to callbacks registered on the FastClick
// layer when they are cancelled.
if (!Event.prototype.stopImmediatePropagation) {
layer.removeEventListener = function(type, callback, capture) {
var rmv = Node.prototype.removeEventListener;
if (type === 'click') {
rmv.call(layer, type, callback.hijacked || callback, capture);
} else {
rmv.call(layer, type, callback, capture);
}
};
layer.addEventListener = function(type, callback, capture) {
var adv = Node.prototype.addEventListener;
if (type === 'click') {
adv.call(layer, type, callback.hijacked || (callback.hijacked = function(event) {
if (!event.propagationStopped) {
callback(event);
}
}), capture);
} else {
adv.call(layer, type, callback, capture);
}
};
}
// If a handler is already declared in the element's onclick attribute, it will be fired before
// FastClick's onClick handler. Fix this by pulling out the user-defined handler function and
// adding it as listener.
if (typeof layer.onclick === 'function') {
// Android browser on at least 3.2 requires a new reference to the function in layer.onclick
// - the old one won't work if passed to addEventListener directly.
oldOnClick = layer.onclick;
layer.addEventListener('click', function(event) {
oldOnClick(event);
}, false);
layer.onclick = null;
}
}
/**
* Android requires exceptions.
*
* @type boolean
*/
FastClick.prototype.deviceIsAndroid = navigator.userAgent.indexOf('Android') > 0;
/**
* iOS requires exceptions.
*
* @type boolean
*/
FastClick.prototype.deviceIsIOS = /iP(ad|hone|od)/.test(navigator.userAgent);
/**
* iOS 4 requires an exception for select elements.
*
* @type boolean
*/
FastClick.prototype.deviceIsIOS4 = FastClick.prototype.deviceIsIOS && (/OS 4_\d(_\d)?/).test(navigator.userAgent);
/**
* iOS 6.0(+?) requires the target element to be manually derived
*
* @type boolean
*/
FastClick.prototype.deviceIsIOSWithBadTarget = FastClick.prototype.deviceIsIOS && (/OS ([6-9]|\d{2})_\d/).test(navigator.userAgent);
/**
* Determine whether a given element requires a native click.
*
* @param {EventTarget|Element} target Target DOM element
* @returns {boolean} Returns true if the element needs a native click
*/
FastClick.prototype.needsClick = function(target) {
'use strict';
switch (target.nodeName.toLowerCase()) {
// Don't send a synthetic click to disabled inputs (issue #62)
case 'button':
case 'select':
case 'textarea':
if (target.disabled) {
return true;
}
break;
case 'input':
// File inputs need real clicks on iOS 6 due to a browser bug (issue #68)
if ((this.deviceIsIOS && target.type === 'file') || target.disabled) {
return true;
}
break;
case 'label':
case 'video':
return true;
}
return (/\bneedsclick\b/).test(target.className);
};
/**
* Determine whether a given element requires a call to focus to simulate click into element.
*
* @param {EventTarget|Element} target Target DOM element
* @returns {boolean} Returns true if the element requires a call to focus to simulate native click.
*/
FastClick.prototype.needsFocus = function(target) {
'use strict';
switch (target.nodeName.toLowerCase()) {
case 'textarea':
return true;
case 'select':
return !this.deviceIsAndroid;
case 'input':
switch (target.type) {
case 'button':
case 'checkbox':
case 'file':
case 'image':
case 'radio':
case 'submit':
return false;
}
// No point in attempting to focus disabled inputs
return !target.disabled && !target.readOnly;
default:
return (/\bneedsfocus\b/).test(target.className);
}
};
/**
* Send a click event to the specified element.
*
* @param {EventTarget|Element} targetElement
* @param {Event} event
*/
FastClick.prototype.sendClick = function(targetElement, event) {
'use strict';
var clickEvent, touch;
// On some Android devices activeElement needs to be blurred otherwise the synthetic click will have no effect (#24)
if (document.activeElement && document.activeElement !== targetElement) {
document.activeElement.blur();
}
touch = event.changedTouches[0];
// Synthesise a click event, with an extra attribute so it can be tracked
clickEvent = document.createEvent('MouseEvents');
clickEvent.initMouseEvent(this.determineEventType(targetElement), true, true, window, 1, touch.screenX, touch.screenY, touch.clientX, touch.clientY, false, false, false, false, 0, null);
clickEvent.forwardedTouchEvent = true;
targetElement.dispatchEvent(clickEvent);
};
FastClick.prototype.determineEventType = function(targetElement) {
'use strict';
//Issue #159: Android Chrome Select Box does not open with a synthetic click event
if (this.deviceIsAndroid && targetElement.tagName.toLowerCase() === 'select') {
return 'mousedown';
}
return 'click';
};
/**
* @param {EventTarget|Element} targetElement
*/
FastClick.prototype.focus = function(targetElement) {
'use strict';
var length;
// Issue #160: on iOS 7, some input elements (e.g. date datetime) throw a vague TypeError on setSelectionRange. These elements don't have an integer value for the selectionStart and selectionEnd properties, but unfortunately that can't be used for detection because accessing the properties also throws a TypeError. Just check the type instead. Filed as Apple bug #15122724.
if (this.deviceIsIOS && targetElement.setSelectionRange && targetElement.type.indexOf('date') !== 0 && targetElement.type !== 'time') {
length = targetElement.value.length;
targetElement.setSelectionRange(length, length);
} else {
targetElement.focus();
}
};
/**
* Check whether the given target element is a child of a scrollable layer and if so, set a flag on it.
*
* @param {EventTarget|Element} targetElement
*/
FastClick.prototype.updateScrollParent = function(targetElement) {
'use strict';
var scrollParent, parentElement;
scrollParent = targetElement.fastClickScrollParent;
// Attempt to discover whether the target element is contained within a scrollable layer. Re-check if the
// target element was moved to another parent.
if (!scrollParent || !scrollParent.contains(targetElement)) {
parentElement = targetElement;
do {
if (parentElement.scrollHeight > parentElement.offsetHeight) {
scrollParent = parentElement;
targetElement.fastClickScrollParent = parentElement;
break;
}
parentElement = parentElement.parentElement;
} while (parentElement);
}
// Always update the scroll top tracker if possible.
if (scrollParent) {
scrollParent.fastClickLastScrollTop = scrollParent.scrollTop;
}
};
/**
* @param {EventTarget} targetElement
* @returns {Element|EventTarget}
*/
FastClick.prototype.getTargetElementFromEventTarget = function(eventTarget) {
'use strict';
// On some older browsers (notably Safari on iOS 4.1 - see issue #56) the event target may be a text node.
if (eventTarget.nodeType === Node.TEXT_NODE) {
return eventTarget.parentNode;
}
return eventTarget;
};
/**
* On touch start, record the position and scroll offset.
*
* @param {Event} event
* @returns {boolean}
*/
FastClick.prototype.onTouchStart = function(event) {
'use strict';
var targetElement, touch, selection;
// Ignore multiple touches, otherwise pinch-to-zoom is prevented if both fingers are on the FastClick element (issue #111).
if (event.targetTouches.length > 1) {
return true;
}
targetElement = this.getTargetElementFromEventTarget(event.target);
touch = event.targetTouches[0];
if (this.deviceIsIOS) {
// Only trusted events will deselect text on iOS (issue #49)
selection = window.getSelection();
if (selection.rangeCount && !selection.isCollapsed) {
return true;
}
if (!this.deviceIsIOS4) {
// Weird things happen on iOS when an alert or confirm dialog is opened from a click event callback (issue #23):
// when the user next taps anywhere else on the page, new touchstart and touchend events are dispatched
// with the same identifier as the touch event that previously triggered the click that triggered the alert.
// Sadly, there is an issue on iOS 4 that causes some normal touch events to have the same identifier as an
// immediately preceeding touch event (issue #52), so this fix is unavailable on that platform.
if (touch.identifier === this.lastTouchIdentifier) {
event.preventDefault();
return false;
}
this.lastTouchIdentifier = touch.identifier;
// If the target element is a child of a scrollable layer (using -webkit-overflow-scrolling: touch) and:
// 1) the user does a fling scroll on the scrollable layer
// 2) the user stops the fling scroll with another tap
// then the event.target of the last 'touchend' event will be the element that was under the user's finger
// when the fling scroll was started, causing FastClick to send a click event to that layer - unless a check
// is made to ensure that a parent layer was not scrolled before sending a synthetic click (issue #42).
this.updateScrollParent(targetElement);
}
}
this.trackingClick = true;
this.trackingClickStart = event.timeStamp;
this.targetElement = targetElement;
this.touchStartX = touch.pageX;
this.touchStartY = touch.pageY;
// Prevent phantom clicks on fast double-tap (issue #36)
if ((event.timeStamp - this.lastClickTime) < 200) {
event.preventDefault();
}
return true;
};
/**
* Based on a touchmove event object, check whether the touch has moved past a boundary since it started.
*
* @param {Event} event
* @returns {boolean}
*/
FastClick.prototype.touchHasMoved = function(event) {
'use strict';
var touch = event.changedTouches[0], boundary = this.touchBoundary;
if (Math.abs(touch.pageX - this.touchStartX) > boundary || Math.abs(touch.pageY - this.touchStartY) > boundary) {
return true;
}
return false;
};
/**
* Update the last position.
*
* @param {Event} event
* @returns {boolean}
*/
FastClick.prototype.onTouchMove = function(event) {
'use strict';
if (!this.trackingClick) {
return true;
}
// If the touch has moved, cancel the click tracking
if (this.targetElement !== this.getTargetElementFromEventTarget(event.target) || this.touchHasMoved(event)) {
this.trackingClick = false;
this.targetElement = null;
}
return true;
};
/**
* Attempt to find the labelled control for the given label element.
*
* @param {EventTarget|HTMLLabelElement} labelElement
* @returns {Element|null}
*/
FastClick.prototype.findControl = function(labelElement) {
'use strict';
// Fast path for newer browsers supporting the HTML5 control attribute
if (labelElement.control !== undefined) {
return labelElement.control;
}
// All browsers under test that support touch events also support the HTML5 htmlFor attribute
if (labelElement.htmlFor) {
return document.getElementById(labelElement.htmlFor);
}
// If no for attribute exists, attempt to retrieve the first labellable descendant element
// the list of which is defined here: http://www.w3.org/TR/html5/forms.html#category-label
return labelElement.querySelector('button, input:not([type=hidden]), keygen, meter, output, progress, select, textarea');
};
/**
* On touch end, determine whether to send a click event at once.
*
* @param {Event} event
* @returns {boolean}
*/
FastClick.prototype.onTouchEnd = function(event) {
'use strict';
var forElement, trackingClickStart, targetTagName, scrollParent, touch, targetElement = this.targetElement;
if (!this.trackingClick) {
return true;
}
// Prevent phantom clicks on fast double-tap (issue #36)
if ((event.timeStamp - this.lastClickTime) < 200) {
this.cancelNextClick = true;
return true;
}
// Reset to prevent wrong click cancel on input (issue #156).
this.cancelNextClick = false;
this.lastClickTime = event.timeStamp;
trackingClickStart = this.trackingClickStart;
this.trackingClick = false;
this.trackingClickStart = 0;
// On some iOS devices, the targetElement supplied with the event is invalid if the layer
// is performing a transition or scroll, and has to be re-detected manually. Note that
// for this to function correctly, it must be called *after* the event target is checked!
// See issue #57; also filed as rdar://13048589 .
if (this.deviceIsIOSWithBadTarget) {
touch = event.changedTouches[0];
// In certain cases arguments of elementFromPoint can be negative, so prevent setting targetElement to null
targetElement = document.elementFromPoint(touch.pageX - window.pageXOffset, touch.pageY - window.pageYOffset) || targetElement;
targetElement.fastClickScrollParent = this.targetElement.fastClickScrollParent;
}
targetTagName = targetElement.tagName.toLowerCase();
if (targetTagName === 'label') {
forElement = this.findControl(targetElement);
if (forElement) {
this.focus(targetElement);
if (this.deviceIsAndroid) {
return false;
}
targetElement = forElement;
}
} else if (this.needsFocus(targetElement)) {
// Case 1: If the touch started a while ago (best guess is 100ms based on tests for issue #36) then focus will be triggered anyway. Return early and unset the target element reference so that the subsequent click will be allowed through.
// Case 2: Without this exception for input elements tapped when the document is contained in an iframe, then any inputted text won't be visible even though the value attribute is updated as the user types (issue #37).
if ((event.timeStamp - trackingClickStart) > 100 || (this.deviceIsIOS && window.top !== window && targetTagName === 'input')) {
this.targetElement = null;
return false;
}
this.focus(targetElement);
// Select elements need the event to go through on iOS 4, otherwise the selector menu won't open.
if (!this.deviceIsIOS4 || targetTagName !== 'select') {
this.targetElement = null;
event.preventDefault();
}
return false;
}
if (this.deviceIsIOS && !this.deviceIsIOS4) {
// Don't send a synthetic click event if the target element is contained within a parent layer that was scrolled
// and this tap is being used to stop the scrolling (usually initiated by a fling - issue #42).
scrollParent = targetElement.fastClickScrollParent;
if (scrollParent && scrollParent.fastClickLastScrollTop !== scrollParent.scrollTop) {
return true;
}
}
// Prevent the actual click from going though - unless the target node is marked as requiring
// real clicks or if it is in the whitelist in which case only non-programmatic clicks are permitted.
if (!this.needsClick(targetElement)) {
event.preventDefault();
this.sendClick(targetElement, event);
}
return false;
};
/**
* On touch cancel, stop tracking the click.
*
* @returns {void}
*/
FastClick.prototype.onTouchCancel = function() {
'use strict';
this.trackingClick = false;
this.targetElement = null;
};
/**
* Determine mouse events which should be permitted.
*
* @param {Event} event
* @returns {boolean}
*/
FastClick.prototype.onMouse = function(event) {
'use strict';
// If a target element was never set (because a touch event was never fired) allow the event
if (!this.targetElement) {
return true;
}
if (event.forwardedTouchEvent) {
return true;
}
// Programmatically generated events targeting a specific element should be permitted
if (!event.cancelable) {
return true;
}
// Derive and check the target element to see whether the mouse event needs to be permitted;
// unless explicitly enabled, prevent non-touch click events from triggering actions,
// to prevent ghost/doubleclicks.
if (!this.needsClick(this.targetElement) || this.cancelNextClick) {
// Prevent any user-added listeners declared on FastClick element from being fired.
if (event.stopImmediatePropagation) {
event.stopImmediatePropagation();
} else {
// Part of the hack for browsers that don't support Event#stopImmediatePropagation (e.g. Android 2)
event.propagationStopped = true;
}
// Cancel the event
event.stopPropagation();
event.preventDefault();
return false;
}
// If the mouse event is permitted, return true for the action to go through.
return true;
};
/**
* On actual clicks, determine whether this is a touch-generated click, a click action occurring
* naturally after a delay after a touch (which needs to be cancelled to avoid duplication), or
* an actual click which should be permitted.
*
* @param {Event} event
* @returns {boolean}
*/
FastClick.prototype.onClick = function(event) {
'use strict';
var permitted;
// It's possible for another FastClick-like library delivered with third-party code to fire a click event before FastClick does (issue #44). In that case, set the click-tracking flag back to false and return early. This will cause onTouchEnd to return early.
if (this.trackingClick) {
this.targetElement = null;
this.trackingClick = false;
return true;
}
// Very odd behaviour on iOS (issue #18): if a submit element is present inside a form and the user hits enter in the iOS simulator or clicks the Go button on the pop-up OS keyboard the a kind of 'fake' click event will be triggered with the submit-type input element as the target.
if (event.target.type === 'submit' && event.detail === 0) {
return true;
}
permitted = this.onMouse(event);
// Only unset targetElement if the click is not permitted. This will ensure that the check for !targetElement in onMouse fails and the browser's click doesn't go through.
if (!permitted) {
this.targetElement = null;
}
// If clicks are permitted, return true for the action to go through.
return permitted;
};
/**
* Remove all FastClick's event listeners.
*
* @returns {void}
*/
FastClick.prototype.destroy = function() {
'use strict';
var layer = this.layer;
if (this.deviceIsAndroid) {
layer.removeEventListener('mouseover', this.onMouse, true);
layer.removeEventListener('mousedown', this.onMouse, true);
layer.removeEventListener('mouseup', this.onMouse, true);
}
layer.removeEventListener('click', this.onClick, true);
layer.removeEventListener('touchstart', this.onTouchStart, false);
layer.removeEventListener('touchmove', this.onTouchMove, false);
layer.removeEventListener('touchend', this.onTouchEnd, false);
layer.removeEventListener('touchcancel', this.onTouchCancel, false);
};
/**
* Check whether FastClick is needed.
*
* @param {Element} layer The layer to listen on
*/
FastClick.notNeeded = function(layer) {
'use strict';
var metaViewport;
var chromeVersion;
// Devices that don't support touch don't need FastClick
if (typeof window.ontouchstart === 'undefined') {
return true;
}
// Chrome version - zero for other browsers
chromeVersion = +(/Chrome\/([0-9]+)/.exec(navigator.userAgent) || [,0])[1];
if (chromeVersion) {
if (FastClick.prototype.deviceIsAndroid) {
metaViewport = document.querySelector('meta[name=viewport]');
if (metaViewport) {
// Chrome on Android with user-scalable="no" doesn't need FastClick (issue #89)
if (metaViewport.content.indexOf('user-scalable=no') !== -1) {
return true;
}
// Chrome 32 and above with width=device-width or less don't need FastClick
if (chromeVersion > 31 && window.innerWidth <= window.screen.width) {
return true;
}
}
// Chrome desktop doesn't need FastClick (issue #15)
} else {
return true;
}
}
// IE10 with -ms-touch-action: none, which disables double-tap-to-zoom (issue #97)
if (layer.style.msTouchAction === 'none') {
return true;
}
return false;
};
/**
* Factory method for creating a FastClick object
*
* @param {Element} layer The layer to listen on
*/
FastClick.attach = function(layer) {
'use strict';
return new FastClick(layer);
};
if (typeof define !== 'undefined' && define.amd) {
// AMD. Register as an anonymous module.
define(function() {
'use strict';
return FastClick;
});
} else if (typeof module !== 'undefined' && module.exports) {
module.exports = FastClick.attach;
module.exports.FastClick = FastClick;
} else {
window.FastClick = FastClick;
}

208
client/vendor/js/jade.js vendored Normal file
View File

@ -0,0 +1,208 @@
(function(e){if("function"==typeof bootstrap)bootstrap("jade",e);else if("object"==typeof exports)module.exports=e();else if("function"==typeof define&&define.amd)define(e);else if("undefined"!=typeof ses){if(!ses.ok())return;ses.makeJade=e}else"undefined"!=typeof window?window.jade=e():global.jade=e()})(function(){var define,ses,bootstrap,module,exports;
return (function(e,t,n){function i(n,s){if(!t[n]){if(!e[n]){var o=typeof require=="function"&&require;if(!s&&o)return o(n,!0);if(r)return r(n,!0);throw new Error("Cannot find module '"+n+"'")}var u=t[n]={exports:{}};e[n][0].call(u.exports,function(t){var r=e[n][1][t];return i(r?r:t)},u,u.exports)}return t[n].exports}var r=typeof require=="function"&&require;for(var s=0;s<n.length;s++)i(n[s]);return i})({1:[function(require,module,exports){
/*!
* Jade - runtime
* Copyright(c) 2010 TJ Holowaychuk <tj@vision-media.ca>
* MIT Licensed
*/
/**
* Lame Array.isArray() polyfill for now.
*/
if (!Array.isArray) {
Array.isArray = function(arr){
return '[object Array]' == Object.prototype.toString.call(arr);
};
}
/**
* Lame Object.keys() polyfill for now.
*/
if (!Object.keys) {
Object.keys = function(obj){
var arr = [];
for (var key in obj) {
if (obj.hasOwnProperty(key)) {
arr.push(key);
}
}
return arr;
}
}
/**
* Merge two attribute objects giving precedence
* to values in object `b`. Classes are special-cased
* allowing for arrays and merging/joining appropriately
* resulting in a string.
*
* @param {Object} a
* @param {Object} b
* @return {Object} a
* @api private
*/
exports.merge = function merge(a, b) {
var ac = a['class'];
var bc = b['class'];
if (ac || bc) {
ac = ac || [];
bc = bc || [];
if (!Array.isArray(ac)) ac = [ac];
if (!Array.isArray(bc)) bc = [bc];
a['class'] = ac.concat(bc).filter(nulls);
}
for (var key in b) {
if (key != 'class') {
a[key] = b[key];
}
}
return a;
};
/**
* Filter null `val`s.
*
* @param {*} val
* @return {Boolean}
* @api private
*/
function nulls(val) {
return val != null && val !== '';
}
/**
* join array as classes.
*
* @param {*} val
* @return {String}
* @api private
*/
function joinClasses(val) {
return Array.isArray(val) ? val.map(joinClasses).filter(nulls).join(' ') : val;
}
/**
* Render the given attributes object.
*
* @param {Object} obj
* @param {Object} escaped
* @return {String}
* @api private
*/
exports.attrs = function attrs(obj, escaped){
var buf = []
, terse = obj.terse;
delete obj.terse;
var keys = Object.keys(obj)
, len = keys.length;
if (len) {
buf.push('');
for (var i = 0; i < len; ++i) {
var key = keys[i]
, val = obj[key];
if ('boolean' == typeof val || null == val) {
if (val) {
terse
? buf.push(key)
: buf.push(key + '="' + key + '"');
}
} else if (0 == key.indexOf('data') && 'string' != typeof val) {
buf.push(key + "='" + JSON.stringify(val) + "'");
} else if ('class' == key) {
if (escaped && escaped[key]){
if (val = exports.escape(joinClasses(val))) {
buf.push(key + '="' + val + '"');
}
} else {
if (val = joinClasses(val)) {
buf.push(key + '="' + val + '"');
}
}
} else if (escaped && escaped[key]) {
buf.push(key + '="' + exports.escape(val) + '"');
} else {
buf.push(key + '="' + val + '"');
}
}
}
return buf.join(' ');
};
/**
* Escape the given string of `html`.
*
* @param {String} html
* @return {String}
* @api private
*/
exports.escape = function escape(html){
return String(html)
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;');
};
/**
* Re-throw the given `err` in context to the
* the jade in `filename` at the given `lineno`.
*
* @param {Error} err
* @param {String} filename
* @param {String} lineno
* @api private
*/
exports.rethrow = function rethrow(err, filename, lineno, str){
if (!(err instanceof Error)) throw err;
if ((typeof window != 'undefined' || !filename) && !str) {
err.message += ' on line ' + lineno;
throw err;
}
try {
str = str || require('fs').readFileSync(filename, 'utf8')
} catch (ex) {
rethrow(err, null, lineno)
}
var context = 3
, lines = str.split('\n')
, start = Math.max(lineno - context, 0)
, end = Math.min(lines.length, lineno + context);
// Error context
var context = lines.slice(start, end).map(function(line, i){
var curr = i + start + 1;
return (curr == lineno ? ' > ' : ' ')
+ curr
+ '| '
+ line;
}).join('\n');
// Alter exception message
err.path = filename;
err.message = (filename || 'Jade') + ':' + lineno
+ '\n' + context + '\n\n' + err.message;
throw err;
};
},{"fs":2}],2:[function(require,module,exports){
// nothing to see here... no file methods for the browser
},{}]},{},[1])(1)
});
;

114
client/vendor/js/jquery.cookie.js vendored Normal file
View File

@ -0,0 +1,114 @@
/*!
* jQuery Cookie Plugin v1.4.0
* https://github.com/carhartl/jquery-cookie
*
* Copyright 2013 Klaus Hartl
* Released under the MIT license
*/
(function (factory) {
if (typeof define === 'function' && define.amd) {
// AMD. Register as anonymous module.
define(['jquery'], factory);
} else {
// Browser globals.
factory(jQuery);
}
}(function ($) {
var pluses = /\+/g;
function encode(s) {
return config.raw ? s : encodeURIComponent(s);
}
function decode(s) {
return config.raw ? s : decodeURIComponent(s);
}
function stringifyCookieValue(value) {
return encode(config.json ? JSON.stringify(value) : String(value));
}
function parseCookieValue(s) {
if (s.indexOf('"') === 0) {
// This is a quoted cookie as according to RFC2068, unescape...
s = s.slice(1, -1).replace(/\\"/g, '"').replace(/\\\\/g, '\\');
}
try {
// Replace server-side written pluses with spaces.
// If we can't decode the cookie, ignore it, it's unusable.
// If we can't parse the cookie, ignore it, it's unusable.
s = decodeURIComponent(s.replace(pluses, ' '));
return config.json ? JSON.parse(s) : s;
} catch(e) {}
}
function read(s, converter) {
var value = config.raw ? s : parseCookieValue(s);
return $.isFunction(converter) ? converter(value) : value;
}
var config = $.cookie = function (key, value, options) {
// Write
if (value !== undefined && !$.isFunction(value)) {
options = $.extend({}, config.defaults, options);
if (typeof options.expires === 'number') {
var days = options.expires, t = options.expires = new Date();
t.setTime(+t + days * 864e+5);
}
return (document.cookie = [
encode(key), '=', stringifyCookieValue(value),
options.expires ? '; expires=' + options.expires.toUTCString() : '', // use expires attribute, max-age is not supported by IE
options.path ? '; path=' + options.path : '',
options.domain ? '; domain=' + options.domain : '',
options.secure ? '; secure' : ''
].join(''));
}
// Read
var result = key ? undefined : {};
// To prevent the for loop in the first place assign an empty array
// in case there are no cookies at all. Also prevents odd result when
// calling $.cookie().
var cookies = document.cookie ? document.cookie.split('; ') : [];
for (var i = 0, l = cookies.length; i < l; i++) {
var parts = cookies[i].split('=');
var name = decode(parts.shift());
var cookie = parts.join('=');
if (key && key === name) {
// If second argument (value) is a function it's a converter...
result = read(cookie, value);
break;
}
// Prevent storing a cookie that we couldn't decode.
if (!key && (cookie = read(cookie)) !== undefined) {
result[name] = cookie;
}
}
return result;
};
config.defaults = {};
$.removeCookie = function (key, options) {
if ($.cookie(key) === undefined) {
return false;
}
// Must not alter options, thus extending a fresh object...
$.cookie(key, '', $.extend({}, options, { expires: -1 }));
return !$.cookie(key);
};
}));

Some files were not shown because too many files have changed in this diff Show More