2015-01-28 00:00:00 +00:00
|
|
|
/// <reference path="BaseAction.ts"/>
|
|
|
|
|
2017-09-24 22:23:22 +00:00
|
|
|
module TK.SpaceTac {
|
2017-04-18 19:51:23 +00:00
|
|
|
/**
|
2017-10-03 16:11:30 +00:00
|
|
|
* Action to trigger an equipment (for example a weapon), with an optional target
|
2017-11-14 00:07:06 +00:00
|
|
|
*
|
|
|
|
* The target will be resolved as a list of ships, on which all the action effects will be applied
|
2017-04-18 19:51:23 +00:00
|
|
|
*/
|
2017-10-03 16:11:30 +00:00
|
|
|
export class TriggerAction extends BaseAction {
|
2017-04-18 19:51:23 +00:00
|
|
|
// Power consumption
|
2017-09-19 15:09:06 +00:00
|
|
|
power: number
|
2017-04-18 19:51:23 +00:00
|
|
|
|
2017-10-03 16:11:30 +00:00
|
|
|
// Maximal range of the weapon (distance to target)
|
2017-04-18 19:51:23 +00:00
|
|
|
range: number
|
|
|
|
|
2017-10-03 16:11:30 +00:00
|
|
|
// Radius around the target that will be impacted
|
2017-09-19 15:09:06 +00:00
|
|
|
blast: number
|
2017-04-18 19:51:23 +00:00
|
|
|
|
2017-10-03 16:11:30 +00:00
|
|
|
// Angle of the area between the source and the target that will be impacted
|
|
|
|
angle: number
|
|
|
|
|
2017-12-08 00:18:15 +00:00
|
|
|
// Influence of "precision" of firing ship (0..100)
|
|
|
|
aim: number
|
|
|
|
|
|
|
|
// Influence of "maneuvrability" of impacted ship (0..100)
|
|
|
|
evasion: number
|
|
|
|
|
|
|
|
// Influence of luck
|
|
|
|
luck: number
|
|
|
|
|
2017-09-19 15:09:06 +00:00
|
|
|
// Effects applied on target
|
|
|
|
effects: BaseEffect[]
|
2015-01-28 00:00:00 +00:00
|
|
|
|
2017-03-09 17:11:00 +00:00
|
|
|
// Equipment cannot be null
|
2017-09-19 15:09:06 +00:00
|
|
|
equipment: Equipment
|
2017-03-09 17:11:00 +00:00
|
|
|
|
2017-12-08 00:18:15 +00:00
|
|
|
constructor(equipment: Equipment, effects: BaseEffect[] = [], power = 1, range = 0, blast = 0, angle = 0, aim = 0, evasion = 0, luck = 0, code = `fire-${equipment.code}`) {
|
2017-11-29 22:03:58 +00:00
|
|
|
super(code, equipment);
|
2015-01-28 00:00:00 +00:00
|
|
|
|
2017-04-18 19:51:23 +00:00
|
|
|
this.power = power;
|
|
|
|
this.range = range;
|
|
|
|
this.effects = effects;
|
|
|
|
this.blast = blast;
|
2017-10-03 16:11:30 +00:00
|
|
|
this.angle = angle;
|
2017-12-08 00:18:15 +00:00
|
|
|
this.aim = aim;
|
|
|
|
this.evasion = evasion;
|
|
|
|
this.luck = luck;
|
2017-04-18 19:51:23 +00:00
|
|
|
}
|
|
|
|
|
2017-11-29 22:03:58 +00:00
|
|
|
getVerb(): string {
|
|
|
|
return this.range ? "Fire" : "Trigger";
|
|
|
|
}
|
|
|
|
|
2017-09-19 15:09:06 +00:00
|
|
|
getDefaultTarget(ship: Ship): Target {
|
|
|
|
if (this.range == 0) {
|
|
|
|
return Target.newFromShip(ship);
|
|
|
|
} else {
|
|
|
|
let battle = ship.getBattle();
|
|
|
|
if (battle) {
|
|
|
|
let harmful = any(this.effects, effect => !effect.isBeneficial());
|
2018-01-16 00:08:24 +00:00
|
|
|
let ships = imaterialize(harmful ? battle.ienemies(ship, true) : ifilter(battle.iallies(ship, true), iship => !iship.is(ship)));
|
2017-09-19 15:09:06 +00:00
|
|
|
let nearest = minBy(ships, iship => arenaDistance(ship.location, iship.location));
|
|
|
|
return Target.newFromShip(nearest);
|
|
|
|
} else {
|
|
|
|
return Target.newFromShip(ship);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-10-03 16:11:30 +00:00
|
|
|
getTargettingMode(ship: Ship): ActionTargettingMode {
|
|
|
|
if (this.blast) {
|
|
|
|
if (this.range) {
|
|
|
|
return ActionTargettingMode.SPACE;
|
|
|
|
} else {
|
|
|
|
return ActionTargettingMode.SURROUNDINGS;
|
|
|
|
}
|
|
|
|
} else if (this.range) {
|
|
|
|
if (this.angle) {
|
|
|
|
return ActionTargettingMode.SPACE;
|
|
|
|
} else {
|
|
|
|
return ActionTargettingMode.SHIP;
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
return ActionTargettingMode.SELF_CONFIRM;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-04-18 19:51:23 +00:00
|
|
|
getActionPointsUsage(ship: Ship, target: Target | null): number {
|
|
|
|
return this.power;
|
|
|
|
}
|
|
|
|
|
|
|
|
getRangeRadius(ship: Ship): number {
|
|
|
|
return this.range;
|
|
|
|
}
|
|
|
|
|
2017-12-08 00:18:15 +00:00
|
|
|
/**
|
|
|
|
* Get the success factor [0-1] for this action applied from a ship to another
|
|
|
|
*
|
|
|
|
* This is a predictible formula, not including random elements.
|
|
|
|
*/
|
|
|
|
getSuccessFactor(from: Ship, to: Ship): number {
|
|
|
|
let aim = this.aim * 0.01;
|
|
|
|
let evasion = this.evasion * 0.01;
|
|
|
|
let f1 = (x: number) => (x < 0 ? -1 : 1) * (1 - 1 / (x * x + 1));
|
|
|
|
let prec = from.getAttribute("precision");
|
|
|
|
let man = to.getAttribute("maneuvrability");
|
|
|
|
let delta = Math.min(aim, evasion) * f1(0.2 * (prec - man));
|
|
|
|
return clamp(1 - aim * (1 - f1(prec)) - evasion * f1(man) + delta, 0, 1);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Get the effective success of the action [0-1].
|
|
|
|
*
|
|
|
|
* Result has more chance to be near the success factor, but may be in the whole [0-1] range.
|
|
|
|
*/
|
|
|
|
getEffectiveSuccess(from: Ship, to: Ship, random = RandomGenerator.global): number {
|
|
|
|
let p = this.getSuccessFactor(from, to);
|
|
|
|
let s = this.luck * 0.01;
|
|
|
|
if (s) {
|
|
|
|
let c = (2 / (2 - s)) - 1;
|
|
|
|
let x = random.random();
|
|
|
|
if (x <= p) {
|
|
|
|
return Math.pow(x, c) / Math.pow(p, c - 1);
|
|
|
|
} else {
|
|
|
|
return 1 - Math.pow(1 - x, c) / Math.pow(1 - p, c - 1);
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
return p;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-10-03 16:11:30 +00:00
|
|
|
filterImpactedShips(source: ArenaLocation, target: Target, ships: Ship[]): Ship[] {
|
|
|
|
if (this.blast) {
|
|
|
|
return ships.filter(ship => arenaDistance(ship.location, target) <= this.blast);
|
|
|
|
} else if (this.angle) {
|
|
|
|
let angle = arenaAngle(source, target);
|
|
|
|
let maxangle = (this.angle * 0.5) * Math.PI / 180;
|
2017-10-05 17:33:10 +00:00
|
|
|
return ships.filter(ship => {
|
|
|
|
let dist = arenaDistance(source, ship.location);
|
|
|
|
if (dist < 0.000001 || dist > this.range) {
|
|
|
|
return false;
|
|
|
|
} else {
|
2017-12-12 23:24:02 +00:00
|
|
|
return Math.abs(angularDifference(arenaAngle(source, ship.location), angle)) < maxangle;
|
2017-10-05 17:33:10 +00:00
|
|
|
}
|
|
|
|
});
|
2017-10-03 16:11:30 +00:00
|
|
|
} else {
|
2017-11-14 00:07:06 +00:00
|
|
|
return ships.filter(ship => ship.is(target.ship_id));
|
2017-10-03 16:11:30 +00:00
|
|
|
}
|
2015-01-28 00:00:00 +00:00
|
|
|
}
|
|
|
|
|
2017-03-09 17:11:00 +00:00
|
|
|
checkLocationTarget(ship: Ship, target: Target): Target | null {
|
2017-10-03 16:11:30 +00:00
|
|
|
if (target && (this.blast > 0 || this.angle > 0)) {
|
2017-04-18 19:51:23 +00:00
|
|
|
target = target.constraintInRange(ship.arena_x, ship.arena_y, this.range);
|
2015-01-28 00:00:00 +00:00
|
|
|
return target;
|
|
|
|
} else {
|
|
|
|
return null;
|
|
|
|
}
|
2015-01-28 00:00:00 +00:00
|
|
|
}
|
|
|
|
|
2017-03-09 17:11:00 +00:00
|
|
|
checkShipTarget(ship: Ship, target: Target): Target | null {
|
2017-11-14 00:07:06 +00:00
|
|
|
if (this.range > 0 && ship.is(target.ship_id)) {
|
2017-09-19 15:09:06 +00:00
|
|
|
// No self fire
|
2015-01-28 00:00:00 +00:00
|
|
|
return null;
|
|
|
|
} else {
|
2015-01-28 00:00:00 +00:00
|
|
|
// Check if target is in range
|
2017-10-03 16:11:30 +00:00
|
|
|
if (this.blast > 0 || this.angle > 0) {
|
2017-03-08 23:18:40 +00:00
|
|
|
return this.checkLocationTarget(ship, new Target(target.x, target.y));
|
2017-04-18 19:51:23 +00:00
|
|
|
} else if (target.isInRange(ship.arena_x, ship.arena_y, this.range)) {
|
2015-01-28 00:00:00 +00:00
|
|
|
return target;
|
|
|
|
} else {
|
|
|
|
return null;
|
|
|
|
}
|
2015-01-28 00:00:00 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-02-27 00:42:12 +00:00
|
|
|
/**
|
|
|
|
* Collect the effects applied by this action
|
2017-12-08 00:18:15 +00:00
|
|
|
*
|
|
|
|
* If *luck* is specified, an effective success factor is used, instead of an estimated one
|
2017-02-27 00:42:12 +00:00
|
|
|
*/
|
2017-12-08 00:18:15 +00:00
|
|
|
getEffects(ship: Ship, target: Target, source = ship.location, luck?: RandomGenerator): [Ship, BaseEffect, number][] {
|
|
|
|
let result: [Ship, BaseEffect, number][] = [];
|
2017-10-03 16:11:30 +00:00
|
|
|
let ships = this.getImpactedShips(ship, target, source);
|
2017-12-08 00:18:15 +00:00
|
|
|
ships.forEach(iship => {
|
|
|
|
let success = luck ? this.getEffectiveSuccess(ship, iship, luck) : this.getSuccessFactor(ship, iship);
|
|
|
|
this.effects.forEach(effect => result.push([iship, effect, success]));
|
2017-02-27 00:42:12 +00:00
|
|
|
});
|
|
|
|
return result;
|
|
|
|
}
|
2015-02-20 00:00:00 +00:00
|
|
|
|
2017-11-14 00:07:06 +00:00
|
|
|
protected getSpecificDiffs(ship: Ship, battle: Battle, target: Target): BaseBattleDiff[] {
|
|
|
|
let result: BaseBattleDiff[] = [];
|
|
|
|
|
|
|
|
if (arenaDistance(ship.location, target) > 1e-6) {
|
2017-09-14 21:33:08 +00:00
|
|
|
// Face the target
|
2017-11-14 00:07:06 +00:00
|
|
|
let angle = arenaAngle(ship.location, target);
|
2017-12-12 23:24:02 +00:00
|
|
|
if (Math.abs(angularDifference(angle, ship.arena_angle)) > 1e-6) {
|
2017-11-14 00:07:06 +00:00
|
|
|
let destination = new ArenaLocationAngle(ship.arena_x, ship.arena_y, angle);
|
|
|
|
let engine = first(ship.listEquipment(SlotType.Engine), () => true);
|
|
|
|
result.push(new ShipMoveDiff(ship, ship.location, destination, engine));
|
|
|
|
}
|
2017-08-17 17:51:22 +00:00
|
|
|
|
2017-11-14 00:07:06 +00:00
|
|
|
// Fire a projectile
|
|
|
|
if (this.equipment && this.equipment.slot_type == SlotType.Weapon) {
|
|
|
|
result.push(new ProjectileFiredDiff(ship, this.equipment, target));
|
|
|
|
}
|
|
|
|
}
|
2015-03-11 00:00:00 +00:00
|
|
|
|
2017-02-27 00:42:12 +00:00
|
|
|
// Apply effects
|
2017-12-08 00:18:15 +00:00
|
|
|
let effects = this.getEffects(ship, target, undefined, RandomGenerator.global);
|
|
|
|
effects.forEach(([ship_target, effect, success]) => {
|
|
|
|
let diffs = effect.getOnDiffs(ship_target, ship, success);
|
2017-11-14 00:07:06 +00:00
|
|
|
result = result.concat(diffs);
|
|
|
|
});
|
|
|
|
|
|
|
|
return result;
|
2015-01-28 00:00:00 +00:00
|
|
|
}
|
2017-04-18 19:51:23 +00:00
|
|
|
|
2017-04-18 22:55:59 +00:00
|
|
|
getEffectsDescription(): string {
|
|
|
|
if (this.effects.length == 0) {
|
|
|
|
return "";
|
|
|
|
}
|
|
|
|
|
2017-10-02 16:02:30 +00:00
|
|
|
let info: string[] = [];
|
|
|
|
if (this.power) {
|
2017-12-08 00:18:15 +00:00
|
|
|
info.push(`power ${this.power}`);
|
2017-10-02 16:02:30 +00:00
|
|
|
}
|
|
|
|
if (this.range) {
|
2017-12-08 00:18:15 +00:00
|
|
|
info.push(`range ${this.range}km`);
|
|
|
|
}
|
|
|
|
if (this.aim) {
|
|
|
|
info.push(`aim +${this.aim}%`);
|
|
|
|
}
|
|
|
|
if (this.evasion) {
|
|
|
|
info.push(`evasion -${this.evasion}%`);
|
|
|
|
}
|
|
|
|
if (this.luck) {
|
|
|
|
info.push(`luck ±${this.luck}%`);
|
2017-10-02 16:02:30 +00:00
|
|
|
}
|
|
|
|
|
2017-12-08 00:18:15 +00:00
|
|
|
let desc = (info.length) ? `${this.getVerb()} (${info.join(", ")})` : this.getVerb();
|
2017-04-18 22:55:59 +00:00
|
|
|
let effects = this.effects.map(effect => {
|
2017-10-03 16:11:30 +00:00
|
|
|
let suffix: string;
|
|
|
|
if (this.blast) {
|
|
|
|
suffix = `in ${this.blast}km radius`;
|
|
|
|
} else if (this.angle) {
|
|
|
|
suffix = `in ${this.angle}° arc`;
|
|
|
|
} else if (this.range) {
|
|
|
|
suffix = "on target";
|
|
|
|
} else {
|
|
|
|
suffix = "on self";
|
|
|
|
}
|
2017-05-10 17:48:28 +00:00
|
|
|
return "• " + effect.getDescription() + " " + suffix;
|
2017-04-18 19:51:23 +00:00
|
|
|
});
|
2017-04-18 22:55:59 +00:00
|
|
|
return `${desc}:\n${effects.join("\n")}`;
|
2017-04-18 19:51:23 +00:00
|
|
|
}
|
2015-01-28 00:00:00 +00:00
|
|
|
}
|
|
|
|
}
|