├── .gitattributes ├── app ├── .buildignore ├── robots.txt ├── favicon.ico ├── images │ └── eprime.png ├── components │ ├── app-config.js.tpl │ ├── editor │ │ ├── editor.css │ │ ├── editor-controller.js │ │ └── editor.html │ ├── modals │ │ ├── api-help-model.html │ │ ├── start-model.html │ │ ├── confirm-model.html │ │ ├── modal-controllers.js │ │ └── help-model.html │ ├── main │ │ ├── main-controller-spec.js │ │ ├── grid.css │ │ ├── map-directive.js │ │ ├── main.html │ │ ├── main.css │ │ ├── grid.js │ │ ├── main-controller.js │ │ └── gameIntro-service.js │ ├── app.js │ ├── panels │ │ ├── panel-controllers.js │ │ └── bot-panel.html │ ├── list │ │ ├── list-template.html │ │ ├── list.css │ │ └── list-directive.js │ ├── app.css │ └── game │ │ ├── script-system.js │ │ ├── game-service.js │ │ ├── world-factory.js │ │ └── bot-factory.js ├── partials │ ├── help.md │ ├── api.md │ └── tutorial.md ├── 404.html ├── index.html.tpl └── .htaccess ├── .bowerrc ├── .gitignore ├── .travis.yml ├── .jshintrc ├── .editorconfig ├── test ├── .jshintrc └── karma.conf.js ├── LICENSE.md ├── package.json ├── bower.json ├── readme.md ├── todo.md └── Gruntfile.js /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto -------------------------------------------------------------------------------- /app/.buildignore: -------------------------------------------------------------------------------- 1 | *.coffee -------------------------------------------------------------------------------- /.bowerrc: -------------------------------------------------------------------------------- 1 | { 2 | "directory": "bower_components" 3 | } 4 | -------------------------------------------------------------------------------- /app/robots.txt: -------------------------------------------------------------------------------- 1 | # robotstxt.org 2 | 3 | User-agent: * 4 | -------------------------------------------------------------------------------- /app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hypercubed/Epsilon-Prime/HEAD/app/favicon.ico -------------------------------------------------------------------------------- /app/images/eprime.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hypercubed/Epsilon-Prime/HEAD/app/images/eprime.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | bower_components 3 | dist 4 | .tmp 5 | .sass-cache 6 | .grunt 7 | *_config.json 8 | Thumbs.db 9 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - '0.10' 4 | before_script: 5 | - 'npm install -g bower grunt-cli' 6 | - 'bower install' 7 | -------------------------------------------------------------------------------- /app/components/app-config.js.tpl: -------------------------------------------------------------------------------- 1 | angular 2 | .module('ePrime') 3 | .constant('siteConfig', { 4 | debug: <%- debug %>, 5 | name: 'eprime', 6 | version: 0.1, 7 | store: 0 8 | }); 9 | -------------------------------------------------------------------------------- /app/components/editor/editor.css: -------------------------------------------------------------------------------- 1 | 2 | 3 | .editor-panel .ace_editor { 4 | width: 100%; 5 | height: 300px; 6 | border: none; 7 | margin-bottom: 0; 8 | border-radius: 0; 9 | 10 | position: relative; 11 | width: 100%; 12 | font-family: Consolas, 'Liberation Mono', Menlo, Courier, monospace; 13 | display: block; 14 | } 15 | 16 | .editor-panel .panel-body { 17 | padding: 0; 18 | } 19 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "node": true, 3 | "browser": true, 4 | "esnext": true, 5 | "bitwise": true, 6 | "camelcase": true, 7 | "curly": true, 8 | "eqeqeq": true, 9 | "immed": true, 10 | "indent": 2, 11 | "latedef": true, 12 | "newcap": true, 13 | "noarg": true, 14 | "quotmark": "single", 15 | "regexp": true, 16 | "undef": true, 17 | "unused": true, 18 | "strict": true, 19 | "trailing": true, 20 | "smarttabs": true, 21 | "globals": { 22 | "angular": false 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /app/components/modals/api-help-model.html: -------------------------------------------------------------------------------- 1 | 6 | 10 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig helps developers define and maintain consistent 2 | # coding styles between different editors and IDEs 3 | # editorconfig.org 4 | 5 | root = true 6 | 7 | 8 | [*] 9 | 10 | # Change these settings to your own preference 11 | indent_style = space 12 | indent_size = 2 13 | 14 | # We recommend you to keep these unchanged 15 | end_of_line = lf 16 | charset = utf-8 17 | trim_trailing_whitespace = true 18 | insert_final_newline = true 19 | 20 | [*.md] 21 | trim_trailing_whitespace = false 22 | -------------------------------------------------------------------------------- /app/components/modals/start-model.html: -------------------------------------------------------------------------------- 1 | 8 | 12 | -------------------------------------------------------------------------------- /app/components/main/main-controller-spec.js: -------------------------------------------------------------------------------- 1 | /* global xit:true */ 2 | 3 | 'use strict'; 4 | 5 | describe('Controller: MainCtrl', function () { 6 | 7 | // load the controller's module 8 | beforeEach(module('ePrime')); 9 | 10 | var MainCtrl, 11 | scope; 12 | 13 | // Initialize the controller and a mock scope 14 | beforeEach(inject(function ($controller, $rootScope) { 15 | scope = $rootScope.$new(); 16 | MainCtrl = $controller('MainCtrl', { 17 | $scope: scope 18 | }); 19 | })); 20 | 21 | xit('should attach a list of awesomeThings to the scope', function () { 22 | expect(scope.awesomeThings.length).toBe(3); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /test/.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "node": true, 3 | "browser": true, 4 | "esnext": true, 5 | "bitwise": true, 6 | "camelcase": true, 7 | "curly": true, 8 | "eqeqeq": true, 9 | "immed": true, 10 | "indent": 2, 11 | "latedef": true, 12 | "newcap": true, 13 | "noarg": true, 14 | "quotmark": "single", 15 | "regexp": true, 16 | "undef": true, 17 | "unused": true, 18 | "strict": true, 19 | "trailing": true, 20 | "smarttabs": true, 21 | "globals": { 22 | "after": false, 23 | "afterEach": false, 24 | "angular": false, 25 | "before": false, 26 | "beforeEach": false, 27 | "browser": false, 28 | "describe": false, 29 | "expect": false, 30 | "inject": false, 31 | "it": false, 32 | "jasmine": false, 33 | "spyOn": false 34 | } 35 | } 36 | 37 | -------------------------------------------------------------------------------- /app/components/modals/confirm-model.html: -------------------------------------------------------------------------------- 1 | 10 | 14 | -------------------------------------------------------------------------------- /app/components/main/grid.css: -------------------------------------------------------------------------------- 1 | 2 | #grid { 3 | padding: 0; 4 | } 5 | 6 | .grid { 7 | overflow: hidden; 8 | height: 100%; 9 | font-size: 14px; 10 | line-height: 12px; 11 | padding: 0; 12 | } 13 | 14 | #grid svg { 15 | font-family: "Courier New", Courier, monospace; 16 | cursor: move; 17 | } 18 | 19 | #grid text { 20 | fill: #000; 21 | font-size: 12px; 22 | /* pointer-events: none; */ 23 | text-shadow: 0 1px 0 #fff, 1px 0 0 #fff, 0 -1px 0 #fff, -1px 0 0 #fff; 24 | } 25 | 26 | #grid .bot { 27 | cursor: pointer; 28 | } 29 | 30 | #grid .bot rect, #grid .bot circle { 31 | fill: #eee; 32 | opacity: 0; 33 | } 34 | 35 | #grid .bot text { 36 | fill: #a94442; 37 | } 38 | 39 | #grid .bot-base text { 40 | fill: #428bca; 41 | } 42 | 43 | #grid .bot.active text { 44 | font-weight: bold; 45 | } 46 | 47 | #grid .bot.active rect, #grid .bot.active circle { 48 | stroke: blue; 49 | stroke-width: 1px; 50 | opacity: 0.4; 51 | } 52 | 53 | #grid .hover-text text { 54 | fill: #aaa; 55 | text-shadow: none; 56 | } 57 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015 Jayson Harshbarger 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | -------------------------------------------------------------------------------- /app/partials/help.md: -------------------------------------------------------------------------------- 1 | # Welcome to Epsilon-Prime 2 | 3 | In Epsilon-prime your goal is to conquer the planet of ε-prime. You do this by commanding an army of bots to explore and exploit the resources of ε-prime. You can control your bots individually using your mouse and keyboard or by writing command scripts in JavaScript. The game begins with a simple (and very inefficient) set of scripts for exploring and collecting resources. If you need help follow the tutorial. Using just these scripts you could complete the demo in <5,000 turns. But you can you do better; and, in the future, the game will become more challenging. 4 | 5 | Epsilon-Prime is a personal project and under active development. It is playable now, however, many things are likely to be broken or change in a future version. Player and developer feedback is appreciated... and needed. If you like the game or the idea please give feedback or encouragement ([Gitter chat](https://gitter.im/Hypercubed/Epsilon-Prime?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge)). Please report issues using [GitHub issues](https://github.com/Hypercubed/Epsilon-Prime/issues). The source code is available on [GitHub](https://github.com/Hypercubed/Epsilon-Prime). 6 | -------------------------------------------------------------------------------- /app/components/app.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | angular 4 | .module('ePrime', [ 5 | 'ngAnimate', 6 | 'ngRoute', 7 | 'ngSanitize', 8 | 'ngMessages', 9 | 'ngTouch', 10 | 'debounce', 11 | 'ui.ace', 12 | 'ui.bootstrap', 13 | 'angularMoment', 14 | 'xeditable', 15 | 'cfp.hotkeys', 16 | 'LocalForageModule', 17 | 'angular-intro', 18 | 'hc.thirdParty', 19 | 'hc.ngEcs', 20 | 'hc.marked' 21 | ]) 22 | .config(function ($routeProvider) { 23 | $routeProvider 24 | .when('/', { 25 | templateUrl: 'components/main/main.html', 26 | controller: 'MainCtrl as main', 27 | resolve: { 28 | savedGame: function(GAME) { 29 | //console.log('resolve'); 30 | return GAME.load(); 31 | } 32 | } 33 | }) 34 | .otherwise({ 35 | redirectTo: '/' 36 | }); 37 | }) 38 | .config(function($logProvider, siteConfig){ 39 | $logProvider.debugEnabled(siteConfig.debug); 40 | }) 41 | .config(function(hotkeysProvider) { 42 | hotkeysProvider.includeCheatSheet = false; 43 | }) 44 | .config(function ($localForageProvider, siteConfig) { 45 | $localForageProvider.config({ 46 | name : siteConfig.name+siteConfig.store 47 | }); 48 | }) 49 | .run(function(editableOptions) { 50 | editableOptions.theme = 'bs3'; 51 | }); 52 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ePrime", 3 | "version": "0.0.1", 4 | "dependencies": {}, 5 | "devDependencies": { 6 | "grunt": "^0.4.5", 7 | "grunt-autoprefixer": "^0.7.3", 8 | "grunt-concurrent": "^0.5.0", 9 | "grunt-contrib-clean": "^0.5.0", 10 | "grunt-contrib-concat": "^0.4.0", 11 | "grunt-contrib-connect": "^0.7.1", 12 | "grunt-contrib-copy": "^0.5.0", 13 | "grunt-contrib-cssmin": "^0.9.0", 14 | "grunt-contrib-htmlmin": "^0.3.0", 15 | "grunt-contrib-imagemin": "^0.8.1", 16 | "grunt-contrib-jshint": "^0.10.0", 17 | "grunt-contrib-uglify": "^0.4.0", 18 | "grunt-contrib-watch": "^0.6.1", 19 | "grunt-filerev": "^0.2.1", 20 | "grunt-google-cdn": "^0.4.0", 21 | "grunt-newer": "^0.7.0", 22 | "grunt-ng-annotate": "^0.3.0", 23 | "grunt-svgmin": "^0.4.0", 24 | "grunt-usemin": "^2.1.1", 25 | "grunt-wiredep": "^1.7.0", 26 | "jshint-stylish": "^0.2.0", 27 | "load-grunt-tasks": "^0.4.0", 28 | "time-grunt": "^0.3.1", 29 | "karma-phantomjs-launcher": "^0.1.4", 30 | "jasmine-core": "^2.1.2", 31 | "karma-jasmine": "^0.3.0", 32 | "karma": "^0.12.25", 33 | "grunt-karma": "^0.9.0", 34 | "grunt-gh-pages": "~0.9.1", 35 | "grunt-template": "~0.2.3" 36 | }, 37 | "engines": { 38 | "node": ">=0.10.0" 39 | }, 40 | "scripts": { 41 | "test": "grunt test" 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /app/components/modals/modal-controllers.js: -------------------------------------------------------------------------------- 1 | 2 | (function() { 3 | 'use strict'; 4 | 5 | angular.module('ePrime') 6 | .service('modals',function($modal) { 7 | var modals = this; 8 | 9 | modals.pauseConfirm = function(message,showReset) { 10 | return $modal.open({ 11 | templateUrl: 'components/modals/confirm-model.html', 12 | backdrop: 'static', 13 | keyboard: true, 14 | size: 'lg', 15 | controller: 'ConfirmInstanceCtrl', 16 | resolve: { 17 | data: function() { 18 | return { 19 | message: message, 20 | showReset: showReset 21 | }; 22 | } 23 | } 24 | }); 25 | }; 26 | 27 | modals.openHelp = function(template) { 28 | return $modal.open({ 29 | templateUrl: template || 'components/modals/help-model.html', 30 | backdrop: 'static', 31 | keyboard: true, 32 | size: 'lg', 33 | controller: 'HelpInstanceCtrl' 34 | }); 35 | }; 36 | 37 | return modals; 38 | }) 39 | 40 | .controller('HelpInstanceCtrl', function ($scope, hotkeys) { 41 | 42 | $scope.hotkeys = hotkeys; 43 | 44 | }) 45 | 46 | .controller('ConfirmInstanceCtrl', function ($scope, data, GAME) { 47 | 48 | $scope.game = GAME; 49 | $scope.message = data.message; 50 | $scope.showReset = data.showReset; 51 | 52 | }); 53 | 54 | })(); 55 | -------------------------------------------------------------------------------- /app/components/panels/panel-controllers.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 'use strict'; 3 | 4 | angular.module('ePrime') 5 | .directive('botPanel', function () { //https://github.com/incuna/angular-bind-html-compile/blob/master/angular-bind-html-compile.js 6 | return { 7 | restrict: 'A', 8 | scope: true, 9 | templateUrl: 'components/panels/bot-panel.html', 10 | //require: '^main', 11 | compile: function(tElem, tAttrs) { 12 | //var getter = $parse(tAttrs.botPanel); 13 | return function link(scope, element, attrs) { 14 | scope.bot = scope.$parent.$eval(tAttrs.botPanel); 15 | scope.showControls = angular.isDefined(attrs.showControls) && scope.$parent.$eval(tAttrs.showControls); 16 | 17 | scope.$parent.$watch(tAttrs.botPanel, function(val) { 18 | scope.bot = val; 19 | }); 20 | 21 | scope.checkName = function(name) { 22 | if (name.length < 3) { 23 | return 'Units name must contain at least three characters'; 24 | } 25 | return true; 26 | }; 27 | 28 | scope.setScript = function(bot, scriptName) { 29 | if (scriptName === null || scriptName.length < 1) { 30 | bot.$remove('script'); 31 | return; 32 | } 33 | bot.$add('script', {scriptName: scriptName, halted: false});  // rename scriptName -> name, use script component constructor? 34 | }; 35 | 36 | }; 37 | } 38 | }; 39 | }); 40 | 41 | })(); 42 | -------------------------------------------------------------------------------- /app/components/main/map-directive.js: -------------------------------------------------------------------------------- 1 | /* global d3:true */ 2 | /* _global _F:true */ 3 | 4 | (function() { 5 | 6 | 'use strict'; 7 | 8 | angular.module('ePrime') 9 | .directive('gameMap', function($log, ngEcs) { // todo: use entities 10 | return { 11 | restrict: 'AE', 12 | scope: { 13 | 'item': '=', 14 | 'change': '&' 15 | }, 16 | link: function link($scope, $element) { 17 | 18 | var bots = ngEcs.systems.bots.$family; 19 | var chunks = ngEcs.systems.chunks.$family; 20 | 21 | var svgStage = new d3.charts.Grid() 22 | .on('click', function(d) { 23 | if (!d.bot) { return; } // not a bot 24 | 25 | $scope.$apply(function() { 26 | $scope.change()(d); 27 | }); 28 | 29 | }); 30 | 31 | function draw() { 32 | svgStage.drawChunks(chunks); 33 | svgStage.drawBots(bots, ngEcs.$delay); 34 | } 35 | 36 | function update() { 37 | //$log.debug('update svg'); 38 | svgStage.updateChunks(chunks); 39 | svgStage.updateBots(ngEcs.$delay); 40 | } 41 | 42 | $scope.$watch('item', function(d) { 43 | if (!d || !d.bot) { return; } 44 | svgStage.zoomTo(d.bot.x, d.bot.y); 45 | svgStage.updateBots(ngEcs.$delay); 46 | }); 47 | 48 | ngEcs.$s('render', { // todo: set priority 49 | $addEntity: draw, 50 | $render: update 51 | }); 52 | 53 | d3.select($element[0]).datum([chunks,bots]).call(svgStage); 54 | 55 | } 56 | }; 57 | }); 58 | 59 | })(); 60 | -------------------------------------------------------------------------------- /app/components/list/list-template.html: -------------------------------------------------------------------------------- 1 |
2 | 3 |
4 | 5 |
6 |
7 |
8 |
9 |
10 | Back 11 |
12 |
13 |
14 | 15 | 16 |
17 |
18 |
19 |
20 |
    21 |
  • 23 | 24 | 27 |
  • 28 |
29 | 32 |
33 |
34 | 35 |
36 | 37 |
38 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ePrime", 3 | "version": "0.0.1", 4 | "dependencies": { 5 | "angular": "~1.4.0", 6 | "json3": "=3.3.1", 7 | "es5-shim": "=3.1.0", 8 | "bootstrap": "=3.3.0", 9 | "angular-sanitize": "~1.4.0", 10 | "angular-animate": "~1.4.0", 11 | "angular-touch": "~1.4.0", 12 | "angular-route": "~1.4.0", 13 | "angular-bootstrap": "=0.12.0", 14 | "angular-ui-ace": "bower", 15 | "angular-messages": "=1.3.8", 16 | "noisejs": "josephg/noisejs", 17 | "angular-xeditable": "=0.1.8", 18 | "bootstrap-material-design": "=0.2.1", 19 | "angular-hotkeys": "chieffancypants/angular-hotkeys#1.7.0", 20 | "angular-localforage": "=1.2.2", 21 | "d3": "=3.5.3", 22 | "_F": "Hypercubed/_F#=0.0.7", 23 | "angular-moment": "=0.9.0", 24 | "lodash": "2.4.1", 25 | "aether": "=0.3.29", 26 | "ng-debounce": "=0.1.5", 27 | "font-awesome": "=4.3.0", 28 | "eventemitter2": "=0.4.14", 29 | "angular-third-party": "Hypercubed/angular-third-party", 30 | "angular-ecs": "Hypercubed/angular-ecs", 31 | "intro.js": "=1.0.0", 32 | "fpsmeter": "darsain/fpsmeter#=0.3.1", 33 | "angular-marked": "=0.0.12", 34 | "angular-intro.js": "^2.1.1" 35 | }, 36 | "devDependencies": { 37 | "angular-mocks": "=1.2.0", 38 | "angular-scenario": "=1.2.0" 39 | }, 40 | "overrides": { 41 | "noisejs": { 42 | "main": "perlin.js" 43 | }, 44 | "SandboxJS": { 45 | "main": "Sandbox.js" 46 | }, 47 | "ace-builds": { 48 | "main": "src-min-noconflict/ace.js" 49 | }, 50 | "JS-Interpreter": { 51 | "main": "acorn.js" 52 | }, 53 | "ng-joyride": { 54 | "main": [ 55 | "ng-joyride.js", 56 | "ng-joyride.css" 57 | ] 58 | } 59 | }, 60 | "appPath": "app", 61 | "resolutions": { 62 | "intro.js": "~1.0.0", 63 | "aether": "=0.3.29", 64 | "angular-hotkeys": "=1.5.0", 65 | "angular": "~1.4.0" 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /test/karma.conf.js: -------------------------------------------------------------------------------- 1 | // Karma configuration 2 | // http://karma-runner.github.io/0.12/config/configuration-file.html 3 | // Generated on 2014-11-19 using 4 | // generator-karma 0.8.3 5 | 6 | module.exports = function(config) { 7 | 'use strict'; 8 | 9 | config.set({ 10 | // enable / disable watching file and executing tests whenever any file changes 11 | autoWatch: true, 12 | 13 | // base path, that will be used to resolve files and exclude 14 | basePath: '../', 15 | 16 | // testing framework to use (jasmine/mocha/qunit/...) 17 | frameworks: ['jasmine'], 18 | 19 | // list of files / patterns to load in the browser 20 | files: [ 21 | 'bower_components/angular/angular.js', 22 | 'bower_components/angular-mocks/angular-mocks.js', 23 | 'bower_components/angular-animate/angular-animate.js', 24 | 'bower_components/angular-route/angular-route.js', 25 | 'bower_components/angular-sanitize/angular-sanitize.js', 26 | 'bower_components/angular-touch/angular-touch.js', 27 | 'app/components/*.js', 28 | 'app/components/**/*.js', 29 | //'test/mock/**/*.js', 30 | //'test/spec/**/*.js' 31 | ], 32 | 33 | // list of files / patterns to exclude 34 | exclude: [], 35 | 36 | // web server port 37 | port: 8080, 38 | 39 | // Start these browsers, currently available: 40 | // - Chrome 41 | // - ChromeCanary 42 | // - Firefox 43 | // - Opera 44 | // - Safari (only Mac) 45 | // - PhantomJS 46 | // - IE (only Windows) 47 | browsers: [ 48 | 'PhantomJS' 49 | ], 50 | 51 | // Which plugins to enable 52 | plugins: [ 53 | 'karma-phantomjs-launcher', 54 | 'karma-jasmine' 55 | ], 56 | 57 | // Continuous Integration mode 58 | // if true, it capture browsers, run tests and exit 59 | singleRun: false, 60 | 61 | colors: true, 62 | 63 | // level of logging 64 | // possible values: LOG_DISABLE || LOG_ERROR || LOG_WARN || LOG_INFO || LOG_DEBUG 65 | logLevel: config.LOG_INFO, 66 | 67 | // Uncomment the following lines if you are using grunt's server to run the tests 68 | // proxies: { 69 | // '/': 'http://localhost:9000/' 70 | // }, 71 | // URL root prevent conflicts with the site root 72 | // urlRoot: '_karma_' 73 | }); 74 | }; 75 | -------------------------------------------------------------------------------- /app/components/list/list.css: -------------------------------------------------------------------------------- 1 | #list { 2 | 3 | } 4 | 5 | .panel-list { 6 | position: absolute; 7 | top: 0; 8 | bottom: 0; 9 | right: 0; 10 | left: 0; 11 | height: 100%; 12 | } 13 | 14 | .panel-list .panel-heading { 15 | position: absolute; 16 | top: 0; 17 | left: 0; 18 | right: 0; 19 | height: 50px; 20 | } 21 | 22 | .panel-list .panel-footer { 23 | position: absolute; 24 | bottom: 0; 25 | left: 0; 26 | right: 0; 27 | height: 50px; 28 | } 29 | 30 | .panel-list .list-group-item { 31 | padding-right: 30px; 32 | padding-left: 10px; 33 | } 34 | 35 | .panel-list .list-group { 36 | position: absolute; 37 | top: 50px; 38 | bottom: 50px; 39 | right: 0; 40 | left: 0; 41 | height: calc(100% - 100px); 42 | overflow-y: scroll; 43 | } 44 | 45 | .panel-list .list-group-item { 46 | overflow: hidden; 47 | } 48 | 49 | .panel-list .list-group-item.opened { 50 | height: 0px; 51 | overflow: hidden; 52 | } 53 | 54 | .panel-list .list-group-item.active.opened { 55 | position: absolute; 56 | top: 0px; 57 | bottom: 100px; 58 | right: 0; 59 | left: 0; 60 | height: calc(100%); 61 | overflow-y: scroll; 62 | } 63 | 64 | .list-group-item.ng-enter, 65 | .list-group-item.ng-leave { 66 | display: none; 67 | --webkit-transition: none; 68 | transition: none; 69 | } 70 | 71 | .list-group-item.active, .list-group-item.active:hover, .list-group-item.active:focus { 72 | color: #000; 73 | background-color: #F7F7F7; 74 | outline: none; 75 | } 76 | 77 | .list-group .list-group-item.active:hover, .list-group .list-group-item.active:focus { 78 | background-color: #F7F7F7; 79 | outline: none; 80 | } 81 | 82 | .filter > input { 83 | background: #fff no-repeat 13px 13px; 84 | border: none; 85 | width: 90%; 86 | margin-left: 10px; 87 | } 88 | 89 | .filter > input:focus { 90 | outline: 0; 91 | } 92 | 93 | .panel-list .list-group-item button.open { 94 | position: absolute; 95 | top: 0; 96 | bottom: 0; 97 | right: 5px; 98 | color: #808080; 99 | outline:0; 100 | } 101 | 102 | .panel-list.opened .list-group-item button.open { 103 | display: none; 104 | } 105 | 106 | /* .panel-list i { 107 | position: absolute; 108 | left: 5px; 109 | top: 20px; 110 | opacity: .3; 111 | width: 50px; 112 | text-align: center; 113 | font-size: 3em; 114 | } 115 | 116 | .panel-body i { 117 | opacity: .3; 118 | width: 50px; 119 | text-align: center; 120 | font-size: 3em; 121 | } */ 122 | 123 | .panel-list .form-group { 124 | margin-bottom: 0; 125 | } 126 | 127 | .panel-footer .pagination { 128 | margin: 0; 129 | } 130 | -------------------------------------------------------------------------------- /app/components/modals/help-model.html: -------------------------------------------------------------------------------- 1 | 93 | 97 | -------------------------------------------------------------------------------- /app/partials/api.md: -------------------------------------------------------------------------------- 1 | # Epsilon-Prime API Help 2 | 3 | ## $bot properties 4 | * `name` -- name 5 | * `x`, `y` -- x and y positions 6 | * `E`, `mE` -- current and maximum energy capacity 7 | * `S`, `mS` -- current and maximum storage capacity 8 | * `mem` -- persistent bot memory storage 9 | 10 | ## $bot methods 11 | 12 | ### unload({string="@"}) 13 | Attempts to unload storage to another bot. Unloading is only successful if the units are located in the same space. If a string is provided the bot will attempt to unload to another bot with the matching name, otherwise "@" (a "heavy" bot) is assumed. If unloading is not possible this method has no effect. 14 | 15 | Example: 16 | ``` 17 | $bot.unload(); // Tries to unload to a heavy unit 18 | $bot.unload('Bob'); // Tries to unload to a bot named 'Bob. 19 | ``` 20 | 21 | ### charge({string="@"}) 22 | Attempts to charge batteries from another bot. Charging is only successful if the units are located in the same space. If a string is provided will attempt to unload to a bot with the matching name. Otherwise "@" (a "heavy" bot) is assumed. If charging is not possible this method has no effect. 23 | 24 | Example: 25 | ``` 26 | $bot.charge(); // Tries to charge from the heavy unit 27 | $bot.charge('Bob'); // Tries to charge from a bot named 'Bob. 28 | ``` 29 | 30 | ### mine() 31 | Attempts to mine at the current location. Returns the number of resources collected or false if no resource are available. If mining is not possible this method has no effect. 32 | 33 | Example: 34 | ``` 35 | $bot.mine(); 36 | ``` 37 | 38 | ### upgrade() 39 | Attempts to upgrade the bot using current resources. If upgrading is not possible this method has no effect. 40 | 41 | Example: 42 | ``` 43 | $bot.upgrade(); 44 | ``` 45 | 46 | ### construct({string}) 47 | Attempts to construct a new bot using current resources. If a string is provided the constructed bot will start with the named script, otherwise "Manual" is assumed. If construction is not possible this method has no effect. 48 | 49 | Example: 50 | ``` 51 | $bot.construct(); 52 | $bot.construct('Collect'); 53 | ``` 54 | 55 | ### find({string}) 56 | Finds the nearest bot or tile whose name or tile character matches the string. 57 | 58 | Example: 59 | ``` 60 | $bot.find('@'); // Finds the nearest heavy bot 61 | $bot.find('X'); // Finds the nearest resource cache 62 | $bot.find('Bob'); // Finds the nearest unit named "Bob" 63 | ``` 64 | 65 | ### moveTo({number},{number}) 66 | Moves towards the given x,y position. Will perform very basic obstacle avoidance. 67 | 68 | Example: 69 | ``` 70 | $bot.moveTo($bot.x + 5,$bot.y + 5); 71 | var bob = $bot.find('Bob'); 72 | if (bob) { 73 | $bot.moveTo(bob.x,bob.y); 74 | } 75 | ``` 76 | 77 | ### distanceTo({object}) 78 | Returns the distance (in steps) to the given object position. 79 | 80 | Example: 81 | ``` 82 | var bob = $bot.find('Bob'); 83 | console.log($bot.distanceTo(bob)); 84 | ``` 85 | 86 | ## $map methods 87 | 88 | ### get({number},{number}) 89 | Gets the tile located at the given x,y position. 90 | 91 | Example: 92 | ``` 93 | var tile = $map.get($bot.x + 5,$bot.y + 5); 94 | if (tile.t === 'X') { 95 | $bot.moveTo(tile.x,tile.y); 96 | } 97 | ``` 98 | 99 | ## console methods 100 | 101 | ### log(...) 102 | Prints to browser debugging console. 103 | 104 | Example: 105 | ``` 106 | var tile = $map.get($bot.x + 5,$bot.y + 5); 107 | console.log(tile); 108 | ``` 109 | -------------------------------------------------------------------------------- /app/partials/tutorial.md: -------------------------------------------------------------------------------- 1 | # Epsilon-Prime Tutorial 2 | 3 | ![Capture](http://cdn.rawgit.com/Hypercubed/Epsilon-Prime/master/app/images/eprime.png) 4 | 5 | *This is a transcript of the in-game tutorial* 6 | 7 | ## Welcome to Epsilon-prime 8 | 9 | In Epsilon-prime your goal is to conquer the planet of ε-prime. You do this by commanding an army of bots to explore and exploit the resources of ε-prime. You can control your bots individually using your mouse and keyboard or by using command scripts written in JavaScript. The game begins with a simple (and very inefficient) set of scripts for exploring and collecting resources. Using just these scripts you could complete this demo in ~2,500 turns. But you can you do better! 10 | 11 | The game map is located on the left. Use the mouse and scroll wheel (or touch screen) to pan and zoom the map. The @ mark is your starting base. 12 | 13 | On the right is a units lists. All your units are listed here. 14 | 15 | At this time you have one unit… the base. Again the base is identified by the @ symbol. 16 | 17 | The red progress bar indicates the unit’s energy storage and capacity. The base unit begins with 100 J of energy. Energy is needed to move and collected resources. 18 | 19 | Above the energy indicator you will find the units movement cost and charging rate. 20 | 21 | The energy of the base unit is depleted at a very high rate while moving. Notice that the base requires a full charge of 100 J to move one space. 22 | 23 | The base unit recharges at just over 2 J per turn. At this rate a heavy base unit can only move one space every 44 turns. 24 | 25 | The blue progress bar indicates a units resource storage and capacity. Resources are used to upgrade units or construct new units. 26 | 27 | Constructing new units costs 100 kg. Construct a new unit now using the button indicated. 28 | 29 | Your new unit will appear in the list... 30 | 31 | and on the map indicated on with an A. 32 | 33 | Notice that the movement cost and recharge rate are both lower. This unit can move one space every two turns using its own power generation. 34 | 35 | However small units can also charge from larger units. Make the rover the active unit by clicking the A in the bot list... 36 | 37 | Now press the action key `s` to charge the Rover using the Base’s energy. This is the action key. It is also used to unload any unit storage to the base and to mine resources. 38 | 39 | You can now begin exploring the map using the `q`-`c` keys. The letters `qweadzxc` are directions of movement (`q` for North West, `c` for South East, etc). Imagine your unit is located at the action key `s` on your keyboard. 40 | 41 | If you encounter an X on the map this is a resource cache (or mine). Collect resources using the action key `s` 42 | 43 | You will notice that the energy depletes as you move. This is because this unit\'s movement cost is greater than its recharge rate. You can use all your energy to mine or return to the base periodically to unload and charge... 44 | 45 | Or use the wait key `.` to wait one turn an recharge your unit. 46 | 47 | You may also use the >| button to advance a turn. 48 | 49 | You can use this dropdown to set a bots automatic actions each turn. Select 'Construct' for the base... 50 | 51 | and 'Collect' for the bot. 52 | 53 | Press play button to automatically cycle turns and watch your bots work autonomously. 54 | 55 | You can modify the action scripts here. 56 | 57 | Your game is automatically saved approximately every 60 seconds. 58 | 59 | Check your progress here. 60 | 61 | ### Enjoy 62 | -------------------------------------------------------------------------------- /app/components/editor/editor-controller.js: -------------------------------------------------------------------------------- 1 | /* global ace:true */ 2 | /* global _F:true */ 3 | 4 | (function() { 5 | 'use strict'; 6 | 7 | angular.module('ePrime') 8 | .controller('EditorCtrl', function($log, $modalInstance, initialScriptId, defaultScripts, GAME, aether, modals) { 9 | 10 | var editor = this; 11 | 12 | editor.set = function(script) { 13 | editor.script = script; 14 | }; 15 | 16 | editor.reset = function(form) { 17 | if (form) { 18 | form.$setPristine(); 19 | form.$setUntouched(); 20 | form.code.$error = {}; 21 | } 22 | editor.scripts = angular.copy(GAME.scripts); 23 | editor.script = editor.scripts[initialScriptId || 0]; 24 | }; 25 | 26 | editor.resetToDefaultScripts = function() { 27 | 28 | defaultScripts.forEach(function(script) { 29 | for (var i = 0; i < editor.scripts.length; i++) { 30 | if (editor.scripts[i].name === script.name) { 31 | angular.copy(script, editor.scripts[i]); 32 | return; 33 | } 34 | } 35 | editor.scripts.push(angular.copy(script)); 36 | }); 37 | 38 | //console.log(GAME.scripts); 39 | //angular.extend(editor.scripts, defaultScripts); 40 | //console.log(GAME.scripts); 41 | //editor.reset(form); 42 | }; 43 | 44 | editor.new = function(name, code) { 45 | name = name || 'new'; 46 | code = code || '$log($bot.name, $bot.x, $bot.y);'; 47 | editor.script = {name: name, code: code }; 48 | editor.scripts.push(editor.script); 49 | deDupNames(); 50 | }; 51 | 52 | editor.delete = function(script) { 53 | var index = editor.scripts.indexOf(script); 54 | editor.scripts.splice(index,1); 55 | if (editor.script === script) { 56 | editor.script = editor.scripts[index < editor.scripts.length ? index : 0]; 57 | } 58 | }; 59 | 60 | function deDupNames() { 61 | var names = editor.scripts.map(_F('name')); 62 | 63 | editor.scripts.forEach(function(d,i) { // cheap way to unique names 64 | while (names.indexOf(d.name) < i) { 65 | d.name = names[i] = d.name+'*'; 66 | } 67 | }); 68 | } 69 | 70 | editor.update = function(script, form) { 71 | 72 | form.code.$error.syntaxError = false; 73 | if (script.code && script.code.length > 0) { 74 | aether.transpile(script.code); 75 | 76 | //console.log(aether.problems); 77 | 78 | if (aether.problems.errors.length > 0) { 79 | form.code.$error.syntaxError = aether.problems.errors.map(_F('message')).join('\n'); 80 | } 81 | } 82 | 83 | }; 84 | 85 | editor.save = function() { 86 | deDupNames(); 87 | 88 | GAME.scripts = angular.copy(editor.scripts); 89 | GAME.scripts.forEach(function(d) { 90 | d.$method = null; 91 | }); 92 | $modalInstance.close(); 93 | }; 94 | 95 | editor.aceLoaded = function(_editor){ 96 | var _session = _editor.getSession(); 97 | 98 | _editor.setShowPrintMargin(false); 99 | 100 | _session.setUseWrapMode(false); 101 | 102 | _session 103 | .setUndoManager(new ace.UndoManager()); 104 | 105 | _session 106 | .setTabSize(2); 107 | }; 108 | 109 | editor.help = function() { 110 | modals.openHelp('components/modals/api-help-model.html'); 111 | }; 112 | 113 | editor.reset(); 114 | 115 | }); 116 | })(); 117 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # Epsilon-Prime 2 | 3 | [![Join the chat at https://gitter.im/Hypercubed/Epsilon-Prime](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/Hypercubed/Epsilon-Prime?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) 4 | 5 | Epsilon-prime is inspired by the classic 4x game Empire. In ε-prime a player will control units to εXplore a procedurally generated world (called ε-prime), εXploit the resource of ε-prime in order to εXpand their army of bots and eventually conquer (εXterminate?) the planet ε-prime. The player uses units (or bots), controlled either manually or via JavaScript command scripts, to manage energy use and collect resources from the ε-prime environment. These resources are used to create new units or upgrade existing units. The players goal is to collect resources in the most efficient manner possible. 6 | 7 | [Play demo now](http://hypercubed.github.io/Epsilon-Prime/) | 8 | [Read the tutorial](https://github.com/Hypercubed/Epsilon-Prime/blob/master/app/partials/tutorial.md) | 9 | [Read the user API](https://github.com/Hypercubed/Epsilon-Prime/blob/master/app/partials/api.md) 10 | 11 | ![Capture](http://cdn.rawgit.com/Hypercubed/Epsilon-Prime/master/app/images/eprime.png) 12 | 13 | ## Development status 14 | Epsilon-Prime is a personal project and under active development. It is playable now (at http://hypercubed.github.io/Epsilon-Prime/), however, many things are likely to be broken or change in a future version. Player and developer feedback is appreciated... and needed. If you like the game or the idea please give feedback or encouragement. 15 | 16 | [![Gitter](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/Hypercubed/Epsilon-Prime?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge) 17 | 18 | Also see [angular-ecs](https://github.com/Hypercubed/angular-ecs) 19 | 20 | ## Current Features 21 | * Fog of war. 22 | * "Near-Infinite" procedurally generated terrain. 23 | * Units scripted using player JavaScript. 24 | * Production of new units, upgrade units. 25 | * End game! 26 | 27 | ## Install (for developers) 28 | ``` 29 | git clone https://github.com/Hypercubed/Epsilon-Prime.git 30 | cd Epsilon-Prime 31 | npm install 32 | bower install 33 | grunt serve 34 | ``` 35 | 36 | ## How to play 37 | [Read the tutorial](https://github.com/Hypercubed/Epsilon-Prime/blob/master/app/partials/tutorial.md) 38 | 39 | ## User script API 40 | [Read the user API](https://github.com/Hypercubed/Epsilon-Prime/blob/master/app/partials/api.md) 41 | 42 | ## License 43 | Copyright (c) 2015 Jayson Harshbarger [![Gittip donate button](http://img.shields.io/gratipay/Hypercubed.svg)](https://www.gittip.com/hypercubed/ "Donate weekly to this project using Gittip") 44 | 45 | [MIT License](http://en.wikipedia.org/wiki/MIT_License) 46 | 47 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 48 | 49 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 50 | 51 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 52 | -------------------------------------------------------------------------------- /app/components/editor/editor.html: -------------------------------------------------------------------------------- 1 | 2 |
3 | 4 |
5 |
6 | 9 | 10 |
11 |
12 | 13 |
14 |
15 | 18 |
19 |
20 |
21 |
22 | 27 | 46 |
47 |
48 |
49 |
50 | 51 |
52 |
53 | 60 |
61 |
62 |
required
63 |
{{form.code.$error.syntaxError}}
64 |
65 | 77 | 78 |
79 | 80 |
81 | -------------------------------------------------------------------------------- /app/components/main/main.html: -------------------------------------------------------------------------------- 1 | 42 | 43 |
44 |
45 | 46 |
47 |
48 |
49 |
50 | 51 |
52 |
53 |
54 |
55 |
56 | 57 |
58 |
59 | 60 | 90 | -------------------------------------------------------------------------------- /app/components/main/main.css: -------------------------------------------------------------------------------- 1 | 2 | .grid-ctrl { 3 | 4 | } 5 | 6 | .grid, .btn-mono { 7 | font-family: "Courier New", Courier, monospace; 8 | } 9 | 10 | .ctrl hr { 11 | margin-top: 4px; 12 | margin-bottom: 4px; 13 | } 14 | 15 | .ctrl .nav { 16 | margin-bottom: 10px; 17 | } 18 | 19 | .ctrl .log { 20 | width: 100%; 21 | height: 300px; 22 | border: 1px solid #ccc; 23 | margin-bottom: 5px; 24 | } 25 | 26 | .ctrl .progress { 27 | margin-bottom: 0px; 28 | height: 20px; 29 | } 30 | 31 | .ctrl .progress-bar { 32 | color: #000; 33 | font-weight: bold; 34 | } 35 | 36 | .unit-ctrl { 37 | 38 | } 39 | 40 | .unit-ctrl h4 { 41 | margin: 0; 42 | } 43 | 44 | .unit-ctrl > [sliding-list] { 45 | height: 100%; 46 | } 47 | 48 | .unit-ctrl .panel, .unit-list .panel { 49 | margin-bottom: 5px; 50 | height: 100%; 51 | overflow-y: hidden; 52 | background-color: #fff; 53 | } 54 | 55 | .unit-ctrl .panel-heading, .unit-list .panel-heading { 56 | margin: 0 0 5px 0; 57 | cursor: pointer; 58 | position: relative; 59 | } 60 | 61 | .unit-ctrl .panel-body, .unit-list .panel-body { 62 | padding: 0 15px 5px 15px; 63 | height: 100%; 64 | overflow-y: scroll; 65 | } 66 | 67 | .panel-list .panel-body { 68 | padding: 0; 69 | height: calc(100% - 50px); 70 | } 71 | 72 | .panel-body > .list-group { 73 | margin-bottom: 0px; 74 | } 75 | 76 | .bot-panel { 77 | outline:0; 78 | padding-top: 5px; 79 | } 80 | 81 | .bot-panel .row-action-primary .text-muted { 82 | color: #E4E4E4; 83 | } 84 | 85 | .script-btns .row { 86 | margin-bottom: 6px; 87 | padding: 0; 88 | } 89 | 90 | .script-btn .btn-hover { 91 | opacity: 0; 92 | -webkit-transition:all cubic-bezier(0.250, 0.460, 0.450, 0.940) 0.5s; 93 | transition:all cubic-bezier(0.250, 0.460, 0.450, 0.940) 0.5s; 94 | } 95 | 96 | .script-btn:hover .btn-hover { 97 | opacity: 0.8; 98 | } 99 | 100 | .bot-panel .radio { 101 | margin-top: 7px; 102 | margin-bottom: 0; 103 | } 104 | 105 | .bot-panel .btn-group.active { 106 | opacity: .65; 107 | box-shadow: inset 0 3px 5px rgba(0,0,0,0.125); 108 | } 109 | 110 | .bot-panel .row-action-primary { 111 | padding-right: 0; 112 | } 113 | 114 | .bot-panel .row-action-primary { 115 | float: left; 116 | display: inline-block; 117 | padding-right: 16px; 118 | } 119 | 120 | .bot-panel .row-action-primary i { 121 | background: rgba(0, 0, 0, 0.25); 122 | border-radius: 100%; 123 | text-align: center; 124 | line-height: 56px; 125 | font-size: 20px; 126 | color: white; 127 | display: block; 128 | width: 56px; 129 | height: 56px; 130 | } 131 | 132 | .bot-panel .row-content { 133 | display: inline-block; 134 | width: calc(100% - 92px); 135 | min-height: 66px; 136 | } 137 | 138 | .bot-panel .console > span { 139 | position: absolute; 140 | margin-top: 8px; 141 | } 142 | 143 | .bot-panel .console > input { 144 | margin-left: 18px; 145 | width: 90%; 146 | } 147 | 148 | .btn-info .badge, .btn-danger .badge { 149 | color: #000; 150 | } 151 | 152 | .badge.energy, .badge.danger { 153 | background-color: #f44336; 154 | color: #000; 155 | } 156 | 157 | .bot-panel .row-action-primary i { 158 | line-height: 50px !important; 159 | font-family: "Courier New", Courier, monospace; 160 | border: 2px solid rgba(0, 0, 0, 0.247059); 161 | cursor: pointer; 162 | } 163 | 164 | .list-group-item.active .bot-panel .row-action-primary i { 165 | border: 2px solid #5555FF; 166 | box-sizing: border-box; 167 | } 168 | 169 | .badge.storage, .badge.info { 170 | background-color: #03a9f4; 171 | color: #000; 172 | } 173 | 174 | .list-group .list-group-item { 175 | overflow: inherit; 176 | } 177 | 178 | .dropdown-menu .active { 179 | background-color: #ccc; 180 | } 181 | 182 | .btn-xs .badge { 183 | display: inline-block; 184 | min-width: 10px; 185 | padding: 1px 3px; 186 | font-size: 10px; 187 | font-weight: bold; 188 | line-height: 1; 189 | text-align: center; 190 | white-space: nowrap; 191 | vertical-align: baseline; 192 | border-radius: 5px; 193 | } 194 | 195 | .btn-xxs, .btn-group-xxs > .btn { 196 | padding: 1px 5px; 197 | font-size: 10px; 198 | line-height: 1.5; 199 | border-radius: 3px; 200 | } 201 | -------------------------------------------------------------------------------- /app/components/list/list-directive.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 'use strict'; 3 | 4 | function ListController($scope, hotkeys) { 5 | 6 | $scope.page = 1; 7 | 8 | $scope.opened = false; 9 | $scope.search = { name: '' }; 10 | //$scope.pagedItems = $scope.items; 11 | 12 | $scope.select = function(item) { 13 | $scope.openItem = $scope.item = item; 14 | var index = $scope.items.indexOf(item); 15 | $scope.page = index + 1; 16 | $scope.items.forEach(function(d) { 17 | d.active = d === item; 18 | }); 19 | $scope.change()(item); 20 | }; 21 | 22 | $scope.open = function(item){ 23 | $scope.select(item); 24 | $scope.page = $scope.items.indexOf(item)+1; 25 | $scope.opened = true; 26 | }; 27 | 28 | $scope.pageChanged = function(page) { 29 | var item = $scope.items[page-1]; 30 | $scope.select(item); 31 | }; 32 | 33 | $scope.close = function() { 34 | $scope.opened = false; 35 | }; 36 | 37 | hotkeys.bindTo($scope) 38 | .add({ 39 | combo: 'k', 40 | //description: 'next bot', 41 | callback: function() { 42 | $scope.page = Math.min($scope.page+1, $scope.items.length); 43 | $scope.pageChanged($scope.page); 44 | } 45 | }) 46 | .add({ 47 | combo: 'j', 48 | //description: 'prev bot', 49 | callback: function() { 50 | $scope.page = Math.max($scope.page-1, 1); 51 | $scope.pageChanged($scope.page); 52 | } 53 | }); 54 | 55 | /* var a = null, b = null; 56 | function pagedItems() { 57 | var index = $scope.page - 1; 58 | var page = index - index % 5; 59 | return $scope.items.slice(page, page+5); 60 | } 61 | 62 | $scope.pagedItems = pagedItems; */ 63 | 64 | } 65 | 66 | angular.module('ePrime') 67 | .controller('ListController', ListController) 68 | .directive('slidingListInject', function(){ 69 | return { 70 | require: '^slidingList', 71 | link: function($scope, $element, $attrs, controller, $transclude) { 72 | 73 | //console.log(controller.parent); 74 | 75 | /* if (!$transclude) { 76 | throw minErr('ngTransclude')('orphan', 77 | 'Illegal use of ngTransclude directive in the template! ' + 78 | 'No parent directive that requires a transclusion found. ' + 79 | 'Element: {0}', 80 | startingTag($element)); 81 | } */ 82 | 83 | var innerScope = controller.parent.$new(); 84 | innerScope[controller.valueKey] = $scope.item; 85 | innerScope.opened = $scope.opened; 86 | innerScope.open = controller.open; 87 | innerScope.$index = $scope.$index; 88 | 89 | $scope.$watch('item', function(val) { 90 | innerScope[controller.valueKey] = val; 91 | }); 92 | 93 | $scope.$watch('opened', function(val) { 94 | innerScope.opened = val; 95 | }); 96 | 97 | $transclude(innerScope, function(clone) { 98 | $element.empty(); 99 | $element.append(clone); 100 | $element.on('$destroy', function() { 101 | innerScope.$destroy(); 102 | }); 103 | }); 104 | } 105 | }; 106 | }) 107 | .directive('slidingList', function($parse) { 108 | return { 109 | restrict: 'AE', 110 | transclude: true, 111 | scope: { 112 | openItem: '=item', 113 | change: '&' 114 | }, 115 | controller: 'ListController', 116 | templateUrl: 'components/list/list-template.html', 117 | link: function link($scope, $element, $attrs, controller) { 118 | 119 | //scope.item = $scope.openItem; 120 | 121 | var expression = $attrs.slidingList; 122 | var match = expression.match(/^\s*([\s\S]+?)\s+in\s+([\s\S]+?)(?:\s+as\s+([\s\S]+?))?(?:\s+track\s+by\s+([\s\S]+?))?\s*$/); 123 | 124 | var lhs = match[1]; 125 | var rhs = match[2]; 126 | 127 | $scope.items = $parse(rhs)($scope.$parent); 128 | $scope.search = {}; 129 | //$scope.opened = $scope.items.length < 2; 130 | 131 | controller.parent = $scope.$parent; 132 | controller.valueKey = lhs; 133 | } 134 | }; 135 | }); 136 | 137 | })(); 138 | -------------------------------------------------------------------------------- /app/404.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Page Not Found :( 6 | 141 | 142 | 143 |
144 |

Not found :(

145 |

Sorry, but the page you were trying to view does not exist.

146 |

It looks like this was the result of either:

147 | 151 | 154 | 155 |
156 | 157 | 158 | -------------------------------------------------------------------------------- /todo.md: -------------------------------------------------------------------------------- 1 | # Todo list 2 | 3 | _\( managed using [todo-md](https://github.com/Hypercubed/todo-md) \)_ 4 | 5 | ## Bugs 6 | - [?] Active bot not selected in some cases (test this) 7 | - [ ] Can't find variable: Uint8ClampedArray in Safari 5.1.7 (shim?) 8 | - [ ] SVG is not size responsive!!! 9 | - [-] Prevent duplicate scripts with the same name 10 | - [ ] Too slow? Test on firefox. 11 | - [ ] Improve GUI Responsiveness to small screens 12 | - [ ] Upgrade using base resources? 13 | - [ ] j/k should only cycle filtered bots? 14 | - [ ] Trap errors in GAME.load. 15 | 16 | ## ECS 17 | - [ ] Decompose BotComponents (Bot, Tile/sprite, Move, Charging) 18 | - [x] Move draw to system? 19 | - [-] Order system updates by priority 20 | - [ ] Move accumulator to engine 21 | 22 | ## Next 23 | - [ ] Make action queue a component e.action.queue.push(fn); 24 | - [ ] Better in game API help 25 | - [ ] Bot failure rate. 26 | - [ ] Bots methods could improve by keeping track of current chunk 27 | - [?] Stop using watchers in map directive 28 | - [x] Add $map API to readme 29 | - [ ] Add $bot.memory to readme 30 | - [ ] More hotkeys (`S` (shift-s) save, `,` to mine,...) 31 | - [ ] Shortcuts for do until E = 0? 32 | - [ ] More orders (Move to target, mine at target) 33 | - [ ] Deactivate orders for active bot on manual keypress? 34 | - [x] Update tutorial for new mechanics, add callback functions 35 | - [ ] Move tutorial to service. 36 | - [ ] Move more GAME stuff to ecs engine 37 | - [ ] Improve $bot logging, error system. 38 | - [ ] Finish Entity component system 39 | - [ ] New screen shot 40 | - [ ] Make defaultScripts as scripts service (or entities?). (crud, reset, validate, run, etc) 41 | - [ ] Tests 42 | - [ ] Move hot keys somewhere? 43 | - [ ] Improve editor save/validation 44 | - [ ] Remove bot actions in MainCtrl, use main.bot.$bot? 45 | - [ ] Change save rate to time based not turns? 46 | - [ ] Better handling Aether errors 47 | - [ ] Try other Aether supported languages? 48 | - [ ] Upgrade units using heavy resources? 49 | - [x] $bot.find should search both tile and name. 50 | - [ ] Wildcard? 51 | - [ ] Indicate bot's current target, (and heading, and action?) 52 | - [ ] Set target using map? 53 | - [ ] d3 hover/click dispatch 54 | - [x] Bots 55 | - [ ] Tiles 56 | - [x] Pan to unit on select 57 | - [ ] Indicate stuck bots? 58 | - [x] Ensure base starts on plain 59 | - [ ] Clean up classes -> Entity component system 60 | - [x] Implement $bot.log() 61 | - [ ] Improve drawing more? 62 | - [ ] Re-balance gameplay. 63 | - [ ] More/better Tooltips? 64 | - [ ] Need to indicate when unit is on mine. 65 | - [ ] Indicate overlapping units. 66 | - [ ] Clean terminology (bots, resources, energy units, etc) 67 | 68 | ## Decisions 69 | - [-] Puase scripts, keep charging? 70 | - [x] Start with full energy? 71 | - [ ] Can bot scripts command other bots? $bot.find('A').moveTo($bot.x,$bot.y)? 72 | - [ ] Can bot scripts access other bot's memory? $bot.find('Base').mem? 73 | - [ ] Limit map and number of bots for performance? 74 | - [ ] Limit FPS in firefox? 75 | - [ ] Map full screen? 76 | - [ ] Work on small screens. Responsive design. 77 | - [ ] Bots can carry other bots? 78 | - [ ] End game shows easter egg? 79 | - [x] Start with rover, upgrade to base? Start with base, build first rover? 80 | - [-] Use ui-router? 81 | - [x] Add about screen. 82 | - [ ] Faster bot (drone)? mE = mS = 1 (Can never upgrade using current rules) 83 | - [ ] Allow scripting of change script? Change, halt, include. 84 | - [x] Upgrade self or upgrade in base? 85 | - [ ] replace progress bars (with sparkline?) 86 | - [ ] add progress circles/bars around units? 87 | - [x] User rename bots? 88 | - [ ] User assign labels (A,B,C,...,@)? 89 | 90 | ## Wish list 91 | - [ ] More terrain types (various movement costs?), biomes. 92 | - [ ] Mining speed/efficency 93 | - [ ] Ensure starting position is not locked 94 | - [ ] Environmental effects (Solar intensity, etc) 95 | - [ ] Async Programming? Support ES6 in user scripts? 96 | - [ ] Aliens? Alien tech? 97 | - [ ] Tech tree 98 | - [ ] Multi-Language support 99 | - [ ] Use $animate 100 | - [ ] Environment modifications. 101 | 102 | ## Bot upgrades 103 | - [ ] Improve Scanner range? 104 | - [ ] Increase mining efficiency? 105 | - [ ] Improve engine? 106 | - [ ] Mountain borer? 107 | - [ ] Flight? 108 | -------------------------------------------------------------------------------- /app/components/app.css: -------------------------------------------------------------------------------- 1 | /* Space out content a bit */ 2 | body { 3 | padding-top: 20px; 4 | padding-bottom: 20px; 5 | overflow: hidden; 6 | } 7 | 8 | /* Everything but the jumbotron gets side spacing for mobile first views */ 9 | .header, 10 | .marketing, 11 | .footer { 12 | padding-left: 15px; 13 | padding-right: 15px; 14 | } 15 | 16 | /* Custom page header */ 17 | .header { 18 | border-bottom: 1px solid #e5e5e5; 19 | } 20 | /* Make the masthead heading the same height as the navigation */ 21 | .header h3 { 22 | margin-top: 0; 23 | margin-bottom: 0; 24 | line-height: 40px; 25 | padding-bottom: 19px; 26 | } 27 | 28 | /* Custom page footer */ 29 | .footer { 30 | padding-top: 19px; 31 | color: #777; 32 | /* border-top: 1px solid #e5e5e5; */ 33 | } 34 | 35 | /* Customize container */ 36 | @media (min-width: 768px) { 37 | .container { 38 | /* max-width: 730px; */ 39 | } 40 | } 41 | 42 | .container { 43 | min-width: 970px; 44 | } 45 | 46 | .container-narrow > hr { 47 | margin: 30px 0; 48 | } 49 | 50 | /* Responsive: Portrait tablets and up */ 51 | @media screen and (min-width: 768px) { 52 | /* Remove the padding we set earlier */ 53 | .header, 54 | .marketing, 55 | .footer { 56 | padding-left: 0; 57 | padding-right: 0; 58 | } 59 | /* Space out the masthead */ 60 | .header { 61 | margin-bottom: 30px; 62 | } 63 | /* Remove the bottom border on the jumbotron for visual effect */ 64 | .jumbotron { 65 | border-bottom: 0; 66 | } 67 | } 68 | 69 | 70 | .btn { 71 | margin: 0; 72 | padding: 6px 12px; 73 | } 74 | 75 | .btn-group-vertical { 76 | margin: 5px 1px; 77 | } 78 | 79 | .btn-xs { 80 | padding: 1px 5px; 81 | } 82 | 83 | .btn-sm { 84 | padding: 5px 10px; 85 | } 86 | 87 | .btn-lg { 88 | padding: 20px 30px; 89 | } 90 | 91 | .btn-group { 92 | margin: 0; 93 | } 94 | 95 | .list-group .list-group-item { 96 | border: 1px solid #ddd; 97 | } 98 | 99 | 100 | 101 | /* Sticky footer styles 102 | -------------------------------------------------- */ 103 | 104 | .container-fluid { 105 | margin: 0 auto; 106 | height: 100%; 107 | /* min-width: 970px; */ 108 | 109 | -moz-box-sizing: border-box; 110 | -webkit-box-sizing: border-box; 111 | box-sizing: border-box; 112 | } 113 | 114 | @media (min-width: 992px) { 115 | html, body { 116 | height: 100%; 117 | } 118 | 119 | #wrap { 120 | overflow: hidden; 121 | } 122 | 123 | .container-fluid { 124 | /* min-width: 970px; */ 125 | } 126 | 127 | .column { 128 | height: 100%; 129 | width: 50%; 130 | } 131 | } 132 | 133 | @media (max-width: 992px) { 134 | 135 | #wrap { 136 | overflow-y: scroll; 137 | overflow-x: hidden; 138 | } 139 | 140 | .container-fluid { 141 | min-width: 510px; 142 | } 143 | 144 | .column { 145 | height: 500px; 146 | min-height: 500px; 147 | margin: 10px 0; 148 | } 149 | } 150 | 151 | 152 | 153 | .column { 154 | /* height: 100%; */ 155 | } 156 | 157 | #wrap { 158 | position: absolute; 159 | top: 40px; 160 | bottom: 30px; 161 | padding-top: 10px; 162 | padding-bottom: 10px; 163 | width: 100%; 164 | height: auto; 165 | } 166 | 167 | #header { 168 | position: fixed; 169 | height: 30px; 170 | top: 0; 171 | width: 100%; 172 | z-index: 1000; 173 | background-color: #f5f5f5; 174 | color: #999999; 175 | border-bottom: 1px solid #e3e3e3; 176 | } 177 | 178 | #header .btn { 179 | color: #999999; 180 | } 181 | 182 | #footer { 183 | position: fixed; 184 | height: 26px; 185 | bottom: 0; 186 | width: 100%; 187 | z-index: 1000; 188 | /* background-color: #f5f5f5; */ 189 | color: #999999; 190 | /* border-top: 1px solid #e3e3e3; */ 191 | } 192 | 193 | .introjs-tooltipReferenceLayer { 194 | z-index: 1000000; 195 | } 196 | 197 | .introjs-tooltip { 198 | min-width: 400px; 199 | } 200 | 201 | .list-group-item.introjs-fixParent { 202 | position: relative !important; 203 | } 204 | 205 | .introjs-helperNumberLayer { 206 | font-size: 8px; 207 | line-height: 14px; 208 | } 209 | 210 | /* ------- */ 211 | 212 | .help-map-key { 213 | display: inline-block; 214 | color: #000; 215 | border-radius: 5px; 216 | text-align: center; 217 | margin-right: 5px; 218 | padding: 5px 9px; 219 | font-size: 0.9em; 220 | font-family: Menlo, Monaco, Consolas, "Courier New", monospace; 221 | } 222 | 223 | .cfp-hotkeys-key { 224 | color: #000; 225 | font-size: 0.9em; 226 | font-family: Menlo, Monaco, Consolas, "Courier New", monospace; 227 | 228 | border: 1px solid #aaa; 229 | border-radius: 0.2em; 230 | -webkit-border-radius: 0.2em; 231 | -moz-border-radius: 0.2em; 232 | -o-border-radius: 0.2em; 233 | -ms-border-radius: 0.2em; 234 | -moz-box-shadow: 0.1em 0.2em 0.2em #ddd; 235 | -webkit-box-shadow: 0.1em 0.2em 0.2em #ddd; 236 | box-shadow: 0.1em 0.2em 0.2em #ddd; 237 | background-color: #f9f9f9; 238 | background-image: -moz-linear-gradient(top, #eee, #f9f9f9, #eee); 239 | background-image: -o-linear-gradient(top, #eee, #f9f9f9, #eee); 240 | background-image: -webkit-linear-gradient(top, #eee, #f9f9f9, #eee); 241 | background-image: linear-gradient(to bottom, #eee, #f9f9f9, #eee); 242 | padding: 0.1em 0.3em; 243 | } 244 | -------------------------------------------------------------------------------- /app/components/game/script-system.js: -------------------------------------------------------------------------------- 1 | ;(function() { 2 | 'use strict'; 3 | 4 | /*jshint -W109 */ 5 | var collect = 6 | "$bot.unload();\n" + 7 | "$bot.charge();\n" + 8 | "\n" + 9 | "if ($bot.S >= $bot.mS) {\n" + 10 | " var home = $bot.find('@');\n" + 11 | " $bot.moveTo(home.x,home.y);\n" + 12 | "} else if ($bot.E >= 1 && $bot.mine() === false) {\n" + 13 | " var mine = $bot.find('X');\n" + 14 | "\n" + 15 | " var x,y;\n" + 16 | " if (mine !== null) {\n "+ 17 | " x = mine.x;\n" + 18 | " y = mine.y;\n" + 19 | " } else {\n" + 20 | " x = 2*Math.random()-1+$bot.x;\n" + 21 | " y = 2*Math.random()-1+$bot.y;\n" + 22 | " }\n"+ 23 | " $bot.moveTo(x,y);\n"+ 24 | "}"; 25 | /*jshint +W109 */ 26 | 27 | //collect = collect.substring(collect.indexOf('{') + 1, collect.lastIndexOf('}')); 28 | 29 | angular.module('ePrime') 30 | .config(function(thirdPartyProvider) { 31 | thirdPartyProvider.register('Aether'); 32 | }) 33 | .constant('defaultScripts', [ // make a hash 34 | { name: 'Collect', code: collect }, 35 | { name: 'Action', code: '$bot.mine();\n$bot.charge();\n$bot.unload();' }, 36 | //{ name: 'Mine', code: '$bot.mine();' }, 37 | //{ name: 'Charge', code: '$bot.charge();' }, 38 | //{ name: 'Unload', code: '$bot.unload();' }, 39 | { name: 'Upgrade', code: '$bot.upgrade();' }, 40 | { name: 'Construct', code: '$bot.construct();' } 41 | ]) 42 | .service('aether', function(Aether) { 43 | /*jshint -W106 */ 44 | return new Aether({ 45 | executionLimit: 1000, 46 | functionName: 'tick', 47 | functionParameters: ['console','$bot','$map'], 48 | problems: { 49 | jshint_W040: {level: 'ignore'}, 50 | aether_MissingThis: {level: 'ignore'} 51 | }, 52 | noSerializationInFlow: true, 53 | includeFlow: false, 54 | includeMetrics: false, 55 | includeStyle: false, 56 | protectAPI: false, 57 | yieldAutomatically: true, 58 | protectBuiltins: false 59 | }); 60 | /*jshint +W106 */ 61 | }) 62 | .service('sandBox', function(aether, GAME, $log) { // move, create tests 63 | 64 | var sandBox = this; 65 | 66 | var $consoleInterface = { 67 | log: console.log.bind(console) 68 | }; 69 | 70 | var $mapInterface = { 71 | get: GAME.world.get.bind(GAME.world) 72 | }; 73 | 74 | sandBox.run = function(script, $bot) { 75 | 76 | if (script === null) { 77 | return; 78 | } 79 | 80 | var method; 81 | if (typeof script === 'object') { 82 | if (!script.$method) { // maybe this should be done in the editor 83 | aether.transpile(script.code); // todo: catch transpile problems here 84 | script.$method = aether.createMethod(); 85 | } 86 | method = script.$method; 87 | } else { 88 | aether.transpile(script); // todo: catch transpile problems here 89 | method = aether.createMethod(); 90 | } 91 | 92 | var result = { done: false }; 93 | try { 94 | 95 | var generator = method($consoleInterface, $bot, $mapInterface); // does this need to be done each time? 96 | aether.sandboxGenerator(generator); 97 | 98 | var c = 0; 99 | while (!result.done && c++ < 200000) { 100 | result = generator.next(); 101 | } 102 | 103 | if (!result.done) { // todo: throw error? 104 | result.error = 'User script execution limit'; 105 | } 106 | 107 | } catch(err) { 108 | var m; 109 | var a = aether.lastStatementRange; 110 | if (a && a[0]) { 111 | m = (a[0].row+1) +':' + a[0].col; 112 | } 113 | $log.debug(aether); 114 | $log.warn('User script error', err.message, m || err.stack || ''); 115 | 116 | result.error = err.message+m; 117 | } 118 | 119 | return result; 120 | 121 | }; 122 | 123 | }) 124 | .run(function($log, ngEcs, TILES, sandBox, GAME) { 125 | 126 | /* function mezclar2(arr) { // fast shuffle 127 | for (var i, tmp, n = arr.length; n; i = Math.floor(Math.random() * n), tmp = arr[--n], arr[n] = arr[i], arr[i] = tmp) {} 128 | return arr; 129 | } */ 130 | 131 | function findScript(name) { 132 | var value, list = GAME.scripts, len = list.length; 133 | for (var i = 0; i < len; i++) { 134 | value = list[i]; 135 | if (value.name === name) { 136 | return value; 137 | } 138 | } 139 | return undefined; 140 | } 141 | 142 | ngEcs.$s('scripts', { 143 | acc: 0, 144 | interval: 1, 145 | $require: ['bot','script'], 146 | $addEntity: function(e) { 147 | var script = findScript(e.script.scriptName); 148 | if (!script) { 149 | $log.error('Script not found'); 150 | } else { 151 | e.script.$script = script; 152 | } 153 | }, 154 | $update: function() { 155 | 156 | GAME.stats.turn++; 157 | 158 | var i = -1,arr = this.$family,len = arr.length,e; 159 | while (++i < len) { 160 | e = arr[i]; 161 | 162 | if( e.script.halted === true || e.script.skip === true) { 163 | e.script.skip = false; 164 | continue; 165 | } 166 | 167 | if (e.bot.E < 1) { continue; } 168 | 169 | var script = findScript(e.script.scriptName); // don't do this 170 | //var E = e.bot.E; 171 | //var ret = true; 172 | while (e.bot.E > 1) { 173 | var ret = sandBox.run(script, e.$bot); 174 | if (ret.error) { 175 | e.bot.error(ret.error); 176 | continue; 177 | } 178 | if (ret.done) { 179 | break; 180 | } 181 | } 182 | 183 | } 184 | 185 | } 186 | }); 187 | 188 | }); 189 | 190 | })(); 191 | -------------------------------------------------------------------------------- /app/components/game/game-service.js: -------------------------------------------------------------------------------- 1 | /* global FPSMeter:true */ 2 | 3 | (function() { 4 | 5 | 'use strict'; 6 | 7 | var TYPED_ARRAY_REGEXP = /^\[object (Uint8(Clamped)?)|(Uint16)|(Uint32)|(Int8)|(Int16)|(Int32)|(Float(32)|(64))Array\]$/; 8 | function isTypedArray(value) { 9 | return TYPED_ARRAY_REGEXP.test(Object.prototype.toString.call(value)); 10 | } 11 | 12 | // must start with object, 13 | // skips keys that start with $ 14 | // navigates down objects but not other times (including arrays) 15 | function ssCopy(src) { // copy objects removing $ props 16 | var dst = {}; 17 | for (var key in src) { 18 | if (src.hasOwnProperty(key) && key.charAt(0) !== '$') { 19 | var s = src[key]; 20 | if (angular.isObject(s) && !isTypedArray(s) && !angular.isArray(s) && !angular.isDate(s)) { 21 | dst[key] = ssCopy(s); 22 | } else if (typeof s !== 'function') { 23 | dst[key] = s; 24 | } 25 | } 26 | } 27 | return dst; 28 | } 29 | 30 | angular.module('ePrime') 31 | .factory('fpsmeter', function() { // should be a directive 32 | var fpsmeter = new FPSMeter({ decimals: 0, graph: true, theme: 'dark', left: '5px', top: '30px' }); 33 | fpsmeter.$hide = true; 34 | fpsmeter.hide(); 35 | return fpsmeter; 36 | }) 37 | .service('GAME', function($log, $localForage, debounce, ngEcs, World, Chunk, TILES, defaultScripts, fpsmeter, $modal) { 38 | 39 | var GAME = this; // todo: GAME === ngEcs 40 | 41 | GAME.ecs = ngEcs; // eventually GAME === ngEcs 42 | ngEcs.$interval = 0.1; 43 | GAME.dT = 1; 44 | 45 | GAME.scripts = angular.copy(defaultScripts); // setup? 46 | 47 | function _nameOrTile(_) { 48 | return function(d) { 49 | d = d.bot; 50 | return d.t === _ || d.name === _; 51 | }; 52 | } 53 | 54 | GAME.scanList = function(_) { // move? 55 | var bots = (!_) ? this.bots : this.bots.filter(_nameOrTile(_)); 56 | var tiles = this.world.findTiles(_); 57 | return tiles.concat(bots); 58 | }; 59 | 60 | GAME.findBotAt = function(_,x,y) { // move 61 | var len = this.bots.length, fn = _nameOrTile(_); 62 | for (var i = 0; i < len; i++) { 63 | var bot = this.bots[i]; 64 | if (fn(bot) && bot.bot.x === x && bot.bot.y === y) { 65 | return bot; 66 | } 67 | } 68 | return null; 69 | }; 70 | 71 | GAME.save = function() { 72 | 73 | var G = { // eventually G = ssCopy(GAME); 74 | stats: ssCopy(GAME.stats), 75 | world: ssCopy(GAME.world), 76 | entities: ssCopy(ngEcs.entities), 77 | scripts: GAME.scripts.map(ssCopy), 78 | //chunks: ssCopy(GAME.world.$$chunks) // todo: entities? 79 | }; 80 | 81 | //localStorageService.set('saveGame', G); 82 | return $localForage.setItem('saveGame', G).then(function() { 83 | GAME.stats.saved = new Date(); 84 | $log.debug('saved'); 85 | }); 86 | 87 | }; 88 | 89 | GAME.load = function() { 90 | 91 | return $localForage.getItem('saveGame').then(function(G) { // trap errors 92 | if (!G) { 93 | $log.debug('saveGame not found'); 94 | return GAME.start(); 95 | } 96 | 97 | $log.debug('game loaded',arguments); 98 | 99 | angular.copy(G.scripts, GAME.scripts); 100 | 101 | if (!G.entities || !G.world) { 102 | $log.debug('saveGame not found'); 103 | return GAME.start(); 104 | } 105 | 106 | angular.extend(GAME.world, G.world); 107 | angular.extend(GAME.stats, G.stats); 108 | 109 | //GAME.world.$$chunks = {}; 110 | //angular.forEach(G.chunks, function(chunk, key) { // todo: make Chunk a component 111 | // GAME.world.$$chunks[key] = new Chunk(chunk.view, chunk.X, chunk.Y); 112 | //}); 113 | 114 | angular.forEach(G.entities, function(value, key) { 115 | ngEcs.$e(key, value); 116 | }); 117 | 118 | 119 | 120 | //G.entities.forEach(ngEcs.$e, ngEcs); 121 | 122 | GAME.bots = GAME.ecs.families.bot;  // get rid 123 | 124 | //console.log(GAME.bots); 125 | 126 | }); 127 | 128 | }; 129 | 130 | GAME.clear = function() { 131 | return $localForage.setItem('saveGame', { // save only scripts 132 | scripts: GAME.scripts.map(ssCopy) 133 | }); 134 | }; 135 | 136 | GAME.setup = function() { 137 | 138 | if (GAME.world) { 139 | GAME.world.$$chunks = {}; 140 | } else { 141 | GAME.world = new World(60); 142 | } 143 | 144 | GAME.stats = GAME.ecs.stats = { 145 | E: 0, 146 | S: 0, 147 | turn: 0, 148 | start: new Date(), 149 | saved: new Date() 150 | }; 151 | 152 | return GAME; 153 | 154 | }; 155 | 156 | GAME.start = function() { 157 | var home = ngEcs.$e({ 158 | $bot: {}, 159 | bot: { 160 | name: 'Base', 161 | x: 30, 162 | y: 10, 163 | E: 10, 164 | $game: GAME 165 | }, 166 | action: {}, 167 | render: {}, 168 | active: true 169 | }); 170 | 171 | GAME.bots = ngEcs.families.bot; // get rid of this 172 | 173 | GAME.world.scanRange(home.bot, 2); 174 | 175 | var chunk = GAME.world.getChunk(home.bot.x, home.bot.y); 176 | 177 | if (chunk.get(home.bot.x, home.bot.y) === 'X') { // hack until mines become entities 178 | chunk.set(home.bot.x, home.bot.y, TILES.FIELD); 179 | } 180 | 181 | return $modal.open({ 182 | keyboard: false, 183 | backdrop: 'static', 184 | templateUrl: 'components/modals/start-model.html' 185 | }).result.then(function (ret) { 186 | GAME.tutorial = ret; 187 | }); 188 | 189 | }; 190 | 191 | ngEcs.$s('fpsMeter', { 192 | $update: function() { 193 | fpsmeter.tick(); 194 | } 195 | }); 196 | 197 | ngEcs.$s('saveGame', { 198 | interval: 60, 199 | $update: function() { 200 | $log.debug('game saved'); 201 | GAME.save(); 202 | } 203 | }); 204 | 205 | /* ngEcs.$s('tick', { 206 | acc: 0, 207 | $update: function(dt) { 208 | fpsmeter.tick(); 209 | 210 | this.acc += dt; 211 | if (this.acc < GAME.dT) { return; } 212 | this.acc = 0; 213 | GAME.stats.turn++; 214 | } 215 | }); */ 216 | 217 | //GAME.takeTurn = function() { // system 218 | // ngEcs.$update(ngEcs.$interval); 219 | //}; 220 | 221 | GAME.setup(); 222 | 223 | }); 224 | 225 | })(); 226 | -------------------------------------------------------------------------------- /app/index.html.tpl: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | ε-prime, a (4-1)εX programming game 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 35 | 36 |
37 | 38 | 39 | 48 | 49 | 50 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | -------------------------------------------------------------------------------- /app/components/main/grid.js: -------------------------------------------------------------------------------- 1 | /* global _F:true */ 2 | /* global d3:true */ 3 | /*jshint -W040 */ 4 | 5 | (function() { 6 | 7 | 'use strict'; 8 | 9 | d3.charts = d3.charts || {}; 10 | 11 | d3.charts.Grid = function Grid() { // TODO: move 12 | var margin = {top: -5, right: -5, bottom: -5, left: -5}, 13 | width = 500, 14 | height = 500; 15 | 16 | var _tile = _F('t'); 17 | //var _x = _F('x'); 18 | //var _y = _F('y'); 19 | 20 | var xScale = d3.scale.linear() 21 | .range([0, width]) 22 | .domain([0,60]); 23 | var yScale = d3.scale.linear() 24 | .range([0, height]) 25 | .domain([0,60]); 26 | 27 | var dx = xScale(1); //, dy = yScale(1); 28 | 29 | var _X = _F('x', xScale), 30 | _Y = _F('y', yScale); 31 | 32 | //var _pos = function(d) { 33 | // return [_X(d),_Y(d)]; 34 | //}; 35 | 36 | //var _posId = function(d) { 37 | // return '@'+[d.x,d.y]; 38 | //}; 39 | 40 | var _translate = function(d) { 41 | return 'translate('+[_X(d),_Y(d)]+')'; 42 | }; 43 | 44 | var _id = _F('_id'); 45 | var _botTile = _F('bot.t'); 46 | 47 | var textAttr = { 48 | 'text-anchor': 'middle', 49 | 'alignment-baseline': 'middle', 50 | x: 0, 51 | y: 0 52 | }; 53 | 54 | var container, svg, gBotsLayer, gTilesLayer, text; 55 | 56 | function zoomed() { 57 | container.attr('transform', 'translate(' + d3.event.translate + ')scale(' + d3.event.scale + ')'); 58 | } 59 | 60 | var zoom = d3.behavior.zoom() 61 | .scaleExtent([0.5, 10]) 62 | .on('zoom', zoomed); 63 | 64 | var dispatch = d3.dispatch('click'); 65 | 66 | function clicked(d) { // TODO: dispatch 67 | dispatch.click(d); 68 | } 69 | 70 | function hover(d) { 71 | var t = d.name || ''; 72 | text.text(t+'@'+[d.x,d.y]); 73 | } 74 | 75 | function my(selection) { 76 | selection.each(function(d) { 77 | 78 | var chunks = d[0]; // TODO: not this 79 | var bots = d[1]; 80 | 81 | if (!container) { 82 | //$log.debug('draw new'); 83 | 84 | width = selection[0][0].clientWidth || width; 85 | width = width - margin.left - margin.right; 86 | 87 | height = selection[0][0].clientHeight || height; 88 | height = height - margin.top - margin.bottom; 89 | 90 | svg = d3.select(this) 91 | .append('svg') 92 | .attr('width', width) 93 | .attr('height', height) 94 | .append('g') 95 | .attr('transform', 'translate(' + margin.left + ',' + margin.right + ')') 96 | .call(zoom); 97 | 98 | svg.append('rect') 99 | .attr('width', width) 100 | .attr('height', height) 101 | .style('fill', 'none') 102 | .style('pointer-events', 'all'); 103 | 104 | container = svg.append('g'); 105 | 106 | gTilesLayer = container.append('g').attr('class','tilesLayer'); 107 | gBotsLayer = container.append('g').attr('class','botsLayer'); 108 | 109 | text = svg.append('g') 110 | .attr('class','hover-text') 111 | .attr('transform', 'translate(10,20)') 112 | .append('text') 113 | .attr({ 114 | 'text-anchor': 'start', 115 | 'alignment-baseline': 'top', 116 | x: 0, 117 | y: 0 118 | }) 119 | .text(''); 120 | 121 | } 122 | 123 | var gChunkWrap; 124 | function drawChunks(chunks) { 125 | 126 | gChunkWrap = gTilesLayer 127 | .selectAll('.chunk').data(chunks, _id); 128 | 129 | gChunkWrap.enter() 130 | .append('g') 131 | .attr('transform', 'translate(0,0)') 132 | .attr('class','chunk'); 133 | 134 | updateChunks(); 135 | 136 | } 137 | 138 | function updateChunks() { 139 | 140 | gChunkWrap 141 | .each(_renderChunk); 142 | 143 | } 144 | 145 | 146 | 147 | function _renderChunk(d) { 148 | if (d.chunk.$hash === 0) { return; } // dirty check 149 | d.chunk.$hash = 0; 150 | 151 | var tiles = d.chunk.getTilesArray(); 152 | 153 | var tilesWrap = d3.select(this) 154 | .selectAll('.tile').data(tiles); 155 | 156 | tilesWrap.enter() 157 | .append('g') 158 | .attr('class', 'tile') 159 | //.attr('transform', _translate) 160 | .on('click', clicked) 161 | .on('mouseenter', hover) 162 | .on('mouseleave', function() { 163 | text.text(''); 164 | }) 165 | .append('text') 166 | .attr(textAttr) 167 | //.text(_tile) 168 | ; 169 | 170 | tilesWrap // shouldn't need to do this but tiles change order and text 171 | .attr('transform', _translate) 172 | .select('text') 173 | .text(_tile); 174 | } 175 | 176 | var botsWrap; 177 | function drawBots(bots, dT) { 178 | botsWrap = gBotsLayer 179 | .selectAll('.bot').data(bots, _id); 180 | 181 | var gBotsEnter = botsWrap.enter() 182 | .append('g') 183 | .attr('class', function() { 184 | return 'bot'; 185 | }) 186 | .on('click', clicked) 187 | .on('mouseenter', hover) 188 | .on('mouseleave', function() { 189 | text.text(''); 190 | }) 191 | .attr('transform', function(d) { 192 | return _translate(d.bot); 193 | }); 194 | 195 | gBotsEnter 196 | .append('circle') 197 | .attr({ 198 | r: 1.2*dx, 199 | cx: 0, 200 | cy: 0 201 | }); 202 | 203 | gBotsEnter 204 | .append('text') 205 | .attr(textAttr); 206 | 207 | botsWrap.exit().remove(); 208 | 209 | updateBots(dT); 210 | 211 | } 212 | 213 | function updateBots() { 214 | 215 | botsWrap 216 | .classed('active', function(d) { 217 | return d.active; 218 | }) 219 | .select('text') 220 | .text(_botTile); 221 | 222 | botsWrap 223 | //.transition().duration(dT > 0 ? dT : 250) 224 | .attr('transform', function(d) { 225 | return _translate(d.bot); 226 | }); 227 | 228 | } 229 | 230 | drawChunks(chunks); 231 | drawBots(bots); 232 | 233 | my.drawBots = drawBots; 234 | my.updateBots = updateBots; 235 | //my.renderTiles = renderTiles; 236 | my.drawChunks = drawChunks; 237 | my.updateChunks = updateChunks; 238 | 239 | my.zoomTo = function(x,y) { 240 | var s = zoom.scale(); 241 | x = -xScale(x)*s+width/2; 242 | y = -yScale(y)*s+height/2; 243 | zoom.translate([x,y]).event(svg); 244 | }; 245 | 246 | }); 247 | } 248 | 249 | d3.rebind(my, dispatch, 'on'); 250 | 251 | return my; 252 | }; 253 | 254 | })(); 255 | -------------------------------------------------------------------------------- /app/components/panels/bot-panel.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | #{{bot._id}} 4 | {{bot.bot.t}} 5 |
6 |
7 |

8 | 9 | 10 | {{ bot.bot.name }} 11 | 12 | {{ bot.bot.name }} 13 | 14 | ! 15 |

16 | 17 |
18 |
19 | 20 |
-{{bot.bot.moveCost | number : 0}} [J/km]
21 | 22 |
+{{bot.bot.chargeRate | number : 2}} [J/s]
23 |
24 |
25 | 26 |
27 | 28 |
29 | 30 | 31 | {{bot.bot.E | number : 1}} / {{bot.bot.mE | number : 0}} [J] 32 |   33 | 34 |
35 | 36 | 37 |
38 | 39 | {{bot.bot.S}} / {{bot.bot.mS}} [kg] 40 | 41 |
42 |
43 |

44 |

45 |
46 | 47 |
48 |
49 | 56 | 57 | 61 | 62 | 63 | 66 | 67 |
68 |
69 | 70 | 76 | 77 |
78 |
79 | 86 | 93 | 94 |
95 |
96 | 97 |
98 | 99 |
100 |
101 | 102 | 103 |
104 |
105 | Movement
106 | 107 | 108 | 109 |

110 | 111 | 112 | 113 |

114 | 115 | 116 | 117 |

118 |

119 |
120 | 121 | Script 122 | 123 |
124 | 128 | 132 |
133 | 134 |
135 | 136 |
137 |
138 | 139 |
140 | 144 |
145 |
146 | {{alert.msg}} 147 | 148 | 149 |
150 |
151 | 152 |
Energy: / fill 153 |
Storage: / fill 154 |
155 |
156 |
157 | 158 | 159 |
160 | 161 | 162 |
163 | 164 |
165 | 166 | 167 | 168 | 169 |
170 | -------------------------------------------------------------------------------- /app/components/main/main-controller.js: -------------------------------------------------------------------------------- 1 | /* _global _F:true */ 2 | /*jshint -W003 */ 3 | /*jshint -W040 */ 4 | 5 | (function() { 6 | 7 | 'use strict'; 8 | 9 | angular.module('ePrime') 10 | .controller('MainCtrl', function ($scope, $compile, $log, $route, $window, $modal, hotkeys, modals, siteConfig, isAt, sandBox, fpsmeter, gameIntro, TILES, GAME) { 11 | 12 | var main = this; 13 | 14 | main.dT = 6; // 1/time between autoturns, move? 15 | main.cheat = false; 16 | main.game = GAME; 17 | main.bots = GAME.bots; // get rid of this 18 | main.intro = gameIntro; 19 | 20 | main.bots.forEach(function(bot) { 21 | if (bot.active) { 22 | main.bot = bot; // get rid of this 23 | } 24 | }); 25 | 26 | $scope.autoStart = !!GAME.tutorial; 27 | delete GAME.tutorial; 28 | 29 | GAME.ecs.$start(); 30 | //main.play(main.dT); 31 | 32 | /* cheat */ 33 | if (siteConfig.debug) { 34 | hotkeys.bindTo($scope) 35 | .add({ 36 | combo: 'f', 37 | //description: '', 38 | callback: function() { 39 | main.cheat = true; 40 | GAME.bots.forEach(function(d) { 41 | d.bot.E = d.bot.mE; 42 | }); 43 | } 44 | }) 45 | .add({ 46 | combo: 'g', 47 | //description: '', 48 | callback: function() { 49 | main.cheat = true; 50 | main.game.world.scanRange(main.bot.bot.x,main.bot.bot.y,40); 51 | //d3Draw(); 52 | } 53 | }); 54 | } 55 | 56 | const SUCCESS = { done: true }; 57 | 58 | main.wait = function() { 59 | var e = main.bot; 60 | 61 | e.action.push(function() { 62 | return SUCCESS; 63 | }); //noop 64 | 65 | //step(); 66 | }; 67 | 68 | main.move = function(dx,dy) { 69 | 70 | //main.bot.action.clear(); 71 | 72 | main.bot.action.push(function move(e) { 73 | if (e.bot.E >= e.bot.moveCost || e.bot.moveCost >= e.bot.mE) { 74 | e.bot.move(dx,dy); 75 | return SUCCESS; 76 | } 77 | return { next: move }; 78 | }); 79 | 80 | //step(); 81 | }; 82 | 83 | main.action = function(e) { 84 | e = e || main.bot; 85 | 86 | e.action.push(function(e) { 87 | var $bot = e.$bot; 88 | $bot.unload(); 89 | $bot.charge(); 90 | $bot.mine(); 91 | return SUCCESS; 92 | }); 93 | 94 | //step(); 95 | }; 96 | 97 | main.mine = function(e) { // used, move? Use action script? 98 | e = e || main.bot; 99 | 100 | e.action.push(function mine(e) { 101 | var $bot = e.$bot; 102 | if (e.bot.E >= 1) { 103 | $bot.mine(); 104 | return SUCCESS; 105 | } 106 | return { next: mine }; 107 | }); 108 | 109 | //step(); 110 | }; 111 | 112 | /* main.mine = function(bot) { // used, move 113 | bot = bot || main.bot; 114 | while(bot.bot.E > 1 && bot.$bot.mine() !== false) {} // mine untill done? 115 | step(); 116 | }; 117 | 118 | main.run = function(code) { // used in bot panel, move? 119 | var ret = sandBox.run(code, main.bot.$bot); 120 | step(); 121 | if (ret !== true) { 122 | main.bot.bot.error(ret); 123 | } 124 | }; */ 125 | 126 | //function step() { 127 | //GAME.ecs.$systems.action.acc = GAME.dT; 128 | //if (main.dT === 0) { 129 | //main.takeTurn(); 130 | // GAME.ecs.$fps = 30; 131 | // GAME.ecs.$start(); 132 | //} 133 | //} 134 | 135 | var d = [ // move somewhere else, combine with directions list in botcomponent 136 | [ 'q' ,'NW',-1,-1], 137 | [ 'w' ,'N' , 0,-1], 138 | [ 'e' ,'NE', 1,-1], 139 | [ 'a' ,'W',-1, 0], 140 | [ 'd' ,'E' , 1, 0], 141 | [ 'z' ,'SW',-1, 1], 142 | [ 'x' ,'S' , 0, 1], 143 | [ 'c' ,'SE', 1, 1] 144 | ]; 145 | 146 | /* bot directions */ // move somewhere else? 147 | d.forEach(function(k) { 148 | hotkeys.bindTo($scope) 149 | .add({ 150 | combo: k[0], 151 | //description: '', 152 | callback: function() { 153 | main.move(k[2],k[3]); 154 | } 155 | }); 156 | }); 157 | 158 | /* bot actions */ // move somewhere else 159 | hotkeys.bindTo($scope) 160 | .add({ 161 | combo: 's', 162 | description: 'Action (Unload/load/mine)', 163 | callback: function() { 164 | main.action(); 165 | } 166 | }) 167 | .add({ 168 | combo: ',', 169 | description: 'Mine', 170 | callback: function() { 171 | main.mine(); 172 | } 173 | }) 174 | .add({ 175 | combo: '.', 176 | description: 'Pass (take turn)', 177 | callback: function() { 178 | main.wait(); 179 | } 180 | }) 181 | .add({ 182 | combo: 'S', 183 | description: 'Save game', 184 | callback: function() { 185 | GAME.save(); 186 | return false; 187 | } 188 | }) 189 | .add({ 190 | combo: '?', 191 | description: 'Show / hide this help menu', 192 | callback: function() { 193 | main.help(); 194 | } 195 | }) 196 | .add({ 197 | combo: '~', 198 | description: 'Show / hide FPS meter', 199 | callback: function() { 200 | if (fpsmeter.$hide) { 201 | fpsmeter.show(); 202 | } else { 203 | fpsmeter.hide(); 204 | } 205 | fpsmeter.$hide = !fpsmeter.$hide; 206 | } 207 | }); 208 | 209 | main.botChange = function(bot) { 210 | main.bots.forEach(function(_bot) { 211 | _bot.active = (_bot === bot); 212 | if (_bot.active) { 213 | main.bot = bot; // get rid of this 214 | } 215 | }); 216 | }; 217 | 218 | function reset() { 219 | main.play(0); 220 | GAME.clear().then(function() { 221 | $window.location.reload(); 222 | }); 223 | } 224 | 225 | function pauseDialog(message,showReset) { 226 | freeze(); 227 | 228 | modals.pauseConfirm(message,showReset).result 229 | .then(reset, function() { 230 | GAME.save(); 231 | unfreeze(); 232 | }); 233 | } 234 | 235 | var _dT; 236 | function freeze() { 237 | _dT = main.dT; 238 | main.play(0); 239 | 240 | hotkeys.pause(); 241 | GAME.ecs.$stop(); 242 | } 243 | 244 | function unfreeze() { 245 | main.play(_dT); 246 | hotkeys.unpause(); 247 | GAME.ecs.$start(); 248 | } 249 | 250 | main.help = function(template) { 251 | freeze(); 252 | 253 | modals.openHelp(template).result 254 | .then(null, unfreeze); 255 | }; 256 | 257 | main.reset = function() { 258 | pauseDialog('

Are you sure?

', true); 259 | }; 260 | 261 | main.relocate = function(e) { // used, move? 262 | if (e.bot.canRelocate()) { // do I really need to check? 263 | pauseDialog('

Congratulations

You have set off to explore another planet.

', true); 264 | } 265 | }; 266 | 267 | main.save = function() { 268 | GAME.save().then(function() { 269 | pauseDialog('

Game saved.

You can safely close this tab

', false); 270 | }); 271 | }; 272 | 273 | main.pause = function() { 274 | pauseDialog('', false); 275 | }; 276 | 277 | main.showScripts = function(_) { 278 | 279 | if (typeof _ === 'string') { // move to dialog? 280 | GAME.scripts.forEach(function(d, i) { // improve this 281 | if (d.name === _) { 282 | _ = i; 283 | } 284 | }); 285 | } 286 | 287 | freeze(); 288 | 289 | function done() { 290 | GAME.save(); 291 | unfreeze(); 292 | } 293 | 294 | $modal.open({ 295 | templateUrl: 'components/editor/editor.html', 296 | size: 'lg', 297 | backdrop: 'static', 298 | controller: 'EditorCtrl as editor', 299 | resolve: { 300 | initialScriptId: function() { return _; } 301 | } 302 | }).result.then(done,done); 303 | }; 304 | 305 | main.takeTurn = function() { 306 | GAME.ecs.systems.scripts.acc = 1/main.dT; 307 | }; 308 | 309 | main.play = function(_dT) { 310 | main.dT = _dT; 311 | GAME.ecs.systems.scripts.interval = 1/_dT; // move GAME.dT to scripts systems? 312 | }; 313 | 314 | 315 | 316 | }); 317 | 318 | })(); 319 | -------------------------------------------------------------------------------- /app/components/main/gameIntro-service.js: -------------------------------------------------------------------------------- 1 | /* _global _F:true */ 2 | /*jshint -W003 */ 3 | /*jshint -W040 */ 4 | 5 | (function() { 6 | 7 | 'use strict'; 8 | 9 | angular.module('ePrime') 10 | .service('gameIntro', function($rootScope, ngEcs) { 11 | var gameIntro = this; 12 | 13 | gameIntro.beforeChange = beforeChange; 14 | gameIntro.afterChange = afterChange; 15 | gameIntro.counter = 1; 16 | 17 | var bots = ngEcs.families.bot; 18 | var startingUnit = bots[0].bot; 19 | 20 | var steps = [ 21 | //{ 22 | // intro: '

Welcome to Epsilon-prime

In Epsilon-prime your goal is to conquer the planet of ε-prime. You do this by commanding an army of bots to explore and exploit the resources of ε-prime. You can control your bots individually using your mouse and keyboard or by using command scripts written in JavaScript. The game begins with a simple (and very inefficient) set of scripts for exploring and collecting resources. Using just these scripts you could complete this demo in ~2,500 turns. But you can you do better!' 23 | //}, 24 | { element: '#left-panel', 25 | intro: 'The game map is located on the left. Use the mouse and scroll wheel (or touch screen) to pan and zoom the map. The A mark is your starting unit.', 26 | position: 'right' 27 | },{ 28 | element: '#list', 29 | intro: 'On the right is your units list.', 30 | position: 'left' 31 | },{ 32 | element: '.list-group-item:nth-child(1)', 33 | intro: 'At this time you have one unit. Here also the starting unit is identified by the A symbol.', 34 | position: 'left' 35 | },{ 36 | element: '.list-group-item:nth-child(1) .energy-bar', 37 | intro: 'This progress bar indicates the unit’s energy and energy storage capacity. The unit begins with no energy but can harvest upto 10 J. Energy is needed to move and collect resources.', 38 | position: 'left' 39 | },{ 40 | element: '#play-buttons', 41 | intro: 'Press the wait key . or use use the button to advance several turns.', 42 | position: 'right' 43 | },{ 44 | onafterchange: function() { 45 | if (startingUnit.E < 2) { 46 | this.previousStep().refresh(); 47 | } 48 | }, 49 | element: '.list-group-item:nth-child(1) .energy-bar', 50 | intro: 'Your unit’s energy will increase.', 51 | position: 'left' 52 | },{ 53 | element: '#movement-buttons', 54 | intro: 'You can now begin exploring the map using the q-c keys. The letters qweadzxc are directions of movement (q for North West, c for South East, etc). Imagine your unit is located at the action key s on your keyboard. ', 55 | position: 'right' 56 | },{ 57 | element: '.list-group-item:nth-child(1) .energy-bar', 58 | intro: 'You will notice that the energy depletes as you move. This is because this unit\'s movement cost is greater than its recharge rate.', 59 | position: 'left' 60 | },{ 61 | element: '.list-group-item:nth-child(1) .energy-cost', 62 | intro: 'Above the energy indicator you will find the units movement cost and charging rate.', 63 | position: 'left' 64 | },{ 65 | element: '.list-group-item:nth-child(1) .energy-cost .movement-cost', 66 | intro: 'Notice that the unit requires 1 J to move one space.', 67 | position: 'left' 68 | },{ 69 | element: '.list-group-item:nth-child(1) .energy-cost .recharge-rate', 70 | intro: 'The unit recharges at 5 J per second. At this rate a unit can move five (5) spaces every second, not counting stored energy.', 71 | position: 'left' 72 | },{ 73 | element: '#left-panel', 74 | intro: 'If you encounter an X on the map this is a resource cache (or mine). Collect resources using the action key s.', 75 | position: 'right' 76 | },{ 77 | element: '.list-group-item:nth-child(1) .storage-bar', 78 | intro: 'This progress bar indicates a unit’s resources and storage capacity. Resources are used to upgrade units or construct new units.', 79 | position: 'left' 80 | },{ 81 | element: '.list-group-item:nth-child(1) .upgrade-button', 82 | intro: 'Upgrading units costs 10 kg. You should pause the tutorial now and explore. Return to the tutrial when you have upgraded your unit.

If you continue the tutorial now we will automatically upgrade your unit. Normally this would cost resources that you need to collect.', 83 | position: 'left' 84 | },{ 85 | onafterchange: function() { 86 | if (startingUnit.mS < 20) { 87 | startingUnit.S = 20; 88 | startingUnit.upgrade(); 89 | } 90 | }, 91 | element: '.list-group-item:nth-child(1) .energy-cost', 92 | intro: 'Notice that the movement cost and recharge rate are both higher after upgrading. Now the unit requires a more turns to move one space even though recharge rate is higher. Also notice that the unit is indicated with a @', 93 | position: 'left' 94 | },{ 95 | element: '.list-group-item:nth-child(1) .construct-button', 96 | intro: 'Once a unit has a storage capacity greater than 20 it is able to construct new units. Constructing new units costs 20 kg. You should pause the tutorial and continue exploring to collect 20 kg of storage.
If you continue the tutorial we will construct a new unit for you. Again this would normally cost resources that you must to collect.', 97 | position: 'left' 98 | },{ 99 | onbeforechange: function() { 100 | if (bots.length < 2) { 101 | startingUnit.S = 20; 102 | startingUnit.construct(); 103 | } 104 | }, 105 | element: '.list-group-item:nth-child(2)', 106 | intro: 'Your new unit will appear in the list...', 107 | position: 'left' 108 | },{ 109 | element: '#left-panel', 110 | intro: 'and on the map indicated on with an A.', 111 | position: 'right' 112 | },{ 113 | element: '.list-group-item:nth-child(2) .energy-cost', 114 | intro: 'Notice that the movement cost and recharge rate are again low.', 115 | position: 'left' 116 | },{ 117 | element: '.list-group-item:nth-child(2)', 118 | intro: 'Small units can also charge from larger units. Make the new unit active unit by clicking the A in the bot list...', 119 | position: 'left' 120 | },{ 121 | element: '.list-group-item:nth-child(2)', 122 | intro: 'Now press the action key s to charge the Rover using the Base’s energy. This is the action key. It is also used to unload any unit storage to the base and to mine resources.', 123 | position: 'left', 124 | onafterchange: function() { 125 | if (!bots[1].active) { 126 | this.previousStep(); 127 | } 128 | } 129 | },{ 130 | element: '.list-group-item:nth-child(1)', 131 | intro: 'You can use this dropdown to set a bots automatic actions each turn. Select \'Construct\' for the base...', 132 | position: 'left' 133 | }, 134 | { element: '.list-group-item:nth-child(2)', 135 | intro: 'and \'Collect\' for the bot.', 136 | position: 'left' 137 | }, 138 | { element: '#play-buttons', 139 | intro: 'Press play button to automatically cycle turns and watch your bots work autonomously.', 140 | position: 'right' 141 | }, 142 | { element: '#scripts-button', 143 | intro: 'You can modify the action scripts here.', 144 | position: 'top' 145 | }, 146 | { element: '#save-button', 147 | intro: 'Your game is automatically saved approximately every 60 seconds.', 148 | position: 'left' 149 | }, 150 | { element: '#stats-button', 151 | intro: 'Check your progress here.', 152 | position: 'left' }, 153 | { intro: 'How quickly can you collect 500 kg in the base unit?

Good luck!

' } 154 | ]; 155 | 156 | gameIntro.options = { 157 | disableInteraction: false, 158 | showStepNumbers: true, 159 | steps: steps 160 | }; 161 | 162 | function beforeChange() { 163 | var intro = this; 164 | refreshIntro(intro); 165 | var currentItem = intro._introItems[intro._currentStep]; 166 | if (currentItem.onbeforechange) { 167 | $rootScope.$apply(function() { 168 | currentItem.onbeforechange.call(intro); 169 | }); 170 | refreshIntro(intro); 171 | } 172 | } 173 | 174 | function afterChange() { 175 | var intro = this; 176 | gameIntro.counter = intro._currentStep+1; 177 | refreshIntro(intro); 178 | var currentItem = intro._introItems[intro._currentStep]; 179 | if (currentItem.onafterchange) { 180 | $rootScope.$apply(function() { 181 | currentItem.onafterchange.call(intro); 182 | }); 183 | refreshIntro(intro); 184 | } 185 | } 186 | 187 | function refreshIntro(intro) { 188 | for (var i = 0; i < intro._options.steps.length; i++) { 189 | var currentItem = intro._introItems[i]; 190 | var step = intro._options.steps[i]; 191 | if (step.element) { 192 | currentItem.element = document.querySelector(step.element); 193 | currentItem.position = step.position; 194 | } 195 | } 196 | } 197 | 198 | }); 199 | 200 | })(); 201 | -------------------------------------------------------------------------------- /app/components/game/world-factory.js: -------------------------------------------------------------------------------- 1 | /* global noise:true */ 2 | /* global d3:true */ 3 | /* global _F:true */ 4 | 5 | (function() { 6 | 'use strict'; 7 | 8 | /* Private functions */ 9 | 10 | function modulo(x,n) { // move somewhere globally usefull 11 | return ((x%n)+n)%n; 12 | } 13 | 14 | function perlin(x,y,N) { 15 | var z = 0, s = 0; 16 | for (var i = 0; i < N; i++) { 17 | var pp = 1/Math.pow(2,i)/4; 18 | var e = Math.PI/2*pp; // rotate angle 19 | var ss = Math.sin(e); 20 | var cc = Math.cos(e); 21 | var xx = (x*ss+y*cc); // rotation 22 | var yy = (-x*cc+y*ss); 23 | s += pp; // total amplitude 24 | z += pp*Math.abs(noise.perlin2(xx/pp,yy/pp)); 25 | } 26 | return 2*z/s; 27 | } 28 | 29 | function poisson(mean) { 30 | var limit = Math.exp(-mean); 31 | 32 | return function() { 33 | var n = 0, 34 | x = Math.random(); 35 | 36 | while(x > limit){ 37 | n++; 38 | x *= Math.random(); 39 | } 40 | return n; 41 | }; 42 | } 43 | 44 | angular.module('ePrime') 45 | .constant('TILES', { 46 | EMPTY: String.fromCharCode(0), 47 | MOUNTAIN: '#', 48 | FIELD: '.', 49 | MINE: 'X', 50 | HILL: ',', 51 | BOT: 'A', 52 | BASE: '@', 53 | HOLE: 'O' 54 | }) 55 | .factory('Chunk', function () { // todo: Chunk component should be view and hash, move position to position component 56 | var SIZE = 60; 57 | var LEN = 60*60; 58 | 59 | function Chunk(_, X, Y) { 60 | 61 | _ = Array.isArray(_) ? _ : LEN; 62 | this.view = new Uint8ClampedArray(_); 63 | 64 | this.X = Math.floor(X); // store offset rather than index? 65 | this.Y = Math.floor(Y); 66 | this.size = SIZE; 67 | this.$hash = 1; // start dirty 68 | } 69 | 70 | Chunk.prototype.id = function() { 71 | return this.X+','+this.Y; 72 | }; 73 | 74 | //Chunk.prototype.index = function(x,y) { // check x and y are in bounds? 75 | // x = modulo(Math.floor(x),SIZE); //Math.floor(x) % s; // not working when x < -2*s 76 | // y = modulo(Math.floor(y),SIZE); //Math.floor(y) % s; 77 | // return y*SIZE+x; 78 | //}; 79 | 80 | Chunk.prototype.get = function(x,y) { 81 | if (arguments.length === 2) { 82 | x = Chunk.getIndex(x,y); 83 | } 84 | return String.fromCharCode(this.view[x]); 85 | }; 86 | 87 | Chunk.prototype.getTile = function(x,y) { // here need to make sure I know x and y 88 | if (angular.isUndefined(y) && angular.isObject(x)) { 89 | if (angular.isUndefined(x.x) || angular.isUndefined(x.y)) { // todo 90 | throw 'Invalid object passed to Chunk.getTile'; 91 | } 92 | y = x.y; 93 | x = x.x; 94 | } 95 | var i = Chunk.getIndex(x,y); 96 | var z = String.fromCharCode(this.view[i]); 97 | return Chunk.makeTile(x,y,z); 98 | }; 99 | 100 | Chunk.prototype.set = function(x,y,z) { // check bounds 101 | if (arguments.length === 3) { 102 | x = Chunk.getIndex(x,y); 103 | } else { 104 | z = y; 105 | } 106 | this.view[x] = z.charCodeAt(0); 107 | this.$hash++; 108 | //console.log(this.$hash); 109 | return this; 110 | }; 111 | 112 | /* Chunk.prototype.getTilesArray = function() { // todo: optomise 113 | 114 | var r = []; 115 | 116 | var X = this.X*SIZE, 117 | Y = this.Y*SIZE; 118 | 119 | var XE = X+SIZE, 120 | YE = Y+SIZE; 121 | 122 | for(var x = X; x < XE; x++) { // try other way around index -> x,y 123 | for(var y = Y; y < YE; y++) { 124 | var z = this.get(x,y); // todo: optomize index calc 125 | if (z !== TILES.EMPTY) { 126 | r.push(Chunk.makeTile(x,y,z)); 127 | } 128 | } 129 | } 130 | 131 | return r; 132 | }; */ 133 | 134 | Chunk.prototype.getAllTilesArray = function() { 135 | var r = []; 136 | 137 | var X = this.X*SIZE, Y = this.Y*SIZE; 138 | 139 | var len = this.view.length, x, y, z; 140 | for(var i = 0; i < len; i++) { 141 | z = this.view[i]; 142 | z = String.fromCharCode(z); 143 | y = Math.floor(i/SIZE); 144 | x = i - y*SIZE; 145 | r.push(Chunk.makeTile(x+X,y+Y,z)); 146 | } 147 | 148 | return r; 149 | }; 150 | 151 | Chunk.prototype.getTilesArray = function() { 152 | var r = []; 153 | 154 | var X = this.X*SIZE, Y = this.Y*SIZE; 155 | 156 | var len = this.view.length, x, y, z; 157 | for(var i = 0; i < len; i++) { 158 | z = this.view[i]; 159 | if (z > 0) { 160 | z = String.fromCharCode(z); 161 | y = Math.floor(i/SIZE); 162 | x = i - y*SIZE; 163 | r.push(Chunk.makeTile(x+X,y+Y,z)); 164 | } 165 | } 166 | 167 | return r; 168 | }; 169 | 170 | Chunk.prototype.findTiles = function(_) { // list of all existing tiles, matching criteria 171 | var r = []; 172 | 173 | if (arguments.length === 1 && '#.XO'.indexOf(_) < 0) { return r; } 174 | 175 | var X = this.X*SIZE, Y = this.Y*SIZE; // store in chunk object? 176 | 177 | var len = this.view.length, x, y, z; 178 | for(var i = 0; i < len; i++) { 179 | z = this.view[i]; 180 | if (z > 0) { 181 | z = String.fromCharCode(z); 182 | if (!_ || z === _) { 183 | y = Math.floor(i/SIZE); 184 | x = i - y*SIZE; 185 | r.push(Chunk.makeTile(x+X,y+Y,z)); 186 | } 187 | } 188 | } 189 | 190 | return r; 191 | }; 192 | 193 | Chunk.getIndex = function(x,y) { // todo: if x is index and y is undefined // memoize? 194 | if (arguments.length < 2 && angular.isObject(x)) { 195 | if (angular.isUndefined(x.x) || angular.isUndefined(x.y)) { // todo 196 | throw 'Invalid object pass to Chunk.getIndex'; 197 | } 198 | y = x.y; 199 | x = x.x; 200 | } 201 | x = modulo(Math.floor(x),SIZE); //Math.floor(x) % s; // not working when x < -2*s 202 | y = modulo(Math.floor(y),SIZE); //Math.floor(y) % s; 203 | return y*SIZE+x; 204 | }; 205 | 206 | Chunk.getChunkId = function(x,y) { // memoize? 207 | if (arguments.length < 2 && angular.isObject(x)) { 208 | if (angular.isUndefined(x.x) || angular.isUndefined(x.y)) { // todo 209 | throw 'Invalid object pass to Chunk.getChunkId'; 210 | } 211 | y = x.y; 212 | x = x.x; 213 | } 214 | var X = Math.floor(x / SIZE); // chunk 215 | var Y = Math.floor(y / SIZE); 216 | return X+','+Y; 217 | }; 218 | 219 | Chunk.makeTile = function(x,y,z) { // should return a component object 220 | return {x: x, y: y, t: z}; // is s still used? 221 | }; 222 | 223 | return Chunk; 224 | }) 225 | .run(function(ngEcs, Chunk) { 226 | ngEcs.$c('chunk', Chunk); 227 | 228 | ngEcs.$s('chunks', { 229 | $require: ['chunk'], 230 | $addEntity: function(e) { 231 | if(false === e.chunk.view instanceof Uint8ClampedArray) { // ensure view is typed array.. shouldn't need this 232 | e.chunk.view = new Uint8ClampedArray(e.chunk.view); 233 | } 234 | } 235 | }); 236 | 237 | }) 238 | .factory('World', function ($log, TILES, Chunk, ngEcs) { 239 | 240 | // this should only be methods that span across chunks, everything else should be in Chunk component 241 | 242 | /* constants */ 243 | var SIZE = 60; 244 | 245 | var digYield = poisson(1.26); 246 | var mineMTTF = 0.05; 247 | 248 | var $$chunks = ngEcs.systems.chunks.$family; 249 | 250 | function World(size, seed) { // todo: remove size 251 | this.size = size = size || SIZE; // remove 252 | this.seed = seed || Math.random(); 253 | } 254 | 255 | World.prototype.getHash = function() { // used? remove shuold use hash per chunk 256 | return d3.sum($$chunks, _F('chunk.$hash')); 257 | }; 258 | 259 | /* World.prototype.getChunkId = function(x,y) { // should be in chunk? 260 | var X = Math.floor(x / SIZE); // chunk 261 | var Y = Math.floor(y / SIZE); 262 | return X+','+Y; 263 | }; */ 264 | 265 | World.prototype.getChunk = function(x,y) { // makes chunks object 266 | var id = Chunk.getChunkId(x,y); 267 | var e = ngEcs.entities[id]; 268 | if (!e) { 269 | $log.debug('new chunk',id); 270 | var chunk = new Chunk(this.size, x / this.size, y / this.size); 271 | e = ngEcs.$e(id, { chunk: chunk }); 272 | } 273 | return e.chunk; 274 | }; 275 | 276 | /* World.prototype.getIndex = function(x,y) { // used, replace with Chunk.getIndex 277 | if (angular.isObject(x)) { 278 | y = x.y; 279 | x = x.x; 280 | } 281 | var X = Math.floor(x), Y = Math.floor(y); 282 | X = X % SIZE; Y = Y % SIZE; 283 | return Y*SIZE+X; 284 | }; */ 285 | 286 | World.prototype.getHeight = function(x,y) { // move? 287 | noise.seed(this.seed); // move this 288 | return perlin((x-30)/SIZE,(y-10)/SIZE,4); 289 | }; 290 | 291 | /* World.prototype._get = function(x,y) { // returns tile 292 | return this.getChunk(x,y).get(x,y); 293 | }; 294 | 295 | World.prototype._set = function(x,y,z) { // sets tile, improve by getting index once 296 | return this.getChunk(x,y).set(x,y,z); 297 | }; */ 298 | 299 | World.prototype.scanTile = function(x,y) { // returns tile object 300 | var chunk = this.getChunk(x,y); 301 | var tile = chunk.get(x,y); // maybe chunk should return charcode 302 | 303 | if (tile === TILES.EMPTY) { // new tile 304 | var z = this.getHeight(x,y); 305 | 306 | tile = TILES.FIELD; 307 | if (z > 0.60) { 308 | tile = TILES.MOUNTAIN; 309 | } else if (Math.random() > 0.98) { 310 | tile = TILES.MINE; 311 | } 312 | 313 | chunk.set(x,y,tile); 314 | } 315 | 316 | return Chunk.makeTile(x,y,tile); // todo: improve storage, store only strings again? 317 | }; 318 | 319 | World.prototype.set = function(x,y,z) { // used? get rid of this, does't work if x is object 320 | this.getChunk(x,y).set(x,y,z); 321 | }; 322 | 323 | //function makeTile(x,y,z) { // move to chunk component 324 | // return {x: x, y: y, t: z, s: false}; 325 | //} 326 | 327 | World.prototype.get = function(x,y) { // used? rename getTile, move to Chunk, does't work if x is object 328 | return this.getChunk(x,y).getTile(x,y); 329 | }; 330 | 331 | World.prototype.scanRange = function(x,y,R) { // optimize!! 332 | if (arguments.length < 3) { 333 | R = y; 334 | y = x.y; 335 | x = x.x; 336 | } 337 | R = R || 2; 338 | var r = []; 339 | for(var i = x-R; i <= x+R; i++) { 340 | for(var j = y-R; j <= y+R; j++) { // to check range 341 | var d = Math.sqrt((x-i)*(x-i)+(y-j)*(y-j)); // euclidian? 342 | if (d < R) { 343 | var t = this.scanTile(i,j); // calls getChunk each time, improve 344 | r.push(t); 345 | } 346 | } 347 | } 348 | return r; 349 | }; 350 | 351 | World.prototype._scanList = function() { // used? 352 | var r = []; 353 | for(var i = 0; i < SIZE; i++) { 354 | for(var j = 0; j < SIZE; j++) { 355 | var t = this.get(i,j); 356 | if (t !== null && t.t !== TILES.EMPTY) { 357 | r.push(t); 358 | } 359 | } 360 | } 361 | return r; 362 | }; 363 | 364 | World.prototype.scanChunk = function(chunk) { // used? 365 | //console.log('scanChunk',chunk); 366 | 367 | var r = []; 368 | 369 | var X = chunk.X*SIZE, 370 | Y = chunk.Y*SIZE; 371 | 372 | var XE = X+SIZE, 373 | YE = Y+SIZE; 374 | 375 | for(var x = X; x < XE; x++) { // try other way around index -> x,y 376 | for(var y = Y; y < YE; y++) { 377 | var z = chunk.get(x,y); 378 | if (z !== TILES.EMPTY) { 379 | r.push(Chunk.makeTile(x,y,z)); 380 | } 381 | } 382 | } 383 | 384 | return r; 385 | }; 386 | 387 | World.prototype.findTiles = function(_) { // list of all existing tiles, matching criteria 388 | if (arguments.length < 1 && '#.XO'.indexOf(_) < 0) { return []; } 389 | 390 | var r = []; 391 | 392 | for (var k in $$chunks) { 393 | var chunk = $$chunks[k].chunk; //console.log($bot.find('.')); 394 | Array.prototype.push.apply(r,chunk.findTiles(_)); 395 | } 396 | 397 | return r; 398 | }; 399 | 400 | World.prototype.dig = function(x,y) { // move, rewrite to use Chunk.getIndex 401 | 402 | if (arguments.length === 1) { 403 | y = x.y; 404 | x = x.x; 405 | } 406 | 407 | var chunk = this.getChunk(x,y); 408 | 409 | var z = chunk.get(x,y); 410 | 411 | if (z === TILES.MINE) { 412 | 413 | var dS = digYield(); 414 | if (dS > 0 && Math.random() < mineMTTF) { 415 | chunk.set(x,y,TILES.HOLE); 416 | } 417 | return dS; 418 | } 419 | return 0; 420 | }; 421 | 422 | World.prototype.canMine = function(x,y) { // used? this should not be here? improve 423 | return this.getChunk(x,y).get(x,y) === TILES.MINE; 424 | }; 425 | 426 | World.prototype.canMove = function(x,y) { // used? move 427 | var t = this.get(x,y); 428 | return t !== null && t.t !== TILES.MOUNTAIN; 429 | }; 430 | 431 | return World; 432 | }); 433 | })(); 434 | -------------------------------------------------------------------------------- /Gruntfile.js: -------------------------------------------------------------------------------- 1 | // Generated on 2014-11-19 using generator-angular 0.9.8 2 | 'use strict'; 3 | 4 | // # Globbing 5 | // for performance reasons we're only matching one level down: 6 | // 'test/spec/{,*/}*.js' 7 | // use this if you want to recursively match all subfolders: 8 | // 'test/spec/**/*.js' 9 | 10 | module.exports = function (grunt) { 11 | 12 | // Load grunt tasks automatically 13 | require('load-grunt-tasks')(grunt); 14 | 15 | // Time how long tasks take. Can help when optimizing build times 16 | require('time-grunt')(grunt); 17 | 18 | grunt.config('env', grunt.option('env') || process.env.GRUNT_ENV || 'development'); 19 | 20 | var bowerJSON = require('./bower.json'); 21 | 22 | // Configurable paths for the application 23 | var appConfig = { 24 | app: bowerJSON.appPath || 'app', 25 | version: bowerJSON.version || '0.0.0', 26 | dist: 'dist', 27 | ga: 'UA-XXXXX-X', 28 | debug: true 29 | }; 30 | 31 | // Overrides based on environment 32 | var configFile = './'+grunt.config('env')+'_config.json'; 33 | if (grunt.file.exists(configFile)) { 34 | grunt.util._.extend(appConfig, grunt.file.readJSON(configFile)); 35 | } 36 | 37 | // Define the configuration for all the tasks 38 | grunt.initConfig({ 39 | 40 | // Project settings 41 | yeoman: appConfig, 42 | 43 | // Watches files for changes and runs tasks based on the changed files 44 | watch: { 45 | bower: { 46 | files: ['bower.json'], 47 | tasks: ['wiredep'] 48 | }, 49 | js: { 50 | files: ['<%= yeoman.app %>/components/{,*/}*.js'], 51 | tasks: ['newer:jshint:all'], 52 | options: { 53 | livereload: '<%= connect.options.livereload %>' 54 | } 55 | }, 56 | jsTest: { 57 | files: ['<%= yeoman.app %>/components/{,*/}*-spec.js'], 58 | tasks: ['newer:jshint:test', 'karma'] 59 | }, 60 | styles: { 61 | files: ['<%= yeoman.app %>/components/{,*/}*.css'], 62 | tasks: ['newer:copy:styles', 'autoprefixer'] 63 | }, 64 | template: { 65 | files: ['<%= yeoman.app %>/{,*/}*.tpl'], 66 | tasks: ['template'] 67 | }, 68 | gruntfile: { 69 | files: ['Gruntfile.js'] 70 | }, 71 | livereload: { 72 | options: { 73 | livereload: '<%= connect.options.livereload %>' 74 | }, 75 | files: [ 76 | '<%= yeoman.app %>/*.html', 77 | '<%= yeoman.app %>/components/{,*/}*.html', 78 | '.tmp/components/{,*/}*.css', 79 | '<%= yeoman.app %>/images/{,*/}*.{png,jpg,jpeg,gif,webp,svg}' 80 | ] 81 | } 82 | }, 83 | 84 | // The actual grunt server settings 85 | connect: { 86 | options: { 87 | port: 9000, 88 | // Change this to '0.0.0.0' to access the server from outside. 89 | hostname: 'localhost', 90 | livereload: 35729 91 | }, 92 | livereload: { 93 | options: { 94 | open: true, 95 | middleware: function (connect) { 96 | return [ 97 | connect.static('.tmp'), 98 | connect().use( 99 | '/bower_components', 100 | connect.static('./bower_components') 101 | ), 102 | connect.static(appConfig.app) 103 | ]; 104 | } 105 | } 106 | }, 107 | test: { 108 | options: { 109 | port: 9001, 110 | middleware: function (connect) { 111 | return [ 112 | connect.static('.tmp'), 113 | connect.static('test'), 114 | connect().use( 115 | '/bower_components', 116 | connect.static('./bower_components') 117 | ), 118 | connect.static(appConfig.app) 119 | ]; 120 | } 121 | } 122 | }, 123 | dist: { 124 | options: { 125 | open: true, 126 | base: '<%= yeoman.dist %>' 127 | } 128 | } 129 | }, 130 | 131 | // Make sure code styles are up to par and there are no obvious mistakes 132 | jshint: { 133 | options: { 134 | jshintrc: '.jshintrc', 135 | reporter: require('jshint-stylish') 136 | }, 137 | all: { 138 | src: [ 139 | 'Gruntfile.js', 140 | '<%= yeoman.app %>/components/{,*/}*.js', 141 | '!<%= yeoman.app %>/components/{,*/}*-spec.js' 142 | ] 143 | }, 144 | test: { 145 | options: { 146 | jshintrc: 'test/.jshintrc' 147 | }, 148 | src: ['<%= yeoman.app %>/components/{,*/}*-spec.js'] 149 | } 150 | }, 151 | 152 | // Empties folders to start fresh 153 | clean: { 154 | dist: { 155 | files: [{ 156 | dot: true, 157 | src: [ 158 | '.tmp', 159 | '<%= yeoman.dist %>/{,*/}*', 160 | '!<%= yeoman.dist %>/.git*' 161 | ] 162 | }] 163 | }, 164 | server: '.tmp' 165 | }, 166 | 167 | // Add vendor prefixed styles 168 | autoprefixer: { 169 | options: { 170 | browsers: ['last 1 version'] 171 | }, 172 | dist: { 173 | files: [{ 174 | expand: true, 175 | cwd: '.tmp/components/', 176 | src: '{,**/}*.css', 177 | dest: '.tmp/components/' 178 | }] 179 | } 180 | }, 181 | 182 | // Automatically inject Bower components into the app 183 | wiredep: { 184 | app: { 185 | src: ['<%= yeoman.app %>/index.html.tpl'], 186 | ignorePath: /\.\.\// 187 | } 188 | }, 189 | 190 | // Renames files for browser caching purposes 191 | filerev: { 192 | dist: { 193 | src: [ 194 | '<%= yeoman.dist %>/components/{,*/}*.js', 195 | '<%= yeoman.dist %>/components/{,*/}*.css', 196 | '<%= yeoman.dist %>/images/{,*/}*.{png,jpg,jpeg,gif,webp,svg}', 197 | '<%= yeoman.dist %>/styles/fonts/*' 198 | ] 199 | } 200 | }, 201 | 202 | // Reads HTML for usemin blocks to enable smart builds that automatically 203 | // concat, minify and revision files. Creates configurations in memory so 204 | // additional tasks can operate on them 205 | useminPrepare: { 206 | html: '<%= yeoman.app %>/index.html.tpl', 207 | options: { 208 | dest: '<%= yeoman.dist %>', 209 | flow: { 210 | html: { 211 | steps: { 212 | js: ['concat', 'uglifyjs'], 213 | css: ['cssmin'] 214 | }, 215 | post: {} 216 | } 217 | } 218 | } 219 | }, 220 | 221 | // Performs rewrites based on filerev and the useminPrepare configuration 222 | usemin: { 223 | html: ['<%= yeoman.dist %>/{,**/}*.html'], 224 | css: ['<%= yeoman.dist %>/components/{,*/}*.css'], 225 | options: { 226 | assetsDirs: ['<%= yeoman.dist %>','<%= yeoman.dist %>/images'] 227 | } 228 | }, 229 | 230 | // The following *-min tasks will produce minified files in the dist folder 231 | // By default, your `index.html`'s will take care of 232 | // minification. These next options are pre-configured if you do not wish 233 | // to use the Usemin blocks. 234 | // cssmin: { 235 | // dist: { 236 | // files: { 237 | // '<%= yeoman.dist %>/styles/main.css': [ 238 | // '.tmp/styles/{,*/}*.css' 239 | // ] 240 | // } 241 | // } 242 | // }, 243 | // uglify: { 244 | // dist: { 245 | // files: { 246 | // '<%= yeoman.dist %>/components/scripts.js': [ 247 | // '<%= yeoman.dist %>/components/scripts.js' 248 | // ] 249 | // } 250 | // } 251 | // }, 252 | // concat: { 253 | // dist: {} 254 | // }, 255 | 256 | imagemin: { 257 | dist: { 258 | files: [{ 259 | expand: true, 260 | cwd: '<%= yeoman.app %>/images', 261 | src: '{,*/}*.{png,jpg,jpeg,gif}', 262 | dest: '<%= yeoman.dist %>/images' 263 | }] 264 | } 265 | }, 266 | 267 | svgmin: { 268 | dist: { 269 | files: [{ 270 | expand: true, 271 | cwd: '<%= yeoman.app %>/images', 272 | src: '{,*/}*.svg', 273 | dest: '<%= yeoman.dist %>/images' 274 | }] 275 | } 276 | }, 277 | 278 | htmlmin: { 279 | dist: { 280 | options: { 281 | collapseWhitespace: true, 282 | conservativeCollapse: true, 283 | collapseBooleanAttributes: true, 284 | removeCommentsFromCDATA: true, 285 | removeOptionalTags: true 286 | }, 287 | files: [{ 288 | expand: true, 289 | cwd: '<%= yeoman.dist %>', 290 | src: ['*.html', 'components/{,*/}*.html'], 291 | dest: '<%= yeoman.dist %>' 292 | }] 293 | } 294 | }, 295 | 296 | // ng-annotate tries to make the code safe for minification automatically 297 | // by using the Angular long form for dependency injection. 298 | ngAnnotate: { 299 | dist: { 300 | files: [{ 301 | expand: true, 302 | cwd: '.tmp/concat/components', 303 | src: ['*.js', '!oldieshim.js'], 304 | dest: '.tmp/concat/components' 305 | }] 306 | } 307 | }, 308 | 309 | // Replace Google CDN references 310 | cdnify: { 311 | dist: { 312 | html: ['<%= yeoman.dist %>/*.html'] 313 | } 314 | }, 315 | 316 | // Copies remaining files to places other tasks can use 317 | copy: { 318 | dist: { 319 | files: [{ 320 | expand: true, 321 | dot: true, 322 | cwd: '<%= yeoman.app %>', 323 | dest: '<%= yeoman.dist %>', 324 | src: [ 325 | '*.{ico,png,txt}', 326 | '.htaccess', 327 | '*.html', 328 | 'components/{,*/}*.html', 329 | 'images/{,*/}*.{webp}', 330 | 'partials/*', 331 | 'fonts/*' 332 | ] 333 | }, { 334 | expand: true, 335 | cwd: '.tmp/images', 336 | dest: '<%= yeoman.dist %>/images', 337 | src: ['generated/*'] 338 | }, { 339 | expand: true, 340 | cwd: 'bower_components/bootstrap/dist', 341 | src: 'fonts/*', 342 | dest: '<%= yeoman.dist %>' 343 | }, { 344 | expand: true, 345 | cwd: 'bower_components/font-awesome', 346 | src: 'fonts/*', 347 | dest: '<%= yeoman.dist %>' 348 | }, 349 | { 350 | expand: true, 351 | cwd: 'bower_components/bootstrap-material-design/dist', 352 | src: 'fonts/*', 353 | dest: '<%= yeoman.dist %>' 354 | }] 355 | }, 356 | styles: { 357 | expand: true, 358 | cwd: '<%= yeoman.app %>/components', 359 | dest: '.tmp/components/', 360 | src: '{,*/}*.css' 361 | } 362 | }, 363 | 364 | template: { 365 | options: { 366 | data: appConfig 367 | }, 368 | dist: { 369 | files: { 370 | '<%= yeoman.dist %>/index.html': ['<%= yeoman.app %>/index.html.tpl'], 371 | '.tmp/components/app-config.js': ['<%= yeoman.app %>/components/app-config.js.tpl'] 372 | } 373 | }, 374 | test: { 375 | files: { 376 | '.tmp/index.html': ['<%= yeoman.app %>/index.html.tpl'], 377 | '.tmp/components/app-config.js': ['<%= yeoman.app %>/components/app-config.js.tpl'] 378 | } 379 | }, 380 | }, 381 | 382 | // Run some tasks in parallel to speed up the build process 383 | concurrent: { 384 | server: [ 385 | 'copy:styles' 386 | ], 387 | test: [ 388 | 'copy:styles' 389 | ], 390 | dist: [ 391 | 'copy:styles', 392 | 'imagemin', 393 | 'svgmin' 394 | ] 395 | }, 396 | 397 | // Test settings 398 | karma: { 399 | unit: { 400 | configFile: 'test/karma.conf.js', 401 | singleRun: true 402 | } 403 | }, 404 | 405 | 'gh-pages': { 406 | options: { 407 | base: 'dist' 408 | }, 409 | src: ['**'] 410 | } 411 | }); 412 | 413 | 414 | grunt.registerTask('serve', 'Compile then start a connect web server', function (target) { 415 | if (target === 'dist') { 416 | return grunt.task.run(['build', 'connect:dist:keepalive']); 417 | } 418 | 419 | grunt.task.run([ 420 | 'clean:server', 421 | 'wiredep', 422 | 'template:test', 423 | 'concurrent:server', 424 | 'autoprefixer', 425 | 'connect:livereload', 426 | 'watch' 427 | ]); 428 | }); 429 | 430 | grunt.registerTask('test', [ 431 | 'clean:server', 432 | 'concurrent:test', 433 | 'autoprefixer', 434 | 'connect:test', 435 | 'karma' 436 | ]); 437 | 438 | grunt.registerTask('build', [ 439 | 'clean:dist', 440 | 'wiredep', 441 | 'useminPrepare', 442 | 'concurrent:dist', 443 | 'autoprefixer', 444 | 'template:dist', 445 | 'concat', 446 | 'ngAnnotate', 447 | 'copy:dist', 448 | 'cdnify', 449 | 'cssmin', 450 | 'uglify', 451 | 'filerev', 452 | 'usemin', 453 | 'htmlmin' 454 | ]); 455 | 456 | grunt.registerTask('deploy', 'Deploy', function () { 457 | if (grunt.option('env') !== 'production') { 458 | grunt.log.warn('`deploy` must be for the production environment.'); 459 | grunt.log.warn('Try `grunt deploy --env=production`'); 460 | return; 461 | } 462 | 463 | grunt.task.run([ 464 | 'build', 465 | 'gh-pages' 466 | ]); 467 | }); 468 | 469 | grunt.registerTask('default', [ 470 | 'newer:jshint', 471 | 'test', 472 | 'build' 473 | ]); 474 | }; 475 | -------------------------------------------------------------------------------- /app/components/game/bot-factory.js: -------------------------------------------------------------------------------- 1 | 2 | 3 | ;(function() { 4 | 'use strict'; 5 | 6 | function distance(a,b) { 7 | var dx = a.x - b.x; 8 | var dy = a.y - b.y; 9 | return Math.max(Math.abs(dx),Math.abs(dy)); 10 | } 11 | 12 | var mathSign = Math.sign || function (value) { // polyfill for Math.sign 13 | var number = +value; 14 | if (number === 0) { return number; } 15 | if (Number.isNaN(number)) { return number; } 16 | return number < 0 ? -1 : 1; 17 | }; 18 | 19 | function isAt(obj,x,y) { 20 | if (angular.isObject(x)) { 21 | return x.x === obj.x && x.y === obj.y; 22 | } 23 | return x === obj.x && y === obj.y; 24 | } 25 | 26 | function modulo(x,n) { // move somewher globally usefull 27 | return ((x%n)+n)%n; 28 | } 29 | 30 | function rnd(x) { 31 | x = Math.round(x); 32 | return x === 0 ? 0 : x/Math.abs(x); 33 | } 34 | 35 | angular.module('ePrime') 36 | .value('isAt', isAt) 37 | .run(function(ngEcs) { 38 | 39 | //function find(bot, _) { // used by unload and charge, move? 40 | // return bot.findNearest(_); 41 | //} 42 | 43 | function Copy(e) { 44 | var self = this; 45 | 46 | ['name','x','y','S','mS','E','mE'].forEach(function(prop) { 47 | self[prop] = e[prop]; 48 | }); 49 | 50 | } 51 | 52 | function Accessor(e) { 53 | var self = this; 54 | 55 | ['name','x','y','S','mS','E','mE','mem'].forEach(function(prop) { 56 | Object.defineProperty(self, prop, { 57 | get: function() {return e[prop]; } 58 | }); 59 | }); 60 | 61 | } 62 | 63 | function BotProxy(e) { 64 | 65 | Accessor.call(this, e.bot); 66 | 67 | this.moveTo = e.bot.moveTo.bind(e.bot); 68 | this.move = e.bot.move.bind(e.bot); 69 | this.mine = e.bot.mine.bind(e.bot); 70 | this.upgrade = e.bot.upgrade.bind(e.bot); 71 | 72 | 73 | this.unload = BotProxy.prototype.unload.bind(e.bot); 74 | this.charge = BotProxy.prototype.charge.bind(e.bot); 75 | this.construct = BotProxy.prototype.construct.bind(e.bot); 76 | this.find = BotProxy.prototype.find.bind(e.bot); 77 | this.distanceTo = BotProxy.prototype.distanceTo.bind(e.bot); 78 | this.log = BotProxy.prototype.log.bind(e.bot); 79 | } 80 | 81 | BotProxy.prototype.unload = function(_) { // should unload to co-located @ 82 | var home = this.findAt(_ || '@'); // todo: just check bot entities 83 | return (home) ? this.unloadTo(home) : null; 84 | }; 85 | 86 | BotProxy.prototype.charge = function(_) { // should charge to co-located @ 87 | var home = this.findAt(_ || '@'); // todo: just check bot entities 88 | return (home) ? home.bot.chargeBot(this.$parent) : null; 89 | }; 90 | 91 | BotProxy.prototype.construct = function(_) { 92 | this.construct(_ || null); 93 | }; 94 | 95 | BotProxy.prototype.distanceTo = function(_) { 96 | return distance(this,_); 97 | }; 98 | 99 | BotProxy.prototype.find = function(_) { 100 | var n = this.findNearest(_); 101 | if (!n) { return null; } 102 | if (n.$bot) { 103 | n = new Copy(n.bot); 104 | } 105 | return n; 106 | }; 107 | 108 | BotProxy.prototype.log = function(msg) { 109 | this.bot.addAlert('success', msg); 110 | }; 111 | 112 | /* function createAccessor(bot) { 113 | var $bot = {}; 114 | 115 | ['name','x','y','S','mS','E','mE'].forEach(function(prop) { 116 | Object.defineProperty($bot, prop, { 117 | get: function() {return bot[prop]; } 118 | }); 119 | }); 120 | 121 | return $bot; 122 | 123 | } 124 | 125 | function createInterface(e) { 126 | var bot = e.bot; 127 | var $bot = createAccessor(bot); 128 | 129 | $bot.move = function $$move(x,y) { 130 | return bot.move(x,y); 131 | }; 132 | 133 | $bot.moveTo = function $$moveTo(x,y) { // this is not good 134 | //if (bot.obs) { 135 | // return bot.moveTo(bot.target.x,bot.target.y); 136 | //} 137 | return bot.moveTo(x,y); 138 | }; 139 | 140 | $bot.mine = function $$mine() { 141 | return bot.mine(); 142 | }; 143 | 144 | $bot.unload = function $$unload(_) { // should unload to co-located @ 145 | var home = find(_ || '@'); // gets closest 146 | return (home) ? bot.unloadTo(home) : null; 147 | }; 148 | 149 | $bot.charge = function $$charge(_) { // should charge to co-located @ 150 | var home = find(_ || '@'); // gets closest 151 | return (home) ? home.bot.chargeBot(e) : null; 152 | }; 153 | 154 | $bot.upgrade = function $$upgrade() { 155 | bot.upgrade(); 156 | }; 157 | 158 | $bot.construct = function $$construct(_) { 159 | bot.construct(_ || null); 160 | }; 161 | 162 | function find(_) { // used by unload and charge, move? 163 | var r = bot.scanList(_); 164 | return (r.length > 0) ? r[0] : null; 165 | } 166 | 167 | $bot.find = function $$find(_) { // move? 168 | var r = find(_); 169 | return (r && r.$bot) ? createAccessor(r.bot) : r; // maybe should just be properties 170 | }; 171 | 172 | $bot.log = function(msg) { 173 | bot.addAlert('success', msg); 174 | }; 175 | 176 | return $bot; 177 | } */ 178 | 179 | ngEcs.$s('bots', { // todo: create charging component 180 | $require: ['bot'], 181 | $addEntity: function(e) { // should be part of scripting? 182 | e.$bot = new BotProxy(e); 183 | e.bot.update(); 184 | }, 185 | $updateEach: function(e,dt) { 186 | //console.log(bot); 187 | //var i = -1,arr = this.$family,len = arr.length,bot,dE; 188 | //while (++i < len) { 189 | var bot = e.bot; 190 | var dE = +Math.min(bot.chargeRate*dt, bot.mE-bot.E); 191 | bot.E += dE; 192 | ngEcs.stats.E += dE; 193 | //} 194 | } 195 | }); 196 | 197 | //var e = this.E; 198 | //this.E = +Math.min(e + dE, this.mE).toFixed(4); 199 | //return this.E - e; 200 | 201 | /* ngEcs.$s('botsRender', { 202 | $require: ['bot','render'], 203 | $addEntity: function(e) { 204 | if (svgStage.renderBots) { 205 | var bots = this.$family; 206 | svgStage.renderBots(bots); 207 | } 208 | }, 209 | $update: function() { 210 | this.$family.forEach(function(e) { 211 | if (e.$render) { 212 | e.$render(); 213 | } 214 | }); 215 | } 216 | }); */ 217 | 218 | }) 219 | .run(function(ngEcs) { 220 | function ActionComponent() { 221 | this.queue = []; 222 | } 223 | 224 | ActionComponent.prototype.push = function(fn) { 225 | this.queue.push(fn); 226 | }; 227 | 228 | ActionComponent.prototype.next = function() { 229 | return this.queue.shift(); 230 | }; 231 | 232 | ActionComponent.prototype.clear = function() { 233 | this.queue = []; 234 | }; 235 | 236 | ngEcs.$c('action', ActionComponent); 237 | 238 | ngEcs.$s('action', { // todo: move 239 | $require: ['bot','action'], 240 | $updateEach: function(e) { 241 | 242 | //console.log(e); 243 | 244 | //var i = -1,arr = this.$family,len = arr.length,e; 245 | //while (++i < len) { 246 | //e = arr[i]; 247 | 248 | if (e.script) { // is this necessary? 249 | e.script.skip = e.action.queue.length > 0; 250 | } 251 | 252 | if (e.bot.E < 1) { return; } // todo: make while 253 | 254 | if (e.action.queue.length > 0) { // remove action component when done? 255 | var fn = e.action.next(); 256 | var ret = fn(e); 257 | if (ret.next) { 258 | e.action.push(ret.next); 259 | } 260 | } 261 | 262 | //} 263 | } 264 | }); 265 | 266 | }) 267 | .run(function (isAt, TILES, GAME, ngEcs) { // Bot components 268 | 269 | var botParams = { 270 | mS0: 10, // Starting storage capacity 271 | mE0: 10, // Starting energy capacity 272 | DIS: 2, // 1+Discharge exponent, faster discharge means lower effeciency 273 | CHAR: 0.5, // Charging effeciency 274 | I: 0.5, // moves per turn for starting unit 275 | E: 2/3, // surface/volume exponent, 276 | constructCost: 20 277 | }; 278 | 279 | /* function Tile() { 280 | this.x = 0; 281 | this.y = 0; 282 | this.t = TILES.BOT; 283 | }; 284 | 285 | Tile.prototype.isAt = function(x,y) { 286 | return isAt(this,x,y); 287 | }; */ 288 | 289 | function Bot(parent) { 290 | 291 | this.name = ''; 292 | this.t = TILES.BOT; 293 | this.x = 0; 294 | this.y = 0; 295 | 296 | this.S = 0; // Raw material storage 297 | this.mS = 10; // Maximum 298 | this.dS = 1; // Mining ability 299 | this.E = 0; // Energy 300 | this.dE = 0.01; // Charging rate 301 | this.mE = 10; // Maximum 302 | 303 | this.active = false; 304 | this.message = ''; 305 | this.alerts = []; 306 | 307 | this.mem = {}; 308 | 309 | this.$parent = parent; 310 | 311 | } 312 | 313 | Bot.prototype.addAlert = function(type, msg) { 314 | this.alerts.push({type:type, msg:msg}); 315 | }; 316 | 317 | Bot.prototype.closeAlert = function(index) { 318 | this.alerts.splice(index, 1); 319 | }; 320 | 321 | Bot.prototype.clearLog = function() { 322 | this.alerts.splice(0); 323 | }; 324 | 325 | Bot.prototype.error = function(msg) { // move 326 | this.$parent.script.halted = true; 327 | //this.message = msg; // used as error flag, get rid of this 328 | this.addAlert('danger',msg); 329 | //this.setCode(null); 330 | }; 331 | 332 | Bot.prototype.charge = function(dE) { 333 | var e = this.E; 334 | this.E = +Math.min(e + dE, this.mE).toFixed(4); 335 | return this.E - e; 336 | }; 337 | 338 | Bot.prototype.isAt = function(x,y) { 339 | return isAt(this,x,y); 340 | }; 341 | 342 | Bot.prototype.isNotAt = function(x,y) { 343 | return this.x !== x || this.y !== y; 344 | }; 345 | 346 | //Bot.prototype.mass = function() { 347 | // return this.mS + this.mE; 348 | //}; 349 | 350 | //var DIS = 1+1; // 1+Discharge exponent, faster discharge means lower effeciency 351 | 352 | //Bot.prototype.moveCost = function() { 353 | // return Math.pow(this.mass/20, DIS); 354 | //}; 355 | 356 | /* var CHAR = 0.5; // Charging effeciency 357 | var I = 1; // moves per turn for base 358 | var E = 2/3; // surface/volume exponent 359 | var N = CHAR*I/(Math.pow(20, E)); // normilization factor 360 | 361 | Bot.prototype.chargeRate = function() { 362 | return N*Math.pow(this.mass(), E); 363 | }; */ 364 | 365 | Bot.prototype.canMove = function(dx,dy) { // TODO: check range 366 | 367 | dx = mathSign(dx); 368 | dy = mathSign(dy); // max +/-1 369 | 370 | var dr = Math.max(Math.abs(dx),Math.abs(dy)); 371 | var dE = this.moveCost*dr; 372 | 373 | return GAME.world.canMove(this.x + dx,this.y + dy) && this.E >= dE; 374 | }; 375 | 376 | Bot.prototype.move = function(dx,dy) { // TODO: check range 377 | 378 | this.obs = false; 379 | 380 | dx = mathSign(dx); 381 | dy = mathSign(dy); // max +/-1 382 | 383 | var dr = Math.max(Math.abs(dx),Math.abs(dy)); // Chebyshev distance 384 | var dE = this.moveCost*dr; 385 | 386 | if (GAME.world.canMove(this.x + dx,this.y + dy)) { // Need to check bot skills, check path 387 | if (this.E >= dE) { 388 | this.last = {x: this.x, y: this.y}; 389 | this.heading = {x: dx, y:dy}; 390 | 391 | this.x += dx; 392 | this.y += dy; 393 | this.E -= dE; 394 | 395 | GAME.world.scanRange(this); 396 | 397 | return true; 398 | } 399 | } 400 | return false; 401 | }; 402 | 403 | Bot.prototype.canWalk = function(dx,dy) { 404 | return GAME.world.canMove(this.x+dx,this.y+dy); 405 | }; 406 | 407 | Bot.prototype.moveStep = function(dx,dy) { // TODO: check range 408 | this.x += dx; 409 | this.y += dy; 410 | this.E -= this.moveCost; 411 | 412 | GAME.world.scanRange(this); 413 | return true; 414 | }; 415 | 416 | Bot.prototype.canMoveTo = function(x,y) { // TODO: check range 417 | 418 | if (angular.isObject(x)) { // TODO: Utility 419 | y = x.y; 420 | x = x.x; 421 | } 422 | 423 | var dx = x - this.x; 424 | var dy = y - this.y; 425 | 426 | return this.canMove(dx,dy); 427 | }; 428 | 429 | var DIR = [ // move 430 | [1,1], // 0 431 | [1,0], // 1 432 | [1,-1], // 2 433 | [0,-1], // 3 434 | [-1,-1],// 4 435 | [-1,0], // 5 436 | [-1,1], // 6 437 | [0,1], // 7 438 | ]; 439 | 440 | var DIR_LEN = DIR.length; 441 | 442 | /* DIR.forEach(function(d) { 443 | var a = modulo(8-Math.round(Math.atan2(d[1], d[0])/0.7853981633974483), 7); 444 | console.log(d, a); 445 | }); */ 446 | 447 | 448 | 449 | Bot.prototype.moveTo = function(x,y) { // this is so bad!!! 450 | 451 | if (angular.isObject(x)) { // TODO: Utility 452 | y = +x.y || 0; 453 | x = +x.x || 0; 454 | } else { 455 | x = +x || 0; 456 | y = +y || 0; 457 | } 458 | 459 | if (isAt(this, x,y)) { 460 | this.obs = false; 461 | return true; 462 | } 463 | 464 | if (this.E < this.moveCost) { 465 | return false; 466 | } 467 | 468 | var dx = x - this.x; 469 | var dy = y - this.y; 470 | var dr = Math.max(Math.abs(dx),Math.abs(dy)); // "distance" to target, not euclidian, Chebyshev distance 471 | //var dr = Math.sqrt(dx*dx+dy*dy); // distance to target, euclidian 472 | 473 | //console.log([dx,dy]); 474 | 475 | dx = rnd(dx/dr); // "unit vector" towards goal, not euclidian 476 | dy = rnd(dy/dr); 477 | 478 | if (!this.target || this.target.x !== x || this.target.y !== y) { // new target 479 | //console.log('new target'); 480 | this.target = {x:x, y:y}; 481 | var _heading = {dx:dx, dy:dy}; 482 | this.obs = this.obs && angular.equals(this.heading, _heading); 483 | this.heading = _heading; 484 | } 485 | 486 | var C = this.canWalk(dx,dy); // is free towards goal 487 | 488 | var targetHeading; 489 | DIR.forEach(function(d, i) { // find ordinal direction (0-7), improve 490 | if (d[0] === dx && d[1] === dy) { 491 | targetHeading = i; 492 | } 493 | }); 494 | 495 | var ddx = x - (this.x + dx); 496 | var ddy = y - (this.y + dy); 497 | var DF = Math.max(Math.abs(ddx),Math.abs(ddy)); // "distance" to target, not euclidian, Chebyshev distance 498 | //var DF = Math.sqrt(ddx*ddx+ddy*ddy); // distance from next step towards goal to goal 499 | 500 | //if (this.$parent.active) { console.log('before', this.obs, C, DF, this.dr); } 501 | 502 | if (this.obs && C && (DF < this.dr)) { // if obs and closer than collision point, need to check if DF === this.dr and starting point 503 | this.obs = false; // not starting point 504 | } 505 | 506 | this.dr = Math.min(dr, this.dr); // minimum distance 507 | 508 | //if (this.$parent.active) { console.log('after', this.obs, C, DF, this.dr); } 509 | 510 | var heading; 511 | if (!this.obs) { // not obs, move to target 512 | 513 | if (C) { 514 | this.moveStep(dx,dy); 515 | return true; 516 | } 517 | 518 | this.obs = true; // new collision 519 | this.dr = dr; 520 | this.P = [x,y]; 521 | heading = targetHeading; 522 | } else { 523 | heading = modulo(this.iHeading-2,DIR_LEN); /// start looking right 524 | } 525 | 526 | var i = 0; 527 | while (i < DIR_LEN) { // look left 528 | var d = DIR[heading]; 529 | 530 | if (this.canWalk(d[0],d[1])) { 531 | this.moveStep(d[0],d[1]); 532 | this.iHeading = heading; // keep heading for next step 533 | return true; 534 | } 535 | 536 | heading++; // turn legft 537 | heading %= DIR_LEN; 538 | i++; 539 | } 540 | 541 | return false; 542 | }; 543 | 544 | Bot.prototype.canMine = function() { 545 | return this.E >= 1 && 546 | this.S < this.mS && 547 | GAME.world.get(this.x,this.y).t === TILES.MINE; 548 | }; 549 | 550 | Bot.prototype.mine = function() { 551 | if (this.canMine()) { 552 | this.E--; 553 | var dS = GAME.world.dig(this.x,this.y); // TODO: bot effeciency? 554 | dS = this.load(dS); 555 | GAME.stats.S += dS; 556 | return dS; 557 | } 558 | return false; 559 | }; 560 | 561 | Bot.prototype.load = function(dS) { // dE? 562 | var s = this.S; 563 | this.S = Math.min(s + dS, this.mS); 564 | return this.S - s; 565 | }; 566 | 567 | Bot.prototype.unload = function() { // dE? 568 | var l = this.S; 569 | this.S = 0; 570 | return l; 571 | }; 572 | 573 | Bot.prototype.chargeBot = function(bot) { 574 | //console.log('charge', bot); 575 | if (isAt(bot.bot, this)) { // TODO: charging range? 576 | var e = Math.min(10, this.E); // TODO: charging speed 577 | e = bot.bot.charge(e); 578 | this.E -= e; 579 | return e; 580 | } 581 | return false; 582 | }; 583 | 584 | Bot.prototype.unloadTo = function(bot) { 585 | if (isAt(bot.bot, this)) {// TODO: unloading range? 586 | var s = this.unload(); 587 | var l = bot.bot.load(s); 588 | this.load(s-l); 589 | return l; 590 | } 591 | return 0; 592 | }; 593 | 594 | Bot.prototype.canUpgrade = function() { 595 | return this.S >= this.upgradeCost; 596 | }; 597 | 598 | //var DIS = 1+1; // 1+Discharge exponent, faster discharge means lower effeciency 599 | //var CHAR = 0.5; // Charging effeciency 600 | //var I = 1; // moves per turn for rover 601 | //var E = 2/3; // surface/volume exponent 602 | var N = 10*botParams.CHAR*botParams.I/(Math.pow(20, botParams.E)); // normilization factor 603 | 604 | Bot.prototype.upgrade = function() { 605 | //C = C || this.upgradeCost; 606 | if (this.S >= this.upgradeCost) { 607 | this.S -= this.upgradeCost; 608 | this.mS += 10; 609 | this.mE += 10; 610 | this.update(); 611 | } 612 | }; 613 | 614 | Bot.prototype.update = function() { 615 | this.mass = this.mE + this.mS; 616 | this.moveCost = Math.pow(this.mass/20, botParams.DIS); 617 | this.chargeRate = N*Math.pow(this.mass, botParams.E); 618 | this.upgradeCost = 0.5*this.mass; 619 | this.constructCost = botParams.constructCost; 620 | 621 | if (this.mS >= this.constructCost) { 622 | this.t = TILES.BASE; 623 | } 624 | }; 625 | 626 | Bot.prototype.canConstruct = function() { // where used? Move this to component 627 | return this.S >= botParams.constructCost; 628 | }; 629 | 630 | Bot.prototype.construct = function(script) { // todo: move to construct component 631 | if (this.S >= botParams.constructCost) { 632 | //var self = this; 633 | 634 | var bot = GAME.ecs.$e({ 635 | bot: { 636 | name: 'Rover', 637 | x: this.x, 638 | y: this.y, 639 | }, 640 | action: {}, 641 | render: {} 642 | }); 643 | 644 | if (script) { 645 | bot.$add('script', { 646 | scriptName: script, 647 | halted: false 648 | }); 649 | } 650 | 651 | this.S -= this.constructCost; 652 | return bot; 653 | } 654 | return null; 655 | }; 656 | 657 | Bot.prototype.canRelocate = function() { // component? 658 | return this.E >= 500; 659 | }; 660 | 661 | Bot.prototype.scan = function() { // used? 662 | return GAME.world.scan(this); 663 | }; 664 | 665 | Bot.prototype.findAt = function(_) { // move 666 | return GAME.findBotAt(_, this.x, this.y); 667 | }; 668 | 669 | Bot.prototype.findNearest = function(_) { 670 | var self = this; 671 | var r = 1e10; 672 | var ret = null; 673 | 674 | GAME.scanList(_) 675 | .forEach(function(e) { // do better 676 | if (e !== self) { 677 | var b = e.bot || e; 678 | var _r = distance(b,self); 679 | if (_r < r) { 680 | ret = e; 681 | r = _r; 682 | } 683 | } 684 | }); 685 | 686 | return ret; 687 | }; 688 | 689 | Bot.prototype.scanList = function(_) { // TODO: move, GAME.scanFrom?, optimize 690 | var self = this; 691 | var l = GAME.scanList(_).filter(function(r) { 692 | return r !== self; 693 | }); 694 | 695 | if (l.length === 0) { return []; } 696 | 697 | l.forEach(function(d) { 698 | var b = d.bot || d; 699 | var dx = b.x - self.x; 700 | var dy = b.y - self.y; 701 | d.r = Math.max(Math.abs(dx),Math.abs(dy)); // don't do this, adds r to entities? 702 | }); 703 | 704 | return l.sort( function(a, b) {return a.r - b.r; } ); 705 | }; 706 | 707 | ngEcs.$c('bot', Bot); 708 | }); 709 | 710 | })(); 711 | -------------------------------------------------------------------------------- /app/.htaccess: -------------------------------------------------------------------------------- 1 | # Apache Configuration File 2 | 3 | # (!) Using `.htaccess` files slows down Apache, therefore, if you have access 4 | # to the main server config file (usually called `httpd.conf`), you should add 5 | # this logic there: http://httpd.apache.org/docs/current/howto/htaccess.html. 6 | 7 | # ############################################################################## 8 | # # CROSS-ORIGIN RESOURCE SHARING (CORS) # 9 | # ############################################################################## 10 | 11 | # ------------------------------------------------------------------------------ 12 | # | Cross-domain AJAX requests | 13 | # ------------------------------------------------------------------------------ 14 | 15 | # Enable cross-origin AJAX requests. 16 | # http://code.google.com/p/html5security/wiki/CrossOriginRequestSecurity 17 | # http://enable-cors.org/ 18 | 19 | # 20 | # Header set Access-Control-Allow-Origin "*" 21 | # 22 | 23 | # ------------------------------------------------------------------------------ 24 | # | CORS-enabled images | 25 | # ------------------------------------------------------------------------------ 26 | 27 | # Send the CORS header for images when browsers request it. 28 | # https://developer.mozilla.org/en/CORS_Enabled_Image 29 | # http://blog.chromium.org/2011/07/using-cross-domain-images-in-webgl-and.html 30 | # http://hacks.mozilla.org/2011/11/using-cors-to-load-webgl-textures-from-cross-domain-images/ 31 | 32 | 33 | 34 | 35 | SetEnvIf Origin ":" IS_CORS 36 | Header set Access-Control-Allow-Origin "*" env=IS_CORS 37 | 38 | 39 | 40 | 41 | # ------------------------------------------------------------------------------ 42 | # | Web fonts access | 43 | # ------------------------------------------------------------------------------ 44 | 45 | # Allow access from all domains for web fonts 46 | 47 | 48 | 49 | Header set Access-Control-Allow-Origin "*" 50 | 51 | 52 | 53 | 54 | # ############################################################################## 55 | # # ERRORS # 56 | # ############################################################################## 57 | 58 | # ------------------------------------------------------------------------------ 59 | # | 404 error prevention for non-existing redirected folders | 60 | # ------------------------------------------------------------------------------ 61 | 62 | # Prevent Apache from returning a 404 error for a rewrite if a directory 63 | # with the same name does not exist. 64 | # http://httpd.apache.org/docs/current/content-negotiation.html#multiviews 65 | # http://www.webmasterworld.com/apache/3808792.htm 66 | 67 | Options -MultiViews 68 | 69 | # ------------------------------------------------------------------------------ 70 | # | Custom error messages / pages | 71 | # ------------------------------------------------------------------------------ 72 | 73 | # You can customize what Apache returns to the client in case of an error (see 74 | # http://httpd.apache.org/docs/current/mod/core.html#errordocument), e.g.: 75 | 76 | ErrorDocument 404 /404.html 77 | 78 | 79 | # ############################################################################## 80 | # # INTERNET EXPLORER # 81 | # ############################################################################## 82 | 83 | # ------------------------------------------------------------------------------ 84 | # | Better website experience | 85 | # ------------------------------------------------------------------------------ 86 | 87 | # Force IE to render pages in the highest available mode in the various 88 | # cases when it may not: http://hsivonen.iki.fi/doctype/ie-mode.pdf. 89 | 90 | 91 | Header set X-UA-Compatible "IE=edge" 92 | # `mod_headers` can't match based on the content-type, however, we only 93 | # want to send this header for HTML pages and not for the other resources 94 | 95 | Header unset X-UA-Compatible 96 | 97 | 98 | 99 | # ------------------------------------------------------------------------------ 100 | # | Cookie setting from iframes | 101 | # ------------------------------------------------------------------------------ 102 | 103 | # Allow cookies to be set from iframes in IE. 104 | 105 | # 106 | # Header set P3P "policyref=\"/w3c/p3p.xml\", CP=\"IDC DSP COR ADM DEVi TAIi PSA PSD IVAi IVDi CONi HIS OUR IND CNT\"" 107 | # 108 | 109 | # ------------------------------------------------------------------------------ 110 | # | Screen flicker | 111 | # ------------------------------------------------------------------------------ 112 | 113 | # Stop screen flicker in IE on CSS rollovers (this only works in 114 | # combination with the `ExpiresByType` directives for images from below). 115 | 116 | # BrowserMatch "MSIE" brokenvary=1 117 | # BrowserMatch "Mozilla/4.[0-9]{2}" brokenvary=1 118 | # BrowserMatch "Opera" !brokenvary 119 | # SetEnvIf brokenvary 1 force-no-vary 120 | 121 | 122 | # ############################################################################## 123 | # # MIME TYPES AND ENCODING # 124 | # ############################################################################## 125 | 126 | # ------------------------------------------------------------------------------ 127 | # | Proper MIME types for all files | 128 | # ------------------------------------------------------------------------------ 129 | 130 | 131 | 132 | # Audio 133 | AddType audio/mp4 m4a f4a f4b 134 | AddType audio/ogg oga ogg 135 | 136 | # JavaScript 137 | # Normalize to standard type (it's sniffed in IE anyways): 138 | # http://tools.ietf.org/html/rfc4329#section-7.2 139 | AddType application/javascript js jsonp 140 | AddType application/json json 141 | 142 | # Video 143 | AddType video/mp4 mp4 m4v f4v f4p 144 | AddType video/ogg ogv 145 | AddType video/webm webm 146 | AddType video/x-flv flv 147 | 148 | # Web fonts 149 | AddType application/font-woff woff 150 | AddType application/vnd.ms-fontobject eot 151 | 152 | # Browsers usually ignore the font MIME types and sniff the content, 153 | # however, Chrome shows a warning if other MIME types are used for the 154 | # following fonts. 155 | AddType application/x-font-ttf ttc ttf 156 | AddType font/opentype otf 157 | 158 | # Make SVGZ fonts work on iPad: 159 | # https://twitter.com/FontSquirrel/status/14855840545 160 | AddType image/svg+xml svg svgz 161 | AddEncoding gzip svgz 162 | 163 | # Other 164 | AddType application/octet-stream safariextz 165 | AddType application/x-chrome-extension crx 166 | AddType application/x-opera-extension oex 167 | AddType application/x-shockwave-flash swf 168 | AddType application/x-web-app-manifest+json webapp 169 | AddType application/x-xpinstall xpi 170 | AddType application/xml atom rdf rss xml 171 | AddType image/webp webp 172 | AddType image/x-icon ico 173 | AddType text/cache-manifest appcache manifest 174 | AddType text/vtt vtt 175 | AddType text/x-component htc 176 | AddType text/x-vcard vcf 177 | 178 | 179 | 180 | # ------------------------------------------------------------------------------ 181 | # | UTF-8 encoding | 182 | # ------------------------------------------------------------------------------ 183 | 184 | # Use UTF-8 encoding for anything served as `text/html` or `text/plain`. 185 | AddDefaultCharset utf-8 186 | 187 | # Force UTF-8 for certain file formats. 188 | 189 | AddCharset utf-8 .atom .css .js .json .rss .vtt .webapp .xml 190 | 191 | 192 | 193 | # ############################################################################## 194 | # # URL REWRITES # 195 | # ############################################################################## 196 | 197 | # ------------------------------------------------------------------------------ 198 | # | Rewrite engine | 199 | # ------------------------------------------------------------------------------ 200 | 201 | # Turning on the rewrite engine and enabling the `FollowSymLinks` option is 202 | # necessary for the following directives to work. 203 | 204 | # If your web host doesn't allow the `FollowSymlinks` option, you may need to 205 | # comment it out and use `Options +SymLinksIfOwnerMatch` but, be aware of the 206 | # performance impact: http://httpd.apache.org/docs/current/misc/perf-tuning.html#symlinks 207 | 208 | # Also, some cloud hosting services require `RewriteBase` to be set: 209 | # http://www.rackspace.com/knowledge_center/frequently-asked-question/why-is-mod-rewrite-not-working-on-my-site 210 | 211 | 212 | Options +FollowSymlinks 213 | # Options +SymLinksIfOwnerMatch 214 | RewriteEngine On 215 | # RewriteBase / 216 | 217 | 218 | # ------------------------------------------------------------------------------ 219 | # | Suppressing / Forcing the "www." at the beginning of URLs | 220 | # ------------------------------------------------------------------------------ 221 | 222 | # The same content should never be available under two different URLs especially 223 | # not with and without "www." at the beginning. This can cause SEO problems 224 | # (duplicate content), therefore, you should choose one of the alternatives and 225 | # redirect the other one. 226 | 227 | # By default option 1 (no "www.") is activated: 228 | # http://no-www.org/faq.php?q=class_b 229 | 230 | # If you'd prefer to use option 2, just comment out all the lines from option 1 231 | # and uncomment the ones from option 2. 232 | 233 | # IMPORTANT: NEVER USE BOTH RULES AT THE SAME TIME! 234 | 235 | # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 236 | 237 | # Option 1: rewrite www.example.com → example.com 238 | 239 | 240 | RewriteCond %{HTTPS} !=on 241 | RewriteCond %{HTTP_HOST} ^www\.(.+)$ [NC] 242 | RewriteRule ^ http://%1%{REQUEST_URI} [R=301,L] 243 | 244 | 245 | # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 246 | 247 | # Option 2: rewrite example.com → www.example.com 248 | 249 | # Be aware that the following might not be a good idea if you use "real" 250 | # subdomains for certain parts of your website. 251 | 252 | # 253 | # RewriteCond %{HTTPS} !=on 254 | # RewriteCond %{HTTP_HOST} !^www\..+$ [NC] 255 | # RewriteRule ^ http://www.%{HTTP_HOST}%{REQUEST_URI} [R=301,L] 256 | # 257 | 258 | 259 | # ############################################################################## 260 | # # SECURITY # 261 | # ############################################################################## 262 | 263 | # ------------------------------------------------------------------------------ 264 | # | Content Security Policy (CSP) | 265 | # ------------------------------------------------------------------------------ 266 | 267 | # You can mitigate the risk of cross-site scripting and other content-injection 268 | # attacks by setting a Content Security Policy which whitelists trusted sources 269 | # of content for your site. 270 | 271 | # The example header below allows ONLY scripts that are loaded from the current 272 | # site's origin (no inline scripts, no CDN, etc). This almost certainly won't 273 | # work as-is for your site! 274 | 275 | # To get all the details you'll need to craft a reasonable policy for your site, 276 | # read: http://html5rocks.com/en/tutorials/security/content-security-policy (or 277 | # see the specification: http://w3.org/TR/CSP). 278 | 279 | # 280 | # Header set Content-Security-Policy "script-src 'self'; object-src 'self'" 281 | # 282 | # Header unset Content-Security-Policy 283 | # 284 | # 285 | 286 | # ------------------------------------------------------------------------------ 287 | # | File access | 288 | # ------------------------------------------------------------------------------ 289 | 290 | # Block access to directories without a default document. 291 | # Usually you should leave this uncommented because you shouldn't allow anyone 292 | # to surf through every directory on your server (which may includes rather 293 | # private places like the CMS's directories). 294 | 295 | 296 | Options -Indexes 297 | 298 | 299 | # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 300 | 301 | # Block access to hidden files and directories. 302 | # This includes directories used by version control systems such as Git and SVN. 303 | 304 | 305 | RewriteCond %{SCRIPT_FILENAME} -d [OR] 306 | RewriteCond %{SCRIPT_FILENAME} -f 307 | RewriteRule "(^|/)\." - [F] 308 | 309 | 310 | # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 311 | 312 | # Block access to backup and source files. 313 | # These files may be left by some text editors and can pose a great security 314 | # danger when anyone has access to them. 315 | 316 | 317 | Order allow,deny 318 | Deny from all 319 | Satisfy All 320 | 321 | 322 | # ------------------------------------------------------------------------------ 323 | # | Secure Sockets Layer (SSL) | 324 | # ------------------------------------------------------------------------------ 325 | 326 | # Rewrite secure requests properly to prevent SSL certificate warnings, e.g.: 327 | # prevent `https://www.example.com` when your certificate only allows 328 | # `https://secure.example.com`. 329 | 330 | # 331 | # RewriteCond %{SERVER_PORT} !^443 332 | # RewriteRule ^ https://example-domain-please-change-me.com%{REQUEST_URI} [R=301,L] 333 | # 334 | 335 | # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 336 | 337 | # Force client-side SSL redirection. 338 | 339 | # If a user types "example.com" in his browser, the above rule will redirect him 340 | # to the secure version of the site. That still leaves a window of opportunity 341 | # (the initial HTTP connection) for an attacker to downgrade or redirect the 342 | # request. The following header ensures that browser will ONLY connect to your 343 | # server via HTTPS, regardless of what the users type in the address bar. 344 | # http://www.html5rocks.com/en/tutorials/security/transport-layer-security/ 345 | 346 | # 347 | # Header set Strict-Transport-Security max-age=16070400; 348 | # 349 | 350 | # ------------------------------------------------------------------------------ 351 | # | Server software information | 352 | # ------------------------------------------------------------------------------ 353 | 354 | # Avoid displaying the exact Apache version number, the description of the 355 | # generic OS-type and the information about Apache's compiled-in modules. 356 | 357 | # ADD THIS DIRECTIVE IN THE `httpd.conf` AS IT WILL NOT WORK IN THE `.htaccess`! 358 | 359 | # ServerTokens Prod 360 | 361 | 362 | # ############################################################################## 363 | # # WEB PERFORMANCE # 364 | # ############################################################################## 365 | 366 | # ------------------------------------------------------------------------------ 367 | # | Compression | 368 | # ------------------------------------------------------------------------------ 369 | 370 | 371 | 372 | # Force compression for mangled headers. 373 | # http://developer.yahoo.com/blogs/ydn/posts/2010/12/pushing-beyond-gzipping 374 | 375 | 376 | SetEnvIfNoCase ^(Accept-EncodXng|X-cept-Encoding|X{15}|~{15}|-{15})$ ^((gzip|deflate)\s*,?\s*)+|[X~-]{4,13}$ HAVE_Accept-Encoding 377 | RequestHeader append Accept-Encoding "gzip,deflate" env=HAVE_Accept-Encoding 378 | 379 | 380 | 381 | # Compress all output labeled with one of the following MIME-types 382 | # (for Apache versions below 2.3.7, you don't need to enable `mod_filter` 383 | # and can remove the `` and `` lines 384 | # as `AddOutputFilterByType` is still in the core directives). 385 | 386 | AddOutputFilterByType DEFLATE application/atom+xml \ 387 | application/javascript \ 388 | application/json \ 389 | application/rss+xml \ 390 | application/vnd.ms-fontobject \ 391 | application/x-font-ttf \ 392 | application/x-web-app-manifest+json \ 393 | application/xhtml+xml \ 394 | application/xml \ 395 | font/opentype \ 396 | image/svg+xml \ 397 | image/x-icon \ 398 | text/css \ 399 | text/html \ 400 | text/plain \ 401 | text/x-component \ 402 | text/xml 403 | 404 | 405 | 406 | 407 | # ------------------------------------------------------------------------------ 408 | # | Content transformations | 409 | # ------------------------------------------------------------------------------ 410 | 411 | # Prevent some of the mobile network providers from modifying the content of 412 | # your site: http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.9.5. 413 | 414 | # 415 | # Header set Cache-Control "no-transform" 416 | # 417 | 418 | # ------------------------------------------------------------------------------ 419 | # | ETag removal | 420 | # ------------------------------------------------------------------------------ 421 | 422 | # Since we're sending far-future expires headers (see below), ETags can 423 | # be removed: http://developer.yahoo.com/performance/rules.html#etags. 424 | 425 | # `FileETag None` is not enough for every server. 426 | 427 | Header unset ETag 428 | 429 | 430 | FileETag None 431 | 432 | # ------------------------------------------------------------------------------ 433 | # | Expires headers (for better cache control) | 434 | # ------------------------------------------------------------------------------ 435 | 436 | # The following expires headers are set pretty far in the future. If you don't 437 | # control versioning with filename-based cache busting, consider lowering the 438 | # cache time for resources like CSS and JS to something like 1 week. 439 | 440 | 441 | 442 | ExpiresActive on 443 | ExpiresDefault "access plus 1 month" 444 | 445 | # CSS 446 | ExpiresByType text/css "access plus 1 year" 447 | 448 | # Data interchange 449 | ExpiresByType application/json "access plus 0 seconds" 450 | ExpiresByType application/xml "access plus 0 seconds" 451 | ExpiresByType text/xml "access plus 0 seconds" 452 | 453 | # Favicon (cannot be renamed!) 454 | ExpiresByType image/x-icon "access plus 1 week" 455 | 456 | # HTML components (HTCs) 457 | ExpiresByType text/x-component "access plus 1 month" 458 | 459 | # HTML 460 | ExpiresByType text/html "access plus 0 seconds" 461 | 462 | # JavaScript 463 | ExpiresByType application/javascript "access plus 1 year" 464 | 465 | # Manifest files 466 | ExpiresByType application/x-web-app-manifest+json "access plus 0 seconds" 467 | ExpiresByType text/cache-manifest "access plus 0 seconds" 468 | 469 | # Media 470 | ExpiresByType audio/ogg "access plus 1 month" 471 | ExpiresByType image/gif "access plus 1 month" 472 | ExpiresByType image/jpeg "access plus 1 month" 473 | ExpiresByType image/png "access plus 1 month" 474 | ExpiresByType video/mp4 "access plus 1 month" 475 | ExpiresByType video/ogg "access plus 1 month" 476 | ExpiresByType video/webm "access plus 1 month" 477 | 478 | # Web feeds 479 | ExpiresByType application/atom+xml "access plus 1 hour" 480 | ExpiresByType application/rss+xml "access plus 1 hour" 481 | 482 | # Web fonts 483 | ExpiresByType application/font-woff "access plus 1 month" 484 | ExpiresByType application/vnd.ms-fontobject "access plus 1 month" 485 | ExpiresByType application/x-font-ttf "access plus 1 month" 486 | ExpiresByType font/opentype "access plus 1 month" 487 | ExpiresByType image/svg+xml "access plus 1 month" 488 | 489 | 490 | 491 | # ------------------------------------------------------------------------------ 492 | # | Filename-based cache busting | 493 | # ------------------------------------------------------------------------------ 494 | 495 | # If you're not using a build process to manage your filename version revving, 496 | # you might want to consider enabling the following directives to route all 497 | # requests such as `/css/style.12345.css` to `/css/style.css`. 498 | 499 | # To understand why this is important and a better idea than `*.css?v231`, read: 500 | # http://stevesouders.com/blog/2008/08/23/revving-filenames-dont-use-querystring 501 | 502 | # 503 | # RewriteCond %{REQUEST_FILENAME} !-f 504 | # RewriteCond %{REQUEST_FILENAME} !-d 505 | # RewriteRule ^(.+)\.(\d+)\.(js|css|png|jpg|gif)$ $1.$3 [L] 506 | # 507 | 508 | # ------------------------------------------------------------------------------ 509 | # | File concatenation | 510 | # ------------------------------------------------------------------------------ 511 | 512 | # Allow concatenation from within specific CSS and JS files, e.g.: 513 | # Inside of `script.combined.js` you could have 514 | # 515 | # 516 | # and they would be included into this single file. 517 | 518 | # 519 | # 520 | # Options +Includes 521 | # AddOutputFilterByType INCLUDES application/javascript application/json 522 | # SetOutputFilter INCLUDES 523 | # 524 | # 525 | # Options +Includes 526 | # AddOutputFilterByType INCLUDES text/css 527 | # SetOutputFilter INCLUDES 528 | # 529 | # 530 | 531 | # ------------------------------------------------------------------------------ 532 | # | Persistent connections | 533 | # ------------------------------------------------------------------------------ 534 | 535 | # Allow multiple requests to be sent over the same TCP connection: 536 | # http://httpd.apache.org/docs/current/en/mod/core.html#keepalive. 537 | 538 | # Enable if you serve a lot of static content but, be aware of the 539 | # possible disadvantages! 540 | 541 | # 542 | # Header set Connection Keep-Alive 543 | # 544 | --------------------------------------------------------------------------------