From c4efd269d4fd76913956957e5a2b0a608b9d76b6 Mon Sep 17 00:00:00 2001 From: penev92 Date: Mon, 3 Nov 2014 00:14:53 +0200 Subject: [PATCH] Step three in implementing sandworms Removed/fixed comments Code style fixes Fixed AttackWander, addressed style nits Fix Travis crash --- OpenRA.Mods.D2k/AttackSwallow.cs | 18 ++-- OpenRA.Mods.D2k/OpenRA.Mods.D2k.csproj | 1 - OpenRA.Mods.D2k/Sandworm.cs | 41 +-------- OpenRA.Mods.D2k/SwallowActor.cs | 68 +++++---------- OpenRA.Mods.D2k/WormManager.cs | 115 +++++++++++++------------ OpenRA.Mods.D2k/WormSpawner.cs | 24 ------ OpenRA.Mods.RA/Attack/AttackBase.cs | 20 ++++- OpenRA.Mods.RA/Attack/AttackWander.cs | 27 +++++- OpenRA.Mods.RA/AutoTarget.cs | 12 +-- mods/d2k/bits/wormspawner.shp | Bin 0 -> 92 bytes mods/d2k/maps/death-depths.oramap | Bin 13154 -> 13205 bytes mods/d2k/maps/shellmap/map.yaml | 4 +- mods/d2k/rules/arrakis.yaml | 15 +--- mods/d2k/rules/misc.yaml | 6 ++ mods/d2k/rules/world.yaml | 1 - mods/d2k/sequences/misc.yaml | 5 ++ 16 files changed, 154 insertions(+), 203 deletions(-) delete mode 100644 OpenRA.Mods.D2k/WormSpawner.cs create mode 100644 mods/d2k/bits/wormspawner.shp diff --git a/OpenRA.Mods.D2k/AttackSwallow.cs b/OpenRA.Mods.D2k/AttackSwallow.cs index f0a2b6dcc7..1f9adea519 100644 --- a/OpenRA.Mods.D2k/AttackSwallow.cs +++ b/OpenRA.Mods.D2k/AttackSwallow.cs @@ -17,24 +17,28 @@ namespace OpenRA.Mods.D2k [Desc("Sandworms use this attack model.")] class AttackSwallowInfo : AttackFrontalInfo, Requires { + [Desc("The number of ticks it takes to return underground.")] + public int ReturnTime = 60; + [Desc("The number of ticks it takes to get in place under the target to attack.")] + public int AttackTime = 30; + public override object Create(ActorInitializer init) { return new AttackSwallow(init.self, this); } } + class AttackSwallow : AttackFrontal { - readonly Sandworm sandworm; + public readonly AttackSwallowInfo AttackSwallowInfo; public AttackSwallow(Actor self, AttackSwallowInfo attackSwallowInfo) : base(self, attackSwallowInfo) { - sandworm = self.Trait(); + AttackSwallowInfo = attackSwallowInfo; } public override void DoAttack(Actor self, Target target) { - // TODO: Worm should ignore Fremen as targets unless they are firing/being fired upon (even moving fremen do not attract worms) - - if (target.Type != TargetType.Actor || !CanAttack(self, target) || !sandworm.CanAttackAtLocation(self, target.Actor.Location)) - // this is so that the worm does not launch an attack against a target that has reached solid rock + // This is so that the worm does not launch an attack against a target that has reached solid rock + if (target.Type != TargetType.Actor || !CanAttack(self, target)) { self.CancelActivity(); return; @@ -48,7 +52,7 @@ namespace OpenRA.Mods.D2k return; self.CancelActivity(); - self.QueueActivity(new SwallowActor(self, target.Actor, a.Weapon)); + self.QueueActivity(new SwallowActor(self, target, a.Weapon)); } } } diff --git a/OpenRA.Mods.D2k/OpenRA.Mods.D2k.csproj b/OpenRA.Mods.D2k/OpenRA.Mods.D2k.csproj index ad6db3439f..a1ec8a43ba 100644 --- a/OpenRA.Mods.D2k/OpenRA.Mods.D2k.csproj +++ b/OpenRA.Mods.D2k/OpenRA.Mods.D2k.csproj @@ -100,7 +100,6 @@ - diff --git a/OpenRA.Mods.D2k/Sandworm.cs b/OpenRA.Mods.D2k/Sandworm.cs index 2bc5c08b38..33b022c28f 100644 --- a/OpenRA.Mods.D2k/Sandworm.cs +++ b/OpenRA.Mods.D2k/Sandworm.cs @@ -17,54 +17,15 @@ namespace OpenRA.Mods.D2k { class SandwormInfo : Requires, Requires, IOccupySpaceInfo { - readonly public int WanderMoveRadius = 20; readonly public string WormSignNotification = "WormSign"; public object Create(ActorInitializer init) { return new Sandworm(this); } } - class Sandworm : INotifyIdle + class Sandworm { - int ticksIdle; - int effectiveMoveRadius; - readonly int maxMoveRadius; - public Sandworm(SandwormInfo info) { - maxMoveRadius = info.WanderMoveRadius; - effectiveMoveRadius = info.WanderMoveRadius; - - // TODO: Someone familiar with how the sounds work should fix this: - // TODO: This should not be here. It should be same as "Enemy unit sighted". - //Sound.PlayNotification(self.Owner, "Speech", info.WormSignNotification, self.Owner.Country.Race); - } - - // TODO: This copies AttackWander and builds on top of it. AttackWander should be revised. - public void TickIdle(Actor self) - { - var globalOffset = new WVec(0, -1024 * effectiveMoveRadius, 0).Rotate(WRot.FromFacing(self.World.SharedRandom.Next(255))); - var offset = new CVec(globalOffset.X/1024, globalOffset.Y/1024); - var targetlocation = self.Location + offset; - - if (!self.World.Map.Bounds.Contains(targetlocation.X, targetlocation.Y)) - { - // If MoveRadius is too big there might not be a valid cell to order the attack to (if actor is on a small island and can't leave) - if (++ticksIdle % 10 == 0) // completely random number - { - effectiveMoveRadius--; - } - return; // We'll be back the next tick; better to sit idle for a few seconds than prolongue this tick indefinitely with a loop - } - - self.World.IssueOrder(new Order("AttackMove", self, false) { TargetLocation = targetlocation }); - - ticksIdle = 0; - effectiveMoveRadius = maxMoveRadius; - } - - public bool CanAttackAtLocation(Actor self, CPos targetLocation) - { - return self.Trait().MovementSpeedForCell(self, targetLocation) != 0; } } } diff --git a/OpenRA.Mods.D2k/SwallowActor.cs b/OpenRA.Mods.D2k/SwallowActor.cs index cb9c18ccec..345826b956 100644 --- a/OpenRA.Mods.D2k/SwallowActor.cs +++ b/OpenRA.Mods.D2k/SwallowActor.cs @@ -17,66 +17,53 @@ using OpenRA.Traits; namespace OpenRA.Mods.D2k { - public enum AttackState { Burrowed, EmergingAboveGround, ReturningUndergrown } + public enum AttackState { Burrowed, EmergingAboveGround, ReturningUnderground } class SwallowActor : Activity { - readonly Actor target; - readonly Mobile mobile; + readonly Target target; readonly Sandworm sandworm; readonly WeaponInfo weapon; + readonly AttackSwallow swallow; + readonly IPositionable positionable; int countdown; AttackState stance = AttackState.Burrowed; - // TODO: random numbers to make it look ok - [Desc("The number of ticks it takes to return underground.")] - const int ReturnTime = 60; - [Desc("The number of ticks it takes to get in place under the target to attack.")] - const int AttackTime = 30; - - public SwallowActor(Actor self, Actor target, WeaponInfo weapon) + public SwallowActor(Actor self, Target target, WeaponInfo weapon) { - if (!target.HasTrait()) - throw new InvalidOperationException("SwallowActor requires a target actor with the Mobile trait"); - this.target = target; this.weapon = weapon; - mobile = self.TraitOrDefault(); + positionable = self.TraitOrDefault(); sandworm = self.TraitOrDefault(); - countdown = AttackTime; + swallow = self.TraitOrDefault(); + countdown = swallow.AttackSwallowInfo.AttackTime; } bool WormAttack(Actor worm) { - var targetLocation = target.Location; + var targetLocation = target.Actor.Location; var lunch = worm.World.ActorMap.GetUnitsAt(targetLocation) - .Except(new[] { worm }) - .Where(t => weapon.IsValidAgainst(t, worm)); + .Where(t => !t.Equals(worm) && weapon.IsValidAgainst(t, worm)); if (!lunch.Any()) return false; stance = AttackState.EmergingAboveGround; - lunch.Do(t => t.World.AddFrameEndTask(_ => { t.World.Remove(t); t.Kill(t); })); // dispose of the evidence (we don't want husks) + lunch.Do(t => t.World.AddFrameEndTask(_ => { t.World.Remove(t); t.Kill(t); })); // Dispose of the evidence (we don't want husks) - mobile.SetPosition(worm, targetLocation); + positionable.SetPosition(worm, targetLocation); PlayAttackAnimation(worm); return true; } - public bool PlayAttackAnimation(Actor self) + void PlayAttackAnimation(Actor self) { var renderUnit = self.Trait(); renderUnit.PlayCustomAnim(self, "sand"); renderUnit.PlayCustomAnim(self, "mouth"); - - // TODO: Someone familiar with how the sounds work should fix this: - //Sound.PlayNotification(self.Owner, "Speech", "WormAttack", self.Owner.Country.Race); - - return true; } public override Activity Tick(Actor self) @@ -87,47 +74,36 @@ namespace OpenRA.Mods.D2k return this; } - if (stance == AttackState.ReturningUndergrown) // wait for the worm to get back underground + if (stance == AttackState.ReturningUnderground) // Wait for the worm to get back underground { - #region DisappearToMapEdge - - // More random numbers used for min and max bounds - var rand = self.World.SharedRandom.Next(200, 400); - if (rand % 2 == 0) // there is a 50-50 chance that the worm would just go away + if (self.World.SharedRandom.Next() % 2 == 0) // There is a 50-50 chance that the worm would just go away { self.CancelActivity(); - //self.World.WorldActor.QueueActivity(new DisappearToMapEdge(self, rand)); self.World.AddFrameEndTask(w => w.Remove(self)); var wormManager = self.World.WorldActor.TraitOrDefault(); if (wormManager != null) wormManager.DecreaseWorms(); } - #endregion - - // TODO: if the worm did not disappear, make the animation reappear here + // TODO: If the worm did not disappear, make the animation reappear here return NextActivity; } - if (stance == AttackState.Burrowed) // wait for the worm to get in position + if (stance == AttackState.Burrowed) // Wait for the worm to get in position { - // TODO: make the worm animation (currenty the lightning) disappear here + // TODO: Make the worm animation (currenty the lightning) disappear here - // this is so that the worm cancels an attack against a target that has reached solid rock - if (sandworm == null || !sandworm.CanAttackAtLocation(self, target.Location)) - { + // This is so that the worm cancels an attack against a target that has reached solid rock + if (sandworm == null || positionable == null || !positionable.CanEnterCell(target.Actor.Location, null, false)) return NextActivity; - } var success = WormAttack(self); if (!success) - { return NextActivity; - } - countdown = ReturnTime; - stance = AttackState.ReturningUndergrown; + countdown = swallow.AttackSwallowInfo.ReturnTime; + stance = AttackState.ReturningUnderground; } return this; diff --git a/OpenRA.Mods.D2k/WormManager.cs b/OpenRA.Mods.D2k/WormManager.cs index af36094fe7..7e04c93891 100644 --- a/OpenRA.Mods.D2k/WormManager.cs +++ b/OpenRA.Mods.D2k/WormManager.cs @@ -15,73 +15,76 @@ using OpenRA.Traits; namespace OpenRA.Mods.D2k { - [Desc("Controls the spawning of sandworms. Attach this to the world actor.")] - class WormManagerInfo : ITraitInfo - { - [Desc("Minimum number of worms")] - public readonly int Minimum = 1; + [Desc("Controls the spawning of sandworms. Attach this to the world actor.")] + class WormManagerInfo : ITraitInfo + { + [Desc("Minimum number of worms")] + public readonly int Minimum = 1; - [Desc("Maximum number of worms")] - public readonly int Maximum = 5; + [Desc("Maximum number of worms")] + public readonly int Maximum = 5; - [Desc("Average time (seconds) between crate spawn")] - public readonly int SpawnInterval = 180; + [Desc("Average time (seconds) between worm spawn")] + public readonly int SpawnInterval = 180; - public readonly string WormSignature = "sandworm"; - public readonly string WormOwnerPlayer = "Creeps"; + public readonly string WormSignature = "sandworm"; + public readonly string WormOwnerPlayer = "Creeps"; - public object Create (ActorInitializer init) { return new WormManager(this, init.self); } - } + public object Create (ActorInitializer init) { return new WormManager(this, init.self); } + } - class WormManager : ITick - { - int countdown; - int wormsPresent; - readonly WormManagerInfo info; - readonly Lazy spawnPoints; + class WormManager : ITick + { + int countdown; + int wormsPresent; + readonly WormManagerInfo info; + readonly Lazy spawnPoints; - public WormManager(WormManagerInfo info, Actor self) - { - this.info = info; - spawnPoints = Exts.Lazy(() => self.World.ActorsWithTrait().Select(x => x.Actor).ToArray()); - } + public WormManager(WormManagerInfo info, Actor self) + { + this.info = info; + spawnPoints = Exts.Lazy(() => self.World.ActorsWithTrait().Select(x => x.Actor).ToArray()); + } - public void Tick(Actor self) - { - // TODO: Add a lobby option to disable worms just like crates + public void Tick(Actor self) + { + // TODO: Add a lobby option to disable worms just like crates - if (--countdown > 0) - return; + // TODO: It would be even better to stop + if (!spawnPoints.Value.Any()) + return; - countdown = info.SpawnInterval * 25; - if (wormsPresent < info.Maximum) - SpawnWorm(self); - } + if (--countdown > 0) + return; - private void SpawnWorm (Actor self) - { - var spawnLocation = GetRandomSpawnPosition(self); - self.World.AddFrameEndTask(w => - w.CreateActor(info.WormSignature, new TypeDictionary - { - new OwnerInit(w.Players.First(x => x.PlayerName == info.WormOwnerPlayer)), - new LocationInit(spawnLocation) - })); - wormsPresent++; - } + countdown = info.SpawnInterval * 25; + if (wormsPresent < info.Maximum) + SpawnWorm(self); + } - private CPos GetRandomSpawnPosition(Actor self) - { - // TODO: This is here only for testing, while the maps don't have valid spawn points - if (!spawnPoints.Value.Any()) - return self.World.Map.ChooseRandomEdgeCell(self.World.SharedRandom); + void SpawnWorm (Actor self) + { + var spawnLocation = GetRandomSpawnPosition(self); + self.World.AddFrameEndTask(w => w.CreateActor(info.WormSignature, new TypeDictionary + { + new OwnerInit(w.Players.First(x => x.PlayerName == info.WormOwnerPlayer)), + new LocationInit(spawnLocation) + })); + wormsPresent++; + } - return spawnPoints.Value[self.World.SharedRandom.Next(0, spawnPoints.Value.Count() - 1)].Location; - } + CPos GetRandomSpawnPosition(Actor self) + { + return spawnPoints.Value.Random(self.World.SharedRandom).Location; + } - public void DecreaseWorms() - { - wormsPresent--; - } - } + public void DecreaseWorms() + { + wormsPresent--; + } + } + + [Desc("An actor with this trait indicates a valid spawn point for sandworms.")] + class WormSpawnerInfo : TraitInfo { } + class WormSpawner { } } diff --git a/OpenRA.Mods.D2k/WormSpawner.cs b/OpenRA.Mods.D2k/WormSpawner.cs deleted file mode 100644 index 99730e79b4..0000000000 --- a/OpenRA.Mods.D2k/WormSpawner.cs +++ /dev/null @@ -1,24 +0,0 @@ -#region Copyright & License Information -/* - * Copyright 2007-2014 The OpenRA Developers (see AUTHORS) - * This file is part of OpenRA, which is free software. It is made - * available to you under the terms of the GNU General Public License - * as published by the Free Software Foundation. For more information, - * see COPYING. - */ -#endregion - -using OpenRA.Traits; - -namespace OpenRA.Mods.D2k -{ - [Desc("An actor with this trait indicates a valid spawn point for sandworms.")] - class WormSpawnerInfo : ITraitInfo - { - public object Create(ActorInitializer init) { return new WormSpawner(); } - } - - class WormSpawner - { - } -} diff --git a/OpenRA.Mods.RA/Attack/AttackBase.cs b/OpenRA.Mods.RA/Attack/AttackBase.cs index 1e5971d7c9..4a3f54a24b 100644 --- a/OpenRA.Mods.RA/Attack/AttackBase.cs +++ b/OpenRA.Mods.RA/Attack/AttackBase.cs @@ -14,6 +14,7 @@ using System.Drawing; using System.Linq; using OpenRA.GameRules; using OpenRA.Mods.RA.Buildings; +using OpenRA.Mods.RA.Move; using OpenRA.Traits; namespace OpenRA.Mods.RA @@ -25,7 +26,8 @@ namespace OpenRA.Mods.RA public readonly string Cursor = "attack"; public readonly string OutsideRangeCursor = "attackoutsiderange"; - [Desc("Does the attack type requires the attacker to enter the target's cell?")] + + [Desc("Does the attack type require the attacker to enter the target's cell?")] public readonly bool AttackRequiresEnteringCell = false; public abstract object Create(ActorInitializer init); @@ -61,6 +63,9 @@ namespace OpenRA.Mods.RA if (!self.IsInWorld) return false; + if (!HasAnyValidWeapons(target)) + return false; + // Building is under construction or is being sold if (building.Value != null && !building.Value.BuildComplete) return false; @@ -137,7 +142,18 @@ namespace OpenRA.Mods.RA public abstract Activity GetAttackActivity(Actor self, Target newTarget, bool allowMove); - public bool HasAnyValidWeapons(Target t) { return Armaments.Any(a => a.Weapon.IsValidAgainst(t, self.World, self)); } + public bool HasAnyValidWeapons(Target t) + { + if (Info.AttackRequiresEnteringCell) + { + var positionable = self.TraitOrDefault(); + if (positionable == null || !positionable.CanEnterCell(t.Actor.Location, null, false)) + return false; + } + + return Armaments.Any(a => a.Weapon.IsValidAgainst(t, self.World, self)); + } + public WRange GetMaximumRange() { return Armaments.Select(a => a.Weapon.Range).Append(WRange.Zero).Max(); diff --git a/OpenRA.Mods.RA/Attack/AttackWander.cs b/OpenRA.Mods.RA/Attack/AttackWander.cs index e48528e1cf..06e194993e 100644 --- a/OpenRA.Mods.RA/Attack/AttackWander.cs +++ b/OpenRA.Mods.RA/Attack/AttackWander.cs @@ -17,14 +17,20 @@ namespace OpenRA.Mods.RA "This conflicts with player orders and should only be added to animal creeps.")] class AttackWanderInfo : ITraitInfo { - public readonly int MoveRadius = 4; + readonly public int WanderMoveRadius = 10; + + [Desc("Number of ticks to wait until decreasing the effective move radius.")] + public readonly int MoveReductionRadiusScale = 5; public object Create(ActorInitializer init) { return new AttackWander(init.self, this); } } class AttackWander : INotifyIdle { + int ticksIdle; + int effectiveMoveRadius; readonly AttackWanderInfo Info; + public AttackWander(Actor self, AttackWanderInfo info) { Info = info; @@ -32,9 +38,22 @@ namespace OpenRA.Mods.RA public void TickIdle(Actor self) { - var target = self.CenterPosition + new WVec(0, -1024*Info.MoveRadius, 0).Rotate(WRot.FromFacing(self.World.SharedRandom.Next(255))); - // TODO: This needs to be looked into again. The bigger MoveRadius is, the bigger chance that the selected coordinates will be invalid. - self.Trait().ResolveOrder(self, new Order("AttackMove", self, false) { TargetLocation = self.World.Map.CellContaining(target) }); + var target = self.CenterPosition + new WVec(0, -1024 * effectiveMoveRadius, 0).Rotate(WRot.FromFacing(self.World.SharedRandom.Next(255))); + var targetCell = self.World.Map.CellContaining(target); + + if (!self.World.Map.Contains(targetCell)) + { + // If MoveRadius is too big there might not be a valid cell to order the attack to (if actor is on a small island and can't leave) + if (++ticksIdle % Info.MoveReductionRadiusScale == 0) + effectiveMoveRadius--; + + return; // We'll be back the next tick; better to sit idle for a few seconds than prolongue this tick indefinitely with a loop + } + + self.Trait().ResolveOrder(self, new Order("AttackMove", self, false) { TargetLocation = targetCell }); + + ticksIdle = 0; + effectiveMoveRadius = Info.WanderMoveRadius; } } } diff --git a/OpenRA.Mods.RA/AutoTarget.cs b/OpenRA.Mods.RA/AutoTarget.cs index 151a02a9c7..4b887faec2 100644 --- a/OpenRA.Mods.RA/AutoTarget.cs +++ b/OpenRA.Mods.RA/AutoTarget.cs @@ -10,7 +10,6 @@ using System.Drawing; using System.Linq; -using OpenRA.Mods.RA.Move; using OpenRA.Traits; namespace OpenRA.Mods.RA @@ -164,15 +163,12 @@ namespace OpenRA.Mods.RA nextScanTime = self.World.SharedRandom.Next(info.MinimumScanTimeInterval, info.MaximumScanTimeInterval); var inRange = self.World.FindActorsInCircle(self.CenterPosition, range); - var mobile = self.TraitOrDefault(); return inRange .Where(a => - a.AppearsHostileTo(self) && - !a.HasTrait() && - attack.HasAnyValidWeapons(Target.FromActor(a)) && - self.Owner.Shroud.IsTargetable(a) && - (!attack.Info.AttackRequiresEnteringCell || (mobile != null && mobile.MovementSpeedForCell(self, a.Location) != 0)) - ) + a.AppearsHostileTo(self) && + !a.HasTrait() && + attack.HasAnyValidWeapons(Target.FromActor(a)) && + self.Owner.Shroud.IsTargetable(a)) .ClosestTo(self); } } diff --git a/mods/d2k/bits/wormspawner.shp b/mods/d2k/bits/wormspawner.shp new file mode 100644 index 0000000000000000000000000000000000000000..efefdfe49617c81fd497ab06fd8a3f80062265e4 GIT binary patch literal 92 zcmZQ%009Xg1OYV$h6WHF10s;Xzf%m%jsG23F6?ADd5+;9C&T|MXBf^tWVnBj;oe$? d|6UB@6aWAK2mny8VzCW1Fn>_5VnwmiR_L|@003eO000O8003=a za4vaaZEUTUUvt|a48Whqr|<`$cLt2H{j{uI?)InY&dJ(m8mo(MwsFB;*M0h7yIH!e z;`Xq+HbI1+BqSl&LIe9j-!@Q0J39g)m%3jY+v=f+QbJ0&A8QNl(o{uM@vnsJ>S0=Z z0e^Zp_|JYgI6^MK?QPu;wdsmTd=rte(R#fiWHk;4V~gnAw6}E!=bMs7XYawCO^O&mD?VmX!LCB{@KZ5n6SIek`JAbgy?cwzX#=+{w|MF_1TaQ0Yc=_s~b2r8| z)l*>n3e!#7(1Y%VUz4TYdrS`&>I&T77B0tTsN<7?MfT#f@LAlCno8YOruE|64%!<0 z-p?&Sk7;IqXlDOI^D=+Zd|dZ$5jg2z_DYYQk=1@Ma~kGBTtsc}di~IO=A+r`rGI*o zgi5}|AMCS)%nW;(VVqMbFeoh|*VlWvZOpv?6DM;jGYnHGmNF`149ifA#ga;iae07K zG*M_sVjL8TW;qIS6ci%F3EW^t6`s2!#0i{}xSxtLG=D4&1||UFnP*Z^k)R-+dnIX- zq8y%k#R8{+Ga9)Bc`S4{5xL@S2l6_fMO{_u*)QktN7;CSZqB#?NH zS6Z4=hVKtA7!}hDw};nDo(4`J$Lodr?uq7|;d$qApn-}&i;M&P9;I2CC36*ACdyrbwfAwu0Lp#o2S5k2_#*?b9t{rx&)G-k(lh;#mF zh2LjMz@G50GK`Y}d%oImP^bVYWuQNaEI_jS#o&-_J62y{dM1`*Gk@6aWAK2mokWQ?U&-Fn?%TQ$_4vfu@53000dN000O8003=a za4vaaZEUSp+j5&Q5Pgomf)7w;kOT;Ls+-g^y~OEYH+fcIQ!@obB+)ee`U(@*nKrBG zLuYKPUF+=atQKvdgW9RP4$5d}CLrWWjcaXs<;o}{q=JX3HDIrFQ$`K{M#!#p^X5xX zZh!BequX0TF2UBO9bK!3G7{fKq)oJ5uLxO9Zm&%lUFiO<9pGY9(dhgG*z?&?2Nt}W zRaLD%zTX~PZ`+^VIFlLmUqasMX=tohc-eoFn9kZS7FKq}0_AuoPv<=w5JzPzlYvYrHMfT#n@kQN_mMS$gy7%I{0s0nv?iU7N z#4NKvEVKV%dC{LNANT!R1y1&tT&$RF8wleA2aAx+h6!%72&o zgLy&70mEL$FwSWzFi=)Tc5G|7>+~W26D}=inqx4D!ZJqF7=z^~jKwKUQw&!GaFQkx zElG?6g`|0b0yzp4B7_sT!<)SxlqMO9 z!?~9%a2hyHy-aAvP#i;XxyPXgiGSlAQ}{U1j^TL66g-q4-Z6Pb6O;#zGhZYD;yK=F zSwR_|AKoxBraA5p@0TJAoIsBE3-{9#<(=WY^Efa-MPS6Fp#~MWLzc+EFcds^1SK9S z-fAMH5-p9uTTO5((CI7Ez>x8QqOS=NnobE7uaHl+{nUJg`9@Ps-QjoZuQO-!D|=qw z=pWF%RNR6ZmkZF1ZWq-?yY{|a9L@61sKIte@fT1_0Rl4s6aWAK2mokWQ