public async Task<bool> Attack(int attack_idx, Actor a) { //returns true if attack hit if (await StunnedThisTurn()) { return false; } //pos pos_of_target = new pos(a.row,a.col); AttackInfo info = AttackList.Attack(atype, attack_idx); if (weapons[0] != WeaponType.NO_WEAPON) { info.damage = Weapon.Damage(weapons[0]); } info.damage.source = this; int plus_to_hit = TotalSkill(SkillType.COMBAT); bool sneak_attack = false; if (this.IsHiddenFrom(a) || !a.CanSee(this) || (this == player && HasAttr(AttrType.SHADOW_CLOAK) && !tile().IsLit() && !a.HasAttr(AttrType.BLINDSIGHT))) { sneak_attack = true; } if (sneak_attack) { //sneak attacks get +25% accuracy. this usually totals 100% vs. unarmored targets. plus_to_hit += 25; } if (HasAttr(AttrType.BLESSED)) { plus_to_hit += 10; } plus_to_hit -= a.ArmorClass() * 2; bool hit = a.IsHit(plus_to_hit); if (HasFeat(FeatType.DRIVE_BACK)) { bool nowhere_to_run = true; int dir = DirectionOf(a); if (a.TileInDirection(dir).passable && a.ActorInDirection(dir) == null) { nowhere_to_run = false; } if (a.TileInDirection(RotateDirection(dir, true)).passable && a.ActorInDirection(RotateDirection(dir, true)) == null) { nowhere_to_run = false; } if (a.TileInDirection(RotateDirection(dir, false)).passable && a.ActorInDirection(RotateDirection(dir, false)) == null) { nowhere_to_run = false; } if (a.HasAttr(AttrType.FROZEN) || a.HasAttr(AttrType.NEVER_MOVES)) { nowhere_to_run = true; } if (nowhere_to_run) { hit = true; } } bool no_armor_message = false; //no_armor_message means "don't print 'your armor blocks the attack' for misses" if (a.HasAttr(AttrType.DEFENSIVE_STANCE) && Global.CoinFlip()) { hit = false; no_armor_message = true; } if ((this.tile().Is(FeatureType.FOG) || a.tile().Is(FeatureType.FOG)) && Global.CoinFlip()) { hit = false; no_armor_message = true; } if (a.IsHiddenFrom(this) || !CanSee(a) || (a == player && a.HasAttr(AttrType.SHADOW_CLOAK) && !a.tile().IsLit() && !HasAttr(AttrType.BLINDSIGHT))) { if (Global.CoinFlip()) { hit = false; no_armor_message = true; } } bool player_in_combat = false; if (this == player || a == player) { player_in_combat = true; } if (attack_idx == 2 && (atype == ActorType.FROSTLING || atype == ActorType.FIRE_DRAKE)) { hit = true; //hack! these are the 2 'area' attacks that always hit player_in_combat = false; } if (a == player && atype == ActorType.DREAM_CLONE) { player_in_combat = false; } if (player_in_combat) { player.attrs[Forays.AttrType.IN_COMBAT]++; } string s = info.desc + ". "; if (hit) { if (HasFeat(FeatType.NECK_SNAP) && a.HasAttr(AttrType.MEDIUM_HUMANOID) && IsHiddenFrom(a)) { if (!HasAttr(AttrType.RESIST_NECK_SNAP)) { B.Add(You("silently snap") + " " + a.Your() + " neck. "); await a.TakeDamage(DamageType.NORMAL, DamageClass.NO_TYPE, 9001, this); Q1(); return true; } else { B.Add(You("silently snap") + " " + a.Your() + " neck. "); B.Add("It doesn't seem to affect " + a.the_name + ". "); } } int dice = info.damage.dice; bool crit = false; int pos = s.IndexOf("&"); if (pos != -1) { s = s.Substring(0, pos) + TheVisible() + s.Substring(pos + 1); } pos = s.IndexOf("^"); if (pos != -1) { string sc = ""; int critical_target = 20; if (weapons[0] == WeaponType.DAGGER) { critical_target -= 2; } if (HasFeat(FeatType.LETHALITY)) { //10% crit plus 5% for each 20% health the target is missing critical_target -= 2; int fifth = a.maxhp / 5; //uses int because it assumes everything has a multiple of 5hp int totaldamage = a.maxhp - a.curhp; if (fifth > 0) { int missing_fifths = totaldamage / fifth; critical_target -= missing_fifths; } } if ((info.damage.type == DamageType.NORMAL || info.damage.type == DamageType.PIERCING || info.damage.type == DamageType.BASHING || info.damage.type == DamageType.SLASHING) && Global.Roll(1, 20) >= critical_target) { //maybe this should become a check for physical damage - todo? crit = true; sc = "critically "; } s = s.Substring(0, pos) + sc + s.Substring(pos + 1); } pos = s.IndexOf("*"); if (pos != -1) { s = s.Substring(0, pos) + a.TheVisible() + s.Substring(pos + 1); } if (sneak_attack && crit) { if (!a.HasAttr(AttrType.UNDEAD) && !a.HasAttr(AttrType.CONSTRUCT) && !a.HasAttr(AttrType.PLANTLIKE) && !a.HasAttr(AttrType.BOSS_MONSTER)) { if (a.atype != ActorType.PLAYER) { //being nice to the player here... switch (weapons[0]) { case WeaponType.SWORD: case WeaponType.FLAMEBRAND: B.Add("You run " + a.TheVisible() + " through! "); break; case WeaponType.MACE: case WeaponType.MACE_OF_FORCE: B.Add("You bash " + a.YourVisible() + " head in! "); break; case WeaponType.DAGGER: case WeaponType.VENOMOUS_DAGGER: B.Add("You pierce one of " + a.YourVisible() + " vital organs! "); break; case WeaponType.STAFF: case WeaponType.STAFF_OF_MAGIC: B.Add("You bring your staff down on " + a.YourVisible() + " head with a loud crack! "); break; case WeaponType.BOW: case WeaponType.HOLY_LONGBOW: B.Add("You choke " + a.TheVisible() + " with your bowstring! "); break; default: break; } MakeNoise(); await a.TakeDamage(DamageType.NORMAL, DamageClass.NO_TYPE, 1337, this); Q1(); return true; } else { //...but not too nice B.Add(AVisible() + " strikes from hiding! "); B.Add("The deadly attack leaves you stunned! "); int lotsofdamage = Math.Max(dice * 6, a.curhp / 2); a.attrs[AttrType.STUNNED]++; Q.Add(new Event(a, Global.Roll(2, 5) * 100, AttrType.STUNNED, "You are no longer stunned. ")); await a.TakeDamage(DamageType.NORMAL, DamageClass.PHYSICAL, lotsofdamage, this, a_name); } } } if (sneak_attack) { B.Add(YouVisible("strike") + " from hiding! "); if (atype != ActorType.PLAYER) { attrs[AttrType.TURNS_VISIBLE] = -1; attrs[Forays.AttrType.NOTICED]++; } else { a.player_visibility_duration = -1; a.attrs[Forays.AttrType.PLAYER_NOTICED]++; } } B.Add(s, this, a); int dmg; if (crit) { dmg = dice * 6; } else { dmg = Global.Roll(dice, 6); } dmg += TotalSkill(SkillType.COMBAT); int r = a.row; int c = a.col; bool troll = (a.atype == ActorType.TROLL || a.atype == ActorType.TROLL_SEER); bool mech_shield = a.HasAttr(AttrType.MECHANICAL_SHIELD); if (crit && mech_shield) { a.attrs[Forays.AttrType.MECHANICAL_SHIELD] = 0; } await a.TakeDamage(info.damage.type, info.damage.damclass, dmg, this, a_name); if (crit && mech_shield) { a.attrs[Forays.AttrType.MECHANICAL_SHIELD]++; } if (M.actor[r, c] != null) { if (HasAttr(AttrType.FIRE_HIT) || attrs[AttrType.ON_FIRE] >= 3) { //todo: a frostling's ranged attack shouldn't apply this if (!a.HasAttr(AttrType.INVULNERABLE)) { //to prevent the message int amount = Global.Roll(6); if (!a.HasAttr(AttrType.RESIST_FIRE) || amount / a.attrs[AttrType.RESIST_FIRE] > 0) { //todo i think resistance is wrong here B.Add(a.YouAre() + " burned. ", a); } await a.TakeDamage(DamageType.FIRE, DamageClass.PHYSICAL, amount, this, a_name); } } } if (troll && HasAttr(AttrType.FIRE_HIT) && M.tile[r, c].Is(FeatureType.TROLL_CORPSE)) { M.tile[r, c].features.Remove(FeatureType.TROLL_CORPSE); B.Add("The troll corpse burns to ashes! ", M.tile[r, c]); } if (troll && HasAttr(AttrType.FIRE_HIT) && M.tile[r, c].Is(FeatureType.TROLL_SEER_CORPSE)) { M.tile[r, c].features.Remove(FeatureType.TROLL_SEER_CORPSE); B.Add("The troll seer corpse burns to ashes! ", M.tile[r, c]); } if (HasAttr(AttrType.COLD_HIT) && attack_idx == 0 && M.actor[r, c] != null) { //hack: only applies to attack 0 if (!a.HasAttr(AttrType.INVULNERABLE)) { //to prevent the message B.Add(a.YouAre() + " chilled. ", a); await a.TakeDamage(DamageType.COLD, DamageClass.PHYSICAL, Global.Roll(1, 6), this, a_name); } } if (HasAttr(AttrType.POISON_HIT) && M.actor[r, c] != null) { if (!a.HasAttr(AttrType.UNDEAD) && !a.HasAttr(AttrType.CONSTRUCT) && !a.HasAttr(AttrType.POISON_HIT) && !a.HasAttr(AttrType.IMMUNE_TOXINS)) { if (a.HasAttr(AttrType.POISONED)) { B.Add(a.YouAre() + " more poisoned. ", a); } else { B.Add(a.YouAre() + " poisoned. ", a); } a.attrs[AttrType.POISONED]++; Q.Add(new Event(a, (Global.Roll(6) + 6) * 100, AttrType.POISONED)); } } if (HasAttr(AttrType.PARALYSIS_HIT) && attack_idx == 1 && atype == ActorType.CARRION_CRAWLER && M.actor[r, c] != null) { if (!a.HasAttr(AttrType.IMMUNE_TOXINS)) { //hack: carrion crawler only B.Add(a.YouAre() + " paralyzed. ", a); a.attrs[AttrType.PARALYZED] = Global.Roll(1, 3) + 3; } } if (HasAttr(AttrType.FORCE_HIT) && M.actor[r, c] != null) { if (Global.OneIn(3)) { if (Global.CoinFlip()) { await a.GetKnockedBack(this); } else { if (!a.HasAttr(AttrType.STUNNED)) { B.Add(a.YouAre() + " stunned. ", a); a.attrs[AttrType.STUNNED]++; int duration = (Global.Roll(4) + 3) * 100; if (crit) { duration += 250; crit = false; //note this - don't try to use crit again after this on-hit stuff. } Q.Add(new Event(a, duration, AttrType.STUNNED, a.YouAre() + " no longer stunned. ", new PhysicalObject[] { a })); } } } } if (HasAttr(AttrType.DIM_VISION_HIT) && M.actor[r, c] != null) { string str = ""; if (a.atype == ActorType.PLAYER) { B.Add("Your vision grows weak. "); str = "Your vision returns to normal. "; } //a.attrs[AttrType.DIM_VISION]++; //Q.Add(new Event(a,a.DurationOfMagicalEffect(Global.Roll(2,20)+20)*100,AttrType.DIM_VISION,str)); a.GainAttrRefreshDuration(AttrType.DIM_VISION, a.DurationOfMagicalEffect(Global.Roll(2, 20) + 20) * 100, str); } if (HasAttr(AttrType.STALAGMITE_HIT)) { List<Tile> tiles = new List<Tile>(); foreach (Tile t in M.tile[r, c].TilesWithinDistance(1)) { if (t.actor() == null && (t.ttype == TileType.FLOOR || t.ttype == TileType.STALAGMITE)) { if (Global.CoinFlip()) { //50% for each... tiles.Add(t); } } } foreach (Tile t in tiles) { if (t.ttype == TileType.STALAGMITE) { Q.KillEvents(t, EventType.STALAGMITE); } else { t.Toggle(this, TileType.STALAGMITE); } } Q.Add(new Event(tiles, 150, EventType.STALAGMITE)); } if (HasAttr(AttrType.GRAB_HIT) && M.actor[r, c] != null && !HasAttr(AttrType.GRABBING) && DistanceFrom(a) == 1) { a.attrs[Forays.AttrType.GRABBED]++; attrs[Forays.AttrType.GRABBING] = DirectionOf(a); B.Add(the_name + " grabs " + a.the_name + ". ", this, a); } if (HasAttr(AttrType.LIFE_DRAIN_HIT) && curhp < maxhp) { curhp += 10; if (curhp > maxhp) { curhp = maxhp; } B.Add(YouFeel() + " restored. ", this); } if (HasAttr(AttrType.STUN_HIT) && M.actor[r, c] != null) { B.Add(a.YouAre() + " stunned. ", a); int duration = 550; if (crit) { duration += 250; crit = false; } a.GainAttrRefreshDuration(AttrType.STUNNED, duration, a.YouAre() + " no longer stunned. ", a); } if (crit && M.actor[r, c] != null) { B.Add(a.YouAre() + " stunned. ", a); a.GainAttrRefreshDuration(AttrType.STUNNED, 250, a.YouAre() + " no longer stunned. ", a); } if (M.actor[r, c] != null && a.atype == ActorType.SWORDSMAN) { if (a.attrs[AttrType.BONUS_COMBAT] > 0) { B.Add(a.the_name + " returns to a defensive stance. ", a); a.attrs[AttrType.BONUS_COMBAT] = 0; } a.attrs[AttrType.COOLDOWN_1]++; Q.Add(new Event(a, 100, AttrType.COOLDOWN_1)); } } else { if (a.HasAttr(AttrType.DEFENSIVE_STANCE) || (a.HasFeat(FeatType.FULL_DEFENSE) && Global.CoinFlip())) { //make an attack against a random enemy next to a List<Actor> list = a.ActorsWithinDistance(1, true); list.Remove(this); //don't consider yourself or the original target if (list.Count > 0) { B.Add(a.You("deflect") + " the attack. ", this, a); return await Attack(attack_idx, list[Global.Roll(1, list.Count) - 1]); } //this would currently enter an infinite loop if two adjacent things used it at the same time } if (this == player || a == player || player.CanSee(this) || player.CanSee(a)) { //didn't change this yet if (s == "& lunges forward and ^hits *. ") { B.Add(the_name + " lunges forward and misses " + a.the_name + ". "); } else { if (s == "& hits * with a blast of cold. ") { B.Add(the_name + " nearly hits " + a.the_name + " with a blast of cold. "); } else { if (s.Length >= 20 && s.Substring(0, 20) == "& extends a tentacle") { B.Add(the_name + " misses " + a.the_name + " with a tentacle. "); } else { if (HasFeat(FeatType.DRIVE_BACK)) { B.Add(You("drive") + " " + a.TheVisible() + " back. "); } else { if (a.ArmorClass() > 0 && !no_armor_message) { if (a.atype != ActorType.PLAYER) { B.Add(a.YourVisible() + " armor blocks " + YourVisible() + " attack. "); } else { int miss_chance = 25 - plus_to_hit; if (Global.Roll(miss_chance) <= Armor.Protection(a.armors[0]) * 2) { B.Add(a.YourVisible() + " armor blocks " + YourVisible() + " attack. "); } else { B.Add(YouVisible("miss", true) + " " + a.TheVisible() + ". "); } } } else { B.Add(YouVisible("miss", true) + " " + a.TheVisible() + ". "); } } } } } } if (HasFeat(FeatType.DRIVE_BACK)) { if (!a.HasAttr(AttrType.FROZEN) && !HasAttr(AttrType.FROZEN)) { await a.AI_Step(this, true); await AI_Step(a); } } if (a.atype == ActorType.SWORDSMAN) { if (a.attrs[AttrType.BONUS_COMBAT] > 0) { B.Add(a.the_name + " returns to a defensive stance. ", a); a.attrs[AttrType.BONUS_COMBAT] = 0; } a.attrs[AttrType.COOLDOWN_1]++; Q.Add(new Event(a, 100, AttrType.COOLDOWN_1)); } } MakeNoise(); Q.Add(new Event(this, info.cost)); return hit; }
public void ActiveAI() { if(path.Count > 0){ path.Clear(); } if(!HasAttr(AttrType.AGGRESSION_MESSAGE_PRINTED)){ PrintAggressionMessage(); } switch(type){ case ActorType.GIANT_BAT: case ActorType.PHANTOM_BLIGHTWING: if(DistanceFrom(target) == 1){ int idx = R.Roll(1,2) - 1; Attack(idx,target); if(target != null && R.CoinFlip()){ //chance of retreating AI_Step(target,true); } } else{ if(R.CoinFlip()){ AI_Step(target); QS(); } else{ AI_Step(TileInDirection(Global.RandomDirection())); QS(); } } break; case ActorType.BLOOD_MOTH: { Tile brightest = null; if(!M.wiz_dark && !M.wiz_lite && !HasAttr(AttrType.BLIND)){ List<Tile> valid = M.AllTiles().Where(x=>x.light_value > 0 && CanSee(x)); valid = valid.WhereGreatest(x=>{ int result = x.light_radius; if(x.Is(FeatureType.FIRE) && result == 0){ result = 1; } if(x.inv != null && x.inv.light_radius > result){ result = x.inv.light_radius; } if(x.actor() != null && x.actor().LightRadius() > result){ result = x.actor().LightRadius(); } return result; }); valid = valid.WhereLeast(x=>DistanceFrom(x)); if(valid.Count > 0){ brightest = valid.RandomOrDefault(); } } if(brightest != null){ if(DistanceFrom(brightest) <= 1){ if(target != null && brightest == target.tile()){ Attack(0,target); if(target == player && player.curhp > 0){ Help.TutorialTip(TutorialTopic.Torch); } } else{ List<Tile> open = new List<Tile>(); foreach(Tile t in TilesAtDistance(1)){ if(t.DistanceFrom(brightest) <= 1 && t.passable && t.actor() == null){ open.Add(t); } } if(open.Count > 0){ AI_Step(open.Random()); } QS(); } } else{ List<Tile> tiles = new List<Tile>(); if(brightest.row == row || brightest.col == col){ int targetdir = DirectionOf(brightest); for(int i=-1;i<=1;++i){ pos adj = p.PosInDir(targetdir.RotateDir(true,i)); if(M.tile[adj].passable && M.actor[adj] == null){ tiles.Add(M.tile[adj]); } } } if(tiles.Count > 0){ AI_Step(tiles.Random()); } else{ AI_Step(brightest); } QS(); } } else{ int dir = Global.RandomDirection(); if(!TileInDirection(dir).passable && TilesAtDistance(1).Where(t => !t.passable).Count > 4){ dir = Global.RandomDirection(); } if(TileInDirection(dir).passable && ActorInDirection(dir) == null){ AI_Step(TileInDirection(dir)); QS(); } else{ if(curhp < maxhp && target != null && ActorInDirection(dir) == target){ Attack(0,target); } else{ if(player.HasLOS(TileInDirection(dir)) && player.HasLOS(this)){ if(!TileInDirection(dir).passable){ B.Add(the_name + " brushes up against " + TileInDirection(dir).the_name + ". ",this); } else{ if(ActorInDirection(dir) != null){ B.Add(the_name + " brushes up against " + ActorInDirection(dir).TheName(true) + ". ",this); } } } QS(); } } } /*PhysicalObject brightest = null; if(!M.wiz_lite && !M.wiz_dark){ List<PhysicalObject> current_brightest = new List<PhysicalObject>(); foreach(Tile t in M.AllTiles()){ int pos_radius = t.light_radius; PhysicalObject pos_obj = t; if(t.Is(FeatureType.FIRE) && pos_radius == 0){ pos_radius = 1; } if(t.inv != null && t.inv.light_radius > pos_radius){ pos_radius = t.inv.light_radius; pos_obj = t.inv; } if(t.actor() != null && t.actor().LightRadius() > pos_radius){ pos_radius = t.actor().LightRadius(); pos_obj = t.actor(); } if(pos_radius > 0){ if(current_brightest.Count == 0 && CanSee(t)){ current_brightest.Add(pos_obj); } else{ foreach(PhysicalObject o in current_brightest){ int object_radius = o.light_radius; if(o is Actor){ object_radius = (o as Actor).LightRadius(); } if(object_radius == 0 && o is Tile && (o as Tile).Is(FeatureType.FIRE)){ object_radius = 1; } if(pos_radius > object_radius){ if(CanSee(t)){ current_brightest.Clear(); current_brightest.Add(pos_obj); break; } } else{ if(pos_radius == object_radius && DistanceFrom(t) < DistanceFrom(o)){ if(CanSee(t)){ current_brightest.Clear(); current_brightest.Add(pos_obj); break; } } else{ if(pos_radius == object_radius && DistanceFrom(t) == DistanceFrom(o) && pos_obj == player){ if(CanSee(t)){ current_brightest.Clear(); current_brightest.Add(pos_obj); break; } } } } } } } } if(current_brightest.Count > 0){ brightest = current_brightest.Random(); } } if(brightest != null){ if(DistanceFrom(brightest) <= 1){ if(brightest == target){ Attack(0,target); if(target == player && player.curhp > 0){ Help.TutorialTip(TutorialTopic.Torch); } } else{ List<Tile> open = new List<Tile>(); foreach(Tile t in TilesAtDistance(1)){ if(t.DistanceFrom(brightest) <= 1 && t.passable && t.actor() == null){ open.Add(t); } } if(open.Count > 0){ AI_Step(open.Random()); } QS(); } } else{ AI_Step(brightest); QS(); } } else{ int dir = Global.RandomDirection(); if(TilesAtDistance(1).Where(t => !t.passable).Count > 4 && !TileInDirection(dir).passable){ dir = Global.RandomDirection(); } if(TileInDirection(dir).passable && ActorInDirection(dir) == null){ AI_Step(TileInDirection(dir)); QS(); } else{ if(curhp < maxhp && target != null && ActorInDirection(dir) == target){ Attack(0,target); } else{ if(player.HasLOS(TileInDirection(dir)) && player.HasLOS(this)){ if(!TileInDirection(dir).passable){ B.Add(the_name + " brushes up against " + TileInDirection(dir).the_name + ". ",this); } else{ if(ActorInDirection(dir) != null){ B.Add(the_name + " brushes up against " + ActorInDirection(dir).TheName(true) + ". ",this); } } } QS(); } } }*/ break; } case ActorType.CARNIVOROUS_BRAMBLE: case ActorType.MUD_TENTACLE: if(DistanceFrom(target) == 1){ Attack(0,target); if(target == player && player.curhp > 0){ Help.TutorialTip(TutorialTopic.RangedAttacks); } } else{ QS(); } break; case ActorType.FROSTLING: { if(DistanceFrom(target) == 1){ if(R.CoinFlip()){ Attack(0,target); } else{ if(AI_Step(target,true)){ QS(); } else{ Attack(0,target); } } } else{ if(FirstActorInLine(target) == target && !HasAttr(AttrType.COOLDOWN_1) && DistanceFrom(target) <= 6){ int cooldown = R.Roll(1,4); if(cooldown != 1){ RefreshDuration(AttrType.COOLDOWN_1,cooldown*100); } AnimateBoltProjectile(target,Color.RandomIce); if(R.CoinFlip()){ B.Add(TheName(true) + " hits " + target.the_name + " with a blast of cold. ",target); target.TakeDamage(DamageType.COLD,DamageClass.PHYSICAL,R.Roll(2,6),this,"a frostling"); } else{ B.Add(TheName(true) + " misses " + target.the_name + " with a blast of cold. ",target); } foreach(Tile t in GetBestLineOfEffect(target)){ t.ApplyEffect(DamageType.COLD); } Q1(); } else{ if(!HasAttr(AttrType.COOLDOWN_2)){ AI_Step(target); } else{ AI_Sidestep(target); //message for this? hmm. } QS(); } } break; } case ActorType.SWORDSMAN: case ActorType.PHANTOM_SWORDMASTER: if(DistanceFrom(target) == 1){ pos target_pos = target.p; Attack(0,target); if(target != null && target.p.Equals(target_pos)){ List<Tile> valid_dirs = new List<Tile>(); foreach(Tile t in target.TilesAtDistance(1)){ if(t.passable && t.actor() == null && DistanceFrom(t) == 1){ valid_dirs.Add(t); } } if(valid_dirs.Count > 0){ AI_Step(valid_dirs.Random()); } } } else{ attrs[AttrType.COMBO_ATTACK] = 0; AI_Step(target); QS(); } break; case ActorType.DREAM_WARRIOR: if(DistanceFrom(target) == 1){ if(curhp <= 10 && !HasAttr(AttrType.COOLDOWN_1)){ //todo: changed to 20hp and a 10hp threshold...better? attrs[AttrType.COOLDOWN_1]++; List<Tile> openspaces = new List<Tile>(); foreach(Tile t in target.TilesAtDistance(1)){ if(t.passable && t.actor() == null){ openspaces.Add(t); } } foreach(Tile t in openspaces){ if(group == null){ group = new List<Actor>{this}; } Create(ActorType.DREAM_WARRIOR_CLONE,t.row,t.col,TiebreakerAssignment.InsertAfterCurrent); t.actor().player_visibility_duration = -1; t.actor().attrs[AttrType.NO_ITEM]++; group.Add(M.actor[t.row,t.col]); M.actor[t.row,t.col].group = group; group.Randomize(); } openspaces.Add(tile()); Tile newtile = openspaces[R.Roll(openspaces.Count)-1]; if(newtile != tile()){ Move(newtile.row,newtile.col,false); } if(openspaces.Count > 1){ B.Add(the_name + " is suddenly standing all around " + target.the_name + ". ",this,target); Q1(); } else{ Attack(0,target); } } else{ Attack(0,target); } } else{ AI_Step(target); QS(); } break; case ActorType.SPITTING_COBRA: if(DistanceFrom(target) <= 3 && !HasAttr(AttrType.COOLDOWN_1) && FirstActorInLine(target) == target){ RefreshDuration(AttrType.COOLDOWN_1,R.Between(50,75)*100); B.Add(TheName(true) + " spits poison in " + target.YourVisible() + " eyes! ",this,target); AnimateBoltProjectile(target,Color.DarkGreen); if(!target.HasAttr(AttrType.NONLIVING)){ target.ApplyStatus(AttrType.BLIND,R.Between(5,8)*100); /*B.Add(target.YouAre() + " blind! ",target); target.RefreshDuration(AttrType.BLIND,R.Between(5,8)*100,target.YouAre() + " no longer blinded. ",target);*/ } Q1(); } else{ if(DistanceFrom(target) == 1){ Attack(0,target); } else{ List<Tile> tiles = new List<Tile>(); if(target.row == row || target.col == col){ int targetdir = DirectionOf(target); for(int i=-1;i<=1;++i){ pos adj = p.PosInDir(targetdir.RotateDir(true,i)); if(M.tile[adj].passable && M.actor[adj] == null){ tiles.Add(M.tile[adj]); } } } if(tiles.Count > 0){ AI_Step(tiles.Random()); } else{ AI_Step(target); } QS(); } } break; case ActorType.KOBOLD: if(!HasAttr(AttrType.COOLDOWN_1)){ if(DistanceFrom(target) > 12){ AI_Step(target); QS(); } else{ if(FirstActorInLine(target) != target){ AI_Sidestep(target); QS(); } else{ attrs[AttrType.COOLDOWN_1]++; AnimateBoltProjectile(target,Color.DarkCyan,30); if(player.CanSee(this)){ B.Add(the_name + " fires a dart at " + target.the_name + ". ",this,target); } else{ B.Add("A dart hits " + target.the_name + "! ",target); if(player.CanSee(tile()) && !IsInvisibleHere()){ attrs[AttrType.TURNS_VISIBLE] = -1; attrs[AttrType.NOTICED] = 1; B.Add("You spot " + the_name + " that fired it. ",this); //B.Add("You notice " + a_name + ". ",tile()); } } if(target.TakeDamage(DamageType.NORMAL,DamageClass.PHYSICAL,R.Roll(6),this,"a kobold's dart")){ target.ApplyStatus(AttrType.VULNERABLE,R.Between(2,4)*100); /*if(!target.HasAttr(AttrType.VULNERABLE)){ B.Add(target.YouFeel() + " vulnerable. ",target); } target.RefreshDuration(AttrType.VULNERABLE,R.Between(2,4)*100,target.YouFeel() + " less vulnerable. ",target);*/ if(target == player){ Help.TutorialTip(TutorialTopic.Vulnerable); } } Q1(); } } } else{ if(DistanceFrom(target) <= 2){ AI_Flee(); QS(); } else{ B.Add(the_name + " starts reloading. ",this); attrs[AttrType.COOLDOWN_1] = 0; Q1(); RefreshDuration(AttrType.COOLDOWN_2,R.Between(5,6)*100 - 50); //Q.Add(new Event(this,R.Between(5,6)*100,EventType.MOVE)); } } break; case ActorType.SPORE_POD: if(DistanceFrom(target) == 1){ TakeDamage(DamageType.NORMAL,DamageClass.PHYSICAL,100,null); } else{ AI_Step(target); QS(); } break; case ActorType.FORASECT: { bool burrow = false; if((curhp * 2 <= maxhp || DistanceFrom(target) > 6) && R.CoinFlip()){ burrow = true; } if(DistanceFrom(target) <= 6 && DistanceFrom(target) > 1){ if(R.OneIn(10)){ burrow = true; } } if(burrow && !HasAttr(AttrType.COOLDOWN_1)){ RefreshDuration(AttrType.COOLDOWN_1,R.Between(8,11)*100); if(curhp * 2 <= maxhp){ Burrow(TilesWithinDistance(6)); } else{ Burrow(GetCone(DirectionOf(target),6,true)); } } else{ if(DistanceFrom(target) == 1){ Attack(0,target); } else{ AI_Step(target); QS(); } } break; } case ActorType.POLTERGEIST: if(inv.Count == 0){ if(DistanceFrom(target) == 1){ pos target_p = target.p; if(Attack(0,target) && M.actor[target_p] != null && M.actor[target_p].inv.Any(i=>!i.do_not_stack)){ target = M.actor[target_p]; Item item = target.inv.Where(i=>!i.do_not_stack).Random(); if(item.quantity > 1){ inv.Add(new Item(item,-1,-1)); item.quantity--; B.Add(YouVisible("steal") + " " + target.YourVisible() + " " + inv[0].Name() + "! ",this,target); } else{ inv.Add(item); target.inv.Remove(item); B.Add(YouVisible("steal") + " " + target.YourVisible() + " " + item.Name() + "! ",this,target); } } } else{ AI_Step(target); QS(); } } else{ attrs[AttrType.KEEPS_DISTANCE] = 1; List<Tile> line = target.GetBestExtendedLineOfEffect(this); Tile next = null; bool found = false; foreach(Tile t in line){ if(found){ next = t; break; } else{ if(t.actor() == this){ found = true; } } } if(next != null){ if(next.passable && next.actor() == null && AI_Step(next)){ QS(); } else{ if(!next.passable){ B.Add(the_name + " disappears into " + next.the_name + ". ",this); foreach(Tile t in TilesWithinDistance(1)){ if(t.DistanceFrom(next) == 1 && t.name == "floor"){ t.AddFeature(FeatureType.SLIME); } } Event e = null; foreach(Event e2 in Q.list){ if(e2.target == this && e2.type == EventType.POLTERGEIST){ e = e2; break; } } if(e != null){ e.target = inv[0]; Actor.tiebreakers[e.tiebreaker] = null; } inv.Clear(); Kill(); } else{ if(next.actor() != null){ if(!next.actor().HasAttr(AttrType.IMMOBILE)){ Move(next.row,next.col); QS(); } else{ if(next.actor().HasAttr(AttrType.IMMOBILE)){ if(AI_Step(next)){ QS(); } else{ if(DistanceFrom(target) == 1){ Attack(1,target); } else{ QS(); } } } } } else{ QS(); } } } } } break; case ActorType.CULTIST: case ActorType.FINAL_LEVEL_CULTIST: if(curhp <= 10 && !HasAttr(AttrType.COOLDOWN_1)){ attrs[AttrType.COOLDOWN_1]++; string invocation; switch(R.Roll(4)){ case 1: invocation = "ae vatra kersai"; break; case 2: invocation = "kersai dzaggath"; break; case 3: invocation = "od fir od bahgal"; break; case 4: invocation = "denei kersai nammat"; break; default: invocation = "denommus pilgni"; break; } if(R.CoinFlip()){ B.Add(You("whisper") + " '" + invocation + "'. ",this); } else{ B.Add(You("scream") + " '" + invocation.ToUpper() + "'. ",this); } if(HasAttr(AttrType.SLIMED)){ B.Add("Nothing happens. ",this); } else{ B.Add("Flames erupt from " + the_name + ". ",this); AnimateExplosion(this,1,Color.RandomFire,'*'); ApplyBurning(); foreach(Tile t in TilesWithinDistance(1)){ t.ApplyEffect(DamageType.FIRE); if(t.actor() != null){ t.actor().ApplyBurning(); } } } Q1(); } else{ if(DistanceFrom(target) == 1){ Attack(0,target); } else{ AI_Step(target); QS(); } } break; case ActorType.GOBLIN_ARCHER: case ActorType.PHANTOM_ARCHER: switch(DistanceFrom(target)){ case 1: /*if(target.EnemiesAdjacent() > 1){ Attack(0,target); } else{*/ if(AI_Flee()){ QS(); } else{ Attack(0,target); } //} break; case 2: if(FirstActorInLine(target) == target){ FireArrow(target); } else{ if(AI_Flee()){ QS(); } else{ if(AI_Sidestep(target)){ B.Add(the_name + " tries to line up a shot. ",this); } QS(); } } break; case 3: case 4: case 5: case 6: case 7: case 8: if(FirstActorInLine(target) == target){ FireArrow(target); } else{ if(AI_Sidestep(target)){ B.Add(the_name + " tries to line up a shot. ",this); } QS(); } break; default: AI_Step(target); QS(); break; } break; case ActorType.GOBLIN_SHAMAN: { if(SilencedThisTurn()){ return; } if(DistanceFrom(target) == 1){ if(exhaustion > 50){ Attack(0,target); } else{ CastCloseRangeSpellOrAttack(target); } } else{ if(DistanceFrom(target) > 12){ AI_Step(target); QS(); } else{ if(FirstActorInLine(target) != target || R.CoinFlip()){ AI_Step(target); QS(); } else{ CastRangedSpellOrMove(target); } } } break; } case ActorType.PHASE_SPIDER: if(DistanceFrom(target) == 1){ Attack(0,target); } else{ Tile t = target.TilesAtDistance(DistanceFrom(target)-1).Where(x=>x.passable && x.actor() == null).RandomOrDefault(); if(t != null){ Move(t.row,t.col); } QS(); } break; case ActorType.ZOMBIE: case ActorType.PHANTOM_ZOMBIE: if(DistanceFrom(target) == 1){ Attack(0,target); } else{ AI_Step(target); if(DistanceFrom(target) == 1){ Attack(1,target); } else{ QS(); } } break; case ActorType.ROBED_ZEALOT: if(HasAttr(AttrType.COOLDOWN_3)){ if(DistanceFrom(target) <= 12 && HasLOS(target)){ target.AnimateExplosion(target,1,Color.Yellow,'*'); B.Add(YouVisible("smite") + " " + target.the_name + "! ",target); int amount = target.curhp / 10; bool still_alive = target.TakeDamage(DamageType.MAGIC,DamageClass.MAGICAL,Math.Max(amount,1),this,"a zealot's wrath"); attrs[AttrType.COOLDOWN_3]--; attrs[AttrType.DETECTING_MONSTERS]--; if(!HasAttr(AttrType.COOLDOWN_3)){ B.Add(YouVisible("stop") + " praying. "); if(still_alive && target.EquippedWeapon.type != WeaponType.NO_WEAPON && !target.EquippedWeapon.status[EquipmentStatus.MERCIFUL]){ target.EquippedWeapon.status[EquipmentStatus.MERCIFUL] = true; B.Add(target.You("feel") + " a strange power enter " + target.Your() + " " + target.EquippedWeapon.NameWithoutEnchantment() + "! ",target); B.PrintAll(); Help.TutorialTip(TutorialTopic.Merciful); } } } else{ attrs[AttrType.COOLDOWN_3]--; attrs[AttrType.DETECTING_MONSTERS]--; } Q1(); } else{ if(!HasAttr(AttrType.COOLDOWN_1)){ attrs[AttrType.COOLDOWN_1] = maxhp; //initialize this value here instead of complicating the spawning code } if(DistanceFrom(target) <= 12 && !HasAttr(AttrType.COOLDOWN_2) && curhp < attrs[AttrType.COOLDOWN_1]){ //if the ability is ready and additional damage has been taken... RefreshDuration(AttrType.COOLDOWN_2,R.Between(11,13)*100); attrs[AttrType.COOLDOWN_1] = curhp; attrs[AttrType.COOLDOWN_3] = 4; attrs[AttrType.DETECTING_MONSTERS] = 4; B.Add(YouVisible("start") + " praying. "); B.Add(the_name + " points directly at you. ",this); Q1(); } else{ if(DistanceFrom(target) == 1){ Attack(0,target); } else{ AI_Step(target); QS(); } } } /*if(HasAttr(AttrType.COOLDOWN_2)){ attrs[AttrType.COOLDOWN_2] = 0; B.Add(the_name + " finishes the prayer. ",this); if(DistanceFrom(target) == 1 && target.EquippedWeapon.type != WeaponType.NO_WEAPON){ target.EquippedWeapon.status[EquipmentStatus.MERCIFUL] = true; B.Add("You feel a strange power enter " + target.Your() + " " + target.EquippedWeapon.NameWithoutEnchantment() + "! ",target); B.PrintAll(); Help.TutorialTip(TutorialTopic.Merciful); } Q1(); } else{ if((maxhp / 5) * 4 > curhp && !HasAttr(AttrType.COOLDOWN_1)){ RefreshDuration(AttrType.COOLDOWN_1,R.Between(14,16)*100); attrs[AttrType.COOLDOWN_2]++; B.Add(the_name + " starts praying. ",this); B.Add("A fiery halo appears above " + the_name + ". ",this); RefreshDuration(AttrType.RADIANT_HALO,R.Between(8,10)*100,Your() + " halo fades. ",this); Q1(); } else{ if(DistanceFrom(target) == 1){ Attack(0,target); } else{ AI_Step(target); QS(); } } }*/ break; case ActorType.GIANT_SLUG: { if(DistanceFrom(target) == 1){ Attack(R.Between(0,1),target); } else{ if(!HasAttr(AttrType.COOLDOWN_1) && DistanceFrom(target) <= 12 && FirstActorInLine(target) == target){ RefreshDuration(AttrType.COOLDOWN_1,R.Between(11,14)*100); B.Add(TheName(true) + " spits slime at " + target.the_name + ". ",target); List<Tile> slimed = GetBestLineOfEffect(target); List<Tile> added = new List<Tile>(); foreach(Tile t in slimed){ foreach(int dir in U.FourDirections){ Tile neighbor = t.TileInDirection(dir); if(R.OneIn(3) && neighbor.passable && !slimed.Contains(neighbor)){ added.AddUnique(neighbor); } } } slimed.AddRange(added); List<pos> cells = new List<pos>(); List<Actor> slimed_actors = new List<Actor>(); for(int i=0;slimed.Count > 0;++i){ List<Tile> removed = new List<Tile>(); foreach(Tile t in slimed){ if(DistanceFrom(t) == i){ t.AddFeature(FeatureType.SLIME); if(t.actor() != null && t.actor() != this && !t.actor().HasAttr(AttrType.SLIMED,AttrType.FROZEN)){ slimed_actors.Add(t.actor()); } removed.Add(t); if(DistanceFrom(t) > 0){ cells.Add(t.p); } } } foreach(Tile t in removed){ slimed.Remove(t); } if(cells.Count > 0){ Screen.AnimateMapCells(cells,new colorchar(',',Color.Green),20); } } M.Draw(); slimed_actors.AddUnique(target); foreach(Actor a in slimed_actors){ a.attrs[AttrType.SLIMED] = 1; a.attrs[AttrType.OIL_COVERED] = 0; a.RefreshDuration(AttrType.BURNING,0); B.Add(a.YouAre() + " covered in slime. ",a); } Q1(); } else{ AI_Step(target); if(tile().Is(FeatureType.SLIME)){ speed = 50; QS(); //normal speed is 150 speed = 150; } else{ QS(); } } } break; } case ActorType.BANSHEE: { if(!HasAttr(AttrType.COOLDOWN_1) && DistanceFrom(target) <= 12){ RefreshDuration(AttrType.COOLDOWN_1,R.Between(13,15)*100); if(player.CanSee(this)){ if(player.IsSilencedHere()){ B.Add(You("seem") + " to scream. ",this); } else{ B.Add(You("scream") + ". ",this); } } else{ if(!player.IsSilencedHere()){ B.Add("You hear a scream! "); } } if(!target.IsSilencedHere()){ if(target.ResistedBySpirit() || target.HasAttr(AttrType.MENTAL_IMMUNITY)){ B.Add(target.You("remain") + " courageous. ",target); } else{ B.Add(target.YouAre() + " terrified! ",target); RefreshDuration(AttrType.TERRIFYING,R.Between(5,8)*100,target.YouAre() + " no longer afraid. ",target); Help.TutorialTip(TutorialTopic.Afraid); } } Q1(); } else{ if(DistanceFrom(target) == 1){ Attack(0,target); } else{ AI_Step(target); QS(); } } break; } case ActorType.CAVERN_HAG: if(curhp < maxhp && HasAttr(AttrType.COOLDOWN_2) && !HasAttr(AttrType.COOLDOWN_1) && DistanceFrom(target) <= 12){ B.Add(TheName(true) + " curses you! "); if(target.ResistedBySpirit()){ B.Add("You resist the curse. "); } else{ switch(R.Roll(4)){ case 1: //light allergy B.Add("You become allergic to light! "); target.RefreshDuration(AttrType.LIGHT_SENSITIVE,(R.Roll(2,20) + 70) * 100,"You are no longer allergic to light. "); break; case 2: //aggravate monsters B.Add("Every sound you make becomes amplified and echoes across the dungeon. "); target.RefreshDuration(AttrType.AGGRAVATING,(R.Roll(2,20) + 70) * 100,"Your sounds are no longer amplified. "); break; case 3: //cursed weapon B.Add("Your " + target.EquippedWeapon + " becomes stuck to your hand! "); target.EquippedWeapon.status[EquipmentStatus.STUCK] = true; Help.TutorialTip(TutorialTopic.Stuck); break; case 4: //heavy weapon B.Add("Your " + target.EquippedWeapon + " suddenly feels much heavier. "); target.EquippedWeapon.status[EquipmentStatus.HEAVY] = true; Help.TutorialTip(TutorialTopic.Heavy); break; } } attrs[AttrType.COOLDOWN_1]++; Q1(); } else{ if(DistanceFrom(target) == 1){ Attack(0,target); } else{ AI_Step(target); QS(); } } break; case ActorType.BERSERKER: { if(HasAttr(AttrType.COOLDOWN_2)){ int dir = attrs[AttrType.COOLDOWN_2]; bool cw = R.CoinFlip(); if(TileInDirection(dir).passable && ActorInDirection(dir) == null && !MovementPrevented(TileInDirection(dir))){ B.Add(the_name + " leaps forward swinging his axe! ",this); Move(TileInDirection(dir).row,TileInDirection(dir).col); M.Draw(); for(int i=-1;i<=1;++i){ Screen.AnimateBoltProjectile(new List<Tile>{tile(),TileInDirection(dir.RotateDir(cw,i))},Color.Red,30); } for(int i=-1;i<=1;++i){ Actor a = ActorInDirection(dir.RotateDir(cw,i)); if(a != null){ B.Add(YourVisible() + " axe hits " + a.TheName(true) + ". ",this,a); a.TakeDamage(DamageType.NORMAL,DamageClass.PHYSICAL,R.Roll(4,6),this,"a berserker's axe"); } TileInDirection(dir.RotateDir(cw,i)).Bump(dir.RotateDir(cw,i)); } Q1(); } else{ if(ActorInDirection(dir) != null || MovementPrevented(TileInDirection(dir)) || TileInDirection(dir).Is(TileType.STANDING_TORCH,TileType.BARREL,TileType.POISON_BULB)){ B.Add(the_name + " swings his axe furiously! ",this); for(int i=-1;i<=1;++i){ Screen.AnimateBoltProjectile(new List<Tile>{tile(),TileInDirection(dir.RotateDir(cw,i))},Color.Red,30); } for(int i=-1;i<=1;++i){ Actor a = ActorInDirection(dir.RotateDir(cw,i)); if(a != null){ B.Add(YourVisible() + " axe hits " + a.TheName(true) + ". ",this,a); a.TakeDamage(DamageType.NORMAL,DamageClass.PHYSICAL,R.Roll(4,6),this,"a berserker's axe"); } TileInDirection(dir.RotateDir(cw,i)).Bump(dir.RotateDir(cw,i)); } Q1(); } else{ if(target != null && HasLOS(target)){ B.Add(the_name + " turns to face " + target.the_name + ". ",this); attrs[AttrType.COOLDOWN_2] = DirectionOf(target); Q1(); } } } } else{ if(DistanceFrom(target) == 1){ Attack(0,target); if(target != null && R.Roll(3) == 3){ B.Add(the_name + " screams with fury! ",this); attrs[AttrType.COOLDOWN_2] = DirectionOf(target); Q.Add(new Event(this,350,AttrType.COOLDOWN_2,Your() + " rage diminishes. ",this)); } } else{ AI_Step(target); QS(); } } break; } case ActorType.DIRE_RAT: { bool slip_past = false; if(DistanceFrom(target) == 1){ foreach(Actor a in ActorsAtDistance(1)){ if(a.type == ActorType.DIRE_RAT && a.DistanceFrom(target) > this.DistanceFrom(target)){ bool can_walk = false; foreach(Tile t in a.TilesAtDistance(1)){ if(t.DistanceFrom(target) < a.DistanceFrom(target) && t.passable && t.actor() == null){ can_walk = true; break; } } if(!can_walk){ //there's a rat that would benefit from a space opening up - now check to see whether a move is possible foreach(Tile t in target.TilesAtDistance(1)){ if(t.passable && t.actor() == null){ slip_past = true; break; } } break; } } } } if(slip_past){ bool moved = false; foreach(Tile t in TilesAtDistance(1)){ if(t.DistanceFrom(target) == 1 && t.passable && t.actor() == null){ AI_Step(t); QS(); moved = true; break; } } if(!moved){ Tile t = target.TilesAtDistance(1).Where(x=>x.passable && x.actor() == null).RandomOrDefault(); if(t != null){ B.Add(TheName(true) + " slips past " + target.TheName(true) + ". ",this,target); Move(t.row,t.col); Q.Add(new Event(this,Speed() + 100,EventType.MOVE)); } else{ QS(); } } } else{ if(DistanceFrom(target) == 1){ Attack(0,target); } else{ AI_Step(target); QS(); } } break; } case ActorType.SKULKING_KILLER: { if(HasAttr(AttrType.KEEPS_DISTANCE)){ bool try_to_hide = false; if(AI_Flee()){ try_to_hide = true; QS(); } else{ if(DistanceFrom(target) == 1){ Attack(0,target); } else{ //give up on fleeing, just attack attrs[AttrType.COOLDOWN_2] = 0; attrs[AttrType.KEEPS_DISTANCE] = 0; AI_Step(target); QS(); } } if(try_to_hide){ bool visible = player.CanSee(this); if(!R.OneIn(5) && (!player.HasLOE(this) || !visible || DistanceFrom(player) > 12)){ //just to add some uncertainty attrs[AttrType.COOLDOWN_2]++; if(attrs[AttrType.COOLDOWN_2] >= 3){ attrs[AttrType.KEEPS_DISTANCE] = 0; attrs[AttrType.COOLDOWN_2] = 0; if(!visible){ attrs[AttrType.TURNS_VISIBLE] = 0; } } } } } else{ if(DistanceFrom(target) == 1){ if(Attack(0,target)){ attrs[AttrType.KEEPS_DISTANCE] = 1; } } else{ AI_Step(target); QS(); } } /*if(!HasAttr(AttrType.COOLDOWN_1) && DistanceFrom(target) <= 3 && R.OneIn(3) && HasLOE(target)){ attrs[AttrType.COOLDOWN_1]++; AnimateProjectile(target,Color.DarkYellow,'%'); Input.FlushInput(); if(target.CanSee(this)){ B.Add(the_name + " throws a bola at " + target.the_name + ". ",this,target); } else{ B.Add("A bola whirls toward " + target.the_name + ". ",this,target); } attrs[AttrType.TURNS_VISIBLE] = -1; target.RefreshDuration(AttrType.SLOWED,(R.Roll(3)+6)*100,target.YouAre() + " no longer slowed. ",target); B.Add(target.YouAre() + " slowed by the bola. ",target); Q1(); } else{ if(DistanceFrom(target) == 1){ Attack(0,target); } else{ AI_Step(target); QS(); } }*/ break; } case ActorType.WILD_BOAR: if(DistanceFrom(target) == 1){ Attack(0,target); if(HasAttr(AttrType.JUST_FLUNG)){ //if it just flung its target... attrs[AttrType.JUST_FLUNG] = 0; attrs[AttrType.COOLDOWN_1] = 0; } else{ //...otherwise it might prepare to fling again if(!HasAttr(AttrType.COOLDOWN_1)){ if(!HasAttr(AttrType.COOLDOWN_2) || R.OneIn(5)){ attrs[AttrType.COOLDOWN_2]++; B.Add(the_name + " lowers its head. ",this); attrs[AttrType.COOLDOWN_1]++; } } } } else{ AI_Step(target); if(!HasAttr(AttrType.COOLDOWN_2)){ attrs[AttrType.COOLDOWN_2]++; B.Add(the_name + " lowers its head. ",this); attrs[AttrType.COOLDOWN_1]++; } QS(); } break; case ActorType.DREAM_SPRITE: if(!HasAttr(AttrType.COOLDOWN_1)){ if(DistanceFrom(target) <= 12 && FirstActorInLine(target) == target){ RefreshDuration(AttrType.COOLDOWN_1,R.Between(3,4)*100); bool visible = false; List<List<Tile>> lines = new List<List<Tile>>{GetBestLineOfEffect(target)}; if(group != null && group.Count > 0){ foreach(Actor a in group){ if(target == player && player.CanSee(a)){ visible = true; } if(a.type == ActorType.DREAM_SPRITE_CLONE){ a.attrs[AttrType.COOLDOWN_1]++; //for them, it means 'skip next turn' if(a.FirstActorInLine(target) == target){ lines.Add(a.GetBestLineOfEffect(target)); } } } } foreach(List<Tile> line in lines){ if(line.Count > 0){ line.RemoveAt(0); } } if(visible){ B.Add(the_name + " hits " + target.the_name + " with stinging magic. ",target); } else{ B.Add(TheName(true) + " hits " + target.the_name + " with stinging magic. ",target); } int max = lines.WhereGreatest(x=>x.Count)[0].Count; for(int i=0;i<max;++i){ List<pos> cells = new List<pos>(); foreach(List<Tile> line in lines){ if(line.Count > i){ cells.Add(line[i].p); } } Screen.AnimateMapCells(cells,new colorchar('*',Color.RandomRainbow)); } target.TakeDamage(DamageType.MAGIC,DamageClass.MAGICAL,R.Roll(2,6),this,"a blast of fairy magic"); Q1(); } else{ if(DistanceFrom(target) > 12){ AI_Step(target); } else{ AI_Sidestep(target); } QS(); } } else{ if(DistanceFrom(target) > 5){ AI_Step(target); } else{ if(DistanceFrom(target) < 3){ AI_Flee(); } else{ Tile t = TilesAtDistance(1).Where(x=>x.passable && x.actor() == null).RandomOrDefault(); if(t != null){ AI_Step(t); } } } QS(); } break; case ActorType.DREAM_SPRITE_CLONE: if(HasAttr(AttrType.COOLDOWN_1)){ attrs[AttrType.COOLDOWN_1] = 0; Q1(); } else{ if(DistanceFrom(target) > 5){ AI_Step(target); } else{ if(DistanceFrom(target) < 3){ AI_Flee(); } else{ Tile t = TilesAtDistance(1).Where(x=>x.passable && x.actor() == null).RandomOrDefault(); if(t != null){ AI_Step(t); } } } QS(); } break; case ActorType.CLOUD_ELEMENTAL: { List<pos> cloud = M.tile.GetFloodFillPositions(p,false,x=>M.tile[x].features.Contains(FeatureType.FOG)); PhysicalObject[] objs = new PhysicalObject[cloud.Count + 1]; int idx = 0; foreach(pos p2 in cloud){ objs[idx++] = M.tile[p2]; } objs[idx] = this; List<colorchar> chars = new List<colorchar>(); colorchar cch = new colorchar('*',Color.RandomLightning); if(cloud.Contains(target.p)){ B.Add(the_name + " electrifies the cloud! ",objs); foreach(pos p2 in cloud){ if(M.actor[p2] != null && M.actor[p2] != this){ M.actor[p2].TakeDamage(DamageType.ELECTRIC,DamageClass.PHYSICAL,R.Roll(3,6),this,"*electrocuted by a cloud elemental"); } if(M.actor[p2] == this){ chars.Add(visual); } else{ chars.Add(cch); } } Screen.AnimateMapCells(cloud,chars,50); Q1(); } else{ if(DistanceFrom(target) == 1){ Tile t = TilesAtDistance(1).Where(x=>x.actor() == null && x.passable).RandomOrDefault(); if(t != null){ AI_Step(t); } QS(); } else{ if(R.OneIn(4)){ Tile t = TilesAtDistance(1).Where(x=>x.actor() == null && x.passable).RandomOrDefault(); if(t != null){ AI_Step(t); } QS(); } else{ AI_Step(target); QS(); } } } break; } case ActorType.DERANGED_ASCETIC: if(DistanceFrom(target) == 1){ Attack(R.Roll(3)-1,target); } else{ AI_Step(target); QS(); } break; case ActorType.SNEAK_THIEF: { if(DistanceFrom(target) <= 12 && !R.OneIn(3) && AI_UseRandomItem()){ Q1(); } else{ if(DistanceFrom(target) == 1){ Attack(0,target); if(target != null){ List<Tile> valid_dirs = new List<Tile>(); foreach(Tile t in target.TilesAtDistance(1)){ if(t.passable && t.actor() == null && DistanceFrom(t) == 1){ valid_dirs.Add(t); } } if(valid_dirs.Count > 0){ AI_Step(valid_dirs.Random()); } } } else{ AI_Step(target); QS(); } } break; } case ActorType.WARG: { bool howl = false; if(DistanceFrom(target) == 1){ if(R.CoinFlip() || group == null || group.Count < 2 || HasAttr(AttrType.COOLDOWN_1)){ Attack(0,target); } else{ howl = true; } } else{ if(group == null || group.Count < 2 || HasAttr(AttrType.COOLDOWN_1)){ if(AI_Step(target)){ QS(); } else{ howl = true; } } else{ howl = true; } } if(howl){ if(group == null || group.Count < 2){ Q1(); break; } B.Add(TheName(true) + " howls. "); PosArray<int> paths = new PosArray<int>(ROWS,COLS); foreach(Actor packmate in group){ packmate.RefreshDuration(AttrType.COOLDOWN_1,2000); if(packmate != this){ var dijkstra = M.tile.GetDijkstraMap(new List<pos>{target.p},x=>!M.tile[x].passable,y=>M.actor[y] != null? 5 : paths[y]+1); if(!dijkstra[packmate.p].IsValidDijkstraValue()){ continue; } List<pos> new_path = new List<pos>(); pos p = packmate.p; while(!p.Equals(target.p)){ p = p.PositionsAtDistance(1,dijkstra).Where(x=>dijkstra[x].IsValidDijkstraValue()).WhereLeast(x=>dijkstra[x]).Random(); new_path.Add(p); paths[p]++; } packmate.path = new_path; } } Q1(); } break; } case ActorType.RUNIC_TRANSCENDENT: { if(SilencedThisTurn()){ return; } if(!HasSpell(SpellType.MERCURIAL_SPHERE)){ if(DistanceFrom(target) == 1){ Attack(0,target); } else{ AI_Step(target); QS(); } return; } if(curmp < 2){ B.Add(the_name + " absorbs mana from the universe. ",this); curmp = maxmp; Q1(); } else{ if(M.safetymap == null){ M.UpdateSafetyMap(player); } Tile t = TilesAtDistance(1).Where(x=>x.DistanceFrom(target) == 3 && x.passable && x.actor() == null).WhereLeast(x=>M.safetymap[x.p]).RandomOrDefault(); if(t != null){ //check safety map. if there's a safer spot at distance 3, step there. AI_Step(t); } else{ if(DistanceFrom(target) > 3){ AI_Step(target); } else{ if(DistanceFrom(target) < 3){ AI_Flee(); } } } if(DistanceFrom(target) <= 12 && FirstActorInLine(target) != null && FirstActorInLine(target).DistanceFrom(target) <= 3){ CastSpell(SpellType.MERCURIAL_SPHERE,target); } else{ QS(); } } break; } case ActorType.CARRION_CRAWLER: if(DistanceFrom(target) == 1){ if(!target.HasAttr(AttrType.PARALYZED)){ Attack(0,target); } else{ Attack(1,target); } } else{ AI_Step(target); QS(); } break; case ActorType.MECHANICAL_KNIGHT: if(attrs[AttrType.COOLDOWN_1] == 3){ //no head int dir = Global.RandomDirection(); if(R.CoinFlip()){ Actor a = ActorInDirection(dir); if(a != null){ if(!Attack(0,a)){ B.Add(the_name + " drops its guard! ",this); attrs[AttrType.MECHANICAL_SHIELD] = 0; } } else{ B.Add(the_name + " attacks empty space. ",this); TileInDirection(dir).Bump(dir); B.Add(the_name + " drops its guard! ",this); attrs[AttrType.MECHANICAL_SHIELD] = 0; Q1(); } } else{ Tile t = TileInDirection(dir); if(t.passable){ if(t.actor() == null){ AI_Step(t); QS(); } else{ B.Add(the_name + " bumps into " + t.actor().TheName(true) + ". ",this); QS(); } } else{ B.Add(the_name + " bumps into " + t.TheName(true) + ". ",this); t.Bump(DirectionOf(t)); QS(); } } } else{ if(DistanceFrom(target) == 1){ if(attrs[AttrType.COOLDOWN_1] == 1){ //no arms Attack(1,target); } else{ if(!Attack(0,target)){ B.Add(the_name + " drops its guard! ",this); attrs[AttrType.MECHANICAL_SHIELD] = 0; } } } else{ if(attrs[AttrType.COOLDOWN_1] != 2){ //no legs AI_Step(target); } QS(); } } break; case ActorType.ALASI_BATTLEMAGE: if(SilencedThisTurn()){ return; } if(DistanceFrom(target) > 12){ AI_Step(target); QS(); } else{ if(DistanceFrom(target) == 1){ if(exhaustion < 50){ CastCloseRangeSpellOrAttack(null,target,true); } else{ Attack(0,target); } } else{ CastRangedSpellOrMove(target); } } break; case ActorType.ALASI_SOLDIER: if(DistanceFrom(target) > 2){ AI_Step(target); QS(); attrs[AttrType.COMBO_ATTACK] = 0; } else{ if(FirstActorInLine(target) != null && !FirstActorInLine(target).name.Contains("alasi")){ //I had planned to make this attack possibly hit multiple targets, but not yet. Attack(0,target); } else{ if(AI_Step(target)){ QS(); } else{ AI_Sidestep(target); QS(); } attrs[AttrType.COMBO_ATTACK] = 0; } } break; case ActorType.SKITTERMOSS: if(DistanceFrom(target) == 1){ Attack(0,target); if(target != null && R.CoinFlip()){ //chance of retreating AI_Step(target,true); } } else{ if(R.CoinFlip()){ AI_Step(target); QS(); } else{ AI_Step(TileInDirection(Global.RandomDirection())); QS(); } } break; case ActorType.ALASI_SCOUT: { if(DistanceFrom(target) == 1){ Attack(0,target); } else{ if(curhp == maxhp){ if(FirstActorInLine(target) == target){ Attack(1,target); } else{ AI_Sidestep(target); QS(); } } else{ AI_Step(target); QS(); } } break; } case ActorType.MUD_ELEMENTAL: { int count = 0; int walls = 0; foreach(Tile t in target.TilesAtDistance(1)){ if(t.p.BoundsCheck(M.tile,false) && t.type == TileType.WALL){ ++walls; if(t.actor() == null){ ++count; } } } if(!HasAttr(AttrType.COOLDOWN_1) && DistanceFrom(target) <= 12 && count >= 2 || (count == 1 && walls == 1)){ RefreshDuration(AttrType.COOLDOWN_1,150); foreach(Tile t in target.TilesAtDistance(1)){ if(t.p.BoundsCheck(M.tile,false) && t.type == TileType.WALL && t.actor() == null){ Create(ActorType.MUD_TENTACLE,t.row,t.col,TiebreakerAssignment.InsertAfterCurrent); M.actor[t.p].player_visibility_duration = -1; } } if(count >= 2){ if(player.CanSee(this)){ B.Add(the_name + " calls mud tentacles from the walls! "); } else{ B.Add("Mud tentacles emerge from the walls! "); } } else{ if(player.CanSee(this)){ B.Add(the_name + " calls a mud tentacle from the wall! "); } else{ B.Add("A mud tentacle emerges from the wall! "); } } Q1(); } else{ if(DistanceFrom(target) == 1){ Attack(0,target); } else{ AI_Step(target); QS(); } } break; } case ActorType.FLAMETONGUE_TOAD: { bool burrow = false; if((curhp * 3 <= maxhp || DistanceFrom(target) > 6) && R.CoinFlip()){ burrow = true; } if(DistanceFrom(target) <= 6 && DistanceFrom(target) > 1){ if(R.OneIn(20)){ burrow = true; } } if(burrow && !HasAttr(AttrType.COOLDOWN_1)){ RefreshDuration(AttrType.COOLDOWN_1,R.Between(12,16)*100); if(curhp * 3 <= maxhp){ Burrow(TilesWithinDistance(6)); } else{ Burrow(GetCone(DirectionOf(target),6,true)); } } else{ if(!HasAttr(AttrType.COOLDOWN_2) && FirstActorInLine(target) != null && FirstActorInLine(target).DistanceFrom(target) <= 1){ RefreshDuration(AttrType.COOLDOWN_2,R.Between(10,14)*100); Actor first = FirstActorInLine(target); B.Add(TheName(true) + " breathes fire! ",this,first); AnimateProjectile(first,'*',Color.RandomFire); AnimateExplosion(first,1,'*',Color.RandomFire); foreach(Tile t in GetBestLineOfEffect(first)){ t.ApplyEffect(DamageType.FIRE); } foreach(Tile t in first.TilesWithinDistance(1)){ t.ApplyEffect(DamageType.FIRE); if(t.actor() != null){ t.actor().ApplyBurning(); } } Q1(); } else{ if(DistanceFrom(target) == 1){ Attack(0,target); } else{ AI_Step(target); QS(); } } } break; } case ActorType.ENTRANCER: if(group == null){ if(AI_Flee()){ QS(); } else{ if(DistanceFrom(target) == 1){ Attack(0,target); } else{ QS(); } } } else{ Actor thrall = group[1]; if(CanSee(thrall) && HasLOE(thrall)){ //cooldown 1 is teleport. cooldown 2 is shield. //if the thrall is visible and you have LOE, the next goal is for the entrancer to be somewhere on the line that starts at the target and extends through the thrall. List<Tile> line_from_target = target.GetBestExtendedLineOfEffect(thrall); bool on_line = line_from_target.Contains(tile()); bool space_near_target = line_from_target.Count > 1 && line_from_target[1].passable && line_from_target[1].actor() == null; if(on_line && DistanceFrom(target) > thrall.DistanceFrom(target)){ if(!HasAttr(AttrType.COOLDOWN_2) && thrall.curhp <= thrall.maxhp / 2){ //check whether you can shield it, if the thrall is low on health. RefreshDuration(AttrType.COOLDOWN_2,1500); B.Add(TheName(true) + " shields " + thrall.TheName(true) + ". ",this,thrall); B.DisplayNow(); Screen.AnimateStorm(thrall.p,1,2,5,'*',Color.White); thrall.attrs[AttrType.SHIELDED] = 1; Q1(); } else{ //check whether you can teleport the thrall closer. if(!HasAttr(AttrType.COOLDOWN_1) && thrall.DistanceFrom(target) > 1 && space_near_target){ Tile dest = line_from_target[1]; RefreshDuration(AttrType.COOLDOWN_1,400); B.Add(TheName(true) + " teleports " + thrall.TheName(true) + ". ",this,thrall); M.Draw(); thrall.Move(dest.row,dest.col); B.DisplayNow(); Screen.AnimateStorm(dest.p,1,1,4,thrall.symbol,thrall.color); foreach(Tile t2 in thrall.GetBestLineOfEffect(dest)){ Screen.AnimateStorm(t2.p,1,1,4,thrall.symbol,thrall.color); } Q1(); } else{ //check whether you can shield it, if the thrall isn't low on health. if(!HasAttr(AttrType.COOLDOWN_2)){ RefreshDuration(AttrType.COOLDOWN_2,1500); B.Add(TheName(true) + " shields " + thrall.TheName(true) + ". ",this,thrall); B.DisplayNow(); Screen.AnimateStorm(thrall.p,1,2,5,'*',Color.White); thrall.attrs[AttrType.SHIELDED] = 1; Q1(); } else{ //check whether you are adjacent to thrall and can step away while remaining on line. List<Tile> valid = line_from_target.Where(x=>DistanceFrom(x) == 1 && x.actor() == null && x.passable); if(DistanceFrom(thrall) == 1 && valid.Count > 0){ AI_Step(valid.Random()); } QS(); } } } } else{ if(on_line){ //if on the line but not behind the thrall, we might be able to swap places or teleport if(DistanceFrom(thrall) == 1){ Move(thrall.row,thrall.col); QS(); } else{ Tile dest = null; foreach(Tile t in line_from_target){ if(t.passable && t.actor() == null){ dest = t; break; } } if(dest != null){ RefreshDuration(AttrType.COOLDOWN_1,400); B.Add(TheName(true) + " teleports " + thrall.TheName(true) + ". ",this,thrall); M.Draw(); thrall.Move(dest.row,dest.col); B.DisplayNow(); Screen.AnimateStorm(dest.p,1,1,4,thrall.symbol,thrall.color); foreach(Tile t2 in thrall.GetBestLineOfEffect(dest)){ Screen.AnimateStorm(t2.p,1,1,4,thrall.symbol,thrall.color); } } Q1(); } } else{ //if there's a free adjacent space on the line and behind the thrall, step there. List<Tile> valid = line_from_target.From(thrall).Where(x=>x.passable && x.actor() == null && x.DistanceFrom(this) == 1); if(valid.Count > 0){ AI_Step(valid.Random()); QS(); } else{ //if you can teleport and there's a free tile on the line between you and the target, teleport the thrall there. List<Tile> valid_between = GetBestLineOfEffect(target).Where(x=>x.passable && x.actor() == null && thrall.HasLOE(x)); if(!HasAttr(AttrType.COOLDOWN_1) && valid_between.Count > 0){ Tile dest = valid_between.Random(); RefreshDuration(AttrType.COOLDOWN_1,400); B.Add(TheName(true) + " teleports " + thrall.TheName(true) + ". ",this,thrall); M.Draw(); thrall.Move(dest.row,dest.col); B.DisplayNow(); Screen.AnimateStorm(dest.p,1,1,4,thrall.symbol,thrall.color); foreach(Tile t2 in thrall.GetBestLineOfEffect(dest)){ Screen.AnimateStorm(t2.p,1,1,4,thrall.symbol,thrall.color); } Q1(); } else{ //step toward a tile on the line (and behind the thrall) List<Tile> valid_behind_thrall = line_from_target.From(thrall).Where(x=>x.passable && x.actor() == null); if(valid_behind_thrall.Count > 0){ AI_Step(valid_behind_thrall.Random()); } QS(); } } } } //the old code: /*if(DistanceFrom(target) < thrall.DistanceFrom(target) && DistanceFrom(thrall) == 1){ Move(thrall.row,thrall.col); QS(); } else{ if(DistanceFrom(target) == 1 && curhp < maxhp){ List<Tile> safe = TilesAtDistance(1).Where(t=>t.passable && t.actor() == null && target.GetBestExtendedLineOfEffect(thrall).Contains(t)); if(DistanceFrom(thrall) == 1 && safe.Count > 0){ AI_Step(safe.Random()); QS(); } else{ if(AI_Flee()){ QS(); } else{ Attack(0,target); } } } else{ if(!HasAttr(AttrType.COOLDOWN_1) && (thrall.DistanceFrom(target) > 1 || !target.GetBestExtendedLineOfEffect(thrall).Any(t=>t.actor()==this))){ //the entrancer tries to be smart about placing the thrall in a position that blocks ranged attacks List<Tile> closest = new List<Tile>(); int dist = 99; foreach(Tile t in thrall.TilesWithinDistance(2).Where(x=>x.passable && (x.actor()==null || x.actor()==thrall))){ if(t.DistanceFrom(target) < dist){ closest.Clear(); closest.Add(t); dist = t.DistanceFrom(target); } else{ if(t.DistanceFrom(target) == dist){ closest.Add(t); } } } List<Tile> in_line = new List<Tile>(); foreach(Tile t in closest){ if(target.GetBestExtendedLineOfEffect(t).Any(x=>x.actor()==this)){ in_line.Add(t); } } Tile tile2 = null; if(in_line.Count > 0){ tile2 = in_line.Random(); } else{ if(closest.Count > 0){ tile2 = closest.Random(); } } if(tile2 != null && tile2.actor() != thrall){ GainAttr(AttrType.COOLDOWN_1,400); B.Add(TheName(true) + " teleports " + thrall.TheName(true) + ". ",this,thrall); M.Draw(); thrall.Move(tile2.row,tile2.col); B.DisplayNow(); Screen.AnimateStorm(tile2.p,1,1,4,thrall.symbol,thrall.color); foreach(Tile t2 in thrall.GetBestLineOfEffect(tile2)){ Screen.AnimateStorm(t2.p,1,1,4,thrall.symbol,thrall.color); } Q1(); } else{ List<Tile> safe = target.GetBestExtendedLineOfEffect(thrall).Where(t=>t.passable && t.actor() == null && t.DistanceFrom(target) > thrall.DistanceFrom(target)).WhereLeast(t=>DistanceFrom(t)); if(safe.Count > 0){ if(safe.Any(t=>t.DistanceFrom(target) > 2)){ AI_Step(safe.Where(t=>t.DistanceFrom(target) > 2).Random()); } else{ AI_Step(safe.Random()); } } QS(); } } else{ if(!HasAttr(AttrType.COOLDOWN_2) && thrall.attrs[AttrType.ARCANE_SHIELDED] < 25){ GainAttr(AttrType.COOLDOWN_2,1500); B.Add(TheName(true) + " shields " + thrall.TheName(true) + ". ",this,thrall); B.DisplayNow(); Screen.AnimateStorm(thrall.p,1,2,5,'*',Color.White); thrall.attrs[AttrType.ARCANE_SHIELDED] = 25; Q1(); } else{ List<Tile> safe = target.GetBestExtendedLineOfEffect(thrall).Where(t=>t.passable && t.actor() == null).WhereLeast(t=>DistanceFrom(t)); if(safe.Count > 0){ if(safe.Any(t=>t.DistanceFrom(target) > 2)){ AI_Step(safe.Where(t=>t.DistanceFrom(target) > 2).Random()); } else{ AI_Step(safe.Random()); } } QS(); } } } }*/ } else{ group[1].FindPath(this); //call for help if(AI_Flee()){ QS(); } else{ if(DistanceFrom(target) == 1){ Attack(0,target); } else{ QS(); } } } } break; case ActorType.ORC_GRENADIER: if(!HasAttr(AttrType.COOLDOWN_1) && DistanceFrom(target) <= 8){ attrs[AttrType.COOLDOWN_1]++; Q.Add(new Event(this,(R.Roll(2)*100)+150,AttrType.COOLDOWN_1)); B.Add(TheName(true) + " tosses a grenade toward " + target.the_name + ". ",target); List<Tile> tiles = new List<Tile>(); foreach(Tile tile in target.TilesWithinDistance(1)){ if(tile.passable && !tile.Is(FeatureType.GRENADE)){ tiles.Add(tile); } } Tile t = tiles[R.Roll(tiles.Count)-1]; if(t.actor() != null){ if(t.actor() == player){ B.Add("It lands under you! "); } else{ B.Add("It lands under " + t.actor().the_name + ". ",t.actor()); } } else{ if(t.inv != null){ B.Add("It lands under " + t.inv.TheName() + ". ",t); } } t.features.Add(FeatureType.GRENADE); Q.Add(new Event(t,100,EventType.GRENADE)); Q1(); } else{ if(curhp <= 18){ if(AI_Step(target,true)){ QS(); } else{ if(DistanceFrom(target) == 1){ Attack(0,target); } else{ QS(); } } } else{ if(DistanceFrom(target) == 1){ Attack(0,target); } else{ AI_Step(target); QS(); } } } break; case ActorType.MARBLE_HORROR: if(DistanceFrom(target) == 1){ Attack(0,target); } else{ AI_Step(target); QS(); } break; case ActorType.SPELLMUDDLE_PIXIE: if(DistanceFrom(target) == 1){ Attack(0,target); if(target != null && R.CoinFlip()){ AI_Step(target,true); } } else{ AI_Step(target); QS(); } break; case ActorType.OGRE_BARBARIAN: //if has grabbed target, check for open spaces near the opposite side. //if one is found, slam target into that tile, then do the attack. //otherwise, slam target into a solid tile (target doesn't move), then attack. //if nothing is grabbed yet, just keep attacking. if(DistanceFrom(target) == 1){ if(target.HasAttr(AttrType.GRABBED) && attrs[AttrType.GRABBING] == DirectionOf(target) && !target.MovementPrevented(tile())){ Tile t = null; Tile opposite = TileInDirection(DirectionOf(target).RotateDir(true,4)); if(opposite.passable && opposite.actor() == null){ t = opposite; } if(t == null){ List<Tile> near_opposite = new List<Tile>(); foreach(int i in new int[]{-1,1}){ Tile near = TileInDirection(DirectionOf(target).RotateDir(true,4+i)); if(near.passable && near.actor() == null){ near_opposite.Add(near); } } if(near_opposite.Count > 0){ t = near_opposite.Random(); } } if(t != null){ target.attrs[AttrType.TURN_INTO_CORPSE]++; Attack(1,target); target.Move(t.row,t.col); target.CollideWith(target.tile()); target.CorpseCleanup(); } else{ target.attrs[AttrType.TURN_INTO_CORPSE]++; Attack(1,target); target.CollideWith(target.tile()); target.CorpseCleanup(); } } else{ Attack(0,target); } } else{ if(speed == 100){ speed = 50; } if(!HasAttr(AttrType.COOLDOWN_1) && target == player && player.CanSee(this)){ B.Add(the_name + " charges! "); attrs[AttrType.COOLDOWN_1] = 1; } AI_Step(target); if(!HasAttr(AttrType.COOLDOWN_1) && target == player && player.CanSee(this)){ //check twice so the message appears ASAP B.Add(the_name + " charges! "); attrs[AttrType.COOLDOWN_1] = 1; } QS(); } break; case ActorType.MARBLE_HORROR_STATUE: QS(); break; case ActorType.PYREN_ARCHER: //still considering some sort of fire trail movement ability for this guy switch(DistanceFrom(target)){ case 1: if(target.EnemiesAdjacent() > 1){ Attack(0,target); } else{ if(AI_Flee()){ QS(); } else{ Attack(0,target); } } break; case 2: if(FirstActorInLine(target) == target){ FireArrow(target); } else{ if(AI_Flee()){ QS(); } else{ if(AI_Sidestep(target)){ B.Add(the_name + " tries to line up a shot. ",this); } QS(); } } break; case 3: case 4: case 5: case 6: case 7: case 8: case 9: case 10: case 11: case 12: if(FirstActorInLine(target) == target){ FireArrow(target); } else{ if(AI_Sidestep(target)){ B.Add(the_name + " tries to line up a shot. ",this); } QS(); } break; default: AI_Step(target); QS(); break; } break; case ActorType.CYCLOPEAN_TITAN: { if(DistanceFrom(target) == 1){ Attack(0,target); } else{ if(DistanceFrom(target) > 2 && DistanceFrom(target) <= 12 && R.OneIn(15) && FirstActorInLine(target) == target){ B.Add(TheName(true) + " lobs a huge rock! ",this,target); AnimateProjectile(target,'*',Color.Gray); pos tp = target.p; int plus_to_hit = -target.TotalSkill(SkillType.DEFENSE)*3; if(target.IsHit(plus_to_hit)){ B.Add("It hits " + target.the_name + "! ",target); if(target.TakeDamage(DamageType.NORMAL,DamageClass.PHYSICAL,R.Roll(4,6),this,"a cyclopean titan's rock")){ if(R.OneIn(8)){ target.ApplyStatus(AttrType.STUNNED,R.Between(3,4)*100); } } } else{ int armor_value = target.TotalProtectionFromArmor(); if(target != player){ armor_value = target.TotalSkill(SkillType.DEFENSE); //if monsters have Defense skill, it's from armor } int roll = R.Roll(25 - plus_to_hit); if(roll <= armor_value * 3){ B.Add(target.Your() + " armor blocks it! ",target); } else{ if(target.HasAttr(AttrType.ROOTS) && roll <= (armor_value + 10) * 3){ //potion of roots gives 10 defense B.Add(target.Your() + " root shell blocks it! ",target); } else{ B.Add(target.You("avoid") + " it! ",target); } } } foreach(pos neighbor in tp.PositionsWithinDistance(1,M.tile)){ Tile t = M.tile[neighbor]; if(t.Is(TileType.FLOOR) && R.OneIn(4)){ t.Toggle(null,TileType.GRAVEL); } } Q1(); } else{ bool smashed = false; if(DistanceFrom(target) == 2 && !HasLOE(target)){ Tile t = FirstSolidTileInLine(target); if(t != null && !t.passable){ smashed = true; B.Add(You("smash",true) + " through " + t.TheName(true) + "! ",t); foreach(int dir in DirectionOf(t).GetArc(1)){ TileInDirection(dir).Smash(dir); } Move(t.row,t.col); QS(); } } if(!smashed){ AI_Step(target); QS(); } } } break; } case ActorType.ALASI_SENTINEL: if(DistanceFrom(target) == 1){ Attack(0,target); if(HasAttr(AttrType.JUST_FLUNG)){ attrs[AttrType.JUST_FLUNG] = 0; } else{ if(target != null){ List<Tile> valid_dirs = new List<Tile>(); foreach(Tile t in target.TilesAtDistance(1)){ if(t.passable && t.actor() == null && DistanceFrom(t) == 1){ valid_dirs.Add(t); } } if(valid_dirs.Count > 0){ AI_Step(valid_dirs.Random()); } } } } else{ AI_Step(target); QS(); } break; case ActorType.NOXIOUS_WORM: if(!HasAttr(AttrType.COOLDOWN_1) && DistanceFrom(target) <= 12 && HasLOE(target)){ B.Add(TheName(true) + " breathes poisonous gas. "); List<Tile> area = new List<Tile>(); foreach(Tile t in target.TilesWithinDistance(1)){ if(t.passable && target.HasLOE(t)){ t.AddFeature(FeatureType.POISON_GAS); area.Add(t); } } List<Tile> area2 = target.tile().AddGaseousFeature(FeatureType.POISON_GAS,8); area.AddRange(area2); Event.RemoveGas(area,600,FeatureType.POISON_GAS,18); RefreshDuration(AttrType.COOLDOWN_1,(R.Roll(6) + 18) * 100); Q1(); } else{ if(DistanceFrom(target) == 1){ Attack(0,target); } else{ AI_Step(target); QS(); } } break; case ActorType.LASHER_FUNGUS: { if(DistanceFrom(target) <= 12){ if(DistanceFrom(target) == 1){ Attack(0,target); } else{ if(FirstActorInLine(target) == target){ List<Tile> line = GetBestLineOfEffect(target.row,target.col); line.Remove(line[line.Count-1]); AnimateBoltBeam(line,Color.DarkGreen); pos target_p = target.p; if(Attack(1,target) && M.actor[target_p] != null){ target = M.actor[target_p]; int rowchange = 0; int colchange = 0; if(target.row < row){ rowchange = 1; } if(target.row > row){ rowchange = -1; } if(target.col < col){ colchange = 1; } if(target.col > col){ colchange = -1; } if(!target.AI_MoveOrOpen(target.row+rowchange,target.col+colchange)){ bool moved = false; if(Math.Abs(target.row - row) > Math.Abs(target.col - col)){ if(target.AI_Step(M.tile[row,target.col])){ moved = true; } } else{ if(Math.Abs(target.row - row) < Math.Abs(target.col - col)){ if(target.AI_Step(M.tile[target.row,col])){ moved = true; } } else{ if(target.AI_Step(this)){ moved = true; } } } if(!moved){ //todo: this still isn't ideal. maybe I need an AI_Step that only considers 3 directions - right now, it'll make you move even if it isn't closer. B.Add(target.You("do",true) + "n't move far. ",target); } } } } else{ Q1(); } } } else{ Q1(); } break; } case ActorType.LUMINOUS_AVENGER: { if(DistanceFrom(target) <= 3){ List<Tile> ext = GetBestExtendedLineOfEffect(target); int max_count = Math.Min(5,ext.Count); //look 4 spaces away unless the line is even shorter than that. List<Actor> targets = new List<Actor>(); Tile destination = null; for(int i=0;i<max_count;++i){ Tile t = ext[i]; if(t.passable){ if(t.actor() == null){ if(targets.Contains(target)){ destination = t; } } else{ if(t.actor() != this){ targets.Add(t.actor()); } } } else{ break; } } if(destination != null){ Move(destination.row,destination.col); foreach(Tile t in ext.To(destination)){ colorchar cch = M.VisibleColorChar(t.row,t.col); cch.bgcolor = Color.Yellow; if(Global.LINUX && !Screen.GLMode){ cch.bgcolor = Color.DarkYellow; } if(cch.color == cch.bgcolor){ cch.color = Color.Black; } Screen.WriteMapChar(t.row,t.col,cch); Game.GLUpdate(); Thread.Sleep(15); } foreach(Actor a in targets){ Attack(0,a,true); } Q1(); } else{ if(DistanceFrom(target) == 1){ Attack(0,target); } else{ AI_Step(target); QS(); } } } else{ AI_Step(target); QS(); } break; } case ActorType.VAMPIRE: if(DistanceFrom(target) == 1){ Attack(0,target); } else{ if(DistanceFrom(target) <= 12){ if(tile().IsLit() && !HasAttr(AttrType.COOLDOWN_1)){ attrs[AttrType.COOLDOWN_1]++; B.Add(the_name + " gestures. ",this); List<Tile> tiles = new List<Tile>(); foreach(Tile t in target.TilesWithinDistance(6)){ if(t.passable && t.actor() == null && DistanceFrom(t) >= DistanceFrom(target) && target.HasLOS(t) && target.HasLOE(t)){ tiles.Add(t); } } if(tiles.Count == 0){ foreach(Tile t in target.TilesWithinDistance(6)){ //same, but with no distance requirement if(t.passable && t.actor() == null && target.HasLOS(t) && target.HasLOE(t)){ tiles.Add(t); } } } if(tiles.Count == 0){ B.Add("Nothing happens. ",this); } else{ if(tiles.Count == 1){ B.Add("A blood moth appears! "); } else{ B.Add("Blood moths appear! "); } for(int i=0;i<2;++i){ if(tiles.Count > 0){ Tile t = tiles.RemoveRandom(); Create(ActorType.BLOOD_MOTH,t.row,t.col,TiebreakerAssignment.InsertAfterCurrent); M.actor[t.row,t.col].player_visibility_duration = -1; } } } Q1(); } else{ AI_Step(target); QS(); } } else{ AI_Step(target); QS(); } } break; case ActorType.ORC_WARMAGE: { if(SilencedThisTurn()){ return; } switch(DistanceFrom(target)){ case 1: { List<SpellType> close_range = new List<SpellType>(); close_range.Add(SpellType.MAGIC_HAMMER); close_range.Add(SpellType.MAGIC_HAMMER); close_range.Add(SpellType.BLINK); if(target.EnemiesAdjacent() > 1 || R.CoinFlip()){ CastCloseRangeSpellOrAttack(close_range,target,false); } else{ if(AI_Step(target,true)){ QS(); } else{ CastCloseRangeSpellOrAttack(close_range,target,false); } } break; } case 2: if(R.CoinFlip()){ if(AI_Step(target,true)){ QS(); } else{ if(FirstActorInLine(target) == target){ CastRangedSpellOrMove(target); } else{ AI_Sidestep(target); QS(); } } } else{ if(FirstActorInLine(target) == target){ CastRangedSpellOrMove(target); } else{ if(AI_Step(target,true)){ QS(); } else{ AI_Sidestep(target); QS(); } } } break; case 3: case 4: case 5: case 6: case 7: case 8: case 9: case 10: case 11: case 12: if(FirstActorInLine(target) == target){ CastRangedSpellOrMove(target); } else{ AI_Sidestep(target); QS(); } break; default: AI_Step(target); QS(); break; } break; } case ActorType.NECROMANCER: { if(!HasAttr(AttrType.COOLDOWN_1) && DistanceFrom(target) <= 12){ attrs[AttrType.COOLDOWN_1]++; Q.Add(new Event(this,(R.Roll(4)+8)*100,AttrType.COOLDOWN_1)); B.Add(the_name + " calls out to the dead. ",this); ActorType summon = R.CoinFlip()? ActorType.SKELETON : ActorType.ZOMBIE; List<Tile> tiles = new List<Tile>(); foreach(Tile tile in TilesWithinDistance(2)){ if(tile.passable && tile.actor() == null && DirectionOf(tile) == DirectionOf(target)){ tiles.Add(tile); } } if(tiles.Count == 0){ foreach(Tile tile in TilesWithinDistance(2)){ if(tile.passable && tile.actor() == null){ tiles.Add(tile); } } } if(tiles.Count == 0 || (group != null && group.Count > 3)){ B.Add("Nothing happens. ",this); } else{ Tile t = tiles.Random(); B.Add(Prototype(summon).a_name + " digs through the floor! "); Create(summon,t.row,t.col,TiebreakerAssignment.InsertAfterCurrent); M.actor[t.row,t.col].player_visibility_duration = -1; if(group == null){ group = new List<Actor>{this}; } group.Add(M.actor[t.row,t.col]); M.actor[t.row,t.col].group = group; } Q1(); } else{ bool blast = false; switch(DistanceFrom(target)){ case 1: if(AI_Step(target,true)){ QS(); } else{ Attack(0,target); } break; case 2: if(R.CoinFlip() && FirstActorInLine(target) == target){ blast = true; } else{ if(AI_Step(target,true)){ QS(); } else{ blast = true; } } break; case 3: case 4: case 5: case 6: if(FirstActorInLine(target) == target){ blast = true; } else{ AI_Sidestep(target); QS(); } break; default: AI_Step(target); QS(); break; } if(blast){ B.Add(TheName(true) + " fires dark energy at " + target.TheName(true) + ". ",this,target); AnimateBoltProjectile(target,Color.DarkBlue); if(target.TakeDamage(DamageType.MAGIC,DamageClass.MAGICAL,R.Roll(6),this,"*blasted by a necromancer")){ target.IncreaseExhaustion(R.Roll(3)); } Q1(); } } break; } case ActorType.STALKING_WEBSTRIDER: { bool burrow = false; if(DistanceFrom(target) >= 2 && DistanceFrom(target) <= 6){ if(R.CoinFlip() && !target.tile().Is(FeatureType.WEB)){ burrow = true; } } if((DistanceFrom(target) > 6 || target.HasAttr(AttrType.POISONED))){ burrow = true; } if(burrow && !HasAttr(AttrType.COOLDOWN_1)){ RefreshDuration(AttrType.COOLDOWN_1,R.Between(5,8)*100); if(DistanceFrom(target) <= 2){ Burrow(TilesWithinDistance(6)); } else{ Burrow(GetCone(DirectionOf(target),6,true)); } } else{ if(DistanceFrom(target) == 1){ Attack(0,target); } else{ AI_Step(target); QS(); } } break; } case ActorType.ORC_ASSASSIN: if(DistanceFrom(target) > 2 && attrs[AttrType.TURNS_VISIBLE] < 0){ Tile t = TilesAtDistance(1).Where(x=>x.passable && x.actor() == null && target.DistanceFrom(x) == target.DistanceFrom(this)-1 && !target.CanSee(x)).RandomOrDefault(); if(t != null){ AI_Step(t); FindPath(target); //so it won't forget where the target is... QS(); } else{ AI_Step(target); QS(); } } else{ if(DistanceFrom(target) == 1){ Attack(0,target); } else{ AI_Step(target); if(DistanceFrom(target) == 1){ Attack(1,target); } else{ QS(); } } } break; case ActorType.MACHINE_OF_WAR: { if(attrs[AttrType.COOLDOWN_1] % 2 == 0){ //the machine of war moves on even turns and fires on odd turns. AI_Step(target); QS(); } else{ if(DistanceFrom(target) <= 12 && FirstActorInLine(target) == target){ B.Add(TheName(true) + " fires a stream of scalding oil at " + target.the_name + ". ",target); List<Tile> covered_in_oil = GetBestLineOfEffect(target); List<Tile> added = new List<Tile>(); foreach(Tile t in covered_in_oil){ foreach(int dir in U.FourDirections){ Tile neighbor = t.TileInDirection(dir); if(R.OneIn(3) && neighbor.passable && !covered_in_oil.Contains(neighbor)){ added.AddUnique(neighbor); } } } covered_in_oil.AddRange(added); List<pos> cells = new List<pos>(); List<Actor> oiled_actors = new List<Actor>(); for(int i=0;covered_in_oil.Count > 0;++i){ List<Tile> removed = new List<Tile>(); foreach(Tile t in covered_in_oil){ if(DistanceFrom(t) == i){ t.AddFeature(FeatureType.OIL); if(t.actor() != null && t.actor() != this){ oiled_actors.Add(t.actor()); } removed.Add(t); if(DistanceFrom(t) > 0){ cells.Add(t.p); } } } foreach(Tile t in removed){ covered_in_oil.Remove(t); } if(cells.Count > 0){ Screen.AnimateMapCells(cells,new colorchar(',',Color.DarkYellow),20); } } oiled_actors.AddUnique(target); M.Draw(); foreach(Actor a in oiled_actors){ if(a.TakeDamage(DamageType.FIRE,DamageClass.PHYSICAL,R.Roll(4,6),this,"a stream of scalding oil")){ if(a.IsBurning()){ a.ApplyBurning(); } else{ if(!a.HasAttr(AttrType.SLIMED,AttrType.FROZEN)){ a.attrs[AttrType.OIL_COVERED]++; B.Add(a.YouAre() + " covered in oil. ",a); } } } } Q1(); } else{ Q1(); } } break; } case ActorType.IMPOSSIBLE_NIGHTMARE: { if(DistanceFrom(target) == 1){ Attack(0,target); } else{ Tile t = target.TilesAtDistance(DistanceFrom(target)-1).Where(x=>x.passable && x.actor() == null).RandomOrDefault(); if(t != null){ Move(t.row,t.col); //todo: fear effect? } QS(); } break; } case ActorType.FIRE_DRAKE: /*if(player.magic_trinkets.Contains(MagicTrinketType.RING_OF_RESISTANCE) && DistanceFrom(player) <= 12 && CanSee(player)){ B.Add(the_name + " exhales an orange mist toward you. "); foreach(Tile t in GetBestLineOfEffect(player)){ Screen.AnimateStorm(t.p,1,2,3,'*',Color.Red); } B.Add("Your ring of resistance melts and drips onto the floor! "); player.magic_trinkets.Remove(MagicTrinketType.RING_OF_RESISTANCE); Q.Add(new Event(this,100,EventType.MOVE)); } else{ if(player.EquippedArmor == ArmorType.FULL_PLATE_OF_RESISTANCE && DistanceFrom(player) <= 12 && CanSee(player)){ B.Add(the_name + " exhales an orange mist toward you. "); foreach(Tile t in GetBestLine(player)){ Screen.AnimateStorm(t.p,1,2,3,'*',Color.Red); } B.Add("The runes drip from your full plate of resistance! "); player.EquippedArmor = ArmorType.FULL_PLATE; player.UpdateOnEquip(ArmorType.FULL_PLATE_OF_RESISTANCE,ArmorType.FULL_PLATE); Q.Add(new Event(this,100,EventType.MOVE)); } else{*/ if(!HasAttr(AttrType.COOLDOWN_1)){ if(DistanceFrom(target) <= 12){ attrs[AttrType.COOLDOWN_1]++; int cooldown = (R.Roll(1,4)+1) * 100; Q.Add(new Event(this,cooldown,AttrType.COOLDOWN_1)); AnimateBeam(target,Color.RandomFire,'*'); B.Add(TheName(true) + " breathes fire. ",target); target.TakeDamage(DamageType.FIRE,DamageClass.PHYSICAL,R.Roll(6,6),this,"*roasted by fire breath"); target.ApplyBurning(); Q.Add(new Event(this,200,EventType.MOVE)); } else{ AI_Step(target); QS(); } } else{ if(DistanceFrom(target) == 1){ Attack(R.Roll(1,2)-1,target); } else{ AI_Step(target); QS(); } } //} //} break; case ActorType.GHOST: { attrs[AttrType.AGGRESSION_MESSAGE_PRINTED] = 1; bool tombstone = false; foreach(Tile t in TilesWithinDistance(1)){ if(t.type == TileType.TOMBSTONE){ tombstone = true; } } if(!tombstone){ B.Add("The ghost vanishes. ",this); Kill(); return; } if(target == null || DistanceFrom(target) > 2){ List<Tile> valid = TilesAtDistance(1).Where(x=>x.TilesWithinDistance(1).Any(y=>y.type == TileType.TOMBSTONE)); if(valid.Count > 0){ AI_Step(valid.Random()); } QS(); } else{ if(DistanceFrom(target) == 1){ Attack(0,target); } else{ List<Tile> valid = tile().NeighborsBetween(target.row,target.col).Where(x=>x.passable && x.actor() == null && x.TilesWithinDistance(1).Any(y=>y.type == TileType.TOMBSTONE)); if(valid.Count == 0){ valid = TilesAtDistance(1).Where(x=>x.TilesWithinDistance(1).Any(y=>y.type == TileType.TOMBSTONE)); } if(valid.Count > 0){ AI_Step(valid.Random()); } QS(); } } break; } case ActorType.BLADE: { attrs[AttrType.AGGRESSION_MESSAGE_PRINTED] = 1; List<Actor> valid_targets = new List<Actor>(); //this is based on EnragedMove(), with an exception for other blades int max_dist = Math.Max(Math.Max(row,col),Math.Max(ROWS-row,COLS-col)); //this should find the farthest edge of the map for(int i=1;i<max_dist && valid_targets.Count == 0;++i){ foreach(Actor a in ActorsAtDistance(i)){ if(a.type != ActorType.BLADE && CanSee(a) && HasLOE(a)){ valid_targets.Add(a); } } } if(valid_targets.Count > 0){ if(target == null || !valid_targets.Contains(target)){ //keep old target if possible target = valid_targets.Random(); } if(DistanceFrom(target) == 1){ Attack(0,target); } else{ AI_Step(target); QS(); } } else{ if(target != null){ SeekAI(); } else{ QS(); } } break; } case ActorType.PHANTOM_CONSTRICTOR: case ActorType.PHANTOM_WASP: { if(DistanceFrom(target) == 1){ Attack(0,target); } else{ List<Tile> tiles = new List<Tile>(); //i should turn this "slither" movement into a standardized attribute or something if(target.row == row || target.col == col){ int targetdir = DirectionOf(target); for(int i=-1;i<=1;++i){ pos adj = p.PosInDir(targetdir.RotateDir(true,i)); if(M.tile[adj].passable && M.actor[adj] == null){ tiles.Add(M.tile[adj]); } } } if(tiles.Count > 0){ AI_Step(tiles.Random()); } else{ AI_Step(target); } QS(); } break; } case ActorType.MINOR_DEMON: case ActorType.FROST_DEMON: case ActorType.BEAST_DEMON: case ActorType.DEMON_LORD: { int damage_threshold = 1; if(type == ActorType.BEAST_DEMON){ damage_threshold = 0; } if(target == player && attrs[AttrType.COOLDOWN_2] > damage_threshold && CanSee(target)){ switch(type){ case ActorType.MINOR_DEMON: case ActorType.BEAST_DEMON: if(DistanceFrom(target) == 1){ Attack(0,target); } else{ AI_Step(target); QS(); } break; case ActorType.FROST_DEMON: if(!HasAttr(AttrType.COOLDOWN_1) && DistanceFrom(target) <= 12 && FirstActorInLine(target) == target){ attrs[AttrType.COOLDOWN_1] = 1; AnimateProjectile(target,'*',Color.RandomIce); foreach(Tile t in GetBestLineOfEffect(target)){ t.ApplyEffect(DamageType.COLD); } B.Add(TheName(true) + " fires a chilling sphere. ",target); if(target.TakeDamage(DamageType.COLD,DamageClass.PHYSICAL,R.Roll(3,6),this,"a frost demon")){ target.ApplyStatus(AttrType.SLOWED,R.Between(4,7)*100); //target.RefreshDuration(AttrType.SLOWED,R.Between(4,7)*100,target.YouAre() + " no longer slowed. ",target); } Q1(); } else{ if(DistanceFrom(target) == 1){ Attack(0,target); } else{ AI_Step(target); QS(); } } break; case ActorType.DEMON_LORD: if(DistanceFrom(target) > 2){ AI_Step(target); QS(); } else{ if(FirstActorInLine(target) != null){ Attack(0,target); } else{ if(AI_Step(target)){ QS(); } else{ AI_Sidestep(target); QS(); } } } break; } } else{ if(row >= 7 && row <= 12 && col >= 30 && col <= 35){ //near the center foreach(Actor a in ActorsAtDistance(1)){ if(a.IsFinalLevelDemon()){ List<Tile> dist2 = new List<Tile>(); foreach(Tile t in TilesWithinDistance(5)){ if(t.TilesAtDistance(2).Any(x=>x.type == TileType.FIRE_RIFT) && !t.TilesAtDistance(1).Any(x=>x.type == TileType.FIRE_RIFT)){ dist2.Add(t); } } //if there's another distance 2 (from the center) tile with no adjacent demons, move there //List<Tile> valid = dist2.Where(x=>DistanceFrom(x) == 1 && x.actor() == null && !x.TilesAtDistance(1).Any(y=>y.actor() != null && y.actor().Is(ActorType.MINOR_DEMON,ActorType.FROST_DEMON,ActorType.BEAST_DEMON,ActorType.DEMON_LORD))); List<Tile> valid = dist2.Where(x=>DistanceFrom(x) == 1); valid = valid.Where(x=>x.actor() == null && !x.TilesAtDistance(1).Any(y=>y.actor() != null && y.actor() != this && y.actor().IsFinalLevelDemon())); if(valid.Count > 0){ AI_Step(valid.Random()); } break; } } if(player.HasLOS(this)){ B.Add(TheName(true) + " chants. ",this); } M.IncrementClock(); Q1(); } else{ if(path != null && path.Count > 0){ if(!PathStep()){ QS(); } } else{ FindPath(9+R.Between(0,1),32+R.Between(0,1)); if(!PathStep()){ QS(); } } } } break; } default: if(DistanceFrom(target) == 1){ Attack(0,target); } else{ AI_Step(target); QS(); } break; } }