Compare commits
14
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a900208273 | ||
|
|
c857293eb4 | ||
|
|
90de51b1a0 | ||
|
|
a5eb673c6e | ||
|
|
e4fb702a1c | ||
|
|
a36eaf27bc | ||
|
|
0b34a9742a | ||
|
|
fd08fe4804 | ||
|
|
8099d69a23 | ||
|
|
befed4c2fa | ||
|
|
b55eda5e51 | ||
|
|
64f4a59b0b | ||
|
|
495424a2b2 | ||
|
|
83cc8f592e |
@@ -3,7 +3,8 @@
|
|||||||
"allow": [
|
"allow": [
|
||||||
"mcp__unreal-mcp__describe_toolset",
|
"mcp__unreal-mcp__describe_toolset",
|
||||||
"mcp__unreal-mcp__call_tool",
|
"mcp__unreal-mcp__call_tool",
|
||||||
"Bash(python *)"
|
"Bash(python *)",
|
||||||
|
"mcp__unreal-mcp__list_toolsets"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"enabledMcpjsonServers": [
|
"enabledMcpjsonServers": [
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -1,6 +1,6 @@
|
|||||||
[/Script/EngineSettings.GameMapsSettings]
|
[/Script/EngineSettings.GameMapsSettings]
|
||||||
GameDefaultMap=/Game/ThirdPerson/Lvl_ThirdPerson.Lvl_ThirdPerson
|
GameDefaultMap=/Game/Levels/MainMenu.MainMenu
|
||||||
EditorStartupMap=/Game/ThirdPerson/Lvl_ThirdPerson.Lvl_ThirdPerson
|
EditorStartupMap=/Game/Levels/MainMenu.MainMenu
|
||||||
GlobalDefaultGameMode=/Game/ThirdPerson/Blueprints/BP_ThirdPersonGameMode.BP_ThirdPersonGameMode_C
|
GlobalDefaultGameMode=/Game/ThirdPerson/Blueprints/BP_ThirdPersonGameMode.BP_ThirdPersonGameMode_C
|
||||||
bOffsetPlayerGamepadIds=False
|
bOffsetPlayerGamepadIds=False
|
||||||
|
|
||||||
@@ -27,6 +27,7 @@ r.Substrate.ProjectGBufferFormat=0
|
|||||||
r.DefaultFeature.LocalExposure.HighlightContrastScale=0.8
|
r.DefaultFeature.LocalExposure.HighlightContrastScale=0.8
|
||||||
|
|
||||||
r.DefaultFeature.LocalExposure.ShadowContrastScale=0.8
|
r.DefaultFeature.LocalExposure.ShadowContrastScale=0.8
|
||||||
|
r.Lumen.HardwareRayTracing.LightingMode=0
|
||||||
|
|
||||||
[/Script/WindowsTargetPlatform.WindowsTargetSettings]
|
[/Script/WindowsTargetPlatform.WindowsTargetSettings]
|
||||||
DefaultGraphicsRHI=DefaultGraphicsRHI_DX12
|
DefaultGraphicsRHI=DefaultGraphicsRHI_DX12
|
||||||
@@ -72,6 +73,65 @@ AppliedTargetedHardwareClass=Desktop
|
|||||||
DefaultGraphicsPerformance=Maximum
|
DefaultGraphicsPerformance=Maximum
|
||||||
AppliedDefaultGraphicsPerformance=Maximum
|
AppliedDefaultGraphicsPerformance=Maximum
|
||||||
|
|
||||||
|
[/Script/Engine.CollisionProfile]
|
||||||
|
-Profiles=(Name="NoCollision",CollisionEnabled=NoCollision,ObjectTypeName="WorldStatic",CustomResponses=((Channel="Visibility",Response=ECR_Ignore),(Channel="Camera",Response=ECR_Ignore)),HelpMessage="No collision",bCanModify=False)
|
||||||
|
-Profiles=(Name="BlockAll",CollisionEnabled=QueryAndPhysics,ObjectTypeName="WorldStatic",CustomResponses=,HelpMessage="WorldStatic object that blocks all actors by default. All new custom channels will use its own default response. ",bCanModify=False)
|
||||||
|
-Profiles=(Name="OverlapAll",CollisionEnabled=QueryOnly,ObjectTypeName="WorldStatic",CustomResponses=((Channel="WorldStatic",Response=ECR_Overlap),(Channel="Pawn",Response=ECR_Overlap),(Channel="Visibility",Response=ECR_Overlap),(Channel="WorldDynamic",Response=ECR_Overlap),(Channel="Camera",Response=ECR_Overlap),(Channel="PhysicsBody",Response=ECR_Overlap),(Channel="Vehicle",Response=ECR_Overlap),(Channel="Destructible",Response=ECR_Overlap)),HelpMessage="WorldStatic object that overlaps all actors by default. All new custom channels will use its own default response. ",bCanModify=False)
|
||||||
|
-Profiles=(Name="BlockAllDynamic",CollisionEnabled=QueryAndPhysics,ObjectTypeName="WorldDynamic",CustomResponses=,HelpMessage="WorldDynamic object that blocks all actors by default. All new custom channels will use its own default response. ",bCanModify=False)
|
||||||
|
-Profiles=(Name="OverlapAllDynamic",CollisionEnabled=QueryOnly,ObjectTypeName="WorldDynamic",CustomResponses=((Channel="WorldStatic",Response=ECR_Overlap),(Channel="Pawn",Response=ECR_Overlap),(Channel="Visibility",Response=ECR_Overlap),(Channel="WorldDynamic",Response=ECR_Overlap),(Channel="Camera",Response=ECR_Overlap),(Channel="PhysicsBody",Response=ECR_Overlap),(Channel="Vehicle",Response=ECR_Overlap),(Channel="Destructible",Response=ECR_Overlap)),HelpMessage="WorldDynamic object that overlaps all actors by default. All new custom channels will use its own default response. ",bCanModify=False)
|
||||||
|
-Profiles=(Name="IgnoreOnlyPawn",CollisionEnabled=QueryOnly,ObjectTypeName="WorldDynamic",CustomResponses=((Channel="Pawn",Response=ECR_Ignore),(Channel="Vehicle",Response=ECR_Ignore)),HelpMessage="WorldDynamic object that ignores Pawn and Vehicle. All other channels will be set to default.",bCanModify=False)
|
||||||
|
-Profiles=(Name="OverlapOnlyPawn",CollisionEnabled=QueryOnly,ObjectTypeName="WorldDynamic",CustomResponses=((Channel="Pawn",Response=ECR_Overlap),(Channel="Vehicle",Response=ECR_Overlap),(Channel="Camera",Response=ECR_Ignore)),HelpMessage="WorldDynamic object that overlaps Pawn, Camera, and Vehicle. All other channels will be set to default. ",bCanModify=False)
|
||||||
|
-Profiles=(Name="Pawn",CollisionEnabled=QueryAndPhysics,ObjectTypeName="Pawn",CustomResponses=((Channel="Visibility",Response=ECR_Ignore)),HelpMessage="Pawn object. Can be used for capsule of any playerable character or AI. ",bCanModify=False)
|
||||||
|
-Profiles=(Name="Spectator",CollisionEnabled=QueryOnly,ObjectTypeName="Pawn",CustomResponses=((Channel="WorldStatic",Response=ECR_Block),(Channel="Pawn",Response=ECR_Ignore),(Channel="Visibility",Response=ECR_Ignore),(Channel="WorldDynamic",Response=ECR_Ignore),(Channel="Camera",Response=ECR_Ignore),(Channel="PhysicsBody",Response=ECR_Ignore),(Channel="Vehicle",Response=ECR_Ignore),(Channel="Destructible",Response=ECR_Ignore)),HelpMessage="Pawn object that ignores all other actors except WorldStatic.",bCanModify=False)
|
||||||
|
-Profiles=(Name="CharacterMesh",CollisionEnabled=QueryOnly,ObjectTypeName="Pawn",CustomResponses=((Channel="Pawn",Response=ECR_Ignore),(Channel="Vehicle",Response=ECR_Ignore),(Channel="Visibility",Response=ECR_Ignore)),HelpMessage="Pawn object that is used for Character Mesh. All other channels will be set to default.",bCanModify=False)
|
||||||
|
-Profiles=(Name="PhysicsActor",CollisionEnabled=QueryAndPhysics,ObjectTypeName="PhysicsBody",CustomResponses=,HelpMessage="Simulating actors",bCanModify=False)
|
||||||
|
-Profiles=(Name="Destructible",CollisionEnabled=QueryAndPhysics,ObjectTypeName="Destructible",CustomResponses=,HelpMessage="Destructible actors",bCanModify=False)
|
||||||
|
-Profiles=(Name="InvisibleWall",CollisionEnabled=QueryAndPhysics,ObjectTypeName="WorldStatic",CustomResponses=((Channel="Visibility",Response=ECR_Ignore)),HelpMessage="WorldStatic object that is invisible.",bCanModify=False)
|
||||||
|
-Profiles=(Name="InvisibleWallDynamic",CollisionEnabled=QueryAndPhysics,ObjectTypeName="WorldDynamic",CustomResponses=((Channel="Visibility",Response=ECR_Ignore)),HelpMessage="WorldDynamic object that is invisible.",bCanModify=False)
|
||||||
|
-Profiles=(Name="Trigger",CollisionEnabled=QueryOnly,ObjectTypeName="WorldDynamic",CustomResponses=((Channel="WorldStatic",Response=ECR_Overlap),(Channel="Pawn",Response=ECR_Overlap),(Channel="Visibility",Response=ECR_Ignore),(Channel="WorldDynamic",Response=ECR_Overlap),(Channel="Camera",Response=ECR_Overlap),(Channel="PhysicsBody",Response=ECR_Overlap),(Channel="Vehicle",Response=ECR_Overlap),(Channel="Destructible",Response=ECR_Overlap)),HelpMessage="WorldDynamic object that is used for trigger. All other channels will be set to default.",bCanModify=False)
|
||||||
|
-Profiles=(Name="Ragdoll",CollisionEnabled=QueryAndPhysics,ObjectTypeName="PhysicsBody",CustomResponses=((Channel="Pawn",Response=ECR_Ignore),(Channel="Visibility",Response=ECR_Ignore)),HelpMessage="Simulating Skeletal Mesh Component. All other channels will be set to default.",bCanModify=False)
|
||||||
|
-Profiles=(Name="Vehicle",CollisionEnabled=QueryAndPhysics,ObjectTypeName="Vehicle",CustomResponses=,HelpMessage="Vehicle object that blocks Vehicle, WorldStatic, and WorldDynamic. All other channels will be set to default.",bCanModify=False)
|
||||||
|
-Profiles=(Name="UI",CollisionEnabled=QueryOnly,ObjectTypeName="WorldDynamic",CustomResponses=((Channel="WorldStatic",Response=ECR_Overlap),(Channel="Pawn",Response=ECR_Overlap),(Channel="Visibility",Response=ECR_Block),(Channel="WorldDynamic",Response=ECR_Overlap),(Channel="Camera",Response=ECR_Overlap),(Channel="PhysicsBody",Response=ECR_Overlap),(Channel="Vehicle",Response=ECR_Overlap),(Channel="Destructible",Response=ECR_Overlap)),HelpMessage="WorldStatic object that overlaps all actors by default. All new custom channels will use its own default response. ",bCanModify=False)
|
||||||
|
-Profiles=(Name="WaterBodyCollision",CollisionEnabled=QueryOnly,bCanModify=False,ObjectTypeName="",CustomResponses=((Channel="WorldDynamic",Response=ECR_Overlap),(Channel="Pawn",Response=ECR_Overlap),(Channel="Visibility",Response=ECR_Ignore),(Channel="Camera",Response=ECR_Ignore),(Channel="PhysicsBody",Response=ECR_Overlap),(Channel="Vehicle",Response=ECR_Overlap),(Channel="Destructible",Response=ECR_Overlap)),HelpMessage="Default Water Collision Profile (Created by Water Plugin)")
|
||||||
|
+Profiles=(Name="NoCollision",CollisionEnabled=NoCollision,bCanModify=False,ObjectTypeName="WorldStatic",CustomResponses=((Channel="Visibility",Response=ECR_Ignore),(Channel="Camera",Response=ECR_Ignore)),HelpMessage="No collision")
|
||||||
|
+Profiles=(Name="BlockAll",CollisionEnabled=QueryAndPhysics,bCanModify=False,ObjectTypeName="WorldStatic",CustomResponses=,HelpMessage="WorldStatic object that blocks all actors by default. All new custom channels will use its own default response. ")
|
||||||
|
+Profiles=(Name="OverlapAll",CollisionEnabled=QueryOnly,bCanModify=False,ObjectTypeName="WorldStatic",CustomResponses=((Channel="WorldStatic",Response=ECR_Overlap),(Channel="Pawn",Response=ECR_Overlap),(Channel="Visibility",Response=ECR_Overlap),(Channel="WorldDynamic",Response=ECR_Overlap),(Channel="Camera",Response=ECR_Overlap),(Channel="PhysicsBody",Response=ECR_Overlap),(Channel="Vehicle",Response=ECR_Overlap),(Channel="Destructible",Response=ECR_Overlap)),HelpMessage="WorldStatic object that overlaps all actors by default. All new custom channels will use its own default response. ")
|
||||||
|
+Profiles=(Name="BlockAllDynamic",CollisionEnabled=QueryAndPhysics,bCanModify=False,ObjectTypeName="WorldDynamic",CustomResponses=,HelpMessage="WorldDynamic object that blocks all actors by default. All new custom channels will use its own default response. ")
|
||||||
|
+Profiles=(Name="OverlapAllDynamic",CollisionEnabled=QueryOnly,bCanModify=False,ObjectTypeName="WorldDynamic",CustomResponses=((Channel="WorldStatic",Response=ECR_Overlap),(Channel="Pawn",Response=ECR_Overlap),(Channel="Visibility",Response=ECR_Overlap),(Channel="WorldDynamic",Response=ECR_Overlap),(Channel="Camera",Response=ECR_Overlap),(Channel="PhysicsBody",Response=ECR_Overlap),(Channel="Vehicle",Response=ECR_Overlap),(Channel="Destructible",Response=ECR_Overlap)),HelpMessage="WorldDynamic object that overlaps all actors by default. All new custom channels will use its own default response. ")
|
||||||
|
+Profiles=(Name="IgnoreOnlyPawn",CollisionEnabled=QueryOnly,bCanModify=False,ObjectTypeName="WorldDynamic",CustomResponses=((Channel="Pawn",Response=ECR_Ignore),(Channel="Vehicle",Response=ECR_Ignore)),HelpMessage="WorldDynamic object that ignores Pawn and Vehicle. All other channels will be set to default.")
|
||||||
|
+Profiles=(Name="OverlapOnlyPawn",CollisionEnabled=QueryOnly,bCanModify=False,ObjectTypeName="WorldDynamic",CustomResponses=((Channel="Pawn",Response=ECR_Overlap),(Channel="Vehicle",Response=ECR_Overlap),(Channel="Camera",Response=ECR_Ignore)),HelpMessage="WorldDynamic object that overlaps Pawn, Camera, and Vehicle. All other channels will be set to default. ")
|
||||||
|
+Profiles=(Name="Pawn",CollisionEnabled=QueryAndPhysics,bCanModify=False,ObjectTypeName="Pawn",CustomResponses=((Channel="Visibility",Response=ECR_Ignore)),HelpMessage="Pawn object. Can be used for capsule of any playerable character or AI. ")
|
||||||
|
+Profiles=(Name="Spectator",CollisionEnabled=QueryOnly,bCanModify=False,ObjectTypeName="Pawn",CustomResponses=((Channel="WorldStatic"),(Channel="Pawn",Response=ECR_Ignore),(Channel="Visibility",Response=ECR_Ignore),(Channel="WorldDynamic",Response=ECR_Ignore),(Channel="Camera",Response=ECR_Ignore),(Channel="PhysicsBody",Response=ECR_Ignore),(Channel="Vehicle",Response=ECR_Ignore),(Channel="Destructible",Response=ECR_Ignore)),HelpMessage="Pawn object that ignores all other actors except WorldStatic.")
|
||||||
|
+Profiles=(Name="CharacterMesh",CollisionEnabled=QueryOnly,bCanModify=False,ObjectTypeName="Pawn",CustomResponses=((Channel="Pawn",Response=ECR_Ignore),(Channel="Vehicle",Response=ECR_Ignore),(Channel="Visibility",Response=ECR_Ignore)),HelpMessage="Pawn object that is used for Character Mesh. All other channels will be set to default.")
|
||||||
|
+Profiles=(Name="PhysicsActor",CollisionEnabled=QueryAndPhysics,bCanModify=False,ObjectTypeName="PhysicsBody",CustomResponses=,HelpMessage="Simulating actors")
|
||||||
|
+Profiles=(Name="Destructible",CollisionEnabled=QueryAndPhysics,bCanModify=False,ObjectTypeName="Destructible",CustomResponses=,HelpMessage="Destructible actors")
|
||||||
|
+Profiles=(Name="InvisibleWall",CollisionEnabled=QueryAndPhysics,bCanModify=False,ObjectTypeName="WorldStatic",CustomResponses=((Channel="Visibility",Response=ECR_Ignore)),HelpMessage="WorldStatic object that is invisible.")
|
||||||
|
+Profiles=(Name="InvisibleWallDynamic",CollisionEnabled=QueryAndPhysics,bCanModify=False,ObjectTypeName="WorldDynamic",CustomResponses=((Channel="Visibility",Response=ECR_Ignore)),HelpMessage="WorldDynamic object that is invisible.")
|
||||||
|
+Profiles=(Name="Trigger",CollisionEnabled=QueryOnly,bCanModify=False,ObjectTypeName="WorldDynamic",CustomResponses=((Channel="WorldStatic",Response=ECR_Overlap),(Channel="Pawn",Response=ECR_Overlap),(Channel="Visibility",Response=ECR_Ignore),(Channel="WorldDynamic",Response=ECR_Overlap),(Channel="Camera",Response=ECR_Overlap),(Channel="PhysicsBody",Response=ECR_Overlap),(Channel="Vehicle",Response=ECR_Overlap),(Channel="Destructible",Response=ECR_Overlap)),HelpMessage="WorldDynamic object that is used for trigger. All other channels will be set to default.")
|
||||||
|
+Profiles=(Name="Ragdoll",CollisionEnabled=QueryAndPhysics,bCanModify=False,ObjectTypeName="PhysicsBody",CustomResponses=((Channel="Pawn",Response=ECR_Ignore),(Channel="Visibility",Response=ECR_Ignore)),HelpMessage="Simulating Skeletal Mesh Component. All other channels will be set to default.")
|
||||||
|
+Profiles=(Name="Vehicle",CollisionEnabled=QueryAndPhysics,bCanModify=False,ObjectTypeName="Vehicle",CustomResponses=,HelpMessage="Vehicle object that blocks Vehicle, WorldStatic, and WorldDynamic. All other channels will be set to default.")
|
||||||
|
+Profiles=(Name="UI",CollisionEnabled=QueryOnly,bCanModify=False,ObjectTypeName="WorldDynamic",CustomResponses=((Channel="WorldStatic",Response=ECR_Overlap),(Channel="Pawn",Response=ECR_Overlap),(Channel="Visibility"),(Channel="WorldDynamic",Response=ECR_Overlap),(Channel="Camera",Response=ECR_Overlap),(Channel="PhysicsBody",Response=ECR_Overlap),(Channel="Vehicle",Response=ECR_Overlap),(Channel="Destructible",Response=ECR_Overlap)),HelpMessage="WorldStatic object that overlaps all actors by default. All new custom channels will use its own default response. ")
|
||||||
|
+Profiles=(Name="WaterBodyCollision",CollisionEnabled=QueryOnly,bCanModify=False,ObjectTypeName="",CustomResponses=((Channel="WorldDynamic",Response=ECR_Overlap),(Channel="Pawn",Response=ECR_Overlap),(Channel="Visibility",Response=ECR_Ignore),(Channel="Camera",Response=ECR_Ignore),(Channel="PhysicsBody",Response=ECR_Overlap),(Channel="Vehicle",Response=ECR_Overlap),(Channel="Destructible",Response=ECR_Overlap)),HelpMessage="Default Water Collision Profile (Created by Water Plugin)")
|
||||||
|
+DefaultChannelResponses=(Channel=ECC_GameTraceChannel1,DefaultResponse=ECR_Block,bTraceType=True,bStaticObject=False,Name="Interactable")
|
||||||
|
-ProfileRedirects=(OldName="BlockingVolume",NewName="InvisibleWall")
|
||||||
|
-ProfileRedirects=(OldName="InterpActor",NewName="IgnoreOnlyPawn")
|
||||||
|
-ProfileRedirects=(OldName="StaticMeshComponent",NewName="BlockAllDynamic")
|
||||||
|
-ProfileRedirects=(OldName="SkeletalMeshActor",NewName="PhysicsActor")
|
||||||
|
-ProfileRedirects=(OldName="InvisibleActor",NewName="InvisibleWallDynamic")
|
||||||
|
+ProfileRedirects=(OldName="BlockingVolume",NewName="InvisibleWall")
|
||||||
|
+ProfileRedirects=(OldName="InterpActor",NewName="IgnoreOnlyPawn")
|
||||||
|
+ProfileRedirects=(OldName="StaticMeshComponent",NewName="BlockAllDynamic")
|
||||||
|
+ProfileRedirects=(OldName="SkeletalMeshActor",NewName="PhysicsActor")
|
||||||
|
+ProfileRedirects=(OldName="InvisibleActor",NewName="InvisibleWallDynamic")
|
||||||
|
-CollisionChannelRedirects=(OldName="Static",NewName="WorldStatic")
|
||||||
|
-CollisionChannelRedirects=(OldName="Dynamic",NewName="WorldDynamic")
|
||||||
|
-CollisionChannelRedirects=(OldName="VehicleMovement",NewName="Vehicle")
|
||||||
|
-CollisionChannelRedirects=(OldName="PawnMovement",NewName="Pawn")
|
||||||
|
+CollisionChannelRedirects=(OldName="Static",NewName="WorldStatic")
|
||||||
|
+CollisionChannelRedirects=(OldName="Dynamic",NewName="WorldDynamic")
|
||||||
|
+CollisionChannelRedirects=(OldName="VehicleMovement",NewName="Vehicle")
|
||||||
|
+CollisionChannelRedirects=(OldName="PawnMovement",NewName="Pawn")
|
||||||
|
|
||||||
[/Script/Engine.Engine]
|
[/Script/Engine.Engine]
|
||||||
+ActiveGameNameRedirects=(OldGameName="TP_ThirdPerson",NewGameName="/Script/KingshearthLegacy")
|
+ActiveGameNameRedirects=(OldGameName="TP_ThirdPerson",NewGameName="/Script/KingshearthLegacy")
|
||||||
+ActiveGameNameRedirects=(OldGameName="/Script/TP_ThirdPerson",NewGameName="/Script/KingshearthLegacy")
|
+ActiveGameNameRedirects=(OldGameName="/Script/TP_ThirdPerson",NewGameName="/Script/KingshearthLegacy")
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
[/Script/EngineSettings.GeneralProjectSettings]
|
[/Script/EngineSettings.GeneralProjectSettings]
|
||||||
ProjectID=CE954E444D2A872317AAC38182169D03
|
ProjectID=CE954E444D2A872317AAC38182169D03
|
||||||
ProjectName=Third Person Game Template
|
ProjectName=Kingsharth Legacy
|
||||||
|
|
||||||
[ConsoleVariables]
|
[ConsoleVariables]
|
||||||
CommonUI.CheckKeyboardFocusAndParentage=1
|
CommonUI.CheckKeyboardFocusAndParentage=1
|
||||||
CommonUI.DisallowUserFocusedWidgetForPendingFocusRecipient=1
|
CommonUI.DisallowUserFocusedWidgetForPendingFocusRecipient=1
|
||||||
CommonUI.FallbackToDesiredOnAutoRestoreFailure=1
|
CommonUI.FallbackToDesiredOnAutoRestoreFailure=1
|
||||||
|
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -42,6 +42,10 @@
|
|||||||
{
|
{
|
||||||
"Name": "EditorToolset",
|
"Name": "EditorToolset",
|
||||||
"Enabled": true
|
"Enabled": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Name": "GameplayAbilities",
|
||||||
|
"Enabled": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,96 @@
|
|||||||
|
#include "HealthAttributeSet.h"
|
||||||
|
#include "GameplayEffectExtension.h"
|
||||||
|
|
||||||
|
UHealthAttributeSet::UHealthAttributeSet()
|
||||||
|
{
|
||||||
|
InitHealth(100.0f);
|
||||||
|
InitMaxHealth(100.0f);
|
||||||
|
}
|
||||||
|
|
||||||
|
void UHealthAttributeSet::PreAttributeChange(const FGameplayAttribute& Attribute, float& NewValue)
|
||||||
|
{
|
||||||
|
Super::PreAttributeChange(Attribute, NewValue);
|
||||||
|
|
||||||
|
if (Attribute == GetHealthAttribute())
|
||||||
|
{
|
||||||
|
NewValue = FMath::Clamp(NewValue, 0.0f, GetMaxHealth());
|
||||||
|
}
|
||||||
|
else if (Attribute == GetMaxHealthAttribute())
|
||||||
|
{
|
||||||
|
NewValue = FMath::Max(NewValue, 0.0f);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void UHealthAttributeSet::PreAttributeBaseChange(const FGameplayAttribute& Attribute, float& NewValue) const
|
||||||
|
{
|
||||||
|
Super::PreAttributeBaseChange(Attribute, NewValue);
|
||||||
|
|
||||||
|
// Instant GameplayEffects (and any future ApplyModToAttribute caller) write BaseValue through this hook,
|
||||||
|
// not PreAttributeChange - without clamping here a large delta can push BaseValue out of range while
|
||||||
|
// PreAttributeChange still clamps the displayed CurrentValue, silently absorbing later deltas. See the
|
||||||
|
// matching comment on UStaminaAttributeSet::PreAttributeBaseChange for how this actually manifested.
|
||||||
|
if (Attribute == GetHealthAttribute())
|
||||||
|
{
|
||||||
|
NewValue = FMath::Clamp(NewValue, 0.0f, GetMaxHealth());
|
||||||
|
}
|
||||||
|
else if (Attribute == GetMaxHealthAttribute())
|
||||||
|
{
|
||||||
|
NewValue = FMath::Max(NewValue, 0.0f);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void UHealthAttributeSet::PostGameplayEffectExecute(const FGameplayEffectModCallbackData& Data)
|
||||||
|
{
|
||||||
|
Super::PostGameplayEffectExecute(Data);
|
||||||
|
|
||||||
|
// Only converts the Damage/Healing meta-attributes into a Health change - this hook fires for GE
|
||||||
|
// "executes" (Instant effects, periodic ticks) only. GE_HealthDebuff is Infinite and non-periodic,
|
||||||
|
// so applying/updating it does NOT fire this - see PostAttributeChange for the hook that does.
|
||||||
|
const FGameplayAttribute& ModifiedAttribute = Data.EvaluatedData.Attribute;
|
||||||
|
|
||||||
|
if (ModifiedAttribute == GetDamageAttribute())
|
||||||
|
{
|
||||||
|
const float DamageDone = GetDamage();
|
||||||
|
SetDamage(0.0f);
|
||||||
|
SetHealth(GetHealth() - DamageDone);
|
||||||
|
}
|
||||||
|
else if (ModifiedAttribute == GetHealingAttribute())
|
||||||
|
{
|
||||||
|
const float HealingDone = GetHealing();
|
||||||
|
SetHealing(0.0f);
|
||||||
|
SetHealth(GetHealth() + HealingDone);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void UHealthAttributeSet::PostAttributeChange(const FGameplayAttribute& Attribute, float OldValue, float NewValue)
|
||||||
|
{
|
||||||
|
Super::PostAttributeChange(Attribute, OldValue, NewValue);
|
||||||
|
|
||||||
|
// Fires for every CurrentValue change regardless of source (GE execute, or - critically - an Infinite
|
||||||
|
// GE like GE_HealthDebuff being applied/removed/re-evaluated, which never triggers PostGameplayEffectExecute).
|
||||||
|
if (Attribute == GetHealthAttribute())
|
||||||
|
{
|
||||||
|
if (!FMath::IsNearlyEqual(OldValue, NewValue))
|
||||||
|
{
|
||||||
|
OnHealthChanged.Broadcast(NewValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (OldValue > 0.0f && NewValue <= 0.0f)
|
||||||
|
{
|
||||||
|
OnDeath.Broadcast();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (Attribute == GetMaxHealthAttribute())
|
||||||
|
{
|
||||||
|
if (!FMath::IsNearlyEqual(OldValue, NewValue))
|
||||||
|
{
|
||||||
|
ClampHealth();
|
||||||
|
OnHealthDebuffChanged.Broadcast(GetHealthDebuff());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void UHealthAttributeSet::ClampHealth()
|
||||||
|
{
|
||||||
|
SetHealth(FMath::Clamp(GetHealth(), 0.0f, GetMaxHealth()));
|
||||||
|
}
|
||||||
@@ -0,0 +1,79 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "CoreMinimal.h"
|
||||||
|
#include "AttributeSet.h"
|
||||||
|
#include "AbilitySystemComponent.h"
|
||||||
|
#include "HealthAttributeSet.generated.h"
|
||||||
|
|
||||||
|
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnHealthChanged, float, NewHealth);
|
||||||
|
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnHealthDebuffChanged, float, NewHealthDebuff);
|
||||||
|
DECLARE_DYNAMIC_MULTICAST_DELEGATE(FOnDeath);
|
||||||
|
|
||||||
|
#define HEALTHATTRIBUTE_ACCESSORS(PropertyName) \
|
||||||
|
GAMEPLAYATTRIBUTE_PROPERTY_GETTER(UHealthAttributeSet, PropertyName) \
|
||||||
|
GAMEPLAYATTRIBUTE_VALUE_GETTER(PropertyName) \
|
||||||
|
GAMEPLAYATTRIBUTE_VALUE_SETTER(PropertyName) \
|
||||||
|
GAMEPLAYATTRIBUTE_VALUE_INITTER(PropertyName)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Health + MaxHealth, replacing UHealthComponent. HealthDebuff no longer exists as its own
|
||||||
|
* attribute - it's GE_HealthDebuff applying a Multiply modifier to MaxHealth, so the "locked"
|
||||||
|
* fraction is just (1 - MaxHealth.CurrentValue / MaxHealth.BaseValue). See GetHealthDebuff()
|
||||||
|
* on AKingshearthLegacyCharacter.
|
||||||
|
*/
|
||||||
|
UCLASS()
|
||||||
|
class KINGSHEARTHLEGACY_API UHealthAttributeSet : public UAttributeSet
|
||||||
|
{
|
||||||
|
GENERATED_BODY()
|
||||||
|
|
||||||
|
public:
|
||||||
|
UHealthAttributeSet();
|
||||||
|
|
||||||
|
UPROPERTY(BlueprintReadOnly, Category = "Health")
|
||||||
|
FGameplayAttributeData Health;
|
||||||
|
HEALTHATTRIBUTE_ACCESSORS(Health)
|
||||||
|
|
||||||
|
/** Nominal cap: BaseValue is the undebuffed max, CurrentValue is what GE_HealthDebuff (and similar) leave after their modifiers. */
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Health")
|
||||||
|
FGameplayAttributeData MaxHealth;
|
||||||
|
HEALTHATTRIBUTE_ACCESSORS(MaxHealth)
|
||||||
|
|
||||||
|
/** Unclamped meta attribute: incoming damage lands here via GE_HealthDamage; PostGameplayEffectExecute converts it into a Health decrease and resets it to 0. Never read directly. */
|
||||||
|
UPROPERTY(BlueprintReadOnly, Category = "Health")
|
||||||
|
FGameplayAttributeData Damage;
|
||||||
|
HEALTHATTRIBUTE_ACCESSORS(Damage)
|
||||||
|
|
||||||
|
/** Unclamped meta attribute: incoming healing lands here via GE_HealthHealing; PostGameplayEffectExecute converts it into a Health increase and resets it to 0. Never read directly. */
|
||||||
|
UPROPERTY(BlueprintReadOnly, Category = "Health")
|
||||||
|
FGameplayAttributeData Healing;
|
||||||
|
HEALTHATTRIBUTE_ACCESSORS(Healing)
|
||||||
|
|
||||||
|
/** Nominal cap, unaffected by debuffs. Feeds the health bar's total width. */
|
||||||
|
float GetNominalMaxHealth() const { return MaxHealth.GetBaseValue(); }
|
||||||
|
|
||||||
|
/** 0-1 fraction of GetNominalMaxHealth() currently locked/unusable (curses, status effects, ...). */
|
||||||
|
float GetHealthDebuff() const
|
||||||
|
{
|
||||||
|
const float NominalMaxHealth = FMath::Max(GetNominalMaxHealth(), KINDA_SMALL_NUMBER);
|
||||||
|
return FMath::Clamp(1.0f - (GetMaxHealth() / NominalMaxHealth), 0.0f, 1.0f);
|
||||||
|
}
|
||||||
|
|
||||||
|
UPROPERTY(BlueprintAssignable, Category = "Health")
|
||||||
|
FOnHealthChanged OnHealthChanged;
|
||||||
|
|
||||||
|
UPROPERTY(BlueprintAssignable, Category = "Health")
|
||||||
|
FOnHealthDebuffChanged OnHealthDebuffChanged;
|
||||||
|
|
||||||
|
UPROPERTY(BlueprintAssignable, Category = "Health")
|
||||||
|
FOnDeath OnDeath;
|
||||||
|
|
||||||
|
virtual void PreAttributeChange(const FGameplayAttribute& Attribute, float& NewValue) override;
|
||||||
|
virtual void PreAttributeBaseChange(const FGameplayAttribute& Attribute, float& NewValue) const override;
|
||||||
|
virtual void PostGameplayEffectExecute(const FGameplayEffectModCallbackData& Data) override;
|
||||||
|
virtual void PostAttributeChange(const FGameplayAttribute& Attribute, float OldValue, float NewValue) override;
|
||||||
|
|
||||||
|
private:
|
||||||
|
void ClampHealth();
|
||||||
|
};
|
||||||
|
|
||||||
|
#undef HEALTHATTRIBUTE_ACCESSORS
|
||||||
@@ -0,0 +1,86 @@
|
|||||||
|
#include "StaminaAttributeSet.h"
|
||||||
|
|
||||||
|
UStaminaAttributeSet::UStaminaAttributeSet()
|
||||||
|
{
|
||||||
|
InitStamina(100.0f);
|
||||||
|
InitMaxStamina(100.0f);
|
||||||
|
InitFatigue(0.0f);
|
||||||
|
}
|
||||||
|
|
||||||
|
void UStaminaAttributeSet::PreAttributeChange(const FGameplayAttribute& Attribute, float& NewValue)
|
||||||
|
{
|
||||||
|
Super::PreAttributeChange(Attribute, NewValue);
|
||||||
|
|
||||||
|
if (Attribute == GetStaminaAttribute())
|
||||||
|
{
|
||||||
|
NewValue = FMath::Clamp(NewValue, 0.0f, GetMaxStamina());
|
||||||
|
}
|
||||||
|
else if (Attribute == GetMaxStaminaAttribute())
|
||||||
|
{
|
||||||
|
NewValue = FMath::Max(NewValue, 0.0f);
|
||||||
|
}
|
||||||
|
else if (Attribute == GetFatigueAttribute())
|
||||||
|
{
|
||||||
|
NewValue = FMath::Clamp(NewValue, 0.0f, 1.0f);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void UStaminaAttributeSet::PreAttributeBaseChange(const FGameplayAttribute& Attribute, float& NewValue) const
|
||||||
|
{
|
||||||
|
Super::PreAttributeBaseChange(Attribute, NewValue);
|
||||||
|
|
||||||
|
// ApplyModToAttribute (SpendStamina/RestoreStamina/regen) writes BaseValue through this hook, not
|
||||||
|
// PreAttributeChange - without clamping here, a large delta (e.g. RestoreStamina's debug "fill to max")
|
||||||
|
// pushes BaseValue past MaxStamina. PreAttributeChange still clamps the displayed CurrentValue, but the
|
||||||
|
// bloated BaseValue then absorbs every subsequent spend/regen delta before CurrentValue moves again.
|
||||||
|
if (Attribute == GetStaminaAttribute())
|
||||||
|
{
|
||||||
|
NewValue = FMath::Clamp(NewValue, 0.0f, GetMaxStamina());
|
||||||
|
}
|
||||||
|
else if (Attribute == GetMaxStaminaAttribute())
|
||||||
|
{
|
||||||
|
NewValue = FMath::Max(NewValue, 0.0f);
|
||||||
|
}
|
||||||
|
else if (Attribute == GetFatigueAttribute())
|
||||||
|
{
|
||||||
|
NewValue = FMath::Clamp(NewValue, 0.0f, 1.0f);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void UStaminaAttributeSet::PostAttributeChange(const FGameplayAttribute& Attribute, float OldValue, float NewValue)
|
||||||
|
{
|
||||||
|
Super::PostAttributeChange(Attribute, OldValue, NewValue);
|
||||||
|
|
||||||
|
if (Attribute == GetStaminaAttribute())
|
||||||
|
{
|
||||||
|
if (!FMath::IsNearlyEqual(OldValue, NewValue))
|
||||||
|
{
|
||||||
|
OnStaminaChanged.Broadcast(NewValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (OldValue > 0.0f && NewValue <= 0.0f)
|
||||||
|
{
|
||||||
|
OnExhausted.Broadcast();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (Attribute == GetMaxStaminaAttribute())
|
||||||
|
{
|
||||||
|
if (!FMath::IsNearlyEqual(OldValue, NewValue))
|
||||||
|
{
|
||||||
|
// MaxStamina shrank/grew (typically GE_StaminaFatiguePenalty re-evaluating) - pull Stamina back in range.
|
||||||
|
ClampStamina();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (Attribute == GetFatigueAttribute())
|
||||||
|
{
|
||||||
|
if (!FMath::IsNearlyEqual(OldValue, NewValue))
|
||||||
|
{
|
||||||
|
OnFatigueChanged.Broadcast(NewValue);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void UStaminaAttributeSet::ClampStamina()
|
||||||
|
{
|
||||||
|
SetStamina(FMath::Clamp(GetStamina(), 0.0f, GetMaxStamina()));
|
||||||
|
}
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "CoreMinimal.h"
|
||||||
|
#include "AttributeSet.h"
|
||||||
|
#include "AbilitySystemComponent.h"
|
||||||
|
#include "StaminaAttributeSet.generated.h"
|
||||||
|
|
||||||
|
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnStaminaChanged, float, NewStamina);
|
||||||
|
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnFatigueChanged, float, NewFatigue);
|
||||||
|
DECLARE_DYNAMIC_MULTICAST_DELEGATE(FOnExhausted);
|
||||||
|
|
||||||
|
#define STAMINAATTRIBUTE_ACCESSORS(PropertyName) \
|
||||||
|
GAMEPLAYATTRIBUTE_PROPERTY_GETTER(UStaminaAttributeSet, PropertyName) \
|
||||||
|
GAMEPLAYATTRIBUTE_VALUE_GETTER(PropertyName) \
|
||||||
|
GAMEPLAYATTRIBUTE_VALUE_SETTER(PropertyName) \
|
||||||
|
GAMEPLAYATTRIBUTE_VALUE_INITTER(PropertyName)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stamina + MaxStamina + Fatigue, replacing UStaminaComponent. Unlike Health, all three attributes
|
||||||
|
* are mutated directly (via AbilitySystemComponent::ApplyModToAttribute from the character's tick,
|
||||||
|
* see SpendStamina/TickStaminaRegen) rather than through discrete GameplayEffects - there's no
|
||||||
|
* damage-type/resistance concept for stamina spend to warrant Health's meta-attribute indirection.
|
||||||
|
* The one thing that IS a real GameplayEffect is GE_StaminaFatiguePenalty: an always-on Infinite
|
||||||
|
* effect (applied once in BeginPlay) that uses MMC_MaxStaminaFatiguePenalty to keep MaxStamina's
|
||||||
|
* CurrentValue equal to BaseValue * (1 - Fatigue), recalculated automatically by GAS's own
|
||||||
|
* aggregator whenever Fatigue changes, replacing the old hand-written GetEffectiveMaxStamina().
|
||||||
|
*
|
||||||
|
* Broadcasts live in PostAttributeChange (fires for ANY mutation path, not just GameplayEffect
|
||||||
|
* execution) specifically so the Fatigue-driven MaxStamina recalculation broadcasts correctly too.
|
||||||
|
*/
|
||||||
|
UCLASS()
|
||||||
|
class KINGSHEARTHLEGACY_API UStaminaAttributeSet : public UAttributeSet
|
||||||
|
{
|
||||||
|
GENERATED_BODY()
|
||||||
|
|
||||||
|
public:
|
||||||
|
UStaminaAttributeSet();
|
||||||
|
|
||||||
|
UPROPERTY(BlueprintReadOnly, Category = "Stamina")
|
||||||
|
FGameplayAttributeData Stamina;
|
||||||
|
STAMINAATTRIBUTE_ACCESSORS(Stamina)
|
||||||
|
|
||||||
|
/** Nominal cap: BaseValue is the undebuffed max, CurrentValue is what GE_StaminaFatiguePenalty leaves after Fatigue. */
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Stamina")
|
||||||
|
FGameplayAttributeData MaxStamina;
|
||||||
|
STAMINAATTRIBUTE_ACCESSORS(MaxStamina)
|
||||||
|
|
||||||
|
/** 0-1 fraction of MaxStamina currently locked away due to over-extension. */
|
||||||
|
UPROPERTY(BlueprintReadOnly, Category = "Fatigue")
|
||||||
|
FGameplayAttributeData Fatigue;
|
||||||
|
STAMINAATTRIBUTE_ACCESSORS(Fatigue)
|
||||||
|
|
||||||
|
/** Nominal cap, unaffected by Fatigue. Feeds the stamina bar's total width. */
|
||||||
|
float GetNominalMaxStamina() const { return MaxStamina.GetBaseValue(); }
|
||||||
|
|
||||||
|
UPROPERTY(BlueprintAssignable, Category = "Stamina")
|
||||||
|
FOnStaminaChanged OnStaminaChanged;
|
||||||
|
|
||||||
|
UPROPERTY(BlueprintAssignable, Category = "Fatigue")
|
||||||
|
FOnFatigueChanged OnFatigueChanged;
|
||||||
|
|
||||||
|
/** Broadcast when Stamina is driven from a positive value to empty. */
|
||||||
|
UPROPERTY(BlueprintAssignable, Category = "Stamina")
|
||||||
|
FOnExhausted OnExhausted;
|
||||||
|
|
||||||
|
virtual void PreAttributeChange(const FGameplayAttribute& Attribute, float& NewValue) override;
|
||||||
|
virtual void PreAttributeBaseChange(const FGameplayAttribute& Attribute, float& NewValue) const override;
|
||||||
|
virtual void PostAttributeChange(const FGameplayAttribute& Attribute, float OldValue, float NewValue) override;
|
||||||
|
|
||||||
|
private:
|
||||||
|
void ClampStamina();
|
||||||
|
};
|
||||||
|
|
||||||
|
#undef STAMINAATTRIBUTE_ACCESSORS
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
#include "GE_HealthDamage.h"
|
||||||
|
#include "HealthAttributeSet.h"
|
||||||
|
#include "KingshearthLegacyGameplayTags.h"
|
||||||
|
|
||||||
|
UGE_HealthDamage::UGE_HealthDamage()
|
||||||
|
{
|
||||||
|
DurationPolicy = EGameplayEffectDurationType::Instant;
|
||||||
|
|
||||||
|
FSetByCallerFloat SetByCaller;
|
||||||
|
SetByCaller.DataTag = TAG_Data_Health_Damage.GetTag();
|
||||||
|
|
||||||
|
FGameplayModifierInfo Modifier;
|
||||||
|
Modifier.Attribute = UHealthAttributeSet::GetDamageAttribute();
|
||||||
|
Modifier.ModifierOp = EGameplayModOp::Additive;
|
||||||
|
Modifier.ModifierMagnitude = FGameplayEffectModifierMagnitude(SetByCaller);
|
||||||
|
|
||||||
|
Modifiers.Add(Modifier);
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "CoreMinimal.h"
|
||||||
|
#include "GameplayEffect.h"
|
||||||
|
#include "GE_HealthDamage.generated.h"
|
||||||
|
|
||||||
|
/** Instant effect: adds a caller-specified amount (SetByCaller TAG_Data_Health_Damage) to HealthAttributeSet::Damage. */
|
||||||
|
UCLASS()
|
||||||
|
class KINGSHEARTHLEGACY_API UGE_HealthDamage : public UGameplayEffect
|
||||||
|
{
|
||||||
|
GENERATED_BODY()
|
||||||
|
|
||||||
|
public:
|
||||||
|
UGE_HealthDamage();
|
||||||
|
};
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
#include "GE_HealthDebuff.h"
|
||||||
|
#include "HealthAttributeSet.h"
|
||||||
|
#include "KingshearthLegacyGameplayTags.h"
|
||||||
|
|
||||||
|
UGE_HealthDebuff::UGE_HealthDebuff()
|
||||||
|
{
|
||||||
|
DurationPolicy = EGameplayEffectDurationType::Infinite;
|
||||||
|
|
||||||
|
FSetByCallerFloat SetByCaller;
|
||||||
|
SetByCaller.DataTag = TAG_Data_HealthDebuff_Multiplier.GetTag();
|
||||||
|
|
||||||
|
FGameplayModifierInfo Modifier;
|
||||||
|
Modifier.Attribute = UHealthAttributeSet::GetMaxHealthAttribute();
|
||||||
|
Modifier.ModifierOp = EGameplayModOp::MultiplyAdditive;
|
||||||
|
Modifier.ModifierMagnitude = FGameplayEffectModifierMagnitude(SetByCaller);
|
||||||
|
|
||||||
|
Modifiers.Add(Modifier);
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "CoreMinimal.h"
|
||||||
|
#include "GameplayEffect.h"
|
||||||
|
#include "GE_HealthDebuff.generated.h"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Infinite effect: multiplies MaxHealth's CurrentValue by a caller-specified fraction
|
||||||
|
* (SetByCaller TAG_Data_HealthDebuff_Multiplier, e.g. 0.7 to lock away 30%). Stands in for
|
||||||
|
* a future curse/status-effect system - AKingshearthLegacyCharacter::SetHealthDebuff applies/
|
||||||
|
* removes one instance of this, tracked by its FActiveGameplayEffectHandle.
|
||||||
|
*/
|
||||||
|
UCLASS()
|
||||||
|
class KINGSHEARTHLEGACY_API UGE_HealthDebuff : public UGameplayEffect
|
||||||
|
{
|
||||||
|
GENERATED_BODY()
|
||||||
|
|
||||||
|
public:
|
||||||
|
UGE_HealthDebuff();
|
||||||
|
};
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
#include "GE_HealthHealing.h"
|
||||||
|
#include "HealthAttributeSet.h"
|
||||||
|
#include "KingshearthLegacyGameplayTags.h"
|
||||||
|
|
||||||
|
UGE_HealthHealing::UGE_HealthHealing()
|
||||||
|
{
|
||||||
|
DurationPolicy = EGameplayEffectDurationType::Instant;
|
||||||
|
|
||||||
|
FSetByCallerFloat SetByCaller;
|
||||||
|
SetByCaller.DataTag = TAG_Data_Health_Healing.GetTag();
|
||||||
|
|
||||||
|
FGameplayModifierInfo Modifier;
|
||||||
|
Modifier.Attribute = UHealthAttributeSet::GetHealingAttribute();
|
||||||
|
Modifier.ModifierOp = EGameplayModOp::Additive;
|
||||||
|
Modifier.ModifierMagnitude = FGameplayEffectModifierMagnitude(SetByCaller);
|
||||||
|
|
||||||
|
Modifiers.Add(Modifier);
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "CoreMinimal.h"
|
||||||
|
#include "GameplayEffect.h"
|
||||||
|
#include "GE_HealthHealing.generated.h"
|
||||||
|
|
||||||
|
/** Instant effect: adds a caller-specified amount (SetByCaller TAG_Data_Health_Healing) to HealthAttributeSet::Healing. */
|
||||||
|
UCLASS()
|
||||||
|
class KINGSHEARTHLEGACY_API UGE_HealthHealing : public UGameplayEffect
|
||||||
|
{
|
||||||
|
GENERATED_BODY()
|
||||||
|
|
||||||
|
public:
|
||||||
|
UGE_HealthHealing();
|
||||||
|
};
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
#include "GE_StaminaFatiguePenalty.h"
|
||||||
|
#include "StaminaAttributeSet.h"
|
||||||
|
#include "MMC_MaxStaminaFatiguePenalty.h"
|
||||||
|
|
||||||
|
UGE_StaminaFatiguePenalty::UGE_StaminaFatiguePenalty()
|
||||||
|
{
|
||||||
|
DurationPolicy = EGameplayEffectDurationType::Infinite;
|
||||||
|
|
||||||
|
FCustomCalculationBasedFloat CustomCalculation;
|
||||||
|
CustomCalculation.CalculationClassMagnitude = UMMC_MaxStaminaFatiguePenalty::StaticClass();
|
||||||
|
|
||||||
|
FGameplayModifierInfo Modifier;
|
||||||
|
Modifier.Attribute = UStaminaAttributeSet::GetMaxStaminaAttribute();
|
||||||
|
Modifier.ModifierOp = EGameplayModOp::MultiplyAdditive;
|
||||||
|
Modifier.ModifierMagnitude = FGameplayEffectModifierMagnitude(CustomCalculation);
|
||||||
|
|
||||||
|
Modifiers.Add(Modifier);
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "CoreMinimal.h"
|
||||||
|
#include "GameplayEffect.h"
|
||||||
|
#include "GE_StaminaFatiguePenalty.generated.h"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Always-on Infinite effect applied once in AKingshearthLegacyCharacter::BeginPlay: multiplies
|
||||||
|
* MaxStamina's CurrentValue by MMC_MaxStaminaFatiguePenalty's (1 - Fatigue), so MaxStamina tracks
|
||||||
|
* Fatigue automatically via GAS's aggregator - replaces the old hand-written GetEffectiveMaxStamina().
|
||||||
|
*/
|
||||||
|
UCLASS()
|
||||||
|
class KINGSHEARTHLEGACY_API UGE_StaminaFatiguePenalty : public UGameplayEffect
|
||||||
|
{
|
||||||
|
GENERATED_BODY()
|
||||||
|
|
||||||
|
public:
|
||||||
|
UGE_StaminaFatiguePenalty();
|
||||||
|
};
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
#include "MMC_MaxStaminaFatiguePenalty.h"
|
||||||
|
#include "StaminaAttributeSet.h"
|
||||||
|
|
||||||
|
UMMC_MaxStaminaFatiguePenalty::UMMC_MaxStaminaFatiguePenalty()
|
||||||
|
{
|
||||||
|
FatigueCaptureDef = FGameplayEffectAttributeCaptureDefinition(
|
||||||
|
UStaminaAttributeSet::GetFatigueAttribute(),
|
||||||
|
EGameplayEffectAttributeCaptureSource::Target,
|
||||||
|
false);
|
||||||
|
|
||||||
|
RelevantAttributesToCapture.Add(FatigueCaptureDef);
|
||||||
|
}
|
||||||
|
|
||||||
|
float UMMC_MaxStaminaFatiguePenalty::CalculateBaseMagnitude_Implementation(const FGameplayEffectSpec& Spec) const
|
||||||
|
{
|
||||||
|
float Fatigue = 0.0f;
|
||||||
|
GetCapturedAttributeMagnitude(FatigueCaptureDef, Spec, FAggregatorEvaluateParameters(), Fatigue);
|
||||||
|
|
||||||
|
return 1.0f - FMath::Clamp(Fatigue, 0.0f, 1.0f);
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "CoreMinimal.h"
|
||||||
|
#include "GameplayModMagnitudeCalculation.h"
|
||||||
|
#include "MMC_MaxStaminaFatiguePenalty.generated.h"
|
||||||
|
|
||||||
|
/** Captures Fatigue (live, non-snapshot) and returns (1 - Fatigue) as the Multiply(Additive) magnitude for GE_StaminaFatiguePenalty's MaxStamina modifier. */
|
||||||
|
UCLASS()
|
||||||
|
class KINGSHEARTHLEGACY_API UMMC_MaxStaminaFatiguePenalty : public UGameplayModMagnitudeCalculation
|
||||||
|
{
|
||||||
|
GENERATED_BODY()
|
||||||
|
|
||||||
|
public:
|
||||||
|
UMMC_MaxStaminaFatiguePenalty();
|
||||||
|
|
||||||
|
virtual float CalculateBaseMagnitude_Implementation(const FGameplayEffectSpec& Spec) const override;
|
||||||
|
|
||||||
|
private:
|
||||||
|
FGameplayEffectAttributeCaptureDefinition FatigueCaptureDef;
|
||||||
|
};
|
||||||
@@ -1,65 +0,0 @@
|
|||||||
#include "HealthComponent.h"
|
|
||||||
|
|
||||||
UHealthComponent::UHealthComponent()
|
|
||||||
{
|
|
||||||
PrimaryComponentTick.bCanEverTick = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
void UHealthComponent::BeginPlay()
|
|
||||||
{
|
|
||||||
Super::BeginPlay();
|
|
||||||
|
|
||||||
CurrentHealth = MaxHealth;
|
|
||||||
}
|
|
||||||
|
|
||||||
float UHealthComponent::GetHealth() const
|
|
||||||
{
|
|
||||||
return CurrentHealth;
|
|
||||||
}
|
|
||||||
|
|
||||||
float UHealthComponent::GetMaxHealth() const
|
|
||||||
{
|
|
||||||
return MaxHealth;
|
|
||||||
}
|
|
||||||
|
|
||||||
void UHealthComponent::SetMaxHealth(float NewMaxHealth)
|
|
||||||
{
|
|
||||||
MaxHealth = FMath::Max(NewMaxHealth, 0.0f);
|
|
||||||
|
|
||||||
CurrentHealth = FMath::Clamp(CurrentHealth, 0.0f, MaxHealth);
|
|
||||||
}
|
|
||||||
|
|
||||||
void UHealthComponent::SetHealth(float NewHealth)
|
|
||||||
{
|
|
||||||
const float PreviousHealth = CurrentHealth;
|
|
||||||
|
|
||||||
CurrentHealth = FMath::Clamp(NewHealth, 0.0f, MaxHealth);
|
|
||||||
|
|
||||||
OnHealthChanged.Broadcast(CurrentHealth);
|
|
||||||
|
|
||||||
// Only trigger death when crossing from alive to dead.
|
|
||||||
if (PreviousHealth > 0.0f && CurrentHealth <= 0.0f)
|
|
||||||
{
|
|
||||||
OnDeath.Broadcast();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void UHealthComponent::AddHealth(float Amount)
|
|
||||||
{
|
|
||||||
if (Amount <= 0.0f)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
SetHealth(CurrentHealth + Amount);
|
|
||||||
}
|
|
||||||
|
|
||||||
void UHealthComponent::RemoveHealth(float Amount)
|
|
||||||
{
|
|
||||||
if (Amount <= 0.0f)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
SetHealth(CurrentHealth - Amount);
|
|
||||||
}
|
|
||||||
@@ -1,54 +0,0 @@
|
|||||||
#pragma once
|
|
||||||
|
|
||||||
#include "CoreMinimal.h"
|
|
||||||
#include "Components/ActorComponent.h"
|
|
||||||
#include "HealthComponent.generated.h"
|
|
||||||
|
|
||||||
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnHealthChanged, float, NewHealth);
|
|
||||||
DECLARE_DYNAMIC_MULTICAST_DELEGATE(FOnDeath);
|
|
||||||
|
|
||||||
UCLASS(ClassGroup = (Custom), meta = (BlueprintSpawnableComponent))
|
|
||||||
class KINGSHEARTHLEGACY_API UHealthComponent : public UActorComponent
|
|
||||||
{
|
|
||||||
GENERATED_BODY()
|
|
||||||
|
|
||||||
public:
|
|
||||||
UHealthComponent();
|
|
||||||
|
|
||||||
protected:
|
|
||||||
virtual void BeginPlay() override;
|
|
||||||
|
|
||||||
public:
|
|
||||||
// Health
|
|
||||||
UFUNCTION(BlueprintPure, Category = "Health")
|
|
||||||
float GetHealth() const;
|
|
||||||
|
|
||||||
UFUNCTION(BlueprintPure, Category = "Health")
|
|
||||||
float GetMaxHealth() const;
|
|
||||||
|
|
||||||
UFUNCTION(BlueprintCallable, Category = "Health")
|
|
||||||
void SetMaxHealth(float NewMaxHealth);
|
|
||||||
|
|
||||||
UFUNCTION(BlueprintCallable, Category = "Health")
|
|
||||||
void SetHealth(float NewHealth);
|
|
||||||
|
|
||||||
UFUNCTION(BlueprintCallable, Category = "Health")
|
|
||||||
void AddHealth(float Amount);
|
|
||||||
|
|
||||||
UFUNCTION(BlueprintCallable, Category = "Health")
|
|
||||||
void RemoveHealth(float Amount);
|
|
||||||
|
|
||||||
protected:
|
|
||||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Health")
|
|
||||||
float MaxHealth = 100.0f;
|
|
||||||
|
|
||||||
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Health")
|
|
||||||
float CurrentHealth = 100.0f;
|
|
||||||
|
|
||||||
public:
|
|
||||||
UPROPERTY(BlueprintAssignable, Category = "Health")
|
|
||||||
FOnHealthChanged OnHealthChanged;
|
|
||||||
|
|
||||||
UPROPERTY(BlueprintAssignable, Category = "Health")
|
|
||||||
FOnDeath OnDeath;
|
|
||||||
};
|
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
// Copyright Epic Games, Inc. All Rights Reserved.
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "CoreMinimal.h"
|
||||||
|
#include "UObject/Interface.h"
|
||||||
|
#include "InteractionTypes.h"
|
||||||
|
#include "Interactable.generated.h"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generic interaction interface. Blueprintable so pure-Blueprint actors
|
||||||
|
* (BP_DoorFrame, BP_JumpPad, ...) can implement it without any C++.
|
||||||
|
*/
|
||||||
|
UINTERFACE(MinimalAPI, Blueprintable)
|
||||||
|
class UInteractable : public UInterface
|
||||||
|
{
|
||||||
|
GENERATED_BODY()
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Implement this and return interaction data - the InteractionComponent decides
|
||||||
|
* on its own whether that means a direct tap, a hold, or a Context Menu.
|
||||||
|
*/
|
||||||
|
class IInteractable
|
||||||
|
{
|
||||||
|
GENERATED_BODY()
|
||||||
|
|
||||||
|
public:
|
||||||
|
|
||||||
|
/** Returns the interactions currently available to Interactor, in priority order (index 0 = primary/tap). */
|
||||||
|
UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category = "Interaction")
|
||||||
|
TArray<FInteractionOption> GetInteractionOptions(AActor* Interactor);
|
||||||
|
|
||||||
|
/** Friendly object name for prompts, e.g. "Berry Bush", "Wooden Chest", "Workbench". Optional - an empty default just drops the "with X" part of the prompt. */
|
||||||
|
UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category = "Interaction")
|
||||||
|
FText GetInteractableDisplayName();
|
||||||
|
|
||||||
|
/** Runs the interaction identified by ActionID (one of the ActionIDs previously returned by GetInteractionOptions). */
|
||||||
|
UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category = "Interaction")
|
||||||
|
void ExecuteInteraction(AActor* Interactor, FName ActionID);
|
||||||
|
};
|
||||||
@@ -0,0 +1,258 @@
|
|||||||
|
// Copyright Epic Games, Inc. All Rights Reserved.
|
||||||
|
|
||||||
|
#include "InteractionComponent.h"
|
||||||
|
#include "Interactable.h"
|
||||||
|
#include "InteractionLookupProvider.h"
|
||||||
|
#include "Engine/World.h"
|
||||||
|
#include "TimerManager.h"
|
||||||
|
#include "GameFramework/Actor.h"
|
||||||
|
#include "GameFramework/Pawn.h"
|
||||||
|
#include "GameFramework/Controller.h"
|
||||||
|
#include "DrawDebugHelpers.h"
|
||||||
|
#include "KingshearthLegacy.h"
|
||||||
|
|
||||||
|
UInteractionComponent::UInteractionComponent()
|
||||||
|
{
|
||||||
|
PrimaryComponentTick.bCanEverTick = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void UInteractionComponent::BeginPlay()
|
||||||
|
{
|
||||||
|
Super::BeginPlay();
|
||||||
|
UpdateFocus();
|
||||||
|
}
|
||||||
|
|
||||||
|
void UInteractionComponent::TickComponent(float DeltaTime, ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction)
|
||||||
|
{
|
||||||
|
Super::TickComponent(DeltaTime, TickType, ThisTickFunction);
|
||||||
|
UpdateFocus();
|
||||||
|
UpdateHoldProgress();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool UInteractionComponent::PerformInteractionTrace(FHitResult& OutHit) const
|
||||||
|
{
|
||||||
|
AActor* Owner = GetOwner();
|
||||||
|
UWorld* World = GetWorld();
|
||||||
|
if (!Owner || !World)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Owner->Implements<UInteractionLookupProvider>())
|
||||||
|
{
|
||||||
|
// Authoritative once implemented - a miss here means "nothing found", full stop.
|
||||||
|
// Don't silently fall through to the default trace below just because this one
|
||||||
|
// came back empty; that would use a completely different (un-offset) trace and
|
||||||
|
// could contradict what the override deliberately decided.
|
||||||
|
return IInteractionLookupProvider::Execute_InteractionLookupLoop(Owner, OutHit);
|
||||||
|
}
|
||||||
|
|
||||||
|
FVector TraceStart = Owner->GetActorLocation();
|
||||||
|
FRotator TraceRotation = Owner->GetActorRotation();
|
||||||
|
|
||||||
|
if (const APawn* OwnerPawn = Cast<APawn>(Owner))
|
||||||
|
{
|
||||||
|
if (const AController* Controller = OwnerPawn->GetController())
|
||||||
|
{
|
||||||
|
Controller->GetPlayerViewPoint(TraceStart, TraceRotation);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
TraceStart = OwnerPawn->GetPawnViewLocation();
|
||||||
|
TraceRotation = OwnerPawn->GetViewRotation();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const FVector TraceEnd = TraceStart + TraceRotation.Vector() * InteractionRange;
|
||||||
|
|
||||||
|
FCollisionQueryParams QueryParams;
|
||||||
|
QueryParams.AddIgnoredActor(Owner);
|
||||||
|
|
||||||
|
const bool bHit = World->LineTraceSingleByChannel(OutHit, TraceStart, TraceEnd, InteractionTraceChannel, QueryParams);
|
||||||
|
|
||||||
|
#if ENABLE_DRAW_DEBUG
|
||||||
|
if (bShowDebugTraces)
|
||||||
|
{
|
||||||
|
const bool bHitInteractable = bHit && OutHit.GetActor() && OutHit.GetActor()->Implements<UInteractable>();
|
||||||
|
DrawDebugLine(World, TraceStart, bHit ? OutHit.Location : TraceEnd, bHitInteractable ? FColor::Green : FColor::Red, false, 0.0f, 0, 1.5f);
|
||||||
|
if (bHit)
|
||||||
|
{
|
||||||
|
DrawDebugSphere(World, OutHit.Location, 8.0f, 12, bHitInteractable ? FColor::Green : FColor::Red, false, 0.0f);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
return bHit;
|
||||||
|
}
|
||||||
|
|
||||||
|
void UInteractionComponent::UpdateFocus()
|
||||||
|
{
|
||||||
|
FHitResult Hit;
|
||||||
|
const bool bHit = PerformInteractionTrace(Hit);
|
||||||
|
const bool bImplementsInteractable = bHit && Hit.GetActor() && Hit.GetActor()->Implements<UInteractable>();
|
||||||
|
AActor* NewFocusedActor = bImplementsInteractable ? Hit.GetActor() : nullptr;
|
||||||
|
|
||||||
|
#if ENABLE_DRAW_DEBUG
|
||||||
|
if (bShowDebugTraces)
|
||||||
|
{
|
||||||
|
UE_LOG(LogKingshearthLegacy, Log, TEXT("[Interaction] Tick: bHit=%s hitActor='%s' implementsInteractable=%s currentFocused='%s'"),
|
||||||
|
bHit ? TEXT("true") : TEXT("false"),
|
||||||
|
*GetNameSafe(bHit ? Hit.GetActor() : nullptr),
|
||||||
|
bImplementsInteractable ? TEXT("true") : TEXT("false"),
|
||||||
|
*GetNameSafe(FocusedActor.Get()));
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
if (NewFocusedActor == FocusedActor.Get())
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
FocusedActor = NewFocusedActor;
|
||||||
|
FocusedOptions = NewFocusedActor ? IInteractable::Execute_GetInteractionOptions(NewFocusedActor, GetOwner()) : TArray<FInteractionOption>();
|
||||||
|
|
||||||
|
#if ENABLE_DRAW_DEBUG
|
||||||
|
if (bShowDebugTraces)
|
||||||
|
{
|
||||||
|
UE_LOG(LogKingshearthLegacy, Log, TEXT("[Interaction] Focus changed to '%s' (%d option(s))"), *GetNameSafe(NewFocusedActor), FocusedOptions.Num());
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
OnFocusedInteractableChanged.Broadcast(NewFocusedActor, FocusedOptions);
|
||||||
|
}
|
||||||
|
|
||||||
|
void UInteractionComponent::UpdateHoldProgress()
|
||||||
|
{
|
||||||
|
const bool bTimerActive = GetWorld()->GetTimerManager().IsTimerActive(HoldTimerHandle);
|
||||||
|
const float Progress = bTimerActive && HoldThreshold > 0.0f
|
||||||
|
? FMath::Clamp(GetWorld()->GetTimerManager().GetTimerElapsed(HoldTimerHandle) / HoldThreshold, 0.0f, 1.0f)
|
||||||
|
: 0.0f;
|
||||||
|
|
||||||
|
if (FMath::IsNearlyEqual(Progress, LastBroadcastHoldProgress, KINDA_SMALL_NUMBER))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
LastBroadcastHoldProgress = Progress;
|
||||||
|
OnInteractHoldProgressChanged.Broadcast(Progress);
|
||||||
|
}
|
||||||
|
|
||||||
|
void UInteractionComponent::TryBeginInteract()
|
||||||
|
{
|
||||||
|
UpdateFocus();
|
||||||
|
|
||||||
|
if (!FocusedActor.IsValid() || FocusedOptions.Num() == 0)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
CurrentTarget = FocusedActor;
|
||||||
|
CurrentOptions = FocusedOptions;
|
||||||
|
HighlightedIndex = 0;
|
||||||
|
bHoldConsumed = false;
|
||||||
|
|
||||||
|
if (CurrentOptions.Num() >= 2)
|
||||||
|
{
|
||||||
|
GetWorld()->GetTimerManager().SetTimer(HoldTimerHandle, this, &UInteractionComponent::HandleHoldThresholdReached, HoldThreshold, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void UInteractionComponent::TryEndInteract()
|
||||||
|
{
|
||||||
|
GetWorld()->GetTimerManager().ClearTimer(HoldTimerHandle);
|
||||||
|
|
||||||
|
if (bContextMenuOpen)
|
||||||
|
{
|
||||||
|
ExecuteOption(HighlightedIndex);
|
||||||
|
RefreshFocusedOptions();
|
||||||
|
CloseContextMenu();
|
||||||
|
}
|
||||||
|
else if (!bHoldConsumed && CurrentOptions.Num() >= 1)
|
||||||
|
{
|
||||||
|
ExecuteOption(0);
|
||||||
|
RefreshFocusedOptions();
|
||||||
|
}
|
||||||
|
|
||||||
|
ClearCachedTarget();
|
||||||
|
}
|
||||||
|
|
||||||
|
void UInteractionComponent::CycleContextMenuOptions(float Direction)
|
||||||
|
{
|
||||||
|
if (!bContextMenuOpen || CurrentOptions.Num() == 0 || FMath::IsNearlyZero(Direction))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Scrolling up (positive Direction) moves the highlight toward the top of the list (index 0),
|
||||||
|
// matching how the mouse wheel scrolls content elsewhere - hence the flipped sign here.
|
||||||
|
const int32 Step = Direction > 0.0f ? -1 : 1;
|
||||||
|
const int32 NewHighlightedIndex = FMath::Clamp(HighlightedIndex + Step, 0, CurrentOptions.Num() - 1);
|
||||||
|
|
||||||
|
if (NewHighlightedIndex == HighlightedIndex)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
HighlightedIndex = NewHighlightedIndex;
|
||||||
|
OnContextMenuHighlightChanged.Broadcast(HighlightedIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
void UInteractionComponent::HandleHoldThresholdReached()
|
||||||
|
{
|
||||||
|
bHoldConsumed = true;
|
||||||
|
|
||||||
|
if (CurrentOptions.Num() == 2)
|
||||||
|
{
|
||||||
|
ExecuteOption(1);
|
||||||
|
RefreshFocusedOptions();
|
||||||
|
ClearCachedTarget();
|
||||||
|
}
|
||||||
|
else if (CurrentOptions.Num() >= 3)
|
||||||
|
{
|
||||||
|
OpenContextMenu();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void UInteractionComponent::OpenContextMenu()
|
||||||
|
{
|
||||||
|
bContextMenuOpen = true;
|
||||||
|
HighlightedIndex = 0;
|
||||||
|
OnContextMenuOpened.Broadcast(CurrentOptions, HighlightedIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
void UInteractionComponent::CloseContextMenu()
|
||||||
|
{
|
||||||
|
bContextMenuOpen = false;
|
||||||
|
OnContextMenuClosed.Broadcast();
|
||||||
|
}
|
||||||
|
|
||||||
|
void UInteractionComponent::ExecuteOption(int32 Index)
|
||||||
|
{
|
||||||
|
AActor* Target = CurrentTarget.Get();
|
||||||
|
if (!Target || !CurrentOptions.IsValidIndex(Index))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
IInteractable::Execute_ExecuteInteraction(Target, GetOwner(), CurrentOptions[Index].ActionID);
|
||||||
|
}
|
||||||
|
|
||||||
|
void UInteractionComponent::RefreshFocusedOptions()
|
||||||
|
{
|
||||||
|
AActor* Actor = FocusedActor.Get();
|
||||||
|
if (!Actor)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
FocusedOptions = IInteractable::Execute_GetInteractionOptions(Actor, GetOwner());
|
||||||
|
OnFocusedInteractableChanged.Broadcast(Actor, FocusedOptions);
|
||||||
|
}
|
||||||
|
|
||||||
|
void UInteractionComponent::ClearCachedTarget()
|
||||||
|
{
|
||||||
|
CurrentTarget = nullptr;
|
||||||
|
CurrentOptions.Reset();
|
||||||
|
HighlightedIndex = 0;
|
||||||
|
bHoldConsumed = false;
|
||||||
|
}
|
||||||
@@ -0,0 +1,171 @@
|
|||||||
|
// Copyright Epic Games, Inc. All Rights Reserved.
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "CoreMinimal.h"
|
||||||
|
#include "Components/ActorComponent.h"
|
||||||
|
#include "InteractionTypes.h"
|
||||||
|
#include "InteractionComponent.generated.h"
|
||||||
|
|
||||||
|
DECLARE_DYNAMIC_MULTICAST_DELEGATE_TwoParams(FOnFocusedInteractableChanged, AActor*, FocusedActor, const TArray<FInteractionOption>&, FocusedOptions);
|
||||||
|
DECLARE_DYNAMIC_MULTICAST_DELEGATE_TwoParams(FOnContextMenuOpened, const TArray<FInteractionOption>&, Options, int32, InitialHighlightedIndex);
|
||||||
|
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnContextMenuHighlightChanged, int32, NewHighlightedIndex);
|
||||||
|
DECLARE_DYNAMIC_MULTICAST_DELEGATE(FOnContextMenuClosed);
|
||||||
|
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnInteractHoldProgressChanged, float, Progress);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Drives the whole "hold E" contextual interaction system for its owning Actor:
|
||||||
|
* every tick it look-traces for an IInteractable, distinguishes tap/hold on the
|
||||||
|
* currently-focused one, and either executes an interaction directly (1-2 options)
|
||||||
|
* or drives a Context Menu (3+ options). Interactable objects need to know nothing
|
||||||
|
* about any of this - they just implement IInteractable and return their own
|
||||||
|
* FInteractionOption data.
|
||||||
|
*
|
||||||
|
* Detection is a single line trace on the "Interactable" trace channel (see
|
||||||
|
* DefaultEngine.ini - ECC_GameTraceChannel1, default response Block), not a
|
||||||
|
* proximity sweep. That channel defaults to blocking everything, exactly like
|
||||||
|
* Visibility, so ordinary geometry occludes it correctly out of the box; to make
|
||||||
|
* only part of an actor count as its "interactable hit box", set that specific
|
||||||
|
* component's Interactable trace response and turn the rest of the mesh's off
|
||||||
|
* (Ignore) in its Collision settings - no C++ changes needed per-actor.
|
||||||
|
*/
|
||||||
|
UCLASS(ClassGroup = (Interaction), Blueprintable, meta = (BlueprintSpawnableComponent))
|
||||||
|
class KINGSHEARTHLEGACY_API UInteractionComponent : public UActorComponent
|
||||||
|
{
|
||||||
|
GENERATED_BODY()
|
||||||
|
|
||||||
|
public:
|
||||||
|
|
||||||
|
UInteractionComponent();
|
||||||
|
|
||||||
|
/** Fired every time the look-traced focus changes, including to/from nothing - drives an ever-present "you can interact" prompt. */
|
||||||
|
UPROPERTY(BlueprintAssignable, Category = "Interaction")
|
||||||
|
FOnFocusedInteractableChanged OnFocusedInteractableChanged;
|
||||||
|
|
||||||
|
/** Fired when 3+ options are available and the hold threshold is reached. */
|
||||||
|
UPROPERTY(BlueprintAssignable, Category = "Interaction")
|
||||||
|
FOnContextMenuOpened OnContextMenuOpened;
|
||||||
|
|
||||||
|
/** Fired whenever CycleContextMenuOptions moves the highlighted entry. */
|
||||||
|
UPROPERTY(BlueprintAssignable, Category = "Interaction")
|
||||||
|
FOnContextMenuHighlightChanged OnContextMenuHighlightChanged;
|
||||||
|
|
||||||
|
/** Fired when the Context Menu closes, whether by confirming a choice or losing its target. */
|
||||||
|
UPROPERTY(BlueprintAssignable, Category = "Interaction")
|
||||||
|
FOnContextMenuClosed OnContextMenuClosed;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fired every tick while the hold timer is running, with the 0-1 fraction of
|
||||||
|
* HoldThreshold elapsed so far - drives a fill bar for the "hold E" prompt.
|
||||||
|
* Fires once more with 0 the instant the hold ends, however it ends (release,
|
||||||
|
* secondary fired, Context Menu opened), so a bound bar always snaps back
|
||||||
|
* empty. Never fires at all for a 1-option target, since no hold timer is
|
||||||
|
* started for it.
|
||||||
|
*/
|
||||||
|
UPROPERTY(BlueprintAssignable, Category = "Interaction")
|
||||||
|
FOnInteractHoldProgressChanged OnInteractHoldProgressChanged;
|
||||||
|
|
||||||
|
/** Call from the Interact input's Started/pressed event. */
|
||||||
|
UFUNCTION(BlueprintCallable, Category = "Interaction")
|
||||||
|
void TryBeginInteract();
|
||||||
|
|
||||||
|
/** Call from the Interact input's Completed/released event. */
|
||||||
|
UFUNCTION(BlueprintCallable, Category = "Interaction")
|
||||||
|
void TryEndInteract();
|
||||||
|
|
||||||
|
/** Call while the Context Menu is open (e.g. from a mouse wheel axis) to move the highlighted entry. No-op otherwise. */
|
||||||
|
UFUNCTION(BlueprintCallable, Category = "Interaction")
|
||||||
|
void CycleContextMenuOptions(float Direction);
|
||||||
|
|
||||||
|
UFUNCTION(BlueprintPure, Category = "Interaction")
|
||||||
|
bool IsContextMenuOpen() const { return bContextMenuOpen; }
|
||||||
|
|
||||||
|
/** The actor currently under the look-trace, or nullptr if none. */
|
||||||
|
UFUNCTION(BlueprintPure, Category = "Interaction")
|
||||||
|
AActor* GetFocusedActor() const { return FocusedActor.Get(); }
|
||||||
|
|
||||||
|
protected:
|
||||||
|
|
||||||
|
virtual void BeginPlay() override;
|
||||||
|
virtual void TickComponent(float DeltaTime, ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction) override;
|
||||||
|
|
||||||
|
/** Max distance of the look trace. */
|
||||||
|
UPROPERTY(EditAnywhere, Category = "Interaction")
|
||||||
|
float InteractionRange = 300.0f;
|
||||||
|
|
||||||
|
/** Trace channel the look trace queries - see the "Interactable" channel set up in DefaultEngine.ini. */
|
||||||
|
UPROPERTY(EditAnywhere, Category = "Interaction")
|
||||||
|
TEnumAsByte<ECollisionChannel> InteractionTraceChannel = ECC_GameTraceChannel1;
|
||||||
|
|
||||||
|
/** Seconds the Interact input must be held before it counts as a hold rather than a tap. */
|
||||||
|
UPROPERTY(EditAnywhere, Category = "Interaction")
|
||||||
|
float HoldThreshold = 0.4f;
|
||||||
|
|
||||||
|
/** Draws the look trace every tick (green = hit an IInteractable, red = blocked/missed) and logs focus changes. */
|
||||||
|
UPROPERTY(EditAnywhere, Category = "Interaction|Debug")
|
||||||
|
bool bShowDebugTraces = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Finds the look-trace target. If the owning Actor implements IInteractionLookupProvider,
|
||||||
|
* defers to its InteractionLookupLoop first - implement that interface directly on your
|
||||||
|
* Character/Actor to fully replace how a target is found (VR hand ray, a cone check, ...)
|
||||||
|
* without touching the tap/hold/menu logic below. Falls back to a line trace from the
|
||||||
|
* controller's view point along its view direction, InteractionRange long, on
|
||||||
|
* InteractionTraceChannel, whenever the interface isn't implemented or returns false.
|
||||||
|
*/
|
||||||
|
bool PerformInteractionTrace(FHitResult& OutHit) const;
|
||||||
|
|
||||||
|
/** Re-runs PerformInteractionTrace, updates FocusedActor/FocusedOptions, and broadcasts OnFocusedInteractableChanged if either changed. */
|
||||||
|
void UpdateFocus();
|
||||||
|
|
||||||
|
/** Re-derives hold progress from HoldTimerHandle every tick and broadcasts OnInteractHoldProgressChanged when it changes. */
|
||||||
|
void UpdateHoldProgress();
|
||||||
|
|
||||||
|
/** Bound to the hold timer; only ever runs when CurrentOptions.Num() >= 2. */
|
||||||
|
void HandleHoldThresholdReached();
|
||||||
|
|
||||||
|
void OpenContextMenu();
|
||||||
|
void CloseContextMenu();
|
||||||
|
|
||||||
|
/** Executes CurrentOptions[Index] against CurrentTarget, if both are still valid. */
|
||||||
|
void ExecuteOption(int32 Index);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Re-pulls GetInteractionOptions() for the still-focused actor and re-broadcasts
|
||||||
|
* OnFocusedInteractableChanged. Call right after ExecuteOption - the interaction just run
|
||||||
|
* may have changed what that same actor now offers (e.g. a chest that's now empty), and
|
||||||
|
* UpdateFocus alone would never notice since it only re-pulls options when the focused
|
||||||
|
* actor's identity changes, not when its offered options do. No-op if nothing is focused.
|
||||||
|
*/
|
||||||
|
void RefreshFocusedOptions();
|
||||||
|
|
||||||
|
void ClearCachedTarget();
|
||||||
|
|
||||||
|
/** Actor currently under the look trace and implementing IInteractable, updated every tick by UpdateFocus. */
|
||||||
|
UPROPERTY()
|
||||||
|
TWeakObjectPtr<AActor> FocusedActor;
|
||||||
|
|
||||||
|
UPROPERTY()
|
||||||
|
TArray<FInteractionOption> FocusedOptions;
|
||||||
|
|
||||||
|
/** Snapshot of FocusedActor/FocusedOptions taken when Interact was pressed - what TryEndInteract actually acts on. */
|
||||||
|
UPROPERTY()
|
||||||
|
TWeakObjectPtr<AActor> CurrentTarget;
|
||||||
|
|
||||||
|
UPROPERTY()
|
||||||
|
TArray<FInteractionOption> CurrentOptions;
|
||||||
|
|
||||||
|
UPROPERTY()
|
||||||
|
int32 HighlightedIndex = 0;
|
||||||
|
|
||||||
|
UPROPERTY()
|
||||||
|
bool bContextMenuOpen = false;
|
||||||
|
|
||||||
|
/** Set the moment the hold timer fires, so TryEndInteract knows not to also run the tap path. */
|
||||||
|
bool bHoldConsumed = false;
|
||||||
|
|
||||||
|
FTimerHandle HoldTimerHandle;
|
||||||
|
|
||||||
|
/** Last value passed to OnInteractHoldProgressChanged, so UpdateHoldProgress only broadcasts on actual change. */
|
||||||
|
float LastBroadcastHoldProgress = 0.0f;
|
||||||
|
};
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user