diff --git a/graphics/ui/map.svg b/graphics/ui/map.svg
index fa1cd53..3586ea9 100644
--- a/graphics/ui/map.svg
+++ b/graphics/ui/map.svg
@@ -231,7 +231,7 @@
fy="142.875"
r="125.15247"
gradientUnits="userSpaceOnUse"
- gradientTransform="translate(-79.375026)" />
+ gradientTransform="translate(-32.808351)" />
-
-
-
+
+
+
+
+
+
+
+
+
+
image/svg+xml
-
+
@@ -900,7 +936,7 @@
@@ -927,7 +963,7 @@
@@ -949,17 +985,17 @@
inkscape:export-filename="/home/michael/workspace/perso/spacetac/out/assets/images/map/location-planet.png"
inkscape:export-xdpi="96"
inkscape:export-ydpi="96"
- transform="translate(-79.375026)">
+ transform="translate(-32.808351)">
+ style="opacity:0.44200003;fill:url(#radialGradient7460);fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:0.3233197;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" />
@@ -983,7 +1019,8 @@
id="g5591"
inkscape:export-filename="/home/michael/workspace/perso/spacetac/out/assets/images/map/location-warp.png"
inkscape:export-xdpi="96"
- inkscape:export-ydpi="96">
+ inkscape:export-ydpi="96"
+ transform="translate(46.566669)">
+ id="g9054"
+ transform="translate(46.566669)">
+ transform="translate(-32.808351)">
+ style="opacity:1;fill:url(#radialGradient7464);fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:0.56499994;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" />
+ transform="translate(-32.808351)">
+ transform="translate(-32.808351)">
+ transform="rotate(135.15474,259.29761,143.11838)" />
@@ -1272,7 +1310,8 @@
id="g5568"
inkscape:export-filename="/home/michael/workspace/perso/spacetac/out/assets/images/map/current-location.png"
inkscape:export-xdpi="96"
- inkscape:export-ydpi="96">
+ inkscape:export-ydpi="96"
+ transform="translate(46.566669)">
3
+
+
+ Find the smugglers' hideout
+
+
+ Keep Acalanth alive
+
+
+ !
+
+
+
+ Deliver the intel
+
+
+
+
+
+
+ Go back to Warna Sector for reward
+
+
+
+
+
{
+ it("starts the main story arc", function () {
+ let missions = new ActiveMissions();
+ expect(missions.main).toBeNull();
+
+ let session = new GameSession();
+ session.startNewGame(true, false);
+
+ missions.startMainStory(session.universe, session.player.fleet);
+ expect(missions.main).not.toBeNull();
+ })
+
+ it("gets the current list of missions, and updates them", function () {
+ let missions = new ActiveMissions();
+
+ missions.main = new Mission([new MissionPart("Do something")]);
+ missions.secondary = [
+ new Mission([new MissionPart("Maybe do something")]),
+ new Mission([new MissionPart("Surely do something")])
+ ];
+
+ expect(missions.getCurrent().map(mission => mission.current_part.title)).toEqual([
+ "Do something",
+ "Maybe do something",
+ "Surely do something",
+ ]);
+
+ let universe = new Universe();
+ let fleet = new Fleet();
+ missions.checkStatus(fleet, universe);
+
+ expect(missions.getCurrent().map(mission => mission.current_part.title)).toEqual([
+ "Do something",
+ "Maybe do something",
+ "Surely do something",
+ ]);
+
+ spyOn(missions.secondary[0].current_part, "checkCompleted").and.returnValue(true);
+ missions.checkStatus(fleet, universe);
+
+ expect(missions.getCurrent().map(mission => mission.current_part.title)).toEqual([
+ "Do something",
+ "Surely do something",
+ ]);
+
+ spyOn(missions.main.current_part, "checkCompleted").and.returnValue(true);
+ missions.checkStatus(fleet, universe);
+
+ expect(missions.getCurrent().map(mission => mission.current_part.title)).toEqual([
+ "Surely do something",
+ ]);
+ expect(missions.main).toBeNull();
+ })
+ })
+}
diff --git a/src/core/missions/ActiveMissions.ts b/src/core/missions/ActiveMissions.ts
new file mode 100644
index 0000000..4994d98
--- /dev/null
+++ b/src/core/missions/ActiveMissions.ts
@@ -0,0 +1,44 @@
+module TS.SpaceTac {
+ /**
+ * A list of active missions
+ */
+ export class ActiveMissions {
+ main: Mission | null = null
+ secondary: Mission[] = []
+
+ constructor() {
+ }
+
+ /**
+ * Start the main story arc
+ */
+ startMainStory(universe: Universe, fleet: Fleet) {
+ this.main = new MainStory(universe, fleet);
+ }
+
+ /**
+ * Get the current list of active missions
+ */
+ getCurrent(): Mission[] {
+ let result: Mission[] = [];
+ if (this.main) {
+ result.push(this.main);
+ }
+ return result.concat(this.secondary);
+ }
+
+ /**
+ * Check status for all active missions
+ *
+ * This will remove ended missions
+ */
+ checkStatus(fleet: Fleet, universe: Universe): void {
+ if (this.main) {
+ if (!this.main.checkStatus(fleet, universe)) {
+ this.main = null;
+ }
+ }
+ this.secondary = this.secondary.filter(mission => mission.checkStatus(fleet, universe));
+ }
+ }
+}
diff --git a/src/core/missions/MainStory.spec.ts b/src/core/missions/MainStory.spec.ts
new file mode 100644
index 0000000..509e2d7
--- /dev/null
+++ b/src/core/missions/MainStory.spec.ts
@@ -0,0 +1,34 @@
+module TS.SpaceTac.Specs {
+ describe("MainStory", () => {
+ function checkPart(story: Mission, index: number, title: string) {
+ expect(story.parts.indexOf(story.current_part)).toBe(index);
+ expect(story.current_part.title).toMatch(title);
+ expect(story.completed).toBe(false);
+ }
+
+ function goTo(fleet: Fleet, location: StarLocation, win_encounter = true) {
+ fleet.setLocation(location, true);
+ if (fleet.battle) {
+ fleet.battle.endBattle(win_encounter ? fleet : fleet.battle.fleets[1]);
+ if (win_encounter) {
+ fleet.player.exitBattle();
+ } else {
+ fleet.player.revertBattle();
+ }
+ }
+ }
+
+ it("can be completed", function () {
+ let session = new GameSession();
+ session.startNewGame(true, true);
+ let fleet = nn(session.player.fleet);
+
+ let missions = session.player.missions;
+ let story = nn(missions.main);
+ checkPart(story, 0, "^Find your contact in .* system$");
+
+ goTo(fleet, (story.current_part).destination);
+ expect(story.completed).toBe(true);
+ })
+ })
+}
diff --git a/src/core/missions/MainStory.ts b/src/core/missions/MainStory.ts
new file mode 100644
index 0000000..eb0279a
--- /dev/null
+++ b/src/core/missions/MainStory.ts
@@ -0,0 +1,23 @@
+///
+
+module TS.SpaceTac {
+ function randomLocation(stars: Star[], excludes: StarLocation[] = []) {
+ let random = RandomGenerator.global;
+ let star = stars.length == 1 ? stars[0] : random.choice(stars);
+ return RandomGenerator.global.choice(star.locations.filter(loc => !contains(excludes, loc)));
+ }
+
+ /**
+ * Main story arc
+ */
+ export class MainStory extends Mission {
+ constructor(universe: Universe, fleet: Fleet) {
+ let random = RandomGenerator.global;
+ let location = nn(fleet.location);
+
+ super([
+ new MissionPartGoTo(randomLocation([location.star], [location]), "Find your contact")
+ ], true);
+ }
+ }
+}
diff --git a/src/core/missions/Mission.spec.ts b/src/core/missions/Mission.spec.ts
new file mode 100644
index 0000000..00926f9
--- /dev/null
+++ b/src/core/missions/Mission.spec.ts
@@ -0,0 +1,36 @@
+module TS.SpaceTac.Specs {
+ describe("Mission", () => {
+ it("check step status", function () {
+ let mission = new Mission([
+ new MissionPart("Part 1"),
+ new MissionPart("Part 2")
+ ]);
+ let universe = new Universe();
+ let fleet = new Fleet();
+
+ expect(mission.current_part).toBe(mission.parts[0]);
+
+ let result = mission.checkStatus(fleet, universe);
+ expect(result).toBe(true);
+ expect(mission.current_part).toBe(mission.parts[0]);
+
+ spyOn(mission.parts[0], "checkCompleted").and.returnValues(false, true);
+
+ result = mission.checkStatus(fleet, universe);
+ expect(result).toBe(true);
+ expect(mission.current_part).toBe(mission.parts[0]);
+ result = mission.checkStatus(fleet, universe);
+ expect(result).toBe(true);
+ expect(mission.current_part).toBe(mission.parts[1]);
+ result = mission.checkStatus(fleet, universe);
+ expect(result).toBe(true);
+ expect(mission.current_part).toBe(mission.parts[1]);
+
+ spyOn(mission.parts[1], "checkCompleted").and.returnValue(true);
+
+ result = mission.checkStatus(fleet, universe);
+ expect(result).toBe(false);
+ expect(mission.current_part).toBe(mission.parts[1]);
+ })
+ })
+}
diff --git a/src/core/missions/Mission.ts b/src/core/missions/Mission.ts
new file mode 100644
index 0000000..f57e1e9
--- /dev/null
+++ b/src/core/missions/Mission.ts
@@ -0,0 +1,47 @@
+module TS.SpaceTac {
+ /**
+ * A mission (or quest) assigned to the player
+ */
+ export class Mission {
+ // Indicator that the quest is part of the main story arc
+ main: boolean
+
+ // Parts of the mission
+ parts: MissionPart[]
+
+ // Current part
+ current_part: MissionPart
+
+ // Indicator that the mission is completed
+ completed: boolean
+
+ constructor(parts: MissionPart[], main = false) {
+ this.main = main;
+ this.parts = parts;
+ this.current_part = parts[0];
+ this.completed = false;
+ }
+
+ /**
+ * Check the status for current part, and move on to next part if necessary.
+ *
+ * Returns true if the mission is still active.
+ */
+ checkStatus(fleet: Fleet, universe: Universe): boolean {
+ if (this.completed) {
+ return false;
+ } else if (this.current_part.checkCompleted(fleet, universe)) {
+ let current_index = this.parts.indexOf(this.current_part);
+ if (current_index < 0 || current_index >= this.parts.length - 1) {
+ this.completed = true;
+ return false;
+ } else {
+ this.current_part = this.parts[current_index + 1];
+ return true;
+ }
+ } else {
+ return true;
+ }
+ }
+ }
+}
diff --git a/src/core/missions/MissionPart.ts b/src/core/missions/MissionPart.ts
new file mode 100644
index 0000000..0db9646
--- /dev/null
+++ b/src/core/missions/MissionPart.ts
@@ -0,0 +1,20 @@
+module TS.SpaceTac {
+ /**
+ * An abstract part of a mission, describing the goal
+ */
+ export class MissionPart {
+ // Very short description
+ title: string
+
+ constructor(title: string) {
+ this.title = title;
+ }
+
+ /**
+ * Abstract checking if the part is completed
+ */
+ checkCompleted(fleet: Fleet, universe: Universe): boolean {
+ return false;
+ }
+ }
+}
diff --git a/src/core/missions/MissionPartGoTo.spec.ts b/src/core/missions/MissionPartGoTo.spec.ts
new file mode 100644
index 0000000..a6d79e7
--- /dev/null
+++ b/src/core/missions/MissionPartGoTo.spec.ts
@@ -0,0 +1,27 @@
+module TS.SpaceTac.Specs {
+ describe("MissionPartGoTo", () => {
+ it("completes when the fleet is at location, without encounter", function () {
+ let destination = new StarLocation(new Star(null, 0, 0, "Atanax"));
+ destination.encounter_random = new SkewedRandomGenerator([0], true);
+ let part = new MissionPartGoTo(destination, "Collect gems");
+
+ let universe = new Universe();
+ let fleet = new Fleet();
+
+ expect(part.title).toEqual("Collect gems in Atanax system");
+ expect(part.checkCompleted(fleet, universe)).toBe(false, "Init location");
+
+ fleet.setLocation(destination, true);
+ expect(part.checkCompleted(fleet, universe)).toBe(false, "Encounter not clear");
+
+ destination.clearEncounter();
+ expect(part.checkCompleted(fleet, universe)).toBe(true, "Encouter cleared");
+
+ fleet.setLocation(new StarLocation(), true);
+ expect(part.checkCompleted(fleet, universe)).toBe(false, "Went to another system");
+
+ fleet.setLocation(destination, true);
+ expect(part.checkCompleted(fleet, universe)).toBe(true, "Back at destination");
+ })
+ })
+}
diff --git a/src/core/missions/MissionPartGoTo.ts b/src/core/missions/MissionPartGoTo.ts
new file mode 100644
index 0000000..9639195
--- /dev/null
+++ b/src/core/missions/MissionPartGoTo.ts
@@ -0,0 +1,20 @@
+///
+
+module TS.SpaceTac {
+ /**
+ * A mission part that requires the fleet to go to a specific location
+ */
+ export class MissionPartGoTo extends MissionPart {
+ destination: StarLocation
+
+ constructor(destination: StarLocation, directive: string, hint = true) {
+ super(hint ? `${directive} in ${destination.star.name} system` : directive);
+
+ this.destination = destination;
+ }
+
+ checkCompleted(fleet: Fleet, universe: Universe): boolean {
+ return fleet.location === this.destination && this.destination.isClear();
+ }
+ }
+}
diff --git a/src/ui/Preload.ts b/src/ui/Preload.ts
index adb91c5..8425d29 100644
--- a/src/ui/Preload.ts
+++ b/src/ui/Preload.ts
@@ -68,7 +68,8 @@ module TS.SpaceTac.UI {
this.loadImage("map/location-star.png");
this.loadImage("map/location-planet.png");
this.loadImage("map/location-warp.png");
- this.loadSheet("map/status.png", 32, 32);
+ this.loadSheet("map/status.png", 32);
+ this.loadSheet("map/missions.png", 70);
this.loadImage("character/sheet.png");
this.loadImage("character/close.png");
this.loadImage("character/ship.png");
diff --git a/src/ui/common/UIComponent.ts b/src/ui/common/UIComponent.ts
index 8b56244..4e7e9ee 100644
--- a/src/ui/common/UIComponent.ts
+++ b/src/ui/common/UIComponent.ts
@@ -202,12 +202,10 @@ module TS.SpaceTac.UI {
/**
* Add a static text.
*/
- addText(x: number, y: number, content: string, color = "#ffffff", size = 16, bold = false, center = true, width = 0): void {
+ addText(x: number, y: number, content: string, color = "#ffffff", size = 16, bold = false, center = true, width = 0, vcenter = center): void {
let style = { font: `${bold ? "bold " : ""}${size}pt Arial`, fill: color, align: center ? "center" : "left" };
let text = new Phaser.Text(this.view.game, x, y, content, style);
- if (center) {
- text.anchor.set(0.5, 0.5);
- }
+ text.anchor.set(center ? 0.5 : 0, vcenter ? 0.5 : 0);
if (width) {
text.wordWrap = true;
text.wordWrapWidth = width;
diff --git a/src/ui/map/ActiveMissionsDisplay.spec.ts b/src/ui/map/ActiveMissionsDisplay.spec.ts
new file mode 100644
index 0000000..9da1deb
--- /dev/null
+++ b/src/ui/map/ActiveMissionsDisplay.spec.ts
@@ -0,0 +1,23 @@
+module TS.SpaceTac.UI.Specs {
+ describe("ActiveMissionsDisplay", function () {
+ let testgame = setupEmptyView();
+
+ it("displays active missions", function () {
+ let view = testgame.baseview;
+ let missions = new ActiveMissions();
+ let display = new ActiveMissionsDisplay(view, missions);
+
+ let container = (display).container;
+ expect(container.children.length).toBe(0);
+
+ missions.secondary.push(new Mission([
+ new MissionPart("Get back to base")
+ ]));
+
+ display.update();
+ expect(container.children.length).toBe(2);
+ expect(container.children[0] instanceof Phaser.Image).toBe(true);
+ checkText(container.children[1], "Get back to base");
+ });
+ });
+}
diff --git a/src/ui/map/ActiveMissionsDisplay.ts b/src/ui/map/ActiveMissionsDisplay.ts
new file mode 100644
index 0000000..c8aedbf
--- /dev/null
+++ b/src/ui/map/ActiveMissionsDisplay.ts
@@ -0,0 +1,31 @@
+///
+
+module TS.SpaceTac.UI {
+ /**
+ * Widget to display the active missions list
+ */
+ export class ActiveMissionsDisplay extends UIComponent {
+ private missions: ActiveMissions
+
+ constructor(parent: BaseView, missions: ActiveMissions) {
+ super(parent, 520, 210);
+ this.missions = missions;
+
+ this.update();
+ }
+
+ /**
+ * Update the current missions list
+ */
+ update() {
+ this.clearContent();
+
+ let active = this.missions.getCurrent();
+ let offset = 245 - active.length * 70;
+ active.forEach((mission, idx) => {
+ this.addImage(35, offset + 70 * idx, "map-missions");
+ this.addText(90, offset + 70 * idx, mission.current_part.title, "#d2e1f3", 22, false, false, 430, true);
+ });
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/ui/map/UniverseMapView.ts b/src/ui/map/UniverseMapView.ts
index b2e8d15..99b1582 100644
--- a/src/ui/map/UniverseMapView.ts
+++ b/src/ui/map/UniverseMapView.ts
@@ -34,6 +34,9 @@ module TS.SpaceTac.UI {
// Actions for selected location
actions: MapLocationMenu
+ // Active missions
+ missions: ActiveMissionsDisplay
+
// Character sheet
character_sheet: CharacterSheet
@@ -91,6 +94,10 @@ module TS.SpaceTac.UI {
this.actions.setPosition(30, 30);
this.actions.moveToLayer(this.layer_overlay);
+ this.missions = new ActiveMissionsDisplay(this, this.player.missions);
+ this.missions.setPosition(20, 720);
+ this.missions.moveToLayer(this.layer_overlay);
+
this.zoom_in = new Phaser.Button(this.game, 1540, 172, "map-buttons", () => this.setZoom(this.zoom + 1), undefined, 3, 0);
this.zoom_in.anchor.set(0.5, 0.5);
this.layer_overlay.add(this.zoom_in);
@@ -153,6 +160,8 @@ module TS.SpaceTac.UI {
this.actions.setFromLocation(this.player.fleet.location, this);
+ this.missions.update();
+
if (interactive) {
this.setInteractionEnabled(true);
}
@@ -258,6 +267,7 @@ module TS.SpaceTac.UI {
setInteractionEnabled(enabled: boolean) {
this.interactive = enabled && !this.session.spectator;
this.actions.setVisible(enabled && this.zoom == 2, 300);
+ this.missions.setVisible(enabled && this.zoom == 2, 300);
this.animations.setVisible(this.zoom_in, enabled && this.zoom < 2, 300);
this.animations.setVisible(this.zoom_out, enabled && this.zoom > 0, 300);
this.animations.setVisible(this.character_sheet, enabled, 300);