1
0
Fork 0

Added basic structure for drones

This commit is contained in:
Michaël Lemaire 2017-02-06 22:46:55 +01:00
parent 3397009850
commit 45a13e9458
11 changed files with 368 additions and 1 deletions

View file

@ -29,7 +29,7 @@ After making changes to sources, you need to recompile:
* **Power** - Available action points (some actions require more power than others)
* **Power recovery** - Power generated at the end of a ship's turn
## Capabilities
## Skills
* **Materials** - Usage of physical materials such as bullets, shells...
* **Electronics** - Components of computers and communication
@ -37,3 +37,19 @@ After making changes to sources, you need to recompile:
* **Human** - Management of a human team and resources
* **Gravity** - Interaction with gravitational forces
* **Time** - Manipulation of time
## 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)
Drones are fully autonomous, and once deployed, are not controlled by their owner ship.
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.

2
TODO
View file

@ -7,6 +7,8 @@
* Merge identical sticky effects
* Handle effects overflowing ship tooltip when too numerous
* Proper arena scaling (not graphical, only space coordinates)
* Add a fleet evaluator to make balanced fleets
* Fix AI playing in background in GameSession.spec.ts
* Mobile: think UI layout so that fingers do not block the view (right and left handed)
* Mobile: display tooltips larger and on the side of screen where the finger is not
* Mobile: targetting in two times, using a draggable target indicator

View file

@ -212,5 +212,40 @@ module TS.SpaceTac.Game {
var result = battle.collectShipsInCircle(Target.newFromLocation(5, 8), 3);
expect(result).toEqual([ship2, ship3]);
});
it("adds and remove drones", function () {
let battle = new Battle();
let ship = new Ship();
let drone = new Drone(ship);
expect(battle.drones).toEqual([]);
expect(battle.log.events).toEqual([]);
battle.addDrone(drone);
expect(battle.drones).toEqual([drone]);
expect(battle.log.events).toEqual([new DroneDeployedEvent(drone)]);
battle.addDrone(drone);
expect(battle.drones).toEqual([drone]);
expect(battle.log.events).toEqual([new DroneDeployedEvent(drone)]);
battle.removeDrone(drone);
expect(battle.drones).toEqual([]);
expect(battle.log.events).toEqual([new DroneDeployedEvent(drone), new DroneDestroyedEvent(drone)]);
battle.removeDrone(drone);
expect(battle.drones).toEqual([]);
expect(battle.log.events).toEqual([new DroneDeployedEvent(drone), new DroneDestroyedEvent(drone)]);
// check initial log fill
battle.drones = [drone];
battle.log.events = [];
battle.injectInitialEvents();
expect(battle.log.events).toEqual([new DroneDeployedEvent(drone)]);
});
});
}

View file

@ -22,6 +22,9 @@ module TS.SpaceTac.Game {
playing_ship_index: number;
playing_ship: Ship;
// List of deployed drones
drones: Drone[] = [];
// Boolean indicating if its the first turn
first_turn: boolean;
@ -239,6 +242,11 @@ module TS.SpaceTac.Game {
log.add(new MoveEvent(ship, ship.arena_x, ship.arena_y));
});
// Simulate drones deployment
this.drones.forEach(drone => {
log.add(new DroneDeployedEvent(drone));
});
// Simulate game turn
if (this.playing_ship) {
log.add(new ShipChangeEvent(this.playing_ship, this.playing_ship));
@ -261,5 +269,27 @@ module TS.SpaceTac.Game {
fleet.ships[i].setArenaFacingAngle(facing_angle);
}
}
/**
* Add a drone to the battle
*/
addDrone(drone: Drone, log = true) {
if (add(this.drones, drone)) {
if (log) {
this.log.add(new DroneDeployedEvent(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));
}
}
}
}
}

123
src/game/Drone.spec.ts Normal file
View file

@ -0,0 +1,123 @@
/// <reference path="effects/BaseEffect.ts" />
module TS.SpaceTac.Game {
/**
* Fake effect to capture apply requests
*/
class FakeEffect extends BaseEffect {
applied: Ship[] = []
constructor() {
super("fake");
}
applyOnShip(ship: Ship): boolean {
this.applied.push(ship);
return true;
}
getApplyCalls() {
let result = acopy(this.applied);
this.applied = [];
return result;
}
}
function newTestDrone(x: number, y: number, radius: number, owner: Ship): [Drone, FakeEffect] {
let drone = new Drone(owner);
drone.x = x;
drone.y = y;
drone.radius = radius;
let effect = new FakeEffect();
drone.effects.push(effect);
return [drone, effect];
}
describe("Drone", function () {
it("applies effects on deployment", function () {
let ship1 = new Ship(null, "ship1");
ship1.setArenaPosition(0, 0);
let ship2 = new Ship(null, "ship2");
ship2.setArenaPosition(5, 5);
let ship3 = new Ship(null, "ship3");
ship3.setArenaPosition(10, 10);
let [drone, effect] = newTestDrone(2, 2, 8, ship1);
expect(effect.getApplyCalls()).toEqual([]);
drone.onDeploy([ship1, ship2, ship3]);
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);
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");
});
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);
target.setArenaPosition(1, 2);
drone.onTurnStart(target);
expect(effect.getApplyCalls()).toEqual([], "start inside");
target.setArenaPosition(2, 2);
drone.onShipMove(target);
expect(effect.getApplyCalls()).toEqual([], "move inside");
drone.onTurnEnd(target);
expect(effect.getApplyCalls()).toEqual([target], "turn end");
drone.onTurnStart(target);
expect(effect.getApplyCalls()).toEqual([], "second turn start");
target.setArenaPosition(12, 12);
drone.onShipMove(target);
expect(effect.getApplyCalls()).toEqual([], "move out");
drone.onTurnEnd(target);
expect(effect.getApplyCalls()).toEqual([], "second turn end");
});
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 result = drone.onTurnStart(other);
expect(result).toBe(true);
result = drone.onTurnStart(owner);
expect(result).toBe(true);
result = drone.onTurnStart(other);
expect(result).toBe(true);
result = drone.onTurnStart(owner);
expect(result).toBe(false);
});
});
}

106
src/game/Drone.ts Normal file
View file

@ -0,0 +1,106 @@
/// <reference path="Serializable.ts"/>
module TS.SpaceTac.Game {
/**
* Drones are static objects that apply effects in a circular zone around themselves.
*/
export class Drone extends Serializable {
// 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;
// Effects to apply
effects: BaseEffect[] = [];
// Ships registered inside the radius
inside: Ship[] = [];
// Ships starting their turn the radius
inside_at_start: Ship[] = [];
constructor(owner: Ship) {
super();
this.owner = owner;
}
/**
* Call a function for each ship in radius.
*/
forEachInRadius(ships: Ship[], callback: (ship: Ship) => any) {
ships.forEach(ship => {
if (ship.isInCircle(this.x, this.y, this.radius)) {
callback(ship);
}
});
}
/**
* Apply the effects on a single ship.
*
* This does not check if the ship is in range.
*/
singleApply(ship: Ship) {
this.effects.forEach(effect => effect.applyOnShip(ship));
}
/**
* Called when the drone is first deployed.
*/
onDeploy(ships: Ship[]) {
this.forEachInRadius(ships, ship => this.singleApply(ship));
}
/**
* Called when a ship turn starts
*
* Returns false if the drone should be destroyed
*/
onTurnStart(ship: Ship): boolean {
if (ship == this.owner) {
if (this.duration <= 1) {
return false;
} else {
this.duration--;
}
}
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);
}
return true;
}
/**
* Called when a ship turn ends
*/
onTurnEnd(ship: Ship) {
if (ship.isInCircle(this.x, this.y, this.radius) && contains(this.inside_at_start, ship)) {
this.singleApply(ship);
}
}
/**
* Called after a ship moved
*/
onShipMove(ship: Ship) {
if (ship.isInCircle(this.x, this.y, this.radius)) {
if (add(this.inside, ship)) {
this.singleApply(ship);
}
} else {
remove(this.inside, ship);
}
}
}
}

View file

@ -10,6 +10,7 @@ module TS.SpaceTac.Game.Specs {
it("serializes to a string", () => {
var session = new GameSession();
session.startQuickBattle(true);
// TODO AI sometimes starts playing in background...
// Dump and reload
var dumped = session.saveToString();

View file

@ -254,5 +254,19 @@ module TS.SpaceTac.Game.Specs {
ship.recoverActionPoints();
expect(ship.ap_current.current).toBe(8);
});
it("checks if a ship is inside a given circle", function () {
let ship = new Ship();
ship.arena_x = 5;
ship.arena_y = 8;
expect(ship.isInCircle(5, 8, 0)).toBe(true);
expect(ship.isInCircle(5, 8, 1)).toBe(true);
expect(ship.isInCircle(5, 7, 1)).toBe(true);
expect(ship.isInCircle(6, 9, 1.7)).toBe(true);
expect(ship.isInCircle(5, 8.1, 0)).toBe(false);
expect(ship.isInCircle(5, 7, 0.9)).toBe(false);
expect(ship.isInCircle(12, -4, 5)).toBe(false);
});
});
}

View file

@ -277,6 +277,16 @@ module TS.SpaceTac.Game {
ended.forEach(effect => this.addBattleEvent(new EffectRemovedEvent(this, effect)));
}
/**
* Check if the ship is inside a given circular area
*/
isInCircle(x: number, y: number, radius: number): boolean {
let dx = this.arena_x - x;
let dy = this.arena_y - y;
let distance = Math.sqrt(dx * dx + dy * dy);
return distance <= radius;
}
// Move toward a location
// This does not check or consume action points
moveTo(x: number, y: number, log: boolean = true): void {

View file

@ -0,0 +1,15 @@
/// <reference path="BaseLogEvent.ts"/>
module TS.SpaceTac.Game {
// Event logged when a drone is deployed by a ship
export class DroneDeployedEvent extends BaseLogEvent {
// Pointer to the drone
drone: Drone;
constructor(drone: Drone) {
super("droneadd", drone.owner);
this.drone = drone;
}
}
}

View file

@ -0,0 +1,15 @@
/// <reference path="BaseLogEvent.ts"/>
module TS.SpaceTac.Game {
// Event logged when a drone is destroyed
export class DroneDestroyedEvent extends BaseLogEvent {
// Pointer to the drone
drone: Drone;
constructor(drone: Drone) {
super("dronedel", drone.owner);
this.drone = drone;
}
}
}