1
0
Fork 0

Removed dead ships from play order

This commit is contained in:
Michaël Lemaire 2017-10-26 00:45:53 +02:00
parent 939313889c
commit 4b2d46754f
38 changed files with 521 additions and 282 deletions

View file

@ -41,7 +41,6 @@ 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
* Remove dead ships from ship list and play order
* 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
@ -64,6 +63,7 @@ Battle
* Add shorcut to perform only the "move" part of a move+fire simulation
* 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
Ships models and equipments
---------------------------

@ -1 +1 @@
Subproject commit a530998523e8f8c7a37323d5dc047241f71f6a36
Subproject commit 586788a4685a5b9a5cc8f4882f48f18c91997dc3

View file

@ -63,54 +63,44 @@ module TK.SpaceTac {
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 ship1 = new Ship(fleet1, "ship1");
var ship2 = new Ship(fleet1, "ship2");
var ship3 = new Ship(fleet2, "ship3");
var battle = new Battle(fleet1, fleet2);
// Check empty play_order case
expect(battle.playing_ship).toBeNull();
expect(battle.playing_ship_index).toBeNull();
battle.advanceToNextShip();
expect(battle.playing_ship).toBeNull();
expect(battle.playing_ship_index).toBeNull();
// Force play order
iforeach(battle.iships(), ship => ship.setAttribute("maneuvrability", 1));
var gen = new SkewedRandomGenerator([0.1, 0.2, 0.0]);
battle.throwInitiative(gen);
expect(battle.playing_ship).toBeNull();
expect(battle.playing_ship_index).toBeNull();
battle.advanceToNextShip();
expect(battle.playing_ship).toBe(ship2);
expect(battle.playing_ship_index).toBe(0);
battle.advanceToNextShip();
expect(battle.playing_ship).toBe(ship1);
expect(battle.playing_ship_index).toBe(1);
battle.advanceToNextShip();
expect(battle.playing_ship).toBe(ship3);
expect(battle.playing_ship_index).toBe(2);
battle.advanceToNextShip();
expect(battle.playing_ship).toBe(ship2);
expect(battle.playing_ship_index).toBe(0);
// A dead ship is not skipped
// A dead ship is skipped
ship1.setDead();
battle.advanceToNextShip();
expect(battle.playing_ship).toBe(ship3);
expect(battle.playing_ship).toBe(ship1);
expect(battle.playing_ship_index).toBe(1);
// Playing ship dies
ship3.setDead();
battle.advanceToNextShip();
expect(battle.playing_ship).toBe(ship2);
});
it("calls startTurn on ships", function () {
@ -290,7 +280,7 @@ module TK.SpaceTac {
expect(battle.canPlay(player)).toBe(false);
let ship = new Ship();
battle.playing_ship = ship;
TestTools.setShipPlaying(battle, ship);
expect(battle.canPlay(player)).toBe(false);
@ -306,28 +296,28 @@ module TK.SpaceTac {
battle.advanceToNextShip();
expect(battle.playing_ship).toBe(battle.play_order[0]);
expect(battle.getTurnsBefore(battle.play_order[0])).toBe(0);
expect(battle.getTurnsBefore(battle.play_order[1])).toBe(1);
expect(battle.getTurnsBefore(battle.play_order[2])).toBe(2);
expect(battle.getPlayOrder(battle.play_order[0])).toBe(0);
expect(battle.getPlayOrder(battle.play_order[1])).toBe(1);
expect(battle.getPlayOrder(battle.play_order[2])).toBe(2);
battle.advanceToNextShip();
expect(battle.playing_ship).toBe(battle.play_order[1]);
expect(battle.getTurnsBefore(battle.play_order[0])).toBe(2);
expect(battle.getTurnsBefore(battle.play_order[1])).toBe(0);
expect(battle.getTurnsBefore(battle.play_order[2])).toBe(1);
expect(battle.getPlayOrder(battle.play_order[0])).toBe(2);
expect(battle.getPlayOrder(battle.play_order[1])).toBe(0);
expect(battle.getPlayOrder(battle.play_order[2])).toBe(1);
battle.advanceToNextShip();
expect(battle.getTurnsBefore(battle.play_order[0])).toBe(1);
expect(battle.getTurnsBefore(battle.play_order[1])).toBe(2);
expect(battle.getTurnsBefore(battle.play_order[2])).toBe(0);
expect(battle.getPlayOrder(battle.play_order[0])).toBe(1);
expect(battle.getPlayOrder(battle.play_order[1])).toBe(2);
expect(battle.getPlayOrder(battle.play_order[2])).toBe(0);
battle.advanceToNextShip();
expect(battle.getTurnsBefore(battle.play_order[0])).toBe(0);
expect(battle.getTurnsBefore(battle.play_order[1])).toBe(1);
expect(battle.getTurnsBefore(battle.play_order[2])).toBe(2);
expect(battle.getPlayOrder(battle.play_order[0])).toBe(0);
expect(battle.getPlayOrder(battle.play_order[1])).toBe(1);
expect(battle.getPlayOrder(battle.play_order[2])).toBe(2);
});
it("lists area effects", function () {

View file

@ -19,15 +19,15 @@ module TK.SpaceTac {
// List of fleets engaged in battle
fleets: Fleet[]
// List of ships, sorted by their initiative throw
// Container of all engaged ships
ships: RObjectContainer<Ship>
// List of playing ships, sorted by their initiative throw
play_order: Ship[]
play_index = -1
// Current turn
turn: number
// Current ship whose turn it is to play
playing_ship_index: number | null
playing_ship: Ship | null
// Current battle "cycle" (one cycle is one turn done for all ships in the play order)
cycle: number
// List of deployed drones
drones: Drone[] = []
@ -47,9 +47,8 @@ module TK.SpaceTac {
// Create a battle between two fleets
constructor(fleet1 = new Fleet(), fleet2 = new Fleet(), width = 1808, height = 948) {
this.fleets = [fleet1, fleet2];
this.ships = new RObjectContainer(fleet1.ships.concat(fleet2.ships));
this.play_order = [];
this.playing_ship_index = null;
this.playing_ship = null;
this.ended = false;
this.width = width;
this.height = height;
@ -82,21 +81,17 @@ module TK.SpaceTac {
}
/**
* Get the number of turns in a game cycle.
* Get the currently playing ship
*/
getCycleLength(): number {
return this.play_order.length;
get playing_ship(): Ship | null {
return this.play_order[this.play_index] || null;
}
/**
* Get the number of turns before a specific ship plays (currently playing ship will return 0).
* Get a ship by its ID.
*/
getTurnsBefore(ship: Ship): number {
let pos = this.play_order.indexOf(ship) - (this.playing_ship_index || 0);
if (pos < 0) {
pos += this.play_order.length;
}
return pos;
getShip(id: RObjectId): Ship | null {
return this.ships.get(id);
}
/**
@ -150,6 +145,42 @@ module TK.SpaceTac {
return (ship2.play_priority - ship1.play_priority);
});
this.play_order = play_order;
this.play_index = -1;
}
/**
* Get the number of turns before a specific ship plays (currently playing ship will return 0).
*
* Returns -1 if the ship is not in the play list.
*/
getPlayOrder(ship: Ship): number {
let index = this.play_order.indexOf(ship);
if (index < 0) {
return -1;
} else {
let result = index - this.play_index;
return (result < 0) ? result + this.play_order.length : result;
}
}
/**
* Add a ship in the play order list
*/
removeFromPlayOrder(idx: number): void {
this.play_order.splice(idx, 1);
if (idx <= this.play_index) {
this.play_index -= 1;
}
}
/**
* Remove a ship from the play order list
*/
insertInPlayOrder(idx: number, ship: Ship): void {
this.play_order.splice(idx, 0, ship);
if (idx <= this.play_index) {
this.play_index += 1;
}
}
// Defines the initial ship positions of all engaged fleets
@ -195,7 +226,7 @@ module TK.SpaceTac {
}
// Apply to all ships
iforeach(this.iships(), ship => ship.endBattle(this.turn));
iforeach(this.iships(), ship => ship.endBattle(this.cycle));
this.stats.onBattleEnd(this.fleets[0], this.fleets[1]);
}
@ -228,10 +259,10 @@ module TK.SpaceTac {
// 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 {
var previous_ship = this.playing_ship;
let previous_ship = this.playing_ship;
if (this.playing_ship && this.playing_ship.playing) {
this.playing_ship.endTurn();
if (previous_ship && previous_ship.playing) {
previous_ship.endTurn();
}
if (this.checkEndBattle(log)) {
@ -240,21 +271,14 @@ module TK.SpaceTac {
this.drones.forEach(drone => drone.activate());
if (this.play_order.length === 0) {
this.playing_ship_index = null;
this.playing_ship = null;
} else {
if (this.playing_ship_index == null) {
this.playing_ship_index = 0;
} else {
this.playing_ship_index = (this.playing_ship_index + 1) % this.play_order.length;
}
this.playing_ship = this.play_order[this.playing_ship_index];
this.play_index += 1;
if (this.play_index >= this.play_order.length) {
this.play_index = 0;
}
if (this.playing_ship) {
if (this.playing_ship_index == 0) {
this.turn += 1;
if (this.play_index == 0) {
this.cycle += 1;
}
this.playing_ship.startTurn();
}
@ -288,7 +312,7 @@ module TK.SpaceTac {
// This will not add any event to the battle log
start(): void {
this.ended = false;
this.turn = 0;
this.cycle = 0;
this.placeShips();
this.stats.onBattleStart(this.fleets[0], this.fleets[1]);
this.throwInitiative();
@ -324,7 +348,7 @@ module TK.SpaceTac {
// Indicate emergency stasis
this.play_order.forEach(ship => {
if (!ship.alive) {
let event = new DeathEvent(ship);
let event = new DeathEvent(this, ship);
event.initial = true;
result.push(event);
}
@ -347,6 +371,18 @@ module TK.SpaceTac {
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);
}
});
}
/**
* Defines the initial ship positions for one fleet
*

View file

@ -22,12 +22,13 @@ module TK.SpaceTac.Specs {
it("adds an equipment", function () {
let battle = new Battle();
battle.playing_ship = new Ship();
battle.playing_ship.upgradeSkill("skill_materials");
let ship = new Ship();
TestTools.setShipPlaying(battle, ship);
ship.upgradeSkill("skill_materials");
expect(battle.playing_ship.listEquipment()).toEqual([]);
expect(ship.listEquipment()).toEqual([]);
battle.cheats.equip("Iron Hull");
expect(battle.playing_ship.listEquipment()).toEqual([<any>jasmine.objectContaining({name: "Iron Hull", level: 1})]);
expect(ship.listEquipment()).toEqual([<any>jasmine.objectContaining({ name: "Iron Hull", level: 1 })]);
})
})
}

View file

@ -83,7 +83,8 @@ module TK.SpaceTac {
});
it("checks if a fleet is alive", function () {
let fleet = new Fleet();
let battle = new Battle();
let fleet = battle.fleets[0];
expect(fleet.isAlive()).toBe(false);
let ship1 = fleet.addShip();

View file

@ -61,6 +61,9 @@ module TK.SpaceTac {
}
add(this.ships, ship);
ship.fleet = this;
if (this.battle) {
this.battle.ships.add(ship);
}
return ship;
}

View file

@ -236,7 +236,7 @@ module TK.SpaceTac.Specs {
expect(action.activated).toBe(false);
let battle = new Battle(ship.fleet);
battle.playing_ship = ship;
TestTools.setShipPlaying(battle, ship);
ship.startTurn();
expect(action.activated).toBe(false);
@ -273,7 +273,7 @@ module TK.SpaceTac.Specs {
let shield = ship1.addSlot(SlotType.Shield).attach(new Equipment(SlotType.Shield));
shield.action = new ToggleAction(shield, 0, 15, [new AttributeEffect("shield_capacity", 5)]);
battle.playing_ship = ship1;
TestTools.setShipPlaying(battle, ship1);
shield.action.apply(ship1);
expect(ship1.getAttribute("shield_capacity")).toBe(5);
@ -325,15 +325,15 @@ module TK.SpaceTac.Specs {
ship.addDamage(5, 0);
expect(ship.alive).toBe(false);
expect(battle.log.events.length).toBe(4);
expect(battle.log.events.length).toBe(3);
expect(battle.log.events[0].code).toEqual("value");
expect(battle.log.events[1].code).toEqual("damage");
expect(battle.log.events[2].code).toEqual("activeeffects");
expect(battle.log.events[3].code).toEqual("death");
expect(battle.log.events[2].code).toEqual("death");
});
it("checks if a ship is able to play", function () {
var ship = new Ship();
let battle = new Battle();
let ship = battle.fleets[0].addShip();
expect(ship.isAbleToPlay()).toBe(false);
expect(ship.isAbleToPlay(false)).toBe(true);

View file

@ -1,8 +1,10 @@
/// <reference path="../common/RObject.ts" />
module TK.SpaceTac {
/**
* A single ship in a fleet
*/
export class Ship {
export class Ship extends RObject {
// Fleet this ship is a member of
fleet: Fleet
@ -56,6 +58,8 @@ module TK.SpaceTac {
// Create a new ship inside a fleet
constructor(fleet: Fleet | null = null, name = "unnamed", model = new ShipModel("default", "Default", 1, 0, false, 0)) {
super();
this.fleet = fleet || new Fleet();
this.name = name;
this.alive = true;
@ -463,20 +467,49 @@ module TK.SpaceTac {
}
}
/**
* Get the events needed to apply changes to ship values or attributes
*/
getValueEvents(name: keyof ShipValues, value: number): BaseBattleEvent[] {
let result: BaseBattleEvent[] = [];
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()));
}
return result;
}
/**
* Produce events to set the ship in emergency stasis
*/
getDeathEvents(battle: Battle): BaseBattleEvent[] {
let result: BaseBattleEvent[] = [];
keys(SHIP_VALUES).forEach(value => {
result = result.concat(this.getValueEvents(value, 0));
});
// TODO Remove sticky effects
result.push(new DeathEvent(battle, this));
return result;
}
/**
* Set the death status on this ship
*/
setDead(log: boolean = true): void {
this.alive = false;
this.values.hull.set(0);
this.values.shield.set(0);
this.values.power.set(0);
this.sticky_effects = [];
this.setActiveEffectsChanged();
if (log) {
this.addBattleEvent(new DeathEvent(this));
setDead(): void {
let battle = this.getBattle();
if (battle) {
let events = this.getDeathEvents(battle);
battle.applyEvents(events);
} else {
console.error("Cannot set ship dead outside of battle", this);
}
}
@ -500,7 +533,7 @@ module TK.SpaceTac {
if (this.values.hull.get() === 0) {
// Ship is dead
this.setDead(log);
this.setDead();
}
}

View file

@ -53,6 +53,13 @@ module TK.SpaceTac {
return equipment;
}
// Set the current playing ship
static setShipPlaying(battle: Battle, ship: Ship): void {
add(battle.play_order, ship);
battle.play_index = battle.play_order.indexOf(ship);
ship.playing = true;
}
// Set a ship action points, adding/updating an equipment if needed
static setShipAP(ship: Ship, points: number, recovery: number = 0): void {
var equipment = this.getOrGenEquipment(ship, SlotType.Power, new Equipments.NuclearReactor());

View file

@ -26,7 +26,7 @@ module TK.SpaceTac {
let battle = new Battle();
let ship = battle.fleets[0].addShip();
ship.setArenaPosition(0, 0);
battle.playing_ship = ship;
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)]);

View file

@ -20,11 +20,11 @@ module TK.SpaceTac.Specs {
let battle = Battle.newQuickRandom();
let action = new EndTurnAction();
expect(battle.playing_ship_index).toBe(0);
expect(battle.play_index).toBe(0);
let result = action.apply(battle.play_order[0], Target.newFromShip(battle.play_order[0]));
expect(result).toBe(true);
expect(battle.playing_ship_index).toBe(1);
expect(battle.play_index).toBe(1);
});
});
}

View file

@ -3,7 +3,7 @@ module TK.SpaceTac {
it("checks movement against remaining AP", function () {
var ship = new Ship();
var battle = new Battle(ship.fleet);
battle.playing_ship = ship;
TestTools.setShipPlaying(battle, ship);
ship.values.power.setMaximal(20);
ship.values.power.set(6);
ship.arena_x = 0;
@ -45,7 +45,7 @@ module TK.SpaceTac {
ship.arena_y = 0;
var engine = new Equipment();
var action = new MoveAction(engine, 1);
battle.playing_ship = ship;
TestTools.setShipPlaying(battle, ship);
spyOn(console, "warn").and.stub();

View file

@ -29,7 +29,7 @@ module TK.SpaceTac {
let battle = new Battle(fleet);
battle.play_order = [ship, ship1, ship2, ship3];
battle.playing_ship = ship;
TestTools.setShipPlaying(battle, ship);
fleet.setBattle(battle);
action.apply(ship, Target.newFromLocation(50, 50));

View file

@ -78,7 +78,7 @@ module TK.SpaceTac {
});
// Run battle
while (!battle.ended && battle.turn < 100) {
while (!battle.ended && battle.cycle < 100) {
if (this.stopped) {
return;
}

View file

@ -24,7 +24,7 @@ module TK.SpaceTac.Specs {
it("applies the highest evaluated maneuver", function () {
let battle = new Battle();
let ship = battle.fleets[0].addShip();
battle.playing_ship = ship;
TestTools.setShipPlaying(battle, ship);
ship.playing = true;
let ai = new TacticalAI(ship, Timer.synchronous);

View file

@ -8,7 +8,7 @@ module TK.SpaceTac.Specs {
let ship1b = battle.fleets[1].addShip(new Ship(null, "1B"));
TestTools.setShipAP(ship0a, 10);
battle.playing_ship = ship0a;
TestTools.setShipPlaying(battle, ship0a);
let result = imaterialize(TacticalAIHelpers.produceDirectShots(ship0a, battle));
expect(result.length).toBe(0);
@ -30,7 +30,7 @@ module TK.SpaceTac.Specs {
let ship = battle.fleets[0].addShip();
TestTools.setShipAP(ship, 10);
battle.playing_ship = ship;
TestTools.setShipPlaying(battle, ship);
let result = imaterialize(TacticalAIHelpers.produceRandomMoves(ship, battle, 2, 1));
expect(result.length).toBe(0);
@ -52,7 +52,7 @@ module TK.SpaceTac.Specs {
let weapon = TestTools.addWeapon(ship, 50, 1, 1000, 105);
TestTools.setShipAP(ship, 10);
battle.playing_ship = ship;
TestTools.setShipPlaying(battle, ship);
let result = imaterialize(TacticalAIHelpers.produceInterestingBlastShots(ship, battle));
expect(result.length).toBe(0);

View file

@ -1,7 +1,8 @@
module TK.SpaceTac.Specs {
describe("DamageEffect", function () {
it("applies damage and wear", function () {
var ship = new Ship();
let battle = new Battle();
let ship = battle.fleets[0].addShip();
TestTools.setShipHP(ship, 150, 400);
let hull = ship.listEquipment(SlotType.Hull)[0];

View file

@ -40,8 +40,7 @@ module TK.SpaceTac.Equipments {
ship1.setArenaPosition(0, 0);
ship2.setArenaPosition(100, 0);
ship3.setArenaPosition(800, 0);
battle.playing_ship = ship1;
ship1.playing = true;
TestTools.setShipPlaying(battle, ship1);
expect(ship1.getAvailableActions()).toEqual([action, new EndTurnAction()]);
TestTools.setShipHP(ship1, 100, 0);

View file

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

View file

@ -28,7 +28,16 @@ module TK.SpaceTac {
*
* By default it does nothing
*/
apply(battle: Battle) {
apply(battle: Battle): void {
}
/**
* Reverts the event from a battle state
*
* By default it applies the reverse event
*/
revert(battle: Battle): void {
this.getReverse().apply(battle);
}
/**

View file

@ -0,0 +1,39 @@
/// <reference path="../../common/Testing.ts" />
module TK.SpaceTac.Specs {
testing("DeathEvent", 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];
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");
let event = new DeathEvent(battle, ship2);
event.apply(battle);
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");
event.revert(battle);
check.equals(ship2.alive, true, "alive again");
check.equals(imaterialize(battle.iships(false)), [ship1, ship2, ship3], "in all ships");
check.equals(imaterialize(battle.iships(true)), [ship1, ship2, ship3], "back 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], "back in play order");
});
});
}

View file

@ -1,10 +1,48 @@
/// <reference path="BaseBattleEvent.ts"/>
module TK.SpaceTac {
// Event logged when a ship is dead
/**
* 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 DeathEvent extends BaseLogShipEvent {
constructor(ship: Ship) {
// Unique ID of the ship in the battle
ship_id: RObjectId
// Index in the play order at which the ship was
play_index: number
constructor(battle: Battle, ship: Ship) {
super("death", ship);
this.ship_id = ship.id;
this.play_index = battle.play_order.indexOf(ship);
}
apply(battle: Battle) {
let ship = battle.getShip(this.ship_id);
if (ship) {
ship.alive = false;
if (this.play_index >= 0) {
battle.removeFromPlayOrder(this.play_index);
}
} else {
console.warn("Ship not found", this);
}
}
revert(battle: Battle) {
let ship = battle.getShip(this.ship_id);
if (ship) {
ship.alive = true;
if (this.play_index >= 0) {
battle.insertInPlayOrder(this.play_index, ship);
}
} else {
console.warn("Ship not found", this);
}
}
}
}

View file

@ -1,9 +1,27 @@
/// <reference path="../../common/Testing.ts" />
module TK.SpaceTac.Specs {
describe("ValueChangeEvent", function () {
it("get reverse event", function () {
testing("ValueChangeEvent", test => {
test.case("get reverse event", check => {
let ship = new Ship();
let event = new ValueChangeEvent(ship, new ShipValue("hull", 15, 22), 10);
expect(event.getReverse()).toEqual(new ValueChangeEvent(ship, new ShipValue("hull", 5, 22), -10));
check.equals(event.getReverse(), new ValueChangeEvent(ship, new ShipValue("hull", 5, 22), -10));
});
test.case("applies and reverts", check => {
let battle = new Battle();
let ship = battle.fleets[0].addShip();
check.equals(ship.getValue("hull"), 0);
let events = ship.getValueEvents("hull", 15);
check.equals(ship.getValue("hull"), 0);
check.equals(events.length, 1);
events[0].apply(battle);
check.equals(ship.getValue("hull"), 15);
events[0].revert(battle);
check.equals(ship.getValue("hull"), 0);
});
});
}

View file

@ -5,15 +5,19 @@ module TK.SpaceTac {
* Event logged when a ship value or attribute changed
*/
export class ValueChangeEvent extends BaseLogShipEvent {
// Ship ID
ship_id: RObjectId
// Saved version of the current value
value: ShipValue;
value: ShipValue
// Value variation
diff: number;
diff: number
constructor(ship: Ship, value: ShipValue, diff: number) {
super("value", ship);
this.ship_id = ship.id;
this.value = copy(value);
this.diff = diff;
}
@ -23,5 +27,14 @@ module TK.SpaceTac {
value.set(value.get() - this.diff);
return new ValueChangeEvent(this.ship, value, -this.diff);
}
apply(battle: Battle): void {
let ship = battle.getShip(this.ship_id);
if (ship) {
ship.setValue(<any>this.value.name, this.value.get(), false, false);
} else {
console.warn("Ship not found", this);
}
}
}
}

View file

@ -1,6 +1,6 @@
module TK.SpaceTac.Multi.Specs {
describe("Connection", function () {
async_it("finds an unused token", async function () {
testing("Connection", test => {
test.acase("finds an unused token", async check => {
let storage = new FakeRemoteStorage();
let connection = new Connection("test", storage);
@ -15,7 +15,7 @@ module TK.SpaceTac.Multi.Specs {
expect(other).toEqual("123456");
});
async_it("loads a session by its id", async function () {
test.acase("loads a session by its id", async check => {
let session = new GameSession();
let serializer = new Serializer(TK.SpaceTac);
let storage = new FakeRemoteStorage();
@ -42,7 +42,7 @@ module TK.SpaceTac.Multi.Specs {
expect(result).toBeNull();
});
async_it("lists saves from a device", async function () {
test.acase("lists saves from a device", async check => {
let storage = new FakeRemoteStorage();
let connection = new Connection("test", storage);
@ -57,7 +57,7 @@ module TK.SpaceTac.Multi.Specs {
expect(result).toEqual({ abc: "ABC", cba: "CBA" });
});
async_it("publishes saves and retrieves them by token", async function () {
test.acase("publishes saves and retrieves them by token", async check => {
let session = new GameSession();
let storage = new FakeRemoteStorage();
let connection = new Connection("test", storage);

View file

@ -1,5 +1,5 @@
module TK.SpaceTac.Multi.Specs {
describe("Exchange", function () {
testing("Exchange", test => {
function newExchange(token: string, storage = new FakeRemoteStorage()): [FakeRemoteStorage, Exchange, Exchange] {
let connection = new Connection("test", storage);
@ -17,7 +17,7 @@ module TK.SpaceTac.Multi.Specs {
spyOn(console, "log").and.stub();
});
async_it("says hello on start", async function () {
test.acase("says hello on start", async function () {
let [storage, peer1, peer2] = newExchange("abc");
spyOn(peer1, "getNextId").and.returnValues("1A", "1B", "1C");
spyOn(peer2, "getNextId").and.returnValues("2A", "2B", "2C");

View file

@ -1,6 +1,6 @@
module TK.SpaceTac.Multi.Specs {
describe("FakeRemoteStorage", function () {
async_it("can fetch a single record", async function () {
testing("FakeRemoteStorage", test => {
test.acase("can fetch a single record", async function () {
let storage = new FakeRemoteStorage();
let result = await storage.find("test", { key: 5 });
@ -21,7 +21,7 @@ module TK.SpaceTac.Multi.Specs {
expect(result).toBeNull();
});
async_it("inserts or updates objects", async function () {
test.acase("inserts or updates objects", async function () {
let storage = new FakeRemoteStorage();
let result = await storage.search("test", { key: 5 });

View file

@ -241,17 +241,21 @@ module TK.SpaceTac.UI {
*/
getImageInfo(name: string): { key: string, frame: number } {
// TODO Cache
let i = 1;
while (this.game.cache.checkImageKey(`atlas-${i}`)) {
let data = this.game.cache.getFrameData(`atlas-${i}`);
let frames = data.getFrames();
let frame = first(frames, frame => AssetLoading.getKey(frame.name) == `graphics-exported-${name}`);
if (frame) {
return { key: `atlas-${i}`, frame: frame.index };
if (this.game.cache.checkImageKey(name)) {
return { key: name, frame: 0 };
} else {
let i = 1;
while (this.game.cache.checkImageKey(`atlas-${i}`)) {
let data = this.game.cache.getFrameData(`atlas-${i}`);
let frames = data.getFrames();
let frame = first(frames, frame => AssetLoading.getKey(frame.name) == `graphics-exported-${name}`);
if (frame) {
return { key: `atlas-${i}`, frame: frame.index };
}
i++;
}
i++;
return { key: `-missing-${name}`, frame: 0 };
}
return { key: `-missing-${name}`, frame: 0 };
}
/**

View file

@ -121,7 +121,7 @@ module TK.SpaceTac.UI {
this.updateEffectsRadius();
// Set location
if (this.battleview.battle.turn == 1 && ship.alive && ship.fleet.player === this.battleview.player) {
if (this.battleview.battle.cycle == 1 && this.battleview.battle.play_index == 0 && ship.alive && ship.fleet.player === this.battleview.player) {
this.position.set(ship.arena_x - 500 * Math.cos(ship.arena_angle), ship.arena_y - 500 * Math.sin(ship.arena_angle));
this.moveTo(ship.arena_x, ship.arena_y, ship.arena_angle);
} else {
@ -145,7 +145,7 @@ module TK.SpaceTac.UI {
if (event.new_ship === this.ship) {
this.play_order.text = "-";
} else {
this.play_order.text = this.battleview.battle.getTurnsBefore(this.ship).toString();
this.play_order.text = this.battleview.battle.getPlayOrder(this.ship).toString();
}
}
return 0;

View file

@ -1,8 +1,19 @@
/// <reference path="../BaseView.ts"/>
module TK.SpaceTac.UI {
// Interactive view of a Battle
export class BattleView extends BaseView {
/**
* Interface for interacting with a ship (hover and click)
*/
export interface IShipButton {
cursorOnShip: (ship: Ship) => void;
cursorOffShip: (ship: Ship) => void;
cursorClicked: () => void;
}
/**
* Interactive view of a Battle
*/
export class BattleView extends BaseView implements IShipButton {
// Displayed battle
battle: Battle
@ -102,8 +113,8 @@ module TK.SpaceTac.UI {
// Add UI elements
this.action_bar = new ActionBar(this);
this.action_bar.position.set(0, this.getHeight() - 132);
this.ship_list = new ShipList(this);
this.ship_list.position.set(this.getWidth() - 112, 0);
this.ship_list = new ShipList(this, this.battle, this.player, this.toggle_tactical_mode, this,
this.layer_borders, this.getWidth() - 112, 0);
this.ship_tooltip = new ShipTooltip(this);
this.character_sheet = new CharacterSheet(this, -this.getWidth());
this.layer_sheets.add(this.character_sheet);
@ -179,7 +190,7 @@ module TK.SpaceTac.UI {
numberPressed(num: number): void {
if (this.interacting) {
if (this.targetting.active) {
let ship = ifirst(this.battle.iships(true), ship => this.battle.getTurnsBefore(ship) == num % 10);
let ship = ifirst(this.battle.iships(true), ship => this.battle.getPlayOrder(ship) == num % 10);
if (ship) {
this.targetting.setTarget(Target.newFromShip(ship));
}

View file

@ -12,7 +12,7 @@ module TK.SpaceTac.UI.Specs {
}
apply(battle: Battle) {
battle.turn += this.diff;
battle.cycle += this.diff;
}
getReverse(): BaseBattleEvent {
@ -31,42 +31,42 @@ module TK.SpaceTac.UI.Specs {
event.apply(battle);
return 0;
});
expect(battle.turn).toBe(1);
expect(battle.cycle).toBe(1);
expect(processor.atStart()).toBe(true);
expect(processor.atEnd()).toBe(true);
processor.stepForward();
expect(battle.turn).toBe(1);
expect(battle.cycle).toBe(1);
expect(processor.atStart()).toBe(true);
expect(processor.atEnd()).toBe(true);
battle.log.add(new FakeEvent());
expect(battle.turn).toBe(1);
expect(battle.cycle).toBe(1);
expect(processor.atStart()).toBe(true);
expect(processor.atEnd()).toBe(false);
processor.stepForward();
expect(battle.turn).toBe(2);
expect(battle.cycle).toBe(2);
expect(processor.atStart()).toBe(false);
expect(processor.atEnd()).toBe(true);
processor.stepForward();
expect(battle.turn).toBe(2);
expect(battle.cycle).toBe(2);
expect(processor.atStart()).toBe(false);
expect(processor.atEnd()).toBe(true);
processor.stepBackward();
expect(battle.turn).toBe(1);
expect(battle.cycle).toBe(1);
expect(processor.atStart()).toBe(true);
expect(processor.atEnd()).toBe(false);
processor.stepBackward();
expect(battle.turn).toBe(1);
expect(battle.cycle).toBe(1);
expect(processor.atStart()).toBe(true);
expect(processor.atEnd()).toBe(false);
processor.stepForward();
expect(battle.turn).toBe(2);
expect(battle.cycle).toBe(2);
expect(processor.atStart()).toBe(false);
expect(processor.atEnd()).toBe(true);
})

View file

@ -288,7 +288,7 @@ module TK.SpaceTac.UI {
private processShipChangeEvent(event: ShipChangeEvent): number {
this.current_ship = event.new_ship;
this.view.arena.setShipPlaying(event.new_ship);
this.view.ship_list.setPlaying(event.new_ship);
this.view.ship_list.refresh(!event.initial);
if (event.ship !== event.new_ship) {
this.view.audio.playOnce("battle-ship-change");
}
@ -306,13 +306,19 @@ module TK.SpaceTac.UI {
// A ship died
private processDeathEvent(event: DeathEvent): number {
if (this.view.ship_hovered === event.ship) {
this.view.setShipHovered(null);
}
this.view.arena.markAsDead(event.ship);
this.view.ship_list.markAsDead(event.ship);
let ship = this.battle.getShip(event.ship_id);
return event.initial ? 0 : 1000;
if (ship) {
if (this.view.ship_hovered === ship) {
this.view.setShipHovered(null);
}
this.view.arena.markAsDead(ship);
this.view.ship_list.refresh(!event.initial);
return event.initial ? 0 : 3000;
} else {
return 0;
}
}
// Weapon used

View file

@ -1,26 +1,65 @@
/// <reference path="../TestGame.ts"/>
module TK.SpaceTac.UI.Specs {
describe("ShipList", function () {
let testgame = setupBattleview();
testing("ShipList", test => {
let testgame = setupEmptyView();
it("handles play position of ships", function () {
let battleview = testgame.view;
var list = battleview.ship_list;
function createList(): ShipList {
let view = testgame.view;
let battle = new Battle();
let tactical_mode = new Toggle();
let ship_buttons = jasmine.createSpyObj("ship_buttons", ["cursorOnShip", "cursorOffShip", "cursorClicked"]);
let list = new ShipList(view, battle, battle.fleets[0].player, tactical_mode, ship_buttons);
return list;
}
expect(battleview.battle.play_order.length).toBe(10);
expect(list.children.length).toBe(11);
test.case("handles play position of ships", check => {
let list = createList();
let battle = list.battle;
check.equals(list.items.length, 0, "no item at first");
expect(list.findPlayPosition(battleview.battle.play_order[0])).toBe(0);
expect(list.findPlayPosition(battleview.battle.play_order[1])).toBe(1);
expect(list.findPlayPosition(battleview.battle.play_order[2])).toBe(2);
battle.fleets[0].addShip();
list.setShipsFromBattle(battle, false);
check.equals(list.items.length, 1, "one ship added but not in play order");
check.equals(list.items[0].visible, false, "one ship added but not in play order");
spyOn(battleview.battle, "playAI").and.stub();
battleview.battle.advanceToNextShip();
battle.throwInitiative();
list.refresh(false);
check.equals(list.items[0].visible, true, "ship now in play order");
expect(list.findPlayPosition(battleview.battle.play_order[0])).toBe(9);
expect(list.findPlayPosition(battleview.battle.play_order[1])).toBe(0);
expect(list.findPlayPosition(battleview.battle.play_order[2])).toBe(1);
battle.fleets[1].addShip();
battle.throwInitiative();
list.setShipsFromBattle(battle, false);
check.equals(list.items.length, 2, "ship added in the other fleet");
check.equals(nn(list.findItem(battle.play_order[0])).position, new Phaser.Point(2, 843));
check.equals(nn(list.findItem(battle.play_order[1])).position, new Phaser.Point(2, 744));
battle.advanceToNextShip();
list.refresh(false);
check.equals(nn(list.findItem(battle.play_order[0])).position, new Phaser.Point(-18, 962));
check.equals(nn(list.findItem(battle.play_order[1])).position, new Phaser.Point(2, 843));
battle.advanceToNextShip();
list.refresh(false);
check.equals(nn(list.findItem(battle.play_order[0])).position, new Phaser.Point(2, 843));
check.equals(nn(list.findItem(battle.play_order[1])).position, new Phaser.Point(-18, 962));
battle.fleets[1].addShip();
battle.throwInitiative();
battle.advanceToNextShip();
list.setShipsFromBattle(battle, false);
check.equals(list.items.length, 3, "three ships");
check.equals(nn(list.findItem(battle.play_order[0])).position, new Phaser.Point(-18, 962));
check.equals(nn(list.findItem(battle.play_order[1])).position, new Phaser.Point(2, 843));
check.equals(nn(list.findItem(battle.play_order[2])).position, new Phaser.Point(2, 744));
let dead = battle.play_order[1];
dead.setDead();
list.refresh(false);
check.equals(list.items.length, 3, "dead ship");
check.equals(nn(list.findItem(battle.play_order[0])).position, new Phaser.Point(-18, 962));
check.equals(nn(list.findItem(dead)).position, new Phaser.Point(200, 843));
check.equals(nn(list.findItem(battle.play_order[1])).position, new Phaser.Point(2, 843));
});
});
}

View file

@ -1,126 +1,117 @@
module TK.SpaceTac.UI {
// Bar with all playing ships, by play order
export class ShipList extends Phaser.Image {
// Link to the parent battleview
battleview: BattleView;
/**
* Side bar with all playing ships, sorted by play order
*/
export class ShipList {
// Link to the parent view
view: BaseView
// Current battle
battle: Battle
// Current player
player: Player
// Interface for acting as ship button
ship_buttons: IShipButton
// Container
container: Phaser.Image
// List of ship items
ships_container: Phaser.Group;
ships: ShipListItem[];
// Playing ship
playing: ShipListItem | null;
items: ShipListItem[]
// Hovered ship
hovered: ShipListItem | null;
hovered: ShipListItem | null
// Info button
info_button: Phaser.Button;
info_button: Phaser.Button
// Create an empty action bar
constructor(battleview: BattleView) {
super(battleview.game, 0, 0, "battle-shiplist-background");
constructor(view: BaseView, battle: Battle, player: Player, tactical_mode: Toggle, ship_buttons: IShipButton, parent?: UIContainer, x = 0, y = 0) {
let builder = new UIBuilder(view, parent);
this.container = builder.image("battle-shiplist-background", x, y);
this.battleview = battleview;
this.ships = [];
this.playing = null;
this.view = view;
// TODO Should use an UI game state, not the actual game state
this.battle = battle;
this.player = player;
this.ship_buttons = ship_buttons;
this.items = [];
this.hovered = null;
this.info_button = new Phaser.Button(this.game, 0, 0, "battle-shiplist-info-button");
this.battleview.inputs.setHoverClick(this.info_button,
() => this.battleview.toggle_tactical_mode.manipulate("button")(true),
() => this.battleview.toggle_tactical_mode.manipulate("button")(false),
this.info_button = new Phaser.Button(view.game, 0, 0, "battle-shiplist-info-button");
this.view.inputs.setHoverClick(this.info_button,
() => tactical_mode.manipulate("shiplist")(true),
() => tactical_mode.manipulate("shiplist")(false),
() => null);
this.addChild(this.info_button);
this.container.addChild(this.info_button);
battleview.layer_borders.add(this);
if (battleview.battle) {
this.setShipsFromBattle(battleview.battle);
}
this.setShipsFromBattle(battle);
}
// Clear the action icons
/**
* Clear all ship cards
*/
clearAll(): void {
this.ships.forEach((ship: ShipListItem) => {
ship.destroy();
});
this.ships = [];
this.items.forEach(ship => ship.destroy());
this.items = [];
}
// Set the ship list from a battle
setShipsFromBattle(battle: Battle): void {
/**
* Rebuild the ship list from an ongoing battle
*/
setShipsFromBattle(battle: Battle, animate = true): void {
this.clearAll();
battle.play_order.forEach((ship: Ship) => {
this.addShip(ship);
}, this);
this.updateItemsLocation();
iforeach(battle.iships(true), ship => this.addShip(ship));
this.refresh(animate);
}
// Add a ship icon
/**
* Add a ship card
*/
addShip(ship: Ship): ShipListItem {
var owned = ship.getPlayer() === this.battleview.player;
var result = new ShipListItem(this, 200, this.height / 2, ship, owned);
this.ships.push(result);
this.addChild(result);
var owned = ship.getPlayer() === this.player;
var result = new ShipListItem(this, 200, this.container.height / 2, ship, owned, this.ship_buttons);
this.items.push(result);
this.container.addChild(result);
return result;
}
// Find an item for a ship
// Returns null if not found
/**
* Find the item (card) that displays a given ship
*/
findItem(ship: Ship): ShipListItem | null {
var found: ShipListItem | null = null;
this.ships.forEach((item: ShipListItem) => {
if (item.ship === ship) {
found = item;
}
});
return found;
return first(this.items, item => item.ship == ship);
}
// Find the play position in play_order for a given ship (0 is currently playing)
findPlayPosition(ship: Ship): number {
var battle = this.battleview.battle;
var idx = battle.play_order.indexOf(ship);
var diff = idx - (battle.playing_ship_index || 0);
if (diff < 0) {
diff += battle.play_order.length;
}
return diff;
}
// Update the locations of all items
updateItemsLocation(animate: boolean = true): void {
this.ships.forEach((item: ShipListItem) => {
var position = this.findPlayPosition(item.ship);
if (position === 0) {
item.moveTo(-18, 962, animate);
/**
* Update the locations of all items
*/
refresh(animate = true): void {
this.items.forEach(item => {
if (item.ship.alive) {
let position = this.battle.getPlayOrder(item.ship);
if (position < 0) {
item.visible = false;
} else {
if (position == 0) {
item.moveTo(-18, 962, animate ? 1000 : 0);
} else {
item.moveTo(2, 942 - position * 99, animate ? 1000 : 0);
}
item.visible = true;
this.container.setChildIndex(item, position);
}
} else {
item.moveTo(2, 942 - position * 99, animate);
item.moveTo(200, item.y, animate ? 1000 : 0);
}
this.setChildIndex(item, position);
});
}
// Remove a ship from the list
markAsDead(ship: Ship): void {
var item = this.findItem(ship);
if (item) {
item.alpha = 0.5;
}
}
// Set the currently playing ship
setPlaying(ship: Ship | null): void {
if (ship) {
this.playing = this.findItem(ship);
} else {
this.playing = null;
}
this.updateItemsLocation();
}
// Set the currently hovered ship
/**
* Set the currently hovered ship
*/
setHovered(ship: Ship | null): void {
if (this.hovered) {
this.hovered.setHovered(false);

View file

@ -2,11 +2,14 @@ module TK.SpaceTac.UI {
// One item in a ship list (used in BattleView)
export class ShipListItem extends Phaser.Button {
// Reference to the view
view: BattleView
view: BaseView
// Reference to the ship game object
ship: Ship
// Callbacks to act as buttons
ship_buttons: IShipButton
// Player indicator
player_indicator: Phaser.Image
@ -20,9 +23,9 @@ module TK.SpaceTac.UI {
hover_indicator: Phaser.Image
// Create a ship button for the battle ship list
constructor(list: ShipList, x: number, y: number, ship: Ship, owned: boolean) {
super(list.battleview.game, x, y, "battle-shiplist-item-background");
this.view = list.battleview;
constructor(list: ShipList, x: number, y: number, ship: Ship, owned: boolean, ship_buttons: IShipButton) {
super(list.view.game, x, y, "battle-shiplist-item-background");
this.view = list.view;
this.ship = ship;
@ -44,9 +47,9 @@ module TK.SpaceTac.UI {
this.addChild(this.hover_indicator);
this.view.inputs.setHoverClick(this,
() => list.battleview.cursorOnShip(ship),
() => list.battleview.cursorOffShip(ship),
() => list.battleview.cursorClicked()
() => ship_buttons.cursorOnShip(ship),
() => ship_buttons.cursorOffShip(ship),
() => ship_buttons.cursorClicked()
);
}
@ -56,11 +59,9 @@ module TK.SpaceTac.UI {
}
// Move to a given location on screen
moveTo(x: number, y: number, animate: boolean) {
if (animate) {
var tween = this.game.tweens.create(this);
tween.to({ x: x, y: y });
tween.start();
moveTo(x: number, y: number, duration: number) {
if (duration && (this.x != x || this.y != y)) {
this.view.animations.addAnimation(this, {x: x, y: y}, duration, Phaser.Easing.Linear.None);
} else {
this.x = x;
this.y = y;

View file

@ -31,7 +31,7 @@ module TK.SpaceTac.UI {
filler.text(ship.getFullName(), 140, 0, { color: enemy ? "#cc0d00" : "#ffffff", size: 22, bold: true });
if (ship.alive) {
let turns = this.battleview.battle.getTurnsBefore(ship);
let turns = this.battleview.battle.getPlayOrder(ship);
filler.text((turns == 0) ? "Playing" : ((turns == 1) ? "Plays next" : `Plays in ${turns} turns`), 140, 36, { color: "#cccccc", size: 18 });
let hsp_builder = filler.styled({ color: "#eb4e4a", size: 20, center: true, vcenter: true, bold: true });

View file

@ -1,10 +1,10 @@
/// <reference path="../TestGame.ts"/>
module TK.SpaceTac.UI.Specs {
describe("FleetCreationView", function () {
testing("FleetCreationView", test => {
let testgame = setupSingleView(() => [new FleetCreationView, []]);
it("has a basic equipment shop with infinite stock", function () {
test.case("has a basic equipment shop with infinite stock", function () {
let shop = testgame.view.infinite_shop;
let itemcount = shop.getStock().length;
expect(unique(shop.getStock().map(equ => equ.code)).length).toEqual(itemcount);
@ -21,7 +21,7 @@ module TK.SpaceTac.UI.Specs {
expect(shop.getStock().length).toBe(itemcount);
})
async_it("validates the fleet creation", async function () {
test.acase("validates the fleet creation", async function () {
expect(testgame.ui.session.isFleetCreated()).toBe(false, "no fleet created");
expect(testgame.ui.session.player.fleet.ships.length).toBe(0, "empty session fleet");
expect(testgame.view.dialogs_layer.children.length).toBe(0, "no dialogs");