From 7c03a1e2d138af569250d5a94a5caf5096deaee3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C3=ABl=20Lemaire?= Date: Tue, 11 Jul 2017 00:50:38 +0200 Subject: [PATCH] Added mission difficulty and reward --- TODO.md | 6 +- src/core/Fleet.spec.ts | 31 ++++++++ src/core/Fleet.ts | 14 ++++ src/core/missions/Mission.spec.ts | 36 +++++++++ src/core/missions/Mission.ts | 62 ++++++++++++++- src/core/missions/MissionGenerator.spec.ts | 90 +++++++++++++++++++--- src/core/missions/MissionGenerator.ts | 73 ++++++++++++++++-- src/ui/map/MissionsDialog.spec.ts | 60 +++++++++++++++ src/ui/map/MissionsDialog.ts | 11 ++- src/ui/map/UniverseMapView.ts | 3 +- 10 files changed, 363 insertions(+), 23 deletions(-) create mode 100644 src/ui/map/MissionsDialog.spec.ts diff --git a/TODO.md b/TODO.md index fd57971..cbdeb9f 100644 --- a/TODO.md +++ b/TODO.md @@ -12,11 +12,9 @@ Map/story * Add initial character creation * Fix quickly zooming in twice preventing to display some UI parts -* Enemy fleet size should start low and increase with system level +* Enemy fleet size should start low and increase with system level (there should be less locations in systems too) * Allow to change/buy ship model -* Add ship personality (with icons to identify ?), with reaction dialogs * Add factions and reputation -* Add generated missions with rewards * Allow to cancel secondary missions * Forbid to end up with more than 5 ships in the fleet because of escorts * Show missions' destination near systems/locations @@ -61,6 +59,7 @@ Ships models and equipments * Chance to hit should increase with precision * Add actions with cost dependent of distance (like current move actions) * Add hull points to drones and make them take area damage +* "Shield Transfer" has no quality offsets Artificial Intelligence ----------------------- @@ -94,6 +93,7 @@ Postponed * Replays * Multiplayer/co-op * Formation or deployment phase +* Add ship personality (with icons to identify ?), with reaction dialogs * New battle internal flow: any game state change should be done through revertable events * Animated arena background, instead of big picture * Hide enemy information (shield, hull, weapons), until they are in play, or until a "spy" effect is used diff --git a/src/core/Fleet.spec.ts b/src/core/Fleet.spec.ts index 10009d4..8a516a9 100644 --- a/src/core/Fleet.spec.ts +++ b/src/core/Fleet.spec.ts @@ -108,5 +108,36 @@ module TS.SpaceTac { ship4.setDead(); expect(fleet.isAlive()).toBe(false); }); + + it("adds cargo in first empty slot", function () { + let fleet = new Fleet(); + let ship1 = fleet.addShip(); + ship1.cargo_space = 1; + let ship2 = fleet.addShip(); + ship2.cargo_space = 2; + + expect(ship1.cargo).toEqual([]); + expect(ship2.cargo).toEqual([]); + + let result = fleet.addCargo(new Equipment()); + expect(result).toBe(true); + expect(ship1.cargo).toEqual([new Equipment()]); + expect(ship2.cargo).toEqual([]); + + result = fleet.addCargo(new Equipment()); + expect(result).toBe(true); + expect(ship1.cargo).toEqual([new Equipment()]); + expect(ship2.cargo).toEqual([new Equipment()]); + + result = fleet.addCargo(new Equipment()); + expect(result).toBe(true); + expect(ship1.cargo).toEqual([new Equipment()]); + expect(ship2.cargo).toEqual([new Equipment(), new Equipment()]); + + result = fleet.addCargo(new Equipment()); + expect(result).toBe(false); + expect(ship1.cargo).toEqual([new Equipment()]); + expect(ship2.cargo).toEqual([new Equipment(), new Equipment()]); + }); }); } diff --git a/src/core/Fleet.ts b/src/core/Fleet.ts index 08dcd80..0197cc2 100644 --- a/src/core/Fleet.ts +++ b/src/core/Fleet.ts @@ -102,5 +102,19 @@ module TS.SpaceTac { return any(this.ships, ship => ship.alive); } } + + /** + * Add an equipment to the first available cargo slot + * + * Returns true on success, false if no empty cargo slot was available. + */ + addCargo(equipment: Equipment): boolean { + let ship = first(this.ships, ship => ship.getFreeCargoSpace() > 0); + if (ship) { + return ship.addCargo(equipment); + } else { + return false; + } + } } } diff --git a/src/core/missions/Mission.spec.ts b/src/core/missions/Mission.spec.ts index 2545774..e86ee03 100644 --- a/src/core/missions/Mission.spec.ts +++ b/src/core/missions/Mission.spec.ts @@ -31,5 +31,41 @@ module TS.SpaceTac.Specs { expect(result).toBe(false); expect(mission.current_part).toBe(mission.parts[1]); }) + + it("stores a reward", function () { + let mission = new Mission(new Universe()); + expect(mission.getRewardText()).toEqual("-"); + + mission.reward = 720; + expect(mission.getRewardText()).toEqual("720 zotys"); + + mission.reward = new Equipment(); + mission.reward.name = "Super Equipment"; + expect(mission.getRewardText()).toEqual("Super Equipment Mk1"); + }) + + it("gives the reward on completion", function () { + let fleet = new Fleet(); + let ship = fleet.addShip(); + ship.cargo_space = 5; + fleet.credits = 150; + + let mission = new Mission(new Universe(), fleet); + mission.reward = 75; + mission.setCompleted(); + expect(mission.completed).toBe(true); + expect(fleet.credits).toBe(225); + + mission.setCompleted(); + expect(fleet.credits).toBe(225); + + mission = new Mission(new Universe(), fleet); + mission.reward = new Equipment(); + expect(ship.cargo).toEqual([]); + mission.setCompleted(); + expect(mission.completed).toBe(true); + expect(fleet.credits).toBe(225); + expect(ship.cargo).toEqual([mission.reward]); + }) }) } diff --git a/src/core/missions/Mission.ts b/src/core/missions/Mission.ts index 3cdc688..d60c774 100644 --- a/src/core/missions/Mission.ts +++ b/src/core/missions/Mission.ts @@ -1,4 +1,18 @@ module TS.SpaceTac { + /** + * Reward for a mission (either an equipment or money) + */ + export type MissionReward = Equipment | number + + /** + * Level of difficulty for a mission + */ + export enum MissionDifficulty { + easy, + normal, + hard + } + /** * A mission (or quest) assigned to the player */ @@ -24,6 +38,13 @@ module TS.SpaceTac { // Title of this mission (should be kept short) title: string + // Estimated mission difficulty and value (expected reward value) + difficulty: MissionDifficulty = MissionDifficulty.normal + value = 0 + + // Reward when this mission is completed + reward: MissionReward | null = null + // Numerical identifier id = -1 @@ -58,6 +79,29 @@ module TS.SpaceTac { return this.parts.indexOf(this.current_part); } + /** + * Get a small text describing the associated reward + */ + getRewardText(): string { + if (this.reward) { + if (this.reward instanceof Equipment) { + return this.reward.getFullName(); + } else { + return `${this.reward} zotys`; + } + } else { + return "-"; + } + } + + /** + * Set the difficulty level + */ + setDifficulty(description: MissionDifficulty, value: number) { + this.difficulty = description; + this.value = value; + } + /** * Set the mission as started (start the first part) */ @@ -83,7 +127,7 @@ module TS.SpaceTac { let current_index = this.getIndex(); if (current_index < 0 || current_index >= this.parts.length - 1) { - this.completed = true; + this.setCompleted(); return false; } else { this.current_part = this.parts[current_index + 1]; @@ -94,5 +138,21 @@ module TS.SpaceTac { return true; } } + + /** + * Set the mission as completed, and give the reward to the fleet + */ + setCompleted(): void { + if (!this.completed) { + this.completed = true; + if (this.reward) { + if (this.reward instanceof Equipment) { + this.fleet.addCargo(this.reward); + } else { + this.fleet.credits += this.reward; + } + } + } + } } } diff --git a/src/core/missions/MissionGenerator.spec.ts b/src/core/missions/MissionGenerator.spec.ts index bed9d22..378cca6 100644 --- a/src/core/missions/MissionGenerator.spec.ts +++ b/src/core/missions/MissionGenerator.spec.ts @@ -1,6 +1,6 @@ module TS.SpaceTac.Specs { - describe("MissionGenerator", () => { - it("generates escort missions", () => { + describe("MissionGenerator", function () { + it("generates escort missions", function () { let universe = new Universe(); let star1 = universe.addStar(1); let loc1 = star1.locations[0]; @@ -17,11 +17,11 @@ module TS.SpaceTac.Specs { let escort = mission.parts[0]; expect(escort.destination).toBe(loc2); expect(escort.ship.level.get()).toBe(2); - }); + }) - it("generates location cleaning missions", () => { + it("generates location cleaning missions", function () { let universe = new Universe(); - let star1 = universe.addStar(1); + let star1 = universe.addStar(1, "TTX"); let loc1 = star1.locations[0]; let loc2 = star1.addLocation(StarLocationType.PLANET); @@ -29,10 +29,82 @@ module TS.SpaceTac.Specs { let mission = generator.generateCleanLocation(); expect(mission.title).toBe("Defeat a level 1 fleet in this system"); - expect(mission.parts.length).toBe(1); + expect(mission.parts.length).toBe(2); expect(mission.parts[0] instanceof MissionPartCleanLocation).toBe(true); - let part = mission.parts[0]; - expect(part.destination).toBe(loc2); - }); + let part1 = mission.parts[0]; + expect(part1.destination).toBe(loc2); + expect(part1.title).toEqual("Clean a planet in TTX system"); + expect(mission.parts[0] instanceof MissionPartGoTo).toBe(true); + let part2 = mission.parts[1]; + expect(part2.destination).toBe(loc1); + expect(part2.title).toEqual("Go back to collect your reward"); + }) + + it("helps to evaluate mission difficulty", function () { + let generator = new MissionGenerator(new Universe(), new StarLocation()); + let mission = new Mission(generator.universe); + expect(mission.difficulty).toBe(MissionDifficulty.normal); + expect(mission.value).toBe(0); + + generator.setDifficulty(mission, 1000, 1); + expect(mission.difficulty).toBe(MissionDifficulty.normal); + expect(mission.value).toBe(1000); + + generator.setDifficulty(mission, 1000, 2); + expect(mission.difficulty).toBe(MissionDifficulty.hard); + expect(mission.value).toBe(2200); + + generator.setDifficulty(mission, 1000, 3); + expect(mission.difficulty).toBe(MissionDifficulty.hard); + expect(mission.value).toBe(3600); + + generator.around.star.level = 10; + + generator.setDifficulty(mission, 1000, 10); + expect(mission.difficulty).toBe(MissionDifficulty.normal); + expect(mission.value).toBe(10000); + + generator.setDifficulty(mission, 1000, 9); + expect(mission.difficulty).toBe(MissionDifficulty.easy); + expect(mission.value).toBe(8100); + + generator.setDifficulty(mission, 1000, 8); + expect(mission.difficulty).toBe(MissionDifficulty.easy); + expect(mission.value).toBe(6400); + }) + + it("generates equipment reward", function () { + let generator = new MissionGenerator(new Universe(), new StarLocation()); + let template = new LootTemplate(SlotType.Weapon, "Test Weapon"); + generator.equipment_generator.templates = [template]; + + template.price = 350; + let result = generator.tryGenerateEquipmentReward(500); + expect(result).toBeNull(); + + template.price = 800; + result = generator.tryGenerateEquipmentReward(500); + expect(result).toBeNull(); + + template.price = 500; + result = generator.tryGenerateEquipmentReward(500); + expect(result).not.toBeNull(); + }) + + it("falls back to money reward when no suitable equipment have been generated", function () { + let generator = new MissionGenerator(new Universe(), new StarLocation()); + generator.equipment_generator.templates = []; + + let result = generator.generateReward(15000); + expect(result).toBe(15000); + + let template = new LootTemplate(SlotType.Weapon, "Test Weapon"); + template.price = 15000; + generator.equipment_generator.templates.push(template); + + generator.random = new SkewedRandomGenerator([0], true); + result = generator.generateReward(15000); + expect(result instanceof Equipment).toBe(true); + }) }); } diff --git a/src/core/missions/MissionGenerator.ts b/src/core/missions/MissionGenerator.ts index 72bede1..f557f37 100644 --- a/src/core/missions/MissionGenerator.ts +++ b/src/core/missions/MissionGenerator.ts @@ -21,11 +21,13 @@ module TS.SpaceTac { universe: Universe around: StarLocation random: RandomGenerator + equipment_generator: LootGenerator constructor(universe: Universe, around: StarLocation, random = RandomGenerator.global) { this.universe = universe; this.around = around; this.random = random; + this.equipment_generator = new LootGenerator(this.random); } /** @@ -39,20 +41,77 @@ module TS.SpaceTac { let generator = this.random.choice(generators); let result = generator(); - // TODO Add reward + if (result.value) { + result.reward = this.generateReward(result.value); + } return result; } /** - * Generate a new ship + * Generate a new ship that may be used in a mission */ - private generateShip(level: number) { + generateShip(level: number) { let generator = new ShipGenerator(this.random); let result = generator.generate(level, null, true); result.name = `${this.random.choice(POOL_SHIP_NAMES)}-${this.random.randInt(10, 999)}`; return result; } + /** + * Try to generate an equipment of given value + */ + tryGenerateEquipmentReward(value: number): Equipment | null { + let minvalue = value * 0.8; + let maxvalue = value * 1.2; + let qualities = [EquipmentQuality.FINE, EquipmentQuality.PREMIUM, EquipmentQuality.LEGENDARY]; + + let candidates: Equipment[] = []; + for (let pass = 0; pass < 10; pass++) { + let equipment: Equipment | null; + let level = 1; + do { + let quality = qualities[this.random.weighted([15, 12, 2])]; + equipment = this.equipment_generator.generate(level, quality); + if (equipment && equipment.getPrice() >= minvalue && equipment.getPrice() <= maxvalue) { + candidates.push(equipment); + } + level += 1; + } while (equipment && equipment.getPrice() < maxvalue * 1.5 && level < 20); + } + + if (candidates.length > 0) { + return this.random.choice(candidates); + } else { + return null; + } + } + + /** + * Generate a reward + */ + generateReward(value: number): MissionReward { + if (this.random.bool()) { + let equipment = this.tryGenerateEquipmentReward(value); + if (equipment) { + return equipment; + } else { + return value; + } + } else { + return value; + } + } + + /** + * Helper to set the difficulty of a mission + */ + setDifficulty(mission: Mission, base_value: number, fight_level: number) { + let level_diff = fight_level - this.around.star.level; + let code = (level_diff > 0) ? MissionDifficulty.hard : (level_diff < 0 ? MissionDifficulty.easy : MissionDifficulty.normal); + let value = fight_level * (base_value + base_value * 0.1 * clamp(level_diff, -5, 5)); + mission.setDifficulty(code, Math.round(value)); + } + /** * Generate an escort mission */ @@ -63,6 +122,7 @@ module TS.SpaceTac { let ship = this.generateShip(dest_star.level); mission.addPart(new MissionPartEscort(mission, destination, ship)); mission.title = `Escort a ship to a level ${dest_star.level} system`; + this.setDifficulty(mission, 1000, dest_star.level); return mission; } @@ -72,13 +132,16 @@ module TS.SpaceTac { generateCleanLocation(): Mission { let mission = new Mission(this.universe); let dest_star = this.random.choice(this.around.star.getNeighbors().concat([this.around.star])); + let here = (dest_star == this.around.star); let choices = dest_star.locations; - if (dest_star == this.around.star) { + if (here) { choices = choices.filter(loc => loc != this.around); } let destination = this.random.choice(choices); mission.addPart(new MissionPartCleanLocation(mission, destination)); - mission.title = `Defeat a level ${destination.star.level} fleet in ${(dest_star == this.around.star) ? "this" : "a nearby"} system`; + mission.addPart(new MissionPartGoTo(mission, this.around, "Go back to collect your reward")); + mission.title = `Defeat a level ${destination.star.level} fleet in ${here ? "this" : "a nearby"} system`; + this.setDifficulty(mission, here ? 300 : 500, dest_star.level); return mission; } } diff --git a/src/ui/map/MissionsDialog.spec.ts b/src/ui/map/MissionsDialog.spec.ts new file mode 100644 index 0000000..46b76b6 --- /dev/null +++ b/src/ui/map/MissionsDialog.spec.ts @@ -0,0 +1,60 @@ +module TS.SpaceTac.UI.Specs { + describe("MissionsDialog", function () { + let testgame = setupEmptyView(); + + function checkTexts(dialog: MissionsDialog, expected: string[]) { + let i = 0; + let container = (dialog).container; + container.children.forEach(child => { + if (child instanceof Phaser.Text) { + expect(child.text).toEqual(expected[i++]); + } + }); + expect(i).toEqual(expected.length); + } + + it("displays active and proposed missions", function () { + let universe = new Universe(); + let player = new Player(); + let shop = new Shop(); + let shop_missions: Mission[] = []; + spyOn(shop, "getMissions").and.callFake(() => shop_missions); + + let missions = new MissionsDialog(testgame.baseview, shop, player); + checkTexts(missions, []); + + let mission = new Mission(universe); + mission.title = "Save the universe!"; + mission.setDifficulty(MissionDifficulty.hard, 1); + mission.reward = 15000; + shop_missions.push(mission); + missions.refresh(); + checkTexts(missions, ["Proposed jobs", "Save the universe!", "Hard - Reward: 15000 zotys"]); + + mission = new Mission(universe); + mission.title = "Do not do evil"; + mission.setDifficulty(MissionDifficulty.easy, 1); + mission.reward = new Equipment(); + mission.reward.name = "Boy Scout Cap"; + shop_missions.push(mission); + missions.refresh(); + checkTexts(missions, ["Proposed jobs", "Save the universe!", "Hard - Reward: 15000 zotys", "Do not do evil", "Easy - Reward: Boy Scout Cap Mk1"]); + + mission = new Mission(universe); + mission.title = "Collect some money"; + mission.setDifficulty(MissionDifficulty.normal, 1); + player.missions.addSecondary(mission, player.fleet); + missions.refresh(); + checkTexts(missions, ["Active jobs", "Collect some money", "Normal - Reward: -", + "Proposed jobs", "Save the universe!", "Hard - Reward: 15000 zotys", "Do not do evil", "Easy - Reward: Boy Scout Cap Mk1"]); + + mission = new Mission(universe, undefined, true); + mission.title = "Kill the villain"; + mission.setDifficulty(MissionDifficulty.hard, 1); + player.missions.main = mission; + missions.refresh(); + checkTexts(missions, ["Active jobs", "Collect some money", "Normal - Reward: -", + "Proposed jobs", "Save the universe!", "Hard - Reward: 15000 zotys", "Do not do evil", "Easy - Reward: Boy Scout Cap Mk1"]); + }); + }); +} diff --git a/src/ui/map/MissionsDialog.ts b/src/ui/map/MissionsDialog.ts index a49b204..cbfb2b3 100644 --- a/src/ui/map/MissionsDialog.ts +++ b/src/ui/map/MissionsDialog.ts @@ -34,7 +34,7 @@ module TS.SpaceTac.UI { offset += 110; active.forEach(mission => { - this.addMission(offset, mission.title, "Reward: ???", 0, () => null); + this.addMission(offset, mission, 0, () => null); offset += 110; }); } @@ -45,7 +45,7 @@ module TS.SpaceTac.UI { offset += 110; proposed.forEach(mission => { - this.addMission(offset, mission.title, "Reward: ???", 2, () => { + this.addMission(offset, mission, 2, () => { this.shop.acceptMission(mission, this.player); this.refresh(); this.on_change(); @@ -58,13 +58,16 @@ module TS.SpaceTac.UI { /** * Add a mission text */ - addMission(yoffset: number, title: string, subtitle: string, button_frame: number, button_callback: Function) { + addMission(yoffset: number, mission: Mission, button_frame: number, button_callback: Function) { + let title = mission.title; + let subtitle = `${capitalize(MissionDifficulty[mission.difficulty])} - Reward: ${mission.getRewardText()}`; + this.addImage(320, yoffset, "map-missions", 1); if (title) { this.addText(380, yoffset - 15, title, "#d2e1f3", 22, false, false, 620, true); } if (subtitle) { - this.addText(380, yoffset + 22, subtitle, "#d2e1f3", 20, false, false, 620, true); + this.addText(380, yoffset + 22, subtitle, "#d2e1f3", 18, false, false, 620, true); } this.addButton(1120, yoffset, button_callback, "map-mission-action", button_frame, button_frame + 1); } diff --git a/src/ui/map/UniverseMapView.ts b/src/ui/map/UniverseMapView.ts index f336e6b..a25920e 100644 --- a/src/ui/map/UniverseMapView.ts +++ b/src/ui/map/UniverseMapView.ts @@ -276,6 +276,7 @@ module TS.SpaceTac.UI { this.setCamera(dest_star.x, dest_star.y, dest_star.radius * 2, duration, Phaser.Easing.Cubic.Out); }, () => { this.setInteractionEnabled(true); + this.refresh(); }); this.setInteractionEnabled(false); } @@ -312,7 +313,7 @@ module TS.SpaceTac.UI { moveToLocation(dest: StarLocation): void { if (this.interactive && dest != this.player.fleet.location) { this.setInteractionEnabled(false); - this.player_fleet.moveToLocation(dest, 1, null, () => this.updateInfo(dest.star)); + this.player_fleet.moveToLocation(dest, 1, null, () => this.refresh()); } }