diff --git a/TODO.md b/TODO.md index 47e8dc5..227a032 100644 --- a/TODO.md +++ b/TODO.md @@ -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 diff --git a/graphics/exported/battle/hud/simulator-failed.png b/graphics/exported/battle/hud/simulator-failed.png new file mode 100644 index 0000000..e003bc0 Binary files /dev/null and b/graphics/exported/battle/hud/simulator-failed.png differ diff --git a/out/assets/images/battle/arena/indicators.xcf b/graphics/layered/indicators.xcf similarity index 73% rename from out/assets/images/battle/arena/indicators.xcf rename to graphics/layered/indicators.xcf index d7688dc..7c669fc 100644 Binary files a/out/assets/images/battle/arena/indicators.xcf and b/graphics/layered/indicators.xcf differ diff --git a/src/core/BattleStats.spec.ts b/src/core/BattleStats.spec.ts index ac04036..6d019e7 100644 --- a/src/core/BattleStats.spec.ts +++ b/src/core/BattleStats.spec.ts @@ -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] }); }) diff --git a/src/core/Equipment.ts b/src/core/Equipment.ts index 0546dda..adb4e98 100644 --- a/src/core/Equipment.ts +++ b/src/core/Equipment.ts @@ -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"; diff --git a/src/core/MoveFireSimulator.spec.ts b/src/core/MoveFireSimulator.spec.ts index cc00b0b..0442ec3 100644 --- a/src/core/MoveFireSimulator.spec.ts +++ b/src/core/MoveFireSimulator.spec.ts @@ -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([]); diff --git a/src/core/MoveFireSimulator.ts b/src/core/MoveFireSimulator.ts index 09d1b2b..f8a16ed 100644 --- a/src/core/MoveFireSimulator.ts +++ b/src/core/MoveFireSimulator.ts @@ -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; diff --git a/src/core/Ship.spec.ts b/src/core/Ship.spec.ts index 2400c16..43ea025 100644 --- a/src/core/Ship.spec.ts +++ b/src/core/Ship.spec.ts @@ -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); diff --git a/src/core/Ship.ts b/src/core/Ship.ts index 4e11137..c39f9d6 100644 --- a/src/core/Ship.ts +++ b/src/core/Ship.ts @@ -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); } }); } diff --git a/src/core/actions/BaseAction.spec.ts b/src/core/actions/BaseAction.spec.ts index 753e2bc..0e94fd8 100644 --- a/src/core/actions/BaseAction.spec.ts +++ b/src/core/actions/BaseAction.spec.ts @@ -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); diff --git a/src/core/actions/BaseAction.ts b/src/core/actions/BaseAction.ts index 99f28ca..37d6fff 100644 --- a/src/core/actions/BaseAction.ts +++ b/src/core/actions/BaseAction.ts @@ -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 { } /** diff --git a/src/core/actions/DeployDroneAction.spec.ts b/src/core/actions/DeployDroneAction.spec.ts index 4e04573..b7d45d3 100644 --- a/src/core/actions/DeployDroneAction.spec.ts +++ b/src/core/actions/DeployDroneAction.spec.ts @@ -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 () { diff --git a/src/core/actions/DeployDroneAction.ts b/src/core/actions/DeployDroneAction.ts index 71b7195..f2d03b9 100644 --- a/src/core/actions/DeployDroneAction.ts +++ b/src/core/actions/DeployDroneAction.ts @@ -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; diff --git a/src/core/actions/EndTurnAction.spec.ts b/src/core/actions/EndTurnAction.spec.ts index 9920341..de408b9 100644 --- a/src/core/actions/EndTurnAction.spec.ts +++ b/src/core/actions/EndTurnAction.spec.ts @@ -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); }); diff --git a/src/core/actions/EndTurnAction.ts b/src/core/actions/EndTurnAction.ts index edf4ab3..b7a9ccf 100644 --- a/src/core/actions/EndTurnAction.ts +++ b/src/core/actions/EndTurnAction.ts @@ -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."; } diff --git a/src/core/actions/FireWeaponAction.spec.ts b/src/core/actions/FireWeaponAction.spec.ts index 7e5b332..5482ef1 100644 --- a/src/core/actions/FireWeaponAction.spec.ts +++ b/src/core/actions/FireWeaponAction.spec.ts @@ -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); }); diff --git a/src/core/actions/FireWeaponAction.ts b/src/core/actions/FireWeaponAction.ts index 5c535b7..5be3244 100644 --- a/src/core/actions/FireWeaponAction.ts +++ b/src/core/actions/FireWeaponAction.ts @@ -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 diff --git a/src/core/actions/MoveAction.ts b/src/core/actions/MoveAction.ts index c64fe0f..bbe7685 100644 --- a/src/core/actions/MoveAction.ts +++ b/src/core/actions/MoveAction.ts @@ -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 { diff --git a/src/core/actions/ToggleAction.ts b/src/core/actions/ToggleAction.ts index ef8996b..b12d498 100644 --- a/src/core/actions/ToggleAction.ts +++ b/src/core/actions/ToggleAction.ts @@ -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 */ diff --git a/src/core/ai/AbstractAI.ts b/src/core/ai/AbstractAI.ts index f74f463..dbe2be4 100644 --- a/src/core/ai/AbstractAI.ts +++ b/src/core/ai/AbstractAI.ts @@ -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); } diff --git a/src/core/ai/Maneuver.spec.ts b/src/core/ai/Maneuver.spec.ts index c871ac5..d1a7035 100644 --- a/src/core/ai/Maneuver.spec.ts +++ b/src/core/ai/Maneuver.spec.ts @@ -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)]]); }); diff --git a/src/core/ai/TacticalAI.spec.ts b/src/core/ai/TacticalAI.spec.ts index ac44272..ed0573b 100644 --- a/src/core/ai/TacticalAI.spec.ts +++ b/src/core/ai/TacticalAI.spec.ts @@ -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; } } diff --git a/src/core/ai/TacticalAIHelpers.spec.ts b/src/core/ai/TacticalAIHelpers.spec.ts index 39e626d..74b54a9 100644 --- a/src/core/ai/TacticalAIHelpers.spec.ts +++ b/src/core/ai/TacticalAIHelpers.spec.ts @@ -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); diff --git a/src/core/equipments/DamageProtector.spec.ts b/src/core/equipments/DamageProtector.spec.ts index 3a91564..fe6ccf2 100644 --- a/src/core/equipments/DamageProtector.spec.ts +++ b/src/core/equipments/DamageProtector.spec.ts @@ -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((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((protector.action).activated).toBe(false); diff --git a/src/core/equipments/PowerDepleter.spec.ts b/src/core/equipments/PowerDepleter.spec.ts index 2dc2f17..804544e 100644 --- a/src/core/equipments/PowerDepleter.spec.ts +++ b/src/core/equipments/PowerDepleter.spec.ts @@ -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([ diff --git a/src/core/equipments/RepairDrone.spec.ts b/src/core/equipments/RepairDrone.spec.ts index 88632e1..8c1b466 100644 --- a/src/core/equipments/RepairDrone.spec.ts +++ b/src/core/equipments/RepairDrone.spec.ts @@ -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); diff --git a/src/core/equipments/SubMunitionMissile.spec.ts b/src/core/equipments/SubMunitionMissile.spec.ts index 99d1004..90d1a15 100644 --- a/src/core/equipments/SubMunitionMissile.spec.ts +++ b/src/core/equipments/SubMunitionMissile.spec.ts @@ -44,7 +44,7 @@ module TS.SpaceTac.Equipments { var template = new Equipments.SubMunitionMissile(); var equipment = template.generate(1); - let action = equipment.action; + let action = nn(equipment.action); action.range = 5; action.blast = 1.5; (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)); }); }); diff --git a/src/ui/battle/ActionIcon.ts b/src/ui/battle/ActionIcon.ts index 450b17c..57ba160 100644 --- a/src/ui/battle/ActionIcon.ts +++ b/src/ui/battle/ActionIcon.ts @@ -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); } } diff --git a/src/ui/battle/Arena.ts b/src/ui/battle/Arena.ts index bf9141e..4c1ae97 100644 --- a/src/ui/battle/Arena.ts +++ b/src/ui/battle/Arena.ts @@ -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); } } diff --git a/src/ui/battle/ArenaShip.ts b/src/ui/battle/ArenaShip.ts index ef7090d..025e0b0 100644 --- a/src/ui/battle/ArenaShip.ts +++ b/src/ui/battle/ArenaShip.ts @@ -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)); diff --git a/src/ui/battle/BattleView.spec.ts b/src/ui/battle/BattleView.spec.ts index 3b792da..f40889e 100644 --- a/src/ui/battle/BattleView.spec.ts +++ b/src/ui/battle/BattleView.spec.ts @@ -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); diff --git a/src/ui/battle/BattleView.ts b/src/ui/battle/BattleView.ts index a982eb0..1327e6c 100644 --- a/src/ui/battle/BattleView.ts +++ b/src/ui/battle/BattleView.ts @@ -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; } diff --git a/src/ui/battle/RangeHint.ts b/src/ui/battle/RangeHint.ts index 5201449..eb36e5b 100644 --- a/src/ui/battle/RangeHint.ts +++ b/src/ui/battle/RangeHint.ts @@ -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; diff --git a/src/ui/battle/Targetting.spec.ts b/src/ui/battle/Targetting.spec.ts index 2733c27..27c05eb 100644 --- a/src/ui/battle/Targetting.spec.ts +++ b/src/ui/battle/Targetting.spec.ts @@ -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; }); diff --git a/src/ui/battle/Targetting.ts b/src/ui/battle/Targetting.ts index 3090701..3754bac 100644 --- a/src/ui/battle/Targetting.ts +++ b/src/ui/battle/Targetting.ts @@ -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(); } /** diff --git a/src/ui/battle/WeaponEffect.ts b/src/ui/battle/WeaponEffect.ts index 6e38369..3a03437 100644 --- a/src/ui/battle/WeaponEffect.ts +++ b/src/ui/battle/WeaponEffect.ts @@ -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);