diff --git a/src/scripts/game/LootGenerator.ts b/src/scripts/game/LootGenerator.ts
new file mode 100644
index 0000000..5722fcc
--- /dev/null
+++ b/src/scripts/game/LootGenerator.ts
@@ -0,0 +1,59 @@
+module SpaceTac.Game {
+ "use strict";
+
+ // Equipment generator from loot templates
+ export class LootGenerator {
+ // List of available templates
+ templates: LootTemplate[];
+
+ // Random generator that will be used
+ random: RandomGenerator;
+
+ // Construct a basic loot generator
+ // The list of templates will be automatically populated
+ constructor() {
+ this.templates = [];
+ this.random = new RandomGenerator();
+
+ this.populate();
+ }
+
+ // Fill the list of templates
+ populate(): void {
+
+ }
+
+ // Generate a random equipment
+ // If slot is specified, it will generate an equipment for this slot type specifically
+ // If level is specified, it will generate an equipment with level requirement inside this range
+ // If no equipment could be generated from available templates, null is returned
+ generate(level: IntegerRange = null, slot: SlotType = null): Equipment {
+ // Generate equipments matching conditions, with each template
+ var equipments: Equipment[] = [];
+ this.templates.forEach((template: LootTemplate) => {
+ if (slot !== null && slot !== template.slot) {
+ return;
+ }
+
+ var equipment: Equipment;
+ if (level === null) {
+ equipment = template.generate(this.random);
+ } else {
+ equipment = template.generateInLevelRange(level, this.random);
+ }
+
+ if (equipment !== null) {
+ equipments.push(equipment);
+ }
+ });
+
+ // No equipment could be generated with given conditions
+ if (equipments.length === 0) {
+ return null;
+ }
+
+ // Pick a random equipment
+ return this.random.choice(equipments);
+ }
+ }
+}
diff --git a/src/scripts/game/LootTemplate.ts b/src/scripts/game/LootTemplate.ts
index 13e237b..cdd59a1 100644
--- a/src/scripts/game/LootTemplate.ts
+++ b/src/scripts/game/LootTemplate.ts
@@ -1,26 +1,6 @@
module SpaceTac.Game {
"use strict";
- // Range of values
- export class Range {
- // Minimal value
- private min: number;
-
- // Maximal value
- private max: number;
-
- // Create a range of values
- constructor(min: number, max: number) {
- this.min = min;
- this.max = max;
- }
-
- // Get a proportional value (give 0.0-1.0 value to obtain a value in range)
- getProportional(cursor: number) :number {
- return (this.max - this.min) * cursor + this.min;
- }
- }
-
// Template used to generate a loot equipment
export class LootTemplate {
// Type of slot this equipment will fit in
@@ -39,8 +19,8 @@ module SpaceTac.Game {
// Effect area's radius
blast: Range;
- // Duration
- duration: Range;
+ // Duration, in number of turns
+ duration: IntegerRange;
// Effects
@@ -48,7 +28,7 @@ module SpaceTac.Game {
ap_usage: Range;
// Level requirement
- min_level: Range;
+ min_level: IntegerRange;
// Create a loot template
constructor(slot: SlotType, name: string) {
@@ -56,14 +36,14 @@ module SpaceTac.Game {
this.name = name;
this.distance = new Range(0, 0);
this.blast = new Range(0, 0);
- this.duration = new Range(0, 0);
+ this.duration = new IntegerRange(0, 0);
this.ap_usage = new Range(0, 0);
- this.min_level = new Range(0, 0);
+ this.min_level = new IntegerRange(0, 0);
}
// Generate a random equipment with this template
- generate(): Equipment {
- var random = new RandomGenerator();
+ generate(random: RandomGenerator = null): Equipment {
+ random = random || new RandomGenerator();
var power = random.throw();
return this.generateFixed(power);
}
@@ -75,13 +55,50 @@ module SpaceTac.Game {
result.slot = this.slot;
result.name = this.name;
- result.distance = Math.floor(this.distance.getProportional(power));
- result.blast = Math.floor(this.blast.getProportional(power));
- result.duration = Math.floor(this.duration.getProportional(power));
- result.ap_usage = Math.floor(this.ap_usage.getProportional(power));
- result.min_level = Math.floor(this.min_level.getProportional(power));
+ result.distance = this.distance.getProportional(power);
+ result.blast = this.blast.getProportional(power);
+ result.duration = this.duration.getProportional(power);
+ result.ap_usage = this.ap_usage.getProportional(power);
+ result.min_level = this.min_level.getProportional(power);
return result;
}
+
+ // Find the power range that will result in the level range
+ getPowerRangeForLevel(level: IntegerRange): Range {
+ if (level.min > this.min_level.max || level.max < this.min_level.min) {
+ return null;
+ } else {
+ var min: number;
+ var max: number;
+
+ if (level.min <= this.min_level.min) {
+ min = 0.0;
+ } else {
+ min = this.min_level.getReverseProportional(level.min);
+ }
+ if (level.max >= this.min_level.max) {
+ max = 1.0;
+ } else {
+ max = this.min_level.getReverseProportional(level.max);
+ }
+
+ return new Range(min, max);
+ }
+ }
+
+ // Generate an equipment that will have its level requirement in the given range
+ // May return null if level range is not compatible with the template
+ generateInLevelRange(level: IntegerRange, random: RandomGenerator = null): Equipment {
+ random = random || new RandomGenerator();
+
+ var random_range = this.getPowerRangeForLevel(level);
+ if (random_range) {
+ var power = random.throw() * (random_range.max - random_range.min) + random_range.min;
+ return this.generateFixed(power);
+ } else {
+ return null;
+ }
+ }
}
}
diff --git a/src/scripts/game/RandomGenerator.ts b/src/scripts/game/RandomGenerator.ts
index 258ed9b..70a6496 100644
--- a/src/scripts/game/RandomGenerator.ts
+++ b/src/scripts/game/RandomGenerator.ts
@@ -20,6 +20,18 @@ module SpaceTac.Game {
}
}
+ // Generate a random integer value in a range
+ throwInt(min: number, max: number): number {
+ var value = this.throw(max - min + 1);
+ return Math.floor(value) + max;
+ }
+
+ // Choose a random item from an array
+ choice(items: any[]): any {
+ var index = this.throwInt(0, items.length - 1);
+ return items[index];
+ }
+
// Fake the generator, by forcing the next value
// Call it several times to set future successive values
// This value will replace the 0.0-1.0 random value, not the final one
diff --git a/src/scripts/game/Range.ts b/src/scripts/game/Range.ts
new file mode 100644
index 0000000..4a86c1a
--- /dev/null
+++ b/src/scripts/game/Range.ts
@@ -0,0 +1,75 @@
+module SpaceTac.Game {
+ "use strict";
+
+ // Range of number values
+ export class Range {
+ // Minimal value
+ min: number;
+
+ // Maximal value
+ max: number;
+
+ // Create a range of values
+ constructor(min: number, max: number) {
+ this.min = min;
+ this.max = max;
+ }
+
+ // Get a proportional value (give 0.0-1.0 value to obtain a value in range)
+ getProportional(cursor: number): number {
+ if (cursor <= 0.0) {
+ return this.min;
+ } else if (cursor >= 1.0) {
+ return this.max;
+ } else {
+ return (this.max - this.min) * cursor + this.min;
+ }
+ }
+
+ // Get the value of the cursor that would give this proportional value (in 0.0-1.0 range)
+ getReverseProportional(expected: number): number {
+ if (expected <= this.min) {
+ return 0;
+ } else if (expected >= this.max) {
+ return 1;
+ } else {
+ return (expected - this.min) / (this.max - this.min);
+ }
+ }
+
+ // Check if a value is in the range
+ isInRange(value: number): boolean {
+ return value >= this.min && value <= this.max;
+ }
+ }
+
+
+ // Range of integer values
+ //
+ // This differs from Range in that it adds space in proportional values to include the 'max'.
+ // Typically, using Range for integers will only yield 'max' for exactly 1.0 proportional, not for 0.999999.
+ // This fixes this behavior.
+ //
+ // As this rounds values to integer, the 'reverse' proportional is no longer a bijection.
+ export class IntegerRange extends Range {
+ getProportional(cursor: number): number {
+ if (cursor <= 0.0) {
+ return this.min;
+ } else if (cursor >= 1.0) {
+ return this.max;
+ } else {
+ return Math.floor((this.max - this.min + 1) * cursor + this.min);
+ }
+ }
+
+ getReverseProportional(expected: number): number {
+ if (expected <= this.min) {
+ return 0;
+ } else if (expected > this.max) {
+ return 1;
+ } else {
+ return (expected - this.min) * 1.0 / (this.max - this.min + 1);
+ }
+ }
+ }
+}
diff --git a/src/scripts/game/Ship.ts b/src/scripts/game/Ship.ts
index bc85da0..284fa25 100644
--- a/src/scripts/game/Ship.ts
+++ b/src/scripts/game/Ship.ts
@@ -43,6 +43,9 @@ module SpaceTac.Game {
// Number of action points used to make a 1.0 move
movement_cost: number;
+ // List of slots, able to contain equipment
+ slots: Slot[];
+
// Create a new ship inside a fleet
constructor(fleet: Fleet, name: string) {
this.fleet = fleet;
@@ -52,6 +55,7 @@ module SpaceTac.Game {
this.ap_maximal = 20;
this.ap_recover = 5;
this.movement_cost = 0.1;
+ this.slots = [];
if (fleet) {
fleet.addShip(this);
diff --git a/src/scripts/game/specs/LootGenerator.spec.ts b/src/scripts/game/specs/LootGenerator.spec.ts
new file mode 100644
index 0000000..4a8bc1d
--- /dev/null
+++ b/src/scripts/game/specs/LootGenerator.spec.ts
@@ -0,0 +1,29 @@
+///
+
+module SpaceTac.Game.Specs {
+ "use strict";
+
+ class TestTemplate extends LootTemplate {
+ constructor() {
+ super(SlotType.Shield, "Hexagrid Shield");
+
+ this.min_level = new IntegerRange(2, 100);
+ this.ap_usage = new Range(6, 8);
+ }
+ }
+
+ describe("LootGenerator", () => {
+ it("generates items within a given level range", () => {
+ var generator = new LootGenerator();
+ generator.templates = [new TestTemplate()];
+ generator.random.forceNextValue(0.5);
+
+ var equipment = generator.generate(new IntegerRange(3, 6));
+
+ expect(equipment.slot).toBe(SlotType.Shield);
+ expect(equipment.name).toEqual("Hexagrid Shield");
+ expect(equipment.min_level).toBe(5);
+ expect(equipment.ap_usage).toEqual(7);
+ });
+ });
+}
diff --git a/src/scripts/game/specs/LootTemplate.spec.ts b/src/scripts/game/specs/LootTemplate.spec.ts
new file mode 100644
index 0000000..f781523
--- /dev/null
+++ b/src/scripts/game/specs/LootTemplate.spec.ts
@@ -0,0 +1,84 @@
+///
+
+module SpaceTac.Game.Specs {
+ "use strict";
+
+ describe("LootTemplate", () => {
+ it("interpolates between weak and strong loot", () => {
+ var template = new LootTemplate(SlotType.Weapon, "Bulletator");
+
+ template.distance = new Range(1, 3);
+ template.blast = new Range(1, 1);
+ template.duration = new IntegerRange(1, 2);
+ template.ap_usage = new Range(4, 12);
+ template.min_level = new IntegerRange(5, 9);
+
+ var equipment = template.generateFixed(0.0);
+
+ expect(equipment.slot).toEqual(SlotType.Weapon);
+ expect(equipment.name).toEqual("Bulletator");
+ expect(equipment.distance).toEqual(1);
+ expect(equipment.blast).toEqual(1);
+ expect(equipment.duration).toEqual(1);
+ expect(equipment.ap_usage).toEqual(4);
+ expect(equipment.min_level).toEqual(5);
+
+ equipment = template.generateFixed(1.0);
+
+ expect(equipment.slot).toEqual(SlotType.Weapon);
+ expect(equipment.name).toEqual("Bulletator");
+ expect(equipment.distance).toEqual(3);
+ expect(equipment.blast).toEqual(1);
+ expect(equipment.duration).toEqual(2);
+ expect(equipment.ap_usage).toEqual(12);
+ expect(equipment.min_level).toEqual(9);
+
+ equipment = template.generateFixed(0.5);
+
+ expect(equipment.slot).toEqual(SlotType.Weapon);
+ expect(equipment.name).toEqual("Bulletator");
+ expect(equipment.distance).toEqual(2);
+ expect(equipment.blast).toEqual(1);
+ expect(equipment.duration).toEqual(2);
+ expect(equipment.ap_usage).toEqual(8);
+ expect(equipment.min_level).toEqual(7);
+ });
+
+ it("restricts power range to stay in a level range", () => {
+ var template = new LootTemplate(SlotType.Weapon, "Bulletator");
+ template.min_level = new IntegerRange(4, 7);
+
+ var result: Range;
+
+ result = template.getPowerRangeForLevel(new IntegerRange(4, 7));
+ expect(result.min).toBe(0);
+ expect(result.max).toBe(1);
+
+ result = template.getPowerRangeForLevel(new IntegerRange(1, 10));
+ expect(result.min).toBe(0);
+ expect(result.max).toBe(1);
+
+ result = template.getPowerRangeForLevel(new IntegerRange(5, 6));
+ expect(result.min).toBeCloseTo(0.25, 0.000001);
+ expect(result.max).toBeCloseTo(0.75, 0.000001);
+
+ result = template.getPowerRangeForLevel(new IntegerRange(5, 12));
+ expect(result.min).toBeCloseTo(0.25, 0.000001);
+ expect(result.max).toBe(1);
+
+ result = template.getPowerRangeForLevel(new IntegerRange(3, 6));
+ expect(result.min).toBe(0);
+ expect(result.max).toBeCloseTo(0.75, 0.000001);
+
+ result = template.getPowerRangeForLevel(new IntegerRange(10, 15));
+ expect(result).toBeNull();
+
+ result = template.getPowerRangeForLevel(new IntegerRange(1, 3));
+ expect(result).toBeNull();
+
+ result = template.getPowerRangeForLevel(new IntegerRange(5, 5));
+ expect(result.min).toBeCloseTo(0.25, 0.000001);
+ expect(result.max).toBeCloseTo(0.5, 0.000001);
+ });
+ });
+}
diff --git a/src/scripts/game/specs/LootTemplate.specs.ts b/src/scripts/game/specs/LootTemplate.specs.ts
deleted file mode 100644
index 15315c0..0000000
--- a/src/scripts/game/specs/LootTemplate.specs.ts
+++ /dev/null
@@ -1,47 +0,0 @@
-///
-
-module SpaceTac.Game.Specs {
- "use strict";
-
- describe("LootTemplate", () => {
- it("interpolates between weak and strong loot", () => {
- var template = new LootTemplate(SlotType.Weapon, "Bulletator");
-
- template.distance = new Range(1, 3);
- template.blast = new Range(1, 1);
- template.duration = new Range(1, 2);
- template.ap_usage = new Range(4, 12);
- template.min_level = new Range(5, 9);
-
- var equipment = template.generateFixed(0.0);
-
- expect(equipment.slot).toEqual(SlotType.Weapon);
- expect(equipment.name).toEqual("Bulletator");
- expect(equipment.distance).toEqual(1);
- expect(equipment.blast).toEqual(1);
- expect(equipment.duration).toEqual(1);
- expect(equipment.ap_usage).toEqual(4);
- expect(equipment.min_level).toEqual(5);
-
- var equipment = template.generateFixed(1.0);
-
- expect(equipment.slot).toEqual(SlotType.Weapon);
- expect(equipment.name).toEqual("Bulletator");
- expect(equipment.distance).toEqual(3);
- expect(equipment.blast).toEqual(1);
- expect(equipment.duration).toEqual(2);
- expect(equipment.ap_usage).toEqual(12);
- expect(equipment.min_level).toEqual(9);
-
- var equipment = template.generateFixed(0.5);
-
- expect(equipment.slot).toEqual(SlotType.Weapon);
- expect(equipment.name).toEqual("Bulletator");
- expect(equipment.distance).toEqual(2);
- expect(equipment.blast).toEqual(1);
- expect(equipment.duration).toEqual(1);
- expect(equipment.ap_usage).toEqual(8);
- expect(equipment.min_level).toEqual(7);
- });
- });
-}
\ No newline at end of file
diff --git a/src/scripts/game/specs/Range.spec.ts b/src/scripts/game/specs/Range.spec.ts
new file mode 100644
index 0000000..f3d978a
--- /dev/null
+++ b/src/scripts/game/specs/Range.spec.ts
@@ -0,0 +1,49 @@
+///
+
+module SpaceTac.Game.Specs {
+ "use strict";
+
+ function checkProportional(range: Range, value1: number, value2: number) {
+ expect(range.getProportional(value1)).toEqual(value2);
+ expect(range.getReverseProportional(value2)).toEqual(value1);
+ }
+
+ describe("Range", () => {
+ it("can work with proportional values", () => {
+ var range = new Range(1, 5);
+
+ checkProportional(range, 0, 1);
+ checkProportional(range, 1, 5);
+ checkProportional(range, 0.5, 3);
+ checkProportional(range, 0.4, 2.6);
+
+ expect(range.getProportional(-0.25)).toEqual(1);
+ expect(range.getProportional(1.8)).toEqual(5);
+
+ expect(range.getReverseProportional(0)).toEqual(0);
+ expect(range.getReverseProportional(6)).toEqual(1);
+ });
+ });
+
+ describe("IntegerRange", () => {
+ it("can work with proportional values", () => {
+ var range = new IntegerRange(1, 5);
+
+ expect(range.getProportional(0)).toEqual(1);
+ expect(range.getProportional(0.1)).toEqual(1);
+ expect(range.getProportional(0.2)).toEqual(2);
+ expect(range.getProportional(0.45)).toEqual(3);
+ expect(range.getProportional(0.5)).toEqual(3);
+ expect(range.getProportional(0.75)).toEqual(4);
+ expect(range.getProportional(0.8)).toEqual(5);
+ expect(range.getProportional(0.99)).toEqual(5);
+ expect(range.getProportional(1)).toEqual(5);
+
+ expect(range.getReverseProportional(1)).toEqual(0);
+ expect(range.getReverseProportional(2)).toEqual(0.2);
+ expect(range.getReverseProportional(3)).toEqual(0.4);
+ expect(range.getReverseProportional(4)).toEqual(0.6);
+ expect(range.getReverseProportional(5)).toEqual(0.8);
+ });
+ });
+}