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