From a8d036929275a0c3cb700444bc8490af61b7bb80 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C3=ABl=20Lemaire?= Date: Wed, 17 May 2017 01:12:05 +0200 Subject: [PATCH] Added equipment overheat / cooldown --- README.md | 23 +++- TODO | 2 +- out/assets/images/battle/action-cooldown.png | Bin 0 -> 708 bytes out/assets/images/battle/action-tooltip.png | Bin 1864 -> 0 bytes src/core/Cooldown.spec.ts | 11 +- src/core/Cooldown.ts | 39 +++++- src/core/Equipment.ts | 6 + src/core/LootTemplate.spec.ts | 17 +++ src/core/LootTemplate.ts | 14 ++ src/core/Ship.spec.ts | 25 ++++ src/core/Ship.ts | 4 + src/core/actions/BaseAction.spec.ts | 29 ++++ src/core/actions/BaseAction.ts | 21 ++- src/core/equipments/ForceField.spec.ts | 12 +- src/core/equipments/ForceField.ts | 4 +- src/core/equipments/GatlingGun.ts | 1 + src/core/equipments/IronHull.spec.ts | 6 +- src/core/equipments/IronHull.ts | 2 +- src/core/equipments/PowerDepleter.ts | 1 + src/core/equipments/RepairDrone.ts | 1 + src/core/equipments/RocketEngine.ts | 1 + .../equipments/SubMunitionMissile.spec.ts | 2 + src/core/equipments/SubMunitionMissile.ts | 1 + src/ui/Preload.ts | 2 +- src/ui/TestGame.ts | 10 ++ src/ui/battle/ActionBar.ts | 14 +- src/ui/battle/ActionIcon.ts | 55 ++++++-- src/ui/battle/ActionTooltip.spec.ts | 56 ++++---- src/ui/battle/ActionTooltip.ts | 129 +++++++++--------- src/ui/battle/Arena.ts | 7 + src/ui/battle/ShipTooltip.ts | 2 +- src/ui/common/UITools.spec.ts | 122 +++++++++-------- src/ui/common/UITools.ts | 16 +++ 33 files changed, 429 insertions(+), 206 deletions(-) create mode 100644 out/assets/images/battle/action-cooldown.png delete mode 100644 out/assets/images/battle/action-tooltip.png diff --git a/README.md b/README.md index 6512f7b..60cf88f 100644 --- a/README.md +++ b/README.md @@ -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. diff --git a/TODO b/TODO index af3a80b..e9a276f 100644 --- a/TODO +++ b/TODO @@ -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 diff --git a/out/assets/images/battle/action-cooldown.png b/out/assets/images/battle/action-cooldown.png new file mode 100644 index 0000000000000000000000000000000000000000..298a9b509939a0485d581233e779eacf0de68755 GIT binary patch literal 708 zcmV;#0z3VQP)>d9DU}2n*MZR+!DJhdtj>`|ll@rW;oPCH-%mALXVIU?%pH8K zkW4&?zgpnd--e-*{SMG<{=SK(k>N<+IDc;H5zzdrf+^bwbS zzXTG-f1L-?Xn?_7wnK*AUjFCJSrKD?PG1;Q#2 zF5%&PEP8tVUF1Ndl}4=!iO_;Oo8NRPkXWyHw_?kvn$Na{w6gtel@UIzPAHIs8@)m* zxqGl}M6Oq%oG#`TU6fM1&u#RgHJE)~ikoxWp;R_j2k)uk1P(HKK)zeDO2 zQn8$9=syusAiY(;a@aMxsI_qIcS9rS9`r;kHa@CA6h#SU{&>JPemO@YRa+2iKb7kl zI~%a@R9S%l0Osb+VcX=T*`uB-Ii%9<+lh%E&GC~rXPmyFNUOf<(-K0c!k7G+cqn); zwe;Lw&U|L%of-fDbg6ZqVmYyU&=awboewyC&ImOMQf}-0|B{~)Qt)EMZjZSD00006OS$YZRQma+0EYp-|FKa78x@ZY}VL?JLi1#R%R;4JJ1^dIBAmPEC43GWU$!>=;@d-{pp2Ad1BfbHa)7??_Z_&UZs+= z<+T6wQ)48Cc2v@bM=DaXD^8;yRLBd;WFVKzMMWhS%Fh*)%0y^cVaIx0Ai#ki(vnY| zsnlBM^d;m>;K{eM+F#5F5fEM5>Q-2*fi>o*uigF`H+FqIv#1Xs-t##an)8it;jnq7 zrF&PY7x{yxOks-xctUvrunz$oJPII?0Gu@Cr&8a`Hd0xIQN$)T!~*x2pAT;4R2SH_ zTXimF_Xrr;L;P^7bSUZp%dmN4RUyxtN>ZNg)!7zLvC+x zAq%tf!GI;X6$t4yg-{H79tZdyOA?*Eg**tL9VFt?MDEl#M~30FBE zueH<-2`c%daIgt4x_OH)u1+@(s>nG$9ufSg5bqlsYvJFvIFuuMi!Z2lAIY3fhoz!F z*X@6PodrDO7w+fn^Cu{MPmK72cX)5T*hh-Zb#4yG`JV7cWhkuAZ4kS~)06j^jmxGt zk>f{4%WPSMH3n8MNg&$EZ*X|<%pHi-CNY_KEhkvK$$~cUz4leK575z2W-fZl!R{l%OGl#zgx=%PLLt#ac@K9wRSyA*(L2b;EFV2W~ zg;v0wdc>gNO%~^MhZ)*wA(O)kuL5YtXjZ3kCaeMFQ~uIiuG!{IUa;%bFX%a)tR*EZWY~ZmECBS?M@Gp zJ&JtG@L=mv#Rx!%Pz&Eke zFdOSMV{;~TH72~<$C3u43CP_hoRnR$TI$APW(S1!`2^6C#x`(2P6xs=f8SNYhY5>D zjO20^wTs&I?hab>lSW2(1{HFMe5o2)(ta6anDO(%zs=|PwIR9&A=h-N5}sVUf&lTm vhKn#^B89Ft`$Xoy3)-_M_BC(*Kh*96!3|E&dqIXKy6Zt&N@nt%L}|@mzpoFC diff --git a/src/core/Cooldown.spec.ts b/src/core/Cooldown.spec.ts index 39ce81c..2bb01eb 100644 --- a/src/core/Cooldown.spec.ts +++ b/src/core/Cooldown.spec.ts @@ -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);*/ }); }); } \ No newline at end of file diff --git a/src/core/Cooldown.ts b/src/core/Cooldown.ts index 2e35080..1d21caa 100644 --- a/src/core/Cooldown.ts +++ b/src/core/Cooldown.ts @@ -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; + } } } diff --git a/src/core/Equipment.ts b/src/core/Equipment.ts index 780c48d..7c64ba9 100644 --- a/src/core/Equipment.ts +++ b/src/core/Equipment.ts @@ -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; } diff --git a/src/core/LootTemplate.spec.ts b/src/core/LootTemplate.spec.ts index c563d29..5de1ec4 100644 --- a/src/core/LootTemplate.spec.ts +++ b/src/core/LootTemplate.spec.ts @@ -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)); diff --git a/src/core/LootTemplate.ts b/src/core/LootTemplate.ts index a34d3ed..6a9a2a5 100644 --- a/src/core/LootTemplate.ts +++ b/src/core/LootTemplate.ts @@ -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. */ diff --git a/src/core/Ship.spec.ts b/src/core/Ship.spec.ts index b75de69..34d0987 100644 --- a/src/core/Ship.spec.ts +++ b/src/core/Ship.spec.ts @@ -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[]; diff --git a/src/core/Ship.ts b/src/core/Ship.ts index fe1c2c2..939d621 100644 --- a/src/core/Ship.ts +++ b/src/core/Ship.ts @@ -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()); } } diff --git a/src/core/actions/BaseAction.spec.ts b/src/core/actions/BaseAction.spec.ts index 8f706a1..024ba33 100644 --- a/src/core/actions/BaseAction.spec.ts +++ b/src/core/actions/BaseAction.spec.ts @@ -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); diff --git a/src/core/actions/BaseAction.ts b/src/core/actions/BaseAction.ts index 68c7a83..d09d9f1 100644 --- a/src/core/actions/BaseAction.ts +++ b/src/core/actions/BaseAction.ts @@ -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); diff --git a/src/core/equipments/ForceField.spec.ts b/src/core/equipments/ForceField.spec.ts index 34fd06d..bfc8864 100644 --- a/src/core/equipments/ForceField.spec.ts +++ b/src/core/equipments/ForceField.spec.ts @@ -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)]); }); }); } diff --git a/src/core/equipments/ForceField.ts b/src/core/equipments/ForceField.ts index e403365..ac44f5b 100644 --- a/src/core/equipments/ForceField.ts +++ b/src/core/equipments/ForceField.ts @@ -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))); } } } diff --git a/src/core/equipments/GatlingGun.ts b/src/core/equipments/GatlingGun.ts index 7c6b0fa..b018789 100644 --- a/src/core/equipments/GatlingGun.ts +++ b/src/core/equipments/GatlingGun.ts @@ -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)) }) ]); diff --git a/src/core/equipments/IronHull.spec.ts b/src/core/equipments/IronHull.spec.ts index ff2c608..250bc9a 100644 --- a/src/core/equipments/IronHull.spec.ts +++ b/src/core/equipments/IronHull.spec.ts @@ -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)]); }); }); } diff --git a/src/core/equipments/IronHull.ts b/src/core/equipments/IronHull.ts index 2ba3042..94fc00f 100644 --- a/src/core/equipments/IronHull.ts +++ b/src/core/equipments/IronHull.ts @@ -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))); } } } diff --git a/src/core/equipments/PowerDepleter.ts b/src/core/equipments/PowerDepleter.ts index 55f9bac..edd0353 100644 --- a/src/core/equipments/PowerDepleter.ts +++ b/src/core/equipments/PowerDepleter.ts @@ -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)) ]); diff --git a/src/core/equipments/RepairDrone.ts b/src/core/equipments/RepairDrone.ts index a75ba6f..c6aef7e 100644 --- a/src/core/equipments/RepairDrone.ts +++ b/src/core/equipments/RepairDrone.ts @@ -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)) }) ]); diff --git a/src/core/equipments/RocketEngine.ts b/src/core/equipments/RocketEngine.ts index f25449c..39fa751 100644 --- a/src/core/equipments/RocketEngine.ts +++ b/src/core/equipments/RocketEngine.ts @@ -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))); } diff --git a/src/core/equipments/SubMunitionMissile.spec.ts b/src/core/equipments/SubMunitionMissile.spec.ts index 3e695e1..577577c 100644 --- a/src/core/equipments/SubMunitionMissile.spec.ts +++ b/src/core/equipments/SubMunitionMissile.spec.ts @@ -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); diff --git a/src/core/equipments/SubMunitionMissile.ts b/src/core/equipments/SubMunitionMissile.ts index 9c1faf1..d434f1f 100644 --- a/src/core/equipments/SubMunitionMissile.ts +++ b/src/core/equipments/SubMunitionMissile.ts @@ -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)) }) ]); diff --git a/src/ui/Preload.ts b/src/ui/Preload.ts index 681658e..a220056 100644 --- a/src/ui/Preload.ts +++ b/src/ui/Preload.ts @@ -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"); diff --git a/src/ui/TestGame.ts b/src/ui/TestGame.ts index aff59e5..ba83da2 100644 --- a/src/ui/TestGame.ts +++ b/src/ui/TestGame.ts @@ -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 = node; + expect(tnode.text).toEqual(content); + } } diff --git a/src/ui/battle/ActionBar.ts b/src/ui/battle/ActionBar.ts index 0618a78..97a2963 100644 --- a/src/ui/battle/ActionBar.ts +++ b/src/ui/battle/ActionBar.ts @@ -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); } diff --git a/src/ui/battle/ActionIcon.ts b/src/ui/battle/ActionIcon.ts index 75c3a1e..dc68bb3 100644 --- a/src/ui/battle/ActionIcon.ts +++ b/src/ui/battle/ActionIcon.ts @@ -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); } diff --git a/src/ui/battle/ActionTooltip.spec.ts b/src/ui/battle/ActionTooltip.spec.ts index ae34030..4dd9466 100644 --- a/src/ui/battle/ActionTooltip.spec.ts +++ b/src/ui/battle/ActionTooltip.spec.ts @@ -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((tooltip).container.content.children[1], "Engine"); + checkText((tooltip).container.content.children[2], "Cost: 1 power per 0km"); + checkText((tooltip).container.content.children[3], "Move: 0km per power point"); + checkText((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((tooltip).container.content.children[1], "Weapon"); + checkText((tooltip).container.content.children[2], "Cost: 2 power"); + checkText((tooltip).container.content.children[3], "Fire (power usage 2, max range 50km):\n• do 12 damage on target"); + checkText((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((tooltip).container.content.children[1], "End turn"); + checkText((tooltip).container.content.children[2], "[ space ]"); }); }); } diff --git a/src/ui/battle/ActionTooltip.ts b/src/ui/battle/ActionTooltip.ts index ba0915f..8e58e2d 100644 --- a/src/ui/battle/ActionTooltip.ts +++ b/src/ui/battle/ActionTooltip.ts @@ -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); } } } diff --git a/src/ui/battle/Arena.ts b/src/ui/battle/Arena.ts index 4b37325..38a1bd2 100644 --- a/src/ui/battle/Arena.ts +++ b/src/ui/battle/Arena.ts @@ -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 }; + } } } diff --git a/src/ui/battle/ShipTooltip.ts b/src/ui/battle/ShipTooltip.ts index d6a3a00..099749c 100644 --- a/src/ui/battle/ShipTooltip.ts +++ b/src/ui/battle/ShipTooltip.ts @@ -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); diff --git a/src/ui/common/UITools.spec.ts b/src/ui/common/UITools.spec.ts index 81439d6..932e4b2 100644 --- a/src/ui/common/UITools.spec.ts +++ b/src/ui/common/UITools.spec.ts @@ -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]); }); }); } diff --git a/src/ui/common/UITools.ts b/src/ui/common/UITools.ts index a997736..b763289 100644 --- a/src/ui/common/UITools.ts +++ b/src/ui/common/UITools.ts @@ -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); + } + } } }