Connect movement speed modifiers
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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,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,
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user