Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Overhaul 'SwitchIn' event for more accurate effect resolution order #10766

Open
wants to merge 23 commits into
base: master
Choose a base branch
from

Conversation

pyuk-bot
Copy link
Contributor

@pyuk-bot pyuk-bot commented Dec 18, 2024

This doesn't include a fix that lets it copy multiple Intrepid Swords, unfortunately. That still needs a larger refactor to how events trigger after simultaneous switches in.

Edit: now it does. Sorry, I got a little carried away.

Copy link
Member

@DaWoblefet DaWoblefet left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some minor notes on the tests.

test/sim/items/mirrorherb.js Outdated Show resolved Hide resolved
test/sim/items/mirrorherb.js Outdated Show resolved Hide resolved
@pyuk-bot pyuk-bot changed the title Mirror Herb: Copy simultaneous boosts from Howl, etc. Overhaul 'SwitchIn' event for more accurate effect resolution order Dec 26, 2024
@pyuk-bot
Copy link
Contributor Author

pyuk-bot commented Dec 26, 2024

Some notes about this refactor: the 'SwitchIn' event now affects every Pokemon on the field at once, similar to the 'Residual' event. Indeed, it now uses the same function, which has been renamed fieldEvent. 'fieldEvent' now accepts a targets parameter, used by the 'SwitchIn' event to indicate which Pokemon switched in; when targets are specified, event handlers will not run for other Pokemon on the field unless they are 'onAny' handlers.

The 'Start' event is no longer run for abilities and items during the 'SwitchIn' event. Instead, abilities and items with an onStart handler but no onSwitchIn handler automatically get the former copied into the latter. As such, many abilities and items now have an onSwitchInPriority set despite not having an explicitly defined onSwitchIn handler.

Handler resolution order has been reworked to automatically sort effects so their handlers activate in the same order they do in-game. This preserves hazard activation timing within the 'SwitchIn' event and simplifies effect order sorting in general. In order to preserve the activation order of multiple hazards on one side, effect state objects now all have an effectOrder property that tracks when the effect became active. This lets us sort older effects before newer ones, similar to how the games sort handlers. To facilitate this, EffectState has been made into a class* to give it a constructor that automatically handles initializing the effectOrder property. This property also replaces abilityOrder, as it accomplishes the same thing. EffectState is still an interface, and its initialization is now handled by the Battle.initEffectState function.

The events 'PreStart' and 'Primal' have been rolled into the 'SwitchIn' event, and their respective handlers have been given higher and lower onSwitchInPriority appropriately. Code that checks if an item is an Orb which causes primal reversion now uses the new isPrimalOrb property rather than checking for an onPrimal handler.

Speed is only sorted once at the start of the switch-in event block in-game, which includes speed ties. To replicate this, an array of fractional values in the range [0, 1) are stored by the Battle object; this array is randomly shuffled at the start of each 'SwitchIn' event, and its values are added to handler speeds to guarantee they are sorted in the correct order.
Pokemon speeds including ties are resolved before all onSwitchIn handlers and aren't re-sorted in-between, so a fractional speed is subtracted from each Pokemon's respective event handlers by using the index of their unique field position (pokemon.side.n * pokemon.battle.sides.length + pokemon.position) in a pre-sorted-by-speed array.

*I rethought this and made EffectState an interface again. Its initialization is handled by a Battle function now since it needed the context of an instance of one anyway.

@pyuk-bot
Copy link
Contributor Author

Several tests have been unskipped, as the mechanics they check for now work correctly. The two tests currently failing are both related to Commander, and before I can fix them I need confirmation on some mechanics questions. The eject pack one was written when Commander activated instantly instead of waiting for its turn in the switch-in event order, and now Intimidate correctly activates first, so I'm not sure if the test's assertions are accurate to the games. If they are, then this test is failing because Eject Pack is bugged in a way that I didn't fix as part of this refactor because I didn't have the relevant information on its activation timings. As for the other test, the issue is that the Tatsugiri already has Commander, so when it transforms into another Pokemon with Commander, its ability isn't restarted. However, Commander's onUpdate handler waits for the ability to be started, so after transforming it can no longer activate. I know how I could fix this now without changing any other behavior, but I'd like some confirmation on the mechanics of why the ability isn't restarted in the first place. I suspect other ability data may not be reset in game like its handler order, i.e. transforming wouldn't change a Pokemon's place in the Lightning Rod redirection order if its ability doesn't change.

@pyuk-bot pyuk-bot force-pushed the mirrorherb branch 3 times, most recently from 95409ab to d54b495 Compare January 2, 2025 05:48
andrebastosdias

This comment was marked as resolved.

@pyuk-bot
Copy link
Contributor Author

Change pokemon.ignoringItem

	ignoringItem() {
		const item = this.getItem();
		if (item.isPrimalOrb) return false;
		return !!(
			this.itemState.knockedOff || // Gen 3-4
			(this.battle.gen >= 5 && !this.isActive) ||
			(!item.ignoreKlutz && this.hasAbility('klutz')) ||
			this.volatiles['embargo'] || this.battle.field.pseudoWeather['magicroom']
		);
	}

and remove the questionable hack. I can't add a suggestion directly on unedited lines.

Edit: Wouldn't cover seeds and Room Service

I forgot I did this. It doesn't need to cover seeds and Room Service because they are properly negated by Magic Room on switch-in, so your suggested change is fine.

pyuk-bot and others added 2 commits January 20, 2025 18:21
* Remove unnused choices

* Remove redundant isStarted

* Override runSwitch

* Complete comment

* Reset abilityState.started

* Update data/mods/gen7letsgo/scripts.ts

---------

Co-authored-by: pyuk-bot
@DaWoblefet
Copy link
Member

onSwitchInPriority review:

  • Several formats already used onSwitchInPriority (e.g. Shared Power); probably should up those above 2 so they happen before any "real" Abilities happen (SSBU uses 100, that's probably fine for all the OMs)
  • The healreplacement condition should have an onSwitchInPriority of 0 (it's the same timing as Healing Wish and Lunar Dance)
  • Healing Wish and Lunar Dance need their onSwitchInPriority updated in the Gen 7 mod to match Gen 9
  • I would like a comment on Klutz that it should have an onSwitchInPriority; I don't think that would ever matter unless GF introduced an item that triggered really early, but it would be good to note.
  • Booster Energy doesn't actually do anything on its own. Protosynthesis / Quark Drive should be activating everything for Booster Energy. That would matter for, say, the order of Protosynthesis in sun vs Booster Energy with Quark Drive being sent out at the same time. The Ability handlers for the Paradox Abilities should be taking care of everything Booster Energy is doing. Don't know how hard that is to address, and it's already wrong so this PR doesn't really affect that. If you want to say that's out of scope, that's fine.

sim/battle.ts Outdated Show resolved Hide resolved
@DaWoblefet
Copy link
Member

DaWoblefet commented Jan 22, 2025

I'd like to see some more tests for our new switch-in behavior. Things like:

  • Sticky Web vs Primal reversion (faster Kyogre takes Sticky Web, still Primal reverts before slower Groudon, check that Primal sun is the final weather active)
  • Hospitality vs hazards (Faster Hospitality heals after Stealth Rock, check HP is full)
  • Costar vs Intimidate (Slower Costar vs opposing faster Intimidate with a Clear Body ally)

Everything else looks really solid from my review. Very good job.

@pyuk-bot
Copy link
Contributor Author

Booster Energy doesn't actually do anything on its own. Protosynthesis / Quark Drive should be activating everything for Booster Energy. That would matter for, say, the order of Protosynthesis in sun vs Booster Energy with Quark Drive being sent out at the same time. The Ability handlers for the Paradox Abilities should be taking care of everything Booster Energy is doing. Don't know how hard that is to address, and it's already wrong so this PR doesn't really affect that. If you want to say that's out of scope, that's fine.

I vvould say that is outside the scope of this PR. I vvas already considering making that change after this before #10832 vvas made, and I brought up the idea of moving all the Booster Energy code there.

@andrebastosdias
Copy link
Contributor

#10832 I've moved the Booster Energy logic to the abilities.ts file, and it's ready for review. This PR will definitely simplify the code.

@andrebastosdias
Copy link
Contributor

Won't the new subOrders conflict with all the existing residual subOrders? These include abilities, conditions, side conditions, field conditions and items, whose subOrders values vary widely. Those that don't explicitly set a value will default to the ones in the new resolvePriority method.

@pyuk-bot
Copy link
Contributor Author

pyuk-bot commented Jan 22, 2025

If there are residual event handlers that don't have a suborder set that are running in the same order as handlers that do have a suborder set, that's already a bug. After combing through all of our residuals, I could only find one residual order vvith multiple handlers in it vvhere one had a suborder set and others didn't: leftovers, berry, and gold berry in gen 2. I've asked Marty vvhat the correct order there should be.

@andrebastosdias
Copy link
Contributor

andrebastosdias commented Jan 22, 2025

If there are residual event handlers that don't have a suborder set that are running in the same order as handlers that do have a suborder set, that's already a bug.

I’m not sure if you understood what I meant. If I'm understanding this correctly:

Currently, all effects without an explicit residual subOrder default to zero. With this PR, they will instead default to the subOrder for their effect type. For instance, Power Construct doesn’t have a defined onResidualSubOrder, so it currently defaults to zero, which causes it to run before Leftovers, which has a subOrder of 1. With this PR, Power Construct will have a subOrder of 7 (abilities), causing it to run after Leftovers.

The current subOrder values are unrelated to the new subOrder values assigned to each effect type, so basically, we have "two different metrics" for subOrder. This is only a problem for residuals, as they are the only events that currently use it.

Let me know if I'm mistaken.

@pyuk-bot
Copy link
Contributor Author

pyuk-bot commented Jan 22, 2025

If there are residual event handlers that don't have a suborder set that are running in the same order as handlers that do have a suborder set, that's already a bug.

I’m not sure if you understood what I meant. If I'm understanding this correctly:

Currently, all effects without an explicit residual subOrder default to zero. With this PR, they will instead default to the subOrder for their effect type. For instance, Power Construct doesn’t have a defined onResidualSubOrder, so it currently defaults to zero, which causes it to run before Leftovers, which has a subOrder of 1. With this PR, Power Construct will have a subOrder of 7 (abilities), causing it to run after Leftovers.

The current subOrder values are unrelated to the new subOrder values assigned to each effect type, so basically, we have "two different metrics" for subOrder. This is only a problem for residuals, as they are the only events that currently use it.

Let me know if I'm mistaken.

Residuals use both onResidualOrder and onResidualSubOrder. Order supercedes speed, which supercedes sub order, so Leftovers with its residual order of 5 always activates before Power Construct and its order of 29. Sub order is only used when multiple different effects activate for one Pokemon before they activate for another Pokemon, like when two Pokemon both have Leftovers in Grassy Terrain.

@DaWoblefet
Copy link
Member

DaWoblefet commented Jan 22, 2025

Won't the new subOrders conflict with all the existing residual subOrders? These include abilities, conditions, side conditions, field conditions and items, whose subOrders values vary widely. Those that don't explicitly set a value will default to the ones in the new resolvePriority method.

I think pyuk answered correctly here, but I'll also mention that in an ideal case, we would never need to hardcode suborders in the first place in modern generations. The suborders should just work out of the box based on the event the various effects are tied to. To put it another way, Abilities should always go before items, no matter if that's when calculating base power, calculating Speed, running effects that happen on switchin, end-of-turn effects, whatever the event. That would of course require having implemented every single event the game uses accurately, so it's not easy or anything.

data/abilities.ts Outdated Show resolved Hide resolved
Copy link
Member

@DaWoblefet DaWoblefet left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks good to go as far as I'm concerned. Since it is a large refactor, I'd like to merge at a convenient time for pyuk to be able to react to any oversights that snuck through.

Copy link
Collaborator

@Marty-D Marty-D left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is very cool, nice work pyuk! I scrolled through and couldn't think of anything missing or out of place, seems good to ship whenever*. 👍

*Since SPL is ongoing for the next two months or so, ideally this is merged soon after the tours server updates and restarts on any given Monday, allowing the most possible time for the main server to catch any issues before the following week.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants