Entities/Abilities/PlayerAbility.cs
namespace BlockParty;

/// <summary>
/// A pluggable, per-character behaviour module hung off <see cref="Player"/>. This is the
/// composition seam for genuinely-new abilities (edge-wrap, teleport, …) that carry their own code
/// and state, as opposed to the simple on/off tunables carried as data in
/// <see cref="CharacterAbilities"/>. The player owns a list of these, built from its character, and
/// calls the hooks at fixed points in its tick — so an ability can react without the shared physics
/// core needing to know about it, and characters can mix any subset of abilities freely (which is
/// why abilities are composed here rather than expressed as <see cref="Player"/> subclasses).
///
/// DETERMINISM CONTRACT (must hold or replays desync): a hook may read only the recorded
/// <see cref="InputState"/> and the player's own fixed-step state; never raw <c>Input</c> or
/// <c>Time.Delta</c>. Gameplay decisions use the authoritative <c>Rng</c> stream, while visual-only
/// randomness uses <c>Rng.Cosmetic*</c>. Altering an existing character's simulation requires bumping
/// <see cref="Sim.VERSION"/> (post-release, together with a version gate so older replays still
/// play the old behaviour — see that constant's doc).
/// </summary>
public abstract class PlayerAbility
{
	/// <summary>Identity-level modules can survive a temporary character-form replacement.</summary>
	public virtual bool PersistsAcrossCharacterChanges => false;

	/// <summary>Called once, when the player's visuals + abilities are created.</summary>
	public virtual void OnSpawn( Player player ) { }

	/// <summary>Called before this module is removed after a mid-run character change. Modules that own
	/// persistent child or stage entities must destroy or retire them here.</summary>
	public virtual void OnRemoved( Player player ) { }

	/// <summary>Called after a kill has emitted its blood, sound, and death animation, but before the
	/// player commits its dead state or notifies the stage. Return true when the ability consumed this
	/// death; the handler may recover the player or chain into another death that commits normally.</summary>
	public virtual bool TryPreventDeath( Player player ) => false;

	/// <summary>Called once at the moment the player dies (every kill path routes through it), BEFORE
	/// the death animation plays. A dead player never ticks again, so stage-level visuals an ability
	/// drives from its tick hooks (e.g. Solar's sunbeams) freeze at their last shape while blocks keep
	/// moving — hide or retire them here. Visuals that should ride out the death animation instead
	/// belong in <see cref="OnDeathAnimationFinished"/>.</summary>
	public virtual void OnDeath( Player player ) { }

	/// <summary>Called whenever the player's sprite starts an animation. Visual-only ability layers
	/// can follow the body without becoming gameplay entities.</summary>
	public virtual void OnAnimationChanged( Player player, string animation ) { }

	/// <summary>Called when a non-looping death animation finishes and the player's body is hidden.</summary>
	public virtual void OnDeathAnimationFinished( Player player ) { }

	/// <summary>Called after the player's sprite orientation is resolved for the tick.</summary>
	public virtual void OnVisualUpdated( Player player, bool flipHorizontal, bool flipVertical ) { }

	/// <summary>Recreate currently-held presentation state after a replay seek silently rebuilds the
	/// fixed-step simulation. Historical transient effects should not be replayed at the destination.</summary>
	public virtual void OnReplayPresentationRebuilt( Player player ) { }

	/// <summary>A mimic transform swapped <paramref name="old"/> for <paramref name="replacement"/> this
	/// tick — re-point any held reference. SIM code must react HERE (or via Block.Replaced), never by
	/// polling engine IsValid(): the queued Destroy flushes at frame cadence, which never happens inside
	/// a one-frame replay rebuild.</summary>
	public virtual void OnBlockReplaced( Player player, Block old, Block replacement ) { }

	/// <summary>Called at the very START of each fixed-step tick, before the player reads its movement
	/// input for the frame. An ability that must act on this tick's input before the player moves (e.g.
	/// the Gunner's double-tap fire detector) belongs here, not in <see cref="PostTick"/>, which runs
	/// too late. Collision flags read here are last frame's values (they haven't been recomputed yet
	/// this tick), matching the rest of the top-of-tick logic.</summary>
	public virtual void PreTick( Player player, float dt ) { }

	/// <summary>Called at the very end of each fixed-step tick, after the player's movement,
	/// collision and bounds handling have fully resolved for the frame.</summary>
	public virtual void PostTick( Player player, float dt ) { }

	/// <summary>Called after a successful floor-relative bounce has resolved its launch velocity.</summary>
	public virtual void OnBouncedOnBlock( Player player, Block block ) { }

	/// <summary>Called when a spent dash charge refills through surface movement (see Player's
	/// DashRecharge*Distance). Not raised for the landing edge re-arm or ability-driven refreshes.</summary>
	public virtual void OnDashRecharged( Player player ) { }

	/// <summary>Called when the Mimic form's crush-proof squash flattens the player. While squashed
	/// the player returns from its tick before the ability loops run, so Pre/PostTick stay silent
	/// until <see cref="OnUnsquashed"/>.</summary>
	public virtual void OnSquashed( Player player ) { }

	/// <summary>Called when the squash's reverse anim finishes and control returns.</summary>
	public virtual void OnUnsquashed( Player player ) { }
}