diff --git a/TODO b/TODO index 636391f..1635399 100644 --- a/TODO +++ b/TODO @@ -21,7 +21,6 @@ * Menu: allow to delete cloud saves * Arena: display effects description instead of attribute changes * Arena: display radius for area effects (both on action hover, and while action is active) -* Arena: add auto-move to attack * Arena: fix effects originating from real ship location instead of current sprite (when AI fires then moves) * Arena: add engine trail * Fix capacity limit effect not refreshing associated value (for example, on "limit power capacity to 3", potential "power" value change is not broadcast) diff --git a/graphics/ui/battle.svg b/graphics/ui/battle.svg index 4c89536..c872b4b 100644 --- a/graphics/ui/battle.svg +++ b/graphics/ui/battle.svg @@ -235,18 +235,6 @@ offset="1" id="stop10127" /> - - - - @@ -710,16 +698,6 @@ x2="1512.2041" y2="877.88531" gradientUnits="userSpaceOnUse" /> - - @@ -1548,6 +1516,12 @@ cx="1551.4003" cy="742.08289" r="31.144533" /> + - - - - - - - - - - - - - - - - + inkscape:export-ydpi="90" /> - + + { - return ichainit(imap(istep(0, irepeat(nr ? 1 / (nr - 1) : 0, nr - 1)), r => { + let rcount = nr ? 1 / (nr - 1) : 0; + return ichainit(imap(istep(0, irepeat(rcount, nr - 1)), r => { let angles = Math.max(1, Math.ceil(na * r)); return imap(istep(0, irepeat(2 * Math.PI / angles, angles - 1)), a => { return new Target(x + r * radius * Math.cos(a), y + r * radius * Math.sin(a)) @@ -125,8 +128,10 @@ module TS.SpaceTac { // Move or approach needed ? let move_target: Target | null = null; + result.move_location = Target.newFromShip(this.ship); if (action instanceof MoveAction) { - let corrected_target = action.applyExclusion(this.ship, target); + let corrected_target = action.applyReachableRange(this.ship, target, move_margin); + corrected_target = action.applyExclusion(this.ship, corrected_target, move_margin); if (corrected_target) { result.need_move = target.getDistanceTo(this.ship.location) > 0; move_target = corrected_target; @@ -174,7 +179,9 @@ module TS.SpaceTac { 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.complete = (!result.need_move || result.can_end_move) && (!result.need_fire || result.can_fire); return result; } diff --git a/src/core/actions/MoveAction.spec.ts b/src/core/actions/MoveAction.spec.ts index 2987c8a..c1b7610 100644 --- a/src/core/actions/MoveAction.spec.ts +++ b/src/core/actions/MoveAction.spec.ts @@ -17,7 +17,7 @@ module TS.SpaceTac { expect(result).toEqual(Target.newFromLocation(0, 2)); result = action.checkTarget(ship, Target.newFromLocation(0, 8)); - expect(result).toEqual(Target.newFromLocation(0, 3)); + expect(result).toEqual(Target.newFromLocation(0, 2.9)); ship.values.power.set(0); result = action.checkTarget(ship, Target.newFromLocation(0, 8)); diff --git a/src/core/actions/MoveAction.ts b/src/core/actions/MoveAction.ts index 3c8fb0a..4b7dd73 100644 --- a/src/core/actions/MoveAction.ts +++ b/src/core/actions/MoveAction.ts @@ -57,7 +57,7 @@ module TS.SpaceTac { /** * Apply exclusion areas (neer arena borders, or other ships) */ - applyExclusion(ship: Ship, target: Target): Target { + applyExclusion(ship: Ship, target: Target, margin = 0.1): Target { let battle = ship.getBattle(); if (battle) { // Keep out of arena borders @@ -76,14 +76,18 @@ module TS.SpaceTac { return target; } + /** + * Apply reachable range, with remaining power + */ + applyReachableRange(ship: Ship, target: Target, margin = 0.1): Target { + let max_distance = this.getRangeRadius(ship); + max_distance = Math.max(0, max_distance - margin); + return target.constraintInRange(ship.arena_x, ship.arena_y, max_distance); + } + checkLocationTarget(ship: Ship, target: Target): Target { - // Apply maximal distance - var max_distance = this.getRangeRadius(ship); - target = target.constraintInRange(ship.arena_x, ship.arena_y, max_distance); - - // Apply exclusion areas + target = this.applyReachableRange(ship, target); target = this.applyExclusion(ship, target); - return target; } diff --git a/src/core/ai/Maneuver.spec.ts b/src/core/ai/Maneuver.spec.ts index 189b958..c871ac5 100644 --- a/src/core/ai/Maneuver.spec.ts +++ b/src/core/ai/Maneuver.spec.ts @@ -42,7 +42,8 @@ module TS.SpaceTac.Specs { it("guesses area effects on final location", function () { let battle = new Battle(); let ship = battle.fleets[0].addShip(); - TestTools.addEngine(ship, 500); + let engine = TestTools.addEngine(ship, 500); + TestTools.setShipAP(ship, 10); let drone = new Drone(ship); drone.effects = [new AttributeEffect("maneuvrability", 1)]; drone.x = 100; @@ -50,11 +51,11 @@ module TS.SpaceTac.Specs { drone.radius = 50; battle.addDrone(drone); - let maneuver = new Maneuver(ship, new MoveAction(new Equipment()), Target.newFromLocation(40, 30)); + let maneuver = new Maneuver(ship, engine.action, Target.newFromLocation(40, 30)); expect(maneuver.getFinalLocation()).toEqual(jasmine.objectContaining({ x: 40, y: 30 })); expect(maneuver.effects).toEqual([]); - maneuver = new Maneuver(ship, new MoveAction(new Equipment()), Target.newFromLocation(100, 30)); + maneuver = new Maneuver(ship, engine.action, 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/ui/BaseView.ts b/src/ui/BaseView.ts index 6c1eb9d..999839c 100644 --- a/src/ui/BaseView.ts +++ b/src/ui/BaseView.ts @@ -50,6 +50,7 @@ module TS.SpaceTac.UI { create() { // Phaser config this.game.stage.backgroundColor = 0x000000; + this.game.stage.disableVisibilityChange = this.gameui.headless; this.scale.scaleMode = Phaser.ScaleManager.SHOW_ALL; this.input.maxPointers = 1; diff --git a/src/ui/Preload.ts b/src/ui/Preload.ts index 2fb69fa..adb91c5 100644 --- a/src/ui/Preload.ts +++ b/src/ui/Preload.ts @@ -35,10 +35,10 @@ module TS.SpaceTac.UI { this.loadImage("battle/actionbar/action-endturn.png"); this.loadSheet("battle/actionbar/button-menu.png", 79, 132); this.loadImage("battle/arena/background.png"); - this.loadImage("battle/arena/ap-indicator.png"); this.loadImage("battle/arena/blast.png"); this.loadSheet("battle/arena/gauges.png", 19, 93); this.loadSheet("battle/arena/small-indicators.png", 10, 10); + this.loadSheet("battle/arena/indicators.png", 64, 64); this.loadSheet("battle/arena/ship-frames.png", 70, 70); this.loadImage("battle/shiplist/background.png"); this.loadImage("battle/shiplist/item-background.png"); diff --git a/src/ui/battle/ActionBar.spec.ts b/src/ui/battle/ActionBar.spec.ts index 7cf90f2..9067a46 100644 --- a/src/ui/battle/ActionBar.spec.ts +++ b/src/ui/battle/ActionBar.spec.ts @@ -49,51 +49,45 @@ module TS.SpaceTac.UI.Specs { expect(bar.action_icons.length).toBe(4); - var checkFading = (fading: number[], available: number[]) => { + var checkFading = (fading: number[], available: number[], message: string) => { fading.forEach((index: number) => { var icon = bar.action_icons[index]; - expect(icon.fading || !icon.active).toBe(true); + expect(icon.fading || !icon.active).toBe(true, `${message} - ${index} should be fading`); }); available.forEach((index: number) => { var icon = bar.action_icons[index]; - expect(icon.fading).toBe(false); + expect(icon.fading).toBe(false, `${message} - ${index} should be available`); }); }; - // Weapon 1 leaves all choices open - bar.action_icons[1].processClick(); - checkFading([], [0, 1, 2, 3]); + bar.updateSelectedActionPower(3, 0, bar.action_icons[1].action); + checkFading([], [0, 1, 2, 3], "Weapon 1 leaves all choices open"); bar.actionEnded(); - // Weapon 2 can't be fired twice - bar.action_icons[2].processClick(); - checkFading([2], [0, 1, 3]); + bar.updateSelectedActionPower(5, 0, bar.action_icons[2].action); + checkFading([2], [0, 1, 3], "Weapon 2 can't be fired twice"); bar.actionEnded(); - // Not enough AP for both weapons ship.setValue("power", 7); - bar.action_icons[2].processClick(); - checkFading([1, 2], [0, 3]); + bar.updateSelectedActionPower(5, 0, bar.action_icons[2].action); + checkFading([1, 2], [0, 3], "Not enough AP for both weapons"); bar.actionEnded(); - // Not enough AP to move ship.setValue("power", 3); - bar.action_icons[1].processClick(); - checkFading([0, 1, 2], [3]); + bar.updateSelectedActionPower(3, 0, bar.action_icons[1].action); + checkFading([0, 1, 2], [3], "Not enough AP to move"); bar.actionEnded(); // Dynamic AP usage for move actions ship.setValue("power", 6); - bar.action_icons[0].processClick(); - checkFading([], [0, 1, 2, 3]); - bar.action_icons[0].processHover(Target.newFromLocation(2, 8)); - checkFading([2], [0, 1, 3]); - bar.action_icons[0].processHover(Target.newFromLocation(3, 8)); - checkFading([1, 2], [0, 3]); - bar.action_icons[0].processHover(Target.newFromLocation(4, 8)); - checkFading([0, 1, 2], [3]); - bar.action_icons[0].processHover(Target.newFromLocation(5, 8)); - checkFading([0, 1, 2], [3]); + bar.updateSelectedActionPower(2, 0, bar.action_icons[0].action); + checkFading([2], [0, 1, 3], "2 move power used"); + bar.updateSelectedActionPower(4, 0, bar.action_icons[0].action); + checkFading([1, 2], [0, 3], "4 move power used"); + bar.updateSelectedActionPower(6, 0, bar.action_icons[0].action); + checkFading([0, 1, 2], [3], "6 move power used"); + bar.updateSelectedActionPower(8, 0, bar.action_icons[0].action); + checkFading([0, 1, 2], [3], "8 move power used"); bar.actionEnded(); }); diff --git a/src/ui/battle/ActionBar.ts b/src/ui/battle/ActionBar.ts index b4c72c4..9ba5a9b 100644 --- a/src/ui/battle/ActionBar.ts +++ b/src/ui/battle/ActionBar.ts @@ -150,7 +150,7 @@ module TS.SpaceTac.UI { /** * Update the power indicator */ - updatePower(selected_action = 0): void { + updatePower(move_power = 0, fire_power = 0): void { let current_power = this.power.children.length; let power_capacity = this.ship_power_capacity; @@ -162,14 +162,16 @@ module TS.SpaceTac.UI { } let power_value = this.ship_power_value; - let remaining_power = power_value - selected_action; + let remaining_power = power_value - move_power - fire_power; this.power.children.forEach((obj, idx) => { let img = obj; let frame: number; if (idx < remaining_power) { frame = 0; - } else if (idx < power_value) { + } else if (idx < remaining_power + move_power) { frame = 2; + } else if (idx < power_value) { + frame = 3; } else { frame = 1; } @@ -179,23 +181,34 @@ module TS.SpaceTac.UI { } /** - * Set current action power usage. + * Temporarily set current action power usage. * * When an action is selected, this will fade the icons not available after the action would be done. * This will also highlight power usage in the power bar. * - * *power_usage* is the consumption of currently selected action. + * *move_power* and *fire_power* is the consumption of currently selected action/target. */ - updateSelectedActionPower(power_usage: number, action: BaseAction): void { - var remaining_ap = this.ship ? (this.ship.values.power.get() - power_usage) : 0; + updateSelectedActionPower(move_power: number, fire_power: number, action: BaseAction): void { + var remaining_ap = this.ship ? (this.ship.getValue("power") - move_power - fire_power) : 0; if (remaining_ap < 0) { remaining_ap = 0; } - this.action_icons.forEach((icon: ActionIcon) => { + this.action_icons.forEach(icon => { icon.updateFadingStatus(remaining_ap, action); }); - this.updatePower(power_usage); + this.updatePower(move_power, fire_power); + } + + /** + * Temporarily set power status for a given move-fire simulation + */ + updateFromSimulation(action: BaseAction, simulation: MoveFireResult) { + if (simulation.complete) { + this.updateSelectedActionPower(simulation.total_move_ap, simulation.total_fire_ap, action); + } else { + this.updateSelectedActionPower(0, 0, action); + } } /** diff --git a/src/ui/battle/ActionIcon.ts b/src/ui/battle/ActionIcon.ts index a00b1cf..362ce9c 100644 --- a/src/ui/battle/ActionIcon.ts +++ b/src/ui/battle/ActionIcon.ts @@ -40,7 +40,7 @@ module TS.SpaceTac.UI { // Create an icon for a single ship action constructor(bar: ActionBar, x: number, y: number, ship: Ship, action: BaseAction, position: number) { - super(bar.game, x, y, "battle-actionbar-icon"); + super(bar.game, x, y, "battle-actionbar-icon", () => this.processClick()); this.bar = bar; this.battleview = bar.battleview; @@ -83,19 +83,6 @@ module TS.SpaceTac.UI { ActionTooltip.fill(filler, this.ship, this.action, position); return true; }); - UITools.setHoverClick(this, - () => { - if (!this.bar.hasActionSelected()) { - this.battleview.arena.range_hint.update(this.ship, this.action); - } - }, - () => { - if (!this.bar.hasActionSelected()) { - this.battleview.arena.range_hint.clear(); - } - }, - () => this.processClick() - ); // Initialize this.updateActiveStatus(true); @@ -120,13 +107,10 @@ module TS.SpaceTac.UI { this.bar.actionStarted(); // Update range hint - if (this.battleview.arena.range_hint) { + if (this.battleview.arena.range_hint && this.action instanceof MoveAction) { this.battleview.arena.range_hint.update(this.ship, this.action); } - // Update fading statuses - this.bar.updateSelectedActionPower(this.action.getActionPointsUsage(this.ship, null), this.action); - // Set the selected state this.setSelected(true); @@ -134,15 +118,7 @@ module TS.SpaceTac.UI { 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(); - if (this.targetting) { - this.targetting.setSource(sprite); - this.targetting.targetSelected.add(this.processSelection, this); - this.targetting.targetHovered.add(this.processHover, this); - if (this.action instanceof MoveAction) { - this.targetting.setApIndicatorsInterval(this.action.getDistanceByActionPoint(this.ship)); - } - } + this.targetting = this.battleview.enterTargettingMode(this.action); } } else { // No target needed, apply action immediately @@ -150,16 +126,6 @@ module TS.SpaceTac.UI { } } - // Called when a target is hovered - // This will check the target against current action and adjust it if needed - processHover(target: Target): void { - let correct_target = this.action.checkTarget(this.ship, target); - if (this.targetting) { - this.targetting.setTarget(correct_target, false, this.action.getBlastRadius(this.ship)); - } - this.bar.updateSelectedActionPower(this.action.getActionPointsUsage(this.ship, correct_target), this.action); - } - // Called when a target is selected processSelection(target: Target | null): void { if (this.action.apply(this.ship, target)) { diff --git a/src/ui/battle/Arena.ts b/src/ui/battle/Arena.ts index 9fcbd07..1cbdaf9 100644 --- a/src/ui/battle/Arena.ts +++ b/src/ui/battle/Arena.ts @@ -67,6 +67,9 @@ module TS.SpaceTac.UI { background.onInputUp.add(() => { battleview.cursorClicked(); }); + background.onInputOut.add(() => { + battleview.targetting.setTarget(null); + }); // Watch mouse move to capture hovering over background this.input_callback = this.game.input.addMoveCallback((pointer: Phaser.Pointer) => { @@ -234,13 +237,6 @@ module TS.SpaceTac.UI { } } - /** - * Highlight ships that would be the target of current action - */ - highlightTargets(ships: Ship[]): void { - this.ship_sprites.forEach(sprite => sprite.setTargetted(contains(ships, sprite.ship))); - } - /** * Switch the tactical mode (shows information on all ships, and fades background) */ diff --git a/src/ui/battle/ArenaShip.ts b/src/ui/battle/ArenaShip.ts index 10f0770..62632af 100644 --- a/src/ui/battle/ArenaShip.ts +++ b/src/ui/battle/ArenaShip.ts @@ -17,9 +17,6 @@ module TS.SpaceTac.UI { // Statis effect stasis: Phaser.Image - // Target effect - target: Phaser.Image - // HSP display hull: ValueBar toggle_hull: Toggle @@ -53,7 +50,7 @@ module TS.SpaceTac.UI { this.sprite = new Phaser.Button(this.game, 0, 0, "ship-" + ship.model.code + "-sprite"); this.sprite.rotation = ship.arena_angle; this.sprite.anchor.set(0.5, 0.5); - this.sprite.scale.set(64 / this.sprite.width); + this.sprite.scale.set(0.25); this.add(this.sprite); // Add stasis effect @@ -62,12 +59,6 @@ module TS.SpaceTac.UI { this.stasis.visible = false; this.add(this.stasis); - // Add target effect - this.target = new Phaser.Image(this.game, 0, 0, "battle-arena-ship-frames", 5); - this.target.anchor.set(0.5, 0.5); - this.target.visible = false; - this.add(this.target); - // Add playing effect this.frame = new Phaser.Image(this.game, 0, 0, "battle-arena-ship-frames", this.enemy ? 0 : 1); this.frame.anchor.set(0.5, 0.5); @@ -202,15 +193,6 @@ module TS.SpaceTac.UI { this.frame.frame = (playing ? 3 : 0) + (this.enemy ? 0 : 1); } - /** - * Set the ship as target of current action - * - * This will toggle the visibility of target indicator - */ - setTargetted(targetted: boolean): void { - this.target.visible = targetted; - } - /** * Activate the dead effect (stasis) */ diff --git a/src/ui/battle/BattleView.spec.ts b/src/ui/battle/BattleView.spec.ts index 2960e94..0b2feba 100644 --- a/src/ui/battle/BattleView.spec.ts +++ b/src/ui/battle/BattleView.spec.ts @@ -6,34 +6,26 @@ module TS.SpaceTac.UI.Specs { it("forwards events in targetting mode", function () { let battleview = testgame.battleview; - expect(battleview.targetting).toBeNull(); + expect(battleview.targetting.active).toBe(false); battleview.setInteractionEnabled(true); + spyOn(battleview.targetting, "validate").and.stub(); + battleview.cursorInSpace(5, 5); - expect(battleview.targetting).toBeNull(); + expect(battleview.targetting.active).toBe(false); // Enter targetting mode - var result = nn(battleview.enterTargettingMode()); + let weapon = TestTools.addWeapon(nn(battleview.battle.playing_ship), 10); + battleview.enterTargettingMode(weapon.action); - expect(battleview.targetting).toBeTruthy(); - expect(result).toBe(nn(battleview.targetting)); - - // Collect targetting events - var hovered: (Target | null)[] = []; - var clicked: Target[] = []; - result.targetHovered.add((target: Target) => { - hovered.push(target); - }); - result.targetSelected.add((target: Target) => { - clicked.push(target); - }); + expect(battleview.targetting.active).toBe(true); // Forward selection in space battleview.cursorInSpace(8, 4); expect(battleview.ship_hovered).toBeNull(); - expect(nn(battleview.targetting).target_corrected).toEqual(Target.newFromLocation(8, 4)); + expect(battleview.targetting.target).toEqual(Target.newFromLocation(8, 4)); // Process a click on space battleview.cursorClicked(); @@ -42,19 +34,19 @@ module TS.SpaceTac.UI.Specs { battleview.cursorOnShip(battleview.battle.play_order[0]); expect(battleview.ship_hovered).toEqual(battleview.battle.play_order[0]); - expect(nn(battleview.targetting).target_corrected).toEqual(Target.newFromShip(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(nn(battleview.targetting).target_corrected).toEqual(Target.newFromShip(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(nn(battleview.targetting).target_corrected).toEqual(Target.newFromShip(battleview.battle.play_order[0])); + expect(battleview.targetting.target).toEqual(Target.newFromShip(battleview.battle.play_order[0])); // Process a click on ship battleview.cursorClicked(); @@ -63,12 +55,12 @@ module TS.SpaceTac.UI.Specs { battleview.cursorOffShip(battleview.battle.play_order[0]); expect(battleview.ship_hovered).toBeNull(); - expect(nn(battleview.targetting).target_corrected).toBeNull(); + expect(battleview.targetting.target).toBeNull(); // Quit targetting battleview.exitTargettingMode(); - expect(battleview.targetting).toBeNull(); + expect(battleview.targetting.active).toBe(false); // Events process normally battleview.cursorInSpace(8, 4); @@ -78,17 +70,6 @@ module TS.SpaceTac.UI.Specs { // Quit twice don't do anything battleview.exitTargettingMode(); - - // Check collected targetting events - expect(hovered).toEqual([ - Target.newFromLocation(8, 4), - Target.newFromShip(battleview.battle.play_order[0]), - null - ]); - expect(clicked).toEqual([ - Target.newFromLocation(8, 4), - Target.newFromShip(battleview.battle.play_order[0]), - ]); }); }); } diff --git a/src/ui/battle/BattleView.ts b/src/ui/battle/BattleView.ts index aec6768..ea299c9 100644 --- a/src/ui/battle/BattleView.ts +++ b/src/ui/battle/BattleView.ts @@ -3,56 +3,55 @@ module TS.SpaceTac.UI { // Interactive view of a Battle export class BattleView extends BaseView { - // Displayed battle - battle: Battle; + battle: Battle // Interacting player - player: Player; + player: Player // Layers - layer_background: Phaser.Group; - layer_arena: Phaser.Group; - layer_borders: Phaser.Group; - layer_overlay: Phaser.Group; - layer_dialogs: Phaser.Group; - layer_sheets: Phaser.Group; + layer_background: Phaser.Group + layer_arena: Phaser.Group + layer_borders: Phaser.Group + layer_overlay: Phaser.Group + layer_dialogs: Phaser.Group + layer_sheets: Phaser.Group // Battleground container - arena: Arena; + arena: Arena // Background image - background: Phaser.Image | null; + background: Phaser.Image | null // Targetting mode (null if we're not in this mode) - targetting: Targetting | null; + targetting: Targetting // Ship list - ship_list: ShipList; + ship_list: ShipList // Action bar - action_bar: ActionBar; + action_bar: ActionBar // Currently hovered ship - ship_hovered: Ship | null; + ship_hovered: Ship | null // Ship tooltip - ship_tooltip: ShipTooltip; + ship_tooltip: ShipTooltip // Outcome dialog layer - outcome_layer: Phaser.Group; + outcome_layer: Phaser.Group // Character sheet - character_sheet: CharacterSheet; + character_sheet: CharacterSheet // Subscription to the battle log - log_processor: LogProcessor; + log_processor: LogProcessor // True if player interaction is allowed - interacting: boolean; + interacting: boolean // Tactical mode toggle - toggle_tactical_mode: Toggle; + toggle_tactical_mode: Toggle // Init the view, binding it to a specific battle init(player: Player, battle: Battle) { @@ -60,7 +59,6 @@ module TS.SpaceTac.UI { this.player = player; this.battle = battle; - this.targetting = null; this.ship_hovered = null; this.background = null; @@ -104,6 +102,10 @@ module TS.SpaceTac.UI { this.character_sheet = new CharacterSheet(this, -this.getWidth()); this.layer_sheets.add(this.character_sheet); + // Targetting info + this.targetting = new Targetting(this, this.action_bar); + this.targetting.moveToLayer(this.arena.layer_targetting); + // "Battle" animation this.displayFightMessage(); @@ -150,8 +152,6 @@ module TS.SpaceTac.UI { // Leaving the view, we unbind the battle shutdown() { - this.exitTargettingMode(); - this.log_processor.destroy(); super.shutdown(); @@ -172,7 +172,7 @@ module TS.SpaceTac.UI { // Method called when cursor starts hovering over a ship (or its icon) cursorOnShip(ship: Ship): void { - if (!this.targetting || ship.alive) { + if (!this.targetting.active || ship.alive) { this.setShipHovered(ship); } } @@ -187,15 +187,15 @@ module TS.SpaceTac.UI { // Method called when cursor moves in space cursorInSpace(x: number, y: number): void { if (!this.ship_hovered) { - if (this.targetting) { - this.targetting.setTargetSpace(x, y); + if (this.targetting.active) { + this.targetting.setTarget(Target.newFromLocation(x, y)); } } } // Method called when cursor has been clicked (in space or on a ship) cursorClicked(): void { - if (this.targetting) { + if (this.targetting.active) { this.targetting.validate(); } else if (this.ship_hovered && this.ship_hovered.getPlayer() == this.player && this.interacting) { this.character_sheet.show(this.ship_hovered); @@ -215,11 +215,11 @@ module TS.SpaceTac.UI { this.ship_tooltip.hide(); } - if (this.targetting) { + if (this.targetting.active) { if (ship) { - this.targetting.setTargetShip(ship); + this.targetting.setTarget(Target.newFromShip(ship)); } else { - this.targetting.unsetTarget(); + this.targetting.setTarget(null); } } } @@ -240,25 +240,18 @@ module TS.SpaceTac.UI { // Enter targetting mode // While in this mode, the Targetting object will receive hover and click events, and handle them - enterTargettingMode(): Targetting | null { + enterTargettingMode(action: BaseAction): Targetting | null { if (!this.interacting) { return null; } - if (this.targetting) { - this.exitTargettingMode(); - } - - this.targetting = new Targetting(this); + this.targetting.setAction(action); return this.targetting; } // Exit targetting mode exitTargettingMode(): void { - if (this.targetting) { - this.targetting.destroy(); - } - this.targetting = null; + this.targetting.setAction(null); } /** diff --git a/src/ui/battle/RangeHint.ts b/src/ui/battle/RangeHint.ts index 8c16365..01576ec 100644 --- a/src/ui/battle/RangeHint.ts +++ b/src/ui/battle/RangeHint.ts @@ -43,7 +43,7 @@ module TS.SpaceTac.UI { /** * Update displayed information */ - update(ship: Ship, action: BaseAction): void { + update(ship: Ship, action: BaseAction, location: ArenaLocation = ship.location): void { let yescolor = 0x000000; let nocolor = 0x242022; this.info.clear(); @@ -54,7 +54,7 @@ module TS.SpaceTac.UI { this.info.drawRect(0, 0, this.width, this.height); this.info.beginFill(yescolor); - this.info.drawCircle(ship.arena_x, ship.arena_y, radius * 2); + this.info.drawCircle(location.x, location.y, radius * 2); if (action instanceof MoveAction) { let safety = action.safety_distance / 2; diff --git a/src/ui/battle/Targetting.spec.ts b/src/ui/battle/Targetting.spec.ts index 2fcdccf..2dbf998 100644 --- a/src/ui/battle/Targetting.spec.ts +++ b/src/ui/battle/Targetting.spec.ts @@ -2,68 +2,111 @@ module TS.SpaceTac.UI.Specs { describe("Targetting", function () { let testgame = setupBattleview(); - it("broadcasts hovering and selection events", function () { - var targetting = new Targetting(null); + it("draws simulation parts", function () { + let targetting = new Targetting(testgame.battleview, testgame.battleview.action_bar); - var hovered: Target[] = []; - var selected: Target[] = []; - targetting.targetHovered.add((target: Target) => { - hovered.push(target); - }); - targetting.targetSelected.add((target: Target) => { - selected.push(target); - }); + let ship = nn(testgame.battleview.battle.playing_ship); + ship.setArenaPosition(10, 20); + let weapon = TestTools.addWeapon(ship); + let engine = TestTools.addEngine(ship, 12); + targetting.setAction(weapon.action); - targetting.setTargetSpace(1, 2); - expect(hovered).toEqual([Target.newFromLocation(1, 2)]); - expect(selected).toEqual([]); + let drawvector = spyOn(targetting, "drawVector").and.stub(); - targetting.validate(); - expect(hovered).toEqual([Target.newFromLocation(1, 2)]); - expect(selected).toEqual([Target.newFromLocation(1, 2)]); + let part = { + action: weapon.action, + target: new Target(50, 30), + ap: 5, + possible: true + }; + targetting.drawPart(part, true, null); + expect(drawvector).toHaveBeenCalledTimes(1); + expect(drawvector).toHaveBeenCalledWith(0xdc6441, 10, 20, 50, 30, 0); + + targetting.drawPart(part, false, null); + expect(drawvector).toHaveBeenCalledTimes(2); + expect(drawvector).toHaveBeenCalledWith(0x8e8e8e, 10, 20, 50, 30, 0); + + targetting.setAction(engine.action); + part.action = engine.action; + targetting.drawPart(part, true, null); + expect(drawvector).toHaveBeenCalledTimes(3); + expect(drawvector).toHaveBeenCalledWith(0xe09c47, 10, 20, 50, 30, 12); }); - it("displays action point indicators", function () { - let battleview = testgame.battleview; - let source = new Phaser.Group(battleview.game, battleview.arena); - source.position.set(0, 0); + it("updates impact indicators on ships inside the blast radius", function () { + let targetting = new Targetting(testgame.battleview, testgame.battleview.action_bar); + let ship = nn(testgame.battleview.battle.playing_ship); - let targetting = new Targetting(battleview); + let collect = spyOn(testgame.battleview.battle, "collectShipsInCircle").and.returnValues( + [new Ship(), new Ship(), new Ship()], + [new Ship(), new Ship()], + []); + targetting.updateImpactIndicators(ship, new Target(20, 10), 50); - targetting.setSource(source); - targetting.setTargetSpace(200, 100); + expect(collect).toHaveBeenCalledTimes(1); + expect(collect).toHaveBeenCalledWith(new Target(20, 10), 50, true); + expect(targetting.fire_impact.children.length).toBe(3); + expect(targetting.fire_impact.visible).toBe(true); + + targetting.updateImpactIndicators(ship, new Target(20, 11), 50); + + expect(collect).toHaveBeenCalledTimes(2); + expect(collect).toHaveBeenCalledWith(new Target(20, 11), 50, true); + expect(targetting.fire_impact.children.length).toBe(2); + expect(targetting.fire_impact.visible).toBe(true); + + let target = Target.newFromShip(new Ship()); + targetting.updateImpactIndicators(ship, target, 0); + + expect(collect).toHaveBeenCalledTimes(2); + expect(targetting.fire_impact.children.length).toBe(1); + expect(targetting.fire_impact.visible).toBe(true); + + targetting.updateImpactIndicators(ship, new Target(20, 12), 50); + + expect(collect).toHaveBeenCalledTimes(3); + expect(collect).toHaveBeenCalledWith(new Target(20, 12), 50, true); + expect(targetting.fire_impact.visible).toBe(false); + }); + + it("updates graphics from simulation", function () { + let targetting = new Targetting(testgame.battleview, testgame.battleview.action_bar); + let ship = nn(testgame.battleview.battle.playing_ship); + + let engine = TestTools.addEngine(ship, 8000); + let weapon = TestTools.addWeapon(ship, 30, 5, 100, 50); + targetting.setAction(weapon.action); + targetting.setTarget(Target.newFromLocation(156, 65)); + + spyOn(targetting, "simulate").and.callFake(() => { + let result = new MoveFireResult(); + result.success = true; + result.complete = true; + result.need_move = true; + result.move_location = Target.newFromLocation(80, 20); + result.can_move = true; + result.can_end_move = true; + 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 } + ] + targetting.simulation = result; + }); targetting.update(); - targetting.updateApIndicators(); - expect(targetting.ap_indicators.length).toBe(0); - expect(battleview.arena.layer_targetting.children.length).toBe(3); - - targetting.setApIndicatorsInterval(Math.sqrt(5) * 20); - - expect(targetting.ap_indicators.length).toBe(5); - expect(battleview.arena.layer_targetting.children.length).toBe(3 + 5); - expect(targetting.ap_indicators[0].position.x).toBe(0); - expect(targetting.ap_indicators[0].position.y).toBe(0); - expect(targetting.ap_indicators[1].position.x).toBeCloseTo(40); - expect(targetting.ap_indicators[1].position.y).toBeCloseTo(20); - expect(targetting.ap_indicators[2].position.x).toBeCloseTo(80); - expect(targetting.ap_indicators[2].position.y).toBeCloseTo(40); - expect(targetting.ap_indicators[3].position.x).toBeCloseTo(120); - expect(targetting.ap_indicators[3].position.y).toBeCloseTo(60); - expect(targetting.ap_indicators[4].position.x).toBeCloseTo(160); - expect(targetting.ap_indicators[4].position.y).toBeCloseTo(80); - - targetting.setApIndicatorsInterval(1000); - expect(targetting.ap_indicators.length).toBe(1); - expect(battleview.arena.layer_targetting.children.length).toBe(3 + 1); - - targetting.setApIndicatorsInterval(1); - expect(targetting.ap_indicators.length).toBe(224); - expect(battleview.arena.layer_targetting.children.length).toBe(3 + 224); - - targetting.destroy(); - - expect(battleview.arena.layer_targetting.children.length).toBe(0); + expect(targetting.container.visible).toBe(true); + expect(targetting.drawn_info.visible).toBe(true); + expect(targetting.fire_arrow.visible).toBe(true); + expect(targetting.fire_arrow.position).toEqual(jasmine.objectContaining({ x: 156, y: 65 })); + expect(targetting.fire_arrow.rotation).toBeCloseTo(0.534594, 5); + expect(targetting.fire_blast.visible).toBe(true); + expect(targetting.fire_blast.position).toEqual(jasmine.objectContaining({ x: 156, y: 65 })); + expect(targetting.move_ghost.visible).toBe(true); + expect(targetting.move_ghost.position).toEqual(jasmine.objectContaining({ x: 80, y: 20 })); + expect(targetting.move_ghost.rotation).toBeCloseTo(0.534594, 5); }); }); } diff --git a/src/ui/battle/Targetting.ts b/src/ui/battle/Targetting.ts index 92b522d..a9cd838 100644 --- a/src/ui/battle/Targetting.ts +++ b/src/ui/battle/Targetting.ts @@ -1,199 +1,265 @@ module TS.SpaceTac.UI { - // Targetting system - // Allows to pick a target for an action + /** + * Targetting system on the arena + * + * This system handles choosing a target for currently selected action, and displays a visual aid. + */ export class Targetting { - // Initial target (as pointed by the user) - target_initial: Target | null; - line_initial: Phaser.Graphics; + // Container group + container: Phaser.Group - // Corrected target (applying action rules) - target_corrected: Target | null; - line_corrected: Phaser.Graphics; + // Current action + ship: Ship | null = null + action: BaseAction | null = null + target: Target | null = null + simulation = new MoveFireResult() - // Circle for effect radius - blast_radius: number; - blast: Phaser.Image; + // Movement projector + drawn_info: Phaser.Graphics + move_ghost: Phaser.Image - // Signal to receive hovering events - targetHovered: Phaser.Signal; + // Fire projector + fire_arrow: Phaser.Image + fire_blast: Phaser.Image + fire_impact: Phaser.Group - // Signal to receive targetting events - targetSelected: Phaser.Signal; + // Collaborators to update + actionbar: ActionBar - // AP usage display - ap_interval: number = 0; - ap_indicators: Phaser.Image[] = []; + // Access to the parent view + view: BaseView - // Access to the parent battle view - private battleview: BattleView | null; + constructor(view: BaseView, actionbar: ActionBar) { + this.view = view; + this.actionbar = actionbar; - // Source of the targetting - private source: PIXI.DisplayObject | null; - - // Create a default targetting mode - constructor(battleview: BattleView | null) { - this.battleview = battleview; - this.targetHovered = new Phaser.Signal(); - this.targetSelected = new Phaser.Signal(); + this.container = view.add.group(); // Visual effects - if (battleview) { - this.blast = new Phaser.Image(battleview.game, 0, 0, "battle-arena-blast"); - this.blast.anchor.set(0.5, 0.5); - this.blast.visible = false; - battleview.arena.layer_targetting.add(this.blast); - this.line_initial = new Phaser.Graphics(battleview.game, 0, 0); - this.line_initial.visible = false; - battleview.arena.layer_targetting.add(this.line_initial); - this.line_corrected = new Phaser.Graphics(battleview.game, 0, 0); - this.line_corrected.visible = false; - battleview.arena.layer_targetting.add(this.line_corrected); - } + this.drawn_info = new Phaser.Graphics(view.game, 0, 0); + this.drawn_info.visible = false; + this.move_ghost = new Phaser.Image(view.game, 0, 0, "common-transparent"); + this.move_ghost.anchor.set(0.5, 0.5); + this.move_ghost.alpha = 0.8; + this.move_ghost.visible = false; + this.fire_arrow = new Phaser.Image(view.game, 0, 0, "battle-arena-indicators", 0); + this.fire_arrow.anchor.set(1, 0.5); + this.fire_arrow.visible = false; + this.fire_impact = new Phaser.Group(view.game); + this.fire_impact.visible = false; + this.fire_blast = new Phaser.Image(view.game, 0, 0, "battle-arena-blast"); + this.fire_blast.anchor.set(0.5, 0.5); + this.fire_blast.visible = false; - this.source = null; - this.target_initial = null; - this.target_corrected = null; + this.container.add(this.fire_impact); + this.container.add(this.fire_blast); + this.container.add(this.drawn_info); + this.container.add(this.fire_arrow); + this.container.add(this.move_ghost); } - // Destructor - destroy(): void { - this.targetHovered.dispose(); - this.targetSelected.dispose(); - if (this.line_initial) { - this.line_initial.destroy(); - } - if (this.line_corrected) { - this.line_corrected.destroy(); - } - if (this.blast) { - this.blast.destroy(); - } - this.ap_indicators.forEach(indicator => indicator.destroy()); - if (this.battleview) { - this.battleview.arena.highlightTargets([]); - } + /** + * Move to a given view layer + */ + moveToLayer(layer: Phaser.Group): void { + layer.add(this.container); } - // Set AP indicators to display at fixed interval along the line - setApIndicatorsInterval(interval: number) { - this.ap_interval = interval; - this.updateApIndicators(); + /** + * Indicator that the targetting is currently active + */ + get active(): boolean { + return (this.ship && this.action) ? true : false; } - // Update visual effects for current targetting - update(): void { - if (this.battleview) { - if (this.source && this.target_initial) { - this.line_initial.clear(); - this.line_initial.lineStyle(3, 0x666666); - this.line_initial.moveTo(this.source.x, this.source.y); - this.line_initial.lineTo(this.target_initial.x, this.target_initial.y); - this.line_initial.visible = true; - } else { - this.line_initial.visible = false; + /** + * Draw a vector, with line and gradation + */ + drawVector(color: number, x1: number, y1: number, x2: number, y2: number, gradation = 0) { + let line = this.drawn_info; + line.lineStyle(6, color); + line.moveTo(x1, y1); + line.lineTo(x2, y2); + line.visible = true; + + if (gradation) { + let dx = x2 - x1; + let dy = y2 - y1; + let dist = Math.sqrt(dx * dx + dy * dy); + let angle = Math.atan2(dy, dx); + dx = Math.cos(angle); + dy = Math.sin(angle); + for (let d = gradation; d <= dist; d += gradation) { + line.moveTo(x1 + dx * d + dy * 10, y1 + dy * d - dx * 10); + line.lineTo(x1 + dx * d - dy * 10, y1 + dy * d + dx * 10); } - - if (this.source && this.target_corrected) { - this.line_corrected.clear(); - this.line_corrected.lineStyle(6, this.ap_interval ? 0xe09c47 : 0xDC6441); - this.line_corrected.moveTo(this.source.x, this.source.y); - this.line_corrected.lineTo(this.target_corrected.x, this.target_corrected.y); - this.line_corrected.visible = true; - } else { - this.line_corrected.visible = false; - } - - if (this.target_corrected && this.blast_radius) { - this.blast.position.set(this.target_corrected.x, this.target_corrected.y); - this.blast.scale.set(this.blast_radius * 2 / 365); - this.blast.visible = true; - - let targets = this.battleview.battle.collectShipsInCircle(this.target_corrected, this.blast_radius, true); - this.battleview.arena.highlightTargets(targets); - } else { - this.blast.visible = false; - - this.battleview.arena.highlightTargets(this.target_corrected && this.target_corrected.ship ? [this.target_corrected.ship] : []); - } - - this.updateApIndicators(); } } - // Update the AP indicators display - updateApIndicators() { - if (!this.battleview || !this.source) { + /** + * Draw a part of the simulation + */ + drawPart(part: MoveFirePart, enabled = true, previous: MoveFirePart | null = null): void { + if (!this.ship) { return; } - // Get indicator count - let count = 0; - let distance = 0; - if (this.line_corrected.visible && this.ap_interval > 0 && this.target_corrected) { - distance = this.target_corrected.getDistanceTo(Target.newFromLocation(this.source.x, this.source.y)) - 0.00001; - count = Math.ceil(distance / this.ap_interval); + let move = part.action instanceof MoveAction; + let color = (enabled && part.possible) ? (move ? 0xe09c47 : 0xdc6441) : 0x8e8e8e; + let src = previous ? previous.target : this.ship.location; + let gradation = part.action instanceof MoveAction ? part.action.distance_per_power : 0; + this.drawVector(color, src.x, src.y, part.target.x, part.target.y, gradation); + } + + /** + * Update impact indicators + */ + updateImpactIndicators(ship: Ship, target: Target, radius: number): void { + let ships: Ship[]; + if (radius) { + let battle = ship.getBattle(); + if (battle) { + ships = battle.collectShipsInCircle(target, radius, true); + } else { + ships = []; + } + } else { + ships = target.ship ? [target.ship] : []; } - // Adjust object count to match - while (this.ap_indicators.length < count) { - let indicator = new Phaser.Image(this.battleview.game, 0, 0, "battle-arena-ap-indicator"); - indicator.anchor.set(0.5, 0.5); - this.battleview.arena.layer_targetting.add(indicator); - this.ap_indicators.push(indicator); - } - while (this.ap_indicators.length > count) { - this.ap_indicators[this.ap_indicators.length - 1].destroy(); - this.ap_indicators.pop(); - } - - // Spread indicators - if (count > 0 && distance > 0 && this.target_corrected) { - let source = this.source; - let dx = this.ap_interval * (this.target_corrected.x - source.x) / distance; - let dy = this.ap_interval * (this.target_corrected.y - source.y) / distance; - this.ap_indicators.forEach((indicator, index) => { - indicator.position.set(source.x + dx * index, source.y + dy * index); + if (ships.length) { + this.fire_impact.removeAll(true); + ships.forEach(iship => { + let frame = this.view.add.image(iship.arena_x, iship.arena_y, "battle-arena-ship-frames", 5, this.fire_impact); + frame.anchor.set(0.5); }); + this.fire_impact.visible = true; + } else { + this.fire_impact.visible = false; } } - // Set the source sprite for the targetting (for visual effects) - setSource(sprite: PIXI.DisplayObject) { - this.source = sprite; + /** + * Update visual effects to show the simulation of current action/target + */ + update(): void { + this.simulate(); + if (this.ship && this.action && this.target) { + let simulation = this.simulation; + + this.drawn_info.clear(); + this.fire_arrow.visible = false; + this.move_ghost.visible = false; + + if (simulation.success) { + let previous: MoveFirePart | null = null; + simulation.parts.forEach(part => { + this.drawPart(part, simulation.complete, previous); + previous = part; + }); + this.fire_arrow.frame = simulation.complete ? 0 : 1; + + 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); + this.move_ghost.rotation = angle; + } else { + this.move_ghost.visible = false; + } + + if (simulation.need_fire) { + let blast = this.action.getBlastRadius(this.ship); + if (blast) { + this.fire_blast.position.set(this.target.x, this.target.y); + this.fire_blast.scale.set(blast * 2 / 365); + this.fire_blast.alpha = simulation.can_fire ? 1 : 0.5; + this.fire_blast.visible = true; + } else { + this.fire_blast.visible = false; + } + this.updateImpactIndicators(this.ship, this.target, blast); + + this.fire_arrow.position.set(this.target.x, this.target.y); + this.fire_arrow.rotation = angle; + this.fire_arrow.frame = simulation.complete ? 0 : 1; + this.fire_arrow.visible = true; + } else { + this.fire_blast.visible = false; + this.fire_impact.visible = false; + this.fire_arrow.visible = false; + } + + this.container.visible = true; + } else { + // TODO Display error + this.container.visible = false; + } + } else { + this.container.visible = false; + } } - // Set a target from a target object - setTarget(target: Target | null, dispatch: boolean = true, blast_radius: number = 0): void { - this.target_corrected = target; - this.blast_radius = blast_radius; - if (dispatch) { - this.target_initial = target ? copy(target) : null; - this.targetHovered.dispatch(this.target_corrected); + /** + * Simulate current action + */ + simulate(): void { + if (this.ship && this.action && this.target) { + let simulator = new MoveFireSimulator(this.ship); + this.simulation = simulator.simulateAction(this.action, this.target, 1); + } else { + this.simulation = new MoveFireResult(); } + } + + /** + * Set the current targetting action, or null to stop targetting + */ + setAction(action: BaseAction | null): 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.move_ghost.loadTexture(`ship-${this.ship.model.code}-sprite`); + this.move_ghost.scale.set(0.25); + } else { + this.ship = null; + this.action = null; + } + this.target = null; this.update(); } - // Set no target - unsetTarget(dispatch: boolean = true): void { - this.setTarget(null, dispatch); - } - - // Set the current target ship (when hovered) - setTargetShip(ship: Ship, dispatch: boolean = true): void { - if (ship.alive) { - this.setTarget(Target.newFromShip(ship), dispatch); + /** + * Set the target for current action + */ + setTarget(target: Target | null): void { + this.target = target; + this.update(); + if (this.action) { + this.actionbar.updateFromSimulation(this.action, this.simulation); } } - // Set the current target in space (when hovered) - setTargetSpace(x: number, y: number, dispatch: boolean = true): void { - this.setTarget(Target.newFromLocation(x, y)); - } - - // Validate the current target (when clicked) - // This will broadcast the targetSelected signal + /** + * Validate the current target. + * + * This will make the needed approach and apply the action. + */ validate(): void { - this.targetSelected.dispatch(this.target_corrected); + this.simulate(); + + if (this.ship && this.simulation.complete) { + let ship = this.ship; + this.simulation.parts.forEach(part => { + if (part.possible) { + part.action.apply(ship, part.target); + } + }); + this.actionbar.actionEnded(); + } } } }