Block 3 — Raid Board & Dispatch (2 days)
Goal: Player walks to the Raid Board, enters the overhead city map, hovers buildings to see target info, selects a target, assigns raccoons, and dispatches. Trash is deducted, crew is flagged Raiding, and an FTNTActiveRaid lands in UTNTRaidSubsystem::ActiveRaids. Raid does not resolve yet — travel, looting, encounters, and AI pawns are Block 4.
Depends on: Block 1 (stations, city map, data model, camera swap), Block 2 (roster, Scout/Hauler types, UTNTRaccoonSubsystem).
Build order: bottom-up. Data model additions →
UTNTResourceSubsystemfull impl →UTNTCityMapSubsystemextensions →UTNTRaidSubsystem::DispatchRaidpartial → DataAsset content →BP_Buildinginteraction wiring → UMG widgets → escape handling. Each section only references things defined above it.Naming: all gameplay types carry the
TNTprefix (UTNT…,FTNT…,ETNT…). Standardised in Block 02; this spec follows the same convention.
Deliverables
Data model additions (C++)
UTNTRaidTargetDataAsset (declared in Block 01) — add dispatch cost field:
UPROPERTY(EditDefaultsOnly, Category = "TNT|Cost")
int32 DispatchCostTrash = 50;Loot bias, encounter pool, and difficulty are already declared on the Block 01 header. No other struct/enum additions in Block 03.
FTNTRaidMapDisplayData and GetActiveRaidDisplayData are deferred to Block 04 alongside the active-raid monitoring HUD.
UTNTResourceSubsystem — Full Implementation (C++)
Class: UTNTResourceSubsystem : UGameInstanceSubsystem.
Header: Source/TrashNTreasure/Public/Subsystems/TNTResourceSubsystem.h · Impl: Source/TrashNTreasure/Private/Subsystems/TNTResourceSubsystem.cpp.
All functions UFUNCTION(BlueprintCallable, Category = "TNT|Resource"):
| Function | Signature | Purpose |
|---|---|---|
AddResource | void (ETNTResourceType, int32) | Increment counter, broadcast OnResourceChanged |
SpendResource | bool (ETNTResourceType, int32) | Return false if insufficient. Decrement, broadcast |
CanAfford | bool (ETNTResourceType, int32) const | Pure check, no mutation |
CanAffordCost | bool (FTNTCostData) const | Multi-resource check |
SpendCost | bool (FTNTCostData) | Atomic multi-resource deduct (all-or-nothing). Single broadcast per affected resource at the end |
GetAmount | int32 (ETNTResourceType) const | Current counter value |
Internal state (Ledger : FTNTResourceLedger declared Block 01):
| Member | Type | Purpose |
|---|---|---|
OnResourceChanged | FOnResourceChanged (DYNAMIC_MULTICAST_DELEGATE_TwoParams) | BlueprintAssignable. Params: ETNTResourceType, int32 NewAmount. Broadcast on any mutation |
SpendCost implementation pattern: snapshot the ledger, apply each deduction, if any underflows restore the snapshot and return false; otherwise commit and broadcast once per affected resource type.
UTNTCityMapSubsystem — Extensions (C++)
Block 01 already shipped SetOverheadCamera, OnOverheadCameraChanged, CityCamera, CachedPlayerPawn, BuildingUnlockStates. Block 03 adds the building lookup, unlock filter, and travel-time helper:
| Function | Signature | Purpose |
|---|---|---|
GetBuildingData | UTNTBuildingDataAsset* (AActor* Building) const | Resolve BP_Building instance → its UTNTBuildingDataAsset (set per-instance in editor) |
GetUnlockedBuildings | TArray<AActor*> () | Filter CachedBuildings by BuildingUnlockStates. MVP: every entry returns true (Average Home + The Dump always unlocked) |
GetEstimatedTravelTimeSeconds | float (const TArray<FGuid>& Crew, AActor* TargetBuilding) const | UNavigationSystemV1::GetPathLength(HideoutExit, Building) / (CrewAvgSpeed * SpeedScale). Returns 0 if crew empty or path invalid. Block 04 reuses this inside DispatchRaid to populate FTNTActiveRaid::TravelTime |
Internal state additions:
| Member | Type | Purpose |
|---|---|---|
CachedBuildings | TArray<TWeakObjectPtr<AActor>> | All BP_Building instances in L_City. Lazy-resolved on first GetUnlockedBuildings / GetBuildingData call (same pattern as CityCamera from Block 01) |
CachedHideoutExit | TWeakObjectPtr<AActor> | BP_HideoutExit instance in L_City. Lazy-resolved. Used by travel-time helper |
SpeedScale | float (UPROPERTY(EditDefaultsOnly, Category = "TNT|Tuning")) | Travel speed tunable, default 1.0. Block 04’s AI pawn MaxSpeed reads the same value |
Why on the city-map subsystem (not raid): travel time needs BP_HideoutExit + building positions + NavMesh — all L_City concerns. UTNTRaidSubsystem calls into it for dispatch in Block 04, so the helper lives where the level data lives.
Lazy resolution pattern (mirrors CityCamera):
if (!CachedBuildings.Num()):
TArray<AActor*> Found
UGameplayStatics::GetAllActorsOfClass(this, BP_Building::StaticClass(), Found)
CachedBuildings = Found // wrap in TWeakObjectPtr on insert
if (!CachedHideoutExit.IsValid()):
UGameplayStatics::GetAllActorsOfClass(this, BP_HideoutExit::StaticClass(), Found)
if (Found.Num() > 0): CachedHideoutExit = Found[0]
UTNTRaidSubsystem::DispatchRaid — Partial (C++)
Block 01 declared UTNTRaidSubsystem : UGameInstanceSubsystem and ActiveRaids : TArray<FTNTActiveRaid>. Block 03 adds the dispatch entry point and forward-declares the encounter delegate so Block 04 has somewhere to broadcast from without re-touching the header.
Header additions (Source/TrashNTreasure/Public/Subsystems/TNTRaidSubsystem.h):
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnRaidDispatched, FGuid, RaidId);
DECLARE_DYNAMIC_MULTICAST_DELEGATE_TwoParams(FOnEncounterPending, FGuid, RaidId, UTNTEncounterDataAsset*, Encounter);
UFUNCTION(BlueprintCallable, Category = "TNT|Raid")
FGuid DispatchRaid(UTNTRaidTargetDataAsset* Target, const TArray<FGuid>& CrewIds);
UPROPERTY(BlueprintAssignable, Category = "TNT|Raid")
FOnRaidDispatched OnRaidDispatched;
UPROPERTY(BlueprintAssignable, Category = "TNT|Raid")
FOnEncounterPending OnEncounterPending; // declared now, broadcast in Block 04DispatchRaid steps (Block 03 scope):
- Validate
Target != nullptrandCrewIds.Num() >= 1. Bail with emptyFGuid()on failure. - Resolve every
CrewIdviaUTNTRaccoonSubsystem::GetRaccoon; require allStatus == Idle. Reject otherwise. UTNTResourceSubsystem::CanAfford(Trash, Target->DispatchCostTrash)— bail if false.SpendResource(Trash, Target->DispatchCostTrash)— must succeed (we just checked).- For each
CrewId:UTNTRaccoonSubsystem::SetStatus(Id, ETNTRaccoonStatus::Raiding). - Build
FTNTActiveRaid:RaidId = FGuid::NewGuid()Target(soft ref)CrewRaccoonIds = CrewIdsPhase = ETNTRaidPhase::Traveling(placeholder — Block 04 owns the state machine)- All other travel/loot/encounter fields default-zeroed; Block 04’s
DispatchRaidextension fills them
- Append to
ActiveRaids. OnRaidDispatched.Broadcast(RaidId).- Return
RaidId.
Block 04 extends
DispatchRaidto compute travel time (viaUTNTCityMapSubsystem::GetEstimatedTravelTimeSeconds), roll encounter chance, snapshotbIsDaytimeRaid, and spawnATNTRaccoonAIPawngroup atBP_HideoutExit. Those steps are spec’d in Block 04, not duplicated here.
Raid Target DataAssets
Author into Content/TrashNTreasure/Data/RaidTargets/:
| Asset | Loot Bias (Trash / Currency / Scrap / Food / Valuables) | Difficulty | Encounters | DispatchCostTrash | RecruitChance |
|---|---|---|---|---|---|
DA_Target_AverageHome | 0.25 / 0.10 / 0.10 / 0.30 / 0.25 | Easy | 1–2 | 50 | 0.0 |
DA_Target_TheDump | 0.50 / 0.05 / 0.20 / 0.15 / 0.10 | Easy | 0–1 | 30 | 0.20 |
Encounter pool fields reference the encounter DAs below.
Encounter DataAssets (stubs)
Author into Content/TrashNTreasure/Data/Encounters/. Stat-check fields and threshold values populated per Block 04’s encounter table; resolution logic is wired in Block 04. Block 03 only needs the assets to exist so target encounter pools can reference them.
DA_Encounter_AggressiveDog— checks Speed / Strength.DA_Encounter_TriggeredAlarm— checks Stealth / Cunning.
Building DataAssets
Author into Content/TrashNTreasure/Data/Buildings/:
DA_Building_AverageHome— display name “Average Home”,RaidTarget = DA_Target_AverageHome, visual type enum.DA_Building_TheDump— display name “The Dump”,RaidTarget = DA_Target_TheDump, visual type enum.
Assign each to its placed BP_Building instance in L_City (Block 01 placed the instances; this block fills the DA reference).
Station DataAsset for Raid Board
Author DA_Station_RaidBoard into Content/TrashNTreasure/Data/Stations/:
StationType = ETNTStationType::RaidBoardRelevantStat = ETNTStatType::CunningBaseStorageCap = 0- No upgrade tiers
UIWidgetClass = nullptr— Raid Board uses the camera swap from Block 01, not a widget
Assign to the placed BP_StationBase Raid Board instance in L_Hideout.
BP_Building — Hover + Select Wiring (Blueprint-only)
Block 01 placed two BP_Building instances with UStaticMeshComponent + UBoxComponent + a UTNTBuildingDataAsset reference. Block 03 layers on the interactive surface:
Material: swap the static mesh’s material for a Material Instance Dynamic created on BeginPlay. Add a scalar parameter HighlightBoost (default 0.0) to drive the hover glow.
Event graph:
Event ReceiveActorBeginCursorOver
→ Set Scalar Parameter (MID, "HighlightBoost", 1.0)
→ Get Game Instance → Get Subsystem (UTNTCityMapSubsystem)
→ Call GetBuildingData(self) → cache as BuildingData
→ Create Widget WBP_BuildingTooltip → set BuildingData → Add to Viewport
→ Cache the spawned widget on self for later removal
Event ReceiveActorEndCursorOver
→ Set Scalar Parameter (MID, "HighlightBoost", 0.0)
→ Remove cached tooltip widget from viewport, clear ref
Event ReceiveActorOnClicked
→ Create Widget WBP_CrewAssignment → set TargetBuilding = self + BuildingData
→ Add to Viewport
→ Get Player Controller → Set Input Mode Game and UI
→ Remove tooltip if still up
Cursor-over events fire because BP_PlayerController::SwitchToCityMapInput uses Set Input Mode Game and UI (see Block 01 amendment below).
UMG Widgets (Blueprint-only)
Match the Block 02 clipboard pattern: WBP_* only, no C++ widget class.
WBP_BuildingTooltip — Content/TrashNTreasure/UI/WBP_BuildingTooltip.uasset
Floating widget that follows the cursor. On Tick: Get Owning Player → Get Mouse Position In Viewport → Set Position In Viewport.
Variables (set by spawner):
BuildingData : UTNTBuildingDataAsset*
Bindings (text blocks):
TargetName←BuildingData->RaidTarget->DisplayNameLootBias← top-2 weighted resources joined as text (e.g."Food + Valuables")Difficulty←RaidTarget->Difficultyenum to textEncounterInfo← if any openWBP_CrewAssignmentreportsbScoutInCrew == true: list encounter names + count fromRaidTarget->EncounterPool. Else:"???"LockStatus←"Unlocked"/"Locked"viaUTNTCityMapSubsystem::GetUnlockedBuildingsmembership (MVP: always unlocked)DispatchCost←"{N} trash"fromRaidTarget->DispatchCostTrash
WBP_CrewAssignment — Content/TrashNTreasure/UI/WBP_CrewAssignment.uasset
Modal-ish overlay; city map remains visible underneath with a semi-transparent backdrop.
Layout:
- Header:
BuildingData->DisplayName, target loot bias + difficulty summary - Body — two columns:
- Left: idle roster scroll (one
WBP_CrewAssignment_RaccoonRowperUTNTRaccoonSubsystem::GetRaccoonsByStatus(Idle)entry) - Right: assigned crew list (mirrors selected raccoons, removable)
- Left: idle roster scroll (one
- Footer: trash cost, estimated travel time, Dispatch button, Close button
Variables (set by spawner):
TargetBuilding : AActor*BuildingData : UTNTBuildingDataAsset*AssignedCrewIds : TArray<FGuid>
Interaction (click-to-toggle, MVP): clicking a row in the idle list adds the raccoon to AssignedCrewIds; clicking in the assigned list removes it. Drag-and-drop is post-MVP polish — keep MVP simple.
Bindings:
On Construct:- Bind
UTNTRaccoonSubsystem::OnRosterChanged→ refresh idle list (covers status flips mid-assignment from other systems) - Bind
UTNTResourceSubsystem::OnResourceChanged→ refresh dispatch button enabled state
- Bind
- On any assignment change:
- Recompute estimated travel time via
UTNTCityMapSubsystem::GetEstimatedTravelTimeSeconds(AssignedCrewIds, TargetBuilding). Display as"~Xm"(game minutes = real seconds; format with 0 or 1 decimal) - Recompute
bScoutInCrew = AssignedCrewIds.Any(GetRaccoon(Id).Type == Scout) - Recompute affordability:
UTNTResourceSubsystem::CanAfford(Trash, BuildingData->RaidTarget->DispatchCostTrash) - Enable Dispatch only if
AssignedCrewIds.Num() >= 1 AND CanAfford
- Recompute estimated travel time via
- Dispatch click →
UTNTRaidSubsystem::DispatchRaid(BuildingData->RaidTarget, AssignedCrewIds). If returnedFGuidis valid: close widget + restore input to map-hover mode (Game and UI, cursor visible) - Close click → close widget + restore map-hover input mode
- Escape → routes through
BP_PlayerController::HandleEscape(see below); equivalent to Close
WBP_CrewAssignment_RaccoonRow — per-row sub-widget
Content/TrashNTreasure/UI/WBP_CrewAssignment_RaccoonRow.uasset. Displays:
- Name, type icon
- Primary + secondary stats from
UTNTRaccoonSubsystem::GetEffectiveStats - Per-raccoon estimated solo travel time (helpful single-contributor preview)
- Click event bubbled up to parent
WBP_CrewAssignmentfor assign/unassign
Scout Pre-Raid Intel
Single rule: tooltip’s encounter info reads bScoutInCrew from the open WBP_CrewAssignment widget. No widget open or no Scout → "???". No new subsystem state needed; the lookup is AssignedCrewIds.ContainsByPredicate(Id => GetRaccoon(Id).Type == ETNTRaccoonType::Scout).
Escape Handling
Block 01 binds IA_Escape on BP_PlayerController directly to UTNTCityMapSubsystem::SetOverheadCamera(false). Block 03 layers UI on top — escape needs to peel the UI first.
Refactor the binding into a BP_PlayerController function HandleEscape:
Function HandleEscape
Get Viewport → find any active WBP_CrewAssignment
If found:
Remove from viewport
Set Input Mode Game and UI (back to map-hover)
Return
Else:
UTNTCityMapSubsystem::SetOverheadCamera(false)
Both the IA_Escape binding and WBP_CrewAssignment’s Close button call HandleEscape so the logic stays in one place.
Block 01 Amendment (small)
BP_PlayerController::SwitchToCityMapInput (Block 01, Player Controller section) sets input mode Game Only. Change to Game and UI so cursor-over events on BP_Building route to the actor. The existing comment “city map needs cursor for click + drag” already supports this; this is a clarification, not a new requirement.
Deferred to Block 04
Spec’d here previously, moved to Block 04 because they all depend on the tick state machine, AI pawns, and encounter rolling that Block 04 introduces:
- Travel time / encounter roll /
bIsDaytimeRaidsnapshot insideDispatchRaid(Block 04 extends the entry point) - AI pawn group spawn at
BP_HideoutExit FTNTActiveRaidtick state machine and phase progression- Encounter resolution,
OnEncounterResolved,OnRaidCompleteddelegates - City map active-raid monitoring HUD:
FTNTRaidMapDisplayData,UTNTCityMapSubsystem::GetActiveRaidDisplayData, status icons, encounter alert markers WBP_BackupAssignmentwidget andUTNTRaidSubsystem::DispatchBackup
OnEncounterPending is declared on UTNTRaidSubsystem in Block 03 (so the header doesn’t need touching again) but is only broadcast in Block 04.
Done When
- Player walks to Raid Board, presses E → switches to overhead city camera (already shipped Block 01; verify still works after
BP_Buildingchanges) - Pan/zoom works on city map
- Hovering
BP_BuildingshowsWBP_BuildingTooltipwith target name, loot bias, difficulty, encounter info or"???", dispatch cost - Scout in assigned crew flips encounter info from
"???"to real list (re-hover after assignment to refresh) - Clicking a building opens
WBP_CrewAssignmentoverlay - Idle roster shows in left column; click to add to crew, click in crew column to remove
- Estimated travel time recomputes live as crew composition changes
- Dispatch button greys out when
AssignedCrewIds.Num() == 0OR!CanAfford(Trash, cost) - Dispatch click: trash deducted, all crew status flipped to
Raiding,FTNTActiveRaidappended toActiveRaids,OnRaidDispatchedbroadcast, widget closes -
OnResourceChangedfires on trash deduction - Block 02 roster UI reflects status flip live (already wired to
OnRosterChanged) - Escape closes
WBP_CrewAssignmentif open; otherwise swaps camera back to hideout - All new DataAssets author and load without errors
- Save/load round-trip preserves
ActiveRaids(Block 02’sUTNTSaveGame.ActiveRaidsalready declared; Block 03 confirms it populates after dispatch)
References
Raid Board · Average Home · The Dump · Raids