1
0
Fork 0

Incomplete WIP

This commit is contained in:
Michaël Lemaire 2019-11-22 11:02:53 +01:00
parent aa640e23f2
commit 0e3e9bc199
10 changed files with 127 additions and 348 deletions

View File

@ -82,9 +82,9 @@ module TK.SpaceTac.Specs {
ship2.setArenaPosition(1000, 1000);
TestTools.setShipModel(ship2, 10);
let vig1 = ship1.actions.addCustom(new VigilanceAction("Vig1", { radius: 100, filter: ActionTargettingFilter.ENEMIES }, { intruder_effects: [new DamageEffect(1)] }));
let vig2 = ship1.actions.addCustom(new VigilanceAction("Vig2", { radius: 50, filter: ActionTargettingFilter.ENEMIES }, { intruder_effects: [new DamageEffect(2)] }));
let vig3 = ship1.actions.addCustom(new VigilanceAction("Vig3", { radius: 100, filter: ActionTargettingFilter.ALLIES }, { intruder_effects: [new DamageEffect(3)] }));
let vig1 = ship1.actions.addCustom(new VigilanceAction("Vig1", { radius: 100, filter: ActionImpactFilter.ENEMIES }, { intruder_effects: [new DamageEffect(1)] }));
let vig2 = ship1.actions.addCustom(new VigilanceAction("Vig2", { radius: 50, filter: ActionImpactFilter.ENEMIES }, { intruder_effects: [new DamageEffect(2)] }));
let vig3 = ship1.actions.addCustom(new VigilanceAction("Vig3", { radius: 100, filter: ActionImpactFilter.ALLIES }, { intruder_effects: [new DamageEffect(3)] }));
battle.applyOneAction(vig1.id);
battle.applyOneAction(vig2.id);
battle.applyOneAction(vig3.id);

View File

@ -1,62 +0,0 @@
module TK.SpaceTac.Specs {
testing("Target", test => {
test.case("initializes from ship or location", check => {
var target: Target;
target = Target.newFromLocation(2, 3);
check.equals(target.x, 2);
check.equals(target.y, 3);
check.equals(target.ship_id, null);
var ship = new Ship();
ship.arena_x = 4;
ship.arena_y = -2.1;
target = Target.newFromShip(ship);
check.equals(target.x, 4);
check.equals(target.y, -2.1);
check.equals(target.ship_id, ship.id);
});
test.case("gets distance to another target", check => {
var t1 = Target.newFromLocation(5, 1);
var t2 = Target.newFromLocation(6, 2);
check.nears(t1.getDistanceTo(t2), Math.sqrt(2));
});
test.case("gets angle to another target", check => {
var t1 = Target.newFromLocation(2, 3);
var t2 = Target.newFromLocation(4, 5);
check.nears(t1.getAngleTo(t2), Math.PI / 4);
});
test.case("checks if a target is in range of another", check => {
var t1 = Target.newFromLocation(5, 4);
check.equals(t1.isInRange(7, 3, 2), false);
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,174 +0,0 @@
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 {
// Coordinates of the target
x: number
y: number
// If the target is a ship, this attribute will be set
ship_id: RObjectId | null
// Standard constructor
constructor(x: number, y: number, ship: Ship | null = null) {
this.x = x;
this.y = y;
this.ship_id = ship ? ship.id : null;
}
jasmineToString() {
if (this.ship_id) {
return `(${this.x},${this.y}) ship_id=${this.ship_id}}`;
} else {
return `(${this.x},${this.y})`;
}
}
// Constructor to target a single ship
static newFromShip(ship: Ship): Target {
return new Target(ship.arena_x, ship.arena_y, ship);
}
// Constructor to target a location in space
static newFromLocation(x: number, y: number): Target {
return new Target(x, y, null);
}
// Get distance to another target
getDistanceTo(other: { x: number, y: number }): number {
var dx = other.x - this.x;
var dy = other.y - this.y;
return Math.sqrt(dx * dx + dy * dy);
}
// Get the normalized angle, in radians, to another target
getAngleTo(other: { x: number, y: number }): number {
var dx = other.x - this.x;
var dy = other.y - this.y;
return Math.atan2(dy, dx);
}
/**
* Returns true if the target is a ship
*/
isShip(): boolean {
return this.ship_id !== null;
}
/**
* Get the targetted ship in a battle
*/
getShip(battle: Battle): Ship | null {
if (this.isShip()) {
return battle.getShip(this.ship_id);
} else {
return null;
}
}
// Check if a target is in range from a specific point
isInRange(x: number, y: number, radius: number): boolean {
var dx = this.x - x;
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
// 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

@ -68,15 +68,15 @@ module TK.SpaceTac.Specs {
let ship2b = fleet2.addShip();
let ships = [ship1a, ship1b, ship2a, ship2b];
check.equals(BaseAction.filterTargets(ship1a, ships, ActionTargettingFilter.ALL),
check.equals(BaseAction.filterTargets(ship1a, ships, ActionImpactFilter.ALL),
[ship1a, ship1b, ship2a, ship2b], "ALL");
check.equals(BaseAction.filterTargets(ship1a, ships, ActionTargettingFilter.ALL_BUT_SELF),
check.equals(BaseAction.filterTargets(ship1a, ships, ActionImpactFilter.ALL_BUT_SELF),
[ship1b, ship2a, ship2b], "ALL_BUT_SELF");
check.equals(BaseAction.filterTargets(ship1a, ships, ActionTargettingFilter.ALLIES),
check.equals(BaseAction.filterTargets(ship1a, ships, ActionImpactFilter.ALLIES),
[ship1a, ship1b], "ALLIES");
check.equals(BaseAction.filterTargets(ship1a, ships, ActionTargettingFilter.ALLIES_BUT_SELF),
check.equals(BaseAction.filterTargets(ship1a, ships, ActionImpactFilter.ALLIES_BUT_SELF),
[ship1b], "ALLIES_BUT_SELF");
check.equals(BaseAction.filterTargets(ship1a, ships, ActionTargettingFilter.ENEMIES),
check.equals(BaseAction.filterTargets(ship1a, ships, ActionImpactFilter.ENEMIES),
[ship2a, ship2b], "ENEMIES");
});
});

View File

@ -5,33 +5,34 @@ module TK.SpaceTac {
export enum ActionCategory {
MOVE,
PASSIVE,
ACTIVE
ACTIVE,
}
/**
* Targetting mode for an action.
*
* This is a hint as to what type of target is required for this action.
* Target for an action
*/
export enum ActionTargettingMode {
// Apply immediately on the ship owning the action, without confirmation
export type ActionTarget = Readonly<{
distance?: number
angle?: number
}>
/**
* Target for an action, as applied from an hypothetical location
*/
export type ActionTargetFrom = Readonly<{
location: IArenaLocation
distance?: number
angle?: number
}>
/**
* Impact filter for an action.
*
* This will filter ships inside the targetted area, to determine which ones will receive the action effects.
*/
export enum ActionImpactFilter {
// Apply only on casting ship
SELF,
// Apply on the ship owning the action, with a confirmation
SELF_CONFIRM,
// Apply on one selected ship
SHIP,
// Apply on a space area
SPACE,
// Apply on the ship owning the action, but has an effect on surroundings
SURROUNDINGS
}
/**
* Targetting filter for an action.
*
* This will filter ships inside the targetted area, to determine which will receive the action effects.
*/
export enum ActionTargettingFilter {
// Apply on all ships
ALL,
// Apply on all ships except the actor
@ -41,7 +42,49 @@ module TK.SpaceTac {
// Apply on all allies, except the actor
ALLIES_BUT_SELF,
// Apply on all enemies
ENEMIES
ENEMIES,
};
/**
* Priority of ships in the target area.
*
* If there are more ships that the action limit, the most prioritized ones will be affected.
*/
export enum ActionImpactPriority {
// Nearest ships are priority
NEAREST,
// Farthest ships are priority
FARTHEST,
// Ships with the most hull are priority
TOUGHEST,
// Ships with the less hull are priority
WEAKEST,
}
/**
* Configuration for the target of an action
*/
export interface BaseActionConfig {
// Identifying code (for assets and effects)
code: string
// Human-friendly name
name: string
// Power cost
power_cost?: number
// Maximal distance the target can be set
min_distance?: number
// Minimal distance the target can be set
max_distance?: number
// Angle that will span the affected area (around the target direction)
angular_span?: number
// Radius of the affected area (around the target location)
radius?: number
// Filtering of ships in the affected area
ship_filter?: ActionImpactFilter
// Maximal number of ships impacted in the affected area
ship_limit?: number
// Priority of ships in affected area
ship_priority?: ActionImpactPriority
}
/**
@ -65,22 +108,28 @@ module TK.SpaceTac {
*
* An action should be the only way to modify a battle state.
*/
export class BaseAction extends RObject {
// Identifier code for the type of action
readonly code: string
// Full name of the action
readonly name: string
export class BaseAction extends RObject implements BaseActionConfig {
code = "nothing"
name = "Nothing"
power_cost = 0
min_distance = 0
max_distance = Infinity
angular_span = undefined
radius = undefined
ship_filter = ActionImpactFilter.ALL
ship_limit = undefined
ship_priority = ActionImpactPriority.NEAREST
// Cooldown configuration
private cooldown = new Cooldown()
// Create the action
constructor(name = "Nothing", code?: string) {
constructor(config?: Partial<Readonly<BaseActionConfig>>) {
super();
this.code = code ? code : name.toLowerCase().replace(" ", "");
this.name = name;
if (config) {
this.configure(config);
}
}
/**
@ -105,17 +154,17 @@ module TK.SpaceTac {
}
/**
* Get the targetting mode
* Get a default target for this action
*/
getTargettingMode(ship: Ship): ActionTargettingMode {
return ActionTargettingMode.SELF;
getDefaultTarget(ship: Ship): ActionTargetFrom {
return { location: ship.location };
}
/**
* Get a default target for this action
* Configure the base settings for this action
*/
getDefaultTarget(ship: Ship): Target {
return Target.newFromShip(ship);
configure(config: Partial<Readonly<BaseActionConfig>>): void {
copyfields(config, this);
}
/**
@ -134,13 +183,13 @@ module TK.SpaceTac {
}
/**
* Check basic conditions to know if the ship can use this action at all
* Check basic conditions to know if the ship can use this action at all, not accounting for power
*
* Method to extend to set conditions
*
* Returns an unavalability reason, null otherwise
*/
checkCannotBeApplied(ship: Ship, remaining_ap: number | null = null): ActionUnavailability | null {
checkCannotBeApplied(ship: Ship): ActionUnavailability | null {
if (!ship.actions.getById(this.id)) {
return ActionUnavailability.NO_SUCH_ACTION;
}
@ -154,17 +203,10 @@ module TK.SpaceTac {
}
/**
* Get the power cost of this action
* Get the power cost of this action for the current state
*/
getPowerUsage(ship: Ship): number {
return 0;
}
/**
* Get the range of this action, for targetting purpose
*/
getRangeRadius(ship: Ship): number {
return 0;
return this.power_cost;
}
/**
@ -172,36 +214,20 @@ module TK.SpaceTac {
*
* This may be used as an indicator for helping the player in targetting, or to effectively apply the effects
*/
filterImpactedShips(ship: Ship, source: IArenaLocation, target: Target, ships: Ship[]): Ship[] {
return [];
}
/**
* Get a list of ships impacted by this action
*/
getImpactedShips(ship: Ship, target: Target, source: IArenaLocation = ship.location): Ship[] {
let battle = ship.getBattle();
if (battle) {
return this.filterImpactedShips(ship, source, target, imaterialize(battle.iships(true)));
} else {
return [];
}
}
/**
* Helper to apply a targetting filter on a list of ships, to determine which ones are impacted
*/
static filterTargets(source: Ship, ships: Ship[], filter: ActionTargettingFilter): Ship[] {
filterImpactedShips(ships: readonly Ship[], source: Ship, target: ActionTargetFrom): readonly Ship[] {
// TODO Allow to work with ghosts (location instead of ships?)
// TODO Apply radius and angle
// TODO Apply limit and priority
return ships.filter(ship => {
if (filter == ActionTargettingFilter.ALL) {
if (this.ship_filter == ActionImpactFilter.ALL) {
return true;
} else if (filter == ActionTargettingFilter.ALL_BUT_SELF) {
} else if (this.ship_filter == ActionImpactFilter.ALL_BUT_SELF) {
return !ship.is(source);
} else if (filter == ActionTargettingFilter.ALLIES) {
} else if (this.ship_filter == ActionImpactFilter.ALLIES) {
return ship.fleet.player.is(source.fleet.player);
} else if (filter == ActionTargettingFilter.ALLIES_BUT_SELF) {
} else if (this.ship_filter == ActionImpactFilter.ALLIES_BUT_SELF) {
return ship.fleet.player.is(source.fleet.player) && !ship.is(source);
} else if (filter == ActionTargettingFilter.ENEMIES) {
} else if (this.ship_filter == ActionImpactFilter.ENEMIES) {
return !ship.fleet.player.is(source.fleet.player);
} else {
return false;
@ -212,16 +238,17 @@ module TK.SpaceTac {
/**
* Get a name to represent the group of ships specified by a target filter
*/
static getFilterDesc(filter: ActionTargettingFilter, plural = true): string {
if (filter == ActionTargettingFilter.ALL) {
static getFilterDesc(filter: ActionImpactFilter, plural = true): string {
// TODO limit and priority
if (filter == ActionImpactFilter.ALL) {
return plural ? "ships" : "ship";
} else if (filter == ActionTargettingFilter.ALL_BUT_SELF) {
} else if (filter == ActionImpactFilter.ALL_BUT_SELF) {
return plural ? "other ships" : "other ship";
} else if (filter == ActionTargettingFilter.ALLIES) {
} else if (filter == ActionImpactFilter.ALLIES) {
return plural ? "team members" : "team member";
} else if (filter == ActionTargettingFilter.ALLIES_BUT_SELF) {
} else if (filter == ActionImpactFilter.ALLIES_BUT_SELF) {
return plural ? "teammates" : "teammates";
} else if (filter == ActionTargettingFilter.ENEMIES) {
} else if (filter == ActionImpactFilter.ENEMIES) {
return plural ? "enemies" : "enemy";
} else {
return "";
@ -231,9 +258,9 @@ module TK.SpaceTac {
/**
* Check if a target is suitable for this action
*
* Will call checkLocationTarget or checkShipTarget by default
* Returns a suggested fixed target (may be the same as the input)
*/
checkTarget(ship: Ship, target: Target): Target | null {
checkTarget(ship: Ship, target: ActionTargetFrom): ActionTargetFrom | null {
if (this.checkCannotBeApplied(ship)) {
return null;
} else {
@ -245,24 +272,12 @@ 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
protected checkLocationTarget(ship: Ship, target: Target): Target | null {
return null;
}
// 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
protected checkShipTarget(ship: Ship, target: Target): Target | null {
return null;
}
/**
* Get the full list of diffs caused by applying this action
*
* This does not perform any check, and assumes the action is doable
*/
getDiffs(ship: Ship, battle: Battle, target = this.getDefaultTarget(ship)): BaseBattleDiff[] {
getDiffs(ship: Ship, battle: Battle, target: ActionTargetFrom): BaseBattleDiff[] {
let result: BaseBattleDiff[] = [];
// Action usage
@ -277,7 +292,7 @@ module TK.SpaceTac {
/**
* Method to reimplement to return the diffs specific to this action
*/
protected getSpecificDiffs(ship: Ship, battle: Battle, target: Target): BaseBattleDiff[] {
protected getSpecificDiffs(ship: Ship, battle: Battle, target: ActionTargetFrom): BaseBattleDiff[] {
return []
}
@ -286,7 +301,7 @@ module TK.SpaceTac {
*
* This will first check that the action can be done, then get the battle diffs and apply them.
*/
apply(battle: Battle, ship: Ship, target = this.getDefaultTarget(ship)): boolean {
apply(battle: Battle, ship: Ship, target: ActionTarget): boolean {
let reject = this.checkCannotBeApplied(ship);
if (reject) {
console.warn(`Action rejected - ${reject}`, ship, this, target);

View File

@ -12,7 +12,7 @@ module TK.SpaceTac {
// Effects applied
effects: BaseEffect[]
// Filtering ships that will receive the effects
filter: ActionTargettingFilter
filter: ActionImpactFilter
}
/**
@ -24,7 +24,7 @@ module TK.SpaceTac {
power = 1
radius = 0
effects: BaseEffect[] = []
filter = ActionTargettingFilter.ALL
filter = ActionImpactFilter.ALL
constructor(name: string, config?: Partial<ToggleActionConfig>, code?: string) {
super(name, code);

View File

@ -38,7 +38,7 @@ module TK.SpaceTac.Specs {
});
check.equals(action.getEffectsDescription(), "Watch a 120km area (power usage 2):\n• hull -1 on the first 3 incoming ships");
action = new VigilanceAction("Reactive Fire", { power: 2, radius: 120, filter: ActionTargettingFilter.ALLIES }, {
action = new VigilanceAction("Reactive Fire", { power: 2, radius: 120, filter: ActionImpactFilter.ALLIES }, {
intruder_count: 3,
intruder_effects: [new ValueEffect("hull", -1)]
});
@ -61,7 +61,7 @@ module TK.SpaceTac.Specs {
TestTools.setShipModel(ship2b, 10, 0, 5);
let engine = ship2b.actions.addCustom(new MoveAction("Move", { max_distance: 1000 }));
let action = ship1a.actions.addCustom(new VigilanceAction("Reactive Shot", { radius: 1000, filter: ActionTargettingFilter.ENEMIES }, {
let action = ship1a.actions.addCustom(new VigilanceAction("Reactive Shot", { radius: 1000, filter: ActionImpactFilter.ENEMIES }, {
intruder_effects: [new DamageEffect(1)]
}));

View File

@ -23,7 +23,7 @@ module TK.SpaceTac {
}, "prokhorovlaser");
laser.configureCooldown(3, 1);
let interceptors = new VigilanceAction("Interceptors Field", { radius: 400, power: 3, filter: ActionTargettingFilter.ENEMIES }, {
let interceptors = new VigilanceAction("Interceptors Field", { radius: 400, power: 3, filter: ActionImpactFilter.ENEMIES }, {
intruder_count: 1,
intruder_effects: [new DamageEffect(4, DamageEffectMode.SHIELD_THEN_HULL)]
}, "interceptors");

View File

@ -30,7 +30,7 @@ module TK.SpaceTac {
}, "gravitshield");
repulse.configureCooldown(1, 1);
let repairdrone = new DeployDroneAction("Repair Drone", { power: 3, filter: ActionTargettingFilter.ALLIES }, {
let repairdrone = new DeployDroneAction("Repair Drone", { power: 3, filter: ActionImpactFilter.ALLIES }, {
deploy_distance: 600,
drone_radius: 300,
drone_effects: [

View File

@ -21,7 +21,7 @@ module TK.SpaceTac {
power: 4,
radius: 400,
effects: [new AttributeEffect("evasion", 1)],
filter: ActionTargettingFilter.ALLIES
filter: ActionImpactFilter.ALLIES
});
let depleter = new TriggerAction("Power Depleter", {