1
0
Fork 0
This commit is contained in:
Michaël Lemaire 2018-07-09 12:28:18 +02:00
parent b4302da855
commit bb67458180
15 changed files with 81 additions and 121 deletions

View file

@ -49,6 +49,7 @@ Battle
* Toggle bar/text display in power section of action bar * Toggle bar/text display in power section of action bar
* Show a cooldown indicator on move action icon, if the simulation would cause the engine to overheat * Show a cooldown indicator on move action icon, if the simulation would cause the engine to overheat
* [WIP] Add an hexagonal grid * [WIP] Add an hexagonal grid
* Use the grid for "border" exclusion areas
* Use distances in units of this grid (add a multiplier to be compatible with no grid mode) * Use distances in units of this grid (add a multiplier to be compatible with no grid mode)
* Fix repel effect to snap to grid (beware of two ships being moved to the same location!) * Fix repel effect to snap to grid (beware of two ships being moved to the same location!)
* Fix engine targetting producing out-of-grid coordinates because of exclusion areas * Fix engine targetting producing out-of-grid coordinates because of exclusion areas

View file

@ -5,6 +5,22 @@ module TK.SpaceTac.Specs {
check.nears(arenaAngle({ x: 0, y: 0 }, { x: 1, y: 1 }), Math.PI / 4); check.nears(arenaAngle({ x: 0, y: 0 }, { x: 1, y: 1 }), Math.PI / 4);
}) })
test.case("checks if a location is in range of another", check => {
check.in("first order", check => {
check.equals(arenaInRange({ x: 0, y: 0 }, { x: 4, y: 4 }, 5), false, "<5");
check.equals(arenaInRange({ x: 0, y: 0 }, { x: 4, y: 4 }, 8), true, "<8");
});
check.in("second order", check => {
check.equals(arenaInRange({ x: 4, y: 4 }, { x: 0, y: 0 }, 5), false, "<5");
check.equals(arenaInRange({ x: 4, y: 4 }, { x: 0, y: 0 }, 8), true, "<8");
});
check.equals(arenaInRange({ x: 0, y: 0 }, { x: 0.99999999999999, y: 0 }, 1), true, "0.99999999999999");
check.equals(arenaInRange({ x: 0, y: 0 }, { x: 1.00000000000001, y: 0 }, 1), true, "1.00000000000001");
check.equals(arenaInRange({ x: 0, y: 0 }, { x: 1.000001, y: 0 }, 1), false, "1.000001");
})
test.case("computes an angular difference", check => { test.case("computes an angular difference", check => {
check.equals(angularDifference(0.5, 1.5), 1.0); check.equals(angularDifference(0.5, 1.5), 1.0);
check.nears(angularDifference(0.5, 1.5 + Math.PI * 6), 1.0); check.nears(angularDifference(0.5, 1.5 + Math.PI * 6), 1.0);

View file

@ -75,6 +75,13 @@ module TK.SpaceTac {
return Math.sqrt(dx * dx + dy * dy); return Math.sqrt(dx * dx + dy * dy);
} }
/**
* Check if a location is in range of another (accounting for rounding errors)
*/
export function arenaInRange(loc1: IArenaLocation, loc2: IArenaLocation, range: number): boolean {
return arenaDistance(loc1, loc2) - range < 1e-8;
}
/** /**
* Check if a location is inside an area * Check if a location is inside an area
*/ */

View file

@ -63,14 +63,6 @@ module TK.SpaceTac {
return first(actions, action => this.ship.actions.getCooldown(action).canUse()); return first(actions, action => this.ship.actions.getCooldown(action).canUse());
} }
/**
* Check that a move action can reach a given destination
*/
canMoveTo(action: MoveAction, target: Target): boolean {
let checked = action.checkLocationTarget(this.ship, target);
return checked != null && checked.x == target.x && checked.y == target.y;
}
/** /**
* Get an iterator for scanning a circle * Get an iterator for scanning a circle
*/ */
@ -104,7 +96,7 @@ module TK.SpaceTac {
let candidates: [number, Target][] = []; let candidates: [number, Target][] = [];
iforeach(this.scanCircle(target.x, target.y, radius), candidate => { iforeach(this.scanCircle(target.x, target.y, radius), candidate => {
if (this.canMoveTo(action, candidate)) { if (action.checkLocationTarget(this.ship, candidate)) {
candidates.push([candidate.getDistanceTo(this.ship.location), candidate]); candidates.push([candidate.getDistanceTo(this.ship.location), candidate]);
} }
}); });

View file

@ -110,10 +110,7 @@ module TK.SpaceTac {
// Check if a target is in range from a specific point // Check if a target is in range from a specific point
isInRange(x: number, y: number, radius: number): boolean { isInRange(x: number, y: number, radius: number): boolean {
var dx = this.x - x; return arenaInRange(this, new ArenaLocation(x, y), radius);
var dy = this.y - y;
var length = Math.sqrt(dx * dx + dy * dy);
return (length <= radius);
} }
// Constraint a target, to be in a given range from a specific point // Constraint a target, to be in a given range from a specific point

View file

@ -17,6 +17,7 @@ module TK.SpaceTac {
var battle = new Battle(fleet1, fleet2); var battle = new Battle(fleet1, fleet2);
if (!hexgrid) { if (!hexgrid) {
battle.grid = new PixelGrid(); battle.grid = new PixelGrid();
battle.placeShips();
} }
battle.ships.list().forEach(ship => TestTools.setShipModel(ship, 1, 0)); battle.ships.list().forEach(ship => TestTools.setShipModel(ship, 1, 0));
battle.play_order = fleet1.ships.concat(fleet2.ships); battle.play_order = fleet1.ships.concat(fleet2.ships);

View file

@ -236,9 +236,9 @@ module TK.SpaceTac {
* *
* Will call checkLocationTarget or checkShipTarget by default * Will call checkLocationTarget or checkShipTarget by default
*/ */
checkTarget(ship: Ship, target: Target): Target | null { checkTarget(ship: Ship, target: Target): boolean {
if (this.checkCannotBeApplied(ship)) { if (this.checkCannotBeApplied(ship)) {
return null; return false;
} else { } else {
if (target.isShip()) { if (target.isShip()) {
return this.checkShipTarget(ship, target); return this.checkShipTarget(ship, target);
@ -248,16 +248,18 @@ module TK.SpaceTac {
} }
} }
// Method to reimplement to check if a space target is suitable /**
// Must return null if the target can't be applied, an altered target, or the original target * Method to reimplement to check if a space target is suitable
protected checkLocationTarget(ship: Ship, target: Target): Target | null { */
return null; protected checkLocationTarget(ship: Ship, target: Target): boolean {
return false;
} }
// Method to reimplement to check if a ship target is suitable /**
// Must return null if the target can't be applied, an altered target, or the original target * Method to reimplement to check if a ship target is suitable
protected checkShipTarget(ship: Ship, target: Target): Target | null { */
return null; protected checkShipTarget(ship: Ship, target: Target): boolean {
return false;
} }
/** /**
@ -302,19 +304,18 @@ module TK.SpaceTac {
return false; return false;
} }
let checked_target = this.checkTarget(ship, target); if (!this.checkTarget(ship, target)) {
if (!checked_target) {
console.warn("Action rejected - invalid target", ship, this, target); console.warn("Action rejected - invalid target", ship, this, target);
return false; return false;
} }
let cost = this.getPowerUsage(ship, checked_target); let cost = this.getPowerUsage(ship, target);
if (ship.getValue("power") < cost) { if (ship.getValue("power") < cost) {
console.warn("Action rejected - not enough power", ship, this, checked_target); console.warn("Action rejected - not enough power", ship, this, target);
return false; return false;
} }
let diffs = this.getDiffs(ship, battle, checked_target); let diffs = this.getDiffs(ship, battle, target);
if (diffs.length) { if (diffs.length) {
battle.applyDiffs(diffs); battle.applyDiffs(diffs);
return true; return true;

View file

@ -18,12 +18,13 @@ module TK.SpaceTac.Specs {
let action = new DeployDroneAction("testdrone", { power: 0 }, { deploy_distance: 8 }); let action = new DeployDroneAction("testdrone", { power: 0 }, { deploy_distance: 8 });
ship.actions.addCustom(action); ship.actions.addCustom(action);
check.equals(action.checkTarget(ship, new Target(8, 0, null)), new Target(8, 0, null)); check.equals(action.checkTarget(ship, new Target(8, 0, null)), true);
check.equals(action.checkTarget(ship, new Target(12, 0, null)), new Target(8, 0, null)); check.equals(action.checkTarget(ship, new Target(12, 0, null)), false);
check.equals(action.checkTarget(ship, Target.newFromShip(ship)), false);
let other = new Ship(); let other = new Ship();
other.setArenaPosition(8, 0); other.setArenaPosition(8, 0);
check.equals(action.checkTarget(ship, new Target(8, 0, other)), null); check.equals(action.checkTarget(ship, new Target(8, 0, other)), false);
}); });
test.case("deploys a new drone", check => { test.case("deploys a new drone", check => {

View file

@ -67,9 +67,12 @@ module TK.SpaceTac {
return result; return result;
} }
checkLocationTarget(ship: Ship, target: Target): Target { checkShipTarget(ship: Ship, target: Target): boolean {
target = target.constraintInRange(ship.arena_x, ship.arena_y, this.deploy_distance); return false;
return target; }
checkLocationTarget(ship: Ship, target: Target): boolean {
return arenaInRange(ship.location, target, this.deploy_distance);
} }
getSpecificDiffs(ship: Ship, battle: Battle, target: Target): BaseBattleDiff[] { getSpecificDiffs(ship: Ship, battle: Battle, target: Target): BaseBattleDiff[] {

View file

@ -59,8 +59,8 @@ module TK.SpaceTac {
} }
} }
protected checkShipTarget(ship: Ship, target: Target): Target | null { protected checkShipTarget(ship: Ship, target: Target): boolean {
return ship.is(target.ship_id) ? target : null; return ship.is(target.ship_id);
} }
getTargettingMode(ship: Ship): ActionTargettingMode { getTargettingMode(ship: Ship): ActionTargettingMode {

View file

@ -3,6 +3,7 @@ module TK.SpaceTac.Specs {
test.case("checks movement against remaining AP", check => { test.case("checks movement against remaining AP", check => {
var ship = new Ship(); var ship = new Ship();
var battle = new Battle(ship.fleet); var battle = new Battle(ship.fleet);
battle.grid = new PixelGrid();
TestTools.setShipPlaying(battle, ship); TestTools.setShipPlaying(battle, ship);
ship.setValue("power", 6); ship.setValue("power", 6);
ship.arena_x = 0; ship.arena_x = 0;
@ -11,14 +12,13 @@ module TK.SpaceTac.Specs {
ship.actions.addCustom(action); ship.actions.addCustom(action);
var result = action.checkTarget(ship, Target.newFromLocation(0, 20)); var result = action.checkTarget(ship, Target.newFromLocation(0, 20));
check.equals(result, Target.newFromLocation(0, 20)); check.equals(result, true);
result = action.checkTarget(ship, Target.newFromLocation(0, 80)); result = action.checkTarget(ship, Target.newFromLocation(0, 80));
check.nears(nn(result).y, 59.9); check.equals(result, false);
ship.setValue("power", 0); result = action.checkTarget(ship, Target.newFromLocation(0, 0));
result = action.checkTarget(ship, Target.newFromLocation(0, 80)); check.equals(result, false);
check.equals(result, null);
}); });
test.case("forbids targetting a ship", check => { test.case("forbids targetting a ship", check => {
@ -37,11 +37,11 @@ module TK.SpaceTac.Specs {
test.case("applies and reverts", check => { test.case("applies and reverts", check => {
let battle = TestTools.createBattle(); let battle = TestTools.createBattle();
let ship = battle.play_order[0]; let ship = battle.play_order[0];
ship.setArenaPosition(500, 600) ship.setArenaPosition(500, 600);
TestTools.setShipModel(ship, 100, 0, 20); TestTools.setShipModel(ship, 100, 0, 20);
ship.setValue("power", 5); ship.setValue("power", 5);
let action = new MoveAction("Engine", { distance_per_power: 1 }); let action = new MoveAction("Engine", { distance_per_power: 4 });
ship.actions.addCustom(action); ship.actions.addCustom(action);
TestTools.actionChain(check, battle, [ TestTools.actionChain(check, battle, [
@ -53,9 +53,9 @@ module TK.SpaceTac.Specs {
check.equals(ship.getValue("power"), 5, "power"); check.equals(ship.getValue("power"), 5, "power");
}, },
check => { check => {
check.nears(ship.arena_x, 504.382693, 5, "ship X"); check.equals(ship.arena_x, 510, "ship X");
check.nears(ship.arena_y, 602.191346, 5, "ship Y"); check.equals(ship.arena_y, 605, "ship Y");
check.equals(ship.getValue("power"), 0, "power"); check.equals(ship.getValue("power"), 2, "power");
} }
]); ]);
}); });
@ -71,51 +71,19 @@ module TK.SpaceTac.Specs {
var action = new MoveAction("Engine", { distance_per_power: 1000, safety_distance: 200 }); var action = new MoveAction("Engine", { distance_per_power: 1000, safety_distance: 200 });
var result = action.checkLocationTarget(ship, Target.newFromLocation(700, 500)); var result = action.checkLocationTarget(ship, Target.newFromLocation(700, 500));
check.equals(result, Target.newFromLocation(700, 500)); check.equals(result, true);
result = action.checkLocationTarget(ship, Target.newFromLocation(800, 500)); result = action.checkLocationTarget(ship, Target.newFromLocation(800, 500));
check.equals(result, Target.newFromLocation(800, 500)); check.equals(result, true);
result = action.checkLocationTarget(ship, Target.newFromLocation(900, 500)); result = action.checkLocationTarget(ship, Target.newFromLocation(900, 500));
check.equals(result, Target.newFromLocation(800, 500)); check.equals(result, false);
result = action.checkLocationTarget(ship, Target.newFromLocation(1000, 500)); result = action.checkLocationTarget(ship, Target.newFromLocation(1000, 500));
check.equals(result, Target.newFromLocation(800, 500)); check.equals(result, false);
result = action.checkLocationTarget(ship, Target.newFromLocation(1200, 500)); result = action.checkLocationTarget(ship, Target.newFromLocation(1200, 500));
check.equals(result, Target.newFromLocation(1200, 500)); check.equals(result, true);
});
test.case("exclusion radius is applied correctly over two ships", check => {
var battle = TestTools.createBattle(1, 2);
var ship = battle.fleets[0].ships[0];
var enemy1 = battle.fleets[1].ships[0];
var enemy2 = battle.fleets[1].ships[1];
TestTools.setShipModel(ship, 100, 0, 100);
enemy1.setArenaPosition(0, 800);
enemy2.setArenaPosition(0, 1000);
var action = new MoveAction("Engine", { distance_per_power: 1000, safety_distance: 150 });
var result = action.checkLocationTarget(ship, Target.newFromLocation(0, 1100));
check.equals(result, Target.newFromLocation(0, 650));
});
test.case("exclusion radius does not make the ship go back", check => {
var battle = TestTools.createBattle(1, 2);
var ship = battle.fleets[0].ships[0];
var enemy1 = battle.fleets[1].ships[0];
var enemy2 = battle.fleets[1].ships[1];
TestTools.setShipModel(ship, 100, 0, 100);
enemy1.setArenaPosition(0, 500);
enemy2.setArenaPosition(0, 800);
var action = new MoveAction("Engine", { distance_per_power: 1000, safety_distance: 600 });
let result = action.checkLocationTarget(ship, Target.newFromLocation(0, 1000));
check.equals(result, null);
result = action.checkLocationTarget(ship, Target.newFromLocation(0, 1400));
check.equals(result, Target.newFromLocation(0, 1400));
}); });
test.case("builds a textual description", check => { test.case("builds a textual description", check => {

View file

@ -124,9 +124,9 @@ module TK.SpaceTac {
return target.constraintInRange(ship.arena_x, ship.arena_y, max_distance); return target.constraintInRange(ship.arena_x, ship.arena_y, max_distance);
} }
checkLocationTarget(ship: Ship, target: Target): Target | null { checkLocationTarget(ship: Ship, target: Target): boolean {
let fixed_target = this.applyExclusion(ship, this.applyReachableRange(ship, target)); let fixed_target = this.applyExclusion(ship, this.applyReachableRange(ship, target));
return fixed_target.getDistanceTo(target) < 1e-8 ? target : null; return fixed_target.getDistanceTo(target) < 1e-8;
} }
protected getSpecificDiffs(ship: Ship, battle: Battle, target: Target): BaseBattleDiff[] { protected getSpecificDiffs(ship: Ship, battle: Battle, target: Target): BaseBattleDiff[] {

View file

@ -67,8 +67,8 @@ module TK.SpaceTac {
return result; return result;
} }
checkShipTarget(ship: Ship, target: Target): Target | null { checkShipTarget(ship: Ship, target: Target): boolean {
return ship.is(target.ship_id) ? target : null; return ship.is(target.ship_id);
} }
getSpecificDiffs(ship: Ship, battle: Battle, target: Target, apply_effects = true): BaseBattleDiff[] { getSpecificDiffs(ship: Ship, battle: Battle, target: Target, apply_effects = true): BaseBattleDiff[] {

View file

@ -38,30 +38,6 @@ module TK.SpaceTac.Specs {
]); ]);
}) })
test.case("transforms ship target in location target, when the weapon has blast radius", check => {
let ship1 = new Ship();
ship1.setArenaPosition(50, 10);
let ship2 = new Ship();
ship2.setArenaPosition(150, 10);
let action = TestTools.addWeapon(ship1, 1, 0, 100, 30);
let target = action.checkTarget(ship1, new Target(150, 10));
check.equals(target, new Target(150, 10));
target = action.checkTarget(ship1, Target.newFromShip(ship2));
check.equals(target, new Target(150, 10));
ship1.setArenaPosition(30, 10);
target = action.checkTarget(ship1, Target.newFromShip(ship2));
check.equals(target, new Target(130, 10));
ship1.setArenaPosition(0, 10);
target = action.checkTarget(ship1, Target.newFromShip(ship2));
check.equals(target, new Target(100, 10));
})
test.case("lists impacted ships", check => { test.case("lists impacted ships", check => {
let ship1 = new Ship(null, "S1"); let ship1 = new Ship(null, "S1");
ship1.setArenaPosition(10, 50); ship1.setArenaPosition(10, 50);
@ -104,7 +80,7 @@ module TK.SpaceTac.Specs {
let battle = TestTools.createBattle(); let battle = TestTools.createBattle();
let ship = battle.play_order[0]; let ship = battle.play_order[0];
let action = TestTools.addWeapon(ship, 1, 0, 100, 30); let action = TestTools.addWeapon(ship, 1, 0, 100, 30);
check.patch(action, "checkTarget", (ship: Ship, target: Target) => target); check.patch(action, "checkTarget", (ship: Ship, target: Target) => true);
check.equals(ship.arena_angle, 0); check.equals(ship.arena_angle, 0);
let result = action.apply(battle, ship, Target.newFromLocation(10, 20)); let result = action.apply(battle, ship, Target.newFromLocation(10, 20));

View file

@ -124,27 +124,24 @@ module TK.SpaceTac {
} }
} }
checkLocationTarget(ship: Ship, target: Target): Target | null { checkLocationTarget(ship: Ship, target: Target): boolean {
if (target && (this.blast > 0 || this.angle > 0)) { if (target && (this.blast > 0 || this.angle > 0)) {
target = target.constraintInRange(ship.arena_x, ship.arena_y, this.range); return arenaInRange(ship.location, target, this.range);
return target;
} else { } else {
return null; return false;
} }
} }
checkShipTarget(ship: Ship, target: Target): Target | null { checkShipTarget(ship: Ship, target: Target): boolean {
if (this.range > 0 && ship.is(target.ship_id)) { if (this.range > 0 && ship.is(target.ship_id)) {
// No self fire // No self fire
return null; return false;
} else { } else {
// Check if target is in range // Check if target is in range
if (this.blast > 0 || this.angle > 0) { if (this.blast > 0 || this.angle > 0) {
return this.checkLocationTarget(ship, new Target(target.x, target.y)); return this.checkLocationTarget(ship, new Target(target.x, target.y));
} else if (target.isInRange(ship.arena_x, ship.arena_y, this.range)) {
return target;
} else { } else {
return null; return arenaInRange(ship.location, target, this.range);
} }
} }
} }