module TK.SpaceTac.UI { /** * Processor of diffs coming from the battle log * * This will bind to the actual battle log, update the "displayed" battle state accordingly, and refresh the view. */ export class LogProcessor { // Link to the battle view private view: BattleView // Log client (to receive actual battle diffs) private log: BattleLogClient // Registered subscribers private subscriber: LogProcessorSubscriber[] = [] // Background delegates promises private background_promises: Promise[] = [] // Speed control private speed = 1 private temp_speed?: number // Debug indicators private debug = false private ai_disabled = false constructor(view: BattleView) { this.view = view; this.log = new BattleLogClient(view.battle, view.actual_battle.log); view.inputs.bindCheat("PageUp", "Step backward", () => { this.log.backward(); }); view.inputs.bindCheat("PageDown", "Step forward", () => { this.log.forward(); }); view.inputs.bindCheat("Home", "Jump to beginning", () => { this.log.jumpToStart(); }); view.inputs.bindCheat("End", "Jump to end", () => { this.log.jumpToEnd(); }); // Internal subscribers this.register((diff) => this.checkReaction(diff)); this.register((diff) => this.checkControl(diff)); this.register((diff) => this.checkProjectileFired(diff)); this.register((diff) => this.checkShipDeath(diff)); this.register((diff) => this.checkBattleEnded(diff)); } /** * Start log processing */ start() { if (!this.view.gameui.headless) { this.log.play(async diff => { while (this.view.isPaused()) { await this.view.timer.sleep(500); } await this.processBattleDiff(diff); if (this.log.atEnd()) { this.temp_speed = undefined; } }); this.transferControl(); } } /** * Process all pending diffs, synchronously */ processPending() { if (this.log.isPlaying()) { throw new Error("Cannot process diffs synchronously while playing the log"); } else { let diff: Diff | null; while (diff = this.log.forward()) { this.processBattleDiff(diff, false); } } } /** * Fast forward to the end of log, then resume normal speed */ fastForward(speed = 3): void { if (!this.log.atEnd()) { this.temp_speed = speed; } } /** * Destroy the processor * * This should be done to ensure it will stop processing and free resources */ destroy() { if (this.log.isPlaying()) { this.log.stop(true); } } /** * Check if we need a player or AI to interact at this point */ getPlayerNeeded(): Player | null { if (this.log.isPlaying() && this.log.atEnd()) { let playing_ship = this.view.actual_battle.playing_ship; return playing_ship ? playing_ship.getPlayer() : null; } else { return null; } } /** * Register a diff subscriber */ register(subscriber: LogProcessorSubscriber) { this.subscriber.push(subscriber); } /** * Register a diff for a specific ship */ registerForShip(ship: Ship, subscriber: (diff: BaseBattleShipDiff) => LogProcessorDelegate) { this.register(diff => { if (diff instanceof BaseBattleShipDiff && diff.ship_id === ship.id) { return subscriber(diff); } else { return {}; } }); } /** * Register to playing ship changes * * If *initial* is true, the callback will be fired once at register time * * If *immediate* is true, the ShipChangeDiff is watched, otherwise the end of the EndTurn action */ watchForShipChange(callback: (ship: Ship) => LogProcessorDelegate, initial = true, immediate = false) { this.register(diff => { let changed = false; if (immediate && diff instanceof ShipChangeDiff) { changed = true; } else if (!immediate && diff instanceof ShipActionEndedDiff) { let ship = this.view.battle.getShip(diff.ship_id); if (ship && ship.actions.getById(diff.action) instanceof EndTurnAction) { changed = true; } } if (changed) { let ship = this.view.battle.playing_ship; if (ship) { return callback(ship); } else { return {}; } } else { return {}; } }); if (initial) { let ship = this.view.battle.playing_ship; if (ship) { let result = callback(ship); if (result.foreground) { let promise = result.foreground(0); if (result.background) { let next = result.background; promise.then(() => next(0)); } } else if (result.background) { result.background(0); } } } } /** * Process a single battle diff */ async processBattleDiff(diff: BaseBattleDiff, timed = true): Promise { if (this.debug) { console.log("Battle diff", diff); } let speed = timed ? (typeof this.temp_speed == "undefined" ? this.speed : this.temp_speed) : 0; // TODO add priority to sort the delegates let delegates = this.subscriber.map(subscriber => subscriber(diff)); let foregrounds = nna(delegates.map(delegate => delegate.foreground || null)); let backgrounds = nna(delegates.map(delegate => delegate.background || null)); if (foregrounds.length > 0) { if (this.background_promises.length > 0) { await Promise.all(this.background_promises); this.background_promises = []; } let promises = foregrounds.map(foreground => foreground(speed)); await Promise.all(promises); } let promises = backgrounds.map(background => background(speed)); this.background_promises = this.background_promises.concat(promises); } /** * Transfer control to the needed player (or not) */ private transferControl() { let player = this.getPlayerNeeded(); if (player) { if (player.is(this.view.player)) { this.view.setInteractionEnabled(true); } else if (!this.ai_disabled) { this.view.playAI(); } else { this.view.applyAction(EndTurnAction.SINGLETON); } } else { this.view.setInteractionEnabled(false); } } /** * Check if a personality reaction should be triggered for a diff */ private checkReaction(diff: BaseBattleDiff): LogProcessorDelegate { if (this.log.isPlaying()) { let reaction = this.view.session.reactions.check(this.view.player, this.view.battle, this.view.battle.playing_ship, diff); if (reaction) { return { foreground: async () => { if (reaction instanceof PersonalityReactionConversation) { let builder = new UIBuilder(this.view, this.view.layer_overlay); let conversation = UIConversation.newFromPieces(builder, reaction.messages); await conversation.waitEnd(); } else { console.warn("[LogProcessor] Unknown personality reaction type", reaction); } } }; } } return {}; } /** * Check if control should be transferred to the player, or an AI, after a diff */ private checkControl(diff: BaseBattleDiff): LogProcessorDelegate { if (diff instanceof ShipActionEndedDiff) { return { foreground: async () => this.transferControl() } } else { return {}; } } /** * Check if a projectile is fired */ private checkProjectileFired(diff: BaseBattleDiff): LogProcessorDelegate { if (diff instanceof ProjectileFiredDiff) { let ship = this.view.battle.getShip(diff.ship_id); if (ship) { let action = ship.actions.getById(diff.action); if (action && action instanceof TriggerAction) { let effect = new WeaponEffect(this.view.arena, ship, diff.target, action); return { foreground: async (speed: number) => { if (speed) { await effect.start(speed); } } } } } } return {}; } /** * Check if a ship died */ private checkShipDeath(diff: BaseBattleDiff): LogProcessorDelegate { if (diff instanceof ShipDeathDiff) { let ship = this.view.battle.getShip(diff.ship_id); if (ship) { let dead_ship = ship; return { foreground: async (speed: number) => { if (dead_ship.is(this.view.ship_hovered)) { this.view.setShipHovered(null); } this.view.arena.markAsDead(dead_ship); this.view.ship_list.refresh(); if (speed) { await this.view.timer.sleep(2000 / speed); } } } } } return {}; } /** * Check if the battle ended */ private checkBattleEnded(diff: BaseBattleDiff): LogProcessorDelegate { if (diff instanceof EndBattleDiff) { return { foreground: async () => this.view.endBattle() } } return {}; } } /** * Effective work done by a subscriber * * *foreground* is started when no other delegate (background or foreground) is working * *background* is started when no other foreground delegate is working or pending */ export type LogProcessorDelegate = { foreground?: (speed: number) => Promise, background?: (speed: number) => Promise, } /** * Subscriber to receive diffs from the battle log */ type LogProcessorSubscriber = (diff: BaseBattleDiff) => LogProcessorDelegate }