1
0
Fork 0
spacetac/src/ui/battle/LogProcessor.ts

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
}