Files
Emberwild/Assets/GAME/Script/Player/Events/PlayerEvents.cs
T
Mathew 78bfdf2828 Merge remote-tracking branch 'origin/feat/inport-props' into feat/craft-discovery
# Conflicts:
#	Assets/External/Animated PBR Chest Demo/Materials/WoodChest.mat
#	Packages/com.distantlands.cozy.core/Content/Integration/Import for BiRP.unitypackage.meta
#	Packages/com.distantlands.cozy.core/Content/Integration/Import for HDRP.unitypackage.meta
#	Packages/com.distantlands.cozy.core/Content/Integration/Import for URP.unitypackage.meta
2026-07-25 19:42:26 +02:00

516 lines
28 KiB
C#

using System;
using System.Collections.Generic;
using System.Diagnostics;
using UnityEngine;
using Ashwild.Audio;
using Ashwild.Building;
using Ashwild.Harvesting;
using Ashwild.Interaction;
using Ashwild.Inventory;
namespace Ashwild.Player
{
public static class PlayerEvents
{
// ============================================================
// State (single source of truth — read by everyone)
// ============================================================
public static bool IsDead { get; private set; }
public static bool IsGrounded { get; private set; } = true;
public static bool IsCrouching { get; private set; }
public static bool IsSprinting { get; private set; }
public static bool IsSliding { get; private set; }
public static bool IsOnSlope { get; private set; }
public static bool IsInventoryOpen { get; private set; }
public static bool IsChestOpen { get; private set; }
public static bool IsBuildMenuOpen { get; private set; }
// True while a build ghost is being positioned in the world (menu closed, no structure placed yet).
public static bool IsPlacingBuild { get; private set; }
// True while the build hammer is in demolition mode (right-click held, menu closed, not placing).
public static bool IsDemolishing { get; private set; }
// Composite "the player is actively building" flag — true whenever the construction menu is open,
// a ghost is being positioned, or the hammer is demolishing. Recomputed from those three states
// (never set directly) and used to widen the crosshair and to suppress item pickup while building.
public static bool IsBuilding { get; private set; }
public static bool IsPaused { get; private set; }
// True when a networked session is live (host or client).
public static bool IsOnline { get; private set; }
// True when this instance owns the server (host = server + local client).
public static bool IsHost { get; private set; }
// Shareable code of the current session (host SteamID64). Empty when offline.
public static string SessionCode { get; private set; } = string.Empty;
// The interactable the player is currently aiming at (null when none).
public static IInteractable HoveredInteractable { get; private set; }
// Single source of truth for "the player must not respond to input right now".
// Used by locomotion, camera, tools and interaction so they all agree.
public static bool InputLocked => IsDead || IsInventoryOpen || IsChestOpen || IsBuildMenuOpen || IsPaused;
// ============================================================
// Input events
// ============================================================
public static event Action<Vector2> MoveInput;
public static event Action<Vector2> LookInput;
public static event Action JumpPressed;
public static event Action<bool> SprintHeld;
public static event Action CrouchPressed;
public static event Action AttackPressed;
public static event Action SecondaryUsePressed; // right-click / secondary use (build menu, aim, block, ...)
public static event Action<bool> SecondaryUseHeld; // right-click pressed (true) / released (false) — for tap-vs-hold gestures
public static event Action InteractPressed;
public static event Action DropPressed;
public static event Action InventoryTogglePressed;
public static event Action<int> HotbarSlotPressed;
public static event Action<float> HotbarScroll;
public static event Action BuildRotatePressed; // rotate the build ghost one step (R key) while placing
public static event Action CancelPressed; // UI "Cancel" (Escape) — back / pause / close
// ============================================================
// Locomotion events
// ============================================================
public static event Action<bool, float> GroundedChanged; // isGrounded, impactSpeed
public static event Action<bool, Vector3> SlopeChanged;
public static event Action<PhysicsMaterial> GroundMaterialChanged;
public static event Action<Vector3> VelocityChanged;
public static event Action<float> YawChanged;
public static event Action Jumped;
public static event Action<bool> CrouchChanged;
public static event Action<bool> SprintChanged;
public static event Action<float, Vector3> CapsuleHeightChanged;
public static event Action<float> CameraHeightChanged;
public static event Action<float> LandingStunStarted;
public static event Action LandingStunEnded;
public static event Action SlideStarted;
public static event Action SlideEnded;
public static event Action<int> MultiJumpPerformed;
// ============================================================
// Stats events
// ============================================================
public static event Action<float, float> HealthChanged;
public static event Action<float, float> HungerChanged;
public static event Action<float, float> ThirstChanged;
public static event Action<float, DamageType> Damaged;
public static event Action<float> FallDamageApplied;
public static event Action<float> StarvationTick;
public static event Action<float> DehydrationTick;
// ============================================================
// Inventory events
// ============================================================
public static event Action<int> InventorySlotChanged;
public static event Action<int> SelectedHotbarSlotChanged;
// (item, quantity, isTransfer) — isTransfer is true when the stack merely moved between the
// player's own containers (a chest withdrawal, a refund) instead of being newly acquired from
// the world. Consumers that announce a gain (notifications) skip transfers; consumers that only
// track "the player has held this" (craft discovery) treat both the same.
public static event Action<ItemData, int, bool> ItemAdded;
public static event Action<ItemData, int> ItemDropped;
public static event Action<ItemData> ItemConsumed;
public static event Action<ItemData, int> ItemPickedUp;
public static event Action<ItemData> ItemDiscovered; // an item entered the inventory for the first time (unlocks recipes)
public static event Action<Sprite, string> RecipeDiscovered; // a recipe was just revealed (icon, name) — for the "NEW" craft notification
public static event Action<ItemData> HeldItemChanged;
public static event Action<bool> InventoryOpenChanged;
public static event Action<bool> ChestOpenChanged;
public static event Action<bool> GamePausedChanged;
// ============================================================
// Interaction events
// ============================================================
public static event Action<IInteractable> InteractableHoverChanged; // null = nothing hovered
// A world interaction (pickup, harvest) could not be carried out — payload is a short, player-facing
// reason such as "Inventory full". Raised whenever an interaction is refused, so a refusal is never
// silent: without it a rejected harvest looks exactly like a successful one that dropped nothing.
public static event Action<string> InteractionRefused;
// The local player just reached out to grab something off the ground. Raised the moment the
// gesture is committed (all checks passed, request sent) rather than when the item lands in the
// inventory, so the grab animation plays instantly instead of after a server round-trip.
public static event Action GrabPerformed;
// ============================================================
// Tools events
// ============================================================
public static event Action<int> AttackSwung; // the swing started — payload is the combo variant index
public static event Action AttackImpact; // the swing's contact frame (Animation Event) — resolve the hit here
public static event Action<Harvestable, float> HarvestableHit;
public static event Action EquipStarted; // play the draw animation for the item now in hand
public static event Action UnequipStarted; // play the put-away animation for the item leaving the hand
public static event Action EquipAnimComplete;
public static event Action UnequipAnimComplete;
// ============================================================
// Building events
// ============================================================
public static event Action BuildMenuToggleRequested; // the build hammer asks to open/close the construction menu
public static event Action BuildCancelRequested; // the build hammer asks to cancel/exit the current ghost placement (right-click while placing)
public static event Action<bool> BuildMenuOpenChanged; // the construction menu opened (true) or closed (false)
public static event Action<bool> BuildPlacingChanged; // a build ghost started (true) / stopped (false) being positioned
public static event Action<bool> DemolishModeChanged; // the hammer entered (true) / left (false) demolition mode (right-click held)
public static event Action<bool> BuildModeChanged; // the composite build mode turned on (true) / off (false) — see IsBuilding
public static event Action<IReadOnlyList<BuildCostView>> BuildCostChanged; // the active build's cost to display near the crosshair (null/empty = clear)
// ============================================================
// Cooking events
// ============================================================
public static event Action<ItemData> FoodPlacedToCook; // the raw item placed on a station
public static event Action<ItemData> FoodCookedReady; // the cooked result is ready to take
public static event Action<ItemData> FuelAdded; // the fuel item consumed to feed a station
// ============================================================
// Audio events
// ============================================================
public static event Action<SurfaceSoundProfile> SurfaceChanged;
public static event Action<SurfaceSoundProfile, float> FootstepTriggered;
// ============================================================
// Lifecycle events
// ============================================================
public static event Action Died;
public static event Action Respawned;
public static event Action<bool> DeathChanged;
public static event Action<Vector3, Quaternion> RespawnPointChanged; // the local player's respawn pose was reassigned (e.g. claimed a bed)
// ============================================================
// Network / session events
// ============================================================
public static event Action SessionStarting; // host/join requested, before transport is up
public static event Action<string> SessionStarted; // host is up — payload is the shareable code
public static event Action<string> SessionJoining; // local client connecting to a code
public static event Action SessionJoined; // local client connected to a host
public static event Action SessionStopped; // session fully torn down (offline again)
public static event Action<string> SessionError; // human-readable failure reason
public static event Action<int> RemotePlayerCountChanged; // number of *other* connected clients
public static event Action LocalPlayerSpawned; // the local owned player NetworkObject is ready
public static event Action LoadingCurtainShown; // loading curtain finished fading in — safe to start the heavy scene load behind it
public static event Action ReturningToMenu; // leaving the session back to the menu — raise the curtain over the swap
public static event Action MenuReady; // the menu scene has finished loading — lower the curtain
public static event Action<ulong, string, string> InviteReceived; // inviter SteamID64, inviter name, connect code
// ============================================================
// Raisers — state is updated automatically here so consumers
// can rely on PlayerEvents.IsX matching the latest event.
// ============================================================
public static void RaiseMoveInput(Vector2 v) { Log(nameof(MoveInput)); MoveInput?.Invoke(v); }
public static void RaiseLookInput(Vector2 v) { LookInput?.Invoke(v); }
public static void RaiseJumpPressed() { Log(nameof(JumpPressed)); JumpPressed?.Invoke(); }
public static void RaiseSprintHeld(bool b) { Log(nameof(SprintHeld)); SprintHeld?.Invoke(b); }
public static void RaiseCrouchPressed() { Log(nameof(CrouchPressed)); CrouchPressed?.Invoke(); }
public static void RaiseAttackPressed() { Log(nameof(AttackPressed)); AttackPressed?.Invoke(); }
public static void RaiseSecondaryUsePressed() { Log(nameof(SecondaryUsePressed)); SecondaryUsePressed?.Invoke(); }
public static void RaiseSecondaryUseHeld(bool held) { Log(nameof(SecondaryUseHeld)); SecondaryUseHeld?.Invoke(held); }
public static void RaiseInteractPressed() { Log(nameof(InteractPressed)); InteractPressed?.Invoke(); }
public static void RaiseDropPressed() { Log(nameof(DropPressed)); DropPressed?.Invoke(); }
public static void RaiseInventoryTogglePressed() { Log(nameof(InventoryTogglePressed)); InventoryTogglePressed?.Invoke(); }
public static void RaiseHotbarSlotPressed(int i) { Log(nameof(HotbarSlotPressed)); HotbarSlotPressed?.Invoke(i); }
public static void RaiseHotbarScroll(float f) { HotbarScroll?.Invoke(f); }
public static void RaiseBuildRotatePressed() { Log(nameof(BuildRotatePressed)); BuildRotatePressed?.Invoke(); }
public static void RaiseCancelPressed() { Log(nameof(CancelPressed)); CancelPressed?.Invoke(); }
public static void RaiseGroundedChanged(bool grounded, float impactSpeed)
{
IsGrounded = grounded;
Log(nameof(GroundedChanged));
GroundedChanged?.Invoke(grounded, impactSpeed);
}
public static void RaiseSlopeChanged(bool onSlope, Vector3 normal)
{
IsOnSlope = onSlope;
SlopeChanged?.Invoke(onSlope, normal);
}
public static void RaiseGroundMaterialChanged(PhysicsMaterial m) { GroundMaterialChanged?.Invoke(m); }
public static void RaiseVelocityChanged(Vector3 v) { VelocityChanged?.Invoke(v); }
public static void RaiseYawChanged(float y) { YawChanged?.Invoke(y); }
public static void RaiseJumped() { Log(nameof(Jumped)); Jumped?.Invoke(); }
public static void RaiseCrouchChanged(bool b)
{
IsCrouching = b;
Log(nameof(CrouchChanged));
CrouchChanged?.Invoke(b);
}
public static void RaiseSprintChanged(bool b)
{
IsSprinting = b;
Log(nameof(SprintChanged));
SprintChanged?.Invoke(b);
}
public static void RaiseCapsuleHeightChanged(float h, Vector3 c) { CapsuleHeightChanged?.Invoke(h, c); }
public static void RaiseCameraHeightChanged(float h) { CameraHeightChanged?.Invoke(h); }
public static void RaiseLandingStunStarted(float d) { Log(nameof(LandingStunStarted)); LandingStunStarted?.Invoke(d); }
public static void RaiseLandingStunEnded() { Log(nameof(LandingStunEnded)); LandingStunEnded?.Invoke(); }
public static void RaiseSlideStarted()
{
IsSliding = true;
Log(nameof(SlideStarted));
SlideStarted?.Invoke();
}
public static void RaiseSlideEnded()
{
IsSliding = false;
Log(nameof(SlideEnded));
SlideEnded?.Invoke();
}
public static void RaiseMultiJumpPerformed(int i) { Log(nameof(MultiJumpPerformed)); MultiJumpPerformed?.Invoke(i); }
public static void RaiseHealthChanged(float c, float m) { HealthChanged?.Invoke(c, m); }
public static void RaiseHungerChanged(float c, float m) { HungerChanged?.Invoke(c, m); }
public static void RaiseThirstChanged(float c, float m) { ThirstChanged?.Invoke(c, m); }
public static void RaiseDamaged(float a, DamageType t) { Log(nameof(Damaged)); Damaged?.Invoke(a, t); }
public static void RaiseFallDamageApplied(float a) { Log(nameof(FallDamageApplied)); FallDamageApplied?.Invoke(a); }
public static void RaiseStarvationTick(float a) { StarvationTick?.Invoke(a); }
public static void RaiseDehydrationTick(float a) { DehydrationTick?.Invoke(a); }
public static void RaiseInventorySlotChanged(int i) { InventorySlotChanged?.Invoke(i); }
public static void RaiseSelectedHotbarSlotChanged(int i) { Log(nameof(SelectedHotbarSlotChanged)); SelectedHotbarSlotChanged?.Invoke(i); }
public static void RaiseItemAdded(ItemData d, int q, bool isTransfer = false) { Log(nameof(ItemAdded)); ItemAdded?.Invoke(d, q, isTransfer); }
public static void RaiseItemDropped(ItemData d, int q) { Log(nameof(ItemDropped)); ItemDropped?.Invoke(d, q); }
public static void RaiseItemConsumed(ItemData d) { Log(nameof(ItemConsumed)); ItemConsumed?.Invoke(d); }
public static void RaiseItemPickedUp(ItemData d, int q) { Log(nameof(ItemPickedUp)); ItemPickedUp?.Invoke(d, q); }
public static void RaiseItemDiscovered(ItemData d) { Log(nameof(ItemDiscovered)); ItemDiscovered?.Invoke(d); }
public static void RaiseRecipeDiscovered(Sprite icon, string recipeName) { Log(nameof(RecipeDiscovered)); RecipeDiscovered?.Invoke(icon, recipeName); }
public static void RaiseHeldItemChanged(ItemData d) { Log(nameof(HeldItemChanged)); HeldItemChanged?.Invoke(d); }
public static void RaiseInventoryOpenChanged(bool b)
{
IsInventoryOpen = b;
Log(nameof(InventoryOpenChanged));
InventoryOpenChanged?.Invoke(b);
}
public static void RaiseChestOpenChanged(bool b)
{
IsChestOpen = b;
Log(nameof(ChestOpenChanged));
ChestOpenChanged?.Invoke(b);
}
public static void RaiseGamePaused(bool b)
{
IsPaused = b;
Log(nameof(GamePausedChanged));
GamePausedChanged?.Invoke(b);
}
public static void RaiseInteractableHoverChanged(IInteractable target)
{
HoveredInteractable = target;
Log(nameof(InteractableHoverChanged));
InteractableHoverChanged?.Invoke(target);
}
/// <summary>
/// Announces that a world interaction was refused, with a short player-facing reason. The HUD turns
/// it into a notification so the player always learns why nothing was gained.
/// </summary>
public static void RaiseInteractionRefused(string reason)
{
if (string.IsNullOrEmpty(reason)) return;
Log(nameof(InteractionRefused));
InteractionRefused?.Invoke(reason);
}
/// <summary>
/// Announces a swing, carrying which combo variant to play. The variant defaults to 0 so a plain
/// one-swing tool never has to think about combos, while a weapon can cycle its own variants.
/// </summary>
public static void RaiseAttackSwung(int variant = 0) { Log(nameof(AttackSwung)); AttackSwung?.Invoke(variant); }
/// <summary>
/// Announces the local player's grab gesture so the arms play their pickup animation. Owner-side
/// only, and deliberately not tied to the item actually arriving: the server still has the final
/// say on the pickup, but the hand should reach out the instant the player presses.
/// </summary>
public static void RaiseGrabPerformed() { Log(nameof(GrabPerformed)); GrabPerformed?.Invoke(); }
/// <summary>
/// Announces the contact frame of a swing, raised from the attack clip's Animation Event. Items
/// resolve their hit here rather than on the input press so the damage lands when the blade does.
/// </summary>
public static void RaiseAttackImpact() { Log(nameof(AttackImpact)); AttackImpact?.Invoke(); }
public static void RaiseHarvestableHit(Harvestable h, float d) { Log(nameof(HarvestableHit)); HarvestableHit?.Invoke(h, d); }
public static void RaiseEquipStarted() { Log(nameof(EquipStarted)); EquipStarted?.Invoke(); }
public static void RaiseUnequipStarted() { Log(nameof(UnequipStarted)); UnequipStarted?.Invoke(); }
public static void RaiseEquipAnimComplete() { EquipAnimComplete?.Invoke(); }
public static void RaiseUnequipAnimComplete() { UnequipAnimComplete?.Invoke(); }
public static void RaiseBuildMenuToggleRequested() { Log(nameof(BuildMenuToggleRequested)); BuildMenuToggleRequested?.Invoke(); }
public static void RaiseBuildCancelRequested() { Log(nameof(BuildCancelRequested)); BuildCancelRequested?.Invoke(); }
public static void RaiseBuildMenuOpenChanged(bool open)
{
IsBuildMenuOpen = open;
Log(nameof(BuildMenuOpenChanged));
BuildMenuOpenChanged?.Invoke(open);
RefreshBuildMode();
}
public static void RaiseBuildPlacingChanged(bool placing)
{
IsPlacingBuild = placing;
Log(nameof(BuildPlacingChanged));
BuildPlacingChanged?.Invoke(placing);
RefreshBuildMode();
}
/// <summary>
/// Publishes the active build's cost lines for the crosshair to render (or a null/empty list to
/// clear it). Owner-side only; BuildManager re-raises it live as the inventory changes so the
/// affordability colouring stays current while a piece is being positioned.
/// </summary>
public static void RaiseBuildCostChanged(IReadOnlyList<BuildCostView> cost)
{
Log(nameof(BuildCostChanged));
BuildCostChanged?.Invoke(cost);
}
/// <summary>
/// Toggles demolition mode (right-click held with the hammer out). Owner-side only; the
/// hammer raises it and BuildManager drives the world-facing targeting/destroy from it.
/// </summary>
public static void RaiseDemolishModeChanged(bool on)
{
IsDemolishing = on;
Log(nameof(DemolishModeChanged));
DemolishModeChanged?.Invoke(on);
RefreshBuildMode();
}
/// <summary>
/// Recomputes the composite build-mode flag from its three sub-states and raises BuildModeChanged
/// only when it actually flips, so consumers (crosshair spacing, pickup gating) react exactly once
/// per transition instead of on every sub-state change.
/// </summary>
private static void RefreshBuildMode()
{
bool building = IsBuildMenuOpen || IsPlacingBuild || IsDemolishing;
if (building == IsBuilding) return;
IsBuilding = building;
Log(nameof(BuildModeChanged));
BuildModeChanged?.Invoke(building);
}
public static void RaiseFoodPlacedToCook(ItemData raw) { Log(nameof(FoodPlacedToCook)); FoodPlacedToCook?.Invoke(raw); }
public static void RaiseFoodCookedReady(ItemData cooked) { Log(nameof(FoodCookedReady)); FoodCookedReady?.Invoke(cooked); }
public static void RaiseFuelAdded(ItemData fuel) { Log(nameof(FuelAdded)); FuelAdded?.Invoke(fuel); }
public static void RaiseSurfaceChanged(SurfaceSoundProfile p) { SurfaceChanged?.Invoke(p); }
public static void RaiseFootstepTriggered(SurfaceSoundProfile p, float v) { FootstepTriggered?.Invoke(p, v); }
public static void RaiseDied()
{
IsDead = true;
Log(nameof(Died));
Died?.Invoke();
DeathChanged?.Invoke(true);
}
public static void RaiseRespawned()
{
IsDead = false;
IsCrouching = false;
IsSliding = false;
IsSprinting = false;
Log(nameof(Respawned));
Respawned?.Invoke();
DeathChanged?.Invoke(false);
}
/// <summary>
/// Reassigns the local player's respawn pose (e.g. after claiming a bed). Owner-side only;
/// the pose is pure local state and is never networked — each client keeps its own.
/// </summary>
public static void RaiseRespawnPointChanged(Vector3 position, Quaternion rotation)
{
Log(nameof(RespawnPointChanged));
RespawnPointChanged?.Invoke(position, rotation);
}
public static void RaiseSessionStarting() { Log(nameof(SessionStarting)); SessionStarting?.Invoke(); }
public static void RaiseSessionStarted(string code)
{
IsOnline = true;
IsHost = true;
SessionCode = code ?? string.Empty;
Log(nameof(SessionStarted));
SessionStarted?.Invoke(SessionCode);
}
public static void RaiseSessionJoining(string code)
{
SessionCode = code ?? string.Empty;
Log(nameof(SessionJoining));
SessionJoining?.Invoke(SessionCode);
}
public static void RaiseSessionJoined()
{
IsOnline = true;
IsHost = false;
Log(nameof(SessionJoined));
SessionJoined?.Invoke();
}
public static void RaiseSessionStopped()
{
IsOnline = false;
IsHost = false;
SessionCode = string.Empty;
ResetPlayerState();
Log(nameof(SessionStopped));
SessionStopped?.Invoke();
}
// Clears the transient, player-local state so a brand-new session/player starts clean.
// The static bus tracks the LOCAL player only; without this, dying or opening the
// inventory and then leaving a session would leave stale flags set for the next one.
private static void ResetPlayerState()
{
IsDead = false;
IsGrounded = true;
IsCrouching = false;
IsSprinting = false;
IsSliding = false;
IsOnSlope = false;
IsInventoryOpen = false;
IsChestOpen = false;
IsBuildMenuOpen = false;
IsPlacingBuild = false;
IsDemolishing = false;
IsBuilding = false;
IsPaused = false;
HoveredInteractable = null;
}
public static void RaiseSessionError(string reason) { Log(nameof(SessionError)); SessionError?.Invoke(reason); }
public static void RaiseRemotePlayerCountChanged(int count) { Log(nameof(RemotePlayerCountChanged)); RemotePlayerCountChanged?.Invoke(count); }
public static void RaiseLocalPlayerSpawned() { Log(nameof(LocalPlayerSpawned)); LocalPlayerSpawned?.Invoke(); }
public static void RaiseLoadingCurtainShown() { Log(nameof(LoadingCurtainShown)); LoadingCurtainShown?.Invoke(); }
public static void RaiseReturningToMenu() { Log(nameof(ReturningToMenu)); ReturningToMenu?.Invoke(); }
public static void RaiseMenuReady() { Log(nameof(MenuReady)); MenuReady?.Invoke(); }
/// <summary>
/// A friend invited the local player to their session (delivered by Steam). Carries the
/// inviter's SteamID64, display name, and the connect code used to join their room.
/// </summary>
public static void RaiseInviteReceived(ulong inviterSteamId, string inviterName, string connectCode)
{
Log(nameof(InviteReceived));
InviteReceived?.Invoke(inviterSteamId, inviterName, connectCode);
}
[Conditional("PLAYER_EVENT_LOG")]
private static void Log(string evtName)
{
UnityEngine.Debug.Log($"[PlayerEvents] {evtName}");
}
}
public enum DamageType { Generic, Fall, Starvation, Dehydration, Combat }
}