├── img ├── epoh.png ├── screen.png ├── favicon.ico ├── unit │ ├── base.png │ ├── cancel.png │ ├── grass.png │ ├── marine.png │ ├── mine.png │ ├── move.png │ ├── rover.png │ ├── turret.png │ ├── worker.png │ ├── bigmine.png │ ├── bombard.png │ ├── teleport.png │ ├── amplifier.png │ ├── deconstruct.png │ ├── powerplant.png │ ├── terminator.png │ └── ungarrison.png └── texture │ └── water.png ├── .gitignore ├── js ├── Array.js ├── Stream.js ├── Q.js ├── String.js ├── String.spec.js ├── WebComponent.js ├── Intent.js ├── Http.js ├── Config.dst.js ├── Coords.spec.js ├── NodeApi.js ├── PerlinGenerator.js ├── Tile.js ├── CubeCoords.js ├── Function.js ├── Api.js ├── Coords.js ├── Cost.js ├── Function.spec.js ├── CubeCoords.spec.js ├── Sector.spec.js ├── Sector.js ├── Unit.js ├── Rules.spec.js ├── Client.js ├── Map.js ├── Rules.js ├── Game.js └── Renderer.js ├── .jshintrc ├── package.json ├── css ├── login.css ├── hud.css └── map.css ├── Makefile ├── todo.txt ├── LICENSE ├── perlin.html ├── README.md ├── rules.html ├── server.js └── index.html /img/epoh.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tautvilas/epoh/HEAD/img/epoh.png -------------------------------------------------------------------------------- /img/screen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tautvilas/epoh/HEAD/img/screen.png -------------------------------------------------------------------------------- /img/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tautvilas/epoh/HEAD/img/favicon.ico -------------------------------------------------------------------------------- /img/unit/base.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tautvilas/epoh/HEAD/img/unit/base.png -------------------------------------------------------------------------------- /img/unit/cancel.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tautvilas/epoh/HEAD/img/unit/cancel.png -------------------------------------------------------------------------------- /img/unit/grass.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tautvilas/epoh/HEAD/img/unit/grass.png -------------------------------------------------------------------------------- /img/unit/marine.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tautvilas/epoh/HEAD/img/unit/marine.png -------------------------------------------------------------------------------- /img/unit/mine.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tautvilas/epoh/HEAD/img/unit/mine.png -------------------------------------------------------------------------------- /img/unit/move.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tautvilas/epoh/HEAD/img/unit/move.png -------------------------------------------------------------------------------- /img/unit/rover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tautvilas/epoh/HEAD/img/unit/rover.png -------------------------------------------------------------------------------- /img/unit/turret.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tautvilas/epoh/HEAD/img/unit/turret.png -------------------------------------------------------------------------------- /img/unit/worker.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tautvilas/epoh/HEAD/img/unit/worker.png -------------------------------------------------------------------------------- /img/texture/water.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tautvilas/epoh/HEAD/img/texture/water.png -------------------------------------------------------------------------------- /img/unit/bigmine.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tautvilas/epoh/HEAD/img/unit/bigmine.png -------------------------------------------------------------------------------- /img/unit/bombard.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tautvilas/epoh/HEAD/img/unit/bombard.png -------------------------------------------------------------------------------- /img/unit/teleport.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tautvilas/epoh/HEAD/img/unit/teleport.png -------------------------------------------------------------------------------- /img/unit/amplifier.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tautvilas/epoh/HEAD/img/unit/amplifier.png -------------------------------------------------------------------------------- /img/unit/deconstruct.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tautvilas/epoh/HEAD/img/unit/deconstruct.png -------------------------------------------------------------------------------- /img/unit/powerplant.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tautvilas/epoh/HEAD/img/unit/powerplant.png -------------------------------------------------------------------------------- /img/unit/terminator.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tautvilas/epoh/HEAD/img/unit/terminator.png -------------------------------------------------------------------------------- /img/unit/ungarrison.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tautvilas/epoh/HEAD/img/unit/ungarrison.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.swp 2 | config.js 3 | lib 4 | node_modules 5 | js/Config.js 6 | bundle.js 7 | jquery.min.js 8 | npm-debug.log 9 | -------------------------------------------------------------------------------- /js/Array.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | Array.prototype.contains = function(needle) { 4 | return this.indexOf(needle) !== -1; 5 | }; 6 | -------------------------------------------------------------------------------- /js/Stream.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var Stream = function() { 4 | this.listeners = []; 5 | 6 | this.push = function(value) { 7 | this.listeners.forEach(function(listener) { 8 | listener(value); 9 | }); 10 | }; 11 | 12 | this.onValue = function(listener) { 13 | this.listeners.push(listener); 14 | }; 15 | 16 | }; 17 | 18 | module.exports = Stream; 19 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "node": true, 3 | "validthis": true, 4 | "loopfunc": true, 5 | "globals": { 6 | "alert": true, 7 | "Promise": true, 8 | "window": true, 9 | "document": true, 10 | "module": true, 11 | "console": true, 12 | "$": true, 13 | "it": true, 14 | "require": true, 15 | "describe": true, 16 | "WebSocket": true 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /js/Q.js: -------------------------------------------------------------------------------- 1 | var Q = {}; 2 | 3 | Q.byId = function(id) { 4 | return document.getElementById(id); 5 | }; 6 | 7 | Q.create = function(tag) { 8 | return document.createElement(tag); 9 | }; 10 | 11 | Q.byTag = function(tag) { 12 | return document.getElementsByTagName(tag); 13 | }; 14 | 15 | Q.byClass = function(cl) { 16 | return document.getElementsByClassName(cl); 17 | }; 18 | 19 | module.exports = Q; 20 | -------------------------------------------------------------------------------- /js/String.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | String.prototype.printf = function() { 4 | var result = this; 5 | 6 | for (var i = 0; i < arguments.length; i++) { 7 | result = result.replace('_', arguments[i] === undefined ? '' : arguments[i]); 8 | } 9 | 10 | return result; 11 | }; 12 | 13 | String.prototype.capitalize = function() { 14 | return this.charAt(0).toUpperCase() + this.slice(1); 15 | }; 16 | -------------------------------------------------------------------------------- /js/String.spec.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var assert = require('assert'); 4 | require('./String'); 5 | 6 | describe('String extensions', function() { 7 | it('should printf', function() { 8 | assert.equal('Example'.printf(), 'Example'); 9 | assert.equal('Hello _!'.printf('John'), 'Hello John!'); 10 | assert.equal('Hello _ _ _!'.printf('John', 'Doe', 30), 'Hello John Doe 30!'); 11 | }); 12 | }); 13 | -------------------------------------------------------------------------------- /js/WebComponent.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var Q = require('./Q'); 4 | 5 | var WebComponent = function(id, initialState) { 6 | var state = initialState || {}; 7 | var template = Q.byId(id); 8 | 9 | function updateVal(key) { 10 | template.querySelector('[data-bind=' + key + ']').textContent = state[key]; 11 | } 12 | 13 | this.setState = function(newState) { 14 | for (var key in newState) { 15 | state[key] = newState[key]; 16 | updateVal(key); 17 | } 18 | }; 19 | }; 20 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "epoh", 3 | "version": "0.4.13", 4 | "description": "Turn based MMOG", 5 | "main": "server.js", 6 | "dependencies": { 7 | "body-parser": "^1.14.2", 8 | "cookie-parser": "^1.4.0", 9 | "crypto": "0.0.3", 10 | "express": "^4.13.3", 11 | "websocket": "^1.0.22" 12 | }, 13 | "devDependencies": { 14 | "jshint": "^2.9.2", 15 | "mocha": "^2.4.5", 16 | "uglify-js": "^2.6.2" 17 | }, 18 | "scripts": { 19 | "postinstall": "cp ./js/Config.dst.js ./js/Config.js" 20 | }, 21 | "author": "Tautvilas ", 22 | "license": "ISC" 23 | } 24 | -------------------------------------------------------------------------------- /js/Intent.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var Coords = require('./Coords'); 4 | 5 | var Intent = function(src, dst, type) { 6 | this.src = src; 7 | this.dst = dst; 8 | this.type = type; 9 | }; 10 | 11 | Intent.fromJson = function(json) { 12 | var src = new Coords(json.src.x, json.src.y); 13 | var dst = new Coords(json.dst.x, json.dst.y); 14 | return new Intent(src, dst, json.type); 15 | }; 16 | 17 | Intent.MOVE = 'move'; 18 | Intent.BOMBARD = 'bombard'; 19 | Intent.MINE = 'mine'; 20 | Intent.BIGMINE = 'bigmine'; 21 | Intent.AMPLIFIER = 'amplifier'; 22 | Intent.POWERPLANT = 'powerplant'; 23 | Intent.UNGARRISON = 'ungarrison'; 24 | Intent.TERMINATOR = 'terminator'; 25 | Intent.DECONSTRUCT = 'deconstruct'; 26 | 27 | module.exports = Intent; 28 | -------------------------------------------------------------------------------- /js/Http.js: -------------------------------------------------------------------------------- 1 | var Http = {}; 2 | 3 | Http.request = function(method, url, data) { 4 | var request = new XMLHttpRequest(); 5 | var promise = new Promise(function(resolve, reject) { 6 | request.open(method, url, true); 7 | request.setRequestHeader('Content-Type', 'application/json; charset=UTF-8'); 8 | request.onload = function() { 9 | resolve(JSON.parse(request.responseText)); 10 | }; 11 | request.onerror = function(error) { 12 | reject(error); 13 | }; 14 | request.send(JSON.stringify(data)); 15 | }); 16 | return promise; 17 | }; 18 | 19 | Http.post = function(url, data) { 20 | return Http.request('POST', url, data); 21 | }; 22 | 23 | Http.get = function(url) { 24 | return Http.request('GET', url); 25 | }; 26 | 27 | Http.delete = function(url) { 28 | return Http.request('DELETE', url); 29 | }; 30 | 31 | -------------------------------------------------------------------------------- /js/Config.dst.js: -------------------------------------------------------------------------------- 1 | var Config = { 2 | SECTOR_WIDTH: 6, 3 | SECTOR_HEIGHT: 6, 4 | RESOURCES_PER_SECTOR: 2, 5 | MAP_SEED: 7892221, 6 | 7 | FIELD_DAMAGE: 0, 8 | 9 | DISABLED_DAMAGE: 5, 10 | DISABLE_TIMEOUT: 2, 11 | 12 | STARTING_MONEY: 300, 13 | STARTING_POWER: 100, 14 | BASE_MONEY: 50, 15 | MINE_MONEY: 25, 16 | UNIT_SUPPORT: 5, 17 | 18 | TICK_LENGTH: 60000, 19 | ROUND_LENGTH: 300000, 20 | 21 | MOCK_PLAYERS: [{name: 'bobby', sector: 'A'}], 22 | MOCK_UNITS: [ 23 | {player: 'jhonny', name: 'marine', coords: '2 1' }, 24 | {player: 'bobby', name: 'marine', coords: '2 0' }, 25 | {player: 'bobby', name: 'marine', coords: '3 0' } 26 | ], 27 | 28 | GOD_MODE: true, 29 | PRODUCTION: false, 30 | PORT: 8080, 31 | USERNAME_VIEW: false, 32 | DEBUG_VIEW: false, 33 | SHROUD: true 34 | 35 | }; 36 | 37 | module.exports = Config; 38 | -------------------------------------------------------------------------------- /js/Coords.spec.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | require('./Function'); 4 | var assert = require('assert'); 5 | var CubeCoords = require('./CubeCoords'); 6 | var Coords = require('./Coords'); 7 | 8 | describe('Coordinates', function() { 9 | 10 | it('should add two cube coordinates', function() { 11 | var c = (new CubeCoords(1, 2, 3)).add(new Coords(4, 5, -6)); 12 | assert.deepEqual(c.vector, [5, 7, -3]); 13 | }); 14 | 15 | it('should floor coordinate', function() { 16 | var c = (new Coords(1.7, 2.1, 3.5)).floor(); 17 | assert.deepEqual(c.vector, [1, 2, 3]); 18 | }); 19 | 20 | it('should scale coordinate', function() { 21 | var c = (new Coords(1.7, 2.1, 3.5)).scale(new Coords(1, 2, 3)); 22 | assert.deepEqual(c.vector, [1.7, 4.2, 10.5]); 23 | }); 24 | 25 | it('should combine operations', function() { 26 | var c = (new Coords(1.7, 2.1, 3.5)).uscale(2).floor(); 27 | assert.deepEqual(c.vector, [3, 4, 7]); 28 | }); 29 | }); 30 | 31 | -------------------------------------------------------------------------------- /css/login.css: -------------------------------------------------------------------------------- 1 | #login { 2 | margin: 0 auto; 3 | width: 474px; 4 | position: absolute; 5 | left: 50%; 6 | margin-left: -237px; 7 | margin-top: -280px; 8 | top: 50%; 9 | } 10 | 11 | #login input, button { 12 | padding: 10px; 13 | border: 1px solid #aaa; 14 | outline: 0; 15 | } 16 | 17 | #login input { 18 | width: 300px; 19 | float: left; 20 | border-right: 0px; 21 | color: #333; 22 | } 23 | 24 | #login #description { 25 | margin-bottom: 30px; 26 | text-align: justify; 27 | color: #333; 28 | } 29 | 30 | #login button { 31 | width: 150px; 32 | float: left; 33 | cursor: pointer; 34 | color: #333; 35 | } 36 | 37 | #login .error { 38 | margin-top: 20px; 39 | color: red; 40 | display: none; 41 | } 42 | 43 | #login #credits { 44 | text-align: center; 45 | position: fixed; 46 | bottom: 50px; 47 | } 48 | 49 | #login #credits > div { 50 | margin-top: 10px; 51 | } 52 | 53 | #login #credits .small { 54 | font-size: 12px; 55 | } 56 | 57 | #login #status { 58 | font-size: 12px; 59 | text-align: center; 60 | margin-top: 100px; 61 | } 62 | 63 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | client_files = js/Function.js \ 2 | js/String.js \ 3 | js/Array.js \ 4 | js/Stream.js \ 5 | js/Coords.js \ 6 | js/Cost.js \ 7 | js/CubeCoords.js \ 8 | js/Tile.js \ 9 | js/Sector.js \ 10 | js/Intent.js \ 11 | js/Unit.js \ 12 | js/Rules.js \ 13 | js/WebComponent.js \ 14 | js/Q.js \ 15 | js/Http.js \ 16 | js/Renderer.js \ 17 | js/Client.js \ 18 | js/NodeApi.js 19 | 20 | uglify = ./node_modules/uglify-js/bin/uglifyjs 21 | mocha = ./node_modules/mocha/bin/mocha 22 | jshint = ./node_modules/jshint/bin/jshint 23 | 24 | bundle.js: $(client_files) 25 | $(uglify) -cm --lint $(client_files) > bundle.js 26 | 27 | .PHONY: test lint deploy 28 | 29 | test: js/*.spec.js 30 | $(mocha) js/*.spec.js 31 | 32 | lint: js/*.js server.js 33 | $(jshint) js/ server.js 34 | 35 | deploy: test lint bundle.js 36 | npm install 37 | npm version patch 38 | git push --follow-tags 39 | scp bundle.js epoh:~/epoh/ 40 | rsync -avz --exclude js/Config.js . epoh:~/epoh 41 | git log | grep commit | wc 42 | -------------------------------------------------------------------------------- /todo.txt: -------------------------------------------------------------------------------- 1 | % 0.5 2 | 3 | ?) mountains too bright, fog of war colors 4 | ?) logging: add time & player name that is performing action 5 | ?) what happens if I bombard building? 6 | ?) each resouce should give unit 7 | *) add unit types to rules 8 | ux) highlight resource when cap reached 9 | ?) different power for different power plants 10 | ux) update income after mine deconstruct 11 | *) community board 12 | ?) check remove unit with intents on garrisoned 13 | 14 | o) merge sprites, merge css 15 | o) tiles behave buggily when recentering on slow connection and sometimes units start animating, cache sector tile data in frontend 16 | o) optimize field drawing via backend, maybe revamp field rendering as bg image, also add hue 17 | o) hex position optimization 18 | 19 | ?) do not place player in sector if there is too much water in it 20 | 21 | x) unit appear/disappear animation 22 | x) show tile height diff 23 | x) show enemy intents after turn 24 | 25 | % 1.0 26 | 27 | *) improvements: fort / wall / bridge 28 | *) terraform water->land; land->water 29 | *) enable/disable units 30 | 31 | *) minimap 32 | *) trading 33 | *) alliances, prestige 34 | 35 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 Tautvilas Mečinskas 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /js/NodeApi.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var Http = require('./Http'); 4 | 5 | var Api = function() { 6 | this.getSector = function(s) { 7 | return Http.get('/sector/_'.printf(s)); 8 | }; 9 | 10 | this.getSectors = function(s) { 11 | return Http.get('/sector/_'.printf(s.join(','))); 12 | }; 13 | 14 | this.addPlayer = function(name) { 15 | return Http.post('/player/_'.printf(name)); 16 | }; 17 | 18 | this.checkSession = function() { 19 | return Http.get('/session'); 20 | }; 21 | 22 | this.getStatus = function(name) { 23 | return Http.get('/player/_'.printf(name)); 24 | }; 25 | 26 | this.cancelIntents = function(name, coords) { 27 | return Http.delete('/tile/_/intents'.printf(coords)); 28 | }; 29 | 30 | this.addIntent = function(name, tile) { 31 | return Http.post('/intent', tile); 32 | }; 33 | 34 | this.endTurn = function() { 35 | return Http.post('/turn'); 36 | }; 37 | 38 | this.listen = function(f) { 39 | var connection = new WebSocket('ws://_:1337'.printf(window.location.hostname)); 40 | /* 41 | connection.onopen = function() { 42 | connection.send(window.document.cookie); 43 | }; 44 | */ 45 | connection.onmessage = function(message) { 46 | f(JSON.parse(message.data)); 47 | }; 48 | }; 49 | }; 50 | -------------------------------------------------------------------------------- /js/PerlinGenerator.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var PerlinGenerator = function(seed) { 4 | this.seed = seed; 5 | 6 | function random2d(x, y) { 7 | var b = Math.sin(x * 15731 + y * seed) * 1376312589; 8 | b = Math.cos(b); 9 | return b - Math.floor(b); 10 | } 11 | 12 | function interpolate(a, b, x) { 13 | var ft = x * 3.1415927; 14 | var f = (1 - Math.cos(ft)) * 0.5; 15 | return a*(1-f) + b*f; 16 | } 17 | 18 | function interpolate2d(x, y, n) { 19 | var intX = Math.floor(x); 20 | var fracX = x - intX; 21 | var intY = Math.floor(y); 22 | var fracY = y - intY; 23 | 24 | var v1 = random2d(intX, intY); 25 | var v2 = random2d(intX + 1, intY); 26 | var v3 = random2d(intX, intY + 1); 27 | var v4 = random2d(intX + 1, intY + 1); 28 | 29 | var i1 = interpolate(v1, v2, fracX); 30 | var i2 = interpolate(v3, v4, fracX); 31 | 32 | return interpolate(i1, i2, fracY); 33 | } 34 | 35 | this.generate = function(x, y) { 36 | var total = 0; 37 | var p = 0.25; 38 | var n = 3; 39 | for (var i = 0; i < n; i++) { 40 | var freq = Math.pow(2, i); 41 | var amp = Math.pow(p, i); 42 | total += interpolate2d(x * freq, y * freq, n) * amp; 43 | } 44 | return total; 45 | }; 46 | }; 47 | 48 | module.exports = PerlinGenerator; 49 | -------------------------------------------------------------------------------- /js/Tile.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var Coords = require('./Coords'); 4 | var Unit = require('./Unit'); 5 | 6 | var Tile = function(proto, coords) { 7 | for (var key in proto) { 8 | this[key] = proto[key]; 9 | } 10 | if (!this.intents) { 11 | this.intents = {}; 12 | } 13 | if (coords) { 14 | this.coords = coords; 15 | } else if (proto.coords) { 16 | this.coords = new Coords(proto.coords.x, proto.coords.y); 17 | } 18 | 19 | this.toString = function() { 20 | return this.name; 21 | }; 22 | }; 23 | 24 | Tile.fromJson = function(json) { 25 | if (json.unit) { 26 | json.unit = Unit.fromPrototype(json.unit); 27 | } 28 | return new Tile(json); 29 | }; 30 | 31 | Tile.WIDTH = 90; 32 | Tile.HEIGHT = 104; 33 | 34 | Tile.WATER = new Tile({ 35 | name: 'water', 36 | defense: 0 37 | }); 38 | Tile.PLAINS = new Tile({ 39 | name: 'plains', 40 | defense: 0 41 | }); 42 | Tile.HILLS = new Tile({ 43 | name: 'hills', 44 | defense: 20 45 | }); 46 | Tile.MOUNTAINS = new Tile({ 47 | name: 'mountains', 48 | defense: 40 49 | }); 50 | Tile.SHROUD = new Tile({ 51 | name: 'shroud', 52 | defense: 0 53 | }); 54 | Tile.GRASS = new Tile({ 55 | name: 'grass', 56 | defense: 0, 57 | cost: { 58 | iron: 100 59 | } 60 | }); 61 | 62 | Tile.fromName = function(name) { 63 | for (var key in Tile) { 64 | var tile = Tile[key]; 65 | if (tile.name && tile.name === name) { 66 | return new Tile(tile); 67 | } 68 | } 69 | }; 70 | 71 | module.exports = Tile; 72 | -------------------------------------------------------------------------------- /js/CubeCoords.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var Coords = require('./Coords'); 4 | 5 | var CubeCoords = function(x, y, z) { 6 | var self = this; 7 | Coords.call(self, x, y, z); 8 | 9 | self.neighbours = function() { 10 | return CubeCoords.directions.map(function(direction) { 11 | return self.add(direction); 12 | }); 13 | }; 14 | 15 | self.ring = function(radius) { 16 | if (!radius) { 17 | return [self]; 18 | } 19 | var results = []; 20 | var coord = self.add(CubeCoords.directions[4].uscale(radius)); 21 | for (var i = 0; i < 6; i++) { 22 | for (var j = 0; j < radius; j++) { 23 | results.push(coord); 24 | coord = coord.neighbours()[i]; 25 | } 26 | } 27 | return results; 28 | }; 29 | 30 | self.toOffset = function(even) { 31 | var coords = self; 32 | var negate = even ? 1 : -1; 33 | var x = coords.x + (coords.z + negate * (coords.z & 1)) / 2; 34 | var y = coords.z; 35 | return new Coords(x, y); 36 | }; 37 | }; 38 | 39 | CubeCoords.fromOffset = function(coords, even) { 40 | var negate = even ? 1 : -1; 41 | var x = coords.x - (coords.y + negate * (coords.y & 1)) / 2; 42 | var z = coords.y; 43 | var y = -x - z; 44 | return new CubeCoords(x, y, z); 45 | }; 46 | 47 | CubeCoords.directions = [ 48 | new CubeCoords(+1, -1, 0), 49 | new CubeCoords(+1, 0, -1), 50 | new CubeCoords(0, +1, -1), 51 | new CubeCoords(-1, +1, 0), 52 | new CubeCoords(-1, 0, +1), 53 | new CubeCoords(0, -1, +1), 54 | ]; 55 | 56 | module.exports = CubeCoords; 57 | -------------------------------------------------------------------------------- /js/Function.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | Function.prototype.applyTo = function(proto, params) { 4 | var O = function() {}; 5 | O.prototype = proto; 6 | var object = new O(); 7 | this.apply(object, params); 8 | return object; 9 | }; 10 | 11 | Function.prototype.boundMemoized = function() { 12 | if (!this._memo) { 13 | this._memo = {}; 14 | } 15 | var currentMap = this._memo; 16 | for (var i = 0; i < arguments.length; i++) { 17 | var argument = arguments[i] === undefined ? undefined : arguments[i].toString(); 18 | if (argument && argument.substr(1,6) === 'object') { 19 | console.error(this.name + ' argument no ' + i + ' does not have toString() defined'); 20 | } 21 | if (!currentMap[argument]) { 22 | currentMap[argument] = {}; 23 | } 24 | currentMap = currentMap[argument]; 25 | } 26 | if (currentMap.__value) { 27 | //console.log('MEMO CACHE HIT: ' + this.name); 28 | return currentMap.__value; 29 | } else { 30 | var newArguments = new Array(arguments.length - 1); 31 | for (i = 1; i < arguments.length; i++) { 32 | newArguments[i - 1] = arguments[i]; 33 | } 34 | var value = this.apply(arguments[0], newArguments); 35 | currentMap.__value = value; 36 | return value; 37 | } 38 | }; 39 | 40 | Function.prototype.memoized = function() { 41 | var newArguments = new Array(arguments.length + 1); 42 | for (var i = 1; i < arguments.length + 1; i++) { 43 | newArguments[i] = arguments[i - 1]; 44 | } 45 | return this.boundMemoized.apply(this, newArguments); 46 | }; 47 | -------------------------------------------------------------------------------- /perlin.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Epoh 5 | 6 | 7 | 8 | 9 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /js/Api.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var Game = require('./Game'); 4 | var Config = require('./Config'); 5 | var Map = require('./Map'); 6 | var Intent = require('./Intent'); 7 | 8 | var Api = function() { 9 | var self = this; 10 | 11 | this.game = new Game(new Map(Config.MAP_SEED)); 12 | 13 | function j(data) { 14 | return JSON.parse(JSON.stringify(data)); 15 | } 16 | 17 | this.getSector = function(s, username) { 18 | return Promise.resolve(j(self.game.getSector(s, username))); 19 | }; 20 | 21 | this.getSectors = function(sectors, username) { 22 | var response = []; 23 | sectors.forEach(function(s) { 24 | response.push(self.game.getSector(s, username)); 25 | }); 26 | return Promise.resolve(j(response)); 27 | }; 28 | 29 | this.addPlayer = function(name) { 30 | return Promise.resolve(j(self.game.addPlayer(name))); 31 | }; 32 | 33 | this.getStatus = function(name) { 34 | return Promise.resolve(j(self.game.getStatus(name))); 35 | }; 36 | 37 | this.addIntent = function(name, intent) { 38 | return Promise.resolve(j(self.game.addIntent(name, Intent.fromJson(j(intent))))); 39 | }; 40 | 41 | this.cancelIntents = function(name, coords) { 42 | return Promise.resolve(j(self.game.cancelIntents(name, coords.toString()))); 43 | }; 44 | 45 | this.checkSession = function() { 46 | return Promise.resolve(false); 47 | }; 48 | 49 | this.endTurn = function(name) { 50 | return Promise.resolve(j(self.game.endTurn(name))); 51 | }; 52 | 53 | this.listen = function(f) { 54 | self.game.events.onValue(function(value) { 55 | f(j(value)); 56 | }); 57 | }; 58 | }; 59 | -------------------------------------------------------------------------------- /js/Coords.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | require('./Function'); 4 | 5 | var Coords = function(x, y, z) { 6 | var self = this; 7 | 8 | this.vector = new Array(arguments.length); 9 | for(var i = 0; i < this.vector.length; ++i) { 10 | this.vector[i] = arguments[i]; 11 | } 12 | 13 | this.x = x; 14 | this.y = y; 15 | this.z = z; 16 | 17 | self.representation = this.vector.join(' '); 18 | 19 | var prototype = Object.getPrototypeOf(self); 20 | 21 | self.add = function(c2) { 22 | return self.constructor.applyTo(prototype, self.vector.map(function(coord, index) { 23 | return coord + c2.vector[index]; 24 | }));}; 25 | 26 | self.substract = function(c2) { 27 | return self.constructor.applyTo(prototype, self.vector.map(function(coord, index) { 28 | return coord - c2.vector[index]; 29 | }));}; 30 | 31 | self.uscale = function(s) { 32 | return self.constructor.applyTo(prototype, self.vector.map(function(coord) { 33 | return coord * s; 34 | }));}; 35 | 36 | self.scale = function(s) { 37 | return self.constructor.applyTo(prototype, self.vector.map(function(coord, index) { 38 | return coord * s.vector[index]; 39 | }));}; 40 | 41 | self.floor = function() { 42 | return self.constructor.applyTo(prototype, self.vector.map(function(coord) { 43 | return Math.floor(coord); 44 | }));}; 45 | 46 | self.length = function() { 47 | return Math.sqrt(self.vector.reduce(function(prev, coord) { 48 | return prev + coord * coord; 49 | }));}; 50 | 51 | self.xcoord = function() { 52 | return Math.max.apply({}, self.vector.map(function(coord) { 53 | return Math.abs(coord); 54 | }));}; 55 | 56 | self.toString = function() { 57 | return self.representation; 58 | }; 59 | }; 60 | 61 | Coords.fromString = function(str) { 62 | var c = {}; 63 | Coords.apply(c, str.split(' ').map(function(coord) { 64 | return parseInt(coord); 65 | })); 66 | c.constructor = Coords; 67 | return c; 68 | }; 69 | 70 | module.exports = Coords; 71 | 72 | -------------------------------------------------------------------------------- /js/Cost.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var Cost = function(cost) { 4 | this.cost = Object.keys(cost || {}).reduce(function(acc, key) {acc[key] = cost[key]; return acc;}, {}); 5 | 6 | this.add = function(c, negate) { 7 | Object.keys(c).forEach(function(key) { 8 | if (!this.cost[key]) { 9 | this.cost[key] = 0; 10 | } 11 | this.cost[key] += c[key] * (negate ? -1 : 1); 12 | }, this); 13 | return this; 14 | }; 15 | 16 | this.divide = function(d) { 17 | Object.keys(this.cost).forEach(function(key) { 18 | this.cost[key] = Math.floor(this.cost[key] / d); 19 | }, this); 20 | return this; 21 | }; 22 | 23 | this.substract = function(c) { 24 | return this.add(c, true); 25 | }; 26 | 27 | this.coveredBy = function(resources) { 28 | var cost = this.cost; 29 | return Object.keys(cost).reduce(function(acc, key) { 30 | if (resources[key] && cost[key] <= resources[key]) { 31 | return acc; 32 | } else { 33 | return false; 34 | } 35 | }, true); 36 | }; 37 | 38 | this.toData = function() { 39 | return this.cost; 40 | }; 41 | 42 | this.toString = function() { 43 | var cost = this.cost; 44 | return Object.keys(cost).reduce(function(acc, key) { 45 | return [acc, key, cost[key]].join(':'); 46 | }, ''); 47 | }; 48 | 49 | this.toHtml = function() { 50 | var cost = this.cost; 51 | return Object.keys(cost).reduce(function(acc, key) { 52 | return acc + '
' + key + ':' + cost[key]; 53 | }, ''); 54 | }; 55 | }; 56 | 57 | Cost.toString = function(c) { 58 | return Cost.fromData(c).toString(); 59 | }; 60 | 61 | Cost.toHtml = function(c) { 62 | return Cost.fromData(c).toHtml(); 63 | }; 64 | 65 | Cost.add = function(c1, c2) { 66 | return Cost.fromData(c1).add(c2).toData(); 67 | }; 68 | 69 | Cost.divide = function(c, d) { 70 | return Cost.fromData(c).divide(d).toData(); 71 | }; 72 | 73 | Cost.covers = function(c1, c2) { 74 | return Cost.fromData(c1).coveredBy(c2); 75 | }; 76 | 77 | Cost.substract = function(c1, c2) { 78 | return Cost.fromData(c1).substract(c2).toData(); 79 | }; 80 | 81 | Cost.fromData = function(data) { 82 | return new Cost(data); 83 | }; 84 | 85 | module.exports = Cost; 86 | -------------------------------------------------------------------------------- /js/Function.spec.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var assert = require('assert'); 4 | require('./Function'); 5 | 6 | describe('Function extensions', function() { 7 | it('should bound memoize', function() { 8 | var addCalled = 0; 9 | var substractCalled = 0; 10 | var add = function(a, b, c) { 11 | addCalled++; 12 | return a + b + c; 13 | }; 14 | var substract = function(a, b, c) { 15 | substractCalled++; 16 | return a - b - c; 17 | }; 18 | assert.equal(add(1, 2, 3), 6); 19 | assert.equal(add.boundMemoized(undefined, 1, 2, 3), 6); 20 | assert.equal(addCalled, 2); 21 | assert.equal(add.boundMemoized(undefined, 1, 2, 3), 6); 22 | assert.equal(add.boundMemoized(undefined, 1, 2, 3), 6); 23 | assert.equal(addCalled, 2); 24 | 25 | assert.equal(substract.boundMemoized(undefined, 1, 2, 3), -4); 26 | assert.equal(substract.boundMemoized(undefined, 1, 2, 3), -4); 27 | assert.equal(substract.boundMemoized(undefined, 1, 2, 3), -4); 28 | assert.equal(substractCalled, 1); 29 | }); 30 | 31 | it('should bound memoize object', function() { 32 | var addCalled = 0; 33 | var Coords = function(x, y) { 34 | this.x = x; 35 | this.y = y; 36 | this.toString = function() {return x + ' ' + y;}; 37 | this.add = function(a, b) { 38 | addCalled++; 39 | return new Coords(this.x + a, this.y + b); 40 | }; 41 | }; 42 | 43 | var c1 = new Coords(10, 20); 44 | assert.equal(c1+'', '10 20'); 45 | var c2 = c1.add.boundMemoized(c1, 5, 4); 46 | c2 = c1.add.boundMemoized(c1, 5, 4); 47 | c2 = c1.add.boundMemoized(c1, 5, 4); 48 | assert.equal(c2+'', '15 24'); 49 | assert.equal(addCalled, 1); 50 | 51 | var c3 = new Coords(0, 0); 52 | var c4 = c3.add.boundMemoized(c3, 5, 4); 53 | c4 = c3.add.boundMemoized(c3, 5, 4); 54 | c4 = c3.add.boundMemoized(c3, 5, 4); 55 | assert.equal(c4+'', '5 4'); 56 | assert.equal(addCalled, 2); 57 | }); 58 | 59 | it('should memoize', function() { 60 | var addCalled = 0; 61 | var add = function(a, b, c) { 62 | addCalled++; 63 | return a + b + c; 64 | }; 65 | assert.equal(add.memoized(1, 2, 3), 6); 66 | assert.equal(add.memoized(1, 2, 3), 6); 67 | assert.equal(addCalled, 1); 68 | }); 69 | }); 70 | -------------------------------------------------------------------------------- /js/CubeCoords.spec.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | require('./Function'); 4 | var assert = require('assert'); 5 | var CubeCoords = require('./CubeCoords'); 6 | var Coords = require('./Coords'); 7 | 8 | describe('CubeCoordinates', function() { 9 | it ('should have 6 directions and all coords sum should be 0', function() { 10 | assert.equal(CubeCoords.directions.length, 6); 11 | CubeCoords.directions.forEach(function(coord) { 12 | assert.equal(coord.vector.reduce(function(prev, cur) {return prev + cur;}, 0), 0); 13 | }); 14 | }); 15 | 16 | it('should return neighbour coords for cube coord', function() { 17 | var c = new CubeCoords(5, -3, 2); 18 | var neighbours = c.neighbours(); 19 | assert.equal(neighbours.length, 6); 20 | assert.equal(neighbours[0] + '', '6 -4 2'); 21 | assert.equal(neighbours[1] + '', '6 -3 1'); 22 | assert.equal(neighbours[5] + '', '5 -4 3'); 23 | }); 24 | 25 | it('should calculate rings by radius', function() { 26 | var c = new CubeCoords(0, 0, 0); 27 | var ring = c.ring(0); 28 | assert.equal(ring.length, 1); 29 | assert.equal(ring[0].toString(), '0 0 0'); 30 | ring = c.ring(1); 31 | assert.equal(ring.length, 6); 32 | assert.equal(ring[0].toString(), '-1 0 1'); 33 | assert.equal(ring[1].toString(), '0 -1 1'); 34 | assert.equal(ring[2].toString(), '1 -1 0'); 35 | assert.equal(ring[3].toString(), '1 0 -1'); 36 | assert.equal(ring[4].toString(), '0 1 -1'); 37 | assert.equal(ring[5].toString(), '-1 1 0'); 38 | ring = c.ring(2); 39 | assert.equal(ring.length, 12); 40 | }); 41 | 42 | it('should create cube coords from offset', function() { 43 | var c = new Coords(1, 1); 44 | var c2 = CubeCoords.fromOffset(c, true); 45 | assert.deepEqual(c2.vector, [0, -1, 1]); 46 | assert.deepEqual(c2.toOffset(true).vector, c.vector); 47 | var c3 = CubeCoords.fromOffset(c); 48 | assert.deepEqual(c3.vector, [1, -2, 1]); 49 | assert.deepEqual(c3.toOffset(false).vector, c.vector); 50 | c = new Coords(0, 0); 51 | c2 = CubeCoords.fromOffset(c, true); 52 | c3 = CubeCoords.fromOffset(c); 53 | assert.deepEqual(c2.vector, [0, 0, 0]); 54 | assert.deepEqual(c2.toOffset(true).vector, c.vector); 55 | assert.deepEqual(c3.vector, [0, 0, 0]); 56 | assert.deepEqual(c3.toOffset(false).vector, c.vector); 57 | c = new Coords(-1, -1); 58 | c2 = CubeCoords.fromOffset(c, true); 59 | assert.deepEqual(c2.vector, [-1, 2, -1]); 60 | assert.deepEqual(c2.toOffset(true).vector, c.vector); 61 | c3 = CubeCoords.fromOffset(c); 62 | assert.deepEqual(c3.vector, [0, 1, -1]); 63 | assert.deepEqual(c3.toOffset(false).vector, c.vector); 64 | }); 65 | 66 | }); 67 | 68 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # EPOH 2 | 3 | Multiplayer turn-based browser strategy game 4 | 5 | ## Try it! ## 6 | 7 | Gameplay video: https://www.youtube.com/watch?v=Sj8hVMo-hfs 8 | 9 | Multiplayer server: [Offline] 10 | 11 | Singleplayer demo: https://tautvilas.github.io/epoh/ 12 | 13 | ![EPOH screenshot](https://tautvilas.github.com/epoh/img/screen.png) 14 | 15 | ## Gameplay guide ## 16 | 17 | ###Your first turn:### 18 | 19 | Click on your base. Base actions will appear at the bottom of the screen. There is one worker garrisoned in the base. 20 | You can ungarrison it by clicking ungarrison action and destination tile. All units move only by one tile so you can 21 | only ungarrison worker next to the base. Select base again, you can build a new worker by selecting 'worker' action 22 | and destination tile. Worker costs 100 iron. You can see your current resources in bottom left of the screen in format 23 | [resource amount]/[max storage] income. 24 | 25 | ###Discover resources & build a mine:### 26 | 27 | Move workers around the map to discover resources on map tiles. Resource type is 28 | indicated by small circle on tile. Also tile & unit stats can be visible while hovering on tile in the top right section of the map. 29 | When you discover resource on tile with a worker you can build mine on it. To build a mine select worker and destination tile. 30 | Each building requires resources and you will not be allowed to build if you don't have enought of them. Building a mine on a tile 31 | with resource will increase your income of that resource. Building bigmine will allso increase storage capacity. You can see 32 | how much resources each building/unit costs by hoving on their build action image. 33 | 34 | ###Build units:### 35 | 36 | Select base to build units. Your combat units can fight enemy units & takover enemy structures (except rover). 37 | 38 | ###Special worker buildings:### 39 | 40 | Build amplifier to expand territory. Build teleport for fast unit transportation between tiles. Build powerplants on gas, oil or coal when you 41 | run out of power. 42 | 43 | More about game mechanics can be found [here](https://tautvilas.github.io/epoh/rules.html) 44 | 45 | ## Local installation & Development ## 46 | 47 | 1) npm install 48 | 49 | 2a) open index.html with browser for singleplayer 50 | 51 | 2b) run "node serve" and open local multiplayer server at localhost:8080 52 | 53 | Change game settings in js/Config.js 54 | 55 | To run unit tests: 56 | 57 | $ make test 58 | 59 | To lint code: 60 | 61 | $ make lint 62 | 63 | To build bundle.js, used when PRODUCTION is true in Config.js 64 | 65 | $ make bundle.js 66 | 67 | 68 | ## Misc ## 69 | 70 | [Map generation visualization](https://tautvilas.github.io/epoh/perlin.html) (takes time to load) 71 | 72 | [Info about cube coordinates calculation](http://www.redblobgames.com/grids/hexagons/) 73 | -------------------------------------------------------------------------------- /css/hud.css: -------------------------------------------------------------------------------- 1 | ul { 2 | list-style: none 3 | } 4 | 5 | #actions-wrapper.hidden { 6 | bottom: -120px; 7 | } 8 | 9 | #actions-wrapper { 10 | position: absolute; 11 | left: 50%; 12 | bottom: 20px; 13 | transition: bottom 0.05s ease-out; 14 | } 15 | 16 | #actions { 17 | position: relative; 18 | z-index: 100; 19 | left: -50%; 20 | } 21 | 22 | #actions .action { 23 | margin-right: 10px; 24 | float: left; 25 | width: 76px; 26 | color: #bbb; 27 | font-size: 10px; 28 | } 29 | 30 | #actions .action button { 31 | cursor: pointer; 32 | outline: 0; 33 | width: 76px; 34 | height: 76px; 35 | } 36 | #actions .action:last-child { 37 | margin-right: 0; 38 | } 39 | 40 | #actions button.selected { 41 | outline: 2px dashed #5ff342; 42 | } 43 | 44 | .endturn { 45 | display: block; 46 | position: absolute; 47 | z-index: 110; 48 | width: 322px; 49 | bottom: 0px; 50 | right: 0px; 51 | cursor: pointer; 52 | color: #bbb; 53 | background-color: rgba(30, 30, 30, 0.8); 54 | border-color: #444; 55 | } 56 | 57 | .endturn:hover { 58 | background-color: rgba(130, 130, 130, 1); 59 | } 60 | .endturn::before { 61 | content: 'End Turn'; 62 | } 63 | 64 | .endturn.disabled::before { 65 | content: 'Waiting for other players'; 66 | } 67 | 68 | .endturn.resolving::before { 69 | content: 'Resolving turn...'; 70 | } 71 | 72 | #log { 73 | width: 300px; 74 | background-color: rgba(30, 30, 30, 0.8); 75 | position: absolute; 76 | top: 0px; 77 | left: 0px; 78 | bottom: 400px; 79 | z-index: 10; 80 | border: 1px solid #444; 81 | padding: 10px; 82 | overflow-y: auto; 83 | } 84 | 85 | #log .message { 86 | color: #bbb; 87 | cursor: pointer; 88 | } 89 | #log .message:hover { 90 | background-color: rgba(130, 130, 130, 0.5); 91 | } 92 | 93 | #right { 94 | position: absolute; 95 | right: 0; 96 | top: 0; 97 | } 98 | 99 | #left { 100 | position: absolute; 101 | left: 0; 102 | top: 0; 103 | bottom: 0; 104 | } 105 | 106 | #infobox, #statusbox, #scores, #unitbox, #actions, #resources { 107 | min-width: 300px; 108 | background-color: rgba(30, 30, 30, 0.8); 109 | margin-top: -1px; 110 | border: 1px solid #444; 111 | padding: 10px; 112 | position: relative; 113 | z-index: 100; 114 | } 115 | 116 | #resources { 117 | position: absolute; 118 | bottom: 0; 119 | height: 380px; 120 | } 121 | 122 | #actions { 123 | min-width: auto; 124 | } 125 | 126 | #infobox .info, 127 | #unitbox .info, 128 | #unitbox .info span, 129 | #infobox .info span, 130 | #statusbox .info, 131 | #statusbox span, 132 | #log .message, 133 | #scores li, 134 | #resources .info, #resources span { 135 | line-height: 1.4em; 136 | font-size: 15px; 137 | color: #bbb; 138 | } 139 | 140 | [data-bind='time'] { 141 | color: lightgreen !important 142 | } 143 | 144 | [data-bind='power'] { 145 | color: #7DF9FF !important 146 | } 147 | -------------------------------------------------------------------------------- /js/Sector.spec.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var assert = require('assert'); 4 | var Sector = require('./Sector'); 5 | 6 | describe('Sector', function() { 7 | it('should costruct sector from coords', function() { 8 | assert.equal(Sector.fromCoords(13, 0).value, 'CWW'); 9 | assert.equal(Sector.fromCoords(0, 0).value, '0'); 10 | assert.equal(Sector.fromCoords(12, 0).value, 'NN'); 11 | assert.equal(Sector.fromCoords(25, 0).value, 'C00'); 12 | assert.equal(Sector.fromCoords(-3, -5).value, 'HN'); 13 | assert.equal(Sector.fromCoords(3, 12).value, 'RU'); 14 | assert.equal(Sector.fromCoords(-5, 5).value, 'F0'); 15 | assert.equal(Sector.fromCoords(-625, -625).value, 'H0000'); 16 | assert.equal(Sector.fromCoords(-1758907, -1748317).value, 'HELLOKITTY'); 17 | assert.equal(Sector.fromCoords(-77244, 386).value, 'G00DL0RD'); 18 | }); 19 | 20 | it('should return sector coords', function() { 21 | assert.equal((new Sector('CWW')).position+'', '13 0'); 22 | assert.equal((new Sector('RU').position)+'', '3 12'); 23 | assert.equal((new Sector('0')).position+'', '0 0'); 24 | assert.equal((new Sector('HN')).position+'', '-3 -5'); 25 | assert.equal((new Sector('HELLOKITTY')).position+'', '-1758907 -1748317'); 26 | assert.equal((new Sector('G00DL0RD')).position+'', '-77244 386'); 27 | }); 28 | 29 | it('should find sector that is right of current sector', function() { 30 | var sector = new Sector('0'); 31 | var values = ['C', 'N', 'CW', 'CG', 'C0', 'CC', 'CN', 'NW', 'NG', 'N0', 'NC', 'NN', 'CWW']; 32 | values.forEach(function(val) { 33 | sector = sector.right(); 34 | assert.equal(sector.value, val); 35 | }); 36 | sector = new Sector('AL'); 37 | values = ['BY', 'BI', 'BJ', 'BK', 'BL', 'MY', 'MI', 'MJ', 'MK', 'ML', 'CXY']; 38 | values.forEach(function(val) { 39 | sector = sector.right(); 40 | assert.equal(sector.value, val); 41 | }); 42 | sector = new Sector('GN'); 43 | values = ['W', 'G', '0', 'C']; 44 | values.forEach(function(val) { 45 | sector = sector.right(); 46 | 47 | assert.equal(sector.value, val); 48 | }); 49 | sector = new Sector('HN'); 50 | values = ['AW', 'AG', 'A0']; 51 | values.forEach(function(val) { 52 | sector = sector.right(); 53 | assert.equal(sector.value, val); 54 | }); 55 | sector = new Sector('GNN'); 56 | assert.equal(sector.right().value, 'WW'); 57 | sector = new Sector('XMM'); 58 | assert.equal(sector.right().value, 'HXX'); 59 | }); 60 | 61 | it('should know neighbour sectors', function() { 62 | assert.equal((new Sector('HN')).position+'', '-3 -5'); 63 | assert.equal((new Sector('HN').right()).position+'', '-2 -5'); 64 | assert.equal((new Sector('HN').left()).position+'', '-4 -5'); 65 | assert.equal((new Sector('HN').up()).position+'', '-3 -6'); 66 | assert.equal((new Sector('HN').down()).position+'', '-3 -4'); 67 | }); 68 | 69 | it('should return next sector', function() { 70 | assert.equal((new Sector('0')).next().value, 'A'); 71 | assert.equal((new Sector('0')).next().next().next().value, 'C'); 72 | assert.equal((new Sector('Y')).next().value, 'A0'); 73 | assert.equal((new Sector('Y')).next().next().value, 'AA'); 74 | assert.equal((new Sector('ABY')).next().value, 'AC0'); 75 | }); 76 | 77 | it('should trim prepending zeroes', function() { 78 | assert.equal((new Sector('0')).value, '0'); 79 | assert.equal((new Sector('0ABB')).value, 'ABB'); 80 | assert.equal((new Sector('00A0B0B0')).value, 'A0B0B0'); 81 | }); 82 | 83 | }); 84 | 85 | -------------------------------------------------------------------------------- /js/Sector.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var Coords = require('./Coords'); 4 | var Config = require('./Config'); 5 | 6 | var Sector = function(sector) { 7 | this.value = sector === '0' ? sector : sector.replace(/^[0]+/g, ''); 8 | this.position = getPosition(sector); 9 | 10 | function getPosition(s) { 11 | var length = sector.length; 12 | var xpos = 0; 13 | var ypos = 0; 14 | var halfx = Math.floor(Sector.MAP[0].length / 2); 15 | var halfy = Math.floor(Sector.MAP.length / 2); 16 | for (var i = 0; i < length; i++) { 17 | var c = s.charAt(i); 18 | xpos += (Sector.LOC[c][1] - halfx) * Math.pow(5, length - i - 1); 19 | ypos += (Sector.LOC[c][0] - halfy) * Math.pow(5, length - i - 1); 20 | } 21 | return new Coords(xpos, ypos); 22 | } 23 | 24 | this.up = function() { 25 | return Sector.fromCoords(this.position.x, this.position.y - 1); 26 | }; 27 | 28 | this.down = function() { 29 | return Sector.fromCoords(this.position.x, this.position.y + 1); 30 | }; 31 | 32 | this.left = function() { 33 | return Sector.fromCoords(this.position.x - 1, this.position.y); 34 | }; 35 | 36 | this.right = function() { 37 | return Sector.fromCoords(this.position.x + 1, this.position.y); 38 | }; 39 | 40 | this.next = function() { 41 | function nextChar(c) { 42 | var cLength = Sector.CHARS.length; 43 | var position = Sector.CHARS.indexOf(c); 44 | if (position != cLength - 1) { 45 | return Sector.CHARS[++position]; 46 | } else { 47 | return false; 48 | } 49 | } 50 | var val = '0' + this.value; 51 | for (var i = val.length - 1; i >= 0; i--) { 52 | var n = nextChar(val.charAt(i)); 53 | if (n) { 54 | var s = val.substr(0, i) + n + val.substr(i+1, val.length).replace(/./g, '0'); 55 | return new Sector(s); 56 | } 57 | } 58 | }; 59 | 60 | this.toString = function() { 61 | return this.value; 62 | }; 63 | }; 64 | 65 | Sector.fromTileCoords = function(val) { 66 | var sectorCoords = new Coords(Math.floor(val.x / Config.SECTOR_WIDTH), Math.floor(val.y / Config.SECTOR_HEIGHT)); 67 | var sector = Sector.fromCoords(sectorCoords.x, sectorCoords.y); 68 | return sector; 69 | }; 70 | 71 | Sector.fromCoords = function(x, y) { 72 | function pcoord(l, pow, len) { 73 | var power = Math.pow(len, pow); 74 | var cx = Math.round(l / power); 75 | return cx; 76 | } 77 | function coord(l, len) { 78 | var pow = -1; 79 | var crd = 3; 80 | do { 81 | crd = pcoord(l, ++pow, len); 82 | } while(Math.abs(crd) > Math.floor(len / 2)); 83 | return pow; 84 | } 85 | 86 | var sector = ''; 87 | var lenx = Sector.MAP[0].length; 88 | var leny = Sector.MAP.length; 89 | var lx = x; 90 | var ly = y; 91 | var maxpow = Math.max(coord(lx, lenx), coord(ly, leny)); 92 | do { 93 | var xx = pcoord(lx, maxpow, lenx); 94 | var yy = pcoord(ly, maxpow, leny); 95 | sector += Sector.MAP[yy + Math.floor(leny / 2)][xx + Math.floor(lenx / 2)]; 96 | lx -= xx * Math.pow(lenx, maxpow); 97 | ly -= yy * Math.pow(leny, maxpow); 98 | maxpow--; 99 | } while(maxpow >= 0); 100 | 101 | return new Sector(sector); 102 | }; 103 | 104 | Sector.MAP = [ 105 | ['Y', 'I', 'J', 'K', 'L'], 106 | ['X', 'H', 'A', 'B', 'M'], 107 | ['W', 'G', '0', 'C', 'N'], 108 | ['V', 'F', 'E', 'D', 'O'], 109 | ['U', 'T', 'S', 'R', 'P'] 110 | ]; 111 | 112 | Sector.CHARS = '0ABCDEFGHIJKLMNOPRSTUVWXY'; 113 | 114 | (function() { 115 | Sector.LOC = {}; 116 | for (var y = 0; y < Sector.MAP.length; y++) { 117 | for (var x = 0; x < Sector.MAP[y].length; x++) { 118 | Sector.LOC[Sector.MAP[y][x]] = [y, x]; 119 | } 120 | } 121 | })(); 122 | 123 | module.exports = Sector; 124 | module.exports.Coords = Coords; 125 | -------------------------------------------------------------------------------- /js/Unit.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var Intent = require('./Intent'); 4 | 5 | var Unit = function(proto, proto2) { 6 | for (var key in proto) { 7 | this[key] = proto[key]; 8 | } 9 | if (proto2) { 10 | for (key in proto2) { 11 | this[key] = proto2[key]; 12 | } 13 | } 14 | }; 15 | 16 | Unit.prototype.toString = function() { 17 | return this.name; 18 | }; 19 | 20 | Unit.TYPE_STRUCTURE = 'structure'; 21 | Unit.TYPE_MECHANICAL = 'mechanical'; 22 | 23 | Unit.BASE = { 24 | actions: ['rover', 'worker', 'marine', Intent.TERMINATOR, Intent.BOMBARD], 25 | cost: { 26 | iron: 500 27 | }, 28 | damage: 0, 29 | defense: 50, 30 | field: 2, 31 | health: 100, 32 | maxHealth: 100, 33 | multiaction: true, 34 | name: 'base', 35 | range: 3, 36 | rangedDmg: 5, 37 | type: Unit.TYPE_STRUCTURE 38 | }; 39 | Unit.ROVER = { 40 | actions: [Intent.MOVE], 41 | cost: { 42 | iron: 100, 43 | power: 10 44 | }, 45 | damage: 10, 46 | health: 15, 47 | maxHealth: 15, 48 | name: 'rover', 49 | range: 2, 50 | type: Unit.TYPE_MECHANICAL 51 | }; 52 | Unit.MARINE = { 53 | actions: [Intent.MOVE], 54 | cost: { 55 | iron: 100, 56 | coal: 60, 57 | power: 10 58 | }, 59 | damage: 15, 60 | health: 20, 61 | maxHealth: 20, 62 | name: 'marine', 63 | garrisonable: true, 64 | range: 2 65 | }; 66 | Unit[Intent.TERMINATOR] = { 67 | actions: [Intent.MOVE], 68 | cost: { 69 | iron: 300, 70 | coal: 100, 71 | oil: 100, 72 | copper: 50, 73 | power: 30 74 | }, 75 | damage: 30, 76 | health: 30, 77 | maxHealth: 50, 78 | name: Intent.TERMINATOR, 79 | garrisonable: true, 80 | range: 3 81 | }; 82 | Unit.WORKER = { 83 | actions: [Intent.MOVE, Intent.DECONSTRUCT, 'amplifier', 'mine', 'bigmine', Intent.POWERPLANT, 'teleport'], 84 | cost: { 85 | iron: 100, 86 | power: 10 87 | }, 88 | damage: 5, 89 | health: 10, 90 | maxHealth: 10, 91 | name: 'worker', 92 | garrisonable: true, 93 | range: 2, 94 | type: Unit.TYPE_MECHANICAL 95 | }; 96 | 97 | Unit.AMPLIFIER = { 98 | actions: [], 99 | cost: { 100 | iron: 50, 101 | power: 20 102 | }, 103 | damage: 0, 104 | defense: 40, 105 | field: 2, 106 | health: 15, 107 | maxHealth: 15, 108 | name: 'amplifier', 109 | range: 3, 110 | type: Unit.TYPE_STRUCTURE 111 | }; 112 | 113 | Unit.TELEPORT = { 114 | actions: [], 115 | cost: { 116 | power: 10 117 | }, 118 | damage: 0, 119 | defense: 20, 120 | health: 1, 121 | maxHealth: 1, 122 | name: 'teleport', 123 | range: 1, 124 | type: Unit.TYPE_STRUCTURE 125 | }; 126 | 127 | Unit.MINE = { 128 | actions: [], 129 | cost: { 130 | iron: 150, 131 | power: 20 132 | }, 133 | damage: 0, 134 | defense: 50, 135 | health: 50, 136 | maxHealth: 50, 137 | name: 'mine', 138 | range: 2, 139 | type: Unit.TYPE_STRUCTURE 140 | }; 141 | 142 | Unit.BIGMINE = { 143 | actions: [], 144 | cost: { 145 | iron: 200, 146 | power: 20 147 | }, 148 | damage: 0, 149 | health: 50, 150 | defense: 50, 151 | maxHealth: 50, 152 | name: 'bigmine', 153 | range: 2, 154 | type: Unit.TYPE_STRUCTURE 155 | }; 156 | 157 | Unit.POWERPLANT = { 158 | actions: [], 159 | cost: { 160 | iron: 200 161 | }, 162 | damage: 0, 163 | health: 50, 164 | defense: 20, 165 | maxHealth: 50, 166 | name: Intent.POWERPLANT, 167 | range: 2, 168 | type: Unit.TYPE_STRUCTURE 169 | }; 170 | 171 | Unit.fromPrototype = function(proto1, proto2) { 172 | return new Unit(proto1, proto2); 173 | }; 174 | 175 | Unit.fromName = function(name, proto) { 176 | for (var key in Unit) { 177 | var unit = Unit[key]; 178 | if (unit.name && unit.name === name) { 179 | return new Unit(unit, proto); 180 | } 181 | } 182 | }; 183 | 184 | module.exports = Unit; 185 | -------------------------------------------------------------------------------- /rules.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Rules 4 | 5 |
 6 | % about
 7 | 
 8 | Discover & mine resources, build your army, expand and conquer your enemies!
 9 | Player with the biggest score wins.
10 | 
11 | % guide
12 | 
13 | Your first turn:
14 | 
15 | LMB on your base. Base actions will appear at the bottom of the screen. There is one worker garrisoned in the base.
16 | You can ungarrison it by clicking ungarrison action and destination tile. All units move only by one tile so you can
17 | only ungarrison worker next to the base. Select base again, you can build a new worker by selecting 'worker' action
18 | and destination tile. Worker costs 100 iron. You can see your current resources in bottom left of the screen in format
19 | [resource amount]/[max storage] income.
20 | 
21 | Discover resources & build a mine:
22 | 
23 | Move workers around the map to discover resources on map tiles. Resource type is
24 | indicated by small circle on tile. Also tile & unit stats can be visible while hovering on tile in the top right section of the map.
25 | When you discover resource on tile with a worker you can build mine on it. To build a mine select worker and destination tile.
26 | Each building requires resources and you will not be allowed to build if you don't have enought of them. Building a mine on a tile
27 | with resource will increase your income of that resource. Building bigmine will allso increase storage capacity. You can see
28 | how much resources each building/unit costs by hoving on their build action image.
29 | 
30 | 
31 | Build units:
32 | 
33 | Select base to build units. Your combat units can fight enemy units & takover enemy structures (except rover).
34 | 
35 | Special worker buildings:
36 | 
37 | Build amplifier to expand territory. Build teleport for fast unit transportation between tiles. Build powerplants on gas, oil or coal when you
38 | run out of power.
39 | 
40 | % controls
41 | 
42 | LMB: elect
43 | RMB: center map
44 | 
45 | protip: click on log event to go to event location
46 | 
47 | % turn resolution order
48 | 
49 | 1) resolve combat without removing units
50 | 2) resolve unit health and remove dead units
51 | 3) resolve deconstruction
52 | 4) resolve movement
53 | 5) resolve restacking
54 | 6) resolve building
55 | 7) resolve teleporting
56 | 
57 | % combat
58 | 
59 | 1) Unit attack damage is proportional to its health.
60 | 2) Unit that does not move and defends agains multiple enemies splits attack dmg by number of enemies and defends agains every attacker
61 | 3) Attacking unit does not get defence modifier if defender is not moving
62 | 4) If unit runs away to empty tile while being attacked it gets only half damage from attacker and no dmg is taken by attacker. No terrain def modifiers for defender
63 | 5) If structure is garrisoned then combat happens between attacker and garrisoned unit
64 | 6) units receive defence from tiles or garrison if they are garrisoned
65 | 7) garrisoned units can not be ungarrisoned onto enemy unit (they are sieged)
66 | 
67 | % resolve health
68 | 
69 | 1) health is rounded up
70 | 2) if unit health is less than 0 it is removed from the map
71 | 
72 | % movement
73 | 
74 | 1) If two or more units go to same tile, first placed intent is resolved first.
75 | 2) If garrisonable units moves onto structure, the structure is garrisoned
76 | 3) Garissoned structure changes owner depending on garrisoned unit
77 | 4) worker can discover resources when moving on tile
78 | 
79 | % restacking
80 | 
81 | 1) If unit moves onto same type non-moving friendly unit then dest health is refilled from source health
82 | 
83 | % building
84 | 
85 | 1) You can only build if you have enough resources
86 | 2) You can only build on tile with your field (except teleports)
87 | 3) Mines can only be built on resources tile
88 | 4) Power plants can be build on oil/coal/gas tiles
89 | 5) If building intent fails player is refunded (if build target tile is occupied)
90 | 6) If worker/base is destroyed with build ordered, build is not refunded
91 | 7) each teleport is linked to previously built unlinked teleport
92 | 
93 | 94 | 95 | -------------------------------------------------------------------------------- /js/Rules.spec.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var assert = require('assert'); 4 | var Tile = require('./Tile'); 5 | var Intent = require('./Intent'); 6 | var Unit = require('./Unit'); 7 | var Rules = require('./Rules'); 8 | var Coords = require('./Coords'); 9 | 10 | describe('Rules', function() { 11 | 12 | var MARINE = { 13 | actions: ['move'], 14 | cost: { 15 | iron: 100, 16 | coal: 60, 17 | power: 10 18 | }, 19 | damage: 10, 20 | health: 20, 21 | maxHealth: 20, 22 | name: 'marine', 23 | range: 2 24 | }; 25 | 26 | describe('Combat', function() { 27 | 28 | it('should reduce damage by terrain defence % for not moving defending unit', function() { 29 | var src = new Tile(Tile.HILLS); 30 | var dest = new Tile(Tile.HILLS); 31 | src.unit = new Unit(MARINE, {player: 'john'}); 32 | dest.unit = new Unit(MARINE, {player: 'peter'}); 33 | var intent = {type: Intent.MOVE}; 34 | var resolution = Rules.resolveCombat(src, dest, intent, {}, {src: 1, dst: 1}); 35 | assert.equal(resolution.srcDiff, -10); 36 | assert.equal(resolution.destDiff, -8); 37 | }); 38 | 39 | it('should not perform combat on ungarrisoned structures', function() { 40 | var src = new Tile(Tile.HILLS); 41 | var dest = new Tile(Tile.HILLS); 42 | src.unit = new Unit(MARINE, {player: 'john'}); 43 | dest.unit = new Unit(Unit.BASE, {player: 'peter'}); 44 | var intent = {type: Intent.MOVE}; 45 | var resolution = Rules.resolveCombat(src, dest, intent, {}, {src: 1, dst: 1}); 46 | assert.equal(resolution, false); 47 | }); 48 | 49 | it('should perform combat with structure garrison', function() { 50 | var src = new Tile(Tile.HILLS); 51 | var dest = new Tile(Tile.HILLS); 52 | src.unit = new Unit(MARINE, {player: 'john'}); 53 | dest.unit = new Unit(Unit.BASE, {player: 'peter'}); 54 | dest.unit.garrison = new Unit(MARINE, {player: 'peter'}); 55 | var intent = {type: Intent.MOVE}; 56 | var resolution = Rules.resolveCombat(src, dest, intent, {}, {src: 1, dst: 1}); 57 | assert.equal(resolution.srcDiff, -10); 58 | assert.equal(resolution.destDiff, -5); 59 | assert.equal(resolution.garrison, dest.unit); 60 | }); 61 | 62 | it('should reduce damage by 50% to fleeing unit', function() { 63 | var src = new Tile(Tile.HILLS); 64 | src.coords = new Coords(0, 1); 65 | var dest = new Tile(Tile.PLAINS); 66 | dest.coords = new Coords(1, 0); 67 | src.unit = new Unit(MARINE, {player: 'john'}); 68 | dest.unit = new Unit(MARINE, {player: 'peter'}); 69 | var intent = {type: Intent.MOVE}; 70 | var destIntents = {}; 71 | destIntents[new Coords(1, 1)] = {type: Intent.MOVE, tile: {}}; 72 | var resolution = Rules.resolveCombat(src, dest, intent, destIntents, {src: 1, dst: 1}); 73 | assert.equal(resolution.srcDiff, 0); 74 | assert.equal(resolution.destDiff, -5); 75 | }); 76 | 77 | }); 78 | 79 | describe('Movement', function() { 80 | it('should move unit if the unit is marine and dest tile is empty', function() { 81 | var src = new Tile(Tile.PLAINS); 82 | var dest = new Tile(Tile.PLAINS); 83 | src.unit = new Unit(MARINE); 84 | var intent = {type: Intent.MOVE}; 85 | var resolution = Rules.resolveMovement(src, dest, intent); 86 | assert.deepEqual(resolution, { 87 | src: undefined, 88 | dest: new Unit(MARINE) 89 | }); 90 | }); 91 | }); 92 | 93 | describe('Deconstruction', function() { 94 | it('should not deconstruct if no src or dst unit or intent type incorrect', function() { 95 | assert.equal(Rules.resolveDeconstruction({}, {}, ''), false); 96 | assert.equal(Rules.resolveDeconstruction({unit: {}}, {}, {type: 'deconstruct'}), false); 97 | assert.equal(Rules.resolveDeconstruction({}, {unit: {}}, {type: 'deconstruct'}), false); 98 | }); 99 | 100 | it('should deconstruct if parameters are correct', function() { 101 | var srcUnit = {}; 102 | assert.deepEqual(Rules.resolveDeconstruction( 103 | {unit: srcUnit}, {unit: {}}, {type: 'deconstruct'}), {src: srcUnit, dest: undefined}); 104 | }); 105 | }); 106 | }); 107 | -------------------------------------------------------------------------------- /js/Client.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var Stream = require('./Stream'); 4 | var WebComponent = require('./WebComponent'); 5 | var Renderer = require('./Renderer'); 6 | var Sector = require('./Sector'); 7 | var Config = require('./Config'); 8 | var Coords = require('./Coords'); 9 | var Api = require('./Api'); 10 | var Q = require('./Q'); 11 | 12 | var Client = function() { 13 | var mapCache = {}; 14 | var api = new Api(); 15 | var username; 16 | var dirtyMap = true; 17 | var statusBox = new WebComponent('status'); 18 | 19 | var bus = new Stream(); 20 | function getSectors(sectors) { 21 | sectors.forEach(function(s) { 22 | mapCache[s] = true; 23 | }); 24 | return api.getSectors(sectors, username).then(function(data) { 25 | data.forEach(function(s, i) { 26 | mapCache[sectors[i]] = s; 27 | }); 28 | bus.push([].concat.apply([], data)); 29 | if (dirtyMap) { 30 | setTimeout(function() { 31 | Q.byClass('endturn')[0].className = 'endturn'; 32 | Q.byId('blocker').className = ''; 33 | }, 100); 34 | dirtyMap = false; 35 | } 36 | }); 37 | } 38 | 39 | var addPlayer = function(username) { 40 | api.addPlayer(username).then(function(player) { 41 | Q.byClass('endturn')[0].onclick = function() { 42 | Q.byTag('title')[0].textContent = 'Epoh'; 43 | api.endTurn(username); 44 | Q.byClass('endturn')[0].classList.add('disabled'); 45 | }; 46 | Q.byId('game').style.display = 'block'; 47 | Q.byId('login').style.display = 'none'; 48 | var renderer = initRenderer(bus); 49 | api.listen(function(evt) { 50 | if (evt.type === 'end') { 51 | alert('Match time is over. Refresh page to restart'); 52 | } else if (evt.type === 'resolution') { 53 | Q.byId('blocker').className = 'visible'; 54 | Q.byClass('endturn')[0].className = 'endturn resolving'; 55 | } else if (evt.type === 'orders') { 56 | api.getStatus(username).then(function(e) { 57 | if (!e.player.units) { 58 | alert("You lost!"); 59 | dirtyMap = true; 60 | mapCache = {}; 61 | renderer.reset(e); 62 | } else { 63 | dirtyMap = true; 64 | mapCache = {}; 65 | renderer.reset(e); 66 | Q.byTag('title')[0].textContent = '* Epoh'; 67 | } 68 | }); 69 | } 70 | }); 71 | api.getStatus(username).then(function(e) { 72 | //mapCache = {}; 73 | renderer.reset(e); 74 | Q.byTag('title')[0].textContent = '* Epoh'; 75 | }); 76 | renderer.setCenter(Coords.fromString(player.base.representation)); 77 | }).catch(function() { 78 | Q.byId('game').style.display = 'none'; 79 | Q.byId('login').style.display = 'block'; 80 | Q.byId('login').querySelector('.error').style.display = 'block'; 81 | }); 82 | }; 83 | 84 | function initRenderer(bus) { 85 | var renderer = new Renderer(bus); 86 | 87 | renderer.tileRequests.onValue(function(values) { 88 | var sectors = []; 89 | if (values.length) { 90 | values.forEach(function(val) { 91 | var sector = Sector.fromTileCoords(val); 92 | if (!mapCache[sector.value] && sectors.indexOf(sector.value) === -1) { 93 | sectors.push(sector.value); 94 | } 95 | }); 96 | } 97 | if (sectors.length) { 98 | getSectors(sectors); 99 | } 100 | }); 101 | 102 | renderer.intentBus.onValue(function(intent) { 103 | if (intent.type === 'cancel') { 104 | api.cancelIntents(username, intent.coords); 105 | } else { 106 | api.addIntent(username, intent); 107 | } 108 | }); 109 | 110 | return renderer; 111 | } 112 | 113 | api.checkSession().then(function(currentUsername) { 114 | statusBox.setState({ 115 | numusers: currentUsername.numplayers, 116 | matchtime: Math.round(currentUsername.matchtime / 60 / 1000) + ' mins' 117 | }); 118 | if (!Config.USERNAME_VIEW || currentUsername.user) { 119 | Q.byId('game').style.display = 'block'; 120 | username = window.location.search.replace("?username=", "") || 121 | currentUsername.user || 122 | (new Date()).getTime().toString(); 123 | addPlayer(username); 124 | } else { 125 | Q.byId('login').style.display = 'block'; 126 | Q.byId('username').focus(); 127 | Q.byId('version').textContent = currentUsername.version; 128 | Q.byId('start').onclick = function() { 129 | username = Q.byId('username').value; 130 | if (username) { 131 | addPlayer(username); 132 | } 133 | }; 134 | } 135 | }); 136 | }; 137 | -------------------------------------------------------------------------------- /css/map.css: -------------------------------------------------------------------------------- 1 | * { 2 | margin: 0; 3 | padding: 0; 4 | line-height: 1.4em; 5 | font-size: 1em; 6 | font-family: "Courier New", Courier, monospace; 7 | color: #171717; 8 | } 9 | 10 | body, html { 11 | height: 100%; 12 | overflow: hidden; 13 | margin: 0; 14 | background-color: #eee; 15 | } 16 | 17 | #blocker { 18 | height: 100%; 19 | background-color: #eee; 20 | z-index: 1000; 21 | position: absolute; 22 | left: 0; 23 | top: 0; 24 | visibility: hidden; 25 | transition: visibility 0.5s, opacity 0.5s linear; 26 | opacity: 0; 27 | width: 100%; 28 | } 29 | 30 | #blocker.visible { 31 | transition: visibility 0s, opacity 0s linear; 32 | opacity: 0.2; 33 | visibility: visible; 34 | } 35 | 36 | .clearfix { 37 | overflow: auto; 38 | } 39 | 40 | #login, #game { 41 | display: none; 42 | } 43 | 44 | #game, #map { 45 | position:relative; 46 | height: 100%; 47 | } 48 | 49 | #map { 50 | overflow: hidden; 51 | float: left; 52 | width: 100%; 53 | } 54 | 55 | .unit { 56 | z-index: 2; 57 | width: 76px; 58 | height: 76px; 59 | margin: -13px 7px; 60 | display: block; 61 | position: absolute; 62 | cursor: pointer; 63 | pointer-events: none; 64 | background-repeat: no-repeat; 65 | background-position: center center; 66 | border-style: solid; 67 | border-color: white; 68 | border-width: 0px; 69 | transition: all 0.5s ease-in-out; 70 | } 71 | 72 | .notransition .unit { 73 | transition: none !important; 74 | } 75 | 76 | .unit[active=true] { 77 | border-color: red; 78 | } 79 | 80 | .unit .health { 81 | height: 2px; 82 | width: 100%; 83 | background-color: #5ff342; 84 | position: absolute; 85 | left: 0; 86 | bottom: -5px; 87 | z-index: 3; 88 | } 89 | 90 | .unit.enemy .health { 91 | background-color: red; 92 | } 93 | 94 | .unit.garrisoned::before { 95 | content: 'G'; 96 | color: white; 97 | position: absolute; 98 | right: 0; 99 | top: 0; 100 | } 101 | 102 | #cursor { 103 | opacity: 0.3; 104 | pointer-events: none; 105 | z-index: 3; 106 | } 107 | 108 | .hex { 109 | background-color: green; 110 | z-index: 0; 111 | border-style: solid; 112 | border-width: 0px 1px; 113 | width: 90px; 114 | height: 52px; 115 | position: absolute; 116 | cursor: pointer; 117 | } 118 | 119 | .hex .intent { 120 | transform: rotate(60deg); 121 | border-left: 45px solid #26c131; 122 | border-top: 26px solid rgba(255, 0, 0, 0); 123 | position: absolute; 124 | border-right: 45px solid rgba(255, 0, 0, 0); 125 | border-bottom: 26px solid rgba(255, 0, 0, 0); 126 | z-index: 10; 127 | } 128 | 129 | .hex .field { 130 | transform: rotate(60deg); 131 | position: absolute; 132 | width: 84px; 133 | height: 52px; 134 | border-left: 5px solid lightgreen; 135 | z-index: 11; 136 | } 137 | 138 | .hex .field.enemy { 139 | border-color: red 140 | } 141 | 142 | .hex.grass { 143 | background-color: 'green'; 144 | } 145 | 146 | .hex.water, .hex.water::before, .hex.water::after { 147 | background-color: #006f7c; 148 | background-image: url(../img/texture/water.png); 149 | } 150 | .hex.hills { 151 | background-color: #fb7329; 152 | } 153 | .hex.mountains { 154 | background-color: #ccc; 155 | } 156 | .hex.plains { 157 | background-color: #a04513; 158 | } 159 | .hex.shroud { 160 | background-color: #171717; 161 | } 162 | 163 | .hex::before, .hex::after { 164 | position: absolute; 165 | border-color: inherit; 166 | background-color: inherit; 167 | border-style: inherit; 168 | border-width: 0px 1px; 169 | display: block; 170 | content: ''; 171 | height: inherit; 172 | width: inherit; 173 | z-index: inherit; 174 | } 175 | 176 | .hex.undiscovered:not(.water)::before { 177 | background-image: radial-gradient(circle at 10px, rgba(0,0,0,0) 2px, white 4px, rgba(0,0,0,0) 1px); 178 | } 179 | 180 | .hex.iron::before { 181 | background-image: radial-gradient(circle at 10px, gray 2px, white 4px, rgba(0,0,0,0) 1px); 182 | } 183 | 184 | .hex.copper::before { 185 | background-image: radial-gradient(circle at 10px, orange 2px, white 4px, rgba(0,0,0,0) 1px); 186 | } 187 | 188 | .hex.gold::before { 189 | background-image: radial-gradient(circle at 10px, yellow 2px, white 4px, rgba(0,0,0,0) 1px); 190 | } 191 | 192 | .hex.oil::before { 193 | background-image: radial-gradient(circle at 10px, black 2px, white 4px, rgba(0,0,0,0) 1px); 194 | } 195 | 196 | .hex.gas::before { 197 | background-image: radial-gradient(circle at 10px, green 2px, white 4px, rgba(0,0,0,0) 1px); 198 | } 199 | 200 | .hex.coal::before { 201 | background-image: radial-gradient(circle at 10px, white 2px, white 4px, rgba(0,0,0,0) 1px); 202 | } 203 | 204 | .hex::before { 205 | transform: rotate(60deg); 206 | } 207 | 208 | .hex::after { 209 | transform: rotate(-60deg); 210 | } 211 | 212 | .hex:hover { 213 | border-color: #abcdef; 214 | border-style: solid; 215 | z-index: 1; 216 | } 217 | 218 | .hex[active=true] { 219 | border-color: #26c131; 220 | border-style: solid; 221 | z-index: 1; 222 | } 223 | 224 | -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | var express = require('express'); 2 | var cookieParser = require('cookie-parser'); 3 | var bodyParser = require('body-parser'); 4 | var WebSocketServer = require('websocket').server; 5 | var http = require('http'); 6 | 7 | var Game = require('./js/Game'); 8 | var Map = require('./js/Map'); 9 | var Config = require('./js/Config'); 10 | var Intent = require('./js/Intent'); 11 | var pjson = require('./package.json'); 12 | 13 | var game = new Game(new Map(Config.MAP_SEED)); 14 | 15 | var users = {}; 16 | var app = express(); 17 | 18 | app.use(bodyParser.json()); 19 | app.use(cookieParser()); 20 | 21 | express.static.mime.define({ 22 | 'text/html': ['dry'] 23 | }); 24 | 25 | app.use('/lib', express.static('lib')); 26 | app.use('/css', express.static('css')); 27 | app.use('/img', express.static('img')); 28 | app.use('/favicon.ico', express.static('img/favicon.ico')); 29 | 30 | app.use('/index.html', express.static('index.html')); 31 | app.use('/rules.html', express.static('rules.html')); 32 | app.use('/main.js', express.static('main.js')); 33 | 34 | if (Config.PRODUCTION) { 35 | app.use('/js/Config.js', express.static('js/Config.js')); 36 | app.use('/bundle.js', express.static('bundle.js')); 37 | } else { 38 | app.use('/js', express.static('js')); 39 | } 40 | 41 | function secureId() { 42 | return require('crypto').randomBytes(64).toString('hex'); 43 | } 44 | 45 | app.get('/', function(req, res) { 46 | res.redirect('/index.html'); 47 | }); 48 | 49 | // authentication 50 | app.use(function(req, res, next) { 51 | console.log(req.method, req.url); 52 | if (req.cookies.name) { 53 | var name = req.cookies.name.substr(0, 30); 54 | var auth = users[name]; 55 | if (auth === req.cookies.auth) { 56 | req.user = name; 57 | } 58 | } 59 | if ((req.url.substr(0, 7) === '/player' && req.method === 'POST') || 60 | req.url.substr(0, 8) === '/session') { 61 | next(); 62 | } else if (req.user) { 63 | next(); 64 | } else { 65 | res.sendStatus(403); 66 | } 67 | }); 68 | 69 | app.get('/session', function(req, res) { 70 | return res.json({ 71 | user: req.user, 72 | version: pjson.version, 73 | numplayers: Object.keys(game.players).length, 74 | matchtime: game.getMatchTime() 75 | }); 76 | }); 77 | 78 | app.post('/player/:name', function(req, res) { 79 | var user = req.user; 80 | var name = req.params.name.substr(0, 30); 81 | var player; 82 | if (user === name) { 83 | player = game.getPlayer(name); 84 | res.json(player); 85 | } else { 86 | player = game.addPlayer(name); 87 | if (player) { 88 | users[name] = secureId(); 89 | res.cookie('auth', users[name]); 90 | res.cookie('name', name); 91 | res.json(player); 92 | } else { 93 | res.sendStatus(409); 94 | } 95 | } 96 | }); 97 | 98 | app.get('/player/:name', function(req, res) { 99 | var name = req.params.name.substr(0, 30); 100 | if (name === req.user) { 101 | res.json(game.getStatus(name)); 102 | } else { 103 | res.sendStatus(403); 104 | } 105 | }); 106 | 107 | app.get('/sector/:s', function(req, res) { 108 | var sectors = req.params.s.split(','); 109 | var response = []; 110 | sectors.forEach(function(s) { 111 | response.push(game.getSector(s, req.user)); 112 | }); 113 | var json = JSON.stringify(response); 114 | res.setHeader('Content-Type', 'application/json'); 115 | res.write(json); 116 | res.end(); 117 | }); 118 | 119 | app.post('/intent', function(req, res) { 120 | var intent = req.body; 121 | res.json(game.addIntent(req.user, Intent.fromJson(intent))); 122 | }); 123 | 124 | app.delete('/tile/:coords/intents', function(req, res) { 125 | var coords = req.params.coords; 126 | res.json(game.cancelIntents(req.user, coords)); 127 | }); 128 | 129 | app.post('/turn', function(req, res) { 130 | res.json(game.endTurn(req.user)); 131 | }); 132 | 133 | var server = app.listen(Config.PORT, function() { 134 | console.log('Epoh server online on port ', Config.PORT); 135 | }); 136 | 137 | /* Websockets server */ 138 | 139 | var server = http.createServer(); 140 | server.listen(1337); 141 | var wsServer = new WebSocketServer({ 142 | httpServer: server, 143 | autoAcceptConnections: false 144 | }); 145 | 146 | var clients = []; 147 | wsServer.on('request', function(request) { 148 | var connection = request.accept(null, request.origin); 149 | clients.push(connection); 150 | connection.on('message', function(message) { 151 | //console.log('Msg received: ' + message.utf8Data); 152 | }); 153 | }); 154 | 155 | game.events.onValue(function(value) { 156 | clients.forEach(function(client) { 157 | client.sendUTF(JSON.stringify(value)); 158 | }); 159 | if (value.type === 'end') { 160 | clients.forEach(function(client) { 161 | client.close(); 162 | }); 163 | console.log('MATCH ENDED', value.scores.length); 164 | if (value.scores.length) { 165 | console.log('[_] Winner: _ (_)'.printf(Date(), value.scores[0].name, value.scores[0].score)); 166 | } 167 | process.exit(); 168 | } 169 | }); 170 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Epoh 5 | 6 | 31 | 32 | 33 | 34 | 35 | 36 |
37 | Fork me on GitHub 38 |
39 | At the end of the 21st century Earth became too hot to sustain life. The wealthiest individuals 40 | gathered all their money and set up a space mission to an earth-like planet 41 | codenamed EPOH. The surface area of this planet was divided into sectors and 42 | assigned to space mission members. After 135 years of interstellar journey the spaceship 43 | 44 | has finally reached its destination. 45 |
46 | 47 | 48 |
Username allready taken
49 |
50 | Users online: - 51 | Match time remaining: 52 |
53 |
54 |
Report bugs and suggestions: zvitruolis@gmail.com
55 |
56 | Version 57 | - Game Rules 58 | - Unit Icons
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
Power:
68 |
Iron:
69 |
Copper:
70 |
Gold:
71 |
Oil:
72 |
Gas:
73 |
Coal:
74 |
75 |
76 |
77 | 99 | 102 | 103 |
104 | 105 | 141 | 142 | 143 | -------------------------------------------------------------------------------- /js/Map.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var PerlinGenerator = require('./PerlinGenerator'); 4 | var Sector = require('./Sector'); 5 | var Coords = require('./Coords'); 6 | var CubeCoords = require('./CubeCoords'); 7 | var Tile = require('./Tile'); 8 | var Config = require('./Config'); 9 | var Unit = require('./Unit'); 10 | 11 | var Map = function(seed) { 12 | var self = this; 13 | this.seed = seed; 14 | this.generator = new PerlinGenerator(seed); 15 | this.units = {}; 16 | this.sectorCache = {}; 17 | this.tilePropsCache = {}; 18 | this.dynamicTilePropsCache = {}; 19 | this.tileCache = {}; 20 | //this.overlays = {}; 21 | this.resourceProbabilities = [ 22 | {name: 'iron', prob: 40}, 23 | {name: 'copper', prob: 60}, 24 | {name: 'coal', prob: 80}, 25 | //{name: 'gold', prob: 80}, 26 | {name: 'oil', prob: 90}, 27 | {name: 'gas', prob: 100} 28 | ]; 29 | 30 | var getTile = function(x, y) { 31 | var c = new Coords(x, y); 32 | var val0 = self.generator.generate(x / 3, y / 3); 33 | var val1 = Math.floor(val0 * 3) * 200; 34 | var tile; 35 | c = new Coords(x, y); 36 | switch(val1) { 37 | case 0: tile = new Tile(Tile.WATER, c); break; 38 | case 200: tile = new Tile(Tile.PLAINS, c); break; 39 | case 400: tile = new Tile(Tile.HILLS, c); break; 40 | default: tile = new Tile(Tile.MOUNTAINS, c); break; 41 | } 42 | return tile; 43 | }; 44 | 45 | var generateSector = function(s) { 46 | var sector = new Sector(s); 47 | var position = sector.position; 48 | var posX = position.x * Map.SECTOR_WIDTH; 49 | var posY = position.y * Map.SECTOR_HEIGHT; 50 | if (!self.sectorCache[sector]) { 51 | //console.log('generate sector _'.printf(s)); 52 | var sectorTiles = []; 53 | for (var y = posY; y < posY + Map.SECTOR_HEIGHT; y++) { 54 | for (var x = posX; x < posX + Map.SECTOR_WIDTH; x++) { 55 | var t = getTile(x, y); 56 | t.sector = sector; 57 | sectorTiles.push(t); 58 | } 59 | } 60 | // add resources 61 | var resourcesAdded = 0; 62 | for (var i = 0; i < 1000; i++) { 63 | var j = Math.floor(Math.random() * sectorTiles.length); 64 | var tile = sectorTiles[j]; 65 | if (!tile.resource && tile.name != Tile.WATER) { 66 | var resourceProb = Math.floor(Math.random() * 100); 67 | for (var z = 0; z < self.resourceProbabilities.length; z++) { 68 | if (resourceProb < self.resourceProbabilities[z].prob) { 69 | sectorTiles[j].resource = self.resourceProbabilities[z].name; 70 | resourcesAdded++; 71 | break; 72 | } 73 | } 74 | if (resourcesAdded >= Config.RESOURCES_PER_SECTOR) { 75 | break; 76 | } 77 | } 78 | } 79 | self.sectorCache[sector] = sectorTiles; 80 | } 81 | return self.sectorCache[sector]; 82 | }; 83 | 84 | this.resetTilePropsCache = function() { 85 | self.tilePropsCache = {}; 86 | }; 87 | 88 | this.getFullTile = function(tc, player) { 89 | var sector; 90 | if (!tc.substract) { 91 | tc = Coords.fromString(tc); 92 | } 93 | // cache sector name for tile because it is complex calculation 94 | if (self.tileCache[tc]) { 95 | sector = self.tileCache[tc]; 96 | } else { 97 | sector = Sector.fromTileCoords(tc); 98 | self.tileCache[tc] = sector; 99 | } 100 | 101 | var sectorTiles = generateSector(sector.value); 102 | var c = tc.substract(sector.position.scale(new Coords(Config.SECTOR_WIDTH, Config.SECTOR_HEIGHT))); 103 | 104 | var base = sectorTiles[c.y * Config.SECTOR_WIDTH + c.x]; 105 | var unit = self.units[tc]; 106 | var newTile = new Tile(base); 107 | var tileProps = self.getTileProps(tc, player); 108 | newTile.sector = sector; 109 | newTile.resource = tileProps.discovered || !player ? base.resource : tileProps.visible ? 'undiscovered' : undefined; 110 | newTile.field = tileProps.field; 111 | newTile.visible = tileProps.visible; 112 | //newTile.overlay = tileProps.overlay; 113 | newTile.unit = unit; 114 | return newTile; 115 | }; 116 | 117 | this.getTileCoords = function(sector, x, y) { 118 | var position = sector.position; 119 | var posX = position.x * Map.SECTOR_WIDTH; 120 | var posY = position.y * Map.SECTOR_HEIGHT; 121 | return new Coords(x, y).add(new Coords(posX, posY)); 122 | }; 123 | 124 | function accessTileProps(coords, player, useCache) { 125 | var cache = useCache || self.tilePropsCache; 126 | var tileProps = cache[coords]; 127 | if (!tileProps) { 128 | tileProps = {}; 129 | cache[coords] = tileProps; 130 | } 131 | if (!tileProps[player]) { 132 | tileProps[player] = {}; 133 | } 134 | return tileProps; 135 | } 136 | 137 | this.calculateProps = function(units) { 138 | Object.keys(units).forEach(function(key) { 139 | var unit = self.units[key]; 140 | var ufield = unit.field || 0; 141 | var urange = unit.range || 0; 142 | var player = unit.player; 143 | var maxRing = Math.max(ufield, urange); 144 | var unitCoords = Coords.fromString(key); 145 | var unitCubeCoords = CubeCoords.fromOffset(unitCoords); 146 | 147 | if (unit.name === Unit.WORKER.name) { 148 | var tileProps = accessTileProps(key, player, self.dynamicTilePropsCache); 149 | tileProps[player].discovered = true; 150 | } 151 | 152 | for (var i = 0; i <= maxRing; i++) { 153 | var ring = unitCubeCoords.ring(i); 154 | ring.forEach(function(tileCoord) { 155 | var tileOffsetCoords = tileCoord.toOffset(); 156 | var props = accessTileProps(tileOffsetCoords, player); 157 | if (i <= urange) { 158 | props[player].visible = true; 159 | } 160 | if (i <= ufield && unit.field && !unit.disabled) { 161 | if (!props.field || props.field.range > i) { 162 | props.field = { 163 | player: player, 164 | range: i 165 | }; 166 | } else if (props.field.range === i && props.field.player !== player) { 167 | props.field.player = undefined; 168 | } 169 | } 170 | }); 171 | } 172 | }); 173 | }; 174 | 175 | this.getTileProps = function(coords, player) { 176 | var props = self.tilePropsCache[coords]; 177 | var dynamicProps = accessTileProps(coords, player, self.dynamicTilePropsCache); 178 | var field = props && props.field ? props.field.player : undefined; 179 | var visible = props && props[player] ? props[player].visible : false; 180 | var discovered = dynamicProps[player].discovered; 181 | //var overlay = self.overlays[coords]; 182 | 183 | return { 184 | field: field, 185 | visible: visible, 186 | //overlay: overlay, 187 | discovered: discovered 188 | }; 189 | }; 190 | 191 | this.getSector = function(sector, player) { 192 | var tiles = generateSector(sector); 193 | // add fog of war 194 | return tiles.map(function(tile) { 195 | var newTile = self.getFullTile(tile.coords, player); 196 | if (!newTile.visible && Config.SHROUD) { 197 | newTile.name = Tile.SHROUD.name; 198 | delete newTile.unit; 199 | delete newTile.field; 200 | } 201 | return newTile; 202 | }); 203 | }; 204 | }; 205 | 206 | Map.coordsInSector = function(coords) { 207 | if (coords.x < 0 || coords.y < 0) return false; 208 | if (coords.x >= Map.SECTOR_WIDTH || coords.y >= Map.SECTOR_HEIGHT) return false; 209 | return true; 210 | }; 211 | 212 | Map.SECTOR_WIDTH = Config.SECTOR_WIDTH; 213 | Map.SECTOR_HEIGHT = Config.SECTOR_HEIGHT; 214 | 215 | module.exports = Map; 216 | -------------------------------------------------------------------------------- /js/Rules.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | require('./String'); 4 | require('./Array'); 5 | var Tile = require('./Tile'); 6 | var CubeCoords = require('./CubeCoords'); 7 | var Intent = require('./Intent'); 8 | var Unit = require('./Unit'); 9 | var Config = require('./Config'); 10 | var Cost = require('./Cost'); 11 | 12 | var Rules = {}; 13 | 14 | Rules.validateIntent = function(player, intent, src, dest, intents) { 15 | var intentNum = Object.keys(intents).length; 16 | 17 | if (!Config.PRODUCTION || typeof window === 'undefined') { 18 | console.log('Validate intent: _ _ _ _' 19 | .printf(intent, src, dest, intentNum)); 20 | } 21 | 22 | if (!player || !intent || !src || !dest || !intents) return false; 23 | 24 | // do not allow to double-intent 25 | 26 | if (intents[dest.coords]) return false; 27 | 28 | if (dest.name == Tile.WATER || !src.unit ) return false; 29 | 30 | if (!(src.unit.garrison && intent === Intent.UNGARRISON) && src.unit.actions.indexOf(intent) === -1) return false; 31 | 32 | if (!src.unit.multiaction && intentNum !== 0) return false; 33 | 34 | if (src.unit.player !== player.name && !Config.GOD_MODE) return false; 35 | 36 | if (intent === Intent.DECONSTRUCT) { 37 | if (!dest.unit || [Unit.TYPE_STRUCTURE, Unit.TYPE_MECHANICAL].indexOf(dest.unit.type) === -1 || dest.unit.player !== player.name || dest.unit.garrison) { 38 | return false; 39 | } 40 | cost = Cost.substract({}, Cost.divide(dest.unit.cost, 2)); 41 | if (cost.power) { 42 | cost.power = cost.power * 2; 43 | } 44 | return { 45 | cost: cost 46 | }; 47 | } else if (intent === Intent.MOVE) { 48 | // non garisonable units can not move on empty structures unless its combat 49 | if (dest.unit && dest.unit.type === Unit.TYPE_STRUCTURE && src.unit && !src.unit.garrisonable && !dest.unit.garrison) return false; 50 | } 51 | 52 | // distance has to be 1 53 | var srcCube = CubeCoords.fromOffset(src.coords); 54 | var destCube = CubeCoords.fromOffset(dest.coords); 55 | var xcoord = srcCube.substract(destCube).xcoord(); 56 | if (xcoord > 1) { 57 | return false; 58 | } 59 | 60 | // evaluate cost 61 | var unit = Unit.fromName(intent); 62 | var cost = new Cost(); 63 | if (unit) { 64 | if (src.unit.actions.indexOf(intent) === -1) return false; 65 | if ([Intent.MINE, Intent.BIGMINE].contains(intent) && ['undiscovered', undefined].contains(dest.resource)) return false; 66 | if (intent === Intent.POWERPLANT && !['gas', 'oil', 'coal'].contains(dest.resource)) return false; 67 | if (dest.field !== player.name && unit.name !== 'teleport') return false; 68 | if (!Cost.covers(unit.cost, player.resources)) return false; 69 | cost.add(unit.cost); 70 | } 71 | var tileOverlay = Tile.fromName(intent); 72 | if (tileOverlay) { 73 | if (!Cost.covers(tileOverlay.cost, player.resources)) return false; 74 | cost.add(tileOverlay.cost); 75 | } 76 | return { 77 | cost: cost.toData() 78 | }; 79 | }; 80 | 81 | Rules.calculateNumEngagements = function(tile, tileIntents, dstIntents) { 82 | if (!tile || !tile.unit) return 0; 83 | var engagements = []; 84 | var intent; 85 | for (var key in dstIntents) { 86 | intent = dstIntents[key]; 87 | if (intent.player !== tile.unit.player && intent.type === Intent.MOVE) { 88 | engagements.push(intent.src + ''); 89 | } 90 | } 91 | for (key in tileIntents) { 92 | intent = tileIntents[key]; 93 | if (intent.type === Intent.MOVE && engagements.indexOf(intent.dst + '') === -1) { 94 | engagements.push(intent.dst + ''); 95 | } 96 | } 97 | return engagements.length; 98 | }; 99 | 100 | Rules.resolveCombat = function(srcTile, destTile, intent, destIntents, engagements) { 101 | var srcUnit = srcTile.unit; 102 | var destUnit = destTile.unit; 103 | if (!Config.PRODUCTION || typeof window === 'undefined') { 104 | console.log('Resolve intent _ _ _'.printf(srcUnit, destUnit, intent.type)); 105 | } 106 | if (!srcUnit || !destUnit) return false; 107 | 108 | var garrison; 109 | if (destUnit.garrison) { 110 | garrison = destUnit; 111 | destUnit = destUnit.garrison; 112 | } 113 | 114 | if (destUnit.type === Unit.TYPE_STRUCTURE) return false; 115 | 116 | var destDefense = (garrison && garrison.defense) ? garrison.defense : destTile.defense; 117 | var resolution = { 118 | src: srcUnit, 119 | dest: destUnit, 120 | srcCoords: srcTile.coords, 121 | destCoords: destTile.coords 122 | }; 123 | if (intent.type === Intent.MOVE) { 124 | var srcUnitDmg = Math.round(srcUnit.health / srcUnit.maxHealth * srcUnit.damage); 125 | var dstUnitDmg = Math.round(destUnit.health / destUnit.maxHealth * destUnit.damage / engagements.dst); 126 | var srcDiff = 0; 127 | var destDiff = 0; 128 | if (destUnit.player !== srcUnit.player) { 129 | var targetMoves = false; 130 | var targetRunsAway = false; 131 | var targetAttacksSrc = false; 132 | for (var srcDestCoords in destIntents) { 133 | var destIntent = destIntents[srcDestCoords]; 134 | if (destIntent.type === Intent.MOVE) { 135 | targetMoves = true; 136 | if (srcDestCoords == srcTile.coords) { 137 | targetAttacksSrc = true; 138 | } else if (!destIntent.tile.unit) { 139 | targetRunsAway = true; 140 | } 141 | } 142 | } 143 | if (!targetMoves) { // target is fortified and attacks all attackers 144 | srcDiff = -dstUnitDmg; 145 | } 146 | if (targetRunsAway) { // target runs away 147 | destDiff = -srcUnitDmg / 2; 148 | } else { 149 | destDiff = -srcUnitDmg * (1 - destDefense / 100); 150 | } 151 | } 152 | resolution.destDiff = destDiff; 153 | resolution.srcDiff = srcDiff; 154 | } else if (intent.type === Intent.BOMBARD) { 155 | resolution.srcDiff = 0; 156 | resolution.destDiff = -srcUnit.rangedDmg * (1 - destDefense / 100); 157 | } 158 | resolution.destDiff = Math.round(resolution.destDiff); 159 | resolution.srcDiff = Math.round(resolution.srcDiff); 160 | resolution.garrison = garrison; 161 | return resolution; 162 | }; 163 | 164 | Rules.resolveBuilding = function(srcTile, destTile, intent) { 165 | var srcUnit = srcTile.unit; 166 | var destUnit = destTile.unit; 167 | if (!srcUnit || intent.type === Intent.MOVE) return false; 168 | var resolution = {src: srcUnit, dest: destUnit}; 169 | var newUnit = Unit.fromName(intent.type); 170 | if (!newUnit) return false; 171 | if (destUnit) { 172 | if (Rules.canBeGarrisoned(newUnit, destUnit)) { 173 | destUnit.garrison = newUnit; 174 | resolution.dest = destUnit; 175 | } else if (Rules.canBeGarrisoned(newUnit, srcUnit)) { 176 | srcUnit.garrison = newUnit; 177 | } else { 178 | return false; 179 | } 180 | } else { 181 | resolution.dest = newUnit; 182 | } 183 | newUnit.hue = srcUnit.hue; 184 | newUnit.player = srcUnit.player; 185 | /* 186 | } else { 187 | var tileOverlay = Tile.fromName(intent.type); 188 | if (tileOverlay) { 189 | resolution.destOverlay = tileOverlay.name; 190 | } 191 | } 192 | */ 193 | return resolution; 194 | }; 195 | 196 | Rules.resolveDeconstruction = function(srcTile, destTile, intent) { 197 | var srcUnit = srcTile.unit; 198 | var destUnit = destTile.unit; 199 | if (!srcUnit || intent.type !== Intent.DECONSTRUCT || !destUnit) return false; 200 | var resolution = {src: srcUnit, dest: undefined}; 201 | return resolution; 202 | }; 203 | 204 | Rules.resolveRestacking = function(srcTile, dstTile) { 205 | var srcUnit = srcTile.unit; 206 | var dstUnit = dstTile.unit; 207 | if (srcUnit && dstUnit && srcUnit.player === dstUnit.player && srcUnit.name === dstUnit.name) { 208 | dstUnit.health += srcUnit.health; 209 | srcUnit.health = dstUnit.health - dstUnit.maxHealth; 210 | dstUnit.health = Math.min(dstUnit.health, dstUnit.maxHealth); 211 | if (srcUnit.health <= 0) { 212 | srcUnit = undefined; 213 | } 214 | return { 215 | src: srcUnit, 216 | dest: dstUnit 217 | }; 218 | } 219 | return false; 220 | }; 221 | 222 | Rules.canBeGarrisoned = function(srcUnit, destUnit) { 223 | if (srcUnit && destUnit && srcUnit.garrisonable && !destUnit.garrison && destUnit.type === Unit.TYPE_STRUCTURE) return true; 224 | return false; 225 | }; 226 | 227 | Rules.resolveMovement = function(srcTile, destTile, intent) { 228 | var srcUnit = srcTile.unit; 229 | if (srcUnit.garrison) { 230 | srcUnit = srcUnit.garrison; 231 | } 232 | var destUnit = destTile.unit; 233 | //console.log('Resolve movement _ _ _'.printf(srcUnit, destUnit, intent.type)); 234 | if (!srcUnit || (intent.type !== Intent.MOVE && intent.type !== Intent.UNGARRISON)) return false; 235 | if (intent.type === Intent.UNGARRISON && !srcTile.unit.garrison) return false; 236 | var resolution; 237 | if (Rules.canBeGarrisoned(srcUnit, destUnit)) { 238 | destUnit.garrison = srcUnit; 239 | destUnit.player = srcUnit.player; 240 | resolution = {src: undefined, dest: destUnit}; 241 | return resolution; 242 | } else if (!destUnit) { 243 | resolution = {src: srcUnit, dest: destUnit}; 244 | resolution.dest = srcUnit; 245 | if (srcTile.unit.garrison) { 246 | resolution.src = srcTile.unit; 247 | resolution.src.garrison = undefined; 248 | } else { 249 | resolution.src = undefined; 250 | } 251 | return resolution; 252 | } else { 253 | return false; 254 | } 255 | }; 256 | 257 | module.exports = Rules; 258 | -------------------------------------------------------------------------------- /js/Game.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var Config = require('./Config'); 4 | var Coords = require('./Coords'); 5 | var Cost = require('./Cost'); 6 | var CubeCoords = require('./CubeCoords'); 7 | var Intent = require('./Intent'); 8 | var Map = require('./Map'); 9 | var Rules = require('./Rules'); 10 | var Sector = require('./Sector'); 11 | var Stream = require('./Stream'); 12 | var Tile = require('./Tile'); 13 | var Unit = require('./Unit'); 14 | 15 | var Game = function(map) { 16 | var self = this; 17 | this.map = map; 18 | this.tick = 0; 19 | this.unitCount = 0; 20 | this.players = {}; 21 | this.intents = {}; 22 | this.topScores = []; 23 | this.events = new Stream(); 24 | var turnTime = Config.TICK_LENGTH; 25 | var tickStart = (new Date()).getTime(); 26 | var gameStart = (new Date()).getTime(); 27 | 28 | this.getMatchTime = function() { 29 | return Math.round((Config.ROUND_LENGTH - ((new Date()).getTime() - gameStart))); 30 | }; 31 | 32 | this.getStatus = function(name, type) { 33 | var now = (new Date()).getTime(); 34 | var time = Math.round((turnTime - (now - tickStart))); 35 | return { 36 | type: type, 37 | tick: self.tick, 38 | time: time, 39 | matchTime: self.getMatchTime(), 40 | player: self.players[name], 41 | scores: self.topScores.sort(function(s1, s2) {return s2.score - s1.score;}) 42 | .filter(function(s, i) {return i < 5;}) 43 | }; 44 | }; 45 | 46 | function endTurn() { 47 | self.events.push(self.getStatus(undefined, 'resolution')); 48 | 49 | var combatResolutions = self.resolveIntents(Rules.resolveCombat); 50 | self.resolveUnitsHealth(combatResolutions); 51 | self.resolveIntents(Rules.resolveDeconstruction); 52 | var numResolutions; 53 | do { 54 | numResolutions = self.resolveIntents(Rules.resolveMovement, true).length; 55 | } while(numResolutions); 56 | self.resolveIntents(Rules.resolveRestacking, true); 57 | self.resolveIntents(Rules.resolveBuilding); 58 | self.resolveTeleports(); 59 | self.linkTeleports(); 60 | 61 | self.map.resetTilePropsCache(); 62 | self.map.calculateProps(self.map.units); 63 | self.countResources(); 64 | self.calculateTopScores(); 65 | self.intents = {}; 66 | self.tick++; 67 | tickStart = (new Date()).getTime(); 68 | var matchTime = self.getMatchTime(); 69 | if (matchTime > 0) { 70 | self.events.push(self.getStatus(undefined, 'orders')); 71 | } else { 72 | self.events.push(self.getStatus(undefined, 'end')); 73 | } 74 | } 75 | 76 | var turnInterval = setInterval(function() { 77 | endTurn(); 78 | }, turnTime); 79 | 80 | this.getSector = function(s, player) { 81 | return this.map.getSector(s, player); 82 | }; 83 | 84 | this.linkTeleports = function() { 85 | var unlinkedTeleports = {}; 86 | var player; 87 | for (var src in this.map.units) { 88 | var unit = this.map.units[src]; 89 | player = unit.player; 90 | if (unit.name === 'teleport' && !unit.target) { 91 | if (!unlinkedTeleports[player]) { 92 | unlinkedTeleports[player] = []; 93 | } 94 | unlinkedTeleports[player].push({port: unit, coords: src}); 95 | } 96 | } 97 | 98 | for (player in unlinkedTeleports) { 99 | var teleports = unlinkedTeleports[player]; 100 | for (var i = 0; i < teleports.length - 1; i++) { 101 | teleports[i].port.target = teleports[i + 1].coords; 102 | teleports[i + 1].port.target = teleports[i].coords; 103 | } 104 | } 105 | }; 106 | 107 | this.resolveTeleports = function() { 108 | var units = this.map.units; 109 | var teleported = {}; 110 | for (var src in units) { 111 | var unit = units[src]; 112 | if (unit.name === 'teleport') { 113 | var target = units[unit.target]; 114 | if (unit.garrison && target && !target.garrison && !teleported[src]) { 115 | target.garrison = unit.garrison; 116 | target.player = unit.garrison.player; 117 | teleported[unit.target] = true; 118 | delete unit.garrison; 119 | } 120 | } 121 | } 122 | }; 123 | 124 | this.countResources = function() { 125 | var units = this.map.units; 126 | for (var player in this.players) { 127 | this.players[player].units = 0; 128 | this.players[player].score = 0; 129 | this.players[player].income = { 130 | iron: 0, 131 | gas: 0, 132 | copper: 0, 133 | coal: 0, 134 | gold: 0, 135 | oil: 0 136 | }; 137 | this.players[player].caps = {}; 138 | } 139 | var unit; 140 | for (var src in units) { 141 | unit = units[src]; 142 | player = unit.player; 143 | if (unit.name === Unit.BASE.name) { 144 | this.players[player].resources.iron += Config.BASE_MONEY; 145 | this.players[player].resources.power = Config.STARTING_POWER; 146 | this.players[player].income.iron += Config.BASE_MONEY; 147 | this.players[player].score += 20; 148 | } else if (unit.name === Unit.MINE.name || unit.name === Unit.BIGMINE.name) { 149 | this.players[player].score += 20; 150 | var tile = this.map.getFullTile(src); 151 | var additionalResource = {}; 152 | additionalResource[tile.resource] = Config.MINE_MONEY; 153 | this.players[player].resources = Cost.add(this.players[player].resources, additionalResource); 154 | this.players[player].resources.power -= unit.cost.power || 0; 155 | this.players[player].income[tile.resource] += Config.MINE_MONEY; 156 | this.players[player].caps[tile.resource] = this.players[player].caps[tile.resource] || 200; 157 | if (unit.name === Intent.BIGMINE) { 158 | this.players[player].caps[tile.resource] += 100; 159 | } 160 | } else if (unit.name === Intent.POWERPLANT) { 161 | this.players[player].resources.power += 60; 162 | this.players[player].score += 10; 163 | } else { 164 | this.players[player].resources.power -= unit.cost.power || 0; 165 | this.players[player].score += 10; 166 | } 167 | if (unit.garrison) { 168 | this.players[unit.garrison.player].resources.power -= unit.garrison.cost.power || 0; 169 | this.players[unit.garrison.player].score += 10; 170 | } 171 | this.players[player].units++; 172 | } 173 | for (var key in this.players) { 174 | var playero = this.players[key]; 175 | playero.resources = Object.keys(playero.resources).reduce(function(acc, res) { 176 | playero.caps[res] = playero.caps[res] || 200; 177 | acc[res] = Math.min(playero.resources[res], playero.caps[res]); 178 | return acc; 179 | }, {}); 180 | } 181 | // calculate field score 182 | var fieldsAdded = {}; 183 | for (src in units) { 184 | unit = units[src]; 185 | if (unit.field) { 186 | var cc = CubeCoords.fromOffset(Coords.fromString(src)); 187 | for (var i = 0; i <= unit.field; i++) { 188 | var ccs = cc.ring(i); 189 | ccs.forEach(function(cc) { 190 | var cco = cc.toOffset(); 191 | if (!fieldsAdded[cco]) { 192 | var props = self.map.getTileProps(cco); 193 | if (props.field) { 194 | self.players[props.field].score += 1; 195 | } 196 | fieldsAdded[cco] = true; 197 | } 198 | }, 0); 199 | } 200 | } 201 | } 202 | for (player in this.players) { 203 | this.players[player].score = Math.round(this.players[player].score); 204 | } 205 | }; 206 | 207 | this.calculateTopScores = function() { 208 | var values = []; 209 | for (var player in this.players) { 210 | values.push(this.players[player]); 211 | } 212 | this.topScores = values.map(function(p) { 213 | return { 214 | name: p.name, 215 | score: p.score 216 | }; 217 | }); 218 | this.topScores.sort(function(a, b) {return a.score - b.score;}); 219 | }; 220 | 221 | this.iterateIntents = function(callback) { 222 | for (var coords in this.intents) { 223 | var intents = this.intents[coords]; 224 | for (var src in intents) { 225 | var intent = intents[src]; 226 | var srcTile = self.map.getFullTile(intent.src); 227 | var dstTile = self.map.getFullTile(intent.dst); 228 | callback.bind(this)(intent, srcTile, dstTile); 229 | } 230 | } 231 | }; 232 | 233 | function getTileIntents(coords) { 234 | var ccs = CubeCoords.fromOffset(coords).neighbours(); 235 | return ccs.reduce(function(intents, cc) { 236 | var offset = cc.toOffset(); 237 | if (self.intents[offset] && self.intents[offset][coords]) { 238 | intents[offset] = self.intents[offset][coords]; 239 | intents[offset].tile = self.map.getFullTile(offset); 240 | } 241 | return intents; 242 | }, {}); 243 | } 244 | 245 | this.resolveUnitsHealth = function(combatResolutions) { 246 | combatResolutions.forEach(function(resolution) { 247 | if (resolution.srcDiff) { 248 | resolution.src.health += resolution.srcDiff; 249 | } 250 | if (resolution.destDiff) { 251 | (resolution.dest.garrison || resolution.dest).health += resolution.destDiff; 252 | } 253 | }); 254 | var units = this.map.units; 255 | for (var coords in units) { 256 | var unit = units[coords].garrison || units[coords]; 257 | var tile = this.map.getFullTile(coords); 258 | unit.disabled = false; 259 | if (self.isPlayerDisabled(unit.player) && !unit.mock) { 260 | unit.health -= Config.DISABLED_DAMAGE; 261 | unit.disabled = true; 262 | } else if (tile.field !== unit.player) { 263 | unit.health -= Config.FIELD_DAMAGE; 264 | logHealthChange(tile, -Config.FIELD_DAMAGE); 265 | } 266 | if (unit.health <= 0) { 267 | addLogMessage(unit.player, '_ @(_) died'.printf(unit.name, coords), Coords.fromString(coords)); 268 | this.removeUnitWithIntents(coords); 269 | } 270 | } 271 | }; 272 | 273 | this.removeUnitWithIntents = function(coords) { 274 | if (this.map.units[coords] && this.map.units[coords].garrison) { 275 | delete this.map.units[coords].garrison; //TODO: remove ungarrison intent 276 | } else { 277 | for (var dst in this.intents) { 278 | //TODO: maybe check if intent exists and compesate the cost 279 | delete this.intents[dst][coords]; 280 | } 281 | delete this.map.units[coords]; 282 | } 283 | }; 284 | 285 | function resolveUnit(resolution, coords) { 286 | if (resolution) { 287 | if (!resolution.id) { 288 | resolution.id = self.unitCount++; 289 | } 290 | self.map.units[coords] = resolution; 291 | } else { 292 | self.removeUnitWithIntents(coords); 293 | } 294 | } 295 | 296 | this.resolveIntents = function(step, deleteResolvedIntent) { 297 | var resolutions = []; 298 | this.iterateIntents(function(intent, srcTile, dstTile) { 299 | var src = srcTile.coords; 300 | var coords = dstTile.coords; 301 | var destIntents = getTileIntents(dstTile.coords); 302 | var engagements = { 303 | src: Rules.calculateNumEngagements(srcTile, getTileIntents(src), self.intents[src]), 304 | dst: Rules.calculateNumEngagements(dstTile, destIntents, self.intents[coords]) 305 | }; 306 | var resolution = step(srcTile, dstTile, intent, destIntents, engagements); 307 | if (!resolution && srcTile.unit && step === Rules.resolveBuilding) { 308 | self.players[srcTile.unit.player].resources = Cost.add(self.players[srcTile.unit.player].resources, intent.cost); 309 | return; 310 | } 311 | if (resolution) { 312 | logHealthChange(srcTile, resolution.srcDiff); 313 | logHealthChange(dstTile, resolution.destDiff); 314 | resolutions.push(resolution); 315 | resolveUnit(resolution.src, src); 316 | resolveUnit(resolution.garrison || resolution.dest, coords); 317 | /* 318 | if (resolution.destOverlay) { 319 | self.map.overlays[coords] = resolution.destOverlay; 320 | } 321 | */ 322 | if (deleteResolvedIntent) { 323 | delete self.intents[coords][src]; 324 | } 325 | } 326 | }); 327 | return resolutions; 328 | }; 329 | 330 | this.cancelIntents = function(name, c) { 331 | var coords = Coords.fromString(c); 332 | var tile = self.map.getFullTile(coords); 333 | if (!tile.unit) return false; 334 | CubeCoords.fromOffset(coords).neighbours().forEach(function(cc) { 335 | var oc = cc.toOffset(); 336 | if (self.intents[oc] && self.intents[oc][coords]) { 337 | self.players[name].resources = Cost.add(self.players[name].resources, self.intents[oc][coords].cost); 338 | delete self.intents[oc][coords]; 339 | } 340 | }); 341 | return 'OK'; 342 | }; 343 | 344 | this.addIntent = function(name, intent) { 345 | if (!this.intents[intent.dst]) { 346 | this.intents[intent.dst] = {}; 347 | } 348 | var src = self.map.getFullTile(intent.src); 349 | var tile = self.map.getFullTile(intent.dst); 350 | var ccn = CubeCoords.fromOffset(intent.src).neighbours(); 351 | var unitIntents = ccn.reduce(function(acc, val) { 352 | var tents = self.intents[val.toOffset()]; 353 | if (tents) { 354 | var tent = tents[src.coords]; 355 | if (tent) { 356 | acc[val.toOffset()] = tent; 357 | } 358 | } 359 | return acc; 360 | }, {}); 361 | var validatedIntent = Rules.validateIntent(self.players[name], intent.type, src, tile, unitIntents); 362 | if (!validatedIntent) { 363 | return false; 364 | } 365 | self.players[name].resources = Cost.substract(self.players[name].resources, validatedIntent.cost); 366 | self.players[name].seenAt = self.tick; 367 | intent.cost = validatedIntent.cost; 368 | intent.player = src.unit.player; 369 | this.intents[intent.dst][intent.src] = intent; 370 | return validatedIntent; 371 | }; 372 | 373 | this.placePlayer = function(sector) { 374 | var position = false; 375 | var start = this.map.getTileCoords(sector, Math.floor(Map.SECTOR_WIDTH / 2), Math.floor(Map.SECTOR_HEIGHT / 2)); 376 | var cube = CubeCoords.fromOffset(start); 377 | var ring = 0; 378 | var outside = true; // all of ring is outside sector 379 | var sectorHasUnits = false; // at least one other unit in sector 380 | 381 | do { 382 | outside = true; 383 | cube.ring(ring).forEach(function(coord) { 384 | var ocord = coord.toOffset(); 385 | if (!Map.coordsInSector(ocord.substract( 386 | new Coords(sector.position.x * Map.SECTOR_WIDTH, 387 | sector.position.y * Map.SECTOR_HEIGHT)))) { 388 | return; 389 | } 390 | outside = false; 391 | var tile = self.map.getFullTile(ocord); 392 | if (tile.unit && !tile.unit.mock) sectorHasUnits = true; 393 | else if (tile.name != Tile.WATER && !tile.resource && !position) { 394 | position = ocord; 395 | } 396 | }); 397 | ring++; 398 | } while(!outside && !sectorHasUnits); 399 | 400 | if (!sectorHasUnits && position) { 401 | return position; 402 | } else { 403 | return false; 404 | } 405 | }; 406 | 407 | function addLogMessage(player, msg, coords) { 408 | self.players[player].log.push({ 409 | time: (new Date()).getTime(), 410 | message: msg, 411 | tick: self.tick, 412 | coords: coords 413 | }); 414 | } 415 | 416 | function logHealthChange(tile, diff) { 417 | if(tile && tile.unit && diff) { 418 | var unit = tile.unit.garrison || tile.unit; 419 | addLogMessage(unit.player, '_ @(_) _'.printf(unit.name, tile.coords, diff), tile.coords); 420 | } 421 | } 422 | 423 | this.addPlayer = function(name, sect, mock) { 424 | if (this.players[name]) { 425 | return false; 426 | } 427 | var sector = new Sector(sect || '0'); 428 | var position = false; 429 | while(!position) { 430 | position = this.placePlayer(sector); 431 | if (position) { 432 | break; 433 | } 434 | sector = sector.next(); 435 | } 436 | var player = { 437 | sector: sector, 438 | base: position, 439 | resources: { 440 | power: Config.STARTING_POWER - 10, 441 | iron: Config.STARTING_MONEY 442 | }, 443 | income: { 444 | iron: Config.BASE_MONEY 445 | }, 446 | caps: { 447 | iron: 200 448 | }, 449 | name: name, 450 | units: 1, 451 | hue: Math.floor(Math.random() * 36) * 10, 452 | log: [], 453 | mock: mock, 454 | seenAt: self.tick 455 | }; 456 | 457 | var unit = Unit.fromPrototype(Unit.BASE, { 458 | player: name, 459 | hue: player.hue, 460 | id: self.unitCount++ 461 | }); 462 | unit.garrison = Unit.fromPrototype(Unit.WORKER, { 463 | player: name, 464 | hue: player.hue, 465 | id: self.unitCount++ 466 | }); 467 | this.map.units[position] = unit; 468 | var units = {}; 469 | units[position] = unit; 470 | 471 | self.map.calculateProps(units); 472 | 473 | this.players[name] = player; 474 | if (Config.MOCK_UNITS) { 475 | Config.MOCK_UNITS.filter(function(unit) { 476 | return unit.player === name; 477 | }).forEach(function(unit) { 478 | self.map.units[unit.coords] = Unit.fromName(unit.name, unit); 479 | self.map.units[unit.coords].mock = true; 480 | self.map.units[unit.coords].hue = player.hue; 481 | self.map.units[unit.coords].id = self.unitCount++; 482 | }); 483 | self.map.calculateProps(self.map.units); 484 | } 485 | addLogMessage(name, '_ joined'.printf(name), player.base); 486 | return player; 487 | }; 488 | 489 | this.getPlayer = function(name) { 490 | return this.players[name]; 491 | }; 492 | 493 | this.isPlayerDisabled = function(name) { 494 | var player = self.players[name]; 495 | if (!player.units || (player.seenAt < self.tick - Config.DISABLE_TIMEOUT) || player.mock) { 496 | return true; 497 | } 498 | return false; 499 | }; 500 | 501 | this.endTurn = function(name) { 502 | this.players[name].seenAt = this.tick; 503 | this.players[name].endedTurn = true; 504 | for (var player in this.players) { 505 | var pl = this.players[player]; 506 | if (!pl.endedTurn && !self.isPlayerDisabled(player)) { 507 | return "WAIT"; 508 | } 509 | } 510 | for (player in this.players) { 511 | this.players[player].endedTurn = false; 512 | } 513 | endTurn(); 514 | clearInterval(turnInterval); 515 | turnInterval = setInterval(function() { 516 | endTurn(); 517 | }, turnTime); 518 | return 'OK'; 519 | }; 520 | 521 | if (Config.MOCK_PLAYERS) { 522 | Config.MOCK_PLAYERS.forEach(function(player) { 523 | self.addPlayer(player.name, player.sector, true); 524 | }); 525 | } 526 | }; 527 | 528 | module.exports = Game; 529 | -------------------------------------------------------------------------------- /js/Renderer.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var Tile = require('./Tile'); 4 | var Rules = require('./Rules'); 5 | var CubeCoords = require('./CubeCoords'); 6 | var Coords = require('./Coords'); 7 | var Cost = require('./Cost'); 8 | var Config = require('./Config'); 9 | var Stream = require('./Stream'); 10 | var Q = require('./Q'); 11 | var WebComponent = require('./WebComponent'); 12 | var Unit = require('./Unit'); 13 | var Intent = require('./Intent'); 14 | 15 | var Renderer = function(tileStream) { 16 | var self = this; 17 | 18 | var StatusBox = new WebComponent('statusbox'); 19 | var ResourceBox = new WebComponent('resources'); 20 | var InfoBox = new WebComponent('infobox'); 21 | var UnitBox = new WebComponent('unitbox'); 22 | 23 | self.tileRequests = new Stream(); 24 | self.intentBus = new Stream(); 25 | 26 | self.center = new Coords(0, 0); 27 | self.centerTileScreen = new Coords(0, 0); 28 | self.positionDiff = new Coords(0, 0); 29 | 30 | self.tiles = {}; 31 | self.tileElements = {}; 32 | 33 | self.units = {}; 34 | self.time = 0; 35 | self.matchTime = 0; 36 | self.matchTimerInterval = undefined; 37 | self.selectedTile = undefined; 38 | self.intentType = undefined; 39 | self.player = undefined; 40 | 41 | var screenCoordsToTileCoords = Function.prototype.memoized.bind( 42 | function _screen_coords_to_tile_coords(screenCoords, centerTileScreenCoords, centerTileCoords) { 43 | return screenCoords.add(centerTileCoords).substract(centerTileScreenCoords); 44 | }); 45 | 46 | tileStream.onValue(function(tiles) { 47 | tiles.forEach(function(t) { 48 | var tile = Tile.fromJson(t); 49 | var currentTile = self.tiles[tile.coords]; 50 | if (tile.name == Tile.SHROUD && currentTile && currentTile.name != Tile.SHROUD) { 51 | currentTile.shrouded = true; 52 | currentTile.dirty = false; 53 | } else { 54 | self.tiles[tile.coords] = tile; 55 | } 56 | }); 57 | self.render(); 58 | }); 59 | 60 | self.reset = function(e) { 61 | self.selectedTile = undefined; 62 | 63 | clearInterval(self.timerInterval); 64 | clearInterval(self.matchTimerInterval); 65 | 66 | self.time = Math.round(e.time / 1000); 67 | self.timerInterval = setInterval(function() { 68 | self.time -= 1; 69 | StatusBox.setState({ 70 | time: self.time 71 | }); 72 | }, 1000); 73 | 74 | self.matchTime = Math.round(e.matchTime / 1000); 75 | self.matchTimerInterval = setInterval(function() { 76 | self.matchTime -= 1; 77 | var minutes = self.matchTime / 60; 78 | StatusBox.setState({ 79 | match: minutes > 1 ? '_ min.'.printf(Math.round(minutes)) : self.matchTime 80 | }); 81 | }, 1000); 82 | 83 | self.player = e.player; 84 | for (var coords in self.tiles) { 85 | self.tiles[coords].dirty = true; 86 | } 87 | self.render(); 88 | updateStatus(e.tick); 89 | updateLog(); 90 | updateScores(e.scores); 91 | document.getElementById('actions-wrapper').className = 'hidden'; 92 | }; 93 | 94 | function updateScores(scores) { 95 | Q.byId('scores').innerHTML = ''; 96 | scores.forEach(function(s) { 97 | var score = Q.create('li'); 98 | score.textContent = '_: _'.printf(s.name, s.score); 99 | Q.byId('scores').appendChild(score); 100 | }); 101 | } 102 | 103 | function updateLog() { 104 | var log = Q.byId('log'); 105 | log.innerHTML = ''; 106 | self.player.log.forEach(function(l) { 107 | var message = Q.create('div'); 108 | message.textContent = '[_] _'.printf(l.tick, l.message); 109 | message.className = 'message'; 110 | message.onclick = (function() { 111 | self.setCenter(Coords.fromString(this.coords.representation)); 112 | }).bind(l); 113 | log.appendChild(message); 114 | log.scrollTop = log.scrollHeight; 115 | }); 116 | } 117 | 118 | function updateStatus(tick) { 119 | StatusBox.setState({ 120 | score: self.player.score || 0, 121 | tick: tick, 122 | time: self.time 123 | }); 124 | var player = self.player; 125 | var state = ['iron', 'gold', 'copper', 'gas', 'coal', 'oil'].reduce(function(acc, key) { 126 | acc[key] = '_/_ +_'.printf(player.resources[key], player.caps[key], player.income[key]); 127 | return acc; 128 | }, {}); 129 | state.power = player.resources.power; 130 | ResourceBox.setState(state); 131 | } 132 | 133 | function renderActions(tile, screenCoords) { 134 | var unit = tile.unit; 135 | var intents = CubeCoords.fromOffset(tile.coords).neighbours().map(function(coords) { 136 | return self.tiles[coords.toOffset()]; 137 | }).filter(function(t) { 138 | if (t && t.intents && t.intents[tile.coords]) { 139 | return true; 140 | } 141 | return false; 142 | }); 143 | 144 | var $actions = Q.byId('actions'); 145 | $actions.innerHTML = ''; 146 | Q.byId('actions-wrapper').className = ''; 147 | 148 | var actions = unit.actions.slice(); 149 | if (unit.garrison) actions.unshift(Intent.UNGARRISON); 150 | self.intentType = actions[0]; 151 | 152 | actions.forEach(function(act, index) { 153 | var unit = Unit.fromName(act); 154 | var cost = unit ? Cost.toString(unit.cost) : '-'; 155 | var img = 'url(img/unit/_.png)'.printf(act); 156 | var $button = Q.create('button'); 157 | $button.style['background-image'] = img; 158 | var $action = Q.create('div'); 159 | $action.className = 'action'; 160 | $action.title = cost; 161 | if (unit && !Cost.covers(unit.cost, self.player.resources)) { 162 | $action.style.opacity = '0.25'; 163 | } 164 | $action.textContent = act; 165 | $action.appendChild($button); 166 | if (!index) { 167 | $action.querySelector('button').className = 'selected'; 168 | } 169 | $action.onclick = (function(act) { 170 | self.intentType = act; 171 | var $selectedButton = $actions.querySelector('button[class=selected]'); 172 | if ($selectedButton) { 173 | $selectedButton.className = ''; 174 | } 175 | this.querySelector('button').className = 'selected'; 176 | }).bind($action, act); 177 | $actions.appendChild($action); 178 | }); 179 | if (intents.length) { 180 | var $action = Q.create('div'); 181 | $action.textContent = 'cancel'; 182 | $action.className = 'action'; 183 | var $button = Q.create('button'); 184 | $button.style['background-image'] = 'url(img/unit/cancel.png)'; 185 | $action.appendChild($button); 186 | $action.onclick = function() { 187 | intents.forEach(function(t) { 188 | var intent = t.intents[tile.coords]; 189 | self.player.resources = Cost.add(self.player.resources, intent.cost); 190 | delete t.intents[tile.coords]; 191 | self.selectedTile = undefined; 192 | tile.unit.selected = false; 193 | renderUnits(tile, screenCoords, screenCoordsToScreenPosition(screenCoords, tile.coords, self.positionDiff, self.center)); 194 | renderIntents(t, tileCoordsToScreenCoords(t.coords, self.centerTileScreen, self.center)); 195 | updateStatus(); 196 | self.intentBus.push({ 197 | type: 'cancel', 198 | coords: tile.coords 199 | }); 200 | }); 201 | Q.byId('actions-wrapper').className = 'hidden'; 202 | }; 203 | $actions.appendChild($action); 204 | } 205 | } 206 | 207 | function renderUnit(tile) { 208 | var selectedScreenCoords = tileCoordsToScreenCoords(tile.coords, self.centerTileScreen, self.center); 209 | var selectedScreenPosition = screenCoordsToScreenPosition(selectedScreenCoords, tile.coords, self.positionDiff, self.center); 210 | renderUnits(tile, selectedScreenCoords, selectedScreenPosition); 211 | } 212 | 213 | function onTileClick() { 214 | var tile = self.tiles[screenCoordsToTileCoords(this, self.centerTileScreen, self.center)]; 215 | var neighbourCoords = CubeCoords.fromOffset(tile.coords).neighbours(); 216 | var selectedNeighbourTile; 217 | var index; 218 | neighbourCoords.forEach(function(coords, i) { 219 | var neighbour = self.tiles[coords.toOffset()]; 220 | var unit = neighbour.unit; 221 | if (unit && unit.selected) { 222 | selectedNeighbourTile = neighbour; 223 | index = i; 224 | } 225 | }); 226 | if (tile.unit && (tile.unit.player === self.player.name || Config.GOD_MODE) && 227 | (!self.selectedTile || self.selectedTile.coords === tile.coords || !selectedNeighbourTile)) { 228 | tile.unit.selected = !tile.unit.selected; 229 | if (tile.unit.selected) { 230 | renderActions(tile, this); 231 | if (self.selectedTile) { 232 | self.selectedTile.unit.selected = false; 233 | renderUnit(self.selectedTile); 234 | } 235 | self.selectedTile = tile; 236 | } else { 237 | document.getElementById('actions-wrapper').className = 'hidden'; 238 | self.selectedTile = undefined; 239 | } 240 | } 241 | if (selectedNeighbourTile) { 242 | var unit = selectedNeighbourTile.unit; 243 | if (unit && unit.selected) { 244 | var validatedIntent = validateIntent(selectedNeighbourTile, tile); 245 | if (validatedIntent) { 246 | selectedNeighbourTile.unit.selected = false; 247 | self.selectedTile = false; 248 | renderUnit(selectedNeighbourTile); 249 | var ncoords = selectedNeighbourTile.coords; 250 | self.intentBus.push(new Intent(ncoords, tile.coords, self.intentType)); 251 | tile.intents[ncoords] = { 252 | direction: index, 253 | type: self.intentType, 254 | cost: validatedIntent.cost 255 | }; 256 | self.player.resources = Cost.substract(self.player.resources, validatedIntent.cost); 257 | document.getElementById('actions-wrapper').className = 'hidden'; 258 | updateStatus(); 259 | } 260 | } 261 | } 262 | var screenPosition = screenCoordsToScreenPosition(this, tile.coords, self.positionDiff, self.center); 263 | renderIntents(tile, this); 264 | renderUnits(tile, this, screenPosition); 265 | } 266 | 267 | function renderIntents(tile, coords) { 268 | var $hex = self.tileElements[coords]; 269 | var $intents = $hex.querySelectorAll('.intent'); 270 | for (var i = 0; i < $intents.length; i++) { 271 | $intents[i].remove(); 272 | } 273 | for (var c in tile.intents) { 274 | var intentId = 'intent-_'.printf(c); 275 | var $intent = Q.create('div'); 276 | $intent.className = 'intent'; 277 | $intent.setAttribute('id', intentId); 278 | var deg = -180 - 60 * tile.intents[c].direction; 279 | $intent.style.transform = 'rotate(_deg)'.printf(deg); 280 | $hex.appendChild($intent); 281 | } 282 | } 283 | 284 | function renderFields(tile, coords) { 285 | var $hex = self.tileElements[coords]; 286 | var $fields = $hex.querySelectorAll('.field'); 287 | for (var i = 0; i < $fields.length; i++) { 288 | $fields[i].remove(); 289 | } 290 | //var odd = (self.centerTileScreen.y % 2) ? !(self.center.y % 2) : (self.center.y % 2); 291 | //var ccn = CubeCoords.fromOffset(coords, odd).neighbours(); 292 | var ccn = CubeCoords.fromOffset(tile.coords).neighbours(); 293 | ccn.forEach(function(c, i) { 294 | //var oc = screenCoordsToTileCoords(c.toOffset(odd), self.centerTileScreen, self.center); 295 | var oc = c.toOffset(); 296 | var tile2 = self.tiles[oc]; 297 | if (!tile2 || tile2.field === tile.field || !tile.field) return; 298 | var clazz = 'field'; 299 | if (tile.field !== self.player.name) clazz += ' enemy'; 300 | var $field = Q.create('div'); 301 | $field.className = clazz; 302 | var deg = -180 - 60 * i; 303 | $field.style.transform = 'rotate(_deg)'.printf(deg); 304 | $hex.appendChild($field); 305 | }); 306 | } 307 | 308 | function renderUnits(tile, coords, position) { 309 | var $hex = self.tileElements[coords]; 310 | if (tile.unit && !tile.shrouded) { 311 | var $unit = self.units[tile.unit.id]; 312 | if (!$unit) { 313 | $unit = Q.create('div'); 314 | $unit.className = 'unit'; 315 | var $health = Q.create('div'); 316 | $health.className = 'health'; 317 | $unit.appendChild($health); 318 | Q.byId('map').appendChild($unit); 319 | self.units[tile.unit.id] = $unit; 320 | } 321 | $unit.rendered = true; 322 | if (tile.unit.player !== self.player.name) { 323 | $unit.classList.add('enemy'); 324 | } else { 325 | $unit.classList.remove('enemy'); 326 | } 327 | if (tile.unit.garrison) { 328 | $unit.classList.add('garrisoned'); 329 | } else { 330 | $unit.classList.remove('garrisoned'); 331 | } 332 | $unit.setAttribute('active', tile.unit.selected || false); 333 | $hex.setAttribute('active', tile.unit.selected || false); 334 | var img = 'url(img/unit/_.png)'.printf(tile.unit.name); 335 | var healthBarWidth = 0; 336 | if (tile.unit.type !== Unit.TYPE_STRUCTURE) { 337 | healthBarWidth = (tile.unit.health / tile.unit.maxHealth * 100); 338 | } else if (tile.unit.garrison) { 339 | healthBarWidth = (tile.unit.garrison.health / tile.unit.garrison.maxHealth * 100); 340 | } 341 | $unit.querySelector('.health').style.width = healthBarWidth + '%'; 342 | $unit.style['border-width'] = Config.DEBUG_VIEW ? 1 : 0; 343 | $unit.style['background-image'] = img; 344 | $unit.style['-webkit-filter'] = 'hue-rotate(_deg)'.printf(tile.unit.player === self.player.name ? 0 : tile.unit.hue); 345 | $unit.style.top = position.y + 'px'; 346 | $unit.style.left = position.x + 'px'; 347 | $unit.style.display = 'block'; 348 | } else { 349 | $hex.setAttribute('active', 'false'); 350 | } 351 | } 352 | 353 | function validateIntent(src, dst) { 354 | if (!self.selectedTile || !self.player) return false; 355 | var ccn = CubeCoords.fromOffset(src.coords).neighbours(); 356 | var intents = ccn.reduce(function(acc, val) { 357 | var intent = self.tiles[val.toOffset()].intents[src.coords]; 358 | if (intent) { 359 | acc[val.toOffset()] = intent; 360 | } 361 | return acc; 362 | }, {}); 363 | return Rules.validateIntent(self.player, self.intentType, src, dst, intents); 364 | } 365 | 366 | function renderTile(tile, screenCoords, position) { 367 | var hex = self.tileElements[screenCoords]; 368 | if (!hex) { 369 | hex = document.createElement('div'); 370 | document.getElementById('map').appendChild(hex); 371 | self.tileElements[screenCoords] = hex; 372 | hex.addEventListener('click', onTileClick.bind(screenCoords)); 373 | hex.addEventListener('contextmenu', (function(e) { 374 | var tile = self.tiles[screenCoordsToTileCoords(this, self.centerTileScreen, self.center)]; 375 | e.preventDefault(); 376 | if (tile) { 377 | self.setCenter(tile.coords); 378 | } 379 | }).bind(screenCoords)); 380 | //hex.style.top = position.y + 'px'; 381 | //hex.style.left = position.x + 'px'; 382 | hex.addEventListener('mouseover', (function() { 383 | var tile = self.tiles[screenCoordsToTileCoords(this, self.centerTileScreen, self.center)]; 384 | var tileUnit = tile.unit ? tile.unit.garrison || tile.unit : undefined; 385 | var baseUnit = tile.unit && tile.unit.garrison ? tile.unit : undefined; 386 | var unitName = ''; 387 | if (baseUnit) { 388 | unitName = '_[_]'.printf(tileUnit.name.capitalize(), baseUnit.name.capitalize()); 389 | } else if (tileUnit) { 390 | unitName = tileUnit.name.capitalize(); 391 | if (tileUnit.garrisonable) { 392 | unitName += '[G]'; 393 | } 394 | } 395 | var unit = tileUnit ? '_ _/_'.printf(unitName, tileUnit.health, tileUnit.maxHealth) : 'None'; 396 | var damage = tileUnit ? '_/_'.printf(Math.round(tileUnit.health / tileUnit.maxHealth * tileUnit.damage), tileUnit.damage) : 'None'; 397 | 398 | UnitBox.setState({ 399 | unit: unit, 400 | bombard: (tile.unit && tile.unit.rangedDmg) ? tile.unit.rangedDmg : 'None', 401 | damage: damage, 402 | owner: tile.unit ? tile.unit.player : 'None' 403 | }); 404 | 405 | InfoBox.setState({ 406 | terrain: '_ (_%)'.printf(tile.toString().capitalize(), (tile.unit && tile.unit.defense) ? tile.unit.defense : tile.defense), 407 | sector: '_ (_)'.printf(tile.sector.value, tile.coords), 408 | resource: (tile.resource || 'None').capitalize(), 409 | field: tile.field ? tile.field : 'None' 410 | }); 411 | 412 | var $cursor = Q.byId('cursor'); 413 | $cursor.style.display= 'none'; 414 | if (self.selectedTile && self.selectedTile.coords+'' !== tile.coords+'') { 415 | if (validateIntent(self.selectedTile, tile)) { 416 | var pos = screenCoordsToScreenPosition(this, tile.coords, self.positionDiff, self.center); 417 | $cursor.style.left = (pos.x + 1) + 'px'; 418 | $cursor.style.top = (pos.y + 1) + 'px'; 419 | $cursor.classList.add('hex'); 420 | $cursor.style.display = 'block'; 421 | } 422 | } 423 | }).bind(screenCoords)); 424 | } 425 | var clazz = ['hex', tile.name, tile.resource].join(' '); 426 | if (hex.className !== clazz) { 427 | hex.className = clazz; 428 | } 429 | if (Config.DEBUG_VIEW) { 430 | hex.setAttribute('coords', tile.coords); 431 | } 432 | var diffy = 0; 433 | /* 434 | if (tile.name == Tile.HILLS) { 435 | diffy = -5; 436 | } else if (tile.name == Tile.MOUNTAINS) { 437 | diffy = -10; 438 | }*/ 439 | var x = position.x + 'px'; 440 | var y = position.y + diffy + 'px'; 441 | if (hex.style.top !== y) { 442 | hex.style.top = y; 443 | } 444 | if (hex.style.left !== x) { 445 | hex.style.left = x; 446 | } 447 | //hex.style['z-index'] = -diffy; 448 | if (tile.shrouded) { 449 | hex.style.opacity = '0.3'; 450 | } else { 451 | hex.style.opacity = '1'; 452 | } 453 | } 454 | 455 | self.setCenter = function(coords) { 456 | Q.byTag('body')[0].className = 'notransition'; 457 | self.center = coords; 458 | self.render(); 459 | Q.byTag('body')[0].className = ''; 460 | }; 461 | 462 | window.onresize = function() { 463 | Q.byTag('body')[0].className = 'notransition'; 464 | self.render(); 465 | Q.byTag('body')[0].className = ''; 466 | }; 467 | 468 | var tileCoordsToScreenCoords = Function.prototype.memoized.bind( 469 | function _tile_coords_to_screen_coords(tileCoords, centerTileScreenCoords, centerTileCoords) { 470 | return tileCoords.add(centerTileScreenCoords).substract(centerTileCoords); 471 | }); 472 | 473 | var adjustScreenCoords = Function.prototype.memoized.bind( 474 | function _adjust_screen_coords(screenCoords, tileCoords, center) { 475 | var dx = 0; 476 | /*jshint -W018 */ 477 | if (center.y % 2 && !(tileCoords.y % 2)) { 478 | dx = -1; 479 | } 480 | return new Coords(screenCoords.x + dx, screenCoords.y); 481 | }); 482 | 483 | var screenCoordsToScreenPosition = Function.prototype.memoized.bind( 484 | function _screen_coords_to_screen_position(screenCoords, tileCoords, positionDiff, center) { 485 | var adjustedCoords = adjustScreenCoords(screenCoords, tileCoords, center); 486 | var offset = 0; 487 | if (tileCoords.y % 2) { 488 | offset = Tile.WIDTH / 2; 489 | } 490 | if (center.y % 2) { 491 | if (tileCoords.y % 2) { 492 | offset -= Tile.WIDTH / 2; 493 | } else { 494 | offset += Tile.WIDTH / 2; 495 | } 496 | } 497 | var position = adjustedCoords.scale(new Coords(Tile.WIDTH, Tile.HEIGHT * 3 / 4)) 498 | .add(new Coords(offset, -1 * adjustedCoords.y)).substract(positionDiff); 499 | return position; 500 | }); 501 | 502 | 503 | self.render = function() { 504 | var $viewport = Q.byId('map'); 505 | var windowSize = new Coords($viewport.offsetWidth, $viewport.offsetHeight); 506 | var windowCenter = windowSize.uscale(0.5).floor(); 507 | var tileCenter = windowCenter.substract(new Coords(Tile.WIDTH / 2, Tile.HEIGHT / 4)).floor(); 508 | 509 | self.centerTileScreen = new Coords(windowSize.x / Tile.WIDTH / 2, windowSize.y / (Tile.HEIGHT / 4 * 3) / 2).floor(); 510 | var centerTileScreenPosition = self.centerTileScreen.scale(new Coords(Tile.WIDTH, Tile.HEIGHT * 3 / 4)); 511 | self.positionDiff = centerTileScreenPosition.substract(tileCenter); 512 | 513 | var requiredTiles = []; 514 | for (var id in self.units) { 515 | self.units[id].rendered = false; 516 | } 517 | for (var x = -1; x * Tile.WIDTH < $viewport.offsetWidth + Tile.WIDTH; x++) { 518 | for (var y = -1; y * Tile.HEIGHT / 2 < $viewport.offsetHeight; y++) { 519 | var screenCoords = new Coords(x, y); 520 | var tileCoords = screenCoordsToTileCoords(screenCoords, self.centerTileScreen, self.center); 521 | var tile = self.tiles[tileCoords]; 522 | if (!tile || tile.dirty) { 523 | requiredTiles.push(tileCoords); 524 | continue; 525 | } 526 | var screenPosition = screenCoordsToScreenPosition(screenCoords, tileCoords, self.positionDiff, self.center); 527 | 528 | renderTile(tile, screenCoords, screenPosition); 529 | renderUnits(tile, screenCoords, screenPosition); 530 | renderIntents(tile, screenCoords, screenPosition); 531 | renderFields(tile, screenCoords, screenPosition); 532 | } 533 | } 534 | if (requiredTiles.length) { 535 | self.tileRequests.push(requiredTiles); 536 | } else { 537 | for (id in self.units) { 538 | var u = self.units[id]; 539 | if (!u.rendered) { 540 | delete self.units[id]; 541 | u.remove(); 542 | } 543 | } 544 | } 545 | }; 546 | }; 547 | 548 | 549 | --------------------------------------------------------------------------------