From 63729f853798debd86b47642a873af876fc19095 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C3=ABl=20Lemaire?= Date: Thu, 16 May 2019 01:04:55 +0200 Subject: [PATCH] Continued work on battle plan display --- graphics/ui/battle.svg | 624 ++++++++++++++++++++------- src/core/ArenaGrid.spec.ts | 29 -- src/core/ArenaGrid.ts | 34 -- src/core/Battle.ts | 9 +- src/core/BattlePlanning.spec.ts | 57 +++ src/core/BattlePlanning.ts | 7 +- src/core/ExclusionAreas.spec.ts | 43 -- src/core/ExclusionAreas.ts | 96 ----- src/core/MoveFireSimulator.spec.ts | 26 -- src/core/MoveFireSimulator.ts | 1 - src/core/Target.ts | 12 - src/core/actions/MoveAction.spec.ts | 58 --- src/core/actions/MoveAction.ts | 19 - src/core/effects/RepelEffect.spec.ts | 14 - src/core/effects/RepelEffect.ts | 3 +- src/ui/battle/BattleView.ts | 9 - src/ui/battle/LogProcessor.ts | 1 - src/ui/battle/PlanDisplay.ts | 92 +++- src/ui/battle/RangeHint.ts | 14 - src/ui/battle/ShipList.spec.ts | 88 ---- src/ui/battle/ShipList.ts | 156 ------- src/ui/battle/ShipListItem.ts | 91 ---- 22 files changed, 611 insertions(+), 872 deletions(-) delete mode 100644 src/core/ArenaGrid.spec.ts delete mode 100644 src/core/ArenaGrid.ts delete mode 100644 src/core/ExclusionAreas.spec.ts delete mode 100644 src/core/ExclusionAreas.ts delete mode 100644 src/ui/battle/ShipList.spec.ts delete mode 100644 src/ui/battle/ShipList.ts delete mode 100644 src/ui/battle/ShipListItem.ts diff --git a/graphics/ui/battle.svg b/graphics/ui/battle.svg index b1cefac..4edabe6 100644 --- a/graphics/ui/battle.svg +++ b/graphics/ui/battle.svg @@ -25,6 +25,22 @@ enable-background="new"> + + + + + @@ -1592,19 +1608,6 @@ stdDeviation="5.9752511" id="feGaussianBlur5167" /> - - - + + + + + + + + + + + + + + + + + style="opacity:0.15899999;filter:url(#filter1892)"> - + id="g2250"> + + Move + + + + + + + + + + + + + + + + + + Power available + Power used + 7 + 5 + + + + + + + + + + + Passive + No available actionof this type + + + + Active + + + + + + + + + + + 5 + + + + id="path2065" + d="m 1712.9662,74.84767 h 147.0783" + style="fill:none;stroke:url(#linearGradient2121);stroke-width:2;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" /> + + + - Passive - Move - Active - - Power available - Power used - 7 - 5 - No available actionof this type + id="image6587" + preserveAspectRatio="none" + height="183.97649" + width="183.97668" /> + id="image5902" + preserveAspectRatio="none" + height="83.066681" + width="83.066681" + style="display:inline;enable-background:new" /> + y="236.27179" + x="-410.59262" + id="image9266" + preserveAspectRatio="none" + height="84.542961" + width="84.542961" /> + x="816.54016" + id="image5590" + preserveAspectRatio="none" + height="76.394867" + width="76.394867" /> { - test.case("snaps coordinates to the nearest grid point, on a biased grid", check => { - let grid = new HexagonalArenaGrid(4, 0.75); - checkLocation(check, grid.snap({ x: 0, y: 0 }), 0, 0); - checkLocation(check, grid.snap({ x: 1, y: 0 }), 0, 0); - checkLocation(check, grid.snap({ x: 1.9, y: 0 }), 0, 0); - checkLocation(check, grid.snap({ x: 2.1, y: 0 }), 4, 0); - checkLocation(check, grid.snap({ x: 1, y: 1 }), 0, 0); - checkLocation(check, grid.snap({ x: 1, y: 2 }), 2, 3); - checkLocation(check, grid.snap({ x: -1, y: -1 }), 0, 0); - checkLocation(check, grid.snap({ x: -2, y: -2 }), -2, -3); - checkLocation(check, grid.snap({ x: -3, y: -1 }), -4, 0); - checkLocation(check, grid.snap({ x: 6, y: -5 }), 8, -6); - }); - - test.case("snaps coordinates to the nearest grid point, on a regular grid", check => { - let grid = new HexagonalArenaGrid(10); - checkLocation(check, grid.snap({ x: 0, y: 0 }), 0, 0); - checkLocation(check, grid.snap({ x: 8, y: 0 }), 10, 0); - checkLocation(check, grid.snap({ x: 1, y: 6 }), 5, 10 * Math.sqrt(0.75)); - }); - }); -} diff --git a/src/core/ArenaGrid.ts b/src/core/ArenaGrid.ts deleted file mode 100644 index b9a6aef..0000000 --- a/src/core/ArenaGrid.ts +++ /dev/null @@ -1,34 +0,0 @@ -module TK.SpaceTac { - /** - * Abstract grid for the arena where the battle takes place - * - * The grid is used to snap arena coordinates for ships and targets - */ - export interface IArenaGrid { - snap(loc: IArenaLocation): IArenaLocation; - } - - /** - * Hexagonal unbounded arena grid - * - * This grid is composed of regular hexagons where all vertices are at a same distance "unit" of the hexagon center - */ - export class HexagonalArenaGrid implements IArenaGrid { - private yunit: number; - - constructor(private unit: number, private yfactor = Math.sqrt(0.75)) { - this.yunit = unit * yfactor; - } - - snap(loc: IArenaLocation): IArenaLocation { - let yr = Math.round(loc.y / this.yunit); - let xr: number; - if (yr % 2 == 0) { - xr = Math.round(loc.x / this.unit); - } else { - xr = Math.round((loc.x - 0.5 * this.unit) / this.unit) + 0.5; - } - return new ArenaLocation((xr * this.unit) || 0, (yr * this.yunit) || 0); - } - } -} diff --git a/src/core/Battle.ts b/src/core/Battle.ts index 51118bf..f86dc09 100644 --- a/src/core/Battle.ts +++ b/src/core/Battle.ts @@ -3,9 +3,6 @@ module TK.SpaceTac { * A turn-based battle between fleets */ export class Battle { - // Grid for the arena - grid?: IArenaGrid - // Battle outcome, if the battle has ended outcome: BattleOutcome | null = null @@ -34,15 +31,11 @@ module TK.SpaceTac { // Size of the battle area width: number height: number - border = 50 - ship_separation = 100 // Indicator that an AI is playing ai_playing = false - constructor(fleet1 = new Fleet(new Player("Attacker")), fleet2 = new Fleet(new Player("Defender")), width = 1808, height = 948) { - this.grid = new HexagonalArenaGrid(50); - + constructor(fleet1 = new Fleet(new Player("Attacker")), fleet2 = new Fleet(new Player("Defender")), width = 1920, height = 1080) { this.fleets = [fleet1, fleet2]; this.ships = new RObjectContainer(fleet1.ships.concat(fleet2.ships)); this.play_order = []; diff --git a/src/core/BattlePlanning.spec.ts b/src/core/BattlePlanning.spec.ts index 756eb71..39a1c34 100644 --- a/src/core/BattlePlanning.spec.ts +++ b/src/core/BattlePlanning.spec.ts @@ -58,5 +58,62 @@ module TK.SpaceTac.Specs { { action: action1.id, category: action1.getCategory(), target: Target.newFromShip(ship) } ]); }); + + test.case("replaces existing actions", check => { + const battle = new Battle(); + const ship = battle.fleets[0].addShip(); + const action1 = ship.actions.addCustom(new BaseAction()); + const action2 = ship.actions.addCustom(new BaseAction()); + const planning = new BattlePlanning(battle); + + check.equals(planning.getShipPlan(ship).actions, []); + + planning.addAction(ship, action1, Target.newFromShip(ship)); + check.equals(planning.getShipPlan(ship).actions, [ + { action: action1.id, category: action1.getCategory(), target: Target.newFromShip(ship) } + ]); + + planning.addAction(ship, action2, Target.newFromShip(ship)); + check.equals(planning.getShipPlan(ship).actions, [ + { action: action1.id, category: action1.getCategory(), target: Target.newFromShip(ship) }, + { action: action2.id, category: action2.getCategory(), target: Target.newFromShip(ship) } + ]); + + planning.addAction(ship, action1, Target.newFromLocation(5, 1)); + check.equals(planning.getShipPlan(ship).actions, [ + { action: action2.id, category: action2.getCategory(), target: Target.newFromShip(ship) }, + { action: action1.id, category: action1.getCategory(), target: Target.newFromLocation(5, 1) } + ]); + }); + + test.case("replaces other similar actions", check => { + const battle = new Battle(); + const ship = battle.fleets[0].addShip(); + const active1 = ship.actions.addCustom(new TriggerAction()); + const active2 = ship.actions.addCustom(new TriggerAction()); + const move1 = ship.actions.addCustom(new MoveAction()); + const move2 = ship.actions.addCustom(new MoveAction()); + const planning = new BattlePlanning(battle); + + planning.addAction(ship, active1); + check.equals(planning.getShipPlan(ship).actions, [ + { action: active1.id, category: active1.getCategory(), target: undefined } + ]); + planning.addAction(ship, active2); + check.equals(planning.getShipPlan(ship).actions, [ + { action: active2.id, category: active2.getCategory(), target: undefined } + ]); + + planning.addAction(ship, move1); + check.equals(planning.getShipPlan(ship).actions, [ + { action: active2.id, category: active2.getCategory(), target: undefined }, + { action: move1.id, category: move1.getCategory(), target: undefined } + ]); + planning.addAction(ship, move2); + check.equals(planning.getShipPlan(ship).actions, [ + { action: active2.id, category: active2.getCategory(), target: undefined }, + { action: move2.id, category: move2.getCategory(), target: undefined } + ]); + }); }); } diff --git a/src/core/BattlePlanning.ts b/src/core/BattlePlanning.ts index ba84867..786e5ca 100644 --- a/src/core/BattlePlanning.ts +++ b/src/core/BattlePlanning.ts @@ -37,11 +37,12 @@ namespace TK.SpaceTac { */ addAction(ship: Ship, action: BaseAction, target?: Target) { const plan = this.getShipPlan(ship); - if (any(plan.actions, iaction => action.is(iaction.action))) { - // TODO replace (or remove if toggle action ?) + if (action.getCategory() == ActionCategory.PASSIVE) { + plan.actions = plan.actions.filter(iaction => !action.is(iaction.action)); } else { - plan.actions.push({ action: action.id, category: action.getCategory(), target }); + plan.actions = plan.actions.filter(iaction => iaction.category != action.getCategory()); } + plan.actions.push({ action: action.id, category: action.getCategory(), target }); } /** diff --git a/src/core/ExclusionAreas.spec.ts b/src/core/ExclusionAreas.spec.ts deleted file mode 100644 index 7700d96..0000000 --- a/src/core/ExclusionAreas.spec.ts +++ /dev/null @@ -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)]); - }) - }) -} \ No newline at end of file diff --git a/src/core/ExclusionAreas.ts b/src/core/ExclusionAreas.ts deleted file mode 100644 index 9befc2e..0000000 --- a/src/core/ExclusionAreas.ts +++ /dev/null @@ -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); - } - } -} diff --git a/src/core/MoveFireSimulator.spec.ts b/src/core/MoveFireSimulator.spec.ts index d34152c..525c756 100644 --- a/src/core/MoveFireSimulator.spec.ts +++ b/src/core/MoveFireSimulator.spec.ts @@ -107,32 +107,6 @@ module TK.SpaceTac.Specs { ]); }); - test.case("accounts for exclusion areas for the approach", check => { - let [ship, simulator, action] = simpleWeaponCase(100, 5, 1, 50); - ship.setArenaPosition(300, 200); - let battle = new Battle(); - battle.fleets[0].addShip(ship); - let ship1 = battle.fleets[0].addShip(); - let moveaction = nn(simulator.findEngine()); - (moveaction).safety_distance = 30; - battle.ship_separation = 30; - - check.same(simulator.getApproach(moveaction, Target.newFromLocation(350, 200), 100), ApproachSimulationError.NO_MOVE_NEEDED); - check.same(simulator.getApproach(moveaction, Target.newFromLocation(400, 200), 100), ApproachSimulationError.NO_MOVE_NEEDED); - check.equals(simulator.getApproach(moveaction, Target.newFromLocation(500, 200), 100), new Target(400, 200)); - - ship1.setArenaPosition(420, 200); - - check.patch(simulator, "scanCircle", () => iarray([ - new Target(400, 200), - new Target(410, 200), - new Target(410, 230), - new Target(420, 210), - new Target(480, 260), - ])); - check.equals(simulator.getApproach(moveaction, Target.newFromLocation(500, 200), 100), new Target(410, 230)); - }); - test.case("moves to get in range, even if not enough AP to fire", check => { let [ship, simulator, action] = simpleWeaponCase(8, 3, 2, 5); let result = simulator.simulateAction(action, new Target(ship.arena_x + 18, ship.arena_y, null)); diff --git a/src/core/MoveFireSimulator.ts b/src/core/MoveFireSimulator.ts index 81a8ec4..6483eb7 100644 --- a/src/core/MoveFireSimulator.ts +++ b/src/core/MoveFireSimulator.ts @@ -133,7 +133,6 @@ module TK.SpaceTac { result.move_location = Target.newFromShip(this.ship); if (action instanceof MoveAction) { let corrected_target = action.applyReachableRange(this.ship, target, move_margin); - corrected_target = action.applyExclusion(this.ship, corrected_target); if (corrected_target) { result.need_move = target.getDistanceTo(this.ship.location) > 0; move_target = corrected_target; diff --git a/src/core/Target.ts b/src/core/Target.ts index 75a12ba..f029ce6 100644 --- a/src/core/Target.ts +++ b/src/core/Target.ts @@ -64,18 +64,6 @@ module TK.SpaceTac { return new Target(x, y, null); } - /** - * Snap to battle grid - */ - snap(grid: IArenaGrid): Target { - if (this.ship_id) { - return this; - } else { - let location = grid.snap(this); - return Target.newFromLocation(location.x, location.y); - } - } - // Get distance to another target getDistanceTo(other: { x: number, y: number }): number { var dx = other.x - this.x; diff --git a/src/core/actions/MoveAction.spec.ts b/src/core/actions/MoveAction.spec.ts index 216407e..dde11b8 100644 --- a/src/core/actions/MoveAction.spec.ts +++ b/src/core/actions/MoveAction.spec.ts @@ -60,64 +60,6 @@ module TK.SpaceTac.Specs { ]); }); - test.case("can't move too much near another ship", check => { - var battle = TestTools.createBattle(1, 1); - var ship = battle.fleets[0].ships[0]; - var enemy = battle.fleets[1].ships[0]; - TestTools.setShipModel(ship, 100, 0, 100); - ship.setArenaPosition(500, 500); - enemy.setArenaPosition(1000, 500); - - var action = new MoveAction("Engine", { distance_per_power: 1000, safety_distance: 200 }); - - var result = action.checkLocationTarget(ship, Target.newFromLocation(700, 500)); - check.equals(result, Target.newFromLocation(700, 500)); - - result = action.checkLocationTarget(ship, Target.newFromLocation(800, 500)); - check.equals(result, Target.newFromLocation(800, 500)); - - result = action.checkLocationTarget(ship, Target.newFromLocation(900, 500)); - check.equals(result, Target.newFromLocation(800, 500)); - - result = action.checkLocationTarget(ship, Target.newFromLocation(1000, 500)); - check.equals(result, Target.newFromLocation(800, 500)); - - result = action.checkLocationTarget(ship, Target.newFromLocation(1200, 500)); - check.equals(result, Target.newFromLocation(1200, 500)); - }); - - 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 => { let action = new MoveAction("Engine", { distance_per_power: 58, safety_distance: 0 }); check.equals(action.getEffectsDescription(), "Move: 58km per power point"); diff --git a/src/core/actions/MoveAction.ts b/src/core/actions/MoveAction.ts index 8ed7c90..3aa4e62 100644 --- a/src/core/actions/MoveAction.ts +++ b/src/core/actions/MoveAction.ts @@ -101,24 +101,6 @@ module TK.SpaceTac { 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 */ @@ -130,7 +112,6 @@ module TK.SpaceTac { checkLocationTarget(ship: Ship, target: Target): Target | null { target = this.applyReachableRange(ship, target); - target = this.applyExclusion(ship, target); return target.getDistanceTo(ship.location) > 0 ? target : null; } diff --git a/src/core/effects/RepelEffect.spec.ts b/src/core/effects/RepelEffect.spec.ts index 5c803bd..117ea1c 100644 --- a/src/core/effects/RepelEffect.spec.ts +++ b/src/core/effects/RepelEffect.spec.ts @@ -22,19 +22,5 @@ module TK.SpaceTac.Specs { check.equals(ship1b.location, new ArenaLocationAngle(262, 100)); check.equals(ship2a.location, new ArenaLocationAngle(100, 292)); }) - - test.case("does not push a ship inside a hard exclusion area", check => { - let battle = new Battle(); - let ship1a = battle.fleets[0].addShip(); - ship1a.setArenaPosition(100, 100); - let ship2a = battle.fleets[1].addShip(); - ship2a.setArenaPosition(100, 200); - let ship2b = battle.fleets[1].addShip(); - ship2b.setArenaPosition(100, 350); - - let effect = new RepelEffect(85); - battle.applyDiffs(effect.getOnDiffs(ship2a, ship1a)); - check.equals(ship2a.location, new ArenaLocationAngle(100, 250)); - }) }) } diff --git a/src/core/effects/RepelEffect.ts b/src/core/effects/RepelEffect.ts index 9c92640..bf8af2c 100644 --- a/src/core/effects/RepelEffect.ts +++ b/src/core/effects/RepelEffect.ts @@ -17,8 +17,7 @@ module TK.SpaceTac { 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); + // TODO Apply collisions // TODO Apply area effect adding/removal return [ new ShipMoveDiff(ship, ship.location, new ArenaLocationAngle(destination.x, destination.y, ship.arena_angle)) diff --git a/src/ui/battle/BattleView.ts b/src/ui/battle/BattleView.ts index 4156a1e..89be280 100644 --- a/src/ui/battle/BattleView.ts +++ b/src/ui/battle/BattleView.ts @@ -45,9 +45,6 @@ module TK.SpaceTac.UI { // Targetting mode (null if we're not in this mode) targetting!: Targetting - // Ship list - ship_list!: ShipList - // Action bar action_bar!: ActionBar @@ -116,10 +113,6 @@ module TK.SpaceTac.UI { // Add UI elements this.action_bar = new ActionBar(this); - this.action_bar.setPosition(0, this.getHeight() - 132); - this.ship_list = new ShipList(this, this.battle, this.player, this.toggle_tactical_mode, this, - this.layer_borders, this.getWidth() - 112, 0); - this.ship_list.bindToLog(this.log_processor); this.ship_tooltip = new ShipTooltip(this); this.character_sheet = new CharacterSheet(this, CharacterSheetMode.DISPLAY); this.character_sheet.moveToLayer(this.layer_sheets); @@ -132,7 +125,6 @@ module TK.SpaceTac.UI { this.audio.startMusic("mechanolith", 0.2); // Key mapping - this.inputs.bind("t", "Show tactical view", () => this.ship_list.info_button.toggle()); this.inputs.bind("Enter", "Validate action", () => this.validationPressed()); this.inputs.bind(" ", "Validate action", () => this.validationPressed()); this.inputs.bind("Escape", "Cancel action", () => this.action_bar.actionEnded()); @@ -311,7 +303,6 @@ module TK.SpaceTac.UI { setShipHovered(ship: Ship | null): void { this.ship_hovered = ship; this.arena.setShipHovered(ship); - this.ship_list.setHovered(ship); if (ship) { this.ship_tooltip.setShip(ship); diff --git a/src/ui/battle/LogProcessor.ts b/src/ui/battle/LogProcessor.ts index 1f55412..96785e2 100644 --- a/src/ui/battle/LogProcessor.ts +++ b/src/ui/battle/LogProcessor.ts @@ -307,7 +307,6 @@ module TK.SpaceTac.UI { this.view.setShipHovered(null); } this.view.arena.markAsDead(dead_ship); - this.view.ship_list.refresh(); if (speed) { await this.view.timer.sleep(2000 / speed); } diff --git a/src/ui/battle/PlanDisplay.ts b/src/ui/battle/PlanDisplay.ts index fbe65e7..2236dec 100644 --- a/src/ui/battle/PlanDisplay.ts +++ b/src/ui/battle/PlanDisplay.ts @@ -1,4 +1,7 @@ module TK.SpaceTac.UI { + const PLAN_COLOR = 0xa5b7da + const PLAN_COLOR_HL = 0xdde6f9 + /** * Displays and maintain a battle plan */ @@ -48,27 +51,102 @@ module TK.SpaceTac.UI { } private updateShip(plan: ShipPlan, parent: UIContainer) { - const move = first(plan.actions, action => action.category == ActionCategory.MOVE); const ship = this.battle ? this.battle.getShip(plan.ship) : null; + if (ship) { + const move = first(plan.actions, action => action.category === ActionCategory.MOVE); this.updateMoveAction(ship, move, parent); + + const final_location = (move && move.target) ? move.target : ship.location; + + const active = first(plan.actions, action => action.category === ActionCategory.ACTIVE); + this.updateActiveAction(ship, final_location, active, parent); } else { console.error("Ship not found to update actions", plan); } } - private updateMoveAction(ship: Ship, action: ActionPlan | null, parent: UIContainer) { - const child = parent.getByName("move"); - const graphics = child ? as(UIGraphics, child) : parent.getBuilder().graphics("move"); + private updateMoveAction(ship: Ship, plan: ActionPlan | null, parent: UIContainer) { + let child = parent.getByName("moveline"); + const graphics = child ? as(UIGraphics, child) : parent.getBuilder().graphics("moveline"); graphics.clear(); - if (action && action.target) { + if (plan && plan.target) { graphics.addLine({ start: ship.location, - end: action.target, + end: plan.target, width: 5, - color: 0xa5b7da + color: PLAN_COLOR }); } + + child = parent.getByName("moveghost"); + const ghost = child ? as(UIImage, child) : parent.getBuilder().image(`ship-${ship.model.code}-sprite`, 0, 0, true); + ghost.setName("moveghost"); + if (plan && plan.target) { + ghost.setVisible(true); + ghost.setPosition(plan.target.x, plan.target.y); + ghost.setTint(PLAN_COLOR); + } else { + ghost.setVisible(false); + } + } + + private updateActiveAction(ship: Ship, from: IArenaLocation, plan: ActionPlan | null, parent: UIContainer) { + let child = parent.getByName("activearea"); + const graphics = child ? as(UIGraphics, child) : parent.getBuilder().graphics("activearea"); + graphics.clear(); + + const action = plan ? ship.actions.getById(plan.action) : null; + if (action && plan) { + let radius = 0; + let angle = 0; + if (action instanceof TriggerAction) { + if (action.angle) { + angle = (action.angle * 0.5) * Math.PI / 180; + radius = action.range; + } else { + radius = action.blast; + } + } else if (action instanceof DeployDroneAction) { + radius = action.drone_radius; + } else if (action instanceof ToggleAction) { + radius = action.radius; + } + + if (radius) { + if (angle) { + const base_angle = plan.target ? Math.atan2(plan.target.y - from.y, plan.target.x - from.x) : 0; + graphics.addCircleArc({ + center: from, + radius, + angle: { start: base_angle - angle, span: angle * 2 }, + fill: { color: PLAN_COLOR, alpha: 0.2 }, + border: { color: PLAN_COLOR_HL, alpha: 0.6, width: 2 }, + }); + graphics.addCircleArc({ + center: from, + radius: radius * 0.95, + angle: { start: base_angle - angle * 0.95, span: angle * 1.9 }, + fill: { color: PLAN_COLOR, alpha: 0.1 }, + border: { color: PLAN_COLOR_HL, alpha: 0.3, width: 1 }, + }); + } else { + const center = plan ? plan.target : from; + graphics.addCircle({ + center, + radius, + fill: { color: PLAN_COLOR, alpha: 0.2 }, + border: { color: PLAN_COLOR_HL, alpha: 0.6, width: 2 }, + }); + graphics.addCircle({ + center, + radius: radius * 0.95, + fill: { color: PLAN_COLOR, alpha: 0.1 }, + border: { color: PLAN_COLOR_HL, alpha: 0.3, width: 1 }, + }); + } + } + } } } } diff --git a/src/ui/battle/RangeHint.ts b/src/ui/battle/RangeHint.ts index ab6fa85..aecf85d 100644 --- a/src/ui/battle/RangeHint.ts +++ b/src/ui/battle/RangeHint.ts @@ -54,20 +54,6 @@ module TK.SpaceTac.UI { 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; diff --git a/src/ui/battle/ShipList.spec.ts b/src/ui/battle/ShipList.spec.ts deleted file mode 100644 index 386363e..0000000 --- a/src/ui/battle/ShipList.spec.ts +++ /dev/null @@ -1,88 +0,0 @@ -module TK.SpaceTac.UI.Specs { - testing("ShipList", test => { - let testgame = setupEmptyView(test); - - function createList(): ShipList { - let view = testgame.view; - let battle = new Battle(); - let player = new Player(); - battle.fleets[0].setPlayer(player); - let tactical_mode = new Toggle(); - let ship_buttons = { - cursorOnShip: nop, - cursorOffShip: nop, - cursorClicked: nop, - }; - let list = new ShipList(view, battle, player, tactical_mode, ship_buttons); - return list; - } - - test.case("handles play position of ships", check => { - let list = createList(); - let battle = list.battle; - check.in("initial", check => { - check.equals(list.items.length, 0, "no item at first"); - }); - - let ship = battle.fleets[0].addShip(); - TestTools.setShipModel(ship, 10, 0); - list.setShipsFromBattle(battle, false); - check.in("one ship added but not in play order", check => { - check.equals(list.items.length, 1, "item count"); - check.equals(list.items[0].visible, false, "ship card not visible"); - }); - - battle.throwInitiative(); - list.refresh(0); - check.in("ship now in play order", check => { - check.equals(list.items[0].visible, true, "ship card visible"); - }); - - ship = battle.fleets[1].addShip(); - TestTools.setShipModel(ship, 10, 0); - battle.throwInitiative(); - list.setShipsFromBattle(battle, false); - check.in("ship added in the other fleet", check => { - check.equals(list.items.length, 2, "item count"); - check.equals(nn(list.findItem(battle.play_order[0])).location, { x: 2, y: 843 }, "first ship position"); - check.equals(nn(list.findItem(battle.play_order[1])).location, { x: 2, y: 744 }, "second ship position"); - }); - - battle.setPlayingShip(battle.play_order[0]); - list.refresh(0); - check.in("started", check => { - check.equals(nn(list.findItem(battle.play_order[0])).location, { x: -14, y: 962 }, "first ship position"); - check.equals(nn(list.findItem(battle.play_order[1])).location, { x: 2, y: 843 }, "second ship position"); - }); - - battle.advanceToNextShip(); - list.refresh(0); - check.in("end turn", check => { - check.equals(nn(list.findItem(battle.play_order[0])).location, { x: 2, y: 843 }, "first ship position"); - check.equals(nn(list.findItem(battle.play_order[1])).location, { x: -14, y: 962 }, "second ship position"); - }); - - ship = battle.fleets[1].addShip(); - TestTools.setShipModel(ship, 10, 0); - battle.throwInitiative(); - battle.setPlayingShip(battle.play_order[0]); - list.setShipsFromBattle(battle, false); - check.in("third ship added", check => { - check.equals(list.items.length, 3, "item count"); - check.equals(nn(list.findItem(battle.play_order[0])).location, { x: -14, y: 962 }, "first ship position"); - check.equals(nn(list.findItem(battle.play_order[1])).location, { x: 2, y: 843 }, "second ship position"); - check.equals(nn(list.findItem(battle.play_order[2])).location, { x: 2, y: 744 }, "third ship position"); - }); - - let dead = battle.play_order[1]; - dead.setDead(); - list.refresh(0); - check.in("ship dead", check => { - check.equals(list.items.length, 3, "item count"); - check.equals(nn(list.findItem(battle.play_order[0])).location, { x: -14, y: 962 }, "first ship position"); - check.equals(nn(list.findItem(dead)).location, { x: 200, y: 843 }, "dead ship position"); - check.equals(nn(list.findItem(battle.play_order[1])).location, { x: 2, y: 843 }, "second ship position"); - }); - }); - }); -} diff --git a/src/ui/battle/ShipList.ts b/src/ui/battle/ShipList.ts deleted file mode 100644 index 7fd197b..0000000 --- a/src/ui/battle/ShipList.ts +++ /dev/null @@ -1,156 +0,0 @@ -module TK.SpaceTac.UI { - /** - * Side bar with all playing ships, sorted by play order - */ - export class ShipList { - // Link to the parent view - view: BaseView - - // Current battle - battle: Battle - - // Current player - player: Player - - // Interface for acting as ship button - ship_buttons: IShipButton - - // Container - container: UIContainer - - // List of ship items - items: ShipListItem[] - - // Hovered ship - hovered: ShipListItem | null - - // Info button - info_button: UIButton - - constructor(view: BaseView, battle: Battle, player: Player, tactical_mode: Toggle, ship_buttons: IShipButton, parent?: UIContainer, x = 0, y = 0) { - let builder = new UIBuilder(view, parent); - this.container = builder.container("shiplist", x, y); - - builder = builder.in(this.container); - let bg = builder.image("battle-shiplist-background", 0, 0); - bg.setInteractive(); - - this.view = view; - this.battle = battle; - this.player = player; - this.ship_buttons = ship_buttons; - - this.items = []; - this.hovered = null; - - this.info_button = builder.button("battle-shiplist-info-button", 0, 0, undefined, "Tactical display", on => tactical_mode.manipulate("shiplist")(on)); - - this.setShipsFromBattle(battle); - } - - /** - * Clear all ship cards - */ - clearAll(): void { - this.items.forEach(ship => ship.destroy()); - this.items = []; - } - - /** - * Rebuild the ship list from an ongoing battle - */ - setShipsFromBattle(battle: Battle, animate = true): void { - this.clearAll(); - iforeach(battle.iships(true), ship => this.addShip(ship)); - this.refresh(animate ? 1 : 0); - } - - /** - * Bind to a log processor, to watch for events - */ - bindToLog(log: LogProcessor): void { - log.watchForShipChange(ship => { - return { - foreground: async (speed: number) => { - this.refresh(speed); - } - } - }); - - log.register(diff => { - if (diff instanceof ShipDamageDiff) { - return { - background: async () => { - let item = this.findItem(diff.ship_id); - if (item) { - item.setDamageHit(); - } - } - } - } else { - return {}; - } - }) - } - - /** - * Add a ship card - */ - addShip(ship: Ship): ShipListItem { - var owned = ship.isPlayedBy(this.player); - var result = new ShipListItem(this, 200, this.container.height / 2, ship, owned, this.ship_buttons); - this.items.push(result); - this.container.add(result); - return result; - } - - /** - * Find the item (card) that displays a given ship - */ - findItem(ship: Ship | RObjectId | null): ShipListItem | null { - return first(this.items, item => item.ship.is(ship)); - } - - /** - * Update the locations of all items - */ - refresh(speed = 1): void { - let duration = speed ? (1000 / speed) : 0; - this.items.forEach(item => { - if (item.ship.alive) { - let position = this.battle.getPlayOrder(item.ship); - if (position < 0) { - item.visible = false; - } else { - if (position == 0) { - item.moveAt(-14, 962, duration); - } else { - item.moveAt(2, 942 - position * 99, duration); - } - item.visible = true; - item.setZ(99 - position); - } - } else { - item.setZ(100); - item.moveAt(200, item.y, duration); - } - }); - } - - /** - * Set the currently hovered ship - */ - setHovered(ship: Ship | null): void { - if (this.hovered) { - this.hovered.setHovered(false); - this.hovered = null; - } - if (ship) { - this.hovered = this.findItem(ship); - if (this.hovered) { - this.hovered.setHovered(true); - } - } - } - } -} diff --git a/src/ui/battle/ShipListItem.ts b/src/ui/battle/ShipListItem.ts deleted file mode 100644 index 69bb352..0000000 --- a/src/ui/battle/ShipListItem.ts +++ /dev/null @@ -1,91 +0,0 @@ -module TK.SpaceTac.UI { - /** - * One item in a ship list (used in BattleView) - */ - export class ShipListItem extends UIContainer { - // Reference to the view - view: BaseView - - // Reference to the ship game object - ship: Ship - - // Player indicator - player_indicator: UIImage - - // Portrait - portrait: UIImage - - // Damage flashing indicator - damage_indicator: UIImage - - // Hover indicator - hover_indicator: UIImage - - // Create a ship button for the battle ship list - constructor(list: ShipList, x: number, y: number, ship: Ship, owned: boolean, ship_buttons: IShipButton) { - // TODO Make it an UIButton - super(list.view, x, y); - this.view = list.view; - this.ship = ship; - - let builder = new UIBuilder(list.view, this); - - builder.image("battle-shiplist-item-background"); - - this.player_indicator = builder.image(owned ? "battle-hud-ship-own-mini" : "battle-hud-ship-enemy-mini", 102, 52, true); - this.player_indicator.setAngle(-90); - - this.portrait = builder.image(`ship-${ship.model.code}-sprite`, 52, 52, true); - this.portrait.setScale(0.8) - this.portrait.setAngle(180); - - this.damage_indicator = builder.image("battle-shiplist-damage", 8, 9); - this.damage_indicator.visible = false; - - this.hover_indicator = builder.image("battle-shiplist-hover", 7, 8); - this.hover_indicator.visible = false; - - this.view.inputs.setHoverClick(this, - () => ship_buttons.cursorOnShip(ship), - () => ship_buttons.cursorOffShip(ship), - () => ship_buttons.cursorClicked() - ); - } - - get location(): { x: number, y: number } { - return { x: this.x, y: this.y }; - } - - /** - * Flash a damage indicator - */ - setDamageHit() { - this.view.tweens.add({ - targets: this.damage_indicator, - duration: 100, - alpha: 1, - repeat: 2, - yoyo: true - }); - } - - /** - * Move to a given location on screen - */ - moveAt(x: number, y: number, duration: number) { - if (duration && (this.x != x || this.y != y)) { - this.view.animations.addAnimation(this, { x: x, y: y }, duration); - } else { - this.x = x; - this.y = y; - } - } - - /** - * Set the hovered status - */ - setHovered(hovered: boolean) { - this.view.animations.setVisible(this.hover_indicator, hovered, 200); - } - } -}