From 9eed8c548380225dc3d8fdc16615ddc77da65ced Mon Sep 17 00:00:00 2001 From: nathan Date: Fri, 15 May 2026 12:51:23 -0700 Subject: [PATCH] Implement sprinting --- AGRARIAN_DEVELOPMENT_ROADMAP.md | 4 +- Content/Input/Actions/IA_Sprint.uasset | Bin 0 -> 1160 bytes Content/Input/IMC_Default.uasset | Bin 9455 -> 10254 bytes .../Blueprints/BP_ThirdPersonCharacter.uasset | Bin 51566 -> 51752 bytes Scripts/setup_sprint_input.py | 88 +++++++++++++++ Scripts/verify_sprint_input.py | 44 ++++++++ Source/AgrarianGame/AgrarianGameCharacter.cpp | 100 +++++++++++++++++- Source/AgrarianGame/AgrarianGameCharacter.h | 53 ++++++++++ 8 files changed, 286 insertions(+), 3 deletions(-) create mode 100644 Content/Input/Actions/IA_Sprint.uasset create mode 100644 Scripts/setup_sprint_input.py create mode 100644 Scripts/verify_sprint_input.py diff --git a/AGRARIAN_DEVELOPMENT_ROADMAP.md b/AGRARIAN_DEVELOPMENT_ROADMAP.md index 2042922..0d62653 100644 --- a/AGRARIAN_DEVELOPMENT_ROADMAP.md +++ b/AGRARIAN_DEVELOPMENT_ROADMAP.md @@ -371,7 +371,7 @@ Target deliverable: A small group can join a server, spawn into one biome, gathe - [x] Decide first-person, third-person, or hybrid camera. Decision: hybrid camera with third person as default and optional first-person toggle. - [x] Implement first/third-person camera toggle. - [x] Implement movement. -- [ ] Implement sprinting. +- [x] Implement sprinting. - [ ] Define real-world baseline walking speed. - [ ] Define real-world baseline running speed. - [ ] Connect movement speed to age, condition, strength, endurance, hunger, thirst, injury, carried weight, and terrain. @@ -1465,4 +1465,4 @@ Earliest incomplete foundation items: Immediate next item: -- [ ] Implement sprinting. +- [ ] Define real-world baseline walking speed. diff --git a/Content/Input/Actions/IA_Sprint.uasset b/Content/Input/Actions/IA_Sprint.uasset new file mode 100644 index 0000000000000000000000000000000000000000..deaf266e13f511e3153feb9b68105e968f22f540 GIT binary patch literal 1160 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 H8S?)D52Lz& literal 9455 zcmeHN4Qv!e6n;SYk%ICsP*gzvN^P$#y|xrY+Upe$OZhqe0-APvY_A7*x0k)W!Xf^F zB0&g&R5S#dNW^GJ2m%H|qd=4dB_^P0NKn8~g9sY_k^eg1?C!BWuH_Qii)VPr?aaJ+ zGjHa5Z{ED!_Q0}{HUBg;G?cX?>fDy-7AryLjwijFc6img&+Dx7zp1TA`RrPEpRF4a z6QSK5is#;#=osSLH}~xmN3Q+&6yyC}x&8M)3#zXs9?#gEUt5$mg7NyYpJ{^N5luF) zFQAxglPsl$VucWJE3AiTEc>l(!>g3qk!TRBW))>*=VV((XIU~c%$A(&oUDROb7sDE zl*MYbShFo$8p2&z?er5wY)w?o+MvN$iB>{JZ%<=<{K~a{$o#ZqPM`GZt&?`ueow3K z^?Duaz?@iAd+JCDRJ6Z)yB@5Sk}|_;PMIdUMZqton$vQ4KhcF(>)J-{XM;GIYd&0w z$~vaEc8JQ;pA7Q`06Ls8_8I#6u(rwWkX=5-Wc5}GUWYi3!$)=74*vyxdN1c%Bw3{C zqZ)pOVe)Cer|>s`k?&Akl9w(dTK!L=Da%4|iY)m=SqYMDd%deG;}!%JF~mGmlDZ#r z6w=lXRo!}`rkd&%F*r%^`CMM7pI&aWXL5UVNejIcJa@J~Mp$V;QqP6`P}A0{YQH~_ z*4K>5f<~)1;0bA*dTi79=fRxf7J{M-B|^DdWW(&DqA-00Vx>^!l4L(Q14B2Bi%K*v z&*rTZWtXDO;V+UUkA{Z4dva2LHVa(RC(J7?5*%VdKv5)bX@SHF5A7UzGz(rEL84f! zP-sr>%=7Rey&ufXLK;@KWIg;)XHRoEE5R>oI96kvj`OEi20Z0{#pPH)JKiyGdyb9c zmefGDNM1!;q-d=|K@n)J`}D2^CdQDVT`$hA?xfaB*f!`*yDtG2;mb2EUvZut2@2Wk z#DPFXXE3CBt_qjPkR0@DI2H&_u~>2lZr5S~Zic!1J_aWBP=YIfa8nlb)803(eA|IZa40D8da@fXWT%s> zr@RFS!%R=(HXpo-E!i=Dfgn@f-i=4FJi8(X49iQWjdvh7U3NL0*y!o;o-Yu~X80)U z(^*TQWhQ-frhE-*v#9Ouj%Nq5nlV19ZsIrD@=I;Z2(sW%XwTOhdJkw?ZeqaWqtq3L zK4?a!Sdtdd-H)fGHzPAisuJnKYZvbz5;U=<>5|jw7A*{HS)ilK>c4C*zD_=6OYMcZ zv`~ULF+(*e-LR#OgAFWh!S5$!;i9z&e8yeRS$8M}UdDKQhsm{ly;_^^=H}L=by}OR z{JeKyKUj~+H(KSD9l;bnw;}Scx|Fw*^Kd6u*Us)T2SAeF#91UhTCs(3+lTarVp_-9MAV#Ss#)+Bc9c*tPk0RQQs%6 ztS{VOwzZ%>wv|UB_}2X_M17Oel6?NcX?!c5$uir~S}ex6@Z1Uag`UyrFIzl=NsE*@ z=`)nDEWJupvpuU}218Dm#(RfHJ@;__-reY8fek=|)qEWVT*)9@Uobt$I4jyX$~y7M^g9(VXP^hx4yLXtZXv^043NH& zfxH!0ka26loQRA~dKx~)>0b5do;FN_DUM;|2LUGn@c>6XM$qBI3@DDF3wa#w;~2pP zOM&7THvK+{6=bjuD2^cuc^a;;71fBj(E{>rKc^n{X61E#Hn(}sMsz7lJ+rwkqtrmq0 zFC?JHkf}KEXY;S{_!A`Pe+}977WJ~BDq@czGk(%(-plas9-!EJnXXYU8!h9%(Kx{V lb?Kw)Ti#9^^W9Ng&H7(u`RdrC>SXTJgdg@Lr)vzz{|iINgXaJM diff --git a/Content/ThirdPerson/Blueprints/BP_ThirdPersonCharacter.uasset b/Content/ThirdPerson/Blueprints/BP_ThirdPersonCharacter.uasset index bdd5c225759b3b50e3d1bd62c2016e2916b760dd..4f1c10ca46f898793a8a61ab202b4f64c2b0d389 100644 GIT binary patch delta 3052 zcmZuz2~1Q+7=E)rVYw861r&Eh#D%3??V{D%ShY>iYBeRbjS{Rj+E^`~p=pi9)+k41 zcj4y{4U0q(6jbolh=6h*q6I{)v8~}~tfqx(O=C?HrT@&ES$D-r-pn`O_x;B^cmK+D zs>pM)1p2#n7Y}XFmdTg54GxDbUzz&E>RL5Y6O}-`Z%LArrII8cT$2M}AXpD4iD%pu zy(BF{Dsd`KI4Wm7U+)JgdxBK`Y)NXxqZ{s#P;v`AREZ&Tmq^k-mleO5$8d}skfcH+ zp2%sc4W9m92otw%-f-eAl}~MYOQiXR@avNIte4KZ?Dox!Q#>H6jMAT2EM|4JWmNA?M&rtqv|}-PGqr7ogMW zylgv+Kf&CNo-HqUDuu@(vT7XLf%z^8mYj#i5WOveYTZJ!_0s`srakYc9X(CLV~eD0 z&p5UX*>b_!^0mR4X*#=I3|>7vH(M?mRV|_%SXr=k(aS@g_eLYn36Dc$!%Nw#FJ=1# zYgffQ-M-`b(;aJG7ZDCMZwl6yrxhO9b=!j7UbK0d_}w3Z2QveFj*6#;uf32K8sPsT z+k+WeXohkAAEEap{;TM3qyBN3uAtKX(oEgZs2gATGWt73@1z&z&(P!i8Jo>)KF{U> zHoJyVHHOU}u{n*+G;PjvIhFi%sfxFp)mw;U(i9P1+IL3E_2JN@?4F4w_{?L-s4(4;(h4!^>`wKKU6<$9Fp zh`__ZkRp|ocqh7;(>>k@R(ya0c>Xh8;^EsKze3eqrj7K_DbqV$~VEUkl`045SH~EK|Hpob$!i z4EHTbY71vDN6zOYlSTLCjF5d4xm?H3!2yQ)5q(0D{3!PZVa3AXL0$$&NAklEtuEXI zaRqr~KXeo<CkX(0q0WY&{z;?=MvoQh?h)czAjf{Cdex z)*tnvjgMLvKa+^e?5<8x zs(@e34qmUiW+JvJH=p$;KI#10Z{};WJSab=XS};Q*+;z3Nwym_gf~5iO%+bQ()TPMa!mL~y=xLY)4M@+mk0LK&~^5I=QOyV delta 3315 zcmZ{me@s(X6vy8!FhL7z2d!-s97CXmg5~F&W^;mBkZE)=Sr%XpO=g@wv$^n>38-

UJ3A+M3FnNNUMoV_-(FgV%L|kf%36m;Md@iGGcO zl!0OCVQ?&c3$89*2mVX65EGsd)W{+?LtgkYjd<6>X8*%L#s>Odz=^A#EO- zk0qqtgOmsa7fNUyY@EetO_V@pL>TFm_;fL!>*(`6B|)Ov4yPhg;AKP@0IGTg6r72-bBj+#tI?>z1 z0xdPFw4EL_k=alNcH=(9a2Z6#?IY1p8JDZ=W9%IqQ&_o~Rv0#AkQJ~lK3-7(@H{pH zKC2Fdfp`_HHYpb#VwxrwiZbcoUYc?t{fVS4x*8SPaMGmGTz1WHfjIua>~y$l%GFeP z5IeIMfjJ>pJAile{ZLH}dr^Zmv?6$vpcAl);a-M6suS>*M3W+*9-b%Wz`evUk_2;! z`^hBsxtelR&_)Go1dHQ7hx`@$6-_o2WJ9T0r5!`C2rrsBE5POT$OhJ=904t0sL0QS z;+x!vD(EGmjuwhEG++=zV!-&S#cSWk14$%0SRnC}9AvyXD2C7`9eaVE;!&D)tvF2FSK3 z(Rzwjp(Rb2qvWXtus>8|fac0oq!J!hW(5_q>MW3KjU(6KQ|ppoaess&tAL}{Jb2a@ zNcGlTqy$wu*jx2GDWw*<%~!%45yEz};5oI?Xcg7YlcP{nm%(O#t1}9-yP@brH4;tz zHbxrj_n@V0NDdysb1xS32|TP1g*R>f5Z@3=M)-n;3O{sfv?Y@fSZUkgwr6d3(6;tP z!S%*tXyNnm2TvOaIS%T6Jv_Ud1Nkim{{6E-gw%k#Sr3-(UHsI!NLsZ^76In-yP4E+ z-c!oww}g25b+?Ls>)IrK585Su=1$Si(JArE@4BlH@@|D1yULXlrtJq<_FX-ySA}nf zC!Jxiu(weC4SP#_*f;{+XE*YXd)w%eSo^j@_J|nEaEZkN$E9LY0UP>D+$*siHV>?2 zin#%*AcH4a=Fvfq%r9gzW(5wcAgMfDQy@CAksiHj Q7`7~i%@cHk7EJW~4`Yd*761SM diff --git a/Scripts/setup_sprint_input.py b/Scripts/setup_sprint_input.py new file mode 100644 index 0000000..3bea140 --- /dev/null +++ b/Scripts/setup_sprint_input.py @@ -0,0 +1,88 @@ +import unreal + + +def load(path): + asset = unreal.EditorAssetLibrary.load_asset(path) + if not asset: + raise RuntimeError(f"Could not load {path}") + return asset + + +def create_input_action(path): + existing = unreal.EditorAssetLibrary.load_asset(path) + if existing: + return existing + + template_path = "/Game/Input/Actions/IA_Interact" + template = unreal.EditorAssetLibrary.load_asset(template_path) + if not template: + template_path = "/Game/Input/Actions/IA_Jump" + template = unreal.EditorAssetLibrary.load_asset(template_path) + if not template: + raise RuntimeError("Could not load an input action template") + + action = unreal.EditorAssetLibrary.duplicate_asset(template_path, path) + if not action: + raise RuntimeError(f"Could not create {path}") + return action + + +def set_boolean_value_type(action): + action.set_editor_property("value_type", unreal.InputActionValueType.BOOLEAN) + try: + action.set_editor_property("triggers", []) + except Exception as exc: + unreal.log_warning(f"Could not clear sprint triggers; keeping template defaults: {exc}") + unreal.EditorAssetLibrary.save_loaded_asset(action) + + +def mapping_exists(context, action, key_name): + mapping_data = context.get_editor_property("default_key_mappings") + for mapping in list(mapping_data.get_editor_property("mappings")): + mapping_key = mapping.get_editor_property("key") + if ( + mapping.get_editor_property("action") == action + and str(mapping_key.get_editor_property("key_name")) == key_name + ): + return True + return False + + +def map_key(context, action, key_name): + if mapping_exists(context, action, key_name): + unreal.log(f"Mapping already exists: {action.get_name()} -> {key_name}") + return + + key = unreal.Key() + key.set_editor_property("key_name", key_name) + + mapping_data = context.get_editor_property("default_key_mappings") + mappings = list(mapping_data.get_editor_property("mappings")) + new_mapping = unreal.EnhancedActionKeyMapping() + new_mapping.set_editor_property("action", action) + new_mapping.set_editor_property("key", key) + mappings.append(new_mapping) + mapping_data.set_editor_property("mappings", mappings) + context.set_editor_property("default_key_mappings", mapping_data) + + unreal.log(f"Added mapping: {action.get_name()} -> {key_name}") + + +def main(): + sprint_action = create_input_action("/Game/Input/Actions/IA_Sprint") + set_boolean_value_type(sprint_action) + + context = load("/Game/Input/IMC_Default") + map_key(context, sprint_action, "LeftShift") + map_key(context, sprint_action, "Gamepad_LeftThumbstick") + unreal.EditorAssetLibrary.save_loaded_asset(context) + + character_bp = load("/Game/ThirdPerson/Blueprints/BP_ThirdPersonCharacter") + character_cdo = unreal.get_default_object(character_bp.generated_class()) + character_cdo.set_editor_property("SprintAction", sprint_action) + unreal.EditorAssetLibrary.save_loaded_asset(character_bp) + + unreal.log("Agrarian sprint input setup complete.") + + +main() diff --git a/Scripts/verify_sprint_input.py b/Scripts/verify_sprint_input.py new file mode 100644 index 0000000..5aaef7a --- /dev/null +++ b/Scripts/verify_sprint_input.py @@ -0,0 +1,44 @@ +import unreal + + +def load(path): + asset = unreal.EditorAssetLibrary.load_asset(path) + if not asset: + raise RuntimeError(f"Could not load {path}") + return asset + + +def mapping_found(context, action, key_name): + mapping_data = context.get_editor_property("default_key_mappings") + for mapping in list(mapping_data.get_editor_property("mappings")): + mapping_key = mapping.get_editor_property("key") + if ( + mapping.get_editor_property("action") == action + and str(mapping_key.get_editor_property("key_name")) == key_name + ): + return True + return False + + +def main(): + action = load("/Game/Input/Actions/IA_Sprint") + context = load("/Game/Input/IMC_Default") + character_bp = load("/Game/ThirdPerson/Blueprints/BP_ThirdPersonCharacter") + character_cdo = unreal.get_default_object(character_bp.generated_class()) + + missing = [] + for key_name in ["LeftShift", "Gamepad_LeftThumbstick"]: + if not mapping_found(context, action, key_name): + missing.append(f"missing mapping {key_name}") + + assigned_action = character_cdo.get_editor_property("SprintAction") + if assigned_action != action: + missing.append("BP_ThirdPersonCharacter SprintAction is not IA_Sprint") + + if missing: + raise RuntimeError("Sprint input verification failed: " + "; ".join(missing)) + + unreal.log("Agrarian sprint input verification complete.") + + +main() diff --git a/Source/AgrarianGame/AgrarianGameCharacter.cpp b/Source/AgrarianGame/AgrarianGameCharacter.cpp index 1d423a8..b359142 100644 --- a/Source/AgrarianGame/AgrarianGameCharacter.cpp +++ b/Source/AgrarianGame/AgrarianGameCharacter.cpp @@ -16,9 +16,12 @@ #include "EnhancedInputSubsystems.h" #include "InputActionValue.h" #include "AgrarianGame.h" +#include "Net/UnrealNetwork.h" AAgrarianGameCharacter::AAgrarianGameCharacter() { + PrimaryActorTick.bCanEverTick = true; + // Set size for collision capsule GetCapsuleComponent()->InitCapsuleSize(42.f, 96.0f); @@ -35,7 +38,7 @@ AAgrarianGameCharacter::AAgrarianGameCharacter() // instead of recompiling to adjust them GetCharacterMovement()->JumpZVelocity = 500.f; GetCharacterMovement()->AirControl = 0.35f; - GetCharacterMovement()->MaxWalkSpeed = 500.f; + GetCharacterMovement()->MaxWalkSpeed = WalkSpeed; GetCharacterMovement()->MinAnalogWalkSpeed = 20.f; GetCharacterMovement()->BrakingDecelerationWalking = 2000.f; GetCharacterMovement()->BrakingDecelerationFalling = 1500.0f; @@ -60,6 +63,37 @@ AAgrarianGameCharacter::AAgrarianGameCharacter() // are set in the derived blueprint asset named ThirdPersonCharacter (to avoid direct content references in C++) } +void AAgrarianGameCharacter::Tick(float DeltaSeconds) +{ + Super::Tick(DeltaSeconds); + + if (!HasAuthority() || !bWantsToSprint) + { + return; + } + + if (!CanSprint()) + { + SetWantsToSprint(false); + return; + } + + if (GetVelocity().SizeSquared2D() > KINDA_SMALL_NUMBER && SurvivalComponent) + { + SurvivalComponent->SpendStamina(SprintStaminaCostPerSecond * DeltaSeconds); + if (!CanSprint()) + { + SetWantsToSprint(false); + } + } +} + +void AAgrarianGameCharacter::GetLifetimeReplicatedProps(TArray& OutLifetimeProps) const +{ + Super::GetLifetimeReplicatedProps(OutLifetimeProps); + DOREPLIFETIME(AAgrarianGameCharacter, bWantsToSprint); +} + void AAgrarianGameCharacter::SetupPlayerInputComponent(UInputComponent* PlayerInputComponent) { // Set up action bindings @@ -81,6 +115,13 @@ void AAgrarianGameCharacter::SetupPlayerInputComponent(UInputComponent* PlayerIn EnhancedInputComponent->BindAction(InteractAction, ETriggerEvent::Started, this, &AAgrarianGameCharacter::Interact); } + if (SprintAction) + { + EnhancedInputComponent->BindAction(SprintAction, ETriggerEvent::Started, this, &AAgrarianGameCharacter::StartSprint); + EnhancedInputComponent->BindAction(SprintAction, ETriggerEvent::Completed, this, &AAgrarianGameCharacter::StopSprint); + EnhancedInputComponent->BindAction(SprintAction, ETriggerEvent::Canceled, this, &AAgrarianGameCharacter::StopSprint); + } + if (ToggleCameraAction) { EnhancedInputComponent->BindAction(ToggleCameraAction, ETriggerEvent::Started, this, &AAgrarianGameCharacter::ToggleCameraPerspective); @@ -115,6 +156,16 @@ void AAgrarianGameCharacter::Interact() TryInteract(); } +void AAgrarianGameCharacter::StartSprint() +{ + SetWantsToSprint(true); +} + +void AAgrarianGameCharacter::StopSprint() +{ + SetWantsToSprint(false); +} + void AAgrarianGameCharacter::ToggleCameraPerspective() { SetFirstPersonCamera(!bFirstPersonCamera); @@ -155,6 +206,48 @@ void AAgrarianGameCharacter::SetFirstPersonCamera(bool bEnableFirstPerson) } } +void AAgrarianGameCharacter::SetWantsToSprint(bool bNewWantsToSprint) +{ + const bool bAllowedSprintIntent = bNewWantsToSprint && CanSprint(); + if (bWantsToSprint == bAllowedSprintIntent) + { + return; + } + + bWantsToSprint = bAllowedSprintIntent; + ApplyMovementSpeed(); + + if (!HasAuthority()) + { + ServerSetWantsToSprint(bAllowedSprintIntent); + } +} + +bool AAgrarianGameCharacter::CanSprint() const +{ + return SurvivalComponent + && SurvivalComponent->IsAlive() + && SurvivalComponent->Survival.Stamina > MinSprintStamina; +} + +bool AAgrarianGameCharacter::IsSprinting() const +{ + return bWantsToSprint && CanSprint(); +} + +void AAgrarianGameCharacter::ApplyMovementSpeed() +{ + if (UCharacterMovementComponent* MovementComponent = GetCharacterMovement()) + { + MovementComponent->MaxWalkSpeed = IsSprinting() ? SprintSpeed : WalkSpeed; + } +} + +void AAgrarianGameCharacter::OnRep_SprintState() +{ + ApplyMovementSpeed(); +} + void AAgrarianGameCharacter::DoMove(float Right, float Forward) { if (GetController() != nullptr) @@ -240,6 +333,11 @@ void AAgrarianGameCharacter::TryInteract() } } +void AAgrarianGameCharacter::ServerSetWantsToSprint_Implementation(bool bNewWantsToSprint) +{ + SetWantsToSprint(bNewWantsToSprint); +} + void AAgrarianGameCharacter::ServerInteract_Implementation(AActor* TargetActor) { if (!TargetActor || !TargetActor->GetClass()->ImplementsInterface(UAgrarianInteractable::StaticClass())) diff --git a/Source/AgrarianGame/AgrarianGameCharacter.h b/Source/AgrarianGame/AgrarianGameCharacter.h index 346d3cb..d94fe23 100644 --- a/Source/AgrarianGame/AgrarianGameCharacter.h +++ b/Source/AgrarianGame/AgrarianGameCharacter.h @@ -73,6 +73,10 @@ protected: UPROPERTY(EditAnywhere, Category="Input") UInputAction* InteractAction; + /** Hold to sprint while stamina allows it. */ + UPROPERTY(EditAnywhere, Category="Input") + UInputAction* SprintAction; + /** Toggle between third-person and first-person camera views. */ UPROPERTY(EditAnywhere, Category="Input") UInputAction* ToggleCameraAction; @@ -81,6 +85,22 @@ protected: UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Agrarian|Interaction", meta = (ClampMin = "100")) float InteractionDistance = 450.0f; + /** Baseline movement speed before sprint, skill, injury, load, and terrain modifiers. */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Agrarian|Movement", meta = (ClampMin = "0")) + float WalkSpeed = 500.0f; + + /** Short-burst movement speed used by the first sprinting pass. */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Agrarian|Movement", meta = (ClampMin = "0")) + float SprintSpeed = 750.0f; + + /** Stamina spent each second while sprinting and moving. */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Agrarian|Movement", meta = (ClampMin = "0")) + float SprintStaminaCostPerSecond = 18.0f; + + /** Minimum stamina required to start or continue sprinting. */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Agrarian|Movement", meta = (ClampMin = "0", ClampMax = "100")) + float MinSprintStamina = 5.0f; + /** Third-person spring arm distance used when returning from first person. */ UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Agrarian|Camera", meta = (ClampMin = "0")) float ThirdPersonCameraDistance = 400.0f; @@ -93,6 +113,10 @@ protected: UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="Agrarian|Camera", meta = (AllowPrivateAccess = "true")) bool bFirstPersonCamera = false; + /** Replicated player intent to sprint; actual sprinting also depends on stamina and alive state. */ + UPROPERTY(ReplicatedUsing = OnRep_SprintState, VisibleAnywhere, BlueprintReadOnly, Category="Agrarian|Movement", meta = (AllowPrivateAccess = "true")) + bool bWantsToSprint = false; + public: /** Constructor */ @@ -100,6 +124,9 @@ public: protected: + virtual void Tick(float DeltaSeconds) override; + virtual void GetLifetimeReplicatedProps(TArray& OutLifetimeProps) const override; + /** Initialize input action bindings */ virtual void SetupPlayerInputComponent(class UInputComponent* PlayerInputComponent) override; @@ -114,12 +141,30 @@ protected: /** Called for interaction input */ void Interact(); + /** Called when sprint input is pressed. */ + void StartSprint(); + + /** Called when sprint input is released. */ + void StopSprint(); + /** Called for camera perspective toggle input */ void ToggleCameraPerspective(); /** Applies local camera presentation state. */ void SetFirstPersonCamera(bool bEnableFirstPerson); + /** Applies the requested sprint intent locally and, when needed, on the server. */ + void SetWantsToSprint(bool bNewWantsToSprint); + + /** Returns true when the character has enough survival state to sprint. */ + bool CanSprint() const; + + /** Applies current walk or sprint speed to character movement. */ + void ApplyMovementSpeed(); + + UFUNCTION() + void OnRep_SprintState(); + public: /** Handles move inputs from either controls or UI interfaces */ @@ -146,10 +191,18 @@ public: UFUNCTION(BlueprintPure, Category="Agrarian|Camera") bool IsFirstPersonCamera() const { return bFirstPersonCamera; } + /** Returns true when sprint intent and current stamina allow sprinting. */ + UFUNCTION(BlueprintPure, Category="Agrarian|Movement") + bool IsSprinting() const; + /** Server-authoritative interaction entry point. */ UFUNCTION(Server, Reliable) void ServerInteract(AActor* TargetActor); + /** Server-authoritative sprint intent update. */ + UFUNCTION(Server, Reliable) + void ServerSetWantsToSprint(bool bNewWantsToSprint); + public: /** Returns CameraBoom subobject **/