224 lines
8.2 KiB
TypeScript
224 lines
8.2 KiB
TypeScript
module TK.SpaceTac {
|
|
/**
|
|
* Error codes for approach simulation
|
|
*/
|
|
export enum ApproachSimulationError {
|
|
NO_MOVE_NEEDED,
|
|
NO_VECTOR_FOUND,
|
|
}
|
|
|
|
/**
|
|
* Status for simulation
|
|
*/
|
|
export enum MoveFireStatus {
|
|
OK,
|
|
NO_ACTION,
|
|
NOT_ENOUGH_POWER,
|
|
INVALID_TARGET,
|
|
IMPOSSIBLE_APPROACH,
|
|
}
|
|
|
|
/**
|
|
* A single action in the sequence result from the simulator
|
|
*/
|
|
export type MoveFirePart = {
|
|
action: BaseAction
|
|
target: Target
|
|
ap: number
|
|
possible: boolean
|
|
}
|
|
|
|
/**
|
|
* A simulation result
|
|
*/
|
|
export class MoveFireResult {
|
|
// Simulation status
|
|
status = MoveFireStatus.OK
|
|
// Ideal successive parts to make the full move+fire
|
|
parts: MoveFirePart[] = []
|
|
|
|
need_move = false
|
|
can_move = false
|
|
can_end_move = false
|
|
total_move_ap = 0
|
|
move_location = new Target(0, 0, null)
|
|
|
|
need_fire = false
|
|
can_fire = false
|
|
total_fire_ap = 0
|
|
fire_location = new Target(0, 0, null)
|
|
};
|
|
|
|
/**
|
|
* Utility to simulate a move+fire action.
|
|
*
|
|
* This is also a helper to bring a ship in range to fire a weapon.
|
|
*/
|
|
export class MoveFireSimulator {
|
|
constructor(
|
|
// Playing ship
|
|
private ship: Ship,
|
|
// Coordinates grid
|
|
private grid: ArenaGrid
|
|
) { }
|
|
|
|
/**
|
|
* Find an engine action, to make an approach
|
|
*
|
|
* This will return the first available engine, in the definition order
|
|
*/
|
|
findEngine(): MoveAction | null {
|
|
let actions = cfilter(this.ship.actions.listAll(), MoveAction);
|
|
return first(actions, action => this.ship.actions.getCooldown(action).canUse());
|
|
}
|
|
|
|
/**
|
|
* Get an iterator for scanning a circle
|
|
*/
|
|
scanCircle(x: number, y: number, radius: number, nr = 6, na = 30): Iterator<Target> {
|
|
// TODO If there is a grid, produce only grid coordinates
|
|
let rcount = nr ? 1 / (nr - 1) : 0;
|
|
return ichainit(imap(istep(0, irepeat(rcount, nr - 1)), r => {
|
|
let angles = Math.max(1, Math.ceil(na * r));
|
|
return imap(istep(0, irepeat(2 * Math.PI / angles, angles - 1)), a => {
|
|
return new Target(x + r * radius * Math.cos(a), y + r * radius * Math.sin(a)).snap(this.grid);
|
|
});
|
|
}));
|
|
}
|
|
|
|
/**
|
|
* Find the best approach location, to put a target in a given range.
|
|
*
|
|
* Return null if no approach vector was found.
|
|
*/
|
|
getApproach(action: MoveAction, target: Target, radius: number, margin = 0): Target | ApproachSimulationError {
|
|
let dx = target.x - this.ship.arena_x;
|
|
let dy = target.y - this.ship.arena_y;
|
|
let distance = Math.sqrt(dx * dx + dy * dy);
|
|
|
|
if (distance <= radius) {
|
|
return ApproachSimulationError.NO_MOVE_NEEDED;
|
|
} else {
|
|
if (margin && radius > margin) {
|
|
radius -= margin;
|
|
}
|
|
|
|
let candidates: [number, Target][] = [];
|
|
iforeach(this.scanCircle(target.x, target.y, radius), candidate => {
|
|
if (action.checkTarget(this.ship, candidate)) {
|
|
candidates.push([candidate.getDistanceTo(this.ship.location), candidate]);
|
|
}
|
|
});
|
|
|
|
if (candidates.length) {
|
|
return minBy(candidates, ([distance, _]) => distance)[1];
|
|
} else {
|
|
return ApproachSimulationError.NO_VECTOR_FOUND;
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Simulate a given action on a given valid target.
|
|
*/
|
|
simulateAction(action: BaseAction, target: Target, move_margin = 0): MoveFireResult {
|
|
let result = new MoveFireResult();
|
|
let ap = this.ship.getValue("power");
|
|
|
|
// Move or approach needed ?
|
|
let move_target: Target | null = null;
|
|
let move_action: MoveAction | null = null;
|
|
result.move_location = Target.newFromShip(this.ship);
|
|
if (action instanceof MoveAction) {
|
|
result.need_move = true;
|
|
move_target = target;
|
|
move_action = action;
|
|
} else {
|
|
move_action = this.findEngine();
|
|
if (move_action) {
|
|
let approach_radius = action.getRangeRadius(this.ship);
|
|
let approach = this.getApproach(move_action, target, approach_radius, move_margin);
|
|
if (approach instanceof Target) {
|
|
result.need_move = true;
|
|
move_target = approach;
|
|
} else if (approach != ApproachSimulationError.NO_MOVE_NEEDED) {
|
|
result.need_move = true;
|
|
result.can_move = false;
|
|
result.status = MoveFireStatus.IMPOSSIBLE_APPROACH;
|
|
return result;
|
|
}
|
|
}
|
|
}
|
|
if (move_target && move_action && result.need_move) {
|
|
if (arenaDistance(move_target, this.ship.location) < 1e-8) {
|
|
result.need_move = false;
|
|
} else {
|
|
result.can_move = bool(move_action.checkTarget(this.ship, move_target));
|
|
if (!result.can_move) {
|
|
result.status = MoveFireStatus.INVALID_TARGET;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Check move AP
|
|
if (result.need_move && move_target && move_action) {
|
|
result.total_move_ap = move_action.getPowerUsage(this.ship, move_target);
|
|
result.can_move = result.can_move && (ap > 0);
|
|
result.can_end_move = result.can_move && (result.total_move_ap <= ap);
|
|
result.move_location = move_target;
|
|
// TODO Split in "this turn" part and "next turn" part if needed
|
|
result.parts.push({ action: move_action, target: move_target, ap: result.total_move_ap, possible: result.can_move });
|
|
|
|
if (!result.can_end_move) {
|
|
result.status = MoveFireStatus.NOT_ENOUGH_POWER;
|
|
}
|
|
|
|
ap -= result.total_move_ap;
|
|
}
|
|
|
|
// Check action AP
|
|
if (!(action instanceof MoveAction)) {
|
|
result.need_fire = true;
|
|
result.total_fire_ap = action.getPowerUsage(this.ship, target);
|
|
if (action.checkTarget(this.ship, target, result.move_location)) {
|
|
if (result.total_fire_ap <= ap) {
|
|
result.can_fire = true;
|
|
} else {
|
|
result.status = MoveFireStatus.NOT_ENOUGH_POWER;
|
|
}
|
|
} else {
|
|
result.status = MoveFireStatus.INVALID_TARGET;
|
|
}
|
|
result.fire_location = target;
|
|
result.parts.push({ action: action, target: target, ap: result.total_fire_ap, possible: (!result.need_move || result.can_end_move) && result.can_fire });
|
|
}
|
|
|
|
if (!result.need_move && !result.need_fire) {
|
|
result.status = MoveFireStatus.NO_ACTION;
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
/**
|
|
* Apply a move-fire simulation result, and predict the diffs it will apply on a battle
|
|
*
|
|
* The original battle passed as parameter will be duplicated, and not altered
|
|
*/
|
|
getExpectedDiffs(battle: Battle, simulation: MoveFireResult): BaseBattleDiff[] {
|
|
let sim_battle = duplicate(battle, TK.SpaceTac);
|
|
let sim_ship = nn(sim_battle.getShip(this.ship.id));
|
|
let results: BaseBattleDiff[] = [];
|
|
simulation.parts.forEach(part => {
|
|
let diffs = part.action.getDiffs(sim_ship, battle, part.target);
|
|
results = results.concat(diffs);
|
|
sim_battle.applyDiffs(diffs);
|
|
|
|
diffs = sim_battle.performChecks();
|
|
results = results.concat(diffs);
|
|
});
|
|
return results;
|
|
}
|
|
}
|
|
}
|