1
0
Fork 0
This commit is contained in:
Michaël Lemaire 2018-07-09 16:04:40 +02:00
parent 7079001885
commit c608ac2a08
17 changed files with 15 additions and 440 deletions

View File

@ -1,43 +0,0 @@
module TK.SpaceTac.Specs {
testing("ExclusionAreas", test => {
test.case("constructs from a ship or battle", check => {
let battle = new Battle();
battle.border = 17;
battle.ship_separation = 31;
let ship1 = battle.fleets[0].addShip();
ship1.setArenaPosition(12, 5);
let ship2 = battle.fleets[1].addShip();
ship2.setArenaPosition(43, 89);
let exclusion = ExclusionAreas.fromBattle(battle);
check.equals(exclusion.hard_border, 17);
check.equals(exclusion.effective_obstacle, 31);
check.equals(exclusion.obstacles, [new ArenaLocationAngle(12, 5), new ArenaLocationAngle(43, 89)]);
exclusion = ExclusionAreas.fromBattle(battle, [ship1], 120);
check.equals(exclusion.hard_border, 17);
check.equals(exclusion.effective_obstacle, 120);
check.equals(exclusion.obstacles, [new ArenaLocationAngle(43, 89)]);
exclusion = ExclusionAreas.fromBattle(battle, [ship2], 10);
check.equals(exclusion.hard_border, 17);
check.equals(exclusion.effective_obstacle, 31);
check.equals(exclusion.obstacles, [new ArenaLocationAngle(12, 5)]);
exclusion = ExclusionAreas.fromShip(ship1);
check.equals(exclusion.hard_border, 17);
check.equals(exclusion.effective_obstacle, 31);
check.equals(exclusion.obstacles, [new ArenaLocationAngle(43, 89)]);
exclusion = ExclusionAreas.fromShip(ship2, 99);
check.equals(exclusion.hard_border, 17);
check.equals(exclusion.effective_obstacle, 99);
check.equals(exclusion.obstacles, [new ArenaLocationAngle(12, 5)]);
exclusion = ExclusionAreas.fromShip(ship2, 10, false);
check.equals(exclusion.hard_border, 17);
check.equals(exclusion.effective_obstacle, 31);
check.equals(exclusion.obstacles, [new ArenaLocationAngle(12, 5), new ArenaLocationAngle(43, 89)]);
})
})
}

View File

@ -1,96 +0,0 @@
module TK.SpaceTac {
/**
* Helper for working with exclusion areas (areas where a ship cannot go)
*
* There are three types of exclusion:
* - Hard border exclusion, that prevents a ship from being too close to the battle edges
* - Hard obstacle exclusion, that prevents two ships from being too close to each other
* - Soft obstacle exclusion, usually associated with an engine, that prevents a ship from moving too close to others
*/
export class ExclusionAreas {
xmin: number
xmax: number
ymin: number
ymax: number
active: boolean
hard_border = 50
hard_obstacle = 100
effective_obstacle = this.hard_obstacle
obstacles: ArenaLocation[] = []
constructor(width: number, height: number) {
this.xmin = 0;
this.xmax = width - 1;
this.ymin = 0;
this.ymax = height - 1;
this.active = width > 0 && height > 0;
}
/**
* Build an exclusion helper from a battle.
*/
static fromBattle(battle: Battle, ignore_ships: Ship[] = [], soft_distance = 0): ExclusionAreas {
let result = new ExclusionAreas(battle.width, battle.height);
result.hard_border = battle.border;
result.hard_obstacle = battle.ship_separation;
let obstacles = imap(ifilter(battle.iships(true), ship => !contains(ignore_ships, ship)), ship => ship.location);
result.configure(imaterialize(obstacles), soft_distance);
return result;
}
/**
* Build an exclusion helper for a ship.
*
* If *ignore_self* is True, the ship will itself not be included in exclusion areas.
*/
static fromShip(ship: Ship, soft_distance = 0, ignore_self = true): ExclusionAreas {
let battle = ship.getBattle();
if (battle) {
return ExclusionAreas.fromBattle(battle, ignore_self ? [ship] : [], soft_distance);
} else {
return new ExclusionAreas(0, 0);
}
}
/**
* Configure the areas for next check calls.
*/
configure(obstacles: ArenaLocation[], soft_distance: number) {
this.obstacles = obstacles;
this.effective_obstacle = Math.max(soft_distance, this.hard_obstacle);
}
/**
* Keep a location outside exclusion areas, when coming from a source.
*
* It will return the furthest location on the [source, location] segment, that is not inside an exclusion
* area.
*/
stopBefore(location: ArenaLocation, source: ArenaLocation): ArenaLocation {
if (!this.active) {
return location;
}
let target = Target.newFromLocation(location.x, location.y);
// Keep out of arena borders
target = target.keepInsideRectangle(this.xmin + this.hard_border, this.ymin + this.hard_border,
this.xmax - this.hard_border, this.ymax - this.hard_border,
source.x, source.y);
// Apply collision prevention
let obstacles = sorted(this.obstacles, (a, b) => cmp(arenaDistance(a, source), arenaDistance(b, source), true));
obstacles.forEach(s => {
let new_target = target.moveOutOfCircle(s.x, s.y, this.effective_obstacle, source.x, source.y);
if (target != new_target && arenaDistance(s, source) < this.effective_obstacle) {
// Already inside the nearest ship's exclusion area
target = Target.newFromLocation(source.x, source.y);
} else {
target = new_target;
}
});
return new ArenaLocation(target.x, target.y);
}
}
}

View File

@ -35,28 +35,5 @@ module TK.SpaceTac.Specs {
check.equals(t1.isInRange(7, 3, 3), true);
check.equals(t1.isInRange(5, 5, 2), true);
});
test.case("constraints a target to a limited range", check => {
var target = Target.newFromLocation(5, 9);
check.equals(target.constraintInRange(1, 1, Math.sqrt(80) * 0.5), Target.newFromLocation(3, 5));
check.same(target.constraintInRange(1, 1, 70), target);
});
test.case("pushes a target out of a given circle", check => {
var target = Target.newFromLocation(5, 5);
check.same(target.moveOutOfCircle(0, 0, 3, 0, 0), target);
check.equals(target.moveOutOfCircle(6, 6, 3, 0, 0), Target.newFromLocation(3.8786796564403576, 3.8786796564403576));
check.equals(target.moveOutOfCircle(4, 4, 3, 10, 10), Target.newFromLocation(6.121320343559642, 6.121320343559642));
check.equals(target.moveOutOfCircle(5, 8, 6, 5, 0), Target.newFromLocation(5, 2));
check.equals(target.moveOutOfCircle(5, 2, 6, 5, 10), Target.newFromLocation(5, 8));
check.equals(target.moveOutOfCircle(8, 5, 6, 0, 5), Target.newFromLocation(2, 5));
check.equals(target.moveOutOfCircle(2, 5, 6, 10, 5), Target.newFromLocation(8, 5));
});
test.case("keeps a target inside a rectangle", check => {
var target = Target.newFromLocation(5, 5);
check.same(target.keepInsideRectangle(0, 0, 10, 10, 0, 0), target);
check.equals(target.keepInsideRectangle(8, 0, 13, 10, 10, 5), Target.newFromLocation(8, 5));
});
});
}

View File

@ -1,34 +1,4 @@
module TK.SpaceTac {
// Find the nearest intersection between a line and a circle
// Circle is supposed to be centered at (0,0)
// Nearest intersection to (x1,y1) is returned
function intersectLineCircle(x1: number, y1: number, x2: number, y2: number, r: number): [number, number] | null {
let a = y2 - y1;
let b = -(x2 - x1);
let c = -(a * x1 + b * y1);
let x0 = -a * c / (a * a + b * b), y0 = -b * c / (a * a + b * b);
let EPS = 10e-8;
if (c * c > r * r * (a * a + b * b) + EPS) {
return null;
} else if (Math.abs(c * c - r * r * (a * a + b * b)) < EPS) {
return [x0, y0];
} else {
let d = r * r - c * c / (a * a + b * b);
let mult = Math.sqrt(d / (a * a + b * b));
let ax, ay, bx, by;
ax = x0 + b * mult;
bx = x0 - b * mult;
ay = y0 - a * mult;
by = y0 + a * mult;
let candidates: [number, number][] = [
[x0 + b * mult, y0 - a * mult],
[x0 - b * mult, y0 + a * mult]
]
return minBy(candidates, ([x, y]) => Math.sqrt((x - x1) * (x - x1) + (y - y1) * (y - y1)));
}
}
// Target for a capability
// This could be a location in space, or a ship
export class Target {
@ -114,72 +84,5 @@ module TK.SpaceTac {
isInRange(x: number, y: number, radius: number): boolean {
return arenaInRange(this, new ArenaLocation(x, y), radius);
}
// Constraint a target, to be in a given range from a specific point
// May return the original target if it's already in radius
constraintInRange(x: number, y: number, radius: number): Target {
var dx = this.x - x;
var dy = this.y - y;
var length = Math.sqrt(dx * dx + dy * dy);
if (length <= radius) {
return this;
} else {
var factor = radius / length;
return Target.newFromLocation(x + dx * factor, y + dy * factor);
}
}
// Force a target to stay out of a given circle
// If the target is in the circle, it will be moved to the nearest intersection between targetting line
// and the circle
// May return the original target if it's already out of the circle
moveOutOfCircle(circlex: number, circley: number, radius: number, sourcex: number, sourcey: number): Target {
var dx = this.x - circlex;
var dy = this.y - circley;
var length = Math.sqrt(dx * dx + dy * dy);
if (length >= radius) {
// Already out of circle
return this;
} else {
// Find nearest intersection with circle
var res = intersectLineCircle(sourcex - circlex, sourcey - circley, dx, dy, radius);
if (res) {
return Target.newFromLocation(res[0] + circlex, res[1] + circley);
} else {
return this;
}
}
}
/**
* Keep the target inside a rectangle
*
* May return the original target if it's already inside the rectangle
*/
keepInsideRectangle(xmin: number, ymin: number, xmax: number, ymax: number, sourcex: number, sourcey: number): Target {
let length = this.getDistanceTo({ x: sourcex, y: sourcey });
let result: Target = this;
if (result.x < xmin) {
let factor = (xmin - sourcex) / (result.x - sourcex);
length *= factor;
result = result.constraintInRange(sourcex, sourcey, length);
}
if (result.x > xmax) {
let factor = (xmax - sourcex) / (result.x - sourcex);
length *= factor;
result = result.constraintInRange(sourcex, sourcey, length);
}
if (result.y < ymin) {
let factor = (ymin - sourcey) / (result.y - sourcey);
length *= factor;
result = result.constraintInRange(sourcex, sourcey, length);
}
if (result.y > ymax) {
let factor = (ymax - sourcey) / (result.y - sourcey);
length *= factor;
result = result.constraintInRange(sourcex, sourcey, length);
}
return result;
}
}
}

View File

@ -87,11 +87,8 @@ module TK.SpaceTac.Specs {
});
test.case("builds a textual description", check => {
let action = new MoveAction("Engine", { distance_per_power: 58, safety_distance: 0 });
let action = new MoveAction("Engine", { distance_per_power: 58 });
check.equals(action.getEffectsDescription(), "Move: 58km per power point");
action = new MoveAction("Engine", { distance_per_power: 58, safety_distance: 12 });
check.equals(action.getEffectsDescription(), "Move: 58km per power point (safety: 12km)");
});
test.case("can't be used while in vigilance", check => {

View File

@ -3,10 +3,8 @@ module TK.SpaceTac {
* Configuration of a trigger action
*/
export interface MoveActionConfig {
// Distance allowed for each power point (raw, without applying maneuvrability)
// Distance allowed for each power point
distance_per_power: number
// Safety distance from other ships
safety_distance: number
}
/**
@ -14,8 +12,6 @@ module TK.SpaceTac {
*/
export class MoveAction extends BaseAction implements MoveActionConfig {
distance_per_power = 0
safety_distance = 120
maneuvrability_factor = 0
constructor(name = "Engine", config?: Partial<MoveActionConfig>, code = "move") {
super(name, code);
@ -91,42 +87,16 @@ module TK.SpaceTac {
}
/**
* Get the distance reachable with a given power
* Get the distance reachable with a given power
*/
getRangeRadiusForPower(ship: Ship, power = ship.getValue("power")): number {
return power * this.distance_per_power;
}
/**
* Get an exclusion helper for this move action
*/
getExclusionAreas(ship: Ship): ExclusionAreas {
return ExclusionAreas.fromShip(ship, this.safety_distance);
}
/**
* Apply exclusion areas (neer arena borders, or other ships)
*/
applyExclusion(ship: Ship, target: Target): Target {
let exclusion = this.getExclusionAreas(ship);
let destination = exclusion.stopBefore(new ArenaLocation(target.x, target.y), ship.location);
target = Target.newFromLocation(destination.x, destination.y);
return target;
}
/**
* Apply reachable range, with remaining power
*/
applyReachableRange(ship: Ship, target: Target, margin = 0.1): Target {
let max_distance = this.getRangeRadius(ship);
max_distance = Math.max(0, max_distance - margin);
return target.constraintInRange(ship.arena_x, ship.arena_y, max_distance);
}
checkLocationTarget(ship: Ship, target: Target): boolean {
let fixed_target = this.applyExclusion(ship, this.applyReachableRange(ship, target));
return fixed_target.getDistanceTo(target) < 1e-8;
// TODO Check it's on the grid
// TODO Check the space is not occupied
return true;
}
protected getSpecificDiffs(ship: Ship, battle: Battle, target: Target): BaseBattleDiff[] {
@ -137,11 +107,6 @@ module TK.SpaceTac {
getEffectsDescription(): string {
let result = `Move: ${this.distance_per_power}km per power point`;
if (this.safety_distance) {
result += ` (safety: ${this.safety_distance}km)`;
}
return result;
}
}

View File

@ -59,7 +59,7 @@ module TK.SpaceTac.Specs {
let ship2b = battle.fleets[1].addShip();
ship2b.setArenaPosition(1200, 0);
TestTools.setShipModel(ship2b, 10, 0, 5);
let engine = ship2b.actions.addCustom(new MoveAction("Move", { distance_per_power: 1000, safety_distance: 100 }));
let engine = ship2b.actions.addCustom(new MoveAction("Move", { distance_per_power: 1000 }));
let action = ship1a.actions.addCustom(new VigilanceAction("Reactive Shot", { radius: 1000, filter: ActionTargettingFilter.ENEMIES }, {
intruder_effects: [new DamageEffect(1)]

View File

@ -16,9 +16,11 @@ module TK.SpaceTac {
getOnDiffs(ship: Ship, source: Ship | Drone): BaseBattleDiff[] {
if (ship != source && !any(ship.getEffects(), effect => effect instanceof PinnedEffect && effect.hard)) {
let angle = arenaAngle(source.location, ship.location);
let destination = new ArenaLocation(ship.arena_x + Math.cos(angle) * this.value, ship.arena_y + Math.sin(angle) * this.value);
let exclusions = ExclusionAreas.fromShip(ship);
destination = exclusions.stopBefore(destination, ship.location);
let destination = new ArenaLocation(
ship.arena_x + Math.cos(angle) * this.value,
ship.arena_y + Math.sin(angle) * this.value
);
// TODO Snap to grid (what if space is already occupied ?)
// TODO Apply area effect adding/removal
return [
new ShipMoveDiff(ship, ship.location, new ArenaLocationAngle(destination.x, destination.y, ship.arena_angle))

View File

@ -13,7 +13,6 @@ module TK.SpaceTac {
getLevelUpgrades(level: number): ShipUpgrade[] {
let engine = new MoveAction("Engine", {
distance_per_power: 60,
safety_distance: 250,
});
engine.configureCooldown(1, 1);

View File

@ -14,7 +14,6 @@ module TK.SpaceTac {
if (level == 1) {
let engine = new MoveAction("Engine", {
distance_per_power: 460,
safety_distance: 100
});
engine.configureCooldown(2, 1);

View File

@ -14,7 +14,6 @@ module TK.SpaceTac {
if (level == 1) {
let engine = new MoveAction("Engine", {
distance_per_power: 310,
safety_distance: 160,
});
let missile = new TriggerAction("SubMunition Missile", {

View File

@ -30,7 +30,6 @@ module TK.SpaceTac {
let disengage = new MoveAction("Disengage", {
distance_per_power: 1000,
safety_distance: 200,
}, "ionthruster");
disengage.configureCooldown(1, 3);

View File

@ -11,9 +11,6 @@ module TK.SpaceTac.UI {
// Boundaries of the arena
private boundaries: IBounded = { x: 0, y: 0, width: 1808, height: 948 }
// Hint for weapon or move range
range_hint: RangeHint
// Input capture
private mouse_capture?: UIImage
@ -31,7 +28,6 @@ module TK.SpaceTac.UI {
// Layer for particles
container: UIContainer
layer_garbage: UIContainer
layer_hints: UIContainer
layer_drones: UIContainer
layer_ships: UIContainer
layer_weapon_effects: UIContainer
@ -46,7 +42,6 @@ module TK.SpaceTac.UI {
this.view = view;
this.playing = null;
this.hovered = null;
this.range_hint = new RangeHint(this);
let builder = new UIBuilder(view, container);
if (!container) {
@ -59,13 +54,11 @@ module TK.SpaceTac.UI {
this.setupMouseCapture();
this.layer_garbage = builder.container("garbage");
this.layer_hints = builder.container("hints");
this.layer_drones = builder.container("drones");
this.layer_ships = builder.container("ships");
this.layer_weapon_effects = builder.container("effects");
this.layer_targetting = builder.container("targetting");
this.range_hint.setLayer(this.layer_hints);
this.addShipSprites();
view.battle.drones.list().forEach(drone => this.addDrone(drone, 0));

View File

@ -122,7 +122,7 @@ module TK.SpaceTac.UI {
this.character_sheet.moveToLayer(this.layer_sheets);
// Targetting info
this.targetting = new Targetting(this, this.action_bar, this.toggle_tactical_mode, this.arena.range_hint);
this.targetting = new Targetting(this, this.action_bar, this.toggle_tactical_mode);
this.targetting.moveToLayer(this.arena.layer_targetting);
// BGM

View File

@ -1,77 +0,0 @@
module TK.SpaceTac.UI {
/**
* Graphical hints for movement and weapon range
*/
export class RangeHint {
// Link to the view
private view: BaseView
// Visual information
private info: UIGraphics
// Size of the arena
private width: number
private height: number
constructor(arena: Arena) {
this.view = arena.view;
let boundaries = arena.getBoundaries();
this.width = boundaries.width;
this.height = boundaries.height;
this.info = new UIGraphics(arena.view, "info", false);
}
/**
* Set the layer in which the info will be displayed
*/
setLayer(layer: UIContainer, x = 0, y = 0) {
this.info.setPosition(x, y);
layer.add(this.info);
}
/**
* Clear displayed information
*/
clear() {
this.info.clear();
this.info.visible = false;
}
/**
* Update displayed information
*/
update(ship: Ship, action: BaseAction, radius = action.getRangeRadius(ship)): void {
let yescolor = 0x000000;
let nocolor = 0x242022;
this.info.clear();
if (radius) {
this.info.fillStyle(nocolor);
this.info.fillRect(0, 0, this.width, this.height);
this.info.fillStyle(yescolor);
this.info.fillCircle(ship.arena_x, ship.arena_y, radius);
if (action instanceof MoveAction) {
let exclusions = action.getExclusionAreas(ship);
this.info.fillStyle(nocolor);
this.info.fillRect(0, 0, this.width, exclusions.hard_border);
this.info.fillRect(0, this.height - exclusions.hard_border, this.width, exclusions.hard_border);
this.info.fillRect(0, exclusions.hard_border, exclusions.hard_border, this.height - exclusions.hard_border * 2);
this.info.fillRect(this.width - exclusions.hard_border, exclusions.hard_border, exclusions.hard_border, this.height - exclusions.hard_border * 2);
exclusions.obstacles.forEach(obstacle => {
this.info.fillCircle(obstacle.x, obstacle.y, exclusions.effective_obstacle);
});
}
this.info.visible = true;
} else {
this.info.visible = false;
}
}
}
}

View File

@ -5,8 +5,7 @@ module TK.SpaceTac.UI.Specs {
function newTargetting(): Targetting {
return new Targetting(testgame.view,
testgame.view.action_bar,
testgame.view.toggle_tactical_mode,
testgame.view.arena.range_hint);
testgame.view.toggle_tactical_mode);
}
test.case("draws simulation parts", check => {
@ -153,36 +152,5 @@ module TK.SpaceTac.UI.Specs {
targetting.setTargetFromLocation({ x: 0, y: 0 });
check.equals(targetting.target, Target.newFromShip(playing_ship), "self 2");
})
test.case("updates the range hint display", check => {
let targetting = newTargetting();
let ship = nn(testgame.view.battle.playing_ship);
ship.setArenaPosition(0, 0);
TestTools.setShipModel(ship, 100, 0, 8);
let move = TestTools.addEngine(ship, 100);
let fire = TestTools.addWeapon(ship, 50, 2, 300, 100);
let last_call: any = null;
check.patch(targetting.range_hint, "clear", () => {
last_call = null;
});
check.patch(targetting.range_hint, "update", (ship: Ship, action: BaseAction, radius: number) => {
last_call = [ship, action, radius];
});
// move action
targetting.setAction(ship, move);
targetting.setTargetFromLocation({ x: 200, y: 0 });
check.equals(last_call, [ship, move, 800]);
// fire action
targetting.setAction(ship, fire);
targetting.setTargetFromLocation({ x: 200, y: 0 });
check.equals(last_call, [ship, fire, undefined]);
// move+fire
targetting.setAction(ship, fire);
targetting.setTargetFromLocation({ x: 400, y: 0 });
check.equals(last_call, [ship, move, 600]);
});
});
}

View File

@ -32,17 +32,15 @@ module TK.SpaceTac.UI {
// Collaborators to update
actionbar: ActionBar
range_hint: RangeHint
tactical_mode: ToggleClient
// Access to the parent view
view: BaseView
constructor(view: BaseView, actionbar: ActionBar, tactical_mode: Toggle, range_hint: RangeHint) {
constructor(view: BaseView, actionbar: ActionBar, tactical_mode: Toggle) {
this.view = view;
this.actionbar = actionbar;
this.tactical_mode = tactical_mode.manipulate("targetting");
this.range_hint = range_hint;
let builder = new UIBuilder(view);
this.container = builder.container("targetting");
@ -310,16 +308,8 @@ module TK.SpaceTac.UI {
if (this.action !== move_action) {
power = Math.max(power - this.action.getPowerUsage(this.ship, this.target), 0);
}
let radius = move_action.getRangeRadiusForPower(this.ship, power);
this.range_hint.update(this.ship, move_action, radius);
} else {
this.range_hint.clear();
}
} else {
this.range_hint.update(this.ship, this.action);
}
} else {
this.range_hint.clear();
}
}