├── .gitignore
├── LICENSE
├── README.md
├── gulpfile.js
├── images
├── comicbubble1.png
├── flare.png
├── help.png
├── particles.png
├── splash.png
├── spritesheet.png
└── xflare.png
├── index.html
├── package.json
├── script.js
├── script
├── Asteroid.js
├── BackgroundStars.js
├── Benchmark.js
├── Bullet.js
├── Button.js
├── CircleExplosion.js
├── Cursor.js
├── Engine.js
├── Game.js
├── Gameover.js
├── Main.js
├── Missile.js
├── Particle.js
├── Planet.js
├── Playground.Scanlines.js
├── Playground.SoundOnDemand.js
├── Playground.js
├── Powerup.js
├── Resource.js
├── Ship.js
├── TextOut.js
├── Trail.js
├── Utils.js
├── bottlenecks.js
├── data.js
└── stats.js
└── sounds
├── ascendancy.mp3
├── ascendancy.ogg
├── build.mp3
├── build.ogg
├── coin.mp3
├── coin.ogg
├── dig.mp3
├── dig.ogg
├── digEnd.mp3
├── digEnd.ogg
├── dust.mp3
├── dust.ogg
├── explosion.mp3
├── explosion.ogg
├── gameover.mp3
├── gameover.ogg
├── laser.mp3
├── laser.ogg
├── planetHit.mp3
├── planetHit.ogg
├── powerup.mp3
├── powerup.ogg
├── upgrade.mp3
└── upgrade.ogg
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | *.log
3 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | * Code is MPL licensed:
2 | This Source Code Form is subject to the terms of the Mozilla Public
3 | License, v. 2.0. If a copy of the MPL was not distributed with this file,
4 | You can obtain one at http://mozilla.org/MPL/2.0/.
5 |
6 |
7 | * Content is licensed under CC-BY-SA 3.0:
8 | http://creativecommons.org/licenses/by-sa/3.0/
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # PowerSurge Game
2 |
3 | **Play and optimize using [Firefox Developer Edition](https://www.mozilla.org/en-US/firefox/developer/).**
4 |
5 | 
6 |
7 | Learn more at [Mozilla Hacks](https://hacks.mozilla.org/category/developer-tools/) and the [Mozilla Developer Network](https://developer.mozilla.org/en-US/docs/Tools/Performance)!
8 |
9 | ## Develop
10 |
11 | * `npm install` - *Install development dependencies*
12 | * `npm start` - *Uses gulp to host a live-updating server*
13 |
14 | or
15 |
16 | * Live-edit in the [CodePen playground](http://codepen.io/mozhacks/pen/xGgevw?editors=001)
17 |
18 | ## IRC Channel
19 |
20 | [#devedition](irc://irc.mozilla.org/devedition) on irc.mozilla.org
21 |
22 | ## License
23 |
24 | Code is licensed under MPL 2.0. Content is licensed under CC-BY-SA 3.0. See the LICENSE file for details.
25 |
26 | ## Credits
27 |
28 | Created by Przemysław [@rezoner](https://twitter.com/rezoner) Sikorski with a little help from Harald [@digitarald](https://twitter.com/digitarald) Kirschner.
29 |
--------------------------------------------------------------------------------
/gulpfile.js:
--------------------------------------------------------------------------------
1 | var gulp = require('gulp');
2 | var concat = require('gulp-concat');
3 | var sourcemaps = require('gulp-sourcemaps');
4 | var connect = require('gulp-connect');
5 |
6 | var scripts = [
7 | './script/data.js',
8 | './script/stats.js',
9 | './script/Utils.js',
10 | './script/Playground.js',
11 | './script/Playground.Scanlines.js',
12 | './script/Playground.SoundOnDemand.js',
13 | './script/Engine.js',
14 | './script/Benchmark.js',
15 | './script/BackgroundStars.js',
16 | './script/CircleExplosion.js',
17 | './script/Ship.js',
18 | './script/Bullet.js',
19 | './script/Asteroid.js',
20 | './script/Cursor.js',
21 | './script/Resource.js',
22 | './script/Button.js',
23 | './script/Particle.js',
24 | './script/Planet.js',
25 | './script/Game.js',
26 | './script/Powerup.js',
27 | './script/TextOut.js',
28 | './script/Trail.js',
29 | './script/Missile.js',
30 | './script/Gameover.js',
31 | './script/Main.js',
32 | './script/bottlenecks.js'
33 | ];
34 |
35 | gulp.task('server', function() {
36 | connect.server({
37 | livereload: true
38 | });
39 | });
40 |
41 | gulp.task('scripts', function() {
42 | gulp.src(scripts)
43 | .pipe(sourcemaps.init())
44 | .pipe(concat('script.js'))
45 | .pipe(sourcemaps.write())
46 | .pipe(gulp.dest('./'))
47 | .pipe(connect.reload());
48 | });
49 |
50 | gulp.task('watch', function() {
51 | gulp.watch('./script/*.js', ['scripts']);
52 | });
53 |
54 | gulp.task('default', ['scripts', 'server', 'watch']);
55 |
--------------------------------------------------------------------------------
/images/comicbubble1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mozilla/devtools-perf-game/57f20b885b23cde2d55cd220d5f33704719065df/images/comicbubble1.png
--------------------------------------------------------------------------------
/images/flare.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mozilla/devtools-perf-game/57f20b885b23cde2d55cd220d5f33704719065df/images/flare.png
--------------------------------------------------------------------------------
/images/help.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mozilla/devtools-perf-game/57f20b885b23cde2d55cd220d5f33704719065df/images/help.png
--------------------------------------------------------------------------------
/images/particles.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mozilla/devtools-perf-game/57f20b885b23cde2d55cd220d5f33704719065df/images/particles.png
--------------------------------------------------------------------------------
/images/splash.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mozilla/devtools-perf-game/57f20b885b23cde2d55cd220d5f33704719065df/images/splash.png
--------------------------------------------------------------------------------
/images/spritesheet.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mozilla/devtools-perf-game/57f20b885b23cde2d55cd220d5f33704719065df/images/spritesheet.png
--------------------------------------------------------------------------------
/images/xflare.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mozilla/devtools-perf-game/57f20b885b23cde2d55cd220d5f33704719065df/images/xflare.png
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
PowerSurge – A Firefox Developer Edition Performance Tools Celebration
4 |
5 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
60 |
61 |
62 |
63 |
64 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "devtools-perf-game",
3 | "version": "0.0.1",
4 | "description": "Power Surge - A Firefox Developer Edition Performance Tools Celebration!",
5 | "devDependencies": {
6 | "gulp-concat": "^2.5.2",
7 | "gulp-connect": "^2.2.0",
8 | "gulp": "^3.8.11",
9 | "gulp-sourcemaps": "^1.5.2"
10 | },
11 | "scripts": {
12 | "start": "gulp"
13 | },
14 | "repository": {
15 | "type": "git",
16 | "url": "https://github.com/mozilla/devtools-perf-game.git"
17 | },
18 | "keywords": [
19 | "game",
20 | "performance"
21 | ],
22 | "author": "Mozilla (https://mozilla.org/)",
23 | "contributors": [
24 | "Przemysław Sikorski (http://rezoner.net)",
25 | "Harald Kirschner (http://digitarald.de)"
26 | ],
27 | "license": "MIT",
28 | "bugs": {
29 | "url": "https://github.com/mozilla/devtools-perf-game/issues"
30 | },
31 | "homepage": "http://codepen.io/mozhacks/pen/xGgevw?editors=001"
32 | }
33 |
--------------------------------------------------------------------------------
/script/Asteroid.js:
--------------------------------------------------------------------------------
1 | ENGINE.Asteroid = function(args) {
2 |
3 | this.max = this.resources = 5;
4 |
5 | Utils.extend(this, {
6 |
7 | hitLifespan: 0
8 |
9 | }, args);
10 |
11 | this.radius = 32;
12 |
13 | this.direction = Math.atan2(app.center.y - this.y, app.center.x - this.x);
14 | this.speed = 8 + this.game.random() * 32;
15 |
16 | this.lifetime = 0;
17 |
18 | this.kind = this.game.random() > 0.8 ? "gold" : "normal";
19 |
20 | this.spriteIndex = Utils.random(0, 2);
21 |
22 | this.collectibles = 0;
23 |
24 |
25 | };
26 |
27 | ENGINE.Asteroid.prototype = {
28 |
29 | constructor: ENGINE.Asteroid,
30 |
31 | quota: 0.5,
32 |
33 | hoverable: "mining",
34 | silent: true,
35 | instant: true,
36 |
37 | type: "asteroid",
38 |
39 |
40 | sprites: {
41 |
42 | normal: [
43 | [341, 239, 52, 39],
44 | [337, 288, 61, 61],
45 | [338, 354, 57, 58]
46 | ],
47 |
48 | gold: [
49 | [408, 238, 52, 39],
50 | [404, 287, 59, 61],
51 | [403, 353, 59, 58]
52 | ],
53 |
54 | hit: [
55 | [476, 127, 52, 39],
56 | [472, 176, 61, 61],
57 | [473, 242, 57, 58]
58 | ]
59 |
60 | },
61 |
62 | pointerenter: function() {
63 |
64 | this.slowdown = true;
65 |
66 | },
67 |
68 | pointerleave: function() {
69 |
70 | this.slowdown = false;
71 |
72 | },
73 |
74 | die: function() {
75 |
76 | app.sound.play("explosion").rate(0.6);
77 |
78 | if (Math.random() > 0.7) {
79 |
80 | this.game.add(ENGINE.Powerup, {
81 | x: this.x,
82 | y: this.y
83 | });
84 |
85 | }
86 |
87 | this.game.remove(this);
88 | this.game.explosion(this.x, this.y, 16, "#aaa");
89 | this.game.spawnAsteroid();
90 |
91 | },
92 |
93 | dig: function() {
94 |
95 | this.hitLifespan = 0.1;
96 |
97 | this.resources--;
98 |
99 | if (this.resources <= 0) {
100 | this.die();
101 | }
102 |
103 | var count = this.kind === "gold" ? 2 : 1;
104 |
105 | this.spawnResources(count);
106 |
107 | this.game.explosion(this.x, this.y, 4, "#fa0");
108 |
109 | if (!this.game.benchmark) app.sound.play("dig");
110 |
111 | },
112 |
113 | spawnResources: function(count) {
114 |
115 | for (var i = 0; i < count; i++) {
116 |
117 | this.game.add(ENGINE.Resource, {
118 | x: this.x,
119 | y: this.y,
120 | parent: this
121 | });
122 |
123 | }
124 |
125 | },
126 |
127 | step: function(dt) {
128 |
129 | dt *= this.game.timeFactor;
130 |
131 | this.lifetime += dt;
132 |
133 | this.hitLifespan -= dt;
134 |
135 | var speed = this.speed * (this.slowdown ? 0.25 : 1.0);
136 |
137 | this.x += Math.cos(this.direction) * speed * dt;
138 | this.y += Math.sin(this.direction) * speed * dt;
139 |
140 | this.game.wrap(this);
141 |
142 | if (Utils.distance(this, app.center) < this.game.player.planet.radius + this.radius) {
143 |
144 | if (this.game.player.planet.asteroidsShield) {
145 |
146 | this.spawnResources(5);
147 |
148 | } else {
149 |
150 | this.game.player.planet.applyDamage(1, this);
151 |
152 | }
153 |
154 | this.die();
155 |
156 | }
157 |
158 | },
159 |
160 | render: function() {
161 |
162 | if (this.hitLifespan > 0) {
163 |
164 | var sprite = this.sprites.hit[this.spriteIndex];
165 |
166 | } else {
167 |
168 | var sprite = this.sprites[this.kind][this.spriteIndex];
169 |
170 | }
171 |
172 | var scale = 0.5 + 0.5 * this.resources / this.max;
173 |
174 | app.ctx.save();
175 |
176 | app.ctx.translate(this.x, this.y)
177 | app.ctx.rotate(this.lifetime)
178 | app.ctx.scale(scale, scale)
179 | app.ctx.drawImage(app.images.spritesheet,
180 | sprite[0], sprite[1], sprite[2], sprite[3], -sprite[2] / 2, -sprite[3] / 2, sprite[2], sprite[3]
181 | );
182 | app.ctx.restore();
183 |
184 | }
185 |
186 | };
--------------------------------------------------------------------------------
/script/BackgroundStars.js:
--------------------------------------------------------------------------------
1 | ENGINE.BackgroundStars = function() {
2 |
3 | this.color = "#0af";
4 |
5 | this.count = Math.max(app.height, app.width) / 16 | 0;
6 |
7 | this.x = 0;
8 | this.y = 0;
9 |
10 | this.populated = false;
11 | this.image = app.getColoredImage(app.images.particles, this.color);
12 |
13 | };
14 |
15 | ENGINE.BackgroundStars.prototype = {
16 |
17 | images: {},
18 |
19 | colors: ["#afc", "#fa0"],
20 |
21 | sprites: [
22 | [0, 13, 5, 5],
23 | [1, 19, 3, 3]
24 | ],
25 |
26 | quota: 0.5,
27 |
28 | populate: function(fill) {
29 |
30 | this.stars = [];
31 |
32 | for (var i = 0; i < this.count; i++) {
33 | this.spawnStar(fill);
34 | }
35 |
36 | },
37 |
38 | spawnStar: function(fill) {
39 |
40 | var star = {
41 | x: Math.random() * app.width,
42 | y: Math.random() * app.height,
43 | z: 0.1 + 0.9 * Math.random(),
44 | s: Utils.random([1, 2, 3]),
45 | spriteIndex: Math.random() * this.sprites.length | 0
46 | };
47 |
48 | star.lx = star.x;
49 | star.ly = star.y;
50 |
51 | this.stars.push(star);
52 |
53 | },
54 |
55 | wrap: function(star) {
56 |
57 | if (star.x > app.width) star.x = 0;
58 | if (star.y > app.height) star.y = 0;
59 |
60 | if (star.x < 0) star.x = app.width;
61 | if (star.y < 0) star.y = app.height;
62 |
63 | },
64 |
65 | step: function(dt) {
66 |
67 | if (!this.populated) {
68 | this.populated = true;
69 | this.populate(true);
70 | }
71 |
72 | var diffX = (10 + app.game.score) * dt;
73 | var diffY = (10 + app.game.score) * dt;
74 |
75 |
76 | for (var i = 0; i < this.stars.length; i++) {
77 |
78 | var star = this.stars[i];
79 |
80 | this.wrap(star);
81 |
82 | star.x += diffX * star.z;
83 | star.y += diffY * star.z;
84 |
85 | }
86 |
87 | },
88 |
89 | render: function(dt) {
90 |
91 |
92 | for (var i = 0; i < this.stars.length; i++) {
93 |
94 | var star = this.stars[i];
95 |
96 | var sprite = this.sprites[star.spriteIndex];
97 |
98 | app.ctx.drawImage(this.image, sprite[0], sprite[1], sprite[2], sprite[3],
99 | star.x, star.y, sprite[2], sprite[3]);
100 |
101 |
102 | }
103 |
104 | }
105 |
106 | };
--------------------------------------------------------------------------------
/script/Benchmark.js:
--------------------------------------------------------------------------------
1 | ENGINE.Benchmark = {
2 |
3 | create: function() {
4 |
5 | this.music = app.music.play("gameover").fadeIn(4).loop();
6 |
7 | this.ready = false;
8 |
9 | // this.gradient = app.layer.createRadialGradient(app.center.x, app.center.y, 0, app.center.x, app.center.y, app.center.x);
10 | // this.gradient.addColorStop(0.0, "transparent");
11 | // this.gradient.addColorStop(1.0, "#000");
12 |
13 | // JIT warmup
14 | this.didWarmup = false;
15 | this.steps = 0;
16 | this.iotaList = [];
17 | this.frameTimes = [];
18 | this.scores = [];
19 | this.runCount = 0;
20 | this.skipCount = 0;
21 | this.skipResetCount = 0;
22 | this.resetCount = 0;
23 | this.scoreStack = [];
24 | this.frameTime = 0.0;
25 | this.startTime = Date.now();
26 | },
27 |
28 |
29 | pointerdown: function() {
30 |
31 | if (this.ready) {
32 | if (window.ga) {
33 | ga('send', {
34 | 'hitType': 'event',
35 | 'eventCategory': 'game',
36 | 'eventAction': 'start'
37 | });
38 | }
39 |
40 | this.music.fadeOut();
41 |
42 | app.setState(ENGINE.Game);
43 | }
44 |
45 | },
46 |
47 | enter: function() {
48 | if (window.ga) {
49 | ga('send', 'screenview', {
50 | 'appName': 'PowerSurge',
51 | 'screenName': 'Splashpage'
52 | });
53 | }
54 |
55 | this.startMod = 0;
56 |
57 | this.iotaCount = this.app.baseline ? Math.floor(this.app.baseline * 0.7) : 1;
58 |
59 | this.app.baseline = 0;
60 |
61 | this.reset();
62 |
63 | },
64 |
65 | // Called between benchmark loops
66 | reset: function() {
67 | this.steps = 0;
68 | this.frameTimes.length = 0;
69 | this.skipCount = 0;
70 | // JIT warmup settings (run unbound loops)
71 | if (!this.didWarmup) {
72 | // console.time('Warmup');
73 | this.app.unbound = true;
74 | this.app.immidiate = false;
75 | } else {
76 | this.app.unbound = false;
77 | this.app.immidiate = true;
78 | }
79 | if (this.iotaList.length == 0) {
80 | this.addIotas(this.didWarmup ? this.iotaCount : 1);
81 | }
82 | },
83 |
84 | step: function(dt) {
85 | if (this.ready) {
86 | return;
87 | }
88 |
89 | var before = performance.now();
90 |
91 | this.iotaList.forEach(function(iota) {
92 | iota.step(dt);
93 | });
94 |
95 | this.frameTime = performance.now() - before;
96 |
97 | if (!this.didWarmup) {
98 | // State: JIT Warmup
99 | this.stepWarmUp();
100 | } else if (this.frameTime) {
101 | // Stresstesting
102 | this.stepStressTest()
103 | }
104 |
105 | },
106 |
107 | stepWarmUp: function() {
108 |
109 | this.steps++;
110 |
111 | if (this.steps > 1100) {
112 | this.didWarmup = true;
113 | // console.timeEnd('Warmup');
114 | // console.log('Warmup with %d iotas', this.iotaList.length);
115 | this.reset();
116 | }
117 | },
118 |
119 | stepStressTest: function() {
120 | var add = 1;
121 | var frameTimes = this.frameTimes;
122 | var MAX_FRAMES = 45;
123 | var MIN_FRAMES = 15;
124 | var COST = 8;
125 | var ERROR = 0.25;
126 | var frameTime = this.frameTime;
127 | if (frameTimes.unshift(frameTime) > MAX_FRAMES) {
128 | frameTimes.length = MAX_FRAMES;
129 | }
130 | if (frameTimes.length >= MIN_FRAMES) {
131 | var sample = this.analyze(frameTimes);
132 | var score = this.iotaList.length;
133 | if (sample.rse <= ERROR && sample.mean > COST) {
134 | this.pushScore(score);
135 | return;
136 | }
137 | if (sample.rse > ERROR || sample.mean > COST) {
138 | // console.log('Skip #' + this.skipCount);
139 | this.skipCount++;
140 | if (this.skipCount > 60) {
141 | console.log(
142 | '[RESET STEP] High sampling error %f%% or mean %fms for %d entities.',
143 | sample.rse * 100, sample.mean, score
144 | );
145 | this.iotaCount = Math.floor(this.lastScore * 0.7);
146 | this.skipResetCount++;
147 | if (this.skipResetCount > 10) {
148 | this.finalize(false);
149 | return;
150 | }
151 | this.finalize(true);
152 | }
153 | return;
154 | }
155 | this.skipCount = 0;
156 | add = Math.round(COST / sample.mean);
157 | }
158 |
159 | this.addIotas(add);
160 | },
161 |
162 | pushScore: function(score) {
163 | var SAVE_SCORES = 3;
164 | var MIN_SCORES = 5;
165 | var MAX_SCORES = 10;
166 | var ERROR = 0.15;
167 |
168 | this.skipResetCount = 0;
169 | var scores = this.scores;
170 | this.runCount++;
171 | if (scores.unshift(score) > MAX_SCORES) {
172 | scores.length = MAX_SCORES;
173 | }
174 | this.iotaCount = Math.ceil(score * 0.7);
175 | var l = scores.length;
176 | if (l >= MIN_SCORES) {
177 | var sample = this.analyze(scores);
178 | if (sample.rse < ERROR) {
179 | this.resetCount = 0;
180 | this.app.baseline = Math.round(sample.mean);
181 | if (window.ga) {
182 | ga('send', {
183 | 'hitType': 'event',
184 | 'eventCategory': 'game',
185 | 'eventAction': 'baselined',
186 | 'eventValue': this.app.baseline,
187 | 'nonInteraction': true
188 | });
189 | }
190 | this.app.baselineErr = sample.rse;
191 | this.scores.splice(SAVE_SCORES);
192 | this.finalize(false);
193 | return;
194 | } else {
195 | console.log(
196 | '[SCORE RESET] Standard error %f%% too high in score samples.',
197 | sample.rse * 100
198 | );
199 | this.resetCount++;
200 | if (this.resetCount > 10) {
201 | this.scores.splice(0);
202 | console.log('[BAIL] Too many [RESET SCORE].');
203 | if (window.ga) {
204 | ga('send', 'exception', {
205 | 'exDescription': 'BenchmarkResetOverflow',
206 | 'exFatal': false
207 | });
208 | }
209 | this.finalize(false);
210 | return;
211 | }
212 | }
213 | }
214 | this.finalize(true);
215 | },
216 |
217 | finalize: function(restart) {
218 |
219 | if (!restart) {
220 | // Remove iotas
221 | this.iotaCount = 0;
222 | this.runCount = 0;
223 | // Reset benchmark engine settings
224 | this.app.unbound = false;
225 | this.app.immidiate = false;
226 | }
227 | // Reduce iotaList to iotaCount
228 | this.iotaList.splice(this.iotaCount).forEach(function(iota) {
229 | iota.destroy();
230 | });
231 | if (restart) {
232 | this.reset();
233 | } else {
234 | if (window.ga) {
235 | ga('send', {
236 | 'hitType': 'timing',
237 | 'timingCategory': 'Benchmark',
238 | 'timingVar': 'Loading',
239 | 'timingValue': Date.now() - this.startTime
240 | });
241 | }
242 | this.ready = true;
243 | app.tween(this).to({
244 | startMod: 1.0
245 | }, 1.0, "outElastic");
246 | }
247 |
248 | },
249 |
250 | addIotas: function(count) {
251 |
252 | for (var j = 0; j < count; j++) {
253 |
254 | this.iotaList.push(new Iota(this.app, this));
255 |
256 | }
257 |
258 | },
259 |
260 | render: function() {
261 |
262 | /* get reference to the application */
263 |
264 | var app = this.app;
265 |
266 | /* get reference to drawing surface */
267 |
268 | var layer = this.app.layer;
269 |
270 | /* clear screen */
271 |
272 | layer.clear("#282245");
273 |
274 |
275 | layer.drawImage(app.images.splash, app.center.x - app.images.splash.width / 2 | 0, app.center.y - app.images.splash.height / 2 | 0)
276 |
277 | layer.save();
278 | layer.translate(600, 290);
279 |
280 | layer.align(0.5, 0.5);
281 | layer.scale(4, 4);
282 | layer.globalAlpha(0.4);
283 | layer.globalCompositeOperation("lighter");
284 | layer.drawImage(app.images.flare, 128 * (32 * (app.lifetime % 1.5 / 1.5) | 0), 0, 128, 128, 0, 0, 128, 128);
285 | layer.restore();
286 |
287 |
288 | app.fontSize(48);
289 |
290 |
291 |
292 | if (!this.ready) {
293 | var textX = app.center.x;
294 | var textY = app.center.y - 16;
295 |
296 | layer.fillStyle("rgba(0,0,0,0.5").fillRect(0, textY - 54, app.width, 74);
297 |
298 | layer.fillStyle("#000").textAlign("center").fillText("LOADING... please wait", textX, textY - 4);
299 | layer.fillStyle("#fff").textAlign("center").fillText("LOADING... please wait", textX, textY);
300 |
301 | } else {
302 |
303 | var textX = app.center.x + 100 + (1 - this.startMod) * 1000;
304 | var textY = app.center.y - 10;
305 |
306 | layer.a(0.5 + Utils.osc(app.lifetime, 1) * 0.5);
307 | layer.fillStyle("#000").textAlign("center").fillText("CLICK TO START!", textX, textY - 4);
308 | layer.fillStyle("#fa0").textAlign("center").fillText("CLICK TO START!", textX, textY);
309 | layer.a(1.0);
310 |
311 | }
312 |
313 |
314 | // app.ctx.fillStyle = this.gradient;
315 | // app.ctx.fillRect(0, 0, app.width, app.height);
316 |
317 | // this.iotaList.forEach(function(iota) {
318 | // iota.render(layer);
319 | // });
320 |
321 | // layer
322 | // .fillStyle('#fff')
323 | // .font("14px 'arial'")
324 | // .fillText('Stress test #' + this.runCount, 5, 15)
325 | // .fillText('Entities: ' + this.iotaList.length, 5, 30)
326 | // .fillText('Frametime:' + this.frameTime.toFixed(1), 5, 45);
327 | },
328 |
329 | analyze: function(population) {
330 |
331 | var l = population.length;
332 | var sum = 0.0;
333 | var sumsq = 0.0;
334 | for (var i = 0; i < l; i++) {
335 | sum += population[i];
336 | sumsq += population[i] * population[i];
337 | }
338 | var mean = sum / l;
339 | var sd = Math.sqrt(sumsq / l - sum * sum / (l * l));
340 | var se = sd / Math.sqrt(l);
341 | // standard error at 95% confidence
342 | var se95 = 1.96 * se;
343 | var rse = se / mean;
344 | return {
345 | mean: mean,
346 | sd: sd,
347 | se: se,
348 | se95: se95,
349 | rse: rse
350 | }
351 |
352 | },
353 |
354 | nearest: function(from, entities) {
355 |
356 | var min = -1;
357 | var result = null;
358 |
359 | for (var i = 0; i < entities.length; i++) {
360 |
361 | var to = entities[i];
362 |
363 | if (from === to) continue;
364 |
365 | var distance = this.distance(from, to);
366 |
367 | if (distance < min || min < 0) {
368 | min = distance;
369 | result = to;
370 | }
371 |
372 | }
373 |
374 | return result;
375 | },
376 |
377 | distance: function(a, b) {
378 |
379 | var dx = a.x - b.x;
380 | var dy = a.y - b.y;
381 |
382 | return Math.sqrt(dx * dx + dy * dy);
383 |
384 | }
385 | };
386 |
387 | var images = ['firefox', 'firefox_beta', 'firefox_developer_edition', 'firefox_nightly'];
388 |
389 | function Iota(app, parent) {
390 | this.x = 0.0;
391 | this.y = 0.0;
392 | this.vx = 0.0;
393 | this.vy = 0.0;
394 | this.vr = 0.0;
395 | this.alpha = 0.0;
396 | this.angle = 0.0;
397 | this.app = app;
398 | this.parent = parent;
399 | this.x = Math.random() * app.width;
400 | this.y = Math.random() * app.height;
401 | this.maxVel = 100.0;
402 | this.maxTorq = Math.PI * 10;
403 | this.vx = Math.random() * this.maxVel * 2 - this.maxVel;
404 | this.vy = Math.random() * this.maxVel * 2 - this.maxVel;
405 | this.vr = Math.random() * this.maxTorq * 2 - this.maxTorq;
406 | this.image = app.images[images[Math.round(Math.random() * 3)]];
407 | this.region = Utils.random([
408 | [548, 88, 46, 47],
409 | [544, 142, 46, 48],
410 | [544, 200, 46, 47],
411 | [545, 253, 44, 48]
412 | ]);
413 | this.maxForce = 100.0;
414 | this.alpha = 0.2 + Math.random() * 0.8;
415 | this.angle = Math.random() * Math.PI;
416 | }
417 |
418 | Iota.prototype = {
419 |
420 | step: function(dt) {
421 |
422 | app.state.nearest(this, this.parent.iotaList);
423 |
424 | var iotaList = this.parent.iotaList;
425 | var forcex = 0.0;
426 | var forcey = 0.0;
427 | var forces = 0;
428 | var maxDist = 60.0;
429 | for (var i = 0, l = iotaList.length; i < l; i++) {
430 | var distx = (this.x - iotaList[i].x) / maxDist;
431 | var disty = (this.y - iotaList[i].y) / maxDist;
432 | var signx = Math.sign(distx);
433 | var signy = Math.sign(disty);
434 | var absx = Math.abs(distx);
435 | var absy = Math.abs(disty);
436 | if (absx < 1 && absy < 1) {
437 | forcex += signx + absx * signx;
438 | forcey += signy + absy * signy;
439 | forces++;
440 | }
441 | }
442 |
443 | if (forces == 0) {
444 | forces = 1;
445 | }
446 | forcex = Math.max(-this.maxForce, Math.min(this.maxForce, forcex / forces)) * 500;
447 | forcey = Math.max(-this.maxForce, Math.min(this.maxForce, forcey / forces)) * 500;
448 | this.vx = this.vx * 0.99 + forcex * 0.01;
449 | this.vy = this.vy * 0.99 + forcey * 0.01;
450 |
451 | var x = this.x + this.vx * dt;
452 | if (x < 0 || x > this.app.width) {
453 | x = Math.random() * this.app.width;
454 | }
455 | this.x = x;
456 |
457 | var y = this.y + this.vy * dt;
458 | if (y < 0 || y > this.app.height) {
459 | y = Math.random() * this.app.height;
460 | }
461 | this.y = y;
462 | this.angle += this.vr * dt;
463 | },
464 |
465 | // render: function(layer) {
466 |
467 | // return;
468 |
469 | // layer.context.save();
470 | // layer.context.translate(this.x | 0, this.y | 0);
471 | // // layer.a(this.alpha);
472 | // layer.context.fillStyle = "#f00";
473 | // layer.context.fillRect(this.x, this.y, 64, 64);
474 | // layer.context.fillStyle = "#fff";
475 | // layer.context.beginPath();
476 | // layer.context.moveTo(this.x, this.y);
477 | // layer.context.arc(this.x, this.y, 64, 0, Math.PI * 2);
478 | // layer.context.rotate(this.angle);
479 | // layer.drawRegion(app.images.spritesheet, this.region, 0, 0);
480 | // layer.context.restore();
481 | // },
482 |
483 | destroy: function() {
484 | this.app = null;
485 | this.parent = null;
486 | }
487 |
488 | }
--------------------------------------------------------------------------------
/script/Bullet.js:
--------------------------------------------------------------------------------
1 | ENGINE.Bullet = function(args) {
2 |
3 | Utils.extend(this, {
4 | speed: 400
5 | }, args);
6 |
7 | this.color = defs.teamColor[this.team];
8 | this.radius = 4;
9 | this.direction = 0;
10 |
11 | this.sprite = this.sprites[this.team];
12 |
13 | };
14 |
15 | ENGINE.Bullet.prototype = {
16 |
17 | sprites: [
18 | [126, 25, 4, 37],
19 | [133, 25, 4, 37]
20 | ],
21 |
22 | quota: 0.5,
23 |
24 | constructor: ENGINE.Bullet,
25 |
26 | step: function(dt) {
27 |
28 | dt *= this.game.timeFactor;
29 |
30 | this.direction = Math.atan2(this.target.y - this.y, this.target.x - this.x);
31 |
32 | this.x += Math.cos(this.direction) * this.speed * dt;
33 | this.y += Math.sin(this.direction) * this.speed * dt;
34 |
35 | if (Utils.distance(this, this.target) < this.radius + this.target.radius) {
36 |
37 | this.hit(this.target);
38 |
39 | }
40 |
41 | },
42 |
43 | hit: function(target) {
44 |
45 | target.applyDamage(this.damage, this.parent);
46 |
47 | this.die();
48 |
49 | },
50 |
51 | die: function() {
52 |
53 | this.dead = true;
54 |
55 | },
56 |
57 | render: function() {
58 |
59 | app.ctx.save();
60 |
61 | app.ctx.translate(this.x, this.y);
62 | app.ctx.rotate(this.direction + Math.PI / 2);
63 | app.ctx.drawImage(app.images.spritesheet,
64 | this.sprite[0], this.sprite[1], this.sprite[2], this.sprite[3], -this.sprite[2] / 2, -this.sprite[3] / 2, this.sprite[2], this.sprite[3]
65 | );
66 |
67 | app.ctx.restore();
68 |
69 | }
70 |
71 | };
--------------------------------------------------------------------------------
/script/Button.js:
--------------------------------------------------------------------------------
1 | ENGINE.Button = function(args) {
2 |
3 | Utils.extend(this, {
4 |
5 | radius: 32
6 |
7 | }, args);
8 |
9 |
10 | this.image = app.images.spritesheet;
11 |
12 | };
13 |
14 | ENGINE.Button.prototype = {
15 |
16 | constructor: ENGINE.Button,
17 |
18 | type: "button",
19 |
20 | pointerenter: function() {
21 |
22 | app.tween(this).discard().to({
23 | radius: 24
24 | }, 0.1).to({
25 | radius: 32
26 | }, 0.2, "outSine");
27 |
28 | },
29 |
30 | action: function() {
31 |
32 |
33 | app.sound.play("laser");
34 |
35 | },
36 |
37 | step: function() {
38 |
39 | },
40 |
41 | render: function() {
42 |
43 |
44 | if (this.sprite) {
45 | var scale = this.radius / 32;
46 |
47 | app.ctx.save();
48 |
49 | app.ctx.translate(this.x, this.y);
50 | app.ctx.drawImage(this.image,
51 | this.sprite[0], this.sprite[1], this.sprite[2], this.sprite[3], -this.sprite[2] / 2, -this.sprite[3] / 2, this.sprite[2], this.sprite[3]
52 | );
53 |
54 | app.ctx.restore();
55 |
56 | }
57 |
58 | if (this.count) {
59 | app.layer.textAlign("center").font("bold 32px Arial").fillStyle(this.color).fillText(this.count, this.x, this.y - this.radius - 48);
60 | }
61 |
62 | }
63 |
64 | };
--------------------------------------------------------------------------------
/script/CircleExplosion.js:
--------------------------------------------------------------------------------
1 | ENGINE.CircleExplosion = function(args) {
2 |
3 | Utils.extend(this, {
4 |
5 | attachedTo: false,
6 | radius: 0,
7 | alpha: 1.0,
8 | duration: 0.5
9 |
10 | }, args);
11 |
12 | this.radius = 0;
13 |
14 | this.tween = app.tween(this).discard().to({
15 | radius: args.radius
16 | }, this.duration, "outElastic").to({
17 | radius: 0
18 | }, this.duration, "outElastic");
19 |
20 | };
21 |
22 | ENGINE.CircleExplosion.prototype = {
23 |
24 | constructor: ENGINE.CircleExplosion,
25 |
26 | type: "circleExplosion",
27 |
28 | action: function() {
29 |
30 | app.sound.play("laser");
31 |
32 | },
33 |
34 | step: function() {
35 |
36 | if(this.attachedTo) {
37 | this.x = this.attachedTo.x;
38 | this.y = this.attachedTo.y;
39 | }
40 |
41 | if (this.tween.finished) this.dead = true;
42 |
43 | },
44 |
45 | render: function() {
46 |
47 | if (this.radius > 0) {
48 |
49 | app.ctx.beginPath();
50 | app.ctx.fillStyle = this.color;
51 | app.ctx.globalCompositeOperation = "lighter";
52 | app.ctx.arc(this.x, this.y, this.radius, 0, Math.PI * 2);
53 | app.ctx.fill();
54 | app.ctx.globalCompositeOperation = "source-over";
55 |
56 |
57 | }
58 |
59 | }
60 |
61 | };
--------------------------------------------------------------------------------
/script/Cursor.js:
--------------------------------------------------------------------------------
1 | ENGINE.Cursor = function(game, team, planet) {
2 |
3 | this.game = game;
4 |
5 | this.actionTimeout = 0;
6 |
7 | this.dotRadius = 8;
8 | this.capacity = 10;
9 | this.resources = 4;
10 | this.x = 0;
11 | this.y = 0;
12 | this.hoverTime = 0;
13 | this.team = team;
14 | this.color = defs.teamColor[team];
15 | this.planet = planet;
16 |
17 | this.targetTimeout = this.targetInterval = 0.25;
18 | this.fireCooldown = this.fireInterval = 0.25;
19 |
20 | /* timers */
21 |
22 | this.times = {
23 | mining: 0.5,
24 | collect: 0.05,
25 | build: 0.5,
26 | repair: 2
27 | };
28 |
29 |
30 | this.tween = app.tween(this);
31 |
32 | if (!this.team) {
33 |
34 | this.ai = new ENGINE.Ai(this);
35 | this.ai.set("idle");
36 |
37 | }
38 |
39 | this.trail = new ENGINE.Trail(this, {
40 | interval: 0.05,
41 | maxPoints: 10,
42 | color: this.color
43 | });
44 |
45 |
46 | };
47 |
48 | ENGINE.Cursor.prototype = {
49 |
50 | constructor: ENGINE.Cursor,
51 |
52 | poke: function() {
53 |
54 | this.tween = app.tween(this).discard()
55 |
56 | .to({
57 | dotRadius: 16
58 | }, 0.1, "outSine")
59 |
60 | .to({
61 | dotRadius: 8
62 | }, 0.05, "inSine");
63 |
64 | },
65 |
66 | step: function(dt) {
67 |
68 | var prevEntity = this.entity;
69 |
70 | this.entity = this.getHoveredEntity();
71 |
72 | if (this.entity !== prevEntity) {
73 |
74 | if (prevEntity && prevEntity.pointerleave) prevEntity.pointerleave(this);
75 | if (this.entity && this.entity.pointerenter) this.entity.pointerenter(this);
76 |
77 | this.onentitychange();
78 |
79 | }
80 |
81 | if (this.action) {
82 |
83 | this.hoverTime += dt;
84 |
85 | this.progressAction(dt);
86 |
87 | }
88 |
89 | /* firing mechanics */
90 |
91 | if (this.target && this.target.dead) this.target = false;
92 |
93 | if ((this.targetTimeout -= dt) <= 0) {
94 |
95 | this.targetTimeout = 0.5;
96 |
97 | this.target = this.getTarget();
98 |
99 | }
100 |
101 |
102 | this.fireCooldown -= dt;
103 |
104 | if (this.canFire()) {
105 |
106 | this.fire();
107 |
108 | }
109 |
110 | this.trail.step(dt);
111 |
112 |
113 | },
114 |
115 | getTarget: function() {
116 |
117 | var pool = [];
118 |
119 | for (var i = 0; i < this.game.entities.length; i++) {
120 |
121 | var entity = this.game.entities[i];
122 |
123 | if (!(entity instanceof ENGINE.Ship)) continue;
124 |
125 | if (Utils.distance(entity, this) > 200) continue;
126 | if (entity.team !== this.team) pool.push(entity);
127 |
128 | }
129 |
130 | return Utils.nearest(this, pool);
131 |
132 | },
133 |
134 | onentitychange: function() {
135 |
136 | this.actionComplete = false;
137 |
138 | this.hoverTime = 0;
139 |
140 | if (this.entity) {
141 |
142 | this.action = this.entity.hoverable;
143 | this.resetAction();
144 |
145 | if (this.entity.instant) this.actionTimeout = 0;
146 |
147 |
148 | } else this.action = false;
149 |
150 | /*
151 | if (!this.actionSound) this.actionSound = app.sound.play("action").loop().rate(0.5);
152 |
153 | if (!this.action) {
154 | this.actionSound.stop();
155 | } else {
156 | this.actionSound.fadeIn();
157 | }
158 | */
159 | this.updateTooltip();
160 |
161 |
162 | },
163 |
164 | resetAction: function() {
165 |
166 |
167 | this.actionTimeout = this.times[this.action];
168 |
169 | this.actionDuration = this.actionTimeout;
170 |
171 | },
172 |
173 | upgrade: function(key) {
174 |
175 | this.game.upgrades[key] ++;
176 |
177 | this.game.buttons[key].count = this.getPrice(key);
178 |
179 | var ships = Utils.filter(this.game.entities, function(e) {
180 |
181 | return (e instanceof ENGINE.Ship) && e.team;
182 |
183 | });
184 |
185 | for (var i = 0; i < ships.length; i++) {
186 |
187 | var ship = ships[i];
188 |
189 | this.game.add(ENGINE.CircleExplosion, {
190 | color: "#0af",
191 | radius: 32,
192 | attachedTo: ship
193 | });
194 |
195 | ship.applyUpgrades(this.game.upgrades)
196 |
197 | }
198 |
199 | },
200 |
201 | getPrice: function(key) {
202 |
203 | return Math.pow(2, this.game.upgrades[key]);
204 |
205 | },
206 |
207 | canProgress: function() {
208 |
209 | switch (this.action) {
210 |
211 | case "repair":
212 |
213 | return this.planet.hp < this.planet.maxHP;
214 |
215 | break;
216 |
217 | case "build":
218 |
219 | if (this.entity.key === "fighter") {
220 |
221 | if (this.game.playerPlanet.max - this.game.playerPlanet.ships <= 0) return false;
222 |
223 | return this.resources > 0;
224 | } else {
225 |
226 | return this.resources >= this.getPrice(this.entity.key);
227 |
228 | }
229 |
230 | break;
231 |
232 | default:
233 |
234 | return true;
235 |
236 | break;
237 |
238 | }
239 | },
240 |
241 | progressAction: function(dt) {
242 |
243 | if (this.canProgress() && (this.actionTimeout -= dt) < 0) {
244 |
245 | this.finalizeAction();
246 | this.resetAction();
247 |
248 | };
249 |
250 | this.progress = 1 - this.actionTimeout / this.actionDuration;
251 |
252 |
253 | },
254 |
255 | finalizeAction: function() {
256 |
257 | this.actionComplete = true;
258 |
259 | switch (this.action) {
260 |
261 | case "repair":
262 |
263 | this.planet.repair();
264 |
265 | break;
266 |
267 | case "mining":
268 |
269 | this.entity.dig();
270 |
271 | break;
272 |
273 |
274 | case "build":
275 |
276 | switch (this.entity.key) {
277 |
278 | case "fighter":
279 |
280 | this.planet.spawnShip("fighter");
281 | this.resources -= 1;
282 | if (!this.game.benchmark) app.sound.play("build");
283 |
284 | break;
285 |
286 | case "life":
287 | case "damage":
288 | case "speed":
289 |
290 | this.resources -= this.getPrice(this.entity.key);
291 |
292 | this.upgrade(this.entity.key);
293 |
294 | if (!this.game.benchmark) app.sound.play("upgrade");
295 |
296 |
297 | break;
298 |
299 | }
300 |
301 | break;
302 | }
303 |
304 | },
305 |
306 | hit: function() {
307 |
308 | this.game.shake();
309 |
310 | this.planet.applyDamage(1, this.planet);
311 |
312 | this.game.add(ENGINE.CircleExplosion, {
313 | x: this.x,
314 | y: this.y,
315 | color: "#c02",
316 | radius: 32
317 | })
318 |
319 | },
320 |
321 | getHoveredEntity: function() {
322 |
323 | for (var i = 0; i < this.game.entities.length; i++) {
324 |
325 | var entity = this.game.entities[i];
326 |
327 | if (entity.hoverable && Utils.distance(entity, this) < entity.radius) return entity;
328 |
329 | }
330 |
331 | return null;
332 |
333 | },
334 |
335 | render: function() {
336 |
337 | this.trail.render();
338 |
339 | app.layer.fillStyle(this.color).fillCircle(this.x, this.y, this.dotRadius);
340 |
341 | if (this.action && !this.entity.silent) {
342 |
343 | var mod = Math.min(1, app.ease(2 * this.hoverTime, "outBounce"));
344 |
345 | app.ctx.save();
346 | app.ctx.translate(this.entity.x, this.entity.y);
347 |
348 | app.ctx.strokeStyle = this.color;
349 | app.ctx.lineWidth = 2;
350 | app.ctx.beginPath();
351 | app.ctx.arc(0, 0, (this.entity.radius + 2) * mod, 0, Math.PI * 2);
352 | app.ctx.stroke();
353 |
354 | app.ctx.lineWidth = 8;
355 | app.ctx.beginPath();
356 | app.ctx.globalAlpha = 0.25;
357 | app.ctx.arc(0, 0, this.entity.radius + 8, 0, Math.PI * 2)
358 | app.ctx.stroke()
359 | app.ctx.globalAlpha = 1.0;
360 |
361 | app.ctx.lineWidth = 8;
362 | app.ctx.beginPath();
363 | app.ctx.arc(0, 0, this.entity.radius + 8, 0, this.progress * Math.PI * 2)
364 | app.ctx.stroke();
365 |
366 | app.ctx.restore();
367 |
368 | }
369 |
370 |
371 |
372 | },
373 |
374 | canFire: function() {
375 |
376 | if (!this.game.checkBonus("laser")) return;
377 |
378 | if (this.fireCooldown > 0) return;
379 | if (!this.target) return;
380 | if (Utils.distance(this, this.target) > this.range) return;
381 |
382 | this.fireCooldown = this.fireInterval;
383 |
384 | this.fire();
385 |
386 | },
387 |
388 | fire: function() {
389 |
390 | this.game.add(ENGINE.Bullet, {
391 | x: this.x,
392 | y: this.y,
393 | team: this.team,
394 | target: this.target,
395 | damage: 2,
396 | speed: 1000
397 | });
398 |
399 | if (!this.game.benchmark) app.sound.play("laser");
400 |
401 | },
402 |
403 | moveTo: function(destination) {
404 |
405 | this.destination = destination;
406 |
407 | },
408 |
409 | updateTooltip: function() {
410 |
411 | if (this.entity) {
412 | if (this.entity.tooltip) this.game.tooltip = this.entity.tooltip;
413 | } else {
414 | this.game.tooltip = false;
415 | }
416 |
417 | }
418 |
419 | }
--------------------------------------------------------------------------------
/script/Engine.js:
--------------------------------------------------------------------------------
1 | ENGINE = { };
--------------------------------------------------------------------------------
/script/Game.js:
--------------------------------------------------------------------------------
1 | /* The counter in the top-left corner is:
2 |
3 | AVERAGE FRAME TIME | DEVICE POWER | ENTITIES COUNT
4 | (baselineFactor)
5 | */
6 |
7 |
8 | /* Reference baseline to calculate device power */
9 |
10 | REFERENCE_BASELINE = 378;
11 |
12 | /* Reference frame time to tell how well the game has been optimized */
13 | /* Make it higher to give user more CPU power */
14 |
15 | REFERENCE_FRAME_TIME = 0.8;
16 |
17 | /* How much optimization value one ship drains */
18 |
19 | SHIP_CPU_COST = 0.1;
20 |
21 | ENGINE.Game = {
22 |
23 | bonuses: {
24 |
25 | magnet: 0.1,
26 | laser: 0.2,
27 | shield: 0.4
28 |
29 | },
30 |
31 | explosion: function(x, y, count, color) {
32 |
33 | if (!this.particlesPool) {
34 |
35 | this.particlesPool = [];
36 |
37 | for (var i = 0; i < 100; i++) {
38 |
39 | var particle = this.add(ENGINE.Particle, {
40 | x: x,
41 | y: y
42 | });
43 |
44 | this.particlesPool.push(particle);
45 |
46 | }
47 |
48 | this.particleIndex = 0;
49 |
50 | }
51 |
52 | for (var i = 0; i <= count; i++) {
53 |
54 | if (++this.particleIndex >= this.particlesPool.length) this.particleIndex = 0;;
55 |
56 | var particle = this.particlesPool[this.particleIndex];
57 |
58 | particle.x = x;
59 | particle.y = y;
60 | particle.color = color;
61 |
62 | particle.reset();
63 |
64 | }
65 |
66 | },
67 |
68 | random: function() {
69 |
70 | return this.benchmark ? 0.5 : Math.random();
71 |
72 | },
73 |
74 | add: function(constructor, args) {
75 |
76 | args = args || {};
77 |
78 | args.game = this;
79 |
80 | var entity = new constructor(args);
81 |
82 | this.entities.push(entity);
83 |
84 | return entity;
85 |
86 | },
87 |
88 | remove: function(entity) {
89 |
90 | entity.dead = true;
91 |
92 | },
93 |
94 | scaleComicBubble: function() {
95 |
96 | this.comicScale = 1.0;
97 |
98 | $comicbubble = document.body.querySelector("#comicbubble");
99 |
100 | var tween = app.tween(this).to({
101 | comicScale: 0.5
102 | });
103 |
104 | tween.onstep = function(app) {
105 |
106 | $comicbubble.style.transform = "scale(" + app.comicScale + "," + app.comicScale + ")";
107 |
108 | }
109 |
110 | },
111 |
112 | enter: function() {
113 | if (window.ga) {
114 | ga('send', 'screenview', {
115 | 'appName': 'PowerSurge',
116 | 'screenName': 'Game'
117 | });
118 | }
119 |
120 | app.renderer.setSmoothing(false);
121 |
122 | this.scaleComicBubble();
123 |
124 | localStorage.setItem("baseline", app.baseline);
125 |
126 | this.music = app.music.play("dust").volume(0.5).fadeIn(4).loop();
127 |
128 | this.gradient = app.ctx.createRadialGradient(app.center.x, app.center.y, 0, app.center.x, app.center.y, app.center.x);
129 |
130 | this.gradient.addColorStop(0.0, "transparent");
131 | this.gradient.addColorStop(1.0, "#000");
132 |
133 | this.reset();
134 |
135 | },
136 |
137 | leave: function() {
138 |
139 | this.music.fadeOut(2);
140 |
141 | },
142 |
143 | getScale: function(entity) {
144 |
145 | return 1 - Math.min(1.0, Utils.distance(entity, app.center) / (app.width * 0.5)) * 0.75;
146 |
147 | },
148 |
149 | spawnAsteroid: function() {
150 |
151 | var angle = Math.random() * Math.PI * 2;
152 | var radius = app.width / 2;
153 | var ox = Math.cos(angle) * radius;
154 | var oy = Math.sin(angle) * radius;
155 |
156 | this.add(ENGINE.Asteroid, {
157 | x: app.center.x + ox,
158 | y: app.center.y + oy
159 | });
160 |
161 | },
162 |
163 | reset: function() {
164 |
165 | this.spawnTimeout = 0;
166 | this.cpuUsage = 0;
167 | this.cpuBarProgress = 0;
168 |
169 | this.upgrades = {
170 |
171 | speed: 1,
172 | damage: 1,
173 | life: 1
174 |
175 | };
176 |
177 | delete this.particlesPool;
178 |
179 | this.score = 0;
180 |
181 | this.wave = 0;
182 |
183 | this.tooltip = false;
184 |
185 | this.entities = [];
186 |
187 | this.stars = this.add(ENGINE.BackgroundStars);
188 |
189 | this.playerPlanet = this.add(ENGINE.Planet, {
190 | x: app.center.x,
191 | y: app.center.y,
192 | team: 1
193 | });
194 |
195 | this.player = new ENGINE.Cursor(this, 1, this.playerPlanet);
196 |
197 | this.player.x = app.center.x;
198 | this.player.y = app.center.y;
199 |
200 | for (var i = 0; i < 8; i++) {
201 |
202 | this.spawnAsteroid();
203 |
204 | }
205 |
206 | var buttons = ["speed", "life", "damage"];
207 |
208 | this.buttons = {};
209 |
210 | for (var i = 0; i < buttons.length; i++) {
211 |
212 | var key = buttons[i];
213 |
214 | this.buttons[key] = this.add(ENGINE.Button, {
215 | color: defs.teamColor[1],
216 | x: app.center.x - 80 + i * 100,
217 | y: app.height - 70,
218 | sprite: defs.buttons[key],
219 | key: key,
220 | count: 1,
221 | hoverable: "build",
222 | tooltip: defs.tooltips[key]
223 | })
224 | }
225 |
226 | this.nextWave();
227 |
228 | this.explosion(app.center.x, app.center.y, 1);
229 |
230 | },
231 |
232 | cpuHistory: [],
233 |
234 | step: function(dt) {
235 |
236 | var before = performance.now();
237 |
238 | /* slow motion - when you collect freeze powerup */
239 |
240 | this.timeFactor = 1.0;
241 |
242 | if (this.freezeLifespan > 0) {
243 |
244 | this.freezeLifespan -= dt;
245 | this.timeFactor = 0.1;
246 |
247 | }
248 |
249 | /* update the game 10 times to magnitude results in profiler */
250 |
251 | var MAGNIFY = 5;
252 |
253 | var quota = 0.0;
254 |
255 | for (var i = 0; i < this.entities.length; i++) {
256 | var entity = this.entities[i];
257 | quota += entity.quota || 0.7;
258 |
259 | for (var j = 0; j < MAGNIFY; j++) {
260 | entity.step(dt / MAGNIFY);
261 |
262 | if (entity.dead) {
263 | this.entities.splice(i--, 1);
264 | break;
265 | }
266 | }
267 | }
268 |
269 | this.quota = quota;
270 |
271 | var frameTime = (performance.now() - before) / MAGNIFY;
272 |
273 | /* measure optimization */
274 |
275 | /* It's the average of 100 frame times */
276 |
277 | /*
278 |
279 | baselineFactor - baseline vs reference sample to get device power
280 | if the device is over-powered we artificialy
281 | make frameTime higher to make it more fair among the players
282 |
283 | optimizationRating - reference frame time divided by (current) average frame time
284 | handicaped by baselineFactor - this gives a factor of
285 | how well user optimized the game
286 |
287 | Make REFERENCE_FRAME_TIME higher to give player MORE cpu output
288 |
289 | */
290 |
291 |
292 | this.cpuHistory.push(frameTime / quota);
293 |
294 | if (this.cpuHistory.length > 60) this.cpuHistory.shift();
295 |
296 | this.averageFrameTime = this.average(this.cpuHistory);
297 |
298 | this.optimizationRating = ((0.8 / app.baseline) / (this.averageFrameTime));
299 |
300 | this.player.step(dt);
301 |
302 | /* use optimization results to affect the game */
303 |
304 | this.applyOptimization(dt);
305 |
306 |
307 | },
308 |
309 | average: function(array) {
310 |
311 | if (!array.length) return 0;
312 |
313 | var sum = 0;
314 |
315 | for (var i = 0; i < array.length; i++) {
316 | sum += array[i];
317 | }
318 |
319 | return sum / array.length;
320 |
321 | },
322 |
323 | applyOptimization: function(dt) {
324 |
325 | var cpuUsage = 0;
326 |
327 | /* calculate (artificial) cpuUsage of ships
328 | if cpuUsage is greater than optimizationRating
329 | freeze a ship
330 | */
331 |
332 | for (var i = 0; i < this.entities.length; i++) {
333 |
334 | var entity = this.entities[i];
335 |
336 | if (!(entity instanceof ENGINE.Ship)) continue;
337 | if (!entity.team) continue;
338 | if (entity.free) continue;
339 |
340 | cpuUsage += SHIP_CPU_COST;
341 |
342 | if (cpuUsage < this.optimizationRating) {
343 |
344 | entity.frozen = false;
345 |
346 | } else {
347 |
348 | entity.frozen = true;
349 |
350 | }
351 |
352 | }
353 |
354 | /* tween cpuUsage instead of setting it instantly (less jittering) */
355 |
356 | this.cpuUsage = Utils.moveTo(this.cpuUsage, cpuUsage, Math.abs(this.cpuUsage - cpuUsage) * 0.25 * dt);
357 | this.realCpuUsage = cpuUsage;
358 |
359 | /* that's the value 0.0 - 1.0 that coresponds with the yellow power bar */
360 |
361 | this.cpuRatio = 1 - Math.min(1.0, this.cpuUsage / this.optimizationRating);
362 | this.cpuBarProgress = Utils.moveTo(this.cpuBarProgress, this.cpuRatio, 0.2 * dt);
363 |
364 | /* spawn ships if there is enough power */
365 |
366 | if ((this.spawnTimeout -= dt) <= 0) {
367 |
368 | this.spawnTimeout = 0.5;
369 |
370 | //if (this.cpuRatio > 0.5) this.playerPlanet.spawnShip("fighter");
371 | if (this.optimizationRating > this.realCpuUsage + 0.1) this.playerPlanet.spawnShip("fighter");
372 |
373 | }
374 |
375 | },
376 |
377 | shake: function() {
378 |
379 | this.shakeLifespan = 0.4;
380 |
381 | },
382 |
383 | render: function(dt) {
384 |
385 | if (!this.averageFrameTime) return;
386 |
387 | app.ctx.textBaseline = "top";
388 | app.ctx.save();
389 |
390 | app.ctx.fillStyle = "#282245";
391 | app.ctx.fillRect(0, 0, app.width, app.height);
392 |
393 | // app.ctx.fillStyle = this.gradient;
394 | //app.ctx.fillRect(0, 0, app.width, app.height);
395 |
396 | if (this.shakeLifespan > 0) {
397 | this.shakeLifespan -= dt;
398 | var chaos = Utils.random(-6, 6);
399 | app.ctx.translate(chaos, chaos)
400 | }
401 |
402 | for (var i = 0; i < this.entities.length; i++) {
403 |
404 | this.entities[i].render();
405 |
406 | }
407 |
408 | this.player.render();
409 |
410 | this.renderTooltip();
411 |
412 | app.ctx.textAlign = "right";
413 | app.ctx.font = "bold 16px Arial";
414 | app.ctx.fillStyle = "#fff";
415 | app.ctx.fillText("SCORE: " + this.score, app.width - 20, 20);
416 |
417 | this.renderCPUBar();
418 | // this.renderBonuses();
419 |
420 | app.ctx.textAlign = "center";
421 | app.ctx.font = "bold 64px Arial";
422 | app.ctx.fillStyle = "#fa0";
423 | app.ctx.fillText(this.player.resources, app.center.x - 180, app.height - 104);
424 |
425 | // app.ctx.textAlign = "left";
426 | // app.ctx.font = "bold 16px Arial";
427 | // app.ctx.fillStyle = "#fff";
428 | // app.ctx.fillText(
429 | // this.optimizationRating.toFixed(2) + " | " +
430 | // // this.baselineFactor.toFixed(2) + " | " +
431 | // this.entities.length + ' + ' +
432 | // this.quota.toFixed(1), 16, 16);
433 |
434 | app.ctx.restore();
435 |
436 | },
437 |
438 | barWidth: 200,
439 |
440 | renderCPUBar: function() {
441 |
442 |
443 | var width = 200;
444 | var currentWidth = this.barWidth * this.cpuBarProgress;
445 |
446 | app.ctx.drawImage(app.images.spritesheet,
447 | defs.frozenSprite[0], defs.frozenSprite[1], defs.frozenSprite[2], defs.frozenSprite[3],
448 | app.center.x - this.barWidth / 2 - 32, 24, defs.frozenSprite[2], defs.frozenSprite[3]);
449 |
450 |
451 | app.ctx.strokeStyle = "#fa0";
452 | app.ctx.fillStyle = "#fa0";
453 | app.ctx.lineWidth = 2;
454 |
455 | app.ctx.strokeRect(app.center.x - this.barWidth / 2, 16, this.barWidth, 32)
456 | app.ctx.fillRect(app.center.x - this.barWidth / 2, 16, currentWidth, 32)
457 |
458 | app.ctx.fillStyle = "#fff";
459 | app.ctx.textAlign = "center";
460 | app.fontSize(16);
461 | app.ctx.fillText("AVAILABLE CPU", app.center.x, 24);
462 |
463 | app.ctx.textAlign = "left";
464 | app.ctx.fillStyle = "#fa0";
465 |
466 | app.ctx.fillText("+ " + this.optimizationRating.toFixed(2), app.center.x + width / 2 + 16, 16);
467 |
468 | app.ctx.fillStyle = "#c40";
469 | app.ctx.fillText("- " + this.realCpuUsage.toFixed(2), app.center.x + width / 2 + 16, 32);
470 |
471 | },
472 |
473 |
474 | renderBonuses: function() {
475 |
476 | app.ctx.save();
477 | app.ctx.translate(app.center.x - this.barWidth / 2, 54);
478 | app.ctx.textAlign = "left";
479 | app.ctx.textBaseline = "top";
480 |
481 | var i = Object.keys(this.bonuses).length;
482 |
483 | for (var key in this.bonuses) {
484 |
485 | var threshold = this.bonuses[key];
486 |
487 | var x = this.barWidth * threshold;
488 | var y = i * 16;
489 |
490 | app.ctx.globalAlpha = this.checkBonus(key) ? 1.0 : 0.4;
491 |
492 | app.ctx.fillStyle = "#fff";
493 | app.ctx.fillRect(x, 0, 2, y);
494 | app.ctx.fillRect(x, y, 16, 2);
495 |
496 | app.ctx.fillStyle = "#fff";
497 | app.fontSize(12);
498 | app.ctx.fillText(defs.bonuses[key].toUpperCase(), x + 20, y - 6);
499 |
500 | i--;
501 |
502 | }
503 |
504 | app.ctx.restore();
505 |
506 | },
507 |
508 |
509 | renderTooltip: function() {
510 |
511 | if (!this.tooltip) return;
512 |
513 | app.layer.textAlign("center").fillStyle("#fff").font("16px Arial").textWithBackground(this.tooltip, app.center.x, app.height - 64, "rgba(0,0,0,0.6)", 16);
514 |
515 | },
516 |
517 | pointermove: function(e) {
518 |
519 | this.player.x = e.x;
520 | this.player.y = e.y;
521 |
522 | },
523 |
524 | wrap: function(entity) {
525 |
526 | if (entity.x + entity.radius < 0) entity.x = app.width + entity.radius;
527 | if (entity.x - entity.radius > app.width) entity.x = -entity.radius;
528 | if (entity.y + entity.radius < 0) entity.y = app.height + entity.radius;
529 | if (entity.y - entity.radius > app.height) entity.y = -entity.radius;
530 |
531 | },
532 |
533 | keydown: function(e) {
534 |
535 | },
536 |
537 | nextWave: function() {
538 |
539 | if (this.benchmark) return;
540 |
541 | this.wave++;
542 |
543 | this.shipsLeft = 0;
544 |
545 | var streamsPositions = [
546 | [0.0, 1.0],
547 | [0.0, 0.5],
548 | [0.0, 0.0],
549 | [1.0, 0.0],
550 | [1.0, 0.5],
551 | [1.0, 1.0]
552 | ];
553 |
554 | var difficulty = this.wave / 20;
555 |
556 | Utils.shuffle(streamsPositions);
557 |
558 | var streamsCount = Math.min(3, 1 + difficulty) + 0.3 | 0;
559 | var shipsPerStream = Math.min(16, 4 + difficulty * 4) | 0;
560 |
561 | var possibleShips = [];
562 |
563 | if (this.wave > 0) possibleShips.push("creep1");
564 | if (this.wave > 3) possibleShips.push("creep2");
565 | if (this.wave > 6) possibleShips.push("creep3");
566 | if (this.wave > 10) possibleShips.push("creep4");
567 |
568 | if (this.wave % 5 === 0) possibleShips = ["boss"];
569 |
570 | for (var i = 0; i < streamsCount; i++) {
571 |
572 | var stream = streamsPositions.pop();
573 |
574 | var x = stream[0] * app.width;
575 | var y = stream[1] * app.height;
576 |
577 | var ship = Utils.random(possibleShips);
578 | var shipData = defs.ships[ship];
579 | var angle = Math.atan2(y - app.center.y, x - app.center.x);
580 |
581 | for (var j = 0; j < shipsPerStream; j++) {
582 |
583 | var entity = this.add(ENGINE.Ship, {
584 | type: ship,
585 | x: x + Math.cos(angle) * j * 100,
586 | y: y + Math.sin(angle) * j * 100,
587 | team: 0
588 | });
589 |
590 | this.shipsLeft++;
591 |
592 | if (shipData.boss) {
593 |
594 | entity.hp = entity.maxHp = this.score;
595 | entity.damage = this.score / 50 | 0;
596 | entity.scale = 0.5 + this.score / 200;
597 |
598 | break;
599 |
600 | }
601 |
602 | }
603 |
604 | }
605 |
606 | },
607 |
608 | repairShips: function() {
609 |
610 | var ships = Utils.filter(this.entities, function(e) {
611 | return (e instanceof ENGINE.Ship) && e.team;
612 | });
613 |
614 | for (var i = 0; i < ships.length; i++) {
615 |
616 | ships[i].repair();
617 |
618 | }
619 |
620 | },
621 |
622 | onenemydeath: function(ship) {
623 |
624 | this.shipsLeft--;
625 |
626 | if (this.shipsLeft <= 0) this.nextWave();
627 |
628 | },
629 |
630 | pointerdown: function(e) {
631 |
632 | },
633 |
634 | gameover: function() {
635 |
636 | ENGINE.Gameover.score = this.score;
637 |
638 | if (window.ga) {
639 | ga('send', {
640 | 'hitType': 'event',
641 | 'eventCategory': 'game',
642 | 'eventAction': 'over',
643 | 'eventValue': this.score,
644 | 'nonInteraction': true
645 | });
646 | }
647 |
648 | app.setState(ENGINE.Gameover);
649 |
650 | },
651 |
652 | checkBonus: function(key) {
653 |
654 | return true;
655 |
656 | }
657 |
658 | };
--------------------------------------------------------------------------------
/script/Gameover.js:
--------------------------------------------------------------------------------
1 | ENGINE.Gameover = {
2 |
3 | score: 737,
4 | hiscore: 0,
5 |
6 | starOff: [382, 177, 15, 16],
7 | starOn: [339, 169, 37, 37],
8 |
9 | enter: function() {
10 | if (window.ga) {
11 | ga('send', 'screenview', {
12 | 'appName': 'PowerSurge',
13 | 'screenName': 'Gameover'
14 | });
15 | }
16 |
17 | this.done = false;
18 |
19 | app.renderer.setSmoothing(true);
20 |
21 | var hiscore = localStorage.getItem("hiscore") | 0;
22 |
23 | if (hiscore < this.score) {
24 |
25 | this.hiscore = this.score;
26 | localStorage.setItem("hiscore", hiscore);
27 |
28 | }
29 |
30 | this.music = app.music.play("gameover").fadeIn(3).loop();
31 |
32 | this.currentScore = 0;
33 | this.stars = [];
34 | this.scoreOffset = -app.width;
35 | this.achievedStars = Math.min(10, (this.score / 500) * 10 | 0);
36 |
37 | for (var i = 0; i < 10; i++) {
38 |
39 | this.stars.push({
40 | x: i * 64,
41 | y: 64,
42 | scale: 0
43 | });
44 |
45 | }
46 |
47 | for (var i = 0; i < this.achievedStars; i++) {
48 |
49 | var star = this.stars[i];
50 |
51 | app.tween(star).wait(i * 0.1).to({
52 | scale: 1.0,
53 | y: 64
54 | }, 2.5, "outElastic");
55 |
56 | }
57 |
58 | app.tween(this).to({
59 |
60 | currentScore: this.score,
61 | scoreOffset: 0
62 |
63 | }, 2.5, "outElastic").on("finished", function() {
64 |
65 | app.state.done = true;
66 |
67 | });
68 |
69 |
70 | },
71 |
72 | step: function() {
73 |
74 | },
75 |
76 | renderStars: function(x, y) {
77 |
78 |
79 | for (var i = 0; i < 10; i++) {
80 |
81 | var star = this.stars[i];
82 |
83 | app.layer.save();
84 |
85 | app.layer.translate(star.x + x, star.y + y);
86 |
87 | app.layer.align(0.5, 0.5);
88 |
89 | app.layer.drawRegion(app.images.spritesheet, this.starOff, 0, 0);
90 |
91 | if (star.scale > 0) {
92 |
93 | app.layer.rotate(app.lifetime);
94 | app.layer.scale(star.scale, star.scale);
95 | app.layer.drawRegion(app.images.spritesheet, this.starOn, 0, 0);
96 | }
97 |
98 | app.layer.restore();
99 |
100 | }
101 |
102 | },
103 |
104 | render: function() {
105 |
106 | app.ctx.fillStyle = "#282245";
107 |
108 | app.ctx.fillRect(0, 0, app.width, app.height);
109 |
110 | app.ctx.drawImage(app.images.help, app.center.x - app.images.help.width * 0.5 | 0, -50)
111 |
112 | this.renderStars(app.center.x - 320, 0);
113 |
114 | app.fontSize(48);
115 |
116 | app.ctx.fillStyle = "#fa0";
117 | app.ctx.textAlign = "center";
118 |
119 | app.ctx.fillText("SCORE: " + (this.currentScore | 0), app.center.x + this.scoreOffset, 180)
120 |
121 | app.fontSize(32);
122 |
123 | app.ctx.fillStyle = "#f40";
124 | app.ctx.textAlign = "center";
125 |
126 | app.ctx.fillText("HI-SCORE: " + (this.hiscore | 0), app.center.x - this.scoreOffset, 220);
127 |
128 | if (this.done) {
129 |
130 | app.ctx.fillStyle = "#cef";
131 | app.ctx.textAlign = "center";
132 |
133 | if (app.lifetime % 1 < 0.5) {
134 |
135 | app.ctx.fillText("CLICK TO TRY AGAIN ", app.center.x - this.scoreOffset, 260)
136 |
137 | }
138 |
139 | }
140 |
141 | },
142 |
143 | pointerdown: function() {
144 |
145 | if (this.done) {
146 | if (window.ga) {
147 | ga('send', {
148 | 'hitType': 'event',
149 | 'eventCategory': 'game',
150 | 'eventAction': 'restart'
151 | });
152 | }
153 |
154 | app.setState(ENGINE.Game);
155 |
156 | ENGINE.Game.reset();
157 |
158 | }
159 |
160 | }
161 |
162 | };
--------------------------------------------------------------------------------
/script/Main.js:
--------------------------------------------------------------------------------
1 | document.addEventListener("DOMContentLoaded", function(event) {
2 |
3 | app = playground({
4 |
5 | width: 1024,
6 | height: 640,
7 |
8 | smoothing: true,
9 |
10 | paths: {
11 |
12 | base: "//mozilla.github.io/devtools-perf-game/"
13 |
14 | },
15 |
16 | updateDownloadText: function() {
17 |
18 | if (navigator.userAgent.indexOf("Firefox/40") > -1) {
19 |
20 | var text = defs.downloadLinks["ffdev"][0];
21 | var link = defs.downloadLinks["ffdev"][1];
22 |
23 | } else {
24 |
25 | var text = defs.downloadLinks["default"][0];
26 | var link = defs.downloadLinks["default"][1];
27 |
28 | }
29 |
30 | document.body.querySelector("#comicbubble").innerHTML = text;
31 | document.body.querySelector("#comicbubble").setAttribute("href", link);
32 |
33 | },
34 |
35 | /* set context font size with default font */
36 |
37 | fontSize: function(size) {
38 |
39 | return this.ctx.font = size + "px 'Squada One'";
40 |
41 | },
42 |
43 | create: function() {
44 |
45 | this.loadImages("spritesheet", "help", "splash", "flare", "particles");
46 |
47 | this.keyboard.preventDefault = false;
48 |
49 | this.sound = this.audio.channel("sound").volume(0.5);
50 | this.music = this.audio.channel("music").volume(0.5);
51 |
52 | this.ctx = app.layer.context;
53 |
54 | this.game = ENGINE.Game;
55 |
56 | },
57 |
58 | /* all images loaded */
59 |
60 | ready: function() {
61 |
62 | this.updateDownloadText();
63 |
64 | /* cache some known colors for spritesheet */
65 |
66 | this.getColoredImage(this.images.spritesheet, "#fff");
67 |
68 | /* start the benchmark */
69 |
70 | this.setState(ENGINE.Benchmark);
71 |
72 | },
73 |
74 | resize: function() {
75 |
76 | this.state.render(0);
77 |
78 | },
79 |
80 | getColoredImage: function(key, color, mode) {
81 |
82 | if (typeof mode === "undefined") mode = "hard-light";
83 |
84 | if (typeof key === "string") {
85 | var image = this.images[key];
86 | } else {
87 | var image = key;
88 | }
89 |
90 | var storekey = "color-" + color;
91 |
92 | if (!image[storekey]) {
93 |
94 | if (typeof mix === "undefined") mix = 1;
95 |
96 | var below = document.createElement("canvas");
97 | belowCtx = below.getContext("2d");
98 |
99 | below.width = image.width;
100 | below.height = image.height;
101 |
102 | belowCtx.drawImage(image, 0, 0);
103 | belowCtx.globalCompositeOperation = "source-atop";
104 | belowCtx.fillStyle = color;
105 | belowCtx.fillRect(0, 0, image.width, image.height);
106 |
107 | image[storekey] = below;
108 |
109 | }
110 |
111 | return image[storekey];
112 |
113 | },
114 |
115 | roundAngle: function(angle) {
116 |
117 | return Utils.ground(angle - Math.PI / 16, Math.PI / 8);
118 |
119 | },
120 |
121 | visibilitychange: function(hidden) {
122 |
123 | if (hidden) {
124 | if (!this.storedSoundVolume) {
125 | this.storedSoundVolume = this.sound.volume();
126 | this.storedMusicVolume = this.music.volume();
127 | this.sound.volume(0);
128 | this.music.volume(0);
129 | }
130 | } else {
131 | if (this.storedSoundVolume) {
132 | this.sound.volume(this.storedSoundVolume);
133 | this.music.volume(this.storedMusicVolume);
134 | this.storedSoundVolume = 0;
135 | this.storedMusicVolume = 0;
136 | }
137 | }
138 |
139 | }
140 |
141 | });
142 |
143 | });
144 |
145 | var performance = window.performance || window.webkitPerformance || Date;
146 |
147 | Math.sign = Math.sign || function(x) {
148 |
149 | x = +x; // convert to a number
150 |
151 | if (x === 0 || isNaN(x)) {
152 |
153 | return x;
154 |
155 | }
156 |
157 | return x > 0 ? 1 : -1;
158 |
159 | };
--------------------------------------------------------------------------------
/script/Missile.js:
--------------------------------------------------------------------------------
1 | ENGINE.Missile = function(args) {
2 |
3 | Utils.extend(this, {
4 | speed: 400
5 | }, args);
6 |
7 | this.color = defs.teamColor[this.team];
8 | this.radius = 4;
9 | this.direction = 0;
10 |
11 | this.force = 400;
12 | this.forceDirection = Math.random() * 6;
13 |
14 | this.trail = new ENGINE.Trail(this, {
15 | interval: 0.05,
16 | maxPoints: 10,
17 | color: "#fa0"
18 | });
19 |
20 | for (var i = 0; i < this.game.entities.length; i++) {
21 |
22 | var e = this.game.entities[i];
23 |
24 | if (!(e instanceof ENGINE.Ship)) continue;
25 |
26 | if (e.missileTarget) continue;
27 | if (e.team === this.team) continue;
28 |
29 | e.missileTarget = this;
30 | this.target = e;
31 |
32 | break;
33 |
34 | }
35 |
36 | };
37 |
38 | ENGINE.Missile.prototype = {
39 |
40 | sprite: [145, 25, 6, 39],
41 |
42 | quota: 0.5,
43 |
44 | constructor: ENGINE.Missile,
45 |
46 | step: function(dt) {
47 |
48 | if(!this.target) return this.die();
49 |
50 | this.direction = Math.atan2(this.target.y - this.y, this.target.x - this.x);
51 |
52 | this.x += Math.cos(this.direction) * this.speed * dt;
53 | this.y += Math.sin(this.direction) * this.speed * dt;
54 |
55 | this.force = Math.max(this.force - dt * 400, 0);
56 |
57 | this.x += Math.cos(this.forceDirection) * this.force * dt;
58 | this.y += Math.sin(this.forceDirection) * this.force * dt;
59 |
60 |
61 | if (Utils.distance(this, this.target) < this.radius + this.target.radius) {
62 |
63 | this.hit(this.target);
64 |
65 | }
66 |
67 | this.trail.step(dt);
68 |
69 |
70 | },
71 |
72 | hit: function(target) {
73 |
74 | target.applyDamage(10 + this.game.score / 10);
75 |
76 | this.die();
77 |
78 | },
79 |
80 | die: function() {
81 |
82 | this.dead = true;
83 |
84 | },
85 |
86 | render: function() {
87 |
88 | this.trail.render();
89 |
90 | }
91 |
92 | };
--------------------------------------------------------------------------------
/script/Particle.js:
--------------------------------------------------------------------------------
1 | ENGINE.Particle = function(args) {
2 |
3 | Utils.extend(this, {
4 | color: "#0fa",
5 | radius: 4
6 | }, args)
7 |
8 | this.spriteIndex = 0;
9 |
10 | this.reset();
11 |
12 | };
13 |
14 | ENGINE.Particle.prototype = {
15 |
16 | constructor: ENGINE.Particle,
17 |
18 | quota: 0.5,
19 |
20 | sprites: [
21 | [0, 0, 6, 6],
22 | [0, 7, 5, 5],
23 | [0, 13, 5, 5],
24 | [1, 19, 3, 3]
25 | ],
26 |
27 | reset: function() {
28 |
29 | this.lifetime = 0;
30 | this.duration = 0.5;
31 |
32 | this.direction = this.game.random() * 6.28;
33 | this.speed = 32 + this.game.random() * 128;
34 |
35 | this.speed *= 3;
36 |
37 | this.damping = this.speed * 2;
38 |
39 | },
40 |
41 | step: function(dt) {
42 |
43 | this.lifetime += dt;
44 |
45 | this.x += Math.cos(this.direction) * this.speed * dt;
46 | this.y += Math.sin(this.direction) * this.speed * dt;
47 |
48 | this.speed = Math.max(0, this.speed - this.damping * dt);
49 |
50 | this.progress = Math.min(this.lifetime / this.duration, 1.0);
51 |
52 | if (this.progress >= 1.0) {
53 | this.x = 0;
54 | this.y = 0;
55 | this.progress = 0;
56 | }
57 |
58 | this.spriteIndex = this.progress * this.sprites.length | 0;
59 |
60 | },
61 |
62 | render: function() {
63 |
64 |
65 | // var s = this.size * (1 - this.progress);
66 |
67 | // if (s > 0) {
68 | if (this.progress >= 1.0) return;
69 |
70 | this.image = app.getColoredImage(app.images.particles, this.color || "#0fa");
71 |
72 | // app.ctx.fillStyle = this.color;
73 | // app.ctx.fillRect(this.x - s / 2, this.y - s / 2, s, s)
74 |
75 | var sprite = this.sprites[this.spriteIndex];
76 |
77 | app.ctx.drawImage(this.image, sprite[0], sprite[1], sprite[2], sprite[3],
78 | this.x, this.y, sprite[2], sprite[3])
79 |
80 | // }
81 |
82 | }
83 |
84 | };
--------------------------------------------------------------------------------
/script/Planet.js:
--------------------------------------------------------------------------------
1 | ENGINE.Planet = function(args) {
2 |
3 | Utils.extend(this, {
4 |
5 | radius: 48,
6 | hp: 20,
7 | max: 100,
8 | ships: 0,
9 | repairProgress: 0,
10 | repairTime: 4,
11 | asteroidsShield: true,
12 | shieldScale: 0.0
13 |
14 | }, args);
15 |
16 | this.maxHP = this.hp;
17 |
18 | this.lifetime = 0;
19 |
20 | };
21 |
22 | ENGINE.Planet.prototype = {
23 |
24 | constructor: ENGINE.Planet,
25 |
26 | type: "planet",
27 |
28 | hoverable: "repair",
29 |
30 | sprite: [201, 215, 104, 104],
31 |
32 | shieldSprite: [492, 320, 124, 124],
33 |
34 | repair: function() {
35 |
36 | this.hp++;
37 |
38 | },
39 |
40 | applyDamage: function(damage, attacker) {
41 |
42 | this.game.shake();
43 |
44 | this.hp--;
45 |
46 | if (this.hp <= 0 && !this.game.benchmark) this.game.gameover();
47 |
48 | if (!this.game.benchmark) app.sound.play("planetHit");
49 |
50 | this.game.add(ENGINE.CircleExplosion, {
51 | x: attacker.x,
52 | y: attacker.y,
53 | color: "#a04",
54 | radius: 32
55 | })
56 |
57 | },
58 |
59 | step: function(dt) {
60 |
61 | this.lifetime += dt;
62 |
63 | var prevShield = this.asteroidsShield;
64 | this.asteroidsShield = false;this.game.checkBonus("shield");
65 |
66 | if (prevShield !== this.asteroidsShield) {
67 |
68 | app.tween(this).discard().to({
69 | shieldScale: this.asteroidsShield ? 1.0 : 0.0
70 | }, 0.5, "outElastic");
71 |
72 | }
73 |
74 | },
75 |
76 | spawnShip: function(type) {
77 |
78 | var ship = this.game.add(ENGINE.Ship, {
79 | x: this.x,
80 | y: this.y,
81 | type: type,
82 | team: 1,
83 | planet: this
84 | });
85 |
86 | ship.forceDirection = Math.random() * 6;
87 | ship.force = 200;
88 |
89 | this.ships++;
90 |
91 | },
92 |
93 | render: function() {
94 |
95 | app.layer.align(0.5, 0.5);
96 | app.layer.drawRegion(app.images.spritesheet, this.sprite, this.x, this.y);
97 | app.layer.textAlign("center").font("bold 48px Arial").fillStyle("#fff").fillText(this.hp, this.x, this.y - 24);
98 | app.layer.realign();
99 |
100 | if (this.asteroidsShield && this.shieldScale > 0) {
101 | var scale = this.shieldScale;
102 | app.ctx.save();
103 | app.ctx.globalAlpha = 0.5;
104 | app.ctx.globalCompositeOperation = "lighter";
105 | app.ctx.translate(this.x, this.y);
106 | app.ctx.scale(scale, scale);
107 | app.ctx.drawImage(app.images.spritesheet, this.shieldSprite[0], this.shieldSprite[1], this.shieldSprite[2], this.shieldSprite[3], -this.shieldSprite[2] / 2, -this.shieldSprite[3] / 2, this.shieldSprite[2], this.shieldSprite[3]);
108 | app.ctx.restore();
109 | }
110 |
111 | }
112 |
113 | };
--------------------------------------------------------------------------------
/script/Playground.Scanlines.js:
--------------------------------------------------------------------------------
1 | /* scanlines plugin for playground's default renderer */
2 |
3 | PLAYGROUND.Scanlines = function(app) {
4 |
5 | this.app = app;
6 |
7 | app.on("resize", this.resize.bind(this));
8 | app.on("postrender", this.postrender.bind(this));
9 |
10 | };
11 |
12 | PLAYGROUND.Scanlines.plugin = true;
13 |
14 | PLAYGROUND.Scanlines.prototype = {
15 |
16 | resize: function() {
17 |
18 | this.image = cq(this.app.width, this.app.height);
19 |
20 | this.image.globalAlpha(0.1);
21 | this.image.fillStyle("#008");
22 |
23 | for (var i = 1; i < this.image.canvas.height; i += 8){
24 |
25 | this.image.fillRect(0, i, this.image.canvas.width, 4);
26 |
27 | }
28 |
29 | this.image = this.image.cache();
30 |
31 | },
32 |
33 | postrender: function() {
34 |
35 | if (this.image) {
36 |
37 | // this.app.layer.drawImage(this.image, 0, 0);
38 |
39 | }
40 |
41 | }
42 |
43 | };
--------------------------------------------------------------------------------
/script/Playground.SoundOnDemand.js:
--------------------------------------------------------------------------------
1 | /*
2 |
3 | SoundOnDemand r1
4 |
5 | (c) 2012-2015 http://rezoner.net
6 |
7 | This library may be freely distributed under the MIT license.
8 |
9 | */
10 |
11 | /* options */
12 |
13 | /* output: output node, default */
14 | /* audioContext: audioContext */
15 |
16 | SoundOnDemand = function(options) {
17 |
18 | options = options || {};
19 |
20 | var canPlayMp3 = (new Audio).canPlayType("audio/mp3");
21 | var canPlayOgg = (new Audio).canPlayType('audio/ogg; codecs="vorbis"');
22 |
23 | if (this.preferedAudioFormat === "mp3") {
24 |
25 | if (canPlayMp3) this.audioFormat = "mp3";
26 | else this.audioFormat = "ogg";
27 |
28 | } else {
29 |
30 | if (canPlayOgg) this.audioFormat = "ogg";
31 | else this.audioFormat = "mp3";
32 |
33 | }
34 |
35 | if (!options.audioContext) {
36 | console.warn('Possible duplicated AudioContext, use options.audioContext');
37 | }
38 | this.audioContext = options.audioContext || new AudioContext;
39 |
40 | this.compressor = this.audioContext.createDynamicsCompressor();
41 | this.compressor.connect(this.audioContext.destination);
42 |
43 | this.gainNode = this.audioContext.createGain()
44 | this.gainNode.connect(this.compressor);
45 |
46 | this.input = this.gainNode;
47 |
48 | this.gainNode.gain.value = 1.0;
49 |
50 | this.buffers = {};
51 |
52 | this.channels = {};
53 | this.aliases = {};
54 |
55 | var lastTick = Date.now();
56 | var engine = this;
57 |
58 | setInterval(function() {
59 |
60 | var delta = (Date.now() - lastTick) / 1000;
61 |
62 | lastTick = Date.now();
63 |
64 | engine.step(delta);
65 |
66 | }, 1000 / 60);
67 |
68 | };
69 |
70 | SoundOnDemand.moveTo = function(value, target, step) {
71 |
72 | if (value < target) {
73 | value += step;
74 | if (value > target) value = target;
75 | }
76 |
77 | if (value > target) {
78 | value -= step;
79 | if (value < target) value = target;
80 | }
81 |
82 | return value;
83 |
84 | };
85 |
86 | SoundOnDemand.prototype = {
87 |
88 | constructor: SoundOnDemand,
89 |
90 | path: "sounds/",
91 |
92 | channel: function(name) {
93 |
94 | if (!this.channels[name]) this.channels[name] = new SoundOnDemand.Channel(this);
95 |
96 | return this.channels[name];
97 |
98 | },
99 |
100 | getAssetEntry: function(path, defaultExtension) {
101 |
102 | /* translate folder according to user provided paths
103 | or leave as is */
104 |
105 | var fileinfo = path.match(/(.*)\..*/);
106 | var key = fileinfo ? fileinfo[1] : path;
107 |
108 | var temp = path.split(".");
109 | var basename = path;
110 |
111 | if (temp.length > 1) {
112 | var ext = temp.pop();
113 | path = temp.join(".");
114 | } else {
115 | var ext = defaultExtension;
116 | basename += "." + defaultExtension;
117 | }
118 |
119 | return {
120 | key: key,
121 | url: this.path + basename,
122 | path: this.path + path,
123 | ext: ext
124 | };
125 |
126 | },
127 |
128 | loaders: {},
129 |
130 | load: function(key) {
131 |
132 | var engine = this;
133 | var entry = engine.getAssetEntry(key, engine.audioFormat);
134 |
135 | if (!this.loaders[key]) {
136 |
137 | this.loaders[key] = new Promise(function(resolve, reject) {
138 |
139 | if (engine.buffers[entry.key]) return resolve(engine.buffers[entry.key]);
140 |
141 | var request = new XMLHttpRequest();
142 |
143 | request.open("GET", entry.url, true);
144 | request.responseType = "arraybuffer";
145 |
146 | request.onload = function() {
147 | engine.audioContext.decodeAudioData(this.response, function(decodedBuffer) {
148 |
149 | engine.buffers[entry.key] = decodedBuffer;
150 | resolve(decodedBuffer);
151 |
152 | });
153 |
154 | }
155 |
156 | request.send();
157 |
158 | });
159 |
160 | }
161 |
162 | return this.loaders[key];
163 |
164 | },
165 |
166 | step: function(delta) {
167 |
168 | for (var key in this.channels) {
169 |
170 | this.channels[key].step(delta);
171 |
172 | }
173 |
174 | },
175 |
176 | duplicate: function(source, as, volume, rate) {
177 |
178 | var engine = this;
179 |
180 | this.load(source).then(function() {
181 |
182 | engine.buffers[source];
183 |
184 | engine.buffers[as] = engine.buffers[source];
185 |
186 | });
187 |
188 | },
189 |
190 | alias: function(name, source, rate, volume) {
191 |
192 | this.aliases[name] = {
193 | source: source,
194 | rate: rate,
195 | volume: volume
196 | };
197 |
198 | }
199 |
200 | };
201 | SoundOnDemand.Events = function() {
202 |
203 | this.listeners = {};
204 |
205 | };
206 |
207 | SoundOnDemand.Events.prototype = {
208 |
209 | on: function(event, callback) {
210 |
211 | if (typeof event === "object") {
212 | var result = {};
213 | for (var key in event) {
214 | result[key] = this.on(key, event[key])
215 | }
216 | return result;
217 | }
218 |
219 | if (!this.listeners[event]) this.listeners[event] = [];
220 |
221 | this.listeners[event].push(callback);
222 |
223 | return callback;
224 | },
225 |
226 | once: function(event, callback) {
227 |
228 | callback.once = true;
229 |
230 | if (!this.listeners[event]) this.listeners[event] = [];
231 |
232 | this.listeners[event].push(callback);
233 |
234 | return callback;
235 |
236 | },
237 |
238 | off: function(event, callback) {
239 |
240 | for (var i = 0, len = this.listeners[event].length; i < len; i++) {
241 | if (this.listeners[event][i]._remove) {
242 | this.listeners[event].splice(i--, 1);
243 | len--;
244 | }
245 | }
246 |
247 | },
248 |
249 | trigger: function(event, data) {
250 |
251 | /* if you prefer events pipe */
252 |
253 | if (this.listeners["event"]) {
254 | for (var i = 0, len = this.listeners["event"].length; i < len; i++) {
255 | this.listeners["event"][i](event, data);
256 | }
257 | }
258 |
259 | /* or subscribed to single event */
260 |
261 | if (this.listeners[event]) {
262 | for (var i = 0, len = this.listeners[event].length; i < len; i++) {
263 | var listener = this.listeners[event][i];
264 | listener.call(this, data);
265 |
266 | if (listener.once) {
267 | this.listeners[event].splice(i--, 1);
268 | len--;
269 | }
270 | }
271 | }
272 |
273 | }
274 |
275 | };
276 | SoundOnDemand.Channel = function(engine) {
277 |
278 | this.engine = engine;
279 | this.audioContext = engine.audioContext;
280 |
281 | /* connection order goes from bottom to top */
282 |
283 | /* gain node */
284 |
285 | this.gainNode = this.audioContext.createGain();
286 |
287 | /* convolver */
288 |
289 | this.convolverWetNode = this.audioContext.createGain();
290 | this.convolverDryNode = this.audioContext.createGain();
291 | this.convolverNode = this.audioContext.createConvolver();
292 | this.convolverEnabled = false;
293 |
294 | this.route();
295 |
296 | this.queue = [];
297 | this.loops = [];
298 |
299 | };
300 |
301 | SoundOnDemand.Channel.prototype = {
302 |
303 | constructor: SoundOnDemand.Channel,
304 |
305 | /* get a sound for further usage */
306 |
307 | xroute: function() {
308 |
309 | if (this.currentRoute) {
310 |
311 | for (var i = 0; i < this.currentRoute.length - 1; i++) {
312 |
313 | this.currentRoute[i].disconnect();
314 |
315 | }
316 |
317 | }
318 |
319 | this.currentRoute = [];
320 |
321 | for (var i = 0; i < arguments.length; i++) {
322 |
323 | if (i < arguments.length - 1) {
324 |
325 | var node = arguments[i];
326 |
327 | node.connect(arguments[i + 1]);
328 |
329 | }
330 |
331 | this.currentRoute.push(node);
332 |
333 | }
334 |
335 | this.input = arguments[0];
336 |
337 | },
338 |
339 | get: function(key) {
340 |
341 | return new SoundOnDemand.Sound(key, this);
342 |
343 | },
344 |
345 | play: function(key) {
346 |
347 | var sound = this.get(key);
348 |
349 | this.add(sound);
350 |
351 | return sound;
352 |
353 | },
354 |
355 | remove: function(sound) {
356 |
357 | sound._remove = true;
358 |
359 | },
360 |
361 | add: function(sound) {
362 |
363 | sound._remove = false;
364 |
365 | this.queue.push(sound);
366 |
367 | },
368 |
369 | step: function(delta) {
370 |
371 | /* process queue */
372 |
373 | for (var i = 0; i < this.queue.length; i++) {
374 |
375 | var sound = this.queue[i];
376 |
377 | sound.step(delta);
378 |
379 | if (sound._remove) this.queue.splice(i--, 1);
380 |
381 | }
382 |
383 | /* process sounds being played */
384 |
385 | },
386 |
387 | volume: function(value) {
388 |
389 | if (arguments.length) {
390 |
391 | this.gainNode.gain.value = value;
392 |
393 | return this;
394 |
395 | } else {
396 |
397 | return this.gainNode.gain.value;
398 |
399 | }
400 |
401 | },
402 |
403 | swapConvolver: function(key) {
404 |
405 | var engine = this.engine;
406 | var channel = this;
407 |
408 | return new Promise(function(resolve, fail) {
409 |
410 | if (channel.currentConvolverImpulse === key) {
411 |
412 | resolve();
413 |
414 | } else {
415 |
416 | engine.load(key).then(function(buffer) {
417 | channel.currentConvolverImpulse = key;
418 | channel.convolverNode.buffer = buffer;
419 | resolve();
420 | });
421 |
422 | }
423 |
424 | });
425 |
426 | },
427 |
428 | updateConvovlerState: function(enabled) {
429 |
430 | this.convolverEnabled = enabled;
431 | this.route();
432 |
433 | },
434 |
435 | subroute: function(nodes) {
436 |
437 | for (var i = 0; i < nodes.length; i++) {
438 |
439 | if (i < nodes.length - 1) {
440 |
441 | var node = nodes[i];
442 | node.disconnect();
443 | node.connect(nodes[i + 1]);
444 |
445 | }
446 |
447 | }
448 |
449 | this.input = nodes[0];
450 |
451 | },
452 |
453 | route: function() {
454 |
455 | this.gainNode.disconnect();
456 |
457 | if (this.convolverEnabled) {
458 |
459 | this.gainNode.connect(this.convolverDryNode);
460 |
461 | this.gainNode.connect(this.convolverNode);
462 | this.convolverNode.connect(this.convolverWetNode);
463 |
464 | this.convolverWetNode.connect(this.engine.input);
465 | this.convolverDryNode.connect(this.engine.input);
466 |
467 | } else {
468 |
469 | this.gainNode.connect(this.engine.input);
470 |
471 | }
472 |
473 | this.input = this.gainNode;
474 |
475 | },
476 |
477 | convolver: function(value, key) {
478 |
479 | var enabled = value > 0;
480 | var channel = this;
481 |
482 | this.swapConvolver(key).then(function() {
483 |
484 | if (enabled !== channel.convolverEnabled) channel.updateConvovlerState(enabled);
485 |
486 | });
487 |
488 | this.convolverWetNode.gain.value = value;
489 | this.convolverDryNode.gain.value = 1 - value;
490 |
491 | return this;
492 |
493 | }
494 |
495 | };
496 | SoundOnDemand.Sound = function(key, channel) {
497 |
498 | this.key = key;
499 | this.bufferKey = key;
500 |
501 | if (channel.engine.aliases[key]) {
502 |
503 | this.alias = channel.engine.aliases[key];
504 |
505 | this.bufferKey = this.alias.source;
506 |
507 | }
508 |
509 | if (!channel.engine.buffers[this.bufferKey]) channel.engine.load(this.bufferKey);
510 |
511 | this.channel = channel;
512 | this.audioContext = this.channel.engine.audioContext;
513 |
514 | this.current = {
515 | volume: 1.0,
516 | rate: 1.0
517 | };
518 |
519 | this.fadeMod = 1.0;
520 |
521 | this.createNodes();
522 |
523 | };
524 |
525 | SoundOnDemand.Sound.prototype = {
526 |
527 | constructor: SoundOnDemand.Sound,
528 |
529 | alias: {
530 | volume: 1.0,
531 | rate: 1.0
532 | },
533 |
534 | createNodes: function() {
535 |
536 | var bufferSource = this.audioContext.createBufferSource();
537 | var gainNode = this.audioContext.createGain();
538 | var panNode = this.audioContext.createStereoPanner();
539 |
540 | bufferSource.connect(panNode);
541 | panNode.connect(gainNode);
542 | gainNode.connect(this.channel.input);
543 |
544 | this.bufferSource = bufferSource;
545 | this.gainNode = gainNode;
546 | this.panNode = panNode;
547 |
548 | },
549 |
550 | volume: function(volume) {
551 |
552 | volume *= this.alias.volume;
553 |
554 | this.current.volume = volume;
555 |
556 | this.updateVolume();
557 |
558 | return this;
559 |
560 | },
561 |
562 | updateVolume: function() {
563 |
564 | this.gainNode.gain.value = this.current.volume * this.fadeMod;
565 |
566 | },
567 |
568 | pan: function(pan) {
569 |
570 | this.current.pan = pan;
571 |
572 | this.updatePanning();
573 |
574 | return this;
575 |
576 | },
577 |
578 | updatePanning: function() {
579 |
580 | this.panNode.pan.value = this.current.pan;
581 |
582 | },
583 |
584 | loop: function() {
585 |
586 | this.bufferSource.loop = true;
587 | this.current.loop = true;
588 |
589 | return this;
590 |
591 | },
592 |
593 | rrate: function(range) {
594 |
595 | return this.rate(this.current.rate + (-1 + Math.random() * 2) * range);
596 |
597 | },
598 |
599 | rate: function(rate) {
600 |
601 | rate *= this.alias.rate;
602 |
603 | this.bufferSource.playbackRate.value = rate;
604 |
605 | this.current.rate = rate;
606 |
607 | return this;
608 |
609 | },
610 |
611 | onended: function() {
612 |
613 | if (!this.current.loop) this.stop();
614 |
615 | },
616 |
617 | step: function(delta) {
618 |
619 | if (!this.ready) {
620 |
621 | if (!this.channel.engine.buffers[this.bufferKey]) return;
622 |
623 | this.ready = true;
624 | this.playing = true;
625 |
626 | this.buffer = this.channel.engine.buffers[this.bufferKey];
627 |
628 | this.bufferSource.buffer = this.buffer;
629 |
630 | this.bufferSource.start(0);
631 | this.bufferSource.onended = this.onended.bind(this);
632 |
633 | this.currentTime = 0;
634 |
635 | this.currentTime += this.bufferSource.playbackRate.value * delta;
636 | }
637 |
638 | if (this.fadeTarget !== this.fadeMod) {
639 |
640 | this.fadeMod = SoundOnDemand.moveTo(this.fadeMod, this.fadeTarget, delta * this.fadeSpeed);
641 |
642 | this.updateVolume();
643 |
644 | } else if (this.fadeTarget === 0) {
645 |
646 | this.pause();
647 |
648 | }
649 |
650 |
651 |
652 | },
653 |
654 | pause: function() {
655 |
656 | this.channel.remove(this);
657 |
658 | this.bufferSource.stop(0);
659 |
660 | this.playing = false;
661 |
662 | },
663 |
664 | stop: function() {
665 |
666 | this.channel.remove(this);
667 |
668 | this.bufferSource.stop(0);
669 |
670 | this.playing = false;
671 |
672 | },
673 |
674 | resume: function() {
675 |
676 | this.createNodes();
677 |
678 | this.bufferSource.buffer = this.buffer;
679 |
680 | this.currentTime = this.currentTime % this.buffer.duration;
681 | this.bufferSource.start(0, this.currentTime);
682 |
683 | this.rate(this.current.rate);
684 | this.volume(this.current.volume);
685 | this.loop(this.current.loop);
686 |
687 | this.channel.add(this);
688 |
689 | this.playing = true;
690 |
691 | },
692 |
693 | fadeTo: function(target, duration) {
694 |
695 | if (!this.playing && this.ready) this.resume();
696 |
697 | duration = duration || 1.0;
698 |
699 | this.fadeTime = 0;
700 | this.fadeTarget = target;
701 | this.fadeDuration = duration;
702 |
703 | this.fadeSpeed = Math.abs(target - this.fadeMod) / duration;
704 |
705 | return this;
706 |
707 | },
708 |
709 | fadeIn: function(duration) {
710 |
711 | if (!this.playing && this.ready) this.resume();
712 |
713 | this.fadeMod = 0;
714 | this.fadeTo(1.0, duration);
715 |
716 | return this;
717 |
718 | },
719 |
720 | fadeOut: function(duration) {
721 |
722 | this.fadeTo(0, duration || 1.0);
723 |
724 | return this;
725 |
726 | },
727 |
728 |
729 |
730 | };
731 |
732 | PLAYGROUND.SoundOnDemand = function(app) {
733 | app.audio = new SoundOnDemand({
734 | audioContext: app.audioContext
735 | });
736 |
737 | app.audio.path = app.getPath("sounds");
738 |
739 | app.loadSounds = function() {
740 |
741 | for (var i = 0; i < arguments.length; i++) {
742 |
743 | var key = arguments[i];
744 |
745 | this.loader.add();
746 |
747 | this.audio.load(key).then(
748 | this.loader.success.bind(this.loader),
749 | this.loader.error.bind(this.loader)
750 | );
751 |
752 | }
753 |
754 | };
755 |
756 | };
757 |
758 | PLAYGROUND.SoundOnDemand.plugin = true;
--------------------------------------------------------------------------------
/script/Powerup.js:
--------------------------------------------------------------------------------
1 | ENGINE.Powerup = function(args) {
2 |
3 | Utils.extend(this, args);
4 |
5 | this.radius = 32;
6 |
7 | this.direction = Math.random() * 6.28;
8 | this.speed = 32;
9 |
10 | this.forceDirection = Math.random() * 6.28;
11 | this.force = 64 + Math.random() * 128;
12 |
13 | this.force *= 3;
14 | this.forceDamping = this.force;
15 |
16 | this.lifetime = 0;
17 | this.duration = 10;
18 |
19 | var keys = ["repair", "missiles", "freeze"];
20 |
21 | var freelanersCount = Utils.filter(this.game.entities, function(e) {
22 | return e.free && e instanceof ENGINE.Ship;
23 | }).length;
24 |
25 | if (freelanersCount < 2) keys.push("freelancer");
26 |
27 | this.key = Utils.random(keys);
28 | this.sprite = this.sprites[this.key];
29 |
30 | };
31 |
32 | ENGINE.Powerup.prototype = {
33 |
34 | constructor: ENGINE.Powerup,
35 |
36 | sprite: [216, 159, 14, 14],
37 |
38 | type: "powerup",
39 |
40 | sprites: {
41 |
42 | "repair": [245, 89, 23, 25],
43 | "freelancer": [276, 51, 32, 32],
44 | "freeze": [242, 119, 19, 21],
45 | "missiles": [311, 13, 28, 32]
46 |
47 | },
48 |
49 | collect: function() {
50 |
51 | this.game.explosion(this.x, this.y, 16, "#fff");
52 |
53 | this.game.remove(this);
54 |
55 | app.sound.play("powerup");
56 |
57 | this.game.player.poke();
58 |
59 | this.game.add(ENGINE.TextOut, {
60 | x: this.x,
61 | y: this.y,
62 | text: this.key
63 | });
64 |
65 | switch (this.key) {
66 |
67 | case "freeze":
68 |
69 | this.game.freezeLifespan = 4.0;
70 |
71 | break;
72 |
73 | case "missiles":
74 |
75 | for (var i = 0; i < 4; i++) this.game.add(ENGINE.Missile, {
76 | x: this.x,
77 | y: this.y,
78 | team: 1
79 | });
80 |
81 | break;
82 |
83 | case "repair":
84 |
85 | this.game.repairShips();
86 |
87 | break;
88 |
89 |
90 | case "freelancer":
91 |
92 | var ship = this.game.add(ENGINE.Ship, {
93 | x: this.x,
94 | y: this.y,
95 | type: "freelancer",
96 | team: 1,
97 | free: true,
98 | planet: this.game.playerPlanet
99 | });
100 |
101 | ship.forceDirection = Math.random() * 6;
102 | ship.force = 200;
103 |
104 | break;
105 | }
106 |
107 | },
108 |
109 | step: function(dt) {
110 |
111 | this.lifetime += dt;
112 |
113 | var playerDistance = Utils.distance(this, this.game.player);
114 |
115 | if (this.force) {
116 |
117 | this.x += Math.cos(this.forceDirection) * this.force * dt;
118 | this.y += Math.sin(this.forceDirection) * this.force * dt;
119 |
120 | this.force = Math.max(0, this.force - this.forceDamping * dt);
121 |
122 | }
123 |
124 | if (this.lifetime > 0.5) {
125 | if (playerDistance < 32) {
126 | this.collect();
127 | this.game.player.resources++;
128 | }
129 | }
130 |
131 | if (this.lifetime > this.duration) this.game.remove(this);
132 |
133 | },
134 |
135 | render: function() {
136 |
137 | var linear = app.lifetime % 0.5 / 0.5;
138 | var scale = 0.8 + Math.sin(Math.PI * linear) * 0.4;
139 |
140 | app.ctx.save();
141 |
142 | app.ctx.translate(this.x, this.y);
143 |
144 | app.ctx.scale(scale, scale);
145 |
146 | app.ctx.drawImage(app.images.spritesheet,
147 | this.sprite[0], this.sprite[1], this.sprite[2], this.sprite[3], -this.sprite[2] / 2, -this.sprite[3] / 2, this.sprite[2], this.sprite[3]
148 | );
149 |
150 | app.ctx.restore();
151 |
152 | }
153 |
154 | };
--------------------------------------------------------------------------------
/script/Resource.js:
--------------------------------------------------------------------------------
1 | ENGINE.Resource = function(args) {
2 |
3 | Utils.extend(this, args);
4 |
5 | this.radius = 32;
6 |
7 | this.direction = Math.random() * 6.28;
8 | this.speed = 32;
9 |
10 | this.forceDirection = Math.random() * 6.28;
11 | this.force = 64 + Math.random() * 128;
12 |
13 | this.force *= 3;
14 | this.forceDamping = this.force;
15 |
16 | this.lifetime = 0;
17 | this.duration = 10;
18 |
19 | this.value = Math.random() * 3 | 0;
20 |
21 | this.sprite = this.sprites[this.value];
22 | };
23 |
24 | ENGINE.Resource.prototype = {
25 |
26 | constructor: ENGINE.Resource,
27 |
28 | quota: 0.7,
29 |
30 | sprites: [
31 | [333, 105, 10, 10],
32 | [320, 104, 12, 12],
33 | [303, 102, 16, 16]
34 | ],
35 |
36 | type: "resource",
37 |
38 |
39 | collect: function() {
40 |
41 | this.game.remove(this);
42 |
43 | if (!this.game.benchmark) app.sound.play("coin");
44 |
45 | this.game.player.poke();
46 |
47 | this.game.add(ENGINE.CircleExplosion, {
48 | color: "#fc0",
49 | radius: 8,
50 | attachedTo: this,
51 | duration: 0.25
52 | });
53 |
54 | this.game.player.resources += this.value;
55 |
56 | },
57 |
58 | step: function(dt) {
59 |
60 | this.lifetime += dt;
61 |
62 | var playerDistance = Utils.distance(this, this.game.player);
63 |
64 | if (this.force) {
65 |
66 | this.x += Math.cos(this.forceDirection) * this.force * dt;
67 | this.y += Math.sin(this.forceDirection) * this.force * dt;
68 |
69 | this.force = Math.max(0, this.force - this.forceDamping * dt);
70 |
71 | }
72 |
73 | if (this.poked && this.game.checkBonus("magnet")) {
74 |
75 | this.direction = Math.atan2(this.game.player.y - this.y, this.game.player.x - this.x);
76 |
77 | this.x += Math.cos(this.direction) * this.speed * dt;
78 | this.y += Math.sin(this.direction) * this.speed * dt;
79 |
80 |
81 | if (!this.force) {
82 | this.speed += 256 * dt;
83 | }
84 |
85 | } else {
86 |
87 | if (playerDistance < 100) {
88 | this.poked = true;
89 | this.speed = 128;
90 | }
91 |
92 | }
93 |
94 |
95 | if (this.lifetime > 0.5) {
96 | if (playerDistance < 32) {
97 | this.collect();
98 | }
99 | }
100 |
101 | if (this.lifetime > this.duration) this.game.remove(this);
102 |
103 | },
104 |
105 | render: function() {
106 |
107 | var scale = 0.2 + 0.8 * Math.sin(Math.PI * (app.lifetime % 0.2 / 0.2));
108 |
109 | app.ctx.save();
110 |
111 | app.ctx.translate(this.x, this.y);
112 |
113 | app.ctx.scale(scale, 1.0);
114 |
115 | app.ctx.drawImage(app.images.spritesheet,
116 | this.sprite[0], this.sprite[1], this.sprite[2], this.sprite[3], -this.sprite[2] / 2, -this.sprite[3] / 2, this.sprite[2], this.sprite[3]
117 | );
118 |
119 | app.ctx.restore();
120 |
121 | }
122 |
123 | };
--------------------------------------------------------------------------------
/script/Ship.js:
--------------------------------------------------------------------------------
1 | ENGINE.Ship = function(args) {
2 |
3 | Utils.extend(this, {
4 |
5 | damage: 1,
6 | firerate: 0.5,
7 | speed: 160,
8 | radius: 16,
9 | rotationSpeed: 5,
10 | hp: 10,
11 | range: 200,
12 | force: 0,
13 | forceDirection: 0,
14 | targetTimeout: 0,
15 | hitLifespan: 0,
16 | scale: 1.0,
17 | rank: 0,
18 | kills: 0
19 |
20 | }, defs.ships[args.type], args);
21 |
22 | this.random = this.game.random();
23 |
24 | this.maxHp = this.hp;
25 |
26 | this.lifetime = this.game.random() * 10;
27 | this.cooldown = this.firerate;
28 | this.desiredDirection = this.direction = this.game.random() * 6;
29 |
30 | this.color = defs.teamColor[this.team];
31 |
32 | this.image = app.images.spritesheet;
33 |
34 | if (this.team) this.applyUpgrades(this.game.upgrades);
35 | else this.applyDifficulty();
36 |
37 | };
38 |
39 | ENGINE.Ship.prototype = {
40 |
41 | constructor: ENGINE.Ship,
42 |
43 | hoverable: true,
44 |
45 | frozenSprite: [193, 86, 11, 19],
46 |
47 | quota: 2,
48 |
49 | pointerenter: function() {
50 |
51 | this.repair();
52 |
53 | },
54 |
55 | ranks: [
56 | [318, 131, 10, 5],
57 | [333, 131, 10, 10],
58 | [348, 131, 10, 15],
59 | [360, 131, 10, 8],
60 | [372, 131, 10, 13],
61 | [384, 131, 10, 18],
62 | [396, 131, 15, 16]
63 | ],
64 |
65 | applyDifficulty: function() {
66 |
67 | var difficulty = this.game.wave / 30;
68 |
69 | this.speed *= 1 + difficulty;
70 | this.damage *= 1 + difficulty;
71 |
72 | },
73 |
74 | applyUpgrades: function(upgrades) {
75 |
76 | var hpmod = this.hp / this.maxHp;
77 |
78 | this.damage = 1 + upgrades.damage * 0.25;
79 | this.maxHp = upgrades.life * 10;
80 | this.hp = hpmod * this.maxHp;
81 | this.speed = 80 + 10 * upgrades.speed;
82 |
83 |
84 | if (this.free) {
85 | this.damage *= 2;
86 | this.maxHp *= 2;
87 | this.hp *= 2;
88 | }
89 |
90 | },
91 |
92 | die: function() {
93 |
94 | if (!this.team) this.game.score++;
95 |
96 | if (this.game.benchmark) {
97 |
98 | this.hp = this.maxHp;
99 |
100 | } else {
101 |
102 | this.dead = true;
103 |
104 | }
105 |
106 | if (this.boss) {
107 |
108 | this.game.shake();
109 |
110 | for (var i = 0; i < 16; i++) {
111 |
112 | this.game.add(ENGINE.Resource, {
113 | x: this.x,
114 | y: this.y
115 | });
116 |
117 | }
118 |
119 | }
120 |
121 | this.game.explosion(this.x, this.y, 16, this.color);
122 |
123 | this.game.add(ENGINE.Resource, {
124 | x: this.x,
125 | y: this.y,
126 | parent: this
127 | });
128 |
129 | if (this.planet) this.planet.ships--;
130 |
131 | if (!this.team) this.game.onenemydeath(this);
132 |
133 | app.sound.play("explosion").rrate(0.2);
134 |
135 | },
136 |
137 | applyDamage: function(damage, attacker) {
138 |
139 | if (this.dead) return;
140 |
141 | this.hitLifespan = 0.1;
142 |
143 | this.hp -= damage;
144 |
145 | if (this.hp <= 0) {
146 | this.die();
147 | if (attacker) attacker.onscore();
148 | }
149 |
150 | this.game.explosion(this.x, this.y, 3, this.color);
151 |
152 |
153 | },
154 |
155 | step: function(dt) {
156 |
157 | dt *= this.game.timeFactor;
158 |
159 | // if (!this.team) dt *= Math.sin((app.lifetime % 2 / 2) * Math.PI);
160 |
161 | this.lifetime += dt;
162 |
163 | if ((this.targetTimeout -= dt) <= 0) {
164 |
165 | this.target = false;
166 | this.targetTimeout = 0.25;
167 |
168 | }
169 |
170 | if (!this.target) {
171 |
172 | this.target = this.getTarget(this.game.entities);
173 |
174 | } else if (this.target.dead) {
175 |
176 | this.target = null;
177 |
178 | }
179 |
180 |
181 | this.foresightCollision();
182 |
183 | var destination = false;
184 | var speed = this.speed;
185 |
186 | var ox = 0;
187 | var oy = 0;
188 |
189 | if (this.team && this.target) {
190 |
191 | ox = Math.cos(this.random * 6.28) * 100;
192 | oy = Math.sin(this.random * 6.28) * 100;
193 |
194 | destination = this.target;
195 |
196 | } else destination = this.game.player.planet;
197 |
198 | if (this.team && Utils.distance(this, app.center) > app.center.y) {
199 |
200 | destination = app.center;
201 |
202 | }
203 |
204 | if (this.collisionDanger) {
205 |
206 | /*
207 |
208 | var angle = Math.atan2(this.collisionDanger.y - this.y, this.collisionDanger.x - this.x) - Math.PI / 2;
209 |
210 | destination = {
211 | x: this.collisionDanger.x + Math.cos(angle) * 150,
212 | y: this.collisionDanger.y + Math.cos(angle) * 150
213 | }
214 |
215 | speed *= 1 - 0.5 * Math.abs(Utils.circDistance(this.direction, angle) / (Math.PI));
216 |
217 | */
218 |
219 | if (this.collisionDistance < 50) {
220 |
221 | var angle = Math.atan2(this.collisionDanger.y - this.y, this.collisionDanger.x - this.x) - Math.PI;
222 |
223 | this.x = this.collisionDanger.x + Math.cos(angle) * 50;
224 | this.y = this.collisionDanger.y + Math.sin(angle) * 50;
225 |
226 | }
227 |
228 | // speed *= this.collisionDistance / 200;
229 |
230 | }
231 |
232 |
233 | if (destination) {
234 |
235 | this.desiredDirection = Math.atan2(destination.y - this.y + ox, destination.x - this.x + oy);
236 |
237 | }
238 |
239 | if (!this.frozen) {
240 |
241 | this.direction = Utils.circWrapTo(this.direction, this.desiredDirection, dt * this.rotationSpeed);
242 |
243 | }
244 |
245 | this.move(dt);
246 |
247 | /* firing mechanics */
248 |
249 | this.cooldown -= dt;
250 |
251 | if (this.canFire()) {
252 |
253 | this.fire();
254 |
255 | }
256 |
257 | if (!this.team && Utils.distance(this, this.game.playerPlanet) < this.game.playerPlanet.radius) {
258 |
259 | if (!this.game.benchmark) {
260 |
261 | this.game.player.planet.applyDamage(1, this);
262 | this.die();
263 |
264 | }
265 |
266 | }
267 |
268 | this.hitLifespan -= dt;
269 |
270 | },
271 |
272 |
273 | move: function(dt) {
274 |
275 | if (!this.frozen) {
276 |
277 | Utils.moveInDirection.call(this, this.direction, this.speed * dt);
278 |
279 | }
280 |
281 | if (this.force > 0) {
282 |
283 | this.force -= 200 * dt;
284 |
285 | Utils.moveInDirection.call(this, this.forceDirection, this.force * dt);
286 |
287 | }
288 |
289 | },
290 |
291 | canFire: function() {
292 |
293 | if (this.frozen) return false;
294 |
295 | if (this.cooldown > 0) return;
296 | if (!this.target) return;
297 | if (Utils.distance(this, this.target) > this.range) return;
298 |
299 | this.cooldown = this.firerate;
300 |
301 | this.fire();
302 |
303 | },
304 |
305 | fire: function() {
306 |
307 | this.game.add(ENGINE.Bullet, {
308 | x: this.x,
309 | y: this.y,
310 | team: this.team,
311 | target: this.target,
312 | damage: this.damage,
313 | parent: this
314 | });
315 |
316 | if (!this.game.benchmark) app.sound.play("laser");
317 |
318 | },
319 |
320 | render: function() {
321 |
322 | /* sprite */
323 |
324 | app.ctx.save();
325 | app.ctx.translate(this.x, this.y);
326 |
327 | this.renderHUD();
328 |
329 | if (this.hitLifespan > 0) {
330 |
331 | var image = app.getColoredImage(this.image, "#fff", "source-in");
332 |
333 | } else {
334 |
335 | var image = this.image;
336 |
337 | }
338 |
339 | app.ctx.rotate(this.direction - Math.PI / 2);
340 | app.ctx.scale(this.scale, this.scale);
341 | app.ctx.drawImage(image, this.sprite[0], this.sprite[1], this.sprite[2], this.sprite[3], -this.sprite[2] / 2, -this.sprite[3] / 2, this.sprite[2], this.sprite[3]);
342 | app.ctx.restore();
343 |
344 | if (this.frozen) {
345 |
346 | app.ctx.drawImage(app.images.spritesheet,
347 | this.frozenSprite[0], this.frozenSprite[1], this.frozenSprite[2], this.frozenSprite[3],
348 | this.x - this.frozenSprite[2] / 2, this.y - this.frozenSprite[3] / 2, this.frozenSprite[2], this.frozenSprite[3]);
349 |
350 | }
351 |
352 | if (this.team) {
353 |
354 | var rankSprite = this.ranks[this.rank];
355 |
356 | app.ctx.drawImage(app.images.spritesheet,
357 | rankSprite[0], rankSprite[1], rankSprite[2], rankSprite[3],
358 | this.x + 24, this.y - 24, rankSprite[2], rankSprite[3]);
359 |
360 |
361 | }
362 |
363 | },
364 |
365 | renderHUD: function() {
366 |
367 | if (this.frozen) return;
368 |
369 | var w = Math.min(100, (this.maxHp / 160) * 100 | 0);
370 |
371 | var mod = this.hp / this.maxHp;
372 |
373 | app.ctx.fillStyle = this.color;
374 | app.ctx.strokeStyle = this.color;
375 | app.ctx.lineWidth = 2;
376 | app.ctx.fillRect(-w * mod / 2 | 0, 32, w * mod, 5);
377 | app.ctx.strokeRect(-w * 0.5 | 0, 32, w, 5);
378 |
379 | },
380 |
381 | collisionRange: 100,
382 |
383 | foresightCollision: function() {
384 |
385 | this.collisionDanger = false;
386 |
387 | var self = this;
388 |
389 | var pool = Utils.filter(this.game.entities, function(e) {
390 |
391 | if (e.type !== "asteroid") return false;
392 |
393 | if (Utils.distance(self, e) > self.collisionRange) return false;
394 |
395 | return true;
396 |
397 | });
398 |
399 | this.collisionDanger = Utils.nearest(this, pool);
400 |
401 | if (this.collisionDanger) this.collisionDistance = Utils.distance(this, this.collisionDanger);
402 |
403 | },
404 |
405 | getTarget: function() {
406 |
407 | var pool = [];
408 |
409 | for (var i = 0; i < this.game.entities.length; i++) {
410 |
411 | var entity = this.game.entities[i];
412 |
413 | if (!(entity instanceof ENGINE.Ship)) continue;
414 |
415 | if (entity.team !== this.team) pool.push(entity);
416 |
417 | }
418 |
419 | return Utils.nearest(this, pool);
420 |
421 | },
422 |
423 | repair: function() {
424 |
425 | if (this.hp >= this.maxHp) return;
426 |
427 | this.game.add(ENGINE.CircleExplosion, {
428 | color: "#a04",
429 | radius: 32,
430 | attachedTo: this
431 | });
432 |
433 | this.hp = this.maxHp;
434 |
435 | },
436 |
437 | onscore: function() {
438 |
439 | this.kills++;
440 |
441 | this.rank = Math.min(this.ranks.length - 1, this.kills / 3 | 0);
442 |
443 | }
444 |
445 | };
--------------------------------------------------------------------------------
/script/TextOut.js:
--------------------------------------------------------------------------------
1 | ENGINE.TextOut = function(args) {
2 |
3 | Utils.extend(this, {
4 | background: "rgba(0,0,0,0.5)",
5 | color: "#fff",
6 | fontSize: 24,
7 | scaleX: 0,
8 | scaleY: 1.0,
9 | text: "void",
10 | duration: 2.0
11 | }, args);
12 |
13 | var textout = this;
14 |
15 | app.tween(this)
16 | .to({
17 | scaleX: 1.0
18 | }, this.duration * 0.25, "outElastic")
19 | .wait(this.duration * 0.5)
20 | .to({
21 | scaleY: 0.0
22 | }, this.duration * 0.25, "outCirc")
23 | .on("finish", function() {
24 | textout.game.remove(textout);
25 | });
26 |
27 | ttt = this;
28 |
29 | };
30 |
31 | ENGINE.TextOut.prototype = {
32 |
33 | constructor: ENGINE.TextOut,
34 |
35 | sprite: [216, 159, 14, 14],
36 |
37 | type: "textout",
38 |
39 | step: function(dt) {
40 |
41 | },
42 |
43 | render: function() {
44 |
45 | if (!this.scaleX || !this.scaleY) return;
46 |
47 | app.ctx.save();
48 |
49 | app.ctx.translate(this.x, this.y);
50 |
51 | app.fontSize(this.fontSize);
52 |
53 | app.ctx.fillStyle = this.color;
54 | app.ctx.textAlign = "center";
55 |
56 | app.ctx.scale(this.scaleX, this.scaleY);
57 | app.ctx.fillText(this.text, 0, 0)
58 |
59 | app.ctx.restore();
60 |
61 | }
62 |
63 | };
--------------------------------------------------------------------------------
/script/Trail.js:
--------------------------------------------------------------------------------
1 | ENGINE.Trail = function(parent, args) {
2 |
3 | this.parent = parent;
4 |
5 | Utils.extend(this, {
6 | color: "#0fc",
7 | points: [],
8 | maxPoints: 5,
9 | width: 10,
10 | lifetime: 0,
11 | lifespan: 0,
12 | paused: false,
13 | interval: 0.15,
14 | stroke: true
15 | }, args);
16 |
17 | };
18 |
19 | ENGINE.Trail.prototype = {
20 |
21 | zIndex: 200,
22 |
23 | quota: 0.3,
24 |
25 | reaction: 8,
26 |
27 | clear: function() {
28 |
29 | this.points = [];
30 |
31 | },
32 |
33 | step: function(delta) {
34 |
35 | this.lifetime += delta;
36 |
37 | if (Utils.interval("point", this.interval, this)) {
38 |
39 | if (!this.paused) this.points.push(this.parent.x, this.parent.y);
40 |
41 | if (
42 | (this.points.length > this.maxPoints * 2) ||
43 | (this.paused && this.points.length > 0)
44 | ) {
45 | this.points.shift();
46 | this.points.shift();
47 | }
48 | }
49 |
50 | this.points[this.points.length - 2] = this.parent.x;
51 | this.points[this.points.length - 1] = this.parent.y;
52 |
53 | if(this.lifespan && this.lifetime > this.lifespan) {
54 | this.paused = true;
55 | }
56 |
57 | },
58 |
59 | render: function() {
60 |
61 | if(this.points.length <= 0) return;
62 |
63 | app.layer.save();
64 | app.layer.strokeStyle(this.color);
65 | app.layer.lineCap("square");
66 |
67 | // if (!this.stroke) app.layer.strokeStyle("rgba(0,0,0,0.1)");
68 |
69 | for (var i = 2; i < this.points.length; i += 2) {
70 |
71 | var ratio = i / (2 * this.maxPoints);
72 | var px = this.points[i - 2];
73 | var py = this.points[i - 1];
74 | var nx = this.points[i];
75 | var ny = this.points[i + 1];
76 | app.layer.beginPath();
77 | app.layer.moveTo(px | 0, py | 0);
78 | app.layer.lineTo(nx | 0, ny | 0);
79 | app.layer.a(ratio).lineWidth(ratio * this.width);
80 | app.layer.stroke();
81 | }
82 |
83 | app.layer.restore();
84 |
85 |
86 | }
87 |
88 | };
--------------------------------------------------------------------------------
/script/Utils.js:
--------------------------------------------------------------------------------
1 | var Utils = {
2 |
3 | extend: function() {
4 | for (var i = 1; i < arguments.length; i++) {
5 | for (var j in arguments[i]) {
6 | arguments[0][j] = arguments[i][j];
7 | }
8 | }
9 |
10 | return arguments[0];
11 | },
12 |
13 | distance: function(a, b) {
14 |
15 | var dx = a.x - b.x;
16 | var dy = a.y - b.y;
17 |
18 | return Math.sqrt(dx * dx + dy * dy);
19 |
20 | },
21 |
22 | nearest: function(from, entities) {
23 |
24 | var min = -1;
25 | var result = null;
26 |
27 | for (var i = 0; i < entities.length; i++) {
28 |
29 | var to = entities[i];
30 |
31 | if (from === to) continue;
32 |
33 | var distance = this.distance(from, to);
34 |
35 | if (distance < min || min < 0) {
36 | min = distance;
37 | result = to;
38 | }
39 |
40 | }
41 |
42 | return result;
43 | },
44 |
45 | circWrap: function(val) {
46 |
47 | return this.wrap(val, 0, Math.PI * 2);
48 |
49 | },
50 |
51 | wrap: function(value, min, max) {
52 |
53 | if (value < min) return max + (value % max);
54 | if (value >= max) return value % max;
55 | return value;
56 |
57 | },
58 |
59 | wrapTo: function(value, target, max, step) {
60 |
61 | if (value === target) return target;
62 |
63 | var result = value;
64 |
65 | var d = this.wrappedDistance(value, target, max);
66 |
67 | if (Math.abs(d) < step) return target;
68 |
69 | result += (d < 0 ? -1 : 1) * step;
70 |
71 | if (result > max) {
72 | result = result - max;
73 | } else if (result < 0) {
74 | result = max + result;
75 | }
76 |
77 | return result;
78 |
79 | },
80 |
81 | circWrapTo: function(value, target, step) {
82 |
83 | return this.wrapTo(value, target, Math.PI * 2, step);
84 |
85 | },
86 |
87 | circDistance: function(a, b) {
88 |
89 | return this.wrappedDistance(a, b, Math.PI * 2);
90 |
91 | },
92 |
93 | wrappedDistance: function(a, b, max) {
94 |
95 | if (a === b) return 0;
96 | else if (a < b) {
97 | var l = -a - max + b;
98 | var r = b - a;
99 | } else {
100 | var l = b - a;
101 | var r = max - a + b;
102 | }
103 |
104 | if (Math.abs(l) > Math.abs(r)) return r;
105 | else return l;
106 |
107 | },
108 |
109 | random: function(a, b) {
110 |
111 | if (a === undefined) {
112 |
113 | return Math.random();
114 |
115 | } else if (b !== undefined) {
116 |
117 | return Math.floor(a + Math.random() * Math.abs(b - a + 1));
118 |
119 | } else {
120 |
121 | if (a instanceof Array) return a[(a.length + 1) * Math.random() - 1 | 0];
122 | else {
123 | return a[this.random(Object.keys(a))];
124 | }
125 |
126 | }
127 |
128 | },
129 |
130 | sincos: function(angle, radius) {
131 |
132 | if (arguments.length === 1) {
133 | radius = angle;
134 | angle = Math.random() * 6.28;
135 | }
136 |
137 | return {
138 | x: Math.cos(angle) * radius,
139 | y: Math.sin(angle) * radius
140 | };
141 | },
142 |
143 | ground: function(num, threshold) {
144 |
145 | return (num / threshold | 0) * threshold;
146 |
147 | },
148 |
149 | shuffle: function(o) {
150 | for (var j, x, i = o.length; i; j = Math.floor(Math.random() * i), x = o[--i], o[i] = o[j], o[j] = x);
151 | return o;
152 | },
153 |
154 | sign: function(value) {
155 |
156 | return value / Math.abs(value);
157 |
158 | },
159 |
160 | moveTo: function(value, target, step) {
161 |
162 | if (value < target) {
163 | value += step;
164 | if (value > target) value = target;
165 | }
166 |
167 | if (value > target) {
168 | value -= step;
169 | if (value < target) value = target;
170 | }
171 |
172 | return value;
173 |
174 | },
175 |
176 | interval: function(key, interval, object) {
177 |
178 | if (!object.throttles) object.throttles = {};
179 | if (!object.throttles[key]) object.throttles[key] = object.lifetime - interval;
180 |
181 | if (object.lifetime - object.throttles[key] >= interval) {
182 | object.throttles[key] = object.lifetime;
183 | return true;
184 | } else return false;
185 |
186 | },
187 |
188 | moveInDirection: function(direction, value) {
189 |
190 | this.x += Math.cos(direction) * value;
191 | this.y += Math.sin(direction) * value;
192 |
193 | },
194 |
195 | osc: function(time, period) {
196 |
197 | return Math.sin(Math.PI * (time % period / period));
198 |
199 | },
200 |
201 | filter: function(array, test) {
202 |
203 | var result = [];
204 |
205 | for (var i = 0; i < array.length; i++) {
206 | if (test(array[i])) result.push(array[i]);
207 | }
208 |
209 | return result;
210 |
211 | },
212 |
213 | rectInRect: function(r1x, r1y, r1w, r1h, r2x, r2y, r2w, r2h) {
214 | return !(r2x > r1x + r1w ||
215 | r2x + r2w < r1x ||
216 | r2y > r1y + r1h ||
217 | r2y + r2h < r1y);
218 | }
219 |
220 |
221 |
222 | };
--------------------------------------------------------------------------------
/script/bottlenecks.js:
--------------------------------------------------------------------------------
1 | /**
2 | * This is bad and unoptimized code just for you to fix :)
3 | *
4 | * Get Firefox Developer Edition to try the new Performance Tools:
5 | * https://www.mozilla.org/firefox/developer/
6 | *
7 | * 1. Open the `Performance` tool in Firefox Developer Edition
8 | * 2. Start recording a performance profile
9 | * 3. Play the game
10 | * 4. Stop profiling and check the Call Tree or Flame Chart for the maleficent
11 | *
12 | * Got ideas for better bottlenecks or even faster code, file
13 | * an issue or send us a pull request:
14 | * https://github.com/mozilla/devtools-perf-game/issues
15 | */
16 |
17 | /**
18 | * Creates a new array with all elements that pass the `test` function
19 | * @param {Array} array The array to filter
20 | * @param {Function} test Function to test each element, invoked with (element)
21 | * @return {Array} A new array with only passed elements
22 | */
23 | Utils.filter = function(array, test) {
24 | var result = array.slice(); // Clone array
25 | for (var i = 0; i < result.length; i++) {
26 | if (!test(result[i])) {
27 | result.splice(i, 1); // Remove element
28 | i--;
29 | }
30 | }
31 | return result;
32 | };
33 |
34 | /**
35 | * Find nearest entity from a list of entities
36 | * @param {Entity} from Entity
37 | * @param {Entity[]} entities List of entities to compare
38 | * @return {Entity} Nearest Entity
39 | */
40 | Utils.nearest = function(from, entities) {
41 | var distances = [];
42 | for (var i = 0; i < entities.length; i++) {
43 | var to = entities[i];
44 | if (from === to) continue;
45 | var distance = this.distance(from, to);
46 | distances.push({
47 | target: to,
48 | distance: distance
49 | });
50 | }
51 | if (!distances.length) {
52 | return null;
53 | }
54 | var sortedDistances = distances.sort(
55 | function sortDistances(a, b) {
56 | return a.distance - b.distance;
57 | }
58 | );
59 | return sortedDistances[0].target;
60 | };
61 |
62 | /**
63 | * Returns nearest ship of opposite team
64 | * @return {Ship} Nearest enemy ship
65 | */
66 | ENGINE.Ship.prototype.getTarget = function() {
67 | var pool = [];
68 | for (var i = 0; i < this.game.entities.length; i++) {
69 | var entity = this.game.entities[i];
70 | if (!(entity instanceof ENGINE.Ship)) continue;
71 | if (entity.team !== this.team) pool.push(entity);
72 | }
73 | // Is Utils.nearest fast enough?
74 | return Utils.nearest(this, pool);
75 | };
76 |
77 | // We update those for positions, maybe we don't need it?
78 | var axes = {
79 | x: Math.cos,
80 | y: Math.sin
81 | };
82 |
83 | /**
84 | * Update position for an entity that has speed and direction.
85 | * @param {Number} direction Angle given in radians
86 | * @param {Number} value Distance to move
87 | */
88 | Utils.moveInDirection = function(direction, value) {
89 | Utils.justAnExpensiveLoop();
90 | value /= 100;
91 | for (var i = 0; i < 100; i++) {
92 | for (var axis in axes) {
93 | this[axis] += axes[axis](this.direction) * value;
94 | }
95 | }
96 | };
97 |
98 | /**
99 | * I am really just an expensive loop ;)
100 | * Remove me and all references calling me!
101 | */
102 | Utils.justAnExpensiveLoop = function() {
103 | // This isn't even doing anything
104 | var oops = Array(1000);
105 | oops.map(function(val, i) {
106 | return Math.PI / 2500 * i;
107 | }).filter(function(rad) {
108 | return Math.sin(rad) > 0;
109 | });
110 | }
111 |
112 | /**
113 | * Update ship position with current direction and speed
114 | * @param {Number} dt Time delta for current frame in seconds
115 | */
116 | ENGINE.Ship.prototype.move = function(dt) {
117 | if (!this.frozen) {
118 | Utils.moveInDirection.apply(this, [this.direction, this.speed * dt]);
119 | }
120 |
121 | if (this.force > 0) {
122 | this.force -= 200 * dt;
123 | Utils.moveInDirection.apply(this, [this.forceDirection, this.force * dt]);
124 | }
125 | };
126 |
127 | /**
128 | * Frame step for a particle
129 | * @param {Number} dt Time delta for current frame in seconds
130 | */
131 | ENGINE.Particle.prototype.step = function(dt) {
132 | this.lifetime += dt;
133 | // Update position
134 | for (var axis in axes) {
135 | this[axis] += axes[axis](this.direction) * this.speed * dt;
136 | }
137 | this.speed = Math.max(0, this.speed - this.damping * dt);
138 |
139 | this.progress = Math.min(this.lifetime / this.duration, 1.0);
140 | // Put particle offscreen for pooling and to keep render time constant
141 | if (this.progress >= 1.0) {
142 | this.x = 0;
143 | this.y = 0;
144 | this.progress = 0;
145 | }
146 | // Update index for current sprite to render
147 | this.spriteIndex = Math.floor(this.progress * this.sprites.length);
148 | }
149 |
150 | /**
151 | * Check if star is in screen boundaries.
152 | * Otherwise wrap it to the opposite side of screen.
153 | * @param {Star} star Probed star
154 | */
155 | ENGINE.BackgroundStars.prototype.wrap = function(star) {
156 | var pos = [star.x, star.y, 1, 1];
157 | var bounds = [0, 0, app.width, app.height];
158 |
159 | if (pos[0] < bounds[0]) star.x = app.width;
160 | if (pos[1] < bounds[1]) star.y = app.height;
161 |
162 | if (pos[0] > bounds[2]) star.x = 0;
163 | if (pos[1] > bounds[3]) star.y = 0;
164 | };
165 |
166 |
--------------------------------------------------------------------------------
/script/data.js:
--------------------------------------------------------------------------------
1 | var defs = {
2 |
3 | teamColor: ["#ff4444", "#00aaff"],
4 |
5 | frozenSprite: [193, 86, 11, 19],
6 |
7 | buttons: {
8 | "fighter": [4, 345, 64, 64],
9 | "speed": [132, 345, 64, 64],
10 | "life": [68, 345, 64, 64],
11 | "damage": [196, 345, 64, 64]
12 | },
13 |
14 | ships: {
15 |
16 | "fighter": {
17 |
18 | preference: ["small"],
19 | cooldown: 0.5,
20 | damage: 1,
21 | hp: 10,
22 | sprite: [407, 18, 32, 32],
23 | price: 1,
24 | speed: 80
25 |
26 | },
27 |
28 | "freelancer": {
29 |
30 | cooldown: 0.5,
31 | damage: 1,
32 | hp: 10,
33 | sprite: [367, 59, 31, 32],
34 | speed: 80
35 |
36 | },
37 |
38 |
39 | "creep1": {
40 |
41 | preference: ["big"],
42 | damage: 2,
43 | cooldown: 2,
44 | hp: 4,
45 | sprite: [444, 23, 22, 21],
46 | price: 5,
47 | speed: 60
48 |
49 | },
50 |
51 | "creep2": {
52 |
53 | preference: ["big"],
54 | damage: 2,
55 | cooldown: 2,
56 | hp: 10,
57 | sprite: [471, 23, 32, 23],
58 | price: 5,
59 | speed: 80
60 |
61 | },
62 |
63 | "creep3": {
64 |
65 | preference: ["big"],
66 | damage: 4,
67 | cooldown: 2,
68 | hp: 30,
69 | sprite: [503, 19, 32, 29],
70 | price: 5,
71 | speed: 50
72 |
73 | },
74 |
75 | "creep4": {
76 |
77 | preference: ["big"],
78 | damage: 6,
79 | cooldown: 2,
80 | hp: 50,
81 | sprite: [535, 18, 32, 32],
82 | price: 5,
83 | speed: 50
84 |
85 | },
86 |
87 | "boss": {
88 |
89 | damage: 10,
90 | cooldown: 2,
91 | hp: 500,
92 | sprite: [456, 53, 64, 64],
93 | speed: 32,
94 | boss: true
95 |
96 | }
97 |
98 | },
99 |
100 | tooltips: {
101 |
102 | "fighter": "build a fighter",
103 | "speed": "upgrade fighters speed",
104 | "life": "upgrade fighters life",
105 | "damage": "upgrade fighters damage"
106 |
107 | },
108 |
109 | bonuses: {
110 | shield: "asteroids shield",
111 | laser: "cursor laser",
112 | magnet: "coin magnet"
113 | },
114 |
115 |
116 | downloadLinks: {
117 |
118 | "ffdev": ["Learn more about Performance Tools in Developer Edition", "https://hacks.mozilla.org/?utm_source=codepen&utm_medium=referral&utm_campaign=firefox-developer-game&utm_content=learn-perf-tools"],
119 | "default": ["Get Firefox Developer Edition to try out the new performance tools", "https://www.mozilla.org/firefox/developer/?utm_source=codepen&utm_medium=referral&utm_campaign=firefox-developer-game&utm_content=game-promo"]
120 |
121 | }
122 |
123 | };
--------------------------------------------------------------------------------
/script/stats.js:
--------------------------------------------------------------------------------
1 | (function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){
2 | (i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new
3 | Date();a=s.createElement(o),
4 |
5 | m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)
6 | })(window,document,'script','//www.google-analytics.com/analytics.js','ga');
7 |
8 | ga('create', 'UA-49796218-26', 'auto');
9 | ga('send', 'pageview');
--------------------------------------------------------------------------------
/sounds/ascendancy.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mozilla/devtools-perf-game/57f20b885b23cde2d55cd220d5f33704719065df/sounds/ascendancy.mp3
--------------------------------------------------------------------------------
/sounds/ascendancy.ogg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mozilla/devtools-perf-game/57f20b885b23cde2d55cd220d5f33704719065df/sounds/ascendancy.ogg
--------------------------------------------------------------------------------
/sounds/build.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mozilla/devtools-perf-game/57f20b885b23cde2d55cd220d5f33704719065df/sounds/build.mp3
--------------------------------------------------------------------------------
/sounds/build.ogg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mozilla/devtools-perf-game/57f20b885b23cde2d55cd220d5f33704719065df/sounds/build.ogg
--------------------------------------------------------------------------------
/sounds/coin.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mozilla/devtools-perf-game/57f20b885b23cde2d55cd220d5f33704719065df/sounds/coin.mp3
--------------------------------------------------------------------------------
/sounds/coin.ogg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mozilla/devtools-perf-game/57f20b885b23cde2d55cd220d5f33704719065df/sounds/coin.ogg
--------------------------------------------------------------------------------
/sounds/dig.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mozilla/devtools-perf-game/57f20b885b23cde2d55cd220d5f33704719065df/sounds/dig.mp3
--------------------------------------------------------------------------------
/sounds/dig.ogg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mozilla/devtools-perf-game/57f20b885b23cde2d55cd220d5f33704719065df/sounds/dig.ogg
--------------------------------------------------------------------------------
/sounds/digEnd.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mozilla/devtools-perf-game/57f20b885b23cde2d55cd220d5f33704719065df/sounds/digEnd.mp3
--------------------------------------------------------------------------------
/sounds/digEnd.ogg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mozilla/devtools-perf-game/57f20b885b23cde2d55cd220d5f33704719065df/sounds/digEnd.ogg
--------------------------------------------------------------------------------
/sounds/dust.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mozilla/devtools-perf-game/57f20b885b23cde2d55cd220d5f33704719065df/sounds/dust.mp3
--------------------------------------------------------------------------------
/sounds/dust.ogg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mozilla/devtools-perf-game/57f20b885b23cde2d55cd220d5f33704719065df/sounds/dust.ogg
--------------------------------------------------------------------------------
/sounds/explosion.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mozilla/devtools-perf-game/57f20b885b23cde2d55cd220d5f33704719065df/sounds/explosion.mp3
--------------------------------------------------------------------------------
/sounds/explosion.ogg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mozilla/devtools-perf-game/57f20b885b23cde2d55cd220d5f33704719065df/sounds/explosion.ogg
--------------------------------------------------------------------------------
/sounds/gameover.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mozilla/devtools-perf-game/57f20b885b23cde2d55cd220d5f33704719065df/sounds/gameover.mp3
--------------------------------------------------------------------------------
/sounds/gameover.ogg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mozilla/devtools-perf-game/57f20b885b23cde2d55cd220d5f33704719065df/sounds/gameover.ogg
--------------------------------------------------------------------------------
/sounds/laser.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mozilla/devtools-perf-game/57f20b885b23cde2d55cd220d5f33704719065df/sounds/laser.mp3
--------------------------------------------------------------------------------
/sounds/laser.ogg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mozilla/devtools-perf-game/57f20b885b23cde2d55cd220d5f33704719065df/sounds/laser.ogg
--------------------------------------------------------------------------------
/sounds/planetHit.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mozilla/devtools-perf-game/57f20b885b23cde2d55cd220d5f33704719065df/sounds/planetHit.mp3
--------------------------------------------------------------------------------
/sounds/planetHit.ogg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mozilla/devtools-perf-game/57f20b885b23cde2d55cd220d5f33704719065df/sounds/planetHit.ogg
--------------------------------------------------------------------------------
/sounds/powerup.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mozilla/devtools-perf-game/57f20b885b23cde2d55cd220d5f33704719065df/sounds/powerup.mp3
--------------------------------------------------------------------------------
/sounds/powerup.ogg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mozilla/devtools-perf-game/57f20b885b23cde2d55cd220d5f33704719065df/sounds/powerup.ogg
--------------------------------------------------------------------------------
/sounds/upgrade.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mozilla/devtools-perf-game/57f20b885b23cde2d55cd220d5f33704719065df/sounds/upgrade.mp3
--------------------------------------------------------------------------------
/sounds/upgrade.ogg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mozilla/devtools-perf-game/57f20b885b23cde2d55cd220d5f33704719065df/sounds/upgrade.ogg
--------------------------------------------------------------------------------