fp: Make the building behaviours generic and work across multiple buildings

This commit is contained in:
doyle 2023-10-06 20:48:20 +11:00
parent bfe2fef528
commit 57a09d6f9a
3 changed files with 156 additions and 65 deletions

View File

@ -1001,13 +1001,12 @@ void FP_EntityActionStateMachine(FP_Game *game, TELY_Audio *audio, TELY_Platform
} }
if (action_has_finished) { if (action_has_finished) {
if (!FP_Game_IsNilEntityHandle(game, entity->club_terry_patron)) { if (!FP_Game_IsNilEntityHandle(game, entity->building_patron)) {
FP_GameEntity *patron = FP_Game_GetEntity(game, entity->club_terry_patron); FP_GameEntity *patron = FP_Game_GetEntity(game, entity->building_patron);
patron->flags |= FP_GameEntityFlag_ExperiencedClubTerry; patron->flags &= ~FP_GameEntityFlag_OccupiedInBuilding;
patron->flags &= ~FP_GameEntityFlag_PartyingAtClubTerry;
patron->base_acceleration_per_s.meters *= .5f; patron->base_acceleration_per_s.meters *= .5f;
entity->club_terry_patron = {};
} }
entity->building_patron = {};
FP_Game_EntityTransitionState(game, entity, FP_EntityClubTerryState_Idle); FP_Game_EntityTransitionState(game, entity, FP_EntityClubTerryState_Idle);
} }
} break; } break;
@ -1063,6 +1062,19 @@ void FP_EntityActionStateMachine(FP_Game *game, TELY_Audio *audio, TELY_Platform
} break; } break;
case FP_EntityAirportTerryState_FlyPassenger: { case FP_EntityAirportTerryState_FlyPassenger: {
if (entering_new_state) {
uint64_t duration_ms = 5000;
FP_Game_EntityActionReset(game, entity->handle, duration_ms, render_data.sprite);
}
if (action_has_finished) {
if (!FP_Game_IsNilEntityHandle(game, entity->building_patron)) {
FP_GameEntity *patron = FP_Game_GetEntity(game, entity->building_patron);
patron->flags &= ~FP_GameEntityFlag_OccupiedInBuilding;
}
entity->building_patron = {};
FP_Game_EntityTransitionState(game, entity, FP_EntityAirportTerryState_Idle);
}
} break; } break;
} }
} break; } break;
@ -1182,7 +1194,22 @@ void FP_EntityActionStateMachine(FP_Game *game, TELY_Audio *audio, TELY_Platform
FP_Game_EntityActionReset(game, entity->handle, duration_ms, render_data.sprite); FP_Game_EntityActionReset(game, entity->handle, duration_ms, render_data.sprite);
} }
} break; } break;
case FP_EntityChurchTerryState_ConvertPatron: break;
case FP_EntityChurchTerryState_ConvertPatron: {
if (entering_new_state) {
uint64_t duration_ms = 5000;
FP_Game_EntityActionReset(game, entity->handle, duration_ms, render_data.sprite);
}
if (action_has_finished) {
if (!FP_Game_IsNilEntityHandle(game, entity->building_patron)) {
FP_GameEntity *patron = FP_Game_GetEntity(game, entity->building_patron);
patron->flags &= ~FP_GameEntityFlag_OccupiedInBuilding;
}
entity->building_patron = {};
FP_Game_EntityTransitionState(game, entity, FP_EntityChurchTerryState_Idle);
}
} break;
} }
} break; } break;
@ -1286,7 +1313,7 @@ void FP_Update(TELY_Platform *platform, FP_Game *game, TELY_PlatformInput *input
FP_GameEntity *entity = it.entity; FP_GameEntity *entity = it.entity;
entity->alive_time_s += PHYSICS_STEP; entity->alive_time_s += PHYSICS_STEP;
if (entity->flags & FP_GameEntityFlag_PartyingAtClubTerry) if (entity->flags & FP_GameEntityFlag_OccupiedInBuilding)
continue; continue;
// NOTE: Move entity by keyboard and gamepad =============================================== // NOTE: Move entity by keyboard and gamepad ===============================================
@ -1344,7 +1371,6 @@ void FP_Update(TELY_Platform *platform, FP_Game *game, TELY_PlatformInput *input
Dqn_V2 entity_pos = FP_Game_CalcEntityWorldPos(game, entity->handle); Dqn_V2 entity_pos = FP_Game_CalcEntityWorldPos(game, entity->handle);
if (entity->flags & FP_GameEntityFlag_AggrosWhenNearTerry) { if (entity->flags & FP_GameEntityFlag_AggrosWhenNearTerry) {
FP_GameFindClosestEntityResult closest_result = FP_Game_FindClosestEntityWithType(game, entity->handle, FP_EntityType_Terry); FP_GameFindClosestEntityResult closest_result = FP_Game_FindClosestEntityWithType(game, entity->handle, FP_EntityType_Terry);
Dqn_f32 aggro_dist_threshold = FP_Game_MetersToPixelsNx1(game, 4.f); Dqn_f32 aggro_dist_threshold = FP_Game_MetersToPixelsNx1(game, 4.f);
@ -1391,31 +1417,62 @@ void FP_Update(TELY_Platform *platform, FP_Game *game, TELY_PlatformInput *input
} }
} }
if (entity->flags & FP_GameEntityFlag_RespondsToClubTerry && (entity->flags & FP_GameEntityFlag_ExperiencedClubTerry) == 0) { if (entity->flags & FP_GameEntityFlag_RespondsToBuildings) {
FP_GameFindClosestEntityResult closest_club = FP_Game_FindClosestEntityWithType(game, entity->handle, FP_EntityType_ClubTerry); FP_GameFindClosestEntityResult closest_building = {};
if (closest_club.dist_squared < DQN_SQUARED(FP_Game_MetersToPixelsNx1(game, 4.f))) { closest_building.dist_squared = DQN_F32_MAX;
closest_building.pos = Dqn_V2_InitNx1(DQN_F32_MAX);
bool has_waypoint_to_club = false; for (FP_GameEntityIterator building_it = {}; FP_Game_DFSPostOrderWalkEntityTree(game, &building_it, game->root_entity); ) {
for (FP_SentinelListLink<FP_GameWaypoint> *link = nullptr; FP_GameEntity *it_entity = building_it.entity;
!has_waypoint_to_club && FP_SentinelList_Iterate<FP_GameWaypoint>(&entity->waypoints, &link); ) { if (it_entity->type != FP_EntityType_ClubTerry &&
has_waypoint_to_club = link->data.entity == closest_club.entity; it_entity->type != FP_EntityType_AirportTerry &&
it_entity->type != FP_EntityType_ChurchTerry)
continue;
bool already_visited_building = false;
for (FP_SentinelListLink<FP_GameEntityHandle> *link_it = {};
!already_visited_building && FP_SentinelList_Iterate(&entity->buildings_visited, &link_it);
) {
FP_GameEntityHandle visit_item = link_it->data;
already_visited_building = visit_item == it_entity->handle;
} }
if (!has_waypoint_to_club) { if (already_visited_building)
Dqn_Rect club_hit_box = FP_Game_CalcEntityWorldHitBox(game, closest_club.entity); continue;
Dqn_V2 club_top_left = Dqn_Rect_TopLeft(club_hit_box);
Dqn_V2 club_top_right = Dqn_Rect_TopRight(club_hit_box); Dqn_V2 pos = FP_Game_CalcEntityWorldPos(game, it_entity->handle);
Dqn_f32 dist = Dqn_V2_LengthSq_V2x2(pos, entity_pos);
if (dist < closest_building.dist_squared) {
closest_building.pos = pos;
closest_building.dist_squared = dist;
closest_building.entity = it_entity->handle;
}
}
if (!FP_Game_IsNilEntityHandle(game, closest_building.entity) &&
closest_building.dist_squared < DQN_SQUARED(FP_Game_MetersToPixelsNx1(game, 5.f))) {
bool has_waypoint_to_building = false;
for (FP_SentinelListLink<FP_GameWaypoint> *link = nullptr;
!has_waypoint_to_building && FP_SentinelList_Iterate<FP_GameWaypoint>(&entity->waypoints, &link); ) {
has_waypoint_to_building = link->data.entity == closest_building.entity;
}
if (!has_waypoint_to_building) {
Dqn_Rect hit_box = FP_Game_CalcEntityWorldHitBox(game, closest_building.entity);
Dqn_V2 top_left = Dqn_Rect_TopLeft(hit_box);
Dqn_V2 top_right = Dqn_Rect_TopRight(hit_box);
FP_SentinelListLink<FP_GameWaypoint> *link = FP_SentinelList_MakeBefore(&entity->waypoints, FP_SentinelList_Front(&entity->waypoints), game->chunk_pool); FP_SentinelListLink<FP_GameWaypoint> *link = FP_SentinelList_MakeBefore(&entity->waypoints, FP_SentinelList_Front(&entity->waypoints), game->chunk_pool);
FP_GameWaypoint *waypoint = &link->data; FP_GameWaypoint *waypoint = &link->data;
waypoint->entity = closest_club.entity; waypoint->entity = closest_building.entity;
waypoint->type = FP_GameWaypointType_Side; waypoint->type = FP_GameWaypointType_Side;
if (entity_pos.x <= club_top_left.x) { if (entity_pos.x <= top_left.x) {
waypoint->type_direction = FP_GameDirection_Left; waypoint->type_direction = FP_GameDirection_Left;
} else if (entity_pos.x >= club_top_right.x) { } else if (entity_pos.x >= top_right.x) {
waypoint->type_direction = FP_GameDirection_Right; waypoint->type_direction = FP_GameDirection_Right;
} else if (entity_pos.y <= club_top_left.y) { } else if (entity_pos.y <= top_left.y) {
waypoint->type_direction = FP_GameDirection_Up; waypoint->type_direction = FP_GameDirection_Up;
} else { } else {
waypoint->type_direction = FP_GameDirection_Down; waypoint->type_direction = FP_GameDirection_Down;
@ -1493,28 +1550,36 @@ void FP_Update(TELY_Platform *platform, FP_Game *game, TELY_PlatformInput *input
// NOTE: We have arrived at the waypoint // NOTE: We have arrived at the waypoint
bool aggro_on_terry = (entity->flags & FP_GameEntityFlag_AggrosWhenNearTerry) && waypoint_entity->type == FP_EntityType_Terry; bool aggro_on_terry = (entity->flags & FP_GameEntityFlag_AggrosWhenNearTerry) && waypoint_entity->type == FP_EntityType_Terry;
bool building_response = (entity->flags & FP_GameEntityFlag_RespondsToBuildings) &&
bool club_terry_response = (entity->flags & FP_GameEntityFlag_RespondsToClubTerry) && waypoint_entity->type == FP_EntityType_ClubTerry ||
((entity->flags & FP_GameEntityFlag_ExperiencedClubTerry) == 0) && waypoint_entity->type == FP_EntityType_AirportTerry ||
waypoint_entity->type == FP_EntityType_ClubTerry; waypoint_entity->type == FP_EntityType_ChurchTerry;
bool heart_response = (entity->flags & FP_GameEntityFlag_PointOfInterestHeart) && waypoint_entity->type == FP_EntityType_Heart; bool heart_response = (entity->flags & FP_GameEntityFlag_PointOfInterestHeart) && waypoint_entity->type == FP_EntityType_Heart;
if (((waypoint->flags & FP_GameWaypointFlag_NonInterruptible) == 0) && (aggro_on_terry || club_terry_response || heart_response)) { if (((waypoint->flags & FP_GameWaypointFlag_NonInterruptible) == 0) && (aggro_on_terry || building_response || heart_response)) {
bool can_attack = !entity->is_dying; // TODO(doyle): State transition needs to check if it's valid to move to making this not necessary bool can_attack = !entity->is_dying; // TODO(doyle): State transition needs to check if it's valid to move to making this not necessary
if (club_terry_response) { if (building_response) {
FP_GameEntity *club = waypoint_entity; FP_GameEntity *building = waypoint_entity;
if (FP_Game_IsNilEntityHandle(game, club->club_terry_patron)) { if (FP_Game_IsNilEntityHandle(game, building->building_patron)) {
club->club_terry_patron = entity->handle; building->building_patron = entity->handle;
FP_Game_EntityTransitionState(game, club, FP_EntityClubTerryState_PartyTime);
entity->flags |= FP_GameEntityFlag_PartyingAtClubTerry;
Dqn_Rect hit_box = FP_Game_CalcEntityWorldHitBox(game, club->handle); if (building->type == FP_EntityType_ClubTerry) {
FP_Game_EntityTransitionState(game, building, FP_EntityClubTerryState_PartyTime);
} else if (building->type == FP_EntityType_AirportTerry) {
FP_Game_EntityTransitionState(game, building, FP_EntityAirportTerryState_FlyPassenger);
} else {
DQN_ASSERT(building->type == FP_EntityType_ChurchTerry);
FP_Game_EntityTransitionState(game, building, FP_EntityChurchTerryState_ConvertPatron);
}
entity->flags |= FP_GameEntityFlag_OccupiedInBuilding;
Dqn_Rect hit_box = FP_Game_CalcEntityWorldHitBox(game, building->handle);
Dqn_V2 exit_pos = Dqn_Rect_InterpolatedPoint(hit_box, Dqn_V2_InitNx2(0.5f, 1.1f)); Dqn_V2 exit_pos = Dqn_Rect_InterpolatedPoint(hit_box, Dqn_V2_InitNx2(0.5f, 1.1f));
entity->local_pos = exit_pos; // TODO(doyle): Only works when parent world pos is 0,0 entity->local_pos = exit_pos; // TODO(doyle): Only works when parent world pos is 0,0
can_attack = false; can_attack = false;
FP_SentinelList_Erase(&entity->waypoints, waypoint_link, game->chunk_pool); FP_SentinelList_Erase(&entity->waypoints, waypoint_link, game->chunk_pool);
FP_SentinelList_Add(&entity->buildings_visited, game->chunk_pool, building->handle);
} }
} }
@ -1557,12 +1622,9 @@ void FP_Update(TELY_Platform *platform, FP_Game *game, TELY_PlatformInput *input
DQN_ASSERT(entity->type == FP_EntityType_Clinger); DQN_ASSERT(entity->type == FP_EntityType_Clinger);
FP_Game_EntityTransitionState(game, entity, FP_EntityClingerState_Attack); FP_Game_EntityTransitionState(game, entity, FP_EntityClingerState_Attack);
} }
entity->direction = best_attack_dir; entity->direction = best_attack_dir;
} break; } break;
case FP_EntityType_Count: DQN_INVALID_CODE_PATH; break;
case FP_EntityType_Nil: break; case FP_EntityType_Nil: break;
case FP_EntityType_ClubTerry: break; case FP_EntityType_ClubTerry: break;
case FP_EntityType_Map: break; case FP_EntityType_Map: break;
@ -1571,9 +1633,11 @@ void FP_Update(TELY_Platform *platform, FP_Game *game, TELY_PlatformInput *input
case FP_EntityType_MerchantGraveyard: break; case FP_EntityType_MerchantGraveyard: break;
case FP_EntityType_MerchantGym: break; case FP_EntityType_MerchantGym: break;
case FP_EntityType_MerchantPhoneCompany: break; case FP_EntityType_MerchantPhoneCompany: break;
case FP_EntityType_AirportTerry: case FP_EntityType_AirportTerry: break;
case FP_EntityType_ChurchTerry: case FP_EntityType_ChurchTerry: break;
case FP_EntityType_KennelTerry: break; case FP_EntityType_KennelTerry: break;
case FP_EntityType_PhoneMessageProjectile: break;
case FP_EntityType_Count: DQN_INVALID_CODE_PATH; break;
} }
} }
@ -1731,7 +1795,7 @@ void FP_Update(TELY_Platform *platform, FP_Game *game, TELY_PlatformInput *input
FP_GameEntity *mob = FP_Game_GetEntity(game, link->data); FP_GameEntity *mob = FP_Game_GetEntity(game, link->data);
mob->waypoints = FP_SentinelList_Init<FP_GameWaypoint>(game->chunk_pool); mob->waypoints = FP_SentinelList_Init<FP_GameWaypoint>(game->chunk_pool);
mob->flags |= FP_GameEntityFlag_AggrosWhenNearTerry; mob->flags |= FP_GameEntityFlag_AggrosWhenNearTerry;
mob->flags |= FP_GameEntityFlag_RespondsToClubTerry; mob->flags |= FP_GameEntityFlag_RespondsToBuildings;
for (FP_GameEntity *waypoint_entity = entity->first_child; waypoint_entity; waypoint_entity = waypoint_entity->next) { for (FP_GameEntity *waypoint_entity = entity->first_child; waypoint_entity; waypoint_entity = waypoint_entity->next) {
if ((waypoint_entity->flags & FP_GameEntityFlag_MobSpawnerWaypoint) == 0) if ((waypoint_entity->flags & FP_GameEntityFlag_MobSpawnerWaypoint) == 0)
@ -1879,8 +1943,7 @@ void FP_Render(FP_Game *game, TELY_Platform *platform, TELY_Renderer *renderer)
// NOTE: Draw entities ========================================================================= // NOTE: Draw entities =========================================================================
for (FP_GameEntityIterator it = {}; FP_Game_DFSPostOrderWalkEntityTree(game, &it, game->root_entity); ) { for (FP_GameEntityIterator it = {}; FP_Game_DFSPostOrderWalkEntityTree(game, &it, game->root_entity); ) {
FP_GameEntity *entity = it.entity; FP_GameEntity *entity = it.entity;
if (entity->flags & FP_GameEntityFlag_OccupiedInBuilding)
if (entity->flags & FP_GameEntityFlag_PartyingAtClubTerry)
continue; continue;
// NOTE: Render shapes in entity =========================================================== // NOTE: Render shapes in entity ===========================================================
@ -2009,12 +2072,17 @@ void FP_Render(FP_Game *game, TELY_Platform *platform, TELY_Renderer *renderer)
TELY_COLOUR_WHITE_V4); TELY_COLOUR_WHITE_V4);
} }
if (entity->type == FP_EntityType_ClubTerry) { if (entity->type == FP_EntityType_ClubTerry ||
FP_EntityClubTerryState *state = DQN_CAST(FP_EntityClubTerryState *)&entity->action.state; entity->type == FP_EntityType_AirportTerry ||
if (*state == FP_EntityClubTerryState_PartyTime) { entity->type == FP_EntityType_ChurchTerry) {
Dqn_f32 duration = entity->action.end_at_clock_ms - DQN_CAST(Dqn_f32)entity->action.started_at_clock_ms;
Dqn_f32 elapsed = DQN_CAST(Dqn_f32)(game->clock_ms - entity->action.started_at_clock_ms);
FP_GameEntityAction const *action = &entity->action;
bool draw_timer = entity->type == FP_EntityType_ClubTerry && action->state == FP_EntityClubTerryState_PartyTime ||
entity->type == FP_EntityType_AirportTerry && action->state == FP_EntityAirportTerryState_FlyPassenger ||
entity->type == FP_EntityType_ChurchTerry && action->state == FP_EntityChurchTerryState_ConvertPatron;
if (draw_timer) {
Dqn_f32 duration = action->end_at_clock_ms - DQN_CAST(Dqn_f32)action->started_at_clock_ms;
Dqn_f32 elapsed = DQN_CAST(Dqn_f32)(game->clock_ms - action->started_at_clock_ms);
Dqn_f32 t01 = DQN_MIN(1.f, elapsed / duration); Dqn_f32 t01 = DQN_MIN(1.f, elapsed / duration);
Dqn_Rect rect = {}; Dqn_Rect rect = {};
rect.pos = Dqn_Rect_InterpolatedPoint(world_hit_box, Dqn_V2_InitNx2(0, -0.3f)); rect.pos = Dqn_Rect_InterpolatedPoint(world_hit_box, Dqn_V2_InitNx2(0, -0.3f));

View File

@ -222,6 +222,7 @@ static FP_GameEntity *FP_Game_MakeEntityPointerFV(FP_Game *game, DQN_FMT_STRING_
result->sprite_height.meters = 1; result->sprite_height.meters = 1;
result->parent = FP_Game_ActiveParentEntityPointer(game); result->parent = FP_Game_ActiveParentEntityPointer(game);
result->name = TELY_ChunkPool_AllocFmtFV(game->chunk_pool, fmt, args); result->name = TELY_ChunkPool_AllocFmtFV(game->chunk_pool, fmt, args);
result->buildings_visited = FP_SentinelList_Init<FP_GameEntityHandle>(game->chunk_pool);
// NOTE: Attach entity as a child to the parent // NOTE: Attach entity as a child to the parent
FP_GameEntity *parent = result->parent; FP_GameEntity *parent = result->parent;
@ -317,6 +318,7 @@ static void FP_Game_DetachEntityIntoFreeList(FP_Game *game, FP_GameEntityHandle
FP_SentinelList_Deinit(&entity->spawn_list, game->chunk_pool); FP_SentinelList_Deinit(&entity->spawn_list, game->chunk_pool);
FP_SentinelList_Deinit(&entity->waypoints, game->chunk_pool); FP_SentinelList_Deinit(&entity->waypoints, game->chunk_pool);
FP_SentinelList_Deinit(&entity->buildings_visited, game->chunk_pool);
if (new_entity_generation > entity_generation) { if (new_entity_generation > entity_generation) {
// NOTE: Update the incremented handle disassociating all prior handles // NOTE: Update the incremented handle disassociating all prior handles
@ -763,6 +765,27 @@ static void FP_Game_EntityTransitionState(FP_Game *game, FP_GameEntity *entity,
entity->last_attack_timestamp = game->clock_ms; entity->last_attack_timestamp = game->clock_ms;
} }
} break; } break;
case FP_EntityType_AirportTerry: {
} break;
case FP_EntityType_ChurchTerry: {
} break;
case FP_EntityType_ClubTerry: {
} break;
case FP_EntityType_Nil:
case FP_EntityType_Catfish:
case FP_EntityType_Heart:
case FP_EntityType_KennelTerry:
case FP_EntityType_Map:
case FP_EntityType_MerchantGraveyard:
case FP_EntityType_MerchantGym:
case FP_EntityType_MerchantPhoneCompany:
case FP_EntityType_MerchantTerry:
case FP_EntityType_PhoneMessageProjectile:
case FP_EntityType_Count: break;
} }
// NOTE: If no returns are hit above we proceed with the state change // NOTE: If no returns are hit above we proceed with the state change
entity->action.next_state = desired_state; entity->action.next_state = desired_state;

View File

@ -16,10 +16,9 @@ enum FP_GameEntityFlag
FP_GameEntityFlag_MobSpawnerWaypoint = 1 << 8, FP_GameEntityFlag_MobSpawnerWaypoint = 1 << 8,
FP_GameEntityFlag_AggrosWhenNearTerry = 1 << 9, FP_GameEntityFlag_AggrosWhenNearTerry = 1 << 9,
FP_GameEntityFlag_Attackable = 1 << 10, FP_GameEntityFlag_Attackable = 1 << 10,
FP_GameEntityFlag_RespondsToClubTerry = 1 << 11, FP_GameEntityFlag_RespondsToBuildings = 1 << 11,
FP_GameEntityFlag_PartyingAtClubTerry = 1 << 12, FP_GameEntityFlag_OccupiedInBuilding = 1 << 12,
FP_GameEntityFlag_ExperiencedClubTerry = 1 << 13, FP_GameEntityFlag_PointOfInterestHeart = 1 << 13,
FP_GameEntityFlag_PointOfInterestHeart = 1 << 11,
FP_GameEntityFlag_CameraTracking = 1 << 14, FP_GameEntityFlag_CameraTracking = 1 << 14,
FP_GameEntityFlag_BuildZone = 1 << 15, FP_GameEntityFlag_BuildZone = 1 << 15,
FP_GameEntityFlag_TTL = 1 << 16, FP_GameEntityFlag_TTL = 1 << 16,
@ -162,7 +161,7 @@ struct FP_GameEntity
FP_SentinelList<FP_GameWaypoint> waypoints; FP_SentinelList<FP_GameWaypoint> waypoints;
FP_GameEntityHandle aggro_slot[FP_GameDirection_Count]; FP_GameEntityHandle aggro_slot[FP_GameDirection_Count];
FP_GameEntityHandle club_terry_patron; FP_GameEntityHandle building_patron;
// NOTE: The entity hit box is positioned at the center of the entity. // NOTE: The entity hit box is positioned at the center of the entity.
Dqn_V2 local_hit_box_size; Dqn_V2 local_hit_box_size;
@ -193,6 +192,7 @@ struct FP_GameEntity
Dqn_FArray<FP_GameShape, 4> shapes; Dqn_FArray<FP_GameShape, 4> shapes;
Dqn_FArray<FP_GameEntityHandle, 4> projectiles; Dqn_FArray<FP_GameEntityHandle, 4> projectiles;
uint64_t ttl_end_timestamp; uint64_t ttl_end_timestamp;
FP_SentinelList<FP_GameEntityHandle> buildings_visited;
}; };
struct FP_GameEntityIterator struct FP_GameEntityIterator