1
0
Fork 0

arena: Added auto-approach to bring in range for action

This commit is contained in:
Michaël Lemaire 2017-06-22 01:32:18 +02:00
parent d3e422fff6
commit 3cc168bae9
22 changed files with 476 additions and 551 deletions

1
TODO
View file

@ -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)

View file

@ -235,18 +235,6 @@
offset="1"
id="stop10127" />
</linearGradient>
<linearGradient
inkscape:collect="always"
id="linearGradient9717">
<stop
style="stop-color:#d1d4b2;stop-opacity:1;"
offset="0"
id="stop9713" />
<stop
style="stop-color:#d1d4b2;stop-opacity:0;"
offset="1"
id="stop9715" />
</linearGradient>
<linearGradient
inkscape:collect="always"
id="linearGradient9609">
@ -710,16 +698,6 @@
x2="1512.2041"
y2="877.88531"
gradientUnits="userSpaceOnUse" />
<radialGradient
inkscape:collect="always"
xlink:href="#linearGradient9717"
id="radialGradient9719"
cx="825.83337"
cy="849.4455"
fx="825.83337"
fy="849.4455"
r="22.5"
gradientUnits="userSpaceOnUse" />
<radialGradient
inkscape:collect="always"
xlink:href="#linearGradient10129"
@ -843,16 +821,6 @@
operator="atop"
result="composite2" />
</filter>
<radialGradient
inkscape:collect="always"
xlink:href="#linearGradient9717"
id="radialGradient5025"
gradientUnits="userSpaceOnUse"
cx="825.83337"
cy="849.4455"
fx="825.83337"
fy="849.4455"
r="22.5" />
<filter
id="filter5041"
inkscape:label="Color impossible"
@ -1166,11 +1134,11 @@
borderopacity="1"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:zoom="0.5"
inkscape:cx="1132.2376"
inkscape:cy="630.86578"
inkscape:zoom="4"
inkscape:cx="358.32238"
inkscape:cy="992.57962"
inkscape:document-units="px"
inkscape:current-layer="layer25"
inkscape:current-layer="layer8"
showgrid="false"
units="px"
showguides="false"
@ -1181,7 +1149,7 @@
inkscape:object-nodes="true"
inkscape:snap-intersection-paths="false"
inkscape:object-paths="true"
inkscape:snap-global="true"
inkscape:snap-global="false"
inkscape:showpageshadow="false"
showborder="true"
borderlayer="true" />
@ -1548,6 +1516,12 @@
cx="1551.4003"
cy="742.08289"
r="31.144533" />
<path
style="display:inline;fill:none;fill-rule:evenodd;stroke:url(#linearGradient9611);stroke-width:5.89960623;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;enable-background:new"
d="M 732.44478,877.88531 H 1512.2041"
id="path9595"
inkscape:connector-curvature="0"
sodipodi:nodetypes="cc" />
<use
x="0"
y="0"
@ -1557,121 +1531,16 @@
width="100%"
height="100%"
style="display:inline;opacity:0.58499995;enable-background:new" />
<path
style="display:inline;fill:none;fill-rule:evenodd;stroke:url(#linearGradient9611);stroke-width:5.89960623;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;enable-background:new"
d="M 732.44478,877.88531 H 1512.2041"
id="path9595"
inkscape:connector-curvature="0"
sodipodi:nodetypes="cc" />
<path
sodipodi:nodetypes="ccccc"
inkscape:connector-curvature="0"
id="use9628"
d="m 1133.0389,877.88531 -23.5347,9.28279 3.6495,-9.28279 -3.6495,-9.28279 z"
inkscape:transform-center-x="-2.67976"
style="display:inline;opacity:1;fill:#362c20;fill-opacity:1;fill-rule:evenodd;stroke:#e09c47;stroke-width:2.43093753;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;enable-background:new" />
<path
style="display:inline;opacity:1;fill:#391b13;fill-opacity:1;fill-rule:evenodd;stroke:#dc6441;stroke-width:2.33156252;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;enable-background:new"
inkscape:transform-center-x="-3.93566"
d="m 1517.8308,877.88531 -34.5651,13.6335 5.3601,-13.6335 -5.3601,-13.6335 z"
id="path10745"
inkscape:connector-curvature="0"
sodipodi:nodetypes="ccccc" />
<g
style="display:inline;enable-background:new"
id="g9681"
transform="translate(-16.499158,28.439814)"
inkscape:export-filename="/home/michael/workspace/perso/spacetac/out/assets/images/battle/arena/ap-indicator.png"
sodipodi:nodetypes="ccccc"
inkscape:export-filename="/tmp/export.png"
inkscape:export-xdpi="90"
inkscape:export-ydpi="90">
<circle
r="22.5"
cy="849.4455"
cx="825.83337"
id="path9677"
style="opacity:1;fill:url(#radialGradient9719);fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:2.33156252;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<use
height="100%"
width="100%"
transform="translate(828.83521,989.66854)"
id="use9656"
xlink:href="#g4573"
y="0"
x="0" />
</g>
<use
style="display:inline;enable-background:new"
x="0"
y="0"
xlink:href="#g9681"
id="use9699"
transform="translate(121.99958)"
width="100%"
height="100%" />
<use
style="display:inline;enable-background:new"
x="0"
y="0"
xlink:href="#g9681"
id="use9701"
transform="translate(243.99916)"
width="100%"
height="100%" />
<use
style="display:inline;enable-background:new"
x="0"
y="0"
xlink:href="#g9681"
id="use9703"
transform="translate(370.64181)"
width="100%"
height="100%" />
<use
style="display:inline;enable-background:new"
x="0"
y="0"
xlink:href="#g9681"
id="use9705"
transform="translate(436.68086)"
width="100%"
height="100%" />
<use
style="display:inline;enable-background:new"
x="0"
y="0"
xlink:href="#g9681"
id="use9707"
transform="translate(502.7198)"
width="100%"
height="100%" />
<use
style="display:inline;enable-background:new"
x="0"
y="0"
xlink:href="#g9681"
id="use9709"
transform="translate(568.75877)"
width="100%"
height="100%" />
<g
transform="translate(618.6426,28.439814)"
id="g5021"
style="display:inline;filter:url(#filter5041);enable-background:new">
<circle
style="opacity:1;fill:url(#radialGradient5025);fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:2.33156252;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="circle5017"
cx="825.83337"
cy="849.4455"
r="22.5" />
<use
x="0"
y="0"
xlink:href="#g4573"
id="use5019"
transform="translate(828.83521,989.66854)"
width="100%"
height="100%" />
</g>
inkscape:export-ydpi="90" />
</g>
<g
inkscape:groupmode="layer"
@ -2262,17 +2131,7 @@
inkscape:export-ydpi="90"
transform="matrix(0.98149613,0,0,1.5780874,-2.5439867,-65.79016)" />
<path
transform="matrix(0.98149613,0,0,1.5780874,52.760346,-65.79016)"
inkscape:export-ydpi="90"
inkscape:export-xdpi="90"
inkscape:export-filename="/home/michael/workspace/perso/spacetac/out/assets/images/battle/power-using.png"
sodipodi:nodetypes="ccccc"
inkscape:connector-curvature="0"
id="path4925"
d="m 197.49714,119.86752 h 51.83694 l 5.86374,-12.12184 h -51.83694 z"
style="opacity:1;fill:#f8f0b5;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:16;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<path
transform="matrix(0.98149613,0,0,1.5780874,108.06471,-65.79016)"
transform="matrix(0.98149613,0,0,1.5780874,160.43481,-65.79016)"
inkscape:export-ydpi="90"
inkscape:export-xdpi="90"
inkscape:export-filename="/home/michael/workspace/perso/spacetac/out/assets/images/battle/power-used.png"
@ -2281,6 +2140,26 @@
id="path4927"
d="m 197.49714,119.86752 h 51.83694 l 5.86374,-12.12184 h -51.83694 z"
style="opacity:1;fill:#6b6443;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:16;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;filter:url(#filter5649)" />
<path
transform="matrix(0.98149613,0,0,1.5780874,51.782267,-65.79016)"
inkscape:export-ydpi="90"
inkscape:export-xdpi="90"
inkscape:export-filename="/tmp/export.png"
sodipodi:nodetypes="ccccc"
inkscape:connector-curvature="0"
id="path5001"
d="m 197.49714,119.86752 h 51.83694 l 5.86374,-12.12184 h -51.83694 z"
style="opacity:1;fill:#e09c47;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:16;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;filter:url(#filter5649)" />
<path
transform="matrix(0.98149613,0,0,1.5780874,106.10853,-65.79016)"
inkscape:export-ydpi="90"
inkscape:export-xdpi="90"
inkscape:export-filename="/tmp/export.png"
sodipodi:nodetypes="ccccc"
inkscape:connector-curvature="0"
id="path5003"
d="m 197.49714,119.86752 h 51.83694 l 5.86374,-12.12184 h -51.83694 z"
style="opacity:1;fill:#dc6441;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:16;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;filter:url(#filter5649)" />
</g>
</g>
<g

Before

Width:  |  Height:  |  Size: 131 KiB

After

Width:  |  Height:  |  Size: 127 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 KiB

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2 KiB

After

Width:  |  Height:  |  Size: 12 KiB

View file

@ -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<Target> {
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;
}

View file

@ -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));

View file

@ -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;
}

View file

@ -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)]]);
});

View file

@ -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;

View file

@ -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");

View file

@ -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();
});

View file

@ -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 = <Phaser.Image>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);
}
}
/**

View file

@ -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)) {

View file

@ -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)
*/

View file

@ -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)
*/

View file

@ -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]),
]);
});
});
}

View file

@ -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);
}
/**

View file

@ -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;

View file

@ -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);
});
});
}

View file

@ -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();
}
}
}
}