1
0
Fork 0

Rewrite of sticky effects system, to allow any other effect to be sticky

This commit is contained in:
Michaël Lemaire 2017-01-24 01:14:04 +01:00
parent 0a822e705e
commit 875b71828d
13 changed files with 140 additions and 66 deletions

1
TODO
View file

@ -1,6 +1,5 @@
* Use succession's tools and serializer * Use succession's tools and serializer
* Effect should be random in a range (eg. "damage target 50-75") * Effect should be random in a range (eg. "damage target 50-75")
* Rewrite sticky effects to be more flexible (should be able to stick any effect)
* Add auto-move to attack * Add auto-move to attack
* Add equipment info (or summary) in ship tooltip * Add equipment info (or summary) in ship tooltip
* Handle effects overflowing ship tooltip when too numerous * Handle effects overflowing ship tooltip when too numerous

View file

@ -35,11 +35,7 @@ module SpaceTac.Game {
// Generate an effect with a given power // Generate an effect with a given power
generateFixed(power: number): BaseEffect { generateFixed(power: number): BaseEffect {
var effect = Tools.copyObject(this.effect); return this.effect.getModifiedCopy(this.modifiers, power);
this.modifiers.forEach((modifier: EffectTemplateModifier) => {
effect[modifier.name] = modifier.range.getProportional(power);
});
return effect;
} }
} }
} }

View file

@ -44,7 +44,7 @@ module SpaceTac.Game.Specs {
expect(equipment.getActionDescription()).toEqual("- 50 damage on all ships in 20km of impact"); expect(equipment.getActionDescription()).toEqual("- 50 damage on all ships in 20km of impact");
equipment.blast = 0; equipment.blast = 0;
equipment.target_effects.push(new AttributeLimitEffect(AttributeCode.Shield, 3, 200)); equipment.target_effects.push(new StickyEffect(new AttributeLimitEffect(AttributeCode.Shield, 200), 3));
expect(equipment.getActionDescription()).toEqual("- 50 damage on target\n- limit shield to 200 for 3 turns on target"); expect(equipment.getActionDescription()).toEqual("- 50 damage on target\n- limit shield to 200 for 3 turns on target");
}); });
}); });

View file

@ -149,9 +149,9 @@ module SpaceTac.Game {
} }
// Convenience function to add a sticking effect on target // Convenience function to add a sticking effect on target
addSticky(effect: StickyEffect, min_value: number, max_value: number = null, addSticky(effect: BaseEffect, min_value: number, max_value: number = null,
min_duration: number = 1, max_duration: number = null): void { min_duration: number = 1, max_duration: number = null, on_stick = false, on_turn_start = false): void {
var template = new EffectTemplate(effect); var template = new EffectTemplate(new StickyEffect(effect, 0, on_stick, on_turn_start));
template.addModifier("value", new IntegerRange(min_value, max_value)); template.addModifier("value", new IntegerRange(min_value, max_value));
template.addModifier("duration", new IntegerRange(min_duration, max_duration)); template.addModifier("duration", new IntegerRange(min_duration, max_duration));
this.target_effects.push(template); this.target_effects.push(template);

View file

@ -123,22 +123,20 @@ module SpaceTac.Game.Specs {
var ship = new Ship(); var ship = new Ship();
var battle = new Battle(ship.fleet); var battle = new Battle(ship.fleet);
var effect = new StickyEffect("test", 2); ship.addStickyEffect(new StickyEffect(new BaseEffect("test"), 2, false, true));
ship.addStickyEffect(effect); expect(ship.sticky_effects).toEqual([new StickyEffect(new BaseEffect("test"), 2, false, true)]);
expect(ship.sticky_effects).toEqual([effect]);
expect(battle.log.events).toEqual([ expect(battle.log.events).toEqual([
new EffectAddedEvent(ship, effect) new EffectAddedEvent(ship, new StickyEffect(new BaseEffect("test"), 2, false, true))
]); ]);
ship.startTurn(); ship.startTurn();
battle.log.clear(); battle.log.clear();
ship.endTurn(); ship.endTurn();
expect(ship.sticky_effects).toEqual([new StickyEffect("test", 1)]); expect(ship.sticky_effects).toEqual([new StickyEffect(new BaseEffect("test"), 1, false, true)]);
expect(battle.log.events).toEqual([ expect(battle.log.events).toEqual([
new EffectDurationChangedEvent(ship, new StickyEffect("test", 1), 2) new EffectDurationChangedEvent(ship, new StickyEffect(new BaseEffect("test"), 1, false, true), 2)
]); ]);
ship.startTurn(); ship.startTurn();
@ -147,7 +145,8 @@ module SpaceTac.Game.Specs {
expect(ship.sticky_effects).toEqual([]); expect(ship.sticky_effects).toEqual([]);
expect(battle.log.events).toEqual([ expect(battle.log.events).toEqual([
new EffectRemovedEvent(ship, new StickyEffect("test", 1)) new EffectDurationChangedEvent(ship, new StickyEffect(new BaseEffect("test"), 0, false, true), 1),
new EffectRemovedEvent(ship, new StickyEffect(new BaseEffect("test"), 0, false, true))
]); ]);
ship.startTurn(); ship.startTurn();

View file

@ -223,6 +223,7 @@ module SpaceTac.Game {
this.initializeActionPoints(); this.initializeActionPoints();
} }
// Method called at the start of this ship turn // Method called at the start of this ship turn
startTurn(): void { startTurn(): void {
if (this.playing) { if (this.playing) {
@ -235,9 +236,8 @@ module SpaceTac.Game {
this.updateAttributes(); this.updateAttributes();
// Apply sticky effects // Apply sticky effects
this.sticky_effects.forEach((effect: StickyEffect) => { this.sticky_effects.forEach(effect => effect.startTurn(this));
effect.singleApply(this, false); this.cleanStickyEffects();
});
} }
// Method called at the end of this ship turn // Method called at the end of this ship turn
@ -251,32 +251,32 @@ module SpaceTac.Game {
// Recover action points for next turn // Recover action points for next turn
this.recoverActionPoints(); this.recoverActionPoints();
// Decrement sticky effects duration // Apply sticky effects
let removed_effects: EffectRemovedEvent[] = []; this.sticky_effects.forEach(effect => effect.endTurn(this));
this.sticky_effects = this.sticky_effects.filter((effect: StickyEffect): boolean => { this.cleanStickyEffects();
if (effect.duration <= 1) {
removed_effects.push(new EffectRemovedEvent(this, effect));
return false;
} else {
return true;
}
});
this.sticky_effects.forEach(effect => {
effect.duration -= 1;
this.addBattleEvent(new EffectDurationChangedEvent(this, effect, effect.duration + 1));
});
removed_effects.forEach(effect => this.addBattleEvent(effect));
} }
// Add a sticky effect /**
// A copy of the effect will be used * Register a sticky effect
addStickyEffect(effect: StickyEffect, log: boolean = true): void { *
this.sticky_effects.push(Tools.copyObject(effect)); * Pay attention to pass a copy, not the original equipment effect, because it will be modified
*/
addStickyEffect(effect: StickyEffect, log = true): void {
this.sticky_effects.push(effect);
if (log) { if (log) {
this.addBattleEvent(new EffectAddedEvent(this, effect)); this.addBattleEvent(new EffectAddedEvent(this, effect));
} }
} }
/**
* Clean sticky effects that are no longer active
*/
cleanStickyEffects() {
let [active, ended] = Tools.binpartition(this.sticky_effects, effect => effect.duration > 0);
this.sticky_effects = active;
ended.forEach(effect => this.addBattleEvent(new EffectRemovedEvent(this, effect)));
}
// Move toward a location // Move toward a location
// This does not check or consume action points // This does not check or consume action points
moveTo(x: number, y: number, log: boolean = true): void { moveTo(x: number, y: number, log: boolean = true): void {

View file

@ -14,7 +14,7 @@ module SpaceTac.Game.Specs {
} }
describe("Tools", () => { describe("Tools", () => {
it("copies full javascript objects", () => { it("copies full javascript objects", function () {
var ini = new TestObj(); var ini = new TestObj();
var cop = Tools.copyObject(ini); var cop = Tools.copyObject(ini);
@ -25,10 +25,14 @@ module SpaceTac.Game.Specs {
expect(cop.get()).toEqual("test"); expect(cop.get()).toEqual("test");
}); });
it("merges objects", () => { it("merges objects", function () {
expect(Tools.merge({}, {})).toEqual({}); expect(Tools.merge({}, {})).toEqual({});
expect(Tools.merge({ "a": 1 }, { "b": 2 })).toEqual({ "a": 1, "b": 2 }); expect(Tools.merge({ "a": 1 }, { "b": 2 })).toEqual({ "a": 1, "b": 2 });
expect(Tools.merge({ "a": 1 }, { "a": 3, "b": 2 })).toEqual({ "a": 3, "b": 2 }); expect(Tools.merge({ "a": 1 }, { "a": 3, "b": 2 })).toEqual({ "a": 3, "b": 2 });
}); });
it("partitions arrays by a predicate", function () {
expect(Tools.binpartition([1, 2, 3, 4], i => i % 2 == 0)).toEqual([[2, 4], [1, 3]]);
});
}); });
} }

View file

@ -3,7 +3,7 @@ module SpaceTac.Game {
export class Tools { export class Tools {
// Copy an object (only a shallow copy of immediate properties) // Copy an object (only a shallow copy of immediate properties)
static copyObject<T> (object: T): T { static copyObject<T>(object: T): T {
var objectCopy = <T>Object.create(object.constructor.prototype); var objectCopy = <T>Object.create(object.constructor.prototype);
for (var key in object) { for (var key in object) {
@ -25,5 +25,13 @@ module SpaceTac.Game {
} }
return result; return result;
} }
// Partition a list by a predicate, returning the items that pass the predicate, then the ones that don't pass it
static binpartition<T>(array: T[], predicate: (T) => boolean): [T[], T[]] {
let pass = [];
let fail = [];
array.forEach(item => (predicate(item) ? pass : fail).push(item));
return [pass, fail];
}
} }
} }

View file

@ -1,27 +1,28 @@
/// <reference path="StickyEffect.ts"/> /// <reference path="BaseEffect.ts"/>
module SpaceTac.Game { module SpaceTac.Game {
// Hard limitation on attribute value // Hard limitation on attribute value
// For example, this could be used to slow a target by limiting its action points // For example, this could be used to slow a target by limiting its action points
export class AttributeLimitEffect extends StickyEffect { export class AttributeLimitEffect extends BaseEffect {
// Affected attribute // Affected attribute
attrcode: AttributeCode; attrcode: AttributeCode;
// Limit of the attribute value // Limit of the attribute value
value: number; value: number;
constructor(attrcode: AttributeCode, duration: number = 0, value: number = 0) { constructor(attrcode: AttributeCode, value: number = 0) {
super("attrlimit", duration); super("attrlimit");
this.attrcode = attrcode; this.attrcode = attrcode;
this.value = value; this.value = value;
} }
singleApply(ship: Ship, on_stick: boolean): void { applyOnShip(ship: Ship): boolean {
var current = ship.attributes.getValue(this.attrcode); var current = ship.attributes.getValue(this.attrcode);
if (current > this.value) { if (current > this.value) {
ship.setAttribute(ship.attributes.getRawAttr(this.attrcode), this.value); ship.setAttribute(ship.attributes.getRawAttr(this.attrcode), this.value);
} }
return true;
} }
getFullCode(): string { getFullCode(): string {

View file

@ -14,6 +14,17 @@ module SpaceTac.Game {
this.code = code; this.code = code;
} }
/**
* Get a copy, modified by template modifiers
*/
getModifiedCopy(modifiers: EffectTemplateModifier[], power: number): BaseEffect {
let result = Tools.copyObject(this);
modifiers.forEach(modifier => {
result[modifier.name] = modifier.range.getProportional(power);
});
return result;
}
// Apply ponctually the effect on a given ship // Apply ponctually the effect on a given ship
// Return true if the effect could be applied // Return true if the effect could be applied
applyOnShip(ship: Ship): boolean { applyOnShip(ship: Ship): boolean {
@ -25,6 +36,11 @@ module SpaceTac.Game {
return false; return false;
} }
// Get a full code, that can be used to identify this effect (for example: "attrlimit-aprecovery")
getFullCode(): string {
return this.code;
}
// Return a human readable description // Return a human readable description
getDescription(): string { getDescription(): string {
return "unknown effect"; return "unknown effect";

View file

@ -1,33 +1,85 @@
/// <reference path="BaseEffect.ts"/> /// <reference path="BaseEffect.ts"/>
module SpaceTac.Game { module SpaceTac.Game {
// Base class for actions that will stick to a target for a number of rounds /**
* Wrapper around another effect, to make it stick to a ship.
*
* The "effect" is to stick the wrapped effect to the ship, that will be applied in time.
*/
export class StickyEffect extends BaseEffect { export class StickyEffect extends BaseEffect {
// Wrapped effect
base: BaseEffect;
// Duration, in number of turns // Duration, in number of turns
duration: number; duration: number;
// Base constructor // Apply the effect on stick (doesn't count against duration)
constructor(code: string, duration: number = 0) { on_stick: boolean;
super(code);
// Apply the effect on turn start instead of end
on_turn_end: boolean;
// Base constructor
constructor(base: BaseEffect, duration = 0, on_stick = false, on_turn_end = false) {
super(base.code);
this.base = base;
this.duration = duration; this.duration = duration;
this.on_stick = on_stick;
this.on_turn_end = on_turn_end;
}
getModifiedCopy(modifiers: EffectTemplateModifier[], power: number): BaseEffect {
let [current, base] = Tools.binpartition(modifiers, modifier => modifier.name == "duration");
let result = <StickyEffect>super.getModifiedCopy(current, power);
result.base = result.base.getModifiedCopy(base, power);
return result;
} }
applyOnShip(ship: Ship): boolean { applyOnShip(ship: Ship): boolean {
ship.addStickyEffect(this); ship.addStickyEffect(new StickyEffect(this.base, this.duration, this.on_stick, this.on_turn_end));
this.singleApply(ship, true); if (this.on_stick) {
this.base.applyOnShip(ship);
}
return true; return true;
} }
// Method to implement to apply the effect ponctually private applyOnce(ship: Ship) {
// on_stick is true when this is called by applyOnShip, and false when called at turn start if (this.duration > 0) {
singleApply(ship: Ship, on_stick: boolean): void { this.base.applyOnShip(ship);
// Abstract this.duration--;
ship.addBattleEvent(new EffectDurationChangedEvent(ship, this, this.duration + 1));
}
}
/**
* Apply the effect at the beginning of the turn, for the ship this effect is sticked to.
*/
startTurn(ship: Ship) {
if (!this.on_turn_end) {
this.applyOnce(ship);
}
}
/**
* Apply the effect at the end of the turn, for the ship this effect is sticked to.
*/
endTurn(ship: Ship) {
if (this.on_turn_end) {
this.applyOnce(ship);
}
}
isBeneficial(): boolean {
return this.base.isBeneficial();
} }
// Get a full code, that can be used to identify this effect (for example: "attrlimit-aprecovery")
getFullCode(): string { getFullCode(): string {
return this.code; return this.base.getFullCode();
}
getDescription(): string {
return this.base.getDescription();
} }
} }
} }

View file

@ -17,19 +17,18 @@ module SpaceTac.Game.Specs {
expect(target.ap_current.current).toBe(4); expect(target.ap_current.current).toBe(4);
expect(target.sticky_effects).toEqual([ expect(target.sticky_effects).toEqual([
new AttributeLimitEffect(AttributeCode.AP, 1, 4) new StickyEffect(new AttributeLimitEffect(AttributeCode.AP, 4), 1, true, false)
]); ]);
// Attribute is limited for one turn, and prevents AP recovery // Attribute is limited for one turn, and prevents AP recovery
target.ap_current.set(6); target.ap_current.set(6);
target.recoverActionPoints();
target.startTurn(); target.startTurn();
expect(target.ap_current.current).toBe(4); expect(target.ap_current.current).toBe(4);
expect(target.sticky_effects).toEqual([ expect(target.sticky_effects).toEqual([]);
new AttributeLimitEffect(AttributeCode.AP, 1, 4)
]);
// Attribute vanishes before the end of turn, so AP recovery happens // Effect vanished, so AP recovery happens
target.endTurn(); target.endTurn();
expect(target.ap_current.current).toBe(6); expect(target.ap_current.current).toBe(6);

View file

@ -10,7 +10,7 @@ module SpaceTac.Game.Equipments {
this.ap_usage = new IntegerRange(4, 5); this.ap_usage = new IntegerRange(4, 5);
this.min_level = new IntegerRange(1, 3); this.min_level = new IntegerRange(1, 3);
this.addSticky(new AttributeLimitEffect(AttributeCode.AP), 4, 3, 1, 2); this.addSticky(new AttributeLimitEffect(AttributeCode.AP), 4, 3, 1, 2, true);
} }
} }
} }