Connect movement speed modifiers

This commit is contained in:
2026-05-15 14:22:00 -07:00
parent 775925c03d
commit 3a6b1da53b
8 changed files with 231 additions and 4 deletions
+2 -2
View File
@@ -374,7 +374,7 @@ Target deliverable: A small group can join a server, spawn into one biome, gathe
- [x] Implement sprinting.
- [x] Define real-world baseline walking speed. Decision: baseline adult walking speed is `1.4 m/s` (`140 Unreal units/s`), with MVP tuning allowance up to about `1.6 m/s` for brisk walking. Movement speed does not scale with the `4 real hours = 1 in-game day` calendar.
- [x] Define real-world baseline running speed. Decision: sustainable adult running target is `3.0 m/s` (`300 Unreal units/s`); MVP sprint is a short burst at `5.5 m/s` (`550 Unreal units/s`) with stamina limits. Movement speed does not scale with the calendar.
- [ ] Connect movement speed to age, condition, strength, endurance, hunger, thirst, injury, carried weight, and terrain.
- [x] Connect movement speed to age, condition, strength, endurance, hunger, thirst, injury, carried weight, and terrain. Implemented with live survival/inventory modifiers plus replicated age, condition, strength, endurance, and terrain hooks.
- [ ] Implement crouching if needed.
- [x] Implement jumping if needed.
- [x] Implement interaction trace.
@@ -1465,4 +1465,4 @@ Earliest incomplete foundation items:
Immediate next item:
- [ ] Connect movement speed to age, condition, strength, endurance, hunger, thirst, injury, carried weight, and terrain.
- [ ] Implement crouching if needed.
+46
View File
@@ -115,3 +115,49 @@ Later movement work should separate:
- stamina cost and recovery;
- injury/load/terrain modifiers;
- skill, age, care history, and condition effects.
## Movement Modifier Implementation
MVP movement speed is now calculated as:
`base speed * movement modifier`
The base speed is either the walking baseline or the short sprint baseline.
The movement modifier is clamped so early gameplay remains controllable while
still allowing meaningful penalties.
Live inputs:
- age placeholder, replicated on the character;
- physical condition placeholder, replicated on the character;
- strength placeholder, replicated on the character;
- endurance placeholder, replicated on the character;
- hunger from the survival component;
- thirst from the survival component;
- injury severity from the survival component;
- carried item weight from the inventory component;
- terrain movement multiplier hook, replicated on the character.
Current rules:
- dead characters have a `0.0` survival movement multiplier;
- hunger below `50` gradually reduces movement to `0.85`;
- thirst below `50` gradually reduces movement to `0.75`;
- injury can reduce movement down to `0.5` at maximum injury severity;
- carried weight has no penalty under comfortable carry weight;
- carried weight scales down to `0.65` as it approaches heavy carry weight;
- carried weight at or above heavy carry weight uses a `0.45` carry multiplier;
- strength increases the effective comfort and heavy carry thresholds;
- endurance improves the movement multiplier and reduces sprint stamina cost;
- terrain can slow or slightly boost movement through a replicated multiplier.
Reserved systems:
- age is currently an editable/replicated placeholder until the lifecycle system
owns it;
- physical condition is currently an editable/replicated placeholder until care,
illness, sleep, and long-term health systems own it;
- strength and endurance are currently editable/replicated placeholders until
character progression owns them;
- terrain is currently a hook for later surface, slope, mud, water, road, and
biome systems.
+7
View File
@@ -6,6 +6,13 @@ MOVEMENT_DEFAULTS = {
"SprintSpeed": 550.0,
"SprintStaminaCostPerSecond": 28.0,
"MinSprintStamina": 5.0,
"AgeYears": 25.0,
"PhysicalConditionMultiplier": 1.0,
"StrengthMultiplier": 1.0,
"EnduranceMultiplier": 1.0,
"ComfortableCarryWeight": 25.0,
"HeavyCarryWeight": 60.0,
"TerrainMovementMultiplier": 1.0,
}
+7
View File
@@ -7,6 +7,13 @@ MOVEMENT_DEFAULTS = {
"SprintSpeed": 550.0,
"SprintStaminaCostPerSecond": 28.0,
"MinSprintStamina": 5.0,
"AgeYears": 25.0,
"PhysicalConditionMultiplier": 1.0,
"StrengthMultiplier": 1.0,
"EnduranceMultiplier": 1.0,
"ComfortableCarryWeight": 25.0,
"HeavyCarryWeight": 60.0,
"TerrainMovementMultiplier": 1.0,
}
+112 -2
View File
@@ -67,6 +67,8 @@ void AAgrarianGameCharacter::Tick(float DeltaSeconds)
{
Super::Tick(DeltaSeconds);
ApplyMovementSpeed();
if (!HasAuthority() || !bWantsToSprint)
{
return;
@@ -80,7 +82,8 @@ void AAgrarianGameCharacter::Tick(float DeltaSeconds)
if (GetVelocity().SizeSquared2D() > KINDA_SMALL_NUMBER && SurvivalComponent)
{
SurvivalComponent->SpendStamina(SprintStaminaCostPerSecond * DeltaSeconds);
const float EffectiveEndurance = FMath::Max(0.25f, EnduranceMultiplier);
SurvivalComponent->SpendStamina((SprintStaminaCostPerSecond / EffectiveEndurance) * DeltaSeconds);
if (!CanSprint())
{
SetWantsToSprint(false);
@@ -92,6 +95,11 @@ void AAgrarianGameCharacter::GetLifetimeReplicatedProps(TArray<FLifetimeProperty
{
Super::GetLifetimeReplicatedProps(OutLifetimeProps);
DOREPLIFETIME(AAgrarianGameCharacter, bWantsToSprint);
DOREPLIFETIME(AAgrarianGameCharacter, AgeYears);
DOREPLIFETIME(AAgrarianGameCharacter, PhysicalConditionMultiplier);
DOREPLIFETIME(AAgrarianGameCharacter, StrengthMultiplier);
DOREPLIFETIME(AAgrarianGameCharacter, EnduranceMultiplier);
DOREPLIFETIME(AAgrarianGameCharacter, TerrainMovementMultiplier);
}
void AAgrarianGameCharacter::SetupPlayerInputComponent(UInputComponent* PlayerInputComponent)
@@ -235,14 +243,116 @@ bool AAgrarianGameCharacter::IsSprinting() const
return bWantsToSprint && CanSprint();
}
float AAgrarianGameCharacter::GetCurrentCarryWeight() const
{
return InventoryComponent ? InventoryComponent->GetTotalWeight() : 0.0f;
}
float AAgrarianGameCharacter::GetCurrentMovementSpeedMultiplier() const
{
return CalculateMovementSpeedMultiplier();
}
void AAgrarianGameCharacter::SetTerrainMovementMultiplier(float NewTerrainMovementMultiplier)
{
TerrainMovementMultiplier = FMath::Clamp(NewTerrainMovementMultiplier, 0.25f, 1.25f);
ApplyMovementSpeed();
}
void AAgrarianGameCharacter::ApplyMovementSpeed()
{
if (UCharacterMovementComponent* MovementComponent = GetCharacterMovement())
{
MovementComponent->MaxWalkSpeed = IsSprinting() ? SprintSpeed : WalkSpeed;
const float BaseSpeed = IsSprinting() ? SprintSpeed : WalkSpeed;
MovementComponent->MaxWalkSpeed = FMath::Max(0.0f, BaseSpeed * CalculateMovementSpeedMultiplier());
}
}
float AAgrarianGameCharacter::CalculateMovementSpeedMultiplier() const
{
const float TraitMultiplier = FMath::Clamp(PhysicalConditionMultiplier, 0.25f, 1.25f)
* FMath::Clamp(EnduranceMultiplier, 0.25f, 2.0f);
return FMath::Clamp(
CalculateAgeMovementMultiplier()
* TraitMultiplier
* CalculateSurvivalMovementMultiplier()
* CalculateCarryWeightMovementMultiplier()
* FMath::Clamp(TerrainMovementMultiplier, 0.25f, 1.25f),
0.15f,
1.35f);
}
float AAgrarianGameCharacter::CalculateAgeMovementMultiplier() const
{
if (AgeYears < 13.0f)
{
return 0.65f;
}
if (AgeYears < 18.0f)
{
return FMath::GetMappedRangeValueClamped(FVector2D(13.0f, 18.0f), FVector2D(0.8f, 1.0f), AgeYears);
}
if (AgeYears <= 50.0f)
{
return 1.0f;
}
if (AgeYears <= 70.0f)
{
return FMath::GetMappedRangeValueClamped(FVector2D(50.0f, 70.0f), FVector2D(1.0f, 0.75f), AgeYears);
}
return 0.65f;
}
float AAgrarianGameCharacter::CalculateSurvivalMovementMultiplier() const
{
if (!SurvivalComponent || !SurvivalComponent->IsAlive())
{
return 0.0f;
}
const FAgrarianSurvivalSnapshot& Survival = SurvivalComponent->Survival;
const float HungerMultiplier = Survival.Hunger >= 50.0f
? 1.0f
: FMath::GetMappedRangeValueClamped(FVector2D(0.0f, 50.0f), FVector2D(0.85f, 1.0f), Survival.Hunger);
const float ThirstMultiplier = Survival.Thirst >= 50.0f
? 1.0f
: FMath::GetMappedRangeValueClamped(FVector2D(0.0f, 50.0f), FVector2D(0.75f, 1.0f), Survival.Thirst);
const float InjuryMultiplier = FMath::GetMappedRangeValueClamped(
FVector2D(0.0f, 100.0f),
FVector2D(1.0f, 0.5f),
Survival.InjurySeverity);
return HungerMultiplier * ThirstMultiplier * InjuryMultiplier;
}
float AAgrarianGameCharacter::CalculateCarryWeightMovementMultiplier() const
{
const float Strength = FMath::Clamp(StrengthMultiplier, 0.25f, 2.0f);
const float EffectiveComfortWeight = ComfortableCarryWeight * Strength;
const float EffectiveHeavyWeight = HeavyCarryWeight * Strength;
const float CurrentCarryWeight = GetCurrentCarryWeight();
if (CurrentCarryWeight <= EffectiveComfortWeight || EffectiveHeavyWeight <= EffectiveComfortWeight)
{
return 1.0f;
}
if (CurrentCarryWeight >= EffectiveHeavyWeight)
{
return 0.45f;
}
return FMath::GetMappedRangeValueClamped(
FVector2D(EffectiveComfortWeight, EffectiveHeavyWeight),
FVector2D(1.0f, 0.65f),
CurrentCarryWeight);
}
void AAgrarianGameCharacter::OnRep_SprintState()
{
ApplyMovementSpeed();
@@ -101,6 +101,34 @@ protected:
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Agrarian|Movement", meta = (ClampMin = "0", ClampMax = "100"))
float MinSprintStamina = 5.0f;
/** Age hook used by movement until full lifecycle/aging systems own it. */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Replicated, Category="Agrarian|Movement|Modifiers", meta = (ClampMin = "0"))
float AgeYears = 25.0f;
/** General physical condition scalar reserved for care, illness, sleep, and long-term state. */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Replicated, Category="Agrarian|Movement|Modifiers", meta = (ClampMin = "0.25", ClampMax = "1.25"))
float PhysicalConditionMultiplier = 1.0f;
/** Strength scalar that mainly offsets carried-weight penalties. */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Replicated, Category="Agrarian|Movement|Modifiers", meta = (ClampMin = "0.25", ClampMax = "2.0"))
float StrengthMultiplier = 1.0f;
/** Endurance scalar that improves stamina efficiency and movement resilience. */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Replicated, Category="Agrarian|Movement|Modifiers", meta = (ClampMin = "0.25", ClampMax = "2.0"))
float EnduranceMultiplier = 1.0f;
/** Comfort carry capacity before speed penalties, in item-weight units. */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Agrarian|Movement|Modifiers", meta = (ClampMin = "0"))
float ComfortableCarryWeight = 25.0f;
/** Severe carry capacity where movement is heavily reduced, in item-weight units. */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Agrarian|Movement|Modifiers", meta = (ClampMin = "0"))
float HeavyCarryWeight = 60.0f;
/** Terrain hook for later surface/volume systems. Values below one slow the character. */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Replicated, Category="Agrarian|Movement|Modifiers", meta = (ClampMin = "0.25", ClampMax = "1.25"))
float TerrainMovementMultiplier = 1.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;
@@ -162,6 +190,13 @@ protected:
/** Applies current walk or sprint speed to character movement. */
void ApplyMovementSpeed();
/** Calculates the current multiplier from survival, carried weight, terrain, and placeholder traits. */
float CalculateMovementSpeedMultiplier() const;
float CalculateAgeMovementMultiplier() const;
float CalculateSurvivalMovementMultiplier() const;
float CalculateCarryWeightMovementMultiplier() const;
UFUNCTION()
void OnRep_SprintState();
@@ -195,6 +230,15 @@ public:
UFUNCTION(BlueprintPure, Category="Agrarian|Movement")
bool IsSprinting() const;
UFUNCTION(BlueprintPure, Category="Agrarian|Movement")
float GetCurrentCarryWeight() const;
UFUNCTION(BlueprintPure, Category="Agrarian|Movement")
float GetCurrentMovementSpeedMultiplier() const;
UFUNCTION(BlueprintCallable, Category="Agrarian|Movement")
void SetTerrainMovementMultiplier(float NewTerrainMovementMultiplier);
/** Server-authoritative interaction entry point. */
UFUNCTION(Server, Reliable)
void ServerInteract(AActor* TargetActor);
@@ -33,6 +33,16 @@ int32 UAgrarianInventoryComponent::GetItemCount(FName ItemId) const
return Count;
}
float UAgrarianInventoryComponent::GetTotalWeight() const
{
float TotalWeight = 0.0f;
for (const FAgrarianItemStack& Stack : Items)
{
TotalWeight += FMath::Max(0.0f, Stack.UnitWeight) * FMath::Max(0, Stack.Quantity);
}
return TotalWeight;
}
bool UAgrarianInventoryComponent::AddItem(const FAgrarianItemStack& Stack)
{
if (!GetOwner() || !GetOwner()->HasAuthority() || !Stack.IsValidStack())
@@ -34,6 +34,9 @@ public:
UFUNCTION(BlueprintCallable, Category = "Agrarian|Inventory")
int32 GetItemCount(FName ItemId) const;
UFUNCTION(BlueprintCallable, Category = "Agrarian|Inventory")
float GetTotalWeight() const;
UFUNCTION(BlueprintCallable, Category = "Agrarian|Inventory")
bool AddItem(const FAgrarianItemStack& Stack);