diff --git a/TODO.md b/TODO.md
index 5a639a5..eeaa121 100644
--- a/TODO.md
+++ b/TODO.md
@@ -100,6 +100,7 @@ Technical
* Pack all images in atlases
* Pack sounds
+* Replace jasmine with mocha+chai
Network
-------
diff --git a/src/common b/src/common
index f4fb99b..a530998 160000
--- a/src/common
+++ b/src/common
@@ -1 +1 @@
-Subproject commit f4fb99bb2a5b6dc393a4fa115d4bf25cdae12ccf
+Subproject commit a530998523e8f8c7a37323d5dc047241f71f6a36
diff --git a/src/core/GameSession.ts b/src/core/GameSession.ts
index 13c9dc5..a113a8f 100644
--- a/src/core/GameSession.ts
+++ b/src/core/GameSession.ts
@@ -14,6 +14,9 @@ module TK.SpaceTac {
// Current connected player
player: Player
+ // Personality reactions
+ reactions: PersonalityReactions
+
// Starting location
start_location: StarLocation
@@ -27,6 +30,7 @@ module TK.SpaceTac {
this.id = RandomGenerator.global.id(20);
this.universe = new Universe();
this.player = new Player(this.universe);
+ this.reactions = new PersonalityReactions();
this.start_location = new StarLocation();
}
@@ -66,6 +70,8 @@ module TK.SpaceTac {
this.player = new Player(this.universe);
+ this.reactions = new PersonalityReactions();
+
if (fleet) {
this.setCampaignFleet(null, story);
}
@@ -97,6 +103,7 @@ module TK.SpaceTac {
let battle = Battle.newQuickRandom(true, RandomGenerator.global.randInt(1, 10));
this.player = battle.fleets[0].player;
this.player.setBattle(battle);
+ this.reactions = new PersonalityReactions();
}
// Get currently played battle, null when none is in progress
diff --git a/src/core/Personality.ts b/src/core/Personality.ts
new file mode 100644
index 0000000..1fb1f75
--- /dev/null
+++ b/src/core/Personality.ts
@@ -0,0 +1,36 @@
+module TK.SpaceTac {
+ /**
+ * List of personality traits (may be used with "keyof").
+ */
+ export interface IPersonalityTraits {
+ aggressive: number
+ funny: number
+ heroic: number
+ optimistic: number
+ }
+
+ /**
+ * A personality is a set of traits that defines how a character thinks and behaves
+ *
+ * Each trait is a number between -1 and 1
+ *
+ * In the game, a personality represents an artificial intelligence, and is transferable
+ * from one ship (body) to another. This is why a personality has a name
+ */
+ export class Personality implements IPersonalityTraits {
+ // Name of this personality
+ name = ""
+
+ // Aggressive 1 / Poised -1
+ aggressive = 0
+
+ // Funny 1 / Serious -1
+ funny = 0
+
+ // Heroic 1 / Coward -1
+ heroic = 0
+
+ // Optimistic 1 / Pessimistic -1
+ optimistic = 0
+ }
+}
diff --git a/src/core/PersonalityReactions.spec.ts b/src/core/PersonalityReactions.spec.ts
new file mode 100644
index 0000000..2d5a06d
--- /dev/null
+++ b/src/core/PersonalityReactions.spec.ts
@@ -0,0 +1,65 @@
+///
+
+module TK.SpaceTac.Specs {
+ describe("PersonalityReactions", function () {
+ function apply(pool: ReactionPool): PersonalityReaction | null {
+ let reactions = new PersonalityReactions();
+ return reactions.check(new Player(), null, null, null, pool);
+ }
+
+ class FakeReaction extends PersonalityReactionConversation {
+ ships: Ship[]
+ constructor(ships: Ship[]) {
+ super([]);
+ this.ships = ships;
+ }
+ static cons(ships: Ship[]): FakeReaction {
+ return new FakeReaction(ships);
+ }
+ }
+
+ it("fetches ships from conditions", function () {
+ let reaction = apply({});
+ expect(reaction).toBeNull();
+
+ let s1 = new Ship(null, "S1");
+ let s2 = new Ship(null, "S2");
+
+ reaction = apply({
+ a: [() => [s1, s2], 1, [[() => 1, FakeReaction.cons]]],
+ });
+ expect(reaction).toEqual(new FakeReaction([s1, s2]));
+ })
+
+ it("applies weight on conditions", function () {
+ let s1 = new Ship(null, "S1");
+ let s2 = new Ship(null, "S2");
+
+ let reaction = apply({
+ a: [() => [s1], 1, [[() => 1, FakeReaction.cons]]],
+ b: [() => [s2], 0, [[() => 1, FakeReaction.cons]]],
+ });
+ expect(reaction).toEqual(new FakeReaction([s1]));
+
+ reaction = apply({
+ a: [() => [s1], 0, [[() => 1, FakeReaction.cons]]],
+ b: [() => [s2], 1, [[() => 1, FakeReaction.cons]]],
+ });
+ expect(reaction).toEqual(new FakeReaction([s2]));
+ })
+
+ it("checks for friendly fire", function () {
+ let condition = BUILTIN_REACTION_POOL['friendly_fire'][0];
+ let battle = new Battle();
+ let ship1a = battle.fleets[0].addShip();
+ let ship1b = battle.fleets[0].addShip();
+ let ship2a = battle.fleets[1].addShip();
+ let ship2b = battle.fleets[1].addShip();
+
+ expect(condition(ship1a.getPlayer(), battle, ship1a, new DamageEvent(ship1a, 50, 10))).toEqual([], "self shoot");
+ expect(condition(ship1a.getPlayer(), battle, ship1a, new DamageEvent(ship1b, 50, 10))).toEqual([ship1b, ship1a]);
+ expect(condition(ship1a.getPlayer(), battle, ship1a, new DamageEvent(ship2a, 50, 10))).toEqual([], "enemy shoot");
+ expect(condition(ship1a.getPlayer(), battle, ship2a, new DamageEvent(ship2a, 50, 10))).toEqual([], "other player event");
+ })
+ })
+}
diff --git a/src/core/PersonalityReactions.ts b/src/core/PersonalityReactions.ts
new file mode 100644
index 0000000..dcf8538
--- /dev/null
+++ b/src/core/PersonalityReactions.ts
@@ -0,0 +1,102 @@
+module TK.SpaceTac {
+ // Reaction triggered
+ export type PersonalityReaction = PersonalityReactionConversation
+
+ // Condition to check if a reaction may happen, returning involved ships (order is important)
+ export type ReactionCondition = (player: Player, battle: Battle | null, ship: Ship | null, event: BaseBattleEvent | null) => Ship[]
+
+ // Reaction profile, giving a probability for types of personality, and an associated reaction constructor
+ export type ReactionProfile = [(traits: IPersonalityTraits) => number, (ships: Ship[]) => PersonalityReaction]
+
+ // Reaction config (condition, chance, profiles)
+ export type ReactionConfig = [ReactionCondition, number, ReactionProfile[]]
+
+ // Pool of reaction config
+ export type ReactionPool = { [code: string]: ReactionConfig }
+
+ /**
+ * Reactions to external events according to personalities.
+ *
+ * This allows for a more "alive" world, as characters tend to speak to react to events.
+ *
+ * This object will store the previous reactions to avoid too much recurrence, and should be global to a whole
+ * game session.
+ */
+ export class PersonalityReactions {
+ done: string[] = []
+ random = RandomGenerator.global
+
+ /**
+ * Check for a reaction.
+ *
+ * This will return a reaction to display, and add it to the done list
+ */
+ check(player: Player, battle: Battle | null = null, ship: Ship | null = null, event: BaseBattleEvent | null = null, pool: ReactionPool = BUILTIN_REACTION_POOL): PersonalityReaction | null {
+ let codes = difference(keys(pool), this.done);
+
+ let candidates = nna(codes.map((code: string): [string, Ship[], ReactionProfile[]] | null => {
+ let [condition, chance, profiles] = pool[code];
+ if (this.random.random() <= chance) {
+ let involved = condition(player, battle, ship, event);
+ if (involved.length > 0) {
+ return [code, involved, profiles];
+ } else {
+ return null;
+ }
+ } else {
+ return null;
+ }
+ }));
+
+ if (candidates.length > 0) {
+ let [code, involved, profiles] = this.random.choice(candidates);
+ let primary = involved[0];
+ let weights = profiles.map(([evaluator, _]) => evaluator(primary.personality));
+ let action_number = this.random.weighted(weights);
+ if (action_number >= 0) {
+ this.done.push(code);
+ let reaction_constructor = profiles[action_number][1];
+ return reaction_constructor(involved);
+ } else {
+ return null;
+ }
+ } else {
+ return null;
+ }
+ }
+ }
+
+ /**
+ * One kind of personality reaction: saying something out loud
+ */
+ export class PersonalityReactionConversation {
+ messages: { interlocutor: Ship, message: string }[]
+ constructor(messages: { interlocutor: Ship, message: string }[]) {
+ this.messages = messages;
+ }
+ }
+
+ /**
+ * Standard reaction pool
+ */
+ export const BUILTIN_REACTION_POOL: ReactionPool = {
+ friendly_fire: [cond_friendly_fire, 1, [
+ [traits => 1, ships => new PersonalityReactionConversation([
+ { interlocutor: ships[0], message: "Hey !!! Watch where you're shooting !" },
+ { interlocutor: ships[1], message: "Sorry mate..." },
+ ])]
+ ]]
+ }
+
+ function cond_friendly_fire(player: Player, battle: Battle | null, ship: Ship | null, event: BaseBattleEvent | null): Ship[] {
+ if (battle && ship && event) {
+ if (event instanceof DamageEvent && event.ship != ship && event.ship.getPlayer() == player && ship.getPlayer() == player) {
+ return [event.ship, ship];
+ } else {
+ return [];
+ }
+ } else {
+ return [];
+ }
+ }
+}
diff --git a/src/core/Ship.ts b/src/core/Ship.ts
index e79e48c..29eeb1f 100644
--- a/src/core/Ship.ts
+++ b/src/core/Ship.ts
@@ -45,6 +45,9 @@ module TK.SpaceTac {
// Ship values
values = new ShipValues()
+ // Personality
+ personality = new Personality()
+
// Boolean set to true if the ship is currently playing its turn
playing = false
diff --git a/src/ui/battle/BattleView.ts b/src/ui/battle/BattleView.ts
index 4c62033..fbea001 100644
--- a/src/ui/battle/BattleView.ts
+++ b/src/ui/battle/BattleView.ts
@@ -142,6 +142,12 @@ module TK.SpaceTac.UI {
}
}
+ shutdown() {
+ super.shutdown();
+
+ this.log_processor.destroy();
+ }
+
/**
* Make the AI play current ship
*
@@ -265,10 +271,10 @@ module TK.SpaceTac.UI {
this.action_bar.setInteractive(enabled);
this.exitTargettingMode();
this.interacting = enabled;
- }
- if (!enabled) {
- this.setShipHovered(null);
+ if (!enabled) {
+ this.setShipHovered(null);
+ }
}
}
diff --git a/src/ui/battle/LogProcessor.ts b/src/ui/battle/LogProcessor.ts
index f19584d..a162ba5 100644
--- a/src/ui/battle/LogProcessor.ts
+++ b/src/ui/battle/LogProcessor.ts
@@ -23,6 +23,9 @@ module TK.SpaceTac.UI {
// Current position in the battle log
private cursor = -1
+ // Indicator that the log is destroyed
+ private destroyed = false
+
// Indicator that the log is being played continuously
private playing = false
@@ -32,6 +35,9 @@ module TK.SpaceTac.UI {
// Time at which the last action was applied
private last_action: number
+ // Playing ship (at current log position)
+ private current_ship = new Ship()
+
constructor(view: BattleView) {
this.view = view;
this.battle = view.battle;
@@ -56,58 +62,70 @@ module TK.SpaceTac.UI {
*/
start() {
this.cursor = this.log.events.length - 1;
+ this.current_ship = new Ship();
this.battle.getBootstrapEvents().forEach(event => this.processBattleEvent(event));
this.playing = true;
if (!this.view.gameui.headless) {
+ // This will be asynchronous background work, until "destroy" is called
this.playContinuous();
}
}
+ /**
+ * Destroy the processor
+ *
+ * This should be done to ensure it will stop processing and free resources
+ */
+ destroy() {
+ this.destroyed = true;
+ }
+
/**
* Play the log continuously
*/
- playContinuous() {
+ async playContinuous() {
let delay = 0;
- if (this.playing && !this.view.game.paused) {
- delay = this.stepForward();
- if (delay == 0) {
- delay = this.transferControl();
+ while (!this.destroyed) {
+ if (this.playing && !this.view.game.paused) {
+ await this.stepForward();
+ await this.transferControl();
+ }
+
+ if (this.atEnd()) {
+ await this.view.timer.sleep(50);
}
}
-
- this.view.timer.schedule(Math.max(delay, 50), () => this.playContinuous());
}
/**
* Make a step backward in time
*/
- stepBackward() {
+ stepBackward(): Promise {
if (!this.atStart()) {
this.cursor -= 1;
this.playing = false;
- this.processBattleEvent(this.log.events[this.cursor + 1].getReverse());
+
+ return this.processBattleEvent(this.log.events[this.cursor + 1].getReverse());
+ } else {
+ return Promise.resolve();
}
}
/**
* Make a step forward in time
- *
- * Returns the duration of the step processing
*/
- stepForward(): number {
+ stepForward(): Promise {
if (!this.atEnd()) {
this.cursor += 1;
- let result = this.processBattleEvent(this.log.events[this.cursor]);
-
if (this.atEnd()) {
this.playing = true;
}
- return result;
+ return this.processBattleEvent(this.log.events[this.cursor]);
} else {
- return 0;
+ return Promise.resolve();
}
}
@@ -151,7 +169,7 @@ module TK.SpaceTac.UI {
* Check if we need a player or AI to interact at this point
*/
getPlayerNeeded(): Player | null {
- if (this.atEnd()) {
+ if (this.playing && this.atEnd()) {
return this.battle.playing_ship ? this.battle.playing_ship.getPlayer() : null;
} else {
return null;
@@ -186,7 +204,7 @@ module TK.SpaceTac.UI {
/**
* Process a single event
*/
- processBattleEvent(event: BaseBattleEvent): number {
+ async processBattleEvent(event: BaseBattleEvent): Promise {
if (this.debug) {
console.log("Battle event", event);
}
@@ -221,34 +239,54 @@ module TK.SpaceTac.UI {
durations.push(this.processDroneAppliedEvent(event));
}
- return max([0].concat(durations));
+ let delay = max([0].concat(durations));
+ if (delay) {
+ await this.view.timer.sleep(delay);
+ }
+
+ if (this.playing) {
+ let reaction = this.view.session.reactions.check(this.view.player, this.battle, this.current_ship, event);
+ if (reaction) {
+ await this.processReaction(reaction);
+ }
+ }
}
/**
* Transfer control to the needed player (or not)
*/
- private transferControl(): number {
+ private async transferControl(): Promise {
let player = this.getPlayerNeeded();
if (player) {
if (this.battle.playing_ship && !this.battle.playing_ship.alive) {
this.view.setInteractionEnabled(false);
this.battle.advanceToNextShip();
- return 200;
+ await this.view.timer.sleep(200);
} else if (player === this.view.player) {
this.view.setInteractionEnabled(true);
- return 0;
} else {
this.view.playAI();
- return 0;
}
} else {
this.view.setInteractionEnabled(false);
- return 0;
+ }
+ }
+
+ /**
+ * Process a personality reaction
+ */
+ private async processReaction(reaction: PersonalityReaction): Promise {
+ if (reaction instanceof PersonalityReactionConversation) {
+ let conversation = UIConversation.newFromPieces(this.view, reaction.messages);
+ await conversation.waitEnd();
+ } else {
+ console.warn("[LogProcessor] Unknown personality reaction type", reaction);
}
}
// Playing ship changed
private processShipChangeEvent(event: ShipChangeEvent): number {
+ this.current_ship = event.new_ship;
this.view.arena.setShipPlaying(event.new_ship);
this.view.ship_list.setPlaying(event.new_ship);
if (event.ship !== event.new_ship) {
diff --git a/src/ui/common/UIConversation.ts b/src/ui/common/UIConversation.ts
new file mode 100644
index 0000000..c2b6163
--- /dev/null
+++ b/src/ui/common/UIConversation.ts
@@ -0,0 +1,171 @@
+///
+
+module TK.SpaceTac.UI {
+ export type UIConversationPiece = { interlocutor: Ship, message: string }
+ export type UIConversationCallback = (conversation: UIConversation, step: number) => boolean
+
+ /**
+ * Style for a conversational message display
+ */
+ export class UIConversationStyle {
+ // Center the message or not
+ center = false
+
+ // Padding between the content and the external border
+ padding = 10
+
+ // Background fill color
+ background = 0x202225
+ alpha = 0.9
+
+ // Border color and width
+ border = 0x404450
+ border_width = 2
+
+ // Text font style
+ text_color = "#ffffff"
+ text_size = 20
+ text_bold = true
+
+ // Portrait or image to display (from atlases)
+ image = ""
+ image_size = 0
+ image_caption = ""
+ }
+
+ /**
+ * Rectangle to display a message that may appear progressively, as in conversations
+ */
+ export class UIConversationMessage extends UIComponent {
+ constructor(parent: BaseView | UIComponent, width: number, height: number, message: string, style = new UIConversationStyle()) {
+ super(parent, width, height);
+
+ this.drawBackground(style.background, style.border, style.border_width, style.alpha);
+
+ let offset = 0;
+ if (style.image_size && style.image) {
+ offset = style.image_size + style.padding;
+ width -= offset;
+
+ let ioffset = style.padding + Math.floor(style.image_size / 2);
+ this.addImage(ioffset, ioffset, style.image);
+
+ if (style.image_caption) {
+ let text_size = Math.ceil(style.text_size * 0.6);
+ this.addText(ioffset, style.padding + style.image_size + text_size, style.image_caption,
+ style.text_color, text_size, false, true, style.image_size);
+ }
+ }
+
+ let text = this.addText(offset + (style.center ? width / 2 : style.padding), style.center ? height / 2 : style.padding, message,
+ style.text_color, style.text_size, style.text_bold, style.center, width - style.padding * 2, style.center);
+
+ let i = 0;
+ let colorchar = () => {
+ text.clearColors();
+ if (i < message.length) {
+ text.addColor("transparent", i);
+ i++;
+ this.view.timer.schedule(10, colorchar);
+ }
+ }
+ colorchar();
+ }
+ }
+
+ /**
+ * Display of an active conversation (sequence of messages)
+ */
+ export class UIConversation extends UIComponent {
+ private step = -1
+ private on_step: UIConversationCallback
+ private ended = false
+ private on_end = new Phaser.Signal()
+
+ constructor(parent: BaseView, on_step: UIConversationCallback) {
+ super(parent, parent.getWidth(), parent.getHeight());
+
+ this.drawBackground(0x404450, undefined, undefined, 0.7, () => this.forward());
+ this.setVisible(false);
+
+ this.on_step = on_step;
+
+ this.forward();
+ }
+
+ destroy() {
+ if (!this.ended) {
+ this.ended = true;
+ this.on_end.dispatch();
+ }
+
+ super.destroy();
+ }
+
+ /**
+ * Promise to wait for the end of conversation
+ */
+ waitEnd(): Promise {
+ if (this.ended) {
+ return Promise.resolve();
+ } else {
+ return new Promise((resolve, reject) => {
+ this.on_end.addOnce(resolve);
+ });
+ }
+ }
+
+ /**
+ * Set the currently displayed message
+ */
+ setCurrentMessage(style: UIConversationStyle, content: string, width: number, height: number, relx: number, rely: number): void {
+ this.clearContent();
+
+ let message = new UIConversationMessage(this, width, height, content, style);
+ message.addButton(width - 60, height - 60, () => this.forward(), "common-arrow");
+ message.setPositionInsideParent(relx, rely);
+
+ this.setVisible(true, 700);
+ }
+
+ /**
+ * Convenience to set the current message from a ship
+ *
+ * This will automatically set the style and position of the message
+ */
+ setCurrentShipMessage(ship: Ship, content: string): void {
+ let style = new UIConversationStyle();
+ style.image = `ship-${ship.model.code}-portrait`;
+ style.image_caption = ship.name;
+ style.image_size = 256;
+
+ let own = ship.getPlayer() == this.view.gameui.session.player;
+ this.setCurrentMessage(style, content, 900, 300, own ? 0.1 : 0.9, own ? 0.2 : 0.8);
+ }
+
+ /**
+ * Go forward to the next message
+ */
+ forward(): void {
+ this.step += 1;
+ if (!this.on_step(this, this.step)) {
+ this.destroy();
+ }
+ }
+
+ /**
+ * Convenience to create a conversation from a list of pieces
+ */
+ static newFromPieces(view: BaseView, pieces: UIConversationPiece[]): UIConversation {
+ let result = new UIConversation(view, (conversation, step) => {
+ if (step >= pieces.length) {
+ return false;
+ } else {
+ conversation.setCurrentShipMessage(pieces[step].interlocutor, pieces[step].message);
+ return true;
+ }
+ });
+ return result;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/ui/intro/IntroSteps.ts b/src/ui/intro/IntroSteps.ts
index b26a42b..d4acc12 100644
--- a/src/ui/intro/IntroSteps.ts
+++ b/src/ui/intro/IntroSteps.ts
@@ -150,9 +150,9 @@ module TK.SpaceTac.UI {
*/
protected message(message: string, layer = 2, clear = true): Function {
return () => {
- let style = new ProgressiveMessageStyle();
+ let style = new UIConversationStyle();
style.center = true;
- let display = new ProgressiveMessage(this.view, 900, 200, message, style);
+ let display = new UIConversationMessage(this.view, 900, 200, message, style);
display.setPositionInsideParent(0.5, 0.9);
display.moveToLayer(this.getLayer(layer, clear));
display.setVisible(false);
diff --git a/src/ui/intro/ProgressiveMessage.ts b/src/ui/intro/ProgressiveMessage.ts
deleted file mode 100644
index a78f02c..0000000
--- a/src/ui/intro/ProgressiveMessage.ts
+++ /dev/null
@@ -1,70 +0,0 @@
-module TK.SpaceTac.UI {
- /**
- * Style for a message display
- */
- export class ProgressiveMessageStyle {
- // Center the message or not
- center = false
-
- // Padding between the content and the external border
- padding = 10
-
- // Background fill color
- background = 0x202225
- alpha = 0.9
-
- // Border color and width
- border = 0x404450
- border_width = 2
-
- // Text font style
- text_color = "#ffffff"
- text_size = 20
- text_bold = true
-
- // Portrait or image to display (from atlases)
- image = ""
- image_size = 0
- image_caption = ""
- }
-
- /**
- * Rectangle to display a message that may appear progressively, as in dialogs
- */
- export class ProgressiveMessage extends UIComponent {
- constructor(parent: BaseView | UIComponent, width: number, height: number, message: string, style = new ProgressiveMessageStyle()) {
- super(parent, width, height);
-
- this.drawBackground(style.background, style.border, style.border_width, style.alpha);
-
- let offset = 0;
- if (style.image_size && style.image) {
- offset = style.image_size + style.padding;
- width -= offset;
-
- let ioffset = style.padding + Math.floor(style.image_size / 2);
- this.addImage(ioffset, ioffset, style.image);
-
- if (style.image_caption) {
- let text_size = Math.ceil(style.text_size * 0.6);
- this.addText(ioffset, style.padding + style.image_size + text_size, style.image_caption,
- style.text_color, text_size, false, true, style.image_size);
- }
- }
-
- let text = this.addText(offset + (style.center ? width / 2 : style.padding), style.center ? height / 2 : style.padding, message,
- style.text_color, style.text_size, style.text_bold, style.center, width - style.padding * 2, style.center);
-
- let i = 0;
- let colorchar = () => {
- text.clearColors();
- if (i < message.length) {
- text.addColor("transparent", i);
- i++;
- this.view.timer.schedule(10, colorchar);
- }
- }
- colorchar();
- }
- }
-}
\ No newline at end of file
diff --git a/src/ui/map/ConversationDisplay.ts b/src/ui/map/ConversationDisplay.ts
deleted file mode 100644
index 23117fe..0000000
--- a/src/ui/map/ConversationDisplay.ts
+++ /dev/null
@@ -1,94 +0,0 @@
-module TK.SpaceTac.UI {
- /**
- * Display of an active conversation
- */
- export class ConversationDisplay extends UIComponent {
- dialog: MissionPartConversation | null = null
- player: Player
- on_end: Function | null = null
-
- constructor(parent: BaseView, player: Player) {
- super(parent, parent.getWidth(), parent.getHeight());
-
- this.player = player;
-
- this.drawBackground(0x404450, undefined, undefined, 0.7, () => this.nextPiece());
- this.setVisible(false);
- }
-
- /**
- * Update from currently active missions
- */
- updateFromMissions(missions: ActiveMissions, on_end: Function | null = null) {
- let parts = missions.getCurrent().map(mission => mission.current_part);
- this.dialog = first(parts, part => part instanceof MissionPartConversation);
-
- if (this.dialog) {
- this.on_end = on_end;
- } else {
- this.on_end = null;
- }
-
- this.refresh();
- }
-
- /**
- * Go to the next dialog piece
- */
- nextPiece(): void {
- if (this.dialog) {
- this.dialog.next();
- this.refresh();
- }
- }
-
- /**
- * Skip the conversation
- */
- skipConversation(): void {
- if (this.dialog) {
- this.dialog.skip();
- this.refresh();
- }
- }
-
- /**
- * Refresh the displayed dialog piece
- */
- refresh() {
- this.clearContent();
-
- if (this.dialog) {
- if (this.dialog.checkCompleted()) {
- if (this.on_end) {
- this.on_end();
- this.on_end = null;
- }
- this.setVisible(false, 700);
- } else {
- let piece = this.dialog.getCurrent();
-
- let style = new ProgressiveMessageStyle();
- if (piece.interlocutor) {
- style.image = `ship-${piece.interlocutor.model.code}-portrait`;
- style.image_caption = piece.interlocutor.name;
- style.image_size = 256;
- }
-
- let message = new ProgressiveMessage(this, 900, 300, piece.message, style);
- message.addButton(840, 240, () => this.nextPiece(), "common-arrow");
-
- if (piece.interlocutor && piece.interlocutor.getPlayer() === this.player) {
- message.setPositionInsideParent(0.1, 0.2);
- } else {
- message.setPositionInsideParent(0.9, 0.8);
- }
-
- this.setVisible(true, 700);
- }
- } else {
- this.setVisible(false);
- }
- }
- }
-}
\ No newline at end of file
diff --git a/src/ui/map/MissionConversationDisplay.ts b/src/ui/map/MissionConversationDisplay.ts
new file mode 100644
index 0000000..9cf7509
--- /dev/null
+++ b/src/ui/map/MissionConversationDisplay.ts
@@ -0,0 +1,74 @@
+///
+
+module TK.SpaceTac.UI {
+ /**
+ * Display of an active mission conversation
+ */
+ export class MissionConversationDisplay extends UIConversation {
+ dialog: MissionPartConversation | null = null
+ on_ended: Function | null = null
+
+ constructor(parent: BaseView) {
+ super(parent, () => true);
+ }
+
+ /**
+ * Update from currently active missions
+ */
+ updateFromMissions(missions: ActiveMissions, on_ended: Function | null = null) {
+ let parts = missions.getCurrent().map(mission => mission.current_part);
+ this.dialog = first(parts, part => part instanceof MissionPartConversation);
+
+ if (this.dialog) {
+ this.on_ended = on_ended;
+ } else {
+ this.on_ended = null;
+ }
+
+ this.refresh();
+ }
+
+ /**
+ * Go to the next dialog piece
+ */
+ forward(): void {
+ if (this.dialog) {
+ this.dialog.next();
+ this.refresh();
+ }
+ }
+
+ /**
+ * Skip the conversation
+ */
+ skipConversation(): void {
+ if (this.dialog) {
+ this.dialog.skip();
+ this.refresh();
+ }
+ }
+
+ /**
+ * Refresh the displayed dialog piece
+ */
+ refresh() {
+ this.clearContent();
+
+ if (this.dialog) {
+ if (this.dialog.checkCompleted()) {
+ if (this.on_ended) {
+ this.on_ended();
+ this.on_ended = null;
+ }
+ this.setVisible(false, 700);
+ } else {
+ let piece = this.dialog.getCurrent();
+ this.setCurrentShipMessage(piece.interlocutor || new Ship(), piece.message);
+ this.setVisible(true, 700);
+ }
+ } else {
+ this.setVisible(false);
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/ui/map/UniverseMapView.ts b/src/ui/map/UniverseMapView.ts
index 5859973..4f6fb27 100644
--- a/src/ui/map/UniverseMapView.ts
+++ b/src/ui/map/UniverseMapView.ts
@@ -37,7 +37,7 @@ module TK.SpaceTac.UI {
// Active missions
missions: ActiveMissionsDisplay
- conversation: ConversationDisplay
+ conversation: MissionConversationDisplay
// Character sheet
character_sheet: CharacterSheet
@@ -127,13 +127,13 @@ module TK.SpaceTac.UI {
this.character_sheet.hide(false);
this.layer_overlay.add(this.character_sheet);
- this.conversation = new ConversationDisplay(this, this.player);
+ this.conversation = new MissionConversationDisplay(this);
this.conversation.moveToLayer(this.layer_overlay);
this.gameui.audio.startMusic("spring-thaw");
// Inputs
- this.inputs.bind(" ", "Conversation step", () => this.conversation.nextPiece());
+ this.inputs.bind(" ", "Conversation step", () => this.conversation.forward());
this.inputs.bind("Escape", "Skip conversation", () => this.conversation.skipConversation());
this.inputs.bindCheat("r", "Reveal whole map", () => this.revealAll());
this.inputs.bindCheat("n", "Next story step", () => {