diff --git a/WORK.md b/WORK.md index 82368e4..eaba624 100644 --- a/WORK.md +++ b/WORK.md @@ -20,6 +20,12 @@ * bonuses 0% * eagle 0% +* game + * classic + * state 0% + * bot 5% + * player: 50% + * render * map 100% * tanks 100% @@ -34,6 +40,7 @@ * proto -* common - * bot 0% - * single game 20% \ No newline at end of file + +* webapp + * angular app 0% + * google analytics 0% \ No newline at end of file diff --git a/build/client.js b/build/client.js index 7fdb160..ebb49f7 100755 --- a/build/client.js +++ b/build/client.js @@ -10,6 +10,9 @@ const version = require('./version'); const prepare = require('./prepare'); const debug = require('../tasks/debug'); const webserver = require('gulp-webserver'); +const run = require('gulp-run'); +const tail = require('../tasks/tail'); +const deb = require('gulp-debian'); const generate = () => function generate() { @@ -54,11 +57,47 @@ const webapp = function () { return gulp.src('src/webapp/*').pipe(gulp.dest('target')); }; + +const buildDeb = gulp.series( + () => gulp.src('src/client/debian/**/*').pipe(gulp.dest('target/debian')), + () => gulp.src('target/linux/**/*').pipe(gulp.dest('target/debian/usr/share/tankz')), + () => gulp.src('target/debian/*') + .pipe(deb({ + package: 'tankz', + version: version, + section: 'base', + priority: 'optional', + architecture: 'all', + maintainer: 'shmyga ', + description: 'Tank\'z', + changelog: [], + postinst: [ + 'if [ "$1" = "configure" ] && [ -x "`which update-menus 2>/dev/null`" ] ; then\n' + + ' update-menus\n' + + 'fi' + ], + _target: '/', + _out: 'target', + _clean: false, + _verbose: true + })) +); + + exports['client:flash:html'] = gulp.parallel(flashIndex, flashJs); exports['client:flash'] = gulp.series(prepare(Haxe.ID), generate(), build('flash'), exports['client:flash:html']); exports['client:html5'] = gulp.series(prepare(Haxe.ID), generate(), build('html5')); +exports['client:linux'] = gulp.series(prepare(Haxe.ID), generate(), build('linux')); exports['client:webapp'] = webapp; -exports['client'] = gulp.series(prepare(Haxe.ID), generate(), gulp.parallel(build('flash'), build('html5')), exports['client:flash:html'], webapp); +exports['client:deb'] = buildDeb; +exports['client'] = gulp.series( + prepare(Haxe.ID), + generate(), + gulp.parallel(build('flash'), build('html5'), build('linux')), + exports['client:flash:html'], + webapp, + buildDeb, +); @@ -70,15 +109,29 @@ const testFlash = function() { }; const testHtml5 = function() { - return gulp.series(build('html5'), () => gulp.src('target/html5').pipe(webserver({ - host: 'localhost', port: 3000, - open: true, - fallback: 'index.html' - })))(); + return gulp.series( + build('html5'), + () => gulp.src('target/html5').pipe(webserver({ + host: 'localhost', port: 3000, + open: true, + fallback: 'index.html' + })) + )(); }; +const testLinux = gulp.series( + build('linux'), + () => gulp.src('target/linux/tankz') + .pipe(run('./tankz', {cwd: 'target/linux', verbosity: 1})) + .pipe(tail(debug.log)) + .pipe(gulp.dest('target/log')) +); + + + exports['client:flash:test'] = gulp.series(prepare(Haxe.ID, FlashPlayer.ID), testFlash); exports['client:html5:test'] = gulp.series(prepare(Haxe.ID), testHtml5); +exports['client:linux:test'] = gulp.series(prepare(Haxe.ID), testLinux); exports['client:test'] = exports['client:flash:test']; diff --git a/build/prepare.js b/build/prepare.js index c9485ff..91e0bf0 100755 --- a/build/prepare.js +++ b/build/prepare.js @@ -1,23 +1,14 @@ const AdobeAir = require('../tasks/adobeAir'); const Haxe = require('../tasks/haxe'); const FlashPlayer = require('../tasks/flashplayer'); - -const packages = [ - {name:'openfl', version:'6.5.3'}, - {name:'lime', version:'5.9.1'}, - {name:'promhx', version:'1.1.0'}, - {name:'protohx', version:'0.4.6'}, - {name:'yaml', version:'1.3.0'}, - {name:'haxework', git:'git@bitbucket.org:shmyga/haxework.git'}, - {name:'orm', version:'2.1.0'}, -]; +const packageInfo = require('../package.json'); const prepareOne = (value) => { switch (value) { case Haxe.ID: const haxe = new Haxe(); - return haxe.prepare().then(() => haxe.install(packages)); + return haxe.prepare().then(() => haxe.install(packageInfo.haxeDependencies)); case AdobeAir.ID: return new AdobeAir().prepare(); case FlashPlayer.ID: diff --git a/config/deploy.rb b/config/deploy.rb index fa74c84..a09244b 100644 --- a/config/deploy.rb +++ b/config/deploy.rb @@ -6,7 +6,7 @@ user = 'holop' # git set :application, app -set :repo_url, "git@bitbucket.org:shmyga/#{app}.git" +set :repo_url, "git@bitbucket.org:infernalgames/#{app}.git" set :user, user set :deploy_to, "/home/#{user}/repo/#{app}" diff --git a/package.json b/package.json index d59f69b..acb1743 100755 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "tankz", - "version": "0.0.6", + "version": "0.2.1", "private": true, "devDependencies": { "ansi-colors": "^1.0.1", @@ -15,6 +15,8 @@ "gulp-babel": "^7.0.0", "gulp-clean": "^0.3.2", "gulp-concat": "^2.6.1", + "gulp-debian": "^0.1.9", + "gulp-run": "^1.7.1", "gulp-template": "^5.0.0", "gulp-uglify": "^3.0.0", "gulp-webserver": "^0.9.1", @@ -26,5 +28,14 @@ "unzip-stream": "^0.2.1", "vinyl-fs": "^3.0.1", "yargs": "^10.0.3" + }, + "haxeDependencies": { + "lime": "6.0.1", + "openfl": "7.0.0", + "promhx": "1.1.0", + "protohx": "0.4.6", + "yaml": "1.3.0", + "haxework": "git@bitbucket.org:shmyga/haxework.git", + "orm": "2.1.0" } } diff --git a/project.xml b/project.xml index f2e7708..66938b0 100755 --- a/project.xml +++ b/project.xml @@ -1,20 +1,22 @@ - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/client/debian/usr/share/applications/tankz.desktop b/src/client/debian/usr/share/applications/tankz.desktop new file mode 100644 index 0000000..88dba30 --- /dev/null +++ b/src/client/debian/usr/share/applications/tankz.desktop @@ -0,0 +1,9 @@ +[Desktop Entry] +Encoding=UTF-8 +Type=Application +Comment=Tank'z game +Exec=bash -c 'cd "/usr/share/tankz" && ./tankz' +Icon=/usr/share/tankz/resources/images/tank/player/tank_p3_0-0.png +Name=Tank'z +Categories=Game +Terminal=false diff --git a/src/client/haxe/layout/frames/game.json b/src/client/haxe/layout/frames/game.json index 19ea442..338c97c 100644 --- a/src/client/haxe/layout/frames/game.json +++ b/src/client/haxe/layout/frames/game.json @@ -1,6 +1,10 @@ { "pWidth": 100, "pHeight": 100, "views": [ + { + "id": "state", "@type": "haxework.gui.LabelView", "@style": "label", + "pWidth": 100, "height": 20 + }, { "id": "render", "@type": "ru.m.tankz.render.Render", "contentSize": true diff --git a/src/client/haxe/layout/frames/level.json b/src/client/haxe/layout/frames/level.json new file mode 100644 index 0000000..4da14f3 --- /dev/null +++ b/src/client/haxe/layout/frames/level.json @@ -0,0 +1,21 @@ +{ + "pWidth": 100, "pHeight": 100, + "views": [ + { + "id":"levels", "@type":"haxework.gui.list.VListView", + "factory": "@class:ru.m.tankz.view.frames.list.LevelView", + "pWidth":100, + "pHeight":100, + "paddings":10, + "scroll":{ + "@type":"haxework.gui.list.VScrollView", + "width":10, "pHeight":100, + "skin":{"@type":"haxework.gui.list.VScrollSkin"} + }, + "skin": { + "@type": "haxework.gui.skin.ColorSkin", + "color": "0x000000", "alpha": 0.0 + } + } + ] +} \ No newline at end of file diff --git a/src/client/haxe/layout/frames/start.json b/src/client/haxe/layout/frames/start.json index 6378242..9315e93 100644 --- a/src/client/haxe/layout/frames/start.json +++ b/src/client/haxe/layout/frames/start.json @@ -19,9 +19,9 @@ "@style": "button" }, { - "id": "start_lan", + "id": "dota", "@type": "haxework.gui.ButtonView", - "text": "Multiplayer", + "text": "DotA", "@style": "button" } ] diff --git a/src/client/haxe/layout/main.json b/src/client/haxe/layout/main.json index fcbeaca..b475189 100755 --- a/src/client/haxe/layout/main.json +++ b/src/client/haxe/layout/main.json @@ -15,6 +15,10 @@ "id": "start", "@type": "ru.m.tankz.view.frames.StartFrame" }, + { + "id": "level", + "@type": "ru.m.tankz.view.frames.LevelFrame" + }, { "id": "game", "@type": "ru.m.tankz.view.frames.GameFrame" diff --git a/src/client/haxe/layout/other.json b/src/client/haxe/layout/other.json index bf66831..d798e31 100644 --- a/src/client/haxe/layout/other.json +++ b/src/client/haxe/layout/other.json @@ -1,13 +1,18 @@ { - "person": { + "level": { "width": 440, "height": 44, + "margins": 5, "views": [ { - "id": "nameLabel", + "id": "label", "@type": "haxework.gui.LabelView", - "pWidth": 100, "height": 44, "text": "Выбор персонажа", + "pWidth": 100, "pHeight": 100, "text": "", "@style": "label" } - ] + ], + "skin": { + "@type": "haxework.gui.skin.ColorSkin", + "color": "0x000000", "alpha": 0.2 + } } } \ No newline at end of file diff --git a/src/client/haxe/ru/m/tankz/Client.hx b/src/client/haxe/ru/m/tankz/Client.hx index 79cbc5f..a90df2d 100755 --- a/src/client/haxe/ru/m/tankz/Client.hx +++ b/src/client/haxe/ru/m/tankz/Client.hx @@ -1,5 +1,8 @@ package ru.m.tankz; +import ru.m.tankz.game.ClassicGame; +import ru.m.tankz.game.DotaGame; +import ru.m.tankz.game.Game; import flash.ui.Keyboard; import flash.events.KeyboardEvent; import flash.text.Font; @@ -10,8 +13,6 @@ import haxework.resources.IResources; import haxework.gui.VGroupView; import haxework.gui.ViewBuilder; import haxework.gui.ButtonView; -import flash.display.Sprite; -import haxework.gui.IGroupView; import ru.m.tankz.PacketBuilder; import haxework.log.JSLogger; import haxework.gui.frame.IFrameSwitcher; @@ -61,11 +62,11 @@ class Client implements IConnectionHandler { Provider.set(IConnection, new ru.m.connect.js.JsConnection("localhost", 5001)); #end - Provider.get(IConnection).handler.addListener(this); - + //Provider.get(IConnection).handler.addListener(this); view = new MainView(); Provider.set(IFrameSwitcher, view.switcher); Root.bind(view); + view.content.stage.stageFocusRect = false; //view.logout.onPress = this; view.switcher.change(StartFrame.ID); @@ -74,6 +75,9 @@ class Client implements IConnectionHandler { view.switcher.change(StartFrame.ID); } }); + + Provider.setFactory(Game, ClassicGame, ClassicGame.TYPE); + Provider.setFactory(Game, DotaGame, DotaGame.TYPE); } public function onPress(view:ButtonView):Void { diff --git a/src/client/haxe/ru/m/tankz/core/PlayerControl.hx b/src/client/haxe/ru/m/tankz/control/HumanControl.hx similarity index 75% rename from src/client/haxe/ru/m/tankz/core/PlayerControl.hx rename to src/client/haxe/ru/m/tankz/control/HumanControl.hx index cb5be6d..0a7e287 100644 --- a/src/client/haxe/ru/m/tankz/core/PlayerControl.hx +++ b/src/client/haxe/ru/m/tankz/control/HumanControl.hx @@ -1,27 +1,27 @@ -package ru.m.tankz.core; +package ru.m.tankz.control; +import ru.m.tankz.control.Control; import haxe.Timer; import ru.m.geom.Direction; import flash.events.FocusEvent; import flash.ui.Keyboard; -import ru.m.tankz.engine.Engine; -import ru.m.tankz.core.Tank.TankAction; import flash.events.KeyboardEvent; import flash.Lib; -class PlayerControl { - private var engine:Engine; - private var id:Int; +typedef KeyBinding = Map; - private var keyBinding:Map; + +class HumanControl extends Control { + public static var TYPE(default, never):ControlType = 'human'; + + private var keyBinding:KeyBinding; private var moveQueue:Array; private var shotTimer:Timer; - public function new(id:Int, engine:Engine, keyBinding:Map) { - this.id = id; - this.engine = engine; - this.keyBinding = keyBinding; + public function new(index:Int) { + super({type:TYPE, index:index}); + this.keyBinding = resolve(index); moveQueue = new Array(); Lib.current.stage.addEventListener(KeyboardEvent.KEY_DOWN, onKeyDown); Lib.current.stage.addEventListener(KeyboardEvent.KEY_UP, onKeyUp); @@ -44,7 +44,7 @@ class PlayerControl { case _: } if (event.keyCode == Keyboard.U) { - engine.action(id, TankAction.LEVEL_UP(1)); + action(TankAction.LEVEL_UP(1)); } } @@ -69,40 +69,40 @@ class PlayerControl { private function updateMove():Void { if (moveQueue.length == 0) { - engine.action(id, TankAction.STOP); + action(TankAction.STOP); } else { switch (keyBinding.get(moveQueue[0])) { case TankAction.MOVE(direction): - engine.action(id, TankAction.MOVE(direction)); + action(TankAction.MOVE(direction)); case _: } } } private function shot():Void { - engine.action(id, TankAction.SHOT); + action(TankAction.SHOT); } - public static function forPlayer(index:Int, tankId:Int, engine:Engine):PlayerControl { + private static function resolve(index:Int):KeyBinding { switch (index) { case 0: - return new PlayerControl(tankId, engine, [ + return [ Keyboard.A => TankAction.MOVE(Direction.LEFT), Keyboard.S => TankAction.MOVE(Direction.BOTTOM), Keyboard.W => TankAction.MOVE(Direction.TOP), Keyboard.D => TankAction.MOVE(Direction.RIGHT), Keyboard.SPACE => TankAction.SHOT - ]); + ]; case 1: - return new PlayerControl(tankId, engine, [ + return [ Keyboard.LEFT => TankAction.MOVE(Direction.LEFT), Keyboard.DOWN => TankAction.MOVE(Direction.BOTTOM), Keyboard.UP => TankAction.MOVE(Direction.TOP), Keyboard.RIGHT => TankAction.MOVE(Direction.RIGHT), Keyboard.NUMPAD_0 => TankAction.SHOT - ]); + ]; case _: - throw 'Invalid player index ${index}'; + throw 'Invalid control index ${index}'; } } } diff --git a/src/client/haxe/ru/m/tankz/render/IRender.hx b/src/client/haxe/ru/m/tankz/render/IRender.hx deleted file mode 100755 index d8a3810..0000000 --- a/src/client/haxe/ru/m/tankz/render/IRender.hx +++ /dev/null @@ -1,10 +0,0 @@ -package ru.m.tankz.render; - -import ru.m.tankz.engine.Engine; -import haxework.gui.IView; - - -interface IRender extends IView { - public function draw(game:Engine):Void; - public function reset():Void; -} diff --git a/src/client/haxe/ru/m/tankz/render/Render.hx b/src/client/haxe/ru/m/tankz/render/Render.hx index 858a7f7..bdc16ae 100755 --- a/src/client/haxe/ru/m/tankz/render/Render.hx +++ b/src/client/haxe/ru/m/tankz/render/Render.hx @@ -1,12 +1,11 @@ package ru.m.tankz.render; -import ru.m.tankz.core.MobileEntity; +import flash.display.DisplayObjectContainer; +import ru.m.tankz.core.EntityType; +import flash.display.DisplayObject; +import Type.ValueType; +import ru.m.tankz.render.RenderItem; import ru.m.tankz.engine.Engine; -import ru.m.tankz.core.Entity; -import ru.m.tankz.map.Brick; -import ru.m.geom.Direction; -import flash.geom.Matrix; -import openfl.Assets; import ru.m.tankz.core.Bullet; import ru.m.tankz.core.Tank; import flash.display.Sprite; @@ -14,64 +13,18 @@ import flash.display.Graphics; import haxework.gui.SpriteView; -interface IState { - public function update(object:T):Bool; -} - -class BrickState implements IState { - private var type:Int; - - public function new() {} - - public function update(object:Brick):Bool { - if (type != object.config.type) { - type = object.config.type; - return true; - } - return false; - } -} - -class EntityState implements IState { - private var x:Float; - private var y:Float; - private var d:Direction; - private var type:Int; - - public function new() {} - - public function update(object:Entity):Bool { - if (x != object.rect.x || y != object.rect.y || !d.equals(object.rect.direction)) { - x = object.rect.x; - y = object.rect.y; - d = object.rect.direction; - return true; - } - if (Std.is(object, Tank)) { - var tank:Tank = cast object; - if (tank.config.type != type) { - type = tank.config.type; - return true; - } - } - return false; - } -} - - -class Render extends SpriteView implements IRender { +class Render extends SpriteView implements EngineListener { private var backgroundLayer:Sprite; private var groundLayer:Sprite; private var entryLayer:Sprite; private var upLayer:Sprite; - private var layersForUpdate:Map; - private var states:Map>; + private var background:Sprite; + private var items:Map>; public function new() { super(); - layersForUpdate = new Map(); backgroundLayer = new Sprite(); groundLayer = new Sprite(); entryLayer = new Sprite(); @@ -83,160 +36,97 @@ class Render extends SpriteView implements IRender { reset(); } - private function invalidateLayers(game:Engine):Void { - for (brick in game.map.bricks) { - if (!states.exists(brick.key)) { - states[brick.key] = new BrickState(); - } - if (states.get(brick.key).update(brick)) { - layersForUpdate[groundLayer] = true; - layersForUpdate[upLayer] = true; - } - } - layersForUpdate[groundLayer] = true; //ToDo: - for (entry in game.entities) { - if (!states.exists(entry.key)) { - states[entry.key] = new EntityState(); - } - if (states.get(entry.key).update(entry)) { - layersForUpdate[entryLayer] = true; - } - } - for (key in game.removedEntities) { - if (states.exists(key)) { - states.remove(key); - layersForUpdate[entryLayer] = true; - } - } - } - private function drawBackground(game:Engine):Void { var mapWidth = game.map.gridWidth * game.map.cellWidth; var mapHeight = game.map.gridHeight * game.map.cellHeight; - - if (layersForUpdate[backgroundLayer]) { - var g:Graphics = backgroundLayer.graphics; - g.clear(); - g.beginFill(0x000000); - g.drawRect(0, 0, mapWidth, mapHeight); - g.endFill(); - if (contentSize) { - width = mapWidth; - height = mapHeight; - } - layersForUpdate[backgroundLayer] = false; - } - } - - private function drawMap(game:Engine):Void { - if (layersForUpdate[groundLayer] || layersForUpdate[upLayer]) { - groundLayer.graphics.clear(); - upLayer.graphics.clear(); - for (brick in game.map.bricks) if (!brick.destroyed) { - var g:Graphics = null; - if (brick.config.layer < 3) { - g = groundLayer.graphics; - } else if (brick.config.layer >= 3) { - g = upLayer.graphics; - } - if (g != null) { - g.beginBitmapFill(Assets.getBitmapData('resources/images/map/map_${brick.config.type}.png')); - g.drawRect( - brick.rect.x, - brick.rect.y, - brick.rect.width, - brick.rect.height - ); - for (cell in brick.cells.iterator()) { - if (cell.destroyed) { - g.beginFill(0x000000); - g.drawRect( - cell.rect.x, - cell.rect.y, - cell.rect.width, - cell.rect.height - ); - } - } - g.endFill(); - } - } - - // Debug cells - /*var g = groundLayer.graphics; - for (c in game.map.grid.cells.iterator()) { - var color:Int = 0x000000; - if (c.armor == 1) { - color = 0xffcc00; - } else if (c.armor > 1) { - color = 0x606060; - } else if (c.layer == 0) { - color = 0xffff90; - } else if (c.layer == 1) { - color = 0x0000ff; - }else if (c.layer > 1) { - color = 0x00ff00; - } - g.beginFill(color); - g.drawRect(c.rect.x, c.rect.y, c.rect.width, c.rect.height); - g.endFill(); - } - layersForUpdate[groundLayer] = false; - layersForUpdate[upLayer] = false;*/ - } - } - - public function drawEntities(game:Engine):Void { - if (layersForUpdate[entryLayer]) { - var g:Graphics = entryLayer.graphics; - g.clear(); - for (ent in game.entities) { - var image:String = null; - if (Std.is(ent, Tank)) { - var tank:Tank = cast ent; - image = 'resources/images/tank/${tank.config.group}/tank_${tank.config.group.charAt(0)}${tank.config.type}_${tank.index}-0.png'; - } else if (Std.is(ent, Bullet)) { - var bullet:Bullet = cast ent; - image = 'resources/images/bullet/bullet_${bullet.config.piercing > 1 ? 1 : 0}.png'; - } else { - image = 'ERROR'; // ToDo: - } - var m = new Matrix(); - if (Std.is(ent, MobileEntity)) { - m.rotate(calcRotate(cast(ent, MobileEntity).direction)); - } - m.translate(ent.rect.x, ent.rect.y); - g.beginBitmapFill(Assets.getBitmapData(image), m, true, true); - g.drawRect(ent.rect.x, ent.rect.y, ent.rect.width, ent.rect.height); - g.endFill(); - } - layersForUpdate[entryLayer] = false; + var g:Graphics = backgroundLayer.graphics; + g.clear(); + g.beginFill(0x000000); + g.drawRect(0, 0, mapWidth, mapHeight); + g.endFill(); + if (contentSize) { + width = mapWidth; + height = mapHeight; } } public function draw(game:Engine):Void { - invalidateLayers(game); - drawBackground(game); - drawMap(game); - drawEntities(game); + for (brick in game.map.bricks) if (brick.config.type > 0) { + if (!items.exists(brick.key)) { + items[brick.key] = new BrickItem(brick); + if (brick.config.layer > 2) { + upLayer.addChild(items[brick.key].view); + } else { + groundLayer.addChild(items[brick.key].view); + } + } + } + for (item in items) { + item.update(); + } + if (background == null) { + drawBackground(game); + } + } + + private function clearLayer(layer:DisplayObjectContainer) { + while (layer.numChildren > 0) layer.removeChildAt(0); } public function reset():Void { - states = new Map>(); - layersForUpdate[backgroundLayer] = true; + items = new Map>(); + if (background != null) { + backgroundLayer.removeChild(background); + background = null; + } + clearLayer(entryLayer); + clearLayer(groundLayer); + clearLayer(upLayer); } - private function calcRotate(direction:Direction):Float { - return (if (direction == Direction.RIGHT) { - 0; - } else if (direction == Direction.LEFT) { - 180; - } else if (direction == Direction.TOP) { - 270; - } else if (direction == Direction.BOTTOM) { - 90; - } else { - 0; - }) * (Math.PI / 180); + public function onSpawn(entity:EntityType):Void { + switch(entity) { + case EntityType.TANK(tank): + var item = new TankItem(tank); + items.set(tank.key, item); + entryLayer.addChild(item.view); + item.update(); + case EntityType.BULLET(bullet): + var item = new BulletItem(bullet); + items.set(bullet.key, item); + entryLayer.addChild(item.view); + item.update(); + case EntityType.EAGLE(eagle): + var item = new EagleItem(eagle); + items.set(eagle.key, item); + entryLayer.addChild(item.view); + item.update(); + case _: + } + } + + public function onCollision(entity:EntityType, with:EntityType):Void { + + } + + public function onDestroy(entity:EntityType):Void { + switch(entity) { + case EntityType.TANK(tank): + if (items.exists(tank.key)) { + entryLayer.removeChild(items.get(tank.key).view); + items.remove(tank.key); + } + case EntityType.BULLET(bullet): + if (items.exists(bullet.key)) { + entryLayer.removeChild(items.get(bullet.key).view); + items.remove(bullet.key); + } + case EntityType.EAGLE(eagle): + if (items.exists(eagle.key)) { + cast(items.get(eagle.key), EagleItem).destoyed = true; + items.get(eagle.key).redraw(); + } + case _: + } } } diff --git a/src/client/haxe/ru/m/tankz/render/RenderItem.hx b/src/client/haxe/ru/m/tankz/render/RenderItem.hx new file mode 100644 index 0000000..c51892b --- /dev/null +++ b/src/client/haxe/ru/m/tankz/render/RenderItem.hx @@ -0,0 +1,154 @@ +package ru.m.tankz.render; + +import ru.m.tankz.core.Eagle; +import flash.display.DisplayObject; +import flash.display.Shape; +import ru.m.geom.Direction; +import ru.m.geom.Rectangle; +import ru.m.tankz.core.Bullet; +import ru.m.tankz.map.Brick; +import openfl.Assets; +import ru.m.tankz.core.Tank; +import flash.display.Bitmap; + + +typedef TRectangle = { + var rect(default, null):Rectangle; +} + + +class RenderItem { + + public var value(default, null):T; + public var view(default, null):DisplayObject; + + private var bitmap:Bitmap; + + public function new(value:T) { + this.value = value; + this.bitmap = new Bitmap(); + this.view = bitmap; + redraw(); + } + + public function redraw():Void { + bitmap.bitmapData = Assets.getBitmapData(getImage()); + } + + public function update():Void { + var rect = value.rect; + view.rotation = calcRotate(rect.direction); + view.x = rect.x - rect.width * (rect.direction.x + 1) / 2 + rect.width * (rect.direction.y + 1) / 2 + 0.5 * rect.width; + view.y = rect.y - rect.height * (rect.direction.x + 1) / 2 - rect.height * (rect.direction.y + 1) / 2 + 1.5 * rect.height; + } + + private function getImage():String { + return 'ERROR'; + } + + private static function calcRotate(direction:Direction):Float { + return (if (direction == Direction.RIGHT) { + 0; + } else if (direction == Direction.LEFT) { + 180; + } else if (direction == Direction.TOP) { + 270; + } else if (direction == Direction.BOTTOM) { + 90; + } else { + 0; + }); + } +} + + +class BrickItem extends RenderItem { + + private var shape:Shape; + private var broken:Int; + + public function new(value:Brick) { + this.shape = new Shape(); + super(value); + this.view = shape; + } + + override public function redraw():Void { + var image = Assets.getBitmapData(getImage()); + var g = shape.graphics; + g.clear(); + if (value.destroyed) return; + g.beginBitmapFill(image); + g.drawRect(0, 0, value.rect.width, value.rect.height); + for (c in value.cells) { + if (c.destroyed) { + g.beginFill(0x000000); + g.drawRect(c.rect.x - value.rect.x, c.rect.y - value.rect.y, c.rect.width, c.rect.height); + } + } + g.endFill(); + } + + override public function update():Void { + super.update(); + var b = value.broken; + if (b != this.broken) { + this.broken = b; + redraw(); + } + } + + override private function getImage():String { + return 'resources/images/map/map_${value.config.type}.png'; + } +} + + + +class TankItem extends RenderItem { + + private var type:String; + + override private function getImage():String { + var group = value.config.group; + var index = value.playerId.index; + if (group == 'human') group = 'player'; + if (group == 'radiant') { + group = 'player'; + index = 0; + } + if (group == 'dire') { + group = 'player'; + index = 1; + } + if (group == 'bot') index = 0; + return 'resources/images/tank/${group}/tank_${group.charAt(0)}${value.config.type}_${index}-0.png'; + } + + override public function update():Void { + super.update(); + var t = value.config.type; + if (t != this.type) { + this.type = t; + redraw(); + } + } +} + + +class BulletItem extends RenderItem { + + override private function getImage():String { + return 'resources/images/bullet/bullet_${value.config.piercing > 1 ? 1 : 0}.png'; + } +} + + +class EagleItem extends RenderItem { + + public var destoyed(default, default):Bool; + + override private function getImage():String { + return 'resources/images/eagle/eagle-${destoyed ? 1 : 0}.png'; + } +} diff --git a/src/client/haxe/ru/m/tankz/view/frames/GameFrame.hx b/src/client/haxe/ru/m/tankz/view/frames/GameFrame.hx index df0cb27..dff3100 100755 --- a/src/client/haxe/ru/m/tankz/view/frames/GameFrame.hx +++ b/src/client/haxe/ru/m/tankz/view/frames/GameFrame.hx @@ -1,62 +1,129 @@ package ru.m.tankz.view.frames; -import ru.m.tankz.config.ConfigBundle; -import ru.m.tankz.proto.core.GameType; -import ru.m.tankz.proto.core.Game; -import ru.m.tankz.core.PlayerControl; -import ru.m.tankz.engine.Engine; -import protohx.Message; -import ru.m.tankz.proto.pack.GameUpdateResponse; -import ru.m.connect.IConnection; -import haxework.gui.ViewBuilder; +import haxe.ds.Option; +import haxe.ds.Option; import flash.events.Event; -import haxework.provider.Provider; +import haxe.Timer; +import haxework.gui.frame.IFrameSwitcher; +import haxework.gui.LabelView; import haxework.gui.VGroupView; +import haxework.gui.ViewBuilder; +import haxework.provider.Provider; +import protohx.Message; +import ru.m.connect.IConnection; +import ru.m.tankz.game.Game; +import ru.m.tankz.game.GameState; +import ru.m.tankz.proto.pack.GameUpdateResponse; +import ru.m.tankz.render.Render; + + +interface GameFrameLayout { + var state(default, null):LabelView; + var render(default, null):Render; +} @:template("layout/frames/game.json", "layout/styles.json") -class GameFrame extends VGroupView implements ViewBuilder implements IPacketHandler { +class GameFrame extends VGroupView implements ViewBuilder implements IPacketHandler implements GameFrameLayout { private static inline var TAG = "GameFrame"; public static inline var ID = "game"; - private var engine:Engine; - private var controls:Map; + private var game:Game; + private var timer:Timer; - public function init():Void { - engine = new Engine(); - controls = new Map(); - } + public function init():Void {} public function onShow():Void { - var game:Game = Provider.get(Game); - engine.init(ConfigBundle.get(GameType.CLASSIC, 0)); - engine.initTanks(game.players); - content.addEventListener(Event.ENTER_FRAME, updateGame); - for (index in 0...game.players.length) { - var playerId:Int = game.players[index].id; - controls.set(playerId, PlayerControl.forPlayer(index, playerId, engine)); - } - Provider.get(IConnection).packetHandler.addListener(this); - render.draw(engine); + start(Provider.get(GameState)); } - public function onHide():Void { - Provider.get(IConnection).packetHandler.removeListener(this); - content.removeEventListener(Event.ENTER_FRAME, updateGame); - engine.clear(); + private function start(s:GameState):Void { + game = Provider.build(Game, s.type); + if (game == null) { + throw 'Unsupported game type "${s.type}"'; + } + game.engine.listeners.push(render); + game.start(s).then(onGameStateChange).endThen(onGameComplete); + content.addEventListener(Event.ENTER_FRAME, redraw); + //Provider.get(IConnection).packetHandler.addListener(this); + render.draw(game.engine); + timer = new Timer(10); + timer.run = updateEngine; + state.text = stateString(s); + } + + private function stop():Void { + //Provider.get(IConnection).packetHandler.removeListener(this); + if (timer != null) { + timer.stop(); + timer = null; + } + content.removeEventListener(Event.ENTER_FRAME, redraw); + if (game != null) { + game.dispose(); + game = null; + } render.reset(); } - private function updateGame(_):Void { - engine.update(); - render.draw(engine); + private function stateString(state:GameState):String { + var result:Array = []; + result.push('Level: ${state.level}'); + for (teamId in state.teams.keys()) { + var ts:TeamState = state.teams[teamId]; + if (ts.lose) { + result.push('${teamId}: LOSE'); + } else if (ts.life > -1) { + result.push('${teamId}: ${ts.life}'); + } else { + for (index in ts.players.keys()) { + var ps:PlayerState = ts.players[index]; + if (ps.life > -1) { + result.push('${teamId}${index}: ${ps.life}'); + } + } + } + } + return '[ ${result.join(' | ')} ]'; + } + + private function onGameStateChange(s:GameState):GameState { + state.text = stateString(s); + return s; + } + + private function onGameComplete(result:Option):Void { + switch (result) { + case Option.Some(s): + state.text = stateString(s); + case Option.None: + } + switch (game.next()) { + case Option.Some(s): + stop(); + start(s); + case Option.None: + Provider.get(IFrameSwitcher).change(StartFrame.ID); + } + } + + public function onHide():Void { + stop(); + } + + private function updateEngine():Void { + game.engine.update(); + } + + private function redraw(_):Void { + render.draw(game.engine); } public function onGameUpdateResponse(packet:GameUpdateResponse):Void { //engine.updateFromChanges(packet.changes); - render.draw(engine); + render.draw(game.engine); } public function onPacket(packet:Message):Void {} diff --git a/src/client/haxe/ru/m/tankz/view/frames/LevelFrame.hx b/src/client/haxe/ru/m/tankz/view/frames/LevelFrame.hx new file mode 100644 index 0000000..a379872 --- /dev/null +++ b/src/client/haxe/ru/m/tankz/view/frames/LevelFrame.hx @@ -0,0 +1,35 @@ +package ru.m.tankz.view.frames; + +import ru.m.tankz.config.ConfigBundle; +import haxework.gui.frame.IFrameSwitcher; +import ru.m.tankz.game.GameState; +import haxework.provider.Provider; +import haxework.gui.list.ListView; +import haxework.gui.ViewBuilder; +import haxework.gui.VGroupView; + + +interface LevelFrameLayout { + var levels(default, null):ListView; +} + + +@:template("layout/frames/level.json", "layout/styles.json") +class LevelFrame extends VGroupView implements ViewBuilder implements LevelFrameLayout implements ListViewListener { + public static inline var ID = "level"; + + public function init():Void { + levels.dispatcher.addListener(this); + } + + public function onShow():Void { + var state = Provider.get(GameState); + var c = ConfigBundle.get(state.type).levels; + levels.data = [for (i in 0...c) i]; + } + + public function onListItemClick(item:IListItemView):Void { + Provider.get(GameState).level = item.data; + Provider.get(IFrameSwitcher).change(GameFrame.ID); + } +} diff --git a/src/client/haxe/ru/m/tankz/view/frames/StartFrame.hx b/src/client/haxe/ru/m/tankz/view/frames/StartFrame.hx index ebccba6..67f0223 100644 --- a/src/client/haxe/ru/m/tankz/view/frames/StartFrame.hx +++ b/src/client/haxe/ru/m/tankz/view/frames/StartFrame.hx @@ -1,43 +1,53 @@ package ru.m.tankz.view.frames; -import ru.m.tankz.proto.core.GameType; -import ru.m.tankz.proto.core.Player; -import ru.m.tankz.proto.core.Game; +import ru.m.tankz.game.Game; +import ru.m.tankz.game.GameState; +import ru.m.tankz.game.ClassicGame; +import ru.m.tankz.game.DotaGame; import haxework.gui.frame.IFrameSwitcher; import haxework.provider.Provider; import haxework.gui.ButtonView; import haxework.gui.ViewBuilder; import haxework.gui.VGroupView; + +interface StartFrameLayout { + var start_1p(default, null):ButtonView; + var start_2p(default, null):ButtonView; + var dota(default, null):ButtonView; +} + + @:template("layout/frames/start.json", "layout/styles.json") -class StartFrame extends VGroupView implements ViewBuilder { +class StartFrame extends VGroupView implements ViewBuilder implements StartFrameLayout { public static inline var ID = "start"; public function init() { start_1p.onPress = this; start_2p.onPress = this; + dota.onPress = this; } public function onPress(view:ButtonView):Void { switch (view.id) { - case "start_1p": - startGame(1); - case "start_2p": - startGame(2); + case 'start_1p': + startGame(ClassicGame.TYPE, 1); + case 'start_2p': + startGame(ClassicGame.TYPE, 2); + case 'dota': + startGame(DotaGame.TYPE, 2); } } - private function startGame(playersCount:Int):Void { - var game = new Game(); - game.type = GameType.CLASSIC; - for (i in 0...playersCount) { - var player = new Player(); - player.id = i; - game.players.push(player); + private function startGame(type:GameType, humans:Int):Void { + switch (type) { + case ClassicGame.TYPE: + Provider.set(GameState, ClassicGame.buildState(0, humans)); + Provider.get(IFrameSwitcher).change(LevelFrame.ID); + case DotaGame.TYPE: + Provider.set(GameState, DotaGame.buildState(0, humans)); + Provider.get(IFrameSwitcher).change(GameFrame.ID); } - game.id = 1; - Provider.set(Game, game); - Provider.get(IFrameSwitcher).change(GameFrame.ID); } } diff --git a/src/client/haxe/ru/m/tankz/view/frames/list/GameView.hx b/src/client/haxe/ru/m/tankz/view/frames/list/GameView.hx deleted file mode 100755 index c7f5e4d..0000000 --- a/src/client/haxe/ru/m/tankz/view/frames/list/GameView.hx +++ /dev/null @@ -1,33 +0,0 @@ -package ru.m.tankz.view.frames.list; - -import ru.m.tankz.proto.Person; -import ru.m.tankz.proto.Game; -import haxework.gui.list.ListView.IListItemView; -import haxework.gui.LabelView; -import haxework.gui.skin.ColorSkin; -import haxework.gui.HGroupView; - -class GameView extends HGroupView implements IListItemView { - - public var item_index(default, default):Int; - public var data(default, set):Game; - - private var nameLabel:LabelView; - - public function new() { - super(); - pWidth = 100; - height = 50; - skin = new ColorSkin(0xffffff); - - nameLabel = new LabelView(); - addView(nameLabel); - } - - private function set_data(value:Game):Game { - this.data = value; - var persons:String = Lambda.map(this.data.persons, function(p:Person) return p.name).join(","); - nameLabel.text = "Game_" + this.data.id + "(" + persons + ")"; - return this.data; - } -} \ No newline at end of file diff --git a/src/client/haxe/ru/m/tankz/view/frames/list/LevelView.hx b/src/client/haxe/ru/m/tankz/view/frames/list/LevelView.hx new file mode 100755 index 0000000..072fed4 --- /dev/null +++ b/src/client/haxe/ru/m/tankz/view/frames/list/LevelView.hx @@ -0,0 +1,24 @@ +package ru.m.tankz.view.frames.list; + +import haxework.gui.ViewBuilder; +import haxework.gui.HGroupView; +import haxework.gui.LabelView; +import haxework.gui.list.ListView.IListItemView; + + +interface LevelViewLayout { + var label(default, null):LabelView; +} + +@:template("layout/other.json@level", "layout/styles.json") +class LevelView extends HGroupView implements ViewBuilder implements IListItemView implements LevelViewLayout { + + public var item_index(default, default):Int; + public var data(default, set):Int; + + private function set_data(value:Int):Int { + data = value; + label.text = 'Level ${data}'; + return data; + } +} \ No newline at end of file diff --git a/src/client/haxe/ru/m/tankz/view/frames/list/PersonView.hx b/src/client/haxe/ru/m/tankz/view/frames/list/PersonView.hx deleted file mode 100755 index 4d2dbe0..0000000 --- a/src/client/haxe/ru/m/tankz/view/frames/list/PersonView.hx +++ /dev/null @@ -1,19 +0,0 @@ -package ru.m.tankz.view.frames.list; - -import haxework.gui.ViewBuilder; -import haxework.gui.list.ListView.IListItemView; -import ru.m.tankz.proto.Person; -import haxework.gui.HGroupView; - -@:template("layout/other.json@person", "layout/styles.json") -class PersonView extends HGroupView implements ViewBuilder implements IListItemView { - - public var item_index(default, default):Int; - public var data(default, set):Person; - - private function set_data(value:Person):Person { - this.data = value; - nameLabel.text = this.data.name; - return this.data; - } -} \ No newline at end of file diff --git a/src/client/resources/config/config.yaml b/src/client/resources/classic/config.yaml similarity index 71% rename from src/client/resources/config/config.yaml rename to src/client/resources/classic/config.yaml index 1800afe..64254a8 100644 --- a/src/client/resources/config/config.yaml +++ b/src/client/resources/classic/config.yaml @@ -1,35 +1,10 @@ +levels: 36 + map: cellWidth: 22 cellHeight: 22 gridWidth: 26 gridHeight: 26 - bricks: null - points: - - type: player - index: 0 - x: 8 - y: 24 - direction: top - - type: player - index: 1 - x: 16 - y: 24 - direction: top - - type: bot - index: 1 - x: 0 - y: 0 - direction: bottom - - type: bot - index: 2 - x: 12 - y: 0 - direction: bottom - - type: bot - index: 3 - x: 24 - y: 0 - direction: bottom bricks: # border @@ -61,6 +36,44 @@ bricks: layer: 2 armor: 1 +teams: + - id: human + spawnInterval: 0 + points: + - type: eagle + index: -1 + x: 12 + y: 24 + direction: right + - type: tank + index: 0 + x: 8 + y: 24 + direction: top + - type: tank + index: 1 + x: 16 + y: 24 + direction: top + - id: bot + spawnInterval: 3000 + points: + - type: tank + index: -1 + x: 0 + y: 0 + direction: bottom + - type: tank + index: -1 + x: 12 + y: 0 + direction: bottom + - type: tank + index: -1 + x: 24 + y: 0 + direction: bottom + bullet: &bullet width: 12 height: 12 @@ -68,7 +81,7 @@ bullet: &bullet piercing: 1 tanks: - player: + human: - type: 0 width: 36 height: 36 diff --git a/src/client/resources/levels/level000.txt b/src/client/resources/classic/levels/level000.txt similarity index 100% rename from src/client/resources/levels/level000.txt rename to src/client/resources/classic/levels/level000.txt diff --git a/src/client/resources/levels/level001.txt b/src/client/resources/classic/levels/level001.txt similarity index 100% rename from src/client/resources/levels/level001.txt rename to src/client/resources/classic/levels/level001.txt diff --git a/src/client/resources/levels/level002.txt b/src/client/resources/classic/levels/level002.txt similarity index 100% rename from src/client/resources/levels/level002.txt rename to src/client/resources/classic/levels/level002.txt diff --git a/src/client/resources/levels/level003.txt b/src/client/resources/classic/levels/level003.txt similarity index 100% rename from src/client/resources/levels/level003.txt rename to src/client/resources/classic/levels/level003.txt diff --git a/src/client/resources/levels/level004.txt b/src/client/resources/classic/levels/level004.txt similarity index 100% rename from src/client/resources/levels/level004.txt rename to src/client/resources/classic/levels/level004.txt diff --git a/src/client/resources/levels/level005.txt b/src/client/resources/classic/levels/level005.txt similarity index 100% rename from src/client/resources/levels/level005.txt rename to src/client/resources/classic/levels/level005.txt diff --git a/src/client/resources/levels/level006.txt b/src/client/resources/classic/levels/level006.txt similarity index 100% rename from src/client/resources/levels/level006.txt rename to src/client/resources/classic/levels/level006.txt diff --git a/src/client/resources/levels/level007.txt b/src/client/resources/classic/levels/level007.txt similarity index 100% rename from src/client/resources/levels/level007.txt rename to src/client/resources/classic/levels/level007.txt diff --git a/src/client/resources/levels/level008.txt b/src/client/resources/classic/levels/level008.txt similarity index 100% rename from src/client/resources/levels/level008.txt rename to src/client/resources/classic/levels/level008.txt diff --git a/src/client/resources/levels/level009.txt b/src/client/resources/classic/levels/level009.txt similarity index 100% rename from src/client/resources/levels/level009.txt rename to src/client/resources/classic/levels/level009.txt diff --git a/src/client/resources/levels/level010.txt b/src/client/resources/classic/levels/level010.txt similarity index 100% rename from src/client/resources/levels/level010.txt rename to src/client/resources/classic/levels/level010.txt diff --git a/src/client/resources/levels/level011.txt b/src/client/resources/classic/levels/level011.txt similarity index 100% rename from src/client/resources/levels/level011.txt rename to src/client/resources/classic/levels/level011.txt diff --git a/src/client/resources/levels/level012.txt b/src/client/resources/classic/levels/level012.txt similarity index 100% rename from src/client/resources/levels/level012.txt rename to src/client/resources/classic/levels/level012.txt diff --git a/src/client/resources/levels/level013.txt b/src/client/resources/classic/levels/level013.txt similarity index 100% rename from src/client/resources/levels/level013.txt rename to src/client/resources/classic/levels/level013.txt diff --git a/src/client/resources/levels/level014.txt b/src/client/resources/classic/levels/level014.txt similarity index 100% rename from src/client/resources/levels/level014.txt rename to src/client/resources/classic/levels/level014.txt diff --git a/src/client/resources/levels/level015.txt b/src/client/resources/classic/levels/level015.txt similarity index 100% rename from src/client/resources/levels/level015.txt rename to src/client/resources/classic/levels/level015.txt diff --git a/src/client/resources/levels/level016.txt b/src/client/resources/classic/levels/level016.txt similarity index 100% rename from src/client/resources/levels/level016.txt rename to src/client/resources/classic/levels/level016.txt diff --git a/src/client/resources/levels/level017.txt b/src/client/resources/classic/levels/level017.txt similarity index 100% rename from src/client/resources/levels/level017.txt rename to src/client/resources/classic/levels/level017.txt diff --git a/src/client/resources/levels/level018.txt b/src/client/resources/classic/levels/level018.txt similarity index 100% rename from src/client/resources/levels/level018.txt rename to src/client/resources/classic/levels/level018.txt diff --git a/src/client/resources/levels/level019.txt b/src/client/resources/classic/levels/level019.txt similarity index 100% rename from src/client/resources/levels/level019.txt rename to src/client/resources/classic/levels/level019.txt diff --git a/src/client/resources/levels/level020.txt b/src/client/resources/classic/levels/level020.txt similarity index 100% rename from src/client/resources/levels/level020.txt rename to src/client/resources/classic/levels/level020.txt diff --git a/src/client/resources/levels/level021.txt b/src/client/resources/classic/levels/level021.txt similarity index 100% rename from src/client/resources/levels/level021.txt rename to src/client/resources/classic/levels/level021.txt diff --git a/src/client/resources/levels/level022.txt b/src/client/resources/classic/levels/level022.txt similarity index 100% rename from src/client/resources/levels/level022.txt rename to src/client/resources/classic/levels/level022.txt diff --git a/src/client/resources/levels/level023.txt b/src/client/resources/classic/levels/level023.txt similarity index 100% rename from src/client/resources/levels/level023.txt rename to src/client/resources/classic/levels/level023.txt diff --git a/src/client/resources/levels/level024.txt b/src/client/resources/classic/levels/level024.txt similarity index 100% rename from src/client/resources/levels/level024.txt rename to src/client/resources/classic/levels/level024.txt diff --git a/src/client/resources/levels/level025.txt b/src/client/resources/classic/levels/level025.txt similarity index 100% rename from src/client/resources/levels/level025.txt rename to src/client/resources/classic/levels/level025.txt diff --git a/src/client/resources/levels/level026.txt b/src/client/resources/classic/levels/level026.txt similarity index 100% rename from src/client/resources/levels/level026.txt rename to src/client/resources/classic/levels/level026.txt diff --git a/src/client/resources/levels/level027.txt b/src/client/resources/classic/levels/level027.txt similarity index 100% rename from src/client/resources/levels/level027.txt rename to src/client/resources/classic/levels/level027.txt diff --git a/src/client/resources/levels/level028.txt b/src/client/resources/classic/levels/level028.txt similarity index 100% rename from src/client/resources/levels/level028.txt rename to src/client/resources/classic/levels/level028.txt diff --git a/src/client/resources/levels/level029.txt b/src/client/resources/classic/levels/level029.txt similarity index 100% rename from src/client/resources/levels/level029.txt rename to src/client/resources/classic/levels/level029.txt diff --git a/src/client/resources/levels/level030.txt b/src/client/resources/classic/levels/level030.txt similarity index 100% rename from src/client/resources/levels/level030.txt rename to src/client/resources/classic/levels/level030.txt diff --git a/src/client/resources/levels/level031.txt b/src/client/resources/classic/levels/level031.txt similarity index 100% rename from src/client/resources/levels/level031.txt rename to src/client/resources/classic/levels/level031.txt diff --git a/src/client/resources/levels/level032.txt b/src/client/resources/classic/levels/level032.txt similarity index 100% rename from src/client/resources/levels/level032.txt rename to src/client/resources/classic/levels/level032.txt diff --git a/src/client/resources/levels/level033.txt b/src/client/resources/classic/levels/level033.txt similarity index 100% rename from src/client/resources/levels/level033.txt rename to src/client/resources/classic/levels/level033.txt diff --git a/src/client/resources/levels/level034.txt b/src/client/resources/classic/levels/level034.txt similarity index 100% rename from src/client/resources/levels/level034.txt rename to src/client/resources/classic/levels/level034.txt diff --git a/src/client/resources/levels/level035.txt b/src/client/resources/classic/levels/level035.txt similarity index 100% rename from src/client/resources/levels/level035.txt rename to src/client/resources/classic/levels/level035.txt diff --git a/src/client/resources/dota/config.yaml b/src/client/resources/dota/config.yaml new file mode 100644 index 0000000..5d8c19a --- /dev/null +++ b/src/client/resources/dota/config.yaml @@ -0,0 +1,134 @@ +levels: 36 + +map: + cellWidth: 22 + cellHeight: 22 + gridWidth: 40 + gridHeight: 30 + +bricks: + # border + - type: -1 + layer: 2 + armor: -1 + # none + - type: 0 + layer: 0 + armor: 0 + # ace + - type: 1 + layer: 0 + armor: 0 + # bush + - type: 2 + layer: 3 + armor: 0 + # water + - type: 3 + layer: 1 + armor: 0 + # armor + - type: 4 + layer: 2 + armor: 2 + # brick + - type: 5 + layer: 2 + armor: 1 + +teams: + - id: radiant + spawnInterval: 0 + points: + - type: eagle + index: -1 + x: 0 + y: 28 + direction: right + - type: tank + index: 0 + x: 0 + y: 0 + direction: top + - type: tank + index: 1 + x: 6 + y: 10 + direction: top + - type: tank + index: 2 + x: 6 + y: 16 + direction: top + - type: tank + index: 3 + x: 6 + y: 22 + direction: top + - type: tank + index: 4 + x: 10 + y: 28 + direction: top + - id: dire + spawnInterval: 0 + points: + - type: eagle + index: -1 + x: 38 + y: 0 + direction: right + - type: tank + index: 0 + x: 38 + y: 28 + direction: bottom + - type: tank + index: 1 + x: 32 + y: 18 + direction: bottom + - type: tank + index: 2 + x: 32 + y: 12 + direction: bottom + - type: tank + index: 3 + x: 32 + y: 6 + direction: bottom + - type: tank + index: 4 + x: 28 + y: 0 + direction: bottom + + +bullet: &bullet + width: 12 + height: 12 + speed: 0 + piercing: 1 + +tanks: + radiant: &tanks + - type: 0 + width: 36 + height: 36 + speed: 2.5 + bullet: + <<: *bullet + speed: 8.0 + bullets: 1 + + - type: 1 + width: 40 + height: 36 + speed: 3.0 + bullet: + <<: *bullet + speed: 8.5 + bullets: 1 + dire: + - <<: *tanks diff --git a/src/client/resources/dota/levels/level000.txt b/src/client/resources/dota/levels/level000.txt new file mode 100644 index 0000000..65ec086 --- /dev/null +++ b/src/client/resources/dota/levels/level000.txt @@ -0,0 +1,30 @@ +0000000000000000000000000000000000005500 +0000000000000000000000000000000000005500 +0000000000000000000000000000000000005555 +0000000000000000000000000000000000005555 +0000000000000000000000000000000000000000 +0000000000000000000000000000000000000000 +0000000000000000000000000000000000000000 +0000000000000000000000000000000000000000 +0000000000000000000000000000000000000000 +0000000000000000000000000000000000000000 +0000000000000000000000000000000000000000 +0000000000000000000000000000000000000000 +0000000000000000000000000000000000000000 +0000000000000000000000000000000000000000 +0000000000000000000000000000000000000000 +0000000000000000000000000000000000000000 +0000000000000000000000000000000000000000 +0000000000000000000000000000000000000000 +0000000000000000000000000000000000000000 +0000000000000000000000000000000000000000 +0000000000000000000000000000000000000000 +0000000000000000000000000000000000000000 +0000000000000000000000000000000000000000 +0000000000000000000000000000000000000000 +0000000000000000000000000000000000000000 +0000000000000000000000000000000000000000 +5555000000000000000000000000000000000000 +5555000000000000000000000000000000000000 +0055000000000000000000000000000000000000 +0055000000000000000000000000000000000000 diff --git a/src/client/resources/fonts/8-BIT WONDER.TTF b/src/client/resources/fonts/8-BIT WONDER.TTF deleted file mode 100644 index 6d9b397..0000000 Binary files a/src/client/resources/fonts/8-BIT WONDER.TTF and /dev/null differ diff --git a/src/client/resources/fonts/8-BIT WONDER.eot b/src/client/resources/fonts/8-BIT WONDER.eot deleted file mode 100644 index e2c9e2a..0000000 Binary files a/src/client/resources/fonts/8-BIT WONDER.eot and /dev/null differ diff --git a/src/client/resources/fonts/8-BIT WONDER.svg b/src/client/resources/fonts/8-BIT WONDER.svg deleted file mode 100644 index 266614a..0000000 --- a/src/client/resources/fonts/8-BIT WONDER.svg +++ /dev/null @@ -1,161 +0,0 @@ - - -!"#$%&'()*+,-./0123456789:;å<>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_` abcdefghijklmnopqrstuvwxyz|{}~ \ No newline at end of file diff --git a/src/client/resources/fonts/8-BIT WONDER.woff b/src/client/resources/fonts/8-BIT WONDER.woff deleted file mode 100644 index 5902288..0000000 Binary files a/src/client/resources/fonts/8-BIT WONDER.woff and /dev/null differ diff --git a/src/common/haxe/ru/m/tankz/bot/BotControl.hx b/src/common/haxe/ru/m/tankz/bot/BotControl.hx new file mode 100644 index 0000000..1ad360a --- /dev/null +++ b/src/common/haxe/ru/m/tankz/bot/BotControl.hx @@ -0,0 +1,68 @@ +package ru.m.tankz.bot; + +import ru.m.tankz.core.EntityType; +import ru.m.tankz.control.Control; +import ru.m.geom.Direction; +import haxe.Timer; + + +class BotControl extends Control { + public static var TYPE(default, never):ControlType = 'bot'; + + private var shotTimer:Timer; + private var turnTimer:Timer; + + public function new(index:Int) { + super({type:TYPE, index:index}); + } + + override public function onCollision(with:EntityType):Void { + switch (with) { + case EntityType.TANK(_): turn(); + case EntityType.CELL(_): turn(); + case _: + } + } + + override public function start():Void { + //var tank = handler.entities.get(tankId); + //action(TankAction.MOVE(tank.rect.direction)); + action(TankAction.MOVE(Direction.BOTTOM)); // ToDo: hardcode bot start direction + if (shotTimer == null) { + shotTimer = new Timer(1000); + shotTimer.run = shot; + } + if (turnTimer == null) { + turnTimer = new Timer(3000); + turnTimer.run = turn; + } + } + + override public function stop():Void { + if (shotTimer != null) { + shotTimer.stop(); + shotTimer = null; + } + if (turnTimer != null) { + turnTimer.stop(); + turnTimer = null; + } + } + + public function shot():Void { + action(TankAction.SHOT); + } + + public function turn():Void { + action(TankAction.MOVE(randomDirection())); + } + + private function randomDirection():Direction { + return [ + Direction.TOP, + Direction.BOTTOM, + Direction.LEFT, + Direction.RIGHT, + ][Math.floor(Math.random() * 4)]; + } +} diff --git a/src/common/haxe/ru/m/tankz/config/Config.hx b/src/common/haxe/ru/m/tankz/config/Config.hx index 0301229..0d5536b 100644 --- a/src/common/haxe/ru/m/tankz/config/Config.hx +++ b/src/common/haxe/ru/m/tankz/config/Config.hx @@ -1,14 +1,8 @@ package ru.m.tankz.config; -@:enum abstract EntityType(String) from String to String { - var PLAYER = 'player'; - var BOT = 'bot'; - var EAGLE = 'eagle'; -} - typedef SpawnPoint = { - var type:EntityType; + var type:String; var index:Int; var x:Int; var y:Int; @@ -20,9 +14,6 @@ typedef MapConfig = { var cellHeight:Float; var gridWidth:Int; var gridHeight:Int; - - var bricks:Array; - var points:Array; } typedef BrickConfig = { @@ -39,9 +30,12 @@ typedef BulletConfig = { } -typedef TankConfig = { +typedef TankType = { var group:String; - var type:Int; + var type:String; +} + +typedef TankConfig = { > TankType, var width:Float; var height:Float; var speed:Float; @@ -50,48 +44,61 @@ typedef TankConfig = { } +typedef TeamConfig = { + var id:String; + var size:Int; + var spawnInterval:Int; + var points:Array; +} + + class Config { + public var type(default, null):String; + public var levels(default, null):Int; public var map(default, null):MapConfig; public var bricks(default, null):Array; public var tanks(default, null):Array; + public var teams(default, null):Array; - private var pointMap:Map>; private var brickMap:Map; - private var tankMap:Map>; + private var tankMap:Map>; + private var teamMap:Map; - public function new(map:MapConfig, bricks:Array, tanks:Array) { + public function new(type:String, levels:Int, map:MapConfig, bricks:Array, teams:Array, tanks:Array) { + this.type = type; + this.levels = levels; this.map = map; this.bricks = bricks; + this.teams = teams; this.tanks = tanks; init(); } private function init() { - pointMap = new Map(); - for (item in map.points) { - if (!pointMap.exists(item.type)) pointMap.set(item.type, new Map()); - pointMap.get(item.type).set(item.index, item); - } brickMap = new Map(); for (item in bricks) { brickMap.set(item.type, item); } + teamMap = new Map(); + for (team in teams) { + teamMap.set(team.id, team); + } tankMap = new Map(); for (item in tanks) { - if (!tankMap.exists(item.group)) tankMap.set(item.group, new Map()); + if (!tankMap.exists(item.group)) tankMap.set(item.group, new Map()); tankMap.get(item.group).set(item.type, item); } } - public function getSpawnPoint(type:EntityType, index:Int):SpawnPoint { - return pointMap.get(type).get(index); - } - public function getBrick(type:Int):BrickConfig { return brickMap.get(type); } - public function getTank(group:String, type:Int):TankConfig { + public function getTeam(id:String):TeamConfig { + return teamMap.get(id); + } + + public function getTank(group:String, type:String):TankConfig { return tankMap.get(group).get(type); } } diff --git a/src/common/haxe/ru/m/tankz/config/ConfigBundle.hx b/src/common/haxe/ru/m/tankz/config/ConfigBundle.hx index f8570b9..e37fba7 100644 --- a/src/common/haxe/ru/m/tankz/config/ConfigBundle.hx +++ b/src/common/haxe/ru/m/tankz/config/ConfigBundle.hx @@ -1,15 +1,17 @@ package ru.m.tankz.config; +import ru.m.tankz.game.ClassicGame; import yaml.Parser; import openfl.Assets; import yaml.Yaml; -import ru.m.tankz.proto.core.GameType; import ru.m.tankz.config.Config; typedef ConfigSource = { + var levels:Int; var map: MapConfig; var bricks: Array; + var teams: Array; var tanks: Dynamic>; } @@ -19,31 +21,16 @@ class ConfigBundle { return raw; } - public static function get(type:Int, level:Int):Config { - switch (type) { - case GameType.CLASSIC: - var source:ConfigSource = convert(Yaml.parse(Assets.getText('resources/config/config.yaml'), Parser.options().useObjects())); - var bricksData:String = Assets.getText('resources/levels/level00${level}.txt'); - var bricks:Array = []; - for (line in ~/\s+/g.split(bricksData)) { - for (c in line.split('')) { - if (c.length > 0) { - bricks.push(source.bricks[Std.parseInt(c) + 1]); - } - } - } - source.map.bricks = bricks; - var tanks:Array = []; - for (group in Reflect.fields(source.tanks)) { - var data:Array = Reflect.field(source.tanks, group); - for (item in data) { - item.group = group; - tanks.push(item); - } - } - return new Config(source.map, source.bricks, tanks); - case _: - return null; + public static function get(type:String):Config { + var source:ConfigSource = convert(Yaml.parse(Assets.getText('resources/${type}/config.yaml'), Parser.options().useObjects())); + var tanks:Array = []; + for (group in Reflect.fields(source.tanks)) { + var data:Array = Reflect.field(source.tanks, group); + for (item in data) { + item.group = group; + tanks.push(item); + } } + return new Config(type, source.levels, source.map, source.bricks, source.teams, tanks); } } diff --git a/src/common/haxe/ru/m/tankz/config/LevelBundle.hx b/src/common/haxe/ru/m/tankz/config/LevelBundle.hx new file mode 100644 index 0000000..dd4361f --- /dev/null +++ b/src/common/haxe/ru/m/tankz/config/LevelBundle.hx @@ -0,0 +1,28 @@ +package ru.m.tankz.config; + +import openfl.Assets; +import ru.m.tankz.game.Game; +import ru.m.tankz.config.Config; + + +class LevelBundle { + + private static function formatLevel(level:Int):String { + var result = Std.string(level); + while (result.length < 3) result = '0${result}'; + return result; + } + + public static function get(type:GameType, config:Config, level:Int):Array { + var bricksData:String = Assets.getText('resources/${type}/levels/level${formatLevel(level)}.txt'); + var bricks:Array = []; + for (line in ~/\s+/g.split(bricksData)) { + for (c in line.split('')) { + if (c.length > 0) { + bricks.push(config.bricks[Std.parseInt(c) + 1]); + } + } + } + return bricks; + } +} diff --git a/src/common/haxe/ru/m/tankz/control/Control.hx b/src/common/haxe/ru/m/tankz/control/Control.hx new file mode 100644 index 0000000..45abfe7 --- /dev/null +++ b/src/common/haxe/ru/m/tankz/control/Control.hx @@ -0,0 +1,61 @@ +package ru.m.tankz.control; + +import ru.m.tankz.core.Entity; +import ru.m.tankz.core.EntityType; +import ru.m.geom.Direction; + + +enum TankAction { + MOVE(direction:Direction); + LEVEL_UP(level:Int); + STOP; + SHOT; +} + + +typedef ControlType = String; + + +typedef ControlId = { + var type:ControlType; + var index:Int; +} + + +class Control { + public var id:ControlId; + public var tankId(default, default):Int; + private var handler:ControlHandler; + + public function new(id:ControlId) { + this.id = id; + } + + public function bind(handler:ControlHandler):Void { + this.handler = handler; + } + + public function action(action:TankAction):Void { + if (tankId != 0 && handler != null) { + handler.action(tankId, action); + } + } + + public function onCollision(with:EntityType):Void { + + } + + public function start():Void {} + + public function stop():Void {} + + public function dispose():Void { + stop(); + handler = null; + } +} + +interface ControlHandler { + public var entities(default, null):Map; + public function action(tankId:Int, action:TankAction):Void; +} diff --git a/src/common/haxe/ru/m/tankz/core/Bullet.hx b/src/common/haxe/ru/m/tankz/core/Bullet.hx index 186ea02..b0f68e2 100644 --- a/src/common/haxe/ru/m/tankz/core/Bullet.hx +++ b/src/common/haxe/ru/m/tankz/core/Bullet.hx @@ -1,20 +1,21 @@ package ru.m.tankz.core; +import ru.m.tankz.game.Game; import ru.m.tankz.config.Config; import ru.m.geom.Rectangle; import ru.m.geom.Direction; class Bullet extends MobileEntity { + public var playerId(default, null):PlayerId; public var tankId(default, null):Int; - public var tankConfig(default, null):TankConfig; public var config(default, null):BulletConfig; - public function new(tankId:Int, tankConfig:TankConfig, config:BulletConfig) { + public function new(tank:Tank) { + this.playerId = tank.playerId; + this.config = tank.config.bullet; super(new Rectangle(0, 0, config.width, config.height), config.speed, Direction.RIGHT); - this.tankId = tankId; - this.tankConfig = tankConfig; - this.config = config; + this.tankId = tank.id; this.layer = 2; } } diff --git a/src/common/haxe/ru/m/tankz/core/Eagle.hx b/src/common/haxe/ru/m/tankz/core/Eagle.hx new file mode 100644 index 0000000..16c6489 --- /dev/null +++ b/src/common/haxe/ru/m/tankz/core/Eagle.hx @@ -0,0 +1,15 @@ +package ru.m.tankz.core; + +import ru.m.tankz.game.Game; +import ru.m.geom.Rectangle; + + +class Eagle extends Entity { + + public var team(default, null):TeamId; + + public function new(team:TeamId) { + super(new Rectangle(0, 0, 44, 44)); + this.team = team; + } +} diff --git a/src/common/haxe/ru/m/tankz/core/EntityType.hx b/src/common/haxe/ru/m/tankz/core/EntityType.hx new file mode 100644 index 0000000..6b40605 --- /dev/null +++ b/src/common/haxe/ru/m/tankz/core/EntityType.hx @@ -0,0 +1,26 @@ +package ru.m.tankz.core; + +import Type.ValueType; +import ru.m.tankz.map.Grid.GridCell; + + +enum EntityType { + EAGLE(eagle:Eagle); + TANK(tank:Tank); + BULLET(bullet:Bullet); + CELL(cell:GridCell); +} + + +class EntityTypeResolver { + + public static function of(entity:Dynamic):EntityType { + return switch (Type.typeof(entity)) { + case ValueType.TClass(Eagle): EntityType.EAGLE(cast entity); + case ValueType.TClass(Tank): EntityType.TANK(cast entity); + case ValueType.TClass(Bullet): EntityType.BULLET(cast entity); + case ValueType.TClass(GridCell): EntityType.CELL(cast entity); + case x: null; + } + } +} \ No newline at end of file diff --git a/src/common/haxe/ru/m/tankz/core/MobileEntity.hx b/src/common/haxe/ru/m/tankz/core/MobileEntity.hx index 0d7f963..db470a2 100755 --- a/src/common/haxe/ru/m/tankz/core/MobileEntity.hx +++ b/src/common/haxe/ru/m/tankz/core/MobileEntity.hx @@ -10,24 +10,21 @@ class MobileEntity extends Entity { public var layer(default, null):Int; public var speed(default, null):Float; - public var direction(default, default):Direction; public function new(rect:Rectangle, speed:Float, direction:Direction) { super(rect); this.speed = speed; - this.direction = direction; this.layer = 0; this.mx = 0; this.my = 0; } public function move(direction:Direction):Void { - if (this.direction != direction) { + if (this.rect.direction != direction) { this.rect.direction = direction; - this.direction = direction; } - mx = direction.x * speed; - my = direction.y * speed; + mx = rect.direction.x * speed; + my = rect.direction.y * speed; } public function stop():Void { diff --git a/src/common/haxe/ru/m/tankz/core/Tank.hx b/src/common/haxe/ru/m/tankz/core/Tank.hx index ebefd36..8e9bc16 100755 --- a/src/common/haxe/ru/m/tankz/core/Tank.hx +++ b/src/common/haxe/ru/m/tankz/core/Tank.hx @@ -1,35 +1,30 @@ package ru.m.tankz.core; +import ru.m.tankz.game.Game; import ru.m.geom.Point; -import ru.m.tankz.config.Config.TankConfig; +import ru.m.tankz.config.Config; import ru.m.tankz.core.Bullet; import ru.m.geom.Rectangle; import ru.m.geom.Direction; -enum TankAction { - MOVE(direction:Direction); - LEVEL_UP(level:Int); - STOP; - SHOT; -} - class Tank extends MobileEntity { - public var index(default, null):Int; + public var playerId(default, null):PlayerId; public var config(default, set):TankConfig; private var bulletsCounter:Int = 0; - public function new(index:Int, config: TankConfig) { + public function new(playerId:PlayerId, config:TankConfig) { super(new Rectangle(0, 0, config.width, config.height), config.speed, Direction.RIGHT); - this.index = index; + this.playerId = playerId; this.config = config; this.layer = 1; } private function set_config(value:TankConfig):TankConfig { + var d = rect.direction; rect = new Rectangle(rect.x, rect.y, value.width, value.height); - rect.direction = direction; + rect.direction = d; speed = value.speed; config = value; return value; @@ -37,22 +32,13 @@ class Tank extends MobileEntity { public function shot():Null { if (bulletsCounter >= config.bullets) return null; - var bullet = new Bullet(id, config, config.bullet); - bullet.rect.center = rect.center.add(new Point(rect.width / 4 * direction.x, rect.height / 4 * direction.y)); - bullet.move(direction); + var bullet = new Bullet(this); + bullet.rect.center = rect.center.add(new Point(rect.width / 4 * rect.direction.x, rect.height / 4 * rect.direction.y)); + bullet.move(rect.direction); bulletsCounter++; return bullet; } - override public function move(direction:Direction):Void { - // ToDo: spike - /*if (direction != this.direction) { - rect.x -= this.direction.x * 4; - rect.y -= this.direction.y * 4; - }*/ - super.move(direction); - } - public function onDestroyBullet():Void { bulletsCounter--; } diff --git a/src/common/haxe/ru/m/tankz/engine/Engine.hx b/src/common/haxe/ru/m/tankz/engine/Engine.hx index 43063d7..ea449ae 100755 --- a/src/common/haxe/ru/m/tankz/engine/Engine.hx +++ b/src/common/haxe/ru/m/tankz/engine/Engine.hx @@ -1,191 +1,146 @@ package ru.m.tankz.engine; -import haxe.macro.Type.Ref; -import haxe.Constraints.Function; -import ru.m.geom.Rectangle; import ru.m.geom.Line; -import ru.m.tankz.core.MobileEntity; -import ru.m.tankz.core.Entity; -import ru.m.geom.Direction; -import ru.m.tankz.config.Config.TankConfig; -import ru.m.geom.Point; -import ru.m.tankz.core.Bullet; -import ru.m.tankz.proto.game.GameObjectType; -import ru.m.tankz.proto.game.GameChangeType; -import ru.m.tankz.proto.game.GameChange; -import ru.m.tankz.proto.core.Player; import ru.m.tankz.config.Config; +import ru.m.tankz.control.Control; +import ru.m.tankz.core.Bullet; +import ru.m.tankz.core.Entity; +import ru.m.tankz.core.EntityType; +import ru.m.tankz.core.MobileEntity; import ru.m.tankz.core.Tank; import ru.m.tankz.map.LevelMap; -class Engine { + +interface EngineListener { + public function onSpawn(entity:EntityType):Void; + public function onCollision(entity:EntityType, with:EntityType):Void; + public function onDestroy(entity:EntityType):Void; +} + + +class CollisionProcessor implements EngineListener { + + private var engine:Engine; + + public function new(engine:Engine) { + this.engine = engine; + } + + public function onSpawn(entity:EntityType):Void {} + + private function checkTankBullet(tank:Tank, bullet:Bullet):Bool { + return tank.playerId.team != bullet.playerId.team; + } + + public function onCollision(entity:EntityType, with:EntityType):Void { + switch (entity) { + case EntityType.TANK(tank1): + switch (with) { + case EntityType.TANK(tank2): + tank1.rect.lean(tank2.rect); + case EntityType.BULLET(bullet2): + if (checkTankBullet(tank1, bullet2)) { + engine.destroy(tank1); + engine.destroy(bullet2); + } + case EntityType.EAGLE(eagle): + tank1.rect.lean(eagle.rect); + case EntityType.CELL(cell): + tank1.rect.lean(cell.rect); + } + case EntityType.BULLET(bullet1): + switch (with) { + case EntityType.TANK(tank2): + if (checkTankBullet(tank2, bullet1)) { + engine.destroy(bullet1); + engine.destroy(tank2); + } + case EntityType.BULLET(bullet2): + engine.destroy(bullet1); + engine.destroy(bullet2); + case EntityType.EAGLE(eagle): + engine.destroy(bullet1); + engine.destroy(eagle); + case EntityType.CELL(cell): + engine.destroy(bullet1); + } + case _: + } + } + + public function onDestroy(entity:EntityType):Void { } +} + + +class Engine implements ControlHandler { public var config(default, default):Config; public var map(default, null):LevelMap; public var entities(default, null):Map; - public var removedEntities(default, null):Array; + public var listeners(default, null):Array; + + private var collision:CollisionProcessor; - private var playerTanks(default, null):Map; private var time:Float; - public function new() {} - - public function clear():Void { - playerTanks = new Map(); - } - - private function buildTank(index:Int, config:TankConfig, point:SpawnPoint):Tank { - var tank = new Tank(index, config); - tank.rect.center = new Point((point.x + 1) * map.cellWidth, (point.y + 1) * map.cellHeight); - tank.direction = Direction.fromString(point.direction); - return tank; - } - - public function init(config:Config):Void { + public function new(config:Config) { this.config = config; + listeners = []; map = new LevelMap(config.map); - playerTanks = new Map(); entities = new Map(); - removedEntities = new Array(); time = Date.now().getTime(); + collision = new CollisionProcessor(this); + listeners.push(collision); } - public function initTanks(players:Array):Array { - var changes = new Array(); - for (index in 0...players.length) { - var player:Player = players[index]; - var point:SpawnPoint = config.getSpawnPoint(EntityType.PLAYER, index); - var tank = buildTank(index, config.getTank('player', 0), point); - playerTanks.set(player.id, tank); - entities.set(tank.id, tank); - changes.push(new GameChange() - .setType(GameChangeType.APPEND) - .setObjectType(GameObjectType.TANK) - .setObjectId(tank.id) - .setX(tank.rect.x) - .setY(tank.rect.y) - .setDirectionX(tank.direction.x) - .setDirectionY(tank.direction.y) - ); - } - - for (index in 1...4) { - var point:SpawnPoint = config.getSpawnPoint(EntityType.BOT, index); - var tank = buildTank(0, config.getTank('bot', 0), point); - entities.set(tank.id, tank); - } - return changes; + public function spawn(entity:Entity):Void { + entities.set(entity.id, entity); + var type = EntityTypeResolver.of(entity); + for (l in listeners) l.onSpawn(type); } - public function action(playerId:Int, action:TankAction):Void { - var tank:Tank = playerTanks.get(playerId); + public function destroy(entity:Entity):Void { + if (entities.exists(entity.id)) { + var type = EntityTypeResolver.of(entity); + switch (type) { + case EntityType.BULLET(bullet): + var tank:Tank = cast entities.get(bullet.tankId); + if (tank != null) tank.onDestroyBullet(); + case _: + } + for (l in listeners) l.onDestroy(type); + entities.remove(entity.id); + } + } + + public function action(tankId:Int, action:TankAction):Void { + if (!entities.exists(tankId)) return; + var tank:Tank = cast entities.get(tankId); switch (action) { case TankAction.MOVE(direction): tank.move(direction); - /*Provider.get(IConnection).send( - Map GameActionRequest() - .setType(GameActionType.MOVE) - .setDirectionX(direction.x) - .setDirectionY(direction.y) - );*/ case TankAction.LEVEL_UP(level): - tank.config = config.getTank('player', Std.int(Math.min(tank.config.type + level, 3))); + // ToDo: + tank.config = config.getTank('human', Std.string(Std.int(Math.min(Std.parseInt(tank.config.type) + level, 3)))); case TankAction.STOP: tank.stop(); - /*Provider.get(IConnection).send( - Map GameActionRequest() - .setType(GameActionType.STOP) - );*/ case TankAction.SHOT: var bullet = tank.shot(); if (bullet != null) { - entities.set(bullet.id, bullet); + spawn(bullet); } - /*Provider.get(IConnection).send( - Map GameActionRequest() - .setType(GameActionType.SHOT) - );*/ } } - /*public function updateFromChanges(changes:Array):Void { - for (change in changes) { - switch (change.type) { - case GameChangeType.APPEND: - switch (change.objectType) { - case GameObjectType.TANK: - var tank:Tank = buildTank(change.personId, change.objectId, change.x, change.y, Direction.from(change.directionX, change.directionY)); - mobileEntities.set(tank.id, tank); - tanks.set(tank.personId, tank); - case GameObjectType.BULLET: - var bullet:Bullet = Map Bullet(change.personId, change.objectId, change.x, change.y, 0, Direction.from(change.directionX, change.directionY)); - mobileEntities.set(bullet.id, bullet); - } - case GameChangeType.DESTROED: - mobileEntities.remove(change.objectId); - case GameChangeType.DIRECTION: - var target = mobileEntities.get(change.objectId); - target.direction = Direction.from(change.directionX, change.directionY); - case GameChangeType.MOVED: - var target = mobileEntities.get(change.objectId); - target.x = change.x; - target.y = change.y; - case GameChangeType.MODIFIED: - - } - } - }*/ - - private function collisionTankTank(a:Tank, b:Tank):Bool { - a.rect.lean(b.rect); - return true; - } - - private function collisionBulletTank(a:Bullet, b:Tank):Bool { - if (a.tankConfig.group != b.config.group) { - removeEntity(a); - removeEntity(b); - return true; - } - return false; - } - - private function collisionTankBullet(a:Tank, b:Bullet):Bool { - if (a.config.group != b.tankConfig.group) { - removeEntity(a); - removeEntity(b); - return true; - } - return false; - } - - private function collisionBulletBullet(a:Bullet, b:Bullet):Bool { - removeEntity(a); - removeEntity(b); - return true; - } - - private function removeEntity(entity:Entity):Void { - if (entities.exists(entity.id)) { - entities.remove(entity.id); - removedEntities.push(entity.key); - if (Std.is(entity, Bullet)) { - var tank:Tank = cast entities.get(cast(entity, Bullet).tankId); - tank.onDestroyBullet(); - } - } - } - - public function update():Array { + public function update():Void { var newTime:Float = Date.now().getTime(); var d:Float = newTime - time; time = newTime; - var changes = new Array(); - for (ent in entities) if (Std.is(ent, MobileEntity)) { + var entityType:EntityType = EntityTypeResolver.of(ent); var entity:MobileEntity = cast ent; /*if (Std.is(entity, Tank)) { @@ -209,19 +164,17 @@ class Engine { if (cell.layer >= entity.layer && cell.layer < 3) { entity.rect.lean(cell.rect); collision = true; + var with = EntityTypeResolver.of(cell); + for (l in listeners) l.onCollision(entityType, with); break; } } for (other in entities.iterator()) { - if (other != ent) { + if (other != ent && other != null) { if (other.rect.intersection2(side)) { - //if (ent.rect.intersection(other.rect)) { - var funName = 'collision${ent.type}${other.type}'; - var fun:Function = Reflect.field(this, funName); - if (fun != null) { - collision = Reflect.callMethod(this, fun, [ent, other]) || collision; - } + var with = EntityTypeResolver.of(other); + for (l in listeners) l.onCollision(entityType, with); } } } @@ -240,19 +193,14 @@ class Engine { } } } - removeEntity(entity); } } - - /*changes.push(new GameChange() - .setType(GameChangeType.MOVED) - .setObjectType(objectType) - .setObjectId(entity.id) - .setX(entity.rect.x) - .setY(entity.rect.y) - );*/ } } - return changes; + } + + public function dispose():Void { + listeners = []; + entities = new Map(); } } diff --git a/src/common/haxe/ru/m/tankz/game/ClassicGame.hx b/src/common/haxe/ru/m/tankz/game/ClassicGame.hx new file mode 100644 index 0000000..976df36 --- /dev/null +++ b/src/common/haxe/ru/m/tankz/game/ClassicGame.hx @@ -0,0 +1,72 @@ +package ru.m.tankz.game; + +import haxe.ds.Option; +import ru.m.tankz.game.GameState.PlayerState; +import ru.m.tankz.game.Game; + + +class ClassicGame extends Game { + + public static var TYPE(default, never):GameType = 'classic'; + + public static var HUMAN(default, never):TeamId = 'human'; + public static var BOT(default, never):TeamId = 'bot'; + + private static var HUMAN_LIFE(default, never):Int = 3; + private static var BOT_LIFE(default, never):Int = 20; + + public function new() { + super(TYPE); + } + + public static function buildState(level:Int, humans:Int):GameState { + var state = new GameState(); + state.type = TYPE; + state.level = level; + state.teams[HUMAN] = {life: -1, players: new Map(), lose: false}; + state.teams[BOT] = {life: BOT_LIFE, players: new Map(), lose: false}; + for (i in 0...humans) { + state.teams[HUMAN].players[i] = { + index:i, + tank:{ + group: HUMAN, + type: '1' + }, + control:{ + type: 'human', + index: i + }, + life:HUMAN_LIFE, + }; + } + for (i in 0...humans*2+2) { + state.teams[BOT].players[i] = { + index:i, + tank:{ + group: BOT, + type: '1' + }, + control:{ + type: 'bot', + index: i + }, + life:-1, + }; + } + return state; + } + + override public function next():Option { + if (!state.teams[HUMAN].lose) { + state.level++; + if (state.level >= config.levels) state.level = 0; + state.teams[BOT].lose = false; + state.teams[BOT].life = BOT_LIFE; + for (ps in state.teams[HUMAN].players) { + if (ps.life > 0) ps.life++; + } + return Option.Some(state); + } + return Option.None; + } +} diff --git a/src/common/haxe/ru/m/tankz/game/DotaGame.hx b/src/common/haxe/ru/m/tankz/game/DotaGame.hx new file mode 100644 index 0000000..723163b --- /dev/null +++ b/src/common/haxe/ru/m/tankz/game/DotaGame.hx @@ -0,0 +1,56 @@ +package ru.m.tankz.game; + +import ru.m.tankz.game.Game; +import ru.m.tankz.game.GameState; + + +class DotaGame extends Game { + + public static var TYPE(default, never):GameType = 'dota'; + + public static var RADIANT(default, never):TeamId = 'radiant'; + public static var DIRE(default, never):TeamId = 'dire'; + + private static var TEAM_SIZE(default, never):Int = 5; + + public function new() { + super(TYPE); + } + + public static function buildState(level:Int, humans:Int):GameState { + var state = new GameState(); + state.type = TYPE; + state.level = level; + state.teams[RADIANT] = {life: 20, players: new Map(), lose: false}; + state.teams[DIRE] = {life: 20, players: new Map(), lose: false}; + for (i in 0...TEAM_SIZE) { + state.teams[RADIANT].players[i] = { + index:i, + tank:{ + group: RADIANT, + type: '1' + }, + control:{ + type: 'bot', + index: i + }, + life:-1, + }; + } + for (i in 0...TEAM_SIZE) { + state.teams[DIRE].players[i] = { + index:i, + tank:{ + group: DIRE, + type: '1' + }, + control:{ + type: 'bot', + index: i + }, + life:-1, + }; + } + return state; + } +} diff --git a/src/common/haxe/ru/m/tankz/game/Game.hx b/src/common/haxe/ru/m/tankz/game/Game.hx new file mode 100644 index 0000000..07c0684 --- /dev/null +++ b/src/common/haxe/ru/m/tankz/game/Game.hx @@ -0,0 +1,242 @@ +package ru.m.tankz.game; + +import haxe.ds.Option; +import haxe.Timer; +import promhx.Deferred; +import promhx.Stream; +import ru.m.geom.Direction; +import ru.m.geom.Point; +import ru.m.tankz.bot.BotControl; +import ru.m.tankz.config.Config; +import ru.m.tankz.config.ConfigBundle; +import ru.m.tankz.config.LevelBundle; +import ru.m.tankz.control.Control; +import ru.m.tankz.control.HumanControl; +import ru.m.tankz.core.Eagle; +import ru.m.tankz.core.Entity; +import ru.m.tankz.core.EntityType; +import ru.m.tankz.core.Tank; +import ru.m.tankz.engine.Engine; +import ru.m.tankz.game.GameState; +import ru.m.tankz.game.Spawner; + + +typedef GameType = String; + +typedef TeamId = String; + +typedef PlayerId = { + var team:TeamId; + var index:Int; +} + + +class Game implements EngineListener { + + public var type(default, null):GameType; + public var state(default, null):GameState; + public var teams(default, null):Map; + public var config(default, null):Config; + public var engine(default, null):Engine; + + private var spawners:Map; + private var deferred:Deferred; + private var stream:Stream; + + public function new(type:GameType) { + this.type = type; + this.config = ConfigBundle.get(type); + this.engine = new Engine(config); + engine.listeners.push(this); + } + + public function getPlayer(playerId:PlayerId):Player { + return teams.get(playerId.team).players[playerId.index]; + } + + private function buildTank(playerId:PlayerId, config:TankConfig, point:SpawnPoint):Tank { + var tank = new Tank(playerId, config); + applyPoint(tank, point); + return tank; + } + + private function applyPoint(entity:Entity, point:SpawnPoint):Void { + entity.rect.center = new Point((point.x + 1) * engine.map.cellWidth, (point.y + 1) * engine.map.cellHeight); + entity.rect.direction = Direction.fromString(point.direction); + } + + public function start(state:GameState):Stream { + this.deferred = new Deferred(); + this.state = state; + var bricks = LevelBundle.get(type, config, state.level); + engine.map.setData(bricks); + teams = new Map(); + spawners = new Map(); + for (teamConfig in config.teams) { + var team = new Team(teamConfig); + for (playerState in state.teams.get(team.id).players) { + var player = new Player({team:team.id, index:playerState.index}); + team.players.push(player); + teams.set(team.id, team); + if (playerState.control != null) { + var control = switch (playerState.control.type) { + case HumanControl.TYPE: new HumanControl(playerState.control.index); + case BotControl.TYPE: new BotControl(playerState.control.index); + case 'none': null; + case _: throw 'Unsupported control type: "${playerState.control.type}"'; + } + if (control != null) { + player.control = control; + player.control.bind(engine); + } + } + } + spawners.set(team.id, new Spawner(team.config, spawn)); + } + + for (team in teams) { + for (player in team.players) { + if (trySpawn(player.id)) { + spawners.get(team.id).push(player.id); + } + } + var eaglePoint = spawners.get(team.id).getPoint('eagle'); + if (eaglePoint != null) { + var eagle = new Eagle(team.id); + applyPoint(eagle, eaglePoint); + engine.spawn(eagle); + } + } + + return stream = deferred.stream(); + } + + private function spawn(task:SpawnTask):Void { + getPlayer(task.playerId).tankId = 0; + if (trySpawn(task.playerId, true)) { + var tank = buildTank(task.playerId, config.getTank(task.playerId.team, '0'), task.point); + var player:Player = getPlayer(task.playerId); + engine.spawn(tank); + player.tankId = tank.id; + } else if (!isTeamAlive(task.playerId.team)) { + state.teams[task.playerId.team].lose = true; + complete(); + } + deferred.resolve(state); + } + + private function complete():Void { + for (team in teams.iterator()) { + for (player in team.players) { + player.control.action(TankAction.STOP); + player.control.dispose(); + } + } + var timer = new Timer(5000); + timer.run = function() { + timer.stop(); + deferred.resolve(state); + stream.end(); + } + } + + public function setControl(playerId:PlayerId, control:Control):Void { + for (team in teams.iterator()) { + if (team.id == playerId.team) { + var player = team.players[playerId.index]; + if (player.control != null) { + player.control.dispose(); + } + player.control = control; + player.control.bind(engine); + break; + } + } + } + + + public function onSpawn(entity:EntityType):Void { + + } + + public function onCollision(entity:EntityType, with:EntityType):Void { + switch (entity) { + case EntityType.TANK(tank): + var control = getPlayer(tank.playerId).control; + if (control != null) control.onCollision(with); + case x: + } + } + + private function isTeamAlive(team:TeamId):Bool { + var ts:TeamState = state.teams[team]; + var life:Int = Lambda.fold(ts.players, function(ps, t) return t + ps.life, ts.life); + if (life > 0) { + return true; + } else { + for (player in teams[team].players) { + if (player.tankId > 0) { + return true; + } + } + if (spawners[team].active) { + return true; + } + } + return false; + } + + private function trySpawn(player:PlayerId, spawn:Bool = false):Bool { + var ts:TeamState = state.teams[player.team]; + var ps:PlayerState = ts.players[player.index]; + var result = false; + if (ps.life > -1) { + if (ps.life > 0) { + if (spawn) ps.life--; + result = true; + } + } else if (ts.life > -1) { + if (ts.life > 0) { + if (spawn) ts.life--; + result = true; + } + } + return result; + } + + public function onDestroy(entity:EntityType):Void { + switch (entity) { + case EntityType.TANK(tank): + getPlayer(tank.playerId).tankId = 0; + var respawn:Bool = trySpawn(tank.playerId); + if (respawn) { + spawners.get(tank.playerId.team).push(tank.playerId); + } + if (!isTeamAlive(tank.playerId.team)) { + state.teams[tank.playerId.team].lose = true; + complete(); + } + deferred.resolve(state); + case EntityType.EAGLE(eagle): + state.teams[eagle.team].lose = true; + complete(); + deferred.resolve(state); + case x: + } + } + + + public function onAction(tankId:Int, action:TankAction):Void { + engine.action(tankId, action); + } + + + public function next():Option { + return Option.None; + } + + + public function dispose():Void { + engine.dispose(); + } +} diff --git a/src/common/haxe/ru/m/tankz/game/GameState.hx b/src/common/haxe/ru/m/tankz/game/GameState.hx new file mode 100644 index 0000000..ae192e7 --- /dev/null +++ b/src/common/haxe/ru/m/tankz/game/GameState.hx @@ -0,0 +1,41 @@ +package ru.m.tankz.game; + +import ru.m.tankz.game.Game; +import ru.m.tankz.config.Config; + + +typedef ControlType = String; + + +typedef ControId = { + var type:ControlType; + var index:Int; +} + + +typedef PlayerState = { + var index:Int; + var tank:TankType; + var life:Int; + var control:ControId; +} + + +typedef TeamState = { + var players:Map; + var life:Int; + var lose:Bool; +} + + +class GameState { + public var type:GameType; + public var level:Int; + public var teams:Map; + + public function new() { + type = null; + level = -1; + teams = new Map(); + } +} diff --git a/src/common/haxe/ru/m/tankz/game/Player.hx b/src/common/haxe/ru/m/tankz/game/Player.hx new file mode 100644 index 0000000..af78f4e --- /dev/null +++ b/src/common/haxe/ru/m/tankz/game/Player.hx @@ -0,0 +1,42 @@ +package ru.m.tankz.game; + +import ru.m.tankz.control.Control; +import ru.m.tankz.game.Game; + + +class Player { + + public var id(default, null):PlayerId; + public var tankId(default, set):Int; + public var control(default, set):Control; + + public function new(id:PlayerId, control:Control=null) { + this.id = id; + this.control = control; + } + + public function set_tankId(value:Int):Int { + tankId = value; + if (control != null) { + control.tankId = tankId; + if (tankId != 0) { + control.start(); + } else { + control.stop(); + } + } + return tankId; + } + + public function set_control(value:Control):Control { + if (control != null) control.dispose(); + control = value; + if (control != null) { + control.tankId = tankId; + if (tankId != 0) { + control.start(); + } + } + return control; + } +} diff --git a/src/common/haxe/ru/m/tankz/game/Spawner.hx b/src/common/haxe/ru/m/tankz/game/Spawner.hx new file mode 100644 index 0000000..3d41b87 --- /dev/null +++ b/src/common/haxe/ru/m/tankz/game/Spawner.hx @@ -0,0 +1,89 @@ +package ru.m.tankz.game; + + +import haxe.Timer; +import ru.m.tankz.game.Game; +import ru.m.tankz.config.Config; + + +typedef SpawnTask = { + var point:SpawnPoint; + var playerId:PlayerId; +} + + +class Spawner { + + public var active(get, never):Bool; + + private var config:TeamConfig; + private var runner:SpawnTask -> Void; + private var queue:Array; + private var timer:Timer; + + private var indexedPoints:Map; + private var anyPoints:Array; + private var index:Int; + + public function new(config:TeamConfig, runner:SpawnTask -> Void) { + this.config = config; + this.runner = runner; + queue = []; + indexedPoints = new Map(); + anyPoints = []; + for (point in config.points) { + if (point.type == 'tank') { + if (point.index > -1) { + indexedPoints.set(point.index, point); + } else { + anyPoints.push(point); + } + } + } + } + + public function getPoint(type:String, index:Int=-1):Null { + for (point in config.points) { + if (point.type == type && point.index == index) { + return point; + } + } + return null; + } + + public function push(playerId:PlayerId):Void { + var point:SpawnPoint = null; + if (indexedPoints.exists(playerId.index)) { + point = indexedPoints.get(playerId.index); + } else { + point = anyPoints[index++]; + if (index >= anyPoints.length) index = 0; + } + if (point != null) { + queue.push({playerId:playerId, point:point}); + run(); + } + } + + private function run():Void { + if (timer == null) { + timer = new Timer(config.spawnInterval); + timer.run = spawn; + } + } + + private function spawn():Void { + if (queue.length == 0) { + if (timer != null) { + timer.stop(); + timer = null; + } + } else { + runner(queue.shift()); + } + } + + private function get_active():Bool { + return queue.length > 0; + } +} \ No newline at end of file diff --git a/src/common/haxe/ru/m/tankz/game/Team.hx b/src/common/haxe/ru/m/tankz/game/Team.hx new file mode 100644 index 0000000..49bf57c --- /dev/null +++ b/src/common/haxe/ru/m/tankz/game/Team.hx @@ -0,0 +1,21 @@ +package ru.m.tankz.game; + +import ru.m.tankz.config.Config.TeamConfig; +import ru.m.tankz.game.Player; +import ru.m.tankz.game.Game; + + +class Team { + + public var id(default, null):TeamId; + public var config(default, null):TeamConfig; + public var players(default, null):Array; + + private static var i:Int = 0; + + public function new(config:TeamConfig) { + this.id = config.id; + this.config = config; + this.players = []; + } +} diff --git a/src/common/haxe/ru/m/tankz/map/Brick.hx b/src/common/haxe/ru/m/tankz/map/Brick.hx index 094e784..d94fcd5 100644 --- a/src/common/haxe/ru/m/tankz/map/Brick.hx +++ b/src/common/haxe/ru/m/tankz/map/Brick.hx @@ -24,6 +24,7 @@ class Brick implements IKey { public var rect(default, null):Rectangle; public var cells(default, null):HashMap; + public var broken(get, null):Int; public var destroyed(get, set):Bool; public function new(mapConfig:MapConfig, config:BrickConfig, cellX:Int, cellY:Int, cells:HashMap) { @@ -40,6 +41,16 @@ class Brick implements IKey { ); } + public function get_broken():Int { + var i:Int = 0; + for (c in cells.iterator()) { + if (c.destroyed) { + i++; + } + } + return i; + } + public function get_destroyed():Bool { var i = 0; var result:Bool = false; diff --git a/src/common/haxe/ru/m/tankz/map/LevelMap.hx b/src/common/haxe/ru/m/tankz/map/LevelMap.hx index 554353f..b91d7fe 100755 --- a/src/common/haxe/ru/m/tankz/map/LevelMap.hx +++ b/src/common/haxe/ru/m/tankz/map/LevelMap.hx @@ -28,15 +28,20 @@ class LevelMap { gridWidth = config.gridWidth; gridHeight = config.gridHeight; bricksMap = new HashMap(); + bricks = []; grid = new Grid( Std.int(cellWidth / 2), Std.int(cellHeight / 2), Std.int(cellWidth * gridWidth), Std.int(cellHeight * gridHeight) ); - bricks = Lambda.array(Lambda.mapi(config.bricks, function(i:Int, brickConfig:BrickConfig):Brick { + } + + public function setData(data:Array):Void { + bricksMap = new HashMap(); + bricks = Lambda.array(Lambda.mapi(data, function(i:Int, brickConfig:BrickConfig):Brick { var cellX = Std.int(i % gridWidth); - var cellY = Std.int(Math.floor(i / gridHeight)); + var cellY = Std.int(Math.floor(i / gridWidth)); var cells:HashMap = new HashMap(); var point:Point = new Point(cellX * 2, cellY * 2); if (brickConfig.layer > 0 || brickConfig.armor > 0) { diff --git a/tasks/debug.js b/tasks/debug.js index fe04b98..ddb80c4 100755 --- a/tasks/debug.js +++ b/tasks/debug.js @@ -44,4 +44,4 @@ module.exports = () => { }) }; -module.exports.log = log; \ No newline at end of file +module.exports.log = _log; diff --git a/tasks/haxe.js b/tasks/haxe.js index 3c889c7..c7cbd4b 100755 --- a/tasks/haxe.js +++ b/tasks/haxe.js @@ -76,6 +76,10 @@ class Haxe extends Sdk { return this.haxelib(args); }; + if (!Array.isArray(packages)) { + packages = Object.entries(packages).map(([k, v]) => ({name: k, version: v})); + } + for (let pack of packages) { const args = []; let version = null; @@ -83,6 +87,10 @@ class Haxe extends Sdk { args.push('install', pack); } else if (typeof pack === 'object') { version = pack.version; + if (version.substr(0, 3) === 'git') { + pack.git = version; + version = null; + } if (pack.git) { args.push('git', pack.name, pack.git); if (pack.branch) args.push(pack.branch); @@ -93,7 +101,11 @@ class Haxe extends Sdk { args.push('--always'); } let path = `${this.path}/lib/${args[1]}`; - if (version) path += `/${version.replace(/\./g, ',')}`; + if (version) { + path += `/${version.replace(/\./g, ',')}`; + } else if (pack.git) { + path += '/git'; + } if (!fs.existsSync(path)) { promise = promise.then(next(args)); } @@ -171,6 +183,7 @@ class Haxe extends Sdk { const result = { 'flash': `${buildDir}/flash/bin/*.swf`, 'html5': `${buildDir}/html5/bin/**/*`, + 'linux': `${buildDir}/linux/bin/**/*`, }[params.platform]; vfs.src(result).pipe(through.obj((file, enc, cb) => { file.debug = debug; diff --git a/tasks/tail.js b/tasks/tail.js new file mode 100644 index 0000000..b4ddeed --- /dev/null +++ b/tasks/tail.js @@ -0,0 +1,43 @@ +const through = require('through2'); +const colors = require('ansi-colors'); +const log = require('fancy-log'); + +const TAG = colors.green('[tail]'); + + +const { Writable } = require('stream'); +const { StringDecoder } = require('string_decoder'); + + +class StringWritable extends Writable { + constructor(handler, options) { + super(options); + this.handler = handler; + const state = this._writableState; + this._decoder = new StringDecoder(state.defaultEncoding); + this.data = ''; + } + _write(chunk, encoding, callback) { + if (encoding === 'buffer') { + chunk = this._decoder.write(chunk); + for (const line of chunk.split('\n')) if (line.length) { + this.handler(line); + } + } + this.data += chunk; + callback(); + } + _final(callback) { + this.data += this._decoder.end(); + callback(); + } +} + + +module.exports = (handler) => { + return through.obj(function (file, enc, callback) { + file.contents.pipe(new StringWritable(handler)); + this.push(file); + callback(); + }); +};