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 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
* 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 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)
* 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
* Find incentives to move from starting position (permanent drones or anomalies?)
* 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
* Merge identical sticky effects
* Allow to undo last moves
* Add a battle log display
* 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"
Ships models and equipments
@ -96,6 +96,7 @@ Common UI
Technical
---------
* Run jasmine tests in random order by default, and fix problems
* Pack all images in atlases
* 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]);
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);
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);
expect(stats.stats).toEqual({ "Power used": [4, 2] });
})

View File

@ -43,7 +43,7 @@ module TS.SpaceTac {
effects: BaseEffect[] = []
// 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)
wear = 0
@ -158,9 +158,11 @@ module TS.SpaceTac {
parts.push(["When equipped:"].concat(this.effects.map(effect => "• " + effect.getDescription())).join("\n"));
}
let action_desc = this.action.getEffectsDescription();
if (action_desc != "") {
parts.push(action_desc);
if (this.action) {
let action_desc = this.action.getEffectsDescription();
if (action_desc != "") {
parts.push(action_desc);
}
}
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 () {
let [ship, simulator, action] = simpleWeaponCase();
let result = simulator.simulateAction(ship.listEquipment(SlotType.Engine)[0].action, new Target(ship.arena_x, ship.arena_y, null));
expect(result.success).toBe(true);
let move_action = nn(ship.listEquipment(SlotType.Engine)[0].action)
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_fire).toBe(false);
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
if (result.need_move && move_target) {
let engine = this.findBestEngine();
if (engine) {
if (engine && engine.action) {
result.total_move_ap = engine.action.getActionPointsUsage(this.ship, move_target);
result.can_move = ap > 0;
result.can_end_move = result.total_move_ap <= ap;
@ -172,15 +175,17 @@ module TS.SpaceTac {
}
// 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.total_fire_ap = action.getActionPointsUsage(this.ship, target);
result.can_fire = result.total_fire_ap <= ap;
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.success = true;
}
result.success = true;
result.complete = (!result.need_move || result.can_end_move) && (!result.need_fire || result.can_fire);
return result;

View File

@ -241,7 +241,7 @@ module TS.SpaceTac.Specs {
ship.startTurn();
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(action.activated).toBe(true);
@ -252,11 +252,11 @@ module TS.SpaceTac.Specs {
expect(action.activated).toBe(false);
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 ActiveEffectsEvent(ship, [], [], [new AttributeEffect("power_capacity", 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 ActiveEffectsEvent(ship, [], [], []),
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));
shield.action = new ToggleAction(shield, 0, 15, [new AttributeEffect("shield_capacity", 5)]);
battle.playing_ship = ship1;
shield.action.apply(ship1, null);
shield.action.apply(ship1);
expect(ship1.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];
slots.forEach(slot => {
this.listEquipment(slot).forEach(equipment => {
if (equipment.action.code != "nothing") {
if (equipment.action) {
actions.push(equipment.action)
}
});
@ -338,7 +338,7 @@ module TS.SpaceTac {
// Reset toggle actions state
this.listEquipment().forEach(equipment => {
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 () {
it("check if equipment can be used with remaining AP", function () {
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);
var ship = new Ship();
ship.addSlot(SlotType.Hull).attach(equipment);
@ -28,7 +28,7 @@ module TS.SpaceTac {
it("check if equipment can be used with overheat", function () {
let equipment = new Equipment();
let action = new BaseAction("test", "Test", false, equipment);
let action = new BaseAction("test", "Test", equipment);
let ship = new Ship();
expect(action.checkCannotBeApplied(ship)).toBe(null);
@ -71,11 +71,13 @@ module TS.SpaceTac {
TestTools.setShipAP(ship, 10);
let power = ship.listEquipment(SlotType.Power)[0];
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(equipment.wear).toBe(0);
action.apply(ship, null);
action.apply(ship);
expect(power.wear).toBe(1);
expect(equipment.wear).toBe(1);

View File

@ -1,4 +1,20 @@
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.
*
@ -11,40 +27,56 @@ module TS.SpaceTac {
// Human-readable name
name: string
// Boolean at true if the action needs a target
needs_target: boolean
// Equipment that triggers this action
equipment: Equipment | null
// 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.name = name;
this.needs_target = needs_target;
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
*/
getCooldownDuration(estimated = false): number {
if (this.equipment) {
return estimated ? this.equipment.cooldown.cooling : this.equipment.cooldown.heat;
} else {
return 0;
}
let cooldown = this.cooldown;
return estimated ? this.cooldown.cooling : this.cooldown.heat;
}
/**
* Get the number of remaining uses before overheat, infinity if there is no overheat
*/
getUsesBeforeOverheat(): number {
if (this.equipment) {
return this.equipment.cooldown.getRemainingUses();
} else {
return Infinity;
}
return this.cooldown.getRemainingUses();
}
/**
@ -71,7 +103,7 @@ module TS.SpaceTac {
}
// Check cooldown
if (this.equipment && !this.equipment.cooldown.canUse()) {
if (!this.cooldown.canUse()) {
return "overheated";
}
@ -93,40 +125,43 @@ module TS.SpaceTac {
return 0;
}
// Method to check if a target is applicable for this action
// Will call checkLocationTarget or checkShipTarget by default
checkTarget(ship: Ship, target: Target | null): Target | null {
/**
* Check if a target is suitable for this action
*
* Will call checkLocationTarget or checkShipTarget by default
*/
checkTarget(ship: Ship, target: Target): Target | null {
if (this.checkCannotBeApplied(ship)) {
return null;
} else if (target) {
} else {
if (target.ship) {
return this.checkShipTarget(ship, target);
} else {
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
checkLocationTarget(ship: Ship, target: Target): Target | null {
protected checkLocationTarget(ship: Ship, target: Target): Target | 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
checkShipTarget(ship: Ship, target: Target): Target | null {
protected checkShipTarget(ship: Ship, target: Target): Target | 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);
if (reject == null) {
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);
return false;
}
@ -140,10 +175,10 @@ module TS.SpaceTac {
if (this.equipment) {
this.equipment.addWear(1);
ship.listEquipment(SlotType.Power).forEach(equipment => equipment.addWear(1));
this.equipment.cooldown.use();
}
this.cooldown.use();
let battle = ship.getBattle();
if (battle) {
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.name).toEqual("Deploy");
expect(action.equipment).toBe(equipment);
expect(action.needs_target).toBe(true);
});
it("allows to deploy in range", function () {

View File

@ -24,7 +24,7 @@ module TS.SpaceTac {
equipment: Equipment;
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.deploy_distance = deploy_distance;

View File

@ -3,25 +3,26 @@ module TS.SpaceTac.Specs {
it("can't be applied to non-playing ship", () => {
spyOn(console, "warn").and.stub();
var battle = Battle.newQuickRandom();
var action = new EndTurnAction();
let battle = Battle.newQuickRandom();
let action = new EndTurnAction();
expect(action.checkCannotBeApplied(battle.play_order[0])).toBe(null);
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(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", () => {
var battle = Battle.newQuickRandom();
var action = new EndTurnAction();
let battle = Battle.newQuickRandom();
let action = new EndTurnAction();
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(battle.playing_ship_index).toBe(1);
});

View File

@ -2,18 +2,28 @@ module TS.SpaceTac {
// Action to end the ship's turn
export class EndTurnAction extends BaseAction {
constructor() {
super("endturn", "End ship's turn", false);
super("endturn", "End ship's turn");
}
protected customApply(ship: Ship, target: Target) {
ship.endTurn();
if (target.ship == ship) {
ship.endTurn();
let battle = ship.getBattle();
if (battle) {
battle.advanceToNextShip();
let battle = ship.getBattle();
if (battle) {
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 {
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.name).toEqual("Fire");
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 () {
@ -49,35 +45,37 @@ module TS.SpaceTac {
let ship2 = new Ship();
ship2.setArenaPosition(150, 10);
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));
target = weapon.action.checkTarget(ship1, Target.newFromShip(ship2));
target = action.checkTarget(ship1, Target.newFromShip(ship2));
expect(target).toEqual(new Target(150, 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));
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));
});
it("rotates toward the target", function () {
let ship = new Ship();
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);
let result = weapon.action.apply(ship, Target.newFromLocation(10, 20));
let result = action.apply(ship, Target.newFromLocation(10, 20));
expect(result).toBe(true);
expect(ship.arena_angle).toBeCloseTo(1.107, 0.001);
weapon.action.needs_target = false;
result = weapon.action.apply(ship, null);
result = action.apply(ship, Target.newFromShip(ship));
expect(result).toBe(true);
expect(ship.arena_angle).toBeCloseTo(1.107, 0.001);
});

View File

@ -6,22 +6,22 @@ module TS.SpaceTac {
*/
export class FireWeaponAction extends BaseAction {
// Power consumption
power: number;
power: number
// Maximal range of the weapon
range: number
// Blast radius
blast: number;
blast: number
// Effects applied on hit
effects: BaseEffect[];
// Effects applied on target
effects: BaseEffect[]
// Equipment cannot be null
equipment: Equipment;
equipment: Equipment
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.range = range;
@ -29,6 +29,23 @@ module TS.SpaceTac {
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 {
return this.power;
}
@ -51,8 +68,8 @@ module TS.SpaceTac {
}
checkShipTarget(ship: Ship, target: Target): Target | null {
if (target.ship && ship.getPlayer() === target.ship.getPlayer()) {
// No friendly fire
if (this.range > 0 && ship == target.ship) {
// No self fire
return null;
} else {
// Check if target is in range
@ -80,13 +97,10 @@ module TS.SpaceTac {
return result;
}
protected customApply(ship: Ship, target: Target | null) {
if (!target) {
// Self-target
target = Target.newFromShip(ship);
} else {
protected customApply(ship: Ship, target: Target) {
if (arenaDistance(ship.location, target) > 0.000001) {
// 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

View File

@ -14,13 +14,21 @@ module TS.SpaceTac {
maneuvrability_factor: number
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.safety_distance = safety_distance;
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 {
let base = super.checkCannotBeApplied(ship, Infinity);
if (base) {
@ -38,13 +46,13 @@ module TS.SpaceTac {
}
}
getActionPointsUsage(ship: Ship, target: Target): number {
if (target === null) {
getActionPointsUsage(ship: Ship, target: Target | null): number {
if (target) {
let distance = Target.newFromShip(ship).getDistanceTo(target);
return Math.ceil(distance / this.getDistanceByActionPoint(ship));
} else {
return 0;
}
var distance = Target.newFromShip(ship).getDistanceTo(target);
return Math.ceil(distance / this.getDistanceByActionPoint(ship));
}
getRangeRadius(ship: Ship): number {

View File

@ -21,7 +21,7 @@ module TS.SpaceTac {
activated = false
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.radius = radius;
@ -40,6 +40,10 @@ module TS.SpaceTac {
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
*/

View File

@ -63,7 +63,7 @@ module TS.SpaceTac {
}
// 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
*/
private applyAction(action: BaseAction, target: Target | null): boolean {
private applyAction(action: BaseAction, target: Target): boolean {
return action.apply(this.ship, target);
}

View File

@ -12,7 +12,7 @@ module TS.SpaceTac.Specs {
TestTools.setShipHP(ship2, 30, 30);
ship3.setArenaPosition(0, 15);
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([
[ship1, new DamageEffect(50)],
[ship2, new DamageEffect(50)]
@ -43,6 +43,7 @@ module TS.SpaceTac.Specs {
let battle = new Battle();
let ship = battle.fleets[0].addShip();
let engine = TestTools.addEngine(ship, 500);
let move = nn(engine.action);
TestTools.setShipAP(ship, 10);
let drone = new Drone(ship);
drone.effects = [new AttributeEffect("maneuvrability", 1)];
@ -51,11 +52,11 @@ module TS.SpaceTac.Specs {
drone.radius = 50;
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.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.effects).toEqual([[ship, new AttributeEffect("maneuvrability", 1)]]);
});

View File

@ -7,7 +7,7 @@ module TS.SpaceTac.Specs {
constructor(score: number) {
let battle = new Battle();
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;
}
}

View File

@ -17,10 +17,10 @@ module TS.SpaceTac.Specs {
let weapon2 = TestTools.addWeapon(ship0a, 15);
result = imaterialize(TacticalAIHelpers.produceDirectShots(ship0a, battle));
expect(result.length).toBe(4);
expect(result).toContain(new Maneuver(ship0a, weapon1.action, Target.newFromShip(ship1a)));
expect(result).toContain(new Maneuver(ship0a, weapon1.action, Target.newFromShip(ship1b)));
expect(result).toContain(new Maneuver(ship0a, weapon2.action, Target.newFromShip(ship1a)));
expect(result).toContain(new Maneuver(ship0a, weapon2.action, Target.newFromShip(ship1b)));
expect(result).toContain(new Maneuver(ship0a, nn(weapon1.action), Target.newFromShip(ship1a)));
expect(result).toContain(new Maneuver(ship0a, nn(weapon1.action), Target.newFromShip(ship1b)));
expect(result).toContain(new Maneuver(ship0a, nn(weapon2.action), Target.newFromShip(ship1a)));
expect(result).toContain(new Maneuver(ship0a, nn(weapon2.action), Target.newFromShip(ship1b)));
});
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)));
expect(result).toEqual([
new Maneuver(ship, engine.action, Target.newFromLocation(25, 25)),
new Maneuver(ship, engine.action, Target.newFromLocation(75, 25)),
new Maneuver(ship, engine.action, Target.newFromLocation(25, 75)),
new Maneuver(ship, engine.action, Target.newFromLocation(75, 75)),
new Maneuver(ship, nn(engine.action), Target.newFromLocation(25, 25)),
new Maneuver(ship, nn(engine.action), Target.newFromLocation(75, 25)),
new Maneuver(ship, nn(engine.action), Target.newFromLocation(25, 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));
expect(result).toEqual([
new Maneuver(ship, 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)),
new Maneuver(ship, nn(weapon.action), Target.newFromLocation(600, 0)),
]);
let enemy3 = battle.fleets[1].addShip();
@ -77,8 +77,8 @@ module TS.SpaceTac.Specs {
result = imaterialize(TacticalAIHelpers.produceInterestingBlastShots(ship, battle));
expect(result).toEqual([
new Maneuver(ship, 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)),
new Maneuver(ship, nn(weapon.action), Target.newFromLocation(600, 0)),
]);
});
@ -86,29 +86,30 @@ module TS.SpaceTac.Specs {
let battle = new Battle();
let ship = battle.fleets[0].addShip();
let weapon = TestTools.addWeapon(ship, 50, 5, 100);
let action = nn(weapon.action);
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);
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);
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);
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
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
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
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
});
@ -119,18 +120,18 @@ module TS.SpaceTac.Specs {
let engine = TestTools.addEngine(ship, 50);
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);
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);
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);
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);
});
@ -138,6 +139,7 @@ module TS.SpaceTac.Specs {
let battle = new Battle();
let ship = battle.fleets[0].addShip();
let weapon = TestTools.addWeapon(ship, 50, 5, 500, 100);
let action = nn(weapon.action);
let enemy1 = battle.fleets[1].addShip();
enemy1.setArenaPosition(250, 0);
@ -147,15 +149,15 @@ module TS.SpaceTac.Specs {
TestTools.setShipHP(enemy2, 25, 0);
// 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);
// 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);
// 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);
});
@ -166,7 +168,7 @@ module TS.SpaceTac.Specs {
TestTools.setShipAP(ship, 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.y).toBe(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 ship = battle.fleets[0].addShip();
let weapon = TestTools.addWeapon(ship, 1, 1, 400);
let action = nn(weapon.action);
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);
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);
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);
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);
});
@ -211,7 +214,7 @@ module TS.SpaceTac.Specs {
let ship = battle.fleets[0].addShip();
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);
weapon.cooldown.configure(1, 1);

View File

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

View File

@ -45,7 +45,7 @@ module TS.SpaceTac.Equipments {
expect(target.sticky_effects).toEqual([]);
// 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.sticky_effects).toEqual([

View File

@ -41,7 +41,7 @@ module TS.SpaceTac.Equipments {
battle.playing_ship = ship;
battle.play_order = [ship];
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(battle.drones.length).toBe(1);

View File

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

View File

@ -87,10 +87,14 @@ module TS.SpaceTac.UI {
// Initialize
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 {
if (!this.bar.interactive) {
return;
@ -110,28 +114,29 @@ module TS.SpaceTac.UI {
this.bar.actionEnded();
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
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);
if (sprite) {
// 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)) {
this.bar.actionEnded();
}
@ -157,7 +162,7 @@ module TS.SpaceTac.UI {
}
// Update the cooldown status
updateCooldownStatus(): void {
updateCooldownStatus(animate = 300): void {
let remaining = this.action.getUsesBeforeOverheat();
if (this.selected && remaining == 1) {
// 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.cooldown.scale.set(0.7);
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) {
// overheated, show cooldown time
let cooldown = this.action.getCooldownDuration(false);
this.battleview.changeImage(this.cooldown, "battle-actionbar-icon-cooldown");
this.cooldown.scale.set(1);
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) {
this.battleview.changeImage(this.cooldown, "battle-actionbar-icon-toggled");
this.cooldown.scale.set(1);
this.cooldown_count.text = "";
this.battleview.animations.setVisible(this.cooldown, true, 300);
this.battleview.animations.setVisible(this.cooldown, true, animate);
} 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
*/
export class Arena extends Phaser.Group {
export class Arena {
// Link to battleview
battleview: BattleView
view: BattleView
// Boundaries of the arena
boundaries: IBounded = { x: 112, y: 132, width: 1808, height: 948 }
@ -32,6 +32,7 @@ module TS.SpaceTac.UI {
private playing: ArenaShip | null
// Layer for particles
container: Phaser.Group
layer_garbage: Phaser.Group
layer_hints: Phaser.Group
layer_drones: Phaser.Group
@ -39,79 +40,101 @@ module TS.SpaceTac.UI {
layer_weapon_effects: Phaser.Group
layer_targetting: Phaser.Group
// Create a graphical arena for ship sprites to fight in a 2D space
constructor(battleview: BattleView) {
super(battleview.game);
// Callbacks to receive cursor events
callbacks_hover: ((location: ArenaLocation | null, ship: Ship | null) => void)[] = []
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.hovered = null;
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
*/
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);
this.mouse_capture = background;
// Capture clicks on background
background.onInputUp.add(() => {
battleview.cursorClicked();
this.callbacks_click.forEach(callback => callback());
});
background.onInputOut.add(() => {
battleview.targetting.setTarget(null);
this.callbacks_hover.forEach(callback => callback(null, null));
});
// 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();
if (battleview.game.input.hitTest(background, pointer, point)) {
battleview.cursorInSpace(point.x * background.scale.x, point.y * background.scale.y);
if (view.input.hitTest(background, pointer, point)) {
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);
this.add(this.mouse_capture);
}
destroy() {
if (this.input_callback) {
this.game.input.deleteMoveCallback(this.input_callback);
this.input_callback = null;
}
super.destroy();
this.container.add(this.mouse_capture);
}
/**
* Initialize state (create sprites)
* Get the ship under a cursor location
*/
init(): void {
this.setupMouseCapture();
getShip(location: ArenaLocation): Ship | null {
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));
this.layer_drones = this.add(new Phaser.Group(this.game));
this.layer_ships = this.add(new Phaser.Group(this.game));
this.layer_weapon_effects = this.add(new Phaser.Group(this.game));
this.layer_targetting = this.add(new Phaser.Group(this.game));
this.range_hint.setLayer(this.layer_hints);
this.addShipSprites();
/**
* Call when the arena is destroyed to properly remove input handlers
*/
destroy() {
if (this.input_callback) {
this.view.input.deleteMoveCallback(this.input_callback);
this.input_callback = null;
}
}
/**
* Add the sprites for all ships
*/
addShipSprites() {
iforeach(this.battleview.battle.iships(), ship => {
iforeach(this.view.battle.iships(), ship => {
let sprite = new ArenaShip(this, ship);
this.layer_ships.add(sprite);
this.ship_sprites.push(sprite);
@ -129,16 +152,18 @@ module TS.SpaceTac.UI {
return base.filter(ship => arenaInside(ship, area, border_inclusive));
}
// Get the current MainUI instance
getGame(): MainUI {
return this.battleview.gameui;
/**
* Get the current MainUI instance
*/
get game(): MainUI {
return this.view.gameui;
}
/**
* Get the current battle displayed
*/
getBattle(): Battle {
return this.battleview.battle;
return this.view.battle;
}
// Remove a ship sprite
@ -210,7 +235,7 @@ module TS.SpaceTac.UI {
*/
addDrone(drone: Drone, animate = true): number {
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);
this.layer_drones.add(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.sprite.rotation = drone.owner.arena_angle;
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;
} else {
@ -254,9 +279,9 @@ module TS.SpaceTac.UI {
setTacticalMode(active: boolean): void {
this.ship_sprites.forEach(sprite => sprite.setHovered(active, true));
this.drone_sprites.forEach(drone => drone.setTacticalMode(active));
this.battleview.animations.setVisible(this.layer_garbage, !active, 200);
if (this.battleview.background) {
this.battleview.animations.setVisible(this.battleview.background, !active, 200);
this.view.animations.setVisible(this.layer_garbage, !active, 200);
if (this.view.background) {
this.view.animations.setVisible(this.view.background, !active, 200);
}
}

View File

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

View File

@ -4,78 +4,66 @@ module TS.SpaceTac.UI.Specs {
describe("BattleView", function () {
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;
expect(battleview.targetting.active).toBe(false);
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);
battleview.enterTargettingMode(weapon.action);
battleview.enterTargettingMode(nn(weapon.action), ActionTargettingMode.SPACE);
expect(battleview.targetting.active).toBe(true);
// Forward selection in space
battleview.cursorInSpace(8, 4);
battleview.cursorHovered(new ArenaLocation(5, 8), null);
expect(battleview.targetting.target).toEqual(Target.newFromLocation(5, 8));
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();
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();
expect(battleview.targetting.active).toBe(false);
// Events process normally
battleview.cursorInSpace(8, 4);
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();
battleview.cursorHovered(new ArenaLocation(5, 8), null);
expect(battleview.targetting.target).toBeNull();
});
it("allows to choose an action and a target with shortcut keys", function () {
let battleview = testgame.battleview;
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.action_bar.hasActionSelected()).toBe(false);
@ -83,7 +71,7 @@ module TS.SpaceTac.UI.Specs {
expect(battleview.action_bar.hasActionSelected()).toBe(true);
expect(battleview.targetting.active).toBe(true);
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);
expect(battleview.targetting.active).toBe(true);
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.layer_background.add(this.background);
// Add arena (local map)
this.arena = new Arena(this);
this.layer_arena.add(this.arena);
// Add arena (local battlefield map)
this.arena = new Arena(this, this.layer_arena);
this.arena.callbacks_hover.push(bound(this, "cursorHovered"));
this.arena.callbacks_click.push(bound(this, "cursorClicked"));
// Add UI elements
this.action_bar = new ActionBar(this);
@ -106,7 +107,7 @@ module TS.SpaceTac.UI {
this.layer_sheets.add(this.character_sheet);
// 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);
// 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 {
if (!this.targetting.active || ship.alive) {
if (ship.alive) {
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 {
if (this.ship_hovered === ship) {
this.setShipHovered(null);
}
}
// Method called when cursor moves in space
cursorInSpace(x: number, y: number): void {
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)
/**
* Method called when cursor has been clicked (in space or on a ship)
*/
cursorClicked(): void {
if (this.targetting.active) {
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 {
this.ship_hovered = ship;
this.arena.setShipHovered(ship);
@ -222,14 +238,6 @@ module TS.SpaceTac.UI {
} else {
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
@ -248,12 +256,12 @@ module TS.SpaceTac.UI {
// Enter targetting mode
// 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) {
return null;
}
this.targetting.setAction(action);
this.targetting.setAction(action, mode);
return this.targetting;
}

View File

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

View File

@ -2,8 +2,15 @@ module TS.SpaceTac.UI.Specs {
describe("Targetting", function () {
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 () {
let targetting = new Targetting(testgame.battleview, testgame.battleview.action_bar, new Toggle());
let targetting = newTargetting();
let ship = nn(testgame.battleview.battle.playing_ship);
ship.setArenaPosition(10, 20);
@ -14,7 +21,7 @@ module TS.SpaceTac.UI.Specs {
let drawvector = spyOn(targetting, "drawVector").and.stub();
let part = {
action: weapon.action,
action: nn(weapon.action),
target: new Target(50, 30),
ap: 5,
possible: true
@ -27,15 +34,15 @@ module TS.SpaceTac.UI.Specs {
expect(drawvector).toHaveBeenCalledTimes(2);
expect(drawvector).toHaveBeenCalledWith(0x8e8e8e, 10, 20, 50, 30, 0);
targetting.setAction(engine.action);
part.action = engine.action;
targetting.action = engine.action;
part.action = nn(engine.action);
targetting.drawPart(part, true, null);
expect(drawvector).toHaveBeenCalledTimes(3);
expect(drawvector).toHaveBeenCalledWith(0xe09c47, 10, 20, 50, 30, 12);
});
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 collect = spyOn(testgame.battleview.battle, "collectShipsInCircle").and.returnValues(
@ -71,7 +78,7 @@ module TS.SpaceTac.UI.Specs {
});
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 engine = TestTools.addEngine(ship, 8000);
@ -90,8 +97,8 @@ module TS.SpaceTac.UI.Specs {
result.need_fire = true;
result.can_fire = true;
result.parts = [
{ action: 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(engine.action), target: Target.newFromLocation(80, 20), ap: 1, possible: true },
{ action: nn(weapon.action), target: Target.newFromLocation(156, 65), ap: 5, possible: true }
]
targetting.simulation = result;
});

View File

@ -12,6 +12,7 @@ module TS.SpaceTac.UI {
ship: Ship | null = null
action: BaseAction | null = null
target: Target | null = null
mode: ActionTargettingMode
simulation = new MoveFireResult()
// Movement projector
@ -25,15 +26,17 @@ module TS.SpaceTac.UI {
// Collaborators to update
actionbar: ActionBar
range_hint: RangeHint
tactical_mode: ToggleClient
// Access to the parent view
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.actionbar = actionbar;
this.tactical_mode = tactical_mode.manipulate("targetting");
this.range_hint = range_hint;
this.container = view.add.group();
@ -155,6 +158,9 @@ module TS.SpaceTac.UI {
this.fire_arrow.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) {
let previous: MoveFirePart | null = null;
simulation.parts.forEach(part => {
@ -162,9 +168,6 @@ module TS.SpaceTac.UI {
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) {
this.move_ghost.visible = true;
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_arrow.visible = false;
}
this.container.visible = true;
} else {
// TODO Display error
this.container.visible = false;
this.drawVector(0x888888, this.ship.arena_x, this.ship.arena_y, this.target.x, this.target.y);
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 {
this.container.visible = false;
}
// Toggle tactical mode
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
*/
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) {
this.ship = action.equipment.attached_to.ship;
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.move_ghost.scale.set(0.4);
this.setTarget(action.getDefaultTarget(this.ship));
} else {
this.ship = 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
constructor(arena: Arena, ship: Ship, target: Target, weapon: Equipment) {
this.ui = arena.getGame();
this.ui = arena.game;
this.arena = arena;
this.view = arena.battleview;
this.timer = arena.battleview.timer;
this.view = arena.view;
this.timer = arena.view.timer;
this.layer = arena.layer_weapon_effects;
this.ship = ship;
this.target = target;
@ -161,7 +161,7 @@ module TS.SpaceTac.UI {
missile.rotation = arenaAngle(this.source, this.destination);
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 tween = this.ui.tweens.create(missile);