Add vegetation ignition risk checks
This commit is contained in:
@@ -838,7 +838,7 @@ Target deliverable: A small group can join a server, spawn into one biome, gathe
|
||||
- [x] Add gathering sounds. Added spatialized resource-node gathering audio hooks with assignable normal/depleted gathering cues and a server-authoritative multicast trigger after successful harvests, keeping multiplayer clients aligned while remaining silent until audio assets are assigned.
|
||||
- [x] Add fire sounds. Added campfire loop, ignition, and extinguish audio hooks with spatialized components, replicated lit-state loop control, and server-triggered multicast event cues so fire audio follows the authoritative campfire state once assets are assigned.
|
||||
- [x] Add unattended and poorly maintained fire risk for campfires and other open-flame sources. Added server-side campfire risk state for lit duration, seconds since maintenance, cleared area, containment, high fuel, wet weather mitigation, and a replicated `FireRiskScore` that later ignition/spread systems can consume.
|
||||
- [ ] Add grass and forest ignition checks from irresponsible fire placement, wind/weather, dry fuel, nearby vegetation, and burn duration.
|
||||
- [x] Add grass and forest ignition checks from irresponsible fire placement, wind/weather, dry fuel, nearby vegetation, and burn duration. Added foliage fuel counting, campfire vegetation ignition risk scores, grass/brush and forest ignition flags, and weather/wind/burn-duration modifiers so unsafe fire placement near dry fuel can now become a server-authoritative ignition risk.
|
||||
- [ ] Add shelter/structure ignition risk when fires are placed too close to primitive shelters, wood piles, flammable crafting stations, or settlement objects.
|
||||
- [ ] Add server-authoritative fire spread rules for grass, brush, trees, shelters, and other burnable actors, including fuel, distance, wind, weather, and suppression hooks.
|
||||
- [ ] Add fire maintenance gameplay so watched, cleared, contained, or extinguished fires are safe, while neglected fires can become dangerous.
|
||||
|
||||
@@ -345,6 +345,14 @@ when excessive fuel is burning, and is reduced by maintenance, cleared area,
|
||||
containment, and wet weather. This first risk layer does not yet ignite nearby
|
||||
vegetation or structures; later 0.1.P items consume the replicated risk score.
|
||||
|
||||
Grass and forest ignition checks consume that risk score. Campfires query
|
||||
`AAgrarianFoliagePatch` for nearby grass, shrub, and tree instance counts inside
|
||||
`VegetationIgnitionCheckRadius`. Unsafe placement near dry fuel accumulates
|
||||
separate replicated grass/brush and forest ignition risk, adjusted by burn
|
||||
duration, wind speed, current weather, cleared-area maintenance, and the
|
||||
campfire's current risk ratio. Reaching the threshold marks the relevant
|
||||
ignition flag, while later spread rules decide how active fires propagate.
|
||||
|
||||
Campfires expose native extinguish logic through `AAgrarianCampfire::Extinguish`.
|
||||
Extinguishing clears remaining fuel, turns off replicated lit state, and reuses
|
||||
the same visual update path as natural fuel depletion.
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Verify grass/forest ignition checks use fire risk, foliage fuel, and weather."""
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
FOLIAGE_H = ROOT / "Source" / "AgrarianGame" / "AgrarianFoliagePatch.h"
|
||||
FOLIAGE_CPP = ROOT / "Source" / "AgrarianGame" / "AgrarianFoliagePatch.cpp"
|
||||
FIRE_H = ROOT / "Source" / "AgrarianGame" / "AgrarianCampfire.h"
|
||||
FIRE_CPP = ROOT / "Source" / "AgrarianGame" / "AgrarianCampfire.cpp"
|
||||
TDD = ROOT / "Docs" / "TechnicalDesignDocument.md"
|
||||
ROADMAP = ROOT / "AGRARIAN_DEVELOPMENT_ROADMAP.md"
|
||||
|
||||
REQUIRED = {
|
||||
FOLIAGE_H: [
|
||||
"GetFuelCountsNearLocation",
|
||||
"GetDryVegetationFuelScoreNearLocation",
|
||||
"CountInstancesNearLocation",
|
||||
],
|
||||
FOLIAGE_CPP: [
|
||||
"AAgrarianFoliagePatch::GetFuelCountsNearLocation",
|
||||
"AAgrarianFoliagePatch::GetDryVegetationFuelScoreNearLocation",
|
||||
"AAgrarianFoliagePatch::CountInstancesNearLocation",
|
||||
"GetInstanceTransform",
|
||||
"FVector::DistSquared2D",
|
||||
],
|
||||
FIRE_H: [
|
||||
"GrassIgnitionRiskScore",
|
||||
"ForestIgnitionRiskScore",
|
||||
"bGrassOrBrushIgnited",
|
||||
"bForestFuelIgnited",
|
||||
"VegetationIgnitionCheckRadius",
|
||||
"VegetationIgnitionFuelScoreThreshold",
|
||||
"UpdateVegetationIgnitionRisk",
|
||||
"GetVegetationFuelScoreNearFire",
|
||||
"GetVegetationIgnitionWeatherMultiplier",
|
||||
],
|
||||
FIRE_CPP: [
|
||||
"#include \"AgrarianFoliagePatch.h\"",
|
||||
"DOREPLIFETIME(AAgrarianCampfire, GrassIgnitionRiskScore)",
|
||||
"DOREPLIFETIME(AAgrarianCampfire, ForestIgnitionRiskScore)",
|
||||
"UpdateVegetationIgnitionRisk(DeltaSeconds);",
|
||||
"UGameplayStatics::GetAllActorsOfClass(this, AAgrarianFoliagePatch::StaticClass()",
|
||||
"GameState->ActiveWeatherInputs.WindSpeedKmh",
|
||||
"EAgrarianWeatherType::Rain",
|
||||
"EAgrarianWeatherType::Storm",
|
||||
"EAgrarianWeatherType::ColdWind",
|
||||
"LitDurationSeconds",
|
||||
"GetFireRiskRatio()",
|
||||
"bGrassOrBrushIgnited = GrassIgnitionRiskScore >= 100.0f",
|
||||
"bForestFuelIgnited = ForestIgnitionRiskScore >= 100.0f",
|
||||
],
|
||||
TDD: [
|
||||
"Grass and forest ignition checks consume that risk score",
|
||||
"`AAgrarianFoliagePatch`",
|
||||
"wind speed",
|
||||
"burn\nduration",
|
||||
],
|
||||
ROADMAP: [
|
||||
"[x] Add grass and forest ignition checks",
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
def main() -> None:
|
||||
missing = []
|
||||
for path, snippets in REQUIRED.items():
|
||||
text = path.read_text(encoding="utf-8")
|
||||
for snippet in snippets:
|
||||
if snippet not in text:
|
||||
missing.append(f"{path.relative_to(ROOT)} missing {snippet!r}")
|
||||
if missing:
|
||||
raise SystemExit("FAILED: " + "; ".join(missing))
|
||||
print("OK: vegetation ignition checks are wired to fire risk, foliage, and weather.")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -1,6 +1,7 @@
|
||||
// Copyright Pacificao. All Rights Reserved.
|
||||
|
||||
#include "AgrarianCampfire.h"
|
||||
#include "AgrarianFoliagePatch.h"
|
||||
#include "AgrarianGameCharacter.h"
|
||||
#include "AgrarianGameState.h"
|
||||
#include "AgrarianInventoryComponent.h"
|
||||
@@ -141,6 +142,7 @@ void AAgrarianCampfire::Tick(float DeltaSeconds)
|
||||
}
|
||||
|
||||
UpdateFireRisk(DeltaSeconds);
|
||||
UpdateVegetationIgnitionRisk(DeltaSeconds);
|
||||
WarmNearbyCharacters(DeltaSeconds);
|
||||
}
|
||||
}
|
||||
@@ -158,6 +160,10 @@ void AAgrarianCampfire::GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& Ou
|
||||
DOREPLIFETIME(AAgrarianCampfire, SecondsSinceMaintenance);
|
||||
DOREPLIFETIME(AAgrarianCampfire, bFireAreaCleared);
|
||||
DOREPLIFETIME(AAgrarianCampfire, bFireContained);
|
||||
DOREPLIFETIME(AAgrarianCampfire, GrassIgnitionRiskScore);
|
||||
DOREPLIFETIME(AAgrarianCampfire, ForestIgnitionRiskScore);
|
||||
DOREPLIFETIME(AAgrarianCampfire, bGrassOrBrushIgnited);
|
||||
DOREPLIFETIME(AAgrarianCampfire, bForestFuelIgnited);
|
||||
}
|
||||
|
||||
FText AAgrarianCampfire::GetInteractionText_Implementation(const AAgrarianGameCharacter* Interactor) const
|
||||
@@ -201,6 +207,10 @@ void AAgrarianCampfire::CapturePersistentState_Implementation(UAgrarianPersisten
|
||||
PersistentComponent->NumberState.Add(TEXT("seconds_since_maintenance"), SecondsSinceMaintenance);
|
||||
PersistentComponent->NumberState.Add(TEXT("fire_area_cleared"), bFireAreaCleared ? 1.0f : 0.0f);
|
||||
PersistentComponent->NumberState.Add(TEXT("fire_contained"), bFireContained ? 1.0f : 0.0f);
|
||||
PersistentComponent->NumberState.Add(TEXT("grass_ignition_risk_score"), GrassIgnitionRiskScore);
|
||||
PersistentComponent->NumberState.Add(TEXT("forest_ignition_risk_score"), ForestIgnitionRiskScore);
|
||||
PersistentComponent->NumberState.Add(TEXT("grass_or_brush_ignited"), bGrassOrBrushIgnited ? 1.0f : 0.0f);
|
||||
PersistentComponent->NumberState.Add(TEXT("forest_fuel_ignited"), bForestFuelIgnited ? 1.0f : 0.0f);
|
||||
}
|
||||
|
||||
void AAgrarianCampfire::ApplyPersistentState_Implementation(UAgrarianPersistentActorComponent* PersistentComponent)
|
||||
@@ -220,6 +230,10 @@ void AAgrarianCampfire::ApplyPersistentState_Implementation(UAgrarianPersistentA
|
||||
const float* SavedSecondsSinceMaintenance = PersistentComponent->NumberState.Find(TEXT("seconds_since_maintenance"));
|
||||
const float* SavedAreaCleared = PersistentComponent->NumberState.Find(TEXT("fire_area_cleared"));
|
||||
const float* SavedContained = PersistentComponent->NumberState.Find(TEXT("fire_contained"));
|
||||
const float* SavedGrassIgnitionRisk = PersistentComponent->NumberState.Find(TEXT("grass_ignition_risk_score"));
|
||||
const float* SavedForestIgnitionRisk = PersistentComponent->NumberState.Find(TEXT("forest_ignition_risk_score"));
|
||||
const float* SavedGrassIgnited = PersistentComponent->NumberState.Find(TEXT("grass_or_brush_ignited"));
|
||||
const float* SavedForestIgnited = PersistentComponent->NumberState.Find(TEXT("forest_fuel_ignited"));
|
||||
|
||||
if (SavedFuelSeconds)
|
||||
{
|
||||
@@ -266,6 +280,26 @@ void AAgrarianCampfire::ApplyPersistentState_Implementation(UAgrarianPersistentA
|
||||
bFireContained = *SavedContained > 0.5f;
|
||||
}
|
||||
|
||||
if (SavedGrassIgnitionRisk)
|
||||
{
|
||||
GrassIgnitionRiskScore = FMath::Clamp(*SavedGrassIgnitionRisk, 0.0f, 100.0f);
|
||||
}
|
||||
|
||||
if (SavedForestIgnitionRisk)
|
||||
{
|
||||
ForestIgnitionRiskScore = FMath::Clamp(*SavedForestIgnitionRisk, 0.0f, 100.0f);
|
||||
}
|
||||
|
||||
if (SavedGrassIgnited)
|
||||
{
|
||||
bGrassOrBrushIgnited = *SavedGrassIgnited > 0.5f;
|
||||
}
|
||||
|
||||
if (SavedForestIgnited)
|
||||
{
|
||||
bForestFuelIgnited = *SavedForestIgnited > 0.5f;
|
||||
}
|
||||
|
||||
SetLit(SavedLit && *SavedLit > 0.5f && FuelSeconds > 0.0f);
|
||||
}
|
||||
|
||||
@@ -291,6 +325,8 @@ void AAgrarianCampfire::Extinguish()
|
||||
{
|
||||
FuelSeconds = 0.0f;
|
||||
FireRiskScore = 0.0f;
|
||||
GrassIgnitionRiskScore = 0.0f;
|
||||
ForestIgnitionRiskScore = 0.0f;
|
||||
LitDurationSeconds = 0.0f;
|
||||
SecondsSinceMaintenance = 0.0f;
|
||||
SetLit(false);
|
||||
@@ -517,3 +553,99 @@ float AAgrarianCampfire::GetFireRiskGrowthPerSecond() const
|
||||
|
||||
return FMath::Max(0.0f, RiskGrowth);
|
||||
}
|
||||
|
||||
void AAgrarianCampfire::UpdateVegetationIgnitionRisk(float DeltaSeconds)
|
||||
{
|
||||
if (!HasAuthority() || !bLit || bFireAreaCleared)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
float GrassFuelScore = 0.0f;
|
||||
float ForestFuelScore = 0.0f;
|
||||
const float TotalFuelScore = GetVegetationFuelScoreNearFire(GrassFuelScore, ForestFuelScore);
|
||||
if (TotalFuelScore < VegetationIgnitionFuelScoreThreshold)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
const float BurnDurationMultiplier = FMath::GetMappedRangeValueClamped(
|
||||
FVector2D(30.0f, 300.0f),
|
||||
FVector2D(0.35f, 1.4f),
|
||||
LitDurationSeconds);
|
||||
const float RiskMultiplier = GetVegetationIgnitionWeatherMultiplier() * BurnDurationMultiplier * FMath::Max(0.0f, GetFireRiskRatio());
|
||||
const float BaseRisk = VegetationIgnitionRiskPerSecond * DeltaSeconds * RiskMultiplier;
|
||||
|
||||
if (GrassFuelScore > 0.0f && !bGrassOrBrushIgnited)
|
||||
{
|
||||
GrassIgnitionRiskScore = FMath::Clamp(GrassIgnitionRiskScore + (BaseRisk * GrassFuelScore), 0.0f, 100.0f);
|
||||
bGrassOrBrushIgnited = GrassIgnitionRiskScore >= 100.0f;
|
||||
}
|
||||
|
||||
if (ForestFuelScore > 0.0f && !bForestFuelIgnited)
|
||||
{
|
||||
ForestIgnitionRiskScore = FMath::Clamp(ForestIgnitionRiskScore + (BaseRisk * ForestFuelScore * 0.6f), 0.0f, 100.0f);
|
||||
bForestFuelIgnited = ForestIgnitionRiskScore >= 100.0f;
|
||||
}
|
||||
}
|
||||
|
||||
float AAgrarianCampfire::GetVegetationFuelScoreNearFire(float& OutGrassFuelScore, float& OutForestFuelScore) const
|
||||
{
|
||||
OutGrassFuelScore = 0.0f;
|
||||
OutForestFuelScore = 0.0f;
|
||||
|
||||
TArray<AActor*> FoliageActors;
|
||||
UGameplayStatics::GetAllActorsOfClass(this, AAgrarianFoliagePatch::StaticClass(), FoliageActors);
|
||||
for (AActor* Actor : FoliageActors)
|
||||
{
|
||||
const AAgrarianFoliagePatch* FoliagePatch = Cast<AAgrarianFoliagePatch>(Actor);
|
||||
if (!FoliagePatch)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
int32 GrassCount = 0;
|
||||
int32 ShrubCount = 0;
|
||||
int32 TreeCount = 0;
|
||||
FoliagePatch->GetFuelCountsNearLocation(GetActorLocation(), VegetationIgnitionCheckRadius, GrassCount, ShrubCount, TreeCount);
|
||||
OutGrassFuelScore += static_cast<float>(GrassCount) + (static_cast<float>(ShrubCount) * 1.5f);
|
||||
OutForestFuelScore += static_cast<float>(TreeCount) * 3.0f;
|
||||
}
|
||||
|
||||
return OutGrassFuelScore + OutForestFuelScore;
|
||||
}
|
||||
|
||||
float AAgrarianCampfire::GetVegetationIgnitionWeatherMultiplier() const
|
||||
{
|
||||
float Multiplier = 1.0f;
|
||||
if (const UWorld* World = GetWorld())
|
||||
{
|
||||
if (const AAgrarianGameState* GameState = World->GetGameState<AAgrarianGameState>())
|
||||
{
|
||||
if (GameState->ActiveWeatherInputs.bHasProviderData)
|
||||
{
|
||||
Multiplier *= FMath::GetMappedRangeValueClamped(
|
||||
FVector2D(0.0f, 55.0f),
|
||||
FVector2D(0.85f, 1.65f),
|
||||
GameState->ActiveWeatherInputs.WindSpeedKmh);
|
||||
}
|
||||
|
||||
switch (GameState->Weather)
|
||||
{
|
||||
case EAgrarianWeatherType::Rain:
|
||||
Multiplier *= 0.2f;
|
||||
break;
|
||||
case EAgrarianWeatherType::Storm:
|
||||
Multiplier *= 0.1f;
|
||||
break;
|
||||
case EAgrarianWeatherType::ColdWind:
|
||||
Multiplier *= 1.25f;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return FMath::Max(0.0f, Multiplier);
|
||||
}
|
||||
|
||||
@@ -114,6 +114,27 @@ public:
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|Fire|Risk", meta = (ClampMin = "0", ClampMax = "1"))
|
||||
float ContainedFireRiskMultiplier = 0.35f;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Replicated, Category = "Agrarian|Fire|Vegetation", meta = (ClampMin = "0", ClampMax = "100"))
|
||||
float GrassIgnitionRiskScore = 0.0f;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Replicated, Category = "Agrarian|Fire|Vegetation", meta = (ClampMin = "0", ClampMax = "100"))
|
||||
float ForestIgnitionRiskScore = 0.0f;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Replicated, Category = "Agrarian|Fire|Vegetation")
|
||||
bool bGrassOrBrushIgnited = false;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Replicated, Category = "Agrarian|Fire|Vegetation")
|
||||
bool bForestFuelIgnited = false;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|Fire|Vegetation", meta = (ClampMin = "0"))
|
||||
float VegetationIgnitionCheckRadius = 700.0f;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|Fire|Vegetation", meta = (ClampMin = "0"))
|
||||
float VegetationIgnitionFuelScoreThreshold = 8.0f;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|Fire|Vegetation", meta = (ClampMin = "0"))
|
||||
float VegetationIgnitionRiskPerSecond = 0.035f;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|Fire|Weather", meta = (ClampMin = "1"))
|
||||
float RainFuelDrainMultiplier = 1.5f;
|
||||
|
||||
@@ -178,4 +199,7 @@ protected:
|
||||
void WarmNearbyCharacters(float DeltaSeconds);
|
||||
void UpdateFireRisk(float DeltaSeconds);
|
||||
float GetFireRiskGrowthPerSecond() const;
|
||||
void UpdateVegetationIgnitionRisk(float DeltaSeconds);
|
||||
float GetVegetationFuelScoreNearFire(float& OutGrassFuelScore, float& OutForestFuelScore) const;
|
||||
float GetVegetationIgnitionWeatherMultiplier() const;
|
||||
};
|
||||
|
||||
@@ -106,3 +106,53 @@ int32 AAgrarianFoliagePatch::GetGrassInstanceCount() const
|
||||
{
|
||||
return GrassInstances ? GrassInstances->GetInstanceCount() : 0;
|
||||
}
|
||||
|
||||
void AAgrarianFoliagePatch::GetFuelCountsNearLocation(
|
||||
const FVector& WorldLocation,
|
||||
float Radius,
|
||||
int32& OutGrassCount,
|
||||
int32& OutShrubCount,
|
||||
int32& OutTreeCount) const
|
||||
{
|
||||
SCOPE_CYCLE_COUNTER(STAT_AgrarianFoliageInstanceMutation);
|
||||
TRACE_CPUPROFILER_EVENT_SCOPE(AgrarianFoliageFuelCountsNearLocation);
|
||||
|
||||
OutGrassCount = CountInstancesNearLocation(GrassInstances, WorldLocation, Radius);
|
||||
OutShrubCount = CountInstancesNearLocation(ShrubInstances, WorldLocation, Radius);
|
||||
OutTreeCount = CountInstancesNearLocation(TreeInstances, WorldLocation, Radius);
|
||||
}
|
||||
|
||||
float AAgrarianFoliagePatch::GetDryVegetationFuelScoreNearLocation(const FVector& WorldLocation, float Radius) const
|
||||
{
|
||||
int32 GrassCount = 0;
|
||||
int32 ShrubCount = 0;
|
||||
int32 TreeCount = 0;
|
||||
GetFuelCountsNearLocation(WorldLocation, Radius, GrassCount, ShrubCount, TreeCount);
|
||||
|
||||
return static_cast<float>(GrassCount) + (static_cast<float>(ShrubCount) * 2.0f) + (static_cast<float>(TreeCount) * 4.0f);
|
||||
}
|
||||
|
||||
int32 AAgrarianFoliagePatch::CountInstancesNearLocation(
|
||||
const UHierarchicalInstancedStaticMeshComponent* Component,
|
||||
const FVector& WorldLocation,
|
||||
float Radius) const
|
||||
{
|
||||
if (!Component || Radius <= 0.0f)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
const float RadiusSquared = FMath::Square(Radius);
|
||||
int32 Count = 0;
|
||||
for (int32 Index = 0; Index < Component->GetInstanceCount(); ++Index)
|
||||
{
|
||||
FTransform InstanceTransform;
|
||||
if (Component->GetInstanceTransform(Index, InstanceTransform, true)
|
||||
&& FVector::DistSquared2D(InstanceTransform.GetLocation(), WorldLocation) <= RadiusSquared)
|
||||
{
|
||||
++Count;
|
||||
}
|
||||
}
|
||||
|
||||
return Count;
|
||||
}
|
||||
|
||||
@@ -49,4 +49,21 @@ public:
|
||||
|
||||
UFUNCTION(BlueprintPure, Category = "Agrarian|Foliage")
|
||||
int32 GetGrassInstanceCount() const;
|
||||
|
||||
UFUNCTION(BlueprintCallable, Category = "Agrarian|Foliage|Fire")
|
||||
void GetFuelCountsNearLocation(
|
||||
const FVector& WorldLocation,
|
||||
float Radius,
|
||||
int32& OutGrassCount,
|
||||
int32& OutShrubCount,
|
||||
int32& OutTreeCount) const;
|
||||
|
||||
UFUNCTION(BlueprintPure, Category = "Agrarian|Foliage|Fire")
|
||||
float GetDryVegetationFuelScoreNearLocation(const FVector& WorldLocation, float Radius) const;
|
||||
|
||||
protected:
|
||||
int32 CountInstancesNearLocation(
|
||||
const UHierarchicalInstancedStaticMeshComponent* Component,
|
||||
const FVector& WorldLocation,
|
||||
float Radius) const;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user