diff options
author | Frank Karlitschek <frank@owncloud.org> | 2014-09-02 17:39:00 +0400 |
---|---|---|
committer | Frank Karlitschek <frank@owncloud.org> | 2014-09-02 17:39:00 +0400 |
commit | a262d35c3f46a511a3e4937f34352018f98335a5 (patch) | |
tree | 736c885ae2797e98b10ba74501c79ca783d03aca /pong | |
parent | dc91843a0cf4c87fdb430f9fd4bbc6ae53e4337b (diff) |
inital commit of the pong app
Diffstat (limited to 'pong')
-rwxr-xr-x | pong/LICENSE | 19 | ||||
-rwxr-xr-x | pong/README.md | 15 | ||||
-rwxr-xr-x | pong/appinfo/app.php | 25 | ||||
-rwxr-xr-x | pong/appinfo/info.xml | 10 | ||||
-rwxr-xr-x | pong/appinfo/version | 1 | ||||
-rwxr-xr-x | pong/css/pong.css | 44 | ||||
-rwxr-xr-x | pong/img/pong.png | bin | 0 -> 195 bytes | |||
-rwxr-xr-x | pong/img/press1.png | bin | 0 -> 2066 bytes | |||
-rwxr-xr-x | pong/img/press2.png | bin | 0 -> 1928 bytes | |||
-rwxr-xr-x | pong/img/winner.png | bin | 0 -> 768 bytes | |||
-rwxr-xr-x | pong/index.php | 35 | ||||
-rwxr-xr-x | pong/js/game.js | 300 | ||||
-rwxr-xr-x | pong/js/pong.js | 641 | ||||
-rwxr-xr-x | pong/js/script.js | 23 | ||||
-rwxr-xr-x | pong/sounds/goal.wav | bin | 0 -> 10690 bytes | |||
-rwxr-xr-x | pong/sounds/ping.wav | bin | 0 -> 4700 bytes | |||
-rwxr-xr-x | pong/sounds/pong.wav | bin | 0 -> 2220 bytes | |||
-rwxr-xr-x | pong/sounds/wall.wav | bin | 0 -> 1068 bytes | |||
-rwxr-xr-x | pong/templates/main.php | 33 |
19 files changed, 1146 insertions, 0 deletions
diff --git a/pong/LICENSE b/pong/LICENSE new file mode 100755 index 000000000..45aed8553 --- /dev/null +++ b/pong/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2011, 2012, 2013 Jake Gordon and contributors + +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/pong/README.md b/pong/README.md new file mode 100755 index 000000000..206abdb7b --- /dev/null +++ b/pong/README.md @@ -0,0 +1,15 @@ +Canvas Pong +=========== + +This is a small experiment to implement a version of the classic Pong game in an HTML <canvas>. + + * You can find the [game here](http://codeincomplete.com/posts/2011/5/14/javascript_pong/demo.html) + * You can find out [how it works](http://codeincomplete.com/posts/2011/5/14/javascript_pong/index.html) + +You can also play this game in the [Chrome Web Store](https://chrome.google.com/webstore/detail/omimkinlomnncbmnceacpkmlbfaapojj?hl=en-US) + + +NOTES +===== + + * No support for mobile devices (need to handle touch events instead of key input) diff --git a/pong/appinfo/app.php b/pong/appinfo/app.php new file mode 100755 index 000000000..33d9d43a0 --- /dev/null +++ b/pong/appinfo/app.php @@ -0,0 +1,25 @@ +<?php + +/** + * ownCloud - pong app + * + * @author Frank Karlitschek + * @copyright 2014 Frank Karlitschek frank@owncloud.org + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE + * License as published by the Free Software Foundation; either + * version 3 of the License, or any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU AFFERO GENERAL PUBLIC LICENSE for more details. + * + * You should have received a copy of the GNU Affero General Public + * License along with this library. If not, see <http://www.gnu.org/licenses/>. + * + */ + +OCP\App::addNavigationEntry( + array('id' => 'pong', 'order' => 80, 'href' => OCP\Util::linkTo('pong', 'index.php') , 'icon' => OCP\Util::imagePath('pong', 'pong.png'), 'name' => 'Pong')); diff --git a/pong/appinfo/info.xml b/pong/appinfo/info.xml new file mode 100755 index 000000000..672032f73 --- /dev/null +++ b/pong/appinfo/info.xml @@ -0,0 +1,10 @@ +<?xml version="1.0"?> +<info> + <id>pong</id> + <name>Pong</name> + <description>A simple pong game. This is using the great javascript pong implementation from Jake Gordon. http://codeincomplete.com/posts/2011/5/14/javascript_pong/</description> + <licence>AGPL, MIT</licence> + <author>Jake Gordon, Frank Karlitschek</author> + <requiremin>7.0</requiremin> + <shipped>false</shipped> +</info> diff --git a/pong/appinfo/version b/pong/appinfo/version new file mode 100755 index 000000000..9f8e9b69a --- /dev/null +++ b/pong/appinfo/version @@ -0,0 +1 @@ +1.0
\ No newline at end of file diff --git a/pong/css/pong.css b/pong/css/pong.css new file mode 100755 index 000000000..50baed942 --- /dev/null +++ b/pong/css/pong.css @@ -0,0 +1,44 @@ +body { background-color: black; color: #AAA; font-size: 12pt; padding: 1em; } + +#unsupported { border: 1px solid yellow; color: black; background-color: #FFFFAD; padding: 2em; margin: 1em; display: inline-block; } + +#sidebar { float: right; font-size: 0.825em; background-color: #333; border: 1px solid white; padding: 1em; margin: 1em; } +#sidebar h2 { color: white; text-align: center; margin: 0; } +#sidebar .parts { padding-left: 1em; list-style-type: none; margin-bottom: 2em; text-align: right; } +#sidebar .parts li a { color: white; text-decoration: none; } +#sidebar .parts li a:visited { color: white; } +#sidebar .parts li a:hover { color: white; text-decoration: underline; } +#sidebar .parts li a.selected { color: #F08010; } +#sidebar .parts li a i { color: #AAA; } +#sidebar .parts li a.selected i { color: #F08010; } +#sidebar .settings { line-height: 1.2em; height: 1.2em; text-align: right; } +#sidebar .settings.size { } +#sidebar .settings.speed { margin-bottom: 1em; } +#sidebar .settings label { vertical-align: middle; } +#sidebar .settings input { vertical-align: middle; } +#sidebar .settings select { vertical-align: middle; } +#sidebar .description { margin-bottom: 2em; } +#sidebar .description b { font-weight: normal; color: #FFF; } + + +@media screen and (min-width: 0px) { + #sidebar { display: none; } + #game { display: block; width: 480px; height: 360px; margin: 0 auto; margin-top: 1em;} +} + +@media screen and (min-width: 800px) { + #game { width: 640px; height: 480px; } +} + +@media screen and (min-width: 1000px) { + #sidebar { display: block; } + #game { margin-left: 18em; } +} + +@media screen and (min-width: 1200px) { + #game { width: 800px; height: 600px; } +} + +@media screen and (min-width: 1600px) { + #game { width: 1024px; height: 768px; } +} diff --git a/pong/img/pong.png b/pong/img/pong.png Binary files differnew file mode 100755 index 000000000..a0adb050b --- /dev/null +++ b/pong/img/pong.png diff --git a/pong/img/press1.png b/pong/img/press1.png Binary files differnew file mode 100755 index 000000000..cd0874cb9 --- /dev/null +++ b/pong/img/press1.png diff --git a/pong/img/press2.png b/pong/img/press2.png Binary files differnew file mode 100755 index 000000000..52ca2b254 --- /dev/null +++ b/pong/img/press2.png diff --git a/pong/img/winner.png b/pong/img/winner.png Binary files differnew file mode 100755 index 000000000..50f4558f8 --- /dev/null +++ b/pong/img/winner.png diff --git a/pong/index.php b/pong/index.php new file mode 100755 index 000000000..ae82fe9f9 --- /dev/null +++ b/pong/index.php @@ -0,0 +1,35 @@ +<?php + +/** + * ownCloud - pong + * + * @author Frank Karlitschek + * @copyright 2014 Frank Karlitschek frank@owncloud.org + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE + * License as published by the Free Software Foundation; either + * version 3 of the License, or any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU AFFERO GENERAL PUBLIC LICENSE for more details. + * + * You should have received a copy of the GNU Affero General Public + * License along with this library. If not, see <http://www.gnu.org/licenses/>. + * + */ + +OCP\User::checkLoggedIn(); + +OCP\App::setActiveNavigationEntry('pong'); + +OCP\Util::addStyle( 'pong', 'pong' ); +OCP\Util::addScript( 'pong', 'game' ); +OCP\Util::addScript( 'pong', 'pong' ); +OCP\Util::addScript( 'pong', 'script' ); + +$tmpl = new OCP\Template('pong', 'main', 'user'); +$tmpl->printPage(); + diff --git a/pong/js/game.js b/pong/js/game.js new file mode 100755 index 000000000..370d00749 --- /dev/null +++ b/pong/js/game.js @@ -0,0 +1,300 @@ +//============================================================================= +// +// We need some ECMAScript 5 methods but we need to implement them ourselves +// for older browsers (compatibility: http://kangax.github.com/es5-compat-table/) +// +// Function.bind: https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Function/bind +// Object.create: http://javascript.crockford.com/prototypal.html +// Object.extend: (defacto standard like jquery $.extend or prototype's Object.extend) +// +// Object.construct: our own wrapper around Object.create that ALSO calls +// an initialize constructor method if one exists +// +//============================================================================= + +if (!Function.prototype.bind) { + Function.prototype.bind = function(obj) { + var slice = [].slice, + args = slice.call(arguments, 1), + self = this, + nop = function () {}, + bound = function () { + return self.apply(this instanceof nop ? this : (obj || {}), args.concat(slice.call(arguments))); + }; + nop.prototype = self.prototype; + bound.prototype = new nop(); + return bound; + }; +} + +if (!Object.create) { + Object.create = function(base) { + function F() {}; + F.prototype = base; + return new F(); + } +} + +if (!Object.construct) { + Object.construct = function(base) { + var instance = Object.create(base); + if (instance.initialize) + instance.initialize.apply(instance, [].slice.call(arguments, 1)); + return instance; + } +} + +if (!Object.extend) { + Object.extend = function(destination, source) { + for (var property in source) { + if (source.hasOwnProperty(property)) + destination[property] = source[property]; + } + return destination; + }; +} + +/* NOT READY FOR PRIME TIME +if (!window.requestAnimationFrame) {// http://paulirish.com/2011/requestanimationframe-for-smart-animating/ + window.requestAnimationFrame = window.webkitRequestAnimationFrame || + window.mozRequestAnimationFrame || + window.oRequestAnimationFrame || + window.msRequestAnimationFrame || + function(callback, element) { + window.setTimeout(callback, 1000 / 60); + } +} +*/ + +//============================================================================= +// GAME +//============================================================================= + +Game = { + + compatible: function() { + return Object.create && + Object.extend && + Function.bind && + document.addEventListener && // HTML5 standard, all modern browsers that support canvas should also support add/removeEventListener + Game.ua.hasCanvas + }, + + start: function(id, game, cfg) { + if (Game.compatible()) + return Object.construct(Game.Runner, id, game, cfg).game; // return the game instance, not the runner (caller can always get at the runner via game.runner) + }, + + ua: function() { // should avoid user agent sniffing... but sometimes you just gotta do what you gotta do + var ua = navigator.userAgent.toLowerCase(); + var key = ((ua.indexOf("opera") > -1) ? "opera" : null); + key = key || ((ua.indexOf("firefox") > -1) ? "firefox" : null); + key = key || ((ua.indexOf("chrome") > -1) ? "chrome" : null); + key = key || ((ua.indexOf("safari") > -1) ? "safari" : null); + key = key || ((ua.indexOf("msie") > -1) ? "ie" : null); + + try { + var re = (key == "ie") ? "msie (\\d)" : key + "\\/(\\d\\.\\d)" + var matches = ua.match(new RegExp(re, "i")); + var version = matches ? parseFloat(matches[1]) : null; + } catch (e) {} + + return { + full: ua, + name: key + (version ? " " + version.toString() : ""), + version: version, + isFirefox: (key == "firefox"), + isChrome: (key == "chrome"), + isSafari: (key == "safari"), + isOpera: (key == "opera"), + isIE: (key == "ie"), + hasCanvas: (document.createElement('canvas').getContext), + hasAudio: (typeof(Audio) != 'undefined') + } + }(), + + addEvent: function(obj, type, fn) { obj.addEventListener(type, fn, false); }, + removeEvent: function(obj, type, fn) { obj.removeEventListener(type, fn, false); }, + + ready: function(fn) { + if (Game.compatible()) + Game.addEvent(document, 'DOMContentLoaded', fn); + }, + + createCanvas: function() { + return document.createElement('canvas'); + }, + + createAudio: function(src) { + try { + var a = new Audio(src); + a.volume = 0.1; // lets be real quiet please + return a; + } catch (e) { + return null; + } + }, + + loadImages: function(sources, callback) { /* load multiple images and callback when ALL have finished loading */ + var images = {}; + var count = sources ? sources.length : 0; + if (count == 0) { + callback(images); + } + else { + for(var n = 0 ; n < sources.length ; n++) { + var source = sources[n]; + var image = document.createElement('img'); + images[source] = image; + Game.addEvent(image, 'load', function() { if (--count == 0) callback(images); }); + image.src = source; + } + } + }, + + random: function(min, max) { + return (min + (Math.random() * (max - min))); + }, + + timestamp: function() { + return new Date().getTime(); + }, + + KEY: { + BACKSPACE: 8, + TAB: 9, + RETURN: 13, + ESC: 27, + SPACE: 32, + LEFT: 37, + UP: 38, + RIGHT: 39, + DOWN: 40, + DELETE: 46, + HOME: 36, + END: 35, + PAGEUP: 33, + PAGEDOWN: 34, + INSERT: 45, + ZERO: 48, + ONE: 49, + TWO: 50, + A: 65, + L: 76, + P: 80, + Q: 81, + TILDA: 192 + }, + + //----------------------------------------------------------------------------- + + Runner: { + + initialize: function(id, game, cfg) { + this.cfg = Object.extend(game.Defaults || {}, cfg || {}); // use game defaults (if any) and extend with custom cfg (if any) + this.fps = this.cfg.fps || 60; + this.interval = 1000.0 / this.fps; + this.canvas = document.getElementById(id); + this.width = this.cfg.width || this.canvas.offsetWidth; + this.height = this.cfg.height || this.canvas.offsetHeight; + this.front = this.canvas; + this.front.width = this.width; + this.front.height = this.height; + this.back = Game.createCanvas(); + this.back.width = this.width; + this.back.height = this.height; + this.front2d = this.front.getContext('2d'); + this.back2d = this.back.getContext('2d'); + this.addEvents(); + this.resetStats(); + + this.game = Object.construct(game, this, this.cfg); // finally construct the game object itself + }, + + start: function() { // game instance should call runner.start() when its finished initializing and is ready to start the game loop + this.lastFrame = Game.timestamp(); + this.timer = setInterval(this.loop.bind(this), this.interval); + }, + + stop: function() { + clearInterval(this.timer); + }, + + loop: function() { + var start = Game.timestamp(); this.update((start - this.lastFrame)/1000.0); // send dt as seconds + var middle = Game.timestamp(); this.draw(); + var end = Game.timestamp(); + this.updateStats(middle - start, end - middle); + this.lastFrame = start; + }, + + update: function(dt) { + this.game.update(dt); + }, + + draw: function() { + this.back2d.clearRect(0, 0, this.width, this.height); + this.game.draw(this.back2d); + this.drawStats(this.back2d); + this.front2d.clearRect(0, 0, this.width, this.height); + this.front2d.drawImage(this.back, 0, 0); + }, + + resetStats: function() { + this.stats = { + count: 0, + fps: 0, + update: 0, + draw: 0, + frame: 0 // update + draw + }; + }, + + updateStats: function(update, draw) { + if (this.cfg.stats) { + this.stats.update = Math.max(1, update); + this.stats.draw = Math.max(1, draw); + this.stats.frame = this.stats.update + this.stats.draw; + this.stats.count = this.stats.count == this.fps ? 0 : this.stats.count + 1; + this.stats.fps = Math.min(this.fps, 1000 / this.stats.frame); + } + }, + + drawStats: function(ctx) { + if (this.cfg.stats) { + ctx.fillText("frame: " + this.stats.count, this.width - 100, this.height - 60); + ctx.fillText("fps: " + this.stats.fps, this.width - 100, this.height - 50); + ctx.fillText("update: " + this.stats.update + "ms", this.width - 100, this.height - 40); + ctx.fillText("draw: " + this.stats.draw + "ms", this.width - 100, this.height - 30); + } + }, + + addEvents: function() { + Game.addEvent(document, 'keydown', this.onkeydown.bind(this)); + Game.addEvent(document, 'keyup', this.onkeyup.bind(this)); + }, + + onkeydown: function(ev) { if (this.game.onkeydown) this.game.onkeydown(ev.keyCode); }, + onkeyup: function(ev) { if (this.game.onkeyup) this.game.onkeyup(ev.keyCode); }, + + hideCursor: function() { this.canvas.style.cursor = 'none'; }, + showCursor: function() { this.canvas.style.cursor = 'auto'; }, + + alert: function(msg) { + this.stop(); // alert blocks thread, so need to stop game loop in order to avoid sending huge dt values to next update + result = window.alert(msg); + this.start(); + return result; + }, + + confirm: function(msg) { + this.stop(); // alert blocks thread, so need to stop game loop in order to avoid sending huge dt values to next update + result = window.confirm(msg); + this.start(); + return result; + } + + //------------------------------------------------------------------------- + + } // Game.Runner +} // Game diff --git a/pong/js/pong.js b/pong/js/pong.js new file mode 100755 index 000000000..38b121e0a --- /dev/null +++ b/pong/js/pong.js @@ -0,0 +1,641 @@ +//============================================================================= +// PONG +//============================================================================= + +Pong = { + + Defaults: { + width: 640, // logical canvas width (browser will scale to physical canvas size - which is controlled by @media css queries) + height: 480, // logical canvas height (ditto) + wallWidth: 12, + paddleWidth: 12, + paddleHeight: 60, + paddleSpeed: 2, // should be able to cross court vertically in 2 seconds + ballSpeed: 4, // should be able to cross court horizontally in 4 seconds, at starting speed ... + ballAccel: 8, // ... but accelerate as time passes + ballRadius: 5, + sound: true + }, + + Colors: { + walls: 'white', + ball: 'white', + score: 'white', + footprint: '#333', + predictionGuess: 'yellow', + predictionExact: 'red' + }, + + Images: [ + OC.imagePath('pong', 'press1.png'), + OC.imagePath('pong', 'press2.png'), + OC.imagePath('pong', 'winner.png'), + ], + + Levels: [ + {aiReaction: 0.2, aiError: 40}, // 0: ai is losing by 8 + {aiReaction: 0.3, aiError: 50}, // 1: ai is losing by 7 + {aiReaction: 0.4, aiError: 60}, // 2: ai is losing by 6 + {aiReaction: 0.5, aiError: 70}, // 3: ai is losing by 5 + {aiReaction: 0.6, aiError: 80}, // 4: ai is losing by 4 + {aiReaction: 0.7, aiError: 90}, // 5: ai is losing by 3 + {aiReaction: 0.8, aiError: 100}, // 6: ai is losing by 2 + {aiReaction: 0.9, aiError: 110}, // 7: ai is losing by 1 + {aiReaction: 1.0, aiError: 120}, // 8: tie + {aiReaction: 1.1, aiError: 130}, // 9: ai is winning by 1 + {aiReaction: 1.2, aiError: 140}, // 10: ai is winning by 2 + {aiReaction: 1.3, aiError: 150}, // 11: ai is winning by 3 + {aiReaction: 1.4, aiError: 160}, // 12: ai is winning by 4 + {aiReaction: 1.5, aiError: 170}, // 13: ai is winning by 5 + {aiReaction: 1.6, aiError: 180}, // 14: ai is winning by 6 + {aiReaction: 1.7, aiError: 190}, // 15: ai is winning by 7 + {aiReaction: 1.8, aiError: 200} // 16: ai is winning by 8 + ], + + //----------------------------------------------------------------------------- + + initialize: function(runner, cfg) { + Game.loadImages(Pong.Images, function(images) { + this.cfg = cfg; + this.runner = runner; + this.width = runner.width; + this.height = runner.height; + this.images = images; + this.playing = false; + this.scores = [0, 0]; + this.menu = Object.construct(Pong.Menu, this); + this.court = Object.construct(Pong.Court, this); + this.leftPaddle = Object.construct(Pong.Paddle, this); + this.rightPaddle = Object.construct(Pong.Paddle, this, true); + this.ball = Object.construct(Pong.Ball, this); + this.sounds = Object.construct(Pong.Sounds, this); + this.runner.start(); + }.bind(this)); + }, + + startDemo: function() { this.start(0); }, + startSinglePlayer: function() { this.start(1); }, + startDoublePlayer: function() { this.start(2); }, + + start: function(numPlayers) { + if (!this.playing) { + this.scores = [0, 0]; + this.playing = true; + this.leftPaddle.setAuto(numPlayers < 1, this.level(0)); + this.rightPaddle.setAuto(numPlayers < 2, this.level(1)); + this.ball.reset(); + this.runner.hideCursor(); + } + }, + + stop: function(ask) { + if (this.playing) { + if (!ask || this.runner.confirm('Abandon game in progress ?')) { + this.playing = false; + this.leftPaddle.setAuto(false); + this.rightPaddle.setAuto(false); + this.runner.showCursor(); + } + } + }, + + level: function(playerNo) { + return 8 + (this.scores[playerNo] - this.scores[playerNo ? 0 : 1]); + }, + + goal: function(playerNo) { + this.sounds.goal(); + this.scores[playerNo] += 1; + if (this.scores[playerNo] == 9) { + this.menu.declareWinner(playerNo); + this.stop(); + } + else { + this.ball.reset(playerNo); + this.leftPaddle.setLevel(this.level(0)); + this.rightPaddle.setLevel(this.level(1)); + } + }, + + update: function(dt) { + this.leftPaddle.update(dt, this.ball); + this.rightPaddle.update(dt, this.ball); + if (this.playing) { + var dx = this.ball.dx; + var dy = this.ball.dy; + this.ball.update(dt, this.leftPaddle, this.rightPaddle); + if (this.ball.dx < 0 && dx > 0) + this.sounds.ping(); + else if (this.ball.dx > 0 && dx < 0) + this.sounds.pong(); + else if (this.ball.dy * dy < 0) + this.sounds.wall(); + + if (this.ball.left > this.width) + this.goal(0); + else if (this.ball.right < 0) + this.goal(1); + } + }, + + draw: function(ctx) { + this.court.draw(ctx, this.scores[0], this.scores[1]); + this.leftPaddle.draw(ctx); + this.rightPaddle.draw(ctx); + if (this.playing) + this.ball.draw(ctx); + else + this.menu.draw(ctx); + }, + + onkeydown: function(keyCode) { + switch(keyCode) { + case Game.KEY.ZERO: this.startDemo(); break; + case Game.KEY.ONE: this.startSinglePlayer(); break; + case Game.KEY.TWO: this.startDoublePlayer(); break; + case Game.KEY.ESC: this.stop(true); break; + case Game.KEY.Q: if (!this.leftPaddle.auto) this.leftPaddle.moveUp(); break; + case Game.KEY.A: if (!this.leftPaddle.auto) this.leftPaddle.moveDown(); break; + case Game.KEY.P: if (!this.rightPaddle.auto) this.rightPaddle.moveUp(); break; + case Game.KEY.L: if (!this.rightPaddle.auto) this.rightPaddle.moveDown(); break; + } + }, + + onkeyup: function(keyCode) { + switch(keyCode) { + case Game.KEY.Q: if (!this.leftPaddle.auto) this.leftPaddle.stopMovingUp(); break; + case Game.KEY.A: if (!this.leftPaddle.auto) this.leftPaddle.stopMovingDown(); break; + case Game.KEY.P: if (!this.rightPaddle.auto) this.rightPaddle.stopMovingUp(); break; + case Game.KEY.L: if (!this.rightPaddle.auto) this.rightPaddle.stopMovingDown(); break; + } + }, + + showStats: function(on) { this.cfg.stats = on; }, + showFootprints: function(on) { this.cfg.footprints = on; this.ball.footprints = []; }, + showPredictions: function(on) { this.cfg.predictions = on; }, + enableSound: function(on) { this.cfg.sound = on; }, + + //============================================================================= + // MENU + //============================================================================= + + Menu: { + + initialize: function(pong) { + var press1 = pong.images[OC.imagePath('pong', 'press1.png')]; + var press2 = pong.images[OC.imagePath('pong', 'press2.png')]; + var winner = pong.images[OC.imagePath('pong', 'winner.png')]; + this.press1 = { image: press1, x: 10, y: pong.cfg.wallWidth }; + this.press2 = { image: press2, x: (pong.width - press2.width - 10), y: pong.cfg.wallWidth }; + this.winner1 = { image: winner, x: (pong.width/2) - winner.width - pong.cfg.wallWidth, y: 6 * pong.cfg.wallWidth }; + this.winner2 = { image: winner, x: (pong.width/2) + pong.cfg.wallWidth, y: 6 * pong.cfg.wallWidth }; + }, + + declareWinner: function(playerNo) { + this.winner = playerNo; + }, + + draw: function(ctx) { + ctx.drawImage(this.press1.image, this.press1.x, this.press1.y); + ctx.drawImage(this.press2.image, this.press2.x, this.press2.y); + if (this.winner == 0) + ctx.drawImage(this.winner1.image, this.winner1.x, this.winner1.y); + else if (this.winner == 1) + ctx.drawImage(this.winner2.image, this.winner2.x, this.winner2.y); + } + + }, + + //============================================================================= + // SOUNDS + //============================================================================= + + Sounds: { + + initialize: function(pong) { + this.game = pong; + this.supported = Game.ua.hasAudio; + if (this.supported) { + this.files = { + ping: Game.createAudio("sounds/ping.wav"), + pong: Game.createAudio("sounds/pong.wav"), + wall: Game.createAudio("sounds/wall.wav"), + goal: Game.createAudio("sounds/goal.wav") + }; + } + }, + + play: function(name) { + if (this.supported && this.game.cfg.sound && this.files[name]) + this.files[name].play(); + }, + + ping: function() { this.play('ping'); }, + pong: function() { this.play('pong'); }, + wall: function() { /*this.play('wall');*/ }, + goal: function() { /*this.play('goal');*/ } + + }, + + //============================================================================= + // COURT + //============================================================================= + + Court: { + + initialize: function(pong) { + var w = pong.width; + var h = pong.height; + var ww = pong.cfg.wallWidth; + + this.ww = ww; + this.walls = []; + this.walls.push({x: 0, y: 0, width: w, height: ww}); + this.walls.push({x: 0, y: h - ww, width: w, height: ww}); + var nMax = (h / (ww*2)); + for(var n = 0 ; n < nMax ; n++) { // draw dashed halfway line + this.walls.push({x: (w / 2) - (ww / 2), + y: (ww / 2) + (ww * 2 * n), + width: ww, height: ww}); + } + + var sw = 3*ww; + var sh = 4*ww; + this.score1 = {x: 0.5 + (w/2) - 1.5*ww - sw, y: 2*ww, w: sw, h: sh}; + this.score2 = {x: 0.5 + (w/2) + 1.5*ww, y: 2*ww, w: sw, h: sh}; + }, + + draw: function(ctx, scorePlayer1, scorePlayer2) { + ctx.fillStyle = Pong.Colors.walls; + for(var n = 0 ; n < this.walls.length ; n++) + ctx.fillRect(this.walls[n].x, this.walls[n].y, this.walls[n].width, this.walls[n].height); + this.drawDigit(ctx, scorePlayer1, this.score1.x, this.score1.y, this.score1.w, this.score1.h); + this.drawDigit(ctx, scorePlayer2, this.score2.x, this.score2.y, this.score2.w, this.score2.h); + }, + + drawDigit: function(ctx, n, x, y, w, h) { + ctx.fillStyle = Pong.Colors.score; + var dw = dh = this.ww*4/5; + var blocks = Pong.Court.DIGITS[n]; + if (blocks[0]) + ctx.fillRect(x, y, w, dh); + if (blocks[1]) + ctx.fillRect(x, y, dw, h/2); + if (blocks[2]) + ctx.fillRect(x+w-dw, y, dw, h/2); + if (blocks[3]) + ctx.fillRect(x, y + h/2 - dh/2, w, dh); + if (blocks[4]) + ctx.fillRect(x, y + h/2, dw, h/2); + if (blocks[5]) + ctx.fillRect(x+w-dw, y + h/2, dw, h/2); + if (blocks[6]) + ctx.fillRect(x, y+h-dh, w, dh); + }, + + DIGITS: [ + [1, 1, 1, 0, 1, 1, 1], // 0 + [0, 0, 1, 0, 0, 1, 0], // 1 + [1, 0, 1, 1, 1, 0, 1], // 2 + [1, 0, 1, 1, 0, 1, 1], // 3 + [0, 1, 1, 1, 0, 1, 0], // 4 + [1, 1, 0, 1, 0, 1, 1], // 5 + [1, 1, 0, 1, 1, 1, 1], // 6 + [1, 0, 1, 0, 0, 1, 0], // 7 + [1, 1, 1, 1, 1, 1, 1], // 8 + [1, 1, 1, 1, 0, 1, 0] // 9 + ] + + }, + + //============================================================================= + // PADDLE + //============================================================================= + + Paddle: { + + initialize: function(pong, rhs) { + this.pong = pong; + this.width = pong.cfg.paddleWidth; + this.height = pong.cfg.paddleHeight; + this.minY = pong.cfg.wallWidth; + this.maxY = pong.height - pong.cfg.wallWidth - this.height; + this.speed = (this.maxY - this.minY) / pong.cfg.paddleSpeed; + this.setpos(rhs ? pong.width - this.width : 0, this.minY + (this.maxY - this.minY)/2); + this.setdir(0); + }, + + setpos: function(x, y) { + this.x = x; + this.y = y; + this.left = this.x; + this.right = this.left + this.width; + this.top = this.y; + this.bottom = this.y + this.height; + }, + + setdir: function(dy) { + this.up = (dy < 0 ? -dy : 0); + this.down = (dy > 0 ? dy : 0); + }, + + setAuto: function(on, level) { + if (on && !this.auto) { + this.auto = true; + this.setLevel(level); + } + else if (!on && this.auto) { + this.auto = false; + this.setdir(0); + } + }, + + setLevel: function(level) { + if (this.auto) + this.level = Pong.Levels[level]; + }, + + update: function(dt, ball) { + if (this.auto) + this.ai(dt, ball); + + var amount = this.down - this.up; + if (amount != 0) { + var y = this.y + (amount * dt * this.speed); + if (y < this.minY) + y = this.minY; + else if (y > this.maxY) + y = this.maxY; + this.setpos(this.x, y); + } + }, + + ai: function(dt, ball) { + if (((ball.x < this.left) && (ball.dx < 0)) || + ((ball.x > this.right) && (ball.dx > 0))) { + this.stopMovingUp(); + this.stopMovingDown(); + return; + } + + this.predict(ball, dt); + + if (this.prediction) { + if (this.prediction.y < (this.top + this.height/2 - 5)) { + this.stopMovingDown(); + this.moveUp(); + } + else if (this.prediction.y > (this.bottom - this.height/2 + 5)) { + this.stopMovingUp(); + this.moveDown(); + } + else { + this.stopMovingUp(); + this.stopMovingDown(); + } + } + }, + + predict: function(ball, dt) { + // only re-predict if the ball changed direction, or its been some amount of time since last prediction + if (this.prediction && + ((this.prediction.dx * ball.dx) > 0) && + ((this.prediction.dy * ball.dy) > 0) && + (this.prediction.since < this.level.aiReaction)) { + this.prediction.since += dt; + return; + } + + var pt = Pong.Helper.ballIntercept(ball, {left: this.left, right: this.right, top: -10000, bottom: 10000}, ball.dx * 10, ball.dy * 10); + if (pt) { + var t = this.minY + ball.radius; + var b = this.maxY + this.height - ball.radius; + + while ((pt.y < t) || (pt.y > b)) { + if (pt.y < t) { + pt.y = t + (t - pt.y); + } + else if (pt.y > b) { + pt.y = t + (b - t) - (pt.y - b); + } + } + this.prediction = pt; + } + else { + this.prediction = null; + } + + if (this.prediction) { + this.prediction.since = 0; + this.prediction.dx = ball.dx; + this.prediction.dy = ball.dy; + this.prediction.radius = ball.radius; + this.prediction.exactX = this.prediction.x; + this.prediction.exactY = this.prediction.y; + var closeness = (ball.dx < 0 ? ball.x - this.right : this.left - ball.x) / this.pong.width; + var error = this.level.aiError * closeness; + this.prediction.y = this.prediction.y + Game.random(-error, error); + } + }, + + draw: function(ctx) { + ctx.fillStyle = Pong.Colors.walls; + ctx.fillRect(this.x, this.y, this.width, this.height); + if (this.prediction && this.pong.cfg.predictions) { + ctx.strokeStyle = Pong.Colors.predictionExact; + ctx.strokeRect(this.prediction.x - this.prediction.radius, this.prediction.exactY - this.prediction.radius, this.prediction.radius*2, this.prediction.radius*2); + ctx.strokeStyle = Pong.Colors.predictionGuess; + ctx.strokeRect(this.prediction.x - this.prediction.radius, this.prediction.y - this.prediction.radius, this.prediction.radius*2, this.prediction.radius*2); + } + }, + + moveUp: function() { this.up = 1; }, + moveDown: function() { this.down = 1; }, + stopMovingUp: function() { this.up = 0; }, + stopMovingDown: function() { this.down = 0; } + + }, + + //============================================================================= + // BALL + //============================================================================= + + Ball: { + + initialize: function(pong) { + this.pong = pong; + this.radius = pong.cfg.ballRadius; + this.minX = this.radius; + this.maxX = pong.width - this.radius; + this.minY = pong.cfg.wallWidth + this.radius; + this.maxY = pong.height - pong.cfg.wallWidth - this.radius; + this.speed = (this.maxX - this.minX) / pong.cfg.ballSpeed; + this.accel = pong.cfg.ballAccel; + }, + + reset: function(playerNo) { + this.footprints = []; + this.setpos(playerNo == 1 ? this.maxX : this.minX, Game.random(this.minY, this.maxY)); + this.setdir(playerNo == 1 ? -this.speed : this.speed, this.speed); + }, + + setpos: function(x, y) { + this.x = x; + this.y = y; + this.left = this.x - this.radius; + this.top = this.y - this.radius; + this.right = this.x + this.radius; + this.bottom = this.y + this.radius; + }, + + setdir: function(dx, dy) { + this.dxChanged = ((this.dx < 0) != (dx < 0)); // did horizontal direction change + this.dyChanged = ((this.dy < 0) != (dy < 0)); // did vertical direction change + this.dx = dx; + this.dy = dy; + }, + + footprint: function() { + if (this.pong.cfg.footprints) { + if (!this.footprintCount || this.dxChanged || this.dyChanged) { + this.footprints.push({x: this.x, y: this.y}); + if (this.footprints.length > 50) + this.footprints.shift(); + this.footprintCount = 5; + } + else { + this.footprintCount--; + } + } + }, + + update: function(dt, leftPaddle, rightPaddle) { + + pos = Pong.Helper.accelerate(this.x, this.y, this.dx, this.dy, this.accel, dt); + + if ((pos.dy > 0) && (pos.y > this.maxY)) { + pos.y = this.maxY; + pos.dy = -pos.dy; + } + else if ((pos.dy < 0) && (pos.y < this.minY)) { + pos.y = this.minY; + pos.dy = -pos.dy; + } + + var paddle = (pos.dx < 0) ? leftPaddle : rightPaddle; + var pt = Pong.Helper.ballIntercept(this, paddle, pos.nx, pos.ny); + + if (pt) { + switch(pt.d) { + case 'left': + case 'right': + pos.x = pt.x; + pos.dx = -pos.dx; + break; + case 'top': + case 'bottom': + pos.y = pt.y; + pos.dy = -pos.dy; + break; + } + + // add/remove spin based on paddle direction + if (paddle.up) + pos.dy = pos.dy * (pos.dy < 0 ? 0.5 : 1.5); + else if (paddle.down) + pos.dy = pos.dy * (pos.dy > 0 ? 0.5 : 1.5); + } + + this.setpos(pos.x, pos.y); + this.setdir(pos.dx, pos.dy); + this.footprint(); + }, + + draw: function(ctx) { + var w = h = this.radius * 2; + ctx.fillStyle = Pong.Colors.ball; + ctx.fillRect(this.x - this.radius, this.y - this.radius, w, h); + if (this.pong.cfg.footprints) { + var max = this.footprints.length; + ctx.strokeStyle = Pong.Colors.footprint; + for(var n = 0 ; n < max ; n++) + ctx.strokeRect(this.footprints[n].x - this.radius, this.footprints[n].y - this.radius, w, h); + } + } + + }, + + //============================================================================= + // HELPER + //============================================================================= + + Helper: { + + accelerate: function(x, y, dx, dy, accel, dt) { + var x2 = x + (dt * dx) + (accel * dt * dt * 0.5); + var y2 = y + (dt * dy) + (accel * dt * dt * 0.5); + var dx2 = dx + (accel * dt) * (dx > 0 ? 1 : -1); + var dy2 = dy + (accel * dt) * (dy > 0 ? 1 : -1); + return { nx: (x2-x), ny: (y2-y), x: x2, y: y2, dx: dx2, dy: dy2 }; + }, + + intercept: function(x1, y1, x2, y2, x3, y3, x4, y4, d) { + var denom = ((y4-y3) * (x2-x1)) - ((x4-x3) * (y2-y1)); + if (denom != 0) { + var ua = (((x4-x3) * (y1-y3)) - ((y4-y3) * (x1-x3))) / denom; + if ((ua >= 0) && (ua <= 1)) { + var ub = (((x2-x1) * (y1-y3)) - ((y2-y1) * (x1-x3))) / denom; + if ((ub >= 0) && (ub <= 1)) { + var x = x1 + (ua * (x2-x1)); + var y = y1 + (ua * (y2-y1)); + return { x: x, y: y, d: d}; + } + } + } + return null; + }, + + ballIntercept: function(ball, rect, nx, ny) { + var pt; + if (nx < 0) { + pt = Pong.Helper.intercept(ball.x, ball.y, ball.x + nx, ball.y + ny, + rect.right + ball.radius, + rect.top - ball.radius, + rect.right + ball.radius, + rect.bottom + ball.radius, + "right"); + } + else if (nx > 0) { + pt = Pong.Helper.intercept(ball.x, ball.y, ball.x + nx, ball.y + ny, + rect.left - ball.radius, + rect.top - ball.radius, + rect.left - ball.radius, + rect.bottom + ball.radius, + "left"); + } + if (!pt) { + if (ny < 0) { + pt = Pong.Helper.intercept(ball.x, ball.y, ball.x + nx, ball.y + ny, + rect.left - ball.radius, + rect.bottom + ball.radius, + rect.right + ball.radius, + rect.bottom + ball.radius, + "bottom"); + } + else if (ny > 0) { + pt = Pong.Helper.intercept(ball.x, ball.y, ball.x + nx, ball.y + ny, + rect.left - ball.radius, + rect.top - ball.radius, + rect.right + ball.radius, + rect.top - ball.radius, + "top"); + } + } + return pt; + } + + } + + //============================================================================= + +}; // Pong diff --git a/pong/js/script.js b/pong/js/script.js new file mode 100755 index 000000000..ab676d781 --- /dev/null +++ b/pong/js/script.js @@ -0,0 +1,23 @@ +$( document ).ready(function() { + + var size = document.getElementById('size'); + var sound = document.getElementById('sound'); + var stats = document.getElementById('stats'); + var footprints = document.getElementById('footprints'); + var predictions = document.getElementById('predictions'); + + var pong = Game.start('game', Pong, { + sound: sound.checked, + stats: stats.checked, + footprints: footprints.checked, + predictions: predictions.checked + }); + + Game.addEvent(sound, 'change', function() { pong.enableSound(sound.checked); }); + Game.addEvent(stats, 'change', function() { pong.showStats(stats.checked); }); + Game.addEvent(footprints, 'change', function() { pong.showFootprints(footprints.checked); }); + Game.addEvent(predictions, 'change', function() { pong.showPredictions(predictions.checked); }); + +}); + + diff --git a/pong/sounds/goal.wav b/pong/sounds/goal.wav Binary files differnew file mode 100755 index 000000000..965c2d2ad --- /dev/null +++ b/pong/sounds/goal.wav diff --git a/pong/sounds/ping.wav b/pong/sounds/ping.wav Binary files differnew file mode 100755 index 000000000..0f45c6804 --- /dev/null +++ b/pong/sounds/ping.wav diff --git a/pong/sounds/pong.wav b/pong/sounds/pong.wav Binary files differnew file mode 100755 index 000000000..e3eedf156 --- /dev/null +++ b/pong/sounds/pong.wav diff --git a/pong/sounds/wall.wav b/pong/sounds/wall.wav Binary files differnew file mode 100755 index 000000000..32a112947 --- /dev/null +++ b/pong/sounds/wall.wav diff --git a/pong/templates/main.php b/pong/templates/main.php new file mode 100755 index 000000000..62f65a1c5 --- /dev/null +++ b/pong/templates/main.php @@ -0,0 +1,33 @@ + + <div id="sidebar"> + + + + <div class='settings'> + <label for='sound'>sound: </label> + <input type='checkbox' id='sound'> + </div> + + <div class='settings'> + <label for='stats'>stats: </label> + <input type='checkbox' id='stats' checked> + </div> + + <div class='settings'> + <label for='footprints'>footprints: </label> + <input type='checkbox' id='footprints'> + </div> + + <div class='settings'> + <label for='predictions'>predictions: </label> + <input type='checkbox' id='predictions'> + </div> + + </div> + + <canvas id="game"> + <div id="unsupported"> + Sorry, this example cannot be run because your browser does not support the <canvas> element + </div> + </canvas> + |