Added basic structure for drones
This commit is contained in:
parent
3397009850
commit
45a13e9458
18
README.md
18
README.md
|
@ -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
2
TODO
|
@ -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
|
||||
|
|
|
@ -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)]);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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
123
src/game/Drone.spec.ts
Normal 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
106
src/game/Drone.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
15
src/game/events/DroneDeployedEvent.ts
Normal file
15
src/game/events/DroneDeployedEvent.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
15
src/game/events/DroneDestroyedEvent.ts
Normal file
15
src/game/events/DroneDestroyedEvent.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue