commit d7316d57995e44a4c4d50e5ccc24614acd54318d Author: Deukhoofd Date: Mon Feb 1 23:19:30 2016 +0100 Lots of stuff diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1e1b977 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +/server/config.coffee +/node_modules/ +/knexfile.coffee \ No newline at end of file diff --git a/.nodemonignore b/.nodemonignore new file mode 100644 index 0000000..9d88b96 --- /dev/null +++ b/.nodemonignore @@ -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 diff --git a/Capfile b/Capfile new file mode 100644 index 0000000..b82d4bd --- /dev/null +++ b/Capfile @@ -0,0 +1,2 @@ +load 'deploy' +load 'config/deploy' # remove this line to skip loading any of the default tasks \ No newline at end of file diff --git a/Gemfile b/Gemfile new file mode 100644 index 0000000..af7f0e6 --- /dev/null +++ b/Gemfile @@ -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' diff --git a/Gemfile.lock b/Gemfile.lock new file mode 100644 index 0000000..470213f --- /dev/null +++ b/Gemfile.lock @@ -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! diff --git a/Gruntfile.coffee b/Gruntfile.coffee new file mode 100644 index 0000000..8873b33 --- /dev/null +++ b/Gruntfile.coffee @@ -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']) diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..7cc37ac --- /dev/null +++ b/LICENSE @@ -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. diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..f10c811 --- /dev/null +++ b/Makefile @@ -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 diff --git a/README.md b/README.md new file mode 100644 index 0000000..979bb6b --- /dev/null +++ b/README.md @@ -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). diff --git a/Vagrantfile b/Vagrantfile new file mode 100644 index 0000000..25cf0dc --- /dev/null +++ b/Vagrantfile @@ -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 diff --git a/api/index.coffee b/api/index.coffee new file mode 100644 index 0000000..1640e60 --- /dev/null +++ b/api/index.coffee @@ -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 diff --git a/api/name_map.json b/api/name_map.json new file mode 100644 index 0000000..21004f2 --- /dev/null +++ b/api/name_map.json @@ -0,0 +1 @@ +{"snubbull": "Snubbull", "lairon": "Lairon", "avalugg": "Avalugg", "bonsly": "Bonsly", "electivire": "Electivire", "charmeleon": "Charmeleon", "celebi": "Celebi", "togetic": "Togetic", "unfezant": "Unfezant", "charmander": "Charmander", "weezing": "Weezing", "palkia": "Palkia", "froakie": "Froakie", "manectric": "Manectric", "snorlax": "Snorlax", "vanillish": "Vanillish", "abra": "Abra", "reuniclus": "Reuniclus", "grovyle": "Grovyle", "machamp": "Machamp", "tympole": "Tympole", "wigglytuff": "Wigglytuff", "wobbuffet": "Wobbuffet", "noibat": "Noibat", "jynx": "Jynx", "teddiursa": "Teddiursa", "lopunny": "Lopunny", "galvantula": "Galvantula", "azurill": "Azurill", "bastiodon": "Bastiodon", "vibrava": "Vibrava", "qwilfish": "Qwilfish", "mienshao": "Mienshao", "scrafty": "Scrafty", "magnemite": "Magnemite", "squirtle": "Squirtle", "tyrunt": "Tyrunt", "misdreavus": "Misdreavus", "wormadam": "Wormadam", "florges": "Florges", "altaria": "Altaria", "delphox": "Delphox", "talonflame": "Talonflame", "leafeon": "Leafeon", "igglybuff": "Igglybuff", "cherrim": "Cherrim", "shedinja": "Shedinja", "zorua": "Zorua", "meganium": "Meganium", "tornadus": "Tornadus", "kricketune": "Kricketune", "vigoroth": "Vigoroth", "roselia": "Roselia", "rhyperior": "Rhyperior", "lombre": "Lombre", "masquerain": "Masquerain", "moltres": "Moltres", "delcatty": "Delcatty", "axew": "Axew", "tyranitar": "Tyranitar", "turtwig": "Turtwig", "fraxure": "Fraxure", "pansear": "Pansear", "deerling": "Deerling", "kecleon": "Kecleon", "boldore": "Boldore", "glameow": "Glameow", "wartortle": "Wartortle", "swinub": "Swinub", "pumpkaboo": "Pumpkaboo", "donphan": "Donphan", "gastly": "Gastly", "simisear": "Simisear", "floette": "Floette", "stunfisk": "Stunfisk", "tropius": "Tropius", "rhydon": "Rhydon", "nidoran-m": "Nidoran\u2642", "abomasnow": "Abomasnow", "infernape": "Infernape", "panpour": "Panpour", "aromatisse": "Aromatisse", "castform": "Castform", "woobat": "Woobat", "exploud": "Exploud", "onix": "Onix", "tentacruel": "Tentacruel", "armaldo": "Armaldo", "sawk": "Sawk", "kyurem": "Kyurem", "dedenne": "Dedenne", "foongus": "Foongus", "starly": "Starly", "bagon": "Bagon", "golett": "Golett", "honchkrow": "Honchkrow", "tirtouga": "Tirtouga", "ampharos": "Ampharos", "flaaffy": "Flaaffy", "gallade": "Gallade", "ledian": "Ledian", "kingdra": "Kingdra", "mandibuzz": "Mandibuzz", "luvdisc": "Luvdisc", "spiritomb": "Spiritomb", "purugly": "Purugly", "scatterbug": "Scatterbug", "eevee": "Eevee", "illumise": "Illumise", "heatran": "Heatran", "girafarig": "Girafarig", "xatu": "Xatu", "poliwrath": "Poliwrath", "gardevoir": "Gardevoir", "lilligant": "Lilligant", "bayleef": "Bayleef", "caterpie": "Caterpie", "slowbro": "Slowbro", "horsea": "Horsea", "electrike": "Electrike", "absol": "Absol", "fletchling": "Fletchling", "plusle": "Plusle", "darumaka": "Darumaka", "granbull": "Granbull", "meditite": "Meditite", "sudowoodo": "Sudowoodo", "flygon": "Flygon", "pidgeot": "Pidgeot", "archen": "Archen", "exeggcute": "Exeggcute", "lucario": "Lucario", "thundurus": "Thundurus", "vullaby": "Vullaby", "tranquill": "Tranquill", "swablu": "Swablu", "grimer": "Grimer", "dragalge": "Dragalge", "sableye": "Sableye", "seviper": "Seviper", "scolipede": "Scolipede", "haunter": "Haunter", "dwebble": "Dwebble", "slakoth": "Slakoth", "pignite": "Pignite", "makuhita": "Makuhita", "vespiquen": "Vespiquen", "palpitoad": "Palpitoad", "heliolisk": "Heliolisk", "gastrodon": "Gastrodon", "shelmet": "Shelmet", "banette": "Banette", "bronzong": "Bronzong", "weavile": "Weavile", "pachirisu": "Pachirisu", "dewott": "Dewott", "eelektrik": "Eelektrik", "chandelure": "Chandelure", "arbok": "Arbok", "haxorus": "Haxorus", "octillery": "Octillery", "venonat": "Venonat", "jolteon": "Jolteon", "machop": "Machop", "mamoswine": "Mamoswine", "shuckle": "Shuckle", "genesect": "Genesect", "cherubi": "Cherubi", "lotad": "Lotad", "blissey": "Blissey", "cryogonal": "Cryogonal", "lunatone": "Lunatone", "herdier": "Herdier", "delibird": "Delibird", "malamar": "Malamar", "entei": "Entei", "lanturn": "Lanturn", "litwick": "Litwick", "weepinbell": "Weepinbell", "riolu": "Riolu", "stunky": "Stunky", "lugia": "Lugia", "rapidash": "Rapidash", "servine": "Servine", "combusken": "Combusken", "lumineon": "Lumineon", "umbreon": "Umbreon", "corphish": "Corphish", "arcanine": "Arcanine", "sneasel": "Sneasel", "venusaur": "Venusaur", "articuno": "Articuno", "mewtwo": "Mewtwo", "volcarona": "Volcarona", "kabuto": "Kabuto", "kadabra": "Kadabra", "jigglypuff": "Jigglypuff", "sandslash": "Sandslash", "munna": "Munna", "hitmonlee": "Hitmonlee", "munchlax": "Munchlax", "aggron": "Aggron", "lampent": "Lampent", "kricketot": "Kricketot", "aipom": "Aipom", "tentacool": "Tentacool", "poliwhirl": "Poliwhirl", "luxray": "Luxray", "furfrou": "Furfrou", "skuntank": "Skuntank", "excadrill": "Excadrill", "gloom": "Gloom", "kabutops": "Kabutops", "greninja": "Greninja", "vulpix": "Vulpix", "linoone": "Linoone", "chinchou": "Chinchou", "bellsprout": "Bellsprout", "espeon": "Espeon", "dratini": "Dratini", "mantine": "Mantine", "glalie": "Glalie", "maractus": "Maractus", "psyduck": "Psyduck", "tepig": "Tepig", "glaceon": "Glaceon", "omastar": "Omastar", "persian": "Persian", "walrein": "Walrein", "mesprit": "Mesprit", "magnezone": "Magnezone", "golem": "Golem", "mew": "Mew", "shaymin": "Shaymin", "pidove": "Pidove", "salamence": "Salamence", "registeel": "Registeel", "pikachu": "Pikachu", "poochyena": "Poochyena", "emolga": "Emolga", "zweilous": "Zweilous", "porygon2": "Porygon2", "conkeldurr": "Conkeldurr", "miltank": "Miltank", "combee": "Combee", "roggenrola": "Roggenrola", "petilil": "Petilil", "hawlucha": "Hawlucha", "hippowdon": "Hippowdon", "shiftry": "Shiftry", "venomoth": "Venomoth", "cobalion": "Cobalion", "sentret": "Sentret", "beartic": "Beartic", "bergmite": "Bergmite", "nidorino": "Nidorino", "larvitar": "Larvitar", "nidorina": "Nidorina", "tyrogue": "Tyrogue", "cleffa": "Cleffa", "latias": "Latias", "meowstic": "Meowstic", "ninetales": "Ninetales", "pidgey": "Pidgey", "pineco": "Pineco", "chikorita": "Chikorita", "hydreigon": "Hydreigon", "deoxys": "Deoxys", "manaphy": "Manaphy", "skrelp": "Skrelp", "shieldon": "Shieldon", "slugma": "Slugma", "hoppip": "Hoppip", "loudred": "Loudred", "voltorb": "Voltorb", "fletchinder": "Fletchinder", "ho-oh": "Ho-Oh", "mightyena": "Mightyena", "duosion": "Duosion", "growlithe": "Growlithe", "mismagius": "Mismagius", "yamask": "Yamask", "klinklang": "Klinklang", "electabuzz": "Electabuzz", "gliscor": "Gliscor", "litleo": "Litleo", "crobat": "Crobat", "seedot": "Seedot", "diglett": "Diglett", "cacnea": "Cacnea", "machoke": "Machoke", "bunnelby": "Bunnelby", "sigilyph": "Sigilyph", "sandshrew": "Sandshrew", "torkoal": "Torkoal", "politoed": "Politoed", "chespin": "Chespin", "minun": "Minun", "venipede": "Venipede", "timburr": "Timburr", "garbodor": "Garbodor", "remoraid": "Remoraid", "virizion": "Virizion", "clefairy": "Clefairy", "meowth": "Meowth", "mime-jr": "Mime Jr.", "swalot": "Swalot", "trapinch": "Trapinch", "suicune": "Suicune", "regice": "Regice", "marshtomp": "Marshtomp", "ledyba": "Ledyba", "sunflora": "Sunflora", "seel": "Seel", "zebstrika": "Zebstrika", "blastoise": "Blastoise", "gourgeist": "Gourgeist", "barbaracle": "Barbaracle", "swirlix": "Swirlix", "chingling": "Chingling", "carracosta": "Carracosta", "gothita": "Gothita", "darmanitan": "Darmanitan", "solrock": "Solrock", "frillish": "Frillish", "carnivine": "Carnivine", "spheal": "Spheal", "kangaskhan": "Kangaskhan", "regirock": "Regirock", "ponyta": "Ponyta", "rotom": "Rotom", "amoonguss": "Amoonguss", "nuzleaf": "Nuzleaf", "pansage": "Pansage", "tynamo": "Tynamo", "drifblim": "Drifblim", "darkrai": "Darkrai", "beldum": "Beldum", "slurpuff": "Slurpuff", "barboach": "Barboach", "ferrothorn": "Ferrothorn", "carvanha": "Carvanha", "espurr": "Espurr", "sceptile": "Sceptile", "porygon": "Porygon", "dunsparce": "Dunsparce", "azumarill": "Azumarill", "vileplume": "Vileplume", "raticate": "Raticate", "slaking": "Slaking", "jirachi": "Jirachi", "golduck": "Golduck", "inkay": "Inkay", "joltik": "Joltik", "meloetta": "Meloetta", "golurk": "Golurk", "togekiss": "Togekiss", "goodra": "Goodra", "hippopotas": "Hippopotas", "dialga": "Dialga", "volbeat": "Volbeat", "drapion": "Drapion", "durant": "Durant", "yveltal": "Yveltal", "carbink": "Carbink", "dragonite": "Dragonite", "larvesta": "Larvesta", "patrat": "Patrat", "piplup": "Piplup", "stoutland": "Stoutland", "bidoof": "Bidoof", "leavanny": "Leavanny", "wurmple": "Wurmple", "tyrantrum": "Tyrantrum", "spinarak": "Spinarak", "terrakion": "Terrakion", "deino": "Deino", "krabby": "Krabby", "weedle": "Weedle", "camerupt": "Camerupt", "magikarp": "Magikarp", "pawniard": "Pawniard", "dusknoir": "Dusknoir", "snover": "Snover", "hitmonchan": "Hitmonchan", "shroomish": "Shroomish", "honedge": "Honedge", "whismur": "Whismur", "croagunk": "Croagunk", "milotic": "Milotic", "azelf": "Azelf", "wailmer": "Wailmer", "swampert": "Swampert", "skorupi": "Skorupi", "kingler": "Kingler", "muk": "Muk", "nosepass": "Nosepass", "magneton": "Magneton", "spewpa": "Spewpa", "blitzle": "Blitzle", "serperior": "Serperior", "empoleon": "Empoleon", "wooper": "Wooper", "forretress": "Forretress", "simisage": "Simisage", "victini": "Victini", "finneon": "Finneon", "electrode": "Electrode", "quagsire": "Quagsire", "farfetchd": "Farfetch'd", "archeops": "Archeops", "grumpig": "Grumpig", "arceus": "Arceus", "gyarados": "Gyarados", "phione": "Phione", "vaporeon": "Vaporeon", "crawdaunt": "Crawdaunt", "froslass": "Froslass", "purrloin": "Purrloin", "karrablast": "Karrablast", "doduo": "Doduo", "snivy": "Snivy", "aron": "Aron", "marowak": "Marowak", "hariyama": "Hariyama", "cacturne": "Cacturne", "whiscash": "Whiscash", "mothim": "Mothim", "zoroark": "Zoroark", "slowpoke": "Slowpoke", "elekid": "Elekid", "magmortar": "Magmortar", "cubone": "Cubone", "chansey": "Chansey", "pinsir": "Pinsir", "dragonair": "Dragonair", "numel": "Numel", "parasect": "Parasect", "xerneas": "Xerneas", "rattata": "Rattata", "metang": "Metang", "victreebel": "Victreebel", "smeargle": "Smeargle", "claydol": "Claydol", "skiddo": "Skiddo", "relicanth": "Relicanth", "aerodactyl": "Aerodactyl", "rhyhorn": "Rhyhorn", "steelix": "Steelix", "lapras": "Lapras", "samurott": "Samurott", "krookodile": "Krookodile", "klang": "Klang", "mankey": "Mankey", "mawile": "Mawile", "spritzee": "Spritzee", "chimecho": "Chimecho", "doublade": "Doublade", "sandile": "Sandile", "typhlosion": "Typhlosion", "ariados": "Ariados", "gible": "Gible", "dodrio": "Dodrio", "klink": "Klink", "feebas": "Feebas", "bronzor": "Bronzor", "natu": "Natu", "zapdos": "Zapdos", "cofagrigus": "Cofagrigus", "hitmontop": "Hitmontop", "floatzel": "Floatzel", "wailord": "Wailord", "swoobat": "Swoobat", "buneary": "Buneary", "mienfoo": "Mienfoo", "gigalith": "Gigalith", "liepard": "Liepard", "spoink": "Spoink", "stantler": "Stantler", "druddigon": "Druddigon", "taillow": "Taillow", "clamperl": "Clamperl", "monferno": "Monferno", "helioptile": "Helioptile", "ivysaur": "Ivysaur", "cresselia": "Cresselia", "omanyte": "Omanyte", "breloom": "Breloom", "tangela": "Tangela", "graveler": "Graveler", "swanna": "Swanna", "kakuna": "Kakuna", "kyogre": "Kyogre", "yanma": "Yanma", "vanilluxe": "Vanilluxe", "clawitzer": "Clawitzer", "basculin": "Basculin", "gurdurr": "Gurdurr", "escavalier": "Escavalier", "bisharp": "Bisharp", "nidoran-f": "Nidoran\u2640", "houndoom": "Houndoom", "beheeyem": "Beheeyem", "gulpin": "Gulpin", "landorus": "Landorus", "groudon": "Groudon", "sliggoo": "Sliggoo", "totodile": "Totodile", "pancham": "Pancham", "mantyke": "Mantyke", "dusclops": "Dusclops", "pelipper": "Pelipper", "mr-mime": "Mr. Mime", "torchic": "Torchic", "surskit": "Surskit", "luxio": "Luxio", "alakazam": "Alakazam", "binacle": "Binacle", "staryu": "Staryu", "zubat": "Zubat", "cubchoo": "Cubchoo", "cottonee": "Cottonee", "zekrom": "Zekrom", "phantump": "Phantump", "drilbur": "Drilbur", "cascoon": "Cascoon", "heracross": "Heracross", "ralts": "Ralts", "gorebyss": "Gorebyss", "pupitar": "Pupitar", "raichu": "Raichu", "grotle": "Grotle", "magcargo": "Magcargo", "slowking": "Slowking", "rayquaza": "Rayquaza", "blaziken": "Blaziken", "trubbish": "Trubbish", "noivern": "Noivern", "spinda": "Spinda", "gligar": "Gligar", "seaking": "Seaking", "dewgong": "Dewgong", "jumpluff": "Jumpluff", "kirlia": "Kirlia", "diggersby": "Diggersby", "bibarel": "Bibarel", "eelektross": "Eelektross", "sylveon": "Sylveon", "tangrowth": "Tangrowth", "shellder": "Shellder", "medicham": "Medicham", "flareon": "Flareon", "heatmor": "Heatmor", "krokorok": "Krokorok", "golbat": "Golbat", "quilladin": "Quilladin", "beautifly": "Beautifly", "poliwag": "Poliwag", "cinccino": "Cinccino", "flabebe": "Flab\u00e9b\u00e9", "spearow": "Spearow", "whirlipede": "Whirlipede", "wingull": "Wingull", "clefable": "Clefable", "scizor": "Scizor", "chimchar": "Chimchar", "yanmega": "Yanmega", "goomy": "Goomy", "silcoon": "Silcoon", "elgyem": "Elgyem", "oshawott": "Oshawott", "duskull": "Duskull", "swadloon": "Swadloon", "togepi": "Togepi", "cloyster": "Cloyster", "tauros": "Tauros", "fearow": "Fearow", "paras": "Paras", "skitty": "Skitty", "metapod": "Metapod", "raikou": "Raikou", "swellow": "Swellow", "chatot": "Chatot", "lickitung": "Lickitung", "ludicolo": "Ludicolo", "beedrill": "Beedrill", "goldeen": "Goldeen", "ekans": "Ekans", "ducklett": "Ducklett", "primeape": "Primeape", "hoothoot": "Hoothoot", "torterra": "Torterra", "sunkern": "Sunkern", "gogoat": "Gogoat", "latios": "Latios", "gothitelle": "Gothitelle", "ursaring": "Ursaring", "budew": "Budew", "ditto": "Ditto", "baltoy": "Baltoy", "skiploom": "Skiploom", "regigigas": "Regigigas", "phanpy": "Phanpy", "koffing": "Koffing", "seismitoad": "Seismitoad", "vanillite": "Vanillite", "nidoqueen": "Nidoqueen", "staravia": "Staravia", "giratina": "Giratina", "minccino": "Minccino", "skarmory": "Skarmory", "noctowl": "Noctowl", "prinplup": "Prinplup", "sewaddle": "Sewaddle", "pichu": "Pichu", "staraptor": "Staraptor", "crustle": "Crustle", "mareep": "Mareep", "huntail": "Huntail", "lileep": "Lileep", "piloswine": "Piloswine", "zygarde": "Zygarde", "rufflet": "Rufflet", "wynaut": "Wynaut", "trevenant": "Trevenant", "porygon-z": "Porygon-Z", "throh": "Throh", "musharna": "Musharna", "happiny": "Happiny", "scraggy": "Scraggy", "garchomp": "Garchomp", "geodude": "Geodude", "rampardos": "Rampardos", "jellicent": "Jellicent", "pidgeotto": "Pidgeotto", "furret": "Furret", "whimsicott": "Whimsicott", "simipour": "Simipour", "mudkip": "Mudkip", "bouffalant": "Bouffalant", "gengar": "Gengar", "roserade": "Roserade", "quilava": "Quilava", "shinx": "Shinx", "murkrow": "Murkrow", "braixen": "Braixen", "sharpedo": "Sharpedo", "pyroar": "Pyroar", "houndour": "Houndour", "scyther": "Scyther", "seadra": "Seadra", "watchog": "Watchog", "shellos": "Shellos", "nidoking": "Nidoking", "burmy": "Burmy", "toxicroak": "Toxicroak", "unown": "Unown", "cradily": "Cradily", "corsola": "Corsola", "keldeo": "Keldeo", "klefki": "Klefki", "marill": "Marill", "ambipom": "Ambipom", "bellossom": "Bellossom", "sealeo": "Sealeo", "clauncher": "Clauncher", "snorunt": "Snorunt", "croconaw": "Croconaw", "drowzee": "Drowzee", "reshiram": "Reshiram", "gabite": "Gabite", "frogadier": "Frogadier", "exeggutor": "Exeggutor", "buizel": "Buizel", "probopass": "Probopass", "solosis": "Solosis", "shuppet": "Shuppet", "anorith": "Anorith", "gothorita": "Gothorita", "aurorus": "Aurorus", "chesnaught": "Chesnaught", "dugtrio": "Dugtrio", "sawsbuck": "Sawsbuck", "ninjask": "Ninjask", "feraligatr": "Feraligatr", "oddish": "Oddish", "pangoro": "Pangoro", "magby": "Magby", "shelgon": "Shelgon", "bulbasaur": "Bulbasaur", "aegislash": "Aegislash", "uxie": "Uxie", "smoochum": "Smoochum", "butterfree": "Butterfree", "ferroseed": "Ferroseed", "charizard": "Charizard", "treecko": "Treecko", "nincada": "Nincada", "accelgor": "Accelgor", "emboar": "Emboar", "amaura": "Amaura", "dustox": "Dustox", "audino": "Audino", "alomomola": "Alomomola", "fennekin": "Fennekin", "drifloon": "Drifloon", "hypno": "Hypno", "braviary": "Braviary", "vivillon": "Vivillon", "metagross": "Metagross", "cyndaquil": "Cyndaquil", "starmie": "Starmie", "lillipup": "Lillipup", "magmar": "Magmar", "lickilicky": "Lickilicky", "cranidos": "Cranidos", "zangoose": "Zangoose", "zigzagoon": "Zigzagoon"} \ No newline at end of file diff --git a/assets.coffee b/assets.coffee new file mode 100644 index 0000000..5f90e27 --- /dev/null +++ b/assets.coffee @@ -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} diff --git a/aws_config.json b/aws_config.json new file mode 100644 index 0000000..21f31aa --- /dev/null +++ b/aws_config.json @@ -0,0 +1,6 @@ +{ + "accessKeyId": "akid", + "secretAccessKey": "secret", + "region": "us-west-2", + "sslEnabled": true +} diff --git a/aws_config.json.example b/aws_config.json.example new file mode 100644 index 0000000..21f31aa --- /dev/null +++ b/aws_config.json.example @@ -0,0 +1,6 @@ +{ + "accessKeyId": "akid", + "secretAccessKey": "secret", + "region": "us-west-2", + "sslEnabled": true +} diff --git a/bootstrap.sh b/bootstrap.sh new file mode 100644 index 0000000..3494201 --- /dev/null +++ b/bootstrap.sh @@ -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 diff --git a/client/app/css/battle.styl b/client/app/css/battle.styl new file mode 100644 index 0000000..e54f349 --- /dev/null +++ b/client/app/css/battle.styl @@ -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 diff --git a/client/app/css/battle_list.styl b/client/app/css/battle_list.styl new file mode 100644 index 0000000..6e92e77 --- /dev/null +++ b/client/app/css/battle_list.styl @@ -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% \ No newline at end of file diff --git a/client/app/css/buttons.styl b/client/app/css/buttons.styl new file mode 100644 index 0000000..a109c70 --- /dev/null +++ b/client/app/css/buttons.styl @@ -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 diff --git a/client/app/css/chat.styl b/client/app/css/chat.styl new file mode 100644 index 0000000..810f67b --- /dev/null +++ b/client/app/css/chat.styl @@ -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 diff --git a/client/app/css/colors.styl b/client/app/css/colors.styl new file mode 100644 index 0000000..e79f871 --- /dev/null +++ b/client/app/css/colors.styl @@ -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 diff --git a/client/app/css/core.styl b/client/app/css/core.styl new file mode 100644 index 0000000..1d8ee34 --- /dev/null +++ b/client/app/css/core.styl @@ -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 diff --git a/client/app/css/header.styl b/client/app/css/header.styl new file mode 100644 index 0000000..13ae209 --- /dev/null +++ b/client/app/css/header.styl @@ -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; +} diff --git a/client/app/css/layout.styl b/client/app/css/layout.styl new file mode 100644 index 0000000..1359fec --- /dev/null +++ b/client/app/css/layout.styl @@ -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 diff --git a/client/app/css/main.styl b/client/app/css/main.styl new file mode 100644 index 0000000..86a9491 --- /dev/null +++ b/client/app/css/main.styl @@ -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' diff --git a/client/app/css/main_buttons.styl b/client/app/css/main_buttons.styl new file mode 100644 index 0000000..5b58b00 --- /dev/null +++ b/client/app/css/main_buttons.styl @@ -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 diff --git a/client/app/css/messages.styl b/client/app/css/messages.styl new file mode 100644 index 0000000..90b32b5 --- /dev/null +++ b/client/app/css/messages.styl @@ -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 diff --git a/client/app/css/miscellaneous.styl b/client/app/css/miscellaneous.styl new file mode 100644 index 0000000..a0d4851 --- /dev/null +++ b/client/app/css/miscellaneous.styl @@ -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 \ No newline at end of file diff --git a/client/app/css/navigation.styl b/client/app/css/navigation.styl new file mode 100644 index 0000000..5559b4f --- /dev/null +++ b/client/app/css/navigation.styl @@ -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 diff --git a/client/app/css/responsive.styl b/client/app/css/responsive.styl new file mode 100644 index 0000000..92d4d7b --- /dev/null +++ b/client/app/css/responsive.styl @@ -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 diff --git a/client/app/css/teambuilder.styl b/client/app/css/teambuilder.styl new file mode 100644 index 0000000..3e3388f --- /dev/null +++ b/client/app/css/teambuilder.styl @@ -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 \ No newline at end of file diff --git a/client/app/js/client.coffee b/client/app/js/client.coffee new file mode 100644 index 0000000..00ac58b --- /dev/null +++ b/client/app/js/client.coffee @@ -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) diff --git a/client/app/js/collections/battles/battle_collection.coffee b/client/app/js/collections/battles/battle_collection.coffee new file mode 100644 index 0000000..1b5fe3e --- /dev/null +++ b/client/app/js/collections/battles/battle_collection.coffee @@ -0,0 +1,8 @@ +class @BattleCollection extends Backbone.Collection + model: Battle + + isPlaying: -> + @find((battle) -> battle.isPlaying())? + + playingBattles: -> + @filter((battle) -> battle.isPlaying()) diff --git a/client/app/js/collections/chats/private_messages.coffee b/client/app/js/collections/chats/private_messages.coffee new file mode 100644 index 0000000..96bb88e --- /dev/null +++ b/client/app/js/collections/chats/private_messages.coffee @@ -0,0 +1,2 @@ +class @PrivateMessages extends Backbone.Collection + model: PrivateMessage diff --git a/client/app/js/collections/chats/rooms.coffee b/client/app/js/collections/chats/rooms.coffee new file mode 100644 index 0000000..22fdd21 --- /dev/null +++ b/client/app/js/collections/chats/rooms.coffee @@ -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) diff --git a/client/app/js/collections/chats/user_list.coffee b/client/app/js/collections/chats/user_list.coffee new file mode 100644 index 0000000..d1840b4 --- /dev/null +++ b/client/app/js/collections/chats/user_list.coffee @@ -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: => diff --git a/client/app/js/collections/replays/replays.coffee b/client/app/js/collections/replays/replays.coffee new file mode 100644 index 0000000..810f664 --- /dev/null +++ b/client/app/js/collections/replays/replays.coffee @@ -0,0 +1,2 @@ +class @Replays extends Backbone.Collection + model: Replay diff --git a/client/app/js/concerns/achievements.coffee b/client/app/js/concerns/achievements.coffee new file mode 100644 index 0000000..7ee4b6a --- /dev/null +++ b/client/app/js/concerns/achievements.coffee @@ -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) \ No newline at end of file diff --git a/client/app/js/concerns/alts.coffee b/client/app/js/concerns/alts.coffee new file mode 100644 index 0000000..5bc79b1 --- /dev/null +++ b/client/app/js/concerns/alts.coffee @@ -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) diff --git a/client/app/js/concerns/battle_consumer.coffee b/client/app/js/concerns/battle_consumer.coffee new file mode 100644 index 0000000..b391041 --- /dev/null +++ b/client/app/js/concerns/battle_consumer.coffee @@ -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) diff --git a/client/app/js/concerns/battle_list.coffee b/client/app/js/concerns/battle_list.coffee new file mode 100644 index 0000000..6bf6ac2 --- /dev/null +++ b/client/app/js/concerns/battle_list.coffee @@ -0,0 +1,2 @@ +PokeBattle.primus.on 'battleList', (battles) -> + PokeBattle.battleList.refreshListComplete(battles) diff --git a/client/app/js/concerns/commands.coffee b/client/app/js/concerns/commands.coffee new file mode 100644 index 0000000..148c1f6 --- /dev/null +++ b/client/app/js/concerns/commands.coffee @@ -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 += " Also #{aliases}. " + 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("#{humanLevel} COMMANDS:") + for name, description of descriptions + message.push("/#{name}: #{description}") + message = message.join("
") + 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', "PBV error: Enter valid Pokemon or PBV.") + else + room.announce('success', "PBV: #{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 »")) + 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", "Data error: Enter a valid Pokemon, item, + move, or ability.") + 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)}", + "#{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")} +

+ #{formatName(speciesName, formeName)}: #{types.join('')} | + #{abilities}
#{statsText} | + #{_(stats).reduce((a, b) -> a + b)} BST + | PBV: #{pokeBattleValue} + #{linkToDex("pokemon/#{speciesSlug}/#{formeSlug}", "See dex entry »")} +

+ """ + message + +dataItem = (itemName) -> + item = window.Generations.XY.ItemData[itemName] + message = "#{itemName}: #{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)}", + "#{move.type}") + category = """#{move.damage}""" + target = """#{move.target}""" + power = move.power || "—" + acc = move.accuracy || "—" + maxpp = Math.floor(move.pp * 8/5) + if move.priority > 0 + priority = "+#{move.priority}" + else if move.priority < 0 + priority = move.priority + message = """#{moveName}: #{type} #{category} #{target} """ + message += "Power: #{power} Acc: #{acc} PP: #{move.pp} (max #{maxpp})" + message += "
" + message += "Priority #{priority}. " if priority + message += move.description + message += " " + message += linkToDex("moves/#{slugify(moveName)}", + "See who learns this move »") + message + +dataAbility = (abilityName) -> + ability = window.Generations.XY.AbilityData[abilityName] + message = """#{abilityName}: #{ability.description} + #{linkToDex("abilities/#{slugify(abilityName)}", + "See who obtains this ability »")}""" + 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) -> + "#{text}" + +pokemonIcon = (speciesName, formeName, classes="") -> + style = window.PokemonIconBackground(speciesName, formeName) + """""" diff --git a/client/app/js/concerns/connections.coffee b/client/app/js/concerns/connections.coffee new file mode 100644 index 0000000..39077ea --- /dev/null +++ b/client/app/js/concerns/connections.coffee @@ -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 = $('