Added framework for personality, and for ships to react to battle events
This commit is contained in:
parent
74acbeb827
commit
0ff23c71a1
1
TODO.md
1
TODO.md
|
@ -100,6 +100,7 @@ Technical
|
|||
|
||||
* Pack all images in atlases
|
||||
* Pack sounds
|
||||
* Replace jasmine with mocha+chai
|
||||
|
||||
Network
|
||||
-------
|
||||
|
|
|
@ -1 +1 @@
|
|||
Subproject commit f4fb99bb2a5b6dc393a4fa115d4bf25cdae12ccf
|
||||
Subproject commit a530998523e8f8c7a37323d5dc047241f71f6a36
|
|
@ -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
|
||||
|
|
36
src/core/Personality.ts
Normal file
36
src/core/Personality.ts
Normal file
|
@ -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
|
||||
}
|
||||
}
|
65
src/core/PersonalityReactions.spec.ts
Normal file
65
src/core/PersonalityReactions.spec.ts
Normal file
|
@ -0,0 +1,65 @@
|
|||
/// <reference path="PersonalityReactions.ts" />
|
||||
|
||||
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");
|
||||
})
|
||||
})
|
||||
}
|
102
src/core/PersonalityReactions.ts
Normal file
102
src/core/PersonalityReactions.ts
Normal file
|
@ -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 [];
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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<void> {
|
||||
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<void> {
|
||||
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<void> {
|
||||
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<void> {
|
||||
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<void> {
|
||||
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) {
|
||||
|
|
171
src/ui/common/UIConversation.ts
Normal file
171
src/ui/common/UIConversation.ts
Normal file
|
@ -0,0 +1,171 @@
|
|||
/// <reference path="UIComponent.ts" />
|
||||
|
||||
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<void> {
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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 = <MissionPartConversation | null>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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
74
src/ui/map/MissionConversationDisplay.ts
Normal file
74
src/ui/map/MissionConversationDisplay.ts
Normal file
|
@ -0,0 +1,74 @@
|
|||
/// <reference path="../common/UIConversation.ts" />
|
||||
|
||||
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 = <MissionPartConversation | null>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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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", () => {
|
||||
|
|
Loading…
Reference in a new issue