352 lines
12 KiB
TypeScript
352 lines
12 KiB
TypeScript
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<void>[] = []
|
|
|
|
// 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.isTesting) {
|
|
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<Battle> | 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<void> {
|
|
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<void>,
|
|
background?: (speed: number) => Promise<void>,
|
|
}
|
|
|
|
/**
|
|
* Subscriber to receive diffs from the battle log
|
|
*/
|
|
type LogProcessorSubscriber = (diff: BaseBattleDiff) => LogProcessorDelegate
|
|
}
|