public override string UpdateServer(Entity entity) { SmartNpc smartNpc = (SmartNpc)entity; if (smartNpc.state == "IDLE") { return(UpdateServer_IDLE(smartNpc)); } if (smartNpc.state == "MOVING") { return(UpdateServer_MOVING(smartNpc)); } if (smartNpc.state == "CASTING") { return(UpdateServer_CASTING(smartNpc)); } if (smartNpc.state == "STUNNED") { return(UpdateServer_STUNNED(smartNpc)); } if (smartNpc.state == "DEAD") { return(UpdateServer_DEAD(smartNpc)); } Debug.LogError("invalid state:" + smartNpc.state); return("IDLE"); }
public override void UpdateClient(Entity entity) { SmartNpc smartNpc = (SmartNpc)entity; if (smartNpc.state == "CASTING") { // keep looking at the target for server & clients (only Y rotation) if (smartNpc.target) { smartNpc.movement.LookAtY(smartNpc.target.transform.position); } } }
// DrawGizmos can be used for debug info public override void DrawGizmos(Entity entity) { SmartNpc smartNpc = (SmartNpc)entity; // draw the movement area (around 'start' if game running, // or around current position if still editing) Vector3 startHelp = Application.isPlaying ? smartNpc.startPosition : smartNpc.transform.position; Gizmos.color = Color.yellow; Gizmos.DrawWireSphere(startHelp, moveDistance); // draw the follow dist Gizmos.color = Color.gray; Gizmos.DrawWireSphere(startHelp, followDistance); }
string UpdateServer_STUNNED(SmartNpc smartNpc) { // events sorted by priority (e.g. target doesn't matter if we died) if (EventDied(smartNpc)) { // we died. return("DEAD"); } if (EventStunned(smartNpc)) { return("STUNNED"); } // go back to idle if we aren't stunned anymore and process all new // events there too return("IDLE"); }
patrolPath; //&& smartNpc.timeSinceArrivedAtWaypoint > waypointDwellTime; // states ////////////////////////////////////////////////////////////////// string UpdateServer_IDLE(SmartNpc smartNpc) { // events sorted by priority (e.g. target doesn't matter if we died) if (EventDied(smartNpc)) { // we died. return("DEAD"); } if (EventStunned(smartNpc)) { smartNpc.movement.Reset(); return("STUNNED"); } if (EventTargetDied(smartNpc)) { // we had a target before, but it died now. clear it. smartNpc.target = null; smartNpc.skills.CancelCast(); return("IDLE"); } if (EventTargetTooFarToFollow(smartNpc)) { // we had a target before, but it's out of follow range now. // clear it and go back to start. don't stay here. smartNpc.target = null; smartNpc.skills.CancelCast(); smartNpc.movement.Navigate(smartNpc.startPosition, 0); return("MOVING"); } if (EventTargetTooFarToAttack(smartNpc)) { // we had a target before, but it's out of attack range now. // follow it. (use collider point(s) to also work with big entities) float stoppingDistance = ((SmartNpcSkills)smartNpc.skills).CurrentCastRange() * attackToMoveRangeRatio; Vector3 destination = Utils.ClosestPoint(smartNpc.target, smartNpc.transform.position); smartNpc.movement.Navigate(destination, stoppingDistance); return("MOVING"); } if (EventTargetEnteredSafeZone(smartNpc)) { // if our target entered the safe zone, we need to be really careful // to avoid kiting. // -> players could pull a smartNpc near a safe zone and then step in // and out of it before/after attacks without ever getting hit by // the smartNpc // -> running back to start won't help, can still kit while running // -> warping back to start won't help, we might accidentally placed // a smartNpc in attack range of a safe zone // -> the 100% secure way is to die and hide it immediately. many // popular MMOs do it the same way to avoid exploits. // => call Entity.OnDeath without rewards etc. and hide immediately smartNpc.OnDeath(); // no looting smartNpc.respawnTimeEnd = NetworkTime.time + smartNpc.respawnTime; // respawn in a while return("DEAD"); } if (EventSkillRequest(smartNpc)) { // we had a target in attack range before and trying to cast a skill // on it. check self (alive, mana, weapon etc.) and target Skill skill = smartNpc.skills.skills[smartNpc.skills.currentSkill]; if (smartNpc.skills.CastCheckSelf(skill)) { if (smartNpc.skills.CastCheckTarget(skill)) { // start casting smartNpc.skills.StartCast(skill); return("CASTING"); } else { // invalid target. clear the attempted current skill. smartNpc.target = null; smartNpc.skills.currentSkill = -1; return("IDLE"); } } else { // we can't cast this skill at the moment (cooldown/low mana/...) // -> clear the attempted current skill, but keep the target to // continue later smartNpc.skills.currentSkill = -1; return("IDLE"); } } if (EventAggro(smartNpc)) { // target in attack range. try to cast a first skill on it if (smartNpc.skills.skills.Count > 0) { smartNpc.skills.currentSkill = ((SmartNpcSkills)smartNpc.skills).NextSkill(); } else { Debug.LogError(name + " has no skills to attack with."); } return("IDLE"); } if (EventMoveRandomly(smartNpc)) { // walk to a random position in movement radius (from 'start') // note: circle y is 0 because we add it to start.y Vector2 circle2D = Random.insideUnitCircle * moveDistance; smartNpc.movement.Navigate(smartNpc.startPosition + new Vector3(circle2D.x, 0, circle2D.y), 0); return("MOVING"); } if (EventPatrolPath(smartNpc)) { Vector3 currentWaypoint = smartNpc.patrolPath.GetWaypoint(smartNpc.currentWaypointIndex); float distanceToWaypoint = Vector3.Distance(smartNpc.transform.position, currentWaypoint); bool atWaypoint = distanceToWaypoint < waypointTolerance; if (atWaypoint) { //smartNpc.SetTimeSinceArrivedAtWaypoint(0); smartNpc.CycleWaypoint(); Vector3 nextPosition = smartNpc.patrolPath.GetWaypoint(smartNpc.currentWaypointIndex); smartNpc.movement.Navigate(nextPosition, 0); return("MOVING"); } smartNpc.movement.Navigate(currentWaypoint, 0); return("MOVING"); } if (EventDeathTimeElapsed(smartNpc)) { } // don't care if (EventRespawnTimeElapsed(smartNpc)) { } // don't care if (EventMoveEnd(smartNpc)) { } // don't care if (EventSkillFinished(smartNpc)) { } // don't care if (EventTargetDisappeared(smartNpc)) { } // don't care return("IDLE"); // nothing interesting happened }
string UpdateServer_DEAD(SmartNpc smartNpc) { // events sorted by priority (e.g. target doesn't matter if we died) if (EventRespawnTimeElapsed(smartNpc)) { // respawn at the start position with full health, visibility, no loot smartNpc.gold = 0; smartNpc.inventory.slots.Clear(); smartNpc.Show(); smartNpc.movement.Warp(smartNpc.startPosition); // recommended over transform.position smartNpc.Revive(); return("IDLE"); } if (EventDeathTimeElapsed(smartNpc)) { // we were lying around dead for long enough now. // hide while respawning, or disappear forever if (smartNpc.respawn) { smartNpc.Hide(); } else { NetworkServer.Destroy(smartNpc.gameObject); } return("DEAD"); } if (EventSkillRequest(smartNpc)) { } // don't care if (EventSkillFinished(smartNpc)) { } // don't care if (EventMoveEnd(smartNpc)) { } // don't care if (EventTargetDisappeared(smartNpc)) { } // don't care if (EventTargetDied(smartNpc)) { } // don't care if (EventTargetTooFarToFollow(smartNpc)) { } // don't care if (EventTargetTooFarToAttack(smartNpc)) { } // don't care if (EventTargetEnteredSafeZone(smartNpc)) { } // don't care if (EventAggro(smartNpc)) { } // don't care if (EventMoveRandomly(smartNpc)) { } // don't care if (EventStunned(smartNpc)) { } // don't care if (EventDied(smartNpc)) { } // don't care, of course we are dead return("DEAD"); // nothing interesting happened }
public bool EventTargetTooFarToFollow(SmartNpc smartNpc) => smartNpc.target != null && Vector3.Distance(smartNpc.startPosition, Utils.ClosestPoint(smartNpc.target, smartNpc.transform.position)) > followDistance;
public bool EventPatrolPath(SmartNpc smartNpc) => patrolPath; //&& smartNpc.timeSinceArrivedAtWaypoint > waypointDwellTime;
public bool EventRespawnTimeElapsed(SmartNpc smartNpc) => smartNpc.state == "DEAD" && smartNpc.respawn && NetworkTime.time >= smartNpc.respawnTimeEnd;
string UpdateServer_CASTING(SmartNpc smartNpc) { // keep looking at the target for server & clients (only Y rotation) if (smartNpc.target) { smartNpc.movement.LookAtY(smartNpc.target.transform.position); } // events sorted by priority (e.g. target doesn't matter if we died) if (EventDied(smartNpc)) { // we died. return("DEAD"); } if (EventStunned(smartNpc)) { smartNpc.skills.CancelCast(); smartNpc.movement.Reset(); return("STUNNED"); } if (EventTargetDisappeared(smartNpc)) { // cancel if the target matters for this skill if (smartNpc.skills.skills[smartNpc.skills.currentSkill].cancelCastIfTargetDied) { smartNpc.skills.CancelCast(); smartNpc.target = null; return("IDLE"); } } if (EventTargetDied(smartNpc)) { // cancel if the target matters for this skill if (smartNpc.skills.skills[smartNpc.skills.currentSkill].cancelCastIfTargetDied) { smartNpc.skills.CancelCast(); smartNpc.target = null; return("IDLE"); } } if (EventTargetEnteredSafeZone(smartNpc)) { // cancel if the target matters for this skill if (smartNpc.skills.skills[smartNpc.skills.currentSkill].cancelCastIfTargetDied) { // if our target entered the safe zone, we need to be really careful // to avoid kiting. // -> players could pull a smartNpc near a safe zone and then step in // and out of it before/after attacks without ever getting hit by // the smartNpc // -> running back to start won't help, can still kit while running // -> warping back to start won't help, we might accidentally placed // a smartNpc in attack range of a safe zone // -> the 100% secure way is to die and hide it immediately. many // popular MMOs do it the same way to avoid exploits. // => call Entity.OnDeath without rewards etc. and hide immediately smartNpc.OnDeath(); // no looting smartNpc.respawnTimeEnd = NetworkTime.time + smartNpc.respawnTime; // respawn in a while return("DEAD"); } } if (EventSkillFinished(smartNpc)) { // finished casting. apply the skill on the target. smartNpc.skills.FinishCast(smartNpc.skills.skills[smartNpc.skills.currentSkill]); // did the target die? then clear it so that the smartNpc doesn't // run towards it if the target respawned // (target might be null if disappeared or targetless skill) if (smartNpc.target != null && smartNpc.target.health.current == 0) { smartNpc.target = null; } // go back to IDLE, reset current skill ((SmartNpcSkills)smartNpc.skills).lastSkill = smartNpc.skills.currentSkill; smartNpc.skills.currentSkill = -1; return("IDLE"); } if (EventDeathTimeElapsed(smartNpc)) { } // don't care if (EventRespawnTimeElapsed(smartNpc)) { } // don't care if (EventMoveEnd(smartNpc)) { } // don't care if (EventTargetTooFarToAttack(smartNpc)) { } // don't care, we were close enough when starting to cast if (EventTargetTooFarToFollow(smartNpc)) { } // don't care, we were close enough when starting to cast if (EventAggro(smartNpc)) { } // don't care, always have aggro while casting if (EventSkillRequest(smartNpc)) { } // don't care, that's why we are here if (EventMoveRandomly(smartNpc)) { } // don't care return("CASTING"); // nothing interesting happened }
public bool EventMoveRandomly(SmartNpc smartNpc) => Random.value <= moveProbability * Time.deltaTime;
// events ////////////////////////////////////////////////////////////////// public bool EventDeathTimeElapsed(SmartNpc smartNpc) => smartNpc.state == "DEAD" && NetworkTime.time >= smartNpc.deathTimeEnd;
string UpdateServer_MOVING(SmartNpc smartNpc) { // events sorted by priority (e.g. target doesn't matter if we died) if (EventDied(smartNpc)) { // we died. smartNpc.movement.Reset(); return("DEAD"); } if (EventStunned(smartNpc)) { smartNpc.movement.Reset(); return("STUNNED"); } if (EventMoveEnd(smartNpc)) { // we reached our destination. return("IDLE"); } if (EventTargetDied(smartNpc)) { // we had a target before, but it died now. clear it. smartNpc.target = null; smartNpc.skills.CancelCast(); smartNpc.movement.Reset(); return("IDLE"); } if (EventTargetTooFarToFollow(smartNpc)) { // we had a target before, but it's out of follow range now. // clear it and go back to start. don't stay here. smartNpc.target = null; smartNpc.skills.CancelCast(); smartNpc.movement.Navigate(smartNpc.startPosition, 0); return("MOVING"); } if (EventTargetTooFarToAttack(smartNpc)) { // we had a target before, but it's out of attack range now. // follow it. (use collider point(s) to also work with big entities) float stoppingDistance = ((SmartNpcSkills)smartNpc.skills).CurrentCastRange() * attackToMoveRangeRatio; Vector3 destination = Utils.ClosestPoint(smartNpc.target, smartNpc.transform.position); smartNpc.movement.Navigate(destination, stoppingDistance); return("MOVING"); } if (EventTargetEnteredSafeZone(smartNpc)) { // if our target entered the safe zone, we need to be really careful // to avoid kiting. // -> players could pull a smartNpc near a safe zone and then step in // and out of it before/after attacks without ever getting hit by // the smartNpc // -> running back to start won't help, can still kit while running // -> warping back to start won't help, we might accidentally placed // a smartNpc in attack range of a safe zone // -> the 100% secure way is to die and hide it immediately. many // popular MMOs do it the same way to avoid exploits. // => call Entity.OnDeath without rewards etc. and hide immediately smartNpc.OnDeath(); // no looting smartNpc.respawnTimeEnd = NetworkTime.time + smartNpc.respawnTime; // respawn in a while return("DEAD"); } if (EventAggro(smartNpc)) { // target in attack range. try to cast a first skill on it // (we may get a target while randomly wandering around) if (smartNpc.skills.skills.Count > 0) { smartNpc.skills.currentSkill = ((SmartNpcSkills)smartNpc.skills).NextSkill(); } else { Debug.LogError(name + " has no skills to attack with."); } smartNpc.movement.Reset(); return("IDLE"); } if (EventDeathTimeElapsed(smartNpc)) { } // don't care if (EventRespawnTimeElapsed(smartNpc)) { } // don't care if (EventSkillFinished(smartNpc)) { } // don't care if (EventTargetDisappeared(smartNpc)) { } // don't care if (EventSkillRequest(smartNpc)) { } // don't care, finish movement first if (EventMoveRandomly(smartNpc)) { } // don't care return("MOVING"); // nothing interesting happened }