1
0
Fork 0

Added vigilance system

This commit is contained in:
Michaël Lemaire 2018-03-30 00:57:53 +02:00
parent b5ee5314f7
commit 7bc6378754
43 changed files with 1379 additions and 623 deletions

View File

@ -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

Binary file not shown.

After

Width:  |  Height:  |  Size: 65 KiB

View File

@ -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

File diff suppressed because it is too large Load Diff

Before

Width:  |  Height:  |  Size: 144 KiB

After

Width:  |  Height:  |  Size: 156 KiB

6
package-lock.json generated
View File

@ -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": {

View File

@ -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": {

@ -1 +1 @@
Subproject commit 0fcd953719ed8dbaab92cfaf525a2e34078b069a
Subproject commit fc4bde326c2dcb4be03380ac29bac8d12b015821

View File

@ -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");
});

View File

@ -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<BaseEffect> {
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));
}
/**

View File

@ -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");
});
})
})
}

View File

@ -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)));
}
}
}

View File

@ -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<BaseEffect> {
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);
}
/**

View File

@ -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");
});
});
}

View File

@ -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
*

View File

@ -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")}`;

View File

@ -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)],

View File

@ -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<ToggleActionConfig>, 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")}`;

View File

@ -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 => {

View File

@ -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));
}
}

View File

@ -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);
});
});
}

View File

@ -0,0 +1,66 @@
/// <reference path="ToggleAction.ts"/>
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<ToggleActionConfig>, vigilance_config?: Partial<VigilanceActionConfig>, code?: string) {
super(name, toggle_config, code);
if (vigilance_config) {
this.configureVigilance(vigilance_config);
}
}
/**
* Configure the deployed drone
*/
configureVigilance(config: Partial<VigilanceActionConfig>): 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")}`;
}
}
}

View File

@ -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

View File

@ -0,0 +1,20 @@
/// <reference path="BaseBattleDiff.ts"/>
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;
}
}
}

View File

@ -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";
}

View File

@ -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)
]);
})
})
}

View File

@ -0,0 +1,36 @@
/// <reference path="BaseEffect.ts"/>
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}`;
}
}
}

View File

@ -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);

View File

@ -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)],

View File

@ -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);

View File

@ -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");

View File

@ -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,

View File

@ -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]

View File

@ -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", {

View File

@ -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");

View File

@ -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");

View File

@ -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()]);

View File

@ -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);

View File

@ -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();
});

View File

@ -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 });

View File

@ -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);

View File

@ -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<Function>, leave: Mock<Function>, click: Mock<Function> }] {
let button = new Phaser.Button(testgame.ui);
let mocks = {
enter: check.mockfunc("enter"),

View File

@ -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 = <Phaser.Group>(<any>tooltip).container;
check.patch((<any>container).content, "getBounds", () => ({ x: 0, y: 0, width: 32, height: 32 }));
check.patch((<any>container).content, "getBounds", () => new PIXI.Rectangle(0, 0, 32, 32));
check.equals(container.visible, false);
button.onInputOver.dispatch();

View File

@ -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 };
}
});