Many more tests and fixes
All checks were successful
Build / Build (push) Successful in 3m12s

This commit is contained in:
2026-08-27 17:11:12 +02:00
parent 32b3ef9c4a
commit d2a82b5fe3
204 changed files with 20255 additions and 242 deletions

View File

@@ -19,6 +19,6 @@ public class Regenerator : Script, IScriptOnSwitchOut
{
BatchId = batchId,
});
oldPokemon.Heal(oldPokemon.MaxHealth / 3, batchId: batchId);
oldPokemon.Heal(oldPokemon.MaxHealth / 3, batchId: batchId, forceHeal: true);
}
}

View File

@@ -8,13 +8,15 @@ public class FairyLockEffect : Script, IScriptOnEndTurn, IScriptPreventSelfRunAw
/// <inheritdoc />
public void PreventSelfRunAway(IFleeChoice choice, ref bool prevent)
{
prevent = true;
if (choice.User.Types.All(x => x.Name != "ghost"))
prevent = true;
}
/// <inheritdoc />
public void PreventSelfSwitch(ISwitchChoice choice, ref bool prevent)
{
prevent = true;
if (choice.User.Types.All(x => x.Name != "ghost"))
prevent = true;
}
/// <inheritdoc />

View File

@@ -3,34 +3,73 @@ namespace PkmnLib.Plugin.Gen7.Scripts.Battle;
[Script(ScriptCategory.Battle, "future_sight")]
public class FutureSightEffect : Script, IScriptOnEndTurn
{
private int _turnsLeft = 3;
private readonly IMoveChoice _moveChoice;
private class PendingStrike
{
public IMoveChoice MoveChoice { get; }
public int Turns { get; set; }
public PendingStrike(IMoveChoice moveChoice)
{
MoveChoice = moveChoice;
Turns = 3;
}
}
private readonly List<PendingStrike> _strikes = new();
public FutureSightEffect(IMoveChoice moveChoice)
{
_moveChoice = moveChoice;
AddStrike(moveChoice);
}
public void AddStrike(IMoveChoice moveChoice)
{
_strikes.Add(new PendingStrike(moveChoice));
}
public bool HasStrikeAt(byte side, byte position)
{
return _strikes.Exists(x => x.MoveChoice.TargetSide == side && x.MoveChoice.TargetPosition == position);
}
/// <inheritdoc />
public void OnEndTurn(IScriptSource owner, IBattle battle)
{
_turnsLeft -= 1;
if (_turnsLeft <= 0)
var toRemove = new List<PendingStrike>();
foreach (var strike in _strikes)
{
var target = battle.GetPokemon(_moveChoice.TargetSide, _moveChoice.TargetPosition);
if (target is not { IsUsable: true })
strike.Turns--;
if (strike.Turns == 0)
{
battle.EventHook.Invoke(new DialogEvent("move_failed"));
return;
ExecuteStrike(battle, strike.MoveChoice);
toRemove.Add(strike);
}
var damageCalculator = battle.Library.DamageCalculator;
var executingMove = new ExecutingMoveImpl([target], 1, _moveChoice.ChosenMove,
_moveChoice.ChosenMove.MoveData, _moveChoice, battle);
var hitData = executingMove.GetHitData(target, 0);
var damage = damageCalculator.GetDamage(executingMove, executingMove.UseMove.Category, executingMove.User,
target, executingMove.TargetCount, 1, hitData);
target.Damage(damage, DamageSource.Misc);
}
foreach (var strike in toRemove)
{
_strikes.Remove(strike);
}
if (_strikes.Count == 0)
{
RemoveSelf();
}
}
private static void ExecuteStrike(IBattle battle, IMoveChoice moveChoice)
{
var target = battle.GetPokemon(moveChoice.TargetSide, moveChoice.TargetPosition);
if (target is not { IsUsable: true })
{
battle.EventHook.Invoke(new DialogEvent("move_failed"));
return;
}
var damageCalculator = battle.Library.DamageCalculator;
var executingMove = new ExecutingMoveImpl([target], 1, moveChoice.ChosenMove, moveChoice.ChosenMove.MoveData,
moveChoice, battle);
var hitData = executingMove.GetHitData(target, 0);
var damage = damageCalculator.GetDamage(executingMove, executingMove.UseMove.Category, executingMove.User,
target, executingMove.TargetCount, 1, hitData);
target.Damage(damage, DamageSource.Misc);
}
}

View File

@@ -1,7 +1,7 @@
namespace PkmnLib.Plugin.Gen7.Scripts.Battle;
[Script(ScriptCategory.Move, "ion_deluge")]
public class IonDelugeEffect : Script, IScriptChangeMoveType
public class IonDelugeEffect : Script, IScriptChangeMoveType, IScriptOnEndTurn
{
/// <inheritdoc />
public void ChangeMoveType(IExecutingMove move, IPokemon target, byte hit, ref TypeIdentifier? moveType)
@@ -12,4 +12,9 @@ public class IonDelugeEffect : Script, IScriptChangeMoveType
moveType = electricType;
}
}
public void OnEndTurn(IScriptSource owner, IBattle battle)
{
RemoveSelf();
}
}

View File

@@ -3,7 +3,7 @@ namespace PkmnLib.Plugin.Gen7.Scripts.Battle;
[Script(ScriptCategory.Battle, "magic_room")]
public class MagicRoomEffect : Script, IScriptOnBeforeAnyHookInvoked, IScriptOnEndTurn, IScriptPreventHeldItemConsume
{
private int _turnsLeft = 5;
private int _turnsLeft = 4;
/// <inheritdoc />
public void PreventHeldItemConsume(IPokemon pokemon, IItem heldItem, ref bool prevented)

View File

@@ -10,7 +10,7 @@ public class MudSportEffect : Script, IScriptChangeBasePower, IScriptOnEndTurn
{
if (move.UseMove.MoveType.Name == "electric")
{
basePower = basePower.MultiplyOrMax(2f / 3f);
basePower = basePower.MultiplyOrMax(1352 / 4096f);
}
}

View File

@@ -12,6 +12,11 @@ public abstract class BaseChargeMove<TVolatile> : Script, IScriptPreventMove, IS
if (move.User.Volatile.Contains<TVolatile>())
return;
var args = new CustomTriggers.BypassChargeMoveArgs(move, false);
move.RunScriptHook<IScriptCustomTrigger>(script => script.CustomTrigger(CustomTriggers.BypassChargeMove, args));
if (args.Bypass)
return;
move.User.Volatile.Add(CreateVolatile(move.User));
move.User.BattleData?.Battle.EventHook.Invoke(new DialogEvent("began_charging", new Dictionary<string, object>
{

View File

@@ -30,7 +30,7 @@ public abstract class ChangeUserStats : Script, IScriptOnInitialize, IScriptOnSe
}
/// <inheritdoc />
public void OnSecondaryEffect(IExecutingMove move, IPokemon target, byte hit)
public virtual void OnSecondaryEffect(IExecutingMove move, IPokemon target, byte hit)
{
move.User.ChangeStatBoost(_stat, _amount, true, false);
}

View File

@@ -0,0 +1,13 @@
using PkmnLib.Plugin.Gen7.Scripts.Pokemon;
namespace PkmnLib.Plugin.Gen7.Scripts.Moves;
[Script(ScriptCategory.Move, "defense_curl")]
public class DefenseCurl : ChangeUserDefense
{
public override void OnSecondaryEffect(IExecutingMove move, IPokemon target, byte hit)
{
base.OnSecondaryEffect(move, target, hit);
move.User.Volatile.StackOrAdd(ScriptUtils.ResolveName<DefenseCurlEffect>(), () => new DefenseCurlEffect());
}
}

View File

@@ -1,15 +1,31 @@
using PkmnLib.Plugin.Gen7.Scripts.Status;
using PkmnLib.Static.Moves;
namespace PkmnLib.Plugin.Gen7.Scripts.Moves;
[Script(ScriptCategory.Move, "facade")]
public class Facade : Script, IScriptChangeBasePower
public class Facade : Script, IScriptChangeBasePower, IScriptChangeMoveDamage
{
/// <inheritdoc />
public void ChangeBasePower(IExecutingMove move, IPokemon target, byte hit, ref ushort basePower)
{
var status = move.User.StatusScript.Script?.Name;
if (status == "paralyzed" || status == "burned" || status == "poisoned")
if (status == ScriptUtils.ResolveName<Paralyzed>() || status == ScriptUtils.ResolveName<Burned>() ||
status == ScriptUtils.ResolveName<Poisoned>() || status == ScriptUtils.ResolveName<BadlyPoisoned>())
{
basePower = basePower.MultiplyOrMax(2);
}
}
public void ChangeMoveDamage(IExecutingMove move, IPokemon target, byte hit, ref uint damage)
{
var status = move.User.StatusScript.Script?.Name;
if (status != ScriptUtils.ResolveName<Burned>())
return;
// Ignore Burn's effect of halving the damage done by physical moves.
if (move.UseMove.Category == MoveCategory.Physical)
{
damage = (uint)(damage * 2f);
}
}
}

View File

@@ -6,7 +6,7 @@ public class FalseSwipe : Script, IScriptChangeMoveDamage
/// <inheritdoc />
public void ChangeMoveDamage(IExecutingMove move, IPokemon target, byte hit, ref uint damage)
{
if (target.CurrentHealth - damage < 1)
if ((int)target.CurrentHealth - damage < 1)
{
damage = target.CurrentHealth - 1;
}

View File

@@ -1,4 +1,5 @@
using PkmnLib.Plugin.Gen7.Scripts.Pokemon;
using PkmnLib.Plugin.Gen7.Scripts.Side;
namespace PkmnLib.Plugin.Gen7.Scripts.Moves;
@@ -8,9 +9,26 @@ public class Feint : Script, IScriptOnBeforeHit
/// <inheritdoc />
public void OnBeforeHit(IExecutingMove move, IPokemon target, byte hitIndex)
{
if (target.Volatile.Contains<ProtectionEffectScript>())
var toRemove = new List<ScriptContainer>();
foreach (var script in target.Volatile)
{
target.Volatile.Remove<ProtectionEffectScript>();
if (script.Script is ProtectionEffectScript)
{
toRemove.Add(script);
}
}
foreach (var script in toRemove)
{
script.Clear();
}
var battleData = target.BattleData;
if (battleData is not null)
{
if (battleData.BattleSide.VolatileScripts.Contains<CraftyShieldEffect>())
battleData.BattleSide.VolatileScripts.Remove<CraftyShieldEffect>();
if (battleData.BattleSide.VolatileScripts.Contains<MatBlockEffect>())
battleData.BattleSide.VolatileScripts.Remove<MatBlockEffect>();
}
}
}

View File

@@ -8,7 +8,7 @@ public class FellStinger : Script, IScriptOnAfterHits
{
if (target.IsFainted)
{
move.User.ChangeStatBoost(Statistic.Attack, 2, true, false);
move.User.ChangeStatBoost(Statistic.Attack, 3, true, false);
}
}
}

View File

@@ -3,11 +3,15 @@ using PkmnLib.Plugin.Gen7.Scripts.Pokemon;
namespace PkmnLib.Plugin.Gen7.Scripts.Moves;
[Script(ScriptCategory.Move, "fire_spin")]
public class FireSpin : MultiHitMove, IScriptOnSecondaryEffect
public class FireSpin : Script, IScriptOnSecondaryEffect
{
/// <inheritdoc />
public void OnSecondaryEffect(IExecutingMove move, IPokemon target, byte hit)
{
target.Volatile.StackOrAdd("fire_spin", () => new FireSpinEffect(target));
target.Volatile.StackOrAdd("fire_spin", () =>
{
var turns = target.BattleData!.Battle.Random.GetInt(4, 6);
return new FireSpinEffect(target, turns, move.User);
});
}
}

View File

@@ -6,7 +6,7 @@ public class Flail : Script, IScriptChangeBasePower
/// <inheritdoc />
public void ChangeBasePower(IExecutingMove move, IPokemon target, byte hit, ref ushort basePower)
{
var remainingHealth = move.User.CurrentHealth / move.User.BoostedStats.Hp;
var remainingHealth = move.User.CurrentHealth / (float)move.User.BoostedStats.Hp;
var fraction = remainingHealth * 48;
basePower = fraction switch
{

View File

@@ -6,7 +6,7 @@ public class FlameBurst : Script, IScriptOnSecondaryEffect
/// <inheritdoc />
public void OnSecondaryEffect(IExecutingMove move, IPokemon target, byte hit)
{
var adjacentFoes = GetAdjacentFoes(move.User).WhereNotNull();
var adjacentFoes = GetAdjacentFoes(target).WhereNotNull();
EventBatchId batchId = new();
foreach (var adjacentFoe in adjacentFoes)
{

View File

@@ -11,18 +11,18 @@ public class FlareBlitz : Script, IScriptOnSecondaryEffect
return;
var hitData = move.GetHitData(target, hit);
var recoilDamage = hitData.Damage * (1 / 3);
var triggerArgs = new CustomTriggers.ModifyRecoilArgs(move, target, hit, hitData.Damage, recoilDamage);
move.RunScriptHook<IScriptCustomTrigger>(x => x.CustomTrigger(CustomTriggers.ModifyRecoil, triggerArgs));
if (triggerArgs.Prevent)
return;
var recoilDamage = hitData.Damage * (1 / 3f);
if (battleData.Battle.Random.EffectChance(10, move, target, hit))
{
target.SetStatus("burned", move.User);
}
move.User.Damage(recoilDamage, DamageSource.Misc);
var triggerArgs = new CustomTriggers.ModifyRecoilArgs(move, target, hit, hitData.Damage, (uint)recoilDamage);
move.RunScriptHook<IScriptCustomTrigger>(x => x.CustomTrigger(CustomTriggers.ModifyRecoil, triggerArgs));
if (triggerArgs.Prevent)
return;
move.User.Damage((uint)recoilDamage, DamageSource.Misc);
}
}

View File

@@ -19,7 +19,7 @@ public class Fling : Script, IScriptOnSecondaryEffect, IScriptChangeBasePower
move.GetHitData(target, hit).Fail();
return;
}
if (!item.TryGetAdditionalData<byte>("fling_power", out var flingPower))
if (!item.TryGetAdditionalData<byte>("flingPower", out var flingPower))
{
move.GetHitData(target, hit).Fail();
return;

View File

@@ -1,3 +1,5 @@
using PkmnLib.Plugin.Gen7.Scripts.Pokemon;
namespace PkmnLib.Plugin.Gen7.Scripts.Moves;
[Script(ScriptCategory.Move, "flower_shield")]
@@ -14,6 +16,7 @@ public class FlowerShield : Script, IScriptOnSecondaryEffect
var batchId = new EventBatchId();
var sides = battleData.Battle.Sides;
var anyFound = false;
foreach (var side in sides)
{
foreach (var pokemon in side.Pokemon)
@@ -22,8 +25,13 @@ public class FlowerShield : Script, IScriptOnSecondaryEffect
continue;
if (!pokemon.Types.Contains(grassType))
continue;
if (pokemon.Volatile.Any(x => x.Script?.IsSemiInvulnerableTurn == true))
continue;
pokemon.ChangeStatBoost(Statistic.Defense, 1, pokemon == move.User, false, batchId);
anyFound = true;
}
}
if (!anyFound)
move.GetHitData(target, hit).Fail();
}
}

View File

@@ -6,6 +6,11 @@ public class FocusEnergy : Script, IScriptOnSecondaryEffect
/// <inheritdoc />
public void OnSecondaryEffect(IExecutingMove move, IPokemon target, byte hit)
{
target.Volatile.Add(new Pokemon.IncreasedCriticalStage());
if (target.Volatile.Contains<Pokemon.FocusEnergyEffect>())
{
move.GetHitData(target, hit).Fail();
return;
}
target.Volatile.Add(new Pokemon.FocusEnergyEffect());
}
}

View File

@@ -1,20 +1,13 @@
using PkmnLib.Plugin.Gen7.Scripts.Pokemon;
namespace PkmnLib.Plugin.Gen7.Scripts.Moves;
[Script(ScriptCategory.Move, "follow_me")]
public class FollowMe : Script, IScriptChangeTargets
public class FollowMe : Script, IScriptOnSecondaryEffect
{
/// <inheritdoc />
public void ChangeTargets(IMoveChoice moveChoice, ref IReadOnlyList<IPokemon?> targets)
public void OnSecondaryEffect(IExecutingMove move, IPokemon target, byte hit)
{
if (targets.Count != 1)
return;
var target = targets[0];
if (target == null)
return;
if (target.BattleData?.SideIndex != moveChoice.User.BattleData?.SideIndex)
return;
targets = [moveChoice.User];
move.User.BattleData!.BattleSide.VolatileScripts.StackOrAdd(ScriptUtils.ResolveName<FollowMeEffect>(),
() => new FollowMeEffect(move.User));
}
}

View File

@@ -11,8 +11,15 @@ public class Foresight : Script, IScriptOnSecondaryEffect
var battleData = target.BattleData;
if (battleData == null)
return;
if (target.StatBoost.Evasion > 0)
target.ChangeStatBoost(Statistic.Evasion, (sbyte)-target.StatBoost.Evasion, false, false);
if (target.Volatile.Contains<ForesightEffect>())
{
move.GetHitData(target, hit).Fail();
return;
}
var typeLibrary = battleData.Battle.Library.StaticLibrary.Types;
target.Volatile.Add(new ForesightEffect(typeLibrary));
target.ChangeStatBoost(Statistic.Evasion, (sbyte)-target.StatBoost.Evasion, false, false);
}
}

View File

@@ -1,3 +1,5 @@
using PkmnLib.Plugin.Gen7.Scripts.Pokemon;
namespace PkmnLib.Plugin.Gen7.Scripts.Moves;
[Script(ScriptCategory.Move, "forests_curse")]
@@ -10,8 +12,18 @@ public class ForestsCurse : Script, IScriptOnSecondaryEffect
if (battleData == null)
return;
var typeLibrary = battleData.Battle.Library.StaticLibrary.Types;
if (!typeLibrary.TryGetTypeIdentifier("grass", out var grassType))
if (!typeLibrary.TryGetTypeIdentifier("grass", out var grassType) || target.Types.Contains(grassType))
{
move.GetHitData(target, hit).Fail();
return;
}
if (target.Volatile.TryGet<HasHadTypeAddedEffect>(out var effect))
{
target.RemoveType(effect.TypeIdentifier);
target.Volatile.Remove<HasHadTypeAddedEffect>();
}
target.AddType(grassType);
target.Volatile.Add(new HasHadTypeAddedEffect(grassType));
}
}

View File

@@ -14,10 +14,11 @@ public class FreezeDry : Script, IScriptChangeEffectiveness, IScriptOnSecondaryE
if (!typeLibrary.TryGetTypeIdentifier("water", out var waterType))
return;
if (target.Types.Contains(waterType))
var hitDataType = move.GetHitData(target, hit).Type;
if (target.Types.Contains(waterType) && hitDataType.HasValue)
{
var effectivenessWithoutWater = target.Types.Where(x => x != waterType)
.Select(x => typeLibrary.GetEffectiveness(x, target.Types)).Aggregate(1f, (a, b) => a * b);
.Select(x => typeLibrary.GetSingleEffectiveness(hitDataType.Value, x)).Aggregate(1f, (a, b) => a * b);
effectiveness = effectivenessWithoutWater * 2;
}
}

View File

@@ -15,7 +15,7 @@ public class FuryCutter : Script, IScriptChangeBasePower
return;
}
if (userEffect.TurnCount < 5)
if (userEffect.TurnCount < 2)
userEffect.TurnCount++;
basePower = basePower.MultiplyOrMax((byte)Math.Pow(2, userEffect.TurnCount));
}

View File

@@ -12,11 +12,11 @@ public class FusionBolt : Script, IScriptChangeDamageModifier
// Grab the choices for the current turn, that have been executed before this move.
var choice = battleData.Battle.PreviousTurnChoices.Last().TakeWhile(x => !Equals(x, move.MoveChoice))
// Of these, find the move choice that used Fusion Flare.
.OfType<MoveChoice>().FirstOrDefault(x => x.ChosenMove.MoveData.Name == "fusion_flare");
// Of these, find the last successfully executed move. Failed moves don't break the combination.
.OfType<MoveChoice>().LastOrDefault(x => !x.HasFailed);
// If Fusion Flare was used, Fusion Bolt's power is doubled.
if (choice != null)
// If Fusion Flare was used immediately preceding, Fusion Bolt's power is doubled.
if (choice is not null && choice.ChosenMove.MoveData.Name == "fusion_flare")
{
modifier *= 2;
}

View File

@@ -12,11 +12,11 @@ public class FusionFlare : Script, IScriptChangeDamageModifier
// Grab the choices for the current turn, that have been executed before this move.
var choice = battleData.Battle.PreviousTurnChoices.Last().TakeWhile(x => !Equals(x, move.MoveChoice))
// Of these, find the move choice that used Fusion Bolt.
.OfType<MoveChoice>().FirstOrDefault(x => x.ChosenMove.MoveData.Name == "fusion_bolt");
// Of these, find the last successfully executed move. Failed moves don't break the combination.
.OfType<MoveChoice>().LastOrDefault(x => !x.HasFailed);
// If Fusion Bolt was used, Fusion Flare's power is doubled.
if (choice != null)
// If Fusion Bolt was used immediately preceding, Fusion Flare's power is doubled.
if (choice is not null && choice.ChosenMove.MoveData.Name == "fusion_bolt")
{
modifier *= 2;
}

View File

@@ -1,4 +1,5 @@
using PkmnLib.Plugin.Gen7.Scripts.Battle;
using PkmnLib.Plugin.Gen7.Scripts.Side;
namespace PkmnLib.Plugin.Gen7.Scripts.Moves;
@@ -11,8 +12,28 @@ public class FutureSight : Script, IScriptStopBeforeMove
var battleData = move.User.BattleData;
if (battleData == null)
return;
battleData.Battle.Volatile.Add(new FutureSightEffect(move.MoveChoice));
var battle = battleData.Battle;
var moveChoice = move.MoveChoice;
prevent = true;
if (IsTargetAlreadySetToBeHit(battle, moveChoice.TargetSide, moveChoice.TargetPosition))
{
moveChoice.Fail();
return;
}
if (battle.Volatile.TryGet<FutureSightEffect>(out var effect))
effect.AddStrike(moveChoice);
else
battle.Volatile.Add(new FutureSightEffect(moveChoice));
}
private static bool IsTargetAlreadySetToBeHit(IBattle battle, byte side, byte position)
{
if (battle.Volatile.TryGet<FutureSightEffect>(out var futureSight) && futureSight.HasStrikeAt(side, position))
return true;
var doomDesire = battle.Sides[side].VolatileScripts.Get<DoomDesireEffect>();
return doomDesire != null && doomDesire.HasTarget(position);
}
}

View File

@@ -23,17 +23,15 @@ public class Gravity : Script, IScriptOnSecondaryEffect
var flyEffect = ScriptUtils.ResolveName<ChargeFlyEffect>();
var skyDropEffect = ScriptUtils.ResolveName<ChargeSkyDropEffect>();
var telekinesisEffect = ScriptUtils.ResolveName<TelekinesisEffect>();
var magnetRiseEffect = ScriptUtils.ResolveName<MagnetRiseEffect>();
foreach (var pokemon in battleData.Battle.Sides.SelectMany(x => x.Pokemon).WhereNotNull())
{
if (pokemon.Volatile.Contains(chargeBounceEffect))
pokemon.Volatile.Remove(chargeBounceEffect);
if (pokemon.Volatile.Contains(flyEffect))
pokemon.Volatile.Remove(flyEffect);
if (pokemon.Volatile.Contains(skyDropEffect))
pokemon.Volatile.Remove(skyDropEffect);
if (pokemon.Volatile.Contains(telekinesisEffect))
pokemon.Volatile.Remove(telekinesisEffect);
pokemon.Volatile.Remove(chargeBounceEffect);
pokemon.Volatile.Remove(flyEffect);
pokemon.Volatile.Remove(skyDropEffect);
pokemon.Volatile.Remove(telekinesisEffect);
pokemon.Volatile.Remove(magnetRiseEffect);
}
}
}

View File

@@ -6,7 +6,6 @@ public class GuardianOfAlola : Script, IScriptChangeMoveDamage
/// <inheritdoc />
public void ChangeMoveDamage(IExecutingMove move, IPokemon target, byte hit, ref uint damage)
{
var maxHp = target.BoostedStats.Hp;
damage = (uint)(maxHp * (3f / 4f));
damage = (uint)Math.Max(target.CurrentHealth * (3f / 4f), 1);
}
}

View File

@@ -6,6 +6,11 @@ public class GyroBall : Script, IScriptChangeBasePower
/// <inheritdoc />
public void ChangeBasePower(IExecutingMove move, IPokemon target, byte hit, ref ushort basePower)
{
basePower = Math.Min((byte)150, (byte)(25 * target.BoostedStats.Speed / move.User.BoostedStats.Speed + 1));
if (move.User.BoostedStats.Speed == 0)
{
basePower = 1;
return;
}
basePower = (ushort)Math.Min(150, 25 * target.BoostedStats.Speed / move.User.BoostedStats.Speed + 1);
}
}

View File

@@ -14,10 +14,9 @@ public class HealBell : Script, IScriptOnSecondaryEffect
foreach (var pokemon in party.Party.WhereNotNull())
{
if (pokemon.BattleData?.IsOnBattlefield == true && pokemon.ActiveAbility?.Name == "soundproof")
continue;
pokemon.ClearStatus();
var confusion = ScriptUtils.ResolveName<Confusion>();
if (pokemon.Volatile.Contains(confusion))
pokemon.Volatile.Remove(confusion);
}
}
}

View File

@@ -12,6 +12,14 @@ public class HealingWish : Script, IScriptOnSecondaryEffect
if (battleData == null)
return;
var responsibleIndex = new ResponsibleIndex(battleData.SideIndex, battleData.Position);
var party = battleData.Battle.Parties.FirstOrDefault(x => x.IsResponsibleForIndex(responsibleIndex));
if (party == null || !party.HasUsablePokemonNotInField())
{
move.GetHitData(target, hit).Fail();
return;
}
var side = battleData.Battle.Sides[battleData.SideIndex];
side.VolatileScripts.Add(new HealingWishEffect(battleData.Position));

View File

@@ -16,8 +16,8 @@ public class HeartSwap : Script, IScriptOnSecondaryEffect
var targetStat = targetStats.GetStatistic(stat);
if (userStat == targetStat)
continue;
move.User.ChangeStatBoost(stat, (sbyte)(userStat - targetStat), true, true, eventBatchId);
target.ChangeStatBoost(stat, (sbyte)(targetStat - userStat), false, true, eventBatchId);
move.User.ChangeStatBoost(stat, (sbyte)(targetStat - userStat), true, true, eventBatchId);
target.ChangeStatBoost(stat, (sbyte)(userStat - targetStat), false, true, eventBatchId);
}
}
}

View File

@@ -9,10 +9,10 @@ public class HeatCrash : Script, IScriptChangeBasePower
var weightMultiplier = move.User.WeightInKg / target.WeightInKg;
basePower = weightMultiplier switch
{
> 5 => 120,
> 4 => 100,
> 3 => 80,
> 2 => 60,
>= 5 => 120,
>= 4 => 100,
>= 3 => 80,
>= 2 => 60,
_ => 40,
};
}

View File

@@ -6,7 +6,7 @@ public class Hex : Script, IScriptChangeBasePower
/// <inheritdoc />
public void ChangeBasePower(IExecutingMove move, IPokemon target, byte hit, ref ushort basePower)
{
if (!target.StatusScript.IsEmpty)
if (!target.StatusScript.IsEmpty || target.ActiveAbility?.Name == "comatose")
{
basePower = basePower.MultiplyOrMax(2);
}

View File

@@ -1,7 +1,7 @@
namespace PkmnLib.Plugin.Gen7.Scripts.Moves;
[Script(ScriptCategory.Move, "hidden_power")]
public class HiddenPower : Script, IScriptChangeMoveType, IScriptChangeBasePower
public class HiddenPower : Script, IScriptChangeMoveType
{
/// <inheritdoc />
public void ChangeMoveType(IExecutingMove move, IPokemon target, byte hit, ref TypeIdentifier? moveType)
@@ -14,16 +14,6 @@ public class HiddenPower : Script, IScriptChangeMoveType, IScriptChangeBasePower
moveType = t;
}
/// <inheritdoc />
public void ChangeBasePower(IExecutingMove move, IPokemon target, byte hit, ref ushort basePower)
{
var ivs = move.User.IndividualValues;
var power = GetHiddenPowerValue(ivs, 0x00000002) * 40 / 63 + 30;
// cast to byte with overflow check
basePower = (byte)Math.Min(power, byte.MaxValue);
}
/// <summary>
/// Helper method to calculate the hidden power value from the IVs.
/// This is used to determine the type and power of the move.

View File

@@ -1,16 +1,23 @@
namespace PkmnLib.Plugin.Gen7.Scripts.Moves;
[Script(ScriptCategory.Move, "high_jump_kick")]
public class HighJumpKick : Script, IScriptOnMoveMiss
public class HighJumpKick : Script, IScriptOnAfterHits
{
/// <inheritdoc />
public void OnMoveMiss(IExecutingMove move, IPokemon target)
public void OnAfterHits(IExecutingMove move, IPokemon target)
{
var damage = move.Hits.Where(h => h.HasExecuted).Sum(h => h.Damage);
if (damage == 0)
{
DoRecoil(move);
}
}
private static void DoRecoil(IExecutingMove move)
{
var damage = move.GetHitData(target, 0).Damage;
var recoil = damage / 2;
// This recoil damage will not exceed half the user's max HP
var maxHp = move.User.BoostedStats.Hp;
recoil = Math.Min(recoil, maxHp / 2);
var recoil = maxHp / 2;
if (recoil == 0)
return;
move.User.Damage(recoil, DamageSource.Misc);
}
}

View File

@@ -1,9 +1,10 @@
using PkmnLib.Plugin.Gen7.Scripts.Pokemon;
using PkmnLib.Plugin.Gen7.Scripts.Side;
namespace PkmnLib.Plugin.Gen7.Scripts.Moves;
[Script(ScriptCategory.Move, "hyperspace_fury")]
public class HyperspaceFury : Script, IScriptOnSecondaryEffect
public class HyperspaceFury : Script, IScriptFailMove, IScriptOnSecondaryEffect
{
/// <inheritdoc />
public void OnSecondaryEffect(IExecutingMove move, IPokemon target, byte hit)
@@ -13,5 +14,21 @@ public class HyperspaceFury : Script, IScriptOnSecondaryEffect
{
target.Volatile.Remove(protectionScript.Name);
}
target.BattleData?.BattleSide.VolatileScripts.Remove(ScriptUtils.ResolveName<CraftyShieldEffect>());
target.BattleData?.BattleSide.VolatileScripts.Remove(ScriptUtils.ResolveName<MatBlockEffect>());
target.BattleData?.BattleSide.VolatileScripts.Remove(ScriptUtils.ResolveName<QuickGuardEffect>());
target.BattleData?.BattleSide.VolatileScripts.Remove(ScriptUtils.ResolveName<WideGuardEffect>());
move.User.ChangeStatBoost(Statistic.Defense, -1, true, false);
}
private static StringKey HoopaName = "hoopa";
private static StringKey UnboundName = "unbound";
public void FailMove(IExecutingMove move, ref bool fail)
{
if (move.User.Species.Name != HoopaName || move.User.Form.Name != UnboundName)
{
fail = true;
}
}
}

View File

@@ -24,9 +24,5 @@ public class IceBall : Script, IScriptOnSecondaryEffect, IScriptChangeBasePower
userEffect = new IceBallEffect(move.User, move.UseMove.Name);
move.User.Volatile.Add(userEffect);
}
else
{
userEffect.TurnCount++;
}
}
}

View File

@@ -8,17 +8,23 @@ public class IceFang : Script, IScriptOnSecondaryEffect
/// <inheritdoc />
public void OnSecondaryEffect(IExecutingMove move, IPokemon target, byte hit)
{
var battleData = move.User.BattleData;
var battleData = target.BattleData;
if (battleData == null)
return;
if (battleData.Battle.Random.EffectChance(10, move, target, hit))
var random = battleData.Battle.Random;
if (random.EffectChance(10, move, target, hit))
{
target.SetStatus("frozen", move.User);
}
if (battleData.Battle.Random.EffectChance(10, move, target, hit))
// It also has an independent 10% chance of causing the target to flinch, if the user attacks before the target.
var choiceQueue = battleData.Battle.ChoiceQueue;
if (choiceQueue?.FirstOrDefault(x => x.User == target) != null)
{
target.Volatile.Add(new FlinchEffect());
if (random.EffectChance(10, move, target, hit))
{
target.Volatile.Add(new FlinchEffect());
}
}
}
}

View File

@@ -8,6 +8,6 @@ public class Imprison : Script, IScriptOnSecondaryEffect
/// <inheritdoc />
public void OnSecondaryEffect(IExecutingMove move, IPokemon target, byte hit)
{
target.Volatile.Add(new ImprisonEffect(move.User));
target.BattleData?.Battle.Volatile.Add(new ImprisonEffect(move.User));
}
}

View File

@@ -6,15 +6,18 @@ public class Incinerate : Script, IScriptOnSecondaryEffect
/// <inheritdoc />
public void OnSecondaryEffect(IExecutingMove move, IPokemon target, byte hit)
{
if (target.HeldItem is not { Category: ItemCategory.Berry } || !target.TryStealHeldItem(out _))
var isValidItem = target.HeldItem is { Category: ItemCategory.Berry } ||
(target.HeldItem?.Name.ToString().EndsWith("_gem") == true && target.HeldItem is
{ Category: ItemCategory.MiscItem });
if (!isValidItem || !target.TryStealHeldItem(out _))
{
move.GetHitData(target, hit).Fail();
return;
}
move.Battle.EventHook.Invoke(new DialogEvent("item_incinerated", new Dictionary<string, object>
{
{ "pokemon", target },
{ "item", target.HeldItem },
{ "item", target.HeldItem! },
}));
}
}

View File

@@ -24,6 +24,16 @@ public class Infestation : Script, IScriptOnSecondaryEffect
{
turns = 5;
}
target.Volatile.Add(new InfestationEffect(target, turns));
var args = new CustomTriggers.ModifyBindArgs(move)
{
Duration = turns,
DamagePercent = 1 / 8f,
};
move.User.RunScriptHook<IScriptCustomTrigger>(x => x.CustomTrigger(CustomTriggers.ModifyBind, args));
var bindTurns = args.Duration;
var bindDamage = args.DamagePercent;
target.Volatile.Add(new InfestationEffect(target, bindTurns, bindDamage));
}
}

View File

@@ -1,4 +1,5 @@
using PkmnLib.Dynamic.BattleFlow;
using PkmnLib.Plugin.Gen7.Scripts.Utils;
namespace PkmnLib.Plugin.Gen7.Scripts.Moves;
@@ -16,7 +17,8 @@ public class Instruct : Script, IScriptOnSecondaryEffect
var lastMoveChoiceByTarget = target.BattleData?.LastMoveChoice;
if (lastMoveChoiceByTarget == null || !battleData.Battle.CanUse(lastMoveChoiceByTarget))
if (lastMoveChoiceByTarget == null || !battleData.Battle.CanUse(lastMoveChoiceByTarget) ||
!lastMoveChoiceByTarget.ChosenMove.MoveData.CanCopyMove())
{
move.GetHitData(target, hit).Fail();
return;

View File

@@ -1,3 +1,5 @@
using PkmnLib.Plugin.Gen7.Scripts.Battle;
namespace PkmnLib.Plugin.Gen7.Scripts.Moves;
[Script(ScriptCategory.Move, "judgement")]
@@ -6,6 +8,12 @@ public class Judgement : Script, IScriptChangeMoveType
/// <inheritdoc />
public void ChangeMoveType(IExecutingMove move, IPokemon target, byte hit, ref TypeIdentifier? moveType)
{
//If Magic Room is in effect, Judgment's type will always be Normal regardless of the Plate held
if (move.Battle.Volatile.Contains<MagicRoomEffect>())
{
return;
}
var heldItem = move.User.HeldItem;
if (heldItem == null)
return;

View File

@@ -3,10 +3,16 @@ namespace PkmnLib.Plugin.Gen7.Scripts.Moves;
[Script(ScriptCategory.Move, "kings_shield")]
public class KingsShield : ProtectionScript
{
protected override Script GetEffectScript() => new Pokemon.KingsShieldEffect();
/// <inheritdoc />
public override void OnSecondaryEffect(IExecutingMove move, IPokemon target, byte hit)
{
base.OnSecondaryEffect(move, target, hit);
// Only change Aegislash form if the move has not failed
if (move.GetHitData(target, hit).HasFailed)
return;
// Default form is shield form
if (move.User.Species.Name == "aegislash" && move.User.Form.Name != "default")
{

View File

@@ -1,14 +1,20 @@
namespace PkmnLib.Plugin.Gen7.Scripts.Moves;
[Script(ScriptCategory.Move, "knock_off")]
public class KnockOff : Script, IScriptOnSecondaryEffect
public class KnockOff : Script, IScriptOnSecondaryEffect, IScriptChangeBasePower
{
/// <inheritdoc />
public void OnSecondaryEffect(IExecutingMove move, IPokemon target, byte hit)
{
if (!target.TryStealHeldItem(out _))
{
move.GetHitData(target, hit).Fail();
}
if (move.User.IsFainted)
return;
target.TryStealHeldItem(out _);
}
public void ChangeBasePower(IExecutingMove move, IPokemon target, byte hit, ref ushort basePower)
{
if (target.HeldItem == null || target.HeldItem.Category == ItemCategory.FormChanger)
return;
basePower = basePower.MultiplyOrMax(1.5f);
}
}

View File

@@ -20,7 +20,7 @@ public class LastResort : Script, IScriptPreventMoveSelection
}
// Grab all move choices
var movesForUserSinceEnteringField = battleData.Battle.PreviousTurnChoices
var movesForUserSinceEnteringField = battleData.Battle.PreviousTurnChoices.Skip((int)battleData.SwitchInTurn)
// Reading backwards
.Reverse().SelectMany(x => x.Reverse())
// We only care about move choices since the user entered the field
@@ -29,7 +29,7 @@ public class LastResort : Script, IScriptPreventMoveSelection
.Where(x => x.User == choice.User)
// Grab the chosen move, and remove duplicates
.Select(x => x.ChosenMove).Distinct().ToList();
if (!userMoves.All(x => movesForUserSinceEnteringField.Contains(x)))
if (!userMoves.All(movesForUserSinceEnteringField.Contains))
{
prevent = true;
}

View File

@@ -8,6 +8,6 @@ public class LockOn : Script, IScriptOnSecondaryEffect
/// <inheritdoc />
public void OnSecondaryEffect(IExecutingMove move, IPokemon target, byte hit)
{
target.Volatile.Add(new LockOnEffect(target));
target.Volatile.Add(new LockOnEffect(move.User));
}
}

View File

@@ -1,4 +1,5 @@
using PkmnLib.Plugin.Gen7.Scripts.Pokemon;
using PkmnLib.Plugin.Gen7.Scripts.Side;
namespace PkmnLib.Plugin.Gen7.Scripts.Moves;
@@ -8,6 +9,6 @@ public class LuckyChant : Script, IScriptOnSecondaryEffect
/// <inheritdoc />
public void OnSecondaryEffect(IExecutingMove move, IPokemon target, byte hit)
{
target.Volatile.Add(new LuckyChantEffect());
target.BattleData?.BattleSide.VolatileScripts.Add(new LuckyChantEffect());
}
}

View File

@@ -9,6 +9,9 @@ public class LunarDance : Script, IScriptOnSecondaryEffect
public void OnSecondaryEffect(IExecutingMove move, IPokemon target, byte hit)
{
var battleData = move.User.BattleData;
battleData?.BattleSide.VolatileScripts.Add(new LunarDanceEffect(battleData.Position));
if (battleData is null)
return;
battleData.BattleSide.VolatileScripts.Add(new LunarDanceEffect(battleData.Position));
move.User.Faint(DamageSource.Misc);
}
}

View File

@@ -13,6 +13,13 @@ public class MagicRoom : Script, IScriptOnSecondaryEffect
return;
var battle = battleData.Battle;
battle.Volatile.Add(new MagicRoomEffect());
if (battle.Volatile.Contains<MagicRoomEffect>())
{
battle.Volatile.Remove<MagicRoomEffect>();
}
else
{
battle.Volatile.Add(new MagicRoomEffect());
}
}
}

View File

@@ -3,11 +3,17 @@ using PkmnLib.Plugin.Gen7.Scripts.Pokemon;
namespace PkmnLib.Plugin.Gen7.Scripts.Moves;
[Script(ScriptCategory.Move, "magma_storm")]
public class MagmaStorm : MultiHitMove, IScriptOnSecondaryEffect
public class MagmaStorm : Script, IScriptOnSecondaryEffect
{
/// <inheritdoc />
public void OnSecondaryEffect(IExecutingMove move, IPokemon target, byte hit)
{
target.Volatile.StackOrAdd("magma_storm", () => new MagmaStormEffect(target));
var args = new CustomTriggers.ModifyBindArgs(move);
move.User.RunScriptHook<IScriptCustomTrigger>(x => x.CustomTrigger(CustomTriggers.ModifyBind, args));
var bindTurns = args.Duration;
var bindDamage = args.DamagePercent;
target.Volatile.StackOrAdd("magma_storm", () => new MagmaStormEffect(target, bindTurns, bindDamage));
}
}

View File

@@ -9,7 +9,7 @@ public class MagnetRise : Script, IScriptOnSecondaryEffect
public void OnSecondaryEffect(IExecutingMove move, IPokemon target, byte hit)
{
if (move.User.Volatile.Contains(ScriptUtils.ResolveName<IngrainEffect>()) ||
move.User.ActiveAbility?.Name == "levitate")
move.User.ActiveAbility?.Name == "levitate" || move.User.Volatile.Contains<MagnetRiseEffect>())
{
move.GetHitData(target, hit).Fail();
return;

View File

@@ -1,4 +1,6 @@
using PkmnLib.Plugin.Gen7.Scripts.MoveVolatile;
using PkmnLib.Plugin.Gen7.Scripts.Utils;
using PkmnLib.Static.Moves;
namespace PkmnLib.Plugin.Gen7.Scripts.Moves;
@@ -18,12 +20,16 @@ public class MeFirst : Script, IScriptChangeMove
choice.Fail();
return;
}
if (battleData.Battle.ChoiceQueue?.FirstOrDefault(x => x.User == target) is not IMoveChoice targetMove)
if (battleData.Battle.ChoiceQueue?.FirstOrDefault(x => x.User == target) is not IMoveChoice
{
ChosenMove.MoveData.Category: MoveCategory.Physical or MoveCategory.Special,
} targetMove)
{
choice.Fail();
return;
}
if (battleData.Battle.Library.MiscLibrary.IsReplacementChoice(targetMove))
if (!targetMove.ChosenMove.MoveData.CanCopyMove() || targetMove.ChosenMove.MoveData.Name == "metal_burst" ||
battleData.Battle.Library.MiscLibrary.IsReplacementChoice(targetMove))
{
choice.Fail();
return;

View File

@@ -5,9 +5,13 @@ namespace PkmnLib.Plugin.Gen7.Scripts.Moves;
[Script(ScriptCategory.Move, "mean_look")]
public class MeanLook : Script, IScriptOnSecondaryEffect
{
private static StringKey GhostTypeName => new("ghost");
/// <inheritdoc />
public void OnSecondaryEffect(IExecutingMove move, IPokemon target, byte hit)
{
if (target.Types.Any(x => x.Name == GhostTypeName))
return;
var targetEffect = target.Volatile.Add(new MeanLookEffectTarget());
if (targetEffect == null)
{

View File

@@ -3,7 +3,7 @@ using PkmnLib.Static.Moves;
namespace PkmnLib.Plugin.Gen7.Scripts.Moves;
[Script(ScriptCategory.Move, "metal_burst")]
public class MetalBurst : Script, IScriptOnBeforeTurnStart, IScriptChangeMoveDamage
public class MetalBurst : Script, IScriptOnBeforeTurnStart, IScriptChangeTargets, IScriptChangeMoveDamage
{
/// <inheritdoc />
public void OnBeforeTurnStart(ITurnChoice choice)
@@ -11,19 +11,31 @@ public class MetalBurst : Script, IScriptOnBeforeTurnStart, IScriptChangeMoveDam
choice.User.Volatile.Add(new MetalBurstHelper());
}
/// <inheritdoc />
public void ChangeTargets(IMoveChoice moveChoice, ref IReadOnlyList<IPokemon?> targets)
{
var helper = moveChoice.User.Volatile.Get<MetalBurstHelper>();
var lastAttacker = helper?.LastAttacker;
if (lastAttacker == null)
{
moveChoice.Fail();
return;
}
targets = [lastAttacker];
}
/// <inheritdoc />
public void ChangeMoveDamage(IExecutingMove move, IPokemon target, byte hit, ref uint damage)
{
var helper = target.Volatile.Get<MetalBurstHelper>();
var helper = move.User.Volatile.Get<MetalBurstHelper>();
if (helper?.LastAttacker == null || helper.LastAttacker != move.User)
if (helper?.LastAttacker == null || helper.LastAttacker != target)
{
move.GetHitData(target, hit).Fail();
return;
}
damage = helper.LastDamage.MultiplyOrMax(1.5f);
target.Volatile.Remove<MetalBurstHelper>();
}
[Script(ScriptCategory.Pokemon, "metal_burst_helper")]

View File

@@ -18,13 +18,13 @@ public class Metronome : Script, IScriptChangeMove
var retryCount = 0;
while (true)
{
// Breaker for infinite loop. The odds that we'd accidentally roll a non-copyable move 100 times in a row
// is extremely unlikely, unless the loaded move library only contains non-copyable moves.
// Breaker for infinite loop. The odds that we'd accidentally roll an unselectable move 100 times in a row
// is extremely unlikely, unless the loaded move library only contains unselectable moves.
// This is a failsafe.
if (retryCount > 100)
throw new Exception("Metronome failed to find a valid move after 100 attempts.");
var randomMove = moveLibrary.GetRandom(battleData.Battle.Random);
if (!randomMove.CanCopyMove())
if (!randomMove.CanBeSelectedByMetronome())
{
retryCount++;
continue;

View File

@@ -8,6 +8,12 @@ public class MiracleEye : Script, IScriptOnSecondaryEffect
/// <inheritdoc />
public void OnSecondaryEffect(IExecutingMove move, IPokemon target, byte hit)
{
if (target.Volatile.Contains<MiracleEyeEffect>())
{
move.GetHitData(target, hit).Fail();
return;
}
if (target.StatBoost.Evasion > 0)
{
target.ChangeStatBoost(Statistic.Evasion, (sbyte)-target.StatBoost.Evasion, false, false);

View File

@@ -3,7 +3,7 @@ using PkmnLib.Static.Moves;
namespace PkmnLib.Plugin.Gen7.Scripts.Moves;
[Script(ScriptCategory.Move, "mirror_coat")]
public class MirrorCoat : Script, IScriptOnBeforeTurnStart, IScriptChangeMoveDamage
public class MirrorCoat : Script, IScriptOnBeforeTurnStart, IScriptChangeTargets, IScriptChangeMoveDamage
{
/// <inheritdoc />
public void OnBeforeTurnStart(ITurnChoice choice)
@@ -11,19 +11,31 @@ public class MirrorCoat : Script, IScriptOnBeforeTurnStart, IScriptChangeMoveDam
choice.User.Volatile.Add(new MirrorCoatHelper());
}
/// <inheritdoc />
public void ChangeTargets(IMoveChoice moveChoice, ref IReadOnlyList<IPokemon?> targets)
{
var helper = moveChoice.User.Volatile.Get<MirrorCoatHelper>();
var lastAttacker = helper?.LastAttacker;
if (lastAttacker == null)
{
moveChoice.Fail();
return;
}
targets = [lastAttacker];
}
/// <inheritdoc />
public void ChangeMoveDamage(IExecutingMove move, IPokemon target, byte hit, ref uint damage)
{
var helper = target.Volatile.Get<MirrorCoatHelper>();
var helper = move.User.Volatile.Get<MirrorCoatHelper>();
if (helper?.LastAttacker == null || helper.LastAttacker != move.User)
if (helper?.LastAttacker == null || helper.LastAttacker != target)
{
move.GetHitData(target, hit).Fail();
return;
}
damage = helper.LastDamage.MultiplyOrMax(2f);
target.Volatile.Remove<MirrorCoatHelper>();
damage = Math.Max(helper.LastDamage.MultiplyOrMax(2f), 1);
}
[Script(ScriptCategory.Pokemon, "mirror_coat_helper")]

View File

@@ -16,10 +16,14 @@ public class MirrorMove : Script, IScriptChangeMove
if (battle.ChoiceQueue == null)
return;
var currentTurn = battle.ChoiceQueue.LastRanChoice;
var target = battle.GetPokemon(choice.TargetSide, choice.TargetPosition);
if (target is null)
{
choice.Fail();
return;
}
var lastMove = battle.PreviousTurnChoices.SelectMany(x => x).OfType<IMoveChoice>()
.TakeWhile(x => !Equals(x, currentTurn)).LastOrDefault(x => x.TargetPosition == choice.TargetPosition &&
x.TargetSide == choice.TargetSide &&
x.User.BattleData?.IsOnBattlefield == true);
.TakeWhile(x => !Equals(x, currentTurn)).LastOrDefault(x => x.User == target);
if (lastMove == null || !lastMove.ChosenMove.MoveData.CanCopyMove())
{
choice.Fail();

View File

@@ -1,3 +1,5 @@
using PkmnLib.Plugin.Gen7.Scripts.Battle;
namespace PkmnLib.Plugin.Gen7.Scripts.Moves;
[Script(ScriptCategory.Move, "multi_attack")]
@@ -6,6 +8,11 @@ public class MultiAttack : Script, IScriptChangeMoveType
/// <inheritdoc />
public void ChangeMoveType(IExecutingMove move, IPokemon target, byte hit, ref TypeIdentifier? moveType)
{
if (move.User.ActiveAbility?.Name == "klutz")
return;
if (move.Battle.Volatile.Contains<MagicRoomEffect>())
return;
var item = move.User.HeldItem?.Name.ToString();
var typeLibrary = move.User.Library.StaticLibrary.Types;

View File

@@ -1,3 +1,5 @@
using PkmnLib.Plugin.Gen7.Scripts.Pokemon;
namespace PkmnLib.Plugin.Gen7.Scripts.Moves;
[Script(ScriptCategory.Move, "trick_or_treat")]
@@ -7,8 +9,18 @@ public class TrickOrTreat : Script, IScriptOnSecondaryEffect
public void OnSecondaryEffect(IExecutingMove move, IPokemon target, byte hit)
{
var library = move.Battle.Library.StaticLibrary.Types;
if (!library.TryGetTypeIdentifier("ghost", out var ghostType))
if (!library.TryGetTypeIdentifier("ghost", out var ghostType) || target.Types.Contains(ghostType))
{
move.GetHitData(target, hit).Fail();
return;
}
if (target.Volatile.TryGet<HasHadTypeAddedEffect>(out var effect))
{
target.RemoveType(effect.TypeIdentifier);
target.Volatile.Remove<HasHadTypeAddedEffect>();
}
target.AddType(ghostType);
target.Volatile.Add(new HasHadTypeAddedEffect(ghostType));
}
}

View File

@@ -8,6 +8,9 @@ public class ChargeBounceEffect : Script, IScriptForceTurnSelection, IScriptChan
{
private readonly IPokemon _owner;
/// <inheritdoc />
public override bool IsSemiInvulnerableTurn => true;
public ChargeBounceEffect(IPokemon owner)
{
_owner = owner;

View File

@@ -8,6 +8,9 @@ public class ChargeFlyEffect : Script, IScriptForceTurnSelection, IScriptChangeI
{
private readonly IPokemon _owner;
/// <inheritdoc />
public override bool IsSemiInvulnerableTurn => true;
public ChargeFlyEffect(IPokemon owner)
{
_owner = owner;
@@ -30,7 +33,7 @@ public class ChargeFlyEffect : Script, IScriptForceTurnSelection, IScriptChangeI
/// <inheritdoc />
public void ChangeIncomingMoveDamage(IExecutingMove move, IPokemon target, byte hit, ref uint damage)
{
if (!move.UseMove.HasFlag(MoveFlags.EffectiveAgainstFly))
if (move.UseMove.HasFlag(MoveFlags.EffectiveAgainstFly))
damage *= 2;
}

View File

@@ -8,6 +8,9 @@ public class ChargeSkyDropEffect : Script, IScriptForceTurnSelection, IScriptCha
{
private readonly IPokemon _owner;
/// <inheritdoc />
public override bool IsSemiInvulnerableTurn => true;
public ChargeSkyDropEffect(IPokemon owner)
{
_owner = owner;

View File

@@ -73,6 +73,9 @@ public class Confusion : Script, IScriptStopBeforeMove
/// <inheritdoc />
public uint Damage { get; } = 0;
/// <inheritdoc />
public bool HasExecuted => true;
/// <inheritdoc />
public TypeIdentifier? Type => null;

View File

@@ -0,0 +1,14 @@
namespace PkmnLib.Plugin.Gen7.Scripts.Pokemon;
[Script(ScriptCategory.Pokemon, "defense_curl")]
public class DefenseCurlEffect : Script, IScriptChangeBasePower
{
private static StringKey RolloutName = "rollout";
private static StringKey IceBallName = "ice_ball";
public void ChangeBasePower(IExecutingMove move, IPokemon target, byte hit, ref ushort basePower)
{
if (move.UseMove.Name == RolloutName || move.UseMove.Name == IceBallName)
basePower = basePower.MultiplyOrMax(2);
}
}

View File

@@ -8,6 +8,9 @@ public class DigEffect : Script, IScriptForceTurnSelection, IScriptChangeIncomin
{
private readonly IPokemon _owner;
/// <inheritdoc />
public override bool IsSemiInvulnerableTurn => true;
public DigEffect(IPokemon owner)
{
_owner = owner;

View File

@@ -8,6 +8,9 @@ public class DiveEffect : Script, IScriptForceTurnSelection, IScriptChangeIncomi
{
private readonly IPokemon _owner;
/// <inheritdoc />
public override bool IsSemiInvulnerableTurn => true;
public DiveEffect(IPokemon owner)
{
_owner = owner;

View File

@@ -5,27 +5,42 @@ public class FireSpinEffect : Script, IScriptOnEndTurn, IScriptPreventSelfRunAwa
IAIInfoScriptExpectedEndOfTurnDamage
{
private readonly IPokemon _owner;
private int _turns;
private readonly float _modifier;
public FireSpinEffect(IPokemon owner)
public FireSpinEffect(IPokemon owner, int turns, IPokemon user)
{
_owner = owner;
_turns = turns;
_modifier = user.HasHeldItem("binding_band") ? 6f : 8f;
}
/// <inheritdoc />
public void OnEndTurn(IScriptSource owner, IBattle battle)
{
_owner.Damage(_owner.BoostedStats.Hp / 8, DamageSource.Misc);
_owner.Damage((uint)(_owner.BoostedStats.Hp / _modifier), DamageSource.Misc);
_turns--;
if (_turns == 0)
RemoveSelf();
}
/// <inheritdoc />
public void PreventSelfRunAway(IFleeChoice choice, ref bool prevent) => prevent = true;
public void PreventSelfRunAway(IFleeChoice choice, ref bool prevent)
{
if (choice.User.Types.All(x => x.Name != "ghost"))
prevent = true;
}
/// <inheritdoc />
public void PreventSelfSwitch(ISwitchChoice choice, ref bool prevent) => prevent = true;
public void PreventSelfSwitch(ISwitchChoice choice, ref bool prevent)
{
if (choice.User.Types.All(x => x.Name != "ghost"))
prevent = true;
}
/// <inheritdoc />
public void ExpectedEndOfTurnDamage(IPokemon pokemon, ref int damage)
{
damage += (int)(pokemon.MaxHealth / 8f);
damage += (int)(pokemon.MaxHealth / _modifier);
}
}

View File

@@ -0,0 +1,15 @@
namespace PkmnLib.Plugin.Gen7.Scripts.Pokemon;
[Script(ScriptCategory.Pokemon, "increased_critical_stage")]
public class FocusEnergyEffect : Script, IScriptChangeCriticalStage
{
public void ChangeCriticalStage(IExecutingMove move, IPokemon target, byte hit, ref byte stage)
{
if (stage == byte.MaxValue)
{
move.GetHitData(target, hit).Fail();
return;
}
stage = (byte)Math.Min(stage + 2, byte.MaxValue);
}
}

View File

@@ -1,3 +1,5 @@
using PkmnLib.Plugin.Gen7.Scripts.Moves;
namespace PkmnLib.Plugin.Gen7.Scripts.Pokemon;
[Script(ScriptCategory.Pokemon, "focus_punch")]
@@ -8,6 +10,8 @@ public class FocusPunchEffect : Script, IScriptOnIncomingHit
/// <inheritdoc />
public void OnIncomingHit(IExecutingMove move, IPokemon target, byte hit)
{
if (move.UseMove.SecondaryEffect?.Name == "one_hit_ko")
return;
WasHit = true;
target.BattleData?.Battle.EventHook.Invoke(new DialogEvent("focus_punch_lost_focus",
new Dictionary<string, object>

View File

@@ -0,0 +1,15 @@
namespace PkmnLib.Plugin.Gen7.Scripts.Pokemon;
/// <summary>
/// Simple marker script to indicate forests curse or trick-or-treat has been used,
/// </summary>
[Script(ScriptCategory.Pokemon, "has_had_type_added_effect")]
public class HasHadTypeAddedEffect : Script
{
public TypeIdentifier TypeIdentifier { get; }
public HasHadTypeAddedEffect(TypeIdentifier typeIdentifier)
{
TypeIdentifier = typeIdentifier;
}
}

View File

@@ -1,12 +1,19 @@
namespace PkmnLib.Plugin.Gen7.Scripts.Pokemon;
[Script(ScriptCategory.Pokemon, "helping_hand")]
public class HelpingHandEffect : Script, IScriptChangeBasePower, IScriptOnEndTurn
public class HelpingHandEffect : Script, IScriptChangeBasePower, IScriptOnEndTurn, IScriptStack
{
private int _stacks = 1;
/// <inheritdoc />
public void ChangeBasePower(IExecutingMove move, IPokemon target, byte hit, ref ushort basePower) =>
basePower = basePower.MultiplyOrMax(1.5f);
basePower = basePower.MultiplyOrMax((float)Math.Pow(1.5f, _stacks));
/// <inheritdoc />
public void OnEndTurn(IScriptSource owner, IBattle battle) => RemoveSelf();
public void Stack()
{
_stacks++;
}
}

View File

@@ -13,6 +13,9 @@ public class ImprisonEffect : Script, IScriptPreventMoveSelection
/// <inheritdoc />
public void PreventMoveSelection(IMoveChoice choice, ref bool prevent)
{
if (choice.User.BattleData?.BattleSide == _user.BattleData?.BattleSide)
return;
if (_user.Moves.WhereNotNull().Any(x => x.MoveData.Name == choice.ChosenMove.MoveData.Name))
prevent = true;
}

View File

@@ -6,11 +6,13 @@ public class InfestationEffect : Script, IScriptOnEndTurn, IScriptPreventSelfSwi
{
private readonly IPokemon _owner;
private int _turns;
private readonly float _bindDamage;
public InfestationEffect(IPokemon owner, int turns)
public InfestationEffect(IPokemon owner, int turns, float bindDamage)
{
_owner = owner;
_turns = turns;
_bindDamage = bindDamage;
}
/// <inheritdoc />
@@ -22,8 +24,8 @@ public class InfestationEffect : Script, IScriptOnEndTurn, IScriptPreventSelfSwi
/// <inheritdoc />
public void OnEndTurn(IScriptSource owner, IBattle battle)
{
var damage = _owner.BoostedStats.Hp / 8;
_owner.Damage(damage, DamageSource.Misc);
var damage = _owner.BoostedStats.Hp * _bindDamage;
_owner.Damage((uint)damage, DamageSource.Misc);
_turns--;
if (_turns <= 0)
@@ -35,6 +37,6 @@ public class InfestationEffect : Script, IScriptOnEndTurn, IScriptPreventSelfSwi
/// <inheritdoc />
public void ExpectedEndOfTurnDamage(IPokemon pokemon, ref int damage)
{
damage += (int)(_owner.MaxHealth / 8f);
damage += (int)(_owner.MaxHealth * _bindDamage);
}
}

View File

@@ -12,10 +12,18 @@ public class IngrainEffect : Script, IScriptFailIncomingMove, IScriptOnEndTurn,
}
/// <inheritdoc />
public void PreventSelfSwitch(ISwitchChoice choice, ref bool prevent) => prevent = true;
public void PreventSelfSwitch(ISwitchChoice choice, ref bool prevent)
{
if (choice.User.Types.All(x => x.Name != "ghost"))
prevent = true;
}
/// <inheritdoc />
public void PreventSelfRunAway(IFleeChoice choice, ref bool prevent) => prevent = true;
public void PreventSelfRunAway(IFleeChoice choice, ref bool prevent)
{
if (choice.User.Types.All(x => x.Name != "ghost"))
prevent = true;
}
/// <inheritdoc />
public void OnEndTurn(IScriptSource owner, IBattle battle)

View File

@@ -3,16 +3,19 @@ using PkmnLib.Static.Moves;
namespace PkmnLib.Plugin.Gen7.Scripts.Pokemon;
[Script(ScriptCategory.Pokemon, "kings_shield")]
public class KingsShield : ProtectionEffectScript
public class KingsShieldEffect : ProtectionEffectScript
{
/// <inheritdoc />
public override void BlockIncomingHit(IExecutingMove executingMove, IPokemon target, byte hitIndex, ref bool block)
{
// Kings Shield doesnt protect from status moves
if (executingMove.UseMove.Category == MoveCategory.Status)
return;
base.BlockIncomingHit(executingMove, target, hitIndex, ref block);
if (executingMove.UseMove.Category != MoveCategory.Status &&
executingMove.GetHitData(target, hitIndex).IsContact)
if (block && executingMove.GetHitData(target, hitIndex).IsContact)
{
executingMove.User.ChangeStatBoost(Statistic.Accuracy, -2, false, false);
executingMove.User.ChangeStatBoost(Statistic.Attack, -2, false, false);
}
}
}

View File

@@ -1,12 +1,21 @@
namespace PkmnLib.Plugin.Gen7.Scripts.Pokemon;
[Script(ScriptCategory.Pokemon, "laser_focus")]
public class LaserFocusEffect : Script, IScriptChangeCriticalStage
public class LaserFocusEffect : Script, IScriptChangeCriticalStage, IScriptOnEndTurn
{
private bool _hasHadEndTurn;
/// <inheritdoc />
public void ChangeCriticalStage(IExecutingMove move, IPokemon target, byte hit, ref byte stage)
{
stage = 100;
RemoveSelf();
}
public void OnEndTurn(IScriptSource owner, IBattle battle)
{
if (_hasHadEndTurn)
RemoveSelf();
else
_hasHadEndTurn = true;
}
}

View File

@@ -1,19 +1,20 @@
namespace PkmnLib.Plugin.Gen7.Scripts.Pokemon;
[Script(ScriptCategory.Pokemon, "lock_on")]
public class LockOnEffect : Script, IScriptOnEndTurn, IScriptChangeAccuracy
public class LockOnEffect : Script, IScriptOnEndTurn, IScriptChangeIncomingAccuracy
{
private readonly IPokemon _placer;
private bool _hasHadEndTurn;
public LockOnEffect(IPokemon placer)
{
_placer = placer;
}
/// <inheritdoc />
public void ChangeAccuracy(IExecutingMove executingMove, IPokemon target, byte hitIndex, ref int modifiedAccuracy)
public void ChangeIncomingAccuracy(IExecutingMove executingMove, IPokemon target, byte hitIndex,
ref int modifiedAccuracy)
{
if (_placer != target)
if (_placer != executingMove.User)
return;
modifiedAccuracy = 255;
}
@@ -21,6 +22,9 @@ public class LockOnEffect : Script, IScriptOnEndTurn, IScriptChangeAccuracy
/// <inheritdoc />
public void OnEndTurn(IScriptSource owner, IBattle battle)
{
RemoveSelf();
if (_hasHadEndTurn)
RemoveSelf();
else
_hasHadEndTurn = true;
}
}

View File

@@ -1,22 +0,0 @@
namespace PkmnLib.Plugin.Gen7.Scripts.Pokemon;
[Script(ScriptCategory.Pokemon, "lucky_chant")]
public class LuckyChantEffect : Script, IScriptBlockCriticalHit, IScriptOnEndTurn
{
private int _turnsLeft = 5;
/// <inheritdoc />
public void BlockCriticalHit(IExecutingMove move, IPokemon target, byte hit, ref bool block)
{
block = true;
}
/// <inheritdoc />
public void OnEndTurn(IScriptSource owner, IBattle battle)
{
_turnsLeft--;
if (_turnsLeft > 0)
return;
RemoveSelf();
}
}

View File

@@ -5,27 +5,45 @@ public class MagmaStormEffect : Script, IScriptOnEndTurn, IScriptPreventSelfRunA
IAIInfoScriptExpectedEndOfTurnDamage
{
private readonly IPokemon _owner;
private int _turns;
private readonly float _percentOfMaxHealth;
public MagmaStormEffect(IPokemon owner)
public MagmaStormEffect(IPokemon owner, int turns, float percentOfMaxHealth)
{
_owner = owner;
_turns = turns;
_percentOfMaxHealth = percentOfMaxHealth;
}
/// <inheritdoc />
public void OnEndTurn(IScriptSource owner, IBattle battle)
{
_owner.Damage(_owner.BoostedStats.Hp / 16, DamageSource.Misc);
if (_turns > 0)
{
_turns--;
_owner.Damage((uint)(_owner.MaxHealth * _percentOfMaxHealth), DamageSource.Misc);
}
if (_turns <= 0)
RemoveSelf();
}
/// <inheritdoc />
public void PreventSelfRunAway(IFleeChoice choice, ref bool prevent) => prevent = true;
public void PreventSelfRunAway(IFleeChoice choice, ref bool prevent)
{
if (choice.User.Types.All(x => x.Name != "ghost"))
prevent = true;
}
/// <inheritdoc />
public void PreventSelfSwitch(ISwitchChoice choice, ref bool prevent) => prevent = true;
public void PreventSelfSwitch(ISwitchChoice choice, ref bool prevent)
{
if (choice.User.Types.All(x => x.Name != "ghost"))
prevent = true;
}
/// <inheritdoc />
public void ExpectedEndOfTurnDamage(IPokemon pokemon, ref int damage)
{
damage += (int)(pokemon.MaxHealth / 16f);
damage += (int)(pokemon.MaxHealth * _percentOfMaxHealth);
}
}

View File

@@ -3,11 +3,14 @@ namespace PkmnLib.Plugin.Gen7.Scripts.Pokemon;
[Script(ScriptCategory.Pokemon, "magnet_rise")]
public class MagnetRiseEffect : Script, IScriptChangeEffectiveness, IScriptOnEndTurn
{
private int _turnsRemaining = 5;
private int _turnsRemaining = 4;
private static readonly StringKey IronBallName = "iron_ball";
/// <inheritdoc />
public void ChangeEffectiveness(IExecutingMove move, IPokemon target, byte hit, ref float effectiveness)
{
if (move.User.HasHeldItem(IronBallName))
return;
if (move.UseMove.MoveType.Name == "ground")
{
effectiveness = 0.0f;

View File

@@ -7,6 +7,9 @@ public class PhantomForceCharge : Script, IScriptForceTurnSelection, IScriptBloc
{
private readonly IPokemon _owner;
/// <inheritdoc />
public override bool IsSemiInvulnerableTurn => true;
public PhantomForceCharge(IPokemon owner)
{
_owner = owner;

View File

@@ -7,6 +7,9 @@ public class ShadowForceCharge : Script, IScriptForceTurnSelection, IScriptBlock
{
private readonly IPokemon _owner;
/// <inheritdoc />
public override bool IsSemiInvulnerableTurn => true;
public ShadowForceCharge(IPokemon owner)
{
_owner = owner;

View File

@@ -0,0 +1,42 @@
namespace PkmnLib.Plugin.Gen7.Scripts.Pokemon;
[Script(ScriptCategory.Side, "follow_me")]
public class FollowMeEffect : Script, IScriptChangeIncomingTargets, IScriptOnEndTurn, IScriptOnFaint, IScriptOnSwitchOut
{
private readonly IPokemon _user;
public FollowMeEffect(IPokemon user)
{
_user = user;
}
/// <inheritdoc />
public void ChangeIncomingTargets(IMoveChoice moveChoice, ref IReadOnlyList<IPokemon?> targets)
{
if (targets.Count != 1)
return;
if (moveChoice.ChosenMove.MoveData.HasFlag(MoveFlags.NoRedirection))
return;
if (moveChoice.User.BattleData?.SideIndex == _user.BattleData?.SideIndex)
return;
targets = [_user];
}
public void OnEndTurn(IScriptSource owner, IBattle battle)
{
RemoveSelf();
}
public void OnFaint(IPokemon pokemon, DamageSource source)
{
if (pokemon == _user)
RemoveSelf();
}
public void OnSwitchOut(IPokemon oldPokemon, byte position)
{
if (oldPokemon == _user)
RemoveSelf();
}
}

View File

@@ -0,0 +1,21 @@
namespace PkmnLib.Plugin.Gen7.Scripts.Side;
[Script(ScriptCategory.Side, "lucky_chant")]
public class LuckyChantEffect : Script, IScriptBlockIncomingCriticalHit, IScriptOnEndTurn
{
private int _turnsLeft = 5;
/// <inheritdoc />
public void OnEndTurn(IScriptSource owner, IBattle battle)
{
_turnsLeft--;
if (_turnsLeft > 0)
return;
RemoveSelf();
}
public void BlockIncomingCriticalHit(IExecutingMove move, IPokemon target, byte hit, ref bool block)
{
block = true;
}
}

View File

@@ -1,13 +1,24 @@
namespace PkmnLib.Plugin.Gen7.Scripts.Side;
[Script(ScriptCategory.Side, "mist")]
public class MistEffect : Script, IScriptPreventStatBoostChange
public class MistEffect : Script, IScriptPreventStatBoostChange, IScriptOnEndTurn
{
private int _turnsRemaining = 5;
/// <inheritdoc />
public void PreventStatBoostChange(IPokemon target, Statistic stat, sbyte amount, bool selfInflicted,
ref bool prevent)
{
if (selfInflicted)
return;
if (amount < 0)
prevent = true;
}
public void OnEndTurn(IScriptSource owner, IBattle battle)
{
_turnsRemaining--;
if (_turnsRemaining == 0)
RemoveSelf();
}
}

View File

@@ -3,7 +3,7 @@ namespace PkmnLib.Plugin.Gen7.Scripts.Side;
[Script(ScriptCategory.Side, "rainbow_effect")]
public class RainbowEffect : Script, IScriptChangeEffectChance, IScriptOnEndTurn
{
private int _turns = 5;
private int _turns = 4;
/// <inheritdoc />
public void ChangeEffectChance(IExecutingMove move, IPokemon target, byte hit, ref float chance)

View File

@@ -1,7 +1,9 @@
using PkmnLib.Plugin.Gen7.Scripts.Pokemon;
namespace PkmnLib.Plugin.Gen7.Scripts.Terrain;
[Script(ScriptCategory.Terrain, "misty_terrain")]
public class MistyTerrain : Script, IScriptChangeBasePower, IScriptPreventStatusChange
public class MistyTerrain : Script, IScriptChangeBasePower, IScriptPreventStatusChange, IScriptPreventVolatileAdd
{
private static bool IsAffectedByTerrain(IPokemon pokemon) =>
!pokemon.IsFloating;
@@ -24,4 +26,10 @@ public class MistyTerrain : Script, IScriptChangeBasePower, IScriptPreventStatus
return;
preventStatus = true;
}
public void PreventVolatileAdd(IScriptSource parent, Script script, ref bool preventVolatileAdd)
{
if (script.Name == ScriptUtils.ResolveName<Confusion>())
preventVolatileAdd = true;
}
}

View File

@@ -38,6 +38,7 @@ public static class CopyableMoves
"follow_me",
"helping_hand",
"hold_hands",
"instruct",
"kings_shield",
"mat_block",
"me_first",
@@ -65,4 +66,66 @@ public static class CopyableMoves
"trick",
"whirlwind",
];
/// <summary>
/// Validates if a move can be selected by Metronome.
/// </summary>
public static bool CanBeSelectedByMetronome(this IMoveData move) =>
!MetronomeUnselectable.Contains(move.Name);
/// <summary>
/// The names of moves that cannot be selected by Metronome. This differs from <see cref="NonCopyable"/>:
/// Metronome additionally cannot select moves such as After You and Wide Guard, while moves that are only
/// excluded for other copying moves (such as semi-invulnerable and forced-switch moves) remain selectable.
/// </summary>
private static readonly HashSet<StringKey> MetronomeUnselectable =
[
"after_you",
"assist",
"baneful_bunker",
"beak_blast",
"belch",
"bestow",
"celebrate",
"chatter",
"copycat",
"counter",
"covet",
"crafty_shield",
"destiny_bond",
"detect",
"endure",
"feint",
"focus_punch",
"follow_me",
"helping_hand",
"hold_hands",
"instruct",
"kings_shield",
"mat_block",
"me_first",
"metronome",
"mimic",
"mirror_coat",
"mirror_move",
"nature_power",
"protect",
"quash",
"quick_guard",
"rage_powder",
"shell_trap",
"sketch",
"sleep_talk",
"snatch",
"snore",
"spectral_thief",
"spiky_shield",
"spotlight",
"struggle",
"switcheroo",
"thief",
"transform",
"trick",
"wide_guard",
];
}