1
0
Fork 0

Refactored targetting system

This commit is contained in:
Michaël Lemaire 2017-09-19 17:09:06 +02:00
parent 8be89b1825
commit d3f4cffde8
36 changed files with 509 additions and 343 deletions

11
TODO.md
View file

@ -36,26 +36,26 @@ Battle
------ ------
* Add a voluntary retreat option * Add a voluntary retreat option
* Add scroll buttons when there are too many actions
* Remove dead ships from ship list and play order * Remove dead ships from ship list and play order
* Add quick animation of playing ship indicator, on ship change * Add quick animation of playing ship indicator, on ship change
* Toggle bar/text display in power section of action bar * Toggle bar/text display in power section of action bar
* Fix ship's active effect radius pushing the tooltip far from the ship * Fix ship's active effect radius pushing the tooltip far from the ship
* Display a hint when a move-fire simulation failed (cannot enter exclusion area for example)
* Display effects description instead of attribute changes * Display effects description instead of attribute changes
* Display radius and power usage hints for area effects on action icon hover + add confirmation? * 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
* Mark action icons unavailable next turn, if if will 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) * 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 * Add engine trail effect, and sound
* Fix targetting not resetting on current cursor location when using keyboard shortcuts
* Allow to skip animations, and allow no animation mode * Allow to skip animations, and allow no animation mode
* Find incentives to move from starting position (permanent drones or anomalies?) * Find incentives to move from starting position (permanent drones or anomalies?)
* Add a "loot all" button (on the character sheet or outcome dialog?) * Add a "loot all" button (on the character sheet or outcome dialog?)
* Do not focus on ship while targetting for area effects (dissociate hover and target) * Mark targetting in error when target is refused by the action (there is already an arrow for this)
* Repair drone has its activation effect sometimes displayed as permanent effect on ships in the radius * Repair drone has its activation effect sometimes displayed as permanent effect on ships in the radius
* Merge identical sticky effects * Merge identical sticky effects
* Allow to undo last moves * Allow to undo last moves
* Add a battle log display * Add a battle log display
* Allow to move targetting indicator with arrow keys * Allow to move targetting indicator with arrow keys
* Trigger targetting mode for all actions (even for damage protector or shield transfer)
* Add targetting shortcuts for "previous target", "next enemy" and "next ally" * Add targetting shortcuts for "previous target", "next enemy" and "next ally"
Ships models and equipments Ships models and equipments
@ -96,6 +96,7 @@ Common UI
Technical Technical
--------- ---------
* Run jasmine tests in random order by default, and fix problems
* Pack all images in atlases * Pack all images in atlases
* Pack sounds * Pack sounds

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

View file

@ -80,11 +80,11 @@ module TS.SpaceTac.Specs {
stats.processLog(battle.log, battle.fleets[0]); stats.processLog(battle.log, battle.fleets[0]);
expect(stats.stats).toEqual({}); expect(stats.stats).toEqual({});
battle.log.add(new ActionAppliedEvent(attacker, new BaseAction("nop", "nop", false), null, 4)); battle.log.add(new ActionAppliedEvent(attacker, new BaseAction("nop", "nop"), null, 4));
stats.processLog(battle.log, battle.fleets[0], true); stats.processLog(battle.log, battle.fleets[0], true);
expect(stats.stats).toEqual({ "Power used": [4, 0] }); expect(stats.stats).toEqual({ "Power used": [4, 0] });
battle.log.add(new ActionAppliedEvent(defender, new BaseAction("nop", "nop", false), null, 2)); battle.log.add(new ActionAppliedEvent(defender, new BaseAction("nop", "nop"), null, 2));
stats.processLog(battle.log, battle.fleets[0], true); stats.processLog(battle.log, battle.fleets[0], true);
expect(stats.stats).toEqual({ "Power used": [4, 2] }); expect(stats.stats).toEqual({ "Power used": [4, 2] });
}) })

View file

@ -43,7 +43,7 @@ module TS.SpaceTac {
effects: BaseEffect[] = [] effects: BaseEffect[] = []
// Action available when equipped // Action available when equipped
action = new BaseAction("nothing", "Do nothing", false) action: BaseAction | null = null
// Equipment wear due to usage in battles (will lower the sell price) // Equipment wear due to usage in battles (will lower the sell price)
wear = 0 wear = 0
@ -158,9 +158,11 @@ module TS.SpaceTac {
parts.push(["When equipped:"].concat(this.effects.map(effect => "• " + effect.getDescription())).join("\n")); parts.push(["When equipped:"].concat(this.effects.map(effect => "• " + effect.getDescription())).join("\n"));
} }
let action_desc = this.action.getEffectsDescription(); if (this.action) {
if (action_desc != "") { let action_desc = this.action.getEffectsDescription();
parts.push(action_desc); if (action_desc != "") {
parts.push(action_desc);
}
} }
return parts.length > 0 ? parts.join("\n\n") : "does nothing"; return parts.length > 0 ? parts.join("\n\n") : "does nothing";

View file

@ -150,8 +150,9 @@ module TS.SpaceTac.Specs {
it("does nothing if trying to move in the same spot", function () { it("does nothing if trying to move in the same spot", function () {
let [ship, simulator, action] = simpleWeaponCase(); let [ship, simulator, action] = simpleWeaponCase();
let result = simulator.simulateAction(ship.listEquipment(SlotType.Engine)[0].action, new Target(ship.arena_x, ship.arena_y, null)); let move_action = nn(ship.listEquipment(SlotType.Engine)[0].action)
expect(result.success).toBe(true); let result = simulator.simulateAction(move_action, new Target(ship.arena_x, ship.arena_y, null));
expect(result.success).toBe(false);
expect(result.need_move).toBe(false); expect(result.need_move).toBe(false);
expect(result.need_fire).toBe(false); expect(result.need_fire).toBe(false);
expect(result.parts).toEqual([]); expect(result.parts).toEqual([]);

View file

@ -155,11 +155,14 @@ module TS.SpaceTac {
} }
} }
} }
if (move_target && arenaDistance(move_target, this.ship.location) < 0.000001) {
result.need_move = false;
}
// Check move AP // Check move AP
if (result.need_move && move_target) { if (result.need_move && move_target) {
let engine = this.findBestEngine(); let engine = this.findBestEngine();
if (engine) { if (engine && engine.action) {
result.total_move_ap = engine.action.getActionPointsUsage(this.ship, move_target); result.total_move_ap = engine.action.getActionPointsUsage(this.ship, move_target);
result.can_move = ap > 0; result.can_move = ap > 0;
result.can_end_move = result.total_move_ap <= ap; result.can_end_move = result.total_move_ap <= ap;
@ -172,15 +175,17 @@ module TS.SpaceTac {
} }
// Check action AP // Check action AP
if (!(action instanceof MoveAction)) { if (action instanceof MoveAction) {
result.success = result.need_move && result.can_move;
} else {
result.need_fire = true; result.need_fire = true;
result.total_fire_ap = action.getActionPointsUsage(this.ship, target); result.total_fire_ap = action.getActionPointsUsage(this.ship, target);
result.can_fire = result.total_fire_ap <= ap; result.can_fire = result.total_fire_ap <= ap;
result.fire_location = target; result.fire_location = target;
result.parts.push({ action: action, target: target, ap: result.total_fire_ap, possible: (!result.need_move || result.can_end_move) && result.can_fire }); result.parts.push({ action: action, target: target, ap: result.total_fire_ap, possible: (!result.need_move || result.can_end_move) && result.can_fire });
result.success = true;
} }
result.success = true;
result.complete = (!result.need_move || result.can_end_move) && (!result.need_fire || result.can_fire); result.complete = (!result.need_move || result.can_end_move) && (!result.need_fire || result.can_fire);
return result; return result;

View file

@ -241,7 +241,7 @@ module TS.SpaceTac.Specs {
ship.startTurn(); ship.startTurn();
expect(action.activated).toBe(false); expect(action.activated).toBe(false);
let result = action.apply(ship, null); let result = action.apply(ship);
expect(result).toBe(true, "Could not be applied"); expect(result).toBe(true, "Could not be applied");
expect(action.activated).toBe(true); expect(action.activated).toBe(true);
@ -252,11 +252,11 @@ module TS.SpaceTac.Specs {
expect(action.activated).toBe(false); expect(action.activated).toBe(false);
expect(battle.log.events).toEqual([ expect(battle.log.events).toEqual([
new ActionAppliedEvent(ship, action, null, 0), new ActionAppliedEvent(ship, action, Target.newFromShip(ship), 0),
new ToggleEvent(ship, action, true), new ToggleEvent(ship, action, true),
new ActiveEffectsEvent(ship, [], [], [new AttributeEffect("power_capacity", 1)]), new ActiveEffectsEvent(ship, [], [], [new AttributeEffect("power_capacity", 1)]),
new ValueChangeEvent(ship, new ShipAttribute("power capacity", 1), 1), new ValueChangeEvent(ship, new ShipAttribute("power capacity", 1), 1),
new ActionAppliedEvent(ship, action, null, 0), new ActionAppliedEvent(ship, action, Target.newFromShip(ship), 0),
new ToggleEvent(ship, action, false), new ToggleEvent(ship, action, false),
new ActiveEffectsEvent(ship, [], [], []), new ActiveEffectsEvent(ship, [], [], []),
new ValueChangeEvent(ship, new ShipAttribute("power capacity", 0), -1), new ValueChangeEvent(ship, new ShipAttribute("power capacity", 0), -1),
@ -274,7 +274,7 @@ module TS.SpaceTac.Specs {
let shield = ship1.addSlot(SlotType.Shield).attach(new Equipment(SlotType.Shield)); let shield = ship1.addSlot(SlotType.Shield).attach(new Equipment(SlotType.Shield));
shield.action = new ToggleAction(shield, 0, 15, [new AttributeEffect("shield_capacity", 5)]); shield.action = new ToggleAction(shield, 0, 15, [new AttributeEffect("shield_capacity", 5)]);
battle.playing_ship = ship1; battle.playing_ship = ship1;
shield.action.apply(ship1, null); shield.action.apply(ship1);
expect(ship1.getAttribute("shield_capacity")).toBe(5); expect(ship1.getAttribute("shield_capacity")).toBe(5);
expect(ship2.getAttribute("shield_capacity")).toBe(5); expect(ship2.getAttribute("shield_capacity")).toBe(5);

View file

@ -137,7 +137,7 @@ module TS.SpaceTac {
let slots = [SlotType.Engine, SlotType.Power, SlotType.Hull, SlotType.Shield, SlotType.Weapon]; let slots = [SlotType.Engine, SlotType.Power, SlotType.Hull, SlotType.Shield, SlotType.Weapon];
slots.forEach(slot => { slots.forEach(slot => {
this.listEquipment(slot).forEach(equipment => { this.listEquipment(slot).forEach(equipment => {
if (equipment.action.code != "nothing") { if (equipment.action) {
actions.push(equipment.action) actions.push(equipment.action)
} }
}); });
@ -338,7 +338,7 @@ module TS.SpaceTac {
// Reset toggle actions state // Reset toggle actions state
this.listEquipment().forEach(equipment => { this.listEquipment().forEach(equipment => {
if (equipment.action instanceof ToggleAction && equipment.action.activated) { if (equipment.action instanceof ToggleAction && equipment.action.activated) {
equipment.action.apply(this, null); equipment.action.apply(this);
} }
}); });
} }

View file

@ -2,7 +2,7 @@ module TS.SpaceTac {
describe("BaseAction", function () { describe("BaseAction", function () {
it("check if equipment can be used with remaining AP", function () { it("check if equipment can be used with remaining AP", function () {
var equipment = new Equipment(SlotType.Hull); var equipment = new Equipment(SlotType.Hull);
var action = new BaseAction("test", "Test", false, equipment); var action = new BaseAction("test", "Test", equipment);
spyOn(action, "getActionPointsUsage").and.returnValue(3); spyOn(action, "getActionPointsUsage").and.returnValue(3);
var ship = new Ship(); var ship = new Ship();
ship.addSlot(SlotType.Hull).attach(equipment); ship.addSlot(SlotType.Hull).attach(equipment);
@ -28,7 +28,7 @@ module TS.SpaceTac {
it("check if equipment can be used with overheat", function () { it("check if equipment can be used with overheat", function () {
let equipment = new Equipment(); let equipment = new Equipment();
let action = new BaseAction("test", "Test", false, equipment); let action = new BaseAction("test", "Test", equipment);
let ship = new Ship(); let ship = new Ship();
expect(action.checkCannotBeApplied(ship)).toBe(null); expect(action.checkCannotBeApplied(ship)).toBe(null);
@ -71,11 +71,13 @@ module TS.SpaceTac {
TestTools.setShipAP(ship, 10); TestTools.setShipAP(ship, 10);
let power = ship.listEquipment(SlotType.Power)[0]; let power = ship.listEquipment(SlotType.Power)[0];
let equipment = new Equipment(SlotType.Weapon); let equipment = new Equipment(SlotType.Weapon);
let action = new BaseAction("test", "Test", false, equipment); let action = new BaseAction("test", "Test", equipment);
spyOn(action, "checkTarget").and.callFake((ship: Ship, target: Target) => target);
expect(power.wear).toBe(0); expect(power.wear).toBe(0);
expect(equipment.wear).toBe(0); expect(equipment.wear).toBe(0);
action.apply(ship, null); action.apply(ship);
expect(power.wear).toBe(1); expect(power.wear).toBe(1);
expect(equipment.wear).toBe(1); expect(equipment.wear).toBe(1);

View file

@ -1,4 +1,20 @@
module TS.SpaceTac { module TS.SpaceTac {
/**
* Targetting mode for an action.
*
* This is a hint as to what type of target is required for this action.
*/
export enum ActionTargettingMode {
// Apply immediately on the ship owning the action, without confirmation
SELF,
// Apply on the ship owning the action, with a confirmation
SELF_CONFIRM,
// Apply on one selected ship
SHIP,
// Apply on a space area
SPACE
}
/** /**
* Base class for a battle action. * Base class for a battle action.
* *
@ -11,40 +27,56 @@ module TS.SpaceTac {
// Human-readable name // Human-readable name
name: string name: string
// Boolean at true if the action needs a target
needs_target: boolean
// Equipment that triggers this action // Equipment that triggers this action
equipment: Equipment | null equipment: Equipment | null
// Create the action // Create the action
constructor(code: string, name: string, needs_target: boolean, equipment: Equipment | null = null) { constructor(code: string, name: string, equipment: Equipment | null = null) {
this.code = code; this.code = code;
this.name = name; this.name = name;
this.needs_target = needs_target;
this.equipment = equipment; this.equipment = equipment;
} }
/**
* Get the relevent cooldown for this action
*/
get cooldown(): Cooldown {
return this.equipment ? this.equipment.cooldown : new Cooldown();
}
/**
* Get the targetting mode
*/
getTargettingMode(ship: Ship): ActionTargettingMode {
if (this.getBlastRadius(ship)) {
return ActionTargettingMode.SPACE;
} else if (this.getRangeRadius(ship)) {
return ActionTargettingMode.SHIP;
} else {
return ActionTargettingMode.SELF_CONFIRM;
}
}
/**
* Get a default target for this action
*/
getDefaultTarget(ship: Ship): Target {
return Target.newFromShip(ship);
}
/** /**
* Get the number of turns this action is unavailable, because of overheating * Get the number of turns this action is unavailable, because of overheating
*/ */
getCooldownDuration(estimated = false): number { getCooldownDuration(estimated = false): number {
if (this.equipment) { let cooldown = this.cooldown;
return estimated ? this.equipment.cooldown.cooling : this.equipment.cooldown.heat; return estimated ? this.cooldown.cooling : this.cooldown.heat;
} else {
return 0;
}
} }
/** /**
* Get the number of remaining uses before overheat, infinity if there is no overheat * Get the number of remaining uses before overheat, infinity if there is no overheat
*/ */
getUsesBeforeOverheat(): number { getUsesBeforeOverheat(): number {
if (this.equipment) { return this.cooldown.getRemainingUses();
return this.equipment.cooldown.getRemainingUses();
} else {
return Infinity;
}
} }
/** /**
@ -71,7 +103,7 @@ module TS.SpaceTac {
} }
// Check cooldown // Check cooldown
if (this.equipment && !this.equipment.cooldown.canUse()) { if (!this.cooldown.canUse()) {
return "overheated"; return "overheated";
} }
@ -93,40 +125,43 @@ module TS.SpaceTac {
return 0; return 0;
} }
// Method to check if a target is applicable for this action /**
// Will call checkLocationTarget or checkShipTarget by default * Check if a target is suitable for this action
checkTarget(ship: Ship, target: Target | null): Target | null { *
* Will call checkLocationTarget or checkShipTarget by default
*/
checkTarget(ship: Ship, target: Target): Target | null {
if (this.checkCannotBeApplied(ship)) { if (this.checkCannotBeApplied(ship)) {
return null; return null;
} else if (target) { } else {
if (target.ship) { if (target.ship) {
return this.checkShipTarget(ship, target); return this.checkShipTarget(ship, target);
} else { } else {
return this.checkLocationTarget(ship, target); return this.checkLocationTarget(ship, target);
} }
} else {
return null;
} }
} }
// Method to reimplement to check if a space target is applicable // Method to reimplement to check if a space target is suitable
// Must return null if the target can't be applied, an altered target, or the original target // Must return null if the target can't be applied, an altered target, or the original target
checkLocationTarget(ship: Ship, target: Target): Target | null { protected checkLocationTarget(ship: Ship, target: Target): Target | null {
return null; return null;
} }
// Method to reimplement to check if a ship target is applicable // Method to reimplement to check if a ship target is suitable
// Must return null if the target can't be applied, an altered target, or the original target // Must return null if the target can't be applied, an altered target, or the original target
checkShipTarget(ship: Ship, target: Target): Target | null { protected checkShipTarget(ship: Ship, target: Target): Target | null {
return null; return null;
} }
// Apply an action, returning true if it was successful /**
apply(ship: Ship, target: Target | null): boolean { * Apply an action, returning true if it was successful
*/
apply(ship: Ship, target = this.getDefaultTarget(ship)): boolean {
let reject = this.checkCannotBeApplied(ship); let reject = this.checkCannotBeApplied(ship);
if (reject == null) { if (reject == null) {
let checked_target = this.checkTarget(ship, target); let checked_target = this.checkTarget(ship, target);
if (!checked_target && this.needs_target) { if (!checked_target) {
console.warn("Action rejected - invalid target", ship, this, target); console.warn("Action rejected - invalid target", ship, this, target);
return false; return false;
} }
@ -140,10 +175,10 @@ module TS.SpaceTac {
if (this.equipment) { if (this.equipment) {
this.equipment.addWear(1); this.equipment.addWear(1);
ship.listEquipment(SlotType.Power).forEach(equipment => equipment.addWear(1)); ship.listEquipment(SlotType.Power).forEach(equipment => equipment.addWear(1));
this.equipment.cooldown.use();
} }
this.cooldown.use();
let battle = ship.getBattle(); let battle = ship.getBattle();
if (battle) { if (battle) {
battle.log.add(new ActionAppliedEvent(ship, this, checked_target, cost)); battle.log.add(new ActionAppliedEvent(ship, this, checked_target, cost));
@ -157,8 +192,10 @@ module TS.SpaceTac {
} }
} }
// Method to reimplement to apply a action /**
protected customApply(ship: Ship, target: Target | null) { * Method to reimplement to apply the action
*/
protected customApply(ship: Ship, target: Target): void {
} }
/** /**

View file

@ -9,7 +9,6 @@ module TS.SpaceTac {
expect(action.code).toEqual("deploy-testdrone"); expect(action.code).toEqual("deploy-testdrone");
expect(action.name).toEqual("Deploy"); expect(action.name).toEqual("Deploy");
expect(action.equipment).toBe(equipment); expect(action.equipment).toBe(equipment);
expect(action.needs_target).toBe(true);
}); });
it("allows to deploy in range", function () { it("allows to deploy in range", function () {

View file

@ -24,7 +24,7 @@ module TS.SpaceTac {
equipment: Equipment; equipment: Equipment;
constructor(equipment: Equipment, power = 1, deploy_distance = 0, lifetime = 0, effect_radius = 0, effects: BaseEffect[] = []) { constructor(equipment: Equipment, power = 1, deploy_distance = 0, lifetime = 0, effect_radius = 0, effects: BaseEffect[] = []) {
super("deploy-" + equipment.code, "Deploy", true, equipment); super("deploy-" + equipment.code, "Deploy", equipment);
this.power = power; this.power = power;
this.deploy_distance = deploy_distance; this.deploy_distance = deploy_distance;

View file

@ -3,25 +3,26 @@ module TS.SpaceTac.Specs {
it("can't be applied to non-playing ship", () => { it("can't be applied to non-playing ship", () => {
spyOn(console, "warn").and.stub(); spyOn(console, "warn").and.stub();
var battle = Battle.newQuickRandom(); let battle = Battle.newQuickRandom();
var action = new EndTurnAction(); let action = new EndTurnAction();
expect(action.checkCannotBeApplied(battle.play_order[0])).toBe(null); expect(action.checkCannotBeApplied(battle.play_order[0])).toBe(null);
expect(action.checkCannotBeApplied(battle.play_order[1])).toBe("ship not playing"); expect(action.checkCannotBeApplied(battle.play_order[1])).toBe("ship not playing");
var result = action.apply(battle.play_order[1], null); let ship = battle.play_order[1];
let result = action.apply(battle.play_order[1]);
expect(result).toBe(false); expect(result).toBe(false);
expect(console.warn).toHaveBeenCalledWith("Action rejected - ship not playing", battle.play_order[1], action, null); expect(console.warn).toHaveBeenCalledWith("Action rejected - ship not playing", ship, action, Target.newFromShip(ship));
}); });
it("ends turn when applied", () => { it("ends turn when applied", () => {
var battle = Battle.newQuickRandom(); let battle = Battle.newQuickRandom();
var action = new EndTurnAction(); let action = new EndTurnAction();
expect(battle.playing_ship_index).toBe(0); expect(battle.playing_ship_index).toBe(0);
var result = action.apply(battle.play_order[0], null); let result = action.apply(battle.play_order[0], Target.newFromShip(battle.play_order[0]));
expect(result).toBe(true); expect(result).toBe(true);
expect(battle.playing_ship_index).toBe(1); expect(battle.playing_ship_index).toBe(1);
}); });

View file

@ -2,18 +2,28 @@ module TS.SpaceTac {
// Action to end the ship's turn // Action to end the ship's turn
export class EndTurnAction extends BaseAction { export class EndTurnAction extends BaseAction {
constructor() { constructor() {
super("endturn", "End ship's turn", false); super("endturn", "End ship's turn");
} }
protected customApply(ship: Ship, target: Target) { protected customApply(ship: Ship, target: Target) {
ship.endTurn(); if (target.ship == ship) {
ship.endTurn();
let battle = ship.getBattle(); let battle = ship.getBattle();
if (battle) { if (battle) {
battle.advanceToNextShip(); battle.advanceToNextShip();
}
} }
} }
protected checkShipTarget(ship: Ship, target: Target): Target | null {
return target.ship == ship ? target : null;
}
getTargettingMode(ship: Ship): ActionTargettingMode {
return ship.getValue("power") ? ActionTargettingMode.SELF_CONFIRM : ActionTargettingMode.SELF;
}
getEffectsDescription(): string { getEffectsDescription(): string {
return "End the current ship's turn.\nWill also generate power and cool down equipments."; return "End the current ship's turn.\nWill also generate power and cool down equipments.";
} }

View file

@ -9,10 +9,6 @@ module TS.SpaceTac {
expect(action.code).toEqual("fire-testweapon"); expect(action.code).toEqual("fire-testweapon");
expect(action.name).toEqual("Fire"); expect(action.name).toEqual("Fire");
expect(action.equipment).toBe(equipment); expect(action.equipment).toBe(equipment);
expect(action.needs_target).toBe(true);
action = new FireWeaponAction(equipment, 4, 0, 10);
expect(action.needs_target).toBe(false);
}); });
it("applies effects to alive ships in blast radius", function () { it("applies effects to alive ships in blast radius", function () {
@ -49,35 +45,37 @@ module TS.SpaceTac {
let ship2 = new Ship(); let ship2 = new Ship();
ship2.setArenaPosition(150, 10); ship2.setArenaPosition(150, 10);
let weapon = TestTools.addWeapon(ship1, 1, 0, 100, 30); let weapon = TestTools.addWeapon(ship1, 1, 0, 100, 30);
let action = nn(weapon.action);
let target = weapon.action.checkTarget(ship1, new Target(150, 10)); let target = action.checkTarget(ship1, new Target(150, 10));
expect(target).toEqual(new Target(150, 10)); expect(target).toEqual(new Target(150, 10));
target = weapon.action.checkTarget(ship1, Target.newFromShip(ship2)); target = action.checkTarget(ship1, Target.newFromShip(ship2));
expect(target).toEqual(new Target(150, 10)); expect(target).toEqual(new Target(150, 10));
ship1.setArenaPosition(30, 10); ship1.setArenaPosition(30, 10);
target = weapon.action.checkTarget(ship1, Target.newFromShip(ship2)); target = action.checkTarget(ship1, Target.newFromShip(ship2));
expect(target).toEqual(new Target(130, 10)); expect(target).toEqual(new Target(130, 10));
ship1.setArenaPosition(0, 10); ship1.setArenaPosition(0, 10);
target = weapon.action.checkTarget(ship1, Target.newFromShip(ship2)); target = action.checkTarget(ship1, Target.newFromShip(ship2));
expect(target).toEqual(new Target(100, 10)); expect(target).toEqual(new Target(100, 10));
}); });
it("rotates toward the target", function () { it("rotates toward the target", function () {
let ship = new Ship(); let ship = new Ship();
let weapon = TestTools.addWeapon(ship, 1, 0, 100, 30); let weapon = TestTools.addWeapon(ship, 1, 0, 100, 30);
let action = nn(weapon.action);
spyOn(action, "checkTarget").and.callFake((ship: Ship, target: Target) => target);
expect(ship.arena_angle).toEqual(0); expect(ship.arena_angle).toEqual(0);
let result = weapon.action.apply(ship, Target.newFromLocation(10, 20)); let result = action.apply(ship, Target.newFromLocation(10, 20));
expect(result).toBe(true); expect(result).toBe(true);
expect(ship.arena_angle).toBeCloseTo(1.107, 0.001); expect(ship.arena_angle).toBeCloseTo(1.107, 0.001);
weapon.action.needs_target = false; result = action.apply(ship, Target.newFromShip(ship));
result = weapon.action.apply(ship, null);
expect(result).toBe(true); expect(result).toBe(true);
expect(ship.arena_angle).toBeCloseTo(1.107, 0.001); expect(ship.arena_angle).toBeCloseTo(1.107, 0.001);
}); });

View file

@ -6,22 +6,22 @@ module TS.SpaceTac {
*/ */
export class FireWeaponAction extends BaseAction { export class FireWeaponAction extends BaseAction {
// Power consumption // Power consumption
power: number; power: number
// Maximal range of the weapon // Maximal range of the weapon
range: number range: number
// Blast radius // Blast radius
blast: number; blast: number
// Effects applied on hit // Effects applied on target
effects: BaseEffect[]; effects: BaseEffect[]
// Equipment cannot be null // Equipment cannot be null
equipment: Equipment; equipment: Equipment
constructor(equipment: Equipment, power = 1, range = 0, blast = 0, effects: BaseEffect[] = [], name = "Fire") { constructor(equipment: Equipment, power = 1, range = 0, blast = 0, effects: BaseEffect[] = [], name = "Fire") {
super("fire-" + equipment.code, name, range > 0, equipment); super("fire-" + equipment.code, name, equipment);
this.power = power; this.power = power;
this.range = range; this.range = range;
@ -29,6 +29,23 @@ module TS.SpaceTac {
this.blast = blast; this.blast = blast;
} }
getDefaultTarget(ship: Ship): Target {
if (this.range == 0) {
return Target.newFromShip(ship);
} else {
let battle = ship.getBattle();
if (battle) {
let harmful = any(this.effects, effect => !effect.isBeneficial());
let player = ship.getPlayer();
let ships = imaterialize(harmful ? battle.ienemies(player, true) : ifilter(battle.iallies(player, true), iship => iship != ship));
let nearest = minBy(ships, iship => arenaDistance(ship.location, iship.location));
return Target.newFromShip(nearest);
} else {
return Target.newFromShip(ship);
}
}
}
getActionPointsUsage(ship: Ship, target: Target | null): number { getActionPointsUsage(ship: Ship, target: Target | null): number {
return this.power; return this.power;
} }
@ -51,8 +68,8 @@ module TS.SpaceTac {
} }
checkShipTarget(ship: Ship, target: Target): Target | null { checkShipTarget(ship: Ship, target: Target): Target | null {
if (target.ship && ship.getPlayer() === target.ship.getPlayer()) { if (this.range > 0 && ship == target.ship) {
// No friendly fire // No self fire
return null; return null;
} else { } else {
// Check if target is in range // Check if target is in range
@ -80,13 +97,10 @@ module TS.SpaceTac {
return result; return result;
} }
protected customApply(ship: Ship, target: Target | null) { protected customApply(ship: Ship, target: Target) {
if (!target) { if (arenaDistance(ship.location, target) > 0.000001) {
// Self-target
target = Target.newFromShip(ship);
} else {
// Face the target // Face the target
ship.rotate(Target.newFromShip(ship).getAngleTo(target), first(ship.listEquipment(SlotType.Engine), () => true)); ship.rotate(arenaAngle(ship.location, target), first(ship.listEquipment(SlotType.Engine), () => true));
} }
// Fire event // Fire event

View file

@ -14,13 +14,21 @@ module TS.SpaceTac {
maneuvrability_factor: number maneuvrability_factor: number
constructor(equipment: Equipment, distance_per_power = 0, safety_distance = 120, maneuvrability_factor = 0) { constructor(equipment: Equipment, distance_per_power = 0, safety_distance = 120, maneuvrability_factor = 0) {
super("move", "Move", true, equipment); super("move", "Move", equipment);
this.distance_per_power = distance_per_power; this.distance_per_power = distance_per_power;
this.safety_distance = safety_distance; this.safety_distance = safety_distance;
this.maneuvrability_factor = maneuvrability_factor; this.maneuvrability_factor = maneuvrability_factor;
} }
getTargettingMode(ship: Ship): ActionTargettingMode {
return ActionTargettingMode.SPACE;
}
getDefaultTarget(ship: Ship): Target {
return Target.newFromLocation(ship.arena_x + Math.cos(ship.arena_angle) * 100, ship.arena_y + Math.sin(ship.arena_angle) * 100);
}
checkCannotBeApplied(ship: Ship, remaining_ap: number | null = null): string | null { checkCannotBeApplied(ship: Ship, remaining_ap: number | null = null): string | null {
let base = super.checkCannotBeApplied(ship, Infinity); let base = super.checkCannotBeApplied(ship, Infinity);
if (base) { if (base) {
@ -38,13 +46,13 @@ module TS.SpaceTac {
} }
} }
getActionPointsUsage(ship: Ship, target: Target): number { getActionPointsUsage(ship: Ship, target: Target | null): number {
if (target === null) { if (target) {
let distance = Target.newFromShip(ship).getDistanceTo(target);
return Math.ceil(distance / this.getDistanceByActionPoint(ship));
} else {
return 0; return 0;
} }
var distance = Target.newFromShip(ship).getDistanceTo(target);
return Math.ceil(distance / this.getDistanceByActionPoint(ship));
} }
getRangeRadius(ship: Ship): number { getRangeRadius(ship: Ship): number {

View file

@ -21,7 +21,7 @@ module TS.SpaceTac {
activated = false activated = false
constructor(equipment: Equipment, power = 1, radius = 0, effects: BaseEffect[] = [], name = "(De)activate") { constructor(equipment: Equipment, power = 1, radius = 0, effects: BaseEffect[] = [], name = "(De)activate") {
super("toggle-" + equipment.code, name, false, equipment); super("toggle-" + equipment.code, name, equipment);
this.power = power; this.power = power;
this.radius = radius; this.radius = radius;
@ -40,6 +40,10 @@ module TS.SpaceTac {
return this.radius; return this.radius;
} }
checkShipTarget(ship: Ship, target: Target): Target | null {
return (ship == target.ship) ? target : null;
}
/** /**
* Get the list of ships in range to be affected * Get the list of ships in range to be affected
*/ */

View file

@ -63,7 +63,7 @@ module TS.SpaceTac {
} }
// End the ship turn // End the ship turn
this.applyAction(new EndTurnAction(), null); this.applyAction(new EndTurnAction(), Target.newFromShip(ship));
} }
/** /**
@ -71,7 +71,7 @@ module TS.SpaceTac {
* *
* This should be the only real interaction point with battle state * This should be the only real interaction point with battle state
*/ */
private applyAction(action: BaseAction, target: Target | null): boolean { private applyAction(action: BaseAction, target: Target): boolean {
return action.apply(this.ship, target); return action.apply(this.ship, target);
} }

View file

@ -12,7 +12,7 @@ module TS.SpaceTac.Specs {
TestTools.setShipHP(ship2, 30, 30); TestTools.setShipHP(ship2, 30, 30);
ship3.setArenaPosition(0, 15); ship3.setArenaPosition(0, 15);
TestTools.setShipHP(ship3, 30, 30); TestTools.setShipHP(ship3, 30, 30);
let maneuver = new Maneuver(ship1, weapon.action, Target.newFromLocation(0, 0)); let maneuver = new Maneuver(ship1, nn(weapon.action), Target.newFromLocation(0, 0));
expect(maneuver.effects).toEqual([ expect(maneuver.effects).toEqual([
[ship1, new DamageEffect(50)], [ship1, new DamageEffect(50)],
[ship2, new DamageEffect(50)] [ship2, new DamageEffect(50)]
@ -43,6 +43,7 @@ module TS.SpaceTac.Specs {
let battle = new Battle(); let battle = new Battle();
let ship = battle.fleets[0].addShip(); let ship = battle.fleets[0].addShip();
let engine = TestTools.addEngine(ship, 500); let engine = TestTools.addEngine(ship, 500);
let move = nn(engine.action);
TestTools.setShipAP(ship, 10); TestTools.setShipAP(ship, 10);
let drone = new Drone(ship); let drone = new Drone(ship);
drone.effects = [new AttributeEffect("maneuvrability", 1)]; drone.effects = [new AttributeEffect("maneuvrability", 1)];
@ -51,11 +52,11 @@ module TS.SpaceTac.Specs {
drone.radius = 50; drone.radius = 50;
battle.addDrone(drone); battle.addDrone(drone);
let maneuver = new Maneuver(ship, engine.action, Target.newFromLocation(40, 30)); let maneuver = new Maneuver(ship, move, Target.newFromLocation(40, 30));
expect(maneuver.getFinalLocation()).toEqual(jasmine.objectContaining({ x: 40, y: 30 })); expect(maneuver.getFinalLocation()).toEqual(jasmine.objectContaining({ x: 40, y: 30 }));
expect(maneuver.effects).toEqual([]); expect(maneuver.effects).toEqual([]);
maneuver = new Maneuver(ship, engine.action, Target.newFromLocation(100, 30)); maneuver = new Maneuver(ship, move, Target.newFromLocation(100, 30));
expect(maneuver.getFinalLocation()).toEqual(jasmine.objectContaining({ x: 100, y: 30 })); expect(maneuver.getFinalLocation()).toEqual(jasmine.objectContaining({ x: 100, y: 30 }));
expect(maneuver.effects).toEqual([[ship, new AttributeEffect("maneuvrability", 1)]]); expect(maneuver.effects).toEqual([[ship, new AttributeEffect("maneuvrability", 1)]]);
}); });

View file

@ -7,7 +7,7 @@ module TS.SpaceTac.Specs {
constructor(score: number) { constructor(score: number) {
let battle = new Battle(); let battle = new Battle();
let ship = battle.fleets[0].addShip(); let ship = battle.fleets[0].addShip();
super(ship, new BaseAction("nothing", "Do nothing", true), new Target(0, 0)); super(ship, new BaseAction("nothing", "Do nothing"), new Target(0, 0));
this.score = score; this.score = score;
} }
} }

View file

@ -17,10 +17,10 @@ module TS.SpaceTac.Specs {
let weapon2 = TestTools.addWeapon(ship0a, 15); let weapon2 = TestTools.addWeapon(ship0a, 15);
result = imaterialize(TacticalAIHelpers.produceDirectShots(ship0a, battle)); result = imaterialize(TacticalAIHelpers.produceDirectShots(ship0a, battle));
expect(result.length).toBe(4); expect(result.length).toBe(4);
expect(result).toContain(new Maneuver(ship0a, weapon1.action, Target.newFromShip(ship1a))); expect(result).toContain(new Maneuver(ship0a, nn(weapon1.action), Target.newFromShip(ship1a)));
expect(result).toContain(new Maneuver(ship0a, weapon1.action, Target.newFromShip(ship1b))); expect(result).toContain(new Maneuver(ship0a, nn(weapon1.action), Target.newFromShip(ship1b)));
expect(result).toContain(new Maneuver(ship0a, weapon2.action, Target.newFromShip(ship1a))); expect(result).toContain(new Maneuver(ship0a, nn(weapon2.action), Target.newFromShip(ship1a)));
expect(result).toContain(new Maneuver(ship0a, weapon2.action, Target.newFromShip(ship1b))); expect(result).toContain(new Maneuver(ship0a, nn(weapon2.action), Target.newFromShip(ship1b)));
}); });
it("produces random moves inside a grid", function () { it("produces random moves inside a grid", function () {
@ -39,10 +39,10 @@ module TS.SpaceTac.Specs {
result = imaterialize(TacticalAIHelpers.produceRandomMoves(ship, battle, 2, 1, new SkewedRandomGenerator([0.5], true))); result = imaterialize(TacticalAIHelpers.produceRandomMoves(ship, battle, 2, 1, new SkewedRandomGenerator([0.5], true)));
expect(result).toEqual([ expect(result).toEqual([
new Maneuver(ship, engine.action, Target.newFromLocation(25, 25)), new Maneuver(ship, nn(engine.action), Target.newFromLocation(25, 25)),
new Maneuver(ship, engine.action, Target.newFromLocation(75, 25)), new Maneuver(ship, nn(engine.action), Target.newFromLocation(75, 25)),
new Maneuver(ship, engine.action, Target.newFromLocation(25, 75)), new Maneuver(ship, nn(engine.action), Target.newFromLocation(25, 75)),
new Maneuver(ship, engine.action, Target.newFromLocation(75, 75)), new Maneuver(ship, nn(engine.action), Target.newFromLocation(75, 75)),
]); ]);
}); });
@ -68,8 +68,8 @@ module TS.SpaceTac.Specs {
result = imaterialize(TacticalAIHelpers.produceInterestingBlastShots(ship, battle)); result = imaterialize(TacticalAIHelpers.produceInterestingBlastShots(ship, battle));
expect(result).toEqual([ expect(result).toEqual([
new Maneuver(ship, weapon.action, Target.newFromLocation(600, 0)), new Maneuver(ship, nn(weapon.action), Target.newFromLocation(600, 0)),
new Maneuver(ship, weapon.action, Target.newFromLocation(600, 0)), new Maneuver(ship, nn(weapon.action), Target.newFromLocation(600, 0)),
]); ]);
let enemy3 = battle.fleets[1].addShip(); let enemy3 = battle.fleets[1].addShip();
@ -77,8 +77,8 @@ module TS.SpaceTac.Specs {
result = imaterialize(TacticalAIHelpers.produceInterestingBlastShots(ship, battle)); result = imaterialize(TacticalAIHelpers.produceInterestingBlastShots(ship, battle));
expect(result).toEqual([ expect(result).toEqual([
new Maneuver(ship, weapon.action, Target.newFromLocation(600, 0)), new Maneuver(ship, nn(weapon.action), Target.newFromLocation(600, 0)),
new Maneuver(ship, weapon.action, Target.newFromLocation(600, 0)), new Maneuver(ship, nn(weapon.action), Target.newFromLocation(600, 0)),
]); ]);
}); });
@ -86,29 +86,30 @@ module TS.SpaceTac.Specs {
let battle = new Battle(); let battle = new Battle();
let ship = battle.fleets[0].addShip(); let ship = battle.fleets[0].addShip();
let weapon = TestTools.addWeapon(ship, 50, 5, 100); let weapon = TestTools.addWeapon(ship, 50, 5, 100);
let action = nn(weapon.action);
let engine = TestTools.addEngine(ship, 25); let engine = TestTools.addEngine(ship, 25);
let maneuver = new Maneuver(ship, new BaseAction("fake", "Nothing", false), new Target(0, 0), 0); let maneuver = new Maneuver(ship, new BaseAction("fake", "Nothing"), new Target(0, 0), 0);
expect(TacticalAIHelpers.evaluateTurnCost(ship, battle, maneuver)).toBe(-1); expect(TacticalAIHelpers.evaluateTurnCost(ship, battle, maneuver)).toBe(-1);
maneuver = new Maneuver(ship, weapon.action, Target.newFromLocation(100, 0), 0); maneuver = new Maneuver(ship, action, Target.newFromLocation(100, 0), 0);
expect(TacticalAIHelpers.evaluateTurnCost(ship, battle, maneuver)).toBe(-Infinity); expect(TacticalAIHelpers.evaluateTurnCost(ship, battle, maneuver)).toBe(-Infinity);
TestTools.setShipAP(ship, 4); TestTools.setShipAP(ship, 4);
maneuver = new Maneuver(ship, weapon.action, Target.newFromLocation(100, 0), 0); maneuver = new Maneuver(ship, action, Target.newFromLocation(100, 0), 0);
expect(TacticalAIHelpers.evaluateTurnCost(ship, battle, maneuver)).toBe(-Infinity); expect(TacticalAIHelpers.evaluateTurnCost(ship, battle, maneuver)).toBe(-Infinity);
TestTools.setShipAP(ship, 10); TestTools.setShipAP(ship, 10);
maneuver = new Maneuver(ship, weapon.action, Target.newFromLocation(100, 0), 0); maneuver = new Maneuver(ship, action, Target.newFromLocation(100, 0), 0);
expect(TacticalAIHelpers.evaluateTurnCost(ship, battle, maneuver)).toBe(0.5); // 5 power remaining on 10 expect(TacticalAIHelpers.evaluateTurnCost(ship, battle, maneuver)).toBe(0.5); // 5 power remaining on 10
maneuver = new Maneuver(ship, weapon.action, Target.newFromLocation(110, 0), 0); maneuver = new Maneuver(ship, action, Target.newFromLocation(110, 0), 0);
expect(TacticalAIHelpers.evaluateTurnCost(ship, battle, maneuver)).toBe(0.4); // 4 power remaining on 10 expect(TacticalAIHelpers.evaluateTurnCost(ship, battle, maneuver)).toBe(0.4); // 4 power remaining on 10
maneuver = new Maneuver(ship, weapon.action, Target.newFromLocation(140, 0), 0); maneuver = new Maneuver(ship, action, Target.newFromLocation(140, 0), 0);
expect(TacticalAIHelpers.evaluateTurnCost(ship, battle, maneuver)).toBe(0.3); // 3 power remaining on 10 expect(TacticalAIHelpers.evaluateTurnCost(ship, battle, maneuver)).toBe(0.3); // 3 power remaining on 10
maneuver = new Maneuver(ship, weapon.action, Target.newFromLocation(310, 0), 0); maneuver = new Maneuver(ship, action, Target.newFromLocation(310, 0), 0);
expect(TacticalAIHelpers.evaluateTurnCost(ship, battle, maneuver)).toBe(-1); // can't do in one turn expect(TacticalAIHelpers.evaluateTurnCost(ship, battle, maneuver)).toBe(-1); // can't do in one turn
}); });
@ -119,18 +120,18 @@ module TS.SpaceTac.Specs {
let engine = TestTools.addEngine(ship, 50); let engine = TestTools.addEngine(ship, 50);
let weapon = TestTools.addWeapon(ship, 10, 2, 100, 10); let weapon = TestTools.addWeapon(ship, 10, 2, 100, 10);
let maneuver = new Maneuver(ship, weapon.action, Target.newFromLocation(0, 0)); let maneuver = new Maneuver(ship, nn(weapon.action), Target.newFromLocation(0, 0));
expect(TacticalAIHelpers.evaluateIdling(ship, battle, maneuver)).toEqual(-0.3); expect(TacticalAIHelpers.evaluateIdling(ship, battle, maneuver)).toEqual(-0.3);
maneuver = new Maneuver(ship, engine.action, Target.newFromLocation(0, 0)); maneuver = new Maneuver(ship, nn(engine.action), Target.newFromLocation(0, 0));
expect(TacticalAIHelpers.evaluateIdling(ship, battle, maneuver)).toEqual(-0.5); expect(TacticalAIHelpers.evaluateIdling(ship, battle, maneuver)).toEqual(-0.5);
ship.setValue("power", 2); ship.setValue("power", 2);
maneuver = new Maneuver(ship, weapon.action, Target.newFromLocation(0, 0)); maneuver = new Maneuver(ship, nn(weapon.action), Target.newFromLocation(0, 0));
expect(TacticalAIHelpers.evaluateIdling(ship, battle, maneuver)).toEqual(0.5); expect(TacticalAIHelpers.evaluateIdling(ship, battle, maneuver)).toEqual(0.5);
maneuver = new Maneuver(ship, engine.action, Target.newFromLocation(0, 0)); maneuver = new Maneuver(ship, nn(engine.action), Target.newFromLocation(0, 0));
expect(TacticalAIHelpers.evaluateIdling(ship, battle, maneuver)).toEqual(0); expect(TacticalAIHelpers.evaluateIdling(ship, battle, maneuver)).toEqual(0);
}); });
@ -138,6 +139,7 @@ module TS.SpaceTac.Specs {
let battle = new Battle(); let battle = new Battle();
let ship = battle.fleets[0].addShip(); let ship = battle.fleets[0].addShip();
let weapon = TestTools.addWeapon(ship, 50, 5, 500, 100); let weapon = TestTools.addWeapon(ship, 50, 5, 500, 100);
let action = nn(weapon.action);
let enemy1 = battle.fleets[1].addShip(); let enemy1 = battle.fleets[1].addShip();
enemy1.setArenaPosition(250, 0); enemy1.setArenaPosition(250, 0);
@ -147,15 +149,15 @@ module TS.SpaceTac.Specs {
TestTools.setShipHP(enemy2, 25, 0); TestTools.setShipHP(enemy2, 25, 0);
// no enemies hurt // no enemies hurt
let maneuver = new Maneuver(ship, weapon.action, Target.newFromLocation(100, 0)); let maneuver = new Maneuver(ship, action, Target.newFromLocation(100, 0));
expect(TacticalAIHelpers.evaluateEnemyHealth(ship, battle, maneuver)).toBeCloseTo(0, 8); expect(TacticalAIHelpers.evaluateEnemyHealth(ship, battle, maneuver)).toBeCloseTo(0, 8);
// one enemy loses half-life // one enemy loses half-life
maneuver = new Maneuver(ship, weapon.action, Target.newFromLocation(180, 0)); maneuver = new Maneuver(ship, action, Target.newFromLocation(180, 0));
expect(TacticalAIHelpers.evaluateEnemyHealth(ship, battle, maneuver)).toBeCloseTo(0.1666666666, 8); expect(TacticalAIHelpers.evaluateEnemyHealth(ship, battle, maneuver)).toBeCloseTo(0.1666666666, 8);
// one enemy loses half-life, the other one is dead // one enemy loses half-life, the other one is dead
maneuver = new Maneuver(ship, weapon.action, Target.newFromLocation(280, 0)); maneuver = new Maneuver(ship, action, Target.newFromLocation(280, 0));
expect(TacticalAIHelpers.evaluateEnemyHealth(ship, battle, maneuver)).toBeCloseTo(0.6666666666, 8); expect(TacticalAIHelpers.evaluateEnemyHealth(ship, battle, maneuver)).toBeCloseTo(0.6666666666, 8);
}); });
@ -166,7 +168,7 @@ module TS.SpaceTac.Specs {
TestTools.setShipAP(ship, 10); TestTools.setShipAP(ship, 10);
let weapon = TestTools.addWeapon(ship, 100, 1, 100, 10); let weapon = TestTools.addWeapon(ship, 100, 1, 100, 10);
let maneuver = new Maneuver(ship, weapon.action, Target.newFromLocation(200, 0), 0.5); let maneuver = new Maneuver(ship, nn(weapon.action), Target.newFromLocation(200, 0), 0.5);
expect(maneuver.simulation.move_location.x).toBeCloseTo(100.5, 1); expect(maneuver.simulation.move_location.x).toBeCloseTo(100.5, 1);
expect(maneuver.simulation.move_location.y).toBe(0); expect(maneuver.simulation.move_location.y).toBe(0);
expect(TacticalAIHelpers.evaluateClustering(ship, battle, maneuver)).toEqual(0); expect(TacticalAIHelpers.evaluateClustering(ship, battle, maneuver)).toEqual(0);
@ -188,21 +190,22 @@ module TS.SpaceTac.Specs {
let battle = new Battle(undefined, undefined, 200, 100); let battle = new Battle(undefined, undefined, 200, 100);
let ship = battle.fleets[0].addShip(); let ship = battle.fleets[0].addShip();
let weapon = TestTools.addWeapon(ship, 1, 1, 400); let weapon = TestTools.addWeapon(ship, 1, 1, 400);
let action = nn(weapon.action);
ship.setArenaPosition(0, 0); ship.setArenaPosition(0, 0);
let maneuver = new Maneuver(ship, weapon.action, new Target(0, 0), 0); let maneuver = new Maneuver(ship, action, new Target(0, 0), 0);
expect(TacticalAIHelpers.evaluatePosition(ship, battle, maneuver)).toEqual(-1); expect(TacticalAIHelpers.evaluatePosition(ship, battle, maneuver)).toEqual(-1);
ship.setArenaPosition(100, 0); ship.setArenaPosition(100, 0);
maneuver = new Maneuver(ship, weapon.action, new Target(0, 0), 0); maneuver = new Maneuver(ship, action, new Target(0, 0), 0);
expect(TacticalAIHelpers.evaluatePosition(ship, battle, maneuver)).toEqual(-1); expect(TacticalAIHelpers.evaluatePosition(ship, battle, maneuver)).toEqual(-1);
ship.setArenaPosition(100, 10); ship.setArenaPosition(100, 10);
maneuver = new Maneuver(ship, weapon.action, new Target(0, 0), 0); maneuver = new Maneuver(ship, action, new Target(0, 0), 0);
expect(TacticalAIHelpers.evaluatePosition(ship, battle, maneuver)).toEqual(-0.6); expect(TacticalAIHelpers.evaluatePosition(ship, battle, maneuver)).toEqual(-0.6);
ship.setArenaPosition(100, 50); ship.setArenaPosition(100, 50);
maneuver = new Maneuver(ship, weapon.action, new Target(0, 0), 0); maneuver = new Maneuver(ship, action, new Target(0, 0), 0);
expect(TacticalAIHelpers.evaluatePosition(ship, battle, maneuver)).toEqual(1); expect(TacticalAIHelpers.evaluatePosition(ship, battle, maneuver)).toEqual(1);
}); });
@ -211,7 +214,7 @@ module TS.SpaceTac.Specs {
let ship = battle.fleets[0].addShip(); let ship = battle.fleets[0].addShip();
let weapon = TestTools.addWeapon(ship, 1, 1, 400); let weapon = TestTools.addWeapon(ship, 1, 1, 400);
let maneuver = new Maneuver(ship, weapon.action, new Target(0, 0)); let maneuver = new Maneuver(ship, nn(weapon.action), new Target(0, 0));
expect(TacticalAIHelpers.evaluateOverheat(ship, battle, maneuver)).toEqual(0); expect(TacticalAIHelpers.evaluateOverheat(ship, battle, maneuver)).toEqual(0);
weapon.cooldown.configure(1, 1); weapon.cooldown.configure(1, 1);

View file

@ -33,6 +33,7 @@ module TS.SpaceTac.Equipments {
let ship1 = battle.fleets[0].addShip(); let ship1 = battle.fleets[0].addShip();
ship1.upgradeSkill("skill_time", 3); ship1.upgradeSkill("skill_time", 3);
let protector = ship1.addSlot(SlotType.Weapon).attach(new DamageProtector().generate(1)); let protector = ship1.addSlot(SlotType.Weapon).attach(new DamageProtector().generate(1));
let action = nn(protector.action);
TestTools.setShipAP(ship1, 10); TestTools.setShipAP(ship1, 10);
let ship2 = battle.fleets[0].addShip(); let ship2 = battle.fleets[0].addShip();
let ship3 = battle.fleets[0].addShip(); let ship3 = battle.fleets[0].addShip();
@ -41,10 +42,7 @@ module TS.SpaceTac.Equipments {
ship3.setArenaPosition(800, 0); ship3.setArenaPosition(800, 0);
battle.playing_ship = ship1; battle.playing_ship = ship1;
ship1.playing = true; ship1.playing = true;
expect(ship1.getAvailableActions()).toEqual([ expect(ship1.getAvailableActions()).toEqual([action, new EndTurnAction()]);
protector.action,
new EndTurnAction()
]);
TestTools.setShipHP(ship1, 100, 0); TestTools.setShipHP(ship1, 100, 0);
TestTools.setShipHP(ship2, 100, 0); TestTools.setShipHP(ship2, 100, 0);
@ -57,7 +55,7 @@ module TS.SpaceTac.Equipments {
expect(ship2.getValue("hull")).toEqual(90); expect(ship2.getValue("hull")).toEqual(90);
expect(ship3.getValue("hull")).toEqual(90); expect(ship3.getValue("hull")).toEqual(90);
let result = protector.action.apply(ship1, null); let result = action.apply(ship1);
expect(result).toBe(true); expect(result).toBe(true);
expect((<ToggleAction>protector.action).activated).toBe(true); expect((<ToggleAction>protector.action).activated).toBe(true);
@ -68,7 +66,7 @@ module TS.SpaceTac.Equipments {
expect(ship2.getValue("hull")).toEqual(82); expect(ship2.getValue("hull")).toEqual(82);
expect(ship3.getValue("hull")).toEqual(80); expect(ship3.getValue("hull")).toEqual(80);
result = protector.action.apply(ship1, null); result = action.apply(ship1);
expect(result).toBe(true); expect(result).toBe(true);
expect((<ToggleAction>protector.action).activated).toBe(false); expect((<ToggleAction>protector.action).activated).toBe(false);

View file

@ -45,7 +45,7 @@ module TS.SpaceTac.Equipments {
expect(target.sticky_effects).toEqual([]); expect(target.sticky_effects).toEqual([]);
// Attribute is immediately limited // Attribute is immediately limited
equipment.action.apply(ship, Target.newFromShip(target)); nn(equipment.action).apply(ship, Target.newFromShip(target));
expect(target.values.power.get()).toBe(3); expect(target.values.power.get()).toBe(3);
expect(target.sticky_effects).toEqual([ expect(target.sticky_effects).toEqual([

View file

@ -41,7 +41,7 @@ module TS.SpaceTac.Equipments {
battle.playing_ship = ship; battle.playing_ship = ship;
battle.play_order = [ship]; battle.play_order = [ship];
TestTools.setShipAP(ship, 10); TestTools.setShipAP(ship, 10);
let result = equipment.action.apply(ship, new Target(5, 5, null)); let result = nn(equipment.action).apply(ship, new Target(5, 5, null));
expect(result).toBe(true); expect(result).toBe(true);
expect(battle.drones.length).toBe(1); expect(battle.drones.length).toBe(1);

View file

@ -44,7 +44,7 @@ module TS.SpaceTac.Equipments {
var template = new Equipments.SubMunitionMissile(); var template = new Equipments.SubMunitionMissile();
var equipment = template.generate(1); var equipment = template.generate(1);
let action = <FireWeaponAction>equipment.action; let action = <FireWeaponAction>nn(equipment.action);
action.range = 5; action.range = 5;
action.blast = 1.5; action.blast = 1.5;
(<DamageEffect>action.effects[0]).base = 20; (<DamageEffect>action.effects[0]).base = 20;
@ -65,11 +65,11 @@ module TS.SpaceTac.Equipments {
// Fire at a ship // Fire at a ship
var target = Target.newFromShip(enemy1); var target = Target.newFromShip(enemy1);
expect(equipment.action.checkCannotBeApplied(ship)).toBe(null); expect(action.checkCannotBeApplied(ship)).toBe(null);
equipment.action.apply(ship, target); action.apply(ship, target);
checkHP(50, 10, 50, 10, 50, 10); checkHP(50, 10, 50, 10, 50, 10);
expect(battle.log.events.length).toBe(5); expect(battle.log.events.length).toBe(5);
expect(battle.log.events[0]).toEqual(new ActionAppliedEvent(ship, equipment.action, Target.newFromLocation(1, 0), 4)); expect(battle.log.events[0]).toEqual(new ActionAppliedEvent(ship, action, Target.newFromLocation(1, 0), 4));
expect(battle.log.events[1]).toEqual(new FireEvent(ship, equipment, Target.newFromLocation(1, 0))); expect(battle.log.events[1]).toEqual(new FireEvent(ship, equipment, Target.newFromLocation(1, 0)));
expect(battle.log.events[2]).toEqual(new DamageEvent(ship, 0, 20)); expect(battle.log.events[2]).toEqual(new DamageEvent(ship, 0, 20));
expect(battle.log.events[3]).toEqual(new DamageEvent(enemy1, 0, 20)); expect(battle.log.events[3]).toEqual(new DamageEvent(enemy1, 0, 20));
@ -80,11 +80,11 @@ module TS.SpaceTac.Equipments {
// Fire in space // Fire in space
target = Target.newFromLocation(2.4, 0); target = Target.newFromLocation(2.4, 0);
expect(equipment.action.checkCannotBeApplied(ship)).toBe(null); expect(action.checkCannotBeApplied(ship)).toBe(null);
equipment.action.apply(ship, target); action.apply(ship, target);
checkHP(50, 10, 40, 0, 40, 0); checkHP(50, 10, 40, 0, 40, 0);
expect(battle.log.events.length).toBe(4); expect(battle.log.events.length).toBe(4);
expect(battle.log.events[0]).toEqual(new ActionAppliedEvent(ship, equipment.action, target, 4)); expect(battle.log.events[0]).toEqual(new ActionAppliedEvent(ship, action, target, 4));
expect(battle.log.events[1]).toEqual(new FireEvent(ship, equipment, target)); expect(battle.log.events[1]).toEqual(new FireEvent(ship, equipment, target));
expect(battle.log.events[2]).toEqual(new DamageEvent(enemy1, 10, 10)); expect(battle.log.events[2]).toEqual(new DamageEvent(enemy1, 10, 10));
expect(battle.log.events[3]).toEqual(new DamageEvent(enemy2, 10, 10)); expect(battle.log.events[3]).toEqual(new DamageEvent(enemy2, 10, 10));
@ -94,11 +94,11 @@ module TS.SpaceTac.Equipments {
// Fire far away // Fire far away
target = Target.newFromLocation(5, 0); target = Target.newFromLocation(5, 0);
expect(equipment.action.checkCannotBeApplied(ship)).toBe(null); expect(action.checkCannotBeApplied(ship)).toBe(null);
equipment.action.apply(ship, target); action.apply(ship, target);
checkHP(50, 10, 40, 0, 40, 0); checkHP(50, 10, 40, 0, 40, 0);
expect(battle.log.events.length).toBe(2); expect(battle.log.events.length).toBe(2);
expect(battle.log.events[0]).toEqual(new ActionAppliedEvent(ship, equipment.action, target, 4)); expect(battle.log.events[0]).toEqual(new ActionAppliedEvent(ship, action, target, 4));
expect(battle.log.events[1]).toEqual(new FireEvent(ship, equipment, target)); expect(battle.log.events[1]).toEqual(new FireEvent(ship, equipment, target));
}); });
}); });

View file

@ -87,10 +87,14 @@ module TS.SpaceTac.UI {
// Initialize // Initialize
this.updateActiveStatus(true); this.updateActiveStatus(true);
this.updateCooldownStatus(); this.updateCooldownStatus(0);
} }
// Process a click event on the action icon /**
* Process a click event on the action icon
*
* This will enter the action's targetting mode, waiting for a target or confirmation to apply the action
*/
processClick(): void { processClick(): void {
if (!this.bar.interactive) { if (!this.bar.interactive) {
return; return;
@ -110,28 +114,29 @@ module TS.SpaceTac.UI {
this.bar.actionEnded(); this.bar.actionEnded();
this.bar.actionStarted(); this.bar.actionStarted();
// Update range hint
if (this.battleview.arena.range_hint && this.action instanceof MoveAction) {
this.battleview.arena.range_hint.update(this.ship, this.action);
}
// Set the selected state // Set the selected state
this.setSelected(true); this.setSelected(true);
if (this.action.needs_target) { let mode = this.action.getTargettingMode(this.ship);
if (mode == ActionTargettingMode.SELF || mode == ActionTargettingMode.SELF_CONFIRM) {
// Apply immediately on the ship
// TODO Handle confirm
this.processSelection(Target.newFromShip(this.ship));
} else {
let sprite = this.battleview.arena.findShipSprite(this.ship); let sprite = this.battleview.arena.findShipSprite(this.ship);
if (sprite) { if (sprite) {
// Switch to targetting mode (will apply action when a target is selected) // Switch to targetting mode (will apply action when a target is selected)
this.targetting = this.battleview.enterTargettingMode(this.action); this.targetting = this.battleview.enterTargettingMode(this.action, mode);
} }
} else {
// No target needed, apply action immediately
this.processSelection(null);
} }
} }
// Called when a target is selected /**
processSelection(target: Target | null): void { * Called when a target is selected
*
* This will effectively apply the action
*/
processSelection(target: Target): void {
if (this.action.apply(this.ship, target)) { if (this.action.apply(this.ship, target)) {
this.bar.actionEnded(); this.bar.actionEnded();
} }
@ -157,7 +162,7 @@ module TS.SpaceTac.UI {
} }
// Update the cooldown status // Update the cooldown status
updateCooldownStatus(): void { updateCooldownStatus(animate = 300): void {
let remaining = this.action.getUsesBeforeOverheat(); let remaining = this.action.getUsesBeforeOverheat();
if (this.selected && remaining == 1) { if (this.selected && remaining == 1) {
// will overheat, hint at the cooldown time // will overheat, hint at the cooldown time
@ -165,21 +170,21 @@ module TS.SpaceTac.UI {
this.battleview.changeImage(this.cooldown, "battle-actionbar-icon-cooldown"); this.battleview.changeImage(this.cooldown, "battle-actionbar-icon-cooldown");
this.cooldown.scale.set(0.7); this.cooldown.scale.set(0.7);
this.cooldown_count.text = `${cooldown}`; this.cooldown_count.text = `${cooldown}`;
this.battleview.animations.setVisible(this.cooldown, true, 300); this.battleview.animations.setVisible(this.cooldown, true, animate);
} else if (remaining == 0) { } else if (remaining == 0) {
// overheated, show cooldown time // overheated, show cooldown time
let cooldown = this.action.getCooldownDuration(false); let cooldown = this.action.getCooldownDuration(false);
this.battleview.changeImage(this.cooldown, "battle-actionbar-icon-cooldown"); this.battleview.changeImage(this.cooldown, "battle-actionbar-icon-cooldown");
this.cooldown.scale.set(1); this.cooldown.scale.set(1);
this.cooldown_count.text = `${cooldown}`; this.cooldown_count.text = `${cooldown}`;
this.battleview.animations.setVisible(this.cooldown, true, 300); this.battleview.animations.setVisible(this.cooldown, true, animate);
} else if (this.action instanceof ToggleAction && this.action.activated) { } else if (this.action instanceof ToggleAction && this.action.activated) {
this.battleview.changeImage(this.cooldown, "battle-actionbar-icon-toggled"); this.battleview.changeImage(this.cooldown, "battle-actionbar-icon-toggled");
this.cooldown.scale.set(1); this.cooldown.scale.set(1);
this.cooldown_count.text = ""; this.cooldown_count.text = "";
this.battleview.animations.setVisible(this.cooldown, true, 300); this.battleview.animations.setVisible(this.cooldown, true, animate);
} else { } else {
this.battleview.animations.setVisible(this.cooldown, false, 300); this.battleview.animations.setVisible(this.cooldown, false, animate);
} }
} }

View file

@ -4,9 +4,9 @@ module TS.SpaceTac.UI {
* *
* This is the area in the BattleView that will display ships with their real positions * This is the area in the BattleView that will display ships with their real positions
*/ */
export class Arena extends Phaser.Group { export class Arena {
// Link to battleview // Link to battleview
battleview: BattleView view: BattleView
// Boundaries of the arena // Boundaries of the arena
boundaries: IBounded = { x: 112, y: 132, width: 1808, height: 948 } boundaries: IBounded = { x: 112, y: 132, width: 1808, height: 948 }
@ -32,6 +32,7 @@ module TS.SpaceTac.UI {
private playing: ArenaShip | null private playing: ArenaShip | null
// Layer for particles // Layer for particles
container: Phaser.Group
layer_garbage: Phaser.Group layer_garbage: Phaser.Group
layer_hints: Phaser.Group layer_hints: Phaser.Group
layer_drones: Phaser.Group layer_drones: Phaser.Group
@ -39,79 +40,101 @@ module TS.SpaceTac.UI {
layer_weapon_effects: Phaser.Group layer_weapon_effects: Phaser.Group
layer_targetting: Phaser.Group layer_targetting: Phaser.Group
// Create a graphical arena for ship sprites to fight in a 2D space // Callbacks to receive cursor events
constructor(battleview: BattleView) { callbacks_hover: ((location: ArenaLocation | null, ship: Ship | null) => void)[] = []
super(battleview.game); callbacks_click: (() => void)[] = []
this.battleview = battleview; // Create a graphical arena for ship sprites to fight in a 2D space
constructor(view: BattleView, container?: Phaser.Group) {
this.view = view;
this.playing = null; this.playing = null;
this.hovered = null; this.hovered = null;
this.range_hint = new RangeHint(this); this.range_hint = new RangeHint(this);
this.position.set(this.boundaries.x, this.boundaries.y); this.container = container || new Phaser.Group(view.game, undefined, "arena");
this.container.position.set(this.boundaries.x, this.boundaries.y);
this.init(); this.setupMouseCapture();
this.layer_garbage = this.container.add(new Phaser.Group(view.game, undefined, "garbage"));
this.layer_hints = this.container.add(new Phaser.Group(view.game, undefined, "hints"));
this.layer_drones = this.container.add(new Phaser.Group(view.game, undefined, "drones"));
this.layer_ships = this.container.add(new Phaser.Group(view.game, undefined, "ships"));
this.layer_weapon_effects = this.container.add(new Phaser.Group(view.game, undefined, "effects"));
this.layer_targetting = this.container.add(new Phaser.Group(view.game, undefined, "targetting"));
this.range_hint.setLayer(this.layer_hints);
this.addShipSprites();
this.container.onDestroy.add(() => this.destroy());
}
/**
* Move to a specific layer
*/
moveToLayer(layer: Phaser.Group): void {
layer.add(this.container);
} }
/** /**
* Setup the mouse capture for targetting events * Setup the mouse capture for targetting events
*/ */
setupMouseCapture() { setupMouseCapture() {
let battleview = this.battleview; let view = this.view;
var background = new Phaser.Button(battleview.game, 0, 0, "battle-arena-background"); var background = new Phaser.Button(view.game, 0, 0, "battle-arena-background");
background.name = "mouse-capture";
background.scale.set(this.boundaries.width / background.width, this.boundaries.height / background.height); background.scale.set(this.boundaries.width / background.width, this.boundaries.height / background.height);
this.mouse_capture = background; this.mouse_capture = background;
// Capture clicks on background // Capture clicks on background
background.onInputUp.add(() => { background.onInputUp.add(() => {
battleview.cursorClicked(); this.callbacks_click.forEach(callback => callback());
}); });
background.onInputOut.add(() => { background.onInputOut.add(() => {
battleview.targetting.setTarget(null); this.callbacks_hover.forEach(callback => callback(null, null));
}); });
// Watch mouse move to capture hovering over background // Watch mouse move to capture hovering over background
this.input_callback = this.game.input.addMoveCallback((pointer: Phaser.Pointer) => { this.input_callback = this.view.input.addMoveCallback((pointer: Phaser.Pointer) => {
var point = new Phaser.Point(); var point = new Phaser.Point();
if (battleview.game.input.hitTest(background, pointer, point)) { if (view.input.hitTest(background, pointer, point)) {
battleview.cursorInSpace(point.x * background.scale.x, point.y * background.scale.y); let location = new ArenaLocation(point.x * background.scale.x, point.y * background.scale.y);
let ship = this.getShip(location);
this.callbacks_hover.forEach(callback => callback(location, ship));
} }
}, null); }, null);
this.add(this.mouse_capture); this.container.add(this.mouse_capture);
}
destroy() {
if (this.input_callback) {
this.game.input.deleteMoveCallback(this.input_callback);
this.input_callback = null;
}
super.destroy();
} }
/** /**
* Initialize state (create sprites) * Get the ship under a cursor location
*/ */
init(): void { getShip(location: ArenaLocation): Ship | null {
this.setupMouseCapture(); let nearest = minBy(this.ship_sprites, sprite => arenaDistance(location, sprite.ship.location));
if (nearest && arenaDistance(location, nearest) < 50) {
return nearest.ship;
} else {
return null;
}
}
this.layer_garbage = this.add(new Phaser.Group(this.game)); /**
this.layer_hints = this.add(new Phaser.Group(this.game)); * Call when the arena is destroyed to properly remove input handlers
this.layer_drones = this.add(new Phaser.Group(this.game)); */
this.layer_ships = this.add(new Phaser.Group(this.game)); destroy() {
this.layer_weapon_effects = this.add(new Phaser.Group(this.game)); if (this.input_callback) {
this.layer_targetting = this.add(new Phaser.Group(this.game)); this.view.input.deleteMoveCallback(this.input_callback);
this.input_callback = null;
this.range_hint.setLayer(this.layer_hints); }
this.addShipSprites();
} }
/** /**
* Add the sprites for all ships * Add the sprites for all ships
*/ */
addShipSprites() { addShipSprites() {
iforeach(this.battleview.battle.iships(), ship => { iforeach(this.view.battle.iships(), ship => {
let sprite = new ArenaShip(this, ship); let sprite = new ArenaShip(this, ship);
this.layer_ships.add(sprite); this.layer_ships.add(sprite);
this.ship_sprites.push(sprite); this.ship_sprites.push(sprite);
@ -129,16 +152,18 @@ module TS.SpaceTac.UI {
return base.filter(ship => arenaInside(ship, area, border_inclusive)); return base.filter(ship => arenaInside(ship, area, border_inclusive));
} }
// Get the current MainUI instance /**
getGame(): MainUI { * Get the current MainUI instance
return this.battleview.gameui; */
get game(): MainUI {
return this.view.gameui;
} }
/** /**
* Get the current battle displayed * Get the current battle displayed
*/ */
getBattle(): Battle { getBattle(): Battle {
return this.battleview.battle; return this.view.battle;
} }
// Remove a ship sprite // Remove a ship sprite
@ -210,7 +235,7 @@ module TS.SpaceTac.UI {
*/ */
addDrone(drone: Drone, animate = true): number { addDrone(drone: Drone, animate = true): number {
if (!this.findDrone(drone)) { if (!this.findDrone(drone)) {
let sprite = new ArenaDrone(this.battleview, drone); let sprite = new ArenaDrone(this.view, drone);
let angle = Math.atan2(drone.y - drone.owner.arena_y, drone.x - drone.owner.arena_x); let angle = Math.atan2(drone.y - drone.owner.arena_y, drone.x - drone.owner.arena_x);
this.layer_drones.add(sprite); this.layer_drones.add(sprite);
this.drone_sprites.push(sprite); this.drone_sprites.push(sprite);
@ -219,7 +244,7 @@ module TS.SpaceTac.UI {
sprite.position.set(drone.owner.arena_x, drone.owner.arena_y); sprite.position.set(drone.owner.arena_x, drone.owner.arena_y);
sprite.sprite.rotation = drone.owner.arena_angle; sprite.sprite.rotation = drone.owner.arena_angle;
let move_duration = Animations.moveInSpace(sprite, drone.x, drone.y, angle, sprite.sprite); let move_duration = Animations.moveInSpace(sprite, drone.x, drone.y, angle, sprite.sprite);
this.game.tweens.create(sprite.radius).from({ alpha: 0 }, 500, Phaser.Easing.Cubic.In, true, move_duration); this.view.tweens.create(sprite.radius).from({ alpha: 0 }, 500, Phaser.Easing.Cubic.In, true, move_duration);
return move_duration + 500; return move_duration + 500;
} else { } else {
@ -254,9 +279,9 @@ module TS.SpaceTac.UI {
setTacticalMode(active: boolean): void { setTacticalMode(active: boolean): void {
this.ship_sprites.forEach(sprite => sprite.setHovered(active, true)); this.ship_sprites.forEach(sprite => sprite.setHovered(active, true));
this.drone_sprites.forEach(drone => drone.setTacticalMode(active)); this.drone_sprites.forEach(drone => drone.setTacticalMode(active));
this.battleview.animations.setVisible(this.layer_garbage, !active, 200); this.view.animations.setVisible(this.layer_garbage, !active, 200);
if (this.battleview.background) { if (this.view.background) {
this.battleview.animations.setVisible(this.battleview.background, !active, 200); this.view.animations.setVisible(this.view.background, !active, 200);
} }
} }

View file

@ -12,7 +12,7 @@ module TS.SpaceTac.UI {
enemy: boolean enemy: boolean
// Ship sprite // Ship sprite
sprite: Phaser.Button sprite: Phaser.Image
// Statis effect // Statis effect
stasis: Phaser.Image stasis: Phaser.Image
@ -44,7 +44,7 @@ module TS.SpaceTac.UI {
constructor(parent: Arena, ship: Ship) { constructor(parent: Arena, ship: Ship) {
super(parent.game); super(parent.game);
this.arena = parent; this.arena = parent;
this.battleview = parent.battleview; this.battleview = parent.view;
this.ship = ship; this.ship = ship;
this.enemy = this.ship.getPlayer() != this.battleview.player; this.enemy = this.ship.getPlayer() != this.battleview.player;
@ -60,8 +60,7 @@ module TS.SpaceTac.UI {
this.setPlaying(false); this.setPlaying(false);
// Add ship sprite // Add ship sprite
let info = this.battleview.getImageInfo(`ship-${ship.model.code}-sprite`); this.sprite = this.battleview.newImage(`ship-${ship.model.code}-sprite`)
this.sprite = new Phaser.Button(this.game, 0, 0, info.key, undefined, undefined, info.frame, info.frame);
this.sprite.rotation = ship.arena_angle; this.sprite.rotation = ship.arena_angle;
this.sprite.anchor.set(0.5, 0.5); this.sprite.anchor.set(0.5, 0.5);
this.sprite.scale.set(0.4); this.sprite.scale.set(0.4);
@ -121,13 +120,6 @@ module TS.SpaceTac.UI {
this.updateActiveEffects(); this.updateActiveEffects();
this.updateEffectsRadius(); this.updateEffectsRadius();
// Handle input on ship sprite
UITools.setHoverClick(this.sprite,
() => this.battleview.cursorOnShip(ship),
() => this.battleview.cursorOffShip(ship),
() => this.battleview.cursorClicked()
);
// Set location // Set location
if (this.battleview.battle.turn == 1 && ship.alive && ship.fleet.player === this.battleview.player) { if (this.battleview.battle.turn == 1 && 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.position.set(ship.arena_x - 500 * Math.cos(ship.arena_angle), ship.arena_y - 500 * Math.sin(ship.arena_angle));

View file

@ -4,78 +4,66 @@ module TS.SpaceTac.UI.Specs {
describe("BattleView", function () { describe("BattleView", function () {
let testgame = setupBattleview(); let testgame = setupBattleview();
it("forwards events in targetting mode", function () { it("handles ship hovering to display tooltip", function () {
let battleview = testgame.battleview;
expect(battleview.ship_hovered).toBeNull("initial state");
let ship = nn(battleview.battle.playing_ship);
battleview.cursorHovered(ship.location, ship);
expect(battleview.ship_hovered).toBe(ship, "ship1 hovered");
ship = nn(battleview.battle.play_order[1]);
battleview.cursorHovered(ship.location, ship);
expect(battleview.ship_hovered).toBe(ship, "ship2 hovered");
battleview.cursorHovered(new ArenaLocation(0, 0), null);
expect(battleview.ship_hovered).toBeNull("out");
battleview.cursorOnShip(ship);
expect(battleview.ship_hovered).toBe(ship, "force on");
battleview.cursorOffShip(battleview.battle.play_order[2]);
expect(battleview.ship_hovered).toBe(ship, "force off on wrong ship");
battleview.cursorOffShip(ship);
expect(battleview.ship_hovered).toBeNull("force off");
});
it("forwards cursor hovering and click to targetting", function () {
let battleview = testgame.battleview; let battleview = testgame.battleview;
expect(battleview.targetting.active).toBe(false); expect(battleview.targetting.active).toBe(false);
battleview.setInteractionEnabled(true); battleview.setInteractionEnabled(true);
spyOn(battleview.targetting, "validate").and.stub();
battleview.cursorInSpace(5, 5);
expect(battleview.targetting.active).toBe(false);
// Enter targetting mode
let weapon = TestTools.addWeapon(nn(battleview.battle.playing_ship), 10); let weapon = TestTools.addWeapon(nn(battleview.battle.playing_ship), 10);
battleview.enterTargettingMode(weapon.action); battleview.enterTargettingMode(nn(weapon.action), ActionTargettingMode.SPACE);
expect(battleview.targetting.active).toBe(true); expect(battleview.targetting.active).toBe(true);
// Forward selection in space battleview.cursorHovered(new ArenaLocation(5, 8), null);
battleview.cursorInSpace(8, 4); expect(battleview.targetting.target).toEqual(Target.newFromLocation(5, 8));
expect(battleview.ship_hovered).toBeNull(); expect(battleview.ship_hovered).toBeNull();
expect(battleview.targetting.target).toEqual(Target.newFromLocation(8, 4));
// Process a click on space let ship = battleview.battle.play_order[3];
battleview.cursorHovered(ship.location, ship);
expect(battleview.targetting.target).toEqual(Target.newFromLocation(ship.arena_x, ship.arena_y));
expect(battleview.ship_hovered).toBe(ship);
spyOn(battleview.targetting, "validate").and.stub();
expect(battleview.targetting.validate).toHaveBeenCalledTimes(0);
battleview.cursorClicked(); battleview.cursorClicked();
expect(battleview.targetting.validate).toHaveBeenCalledTimes(1);
// Forward ship hovering
battleview.cursorOnShip(battleview.battle.play_order[0]);
expect(battleview.ship_hovered).toEqual(battleview.battle.play_order[0]);
expect(battleview.targetting.target).toEqual(Target.newFromShip(battleview.battle.play_order[0]));
// Don't leave a ship we're not hovering
battleview.cursorOffShip(battleview.battle.play_order[1]);
expect(battleview.ship_hovered).toEqual(battleview.battle.play_order[0]);
expect(battleview.targetting.target).toEqual(Target.newFromShip(battleview.battle.play_order[0]));
// Don't move in space while on ship
battleview.cursorInSpace(1, 3);
expect(battleview.ship_hovered).toEqual(battleview.battle.play_order[0]);
expect(battleview.targetting.target).toEqual(Target.newFromShip(battleview.battle.play_order[0]));
// Process a click on ship
battleview.cursorClicked();
// Leave the ship
battleview.cursorOffShip(battleview.battle.play_order[0]);
expect(battleview.ship_hovered).toBeNull();
expect(battleview.targetting.target).toBeNull();
// Quit targetting
battleview.exitTargettingMode(); battleview.exitTargettingMode();
expect(battleview.targetting.active).toBe(false); expect(battleview.targetting.active).toBe(false);
// Events process normally battleview.cursorHovered(new ArenaLocation(5, 8), null);
battleview.cursorInSpace(8, 4); expect(battleview.targetting.target).toBeNull();
expect(battleview.ship_hovered).toBeNull();
battleview.cursorOnShip(battleview.battle.play_order[0]);
expect(battleview.ship_hovered).toEqual(battleview.battle.play_order[0]);
// Quit twice don't do anything
battleview.exitTargettingMode();
}); });
it("allows to choose an action and a target with shortcut keys", function () { it("allows to choose an action and a target with shortcut keys", function () {
let battleview = testgame.battleview; let battleview = testgame.battleview;
battleview.setInteractionEnabled(true); battleview.setInteractionEnabled(true);
let action_icon = nn(first(battleview.action_bar.action_icons, icon => icon.action.needs_target)); let action_icon = battleview.action_bar.action_icons[0];
expect(battleview.targetting.active).toBe(false); expect(battleview.targetting.active).toBe(false);
expect(battleview.action_bar.hasActionSelected()).toBe(false); expect(battleview.action_bar.hasActionSelected()).toBe(false);
@ -83,7 +71,7 @@ module TS.SpaceTac.UI.Specs {
expect(battleview.action_bar.hasActionSelected()).toBe(true); expect(battleview.action_bar.hasActionSelected()).toBe(true);
expect(battleview.targetting.active).toBe(true); expect(battleview.targetting.active).toBe(true);
expect(battleview.targetting.action).toBe(action_icon.action); expect(battleview.targetting.action).toBe(action_icon.action);
expect(battleview.targetting.target).toBe(null); expect(battleview.targetting.target).toEqual(action_icon.action.getDefaultTarget(action_icon.ship));
battleview.numberPressed(3); battleview.numberPressed(3);
expect(battleview.targetting.active).toBe(true); expect(battleview.targetting.active).toBe(true);
expect(battleview.targetting.action).toBe(action_icon.action); expect(battleview.targetting.action).toBe(action_icon.action);

View file

@ -94,9 +94,10 @@ module TS.SpaceTac.UI {
this.background = new Phaser.Image(game, 0, 0, "battle-background", 0); this.background = new Phaser.Image(game, 0, 0, "battle-background", 0);
this.layer_background.add(this.background); this.layer_background.add(this.background);
// Add arena (local map) // Add arena (local battlefield map)
this.arena = new Arena(this); this.arena = new Arena(this, this.layer_arena);
this.layer_arena.add(this.arena); this.arena.callbacks_hover.push(bound(this, "cursorHovered"));
this.arena.callbacks_click.push(bound(this, "cursorClicked"));
// Add UI elements // Add UI elements
this.action_bar = new ActionBar(this); this.action_bar = new ActionBar(this);
@ -106,7 +107,7 @@ module TS.SpaceTac.UI {
this.layer_sheets.add(this.character_sheet); this.layer_sheets.add(this.character_sheet);
// Targetting info // Targetting info
this.targetting = new Targetting(this, this.action_bar, this.toggle_tactical_mode); this.targetting = new Targetting(this, this.action_bar, this.toggle_tactical_mode, this.arena.range_hint);
this.targetting.moveToLayer(this.arena.layer_targetting); this.targetting.moveToLayer(this.arena.layer_targetting);
// BGM // BGM
@ -178,30 +179,43 @@ module TS.SpaceTac.UI {
} }
} }
// Method called when cursor starts hovering over a ship (or its icon) /**
* Method called when the arena cursor is hovered
*/
cursorHovered(location: ArenaLocation | null, ship: Ship | null) {
if (this.targetting.active) {
this.targetting.setTargetFromLocation(location);
}
if (ship && this.ship_hovered != ship) {
// TODO if targetting is active, this may hide targetting info with the tooltip
this.cursorOnShip(ship);
} else if (!ship && this.ship_hovered) {
this.cursorOffShip(this.ship_hovered);
}
}
/**
* Method called when cursor starts hovering over a ship (or its icon)
*/
cursorOnShip(ship: Ship): void { cursorOnShip(ship: Ship): void {
if (!this.targetting.active || ship.alive) { if (ship.alive) {
this.setShipHovered(ship); this.setShipHovered(ship);
} }
} }
// Method called when cursor stops hovering over a ship (or its icon) /**
* Method called when cursor stops hovering over a ship (or its icon)
*/
cursorOffShip(ship: Ship): void { cursorOffShip(ship: Ship): void {
if (this.ship_hovered === ship) { if (this.ship_hovered === ship) {
this.setShipHovered(null); this.setShipHovered(null);
} }
} }
// Method called when cursor moves in space /**
cursorInSpace(x: number, y: number): void { * Method called when cursor has been clicked (in space or on a ship)
if (!this.ship_hovered) { */
if (this.targetting.active) {
this.targetting.setTarget(Target.newFromLocation(x, y));
}
}
}
// Method called when cursor has been clicked (in space or on a ship)
cursorClicked(): void { cursorClicked(): void {
if (this.targetting.active) { if (this.targetting.active) {
this.targetting.validate(); this.targetting.validate();
@ -211,7 +225,9 @@ module TS.SpaceTac.UI {
} }
} }
// Set the currently hovered ship /**
* Set the currently hovered ship
*/
setShipHovered(ship: Ship | null): void { setShipHovered(ship: Ship | null): void {
this.ship_hovered = ship; this.ship_hovered = ship;
this.arena.setShipHovered(ship); this.arena.setShipHovered(ship);
@ -222,14 +238,6 @@ module TS.SpaceTac.UI {
} else { } else {
this.ship_tooltip.hide(); this.ship_tooltip.hide();
} }
if (this.targetting.active) {
if (ship) {
this.targetting.setTarget(Target.newFromShip(ship));
} else {
this.targetting.setTarget(null);
}
}
} }
// Enable or disable the global player interaction // Enable or disable the global player interaction
@ -248,12 +256,12 @@ module TS.SpaceTac.UI {
// Enter targetting mode // Enter targetting mode
// While in this mode, the Targetting object will receive hover and click events, and handle them // While in this mode, the Targetting object will receive hover and click events, and handle them
enterTargettingMode(action: BaseAction): Targetting | null { enterTargettingMode(action: BaseAction, mode: ActionTargettingMode): Targetting | null {
if (!this.interacting) { if (!this.interacting) {
return null; return null;
} }
this.targetting.setAction(action); this.targetting.setAction(action, mode);
return this.targetting; return this.targetting;
} }

View file

@ -14,7 +14,7 @@ module TS.SpaceTac.UI {
private height: number private height: number
constructor(arena: Arena) { constructor(arena: Arena) {
this.view = arena.battleview; this.view = arena.view;
let boundaries = arena.getBoundaries(); let boundaries = arena.getBoundaries();
this.width = boundaries.width; this.width = boundaries.width;

View file

@ -2,8 +2,15 @@ module TS.SpaceTac.UI.Specs {
describe("Targetting", function () { describe("Targetting", function () {
let testgame = setupBattleview(); let testgame = setupBattleview();
function newTargetting(): Targetting {
return new Targetting(testgame.battleview,
testgame.battleview.action_bar,
testgame.battleview.toggle_tactical_mode,
testgame.battleview.arena.range_hint);
}
it("draws simulation parts", function () { it("draws simulation parts", function () {
let targetting = new Targetting(testgame.battleview, testgame.battleview.action_bar, new Toggle()); let targetting = newTargetting();
let ship = nn(testgame.battleview.battle.playing_ship); let ship = nn(testgame.battleview.battle.playing_ship);
ship.setArenaPosition(10, 20); ship.setArenaPosition(10, 20);
@ -14,7 +21,7 @@ module TS.SpaceTac.UI.Specs {
let drawvector = spyOn(targetting, "drawVector").and.stub(); let drawvector = spyOn(targetting, "drawVector").and.stub();
let part = { let part = {
action: weapon.action, action: nn(weapon.action),
target: new Target(50, 30), target: new Target(50, 30),
ap: 5, ap: 5,
possible: true possible: true
@ -27,15 +34,15 @@ module TS.SpaceTac.UI.Specs {
expect(drawvector).toHaveBeenCalledTimes(2); expect(drawvector).toHaveBeenCalledTimes(2);
expect(drawvector).toHaveBeenCalledWith(0x8e8e8e, 10, 20, 50, 30, 0); expect(drawvector).toHaveBeenCalledWith(0x8e8e8e, 10, 20, 50, 30, 0);
targetting.setAction(engine.action); targetting.action = engine.action;
part.action = engine.action; part.action = nn(engine.action);
targetting.drawPart(part, true, null); targetting.drawPart(part, true, null);
expect(drawvector).toHaveBeenCalledTimes(3); expect(drawvector).toHaveBeenCalledTimes(3);
expect(drawvector).toHaveBeenCalledWith(0xe09c47, 10, 20, 50, 30, 12); expect(drawvector).toHaveBeenCalledWith(0xe09c47, 10, 20, 50, 30, 12);
}); });
it("updates impact indicators on ships inside the blast radius", function () { it("updates impact indicators on ships inside the blast radius", function () {
let targetting = new Targetting(testgame.battleview, testgame.battleview.action_bar, new Toggle()); let targetting = newTargetting();
let ship = nn(testgame.battleview.battle.playing_ship); let ship = nn(testgame.battleview.battle.playing_ship);
let collect = spyOn(testgame.battleview.battle, "collectShipsInCircle").and.returnValues( let collect = spyOn(testgame.battleview.battle, "collectShipsInCircle").and.returnValues(
@ -71,7 +78,7 @@ module TS.SpaceTac.UI.Specs {
}); });
it("updates graphics from simulation", function () { it("updates graphics from simulation", function () {
let targetting = new Targetting(testgame.battleview, testgame.battleview.action_bar, new Toggle()); let targetting = newTargetting();
let ship = nn(testgame.battleview.battle.playing_ship); let ship = nn(testgame.battleview.battle.playing_ship);
let engine = TestTools.addEngine(ship, 8000); let engine = TestTools.addEngine(ship, 8000);
@ -90,8 +97,8 @@ module TS.SpaceTac.UI.Specs {
result.need_fire = true; result.need_fire = true;
result.can_fire = true; result.can_fire = true;
result.parts = [ result.parts = [
{ action: engine.action, target: Target.newFromLocation(80, 20), ap: 1, possible: true }, { action: nn(engine.action), target: Target.newFromLocation(80, 20), ap: 1, possible: true },
{ action: weapon.action, target: Target.newFromLocation(156, 65), ap: 5, possible: true } { action: nn(weapon.action), target: Target.newFromLocation(156, 65), ap: 5, possible: true }
] ]
targetting.simulation = result; targetting.simulation = result;
}); });

View file

@ -12,6 +12,7 @@ module TS.SpaceTac.UI {
ship: Ship | null = null ship: Ship | null = null
action: BaseAction | null = null action: BaseAction | null = null
target: Target | null = null target: Target | null = null
mode: ActionTargettingMode
simulation = new MoveFireResult() simulation = new MoveFireResult()
// Movement projector // Movement projector
@ -25,15 +26,17 @@ module TS.SpaceTac.UI {
// Collaborators to update // Collaborators to update
actionbar: ActionBar actionbar: ActionBar
range_hint: RangeHint
tactical_mode: ToggleClient tactical_mode: ToggleClient
// Access to the parent view // Access to the parent view
view: BaseView view: BaseView
constructor(view: BaseView, actionbar: ActionBar, tactical_mode: Toggle) { constructor(view: BaseView, actionbar: ActionBar, tactical_mode: Toggle, range_hint: RangeHint) {
this.view = view; this.view = view;
this.actionbar = actionbar; this.actionbar = actionbar;
this.tactical_mode = tactical_mode.manipulate("targetting"); this.tactical_mode = tactical_mode.manipulate("targetting");
this.range_hint = range_hint;
this.container = view.add.group(); this.container = view.add.group();
@ -155,6 +158,9 @@ module TS.SpaceTac.UI {
this.fire_arrow.visible = false; this.fire_arrow.visible = false;
this.move_ghost.visible = false; this.move_ghost.visible = false;
let from = simulation.need_fire ? simulation.move_location : this.ship.location;
let angle = Math.atan2(this.target.y - from.y, this.target.x - from.x);
if (simulation.success) { if (simulation.success) {
let previous: MoveFirePart | null = null; let previous: MoveFirePart | null = null;
simulation.parts.forEach(part => { simulation.parts.forEach(part => {
@ -162,9 +168,6 @@ module TS.SpaceTac.UI {
previous = part; previous = part;
}); });
let from = simulation.need_fire ? simulation.move_location : this.ship.location;
let angle = Math.atan2(this.target.y - from.y, this.target.x - from.x);
if (simulation.need_move) { if (simulation.need_move) {
this.move_ghost.visible = true; this.move_ghost.visible = true;
this.move_ghost.position.set(simulation.move_location.x, simulation.move_location.y); this.move_ghost.position.set(simulation.move_location.x, simulation.move_location.y);
@ -194,17 +197,46 @@ module TS.SpaceTac.UI {
this.fire_impact.visible = false; this.fire_impact.visible = false;
this.fire_arrow.visible = false; this.fire_arrow.visible = false;
} }
this.container.visible = true;
} else { } else {
// TODO Display error this.drawVector(0x888888, this.ship.arena_x, this.ship.arena_y, this.target.x, this.target.y);
this.container.visible = false; this.fire_arrow.position.set(this.target.x, this.target.y);
this.fire_arrow.rotation = angle;
this.view.changeImage(this.fire_arrow, "battle-hud-simulator-failed");
this.fire_arrow.visible = true;
this.fire_blast.visible = false;
} }
this.container.visible = true;
} else { } else {
this.container.visible = false; this.container.visible = false;
} }
// Toggle tactical mode
this.tactical_mode(bool(this.action)); this.tactical_mode(bool(this.action));
// Toggle range hint
if (this.ship && this.action) {
if (this.simulation.need_move) {
if (this.simulation.success) {
let last_move = first(acopy(this.simulation.parts).reverse(), part => part.action instanceof MoveAction);
if (last_move) {
this.range_hint.update(this.ship, last_move.action);
} else {
this.range_hint.clear();
}
} else {
let engine = new MoveFireSimulator(this.ship).findBestEngine();
if (engine && engine.action) {
this.range_hint.update(this.ship, engine.action);
} else {
this.range_hint.clear();
}
}
} else {
this.range_hint.update(this.ship, this.action);
}
} else {
this.range_hint.clear();
}
} }
/** /**
@ -222,19 +254,44 @@ module TS.SpaceTac.UI {
/** /**
* Set the current targetting action, or null to stop targetting * Set the current targetting action, or null to stop targetting
*/ */
setAction(action: BaseAction | null): void { setAction(action: BaseAction | null, mode?: ActionTargettingMode): void {
if (action && action.equipment && action.equipment.attached_to && action.equipment.attached_to.ship) { if (action && action.equipment && action.equipment.attached_to && action.equipment.attached_to.ship) {
this.ship = action.equipment.attached_to.ship; this.ship = action.equipment.attached_to.ship;
this.action = action; this.action = action;
this.mode = (typeof mode == "undefined") ? action.getTargettingMode(this.ship) : mode;
this.view.changeImage(this.move_ghost, `ship-${this.ship.model.code}-sprite`); this.view.changeImage(this.move_ghost, `ship-${this.ship.model.code}-sprite`);
this.move_ghost.scale.set(0.4); this.move_ghost.scale.set(0.4);
this.setTarget(action.getDefaultTarget(this.ship));
} else { } else {
this.ship = null; this.ship = null;
this.action = null; this.action = null;
this.setTarget(null);
}
}
/**
* Set the target according to a hovered arena location
*
* This will apply the current targetting mode, to assist the player
*/
setTargetFromLocation(location: ArenaLocation | null): void {
if (location && this.ship) {
let battle = this.ship.getBattle();
if (this.mode == ActionTargettingMode.SHIP && battle) {
let targets = imaterialize(battle.iships(true));
let nearest = minBy(targets, ship => arenaDistance(ship.location, location));
this.setTarget(Target.newFromShip(nearest ? nearest : this.ship));
} else if (this.mode == ActionTargettingMode.SPACE) {
this.setTarget(Target.newFromLocation(location.x, location.y));
} else {
this.setTarget(Target.newFromShip(this.ship));
}
} else {
this.setTarget(null);
} }
this.target = null;
this.update();
} }
/** /**

View file

@ -39,10 +39,10 @@ module TS.SpaceTac.UI {
private effect: Function private effect: Function
constructor(arena: Arena, ship: Ship, target: Target, weapon: Equipment) { constructor(arena: Arena, ship: Ship, target: Target, weapon: Equipment) {
this.ui = arena.getGame(); this.ui = arena.game;
this.arena = arena; this.arena = arena;
this.view = arena.battleview; this.view = arena.view;
this.timer = arena.battleview.timer; this.timer = arena.view.timer;
this.layer = arena.layer_weapon_effects; this.layer = arena.layer_weapon_effects;
this.ship = ship; this.ship = ship;
this.target = target; this.target = target;
@ -161,7 +161,7 @@ module TS.SpaceTac.UI {
missile.rotation = arenaAngle(this.source, this.destination); missile.rotation = arenaAngle(this.source, this.destination);
this.layer.add(missile); this.layer.add(missile);
let blast_radius = this.weapon.action.getBlastRadius(this.ship); let blast_radius = this.weapon.action ? this.weapon.action.getBlastRadius(this.ship) : 0;
let projectile_duration = arenaDistance(this.source, this.destination) * 1.5; let projectile_duration = arenaDistance(this.source, this.destination) * 1.5;
let tween = this.ui.tweens.create(missile); let tween = this.ui.tweens.create(missile);