diff --git a/src/game/MoveFireSimulator.spec.ts b/src/game/MoveFireSimulator.spec.ts new file mode 100644 index 0000000..4d69736 --- /dev/null +++ b/src/game/MoveFireSimulator.spec.ts @@ -0,0 +1,93 @@ +module TS.SpaceTac.Game.Specs { + describe("MoveFireSimulator", function () { + + function simpleWeaponCase(distance = 10, ship_ap = 5, weapon_ap = 3, engine_distance = 5): [Ship, MoveFireSimulator, BaseAction] { + let ship = new Ship(); + TestTools.setShipAP(ship, ship_ap); + TestTools.addEngine(ship, engine_distance); + let action = new FireWeaponAction(new Equipment(), true); + action.equipment.distance = distance; + action.equipment.ap_usage = weapon_ap; + let simulator = new MoveFireSimulator(ship); + return [ship, simulator, action]; + } + + it("finds the best engine to make a move", function () { + let ship = new Ship(); + let simulator = new MoveFireSimulator(ship); + expect(simulator.findBestEngine()).toBe(null); + let engine1 = TestTools.addEngine(ship, 100); + expect(simulator.findBestEngine()).toBe(engine1); + let engine2 = TestTools.addEngine(ship, 120); + let engine3 = TestTools.addEngine(ship, 150); + let engine4 = TestTools.addEngine(ship, 70); + expect(simulator.findBestEngine()).toBe(engine3); + expect(simulator.findBestEngine().distance).toBe(150); + }); + + it("fires directly when in range", function () { + let [ship, simulator, action] = simpleWeaponCase(); + let result = simulator.simulateAction(action, new Target(ship.arena_x + 5, ship.arena_y, null)); + + expect(result.success).toBe(true, 'success'); + expect(result.need_move).toBe(false, 'need_move'); + expect(result.need_fire).toBe(true, 'need_fire'); + expect(result.can_fire).toBe(true, 'can_fire'); + expect(result.total_fire_ap).toBe(3, 'total_fire_ap'); + + expect(result.parts).toEqual([ + { action: jasmine.objectContaining({ code: "fire-null" }), target: new Target(ship.arena_x + 5, ship.arena_y, null), ap: 3 } + ]); + }); + + it("can't fire when in range, but not enough AP", function () { + let [ship, simulator, action] = simpleWeaponCase(10, 2, 3); + let result = simulator.simulateAction(action, new Target(ship.arena_x + 5, ship.arena_y, null)); + expect(result.success).toBe(true, 'success'); + expect(result.need_move).toBe(false, 'need_move'); + expect(result.need_fire).toBe(true, 'need_fire'); + expect(result.can_fire).toBe(false, 'can_fire'); + expect(result.total_fire_ap).toBe(3, 'total_fire_ap'); + + expect(result.parts).toEqual([ + { action: jasmine.objectContaining({ code: "fire-null" }), target: new Target(ship.arena_x + 5, ship.arena_y, null), ap: 3 } + ]); + }); + + it("moves straight to get within range", function () { + let [ship, simulator, action] = simpleWeaponCase(); + let result = simulator.simulateAction(action, new Target(ship.arena_x + 15, ship.arena_y, null)); + expect(result.success).toBe(true, 'success'); + expect(result.need_move).toBe(true, 'need_move'); + expect(result.can_end_move).toBe(true, 'can_end_move'); + expect(result.move_location).toEqual(new Target(ship.arena_x + 5, ship.arena_y, null)); + expect(result.total_move_ap).toBe(1); + expect(result.need_fire).toBe(true, 'need_fire'); + expect(result.can_fire).toBe(true, 'can_fire'); + expect(result.total_fire_ap).toBe(3, 'total_fire_ap'); + + expect(result.parts).toEqual([ + { action: jasmine.objectContaining({ code: "move" }), target: new Target(ship.arena_x + 5, ship.arena_y, null), ap: 1 }, + { action: jasmine.objectContaining({ code: "fire-null" }), target: new Target(ship.arena_x + 15, ship.arena_y, null), ap: 3 } + ]); + }); + + it("moves to get in range, even if not enough AP to fire", function () { + let [ship, simulator, action] = simpleWeaponCase(8, 3, 2, 5); + let result = simulator.simulateAction(action, new Target(ship.arena_x + 18, ship.arena_y, null)); + expect(result.success).toBe(true, 'success'); + expect(result.need_move).toBe(true, 'need_move'); + expect(result.can_end_move).toBe(true, 'can_end_move'); + expect(result.move_location).toEqual(new Target(ship.arena_x + 10, ship.arena_y, null)); + expect(result.total_move_ap).toBe(2); + expect(result.need_fire).toBe(true, 'need_fire'); + expect(result.can_fire).toBe(false, 'can_fire'); + expect(result.total_fire_ap).toBe(2, 'total_fire_ap'); + + expect(result.parts).toEqual([ + { action: jasmine.objectContaining({ code: "move" }), target: new Target(ship.arena_x + 10, ship.arena_y, null), ap: 2 }, + { action: jasmine.objectContaining({ code: "fire-null" }), target: new Target(ship.arena_x + 18, ship.arena_y, null), ap: 2 } + ]); + }); + }); +} diff --git a/src/game/MoveFireSimulator.ts b/src/game/MoveFireSimulator.ts new file mode 100644 index 0000000..d0fe3d9 --- /dev/null +++ b/src/game/MoveFireSimulator.ts @@ -0,0 +1,101 @@ +module TS.SpaceTac.Game { + + /** + * A single action in the sequence result from the simulator + */ + type MoveFirePart = { + action: BaseAction + target: Target + ap: number + } + + /** + * A simulation result + */ + class MoveFireResult { + // Simulation success, false only if no route can be found + success = false + // Ideal successive parts to make the full move+fire + parts: MoveFirePart[] = [] + + need_move = false + can_move = false + can_end_move = false + total_move_ap = 0 + move_location = new Target(0, 0, null) + + need_fire = false + can_fire = false + total_fire_ap = 0 + fire_location = new Target(0, 0, null) + }; + + /** + * Utility to simulate a move+fire action. + * + * This is also a helper to bring a ship in range to fire a weapon. + */ + export class MoveFireSimulator { + ship: Ship; + + constructor(ship: Ship) { + this.ship = ship; + } + + /** + * Find the best available engine for moving + */ + findBestEngine(): Equipment | null { + let engines = this.ship.listEquipment(SlotType.Engine); + if (engines.length == 0) { + return null; + } else { + engines.sort((a, b) => cmp(b.distance, a.distance)); + return engines[0]; + } + } + + /** + * Simulate a given action on a given valid target. + */ + simulateAction(action: BaseAction, target: Target): MoveFireResult { + let dx = target.x - this.ship.arena_x; + let dy = target.y - this.ship.arena_y; + let distance = Math.sqrt(dx * dx + dy * dy); + let result = new MoveFireResult(); + let ap = this.ship.ap_current.current; + + if (distance > action.getRangeRadius(this.ship)) { + result.need_move = true; + let move_distance = distance - action.getRangeRadius(this.ship); + let move_target = new Target(this.ship.arena_x + dx * move_distance / distance, this.ship.arena_y + dy * move_distance / distance, null); + let engine = this.findBestEngine(); + if (engine) { + result.total_move_ap = engine.action.getActionPointsUsage(this.ship.getBattle(), this.ship, move_target); + result.can_move = ap > 0; + result.can_end_move = result.total_move_ap <= ap; + result.move_location = move_target; + result.parts.push({ action: engine.action, target: move_target, ap: result.total_move_ap }); + + ap -= result.total_move_ap; + distance -= move_distance; + } + } + + if (distance <= action.getRangeRadius(this.ship)) { + result.success = true; + if (!(action instanceof MoveAction)) { + result.need_fire = true; + result.total_fire_ap = action.getActionPointsUsage(this.ship.getBattle(), this.ship, target); + result.can_fire = result.total_fire_ap <= ap; + result.fire_location = target; + result.parts.push({ action: action, target: target, ap: result.total_fire_ap }); + } + } else { + result.success = false; + } + + return result; + } + } +} diff --git a/src/game/TestTools.ts b/src/game/TestTools.ts index 835ecf8..ed327d7 100644 --- a/src/game/TestTools.ts +++ b/src/game/TestTools.ts @@ -21,10 +21,10 @@ module TS.SpaceTac.Game { } // Get or add an equipment of a given slot type - static getOrGenEquipment(ship: Ship, slot: SlotType, template: LootTemplate): Equipment { + static getOrGenEquipment(ship: Ship, slot: SlotType, template: LootTemplate, force_generate = false): Equipment { var equipped = ship.listEquipment(slot); var equipment: Equipment; - if (equipped.length === 0) { + if (force_generate || equipped.length === 0) { equipment = template.generateFixed(0); ship.addSlot(slot).attach(equipment); } else { @@ -36,7 +36,7 @@ module TS.SpaceTac.Game { // Add an engine, allowing a ship to move *distance*, for each action points static addEngine(ship: Ship, distance: number): Equipment { - var equipment = this.getOrGenEquipment(ship, SlotType.Engine, new Equipments.ConventionalEngine()); + var equipment = this.getOrGenEquipment(ship, SlotType.Engine, new Equipments.ConventionalEngine(), true); equipment.ap_usage = 1; equipment.distance = distance; return equipment; diff --git a/src/view/battle/BattleView.ts b/src/view/battle/BattleView.ts index a11af03..5aa8176 100644 --- a/src/view/battle/BattleView.ts +++ b/src/view/battle/BattleView.ts @@ -53,6 +53,10 @@ module TS.SpaceTac.View { this.ship_hovered = null; this.log_processor = null; this.background = null; + + if (typeof window != "undefined") { + (window).battle = this.battle; + } } // Create view graphics diff --git a/src/view/battle/Targetting.ts b/src/view/battle/Targetting.ts index de235b0..879d8c5 100644 --- a/src/view/battle/Targetting.ts +++ b/src/view/battle/Targetting.ts @@ -128,6 +128,7 @@ module TS.SpaceTac.View { while (this.ap_indicators.length < count) { let indicator = new Phaser.Image(this.battleview.game, 0, 0, "battle-arena-ap-indicator"); indicator.anchor.set(0.5, 0.5); + indicator.scale.set(0.5, 0.5); this.battleview.arena.addChild(indicator); this.ap_indicators.push(indicator); }