1
0
Fork 0

Use DiffLog to apply revertable diffs to battle state

This commit is contained in:
Michaël Lemaire 2017-11-14 01:07:06 +01:00
parent 046c476c0a
commit 8b0ff9ee27
150 changed files with 3433 additions and 3355 deletions

View file

@ -41,12 +41,9 @@ Battle
* Fix arena's ship hovering happening even when the character sheet is open on top
* Add a voluntary retreat option
* Add scroll buttons when there are too many actions
* Add quick animation of playing ship indicator, on ship change
* Toggle bar/text display in power section of action bar
* Display effects description instead of attribute changes
* End the battle as soon as victory or defeat condition is detected (do not wait for the turn to end)
* Show a cooldown indicator on move action icon, if the simulation would cause the engine to overheat
* Any displayed info should be based on a ship copy stored in ArenaShip, and in sync with current log index (not the game state ship)
* Add engine trail effect, and sound
* Allow to skip animations, and allow no animation mode
* Find incentives to move from starting position (permanent drones or anomalies?)
@ -64,6 +61,7 @@ Battle
* Fix delay of shield/hull impact effects (should depend on weapon animation, and ship location)
* Indicate visually the power gain of "end turn"
* Add a turn count marker in the ship list
* BattleChecks should be done proactively when all diffs have been simulated by an action, in addition to reactively after applying
Ships models and equipments
---------------------------
@ -79,11 +77,12 @@ Ships models and equipments
* RepelEffect should apply on ships in a good order (distance decreasing)
* Add hull points to drones and make them take area damage
* Quality modifiers should be based on an "quality diff" to reach
* Drones effects should be classified: permanent effects apply permanently, ponctual effects may be applied by an owner's action (if in range)
Artificial Intelligence
-----------------------
* Work on a simple representation of battle state, simulating effects on it, evaluating it, and only reevaluating parts that changed
* Evaluate diffs instead of effects
* Use a first batch of producers, and only if no "good" move has been found, go on with some infinite producers
* Abandon fight if the AI judges there is no hope of victory
* Add combination of random small move and actual maneuver, as producer
@ -125,7 +124,6 @@ Postponed
* Multiplayer/co-op
* Formation or deployment phase
* Add ship personality (with icons to identify?), with reaction dialogs
* New battle internal flow: any game state change should be done through revertable events
* Hide enemy information (shield, hull, weapons), until they are in play, or until a "spy" effect is used
* Invocation/reinforcements (need to up the 10 ships limit)
* Dynamic music composition

View file

@ -21,24 +21,24 @@
"author": "Michael Lemaire",
"license": "MIT",
"devDependencies": {
"@types/jasmine": "^2.6.0",
"babel-polyfill": "^6.26.0",
"codecov": "^2.3.0",
"@types/jasmine": "2.6.2",
"babel-polyfill": "6.26.0",
"codecov": "2.3.0",
"gamefroot-texture-packer": "Gamefroot/Gamefroot-Texture-Packer.git#f3687111afc94f80ea8f2877c188fb8e2004e8ff",
"jasmine": "^2.8.0",
"karma": "^1.7.0",
"karma-coverage": "^1.1.1",
"karma-jasmine": "^1.1.0",
"karma-phantomjs-launcher": "^1.0.4",
"karma-spec-reporter": "^0.0.31",
"live-server": "^1.2.0",
"remap-istanbul": "^0.9.5",
"typescript": "^2.5.3"
"jasmine": "2.5.2",
"karma": "1.7.1",
"karma-coverage": "1.1.1",
"karma-jasmine": "1.1.0",
"karma-phantomjs-launcher": "1.0.4",
"karma-spec-reporter": "0.0.31",
"live-server": "1.2.0",
"remap-istanbul": "0.9.5",
"typescript": "2.5.3"
},
"dependencies": {
"jasmine-core": "^2.8.0",
"parse": "^1.9.2",
"phaser": "^2.6.2",
"phaser-plugin-scene-graph": "^1.0.4"
"jasmine-core": "2.5.2",
"parse": "1.9.2",
"phaser": "2.6.2",
"phaser-plugin-scene-graph": "1.0.4"
}
}
}

@ -1 +1 @@
Subproject commit 639fcb0ac311767b413067f6854f6c2aecd41e5a
Subproject commit c1aa0e6263e9202fefc839bd97cbf16563cdca51

View file

@ -5,15 +5,15 @@ module TK.SpaceTac {
var fleet2 = new Fleet();
var ship1 = new Ship(fleet1, "F1S1");
ship1.setAttribute("maneuvrability", 2);
TestTools.setAttribute(ship1, "maneuvrability", 2);
var ship2 = new Ship(fleet1, "F1S2");
ship2.setAttribute("maneuvrability", 4);
TestTools.setAttribute(ship2, "maneuvrability", 4);
var ship3 = new Ship(fleet1, "F1S3");
ship3.setAttribute("maneuvrability", 1);
TestTools.setAttribute(ship3, "maneuvrability", 1);
var ship4 = new Ship(fleet2, "F2S1");
ship4.setAttribute("maneuvrability", 8);
TestTools.setAttribute(ship4, "maneuvrability", 8);
var ship5 = new Ship(fleet2, "F2S2");
ship5.setAttribute("maneuvrability", 2);
TestTools.setAttribute(ship5, "maneuvrability", 2);
var battle = new Battle(fleet1, fleet2);
check.equals(battle.play_order.length, 0);
@ -68,6 +68,7 @@ module TK.SpaceTac {
var ship3 = new Ship(fleet2, "ship3");
var battle = new Battle(fleet1, fleet2);
battle.ships.list().forEach(ship => TestTools.setShipHP(ship, 10, 0));
// Check empty play_order case
check.equals(battle.playing_ship, null);
@ -75,7 +76,7 @@ module TK.SpaceTac {
check.equals(battle.playing_ship, null);
// Force play order
iforeach(battle.iships(), ship => ship.setAttribute("maneuvrability", 1));
iforeach(battle.iships(), ship => TestTools.setAttribute(ship, "maneuvrability", 1));
var gen = new SkewedRandomGenerator([0.1, 0.2, 0.0]);
battle.throwInitiative(gen);
check.equals(battle.playing_ship, null);
@ -103,69 +104,32 @@ module TK.SpaceTac {
check.same(battle.playing_ship, ship2);
});
test.case("calls startTurn on ships", check => {
var fleet1 = new Fleet();
var fleet2 = new Fleet();
var ship1 = new Ship(fleet1, "F1S1");
var ship2 = new Ship(fleet1, "F1S2");
var ship3 = new Ship(fleet2, "F2S1");
var battle = new Battle(fleet1, fleet2);
let mock1 = check.patch(ship1, "startTurn");
let mock2 = check.patch(ship2, "startTurn");
let mock3 = check.patch(ship3, "startTurn");
// Force play order
var gen = new SkewedRandomGenerator([0.3, 0.2, 0.1]);
battle.throwInitiative(gen);
battle.advanceToNextShip();
check.called(mock1, 1);
check.called(mock2, 0);
check.called(mock3, 0);
battle.advanceToNextShip();
check.called(mock1, 0);
check.called(mock2, 1);
check.called(mock3, 0);
battle.advanceToNextShip();
check.called(mock1, 0);
check.called(mock2, 0);
check.called(mock3, 1);
battle.advanceToNextShip();
check.called(mock1, 1);
check.called(mock2, 0);
check.called(mock3, 0);
});
test.case("detects victory condition and logs a final EndBattleEvent", check => {
var fleet1 = new Fleet();
var fleet2 = new Fleet();
var ship1 = new Ship(fleet1, "F1S1");
var ship2 = new Ship(fleet1, "F1S2");
new Ship(fleet2, "F2S1");
let ship3 = new Ship(fleet2, "F2S1");
var battle = new Battle(fleet1, fleet2);
battle.ships.list().forEach(ship => TestTools.setShipHP(ship, 10, 0));
battle.start();
battle.play_order = [ship3, ship2, ship1];
check.equals(battle.ended, false);
ship1.setDead();
ship2.setDead();
battle.log.clear();
battle.advanceToNextShip();
check.equals(battle.ended, true);
check.equals(battle.log.events.length, 1);
check.equals(battle.log.events[0].code, "endbattle");
check.notequals((<EndBattleEvent>battle.log.events[0]).outcome.winner, null);
check.same((<EndBattleEvent>battle.log.events[0]).outcome.winner, fleet2);
let diff = battle.log.get(battle.log.count() - 1);
if (diff instanceof EndBattleDiff) {
check.notequals(diff.outcome.winner, null);
check.same(diff.outcome.winner, fleet2);
} else {
check.fail("Not an EndBattleDiff");
}
});
test.case("wear down equipment at the end of battle", check => {
@ -180,6 +144,7 @@ module TK.SpaceTac {
let eng2a = TestTools.addEngine(ship2a, 50);
let battle = new Battle(fleet1, fleet2);
battle.ships.list().forEach(ship => TestTools.setShipHP(ship, 10, 0));
battle.start();
check.equals(equ1a.wear, 0);
@ -220,12 +185,17 @@ module TK.SpaceTac {
ship3.setDead();
battle.log.clear();
battle.advanceToNextShip();
check.equals(battle.ended, false);
battle.performChecks();
check.equals(battle.ended, true);
check.equals(battle.log.events.length, 1);
check.equals(battle.log.events[0].code, "endbattle");
check.equals((<EndBattleEvent>battle.log.events[0]).outcome.winner, null);
check.equals(battle.log.count(), 1);
let diff = battle.log.get(0);
if (diff instanceof EndBattleDiff) {
check.equals(diff.outcome.winner, null);
} else {
check.fail("Not an EndBattleDiff");
}
});
test.case("collects ships present in a circle", check => {
@ -250,35 +220,20 @@ module TK.SpaceTac {
let battle = new Battle();
let ship = new Ship();
let drone = new Drone(ship);
check.equals(battle.drones, []);
check.equals(battle.log.events, []);
check.equals(battle.drones.count(), 0);
battle.addDrone(drone);
check.equals(battle.drones, [drone]);
check.equals(battle.log.events, [new DroneDeployedEvent(drone)]);
check.equals(battle.drones.count(), 1);
check.same(battle.drones.get(drone.id), drone);
battle.addDrone(drone);
check.equals(battle.drones, [drone]);
check.equals(battle.log.events, [new DroneDeployedEvent(drone)]);
check.equals(battle.drones.count(), 1);
battle.removeDrone(drone);
check.equals(battle.drones, []);
check.equals(battle.log.events, [new DroneDeployedEvent(drone), new DroneDestroyedEvent(drone)]);
check.equals(battle.drones.count(), 0);
battle.removeDrone(drone);
check.equals(battle.drones, []);
check.equals(battle.log.events, [new DroneDeployedEvent(drone), new DroneDestroyedEvent(drone)]);
// check initial log fill
battle.drones = [drone];
let expected = new DroneDeployedEvent(drone);
expected.initial = true;
check.equals(battle.getBootstrapEvents(), [expected]);
check.equals(battle.drones.count(), 0);
});
test.case("checks if a player is able to play", check => {
@ -298,34 +253,41 @@ module TK.SpaceTac {
});
test.case("gets the number of turns before a specific ship plays", check => {
let battle = new Battle();
check.patch(battle, "checkEndBattle", () => false);
battle.play_order = [new Ship(), new Ship(), new Ship()];
battle.advanceToNextShip();
let battle = TestTools.createBattle(2, 1);
check.same(battle.playing_ship, battle.play_order[0]);
check.equals(battle.getPlayOrder(battle.play_order[0]), 0);
check.equals(battle.getPlayOrder(battle.play_order[1]), 1);
check.equals(battle.getPlayOrder(battle.play_order[2]), 2);
check.in("initial", check => {
check.same(battle.playing_ship, battle.play_order[0], "first ship playing");
check.equals(battle.getPlayOrder(battle.play_order[0]), 0);
check.equals(battle.getPlayOrder(battle.play_order[1]), 1);
check.equals(battle.getPlayOrder(battle.play_order[2]), 2);
});
battle.advanceToNextShip();
check.same(battle.playing_ship, battle.play_order[1]);
check.equals(battle.getPlayOrder(battle.play_order[0]), 2);
check.equals(battle.getPlayOrder(battle.play_order[1]), 0);
check.equals(battle.getPlayOrder(battle.play_order[2]), 1);
check.in("1 step", check => {
check.same(battle.playing_ship, battle.play_order[1], "second ship playing");
check.equals(battle.getPlayOrder(battle.play_order[0]), 2);
check.equals(battle.getPlayOrder(battle.play_order[1]), 0);
check.equals(battle.getPlayOrder(battle.play_order[2]), 1);
});
battle.advanceToNextShip();
check.equals(battle.getPlayOrder(battle.play_order[0]), 1);
check.equals(battle.getPlayOrder(battle.play_order[1]), 2);
check.equals(battle.getPlayOrder(battle.play_order[2]), 0);
check.in("2 steps", check => {
check.same(battle.playing_ship, battle.play_order[2], "third ship playing");
check.equals(battle.getPlayOrder(battle.play_order[0]), 1);
check.equals(battle.getPlayOrder(battle.play_order[1]), 2);
check.equals(battle.getPlayOrder(battle.play_order[2]), 0);
});
battle.advanceToNextShip();
check.equals(battle.getPlayOrder(battle.play_order[0]), 0);
check.equals(battle.getPlayOrder(battle.play_order[1]), 1);
check.equals(battle.getPlayOrder(battle.play_order[2]), 2);
check.in("3 steps", check => {
check.same(battle.playing_ship, battle.play_order[0], "first ship playing");
check.equals(battle.getPlayOrder(battle.play_order[0]), 0);
check.equals(battle.getPlayOrder(battle.play_order[1]), 1);
check.equals(battle.getPlayOrder(battle.play_order[2]), 2);
});
});
test.case("lists area effects", check => {
@ -347,9 +309,7 @@ module TK.SpaceTac {
drone2.effects = [new DamageEffect(14)];
battle.addDrone(drone2);
check.equals(imaterialize(battle.iAreaEffects(100, 50)), [
new DamageEffect(12)
]);
check.equals(imaterialize(battle.iAreaEffects(100, 50)), [drone1.effects[0]]);
let eq1 = ship.addSlot(SlotType.Weapon).attach(new Equipment(SlotType.Weapon));
eq1.action = new ToggleAction(eq1, 0, 500, [new AttributeEffect("maneuvrability", 1)]);
@ -362,7 +322,8 @@ module TK.SpaceTac {
(<ToggleAction>eq3.action).activated = true;
check.equals(imaterialize(battle.iAreaEffects(100, 50)), [
new DamageEffect(12), new AttributeEffect("maneuvrability", 1)
drone1.effects[0],
(<ToggleAction>eq1.action).effects[0],
]);
});
@ -379,5 +340,43 @@ module TK.SpaceTac {
battle.ai_playing = false;
check.equals(loaded, battle);
});
test.case("can revert the last action", check => {
let battle = new Battle();
let ship = battle.fleets[0].addShip();
ship.setValue("hull", 13);
battle.log.clear();
battle.log.add(new ShipValueDiff(ship, "hull", 4));
battle.log.add(new ShipActionUsedDiff(ship, EndTurnAction.SINGLETON, Target.newFromShip(ship)));
battle.log.add(new ShipValueDiff(ship, "hull", 7));
battle.log.add(new ShipActionUsedDiff(ship, EndTurnAction.SINGLETON, Target.newFromShip(ship)));
battle.log.add(new ShipValueDiff(ship, "hull", 2));
check.in("initial state", check => {
check.equals(ship.getValue("hull"), 13, "hull=13");
check.equals(battle.log.count(), 5, "log count=5");
});
battle.revertOneAction();
check.in("revert 1 action", check => {
check.equals(ship.getValue("hull"), 11, "hull=11");
check.equals(battle.log.count(), 3, "log count=3");
});
battle.revertOneAction();
check.in("revert 2 actions", check => {
check.equals(ship.getValue("hull"), 4, "hull=4");
check.equals(battle.log.count(), 1, "log count=1");
});
battle.revertOneAction();
check.in("revert 3 actions", check => {
check.equals(ship.getValue("hull"), 0, "hull=0");
check.equals(battle.log.count(), 0, "log count=0");
});
})
});
}

View file

@ -1,11 +1,10 @@
module TK.SpaceTac {
// A turn-based battle between fleets
/**
* A turn-based battle between fleets
*/
export class Battle {
// Flag indicating if the battle is ended
ended: boolean
// Battle outcome, if *ended* is true
outcome: BattleOutcome
// Battle outcome, if the battle has ended
outcome: BattleOutcome | null = null
// Battle cheats
cheats: BattleCheats
@ -27,10 +26,10 @@ module TK.SpaceTac {
play_index = -1
// Current battle "cycle" (one cycle is one turn done for all ships in the play order)
cycle: number
cycle = 0
// List of deployed drones
drones: Drone[] = []
drones = new RObjectContainer<Drone>()
// Size of the battle area
width: number
@ -44,12 +43,10 @@ module TK.SpaceTac {
// Indicator that an AI is playing
ai_playing = false
// Create a battle between two fleets
constructor(fleet1 = new Fleet(), fleet2 = new Fleet(), width = 1808, height = 948) {
constructor(fleet1 = new Fleet(new Player(undefined, "Attacker")), fleet2 = new Fleet(new Player(undefined, "Defender")), width = 1808, height = 948) {
this.fleets = [fleet1, fleet2];
this.ships = new RObjectContainer(fleet1.ships.concat(fleet2.ships));
this.play_order = [];
this.ended = false;
this.width = width;
this.height = height;
@ -66,6 +63,23 @@ module TK.SpaceTac {
this.ai_playing = false;
}
/**
* Property is true if the battle has ended
*/
get ended(): boolean {
return bool(this.outcome);
}
/**
* Apply a list of diffs to the game state, and add them to the log.
*
* This should be the main way to modify the game state.
*/
applyDiffs(diffs: BaseBattleDiff[]): void {
let client = new BattleLogClient(this, this.log);
diffs.forEach(diff => client.add(diff));
}
/**
* Create a quick random battle, for testing purposes, or quick skirmish
*/
@ -90,8 +104,12 @@ module TK.SpaceTac {
/**
* Get a ship by its ID.
*/
getShip(id: RObjectId): Ship | null {
return this.ships.get(id);
getShip(id: RObjectId | null): Ship | null {
if (id === null) {
return null;
} else {
return this.ships.get(id);
}
}
/**
@ -159,7 +177,7 @@ module TK.SpaceTac {
return -1;
} else {
let result = index - this.play_index;
return (result < 0) ? result + this.play_order.length : result;
return (result < 0) ? (result + this.play_order.length) : result;
}
}
@ -183,6 +201,24 @@ module TK.SpaceTac {
}
}
/**
* Set the currently playing ship
*/
setPlayingShip(ship: Ship): void {
let current = this.playing_ship;
if (current) {
current.playing = false;
}
this.play_index = this.play_order.indexOf(ship);
this.ai_playing = false;
current = this.playing_ship;
if (current) {
current.playing = true;
}
}
// Defines the initial ship positions of all engaged fleets
placeShips(vertical = true): void {
if (vertical) {
@ -194,100 +230,23 @@ module TK.SpaceTac {
}
}
// Count the number of fleets still alive
countAliveFleets(): number {
var result = 0;
this.fleets.forEach((fleet: Fleet) => {
if (fleet.isAlive()) {
result += 1;
}
});
return result;
}
// Collect all ships within a given radius of a target
collectShipsInCircle(center: Target, radius: number, alive_only = false): Ship[] {
return imaterialize(ifilter(this.iships(), ship => (ship.alive || !alive_only) && Target.newFromShip(ship).getDistanceTo(center) <= radius));
}
/**
* Ends a battle and sets the outcome
* Ends the battle and sets the outcome
*/
endBattle(winner: Fleet | null, log = true) {
this.ended = true;
this.outcome = new BattleOutcome(winner);
// Apply experience
this.outcome.grantExperience(this.fleets);
// Broadcast
if (log && this.log) {
this.log.add(new EndBattleEvent(this.outcome));
}
// Apply to all ships
iforeach(this.iships(), ship => ship.endBattle(this.cycle));
this.stats.onBattleEnd(this.fleets[0], this.fleets[1]);
endBattle(winner: Fleet | null) {
this.applyDiffs([new EndBattleDiff(winner, this.cycle)]);
}
// Checks end battle conditions, returns true if the battle ended
checkEndBattle(log: boolean = true) {
if (this.ended) {
return true;
}
var alive_fleets = this.countAliveFleets();
if (alive_fleets === 0) {
// It's a draw
this.endBattle(null, log);
} else if (alive_fleets === 1) {
// We have a winner
var winner: Fleet | null = null;
this.fleets.forEach((fleet: Fleet) => {
if (fleet.isAlive()) {
winner = fleet;
}
});
this.endBattle(winner, log);
}
return this.ended;
}
// End the current ship turn, passing control to the next one in play order
// If at the end of the play order, next turn will start automatically
// Member 'play_order' must be defined before calling this function
advanceToNextShip(log: boolean = true): void {
let previous_ship = this.playing_ship;
if (previous_ship && previous_ship.playing) {
previous_ship.endTurn();
}
if (this.checkEndBattle(log)) {
return;
}
this.drones.forEach(drone => drone.activate());
this.play_index += 1;
if (this.play_index >= this.play_order.length) {
this.play_index = 0;
}
if (this.playing_ship) {
if (this.play_index == 0) {
this.cycle += 1;
}
this.playing_ship.startTurn();
}
this.ai_playing = false;
if (log && previous_ship && this.playing_ship) {
this.log.add(new ShipChangeEvent(previous_ship, this.playing_ship));
}
/**
* Get the next playing ship
*/
getNextShip(): Ship {
return this.play_order[(this.play_index + 1) % this.play_order.length];
}
/**
@ -307,80 +266,32 @@ module TK.SpaceTac {
}
}
// Start the battle
// This will call all necessary initialization steps (initiative, placement...)
// This will not add any event to the battle log
/**
* Start the battle
*
* This will call all necessary initialization steps (initiative, placement...)
*
* This should not put any diff in the log
*/
start(): void {
this.ended = false;
this.cycle = 0;
this.outcome = null;
this.cycle = 1;
this.placeShips();
this.stats.onBattleStart(this.fleets[0], this.fleets[1]);
this.throwInitiative();
iforeach(this.iships(), ship => ship.startBattle());
this.advanceToNextShip();
// For now, we inject bootstrap events in the log
this.getBootstrapEvents().forEach(event => this.log.add(event));
iforeach(this.iships(), ship => ship.restoreInitialState());
this.setPlayingShip(this.play_order[0]);
}
/**
* Get a set of minimal events required to reach current state from an empty state.
* Force current ship's turn to end, then advance to the next one
*/
getBootstrapEvents(): BaseBattleEvent[] {
let result: BaseBattleEvent[] = [];
// Simulate initial ship placement
this.play_order.forEach(ship => {
let event = new MoveEvent(ship, ship.location, ship.location);
event.initial = true;
result.push(event);
});
// Simulate active effects
this.play_order.forEach(ship => {
if (ship.alive) {
let event = ship.getActiveEffects();
event.initial = true;
result.push(event);
}
});
// Indicate emergency stasis
this.play_order.forEach(ship => {
if (!ship.alive) {
let event = new DeathEvent(this, ship);
event.initial = true;
result.push(event);
}
});
// Simulate drones deployment
this.drones.forEach(drone => {
let event = new DroneDeployedEvent(drone);
event.initial = true;
result.push(event);
});
// Simulate game turn
advanceToNextShip(): void {
if (this.playing_ship) {
let event = new ShipChangeEvent(this.playing_ship, this.playing_ship);
event.initial = true;
result.push(event);
this.applyOneAction(EndTurnAction.SINGLETON);
} else if (this.play_order.length) {
this.setPlayingShip(this.play_order[0]);
}
return result;
}
/**
* Apply a list of events on this game state (and optionally store the events in the battle log)
*/
applyEvents(events: BaseBattleEvent[], log = true): void {
events.forEach(event => {
event.apply(this);
if (log) {
this.log.add(event);
}
});
}
/**
@ -407,35 +318,71 @@ module TK.SpaceTac {
/**
* Add a drone to the battle
*/
addDrone(drone: Drone, log = true) {
if (add(this.drones, drone)) {
if (log) {
this.log.add(new DroneDeployedEvent(drone));
}
}
addDrone(drone: Drone) {
this.drones.add(drone);
}
/**
* Remove a drone from the battle
*/
removeDrone(drone: Drone, log = true) {
if (remove(this.drones, drone)) {
if (log) {
this.log.add(new DroneDestroyedEvent(drone));
}
}
removeDrone(drone: Drone) {
this.drones.remove(drone);
}
/**
* Get the list of area effects at a given location
*/
iAreaEffects(x: number, y: number): Iterator<BaseEffect> {
let drones_in_range = ifilter(iarray(this.drones), drone => drone.isInRange(x, y));
let drones_in_range = ifilter(this.drones.iterator(), drone => drone.isInRange(x, y));
return ichain(
ichainit(imap(drones_in_range, drone => iarray(drone.effects))),
ichainit(imap(this.iships(), ship => ship.iAreaEffects(x, y)))
);
}
/**
* Perform all battle checks to ensure the state is consistent
*/
performChecks(): void {
let checks = new BattleChecks(this);
checks.apply();
}
/**
* Apply one action to the battle state
*
* At the end of the action, some checks will be applied to ensure the battle state is consistent
*/
applyOneAction(action: BaseAction, target?: Target): boolean {
let ship = this.playing_ship;
if (ship) {
if (action.apply(this, ship, target)) {
this.performChecks();
return true;
} else {
return false;
}
} else {
console.error("Cannot apply action - ship not playing", action, this);
return false;
}
}
/**
* Revert the last applied action
*
* This will remove diffs from the log, so pay attention to other log clients!
*/
revertOneAction(): void {
let client = new BattleLogClient(this, this.log);
while (!client.atStart() && !(client.getCurrent() instanceof ShipActionUsedDiff)) {
client.backward();
}
if (!client.atStart()) {
client.backward();
}
client.truncate();
}
}
}

View file

@ -4,20 +4,18 @@ module TK.SpaceTac.Specs {
let battle = Battle.newQuickRandom();
battle.cheats.win();
check.same(battle.ended, true, "ended");
check.same(battle.outcome.winner, battle.fleets[0], "winner");
check.equals(battle.log.events.filter(event => event instanceof DeathEvent).map(event => event.ship), battle.fleets[1].ships, "all mark dead");
check.same(any(battle.fleets[1].ships, ship => !ship.alive), false, "all restored");
check.equals(battle.ended, true, "ended");
check.same(nn(battle.outcome).winner, battle.fleets[0], "winner");
check.equals(any(battle.fleets[1].ships, ship => ship.alive), false, "all enemies dead");
})
test.case("loses a battle", check => {
let battle = Battle.newQuickRandom();
battle.cheats.lose();
check.same(battle.ended, true, "ended");
check.same(battle.outcome.winner, battle.fleets[1], "winner");
check.equals(battle.log.events.filter(event => event instanceof DeathEvent).map(event => event.ship), battle.fleets[0].ships, "all mark dead");
check.same(any(battle.fleets[0].ships, ship => !ship.alive), false, "all restored");
check.equals(battle.ended, true, "ended");
check.same(nn(battle.outcome).winner, battle.fleets[1], "winner");
check.equals(any(battle.fleets[0].ships, ship => ship.alive), false, "all allies dead");
})
test.case("adds an equipment", check => {

View file

@ -0,0 +1,48 @@
module TK.SpaceTac.Specs {
testing("BattleChecks", test => {
test.case("detects victory conditions", check => {
let battle = new Battle();
let ship1 = battle.fleets[0].addShip();
let ship2 = battle.fleets[1].addShip();
let checks = new BattleChecks(battle);
check.equals(checks.checkVictory(), [], "no victory");
battle.cycle = 5;
ship1.setDead();
check.equals(checks.checkVictory(), [new EndBattleDiff(battle.fleets[1], 5)], "victory");
})
test.case("fixes ship values", check => {
let battle = new Battle();
let ship1 = battle.fleets[0].addShip();
let ship2 = battle.fleets[1].addShip();
let checks = new BattleChecks(battle);
check.equals(checks.checkShipValues(), [], "no value to fix");
ship1.setValue("hull", -4);
TestTools.setAttribute(ship2, "shield_capacity", 48);
ship2.setValue("shield", 60);
check.equals(checks.checkShipValues(), [
new ShipValueDiff(ship1, "hull", 4),
new ShipValueDiff(ship2, "shield", -12),
], "fixed values");
})
test.case("marks ships as dead", check => {
let battle = new Battle();
let ship1 = battle.fleets[0].addShip();
let ship2 = battle.fleets[1].addShip();
let ship3 = battle.fleets[1].addShip();
battle.ships.list().forEach(ship => TestTools.setShipHP(ship, 10, 0));
let checks = new BattleChecks(battle);
check.equals(checks.checkDeadShips(), [], "no ship to mark as dead");
ship1.setValue("hull", 0);
ship3.setValue("hull", 0);
check.equals(checks.checkDeadShips(), [
new ShipDeathDiff(battle, ship1),
new ShipDeathDiff(battle, ship3),
], "2 ships to mark as dead");
})
})
}

116
src/core/BattleChecks.ts Normal file
View file

@ -0,0 +1,116 @@
module TK.SpaceTac {
/**
* List of checks to apply at the end of an action, to ensure a correct battle state
*
* This is useful when the list of effects simulated by an action was missing something
*
* To fix the state, new diffs will be applied
*/
export class BattleChecks {
private battle: Battle;
constructor(battle: Battle) {
this.battle = battle;
}
/**
* Apply all the checks
*/
apply(): void {
let diffs: BaseBattleDiff[];
let loops = 0;
do {
diffs = this.checkAll();
if (diffs.length > 0) {
this.battle.applyDiffs(diffs);
}
loops += 1;
if (loops >= 1000) {
console.error("Battle checks locked in infinite loop", diffs);
break;
}
} while (diffs.length > 0);
}
/**
* Get a list of diffs to apply to fix the battle state
*
* This may not contain ALL the diffs needed, and should be called again while it returns diffs.
*/
checkAll(): BaseBattleDiff[] {
let diffs = this.checkVictory();
if (diffs.length) {
return diffs;
}
diffs = this.checkShipValues();
if (diffs.length) {
return diffs;
}
diffs = this.checkDeadShips();
if (diffs.length) {
return diffs;
}
return [];
}
/**
* Checks victory conditions, to put an end to the battle
*/
checkVictory(): BaseBattleDiff[] {
if (this.battle.ended) {
return [];
}
let fleets = this.battle.fleets;
if (any(fleets, fleet => !fleet.isAlive())) {
const winner = first(fleets, fleet => fleet.isAlive());
return [new EndBattleDiff(winner, this.battle.cycle)];
} else {
return [];
}
}
/**
* Check that ship values stays in their allowed range
*/
checkShipValues(): BaseBattleDiff[] {
let result: BaseBattleDiff[] = [];
iforeach(this.battle.iships(true), ship => {
keys(SHIP_VALUES).forEach(valuename => {
let value = ship.getValue(valuename);
if (value < 0) {
result.push(new ShipValueDiff(ship, valuename, -value));
} else {
let maximum = ship.getAttribute(<any>(valuename + "_capacity"));
if (value > maximum) {
result.push(new ShipValueDiff(ship, valuename, maximum - value));
}
}
});
});
return result;
}
/**
* Check that ship with no more hull are dead
*/
checkDeadShips(): BaseBattleDiff[] {
let result: BaseBattleDiff[] = [];
iforeach(this.battle.iships(true), ship => {
if (ship.getValue("hull") == 0) {
result.push(new ShipDeathDiff(this.battle, ship));
}
});
return result;
}
}
}

View file

@ -1,103 +0,0 @@
/// <reference path="events/BaseBattleEvent.ts"/>
module TK.SpaceTac {
testing("BattleLog", test => {
// Check a single game log event
function checkEvent(got: BaseBattleEvent, ship: Ship, code: string,
target_ship: Ship | null = null, target_x: number | null = null, target_y: number | null = null): void {
if (target_ship) {
if (target_x === null) {
target_x = target_ship.arena_x;
}
if (target_y === null) {
target_y = target_ship.arena_y;
}
}
let check = test.check;
check.same(got.ship, ship);
check.equals(got.code, code);
if (got.target) {
check.same(got.target.ship, target_ship);
if (target_x === null) {
check.equals(got.target.x, null);
} else {
check.nears(got.target.x, target_x);
}
if (target_y === null) {
check.equals(got.target.y, null);
} else {
check.nears(got.target.y, target_y);
}
} else if (target_ship || target_x || target_y) {
check.fail("Got no target");
}
}
// Fake event
class FakeEvent extends BaseBattleEvent {
constructor() {
super("fake", new Ship());
}
}
test.case("forwards events to subscribers, until unsubscribe", check => {
var log = new BattleLog();
var received: BaseBattleEvent[] = [];
var fake = new FakeEvent();
var sub = log.subscribe(function (event: BaseBattleEvent) {
received.push(event);
});
log.add(fake);
check.equals(received, [fake]);
log.add(fake);
check.equals(received, [fake, fake]);
log.unsubscribe(sub);
log.add(fake);
check.equals(received, [fake, fake]);
});
test.case("logs ship change events", check => {
var battle = Battle.newQuickRandom();
battle.log.clear();
battle.log.addFilter("value");
check.equals(battle.log.events.length, 0);
battle.advanceToNextShip();
check.equals(battle.log.events.length, 1);
checkEvent(battle.log.events[0], battle.play_order[0], "ship_change", battle.play_order[1]);
});
test.case("can receive simulated initial state events", check => {
let battle = Battle.newQuickRandom(true, 1, 4);
let playing = nn(battle.playing_ship);
let result = battle.getBootstrapEvents();
check.equals(result.length, 17);
for (var i = 0; i < 8; i++) {
checkEvent(result[i], battle.play_order[i], "move", null,
battle.play_order[i].arena_x, battle.play_order[i].arena_y);
}
for (var i = 0; i < 8; i++) {
checkEvent(result[8 + i], battle.play_order[i], "activeeffects");
}
checkEvent(result[16], playing, "ship_change", playing);
});
test.case("stop accepting events once the battle is ended", check => {
let log = new BattleLog();
log.add(new ValueChangeEvent(new Ship(), new ShipValue("test"), 1));
log.add(new EndBattleEvent(new BattleOutcome(null)));
log.add(new ShipChangeEvent(new Ship(), new Ship()));
check.equals(log.events.length, 2);
check.equals(log.events[0] instanceof ValueChangeEvent, true);
check.equals(log.events[1] instanceof EndBattleEvent, true);
});
});
}

View file

@ -1,84 +1,15 @@
/// <reference path="../common/DiffLog.ts" />
module TK.SpaceTac {
/**
* Function called to inform subscribers of new events.
* Log of diffs that change the state of a battle
*/
export type LogSubscriber = (event: BaseBattleEvent) => any;
export class BattleLog extends DiffLog<Battle> {
}
// Log of a battle
// This keeps track of all events in a battle
// It also allows to register a callback to receive these events
export class BattleLog {
// Full list of battle events
events: BaseBattleEvent[]
// List of subscribers
private subscribers: LogSubscriber[]
// List of event codes to ignore
private filters: string[]
// Indicator that the battle has ended
private ended = false
// Create an initially empty log
constructor() {
this.events = [];
this.subscribers = [];
this.filters = [];
}
postUnserialize(): void {
this.subscribers = [];
}
// Clear the stored events
clear(): void {
this.ended = false;
this.events = [];
}
// Add a battle event to the log
add(event: BaseBattleEvent): void {
// Apply filters
var filtered = false;
this.filters.forEach(code => {
if (event.code === code) {
filtered = true;
}
});
if (filtered || this.ended) {
return;
}
this.events.push(event);
this.subscribers.forEach(subscriber => {
subscriber(event);
});
if (event instanceof EndBattleEvent) {
this.ended = true;
}
}
// Filter out a type of event
addFilter(event_code: string): void {
this.filters.push(event_code);
}
// Subscribe a callback to receive further events
subscribe(callback: LogSubscriber): LogSubscriber {
this.subscribers.push(callback);
return callback;
}
// Unsubscribe a callback
// Pass the value returned by 'subscribe' as argument
unsubscribe(callback: LogSubscriber): void {
var index = this.subscribers.indexOf(callback);
if (index >= 0) {
this.subscribers.splice(index, 1);
}
}
/**
* Client for a battle log
*/
export class BattleLogClient extends DiffLogClient<Battle> {
}
}

View file

@ -25,15 +25,15 @@ module TK.SpaceTac.Specs {
stats.processLog(battle.log, battle.fleets[0]);
check.equals(stats.stats, {});
battle.log.add(new DamageEvent(attacker, 10, 12));
battle.log.add(new ShipDamageDiff(attacker, 10, 12));
stats.processLog(battle.log, battle.fleets[0], true);
check.equals(stats.stats, { "Damage dealt": [0, 22] });
battle.log.add(new DamageEvent(defender, 40, 0));
battle.log.add(new ShipDamageDiff(defender, 40, 0));
stats.processLog(battle.log, battle.fleets[0], true);
check.equals(stats.stats, { "Damage dealt": [40, 22] });
battle.log.add(new DamageEvent(attacker, 5, 4));
battle.log.add(new ShipDamageDiff(attacker, 5, 4));
stats.processLog(battle.log, battle.fleets[0], true);
check.equals(stats.stats, { "Damage dealt": [40, 31] });
})
@ -46,11 +46,11 @@ module TK.SpaceTac.Specs {
stats.processLog(battle.log, battle.fleets[0]);
check.equals(stats.stats, {});
battle.log.add(new MoveEvent(attacker, new ArenaLocationAngle(0, 0), new ArenaLocationAngle(10, 0)));
battle.log.add(new ShipMoveDiff(attacker, new ArenaLocationAngle(0, 0), new ArenaLocationAngle(10, 0)));
stats.processLog(battle.log, battle.fleets[0], true);
check.equals(stats.stats, { "Move distance (km)": [10, 0] });
battle.log.add(new MoveEvent(defender, new ArenaLocationAngle(10, 5), new ArenaLocationAngle(10, 63)));
battle.log.add(new ShipMoveDiff(defender, new ArenaLocationAngle(10, 5), new ArenaLocationAngle(10, 63)));
stats.processLog(battle.log, battle.fleets[0], true);
check.equals(stats.stats, { "Move distance (km)": [10, 58] });
})
@ -63,32 +63,15 @@ module TK.SpaceTac.Specs {
stats.processLog(battle.log, battle.fleets[0]);
check.equals(stats.stats, {});
battle.log.add(new DroneDeployedEvent(new Drone(attacker)));
battle.log.add(new DroneDeployedDiff(new Drone(attacker)));
stats.processLog(battle.log, battle.fleets[0], true);
check.equals(stats.stats, { "Drones deployed": [1, 0] });
battle.log.add(new DroneDeployedEvent(new Drone(defender)));
battle.log.add(new DroneDeployedDiff(new Drone(defender)));
stats.processLog(battle.log, battle.fleets[0], true);
check.equals(stats.stats, { "Drones deployed": [1, 1] });
})
test.case("collects power usage", check => {
let stats = new BattleStats();
let battle = new Battle();
let attacker = battle.fleets[0].addShip();
let defender = battle.fleets[1].addShip();
stats.processLog(battle.log, battle.fleets[0]);
check.equals(stats.stats, {});
battle.log.add(new ActionAppliedEvent(attacker, new BaseAction("nop", "nop"), null, 4));
stats.processLog(battle.log, battle.fleets[0], true);
check.equals(stats.stats, { "Power used": [4, 0] });
battle.log.add(new ActionAppliedEvent(defender, new BaseAction("nop", "nop"), null, 2));
stats.processLog(battle.log, battle.fleets[0], true);
check.equals(stats.stats, { "Power used": [4, 2] });
})
test.case("evaluates equipment depreciation", check => {
let stats = new BattleStats();
let battle = new Battle();

View file

@ -42,17 +42,21 @@ module TK.SpaceTac {
this.stats = {};
}
log.events.forEach(event => {
if (event instanceof ActionAppliedEvent) {
this.addStat("Power used", event.power, event.ship.fleet === attacker);
} else if (event instanceof DamageEvent) {
this.addStat("Damage dealt", event.hull + event.shield, event.ship.fleet !== attacker);
} else if (event instanceof MoveEvent) {
this.addStat("Move distance (km)", event.getDistance(), event.ship.fleet === attacker);
} else if (event instanceof DroneDeployedEvent) {
this.addStat("Drones deployed", 1, event.ship.fleet === attacker);
let n = log.count();
for (let i = 0; i < n; i++) {
let diff = log.get(i);
if (diff instanceof BaseBattleShipDiff) {
let diff_ship = diff.ship_id;
let attacker_ship = any(attacker.ships, ship => ship.is(diff_ship));
if (diff instanceof ShipDamageDiff) {
this.addStat("Damage dealt", diff.hull + diff.shield, !attacker_ship);
} else if (diff instanceof ShipMoveDiff) {
this.addStat("Move distance (km)", diff.getDistance(), attacker_ship);
} else if (diff instanceof DroneDeployedDiff) {
this.addStat("Drones deployed", 1, attacker_ship);
}
}
});
}
}
/**

View file

@ -60,11 +60,13 @@ module TK.SpaceTac {
/**
* Use the equipment, increasing the heat
*/
use(): void {
use(times = 1): void {
if (this.overheat) {
this.uses += 1;
this.uses += times;
if (this.uses >= this.overheat) {
this.heat = this.cooling;
} else {
this.heat = 0;
}
}
}

View file

@ -11,9 +11,9 @@ module TK.SpaceTac {
super("fake");
}
applyOnShip(ship: Ship, source: Ship | Drone): boolean {
getOnDiffs(ship: Ship, source: Ship | Drone): BaseBattleDiff[] {
this.applied.push(ship);
return true;
return [];
}
getApplyCalls() {
@ -30,6 +30,10 @@ module TK.SpaceTac {
drone.radius = radius;
let effect = new FakeEffect();
drone.effects.push(effect);
let battle = owner.getBattle();
if (battle) {
battle.addDrone(drone);
}
return [drone, effect];
}
@ -49,7 +53,7 @@ module TK.SpaceTac {
check.equals(effect.getApplyCalls(), []);
drone.activate();
drone.activate(battle);
check.equals(effect.getApplyCalls(), [ship1, ship2]);
});
@ -61,33 +65,55 @@ module TK.SpaceTac {
let removeDrone = check.patch(battle, "removeDrone", null);
drone.activate();
drone.activate(battle);
check.equals(drone.duration, 2);
check.called(removeDrone, 0);
drone.activate();
drone.activate(battle);
check.equals(drone.duration, 1);
check.called(removeDrone, 0);
drone.activate();
check.called(removeDrone, [
[drone, true]
]);
drone.activate(battle);
check.equals(drone.duration, 0);
check.called(removeDrone, [[drone]]);
});
test.case("logs each activation", check => {
test.case("builds diffs on activation", check => {
let battle = new Battle();
let ship = new Ship();
ship.fleet.setBattle(battle);
let other = new Ship();
let drone = new Drone(ship);
drone.apply([ship, other]);
drone.apply([]);
drone.apply([other]);
check.equals(battle.log.events, [
new DroneAppliedEvent(drone, [ship, other]),
new DroneAppliedEvent(drone, [other])
]);
drone.duration = 2;
check.in("duration=2", check => {
check.equals(drone.getDiffs(battle, [ship, other]), [
new DroneAppliedDiff(drone, [ship, other]),
], "two ships in range");
check.equals(drone.getDiffs(battle, []), [
], "no ship in range");
});
drone.duration = 1;
check.in("duration=1", check => {
check.equals(drone.getDiffs(battle, [ship, other]), [
new DroneAppliedDiff(drone, [ship, other]),
new DroneDestroyedDiff(drone),
], "two ships in range");
check.equals(drone.getDiffs(battle, []), [
new DroneDestroyedDiff(drone),
], "no ship in range");
});
drone.duration = 0;
check.in("duration=0", check => {
check.equals(drone.getDiffs(battle, [ship, other]), [
new DroneDestroyedDiff(drone),
], "two ships in range");
check.equals(drone.getDiffs(battle, []), [
new DroneDestroyedDiff(drone),
], "no ship in range");
});
});
test.case("builds a textual description", check => {

View file

@ -2,30 +2,28 @@ module TK.SpaceTac {
/**
* Drones are static objects that apply effects in a circular zone around themselves.
*/
export class Drone {
// Battle in which the drone is deployed
battle: Battle;
// Ship that launched the drone (informative, a drone is autonomous)
owner: Ship;
export class Drone extends RObject {
// ID of the owning ship
owner: RObjectId
// Code of the drone
code: string;
code: string
// Location in arena
x: number;
y: number;
radius: number;
x: number
y: number
radius: number
// Remaining lifetime in number of turns
duration: number;
duration: number
// Effects to apply
effects: BaseEffect[] = [];
effects: BaseEffect[] = []
constructor(owner: Ship, code = "drone", base_duration = 1) {
this.battle = owner.getBattle() || new Battle();
this.owner = owner;
super();
this.owner = owner.id;
this.code = code;
this.duration = base_duration;
}
@ -58,38 +56,40 @@ module TK.SpaceTac {
/**
* Get the list of affected ships.
*/
getAffectedShips(): Ship[] {
let ships = ifilter(this.battle.iships(), ship => ship.alive && ship.isInCircle(this.x, this.y, this.radius));
getAffectedShips(battle: Battle): Ship[] {
let ships = ifilter(battle.iships(), ship => ship.alive && ship.isInCircle(this.x, this.y, this.radius));
return imaterialize(ships);
}
/**
* Apply the effects on a list of ships
* Get the list of diffs needed to apply the drone effects on a list of ships.
*
* This does not check if the ships are in range.
*/
apply(ships: Ship[], log = true) {
if (ships.length > 0) {
if (log) {
this.battle.log.add(new DroneAppliedEvent(this, ships));
}
getDiffs(battle: Battle, ships: Ship[]): BaseBattleDiff[] {
let result: BaseBattleDiff[] = [];
if (this.duration >= 1 && ships.length > 0) {
result.push(new DroneAppliedDiff(this, ships));
ships.forEach(ship => {
this.effects.forEach(effect => effect.applyOnShip(ship, this));
result = result.concat(flatten(this.effects.map(effect => effect.getOnDiffs(ship, this))));
});
}
if (this.duration <= 1) {
result.push(new DroneDestroyedDiff(this));
}
return result;
}
/**
* Activate the drone
* Apply one drone "activation"
*/
activate(log = true) {
this.apply(this.getAffectedShips(), log);
this.duration--;
if (this.duration == 0) {
this.battle.removeDrone(this, log);
}
activate(battle: Battle) {
let diffs = this.getDiffs(battle, this.getAffectedShips(battle));
battle.applyDiffs(diffs);
}
}
}

View file

@ -21,15 +21,15 @@ module TK.SpaceTac.Specs {
check.equals(equipment.canBeEquipped(ship.attributes), false);
ship.attributes.skill_time.set(1);
TestTools.setAttribute(ship, "skill_time", 1);
check.equals(equipment.canBeEquipped(ship.attributes), false);
ship.attributes.skill_time.set(2);
TestTools.setAttribute(ship, "skill_time", 2);
check.equals(equipment.canBeEquipped(ship.attributes), true);
ship.attributes.skill_time.set(3);
TestTools.setAttribute(ship, "skill_time", 3);
check.equals(equipment.canBeEquipped(ship.attributes), true);
@ -38,7 +38,7 @@ module TK.SpaceTac.Specs {
check.equals(equipment.canBeEquipped(ship.attributes), false);
ship.attributes.skill_materials.set(4);
TestTools.setAttribute(ship, "skill_materials", 4);
check.equals(equipment.canBeEquipped(ship.attributes), true);
});

View file

@ -11,7 +11,7 @@ module TK.SpaceTac {
}
// Piece of equipment to attach in slots
export class Equipment {
export class Equipment extends RObject {
// Type of slot this equipment can fit in
slot_type: SlotType | null
@ -53,6 +53,8 @@ module TK.SpaceTac {
// Basic constructor
constructor(slot: SlotType | null = null, code = "equipment") {
super();
this.slot_type = slot;
this.code = code;
this.name = code;
@ -80,7 +82,7 @@ module TK.SpaceTac {
let requirements: string[] = [];
iteritems(this.requirements, (skill: keyof ShipAttributes, value) => {
if (value > 0) {
requirements.push(`${SHIP_ATTRIBUTES[skill].name} ${value}`);
requirements.push(`${SHIP_VALUES_NAMES[skill]} ${value}`);
}
});

View file

@ -120,25 +120,29 @@ module TK.SpaceTac {
check.equals(ship1.cargo, []);
check.equals(ship2.cargo, []);
let result = fleet.addCargo(new Equipment());
let equipment1 = new Equipment();
let result = fleet.addCargo(equipment1);
check.equals(result, true);
check.equals(ship1.cargo, [new Equipment()]);
check.equals(ship1.cargo, [equipment1]);
check.equals(ship2.cargo, []);
result = fleet.addCargo(new Equipment());
let equipment2 = new Equipment();
result = fleet.addCargo(equipment2);
check.equals(result, true);
check.equals(ship1.cargo, [new Equipment()]);
check.equals(ship2.cargo, [new Equipment()]);
check.equals(ship1.cargo, [equipment1]);
check.equals(ship2.cargo, [equipment2]);
result = fleet.addCargo(new Equipment());
let equipment3 = new Equipment();
result = fleet.addCargo(equipment3);
check.equals(result, true);
check.equals(ship1.cargo, [new Equipment()]);
check.equals(ship2.cargo, [new Equipment(), new Equipment()]);
check.equals(ship1.cargo, [equipment1]);
check.equals(ship2.cargo, [equipment2, equipment3]);
result = fleet.addCargo(new Equipment());
let equipment4 = new Equipment();
result = fleet.addCargo(equipment4);
check.equals(result, false);
check.equals(ship1.cargo, [new Equipment()]);
check.equals(ship2.cargo, [new Equipment(), new Equipment()]);
check.equals(ship1.cargo, [equipment1]);
check.equals(ship2.cargo, [equipment2, equipment3]);
});
});
}

View file

@ -55,7 +55,7 @@ module TK.SpaceTac {
/**
* Add a ship this fleet
*/
addShip(ship = new Ship()): Ship {
addShip(ship = new Ship(null, `${this.player.name} ${this.ships.length + 1}`)): Ship {
if (ship.fleet && ship.fleet != this) {
remove(ship.fleet.ships, ship);
}

View file

@ -49,7 +49,7 @@ module TK.SpaceTac.Specs {
let battle = nn(session.getBattle());
battle.endBattle(session.player.fleet);
let spyloot = check.patch(battle.outcome, "createLoot", null);
let spyloot = check.patch(nn(battle.outcome), "createLoot", null);
session.setBattleEnded();
check.notequals(session.getBattle(), null);
check.equals(location1.encounter, null);
@ -64,7 +64,7 @@ module TK.SpaceTac.Specs {
battle = nn(session.getBattle());
battle.endBattle(null);
spyloot = check.patch(battle.outcome, "createLoot", null);
spyloot = check.patch(nn(battle.outcome), "createLoot", null);
session.setBattleEnded();
check.notequals(session.getBattle(), null);
check.notequals(location2.encounter, null);

View file

@ -120,19 +120,22 @@ module TK.SpaceTac {
setBattleEnded() {
let battle = this.getBattle();
if (battle && battle.ended) {
if (battle && battle.ended && battle.outcome) {
// Generate experience
battle.outcome.grantExperience(battle.fleets);
if (battle.outcome.winner == this.player.fleet) {
// In case of victory, generate loot
battle.outcome.createLoot(battle);
// Reset ships status
iforeach(battle.iships(), ship => ship.restoreInitialState());
// In case of victorious encounter, clear the encouter
let location = this.player.fleet.location;
if (location) {
location.clearEncounter();
}
// In case of victory for current player, generate loot
if (battle.outcome.winner == this.player.fleet) {
battle.outcome.createLoot(battle);
}
// If the battle happened in a star location, keep it informed
let location = this.player.fleet.location;
if (location) {
location.resolveEncounter(battle.outcome);
}
}
}

View file

@ -9,6 +9,61 @@ module TK.SpaceTac.Specs {
}
}
function strip<T>(obj: T, attr: keyof T): any {
let result: any = {};
copyfields(obj, result);
delete result[attr];
return result;
}
function strip_id(effect: RObject): any {
if (effect instanceof StickyEffect) {
let result = strip(effect, "id");
result.base = strip_id(result.base);
return result;
} else {
return strip(effect, "id");
}
}
export function compare_effects(check: TestContext, effects1: BaseEffect[], effects2: BaseEffect[]): void {
check.equals(effects1.map(strip_id), effects2.map(strip_id), "effects");
}
export function compare_action(check: TestContext, action1: BaseAction | null, action2: BaseAction | null): void {
if (action1 === null || action2 === null) {
check.equals(action1, action2, "action");
} else {
check.equals(strip_id(action1), strip_id(action2), "action");
}
}
export function compare_trigger_action(check: TestContext, action1: BaseAction | null, action2: TriggerAction | null): void {
if (action1 === null || action2 === null || !(action1 instanceof TriggerAction)) {
check.equals(action1, action2, "action");
} else {
check.equals(strip_id(strip(action1, "effects")), strip_id(strip(action2, "effects")), "action");
compare_effects(check, action1.effects, action2.effects);
}
}
export function compare_toggle_action(check: TestContext, action1: BaseAction | null, action2: ToggleAction | null): void {
if (action1 === null || action2 === null || !(action1 instanceof ToggleAction)) {
check.equals(action1, action2, "action");
} else {
check.equals(strip_id(strip(action1, "effects")), strip_id(strip(action2, "effects")), "action");
compare_effects(check, action1.effects, action2.effects);
}
}
export function compare_drone_action(check: TestContext, action1: BaseAction | null, action2: DeployDroneAction | null): void {
if (action1 === null || action2 === null || !(action1 instanceof DeployDroneAction)) {
check.equals(action1, action2, "action");
} else {
check.equals(strip_id(strip(action1, "effects")), strip_id(strip(action2, "effects")), "action");
compare_effects(check, action1.effects, action2.effects);
}
}
testing("LootTemplate", test => {
test.case("generates equipment with correct information", check => {
let template = new LootTemplate(SlotType.Power, "Power Generator", "A great power generator !");
@ -25,10 +80,10 @@ module TK.SpaceTac.Specs {
template.addAttributeEffect("power_capacity", istep(10));
result = template.generate(1, EquipmentQuality.COMMON);
check.equals(result.quality, EquipmentQuality.COMMON);
check.equals(result.effects, [new AttributeEffect("power_capacity", 10)]);
compare_effects(check, result.effects, [new AttributeEffect("power_capacity", 10)]);
result = template.generate(1, EquipmentQuality.PREMIUM);
check.equals(result.quality, EquipmentQuality.PREMIUM);
check.equals(result.effects, [new AttributeEffect("power_capacity", 13)]);
compare_effects(check, result.effects, [new AttributeEffect("power_capacity", 13)]);
});
test.case("applies requirements on skills", check => {
@ -76,10 +131,10 @@ module TK.SpaceTac.Specs {
template.addAttributeEffect("shield_capacity", irange(undefined, 50, 10));
let result = template.generate(1);
check.equals(result.effects, [new AttributeEffect("shield_capacity", 50)]);
compare_effects(check, result.effects, [new AttributeEffect("shield_capacity", 50)]);
result = template.generate(2);
check.equals(result.effects, [new AttributeEffect("shield_capacity", 60)]);
compare_effects(check, result.effects, [new AttributeEffect("shield_capacity", 60)]);
});
test.case("adds move actions", check => {
@ -87,10 +142,10 @@ module TK.SpaceTac.Specs {
template.addMoveAction(irange(undefined, 100, 10), istep(50, irepeat(10)), irepeat(95));
let result = template.generate(1);
check.equals(result.action, new MoveAction(result, 100, 50, 95));
compare_action(check, result.action, new MoveAction(result, 100, 50, 95));
result = template.generate(2);
check.equals(result.action, new MoveAction(result, 110, 60, 95));
compare_action(check, result.action, new MoveAction(result, 110, 60, 95));
});
test.case("adds fire actions", check => {
@ -100,10 +155,10 @@ module TK.SpaceTac.Specs {
], istep(100), istep(50), istep(10));
let result = template.generate(1);
check.equals(result.action, new TriggerAction(result, [new FakeEffect(8)], 1, 100, 50, 10));
compare_trigger_action(check, result.action, new TriggerAction(result, [new FakeEffect(8)], 1, 100, 50, 10));
result = template.generate(2);
check.equals(result.action, new TriggerAction(result, [new FakeEffect(9)], 2, 101, 51, 11));
compare_trigger_action(check, result.action, new TriggerAction(result, [new FakeEffect(9)], 2, 101, 51, 11));
});
test.case("adds drone actions", check => {
@ -113,10 +168,10 @@ module TK.SpaceTac.Specs {
]);
let result = template.generate(1);
check.equals(result.action, new DeployDroneAction(result, 1, 100, 2, 50, [new FakeEffect(8)]));
compare_drone_action(check, result.action, new DeployDroneAction(result, 1, 100, 2, 50, [new FakeEffect(8)]));
result = template.generate(2);
check.equals(result.action, new DeployDroneAction(result, 2, 101, 3, 51, [new FakeEffect(9)]));
compare_drone_action(check, result.action, new DeployDroneAction(result, 2, 101, 3, 51, [new FakeEffect(9)]));
});
test.case("checks the presence of damaging effects", check => {

View file

@ -80,7 +80,7 @@ module TK.SpaceTac {
let [name, value] = modifier;
(<any>result)[name] = resolveForLevel(value, level);
});
return new StickyEffect(result, resolveForLevel(this.duration, level), true);
return new StickyEffect(result, resolveForLevel(this.duration, level));
}
}
@ -145,7 +145,7 @@ module TK.SpaceTac {
let level = 1;
let equipment: Equipment | null = null;
let attributes = new ShipAttributes();
keys(skills).forEach(skill => attributes[skill].set(skills[skill].get()));
keys(skills).forEach(skill => attributes[skill].addModifier(skills[skill].get()));
do {
let nequipment = this.generate(level, quality, random);
if (nequipment.canBeEquipped(attributes)) {

View file

@ -56,10 +56,10 @@ module TK.SpaceTac.Specs {
let ship2a = battle.fleets[1].addShip();
let ship2b = battle.fleets[1].addShip();
check.equals(condition(ship1a.getPlayer(), battle, ship1a, new DamageEvent(ship1a, 50, 10)), [], "self shoot");
check.equals(condition(ship1a.getPlayer(), battle, ship1a, new DamageEvent(ship1b, 50, 10)), [ship1b, ship1a]);
check.equals(condition(ship1a.getPlayer(), battle, ship1a, new DamageEvent(ship2a, 50, 10)), [], "enemy shoot");
check.equals(condition(ship1a.getPlayer(), battle, ship2a, new DamageEvent(ship2a, 50, 10)), [], "other player event");
check.equals(condition(ship1a.getPlayer(), battle, ship1a, new ShipDamageDiff(ship1a, 50, 10)), [], "self shoot");
check.equals(condition(ship1a.getPlayer(), battle, ship1a, new ShipDamageDiff(ship1b, 50, 10)), [ship1b, ship1a]);
check.equals(condition(ship1a.getPlayer(), battle, ship1a, new ShipDamageDiff(ship2a, 50, 10)), [], "enemy shoot");
check.equals(condition(ship1a.getPlayer(), battle, ship2a, new ShipDamageDiff(ship2a, 50, 10)), [], "other player event");
})
})
}

View file

@ -3,7 +3,7 @@ module TK.SpaceTac {
export type PersonalityReaction = PersonalityReactionConversation
// Condition to check if a reaction may happen, returning involved ships (order is important)
export type ReactionCondition = (player: Player, battle: Battle | null, ship: Ship | null, event: BaseBattleEvent | null) => Ship[]
export type ReactionCondition = (player: Player, battle: Battle | null, ship: Ship | null, event: BaseBattleDiff | null) => Ship[]
// Reaction profile, giving a probability for types of personality, and an associated reaction constructor
export type ReactionProfile = [(traits: IPersonalityTraits) => number, (ships: Ship[]) => PersonalityReaction]
@ -31,7 +31,7 @@ module TK.SpaceTac {
*
* This will return a reaction to display, and add it to the done list
*/
check(player: Player, battle: Battle | null = null, ship: Ship | null = null, event: BaseBattleEvent | null = null, pool: ReactionPool = BUILTIN_REACTION_POOL): PersonalityReaction | null {
check(player: Player, battle: Battle | null = null, ship: Ship | null = null, event: BaseBattleDiff | null = null, pool: ReactionPool = BUILTIN_REACTION_POOL): PersonalityReaction | null {
let codes = difference(keys(pool), this.done);
let candidates = nna(codes.map((code: string): [string, Ship[], ReactionProfile[]] | null => {
@ -88,10 +88,14 @@ module TK.SpaceTac {
]]
}
function cond_friendly_fire(player: Player, battle: Battle | null, ship: Ship | null, event: BaseBattleEvent | null): Ship[] {
/**
* Check for a friendly fire condition (one of player's ships fired on another)
*/
function cond_friendly_fire(player: Player, battle: Battle | null, ship: Ship | null, event: BaseBattleDiff | null): Ship[] {
if (battle && ship && event) {
if (event instanceof DamageEvent && event.ship != ship && event.ship.getPlayer() == player && ship.getPlayer() == player) {
return [event.ship, ship];
if (event instanceof ShipDamageDiff && player.is(ship.getPlayer()) && !ship.is(event.ship_id)) {
let hurt = battle.getShip(event.ship_id);
return (hurt && hurt.getPlayer().is(player)) ? [hurt, ship] : [];
} else {
return [];
}

View file

@ -1,6 +1,10 @@
/// <reference path="../common/RObject.ts" />
module TK.SpaceTac {
// One player (human or IA)
export class Player {
/**
* One player (human or IA)
*/
export class Player extends RObject {
// Player's name
name: string
@ -18,6 +22,8 @@ module TK.SpaceTac {
// Create a player, with an empty fleet
constructor(universe: Universe = new Universe(), name = "Player") {
super();
this.universe = universe;
this.name = name;
this.fleet = new Fleet(this);

View file

@ -14,84 +14,20 @@ module TK.SpaceTac.Specs {
check.equals(ship.getFullName(true), "Emperor's Level 3 Titan");
});
test.case("moves and computes facing angle", check => {
test.case("moves in the arena", check => {
let ship = new Ship(null, "Test");
let engine = TestTools.addEngine(ship, 50);
ship.setArenaFacingAngle(0);
ship.setArenaPosition(50, 50);
check.equals(ship.arena_x, 50);
check.equals(ship.arena_x, 0);
check.equals(ship.arena_y, 0);
check.equals(ship.arena_angle, 0);
ship.setArenaFacingAngle(1.2);
ship.setArenaPosition(12, 50);
check.equals(ship.arena_x, 12);
check.equals(ship.arena_y, 50);
check.equals(ship.arena_angle, 0);
ship.moveTo(51, 50, engine);
check.equals(ship.arena_x, 51);
check.equals(ship.arena_y, 50);
check.equals(ship.arena_angle, 0);
ship.moveTo(50, 50, engine);
check.nears(ship.arena_angle, 3.14159265, 5);
ship.moveTo(51, 51, engine);
check.nears(ship.arena_angle, 0.785398, 5);
ship.moveTo(51, 52, engine);
check.nears(ship.arena_angle, 1.5707963, 5);
ship.moveTo(52, 52, engine);
check.equals(ship.arena_x, 52);
check.equals(ship.arena_y, 52);
check.equals(ship.arena_angle, 0);
ship.moveTo(52, 50, engine);
check.nears(ship.arena_angle, -1.5707963, 5);
ship.moveTo(50, 50, engine);
check.nears(ship.arena_angle, 3.14159265, 5);
let battle = new Battle();
battle.fleets[0].addShip(ship);
check.equals(battle.log.events, []);
ship.moveTo(70, 50, engine);
check.equals(battle.log.events, [new MoveEvent(ship, new ArenaLocationAngle(50, 50, Math.PI), new ArenaLocationAngle(70, 50, 0), engine)]);
battle.log.clear();
ship.rotate(2.1);
check.equals(battle.log.events, [
new MoveEvent(ship, new ArenaLocationAngle(70, 50, 0), new ArenaLocationAngle(70, 50, 2.1))
]);
battle.log.clear();
ship.moveTo(0, 0, null);
check.equals(battle.log.events, [
new MoveEvent(ship, new ArenaLocationAngle(70, 50, 2.1), new ArenaLocationAngle(0, 0, 2.1))
]);
});
test.case("applies equipment cooldown", check => {
let ship = new Ship();
let equipment = new Equipment(SlotType.Weapon);
equipment.cooldown.configure(1, 2);
ship.addSlot(SlotType.Weapon).attach(equipment);
check.same(equipment.cooldown.canUse(), true, "1");
equipment.cooldown.use();
check.same(equipment.cooldown.canUse(), false, "2");
ship.startBattle();
check.same(equipment.cooldown.canUse(), true, "3");
ship.startTurn();
equipment.cooldown.use();
check.same(equipment.cooldown.canUse(), false, "4");
ship.endTurn();
check.same(equipment.cooldown.canUse(), false, "5");
ship.startTurn();
check.same(equipment.cooldown.canUse(), false, "6");
ship.endTurn();
check.same(equipment.cooldown.canUse(), true, "7");
check.nears(ship.arena_angle, 1.2);
});
test.case("lists available actions from attached equipment", check => {
@ -149,207 +85,32 @@ module TK.SpaceTac.Specs {
test.case("repairs hull and recharges shield", check => {
var ship = new Ship(null, "Test");
ship.setAttribute("hull_capacity", 120);
ship.setAttribute("shield_capacity", 150);
TestTools.setAttribute(ship, "hull_capacity", 120);
TestTools.setAttribute(ship, "shield_capacity", 150);
check.equals(ship.values.hull.get(), 0);
check.equals(ship.values.shield.get(), 0);
check.equals(ship.getValue("hull"), 0);
check.equals(ship.getValue("shield"), 0);
ship.restoreHealth();
check.equals(ship.values.hull.get(), 120);
check.equals(ship.values.shield.get(), 150);
});
test.case("applies and logs hull and shield damage", check => {
var fleet = new Fleet();
var battle = new Battle(fleet);
var ship = new Ship(fleet);
TestTools.setShipHP(ship, 150, 400);
ship.restoreHealth();
battle.log.clear();
ship.addDamage(10, 20);
check.equals(ship.values.hull.get(), 140);
check.equals(ship.values.shield.get(), 380);
check.equals(battle.log.events.length, 3);
check.equals(battle.log.events[0], new ValueChangeEvent(ship, ship.values.shield, -20));
check.equals(battle.log.events[1], new ValueChangeEvent(ship, ship.values.hull, -10));
check.equals(battle.log.events[2], new DamageEvent(ship, 10, 20));
battle.log.clear();
ship.addDamage(15, 25, false);
check.equals(ship.values.hull.get(), 125);
check.equals(ship.values.shield.get(), 355);
check.equals(battle.log.events.length, 0);
ship.addDamage(125, 355, false);
check.equals(ship.values.hull.get(), 0);
check.equals(ship.values.shield.get(), 0);
check.equals(ship.alive, false);
});
test.case("sets and logs sticky effects", check => {
var ship = new Ship();
var battle = new Battle(ship.fleet);
ship.addStickyEffect(new StickyEffect(new BaseEffect("test"), 2, false, true));
check.equals(ship.sticky_effects, [new StickyEffect(new BaseEffect("test"), 2, false, true)]);
check.equals(battle.log.events, [
new ActiveEffectsEvent(ship, [], [new StickyEffect(new BaseEffect("test"), 2, false, true)])
]);
ship.startTurn();
battle.log.clear();
ship.endTurn();
check.equals(ship.sticky_effects, [new StickyEffect(new BaseEffect("test"), 1, false, true)]);
check.equals(battle.log.events, [
new ActiveEffectsEvent(ship, [], [new StickyEffect(new BaseEffect("test"), 1, false, true)])
]);
ship.startTurn();
battle.log.clear();
ship.endTurn();
check.equals(ship.sticky_effects, []);
check.equals(battle.log.events, [
new ActiveEffectsEvent(ship, [], [new StickyEffect(new BaseEffect("test"), 0, false, true)]),
new ActiveEffectsEvent(ship, [], [])
]);
ship.startTurn();
battle.log.clear();
ship.endTurn();
check.equals(ship.sticky_effects, []);
check.equals(battle.log.events, []);
});
test.case("resets toggle actions at the start of turn", check => {
let ship = new Ship();
let equ = ship.addSlot(SlotType.Weapon).attach(new Equipment(SlotType.Weapon));
let action = equ.action = new ToggleAction(equ, 0, 10, [new AttributeEffect("power_capacity", 1)]);
check.equals(action.activated, false);
let battle = new Battle(ship.fleet);
TestTools.setShipPlaying(battle, ship);
ship.startTurn();
check.equals(action.activated, false);
let result = action.apply(ship);
check.same(result, true, "Could not be applied");
check.equals(action.activated, true);
ship.endTurn();
check.equals(action.activated, true);
ship.startTurn();
check.equals(action.activated, false);
check.equals(battle.log.events, [
new ActionAppliedEvent(ship, action, Target.newFromShip(ship), 0),
new ToggleEvent(ship, action, true),
new ActiveEffectsEvent(ship, [], [], [new AttributeEffect("power_capacity", 1)]),
new ValueChangeEvent(ship, new ShipAttribute("power capacity", 1), 1),
new ActionAppliedEvent(ship, action, Target.newFromShip(ship), 0),
new ToggleEvent(ship, action, false),
new ActiveEffectsEvent(ship, [], [], []),
new ValueChangeEvent(ship, new ShipAttribute("power capacity", 0), -1),
]);
});
test.case("updates area effects when the ship moves", check => {
let battle = new Battle();
let ship1 = battle.fleets[0].addShip();
let ship2 = battle.fleets[0].addShip();
ship2.setArenaPosition(10, 0);
let ship3 = battle.fleets[0].addShip();
ship3.setArenaPosition(20, 0);
let shield = ship1.addSlot(SlotType.Shield).attach(new Equipment(SlotType.Shield));
shield.action = new ToggleAction(shield, 0, 15, [new AttributeEffect("shield_capacity", 5)]);
TestTools.setShipPlaying(battle, ship1);
shield.action.apply(ship1);
check.equals(ship1.getAttribute("shield_capacity"), 5);
check.equals(ship2.getAttribute("shield_capacity"), 5);
check.equals(ship3.getAttribute("shield_capacity"), 0);
ship1.moveTo(15, 0);
check.equals(ship1.getAttribute("shield_capacity"), 5);
check.equals(ship2.getAttribute("shield_capacity"), 5);
check.equals(ship3.getAttribute("shield_capacity"), 5);
ship1.moveTo(30, 0);
check.equals(ship1.getAttribute("shield_capacity"), 5);
check.equals(ship2.getAttribute("shield_capacity"), 0);
check.equals(ship3.getAttribute("shield_capacity"), 5);
ship1.moveTo(50, 0);
check.equals(ship1.getAttribute("shield_capacity"), 5);
check.equals(ship2.getAttribute("shield_capacity"), 0);
check.equals(ship3.getAttribute("shield_capacity"), 0);
ship2.moveTo(40, 0);
check.equals(ship1.getAttribute("shield_capacity"), 5);
check.equals(ship2.getAttribute("shield_capacity"), 5);
check.equals(ship3.getAttribute("shield_capacity"), 0);
});
test.case("sets and logs death state", check => {
var fleet = new Fleet();
var battle = new Battle(fleet);
var ship = new Ship(fleet);
check.equals(ship.alive, true);
ship.values.hull.set(10);
battle.log.clear();
ship.addDamage(5, 0);
check.equals(ship.alive, true);
check.equals(battle.log.events.length, 2);
check.equals(battle.log.events[0].code, "value");
check.equals(battle.log.events[1].code, "damage");
battle.log.clear();
ship.addDamage(5, 0);
check.equals(ship.alive, false);
check.equals(battle.log.events.length, 3);
check.equals(battle.log.events[0].code, "value");
check.equals(battle.log.events[1].code, "damage");
check.equals(battle.log.events[2].code, "death");
check.equals(ship.getValue("hull"), 120);
check.equals(ship.getValue("shield"), 150);
});
test.case("checks if a ship is able to play", check => {
let battle = new Battle();
let ship = battle.fleets[0].addShip();
ship.setValue("hull", 10);
check.equals(ship.isAbleToPlay(), false);
check.equals(ship.isAbleToPlay(false), true);
ship.values.power.set(5);
ship.setValue("power", 5);
check.equals(ship.isAbleToPlay(), true);
check.equals(ship.isAbleToPlay(false), true);
ship.values.hull.set(10);
ship.addDamage(8, 0);
check.equals(ship.isAbleToPlay(), true);
check.equals(ship.isAbleToPlay(false), true);
ship.addDamage(8, 0);
ship.setDead();
check.equals(ship.isAbleToPlay(), false);
check.equals(ship.isAbleToPlay(false), false);
@ -387,27 +148,6 @@ module TK.SpaceTac.Specs {
check.same(picked, ship.slots[2].attached);
});
test.case("recover action points at end of turn", check => {
var ship = new Ship();
let power_generator = new Equipment(SlotType.Power);
power_generator.effects = [
new AttributeEffect("power_capacity", 8),
new AttributeEffect("power_generation", 3),
]
ship.addSlot(SlotType.Power).attach(power_generator);
check.equals(ship.values.power.get(), 0);
ship.initializeActionPoints();
check.equals(ship.values.power.get(), 8);
ship.values.power.set(3);
check.equals(ship.values.power.get(), 3);
ship.recoverActionPoints();
check.equals(ship.values.power.get(), 6);
ship.recoverActionPoints();
check.equals(ship.values.power.get(), 8);
});
test.case("checks if a ship is inside a given circle", check => {
let ship = new Ship();
ship.arena_x = 5;
@ -565,24 +305,29 @@ module TK.SpaceTac.Specs {
let ship = new Ship();
TestTools.setShipHP(ship, 10, 20);
TestTools.setShipAP(ship, 5, 0);
ship.addDamage(5, 5);
ship.addStickyEffect(new StickyEffect(new DamageEffect(10), 8));
ship.addStickyEffect(new StickyEffect(new AttributeLimitEffect("power_capacity", 3), 12));
ship.setValue("hull", 5);
ship.setValue("shield", 15);
ship.setValue("power", 2);
ship.active_effects.add(new StickyEffect(new AttributeLimitEffect("power_capacity", 3), 12));
ship.updateAttributes();
check.equals(ship.getValue("hull"), 5);
check.equals(ship.getValue("shield"), 15);
check.equals(ship.getValue("power"), 3);
check.equals(ship.sticky_effects.length, 2);
check.equals(ship.getAttribute("power_capacity"), 3);
check.in("before", check => {
check.equals(ship.getValue("hull"), 5, "hull");
check.equals(ship.getValue("shield"), 15, "shield");
check.equals(ship.getValue("power"), 2, "power");
check.equals(ship.active_effects.count(), 1, "effects count");
check.equals(ship.getAttribute("power_capacity"), 3, "power capacity");
});
ship.endBattle(1);
ship.restoreInitialState();
check.equals(ship.getValue("hull"), 10);
check.equals(ship.getValue("shield"), 20);
check.equals(ship.getValue("power"), 5);
check.equals(ship.sticky_effects.length, 0);
check.equals(ship.getAttribute("power_capacity"), 5);
check.in("after", check => {
check.equals(ship.getValue("hull"), 10, "hull");
check.equals(ship.getValue("shield"), 20, "shield");
check.equals(ship.getValue("power"), 5, "power");
check.equals(ship.active_effects.count(), 0, "effects count");
check.equals(ship.getAttribute("power_capacity"), 5, "power capacity");
});
});
test.case("lists active effects", check => {
@ -592,16 +337,13 @@ module TK.SpaceTac.Specs {
let equipment = ship.addSlot(SlotType.Engine).attach(new Equipment(SlotType.Engine));
check.equals(imaterialize(ship.ieffects()), []);
equipment.effects.push(new AttributeEffect("precision", 4));
check.equals(imaterialize(ship.ieffects()), [
new AttributeEffect("precision", 4)
]);
let effect1 = new AttributeEffect("precision", 4);
equipment.effects.push(effect1);
check.equals(imaterialize(ship.ieffects()), [effect1]);
ship.addStickyEffect(new StickyEffect(new AttributeLimitEffect("precision", 2), 4));
check.equals(imaterialize(ship.ieffects()), [
new AttributeEffect("precision", 4),
new AttributeLimitEffect("precision", 2)
]);
let effect2 = new AttributeLimitEffect("precision", 2);
ship.active_effects.add(new StickyEffect(effect2, 4));
check.equals(imaterialize(ship.ieffects()), [effect1, effect2]);
});
test.case("gets a textual description of an attribute", check => {
@ -620,7 +362,7 @@ module TK.SpaceTac.Specs {
ship.upgradeSkill("skill_photons");
check.equals(ship.getAttributeDescription("skill_photons"), "Forces of light, and electromagnetic radiation\n\nLevelled up: +2\nPhotonic engine Mk1: +4");
ship.addStickyEffect(new StickyEffect(new AttributeLimitEffect("skill_photons", 3)));
ship.active_effects.add(new StickyEffect(new AttributeLimitEffect("skill_photons", 3)));
check.equals(ship.getAttributeDescription("skill_photons"), "Forces of light, and electromagnetic radiation\n\nLevelled up: +2\nPhotonic engine Mk1: +4\n???: limit to 3");
});
});

View file

@ -31,8 +31,8 @@ module TK.SpaceTac {
// Facing direction in the arena
arena_angle: number
// Sticky effects that applies a given number of times
sticky_effects: StickyEffect[]
// Active effects (sticky or area)
active_effects = new RObjectContainer<BaseEffect>()
// List of slots, able to contain equipment
slots: Slot[]
@ -63,7 +63,6 @@ module TK.SpaceTac {
this.fleet = fleet || new Fleet();
this.name = name;
this.alive = true;
this.sticky_effects = [];
this.slots = [];
this.arena_x = 0;
@ -95,7 +94,7 @@ module TK.SpaceTac {
// Returns true if the ship is able to play
// If *check_ap* is true, ap_current=0 will make this function return false
isAbleToPlay(check_ap: boolean = true): boolean {
var ap_checked = !check_ap || this.values.power.get() > 0;
var ap_checked = !check_ap || this.getValue("power") > 0;
return this.alive && ap_checked;
}
@ -126,6 +125,13 @@ module TK.SpaceTac {
return this.fleet.player;
}
/**
* Check if a player is playing this ship
*/
isPlayedBy(player: Player): boolean {
return this.getPlayer().is(player);
}
// get the current battle this ship is engaged in
getBattle(): Battle | null {
return this.fleet.battle;
@ -151,10 +157,17 @@ module TK.SpaceTac {
});
}
actions.push(new EndTurnAction());
actions.push(EndTurnAction.SINGLETON);
return actions;
}
/**
* Get an available action by its ID
*/
getAction(action_id: RObjectId): BaseAction | null {
return first(this.getAvailableActions(), action => action.is(action_id));
}
/**
* Get the number of upgrade points available to improve skills
*/
@ -167,14 +180,16 @@ module TK.SpaceTac {
* Try to upgrade a skill by 1 point or more
*/
upgradeSkill(skill: keyof ShipSkills, points = 1) {
if (this.getAvailableUpgradePoints() >= points) {
this.skills[skill].add(points);
if (this.getBattle()) {
console.error("Cannot upgrade skill during battle");
} else if (this.getAvailableUpgradePoints() >= points) {
this.skills[skill].addModifier(points);
this.updateAttributes();
}
}
// Add an event to the battle log, if any
addBattleEvent(event: BaseBattleEvent): void {
addBattleEvent(event: BaseBattleDiff): void {
var battle = this.getBattle();
if (battle && battle.log) {
battle.log.add(event);
@ -185,46 +200,17 @@ module TK.SpaceTac {
* Get a ship value
*/
getValue(name: keyof ShipValues): number {
if (!this.values.hasOwnProperty(name)) {
console.error(`No such ship value: ${name}`);
return 0;
}
return this.values[name].get();
return this.values[name];
}
/**
* Set a ship value
*
* If *offset* is true, the value will be added to current value.
* If *log* is true, an attribute event will be added to the battle log
*
* Returns true if the value changed.
*/
setValue(name: keyof ShipValues, value: number, offset = false, log = true): boolean {
let diff = 0;
let val = this.values[name];
if (offset) {
diff = val.add(value);
} else {
diff = val.set(value);
setValue(name: keyof ShipValues, value: number, relative = false): void {
if (relative) {
value += this.values[name];
}
if (log && diff != 0 && this.alive) {
this.addBattleEvent(new ValueChangeEvent(this, val, diff));
}
return diff != 0;
}
/**
* Set a value's maximal capacity
*/
setValueCapacity(name: keyof ShipValues, maximal: number, log = true): void {
if (this.getValue(name) > maximal) {
this.setValue(name, maximal, false, log);
}
this.values[name].setMaximal(maximal);
this.values[name] = value;
}
/**
@ -238,55 +224,16 @@ module TK.SpaceTac {
return this.attributes[name].get();
}
/**
* Set a ship attribute
*
* If *log* is true, an attribute event will be added to the battle log
*
* Returns true if the value changed.
*/
setAttribute(name: keyof ShipAttributes, value: number, log = true): boolean {
let attr = this.attributes[name];
let diff = attr.set(value);
// TODO more generic
if (name == "power_capacity") {
this.setValueCapacity("power", attr.get());
} else if (name == "shield_capacity") {
this.setValueCapacity("shield", attr.get());
} else if (name == "hull_capacity") {
this.setValueCapacity("hull", attr.get());
}
if (log && diff != 0 && this.alive) {
this.addBattleEvent(new ValueChangeEvent(this, attr, diff));
}
return diff != 0;
}
// Initialize the action points counter
// This should be called once at the start of a battle
// If no value is provided, the attribute ap_initial will be used
initializeActionPoints(value: number | null = null): void {
// If no value is provided, the attribute power_capacity will be used
private initializePower(value: number | null = null): void {
if (value === null) {
value = this.attributes.power_capacity.get();
value = this.getAttribute("power_capacity");
}
this.setValue("power", value);
}
// Recover action points
// This should be called once at the end of a turn
// If no value is provided, the current attribute ap_recovery will be used
recoverActionPoints(value: number | null = null): void {
if (this.alive) {
if (value === null) {
value = this.attributes.power_generation.get();
}
this.setValue("power", value, true);
}
}
/**
* Consumes action points
*
@ -302,102 +249,17 @@ module TK.SpaceTac {
}
/**
* Method called at the start of battle
* Method called at the start of battle, to restore a pristine condition on the ship
*/
startBattle() {
restoreInitialState() {
this.alive = true;
this.sticky_effects = [];
this.active_effects = new RObjectContainer();
this.updateAttributes();
this.restoreHealth();
this.initializeActionPoints();
this.initializePower();
this.listEquipment().forEach(equipment => equipment.cooldown.reset());
}
/**
* Method called at the end of battle
*/
endBattle(turncount: number) {
// Restore as pristine
this.startBattle();
// Wear down equipment
this.listEquipment().forEach(equipment => {
equipment.addWear(turncount);
});
}
// Method called at the start of this ship turn
startTurn(): void {
if (this.playing) {
console.error("startTurn called twice", this);
return;
}
this.playing = true;
if (this.alive) {
// Recompute attributes
this.updateAttributes();
// Apply sticky effects
this.sticky_effects.forEach(effect => effect.startTurn(this));
this.cleanStickyEffects();
// Reset toggle actions state
this.listEquipment().forEach(equipment => {
if (equipment.action instanceof ToggleAction && equipment.action.activated) {
equipment.action.apply(this);
}
});
}
}
// Method called at the end of this ship turn
endTurn(): void {
if (!this.playing) {
console.error("endTurn called before startTurn", this);
return;
}
this.playing = false;
if (this.alive) {
// Recover action points for next turn
this.updateAttributes();
this.recoverActionPoints();
// Apply sticky effects
this.sticky_effects.forEach(effect => effect.endTurn(this));
this.cleanStickyEffects();
// Cool down equipment
this.listEquipment().forEach(equipment => equipment.cooldown.cool());
}
}
/**
* Register a sticky effect
*
* Pay attention to pass a copy, not the original equipment effect, because it will be modified
*/
addStickyEffect(effect: StickyEffect, log = true): void {
if (this.alive) {
this.sticky_effects.push(effect);
if (log) {
this.setActiveEffectsChanged();
}
}
}
/**
* Clean sticky effects that are no longer active
*/
cleanStickyEffects() {
let [active, ended] = binpartition(this.sticky_effects, effect => this.alive && effect.duration > 0);
this.sticky_effects = active;
if (ended.length) {
this.setActiveEffectsChanged();
}
}
/**
* Check if the ship is inside a given circular area
*/
@ -416,86 +278,38 @@ module TK.SpaceTac {
}
/**
* Rotate the ship in place to face a direction
* Get the diffs needed to apply changes to a ship value
*/
rotate(angle: number, engine: Equipment | null = null, log = true) {
if (angle != this.arena_angle) {
let start = copy(this.location);
this.setArenaFacingAngle(angle);
if (log) {
this.addBattleEvent(new MoveEvent(this, start, copy(this.location), engine));
}
}
}
/**
* Move the ship to another location
*
* This does not check or consume action points, but will update area effects (for this ship and the others).
*
* If *engine* is specified, the facing angle will be updated to simulate an engine maneuver.
*/
moveTo(x: number, y: number, engine: Equipment | null = null, log = true): void {
let dx = x - this.arena_x;
let dy = y - this.arena_y;
if (dx != 0 || dy != 0) {
let start = copy(this.location);
let area_effects = imaterialize(this.iToggleActions(true));
let old_impacted_ships = area_effects.map(action => action.getImpactedShips(this, Target.newFromShip(this)));
let old_area_effects = this.getActiveEffects().area;
if (engine) {
let angle = Math.atan2(dy, dx);
this.setArenaFacingAngle(angle);
}
this.setArenaPosition(x, y);
if (log) {
this.addBattleEvent(new MoveEvent(this, start, copy(this.location), engine));
}
let new_impacted_ships = area_effects.map(action => action.getImpactedShips(this, Target.newFromShip(this)));
let diff_impacted_ships = flatten(zip(old_impacted_ships, new_impacted_ships).map(([a, b]) => disjunctunion(a, b)));
let new_area_effects = this.getActiveEffects().area;
if (disjunctunion(old_area_effects, new_area_effects).length > 0) {
diff_impacted_ships.push(this);
}
unique(diff_impacted_ships).forEach(ship => ship.setActiveEffectsChanged());
}
}
/**
* Get the events needed to apply changes to ship values or attributes
*/
getValueEvents(name: keyof ShipValues, value: number): BaseBattleEvent[] {
let result: BaseBattleEvent[] = [];
getValueDiffs(name: keyof ShipValues, value: number, relative = false): BaseBattleDiff[] {
let result: BaseBattleDiff[] = [];
let current = this.values[name];
if (current.get() != value) {
let newval = copy(current);
newval.set(value);
result.push(new ValueChangeEvent(this, newval, value - current.get()));
if (relative) {
value += current;
}
// TODO apply range limitations
if (current != value) {
result.push(new ShipValueDiff(this, name, value - current));
}
return result;
}
/**
* Produce events to set the ship in emergency stasis
* Produce diffs needed to put the ship in emergency stasis
*/
getDeathEvents(battle: Battle): BaseBattleEvent[] {
let result: BaseBattleEvent[] = [];
getDeathDiffs(battle: Battle): BaseBattleDiff[] {
let result: BaseBattleDiff[] = [];
keys(SHIP_VALUES).forEach(value => {
result = result.concat(this.getValueEvents(value, 0));
result = result.concat(this.getValueDiffs(value, 0));
});
// TODO Remove sticky effects
result.push(new DeathEvent(battle, this));
result.push(new ShipDeathDiff(battle, this));
return result;
}
@ -506,37 +320,13 @@ module TK.SpaceTac {
setDead(): void {
let battle = this.getBattle();
if (battle) {
let events = this.getDeathEvents(battle);
battle.applyEvents(events);
let events = this.getDeathDiffs(battle);
battle.applyDiffs(events);
} else {
console.error("Cannot set ship dead outside of battle", this);
}
}
/**
* Apply damages to hull and/or shield
*
* Also apply wear to impacted equipment
*/
addDamage(hull: number, shield: number, log: boolean = true): void {
if (shield > 0) {
this.setValue("shield", -shield, true, log);
}
if (hull > 0) {
this.setValue("hull", -hull, true, log);
}
if (log) {
this.addBattleEvent(new DamageEvent(this, hull, shield));
}
if (this.values.hull.get() === 0) {
// Ship is dead
this.setDead();
}
}
/**
* Get cargo space not occupied by items
*/
@ -686,70 +476,52 @@ module TK.SpaceTac {
}
}
// Update attributes, taking into account attached equipment and active effects
/**
* Get the list of equipped items
*/
listEquipments(): Equipment[] {
return nna(this.slots.map(slot => slot.attached));
}
/**
* Get an equipment by its ID
*/
getEquipment(id: RObjectId): Equipment | null {
return first(this.listEquipments(), equipment => equipment.id === id);
}
/**
* Update attributes, taking into account attached equipment and active effects
*/
updateAttributes(): void {
let new_attrs = new ShipAttributes();
// Reset attributes
keys(this.attributes).forEach(attr => this.attributes[attr].reset());
if (this.alive) {
// TODO better typing for iteritems
// Apply base skills
keys(this.skills).forEach(skill => this.attributes[skill].addModifier(this.skills[skill].get()));
// Apply base skills
iteritems(<any>this.skills, (key: keyof ShipAttributes, skill: ShipAttribute) => {
new_attrs[key].add(skill.get());
});
// Sum all attribute effects
this.collectEffects("attr").forEach((effect: AttributeEffect) => {
new_attrs[effect.attrcode].add(effect.value);
});
// Apply limit attributes
this.collectEffects("attrlimit").forEach((effect: AttributeLimitEffect) => {
new_attrs[effect.attrcode].setMaximal(effect.value);
});
}
// Set final attributes
iteritems(<any>new_attrs, (key, value) => {
this.setAttribute(<keyof ShipAttributes>key, (<ShipAttribute>value).get());
// Apply attribute effects
iforeach(this.ieffects(), effect => {
if (effect instanceof AttributeEffect) {
this.attributes[effect.attrcode].addModifier(effect.value);
} else if (effect instanceof AttributeMultiplyEffect) {
this.attributes[effect.attrcode].addModifier(undefined, effect.value);
} else if (effect instanceof AttributeLimitEffect) {
this.attributes[effect.attrcode].addModifier(undefined, undefined, effect.value);
}
});
}
// Fully restore hull and shield
/**
* Fully restore hull and shield, at their maximal capacity
*/
restoreHealth(): void {
if (this.alive) {
this.values.hull.set(this.attributes.hull_capacity.get());
this.values.shield.set(this.attributes.shield_capacity.get());
this.setValue("hull", this.getAttribute("hull_capacity"));
this.setValue("shield", this.getAttribute("shield_capacity"));
}
}
/**
* Get the list of all effects applied on this ship
*
* This includes:
* - Permanent equipment effects
* - Sticky effects
* - Area effects at current location
*/
getActiveEffects(): ActiveEffectsEvent {
let result = new ActiveEffectsEvent(this);
if (this.alive) {
result.equipment = flatten(this.slots.map(slot => slot.attached ? slot.attached.effects : []));
result.sticky = this.sticky_effects;
let battle = this.getBattle();
result.area = battle ? imaterialize(battle.iAreaEffects(this.arena_x, this.arena_y)) : [];
}
return result;
}
/**
* Indicate a change in active effects to the log
*/
setActiveEffectsChanged(): void {
this.addBattleEvent(this.getActiveEffects());
this.updateAttributes();
}
/**
* Iterator over all effects active for this ship.
*/
@ -758,7 +530,7 @@ module TK.SpaceTac {
let area_effects = battle ? battle.iAreaEffects(this.arena_x, this.arena_y) : IEMPTY;
return ichain(
ichainit(imap(iarray(this.slots), slot => slot.attached ? iarray(slot.attached.effects) : IEMPTY)),
imap(iarray(this.sticky_effects), effect => effect.base),
imap(this.active_effects.iterator(), effect => (effect instanceof StickyEffect) ? effect.base : effect),
area_effects
);
}
@ -786,16 +558,11 @@ module TK.SpaceTac {
}));
}
// Collect all effects to apply for updateAttributes
private collectEffects(code: string): BaseEffect[] {
return imaterialize(ifilter(this.ieffects(), effect => effect.code == code));
}
/**
* Get a textual description of an attribute, and the origin of its value
*/
getAttributeDescription(attribute: keyof ShipAttributes): string {
let result = this.attributes[attribute].description;
let result = SHIP_VALUES_DESCRIPTIONS[attribute];
let diffs: string[] = [];
let limits: string[] = [];
@ -822,7 +589,9 @@ module TK.SpaceTac {
}
});
this.sticky_effects.forEach(effect => addEffect("???", effect.base));
this.active_effects.list().forEach(effect => {
addEffect("???", (effect instanceof StickyEffect) ? effect.base : effect)
});
let sources = diffs.concat(limits).join("\n");
return sources ? (result + "\n\n" + sources) : result;

View file

@ -1,72 +1,29 @@
module TK.SpaceTac {
testing("ShipValue", test => {
test.case("is initially not limited", check => {
var attr = new ShipValue("test");
testing("ShipAttribute", test => {
test.case("applies cumulative, multiplier and limit", check => {
let attribute = new ShipAttribute();
check.equals(attribute.get(), 0, "initial");
attr.set(8888888);
check.equals(attr.get(), 8888888);
});
attribute.addModifier(4);
check.equals(attribute.get(), 4, "added 4");
test.case("applies minimal and maximal value", check => {
var attr = new ShipValue("test", 50, 100);
check.equals(attr.get(), 50);
attribute.addModifier(2);
check.equals(attribute.get(), 6, "added 6");
attr.add(8);
check.equals(attr.get(), 58);
attribute.addModifier(undefined, 20);
check.equals(attribute.get(), 7, "added 20%");
attr.add(60);
check.equals(attr.get(), 100);
attribute.addModifier(undefined, 5);
check.equals(attribute.get(), 8, "added 5%");
attr.add(-72);
check.equals(attr.get(), 28);
attribute.addModifier(undefined, undefined, 6);
check.equals(attribute.get(), 6, "limited to 6");
attr.add(-60);
check.equals(attr.get(), 0);
attribute.addModifier(undefined, undefined, 4);
check.equals(attribute.get(), 4, "limited to 4");
attr.set(8);
check.equals(attr.get(), 8);
attr.set(-4);
check.equals(attr.get(), 0);
attr.set(105);
check.equals(attr.get(), 100);
attr.setMaximal(50);
check.equals(attr.get(), 50);
attr.setMaximal(80);
check.equals(attr.get(), 50);
});
test.case("tells the value variation", check => {
var result: number;
var attr = new ShipValue("test", 50, 100);
check.equals(attr.get(), 50);
result = attr.set(51);
check.equals(result, 1);
result = attr.set(51);
check.equals(result, 0);
result = attr.add(1);
check.equals(result, 1);
result = attr.add(0);
check.equals(result, 0);
result = attr.add(1000);
check.equals(result, 48);
result = attr.add(2000);
check.equals(result, 0);
result = attr.set(-500);
check.same(result, -100);
result = attr.add(-600);
check.equals(result, 0);
attribute.addModifier(undefined, undefined, 10);
check.equals(attribute.get(), 4, "limited to 10");
});
});
}

View file

@ -1,41 +1,55 @@
module TK.SpaceTac {
const SHIP_VALUES_DESCRIPTIONS: { [name: string]: string } = {
"materials skill": "Usage of physical materials such as bullets, shells...",
"photons skill": "Forces of light, and electromagnetic radiation",
"antimatter skill": "Manipulation of matter and antimatter particles",
"quantum skill": "Application of quantum uncertainty principle",
"gravity skill": "Interaction with gravitational forces",
"time skill": "Control of relativity's time properties",
"hull capacity": "Maximal Hull value before the ship risks collapsing",
"shield capacity": "Maximal Shield value to protect the hull from damage",
"power capacity": "Maximal Power value to use equipment",
"power generation": "Power generated at the end of the ship's turn",
type ShipValuesMapping = {
[P in (keyof ShipValues | keyof ShipAttributes)]: string
}
export const SHIP_VALUES_DESCRIPTIONS: ShipValuesMapping = {
"hull": "Physical structure of the ship",
"shield": "Shield around the ship that may absorb damage",
"power": "Power available to supply the equipments",
"skill_materials": "Usage of physical materials such as bullets, shells...",
"skill_photons": "Forces of light, and electromagnetic radiation",
"skill_antimatter": "Manipulation of matter and antimatter particles",
"skill_quantum": "Application of quantum uncertainty principle",
"skill_gravity": "Interaction with gravitational forces",
"skill_time": "Control of relativity's time properties",
"hull_capacity": "Maximal Hull value before the ship risks collapsing",
"shield_capacity": "Maximal Shield value to protect the hull from damage",
"power_capacity": "Maximal Power value to use equipment",
"power_generation": "Power generated at the end of the ship's turn",
"maneuvrability": "Ability to move first and fast",
"precision": "Ability to target far and good",
}
export const SHIP_VALUES_NAMES: ShipValuesMapping = {
"hull": "hull",
"shield": "shield",
"power": "power",
"skill_materials": "materials skill",
"skill_photons": "photons skill",
"skill_antimatter": "antimatter skill",
"skill_quantum": "quantum skill",
"skill_gravity": "gravity skill",
"skill_time": "time skill",
"hull_capacity": "hull capacity",
"shield_capacity": "shield capacity",
"power_capacity": "power capacity",
"power_generation": "power generation",
"maneuvrability": "maneuvrability",
"precision": "precision",
}
/**
* A ship value is a number that may vary and be constrained in a given range.
* A ship attribute is a number resulting of a list of modifiers.
*/
export class ShipValue {
// Name of the value
name: string
export class ShipAttribute {
// Current value
private current: number
private current = 0
// Upper bound
private maximal: number | null
constructor(code: string, current = 0, maximal: number | null = null) {
this.name = code;
this.current = current;
this.maximal = maximal;
}
get description(): string {
return SHIP_VALUES_DESCRIPTIONS[this.name];
}
// Modifiers
private cumulatives: number[] = []
private multipliers: number[] = []
private limits: number[] = []
/**
* Get the current value
@ -45,68 +59,60 @@ module TK.SpaceTac {
}
/**
* Get the maximal value
* Reset all modifiers
*/
getMaximal(): number | null {
return this.maximal;
reset(): void {
this.cumulatives = [];
this.multipliers = [];
this.limits = [];
this.update();
}
/**
* Set the upper bound the value must not cross
* Add a modifier
*/
setMaximal(value: number): void {
this.maximal = value;
this.fix();
addModifier(cumulative?: number, multiplier?: number, limit?: number): void {
if (typeof cumulative != "undefined") {
this.cumulatives.push(cumulative);
}
if (typeof multiplier != "undefined") {
this.multipliers.push(multiplier);
}
if (typeof limit != "undefined") {
this.limits.push(limit);
}
this.update();
}
/**
* Set an absolute value
*
* Returns the variation in value
* Remove a modifier
*/
set(value: number): number {
var old_value = this.current;
removeModifier(cumulative?: number, multiplier?: number, limit?: number): void {
if (typeof cumulative != "undefined") {
remove(this.cumulatives, cumulative);
}
if (typeof multiplier != "undefined") {
remove(this.multipliers, multiplier);
}
if (typeof limit != "undefined") {
remove(this.limits, limit);
}
this.update();
}
/**
* Update the current value
*/
private update(): void {
let value = sum(this.cumulatives);
if (this.multipliers.length) {
value = Math.round(value * (1 + sum(this.multipliers) / 100));
}
if (this.limits.length) {
value = Math.min(value, min(this.limits));
}
this.current = value;
this.fix();
return this.current - old_value;
}
/**
* Add an offset to current value
*
* Returns true if the value changed
*/
add(value: number): number {
var old_value = this.current;
this.current += value;
this.fix();
return this.current - old_value;
}
/**
* Fix the value to be positive and lower than maximal
*/
private fix(): void {
if (this.maximal !== null && this.current > this.maximal) {
this.current = this.maximal;
}
if (this.current < 0) {
this.current = 0;
}
}
}
/**
* A ship attribute is a value computed by a sum of contributions from equipments and sticky effects.
*
* A value may be limited by other effects.
*/
export class ShipAttribute extends ShipValue {
// Raw contributions value (without limits)
private raw = 0
// Temporary limits
private limits: number[] = []
}
/**
@ -114,12 +120,12 @@ module TK.SpaceTac {
*/
export class ShipSkills {
// Skills
skill_materials = new ShipAttribute("materials skill")
skill_photons = new ShipAttribute("photons skill")
skill_antimatter = new ShipAttribute("antimatter skill")
skill_quantum = new ShipAttribute("quantum skill")
skill_gravity = new ShipAttribute("gravity skill")
skill_time = new ShipAttribute("time skill")
skill_materials = new ShipAttribute()
skill_photons = new ShipAttribute()
skill_antimatter = new ShipAttribute()
skill_quantum = new ShipAttribute()
skill_gravity = new ShipAttribute()
skill_time = new ShipAttribute()
}
/**
@ -127,32 +133,42 @@ module TK.SpaceTac {
*/
export class ShipAttributes extends ShipSkills {
// Maximal hull value
hull_capacity = new ShipAttribute("hull capacity")
hull_capacity = new ShipAttribute()
// Maximal shield value
shield_capacity = new ShipAttribute("shield capacity")
shield_capacity = new ShipAttribute()
// Maximal power value
power_capacity = new ShipAttribute("power capacity")
power_capacity = new ShipAttribute()
// Power value recovered each turn
power_generation = new ShipAttribute("power generation")
power_generation = new ShipAttribute()
// Ability to move first and fast
maneuvrability = new ShipAttribute("maneuvrability")
maneuvrability = new ShipAttribute()
// Ability to fire far and good
precision = new ShipAttribute("precision")
precision = new ShipAttribute()
}
/**
* Set of ShipValue for a ship
* Set of simple values for a ship
*/
export class ShipValues {
hull = new ShipValue("hull")
shield = new ShipValue("shield")
power = new ShipValue("power")
hull = 0
shield = 0
power = 0
}
/**
* Static attributes and values object for name queries
* Static attributes and values object for property queries
*/
export const SHIP_SKILLS = new ShipSkills();
export const SHIP_ATTRIBUTES = new ShipAttributes();
export const SHIP_VALUES = new ShipValues();
/**
* Type guards
*/
export function isShipValue(key: string): key is keyof ShipValues {
return SHIP_VALUES.hasOwnProperty(key);
}
export function isShipAttribute(key: string): key is keyof ShipAttributes {
return SHIP_ATTRIBUTES.hasOwnProperty(key);
}
}

View file

@ -1,6 +1,8 @@
module TK.SpaceTac.Specs {
testing("Slot", test => {
test.case("checks equipment type", check => {
check.patch(console, "warn", null);
var ship = new Ship();
var slot = ship.addSlot(SlotType.Engine);
@ -18,6 +20,8 @@ module TK.SpaceTac.Specs {
});
test.case("checks equipment capabilities", check => {
check.patch(console, "warn", null);
var ship = new Ship();
var slot = ship.addSlot(SlotType.Shield);
@ -29,7 +33,7 @@ module TK.SpaceTac.Specs {
slot.attach(equipment);
check.equals(slot.attached, null);
ship.attributes.skill_gravity.set(6);
TestTools.setAttribute(ship, "skill_gravity", 6);
slot.attach(equipment);
check.same(slot.attached, equipment);

View file

@ -35,6 +35,8 @@ module TK.SpaceTac {
if (this.ship) {
this.ship.updateAttributes();
}
} else {
console.warn("Equipment cannot be attached to slot", equipment, this);
}
return equipment;
}

View file

@ -5,13 +5,15 @@ module TK.SpaceTac.Specs {
var fleet = new Fleet();
fleet.addShip();
location.encounter_random = new SkewedRandomGenerator([0]);
var battle = location.enterLocation(fleet);
var battle = nn(location.enterLocation(fleet));
check.notequals(location.encounter, null);
check.notequals(battle, null);
nn(battle).endBattle(fleet);
battle.endBattle(fleet);
check.notequals(location.encounter, null);
location.resolveEncounter(nn(battle.outcome));
check.equals(location.encounter, null);
});
@ -20,13 +22,15 @@ module TK.SpaceTac.Specs {
var fleet = new Fleet();
fleet.addShip();
location.encounter_random = new SkewedRandomGenerator([0]);
var battle = location.enterLocation(fleet);
var battle = nn(location.enterLocation(fleet));
check.notequals(location.encounter, null);
check.notequals(battle, null);
nn(battle).endBattle(location.encounter);
battle.endBattle(location.encounter);
check.notequals(location.encounter, null);
location.resolveEncounter(nn(battle.outcome));
check.notequals(location.encounter, null);
});
});

View file

@ -90,17 +90,9 @@ module TK.SpaceTac {
// *fleet* is the player fleet, entering the location
// Returns the engaged battle, null if no encounter happens
enterLocation(fleet: Fleet): Battle | null {
var encounter = this.tryGenerateEncounter();
let encounter = this.tryGenerateEncounter();
if (encounter) {
var battle = new Battle(fleet, encounter);
battle.log.subscribe(event => {
if (event instanceof EndBattleEvent) {
if (!event.outcome.draw && event.outcome.winner !== encounter) {
// The encounter fleet lost, remove it
this.encounter = null;
}
}
});
let battle = new Battle(fleet, encounter);
battle.start();
return battle;
} else {
@ -144,5 +136,14 @@ module TK.SpaceTac {
let [level, enemies] = this.encounter_random.choice(variations);
this.encounter = fleet_generator.generate(level, new Player(this.star.universe, "Enemy"), enemies, true);
}
/**
* Resolves the encounter from a battle outcome
*/
resolveEncounter(outcome: BattleOutcome) {
if (this.encounter && outcome.winner && outcome.winner != this.encounter) {
this.clearEncounter();
}
}
}
}

View file

@ -6,7 +6,7 @@ module TK.SpaceTac.Specs {
target = Target.newFromLocation(2, 3);
check.equals(target.x, 2);
check.equals(target.y, 3);
check.equals(target.ship, null);
check.equals(target.ship_id, null);
var ship = new Ship();
ship.arena_x = 4;
@ -14,7 +14,7 @@ module TK.SpaceTac.Specs {
target = Target.newFromShip(ship);
check.equals(target.x, 4);
check.equals(target.y, -2.1);
check.same(target.ship, ship);
check.equals(target.ship_id, ship.id);
});
test.case("gets distance to another target", check => {

View file

@ -33,22 +33,22 @@ module TK.SpaceTac {
// This could be a location in space, or a ship
export class Target {
// Coordinates of the target
x: number;
y: number;
x: number
y: number
// If the target is a ship, this attribute will be set
ship: Ship | null;
ship_id: RObjectId | null
// Standard constructor
constructor(x: number, y: number, ship: Ship | null = null) {
this.x = x;
this.y = y;
this.ship = ship;
this.ship_id = ship ? ship.id : null;
}
jasmineToString() {
if (this.ship) {
return `(${this.x},${this.y}) ${this.ship.jasmineToString()}`;
if (this.ship_id) {
return `(${this.x},${this.y}) ship_id=${this.ship_id}}`;
} else {
return `(${this.x},${this.y})`;
}
@ -78,6 +78,24 @@ module TK.SpaceTac {
return Math.atan2(dy, dx);
}
/**
* Returns true if the target is a ship
*/
isShip(): boolean {
return this.ship_id !== null;
}
/**
* Get the targetted ship in a battle
*/
getShip(battle: Battle): Ship | null {
if (this.isShip()) {
return battle.getShip(this.ship_id);
} else {
return null;
}
}
// Check if a target is in range from a specific point
isInRange(x: number, y: number, radius: number): boolean {
var dx = this.x - x;

View file

@ -3,7 +3,7 @@ module TK.SpaceTac {
export class TestTools {
// Create a battle between two fleets, with a fixed play order (owned ships, then enemy ships)
static createBattle(own_ships: number, enemy_ships: number): Battle {
static createBattle(own_ships = 1, enemy_ships = 0): Battle {
var fleet1 = new Fleet();
var fleet2 = new Fleet();
@ -15,8 +15,9 @@ module TK.SpaceTac {
}
var battle = new Battle(fleet1, fleet2);
battle.ships.list().forEach(ship => TestTools.setShipHP(ship, 1, 0));
battle.play_order = fleet1.ships.concat(fleet2.ships);
battle.advanceToNextShip();
battle.setPlayingShip(battle.play_order[0]);
return battle;
}
@ -61,7 +62,7 @@ module TK.SpaceTac {
}
// Set a ship action points, adding/updating an equipment if needed
static setShipAP(ship: Ship, points: number, recovery: number = 0): void {
static setShipAP(ship: Ship, points: number, recovery: number = 0): Equipment {
var equipment = this.getOrGenEquipment(ship, SlotType.Power, new Equipments.NuclearReactor());
equipment.effects.forEach(effect => {
@ -76,14 +77,16 @@ module TK.SpaceTac {
ship.updateAttributes();
ship.setValue("power", points);
return equipment;
}
// Set a ship hull and shield points, adding/updating an equipment if needed
static setShipHP(ship: Ship, hull_points: number, shield_points: number): void {
var armor = TestTools.getOrGenEquipment(ship, SlotType.Hull, new Equipments.IronHull());
static setShipHP(ship: Ship, hull_points: number, shield_points: number): [Equipment, Equipment] {
var hull = TestTools.getOrGenEquipment(ship, SlotType.Hull, new Equipments.IronHull());
var shield = TestTools.getOrGenEquipment(ship, SlotType.Shield, new Equipments.ForceField());
armor.effects.forEach(effect => {
hull.effects.forEach(effect => {
if (effect instanceof AttributeEffect) {
if (effect.attrcode === "hull_capacity") {
effect.value = hull_points;
@ -100,6 +103,58 @@ module TK.SpaceTac {
ship.updateAttributes();
ship.restoreHealth();
return [hull, shield];
}
/**
* Force a ship attribute to a given value
*/
static setAttribute(ship: Ship, name: keyof ShipAttributes, value: number): void {
let attr = ship.attributes[name];
attr.reset();
attr.addModifier(value);
}
/**
* Check a diff chain on a given battle
*
* This will apply all diffs, then reverts them, checking at each step the battle state
*/
static diffChain(check: TestContext, battle: Battle, diffs: BaseBattleDiff[], checks: ((check: TestContext) => void)[]): void {
checks[0](check.sub("initial state"));
for (let i = 0; i < diffs.length; i++) {
diffs[i].apply(battle);
checks[i + 1](check.sub(`after diff ${i + 1} applied`));
}
for (let i = diffs.length - 1; i >= 0; i--) {
diffs[i].revert(battle);
checks[i](check.sub(`after diff ${i + 1} reverted`));
}
}
/**
* Check an action chain on a given battle
*
* This will apply all actions, then reverts them, checking at each step the battle state
*/
static actionChain(check: TestContext, battle: Battle, actions: [Ship, BaseAction, Target | undefined][], checks: ((check: TestContext) => void)[]): void {
checks[0](check.sub("initial state"));
for (let i = 0; i < actions.length; i++) {
let [ship, action, target] = actions[i];
battle.setPlayingShip(ship);
let result = battle.applyOneAction(action, target);
check.equals(result, true, `action ${i + 1} successfully applied`);
checks[i + 1](check.sub(`after action ${i + 1} applied`));
}
for (let i = actions.length - 1; i >= 0; i--) {
battle.revertOneAction();
checks[i](check.sub(`after action ${i + 1} reverted`));
}
}
}
}

View file

@ -1,4 +1,4 @@
module TK.SpaceTac {
module TK.SpaceTac.Specs {
testing("BaseAction", test => {
test.case("check if equipment can be used with remaining AP", check => {
var equipment = new Equipment(SlotType.Hull);
@ -6,22 +6,21 @@ module TK.SpaceTac {
check.patch(action, "getActionPointsUsage", () => 3);
var ship = new Ship();
ship.addSlot(SlotType.Hull).attach(equipment);
ship.values.power.setMaximal(10);
check.equals(action.checkCannotBeApplied(ship), "not enough power");
ship.values.power.set(5);
ship.setValue("power", 5);
check.equals(action.checkCannotBeApplied(ship), null);
check.equals(action.checkCannotBeApplied(ship, 4), null);
check.equals(action.checkCannotBeApplied(ship, 3), null);
check.equals(action.checkCannotBeApplied(ship, 2), "not enough power");
ship.values.power.set(3);
ship.setValue("power", 3);
check.equals(action.checkCannotBeApplied(ship), null);
ship.values.power.set(2);
ship.setValue("power", 2);
check.equals(action.checkCannotBeApplied(ship), "not enough power");
})
@ -67,20 +66,23 @@ module TK.SpaceTac {
})
test.case("wears down equipment and power generators", check => {
let ship = new Ship();
let battle = TestTools.createBattle();
let ship = battle.play_order[0];
TestTools.setShipAP(ship, 10);
let power = ship.listEquipment(SlotType.Power)[0];
let equipment = new Equipment(SlotType.Weapon);
let action = new BaseAction("test", "Test", equipment);
equipment.action = action;
ship.addSlot(SlotType.Weapon).attach(equipment);
check.patch(action, "checkTarget", (ship: Ship, target: Target) => target);
check.equals(power.wear, 0);
check.equals(equipment.wear, 0);
action.apply(ship);
check.equals(power.wear, 0, "power wear");
check.equals(equipment.wear, 0, "equipment wear");
action.apply(battle, ship);
check.equals(power.wear, 1);
check.equals(equipment.wear, 1);
check.equals(power.wear, 1, "power wear");
check.equals(equipment.wear, 1, "equipment wear");
})
});
}

View file

@ -22,7 +22,7 @@ module TK.SpaceTac {
*
* An action should be the only way to modify a battle state.
*/
export class BaseAction {
export class BaseAction extends RObject {
// Identifier code for the type of action
code: string
@ -34,6 +34,8 @@ module TK.SpaceTac {
// Create the action
constructor(code = "nothing", name = "Idle", equipment: Equipment | null = null) {
super();
this.code = code;
this.name = name;
this.equipment = equipment;
@ -91,7 +93,7 @@ module TK.SpaceTac {
// Check AP usage
if (remaining_ap === null) {
remaining_ap = ship.values.power.get();
remaining_ap = ship.getValue("power");
}
var ap_usage = this.getActionPointsUsage(ship, null);
if (remaining_ap < ap_usage) {
@ -150,7 +152,7 @@ module TK.SpaceTac {
if (this.checkCannotBeApplied(ship)) {
return null;
} else {
if (target.ship) {
if (target.isShip()) {
return this.checkShipTarget(ship, target);
} else {
return this.checkLocationTarget(ship, target);
@ -171,47 +173,67 @@ module TK.SpaceTac {
}
/**
* Apply an action, returning true if it was successful
* Get the full list of diffs caused by applying this action
*/
apply(ship: Ship, target = this.getDefaultTarget(ship)): boolean {
getDiffs(ship: Ship, battle: Battle, target = this.getDefaultTarget(ship)): BaseBattleDiff[] {
let reject = this.checkCannotBeApplied(ship);
if (reject == null) {
let checked_target = this.checkTarget(ship, target);
if (!checked_target) {
console.warn("Action rejected - invalid target", ship, this, target);
return false;
}
let cost = this.getActionPointsUsage(ship, checked_target);
if (!ship.useActionPoints(cost)) {
console.warn("Action rejected - not enough power", ship, this, checked_target);
return false;
}
if (this.equipment) {
this.equipment.addWear(1);
ship.listEquipment(SlotType.Power).forEach(equipment => equipment.addWear(1));
}
this.cooldown.use();
let battle = ship.getBattle();
if (battle) {
battle.log.add(new ActionAppliedEvent(ship, this, checked_target, cost));
}
this.customApply(ship, checked_target);
return true;
} else {
if (reject) {
console.warn(`Action rejected - ${reject}`, ship, this, target);
return false;
return [];
}
let checked_target = this.checkTarget(ship, target);
if (!checked_target) {
console.warn("Action rejected - invalid target", ship, this, target);
return [];
}
let cost = this.getActionPointsUsage(ship, checked_target);
if (ship.getValue("power") < cost) {
console.warn("Action rejected - not enough power", ship, this, checked_target);
return [];
}
let result: BaseBattleDiff[] = [];
// Action usage
result.push(new ShipActionUsedDiff(ship, this, checked_target));
// Power usage
if (cost) {
result = result.concat(ship.getValueDiffs("power", -cost, true));
}
// Action effects
result = result.concat(this.getSpecificDiffs(ship, battle, checked_target));
return result;
}
/**
* Method to reimplement to apply the action
* Method to reimplement to return the diffs specific to this action
*/
protected customApply(ship: Ship, target: Target): void {
protected getSpecificDiffs(ship: Ship, battle: Battle, target: Target): BaseBattleDiff[] {
return []
}
/**
* Apply the action on a battle state
*/
apply(battle: Battle, ship: Ship, target = this.getDefaultTarget(ship)): boolean {
if (this.checkTarget(ship, target)) {
let diffs = this.getDiffs(ship, battle, target);
if (diffs.length) {
battle.applyDiffs(diffs);
return true;
} else {
console.error("Could not apply action, no diff produced");
return false;
}
} else {
console.error("Could not apply action, target rejected");
return false;
}
}
/**

View file

@ -1,4 +1,4 @@
module TK.SpaceTac {
module TK.SpaceTac.Specs {
testing("DeployDroneAction", test => {
test.case("stores useful information", check => {
let equipment = new Equipment(SlotType.Weapon, "testdrone");
@ -23,35 +23,37 @@ module TK.SpaceTac {
});
test.case("deploys a new drone", check => {
let battle = new Battle();
let ship = battle.fleets[0].addShip();
let battle = TestTools.createBattle();
let ship = battle.play_order[0];
ship.setArenaPosition(0, 0);
TestTools.setShipPlaying(battle, ship);
TestTools.setShipAP(ship, 3);
let equipment = new Equipment(SlotType.Weapon, "testdrone");
let action = new DeployDroneAction(equipment, 2, 8, 2, 4, [new DamageEffect(50)]);
equipment.action = action;
ship.addSlot(SlotType.Weapon).attach(equipment);
battle.log.clear();
battle.log.addFilter("value");
let result = action.apply(ship, new Target(5, 0, null));
TestTools.actionChain(check, battle, [
[ship, action, new Target(5, 0)],
], [
check => {
check.equals(ship.getValue("power"), 3, "power=3");
check.equals(battle.drones.count(), 0, "drones=0");
},
check => {
check.equals(ship.getValue("power"), 1, "power=1");
check.equals(battle.drones.count(), 1, "drones=1");
check.equals(result, true);
check.equals(battle.drones.length, 1);
let drone = battle.drones[0];
check.equals(drone.code, "testdrone");
check.equals(drone.duration, 2);
check.same(drone.owner, ship);
check.equals(drone.x, 5);
check.equals(drone.y, 0);
check.equals(drone.radius, 4);
check.equals(drone.effects, [new DamageEffect(50)]);
check.equals(battle.log.events, [
new ActionAppliedEvent(ship, action, Target.newFromLocation(5, 0), 2),
new DroneDeployedEvent(drone)
]);
check.equals(ship.values.power.get(), 1);
let drone = battle.drones.list()[0];
check.equals(drone.code, "testdrone");
check.equals(drone.duration, 2);
check.same(drone.owner, ship.id);
check.equals(drone.x, 5);
check.equals(drone.y, 0);
check.equals(drone.radius, 4);
compare_effects(check, drone.effects, [new DamageEffect(50)]);
}
])
});
});
}

View file

@ -55,17 +55,14 @@ module TK.SpaceTac {
return target;
}
protected customApply(ship: Ship, target: Target) {
protected getSpecificDiffs(ship: Ship, battle: Battle, target: Target): BaseBattleDiff[] {
let drone = new Drone(ship, this.equipment.code, this.lifetime);
drone.x = target.x;
drone.y = target.y;
drone.radius = this.effect_radius;
drone.effects = this.effects;
let battle = ship.getBattle();
if (battle) {
battle.addDrone(drone);
}
return [new DroneDeployedDiff(drone)];
}
getEffectsDescription(): string {

View file

@ -1,31 +1,64 @@
module TK.SpaceTac.Specs {
testing("EndTurnAction", test => {
test.case("can't be applied to non-playing ship", check => {
let mock_warn = check.patch(console, "warn", null);
let battle = new Battle();
battle.fleets[0].addShip();
battle.fleets[0].addShip();
battle.throwInitiative();
battle.setPlayingShip(battle.play_order[0]);
let battle = Battle.newQuickRandom();
let action = new EndTurnAction();
check.equals(action.checkCannotBeApplied(battle.play_order[0]), null);
check.equals(action.checkCannotBeApplied(battle.play_order[1]), "ship not playing");
let ship = battle.play_order[1];
let result = action.apply(battle.play_order[1]);
check.equals(result, false);
check.called(mock_warn, [
["Action rejected - ship not playing", ship, action, Target.newFromShip(ship)]
]);
});
test.case("ends turn when applied", check => {
let battle = Battle.newQuickRandom();
let action = new EndTurnAction();
let battle = TestTools.createBattle(2, 0);
check.equals(battle.play_index, 0);
TestTools.actionChain(check, battle, [
[battle.play_order[0], EndTurnAction.SINGLETON, undefined],
], [
check => {
check.equals(battle.play_index, 0, "play_index is 0");
check.same(battle.playing_ship, battle.play_order[0], "first ship is playing");
check.equals(battle.play_order[0].playing, true, "first ship is playing");
check.equals(battle.play_order[1].playing, false, "second ship is not playing");
},
check => {
check.equals(battle.play_index, 1, "play_index is 1");
check.same(battle.playing_ship, battle.play_order[1], "second ship is playing");
check.equals(battle.play_order[0].playing, false, "first ship is not playing");
check.equals(battle.play_order[1].playing, true, "second ship is playing");
}
]);
});
let result = action.apply(battle.play_order[0], Target.newFromShip(battle.play_order[0]));
check.equals(result, true);
check.equals(battle.play_index, 1);
test.case("generates power for previous ship", check => {
check.patch(console, "warn", null);
let battle = TestTools.createBattle(1, 0);
let ship = battle.play_order[0];
TestTools.setShipAP(ship, 10, 3);
ship.setValue("power", 6);
TestTools.actionChain(check, battle, [
[ship, EndTurnAction.SINGLETON, Target.newFromShip(ship)],
[ship, EndTurnAction.SINGLETON, Target.newFromShip(ship)],
[ship, EndTurnAction.SINGLETON, Target.newFromShip(ship)],
], [
check => {
check.equals(ship.getValue("power"), 6, "power=6");
},
check => {
check.equals(ship.getValue("power"), 9, "power=9");
},
check => {
check.equals(ship.getValue("power"), 10, "power=10");
},
check => {
check.equals(ship.getValue("power"), 10, "power=10");
}
]);
});
});
}

View file

@ -1,29 +1,50 @@
/// <reference path="BaseAction.ts" />
module TK.SpaceTac {
// Action to end the ship's turn
/**
* Action to end the ship's turn
*
* This action is not provided by an equipment and is always available
*/
export class EndTurnAction extends BaseAction {
// Singleton that may be used for all ships
static SINGLETON = new EndTurnAction();
constructor() {
super("endturn", "End ship's turn");
}
protected customApply(ship: Ship, target: Target) {
if (target.ship == ship) {
ship.endTurn();
protected getSpecificDiffs(ship: Ship, battle: Battle, target: Target): BaseBattleDiff[] {
if (ship.is(battle.playing_ship)) {
let result: BaseBattleDiff[] = [];
let new_ship = battle.getNextShip();
let battle = ship.getBattle();
if (battle) {
battle.advanceToNextShip();
}
// Generate power
result = result.concat(ship.getValueDiffs("power", ship.getAttribute("power_generation"), true));
// TODO previous: apply sticky effects
// TODO previous: cool down equipment
// TODO new: apply sticky effects
// TODO new: reset toggle actions
let cycle_diff = (battle.play_order.indexOf(new_ship) == 0) ? 1 : 0;
result.push(new ShipChangeDiff(ship, new_ship, cycle_diff));
return result;
} else {
return [];
}
}
protected checkShipTarget(ship: Ship, target: Target): Target | null {
return target.ship == ship ? target : null;
return ship.is(target.ship_id) ? target : null;
}
getTargettingMode(ship: Ship): ActionTargettingMode {
return ship.getValue("power") ? ActionTargettingMode.SELF_CONFIRM : ActionTargettingMode.SELF;
}
getEffectsDescription(): string {
return "End the current ship's turn.\nWill also generate power and cool down equipments.";
}

View file

@ -1,11 +1,10 @@
module TK.SpaceTac {
module TK.SpaceTac.Specs {
testing("MoveAction", test => {
test.case("checks movement against remaining AP", check => {
var ship = new Ship();
var battle = new Battle(ship.fleet);
TestTools.setShipPlaying(battle, ship);
ship.values.power.setMaximal(20);
ship.values.power.set(6);
ship.setValue("power", 6);
ship.arena_x = 0;
ship.arena_y = 0;
var engine = new Equipment();
@ -19,7 +18,7 @@ module TK.SpaceTac {
result = action.checkTarget(ship, Target.newFromLocation(0, 80));
check.nears(nn(result).y, 59.9);
ship.values.power.set(0);
ship.setValue("power", 0);
result = action.checkTarget(ship, Target.newFromLocation(0, 80));
check.equals(result, null);
});
@ -36,46 +35,31 @@ module TK.SpaceTac {
check.equals(result, null);
});
test.case("applies to ship location, battle log and AP", check => {
var ship = new Ship();
var battle = new Battle(ship.fleet);
ship.values.power.setMaximal(20);
ship.values.power.set(5);
ship.arena_x = 0;
ship.arena_y = 0;
var engine = new Equipment();
var action = new MoveAction(engine, 1);
TestTools.setShipPlaying(battle, ship);
test.case("applies and reverts", check => {
let battle = TestTools.createBattle();
let ship = battle.play_order[0];
TestTools.setShipAP(ship, 20);
ship.setValue("power", 5);
check.patch(console, "warn", null);
let engine = new Equipment(SlotType.Engine);
let action = new MoveAction(engine, 1);
engine.action = action;
ship.addSlot(SlotType.Engine).attach(engine);
var result = action.apply(ship, Target.newFromLocation(10, 10));
check.equals(result, true);
check.nears(ship.arena_x, 3.464823, 5);
check.nears(ship.arena_y, 3.464823, 5);
check.equals(ship.values.power.get(), 0);
result = action.apply(ship, Target.newFromLocation(10, 10));
check.equals(result, false);
check.nears(ship.arena_x, 3.464823, 5);
check.nears(ship.arena_y, 3.464823, 5);
check.equals(ship.values.power.get(), 0);
check.equals(battle.log.events.length, 3);
check.equals(battle.log.events[0].code, "value");
check.same(battle.log.events[0].ship, ship);
check.equals((<ValueChangeEvent>battle.log.events[0]).value,
new ShipValue("power", 0, 20));
check.equals(battle.log.events[1].code, "action");
check.same(battle.log.events[1].ship, ship);
check.equals(battle.log.events[2].code, "move");
check.same(battle.log.events[2].ship, ship);
let dest = (<MoveEvent>battle.log.events[2]).end;
check.nears(dest.x, 3.464823, 5);
check.nears(dest.y, 3.464823, 5);
TestTools.actionChain(check, battle, [
[ship, action, Target.newFromLocation(10, 5)],
], [
check => {
check.equals(ship.arena_x, 0, "ship X");
check.equals(ship.arena_y, 0, "ship Y");
check.equals(ship.getValue("power"), 5, "power");
},
check => {
check.nears(ship.arena_x, 4.382693, 5, "ship X");
check.nears(ship.arena_y, 2.191346, 5, "ship Y");
check.equals(ship.getValue("power"), 0, "power");
}
]);
});
test.case("can't move too much near another ship", check => {
@ -146,19 +130,19 @@ module TK.SpaceTac {
let ship = new Ship();
let action = new MoveAction(new Equipment(), 100, undefined, 60);
ship.setAttribute("maneuvrability", 0);
TestTools.setAttribute(ship, "maneuvrability", 0);
check.nears(action.getDistanceByActionPoint(ship), 40);
ship.setAttribute("maneuvrability", 1);
TestTools.setAttribute(ship, "maneuvrability", 1);
check.nears(action.getDistanceByActionPoint(ship), 60);
ship.setAttribute("maneuvrability", 2);
TestTools.setAttribute(ship, "maneuvrability", 2);
check.nears(action.getDistanceByActionPoint(ship), 70);
ship.setAttribute("maneuvrability", 10);
TestTools.setAttribute(ship, "maneuvrability", 10);
check.nears(action.getDistanceByActionPoint(ship), 90);
action = new MoveAction(new Equipment(), 100, undefined, 0);
ship.setAttribute("maneuvrability", 0);
TestTools.setAttribute(ship, "maneuvrability", 0);
check.nears(action.getDistanceByActionPoint(ship), 100);
ship.setAttribute("maneuvrability", 10);
TestTools.setAttribute(ship, "maneuvrability", 10);
check.nears(action.getDistanceByActionPoint(ship), 100);
});

View file

@ -1,5 +1,7 @@
module TK.SpaceTac {
// Action to move to a given location
/**
* Action to move the ship to a specific location
*/
export class MoveAction extends BaseAction {
// Distance allowed for each power point (raw, without applying maneuvrability)
distance_per_power: number
@ -37,7 +39,7 @@ module TK.SpaceTac {
// Check AP usage
if (remaining_ap === null) {
remaining_ap = ship.values.power.get();
remaining_ap = ship.getValue("power");
}
if (remaining_ap > 0.0001) {
return null;
@ -119,8 +121,10 @@ module TK.SpaceTac {
return target.getDistanceTo(ship.location) > 0 ? target : null;
}
protected customApply(ship: Ship, target: Target) {
ship.moveTo(target.x, target.y, this.equipment);
protected getSpecificDiffs(ship: Ship, battle: Battle, target: Target): BaseBattleDiff[] {
let angle = (arenaDistance(target, ship.location) < 0.00001) ? ship.arena_angle : arenaAngle(ship.location, target);
let destination = new ArenaLocationAngle(target.x, target.y, angle);
return [new ShipMoveDiff(ship, ship.location, destination, this.equipment)];
}
getEffectsDescription(): string {

View file

@ -1,4 +1,4 @@
module TK.SpaceTac {
module TK.SpaceTac.Specs {
testing("ToggleAction", test => {
test.case("returns correct targetting mode", check => {
let action = new ToggleAction(new Equipment(), 1, 0, []);

View file

@ -49,7 +49,7 @@ module TK.SpaceTac {
}
checkShipTarget(ship: Ship, target: Target): Target | null {
return (ship == target.ship) ? target : null;
return ship.is(target.ship_id) ? target : null;
}
/**
@ -64,10 +64,11 @@ module TK.SpaceTac {
return result;
}
protected customApply(ship: Ship, target: Target) {
this.activated = !this.activated;
ship.addBattleEvent(new ToggleEvent(ship, this, this.activated));
this.getImpactedShips(ship, target).forEach(iship => iship.setActiveEffectsChanged());
protected getSpecificDiffs(ship: Ship, battle: Battle, target: Target): BaseBattleDiff[] {
// TODO Add effects to ships in range
return [
new ShipActionToggleDiff(ship, this, !this.activated)
]
}
getEffectsDescription(): string {

View file

@ -1,4 +1,4 @@
module TK.SpaceTac {
module TK.SpaceTac.Specs {
testing("TriggerAction", test => {
test.case("constructs correctly", check => {
let equipment = new Equipment(SlotType.Weapon, "testweapon");
@ -14,8 +14,10 @@ module TK.SpaceTac {
let ship = new Ship(fleet, "ship");
let equipment = new Equipment(SlotType.Weapon, "testweapon");
let effect = new BaseEffect("testeffect");
let mock_apply = check.patch(effect, "applyOnShip", null);
let mock_apply = check.patch(effect, "getOnDiffs");
let action = new TriggerAction(equipment, [effect], 5, 100, 10);
equipment.action = action;
ship.addSlot(SlotType.Weapon).attach(equipment);
TestTools.setShipAP(ship, 10);
@ -32,7 +34,7 @@ module TK.SpaceTac {
TestTools.setShipPlaying(battle, ship);
fleet.setBattle(battle);
action.apply(ship, Target.newFromLocation(50, 50));
action.apply(battle, ship, Target.newFromLocation(50, 50));
check.called(mock_apply, [
[ship2, ship]
]);
@ -103,17 +105,18 @@ module TK.SpaceTac {
})
test.case("rotates toward the target", check => {
let ship = new Ship();
let battle = TestTools.createBattle();
let ship = battle.play_order[0];
let weapon = TestTools.addWeapon(ship, 1, 0, 100, 30);
let action = nn(weapon.action);
check.patch(action, "checkTarget", (ship: Ship, target: Target) => target);
check.equals(ship.arena_angle, 0);
let result = action.apply(ship, Target.newFromLocation(10, 20));
let result = action.apply(battle, ship, Target.newFromLocation(10, 20));
check.equals(result, true);
check.nears(ship.arena_angle, 1.107, 3);
result = action.apply(ship, Target.newFromShip(ship));
result = action.apply(battle, ship, Target.newFromShip(ship));
check.equals(result, true);
check.nears(ship.arena_angle, 1.107, 3);
})

View file

@ -3,6 +3,8 @@
module TK.SpaceTac {
/**
* Action to trigger an equipment (for example a weapon), with an optional target
*
* The target will be resolved as a list of ships, on which all the action effects will be applied
*/
export class TriggerAction extends BaseAction {
// Power consumption
@ -91,7 +93,7 @@ module TK.SpaceTac {
}
});
} else {
return ships.filter(ship => target.ship === ship);
return ships.filter(ship => ship.is(target.ship_id));
}
}
@ -105,7 +107,7 @@ module TK.SpaceTac {
}
checkShipTarget(ship: Ship, target: Target): Target | null {
if (this.range > 0 && ship == target.ship) {
if (this.range > 0 && ship.is(target.ship_id)) {
// No self fire
return null;
} else {
@ -132,18 +134,32 @@ module TK.SpaceTac {
return result;
}
protected customApply(ship: Ship, target: Target) {
if (arenaDistance(ship.location, target) > 0.000001) {
// Face the target
ship.rotate(arenaAngle(ship.location, target), first(ship.listEquipment(SlotType.Engine), () => true));
}
protected getSpecificDiffs(ship: Ship, battle: Battle, target: Target): BaseBattleDiff[] {
let result: BaseBattleDiff[] = [];
// Fire event
ship.addBattleEvent(new FireEvent(ship, this.equipment, target));
if (arenaDistance(ship.location, target) > 1e-6) {
// Face the target
let angle = arenaAngle(ship.location, target);
if (Math.abs(angularDistance(angle, ship.arena_angle)) > 1e-6) {
let destination = new ArenaLocationAngle(ship.arena_x, ship.arena_y, angle);
let engine = first(ship.listEquipment(SlotType.Engine), () => true);
result.push(new ShipMoveDiff(ship, ship.location, destination, engine));
}
// Fire a projectile
if (this.equipment && this.equipment.slot_type == SlotType.Weapon) {
result.push(new ProjectileFiredDiff(ship, this.equipment, target));
}
}
// Apply effects
let effects = this.getEffects(ship, target);
effects.forEach(([ship_target, effect]) => effect.applyOnShip(ship_target, ship));
effects.forEach(([ship_target, effect]) => {
let diffs = effect.getOnDiffs(ship_target, ship);
result = result.concat(diffs);
});
return result;
}
getEffectsDescription(): string {

View file

@ -92,7 +92,7 @@ module TK.SpaceTac {
}
// Update results, and go on to next battle
if (!battle.outcome.draw && battle.outcome.winner) {
if (battle.outcome && !battle.outcome.draw && battle.outcome.winner) {
this.update(battle.fleets.indexOf(battle.outcome.winner));
} else {
this.update(-1);

View file

@ -63,7 +63,7 @@ module TK.SpaceTac {
}
// End the ship turn
this.applyAction(new EndTurnAction(), Target.newFromShip(ship));
this.applyAction(EndTurnAction.SINGLETON, Target.newFromShip(ship));
}
/**
@ -72,7 +72,12 @@ module TK.SpaceTac {
* This should be the only real interaction point with battle state
*/
private applyAction(action: BaseAction, target: Target): boolean {
return action.apply(this.ship, target);
let battle = this.ship.getBattle();
if (battle) {
return battle.applyOneAction(action, target);
} else {
return false;
}
}
/**

View file

@ -1,5 +1,12 @@
module TK.SpaceTac.Specs {
testing("Maneuver", test => {
function compare_maneuver_effects(check: TestContext, meff1: [Ship, BaseEffect][], meff2: [Ship, BaseEffect][]): void {
let [ships1, effects1] = unzip(meff1);
let [ships2, effects2] = unzip(meff2);
check.equals(ships1, ships2, "impacted ships");
compare_effects(check, effects1, effects2);
}
test.case("guesses weapon effects", check => {
let battle = new Battle();
let ship1 = battle.fleets[0].addShip();
@ -13,7 +20,7 @@ module TK.SpaceTac.Specs {
ship3.setArenaPosition(0, 15);
TestTools.setShipHP(ship3, 30, 30);
let maneuver = new Maneuver(ship1, nn(weapon.action), Target.newFromLocation(0, 0));
check.equals(maneuver.effects, [
compare_maneuver_effects(check, maneuver.effects, [
[ship1, new DamageEffect(50)],
[ship2, new DamageEffect(50)]
]);
@ -33,7 +40,7 @@ module TK.SpaceTac.Specs {
ship3.setArenaPosition(0, 15);
TestTools.setShipHP(ship3, 30, 30);
let maneuver = new Maneuver(ship1, weapon.action, Target.newFromLocation(0, 0));
check.equals(maneuver.effects, [
compare_maneuver_effects(check, maneuver.effects, [
[ship1, new ValueEffect("shield", 10)],
[ship2, new ValueEffect("shield", 10)]
]);
@ -58,7 +65,9 @@ module TK.SpaceTac.Specs {
maneuver = new Maneuver(ship, move, Target.newFromLocation(100, 30));
check.containing(maneuver.getFinalLocation(), { x: 100, y: 30 });
check.equals(maneuver.effects, [[ship, new AttributeEffect("maneuvrability", 1)]]);
compare_maneuver_effects(check, maneuver.effects, [
[ship, new AttributeEffect("maneuvrability", 1)]
]);
});
});
}

View file

@ -89,7 +89,7 @@ module TK.SpaceTac {
// Area effects on final location
let location = this.getFinalLocation();
let effects = this.battle.drones.forEach(drone => {
let effects = this.battle.drones.list().forEach(drone => {
if (Target.newFromLocation(location.x, location.y).isInRange(drone.x, drone.y, drone.radius)) {
result = result.concat(drone.effects.map(effect => <[Ship, BaseEffect]>[this.ship, effect]));
}

View file

@ -0,0 +1,55 @@
/// <reference path="../../common/DiffLog.ts" />
module TK.SpaceTac {
/**
* Base class for battle diffs
*
* Events are the proper way to modify the battle state
*/
export class BaseBattleDiff extends Diff<Battle> {
}
/**
* Base class for battle diffs related to a ship
*/
export class BaseBattleShipDiff extends BaseBattleDiff {
ship_id: RObjectId
constructor(ship: Ship | RObjectId) {
super();
this.ship_id = (ship instanceof Ship) ? ship.id : ship;
}
apply(battle: Battle): void {
let ship = battle.getShip(this.ship_id);
if (ship) {
this.applyOnShip(ship, battle);
} else {
console.error("Diff apply failed - Ship not found", this);
}
}
/**
* Apply the diff on the ship
*/
protected applyOnShip(ship: Ship, battle: Battle): void {
}
revert(battle: Battle): void {
let ship = battle.getShip(this.ship_id);
if (ship) {
this.revertOnShip(ship, battle);
} else {
console.error("Diff revert failed - Ship not found", this);
}
}
/**
* Revert the diff on the ship
*/
protected revertOnShip(ship: Ship, battle: Battle): void {
this.getReverse().apply(battle);
}
}
}

View file

@ -0,0 +1,39 @@
/// <reference path="BaseBattleDiff.ts"/>
module TK.SpaceTac {
/**
* A drone applies its effect on ships around
*/
export class DroneAppliedDiff extends BaseBattleDiff {
// ID of the drone
drone: RObjectId
// IDs of impacted ships
ships: RObjectId[]
constructor(drone: Drone, ships: Ship[]) {
super();
this.drone = drone.id;
this.ships = ships.map(ship => ship.id);
}
apply(battle: Battle): void {
let drone = battle.drones.get(this.drone);
if (drone) {
drone.duration -= 1;
} else {
console.error("Cannot apply diff - Drone not found", this);
}
}
revert(battle: Battle): void {
let drone = battle.drones.get(this.drone);
if (drone) {
drone.duration += 1;
} else {
console.error("Cannot revert diff - Drone not found", this);
}
}
}
}

View file

@ -0,0 +1,48 @@
module TK.SpaceTac.Specs {
testing("DroneDeployedDiff", test => {
test.case("applies and reverts", check => {
let battle = TestTools.createBattle();
let drone1 = new Drone(battle.play_order[0]);
let drone2 = new Drone(battle.play_order[0], "test", 2);
TestTools.diffChain(check, battle, [
new DroneDeployedDiff(drone1, 3),
new DroneDeployedDiff(drone2),
new DroneAppliedDiff(drone1, []),
new DroneAppliedDiff(drone1, []),
new DroneDestroyedDiff(drone1, 1),
new DroneDestroyedDiff(drone2),
], [
check => {
check.equals(battle.drones.count(), 0, "drone count");
},
check => {
check.equals(battle.drones.count(), 1, "drone count");
check.equals(nn(battle.drones.get(drone1.id)).duration, 3, "drone1 duration");
},
check => {
check.equals(battle.drones.count(), 2, "drone count");
check.equals(nn(battle.drones.get(drone1.id)).duration, 3, "drone1 duration");
check.equals(nn(battle.drones.get(drone2.id)).duration, 2, "drone2 duration");
},
check => {
check.equals(battle.drones.count(), 2, "drone count");
check.equals(nn(battle.drones.get(drone1.id)).duration, 2, "drone1 duration");
check.equals(nn(battle.drones.get(drone2.id)).duration, 2, "drone2 duration");
},
check => {
check.equals(battle.drones.count(), 2, "drone count");
check.equals(nn(battle.drones.get(drone1.id)).duration, 1, "drone1 duration");
check.equals(nn(battle.drones.get(drone2.id)).duration, 2, "drone2 duration");
},
check => {
check.equals(battle.drones.count(), 1, "drone count");
check.equals(nn(battle.drones.get(drone2.id)).duration, 2, "drone2 duration");
},
check => {
check.equals(battle.drones.count(), 0, "drone count");
},
]);
});
});
}

View file

@ -0,0 +1,56 @@
/// <reference path="BaseBattleDiff.ts"/>
module TK.SpaceTac {
/**
* A drone is deployed by a ship
*/
export class DroneDeployedDiff extends BaseBattleShipDiff {
// Drone object
drone: Drone
// Initial duration (number of activations)
duration: number
constructor(drone: Drone, duration = drone.duration) {
super(drone.owner);
this.drone = drone;
this.duration = duration;
}
protected applyOnShip(ship: Ship, battle: Battle): void {
this.drone.duration = this.duration;
battle.addDrone(this.drone);
}
protected getReverse(): BaseBattleDiff {
return new DroneDestroyedDiff(this.drone, this.duration);
}
}
/**
* A drone is destroyed
*/
export class DroneDestroyedDiff extends BaseBattleShipDiff {
// Drone object
drone: Drone
// Remaining duration
duration: number
constructor(drone: Drone, duration = drone.duration) {
super(drone.owner);
this.drone = drone;
this.duration = duration;
}
protected applyOnShip(ship: Ship, battle: Battle): void {
battle.removeDrone(this.drone);
}
protected getReverse(): BaseBattleDiff {
return new DroneDeployedDiff(this.drone, this.duration);
}
}
}

View file

@ -0,0 +1,20 @@
module TK.SpaceTac.Specs {
testing("EndBattle", test => {
test.case("applies and reverts", check => {
let battle = new Battle();
TestTools.diffChain(check, battle, [
new EndBattleDiff(battle.fleets[1], 4)
], [
check => {
check.equals(battle.ended, false, "battle is ongoing");
check.equals(battle.outcome, null, "battle has no outcome");
},
check => {
check.equals(battle.ended, true, "battle is ended");
check.same(nn(battle.outcome).winner, battle.fleets[1], "battle has an outcome");
},
]);
});
});
}

View file

@ -0,0 +1,43 @@
/// <reference path="BaseBattleDiff.ts"/>
module TK.SpaceTac {
/**
* A battle ends
*
* This should be the last diff of a battle log
*/
export class EndBattleDiff extends BaseBattleDiff {
// Outcome of the battle
outcome: BattleOutcome
// Number of battle cycles
cycles: number
constructor(winner: Fleet | null, cycles: number) {
super();
this.outcome = new BattleOutcome(winner);
this.cycles = cycles;
}
apply(battle: Battle): void {
battle.outcome = this.outcome;
iforeach(battle.iships(), ship => {
ship.listEquipment().forEach(equipment => {
equipment.addWear(this.cycles);
});
});
}
revert(battle: Battle): void {
battle.outcome = null;
iforeach(battle.iships(), ship => {
ship.listEquipment().forEach(equipment => {
equipment.addWear(this.cycles);
});
});
}
}
}

View file

@ -0,0 +1,20 @@
/// <reference path="BaseBattleDiff.ts"/>
module TK.SpaceTac {
/**
* A projectile is fired
*
* This does not do anything, and is just there for animations
*/
export class ProjectileFiredDiff extends BaseBattleShipDiff {
equipment: RObjectId
target: Target
constructor(ship: Ship, equipment: Equipment, target: Target) {
super(ship);
this.equipment = equipment.id;
this.target = target;
}
}
}

View file

@ -0,0 +1,28 @@
module TK.SpaceTac.Specs {
testing("ShipActionToggleDiff", test => {
test.case("applies and reverts", check => {
let battle = new Battle();
let ship = battle.fleets[0].addShip();
let generator = TestTools.setShipAP(ship, 10);
let weapon = TestTools.addWeapon(ship, 50, 3, 10, 20);
let action = new ToggleAction(weapon, 2);
weapon.action = action;
TestTools.diffChain(check, battle, [
new ShipActionToggleDiff(ship, action, true),
new ShipActionToggleDiff(ship, action, false),
], [
check => {
check.equals(action.activated, false, "not activated");
},
check => {
check.equals(action.activated, true, "activated");
},
check => {
check.equals(action.activated, false, "not activated");
},
]);
});
});
}

View file

@ -0,0 +1,38 @@
/// <reference path="BaseBattleDiff.ts"/>
module TK.SpaceTac {
/**
* A ship activated or deactivated a toggle action
*/
export class ShipActionToggleDiff extends BaseBattleShipDiff {
// Pointer to the action
action: RObjectId
// true for activation, false for deactivation
activated: boolean
constructor(ship: Ship | RObjectId, action: BaseAction | RObjectId, activated: boolean) {
super(ship);
this.action = (action instanceof BaseAction) ? action.id : action;
this.activated = activated;
}
applyOnShip(ship: Ship, battle: Battle): void {
let action = ship.getAction(this.action);
if (action && action instanceof ToggleAction) {
if (action.activated == this.activated) {
console.warn("Diff not applied - action already in good state", this, action);
} else {
action.activated = this.activated;
}
} else {
console.error("Diff not applied - action not found on ship", this, ship);
}
}
getReverse(): BaseBattleDiff {
return new ShipActionToggleDiff(this.ship_id, this.action, !this.activated);
}
}
}

View file

@ -0,0 +1,33 @@
module TK.SpaceTac.Specs {
testing("ShipActionUsedDiff", test => {
test.case("applies and reverts", check => {
let battle = new Battle();
let ship = battle.fleets[0].addShip();
let generator = TestTools.setShipAP(ship, 10);
let weapon = TestTools.addWeapon(ship, 50, 3, 10, 20);
weapon.cooldown.configure(2, 1);
TestTools.diffChain(check, battle, [
new ShipActionUsedDiff(ship, nn(weapon.action), Target.newFromShip(ship)),
new ShipActionUsedDiff(ship, nn(weapon.action), Target.newFromShip(ship)),
], [
check => {
check.equals(weapon.cooldown.getRemainingUses(), 2, "cooldown");
check.equals(weapon.wear, 0, "weapon wear");
check.equals(generator.wear, 0, "generator wear");
},
check => {
check.equals(weapon.cooldown.getRemainingUses(), 1, "cooldown");
check.equals(weapon.wear, 1, "weapon wear");
check.equals(generator.wear, 1, "generator wear");
},
check => {
check.equals(weapon.cooldown.getRemainingUses(), 0, "cooldown");
check.equals(weapon.wear, 2, "weapon wear");
check.equals(generator.wear, 2, "generator wear");
},
]);
});
});
}

View file

@ -0,0 +1,62 @@
/// <reference path="BaseBattleDiff.ts"/>
module TK.SpaceTac {
/**
* A ship uses an action
*
* This will change:
* - The cooldown on the action and/or equipment
* - The wearing down of the equipment
*/
export class ShipActionUsedDiff extends BaseBattleShipDiff {
// Action applied
action: RObjectId
// Target for the action
target: Target
constructor(ship: Ship, action: BaseAction, target: Target) {
super(ship);
this.action = action.id;
this.target = target;
}
protected applyOnShip(ship: Ship, battle: Battle): void {
let action = first(ship.getAvailableActions(), action => action.is(this.action));
if (!action) {
console.error("Action failed - not found on ship", this, ship);
return;
}
if (action.cooldown.canUse()) {
action.cooldown.use(1);
} else {
console.error("Action apply failed - in cooldown", this, ship);
return;
}
if (action.equipment) {
action.equipment.addWear(1);
ship.listEquipment(SlotType.Power).forEach(equipment => equipment.addWear(1));
}
}
protected revertOnShip(ship: Ship, battle: Battle): void {
let action = first(ship.getAvailableActions(), action => action.is(this.action));
if (!action) {
console.error("Action revert failed - not found on ship", this, ship);
return;
}
action.cooldown.use(-1);
if (action.equipment) {
action.equipment.addWear(-1);
ship.listEquipment(SlotType.Power).forEach(equipment => equipment.addWear(-1));
}
}
}
}

View file

@ -0,0 +1,53 @@
/// <reference path="../../common/Testing.ts" />
module TK.SpaceTac.Specs {
testing("ShipAttributeDiff", test => {
test.case("applies and reverts", check => {
let battle = new Battle();
let ship = battle.fleets[0].addShip();
TestTools.diffChain(check, battle, [
new ShipAttributeDiff(ship, "precision", { cumulative: 5 }, {}),
new ShipAttributeDiff(ship, "maneuvrability", { cumulative: 8 }, {}),
new ShipAttributeDiff(ship, "precision", { cumulative: 2 }, {}),
new ShipAttributeDiff(ship, "precision", { cumulative: 4 }, { cumulative: 5 }),
new ShipAttributeDiff(ship, "maneuvrability", { multiplier: 50 }, {}),
new ShipAttributeDiff(ship, "maneuvrability", { limit: 2 }, {}),
new ShipAttributeDiff(ship, "maneuvrability", {}, { multiplier: 50, limit: 2 }),
], [
check => {
check.equals(ship.getAttribute("precision"), 0, "precision value");
check.equals(ship.getAttribute("maneuvrability"), 0, "maneuvrability value");
},
check => {
check.equals(ship.getAttribute("precision"), 5, "precision value");
check.equals(ship.getAttribute("maneuvrability"), 0, "maneuvrability value");
},
check => {
check.equals(ship.getAttribute("precision"), 5, "precision value");
check.equals(ship.getAttribute("maneuvrability"), 8, "maneuvrability value");
},
check => {
check.equals(ship.getAttribute("precision"), 7, "precision value");
check.equals(ship.getAttribute("maneuvrability"), 8, "maneuvrability value");
},
check => {
check.equals(ship.getAttribute("precision"), 6, "precision value");
check.equals(ship.getAttribute("maneuvrability"), 8, "maneuvrability value");
},
check => {
check.equals(ship.getAttribute("precision"), 6, "precision value");
check.equals(ship.getAttribute("maneuvrability"), 12, "maneuvrability value");
},
check => {
check.equals(ship.getAttribute("precision"), 6, "precision value");
check.equals(ship.getAttribute("maneuvrability"), 2, "maneuvrability value");
},
check => {
check.equals(ship.getAttribute("precision"), 6, "precision value");
check.equals(ship.getAttribute("maneuvrability"), 8, "maneuvrability value");
},
])
});
});
}

View file

@ -0,0 +1,41 @@
/// <reference path="BaseBattleDiff.ts"/>
module TK.SpaceTac {
type ShipAttributeModifier = {
cumulative?: number,
multiplier?: number,
limit?: number
}
/**
* A ship attribute modifier changed
*/
export class ShipAttributeDiff extends BaseBattleShipDiff {
// Attribute that changes
code: keyof ShipAttributes
// Modifiers added
added: ShipAttributeModifier
// Modifiers removed
removed: ShipAttributeModifier
constructor(ship: Ship | RObjectId, code: keyof ShipAttributes, added: ShipAttributeModifier, removed: ShipAttributeModifier) {
super(ship);
this.code = code;
this.added = added;
this.removed = removed;
}
getReverse(): BaseBattleDiff {
return new ShipAttributeDiff(this.ship_id, this.code, this.removed, this.added);
}
applyOnShip(ship: Ship, battle: Battle): void {
let attribute = ship.attributes[this.code];
attribute.addModifier(this.added.cumulative, this.added.multiplier, this.added.limit);
attribute.removeModifier(this.removed.cumulative, this.removed.multiplier, this.removed.limit);
}
}
}

View file

@ -0,0 +1,36 @@
module TK.SpaceTac.Specs {
testing("ShipChangeDiff", test => {
test.case("applies and reverts", check => {
let battle = new Battle();
let attacker1 = battle.fleets[0].addShip();
let attacker2 = battle.fleets[0].addShip();
let defender1 = battle.fleets[1].addShip();
battle.play_order = [defender1, attacker2, attacker1];
battle.play_index = 0;
battle.cycle = 1;
TestTools.diffChain(check, battle, [
new ShipChangeDiff(battle.play_order[0], battle.play_order[1]),
new ShipChangeDiff(battle.play_order[1], battle.play_order[2]),
new ShipChangeDiff(battle.play_order[2], battle.play_order[0], 1),
], [
check => {
check.same(battle.playing_ship, defender1, "first ship playing");
check.equals(battle.cycle, 1, "first cycle");
},
check => {
check.same(battle.playing_ship, attacker2, "second ship playing");
check.equals(battle.cycle, 1, "first cycle");
},
check => {
check.same(battle.playing_ship, attacker1, "third ship playing");
check.equals(battle.cycle, 1, "first cycle");
},
check => {
check.same(battle.playing_ship, defender1, "first ship playing again");
check.equals(battle.cycle, 2, "second cycle");
},
]);
});
});
}

View file

@ -0,0 +1,39 @@
/// <reference path="BaseBattleDiff.ts"/>
module TK.SpaceTac {
/**
* Current playing ship changes
*/
export class ShipChangeDiff extends BaseBattleShipDiff {
// ID of the new playing ship
new_ship: RObjectId
// Diff in the cycle count
cycle_diff: number
constructor(ship: Ship | RObjectId, new_ship: Ship | RObjectId, cycle_diff = 0) {
super(ship);
this.new_ship = (new_ship instanceof Ship) ? new_ship.id : new_ship;
this.cycle_diff = cycle_diff;
}
applyOnShip(ship: Ship, battle: Battle) {
if (ship.is(battle.playing_ship)) {
let new_ship = battle.getShip(this.new_ship);
if (new_ship) {
battle.setPlayingShip(new_ship);
battle.cycle += this.cycle_diff;
} else {
console.error("Cannot apply diff - new ship not found", this);
}
} else {
console.error("Cannot apply diff - ship is not playing", this);
}
}
getReverse(): BaseBattleDiff {
return new ShipChangeDiff(this.new_ship, this.ship_id, -this.cycle_diff);
}
}
}

View file

@ -0,0 +1,26 @@
module TK.SpaceTac.Specs {
testing("ShipCooldownDiff", test => {
test.case("applies and reverts", check => {
let battle = TestTools.createBattle();
let ship = battle.play_order[0];
let weapon = TestTools.addWeapon(ship);
weapon.cooldown.configure(1, 3);
weapon.cooldown.use();
TestTools.diffChain(check, battle, [
new ShipCooldownDiff(ship, weapon, 1),
new ShipCooldownDiff(ship, weapon, 2),
], [
check => {
check.equals(weapon.cooldown.heat, 3, "in cooldown for 3 turns");
},
check => {
check.equals(weapon.cooldown.heat, 2, "in cooldown for 2 turns");
},
check => {
check.equals(weapon.cooldown.heat, 0, "not in cooldown");
},
]);
});
});
}

View file

@ -0,0 +1,39 @@
/// <reference path="BaseBattleDiff.ts"/>
module TK.SpaceTac {
/**
* A ship's equipment cools down
*/
export class ShipCooldownDiff extends BaseBattleShipDiff {
// Equipment to cool
equipment: RObjectId
// Quantity of heat to dissipate
heat: number
constructor(ship: Ship | RObjectId, equipment: Equipment | RObjectId, heat: number) {
super(ship);
this.equipment = (equipment instanceof Equipment) ? equipment.id : equipment;
this.heat = heat;
}
applyOnShip(ship: Ship, battle: Battle) {
let equipment = ship.getEquipment(this.equipment);
if (equipment) {
equipment.cooldown.heat -= this.heat;
} else {
console.error("Cannot apply diff, equipment not found", this);
}
}
revertOnShip(ship: Ship, battle: Battle) {
let equipment = ship.getEquipment(this.equipment);
if (equipment) {
equipment.cooldown.heat += this.heat;
} else {
console.error("Cannot revert diff, equipment not found", this);
}
}
}
}

View file

@ -0,0 +1,40 @@
module TK.SpaceTac.Specs {
testing("ShipDamageDiff", test => {
test.case("applies and reverts", check => {
let battle = TestTools.createBattle();
let ship = battle.play_order[0];
let [hull, shield] = TestTools.setShipHP(ship, 80, 100);
TestTools.diffChain(check, battle, [
new ShipDamageDiff(ship, 0, 10),
new ShipDamageDiff(ship, 19, 0),
new ShipDamageDiff(ship, 30, 90),
], [
check => {
check.equals(hull.wear, 0, "hull wear");
check.equals(shield.wear, 0, "shield wear");
check.equals(ship.getValue("hull"), 80, "hull value");
check.equals(ship.getValue("shield"), 100, "shield value");
},
check => {
check.equals(hull.wear, 0, "hull wear");
check.equals(shield.wear, 1, "shield wear");
check.equals(ship.getValue("hull"), 80, "hull value");
check.equals(ship.getValue("shield"), 100, "shield value");
},
check => {
check.equals(hull.wear, 2, "hull wear");
check.equals(shield.wear, 1, "shield wear");
check.equals(ship.getValue("hull"), 80, "hull value");
check.equals(ship.getValue("shield"), 100, "shield value");
},
check => {
check.equals(hull.wear, 5, "hull wear");
check.equals(shield.wear, 10, "shield wear");
check.equals(ship.getValue("hull"), 80, "hull value");
check.equals(ship.getValue("shield"), 100, "shield value");
},
]);
});
});
}

View file

@ -0,0 +1,41 @@
/// <reference path="BaseBattleDiff.ts"/>
module TK.SpaceTac {
/**
* A ship takes damage (to hull or shield)
*
* This does not apply the damage on ship values (there are ShipValueDiff for this), but apply equipment wear.
*/
export class ShipDamageDiff extends BaseBattleShipDiff {
// Damage to hull
hull: number
// Damage to shield
shield: number
constructor(ship: Ship, hull: number, shield: number) {
super(ship);
this.hull = hull;
this.shield = shield;
}
protected applyOnShip(ship: Ship, battle: Battle): void {
if (this.shield > 0) {
ship.listEquipment(SlotType.Shield).forEach(equipment => equipment.addWear(Math.ceil(this.shield * 0.1)));
}
if (this.hull > 0) {
ship.listEquipment(SlotType.Hull).forEach(equipment => equipment.addWear(Math.ceil(this.hull * 0.1)));
}
}
protected revertOnShip(ship: Ship, battle: Battle): void {
if (this.shield > 0) {
ship.listEquipment(SlotType.Shield).forEach(equipment => equipment.addWear(-Math.ceil(this.shield * 0.1)));
}
if (this.hull > 0) {
ship.listEquipment(SlotType.Hull).forEach(equipment => equipment.addWear(-Math.ceil(this.hull * 0.1)));
}
}
}
}

View file

@ -0,0 +1,34 @@
/// <reference path="../../common/Testing.ts" />
module TK.SpaceTac.Specs {
testing("ShipDeathDiff", test => {
test.case("applies and reverts", check => {
let battle = new Battle();
let ship1 = battle.fleets[0].addShip();
let ship2 = battle.fleets[0].addShip();
let ship3 = battle.fleets[1].addShip();
battle.play_order = [ship3, ship2, ship1];
TestTools.diffChain(check, battle, [
new ShipDeathDiff(battle, ship2)
], [
check => {
check.equals(ship2.alive, true, "alive");
check.equals(imaterialize(battle.iships(false)), [ship1, ship2, ship3], "in all ships");
check.equals(imaterialize(battle.iships(true)), [ship1, ship2, ship3], "in alive ships");
check.equals(battle.fleets[0].ships, [ship1, ship2], "fleet1");
check.equals(battle.fleets[1].ships, [ship3], "fleet2");
check.equals(battle.play_order, [ship3, ship2, ship1], "in play order");
},
check => {
check.equals(ship2.alive, false, "dead");
check.equals(imaterialize(battle.iships(false)), [ship1, ship2, ship3], "in all ships");
check.equals(imaterialize(battle.iships(true)), [ship1, ship3], "not in alive ships anymore");
check.equals(battle.fleets[0].ships, [ship1, ship2], "fleet1");
check.equals(battle.fleets[1].ships, [ship3], "fleet2");
check.equals(battle.play_order, [ship3, ship1], "removed from play order");
},
]);
});
});
}

View file

@ -0,0 +1,34 @@
/// <reference path="BaseBattleDiff.ts"/>
module TK.SpaceTac {
/**
* A ship dies (or rather is put in emergency stasis mode)
*
* This typically happens when the ship's hull reaches 0.
* A dead ship cannot be interacted with, and will be removed from play order.
*/
export class ShipDeathDiff extends BaseBattleShipDiff {
// Index in the play order at which the ship was
play_index: number
constructor(battle: Battle, ship: Ship) {
super(ship);
this.play_index = battle.play_order.indexOf(ship);
}
protected applyOnShip(ship: Ship, battle: Battle): void {
ship.alive = false;
if (this.play_index >= 0) {
battle.removeFromPlayOrder(this.play_index);
}
}
protected revertOnShip(ship: Ship, battle: Battle): void {
ship.alive = true;
if (this.play_index >= 0) {
battle.insertInPlayOrder(this.play_index, ship);
}
}
}
}

View file

@ -0,0 +1,37 @@
module TK.SpaceTac.Specs {
testing("ShipEffectAddedDiff", test => {
test.case("applies and reverts", check => {
let battle = TestTools.createBattle();
let ship = battle.play_order[0];
let effect1 = new BaseEffect("e1");
let effect2 = new BaseEffect("e2");
TestTools.diffChain(check, battle, [
new ShipEffectAddedDiff(ship, effect1),
new ShipEffectAddedDiff(ship, effect2),
new ShipEffectRemovedDiff(ship, effect1),
new ShipEffectRemovedDiff(ship, effect2),
], [
check => {
check.equals(ship.active_effects.count(), 0, "effect count");
},
check => {
check.equals(ship.active_effects.count(), 1, "effect count");
check.equals(ship.active_effects.get(effect1.id), effect1, "effect1 present");
},
check => {
check.equals(ship.active_effects.count(), 2, "effect count");
check.equals(ship.active_effects.get(effect2.id), effect2, "effect2 present");
},
check => {
check.equals(ship.active_effects.count(), 1, "effect count");
check.equals(ship.active_effects.get(effect1.id), null, "effect1 missing");
},
check => {
check.equals(ship.active_effects.count(), 0, "effect count");
},
]);
});
});
}

View file

@ -0,0 +1,47 @@
/// <reference path="BaseBattleDiff.ts"/>
module TK.SpaceTac {
/**
* An effect is attached to a ship
*/
export class ShipEffectAddedDiff extends BaseBattleShipDiff {
// Effect added
effect: BaseEffect
constructor(ship: Ship | RObjectId, effect: BaseEffect) {
super(ship);
this.effect = effect;
}
protected applyOnShip(ship: Ship, battle: Battle): void {
ship.active_effects.add(this.effect);
}
protected getReverse(): BaseBattleDiff {
return new ShipEffectRemovedDiff(this.ship_id, this.effect);
}
}
/**
* An attached effect is removed from a ship
*/
export class ShipEffectRemovedDiff extends BaseBattleShipDiff {
// Effect removed
effect: BaseEffect
constructor(ship: Ship | RObjectId, effect: BaseEffect) {
super(ship);
this.effect = effect;
}
protected applyOnShip(ship: Ship, battle: Battle): void {
ship.active_effects.remove(this.effect);
}
protected getReverse(): BaseBattleDiff {
return new ShipEffectAddedDiff(this.ship_id, this.effect);
}
}
}

View file

View file

@ -0,0 +1,17 @@
module TK.SpaceTac.Specs {
testing("ShipMoveDiff", test => {
test.case("applies and reverts", check => {
let battle = new Battle();
let ship = battle.fleets[0].addShip();
check.equals(ship.location, new ArenaLocationAngle(0, 0, 0));
let engine = new Equipment();
let event = new ShipMoveDiff(ship, ship.location, new ArenaLocationAngle(50, 20, 1.2), engine);
event.apply(battle);
check.equals(ship.location, new ArenaLocationAngle(50, 20, 1.2));
event.revert(battle);
check.equals(ship.location, new ArenaLocationAngle(0, 0, 0));
});
});
}

View file

@ -0,0 +1,41 @@
/// <reference path="BaseBattleDiff.ts"/>
module TK.SpaceTac {
/**
* A ship moves in the arena
*/
export class ShipMoveDiff extends BaseBattleShipDiff {
// Previous location
start: ArenaLocationAngle
// New location
end: ArenaLocationAngle
// Engine used
engine: Equipment | null
constructor(ship: Ship | RObjectId, start: ArenaLocationAngle, end: ArenaLocationAngle, engine: Equipment | null = null) {
super(ship);
this.start = start;
this.end = end;
this.engine = engine;
}
/**
* Get the distance travelled
*/
getDistance(): number {
return arenaDistance(this.start, this.end);
}
applyOnShip(ship: Ship, battle: Battle) {
ship.setArenaPosition(this.end.x, this.end.y);
ship.setArenaFacingAngle(this.end.angle);
}
getReverse(): ShipMoveDiff {
return new ShipMoveDiff(this.ship_id, this.end, this.start, this.engine);
}
}
}

View file

@ -0,0 +1,25 @@
/// <reference path="../../common/Testing.ts" />
module TK.SpaceTac.Specs {
testing("ShipValueDiff", test => {
test.case("applies and reverts", check => {
let battle = new Battle();
let ship = battle.fleets[0].addShip();
TestTools.diffChain(check, battle, [
new ShipValueDiff(ship, "hull", 15),
new ShipValueDiff(ship, "hull", -7)
], [
check => {
check.equals(ship.getValue("hull"), 0, "hull value");
},
check => {
check.equals(ship.getValue("hull"), 15, "hull value");
},
check => {
check.equals(ship.getValue("hull"), 8, "hull value");
},
])
});
});
}

View file

@ -0,0 +1,29 @@
/// <reference path="BaseBattleDiff.ts"/>
module TK.SpaceTac {
/**
* A ship value changed
*/
export class ShipValueDiff extends BaseBattleShipDiff {
// Value that changes
code: keyof ShipValues
// Value variation
diff: number
constructor(ship: Ship | RObjectId, code: keyof ShipValues, diff: number) {
super(ship);
this.code = code;
this.diff = diff;
}
getReverse(): BaseBattleDiff {
return new ShipValueDiff(this.ship_id, this.code, -this.diff);
}
applyOnShip(ship: Ship, battle: Battle): void {
ship.values[this.code] += this.diff;
}
}
}

View file

@ -1,16 +1,23 @@
module TK.SpaceTac {
testing("AttributeEffect", test => {
test.case("is not applied directly", check => {
let ship = new Ship();
check.equals(ship.getAttribute("maneuvrability"), 0);
test.case("applies cumulatively on attribute", check => {
let battle = new Battle();
let ship = battle.fleets[0].addShip();
check.equals(ship.getAttribute("maneuvrability"), 0, "initial");
let effect = new AttributeEffect("maneuvrability", 20);
effect.applyOnShip(ship, ship);
check.equals(ship.getAttribute("maneuvrability"), 0);
let effect1 = new AttributeEffect("maneuvrability", 20);
battle.applyDiffs(effect1.getOnDiffs(ship, ship));
check.equals(ship.getAttribute("maneuvrability"), 20, "applied 1");
ship.sticky_effects.push(new StickyEffect(effect, 2));
ship.updateAttributes();
check.equals(ship.getAttribute("maneuvrability"), 20);
let effect2 = new AttributeEffect("maneuvrability", 10);
battle.applyDiffs(effect2.getOnDiffs(ship, ship));
check.equals(ship.getAttribute("maneuvrability"), 30, "applied 2");
battle.applyDiffs(effect1.getOffDiffs(ship, ship));
check.equals(ship.getAttribute("maneuvrability"), 10, "reverted 1");
battle.applyDiffs(effect2.getOffDiffs(ship, ship));
check.equals(ship.getAttribute("maneuvrability"), 0, "reverted 2");
});
test.case("has a description", check => {

View file

@ -8,10 +8,10 @@ module TK.SpaceTac {
*/
export class AttributeEffect extends BaseEffect {
// Affected attribute
attrcode: keyof ShipAttributes;
attrcode: keyof ShipAttributes
// Base value
value: number;
value: number
constructor(attrcode: keyof ShipAttributes, value = 0) {
super("attr");
@ -20,9 +20,16 @@ module TK.SpaceTac {
this.value = value;
}
applyOnShip(ship: Ship, source: Ship | Drone): boolean {
ship.updateAttributes();
return true;
getOnDiffs(ship: Ship, source: Ship | Drone): BaseBattleDiff[] {
return [
new ShipAttributeDiff(ship, this.attrcode, { cumulative: this.value }, {}),
];
}
getOffDiffs(ship: Ship, source: Ship | Drone): BaseBattleDiff[] {
return [
new ShipAttributeDiff(ship, this.attrcode, {}, { cumulative: this.value }),
];
}
isBeneficial(): boolean {
@ -34,7 +41,7 @@ module TK.SpaceTac {
}
getDescription(): string {
let attrname = SHIP_ATTRIBUTES[this.attrcode].name;
let attrname = SHIP_VALUES_NAMES[this.attrcode];
return `${attrname} ${this.value > 0 ? "+" : "-"}${Math.abs(this.value)}`;
}
}

View file

@ -1,32 +1,29 @@
module TK.SpaceTac {
testing("AttributeLimitEffect", test => {
test.case("limits an attribute", check => {
test.case("applies cumulatively on attribute", check => {
let battle = new Battle();
let ship = battle.fleets[0].addShip();
check.equals(ship.getAttribute("shield_capacity"), 0);
check.equals(ship.getValue("shield"), 0);
ship.attributes.precision.addModifier(12);
check.equals(ship.getAttribute("precision"), 12, "initial");
TestTools.setShipHP(ship, 100, 50);
ship.setValue("shield", 40);
check.equals(ship.getAttribute("shield_capacity"), 50);
check.equals(ship.getValue("shield"), 40);
let effect1 = new AttributeLimitEffect("precision", 5);
battle.applyDiffs(effect1.getOnDiffs(ship, ship));
check.equals(ship.getAttribute("precision"), 5, "applied 1");
battle.log.clear();
let effect = new StickyEffect(new AttributeLimitEffect("shield_capacity", 30));
ship.addStickyEffect(effect);
let effect2 = new AttributeLimitEffect("precision", 3);
battle.applyDiffs(effect2.getOnDiffs(ship, ship));
check.equals(ship.getAttribute("precision"), 3, "applied 2");
check.equals(ship.getAttribute("shield_capacity"), 30);
check.equals(ship.getValue("shield"), 30);
check.equals(battle.log.events, [
new ActiveEffectsEvent(ship, [new AttributeEffect("hull_capacity", 100), new AttributeEffect("shield_capacity", 50)], [effect]),
new ValueChangeEvent(ship, new ShipValue("shield", 30, 50), -10),
new ValueChangeEvent(ship, new ShipAttribute("shield capacity", 30), -20),
]);
battle.applyDiffs(effect1.getOffDiffs(ship, ship));
check.equals(ship.getAttribute("precision"), 3, "reverted 1");
ship.cleanStickyEffects();
battle.applyDiffs(effect2.getOffDiffs(ship, ship));
check.equals(ship.getAttribute("precision"), 12, "reverted 2");
});
check.equals(ship.getAttribute("shield_capacity"), 50);
check.equals(ship.getValue("shield"), 30);
test.case("has a description", check => {
let effect = new AttributeLimitEffect("power_capacity", 4);
check.equals(effect.getDescription(), "limit power capacity to 4");
});
});
}

View file

@ -20,9 +20,20 @@ module TK.SpaceTac {
this.value = value;
}
applyOnShip(ship: Ship, source: Ship | Drone): boolean {
ship.updateAttributes();
return true;
getOnDiffs(ship: Ship, source: Ship | Drone): BaseBattleDiff[] {
return [
new ShipAttributeDiff(ship, this.attrcode, { limit: this.value }, {}),
];
}
getOffDiffs(ship: Ship, source: Ship | Drone): BaseBattleDiff[] {
return [
new ShipAttributeDiff(ship, this.attrcode, {}, { limit: this.value }),
];
}
isBeneficial(): boolean {
return false;
}
getFullCode(): string {
@ -30,7 +41,7 @@ module TK.SpaceTac {
}
getDescription(): string {
let attrname = SHIP_ATTRIBUTES[this.attrcode].name;
let attrname = SHIP_VALUES_NAMES[this.attrcode];
return `limit ${attrname} to ${this.value}`;
}
}

View file

@ -0,0 +1,29 @@
module TK.SpaceTac {
testing("AttributeMultiplyEffect", test => {
test.case("boosts or reduces cumulatively an attribute", check => {
let battle = new Battle();
let ship = battle.fleets[0].addShip();
ship.attributes.hull_capacity.addModifier(100);
check.equals(ship.getAttribute("hull_capacity"), 100, "initial");
let effect1 = new AttributeMultiplyEffect("hull_capacity", 30);
battle.applyDiffs(effect1.getOnDiffs(ship, ship));
check.equals(ship.getAttribute("hull_capacity"), 130, "applied 1");
let effect2 = new AttributeMultiplyEffect("hull_capacity", -10);
battle.applyDiffs(effect2.getOnDiffs(ship, ship));
check.equals(ship.getAttribute("hull_capacity"), 120, "applied 2");
battle.applyDiffs(effect1.getOffDiffs(ship, ship));
check.equals(ship.getAttribute("hull_capacity"), 90, "reverted 1");
battle.applyDiffs(effect2.getOffDiffs(ship, ship));
check.equals(ship.getAttribute("hull_capacity"), 100, "reverted 2");
});
test.case("has a description", check => {
let effect = new AttributeMultiplyEffect("power_generation", 20);
check.equals(effect.getDescription(), "power generation +20%");
});
});
}

View file

@ -0,0 +1,49 @@
/// <reference path="BaseEffect.ts"/>
module TK.SpaceTac {
/**
* Boost or reduce an attribute value
*
* This effect is stored as "20" for "+20%", or "-10" for "-10%".
* Several multiply effects are cumulative (+20 and +10 will apply a +30 boost).
*/
export class AttributeMultiplyEffect extends BaseEffect {
// Affected attribute
attrcode: keyof ShipAttributes;
// Boost factor (percentage)
value: number;
constructor(attrcode: keyof ShipAttributes, value = 0) {
super("attrmult");
this.attrcode = attrcode;
this.value = value;
}
getOnDiffs(ship: Ship, source: Ship | Drone): BaseBattleDiff[] {
return [
new ShipAttributeDiff(ship, this.attrcode, { multiplier: this.value }, {}),
];
}
getOffDiffs(ship: Ship, source: Ship | Drone): BaseBattleDiff[] {
return [
new ShipAttributeDiff(ship, this.attrcode, {}, { multiplier: this.value }),
];
}
isBeneficial(): boolean {
return false;
}
getFullCode(): string {
return this.code + "-" + this.attrcode;
}
getDescription(): string {
let attrname = SHIP_VALUES_NAMES[this.attrcode];
return `${attrname} ${this.value > 0 ? "+" : "-"}${Math.abs(this.value)}%`;
}
}
}

View file

@ -1,24 +1,35 @@
/// <reference path="../diffs/BaseBattleDiff.ts" />
module TK.SpaceTac {
export type EffectAmount = number | { base: number, span: number };
/**
* Base class for effects of actions that can be applied on ships
*
* Effects are typically one shot, but sticky effects can be used to apply effects over a period
* Effects will generate diffs to modify the battle state
*/
export class BaseEffect {
export class BaseEffect extends RObject {
// Identifier code for the type of effect
code: string;
code: string
// Base constructor
constructor(code: string) {
super();
this.code = code;
}
// Apply ponctually the effect on a given ship
// Return true if the effect could be applied
applyOnShip(ship: Ship, source: Ship | Drone): boolean {
return false;
/**
* Get the list of diffs needed to activate this effect on a ship
*/
getOnDiffs(ship: Ship, source: Ship | Drone): BaseBattleDiff[] {
return [];
}
/**
* Get the list of diffs needed to remove this effect on a ship
*/
getOffDiffs(ship: Ship, source: Ship | Drone): BaseBattleDiff[] {
return [];
}
// Return true if the effect is beneficial to the ship, false if it's a drawback

View file

@ -1,27 +1,31 @@
module TK.SpaceTac {
testing("CooldownEffect", test => {
test.case("cools down equipment", check => {
let ship = new Ship();
let battle = new Battle();
let ship = battle.fleets[0].addShip();
let weapons = [TestTools.addWeapon(ship), TestTools.addWeapon(ship), TestTools.addWeapon(ship)];
weapons.forEach(weapon => weapon.cooldown.configure(1, 3));
check.equals(weapons.map(weapon => weapon.cooldown.heat), [0, 0, 0]);
new CooldownEffect(0, 0).applyOnShip(ship, ship);
let effect = new CooldownEffect(0, 0);
battle.applyDiffs(effect.getOnDiffs(ship, ship));
check.equals(weapons.map(weapon => weapon.cooldown.heat), [0, 0, 0]);
weapons.forEach(weapon => weapon.cooldown.use());
check.equals(weapons.map(weapon => weapon.cooldown.heat), [3, 3, 3]);
new CooldownEffect(0, 0).applyOnShip(ship, ship);
battle.applyDiffs(effect.getOnDiffs(ship, ship));
check.equals(weapons.map(weapon => weapon.cooldown.heat), [0, 0, 0]);
weapons.forEach(weapon => weapon.cooldown.use());
check.equals(weapons.map(weapon => weapon.cooldown.heat), [3, 3, 3]);
new CooldownEffect(1, 0).applyOnShip(ship, ship);
effect = new CooldownEffect(1, 0);
battle.applyDiffs(effect.getOnDiffs(ship, ship));
check.equals(weapons.map(weapon => weapon.cooldown.heat), [2, 2, 2]);
new CooldownEffect(1, 2).applyOnShip(ship, ship);
effect = new CooldownEffect(1, 2);
battle.applyDiffs(effect.getOnDiffs(ship, ship));
check.equals(weapons.map(weapon => weapon.cooldown.heat).sort(), [1, 1, 2]);
})

View file

@ -18,7 +18,7 @@ module TK.SpaceTac {
this.maxcount = maxcount;
}
applyOnShip(ship: Ship, source: Ship | Drone): boolean {
getOnDiffs(ship: Ship, source: Ship | Drone): BaseBattleDiff[] {
let equipments = ship.listEquipment().filter(equ => equ.cooldown.heat > 0);
if (this.maxcount && equipments.length > this.maxcount) {
@ -26,9 +26,7 @@ module TK.SpaceTac {
equipments = random.sample(equipments, this.maxcount);
}
equipments.forEach(equ => equ.cooldown.cool(this.cooling || equ.cooldown.heat));
return true;
return equipments.map(equ => new ShipCooldownDiff(ship, equ, this.cooling || equ.cooldown.heat));
}
isBeneficial(): boolean {

View file

@ -9,36 +9,28 @@ module TK.SpaceTac.Specs {
let shield = ship.listEquipment(SlotType.Shield)[0];
ship.restoreHealth();
check.equals(ship.getValue("hull"), 150);
check.equals(ship.getValue("shield"), 400);
check.equals(hull.wear, 0);
check.equals(shield.wear, 0);
function checkValues(desc: string, hull_value: number, shield_value: number, hull_wear: number, shield_wear: number) {
check.in(desc, check => {
check.equals(ship.getValue("hull"), hull_value, "hull value");
check.equals(ship.getValue("shield"), shield_value, "shield value");
check.equals(hull.wear, hull_wear, "hull wear");
check.equals(shield.wear, shield_wear, "shield wear");
});
}
new DamageEffect(50).applyOnShip(ship, ship);
check.equals(ship.getValue("hull"), 150);
check.equals(ship.getValue("shield"), 350);
check.equals(hull.wear, 0);
check.equals(shield.wear, 1);
checkValues("initial", 150, 400, 0, 0);
new DamageEffect(250).applyOnShip(ship, ship);
check.equals(ship.getValue("hull"), 150);
check.equals(ship.getValue("shield"), 100);
check.equals(hull.wear, 0);
check.equals(shield.wear, 4);
battle.applyDiffs(new DamageEffect(50).getOnDiffs(ship, ship));
checkValues("after 50 damage", 150, 350, 0, 5);
new DamageEffect(201).applyOnShip(ship, ship);
check.equals(ship.getValue("hull"), 49);
check.equals(ship.getValue("shield"), 0);
check.equals(hull.wear, 2);
check.equals(shield.wear, 5);
check.equals(ship.alive, true);
battle.applyDiffs(new DamageEffect(250).getOnDiffs(ship, ship));
checkValues("after 250 damage", 150, 100, 0, 30);
new DamageEffect(8000).applyOnShip(ship, ship);
check.equals(ship.getValue("hull"), 0);
check.equals(ship.getValue("shield"), 0);
check.equals(hull.wear, 3);
check.equals(shield.wear, 5);
check.equals(ship.alive, false);
battle.applyDiffs(new DamageEffect(201).getOnDiffs(ship, ship));
checkValues("after 201 damage", 49, 0, 11, 40);
battle.applyDiffs(new DamageEffect(8000).getOnDiffs(ship, ship));
checkValues("after 8000 damage", 0, 0, 16, 40);
});
test.case("gets a textual description", check => {

View file

@ -45,16 +45,16 @@ module TK.SpaceTac {
damage = Math.round(damage * this.getFactor(ship));
// Apply on shields
if (damage >= ship.values.shield.get()) {
shield = ship.values.shield.get();
if (damage >= ship.getValue("shield")) {
shield = ship.getValue("shield");
} else {
shield = damage;
}
damage -= shield;
// Apply on hull
if (damage >= ship.values.hull.get()) {
hull = ship.values.hull.get();
if (damage >= ship.getValue("hull")) {
hull = ship.getValue("hull");
} else {
hull = damage;
}
@ -62,19 +62,24 @@ module TK.SpaceTac {
return [shield, hull];
}
applyOnShip(ship: Ship, source: Ship | Drone): boolean {
getOnDiffs(ship: Ship, source: Ship | Drone): BaseBattleDiff[] {
let [shield, hull] = this.getEffectiveDamage(ship);
ship.addDamage(hull, shield);
let result: BaseBattleDiff[] = [];
if (shield > 0) {
ship.listEquipment(SlotType.Shield).forEach(equipment => equipment.addWear(Math.ceil(shield * 0.01)));
}
if (hull > 0) {
ship.listEquipment(SlotType.Hull).forEach(equipment => equipment.addWear(Math.ceil(hull * 0.01)));
if (shield || hull) {
result.push(new ShipDamageDiff(ship, hull, shield));
}
return true;
if (shield) {
result.push(new ShipValueDiff(ship, "shield", -shield));
}
if (hull) {
result.push(new ShipValueDiff(ship, "hull", -hull));
}
return result;
}
getDescription(): string {

View file

@ -3,7 +3,7 @@ module TK.SpaceTac.Specs {
test.case("shows a textual description", check => {
check.equals(new RepelEffect(34).getDescription(), "repel ships 34km away");
})
test.case("repel other ships from a central point", check => {
let battle = new Battle();
let ship1a = battle.fleets[0].addShip();
@ -14,9 +14,9 @@ module TK.SpaceTac.Specs {
ship2a.setArenaPosition(100, 280);
let effect = new RepelEffect(12);
effect.applyOnShip(ship1a, ship1a);
effect.applyOnShip(ship1b, ship1a);
effect.applyOnShip(ship2a, ship1a);
battle.applyDiffs(effect.getOnDiffs(ship1a, ship1a));
battle.applyDiffs(effect.getOnDiffs(ship1b, ship1a));
battle.applyDiffs(effect.getOnDiffs(ship2a, ship1a));
check.equals(ship1a.location, new ArenaLocationAngle(100, 100));
check.equals(ship1b.location, new ArenaLocationAngle(262, 100));
@ -33,7 +33,7 @@ module TK.SpaceTac.Specs {
ship2b.setArenaPosition(100, 350);
let effect = new RepelEffect(85);
effect.applyOnShip(ship2a, ship1a);
battle.applyDiffs(effect.getOnDiffs(ship2a, ship1a));
check.equals(ship2a.location, new ArenaLocationAngle(100, 250));
})
})

View file

@ -13,15 +13,19 @@ module TK.SpaceTac {
this.value = value;
}
applyOnShip(ship: Ship, source: Ship | Drone): boolean {
getOnDiffs(ship: Ship, source: Ship | Drone): BaseBattleDiff[] {
if (ship != source) {
let angle = arenaAngle(source.location, ship.location);
let destination = new ArenaLocation(ship.arena_x + Math.cos(angle) * this.value, ship.arena_y + Math.sin(angle) * this.value);
let exclusions = ExclusionAreas.fromShip(ship);
destination = exclusions.stopBefore(destination, ship.location);
ship.moveTo(destination.x, destination.y);
// TODO Apply area effect adding/removal
return [
new ShipMoveDiff(ship, ship.location, new ArenaLocationAngle(destination.x, destination.y, ship.arena_angle))
];
} else {
return [];
}
return true;
}
getDescription(): string {

View file

@ -0,0 +1,32 @@
module TK.SpaceTac.Specs {
testing("StickyEffect", test => {
test.case("applies to ship", check => {
let battle = new Battle();
let ship = battle.fleets[0].addShip();
check.in("before", check => {
check.equals(ship.active_effects.count(), 0, "no sticky effect");
check.equals(ship.getAttribute("precision"), 0, "precision");
})
let effect = new StickyEffect(new AttributeEffect("precision", 1), 2);
battle.applyDiffs(effect.getOnDiffs(ship, ship));
check.in("after", check => {
check.equals(ship.active_effects.count(), 1, "one sticky effect");
let sticked = ship.active_effects.list()[0];
if (sticked instanceof StickyEffect) {
check.equals(sticked.base, effect.base, "sticked effect");
check.equals(sticked.duration, 2, "sticked duration");
check.equals(ship.getAttribute("precision"), 1, "precision");
} else {
check.fail("Not a sticky effect");
}
})
});
test.case("gets a textual description", check => {
check.equals(new StickyEffect(new DamageEffect(10), 2).getDescription(), "do 10 damage for 2 turns");
});
});
}

View file

@ -2,65 +2,34 @@
module TK.SpaceTac {
/**
* Wrapper around another effect, to make it stick to a ship.
* Wrapper around another effect, to make it stick to a ship for a given number of turns.
*
* The "effect" is to stick the wrapped effect to the ship, that will be applied in time.
* The "effect" is to stick the wrapped effect to the ship.
*/
export class StickyEffect extends BaseEffect {
// Wrapped effect
base: BaseEffect;
base: BaseEffect
// Duration, in number of turns
duration: number;
// Apply the effect on stick (doesn't count against duration)
on_stick: boolean;
// Apply the effect on turn start instead of end
on_turn_end: boolean;
duration: number
// Base constructor
constructor(base: BaseEffect, duration = 0, on_stick = false, on_turn_end = false) {
constructor(base: BaseEffect, duration = 0) {
super(base.code);
this.base = base;
this.duration = duration;
this.on_stick = on_stick;
this.on_turn_end = on_turn_end;
}
applyOnShip(ship: Ship, source: Ship | Drone): boolean {
ship.addStickyEffect(new StickyEffect(this.base, this.duration, this.on_stick, this.on_turn_end));
if (this.on_stick) {
this.base.applyOnShip(ship, source);
}
return true;
}
getOnDiffs(ship: Ship, source: Ship | Drone): BaseBattleDiff[] {
// TODO if already there, remove the previous one to replace it
let result: BaseBattleDiff[] = [
new ShipEffectAddedDiff(ship, new StickyEffect(this.base, this.duration)),
]
private applyOnce(ship: Ship) {
if (this.duration > 0) {
this.base.applyOnShip(ship, ship); // FIXME Does not remember the source
this.duration--;
ship.setActiveEffectsChanged();
}
}
result = result.concat(this.base.getOnDiffs(ship, source));
/**
* Apply the effect at the beginning of the turn, for the ship this effect is sticked to.
*/
startTurn(ship: Ship) {
if (!this.on_turn_end) {
this.applyOnce(ship);
}
}
/**
* Apply the effect at the end of the turn, for the ship this effect is sticked to.
*/
endTurn(ship: Ship) {
if (this.on_turn_end) {
this.applyOnce(ship);
}
return result;
}
isBeneficial(): boolean {

View file

@ -3,16 +3,16 @@ module TK.SpaceTac {
test.case("adds an amount to a ship value", check => {
let effect = new ValueEffect("shield", 20);
let ship = new Ship();
ship.values.shield.setMaximal(80);
let battle = new Battle();
let ship = battle.fleets[0].addShip();
ship.setValue("shield", 55);
check.equals(ship.values.shield.get(), 55);
check.equals(ship.getValue("shield"), 55);
effect.applyOnShip(ship, ship);
check.equals(ship.values.shield.get(), 75);
battle.applyDiffs(effect.getOnDiffs(ship, ship));
check.equals(ship.getValue("shield"), 75);
effect.applyOnShip(ship, ship);
check.equals(ship.values.shield.get(), 80);
battle.applyDiffs(effect.getOnDiffs(ship, ship));
check.equals(ship.getValue("shield"), 95);
});
test.case("has a description", check => {

View file

@ -8,10 +8,10 @@ module TK.SpaceTac {
*/
export class ValueEffect extends BaseEffect {
// Affected value
valuetype: keyof ShipValues;
valuetype: keyof ShipValues
// Value to add (or subtract if negative)
value: number;
value: number
constructor(valuetype: keyof ShipValues, value: number = 0) {
super("value");
@ -20,8 +20,8 @@ module TK.SpaceTac {
this.value = value;
}
applyOnShip(ship: Ship, source: Ship | Drone): boolean {
return ship.setValue(this.valuetype, this.value, true);
getOnDiffs(ship: Ship, source: Ship | Drone): BaseBattleDiff[] {
return ship.getValueDiffs(this.valuetype, this.value, true);
}
isBeneficial(): boolean {
@ -33,7 +33,7 @@ module TK.SpaceTac {
}
getDescription(): string {
let attrname = SHIP_VALUES[this.valuetype].name;
let attrname = SHIP_VALUES_NAMES[this.valuetype];
return `${attrname} ${this.value > 0 ? "+" : "-"}${Math.abs(this.value)}`;
}
}

View file

@ -1,21 +1,22 @@
module TK.SpaceTac.Specs {
testing("ValueTransferEffect", test => {
test.case("takes or gives value", check => {
let ship1 = new Ship();
let battle = new Battle();
let ship1 = battle.fleets[0].addShip();
TestTools.setShipHP(ship1, 100, 50);
ship1.setValue("hull", 10);
let ship2 = new Ship();
let ship2 = battle.fleets[0].addShip();
TestTools.setShipHP(ship2, 100, 50);
let effect = new ValueTransferEffect("hull", -30);
effect.applyOnShip(ship2, ship1);
battle.applyDiffs(effect.getOnDiffs(ship2, ship1));
check.equals(ship1.getValue("hull"), 40);
check.equals(ship2.getValue("hull"), 70);
effect = new ValueTransferEffect("hull", 1000);
effect.applyOnShip(ship2, ship1);
battle.applyDiffs(effect.getOnDiffs(ship2, ship1));
check.equals(ship1.getValue("hull"), 0);
check.equals(ship2.getValue("hull"), 100);
check.equals(ship2.getValue("hull"), 110); // over limit but will be fixed later
})
test.case("builds a description", check => {

View file

@ -18,22 +18,20 @@ module TK.SpaceTac {
this.amount = amount;
}
applyOnShip(ship: Ship, source: Ship | Drone): boolean {
getOnDiffs(ship: Ship, source: Ship | Drone): BaseBattleDiff[] {
if (source instanceof Ship) {
if (this.amount < 0) {
return new ValueTransferEffect(this.valuetype, -this.amount).applyOnShip(source, ship);
return new ValueTransferEffect(this.valuetype, -this.amount).getOnDiffs(source, ship);
} else {
let amount = Math.min(source.getValue(this.valuetype), this.amount);
if (amount) {
source.setValue(this.valuetype, -amount, true);
ship.setValue(this.valuetype, amount, true);
return true;
return source.getValueDiffs(this.valuetype, -amount, true).concat(ship.getValueDiffs(this.valuetype, amount, true));
} else {
return false;
return [];
}
}
} else {
return false;
return [];
}
}
@ -46,7 +44,7 @@ module TK.SpaceTac {
}
getDescription(): string {
let attrname = SHIP_VALUES[this.valuetype].name;
let attrname = SHIP_VALUES_NAMES[this.valuetype];
let verb = (this.amount < 0 ? "steal" : "give");
return `${verb} ${Math.abs(this.amount)} ${attrname}`;
}

Some files were not shown because too many files have changed in this diff Show more