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: BaseBattleDiff | 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: BaseBattleDiff | 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..." }, ])] ]] } /** * Check for a friendly fire condition (one of player's ships fired on another) */ function cond_friendly_fire(player: Player, battle: Battle | null, ship: Ship | null, event: BaseBattleDiff | null): Ship[] { if (battle && ship && event) { if (event instanceof ShipDamageDiff && player.is(ship.fleet.player) && !ship.is(event.ship_id)) { let hurt = battle.getShip(event.ship_id); return (hurt && player.is(hurt.fleet.player)) ? [hurt, ship] : []; } else { return []; } } else { return []; } } }