1
0
Fork 0
spacetac/src/core/Ship.ts

696 lines
22 KiB
TypeScript
Raw Normal View History

/// <reference path="ShipAttribute.ts"/>
/// <reference path="ShipValue.ts"/>
2017-02-09 00:00:35 +00:00
module TS.SpaceTac {
2017-03-17 00:07:00 +00:00
/**
* Set of upgradable skills for a ship
*/
export class ShipSkills {
// Skills
skill_material = new ShipAttribute("material skill")
skill_energy = new ShipAttribute("energy skill")
skill_electronics = new ShipAttribute("electronics skill")
skill_human = new ShipAttribute("human skill")
skill_time = new ShipAttribute("time skill")
skill_gravity = new ShipAttribute("gravity skill")
}
/**
* Set of ShipAttribute for a ship
*/
2017-03-17 00:07:00 +00:00
export class ShipAttributes extends ShipSkills {
// Attribute controlling the play order
initiative = new ShipAttribute("initiative")
// Maximal hull value
hull_capacity = new ShipAttribute("hull capacity")
// Maximal shield value
shield_capacity = new ShipAttribute("shield capacity")
// Maximal power value
power_capacity = new ShipAttribute("power capacity")
// Initial power value at the start of a battle
power_initial = new ShipAttribute("initial power")
// Power value recovered each turn
power_recovery = new ShipAttribute("power recovery")
}
/**
* Set of ShipValue for a ship
*/
export class ShipValues {
hull = new ShipValue("hull")
shield = new ShipValue("shield")
power = new ShipValue("power")
}
/**
* Static attributes and values object for name queries
*/
2017-03-17 00:07:00 +00:00
export const SHIP_SKILLS = new ShipSkills();
export const SHIP_ATTRIBUTES = new ShipAttributes();
export const SHIP_VALUES = new ShipValues();
/**
* A single ship in a fleet
*/
export class Ship {
2014-12-29 00:00:00 +00:00
// Fleet this ship is a member of
fleet: Fleet
2014-12-29 00:00:00 +00:00
// Level of this ship
2017-03-17 00:07:00 +00:00
level = new ShipLevel()
skills = new ShipSkills()
// Name of the ship
name: string
2015-05-05 19:49:33 +00:00
// Code of the ShipModel used to create it
model: ShipModel
2015-05-05 19:49:33 +00:00
// Flag indicating if the ship is alive
alive: boolean
// Position in the arena
arena_x: number
arena_y: number
// Facing direction in the arena
arena_angle: number
2017-01-23 23:07:54 +00:00
// Sticky effects that applies a given number of times
sticky_effects: StickyEffect[]
2015-01-14 00:00:00 +00:00
// List of slots, able to contain equipment
slots: Slot[]
// Cargo
cargo_space: number = 0
cargo: Equipment[] = []
// Ship attributes
attributes = new ShipAttributes()
2015-01-14 00:00:00 +00:00
// Ship values
values = new ShipValues()
// Boolean set to true if the ship is currently playing its turn
playing = false
// Priority in play_order
play_priority = 0;
// Create a new ship inside a fleet
constructor(fleet: Fleet | null = null, name = "Ship", model = new ShipModel("default", "Default", 1, 0, false, 0)) {
this.fleet = fleet || new Fleet();
this.name = name;
this.alive = true;
2017-01-23 23:07:54 +00:00
this.sticky_effects = [];
2015-01-14 00:00:00 +00:00
this.slots = [];
this.arena_x = 0;
this.arena_y = 0;
this.arena_angle = 0;
2014-12-31 00:00:00 +00:00
if (fleet) {
fleet.addShip(this);
}
this.model = model;
this.setCargoSpace(model.cargo);
model.slots.forEach(slot => this.addSlot(slot));
}
// 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.values.power.get() > 0;
return this.alive && ap_checked;
}
// Set position in the arena
2014-12-31 00:00:00 +00:00
// 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 "Ship " + this.name;
}
// Make an initiative throw, to resolve play order in a battle
throwInitiative(gen: RandomGenerator): void {
2017-02-26 17:44:15 +00:00
this.play_priority = gen.random() * this.attributes.initiative.get();
}
// 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-31 00:00:00 +00:00
2014-12-31 00:00:00 +00:00
// Get the list of actions available
// This list does not filter out actions unavailable due to insufficient AP, it only filters out
2014-12-31 00:00:00 +00:00
// actions that are not allowed/available at all on the ship
getAvailableActions(): BaseAction[] {
var actions: BaseAction[] = [];
2017-02-15 22:34:27 +00:00
if (this.alive) {
this.slots.forEach((slot: Slot) => {
if (slot.attached && slot.attached.action && slot.attached.action.code != "nothing") {
2017-02-15 22:34:27 +00:00
actions.push(slot.attached.action);
}
});
}
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;
}
/**
* Try to upgrade a skill by 1 point
*/
upgradeSkill(skill: keyof ShipSkills) {
if (this.getAvailableUpgradePoints() > 0) {
this.skills[skill].add(1);
this.updateAttributes();
}
}
// Add an event to the battle log, if any
addBattleEvent(event: BaseBattleEvent): void {
var battle = this.getBattle();
if (battle && battle.log) {
battle.log.add(event);
}
}
2017-02-07 00:08:07 +00:00
/**
* Get a ship value
*/
getValue(name: keyof ShipValues): number {
if (!this.values.hasOwnProperty(name)) {
console.error(`No such ship value: ${name}`);
return 0;
}
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
*
* Returns true if the value changed.
2017-02-07 00:08:07 +00:00
*/
setValue(name: keyof ShipValues, value: number, offset = false, log = true): boolean {
let diff = 0;
let val = this.values[name];
2015-01-19 00:00:00 +00:00
if (offset) {
diff = val.add(value);
2015-01-19 00:00:00 +00:00
} else {
diff = val.set(value);
2015-01-19 00:00:00 +00:00
}
if (log && diff != 0) {
this.addBattleEvent(new ValueChangeEvent(this, val, diff));
}
return diff != 0;
}
/**
* 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();
}
/**
* 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];
let diff = attr.set(value);
// TODO more generic
if (name == "power_capacity") {
this.values.power.setMaximal(attr.get());
} else if (name == "shield_capacity") {
this.values.shield.setMaximal(attr.get());
} else if (name == "hull_capacity") {
this.values.hull.setMaximal(attr.get());
}
if (log && diff != 0) {
this.addBattleEvent(new ValueChangeEvent(this, attr, diff));
2015-01-19 00:00:00 +00:00
}
2017-02-07 00:08:07 +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) {
value = this.attributes.power_initial.get();
2015-01-19 00:00:00 +00:00
}
this.setValue("power", value);
2015-01-19 00:00:00 +00:00
}
// Recover action points
// 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) {
value = this.attributes.power_recovery.get();
}
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
/**
* Method called at the start of battle
*/
startBattle() {
2017-03-14 22:28:07 +00:00
this.alive = true;
this.sticky_effects = [];
this.updateAttributes();
this.restoreHealth();
this.initializeActionPoints();
2017-05-16 23:12:05 +00:00
this.listEquipment().forEach(equipment => equipment.cooldown.reset());
}
/**
* 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
startTurn(): void {
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();
2017-02-15 22:34:27 +00:00
// Apply sticky effects
this.sticky_effects.forEach(effect => effect.startTurn(this));
this.cleanStickyEffects();
}
}
// Method called at the end of this ship turn
endTurn(): void {
if (!this.playing) {
console.error("endTurn called before startTurn", this);
return;
}
this.playing = false;
if (this.alive) {
2017-02-15 22:34:27 +00:00
// Recover action points for next turn
this.updateAttributes();
this.recoverActionPoints();
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
}
}
/**
* 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) {
this.addBattleEvent(new EffectAddedEvent(this, effect));
}
}
2014-12-31 00:00:00 +00:00
}
/**
* Clean sticky effects that are no longer active
*/
cleanStickyEffects() {
let [active, ended] = binpartition(this.sticky_effects, effect => effect.duration > 0);
this.sticky_effects = active;
ended.forEach(effect => this.addBattleEvent(new EffectRemovedEvent(this, effect)));
}
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));
}
/**
* Rotate the ship in place to face a direction
*/
rotate(angle: number, log = true) {
if (angle != this.arena_angle) {
this.setArenaFacingAngle(angle);
if (log) {
2017-05-25 23:09:29 +00:00
this.addBattleEvent(new MoveEvent(this, this.arena_x, this.arena_y, 0));
}
}
}
// Move toward a location
// This does not check or consume action points
moveTo(x: number, y: number, log: boolean = 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) {
let angle = Math.atan2(dy, dx);
this.setArenaFacingAngle(angle);
this.setArenaPosition(x, y);
if (log) {
2017-05-25 23:09:29 +00:00
this.addBattleEvent(new MoveEvent(this, x, y, Math.sqrt(dx * dx + dy * dy)));
}
}
2014-12-31 00:00:00 +00:00
}
2015-02-09 00:00:00 +00:00
// Set the death status on this ship
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);
2015-02-09 00:00:00 +00:00
if (log) {
this.addBattleEvent(new DeathEvent(this));
}
}
/**
* 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 {
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
if (log) {
this.addBattleEvent(new DamageEvent(this, hull, shield));
}
if (this.values.hull.get() === 0) {
// Ship is dead
2015-02-09 00:00:00 +00:00
this.setDead(log);
2015-02-03 00:00:00 +00:00
}
}
/**
* Get cargo space not occupied by items
*/
getFreeCargoSpace(): number {
return this.cargo_space - this.cargo.length;
}
/**
* Set the available cargo space.
*/
setCargoSpace(cargo: number) {
this.cargo_space = cargo;
this.cargo.splice(this.cargo_space);
}
/**
* 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;
}
}
/**
* Remove an item from cargo space
*
* Returns true if successful
*/
removeCargo(item: Equipment): boolean {
return remove(this.cargo, item);
}
/**
* Equip an item from cargo to the first available slot
*
* Returns true if successful
*/
equip(item: Equipment, from_cargo = true): boolean {
let free_slot = this.canEquip(item);
if (free_slot && (!from_cargo || remove(this.cargo, item))) {
free_slot.attach(item);
if (item.attached_to == free_slot && free_slot.attached == item) {
this.updateAttributes();
return true;
} else {
return false;
}
} else {
return false;
}
}
/**
* 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) {
if (item.canBeEquipped(this.attributes)) {
return free_slot;
} else {
return null;
}
} else {
return null;
}
}
/**
* Remove an equipped item, returning it to cargo
*
* Returns true if successful
*/
unequip(item: Equipment, to_cargo = true): boolean {
if (item.attached_to && item.attached_to.attached == item && (!to_cargo || this.cargo.length < this.cargo_space)) {
item.detach();
if (to_cargo) {
add(this.cargo, item);
}
this.updateAttributes();
return true;
} else {
return false;
}
}
/**
* Add an empty equipment slot of the given type
*/
addSlot(type: SlotType): Slot {
var result = new Slot(this, type);
this.slots.push(result);
return result;
2015-02-17 00:00:00 +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));
}
/**
* 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);
}
// 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 {
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;
var index = 0;
this.slots.forEach((slot: Slot) => {
if (slot.attached) {
if (index === picked) {
result = slot.attached;
}
index++;
}
});
return result;
}
}
// Update attributes, taking into account attached equipment and active effects
updateAttributes(): void {
2017-02-15 22:34:27 +00:00
if (this.alive) {
var new_attrs = new ShipAttributes();
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);
});
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-03-17 00:07:00 +00:00
// Set final attributes
2017-02-15 22:34:27 +00:00
iteritems(<any>new_attrs, (key, value) => {
this.setAttribute(<keyof ShipAttributes>key, (<ShipAttribute>value).get());
});
}
}
// 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());
}
}
// Collect all effects to apply for updateAttributes
private collectEffects(code: string): BaseEffect[] {
var result: BaseEffect[] = [];
this.slots.forEach(slot => {
if (slot.attached) {
slot.attached.effects.forEach(effect => {
if (effect.code == code) {
result.push(effect);
}
});
}
});
this.sticky_effects.forEach(effect => {
if (effect.base.code == code) {
result.push(effect.base);
}
});
return result;
}
2014-12-29 00:00:00 +00:00
}
2015-01-07 00:00:00 +00:00
}