2017-09-24 22:23:22 +00:00
|
|
|
module TK.SpaceTac {
|
2017-02-07 19:15:21 +00:00
|
|
|
/**
|
|
|
|
* A single ship in a fleet
|
|
|
|
*/
|
2017-02-07 18:54:53 +00:00
|
|
|
export class Ship {
|
2014-12-29 00:00:00 +00:00
|
|
|
// Fleet this ship is a member of
|
2017-02-07 19:15:21 +00:00
|
|
|
fleet: Fleet
|
2014-12-29 00:00:00 +00:00
|
|
|
|
2015-03-12 00:00:00 +00:00
|
|
|
// Level of this ship
|
2017-03-17 00:07:00 +00:00
|
|
|
level = new ShipLevel()
|
|
|
|
skills = new ShipSkills()
|
2015-03-12 00:00:00 +00:00
|
|
|
|
2014-12-30 00:00:00 +00:00
|
|
|
// Name of the ship
|
2017-02-07 19:15:21 +00:00
|
|
|
name: string
|
2014-12-30 00:00:00 +00:00
|
|
|
|
2015-05-05 19:49:33 +00:00
|
|
|
// Code of the ShipModel used to create it
|
2017-04-25 23:27:42 +00:00
|
|
|
model: ShipModel
|
2015-05-05 19:49:33 +00:00
|
|
|
|
2015-02-06 00:00:00 +00:00
|
|
|
// Flag indicating if the ship is alive
|
2017-02-07 19:15:21 +00:00
|
|
|
alive: boolean
|
2015-02-06 00:00:00 +00:00
|
|
|
|
2017-07-02 18:21:04 +00:00
|
|
|
// Flag indicating that the ship is mission critical (escorted ship)
|
|
|
|
critical = false
|
|
|
|
|
2014-12-30 00:00:00 +00:00
|
|
|
// Position in the arena
|
2017-02-07 19:15:21 +00:00
|
|
|
arena_x: number
|
|
|
|
arena_y: number
|
2014-12-30 00:00:00 +00:00
|
|
|
|
|
|
|
// Facing direction in the arena
|
2017-02-07 19:15:21 +00:00
|
|
|
arena_angle: number
|
2015-01-28 00:00:00 +00:00
|
|
|
|
2017-01-23 23:07:54 +00:00
|
|
|
// Sticky effects that applies a given number of times
|
2017-02-07 19:15:21 +00:00
|
|
|
sticky_effects: StickyEffect[]
|
2015-02-06 00:00:00 +00:00
|
|
|
|
2015-01-14 00:00:00 +00:00
|
|
|
// List of slots, able to contain equipment
|
2017-02-07 19:15:21 +00:00
|
|
|
slots: Slot[]
|
|
|
|
|
2017-03-05 17:48:13 +00:00
|
|
|
// Cargo
|
|
|
|
cargo_space: number = 0
|
|
|
|
cargo: Equipment[] = []
|
|
|
|
|
2017-02-07 19:15:21 +00:00
|
|
|
// Ship attributes
|
|
|
|
attributes = new ShipAttributes()
|
2015-01-14 00:00:00 +00:00
|
|
|
|
2017-02-07 19:15:21 +00:00
|
|
|
// Ship values
|
|
|
|
values = new ShipValues()
|
2015-01-22 00:00:00 +00:00
|
|
|
|
2017-10-08 21:26:33 +00:00
|
|
|
// Personality
|
|
|
|
personality = new Personality()
|
|
|
|
|
2017-01-20 00:02:18 +00:00
|
|
|
// Boolean set to true if the ship is currently playing its turn
|
2017-02-07 19:15:21 +00:00
|
|
|
playing = false
|
|
|
|
|
|
|
|
// Priority in play_order
|
|
|
|
play_priority = 0;
|
2017-01-20 00:02:18 +00:00
|
|
|
|
2014-12-30 00:00:00 +00:00
|
|
|
// Create a new ship inside a fleet
|
2017-06-22 22:37:38 +00:00
|
|
|
constructor(fleet: Fleet | null = null, name = "unnamed", model = new ShipModel("default", "Default", 1, 0, false, 0)) {
|
2015-01-29 00:00:00 +00:00
|
|
|
this.fleet = fleet || new Fleet();
|
2014-12-30 00:00:00 +00:00
|
|
|
this.name = name;
|
2015-02-06 00:00:00 +00:00
|
|
|
this.alive = true;
|
2017-01-23 23:07:54 +00:00
|
|
|
this.sticky_effects = [];
|
2015-01-14 00:00:00 +00:00
|
|
|
this.slots = [];
|
2014-12-30 00:00:00 +00:00
|
|
|
|
2015-01-28 00:00:00 +00:00
|
|
|
this.arena_x = 0;
|
|
|
|
this.arena_y = 0;
|
2015-01-29 00:00:00 +00:00
|
|
|
this.arena_angle = 0;
|
2015-01-28 00:00:00 +00:00
|
|
|
|
2017-06-13 22:01:39 +00:00
|
|
|
this.fleet.addShip(this);
|
2017-04-25 23:27:42 +00:00
|
|
|
|
|
|
|
this.model = model;
|
|
|
|
this.setCargoSpace(model.cargo);
|
|
|
|
model.slots.forEach(slot => this.addSlot(slot));
|
2014-12-30 00:00:00 +00:00
|
|
|
}
|
|
|
|
|
2017-05-30 16:24:55 +00:00
|
|
|
/**
|
|
|
|
* Return the current location and angle of this ship
|
|
|
|
*/
|
|
|
|
get location(): ArenaLocationAngle {
|
|
|
|
return new ArenaLocationAngle(this.arena_x, this.arena_y, this.arena_angle);
|
|
|
|
}
|
|
|
|
|
2017-07-31 18:17:43 +00:00
|
|
|
/**
|
|
|
|
* Returns the full name of this ship
|
|
|
|
*/
|
|
|
|
getFullName(owner = true): string {
|
|
|
|
let result = `Level ${this.level.get()} ${this.name}`;
|
|
|
|
return owner ? `${this.fleet.player.name}'s ${result}` : result;
|
|
|
|
}
|
|
|
|
|
2015-02-06 00:00:00 +00:00
|
|
|
// 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 {
|
2017-02-07 19:15:21 +00:00
|
|
|
var ap_checked = !check_ap || this.values.power.get() > 0;
|
2015-02-06 00:00:00 +00:00
|
|
|
return this.alive && ap_checked;
|
|
|
|
}
|
|
|
|
|
2014-12-30 00:00:00 +00:00
|
|
|
// Set position in the arena
|
2014-12-31 00:00:00 +00:00
|
|
|
// This does not consumes action points
|
2014-12-30 00:00:00 +00:00
|
|
|
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;
|
|
|
|
}
|
|
|
|
|
2014-12-30 00:00:00 +00:00
|
|
|
// String repr
|
|
|
|
jasmineToString(): string {
|
|
|
|
return "Ship " + this.name;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Make an initiative throw, to resolve play order in a battle
|
|
|
|
throwInitiative(gen: RandomGenerator): void {
|
2017-06-11 20:44:12 +00:00
|
|
|
this.play_priority = gen.random() * this.attributes.maneuvrability.get();
|
2014-12-30 00:00:00 +00:00
|
|
|
}
|
2014-12-30 00:00:00 +00:00
|
|
|
|
|
|
|
// Return the player owning this ship
|
|
|
|
getPlayer(): Player {
|
2017-03-09 17:11:00 +00:00
|
|
|
return this.fleet.player;
|
2015-01-19 00:00:00 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// get the current battle this ship is engaged in
|
2017-03-09 17:11:00 +00:00
|
|
|
getBattle(): Battle | null {
|
|
|
|
return this.fleet.battle;
|
2014-12-30 00:00:00 +00:00
|
|
|
}
|
2014-12-31 00:00:00 +00:00
|
|
|
|
2017-09-17 23:12:51 +00:00
|
|
|
/**
|
|
|
|
* Get the list of actions available
|
|
|
|
*
|
|
|
|
* This list does not filter out actions unavailable due to insufficient AP, it only filters out actions that
|
|
|
|
* are not allowed/available at all on the ship
|
|
|
|
*/
|
2014-12-31 00:00:00 +00:00
|
|
|
getAvailableActions(): BaseAction[] {
|
2015-01-16 00:00:00 +00:00
|
|
|
var actions: BaseAction[] = [];
|
|
|
|
|
2017-02-15 22:34:27 +00:00
|
|
|
if (this.alive) {
|
2017-09-17 23:12:51 +00:00
|
|
|
let slots = [SlotType.Engine, SlotType.Power, SlotType.Hull, SlotType.Shield, SlotType.Weapon];
|
|
|
|
slots.forEach(slot => {
|
|
|
|
this.listEquipment(slot).forEach(equipment => {
|
2017-09-19 15:09:06 +00:00
|
|
|
if (equipment.action) {
|
2017-09-17 23:12:51 +00:00
|
|
|
actions.push(equipment.action)
|
|
|
|
}
|
|
|
|
});
|
2017-02-15 22:34:27 +00:00
|
|
|
});
|
|
|
|
}
|
2015-01-16 00:00:00 +00:00
|
|
|
|
|
|
|
actions.push(new EndTurnAction());
|
|
|
|
return actions;
|
2014-12-31 00:00:00 +00:00
|
|
|
}
|
|
|
|
|
2017-03-17 00:07:00 +00:00
|
|
|
/**
|
|
|
|
* Get the number of upgrade points available to improve skills
|
|
|
|
*/
|
|
|
|
getAvailableUpgradePoints(): number {
|
|
|
|
let used = keys(SHIP_SKILLS).map(skill => this.skills[skill].get()).reduce((a, b) => a + b, 0);
|
|
|
|
return this.level.getSkillPoints() - used;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2017-09-17 22:49:53 +00:00
|
|
|
* Try to upgrade a skill by 1 point or more
|
2017-03-17 00:07:00 +00:00
|
|
|
*/
|
2017-09-17 22:49:53 +00:00
|
|
|
upgradeSkill(skill: keyof ShipSkills, points = 1) {
|
|
|
|
if (this.getAvailableUpgradePoints() >= points) {
|
|
|
|
this.skills[skill].add(points);
|
2017-03-17 00:07:00 +00:00
|
|
|
this.updateAttributes();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2015-02-06 00:00:00 +00:00
|
|
|
// Add an event to the battle log, if any
|
2017-05-29 23:15:32 +00:00
|
|
|
addBattleEvent(event: BaseBattleEvent): void {
|
2015-02-06 00:00:00 +00:00
|
|
|
var battle = this.getBattle();
|
|
|
|
if (battle && battle.log) {
|
|
|
|
battle.log.add(event);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-02-07 00:08:07 +00:00
|
|
|
/**
|
2017-02-07 19:15:21 +00:00
|
|
|
* Get a ship value
|
|
|
|
*/
|
|
|
|
getValue(name: keyof ShipValues): number {
|
2017-04-18 19:51:23 +00:00
|
|
|
if (!this.values.hasOwnProperty(name)) {
|
|
|
|
console.error(`No such ship value: ${name}`);
|
|
|
|
return 0;
|
|
|
|
}
|
2017-02-07 19:15:21 +00:00
|
|
|
return this.values[name].get();
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Set a ship value
|
2017-02-07 00:08:07 +00:00
|
|
|
*
|
|
|
|
* If *offset* is true, the value will be added to current value.
|
|
|
|
* If *log* is true, an attribute event will be added to the battle log
|
|
|
|
*
|
2017-02-07 19:15:21 +00:00
|
|
|
* Returns true if the value changed.
|
2017-02-07 00:08:07 +00:00
|
|
|
*/
|
2017-02-07 19:15:21 +00:00
|
|
|
setValue(name: keyof ShipValues, value: number, offset = false, log = true): boolean {
|
2017-02-12 22:18:36 +00:00
|
|
|
let diff = 0;
|
2017-02-07 19:15:21 +00:00
|
|
|
let val = this.values[name];
|
2015-01-19 00:00:00 +00:00
|
|
|
|
|
|
|
if (offset) {
|
2017-02-12 22:18:36 +00:00
|
|
|
diff = val.add(value);
|
2015-01-19 00:00:00 +00:00
|
|
|
} else {
|
2017-02-12 22:18:36 +00:00
|
|
|
diff = val.set(value);
|
2015-01-19 00:00:00 +00:00
|
|
|
}
|
|
|
|
|
2017-06-25 21:07:53 +00:00
|
|
|
if (log && diff != 0 && this.alive) {
|
2017-02-12 22:18:36 +00:00
|
|
|
this.addBattleEvent(new ValueChangeEvent(this, val, diff));
|
2017-02-07 19:15:21 +00:00
|
|
|
}
|
|
|
|
|
2017-02-12 22:18:36 +00:00
|
|
|
return diff != 0;
|
2017-02-07 19:15:21 +00:00
|
|
|
}
|
|
|
|
|
2017-06-22 23:04:54 +00:00
|
|
|
/**
|
|
|
|
* Set a value's maximal capacity
|
|
|
|
*/
|
|
|
|
setValueCapacity(name: keyof ShipValues, maximal: number, log = true): void {
|
|
|
|
if (this.getValue(name) > maximal) {
|
|
|
|
this.setValue(name, maximal, false, log);
|
|
|
|
}
|
|
|
|
this.values[name].setMaximal(maximal);
|
|
|
|
}
|
|
|
|
|
2017-02-07 19:15:21 +00:00
|
|
|
/**
|
|
|
|
* Get a ship attribute's current value
|
|
|
|
*/
|
|
|
|
getAttribute(name: keyof ShipAttributes): number {
|
2017-04-18 19:51:23 +00:00
|
|
|
if (!this.attributes.hasOwnProperty(name)) {
|
|
|
|
console.error(`No such ship attribute: ${name}`);
|
|
|
|
return 0;
|
|
|
|
}
|
2017-02-07 19:15:21 +00:00
|
|
|
return this.attributes[name].get();
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Set a ship attribute
|
|
|
|
*
|
|
|
|
* If *log* is true, an attribute event will be added to the battle log
|
|
|
|
*
|
|
|
|
* Returns true if the value changed.
|
|
|
|
*/
|
|
|
|
setAttribute(name: keyof ShipAttributes, value: number, log = true): boolean {
|
|
|
|
let attr = this.attributes[name];
|
2017-02-12 22:18:36 +00:00
|
|
|
let diff = attr.set(value);
|
2017-02-07 19:15:21 +00:00
|
|
|
|
|
|
|
// TODO more generic
|
|
|
|
if (name == "power_capacity") {
|
2017-06-22 23:04:54 +00:00
|
|
|
this.setValueCapacity("power", attr.get());
|
2017-02-07 19:15:21 +00:00
|
|
|
} else if (name == "shield_capacity") {
|
2017-06-22 23:04:54 +00:00
|
|
|
this.setValueCapacity("shield", attr.get());
|
2017-02-07 19:15:21 +00:00
|
|
|
} else if (name == "hull_capacity") {
|
2017-06-22 23:04:54 +00:00
|
|
|
this.setValueCapacity("hull", attr.get());
|
2017-02-07 19:15:21 +00:00
|
|
|
}
|
|
|
|
|
2017-06-25 21:07:53 +00:00
|
|
|
if (log && diff != 0 && this.alive) {
|
2017-02-12 22:18:36 +00:00
|
|
|
this.addBattleEvent(new ValueChangeEvent(this, attr, diff));
|
2015-01-19 00:00:00 +00:00
|
|
|
}
|
2017-02-07 00:08:07 +00:00
|
|
|
|
2017-02-12 22:18:36 +00:00
|
|
|
return diff != 0;
|
2015-01-19 00:00:00 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// Initialize the action points counter
|
|
|
|
// This should be called once at the start of a battle
|
|
|
|
// If no value is provided, the attribute ap_initial will be used
|
2017-03-09 17:11:00 +00:00
|
|
|
initializeActionPoints(value: number | null = null): void {
|
2015-01-19 00:00:00 +00:00
|
|
|
if (value === null) {
|
2017-06-11 20:44:12 +00:00
|
|
|
value = this.attributes.power_capacity.get();
|
2015-01-19 00:00:00 +00:00
|
|
|
}
|
2017-02-07 19:15:21 +00:00
|
|
|
this.setValue("power", value);
|
2015-01-19 00:00:00 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// Recover action points
|
2017-01-12 00:36:34 +00:00
|
|
|
// This should be called once at the end of a turn
|
2015-01-19 00:00:00 +00:00
|
|
|
// If no value is provided, the current attribute ap_recovery will be used
|
2017-03-09 17:11:00 +00:00
|
|
|
recoverActionPoints(value: number | null = null): void {
|
2017-02-15 22:34:27 +00:00
|
|
|
if (this.alive) {
|
|
|
|
if (value === null) {
|
2017-06-11 20:44:12 +00:00
|
|
|
value = this.attributes.power_generation.get();
|
2017-02-15 22:34:27 +00:00
|
|
|
}
|
|
|
|
this.setValue("power", value, true);
|
2015-01-19 00:00:00 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-02-19 21:52:11 +00:00
|
|
|
/**
|
|
|
|
* Consumes action points
|
|
|
|
*
|
|
|
|
* Return true if it was possible, false if there wasn't enough points.
|
|
|
|
*/
|
|
|
|
useActionPoints(value: number): boolean {
|
|
|
|
if (this.getValue("power") >= value) {
|
|
|
|
this.setValue("power", -value, true);
|
|
|
|
return true;
|
|
|
|
} else {
|
|
|
|
return false;
|
|
|
|
}
|
2015-01-19 00:00:00 +00:00
|
|
|
}
|
2014-12-31 00:00:00 +00:00
|
|
|
|
2017-05-10 17:16:57 +00:00
|
|
|
/**
|
|
|
|
* Method called at the start of battle
|
|
|
|
*/
|
2017-01-12 00:36:34 +00:00
|
|
|
startBattle() {
|
2017-03-14 22:28:07 +00:00
|
|
|
this.alive = true;
|
2017-05-10 17:16:57 +00:00
|
|
|
this.sticky_effects = [];
|
2017-01-12 00:36:34 +00:00
|
|
|
this.updateAttributes();
|
|
|
|
this.restoreHealth();
|
|
|
|
this.initializeActionPoints();
|
2017-05-16 23:12:05 +00:00
|
|
|
this.listEquipment().forEach(equipment => equipment.cooldown.reset());
|
2017-01-12 00:36:34 +00:00
|
|
|
}
|
|
|
|
|
2017-05-10 17:16:57 +00:00
|
|
|
/**
|
|
|
|
* Method called at the end of battle
|
|
|
|
*/
|
|
|
|
endBattle(turncount: number) {
|
|
|
|
// Restore as pristine
|
|
|
|
this.startBattle();
|
|
|
|
|
|
|
|
// Wear down equipment
|
|
|
|
this.listEquipment().forEach(equipment => {
|
|
|
|
equipment.addWear(turncount);
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2015-01-19 00:00:00 +00:00
|
|
|
// Method called at the start of this ship turn
|
2017-01-12 00:36:34 +00:00
|
|
|
startTurn(): void {
|
2017-01-20 00:02:18 +00:00
|
|
|
if (this.playing) {
|
|
|
|
console.error("startTurn called twice", this);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
this.playing = true;
|
|
|
|
|
2017-02-15 22:34:27 +00:00
|
|
|
if (this.alive) {
|
|
|
|
// Recompute attributes
|
|
|
|
this.updateAttributes();
|
2015-01-22 00:00:00 +00:00
|
|
|
|
2017-02-15 22:34:27 +00:00
|
|
|
// Apply sticky effects
|
|
|
|
this.sticky_effects.forEach(effect => effect.startTurn(this));
|
|
|
|
this.cleanStickyEffects();
|
2017-06-13 22:01:39 +00:00
|
|
|
|
|
|
|
// Reset toggle actions state
|
|
|
|
this.listEquipment().forEach(equipment => {
|
|
|
|
if (equipment.action instanceof ToggleAction && equipment.action.activated) {
|
2017-09-19 15:09:06 +00:00
|
|
|
equipment.action.apply(this);
|
2017-06-13 22:01:39 +00:00
|
|
|
}
|
|
|
|
});
|
2017-02-15 22:34:27 +00:00
|
|
|
}
|
2015-02-27 00:00:00 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// Method called at the end of this ship turn
|
|
|
|
endTurn(): void {
|
2017-01-20 00:02:18 +00:00
|
|
|
if (!this.playing) {
|
|
|
|
console.error("endTurn called before startTurn", this);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
this.playing = false;
|
|
|
|
|
2017-05-10 17:28:27 +00:00
|
|
|
if (this.alive) {
|
2017-02-15 22:34:27 +00:00
|
|
|
// Recover action points for next turn
|
|
|
|
this.updateAttributes();
|
|
|
|
this.recoverActionPoints();
|
2017-01-12 00:36:34 +00:00
|
|
|
|
2017-02-15 22:34:27 +00:00
|
|
|
// Apply sticky effects
|
|
|
|
this.sticky_effects.forEach(effect => effect.endTurn(this));
|
|
|
|
this.cleanStickyEffects();
|
2017-05-16 23:12:05 +00:00
|
|
|
|
|
|
|
// Cool down equipment
|
|
|
|
this.listEquipment().forEach(equipment => equipment.cooldown.cool());
|
2017-02-15 22:34:27 +00:00
|
|
|
}
|
2015-02-27 00:00:00 +00:00
|
|
|
}
|
|
|
|
|
2017-01-24 00:14:04 +00:00
|
|
|
/**
|
|
|
|
* Register a sticky effect
|
|
|
|
*
|
|
|
|
* Pay attention to pass a copy, not the original equipment effect, because it will be modified
|
|
|
|
*/
|
|
|
|
addStickyEffect(effect: StickyEffect, log = true): void {
|
2017-02-15 22:34:27 +00:00
|
|
|
if (this.alive) {
|
|
|
|
this.sticky_effects.push(effect);
|
|
|
|
if (log) {
|
2017-06-13 22:01:39 +00:00
|
|
|
this.setActiveEffectsChanged();
|
2017-02-15 22:34:27 +00:00
|
|
|
}
|
2015-02-27 00:00:00 +00:00
|
|
|
}
|
2014-12-31 00:00:00 +00:00
|
|
|
}
|
|
|
|
|
2017-01-24 00:14:04 +00:00
|
|
|
/**
|
|
|
|
* Clean sticky effects that are no longer active
|
|
|
|
*/
|
|
|
|
cleanStickyEffects() {
|
2017-06-25 21:07:53 +00:00
|
|
|
let [active, ended] = binpartition(this.sticky_effects, effect => this.alive && effect.duration > 0);
|
2017-01-24 00:14:04 +00:00
|
|
|
this.sticky_effects = active;
|
2017-06-13 22:01:39 +00:00
|
|
|
if (ended.length) {
|
|
|
|
this.setActiveEffectsChanged();
|
|
|
|
}
|
2017-01-24 00:14:04 +00:00
|
|
|
}
|
|
|
|
|
2017-02-06 21:46:55 +00:00
|
|
|
/**
|
|
|
|
* 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;
|
|
|
|
}
|
|
|
|
|
2017-05-29 18:12:57 +00:00
|
|
|
/**
|
|
|
|
* Get the distance to another ship
|
|
|
|
*/
|
|
|
|
getDistanceTo(other: Ship): number {
|
|
|
|
return Target.newFromShip(this).getDistanceTo(Target.newFromShip(other));
|
|
|
|
}
|
|
|
|
|
2017-02-14 00:30:50 +00:00
|
|
|
/**
|
|
|
|
* Rotate the ship in place to face a direction
|
|
|
|
*/
|
2017-08-17 17:51:22 +00:00
|
|
|
rotate(angle: number, engine: Equipment | null = null, log = true) {
|
2017-02-14 00:30:50 +00:00
|
|
|
if (angle != this.arena_angle) {
|
2017-06-07 17:09:06 +00:00
|
|
|
let start = copy(this.location);
|
2017-02-14 00:30:50 +00:00
|
|
|
this.setArenaFacingAngle(angle);
|
|
|
|
|
|
|
|
if (log) {
|
2017-08-17 17:51:22 +00:00
|
|
|
this.addBattleEvent(new MoveEvent(this, start, copy(this.location), engine));
|
2017-02-14 00:30:50 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-08-17 17:51:22 +00:00
|
|
|
/**
|
|
|
|
* Move the ship to another location
|
|
|
|
*
|
|
|
|
* This does not check or consume action points, but will update area effects (for this ship and the others).
|
|
|
|
*
|
|
|
|
* If *engine* is specified, the facing angle will be updated to simulate an engine maneuver.
|
|
|
|
*/
|
|
|
|
moveTo(x: number, y: number, engine: Equipment | null = null, log = true): void {
|
2017-05-25 23:09:29 +00:00
|
|
|
let dx = x - this.arena_x;
|
|
|
|
let dy = y - this.arena_y;
|
|
|
|
if (dx != 0 || dy != 0) {
|
2017-05-30 16:24:55 +00:00
|
|
|
let start = copy(this.location);
|
|
|
|
|
2017-06-13 22:01:39 +00:00
|
|
|
let area_effects = imaterialize(this.iToggleActions(true));
|
2017-10-03 16:11:30 +00:00
|
|
|
let old_impacted_ships = area_effects.map(action => action.getImpactedShips(this, Target.newFromShip(this)));
|
2017-06-13 22:01:39 +00:00
|
|
|
let old_area_effects = this.getActiveEffects().area;
|
|
|
|
|
2017-08-17 17:51:22 +00:00
|
|
|
if (engine) {
|
|
|
|
let angle = Math.atan2(dy, dx);
|
|
|
|
this.setArenaFacingAngle(angle);
|
|
|
|
}
|
|
|
|
|
2017-02-14 00:30:50 +00:00
|
|
|
this.setArenaPosition(x, y);
|
2015-01-29 00:00:00 +00:00
|
|
|
|
2017-02-14 00:30:50 +00:00
|
|
|
if (log) {
|
2017-08-17 17:51:22 +00:00
|
|
|
this.addBattleEvent(new MoveEvent(this, start, copy(this.location), engine));
|
2017-02-14 00:30:50 +00:00
|
|
|
}
|
2017-06-13 22:01:39 +00:00
|
|
|
|
2017-10-03 16:11:30 +00:00
|
|
|
let new_impacted_ships = area_effects.map(action => action.getImpactedShips(this, Target.newFromShip(this)));
|
2017-06-13 22:01:39 +00:00
|
|
|
let diff_impacted_ships = flatten(zip(old_impacted_ships, new_impacted_ships).map(([a, b]) => disjunctunion(a, b)));
|
|
|
|
let new_area_effects = this.getActiveEffects().area;
|
|
|
|
if (disjunctunion(old_area_effects, new_area_effects).length > 0) {
|
|
|
|
diff_impacted_ships.push(this);
|
|
|
|
}
|
|
|
|
unique(diff_impacted_ships).forEach(ship => ship.setActiveEffectsChanged());
|
2017-02-14 00:30:50 +00:00
|
|
|
}
|
2014-12-31 00:00:00 +00:00
|
|
|
}
|
2015-01-14 00:00:00 +00:00
|
|
|
|
2017-06-25 21:07:53 +00:00
|
|
|
/**
|
|
|
|
* Set the death status on this ship
|
|
|
|
*/
|
2015-02-09 00:00:00 +00:00
|
|
|
setDead(log: boolean = true): void {
|
|
|
|
this.alive = false;
|
2017-02-15 22:34:27 +00:00
|
|
|
this.values.hull.set(0);
|
|
|
|
this.values.shield.set(0);
|
|
|
|
this.values.power.set(0);
|
2017-06-25 21:07:53 +00:00
|
|
|
|
|
|
|
this.sticky_effects = [];
|
|
|
|
this.setActiveEffectsChanged();
|
|
|
|
|
2015-02-09 00:00:00 +00:00
|
|
|
if (log) {
|
|
|
|
this.addBattleEvent(new DeathEvent(this));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-04-24 17:59:16 +00:00
|
|
|
/**
|
|
|
|
* Apply damages to hull and/or shield
|
|
|
|
*
|
|
|
|
* Also apply wear to impacted equipment
|
|
|
|
*/
|
2015-02-03 00:00:00 +00:00
|
|
|
addDamage(hull: number, shield: number, log: boolean = true): void {
|
2017-04-24 17:59:16 +00:00
|
|
|
if (shield > 0) {
|
|
|
|
this.setValue("shield", -shield, true, log);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (hull > 0) {
|
|
|
|
this.setValue("hull", -hull, true, log);
|
|
|
|
}
|
2015-02-03 00:00:00 +00:00
|
|
|
|
2015-02-06 00:00:00 +00:00
|
|
|
if (log) {
|
|
|
|
this.addBattleEvent(new DamageEvent(this, hull, shield));
|
|
|
|
}
|
|
|
|
|
2017-02-07 19:15:21 +00:00
|
|
|
if (this.values.hull.get() === 0) {
|
2015-02-06 00:00:00 +00:00
|
|
|
// Ship is dead
|
2015-02-09 00:00:00 +00:00
|
|
|
this.setDead(log);
|
2015-02-03 00:00:00 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-03-22 21:16:59 +00:00
|
|
|
/**
|
|
|
|
* Get cargo space not occupied by items
|
|
|
|
*/
|
|
|
|
getFreeCargoSpace(): number {
|
|
|
|
return this.cargo_space - this.cargo.length;
|
|
|
|
}
|
|
|
|
|
2017-03-05 17:48:13 +00:00
|
|
|
/**
|
|
|
|
* Set the available cargo space.
|
|
|
|
*/
|
|
|
|
setCargoSpace(cargo: number) {
|
|
|
|
this.cargo_space = cargo;
|
2017-03-05 23:29:02 +00:00
|
|
|
this.cargo.splice(this.cargo_space);
|
2017-03-05 17:48:13 +00:00
|
|
|
}
|
|
|
|
|
2017-03-05 23:29:02 +00:00
|
|
|
/**
|
|
|
|
* Add an equipment to cargo space
|
|
|
|
*
|
|
|
|
* Returns true if successful
|
|
|
|
*/
|
|
|
|
addCargo(item: Equipment): boolean {
|
|
|
|
if (this.cargo.length < this.cargo_space) {
|
|
|
|
return add(this.cargo, item);
|
|
|
|
} else {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-03-14 17:48:04 +00:00
|
|
|
/**
|
|
|
|
* Remove an item from cargo space
|
|
|
|
*
|
|
|
|
* Returns true if successful
|
|
|
|
*/
|
|
|
|
removeCargo(item: Equipment): boolean {
|
|
|
|
return remove(this.cargo, item);
|
|
|
|
}
|
|
|
|
|
2017-03-05 23:29:02 +00:00
|
|
|
/**
|
|
|
|
* Equip an item from cargo to the first available slot
|
|
|
|
*
|
|
|
|
* Returns true if successful
|
|
|
|
*/
|
2017-03-22 21:16:59 +00:00
|
|
|
equip(item: Equipment, from_cargo = true): boolean {
|
2017-04-25 18:24:43 +00:00
|
|
|
let free_slot = this.canEquip(item);
|
2017-03-05 23:29:02 +00:00
|
|
|
|
2017-03-22 21:16:59 +00:00
|
|
|
if (free_slot && (!from_cargo || remove(this.cargo, item))) {
|
2017-03-05 23:29:02 +00:00
|
|
|
free_slot.attach(item);
|
2017-04-25 18:24:43 +00:00
|
|
|
if (item.attached_to == free_slot && free_slot.attached == item) {
|
|
|
|
this.updateAttributes();
|
|
|
|
return true;
|
|
|
|
} else {
|
|
|
|
return false;
|
|
|
|
}
|
2017-03-05 23:29:02 +00:00
|
|
|
} else {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-04-25 18:24:43 +00:00
|
|
|
/**
|
|
|
|
* Check if a ship is able to equip en item, and return the slot it may fit in, or null
|
|
|
|
*/
|
|
|
|
canEquip(item: Equipment): Slot | null {
|
|
|
|
let free_slot = first(this.slots, slot => slot.type == item.slot_type && !slot.attached);
|
|
|
|
if (free_slot) {
|
2017-04-25 23:27:42 +00:00
|
|
|
if (item.canBeEquipped(this.attributes)) {
|
2017-04-25 18:24:43 +00:00
|
|
|
return free_slot;
|
|
|
|
} else {
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-03-05 23:29:02 +00:00
|
|
|
/**
|
|
|
|
* Remove an equipped item, returning it to cargo
|
|
|
|
*
|
|
|
|
* Returns true if successful
|
|
|
|
*/
|
2017-03-22 21:16:59 +00:00
|
|
|
unequip(item: Equipment, to_cargo = true): boolean {
|
|
|
|
if (item.attached_to && item.attached_to.attached == item && (!to_cargo || this.cargo.length < this.cargo_space)) {
|
2017-03-05 23:29:02 +00:00
|
|
|
item.detach();
|
2017-03-22 21:16:59 +00:00
|
|
|
if (to_cargo) {
|
|
|
|
add(this.cargo, item);
|
|
|
|
}
|
2017-03-05 23:29:02 +00:00
|
|
|
|
|
|
|
this.updateAttributes();
|
|
|
|
|
|
|
|
return true;
|
|
|
|
} else {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Add an empty equipment slot of the given type
|
|
|
|
*/
|
2015-01-14 00:00:00 +00:00
|
|
|
addSlot(type: SlotType): Slot {
|
|
|
|
var result = new Slot(this, type);
|
|
|
|
this.slots.push(result);
|
|
|
|
return result;
|
2015-02-17 00:00:00 +00:00
|
|
|
}
|
|
|
|
|
2017-03-05 23:29:02 +00:00
|
|
|
/**
|
|
|
|
* List all equipments attached to slots of a given type (any slot type if null)
|
|
|
|
*/
|
|
|
|
listEquipment(slottype: SlotType | null = null): Equipment[] {
|
2017-03-09 17:11:00 +00:00
|
|
|
return nna(this.slots.filter(slot => slot.attached && (slottype == null || slot.type == slottype)).map(slot => slot.attached));
|
2015-01-14 00:00:00 +00:00
|
|
|
}
|
2015-01-22 00:00:00 +00:00
|
|
|
|
2017-03-22 21:16:59 +00:00
|
|
|
/**
|
|
|
|
* Get the first free slot of a given type, null if none is available
|
|
|
|
*/
|
|
|
|
getFreeSlot(type: SlotType): Slot | null {
|
|
|
|
return first(this.slots, slot => slot.type == type && slot.attached == null);
|
|
|
|
}
|
|
|
|
|
2015-02-13 00:00:00 +00:00
|
|
|
// Get the number of attached equipments
|
|
|
|
getEquipmentCount(): number {
|
|
|
|
var result = 0;
|
|
|
|
this.slots.forEach((slot: Slot) => {
|
|
|
|
if (slot.attached) {
|
|
|
|
result++;
|
|
|
|
}
|
|
|
|
});
|
|
|
|
return result;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Get a random attached equipment, null if no equipment is attached
|
2017-03-09 17:11:00 +00:00
|
|
|
getRandomEquipment(random = RandomGenerator.global): Equipment | null {
|
2015-02-13 00:00:00 +00:00
|
|
|
var count = this.getEquipmentCount();
|
|
|
|
if (count === 0) {
|
|
|
|
return null;
|
|
|
|
} else {
|
2017-02-26 17:44:15 +00:00
|
|
|
var picked = random.randInt(0, count - 1);
|
2017-03-09 17:11:00 +00:00
|
|
|
var result: Equipment | null = null;
|
2015-02-13 00:00:00 +00:00
|
|
|
var index = 0;
|
|
|
|
this.slots.forEach((slot: Slot) => {
|
|
|
|
if (slot.attached) {
|
|
|
|
if (index === picked) {
|
|
|
|
result = slot.attached;
|
|
|
|
}
|
|
|
|
index++;
|
|
|
|
}
|
|
|
|
});
|
|
|
|
return result;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2015-01-22 00:00:00 +00:00
|
|
|
// Update attributes, taking into account attached equipment and active effects
|
|
|
|
updateAttributes(): void {
|
2017-06-25 21:07:53 +00:00
|
|
|
let new_attrs = new ShipAttributes();
|
2017-03-17 00:07:00 +00:00
|
|
|
|
2017-06-25 21:07:53 +00:00
|
|
|
if (this.alive) {
|
2017-03-17 00:07:00 +00:00
|
|
|
// TODO better typing for iteritems
|
|
|
|
|
|
|
|
// Apply base skills
|
2017-05-02 21:33:58 +00:00
|
|
|
iteritems(<any>this.skills, (key: keyof ShipAttributes, skill: ShipAttribute) => {
|
2017-03-17 00:07:00 +00:00
|
|
|
new_attrs[key].add(skill.get());
|
|
|
|
});
|
|
|
|
|
|
|
|
// Sum all attribute effects
|
2017-02-15 22:34:27 +00:00
|
|
|
this.collectEffects("attr").forEach((effect: AttributeEffect) => {
|
|
|
|
new_attrs[effect.attrcode].add(effect.value);
|
|
|
|
});
|
2015-01-22 00:00:00 +00:00
|
|
|
|
2017-02-15 22:34:27 +00:00
|
|
|
// Apply limit attributes
|
|
|
|
this.collectEffects("attrlimit").forEach((effect: AttributeLimitEffect) => {
|
|
|
|
new_attrs[effect.attrcode].setMaximal(effect.value);
|
|
|
|
});
|
|
|
|
}
|
2017-06-25 21:07:53 +00:00
|
|
|
|
|
|
|
// Set final attributes
|
|
|
|
iteritems(<any>new_attrs, (key, value) => {
|
|
|
|
this.setAttribute(<keyof ShipAttributes>key, (<ShipAttribute>value).get());
|
|
|
|
});
|
2015-01-22 00:00:00 +00:00
|
|
|
}
|
|
|
|
|
2015-01-28 00:00:00 +00:00
|
|
|
// Fully restore hull and shield
|
|
|
|
restoreHealth(): void {
|
2017-02-15 22:34:27 +00:00
|
|
|
if (this.alive) {
|
|
|
|
this.values.hull.set(this.attributes.hull_capacity.get());
|
|
|
|
this.values.shield.set(this.attributes.shield_capacity.get());
|
|
|
|
}
|
2015-01-28 00:00:00 +00:00
|
|
|
}
|
|
|
|
|
2017-06-12 22:28:54 +00:00
|
|
|
/**
|
2017-06-13 22:01:39 +00:00
|
|
|
* Get the list of all effects applied on this ship
|
2017-06-12 22:28:54 +00:00
|
|
|
*
|
|
|
|
* This includes:
|
|
|
|
* - Permanent equipment effects
|
|
|
|
* - Sticky effects
|
|
|
|
* - Area effects at current location
|
|
|
|
*/
|
2017-06-13 22:01:39 +00:00
|
|
|
getActiveEffects(): ActiveEffectsEvent {
|
|
|
|
let result = new ActiveEffectsEvent(this);
|
2017-06-25 21:07:53 +00:00
|
|
|
if (this.alive) {
|
|
|
|
result.equipment = flatten(this.slots.map(slot => slot.attached ? slot.attached.effects : []));
|
|
|
|
result.sticky = this.sticky_effects;
|
|
|
|
let battle = this.getBattle();
|
|
|
|
result.area = battle ? imaterialize(battle.iAreaEffects(this.arena_x, this.arena_y)) : [];
|
|
|
|
}
|
2017-06-13 22:01:39 +00:00
|
|
|
return result;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Indicate a change in active effects to the log
|
|
|
|
*/
|
|
|
|
setActiveEffectsChanged(): void {
|
|
|
|
this.addBattleEvent(this.getActiveEffects());
|
|
|
|
this.updateAttributes();
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Iterator over all effects active for this ship.
|
|
|
|
*/
|
2017-06-12 22:28:54 +00:00
|
|
|
ieffects(): Iterator<BaseEffect> {
|
|
|
|
let battle = this.getBattle();
|
|
|
|
let area_effects = battle ? battle.iAreaEffects(this.arena_x, this.arena_y) : IEMPTY;
|
|
|
|
return ichain(
|
|
|
|
ichainit(imap(iarray(this.slots), slot => slot.attached ? iarray(slot.attached.effects) : IEMPTY)),
|
|
|
|
imap(iarray(this.sticky_effects), effect => effect.base),
|
|
|
|
area_effects
|
|
|
|
);
|
|
|
|
}
|
2015-01-22 00:00:00 +00:00
|
|
|
|
2017-06-13 22:01:39 +00:00
|
|
|
/**
|
|
|
|
* Iterator over toggle actions
|
|
|
|
*/
|
|
|
|
iToggleActions(only_active = false): Iterator<ToggleAction> {
|
|
|
|
return <Iterator<ToggleAction>>ifilter(iarray(this.getAvailableActions()), action => {
|
|
|
|
return (action instanceof ToggleAction && (action.activated || !only_active));
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2017-06-12 22:28:54 +00:00
|
|
|
/**
|
|
|
|
* Iterator over area effects from this ship impacting a location
|
|
|
|
*/
|
|
|
|
iAreaEffects(x: number, y: number): Iterator<BaseEffect> {
|
|
|
|
let distance = Target.newFromShip(this).getDistanceTo({ x: x, y: y });
|
2017-06-13 22:01:39 +00:00
|
|
|
return ichainit(imap(this.iToggleActions(true), action => {
|
|
|
|
if (distance <= action.radius) {
|
2017-06-12 22:28:54 +00:00
|
|
|
return iarray(action.effects);
|
|
|
|
} else {
|
|
|
|
return IEMPTY;
|
2017-02-07 19:15:21 +00:00
|
|
|
}
|
2017-06-12 22:28:54 +00:00
|
|
|
}));
|
|
|
|
}
|
2017-02-07 19:15:21 +00:00
|
|
|
|
2017-06-12 22:28:54 +00:00
|
|
|
// Collect all effects to apply for updateAttributes
|
|
|
|
private collectEffects(code: string): BaseEffect[] {
|
|
|
|
return imaterialize(ifilter(this.ieffects(), effect => effect.code == code));
|
2015-01-22 00:00:00 +00:00
|
|
|
}
|
2017-07-12 23:18:20 +00:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Get a textual description of an attribute, and the origin of its value
|
|
|
|
*/
|
|
|
|
getAttributeDescription(attribute: keyof ShipAttributes): string {
|
|
|
|
let result = this.attributes[attribute].description;
|
|
|
|
|
|
|
|
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}`);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if (attribute in this.skills) {
|
|
|
|
let skill = this.skills[<keyof ShipSkills>attribute];
|
|
|
|
if (skill.get()) {
|
|
|
|
diffs.push(`Levelled up: +${skill.get()}`);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
this.slots.forEach(slot => {
|
|
|
|
if (slot.attached) {
|
|
|
|
let equipment = slot.attached;
|
|
|
|
equipment.effects.forEach(effect => addEffect(equipment.getFullName(), effect));
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
this.sticky_effects.forEach(effect => addEffect("???", effect.base));
|
|
|
|
|
|
|
|
let sources = diffs.concat(limits).join("\n");
|
|
|
|
return sources ? (result + "\n\n" + sources) : result;
|
|
|
|
}
|
2014-12-29 00:00:00 +00:00
|
|
|
}
|
2015-01-07 00:00:00 +00:00
|
|
|
}
|