private void CheckForLearnMoves() { if (_learningMoves.Count != 0) { int index = _pkmn.Moveset.GetFirstEmptySlot(); if (index == -1) { SetWantsToLearnMove(); } else { Moveset.MovesetSlot slot = _pkmn.Moveset[index]; PBEMove move = _learningMoves.Dequeue(); // Remove from queue string moveStr = PBELocalizedString.GetMoveName(move).English; slot.Move = move; PBEMoveData mData = PBEMoveData.Data[move]; slot.PP = PBEDataUtils.CalcMaxPP(mData.PPTier, 0, PkmnConstants.PBESettings); slot.PPUps = 0; CreateMessage(string.Format("{0} learned {1}!", _pkmn.Nickname, moveStr)); _state = State.LearnMove_ForgotMsg; } } else { SetFadeOut(); } }
private void DetermineTurnOrder() { _turnOrder.Clear(); IEnumerable <PBEPokemon> pkmnSwitchingOut = ActiveBattlers.Where(p => p.TurnAction.Decision == PBETurnDecision.SwitchOut); IEnumerable <PBEPokemon> pkmnFighting = ActiveBattlers.Where(p => p.TurnAction.Decision == PBETurnDecision.Fight); // Switching happens first: _turnOrder.AddRange(GetActingOrder(pkmnSwitchingOut, true)); // Moves: sbyte GetPrio(PBEPokemon p) { PBEMoveData mData = PBEMoveData.Data[p.TurnAction.FightMove]; return((sbyte)PBEUtils.Clamp(mData.Priority + (p.Ability == PBEAbility.Prankster && mData.Category == PBEMoveCategory.Status ? 1 : 0), sbyte.MinValue, sbyte.MaxValue)); } foreach (sbyte priority in pkmnFighting.Select(p => GetPrio(p)).Distinct().OrderByDescending(p => p)) { PBEPokemon[] pkmnWithThisPriority = pkmnFighting.Where(p => GetPrio(p) == priority).ToArray(); if (pkmnWithThisPriority.Length > 0) { Debug.WriteLine("Priority {0} bracket...", priority); _turnOrder.AddRange(GetActingOrder(pkmnWithThisPriority, false)); } } }
private int CalculateDamage(PBEBattlePokemon user, PBEBattlePokemon target, PBEMoveData mData, PBEType moveType, double basePower, bool criticalHit) { PBEBattlePokemon aPkmn; PBEMoveCategory aCat = mData.Category, dCat; switch (mData.Effect) { case PBEMoveEffect.FoulPlay: { aPkmn = target; dCat = aCat; break; } case PBEMoveEffect.Psyshock: { aPkmn = user; dCat = PBEMoveCategory.Physical; break; } default: { aPkmn = user; dCat = aCat; break; } } bool ignoreA = user != target && target.Ability == PBEAbility.Unaware && !user.HasCancellingAbility(); bool ignoreD = user != target && (mData.Effect == PBEMoveEffect.ChipAway || user.Ability == PBEAbility.Unaware); double a, d; if (aCat == PBEMoveCategory.Physical) { double m = ignoreA ? 1 : GetStatChangeModifier(criticalHit ? Math.Max((sbyte)0, aPkmn.AttackChange) : aPkmn.AttackChange, false); a = CalculateAttack(user, target, moveType, aPkmn.Attack * m); } else { double m = ignoreA ? 1 : GetStatChangeModifier(criticalHit ? Math.Max((sbyte)0, aPkmn.SpAttackChange) : aPkmn.SpAttackChange, false); a = CalculateSpAttack(user, target, moveType, aPkmn.SpAttack * m); } if (dCat == PBEMoveCategory.Physical) { double m = ignoreD ? 1 : GetStatChangeModifier(criticalHit ? Math.Min((sbyte)0, target.DefenseChange) : target.DefenseChange, false); d = CalculateDefense(user, target, target.Defense * m); } else { double m = ignoreD ? 1 : GetStatChangeModifier(criticalHit ? Math.Min((sbyte)0, target.SpDefenseChange) : target.SpDefenseChange, false); d = CalculateSpDefense(user, target, target.SpDefense * m); } return(CalculateDamage(user, a, d, basePower)); }
public async Task Info([Remainder] string moveName) { PBEMove?nMove = PBELocalizedString.GetMoveByName(moveName); if (!nMove.HasValue || nMove.Value == PBEMove.None) { await Context.Channel.SendMessageAsync($"{Context.User.Mention} ― Invalid move!"); } else { PBEMove move = nMove.Value; moveName = PBELocalizedString.GetMoveName(move).English; PBEMoveData mData = PBEMoveData.Data[move]; EmbedBuilder embed = new EmbedBuilder() .WithAuthor(Context.User) .WithColor(Utils.TypeColors[mData.Type]) .WithTitle(moveName) .WithUrl(Utils.URL) .WithDescription(PBELocalizedString.GetMoveDescription(move).English.Replace('\n', ' ')) .AddField("Type", Utils.TypeEmotes[mData.Type], true) .AddField("Category", mData.Category, true) .AddField("Priority", mData.Priority, true) .AddField("PP", Math.Max(1, mData.PPTier * PBESettings.DefaultPPMultiplier), true) .AddField("Power", mData.Power == 0 ? "―" : mData.Power.ToString(), true) .AddField("Accuracy", mData.Accuracy == 0 ? "―" : mData.Accuracy.ToString(), true) .AddField("Targets", mData.Targets, true) .AddField("Flags", mData.Flags, true); switch (mData.Effect) { case PBEMoveEffect.Recoil: embed.AddField("Recoil", $"1/{mData.EffectParam} damage dealt"); break; case PBEMoveEffect.Recoil__10PercentBurn: embed.AddField("Recoil", $"1/{mData.EffectParam} damage dealt"); break; // TODO: Burn chance case PBEMoveEffect.Recoil__10PercentParalyze: embed.AddField("Recoil", $"1/{mData.EffectParam} damage dealt"); break; // TODO: Paralyze chance case PBEMoveEffect.Struggle: embed.AddField("Recoil", "1/4 user's max HP"); break; case PBEMoveEffect.TODOMOVE: embed.AddField("**ATTENTION**", $"{moveName} is not yet implemented in Pokémon Battle Engine"); break; } await Context.Channel.SendMessageAsync(string.Empty, embed : embed.Build()); } }
public async Task Info([Remainder] string moveName) { PBEMove move = 0; PBELocalizedString localized = PBEMoveLocalization.Names.Values.FirstOrDefault(l => l.Contains(moveName)); if (localized != null) { move = PBEMoveLocalization.Names.First(p => p.Value == localized).Key; } else { Enum.TryParse(moveName, true, out move); } if (move != 0) { if (move == PBEMove.None) { goto invalid; } PBEMoveData mData = PBEMoveData.Data[move]; var embed = new EmbedBuilder() .WithColor(Utils.TypeToColor[mData.Type]) .WithUrl("https://github.com/Kermalis/PokemonBattleEngine") .WithTitle(PBEMoveLocalization.Names[move].English) .WithAuthor(Context.User) .AddField("Type", mData.Type, true) .AddField("Category", mData.Category, true) .AddField("Priority", mData.Priority, true) .AddField("PP", mData.PPTier * PBESettings.DefaultSettings.PPMultiplier, true) .AddField("Power", mData.Power == 0 ? "--" : mData.Power.ToString(), true) .AddField("Accuracy", mData.Accuracy == 0 ? "--" : mData.Accuracy.ToString(), true) .AddField("Effect", mData.Effect, true) .AddField("Effect Param", mData.EffectParam, true) .AddField("Targets", mData.Targets, true) .AddField("Flags", mData.Flags, true); await Context.Channel.SendMessageAsync(string.Empty, embed : embed.Build()); return; } invalid: await Context.Channel.SendMessageAsync($"{Context.User.Mention} Invalid move!"); }
public async Task Info([Remainder] string moveName) { PBEMove?nMove = PBELocalizedString.GetMoveByName(moveName); if (!nMove.HasValue || nMove.Value == PBEMove.None) { await Context.Channel.SendMessageAsync($"{Context.User.Mention} Invalid move!"); } else { PBEMove move = nMove.Value; PBEMoveData mData = PBEMoveData.Data[move]; EmbedBuilder embed = new EmbedBuilder() .WithAuthor(Context.User) .WithColor(Utils.TypeToColor[mData.Type]) .WithTitle(PBELocalizedString.GetMoveName(move).English) .WithUrl(Utils.URL) .WithDescription(PBELocalizedString.GetMoveDescription(move).English.Replace('\n', ' ')) .AddField("Type", mData.Type, true) .AddField("Category", mData.Category, true) .AddField("Priority", mData.Priority, true) .AddField("PP", Math.Max(1, mData.PPTier * PBESettings.DefaultSettings.PPMultiplier), true) .AddField("Power", mData.Power == 0 ? "--" : mData.Power.ToString(), true) .AddField("Accuracy", mData.Accuracy == 0 ? "--" : mData.Accuracy.ToString(), true); switch (mData.Effect) { case PBEMoveEffect.FlareBlitz: embed.AddField("Recoil", "1/3 damage dealt", true); break; // TODO: Burn chance case PBEMoveEffect.Recoil: embed.AddField("Recoil", $"1/{mData.EffectParam} damage dealt", true); break; case PBEMoveEffect.Struggle: embed.AddField("Recoil", "1/4 user's max HP", true); break; case PBEMoveEffect.VoltTackle: embed.AddField("Recoil", "1/3 damage dealt", true); break; // TODO: Paralyze chance } embed.AddField("Targets", mData.Targets, true) .AddField("Flags", mData.Flags, true); await Context.Channel.SendMessageAsync(string.Empty, embed : embed.Build()); } }
public static IReadOnlyCollection <PBEMove> GetLegalMoves(PBESpecies species, PBEForm form, byte level, PBESettings settings) { if (settings == null) { throw new ArgumentNullException(nameof(settings)); } if (!settings.IsReadOnly) { throw new ArgumentException("Settings must be read-only.", nameof(settings)); } ValidateSpecies(species, form, true); ValidateLevel(level, settings); List <(PBESpecies, PBEForm)> speciesToStealFrom = GetSpecies(species, form); var moves = new List <PBEMove>(); foreach ((PBESpecies spe, PBEForm fo) in speciesToStealFrom) { var pData = PBEPokemonData.GetData(spe, fo); // Disallow moves learned after the current level moves.AddRange(pData.LevelUpMoves.Where(t => t.Level <= level).Select(t => t.Move)); // Disallow form-specific moves from other forms (Rotom) moves.AddRange(pData.OtherMoves.Where(t => (spe == species && fo == form) || t.ObtainMethod != PBEMoveObtainMethod.Form).Select(t => t.Move)); // Event Pokémon checking is extremely basic and wrong, but the goal is not to be super restricting or accurate if (PBEEventPokemon.Events.TryGetValue(spe, out ReadOnlyCollection <PBEEventPokemon> events)) { // Disallow moves learned after the current level moves.AddRange(events.Where(e => e.Level <= level).SelectMany(e => e.Moves).Where(m => m != PBEMove.None)); } if (moves.Any(m => PBEMoveData.Data[m].Effect == PBEMoveEffect.Sketch)) { return(PBEDataUtils.SketchLegalMoves); } } return(moves.Distinct().Where(m => PBEMoveData.IsMoveUsable(m)).ToArray()); }
private unsafe void DrawMovesPage(uint *bmpAddress, int bmpWidth, int bmpHeight) { const float winX = 0.08f; const float winY = 0.15f; const float winW = 0.75f - winX; const float winH = 0.9f - winY; const float moveColX = winX + 0.03f; const float moveTextX = moveColX + 0.02f; const float moveColW = 0.69f - winX; const float itemSpacingY = winH / (PkmnConstants.NumMoves + 0.75f); const float moveX = 0.21f; const float moveY = 0.03f; const float ppX = 0.12f; const float ppNumX = 0.35f; const float ppY = itemSpacingY / 2; const float cancelY = winY + moveY + (PkmnConstants.NumMoves * itemSpacingY); RenderUtils.FillRoundedRectangle(bmpAddress, bmpWidth, bmpHeight, winX, winY, winX + winW, winY + winH, 15, RenderUtils.Color(250, 128, 120, 255)); Font moveFont = Font.Default; uint[] moveColors = Font.DefaultWhite_DarkerOutline_I; uint[] ppColors = Font.DefaultBlack_I; void Place(int i, PBEMove move, int pp, int maxPP) { PBEMoveData mData = PBEMoveData.Data[move]; float x = moveTextX; float y = winY + moveY + (i * itemSpacingY); string str = PBELocalizedString.GetTypeName(mData.Type).English; moveFont.DrawString(bmpAddress, bmpWidth, bmpHeight, x, y, str, moveColors); x += moveX; str = PBELocalizedString.GetMoveName(move).English; moveFont.DrawString(bmpAddress, bmpWidth, bmpHeight, x, y, str, moveColors); x = moveTextX + ppX; y += ppY; str = "PP"; moveFont.DrawString(bmpAddress, bmpWidth, bmpHeight, x, y, str, ppColors); x = moveTextX + ppNumX; str = string.Format("{0}/{1}", pp, maxPP); moveFont.MeasureString(str, out int strW, out _); moveFont.DrawString(bmpAddress, bmpWidth, bmpHeight, RenderUtils.GetCoordinatesForCentering(bmpWidth, strW, x), (int)(bmpHeight * y), str, ppColors); DrawSelection(i); } void DrawSelection(int i) { if (_selectingMove != i) { return; } float x = moveColX; float y = winY + moveY + (i * itemSpacingY); float w = moveColW; float h = i == PkmnConstants.NumMoves ? itemSpacingY / 2 : itemSpacingY; RenderUtils.DrawRoundedRectangle(bmpAddress, bmpWidth, bmpHeight, x, y, x + w, y + h, 5, RenderUtils.Color(48, 180, 255, 200)); } // Moves if (_pPkmn is not null) { Moveset moves = _pPkmn.Moveset; for (int m = 0; m < PkmnConstants.NumMoves; m++) { Moveset.MovesetSlot slot = moves[m]; PBEMove move = slot.Move; if (move == PBEMove.None) { continue; } int pp = slot.PP; int maxPP = PBEDataUtils.CalcMaxPP(move, slot.PPUps, PkmnConstants.PBESettings); Place(m, move, pp, maxPP); } } else if (_pcPkmn is not null) { BoxMoveset moves = _pcPkmn.Moveset; for (int m = 0; m < PkmnConstants.NumMoves; m++) { BoxMoveset.BoxMovesetSlot slot = moves[m]; PBEMove move = slot.Move; if (move == PBEMove.None) { continue; } int maxPP = PBEDataUtils.CalcMaxPP(move, slot.PPUps, PkmnConstants.PBESettings); Place(m, move, maxPP, maxPP); } } else { PBEBattlePokemon bPkmn = _bPkmn.Pkmn; PBEBattleMoveset moves = bPkmn.Status2.HasFlag(PBEStatus2.Transformed) ? bPkmn.TransformBackupMoves : bPkmn.Moves; for (int m = 0; m < PkmnConstants.NumMoves; m++) { PBEBattleMoveset.PBEBattleMovesetSlot slot = moves[m]; PBEMove move = slot.Move; if (move == PBEMove.None) { continue; } int pp = slot.PP; int maxPP = slot.MaxPP; Place(m, move, pp, maxPP); } } // Cancel or new move if (_learningMove != PBEMove.None) { uint[] learnColors = Font.DefaultBlue_I; PBEMoveData mData = PBEMoveData.Data[_learningMove]; float x = moveTextX; string str = PBELocalizedString.GetTypeName(mData.Type).English; moveFont.DrawString(bmpAddress, bmpWidth, bmpHeight, x, cancelY, str, learnColors); x += moveX; str = PBELocalizedString.GetMoveName(_learningMove).English; moveFont.DrawString(bmpAddress, bmpWidth, bmpHeight, x, cancelY, str, learnColors); DrawSelection(PkmnConstants.NumMoves); } else { if (_selectingMove != -1) { string str = "Cancel"; moveFont.DrawString(bmpAddress, bmpWidth, bmpHeight, moveTextX, cancelY, str, moveColors); DrawSelection(PkmnConstants.NumMoves); } } }
private double CalculateDamageMultiplier(PBEBattlePokemon user, PBEBattlePokemon target, PBEMoveData mData, PBEType moveType, PBEResult moveResult, bool criticalHit) { double damageMultiplier = 1; if (target.Status2.HasFlag(PBEStatus2.Airborne) && mData.Flags.HasFlag(PBEMoveFlag.DoubleDamageAirborne)) { damageMultiplier *= 2.0; } if (target.Minimize_Used && mData.Flags.HasFlag(PBEMoveFlag.DoubleDamageMinimized)) { damageMultiplier *= 2.0; } if (target.Status2.HasFlag(PBEStatus2.Underground) && mData.Flags.HasFlag(PBEMoveFlag.DoubleDamageUnderground)) { damageMultiplier *= 2.0; } if (target.Status2.HasFlag(PBEStatus2.Underwater) && mData.Flags.HasFlag(PBEMoveFlag.DoubleDamageUnderwater)) { damageMultiplier *= 2.0; } if (criticalHit) { damageMultiplier *= Settings.CritMultiplier; if (user.Ability == PBEAbility.Sniper) { damageMultiplier *= 1.5; } } else if (user.Ability != PBEAbility.Infiltrator) { if ((target.Team.TeamStatus.HasFlag(PBETeamStatus.Reflect) && mData.Category == PBEMoveCategory.Physical) || (target.Team.TeamStatus.HasFlag(PBETeamStatus.LightScreen) && mData.Category == PBEMoveCategory.Special)) { if (target.Team.NumPkmnOnField == 1) { damageMultiplier *= 0.5; } else { damageMultiplier *= 0.66; } } } switch (moveResult) { case PBEResult.NotVeryEffective_Type: { if (user.Ability == PBEAbility.TintedLens) { damageMultiplier *= 2.0; } break; } case PBEResult.SuperEffective_Type: { if ((target.Ability == PBEAbility.Filter || target.Ability == PBEAbility.SolidRock) && !user.HasCancellingAbility()) { damageMultiplier *= 0.75; } if (user.Item == PBEItem.ExpertBelt) { damageMultiplier *= 1.2; } break; } } if (user.ReceivesSTAB(moveType)) { if (user.Ability == PBEAbility.Adaptability) { damageMultiplier *= 2.0; } else { damageMultiplier *= 1.5; } } if (mData.Category == PBEMoveCategory.Physical && user.Status1 == PBEStatus1.Burned && user.Ability != PBEAbility.Guts) { damageMultiplier *= 0.5; } if (moveType == PBEType.Fire && target.Ability == PBEAbility.Heatproof && !user.HasCancellingAbility()) { damageMultiplier *= 0.5; } return(damageMultiplier); }
// None of these moves are multi-target private void FixedDamageHit(PBEBattlePokemon user, PBEBattlePokemon[] targets, PBEMoveData mData, Func <PBEBattlePokemon, int> damageFunc, Func <PBEBattlePokemon, PBEResult> failFunc = null, Action beforePostHit = null) { PBEType moveType = user.GetMoveType(mData); // Endeavor fails if the target's HP is <= the user's HP // One hit knockout moves fail if the target's level is > the user's level Hit_GetVictims(user, targets, mData, moveType, out List <PBEAttackVictim> victims, failFunc: failFunc); if (victims.Count == 0) { return; } // BUG: Gems activate for these moves despite base power not being involved if (!Settings.BugFix) { double unused = CalculateBasePower(user, targets, mData, moveType); } void CalcDamage(PBEAttackVictim victim) { PBEBattlePokemon target = victim.Pkmn; // FinalGambit user faints here victim.Damage = DealDamage(user, target, damageFunc.Invoke(target)); } void DoSub(IEnumerable <PBEAttackVictim> subs) { foreach (PBEAttackVictim victim in subs) { CalcDamage(victim); PBEBattlePokemon target = victim.Pkmn; if (target.SubstituteHP == 0) { BroadcastStatus2(target, user, PBEStatus2.Substitute, PBEStatusAction.Ended); } } } void DoNormal(IEnumerable <PBEAttackVictim> normals) { foreach (PBEAttackVictim victim in normals) { CalcDamage(victim); } foreach (PBEAttackVictim victim in normals) { PBEBattlePokemon target = victim.Pkmn; // "It's a one-hit KO!" beforePostHit?.Invoke(); DoPostHitEffects(user, target, mData, moveType); } Hit_FaintCheck(normals); } Hit_HitTargets(user.Team, DoSub, DoNormal, victims, out IEnumerable <PBEAttackVictim> allies, out IEnumerable <PBEAttackVictim> foes); DoPostAttackedEffects(user, allies, foes, false, colorChangeType: moveType); }
private double CalculateBasePower(PBEPokemon user, PBEPokemon[] targets, PBEMove move, PBEType moveType) { PBEMoveData mData = PBEMoveData.Data[move]; // Get move's base power double basePower; switch (move) { case PBEMove.CrushGrip: case PBEMove.WringOut: { PBEPokemon target = targets[0]; basePower = Math.Max(1, 120 * target.HP / target.MaxHP); break; } case PBEMove.Eruption: case PBEMove.WaterSpout: { basePower = Math.Max(1, mData.Power * user.HP / user.MaxHP); break; } case PBEMove.Flail: case PBEMove.Reversal: { int val = 48 * user.HP / user.MaxHP; if (val < 2) { basePower = 200; } else if (val < 4) { basePower = 150; } else if (val < 8) { basePower = 100; } else if (val < 16) { basePower = 80; } else if (val < 32) { basePower = 40; } else { basePower = 20; } break; } case PBEMove.Frustration: { basePower = Math.Max(1, (byte.MaxValue - user.Friendship) / 2.5); break; } case PBEMove.GrassKnot: case PBEMove.LowKick: { PBEPokemon target = targets[0]; if (target.Weight >= 200.0) { basePower = 120; } else if (target.Weight >= 100.0) { basePower = 100; } else if (target.Weight >= 50.0) { basePower = 80; } else if (target.Weight >= 25.0) { basePower = 60; } else if (target.Weight >= 10.0) { basePower = 40; } else { basePower = 20; } break; } case PBEMove.HeatCrash: case PBEMove.HeavySlam: { PBEPokemon target = targets[0]; double relative = user.Weight / target.Weight; if (relative < 2) { basePower = 40; } else if (relative < 3) { basePower = 60; } else if (relative < 4) { basePower = 80; } else if (relative < 5) { basePower = 100; } else { basePower = 120; } break; } case PBEMove.HiddenPower: { basePower = user.IndividualValues.HiddenPowerBasePower; break; } case PBEMove.Magnitude: { int val = PBEUtils.RandomInt(0, 99); byte magnitude; if (val < 5) // Magnitude 4 - 5% { magnitude = 4; basePower = 10; } else if (val < 15) // Magnitude 5 - 10% { magnitude = 5; basePower = 30; } else if (val < 35) // Magnitude 6 - 20% { magnitude = 6; basePower = 50; } else if (val < 65) // Magnitude 7 - 30% { magnitude = 7; basePower = 70; } else if (val < 85) // Magnitude 8 - 20% { magnitude = 8; basePower = 90; } else if (val < 95) // Magnitude 9 - 10% { magnitude = 9; basePower = 110; } else // Magnitude 10 - 5% { magnitude = 10; basePower = 150; } BroadcastMagnitude(magnitude); break; } case PBEMove.Punishment: { PBEPokemon target = targets[0]; basePower = Math.Max(1, Math.Min(200, 60 + (20 * target.GetPositiveStatTotal()))); break; } case PBEMove.Return: { basePower = Math.Max(1, user.Friendship / 2.5); break; } case PBEMove.StoredPower: { basePower = mData.Power + (20 * user.GetPositiveStatTotal()); break; } default: { basePower = mData.Power; break; } } // Ability/Item-specific power boosts bool canUseGems = !mData.Flags.HasFlag(PBEMoveFlag.UnaffectedByGems); switch (moveType) { case PBEType.Bug: { if (user.Ability == PBEAbility.Swarm && user.HP <= user.MaxHP / 3) { basePower *= 1.5; } switch (user.Item) { case PBEItem.InsectPlate: case PBEItem.SilverPowder: { basePower *= 1.2; break; } case PBEItem.BugGem: { if (canUseGems) { BroadcastItem(user, user, PBEItem.BugGem, PBEItemAction.Consumed); basePower *= 1.5; } break; } } break; } case PBEType.Dark: { switch (user.Item) { case PBEItem.BlackGlasses: case PBEItem.DreadPlate: { basePower *= 1.2; break; } case PBEItem.DarkGem: { if (canUseGems) { BroadcastItem(user, user, PBEItem.DarkGem, PBEItemAction.Consumed); basePower *= 1.5; } break; } } break; } case PBEType.Dragon: { switch (user.Item) { case PBEItem.AdamantOrb: { if (user.OriginalSpecies == PBESpecies.Dialga) { basePower *= 1.2; } break; } case PBEItem.DracoPlate: case PBEItem.DragonFang: { basePower *= 1.2; break; } case PBEItem.GriseousOrb: { if (user.OriginalSpecies == PBESpecies.Giratina_Origin) { basePower *= 1.2; } break; } case PBEItem.LustrousOrb: { if (user.OriginalSpecies == PBESpecies.Palkia) { basePower *= 1.2; } break; } case PBEItem.DragonGem: { if (canUseGems) { BroadcastItem(user, user, PBEItem.DragonGem, PBEItemAction.Consumed); basePower *= 1.5; } break; } } break; } case PBEType.Electric: { switch (user.Item) { case PBEItem.Magnet: case PBEItem.ZapPlate: { basePower *= 1.2; break; } case PBEItem.ElectricGem: { if (canUseGems) { BroadcastItem(user, user, PBEItem.ElectricGem, PBEItemAction.Consumed); basePower *= 1.5; } break; } } break; } case PBEType.Fighting: { switch (user.Item) { case PBEItem.BlackBelt: case PBEItem.FistPlate: { basePower *= 1.2; break; } case PBEItem.FightingGem: { if (canUseGems) { BroadcastItem(user, user, PBEItem.FightingGem, PBEItemAction.Consumed); basePower *= 1.5; } break; } } break; } case PBEType.Fire: { switch (user.Item) { case PBEItem.Charcoal: case PBEItem.FlamePlate: { basePower *= 1.2; break; } case PBEItem.FireGem: { if (canUseGems) { BroadcastItem(user, user, PBEItem.FireGem, PBEItemAction.Consumed); basePower *= 1.5; } break; } } break; } case PBEType.Flying: { switch (user.Item) { case PBEItem.SharpBeak: case PBEItem.SkyPlate: { basePower *= 1.2; break; } case PBEItem.FlyingGem: { if (canUseGems) { BroadcastItem(user, user, PBEItem.FlyingGem, PBEItemAction.Consumed); basePower *= 1.5; } break; } } break; } case PBEType.Ghost: { switch (user.Item) { case PBEItem.GriseousOrb: { if (user.OriginalSpecies == PBESpecies.Giratina_Origin) { basePower *= 1.2; } break; } case PBEItem.SpellTag: case PBEItem.SpookyPlate: { basePower *= 1.2; break; } case PBEItem.GhostGem: { if (canUseGems) { BroadcastItem(user, user, PBEItem.GhostGem, PBEItemAction.Consumed); basePower *= 1.5; } break; } } break; } case PBEType.Grass: { switch (user.Item) { case PBEItem.MeadowPlate: case PBEItem.MiracleSeed: case PBEItem.RoseIncense: { basePower *= 1.2; break; } case PBEItem.GrassGem: { if (canUseGems) { BroadcastItem(user, user, PBEItem.GrassGem, PBEItemAction.Consumed); basePower *= 1.5; } break; } } break; } case PBEType.Ground: { switch (user.Item) { case PBEItem.EarthPlate: case PBEItem.SoftSand: { basePower *= 1.2; break; } case PBEItem.GroundGem: { if (canUseGems) { BroadcastItem(user, user, PBEItem.GroundGem, PBEItemAction.Consumed); basePower *= 1.5; } break; } } break; } case PBEType.Ice: { switch (user.Item) { case PBEItem.IciclePlate: case PBEItem.NeverMeltIce: { basePower *= 1.2; break; } case PBEItem.IceGem: { if (canUseGems) { BroadcastItem(user, user, PBEItem.IceGem, PBEItemAction.Consumed); basePower *= 1.5; } break; } } break; } case PBEType.None: { break; } case PBEType.Normal: { switch (user.Item) { case PBEItem.SilkScarf: { basePower *= 1.2; break; } case PBEItem.NormalGem: { if (canUseGems) { BroadcastItem(user, user, PBEItem.NormalGem, PBEItemAction.Consumed); basePower *= 1.5; } break; } } break; } case PBEType.Poison: { switch (user.Item) { case PBEItem.PoisonBarb: case PBEItem.ToxicPlate: { basePower *= 1.2; break; } case PBEItem.PoisonGem: { if (canUseGems) { BroadcastItem(user, user, PBEItem.PoisonGem, PBEItemAction.Consumed); basePower *= 1.5; } break; } } break; } case PBEType.Psychic: { switch (user.Item) { case PBEItem.MindPlate: case PBEItem.OddIncense: case PBEItem.TwistedSpoon: { basePower *= 1.2; break; } case PBEItem.PsychicGem: { if (canUseGems) { BroadcastItem(user, user, PBEItem.PsychicGem, PBEItemAction.Consumed); basePower *= 1.5; } break; } } break; } case PBEType.Rock: { switch (user.Item) { case PBEItem.HardStone: case PBEItem.RockIncense: case PBEItem.StonePlate: { basePower *= 1.2; break; } case PBEItem.RockGem: { if (canUseGems) { BroadcastItem(user, user, PBEItem.RockGem, PBEItemAction.Consumed); basePower *= 1.5; } break; } } break; } case PBEType.Steel: { switch (user.Item) { case PBEItem.AdamantOrb: { if (user.OriginalSpecies == PBESpecies.Dialga) { basePower *= 1.2; } break; } case PBEItem.IronPlate: case PBEItem.MetalCoat: { basePower *= 1.2; break; } case PBEItem.SteelGem: { if (canUseGems) { BroadcastItem(user, user, PBEItem.SteelGem, PBEItemAction.Consumed); basePower *= 1.5; } break; } } break; } case PBEType.Water: { switch (user.Item) { case PBEItem.LustrousOrb: { if (user.OriginalSpecies == PBESpecies.Palkia) { basePower *= 1.2; } break; } case PBEItem.MysticWater: case PBEItem.SeaIncense: case PBEItem.SplashPlate: case PBEItem.WaveIncense: { basePower *= 1.2; break; } case PBEItem.WaterGem: { if (canUseGems) { BroadcastItem(user, user, PBEItem.WaterGem, PBEItemAction.Consumed); basePower *= 1.5; } break; } } break; } default: throw new ArgumentOutOfRangeException(nameof(moveType)); } // Move-specific power boosts switch (move) { case PBEMove.Acrobatics: { if (user.Item == PBEItem.None) { basePower *= 2.0; } break; } case PBEMove.Brine: { PBEPokemon target = targets[0]; if (target.HP <= target.HP / 2) { basePower *= 2.0; } break; } case PBEMove.Facade: { if (user.Status1 == PBEStatus1.Burned || user.Status1 == PBEStatus1.Paralyzed || user.Status1 == PBEStatus1.Poisoned || user.Status1 == PBEStatus1.BadlyPoisoned) { basePower *= 2.0; } break; } case PBEMove.Hex: { PBEPokemon target = targets[0]; if (target.Status1 != PBEStatus1.None) { basePower *= 2.0; } break; } case PBEMove.Retaliate: { if (user.Team.MonFaintedLastTurn) { basePower *= 2.0; } break; } case PBEMove.Venoshock: { PBEPokemon target = targets[0]; if (target.Status1 == PBEStatus1.Poisoned || target.Status1 == PBEStatus1.BadlyPoisoned) { basePower *= 2.0; } break; } case PBEMove.WeatherBall: { if (ShouldDoWeatherEffects() && Weather != PBEWeather.None) { basePower *= 2.0; } break; } } // Weather-specific power boosts if (ShouldDoWeatherEffects()) { switch (Weather) { case PBEWeather.HarshSunlight: { if (moveType == PBEType.Fire) { basePower *= 1.5; } else if (moveType == PBEType.Water) { basePower *= 0.5; } break; } case PBEWeather.Rain: { if (moveType == PBEType.Water) { basePower *= 1.5; } else if (moveType == PBEType.Fire) { basePower *= 0.5; } break; } case PBEWeather.Sandstorm: { if (user.Ability == PBEAbility.SandForce && (user.HasType(PBEType.Rock) || user.HasType(PBEType.Ground) || user.HasType(PBEType.Steel))) { basePower *= 1.3; } break; } } } // Other power boosts if (user.Status2.HasFlag(PBEStatus2.HelpingHand)) { basePower *= 1.5; } if (user.Ability == PBEAbility.ToxicBoost && mData.Category == PBEMoveCategory.Physical && (user.Status1 == PBEStatus1.Poisoned || user.Status1 == PBEStatus1.BadlyPoisoned)) { basePower *= 1.5; } if (user.Item == PBEItem.LifeOrb) { basePower *= 1.3; } if (user.Ability == PBEAbility.IronFist && mData.Flags.HasFlag(PBEMoveFlag.AffectedByIronFist)) { basePower *= 1.2; } if (user.Ability == PBEAbility.Reckless && mData.Flags.HasFlag(PBEMoveFlag.AffectedByReckless)) { basePower *= 1.2; } if (mData.Category == PBEMoveCategory.Physical && user.Item == PBEItem.MuscleBand) { basePower *= 1.1; } if (mData.Category == PBEMoveCategory.Special && user.Item == PBEItem.WiseGlasses) { basePower *= 1.1; } return(basePower); }
// TODO: TripleKick miss logic private void Hit_GetVictims(PBEBattlePokemon user, PBEBattlePokemon[] targets, PBEMoveData mData, PBEType moveType, out List <PBEAttackVictim> victims, Func <PBEBattlePokemon, PBEResult> failFunc = null) { victims = new List <PBEAttackVictim>(targets.Length); foreach (PBEBattlePokemon target in targets) { if (!AttackTypeCheck(user, target, moveType, out PBEResult result, out double typeEffectiveness)) { continue; } // Verified: These fails are after type effectiveness (So SuckerPunch will not affect Ghost types due to Normalize before it fails due to invalid conditions) if (failFunc != null && failFunc.Invoke(target) != PBEResult.Success) { continue; } victims.Add(new PBEAttackVictim(target, result, typeEffectiveness)); } if (victims.Count == 0) { return; } victims.RemoveAll(t => MissCheck(user, t.Pkmn, mData)); return; }
// None of these moves are multi-target private void MultiHit(PBEBattlePokemon user, PBEBattlePokemon[] targets, PBEMoveData mData, byte numHits, bool subsequentMissChecks = false, Action <PBEBattlePokemon> beforePostHit = null) { PBEType moveType = user.GetMoveType(mData); Hit_GetVictims(user, targets, mData, moveType, out List <PBEAttackVictim> victims); if (victims.Count == 0) { return; } double basePower = CalculateBasePower(user, targets, mData, moveType); // Verified: Gem boost applies to all hits double initDamageMultiplier = victims.Count > 1 ? 0.75 : 1d; void CalcDamage(PBEAttackVictim victim) { PBEBattlePokemon target = victim.Pkmn; PBEResult result = victim.Result; double damageMultiplier = initDamageMultiplier * victim.TypeEffectiveness; bool crit = CritCheck(user, target, mData); damageMultiplier *= CalculateDamageMultiplier(user, target, mData, moveType, result, crit); int damage = (int)(damageMultiplier * CalculateDamage(user, target, mData, moveType, basePower, crit)); victim.Damage = DealDamage(user, target, damage, ignoreSubstitute: false, ignoreSturdy: false); victim.Crit = crit; } void DoSub(IEnumerable <PBEAttackVictim> subs) { foreach (PBEAttackVictim victim in subs) { CalcDamage(victim); PBEBattlePokemon target = victim.Pkmn; if (victim.Crit) { BroadcastMoveCrit(target); } if (target.SubstituteHP == 0) { BroadcastStatus2(target, user, PBEStatus2.Substitute, PBEStatusAction.Ended); } } } void DoNormal(IEnumerable <PBEAttackVictim> normals) { normals = normals.Where(v => v.Pkmn.HP > 0); // Remove ones that fainted from previous hits foreach (PBEAttackVictim victim in normals) { CalcDamage(victim); } Hit_DoCrit(normals); foreach (PBEAttackVictim victim in normals) { PBEBattlePokemon target = victim.Pkmn; // Twineedle has a chance to poison on each strike beforePostHit?.Invoke(target); DoPostHitEffects(user, target, mData, moveType); } } byte hit = 0; IEnumerable <PBEAttackVictim> allies, foes; do { Hit_HitTargets(user.Team, DoSub, DoNormal, victims, out allies, out foes); hit++; } while (hit < numHits && user.HP > 0 && user.Status1 != PBEStatus1.Asleep && victims.Any(v => v.Pkmn.HP > 0)); Hit_DoMoveResult(user, allies); Hit_DoMoveResult(user, foes); BroadcastMultiHit(hit); Hit_FaintCheck(allies); Hit_FaintCheck(foes); DoPostAttackedEffects(user, allies, foes, true, colorChangeType: moveType); }
/// <summary>Creates valid actions for a battle turn for a specific team.</summary> /// <param name="team">The team to create actions for.</param> /// <exception cref="InvalidOperationException">Thrown when <paramref name="team"/> has no active battlers or <paramref name="team"/>'s <see cref="PBETeam.Battle"/>'s <see cref="PBEBattle.BattleState"/> is not <see cref="PBEBattleState.WaitingForActions"/>.</exception> /// <exception cref="ArgumentOutOfRangeException">Thrown when a Pokémon has no moves, the AI tries to use a move with invalid targets, or <paramref name="team"/>'s <see cref="PBETeam.Battle"/>'s <see cref="PBEBattle.BattleFormat"/> is invalid.</exception> public static PBETurnAction[] CreateActions(PBETeam team) { if (team == null) { throw new ArgumentNullException(nameof(team)); } if (team.Battle.BattleState != PBEBattleState.WaitingForActions) { throw new InvalidOperationException($"{nameof(team.Battle.BattleState)} must be {PBEBattleState.WaitingForActions} to create actions."); } var actions = new PBETurnAction[team.ActionsRequired.Count]; var standBy = new List <PBEPokemon>(); for (int i = 0; i < actions.Length; i++) { PBEPokemon pkmn = team.ActionsRequired[i]; // If a Pokémon is forced to struggle, it is best that it just stays in until it faints if (pkmn.IsForcedToStruggle()) { actions[i] = new PBETurnAction(pkmn.Id, PBEMove.Struggle, GetPossibleTargets(pkmn, pkmn.GetMoveTargets(PBEMove.Struggle))[0]); } // If a Pokémon has a temp locked move (Dig, Dive, Shadow Force) it must be used else if (pkmn.TempLockedMove != PBEMove.None) { actions[i] = new PBETurnAction(pkmn.Id, pkmn.TempLockedMove, pkmn.TempLockedTargets); } // The Pokémon is free to switch or fight (unless it cannot switch due to Magnet Pull etc) else { // Gather all options of switching and moves PBEPokemon[] availableForSwitch = team.Party.Except(standBy).Where(p => p.FieldPosition == PBEFieldPosition.None && p.HP > 0).ToArray(); PBEMove[] usableMoves = pkmn.GetUsableMoves(); var possibleActions = new List <(PBETurnAction Action, double Score)>(); for (int m = 0; m < usableMoves.Length; m++) // Score moves { PBEMove move = usableMoves[m]; PBEType moveType = pkmn.GetMoveType(move); PBEMoveTarget moveTargets = pkmn.GetMoveTargets(move); PBETurnTarget[] possibleTargets = PBEMoveData.IsSpreadMove(moveTargets) ? new PBETurnTarget[] { GetSpreadMoveTargets(pkmn, moveTargets) } : GetPossibleTargets(pkmn, moveTargets); foreach (PBETurnTarget possibleTarget in possibleTargets) { // TODO: RandomFoeSurrounding (probably just account for the specific effects that use this target type) var targets = new List <PBEPokemon>(); if (possibleTarget.HasFlag(PBETurnTarget.AllyLeft)) { targets.Add(team.TryGetPokemon(PBEFieldPosition.Left)); } if (possibleTarget.HasFlag(PBETurnTarget.AllyCenter)) { targets.Add(team.TryGetPokemon(PBEFieldPosition.Center)); } if (possibleTarget.HasFlag(PBETurnTarget.AllyRight)) { targets.Add(team.TryGetPokemon(PBEFieldPosition.Right)); } if (possibleTarget.HasFlag(PBETurnTarget.FoeLeft)) { targets.Add(team.OpposingTeam.TryGetPokemon(PBEFieldPosition.Left)); } if (possibleTarget.HasFlag(PBETurnTarget.FoeCenter)) { targets.Add(team.OpposingTeam.TryGetPokemon(PBEFieldPosition.Center)); } if (possibleTarget.HasFlag(PBETurnTarget.FoeRight)) { targets.Add(team.OpposingTeam.TryGetPokemon(PBEFieldPosition.Right)); } double score = 0.0; switch (PBEMoveData.Data[move].Effect) { case PBEMoveEffect.Burn: case PBEMoveEffect.Paralyze: case PBEMoveEffect.Poison: case PBEMoveEffect.Sleep: case PBEMoveEffect.Toxic: { foreach (PBEPokemon target in targets) { if (target == null) { // TODO: If all targets are null, this should give a bad score } else { // TODO: Effectiveness check // TODO: Favor sleep with Bad Dreams (unless ally) if (target.Status1 != PBEStatus1.None) { score += target.Team == team.OpposingTeam ? -60 : 0; } else { score += target.Team == team.OpposingTeam ? +40 : -20; } } } break; } case PBEMoveEffect.Hail: { if (team.Battle.Weather == PBEWeather.Hailstorm) { score -= 100; } break; } case PBEMoveEffect.BrickBreak: case PBEMoveEffect.Dig: case PBEMoveEffect.Dive: case PBEMoveEffect.Fly: case PBEMoveEffect.Hit: case PBEMoveEffect.Hit__MaybeBurn: case PBEMoveEffect.Hit__MaybeConfuse: case PBEMoveEffect.Hit__MaybeFlinch: case PBEMoveEffect.Hit__MaybeFreeze: case PBEMoveEffect.Hit__MaybeLowerTarget_ACC_By1: case PBEMoveEffect.Hit__MaybeLowerTarget_ATK_By1: case PBEMoveEffect.Hit__MaybeLowerTarget_DEF_By1: case PBEMoveEffect.Hit__MaybeLowerTarget_SPATK_By1: case PBEMoveEffect.Hit__MaybeLowerTarget_SPDEF_By1: case PBEMoveEffect.Hit__MaybeLowerTarget_SPDEF_By2: case PBEMoveEffect.Hit__MaybeLowerTarget_SPE_By1: case PBEMoveEffect.Hit__MaybeLowerUser_ATK_DEF_By1: case PBEMoveEffect.Hit__MaybeLowerUser_DEF_SPDEF_By1: case PBEMoveEffect.Hit__MaybeLowerUser_SPATK_By2: case PBEMoveEffect.Hit__MaybeLowerUser_SPE_By1: case PBEMoveEffect.Hit__MaybeLowerUser_SPE_DEF_SPDEF_By1: case PBEMoveEffect.Hit__MaybeParalyze: case PBEMoveEffect.Hit__MaybePoison: case PBEMoveEffect.Hit__MaybeRaiseUser_ATK_By1: case PBEMoveEffect.Hit__MaybeRaiseUser_ATK_DEF_SPATK_SPDEF_SPE_By1: case PBEMoveEffect.Hit__MaybeRaiseUser_DEF_By1: case PBEMoveEffect.Hit__MaybeRaiseUser_SPATK_By1: case PBEMoveEffect.Hit__MaybeRaiseUser_SPE_By1: case PBEMoveEffect.Hit__MaybeToxic: case PBEMoveEffect.HPDrain: case PBEMoveEffect.Recoil: case PBEMoveEffect.FlareBlitz: case PBEMoveEffect.SuckerPunch: case PBEMoveEffect.VoltTackle: { foreach (PBEPokemon target in targets) { if (target == null) { // TODO: If all targets are null, this should give a bad score } else { // TODO: Put type checking somewhere in PBEPokemon (levitate, wonder guard, etc) // TODO: Favor hitting ally with move if it absorbs it // TODO: Check items // TODO: Stat changes and accuracy // TODO: Check base power specifically against hp remaining (include spread move damage reduction) double typeEffectiveness = PBEPokemonData.TypeEffectiveness[moveType][target.KnownType1]; typeEffectiveness *= PBEPokemonData.TypeEffectiveness[moveType][target.KnownType2]; if (typeEffectiveness <= 0.0) // (-infinity, 0.0] Ineffective { score += target.Team == team.OpposingTeam ? -60 : -1; } else if (typeEffectiveness <= 0.25) // (0.0, 0.25] NotVeryEffective { score += target.Team == team.OpposingTeam ? -30 : -5; } else if (typeEffectiveness < 1.0) // (0.25, 1.0) NotVeryEffective { score += target.Team == team.OpposingTeam ? -10 : -10; } else if (typeEffectiveness == 1.0) // [1.0, 1.0] Normal { score += target.Team == team.OpposingTeam ? +10 : -15; } else if (typeEffectiveness < 4.0) // (1.0, 4.0) SuperEffective { score += target.Team == team.OpposingTeam ? +25 : -20; } else // [4.0, infinity) SuperEffective { score += target.Team == team.OpposingTeam ? +40 : -30; } if (pkmn.HasType(moveType) && typeEffectiveness > 0.0) // STAB { score += (pkmn.Ability == PBEAbility.Adaptability ? 15 : 10) * (target.Team == team.OpposingTeam ? +1 : -1); } } } break; } case PBEMoveEffect.Moonlight: case PBEMoveEffect.Rest: case PBEMoveEffect.RestoreTargetHP: case PBEMoveEffect.RestoreUserHP: { PBEPokemon target = targets[0]; if (target == null || target.Team == team.OpposingTeam) { score -= 100; } else // Ally { // 0% = +45, 25% = +30, 50% = +15, 75% = 0, 100% = -15 score -= (60 * target.HPPercentage) - 45; } break; } case PBEMoveEffect.RainDance: { if (team.Battle.Weather == PBEWeather.Rain) { score -= 100; } break; } case PBEMoveEffect.Sandstorm: { if (team.Battle.Weather == PBEWeather.Sandstorm) { score -= 100; } break; } case PBEMoveEffect.SunnyDay: { if (team.Battle.Weather == PBEWeather.HarshSunlight) { score -= 100; } break; } case PBEMoveEffect.Attract: case PBEMoveEffect.ChangeTarget_ACC: case PBEMoveEffect.ChangeTarget_ATK: case PBEMoveEffect.ChangeTarget_DEF: case PBEMoveEffect.ChangeTarget_EVA: case PBEMoveEffect.ChangeTarget_SPDEF: case PBEMoveEffect.ChangeTarget_SPE: case PBEMoveEffect.ChangeUser_ATK: case PBEMoveEffect.ChangeUser_DEF: case PBEMoveEffect.ChangeUser_EVA: case PBEMoveEffect.ChangeUser_SPATK: case PBEMoveEffect.ChangeUser_SPDEF: case PBEMoveEffect.ChangeUser_SPE: case PBEMoveEffect.Confuse: case PBEMoveEffect.Curse: case PBEMoveEffect.Endeavor: case PBEMoveEffect.Fail: case PBEMoveEffect.FinalGambit: case PBEMoveEffect.Flatter: case PBEMoveEffect.FocusEnergy: case PBEMoveEffect.GastroAcid: case PBEMoveEffect.Growth: case PBEMoveEffect.HelpingHand: case PBEMoveEffect.LeechSeed: case PBEMoveEffect.LightScreen: case PBEMoveEffect.LowerTarget_ATK_DEF_By1: case PBEMoveEffect.LowerUser_DEF_SPDEF_By1_Raise_ATK_SPATK_SPE_By2: case PBEMoveEffect.LuckyChant: case PBEMoveEffect.Metronome: case PBEMoveEffect.OneHitKnockout: case PBEMoveEffect.PainSplit: case PBEMoveEffect.Protect: case PBEMoveEffect.PsychUp: case PBEMoveEffect.Psywave: case PBEMoveEffect.RaiseUser_ATK_ACC_By1: case PBEMoveEffect.RaiseUser_ATK_DEF_By1: case PBEMoveEffect.RaiseUser_ATK_DEF_ACC_By1: case PBEMoveEffect.RaiseUser_ATK_SPATK_By1: case PBEMoveEffect.RaiseUser_ATK_SPE_By1: case PBEMoveEffect.RaiseUser_DEF_SPDEF_By1: case PBEMoveEffect.RaiseUser_SPATK_SPDEF_By1: case PBEMoveEffect.RaiseUser_SPATK_SPDEF_SPE_By1: case PBEMoveEffect.RaiseUser_SPE_By2_ATK_By1: case PBEMoveEffect.Reflect: case PBEMoveEffect.SeismicToss: case PBEMoveEffect.Selfdestruct: case PBEMoveEffect.SetDamage: case PBEMoveEffect.Snore: case PBEMoveEffect.Spikes: case PBEMoveEffect.StealthRock: case PBEMoveEffect.Substitute: case PBEMoveEffect.SuperFang: case PBEMoveEffect.Swagger: case PBEMoveEffect.ToxicSpikes: case PBEMoveEffect.Transform: case PBEMoveEffect.TrickRoom: case PBEMoveEffect.Whirlwind: case PBEMoveEffect.WideGuard: { // TODO Moves break; } default: throw new ArgumentOutOfRangeException(nameof(PBEMoveData.Effect)); } possibleActions.Add((new PBETurnAction(pkmn.Id, move, possibleTarget), score)); } } if (pkmn.CanSwitchOut()) { for (int s = 0; s < availableForSwitch.Length; s++) // Score switches { PBEPokemon switchPkmn = availableForSwitch[s]; // TODO: Entry hazards // TODO: Known moves of active battlers // TODO: Type effectiveness double score = 0.0; possibleActions.Add((new PBETurnAction(pkmn.Id, switchPkmn.Id), score)); } } string ToDebugString((PBETurnAction Action, double Score) t) { string str = "{"; if (t.Action.Decision == PBETurnDecision.Fight) { str += string.Format("Fight {0} {1}", t.Action.FightMove, t.Action.FightTargets); } else { str += string.Format("Switch {0}", team.TryGetPokemon(t.Action.SwitchPokemonId).Nickname); } str += " [" + t.Score + "]}"; return(str); } IOrderedEnumerable <(PBETurnAction Action, double Score)> byScore = possibleActions.OrderByDescending(t => t.Score); Debug.WriteLine("{0}'s possible actions: {1}", pkmn.Nickname, byScore.Select(t => ToDebugString(t)).Print()); double bestScore = byScore.First().Score; actions[i] = byScore.Where(t => t.Score == bestScore).ToArray().RandomElement().Action; // Pick random action of the ones that tied for best score } // Action was chosen, finish up for this Pokémon if (actions[i].Decision == PBETurnDecision.SwitchOut) { standBy.Add(team.TryGetPokemon(actions[i].SwitchPokemonId)); } } return(actions); }
private void CB_Evolution() { switch (_state) { case State.FadeIn: { if (_fadeTransition.IsDone) { _fadeTransition = null; _stringWindow = new Window(0, 0.79f, 1, 0.16f, RenderUtils.Color(255, 255, 255, 255)); CreateMessage(string.Format("{0} is evolving!", _oldNickname)); _state = State.IsEvolvingMsg; } return; } case State.IsEvolvingMsg: { if (ReadMessage()) { _stringPrinter.Close(); _stringPrinter = null; _fadeTransition = new FadeToColorTransition(1_000, RenderUtils.ColorNoA(200, 200, 200)); _state = State.FadeToWhite; } return; } case State.FadeToWhite: { if (TryCancelEvolution()) { _fadeTransition = null; CreateMessage(string.Format("{0} stopped evolving!", _oldNickname)); _state = State.CancelledMsg; return; } if (_fadeTransition.IsDone) { _fadeTransition = null; if (_evo.Method == EvoMethod.Ninjask_LevelUp) { Evolution.TryCreateShedinja(_pkmn); } _pkmn.Evolve(_evo); LoadPkmnImage(); _fadeTransition = new FadeFromColorTransition(1_000, RenderUtils.ColorNoA(200, 200, 200)); _state = State.FadeToEvo; } return; } case State.FadeToEvo: { if (_fadeTransition.IsDone) { _fadeTransition = null; SoundControl.PlayCry(_pkmn.Species, _pkmn.Form); CreateMessage(string.Format("{0} evolved into {1}!", _oldNickname, PBELocalizedString.GetSpeciesName(_pkmn.Species).English)); _state = State.EvolvedIntoMsg; } return; } case State.EvolvedIntoMsg: { if (ReadMessage()) { _stringPrinter.Close(); _stringPrinter = null; // Check for moves to learn _learningMoves = new Queue <PBEMove>(new LevelUpData(_pkmn.Species, _pkmn.Form).GetNewMoves(_pkmn.Level)); CheckForLearnMoves(); } return; } case State.FadeOut: { if (_fadeTransition.IsDone) { _fadeTransition = null; OverworldGUI.Instance.ReturnToFieldWithFadeInAfterEvolutionCheck(); } return; } case State.CancelledMsg: { if (ReadMessage()) { _stringPrinter.Close(); _stringPrinter = null; SetFadeOut(); } return; } // Learning moves case State.LearnMove_WantsToLearnMoveMsg: { if (ReadMessageEnded()) { TextGUIChoices.CreateStandardYesNoChoices(ShouldLearnMoveAction, out _textChoices, out _textChoicesWindow); _state = State.LearnMove_WantsToLearnMoveChoice; } return; } case State.LearnMove_WantsToLearnMoveChoice: case State.LearnMove_GiveUpLearningChoice: { HandleMultichoice(); return; } case State.LearnMove_FadeToSummary: { if (_fadeTransition.IsDone) { _fadeTransition = null; _stringWindow.IsInvisible = true; _textChoicesWindow.Close(); _textChoicesWindow = null; _textChoices.Dispose(); _textChoices = null; _stringPrinter.Close(); _stringPrinter = null; _ = new SummaryGUI(_pkmn, SummaryGUI.Mode.LearnMove, OnSummaryClosed, learningMove: _learningMoves.Peek()); } return; } case State.LearnMove_FadeFromSummary: { if (_fadeTransition.IsDone) { // Give up on learning if (_forgetMove == -1 || _forgetMove == PkmnConstants.NumMoves) { SetGiveUpLearningMove(); } else { Moveset.MovesetSlot slot = _pkmn.Moveset[_forgetMove]; PBEMove oldMove = slot.Move; string oldMoveStr = PBELocalizedString.GetMoveName(oldMove).English; PBEMove move = _learningMoves.Dequeue(); // Remove from queue string moveStr = PBELocalizedString.GetMoveName(move).English; slot.Move = move; PBEMoveData mData = PBEMoveData.Data[move]; slot.PP = PBEDataUtils.CalcMaxPP(mData.PPTier, 0, PkmnConstants.PBESettings); slot.PPUps = 0; CreateMessage(string.Format("{0} forgot {1}\nand learned {2}!", _pkmn.Nickname, oldMoveStr, moveStr)); _state = State.LearnMove_ForgotMsg; } } return; } case State.LearnMove_GiveUpLearningMsg: { if (ReadMessageEnded()) { TextGUIChoices.CreateStandardYesNoChoices(ShouldGiveUpMoveAction, out _textChoices, out _textChoicesWindow); _state = State.LearnMove_GiveUpLearningChoice; } return; } case State.LearnMove_DidNotLearnMsg: case State.LearnMove_ForgotMsg: { if (ReadMessage()) { _stringPrinter.Close(); _stringPrinter = null; CheckForLearnMoves(); } return; } } }
/// <summary>Creates valid actions for a battle turn for a specific team.</summary> /// <param name="team">The team to create actions for.</param> /// <exception cref="InvalidOperationException">Thrown when <paramref name="team"/> has no active battlers or <paramref name="team"/>'s <see cref="PBETeam.Battle"/>'s <see cref="PBEBattle.BattleState"/> is not <see cref="PBEBattleState.WaitingForActions"/>.</exception> /// <exception cref="ArgumentOutOfRangeException">Thrown when a Pokémon has no moves, the AI tries to use a move with invalid targets, or <paramref name="team"/>'s <see cref="PBETeam.Battle"/>'s <see cref="PBEBattle.BattleFormat"/> is invalid.</exception> public static PBETurnAction[] CreateActions(PBETeam team) { if (team == null) { throw new ArgumentNullException(nameof(team)); } if (team.IsDisposed) { throw new ObjectDisposedException(nameof(team)); } if (team.Battle.BattleState != PBEBattleState.WaitingForActions) { throw new InvalidOperationException($"{nameof(team.Battle.BattleState)} must be {PBEBattleState.WaitingForActions} to create actions."); } var actions = new PBETurnAction[team.ActionsRequired.Count]; var standBy = new List <PBEPokemon>(); for (int i = 0; i < actions.Length; i++) { PBEPokemon user = team.ActionsRequired[i]; // If a Pokémon is forced to struggle, it is best that it just stays in until it faints if (user.IsForcedToStruggle()) { actions[i] = new PBETurnAction(user.Id, PBEMove.Struggle, GetPossibleTargets(user, user.GetMoveTargets(PBEMove.Struggle))[0]); } // If a Pokémon has a temp locked move (Dig, Dive, Shadow Force) it must be used else if (user.TempLockedMove != PBEMove.None) { actions[i] = new PBETurnAction(user.Id, user.TempLockedMove, user.TempLockedTargets); } // The Pokémon is free to switch or fight (unless it cannot switch due to Magnet Pull etc) else { // Gather all options of switching and moves PBEPokemon[] availableForSwitch = team.Party.Except(standBy).Where(p => p.FieldPosition == PBEFieldPosition.None && p.HP > 0).ToArray(); PBEMove[] usableMoves = user.GetUsableMoves(); var possibleActions = new List <(PBETurnAction Action, double Score)>(); for (int m = 0; m < usableMoves.Length; m++) // Score moves { PBEMove move = usableMoves[m]; PBEType moveType = user.GetMoveType(move); PBEMoveTarget moveTargets = user.GetMoveTargets(move); PBETurnTarget[] possibleTargets = PBEMoveData.IsSpreadMove(moveTargets) ? new PBETurnTarget[] { GetSpreadMoveTargets(user, moveTargets) } : GetPossibleTargets(user, moveTargets); foreach (PBETurnTarget possibleTarget in possibleTargets) { // TODO: RandomFoeSurrounding (probably just account for the specific effects that use this target type) // TODO: Don't queue up to do the same thing (two trying to afflict the same target when there are multiple targets) var targets = new List <PBEPokemon>(); if (possibleTarget.HasFlag(PBETurnTarget.AllyLeft)) { targets.Add(team.TryGetPokemon(PBEFieldPosition.Left)); } if (possibleTarget.HasFlag(PBETurnTarget.AllyCenter)) { targets.Add(team.TryGetPokemon(PBEFieldPosition.Center)); } if (possibleTarget.HasFlag(PBETurnTarget.AllyRight)) { targets.Add(team.TryGetPokemon(PBEFieldPosition.Right)); } if (possibleTarget.HasFlag(PBETurnTarget.FoeLeft)) { targets.Add(team.OpposingTeam.TryGetPokemon(PBEFieldPosition.Left)); } if (possibleTarget.HasFlag(PBETurnTarget.FoeCenter)) { targets.Add(team.OpposingTeam.TryGetPokemon(PBEFieldPosition.Center)); } if (possibleTarget.HasFlag(PBETurnTarget.FoeRight)) { targets.Add(team.OpposingTeam.TryGetPokemon(PBEFieldPosition.Right)); } double score; if (targets.All(p => p == null)) { score = -100; } else { score = 0d; targets.RemoveAll(p => p == null); PBEMoveData mData = PBEMoveData.Data[move]; switch (mData.Effect) { case PBEMoveEffect.Attract: { foreach (PBEPokemon target in targets) { // TODO: Destiny knot if (target.IsAttractionPossible(user, useKnownInfo: true) == PBEResult.Success) { score += target.Team == team ? -20 : +40; } else { score += target.Team == team ? 0 : -60; } } break; } case PBEMoveEffect.BrickBreak: case PBEMoveEffect.Dig: case PBEMoveEffect.Dive: case PBEMoveEffect.Fly: case PBEMoveEffect.Hit: case PBEMoveEffect.Hit__MaybeBurn: case PBEMoveEffect.Hit__MaybeBurn__10PercentFlinch: case PBEMoveEffect.Hit__MaybeConfuse: case PBEMoveEffect.Hit__MaybeFlinch: case PBEMoveEffect.Hit__MaybeFreeze: case PBEMoveEffect.Hit__MaybeFreeze__10PercentFlinch: case PBEMoveEffect.Hit__MaybeLowerTarget_ACC_By1: case PBEMoveEffect.Hit__MaybeLowerTarget_ATK_By1: case PBEMoveEffect.Hit__MaybeLowerTarget_DEF_By1: case PBEMoveEffect.Hit__MaybeLowerTarget_SPATK_By1: case PBEMoveEffect.Hit__MaybeLowerTarget_SPDEF_By1: case PBEMoveEffect.Hit__MaybeLowerTarget_SPDEF_By2: case PBEMoveEffect.Hit__MaybeLowerTarget_SPE_By1: case PBEMoveEffect.Hit__MaybeLowerUser_ATK_DEF_By1: case PBEMoveEffect.Hit__MaybeLowerUser_DEF_SPDEF_By1: case PBEMoveEffect.Hit__MaybeLowerUser_SPATK_By2: case PBEMoveEffect.Hit__MaybeLowerUser_SPE_By1: case PBEMoveEffect.Hit__MaybeLowerUser_SPE_DEF_SPDEF_By1: case PBEMoveEffect.Hit__MaybeParalyze: case PBEMoveEffect.Hit__MaybeParalyze__10PercentFlinch: case PBEMoveEffect.Hit__MaybePoison: case PBEMoveEffect.Hit__MaybeRaiseUser_ATK_By1: case PBEMoveEffect.Hit__MaybeRaiseUser_ATK_DEF_SPATK_SPDEF_SPE_By1: case PBEMoveEffect.Hit__MaybeRaiseUser_DEF_By1: case PBEMoveEffect.Hit__MaybeRaiseUser_SPATK_By1: case PBEMoveEffect.Hit__MaybeRaiseUser_SPE_By1: case PBEMoveEffect.Hit__MaybeToxic: case PBEMoveEffect.HPDrain: case PBEMoveEffect.Recoil: case PBEMoveEffect.Recoil__10PercentBurn: case PBEMoveEffect.Recoil__10PercentParalyze: case PBEMoveEffect.SuckerPunch: { foreach (PBEPokemon target in targets) { // TODO: Favor hitting ally with move if waterabsorb/voltabsorb etc // TODO: Liquid ooze // TODO: Check items // TODO: Stat changes and accuracy (even thunder/guillotine accuracy) // TODO: Check base power specifically against hp remaining (include spread move damage reduction) target.IsAffectedByMove(user, moveType, out double damageMultiplier, useKnownInfo: true); if (damageMultiplier <= 0) // (-infinity, 0.0] Ineffective { score += target.Team == team ? 0 : -60; } else if (damageMultiplier <= 0.25) // (0.0, 0.25] NotVeryEffective { score += target.Team == team ? -5 : -30; } else if (damageMultiplier < 1) // (0.25, 1.0) NotVeryEffective { score += target.Team == team ? -10 : -10; } else if (damageMultiplier == 1) // [1.0, 1.0] Normal { score += target.Team == team ? -15 : +10; } else if (damageMultiplier < 4) // (1.0, 4.0) SuperEffective { score += target.Team == team ? -20 : +25; } else // [4.0, infinity) SuperEffective { score += target.Team == team ? -30 : +40; } if (user.HasType(moveType) && damageMultiplier > 0) // STAB { score += (user.Ability == PBEAbility.Adaptability ? 7 : 5) * (target.Team == team ? -1 : +1); } } break; } case PBEMoveEffect.Burn: { foreach (PBEPokemon target in targets) { // TODO: Heatproof, physical attacker if (target.IsBurnPossible(user, useKnownInfo: true) == PBEResult.Success) { score += target.Team == team ? -20 : +40; } else { score += target.Team == team ? 0 : -60; } } break; } case PBEMoveEffect.ChangeTarget_ACC: { foreach (PBEPokemon target in targets) { ScoreStatChange(user, target, PBEStat.Accuracy, mData.EffectParam, ref score); } break; } case PBEMoveEffect.ChangeTarget_ATK: { foreach (PBEPokemon target in targets) { ScoreStatChange(user, target, PBEStat.Attack, mData.EffectParam, ref score); } break; } case PBEMoveEffect.ChangeTarget_DEF: { foreach (PBEPokemon target in targets) { ScoreStatChange(user, target, PBEStat.Defense, mData.EffectParam, ref score); } break; } case PBEMoveEffect.ChangeTarget_EVA: { foreach (PBEPokemon target in targets) { ScoreStatChange(user, target, PBEStat.Evasion, mData.EffectParam, ref score); } break; } case PBEMoveEffect.ChangeTarget_SPATK: { foreach (PBEPokemon target in targets) { ScoreStatChange(user, target, PBEStat.SpAttack, mData.EffectParam, ref score); } break; } case PBEMoveEffect.ChangeTarget_SPDEF: { foreach (PBEPokemon target in targets) { ScoreStatChange(user, target, PBEStat.SpDefense, mData.EffectParam, ref score); } break; } case PBEMoveEffect.ChangeTarget_SPE: { foreach (PBEPokemon target in targets) { ScoreStatChange(user, target, PBEStat.Speed, mData.EffectParam, ref score); } break; } case PBEMoveEffect.Confuse: case PBEMoveEffect.Flatter: case PBEMoveEffect.Swagger: { foreach (PBEPokemon target in targets) { // TODO: Only swagger/flatter if the opponent most likely won't use it against you if (target.IsConfusionPossible(user, useKnownInfo: true) == PBEResult.Success) { score += target.Team == team ? -20 : +40; } else { score += target.Team == team ? 0 : -60; } } break; } case PBEMoveEffect.Growth: { int change = team.Battle.WillLeafGuardActivate() ? +2 : +1; foreach (PBEPokemon target in targets) { ScoreStatChange(user, target, PBEStat.Attack, change, ref score); ScoreStatChange(user, target, PBEStat.SpAttack, change, ref score); } break; } case PBEMoveEffect.LeechSeed: { foreach (PBEPokemon target in targets) { if (target.IsLeechSeedPossible(useKnownInfo: true) == PBEResult.Success) { score += target.Team == team ? -20 : +40; } else { score += target.Team == team ? 0 : -60; } } break; } case PBEMoveEffect.LightScreen: { score += team.TeamStatus.HasFlag(PBETeamStatus.LightScreen) || IsTeammateUsingEffect(actions, PBEMoveEffect.LightScreen) ? -100 : +40; break; } case PBEMoveEffect.LowerTarget_ATK_DEF_By1: { foreach (PBEPokemon target in targets) { ScoreStatChange(user, target, PBEStat.Attack, -1, ref score); ScoreStatChange(user, target, PBEStat.Defense, -1, ref score); } break; } case PBEMoveEffect.LowerTarget_DEF_SPDEF_By1_Raise_ATK_SPATK_SPE_By2: { foreach (PBEPokemon target in targets) { ScoreStatChange(user, target, PBEStat.Defense, -1, ref score); ScoreStatChange(user, target, PBEStat.SpDefense, -1, ref score); ScoreStatChange(user, target, PBEStat.Attack, +2, ref score); ScoreStatChange(user, target, PBEStat.SpAttack, +2, ref score); ScoreStatChange(user, target, PBEStat.Speed, +2, ref score); } break; } case PBEMoveEffect.LuckyChant: { score += team.TeamStatus.HasFlag(PBETeamStatus.LuckyChant) || IsTeammateUsingEffect(actions, PBEMoveEffect.LuckyChant) ? -100 : +40; break; } case PBEMoveEffect.Moonlight: case PBEMoveEffect.Rest: case PBEMoveEffect.RestoreTargetHP: { foreach (PBEPokemon target in targets) { if (target.Team == team) { score += HPAware(target.HPPercentage, +45, -15); } else { score -= 100; } } break; } case PBEMoveEffect.Nothing: case PBEMoveEffect.Teleport: { score -= 100; break; } case PBEMoveEffect.Paralyze: { foreach (PBEPokemon target in targets) { // TODO: Effectiveness with thunder wave/glare if (target.IsParalysisPossible(user, useKnownInfo: true) == PBEResult.Success) { score += target.Team == team ? -20 : +40; } else { score += target.Team == team ? 0 : -60; } } break; } case PBEMoveEffect.Poison: case PBEMoveEffect.Toxic: { foreach (PBEPokemon target in targets) { // TODO: Poison Heal if (target.IsPoisonPossible(user, useKnownInfo: true) == PBEResult.Success) { score += target.Team == team ? -20 : +40; } else { score += target.Team == team ? 0 : -60; } } break; } case PBEMoveEffect.RaiseTarget_ATK_ACC_By1: { foreach (PBEPokemon target in targets) { ScoreStatChange(user, target, PBEStat.Attack, +1, ref score); ScoreStatChange(user, target, PBEStat.Accuracy, +1, ref score); } break; } case PBEMoveEffect.RaiseTarget_ATK_DEF_By1: { foreach (PBEPokemon target in targets) { ScoreStatChange(user, target, PBEStat.Attack, +1, ref score); ScoreStatChange(user, target, PBEStat.Defense, +1, ref score); } break; } case PBEMoveEffect.RaiseTarget_ATK_DEF_ACC_By1: { foreach (PBEPokemon target in targets) { ScoreStatChange(user, target, PBEStat.Attack, +1, ref score); ScoreStatChange(user, target, PBEStat.Defense, +1, ref score); ScoreStatChange(user, target, PBEStat.Accuracy, +1, ref score); } break; } case PBEMoveEffect.RaiseTarget_ATK_SPATK_By1: { foreach (PBEPokemon target in targets) { ScoreStatChange(user, target, PBEStat.Attack, +1, ref score); ScoreStatChange(user, target, PBEStat.SpAttack, +1, ref score); } break; } case PBEMoveEffect.RaiseTarget_ATK_SPE_By1: { foreach (PBEPokemon target in targets) { ScoreStatChange(user, target, PBEStat.Attack, +1, ref score); ScoreStatChange(user, target, PBEStat.Speed, +1, ref score); } break; } case PBEMoveEffect.RaiseTarget_DEF_SPDEF_By1: { foreach (PBEPokemon target in targets) { ScoreStatChange(user, target, PBEStat.Defense, +1, ref score); ScoreStatChange(user, target, PBEStat.SpDefense, +1, ref score); } break; } case PBEMoveEffect.RaiseTarget_SPATK_SPDEF_By1: { foreach (PBEPokemon target in targets) { ScoreStatChange(user, target, PBEStat.SpAttack, +1, ref score); ScoreStatChange(user, target, PBEStat.SpDefense, +1, ref score); } break; } case PBEMoveEffect.RaiseTarget_SPATK_SPDEF_SPE_By1: { foreach (PBEPokemon target in targets) { ScoreStatChange(user, target, PBEStat.SpAttack, +1, ref score); ScoreStatChange(user, target, PBEStat.SpDefense, +1, ref score); ScoreStatChange(user, target, PBEStat.Speed, +1, ref score); } break; } case PBEMoveEffect.RaiseTarget_SPE_By2_ATK_By1: { foreach (PBEPokemon target in targets) { ScoreStatChange(user, target, PBEStat.Speed, +2, ref score); ScoreStatChange(user, target, PBEStat.Attack, +1, ref score); } break; } case PBEMoveEffect.Reflect: { score += team.TeamStatus.HasFlag(PBETeamStatus.Reflect) || IsTeammateUsingEffect(actions, PBEMoveEffect.Reflect) ? -100 : +40; break; } case PBEMoveEffect.Safeguard: { score += team.TeamStatus.HasFlag(PBETeamStatus.Safeguard) || IsTeammateUsingEffect(actions, PBEMoveEffect.Safeguard) ? -100 : +40; break; } case PBEMoveEffect.Sleep: { foreach (PBEPokemon target in targets) { // TODO: Bad Dreams if (target.IsSleepPossible(user, useKnownInfo: true) == PBEResult.Success) { score += target.Team == team ? -20 : +40; } else { score += target.Team == team ? 0 : -60; } } break; } case PBEMoveEffect.Substitute: { foreach (PBEPokemon target in targets) { if (target.IsSubstitutePossible() == PBEResult.Success) { score += target.Team == team?HPAware(target.HPPercentage, -30, +50) : -60; } else { score += target.Team == team ? 0 : -20; } } break; } case PBEMoveEffect.ChangeTarget_SPATK__IfAttractionPossible: case PBEMoveEffect.Conversion: case PBEMoveEffect.Curse: case PBEMoveEffect.Endeavor: case PBEMoveEffect.FinalGambit: case PBEMoveEffect.FocusEnergy: case PBEMoveEffect.GastroAcid: case PBEMoveEffect.Hail: case PBEMoveEffect.Haze: case PBEMoveEffect.HelpingHand: case PBEMoveEffect.HPDrain__RequireSleep: case PBEMoveEffect.MagnetRise: case PBEMoveEffect.Metronome: case PBEMoveEffect.OneHitKnockout: case PBEMoveEffect.PainSplit: case PBEMoveEffect.PowerTrick: case PBEMoveEffect.Protect: case PBEMoveEffect.PsychUp: case PBEMoveEffect.Psywave: case PBEMoveEffect.RainDance: case PBEMoveEffect.Sandstorm: case PBEMoveEffect.SeismicToss: case PBEMoveEffect.Selfdestruct: case PBEMoveEffect.SetDamage: case PBEMoveEffect.SimpleBeam: case PBEMoveEffect.Snore: case PBEMoveEffect.Soak: case PBEMoveEffect.Spikes: case PBEMoveEffect.StealthRock: case PBEMoveEffect.SunnyDay: case PBEMoveEffect.SuperFang: case PBEMoveEffect.Tailwind: case PBEMoveEffect.ToxicSpikes: case PBEMoveEffect.Transform: case PBEMoveEffect.TrickRoom: case PBEMoveEffect.Whirlwind: case PBEMoveEffect.WideGuard: { // TODO Moves break; } default: throw new ArgumentOutOfRangeException(nameof(PBEMoveData.Effect)); } } possibleActions.Add((new PBETurnAction(user.Id, move, possibleTarget), score)); } } if (user.CanSwitchOut()) { for (int s = 0; s < availableForSwitch.Length; s++) // Score switches { PBEPokemon switchPkmn = availableForSwitch[s]; // TODO: Entry hazards // TODO: Known moves of active battlers // TODO: Type effectiveness double score = -10d; possibleActions.Add((new PBETurnAction(user.Id, switchPkmn.Id), score)); } } string ToDebugString((PBETurnAction Action, double Score) t) { string str = "{"; if (t.Action.Decision == PBETurnDecision.Fight) { str += string.Format("Fight {0} {1}", t.Action.FightMove, t.Action.FightTargets); } else { str += string.Format("Switch {0}", team.TryGetPokemon(t.Action.SwitchPokemonId).Nickname); } str += " [" + t.Score + "]}"; return(str); } IOrderedEnumerable <(PBETurnAction Action, double Score)> byScore = possibleActions.OrderByDescending(t => t.Score); Debug.WriteLine("{0}'s possible actions: {1}", user.Nickname, byScore.Select(t => ToDebugString(t)).Print()); double bestScore = byScore.First().Score; actions[i] = byScore.Where(t => t.Score == bestScore).ToArray().RandomElement().Action; // Pick random action of the ones that tied for best score } // Action was chosen, finish up for this Pokémon if (actions[i].Decision == PBETurnDecision.SwitchOut) { standBy.Add(team.TryGetPokemon(actions[i].SwitchPokemonId)); } } return(actions); }
private double CalculateBasePower(PBEBattlePokemon user, PBEBattlePokemon[] targets, PBEMoveData mData, PBEType moveType) { double basePower; #region Get move's base power switch (mData.Effect) { case PBEMoveEffect.CrushGrip: { basePower = Math.Max(1, targets.Select(t => mData.Power * t.HP / t.MaxHP).Average()); break; } case PBEMoveEffect.Eruption: { basePower = Math.Max(1, mData.Power * user.HP / user.MaxHP); break; } case PBEMoveEffect.Flail: { int val = 48 * user.HP / user.MaxHP; if (val < 2) { basePower = 200; } else if (val < 4) { basePower = 150; } else if (val < 8) { basePower = 100; } else if (val < 16) { basePower = 80; } else if (val < 32) { basePower = 40; } else { basePower = 20; } break; } case PBEMoveEffect.Frustration: { basePower = Math.Max(1, (byte.MaxValue - user.Friendship) / 2.5); break; } case PBEMoveEffect.GrassKnot: { basePower = targets.Select(t => { if (t.Weight >= 200.0) { return(120); } else if (t.Weight >= 100.0) { return(100); } else if (t.Weight >= 50.0) { return(80); } else if (t.Weight >= 25.0) { return(60); } else if (t.Weight >= 10.0) { return(40); } return(20); }).Average(); break; } case PBEMoveEffect.HeatCrash: { basePower = targets.Select(t => { double relative = user.Weight / t.Weight; if (relative < 2) { return(40); } else if (relative < 3) { return(60); } else if (relative < 4) { return(80); } else if (relative < 5) { return(100); } return(120); }).Average(); break; } case PBEMoveEffect.HiddenPower: { basePower = user.IndividualValues.GetHiddenPowerBasePower(Settings); break; } case PBEMoveEffect.Magnitude: { int val = PBERandom.RandomInt(0, 99); byte magnitude; if (val < 5) // Magnitude 4 - 5% { magnitude = 4; basePower = 10; } else if (val < 15) // Magnitude 5 - 10% { magnitude = 5; basePower = 30; } else if (val < 35) // Magnitude 6 - 20% { magnitude = 6; basePower = 50; } else if (val < 65) // Magnitude 7 - 30% { magnitude = 7; basePower = 70; } else if (val < 85) // Magnitude 8 - 20% { magnitude = 8; basePower = 90; } else if (val < 95) // Magnitude 9 - 10% { magnitude = 9; basePower = 110; } else // Magnitude 10 - 5% { magnitude = 10; basePower = 150; } BroadcastMagnitude(magnitude); break; } case PBEMoveEffect.Punishment: { basePower = Math.Max(1, Math.Min(200, targets.Select(t => mData.Power + (20 * t.GetPositiveStatTotal())).Average())); break; } case PBEMoveEffect.Return: { basePower = Math.Max(1, user.Friendship / 2.5); break; } case PBEMoveEffect.StoredPower: { basePower = mData.Power + (20 * user.GetPositiveStatTotal()); break; } default: { basePower = Math.Max(1, (int)mData.Power); break; } } #endregion // Technician goes before any other power boosts if (user.Ability == PBEAbility.Technician && basePower <= 60) { basePower *= 1.5; } #region Item-specific power boosts switch (moveType) { case PBEType.Bug: { switch (user.Item) { case PBEItem.InsectPlate: case PBEItem.SilverPowder: { basePower *= 1.2; break; } case PBEItem.BugGem: { BroadcastItem(user, user, PBEItem.BugGem, PBEItemAction.Consumed); basePower *= 1.5; break; } } break; } case PBEType.Dark: { switch (user.Item) { case PBEItem.BlackGlasses: case PBEItem.DreadPlate: { basePower *= 1.2; break; } case PBEItem.DarkGem: { BroadcastItem(user, user, PBEItem.DarkGem, PBEItemAction.Consumed); basePower *= 1.5; break; } } break; } case PBEType.Dragon: { switch (user.Item) { case PBEItem.AdamantOrb: { if (user.OriginalSpecies == PBESpecies.Dialga) { basePower *= 1.2; } break; } case PBEItem.DracoPlate: case PBEItem.DragonFang: { basePower *= 1.2; break; } case PBEItem.GriseousOrb: { if (user.OriginalSpecies == PBESpecies.Giratina && user.RevertForm == PBEForm.Giratina_Origin) { basePower *= 1.2; } break; } case PBEItem.LustrousOrb: { if (user.OriginalSpecies == PBESpecies.Palkia) { basePower *= 1.2; } break; } case PBEItem.DragonGem: { BroadcastItem(user, user, PBEItem.DragonGem, PBEItemAction.Consumed); basePower *= 1.5; break; } } break; } case PBEType.Electric: { switch (user.Item) { case PBEItem.Magnet: case PBEItem.ZapPlate: { basePower *= 1.2; break; } case PBEItem.ElectricGem: { BroadcastItem(user, user, PBEItem.ElectricGem, PBEItemAction.Consumed); basePower *= 1.5; break; } } break; } case PBEType.Fighting: { switch (user.Item) { case PBEItem.BlackBelt: case PBEItem.FistPlate: { basePower *= 1.2; break; } case PBEItem.FightingGem: { BroadcastItem(user, user, PBEItem.FightingGem, PBEItemAction.Consumed); basePower *= 1.5; break; } } break; } case PBEType.Fire: { switch (user.Item) { case PBEItem.Charcoal: case PBEItem.FlamePlate: { basePower *= 1.2; break; } case PBEItem.FireGem: { BroadcastItem(user, user, PBEItem.FireGem, PBEItemAction.Consumed); basePower *= 1.5; break; } } break; } case PBEType.Flying: { switch (user.Item) { case PBEItem.SharpBeak: case PBEItem.SkyPlate: { basePower *= 1.2; break; } case PBEItem.FlyingGem: { BroadcastItem(user, user, PBEItem.FlyingGem, PBEItemAction.Consumed); basePower *= 1.5; break; } } break; } case PBEType.Ghost: { switch (user.Item) { case PBEItem.GriseousOrb: { if (user.OriginalSpecies == PBESpecies.Giratina && user.RevertForm == PBEForm.Giratina_Origin) { basePower *= 1.2; } break; } case PBEItem.SpellTag: case PBEItem.SpookyPlate: { basePower *= 1.2; break; } case PBEItem.GhostGem: { BroadcastItem(user, user, PBEItem.GhostGem, PBEItemAction.Consumed); basePower *= 1.5; break; } } break; } case PBEType.Grass: { switch (user.Item) { case PBEItem.MeadowPlate: case PBEItem.MiracleSeed: case PBEItem.RoseIncense: { basePower *= 1.2; break; } case PBEItem.GrassGem: { BroadcastItem(user, user, PBEItem.GrassGem, PBEItemAction.Consumed); basePower *= 1.5; break; } } break; } case PBEType.Ground: { switch (user.Item) { case PBEItem.EarthPlate: case PBEItem.SoftSand: { basePower *= 1.2; break; } case PBEItem.GroundGem: { BroadcastItem(user, user, PBEItem.GroundGem, PBEItemAction.Consumed); basePower *= 1.5; break; } } break; } case PBEType.Ice: { switch (user.Item) { case PBEItem.IciclePlate: case PBEItem.NeverMeltIce: { basePower *= 1.2; break; } case PBEItem.IceGem: { BroadcastItem(user, user, PBEItem.IceGem, PBEItemAction.Consumed); basePower *= 1.5; break; } } break; } case PBEType.None: { break; } case PBEType.Normal: { switch (user.Item) { case PBEItem.SilkScarf: { basePower *= 1.2; break; } case PBEItem.NormalGem: { BroadcastItem(user, user, PBEItem.NormalGem, PBEItemAction.Consumed); basePower *= 1.5; break; } } break; } case PBEType.Poison: { switch (user.Item) { case PBEItem.PoisonBarb: case PBEItem.ToxicPlate: { basePower *= 1.2; break; } case PBEItem.PoisonGem: { BroadcastItem(user, user, PBEItem.PoisonGem, PBEItemAction.Consumed); basePower *= 1.5; break; } } break; } case PBEType.Psychic: { switch (user.Item) { case PBEItem.MindPlate: case PBEItem.OddIncense: case PBEItem.TwistedSpoon: { basePower *= 1.2; break; } case PBEItem.PsychicGem: { BroadcastItem(user, user, PBEItem.PsychicGem, PBEItemAction.Consumed); basePower *= 1.5; break; } } break; } case PBEType.Rock: { switch (user.Item) { case PBEItem.HardStone: case PBEItem.RockIncense: case PBEItem.StonePlate: { basePower *= 1.2; break; } case PBEItem.RockGem: { BroadcastItem(user, user, PBEItem.RockGem, PBEItemAction.Consumed); basePower *= 1.5; break; } } break; } case PBEType.Steel: { switch (user.Item) { case PBEItem.AdamantOrb: { if (user.OriginalSpecies == PBESpecies.Dialga) { basePower *= 1.2; } break; } case PBEItem.IronPlate: case PBEItem.MetalCoat: { basePower *= 1.2; break; } case PBEItem.SteelGem: { BroadcastItem(user, user, PBEItem.SteelGem, PBEItemAction.Consumed); basePower *= 1.5; break; } } break; } case PBEType.Water: { switch (user.Item) { case PBEItem.LustrousOrb: { if (user.OriginalSpecies == PBESpecies.Palkia) { basePower *= 1.2; } break; } case PBEItem.MysticWater: case PBEItem.SeaIncense: case PBEItem.SplashPlate: case PBEItem.WaveIncense: { basePower *= 1.2; break; } case PBEItem.WaterGem: { BroadcastItem(user, user, PBEItem.WaterGem, PBEItemAction.Consumed); basePower *= 1.5; break; } } break; } default: throw new ArgumentOutOfRangeException(nameof(moveType)); } #endregion #region Move-specific power boosts switch (mData.Effect) { case PBEMoveEffect.Acrobatics: { if (user.Item == PBEItem.None) { basePower *= 2.0; } break; } case PBEMoveEffect.Brine: { if (targets.Any(t => t.HP <= t.HP / 2)) { basePower *= 2.0; } break; } case PBEMoveEffect.Facade: { if (user.Status1 == PBEStatus1.Burned || user.Status1 == PBEStatus1.Paralyzed || user.Status1 == PBEStatus1.Poisoned || user.Status1 == PBEStatus1.BadlyPoisoned) { basePower *= 2.0; } break; } case PBEMoveEffect.Hex: { if (targets.Any(t => t.Status1 != PBEStatus1.None)) { basePower *= 2.0; } break; } case PBEMoveEffect.Payback: { if (targets.Any(t => t.HasUsedMoveThisTurn)) { basePower *= 2.0; } break; } case PBEMoveEffect.Retaliate: { if (user.Team.MonFaintedLastTurn) { basePower *= 2.0; } break; } case PBEMoveEffect.SmellingSalt: { if (targets.Any(t => t.Status1 == PBEStatus1.Paralyzed)) { basePower *= 2.0; } break; } case PBEMoveEffect.Venoshock: { if (targets.Any(t => t.Status1 == PBEStatus1.Poisoned || t.Status1 == PBEStatus1.BadlyPoisoned)) { basePower *= 2.0; } break; } case PBEMoveEffect.WakeUpSlap: { if (targets.Any(t => t.Status1 == PBEStatus1.Asleep)) { basePower *= 2.0; } break; } case PBEMoveEffect.WeatherBall: { if (ShouldDoWeatherEffects() && Weather != PBEWeather.None) { basePower *= 2.0; } break; } } #endregion #region Weather-specific power boosts if (ShouldDoWeatherEffects()) { switch (Weather) { case PBEWeather.HarshSunlight: { if (moveType == PBEType.Fire) { basePower *= 1.5; } else if (moveType == PBEType.Water) { basePower *= 0.5; } break; } case PBEWeather.Rain: { if (moveType == PBEType.Water) { basePower *= 1.5; } else if (moveType == PBEType.Fire) { basePower *= 0.5; } break; } case PBEWeather.Sandstorm: { if (user.Ability == PBEAbility.SandForce && (moveType == PBEType.Rock || moveType == PBEType.Ground || moveType == PBEType.Steel)) { basePower *= 1.3; } break; } } } #endregion #region Other power boosts if (user.Status2.HasFlag(PBEStatus2.HelpingHand)) { basePower *= 1.5; } if (user.Ability == PBEAbility.FlareBoost && mData.Category == PBEMoveCategory.Special && user.Status1 == PBEStatus1.Burned) { basePower *= 1.5; } if (user.Ability == PBEAbility.ToxicBoost && mData.Category == PBEMoveCategory.Physical && (user.Status1 == PBEStatus1.Poisoned || user.Status1 == PBEStatus1.BadlyPoisoned)) { basePower *= 1.5; } if (user.Item == PBEItem.LifeOrb) { basePower *= 1.3; } if (user.Ability == PBEAbility.IronFist && mData.Flags.HasFlag(PBEMoveFlag.AffectedByIronFist)) { basePower *= 1.2; } if (user.Ability == PBEAbility.Reckless && mData.Flags.HasFlag(PBEMoveFlag.AffectedByReckless)) { basePower *= 1.2; } if (user.Item == PBEItem.MuscleBand && mData.Category == PBEMoveCategory.Physical) { basePower *= 1.1; } if (user.Item == PBEItem.WiseGlasses && mData.Category == PBEMoveCategory.Special) { basePower *= 1.1; } #endregion return(basePower); }
private double CalculateDamageMultiplier(PBEPokemon user, PBEPokemon target, PBEMove move, PBEType moveType, PBEResult moveResult, bool criticalHit) { PBEMoveData mData = PBEMoveData.Data[move]; double damageMultiplier = 1; switch (move) { case PBEMove.Gust: case PBEMove.Twister: { if (target.Status2.HasFlag(PBEStatus2.Airborne)) { damageMultiplier *= 2.0; } break; } case PBEMove.Earthquake: case PBEMove.Magnitude: { if (target.Status2.HasFlag(PBEStatus2.Underground)) { damageMultiplier *= 2.0; } break; } case PBEMove.Steamroller: case PBEMove.Stomp: { if (target.Minimize_Used) { damageMultiplier *= 2.0; } break; } case PBEMove.Surf: { if (target.Status2.HasFlag(PBEStatus2.Underwater)) { damageMultiplier *= 2.0; } break; } } if (criticalHit) { damageMultiplier *= Settings.CritMultiplier; if (user.Ability == PBEAbility.Sniper) { damageMultiplier *= 1.5; } } else { if ((target.Team.TeamStatus.HasFlag(PBETeamStatus.Reflect) && mData.Category == PBEMoveCategory.Physical) || (target.Team.TeamStatus.HasFlag(PBETeamStatus.LightScreen) && mData.Category == PBEMoveCategory.Special)) { if (target.Team.NumPkmnOnField == 1) { damageMultiplier *= 0.5; } else { damageMultiplier *= 0.66; } } } switch (moveResult) { case PBEResult.NotVeryEffective_Type: { if (user.Ability == PBEAbility.TintedLens) { damageMultiplier *= 2.0; } break; } case PBEResult.SuperEffective_Type: { if ((target.Ability == PBEAbility.Filter || target.Ability == PBEAbility.SolidRock) && !user.HasCancellingAbility()) { damageMultiplier *= 0.75; } if (user.Item == PBEItem.ExpertBelt) { damageMultiplier *= 1.2; } break; } } if (user.HasType(moveType)) { if (user.Ability == PBEAbility.Adaptability) { damageMultiplier *= 2.0; } else { damageMultiplier *= 1.5; } } if (mData.Category == PBEMoveCategory.Physical && user.Status1 == PBEStatus1.Burned && user.Ability != PBEAbility.Guts) { damageMultiplier *= 0.5; } if (moveType == PBEType.Fire && target.Ability == PBEAbility.Heatproof && !user.HasCancellingAbility()) { damageMultiplier *= 0.5; } return(damageMultiplier); }
private void BasicHit(PBEBattlePokemon user, PBEBattlePokemon[] targets, PBEMoveData mData, Func <PBEBattlePokemon, PBEResult> failFunc = null, Action <PBEBattlePokemon> beforeDoingDamage = null, Action <PBEBattlePokemon, ushort> beforePostHit = null, Action <PBEBattlePokemon> afterPostHit = null, Func <int, int?> recoilFunc = null) { // Targets array is [FoeLeft, FoeCenter, FoeRight, AllyLeft, AllyCenter, AllyRight] // User can faint or heal with a berry at LiquidOoze, IronBarbs/RockyHelmet, and also at Recoil/LifeOrb // -------------Official order------------- // Setup - [effectiveness/fail checks foes], [effectiveness/fail checks allies], [miss/protection checks foes] [miss/protection checks allies], gem, // Allies - [sub damage allies, sub effectiveness allies, sub crit allies, sub break allies], [hit allies], [effectiveness allies], [crit allies], [posthit allies], [faint allies], // Foes - [sub damage foes, sub effectiveness foes, sub crit foes, sub break foes], [hit foes], [effectiveness foes], [crit foes], [posthit foes], [faint foes], // Cleanup - recoil, lifeorb, [colorchange foes], [colorchange allies], [berry allies], [berry foes], [antistatusability allies], [antistatusability foes], exp PBEType moveType = user.GetMoveType(mData); // DreamEater checks for sleep before gem activates // SuckerPunch fails Hit_GetVictims(user, targets, mData, moveType, out List <PBEAttackVictim> victims, failFunc: failFunc); if (victims.Count == 0) { return; } double basePower = CalculateBasePower(user, targets, mData, moveType); // Gem activates here double initDamageMultiplier = victims.Count > 1 ? 0.75 : 1d; int totalDamageDealt = 0; void CalcDamage(PBEAttackVictim victim) { PBEBattlePokemon target = victim.Pkmn; PBEResult result = victim.Result; double damageMultiplier = initDamageMultiplier * victim.TypeEffectiveness; // Brick Break destroys Light Screen and Reflect before doing damage (after gem) // Feint destroys protection // Pay Day scatters coins beforeDoingDamage?.Invoke(target); bool crit = CritCheck(user, target, mData); damageMultiplier *= CalculateDamageMultiplier(user, target, mData, moveType, result, crit); int damage = (int)(damageMultiplier * CalculateDamage(user, target, mData, moveType, basePower, crit)); victim.Damage = DealDamage(user, target, damage, ignoreSubstitute: false, ignoreSturdy: false); totalDamageDealt += victim.Damage; victim.Crit = crit; } void DoSub(IEnumerable <PBEAttackVictim> subs) { foreach (PBEAttackVictim victim in subs) { CalcDamage(victim); PBEBattlePokemon target = victim.Pkmn; PBEResult result = victim.Result; if (result != PBEResult.Success) { BroadcastMoveResult(user, target, result); } if (victim.Crit) { BroadcastMoveCrit(target); } if (target.SubstituteHP == 0) { BroadcastStatus2(target, user, PBEStatus2.Substitute, PBEStatusAction.Ended); } } } void DoNormal(IEnumerable <PBEAttackVictim> normals) { foreach (PBEAttackVictim victim in normals) { CalcDamage(victim); } Hit_DoMoveResult(user, normals); Hit_DoCrit(normals); foreach (PBEAttackVictim victim in normals) { PBEBattlePokemon target = victim.Pkmn; // Stats/statuses are changed before post-hit effects // HP-draining moves restore HP beforePostHit?.Invoke(target, victim.Damage); // TODO: LiquidOoze fainting/healing DoPostHitEffects(user, target, mData, moveType); // ShadowForce destroys protection // SmellingSalt cures paralysis // WakeUpSlap cures sleep afterPostHit?.Invoke(target); // Verified: These happen before Recoil/LifeOrb } Hit_FaintCheck(normals); } Hit_HitTargets(user.Team, DoSub, DoNormal, victims, out IEnumerable <PBEAttackVictim> allies, out IEnumerable <PBEAttackVictim> foes); DoPostAttackedEffects(user, allies, foes, true, recoilDamage: recoilFunc?.Invoke(totalDamageDealt), colorChangeType: moveType); }