From 875b71828d41f0b676017aa3f0f86ee3f6f7982a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C3=ABl=20Lemaire?= Date: Tue, 24 Jan 2017 01:14:04 +0100 Subject: [PATCH] Rewrite of sticky effects system, to allow any other effect to be sticky --- TODO | 1 - src/game/EffectTemplate.ts | 6 +- src/game/Equipment.spec.ts | 2 +- src/game/LootTemplate.ts | 6 +- src/game/Ship.spec.ts | 15 ++--- src/game/Ship.ts | 44 ++++++------- src/game/Tools.spec.ts | 8 ++- src/game/Tools.ts | 10 ++- src/game/effects/AttributeLimitEffect.ts | 11 ++-- src/game/effects/BaseEffect.ts | 16 +++++ src/game/effects/StickyEffect.ts | 76 ++++++++++++++++++---- src/game/equipments/EnergyDepleter.spec.ts | 9 ++- src/game/equipments/EnergyDepleter.ts | 2 +- 13 files changed, 140 insertions(+), 66 deletions(-) diff --git a/TODO b/TODO index 60314ab..35e6e8c 100644 --- a/TODO +++ b/TODO @@ -1,6 +1,5 @@ * Use succession's tools and serializer * 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 equipment info (or summary) in ship tooltip * Handle effects overflowing ship tooltip when too numerous diff --git a/src/game/EffectTemplate.ts b/src/game/EffectTemplate.ts index 85a6b2d..ab59d3d 100644 --- a/src/game/EffectTemplate.ts +++ b/src/game/EffectTemplate.ts @@ -35,11 +35,7 @@ module SpaceTac.Game { // Generate an effect with a given power generateFixed(power: number): BaseEffect { - var effect = Tools.copyObject(this.effect); - this.modifiers.forEach((modifier: EffectTemplateModifier) => { - effect[modifier.name] = modifier.range.getProportional(power); - }); - return effect; + return this.effect.getModifiedCopy(this.modifiers, power); } } } diff --git a/src/game/Equipment.spec.ts b/src/game/Equipment.spec.ts index ba41eee..a1cd239 100644 --- a/src/game/Equipment.spec.ts +++ b/src/game/Equipment.spec.ts @@ -44,7 +44,7 @@ module SpaceTac.Game.Specs { expect(equipment.getActionDescription()).toEqual("- 50 damage on all ships in 20km of impact"); 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"); }); }); diff --git a/src/game/LootTemplate.ts b/src/game/LootTemplate.ts index 4716185..4945a44 100644 --- a/src/game/LootTemplate.ts +++ b/src/game/LootTemplate.ts @@ -149,9 +149,9 @@ module SpaceTac.Game { } // Convenience function to add a sticking effect on target - addSticky(effect: StickyEffect, min_value: number, max_value: number = null, - min_duration: number = 1, max_duration: number = null): void { - var template = new EffectTemplate(effect); + addSticky(effect: BaseEffect, min_value: number, max_value: number = null, + min_duration: number = 1, max_duration: number = null, on_stick = false, on_turn_start = false): void { + var template = new EffectTemplate(new StickyEffect(effect, 0, on_stick, on_turn_start)); template.addModifier("value", new IntegerRange(min_value, max_value)); template.addModifier("duration", new IntegerRange(min_duration, max_duration)); this.target_effects.push(template); diff --git a/src/game/Ship.spec.ts b/src/game/Ship.spec.ts index 6543f97..d1ca4d7 100644 --- a/src/game/Ship.spec.ts +++ b/src/game/Ship.spec.ts @@ -123,22 +123,20 @@ module SpaceTac.Game.Specs { var ship = new Ship(); 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([effect]); + expect(ship.sticky_effects).toEqual([new StickyEffect(new BaseEffect("test"), 2, false, true)]); expect(battle.log.events).toEqual([ - new EffectAddedEvent(ship, effect) + new EffectAddedEvent(ship, new StickyEffect(new BaseEffect("test"), 2, false, true)) ]); ship.startTurn(); battle.log.clear(); 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([ - new EffectDurationChangedEvent(ship, new StickyEffect("test", 1), 2) + new EffectDurationChangedEvent(ship, new StickyEffect(new BaseEffect("test"), 1, false, true), 2) ]); ship.startTurn(); @@ -147,7 +145,8 @@ module SpaceTac.Game.Specs { expect(ship.sticky_effects).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(); diff --git a/src/game/Ship.ts b/src/game/Ship.ts index 7d23156..1f8cd23 100644 --- a/src/game/Ship.ts +++ b/src/game/Ship.ts @@ -223,6 +223,7 @@ module SpaceTac.Game { this.initializeActionPoints(); } + // Method called at the start of this ship turn startTurn(): void { if (this.playing) { @@ -235,9 +236,8 @@ module SpaceTac.Game { this.updateAttributes(); // Apply sticky effects - this.sticky_effects.forEach((effect: StickyEffect) => { - effect.singleApply(this, false); - }); + this.sticky_effects.forEach(effect => effect.startTurn(this)); + this.cleanStickyEffects(); } // Method called at the end of this ship turn @@ -251,32 +251,32 @@ module SpaceTac.Game { // Recover action points for next turn this.recoverActionPoints(); - // Decrement sticky effects duration - let removed_effects: EffectRemovedEvent[] = []; - this.sticky_effects = this.sticky_effects.filter((effect: StickyEffect): boolean => { - 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)); + // Apply sticky effects + this.sticky_effects.forEach(effect => effect.endTurn(this)); + this.cleanStickyEffects(); } - // Add a sticky effect - // A copy of the effect will be used - addStickyEffect(effect: StickyEffect, log: boolean = true): void { - this.sticky_effects.push(Tools.copyObject(effect)); + /** + * Register a sticky 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) { 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 // This does not check or consume action points moveTo(x: number, y: number, log: boolean = true): void { diff --git a/src/game/Tools.spec.ts b/src/game/Tools.spec.ts index 0978f6b..e1915cd 100644 --- a/src/game/Tools.spec.ts +++ b/src/game/Tools.spec.ts @@ -14,7 +14,7 @@ module SpaceTac.Game.Specs { } describe("Tools", () => { - it("copies full javascript objects", () => { + it("copies full javascript objects", function () { var ini = new TestObj(); var cop = Tools.copyObject(ini); @@ -25,10 +25,14 @@ module SpaceTac.Game.Specs { expect(cop.get()).toEqual("test"); }); - it("merges objects", () => { + it("merges objects", function () { expect(Tools.merge({}, {})).toEqual({}); 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 }); }); + + it("partitions arrays by a predicate", function () { + expect(Tools.binpartition([1, 2, 3, 4], i => i % 2 == 0)).toEqual([[2, 4], [1, 3]]); + }); }); } diff --git a/src/game/Tools.ts b/src/game/Tools.ts index 27bf73c..d374f5c 100644 --- a/src/game/Tools.ts +++ b/src/game/Tools.ts @@ -3,7 +3,7 @@ module SpaceTac.Game { export class Tools { // Copy an object (only a shallow copy of immediate properties) - static copyObject (object: T): T { + static copyObject(object: T): T { var objectCopy = Object.create(object.constructor.prototype); for (var key in object) { @@ -25,5 +25,13 @@ module SpaceTac.Game { } 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(array: T[], predicate: (T) => boolean): [T[], T[]] { + let pass = []; + let fail = []; + array.forEach(item => (predicate(item) ? pass : fail).push(item)); + return [pass, fail]; + } } } diff --git a/src/game/effects/AttributeLimitEffect.ts b/src/game/effects/AttributeLimitEffect.ts index acad94e..04f48ea 100644 --- a/src/game/effects/AttributeLimitEffect.ts +++ b/src/game/effects/AttributeLimitEffect.ts @@ -1,27 +1,28 @@ -/// +/// module SpaceTac.Game { // Hard limitation on attribute value // 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 attrcode: AttributeCode; // Limit of the attribute value value: number; - constructor(attrcode: AttributeCode, duration: number = 0, value: number = 0) { - super("attrlimit", duration); + constructor(attrcode: AttributeCode, value: number = 0) { + super("attrlimit"); this.attrcode = attrcode; this.value = value; } - singleApply(ship: Ship, on_stick: boolean): void { + applyOnShip(ship: Ship): boolean { var current = ship.attributes.getValue(this.attrcode); if (current > this.value) { ship.setAttribute(ship.attributes.getRawAttr(this.attrcode), this.value); } + return true; } getFullCode(): string { diff --git a/src/game/effects/BaseEffect.ts b/src/game/effects/BaseEffect.ts index 91085b4..ce908d9 100644 --- a/src/game/effects/BaseEffect.ts +++ b/src/game/effects/BaseEffect.ts @@ -14,6 +14,17 @@ module SpaceTac.Game { 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 // Return true if the effect could be applied applyOnShip(ship: Ship): boolean { @@ -25,6 +36,11 @@ module SpaceTac.Game { 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 getDescription(): string { return "unknown effect"; diff --git a/src/game/effects/StickyEffect.ts b/src/game/effects/StickyEffect.ts index 68b4daf..c8f5378 100644 --- a/src/game/effects/StickyEffect.ts +++ b/src/game/effects/StickyEffect.ts @@ -1,33 +1,85 @@ /// 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 { + // Wrapped effect + base: BaseEffect; + // Duration, in number of turns duration: number; - // Base constructor - constructor(code: string, duration: number = 0) { - super(code); + // Apply the effect on stick (doesn't count against duration) + on_stick: boolean; + // 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.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 = super.getModifiedCopy(current, power); + result.base = result.base.getModifiedCopy(base, power); + return result; } applyOnShip(ship: Ship): boolean { - ship.addStickyEffect(this); - this.singleApply(ship, true); + ship.addStickyEffect(new StickyEffect(this.base, this.duration, this.on_stick, this.on_turn_end)); + if (this.on_stick) { + this.base.applyOnShip(ship); + } return true; } - // Method to implement to apply the effect ponctually - // on_stick is true when this is called by applyOnShip, and false when called at turn start - singleApply(ship: Ship, on_stick: boolean): void { - // Abstract + private applyOnce(ship: Ship) { + if (this.duration > 0) { + this.base.applyOnShip(ship); + 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 { - return this.code; + return this.base.getFullCode(); + } + + getDescription(): string { + return this.base.getDescription(); } } } diff --git a/src/game/equipments/EnergyDepleter.spec.ts b/src/game/equipments/EnergyDepleter.spec.ts index 7238d81..6a5414e 100644 --- a/src/game/equipments/EnergyDepleter.spec.ts +++ b/src/game/equipments/EnergyDepleter.spec.ts @@ -17,19 +17,18 @@ module SpaceTac.Game.Specs { expect(target.ap_current.current).toBe(4); 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 target.ap_current.set(6); + target.recoverActionPoints(); target.startTurn(); expect(target.ap_current.current).toBe(4); - expect(target.sticky_effects).toEqual([ - new AttributeLimitEffect(AttributeCode.AP, 1, 4) - ]); + expect(target.sticky_effects).toEqual([]); - // Attribute vanishes before the end of turn, so AP recovery happens + // Effect vanished, so AP recovery happens target.endTurn(); expect(target.ap_current.current).toBe(6); diff --git a/src/game/equipments/EnergyDepleter.ts b/src/game/equipments/EnergyDepleter.ts index 0f0996f..a3e2b60 100644 --- a/src/game/equipments/EnergyDepleter.ts +++ b/src/game/equipments/EnergyDepleter.ts @@ -10,7 +10,7 @@ module SpaceTac.Game.Equipments { this.ap_usage = new IntegerRange(4, 5); 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); } } }