1
0
Fork 0

New weapon effectiveness system, based on precision, maneuvrability and luck

This commit is contained in:
Michaël Lemaire 2017-12-08 01:18:15 +01:00
parent 56a85333f5
commit 231484f7a8
36 changed files with 326 additions and 168 deletions

View File

@ -36,6 +36,7 @@ Character sheet
Battle
------
* Display precision and maneuvrability in ship tooltip
* Add a voluntary retreat option
* Add scroll buttons when there are too many actions
* Toggle bar/text display in power section of action bar
@ -60,9 +61,8 @@ Ships models and equipments
---------------------------
* Add permanent effects and actions to ship models
* Add critical hit/miss
* Add critical hit/miss (or indicate lucky/unlucky throws)
* Add damage over time effect (tricky to make intuitive)
* Chance to hit should increase with precision
* Add actions with cost dependent of distance (like current move actions)
* Add disc targetting (for some jump move actions)
* Add "chain" effects

View File

@ -49,14 +49,14 @@ module TK.SpaceTac.Specs {
let action = new TriggerAction(equipment, [new DamageEffect(50)], 1, 200, 0);
equipment.action = action;
check.equals(equipment.getEffectsDescription(), "Fire (power usage 1, max range 200km):\n• do 50 damage on target");
check.equals(equipment.getEffectsDescription(), "Fire (power 1, range 200km):\n• do 50 damage on target");
action.blast = 20;
check.equals(equipment.getEffectsDescription(), "Fire (power usage 1, max range 200km):\n• do 50 damage in 20km radius");
check.equals(equipment.getEffectsDescription(), "Fire (power 1, range 200km):\n• do 50 damage in 20km radius");
action.blast = 0;
action.effects.push(new StickyEffect(new AttributeLimitEffect("shield_capacity", 200), 3));
check.equals(equipment.getEffectsDescription(), "Fire (power usage 1, max range 200km):\n• do 50 damage on target\n• limit shield capacity to 200 for 3 turns on target");
check.equals(equipment.getEffectsDescription(), "Fire (power 1, range 200km):\n• do 50 damage on target\n• limit shield capacity to 200 for 3 turns on target");
});
test.case("gets a minimal level, based on skills requirements", check => {

View File

@ -206,10 +206,10 @@ module TK.SpaceTac {
/**
* Add a trigger action.
*/
addTriggerAction(power: LeveledValue, effects: EffectTemplate<any>[], range: LeveledValue = irepeat(0), blast: LeveledValue = irepeat(0), angle: LeveledValue = irepeat(0)): void {
addTriggerAction(power: LeveledValue, effects: EffectTemplate<any>[], range: LeveledValue = irepeat(0), blast: LeveledValue = irepeat(0), angle: LeveledValue = irepeat(0), aim: LeveledValue = irepeat(0), evasion: LeveledValue = irepeat(0), luck: LeveledValue = irepeat(0)): void {
this.base_modifiers.push((equipment, level) => {
let reffects = effects.map(effect => effect.generate(level));
equipment.action = new TriggerAction(equipment, reffects, resolveForLevel(power, level), resolveForLevel(range, level), resolveForLevel(blast, level), resolveForLevel(angle, level));
equipment.action = new TriggerAction(equipment, reffects, resolveForLevel(power, level), resolveForLevel(range, level), resolveForLevel(blast, level), resolveForLevel(angle, level), resolveForLevel(aim, level), resolveForLevel(evasion, level), resolveForLevel(luck, level));
});
}

View File

@ -17,8 +17,8 @@ module TK.SpaceTac {
"shield_capacity": "Maximal Shield value to protect the hull from damage",
"power_capacity": "Maximal Power value to use equipment",
"power_generation": "Power generated at the end of the ship's turn",
"maneuvrability": "Ability to move first and fast",
"precision": "Ability to target far and good",
"maneuvrability": "Ability to move first, fast and to evade weapons",
"precision": "Ability to target far and aim good",
}
export const SHIP_VALUES_NAMES: ShipValuesMapping = {

View File

@ -120,7 +120,9 @@ module TK.SpaceTac {
return 0;
}
// Get the range of this action
/**
* Get the range of this action, for targetting purpose
*/
getRangeRadius(ship: Ship): number {
return 0;
}

View File

@ -36,7 +36,7 @@ module TK.SpaceTac.Specs {
action.apply(battle, ship, Target.newFromLocation(50, 50));
check.called(mock_apply, [
[ship2, ship]
[ship2, ship, 1]
]);
})
@ -85,6 +85,85 @@ module TK.SpaceTac.Specs {
check.equals(action.filterImpactedShips({ x: 0, y: 51 }, Target.newFromLocation(30, 50), ships), [ship1, ship2]);
})
test.case("computes a success factor, from precision and maneuvrability", check => {
function verify(precision: number, precision_factor: number, maneuvrability: number, maneuvrability_factor: number, result: number) {
let ship1 = new Ship();
let ship2 = new Ship();
TestTools.setAttribute(ship1, "precision", precision);
TestTools.setAttribute(ship2, "maneuvrability", maneuvrability);
let action = new TriggerAction(new Equipment());
action.aim = precision_factor;
action.evasion = maneuvrability_factor;
check.nears(action.getSuccessFactor(ship1, ship2), result, 3,
`precision ${precision} (weight ${precision_factor}), maneuvrability ${maneuvrability} (weight ${maneuvrability_factor})`);
}
// no weight => always 100%
verify(0, 0, 0, 0, 1);
verify(10, 0, 20, 0, 1);
verify(40, 0, -5, 0, 1);
// precision only
verify(0, 100, 0, 0, 0);
verify(1, 100, 1, 0, 0.5);
verify(2, 100, 2, 0, 0.8);
verify(10, 100, 10, 0, 0.99);
verify(1, 50, 1, 0, 0.75);
// maneuvrability only
verify(0, 0, 0, 100, 1);
verify(1, 0, 1, 100, 0.5);
verify(2, 0, 2, 100, 0.2);
verify(10, 0, 10, 100, 0.01);
verify(1, 0, 1, 50, 0.75);
// precision vs maneuvrability
verify(0, 100, 0, 100, 0);
verify(4, 50, 4, 50, 0.5);
verify(4, 50, 8, 50, 0.283);
verify(4, 50, 20, 50, 0.016);
verify(4, 50, 4, 50, 0.5);
verify(8, 50, 4, 50, 0.717);
verify(20, 50, 4, 50, 0.984);
// complex example
verify(7, 20, 5, 40, 0.639);
})
test.case("computes an effective success value, with random element", check => {
function verify(success_base: number, luck: number, random: number, expected: number) {
let ship1 = new Ship();
let ship2 = new Ship();
let action = new TriggerAction(new Equipment());
action.luck = luck;
check.patch(action, "getSuccessFactor", () => success_base);
check.nears(action.getEffectiveSuccess(ship1, ship2, new SkewedRandomGenerator([random])), expected, 5,
`success ${success_base}, luck ${luck}, random ${random}`);
}
// no luck influence
verify(0.3, 0, 0.4, 0.3);
verify(0.51, 0, 0.7, 0.51);
// small luck influence
verify(0.5, 5, 0.0, 0);
verify(0.5, 5, 0.1, 0.47979);
verify(0.5, 5, 0.4, 0.49715);
verify(0.5, 5, 0.8, 0.51161);
verify(0.5, 5, 1.0, 1.0);
verify(0.7, 5, 0.5, 0.69399);
// large luck influence
verify(0.5, 45, 0.0, 0);
verify(0.5, 45, 0.1, 0.31336);
verify(0.5, 45, 0.4, 0.46864);
verify(0.5, 45, 0.8, 0.61679);
verify(0.5, 45, 1.0, 1);
verify(0.7, 45, 0.5, 0.63485);
})
test.case("guesses targetting mode", check => {
let ship = new Ship();
let equ = new Equipment();
@ -120,5 +199,35 @@ module TK.SpaceTac.Specs {
check.equals(result, true);
check.nears(ship.arena_angle, 1.107, 3);
})
test.case("builds a textual description", check => {
let action = new TriggerAction(new Equipment());
action.power = 0;
check.equals(action.getEffectsDescription(), "");
action.effects.push(new AttributeMultiplyEffect("precision", 20));
check.equals(action.getEffectsDescription(), "Trigger:\n• precision +20% on self");
action.power = 2;
check.equals(action.getEffectsDescription(), "Trigger (power 2):\n• precision +20% on self");
action.range = 120;
check.equals(action.getEffectsDescription(), "Fire (power 2, range 120km):\n• precision +20% on target");
action.aim = 10;
check.equals(action.getEffectsDescription(), "Fire (power 2, range 120km, aim +10%):\n• precision +20% on target");
action.evasion = 35;
check.equals(action.getEffectsDescription(), "Fire (power 2, range 120km, aim +10%, evasion -35%):\n• precision +20% on target");
action.angle = 80;
check.equals(action.getEffectsDescription(), "Fire (power 2, range 120km, aim +10%, evasion -35%):\n• precision +20% in 80° arc");
action.blast = 100;
check.equals(action.getEffectsDescription(), "Fire (power 2, range 120km, aim +10%, evasion -35%):\n• precision +20% in 100km radius");
action.luck = 15;
check.equals(action.getEffectsDescription(), "Fire (power 2, range 120km, aim +10%, evasion -35%, luck ±15%):\n• precision +20% in 100km radius");
})
});
}

View File

@ -19,13 +19,22 @@ module TK.SpaceTac {
// Angle of the area between the source and the target that will be impacted
angle: number
// Influence of "precision" of firing ship (0..100)
aim: number
// Influence of "maneuvrability" of impacted ship (0..100)
evasion: number
// Influence of luck
luck: number
// Effects applied on target
effects: BaseEffect[]
// Equipment cannot be null
equipment: Equipment
constructor(equipment: Equipment, effects: BaseEffect[] = [], power = 1, range = 0, blast = 0, angle = 0, code = `fire-${equipment.code}`) {
constructor(equipment: Equipment, effects: BaseEffect[] = [], power = 1, range = 0, blast = 0, angle = 0, aim = 0, evasion = 0, luck = 0, code = `fire-${equipment.code}`) {
super(code, equipment);
this.power = power;
@ -33,6 +42,9 @@ module TK.SpaceTac {
this.effects = effects;
this.blast = blast;
this.angle = angle;
this.aim = aim;
this.evasion = evasion;
this.luck = luck;
}
getVerb(): string {
@ -82,6 +94,42 @@ module TK.SpaceTac {
return this.range;
}
/**
* Get the success factor [0-1] for this action applied from a ship to another
*
* This is a predictible formula, not including random elements.
*/
getSuccessFactor(from: Ship, to: Ship): number {
let aim = this.aim * 0.01;
let evasion = this.evasion * 0.01;
let f1 = (x: number) => (x < 0 ? -1 : 1) * (1 - 1 / (x * x + 1));
let prec = from.getAttribute("precision");
let man = to.getAttribute("maneuvrability");
let delta = Math.min(aim, evasion) * f1(0.2 * (prec - man));
return clamp(1 - aim * (1 - f1(prec)) - evasion * f1(man) + delta, 0, 1);
}
/**
* Get the effective success of the action [0-1].
*
* Result has more chance to be near the success factor, but may be in the whole [0-1] range.
*/
getEffectiveSuccess(from: Ship, to: Ship, random = RandomGenerator.global): number {
let p = this.getSuccessFactor(from, to);
let s = this.luck * 0.01;
if (s) {
let c = (2 / (2 - s)) - 1;
let x = random.random();
if (x <= p) {
return Math.pow(x, c) / Math.pow(p, c - 1);
} else {
return 1 - Math.pow(1 - x, c) / Math.pow(1 - p, c - 1);
}
} else {
return p;
}
}
filterImpactedShips(source: ArenaLocation, target: Target, ships: Ship[]): Ship[] {
if (this.blast) {
return ships.filter(ship => arenaDistance(ship.location, target) <= this.blast);
@ -128,12 +176,15 @@ module TK.SpaceTac {
/**
* Collect the effects applied by this action
*
* If *luck* is specified, an effective success factor is used, instead of an estimated one
*/
getEffects(ship: Ship, target: Target, source = ship.location): [Ship, BaseEffect][] {
let result: [Ship, BaseEffect][] = [];
getEffects(ship: Ship, target: Target, source = ship.location, luck?: RandomGenerator): [Ship, BaseEffect, number][] {
let result: [Ship, BaseEffect, number][] = [];
let ships = this.getImpactedShips(ship, target, source);
ships.forEach(ship => {
this.effects.forEach(effect => result.push([ship, effect]));
ships.forEach(iship => {
let success = luck ? this.getEffectiveSuccess(ship, iship, luck) : this.getSuccessFactor(ship, iship);
this.effects.forEach(effect => result.push([iship, effect, success]));
});
return result;
}
@ -157,9 +208,9 @@ module TK.SpaceTac {
}
// Apply effects
let effects = this.getEffects(ship, target);
effects.forEach(([ship_target, effect]) => {
let diffs = effect.getOnDiffs(ship_target, ship);
let effects = this.getEffects(ship, target, undefined, RandomGenerator.global);
effects.forEach(([ship_target, effect, success]) => {
let diffs = effect.getOnDiffs(ship_target, ship, success);
result = result.concat(diffs);
});
@ -173,13 +224,22 @@ module TK.SpaceTac {
let info: string[] = [];
if (this.power) {
info.push(`power usage ${this.power}`);
info.push(`power ${this.power}`);
}
if (this.range) {
info.push(`max range ${this.range}km`);
info.push(`range ${this.range}km`);
}
if (this.aim) {
info.push(`aim +${this.aim}%`);
}
if (this.evasion) {
info.push(`evasion -${this.evasion}%`);
}
if (this.luck) {
info.push(`luck ±${this.luck}%`);
}
let desc = `${this.getVerb()} (${info.join(", ")})`;
let desc = (info.length) ? `${this.getVerb()} (${info.join(", ")})` : this.getVerb();
let effects = this.effects.map(effect => {
let suffix: string;
if (this.blast) {

View File

@ -1,10 +1,9 @@
module TK.SpaceTac.Specs {
testing("Maneuver", test => {
function compare_maneuver_effects(check: TestContext, meff1: [Ship, BaseEffect][], meff2: [Ship, BaseEffect][]): void {
let [ships1, effects1] = unzip(meff1);
let [ships2, effects2] = unzip(meff2);
check.equals(ships1, ships2, "impacted ships");
compare_effects(check, effects1, effects2);
function compare_maneuver_effects(check: TestContext, meff1: ManeuverEffect[], meff2: ManeuverEffect[]): void {
check.equals(meff1.map(ef => ef.ship), meff2.map(ef => ef.ship), "impacted ships");
compare_effects(check, meff1.map(ef => ef.effect), meff2.map(ef => ef.effect));
check.equals(meff1.map(ef => ef.success), meff2.map(ef => ef.success), "success factor");
}
test.case("guesses weapon effects", check => {
@ -21,8 +20,8 @@ module TK.SpaceTac.Specs {
TestTools.setShipHP(ship3, 30, 30);
let maneuver = new Maneuver(ship1, nn(weapon.action), Target.newFromLocation(0, 0));
compare_maneuver_effects(check, maneuver.effects, [
[ship1, new DamageEffect(50)],
[ship2, new DamageEffect(50)]
{ ship: ship1, effect: new DamageEffect(50), success: 1 },
{ ship: ship2, effect: new DamageEffect(50), success: 1 },
]);
});
@ -41,8 +40,8 @@ module TK.SpaceTac.Specs {
TestTools.setShipHP(ship3, 30, 30);
let maneuver = new Maneuver(ship1, weapon.action, Target.newFromLocation(0, 0));
compare_maneuver_effects(check, maneuver.effects, [
[ship1, new ValueEffect("shield", 10)],
[ship2, new ValueEffect("shield", 10)]
{ ship: ship1, effect: new ValueEffect("shield", 10), success: 1 },
{ ship: ship2, effect: new ValueEffect("shield", 10), success: 1 },
]);
});
@ -66,7 +65,7 @@ module TK.SpaceTac.Specs {
maneuver = new Maneuver(ship, move, Target.newFromLocation(100, 30));
check.containing(maneuver.getFinalLocation(), { x: 100, y: 30 });
compare_maneuver_effects(check, maneuver.effects, [
[ship, new AttributeEffect("maneuvrability", 1)]
{ ship: ship, effect: new AttributeEffect("maneuvrability", 1), success: 1 },
]);
});
});

View File

@ -1,4 +1,11 @@
module TK.SpaceTac {
// Single effect of a maneuver
export type ManeuverEffect = {
ship: Ship
effect: BaseEffect
success: number
}
/**
* Ship maneuver for an artifical intelligence
*
@ -21,7 +28,7 @@ module TK.SpaceTac {
simulation: MoveFireResult
// List of guessed effects of this maneuver
effects: [Ship, BaseEffect][]
effects: ManeuverEffect[]
constructor(ship: Ship, action: BaseAction, target: Target, move_margin = 1) {
this.ship = ship;
@ -74,16 +81,18 @@ module TK.SpaceTac {
/**
* Guess what will be the effects applied on any ship by this maneuver
*/
guessEffects(): [Ship, BaseEffect][] {
let result: [Ship, BaseEffect][] = [];
guessEffects(): ManeuverEffect[] {
let result: ManeuverEffect[] = [];
// Effects of weapon
if (this.action instanceof TriggerAction) {
result = result.concat(this.action.getEffects(this.ship, this.target));
this.action.getEffects(this.ship, this.target).forEach(([ship, effect, success]) => {
result.push({ ship: ship, effect: effect, success: success });
})
} else if (this.action instanceof DeployDroneAction) {
let ships = this.battle.collectShipsInCircle(this.target, this.action.drone_radius, true);
this.action.drone_effects.forEach(effect => {
result = result.concat(ships.map(ship => <[Ship, BaseEffect]>[ship, effect]));
result = result.concat(ships.map(ship => ({ ship: ship, effect: effect, success: 1 })));
});
}
@ -91,7 +100,9 @@ module TK.SpaceTac {
let location = this.getFinalLocation();
let effects = this.battle.drones.list().forEach(drone => {
if (Target.newFromLocation(location.x, location.y).isInRange(drone.x, drone.y, drone.radius)) {
result = result.concat(drone.effects.map(effect => <[Ship, BaseEffect]>[this.ship, effect]));
result = result.concat(drone.effects.map(effect => (
{ ship: this.ship, effect: effect, success: 1 }
)));
}
});

View File

@ -29,17 +29,17 @@ module TK.SpaceTac {
let dhull = 0;
let dshield = 0;
maneuver.effects.forEach(([iship, effect]) => {
if (iship === ship) {
if (effect instanceof DamageEffect) {
let [ds, dh] = effect.getEffectiveDamage(ship);
dhull -= dh;
dshield -= ds;
} else if (effect instanceof ValueEffect) {
if (effect.valuetype == "hull") {
dhull = clamp(hull + effect.value_on, 0, chull) - hull;
} else if (effect.valuetype == "shield") {
dshield += clamp(shield + effect.value_on, 0, cshield) - shield;
maneuver.effects.forEach(result => {
if (result.ship === ship) {
if (result.effect instanceof DamageEffect) {
let damage = result.effect.getEffectiveDamage(ship, result.success);
dhull -= damage.hull;
dshield -= damage.shield;
} else if (result.effect instanceof ValueEffect) {
if (result.effect.valuetype == "hull") {
dhull = clamp(hull + result.effect.value_on, 0, chull) - hull;
} else if (result.effect.valuetype == "shield") {
dshield += clamp(shield + result.effect.value_on, 0, cshield) - shield;
}
}
}

View File

@ -13,11 +13,15 @@ module TK.SpaceTac {
// Damage to shield
shield: number
constructor(ship: Ship, hull: number, shield: number) {
// Theoretical damage value
theoretical: number
constructor(ship: Ship, hull: number, shield: number, theoretical = hull + shield) {
super(ship);
this.hull = hull;
this.shield = shield;
this.theoretical = theoretical;
}
protected applyOnShip(ship: Ship, battle: Battle): void {

View File

@ -6,11 +6,11 @@ module TK.SpaceTac {
check.equals(ship.getAttribute("maneuvrability"), 0, "initial");
let effect1 = new AttributeEffect("maneuvrability", 20);
battle.applyDiffs(effect1.getOnDiffs(ship, ship));
battle.applyDiffs(effect1.getOnDiffs(ship, ship, 1));
check.equals(ship.getAttribute("maneuvrability"), 20, "applied 1");
let effect2 = new AttributeEffect("maneuvrability", 10);
battle.applyDiffs(effect2.getOnDiffs(ship, ship));
battle.applyDiffs(effect2.getOnDiffs(ship, ship, 1));
check.equals(ship.getAttribute("maneuvrability"), 30, "applied 2");
battle.applyDiffs(effect1.getOffDiffs(ship));

View File

@ -20,7 +20,7 @@ module TK.SpaceTac {
this.value = value;
}
getOnDiffs(ship: Ship, source: Ship | Drone): BaseBattleDiff[] {
getOnDiffs(ship: Ship, source: Ship | Drone, success: number): BaseBattleDiff[] {
return [
new ShipAttributeDiff(ship, this.attrcode, { cumulative: this.value }, {}),
];

View File

@ -7,11 +7,11 @@ module TK.SpaceTac {
check.equals(ship.getAttribute("precision"), 12, "initial");
let effect1 = new AttributeLimitEffect("precision", 5);
battle.applyDiffs(effect1.getOnDiffs(ship, ship));
battle.applyDiffs(effect1.getOnDiffs(ship, ship, 1));
check.equals(ship.getAttribute("precision"), 5, "applied 1");
let effect2 = new AttributeLimitEffect("precision", 3);
battle.applyDiffs(effect2.getOnDiffs(ship, ship));
battle.applyDiffs(effect2.getOnDiffs(ship, ship, 1));
check.equals(ship.getAttribute("precision"), 3, "applied 2");
battle.applyDiffs(effect1.getOffDiffs(ship));

View File

@ -20,7 +20,7 @@ module TK.SpaceTac {
this.value = value;
}
getOnDiffs(ship: Ship, source: Ship | Drone): BaseBattleDiff[] {
getOnDiffs(ship: Ship, source: Ship | Drone, success: number): BaseBattleDiff[] {
return [
new ShipAttributeDiff(ship, this.attrcode, { limit: this.value }, {}),
];

View File

@ -7,11 +7,11 @@ module TK.SpaceTac {
check.equals(ship.getAttribute("hull_capacity"), 100, "initial");
let effect1 = new AttributeMultiplyEffect("hull_capacity", 30);
battle.applyDiffs(effect1.getOnDiffs(ship, ship));
battle.applyDiffs(effect1.getOnDiffs(ship, ship, 1));
check.equals(ship.getAttribute("hull_capacity"), 130, "applied 1");
let effect2 = new AttributeMultiplyEffect("hull_capacity", -10);
battle.applyDiffs(effect2.getOnDiffs(ship, ship));
battle.applyDiffs(effect2.getOnDiffs(ship, ship, 1));
check.equals(ship.getAttribute("hull_capacity"), 120, "applied 2");
battle.applyDiffs(effect1.getOffDiffs(ship));

View File

@ -21,7 +21,7 @@ module TK.SpaceTac {
this.value = value;
}
getOnDiffs(ship: Ship, source: Ship | Drone): BaseBattleDiff[] {
getOnDiffs(ship: Ship, source: Ship | Drone, success: number): BaseBattleDiff[] {
return [
new ShipAttributeDiff(ship, this.attrcode, { multiplier: this.value }, {}),
];

View File

@ -1,11 +0,0 @@
module TK.SpaceTac.Specs {
testing("BaseEffect", test => {
test.case("gets a fixed or variable amount", check => {
let effect = new BaseEffect("test");
check.equals(effect.resolveAmount(50), 50);
check.equals(effect.resolveAmount({ base: 20, span: 10 }, new SkewedRandomGenerator([0.3])), 23);
check.equals(effect.resolveAmount({ base: 20, span: 0 }, new SkewedRandomGenerator([0.3])), 20);
})
})
}

View File

@ -1,8 +1,6 @@
/// <reference path="../diffs/BaseBattleDiff.ts" />
module TK.SpaceTac {
export type EffectAmount = number | { base: number, span: number };
/**
* Base class for effects of actions that can be applied on ships
*
@ -21,7 +19,7 @@ module TK.SpaceTac {
/**
* Get the list of diffs needed to activate this effect on a ship
*/
getOnDiffs(ship: Ship, source: Ship | Drone): BaseBattleDiff[] {
getOnDiffs(ship: Ship, source: Ship | Drone, success = 1): BaseBattleDiff[] {
return [];
}
@ -60,18 +58,5 @@ module TK.SpaceTac {
getDescription(): string {
return "unknown effect";
}
/**
* Resolve an effect amount
*/
resolveAmount(val: EffectAmount, random = RandomGenerator.global): number {
if (typeof val == "number") {
return val;
} else if (val.span) {
return random.randInt(val.base, val.base + val.span);
} else {
return val.base;
}
}
}
}

View File

@ -8,24 +8,24 @@ module TK.SpaceTac {
check.equals(weapons.map(weapon => weapon.cooldown.heat), [0, 0, 0]);
let effect = new CooldownEffect(0, 0);
battle.applyDiffs(effect.getOnDiffs(ship, ship));
battle.applyDiffs(effect.getOnDiffs(ship, ship, 1));
check.equals(weapons.map(weapon => weapon.cooldown.heat), [0, 0, 0]);
weapons.forEach(weapon => weapon.cooldown.use());
check.equals(weapons.map(weapon => weapon.cooldown.heat), [3, 3, 3]);
battle.applyDiffs(effect.getOnDiffs(ship, ship));
battle.applyDiffs(effect.getOnDiffs(ship, ship, 1));
check.equals(weapons.map(weapon => weapon.cooldown.heat), [0, 0, 0]);
weapons.forEach(weapon => weapon.cooldown.use());
check.equals(weapons.map(weapon => weapon.cooldown.heat), [3, 3, 3]);
effect = new CooldownEffect(1, 0);
battle.applyDiffs(effect.getOnDiffs(ship, ship));
battle.applyDiffs(effect.getOnDiffs(ship, ship, 1));
check.equals(weapons.map(weapon => weapon.cooldown.heat), [2, 2, 2]);
effect = new CooldownEffect(1, 2);
battle.applyDiffs(effect.getOnDiffs(ship, ship));
battle.applyDiffs(effect.getOnDiffs(ship, ship, 1));
check.equals(weapons.map(weapon => weapon.cooldown.heat).sort(), [1, 1, 2]);
})

View File

@ -18,7 +18,7 @@ module TK.SpaceTac {
this.maxcount = maxcount;
}
getOnDiffs(ship: Ship, source: Ship | Drone): BaseBattleDiff[] {
getOnDiffs(ship: Ship, source: Ship | Drone, success: number): BaseBattleDiff[] {
let equipments = ship.listEquipment().filter(equ => equ.cooldown.heat > 0);
if (this.maxcount && equipments.length > this.maxcount) {

View File

@ -20,16 +20,16 @@ module TK.SpaceTac.Specs {
checkValues("initial", 150, 400, 0, 0);
battle.applyDiffs(new DamageEffect(50).getOnDiffs(ship, ship));
battle.applyDiffs(new DamageEffect(50).getOnDiffs(ship, ship, 1));
checkValues("after 50 damage", 150, 350, 0, 5);
battle.applyDiffs(new DamageEffect(250).getOnDiffs(ship, ship));
battle.applyDiffs(new DamageEffect(250).getOnDiffs(ship, ship, 1));
checkValues("after 250 damage", 150, 100, 0, 30);
battle.applyDiffs(new DamageEffect(201).getOnDiffs(ship, ship));
battle.applyDiffs(new DamageEffect(201).getOnDiffs(ship, ship, 1));
checkValues("after 201 damage", 49, 0, 11, 40);
battle.applyDiffs(new DamageEffect(8000).getOnDiffs(ship, ship));
battle.applyDiffs(new DamageEffect(8000).getOnDiffs(ship, ship, 1));
checkValues("after 8000 damage", 0, 0, 16, 40);
});
@ -43,7 +43,7 @@ module TK.SpaceTac.Specs {
TestTools.setShipHP(ship, 1000, 1000);
let damage = new DamageEffect(200);
check.equals(damage.getEffectiveDamage(ship), [200, 0]);
check.equals(damage.getEffectiveDamage(ship, 1), new ShipDamageDiff(ship, 0, 200));
check.patch(ship, "ieffects", iterator([
isingle(new DamageModifierEffect(-15)),
@ -54,14 +54,14 @@ module TK.SpaceTac.Specs {
isingle(new DamageModifierEffect(3))
]));
check.equals(damage.getEffectiveDamage(ship), [170, 0]);
check.equals(damage.getEffectiveDamage(ship), [240, 0]);
check.equals(damage.getEffectiveDamage(ship), [0, 0]);
check.equals(damage.getEffectiveDamage(ship), [400, 0]);
check.equals(damage.getEffectiveDamage(ship), [190, 0]);
check.equals(damage.getEffectiveDamage(ship, 1), new ShipDamageDiff(ship, 0, 170));
check.equals(damage.getEffectiveDamage(ship, 1), new ShipDamageDiff(ship, 0, 240));
check.equals(damage.getEffectiveDamage(ship, 1), new ShipDamageDiff(ship, 0, 0));
check.equals(damage.getEffectiveDamage(ship, 1), new ShipDamageDiff(ship, 0, 400));
check.equals(damage.getEffectiveDamage(ship, 1), new ShipDamageDiff(ship, 0, 190));
damage = new DamageEffect(40);
check.equals(damage.getEffectiveDamage(ship), [41, 0]);
check.equals(damage.getEffectiveDamage(ship, 1), new ShipDamageDiff(ship, 0, 41));
});
});
}

View File

@ -8,10 +8,10 @@ module TK.SpaceTac {
*/
export class DamageEffect extends BaseEffect {
// Base damage points
base: number;
base: number
// Range of randomness (effective damage will be between *value* and *value+range*)
span: number;
span: number
constructor(value = 0, span = 0) {
super("damage");
@ -36,47 +36,36 @@ module TK.SpaceTac {
/**
* Get the effective damage done to both shield and hull (in this order)
*/
getEffectiveDamage(ship: Ship): [number, number] {
var damage = (this.span > 0) ? RandomGenerator.global.randInt(this.base, this.base + this.span) : this.base;
var hull: number;
var shield: number;
getEffectiveDamage(ship: Ship, success: number): ShipDamageDiff {
// Apply modifiers
damage = Math.round(damage * this.getFactor(ship));
let theoritical = Math.round((this.base + this.span * success) * this.getFactor(ship));
let damage = theoritical;
// Apply on shields
if (damage >= ship.getValue("shield")) {
shield = ship.getValue("shield");
} else {
shield = damage;
}
let shield = (damage >= ship.getValue("shield")) ? ship.getValue("shield") : damage;
damage -= shield;
// Apply on hull
if (damage >= ship.getValue("hull")) {
hull = ship.getValue("hull");
} else {
hull = damage;
}
let hull = (damage >= ship.getValue("hull")) ? ship.getValue("hull") : damage;
return [shield, hull];
return new ShipDamageDiff(ship, hull, shield, theoritical);
}
getOnDiffs(ship: Ship, source: Ship | Drone): BaseBattleDiff[] {
let [shield, hull] = this.getEffectiveDamage(ship);
getOnDiffs(ship: Ship, source: Ship | Drone, success: number): BaseBattleDiff[] {
let result: BaseBattleDiff[] = [];
if (shield || hull) {
result.push(new ShipDamageDiff(ship, hull, shield));
let damage = this.getEffectiveDamage(ship, success);
if (damage.shield || damage.hull) {
result.push(damage);
}
if (shield) {
result.push(new ShipValueDiff(ship, "shield", -shield));
if (damage.shield) {
result.push(new ShipValueDiff(ship, "shield", -damage.shield));
}
if (hull) {
result.push(new ShipValueDiff(ship, "hull", -hull));
if (damage.hull) {
result.push(new ShipValueDiff(ship, "hull", -damage.hull));
}
return result;

View File

@ -14,9 +14,9 @@ module TK.SpaceTac.Specs {
ship2a.setArenaPosition(100, 280);
let effect = new RepelEffect(12);
battle.applyDiffs(effect.getOnDiffs(ship1a, ship1a));
battle.applyDiffs(effect.getOnDiffs(ship1b, ship1a));
battle.applyDiffs(effect.getOnDiffs(ship2a, ship1a));
battle.applyDiffs(effect.getOnDiffs(ship1a, ship1a, 1));
battle.applyDiffs(effect.getOnDiffs(ship1b, ship1a, 1));
battle.applyDiffs(effect.getOnDiffs(ship2a, ship1a, 1));
check.equals(ship1a.location, new ArenaLocationAngle(100, 100));
check.equals(ship1b.location, new ArenaLocationAngle(262, 100));
@ -33,7 +33,7 @@ module TK.SpaceTac.Specs {
ship2b.setArenaPosition(100, 350);
let effect = new RepelEffect(85);
battle.applyDiffs(effect.getOnDiffs(ship2a, ship1a));
battle.applyDiffs(effect.getOnDiffs(ship2a, ship1a, 1));
check.equals(ship2a.location, new ArenaLocationAngle(100, 250));
})
})

View File

@ -13,7 +13,7 @@ module TK.SpaceTac {
this.value = value;
}
getOnDiffs(ship: Ship, source: Ship | Drone): BaseBattleDiff[] {
getOnDiffs(ship: Ship, source: Ship | Drone, success: number): BaseBattleDiff[] {
if (ship != source) {
let angle = arenaAngle(source.location, ship.location);
let destination = new ArenaLocation(ship.arena_x + Math.cos(angle) * this.value, ship.arena_y + Math.sin(angle) * this.value);

View File

@ -10,7 +10,7 @@ module TK.SpaceTac.Specs {
})
let effect = new StickyEffect(new AttributeEffect("precision", 1), 2);
battle.applyDiffs(effect.getOnDiffs(ship, ship));
battle.applyDiffs(effect.getOnDiffs(ship, ship, 1));
check.in("after", check => {
check.equals(ship.active_effects.count(), 1, "one sticky effect");
@ -25,7 +25,7 @@ module TK.SpaceTac.Specs {
}
})
battle.applyDiffs(effect.getOnDiffs(ship, ship));
battle.applyDiffs(effect.getOnDiffs(ship, ship, 1));
check.in("after second apply", check => {
check.equals(ship.active_effects.count(), 1, "one sticky effect");

View File

@ -21,7 +21,7 @@ module TK.SpaceTac {
this.duration = duration;
}
getOnDiffs(ship: Ship, source: Ship | Drone): BaseBattleDiff[] {
getOnDiffs(ship: Ship, source: Ship | Drone, success: number): BaseBattleDiff[] {
let result: BaseBattleDiff[] = [];
let previous = ship.active_effects.get(this.id);

View File

@ -8,10 +8,10 @@ module TK.SpaceTac {
ship.setValue("shield", 55);
check.equals(ship.getValue("shield"), 55);
battle.applyDiffs(effect.getOnDiffs(ship, ship));
battle.applyDiffs(effect.getOnDiffs(ship, ship, 1));
check.equals(ship.getValue("shield"), 75);
battle.applyDiffs(effect.getOnDiffs(ship, ship));
battle.applyDiffs(effect.getOnDiffs(ship, ship, 1));
check.equals(ship.getValue("shield"), 95);
});

View File

@ -36,7 +36,7 @@ module TK.SpaceTac {
this.value_end = value_end;
}
getOnDiffs(ship: Ship, source: Ship | Drone): BaseBattleDiff[] {
getOnDiffs(ship: Ship, source: Ship | Drone, success: number): BaseBattleDiff[] {
if (this.value_on) {
return ship.getValueDiffs(this.valuetype, this.value_on, true);
} else {

View File

@ -9,12 +9,12 @@ module TK.SpaceTac.Specs {
TestTools.setShipHP(ship2, 100, 50);
let effect = new ValueTransferEffect("hull", -30);
battle.applyDiffs(effect.getOnDiffs(ship2, ship1));
battle.applyDiffs(effect.getOnDiffs(ship2, ship1, 1));
check.equals(ship1.getValue("hull"), 40);
check.equals(ship2.getValue("hull"), 70);
effect = new ValueTransferEffect("hull", 1000);
battle.applyDiffs(effect.getOnDiffs(ship2, ship1));
battle.applyDiffs(effect.getOnDiffs(ship2, ship1, 1));
check.equals(ship1.getValue("hull"), 0);
check.equals(ship2.getValue("hull"), 110); // over limit but will be fixed later
})

View File

@ -18,10 +18,10 @@ module TK.SpaceTac {
this.amount = amount;
}
getOnDiffs(ship: Ship, source: Ship | Drone): BaseBattleDiff[] {
getOnDiffs(ship: Ship, source: Ship | Drone, success: number): BaseBattleDiff[] {
if (source instanceof Ship) {
if (this.amount < 0) {
return new ValueTransferEffect(this.valuetype, -this.amount).getOnDiffs(source, ship);
return new ValueTransferEffect(this.valuetype, -this.amount).getOnDiffs(source, ship, success);
} else {
let amount = Math.min(source.getValue(this.valuetype), this.amount);
if (amount) {

View File

@ -5,25 +5,25 @@ module TK.SpaceTac.Specs {
let equipment = template.generate(1);
check.equals(equipment.requirements, { "skill_materials": 1 });
compare_trigger_action(check, equipment.action, new TriggerAction(equipment, [new DamageEffect(30, 20)], 3, 400, 0));
compare_trigger_action(check, equipment.action, new TriggerAction(equipment, [new DamageEffect(30, 20)], 3, 400, 0, 0, 60, 20, 15));
check.equals(equipment.price, 100);
check.equals(equipment.cooldown, new Cooldown(2, 2));
equipment = template.generate(2);
check.equals(equipment.requirements, { "skill_materials": 2 });
compare_trigger_action(check, equipment.action, new TriggerAction(equipment, [new DamageEffect(42, 28)], 3, 412, 0));
compare_trigger_action(check, equipment.action, new TriggerAction(equipment, [new DamageEffect(42, 28)], 3, 412, 0, 0, 60, 20, 15));
check.equals(equipment.price, 350);
check.equals(equipment.cooldown, new Cooldown(2, 2));
equipment = template.generate(3);
check.equals(equipment.requirements, { "skill_materials": 4 });
compare_trigger_action(check, equipment.action, new TriggerAction(equipment, [new DamageEffect(56, 37)], 3, 426, 0));
compare_trigger_action(check, equipment.action, new TriggerAction(equipment, [new DamageEffect(56, 37)], 3, 426, 0, 0, 60, 20, 15));
check.equals(equipment.price, 850);
check.equals(equipment.cooldown, new Cooldown(2, 2));
equipment = template.generate(10);
check.equals(equipment.requirements, { "skill_materials": 23 });
compare_trigger_action(check, equipment.action, new TriggerAction(equipment, [new DamageEffect(224, 149)], 3, 594, 0));
compare_trigger_action(check, equipment.action, new TriggerAction(equipment, [new DamageEffect(224, 149)], 3, 594, 0, 0, 60, 20, 15));
check.equals(equipment.price, 11350);
check.equals(equipment.cooldown, new Cooldown(2, 2));
});
@ -33,25 +33,25 @@ module TK.SpaceTac.Specs {
let equipment = template.generate(1);
check.equals(equipment.requirements, { "skill_materials": 1, "skill_photons": 1 });
compare_trigger_action(check, equipment.action, new TriggerAction(equipment, [new DamageEffect(26, 4)], 4, 500, 150));
compare_trigger_action(check, equipment.action, new TriggerAction(equipment, [new DamageEffect(26, 4)], 4, 500, 150, 0, 30, 40, 10));
check.equals(equipment.cooldown, new Cooldown(1, 0));
check.equals(equipment.price, 163);
equipment = template.generate(2);
check.equals(equipment.requirements, { "skill_materials": 2, "skill_photons": 1 });
compare_trigger_action(check, equipment.action, new TriggerAction(equipment, [new DamageEffect(28, 5)], 4, 520, 155));
compare_trigger_action(check, equipment.action, new TriggerAction(equipment, [new DamageEffect(28, 5)], 4, 520, 155, 0, 30, 40, 10));
check.equals(equipment.cooldown, new Cooldown(1, 0));
check.equals(equipment.price, 570);
equipment = template.generate(3);
check.equals(equipment.requirements, { "skill_materials": 3, "skill_photons": 2 });
compare_trigger_action(check, equipment.action, new TriggerAction(equipment, [new DamageEffect(30, 6)], 4, 544, 161));
compare_trigger_action(check, equipment.action, new TriggerAction(equipment, [new DamageEffect(30, 6)], 4, 544, 161, 0, 30, 40, 10));
check.equals(equipment.cooldown, new Cooldown(1, 0));
check.equals(equipment.price, 1385);
equipment = template.generate(10);
check.equals(equipment.requirements, { "skill_materials": 20, "skill_photons": 13 });
compare_trigger_action(check, equipment.action, new TriggerAction(equipment, [new DamageEffect(58, 20)], 4, 824, 231));
compare_trigger_action(check, equipment.action, new TriggerAction(equipment, [new DamageEffect(58, 20)], 4, 824, 231, 0, 30, 40, 10));
check.equals(equipment.cooldown, new Cooldown(1, 0));
check.equals(equipment.price, 18500);
});
@ -61,25 +61,25 @@ module TK.SpaceTac.Specs {
let equipment = template.generate(1);
check.equals(equipment.requirements, { "skill_photons": 1, "skill_quantum": 1 });
compare_trigger_action(check, equipment.action, new TriggerAction(equipment, [new DamageEffect(20, 25)], 5, 300, 0, 40));
compare_trigger_action(check, equipment.action, new TriggerAction(equipment, [new DamageEffect(20, 25)], 5, 300, 0, 40, 45, 60, 20));
check.equals(equipment.cooldown, new Cooldown(1, 1));
check.equals(equipment.price, 152);
equipment = template.generate(2);
check.equals(equipment.requirements, { "skill_antimatter": 1, "skill_photons": 2, "skill_quantum": 2 });
compare_trigger_action(check, equipment.action, new TriggerAction(equipment, [new DamageEffect(28, 35)], 5, 310, 0, 42));
compare_trigger_action(check, equipment.action, new TriggerAction(equipment, [new DamageEffect(28, 35)], 5, 310, 0, 42, 45, 60, 20));
check.equals(equipment.cooldown, new Cooldown(1, 1));
check.equals(equipment.price, 532);
equipment = template.generate(3);
check.equals(equipment.requirements, { "skill_antimatter": 1, "skill_photons": 4, "skill_quantum": 3 });
compare_trigger_action(check, equipment.action, new TriggerAction(equipment, [new DamageEffect(37, 47)], 5, 322, 0, 44));
compare_trigger_action(check, equipment.action, new TriggerAction(equipment, [new DamageEffect(37, 47)], 5, 322, 0, 44, 45, 60, 20));
check.equals(equipment.cooldown, new Cooldown(1, 1));
check.equals(equipment.price, 1292);
equipment = template.generate(10);
check.equals(equipment.requirements, { "skill_antimatter": 11, "skill_photons": 23, "skill_quantum": 20 });
compare_trigger_action(check, equipment.action, new TriggerAction(equipment, [new DamageEffect(149, 187)], 5, 462, 0, 72));
compare_trigger_action(check, equipment.action, new TriggerAction(equipment, [new DamageEffect(149, 187)], 5, 462, 0, 72, 45, 60, 20));
check.equals(equipment.cooldown, new Cooldown(1, 1));
check.equals(equipment.price, 17252);
});

View File

@ -9,7 +9,7 @@ module TK.SpaceTac.Equipments {
this.setCooldown(irepeat(2), irepeat(2));
this.addTriggerAction(irepeat(3), [
new EffectTemplate(new DamageEffect(), { base: leveled(30), span: leveled(20) })
], leveled(400, 12));
], leveled(400, 12), undefined, undefined, irepeat(60), irepeat(20), irepeat(15));
}
}
@ -21,7 +21,7 @@ module TK.SpaceTac.Equipments {
this.setCooldown(irepeat(1), irepeat(0));
this.addTriggerAction(irepeat(4), [
new EffectTemplate(new DamageEffect(), { base: leveled(26, 2), span: leveled(4, 1) })
], leveled(500, 20), leveled(150, 5));
], leveled(500, 20), leveled(150, 5), undefined, irepeat(30), irepeat(40), irepeat(10));
}
}
@ -34,7 +34,7 @@ module TK.SpaceTac.Equipments {
this.setCooldown(irepeat(1), irepeat(1));
this.addTriggerAction(irepeat(5), [
new EffectTemplate(new DamageEffect(), { base: leveled(20), span: leveled(25) })
], leveled(300, 10), irepeat(0), leveled(40, 2));
], leveled(300, 10), irepeat(0), leveled(40, 2), irepeat(45), irepeat(60), irepeat(20));
}
}
}

View File

@ -28,7 +28,7 @@ module TK.SpaceTac.UI.Specs {
ActionTooltip.fill(tooltip.getFiller(), ship, action2, 1);
checkText(check, (<any>tooltip).container.content.children[1], "Weapon");
checkText(check, (<any>tooltip).container.content.children[2], "Cost: 2 power");
checkText(check, (<any>tooltip).container.content.children[3], "Fire (power usage 2, max range 50km):\n• do 12 damage on target");
checkText(check, (<any>tooltip).container.content.children[3], "Fire (power 2, range 50km):\n• do 12 damage on target");
checkText(check, (<any>tooltip).container.content.children[4], "[ 2 ]");
tooltip.hide();

View File

@ -68,7 +68,8 @@ module TK.SpaceTac.UI {
// Add stasis effect
this.stasis = this.battleview.newImage("battle-hud-ship-stasis");
this.stasis.anchor.set(0.5, 0.5);
this.stasis.visible = false;
this.stasis.alpha = 0.9;
this.stasis.visible = !ship.alive;
this.add(this.stasis);
// HSP display
@ -202,7 +203,7 @@ module TK.SpaceTac.UI {
return {
background: async (animate, timer) => {
if (animate) {
await this.displayEffect(`${diff.hull + diff.shield} damage`, false);
await this.displayEffect(`${diff.theoretical} damage`, false);
await timer.sleep(1000);
}
}

View File

@ -55,11 +55,11 @@ module TK.SpaceTac.UI {
this.impact_area = new Phaser.Graphics(view.game);
this.impact_area.visible = false;
this.container.add(this.impact_indicators);
this.container.add(this.impact_area);
this.container.add(this.drawn_info);
this.container.add(this.move_ghost);
this.container.add(this.fire_arrow);
this.container.add(this.impact_indicators);
}
/**
@ -117,7 +117,7 @@ module TK.SpaceTac.UI {
}
/**
* Update impact indicators (highlighting impacted ships)
* Update impact indicators (highlighting impacted ships, with success factor)
*/
updateImpactIndicators(impacts: Phaser.Group, ship: Ship, action: BaseAction, target: Target, source: IArenaLocation = ship.location): void {
let ships = action.getImpactedShips(ship, target, source);
@ -125,9 +125,18 @@ module TK.SpaceTac.UI {
// TODO differential
impacts.removeAll(true);
ships.forEach(iship => {
let indicator = this.view.newImage("battle-hud-ship-impacted", iship.arena_x, iship.arena_y);
indicator.anchor.set(0.5);
impacts.add(indicator);
let builder = new UIBuilder(this.view, impacts);
let indicator = builder.image("battle-hud-ship-impacted", iship.arena_x, iship.arena_y);
indicator.anchor.set(0.5, 0.5);
if (action instanceof TriggerAction) {
let success = action.getSuccessFactor(ship, iship);
builder.in(indicator, builder => {
builder.text(`${Math.round(success * 100)}%`, 0, -32,
{ center: true, color: "#c69b70", size: 14, shadow: true });
});
}
});
impacts.visible = true;
} else {