From eb28591abda2a56fac0f4faa418b9dd7c052e37c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C3=ABl=20Lemaire?= Date: Tue, 25 Apr 2017 20:24:43 +0200 Subject: [PATCH] character sheet: Fixed a possible loss of equipment --- README.md | 1 + TODO | 4 +- src/core/Ship.spec.ts | 24 ++++ src/core/Ship.ts | 28 ++++- src/ui/character/CharacterCargo.ts | 4 + src/ui/character/CharacterEquipment.spec.ts | 119 ++++++++++++++++++++ src/ui/character/CharacterEquipment.ts | 58 ++++++---- src/ui/character/CharacterSlot.spec.ts | 38 +++++++ src/ui/character/CharacterSlot.ts | 2 +- 9 files changed, 248 insertions(+), 30 deletions(-) create mode 100644 src/ui/character/CharacterEquipment.spec.ts create mode 100644 src/ui/character/CharacterSlot.spec.ts diff --git a/README.md b/README.md index eab3390..9831e92 100644 --- a/README.md +++ b/README.md @@ -24,6 +24,7 @@ After making changes to sources, you need to recompile: ## Credits * **[Michaƫl Lemaire](https://thunderk.net/)** - Code and graphics +* **[Matthieu Desprez](https://github.com/edistra)** - Beta testing and ideas * **Nicolas Forgo** - Ship models * **[Phaser](http://phaser.io)** - Game engine * **[Kevin MacLeod](http://www.incompetech.com/)** - Musics diff --git a/TODO b/TODO index 22c3745..edfabce 100644 --- a/TODO +++ b/TODO @@ -3,7 +3,7 @@ * Character sheet: disable interaction during battle (except for loot screen) * Character sheet: paginate loot and shop items * Character sheet: improve eye-catching for shop and loot section -* Character sheet: equipping an item without the requirements make it disappear +* Character sheet: highlight allowed destinations during drag-and-drop * Add permanent effects to ship models to ease balancing * Ships should start battle in formation to force them to move * Fix targetting not resetting when using action shortcuts @@ -36,6 +36,7 @@ * AI: apply safety distances to move actions * AI: use support equipments (repair drones...) * AI: fix not being able to apply simulated maneuver +* AI: do not always move first, they are defenders * TacticalAI: allow to play several moves in the same turn * TacticalAI: add pauses to not play too quickly * TacticalAI: replace BullyAI @@ -55,5 +56,6 @@ Later, if possible: * Replays * Multiplayer +* Formation or deployment phase * Saving to external file * Saving to cloud diff --git a/src/core/Ship.spec.ts b/src/core/Ship.spec.ts index 5229d64..f076074 100644 --- a/src/core/Ship.spec.ts +++ b/src/core/Ship.spec.ts @@ -414,6 +414,30 @@ module TS.SpaceTac.Specs { expect(ship.cargo).toEqual([equipment]); }); + it("checks equipment requirements", function () { + let ship = new Ship(); + let equipment = new Equipment(SlotType.Hull); + expect(ship.canEquip(equipment)).toBe(null); + + ship.addSlot(SlotType.Engine); + expect(ship.canEquip(equipment)).toBe(null); + + let slot = ship.addSlot(SlotType.Hull); + expect(ship.canEquip(equipment)).toBe(slot); + + equipment.requirements["skill_energy"] = 2; + expect(ship.canEquip(equipment)).toBe(null); + + ship.upgradeSkill("skill_energy"); + expect(ship.canEquip(equipment)).toBe(null); + + ship.upgradeSkill("skill_energy"); + expect(ship.canEquip(equipment)).toBe(slot); + + slot.attach(new Equipment(SlotType.Hull)); + expect(ship.canEquip(equipment)).toBe(null); + }); + it("allow skills upgrading from current level", function () { let ship = new Ship(); expect(ship.level.get()).toBe(1); diff --git a/src/core/Ship.ts b/src/core/Ship.ts index 95886b9..ce4fa5f 100644 --- a/src/core/Ship.ts +++ b/src/core/Ship.ts @@ -511,19 +511,37 @@ module TS.SpaceTac { * Returns true if successful */ equip(item: Equipment, from_cargo = true): boolean { - let free_slot = first(this.slots, slot => slot.type == item.slot_type && !slot.attached); + let free_slot = this.canEquip(item); if (free_slot && (!from_cargo || remove(this.cargo, item))) { free_slot.attach(item); - - this.updateAttributes(); - - return true; + if (item.attached_to == free_slot && free_slot.attached == item) { + this.updateAttributes(); + return true; + } else { + return false; + } } else { return false; } } + /** + * Check if a ship is able to equip en item, and return the slot it may fit in, or null + */ + canEquip(item: Equipment): Slot | null { + let free_slot = first(this.slots, slot => slot.type == item.slot_type && !slot.attached); + if (free_slot) { + if (item.canBeEquipped(this)) { + return free_slot; + } else { + return null; + } + } else { + return null; + } + } + /** * Remove an equipped item, returning it to cargo * diff --git a/src/ui/character/CharacterCargo.ts b/src/ui/character/CharacterCargo.ts index dcaab3f..9028350 100644 --- a/src/ui/character/CharacterCargo.ts +++ b/src/ui/character/CharacterCargo.ts @@ -13,6 +13,10 @@ module TS.SpaceTac.UI { this.sheet = sheet; } + jasmineToString() { + return "CharacterCargo"; + } + /** * CharacterEquipmentContainer interface */ diff --git a/src/ui/character/CharacterEquipment.spec.ts b/src/ui/character/CharacterEquipment.spec.ts new file mode 100644 index 0000000..eac781d --- /dev/null +++ b/src/ui/character/CharacterEquipment.spec.ts @@ -0,0 +1,119 @@ +module TS.SpaceTac.UI.Specs { + describe("CharacterEquipment", function () { + let testgame = setupEmptyView(); + + class FakeContainer implements CharacterEquipmentContainer { + name: string; + x: number; + inside: CharacterEquipment | null; + constructor(name: string, x: number) { + this.name = name; + this.x = x; + this.inside = null; + } + jasmineToString() { + return this.name; + } + isInside(x: number, y: number): boolean { + return x == this.x; + } + getEquipmentAnchor(): { x: number, y: number, scale: number } { + return { + x: this.x, + y: 0, + scale: 0.5 + } + } + getPriceOffset(): number { + return 12; + } + addEquipment(equipment: CharacterEquipment, source: CharacterEquipmentContainer | null, test: boolean): boolean { + if (this.x < 150) { + if (!test) { + this.inside = equipment; + } + return true; + } else { + return false; + } + } + removeEquipment(equipment: CharacterEquipment, destination: CharacterEquipmentContainer | null, test: boolean): boolean { + if (this.x < 150 && this.inside == equipment) { + if (!test) { + this.inside = null; + } + return true; + } else { + return false; + } + } + } + + it("handles drag-and-drop to move equipment", function () { + let view = testgame.baseview; + let sheet = new CharacterSheet(view); + sheet.show(new Ship()); + let refresh = spyOn(sheet, "refresh").and.stub(); + + let container1 = new FakeContainer("container1", 0); + let container2 = new FakeContainer("container2", 100); + let container3 = new FakeContainer("container3", 200); + let equipment = new CharacterEquipment(sheet, new Equipment(), container1); + container1.inside = equipment; + spyOn(sheet, "iEquipmentContainers").and.returnValue(iarray([container1, container2, container3])); + + expect(equipment.container).toBe(container1); + expect(equipment.x).toBe(0); + expect(equipment.scale.x).toBe(0.25); + + // drop on nothing + equipment.events.onDragStart.dispatch(); + equipment.x = 812; + equipment.events.onDragStop.dispatch(); + expect(equipment.container).toBe(container1); + expect(equipment.x).toBe(0); + expect(refresh).toHaveBeenCalledTimes(0); + + // drop on accepting destination + equipment.events.onDragStart.dispatch(); + equipment.x = 100; + equipment.events.onDragStop.dispatch(); + expect(equipment.container).toBe(container2); + expect(equipment.x).toBe(100); + expect(container1.inside).toBe(null); + expect(container2.inside).toBe(equipment); + expect(refresh).toHaveBeenCalledTimes(1); + + // drop on refusing destination + equipment.events.onDragStart.dispatch(); + equipment.x = 200; + equipment.events.onDragStop.dispatch(); + expect(equipment.container).toBe(container2); + expect(equipment.x).toBe(100); + expect(container2.inside).toBe(equipment); + expect(container3.inside).toBe(null); + expect(refresh).toHaveBeenCalledTimes(1); + + // broken destination, should return to source + let log = spyOn(console, "error").and.stub(); + spyOn(container3, "addEquipment").and.returnValues(true, false, true, false); + equipment.events.onDragStart.dispatch(); + equipment.x = 200; + equipment.events.onDragStop.dispatch(); + expect(equipment.container).toBe(container2); + expect(equipment.x).toBe(100); + expect(refresh).toHaveBeenCalledTimes(1); + expect(log).toHaveBeenCalledWith('Destination container refused to accept equipment', equipment, container2, container3); + + // broken destination and source, item is lost ! + spyOn(container2, "addEquipment").and.returnValue(false); + equipment.events.onDragStart.dispatch(); + equipment.x = 200; + equipment.events.onDragStop.dispatch(); + expect(equipment.container).toBe(container3); + expect(equipment.x).toBe(200); + expect(refresh).toHaveBeenCalledTimes(2); + expect(log).toHaveBeenCalledWith('Equipment lost in bad exchange !', equipment, container2, container3); + }); + }); +} diff --git a/src/ui/character/CharacterEquipment.ts b/src/ui/character/CharacterEquipment.ts index ac1c6b8..1392fa2 100644 --- a/src/ui/character/CharacterEquipment.ts +++ b/src/ui/character/CharacterEquipment.ts @@ -43,8 +43,6 @@ module TS.SpaceTac.UI { this.container = container; this.price = 0; - this.container.addEquipment(this, null, false); - this.anchor.set(0.5, 0.5); this.setupDragDrop(sheet); @@ -53,6 +51,10 @@ module TS.SpaceTac.UI { sheet.view.tooltip.bind(this, container => this.fillTooltip(container)); } + jasmineToString() { + return this.item.jasmineToString(); + } + /** * Find the container under a specific screen location */ @@ -103,17 +105,16 @@ module TS.SpaceTac.UI { this.scale.set(0.5, 0.5); this.alpha = 0.8; }); - this.events.onDragUpdate.add(() => { - let destination = this.findContainerAt(this.x, this.y); - if (destination) { - this.applyDragDrop(this.container, destination, true); - } - }); this.events.onDragStop.add(() => { let destination = this.findContainerAt(this.x, this.y); - if (destination) { - this.applyDragDrop(this.container, destination, false); - sheet.refresh(); + if (destination && destination != this.container) { + if (this.applyDragDrop(this.container, destination, false)) { + this.container = destination; + this.snapToContainer(); + sheet.refresh(); // TODO Only if required (destination is "virtual") + } else { + this.snapToContainer(); + } } else { this.snapToContainer(); } @@ -122,22 +123,33 @@ module TS.SpaceTac.UI { /** * Apply drag and drop between two containers + * + * Return true if something changed (or would change, if test=true). */ - applyDragDrop(source: CharacterEquipmentContainer, destination: CharacterEquipmentContainer, hold: boolean) { - if (source.removeEquipment(this, destination, true) && destination.addEquipment(this, source, true)) { - if (!hold) { - if (source.removeEquipment(this, destination, false)) { - if (!destination.addEquipment(this, source, false)) { - console.error("Destination container refused to accept equipment", this, source, destination); - // Go back to source - if (!source.addEquipment(this, null, true)) { - console.error("Equipment lost in bad exchange !", this, source, destination); - } - } + applyDragDrop(source: CharacterEquipmentContainer, destination: CharacterEquipmentContainer, test: boolean): boolean { + let possible = source.removeEquipment(this, destination, true) && destination.addEquipment(this, source, true); + if (test) { + return possible; + } else if (possible) { + if (source.removeEquipment(this, destination, false)) { + if (destination.addEquipment(this, source, false)) { + return true; } else { - console.error("Source container refused to give away equipment", this, source, destination); + console.error("Destination container refused to accept equipment", this, source, destination); + // Go back to source + if (source.addEquipment(this, null, false)) { + return false; + } else { + console.error("Equipment lost in bad exchange !", this, source, destination); + return true; + } } + } else { + console.error("Source container refused to give away equipment", this, source, destination); + return false; } + } else { + return false; } } diff --git a/src/ui/character/CharacterSlot.spec.ts b/src/ui/character/CharacterSlot.spec.ts new file mode 100644 index 0000000..4088319 --- /dev/null +++ b/src/ui/character/CharacterSlot.spec.ts @@ -0,0 +1,38 @@ +module TS.SpaceTac.UI.Specs { + describe("CharacterSlot", function () { + let testgame = setupEmptyView(); + + it("allows dragging equipment", function () { + let view = testgame.baseview; + let ship = new Ship(); + ship.addSlot(SlotType.Hull); + let sheet = new CharacterSheet(view); + sheet.show(ship); + let source = new CharacterLootSlot(sheet, 0, 0); + sheet.addChild(source); + let equipment = new CharacterEquipment(sheet, new Equipment(SlotType.Engine), source); + + let slot = new CharacterSlot(sheet, 0, 0, SlotType.Engine); + expect(slot.addEquipment(equipment, source, true)).toBe(false); + expect(slot.removeEquipment(equipment, source, true)).toBe(false); + + ship.addSlot(SlotType.Engine); + expect(slot.addEquipment(equipment, source, true)).toBe(true); + + equipment.item.requirements["skill_time"] = 1; + expect(slot.addEquipment(equipment, source, true)).toBe(false); + + ship.upgradeSkill("skill_time"); + expect(slot.addEquipment(equipment, source, true)).toBe(true); + + expect(ship.listEquipment(SlotType.Engine)).toEqual([]); + let result = slot.addEquipment(equipment, source, false); + expect(result).toBe(true); + expect(ship.listEquipment(SlotType.Engine)).toEqual([equipment.item]); + + result = slot.removeEquipment(equipment, source, false); + expect(result).toBe(true); + expect(ship.listEquipment(SlotType.Engine)).toEqual([]); + }); + }); +} diff --git a/src/ui/character/CharacterSlot.ts b/src/ui/character/CharacterSlot.ts index 6943e0b..e51fe79 100644 --- a/src/ui/character/CharacterSlot.ts +++ b/src/ui/character/CharacterSlot.ts @@ -36,7 +36,7 @@ module TS.SpaceTac.UI { return 66; } addEquipment(equipment: CharacterEquipment, source: CharacterEquipmentContainer | null, test: boolean): boolean { - if (equipment.item.slot_type !== null && this.sheet.ship.getFreeSlot(equipment.item.slot_type)) { + if (this.sheet.ship.canEquip(equipment.item)) { if (test) { return true; } else {