1
0
Fork 0

Added escort mission part

This commit is contained in:
Michaël Lemaire 2017-07-02 20:21:04 +02:00
parent f7424692fe
commit 993ed0e897
18 changed files with 344 additions and 95 deletions

67
TODO
View File

@ -1,67 +0,0 @@
* New battle internal flow: any game state change should be done through revertable events
* UI: use a common component class, and a layer abstraction
* Character sheet: add initial character creation
* Character sheet: disable interaction during battle (except for loot screen)
* Character sheet: improve eye-catching for shop and loot section
* Character sheet: highlight allowed destinations during drag-and-drop, with text hints
* Character sheet: when transferring to another ship, if the item can't be equipped (unmatched requirements), the transfer is cancelled instead of trying cargo
* Character sheet: effective skill is sometimes not updated when upgrading base skill
* Character sheet: tooltip to show the sources of attributes
* Character sheet: add a "loot all" button
* Shops: allow to change/buy ship model
* Loot: lucky finds should be proportional to cargo space
* Ship models: Add permanent effects
* Ship models: Add permanent actions
* Equipment: add critical hit/miss
* Equipment: add damage over time effect
* Menu: allow to delete cloud saves
* Fix cloud save games with "Level 0 - 0 ships"
* Arena: display effects description instead of attribute changes
* Arena: display radius for area effects (both on action hover, and while action is active)
* Arena: any displayed info should be based on a ship copy stored in ArenaShip, and in sync with current log index (not the game state ship)
* Arena: add engine trail
* Log processor: Cancel previous animations, and allow no animation mode
* Actions: fix targetting not resetting on current cursor location when using keyboard shortcuts
* Add actions with cost dependent of distance (like current move actions)
* Find incentives to move from starting position
* Outcome: disable the loot button if there is no loot
* Ensure that tweens and particle emitters get destroyed once animation is done (or view changes)
* UI: add standard confirm dialog
* Controls: do not focus on ship while targetting for area effects (dissociate hover and target)
* Controls: fix hover being stuck when the cursor exits the window, or the item moves or is hidden
* Drones: add hull points and take area damage
* Drones: find a way to avoid arena cluttering
* Drones: repair drone has its activation effect displayed as permanent effect on ships in the radius
* Add a battle log display
* Allow to undo last moves
* Merge identical sticky effects
* Allow to skip animations and AI delays in battle
* Hide enemy information (shield, hull, weapons), until they are in play, or until a "spy" effect is used
* Mobile: think UI layout so that fingers do not block the view (right and left handed)
* Mobile: display tooltips larger and on the side of screen where the finger is not
* Mobile: targetting in two times, using a draggable target indicator
* AI: use a first batch of producers, and only if no "good" move has been found, go on with some infinite producers
* AI: evaluate buffs/debuffs
* AI: abandon fight
* AI: add combination of random small move and actual maneuver, as producer
* AI: new duel page with producers/evaluators tweaking
* AI: work in a dedicated process
* Map: remove jump links that cross the radius of other systems
* Tutorial
* Campaign: Add ship personality (with icons to identify ?), with reaction to battle and map movements
* Campaign: Add factions and reputation
* Campaign: Missions/quests system
* Campaign: Main story arc
Equipment ideas:
* Shield with toggle effect that absorbs damage in an area
* Drive with minimal distance
* Cooldown drone or ability
Later, if possible:
* Animated arena background, instead of big picture
* Invocation/reinforcements (need to up the 10 ships limit)
* Dynamic music composition
* Replays
* Multiplayer/co-op
* Formation or deployment phase

101
TODO.md Normal file
View File

@ -0,0 +1,101 @@
To-Do-list
==========
Menu/settings/saves
-------------------
* Allow to delete cloud saves
* Fix cloud save games with "Level 0 - 0 ships"
Map/story
---------
* Add initial character creation
* Remove jump links that cross the radius of other systems
* Fix quickly zooming in twice preventing to display some UI parts
* Enemy fleet size should start low and increase with system level
* 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
* Show missions' destination near systems/locations
Character sheet
---------------
* Disable interaction during battle (except for loot screen)
* Improve eye-catching for shop and loot section
* Highlight allowed destinations during drag-and-drop, with text hints
* When transferring to another ship, if the item can't be equipped (unmatched requirements), the transfer is cancelled instead of trying cargo
* Effective skill is sometimes not updated when upgrading base skill
* Tooltip to show the sources of attributes
* Fix ship list not refreshing when escorted ship is added or removed
* Forbid to modify escorted ship
* Add merged cargo display for the whole fleet
Battle
------
* Add a voluntary retreat option
* Display effects description instead of attribute changes
* Display radius for area effects (both on action hover, and while action is active)
* Any displayed info should be based on a ship copy stored in ArenaShip, and in sync with current log index (not the game state ship)
* Add engine trail effect, and sound
* Fix targetting not resetting on current cursor location when using keyboard shortcuts
* Allow to skip animations, and allow no animation mode
* Find incentives to move from starting position (permanent drones ?)
* Add a "loot all" button, disable the loot button if there is no loot
* Do not focus on ship while targetting for area effects (dissociate hover and target)
* Repair drone has its activation effect sometimes displayed as permanent effect on ships in the radius
* Merge identical sticky effects
* Allow to undo last moves
* Add a battle log display
Ships models and equipments
---------------------------
* Add permanent effects and actions to ship models
* Add critical hit/miss
* Add damage over time effect (tricky to make intuitive)
* Move distance should increase with maneuvrability
* 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
Artificial Intelligence
-----------------------
* Use a first batch of producers, and only if no "good" move has been fo go on with some infinite producers
* Evaluate buffs/debuffs
* Abandon fight if the AI judges there is no hope of victory
* Add combination of random small move and actual maneuver, as prer
* New duel page with producers/evaluators tweaking
* Work in a dedicated process (webworker)
Technical
---------
* Ensure that tweens and particle emitters get destroyed once animation is done (or view changes)
Common UI
---------
* Fix hover being stuck when the cursor exits the window, or the item moves or is hidden
* Add a standard confirm dialog
* Mobile: think UI layout so that fingers do not block the view (right and left handed)
* Mobile: display tooltips larger and on the side of screen where the finger is not
* Mobile: targetting in two times, using a draggable target indicator
Postponed
---------
* Tutorial
* Secondary story arcs
* Replays
* Multiplayer/co-op
* Formation or deployment phase
* 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
* Invocation/reinforcements (need to up the 10 ships limit)
* Dynamic music composition

View File

@ -14,6 +14,39 @@ module TS.SpaceTac {
expect(fleet.getLevel()).toEqual(4);
});
it("adds and removes ships", function () {
let fleet1 = new Fleet();
let fleet2 = new Fleet();
let ship1 = fleet1.addShip();
expect(fleet1.ships).toEqual([ship1]);
expect(fleet2.ships).toEqual([]);
let ship2 = new Ship();
expect(fleet1.ships).toEqual([ship1]);
expect(fleet2.ships).toEqual([]);
fleet2.addShip(ship2);
expect(fleet1.ships).toEqual([ship1]);
expect(fleet2.ships).toEqual([ship2]);
fleet1.addShip(ship2);
expect(fleet1.ships).toEqual([ship1, ship2]);
expect(fleet2.ships).toEqual([]);
fleet1.removeShip(ship1, fleet2);
expect(fleet1.ships).toEqual([ship2]);
expect(fleet2.ships).toEqual([ship1]);
fleet1.removeShip(ship1);
expect(fleet1.ships).toEqual([ship2]);
expect(fleet2.ships).toEqual([ship1]);
fleet1.removeShip(ship2);
expect(fleet1.ships).toEqual([]);
expect(fleet2.ships).toEqual([ship1]);
});
it("changes location, only using jumps to travel between systems", function () {
let fleet = new Fleet();
let system1 = new Star();
@ -48,5 +81,32 @@ module TS.SpaceTac {
expect(result).toBe(true);
expect(fleet.location).toBe(jump1);
});
it("checks if a fleet is alive", function () {
let fleet = new Fleet();
expect(fleet.isAlive()).toBe(false);
let ship1 = fleet.addShip();
expect(fleet.isAlive()).toBe(true);
let ship2 = fleet.addShip();
expect(fleet.isAlive()).toBe(true);
ship1.setDead();
expect(fleet.isAlive()).toBe(true);
ship2.setDead();
expect(fleet.isAlive()).toBe(false);
let ship3 = fleet.addShip();
expect(fleet.isAlive()).toBe(true);
let ship4 = fleet.addShip();
ship4.critical = true;
expect(fleet.isAlive()).toBe(true);
ship4.setDead();
expect(fleet.isAlive()).toBe(false);
});
});
}

View File

@ -25,6 +25,10 @@ module TS.SpaceTac {
this.ships = [];
}
jasmineToString(): string {
return `${this.player.name}'s fleet [${this.ships.map(ship => ship.name).join(",")}]`;
}
/**
* Set the current location of the fleet
*
@ -48,7 +52,9 @@ module TS.SpaceTac {
return true;
}
// Add a ship in this fleet
/**
* Add a ship this fleet
*/
addShip(ship = new Ship()): Ship {
if (ship.fleet && ship.fleet != this) {
remove(ship.fleet.ships, ship);
@ -58,6 +64,15 @@ module TS.SpaceTac {
return ship;
}
/**
* Remove the ship from this fleet, transferring it to another fleet
*/
removeShip(ship: Ship, fleet = new Fleet()): void {
if (ship.fleet === this) {
fleet.addShip(ship);
}
}
// Set the current battle
setBattle(battle: Battle | null): void {
this.battle = battle;
@ -77,15 +92,15 @@ module TS.SpaceTac {
return Math.floor(avg);
}
// Check if the fleet still has living ships
/**
* Check if the fleet is considered alive (at least one ship alive, and no critical ship dead)
*/
isAlive(): boolean {
var count = 0;
this.ships.forEach((ship: Ship) => {
if (ship.alive) {
count += 1;
}
});
return (count > 0);
if (any(this.ships, ship => ship.critical && !ship.alive)) {
return false;
} else {
return any(this.ships, ship => ship.alive);
}
}
}
}

View File

@ -86,7 +86,7 @@ module TS.SpaceTac.Specs {
expect(session.start_location.encounter_gen).toBe(true);
session.setCampaignFleet();
expect(session.player.fleet.ships.length).toBe(4);
expect(session.player.fleet.ships.length).toBe(2);
expect(session.player.fleet.credits).toBe(500);
expect(session.player.fleet.location).toBe(session.start_location);
});

View File

@ -5,7 +5,7 @@ module TS.SpaceTac {
* This represents the current state of game
*/
export class GameSession {
// "Hopefully"" unique session id
// "Hopefully" unique session id
id: string
// Game universe
@ -81,7 +81,7 @@ module TS.SpaceTac {
this.player.fleet = fleet;
} else {
let fleet_generator = new FleetGenerator();
this.player.fleet = fleet_generator.generate(1, this.player, 4);
this.player.fleet = fleet_generator.generate(1, this.player, 2);
}
this.player.fleet.setLocation(this.start_location);

View File

@ -70,6 +70,9 @@ module TS.SpaceTac {
// Flag indicating if the ship is alive
alive: boolean
// Flag indicating that the ship is mission critical (escorted ship)
critical = false
// Position in the arena
arena_x: number
arena_y: number

View File

@ -1,9 +1,11 @@
module TS.SpaceTac.Specs {
describe("MainStory", () => {
function checkPart(story: Mission, index: number, title: string) {
function checkPart(story: Mission, index: number, title: string, completed = false) {
let result = story.checkStatus();
expect(story.parts.indexOf(story.current_part)).toBe(index);
expect(story.current_part.title).toMatch(title);
expect(story.completed).toBe(false);
expect(story.completed).toBe(completed);
expect(result).toBe(!completed);
}
function goTo(fleet: Fleet, location: StarLocation, win_encounter = true) {
@ -25,12 +27,21 @@ module TS.SpaceTac.Specs {
let missions = session.player.missions;
let story = nn(missions.main);
checkPart(story, 0, "^Find your contact in .* system$");
goTo(fleet, (<MissionPartGoTo>story.current_part).destination);
checkPart(story, 1, "^Speak with your contact .*$");
let fleet_size = fleet.ships.length;
checkPart(story, 0, "^Travel to Terranax galaxy$");
(<MissionPartDialog>story.current_part).skip();
checkPart(story, 1, "^Find your contact in .*$");
goTo(fleet, (<MissionPartGoTo>story.current_part).destination);
checkPart(story, 2, "^Speak with your contact");
(<MissionPartDialog>story.current_part).skip();
checkPart(story, 3, "^Go with .* in .* system$");
expect(fleet.ships.length).toBe(fleet_size + 1);
goTo(fleet, (<MissionPartEscort>story.current_part).destination);
expect(story.checkStatus()).toBe(false, "story not complete");
})
})

View File

@ -16,12 +16,18 @@ module TS.SpaceTac {
let random = RandomGenerator.global;
let start_location = nn(fleet.location);
// Arrival
let dialog = this.addPart(new MissionPartDialog(this, [], "Travel to Terranax galaxy"));
dialog.addPiece(null, "Wow ! From what my sensors tell me, there is not much activity around here.");
dialog.addPiece(null, "I remember the last time I came in this galaxy, you needed to be aware of collisions at all time, so crowded it was.");
dialog.addPiece(null, "Well...I did not pick a signal from our contact yet. We should be looking for her in this system.");
// Get in touch with our contact
let contact_location = randomLocation(random, [start_location.star], [start_location]);
let contact_character = new Ship(null, "Osten-37", ShipModel.getRandomModel(1, random));
contact_character.fleet.setLocation(contact_location, true);
this.addPart(new MissionPartGoTo(this, contact_location, "Find your contact"));
let dialog = this.addPart(new MissionPartDialog(this, [contact_character], "Speak with your contact"));
this.addPart(new MissionPartGoTo(this, contact_location, `Find your contact in ${contact_location.star.name}`));
dialog = this.addPart(new MissionPartDialog(this, [contact_character], "Speak with your contact"));
dialog.addPiece(contact_character, "Finally, you came!");
dialog.addPiece(contact_character, "Sorry for not broadcasting my position. As you may have encountered, this star system is not safe anymore.");
dialog.addPiece(null, "Nothing we could not handle, we just hope the other teams have not run across more trouble.");
@ -32,6 +38,11 @@ module TS.SpaceTac {
dialog.addPiece(contact_character, "Yes, some merchants and miners have rallied behind a retired TSF general, but I lost contact with them weeks ago.");
dialog.addPiece(contact_character, "We may go to their last known location, but first I want you to see something in a nearby system.");
dialog.addPiece(null, "Ok, let's go...");
// Go take a look at the graveyard
let nearby_systems = nna(start_location.star.getLinks().map(link => link.getPeer(contact_location.star)));
let graveyard_location = randomLocation(random, [minBy(nearby_systems, system => system.level)]);
this.addPart(new MissionPartEscort(this, graveyard_location, contact_character, `Go with ${contact_character.name} in ${graveyard_location.star.name} system`));
}
}
}

View File

@ -53,12 +53,15 @@ module TS.SpaceTac {
if (this.completed) {
return false;
} else if (this.current_part.checkCompleted()) {
this.current_part.onEnded();
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];
this.current_part.onStarted();
return true;
}
} else {

View File

@ -35,5 +35,17 @@ module TS.SpaceTac {
*/
forceComplete(): void {
}
/**
* Event called when the part starts
*/
onStarted(): void {
}
/**
* Event called when the part ends
*/
onEnded(): void {
}
}
}

View File

@ -1,11 +1,11 @@
module TS.SpaceTac.Specs {
describe("MissionPartGoTo", () => {
describe("MissionPartDialog", () => {
it("advances through dialog", function () {
let universe = new Universe();
let fleet = new Fleet();
let ship1 = new Ship(null, "Tim");
let ship2 = new Ship(null, "Ben");
let part = new MissionPartDialog(new Mission(universe, fleet), [ship1, ship2], "Talk to");
let part = new MissionPartDialog(new Mission(universe, fleet), [ship1, ship2], "Talk to Tim");
expect(part.title).toEqual("Talk to Tim");
expect(part.checkCompleted()).toBe(true, "No dialog piece");

View File

@ -25,8 +25,8 @@ module TS.SpaceTac {
// Current piece
current_piece = 0
constructor(mission: Mission, interlocutors: Ship[], directive = "Speak with") {
super(mission, `${directive} ${interlocutors[0].name}`);
constructor(mission: Mission, interlocutors: Ship[], directive?: string) {
super(mission, directive || `Speak with ${interlocutors[0].name}`);
this.interlocutors = interlocutors;
}

View File

@ -0,0 +1,67 @@
module TS.SpaceTac.Specs {
describe("MissionPartEscort", () => {
it("completes when the fleet is at location, with its escort", function () {
let destination = new StarLocation(new Star(null, 0, 0, "Atanax"));
destination.encounter_random = new SkewedRandomGenerator([0], true);
let universe = new Universe();
let fleet = new Fleet();
let ship = new Ship(null, "Zybux");
let part = new MissionPartEscort(new Mission(universe, fleet), destination, ship);
expect(fleet.ships).not.toContain(ship);
expect(part.title).toEqual("Escort Zybux to Atanax system");
expect(part.checkCompleted()).toBe(false, "Init location");
part.onStarted();
expect(fleet.ships).toContain(ship);
fleet.setLocation(destination, true);
expect(part.checkCompleted()).toBe(false, "Encounter not clear");
destination.clearEncounter();
expect(part.checkCompleted()).toBe(true, "Encouter cleared");
fleet.setLocation(new StarLocation(), true);
expect(part.checkCompleted()).toBe(false, "Went to another system");
fleet.setLocation(destination, true);
expect(part.checkCompleted()).toBe(true, "Back at destination");
expect(fleet.ships).toContain(ship);
part.onEnded();
expect(fleet.ships).not.toContain(ship);
})
it("sets the escorted ship as critical in battles", function () {
let universe = new Universe();
let fleet = new Fleet();
let ship1 = fleet.addShip();
let ship2 = fleet.addShip();
let ship = new Ship();
let destination = new StarLocation(new Star());
let part = new MissionPartEscort(new Mission(universe, fleet), destination, ship);
part.onStarted();
expect(fleet.ships).toContain(ship);
let enemy = new Fleet();
enemy.addShip();
let battle = new Battle(fleet, enemy);
battle.start();
battle.checkEndBattle();
expect(battle.ended).toBe(false);
// if a fleet member dies, it is not over
ship1.setDead();
battle.checkEndBattle();
expect(battle.ended).toBe(false);
// if the critical ship dies, it is defeat
ship.setDead();
battle.checkEndBattle();
expect(battle.ended).toBe(true);
expect(battle.outcome.winner).not.toBe(fleet);
})
})
}

View File

@ -0,0 +1,29 @@
/// <reference path="MissionPartGoTo.ts" />
module TS.SpaceTac {
/**
* A mission part that requires the fleet to escort a specific ship to a destination
*/
export class MissionPartEscort extends MissionPartGoTo {
ship: Ship
constructor(mission: Mission, destination: StarLocation, ship: Ship, directive?: string) {
super(mission, destination, directive || `Escort ${ship.name} to ${destination.star.name} system`);
this.ship = ship;
}
checkCompleted(): boolean {
return super.checkCompleted() && contains(this.fleet.ships, this.ship);
}
onStarted(): void {
this.ship.critical = true;
this.fleet.addShip(this.ship);
}
onEnded(): void {
this.fleet.removeShip(this.ship);
}
}
}

View File

@ -6,9 +6,9 @@ module TS.SpaceTac.Specs {
let universe = new Universe();
let fleet = new Fleet();
let part = new MissionPartGoTo(new Mission(universe, fleet), destination, "Collect gems");
let part = new MissionPartGoTo(new Mission(universe, fleet), destination);
expect(part.title).toEqual("Collect gems in Atanax system");
expect(part.title).toEqual("Go to Atanax system");
expect(part.checkCompleted()).toBe(false, "Init location");
fleet.setLocation(destination, true);
@ -32,6 +32,7 @@ module TS.SpaceTac.Specs {
let fleet = new Fleet();
let part = new MissionPartGoTo(new Mission(universe, fleet), destination, "Investigate");
expect(part.title).toEqual("Investigate");
expect(part.checkCompleted()).toBe(false);
part.forceComplete();
expect(part.checkCompleted()).toBe(true);

View File

@ -7,8 +7,8 @@ module TS.SpaceTac {
export class MissionPartGoTo extends MissionPart {
destination: StarLocation
constructor(mission: Mission, destination: StarLocation, directive: string, hint = true) {
super(mission, hint ? `${directive} in ${destination.star.name} system` : directive);
constructor(mission: Mission, destination: StarLocation, directive?: string) {
super(mission, directive || `Go to ${destination.star.name} system`);
this.destination = destination;
}

View File

@ -201,6 +201,9 @@ module TS.SpaceTac.UI {
this.credits.setText(fleet.credits.toString());
this.portraits.scale.set(980 * this.portraits.scale.x / this.portraits.height, 980 * this.portraits.scale.y / this.portraits.height);
if (this.portraits.width > 308) {
this.portraits.scale.set(308 * this.portraits.scale.x / this.portraits.width, 308 * this.portraits.scale.y / this.portraits.width);
}
this.portraits.y = 80 + 160 * this.portraits.scale.x;
}