private void SetSoundToFollow(SoundEffect sound, double interest) { CurrentSoundID = sound.ID; CurrentSoundInterestScore = interest; currentSound = sound; BlockedOnSoundTurns = 0; }
/// <summary> /// Main loop called on each turn when we move /// </summary> public override void ProcessTurn() { //If in pursuit state, continue to pursue enemy until it is dead (or creature itself is killed) [no FOV used after initial target selected] //TODO: add forget mode? Random rand = Game.Random; Point startOfTurnLocation = LocationMap; //RESTORE STATE AFTER SAVE //Creature references may be circular, and will crash serialization, so an index is used instead //Restore currentTarget reference from ID, in case we have reloaded if (currentTargetID == -1) { currentTarget = null; } else { currentTarget = Game.Dungeon.GetCreatureByUniqueID(currentTargetID); } //Restore lastAttackedByFromID if (LastAttackedByID == -1) { LastAttackedBy = null; } else { LastAttackedBy = Game.Dungeon.GetCreatureByUniqueID(LastAttackedByID); } //Restore sound if (CurrentSoundID == -1) { currentSound = null; } else { currentSound = Game.Dungeon.GetSoundByID(CurrentSoundID); if (currentSound == null) { ResetFollowingSound(); AIState = SimpleAIStates.Patrol; LogFile.Log.LogEntryDebug("Error restoring sound, resetting", LogDebugLevel.High); } } //Stunned creatures miss turns if (StunnedTurns > 0) { StunnedTurns--; LogFile.Log.LogEntryDebug(this.Representation + " is stunned for " + StunnedTurns + " more turns", LogDebugLevel.Low); ResetTurnsMoving(); return; } //TEST SLEEPING CREATURES //Sleeping is a Creature state that is used like an AI state //This is OK since we exit immediately //Creatures which sleep until seen (i.e. for ease of processing, not game effects) if (Sleeping && WakesOnBeingSeen()) { //Check to see if we should wake by looking for woken creatures in POV //(when we drop through currentFOV may be unnecessarily recalculated) CreatureFOV currentFOV = Game.Dungeon.CalculateCreatureFOV(Game.Dungeon.Player); //Player sees monster, wake up if (currentFOV.CheckTileFOV(LocationMap.x, LocationMap.y)) { Sleeping = false; AIState = SimpleAIStates.Patrol; LogFile.Log.LogEntryDebug(this.Representation + " spotted by player so wakes", LogDebugLevel.Low); } } //Sleeping creatures don't react until they see a woken creature if (Sleeping && WakesOnSight()) { //Check to see if we should wake by looking for woken creatures in POV //(when we drop through currentFOV may be unnecessarily recalculated) CreatureFOV currentFOV = Game.Dungeon.CalculateCreatureFOV(this); foreach (Monster monster in Game.Dungeon.Monsters) { //Same monster if (monster == this) continue; //Not on the same level if (monster.LocationLevel != this.LocationLevel) continue; //Not in FOV if (!currentFOV.CheckTileFOV(monster.LocationMap.x, monster.LocationMap.y)) continue; //Otherwise in FOV //Check if it's awake. If so, wake up and stop if (!monster.Sleeping) { Sleeping = false; AIState = SimpleAIStates.Patrol; LogFile.Log.LogEntryDebug(this.Representation + " spots awake " + monster.Representation + " and wakes", LogDebugLevel.Low); break; } } //Check if we can see the player if (Game.Dungeon.Player.LocationLevel == this.LocationLevel && currentFOV.CheckTileFOV(Game.Dungeon.Player.LocationMap.x, Game.Dungeon.Player.LocationMap.y) && !Game.Dungeon.Player.isStealthed()) { //In FOV wake Sleeping = false; AIState = SimpleAIStates.Patrol; LogFile.Log.LogEntryDebug(this.Representation + " spots player and wakes", LogDebugLevel.Low); } } //If we're still sleeping then skip this go if (Sleeping) { ResetTurnsMoving(); return; } //RETURNING - used when a charmed creature gets a long way from the PC if (AIState == SimpleAIStates.Returning) { //Don't stop on an attack otherwise charmed creatures will be frozen in front of missile troops //We have been attacked by someone new //if (LastAttackedBy != null && LastAttackedBy.Alive) //{ //Reset the AI, will drop through and chase the nearest target // AIState = SimpleAIStates.RandomWalk; //} //else { //Are we close enough to the PC? double distance = GetDistance(this, Game.Dungeon.Player); if (distance <= recoverDistance) { //Reset AI and fall through AIState = SimpleAIStates.Patrol; LogFile.Log.LogEntryDebug(this.Representation + " close enough to PC", LogDebugLevel.Low); } //Otherwise follow the PC back FollowPC(); //} } //PURSUIT MODES - Pursuit [active] and Fleeing [temporarily fleeing, will return to target] if (AIState == SimpleAIStates.Fleeing || AIState == SimpleAIStates.Pursuit) { Monster targetMonster = currentTarget as Monster; //Fleeing //Check we have a valid target (may not after reload) //still required? if (currentTarget == null) { AIState = SimpleAIStates.Patrol; } //Is target yet living? else if (currentTarget.Alive == false) { //If not, go to non-chase state AIState = SimpleAIStates.Patrol; } //Charmed creatures should not attack other charmed creatures else if (Charmed && targetMonster != null && targetMonster.Charmed) { //Go to non-chase state AIState = SimpleAIStates.Patrol; } //Is target on another level (i.e. has escaped down the stairs) else if (currentTarget.LocationLevel != this.LocationLevel) { AIState = SimpleAIStates.Patrol; } //Have we just become charmed? Reset AI (stop chasing player) else if (currentTarget == Game.Dungeon.Player && Charmed) { AIState = SimpleAIStates.Patrol; } //Have we just become passive? Reset AI (stop chasing player) else if (currentTarget == Game.Dungeon.Player && Passive) { AIState = SimpleAIStates.Patrol; } //Has the player stealthed? else if (currentTarget == Game.Dungeon.Player && Game.Dungeon.Player.isStealthed()) { LogFile.Log.LogEntryDebug(this.Representation + " stop chasing. Player went stealthed", LogDebugLevel.Medium); AIState = SimpleAIStates.Patrol; } //Have we just been attacked by a new enemy? else if (LastAttackedBy != null && LastAttackedBy.Alive && LastAttackedBy != currentTarget) { //Reset the AI for now AIState = SimpleAIStates.Patrol; } else { //Otherwise continue to pursue or flee ChaseCreature(currentTarget); } } //PATROL STATE OR INVESTIGATE STATE //Check states which override patrol or investigate (e.g being attacked, charmed, seeing the PC) if(AIState == SimpleAIStates.Patrol || AIState == SimpleAIStates.InvestigateSound) { Map currentMap = Game.Dungeon.Levels[LocationLevel]; //AI branches here depending on if we are charmed or passive if (this.Charmed) { //Charmed - will fight for the PC //Won't attack passive creatures (otherwise will de-passify them and it would be annoying) //Look for creatures in FOV CreatureFOV currentFOV = Game.Dungeon.CalculateCreatureFOV(this); //List will contain monsters & player List<Monster> monstersInFOV = new List<Monster>(); foreach (Monster monster in Game.Dungeon.Monsters) { //Same monster if (monster == this) continue; //Not on the same level if (monster.LocationLevel != this.LocationLevel) continue; //Not in FOV if (!currentFOV.CheckTileFOV(monster.LocationMap.x, monster.LocationMap.y)) continue; //Otherwise add to list of possible targets monstersInFOV.Add(monster); LogFile.Log.LogEntryDebug(this.Representation + " spots " + monster.Representation, LogDebugLevel.Low); } //Look for creatures which aren't passive or charmed List<Monster> notPassiveTargets = monstersInFOV.FindAll(x => !x.Passive); List<Monster> notCharmedOrPassiveTargets = notPassiveTargets.FindAll(x => !x.Charmed); //Go chase a not-passive, not-charmed creature if (notCharmedOrPassiveTargets.Count > 0) { //Find the closest creature Monster closestCreature = null; double closestDistance = Double.MaxValue; //a long way foreach (Monster creature in notCharmedOrPassiveTargets) { double distanceSq = Math.Pow(creature.LocationMap.x - this.LocationMap.x, 2) + Math.Pow(creature.LocationMap.y - this.LocationMap.y, 2); double distance = Math.Sqrt(distanceSq); if (distance < closestDistance) { closestDistance = distance; closestCreature = creature; } } //Start chasing this creature LogFile.Log.LogEntryDebug(this.Representation + " charm chases " + closestCreature.Representation, LogDebugLevel.Low); AIState = SimpleAIStates.Pursuit; ChaseCreature(closestCreature); } else { //No creature to chase, go find PC FollowPC(); } } else if(!Passive) { //Normal fighting behaviour //Optional: check next move and open any doors if possible. This gives us a chance to shoot lurking PCs //Removed this while closing doors is not possible - avoids repeated abuse /* if (CanOpenDoors() && (AIState == SimpleAIStates.Patrol || AIState == SimpleAIStates.InvestigateSound)) { //Very simple version, monsters open any doors they are facing List<Point> directedAhead = DirectionUtil.SurroundingPointsFromDirection(Heading, LocationMap, 3); foreach (Point p in directedAhead) { MapTerrain doorTerrain = Game.Dungeon.GetTerrainAtPoint(this.LocationLevel, p); if (doorTerrain == MapTerrain.ClosedDoor) { LogFile.Log.LogEntryDebug(this.Representation + " : door detected ahead, opening", LogDebugLevel.Medium); Game.Dungeon.OpenDoor(this.LocationLevel, p); } } }*/ //Find creatures & PC in FOV CreatureFOV currentFOV = Game.Dungeon.CalculateCreatureFOV(this); List<Creature> monstersInFOV = new List<Creature>(); foreach (Creature monster in Game.Dungeon.Monsters) { //Same monster if (monster == this) continue; //Not on the same level if (monster.LocationLevel != this.LocationLevel) continue; //Not in FOV if (!currentFOV.CheckTileFOV(monster.LocationMap.x, monster.LocationMap.y)) continue; //Otherwise add to list of possible targets monstersInFOV.Add(monster); LogFile.Log.LogEntryDebug(this.Representation + " spots " + monster.Representation, LogDebugLevel.Low); } if (Game.Dungeon.Player.LocationLevel == this.LocationLevel) { if (currentFOV.CheckTileFOV(Game.Dungeon.Player.LocationMap.x, Game.Dungeon.Player.LocationMap.y) && !Game.Dungeon.Player.isStealthed()) { monstersInFOV.Add(Game.Dungeon.Player); LogFile.Log.LogEntryDebug(this.Representation + " spots " + Game.Dungeon.Player.Representation, LogDebugLevel.Low); } } //Have we just been attacked by a new enemy? If so, respond to them if (LastAttackedBy != null && LastAttackedBy.Alive && LastAttackedBy != currentTarget) { //Is this target within FOV? If so, attack it if (monstersInFOV.Contains(LastAttackedBy)) { LogFile.Log.LogEntryDebug(this.Representation + " changes target to " + LastAttackedBy.Representation, LogDebugLevel.Medium); AIState = SimpleAIStates.Pursuit; ChaseCreature(LastAttackedBy); } else { //Continue chasing whoever it was we were chasing last if (currentTarget != null) { AIState = SimpleAIStates.Pursuit; ChaseCreature(currentTarget); } } } //Check if we can see the PC and pursue them //If we are not currently pursuing anything and we see the PC, pursue if seen //Technically, go into pursuit mode, which may not involve actual movement if ((AIState == SimpleAIStates.Patrol || AIState == SimpleAIStates.InvestigateSound) && monstersInFOV.Contains(Game.Dungeon.Player) && !Game.Dungeon.Player.isStealthed()) { Creature closestCreature = Game.Dungeon.Player; //Start chasing this creature LogFile.Log.LogEntryDebug(this.Representation + " chases " + closestCreature.Representation, LogDebugLevel.Medium); AIState = SimpleAIStates.Pursuit; ChaseCreature(closestCreature); } } //This is so we don't have to instrument each state if (LocationMap == startOfTurnLocation) { ResetTurnsMoving(); AddTurnsInactive(); } else { ResetTurnsInactive(); AddTurnsMoving(); } } //INVESTIGATE SOUNDS //If a new sound has happened, calculate interest //If high interest, make this our target //Continue to investigate the sound //If we've reached the target, return to Patrol [sightings are handled above] //Monster that don't pursue still have a chance to direct their FOVs at sounds bool moveFollowingSound = false; if ((AIState == SimpleAIStates.Patrol || AIState == SimpleAIStates.InvestigateSound) && WillInvestigateSounds() ) { double currentSoundInterest; if (CurrentSoundID == -1) { currentSoundInterest = 0.0; } else { //Interest in the last interesting sound will decay over time currentSoundInterest = currentSound.DecayedInterest(CurrentSoundInterestScore, Game.Dungeon.WorldClock); } //Get sounds which have happened since we last looked (and update) //TODO: reset this when we leave a pursuit state - could look at very old sounds then?? List<SoundEffect> newSounds = Game.Dungeon.GetSoundsAfterTime(LastCheckedSounds); LastCheckedSounds = Game.Dungeon.WorldClock; SoundEffect newSoundToFollow = null; int newSoundToFollowID = -1; double newSoundInterest = currentSoundInterest; foreach(SoundEffect soundEvent in newSounds) { SoundEffect sEffect = soundEvent; double newSoundScore = sEffect.DecayedMagnitude(this.LocationLevel, this.LocationMap); if (newSoundScore > newSoundInterest) { newSoundToFollowID = sEffect.ID; newSoundToFollow = sEffect; newSoundInterest = newSoundScore; } } //Have we found a new more interesting sound? //If so, follow it if (newSoundToFollowID != -1) { LogFile.Log.LogEntryDebug(this.Representation + " new sound target: " + newSoundToFollow + "[ int: " + newSoundInterest + "] (old: " + currentSound + " [ int: " + currentSoundInterest + "])", LogDebugLevel.Medium); //Change sound //A sound we choose gets a boost in interest to give us a bit of hystersis SetSoundToFollow(newSoundToFollow, newSoundInterest * 1.5); AIState = SimpleAIStates.InvestigateSound; } else { if (currentSoundInterest < 0.01) { //Sound has decayed so much it's not interesting, or we never had an interesting sound ResetFollowingSound(); if(AIState == SimpleAIStates.InvestigateSound) LogFile.Log.LogEntryDebug(this.Representation + " sound " + currentSound + " is old, resetting", LogDebugLevel.Low); AIState = SimpleAIStates.Patrol; } } //For a new or existing sound, pursue it if (AIState == SimpleAIStates.InvestigateSound) { moveFollowingSound = InvestigateSound(); } } //If nothing else happened, do the Patrol action //Don't if we moved in response to a sound if ((AIState == SimpleAIStates.Patrol && !moveFollowingSound) || (WillAlwaysPatrol() && !headingSetToSound)) { //We haven't got anything to do and we can't see the PC //Do normal movement DoPatrol(); } //Reset the skip-patrol if we looked at sound flag headingSetToSound = false; }
/// <summary> /// Reset sound state. Doesn't change AI /// </summary> private void ResetFollowingSound() { CurrentSoundID = -1; currentSound = null; CurrentSoundInterestScore = 0.0; }