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); + }); + }); +}