diff --git a/TODO.md b/TODO.md
index c616750..8f3a1ec 100644
--- a/TODO.md
+++ b/TODO.md
@@ -31,6 +31,8 @@ Character sheet
Battle
------
+* Improve arena ships layering (sometimes information is displayed behind other sprites)
+* In the ship tooltip, show power cost, toggled and overheat states
* Display shield (and its (dis)appearance)
* Display estimated damage and displacement in targetting mode
* Add a voluntary retreat option
@@ -54,9 +56,10 @@ Battle
Ships models and actions
------------------------
+* Fix vigilance action triggering when the ship moves with one active (moving should disable vigilance actions)
+* Fix vigilance action not disabling when reaching the maximum number of triggerings
+* Highlight the effects area that will contain the new position when move-targetting
* Add movement attribute (for main engine action, km/power)
-* Add vigilance system, to watch if another ship enters a given radius, to be able to interrupt its turn
-* Remove safety margin for move actions (vigilance system should replace it)
* Add damage over time effect (tricky to make intuitive)
* Add actions with cost dependent of distance (like current move actions)
* Add disc targetting (for some jump move actions)
@@ -102,6 +105,7 @@ Technical
---------
* Fix "npm test" returning 0 even on failure
+* Fix "npm start" stopping when there is an error in initial build
* Pack sounds
* Add toggles for shaders, automatically disable them if too slow, and initially disable them on mobile
diff --git a/data/stage2/image/action/interceptors.png b/data/stage2/image/action/interceptors.png
new file mode 100644
index 0000000..881fbd4
Binary files /dev/null and b/data/stage2/image/action/interceptors.png differ
diff --git a/docs/balancing.md b/docs/balancing.md
index 10eaa98..e0bc9a7 100644
--- a/docs/balancing.md
+++ b/docs/balancing.md
@@ -9,9 +9,11 @@
* Mean HP = 5
* Mean damage = 3
* Power = move half arena + one action
+* 2 or 3 actions
## Level 10
* Mean HP = 15
* Mean damage = 6
* Power = move across arena + two actions (or one action and move again)
+* Up to 8 actions
diff --git a/graphics/ui/actions.svg b/graphics/ui/actions.svg
index 92b6bef..8e3362f 100644
--- a/graphics/ui/actions.svg
+++ b/graphics/ui/actions.svg
@@ -10,19 +10,51 @@
xmlns:xlink="http://www.w3.org/1999/xlink"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
- width="273.06668"
- height="273.06668"
+ width="256"
+ height="256"
id="svg2"
version="1.1"
inkscape:version="0.92.1 r15371"
sodipodi:docname="actions.svg"
- inkscape:export-filename="/home/michael/workspace/perso/spacetac/graphics/exported/action/damageprotector.png"
- inkscape:export-xdpi="90"
- inkscape:export-ydpi="90"
+ inkscape:export-filename="/home/michael/workspace/spacetac/data/stage2/image/action/interceptors.png"
+ inkscape:export-xdpi="96.000008"
+ inkscape:export-ydpi="96.000008"
viewBox="0 0 256 256"
- enable-background="new">
+ style="enable-background:new">
+
+
+
+
+
+
+
+
+
+
@@ -145,45 +177,45 @@
-
-
@@ -575,34 +587,13 @@
stdDeviation="2.3324547"
id="feGaussianBlur10826" />
-
-
-
+ gradientUnits="userSpaceOnUse"
+ gradientTransform="translate(0,17.066673)" />
+ gradientUnits="userSpaceOnUse"
+ gradientTransform="translate(0,17.066673)" />
+ width="1.1060725"
+ y="-0.68018013"
+ height="2.3603604">
@@ -675,50 +659,6 @@
stdDeviation="2.3987354"
id="feGaussianBlur4731" />
-
-
-
-
+ gradientUnits="userSpaceOnUse"
+ gradientTransform="translate(0,17.066673)" />
+ height="1.8904001">
+ gradientUnits="userSpaceOnUse"
+ gradientTransform="translate(0,17.066673)" />
-
+ gradientUnits="userSpaceOnUse"
+ gradientTransform="translate(0,17.066673)" />
-
+ y="-0.98874992"
+ height="2.9775">
@@ -1151,19 +1074,8 @@
stdDeviation="2.6125488"
id="feGaussianBlur5874" />
-
-
+ id="feComposite6201"
+ k2="0"
+ k3="0"
+ k4="0" />
+ id="feComposite6203"
+ k1="0"
+ k4="0" />
@@ -1395,10 +1301,11 @@
result="result4"
in="fbSourceGraphic"
in2="result2"
- id="feComposite6227" />
+ id="feComposite6227"
+ k1="0"
+ k4="0" />
@@ -1411,10 +1318,10 @@
inkscape:collect="always"
style="color-interpolation-filters:sRGB"
id="filter6299"
- x="-0.058554495"
- width="1.117109"
- y="-0.87426646"
- height="2.7485329">
+ x="-0.058554497"
+ width="1.1171089"
+ y="-0.87426645"
+ height="2.748533">
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
image/svg+xml
-
+
@@ -1487,10 +1597,11 @@
inkscape:label="endturn"
inkscape:groupmode="layer"
id="layer1"
- transform="translate(0,-796.36216)"
+ transform="translate(0,-813.42884)"
style="display:none">
+ id="g4487"
+ transform="translate(0,17.066673)">
+ style="display:none"
+ transform="translate(0,-17.066682)">
+ style="display:none"
+ transform="translate(0,-17.066682)">
+ transform="matrix(0.72068912,-0.72068912,0.72068912,0.72068912,-79.281576,168.53478)">
+ style="opacity:1;fill:url(#linearGradient5187);fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:10.39999962;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:0" />
+ style="opacity:1;fill:url(#linearGradient5189);fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:3.5999999;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
@@ -1923,43 +2040,43 @@
@@ -1967,20 +2084,21 @@
style="fill:#ff0000;fill-rule:evenodd;stroke:#000000;stroke-width:0.93749994px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
id="path4728"
cx="363.61606"
- cy="-31.946413"
+ cy="-14.879741"
r="12.053571" />
+ inkscape:groupmode="layer"
+ transform="translate(0,-17.066682)">
@@ -2041,43 +2159,43 @@
@@ -2091,55 +2209,55 @@
@@ -2148,7 +2266,8 @@
style="display:none"
inkscape:label="FractalHull"
id="g5356"
- inkscape:groupmode="layer">
+ inkscape:groupmode="layer"
+ transform="translate(0,-17.066682)">
@@ -2209,43 +2328,43 @@
@@ -2259,43 +2378,43 @@
@@ -2309,43 +2428,43 @@
@@ -2359,43 +2478,43 @@
@@ -2409,43 +2528,43 @@
@@ -2459,43 +2578,43 @@
@@ -2509,43 +2628,43 @@
@@ -2554,10 +2673,11 @@
inkscape:groupmode="layer"
id="layer7"
inkscape:label="ForceField"
- style="display:none">
+ style="display:none"
+ transform="translate(0,-17.066682)">
@@ -2565,11 +2685,11 @@
style="fill:#30383d;fill-opacity:1;fill-rule:evenodd;stroke:#465158;stroke-width:12.15708637;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="path4757"
cx="128"
- cy="128"
+ cy="145.06667"
r="98.321274" />
@@ -2578,27 +2698,29 @@
style="display:none"
inkscape:label="GravitShield"
id="g5022"
- inkscape:groupmode="layer">
+ inkscape:groupmode="layer"
+ transform="translate(0,-17.066682)">
+ transform="translate(-59.137619,95.614841)">
+ style="fill:url(#radialGradient5199);fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:0.93749994px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;filter:url(#filter5260)" />
+ id="g5114"
+ transform="translate(0,17.066673)">
+ transform="rotate(45,128,145.06668)" />
+ style="display:none"
+ transform="translate(0,-17.066682)">
@@ -2654,10 +2777,11 @@
style="fill:#2a2a2a;fill-opacity:0.76595746;fill-rule:evenodd;stroke:#c6b1d3;stroke-width:5.42480278;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;filter:url(#filter5572)"
id="circle5026"
cx="128"
- cy="128"
+ cy="145.06667"
r="98.321274" />
+ id="g5494"
+ transform="translate(0,17.066673)">
+ transform="rotate(15,127.9593,144.9372)" />
+ transform="rotate(30,127.9593,144.9372)" />
+ transform="rotate(45,127.9593,144.9372)" />
+ transform="rotate(60,127.9593,144.9372)" />
+ transform="rotate(75,127.9593,144.9372)" />
+ style="display:none"
+ transform="translate(0,-17.066682)">
+ y="44.986328" />
+ y="63.468468" />
+ transform="matrix(-1,0,0,1,256,-3.5839838e-7)" />
+ transform="translate(0,83.811087)" />
+ y="95.047005" />
+ style="display:none"
+ transform="translate(0,-17.066682)">
@@ -2811,24 +2937,24 @@
width="118.45962"
id="rect4695"
style="opacity:0.60500004;fill:#343434;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:3.48488426;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:0.3148936;filter:url(#filter4729)"
- transform="matrix(1.1343516,0,0,1.1343516,-17.197,-18.002129)" />
+ transform="matrix(1.1343516,0,0,1.1343516,-17.197,-0.93545636)" />
+ transform="matrix(1.1343516,0,0,1.1343516,-87.460959,20.827378)">
+ style="fill:url(#radialGradient5205);fill-opacity:1;fill-rule:evenodd;stroke:#000000;stroke-width:1.13441122;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:0.31276598" />
+ style="fill:url(#radialGradient5207);fill-opacity:1;fill-rule:evenodd;stroke:#000000;stroke-width:0.26590008px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:0.1851064" />
+ style="fill:url(#radialGradient5209);fill-opacity:1;fill-rule:evenodd;stroke:#000000;stroke-width:0.26590008px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:0.2" />
+ style="fill:url(#radialGradient5211);fill-opacity:1;fill-rule:evenodd;stroke:#000000;stroke-width:0.26590008px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:0.33191492" />
+ inkscape:groupmode="layer"
+ transform="translate(0,-17.066682)">
+ transform="translate(-106.6089,90.529949)">
+ style="display:none"
+ transform="translate(0,-17.066682)">
+ transform="translate(1.6322846,-4.6399234)">
+ style="display:none"
+ transform="translate(0,-17.066682)">
+ transform="matrix(0.86849352,0,0,0.86849352,21.648667,50.529762)">
+ transform="translate(-23.675449,36.007032)">
+ style="display:none"
+ transform="translate(0,-17.066682)">
+ transform="rotate(144.83146,129.4117,148.24125)" />
+ transform="translate(-2.6042991,14.462373)">
+ transform="matrix(2.2706815,0,0,2.2706815,-162.64723,-145.58056)" />
+ transform="rotate(-80.838515,124.57679,121.95315)" />
+ style="display:none"
+ transform="translate(0,-17.066682)">
+ transform="rotate(-65.572632,149.50521,139.61448)">
+ style="display:none"
+ transform="translate(0,-17.066682)">
+ transform="matrix(0.77942184,0,0,0.77942184,28.096496,40.012935)">
+ y="95.544083" />
+ transform="matrix(0.77942184,0,0,0.77942184,28.52464,45.721662)">
@@ -3511,7 +3643,7 @@
+ transform="matrix(0.77942184,0,0,0.77942184,26.812044,47.434189)">
@@ -3595,12 +3727,12 @@
+ transform="matrix(0.77942184,0,0,0.77942184,28.23197,45.300677)">
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/package-lock.json b/package-lock.json
index 0ae8597..65e94eb 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -6851,9 +6851,9 @@
"dev": true
},
"typescript": {
- "version": "2.7.2",
- "resolved": "https://registry.npmjs.org/typescript/-/typescript-2.7.2.tgz",
- "integrity": "sha512-p5TCYZDAO0m4G344hD+wx/LATebLWZNkkh2asWUFqSsD2OrDNhbAHuSjobrmsUmdzjJjEeZVU9g1h3O6vpstnw==",
+ "version": "2.8.1",
+ "resolved": "https://registry.npmjs.org/typescript/-/typescript-2.8.1.tgz",
+ "integrity": "sha512-Ao/f6d/4EPLq0YwzsQz8iXflezpTkQzqAyenTiw4kCUGr1uPiFLC3+fZ+gMZz6eeI/qdRUqvC+HxIJzUAzEFdg==",
"dev": true
},
"uglify-js": {
diff --git a/package.json b/package.json
index 4b89cd5..44c3fcf 100644
--- a/package.json
+++ b/package.json
@@ -33,7 +33,7 @@
"remap-istanbul": "^0.10.1",
"runjs": "^4.3.0",
"shelljs": "^0.8.1",
- "typescript": "^2.7.1",
+ "typescript": "^2.8.1",
"uglify-js": "^3.3.13"
},
"dependencies": {
diff --git a/src/common b/src/common
index 0fcd953..fc4bde3 160000
--- a/src/common
+++ b/src/common
@@ -1 +1 @@
-Subproject commit 0fcd953719ed8dbaab92cfaf525a2e34078b069a
+Subproject commit fc4bde326c2dcb4be03380ac29bac8d12b015821
diff --git a/src/core/Battle.spec.ts b/src/core/Battle.spec.ts
index e8aa00b..c3f1dda 100644
--- a/src/core/Battle.spec.ts
+++ b/src/core/Battle.spec.ts
@@ -280,8 +280,10 @@ module TK.SpaceTac {
test.case("lists area effects", check => {
let battle = new Battle();
let ship = battle.fleets[0].addShip();
+ let peer = battle.fleets[1].addShip();
+ peer.setArenaPosition(100, 50);
- check.equals(imaterialize(battle.iAreaEffects(100, 50)), [], "initial");
+ check.equals(battle.getAreaEffects(peer), [], "initial");
let drone1 = new Drone(ship);
drone1.x = 120;
@@ -296,7 +298,7 @@ module TK.SpaceTac {
drone2.effects = [new DamageEffect(14)];
battle.addDrone(drone2);
- check.equals(imaterialize(battle.iAreaEffects(100, 50)), [drone1.effects[0]], "drone effects");
+ check.equals(battle.getAreaEffects(peer), [[drone1, drone1.effects[0]]], "drone effects");
let eq1 = new ToggleAction("eq1", { power: 0, radius: 500, effects: [new AttributeEffect("initiative", 1)] });
ship.actions.addCustom(eq1);
@@ -308,9 +310,9 @@ module TK.SpaceTac {
ship.actions.addCustom(eq3);
ship.actions.toggle(eq3, true);
- check.equals(imaterialize(battle.iAreaEffects(100, 50)), [
- drone1.effects[0],
- eq1.effects[0],
+ check.equals(battle.getAreaEffects(peer), [
+ [drone1, drone1.effects[0]],
+ [ship, eq1.effects[0]],
], "drone and toggle effects");
});
diff --git a/src/core/Battle.ts b/src/core/Battle.ts
index f095b4c..9bab616 100644
--- a/src/core/Battle.ts
+++ b/src/core/Battle.ts
@@ -328,15 +328,23 @@ module TK.SpaceTac {
}
/**
- * Get the list of area effects at a given location
+ * Get the list of area effects that are expected to apply on a given ship
*/
- iAreaEffects(x: number, y: number): Iterator {
- let drones_in_range = ifilter(this.drones.iterator(), drone => drone.isInRange(x, y));
+ getAreaEffects(ship: Ship): [Ship | Drone, BaseEffect][] {
+ let drone_effects = this.drones.list().map(drone => {
+ // FIXME Should apply filterImpactedShips from drone action
+ if (drone.isInRange(ship.arena_x, ship.arena_y)) {
+ return drone.effects.map((effect): [Ship | Drone, BaseEffect] => [drone, effect]);
+ } else {
+ return [];
+ }
+ });
- return ichain(
- ichainit(imap(drones_in_range, drone => iarray(drone.effects))),
- ichainit(imap(this.iships(), ship => ship.iAreaEffects(x, y)))
- );
+ let ships_effects = this.ships.list().map(iship => {
+ return iship.getAreaEffects(ship).map((effect): [Ship | Drone, BaseEffect] => [iship, effect]);
+ });
+
+ return flatten(drone_effects.concat(ships_effects));
}
/**
diff --git a/src/core/BattleChecks.spec.ts b/src/core/BattleChecks.spec.ts
index 0dd8d12..8777bb7 100644
--- a/src/core/BattleChecks.spec.ts
+++ b/src/core/BattleChecks.spec.ts
@@ -61,7 +61,7 @@ module TK.SpaceTac.Specs {
let effect1 = ship1.active_effects.add(new StickyEffect(new BaseEffect("e1")));
let effect2 = ship1.active_effects.add(new BaseEffect("e2"));
let effect3 = ship1.active_effects.add(new BaseEffect("e3"));
- check.patch(battle, "iAreaEffects", () => isingle(effect3));
+ check.patch(battle, "getAreaEffects", (): [Ship, BaseEffect][] => [[ship1, effect3]]);
check.in("sticky+obsolete+missing", check => {
check.equals(checks.checkAreaEffects(), [
new ShipEffectRemovedDiff(ship1, effect2),
@@ -69,5 +69,38 @@ module TK.SpaceTac.Specs {
], "effects diff");
});
})
+
+ test.case("applies vigilance actions", check => {
+ let battle = new Battle();
+ let ship1 = battle.fleets[0].addShip();
+ ship1.setArenaPosition(100, 100);
+ TestTools.setShipModel(ship1, 10, 0, 5);
+ let ship2 = battle.fleets[1].addShip();
+ ship2.setArenaPosition(1000, 1000);
+ TestTools.setShipModel(ship2, 10);
+ TestTools.setShipPlaying(battle, ship1);
+
+ let vig1 = ship1.actions.addCustom(new VigilanceAction("Vig1", { radius: 100, filter: ActionTargettingFilter.ENEMIES }, { intruder_effects: [new DamageEffect(1)] }));
+ let vig2 = ship1.actions.addCustom(new VigilanceAction("Vig2", { radius: 50, filter: ActionTargettingFilter.ENEMIES }, { intruder_effects: [new DamageEffect(2)] }));
+ let vig3 = ship1.actions.addCustom(new VigilanceAction("Vig3", { radius: 100, filter: ActionTargettingFilter.ALLIES }, { intruder_effects: [new DamageEffect(3)] }));
+ battle.applyOneAction(vig1.id);
+ battle.applyOneAction(vig2.id);
+ battle.applyOneAction(vig3.id);
+
+ let checks = new BattleChecks(battle);
+ check.in("initial state", check => {
+ check.equals(checks.checkAreaEffects(), [], "effects diff");
+ });
+
+ ship2.setArenaPosition(100, 160);
+ check.in("ship2 moved in range", check => {
+ check.equals(checks.checkAreaEffects(), [
+ new ShipEffectAddedDiff(ship2, vig1.effects[0]),
+ new VigilanceAppliedDiff(ship1, vig1, ship2),
+ new ShipDamageDiff(ship2, 1, 0),
+ new ShipValueDiff(ship2, "hull", -1),
+ ], "effects diff");
+ });
+ })
})
}
diff --git a/src/core/BattleChecks.ts b/src/core/BattleChecks.ts
index ce91a32..d404dc6 100644
--- a/src/core/BattleChecks.ts
+++ b/src/core/BattleChecks.ts
@@ -7,9 +7,7 @@ module TK.SpaceTac {
* To fix the state, new diffs will be applied
*/
export class BattleChecks {
- private battle: Battle;
- constructor(battle: Battle) {
- this.battle = battle;
+ constructor(private battle: Battle) {
}
/**
@@ -23,12 +21,13 @@ module TK.SpaceTac {
diffs = this.checkAll();
if (diffs.length > 0) {
+ //console.log("Battle checks diffs", diffs);
this.battle.applyDiffs(diffs);
}
loops += 1;
if (loops >= 1000) {
- console.error("Battle checks locked in infinite loop", diffs);
+ console.error("Battle checks stuck in infinite loop", diffs);
break;
}
} while (diffs.length > 0);
@@ -124,32 +123,38 @@ module TK.SpaceTac {
}
/**
- * Check area effects (remove obsolete ones, and add missing ones)
+ * Get the diffs to apply to a ship, if moving at a given location
*/
- checkAreaEffects(): BaseBattleDiff[] {
+ getAreaEffectsDiff(ship: Ship): BaseBattleDiff[] {
let result: BaseBattleDiff[] = [];
+ let expected = this.battle.getAreaEffects(ship);
+ let expected_hash = new RObjectContainer(expected.map(x => x[1]));
- iforeach(this.battle.iships(true), ship => {
- let expected = new RObjectContainer(imaterialize(this.battle.iAreaEffects(ship.arena_x, ship.arena_y)));
+ // Remove obsolete effects
+ ship.active_effects.list().forEach(effect => {
+ if (!(effect instanceof StickyEffect) && !expected_hash.get(effect.id)) {
+ result.push(new ShipEffectRemovedDiff(ship, effect));
+ result = result.concat(effect.getOffDiffs(ship));
+ }
+ });
- // Remove obsolete effects
- ship.active_effects.list().forEach(effect => {
- if (!(effect instanceof StickyEffect) && !expected.get(effect.id)) {
- result.push(new ShipEffectRemovedDiff(ship, effect));
- result = result.concat(effect.getOffDiffs(ship));
- }
- });
-
- // Add missing effects
- expected.list().forEach(effect => {
- if (!ship.active_effects.get(effect.id)) {
- result.push(new ShipEffectAddedDiff(ship, effect));
- result = result.concat(effect.getOnDiffs(ship, ship)); // TODO correct source
- }
- });
+ // Add missing effects
+ expected.forEach(([source, effect]) => {
+ if (!ship.active_effects.get(effect.id)) {
+ result.push(new ShipEffectAddedDiff(ship, effect));
+ result = result.concat(effect.getOnDiffs(ship, source));
+ }
});
return result;
}
+
+ /**
+ * Check area effects (remove obsolete ones, and add missing ones)
+ */
+ checkAreaEffects(): BaseBattleDiff[] {
+ let ships = imaterialize(this.battle.iships(true));
+ return flatten(ships.map(ship => this.getAreaEffectsDiff(ship)));
+ }
}
}
diff --git a/src/core/Ship.ts b/src/core/Ship.ts
index 3d169d9..3ed5c1b 100644
--- a/src/core/Ship.ts
+++ b/src/core/Ship.ts
@@ -388,17 +388,18 @@ module TK.SpaceTac {
}
/**
- * Iterator over area effects from this ship impacting a location
+ * Get the effects that this ship has on another ship (which may be herself)
*/
- iAreaEffects(x: number, y: number): Iterator {
- let distance = Target.newFromShip(this).getDistanceTo({ x: x, y: y });
- return ichainit(imap(this.iToggleActions(true), action => {
- if (distance <= action.radius) {
- return iarray(action.effects);
+ getAreaEffects(ship: Ship): BaseEffect[] {
+ let toggled = imaterialize(this.iToggleActions(true));
+ let effects = toggled.map(action => {
+ if (bool(action.filterImpactedShips(this, this.location, Target.newFromShip(ship), [ship]))) {
+ return action.effects;
} else {
- return IEMPTY;
+ return [];
}
- }));
+ });
+ return flatten(effects);
}
/**
diff --git a/src/core/actions/BaseAction.spec.ts b/src/core/actions/BaseAction.spec.ts
index d0054ab..40cd825 100644
--- a/src/core/actions/BaseAction.spec.ts
+++ b/src/core/actions/BaseAction.spec.ts
@@ -91,5 +91,26 @@ module TK.SpaceTac.Specs {
cooldown.cool();
check.equals(action.checkCannotBeApplied(ship), null);
})
+
+ test.case("helps applying a targetting filter", check => {
+ let fleet1 = new Fleet();
+ let fleet2 = new Fleet();
+ let ship1a = fleet1.addShip();
+ let ship1b = fleet1.addShip();
+ let ship2a = fleet2.addShip();
+ let ship2b = fleet2.addShip();
+ let ships = [ship1a, ship1b, ship2a, ship2b];
+
+ check.equals(BaseAction.filterTargets(ship1a, ships, ActionTargettingFilter.ALL),
+ [ship1a, ship1b, ship2a, ship2b], "ALL");
+ check.equals(BaseAction.filterTargets(ship1a, ships, ActionTargettingFilter.ALL_BUT_SELF),
+ [ship1b, ship2a, ship2b], "ALL_BUT_SELF");
+ check.equals(BaseAction.filterTargets(ship1a, ships, ActionTargettingFilter.ALLIES),
+ [ship1a, ship1b], "ALLIES");
+ check.equals(BaseAction.filterTargets(ship1a, ships, ActionTargettingFilter.ALLIES_BUT_SELF),
+ [ship1b], "ALLIES_BUT_SELF");
+ check.equals(BaseAction.filterTargets(ship1a, ships, ActionTargettingFilter.ENEMIES),
+ [ship2a, ship2b], "ENEMIES");
+ });
});
}
diff --git a/src/core/actions/BaseAction.ts b/src/core/actions/BaseAction.ts
index 53918c3..e177d50 100644
--- a/src/core/actions/BaseAction.ts
+++ b/src/core/actions/BaseAction.ts
@@ -17,6 +17,24 @@ module TK.SpaceTac {
SURROUNDINGS
}
+ /**
+ * Targetting filter for an action.
+ *
+ * This will filter ships inside the targetted area, to determine which will receive the action effects.
+ */
+ export enum ActionTargettingFilter {
+ // Apply on all ships
+ ALL,
+ // Apply on all ships except the actor
+ ALL_BUT_SELF,
+ // Apply on all allies, including the actor
+ ALLIES,
+ // Apply on all allies, except the actor
+ ALLIES_BUT_SELF,
+ // Apply on all enemies
+ ENEMIES
+ }
+
/**
* Base class for a battle action.
*
@@ -139,7 +157,7 @@ module TK.SpaceTac {
*
* This may be used as an indicator for helping the player in targetting, or to effectively apply the effects
*/
- filterImpactedShips(source: IArenaLocation, target: Target, ships: Ship[]): Ship[] {
+ filterImpactedShips(ship: Ship, source: IArenaLocation, target: Target, ships: Ship[]): Ship[] {
return [];
}
@@ -149,12 +167,52 @@ module TK.SpaceTac {
getImpactedShips(ship: Ship, target: Target, source: IArenaLocation = ship.location): Ship[] {
let battle = ship.getBattle();
if (battle) {
- return this.filterImpactedShips(source, target, imaterialize(battle.iships(true)));
+ return this.filterImpactedShips(ship, source, target, imaterialize(battle.iships(true)));
} else {
return [];
}
}
+ /**
+ * Helper to apply a targetting filter on a list of ships, to determine which ones are impacted
+ */
+ static filterTargets(source: Ship, ships: Ship[], filter: ActionTargettingFilter): Ship[] {
+ return ships.filter(ship => {
+ if (filter == ActionTargettingFilter.ALL) {
+ return true;
+ } else if (filter == ActionTargettingFilter.ALL_BUT_SELF) {
+ return !ship.is(source);
+ } else if (filter == ActionTargettingFilter.ALLIES) {
+ return ship.fleet.player.is(source.fleet.player);
+ } else if (filter == ActionTargettingFilter.ALLIES_BUT_SELF) {
+ return ship.fleet.player.is(source.fleet.player) && !ship.is(source);
+ } else if (filter == ActionTargettingFilter.ENEMIES) {
+ return !ship.fleet.player.is(source.fleet.player);
+ } else {
+ return false;
+ }
+ });
+ }
+
+ /**
+ * Get a name to represent the group of ships specified by a target filter
+ */
+ static getFilterDesc(filter: ActionTargettingFilter, plural = true): string {
+ if (filter == ActionTargettingFilter.ALL) {
+ return plural ? "ships" : "ship";
+ } else if (filter == ActionTargettingFilter.ALL_BUT_SELF) {
+ return plural ? "other ships" : "other ship";
+ } else if (filter == ActionTargettingFilter.ALLIES) {
+ return plural ? "team members" : "team member";
+ } else if (filter == ActionTargettingFilter.ALLIES_BUT_SELF) {
+ return plural ? "teammates" : "teammates";
+ } else if (filter == ActionTargettingFilter.ENEMIES) {
+ return plural ? "enemies" : "enemy";
+ } else {
+ return "";
+ }
+ }
+
/**
* Check if a target is suitable for this action
*
diff --git a/src/core/actions/DeployDroneAction.ts b/src/core/actions/DeployDroneAction.ts
index c1bbe1e..15ca893 100644
--- a/src/core/actions/DeployDroneAction.ts
+++ b/src/core/actions/DeployDroneAction.ts
@@ -2,7 +2,7 @@
module TK.SpaceTac {
/**
- * Configuration of a toggle action
+ * Configuration of a drone deployment action
*/
export interface DeployDroneActionConfig {
// Maximal distance the drone may be deployed
@@ -61,8 +61,10 @@ module TK.SpaceTac {
return ship.actions.isToggled(this) ? 0 : this.deploy_distance;
}
- filterImpactedShips(source: ArenaLocation, target: Target, ships: Ship[]): Ship[] {
- return ships.filter(ship => arenaDistance(ship.location, target) <= this.drone_radius);
+ filterImpactedShips(ship: Ship, source: ArenaLocation, target: Target, ships: Ship[]): Ship[] {
+ let result = ships.filter(iship => arenaDistance(iship.location, target) <= this.radius);
+ result = BaseAction.filterTargets(ship, result, this.filter);
+ return result;
}
checkLocationTarget(ship: Ship, target: Target): Target {
@@ -96,8 +98,8 @@ module TK.SpaceTac {
getEffectsDescription(): string {
let desc = `Deploy drone (power usage ${this.power}, max range ${this.deploy_distance}km)`;
+ let suffix = `on ${BaseAction.getFilterDesc(this.filter)} in ${this.drone_radius}km radius`;
let effects = this.drone_effects.map(effect => {
- let suffix = `for ships in ${this.drone_radius}km radius`;
return "• " + effect.getDescription() + " " + suffix;
});
return `${desc}:\n${effects.join("\n")}`;
diff --git a/src/core/actions/EndTurnAction.spec.ts b/src/core/actions/EndTurnAction.spec.ts
index d7835e7..a444393 100644
--- a/src/core/actions/EndTurnAction.spec.ts
+++ b/src/core/actions/EndTurnAction.spec.ts
@@ -164,7 +164,7 @@ module TK.SpaceTac.Specs {
ship.active_effects.add(effect1);
ship.active_effects.add(effect2);
effect2.base.getOnDiffs(ship, ship).forEach(effect => effect.apply(battle));
- check.patch(battle, "iAreaEffects", () => isingle(effect1));
+ check.patch(battle, "getAreaEffects", (): [Ship, BaseEffect][] => [[ship, effect1]]);
TestTools.actionChain(check, battle, [
[ship, EndTurnAction.SINGLETON, Target.newFromShip(ship)],
diff --git a/src/core/actions/ToggleAction.ts b/src/core/actions/ToggleAction.ts
index 60259e2..2637536 100644
--- a/src/core/actions/ToggleAction.ts
+++ b/src/core/actions/ToggleAction.ts
@@ -11,6 +11,8 @@ module TK.SpaceTac {
radius: number
// Effects applied
effects: BaseEffect[]
+ // Filtering ships that will receive the effects
+ filter: ActionTargettingFilter
}
/**
@@ -22,6 +24,7 @@ module TK.SpaceTac {
power = 1
radius = 0
effects: BaseEffect[] = []
+ filter = ActionTargettingFilter.ALL
constructor(name: string, config?: Partial, code?: string) {
super(name, code);
@@ -58,15 +61,17 @@ module TK.SpaceTac {
return 0;
}
- filterImpactedShips(source: ArenaLocation, target: Target, ships: Ship[]): Ship[] {
- return ships.filter(ship => arenaDistance(ship.location, source) <= this.radius);
+ filterImpactedShips(ship: Ship, source: ArenaLocation, target: Target, ships: Ship[]): Ship[] {
+ let result = ships.filter(iship => arenaDistance(iship.location, source) <= this.radius);
+ result = BaseAction.filterTargets(ship, result, this.filter);
+ return result;
}
checkShipTarget(ship: Ship, target: Target): Target | null {
return ship.is(target.ship_id) ? target : null;
}
- getSpecificDiffs(ship: Ship, battle: Battle, target: Target): BaseBattleDiff[] {
+ getSpecificDiffs(ship: Ship, battle: Battle, target: Target, apply_effects = true): BaseBattleDiff[] {
let activated = ship.actions.isToggled(this);
let result: BaseBattleDiff[] = [
@@ -78,10 +83,14 @@ module TK.SpaceTac {
this.effects.forEach(effect => {
if (activated) {
result.push(new ShipEffectRemovedDiff(iship, effect));
- result = result.concat(effect.getOffDiffs(iship));
+ if (apply_effects) {
+ result = result.concat(effect.getOffDiffs(iship));
+ }
} else {
result.push(new ShipEffectAddedDiff(iship, effect));
- result = result.concat(effect.getOnDiffs(iship, ship));
+ if (apply_effects) {
+ result = result.concat(effect.getOnDiffs(iship, ship));
+ }
}
});
});
@@ -94,9 +103,10 @@ module TK.SpaceTac {
return "";
}
+ // TODO filter
let desc = `When active (power usage ${this.power})`;
let effects = this.effects.map(effect => {
- let suffix = this.radius ? `in ${this.radius}km radius` : "on owner ship";
+ let suffix = this.radius ? `on ${BaseAction.getFilterDesc(this.filter)} in ${this.radius}km radius` : "on owner ship";
return "• " + effect.getDescription() + " " + suffix;
});
return `${desc}:\n${effects.join("\n")}`;
diff --git a/src/core/actions/TriggerAction.spec.ts b/src/core/actions/TriggerAction.spec.ts
index 88fc662..26cda5a 100644
--- a/src/core/actions/TriggerAction.spec.ts
+++ b/src/core/actions/TriggerAction.spec.ts
@@ -72,14 +72,14 @@ module TK.SpaceTac.Specs {
let ships = [ship1, ship2, ship3];
let action = new TriggerAction("testaction", { range: 50 });
- check.equals(action.filterImpactedShips({ x: 0, y: 0 }, Target.newFromShip(ship2), ships), [ship2]);
- check.equals(action.filterImpactedShips({ x: 0, y: 0 }, Target.newFromLocation(10, 50), ships), []);
+ check.equals(action.filterImpactedShips(ship1, { x: 0, y: 0 }, Target.newFromShip(ship2), ships), [ship2]);
+ check.equals(action.filterImpactedShips(ship1, { x: 0, y: 0 }, Target.newFromLocation(10, 50), ships), []);
action = new TriggerAction("testaction", { range: 50, blast: 40 });
- check.equals(action.filterImpactedShips({ x: 0, y: 0 }, Target.newFromLocation(20, 20), ships), [ship1, ship3]);
+ check.equals(action.filterImpactedShips(ship1, { x: 0, y: 0 }, Target.newFromLocation(20, 20), ships), [ship1, ship3]);
action = new TriggerAction("testaction", { range: 100, angle: 30 });
- check.equals(action.filterImpactedShips({ x: 0, y: 51 }, Target.newFromLocation(30, 50), ships), [ship1, ship2]);
+ check.equals(action.filterImpactedShips(ship1, { x: 0, y: 51 }, Target.newFromLocation(30, 50), ships), [ship1, ship2]);
})
test.case("guesses targetting mode", check => {
diff --git a/src/core/actions/TriggerAction.ts b/src/core/actions/TriggerAction.ts
index 08eb9b5..b1467d6 100644
--- a/src/core/actions/TriggerAction.ts
+++ b/src/core/actions/TriggerAction.ts
@@ -90,22 +90,22 @@ module TK.SpaceTac {
return this.range;
}
- filterImpactedShips(source: ArenaLocation, target: Target, ships: Ship[]): Ship[] {
+ filterImpactedShips(ship: Ship, source: ArenaLocation, target: Target, ships: Ship[]): Ship[] {
if (this.blast) {
return ships.filter(ship => arenaDistance(ship.location, target) <= this.blast);
} else if (this.angle) {
let angle = arenaAngle(source, target);
let maxangle = (this.angle * 0.5) * Math.PI / 180;
- return ships.filter(ship => {
- let dist = arenaDistance(source, ship.location);
+ return ships.filter(iship => {
+ let dist = arenaDistance(source, iship.location);
if (dist < 0.000001 || dist > this.range) {
return false;
} else {
- return Math.abs(angularDifference(arenaAngle(source, ship.location), angle)) < maxangle;
+ return Math.abs(angularDifference(arenaAngle(source, iship.location), angle)) < maxangle;
}
});
} else {
- return ships.filter(ship => ship.is(target.ship_id));
+ return ships.filter(iship => iship.is(target.ship_id));
}
}
diff --git a/src/core/actions/VigilanceAction.spec.ts b/src/core/actions/VigilanceAction.spec.ts
new file mode 100644
index 0000000..341148d
--- /dev/null
+++ b/src/core/actions/VigilanceAction.spec.ts
@@ -0,0 +1,108 @@
+module TK.SpaceTac.Specs {
+ testing("VigilanceAction", test => {
+ test.case("configures", check => {
+ let ship = new Ship();
+ let action = new VigilanceAction("Reactive Fire", { power: 2, radius: 120 }, { intruder_count: 3 }, "reactfire");
+ ship.actions.addCustom(action);
+
+ check.equals(action.code, "reactfire");
+ check.equals(action.getPowerUsage(ship, null), 2);
+ check.equals(action.radius, 120);
+ check.equals(action.intruder_count, 3);
+ check.equals(action.getRangeRadius(ship), 0);
+ check.equals(action.getTargettingMode(ship), ActionTargettingMode.SURROUNDINGS);
+ check.equals(action.getVerb(ship), "Watch with");
+
+ ship.actions.toggle(action, true);
+ check.equals(action.getVerb(ship), "Stop");
+ check.equals(action.getPowerUsage(ship, null), -2);
+ check.equals(action.getTargettingMode(ship), ActionTargettingMode.SELF_CONFIRM);
+ });
+
+ test.case("builds a textual description", check => {
+ let action = new VigilanceAction("Reactive Fire", { power: 2, radius: 120 }, {
+ intruder_count: 0,
+ intruder_effects: [new ValueEffect("hull", -1)]
+ });
+ check.equals(action.getEffectsDescription(), "Watch a 120km area (power usage 2):\n• hull -1 for all incoming ships");
+
+ action = new VigilanceAction("Reactive Fire", { power: 2, radius: 120 }, {
+ intruder_count: 1,
+ intruder_effects: [new ValueEffect("hull", -1)]
+ });
+ check.equals(action.getEffectsDescription(), "Watch a 120km area (power usage 2):\n• hull -1 for the first incoming ship");
+
+ action = new VigilanceAction("Reactive Fire", { power: 2, radius: 120 }, {
+ intruder_count: 3,
+ intruder_effects: [new ValueEffect("hull", -1)]
+ });
+ check.equals(action.getEffectsDescription(), "Watch a 120km area (power usage 2):\n• hull -1 for the first 3 incoming ships");
+
+ action = new VigilanceAction("Reactive Fire", { power: 2, radius: 120, filter: ActionTargettingFilter.ALLIES }, {
+ intruder_count: 3,
+ intruder_effects: [new ValueEffect("hull", -1)]
+ });
+ check.equals(action.getEffectsDescription(), "Watch a 120km area (power usage 2):\n• hull -1 for the first 3 incoming team members");
+ });
+
+ test.case("handles the vigilance effect to know who to target", check => {
+ let battle = new Battle();
+ let ship1a = battle.fleets[0].addShip();
+ ship1a.setArenaPosition(0, 0);
+ TestTools.setShipModel(ship1a, 10, 0, 5);
+ let ship1b = battle.fleets[0].addShip();
+ ship1b.setArenaPosition(800, 0);
+ TestTools.setShipModel(ship1b, 10, 0, 5);
+ let ship2a = battle.fleets[1].addShip();
+ ship2a.setArenaPosition(800, 0);
+ TestTools.setShipModel(ship2a, 10, 0, 5);
+ let ship2b = battle.fleets[1].addShip();
+ ship2b.setArenaPosition(1200, 0);
+ TestTools.setShipModel(ship2b, 10, 0, 5);
+ let engine = ship2b.actions.addCustom(new MoveAction("Move", { distance_per_power: 1000, safety_distance: 100 }));
+
+ let action = ship1a.actions.addCustom(new VigilanceAction("Reactive Shot", { radius: 1000, filter: ActionTargettingFilter.ENEMIES }, {
+ intruder_effects: [new DamageEffect(1)]
+ }));
+
+ let diffs = action.getDiffs(ship1a, battle);
+ check.equals(diffs, [
+ new ShipActionUsedDiff(ship1a, action, Target.newFromShip(ship1a)),
+ new ShipValueDiff(ship1a, "power", -1),
+ new ShipActionToggleDiff(ship1a, action, true),
+ new ShipEffectAddedDiff(ship2a, action.effects[0])
+ ]);
+ battle.applyDiffs(diffs);
+
+ check.equals(ship1a.active_effects.list(), []);
+ check.equals(ship1b.active_effects.list(), []);
+ check.equals(ship2a.active_effects.list(), [action.effects[0]]);
+ check.equals(ship2b.active_effects.list(), []);
+
+ check.equals(ship1a.getValue("hull"), 10);
+ check.equals(ship1b.getValue("hull"), 10);
+ check.equals(ship2a.getValue("hull"), 10);
+ check.equals(ship2b.getValue("hull"), 10);
+
+ TestTools.setShipPlaying(battle, ship2b);
+ battle.applyOneAction(engine.id, Target.newFromLocation(500, 0));
+
+ check.equals(ship1a.active_effects.list(), []);
+ check.equals(ship1b.active_effects.list(), []);
+ check.equals(ship2a.active_effects.list(), [action.effects[0]]);
+ check.equals(ship2b.active_effects.list(), [action.effects[0]]);
+
+ check.equals(ship1a.getValue("hull"), 10);
+ check.equals(ship1b.getValue("hull"), 10);
+ check.equals(ship2a.getValue("hull"), 10);
+ check.equals(ship2b.getValue("hull"), 9);
+
+ battle.applyOneAction(engine.id, Target.newFromLocation(400, 0));
+ check.equals(ship2b.getValue("hull"), 9);
+
+ battle.applyOneAction(engine.id, Target.newFromLocation(1200, 0));
+ battle.applyOneAction(engine.id, Target.newFromLocation(700, 0));
+ check.equals(ship2b.getValue("hull"), 8);
+ });
+ });
+}
\ No newline at end of file
diff --git a/src/core/actions/VigilanceAction.ts b/src/core/actions/VigilanceAction.ts
new file mode 100644
index 0000000..fd6e69b
--- /dev/null
+++ b/src/core/actions/VigilanceAction.ts
@@ -0,0 +1,66 @@
+///
+
+module TK.SpaceTac {
+ /**
+ * Configuration of a vigilance action
+ */
+ export interface VigilanceActionConfig {
+ // Maximal number of trespassing ships before deactivating (0 for unlimited)
+ intruder_count: number
+ // Effects to be applied on ships entering the area
+ intruder_effects: BaseEffect[]
+ }
+
+ /**
+ * Action to watch the ship surroundings, and trigger specific effects on any ship that enters the area
+ */
+ export class VigilanceAction extends ToggleAction implements VigilanceActionConfig {
+ intruder_count = 1;
+ intruder_effects: BaseEffect[] = [];
+
+ constructor(name: string, toggle_config?: Partial, vigilance_config?: Partial, code?: string) {
+ super(name, toggle_config, code);
+
+ if (vigilance_config) {
+ this.configureVigilance(vigilance_config);
+ }
+ }
+
+ /**
+ * Configure the deployed drone
+ */
+ configureVigilance(config: Partial): void {
+ copyfields(config, this);
+ this.effects = [new VigilanceEffect(this)];
+ }
+
+ getVerb(ship: Ship): string {
+ return ship.actions.isToggled(this) ? "Stop" : "Watch with";
+ }
+
+ getSpecificDiffs(ship: Ship, battle: Battle, target: Target): BaseBattleDiff[] {
+ // Do not apply effects, only register the VigilanceEffect on the ships already in the area
+ let result = super.getSpecificDiffs(ship, battle, target, false);
+ return result;
+ }
+
+ getEffectsDescription(): string {
+ let desc = `Watch a ${this.radius}km area (power usage ${this.power})`;
+
+ let suffix: string;
+ if (this.intruder_count == 0) {
+ suffix = `for all incoming ${BaseAction.getFilterDesc(this.filter)}`;
+ } else if (this.intruder_count == 1) {
+ suffix = `for the first incoming ${BaseAction.getFilterDesc(this.filter, false)}`;
+ } else {
+ suffix = `for the first ${this.intruder_count} incoming ${BaseAction.getFilterDesc(this.filter)}`;
+ }
+
+ let effects = this.intruder_effects.map(effect => {
+ return "• " + effect.getDescription() + " " + suffix;
+ });
+
+ return `${desc}:\n${effects.join("\n")}`;
+ }
+ }
+}
diff --git a/src/core/ai/TacticalAIHelpers.spec.ts b/src/core/ai/TacticalAIHelpers.spec.ts
index 0660e4e..d004590 100644
--- a/src/core/ai/TacticalAIHelpers.spec.ts
+++ b/src/core/ai/TacticalAIHelpers.spec.ts
@@ -161,7 +161,6 @@ module TK.SpaceTac.Specs {
// no enemies hurt
let maneuver = new Maneuver(ship, action, Target.newFromLocation(100, 0));
- console.log(maneuver)
check.nears(TacticalAIHelpers.evaluateEnemyHealth(ship, battle, maneuver), 0, 8);
// one enemy loses half-life
diff --git a/src/core/diffs/VigilanceAppliedDiff.ts b/src/core/diffs/VigilanceAppliedDiff.ts
new file mode 100644
index 0000000..a8d5649
--- /dev/null
+++ b/src/core/diffs/VigilanceAppliedDiff.ts
@@ -0,0 +1,20 @@
+///
+
+module TK.SpaceTac {
+ /**
+ * A vigilance reaction has been triggered
+ *
+ * This does not do anything, and is just there for animations
+ */
+ export class VigilanceAppliedDiff extends BaseBattleShipDiff {
+ action: RObjectId
+ target: RObjectId
+
+ constructor(source: Ship, action: VigilanceAction, target: Ship) {
+ super(source);
+
+ this.action = action.id;
+ this.target = target.id;
+ }
+ }
+}
diff --git a/src/core/effects/BaseEffect.ts b/src/core/effects/BaseEffect.ts
index f799ede..f0b8a65 100644
--- a/src/core/effects/BaseEffect.ts
+++ b/src/core/effects/BaseEffect.ts
@@ -44,17 +44,30 @@ module TK.SpaceTac {
return [];
}
- // Return true if the effect is beneficial to the ship, false if it's a drawback
+ /**
+ * Return true if the effect is internal and should not be displayed to the players
+ */
+ isInternal(): boolean {
+ return false;
+ }
+
+ /**
+ * Return true if the effect is beneficial to the ship, false if it's a drawback
+ */
isBeneficial(): boolean {
return false;
}
- // Get a full code, that can be used to identify this effect (for example: "attrlimit-aprecovery")
+ /**
+ * Get a full code, that can be used to identify this effect (for example: "attrlimit-aprecovery")
+ */
getFullCode(): string {
return this.code;
}
- // Return a human readable description
+ /**
+ * Return a human readable description
+ */
getDescription(): string {
return "unknown effect";
}
diff --git a/src/core/effects/VigilanceEffect.spec.ts b/src/core/effects/VigilanceEffect.spec.ts
new file mode 100644
index 0000000..b64d4b3
--- /dev/null
+++ b/src/core/effects/VigilanceEffect.spec.ts
@@ -0,0 +1,25 @@
+module TK.SpaceTac.Specs {
+ testing("VigilanceEffect", test => {
+ test.case("applies vigilance effects on intruding ships", check => {
+ let battle = new Battle();
+ let source = battle.fleets[0].addShip();
+ let target = battle.fleets[1].addShip();
+
+ let action = source.actions.addCustom(new VigilanceAction("Reactive Shot"));
+ action.configureVigilance({ intruder_effects: [new DamageEffect(1)] });
+ let effect = new VigilanceEffect(action);
+
+ let diffs = effect.getOnDiffs(target, source);
+ check.equals(diffs, []);
+
+ TestTools.setShipModel(target, 10);
+
+ diffs = effect.getOnDiffs(target, source);
+ check.equals(diffs, [
+ new VigilanceAppliedDiff(source, action, target),
+ new ShipDamageDiff(target, 1, 0),
+ new ShipValueDiff(target, "hull", -1)
+ ]);
+ })
+ })
+}
diff --git a/src/core/effects/VigilanceEffect.ts b/src/core/effects/VigilanceEffect.ts
new file mode 100644
index 0000000..a3c57fe
--- /dev/null
+++ b/src/core/effects/VigilanceEffect.ts
@@ -0,0 +1,36 @@
+///
+
+module TK.SpaceTac {
+ /**
+ * Apply vigilance effects on a ship that enters a vigilance area
+ */
+ export class VigilanceEffect extends BaseEffect {
+ constructor(private action: VigilanceAction) {
+ super("vigilance");
+ }
+
+ getOnDiffs(ship: Ship, source: Ship | Drone): BaseBattleDiff[] {
+ if (source instanceof Ship) {
+ let result = flatten(this.action.intruder_effects.map(effect => effect.getOnDiffs(ship, source)));
+ if (result.length > 0) {
+ result.unshift(new VigilanceAppliedDiff(source, this.action, ship));
+ }
+ return result;
+ } else {
+ return [];
+ }
+ }
+
+ isInternal(): boolean {
+ return true;
+ }
+
+ isBeneficial(): boolean {
+ return false;
+ }
+
+ getDescription(): string {
+ return `Vigilance from ${this.action.name}`;
+ }
+ }
+}
diff --git a/src/core/missions/Mission.spec.ts b/src/core/missions/Mission.spec.ts
index fd93c20..627c1e4 100644
--- a/src/core/missions/Mission.spec.ts
+++ b/src/core/missions/Mission.spec.ts
@@ -13,7 +13,7 @@ module TK.SpaceTac.Specs {
check.equals(result, true);
check.same(mission.current_part, mission.parts[0]);
- check.patch(mission.parts[0], "checkCompleted", iterator([false, true]));
+ check.patch(mission.parts[0], "checkCompleted", nnf(true, iterator([false, true])));
result = mission.checkStatus();
check.equals(result, true);
diff --git a/src/core/models/ModelBreeze.ts b/src/core/models/ModelBreeze.ts
index 0245fac..ec88472 100644
--- a/src/core/models/ModelBreeze.ts
+++ b/src/core/models/ModelBreeze.ts
@@ -23,7 +23,7 @@ module TK.SpaceTac {
power: 2,
range: 200,
}, "gatlinggun");
- gatling.configureCooldown(3, 1);
+ gatling.configureCooldown(2, 1);
let shield_steal = new TriggerAction("Shield Steal", {
effects: [new ValueTransferEffect("shield", -1)],
diff --git a/src/core/models/ModelCommodore.ts b/src/core/models/ModelCommodore.ts
index cb4bcb2..eadae10 100644
--- a/src/core/models/ModelCommodore.ts
+++ b/src/core/models/ModelCommodore.ts
@@ -23,6 +23,11 @@ module TK.SpaceTac {
}, "prokhorovlaser");
laser.configureCooldown(3, 1);
+ let interceptors = new VigilanceAction("Interceptors Field", { radius: 200, power: 3, filter: ActionTargettingFilter.ENEMIES }, {
+ intruder_count: 1,
+ intruder_effects: [new DamageEffect(4, DamageEffectMode.SHIELD_THEN_HULL)]
+ }, "interceptors");
+
let power_steal = new TriggerAction("Power Thief", {
effects: [new ValueTransferEffect("power", -1)],
power: 1,
@@ -52,6 +57,10 @@ module TK.SpaceTac {
code: "Power Thief",
actions: [power_steal]
},
+ {
+ code: "Interceptors Field",
+ actions: [interceptors]
+ },
];
} else {
return this.getStandardUpgrades(level);
diff --git a/src/core/models/ModelCreeper.ts b/src/core/models/ModelCreeper.ts
index cfc3097..affc6e8 100644
--- a/src/core/models/ModelCreeper.ts
+++ b/src/core/models/ModelCreeper.ts
@@ -30,11 +30,11 @@ module TK.SpaceTac {
}, "gravitshield");
repulse.configureCooldown(1, 1);
- let repairdrone = new DeployDroneAction("Repair Drone", { power: 3 }, {
+ let repairdrone = new DeployDroneAction("Repair Drone", { power: 3, filter: ActionTargettingFilter.ALLIES }, {
deploy_distance: 300,
drone_radius: 150,
drone_effects: [
- new ValueEffect("hull", undefined, undefined, undefined, 1)
+ new ValueEffect("hull", 0, 0, 0, 1)
]
}, "repairdrone");
diff --git a/src/core/models/ModelFalcon.ts b/src/core/models/ModelFalcon.ts
index 167945f..3c0859d 100644
--- a/src/core/models/ModelFalcon.ts
+++ b/src/core/models/ModelFalcon.ts
@@ -23,6 +23,7 @@ module TK.SpaceTac {
}, "submunitionmissile");
missile.configureCooldown(2, 2);
+ // TODO targetting enemies only
let gatling = new TriggerAction("Multi-head Gatling", {
effects: [new DamageEffect(2)],
power: 2,
diff --git a/src/core/models/ModelTomahawk.ts b/src/core/models/ModelTomahawk.ts
index 1f24060..ad849e9 100644
--- a/src/core/models/ModelTomahawk.ts
+++ b/src/core/models/ModelTomahawk.ts
@@ -13,34 +13,27 @@ module TK.SpaceTac {
getLevelUpgrades(level: number): ShipUpgrade[] {
if (level == 1) {
let engine = new MoveAction("Engine", {
- distance_per_power: 160,
+ distance_per_power: 120,
});
let gatling1 = new TriggerAction("Primary Gatling", {
effects: [new DamageEffect(3)],
power: 2, range: 400
}, "gatlinggun");
- gatling1.configureCooldown(1, 1);
+ gatling1.configureCooldown(1, 2);
let gatling2 = new TriggerAction("Secondary Gatling", {
effects: [new DamageEffect(2)],
power: 1, range: 200
}, "gatlinggun");
- gatling2.configureCooldown(1, 1);
+ gatling2.configureCooldown(1, 2);
let missile = new TriggerAction("Diffuse Missiles", {
effects: [new DamageEffect(2)],
power: 2,
range: 200, blast: 100,
}, "submunitionmissile");
- missile.configureCooldown(1, 1);
-
- let laser = new TriggerAction("Low-power Laser", {
- effects: [new DamageEffect(2)],
- power: 2,
- range: 200, angle: 30
- }, "prokhorovlaser");
- laser.configureCooldown(1, 1);
+ missile.configureCooldown(1, 2);
let cooler = new TriggerAction("Circuits Cooler", {
effects: [new CooldownEffect(1, 1)],
@@ -54,7 +47,7 @@ module TK.SpaceTac {
new AttributeEffect("initiative", 2),
new AttributeEffect("hull_capacity", 2),
new AttributeEffect("shield_capacity", 1),
- new AttributeEffect("power_capacity", 7),
+ new AttributeEffect("power_capacity", 5),
]
},
{
@@ -73,10 +66,6 @@ module TK.SpaceTac {
code: "SubMunition Missile",
actions: [missile]
},
- {
- code: "Laser",
- actions: [laser]
- },
{
code: "Cooler",
actions: [cooler]
diff --git a/src/core/models/ModelTrapper.ts b/src/core/models/ModelTrapper.ts
index 7e79a58..17ef046 100644
--- a/src/core/models/ModelTrapper.ts
+++ b/src/core/models/ModelTrapper.ts
@@ -18,9 +18,10 @@ module TK.SpaceTac {
engine.configureCooldown(1, 1);
let protector = new ToggleAction("Damage Protector", {
- power: 3,
+ power: 4,
radius: 300,
- effects: [new AttributeEffect("evasion", 1)]
+ effects: [new AttributeEffect("evasion", 1)],
+ filter: ActionTargettingFilter.ALLIES
});
let depleter = new TriggerAction("Power Depleter", {
diff --git a/src/core/models/ShipModel.spec.ts b/src/core/models/ShipModel.spec.ts
index 4650e03..63242d2 100644
--- a/src/core/models/ShipModel.spec.ts
+++ b/src/core/models/ShipModel.spec.ts
@@ -2,13 +2,12 @@ module TK.SpaceTac.Specs {
testing("ShipModel", test => {
test.case("picks random models from default collection", check => {
check.patch(console, "error", null);
- check.patch(ShipModel, "getDefaultCollection", iterator([
+ check.patch(ShipModel, "getDefaultCollection", nnf([], iterator([
[new ShipModel("a")],
[],
[new ShipModel("a"), new ShipModel("b")],
[new ShipModel("a")],
- [],
- ]));
+ ])));
check.equals(ShipModel.getRandomModel(), new ShipModel("a"), "pick from a one-item list");
check.equals(ShipModel.getRandomModel(), new ShipModel(), "pick from an empty list");
diff --git a/src/multi/Connection.spec.ts b/src/multi/Connection.spec.ts
index 020220d..a86f9e9 100644
--- a/src/multi/Connection.spec.ts
+++ b/src/multi/Connection.spec.ts
@@ -9,7 +9,7 @@ module TK.SpaceTac.Multi.Specs {
await storage.upsert("sessioninfo", { token: token }, {});
- check.patch(connection, "generateToken", iterator([token, "123456"]));
+ check.patch(connection, "generateToken", nnf("", iterator([token, "123456"])));
let other = await connection.getUnusedToken(5);
check.equals(other, "123456");
diff --git a/src/multi/Exchange.spec.ts b/src/multi/Exchange.spec.ts
index 699a433..c6a82b1 100644
--- a/src/multi/Exchange.spec.ts
+++ b/src/multi/Exchange.spec.ts
@@ -19,8 +19,8 @@ module TK.SpaceTac.Multi.Specs {
test.acase("says hello on start", async check => {
let [storage, peer1, peer2] = newExchange("abc");
- check.patch(peer1, "getNextId", iterator(["1A", "1B", "1C"]));
- check.patch(peer2, "getNextId", iterator(["2A", "2B", "2C"]));
+ check.patch(peer1, "getNextId", nnf("", iterator(["1A", "1B", "1C"])));
+ check.patch(peer2, "getNextId", nnf("", iterator(["2A", "2B", "2C"])));
check.equals(peer1.next, "hello");
check.equals(peer2.next, "hello");
@@ -49,8 +49,8 @@ module TK.SpaceTac.Multi.Specs {
// same peers, new message chain
[storage, peer1, peer2] = newExchange("abc", storage);
- check.patch(peer1, "getNextId", iterator(["1R", "1S", "1T"]));
- check.patch(peer2, "getNextId", iterator(["2R", "2S", "2T"]));
+ check.patch(peer1, "getNextId", nnf("", iterator(["1R", "1S", "1T"])));
+ check.patch(peer2, "getNextId", nnf("", iterator(["2R", "2S", "2T"])));
await Promise.all([peer1.start(), peer2.start()]);
diff --git a/src/ui/battle/ArenaDrone.ts b/src/ui/battle/ArenaDrone.ts
index 3ed4639..c6bec41 100644
--- a/src/ui/battle/ArenaDrone.ts
+++ b/src/ui/battle/ArenaDrone.ts
@@ -25,8 +25,8 @@ module TK.SpaceTac.UI {
this.drone = drone;
this.radius = new Phaser.Graphics(this.game, 0, 0);
- this.radius.lineStyle(2, 0xe9f2f9, 0.3);
- this.radius.beginFill(0xe9f2f9, 0.0);
+ this.radius.lineStyle(2, 0xe9f2f9, 0.5);
+ this.radius.beginFill(0xe9f2f9, 0.1);
this.radius.drawCircle(0, 0, drone.radius * 2);
this.radius.endFill();
this.add(this.radius);
diff --git a/src/ui/battle/ArenaShip.ts b/src/ui/battle/ArenaShip.ts
index 5f7db3a..a0bcd36 100644
--- a/src/ui/battle/ArenaShip.ts
+++ b/src/ui/battle/ArenaShip.ts
@@ -263,6 +263,16 @@ module TK.SpaceTac.UI {
}
}
};
+ } else if (diff instanceof VigilanceAppliedDiff) {
+ let action = this.ship.actions.getById(diff.action);
+ return {
+ foreground: async (animate, timer) => {
+ if (animate && action) {
+ await this.displayEffect(`${action.name} (vigilance)`, true);
+ await timer.sleep(300);
+ }
+ }
+ }
} else {
return {};
}
@@ -386,7 +396,7 @@ module TK.SpaceTac.UI {
updateActiveEffects() {
this.active_effects_display.removeAll();
- let effects = this.ship.active_effects.list();
+ let effects = this.ship.active_effects.list().filter(effect => !effect.isInternal());
let count = effects.length;
if (count) {
@@ -407,8 +417,9 @@ module TK.SpaceTac.UI {
updateEffectsRadius(): void {
this.effects_radius.clear();
this.ship.actions.listToggled().forEach(action => {
- this.effects_radius.lineStyle(2, 0xe9f2f9, 0.3);
- this.effects_radius.beginFill(0xe9f2f9, 0.0);
+ let color = (action instanceof VigilanceAction) ? 0xf4bf42 : 0xe9f2f9;
+ this.effects_radius.lineStyle(2, color, 0.5);
+ this.effects_radius.beginFill(color, 0.1);
this.effects_radius.drawCircle(0, 0, action.radius * 2);
this.effects_radius.endFill();
});
diff --git a/src/ui/battle/ShipTooltip.ts b/src/ui/battle/ShipTooltip.ts
index ac97414..84b1947 100644
--- a/src/ui/battle/ShipTooltip.ts
+++ b/src/ui/battle/ShipTooltip.ts
@@ -53,8 +53,10 @@ module TK.SpaceTac.UI {
});
ship.active_effects.list().forEach(effect => {
- builder.text(`• ${effect.getDescription()}`, 0, iy, { color: effect.isBeneficial() ? "#afe9c6" : "#e9afaf" });
- iy += 32;
+ if (!effect.isInternal()) {
+ builder.text(`• ${effect.getDescription()}`, 0, iy, { color: effect.isBeneficial() ? "#afe9c6" : "#e9afaf" });
+ iy += 32;
+ }
});
builder.text(ship.model.getDescription(), 0, iy + 4, { size: 14, color: "#999999", width: 540 });
diff --git a/src/ui/battle/Targetting.spec.ts b/src/ui/battle/Targetting.spec.ts
index cc4af9e..752cc98 100644
--- a/src/ui/battle/Targetting.spec.ts
+++ b/src/ui/battle/Targetting.spec.ts
@@ -50,11 +50,11 @@ module TK.SpaceTac.UI.Specs {
let impacts = targetting.impact_indicators;
let action = new TriggerAction("weapon", { range: 50 });
- let collect = check.patch(action, "getImpactedShips", iterator([
+ let collect = check.patch(action, "getImpactedShips", nnf([], iterator([
[new Ship(), new Ship(), new Ship()],
[new Ship(), new Ship()],
[]
- ]));
+ ])));
targetting.updateImpactIndicators(impacts, ship, action, new Target(20, 10));
check.called(collect, [
@@ -159,8 +159,12 @@ module TK.SpaceTac.UI.Specs {
let move = TestTools.addEngine(ship, 100);
let fire = TestTools.addWeapon(ship, 50, 2, 300, 100);
let last_call: any = null;
- check.patch(targetting.range_hint, "clear", () => last_call = null);
- check.patch(targetting.range_hint, "update", (ship: Ship, action: BaseAction, radius: number) => last_call = [ship, action, radius]);
+ check.patch(targetting.range_hint, "clear", () => {
+ last_call = null;
+ });
+ check.patch(targetting.range_hint, "update", (ship: Ship, action: BaseAction, radius: number) => {
+ last_call = [ship, action, radius];
+ });
// move action
targetting.setAction(ship, move);
diff --git a/src/ui/common/InputManager.spec.ts b/src/ui/common/InputManager.spec.ts
index 4f72342..2d3e2c3 100644
--- a/src/ui/common/InputManager.spec.ts
+++ b/src/ui/common/InputManager.spec.ts
@@ -7,7 +7,7 @@ module TK.SpaceTac.UI.Specs {
let inputs = testgame.view.inputs;
let pointer = new Phaser.Pointer(testgame.ui, 0);
- function newButton(): [Phaser.Button, { enter: Mock, leave: Mock, click: Mock }] {
+ function newButton(): [Phaser.Button, { enter: Mock, leave: Mock, click: Mock }] {
let button = new Phaser.Button(testgame.ui);
let mocks = {
enter: check.mockfunc("enter"),
diff --git a/src/ui/common/Tooltip.spec.ts b/src/ui/common/Tooltip.spec.ts
index 185d1f8..e5e91f9 100644
--- a/src/ui/common/Tooltip.spec.ts
+++ b/src/ui/common/Tooltip.spec.ts
@@ -5,13 +5,13 @@ module TK.SpaceTac.UI.Specs {
test.case("shows near the hovered button", check => {
let button = testgame.view.add.button();
- check.patch(button, "getBounds", () => ({ x: 100, y: 50, width: 50, height: 25 }));
+ check.patch(button, "getBounds", () => new PIXI.Rectangle(100, 50, 50, 25));
let tooltip = new Tooltip(testgame.view);
tooltip.bind(button, filler => true);
let container = (tooltip).container;
- check.patch((container).content, "getBounds", () => ({ x: 0, y: 0, width: 32, height: 32 }));
+ check.patch((container).content, "getBounds", () => new PIXI.Rectangle(0, 0, 32, 32));
check.equals(container.visible, false);
button.onInputOver.dispatch();
diff --git a/src/ui/common/UIBuilder.spec.ts b/src/ui/common/UIBuilder.spec.ts
index 10b7d5a..80d7511 100644
--- a/src/ui/common/UIBuilder.spec.ts
+++ b/src/ui/common/UIBuilder.spec.ts
@@ -295,13 +295,13 @@ module TK.SpaceTac.UI.Specs {
check.patch(UITools, "getScreenBounds", (obj: any) => {
if (obj === c1) {
- return { width: 100, height: 51 };
+ return { x: 0, y: 0, width: 100, height: 51 };
} else if (obj === c2) {
- return { width: 20, height: 7 };
+ return { x: 0, y: 0, width: 20, height: 7 };
} else if (obj === c3) {
- return { width: 60, height: 11 };
+ return { x: 0, y: 0, width: 60, height: 11 };
} else {
- return { width: 0, height: 0 };
+ return { x: 0, y: 0, width: 0, height: 0 };
}
});