diff --git a/TODO.md b/TODO.md index 2f5a10e..0429c7f 100644 --- a/TODO.md +++ b/TODO.md @@ -77,8 +77,8 @@ Ships models and equipments Artificial Intelligence ----------------------- +* Work on a simple representation of battle state, simulating effects on it, evaluating it, and only reevaluating parts that changed * Use a first batch of producers, and only if no "good" move has been found, go on with some infinite producers -* Evaluate buffs/debuffs * Abandon fight if the AI judges there is no hope of victory * Add combination of random small move and actual maneuver, as producer * New duel page with producers/evaluators tweaking diff --git a/graphics/exported/battle/effects/laser.png b/graphics/exported/battle/effects/laser.png new file mode 100644 index 0000000..8632674 Binary files /dev/null and b/graphics/exported/battle/effects/laser.png differ diff --git a/graphics/ui/weaponeffects.svg b/graphics/ui/weaponeffects.svg index 07e3fad..073bdc4 100644 --- a/graphics/ui/weaponeffects.svg +++ b/graphics/ui/weaponeffects.svg @@ -15,7 +15,7 @@ viewBox="0 0 128 128" version="1.1" id="svg8" - inkscape:version="0.92.0 r15299" + inkscape:version="0.92.1 r15371" sodipodi:docname="weaponeffects.svg" inkscape:export-filename="/tmp/image.png" inkscape:export-xdpi="96.000008" @@ -198,6 +198,19 @@ r="30.446428" gradientUnits="userSpaceOnUse" gradientTransform="matrix(1.0997067,0,0,1.0997067,-4.1425418,-8.3253595)" /> + + + + inkscape:label="Stasis" + style="display:none"> + + + diff --git a/out/assets/images/battle/arena/blast.png b/out/assets/images/battle/arena/blast.png deleted file mode 100644 index e3dc23d..0000000 Binary files a/out/assets/images/battle/arena/blast.png and /dev/null differ diff --git a/package.json b/package.json index 2373010..7b5ee2f 100644 --- a/package.json +++ b/package.json @@ -36,7 +36,7 @@ "typescript": "^2.5.3" }, "dependencies": { - "jasmine-core": "2.8.0", + "jasmine-core": "2.5.2", "parse": "^1.9.2", "phaser": "^2.6.2", "phaser-plugin-scene-graph": "^1.0.4" diff --git a/src/core/ArenaLocation.spec.ts b/src/core/ArenaLocation.spec.ts index 9faebe9..994b0bf 100644 --- a/src/core/ArenaLocation.spec.ts +++ b/src/core/ArenaLocation.spec.ts @@ -11,5 +11,10 @@ module TK.SpaceTac.Specs { expect(angularDistance(0.5, -0.5)).toBe(-1.0); expect(angularDistance(0.5, -0.3 - Math.PI * 4)).toBeCloseTo(-0.8, 0.000001); }) + + it("converts between degrees and radians", function () { + expect(degrees(Math.PI / 2)).toBeCloseTo(90, 0.000001); + expect(radians(45)).toBeCloseTo(Math.PI / 4, 0.000001); + }); }); } diff --git a/src/core/ArenaLocation.ts b/src/core/ArenaLocation.ts index 51ae854..9e0e5ee 100644 --- a/src/core/ArenaLocation.ts +++ b/src/core/ArenaLocation.ts @@ -81,4 +81,18 @@ module TK.SpaceTac { let dist = arenaDistance(loc1, loc2); return border_inclusive ? (dist <= loc2.radius) : (dist < loc2.radius); } + + /** + * Convert radians angle to degrees + */ + export function degrees(angle: number): number { + return angle * 180 / Math.PI; + } + + /** + * Convert degrees angle to radians + */ + export function radians(angle: number): number { + return angle * Math.PI / 180; + } } diff --git a/src/ui/Preload.ts b/src/ui/Preload.ts index af2dad2..865a907 100644 --- a/src/ui/Preload.ts +++ b/src/ui/Preload.ts @@ -36,7 +36,6 @@ module TK.SpaceTac.UI { this.loadImage("battle/actionbar/actions-background.png"); this.loadSheet("battle/actionbar/button-menu.png", 79, 132); this.loadImage("battle/arena/background.png"); - this.loadImage("battle/arena/blast.png"); this.loadImage("battle/shiplist/background.png"); this.loadImage("battle/shiplist/item-background.png"); this.loadImage("battle/shiplist/damage.png"); diff --git a/src/ui/battle/Arena.ts b/src/ui/battle/Arena.ts index ffeda8e..87b0236 100644 --- a/src/ui/battle/Arena.ts +++ b/src/ui/battle/Arena.ts @@ -141,17 +141,6 @@ module TK.SpaceTac.UI { }); } - /** - * Get all ship sprites in a circle area - */ - getShipsInCircle(area: ArenaCircleArea, alive_only = true, border_inclusive = true): ArenaShip[] { - let base = this.ship_sprites; - if (alive_only) { - base = base.filter(ship => !ship.isDead()); - } - return base.filter(ship => arenaInside(ship, area, border_inclusive)); - } - /** * Get the current MainUI instance */ diff --git a/src/ui/battle/ArenaShip.ts b/src/ui/battle/ArenaShip.ts index 17fd140..5a264ee 100644 --- a/src/ui/battle/ArenaShip.ts +++ b/src/ui/battle/ArenaShip.ts @@ -133,6 +133,10 @@ module TK.SpaceTac.UI { this.battleview.log_processor.registerForShip(ship, event => this.processShipLogEvent(event)); } + jasmineToString(): string { + return `ArenaShip ${this.ship.jasmineToString()}`; + } + /** * Process a battle log event */ diff --git a/src/ui/battle/Targetting.ts b/src/ui/battle/Targetting.ts index 0104dbf..ac241f4 100644 --- a/src/ui/battle/Targetting.ts +++ b/src/ui/battle/Targetting.ts @@ -161,8 +161,8 @@ module TK.SpaceTac.UI { } if (radius) { - area.lineStyle(2, 0x90481e, 0.6); - area.beginFill(0x90481e, 0.2); + area.lineStyle(2, color, 0.6); + area.beginFill(color, 0.2); if (angle) { area.arc(0, 0, radius, angle, -angle, true); } else { diff --git a/src/ui/battle/WeaponEffect.spec.ts b/src/ui/battle/WeaponEffect.spec.ts index 74f52cf..ca7a9d2 100644 --- a/src/ui/battle/WeaponEffect.spec.ts +++ b/src/ui/battle/WeaponEffect.spec.ts @@ -33,31 +33,45 @@ module TK.SpaceTac.UI.Specs { let battleview = testgame.battleview; battleview.timer = new Timer(); + let ship = nn(battleview.battle.playing_ship); + let effect = new WeaponEffect(battleview.arena, new Ship(), Target.newFromShip(ship), new Equipment()); + effect.gunEffect(); + + let layer = battleview.arena.layer_weapon_effects; + expect(layer.children.length).toBe(1); + expect(layer.children[0] instanceof Phaser.Particles.Arcade.Emitter).toBe(true); + }); + + it("displays shield and hull effect on impacted ships", function () { + let battleview = testgame.battleview; + battleview.timer = new Timer(); + let ship = nn(battleview.battle.playing_ship); let sprite = nn(battleview.arena.findShipSprite(ship)); ship.setArenaPosition(50, 30); sprite.position.set(50, 30); sprite.hull_bar.setValue(10, 10); sprite.shield_bar.setValue(0, 10); - let effect = new WeaponEffect(battleview.arena, new Ship(), Target.newFromShip(ship), new Equipment()); + + let weapon = new Equipment(); + weapon.action = new TriggerAction(weapon, [new DamageEffect()], 1, 500); + spyOn(weapon.action, "getImpactedShips").and.returnValue([ship]); + + let effect = new WeaponEffect(battleview.arena, new Ship(), Target.newFromShip(ship), weapon); + spyOn(effect, "getEffectForWeapon").and.returnValue(() => 100); let mock_shield_impact = spyOn(effect, "shieldImpactEffect").and.stub(); let mock_hull_impact = spyOn(effect, "hullImpactEffect").and.stub(); - effect.gunEffect(); - - let layer = battleview.arena.layer_weapon_effects; - expect(layer.children.length).toBe(1); - - expect(layer.children[0] instanceof Phaser.Particles.Arcade.Emitter).toBe(true); + effect.start(); expect(mock_shield_impact).toHaveBeenCalledTimes(0); expect(mock_hull_impact).toHaveBeenCalledTimes(1); - expect(mock_hull_impact).toHaveBeenCalledWith(jasmine.objectContaining({ x: 0, y: 0 }), jasmine.objectContaining({ x: 50, y: 30 }), 100, 800); + expect(mock_hull_impact).toHaveBeenCalledWith(jasmine.objectContaining({ x: 0, y: 0 }), jasmine.objectContaining({ x: 50, y: 30 }), 40, 400); sprite.shield_bar.setValue(10, 10); - effect.gunEffect(); + effect.start(); expect(mock_shield_impact).toHaveBeenCalledTimes(1); - expect(mock_shield_impact).toHaveBeenCalledWith(jasmine.objectContaining({ x: 0, y: 0 }), jasmine.objectContaining({ x: 50, y: 30 }), 100, 800, true); + expect(mock_shield_impact).toHaveBeenCalledWith(jasmine.objectContaining({ x: 0, y: 0 }), jasmine.objectContaining({ x: 50, y: 30 }), 40, 800, false); expect(mock_hull_impact).toHaveBeenCalledTimes(1); }); @@ -68,7 +82,7 @@ module TK.SpaceTac.UI.Specs { let effect = new WeaponEffect(battleview.arena, new Ship(), Target.newFromLocation(50, 50), new Equipment()); effect.gunEffect(); - checkEmitters("gun effect started", 2); + checkEmitters("gun effect started", 1); fastForward(6000); checkEmitters("gun effect ended", 0); @@ -77,5 +91,31 @@ module TK.SpaceTac.UI.Specs { fastForward(8500); checkEmitters("hull effect ended", 0); }); + + it("adds a laser effect", function () { + let battleview = testgame.battleview; + battleview.timer = new Timer(); + + let effect = new WeaponEffect(battleview.arena, new Ship(), Target.newFromLocation(31, 49), new Equipment()); + + let result = effect.angularLaser({ x: 20, y: 30 }, 300, Math.PI / 4, -Math.PI / 2, 5); + expect(result).toBe(200); + + let layer = battleview.arena.layer_weapon_effects; + expect(layer.children.length).toBe(1); + expect(layer.children[0] instanceof Phaser.Image).toBe(true, "is image"); + let image = layer.children[0]; + expect(image.name).toEqual("battle-effects-laser"); + expect(image.width).toBe(300); + expect(image.x).toEqual(20); + expect(image.y).toEqual(30); + expect(image.rotation).toBeCloseTo(Math.PI / 4, 0.000001); + + let values = battleview.animations.simulate(image, "rotation", 4, result); + expect(values[0]).toBeCloseTo(Math.PI / 4, 0.000001); + expect(values[1]).toBeCloseTo(0, 0.000001); + expect(values[2]).toBeCloseTo(-Math.PI / 4, 0.000001); + expect(values[3]).toBeCloseTo(-Math.PI / 2, 0.000001); + }); }); } diff --git a/src/ui/battle/WeaponEffect.ts b/src/ui/battle/WeaponEffect.ts index 578e943..41bfa93 100644 --- a/src/ui/battle/WeaponEffect.ts +++ b/src/ui/battle/WeaponEffect.ts @@ -35,9 +35,6 @@ module TK.SpaceTac.UI { // Weapon used private weapon: Equipment - // Effect in use - private effect: Function - constructor(arena: Arena, ship: Ship, target: Target, weapon: Equipment) { this.ui = arena.game; this.arena = arena; @@ -47,7 +44,6 @@ module TK.SpaceTac.UI { this.ship = ship; this.target = target; this.weapon = weapon; - this.effect = this.getEffectForWeapon(weapon.code); this.source = this.getCoords(Target.newFromShip(this.ship)); this.destination = this.getCoords(this.target); @@ -59,11 +55,45 @@ module TK.SpaceTac.UI { * Returns the duration of the effect. */ start(): number { - if (this.effect) { - return this.effect(); - } else { - return 0; + // Fire effect + let effect = this.getEffectForWeapon(this.weapon.code, this.weapon.action); + let duration = effect(); + + // Damage effect + let action = this.weapon.action; + if (action instanceof TriggerAction && any(action.effects, effect => effect instanceof DamageEffect)) { + let ships = action.getImpactedShips(this.ship, this.target, this.source); + let source = action.blast ? this.target : this.source; + let damage_duration = this.damageEffect(source, ships, duration * 0.4, this.weapon.code == "gatlinggun"); + duration = Math.max(duration, damage_duration); } + + return duration; + } + + /** + * Add a damage effect on ships impacted by a weapon + */ + damageEffect(source: IArenaLocation, ships: Ship[], base_delay = 0, shield_flares = false): number { + let duration = 0; + + // TODO For each ship, delay should depend on fire effect animation + let delay = base_delay; + + ships.forEach(ship => { + let sprite = this.arena.findShipSprite(ship); + if (sprite) { + if (sprite.getValue("shield") > 0) { + this.shieldImpactEffect(source, sprite, delay, 800, shield_flares); + duration = Math.max(duration, delay + 800); + } else { + this.hullImpactEffect(source, sprite, delay, 400); + duration = Math.max(duration, delay + 400); + } + } + }); + + return duration; } /** @@ -85,12 +115,17 @@ module TK.SpaceTac.UI { /** * Get the function that will be called to start the visual effect */ - getEffectForWeapon(weapon: string): Function { + getEffectForWeapon(weapon: string, action: BaseAction | null): () => number { switch (weapon) { case "gatlinggun": - return this.gunEffect.bind(this); + return () => this.gunEffect(); + case "prokhorovlaser": + let trigger = nn(action); + let angle = arenaAngle(this.source, this.target); + let dangle = radians(trigger.angle) * 0.5; + return () => this.angularLaser(this.source, trigger.range, angle - dangle, angle + dangle); default: - return this.defaultEffect.bind(this); + return () => this.defaultEffect(); } } @@ -185,22 +220,28 @@ module TK.SpaceTac.UI { }); tween.start(); - if (blast_radius > 0 && this.weapon.action instanceof TriggerAction) { - if (any(this.weapon.action.effects, effect => effect instanceof DamageEffect)) { - let ships = this.arena.getShipsInCircle(new ArenaCircleArea(this.destination.x, this.destination.y, blast_radius)); - ships.forEach(sprite => { - if (sprite.getValue("shield") > 0) { - this.shieldImpactEffect(this.target, sprite, 1200, 800); - } else { - this.hullImpactEffect(this.target, sprite, 1200, 400); - } - }); - } - } - return projectile_duration + (blast_radius ? 1500 : 0); } + /** + * Laser effect, scanning from one angle to the other + */ + angularLaser(source: IArenaLocation, radius: number, start_angle: number, end_angle: number, speed = 1): number { + let duration = 1000 / speed; + + let laser = this.view.newImage("battle-effects-laser", source.x, source.y); + laser.anchor.set(0, 0.5); + laser.rotation = start_angle; + laser.scale.set(radius / laser.width); + this.layer.add(laser); + + let tween = this.view.tweens.create(laser).to({ rotation: end_angle }, duration); + tween.onComplete.addOnce(() => laser.destroy()); + tween.start(); + + return duration; + } + /** * Submachine gun effect (quick chain of small bullets) */ @@ -230,12 +271,6 @@ module TK.SpaceTac.UI { this.layer.add(emitter); this.timer.schedule(5000, () => emitter.destroy()); - if (has_shield) { - this.shieldImpactEffect(this.source, this.target, 100, 800, true); - } else { - this.hullImpactEffect(this.source, this.target, 100, 800); - } - return 1000; } } diff --git a/src/ui/common/Animations.ts b/src/ui/common/Animations.ts index eb5c490..ab98971 100644 --- a/src/ui/common/Animations.ts +++ b/src/ui/common/Animations.ts @@ -58,7 +58,7 @@ module TK.SpaceTac.UI { simulate(obj: any, property: string, points = 5, duration = 1000): number[] { let tween = first(this.tweens.getAll().concat((this.tweens)._add), tween => tween.target === obj && !tween.pendingDelete); if (tween) { - return [obj[property]].concat(tween.generateData(points - 1).map(data => data[property])); + return [obj[property]].concat(tween.generateData(1000 * (points - 1) / duration).map(data => data[property])); } else { return []; } diff --git a/yarn.lock b/yarn.lock index 1e46572..a3cb002 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1315,7 +1315,11 @@ istanbul@0.4.5, istanbul@^0.4.0: which "^1.1.1" wordwrap "^1.0.0" -jasmine-core@2.8.0, jasmine-core@~2.8.0: +jasmine-core@2.5.2: + version "2.5.2" + resolved "https://registry.yarnpkg.com/jasmine-core/-/jasmine-core-2.5.2.tgz#6f61bd79061e27f43e6f9355e44b3c6cab6ff297" + +jasmine-core@~2.8.0: version "2.8.0" resolved "https://registry.yarnpkg.com/jasmine-core/-/jasmine-core-2.8.0.tgz#bcc979ae1f9fd05701e45e52e65d3a5d63f1a24e"