module TK.SpaceTac { /** * Category of action */ export enum ActionCategory { MOVE, PASSIVE, ACTIVE, } /** * Target for an action */ export type ActionTarget = Readonly<{ distance?: number angle?: number }> /** * Target for an action, as applied from an hypothetical location */ export type ActionTargetFrom = Readonly<{ location: IArenaLocation distance?: number angle?: number }> /** * Impact filter for an action. * * This will filter ships inside the targetted area, to determine which ones will receive the action effects. */ export enum ActionImpactFilter { // Apply only on casting ship SELF, // Apply on all ships ALL, // Apply on all ships except the actor ALL_BUT_SELF, // Apply on all allies, including the actor ALLIES, // Apply on all allies, except the actor ALLIES_BUT_SELF, // Apply on all enemies ENEMIES, }; /** * Priority of ships in the target area. * * If there are more ships that the action limit, the most prioritized ones will be affected. */ export enum ActionImpactPriority { // Nearest ships are priority NEAREST, // Farthest ships are priority FARTHEST, // Ships with the most hull are priority TOUGHEST, // Ships with the less hull are priority WEAKEST, } /** * Configuration for the target of an action */ export interface BaseActionConfig { // Identifying code (for assets and effects) code: string // Human-friendly name name: string // Power cost power_cost?: number // Maximal distance the target can be set min_distance?: number // Minimal distance the target can be set max_distance?: number // Angle that will span the affected area (around the target direction) angular_span?: number // Radius of the affected area (around the target location) radius?: number // Filtering of ships in the affected area ship_filter?: ActionImpactFilter // Maximal number of ships impacted in the affected area ship_limit?: number // Priority of ships in affected area ship_priority?: ActionImpactPriority } /** * Reasons for action unavailibility */ export enum ActionUnavailability { // Ship is not playing NOT_PLAYING = "Not this ship turn", // Action is not available NO_SUCH_ACTION = "Action not available", // Action is overheated OVERHEATED = "Overheated", // Vigilance is activated VIGILANCE = "In vigilance", // Ship is pinned PINNED = "Pinned", } /** * Base class for a battle action. * * An action should be the only way to modify a battle state. */ export class BaseAction extends RObject implements BaseActionConfig { code = "nothing" name = "Nothing" power_cost = 0 min_distance = 0 max_distance = Infinity angular_span = undefined radius = undefined ship_filter = ActionImpactFilter.ALL ship_limit = undefined ship_priority = ActionImpactPriority.NEAREST // Cooldown configuration private cooldown = new Cooldown() // Create the action constructor(config?: Partial>) { super(); if (config) { this.configure(config); } } /** * Get the category for this action */ getCategory(): ActionCategory { return ActionCategory.PASSIVE; } /** * Get the verb for this action */ getVerb(ship: Ship): string { return "Do"; } /** * Get the full title for this action (verb and name) */ getTitle(ship: Ship): string { return `${this.getVerb(ship)} ${this.name}`; } /** * Get a default target for this action */ getDefaultTarget(ship: Ship): ActionTargetFrom { return { location: ship.location }; } /** * Configure the base settings for this action */ configure(config: Partial>): void { copyfields(config, this); } /** * Configure the cooldown for this action */ configureCooldown(overheat: number, cooling: number): void { this.cooldown.configure(overheat, cooling); } /** * Get the cooldown configuration */ getCooldown(): Cooldown { // TODO Split configuration (readonly) and usage return this.cooldown; } /** * Check basic conditions to know if the ship can use this action at all, not accounting for power * * Method to extend to set conditions * * Returns an unavalability reason, null otherwise */ checkCannotBeApplied(ship: Ship): ActionUnavailability | null { if (!ship.actions.getById(this.id)) { return ActionUnavailability.NO_SUCH_ACTION; } // Check cooldown if (!ship.actions.isUsable(this)) { return ActionUnavailability.OVERHEATED; } return null; } /** * Get the power cost of this action for the current state */ getPowerUsage(ship: Ship): number { return this.power_cost; } /** * Filter a list of ships to return only those impacted by this action * * This may be used as an indicator for helping the player in targetting, or to effectively apply the effects */ filterImpactedShips(ships: readonly Ship[], source: Ship, target: ActionTargetFrom): readonly Ship[] { // TODO Allow to work with ghosts (location instead of ships?) // TODO Apply radius and angle // TODO Apply limit and priority return ships.filter(ship => { if (this.ship_filter == ActionImpactFilter.ALL) { return true; } else if (this.ship_filter == ActionImpactFilter.ALL_BUT_SELF) { return !ship.is(source); } else if (this.ship_filter == ActionImpactFilter.ALLIES) { return ship.fleet.player.is(source.fleet.player); } else if (this.ship_filter == ActionImpactFilter.ALLIES_BUT_SELF) { return ship.fleet.player.is(source.fleet.player) && !ship.is(source); } else if (this.ship_filter == ActionImpactFilter.ENEMIES) { return !ship.fleet.player.is(source.fleet.player); } else { return false; } }); } /** * Get a name to represent the group of ships specified by a target filter */ static getFilterDesc(filter: ActionImpactFilter, plural = true): string { // TODO limit and priority if (filter == ActionImpactFilter.ALL) { return plural ? "ships" : "ship"; } else if (filter == ActionImpactFilter.ALL_BUT_SELF) { return plural ? "other ships" : "other ship"; } else if (filter == ActionImpactFilter.ALLIES) { return plural ? "team members" : "team member"; } else if (filter == ActionImpactFilter.ALLIES_BUT_SELF) { return plural ? "teammates" : "teammates"; } else if (filter == ActionImpactFilter.ENEMIES) { return plural ? "enemies" : "enemy"; } else { return ""; } } /** * Check if a target is suitable for this action * * Returns a suggested fixed target (may be the same as the input) */ checkTarget(ship: Ship, target: ActionTargetFrom): ActionTargetFrom | null { if (this.checkCannotBeApplied(ship)) { return null; } else { if (target.isShip()) { return this.checkShipTarget(ship, target); } else { return this.checkLocationTarget(ship, target); } } } /** * Get the full list of diffs caused by applying this action * * This does not perform any check, and assumes the action is doable */ getDiffs(ship: Ship, battle: Battle, target: ActionTargetFrom): BaseBattleDiff[] { let result: BaseBattleDiff[] = []; // Action usage result.push(new ShipActionUsedDiff(ship, this, target)); // Action effects result = result.concat(this.getSpecificDiffs(ship, battle, target)); return result; } /** * Method to reimplement to return the diffs specific to this action */ protected getSpecificDiffs(ship: Ship, battle: Battle, target: ActionTargetFrom): BaseBattleDiff[] { return [] } /** * Apply the action on a battle state * * This will first check that the action can be done, then get the battle diffs and apply them. */ apply(battle: Battle, ship: Ship, target: ActionTarget): boolean { let reject = this.checkCannotBeApplied(ship); if (reject) { console.warn(`Action rejected - ${reject}`, ship, this, target); return false; } let checked_target = this.checkTarget(ship, target); if (!checked_target) { console.warn("Action rejected - invalid target", ship, this, target); return false; } let diffs = this.getDiffs(ship, battle, checked_target); if (diffs.length) { battle.applyDiffs(diffs); return true; } else { console.error("Could not apply action, no diff produced"); return false; } } /** * Get textual description of effects */ getEffectsDescription(): string { return ""; } } }