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", () => {