diff --git a/README.md b/README.md index 87edf2f..5787c54 100644 --- a/README.md +++ b/README.md @@ -23,6 +23,14 @@ After making changes to sources, you need to recompile: ## Ships +### Level and experience + +A ship gains experience during battles. When reaching a certain amount of experience points, +a ship will automatically level up (which is, gain 1 level). + +A ship starts at level 1. There is no upper limit to level value (except 99, for display sake, +but it may not be reached in a classic campaign). + ### In-combat values (HSP) In combat, a ship's vitals are represented by the HSP system (Hull-Shield-Power): @@ -55,8 +63,6 @@ or a temporary effect caused by a weapon or a drone). For example, a ship that equips a power generator with "power recovery +3", but has a sticky effect of "power recovery -1" from a previous weapon hit, will have an effective power recovery of 2. -Attributes may also be upgraded permanently during level up. - ### Skills Skills represent a ship's ability to use equipments: @@ -72,7 +78,8 @@ Each equipment has minimal skill requirements to be used. For example, a weapon and "energy >= 3" to be equipped. A ship that does not meet these requirements will not be able to use the equipment. -Like for attributes, skill values are controlled by equipments, effects and level up. +Skills are defined by the player, using points given while leveling up. +As for attributes, skill values may also be altered by equipments. If an equipped item has a requirement of "time skill >= 2", that the ship has "time skill" of exactly 2, and that a temporary effect of "time skill -1" is active, the requirement is no longer fulfilled and the equipped diff --git a/TODO b/TODO index 1b2f7fb..e24b0de 100644 --- a/TODO +++ b/TODO @@ -1,9 +1,10 @@ * UI: Use a common component class, and a layer abstraction * Character sheet: allow item moving to another ship * Character sheet: add tooltips (on values, slots and equipments) -* Character sheet: add levelling up (spending of available points) + initial character creation +* Character sheet: add initial character creation * Character sheet: disable interaction during battle (except for loot screen) * Add battle statistics and/or critics in outcome dialog +* Add battle experience and feedback on level up * 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) diff --git a/out/assets/images/battle/action-tooltip.png b/out/assets/images/battle/action-tooltip.png index 08438c0..f8d45a1 100644 Binary files a/out/assets/images/battle/action-tooltip.png and b/out/assets/images/battle/action-tooltip.png differ diff --git a/out/assets/images/battle/shiplist-enemy.png b/out/assets/images/battle/shiplist-enemy.png index 0fb195b..17d5b77 100644 Binary files a/out/assets/images/battle/shiplist-enemy.png and b/out/assets/images/battle/shiplist-enemy.png differ diff --git a/out/assets/images/battle/shiplist-own.png b/out/assets/images/battle/shiplist-own.png index 6cf301e..58a18d3 100644 Binary files a/out/assets/images/battle/shiplist-own.png and b/out/assets/images/battle/shiplist-own.png differ diff --git a/out/assets/images/character/skill-upgrade.png b/out/assets/images/character/skill-upgrade.png new file mode 100644 index 0000000..b299892 Binary files /dev/null and b/out/assets/images/character/skill-upgrade.png differ diff --git a/src/common b/src/common index fb3268b..2b11ca4 160000 --- a/src/common +++ b/src/common @@ -1 +1 @@ -Subproject commit fb3268b8ef0c8f6e32c5cf10e896b9a134d45f6a +Subproject commit 2b11ca44c3dcf368baa9099467842750aa176be7 diff --git a/src/core/BattleOutcome.spec.ts b/src/core/BattleOutcome.spec.ts index aec21c9..97c2bc5 100644 --- a/src/core/BattleOutcome.spec.ts +++ b/src/core/BattleOutcome.spec.ts @@ -9,8 +9,8 @@ module TS.SpaceTac.Specs { fleet2.addShip(new Ship()); fleet2.addShip(new Ship()); - fleet2.ships[2].level = 5; - fleet2.ships[3].level = 5; + fleet2.ships[2].level.forceLevel(5); + fleet2.ships[3].level.forceLevel(5); fleet2.ships[0].addSlot(SlotType.Weapon).attach(new Equipment(SlotType.Weapon, "0a")); fleet2.ships[0].addSlot(SlotType.Weapon).attach(new Equipment(SlotType.Weapon, "0b")); diff --git a/src/core/BattleOutcome.ts b/src/core/BattleOutcome.ts index 6d80591..94715ce 100644 --- a/src/core/BattleOutcome.ts +++ b/src/core/BattleOutcome.ts @@ -28,7 +28,7 @@ module TS.SpaceTac { var luck = random.random(); if (luck > 0.9) { // Salvage a supposedly transported item - var transported = this.generateLootItem(random, ship.level); + var transported = this.generateLootItem(random, ship.level.get()); if (transported) { this.loot.push(transported); } diff --git a/src/core/Fleet.spec.ts b/src/core/Fleet.spec.ts index e4dad0f..8d43906 100644 --- a/src/core/Fleet.spec.ts +++ b/src/core/Fleet.spec.ts @@ -8,9 +8,9 @@ module TS.SpaceTac { fleet.addShip(new Ship()); fleet.addShip(new Ship()); - fleet.ships[0].level = 2; - fleet.ships[1].level = 4; - fleet.ships[2].level = 7; + fleet.ships[0].level.forceLevel(2); + fleet.ships[1].level.forceLevel(4); + fleet.ships[2].level.forceLevel(7); expect(fleet.getLevel()).toEqual(4); }); diff --git a/src/core/Fleet.ts b/src/core/Fleet.ts index 34e6a97..ee369f1 100644 --- a/src/core/Fleet.ts +++ b/src/core/Fleet.ts @@ -71,10 +71,10 @@ module TS.SpaceTac { var sum = 0; this.ships.forEach((ship: Ship) => { - sum += ship.level; + sum += ship.level.get(); }); var avg = sum / this.ships.length; - return Math.round(avg); + return Math.floor(avg); } // Check if the fleet still has living ships diff --git a/src/core/Ship.spec.ts b/src/core/Ship.spec.ts index 5f016c7..5b18aac 100644 --- a/src/core/Ship.spec.ts +++ b/src/core/Ship.spec.ts @@ -400,5 +400,28 @@ module TS.SpaceTac.Specs { expect(ship.listEquipment()).toEqual([]); expect(ship.cargo).toEqual([equipment]); }); + + it("allow skills upgrading from current level", function () { + let ship = new Ship(); + expect(ship.level.get()).toBe(1); + expect(ship.getAvailableUpgradePoints()).toBe(10); + + ship.level.forceLevel(2); + expect(ship.level.get()).toBe(2); + expect(ship.getAvailableUpgradePoints()).toBe(15); + + expect(ship.getAttribute("skill_energy")).toBe(0); + ship.upgradeSkill("skill_energy"); + expect(ship.getAttribute("skill_energy")).toBe(1); + + range(50).forEach(() => ship.upgradeSkill("skill_gravity")); + expect(ship.getAttribute("skill_energy")).toBe(1); + expect(ship.getAttribute("skill_gravity")).toBe(14); + expect(ship.getAvailableUpgradePoints()).toBe(0); + + ship.updateAttributes(); + expect(ship.getAttribute("skill_energy")).toBe(1); + expect(ship.getAttribute("skill_gravity")).toBe(14); + }); }); } diff --git a/src/core/Ship.ts b/src/core/Ship.ts index 094479e..ffa7759 100644 --- a/src/core/Ship.ts +++ b/src/core/Ship.ts @@ -3,10 +3,23 @@ module TS.SpaceTac { + /** + * Set of upgradable skills for a ship + */ + export class ShipSkills { + // Skills + skill_material = new ShipAttribute("material skill") + skill_energy = new ShipAttribute("energy skill") + skill_electronics = new ShipAttribute("electronics skill") + skill_human = new ShipAttribute("human skill") + skill_time = new ShipAttribute("time skill") + skill_gravity = new ShipAttribute("gravity skill") + } + /** * Set of ShipAttribute for a ship */ - export class ShipAttributes { + export class ShipAttributes extends ShipSkills { // Attribute controlling the play order initiative = new ShipAttribute("initiative") // Maximal hull value @@ -19,13 +32,6 @@ module TS.SpaceTac { power_initial = new ShipAttribute("initial power") // Power value recovered each turn power_recovery = new ShipAttribute("power recovery") - // Skills - skill_material = new ShipAttribute("material skill") - skill_energy = new ShipAttribute("energy skill") - skill_electronics = new ShipAttribute("electronics skill") - skill_human = new ShipAttribute("human skill") - skill_time = new ShipAttribute("time skill") - skill_gravity = new ShipAttribute("gravity skill") } /** @@ -40,6 +46,7 @@ module TS.SpaceTac { /** * Static attributes and values object for name queries */ + export const SHIP_SKILLS = new ShipSkills(); export const SHIP_ATTRIBUTES = new ShipAttributes(); export const SHIP_VALUES = new ShipValues(); @@ -51,7 +58,8 @@ module TS.SpaceTac { fleet: Fleet // Level of this ship - level: number + level = new ShipLevel() + skills = new ShipSkills() // Name of the ship name: string @@ -91,13 +99,9 @@ module TS.SpaceTac { // Priority in play_order play_priority = 0; - // Upgrade points available - upgrade_points = 0; - // Create a new ship inside a fleet constructor(fleet: Fleet | null = null, name = "Ship") { this.fleet = fleet || new Fleet(); - this.level = 1; this.name = name; this.model = "default"; this.alive = true; @@ -172,6 +176,24 @@ module TS.SpaceTac { return actions; } + /** + * Get the number of upgrade points available to improve skills + */ + getAvailableUpgradePoints(): number { + let used = keys(SHIP_SKILLS).map(skill => this.skills[skill].get()).reduce((a, b) => a + b, 0); + return this.level.getSkillPoints() - used; + } + + /** + * Try to upgrade a skill by 1 point + */ + upgradeSkill(skill: keyof ShipSkills) { + if (this.getAvailableUpgradePoints() > 0) { + this.skills[skill].add(1); + this.updateAttributes(); + } + } + // Add an event to the battle log, if any addBattleEvent(event: BaseLogEvent): void { var battle = this.getBattle(); @@ -550,8 +572,16 @@ module TS.SpaceTac { // Update attributes, taking into account attached equipment and active effects updateAttributes(): void { if (this.alive) { - // Sum all attribute effects var new_attrs = new ShipAttributes(); + + // TODO better typing for iteritems + + // Apply base skills + iteritems(this.skills, (key, skill: ShipAttribute) => { + new_attrs[key].add(skill.get()); + }); + + // Sum all attribute effects this.collectEffects("attr").forEach((effect: AttributeEffect) => { new_attrs[effect.attrcode].add(effect.value); }); @@ -561,7 +591,7 @@ module TS.SpaceTac { new_attrs[effect.attrcode].setMaximal(effect.value); }); - // TODO better typing + // Set final attributes iteritems(new_attrs, (key, value) => { this.setAttribute(key, (value).get()); }); diff --git a/src/core/ShipLevel.spec.ts b/src/core/ShipLevel.spec.ts new file mode 100644 index 0000000..7c8cf29 --- /dev/null +++ b/src/core/ShipLevel.spec.ts @@ -0,0 +1,43 @@ +module TS.SpaceTac.Specs { + describe("ShipLevel", () => { + it("level up from experience points", () => { + let level = new ShipLevel(); + expect(level.get()).toEqual(1); + expect(level.getNextGoal()).toEqual(100); + expect(level.getSkillPoints()).toEqual(10); + + level.addExperience(60); // 60 + expect(level.get()).toEqual(1); + expect(level.checkLevelUp()).toBe(false); + + level.addExperience(70); // 130 + expect(level.get()).toEqual(1); + expect(level.checkLevelUp()).toBe(true); + expect(level.get()).toEqual(2); + expect(level.getNextGoal()).toEqual(300); + expect(level.getSkillPoints()).toEqual(15); + + level.addExperience(200); // 330 + expect(level.get()).toEqual(2); + expect(level.checkLevelUp()).toBe(true); + expect(level.get()).toEqual(3); + expect(level.getNextGoal()).toEqual(600); + expect(level.getSkillPoints()).toEqual(20); + + level.addExperience(320); // 650 + expect(level.get()).toEqual(3); + expect(level.checkLevelUp()).toBe(true); + expect(level.get()).toEqual(4); + expect(level.getNextGoal()).toEqual(1000); + expect(level.getSkillPoints()).toEqual(25); + }); + + it("forces a given level", () => { + let level = new ShipLevel(); + expect(level.get()).toEqual(1); + + level.forceLevel(10); + expect(level.get()).toEqual(10); + }); + }); +} diff --git a/src/core/ShipLevel.ts b/src/core/ShipLevel.ts new file mode 100644 index 0000000..925c8f5 --- /dev/null +++ b/src/core/ShipLevel.ts @@ -0,0 +1,61 @@ +module TS.SpaceTac { + /** + * Level and experience system for a ship. + */ + export class ShipLevel { + private level = 1; + private experience = 0; + + /** + * Get current level + */ + get(): number { + return this.level; + } + + /** + * Get the next experience goal to reach, to gain one level + */ + getNextGoal(): number { + return isum(imap(irange(this.level), i => (i + 1) * 100)); + } + + /** + * Force experience gain, to reach a given level + */ + forceLevel(level: number) { + while (this.level < level) { + this.addExperience(this.getNextGoal()); + this.checkLevelUp(); + } + } + + /** + * Check for level-up + * + * Returns true if level changed + */ + checkLevelUp(): boolean { + let changed = false; + while (this.experience > this.getNextGoal()) { + this.level++; + changed = true; + } + return changed; + } + + /** + * Add experience points + */ + addExperience(points: number) { + this.experience += points; + } + + /** + * Get skill points given by current level + */ + getSkillPoints(): number { + return 5 + 5 * this.level; + } + } +} diff --git a/src/ui/Preload.ts b/src/ui/Preload.ts index a0adb87..2270cff 100644 --- a/src/ui/Preload.ts +++ b/src/ui/Preload.ts @@ -93,6 +93,7 @@ module TS.SpaceTac.UI { this.loadImage("character/close.png"); this.loadImage("character/ship.png"); this.loadImage("character/ship-selected.png"); + this.loadImage("character/skill-upgrade.png"); this.loadImage("character/cargo-slot.png"); this.loadImage("character/equipment-slot.png"); this.loadImage("character/slot-power.png"); diff --git a/src/ui/battle/ShipListItem.ts b/src/ui/battle/ShipListItem.ts index cd09520..cedd720 100644 --- a/src/ui/battle/ShipListItem.ts +++ b/src/ui/battle/ShipListItem.ts @@ -58,6 +58,10 @@ module TS.SpaceTac.UI { this.updateAttributes(); this.updateEffects(); + let level = new Phaser.Text(this.game, 103, 22, `${ship.level.get()}`, { align: "center", font: "bold 10pt Arial", fill: "#000000" }); + level.anchor.set(0.5, 0.5); + this.addChild(level); + Tools.setHoverClick(this, () => list.battleview.cursorOnShip(ship), () => list.battleview.cursorOffShip(ship), () => list.battleview.cursorClicked()); } diff --git a/src/ui/character/CharacterSheet.ts b/src/ui/character/CharacterSheet.ts index 7d28193..0093c9a 100644 --- a/src/ui/character/CharacterSheet.ts +++ b/src/ui/character/CharacterSheet.ts @@ -27,8 +27,9 @@ module TS.SpaceTac.UI { // Ship level ship_level: Phaser.Text; - // Ship upgrade points - ship_upgrades: Phaser.Text; + // Ship skill upgrade + ship_upgrade_points: Phaser.Text; + ship_upgrades: Phaser.Group; // Ship slots ship_slots: Phaser.Group; @@ -75,8 +76,11 @@ module TS.SpaceTac.UI { this.ship_level.anchor.set(0.5, 0.5); this.addChild(this.ship_level); - this.ship_upgrades = new Phaser.Text(this.game, 1066, 1054, "", { align: "center", font: "30pt Arial", fill: "#FFFFFF" }); - this.ship_upgrades.anchor.set(0.5, 0.5); + this.ship_upgrade_points = new Phaser.Text(this.game, 1066, 1054, "", { align: "center", font: "30pt Arial", fill: "#FFFFFF" }); + this.ship_upgrade_points.anchor.set(0.5, 0.5); + this.addChild(this.ship_upgrade_points); + + this.ship_upgrades = new Phaser.Group(this.game); this.addChild(this.ship_upgrades); this.ship_slots = new Phaser.Group(this.game); @@ -106,29 +110,40 @@ module TS.SpaceTac.UI { let x1 = 664; let x2 = 1066; let y = 662; - this.addAttribute(SHIP_ATTRIBUTES.initiative, x1, y); - this.addAttribute(SHIP_ATTRIBUTES.hull_capacity, x1, y + 64); - this.addAttribute(SHIP_ATTRIBUTES.shield_capacity, x1, y + 128); - this.addAttribute(SHIP_ATTRIBUTES.power_capacity, x1, y + 192); - this.addAttribute(SHIP_ATTRIBUTES.power_initial, x1, y + 256); - this.addAttribute(SHIP_ATTRIBUTES.power_recovery, x1, y + 320); - this.addAttribute(SHIP_ATTRIBUTES.skill_material, x2, y); - this.addAttribute(SHIP_ATTRIBUTES.skill_electronics, x2, y + 64); - this.addAttribute(SHIP_ATTRIBUTES.skill_energy, x2, y + 128); - this.addAttribute(SHIP_ATTRIBUTES.skill_human, x2, y + 192); - this.addAttribute(SHIP_ATTRIBUTES.skill_gravity, x2, y + 256); - this.addAttribute(SHIP_ATTRIBUTES.skill_time, x2, y + 320); + this.addAttribute("initiative", x1, y); + this.addAttribute("hull_capacity", x1, y + 64); + this.addAttribute("shield_capacity", x1, y + 128); + this.addAttribute("power_capacity", x1, y + 192); + this.addAttribute("power_initial", x1, y + 256); + this.addAttribute("power_recovery", x1, y + 320); + this.addAttribute("skill_material", x2, y); + this.addAttribute("skill_electronics", x2, y + 64); + this.addAttribute("skill_energy", x2, y + 128); + this.addAttribute("skill_human", x2, y + 192); + this.addAttribute("skill_gravity", x2, y + 256); + this.addAttribute("skill_time", x2, y + 320); } /** * Add an attribute display */ - private addAttribute(attribute: ShipAttribute, x: number, y: number) { + private addAttribute(attribute: keyof ShipAttributes, x: number, y: number) { let text = new Phaser.Text(this.game, x, y, "", { align: "center", font: "18pt Arial", fill: "#FFFFFF" }); text.anchor.set(0.5, 0.5); this.addChild(text); - this.attributes[attribute.name] = text; + this.attributes[SHIP_ATTRIBUTES[attribute].name] = text; + + if (SHIP_SKILLS[attribute]) { + let button = new Phaser.Button(this.game, x + 54, y - 4, "character-skill-upgrade", () => { + this.ship.upgradeSkill(attribute); + this.refresh(); + }); + button.anchor.set(0.5, 0.5); + this.ship_upgrades.add(button); + + this.view.tooltip.bindStaticText(button, `Spend one point to upgrade ${SHIP_ATTRIBUTES[attribute].name}`); + } } /** @@ -172,9 +187,12 @@ module TS.SpaceTac.UI { this.equipments.removeAll(true); + let upgrade_points = ship.getAvailableUpgradePoints(); + this.ship_name.setText(ship.name); - this.ship_level.setText(ship.level.toString()); - this.ship_upgrades.setText(ship.upgrade_points.toString()); + this.ship_level.setText(ship.level.get().toString()); + this.ship_upgrade_points.setText(upgrade_points.toString()); + this.ship_upgrades.visible = upgrade_points > 0; iteritems(ship.attributes, (key, value: ShipAttribute) => { let text = this.attributes[value.name];