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 * Menu: allow to delete cloud saves
* Arena: display effects description instead of attribute changes * Arena: display effects description instead of attribute changes
* Arena: display radius for area effects (both on action hover, and while action is active) * 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: fix effects originating from real ship location instead of current sprite (when AI fires then moves)
* Arena: add engine trail * 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) * 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" offset="1"
id="stop10127" /> id="stop10127" />
</linearGradient> </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 <linearGradient
inkscape:collect="always" inkscape:collect="always"
id="linearGradient9609"> id="linearGradient9609">
@ -710,16 +698,6 @@
x2="1512.2041" x2="1512.2041"
y2="877.88531" y2="877.88531"
gradientUnits="userSpaceOnUse" /> 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 <radialGradient
inkscape:collect="always" inkscape:collect="always"
xlink:href="#linearGradient10129" xlink:href="#linearGradient10129"
@ -843,16 +821,6 @@
operator="atop" operator="atop"
result="composite2" /> result="composite2" />
</filter> </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 <filter
id="filter5041" id="filter5041"
inkscape:label="Color impossible" inkscape:label="Color impossible"
@ -1166,11 +1134,11 @@
borderopacity="1" borderopacity="1"
inkscape:pageopacity="0" inkscape:pageopacity="0"
inkscape:pageshadow="2" inkscape:pageshadow="2"
inkscape:zoom="0.5" inkscape:zoom="4"
inkscape:cx="1132.2376" inkscape:cx="358.32238"
inkscape:cy="630.86578" inkscape:cy="992.57962"
inkscape:document-units="px" inkscape:document-units="px"
inkscape:current-layer="layer25" inkscape:current-layer="layer8"
showgrid="false" showgrid="false"
units="px" units="px"
showguides="false" showguides="false"
@ -1181,7 +1149,7 @@
inkscape:object-nodes="true" inkscape:object-nodes="true"
inkscape:snap-intersection-paths="false" inkscape:snap-intersection-paths="false"
inkscape:object-paths="true" inkscape:object-paths="true"
inkscape:snap-global="true" inkscape:snap-global="false"
inkscape:showpageshadow="false" inkscape:showpageshadow="false"
showborder="true" showborder="true"
borderlayer="true" /> borderlayer="true" />
@ -1548,6 +1516,12 @@
cx="1551.4003" cx="1551.4003"
cy="742.08289" cy="742.08289"
r="31.144533" /> 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 <use
x="0" x="0"
y="0" y="0"
@ -1557,121 +1531,16 @@
width="100%" width="100%"
height="100%" height="100%"
style="display:inline;opacity:0.58499995;enable-background:new" /> 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 <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" 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" 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" d="m 1517.8308,877.88531 -34.5651,13.6335 5.3601,-13.6335 -5.3601,-13.6335 z"
id="path10745" id="path10745"
inkscape:connector-curvature="0" inkscape:connector-curvature="0"
sodipodi:nodetypes="ccccc" /> sodipodi:nodetypes="ccccc"
<g inkscape:export-filename="/tmp/export.png"
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"
inkscape:export-xdpi="90" inkscape:export-xdpi="90"
inkscape:export-ydpi="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>
</g> </g>
<g <g
inkscape:groupmode="layer" inkscape:groupmode="layer"
@ -2262,17 +2131,7 @@
inkscape:export-ydpi="90" inkscape:export-ydpi="90"
transform="matrix(0.98149613,0,0,1.5780874,-2.5439867,-65.79016)" /> transform="matrix(0.98149613,0,0,1.5780874,-2.5439867,-65.79016)" />
<path <path
transform="matrix(0.98149613,0,0,1.5780874,52.760346,-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-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)"
inkscape:export-ydpi="90" inkscape:export-ydpi="90"
inkscape:export-xdpi="90" inkscape:export-xdpi="90"
inkscape:export-filename="/home/michael/workspace/perso/spacetac/out/assets/images/battle/power-used.png" inkscape:export-filename="/home/michael/workspace/perso/spacetac/out/assets/images/battle/power-used.png"
@ -2281,6 +2140,26 @@
id="path4927" id="path4927"
d="m 197.49714,119.86752 h 51.83694 l 5.86374,-12.12184 h -51.83694 z" 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)" /> 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> </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 success = false
// Ideal successive parts to make the full move+fire // Ideal successive parts to make the full move+fire
parts: MoveFirePart[] = [] parts: MoveFirePart[] = []
// Simulation complete (both move and fire are possible)
complete = false
need_move = false need_move = false
can_move = false can_move = false
@ -74,7 +76,8 @@ module TS.SpaceTac {
* Get an iterator for scanning a circle * Get an iterator for scanning a circle
*/ */
scanCircle(x: number, y: number, radius: number, nr = 6, na = 30): Iterator<Target> { 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)); let angles = Math.max(1, Math.ceil(na * r));
return imap(istep(0, irepeat(2 * Math.PI / angles, angles - 1)), a => { 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)) 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 ? // Move or approach needed ?
let move_target: Target | null = null; let move_target: Target | null = null;
result.move_location = Target.newFromShip(this.ship);
if (action instanceof MoveAction) { 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) { if (corrected_target) {
result.need_move = target.getDistanceTo(this.ship.location) > 0; result.need_move = target.getDistanceTo(this.ship.location) > 0;
move_target = corrected_target; move_target = corrected_target;
@ -174,7 +179,9 @@ module TS.SpaceTac {
result.fire_location = target; 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.parts.push({ action: action, target: target, ap: result.total_fire_ap, possible: (!result.need_move || result.can_end_move) && result.can_fire });
} }
result.success = true; result.success = true;
result.complete = (!result.need_move || result.can_end_move) && (!result.need_fire || result.can_fire);
return result; return result;
} }

View file

@ -17,7 +17,7 @@ module TS.SpaceTac {
expect(result).toEqual(Target.newFromLocation(0, 2)); expect(result).toEqual(Target.newFromLocation(0, 2));
result = action.checkTarget(ship, Target.newFromLocation(0, 8)); 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); ship.values.power.set(0);
result = action.checkTarget(ship, Target.newFromLocation(0, 8)); 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) * 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(); let battle = ship.getBattle();
if (battle) { if (battle) {
// Keep out of arena borders // Keep out of arena borders
@ -76,14 +76,18 @@ module TS.SpaceTac {
return target; 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 { checkLocationTarget(ship: Ship, target: Target): Target {
// Apply maximal distance target = this.applyReachableRange(ship, target);
var max_distance = this.getRangeRadius(ship);
target = target.constraintInRange(ship.arena_x, ship.arena_y, max_distance);
// Apply exclusion areas
target = this.applyExclusion(ship, target); target = this.applyExclusion(ship, target);
return target; return target;
} }

View file

@ -42,7 +42,8 @@ module TS.SpaceTac.Specs {
it("guesses area effects on final location", function () { it("guesses area effects on final location", function () {
let battle = new Battle(); let battle = new Battle();
let ship = battle.fleets[0].addShip(); 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); let drone = new Drone(ship);
drone.effects = [new AttributeEffect("maneuvrability", 1)]; drone.effects = [new AttributeEffect("maneuvrability", 1)];
drone.x = 100; drone.x = 100;
@ -50,11 +51,11 @@ module TS.SpaceTac.Specs {
drone.radius = 50; drone.radius = 50;
battle.addDrone(drone); 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.getFinalLocation()).toEqual(jasmine.objectContaining({ x: 40, y: 30 }));
expect(maneuver.effects).toEqual([]); 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.getFinalLocation()).toEqual(jasmine.objectContaining({ x: 100, y: 30 }));
expect(maneuver.effects).toEqual([[ship, new AttributeEffect("maneuvrability", 1)]]); expect(maneuver.effects).toEqual([[ship, new AttributeEffect("maneuvrability", 1)]]);
}); });

View file

@ -50,6 +50,7 @@ module TS.SpaceTac.UI {
create() { create() {
// Phaser config // Phaser config
this.game.stage.backgroundColor = 0x000000; this.game.stage.backgroundColor = 0x000000;
this.game.stage.disableVisibilityChange = this.gameui.headless;
this.scale.scaleMode = Phaser.ScaleManager.SHOW_ALL; this.scale.scaleMode = Phaser.ScaleManager.SHOW_ALL;
this.input.maxPointers = 1; this.input.maxPointers = 1;

View file

@ -35,10 +35,10 @@ module TS.SpaceTac.UI {
this.loadImage("battle/actionbar/action-endturn.png"); this.loadImage("battle/actionbar/action-endturn.png");
this.loadSheet("battle/actionbar/button-menu.png", 79, 132); this.loadSheet("battle/actionbar/button-menu.png", 79, 132);
this.loadImage("battle/arena/background.png"); this.loadImage("battle/arena/background.png");
this.loadImage("battle/arena/ap-indicator.png");
this.loadImage("battle/arena/blast.png"); this.loadImage("battle/arena/blast.png");
this.loadSheet("battle/arena/gauges.png", 19, 93); this.loadSheet("battle/arena/gauges.png", 19, 93);
this.loadSheet("battle/arena/small-indicators.png", 10, 10); 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.loadSheet("battle/arena/ship-frames.png", 70, 70);
this.loadImage("battle/shiplist/background.png"); this.loadImage("battle/shiplist/background.png");
this.loadImage("battle/shiplist/item-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); 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) => { fading.forEach((index: number) => {
var icon = bar.action_icons[index]; 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) => { available.forEach((index: number) => {
var icon = bar.action_icons[index]; 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.updateSelectedActionPower(3, 0, bar.action_icons[1].action);
bar.action_icons[1].processClick(); checkFading([], [0, 1, 2, 3], "Weapon 1 leaves all choices open");
checkFading([], [0, 1, 2, 3]);
bar.actionEnded(); bar.actionEnded();
// Weapon 2 can't be fired twice bar.updateSelectedActionPower(5, 0, bar.action_icons[2].action);
bar.action_icons[2].processClick(); checkFading([2], [0, 1, 3], "Weapon 2 can't be fired twice");
checkFading([2], [0, 1, 3]);
bar.actionEnded(); bar.actionEnded();
// Not enough AP for both weapons
ship.setValue("power", 7); ship.setValue("power", 7);
bar.action_icons[2].processClick(); bar.updateSelectedActionPower(5, 0, bar.action_icons[2].action);
checkFading([1, 2], [0, 3]); checkFading([1, 2], [0, 3], "Not enough AP for both weapons");
bar.actionEnded(); bar.actionEnded();
// Not enough AP to move
ship.setValue("power", 3); ship.setValue("power", 3);
bar.action_icons[1].processClick(); bar.updateSelectedActionPower(3, 0, bar.action_icons[1].action);
checkFading([0, 1, 2], [3]); checkFading([0, 1, 2], [3], "Not enough AP to move");
bar.actionEnded(); bar.actionEnded();
// Dynamic AP usage for move actions // Dynamic AP usage for move actions
ship.setValue("power", 6); ship.setValue("power", 6);
bar.action_icons[0].processClick(); bar.updateSelectedActionPower(2, 0, bar.action_icons[0].action);
checkFading([], [0, 1, 2, 3]); checkFading([2], [0, 1, 3], "2 move power used");
bar.action_icons[0].processHover(Target.newFromLocation(2, 8)); bar.updateSelectedActionPower(4, 0, bar.action_icons[0].action);
checkFading([2], [0, 1, 3]); checkFading([1, 2], [0, 3], "4 move power used");
bar.action_icons[0].processHover(Target.newFromLocation(3, 8)); bar.updateSelectedActionPower(6, 0, bar.action_icons[0].action);
checkFading([1, 2], [0, 3]); checkFading([0, 1, 2], [3], "6 move power used");
bar.action_icons[0].processHover(Target.newFromLocation(4, 8)); bar.updateSelectedActionPower(8, 0, bar.action_icons[0].action);
checkFading([0, 1, 2], [3]); checkFading([0, 1, 2], [3], "8 move power used");
bar.action_icons[0].processHover(Target.newFromLocation(5, 8));
checkFading([0, 1, 2], [3]);
bar.actionEnded(); bar.actionEnded();
}); });

View file

@ -150,7 +150,7 @@ module TS.SpaceTac.UI {
/** /**
* Update the power indicator * 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 current_power = this.power.children.length;
let power_capacity = this.ship_power_capacity; let power_capacity = this.ship_power_capacity;
@ -162,14 +162,16 @@ module TS.SpaceTac.UI {
} }
let power_value = this.ship_power_value; 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) => { this.power.children.forEach((obj, idx) => {
let img = <Phaser.Image>obj; let img = <Phaser.Image>obj;
let frame: number; let frame: number;
if (idx < remaining_power) { if (idx < remaining_power) {
frame = 0; frame = 0;
} else if (idx < power_value) { } else if (idx < remaining_power + move_power) {
frame = 2; frame = 2;
} else if (idx < power_value) {
frame = 3;
} else { } else {
frame = 1; 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. * 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. * 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 { updateSelectedActionPower(move_power: number, fire_power: number, action: BaseAction): void {
var remaining_ap = this.ship ? (this.ship.values.power.get() - power_usage) : 0; var remaining_ap = this.ship ? (this.ship.getValue("power") - move_power - fire_power) : 0;
if (remaining_ap < 0) { if (remaining_ap < 0) {
remaining_ap = 0; remaining_ap = 0;
} }
this.action_icons.forEach((icon: ActionIcon) => { this.action_icons.forEach(icon => {
icon.updateFadingStatus(remaining_ap, action); 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 // Create an icon for a single ship action
constructor(bar: ActionBar, x: number, y: number, ship: Ship, action: BaseAction, position: number) { 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.bar = bar;
this.battleview = bar.battleview; this.battleview = bar.battleview;
@ -83,19 +83,6 @@ module TS.SpaceTac.UI {
ActionTooltip.fill(filler, this.ship, this.action, position); ActionTooltip.fill(filler, this.ship, this.action, position);
return true; 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 // Initialize
this.updateActiveStatus(true); this.updateActiveStatus(true);
@ -120,13 +107,10 @@ module TS.SpaceTac.UI {
this.bar.actionStarted(); this.bar.actionStarted();
// Update range hint // 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); 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 // Set the selected state
this.setSelected(true); this.setSelected(true);
@ -134,15 +118,7 @@ module TS.SpaceTac.UI {
let sprite = this.battleview.arena.findShipSprite(this.ship); let sprite = this.battleview.arena.findShipSprite(this.ship);
if (sprite) { if (sprite) {
// Switch to targetting mode (will apply action when a target is selected) // Switch to targetting mode (will apply action when a target is selected)
this.targetting = this.battleview.enterTargettingMode(); this.targetting = this.battleview.enterTargettingMode(this.action);
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));
}
}
} }
} else { } else {
// No target needed, apply action immediately // 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 // Called when a target is selected
processSelection(target: Target | null): void { processSelection(target: Target | null): void {
if (this.action.apply(this.ship, target)) { if (this.action.apply(this.ship, target)) {

View file

@ -67,6 +67,9 @@ module TS.SpaceTac.UI {
background.onInputUp.add(() => { background.onInputUp.add(() => {
battleview.cursorClicked(); battleview.cursorClicked();
}); });
background.onInputOut.add(() => {
battleview.targetting.setTarget(null);
});
// Watch mouse move to capture hovering over background // Watch mouse move to capture hovering over background
this.input_callback = this.game.input.addMoveCallback((pointer: Phaser.Pointer) => { 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) * Switch the tactical mode (shows information on all ships, and fades background)
*/ */

View file

@ -17,9 +17,6 @@ module TS.SpaceTac.UI {
// Statis effect // Statis effect
stasis: Phaser.Image stasis: Phaser.Image
// Target effect
target: Phaser.Image
// HSP display // HSP display
hull: ValueBar hull: ValueBar
toggle_hull: Toggle 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 = new Phaser.Button(this.game, 0, 0, "ship-" + ship.model.code + "-sprite");
this.sprite.rotation = ship.arena_angle; this.sprite.rotation = ship.arena_angle;
this.sprite.anchor.set(0.5, 0.5); 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); this.add(this.sprite);
// Add stasis effect // Add stasis effect
@ -62,12 +59,6 @@ module TS.SpaceTac.UI {
this.stasis.visible = false; this.stasis.visible = false;
this.add(this.stasis); 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 // Add playing effect
this.frame = new Phaser.Image(this.game, 0, 0, "battle-arena-ship-frames", this.enemy ? 0 : 1); 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); 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); 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) * Activate the dead effect (stasis)
*/ */

View file

@ -6,34 +6,26 @@ module TS.SpaceTac.UI.Specs {
it("forwards events in targetting mode", function () { it("forwards events in targetting mode", function () {
let battleview = testgame.battleview; let battleview = testgame.battleview;
expect(battleview.targetting).toBeNull(); expect(battleview.targetting.active).toBe(false);
battleview.setInteractionEnabled(true); battleview.setInteractionEnabled(true);
spyOn(battleview.targetting, "validate").and.stub();
battleview.cursorInSpace(5, 5); battleview.cursorInSpace(5, 5);
expect(battleview.targetting).toBeNull(); expect(battleview.targetting.active).toBe(false);
// Enter targetting mode // 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(battleview.targetting.active).toBe(true);
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);
});
// Forward selection in space // Forward selection in space
battleview.cursorInSpace(8, 4); battleview.cursorInSpace(8, 4);
expect(battleview.ship_hovered).toBeNull(); 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 // Process a click on space
battleview.cursorClicked(); battleview.cursorClicked();
@ -42,19 +34,19 @@ module TS.SpaceTac.UI.Specs {
battleview.cursorOnShip(battleview.battle.play_order[0]); battleview.cursorOnShip(battleview.battle.play_order[0]);
expect(battleview.ship_hovered).toEqual(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 // Don't leave a ship we're not hovering
battleview.cursorOffShip(battleview.battle.play_order[1]); battleview.cursorOffShip(battleview.battle.play_order[1]);
expect(battleview.ship_hovered).toEqual(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 move in space while on ship // Don't move in space while on ship
battleview.cursorInSpace(1, 3); battleview.cursorInSpace(1, 3);
expect(battleview.ship_hovered).toEqual(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]));
// Process a click on ship // Process a click on ship
battleview.cursorClicked(); battleview.cursorClicked();
@ -63,12 +55,12 @@ module TS.SpaceTac.UI.Specs {
battleview.cursorOffShip(battleview.battle.play_order[0]); battleview.cursorOffShip(battleview.battle.play_order[0]);
expect(battleview.ship_hovered).toBeNull(); expect(battleview.ship_hovered).toBeNull();
expect(nn(battleview.targetting).target_corrected).toBeNull(); expect(battleview.targetting.target).toBeNull();
// Quit targetting // Quit targetting
battleview.exitTargettingMode(); battleview.exitTargettingMode();
expect(battleview.targetting).toBeNull(); expect(battleview.targetting.active).toBe(false);
// Events process normally // Events process normally
battleview.cursorInSpace(8, 4); battleview.cursorInSpace(8, 4);
@ -78,17 +70,6 @@ module TS.SpaceTac.UI.Specs {
// Quit twice don't do anything // Quit twice don't do anything
battleview.exitTargettingMode(); 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 { module TS.SpaceTac.UI {
// Interactive view of a Battle // Interactive view of a Battle
export class BattleView extends BaseView { export class BattleView extends BaseView {
// Displayed battle // Displayed battle
battle: Battle; battle: Battle
// Interacting player // Interacting player
player: Player; player: Player
// Layers // Layers
layer_background: Phaser.Group; layer_background: Phaser.Group
layer_arena: Phaser.Group; layer_arena: Phaser.Group
layer_borders: Phaser.Group; layer_borders: Phaser.Group
layer_overlay: Phaser.Group; layer_overlay: Phaser.Group
layer_dialogs: Phaser.Group; layer_dialogs: Phaser.Group
layer_sheets: Phaser.Group; layer_sheets: Phaser.Group
// Battleground container // Battleground container
arena: Arena; arena: Arena
// Background image // Background image
background: Phaser.Image | null; background: Phaser.Image | null
// Targetting mode (null if we're not in this mode) // Targetting mode (null if we're not in this mode)
targetting: Targetting | null; targetting: Targetting
// Ship list // Ship list
ship_list: ShipList; ship_list: ShipList
// Action bar // Action bar
action_bar: ActionBar; action_bar: ActionBar
// Currently hovered ship // Currently hovered ship
ship_hovered: Ship | null; ship_hovered: Ship | null
// Ship tooltip // Ship tooltip
ship_tooltip: ShipTooltip; ship_tooltip: ShipTooltip
// Outcome dialog layer // Outcome dialog layer
outcome_layer: Phaser.Group; outcome_layer: Phaser.Group
// Character sheet // Character sheet
character_sheet: CharacterSheet; character_sheet: CharacterSheet
// Subscription to the battle log // Subscription to the battle log
log_processor: LogProcessor; log_processor: LogProcessor
// True if player interaction is allowed // True if player interaction is allowed
interacting: boolean; interacting: boolean
// Tactical mode toggle // Tactical mode toggle
toggle_tactical_mode: Toggle; toggle_tactical_mode: Toggle
// Init the view, binding it to a specific battle // Init the view, binding it to a specific battle
init(player: Player, battle: Battle) { init(player: Player, battle: Battle) {
@ -60,7 +59,6 @@ module TS.SpaceTac.UI {
this.player = player; this.player = player;
this.battle = battle; this.battle = battle;
this.targetting = null;
this.ship_hovered = null; this.ship_hovered = null;
this.background = null; this.background = null;
@ -104,6 +102,10 @@ module TS.SpaceTac.UI {
this.character_sheet = new CharacterSheet(this, -this.getWidth()); this.character_sheet = new CharacterSheet(this, -this.getWidth());
this.layer_sheets.add(this.character_sheet); 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 // "Battle" animation
this.displayFightMessage(); this.displayFightMessage();
@ -150,8 +152,6 @@ module TS.SpaceTac.UI {
// Leaving the view, we unbind the battle // Leaving the view, we unbind the battle
shutdown() { shutdown() {
this.exitTargettingMode();
this.log_processor.destroy(); this.log_processor.destroy();
super.shutdown(); super.shutdown();
@ -172,7 +172,7 @@ module TS.SpaceTac.UI {
// Method called when cursor starts hovering over a ship (or its icon) // Method called when cursor starts hovering over a ship (or its icon)
cursorOnShip(ship: Ship): void { cursorOnShip(ship: Ship): void {
if (!this.targetting || ship.alive) { if (!this.targetting.active || ship.alive) {
this.setShipHovered(ship); this.setShipHovered(ship);
} }
} }
@ -187,15 +187,15 @@ module TS.SpaceTac.UI {
// Method called when cursor moves in space // Method called when cursor moves in space
cursorInSpace(x: number, y: number): void { cursorInSpace(x: number, y: number): void {
if (!this.ship_hovered) { if (!this.ship_hovered) {
if (this.targetting) { if (this.targetting.active) {
this.targetting.setTargetSpace(x, y); this.targetting.setTarget(Target.newFromLocation(x, y));
} }
} }
} }
// Method called when cursor has been clicked (in space or on a ship) // Method called when cursor has been clicked (in space or on a ship)
cursorClicked(): void { cursorClicked(): void {
if (this.targetting) { if (this.targetting.active) {
this.targetting.validate(); this.targetting.validate();
} else if (this.ship_hovered && this.ship_hovered.getPlayer() == this.player && this.interacting) { } else if (this.ship_hovered && this.ship_hovered.getPlayer() == this.player && this.interacting) {
this.character_sheet.show(this.ship_hovered); this.character_sheet.show(this.ship_hovered);
@ -215,11 +215,11 @@ module TS.SpaceTac.UI {
this.ship_tooltip.hide(); this.ship_tooltip.hide();
} }
if (this.targetting) { if (this.targetting.active) {
if (ship) { if (ship) {
this.targetting.setTargetShip(ship); this.targetting.setTarget(Target.newFromShip(ship));
} else { } else {
this.targetting.unsetTarget(); this.targetting.setTarget(null);
} }
} }
} }
@ -240,25 +240,18 @@ module TS.SpaceTac.UI {
// Enter targetting mode // Enter targetting mode
// While in this mode, the Targetting object will receive hover and click events, and handle them // 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) { if (!this.interacting) {
return null; return null;
} }
if (this.targetting) { this.targetting.setAction(action);
this.exitTargettingMode();
}
this.targetting = new Targetting(this);
return this.targetting; return this.targetting;
} }
// Exit targetting mode // Exit targetting mode
exitTargettingMode(): void { exitTargettingMode(): void {
if (this.targetting) { this.targetting.setAction(null);
this.targetting.destroy();
}
this.targetting = null;
} }
/** /**

View file

@ -43,7 +43,7 @@ module TS.SpaceTac.UI {
/** /**
* Update displayed information * Update displayed information
*/ */
update(ship: Ship, action: BaseAction): void { update(ship: Ship, action: BaseAction, location: ArenaLocation = ship.location): void {
let yescolor = 0x000000; let yescolor = 0x000000;
let nocolor = 0x242022; let nocolor = 0x242022;
this.info.clear(); this.info.clear();
@ -54,7 +54,7 @@ module TS.SpaceTac.UI {
this.info.drawRect(0, 0, this.width, this.height); this.info.drawRect(0, 0, this.width, this.height);
this.info.beginFill(yescolor); 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) { if (action instanceof MoveAction) {
let safety = action.safety_distance / 2; let safety = action.safety_distance / 2;

View file

@ -2,68 +2,111 @@ module TS.SpaceTac.UI.Specs {
describe("Targetting", function () { describe("Targetting", function () {
let testgame = setupBattleview(); let testgame = setupBattleview();
it("broadcasts hovering and selection events", function () { it("draws simulation parts", function () {
var targetting = new Targetting(null); let targetting = new Targetting(testgame.battleview, testgame.battleview.action_bar);
var hovered: Target[] = []; let ship = nn(testgame.battleview.battle.playing_ship);
var selected: Target[] = []; ship.setArenaPosition(10, 20);
targetting.targetHovered.add((target: Target) => { let weapon = TestTools.addWeapon(ship);
hovered.push(target); let engine = TestTools.addEngine(ship, 12);
}); targetting.setAction(weapon.action);
targetting.targetSelected.add((target: Target) => {
selected.push(target);
});
targetting.setTargetSpace(1, 2); let drawvector = spyOn(targetting, "drawVector").and.stub();
expect(hovered).toEqual([Target.newFromLocation(1, 2)]);
expect(selected).toEqual([]);
targetting.validate(); let part = {
expect(hovered).toEqual([Target.newFromLocation(1, 2)]); action: weapon.action,
expect(selected).toEqual([Target.newFromLocation(1, 2)]); 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 () { it("updates impact indicators on ships inside the blast radius", function () {
let battleview = testgame.battleview; let targetting = new Targetting(testgame.battleview, testgame.battleview.action_bar);
let source = new Phaser.Group(battleview.game, battleview.arena); let ship = nn(testgame.battleview.battle.playing_ship);
source.position.set(0, 0);
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); expect(collect).toHaveBeenCalledTimes(1);
targetting.setTargetSpace(200, 100); 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.update();
targetting.updateApIndicators();
expect(targetting.ap_indicators.length).toBe(0); expect(targetting.container.visible).toBe(true);
expect(battleview.arena.layer_targetting.children.length).toBe(3); expect(targetting.drawn_info.visible).toBe(true);
expect(targetting.fire_arrow.visible).toBe(true);
targetting.setApIndicatorsInterval(Math.sqrt(5) * 20); expect(targetting.fire_arrow.position).toEqual(jasmine.objectContaining({ x: 156, y: 65 }));
expect(targetting.fire_arrow.rotation).toBeCloseTo(0.534594, 5);
expect(targetting.ap_indicators.length).toBe(5); expect(targetting.fire_blast.visible).toBe(true);
expect(battleview.arena.layer_targetting.children.length).toBe(3 + 5); expect(targetting.fire_blast.position).toEqual(jasmine.objectContaining({ x: 156, y: 65 }));
expect(targetting.ap_indicators[0].position.x).toBe(0); expect(targetting.move_ghost.visible).toBe(true);
expect(targetting.ap_indicators[0].position.y).toBe(0); expect(targetting.move_ghost.position).toEqual(jasmine.objectContaining({ x: 80, y: 20 }));
expect(targetting.ap_indicators[1].position.x).toBeCloseTo(40); expect(targetting.move_ghost.rotation).toBeCloseTo(0.534594, 5);
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);
}); });
}); });
} }

View file

@ -1,199 +1,265 @@
module TS.SpaceTac.UI { 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 { export class Targetting {
// Initial target (as pointed by the user) // Container group
target_initial: Target | null; container: Phaser.Group
line_initial: Phaser.Graphics;
// Corrected target (applying action rules) // Current action
target_corrected: Target | null; ship: Ship | null = null
line_corrected: Phaser.Graphics; action: BaseAction | null = null
target: Target | null = null
simulation = new MoveFireResult()
// Circle for effect radius // Movement projector
blast_radius: number; drawn_info: Phaser.Graphics
blast: Phaser.Image; move_ghost: Phaser.Image
// Signal to receive hovering events // Fire projector
targetHovered: Phaser.Signal; fire_arrow: Phaser.Image
fire_blast: Phaser.Image
fire_impact: Phaser.Group
// Signal to receive targetting events // Collaborators to update
targetSelected: Phaser.Signal; actionbar: ActionBar
// AP usage display // Access to the parent view
ap_interval: number = 0; view: BaseView
ap_indicators: Phaser.Image[] = [];
// Access to the parent battle view constructor(view: BaseView, actionbar: ActionBar) {
private battleview: BattleView | null; this.view = view;
this.actionbar = actionbar;
// Source of the targetting this.container = view.add.group();
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();
// Visual effects // Visual effects
if (battleview) { this.drawn_info = new Phaser.Graphics(view.game, 0, 0);
this.blast = new Phaser.Image(battleview.game, 0, 0, "battle-arena-blast"); this.drawn_info.visible = false;
this.blast.anchor.set(0.5, 0.5); this.move_ghost = new Phaser.Image(view.game, 0, 0, "common-transparent");
this.blast.visible = false; this.move_ghost.anchor.set(0.5, 0.5);
battleview.arena.layer_targetting.add(this.blast); this.move_ghost.alpha = 0.8;
this.line_initial = new Phaser.Graphics(battleview.game, 0, 0); this.move_ghost.visible = false;
this.line_initial.visible = false; this.fire_arrow = new Phaser.Image(view.game, 0, 0, "battle-arena-indicators", 0);
battleview.arena.layer_targetting.add(this.line_initial); this.fire_arrow.anchor.set(1, 0.5);
this.line_corrected = new Phaser.Graphics(battleview.game, 0, 0); this.fire_arrow.visible = false;
this.line_corrected.visible = false; this.fire_impact = new Phaser.Group(view.game);
battleview.arena.layer_targetting.add(this.line_corrected); 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.container.add(this.fire_impact);
this.target_initial = null; this.container.add(this.fire_blast);
this.target_corrected = null; this.container.add(this.drawn_info);
this.container.add(this.fire_arrow);
this.container.add(this.move_ghost);
} }
// Destructor /**
destroy(): void { * Move to a given view layer
this.targetHovered.dispose(); */
this.targetSelected.dispose(); moveToLayer(layer: Phaser.Group): void {
if (this.line_initial) { layer.add(this.container);
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([]);
}
} }
// Set AP indicators to display at fixed interval along the line /**
setApIndicatorsInterval(interval: number) { * Indicator that the targetting is currently active
this.ap_interval = interval; */
this.updateApIndicators(); get active(): boolean {
return (this.ship && this.action) ? true : false;
} }
// Update visual effects for current targetting /**
update(): void { * Draw a vector, with line and gradation
if (this.battleview) { */
if (this.source && this.target_initial) { drawVector(color: number, x1: number, y1: number, x2: number, y2: number, gradation = 0) {
this.line_initial.clear(); let line = this.drawn_info;
this.line_initial.lineStyle(3, 0x666666); line.lineStyle(6, color);
this.line_initial.moveTo(this.source.x, this.source.y); line.moveTo(x1, y1);
this.line_initial.lineTo(this.target_initial.x, this.target_initial.y); line.lineTo(x2, y2);
this.line_initial.visible = true; line.visible = true;
} else {
this.line_initial.visible = false; 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() { * Draw a part of the simulation
if (!this.battleview || !this.source) { */
drawPart(part: MoveFirePart, enabled = true, previous: MoveFirePart | null = null): void {
if (!this.ship) {
return; return;
} }
// Get indicator count let move = part.action instanceof MoveAction;
let count = 0; let color = (enabled && part.possible) ? (move ? 0xe09c47 : 0xdc6441) : 0x8e8e8e;
let distance = 0; let src = previous ? previous.target : this.ship.location;
if (this.line_corrected.visible && this.ap_interval > 0 && this.target_corrected) { let gradation = part.action instanceof MoveAction ? part.action.distance_per_power : 0;
distance = this.target_corrected.getDistanceTo(Target.newFromLocation(this.source.x, this.source.y)) - 0.00001; this.drawVector(color, src.x, src.y, part.target.x, part.target.y, gradation);
count = Math.ceil(distance / this.ap_interval); }
/**
* 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 if (ships.length) {
while (this.ap_indicators.length < count) { this.fire_impact.removeAll(true);
let indicator = new Phaser.Image(this.battleview.game, 0, 0, "battle-arena-ap-indicator"); ships.forEach(iship => {
indicator.anchor.set(0.5, 0.5); let frame = this.view.add.image(iship.arena_x, iship.arena_y, "battle-arena-ship-frames", 5, this.fire_impact);
this.battleview.arena.layer_targetting.add(indicator); frame.anchor.set(0.5);
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);
}); });
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) { * Update visual effects to show the simulation of current action/target
this.source = sprite; */
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 { * Simulate current action
this.target_corrected = target; */
this.blast_radius = blast_radius; simulate(): void {
if (dispatch) { if (this.ship && this.action && this.target) {
this.target_initial = target ? copy(target) : null; let simulator = new MoveFireSimulator(this.ship);
this.targetHovered.dispatch(this.target_corrected); 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(); this.update();
} }
// Set no target /**
unsetTarget(dispatch: boolean = true): void { * Set the target for current action
this.setTarget(null, dispatch); */
} setTarget(target: Target | null): void {
this.target = target;
// Set the current target ship (when hovered) this.update();
setTargetShip(ship: Ship, dispatch: boolean = true): void { if (this.action) {
if (ship.alive) { this.actionbar.updateFromSimulation(this.action, this.simulation);
this.setTarget(Target.newFromShip(ship), dispatch);
} }
} }
// Set the current target in space (when hovered) /**
setTargetSpace(x: number, y: number, dispatch: boolean = true): void { * Validate the current target.
this.setTarget(Target.newFromLocation(x, y)); *
} * This will make the needed approach and apply the action.
*/
// Validate the current target (when clicked)
// This will broadcast the targetSelected signal
validate(): void { 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();
}
} }
} }
} }