/// <summary> /// Deal damage to a target, and build and return the result. /// </summary> private static SpellResult HandleDamageSpellResult(Monster target, int damage) { SpellResult res = new SpellResult(damage); target.DamageHP(damage); if (target.IsDead()) { res.Results.Add(target.Name + " fell"); } return(res); }
private bool ExecuteActions() { turn = 0; foreach (Action action in GetSortedActionArray()) { // Update and display turn number turn++; turnTotal++; TextManager.SetTurnText(turn); // If an actor was killed before its turn, ignore the turn if (action.Actor == null || action.Actor.IsDead()) { continue; } // Apply the action to each target foreach (MonoMonster target in action.Targets) { // Ignore invalid target and spells against the dead if (target == null || (action.Spell != null && target.IsDead())) { continue; } TextManager.SetActorText(action.Actor.Name); Thread.Sleep(gameTick); // If the action is nothing, display and bail out if (action.Nothing) { TextManager.SetResultsText("Nothing"); continue; } TextManager.SetTargetText(target.Name); Thread.Sleep(gameTick); if (action.Physical) { // Physical attacks can target the dead, but are ineffective if (target.IsDead()) { TextManager.SetResultsText("Ineffective"); continue; } // Apply the attack and display the results AttackResult atkRes = AttackManager.AttackMonster(action.Actor, target); // On a miss, display and bail out if (string.Equals(atkRes.DamageMessage, "Miss")) { TextManager.SetDamageText("Miss"); continue; } // Flicker sprite // TODO: Different sounds and animations need to play based on the attack type if (gameTick > 30) { SoundManager.PlaySound(SoundManager.Sound.Physical); target.Flicker(16); } TextManager.SetHitsText(atkRes.HitsMessage); Thread.Sleep(gameTick); TextManager.SetDamageText(atkRes.DamageMessage); Thread.Sleep(gameTick); // Fade the monster if dead // TODO: This is awkward here if (target.IsDead()) { target.StartDeath(); Thread.Sleep(30); // 300 } // Display each result, tearing down existing results as needed for (int i = 0; i < atkRes.Results.Count; i++) { string res = atkRes.Results[i]; TextManager.SetResultsText(res); if (i < atkRes.Results.Count - 1) { Thread.Sleep(gameTick * 2); TextManager.TearDownResults(); Thread.Sleep(teardownTick); } } break; } else { // Casting a spell TextManager.SetHitsText(action.Spell.Name + " " + action.SpellLevel); Thread.Sleep(gameTick); // Cast the spell and display the results SpellResult spellRes = SpellManager.CastSpell(action.Actor, target, action.Spell, action.SpellLevel, action.Targets.Count > 1); // Set the spell animation and sound based on the spell's effect (kinda). This is awkward but fine for now. MagicSprite.MagicAnimation magicAnim = MagicSprite.MagicAnimation.Attack; SoundManager.Sound magicSnd = SoundManager.Sound.AttackSpell; if (action.Spell.Effect == "Buff") { magicAnim = MagicSprite.MagicAnimation.Buff; magicSnd = SoundManager.Sound.Buff; } if (action.Spell.Effect == "Debuff" || action.Spell.Effect == "TempStatus" || action.Spell.Effect == "PermStatus") { magicAnim = MagicSprite.MagicAnimation.Debuff; magicSnd = SoundManager.Sound.Debuff; } if (action.Spell.Effect == "Heal" || action.Spell.Effect == "Revive" || action.Spell.Effect == "ItemFullRestore") { magicAnim = MagicSprite.MagicAnimation.Heal; magicSnd = SoundManager.Sound.Heal; } MagicSpriteManager.GenerateSpellBurst((int)target.Position.X, (int)target.Position.Y, target.Width, target.Height, magicAnim); SoundManager.PlaySound(magicSnd); Thread.Sleep(SPELL_ANIMATION_DELAY); if (spellRes.Damage >= 0) { TextManager.SetDamageText(spellRes.Damage.ToString()); Thread.Sleep(gameTick); } // Kill the target if it's dead. // TODO: This is weird here, should be handled by the monster itself? if (target.IsDead()) { target.StartDeath(); Thread.Sleep(300); } // Display each result, tearing down existing results as needed for (int i = 0; i < spellRes.Results.Count; i++) { string res = spellRes.Results[i]; TextManager.SetResultsText(res); if (i < spellRes.Results.Count - 1) { Thread.Sleep(gameTick * 2); TextManager.TearDownResults(); Thread.Sleep(teardownTick); } } // Tear down between each target Thread.Sleep(gameTick * 5); while (TextManager.TearDownText()) { Thread.Sleep(teardownTick); } } // TODO: If both scenes only contain "Soul" enemies, they cannot kill eachother if (!sceneOne.HasLivingMonsters() || !sceneTwo.HasLivingMonsters() || round >= ROUND_LIMIT) { break; } } // Turn end, clean up text display Thread.Sleep(gameTick * 5); while (TextManager.TearDownText()) { Thread.Sleep(teardownTick); } if (!sceneOne.HasLivingMonsters() || !sceneTwo.HasLivingMonsters() || round >= ROUND_LIMIT) { break; } } return(false); }
/// <summary> /// Cast a spell against a target. Applies the effects and returns the result. /// </summary> /// <param name="multiTarget">Whether the spell being cast is targeting multiple monsters. Halves accuracy and quarters power.</param> /// <returns>Result of casting the spell</returns> public static SpellResult CastSpell(Monster caster, Monster target, Spell spell, int level, bool multiTarget = false) { // Catch the errors first if (caster == null || target == null || spell == null) { throw new ArgumentNullException("Invalid parameter provided"); } if (level < 0 || level > 16) { throw new ArgumentOutOfRangeException("Level out of range. Must be 0-16. Found: " + level); } // Helpers SpellResult failedResult = new SpellResult(new List <string> { FAILED_SPELL_MESSAGE }); SpellResult statusSuccessResult = new SpellResult(new List <string> { spell.SuccessMessage }); // If target's wall is high enough, the spell fails outright if (target.HasBuff(Buff.Wall) && level <= target.GetBuffStacks(Buff.Wall)) { return(failedResult); } // Reduce accuracy and power if multi-targetting int adjustedAccuracy = spell.Accuracy; if (multiTarget) { adjustedAccuracy = adjustedAccuracy / 2; } int adjustedPower = spell.Power; if (multiTarget) { adjustedPower = adjustedPower / 4; } // Check for absorption. No effect except HP gain. All spells calculate damage. if (target.IsAbsorbentTo(spell.Element)) { int totalHeal = GetDamage(adjustedAccuracy, level); target.HealHP(totalHeal); return(new SpellResult(new List <string> { "HP up!" })); } //// Notes of below // Resistances: Success fail. Damage spells achieve minimum successes (level) and halve damage // Weaknesses: Success is automatic. Damage spells achieve perfect successes (lvl*2) and double damage // If resisted and weak, successes fail, damage is not halved or doubled // Strictly positive effects not subject to Magic Resistance. (e.g. CURE, BLINK, etc.) switch (spell.Effect) { case "Damage": case "Damage_2": case "Damage_3": if (target.IsResistantTo(spell.Element)) { return(HandleDamageSpellResult(target, GetDamage(adjustedPower, level) / 2)); } if (target.IsWeakTo(spell.Element)) { return(HandleDamageSpellResult(target, GetDamage(adjustedPower, level * 2) * 2)); } if (target.IsWeakTo(spell.Element) && target.IsResistantTo(spell.Element)) { // Damage as usual. Best guess at hits: level - blocks int rwHits = level - target.RollMagicBlocks(); return(HandleDamageSpellResult(target, GetDamage(adjustedPower, rwHits))); } // Normal logic int hits = level + GetSpellSuccesses(level, adjustedAccuracy) - target.RollMagicBlocks(); return(HandleDamageSpellResult(target, GetDamage(adjustedPower, hits))); case "Damage_Ultima": // TODO: Ultima damage bug break; case "Heal": // This is CURE, not HEAL. Damage undead, heal otherwise if (target.Families.Contains(MonsterFamily.Undead)) { int healHits = GetHitsAgainstTarget(level, adjustedAccuracy, target); return(HandleDamageSpellResult(target, GetDamage(adjustedPower, healHits))); } int totalHeal = GetDamage(adjustedPower, GetSpellSuccesses(level, adjustedAccuracy)); target.HealHP(totalHeal); return(new SpellResult(new List <string> { "HP up!" })); case "Revive": // Chance to kill undead, otherwise fail. Fails if multi-targeting. if (multiTarget) { return(failedResult); } if (target.Families.Contains(MonsterFamily.Undead)) { if (GetHitsAgainstTarget(level, adjustedAccuracy, target) > 0) { target.Kill(); return(new SpellResult(new List <string> { target.Name + " fell", "Collapsed" })); } } break; case "Buff": // Autohits, ignores magic resistance rolls Buff buff = (Buff)Enum.Parse(typeof(Buff), spell.Status); int buffHits = GetSpellSuccesses(level, adjustedAccuracy); if (buffHits == 0) { return(failedResult); } if (spell.Name == "SAFE") { // NES bug. SAFE only works on the caster if (!Globals.BUG_FIXES) { if (target == caster) { target.AddBuff(buff, buffHits); } else { return(failedResult); } } } target.AddBuff(buff, buffHits); // AURA and BARR have their unique results based on # of successes. Messages are displayed highest to lowest. switch (spell.Name) { case "AURA": string[] auraMessages = { "White", "Yellow", "Green", "Black", "Blue", "Orange", "Red" }; List <string> auraMsgList = new List <string>(); buffHits--; for (int i = buffHits > 6 ? 6 : buffHits; i >= 0; i--) { auraMsgList.Add(auraMessages[i] + " Aura"); } return(new SpellResult(auraMsgList)); case "BARR": string[] barrMessages = { "Ice", "Critical Hit!", "Poison", "Death", "Bolt", "Soul", "Fire" }; List <string> barrMsgList = new List <string>(); buffHits--; for (int i = buffHits > 6 ? 6 : buffHits; i >= 0; i--) { barrMsgList.Add(barrMessages[i] + " Df"); } return(new SpellResult(barrMsgList)); default: return(statusSuccessResult); } case "Debuff": if (target.IsResistantTo(spell.Element)) { return(failedResult); } if (target.IsWeakTo(spell.Element)) { Debuff debuff = (Debuff)Enum.Parse(typeof(Debuff), spell.Status); target.AddDebuff(debuff, level); // TODO: DSPL has unique results messages based on # of successes return(statusSuccessResult); } // Normal logic int debuffHits = GetHitsAgainstTarget(level, adjustedAccuracy, target); if (debuffHits > 0) { Debuff debuff = (Debuff)Enum.Parse(typeof(Debuff), spell.Status); target.AddDebuff(debuff, debuffHits); return(statusSuccessResult); } break; case "TempStatus": if (target.IsResistantTo(spell.Element)) { return(failedResult); } if (target.IsWeakTo(spell.Element)) { TempStatus tempStatus = (TempStatus)Enum.Parse(typeof(TempStatus), spell.Status); target.AddTempStatus(tempStatus); return(statusSuccessResult); } // Normal logic. A single hit = success if (GetHitsAgainstTarget(level, adjustedAccuracy, target) > 0) { TempStatus tempStatus = (TempStatus)Enum.Parse(typeof(TempStatus), spell.Status); target.AddTempStatus(tempStatus); return(statusSuccessResult); } break; case "PermStatus": if (target.IsResistantTo(spell.Element)) { return(failedResult); } if (target.IsWeakTo(spell.Element)) { PermStatus permStatus = (PermStatus)Enum.Parse(typeof(PermStatus), spell.Status); target.AddPermStatus(permStatus); return(statusSuccessResult); } // Normal logic. A single hit = success if (GetHitsAgainstTarget(level, adjustedAccuracy, target) > 0) { PermStatus permStatus = (PermStatus)Enum.Parse(typeof(PermStatus), spell.Status); target.AddPermStatus(permStatus); return(statusSuccessResult); } break; case "CureTempStatus": // This is PEEP // 1 = Venom & Sleep, then one more per level, up to 5 TempStatus[] tempCureOrder = { TempStatus.Venom, TempStatus.Sleep, TempStatus.Mini, TempStatus.Mute, TempStatus.Paralysis, TempStatus.Confuse }; String[] peepMsgOrder = { "Devenomed", "Scared", "Grew", "Can speak", "Can move", "Normal" }; List <string> peepMsgs = new List <string>(); if (target.RemoveTempStatus(TempStatus.Venom)) { peepMsgs.Add("Devenomed"); } for (int i = 1; i < level; i++) { if (i >= tempCureOrder.Length) { break; } if (target.RemoveTempStatus(tempCureOrder[i])) { peepMsgs.Add(peepMsgOrder[i]); } } // TODO: If nothing is cured, is ineffective returned? return(new SpellResult(peepMsgs)); case "CurePermStatus": // This is HEAL. Cure everything up to and including level. PermStatus[] permCureOrder = { PermStatus.Darkness, PermStatus.Poison, PermStatus.Curse, PermStatus.Amnesia, PermStatus.Toad, PermStatus.Stone, PermStatus.KO }; String[] healMsgOrder = { "Can see", "Poison left", "Uncursed", "Remembers", "Regained form", "Normal body", "" }; List <string> healMsgs = new List <string>(); for (int i = 0; i < level; i++) { if (i >= permCureOrder.Length) { break; } if (target.RemovePermStatus(permCureOrder[i])) { healMsgs.Add(healMsgOrder[i]); } } // TODO: If nothing is cured, is ineffective returned? return(new SpellResult(healMsgs)); case "Dispel": // break; case "Special": // Spells that have unique effects: DRAN, ASPL, CHNG, ANTI, Blast switch (spell.Name.ToUpper()) { case "DRAN": // Drain HP // Each hit target loses 1/16 (round down) of Max HP. Caster gains that amount int hpDrainAmt = target.HPMax / 16; // NOTE: Int division int dranHits = GetHitsAgainstTarget(level, adjustedAccuracy, target); if (dranHits > 0) { int dranAmt = hpDrainAmt * dranHits; // Opposite effect vs. Undead if (target.Families.Contains(MonsterFamily.Undead)) { caster.DamageHP(dranAmt); // TODO: This is awkward here if (caster.IsDead()) { MonoMonster mm = (MonoMonster)caster; mm.IsFading = true; } target.HealHP(dranAmt); } else { caster.HealHP(dranAmt); target.DamageHP(dranAmt); } return(statusSuccessResult); } break; case "ASPL": // Drain MP // Each hit target loses 1/16 (round down) of Max MP. Caster gains that amount int mpDrainAmt = target.MPMax / 16; // NOTE: Int division int asplHits = GetHitsAgainstTarget(level, adjustedAccuracy, target); if (asplHits > 0) { int asplAmt = mpDrainAmt * asplHits; // Opposite effect vs. Undead if (target.Families.Contains(MonsterFamily.Undead)) { caster.DamageMP(asplAmt); target.HealMP(asplAmt); } else { caster.HealMP(asplAmt); target.DamageMP(asplAmt); } return(statusSuccessResult); } break; case "ANTI": // Halve MP if (target.IsResistantTo(spell.Element)) { return(failedResult); } if (GetHitsAgainstTarget(level, adjustedAccuracy, target) > 0) { // Target loses half of MP if (target.MP > 0) { target.MP = target.MP / 2; } // NES_BUG: This only effects the first byte of the MP value return(statusSuccessResult); } break; case "CHNG": // Caster and Target swap HP and MP if (target.IsResistantTo(spell.Element)) { return(failedResult); } if (GetHitsAgainstTarget(level, adjustedAccuracy, target) > 0) { int casterHP = caster.HP; int casterMP = caster.MP; caster.HP = target.HP; caster.MP = target.MP; target.HP = casterHP; target.MP = casterMP; return(statusSuccessResult); } break; case "BLAST": // Bomb's Explosion Spell // Fails if HP is full, otherwise kills the caster // Deals damage like a physical attack if (caster.HP == caster.HPMax) { return(failedResult); } int blastSum = 0; for (int i = 0; i < level; i++) { int blastRoll = Globals.rnd.Next(20, 41) - target.Defense; blastSum += blastRoll > 0 ? blastRoll : 0; } caster.Kill(); // Is this legit? MonoMonster m = (MonoMonster)caster; m.IsFading = true; return(HandleDamageSpellResult(target, blastSum)); } break; case "ItemFullRestore": // Elixir target.HealHP(target.HPMax); target.HealMP(target.MPMax); return(statusSuccessResult); // The below are currently unused case "ItemHeal": case "ItemCure": case "ItemRevive": break; default: throw new Exception("Invalid spell effect found: " + spell.Effect); } return(failedResult); }