using System.Numerics; using LiteNetLib.Utils; using OnekoOnline.Net; using Raylib_cs; namespace OnekoOnline; class OnekoLocal : Oneko { public static Oneko? Instance; static int Id => OnekoOnline.Client?.Id ?? -1; OnekoAIState CurrentAIState; readonly OnekoAIState[] AIStates; OnekoAnimation Animation; float updateTimer = 0f; const float updateRate = 1f/5f; int Frame = 0; int FrameCounter = 0; public override int DrawSort => (int)(Position.Y - (Size.Y/2.5f)); public OnekoLocal() : base() { Instance ??= this; Name = OnekoOnline.Config.GetValue("NekoName", "Oneko"); AIStates = [ new BoredState(this), new ChaseMouseState(this), new ItchyState(this), new SleepState(this), new RunAwayFromCatState(this), new RandomSpotState(this), new ChaseOtherCatState(this), new YawnState(this) ]; CurrentAIState = AIStates[0]; } public override void Update(float delta) { updateTimer += delta; if (updateTimer < updateRate) return; OnekoUpdate(updateTimer); updateTimer = 0f; } public override void Draw() { #if DEBUG Raylib.DrawText($"Playfullness {OnekoAIState.Playfullness}", 0, 210, 10, Color.White); Raylib.DrawText($"Energy {OnekoAIState.Energy}", 0, 220, 10, Color.White); Raylib.DrawText($"State {CurrentAIState.GetType().Name}", 0, 230, 10, Color.White); #endif base.Draw(); } public void OnekoUpdate(float delta) { //AI STUFF if (Random.Shared.NextSingle() > 0.997f) OnekoAIState.Playfullness = Random.Shared.NextSingle(); OnekoAIState HighestPriority = AIStates.MaxBy(state => state.GetPriority())!; if (HighestPriority != CurrentAIState) { CurrentAIState.ExitState(); CurrentAIState = HighestPriority; CurrentAIState.StartState(); } CurrentAIState.Update(delta); FrameCounter = (FrameCounter+1)%(Animation.AnimSpeed+1); Frame = (int)MathF.Round(FrameCounter/(float)Animation.AnimSpeed); Sprite = Animation.GetFrame(Frame); if (OnekoOnline.Client?.Connected == true) { NetDataWriter writer = new(); writer.Put(new PacketInfo(PacketType.OnekoState, Id)); writer.Put(Position); writer.Put(FrameId); OnekoOnline.Client?.ConnectedServer.Send(writer, LiteNetLib.DeliveryMethod.Unreliable); } Position = Vector2.Clamp(Position, Vector2.Zero, new(OnekoOnline.WindowX, OnekoOnline.WindowY)); } void MoveTowardsPosition(Vector2 TargetPosition) { if (TargetPosition.Round() == Position) { Animation = Idle; return; } Vector2 TargetDirection = (TargetPosition-Position).LimitLength(8+(OnekoAIState.Energy*4f)); if (TargetDirection.Length() > 1) { Animation = GetDirectionalRun(TargetDirection); } Position += TargetDirection; } protected abstract class OnekoAIState(OnekoLocal neko) { protected OnekoLocal Neko = neko; public abstract int GetPriority(); public abstract void Update(float delta); public virtual void StartState() {} public virtual void ExitState() {} protected bool IsCurrentState => Neko.CurrentAIState == this; //Mood private static float _playfulness = 0.8f; public static float Playfullness { get => _playfulness; set => _playfulness = Math.Clamp(value, 0f, 1f); } private static float _energy = 0.8f; public static float Energy { get => _energy; set => _energy = Math.Clamp(value, 0f, 1f); } } class BoredState(OnekoLocal neko) : OnekoAIState(neko) { float InactivityTimer = 0f; public override int GetPriority() => 0; public override void Update(float delta) { InactivityTimer += delta; Energy -= delta/150f; Neko.Animation = Idle; if (InactivityTimer > 1.5f && InactivityTimer < 3f) Neko.Animation = Clean; if (InactivityTimer > 5f && InactivityTimer < 6.5f) Neko.Animation = ScratchSelf; if (InactivityTimer > 10f) { if (Energy < 0.7f) SleepState.Sleeping = true; else RandomSpotState.NewSpotNow = true; } } public override void ExitState() => InactivityTimer = 0f; } class SleepState : OnekoAIState { float SleepTimer = 0f; public static bool Sleeping; public SleepState(OnekoLocal neko) : base(neko) { Mouse.Clicked += mouse => { if (Sleeping && Energy > 0.25f && mouse is MouseLocal) Sleeping = false; }; } public override int GetPriority() { if (Energy == 0f) Sleeping = true; if (Energy > 0.5f && Sleeping) return 1; if (Sleeping) return 900; else return -1; } public override void Update(float delta) { SleepTimer += delta; Energy += delta/60f; if (SleepTimer < 1f) Neko.Animation = Yawn; else Neko.Animation = Sleep; } public override void ExitState() { SleepTimer = 0f; Sleeping = false; } } class RunAwayFromCatState(OnekoLocal neko) : OnekoAIState(neko) { Oneko? ClosestNeko; float RandOffset; public override int GetPriority() { if (AllNekos.Count < 2) return -1; //If a cat is too close, run! ClosestNeko = AllNekos.Where(neko => neko != Neko && Vector2.Distance(neko.Position, Neko.Position) < 30).FirstOrDefault(); if (ClosestNeko != null) return 5; return -1; } public override void StartState() => RandOffset = Random.Shared.NextSingle() * 100f; public override void Update(float delta) { if (ClosestNeko is null) return; Energy -= delta/60f; Playfullness += delta/60f; Vector2 Direction = Raymath.Vector2Rotate(Directions.Up, DateTime.Now.Second+RandOffset); Neko.MoveTowardsPosition(ClosestNeko.Position + Direction*100); } } class ChaseOtherCatState(OnekoLocal neko) : OnekoAIState(neko) { Oneko? Victim; DateTime LastChase = DateTime.UnixEpoch; float RandOffset; public override int GetPriority() { //If feeling playful, chase another cat! if (Victim != null && LastChase.AddSeconds(Energy > 0.5 ? 24 : 12) > DateTime.Now) return 80; if (Playfullness < 0.7f || AllNekos.Count < 2 || LastChase.AddMinutes(Energy > 0.5 ? 2 : 5) > DateTime.Now) return -1; Victim = AllNekos.Where(neko => neko != Neko).MinBy(neko => Random.Shared.NextSingle()); if (Victim != null) return 80; return -1; } public override void StartState() { RandOffset = Random.Shared.NextSingle() * 100f; LastChase = DateTime.Now; } public override void Update(float delta) { if (Victim is null) return; Playfullness -= delta/40f; Energy -= delta/60f; Neko.MoveTowardsPosition(Victim.Position + Raymath.Vector2Rotate(Directions.Down, DateTime.Now.Second+RandOffset)*28); } } class ChaseMouseState : OnekoAIState { Mouse? MouseToChase; Vector2 MousePosition; float AlertTimer = 0f; public ChaseMouseState(OnekoLocal neko) : base(neko) { Mouse.Clicked += mouse => { Playfullness += 0.03f; if (MouseToChase != mouse && Random.Shared.NextSingle() > 0.3f) { AlertTimer = 0.5f; MouseToChase = mouse; } }; } public override int GetPriority() { if (Playfullness < 0.5f) return -1; MouseToChase ??= Mouse.AllMice.Where(m => m.Visible).MinBy(m => Vector2.Distance(m.Position, Neko.Position)); if (MouseToChase is null || !MouseToChase.Visible) return -1; if (MouseToChase.Position == MousePosition && !IsCurrentState) return -1; if (Vector2.Distance(MouseToChase.Position, Neko.Position) > 30) return (int)(Playfullness*150f); return -1; } public override void Update(float delta) { if (AlertTimer < 1f) { AlertTimer += delta; Neko.Animation = Alert; return; } if (MouseToChase is null || !MouseToChase.Visible) return; MousePosition = MouseToChase.Position; Neko.MoveTowardsPosition(MousePosition + (Raymath.Vector2Rotate(Directions.Down, Id*2f) * 20f)); Energy -= delta/60f; Playfullness -= delta/30f; } public override void ExitState() { MouseToChase = null; AlertTimer = 0f; } } class RandomSpotState(OnekoLocal neko) : OnekoAIState(neko) { DateTime NextInvestigation = DateTime.Now; Vector2 Spot; float AlertTimer = 0f; public static bool NewSpotNow = false; public override int GetPriority() { if (NewSpotNow) { NewSpotNow = false; return 50; } if (NextInvestigation < DateTime.Now || (Energy > 0.75f && Random.Shared.NextSingle() > 0.99f)) return 50; return -1; } public override void StartState() { Spot = new(Random.Shared.NextSingle()*OnekoOnline.WindowX, Random.Shared.NextSingle()*OnekoOnline.WindowY); } public override void Update(float delta) { if (AlertTimer < 1f) { AlertTimer += delta; Neko.Animation = Alert; return; } Neko.MoveTowardsPosition(Spot); Energy -= delta/60f; if (Vector2.Distance(Spot, Neko.Position) < 10) NextInvestigation = DateTime.Now.AddSeconds(Random.Shared.NextSingle() * 120); } public override void ExitState() => AlertTimer = 0f; } class ItchyState(OnekoLocal neko) : OnekoAIState(neko) { float ItchTimer = -1f; bool Cleaning = false; public override int GetPriority() { if (Neko.CurrentAIState is SleepState && Energy < 0.5f) return -1; if (ItchTimer > 0f || Random.Shared.NextSingle() > 0.995f) return 1000; return -1; } public override void StartState() { ItchTimer = 1f + Random.Shared.NextSingle()*4f; Cleaning = Random.Shared.NextSingle() > 0.5f; } public override void Update(float delta) { ItchTimer -= delta; Neko.Animation = Cleaning ? Clean : ScratchSelf; } } class YawnState(OnekoLocal neko) : OnekoAIState(neko) { float YawnTimer = -1f; public override int GetPriority() { if (Energy < 0.25f && Neko.CurrentAIState is not SleepState && (YawnTimer > 0f || Random.Shared.NextSingle() > 0.98f)) return 2000; else return -1; } public override void StartState() => YawnTimer = 2f; public override void Update(float delta) { YawnTimer -= delta; Energy -= delta/60f; Neko.Animation = Yawn; } } }