public bool Attack(int attack_idx,Actor a,bool attack_is_part_of_another_action) { //returns true if attack hit AttackInfo info = attack[type][attack_idx]; pos original_pos = p; pos target_original_pos = a.p; if(EquippedWeapon.type != WeaponType.NO_WEAPON){ info = EquippedWeapon.Attack(); } info.damage.source = this; if(a.HasFeat(FeatType.DEFLECT_ATTACK) && DistanceFrom(a) == 1){ //Actor other = a.ActorsWithinDistance(1).Where(x=>x.DistanceFrom(this) == 1).Random(); Actor other = a.ActorsWithinDistance(1).Where(x=>x != this).RandomOrDefault(); if(other != a){ B.Add(a.You("deflect") + "! ",this,a); return Attack(attack_idx,other,attack_is_part_of_another_action); } } if(!attack_is_part_of_another_action && StunnedThisTurn()){ return false; } if(!attack_is_part_of_another_action && exhaustion == 100 && R.CoinFlip()){ B.Add(You("fumble") + " from exhaustion. ",this); Q1(); //this is checked in PlayerWalk if attack_is_part_of_another_action is true return false; } if(!attack_is_part_of_another_action && this == player && EquippedWeapon.status[EquipmentStatus.POSSESSED] && R.CoinFlip()){ List<Actor> actors = ActorsWithinDistance(1); Actor chosen = actors.RandomOrDefault(); if(chosen != a){ if(chosen == this){ B.Add("Your possessed " + EquippedWeapon.NameWithEnchantment() + " tries to attack you! "); B.Add("You fight it off! "); //this is also checked in PlayerWalk if attack_is_part_of_another_action is true Q1(); return false; } else{ return Attack(attack_idx,chosen); } } } bool player_in_combat = false; if(this == player || a == player){ player_in_combat = true; } /*if(a == player && (type == ActorType.DREAM_WARRIOR_CLONE || type == ActorType.DREAM_SPRITE_CLONE)){ player_in_combat = false; }*/ if(player_in_combat){ player.attrs[AttrType.IN_COMBAT]++; } if(a.HasAttr(AttrType.CAN_DODGE) && a.CanSee(this)){ int dodge_dir = R.Roll(9); Tile dodge_tile = a.TileInDirection(dodge_dir); bool failed_to_dodge = false; if(HasAttr(AttrType.CONFUSED,AttrType.SLOWED,AttrType.STUNNED) && R.CoinFlip()){ failed_to_dodge = true; } if(a.tile().Is(FeatureType.WEB) && !a.HasAttr(AttrType.BURNING,AttrType.OIL_COVERED,AttrType.SLIMED,AttrType.BRUTISH_STRENGTH)){ failed_to_dodge = true; } if(!failed_to_dodge && dodge_tile.passable && dodge_tile.actor() == null && !a.MovementPrevented(dodge_tile) && !a.HasAttr(AttrType.PARALYZED)){ B.Add(a.You("dodge") + " " + YourVisible() + " attack. ",this,a); if(player.CanSee(a)){ Help.TutorialTip(TutorialTopic.Dodging); } if(a == player){ B.DisplayNow(); Screen.AnimateMapCell(a.row,a.col,new colorchar('!',Color.Green),80); } a.Move(dodge_tile.row,dodge_tile.col); if(a != player && DistanceFrom(dodge_tile) > 1){ M.Draw(); Thread.Sleep(40); } if(!attack_is_part_of_another_action){ Q.Add(new Event(this,info.cost)); } return false; } } if(a.HasFeat(FeatType.CUNNING_DODGE) && !this.HasAttr(AttrType.DODGED)){ attrs[AttrType.DODGED]++; B.Add(a.You("dodge") + " " + YourVisible() + " attack. ",this,a); if(!attack_is_part_of_another_action){ Q.Add(new Event(this,info.cost)); } return false; } if(IsInvisibleHere() || a.IsInvisibleHere()){ Help.TutorialTip(TutorialTopic.FightingTheUnseen); } //pos pos_of_target = new pos(a.row,a.col); bool a_moved_last_turn = !a.HasAttr(AttrType.TURNS_HERE); bool drive_back_applied = HasFeat(FeatType.DRIVE_BACK); if(drive_back_applied && !ConfirmsSafetyPrompts(a.tile())){ drive_back_applied = false; } bool drive_back_nowhere_to_run = false; if(!attack_is_part_of_another_action && drive_back_applied){ //doesn't work while moving drive_back_nowhere_to_run = true; int dir = DirectionOf(a); foreach(int next_dir in new List<int>{dir,dir.RotateDir(true),dir.RotateDir(false)}){ Tile t = a.TileInDirection(next_dir); if(t.passable && t.actor() == null && !a.MovementPrevented(t)){ drive_back_nowhere_to_run = false; break; } } /*if(a.TileInDirection(dir).passable && a.ActorInDirection(dir) == null && !a.GrabPreventsMovement(TileInDirection(dir))){ drive_back_nowhere_to_run = false; } if(a.TileInDirection(dir.RotateDir(true)).passable && a.ActorInDirection(dir.RotateDir(true)) == null && !a.GrabPreventsMovement(TileInDirection(dir.RotateDir(true)))){ drive_back_nowhere_to_run = false; } if(a.TileInDirection(dir.RotateDir(false)).passable && a.ActorInDirection(dir.RotateDir(false)) == null && !a.GrabPreventsMovement(TileInDirection(dir.RotateDir(false)))){ drive_back_nowhere_to_run = false; }*/ if(a.tile().IsSlippery() && !(a.tile().Is(TileType.ICE) && a.type == ActorType.FROSTLING)){ if(R.OneIn(5) && !HasAttr(AttrType.FLYING,AttrType.NONEUCLIDEAN_MOVEMENT) && !Is(ActorType.GIANT_SLUG,ActorType.MACHINE_OF_WAR,ActorType.MUD_ELEMENTAL)){ drive_back_nowhere_to_run = true; } } if(a.HasAttr(AttrType.FROZEN) || a.HasAttr(AttrType.IMMOBILE)){ drive_back_nowhere_to_run = true; //todo: exception for noneuclidean monsters? i think they'll just move out of the way. } } bool obscured_vision_miss = false; { bool fog = false; bool hidden = false; if((this.tile().Is(FeatureType.FOG,FeatureType.THICK_DUST) || a.tile().Is(FeatureType.FOG,FeatureType.THICK_DUST))){ fog = true; } if(a.IsHiddenFrom(this) || !CanSee(a) || (a.IsInvisibleHere() && !HasAttr(AttrType.BLINDSIGHT))){ hidden = true; } if(!HasAttr(AttrType.DETECTING_MONSTERS) && (fog || hidden) && R.CoinFlip()){ obscured_vision_miss = true; } } int plus_to_hit = TotalSkill(SkillType.COMBAT); bool sneak_attack = false; if(this.IsHiddenFrom(a) || !a.CanSee(this) || (this == player && IsInvisibleHere() && !a.HasAttr(AttrType.BLINDSIGHT))){ sneak_attack = true; a.attrs[AttrType.SEES_ADJACENT_PLAYER] = 1; if(DistanceFrom(a) > 2 && this != player){ sneak_attack = false; //no phantom blade sneak attacks from outside your view - but the player can sneak attack at this range with a wand of reach. } } //...insert any other changes to sneak attack calculation here... if(sneak_attack || HasAttr(AttrType.LUNGING_AUTO_HIT) || (EquippedWeapon == Dagger && !tile().IsLit()) || (EquippedWeapon == Staff && a_moved_last_turn) || a.HasAttr(AttrType.SWITCHING_ARMOR)){ //some attacks get +25% accuracy. this usually totals 100% vs. unarmored targets. plus_to_hit += 25; } plus_to_hit -= a.TotalSkill(SkillType.DEFENSE) * 3; bool attack_roll_hit = a.IsHit(plus_to_hit); bool blocked_by_armor_miss = false; bool blocked_by_root_shell_miss = false; bool mace_through_armor = false; if(!attack_roll_hit){ int armor_value = a.TotalProtectionFromArmor(); if(a != player){ armor_value = a.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){ bool mace = (EquippedWeapon == Mace || type == ActorType.CRUSADING_KNIGHT || type == ActorType.PHANTOM_CRUSADER); if(mace){ attack_roll_hit = true; mace_through_armor = true; } else{ if(type == ActorType.CORROSIVE_OOZE || type == ActorType.LASHER_FUNGUS){ //this is a bit hacky, but these are the only ones that aren't stopped by armor right now. attack_roll_hit = true; } else{ blocked_by_armor_miss = true; } } } else{ if(a.HasAttr(AttrType.ROOTS) && roll <= (armor_value + 10) * 3){ //potion of roots gives 10 defense blocked_by_root_shell_miss = true; } } } bool hit = true; if(obscured_vision_miss){ //this calculation turned out to be pretty complicated hit = false; } else{ if(blocked_by_armor_miss || blocked_by_root_shell_miss){ hit = false; } else{ if(drive_back_nowhere_to_run || attack_roll_hit){ hit = true; } else{ hit = false; } } } if(a.HasAttr(AttrType.GRABBED) && attrs[AttrType.GRABBING] == DirectionOf(a)){ hit = true; //one more modifier: automatically hit things you're grabbing. } bool weapon_just_poisoned = false; if(!hit){ if(blocked_by_armor_miss){ bool initial_message_printed = false; //for better pronoun usage if(info.blocked != ""){ initial_message_printed = true; string s = info.blocked + ". "; int pos = -1; do{ pos = s.IndexOf('&'); if(pos != -1){ s = s.Substring(0,pos) + TheName(true) + s.Substring(pos+1); } } while(pos != -1); // do{ pos = s.IndexOf('*'); if(pos != -1){ s = s.Substring(0,pos) + a.TheName(true) + s.Substring(pos+1); } } while(pos != -1); B.Add(s,this,a); } if(a.HasFeat(FeatType.ARMOR_MASTERY) && !(a.type == ActorType.ALASI_SCOUT && attack_idx == 1)){ B.Add(a.YourVisible() + " armor blocks the attack, leaving " + TheName(true) + " off-balance. ",a,this); RefreshDuration(AttrType.SUSCEPTIBLE_TO_CRITS,100); } else{ if(initial_message_printed){ B.Add(a.YourVisible() + " armor blocks the attack. ",this,a); } else{ B.Add(a.YourVisible() + " armor blocks " + YourVisible() + " attack. ",this,a); } } if(a.EquippedArmor.type == ArmorType.FULL_PLATE && !HasAttr(AttrType.BRUTISH_STRENGTH)){ a.IncreaseExhaustion(3); Help.TutorialTip(TutorialTopic.HeavyPlateArmor); } } else{ if(blocked_by_root_shell_miss){ B.Add(a.YourVisible() + " root shell blocks " + YourVisible() + " attack. ",this,a); } else{ if(obscured_vision_miss){ B.Add(Your() + " attack goes wide. ",this); } else{ if(!attack_is_part_of_another_action && drive_back_applied && !MovementPrevented(M.tile[target_original_pos])){ B.Add(You("drive") + " " + a.TheName(true) + " back. ",this,a); /*if(!a.HasAttr(AttrType.FROZEN) && !HasAttr(AttrType.FROZEN)){ a.AI_Step(this,true); AI_Step(a); }*/ Tile dest = null; int dir = DirectionOf(target_original_pos); foreach(int next_dir in new List<int>{dir,dir.RotateDir(true),dir.RotateDir(false)}){ Tile t = a.TileInDirection(next_dir); if(t.passable && t.actor() == null && !a.MovementPrevented(t)){ dest = t; break; } } if(dest != null){ a.AI_MoveOrOpen(dest.row,dest.col); if(M.actor[target_original_pos] == null){ AI_MoveOrOpen(target_original_pos.row,target_original_pos.col); } } } else{ if(info.miss != ""){ string s = info.miss + ". "; int pos = -1; do{ pos = s.IndexOf('&'); if(pos != -1){ s = s.Substring(0,pos) + TheName(true) + s.Substring(pos+1); } } while(pos != -1); // do{ pos = s.IndexOf('*'); if(pos != -1){ s = s.Substring(0,pos) + a.TheName(true) + s.Substring(pos+1); } } while(pos != -1); B.Add(s,this,a); } else{ B.Add(YouVisible("miss",true) + " " + a.TheName(true) + ". ",this,a); } } } } } if(type == ActorType.SWORDSMAN || type == ActorType.PHANTOM_SWORDMASTER || type == ActorType.ALASI_SOLDIER){ attrs[AttrType.COMBO_ATTACK] = 0; } } else{ string s = info.hit + ". "; if(!attack_is_part_of_another_action && HasFeat(FeatType.NECK_SNAP) && a.HasAttr(AttrType.MEDIUM_HUMANOID) && (IsHiddenFrom(a) || a.IsHelpless())){ if(!a.HasAttr(AttrType.RESIST_NECK_SNAP)){ B.Add(You("silently snap") + " " + a.Your() + " neck. "); a.Kill(); Q1(); return true; } else{ B.Add(You("silently snap") + " " + a.Your() + " neck. "); B.Add("It doesn't seem to affect " + a.the_name + ". "); } } bool crit = false; int crit_chance = 8; //base crit rate is 1/8 if(EquippedWeapon.type == WeaponType.DAGGER && !tile().IsLit()){ crit_chance /= 2; } if(a.EquippedArmor != null && (a.EquippedArmor.status[EquipmentStatus.WEAK_POINT] || a.EquippedArmor.status[EquipmentStatus.DAMAGED] || a.HasAttr(AttrType.SWITCHING_ARMOR))){ crit_chance /= 2; } if(a.HasAttr(AttrType.SUSCEPTIBLE_TO_CRITS)){ //caused by armor mastery crit_chance /= 2; } if(EquippedWeapon.enchantment == EnchantmentType.PRECISION && !EquippedWeapon.status[EquipmentStatus.NEGATED]){ crit_chance /= 2; } if(drive_back_nowhere_to_run){ crit_chance /= 2; } if(crit_chance <= 1 || R.OneIn(crit_chance)){ crit = true; } int pos = -1; do{ pos = s.IndexOf('&'); if(pos != -1){ s = s.Substring(0,pos) + TheName(true) + s.Substring(pos+1); } } while(pos != -1); // do{ pos = s.IndexOf('*'); if(pos != -1){ s = s.Substring(0,pos) + a.TheName(true) + s.Substring(pos+1); } } while(pos != -1); int dice = info.damage.dice; if(sneak_attack && crit && this == player){ if(!a.HasAttr(AttrType.NONLIVING,AttrType.PLANTLIKE,AttrType.BOSS_MONSTER) && a.type != ActorType.CYCLOPEAN_TITAN){ switch(EquippedWeapon.type){ //todo: should this check for shielded/blocking? case WeaponType.SWORD: B.Add("You run " + a.TheName(true) + " through! "); break; case WeaponType.MACE: B.Add("You bash " + a.YourVisible() + " head in! "); break; case WeaponType.DAGGER: B.Add("You pierce one of " + a.YourVisible() + " vital organs! "); break; case WeaponType.STAFF: B.Add("You bring your staff down on " + a.YourVisible() + " head with a loud crack! "); break; case WeaponType.BOW: B.Add("You choke " + a.TheName(true) + " with your bowstring! "); break; default: break; } Help.TutorialTip(TutorialTopic.InstantKills); MakeNoise(6); if(a.type == ActorType.BERSERKER && a.target == this){ a.attrs[AttrType.SHIELDED] = 0; a.TakeDamage(DamageType.NORMAL,DamageClass.NO_TYPE,a.curhp,this); } else{ a.Kill(); } if(!attack_is_part_of_another_action){ Q1(); } return true; } } if(sneak_attack && (this == player || a == player)){ B.Add(YouVisible("strike") + " from hiding! "); if(type != ActorType.PLAYER){ if(a == player && attrs[AttrType.TURNS_VISIBLE] >= 0){ B.PrintAll(); } attrs[AttrType.TURNS_VISIBLE] = -1; attrs[AttrType.NOTICED] = 1; attrs[AttrType.DANGER_SENSED] = 1; } else{ a.player_visibility_duration = -1; a.attrs[AttrType.PLAYER_NOTICED] = 1; } } if(a == player){ if(a.HasAttr(AttrType.SWITCHING_ARMOR)){ B.Add("You're unguarded! "); } else{ if(a.EquippedArmor.status[EquipmentStatus.DAMAGED]){ B.Add("Your damaged armor leaves you open! "); } else{ if(crit && R.CoinFlip()){ if(a.EquippedArmor.status[EquipmentStatus.WEAK_POINT]){ B.Add(TheName(true) + " finds a weak point. "); } } } } } if(mace_through_armor){ if(type == ActorType.CRUSADING_KNIGHT || type == ActorType.PHANTOM_CRUSADER){ B.Add(YourVisible() + " huge mace punches through " + a.YourVisible() + " armor. ",this,a); } else{ B.Add(YourVisible() + " mace punches through " + a.YourVisible() + " armor. ",this,a); } } else{ B.Add(s,this,a); } if(crit && info.crit != AttackEffect.NO_CRIT){ if(this == player || a == player){ Help.TutorialTip(TutorialTopic.CriticalHits); } } if(a == player && !player.CanSee(this)){ Screen.AnimateMapCell(row,col,new colorchar('?',Color.DarkGray),50); } if(a.type == ActorType.GHOST && EquippedWeapon.enchantment != EnchantmentType.NO_ENCHANTMENT && !EquippedWeapon.status[EquipmentStatus.NEGATED]){ EquippedWeapon.status[EquipmentStatus.NEGATED] = true; B.Add(Your() + " " + EquippedWeapon.NameWithEnchantment() + "'s magic is suppressed! ",this); Help.TutorialTip(TutorialTopic.Negated); } if(!Help.displayed[TutorialTopic.SwitchingEquipment] && this == player && a.Is(ActorType.SPORE_POD,ActorType.SKELETON,ActorType.STONE_GOLEM,ActorType.MECHANICAL_KNIGHT,ActorType.MACHINE_OF_WAR) && EquippedWeapon.type == WeaponType.SWORD){ Help.TutorialTip(TutorialTopic.SwitchingEquipment); } int dmg = R.Roll(dice,6); bool no_max_damage_message = false; List<AttackEffect> effects = new List<AttackEffect>(); if(crit && info.crit != AttackEffect.NO_CRIT){ effects.AddUnique(info.crit); } if(info.effects != null){ foreach(AttackEffect effect in info.effects){ effects.AddUnique(effect); } } if(type == ActorType.DEMON_LORD && DistanceFrom(a) == 2){ effects.AddUnique(AttackEffect.PULL); } if(type == ActorType.SWORDSMAN && attrs[AttrType.COMBO_ATTACK] == 2){ effects.AddUnique(AttackEffect.BLEED); effects.AddUnique(AttackEffect.STRONG_KNOCKBACK); } if(type == ActorType.PHANTOM_SWORDMASTER && attrs[AttrType.COMBO_ATTACK] == 2){ effects.AddUnique(AttackEffect.PERCENT_DAMAGE); effects.AddUnique(AttackEffect.STRONG_KNOCKBACK); } if(type == ActorType.ALASI_SOLDIER){ if(attrs[AttrType.COMBO_ATTACK] == 1){ effects.AddUnique(AttackEffect.ONE_TURN_STUN); } else{ if(attrs[AttrType.COMBO_ATTACK] == 2){ effects.AddUnique(AttackEffect.ONE_TURN_PARALYZE); } } } if(type == ActorType.WILD_BOAR && HasAttr(AttrType.COOLDOWN_1)){ effects.AddUnique(AttackEffect.FLING); } if(type == ActorType.ALASI_SENTINEL && R.OneIn(3)){ effects.AddUnique(AttackEffect.FLING); } if(this == player && a.type == ActorType.CYCLOPEAN_TITAN && crit){ effects = new List<AttackEffect>(); //remove all other effects (so far) and check for edged weapons if(EquippedWeapon == Sword || EquippedWeapon == Dagger){ effects.Add(AttackEffect.PERMANENT_BLIND); } } if(EquippedWeapon.status[EquipmentStatus.POISONED]){ effects.AddUnique(AttackEffect.POISON); } if(HasAttr(AttrType.PSEUDO_VAMPIRIC)){ effects.AddUnique(AttackEffect.DRAIN_LIFE); } if(HasAttr(AttrType.BRUTISH_STRENGTH)){ effects.AddUnique(AttackEffect.MAX_DAMAGE); effects.AddUnique(AttackEffect.STRONG_KNOCKBACK); effects.Remove(AttackEffect.KNOCKBACK); //strong knockback replaces these effects.Remove(AttackEffect.TRIP); effects.Remove(AttackEffect.FLING); } if(EquippedWeapon != null && !EquippedWeapon.status[EquipmentStatus.NEGATED]){ switch(EquippedWeapon.enchantment){ case EnchantmentType.CHILLING: effects.AddUnique(AttackEffect.CHILL); break; case EnchantmentType.DISRUPTION: effects.AddUnique(AttackEffect.DISRUPTION); //not entirely sure that these should be crit effects break; case EnchantmentType.VICTORY: if(a.maxhp > 1){ // no illusions, phantoms, or minions effects.AddUnique(AttackEffect.VICTORY); } break; } } if(type == ActorType.SKITTERMOSS && HasAttr(AttrType.COOLDOWN_1)){ effects.Remove(AttackEffect.INFEST); } if(a.HasAttr(AttrType.NONLIVING)){ effects.Remove(AttackEffect.DRAIN_LIFE); } foreach(AttackEffect effect in effects){ //pre-damage effects - these can alter the amount of damage. switch(effect){ case AttackEffect.MAX_DAMAGE: dmg = Math.Max(dmg,dice * 6); break; case AttackEffect.PERCENT_DAMAGE: dmg = Math.Max(dmg,(a.maxhp+1)/2); no_max_damage_message = true; if(!EquippedWeapon.status[EquipmentStatus.DULLED]){ if(this == player){ B.Add("Your sword cuts deep! "); } else{ B.Add(Your() + " attack cuts deep! ",this); } } break; case AttackEffect.ONE_HP: dmg = a.curhp - 1; if(a.HasAttr(AttrType.VULNERABLE)){ a.attrs[AttrType.VULNERABLE] = 0; } if(a == player){ B.Add("You shudder. "); } no_max_damage_message = true; break; } } if(dice < 2){ no_max_damage_message = true; } if(a.type == ActorType.SPORE_POD && EquippedWeapon.IsBlunt()){ no_max_damage_message = true; } if(EquippedWeapon.status[EquipmentStatus.MERCIFUL]){ no_max_damage_message = true; } if(a.HasAttr(AttrType.RESIST_WEAPONS) && EquippedWeapon.type != WeaponType.NO_WEAPON){ B.Add("Your " + EquippedWeapon.NameWithoutEnchantment() + " isn't very effective. "); dmg = dice; //minimum damage } else{ if(EquippedWeapon.status[EquipmentStatus.DULLED]){ B.Add("Your dull " + EquippedWeapon.NameWithoutEnchantment() + " isn't very effective. "); dmg = dice; //minimum damage } else{ if(type == ActorType.MUD_TENTACLE){ //getting surrounded by these guys should be dangerous, but not simply because of their damage. dmg = dice; } } } if(dmg >= dice * 6 && !no_max_damage_message){ if(this == player){ B.Add("It was a good hit! "); } else{ if(a == player){ B.Add("Ow! "); } } } dmg += TotalSkill(SkillType.COMBAT); if(a.type == ActorType.SPORE_POD && EquippedWeapon.IsBlunt()){ dmg = 0; dice = 0; effects.AddUnique(AttackEffect.STRONG_KNOCKBACK); B.Add("Your " + EquippedWeapon.NameWithoutEnchantment() + " knocks the spore pod away. ",a); } if(EquippedWeapon.status[EquipmentStatus.MERCIFUL] && dmg >= a.curhp){ dmg = a.curhp - 1; B.Add("Your " + EquippedWeapon.NameWithoutEnchantment() + " refuses to finish " + a.TheName(true) + ". "); B.Print(true); } if(a.HasAttr(AttrType.DULLS_BLADES) && R.CoinFlip() && (EquippedWeapon == Sword || EquippedWeapon == Dagger)){ EquippedWeapon.status[EquipmentStatus.DULLED] = true; B.Add(Your() + " " + EquippedWeapon.NameWithEnchantment() + " becomes dull! ",this); Help.TutorialTip(TutorialTopic.Dulled); } if(a.type == ActorType.CORROSIVE_OOZE && R.CoinFlip() && (EquippedWeapon == Sword || EquippedWeapon == Dagger || EquippedWeapon == Mace)){ EquippedWeapon.status[EquipmentStatus.DULLED] = true; B.Add("The acid dulls " + Your() + " " + EquippedWeapon.NameWithEnchantment() + "! ",this); Help.TutorialTip(TutorialTopic.Acidified); Help.TutorialTip(TutorialTopic.Dulled); } if(a.HasAttr(AttrType.CAN_POISON_WEAPONS) && R.CoinFlip() && EquippedWeapon.type != WeaponType.NO_WEAPON && !EquippedWeapon.status[EquipmentStatus.POISONED]){ EquippedWeapon.status[EquipmentStatus.POISONED] = true; weapon_just_poisoned = true; B.Add(Your() + " " + EquippedWeapon.NameWithEnchantment() + " is covered in poison! ",this); } int r = a.row; int c = a.col; bool still_alive = true; bool knockback_effect = effects.Contains(AttackEffect.KNOCKBACK) || effects.Contains(AttackEffect.STRONG_KNOCKBACK) || effects.Contains(AttackEffect.TRIP) || effects.Contains(AttackEffect.FLING) || effects.Contains(AttackEffect.SWAP_POSITIONS) || effects.Contains(AttackEffect.DRAIN_LIFE); if(knockback_effect){ a.attrs[AttrType.TURN_INTO_CORPSE]++; } Color blood = a.BloodColor(); bool homunculus = a.type == ActorType.HOMUNCULUS; if(dmg > 0){ Damage damage = new Damage(info.damage.type,info.damage.damclass,true,this,dmg); damage.weapon_used = EquippedWeapon.type; still_alive = a.TakeDamage(damage,a_name); } if(homunculus){ //todo: or will this happen on any major damage, not just melee attacks? M.tile[target_original_pos].AddFeature(FeatureType.OIL); } else{ if(blood != Color.Black && (!still_alive || !a.HasAttr(AttrType.FROZEN,AttrType.INVULNERABLE))){ /*List<Tile> valid = new List<Tile>{M.tile[target_original_pos]}; for(int i=-1;i<=1;++i){ valid.Add(M.tile[target_original_pos].TileInDirection(original_pos.DirectionOf(target_original_pos).RotateDir(true,i))); } for(int i=dmg/10;i>0;--i){ Tile t = valid.RemoveRandom(); if(t.Is(TileType.WALL) || t.name == "floor"){ t.color = blood; } }*/ List<Tile> cone = M.tile[target_original_pos].GetCone(original_pos.DirectionOf(target_original_pos),dmg>=20? 2 : 1,false); cone.Add(M.tile[target_original_pos].TileInDirection(original_pos.DirectionOf(target_original_pos))); cone.Add(M.tile[target_original_pos].TileInDirection(original_pos.DirectionOf(target_original_pos))); cone.Add(M.tile[target_original_pos].TileInDirection(original_pos.DirectionOf(target_original_pos))); for(int i=(dmg-5)/5;i>0;--i){ if(cone.Count == 0){ break; } Tile t = cone.Random(); while(cone.Remove(t)){ } //remove all if(t.Is(TileType.WALL) || t.name == "floor"){ t.color = blood; switch(blood){ case Color.DarkRed: M.aesthetics[t.p] = AestheticFeature.BloodDarkRed; break; default: M.aesthetics[t.p] = AestheticFeature.BloodOther; break; } } } } } if(still_alive){ //post-damage crit effects that require the target to still be alive foreach(AttackEffect effect in effects){ if(still_alive){ switch(effect){ //todo: some of these messages shouldn't be printed if the effect already exists case AttackEffect.CONFUSE: a.ApplyStatus(AttrType.CONFUSED,R.Between(2,3)*100); break; case AttackEffect.BLEED: if(!a.HasAttr(AttrType.NONLIVING,AttrType.FROZEN)){ if(a.HasAttr(AttrType.BLEEDING)){ if(a == player){ if(a.attrs[AttrType.BLEEDING] > 15){ B.Add("Your bleeding worsens. "); } else{ B.Add("You're bleeding badly now! "); } } else{ B.Add(a.YouAre() + " bleeding badly! ",a); } } a.attrs[AttrType.BLEEDING] += R.Between(10,15); if(a.attrs[AttrType.BLEEDING] > 25){ a.attrs[AttrType.BLEEDING] = 25; //this seems like a reasonable cap, so repeated bleed effects don't just last *forever*. } if(a == player){ Help.TutorialTip(TutorialTopic.Bleeding); } } break; case AttackEffect.BLIND: a.ApplyStatus(AttrType.BLIND,R.Between(5,7)*100); //B.Add(a.YouAre() + " blinded! ",a); //a.RefreshDuration(AttrType.BLIND,R.Between(5,7)*100); break; case AttackEffect.PERMANENT_BLIND: { if(!a.HasAttr(AttrType.COOLDOWN_1)){ B.Add("You drive your " + EquippedWeapon.NameWithoutEnchantment() + " into its eye, blinding it! "); Q.KillEvents(a,AttrType.BLIND); a.attrs[AttrType.BLIND] = 1; a.attrs[AttrType.COOLDOWN_1] = 1; } break; } case AttackEffect.DIM_VISION: if(a.ResistedBySpirit()){ B.Add(a.Your() + " vision is dimmed, but only for a moment. ",a); } else{ B.Add(a.Your() + " vision is dimmed. ",a); a.RefreshDuration(AttrType.DIM_VISION,(R.Roll(2,20)+20)*100); } break; case AttackEffect.CHILL: if(!a.HasAttr(AttrType.IMMUNE_COLD)){ B.Add(a.the_name + " is chilled. ",a); if(!a.HasAttr(AttrType.CHILLED)){ a.attrs[AttrType.CHILLED] = 1; } else{ a.attrs[AttrType.CHILLED] *= 2; } if(!a.TakeDamage(DamageType.COLD,DamageClass.MAGICAL,a.attrs[AttrType.CHILLED],this)){ still_alive = false; } } break; case AttackEffect.DISRUPTION: if(a.HasAttr(AttrType.NONLIVING)){ B.Add(a.the_name + " is disrupted. ",a); if(!a.TakeDamage(DamageType.MAGIC,DamageClass.MAGICAL,a.maxhp / 5,this)){ still_alive = false; } } break; case AttackEffect.FREEZE: a.tile().ApplyEffect(DamageType.COLD); a.ApplyFreezing(); break; case AttackEffect.GRAB: if(!HasAttr(AttrType.GRABBING) && DistanceFrom(a) == 1 && !a.HasAttr(AttrType.FROZEN)){ a.attrs[AttrType.GRABBED]++; attrs[AttrType.GRABBING] = DirectionOf(a); B.Add(YouVisible("grab") + " " + a.TheName(true) + ". ",this,a); if(a == player){ Help.TutorialTip(TutorialTopic.Grabbed); } } break; case AttackEffect.POISON: if(!a.HasAttr(AttrType.NONLIVING,AttrType.CAN_POISON_WEAPONS,AttrType.INVULNERABLE,AttrType.FROZEN)){ a.ApplyStatus(AttrType.POISONED,(R.Roll(2,6)+2)*100); } break; case AttackEffect.PARALYZE: if(!a.HasAttr(AttrType.NONLIVING) || type != ActorType.CARRION_CRAWLER){ if(a.ResistedBySpirit()){ B.Add(a.Your() + " muscles stiffen, but only for a moment. ",a); } else{ if(a == player){ B.Add("You suddenly can't move! "); } else{ B.Add(a.YouAre() + " paralyzed. ",a); } a.attrs[AttrType.PARALYZED] = R.Between(3,5); } } break; case AttackEffect.ONE_TURN_PARALYZE: Event e = Q.FindAttrEvent(a,AttrType.STUNNED); if(e != null && e.delay == 100 && e.TimeToExecute() == Q.turn){ //if the target was hit with a 1-turn stun that's about to expire, don't print a message for it. e.msg = ""; } if(a.ResistedBySpirit()){ B.Add(a.Your() + " muscles stiffen, but only for a moment. ",a); } else{ B.Add(a.YouAre() + " paralyzed! ",a); a.attrs[AttrType.PARALYZED] = 2; //setting it to 1 means it would end immediately } break; case AttackEffect.INFLICT_VULNERABILITY: a.ApplyStatus(AttrType.VULNERABLE,R.Between(2,4)*100); /*B.Add(a.You("become") + " vulnerable. ",a); a.RefreshDuration(AttrType.VULNERABLE,R.Between(2,4)*100);*/ if(a == player){ Help.TutorialTip(TutorialTopic.Vulnerable); } break; case AttackEffect.IGNITE: break; case AttackEffect.INFEST: if(a == player && !a.EquippedArmor.status[EquipmentStatus.INFESTED]){ B.Add("Thousands of insects crawl into your " + a.EquippedArmor.NameWithoutEnchantment() + "! "); a.EquippedArmor.status[EquipmentStatus.INFESTED] = true; Help.TutorialTip(TutorialTopic.Infested); } break; case AttackEffect.SLOW: a.ApplyStatus(AttrType.SLOWED,R.Between(4,6)*100); break; case AttackEffect.REDUCE_ACCURACY: //also about 2d4 turns? break; case AttackEffect.SLIME: B.Add(a.YouAre() + " covered in slime. ",a); a.attrs[AttrType.SLIMED] = 1; if(a == player){ Help.TutorialTip(TutorialTopic.Slimed); } break; case AttackEffect.STUN: //2d3 turns, at most { a.ApplyStatus(AttrType.STUNNED,R.Roll(2,3)*100); /*B.Add(a.YouAre() + " stunned! ",a); a.RefreshDuration(AttrType.STUNNED,a.DurationOfMagicalEffect(R.Roll(2,3)) * 100,a.YouAre() + " no longer stunned. ",a);*/ if(a == player){ Help.TutorialTip(TutorialTopic.Stunned); } break; } case AttackEffect.ONE_TURN_STUN: { a.ApplyStatus(AttrType.STUNNED,100); /*B.Add(a.YouAre() + " stunned! ",a); a.RefreshDuration(AttrType.STUNNED,100,a.YouAre() + " no longer stunned. ",a);*/ if(a == player){ Help.TutorialTip(TutorialTopic.Stunned); } break; } case AttackEffect.SILENCE: { if(a.ResistedBySpirit()){ if(!HasAttr(AttrType.SILENCED)){ B.Add(a.You("resist") + " being silenced. ",a); } } else{ if(!HasAttr(AttrType.SILENCED)){ B.Add(TheName(true) + " silences " + a.the_name + ". ",a); } a.RefreshDuration(AttrType.SILENCED,R.Between(3,4)*100,a.YouAre() + " no longer silenced. ",a); } if(a == player){ Help.TutorialTip(TutorialTopic.Silenced); } break; } case AttackEffect.WEAK_POINT: if(!a.EquippedArmor.status[EquipmentStatus.WEAK_POINT] && a == player){ a.EquippedArmor.status[EquipmentStatus.WEAK_POINT] = true; B.Add(YouVisible("expose") + " a weak point on your armor! ",this); Help.TutorialTip(TutorialTopic.WeakPoint); } break; case AttackEffect.WORN_OUT: if(a == player && !a.EquippedArmor.status[EquipmentStatus.DAMAGED]){ if(a.EquippedArmor.status[EquipmentStatus.WORN_OUT]){ a.EquippedArmor.status[EquipmentStatus.WORN_OUT] = false; a.EquippedArmor.status[EquipmentStatus.WEAK_POINT] = false; a.EquippedArmor.status[EquipmentStatus.DAMAGED] = true; B.Add(a.Your() + " " + a.EquippedArmor.NameWithEnchantment() + " is damaged! "); Help.TutorialTip(TutorialTopic.Damaged); } else{ a.EquippedArmor.status[EquipmentStatus.WORN_OUT] = true; B.Add(a.Your() + " " + a.EquippedArmor.NameWithEnchantment() + " looks worn out. "); Help.TutorialTip(TutorialTopic.WornOut); } } break; case AttackEffect.ACID: if(a == player && !a.HasAttr(AttrType.ACIDIFIED) && R.CoinFlip()){ a.RefreshDuration(AttrType.ACIDIFIED,300); if(a.EquippedArmor != a.Leather && !a.EquippedArmor.status[EquipmentStatus.DAMAGED]){ B.Add("The acid hisses as it touches your " + a.EquippedArmor.NameWithEnchantment() + "! "); if(a.EquippedArmor.status[EquipmentStatus.WORN_OUT]){ a.EquippedArmor.status[EquipmentStatus.WORN_OUT] = false; a.EquippedArmor.status[EquipmentStatus.WEAK_POINT] = false; a.EquippedArmor.status[EquipmentStatus.DAMAGED] = true; B.Add(a.Your() + " " + a.EquippedArmor.NameWithEnchantment() + " is damaged! "); } else{ a.EquippedArmor.status[EquipmentStatus.WORN_OUT] = true; B.Add(a.Your() + " " + a.EquippedArmor.NameWithEnchantment() + " looks worn out. "); } Help.TutorialTip(TutorialTopic.Acidified); } } break; case AttackEffect.PULL: { List<Tile> tiles = tile().NeighborsBetween(a.row,a.col).Where(x=>x.actor() == null && x.passable); if(tiles.Count > 0){ Tile t = tiles.Random(); if(!a.MovementPrevented(t)){ B.Add(TheName(true) + " pulls " + a.TheName(true) + " closer. ",this,a); a.Move(t.row,t.col); } } break; } case AttackEffect.STEAL: { if(a.inv != null && a.inv.Count > 0){ Item i = a.inv.Random(); Item stolen = i; if(i.quantity > 1){ stolen = new Item(i,i.row,i.col); stolen.revealed_by_light = i.revealed_by_light; i.quantity--; } else{ a.inv.Remove(stolen); } GetItem(stolen); B.Add(YouVisible("steal") + " " + a.YourVisible() + " " + stolen.SingularName() + "! ",this,a); B.PrintAll(); } break; } case AttackEffect.EXHAUST: { if(a == player){ B.Add("You feel fatigued. "); } a.IncreaseExhaustion(R.Roll(2,4)); break; } } } } } foreach(AttackEffect effect in effects){ //effects that don't care whether the target is still alive switch(effect){ case AttackEffect.DRAIN_LIFE: { if(curhp < maxhp){ curhp += 10; if(curhp > maxhp){ curhp = maxhp; } B.Add(You("drain") + " some life from " + a.TheName(true) + ". ",this); } break; } case AttackEffect.VICTORY: if(!still_alive){ curhp += 5; if(curhp > maxhp){ curhp = maxhp; } } break; case AttackEffect.STALAGMITES: { List<Tile> tiles = new List<Tile>(); foreach(Tile t in M.tile[r,c].TilesWithinDistance(1)){ //if(t.actor() == null && (t.type == TileType.FLOOR || t.type == TileType.STALAGMITE)){ if(t.actor() == null && t.inv == null && (t.IsTrap() || t.Is(TileType.FLOOR,TileType.GRAVE_DIRT,TileType.GRAVEL,TileType.STALAGMITE))){ if(R.CoinFlip()){ tiles.Add(t); } } } foreach(Tile t in tiles){ if(t.type == TileType.STALAGMITE){ Q.KillEvents(t,EventType.STALAGMITE); } else{ TileType previous_type = t.type; t.Toggle(this,TileType.STALAGMITE); t.toggles_into = previous_type; } } Q.Add(new Event(tiles,150,EventType.STALAGMITE)); break; } case AttackEffect.MAKE_NOISE: break; case AttackEffect.SWAP_POSITIONS: if(original_pos.DistanceFrom(target_original_pos) == 1 && p.Equals(original_pos) && M.actor[target_original_pos] != null && !M.actor[target_original_pos].HasAttr(AttrType.IMMOBILE)){ B.Add(YouVisible("move") + " past " + M.actor[target_original_pos].TheName(true) + ". ",this,M.actor[target_original_pos]); Move(target_original_pos.row,target_original_pos.col); } break; case AttackEffect.TRIP: if(!a.HasAttr(AttrType.FLYING) && (a.curhp > 0 || !a.HasAttr(AttrType.NO_CORPSE_KNOCKBACK))){ B.Add(YouVisible("trip") + " " + a.TheName(true) + ". ",this,a); a.IncreaseExhaustion(R.Between(2,4)); a.CollideWith(a.tile());//todo: if it's a corpse, ONLY trip it if something is going to happen when it collides with the floor. } break; case AttackEffect.KNOCKBACK: if(a.curhp > 0 || !a.HasAttr(AttrType.NO_CORPSE_KNOCKBACK)){ KnockObjectBack(a,3,this); } break; case AttackEffect.STRONG_KNOCKBACK: if(a.curhp > 0 || !a.HasAttr(AttrType.NO_CORPSE_KNOCKBACK)){ KnockObjectBack(a,5,this); } break; case AttackEffect.FLING: if(a.curhp > 0 || !a.HasAttr(AttrType.NO_CORPSE_KNOCKBACK)){ attrs[AttrType.JUST_FLUNG] = 1; int dir = DirectionOf(a).RotateDir(true,4); Tile t = null; if(tile().p.PosInDir(dir).PosInDir(dir).BoundsCheck(M.tile,true)){ Tile t2 = tile().TileInDirection(dir).TileInDirection(dir); if(HasLOE(t2)){ t = t2; } } if(t == null){ if(tile().p.PosInDir(dir).BoundsCheck(M.tile,false)){ Tile t2 = tile().TileInDirection(dir); if(HasLOE(t2)){ t = t2; } } } if(t == null){ t = tile(); } B.Add(YouVisible("fling") + " " + a.TheName(true) + "! ",this,a); foreach(Tile nearby in M.ReachableTilesByDistance(t.row,t.col,false)){ if(nearby.passable && nearby.actor() == null && HasLOE(nearby)){ a.Move(nearby.row,nearby.col); a.CollideWith(nearby); break; } } } break; } } if(knockback_effect){ if(a.curhp > 0 && this != player){ target_location = target.tile(); } a.CorpseCleanup(); } if(type == ActorType.SWORDSMAN || type == ActorType.PHANTOM_SWORDMASTER || type == ActorType.ALASI_SOLDIER){ if(attrs[AttrType.COMBO_ATTACK] == 1 && (type == ActorType.SWORDSMAN || type == ActorType.PHANTOM_SWORDMASTER)){ B.Add(the_name + " prepares a devastating strike! ",this); } attrs[AttrType.COMBO_ATTACK]++; if(attrs[AttrType.COMBO_ATTACK] == 3){ //all these have 3-part combos attrs[AttrType.COMBO_ATTACK] = 0; } } } /*if(!hit && HasAttr(AttrType.BRUTISH_STRENGTH) && p.Equals(original_pos) && M.actor[target_original_pos] != null){ Actor a2 = M.actor[target_original_pos]; if(a2.HasAttr(AttrType.NO_CORPSE_KNOCKBACK) && a2.maxhp == 1){ B.Add(YouVisible("push",true) + " " + a2.TheName(true) + ". ",this,a2); a2.TakeDamage(DamageType.NORMAL,DamageClass.PHYSICAL,false,1,this); } else{ a2.attrs[AttrType.TURN_INTO_CORPSE]++; KnockObjectBack(a2,5); a2.CorpseCleanup(); } }*/ if(!hit && sneak_attack && this != player){ attrs[AttrType.TURNS_VISIBLE] = -1; attrs[AttrType.NOTICED]++; } if(!attack_is_part_of_another_action && hit && HasAttr(AttrType.BRUTISH_STRENGTH) && p.Equals(original_pos) && M.actor[target_original_pos] == null && DistanceFrom(target_original_pos) == 1 && !MovementPrevented(M.tile[target_original_pos])){ Tile t = M.tile[target_original_pos]; if(t.IsTrap()){ t.SetName(Tile.Prototype(t.type).name); t.TurnToFloor(); } if(HasFeat(FeatType.WHIRLWIND_STYLE)){ WhirlwindMove(t.row,t.col); } else{ Move(t.row,t.col); } } if(hit && EquippedWeapon.enchantment == EnchantmentType.ECHOES && !EquippedWeapon.status[EquipmentStatus.NEGATED]){ List<Tile> line = GetBestExtendedLineOfEffect(target_original_pos.row,target_original_pos.col); int idx = line.IndexOf(M.tile[target_original_pos]); if(idx != -1 && line.Count > idx + 1){ Actor next = line[idx+1].actor(); if(next != null && next != this){ Attack(attack_idx,next,true); } } } //if(!attack_is_part_of_another_action && EquippedWeapon == Staff && p.Equals(original_pos) && a_moved_last_turn && !HasAttr(AttrType.IMMOBILE) && M.tile[target_original_pos].passable && (M.actor[target_original_pos] == null || !M.actor[target_original_pos].HasAttr(AttrType.IMMOBILE))){ if(!attack_is_part_of_another_action && EquippedWeapon == Staff && p.Equals(original_pos) && a_moved_last_turn && !MovementPrevented(M.tile[target_original_pos]) && M.tile[target_original_pos].passable && (M.actor[target_original_pos] == null || !M.actor[target_original_pos].MovementPrevented(this))){ if(M.actor[target_original_pos] != null){ M.actor[target_original_pos].attrs[AttrType.TURNS_HERE]++; //this is a hack to prevent fast monsters from swapping *back* on the next hit. } if(HasFeat(FeatType.WHIRLWIND_STYLE)){ WhirlwindMove(target_original_pos.row,target_original_pos.col,true,new List<Actor>{M.actor[target_original_pos]}); //whirlwind move, but don't attack the original target again } else{ Move(target_original_pos.row,target_original_pos.col); } } if(!attack_is_part_of_another_action && EquippedWeapon.status[EquipmentStatus.POISONED] && !weapon_just_poisoned && R.OneIn(16)){ ApplyStatus(AttrType.POISONED,(R.Roll(2,6)+2)*100,"You manage to poison yourself with your " + EquippedWeapon.NameWithoutEnchantment() + ". ","","You resist the poison dripping from your " + EquippedWeapon.NameWithoutEnchantment() + ". "); } if(!attack_is_part_of_another_action && EquippedWeapon.status[EquipmentStatus.HEAVY] && R.CoinFlip() && !HasAttr(AttrType.BRUTISH_STRENGTH)){ B.Add("Attacking with your heavy " + EquippedWeapon.NameWithoutEnchantment() + " exhausts you. "); IncreaseExhaustion(5); } MakeNoise(6); if(!attack_is_part_of_another_action){ Q.Add(new Event(this,info.cost)); } return hit; }
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; } }