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"