From 3cc168bae9373ad629e08fd02f259d43c0d4e5bd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C3=ABl=20Lemaire?= Date: Thu, 22 Jun 2017 01:32:18 +0200 Subject: [PATCH] arena: Added auto-approach to bring in range for action --- TODO | 1 - graphics/ui/battle.svg | 191 ++------- out/assets/images/battle/actionbar/power.png | Bin 2165 -> 3686 bytes .../images/battle/arena/ap-indicator.png | Bin 3579 -> 0 bytes out/assets/images/battle/arena/indicators.png | Bin 0 -> 11939 bytes out/assets/images/ship/scout/sprite.png | Bin 2022 -> 12168 bytes src/core/MoveFireSimulator.ts | 11 +- src/core/actions/MoveAction.spec.ts | 2 +- src/core/actions/MoveAction.ts | 18 +- src/core/ai/Maneuver.spec.ts | 7 +- src/ui/BaseView.ts | 1 + src/ui/Preload.ts | 2 +- src/ui/battle/ActionBar.spec.ts | 44 +- src/ui/battle/ActionBar.ts | 31 +- src/ui/battle/ActionIcon.ts | 40 +- src/ui/battle/Arena.ts | 10 +- src/ui/battle/ArenaShip.ts | 20 +- src/ui/battle/BattleView.spec.ts | 45 +-- src/ui/battle/BattleView.ts | 75 ++-- src/ui/battle/RangeHint.ts | 4 +- src/ui/battle/Targetting.spec.ts | 149 ++++--- src/ui/battle/Targetting.ts | 376 ++++++++++-------- 22 files changed, 476 insertions(+), 551 deletions(-) delete mode 100644 out/assets/images/battle/arena/ap-indicator.png create mode 100644 out/assets/images/battle/arena/indicators.png 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" /> - + + 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 IM6N<$f-CR!*Z=?k delta 2105 zcmV-92*&s39Q6>8Q-25vCk%(0dI646&k^ zIK)byev-jcpP_2NGzu4PDwMG#T-pP^?sN4`>ICFxPR>JjQ7cC-w^;e2I6Zp z`zn_Wz!PicePHmh>8_jeS1JilJF>mKodMay4=2uUcc6w-vc8@;x3<=N_3HaP_~4Qs z`EcDkSFThuKfFJF>3IlevDZM$5Zge8g22szj({8?MC36r!t{UxGVL*D52|(TV zS*VR;jwIQNaO|7{6J*;4RThP;fr&X$CTJA!=dZthd4DujP&%X2l-fkzM4Z500Xj$- zR!E{YAW01%V9e?P=!D$6;+Hq4sCx8{>4kE(@HG{G)LKqV* zmpF{TZKYT%WR}Rd1u$Jul}{mUU>*QA5J^yzV+REI_#-!Er;Vt9F)*wv$QaziY7z6@ zna4xLt@Cm0d!qE$y0ps+P(h5Df059(Yj#Wu#TKcF3JuCZ3?G}kT?xd(hxfb1u9?P zsX6Z#)SYvLxC&lHT20x>GNzq+%cO@Mdm&0?C}n3^&ss$aIxBS@|8w^7@|>(y#HwKx zvww=TjA&zOL7U*_>+X4!6BXdBcHz$Q?2z+*L9_TAu#)--%ZMe+QY~nJx`!S=XKfRx z2!QKglj|`?CA#zJtm9eGa}}%zuYi^jOM)dw+NApwXoO9`PQhDEK6?6W5Fpx=2cz+) zQw8$Oslr= zlb#Y9(N&JjEJ;X?j4m1j1<`+>b#kccY-V*VcR($0ODHqSj)>Wu<%yB8gzQQ6fq${B zLXP)v-YaPEIZzup$F_gT@?ZP6cGOj>lL_uT(9>t~chf@XhwF?~_&k)G5vPnQ7B9VxwkRwKoz!ecI2`LK1 z2zvw?05|m5JA2cPz4qFLPVA4)2!G{Hj!|jOBN|7nN;tYP8L9}b3}c}ptu{T9G_ZW* zjW_KL z5rs;3{aahg$Pr`Akf6RHQ_6rAIIAh*q4O&%~%RX>G)IRO%gPHv^&rjMa1b;(NAJa;k2#;*% zNWr2J&p^}S3lox2LlaIP2)F%UVjh0(p_vp%^Zy@TDWq1kD&d}FP#FuF*nmnrQa;HprNH#XwsW*|BIUN zmzAxTZY;E*%+YozXfey}p`hOV;~H>&Zn|fz-d<=yi}v8Vz|x#BebRd1dw*MKL6PUj z-CDnv~%hIdGLJJCW jwC-EraplWvPOtv}Zu>k$NZULW00000NkvXXu0mjf`kwYh diff --git a/out/assets/images/battle/arena/ap-indicator.png b/out/assets/images/battle/arena/ap-indicator.png deleted file mode 100644 index 1d0bdb29f8b7e579a89c4283859988e397f38378..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3579 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 B*z^DZ diff --git a/out/assets/images/battle/arena/indicators.png b/out/assets/images/battle/arena/indicators.png new file mode 100644 index 0000000000000000000000000000000000000000..fe3e2357cbcbaf9da5e206766cad10e82205fdb5 GIT binary patch literal 11939 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 qYnE#kGB%Sd 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 diff --git a/src/core/MoveFireSimulator.ts b/src/core/MoveFireSimulator.ts index f226132..cbe4172 100644 --- a/src/core/MoveFireSimulator.ts +++ b/src/core/MoveFireSimulator.ts @@ -25,6 +25,8 @@ module TS.SpaceTac { success = false // Ideal successive parts to make the full move+fire parts: MoveFirePart[] = [] + // Simulation complete (both move and fire are possible) + complete = false need_move = false can_move = false @@ -74,7 +76,8 @@ module TS.SpaceTac { * Get an iterator for scanning a circle */ scanCircle(x: number, y: number, radius: number, nr = 6, na = 30): Iterator { - 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(); + } } } }