1
0
Fork 0

Added ability to loot items at the end of battle

This commit is contained in:
Michaël Lemaire 2017-03-14 18:48:04 +01:00
parent 5ab3c12039
commit ca7be2e416
17 changed files with 260 additions and 104 deletions

4
TODO
View file

@ -3,6 +3,8 @@
* Character sheet: add tooltips (on values, slots and equipments)
* Character sheet: add levelling up (spending of available points)
* Character sheet: disable interaction during battle (except for loot screen)
* Add battle statistics and/or critics in outcome dialog
* Add battle reverting in defeat outcome dialog
* Ensure that tweens and particle emitters get destroyed once animation is done (or view changes)
* Highlight ships that will be included as target of current action
* Controls: Do not focus on ship while targetting for area effects (dissociate hover and target)
@ -32,8 +34,6 @@
* TacticalAI: allow to play several moves in the same turn
* TacticalAI: add pauses to not play too quickly
* TacticalAI: replace BullyAI
* Add a defeat screen (game over for now)
* Add a victory screen, with loot display
* Add retreat from battle
* Map: current fleet position is not visible enough
* Map: restore fog of war

@ -1 +1 @@
Subproject commit 79e3c649cf7a411d06af7372aa825518e0d8df30
Subproject commit fb3268b8ef0c8f6e32c5cf10e896b9a134d45f6a

View file

@ -129,10 +129,10 @@ module TS.SpaceTac {
}
// Ends a battle and sets the outcome
endBattle(winner: Fleet | null, log: boolean = true) {
endBattle(winner: Fleet | null, log = true, loot = true) {
this.ended = true;
this.outcome = new BattleOutcome(winner);
if (winner) {
if (loot && winner) {
this.outcome.createLoot(this);
}
if (log && this.log) {

View file

@ -1,48 +1,32 @@
module TS.SpaceTac.Specs {
describe("BattleOutcome", () => {
it("generates loot from dead ships, for the winner to take", () => {
it("generates loot from defeated ships", () => {
var fleet1 = new Fleet();
fleet1.addShip(new Ship(fleet1));
fleet1.addShip(new Ship(fleet1));
fleet1.addShip(new Ship(fleet1));
fleet1.addShip(new Ship());
var fleet2 = new Fleet();
fleet2.addShip(new Ship(fleet2));
fleet2.addShip(new Ship(fleet2));
fleet2.addShip(new Ship(fleet2));
fleet2.addShip(new Ship(fleet2));
fleet2.addShip(new Ship());
fleet2.addShip(new Ship());
fleet2.addShip(new Ship());
fleet2.addShip(new Ship());
fleet2.ships[2].level = 5;
fleet2.ships[3].level = 5;
fleet1.ships[0].setDead();
fleet1.ships[0].addSlot(SlotType.Hull).attach(new Equipment(SlotType.Hull));
fleet1.ships[1].setDead();
fleet1.ships[1].addSlot(SlotType.Engine).attach(new Equipment(SlotType.Engine, "1.1.1"));
fleet1.ships[1].addSlot(SlotType.Engine).attach(new Equipment(SlotType.Engine, "1.1.2"));
fleet1.ships[1].addSlot(SlotType.Engine).attach(new Equipment(SlotType.Engine, "1.1.3"));
fleet1.ships[1].addSlot(SlotType.Engine).attach(new Equipment(SlotType.Engine, "1.1.4"));
fleet2.ships[0].setDead();
fleet2.ships[0].addSlot(SlotType.Weapon).attach(new Equipment(SlotType.Weapon, "2.0.1"));
fleet2.ships[1].setDead();
fleet2.ships[1].addSlot(SlotType.Weapon).attach(new Equipment(SlotType.Weapon, "2.1.1"));
fleet2.ships[2].setDead();
fleet2.ships[2].addSlot(SlotType.Weapon).attach(new Equipment(SlotType.Weapon, "2.2.1"));
fleet2.ships[3].setDead();
fleet2.ships[3].addSlot(SlotType.Weapon).attach(new Equipment(SlotType.Weapon, "2.3.1"));
fleet2.ships[0].addSlot(SlotType.Weapon).attach(new Equipment(SlotType.Weapon, "0a"));
fleet2.ships[0].addSlot(SlotType.Weapon).attach(new Equipment(SlotType.Weapon, "0b"));
fleet2.ships[1].addSlot(SlotType.Weapon).attach(new Equipment(SlotType.Weapon, "1a"));
fleet2.ships[2].addSlot(SlotType.Weapon).attach(new Equipment(SlotType.Weapon, "2b"));
fleet2.ships[3].addSlot(SlotType.Weapon).attach(new Equipment(SlotType.Weapon, "3b"));
var battle = new Battle(fleet1, fleet2);
var outcome = new BattleOutcome(fleet1);
var random = new SkewedRandomGenerator([
0, // leave first ship alone
0.45, // take 2 equipments from the 4 of second ship
0.999, // - take last equipment
0.6, // standard loot on first ship
0, // - take first equipment
0.6, // standard loot on first ship of second fleet
0, // - take first equipment
0.4, // no loot on second ship
0, // leave second ship alone
0.95, // lucky loot on third ship
0, // - lower end of level range (dead ship has 5, so range is 4-6)
0, // - lower end of level range (ship has 5, so range is 4-6)
0, // - take first generated equipment (there is only one anyway)
0.96, // lucky loot on fourth ship
0.999 // - higher end of level range
@ -58,16 +42,14 @@ module TS.SpaceTac.Specs {
outcome.createLoot(battle, random);
expect(outcome.loot.length).toBe(5);
expect(outcome.loot[0].name).toBe("1.1.4");
expect(outcome.loot[1].name).toBe("1.1.1");
expect(outcome.loot[2].name).toBe("2.0.1");
expect(outcome.loot[3].name).toBe("Nuclear Reactor");
expect(outcome.loot[3].min_level).toBe(4);
expect(outcome.loot[3].distance).toEqual(1);
expect(outcome.loot[4].name).toBe("Nuclear Reactor");
expect(outcome.loot[4].min_level).toBe(6);
expect(outcome.loot[4].distance).toBeCloseTo(4, 0.000001);
expect(outcome.loot.length).toBe(3);
expect(outcome.loot[0].name).toBe("0a");
expect(outcome.loot[1].name).toBe("Nuclear Reactor");
expect(outcome.loot[1].min_level).toBe(4);
expect(outcome.loot[1].distance).toEqual(1);
expect(outcome.loot[2].name).toBe("Nuclear Reactor");
expect(outcome.loot[2].min_level).toBe(6);
expect(outcome.loot[2].distance).toBeCloseTo(4, 0.000001);
});
});
}

View file

@ -16,43 +16,32 @@ module TS.SpaceTac {
this.loot = [];
}
// Create loot from dead ships
createLoot(battle: Battle, random: RandomGenerator = new RandomGenerator()): void {
/**
* Fill loot from defeated fleet
*/
createLoot(battle: Battle, random = RandomGenerator.global): void {
this.loot = [];
battle.fleets.forEach((fleet: Fleet) => {
fleet.ships.forEach((ship: Ship) => {
if (!ship.alive) {
if (ship.fleet === this.winner) {
// Member of the winner fleet, salvage a number of equipments
var count = random.randInt(0, ship.getEquipmentCount());
while (count > 0) {
var salvaged = ship.getRandomEquipment(random);
if (salvaged) {
salvaged.detach();
this.loot.push(salvaged);
}
count--;
}
} else {
var luck = random.random();
if (luck > 0.9) {
// Salvage a supposedly transported item
var transported = this.generateLootItem(random, ship.level);
if (transported) {
this.loot.push(transported);
}
} else if (luck > 0.5) {
// Salvage one equipped item
var token = ship.getRandomEquipment(random);
if (token) {
token.detach();
this.loot.push(token);
}
battle.fleets.forEach(fleet => {
if (this.winner && this.winner.player != fleet.player) {
fleet.ships.forEach(ship => {
var luck = random.random();
if (luck > 0.9) {
// Salvage a supposedly transported item
var transported = this.generateLootItem(random, ship.level);
if (transported) {
this.loot.push(transported);
}
} else if (luck > 0.5) {
// Salvage one equipped item
var token = ship.getRandomEquipment(random);
if (token) {
token.detach();
this.loot.push(token);
}
}
}
});
});
}
});
}

View file

@ -51,6 +51,10 @@ module TS.SpaceTac {
this.action = new BaseAction("nothing", "Do nothing", false);
}
jasmineToString() {
return this.attached_to ? `${this.attached_to.ship.name} - ${this.name}` : this.name;
}
// Returns true if the equipment can be equipped on a ship
// This checks *requirements* against the ship capabilities
canBeEquipped(ship: Ship): boolean {

View file

@ -15,7 +15,7 @@ module TS.SpaceTac.Specs {
var battle = nn(session.getBattle());
battle.advanceToNextShip();
// TODO Make some fixed moves (AI?)
battle.endBattle(battle.fleets[0]);
battle.endBattle(battle.fleets[0], true, false);
}
it("serializes to a string", () => {

View file

@ -451,6 +451,15 @@ module TS.SpaceTac {
}
}
/**
* Remove an item from cargo space
*
* Returns true if successful
*/
removeCargo(item: Equipment): boolean {
return remove(this.cargo, item);
}
/**
* Equip an item from cargo to the first available slot
*

View file

@ -98,9 +98,19 @@ module TS.SpaceTac.UI {
// Key mapping
this.inputs.bindCheat(Phaser.Keyboard.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(Phaser.Keyboard.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(Phaser.Keyboard.A, "Use AI to play", () => {

View file

@ -13,7 +13,7 @@ module TS.SpaceTac.UI {
this.addChild(title);
if (victory) {
this.addChild(new Phaser.Button(this.game, 350, 842, "battle-outcome-button-loot", () => {
this.addChild(new Phaser.Button(this.game, 344, 842, "battle-outcome-button-loot", () => {
// Open loot screen
if (outcome.winner) {
parent.character_sheet.show(outcome.winner.ships[0]);
@ -25,7 +25,7 @@ module TS.SpaceTac.UI {
parent.exitBattle();
}));
} else {
this.addChild(new Phaser.Button(this.game, 350, 842, "battle-outcome-button-revert", () => {
this.addChild(new Phaser.Button(this.game, 344, 842, "battle-outcome-button-revert", () => {
// Revert just before battle
// TODO
}));

View file

@ -15,7 +15,8 @@ module TS.SpaceTac.UI {
* Snap the equipment icon inside the slot
*/
snapEquipment(equipment: CharacterEquipment) {
equipment.position.set(this.x + this.parent.x + 98, this.y + this.parent.y + 98);
equipment.position.set(this.x + this.parent.x + 98 * this.scale.x, this.y + this.parent.y + 98 * this.scale.y);
equipment.setContainerScale(this.scale.x);
}
/**
@ -23,10 +24,21 @@ module TS.SpaceTac.UI {
*/
canDropEquipment(equipment: Equipment, x: number, y: number): CharacterEquipmentDrop | null {
if (this.getBounds().contains(x, y)) {
return {
message: "Unequip",
callback: () => this.sheet.ship.unequip(equipment)
};
if (contains(this.sheet.loot_items, equipment)) {
return {
message: "Loot",
callback: () => {
if (this.sheet.ship.addCargo(equipment)) {
remove(this.sheet.loot_items, equipment);
}
}
};
} else {
return {
message: "Unequip",
callback: () => this.sheet.ship.unequip(equipment)
};
}
} else {
return null;
}

View file

@ -10,37 +10,57 @@ module TS.SpaceTac.UI {
* Display a ship equipment, either attached to a slot, in cargo, or being dragged down
*/
export class CharacterEquipment extends Phaser.Image {
equipment: Equipment;
constructor(sheet: CharacterSheet, equipment: Equipment) {
let icon = sheet.game.cache.checkImageKey(`equipment-${equipment.code}`) ? `equipment-${equipment.code}` : `battle-actions-${equipment.action.code}`;
super(sheet.game, 0, 0, icon);
this.equipment = equipment;
this.anchor.set(0.5, 0.5);
this.scale.set(0.5, 0.5);
this.setupDragDrop(sheet);
}
/**
* Enable dragging to another slot
*/
setupDragDrop(sheet: CharacterSheet) {
this.inputEnabled = true;
this.input.enableDrag(false, true);
let origin: [number, number] | null = null;
let origin: [number, number, number, number] | null = null;
let drop: CharacterEquipmentDrop | null = null;
this.events.onDragStart.add(() => {
origin = [this.x, this.y];
origin = [this.x, this.y, this.scale.x, this.scale.y];
this.scale.set(0.5, 0.5);
this.alpha = 0.8;
});
this.events.onDragUpdate.add(() => {
drop = sheet.canDropEquipment(equipment, this.x, this.y);
drop = sheet.canDropEquipment(this.equipment, this.x, this.y);
});
this.events.onDragStop.add(() => {
if (drop) {
drop.callback(equipment);
drop.callback(this.equipment);
sheet.refresh();
} else {
if (origin) {
this.position.set(origin[0], origin[1]);
this.scale.set(origin[2], origin[3]);
origin = null;
}
this.alpha = 1;
}
});
}
/**
* Set the scaling of container in which the equipment icon is snapped
*/
setContainerScale(scale: number) {
this.scale.set(0.5 * scale, 0.5 * scale);
}
}
}

View file

@ -39,6 +39,89 @@ module TS.SpaceTac.UI.Specs {
expect(sheet.ship_slots.length).toBe(1);
expect(sheet.ship_cargo.length).toBe(2);
});
it("moves equipment around", function () {
let fleet = new Fleet();
let ship = fleet.addShip();
ship.setCargoSpace(2);
let equ1 = TestTools.addEngine(ship, 1);
let equ2 = new Equipment(SlotType.Weapon);
ship.addCargo(equ2);
let equ3 = new Equipment(SlotType.Hull);
let equ4 = new Equipment(SlotType.Power);
let loot = [equ3, equ4];
ship.addSlot(SlotType.Weapon);
let sheet = new CharacterSheet(testgame.baseview);
sheet.show(ship, false);
expect(sheet.loot_slots.visible).toBe(false);
expect(sheet.equipments.children.length).toBe(2);
sheet.setLoot(loot);
expect(sheet.loot_slots.visible).toBe(true);
expect(sheet.equipments.children.length).toBe(4);
let findsprite = (equ: Equipment) => nn(first(<CharacterEquipment[]>sheet.equipments.children, sp => sp.equipment == equ));
let draddrop = (sp: CharacterEquipment, dest: CharacterCargo | CharacterSlot) => {
let destbounds = dest.getBounds();
/*sp.events.onDragStart.dispatch();
sp.position.set(destbounds.x, destbounds.y);
sp.events.onDragUpdate.dispatch();
sp.events.onDragStop.dispatch();*/
nn(dest.canDropEquipment(sp.equipment, destbounds.x, destbounds.y)).callback(sp.equipment);
}
// Unequip
let sprite = findsprite(equ1);
expect(equ1.attached_to).not.toBeNull();
expect(ship.cargo.length).toBe(1);
draddrop(sprite, <CharacterCargo>sheet.ship_cargo.children[0]);
expect(equ1.attached_to).toBeNull();
expect(ship.cargo.length).toBe(2);
expect(ship.cargo).toContain(equ1);
// Equip
sprite = findsprite(equ2);
expect(equ2.attached_to).toBeNull();
expect(ship.cargo).toContain(equ2);
draddrop(sprite, <CharacterSlot>sheet.ship_slots.children[0]);
expect(equ2.attached_to).toBe(ship.slots[1]);
expect(ship.cargo).not.toContain(equ2);
// Loot
sprite = findsprite(equ3);
expect(equ3.attached_to).toBeNull();
expect(ship.cargo).not.toContain(equ3);
expect(loot).toContain(equ3);
draddrop(sprite, <CharacterCargo>sheet.ship_cargo.children[0]);
expect(equ3.attached_to).toBeNull();
expect(ship.cargo).toContain(equ3);
expect(loot).not.toContain(equ3);
// Can't loop - no cargo space available
sprite = findsprite(equ4);
expect(ship.cargo).not.toContain(equ4);
expect(loot).toContain(equ4);
draddrop(sprite, <CharacterCargo>sheet.ship_cargo.children[0]);
expect(ship.cargo).not.toContain(equ4);
expect(loot).toContain(equ4);
// Discard
sprite = findsprite(equ1);
expect(ship.cargo).toContain(equ1);
expect(loot).not.toContain(equ1);
draddrop(sprite, <CharacterCargo>sheet.ship_cargo.children[0]);
expect(equ1.attached_to).toBeNull();
expect(loot).not.toContain(equ1);
// Can't equip - no slot available
sprite = findsprite(equ3);
expect(equ3.attached_to).toBeNull();
draddrop(sprite, <CharacterSlot>sheet.ship_slots.children[0]);
expect(equ3.attached_to).toBeNull();
});
});
it("fits slots in area", function () {
@ -47,6 +130,12 @@ module TS.SpaceTac.UI.Specs {
positions: [{ x: 0, y: 0 }, { x: 100, y: 0 }, { x: 200, y: 0 }, { x: 0, y: 100 }, { x: 100, y: 100 }, { x: 200, y: 100 }],
scaling: 1
});
result = CharacterSheet.getSlotPositions(6, 299, 199, 100, 100);
expect(result).toEqual({
positions: [{ x: 0, y: 0 }, { x: 100, y: 0 }, { x: 200, y: 0 }, { x: 0, y: 100 }, { x: 100, y: 100 }, { x: 200, y: 100 }],
scaling: 0.99
});
});
});
}

View file

@ -34,7 +34,8 @@ module TS.SpaceTac.UI {
ship_cargo: Phaser.Group;
// Loot items
loot_items: Phaser.Group;
loot_slots: Phaser.Group;
loot_items: Equipment[] = [];
// Fleet's portraits
portraits: Phaser.Group;
@ -80,9 +81,10 @@ module TS.SpaceTac.UI {
this.ship_cargo.position.set(1240, 86);
this.addChild(this.ship_cargo);
this.loot_items = new Phaser.Group(this.game);
this.loot_items.position.set(1270, 670);
this.addChild(this.loot_items);
this.loot_slots = new Phaser.Group(this.game);
this.loot_slots.position.set(1270, 670);
this.loot_slots.visible = false;
this.addChild(this.loot_slots);
this.portraits = new Phaser.Group(this.game);
this.portraits.position.set(152, 0);
@ -201,6 +203,8 @@ module TS.SpaceTac.UI {
}
});
this.updateLoot();
this.updateFleet(ship.fleet);
if (animate) {
@ -214,6 +218,8 @@ module TS.SpaceTac.UI {
* Hide the sheet
*/
hide(animate = true) {
this.loot_slots.visible = false;
this.portraits.children.forEach((portrait: Phaser.Button) => portrait.loadTexture("character-ship"));
if (animate) {
@ -224,21 +230,30 @@ module TS.SpaceTac.UI {
}
/**
* Display the loot section
* Set the list of lootable equipment
*
* The list of equipments may be altered if items are taken from it
*/
setLoot(loot: Equipment[]) {
this.loot_items.removeAll(true);
this.loot_items = loot;
this.updateLoot();
this.loot_slots.visible = true;
}
let info = CharacterSheet.getSlotPositions(12, 596, 360, 196, 196);
/**
* Update the loot slots
*/
private updateLoot() {
this.loot_slots.removeAll(true);
let info = CharacterSheet.getSlotPositions(12, 588, 354, 196, 196);
range(12).forEach(idx => {
let loot_slot = new CharacterCargo(this, info.positions[idx].x, info.positions[idx].y);
let loot_slot = new LootSlot(this, info.positions[idx].x, info.positions[idx].y);
loot_slot.scale.set(info.scaling, info.scaling);
this.loot_items.addChild(loot_slot);
this.loot_slots.addChild(loot_slot);
if (idx < loot.length) {
let equipment = new CharacterEquipment(this, loot[idx]);
if (idx < this.loot_items.length) {
let equipment = new CharacterEquipment(this, this.loot_items[idx]);
this.equipments.addChild(equipment);
loot_slot.snapEquipment(equipment);
}
@ -251,7 +266,8 @@ module TS.SpaceTac.UI {
canDropEquipment(equipment: Equipment, x: number, y: number): CharacterEquipmentDrop | null {
let candidates: Iterator<CharacterEquipmentDestination> = ichain(
iarray(<CharacterSlot[]>this.ship_slots.children),
iarray(<CharacterCargo[]>this.ship_cargo.children)
iarray(<CharacterCargo[]>this.ship_cargo.children),
this.loot_slots.visible ? iarray(<LootSlot[]>this.loot_slots.children) : IEMPTY
);
return ifirstmap(candidates, candidate => candidate.canDropEquipment(equipment, x, y));
@ -278,7 +294,7 @@ module TS.SpaceTac.UI {
// Find scaling
let scaling = 1;
while (slotwidth * scaling > areawidth || slotheight * scaling > areaheight) {
while (slotwidth * scaling * columns > areawidth || slotheight * scaling * rows > areaheight) {
scaling *= 0.99;
}

View file

@ -19,7 +19,8 @@ module TS.SpaceTac.UI {
* Snap the equipment icon inside the slot
*/
snapEquipment(equipment: CharacterEquipment) {
equipment.position.set(this.x + this.parent.x + 84, this.y + this.parent.y + 83);
equipment.position.set(this.x + this.parent.x + 84 * this.scale.x, this.y + this.parent.y + 83 * this.scale.y);
equipment.setContainerScale(this.scale.x);
}
/**

View file

@ -0,0 +1,24 @@
module TS.SpaceTac.UI {
/**
* Display a loot slot
*/
export class LootSlot extends CharacterCargo {
/**
* Check if an equipment can be dropped in this slot
*/
canDropEquipment(equipment: Equipment, x: number, y: number): CharacterEquipmentDrop | null {
if (this.getBounds().contains(x, y) && contains(this.sheet.ship.cargo, equipment)) {
return {
message: "Discard",
callback: () => {
if (this.sheet.ship.removeCargo(equipment)) {
add(this.sheet.loot_items, equipment);
}
}
};
} else {
return null;
}
}
}
}