1
0
Fork 0

Simplified drones behavior

This commit is contained in:
Michaël Lemaire 2017-05-11 00:52:16 +02:00
parent d1f1c1281d
commit 5d4315f1ca
10 changed files with 132 additions and 215 deletions

View file

@ -96,18 +96,14 @@ item is then temporarily disabled (no more effects and cannot be used), until th
## Drones
Drones are static objects, deployed by ships, that apply effects in a circular zone around themselves.
Drone effects are applied :
* On all ships in the zone at the time the drone is deployed
* On any ship entering the zone
* On any ship inside the zone at the start and end of its turn (there and staying there)
A drone lasts for a given number of battle cycles. For example, if there are 8 ships in play, a 2-cycles
drone will try to activate 16 times, before being destroyed.
All drones activate between two ship turns. At each activation, the drone effects are applied to any ship
in the surrounding zone, except if less than a battle cycle passed since last activation for this ship.
Drones are fully autonomous, and once deployed, are not controlled by their owner ship.
They are small and cannot be the direct target of weapons. They are not affected by area effects,
except for area damage and area effects specifically designed for drones.
A drone lasts for a given number of turns, counting down each time its owner's turn starts.
When reaching the number of turns, the drone is destroyed (before the owner's turn is started).
For example, a drone with 1-turn duration will destroy just before the next turn of its owner.

View file

@ -252,7 +252,6 @@ module TS.SpaceTac {
let battle = new Battle();
let ship = new Ship();
let drone = new Drone(ship);
let onDeploy = spyOn(drone, "onDeploy");
expect(battle.drones).toEqual([]);
expect(battle.log.events).toEqual([]);
@ -261,13 +260,11 @@ module TS.SpaceTac {
expect(battle.drones).toEqual([drone]);
expect(battle.log.events).toEqual([new DroneDeployedEvent(drone)]);
expect(onDeploy).toHaveBeenCalledTimes(1);
battle.addDrone(drone);
expect(battle.drones).toEqual([drone]);
expect(battle.log.events).toEqual([new DroneDeployedEvent(drone)]);
expect(onDeploy).toHaveBeenCalledTimes(1);
battle.removeDrone(drone);

View file

@ -61,6 +61,13 @@ module TS.SpaceTac {
return result;
}
/**
* Get the number of turns in a game cycle.
*/
getCycleLength(): number {
return this.play_order.length;
}
/**
* Return an iterator over all ships engaged in the battle
*/
@ -178,13 +185,15 @@ module TS.SpaceTac {
advanceToNextShip(log: boolean = true): void {
var previous_ship = this.playing_ship;
if (this.playing_ship && this.playing_ship.playing) {
this.playing_ship.endTurn();
}
if (this.checkEndBattle(log)) {
return;
}
if (this.playing_ship && this.playing_ship.playing) {
this.playing_ship.endTurn();
}
this.drones.forEach(drone => drone.activate());
if (this.play_order.length === 0) {
this.playing_ship_index = null;
@ -298,7 +307,6 @@ module TS.SpaceTac {
if (log) {
this.log.add(new DroneDeployedEvent(drone));
}
drone.onDeploy(this.play_order);
}
}

View file

@ -34,94 +34,78 @@ module TS.SpaceTac {
}
describe("Drone", function () {
it("applies effects on deployment", function () {
let ship1 = new Ship(null, "ship1");
it("applies effects on all ships inside the radius", function () {
let battle = new Battle();
let ship1 = new Ship(battle.fleets[0], "ship1");
ship1.setArenaPosition(0, 0);
let ship2 = new Ship(null, "ship2");
let ship2 = new Ship(battle.fleets[0], "ship2");
ship2.setArenaPosition(5, 5);
let ship3 = new Ship(null, "ship3");
let ship3 = new Ship(battle.fleets[0], "ship3");
ship3.setArenaPosition(10, 10);
let ship4 = new Ship(battle.fleets[0], "ship4");
ship4.setArenaPosition(0, 0);
ship4.setDead();
let [drone, effect] = newTestDrone(2, 2, 8, ship1);
expect(effect.getApplyCalls()).toEqual([]);
drone.onDeploy([ship1, ship2, ship3]);
drone.activate();
expect(effect.getApplyCalls()).toEqual([ship1, ship2]);
});
it("applies effects on ships entering the radius", function () {
let owner = new Ship(null, "owner");
let target = new Ship(null, "target");
target.setArenaPosition(10, 10);
let [drone, effect] = newTestDrone(0, 0, 5, owner);
it("maintains ship application countdown", function () {
let battle = new Battle();
spyOn(battle, "getCycleLength").and.returnValue(7);
let ship = new Ship(battle.fleets[0]);
let drone = new Drone(ship, "test", 2);
expect(effect.getApplyCalls()).toEqual([], "initial");
drone.onTurnStart(target);
expect(effect.getApplyCalls()).toEqual([], "turn start");
target.setArenaPosition(2, 3);
drone.onShipMove(target);
expect(effect.getApplyCalls()).toEqual([target], "enter");
target.setArenaPosition(1, 1);
drone.onShipMove(target);
expect(effect.getApplyCalls()).toEqual([], "move inside");
target.setArenaPosition(12, 12);
drone.onShipMove(target);
expect(effect.getApplyCalls()).toEqual([], "exit");
target.setArenaPosition(1, 1);
drone.onShipMove(target);
expect(effect.getApplyCalls()).toEqual([target], "re-enter");
expect(drone.getShipCountdown(ship)).toBe(0);
drone.startShipCountdown(ship);
expect(drone.getShipCountdown(ship)).toBe(7);
});
it("applies effects on ships remaining in the radius", function () {
let owner = new Ship(null, "owner");
let target = new Ship(null, "target");
let [drone, effect] = newTestDrone(0, 0, 5, owner);
it("applies at most once per battle cycle", function () {
let battle = new Battle();
let ship1 = new Ship(battle.fleets[0], "ship1");
ship1.setArenaPosition(0, 0);
let ship2 = new Ship(battle.fleets[1], "ship2");
ship2.setArenaPosition(100, 100);
battle.throwInitiative();
expect(battle.getCycleLength()).toEqual(2);
target.setArenaPosition(1, 2);
drone.onTurnStart(target);
expect(effect.getApplyCalls()).toEqual([], "start inside");
let [drone, effect] = newTestDrone(2, 2, 8, ship1);
expect(effect.getApplyCalls()).toEqual([]);
target.setArenaPosition(2, 2);
drone.onShipMove(target);
expect(effect.getApplyCalls()).toEqual([], "move inside");
drone.activate();
expect(effect.getApplyCalls()).toEqual([ship1]);
drone.onTurnEnd(target);
expect(effect.getApplyCalls()).toEqual([target], "turn end");
drone.activate();
expect(effect.getApplyCalls()).toEqual([]);
drone.onTurnStart(target);
expect(effect.getApplyCalls()).toEqual([], "second turn start");
drone.activate();
expect(effect.getApplyCalls()).toEqual([ship1]);
target.setArenaPosition(12, 12);
drone.onShipMove(target);
expect(effect.getApplyCalls()).toEqual([], "move out");
drone.activate();
expect(effect.getApplyCalls()).toEqual([]);
drone.onTurnEnd(target);
expect(effect.getApplyCalls()).toEqual([], "second turn end");
drone.activate();
expect(effect.getApplyCalls()).toEqual([ship1]);
});
it("signals the need for destruction after its lifetime", function () {
let owner = new Ship(null, "owner");
let other = new Ship(null, "other");
let [drone, effect] = newTestDrone(0, 0, 5, owner);
drone.duration = 2;
let battle = new Battle();
spyOn(owner, "getBattle").and.returnValue(battle);
let owner = new Ship(battle.fleets[0]);
let [drone, effect] = newTestDrone(0, 0, 5, owner);
drone.duration = 3;
let removeDrone = spyOn(battle, "removeDrone").and.callThrough();
drone.onTurnStart(other);
drone.activate();
expect(removeDrone).not.toHaveBeenCalled();
drone.onTurnStart(owner);
drone.activate();
expect(removeDrone).not.toHaveBeenCalled();
drone.onTurnStart(other);
expect(removeDrone).not.toHaveBeenCalled();
drone.onTurnStart(owner);
expect(removeDrone).toHaveBeenCalledWith(drone);
drone.activate();
expect(removeDrone).toHaveBeenCalledWith(drone, true);
});
it("logs each activation", function () {
@ -144,14 +128,14 @@ module TS.SpaceTac {
it("builds a textual description", function () {
let drone = new Drone(new Ship());
drone.duration = 1;
expect(drone.getDescription()).toEqual("For 1 turn:\n• do nothing");
expect(drone.getDescription()).toEqual("For 1 activation:\n• do nothing");
drone.duration = 3;
drone.effects = [
new DamageEffect(5),
new AttributeEffect("skill_human", 1)
]
expect(drone.getDescription()).toEqual("For 3 turns:\n• do 5 damage\n• human skill +1");
expect(drone.getDescription()).toEqual("For 3 activations:\n• do 5 damage\n• human skill +1");
});
});
}

View file

@ -3,32 +3,34 @@ module TS.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;
// Code of the drone
code: string;
// Ship that deployed the drone
owner: Ship;
// Location in arena
x: number;
y: number;
radius: number;
// Lifetime in number of turns (not including the initial effect on deployment)
duration: number = 1;
// Remaining lifetime in number of turns
duration: number;
// Effects to apply
effects: BaseEffect[] = [];
// Ships registered inside the radius
inside: Ship[] = [];
// Cycle countdown for ships
countdown: [Ship, number][] = [];
// Ships starting their turn the radius
inside_at_start: Ship[] = [];
constructor(owner: Ship, code = "drone") {
constructor(owner: Ship, code = "drone", base_duration = 1) {
this.battle = owner.getBattle() || new Battle();
this.owner = owner;
this.code = code;
this.duration = base_duration * this.battle.getCycleLength();
}
/**
@ -39,14 +41,46 @@ module TS.SpaceTac {
if (effects.length == 0) {
effects = "• do nothing";
}
return `For ${this.duration} turn${this.duration > 1 ? "s" : ""}:\n${effects}`;
return `For ${this.duration} activation${this.duration > 1 ? "s" : ""}:\n${effects}`;
}
/**
* Filter the list of ships in radius.
* Get countdown until next activation for a given ship
*/
filterShipsInRadius(ships: Ship[]): Ship[] {
return ships.filter(ship => ship.isInCircle(this.x, this.y, this.radius));
getShipCountdown(ship: Ship): number {
let countdown = 0;
this.countdown.forEach(([iship, icountdown]) => {
if (iship === ship) {
countdown = icountdown;
}
});
return countdown;
}
/**
* Start the countdown for a given ship
*/
startShipCountdown(ship: Ship): void {
let found = false;
this.countdown = this.countdown.map(([iship, countdown]): [Ship, number] => {
if (iship === ship) {
found = true;
return [iship, this.battle.getCycleLength()];
} else {
return [iship, countdown];
}
});
if (!found) {
this.countdown.push([ship, this.battle.getCycleLength()]);
}
}
/**
* 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) && this.getShipCountdown(ship) == 0);
return imaterialize(ships);
}
/**
@ -55,68 +89,29 @@ module TS.SpaceTac {
* This does not check if the ships are in range.
*/
apply(ships: Ship[], log = true) {
ships = ships.filter(ship => ship.alive);
if (ships.length > 0) {
let battle = this.owner.getBattle();
if (battle && log) {
battle.log.add(new DroneAppliedEvent(this, ships));
if (log) {
this.battle.log.add(new DroneAppliedEvent(this, ships));
}
ships.forEach(ship => this.effects.forEach(effect => effect.applyOnShip(ship)));
ships.forEach(ship => {
this.startShipCountdown(ship);
this.effects.forEach(effect => effect.applyOnShip(ship));
});
}
}
/**
* Called when the drone is first deployed.
* Activate the drone
*/
onDeploy(ships: Ship[]) {
this.apply(this.filterShipsInRadius(ships));
}
activate(log = true) {
this.apply(this.getAffectedShips(), log);
/**
* Called when a ship turn starts
*/
onTurnStart(ship: Ship) {
if (ship == this.owner) {
this.duration--;
}
this.countdown = this.countdown.map(([ship, countdown]): [Ship, number] => [ship, countdown - 1]).filter(([ship, countdown]) => countdown > 0);
if (this.duration <= 0) {
if (this.owner) {
let battle = this.owner.getBattle();
if (battle) {
battle.removeDrone(this);
}
}
return;
}
if (ship.isInCircle(this.x, this.y, this.radius)) {
add(this.inside, ship);
add(this.inside_at_start, ship);
} else {
remove(this.inside_at_start, ship);
}
}
/**
* Called when a ship turn ends
*/
onTurnEnd(ship: Ship) {
if (this.duration > 0 && ship.isInCircle(this.x, this.y, this.radius) && contains(this.inside_at_start, ship)) {
this.apply([ship]);
}
}
/**
* Called after a ship moved
*/
onShipMove(ship: Ship) {
if (this.duration > 0 && ship.isInCircle(this.x, this.y, this.radius)) {
if (add(this.inside, ship)) {
this.apply([ship]);
}
} else {
remove(this.inside, ship);
this.duration--;
if (this.duration == 0) {
this.battle.removeDrone(this, log);
}
}
}

View file

@ -283,53 +283,6 @@ module TS.SpaceTac.Specs {
expect(ship.isInCircle(12, -4, 5)).toBe(false);
});
it("broadcasts to drones", function () {
let battle = new Battle();
let fleet = new Fleet();
fleet.setBattle(battle);
let ship = new Ship(fleet);
let drone = new Drone(ship);
let onTurnStart = spyOn(drone, "onTurnStart");
let onTurnEnd = spyOn(drone, "onTurnEnd");
let onShipMove = spyOn(drone, "onShipMove");
battle.addDrone(drone);
expect(onTurnStart).toHaveBeenCalledTimes(0);
expect(onTurnEnd).toHaveBeenCalledTimes(0);
expect(onShipMove).toHaveBeenCalledTimes(0);
ship.startTurn();
expect(onTurnStart).toHaveBeenCalledTimes(1);
expect(onTurnStart).toHaveBeenCalledWith(ship);
expect(onTurnEnd).toHaveBeenCalledTimes(0);
expect(onShipMove).toHaveBeenCalledTimes(0);
ship.moveTo(10, 10);
expect(onTurnStart).toHaveBeenCalledTimes(1);
expect(onTurnEnd).toHaveBeenCalledTimes(0);
expect(onShipMove).toHaveBeenCalledTimes(1);
expect(onShipMove).toHaveBeenCalledWith(ship);
ship.endTurn();
expect(onTurnStart).toHaveBeenCalledTimes(1);
expect(onTurnEnd).toHaveBeenCalledTimes(1);
expect(onTurnEnd).toHaveBeenCalledWith(ship);
expect(onShipMove).toHaveBeenCalledTimes(1);
ship.setDead();
ship.startTurn();
ship.endTurn();
expect(onTurnStart).toHaveBeenCalledTimes(2);
expect(onTurnEnd).toHaveBeenCalledTimes(2);
expect(onShipMove).toHaveBeenCalledTimes(1);
});
it("stores items in cargo space", function () {
let ship = new Ship();
let equipment1 = new Equipment();

View file

@ -313,14 +313,6 @@ module TS.SpaceTac {
}
}
// Call a method for each drone of the battlefield
forEachDrone(callback: (drone: Drone) => any) {
let battle = this.getBattle();
if (battle) {
battle.drones.forEach(callback);
}
}
/**
* Method called at the start of battle
*/
@ -361,9 +353,6 @@ module TS.SpaceTac {
this.sticky_effects.forEach(effect => effect.startTurn(this));
this.cleanStickyEffects();
}
// Broadcast to drones
this.forEachDrone(drone => drone.onTurnStart(this));
}
// Method called at the end of this ship turn
@ -374,9 +363,6 @@ module TS.SpaceTac {
}
this.playing = false;
// Broadcast to drones
this.forEachDrone(drone => drone.onTurnEnd(this));
if (this.alive) {
// Recover action points for next turn
this.updateAttributes();
@ -446,9 +432,6 @@ module TS.SpaceTac {
if (log) {
this.addBattleEvent(new MoveEvent(this, x, y));
}
// Broadcast to drones
this.forEachDrone(drone => drone.onShipMove(this));
}
}

View file

@ -30,6 +30,7 @@ module TS.SpaceTac {
let ship = battle.fleets[0].addShip();
ship.setArenaPosition(0, 0);
battle.playing_ship = ship;
spyOn(battle, "getCycleLength").and.returnValue(4);
TestTools.setShipAP(ship, 3);
let equipment = new Equipment(SlotType.Weapon, "testdrone");
let action = new DeployDroneAction(equipment, 2, 8, 2, 4, [new DamageEffect(50)]);
@ -43,7 +44,7 @@ module TS.SpaceTac {
let drone = battle.drones[0];
expect(drone.code).toEqual("testdrone");
expect(drone.duration).toEqual(2);
expect(drone.duration).toEqual(8);
expect(drone.owner).toBe(ship);
expect(drone.x).toEqual(5);
expect(drone.y).toEqual(0);

View file

@ -52,12 +52,11 @@ module TS.SpaceTac {
}
protected customApply(ship: Ship, target: Target) {
let drone = new Drone(ship, this.equipment.code);
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;
drone.duration = this.lifetime;
let battle = ship.getBattle();
if (battle) {
@ -66,7 +65,7 @@ module TS.SpaceTac {
}
getEffectsDescription(): string {
let desc = `Deploy drone for ${this.lifetime} turn${this.lifetime > 1 ? "s" : ""} (power usage ${this.power}, max range ${this.deploy_distance}km)`;
let desc = `Deploy drone for ${this.lifetime} cycle${this.lifetime > 1 ? "s" : ""} (power usage ${this.power}, max range ${this.deploy_distance}km)`;
let effects = this.effects.map(effect => {
let suffix = `for ships in ${this.effect_radius}km radius`;
if (effect instanceof StickyEffect) {

View file

@ -29,6 +29,7 @@ module TS.SpaceTac.Equipments {
let battle = new Battle();
let ship = battle.fleets[0].addShip();
battle.playing_ship = ship;
battle.play_order = [ship];
TestTools.setShipAP(ship, 10);
let result = equipment.action.apply(ship, new Target(5, 5, null));
expect(result).toBe(true);