diff --git a/src/index.html b/src/index.html index 06557f8..14dd02e 100644 --- a/src/index.html +++ b/src/index.html @@ -36,7 +36,7 @@ var queryString = new jasmine.QueryString({ getWindowLocation: function() { return window.location; } }); - if (queryString.getParam('onlytests')) { + if (queryString.getParam('onlytests') || queryString.getParam('spec')) { document.getElementById("-space-tac").hidden = true; } else { new SpaceTac.GameRouter(); diff --git a/src/scripts/game/actions/BaseAction.ts b/src/scripts/game/actions/BaseAction.ts index ad8305e..dbd2352 100644 --- a/src/scripts/game/actions/BaseAction.ts +++ b/src/scripts/game/actions/BaseAction.ts @@ -21,8 +21,18 @@ module SpaceTac.Game { // Check basic conditions to know if the ship can use this action at all // Method to reimplement to set conditions - canBeUsed(battle: Battle, ship: Ship): boolean { - return true; + canBeUsed(battle: Battle, ship: Ship, remaining_ap: number = null): boolean { + if (battle && battle.playing_ship !== ship) { + // Ship is not playing + return false; + } + + // Check AP usage + if (remaining_ap === null) { + remaining_ap = ship.ap_current.current; + } + var ap_usage = this.equipment ? this.equipment.ap_usage : 0; + return remaining_ap >= ap_usage; } // Method to check if a target is applicable for this action diff --git a/src/scripts/game/actions/EndTurnAction.ts b/src/scripts/game/actions/EndTurnAction.ts index 7eb8352..8407b7d 100644 --- a/src/scripts/game/actions/EndTurnAction.ts +++ b/src/scripts/game/actions/EndTurnAction.ts @@ -7,10 +7,6 @@ module SpaceTac.Game { super("endturn", false); } - canBeUsed(battle: Battle, ship: Ship): boolean { - return battle.playing_ship === ship; - } - protected customApply(battle: Battle, ship: Ship, target: Target): boolean { battle.advanceToNextShip(); return true; diff --git a/src/scripts/game/actions/FireWeaponAction.ts b/src/scripts/game/actions/FireWeaponAction.ts index d73b239..96048e4 100644 --- a/src/scripts/game/actions/FireWeaponAction.ts +++ b/src/scripts/game/actions/FireWeaponAction.ts @@ -14,10 +14,6 @@ module SpaceTac.Game { this.can_target_space = can_target_space; } - canBeUsed(battle: Battle, ship: Ship): boolean { - return ship.ap_current.current >= this.equipment.ap_usage; - } - checkLocationTarget(battle: Battle, ship: Ship, target: Target): Target { if (this.can_target_space) { target = target.constraintInRange(ship.arena_x, ship.arena_y, this.equipment.distance); diff --git a/src/scripts/game/actions/MoveAction.ts b/src/scripts/game/actions/MoveAction.ts index 55fe1e0..8a6c294 100644 --- a/src/scripts/game/actions/MoveAction.ts +++ b/src/scripts/game/actions/MoveAction.ts @@ -7,8 +7,16 @@ module SpaceTac.Game { super("move", true, equipment); } - canBeUsed(battle: Battle, ship: Ship): boolean { - return ship.ap_current.current > 0; + canBeUsed(battle: Battle, ship: Ship, remaining_ap: number = null): boolean { + if (battle && battle.playing_ship !== ship) { + return false; + } + + // Check AP usage + if (remaining_ap === null) { + remaining_ap = ship.ap_current.current; + } + return remaining_ap > 0.0001; } checkLocationTarget(battle: Battle, ship: Ship, target: Target): Target { diff --git a/src/scripts/game/ai/specs/BullyAI.spec.ts b/src/scripts/game/ai/specs/BullyAI.spec.ts index eb105a4..ce5a4ed 100644 --- a/src/scripts/game/ai/specs/BullyAI.spec.ts +++ b/src/scripts/game/ai/specs/BullyAI.spec.ts @@ -199,6 +199,7 @@ module SpaceTac.Game.AI.Specs { var move = ai.checkBullyMove(ship2, weapon); expect(move).not.toBeNull(); + battle.playing_ship = ai.ship; battle.log.clear(); ai.applyMove(move); diff --git a/src/scripts/game/specs/BaseAction.spec.ts b/src/scripts/game/specs/BaseAction.spec.ts new file mode 100644 index 0000000..eb4f509 --- /dev/null +++ b/src/scripts/game/specs/BaseAction.spec.ts @@ -0,0 +1,33 @@ +/// + +module SpaceTac.Game { + "use strict"; + + describe("BaseAction", function () { + it("check if equipment can be used with remaining AP", function () { + var equipment = new Equipment(SlotType.Armor); + equipment.ap_usage = 3; + var action = new BaseAction("test", false, equipment); + var ship = new Ship(); + ship.addSlot(SlotType.Armor).attach(equipment); + ship.ap_current.setMaximal(10); + + expect(action.canBeUsed(null, ship)).toBe(false); + + ship.ap_current.set(5); + + expect(action.canBeUsed(null, ship)).toBe(true); + expect(action.canBeUsed(null, ship, 4)).toBe(true); + expect(action.canBeUsed(null, ship, 3)).toBe(true); + expect(action.canBeUsed(null, ship, 2)).toBe(false); + + ship.ap_current.set(3); + + expect(action.canBeUsed(null, ship)).toBe(true); + + ship.ap_current.set(2); + + expect(action.canBeUsed(null, ship)).toBe(false); + }); + }); +} diff --git a/src/scripts/game/specs/MoveAction.spec.ts b/src/scripts/game/specs/MoveAction.spec.ts index a1158c5..e0b6f55 100644 --- a/src/scripts/game/specs/MoveAction.spec.ts +++ b/src/scripts/game/specs/MoveAction.spec.ts @@ -49,6 +49,7 @@ module SpaceTac.Game { engine.distance = 1; engine.ap_usage = 1; var action = new MoveAction(engine); + battle.playing_ship = ship; var result = action.apply(battle, ship, Target.newFromLocation(10, 10)); expect(result).toBe(true); diff --git a/src/scripts/view/Preload.ts b/src/scripts/view/Preload.ts index a9afc3b..5591ce0 100644 --- a/src/scripts/view/Preload.ts +++ b/src/scripts/view/Preload.ts @@ -22,6 +22,7 @@ module SpaceTac.View { this.loadImage("battle/actionbar.png"); this.loadImage("battle/action-inactive.png"); this.loadImage("battle/action-active.png"); + this.loadImage("battle/action-fading.png"); this.loadImage("battle/actionpointsempty.png"); this.loadImage("battle/actionpointsfull.png"); this.loadImage("battle/arena/shipspritehover.png"); diff --git a/src/scripts/view/battle/ActionBar.ts b/src/scripts/view/battle/ActionBar.ts index 5773cab..4ad2728 100644 --- a/src/scripts/view/battle/ActionBar.ts +++ b/src/scripts/view/battle/ActionBar.ts @@ -60,6 +60,19 @@ module SpaceTac.View { } } + // Update fading flags + // ap_usage is the consumption of currently selected action + updateFadings(ap_usage: number): void { + var remaining_ap = this.ship.ap_current.current - ap_usage; + if (remaining_ap < 0) { + remaining_ap = 0; + } + + this.actions.forEach((icon: ActionIcon) => { + icon.updateFadingStatus(remaining_ap); + }); + } + // Set action icons from selected ship setShip(ship: Game.Ship): void { var action_bar = this; diff --git a/src/scripts/view/battle/ActionIcon.ts b/src/scripts/view/battle/ActionIcon.ts index e49a01e..49f7d33 100644 --- a/src/scripts/view/battle/ActionIcon.ts +++ b/src/scripts/view/battle/ActionIcon.ts @@ -15,6 +15,12 @@ module SpaceTac.View { // Related game action action: Game.BaseAction; + // True if the action can be used + active: boolean; + + // True if an action is currently selected, and this one won't be available after its use + fading: boolean; + // Current targetting private targetting: Targetting; @@ -24,6 +30,9 @@ module SpaceTac.View { // Layer applied when the action is active private layer_active: Phaser.Image; + // Layer applied when the action will become unavailable if another action is played + private layer_fading: Phaser.Image; + // Create an icon for a single ship action constructor(bar: ActionBar, x: number, y: number, ship: Game.Ship, action: Game.BaseAction) { this.bar = bar; @@ -35,13 +44,21 @@ module SpaceTac.View { bar.addChild(this); // Active layer + this.active = false; this.layer_active = new Phaser.Image(this.game, 0, 0, "battle-action-active", 0); + this.layer_active.visible = false; this.addChild(this.layer_active); // Icon layer this.layer_icon = new Phaser.Image(this.game, 15, 18, "battle-actions-" + action.code, 0); this.addChild(this.layer_icon); + // Fading layer + this.fading = false; + this.layer_fading = new Phaser.Image(this.game, 0, 0, "battle-action-fading", 0); + this.layer_fading.visible = false; + this.addChild(this.layer_fading); + // Click process this.onInputUp.add(() => { this.processClick(); @@ -62,8 +79,14 @@ module SpaceTac.View { // End any previously selected action this.bar.actionEnded(); + // Update fading statuses + this.bar.updateFadings(this.action.equipment.ap_usage); + // Set the lighting color to highlight - this.layer_active.tint = 0xFFD060; + if (this.game.renderType !== Phaser.HEADLESS) { + // Tint doesn't work in headless renderer + this.layer_active.tint = 0xFFD060; + } if (this.action.needs_target) { // Switch to targetting mode (will apply action when a target is selected) @@ -104,10 +127,16 @@ module SpaceTac.View { // Update the active status, from the action canBeUsed result updateActiveStatus(): void { - var active = this.action.canBeUsed(this.battleview.battle, this.ship); - Animation.setVisibility(this.game, this.layer_active, active, 500); - this.game.tweens.create(this.layer_icon).to({alpha: active ? 1 : 0.3}, 500).start(); - this.input.useHandCursor = active; + this.active = this.action.canBeUsed(this.battleview.battle, this.ship); + Animation.setVisibility(this.game, this.layer_active, this.active, 500); + this.game.tweens.create(this.layer_icon).to({alpha: this.active ? 1 : 0.3}, 500).start(); + this.input.useHandCursor = this.active; + } + + // Update the fading status, given an hypothetical remaining AP + updateFadingStatus(remaining_ap: number): void { + this.fading = !this.action.canBeUsed(this.battleview.battle, this.ship, remaining_ap); + Animation.setVisibility(this.game, this.layer_fading, this.fading, 200); } } } diff --git a/src/scripts/view/specs/ActionBar.spec.ts b/src/scripts/view/specs/ActionBar.spec.ts new file mode 100644 index 0000000..e5a4634 --- /dev/null +++ b/src/scripts/view/specs/ActionBar.spec.ts @@ -0,0 +1,87 @@ +/// +/// + +module SpaceTac.View.Specs { + "use strict"; + + describe("ActionBar", () => { + inbattleview_it("lists available actions for selected ship", (battleview: BattleView) => { + var bar = battleview.action_bar; + + // Ship not owned by current battleview player + var ship = new Game.Ship(); + bar.setShip(ship); + expect(bar.actions.length).toBe(0); + + // Ship with no equipment (only endturn action) + battleview.player = ship.getPlayer(); + bar.setShip(ship); + expect(bar.actions.length).toBe(1); + expect(bar.actions[0].action.code).toEqual("endturn"); + + // Add an engine, with move action + ship.addSlot(Game.SlotType.Engine).attach((new Game.Equipments.ConventionalEngine()).generate()); + bar.setShip(ship); + expect(bar.actions.length).toBe(2); + expect(bar.actions[0].action.code).toEqual("move"); + + // Add a weapon, with fire action + ship.addSlot(Game.SlotType.Weapon).attach((new Game.Equipments.GatlingGun()).generate()); + bar.setShip(ship); + expect(bar.actions.length).toBe(3); + expect(bar.actions[1].action.code).toEqual("fire-gatlinggun"); + }); + + inbattleview_it("mark actions that would become unavailable after use", (battleview: BattleView) => { + var bar = battleview.action_bar; + var ship = new Game.Ship(); + var engine = (new Game.Equipments.ConventionalEngine()).generate(); + engine.ap_usage = 8; + engine.distance = 4; + ship.addSlot(Game.SlotType.Engine).attach(engine); + var weapon1 = (new Game.Equipments.GatlingGun()).generate(); + weapon1.ap_usage = 3; + ship.addSlot(Game.SlotType.Weapon).attach(weapon1); + var weapon2 = (new Game.Equipments.GatlingGun()).generate(); + weapon2.ap_usage = 5; + ship.addSlot(Game.SlotType.Weapon).attach(weapon2); + battleview.battle.playing_ship = ship; + battleview.player = ship.getPlayer(); + + ship.ap_current.setMaximal(10); + ship.ap_current.set(9); + bar.setShip(ship); + + expect(bar.actions.length).toBe(4); + + var checkFading = (fading: number[], available: number[]) => { + fading.forEach((index: number) => { + var icon = bar.actions[index]; + expect(icon.fading).toBe(true); + }); + available.forEach((index: number) => { + var icon = bar.actions[index]; + expect(icon.fading).toBe(false); + }); + }; + + // Weapon 1 leaves all choices open + bar.actions[1].processClick(); + checkFading([], [0, 1, 2, 3]); + + // Weapon 2 can't be fired twice + bar.actions[2].processClick(); + checkFading([2], [0, 1, 3]); + + // Not enough AP for both weapons + ship.ap_current.set(7); + bar.actions[2].processClick(); + checkFading([1, 2], [0, 3]); + + // Not enough AP to move + ship.ap_current.set(3); + bar.actions[1].processClick(); + checkFading([0, 1, 2], [3]); + }); + }); +} diff --git a/src/scripts/view/specs/BattleView.spec.ts b/src/scripts/view/specs/BattleView.spec.ts index 50bfd6d..60bc682 100644 --- a/src/scripts/view/specs/BattleView.spec.ts +++ b/src/scripts/view/specs/BattleView.spec.ts @@ -5,15 +5,6 @@ module SpaceTac.View.Specs { "use strict"; - export function inbattleview_it(desc: string, func: (battleview: BattleView) => void) { - var battleview = new BattleView(); - var battle = Game.Battle.newQuickRandom(); - var player = battle.playing_ship.getPlayer(); - ingame_it(desc, (game: Phaser.Game, state: Phaser.State) => { - func(battleview); - }, battleview, player, battle); - } - describe("BattleView", () => { inbattleview_it("forwards events in targetting mode", (battleview: BattleView) => { expect(battleview.targetting).toBeNull(); diff --git a/src/scripts/view/specs/TestGame.ts b/src/scripts/view/specs/TestGame.ts index 9caa373..9a375a4 100644 --- a/src/scripts/view/specs/TestGame.ts +++ b/src/scripts/view/specs/TestGame.ts @@ -7,6 +7,9 @@ module SpaceTac.View.Specs { export function ingame_it(desc: string, func: (game: Phaser.Game, state: Phaser.State) => void, state: Phaser.State = null, ...stateargs: any[]) { it(desc, (done: () => void) => { + spyOn(console, "log").and.stub(); + spyOn(console, "warn").and.stub(); + var game = new Phaser.Game(500, 500, Phaser.HEADLESS); if (!state) { @@ -31,4 +34,14 @@ module SpaceTac.View.Specs { game.state.start.apply(game.state, args); }); } + + // Test game wrapper, with a battleview initialized on a random battle + export function inbattleview_it(desc: string, func: (battleview: BattleView) => void) { + var battleview = new BattleView(); + var battle = Game.Battle.newQuickRandom(); + var player = battle.playing_ship.getPlayer(); + ingame_it(desc, (game: Phaser.Game, state: Phaser.State) => { + func(battleview); + }, battleview, player, battle); + } }