1
0
Fork 0

Added equipment generation of different quality

This commit is contained in:
Michaël Lemaire 2017-04-20 23:20:50 +02:00
parent 5861deca6d
commit 5a5be8addc
13 changed files with 255 additions and 35 deletions

3
TODO
View file

@ -4,8 +4,7 @@
* Character sheet: paginate loot and shop items
* Character sheet: improve eye-catching for shop and loot section
* Character sheet: equipping an item without the requirements make it disappear
* Character sheet: display requirements on equipment tooltips
* Shops: add equipment pricing, with usage depreciation
* Shops: add price depreciation by item's previous usage
* Add permanent effects to ship models to ease balancing
* Ships should start battle in formation to force them to move
* Fix targetting not resetting when using action shortcuts

59
out/loot.html Normal file
View file

@ -0,0 +1,59 @@
<!DOCTYPE HTML>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>SpaceTac - Loot Generator Samples</title>
<style>
* {
margin: 0;
}
body {
background-color: #111;
color: #eee;
}
h1 {
font-size: 30px;
}
h2 {
font-size: 22px;
margin-top: 12px;
}
</style>
</head>
<body>
<script src="vendor/phaser/build/phaser.min.js"></script>
<script src="build.js"></script>
<div id="loot">
<h1>SpaceTac - Loot Generator Samples</h1>
</div>
<script>
window.onload = function () {
var generator = new TS.SpaceTac.LootGenerator();
generator.templates.forEach(function (template) {
TS.range(5).forEach(function (level) {
TS.iterenum(TS.SpaceTac.EquipmentQuality, function (quality) {
var loot = template.generate(level + 1, quality);
let title = document.createElement("h2");
title.textContent = loot.getFullName() + " (Price " + loot.price.toString() + ")";
document.body.appendChild(title);
let description = document.createElement("pre");
description.textContent = loot.getFullDescription();
document.body.appendChild(description);
});
});
});
};
</script>
</body>
</html>

@ -1 +1 @@
Subproject commit 3092ff0f3b8af5fd1525480c5ec7033741cac100
Subproject commit 78179da26a37232fcc25a2df9109dc9c2124ec76

View file

@ -27,15 +27,18 @@ module TS.SpaceTac.Specs {
0, // leave second ship alone
0.95, // lucky loot on third ship
0, // - lower end of level range (ship has 5, so range is 4-6)
0.5, // - common quality
0, // - take first generated equipment (there is only one anyway)
0.96, // lucky loot on fourth ship
0.999 // - higher end of level range
0.999, // - higher end of level range
0.98 // - premium quality
]);
// Force lucky finds with one template
var looter = new LootGenerator(random, false);
var template = new LootTemplate(SlotType.Power, "Nuclear Reactor");
template.setSkillsRequirements({ "skill_energy": istep(4) });
template.addAttributeEffect("power_capacity", 1);
looter.templates = [template];
spyOn(outcome, "getLootGenerator").and.returnValue(looter);
@ -44,8 +47,12 @@ module TS.SpaceTac.Specs {
expect(outcome.loot.length).toBe(3);
expect(outcome.loot[0].name).toBe("0a");
expect(outcome.loot[1].name).toBe("Nuclear Reactor");
expect(outcome.loot[1].level).toBe(4);
expect(outcome.loot[1].quality).toBe(EquipmentQuality.COMMON);
expect(outcome.loot[1].requirements).toEqual({ "skill_energy": 7 });
expect(outcome.loot[2].name).toBe("Nuclear Reactor");
expect(outcome.loot[2].level).toBe(6);
expect(outcome.loot[2].quality).toBe(EquipmentQuality.PREMIUM);
expect(outcome.loot[2].requirements).toEqual({ "skill_energy": 9 });
});
});

View file

@ -1,5 +1,9 @@
module TS.SpaceTac {
// Result of an ended battle
/**
* Result of an ended battle
*
* This stores the winner, and the retrievable loot
*/
export class BattleOutcome {
// Indicates if the battle is a draw (no winner)
draw: boolean;
@ -45,17 +49,40 @@ module TS.SpaceTac {
});
}
// Create a loot generator for lucky loots
/**
* Create a loot generator for lucky finds
*/
getLootGenerator(random: RandomGenerator): LootGenerator {
return new LootGenerator(random);
}
// Generate a special loot item for the winner fleet
// The equipment will be in the dead ship range
/**
* Generate a loot item for the winner fleet
*
* The equipment will be in the dead ship range
*/
generateLootItem(random: RandomGenerator, base_level: number): Equipment | null {
var generator = this.getLootGenerator(random);
var level = random.randInt(Math.max(base_level - 1, 1), base_level + 1);
return generator.generate(level);
let generator = this.getLootGenerator(random);
let level = random.randInt(Math.max(base_level - 1, 1), base_level + 1);
let quality = random.random();
return generator.generate(level, this.getQuality(quality));
}
/**
* Get the quality enum matching a 0-1 value
*/
getQuality(quality: number): EquipmentQuality {
if (quality < 0.1) {
return EquipmentQuality.WEAK;
} else if (quality > 0.99) {
return EquipmentQuality.LEGENDARY;
} else if (quality > 0.95) {
return EquipmentQuality.PREMIUM;
} else if (quality > 0.8) {
return EquipmentQuality.FINE;
} else {
return EquipmentQuality.COMMON;
}
}
}
}

View file

@ -25,7 +25,7 @@ module TS.SpaceTac {
name: string
// Equipment generic description
description: string
description = ""
// Indicative equipment level
level = 1
@ -33,14 +33,17 @@ module TS.SpaceTac {
// Indicative equipment quality
quality = EquipmentQuality.COMMON
// Base price
price = 0
// Minimum skills to be able to equip this
requirements: { [key: string]: number }
requirements: { [key: string]: number } = {}
// Permanent effects on the ship that equips this
effects: BaseEffect[]
effects: BaseEffect[] = []
// Action available when equipped
action: BaseAction
action = new BaseAction("nothing", "Do nothing", false)
// Usage made of this equipment (will lower the sell price)
usage: number
@ -50,10 +53,6 @@ module TS.SpaceTac {
this.slot_type = slot;
this.code = code;
this.name = code;
this.description = "";
this.requirements = {};
this.effects = [];
this.action = new BaseAction("nothing", "Do nothing", false);
}
jasmineToString() {
@ -75,10 +74,20 @@ module TS.SpaceTac {
* Get the full textual description for this equipment (without the full name).
*/
getFullDescription(): string {
let requirements: string[] = [];
iteritems(this.requirements, (skill, value) => {
if (value > 0) {
requirements.push(`- ${SHIP_ATTRIBUTES[skill].name} ${value}`);
}
});
let description = this.getEffectsDescription();
if (this.description) {
description += "\n\n" + this.description;
}
if (requirements.length > 0) {
description = "Requires:\n" + requirements.join("\n") + "\n\n" + description;
}
return description;
}
@ -92,6 +101,13 @@ module TS.SpaceTac {
return ShipLevel.getLevelForPoints(points);
}
/**
* Get the equipment price value.
*/
getPrice(): number {
return this.price;
}
/**
* Returns true if the equipment can be equipped on a ship.
*

View file

@ -32,14 +32,13 @@ module TS.SpaceTac {
}
// TODO Add generator from skills
// TODO Add generator of other qualities
// Generate a random equipment for a specific level
// If slot is specified, it will generate an equipment for this slot type specifically
// If no equipment could be generated from available templates, null is returned
generate(level: number, slot: SlotType | null = null): Equipment | null {
generate(level: number, quality = EquipmentQuality.COMMON, slot: SlotType | null = null): Equipment | null {
// Generate equipments matching conditions, with each template
let equipments = this.templates.filter(template => slot == null || slot == template.slot).map(template => template.generate(level));
let equipments = this.templates.filter(template => slot == null || slot == template.slot).map(template => template.generate(level, quality, this.random));
// No equipment could be generated with given conditions
if (equipments.length === 0) {

View file

@ -17,9 +17,18 @@ module TS.SpaceTac.Specs {
expect(result.slot_type).toEqual(SlotType.Power);
expect(result.code).toEqual("powergenerator");
expect(result.name).toEqual("Power Generator");
expect(result.price).toEqual(300);
expect(result.level).toEqual(2);
expect(result.quality).toEqual(EquipmentQuality.PREMIUM);
expect(result.quality).toEqual(EquipmentQuality.COMMON);
expect(result.description).toEqual("A great power generator !");
template.addAttributeEffect("power_capacity", istep(10));
result = template.generate(1, EquipmentQuality.COMMON);
expect(result.quality).toEqual(EquipmentQuality.COMMON);
expect(result.effects).toEqual([new AttributeEffect("power_capacity", 10)]);
result = template.generate(1, EquipmentQuality.PREMIUM);
expect(result.quality).toEqual(EquipmentQuality.PREMIUM);
expect(result.effects).toEqual([new AttributeEffect("power_capacity", 13)]);
});
it("applies requirements on skills", function () {

View file

@ -4,6 +4,12 @@ module TS.SpaceTac {
*/
type LeveledValue = number | Iterator<number>;
/**
* Modifiers of generated equipment
*/
type QualityModifier = (equipment: Equipment, quality: EquipmentQuality, random: RandomGenerator) => boolean;
type CommonModifier = (equipment: Equipment, level: number) => void;
/**
* Resolve a leveled value
*/
@ -75,6 +81,84 @@ module TS.SpaceTac {
}
}
/**
* Generic quality modifier
*/
function standardQualityModifier(equipment: Equipment, quality: EquipmentQuality, random: RandomGenerator): boolean {
// Collect available modifiers
let modifiers: Function[] = [];
let factor = 1;
if (quality == EquipmentQuality.WEAK) {
factor = 0.8;
} else if (quality == EquipmentQuality.FINE) {
factor = 1.1;
} else if (quality == EquipmentQuality.PREMIUM) {
factor = 1.3;
} else if (quality == EquipmentQuality.LEGENDARY) {
factor = 1.6;
}
if (quality == EquipmentQuality.WEAK && any(values(equipment.requirements), value => value > 0)) {
modifiers.push(() => {
iteritems(copy(equipment.requirements), (skill, value) => {
equipment.requirements[skill] = Math.max(equipment.requirements[skill] + 1, Math.floor(equipment.requirements[skill] / factor));
});
});
}
function simpleFactor<T>(obj: T, attr: keyof T, inverse = false) {
let val = <any>obj[attr];
if (val && val > 0) {
let nval = Math.round((inverse ? (1 / factor) : factor) * val);
if (nval != val) {
modifiers.push(() => (<any>obj)[attr] = nval);
}
}
}
function effectFactor(effect: BaseEffect) {
if (effect instanceof ValueEffect || effect instanceof AttributeEffect) {
simpleFactor(effect, 'value');
} else if (effect instanceof AttributeLimitEffect) {
simpleFactor(effect, 'value', true);
} else if (effect instanceof StickyEffect) {
simpleFactor(effect, 'duration');
effectFactor(effect.base);
} else if (effect instanceof DamageEffect) {
simpleFactor(effect, 'value');
}
}
equipment.effects.forEach(effectFactor);
if (equipment.action instanceof FireWeaponAction) {
simpleFactor(equipment.action, 'blast');
simpleFactor(equipment.action, 'range');
equipment.action.effects.forEach(effectFactor);
}
if (equipment.action instanceof DeployDroneAction) {
simpleFactor(equipment.action, 'deploy_distance');
simpleFactor(equipment.action, 'effect_radius');
equipment.action.effects.forEach(effectFactor);
}
if (equipment.action instanceof MoveAction) {
simpleFactor(equipment.action, 'distance_per_power');
}
// Choose a random one
if (modifiers.length > 0) {
let chosen = random.choice(modifiers);
chosen();
equipment.price = Math.ceil(equipment.price * factor * factor);
return true;
} else {
return false;
}
}
/**
* Template used to generate a loot equipment
*/
@ -88,14 +172,22 @@ module TS.SpaceTac {
// Generic description of the equipment
description: string
// Base price
price: LeveledValue
// Modifiers applied to obtain the "common" equipment, based on level
protected base_modifiers: ((equipment: Equipment, level: number) => void)[];
protected base_modifiers: CommonModifier[]
// Modifiers applied to "common" equipment to obtain a specific quality
protected quality_modifiers: QualityModifier[]
constructor(slot: SlotType, name: string, description = "") {
this.slot = slot;
this.name = name;
this.description = description;
this.price = istep(100, istep(200, irepeat(200)));
this.base_modifiers = [];
this.quality_modifiers = [standardQualityModifier];
}
/**
@ -105,12 +197,19 @@ module TS.SpaceTac {
let result = new Equipment(this.slot, (this.name || "").toLowerCase().replace(/ /g, ""));
result.level = level;
result.quality = quality;
result.name = this.name;
result.description = this.description;
result.price = resolveForLevel(this.price, level);
this.base_modifiers.forEach(modifier => modifier(result, level));
if (quality == EquipmentQuality.COMMON) {
result.quality = quality;
} else {
let quality_applied = this.quality_modifiers.map(modifier => modifier(result, quality, random));
result.quality = any(quality_applied, x => x) ? quality : EquipmentQuality.COMMON;
}
return result;
}

View file

@ -31,7 +31,7 @@ module TS.SpaceTac {
// Fill equipment slots
result.slots.forEach((slot: Slot) => {
var equipment = loot.generate(level, slot.type);
var equipment = loot.generate(level, EquipmentQuality.COMMON, slot.type);
if (equipment) {
slot.attach(equipment)
if (slot.attached !== equipment) {

View file

@ -4,7 +4,7 @@ module TS.SpaceTac.Specs {
let shop = new Shop();
expect(shop.stock.length).toBe(0);
shop.generateStock(8);
shop.generateStock(8, 1);
expect(shop.stock.length).toBe(8);
});

View file

@ -8,11 +8,17 @@ module TS.SpaceTac {
/**
* Generate a random stock
*
* *level* is the preferential level, but equipment around it may be generated
*/
generateStock(items: number) {
// TODO other levels
let generator = new LootGenerator();
this.stock = nna(range(items).map(i => generator.generate(1)));
generateStock(items: number, level: number, random = RandomGenerator.global) {
let generator = new LootGenerator(random);
this.stock = nna(range(items).map(() => {
let equlevel = random.weighted(range(level + 3).map(i => i + 1).map(i => (i > level) ? 1 : i)) + 1;
let quality = random.weighted([1, 7, 2]);
return generator.generate(equlevel, quality);
}));
this.sortStock();
}
@ -21,15 +27,14 @@ module TS.SpaceTac {
* Sort the stock by equipment level, then by value
*/
sortStock() {
// TODO
this.stock.sort((a, b) => (a.level == b.level) ? cmp(a.getPrice(), b.getPrice()) : cmp(a.level, b.level));
}
/**
* Get the buy/sell price for an equipment
*/
getPrice(equipment: Equipment): number {
// TODO
return 100;
return equipment.getPrice();
}
/**

View file

@ -50,7 +50,7 @@ module TS.SpaceTac {
addShop(generate_items = 0) {
this.shop = new Shop();
if (generate_items) {
this.shop.generateStock(generate_items);
this.shop.generateStock(generate_items, 1);
}
}