Author SHA1 Message Date
Dolobarsch 90de51b1a0 Aktualisiere BP_Campfire.uasset
Die Datei `BP_Campfire.uasset` wurde geändert
2026-09-14 13:41:00 +02:00
Dolobarsch a5eb673c6e Interaktionsoptionen und UI-Verbesserungen
Neue Methode `RefreshFocusedOptions()` hinzugefügt, um die Optionen des fokussierten Akteurs nach einer Interaktion zu aktualisieren. Änderungen an der Navigation im Kontextmenü: Begrenzte Navigation ersetzt zyklische Navigation.

`FInteractionOption` erweitert um `TextColorOverride` und `HighlightColorOverride`, um individuelle Farben für Optionen zu ermöglichen.

UI-Widgets (`InteractionOptionEntryWidget`) angepasst, um Hintergrund- und Hervorhebungsfarben dynamisch zu setzen. Hinweistext für sekundäre Interaktionen zeigt nun spezifische Optionennamen an.
2026-09-11 01:52:59 +02:00
Dolobarsch e4fb702a1c "Hold-to-Interact"-System hinzugefügt
Ein neues "Hold-to-Interact"-System wurde implementiert, das es Spielern ermöglicht, durch Halten einer Taste mit Objekten zu interagieren.

- `UInteractionComponent` erweitert, um den Fortschritt des Halte-Timers zu berechnen und über das neue `OnInteractHoldProgressChanged`-Event zu übertragen.
- Neues Widget `UInteractionPromptWidget` eingeführt, das die Fortschrittsanzeige und Interaktionsaufforderungen modular handhabt.
- Veraltete Logik aus `UInteractionContextMenuWidget` entfernt und in `UInteractionPromptWidget` ausgelagert.
- Zwei neue Widgets (`WBP_InteractionBar.uasset`, `WBP_InteractionQuick.uasset`) hinzugefügt.
- Logging und neue Abhängigkeiten (`USegmentedBarWidget`) integriert.
- Verbesserte Modularität und Benutzererfahrung durch visuelles Feedback und klare Trennung der Logik.
2026-09-05 21:57:27 +02:00
Dolobarsch a36eaf27bc Optimierung von Kollisions- und Hardwareeinstellungen
Die Datei `DefaultEngine.ini` wurde aktualisiert, um neue Kollisionsprofile hinzuzufügen und bestehende zu überarbeiten. Ein neuer Kanal `Interactable` wurde hinzugefügt. Alte Profile und Kanäle wurden mithilfe von Redirects auf neue Namen umgeleitet.

Die `TargetedHardwareClass` wurde auf `Desktop` gesetzt, und die `DefaultGraphicsPerformance` wurde auf `Maximum` geändert, um die Leistung für Desktop-Systeme zu optimieren.

`ActiveGameNameRedirects` und `ActiveClassRedirects` wurden hinzugefügt, um alte Spiel- und Klassennamen auf neue Namen für das Projekt `KingshearthLegacy` umzuleiten.

Die Datei `BP_Campfire.uasset` wurde ebenfalls geändert, jedoch sind die Änderungen binär und nicht im Detail sichtbar.
2026-09-05 19:09:47 +02:00
Dolobarsch 0b34a9742a Verbesserung der Interaktionslogik und UI-Optimierung
Die Änderungen umfassen:
- Projektname in `DefaultGame.ini` auf "Kingshearth Legacy" geändert.
- Neue Konsolenvariablen für CommonUI hinzugefügt.
- `ListView` ersetzt `VerticalBox` für Kontextmenü-Optionen.
- `FInteractionOption` um `Icon` und `PromptOverride` erweitert.
- Debugging-Logs in `InteractionComponent.cpp` hinzugefügt.
- Neue Klassen: `UInteractionOptionEntryWidget` und `UInteractionOptionListItem`.
- Verbesserte Eingabeaufforderungslogik und Sichtbarkeitssteuerung.
- Binärdateien (`.uasset`) aktualisiert und neue hinzugefügt.
2026-09-05 19:01:41 +02:00
Dolobarsch fd08fe4804 Interaktionslogik und UI-Aufforderungen verbessert
Neue Funktion `GetInteractableDisplayName` in `Interactable.h` hinzugefügt, um benutzerfreundliche Objektnamen für Interaktionsaufforderungen bereitzustellen.

Die Logik in `InteractionContextMenuWidget.cpp` wurde überarbeitet:
- Aufforderungen werden ausgeblendet, wenn keine Optionen verfügbar sind.
- Objektnamen werden in die Aufforderungen integriert.
- Zusätzliche Hinweise für sekundäre Interaktionen und Kontextmenüs hinzugefügt.

Binärdateien (`BP_BerryBush.uasset`, `BP_Campfire.uasset`, `BP_ThirdPersonCharacter.uasset`, `WBP_InteractionContextMenu.uasset`) aktualisiert.
2026-09-05 03:13:51 +02:00
Dolobarsch 8099d69a23 Neues Interaktionssystem implementiert
Einführung eines modularen Interaktionssystems:
- Hinzufügen der `IInteractable`- und `IInteractionLookupProvider`-Schnittstellen.
- Implementierung der `UInteractionComponent` zur Steuerung von Interaktionen (Tippen, Halten, Kontextmenü).
- Integration der `InteractionComponent` in `KingshearthLegacyCharacter`.
- Hinzufügen des `InteractionContextMenuWidget` für UI-Interaktionen.
- Änderungen an `PlayerHUDWidget`, um Interaktions-Widgets zu unterstützen.
- Neue Assets und Konfigurationsänderungen für Kollisionen und Redirects.
- Verbesserte Modularität und Benutzerfreundlichkeit durch klare Trennung von Logik und Darstellung.
2026-09-05 03:04:12 +02:00
Dolobarsch befed4c2fa Merge pull request 'Neues Menüsystem implementiert' (#5) from Basic-HUD into main
Reviewed-on: #5
2026-09-05 00:35:26 +02:00
Dolobarsch b55eda5e51 Neues Menüsystem implementiert
Ein flexibles Menüsystem wurde hinzugefügt, das ein Hauptmenü und ein In-Game-Menü umfasst. Das Hauptmenü bietet Optionen wie "Neues Spiel starten", "Optionen" und "Credits". Das In-Game-Menü ermöglicht Pausenfunktionen wie "Fortsetzen", "Optionen" und "Zum Desktop beenden".

Neue Klassen wurden eingeführt, darunter `MenuManagerSubsystem` zur Verwaltung von Menübildschirmen, `MenuScreenWidget` als Basisklasse für Menüs sowie spezifische Widgets wie `MainMenuWidget`, `InGameMenuWidget` und `OptionsMenuWidget`.

Zusätzlich wurden stilisierte Schaltflächen (`MenuButtonWidget`) und ein neuer `MenuPlayerController` für menübasierte Levels implementiert. Änderungen an `KingshearthLegacyPlayerController` ermöglichen die Steuerung des In-Game-Menüs. Binäre Assets für Menüs und UI wurden hinzugefügt.
2026-09-05 00:33:04 +02:00
Dolobarsch 64f4a59b0b Integriere Gameplay Ability System (GAS)
Ersetze HealthComponent und StaminaComponent durch GAS-basierte AttributeSets (`UHealthAttributeSet`, `UStaminaAttributeSet`) und füge `UAbilitySystemComponent` hinzu.

Implementiere neue GameplayEffects (`UGE_HealthDamage`, `UGE_HealthHealing`, `UGE_HealthDebuff`, `UGE_StaminaFatiguePenalty`) und eine Modifikator-Berechnung (`UMMC_MaxStaminaFatiguePenalty`).

Erweitere `AKingshearthLegacyCharacter` um Methoden zur Attributmanipulation (`ApplyDamage`, `SpendStamina`, etc.) und Stamina-Regeneration.

Aktualisiere UI-Widgets (`HealthBarWidget`, `StaminaBarWidget`) zur Nutzung der neuen AttributeSets. Entferne Legacy-Code für Health- und Stamina-Komponenten.

Füge neue Assets (Niagara-Systeme, Materialien, Texturen) hinzu und passe Projektkonfiguration an (z. B. `GameplayAbilities`-Modul, `r.Substrate`).
2026-09-04 22:54:46 +02:00
Dolobarsch 495424a2b2 Füge HealthDebuff-Mechanik hinzu
Die HealthComponent wurde erweitert, um einen neuen `HealthDebuff`-Wert zu unterstützen, der einen Teil der maximalen Gesundheit des Charakters unbrauchbar macht.

- Neue Methoden `GetHealthDebuff`, `SetHealthDebuff` und `GetEffectiveMaxHealth` hinzugefügt.
- `CurrentHealth` wird nun gegen den effektiven Maximalwert (`GetEffectiveMaxHealth`) begrenzt.
- Neues Event `OnHealthDebuffChanged` implementiert, um Änderungen am Debuff zu signalisieren.
- HealthBar-Widget aktualisiert, um den `HealthDebuff` dynamisch anzuzeigen.
- Sprint-Logik verbessert: Sprinten nur möglich, wenn der Charakter am Boden ist.
- Binärdateien (`BP_ThirdPersonCharacter.uasset`, `WBP_StaminaBar.uasset`) geändert.
2026-08-28 00:01:16 +02:00
Dolobarsch 83cc8f592e Sprint- und Ausdauermechanik hinzugefügt
Die Sprint-Funktionalität wurde in `AKingshearthLegacyCharacter` implementiert, einschließlich neuer Methoden (`StartSprint`, `StopSprint`, `Tick`) und Eigenschaften (`SprintSpeedMultiplier`, `SprintStaminaDrainRate`, etc.). Die Eingabeverarbeitung wurde entsprechend erweitert.

Eine neue `UStaminaComponent` wurde hinzugefügt, die Ausdauer, Fatigue und Regeneration verwaltet. Ereignisse wie `OnStaminaChanged` und `OnExhausted` ermöglichen die Überwachung von Änderungen.

Die Benutzeroberfläche wurde angepasst: `UPlayerHUDWidget` und `UStaminaBarWidget` unterstützen nun die Anzeige der Ausdauer. Alte, fest codierte Werte wurden entfernt, und die Widgets sind nun dynamisch an die `StaminaComponent` gebunden.

Zusätzlich wurden Änderungen an `.uasset`-Dateien vorgenommen, um die neuen Mechaniken in Unreal Engine zu unterstützen.
2026-08-27 23:43:11 +02:00
127 changed files with 3230 additions and 183 deletions
+2 -1
View File
@@ -3,7 +3,8 @@
"allow": [
"mcp__unreal-mcp__describe_toolset",
"mcp__unreal-mcp__call_tool",
"Bash(python *)"
"Bash(python *)",
"mcp__unreal-mcp__list_toolsets"
]
},
"enabledMcpjsonServers": [
File diff suppressed because one or more lines are too long
+62 -2
View File
@@ -1,6 +1,6 @@
[/Script/EngineSettings.GameMapsSettings]
GameDefaultMap=/Game/ThirdPerson/Lvl_ThirdPerson.Lvl_ThirdPerson
EditorStartupMap=/Game/ThirdPerson/Lvl_ThirdPerson.Lvl_ThirdPerson
GameDefaultMap=/Game/Levels/MainMenu.MainMenu
EditorStartupMap=/Game/Levels/MainMenu.MainMenu
GlobalDefaultGameMode=/Game/ThirdPerson/Blueprints/BP_ThirdPersonGameMode.BP_ThirdPersonGameMode_C
bOffsetPlayerGamepadIds=False
@@ -27,6 +27,7 @@ r.Substrate.ProjectGBufferFormat=0
r.DefaultFeature.LocalExposure.HighlightContrastScale=0.8
r.DefaultFeature.LocalExposure.ShadowContrastScale=0.8
r.Lumen.HardwareRayTracing.LightingMode=0
[/Script/WindowsTargetPlatform.WindowsTargetSettings]
DefaultGraphicsRHI=DefaultGraphicsRHI_DX12
@@ -72,6 +73,65 @@ AppliedTargetedHardwareClass=Desktop
DefaultGraphicsPerformance=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]
+ActiveGameNameRedirects=(OldGameName="TP_ThirdPerson",NewGameName="/Script/KingshearthLegacy")
+ActiveGameNameRedirects=(OldGameName="/Script/TP_ThirdPerson",NewGameName="/Script/KingshearthLegacy")
+2 -1
View File
@@ -1,8 +1,9 @@
[/Script/EngineSettings.GeneralProjectSettings]
ProjectID=CE954E444D2A872317AAC38182169D03
ProjectName=Third Person Game Template
ProjectName=Kingsharth Legacy
[ConsoleVariables]
CommonUI.CheckKeyboardFocusAndParentage=1
CommonUI.DisallowUserFocusedWidgetForPendingFocusRecipient=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.
+4
View File
@@ -42,6 +42,10 @@
{
"Name": "EditorToolset",
"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;
};
@@ -0,0 +1,33 @@
// Copyright Epic Games, Inc. All Rights Reserved.
#pragma once
#include "CoreMinimal.h"
#include "UObject/Interface.h"
#include "InteractionLookupProvider.generated.h"
UINTERFACE(MinimalAPI, Blueprintable)
class UInteractionLookupProvider : public UInterface
{
GENERATED_BODY()
};
/**
* Optional interface for the Actor a UInteractionComponent is attached to (e.g.
* BP_ThirdPersonCharacter). Implement it there - Class Settings > Implement Interface,
* just like IInteractable on an interactable object - and override InteractionLookupLoop
* in that Actor's own Event Graph to replace how its "look at" target is found (a
* different origin, a cone check, a VR hand-ray, ...). No separate component subclass
* or class-swapping needed. If not implemented, or the implementation returns false,
* UInteractionComponent falls back to its own default camera-forward line trace.
*/
class IInteractionLookupProvider
{
GENERATED_BODY()
public:
/** Return true and fill OutHit with what was found; return false to fall back to the component's default trace. */
UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category = "Interaction")
bool InteractionLookupLoop(FHitResult& OutHit);
};
@@ -0,0 +1,58 @@
// Copyright Epic Games, Inc. All Rights Reserved.
#pragma once
#include "CoreMinimal.h"
#include "InteractionTypes.generated.h"
class UTexture2D;
/**
* A single interaction an IInteractable currently offers. Order matters:
* index 0 is always the primary (tap) action, index 1 (if present) is the
* secondary (hold) action, and 3+ entries populate the Context Menu in order.
*/
USTRUCT(BlueprintType)
struct FInteractionOption
{
GENERATED_BODY()
/** Shown to the player (Context Menu entry, interaction prompt, ...) */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Interaction")
FText DisplayName;
/** Opaque id passed back to IInteractable::ExecuteInteraction - the object interprets it itself */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Interaction")
FName ActionID;
/** Optional - shown by UInteractionOptionEntryWidget's Icon image when set, hidden otherwise. */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Interaction")
TObjectPtr<UTexture2D> Icon = nullptr;
/**
* Optional - when this is the primary (index 0) option and this is set, the ever-present
* prompt's first line uses this verbatim instead of building "{DisplayName} with
* {ObjectName}" - e.g. a chest can set this to "Open Chest" instead of ending up as
* "Open with Chest". The "Press E" / "Hold E for ..." lines are unaffected.
*/
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Interaction")
FText PromptOverride;
/**
* Optional per-option override for the Context Menu row's text color (both normal and
* highlighted state use this same color, so the indication doesn't disappear on hover) -
* e.g. red for an action you currently can't perform, gold for a special one. Overrides
* UInteractionOptionEntryWidget's NormalTextColor/HighlightedTextColor for this option
* only. Alpha 0 (the default) means "use the entry widget's own colors".
*/
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Interaction")
FLinearColor TextColorOverride = FLinearColor(0.0f, 0.0f, 0.0f, 0.0f);
/**
* Optional per-option override for the Context Menu row's highlight tint - overrides
* UInteractionOptionEntryWidget's HighlightColor for this option only. Alpha 0 (the
* default) means "use the entry widget's own color".
*/
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Interaction")
FLinearColor HighlightColorOverride = FLinearColor(0.0f, 0.0f, 0.0f, 0.0f);
};
@@ -18,13 +18,21 @@ public class KingshearthLegacy : ModuleRules
"StateTreeModule",
"GameplayStateTreeModule",
"UMG",
"Slate"
"Slate",
"SlateCore",
"GameplayAbilities",
"GameplayTags",
"GameplayTasks"
});
PrivateDependencyModuleNames.AddRange(new string[] { });
PublicIncludePaths.AddRange(new string[] {
"KingshearthLegacy",
"KingshearthLegacy/AttributeSets",
"KingshearthLegacy/GameplayEffects",
"KingshearthLegacy/Interaction",
"KingshearthLegacy/UI/Menu",
"KingshearthLegacy/Variant_Platforming",
"KingshearthLegacy/Variant_Platforming/Animation",
"KingshearthLegacy/Variant_Combat",
@@ -11,6 +11,15 @@
#include "EnhancedInputSubsystems.h"
#include "InputActionValue.h"
#include "KingshearthLegacy.h"
#include "AbilitySystemComponent.h"
#include "HealthAttributeSet.h"
#include "StaminaAttributeSet.h"
#include "GE_HealthDamage.h"
#include "GE_HealthHealing.h"
#include "GE_HealthDebuff.h"
#include "GE_StaminaFatiguePenalty.h"
#include "KingshearthLegacyGameplayTags.h"
#include "Interaction/InteractionComponent.h"
AKingshearthLegacyCharacter::AKingshearthLegacyCharacter()
{
@@ -48,6 +57,207 @@ AKingshearthLegacyCharacter::AKingshearthLegacyCharacter()
// Note: The skeletal mesh and anim blueprint references on the Mesh component (inherited from Character)
// are set in the derived blueprint asset named ThirdPersonCharacter (to avoid direct content references in C++)
AbilitySystemComponent = CreateDefaultSubobject<UAbilitySystemComponent>(TEXT("AbilitySystemComponent"));
HealthAttributeSet = CreateDefaultSubobject<UHealthAttributeSet>(TEXT("HealthAttributeSet"));
AbilitySystemComponent->AddAttributeSetSubobject(HealthAttributeSet.Get());
StaminaAttributeSet = CreateDefaultSubobject<UStaminaAttributeSet>(TEXT("StaminaAttributeSet"));
AbilitySystemComponent->AddAttributeSetSubobject(StaminaAttributeSet.Get());
InteractionComponent = CreateDefaultSubobject<UInteractionComponent>(TEXT("InteractionComponent"));
}
void AKingshearthLegacyCharacter::BeginPlay()
{
Super::BeginPlay();
BaseWalkSpeed = GetCharacterMovement()->MaxWalkSpeed;
if (AbilitySystemComponent)
{
AbilitySystemComponent->InitAbilityActorInfo(this, this);
// Always-on: keeps MaxStamina tracking Fatigue via MMC_MaxStaminaFatiguePenalty for the character's lifetime.
const FGameplayEffectSpecHandle FatiguePenaltySpec = AbilitySystemComponent->MakeOutgoingSpec(UGE_StaminaFatiguePenalty::StaticClass(), 1.0f, AbilitySystemComponent->MakeEffectContext());
if (FatiguePenaltySpec.IsValid())
{
AbilitySystemComponent->ApplyGameplayEffectSpecToSelf(*FatiguePenaltySpec.Data.Get());
}
}
}
UAbilitySystemComponent* AKingshearthLegacyCharacter::GetAbilitySystemComponent() const
{
return AbilitySystemComponent;
}
float AKingshearthLegacyCharacter::GetHealth() const
{
return HealthAttributeSet ? HealthAttributeSet->GetHealth() : 0.0f;
}
float AKingshearthLegacyCharacter::GetMaxHealth() const
{
return HealthAttributeSet ? HealthAttributeSet->GetNominalMaxHealth() : 0.0f;
}
float AKingshearthLegacyCharacter::GetHealthDebuff() const
{
return HealthAttributeSet ? HealthAttributeSet->GetHealthDebuff() : 0.0f;
}
void AKingshearthLegacyCharacter::ApplyDamage(float Amount)
{
if (Amount <= 0.0f || !AbilitySystemComponent)
{
return;
}
const FGameplayEffectSpecHandle SpecHandle = AbilitySystemComponent->MakeOutgoingSpec(UGE_HealthDamage::StaticClass(), 1.0f, AbilitySystemComponent->MakeEffectContext());
if (SpecHandle.IsValid())
{
SpecHandle.Data->SetSetByCallerMagnitude(TAG_Data_Health_Damage.GetTag(), Amount);
AbilitySystemComponent->ApplyGameplayEffectSpecToSelf(*SpecHandle.Data.Get());
}
}
void AKingshearthLegacyCharacter::ApplyHealing(float Amount)
{
if (Amount <= 0.0f || !AbilitySystemComponent)
{
return;
}
const FGameplayEffectSpecHandle SpecHandle = AbilitySystemComponent->MakeOutgoingSpec(UGE_HealthHealing::StaticClass(), 1.0f, AbilitySystemComponent->MakeEffectContext());
if (SpecHandle.IsValid())
{
SpecHandle.Data->SetSetByCallerMagnitude(TAG_Data_Health_Healing.GetTag(), Amount);
AbilitySystemComponent->ApplyGameplayEffectSpecToSelf(*SpecHandle.Data.Get());
}
}
void AKingshearthLegacyCharacter::SetHealthDebuff(float NewHealthDebuff)
{
if (!AbilitySystemComponent)
{
return;
}
if (HealthDebuffEffectHandle.IsValid())
{
AbilitySystemComponent->RemoveActiveGameplayEffect(HealthDebuffEffectHandle);
HealthDebuffEffectHandle.Invalidate();
}
const float ClampedDebuff = FMath::Clamp(NewHealthDebuff, 0.0f, 1.0f);
if (ClampedDebuff <= 0.0f)
{
return;
}
const FGameplayEffectSpecHandle SpecHandle = AbilitySystemComponent->MakeOutgoingSpec(UGE_HealthDebuff::StaticClass(), 1.0f, AbilitySystemComponent->MakeEffectContext());
if (SpecHandle.IsValid())
{
SpecHandle.Data->SetSetByCallerMagnitude(TAG_Data_HealthDebuff_Multiplier.GetTag(), 1.0f - ClampedDebuff);
HealthDebuffEffectHandle = AbilitySystemComponent->ApplyGameplayEffectSpecToSelf(*SpecHandle.Data.Get());
}
}
float AKingshearthLegacyCharacter::GetStamina() const
{
return StaminaAttributeSet ? StaminaAttributeSet->GetStamina() : 0.0f;
}
float AKingshearthLegacyCharacter::GetMaxStamina() const
{
return StaminaAttributeSet ? StaminaAttributeSet->GetNominalMaxStamina() : 0.0f;
}
float AKingshearthLegacyCharacter::GetFatigue() const
{
return StaminaAttributeSet ? StaminaAttributeSet->GetFatigue() : 0.0f;
}
void AKingshearthLegacyCharacter::SpendStamina(float Amount)
{
if (Amount <= 0.0f || !AbilitySystemComponent || !StaminaAttributeSet)
{
return;
}
AbilitySystemComponent->ApplyModToAttribute(UStaminaAttributeSet::GetStaminaAttribute(), EGameplayModOp::AddBase, -Amount);
TimeSinceLastStaminaSpend = 0.0f;
}
void AKingshearthLegacyCharacter::RestoreStamina(float Amount)
{
if (Amount <= 0.0f || !AbilitySystemComponent || !StaminaAttributeSet)
{
return;
}
AbilitySystemComponent->ApplyModToAttribute(UStaminaAttributeSet::GetStaminaAttribute(), EGameplayModOp::AddBase, Amount);
}
void AKingshearthLegacyCharacter::TickStaminaRegen(float DeltaSeconds)
{
if (!AbilitySystemComponent || !StaminaAttributeSet)
{
return;
}
TimeSinceLastStaminaSpend += DeltaSeconds;
const float TimeSinceRegenStarted = TimeSinceLastStaminaSpend - StaminaRegenDelay;
if (TimeSinceRegenStarted > 0.0f && StaminaAttributeSet->GetStamina() < StaminaAttributeSet->GetMaxStamina())
{
const float RampedRegenRate = StaminaRegenRampTime > 0.0f
? FMath::Min(TimeSinceRegenStarted / StaminaRegenRampTime, 1.0f) * StaminaRegenRate
: StaminaRegenRate;
AbilitySystemComponent->ApplyModToAttribute(UStaminaAttributeSet::GetStaminaAttribute(), EGameplayModOp::AddBase, RampedRegenRate * DeltaSeconds);
// Fatigue only builds while Stamina is actively regaining - never while draining, never while idle.
// It never recovers on its own; only an explicit SetFatigue call (e.g. a debug key or a future rest mechanic) reduces it.
AbilitySystemComponent->ApplyModToAttribute(UStaminaAttributeSet::GetFatigueAttribute(), EGameplayModOp::AddBase, FatigueGainRateWhileRegaining * DeltaSeconds);
}
}
void AKingshearthLegacyCharacter::SetFatigue(float NewFatigue)
{
if (!AbilitySystemComponent || !StaminaAttributeSet)
{
return;
}
AbilitySystemComponent->ApplyModToAttribute(UStaminaAttributeSet::GetFatigueAttribute(), EGameplayModOp::Override, FMath::Clamp(NewFatigue, 0.0f, 1.0f));
}
void AKingshearthLegacyCharacter::Tick(float DeltaSeconds)
{
Super::Tick(DeltaSeconds);
if (!StaminaAttributeSet)
{
return;
}
const bool bIsMoving = GetVelocity().SizeSquared() > FMath::Square(SprintMovementThreshold);
const bool bIsGrounded = GetCharacterMovement()->IsMovingOnGround();
if (bWantsToSprint && bIsMoving && bIsGrounded && StaminaAttributeSet->GetStamina() > 0.0f)
{
GetCharacterMovement()->MaxWalkSpeed = BaseWalkSpeed * SprintSpeedMultiplier;
SpendStamina(SprintStaminaDrainRate * DeltaSeconds);
}
else
{
GetCharacterMovement()->MaxWalkSpeed = BaseWalkSpeed;
}
TickStaminaRegen(DeltaSeconds);
}
void AKingshearthLegacyCharacter::SetupPlayerInputComponent(UInputComponent* PlayerInputComponent)
@@ -65,6 +275,15 @@ void AKingshearthLegacyCharacter::SetupPlayerInputComponent(UInputComponent* Pla
// Looking
EnhancedInputComponent->BindAction(LookAction, ETriggerEvent::Triggered, this, &AKingshearthLegacyCharacter::Look);
// Sprinting
EnhancedInputComponent->BindAction(SprintAction, ETriggerEvent::Started, this, &AKingshearthLegacyCharacter::StartSprint);
EnhancedInputComponent->BindAction(SprintAction, ETriggerEvent::Completed, this, &AKingshearthLegacyCharacter::StopSprint);
// Interacting
EnhancedInputComponent->BindAction(InteractAction, ETriggerEvent::Started, this, &AKingshearthLegacyCharacter::DoInteractStart);
EnhancedInputComponent->BindAction(InteractAction, ETriggerEvent::Completed, this, &AKingshearthLegacyCharacter::DoInteractEnd);
EnhancedInputComponent->BindAction(InteractCycleAction, ETriggerEvent::Triggered, this, &AKingshearthLegacyCharacter::InteractCycle);
}
else
{
@@ -131,3 +350,54 @@ void AKingshearthLegacyCharacter::DoJumpEnd()
// signal the character to stop jumping
StopJumping();
}
void AKingshearthLegacyCharacter::DoInteractStart()
{
if (InteractionComponent)
{
InteractionComponent->TryBeginInteract();
}
}
void AKingshearthLegacyCharacter::DoInteractEnd()
{
if (InteractionComponent)
{
InteractionComponent->TryEndInteract();
}
}
void AKingshearthLegacyCharacter::DoInteractCycle(float Direction)
{
if (InteractionComponent)
{
InteractionComponent->CycleContextMenuOptions(Direction);
}
}
void AKingshearthLegacyCharacter::InteractCycle(const FInputActionValue& Value)
{
DoInteractCycle(Value.Get<float>());
}
void AKingshearthLegacyCharacter::Jump()
{
const bool bCouldJump = CanJump();
Super::Jump();
if (bCouldJump)
{
SpendStamina(JumpStaminaCost);
}
}
void AKingshearthLegacyCharacter::StartSprint()
{
bWantsToSprint = true;
}
void AKingshearthLegacyCharacter::StopSprint()
{
bWantsToSprint = false;
}
@@ -5,11 +5,17 @@
#include "CoreMinimal.h"
#include "GameFramework/Character.h"
#include "Logging/LogMacros.h"
#include "AbilitySystemInterface.h"
#include "ActiveGameplayEffectHandle.h"
#include "KingshearthLegacyCharacter.generated.h"
class USpringArmComponent;
class UCameraComponent;
class UInputAction;
class UAbilitySystemComponent;
class UHealthAttributeSet;
class UStaminaAttributeSet;
class UInteractionComponent;
struct FInputActionValue;
DECLARE_LOG_CATEGORY_EXTERN(LogTemplateCharacter, Log, All);
@@ -19,7 +25,7 @@ DECLARE_LOG_CATEGORY_EXTERN(LogTemplateCharacter, Log, All);
* Implements a controllable orbiting camera
*/
UCLASS(abstract)
class AKingshearthLegacyCharacter : public ACharacter
class AKingshearthLegacyCharacter : public ACharacter, public IAbilitySystemInterface
{
GENERATED_BODY()
@@ -49,6 +55,50 @@ protected:
UPROPERTY(EditAnywhere, Category="Input")
UInputAction* MouseLookAction;
/** Sprint Input Action */
UPROPERTY(EditAnywhere, Category="Input")
UInputAction* SprintAction;
/** Interact Input Action - Started begins a tap/hold, Completed resolves it */
UPROPERTY(EditAnywhere, Category="Input")
UInputAction* InteractAction;
/** Cycles the Context Menu's highlighted option while it is open (e.g. mouse wheel axis) */
UPROPERTY(EditAnywhere, Category="Input")
UInputAction* InteractCycleAction;
/** Walk speed is multiplied by this while sprinting */
UPROPERTY(EditAnywhere, Category="Sprint")
float SprintSpeedMultiplier = 1.6f;
/** Stamina drained per second while sprinting */
UPROPERTY(EditAnywhere, Category="Sprint")
float SprintStaminaDrainRate = 25.0f;
/** Minimum speed (cm/s) for the character to be considered "moving" for sprint purposes. */
UPROPERTY(EditAnywhere, Category="Sprint")
float SprintMovementThreshold = 10.0f;
/** Stamina consumed by each successful jump. */
UPROPERTY(EditAnywhere, Category="Stamina")
float JumpStaminaCost = 15.0f;
/** Regen rate once fully ramped up, in stamina/sec. */
UPROPERTY(EditAnywhere, Category="Stamina")
float StaminaRegenRate = 20.0f;
/** Seconds after the last stamina spend before regen starts at all. */
UPROPERTY(EditAnywhere, Category="Stamina")
float StaminaRegenDelay = 1.0f;
/** Seconds for the regen rate to ramp from 0 up to StaminaRegenRate once regen starts, instead of kicking in at full speed immediately. */
UPROPERTY(EditAnywhere, Category="Stamina")
float StaminaRegenRampTime = 1.5f;
/** Fatigue gained per second while Stamina is actively regaining (ramping back up). Never recovers on its own otherwise - only an explicit debug/design call reduces it. */
UPROPERTY(EditAnywhere, Category="Fatigue")
float FatigueGainRateWhileRegaining = 0.1f;
public:
/** Constructor */
@@ -56,9 +106,15 @@ public:
protected:
virtual void BeginPlay() override;
virtual void Tick(float DeltaSeconds) override;
/** Initialize input action bindings */
virtual void SetupPlayerInputComponent(class UInputComponent* PlayerInputComponent) override;
/** Overridden to charge JumpStaminaCost whenever a jump actually starts. */
virtual void Jump() override;
protected:
/** Called for movement input */
@@ -67,8 +123,102 @@ protected:
/** Called for looking input */
void Look(const FInputActionValue& Value);
/** Called when the sprint input is pressed */
void StartSprint();
/** Called when the sprint input is released */
void StopSprint();
/** Called for Context Menu cycling input (e.g. mouse wheel axis) */
void InteractCycle(const FInputActionValue& Value);
/** Advances the regen delay/ramp curve and applies Stamina/Fatigue regen for this frame. */
void TickStaminaRegen(float DeltaSeconds);
/** Cached from the CharacterMovementComponent's MaxWalkSpeed in BeginPlay, restored whenever not sprinting. */
float BaseWalkSpeed = 0.0f;
bool bWantsToSprint = false;
/** Seconds since Stamina was last reduced by a spend. Resets the regen delay/ramp whenever SpendStamina runs. */
float TimeSinceLastStaminaSpend = 0.0f;
/** Drives all attribute sets (Health, Stamina, and future Nutrition/Blood). Self-owned: this character is both owner and avatar. */
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="Abilities", meta = (AllowPrivateAccess = "true"))
TObjectPtr<UAbilitySystemComponent> AbilitySystemComponent;
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="Abilities", meta = (AllowPrivateAccess = "true"))
TObjectPtr<UHealthAttributeSet> HealthAttributeSet;
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="Abilities", meta = (AllowPrivateAccess = "true"))
TObjectPtr<UStaminaAttributeSet> StaminaAttributeSet;
/** Currently-applied GE_HealthDebuff instance, if any - reapplied/removed wholesale by SetHealthDebuff so it behaves like the old "set to X" API instead of stacking. */
FActiveGameplayEffectHandle HealthDebuffEffectHandle;
/** Drives the contextual "hold E" interaction system - see UInteractionComponent. */
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="Interaction", meta = (AllowPrivateAccess = "true"))
TObjectPtr<UInteractionComponent> InteractionComponent;
public:
//~ Begin IAbilitySystemInterface
virtual UAbilitySystemComponent* GetAbilitySystemComponent() const override;
//~ End IAbilitySystemInterface
/** Exposed for Blueprint to bind OnHealthChanged/OnHealthDebuffChanged/OnDeath - there's no "component bound event" for a plain UObject subobject, so bind via this getter's return pin instead (drag off it, search "Bind Event to..."). */
UFUNCTION(BlueprintPure, Category = "Health")
FORCEINLINE UHealthAttributeSet* GetHealthAttributeSet() const { return HealthAttributeSet; }
/** Exposed for Blueprint to bind OnStaminaChanged/OnFatigueChanged/OnExhausted - see GetHealthAttributeSet(). */
UFUNCTION(BlueprintPure, Category = "Stamina")
FORCEINLINE UStaminaAttributeSet* GetStaminaAttributeSet() const { return StaminaAttributeSet; }
// Health
UFUNCTION(BlueprintPure, Category = "Health")
float GetHealth() const;
/** Nominal cap, unaffected by HealthDebuff. Feeds the health bar's total width. */
UFUNCTION(BlueprintPure, Category = "Health")
float GetMaxHealth() const;
/** 0-1 fraction of MaxHealth currently locked/unusable (curses, status effects, ...). */
UFUNCTION(BlueprintPure, Category = "Health")
float GetHealthDebuff() const;
UFUNCTION(BlueprintCallable, Category = "Health")
void ApplyDamage(float Amount);
UFUNCTION(BlueprintCallable, Category = "Health")
void ApplyHealing(float Amount);
/** Sets how much of MaxHealth is currently locked away (0-1) by applying/refreshing/removing a single GE_HealthDebuff instance. */
UFUNCTION(BlueprintCallable, Category = "Health")
void SetHealthDebuff(float NewHealthDebuff);
// Stamina
UFUNCTION(BlueprintPure, Category = "Stamina")
float GetStamina() const;
/** Nominal cap, unaffected by Fatigue. Feeds the stamina bar's total width. */
UFUNCTION(BlueprintPure, Category = "Stamina")
float GetMaxStamina() const;
UFUNCTION(BlueprintPure, Category = "Fatigue")
float GetFatigue() const;
/** Deducts Amount from Stamina and resets the regen delay/ramp. No-op for Amount <= 0. */
UFUNCTION(BlueprintCallable, Category = "Stamina")
void SpendStamina(float Amount);
/** Adds Amount to Stamina without affecting the regen delay/ramp (e.g. a stamina potion). No-op for Amount <= 0. */
UFUNCTION(BlueprintCallable, Category = "Stamina")
void RestoreStamina(float Amount);
/** Sets Fatigue directly (0-1). Fatigue only ever builds on its own; this is the only way to reduce it (e.g. a debug key or a future rest mechanic). */
UFUNCTION(BlueprintCallable, Category = "Fatigue")
void SetFatigue(float NewFatigue);
/** Handles move inputs from either controls or UI interfaces */
UFUNCTION(BlueprintCallable, Category="Input")
virtual void DoMove(float Right, float Forward);
@@ -85,6 +235,21 @@ public:
UFUNCTION(BlueprintCallable, Category="Input")
virtual void DoJumpEnd();
/** Handles interact pressed inputs from either controls or UI interfaces - begins a tap/hold */
UFUNCTION(BlueprintCallable, Category="Input")
virtual void DoInteractStart();
/** Handles interact released inputs from either controls or UI interfaces - resolves the tap/hold */
UFUNCTION(BlueprintCallable, Category="Input")
virtual void DoInteractEnd();
/** Handles Context Menu cycling inputs (e.g. mouse wheel) from either controls or UI interfaces */
UFUNCTION(BlueprintCallable, Category="Input")
virtual void DoInteractCycle(float Direction);
/** Returns InteractionComponent subobject **/
FORCEINLINE UInteractionComponent* GetInteractionComponent() const { return InteractionComponent; }
public:
/** Returns CameraBoom subobject **/
@@ -0,0 +1,5 @@
#include "KingshearthLegacyGameplayTags.h"
UE_DEFINE_GAMEPLAY_TAG(TAG_Data_Health_Damage, "Data.Health.Damage");
UE_DEFINE_GAMEPLAY_TAG(TAG_Data_Health_Healing, "Data.Health.Healing");
UE_DEFINE_GAMEPLAY_TAG(TAG_Data_HealthDebuff_Multiplier, "Data.HealthDebuff.Multiplier");
@@ -0,0 +1,8 @@
#pragma once
#include "NativeGameplayTags.h"
// SetByCaller data tags - the Name a GameplayEffect spec's magnitude is set against at runtime.
KINGSHEARTHLEGACY_API UE_DECLARE_GAMEPLAY_TAG_EXTERN(TAG_Data_Health_Damage)
KINGSHEARTHLEGACY_API UE_DECLARE_GAMEPLAY_TAG_EXTERN(TAG_Data_Health_Healing)
KINGSHEARTHLEGACY_API UE_DECLARE_GAMEPLAY_TAG_EXTERN(TAG_Data_HealthDebuff_Multiplier)
@@ -2,14 +2,18 @@
#include "KingshearthLegacyPlayerController.h"
#include "EnhancedInputComponent.h"
#include "EnhancedInputSubsystems.h"
#include "Engine/GameInstance.h"
#include "Engine/LocalPlayer.h"
#include "InputMappingContext.h"
#include "Blueprint/UserWidget.h"
#include "KingshearthLegacy.h"
#include "Widgets/Input/SVirtualJoystick.h"
#include "UI/PlayerHUDWidget.h"
#include "HealthComponent.h"
#include "UI/Menu/InGameMenuWidget.h"
#include "UI/Menu/MenuManagerSubsystem.h"
#include "KingshearthLegacyCharacter.h"
void AKingshearthLegacyPlayerController::BeginPlay()
{
@@ -59,6 +63,14 @@ void AKingshearthLegacyPlayerController::SetupInputComponent()
}
}
}
if (UEnhancedInputComponent* EnhancedInputComponent = Cast<UEnhancedInputComponent>(InputComponent))
{
if (MenuAction)
{
EnhancedInputComponent->BindAction(MenuAction, ETriggerEvent::Started, this, &AKingshearthLegacyPlayerController::ToggleInGameMenu);
}
}
}
}
@@ -87,7 +99,12 @@ void AKingshearthLegacyPlayerController::OnPossess(APawn* InPawn)
if (PlayerHUD)
{
PlayerHUD->InitializeHUD(InPawn->FindComponentByClass<UHealthComponent>());
const AKingshearthLegacyCharacter* PossessedCharacter = Cast<AKingshearthLegacyCharacter>(InPawn);
UHealthAttributeSet* PossessedHealthAttributeSet = PossessedCharacter ? PossessedCharacter->GetHealthAttributeSet() : nullptr;
UStaminaAttributeSet* PossessedStaminaAttributeSet = PossessedCharacter ? PossessedCharacter->GetStaminaAttributeSet() : nullptr;
UInteractionComponent* PossessedInteractionComponent = PossessedCharacter ? PossessedCharacter->GetInteractionComponent() : nullptr;
PlayerHUD->InitializeHUD(PossessedHealthAttributeSet, PossessedStaminaAttributeSet, PossessedInteractionComponent);
}
}
@@ -96,3 +113,27 @@ bool AKingshearthLegacyPlayerController::ShouldUseTouchControls() const
// are we on a mobile platform? Should we force touch?
return SVirtualJoystick::ShouldDisplayTouchInterface() || bForceTouchControls;
}
void AKingshearthLegacyPlayerController::ToggleInGameMenu()
{
if (!IsLocalPlayerController() || !InGameMenuClass)
{
return;
}
UGameInstance* GI = GetGameInstance();
UMenuManagerSubsystem* MenuManager = GI ? GI->GetSubsystem<UMenuManagerSubsystem>() : nullptr;
if (!MenuManager)
{
return;
}
if (MenuManager->IsAnyScreenOpen())
{
MenuManager->PopScreen();
}
else
{
MenuManager->PushScreen(InGameMenuClass);
}
}
@@ -7,8 +7,10 @@
#include "KingshearthLegacyPlayerController.generated.h"
class UInputMappingContext;
class UInputAction;
class UUserWidget;
class UPlayerHUDWidget;
class UInGameMenuWidget;
/**
* Basic PlayerController class for a third person game
@@ -49,16 +51,28 @@ protected:
UPROPERTY(BlueprintReadOnly, Category = "UI")
TObjectPtr<UPlayerHUDWidget> PlayerHUD;
/** In-game pause menu widget, pushed/popped through the menu manager by ToggleInGameMenu */
UPROPERTY(EditAnywhere, Category = "UI")
TSubclassOf<UInGameMenuWidget> InGameMenuClass;
/** Input action that opens the in-game menu, or closes the top menu screen if one is already open */
UPROPERTY(EditAnywhere, Category = "Input|Input Mappings")
TObjectPtr<UInputAction> MenuAction;
/** Gameplay initialization */
virtual void BeginPlay() override;
/** Input mapping context setup */
virtual void SetupInputComponent() override;
/** Spawns/initializes the player HUD once a pawn (and its HealthComponent) is available */
/** Spawns/initializes the player HUD once a pawn (and its HealthAttributeSet) is available */
virtual void OnPossess(APawn* InPawn) override;
/** Returns true if the player should use UMG touch controls */
bool ShouldUseTouchControls() const;
/** Opens InGameMenuClass, or closes the top menu screen if one is already open */
UFUNCTION(BlueprintCallable, Category = "UI")
void ToggleInGameMenu();
};

Some files were not shown because too many files have changed in this diff Show More