diff --git a/.vscode/settings.json b/.vscode/settings.json index 0763451..6d81f78 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -2,5 +2,13 @@ "typescript.tsdk": "./node_modules/typescript/lib", "editor.rulers": [ 120 - ] + ], + "files.exclude": { + "**/.git": true, + "**/.svn": true, + "**/.hg": true, + "**/CVS": true, + "**/.DS_Store": true, + "node_modules": true + } } \ No newline at end of file diff --git a/TODO.md b/TODO.md index 505abb5..8aa5789 100644 --- a/TODO.md +++ b/TODO.md @@ -37,6 +37,7 @@ Battle * Add a voluntary retreat option * Remove dead ships from ship list and play order * Add quick animation of playing ship indicator, on ship change +* Display a hint when a move-fire simulation failed (cannot enter exclusion area for example) * Display effects description instead of attribute changes * Display radius and power usage hints for area effects on action icon hover + add confirmation ? * Any displayed info should be based on a ship copy stored in ArenaShip, and in sync with current log index (not the game state ship) @@ -57,14 +58,13 @@ Ships models and equipments * Add permanent effects and actions to ship models * Add critical hit/miss * Add damage over time effect (tricky to make intuitive) -* Safety margin should only be applied on ships, not arena borders (which should be fixed) * Move distance should increase with maneuvrability * Chance to hit should increase with precision * Add actions with cost dependent of distance (like current move actions) * Add "cone" targetting * Add disc targetting (for some jump move actions) * Add "chain" effects -* Add "forced move" effects (like a gravity well) +* RepelEffect should apply on ships in a good order (distance decreasing) * Add hull points to drones and make them take area damage * "Shield Transfer" has no quality offsets diff --git a/graphics/exported/equipment/gravitshield.png b/graphics/exported/equipment/gravitshield.png index 68b259e..020d23b 100644 Binary files a/graphics/exported/equipment/gravitshield.png and b/graphics/exported/equipment/gravitshield.png differ diff --git a/graphics/ui/actions.svg b/graphics/ui/actions.svg index 1a5411f..91eb120 100644 --- a/graphics/ui/actions.svg +++ b/graphics/ui/actions.svg @@ -16,13 +16,25 @@ version="1.1" inkscape:version="0.92.1 r15371" sodipodi:docname="actions.svg" - inkscape:export-filename="/home/michael/workspace/perso/spacetac/graphics/exported/equipment/voidhawkengine.png" + inkscape:export-filename="/home/michael/workspace/perso/spacetac/graphics/exported/equipment/gravitshield.png" inkscape:export-xdpi="90" inkscape:export-ydpi="90" viewBox="0 0 256 256" enable-background="new"> + + + + @@ -63,23 +75,11 @@ offset="1" id="stop4864" /> - - - - + x="-0.5" + width="2" + y="-0.5" + height="2"> + - - - + id="g5114"> - - - - - - + + + style="display:none"> diff --git a/graphics/ui/title.svg b/graphics/ui/title.svg index 004e6f5..ad9184f 100644 --- a/graphics/ui/title.svg +++ b/graphics/ui/title.svg @@ -170,7 +170,8 @@ y1="143.13321" x2="259.57538" y2="95.291969" - gradientUnits="userSpaceOnUse" /> + gradientUnits="userSpaceOnUse" + gradientTransform="translate(6.0768709,2.3920799)" /> + height="100%" + inkscape:export-filename="/home/michael/workspace/perso/spacetac/out/assets/images/menu/title1.png" + inkscape:export-xdpi="96" + inkscape:export-ydpi="96" /> + inkscape:connector-curvature="0" + inkscape:export-filename="/home/michael/workspace/perso/spacetac/out/assets/images/menu/title1.png" + inkscape:export-xdpi="96" + inkscape:export-ydpi="96" /> SpaceTac + x="122.79533" + y="134.41782" + style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:56.44444656px;line-height:6.61000013px;font-family:DAGGERSQUARE;-inkscape-font-specification:DAGGERSQUARE;fill:url(#radialGradient5288);fill-opacity:1;stroke:url(#linearGradient5254);stroke-width:0.96499997;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1">SpaceTac + ry="3.60095" + inkscape:export-filename="/home/michael/workspace/perso/spacetac/out/assets/images/menu/title1.png" + inkscape:export-xdpi="96" + inkscape:export-ydpi="96" /> a tactical turn-based RPG + style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-family:DAGGERSQUARE;-inkscape-font-specification:DAGGERSQUARE;fill:#b2b2b2;fill-opacity:1;stroke-width:0.26458332px">a tactical turn-based RPG + transform="translate(-1.0583333)" + inkscape:export-filename="/home/michael/workspace/perso/spacetac/out/assets/images/menu/title1.png" + inkscape:export-xdpi="96" + inkscape:export-ydpi="96"> + height="100%" + inkscape:export-filename="/home/michael/workspace/perso/spacetac/out/assets/images/menu/title1.png" + inkscape:export-xdpi="96" + inkscape:export-ydpi="96" /> + height="100%" + inkscape:export-filename="/home/michael/workspace/perso/spacetac/out/assets/images/menu/title1.png" + inkscape:export-xdpi="96" + inkscape:export-ydpi="96" /> + height="100%" + inkscape:export-filename="/home/michael/workspace/perso/spacetac/out/assets/images/menu/title1.png" + inkscape:export-xdpi="96" + inkscape:export-ydpi="96" /> + sodipodi:nodetypes="cc" + inkscape:export-filename="/home/michael/workspace/perso/spacetac/out/assets/images/menu/title1.png" + inkscape:export-xdpi="96" + inkscape:export-ydpi="96" /> New game + x="48.484272" + y="197.66304" + style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:14.11111069px;font-family:DAGGERSQUARE;-inkscape-font-specification:DAGGERSQUARE;fill:#529aee;fill-opacity:1;stroke-width:0.26458332px">New game Load game + x="215.93364" + y="198.09712" + style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:14.11111069px;font-family:DAGGERSQUARE;-inkscape-font-specification:DAGGERSQUARE;fill:#529aee;fill-opacity:1;stroke-width:0.26458332px">Load game Quick battle + x="383.20383" + y="198.09712" + style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:14.11111069px;font-family:DAGGERSQUARE;-inkscape-font-specification:DAGGERSQUARE;fill:#4b8ad4;fill-opacity:1;stroke-width:0.26458332px">Quick battle { fleet.setBattle(this); diff --git a/src/core/BattleCheats.spec.ts b/src/core/BattleCheats.spec.ts new file mode 100644 index 0000000..b4a165e --- /dev/null +++ b/src/core/BattleCheats.spec.ts @@ -0,0 +1,33 @@ +module TS.SpaceTac.Specs { + describe("BattleCheats", function () { + it("wins a battle", function () { + let battle = Battle.newQuickRandom(); + + battle.cheats.win(); + expect(battle.ended).toBe(true, "ended"); + expect(battle.outcome.winner).toBe(battle.fleets[0], "winner"); + expect(battle.log.events.filter(event => event instanceof DeathEvent).map(event => event.ship)).toEqual(battle.fleets[1].ships, "all mark dead"); + expect(any(battle.fleets[1].ships, ship => !ship.alive)).toBe(false, "all restored"); + }) + + it("loses a battle", function () { + let battle = Battle.newQuickRandom(); + + battle.cheats.lose(); + expect(battle.ended).toBe(true, "ended"); + expect(battle.outcome.winner).toBe(battle.fleets[1], "winner"); + expect(battle.log.events.filter(event => event instanceof DeathEvent).map(event => event.ship)).toEqual(battle.fleets[0].ships, "all mark dead"); + expect(any(battle.fleets[0].ships, ship => !ship.alive)).toBe(false, "all restored"); + }) + + it("adds an equipment", function () { + let battle = new Battle(); + battle.playing_ship = new Ship(); + battle.playing_ship.upgradeSkill("skill_materials"); + + expect(battle.playing_ship.listEquipment()).toEqual([]); + battle.cheats.equip("Iron Hull"); + expect(battle.playing_ship.listEquipment()).toEqual([jasmine.objectContaining({name: "Iron Hull", level: 1})]); + }) + }) +} diff --git a/src/core/BattleCheats.ts b/src/core/BattleCheats.ts new file mode 100644 index 0000000..f162b89 --- /dev/null +++ b/src/core/BattleCheats.ts @@ -0,0 +1,58 @@ +module TS.SpaceTac { + /** + * Cheat helpers for current battle + * + * May be used from the console to help development + */ + export class BattleCheats { + battle: Battle + player: Player + + constructor(battle: Battle, player: Player) { + this.battle = battle; + this.player = player; + } + + /** + * Make player win the current battle + */ + win(): void { + iforeach(this.battle.iships(), ship => { + if (ship.fleet.player != this.player) { + ship.setDead(); + } + }); + this.battle.endBattle(this.player.fleet); + } + + /** + * Make player lose the current battle + */ + lose(): void { + iforeach(this.battle.iships(), ship => { + if (ship.fleet.player == this.player) { + ship.setDead(); + } + }); + this.battle.endBattle(first(this.battle.fleets, fleet => fleet.player != this.player)); + } + + /** + * Add an equipment to current playing ship + */ + equip(name: string): void { + let ship = this.battle.playing_ship; + if (ship) { + let generator = new LootGenerator(); + generator.setTemplateFilter(template => template.name == name); + + let equipment = generator.generateHighest(ship.skills); + if (equipment) { + let slot_type = nn(equipment.slot_type); + let slot = ship.getFreeSlot(slot_type) || ship.addSlot(slot_type); + slot.attach(equipment); + } + } + } + } +} diff --git a/src/core/Drone.ts b/src/core/Drone.ts index 85194c1..ec26828 100644 --- a/src/core/Drone.ts +++ b/src/core/Drone.ts @@ -30,6 +30,13 @@ module TS.SpaceTac { this.duration = base_duration; } + /** + * Return the current location of the drone + */ + get location(): ArenaLocation { + return new ArenaLocation(this.x, this.y); + } + /** * Get a textual description of this drone */ diff --git a/src/core/ExclusionAreas.spec.ts b/src/core/ExclusionAreas.spec.ts new file mode 100644 index 0000000..fda2581 --- /dev/null +++ b/src/core/ExclusionAreas.spec.ts @@ -0,0 +1,43 @@ +module TS.SpaceTac.Specs { + describe("ExclusionAreas", function () { + it("constructs from a ship or battle", function () { + 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); + expect(exclusion.hard_border).toEqual(17); + expect(exclusion.effective_obstacle).toEqual(31); + expect(exclusion.obstacles).toEqual([new ArenaLocationAngle(12, 5), new ArenaLocationAngle(43, 89)]); + + exclusion = ExclusionAreas.fromBattle(battle, [ship1], 120); + expect(exclusion.hard_border).toEqual(17); + expect(exclusion.effective_obstacle).toEqual(120); + expect(exclusion.obstacles).toEqual([new ArenaLocationAngle(43, 89)]); + + exclusion = ExclusionAreas.fromBattle(battle, [ship2], 10); + expect(exclusion.hard_border).toEqual(17); + expect(exclusion.effective_obstacle).toEqual(31); + expect(exclusion.obstacles).toEqual([new ArenaLocationAngle(12, 5)]); + + exclusion = ExclusionAreas.fromShip(ship1); + expect(exclusion.hard_border).toEqual(17); + expect(exclusion.effective_obstacle).toEqual(31); + expect(exclusion.obstacles).toEqual([new ArenaLocationAngle(43, 89)]); + + exclusion = ExclusionAreas.fromShip(ship2, 99); + expect(exclusion.hard_border).toEqual(17); + expect(exclusion.effective_obstacle).toEqual(99); + expect(exclusion.obstacles).toEqual([new ArenaLocationAngle(12, 5)]); + + exclusion = ExclusionAreas.fromShip(ship2, 10, false); + expect(exclusion.hard_border).toEqual(17); + expect(exclusion.effective_obstacle).toEqual(31); + expect(exclusion.obstacles).toEqual([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 new file mode 100644 index 0000000..b21cdab --- /dev/null +++ b/src/core/ExclusionAreas.ts @@ -0,0 +1,96 @@ +module TS.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(), 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 e28b233..cc00b0b 100644 --- a/src/core/MoveFireSimulator.spec.ts +++ b/src/core/MoveFireSimulator.spec.ts @@ -112,6 +112,7 @@ module TS.SpaceTac.Specs { let ship1 = battle.fleets[0].addShip(); let moveaction = nn(simulator.findBestEngine()).action; moveaction.safety_distance = 30; + battle.ship_separation = 30; expect(simulator.getApproach(moveaction, Target.newFromLocation(350, 200), 100)).toBe(ApproachSimulationError.NO_MOVE_NEEDED); expect(simulator.getApproach(moveaction, Target.newFromLocation(400, 200), 100)).toBe(ApproachSimulationError.NO_MOVE_NEEDED); diff --git a/src/core/MoveFireSimulator.ts b/src/core/MoveFireSimulator.ts index 984da5f..1648abd 100644 --- a/src/core/MoveFireSimulator.ts +++ b/src/core/MoveFireSimulator.ts @@ -68,7 +68,8 @@ module TS.SpaceTac { * Check that a move action can reach a given destination */ canMoveTo(action: MoveAction, target: Target): boolean { - return action.checkLocationTarget(this.ship, target) == target; + let checked = action.checkLocationTarget(this.ship, target); + return checked != null && checked.x == target.x && checked.y == target.y; } /** @@ -133,7 +134,7 @@ module TS.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, 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/Ship.spec.ts b/src/core/Ship.spec.ts index 6c1c763..e79f028 100644 --- a/src/core/Ship.spec.ts +++ b/src/core/Ship.spec.ts @@ -15,7 +15,8 @@ module TS.SpaceTac.Specs { }); it("moves and computes facing angle", function () { - var ship = new Ship(null, "Test"); + let ship = new Ship(null, "Test"); + let engine = TestTools.addEngine(ship, 50); ship.setArenaFacingAngle(0); ship.setArenaPosition(50, 50); @@ -23,43 +24,49 @@ module TS.SpaceTac.Specs { expect(ship.arena_y).toEqual(50); expect(ship.arena_angle).toEqual(0); - ship.moveTo(51, 50); + ship.moveTo(51, 50, engine); expect(ship.arena_x).toEqual(51); expect(ship.arena_y).toEqual(50); expect(ship.arena_angle).toEqual(0); - ship.moveTo(50, 50); + ship.moveTo(50, 50, engine); expect(ship.arena_angle).toBeCloseTo(3.14159265, 0.00001); - ship.moveTo(51, 51); + ship.moveTo(51, 51, engine); expect(ship.arena_angle).toBeCloseTo(0.785398, 0.00001); - ship.moveTo(51, 52); + ship.moveTo(51, 52, engine); expect(ship.arena_angle).toBeCloseTo(1.5707963, 0.00001); - ship.moveTo(52, 52); + ship.moveTo(52, 52, engine); expect(ship.arena_x).toEqual(52); expect(ship.arena_y).toEqual(52); expect(ship.arena_angle).toEqual(0); - ship.moveTo(52, 50); + ship.moveTo(52, 50, engine); expect(ship.arena_angle).toBeCloseTo(-1.5707963, 0.00001); - ship.moveTo(50, 50); + ship.moveTo(50, 50, engine); expect(ship.arena_angle).toBeCloseTo(3.14159265, 0.00001); let battle = new Battle(); battle.fleets[0].addShip(ship); expect(battle.log.events).toEqual([]); - ship.moveTo(70, 50); - expect(battle.log.events).toEqual([new MoveEvent(ship, new ArenaLocationAngle(50, 50, Math.PI), new ArenaLocationAngle(70, 50, 0))]); + ship.moveTo(70, 50, engine); + expect(battle.log.events).toEqual([new MoveEvent(ship, new ArenaLocationAngle(50, 50, Math.PI), new ArenaLocationAngle(70, 50, 0), engine)]); + battle.log.clear(); ship.rotate(2.1); expect(battle.log.events).toEqual([ - new MoveEvent(ship, new ArenaLocationAngle(50, 50, Math.PI), new ArenaLocationAngle(70, 50, 0)), new MoveEvent(ship, new ArenaLocationAngle(70, 50, 0), new ArenaLocationAngle(70, 50, 2.1)) ]); + + battle.log.clear(); + ship.moveTo(0, 0, null); + expect(battle.log.events).toEqual([ + new MoveEvent(ship, new ArenaLocationAngle(70, 50, 2.1), new ArenaLocationAngle(0, 0, 2.1)) + ]); }); it("applies equipment cooldown", function () { diff --git a/src/core/Ship.ts b/src/core/Ship.ts index f7605ec..28ca951 100644 --- a/src/core/Ship.ts +++ b/src/core/Ship.ts @@ -405,20 +405,25 @@ module TS.SpaceTac { /** * Rotate the ship in place to face a direction */ - rotate(angle: number, log = true) { + rotate(angle: number, engine: Equipment | null = null, log = true) { if (angle != this.arena_angle) { let start = copy(this.location); this.setArenaFacingAngle(angle); if (log) { - this.addBattleEvent(new MoveEvent(this, start, copy(this.location))); + this.addBattleEvent(new MoveEvent(this, start, copy(this.location), engine)); } } } - // Move toward a location - // This does not check or consume action points - moveTo(x: number, y: number, log: boolean = true): void { + /** + * Move the ship to another location + * + * This does not check or consume action points, but will update area effects (for this ship and the others). + * + * If *engine* is specified, the facing angle will be updated to simulate an engine maneuver. + */ + moveTo(x: number, y: number, engine: Equipment | null = null, log = true): void { let dx = x - this.arena_x; let dy = y - this.arena_y; if (dx != 0 || dy != 0) { @@ -428,12 +433,15 @@ module TS.SpaceTac { let old_impacted_ships = area_effects.map(action => action.getAffectedShips(this)); let old_area_effects = this.getActiveEffects().area; - let angle = Math.atan2(dy, dx); - this.setArenaFacingAngle(angle); + if (engine) { + let angle = Math.atan2(dy, dx); + this.setArenaFacingAngle(angle); + } + this.setArenaPosition(x, y); if (log) { - this.addBattleEvent(new MoveEvent(this, start, copy(this.location))); + this.addBattleEvent(new MoveEvent(this, start, copy(this.location), engine)); } let new_impacted_ships = area_effects.map(action => action.getAffectedShips(this)); diff --git a/src/core/actions/FireWeaponAction.spec.ts b/src/core/actions/FireWeaponAction.spec.ts index b196b48..a058f75 100644 --- a/src/core/actions/FireWeaponAction.spec.ts +++ b/src/core/actions/FireWeaponAction.spec.ts @@ -4,12 +4,15 @@ module TS.SpaceTac { describe("FireWeaponAction", function () { it("constructs correctly", function () { let equipment = new Equipment(SlotType.Weapon, "testweapon"); - let action = new FireWeaponAction(equipment); + let action = new FireWeaponAction(equipment, 4, 30, 10); expect(action.code).toEqual("fire-testweapon"); expect(action.name).toEqual("Fire"); expect(action.equipment).toBe(equipment); expect(action.needs_target).toBe(true); + + action = new FireWeaponAction(equipment, 4, 0, 10); + expect(action.needs_target).toBe(false); }); it("applies effects to alive ships in blast radius", function () { diff --git a/src/core/actions/FireWeaponAction.ts b/src/core/actions/FireWeaponAction.ts index 42a939a..4d43980 100644 --- a/src/core/actions/FireWeaponAction.ts +++ b/src/core/actions/FireWeaponAction.ts @@ -21,7 +21,7 @@ module TS.SpaceTac { equipment: Equipment; constructor(equipment: Equipment, power = 1, range = 0, blast = 0, effects: BaseEffect[] = [], name = "Fire") { - super("fire-" + equipment.code, name, true, equipment); + super("fire-" + equipment.code, name, range > 0, equipment); this.power = power; this.range = range; @@ -80,9 +80,14 @@ module TS.SpaceTac { return result; } - protected customApply(ship: Ship, target: Target) { + protected customApply(ship: Ship, target: Target | null) { + if (!target) { + // Self-target + target = Target.newFromShip(ship); + } + // Face the target - ship.rotate(Target.newFromShip(ship).getAngleTo(target)); + ship.rotate(Target.newFromShip(ship).getAngleTo(target), first(ship.listEquipment(SlotType.Engine), () => true)); // Fire event ship.addBattleEvent(new FireEvent(ship, this.equipment, target)); diff --git a/src/core/actions/MoveAction.spec.ts b/src/core/actions/MoveAction.spec.ts index 34e9380..c4d9e7b 100644 --- a/src/core/actions/MoveAction.spec.ts +++ b/src/core/actions/MoveAction.spec.ts @@ -83,27 +83,27 @@ module TS.SpaceTac { var ship = battle.fleets[0].ships[0]; var enemy = battle.fleets[1].ships[0]; TestTools.setShipAP(ship, 100); - ship.setArenaPosition(5, 5); - enemy.setArenaPosition(10, 5); + ship.setArenaPosition(500, 500); + enemy.setArenaPosition(1000, 500); var action = new MoveAction(new Equipment()); - action.distance_per_power = 10; - action.safety_distance = 2; + action.distance_per_power = 1000; + action.safety_distance = 200; - var result = action.checkLocationTarget(ship, Target.newFromLocation(7, 5)); - expect(result).toEqual(Target.newFromLocation(7, 5)); + var result = action.checkLocationTarget(ship, Target.newFromLocation(700, 500)); + expect(result).toEqual(Target.newFromLocation(700, 500)); - result = action.checkLocationTarget(ship, Target.newFromLocation(8, 5)); - expect(result).toEqual(Target.newFromLocation(8, 5)); + result = action.checkLocationTarget(ship, Target.newFromLocation(800, 500)); + expect(result).toEqual(Target.newFromLocation(800, 500)); - result = action.checkLocationTarget(ship, Target.newFromLocation(9, 5)); - expect(result).toEqual(Target.newFromLocation(8, 5)); + result = action.checkLocationTarget(ship, Target.newFromLocation(900, 500)); + expect(result).toEqual(Target.newFromLocation(800, 500)); - result = action.checkLocationTarget(ship, Target.newFromLocation(10, 5)); - expect(result).toEqual(Target.newFromLocation(8, 5)); + result = action.checkLocationTarget(ship, Target.newFromLocation(1000, 500)); + expect(result).toEqual(Target.newFromLocation(800, 500)); - result = action.checkLocationTarget(ship, Target.newFromLocation(12, 5)); - expect(result).toEqual(Target.newFromLocation(12, 5)); + result = action.checkLocationTarget(ship, Target.newFromLocation(1200, 500)); + expect(result).toEqual(Target.newFromLocation(1200, 500)); }); it("exclusion radius is applied correctly over two ships", function () { @@ -112,15 +112,15 @@ module TS.SpaceTac { var enemy1 = battle.fleets[1].ships[0]; var enemy2 = battle.fleets[1].ships[1]; TestTools.setShipAP(ship, 100); - enemy1.setArenaPosition(0, 80); - enemy2.setArenaPosition(0, 100); + enemy1.setArenaPosition(0, 800); + enemy2.setArenaPosition(0, 1000); var action = new MoveAction(new Equipment()); action.distance_per_power = 1000; - action.safety_distance = 15; + action.safety_distance = 150; - var result = action.checkLocationTarget(ship, Target.newFromLocation(0, 110)); - expect(result).toEqual(Target.newFromLocation(0, 65)); + var result = action.checkLocationTarget(ship, Target.newFromLocation(0, 1100)); + expect(result).toEqual(Target.newFromLocation(0, 650)); }); it("exclusion radius does not make the ship go back", function () { @@ -129,17 +129,17 @@ module TS.SpaceTac { var enemy1 = battle.fleets[1].ships[0]; var enemy2 = battle.fleets[1].ships[1]; TestTools.setShipAP(ship, 100); - enemy1.setArenaPosition(0, 50); - enemy2.setArenaPosition(0, 80); + enemy1.setArenaPosition(0, 500); + enemy2.setArenaPosition(0, 800); var action = new MoveAction(new Equipment()); action.distance_per_power = 1000; - action.safety_distance = 60; + action.safety_distance = 600; - let result = action.checkLocationTarget(ship, Target.newFromLocation(0, 100)); + let result = action.checkLocationTarget(ship, Target.newFromLocation(0, 1000)); expect(result).toBeNull(); - result = action.checkLocationTarget(ship, Target.newFromLocation(0, 140)); - expect(result).toEqual(Target.newFromLocation(0, 140)); + result = action.checkLocationTarget(ship, Target.newFromLocation(0, 1400)); + expect(result).toEqual(Target.newFromLocation(0, 1400)); }); }); } diff --git a/src/core/actions/MoveAction.ts b/src/core/actions/MoveAction.ts index 50e4dd2..851d6c0 100644 --- a/src/core/actions/MoveAction.ts +++ b/src/core/actions/MoveAction.ts @@ -4,7 +4,7 @@ module TS.SpaceTac { // Distance allowed for each power point distance_per_power: number - // Safety distance from other ships and arena borders + // Safety distance from other ships safety_distance: number // Equipment cannot be null (engine) @@ -54,31 +54,21 @@ module TS.SpaceTac { return 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, margin = 0.1): Target { - let battle = ship.getBattle(); - if (battle) { - // Keep out of arena borders - let border = this.safety_distance * 0.5; - target = target.keepInsideRectangle(border, border, - battle.width - border, battle.height - border, - ship.arena_x, ship.arena_y); + applyExclusion(ship: Ship, target: Target): Target { + let exclusion = this.getExclusionAreas(ship); - // Apply collision prevention - let ships = imaterialize(ifilter(battle.iships(true), s => s !== ship)); - ships = ships.sort((a, b) => cmp(a.getDistanceTo(ship), b.getDistanceTo(ship), true)); - ships.forEach(s => { - let new_target = target.moveOutOfCircle(s.arena_x, s.arena_y, this.safety_distance, ship.arena_x, ship.arena_y); - if (target != new_target && s.getDistanceTo(ship) < this.safety_distance) { - // Already inside the nearest ship's exclusion area - target = Target.newFromLocation(ship.arena_x, ship.arena_y); - } else { - target = new_target; - } - }); - } + let destination = exclusion.stopBefore(new ArenaLocation(target.x, target.y), ship.location); + target = Target.newFromLocation(destination.x, destination.y); return target; } @@ -98,7 +88,7 @@ module TS.SpaceTac { } protected customApply(ship: Ship, target: Target) { - ship.moveTo(target.x, target.y); + ship.moveTo(target.x, target.y, this.equipment); } getEffectsDescription(): string { diff --git a/src/core/effects/RepelEffect.spec.ts b/src/core/effects/RepelEffect.spec.ts new file mode 100644 index 0000000..2e31380 --- /dev/null +++ b/src/core/effects/RepelEffect.spec.ts @@ -0,0 +1,40 @@ +module TS.SpaceTac.Specs { + describe("RepelEffect", function () { + it("shows a textual description", function () { + expect(new RepelEffect(34).getDescription()).toEqual("repel ships 34km away"); + }) + + it("repel other ships from a central point", function () { + let battle = new Battle(); + let ship1a = battle.fleets[0].addShip(); + ship1a.setArenaPosition(100, 100); + let ship1b = battle.fleets[0].addShip(); + ship1b.setArenaPosition(250, 100); + let ship2a = battle.fleets[1].addShip(); + ship2a.setArenaPosition(100, 280); + + let effect = new RepelEffect(12); + effect.applyOnShip(ship1a, ship1a); + effect.applyOnShip(ship1b, ship1a); + effect.applyOnShip(ship2a, ship1a); + + expect(ship1a.location).toEqual(new ArenaLocationAngle(100, 100)); + expect(ship1b.location).toEqual(new ArenaLocationAngle(262, 100)); + expect(ship2a.location).toEqual(new ArenaLocationAngle(100, 292)); + }) + + it("does not push a ship inside a hard exclusion area", function () { + 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); + effect.applyOnShip(ship2a, ship1a); + expect(ship2a.location).toEqual(new ArenaLocationAngle(100, 250)); + }) + }) +} diff --git a/src/core/effects/RepelEffect.ts b/src/core/effects/RepelEffect.ts new file mode 100644 index 0000000..3370378 --- /dev/null +++ b/src/core/effects/RepelEffect.ts @@ -0,0 +1,31 @@ +/// + +module TS.SpaceTac { + /** + * Repel ships from a central point + */ + export class RepelEffect extends BaseEffect { + value: number; + + constructor(value = 0) { + super("repel"); + + this.value = value; + } + + applyOnShip(ship: Ship, source: Ship | Drone): boolean { + if (ship != source) { + 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); + ship.moveTo(destination.x, destination.y); + } + return true; + } + + getDescription(): string { + return `repel ships ${this.value}km away`; + } + } +} diff --git a/src/core/equipments/Shields.spec.ts b/src/core/equipments/Shields.spec.ts index 446bc47..041fc96 100644 --- a/src/core/equipments/Shields.spec.ts +++ b/src/core/equipments/Shields.spec.ts @@ -30,33 +30,33 @@ module TS.SpaceTac.Equipments { let equipment = template.generate(1); expect(equipment.requirements).toEqual({ "skill_gravity": 2 }); expect(equipment.effects).toEqual([ - new AttributeEffect("shield_capacity", 160), - new AttributeEffect("precision", -1), + new AttributeEffect("shield_capacity", 80), ]); + expect(equipment.action).toEqual(new FireWeaponAction(equipment, 2, 0, 300, [new RepelEffect(100)])); expect(equipment.price).toEqual(140); equipment = template.generate(2); expect(equipment.requirements).toEqual({ "skill_gravity": 5 }); expect(equipment.effects).toEqual([ - new AttributeEffect("shield_capacity", 190), - new AttributeEffect("precision", -2), + new AttributeEffect("shield_capacity", 110), ]); + expect(equipment.action).toEqual(new FireWeaponAction(equipment, 2, 0, 310, [new RepelEffect(105)])); expect(equipment.price).toEqual(320); equipment = template.generate(3); expect(equipment.requirements).toEqual({ "skill_gravity": 8 }); expect(equipment.effects).toEqual([ - new AttributeEffect("shield_capacity", 220), - new AttributeEffect("precision", -3), + new AttributeEffect("shield_capacity", 140), ]); + expect(equipment.action).toEqual(new FireWeaponAction(equipment, 2, 0, 320, [new RepelEffect(110)])); expect(equipment.price).toEqual(680); equipment = template.generate(10); expect(equipment.requirements).toEqual({ "skill_gravity": 29 }); expect(equipment.effects).toEqual([ - new AttributeEffect("shield_capacity", 430), - new AttributeEffect("precision", -10), + new AttributeEffect("shield_capacity", 350), ]); + expect(equipment.action).toEqual(new FireWeaponAction(equipment, 2, 0, 390, [new RepelEffect(145)])); expect(equipment.price).toEqual(8240); }); @@ -74,7 +74,7 @@ module TS.SpaceTac.Equipments { equipment = template.generate(2); expect(equipment.requirements).toEqual({ "skill_antimatter": 4 }); expect(equipment.effects).toEqual([ - new AttributeEffect("shield_capacity", 165), + new AttributeEffect("shield_capacity", 175), new AttributeEffect("power_generation", -1), ]); expect(equipment.price).toEqual(460); @@ -82,7 +82,7 @@ module TS.SpaceTac.Equipments { equipment = template.generate(3); expect(equipment.requirements).toEqual({ "skill_antimatter": 6 }); expect(equipment.effects).toEqual([ - new AttributeEffect("shield_capacity", 200), + new AttributeEffect("shield_capacity", 220), new AttributeEffect("power_generation", -1), ]); expect(equipment.price).toEqual(780); @@ -90,7 +90,7 @@ module TS.SpaceTac.Equipments { equipment = template.generate(10); expect(equipment.requirements).toEqual({ "skill_antimatter": 20 }); expect(equipment.effects).toEqual([ - new AttributeEffect("shield_capacity", 445), + new AttributeEffect("shield_capacity", 535), new AttributeEffect("power_generation", -3), ]); expect(equipment.price).toEqual(7500); diff --git a/src/core/equipments/Shields.ts b/src/core/equipments/Shields.ts index fd3c839..7cfeb18 100644 --- a/src/core/equipments/Shields.ts +++ b/src/core/equipments/Shields.ts @@ -12,11 +12,13 @@ module TS.SpaceTac.Equipments { export class GravitShield extends LootTemplate { constructor() { - super(SlotType.Shield, "Gravit Shield", "A shield with micro-gravity wells to help absorb damage", 140, 180); + super(SlotType.Shield, "Gravit Shield", "A shield able to repel damage and enemies using micro-gravity wells", 140, 180); this.setSkillsRequirements({ "skill_gravity": istep(2, irepeat(3)) }); - this.addAttributeEffect("shield_capacity", istep(160, irepeat(30))); - this.addAttributeEffect("precision", istep(-1, irepeat(-1))); + this.addAttributeEffect("shield_capacity", istep(80, irepeat(30))); + this.addFireAction(irepeat(2), 0, istep(300, irepeat(10)), [ + new EffectTemplate(new RepelEffect(), { value: istep(100, irepeat(5)) }) + ]); } } @@ -25,7 +27,7 @@ module TS.SpaceTac.Equipments { super(SlotType.Shield, "Inverter Shield", "An antimatter shield that tries to cancel inbound energy", 300, 160); this.setSkillsRequirements({ "skill_antimatter": istep(2, irepeat(2)) }); - this.addAttributeEffect("shield_capacity", istep(130, irepeat(35))); + this.addAttributeEffect("shield_capacity", istep(130, irepeat(45))); this.addAttributeEffect("power_generation", istep(-0.2, irepeat(-0.3))); } } diff --git a/src/core/events/MoveEvent.ts b/src/core/events/MoveEvent.ts index d27326e..b980763 100644 --- a/src/core/events/MoveEvent.ts +++ b/src/core/events/MoveEvent.ts @@ -11,15 +11,19 @@ module TS.SpaceTac { // New location end: ArenaLocationAngle - constructor(ship: Ship, start: ArenaLocationAngle, end: ArenaLocationAngle) { + // Engine used + engine: Equipment | null + + constructor(ship: Ship, start: ArenaLocationAngle, end: ArenaLocationAngle, engine: Equipment | null = null) { super("move", ship, Target.newFromLocation(end.x, end.y)); this.start = start; this.end = end; + this.engine = engine; } getReverse(): BaseBattleEvent { - return new MoveEvent(this.ship, this.end, this.start); + return new MoveEvent(this.ship, this.end, this.start, this.engine); } /** diff --git a/src/ui/battle/ArenaShip.ts b/src/ui/battle/ArenaShip.ts index af053f7..c7b7723 100644 --- a/src/ui/battle/ArenaShip.ts +++ b/src/ui/battle/ArenaShip.ts @@ -177,7 +177,7 @@ module TS.SpaceTac.UI { return 0; } else if (event instanceof MoveEvent && !event.initial) { this.moveTo(event.start.x, event.start.y, event.start.angle, false); - let duration = this.moveTo(event.end.x, event.end.y, event.end.angle, true); + let duration = this.moveTo(event.end.x, event.end.y, event.end.angle, true, !!event.engine); return duration; } else { return 0; @@ -248,9 +248,10 @@ module TS.SpaceTac.UI { * * Return the duration of animation */ - moveTo(x: number, y: number, facing_angle: number, animate = true): number { + moveTo(x: number, y: number, facing_angle: number, animate = true, engine = true): number { if (animate) { - let duration = Animations.moveInSpace(this, x, y, facing_angle, this.sprite); + let animation = engine ? Animations.moveInSpace : Animations.moveTo; + let duration = animation(this, x, y, facing_angle, this.sprite); return duration; } else { this.x = x; diff --git a/src/ui/battle/BattleView.ts b/src/ui/battle/BattleView.ts index 96f8d16..3894a3a 100644 --- a/src/ui/battle/BattleView.ts +++ b/src/ui/battle/BattleView.ts @@ -118,22 +118,8 @@ module TS.SpaceTac.UI { // Key mapping this.inputs.bind("t", "Show tactical view", () => this.toggle_tactical_mode.switch(3000)); - this.inputs.bindCheat("w", "Win current battle", () => { - iforeach(this.battle.iships(), ship => { - if (ship.fleet.player != this.player) { - ship.setDead(); - } - }); - this.battle.endBattle(this.player.fleet); - }); - this.inputs.bindCheat("x", "Lose current battle", () => { - iforeach(this.battle.iships(), ship => { - if (ship.fleet.player == this.player) { - ship.setDead(); - } - }); - this.battle.endBattle(first(this.battle.fleets, fleet => fleet.player != this.player)); - }); + this.inputs.bindCheat("w", "Win current battle", () => this.battle.cheats.win()); + this.inputs.bindCheat("x", "Lose current battle", () => this.battle.cheats.lose()); this.inputs.bindCheat("a", "Use AI to play", () => this.playAI()); // Start processing the log diff --git a/src/ui/battle/RangeHint.ts b/src/ui/battle/RangeHint.ts index 01576ec..5201449 100644 --- a/src/ui/battle/RangeHint.ts +++ b/src/ui/battle/RangeHint.ts @@ -57,21 +57,17 @@ module TS.SpaceTac.UI { this.info.drawCircle(location.x, location.y, radius * 2); if (action instanceof MoveAction) { - let safety = action.safety_distance / 2; - this.info.beginFill(nocolor); - this.info.drawRect(0, 0, this.width, safety); - this.info.drawRect(0, this.height - safety, this.width, safety); - this.info.drawRect(0, safety, safety, this.height - safety * 2); - this.info.drawRect(this.width - safety, safety, safety, this.height - safety * 2); + let exclusions = action.getExclusionAreas(ship); - let battle = ship.getBattle(); - if (battle) { - iforeach(battle.iships(true), s => { - if (s !== ship) { - this.info.drawCircle(s.arena_x, s.arena_y, safety * 4); - } - }); - } + this.info.beginFill(nocolor); + this.info.drawRect(0, 0, this.width, exclusions.hard_border); + this.info.drawRect(0, this.height - exclusions.hard_border, this.width, exclusions.hard_border); + this.info.drawRect(0, exclusions.hard_border, exclusions.hard_border, this.height - exclusions.hard_border * 2); + this.info.drawRect(this.width - exclusions.hard_border, exclusions.hard_border, exclusions.hard_border, this.height - exclusions.hard_border * 2); + + exclusions.obstacles.forEach(obstacle => { + this.info.drawCircle(obstacle.x, obstacle.y, exclusions.effective_obstacle * 2); + }); } this.info.visible = true; diff --git a/src/ui/common/Animations.ts b/src/ui/common/Animations.ts index dfaae68..a0ee73f 100644 --- a/src/ui/common/Animations.ts +++ b/src/ui/common/Animations.ts @@ -174,6 +174,23 @@ module TS.SpaceTac.UI { return duration; } + /** + * Move an object linearly to another position + * + * Returns the animation duration. + */ + static moveTo(obj: PhaserGraphics, x: number, y: number, angle: number, rotated_obj = obj, ease = true): number { + let tween_rot = obj.game.tweens.create(rotated_obj); + let duration_rot = Animations.rotationTween(tween_rot, angle, 0.5); + let tween_pos = obj.game.tweens.create(obj); + let duration_pos = arenaDistance(obj, { x: x, y: y }) * 2; + tween_pos.to({ x: x, y: y }, duration_pos, ease ? Phaser.Easing.Quadratic.InOut : undefined); + + tween_rot.start(); + tween_pos.start(); + return Math.max(duration_rot, duration_pos); + } + /** * Make an object move toward a location in space, with a ship-like animation. *