SHARE
TWEET

Lurker Behavior

a guest Aug 31st, 2013 50 Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
  1. using System.Collections.Generic;
  2. using ObeliskImplementation.CommonValues;
  3. using ObeliskImplementation.Effects;
  4. using ObeliskImplementation.GameConfiguration;
  5. using ObeliskImplementation.Misc;
  6. using ObeliskImplementation.Players;
  7. using ObeliskImplementation.ScreenEffects;
  8. using ObeliskImplementation.Textures;
  9. using ObeliskImplementation.Triggers;
  10. using ObeliskInterfaces.Args;
  11. using ObeliskInterfaces.Behaviors;
  12. using ObeliskInterfaces.Interfaces;
  13. using ObeliskInterfaces.Scripts;
  14. using ObeliskInterfaces.Types.Controllers;
  15. using ObeliskInterfaces.Utility;
  16. using OpenTK;
  17.  
  18. namespace ObeliskImplementation.Enemies
  19. {
  20.     public class Lurker : BehaviorTemplate
  21.     {
  22.         private const float STRIKE_TRIGGER_DISTANCE = 92;  //When the player gets this close, the lurker awakens (or attacks)
  23.         private const float EMERGING_TIME = .6f;           //Time spent emerging from the ground
  24.         private const float RISING_TIME = .5f;           //Time spent emerging from the ground
  25.         private const float LURKER_SEGMENT_SIZE = 64;      //Size of the head/segments
  26.         private const float ROAM_RADIUS = 64;              //Radius when circling around the hole
  27.         private const float HIT_RADIUS = 64;               //Recoil distance (from center) when hit
  28.         private const float BACK_STRETCH = 128;            //Recoil distance (from center) when preparing for an attack
  29.         private const float STRIKE_DISTANCE = 92;          //Distance moved (from center) when striking
  30.         private const float ANGLE_ROAM = 32;               //Random 'sway' distance
  31.         private const int NUMBER_OF_SEGMENTS = 6;                        //Body segments
  32.        
  33.         private const float STRIKE_PREP_TIME = .2f;        //Time spent moving back into striking position
  34.         private const float STRIKE_DELAY = .2f;            //Time spent sitting in striking position
  35.         private const float STRIKE_TIME = .25f;            //Time spent attacking
  36.         private const float STUCK_DURATION = 1f;            //Time spent attacking
  37.  
  38.         private const float FRAME_MOUTH_CLOSED = 0f;
  39.         private const float FRAME_MOUTH_DEFAULT = .74f;
  40.         private const float FRAME_MOUTH_CLENCH = .99f;
  41.        
  42.  
  43.         private float _health = 2;            //Amount of health remaining
  44.         private IEntityInterface _head;
  45.        
  46.         //Instead of setting the head position directly, we set this variable and always perform an interpolated motion towards it
  47.         private Vector2 _headPosition;
  48.  
  49.         private List<IEntityInterface> _segments = new List<IEntityInterface>();
  50.         private IEntityInterface _crack;
  51.         private float _currentRoamAngle;
  52.  
  53.         public override EditorConfiguration EditorConfig
  54.         {
  55.             get
  56.             {
  57.                 //Provide a key and texture name for the level editor
  58.                 return new EditorConfiguration("Lurker", TextureNames.LurkerHeadDown);
  59.             }
  60.         }
  61.  
  62.         public override Behavior Create()
  63.         {
  64.             Behavior b = new Behavior();
  65.             b.Update(new UpdateBehavior(Update));
  66.            
  67.             //Hidden: Sitting underground
  68.             var hidden = b.AddState("hidden");
  69.             hidden.Update(new UpdateBehavior(HiddenUpdate, .25f));
  70.  
  71.             //Rumbling: Preparing to emerge
  72.             var rumbling = b.AddState("rumbling");
  73.             rumbling.Init(new InitBehavior(RumblingInit));
  74.  
  75.             //Rising: Bursting out of the ground
  76.             var rising = b.AddState("rising");
  77.             rising.Init(new InitBehavior(RisingInit));
  78.  
  79.             //Roaming: Avoiding the player, swaying back and forth, and looking for an attack target
  80.             var roaming = b.AddState("roaming");
  81.             roaming.Update(new UpdateBehavior(RoamingInit));
  82.             roaming.Update(new UpdateBehavior(RoamingUpdate));
  83.  
  84.             //Preparing to attack
  85.             var prepping = b.AddState("prepping");
  86.             prepping.Init(new InitBehavior(PreppingInit));
  87.  
  88.             //Attacking the player
  89.             var striking = b.AddState("striking");
  90.             striking.Init(new InitBehavior(StrikingInit));
  91.  
  92.             //Stuck in the ground after an unsuccessful attack
  93.             var stuck = b.AddState("stuck");
  94.             stuck.Init(new InitBehavior(StuckInit));
  95.             stuck.StateLeave(new StateLeaveBehavior(StuckStateLeave));
  96.            
  97.             //Recoiling or dying
  98.             var hurt = b.AddState("hurt");
  99.             hurt.Init(new InitBehavior<DamageEnemy>(HurtInit));
  100.  
  101.             b.InitialState = "hidden";
  102.            
  103.             return b;
  104.  
  105.         }
  106.  
  107.  
  108.         private void HiddenUpdate(UpdateArgs obj)
  109.         {
  110.             //Search for a player that's nearby and switch to 'rumbling' if found
  111.             var player = PlayerFinder.Find(Slab);
  112.             if (player != null && ((player.WorldPosition - Entity.WorldPosition).LengthSquared < STRIKE_TRIGGER_DISTANCE * STRIKE_TRIGGER_DISTANCE))
  113.             {
  114.                 Behavior.SetState("rumbling");
  115.             }
  116.         }
  117.  
  118.         private void RumblingInit(InitArgs obj)
  119.         {
  120.             //Play a noise, shake the screen, and create the 'crack' entity
  121.             Audio.PlaySound(SoundNames.Rumble);
  122.             ScreenShaker.ShakeConstantly(Section, 3, EMERGING_TIME);
  123.             _crack = Slab.CreateEntity();
  124.             _crack.TextureName = TextureNames.LurkerCrack;
  125.             _crack.Layer = Layers.FootLayer;
  126.             _crack.Size = new Vector2(48, 48);
  127.  
  128.             //The crack entity has 3 frames. Cycle through them and switch to the 'rising' state
  129.             Behavior.Control(
  130.                 new Sequence(
  131.                     new Range(ControllerMode.Normal, EMERGING_TIME, new FloatInterp(FloatProp.Frame, 0, 1, CurveType.Linear)),
  132.                     new Execute(() => Behavior.SetState("rising"))),
  133.                     _crack);
  134.             Entity.AppendChild(_crack);
  135.         }
  136.  
  137.         private void RisingInit(InitArgs obj)
  138.         {
  139.             //Turn the crack into a hole, and make it so collision with it damages the player.
  140.             //Its model is a half-sized square, so it's fairly forgiving.
  141.             _crack.TextureName = TextureNames.LurkerHole;
  142.             _crack.ModelType = Models.HalfSolid;
  143.             _crack.AttachCollide(arg => arg.Other.Trigger(new DamagePlayer(Entity.WorldPosition, 1)), Player.CollisionCategory);
  144.  
  145.             //Make shards
  146.             for (int i = 0; i < 30; i++)
  147.             {
  148.                 var shard = Slab.CreateEntity();
  149.                 shard.TextureName = TextureNames.LurkerFloorShard;
  150.                 shard.Rotation = OMath.RandomAngle();
  151.                 shard.Size = OMath.RandomSize(16, 32);
  152.                 shard.Layer = Layers.OverActionLayer;
  153.                 shard.Position = Entity.WorldPosition;
  154.                
  155.                 //Apply spin and motion
  156.                 shard.Control(new Rotation(OMath.RandomNumber(-OMath.PI, OMath.PI)));
  157.                 shard.Control(new Motion(OMath.RandomDirection(OMath.RandomNumber(60, 120))));
  158.  
  159.                 var shardDuration = OMath.RandomNumber(.5f, 1f);
  160.                 //Fade out halfway through the flight
  161.                 shard.Control(
  162.                     new Delay(shardDuration / 2f),
  163.                     new Range(ControllerMode.Normal, shardDuration / 2f, new FloatInterp(FloatProp.Opacity, 0, CurveType.Linear)));
  164.                
  165.                 //Fall down during the flight
  166.                 shard.Control(
  167.                     new Sequence(
  168.                         new Range(ControllerMode.ZigZag, shardDuration, new FloatInterp(FloatProp.OffsetY, 30, CurveType.EaseIn)),
  169.                         new Execute(() => shard.Destroy())));
  170.             }
  171.  
  172.  
  173.             //Create the head and attach a 'segment' behavior to it, which stamps its position
  174.             // every frame. The segments can then query the position N frames ago, giving it a wavy appearance.
  175.             SegmentBehavior headSegmentBehavior = new SegmentBehavior();
  176.             _head = Slab.CreateEntity(headSegmentBehavior);
  177.             Entity.AppendChild(_head);
  178.             _head.Size = new Vector2(LURKER_SEGMENT_SIZE, LURKER_SEGMENT_SIZE);
  179.             ConfigureHeadAppearance(_head);
  180.             _head.Layer = Layers.OverActionLayer;
  181.             _head.AttachCollide(HeadHitsPlayer, Player.CollisionCategory);
  182.             _head.AttachTrigger<DamageEnemy>(Hit);
  183.             _head.AddTag(Tags.Enemy);
  184.             Section.AddBehavior(_head, new LurkerSegmentBehavior(Entity));
  185.            
  186.             //Move straight up a certain distance, then go to 'roaming' state
  187.             Behavior.Control(
  188.                 new Sequence(
  189.                     new Range(ControllerMode.Normal, RISING_TIME, new FunctionInterp(v => _headPosition = new Vector2(0, v), 0, ROAM_RADIUS, CurveType.EaseIn)),
  190.                     new Execute(() => Behavior.SetState("roaming"))));
  191.  
  192.             //Custom sort values control the painters algorithm
  193.             _head.CustomSortValue = Entity.WorldPosition.Y - 2;
  194.            
  195.             for (int i = 0; i < NUMBER_OF_SEGMENTS; i++)
  196.             {
  197.                 //Configure the body size and parameters; this was mostly just guessing until it looked right
  198.                 float lag = (i * .1f);
  199.                 float ratio = 1 - ((float)(i + 1) / (NUMBER_OF_SEGMENTS + 1));
  200.                 var segment = Slab.CreateEntity();
  201.                 segment.TextureName = TextureNames.LurkerBody;
  202.                 segment.Size = new Vector2(LURKER_SEGMENT_SIZE, LURKER_SEGMENT_SIZE) *(.8f - i * .1f);
  203.                 segment.Layer = Layers.ActionLayer;
  204.                
  205.                 //Again, painter's algorithm in reverse to give the illusion of stacking
  206.                 segment.CustomSortValue = Entity.WorldPosition.Y + lag * .1f;
  207.  
  208.                 //Flail the arms of the body, starting at a random frame
  209.                 segment.Control(new Range(ControllerMode.Normal, .2f, OMath.RandomNumber(), 0, new FloatInterp(FloatProp.Frame, 0, 1, CurveType.Linear)));
  210.                 _segments.Add(segment);
  211.                 Entity.AppendChild(segment);
  212.                
  213.                 //Update the position of the segment based on the head's lagged (stamped) position
  214.                 segment.AttachUpdate(v => HandleSegmentUpdate(segment, headSegmentBehavior, lag, ratio));
  215.             }
  216.         }
  217.  
  218.         private void HandleSegmentUpdate(IEntityInterface segment, SegmentBehavior headBehavior, float lag, float ratio)
  219.         {
  220.             var oldPosition = headBehavior.GetOldPosition(lag);
  221.             var newPosition = Vector2.Lerp(Entity.WorldPosition, oldPosition, ratio);
  222.             segment.PositionSmooth = newPosition - Entity.WorldPosition;
  223.         }
  224.  
  225.         private void RoamingInit(UpdateArgs obj)
  226.         {
  227.             _head.Frame = FRAME_MOUTH_CLOSED;
  228.         }
  229.  
  230.         private void RoamingUpdate(UpdateArgs obj)
  231.         {
  232.             //Sinusoidal motion, swaying back and forth.
  233.             _currentRoamAngle += obj.Dt * 4;
  234.             if (_currentRoamAngle > OMath.TwoPI)
  235.                 _currentRoamAngle -= OMath.TwoPI;
  236.  
  237.             var player = PlayerFinder.Find(Section);
  238.             if (player == null)
  239.                 return;
  240.  
  241.             //Always avoid the player
  242.             var oppositePlayer = DirectionHelper.GetTowardsDirection(player.WorldPosition, Entity.WorldPosition) * ROAM_RADIUS;
  243.             var angle = OMath.Angle(oppositePlayer) + OMath.PI / 2f;
  244.             var angleComponent = OMath.AngleDirection(angle);
  245.  
  246.             _headPosition = oppositePlayer + angleComponent * OMath.Sin(_currentRoamAngle) * ANGLE_ROAM;
  247.  
  248.             ConfigureHeadAppearance(_head);
  249.            
  250.             //If close enough to player (and it's been at least a second since last strike), prep to strike the player.
  251.             var distance = (player.WorldPosition - Entity.WorldPosition).LengthSquared;
  252.             if (distance < STRIKE_TRIGGER_DISTANCE * STRIKE_TRIGGER_DISTANCE && _lastHit > 1)
  253.             {
  254.                 Behavior.SetState("prepping");
  255.             }
  256.         }
  257.  
  258.  
  259.         private void PreppingInit(InitArgs obj)
  260.         {
  261.             var player = PlayerFinder.Find(Section);
  262.             if (player == null)
  263.                 return;
  264.  
  265.             //Stretch backwards, wait, and then change state to striking
  266.             var stretchPosition = DirectionHelper.GetTowardsDirection(player.WorldPosition, Entity.WorldPosition) * BACK_STRETCH;
  267.             Behavior.Control(
  268.                 new Range(ControllerMode.Normal, STRIKE_PREP_TIME, new FunctionInterp(v => _headPosition = stretchPosition, 0, 1, CurveType.EaseIn)),
  269.                 new Delay(STRIKE_DELAY),
  270.                 new Execute(() => Behavior.SetState("striking")));
  271.            
  272.             //Also, open the mouth during this.
  273.             Behavior.Control(
  274.                 new Range(ControllerMode.Normal, STRIKE_PREP_TIME, new FloatInterp(FloatProp.Frame, FRAME_MOUTH_CLOSED, FRAME_MOUTH_DEFAULT, CurveType.Linear)), _head);
  275.         }
  276.  
  277.         private void StrikingInit(InitArgs obj)
  278.         {
  279.             var player = PlayerFinder.Find(Section);
  280.             if (player == null)
  281.                 return;
  282.  
  283.             //Find the player and attack! Update the animation frames and set state to 'stuck'
  284.             var target = DirectionHelper.GetTowardsDirection(Entity.WorldPosition, player.WorldPosition) * STRIKE_DISTANCE;
  285.             Behavior.Control(
  286.                 new Sequence(
  287.                     new Range(ControllerMode.Normal, STRIKE_TIME * .75f, new FloatInterp(FloatProp.Frame, FRAME_MOUTH_CLOSED, 0f, CurveType.Linear)),
  288.                     new Execute(() => _head.Frame = FRAME_MOUTH_CLENCH)),
  289.                     _head);
  290.  
  291.            
  292.             Behavior.Control(
  293.                 new Sequence(
  294.                     new Range(ControllerMode.Normal, STRIKE_TIME,
  295.                         new FunctionInterpVector2(v => _headPosition = v, _headPosition, target, CurveType.Linear)),
  296.                     new Execute(() => Behavior.SetState("stuck"))));
  297.         }
  298.  
  299.  
  300.         private void StuckInit(InitArgs obj)
  301.         {
  302.             //Pretty basic--shake the screen, wait a delay, and go back to roaming state
  303.             _head.Layer = Layers.ActionLayer;
  304.             ScreenShaker.ShakeThingSlams(Section);
  305.             Behavior.Control(
  306.                 new Delay(STUCK_DURATION),
  307.                 new Execute(() => Behavior.SetState("roaming")));
  308.         }
  309.  
  310.         private void StuckStateLeave(StateLeaveArgs obj)
  311.         {
  312.             _head.Layer = Layers.OverActionLayer;
  313.         }
  314.  
  315.         private void Update(UpdateArgs obj)
  316.         {
  317.             _lastHit += obj.Dt;
  318.  
  319.             if (_head != null)
  320.             {
  321.                 _head.PositionSmooth += (_headPosition - _head.Position) * (obj.Dt * 10);
  322.             }
  323.         }
  324.  
  325.  
  326.        
  327.         private void Hit(TriggerArgs<DamageEnemy> obj)
  328.         {
  329.             if (Behavior.State != "hurt")
  330.                 Behavior.SetState("hurt", obj.Data);
  331.         }
  332.  
  333.         private void FinishHead()
  334.         {
  335.             //Create a bunch of explosions
  336.             var position = _head.WorldPosition;
  337.             for (int i = 0; i < 12; i++)
  338.             {
  339.                 var temp = i;
  340.                 Behavior.Control(
  341.                     new Delay(i * .075f),
  342.                     new Execute(delegate
  343.                     {
  344.                         if (temp % 2 == 0)
  345.                             Audio.PlaySound(SoundNames.Bomb);
  346.                         var cloud = Slab.CreateEntity(new DeathCloud(position + OMath.RandomDirection(0, 48), new Vector2(32, 32), delegate { }) { ShortExplosion = true });
  347.                         cloud.Size = new Vector2(48, 48);
  348.                         cloud.Layer = Layers.OverActionLayer;
  349.                     }));
  350.             }
  351.            
  352.             _head.Destroy();
  353.  
  354.             //Shrink the crack down to nothing and remove the entire entity
  355.             Behavior.Control(
  356.                 new Sequence(
  357.                     new Range(ControllerMode.Normal, 1,
  358.                         new FloatInterp(FloatProp.Size, 0, CurveType.EaseOutBounce),
  359.                         new FloatInterp(FloatProp.Opacity, 0f, CurveType.EaseOutBounce),
  360.                         new FloatInterp(FloatProp.Rotation, 1f, CurveType.Linear)),
  361.                        
  362.                     new Execute(() => Entity.Destroy())), _crack);
  363.  
  364.         }
  365.  
  366.         private void FinishSegment(IEntityInterface segment)
  367.         {
  368.             //Create an explosion
  369.             Audio.PlaySound(SoundNames.Bomb);
  370.             Slab.CreateEntity(new DeathCloud(segment.WorldPosition, new Vector2(32, 32), delegate {  }) { ShortExplosion = true });
  371.             segment.Destroy();
  372.         }
  373.  
  374.         private void HurtInit(InitArgs obj, DamageEnemy damageEnemy)
  375.         {
  376.             Audio.PlaySound(SoundNames.Splat);
  377.             var currentPosition = _headPosition;
  378.             var targetPosition = _headPosition + DamageHelper.GetDamageDirection(_head.WorldPosition, damageEnemy.DamageSource) * 200;
  379.             var angle = OMath.Angle(targetPosition);
  380.             targetPosition = OMath.AngleDirection(angle) * HIT_RADIUS;
  381.             _head.Frame = FRAME_MOUTH_DEFAULT;
  382.             _health -= damageEnemy.DamageAmount;
  383.             if (_health <= 0)
  384.             {
  385.                 Behavior.Control(
  386.                     new Range(ControllerMode.Normal, .3f, new FunctionInterpVector2(v => _headPosition = v, currentPosition, targetPosition, CurveType.Linear)));
  387.  
  388.                 ApplyShift(0, _head, true, 0);
  389.                 _head.Control(
  390.                     new Delay((_segments.Count + 2) * .4f),
  391.                     new Execute(() => FinishHead()));
  392.                
  393.                 for (int i = 0; i < _segments.Count; i++)
  394.                 {
  395.                     var segment = _segments[i];
  396.                     ApplyShift(0, segment, true, i * .1f);
  397.                    
  398.                     segment.Control(
  399.                         new Delay(1f + (_segments.Count - i) * .2f),
  400.                         new Execute(() => FinishSegment(segment)));
  401.                 }
  402.             }
  403.             else
  404.             {
  405.                 Behavior.Control(
  406.                     new Range(ControllerMode.Normal, .3f, new FunctionInterpVector2(v => _headPosition = v, currentPosition, targetPosition, CurveType.Linear)),
  407.                     new Delay(.5f),
  408.                     new Execute(() => Behavior.SetState("roaming")));
  409.                 ApplyShift(0, _head, false, 0);
  410.                
  411.                 for (int i = 0; i < _segments.Count; i++)
  412.                 {
  413.                     var segment = _segments[i];
  414.                     ApplyShift(i * .1f, segment, false, 0);
  415.                 }
  416.             }
  417.         }
  418.  
  419.         private void ApplyShift(float delay, IEntityInterface entity, bool forever, float initialValue)
  420.         {
  421.             var iterations = forever ? 0 : 1;
  422.             entity.Control(
  423.                 new Sequence(
  424.                     new Delay(delay),
  425.                     new Execute(() => entity.Color = Colors.Pink(.5f)),
  426.                     new Range(ControllerMode.Normal, .5f, initialValue, iterations, new FloatInterp(FloatProp.Hue, 0, 1, CurveType.Linear)),
  427.                     new Execute(delegate
  428.                     {
  429.                         entity.Hue = 0;
  430.                         entity.Color = Colors.White;
  431.                     })));
  432.         }
  433.  
  434.         private void ConfigureHeadAppearance(IEntityInterface head)
  435.         {
  436.             var player = PlayerFinder.Find(Section);
  437.             if (player == null)
  438.             {
  439.                 head.TextureName = TextureNames.LurkerHeadSide;
  440.                 return;
  441.             }
  442.             var facing = DirectionHelper.GetDirection(head.WorldPosition, player.WorldPosition);
  443.             head.HFlip = false;
  444.             if (facing == Direction.Left)
  445.             {
  446.                 head.TextureName = TextureNames.LurkerHeadSide;
  447.                 head.HFlip = true;
  448.             }
  449.             else if (facing == Direction.Right)
  450.                 head.TextureName = TextureNames.LurkerHeadSide;
  451.             else if (facing == Direction.Up)
  452.                 head.TextureName = TextureNames.LurkerHeadUp;
  453.             else
  454.                 head.TextureName = TextureNames.LurkerHeadDown;
  455.         }
  456.  
  457.         private float _lastHit = 1;
  458.         private void HeadHitsPlayer(CollideArgs obj)
  459.         {
  460.             _lastHit = 0;
  461.             if (Behavior.State != "hurt")
  462.             {
  463.                 obj.Other.Trigger(new DamagePlayer(_head.WorldPosition, 1));
  464.                 if (Behavior.State == "striking")
  465.                     Behavior.SetState("roaming");
  466.             }
  467.         }
  468.     }
  469. }
RAW Paste Data
Want to get better at C#?
Learn to code C# in 2017
Pastebin PRO Summer Special!
Get 40% OFF on Pastebin PRO accounts!
Top