Advertisement
Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- using System.Collections.Generic;
- using ObeliskImplementation.CommonValues;
- using ObeliskImplementation.Effects;
- using ObeliskImplementation.GameConfiguration;
- using ObeliskImplementation.Misc;
- using ObeliskImplementation.Players;
- using ObeliskImplementation.ScreenEffects;
- using ObeliskImplementation.Textures;
- using ObeliskImplementation.Triggers;
- using ObeliskInterfaces.Args;
- using ObeliskInterfaces.Behaviors;
- using ObeliskInterfaces.Interfaces;
- using ObeliskInterfaces.Scripts;
- using ObeliskInterfaces.Types.Controllers;
- using ObeliskInterfaces.Utility;
- using OpenTK;
- namespace ObeliskImplementation.Enemies
- {
- public class Lurker : BehaviorTemplate
- {
- private const float STRIKE_TRIGGER_DISTANCE = 92; //When the player gets this close, the lurker awakens (or attacks)
- private const float EMERGING_TIME = .6f; //Time spent emerging from the ground
- private const float RISING_TIME = .5f; //Time spent emerging from the ground
- private const float LURKER_SEGMENT_SIZE = 64; //Size of the head/segments
- private const float ROAM_RADIUS = 64; //Radius when circling around the hole
- private const float HIT_RADIUS = 64; //Recoil distance (from center) when hit
- private const float BACK_STRETCH = 128; //Recoil distance (from center) when preparing for an attack
- private const float STRIKE_DISTANCE = 92; //Distance moved (from center) when striking
- private const float ANGLE_ROAM = 32; //Random 'sway' distance
- private const int NUMBER_OF_SEGMENTS = 6; //Body segments
- private const float STRIKE_PREP_TIME = .2f; //Time spent moving back into striking position
- private const float STRIKE_DELAY = .2f; //Time spent sitting in striking position
- private const float STRIKE_TIME = .25f; //Time spent attacking
- private const float STUCK_DURATION = 1f; //Time spent attacking
- private const float FRAME_MOUTH_CLOSED = 0f;
- private const float FRAME_MOUTH_DEFAULT = .74f;
- private const float FRAME_MOUTH_CLENCH = .99f;
- private float _health = 2; //Amount of health remaining
- private IEntityInterface _head;
- //Instead of setting the head position directly, we set this variable and always perform an interpolated motion towards it
- private Vector2 _headPosition;
- private List<IEntityInterface> _segments = new List<IEntityInterface>();
- private IEntityInterface _crack;
- private float _currentRoamAngle;
- public override EditorConfiguration EditorConfig
- {
- get
- {
- //Provide a key and texture name for the level editor
- return new EditorConfiguration("Lurker", TextureNames.LurkerHeadDown);
- }
- }
- public override Behavior Create()
- {
- Behavior b = new Behavior();
- b.Update(new UpdateBehavior(Update));
- //Hidden: Sitting underground
- var hidden = b.AddState("hidden");
- hidden.Update(new UpdateBehavior(HiddenUpdate, .25f));
- //Rumbling: Preparing to emerge
- var rumbling = b.AddState("rumbling");
- rumbling.Init(new InitBehavior(RumblingInit));
- //Rising: Bursting out of the ground
- var rising = b.AddState("rising");
- rising.Init(new InitBehavior(RisingInit));
- //Roaming: Avoiding the player, swaying back and forth, and looking for an attack target
- var roaming = b.AddState("roaming");
- roaming.Update(new UpdateBehavior(RoamingInit));
- roaming.Update(new UpdateBehavior(RoamingUpdate));
- //Preparing to attack
- var prepping = b.AddState("prepping");
- prepping.Init(new InitBehavior(PreppingInit));
- //Attacking the player
- var striking = b.AddState("striking");
- striking.Init(new InitBehavior(StrikingInit));
- //Stuck in the ground after an unsuccessful attack
- var stuck = b.AddState("stuck");
- stuck.Init(new InitBehavior(StuckInit));
- stuck.StateLeave(new StateLeaveBehavior(StuckStateLeave));
- //Recoiling or dying
- var hurt = b.AddState("hurt");
- hurt.Init(new InitBehavior<DamageEnemy>(HurtInit));
- b.InitialState = "hidden";
- return b;
- }
- private void HiddenUpdate(UpdateArgs obj)
- {
- //Search for a player that's nearby and switch to 'rumbling' if found
- var player = PlayerFinder.Find(Slab);
- if (player != null && ((player.WorldPosition - Entity.WorldPosition).LengthSquared < STRIKE_TRIGGER_DISTANCE * STRIKE_TRIGGER_DISTANCE))
- {
- Behavior.SetState("rumbling");
- }
- }
- private void RumblingInit(InitArgs obj)
- {
- //Play a noise, shake the screen, and create the 'crack' entity
- Audio.PlaySound(SoundNames.Rumble);
- ScreenShaker.ShakeConstantly(Section, 3, EMERGING_TIME);
- _crack = Slab.CreateEntity();
- _crack.TextureName = TextureNames.LurkerCrack;
- _crack.Layer = Layers.FootLayer;
- _crack.Size = new Vector2(48, 48);
- //The crack entity has 3 frames. Cycle through them and switch to the 'rising' state
- Behavior.Control(
- new Sequence(
- new Range(ControllerMode.Normal, EMERGING_TIME, new FloatInterp(FloatProp.Frame, 0, 1, CurveType.Linear)),
- new Execute(() => Behavior.SetState("rising"))),
- _crack);
- Entity.AppendChild(_crack);
- }
- private void RisingInit(InitArgs obj)
- {
- //Turn the crack into a hole, and make it so collision with it damages the player.
- //Its model is a half-sized square, so it's fairly forgiving.
- _crack.TextureName = TextureNames.LurkerHole;
- _crack.ModelType = Models.HalfSolid;
- _crack.AttachCollide(arg => arg.Other.Trigger(new DamagePlayer(Entity.WorldPosition, 1)), Player.CollisionCategory);
- //Make shards
- for (int i = 0; i < 30; i++)
- {
- var shard = Slab.CreateEntity();
- shard.TextureName = TextureNames.LurkerFloorShard;
- shard.Rotation = OMath.RandomAngle();
- shard.Size = OMath.RandomSize(16, 32);
- shard.Layer = Layers.OverActionLayer;
- shard.Position = Entity.WorldPosition;
- //Apply spin and motion
- shard.Control(new Rotation(OMath.RandomNumber(-OMath.PI, OMath.PI)));
- shard.Control(new Motion(OMath.RandomDirection(OMath.RandomNumber(60, 120))));
- var shardDuration = OMath.RandomNumber(.5f, 1f);
- //Fade out halfway through the flight
- shard.Control(
- new Delay(shardDuration / 2f),
- new Range(ControllerMode.Normal, shardDuration / 2f, new FloatInterp(FloatProp.Opacity, 0, CurveType.Linear)));
- //Fall down during the flight
- shard.Control(
- new Sequence(
- new Range(ControllerMode.ZigZag, shardDuration, new FloatInterp(FloatProp.OffsetY, 30, CurveType.EaseIn)),
- new Execute(() => shard.Destroy())));
- }
- //Create the head and attach a 'segment' behavior to it, which stamps its position
- // every frame. The segments can then query the position N frames ago, giving it a wavy appearance.
- SegmentBehavior headSegmentBehavior = new SegmentBehavior();
- _head = Slab.CreateEntity(headSegmentBehavior);
- Entity.AppendChild(_head);
- _head.Size = new Vector2(LURKER_SEGMENT_SIZE, LURKER_SEGMENT_SIZE);
- ConfigureHeadAppearance(_head);
- _head.Layer = Layers.OverActionLayer;
- _head.AttachCollide(HeadHitsPlayer, Player.CollisionCategory);
- _head.AttachTrigger<DamageEnemy>(Hit);
- _head.AddTag(Tags.Enemy);
- Section.AddBehavior(_head, new LurkerSegmentBehavior(Entity));
- //Move straight up a certain distance, then go to 'roaming' state
- Behavior.Control(
- new Sequence(
- new Range(ControllerMode.Normal, RISING_TIME, new FunctionInterp(v => _headPosition = new Vector2(0, v), 0, ROAM_RADIUS, CurveType.EaseIn)),
- new Execute(() => Behavior.SetState("roaming"))));
- //Custom sort values control the painters algorithm
- _head.CustomSortValue = Entity.WorldPosition.Y - 2;
- for (int i = 0; i < NUMBER_OF_SEGMENTS; i++)
- {
- //Configure the body size and parameters; this was mostly just guessing until it looked right
- float lag = (i * .1f);
- float ratio = 1 - ((float)(i + 1) / (NUMBER_OF_SEGMENTS + 1));
- var segment = Slab.CreateEntity();
- segment.TextureName = TextureNames.LurkerBody;
- segment.Size = new Vector2(LURKER_SEGMENT_SIZE, LURKER_SEGMENT_SIZE) *(.8f - i * .1f);
- segment.Layer = Layers.ActionLayer;
- //Again, painter's algorithm in reverse to give the illusion of stacking
- segment.CustomSortValue = Entity.WorldPosition.Y + lag * .1f;
- //Flail the arms of the body, starting at a random frame
- segment.Control(new Range(ControllerMode.Normal, .2f, OMath.RandomNumber(), 0, new FloatInterp(FloatProp.Frame, 0, 1, CurveType.Linear)));
- _segments.Add(segment);
- Entity.AppendChild(segment);
- //Update the position of the segment based on the head's lagged (stamped) position
- segment.AttachUpdate(v => HandleSegmentUpdate(segment, headSegmentBehavior, lag, ratio));
- }
- }
- private void HandleSegmentUpdate(IEntityInterface segment, SegmentBehavior headBehavior, float lag, float ratio)
- {
- var oldPosition = headBehavior.GetOldPosition(lag);
- var newPosition = Vector2.Lerp(Entity.WorldPosition, oldPosition, ratio);
- segment.PositionSmooth = newPosition - Entity.WorldPosition;
- }
- private void RoamingInit(UpdateArgs obj)
- {
- _head.Frame = FRAME_MOUTH_CLOSED;
- }
- private void RoamingUpdate(UpdateArgs obj)
- {
- //Sinusoidal motion, swaying back and forth.
- _currentRoamAngle += obj.Dt * 4;
- if (_currentRoamAngle > OMath.TwoPI)
- _currentRoamAngle -= OMath.TwoPI;
- var player = PlayerFinder.Find(Section);
- if (player == null)
- return;
- //Always avoid the player
- var oppositePlayer = DirectionHelper.GetTowardsDirection(player.WorldPosition, Entity.WorldPosition) * ROAM_RADIUS;
- var angle = OMath.Angle(oppositePlayer) + OMath.PI / 2f;
- var angleComponent = OMath.AngleDirection(angle);
- _headPosition = oppositePlayer + angleComponent * OMath.Sin(_currentRoamAngle) * ANGLE_ROAM;
- ConfigureHeadAppearance(_head);
- //If close enough to player (and it's been at least a second since last strike), prep to strike the player.
- var distance = (player.WorldPosition - Entity.WorldPosition).LengthSquared;
- if (distance < STRIKE_TRIGGER_DISTANCE * STRIKE_TRIGGER_DISTANCE && _lastHit > 1)
- {
- Behavior.SetState("prepping");
- }
- }
- private void PreppingInit(InitArgs obj)
- {
- var player = PlayerFinder.Find(Section);
- if (player == null)
- return;
- //Stretch backwards, wait, and then change state to striking
- var stretchPosition = DirectionHelper.GetTowardsDirection(player.WorldPosition, Entity.WorldPosition) * BACK_STRETCH;
- Behavior.Control(
- new Range(ControllerMode.Normal, STRIKE_PREP_TIME, new FunctionInterp(v => _headPosition = stretchPosition, 0, 1, CurveType.EaseIn)),
- new Delay(STRIKE_DELAY),
- new Execute(() => Behavior.SetState("striking")));
- //Also, open the mouth during this.
- Behavior.Control(
- new Range(ControllerMode.Normal, STRIKE_PREP_TIME, new FloatInterp(FloatProp.Frame, FRAME_MOUTH_CLOSED, FRAME_MOUTH_DEFAULT, CurveType.Linear)), _head);
- }
- private void StrikingInit(InitArgs obj)
- {
- var player = PlayerFinder.Find(Section);
- if (player == null)
- return;
- //Find the player and attack! Update the animation frames and set state to 'stuck'
- var target = DirectionHelper.GetTowardsDirection(Entity.WorldPosition, player.WorldPosition) * STRIKE_DISTANCE;
- Behavior.Control(
- new Sequence(
- new Range(ControllerMode.Normal, STRIKE_TIME * .75f, new FloatInterp(FloatProp.Frame, FRAME_MOUTH_CLOSED, 0f, CurveType.Linear)),
- new Execute(() => _head.Frame = FRAME_MOUTH_CLENCH)),
- _head);
- Behavior.Control(
- new Sequence(
- new Range(ControllerMode.Normal, STRIKE_TIME,
- new FunctionInterpVector2(v => _headPosition = v, _headPosition, target, CurveType.Linear)),
- new Execute(() => Behavior.SetState("stuck"))));
- }
- private void StuckInit(InitArgs obj)
- {
- //Pretty basic--shake the screen, wait a delay, and go back to roaming state
- _head.Layer = Layers.ActionLayer;
- ScreenShaker.ShakeThingSlams(Section);
- Behavior.Control(
- new Delay(STUCK_DURATION),
- new Execute(() => Behavior.SetState("roaming")));
- }
- private void StuckStateLeave(StateLeaveArgs obj)
- {
- _head.Layer = Layers.OverActionLayer;
- }
- private void Update(UpdateArgs obj)
- {
- _lastHit += obj.Dt;
- if (_head != null)
- {
- _head.PositionSmooth += (_headPosition - _head.Position) * (obj.Dt * 10);
- }
- }
- private void Hit(TriggerArgs<DamageEnemy> obj)
- {
- if (Behavior.State != "hurt")
- Behavior.SetState("hurt", obj.Data);
- }
- private void FinishHead()
- {
- //Create a bunch of explosions
- var position = _head.WorldPosition;
- for (int i = 0; i < 12; i++)
- {
- var temp = i;
- Behavior.Control(
- new Delay(i * .075f),
- new Execute(delegate
- {
- if (temp % 2 == 0)
- Audio.PlaySound(SoundNames.Bomb);
- var cloud = Slab.CreateEntity(new DeathCloud(position + OMath.RandomDirection(0, 48), new Vector2(32, 32), delegate { }) { ShortExplosion = true });
- cloud.Size = new Vector2(48, 48);
- cloud.Layer = Layers.OverActionLayer;
- }));
- }
- _head.Destroy();
- //Shrink the crack down to nothing and remove the entire entity
- Behavior.Control(
- new Sequence(
- new Range(ControllerMode.Normal, 1,
- new FloatInterp(FloatProp.Size, 0, CurveType.EaseOutBounce),
- new FloatInterp(FloatProp.Opacity, 0f, CurveType.EaseOutBounce),
- new FloatInterp(FloatProp.Rotation, 1f, CurveType.Linear)),
- new Execute(() => Entity.Destroy())), _crack);
- }
- private void FinishSegment(IEntityInterface segment)
- {
- //Create an explosion
- Audio.PlaySound(SoundNames.Bomb);
- Slab.CreateEntity(new DeathCloud(segment.WorldPosition, new Vector2(32, 32), delegate { }) { ShortExplosion = true });
- segment.Destroy();
- }
- private void HurtInit(InitArgs obj, DamageEnemy damageEnemy)
- {
- Audio.PlaySound(SoundNames.Splat);
- var currentPosition = _headPosition;
- var targetPosition = _headPosition + DamageHelper.GetDamageDirection(_head.WorldPosition, damageEnemy.DamageSource) * 200;
- var angle = OMath.Angle(targetPosition);
- targetPosition = OMath.AngleDirection(angle) * HIT_RADIUS;
- _head.Frame = FRAME_MOUTH_DEFAULT;
- _health -= damageEnemy.DamageAmount;
- if (_health <= 0)
- {
- Behavior.Control(
- new Range(ControllerMode.Normal, .3f, new FunctionInterpVector2(v => _headPosition = v, currentPosition, targetPosition, CurveType.Linear)));
- ApplyShift(0, _head, true, 0);
- _head.Control(
- new Delay((_segments.Count + 2) * .4f),
- new Execute(() => FinishHead()));
- for (int i = 0; i < _segments.Count; i++)
- {
- var segment = _segments[i];
- ApplyShift(0, segment, true, i * .1f);
- segment.Control(
- new Delay(1f + (_segments.Count - i) * .2f),
- new Execute(() => FinishSegment(segment)));
- }
- }
- else
- {
- Behavior.Control(
- new Range(ControllerMode.Normal, .3f, new FunctionInterpVector2(v => _headPosition = v, currentPosition, targetPosition, CurveType.Linear)),
- new Delay(.5f),
- new Execute(() => Behavior.SetState("roaming")));
- ApplyShift(0, _head, false, 0);
- for (int i = 0; i < _segments.Count; i++)
- {
- var segment = _segments[i];
- ApplyShift(i * .1f, segment, false, 0);
- }
- }
- }
- private void ApplyShift(float delay, IEntityInterface entity, bool forever, float initialValue)
- {
- var iterations = forever ? 0 : 1;
- entity.Control(
- new Sequence(
- new Delay(delay),
- new Execute(() => entity.Color = Colors.Pink(.5f)),
- new Range(ControllerMode.Normal, .5f, initialValue, iterations, new FloatInterp(FloatProp.Hue, 0, 1, CurveType.Linear)),
- new Execute(delegate
- {
- entity.Hue = 0;
- entity.Color = Colors.White;
- })));
- }
- private void ConfigureHeadAppearance(IEntityInterface head)
- {
- var player = PlayerFinder.Find(Section);
- if (player == null)
- {
- head.TextureName = TextureNames.LurkerHeadSide;
- return;
- }
- var facing = DirectionHelper.GetDirection(head.WorldPosition, player.WorldPosition);
- head.HFlip = false;
- if (facing == Direction.Left)
- {
- head.TextureName = TextureNames.LurkerHeadSide;
- head.HFlip = true;
- }
- else if (facing == Direction.Right)
- head.TextureName = TextureNames.LurkerHeadSide;
- else if (facing == Direction.Up)
- head.TextureName = TextureNames.LurkerHeadUp;
- else
- head.TextureName = TextureNames.LurkerHeadDown;
- }
- private float _lastHit = 1;
- private void HeadHitsPlayer(CollideArgs obj)
- {
- _lastHit = 0;
- if (Behavior.State != "hurt")
- {
- obj.Other.Trigger(new DamagePlayer(_head.WorldPosition, 1));
- if (Behavior.State == "striking")
- Behavior.SetState("roaming");
- }
- }
- }
- }
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement