1
0
Fork 0

Added equipment overheat / cooldown

This commit is contained in:
Michaël Lemaire 2017-05-17 01:12:05 +02:00
parent 8823c377dc
commit a8d0369292
33 changed files with 429 additions and 206 deletions

View file

@ -101,6 +101,25 @@ If an equipped item has a requirement of "time skill >= 2", that the ship has "t
that a temporary effect of "time skill -1" is active, the requirement is no longer fulfilled and the equipped
item is then temporarily disabled (no more effects and cannot be used), until the "time skill -1" effect is lifted.
## Equipments
### Overheat/Cooldown
Equipments may overheat, and need to cooldown for some time, during which it cannot be used.
If an equipment has "overheat 2 / cooldown 3", using it twice in the same turn will cause it to
overheat. It then cannot be used for the next three turns. Using this equipment only once per turn
is safe, and will never overheat it.
If an equipment has multiple actions associated, any of these actions will increase the shared heat.
*Not done yet :* Some equipments may have a "cumulative overheat", meaning that the heat is stored between turns, cooling down 1
point at the end of turn.
*Not done yet :* Some equipments may have a "stacked overheat", which
is similar to "cumulative overheat", except it does not cool down at
the end of turn (it will only start cooling down after being overheated).
## Drones
Drones are static objects, deployed by ships, that apply effects in a circular zone around themselves.
@ -113,5 +132,7 @@ in the surrounding zone, except if less than a battle cycle passed since last ac
Drones are fully autonomous, and once deployed, are not controlled by their owner ship.
They are small and cannot be the direct target of weapons. They are not affected by area effects,
They are small and cannot be the direct target of weapons.
*Not done yet :* They are not affected by area effects,
except for area damage and area effects specifically designed for drones.

2
TODO
View file

@ -15,6 +15,7 @@
* Arena: add sticky effects indicators on ships
* Arena: add power indicator in ship hover information
* Arena: temporarily show ship information when it changes
* Arena: display important changes (damages, effects...) instead of attribute changes
* Suspend AI operation when the game is paused (window not focused)
* Add permanent effects to ship models to ease balancing
* Find incentives to move from starting position
@ -34,7 +35,6 @@
* Prevent arena effects information (eg. "shield -36") to overflow out of the arena
* Allow to undo last moves
* Add critical hit/miss
* Add an overheat/cooling system
* Add auto-move to attack
* Merge identical sticky effects
* Allow to skip animations and AI delays in battle

Binary file not shown.

After

Width:  |  Height:  |  Size: 708 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

View file

@ -4,7 +4,10 @@ module TS.SpaceTac.Specs {
let cooldown = new Cooldown();
expect(cooldown.canUse()).toBe(true);
cooldown.configure(2, 3);
cooldown.use();
expect(cooldown.canUse()).toBe(true);
cooldown.configure(2, 2);
expect(cooldown.canUse()).toBe(true);
cooldown.use();
@ -21,6 +24,12 @@ module TS.SpaceTac.Specs {
cooldown.cool();
expect(cooldown.canUse()).toBe(true);
/*cooldown.configure(1, 0);
expect(cooldown.canUse()).toBe(true);
cooldown.use();
expect(cooldown.canUse()).toBe(false);*/
});
});
}

View file

@ -15,6 +15,14 @@ module TS.SpaceTac {
// Number of turns needed to cooldown when overheated
cooling = 0
constructor(overheat = 0, cooling = 0) {
this.configure(overheat, cooling);
}
toString(): string {
return `Overheat ${this.overheat} / Cooldown ${this.cooling}`;
}
/**
* Check if the equipment can be used in regards to heat
*/
@ -22,21 +30,38 @@ module TS.SpaceTac {
return this.heat == 0;
}
/**
* Check if the equipment would overheat if used
*/
willOverheat(): boolean {
return this.overheat > 0 && this.uses + 1 >= this.overheat;
}
/**
* Check the number of uses before overheating
*/
getRemainingUses(): number {
return this.overheat - this.uses;
}
/**
* Configure the overheat and cooling
*/
configure(overheat: number, cooling: number) {
this.overheat = overheat;
this.cooling = cooling;
this.reset();
}
/**
* Use the equipment, increasing the heat
*/
use(): void {
this.uses += 1;
if (this.uses >= this.overheat) {
this.heat = this.cooling;
if (this.overheat) {
this.uses += 1;
if (this.uses >= this.overheat) {
this.heat = this.cooling + 1;
}
}
}
@ -49,5 +74,13 @@ module TS.SpaceTac {
this.heat -= 1;
}
}
/**
* Reset the cooldown (typically at the end of turn)
*/
reset(): void {
this.uses = 0;
this.heat = 0;
}
}
}

View file

@ -48,6 +48,9 @@ module TS.SpaceTac {
// Equipment wear due to usage in battles (will lower the sell price)
wear = 0
// Cooldown needed by the equipment
cooldown = new Cooldown()
// Basic constructor
constructor(slot: SlotType | null = null, code = "equipment") {
this.slot_type = slot;
@ -88,6 +91,9 @@ module TS.SpaceTac {
if (requirements.length > 0) {
description = "Requires:\n" + requirements.join("\n") + "\n\n" + description;
}
if (this.cooldown.overheat > 0) {
description = `${this.cooldown}\n\n${description}`;
}
if (this.wear > 0) {
description = (this.wear >= 100 ? "Worn" : "Second hand") + "\n\n" + description;
}

View file

@ -54,6 +54,23 @@ module TS.SpaceTac.Specs {
});
});
it("applies cooldown", function () {
let template = new LootTemplate(SlotType.Weapon, "Weapon");
template.setCooldown(istep(1), istep(2));
let result = template.generate(1);
expect(result.cooldown.overheat).toBe(1);
expect(result.cooldown.cooling).toBe(2);
result = template.generate(2);
expect(result.cooldown.overheat).toBe(2);
expect(result.cooldown.cooling).toBe(3);
result = template.generate(10);
expect(result.cooldown.overheat).toBe(10);
expect(result.cooldown.cooling).toBe(11);
});
it("applies attributes permenant effects", function () {
let template = new LootTemplate(SlotType.Shield, "Shield");
template.addAttributeEffect("shield_capacity", irange(undefined, 50, 10));

View file

@ -149,6 +149,11 @@ module TS.SpaceTac {
simpleFactor(equipment.action, 'distance_per_power');
}
if (equipment.cooldown.overheat) {
simpleFactor(equipment.cooldown, 'overheat', true);
simpleFactor(equipment.cooldown, 'cooling', true);
}
// Choose a random one
if (modifiers.length > 0) {
let chosen = random.choice(modifiers);
@ -248,6 +253,15 @@ module TS.SpaceTac {
});
}
/**
* Set the overheat/cooldown
*/
setCooldown(overheat: LeveledValue, cooldown: LeveledValue): void {
this.base_modifiers.push((equipment, level) => {
equipment.cooldown.configure(resolveForLevel(overheat, level), resolveForLevel(cooldown, level));
});
}
/**
* Add a permanent attribute effect, when the item is equipped.
*/

View file

@ -35,6 +35,31 @@ module TS.SpaceTac.Specs {
expect(ship.arena_angle).toBeCloseTo(3.14159265, 0.00001);
});
it("applies equipment cooldown", function () {
let ship = new Ship();
let equipment = new Equipment(SlotType.Weapon);
equipment.cooldown.configure(1, 1);
ship.addSlot(SlotType.Weapon).attach(equipment);
expect(equipment.cooldown.canUse()).toBe(true, 1);
equipment.cooldown.use();
expect(equipment.cooldown.canUse()).toBe(false, 2);
ship.startBattle();
expect(equipment.cooldown.canUse()).toBe(true, 3);
ship.startTurn();
equipment.cooldown.use();
expect(equipment.cooldown.canUse()).toBe(false, 4);
ship.endTurn();
expect(equipment.cooldown.canUse()).toBe(false, 5);
ship.startTurn();
expect(equipment.cooldown.canUse()).toBe(false, 6);
ship.endTurn();
expect(equipment.cooldown.canUse()).toBe(true, 7);
});
it("lists available actions from attached equipment", function () {
var ship = new Ship(null, "Test");
var actions: BaseAction[];

View file

@ -322,6 +322,7 @@ module TS.SpaceTac {
this.updateAttributes();
this.restoreHealth();
this.initializeActionPoints();
this.listEquipment().forEach(equipment => equipment.cooldown.reset());
}
/**
@ -371,6 +372,9 @@ module TS.SpaceTac {
// Apply sticky effects
this.sticky_effects.forEach(effect => effect.endTurn(this));
this.cleanStickyEffects();
// Cool down equipment
this.listEquipment().forEach(equipment => equipment.cooldown.cool());
}
}

View file

@ -26,6 +26,35 @@ module TS.SpaceTac {
expect(action.checkCannotBeApplied(ship)).toBe("not enough power");
});
it("check if equipment can be used with overheat", function () {
let equipment = new Equipment();
let action = new BaseAction("test", "Test", false, equipment);
let ship = new Ship();
expect(action.checkCannotBeApplied(ship)).toBe(null);
equipment.cooldown.use();
expect(action.checkCannotBeApplied(ship)).toBe(null);
equipment.cooldown.configure(2, 2);
expect(action.checkCannotBeApplied(ship)).toBe(null);
equipment.cooldown.use();
expect(action.checkCannotBeApplied(ship)).toBe(null);
equipment.cooldown.use();
expect(action.checkCannotBeApplied(ship)).toBe("overheated");
equipment.cooldown.cool();
expect(action.checkCannotBeApplied(ship)).toBe("overheated");
equipment.cooldown.cool();
expect(action.checkCannotBeApplied(ship)).toBe("overheated");
equipment.cooldown.cool();
expect(action.checkCannotBeApplied(ship)).toBe(null);
});
it("wears down equipment and power generators", function () {
let ship = new Ship();
TestTools.setShipAP(ship, 10);

View file

@ -2,16 +2,16 @@ module TS.SpaceTac {
// Base class for action definitions
export class BaseAction {
// Identifier code for the type of action
code: string;
code: string
// Human-readable name
name: string;
name: string
// Boolean at true if the action needs a target
needs_target: boolean;
needs_target: boolean
// Equipment that triggers this action
equipment: Equipment | null;
equipment: Equipment | null
// Create the action
constructor(code: string, name: string, needs_target: boolean, equipment: Equipment | null = null) {
@ -40,11 +40,16 @@ module TS.SpaceTac {
remaining_ap = ship.values.power.get();
}
var ap_usage = this.getActionPointsUsage(ship, null);
if (remaining_ap >= ap_usage) {
return null;
} else {
if (remaining_ap < ap_usage) {
return "not enough power";
}
// Check cooldown
if (this.equipment && !this.equipment.cooldown.canUse()) {
return "overheated";
}
return null;
}
// Get the number of action points the action applied to a target would use
@ -109,6 +114,8 @@ module TS.SpaceTac {
if (this.equipment) {
this.equipment.addWear(1);
ship.listEquipment(SlotType.Power).forEach(equipment => equipment.addWear(1));
this.equipment.cooldown.use();
}
this.customApply(ship, checked_target);

View file

@ -8,16 +8,16 @@ module TS.SpaceTac.Equipments {
expect(equipment.effects).toEqual([new AttributeEffect("shield_capacity", 100)]);
equipment = template.generate(2);
expect(equipment.requirements).toEqual({ "skill_energy": 2 });
expect(equipment.effects).toEqual([new AttributeEffect("shield_capacity", 150)]);
expect(equipment.requirements).toEqual({ "skill_energy": 3 });
expect(equipment.effects).toEqual([new AttributeEffect("shield_capacity", 140)]);
equipment = template.generate(3);
expect(equipment.requirements).toEqual({ "skill_energy": 3 });
expect(equipment.effects).toEqual([new AttributeEffect("shield_capacity", 200)]);
expect(equipment.requirements).toEqual({ "skill_energy": 5 });
expect(equipment.effects).toEqual([new AttributeEffect("shield_capacity", 180)]);
equipment = template.generate(10);
expect(equipment.requirements).toEqual({ "skill_energy": 10 });
expect(equipment.effects).toEqual([new AttributeEffect("shield_capacity", 550)]);
expect(equipment.requirements).toEqual({ "skill_energy": 19 });
expect(equipment.effects).toEqual([new AttributeEffect("shield_capacity", 460)]);
});
});
}

View file

@ -5,8 +5,8 @@ module TS.SpaceTac.Equipments {
constructor() {
super(SlotType.Shield, "Force Field", "A basic force field, generated by radiating waves of compressed energy");
this.setSkillsRequirements({ "skill_energy": 1 });
this.addAttributeEffect("shield_capacity", istep(100, irepeat(50)));
this.setSkillsRequirements({ "skill_energy": istep(1, irepeat(2)) });
this.addAttributeEffect("shield_capacity", istep(100, irepeat(40)));
}
}
}

View file

@ -6,6 +6,7 @@ module TS.SpaceTac.Equipments {
super(SlotType.Weapon, "Gatling Gun", "Mechanical weapon using loads of metal bullets propelled by guided explosions");
this.setSkillsRequirements({ "skill_material": 1 });
this.setCooldown(irepeat(2), irepeat(1));
this.addFireAction(irepeat(3), irepeat(600), 0, [
new EffectTemplate(new DamageEffect(), { base: istep(30, irepeat(5)), span: istep(20, irepeat(5)) })
]);

View file

@ -9,15 +9,15 @@ module TS.SpaceTac.Equipments {
equipment = template.generate(2);
expect(equipment.requirements).toEqual({ "skill_material": 2 });
expect(equipment.effects).toEqual([new AttributeEffect("hull_capacity", 250)]);
expect(equipment.effects).toEqual([new AttributeEffect("hull_capacity", 220)]);
equipment = template.generate(3);
expect(equipment.requirements).toEqual({ "skill_material": 3 });
expect(equipment.effects).toEqual([new AttributeEffect("hull_capacity", 300)]);
expect(equipment.effects).toEqual([new AttributeEffect("hull_capacity", 240)]);
equipment = template.generate(10);
expect(equipment.requirements).toEqual({ "skill_material": 10 });
expect(equipment.effects).toEqual([new AttributeEffect("hull_capacity", 650)]);
expect(equipment.effects).toEqual([new AttributeEffect("hull_capacity", 380)]);
});
});
}

View file

@ -6,7 +6,7 @@ module TS.SpaceTac.Equipments {
super(SlotType.Hull, "Iron Hull", "Protective hull, based on layered iron alloys");
this.setSkillsRequirements({ "skill_material": 1 });
this.addAttributeEffect("hull_capacity", istep(200, irepeat(50)));
this.addAttributeEffect("hull_capacity", istep(200, irepeat(20)));
}
}
}

View file

@ -6,6 +6,7 @@ module TS.SpaceTac.Equipments {
super(SlotType.Weapon, "Power Depleter", "Direct-hit weapon that creates an energy well near the target, sucking its power surplus");
this.setSkillsRequirements({ "skill_energy": 1 });
this.setCooldown(irepeat(2), irepeat(2));
this.addFireAction(irepeat(4), istep(500, irepeat(20)), 0, [
new StickyEffectTemplate(new AttributeLimitEffect("power_capacity"), { "value": irepeat(3) }, irepeat(2))
]);

View file

@ -9,6 +9,7 @@ module TS.SpaceTac.Equipments {
super(SlotType.Weapon, "Repair Drone", "Drone able to repair small hull breaches, remotely controlled by human pilots");
this.setSkillsRequirements({ "skill_human": 1 });
this.setCooldown(irepeat(1), istep(2, irepeat(0.2)));
this.addDroneAction(irepeat(4), istep(300, irepeat(10)), istep(1, irepeat(0.2)), istep(100, irepeat(10)), [
new EffectTemplate(new ValueEffect("hull"), { "value": istep(30, irepeat(3)) })
]);

View file

@ -6,6 +6,7 @@ module TS.SpaceTac.Equipments {
super(SlotType.Engine, "Rocket Engine", "First-era conventional deep-space engine, based on gas exhausts pushed through a nozzle");
this.setSkillsRequirements({ "skill_energy": 1 });
this.setCooldown(irepeat(3), 0);
this.addAttributeEffect("initiative", 1);
this.addMoveAction(istep(200, irepeat(20)));
}

View file

@ -67,6 +67,7 @@ module TS.SpaceTac.Equipments {
expect(battle.log.events[3]).toEqual(new DamageEvent(enemy2, 0, 20));
battle.log.clear();
equipment.cooldown.cool();
// Fire in space
target = Target.newFromLocation(2.4, 0);
@ -79,6 +80,7 @@ module TS.SpaceTac.Equipments {
expect(battle.log.events[2]).toEqual(new DamageEvent(enemy2, 10, 10));
battle.log.clear();
equipment.cooldown.cool();
// Fire far away
target = Target.newFromLocation(5, 0);

View file

@ -6,6 +6,7 @@ module TS.SpaceTac.Equipments {
super(SlotType.Weapon, "SubMunition Missile", "Explosive missile releasing small shelled payloads, that will in turn explode on impact");
this.setSkillsRequirements({ "skill_material": 1 });
this.setCooldown(irepeat(1), irepeat(0));
this.addFireAction(irepeat(4), istep(500, irepeat(20)), istep(150, irepeat(5)), [
new EffectTemplate(new DamageEffect(), { base: istep(30, irepeat(2)), span: istep(2, irepeat(1)) })
]);

View file

@ -33,7 +33,7 @@ module TS.SpaceTac.UI {
this.loadImage("battle/action-inactive.png");
this.loadImage("battle/action-active.png");
this.loadImage("battle/action-selected.png");
this.loadImage("battle/action-tooltip.png");
this.loadImage("battle/action-cooldown.png");
this.loadImage("battle/power-available.png");
this.loadImage("battle/power-using.png");
this.loadImage("battle/power-used.png");

View file

@ -104,4 +104,14 @@ module TS.SpaceTac.UI.Specs {
return [testgame.mapview, [session.universe, session.player]];
});
}
/**
* Check a given text node
*/
export function checkText(node: any, content: string): void {
expect(node instanceof Phaser.Text).toBe(true);
let tnode = <Phaser.Text>node;
expect(tnode.text).toEqual(content);
}
}

View file

@ -11,9 +11,6 @@ module TS.SpaceTac.UI {
// Power bar
power: Phaser.Group;
// Tooltip to display hovered action info
tooltip: ActionTooltip;
// Indicator of interaction disabled
icon_waiting: Phaser.Image;
@ -52,10 +49,6 @@ module TS.SpaceTac.UI {
this.icon_waiting.scale.set(0.5, 0.5);
this.addChild(this.icon_waiting);
// Tooltip
this.tooltip = new ActionTooltip(this);
this.addChild(this.tooltip);
// Key bindings
battleview.inputs.bind("Escape", "Cancel action", () => this.actionEnded());
battleview.inputs.bind(" ", "End turn", () => this.keyActionPressed(-1));
@ -128,16 +121,13 @@ module TS.SpaceTac.UI {
action.destroy();
});
this.action_icons = [];
this.tooltip.setAction(null);
}
// Add an action icon
addAction(ship: Ship, action: BaseAction): ActionIcon {
var icon = new ActionIcon(this, 192 + this.action_icons.length * 88, 8, ship, action);
var icon = new ActionIcon(this, 192 + this.action_icons.length * 88, 8, ship, action, this.action_icons.length);
this.action_icons.push(icon);
this.tooltip.bringToTop();
return icon;
}
@ -187,7 +177,7 @@ module TS.SpaceTac.UI {
}
this.action_icons.forEach((icon: ActionIcon) => {
icon.updateFadingStatus(remaining_ap);
icon.updateFadingStatus(remaining_ap, true);
});
this.updatePower(power_usage);
}

View file

@ -34,8 +34,11 @@ module TS.SpaceTac.UI {
// Layer applied when the action is selected
private layer_selected: Phaser.Image;
// Cooldown indicators
private layer_cooldown: Phaser.Group
// Create an icon for a single ship action
constructor(bar: ActionBar, x: number, y: number, ship: Ship, action: BaseAction) {
constructor(bar: ActionBar, x: number, y: number, ship: Ship, action: BaseAction, position: number) {
super(bar.game, x, y, "battle-action-inactive");
this.bar = bar;
@ -65,22 +68,24 @@ module TS.SpaceTac.UI {
this.layer_icon.scale.set(0.25, 0.25);
this.addChild(this.layer_icon);
let show_info = () => {
if (this.bar.ship) {
this.bar.tooltip.setAction(this);
this.battleview.arena.range_hint.setSecondary(this.ship, this.action);
}
};
let hide_info = () => {
this.bar.tooltip.setAction(null);
this.battleview.arena.range_hint.clearSecondary();
};
// Cooldown layer
this.layer_cooldown = new Phaser.Group(this.game);
this.addChild(this.layer_cooldown);
// Events
UITools.setHoverClick(this, show_info, hide_info, () => this.processClick());
this.battleview.tooltip.bind(this, filler => {
ActionTooltip.fill(filler, this.ship, this.action, position);
return true;
});
UITools.setHoverClick(this,
() => this.battleview.arena.range_hint.setSecondary(this.ship, this.action),
() => this.battleview.arena.range_hint.clearSecondary(),
() => this.processClick()
);
// Initialize
this.updateActiveStatus(true);
this.updateCooldownStatus();
}
// Process a click event on the action icon
@ -154,6 +159,7 @@ module TS.SpaceTac.UI {
this.targetting = null;
}
this.setSelected(false);
this.updateCooldownStatus();
this.updateActiveStatus();
this.updateFadingStatus(this.ship.values.power.get());
this.battleview.arena.range_hint.clearPrimary();
@ -165,6 +171,24 @@ module TS.SpaceTac.UI {
this.battleview.animations.setVisible(this.layer_selected, this.selected, 300);
}
// Update the cooldown status
updateCooldownStatus(): void {
this.layer_cooldown.removeAll();
if (this.action.equipment) {
let cooldown = this.action.equipment.cooldown;
let count = cooldown.heat ? cooldown.heat : (cooldown.willOverheat() ? cooldown.cooling + 1 : 0);
if (count) {
let positions = UITools.evenlySpace(68, 18, count);
range(count).forEach(i => {
let dot = new Phaser.Image(this.game, 10 + positions[i], 10, "battle-action-cooldown");
dot.anchor.set(0.5, 0.5);
dot.alpha = cooldown.heat ? 1 : 0.5;
this.layer_cooldown.add(dot);
});
}
}
}
// Update the active status, from the action canBeUsed result
updateActiveStatus(force = false): void {
var old_active = this.active;
@ -177,9 +201,10 @@ module TS.SpaceTac.UI {
}
// Update the fading status, given an hypothetical remaining AP
updateFadingStatus(remaining_ap: number): void {
var old_fading = this.fading;
this.fading = this.active && (this.action.checkCannotBeApplied(this.ship, remaining_ap) != null);
updateFadingStatus(remaining_ap: number, action = false): void {
let old_fading = this.fading;
let overheat = action && (this.action.equipment !== null && this.action.equipment.cooldown.willOverheat());
this.fading = this.active && (this.action.checkCannotBeApplied(this.ship, remaining_ap) != null || overheat);
if (this.fading != old_fading) {
this.battleview.animations.setVisible(this.layer_active, this.active && !this.fading, 500);
}

View file

@ -2,41 +2,39 @@
module TS.SpaceTac.UI.Specs {
describe("ActionTooltip", function () {
let testgame = setupBattleview();
let testgame = setupEmptyView();
it("displays action information", () => {
let battleview = testgame.battleview;
let bar = battleview.action_bar;
let tooltip = bar.tooltip;
let tooltip = new Tooltip(testgame.baseview);
let ship = new Ship();
TestTools.setShipAP(ship, 10);
bar.clearAll();
let ship = nn(battleview.battle.playing_ship);
let a1 = bar.addAction(ship, new MoveAction(new Equipment()));
nn(a1.action.equipment).name = "Engine";
a1.action.name = "Move";
let a2 = bar.addAction(ship, new FireWeaponAction(new Equipment(), 2, 50, 0, [new DamageEffect(12)]));
nn(a2.action.equipment).name = "Weapon";
a2.action.name = "Fire";
let a3 = bar.addAction(ship, new EndTurnAction());
a3.action.name = "End turn";
let action1 = new MoveAction(new Equipment());
nn(action1.equipment).name = "Engine";
action1.name = "Move";
let action2 = new FireWeaponAction(new Equipment(), 2, 50, 0, [new DamageEffect(12)]);
nn(action2.equipment).name = "Weapon";
action2.name = "Fire";
let action3 = new EndTurnAction();
action3.name = "End turn";
tooltip.setAction(a1);
expect(tooltip.main_title.text).toEqual("Engine");
expect(tooltip.sub_title.text).toEqual("Move");
expect(tooltip.shortcut.text).toEqual("[ 1 ]");
expect(tooltip.description.text).toEqual("Move: 0km per power point");
ActionTooltip.fill(tooltip.getFiller(), ship, action1, 0);
checkText((<any>tooltip).container.content.children[1], "Engine");
checkText((<any>tooltip).container.content.children[2], "Cost: 1 power per 0km");
checkText((<any>tooltip).container.content.children[3], "Move: 0km per power point");
checkText((<any>tooltip).container.content.children[4], "[ 1 ]");
tooltip.setAction(a2);
expect(tooltip.main_title.text).toEqual("Weapon");
expect(tooltip.sub_title.text).toEqual("Fire");
expect(tooltip.shortcut.text).toEqual("[ 2 ]");
expect(tooltip.description.text).toEqual("Fire (power usage 2, max range 50km):\n• do 12 damage on target");
tooltip.hide();
ActionTooltip.fill(tooltip.getFiller(), ship, action2, 1);
checkText((<any>tooltip).container.content.children[1], "Weapon");
checkText((<any>tooltip).container.content.children[2], "Cost: 2 power");
checkText((<any>tooltip).container.content.children[3], "Fire (power usage 2, max range 50km):\n• do 12 damage on target");
checkText((<any>tooltip).container.content.children[4], "[ 2 ]");
tooltip.setAction(a3);
expect(tooltip.main_title.text).toEqual("End turn");
expect(tooltip.sub_title.text).toEqual("");
expect(tooltip.shortcut.text).toEqual("[ space ]");
expect(tooltip.description.text).toEqual("");
tooltip.hide();
ActionTooltip.fill(tooltip.getFiller(), ship, action3, 2);
checkText((<any>tooltip).container.content.children[1], "End turn");
checkText((<any>tooltip).container.content.children[2], "[ space ]");
});
});
}

View file

@ -1,79 +1,74 @@
module TS.SpaceTac.UI {
// Tooltip to display action information
export class ActionTooltip extends Phaser.Sprite {
bar: ActionBar;
icon: Phaser.Image | null;
main_title: Phaser.Text;
sub_title: Phaser.Text;
cost: Phaser.Text;
description: Phaser.Text;
shortcut: Phaser.Text;
/**
* Tooltip displaying action information
*/
export class ActionTooltip {
/**
* Fill the tooltip
*/
static fill(filler: TooltipFiller, ship: Ship, action: BaseAction, position: number) {
filler.addImage(0, 0, "battle-actions-" + action.code, 0.5);
constructor(parent: ActionBar) {
super(parent.game, 0, 0, "battle-action-tooltip");
this.bar = parent;
this.visible = false;
filler.addText(150, 0, action.equipment ? action.equipment.name : action.name, "#ffffff", 24);
this.icon = null;
this.main_title = new Phaser.Text(this.game, 325, 20, "", { font: "24pt Arial", fill: "#ffffff" });
this.main_title.anchor.set(0.5, 0);
this.addChild(this.main_title);
this.sub_title = new Phaser.Text(this.game, 325, 60, "", { font: "22pt Arial", fill: "#ffffff" });
this.sub_title.anchor.set(0.5, 0);
this.addChild(this.sub_title);
this.cost = new Phaser.Text(this.game, 325, 100, "", { font: "20pt Arial", fill: "#ffff00" });
this.cost.anchor.set(0.5, 0);
this.addChild(this.cost);
this.description = new Phaser.Text(this.game, 21, 144, "", { font: "14pt Arial", fill: "#ffffff" });
this.description.wordWrap = true;
this.description.wordWrapWidth = 476;
this.addChild(this.description);
this.shortcut = new Phaser.Text(this.game, this.width - 5, this.height - 5, "", { font: "12pt Arial", fill: "#aaaaaa" });
this.shortcut.anchor.set(1, 1);
this.addChild(this.shortcut);
}
// Set current action to display, null to hide
setAction(action: ActionIcon | null): void {
if (action) {
if (this.icon) {
this.icon.destroy(true);
}
this.icon = new Phaser.Image(this.game, 76, 72, "battle-actions-" + action.action.code);
this.icon.anchor.set(0.5, 0.5);
this.icon.scale.set(0.44, 0.44);
this.addChild(this.icon);
this.position.set(action.x, action.y + action.height + 44);
this.main_title.setText(action.action.equipment ? action.action.equipment.name : action.action.name);
this.sub_title.setText(action.action.equipment ? action.action.name : "");
if (action.action instanceof MoveAction) {
this.cost.setText(`Cost: 1 power per ${action.action.distance_per_power}km`);
let cost = "";
if (action instanceof MoveAction) {
if (ship.getValue("power") == 0) {
cost = "Not enough power";
} else {
let cost = action.action.getActionPointsUsage(action.ship, null);
this.cost.setText(cost == 0 ? "" : `Cost: ${cost} power`);
cost = `Cost: 1 power per ${action.distance_per_power}km`;
}
this.description.setText(action.action.getEffectsDescription());
} else if (action.equipment) {
let power_usage = action.getActionPointsUsage(ship, null);
if (power_usage) {
if (ship.getValue("power") < power_usage) {
cost = "Not enough power";
} else {
cost = `Cost: ${power_usage} power`;
}
}
}
if (cost) {
filler.addText(150, 50, cost, "#ffdd4b", 20);
}
let position = this.bar.action_icons.indexOf(action);
if (action.action instanceof EndTurnAction) {
this.shortcut.setText("[ space ]");
} else if (position == 9) {
this.shortcut.setText("[ 0 ]");
} else if (position >= 0 && position < 9) {
this.shortcut.setText(`[ ${position + 1} ]`);
if (action.equipment && action.equipment.cooldown.overheat) {
let cooldown = action.equipment.cooldown;
let uses = cooldown.getRemainingUses();
let uses_message = "";
if (uses == 0) {
uses_message = "Overheated !";
if (cooldown.heat == 1) {
uses_message += " Available next turn";
} else if (cooldown.heat == 2) {
uses_message += " Unavailable next turn";
} else {
uses_message += ` Unavailable next ${cooldown.heat - 1} turns`;
}
} else {
this.shortcut.setText("");
uses_message = uses == 1 ? "Overheats if used" : `${uses} uses before overheat`;
if (cooldown.cooling) {
uses_message += ` (for ${cooldown.cooling} turn${cooldown.cooling ? "s" : ""})`;
}
}
filler.addText(150, 90, uses_message, "#c9604c", 20);
}
this.bar.battleview.animations.show(this, 200, 0.9);
} else {
this.bar.battleview.animations.hide(this, 200);
let description = action.getEffectsDescription();
if (description) {
filler.addText(0, 150, description, "#ffffff", 14);
}
let shortcut = "";
if (action instanceof EndTurnAction) {
shortcut = "[ space ]";
} else if (position == 9) {
shortcut = "[ 0 ]";
} else if (position >= 0 && position < 9) {
shortcut = `[ ${position + 1} ]`;
}
if (shortcut) {
filler.addText(0, 0, shortcut, "#aaaaaa", 12);
}
}
}

View file

@ -219,5 +219,12 @@ module TS.SpaceTac.UI {
this.battleview.animations.setVisible(this.battleview.background, !active, 200);
}
}
/**
* Get the boundaries of the arena on display
*/
getBoundaries(): IBounded {
return { x: 130, y: 140, width: 1920 - 138, height: 1080 - 148 };
}
}
}

View file

@ -21,7 +21,7 @@ module TS.SpaceTac.UI {
let filler = this.getFiller();
filler.configure(10, 6, { x: 130, y: 140, width: 1920 - 138, height: 1080 - 148 });
filler.configure(10, 6, this.battleview.arena.getBoundaries());
filler.addImage(0, 0, `ship-${ship.model.code}-portrait`, 0.5);

View file

@ -1,17 +1,68 @@
module TS.SpaceTac.UI.Specs {
describe("UITools", function () {
let testgame = setupEmptyView();
describe("in UI", function () {
let testgame = setupEmptyView();
it("keeps objects inside bounds", function () {
let image = testgame.baseview.add.graphics(150, 100);
image.beginFill(0xff0000);
image.drawEllipse(50, 25, 50, 25);
image.endFill();
it("keeps objects inside bounds", function () {
let image = testgame.baseview.add.graphics(150, 100);
image.beginFill(0xff0000);
image.drawEllipse(50, 25, 50, 25);
image.endFill();
UITools.keepInside(image, { x: 0, y: 0, width: 200, height: 200 });
UITools.keepInside(image, { x: 0, y: 0, width: 200, height: 200 });
expect(image.x).toBe(100);
expect(image.y).toBe(100);
expect(image.x).toBe(100);
expect(image.y).toBe(100);
});
it("handles hover and click on desktops and mobile targets", function (done) {
jasmine.clock().uninstall();
let newButton: () => [Phaser.Button, any] = () => {
var button = new Phaser.Button(testgame.ui);
var funcs = {
enter: () => null,
leave: () => null,
click: () => null,
};
spyOn(funcs, "enter");
spyOn(funcs, "leave");
spyOn(funcs, "click");
UITools.setHoverClick(button, funcs.enter, funcs.leave, funcs.click, 50, 100);
return [button, funcs];
}
// Simple click on desktop
let [button, funcs] = newButton();
button.onInputOver.dispatch();
button.onInputDown.dispatch();
button.onInputUp.dispatch();
expect(funcs.enter).toHaveBeenCalledTimes(0);
expect(funcs.leave).toHaveBeenCalledTimes(0);
expect(funcs.click).toHaveBeenCalledTimes(1);
// Simple click on mobile
[button, funcs] = newButton();
button.onInputDown.dispatch();
button.onInputUp.dispatch();
expect(funcs.enter).toHaveBeenCalledTimes(1);
expect(funcs.leave).toHaveBeenCalledTimes(1);
expect(funcs.click).toHaveBeenCalledTimes(1);
// Hold to hover on mobile
[button, funcs] = newButton();
button.onInputDown.dispatch();
Timer.global.schedule(150, () => {
expect(funcs.enter).toHaveBeenCalledTimes(1);
expect(funcs.leave).toHaveBeenCalledTimes(0);
expect(funcs.click).toHaveBeenCalledTimes(0);
button.onInputUp.dispatch();
expect(funcs.enter).toHaveBeenCalledTimes(1);
expect(funcs.leave).toHaveBeenCalledTimes(1);
expect(funcs.click).toHaveBeenCalledTimes(0);
done();
});
});
});
it("normalizes angles", function () {
@ -23,53 +74,12 @@ module TS.SpaceTac.UI.Specs {
expect(UITools.normalizeAngle(-Math.PI - 0.5)).toBeCloseTo(Math.PI - 0.5, 0.000001);
});
it("handles hover and click on desktops and mobile targets", function (done) {
jasmine.clock().uninstall();
let newButton: () => [Phaser.Button, any] = () => {
var button = new Phaser.Button(testgame.ui);
var funcs = {
enter: () => null,
leave: () => null,
click: () => null,
};
spyOn(funcs, "enter");
spyOn(funcs, "leave");
spyOn(funcs, "click");
UITools.setHoverClick(button, funcs.enter, funcs.leave, funcs.click, 50, 100);
return [button, funcs];
}
// Simple click on desktop
let [button, funcs] = newButton();
button.onInputOver.dispatch();
button.onInputDown.dispatch();
button.onInputUp.dispatch();
expect(funcs.enter).toHaveBeenCalledTimes(0);
expect(funcs.leave).toHaveBeenCalledTimes(0);
expect(funcs.click).toHaveBeenCalledTimes(1);
// Simple click on mobile
[button, funcs] = newButton();
button.onInputDown.dispatch();
button.onInputUp.dispatch();
expect(funcs.enter).toHaveBeenCalledTimes(1);
expect(funcs.leave).toHaveBeenCalledTimes(1);
expect(funcs.click).toHaveBeenCalledTimes(1);
// Hold to hover on mobile
[button, funcs] = newButton();
button.onInputDown.dispatch();
Timer.global.schedule(150, () => {
expect(funcs.enter).toHaveBeenCalledTimes(1);
expect(funcs.leave).toHaveBeenCalledTimes(0);
expect(funcs.click).toHaveBeenCalledTimes(0);
button.onInputUp.dispatch();
expect(funcs.enter).toHaveBeenCalledTimes(1);
expect(funcs.leave).toHaveBeenCalledTimes(1);
expect(funcs.click).toHaveBeenCalledTimes(0);
done();
});
it("spaces items evenly", function () {
expect(UITools.evenlySpace(100, 20, 0)).toEqual([]);
expect(UITools.evenlySpace(100, 20, 1)).toEqual([50]);
expect(UITools.evenlySpace(100, 20, 2)).toEqual([25, 75]);
expect(UITools.evenlySpace(100, 20, 5)).toEqual([10, 30, 50, 70, 90]);
expect(UITools.evenlySpace(100, 20, 6)).toEqual([10, 26, 42, 58, 74, 90]);
});
});
}

View file

@ -131,5 +131,21 @@ module TS.SpaceTac.UI {
return angle;
}
}
/**
* Evenly space identical items in a parent
*
* Returns the relative position of item's center inside parent_width
*/
static evenlySpace(parent_width: number, item_width: number, item_count: number): number[] {
if (item_width * item_count <= parent_width) {
let spacing = parent_width / item_count;
return range(item_count).map(i => (i + 0.5) * spacing);
} else {
let breadth = parent_width - item_width;
let spacing = breadth / (item_count - 1);
return range(item_count).map(i => item_width / 2 + i * spacing);
}
}
}
}