#include "Systems/DescentBuild.h" #include "Audio/DescentAudio.h" #include "Core/DescentBoot.h" #include "Core/DescentMath.h" #include "Core/DescentSimSubsystem.h" #include "Core/Tuning/DescentTune_Cargo.h" #include "Core/Tuning/DescentTune_Carrier.h" #include "Core/Tuning/DescentTune_Tender.h" #include "Debug/DescentStateRegistry.h" #include "Player/DescentCharacter.h" #include "Systems/DescentAutomation.h" #include "Systems/DescentBio.h" #include "Systems/DescentCargo.h" #include "Systems/DescentCraft.h" #include "UI/DescentHudSubsystem.h" #include "UI/DescentUiSubsystem.h" #include "Vehicles/CarrierBlockout.h" #include "Vehicles/CarrierLayout.h" #include "Vehicles/DescentCarrier.h" #include "DescentUnits.h" #include "TheDescent.h" #include "Components/PointLightComponent.h" #include "Components/StaticMeshComponent.h" #include "Engine/StaticMesh.h" #include "Engine/World.h" #include "Materials/MaterialInstanceDynamic.h" namespace BTune = Descent::Tune::Carrier; namespace { UStaticMesh* BuildCube() { return LoadObject(nullptr, TEXT("/Engine/BasicShapes/Cube.Cube")); } UMaterialInstanceDynamic* BuildSurface(UObject* Outer, const FLinearColor& Base, const FLinearColor& Emissive, float Strength) { UMaterialInterface* Parent = LoadObject(nullptr, TEXT("/Game/Materials/M_DescentSurface.M_DescentSurface"), nullptr, LOAD_NoWarn); if (!Parent) { return nullptr; } UMaterialInstanceDynamic* MID = UMaterialInstanceDynamic::Create(Parent, Outer); MID->SetVectorParameterValue(TEXT("BaseColor"), Base); MID->SetVectorParameterValue(TEXT("EmissiveColor"), Emissive); MID->SetScalarParameterValue(TEXT("EmissiveStrength"), Strength); MID->SetScalarParameterValue(TEXT("Roughness"), 0.6f); return MID; } FLinearColor KitColour(FName Kit) { if (Kit == TEXT("fabricator")) { return FLinearColor(0.10f, 0.13f, 0.16f); } if (Kit == TEXT("locker")) { return FLinearColor(0.16f, 0.18f, 0.14f); } if (Kit == TEXT("light")) { return FLinearColor(0.5f, 0.5f, 0.45f); } if (Kit == TEXT("couch") || Kit == TEXT("bunk") || Kit == TEXT("chair")) { return FLinearColor(0.20f, 0.10f, 0.07f); } if (Kit == TEXT("table") || Kit == TEXT("shelf")) { return FLinearColor(0.22f, 0.15f, 0.09f); } return FLinearColor(0.12f, 0.12f, 0.13f); } /** Section 6.10 rule 3: the keep-out boxes that do not depend on the object. */ const TArray& KeepOutBoxes() { static TArray Boxes; if (Boxes.Num() > 0) { return Boxes; } const Descent::Carrier::FLayout& L = Descent::Carrier::FLayout::Get(); for (const Descent::Carrier::FDoorDef& D : L.Doors) { const FVector2D Mid = D.Mid(); const double Half = 0.5 * D.Width() + 0.2, Z = D.FloorZ(); if (D.WallAtConstX()) { Boxes.Add(FBox(FVector(Mid.X - 1.2, Mid.Y - Half, Z - 0.1), FVector(Mid.X + 1.2, Mid.Y + Half, Z + 2.4))); } else { Boxes.Add(FBox(FVector(Mid.X - Half, Mid.Y - 1.2, Z - 0.1), FVector(Mid.X + Half, Mid.Y + 1.2, Z + 2.4))); } } // stair ramp and landings: the whole stairwell if (const Descent::Carrier::FRoomDef* S = L.FindRoom(TEXT("stair"))) { Boxes.Add(FBox(FVector(S->X0, S->Y0, S->FloorZ - 0.5), FVector(S->X1, S->Y1, S->CeilZ))); } // the pool rectangle grown by 0.6 m Boxes.Add(FBox(FVector(BTune::PoolX0 - 0.6, BTune::PoolY0 - 0.6, BTune::DeckFloorZ1 - 1.0), FVector(BTune::PoolX1 + 0.6, BTune::PoolY1 + 0.6, BTune::DeckFloorZ1 + 6.0))); // the helm Boxes.Add(FBox(FVector(43.0, -1.8, BTune::DeckFloorZ3 - 0.1), FVector(46.6, 1.8, BTune::DeckFloorZ3 + 2.4))); // 1.2 m in front of the packer outfeed (the outfeed faces -x) namespace CT = Descent::Tune::Cargo; Boxes.Add(FBox(FVector(CT::OutfeedX - 1.5, CT::OutfeedY0 - 0.4, CT::CargoFloorZ - 0.1), FVector(CT::OutfeedX + 0.4, CT::OutfeedY0 + CT::OutfeedDY * (CT::OutfeedSlots - 1) + 0.4, CT::CargoFloorZ + 2.0))); // 1.0 m around each nutrient port, and the bodies of the other fixtures for (const Descent::Carrier::FAnchorDef& A : L.Anchors) { const double R = A.Kind == TEXT("Port") ? 1.0 : 0.5; Boxes.Add(FBox(A.PosM - FVector(R, R, 1.5), A.PosM + FVector(R, R, 1.0))); } return Boxes; } } // ---- the kit table and the rules (pure) ------------------------------------------------------------------------------ namespace Descent::Build { const TArray& Kits() { static const TArray Table = { { TEXT("fabricator"), TEXT("Fabricator"), FVector(2.2, 1.6, 1.8), Floor }, { TEXT("refinery"), TEXT("Refinery"), FVector(3.2, 3.2, 3.2), Floor }, { TEXT("locker"), TEXT("Storage Locker"), FVector(0.8, 1.2, 2.3), Floor }, { TEXT("rack"), TEXT("Box Rack"), FVector(1.4, 0.5, 1.0), Floor }, { TEXT("arm1"), TEXT("Handling Arm Mk I"), FVector(0.6, 0.6, 1.2), Floor | Ceiling }, { TEXT("arm2"), TEXT("Handling Arm Mk II"), FVector(0.6, 0.6, 1.2), Floor | Ceiling }, { TEXT("light"), TEXT("Work Light"), FVector(0.3, 0.3, 0.15), Wall | Ceiling }, { TEXT("table"), TEXT("Table"), FVector(1.4, 0.9, 0.78), Floor }, { TEXT("chair"), TEXT("Chair"), FVector(0.5, 0.5, 0.9), Floor }, { TEXT("shelf"), TEXT("Shelf"), FVector(1.2, 0.4, 2.0), Floor }, { TEXT("couch"), TEXT("Couch"), FVector(3.4, 1.0, 0.8), Floor }, { TEXT("bunk"), TEXT("Bunk"), FVector(2.1, 1.0, 0.6), Floor }, }; return Table; } const FKitDef* FindKit(FName Id) { FString S = Id.ToString().ToLower(); S.RemoveFromStart(TEXT("kit_")); const FName Key(*S); for (const FKitDef& K : Kits()) { if (K.Id == Key) { return &K; } } return nullptr; } FString MountText(uint8 Mount) { TArray Parts; if (Mount & Floor) { Parts.Add(TEXT("floor")); } if (Mount & Wall) { Parts.Add(TEXT("wall")); } if (Mount & Ceiling) { Parts.Add(TEXT("ceiling")); } return FString::Join(Parts, TEXT(" or ")); } uint8 SurfaceOf(const FVector& Normal) { const double Up = Normal.GetSafeNormal().Z; return Up > 0.9 ? Floor : (Up < -0.9 ? Ceiling : (FMath::Abs(Up) < 0.2 ? Wall : 0)); } FPlacement MakePlacementAt(const FKitDef& Kit, const FVector& PosM, double YawDeg, uint8 Surface) { FPlacement P; P.PosM = PosM; P.YawDeg = YawDeg; P.Surface = Surface; if (Surface == Wall) { P.BodySizeM = FVector(Kit.SizeM.Z, Kit.SizeM.X, Kit.SizeM.Y); P.BodyOffsetM = FVector(0.5 * Kit.SizeM.Z, 0, 0); } else { P.BodySizeM = Kit.SizeM; P.BodyOffsetM = FVector(0, 0, (Surface == Ceiling ? -0.5 : 0.5) * Kit.SizeM.Z); } const double C = FMath::Abs(FMath::Cos(FMath::DegreesToRadians(YawDeg))), S = FMath::Abs(FMath::Sin(FMath::DegreesToRadians(YawDeg))); P.HalfM = FVector(0.5 * (C * P.BodySizeM.X + S * P.BodySizeM.Y), 0.5 * (S * P.BodySizeM.X + C * P.BodySizeM.Y), 0.5 * P.BodySizeM.Z); P.CentreM = PosM + FRotator(0.0, YawDeg, 0.0).RotateVector(P.BodyOffsetM); return P; } FPlacement MakePlacement(const FKitDef& Kit, const FVector& HitM, const FVector& Normal, double YawDeg, bool bSnap) { uint8 S = SurfaceOf(Normal); if (S == 0) { S = Floor; } FVector P = HitM; auto Snap = [bSnap](double V) { return bSnap ? FMath::RoundToDouble(V * 4.0) / 4.0 : V; }; if (S == Wall) { // the back goes against the wall: the front (+x) is the wall's normal YawDeg = FMath::RadiansToDegrees(FMath::Atan2(Normal.Y, Normal.X)); if (FMath::Abs(Normal.X) > FMath::Abs(Normal.Y)) { P.Y = Snap(P.Y); } else { P.X = Snap(P.X); } P.Z = Snap(P.Z); } else { P.X = Snap(P.X); P.Y = Snap(P.Y); } return MakePlacementAt(Kit, P, YawDeg, S); } bool RuleMount(const FKitDef& Kit, const FVector& Normal) { return (SurfaceOf(Normal) & Kit.Mount) != 0; } bool RuleInRoom(const FPlacement& P, FName* OutRoom) { const Descent::Carrier::FLayout& L = Descent::Carrier::FLayout::Get(); const FBox B = P.Bounds(); const Descent::Carrier::FRoomDef* Room = nullptr; for (int32 I = 0; I < 8; ++I) { const FVector Corner((I & 1) ? B.Max.X : B.Min.X, (I & 2) ? B.Max.Y : B.Min.Y, (I & 4) ? B.Max.Z : B.Min.Z); const Descent::Carrier::FRoomDef* R = L.RoomAt(Corner); if (!R || (Room && R != Room) || !Descent::Carrier::IsDryLocal(Corner)) { return false; } Room = R; } if (OutRoom && Room) { *OutRoom = Room->Id; } return Room != nullptr; } bool RuleKeepOut(const FPlacement& P) { const FBox B = P.Bounds(); for (const FBox& K : KeepOutBoxes()) { if (K.Intersect(B)) { return false; } } // in the bay nothing may reach above carrier z -3.5 within 9.0 m of the hub axis (the Tenders sweep there) namespace CT = Descent::Tune::Tender; if (B.Max.Z > -3.5 && B.Min.X < -18.0) { const double Dx = FMath::Max3(B.Min.X - CT::HubX, 0.0, CT::HubX - B.Max.X), Dy = FMath::Max3(B.Min.Y - CT::HubY, 0.0, CT::HubY - B.Max.Y); if (Dx * Dx + Dy * Dy < 81.0) { return false; } } return true; } bool RuleNoOverlap(const FPlacement& P, TConstArrayView Blockers) { const FBox B = P.Bounds(); for (const FBox& K : Blockers) { if (K.Intersect(B)) { return false; } } return true; } bool RuleFloorCorners(const FPlacement& P) { if (P.Surface != Floor) { return true; } const Descent::Carrier::FLayout& L = Descent::Carrier::FLayout::Get(); const FBox B = P.Bounds(); for (int32 I = 0; I < 4; ++I) { const FVector Corner((I & 1) ? B.Max.X : B.Min.X, (I & 2) ? B.Max.Y : B.Min.Y, P.PosM.Z + 0.05); const Descent::Carrier::FRoomDef* R = L.RoomAt(Corner); if (!R || FMath::Abs(R->FloorZ - P.PosM.Z) > 0.05) { return false; } if (Corner.X > BTune::PoolX0 && Corner.X < BTune::PoolX1 && Corner.Y > BTune::PoolY0 && Corner.Y < BTune::PoolY1) { return false; } } return true; } bool RuleCount(int32 NumPlaced) { return NumPlaced < MaxPlaced; } bool Validate(const FKitDef& Kit, const FVector& HitM, const FVector& Normal, double YawDeg, bool bSnap, TConstArrayView Blockers, int32 NumPlaced, FPlacement& Out, FString& Reason) { Out = MakePlacement(Kit, HitM, Normal, YawDeg, bSnap); Reason.Reset(); if (!RuleMount(Kit, Normal)) { Reason = TEXT("BAD_SURFACE"); } else if (!RuleInRoom(Out)) { Reason = TEXT("OUT_OF_ROOM"); } else if (!RuleKeepOut(Out)) { Reason = TEXT("KEEP_OUT"); } else if (!RuleNoOverlap(Out, Blockers)) { Reason = TEXT("BLOCKED"); } else if (!RuleFloorCorners(Out)) { Reason = TEXT("OVERHANG"); } else if (!RuleCount(NumPlaced)) { Reason = TEXT("LIMIT_200"); } return Reason.IsEmpty(); } } // ---- the placed object ----------------------------------------------------------------------------------------------- ADescentPlaceable::ADescentPlaceable() { PrimaryActorTick.bCanEverTick = false; Root = CreateDefaultSubobject(TEXT("Root")); Root->SetMobility(EComponentMobility::Movable); SetRootComponent(Root); } UStaticMeshComponent* ADescentPlaceable::AddPart(const TCHAR* Name, const FVector& OffsetM, const FVector& SizeM, const FLinearColor& Base, const FLinearColor& Emissive, float Strength, bool bSolid, USceneComponent* Parent) { UStaticMeshComponent* M = NewObject(this, Name); M->SetMobility(EComponentMobility::Movable); M->SetupAttachment(Parent ? Parent : Root.Get()); M->SetStaticMesh(BuildCube()); M->SetRelativeLocation(Descent::MetresToUU(OffsetM)); M->SetRelativeScale3D(SizeM); if (UMaterialInstanceDynamic* MID = BuildSurface(this, Base, Emissive, Strength)) { M->SetMaterial(0, MID); } if (bSolid) { UCarrierBlockoutComponent::MakeSolid(M); } else { M->SetCollisionEnabled(ECollisionEnabled::NoCollision); } UCarrierBlockoutComponent::MakeInterior(M); M->RegisterComponent(); return M; } void ADescentPlaceable::InitKit(const Descent::Build::FPlacement& P, FName InKit, int32 InId) { KitId = InKit; PlaceId = InId; Place = P; Body = AddPart(TEXT("Body"), P.BodyOffsetM, P.BodySizeM, KitColour(KitId), FLinearColor::Black, 0.f, true); if (KitId == TEXT("fabricator")) { // the screen on the front (+x) Face = AddPart(TEXT("Screen"), P.BodyOffsetM + FVector(0.5 * P.BodySizeM.X + 0.01, 0.0, 0.25), FVector(0.02, 1.2, 0.7), FLinearColor::Black, FLinearColor(0.1f, 0.85f, 1.0f), 1.2f); } else if (KitId == TEXT("light")) { Face = AddPart(TEXT("Lens"), P.BodyOffsetM * 2.0, FVector(0.02, 0.2, 0.2), FLinearColor::Black, FLinearColor(1.0f, 0.93f, 0.8f), 3.0f); Lamp = NewObject(this, TEXT("Lamp")); Lamp->SetMobility(EComponentMobility::Movable); Lamp->SetupAttachment(Root); Lamp->SetRelativeLocation(Descent::MetresToUU(P.BodyOffsetM * 3.0)); Lamp->SetIntensityUnits(ELightUnits::Candelas); Lamp->SetIntensity(60.f); Lamp->SetAttenuationRadius(800.f); Lamp->SetLightColor(FLinearColor(1.0f, 0.93f, 0.8f)); Lamp->SetCastShadows(false); Lamp->SetLightingChannels(false, true, false); Lamp->RegisterComponent(); } else if (KitId == TEXT("locker")) { if (UDescentCargoSubsystem* Cargo = UDescentCargoSubsystem::Get(GetWorld())) { Cargo->StorageCap += 40; } } if (UDescentInteractionRegistry* Reg = UDescentInteractionRegistry::Get(GetWorld())) { Reg->Register(this); } } void ADescentPlaceable::EndPlay(const EEndPlayReason::Type Reason) { if (UDescentInteractionRegistry* Reg = UDescentInteractionRegistry::Get(GetWorld())) { Reg->Unregister(this); } if (UDescentBuildSubsystem* Build = UDescentBuildSubsystem::Get(GetWorld())) { Build->Forget(this); } Super::EndPlay(Reason); } bool ADescentPlaceable::CanPickUp(FString* OutWhyNot) const { if (KitId == TEXT("locker")) { const UDescentCargoSubsystem* Cargo = UDescentCargoSubsystem::Get(GetWorld()); if (Cargo && Cargo->StorageUsed() > Cargo->StorageCap - 40) { if (OutWhyNot) { *OutWhyNot = TEXT("The storage would be over capacity"); } return false; } } if (KitId == TEXT("fabricator")) { const UDescentCraftSubsystem* Craft = UDescentCraftSubsystem::Get(GetWorld()); if (Craft && Craft->IsBuilding()) { if (OutWhyNot) { *OutWhyNot = TEXT("The fabricator is building"); } return false; } } return true; } void ADescentPlaceable::OnPickedUp() { if (KitId == TEXT("locker")) { if (UDescentCargoSubsystem* Cargo = UDescentCargoSubsystem::Get(GetWorld())) { Cargo->StorageCap -= 40; } } } FVector ADescentPlaceable::LocalM(const FVector& ActorLocalM) const { return Place.PosM + FRotator(0.0, Place.YawDeg, 0.0).RotateVector(ActorLocalM); } FVector ADescentPlaceable::WorldUUOf(const FVector& ActorLocalM) const { return GetActorTransform().TransformPositionNoScale(Descent::MetresToUU(ActorLocalM)); } FName ADescentPlaceable::RoomId() const { const Descent::Carrier::FRoomDef* R = Descent::Carrier::FLayout::Get().RoomAt(Place.CentreM); return R ? R->Id : NAME_None; } FString ADescentPlaceable::GetPrompt() const { const Descent::Build::FKitDef* K = Descent::Build::FindKit(KitId); const FString Name = K ? K->Name : KitId.ToString(); if (KitId == TEXT("fabricator")) { return TEXT("Fabricator - build (hold E: pick up)"); } if (KitId == TEXT("bunk")) { return TEXT("Bunk - sleep (hold E: pick up)"); } return FString::Printf(TEXT("%s - hold E to pick up"), *Name); } FVector ADescentPlaceable::GetFocusPointUU() const { return WorldUUOf(Place.BodyOffsetM + (KitId == TEXT("fabricator") ? FVector(0.5 * Place.BodySizeM.X, 0.0, 0.25) : FVector::ZeroVector)); } void ADescentPlaceable::Use(ADescentCharacter* User) { if (KitId == TEXT("fabricator")) { if (UDescentCraftSubsystem* Craft = UDescentCraftSubsystem::Get(GetWorld())) { Craft->OpenFabricator(); } } else if (KitId == TEXT("bunk")) { if (UDescentBioSubsystem* Bio = UDescentBioSubsystem::Get(GetWorld())) { Bio->Sleep(User); } } } // ---- the subsystem --------------------------------------------------------------------------------------------------- UDescentBuildSubsystem* UDescentBuildSubsystem::Get(const UWorld* World) { return World ? World->GetSubsystem() : nullptr; } void UDescentBuildSubsystem::OnWorldBeginPlay(UWorld& InWorld) { Super::OnWorldBeginPlay(InWorld); if (UDescentSimSubsystem* Sim = UDescentSimSubsystem::Get(&InWorld)) { FDescentSimRegisterOptions Opt; Opt.Owner = TEXT("Build"); Opt.SubOrder = 20; StepHandle = Sim->RegisterLateStep(EDescentLateStep::InteractionFocus, [this](const FDescentSimFrame& F) { Step(F.Dt); }, Opt); } } void UDescentBuildSubsystem::Deinitialize() { if (UDescentSimSubsystem* Sim = UDescentSimSubsystem::Get(GetWorld())) { Sim->Unregister(StepHandle); } Super::Deinitialize(); } ADescentCarrier* UDescentBuildSubsystem::Carrier() const { return UDescentCarrierSubsystem::GetCarrier(GetWorld()); } int32 UDescentBuildSubsystem::KitCount(FName Kit) const { const Descent::Build::FKitDef* K = Descent::Build::FindKit(Kit); const int32* N = K ? KitCounts.Find(K->Id) : nullptr; return N ? *N : 0; } void UDescentBuildSubsystem::AddKit(FName Kit, int32 N) { if (const Descent::Build::FKitDef* K = Descent::Build::FindKit(Kit)) { int32& C = KitCounts.FindOrAdd(K->Id); C = FMath::Max(0, C + N); } } ADescentPlaceable* UDescentBuildSubsystem::FindPlaced(int32 Id) const { for (ADescentPlaceable* P : PlacedList) { if (P && P->PlaceId == Id) { return P; } } return nullptr; } ADescentPlaceable* UDescentBuildSubsystem::FindFirst(FName Kit) const { const Descent::Build::FKitDef* K = Descent::Build::FindKit(Kit); for (ADescentPlaceable* P : PlacedList) { if (P && K && P->KitId == K->Id) { return P; } } return nullptr; } ADescentPlaceable* UDescentBuildSubsystem::Spawn(FName Kit, const FVector& PosM, double YawDeg, uint8 Surface) { ADescentCarrier* C = Carrier(); const Descent::Build::FKitDef* K = Descent::Build::FindKit(Kit); if (!C || !K) { return nullptr; } const Descent::Build::FPlacement P = Descent::Build::MakePlacementAt(*K, PosM, YawDeg, Surface); UClass* Class = ADescentPlaceable::StaticClass(); if (K->Id == TEXT("refinery")) { Class = ADescentRefinery::StaticClass(); } else if (K->Id == TEXT("rack")) { Class = ADescentBoxRack::StaticClass(); } else if (K->Id == TEXT("arm1") || K->Id == TEXT("arm2")) { Class = ADescentRobotArm::StaticClass(); } const FQuat Rot = C->GetActorTransform().TransformRotation(FRotator(0.0, YawDeg, 0.0).Quaternion()); ADescentPlaceable* A = GetWorld()->SpawnActor(Class, C->ToWorldUU(PosM), Rot.Rotator()); if (!A) { return nullptr; } A->AttachToActor(C, FAttachmentTransformRules::KeepWorldTransform); A->InitKit(P, K->Id, NextId++); PlacedList.Add(A); return A; } bool UDescentBuildSubsystem::PickUp(ADescentPlaceable* P, FString* OutWhyNot) { if (!P) { if (OutWhyNot) { *OutWhyNot = TEXT("nothing to pick up"); } return false; } if (!P->CanPickUp(OutWhyNot)) { return false; } P->OnPickedUp(); AddKit(P->KitId, 1); ++PickedUp; PlacedList.Remove(P); P->Destroy(); return true; } void UDescentBuildSubsystem::DestroyAll() { TArray> Copy = PlacedList; for (ADescentPlaceable* P : Copy) { if (P) { P->OnPickedUp(); P->Destroy(); } } PlacedList.Reset(); } bool UDescentBuildSubsystem::BeginGhost(FName Kit, FString* OutWhyNot) { const Descent::Build::FKitDef* K = Descent::Build::FindKit(Kit); if (!K) { if (OutWhyNot) { *OutWhyNot = TEXT("unknown kit"); } return false; } if (KitCount(K->Id) <= 0) { if (OutWhyNot) { *OutWhyNot = FString::Printf(TEXT("No %s kit in storage"), *K->Name); } return false; } GhostKit = K->Id; GhostYawDeg = 0.0; bDebugAim = false; UpdateGhost(); return true; } void UDescentBuildSubsystem::CancelGhost() { GhostKit = NAME_None; bGhostValid = false; bDebugAim = false; GhostReason.Reset(); if (GhostActor) { GhostActor->SetActorHiddenInGame(true); } if (UDescentHudSubsystem* Hud = UDescentHudSubsystem::Get(GetWorld())) { Hud->Model().BuildReason.Reset(); } } bool UDescentBuildSubsystem::ConfirmGhost(FString* OutWhyNot) { if (!IsGhosting()) { if (OutWhyNot) { *OutWhyNot = TEXT("no ghost"); } return false; } UpdateGhost(); UDescentAudioSubsystem* Audio = UDescentAudioSubsystem::Get(GetWorld()); if (Audio) { Audio->OneShot(bGhostValid ? TEXT("place_ok") : TEXT("place_bad")); } if (!bGhostValid) { if (OutWhyNot) { *OutWhyNot = GhostReason; } return false; } const FName Kit = GhostKit; const Descent::Build::FPlacement P = GhostPlace; CancelGhost(); if (!Spawn(Kit, P.PosM, P.YawDeg, P.Surface)) { if (OutWhyNot) { *OutWhyNot = TEXT("could not place"); } return false; } AddKit(Kit, -1); ++PlacedByPlayer; return true; } void UDescentBuildSubsystem::Step(double Dt) { if (IsGhosting()) { UpdateGhost(); } } void UDescentBuildSubsystem::UpdateGhost() { ADescentCarrier* C = Carrier(); UWorld* World = GetWorld(); const Descent::Build::FKitDef* K = Descent::Build::FindKit(GhostKit); if (!C || !World || !K) { return; } const APlayerController* PC = World->GetFirstPlayerController(); ADescentCharacter* Ch = PC ? Cast(PC->GetPawn()) : nullptr; FVector HitM = DebugAimM, Normal = DebugNormal; bGhostHit = bDebugAim; bool bOnCarrier = true; if (!bDebugAim && Ch) { // trace from the eye, 6 m const FVector Eye = Ch->EyeWorldUU(), Dir = Ch->GetControlRotation().Vector(); FCollisionQueryParams Params(TEXT("BuildGhost"), false, Ch); FHitResult Hit; if (World->LineTraceSingleByChannel(Hit, Eye, Eye + Dir * 600.0, ECC_Visibility, Params)) { bGhostHit = true; HitM = C->ToLocalM(Hit.ImpactPoint); Normal = C->GetActorTransform().InverseTransformVectorNoScale(Hit.ImpactNormal); bOnCarrier = Hit.GetActor() == C; } else { HitM = C->ToLocalM(Eye + Dir * 300.0); } } // ore boxes on the deck block (they have no collision of their own) TArray Blockers; if (const UDescentCargoSubsystem* Cargo = UDescentCargoSubsystem::Get(World)) { for (const ADescentOreBox* B : Cargo->Boxes()) { if (B && B->Where == TEXT("floor")) { const FVector M = C->ToLocalM(B->GetActorLocation()); Blockers.Add(FBox(M - FVector(0.3, 0.3, 0.2), M + FVector(0.3, 0.3, 0.2))); } } } FString Reason; bool bValid = Descent::Build::Validate(*K, HitM, Normal, GhostYawDeg, !bFreePlacement, Blockers, PlacedList.Num(), GhostPlace, Reason); if (!bGhostHit) { bValid = false; Reason = TEXT("OUT_OF_REACH"); } else if (bValid && !bOnCarrier) { bValid = false; Reason = TEXT("BAD_SURFACE"); } const FQuat Rot = C->GetActorTransform().TransformRotation(FRotator(0.0, GhostPlace.YawDeg, 0.0).Quaternion()); const FVector CentreUU = C->ToWorldUU(GhostPlace.CentreM); if (bValid) { // rule 4 against the real walls, props, placed objects and the player const FVector Half = Descent::MetresToUU(0.5 * GhostPlace.BodySizeM - FVector(0.03)); if (World->OverlapBlockingTestByChannel(CentreUU, Rot, ECC_Pawn, FCollisionShape::MakeBox(Half.ComponentMax(FVector(1.0))))) { bValid = false; Reason = TEXT("BLOCKED"); } } bGhostValid = bValid; GhostReason = Reason; if (UDescentHudSubsystem* Hud = UDescentHudSubsystem::Get(World)) { Hud->Model().BuildReason = bValid ? FString() : Reason; } // the hologram: green #72e3a5 when valid, red #ff6b57 when not if (!GhostActor) { GhostActor = World->SpawnActor(); GhostMesh = NewObject(GhostActor, TEXT("Ghost")); GhostMesh->SetMobility(EComponentMobility::Movable); GhostActor->SetRootComponent(GhostMesh); GhostMesh->SetStaticMesh(BuildCube()); GhostMesh->SetCollisionEnabled(ECollisionEnabled::NoCollision); GhostMesh->SetCastShadow(false); GhostMID = BuildSurface(GhostActor, FLinearColor::Black, FLinearColor::Green, 1.5f); if (GhostMID) { GhostMesh->SetMaterial(0, GhostMID); } UCarrierBlockoutComponent::MakeInterior(GhostMesh); GhostMesh->RegisterComponent(); } GhostActor->SetActorHiddenInGame(false); GhostActor->SetActorLocationAndRotation(CentreUU, Rot); GhostActor->SetActorScale3D(GhostPlace.BodySizeM); if (GhostMID) { GhostMID->SetVectorParameterValue(TEXT("EmissiveColor"), bValid ? FLinearColor(0.447f, 0.89f, 0.647f) : FLinearColor(1.0f, 0.42f, 0.34f)); } } void UDescentBuildSubsystem::OpenMenu() { UDescentUiSubsystem* Ui = UDescentUiSubsystem::Get(GetWorld()); if (!Ui) { return; } FDescentBuildMenuViewModel& M = Ui->Models().Build; M.Kits.Reset(); M.Placed = PlacedList.Num(); M.PlacedMax = Descent::Build::MaxPlaced; TWeakObjectPtr Weak(this); for (const Descent::Build::FKitDef& K : Descent::Build::Kits()) { FDescentBuildKitRow Row; Row.KitId = K.Id; Row.Name = K.Name; Row.SizeText = FString::Printf(TEXT("%.1f x %.1f x %.1f m"), K.SizeM.X, K.SizeM.Y, K.SizeM.Z); Row.MountText = Descent::Build::MountText(K.Mount); Row.InStorage = KitCount(K.Id); const FName Id = K.Id; Row.OnPick = [Weak, Id]() { UDescentBuildSubsystem* S = Weak.Get(); if (!S) { return; } if (UDescentUiSubsystem* U = UDescentUiSubsystem::Get(S->GetWorld())) { U->Close(); } FString Why; if (!S->BeginGhost(Id, &Why)) { if (UDescentHudSubsystem* Hud = UDescentHudSubsystem::Get(S->GetWorld())) { Hud->Model().PushToast(Why, EDescentHudToastKind::Warn); } } }; M.Kits.Add(MoveTemp(Row)); } Ui->NotifyChanged(); Ui->Open(EDescentWindow::Build); } // ---- boot: the start fabricator (the start refinery is spawned by the cargo subsystem) --------------------------------- DESCENT_BOOT_STEP(SpawnStartFabricator, Descent::Boot::Order::SpawnStartPlaceables + 1) { UDescentBuildSubsystem* Build = UDescentBuildSubsystem::Get(Ctx.World); if (!Build || !Build->Spawn(TEXT("fabricator"), FVector(-1.0, -7.75, BTune::DeckFloorZ3), 90.0)) { Ctx.Fail(TEXT("could not spawn the start fabricator")); } } // ---- state section `build` (section 9.4) ----------------------------------------------------------------------------- static FDescentStateProviderRegistrar GBuildStateSection(TEXT("build"), [](UWorld* World, FJsonObject& Out) { const UDescentBuildSubsystem* S = UDescentBuildSubsystem::Get(World); Out.SetBoolField(TEXT("available"), S != nullptr); if (!S) { return; } Out.SetNumberField(TEXT("placed"), S->Placed().Num()); Out.SetNumberField(TEXT("placed_by_player"), S->PlacedByPlayer); Out.SetNumberField(TEXT("picked_up"), S->PickedUp); TSharedRef Kits = MakeShared(); for (const Descent::Build::FKitDef& K : Descent::Build::Kits()) { Kits->SetNumberField(K.Id.ToString(), S->KitCount(K.Id)); } Out.SetObjectField(TEXT("kits"), Kits); TSharedRef G = MakeShared(); G->SetStringField(TEXT("kit"), S->GhostKit.IsNone() ? FString() : S->GhostKit.ToString()); G->SetBoolField(TEXT("valid"), S->bGhostValid); G->SetStringField(TEXT("reason"), S->GhostReason); G->SetField(TEXT("pos_m"), Descent::Json::Vec3(S->GhostPlace.PosM)); G->SetNumberField(TEXT("yaw_deg"), Descent::Json::Round(S->GhostPlace.YawDeg)); Out.SetObjectField(TEXT("ghost"), G); TArray> Items; for (const ADescentPlaceable* P : S->Placed()) { if (!P) { continue; } TSharedRef J = MakeShared(); J->SetNumberField(TEXT("id"), P->PlaceId); J->SetStringField(TEXT("kit"), P->KitId.ToString()); J->SetField(TEXT("pos_m"), Descent::Json::Vec3(P->Place.PosM)); J->SetNumberField(TEXT("yaw_deg"), Descent::Json::Round(P->Place.YawDeg)); J->SetStringField(TEXT("room"), P->RoomId().ToString()); Items.Add(MakeShared(J)); } Out.SetArrayField(TEXT("items"), Items); });