/// module TK.SpaceTac { /** * A single ship in a fleet */ export class Ship extends RObject { // Ship model model: ShipModel // Fleet this ship is a member of fleet: Fleet // Level of this ship level = new ShipLevel() // Name of the ship, null if unimportant name: string | null // Flag indicating if the ship is alive alive: boolean // Flag indicating that the ship is mission critical (escorted ship) critical = false // Position in the arena arena_x: number arena_y: number // Facing direction in the arena arena_angle: number // Available actions actions = new ActionList() // Active effects (sticky, self or area) active_effects = new RObjectContainer() // Ship attributes attributes = new ShipAttributes() // Ship values values = new ShipValues() // Personality personality = new Personality() // Boolean set to true if the ship is currently playing its turn playing = false // Priority in current battle's play_order (used as sort key) play_priority = 0 // Create a new ship inside a fleet constructor(fleet: Fleet | null = null, name: string | null = null, model = new ShipModel()) { super(); this.fleet = fleet || new Fleet(); this.name = name; this.alive = true; this.arena_x = 0; this.arena_y = 0; this.arena_angle = 0; this.model = model; this.updateAttributes(); this.actions.updateFromShip(this); this.fleet.addShip(this); } /** * Return the current location and angle of this ship */ get location(): ArenaLocationAngle { return new ArenaLocationAngle(this.arena_x, this.arena_y, this.arena_angle); } /** * Returns the name of this ship */ getName(level = true): string { let name = this.name || this.model.name; return level ? `Level ${this.level.get()} ${name}` : name; } // Returns true if the ship is able to play // If *check_ap* is true, ap_current=0 will make this function return false isAbleToPlay(check_ap: boolean = true): boolean { var ap_checked = !check_ap || this.getValue("power") > 0; return this.alive && ap_checked; } // Set position in the arena // This does not consumes action points setArenaPosition(x: number, y: number) { this.arena_x = x; this.arena_y = y; } // Set facing angle in the arena setArenaFacingAngle(angle: number) { this.arena_angle = angle; } // String repr jasmineToString(): string { return this.getName(); } // Make an initiative throw, to resolve play order in a battle throwInitiative(gen: RandomGenerator): void { this.play_priority = gen.random() * this.attributes.initiative.get(); } /** * Return the player that plays this ship */ getPlayer(): Player { return this.fleet.player; } /** * Check if a player is playing this ship */ isPlayedBy(player: Player): boolean { return player.is(this.fleet.player); } /** * Get the battle this ship is currently engaged in */ getBattle(): Battle | null { return this.fleet.battle; } /** * Get the list of activated upgrades */ getUpgrades(): ShipUpgrade[] { return this.model.getActivatedUpgrades(this.level.get(), this.level.getUpgrades()); } /** * Refresh the actions and attributes from the bound model */ refreshFromModel(): void { this.updateAttributes(); this.actions.updateFromShip(this); } /** * Change the ship model */ setModel(model: ShipModel): void { this.model = model; this.level.clearUpgrades(); this.refreshFromModel(); } /** * Toggle an upgrade */ activateUpgrade(upgrade: ShipUpgrade, on: boolean): void { if (on && (upgrade.cost || 0) > this.getAvailableUpgradePoints()) { return; } this.level.activateUpgrade(upgrade, on); this.refreshFromModel(); } /** * Get the number of upgrade points available */ getAvailableUpgradePoints(): number { let upgrades = this.getUpgrades(); return this.level.getUpgradePoints() - sum(upgrades.map(upgrade => upgrade.cost || 0)); } /** * Add an event to the battle log, if any */ addBattleEvent(event: BaseBattleDiff): void { var battle = this.getBattle(); if (battle && battle.log) { battle.log.add(event); } } /** * Get a ship value */ getValue(name: keyof ShipValues): number { return this.values[name]; } /** * Set a ship value */ setValue(name: keyof ShipValues, value: number, relative = false): void { if (relative) { value += this.values[name]; } this.values[name] = value; } /** * Get a ship attribute's current value */ getAttribute(name: keyof ShipAttributes): number { if (!this.attributes.hasOwnProperty(name)) { console.error(`No such ship attribute: ${name}`); return 0; } return this.attributes[name].get(); } /** * Initialize the action points counter * This should be called once at the start of a battle * If no value is provided, the attribute power_capacity will be used */ private initializePower(value: number | null = null): void { if (value === null) { value = this.getAttribute("power_capacity"); } this.setValue("power", value); } /** * Method called at the start of battle, to restore a pristine condition on the ship */ restoreInitialState() { this.alive = true; this.actions.updateFromShip(this); this.active_effects = new RObjectContainer(); this.updateAttributes(); this.restoreHealth(); this.initializePower(); } /** * Check if the ship is inside a given circular area */ isInCircle(x: number, y: number, radius: number): boolean { let dx = this.arena_x - x; let dy = this.arena_y - y; let distance = Math.sqrt(dx * dx + dy * dy); return distance <= radius; } /** * Get the distance to another ship */ getDistanceTo(other: Ship): number { return Target.newFromShip(this).getDistanceTo(Target.newFromShip(other)); } /** * Get the diffs needed to apply changes to a ship value */ getValueDiffs(name: keyof ShipValues, value: number, relative = false): BaseBattleDiff[] { let result: BaseBattleDiff[] = []; let current = this.values[name]; if (relative) { value += current; } // TODO apply range limitations if (current != value) { result.push(new ShipValueDiff(this, name, value - current)); } return result; } /** * Produce diffs needed to put the ship in emergency stasis */ getDeathDiffs(battle: Battle): BaseBattleDiff[] { let result: BaseBattleDiff[] = []; // Remove active effects this.active_effects.list().forEach(effect => { if (!(effect instanceof StickyEffect)) { result.push(new ShipEffectRemovedDiff(this, effect)); } result = result.concat(effect.getOffDiffs(this)); }); // Deactivate toggle actions this.getToggleActions(true).forEach(action => { result = result.concat(action.getSpecificDiffs(this, battle, Target.newFromShip(this))); }); // Put all values to 0 keys(SHIP_VALUES).forEach(value => { result = result.concat(this.getValueDiffs(value, 0)); }); // Mark as dead result.push(new ShipDeathDiff(battle, this)); return result; } /** * Set the death status on this ship */ setDead(): void { let battle = this.getBattle(); if (battle) { let events = this.getDeathDiffs(battle); battle.applyDiffs(events); } else { console.error("Cannot set ship dead outside of battle", this); } } /** * Update attributes, taking into account model's permanent effects and active effects */ updateAttributes(): void { // Reset attributes keys(this.attributes).forEach(attr => this.attributes[attr].reset()); // Apply attribute effects this.getEffects().forEach(effect => { if (effect instanceof AttributeEffect) { this.attributes[effect.attrcode].addModifier(effect.value); } else if (effect instanceof AttributeMultiplyEffect) { this.attributes[effect.attrcode].addModifier(undefined, effect.value); } else if (effect instanceof AttributeLimitEffect) { this.attributes[effect.attrcode].addModifier(undefined, undefined, effect.value); } }); } /** * Fully restore hull and shield, at their maximal capacity */ restoreHealth(): void { if (this.alive) { this.setValue("hull", this.getAttribute("hull_capacity")); this.setValue("shield", this.getAttribute("shield_capacity")); } } /** * Get actions from the ship model */ getModelActions(): BaseAction[] { return this.model.getActions(this.level.get(), this.level.getUpgrades()); } /** * Get permanent effects from the ship model */ getModelEffects(): BaseEffect[] { return this.model.getEffects(this.level.get(), this.level.getUpgrades()); } /** * Iterator over all effects active for this ship. * * This combines the permanent effects from ship model, with sticky and area effects. */ getEffects(): BaseEffect[] { return this.getModelEffects().concat( this.active_effects.list().map(effect => (effect instanceof StickyEffect) ? effect.base : effect) ); } /** * Iterator over toggle actions */ getToggleActions(only_active = false): ToggleAction[] { let result = cfilter(this.actions.listAll(), ToggleAction); if (only_active) { result = result.filter(action => this.actions.isToggled(action)); } return result; } /** * Get the effects that this ship has on another ship (which may be herself) */ getAreaEffects(ship: Ship): BaseEffect[] { let toggled = this.getToggleActions(true); let effects = toggled.map(action => { if (bool(action.filterImpactedShips(this, this.location, Target.newFromShip(ship), [ship]))) { return action.effects; } else { return []; } }); return flatten(effects); } /** * Get a textual description of an attribute, and the origin of its value */ getAttributeDescription(attribute: keyof ShipAttributes): string { let result = SHIP_VALUES_DESCRIPTIONS[attribute]; let diffs: string[] = []; let limits: string[] = []; function addEffect(base: string, effect: BaseEffect) { if (effect instanceof AttributeEffect && effect.attrcode == attribute) { diffs.push(`${base}: ${effect.value > 0 ? "+" + effect.value.toString() : effect.value}`); } else if (effect instanceof AttributeLimitEffect && effect.attrcode == attribute) { limits.push(`${base}: limit to ${effect.value}`); } } this.getUpgrades().forEach(upgrade => { if (upgrade.effects) { upgrade.effects.forEach(effect => addEffect(upgrade.code, effect)); } }); this.active_effects.list().forEach(effect => { if (effect instanceof StickyEffect) { addEffect("Sticky effect", effect.base); } else { addEffect("Active effect", effect); } }); let sources = diffs.concat(limits).join("\n"); return sources ? (result + "\n\n" + sources) : result; } } }