diff --git a/External/tely b/External/tely index 8830079..e5c7d83 160000 --- a/External/tely +++ b/External/tely @@ -1 +1 @@ -Subproject commit 883007994e1077746ffe86f78d380559174d188f +Subproject commit e5c7d83121adb093e23655d7ae01820374872340 diff --git a/feely_pona.cpp b/feely_pona.cpp index e8a7d57..3fb075c 100644 --- a/feely_pona.cpp +++ b/feely_pona.cpp @@ -1577,7 +1577,10 @@ void FP_Update(TELY_Platform *platform, FP_Game *game, TELY_PlatformInput *input } } - if (entity->flags & FP_GameEntityFlag_RespondsToBuildings) { + // NOTE: Make waypoint to building ===================================================== + // We can queue up to ente a building if we respond to the building AND we aren't + // already queuing up in a building already. + if (entity->flags & FP_GameEntityFlag_RespondsToBuildings && FP_Game_IsNilEntityHandle(game, entity->queued_at_building)) { FP_GameFindClosestEntityResult closest_building = {}; closest_building.dist_squared = DQN_F32_MAX; closest_building.pos = Dqn_V2_InitNx1(DQN_F32_MAX); @@ -1589,13 +1592,18 @@ void FP_Update(TELY_Platform *platform, FP_Game *game, TELY_PlatformInput *input it_entity->type != FP_EntityType_ChurchTerry) continue; - // NOTE: Already converted, we cannot attend church again ====================== + // NOTE: Already converted, we cannot attend church again if (entity->converted_faction && it_entity->type == FP_EntityType_ChurchTerry) continue; + // NOTE: The queue to enter the building is completely full skip if (it_entity->building_queue.size == Dqn_FArray_Max(&it_entity->building_queue)) continue; + // NOTE: Entity is already in the building queue, skip + if (Dqn_FArray_Find(&it_entity->building_queue, entity->handle).data) + continue; + bool already_visited_building = false; for (FP_SentinelListLink *link_it = {}; !already_visited_building && FP_SentinelList_Iterate(&entity->buildings_visited, &link_it); @@ -1633,32 +1641,84 @@ void FP_Update(TELY_Platform *platform, FP_Game *game, TELY_PlatformInput *input FP_SentinelListLink *link = FP_SentinelList_MakeBefore(&entity->waypoints, FP_SentinelList_Front(&entity->waypoints), game->play.chunk_pool); FP_GameWaypoint *waypoint = &link->data; waypoint->entity = closest_building.entity; - waypoint->type = FP_GameWaypointType_Side; + waypoint->type = FP_GameWaypointType_Queue; + // NOTE: Add the entity to the building queue + FP_GameEntity *building = FP_Game_GetEntity(game, closest_building.entity); + Dqn_FArray_Add(&building->building_queue, entity->handle); - uint32_t *direction_hit_count = nullptr; - FP_GameDirection least_encountered_direction = FP_GameDirection_Down; - - DQN_FOR_UINDEX(dir_index, FP_GameDirection_Count) { - FP_GameEntity *waypoint_entity = FP_Game_GetEntity(game, waypoint->entity); - uint32_t *hit_count = waypoint_entity->count_of_entities_targetting_sides + dir_index; - if (!direction_hit_count || *hit_count < *direction_hit_count) { - direction_hit_count = hit_count; - least_encountered_direction = DQN_CAST(FP_GameDirection)dir_index; - } else if (hit_count == direction_hit_count) { - if (Dqn_PCG32_NextF32(&game->play.rng) >= 0.5f) { - direction_hit_count = hit_count; - least_encountered_direction = DQN_CAST(FP_GameDirection)dir_index; - } - } - } - - waypoint->type_direction = least_encountered_direction; - (*direction_hit_count)++; + // NOTE: Remember the building we are queued at + entity->queued_at_building = building->handle; } } } + // NOTE: Building queue ================================================================ + for (Dqn_usize index = 0; index < entity->building_queue.size; index++) { + FP_GameEntityHandle queue_entity_handle = entity->building_queue.data[index]; + // NOTE: Delete dead entities + if (FP_Game_IsNilEntityHandle(game, queue_entity_handle)) { + index = Dqn_FArray_EraseRange(&entity->building_queue, index, 1 /*count*/, Dqn_ArrayErase_Stable).it_index; + continue; + } + + // NOTE: Delete far away entities + FP_GameEntity *queue_entity = FP_Game_GetEntity(game, queue_entity_handle); + Dqn_V2 queue_entity_p = FP_Game_CalcEntityWorldPos(game, queue_entity_handle); + Dqn_V2 building_p = FP_Game_CalcEntityWorldPos(game, entity->handle); + Dqn_f32 dist_sq = Dqn_V2_LengthSq_V2x2(queue_entity_p, building_p); + Dqn_f32 threshold_sq = DQN_SQUARED(FP_Game_MetersToPixelsNx1(game->play, 10.f)); + if (dist_sq >= threshold_sq || queue_entity->converted_faction) { + // NOTE: Remove the entity from the building + queue_entity->queued_at_building = {}; + + // NOTE: Remove it from the queue + index = Dqn_FArray_EraseRange(&entity->building_queue, index, 1 /*count*/, Dqn_ArrayErase_Stable).it_index; + + // NOTE: Make sure the entity doesnt' try and revisit + FP_SentinelList_Add(&queue_entity->buildings_visited, game->play.chunk_pool, entity->handle); + + // NOTE: Remove the waypoint from the entity + if (queue_entity->waypoints.size) { + for (FP_SentinelListLink *link = nullptr; + FP_SentinelList_Iterate(&queue_entity->waypoints, &link); ) { + if (link->data.entity == entity->handle) { + FP_SentinelList_Erase(&queue_entity->waypoints, link, game->play.chunk_pool); + } + } + } + } + } + + #if 0 + // NOTE: Bubble sort entities in the building queue by distance to the building ======== + // For example the closest entity will be assigned the first queue slot to the building + if (entity->building_queue.size && game->play.clock_ms >= entity->building_queue_next_sort_timestamp_ms) { + + // NOTE: We only sort the queue for the building every second. This prevents the queue + // from stagnating incase some entity has locked a position in the queue but had + // some dodgy physics that sent it far away + entity->building_queue_next_sort_timestamp_ms = game->play.clock_ms + 1000; + for (bool swapped = true; swapped; ) { + swapped = false; + for (Dqn_usize index = 0; index < (entity->building_queue.size - 1); index++) { + FP_GameEntityHandle left_handle = entity->building_queue.data[index + 0]; + FP_GameEntityHandle right_handle = entity->building_queue.data[index + 1]; + Dqn_V2 left_world_p = FP_Game_CalcEntityWorldPos(game, left_handle); + Dqn_V2 right_world_p = FP_Game_CalcEntityWorldPos(game, right_handle); + Dqn_f32 left_dist_sq = Dqn_V2_LengthSq_V2x2(entity_pos, left_world_p); + Dqn_f32 right_dist_sq = Dqn_V2_LengthSq_V2x2(entity_pos, right_world_p); + + if (left_dist_sq > (right_dist_sq * 1.1f)) { + DQN_SWAP(entity->building_queue.data[index + 0], entity->building_queue.data[index + 1]); + swapped = true; + } + } + } + } + #endif + + // NOTE: Handle waypoints ============================================================== while (entity->waypoints.size) { FP_SentinelListLink *waypoint_link = entity->waypoints.sentinel->next; FP_GameWaypoint const *waypoint = &waypoint_link->data; @@ -1760,7 +1820,7 @@ void FP_Update(TELY_Platform *platform, FP_Game *game, TELY_PlatformInput *input } } } else if (entity->type == FP_EntityType_Clinger) { - if (game->play.clock_ms >= entity->clinger_next_dash_timestamp) { + if (game->play.clock_ms >= entity->clinger_next_dash_timestamp && dist_to_waypoint_sq > (DQN_SQUARED(arrival_threshold * 4.f))) { entity->clinger_next_dash_timestamp = game->play.clock_ms + 2000; acceleration_meters_per_s = entity_to_waypoint_norm * entity->base_acceleration_per_s.meters * 45.f; } @@ -1779,30 +1839,53 @@ void FP_Update(TELY_Platform *platform, FP_Game *game, TELY_PlatformInput *input waypoint_entity->type == FP_EntityType_ClubTerry || waypoint_entity->type == FP_EntityType_AirportTerry || waypoint_entity->type == FP_EntityType_ChurchTerry; - if (((waypoint->flags & FP_GameWaypointFlag_NonInterruptible) == 0) && (aggro || building_response)) { + + if (aggro || building_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 if (building_response) { FP_GameEntity *building = waypoint_entity; + can_attack = false; + + DQN_ASSERTF( + waypoint->type == FP_GameWaypointType_Queue, + "There's nothing stopping us from supporting other " + "waypoint types to buildings, but, for this game " + "we only ever make mobs queue at the building"); + + DQN_ASSERTF( + building->building_queue.size, + "An entity should only be forming a waypoint to " + "the building if there was space in the queue and " + "they were added the the queue"); + if (FP_Game_IsNilEntityHandle(game, building->building_patron)) { - building->building_patron = entity->handle; + if (waypoint_entity->building_queue.data[0] == entity->handle) { + // NOTE: This entity is front-in-line in the queue to enter the building, we can enter! + building->building_patron = entity->handle; + entity->queued_at_building = {}; - 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)); - if (building->type == FP_EntityType_ClubTerry) { - FP_Game_EntityTransitionState(game, building, FP_EntityClubTerryState_PartyTime); - entity->local_pos = exit_pos; // TODO(doyle): Only works when parent world pos is 0,0 - } else if (building->type == FP_EntityType_AirportTerry) { - FP_Game_EntityTransitionState(game, building, FP_EntityAirportTerryState_FlyPassenger); - entity->local_pos = exit_pos; // TODO(doyle): Only works when parent world pos is 0,0 - } else { - DQN_ASSERT(building->type == FP_EntityType_ChurchTerry); - FP_Game_EntityTransitionState(game, building, FP_EntityChurchTerryState_ConvertPatron); + // NOTE: Remove them from the queue + Dqn_FArray_EraseRange(&waypoint_entity->building_queue, 0 /*index*/, 1 /*count*/, Dqn_ArrayErase_Stable); + + 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)); + if (building->type == FP_EntityType_ClubTerry) { + FP_Game_EntityTransitionState(game, building, FP_EntityClubTerryState_PartyTime); + entity->local_pos = exit_pos; // TODO(doyle): Only works when parent world pos is 0,0 + } else if (building->type == FP_EntityType_AirportTerry) { + FP_Game_EntityTransitionState(game, building, FP_EntityAirportTerryState_FlyPassenger); + entity->local_pos = exit_pos; // TODO(doyle): Only works when parent world pos is 0,0 + } else { + DQN_ASSERT(building->type == FP_EntityType_ChurchTerry); + FP_Game_EntityTransitionState(game, building, FP_EntityChurchTerryState_ConvertPatron); + } + + entity->flags |= FP_GameEntityFlag_OccupiedInBuilding; + FP_SentinelList_Erase(&entity->waypoints, waypoint_link, game->play.chunk_pool); + + // NOTE: Add the building to the entity's visit list to prevent them from re-entering + FP_SentinelList_Add(&entity->buildings_visited, game->play.chunk_pool, building->handle); } - - entity->flags |= FP_GameEntityFlag_OccupiedInBuilding; - can_attack = false; - FP_SentinelList_Erase(&entity->waypoints, waypoint_link, game->play.chunk_pool); - FP_SentinelList_Add(&entity->buildings_visited, game->play.chunk_pool, building->handle); } } @@ -3294,15 +3377,17 @@ void FP_Render(FP_Game *game, TELY_Platform *platform, TELY_Renderer *renderer, Dqn_V2 draw_p = Dqn_V2_InitNx2(32.f, platform->core.window_size.h * .5f); TELY_Render_PushFont(renderer, game->inter_regular_font); TELY_Render_TextF(renderer, draw_p, Dqn_V2_Zero, "DEBUG MENU"); draw_p.y += TELY_Render_FontHeight(renderer, assets); - TELY_Render_TextF(renderer, draw_p, Dqn_V2_Zero, " F1 Debug info"); draw_p.y += TELY_Render_FontHeight(renderer, assets); - TELY_Render_TextF(renderer, draw_p, Dqn_V2_Zero, " F2 Add coins x10,000"); draw_p.y += TELY_Render_FontHeight(renderer, assets); - TELY_Render_TextF(renderer, draw_p, Dqn_V2_Zero, " F3 Win game"); draw_p.y += TELY_Render_FontHeight(renderer, assets); - TELY_Render_TextF(renderer, draw_p, Dqn_V2_Zero, " F4 Reset game"); draw_p.y += TELY_Render_FontHeight(renderer, assets); - TELY_Render_TextF(renderer, draw_p, Dqn_V2_Zero, " F5 Increase health"); draw_p.y += TELY_Render_FontHeight(renderer, assets); - TELY_Render_TextF(renderer, draw_p, Dqn_V2_Zero, " F6 Increase stamina"); draw_p.y += TELY_Render_FontHeight(renderer, assets); - TELY_Render_TextF(renderer, draw_p, Dqn_V2_Zero, " F7 Increase mobile data"); draw_p.y += TELY_Render_FontHeight(renderer, assets); - TELY_Render_TextF(renderer, draw_p, Dqn_V2_Zero, " F8 %s god mode", game->play.god_mode ? "Disable" : "Enable"); draw_p.y += TELY_Render_FontHeight(renderer, assets); - TELY_Render_TextF(renderer, draw_p, Dqn_V2_Zero, " F9 %s noclip", player->flags & FP_GameEntityFlag_NoClip ? "Disable" : "Enable"); draw_p.y += TELY_Render_FontHeight(renderer, assets); + TELY_Render_TextF(renderer, draw_p, Dqn_V2_Zero, " F1 Debug info"); draw_p.y += TELY_Render_FontHeight(renderer, assets); + TELY_Render_TextF(renderer, draw_p, Dqn_V2_Zero, " F2 Add coins x10,000"); draw_p.y += TELY_Render_FontHeight(renderer, assets); + TELY_Render_TextF(renderer, draw_p, Dqn_V2_Zero, " F3 Win game"); draw_p.y += TELY_Render_FontHeight(renderer, assets); + TELY_Render_TextF(renderer, draw_p, Dqn_V2_Zero, " F4 Reset game"); draw_p.y += TELY_Render_FontHeight(renderer, assets); + TELY_Render_TextF(renderer, draw_p, Dqn_V2_Zero, " F5 Increase health"); draw_p.y += TELY_Render_FontHeight(renderer, assets); + TELY_Render_TextF(renderer, draw_p, Dqn_V2_Zero, " F6 Increase stamina"); draw_p.y += TELY_Render_FontHeight(renderer, assets); + TELY_Render_TextF(renderer, draw_p, Dqn_V2_Zero, " F7 Increase mobile data"); draw_p.y += TELY_Render_FontHeight(renderer, assets); + TELY_Render_TextF(renderer, draw_p, Dqn_V2_Zero, " F8 %s god mode", game->play.god_mode ? "Disable" : "Enable"); draw_p.y += TELY_Render_FontHeight(renderer, assets); + TELY_Render_TextF(renderer, draw_p, Dqn_V2_Zero, " F9 %s noclip", player->flags & FP_GameEntityFlag_NoClip ? "Disable" : "Enable"); draw_p.y += TELY_Render_FontHeight(renderer, assets); + TELY_Render_TextF(renderer, draw_p, Dqn_V2_Zero, " F10 Building inventory +1"); draw_p.y += TELY_Render_FontHeight(renderer, assets); + TELY_Render_TextF(renderer, draw_p, Dqn_V2_Zero, " F11 %s by enemies", player->faction == FP_GameEntityFaction_Nil ? "Attacked" : "Ignored"); draw_p.y += TELY_Render_FontHeight(renderer, assets); TELY_Render_PopFont(renderer); TELY_Render_PopColourV4(renderer); @@ -3346,6 +3431,18 @@ void FP_Render(FP_Game *game, TELY_Platform *platform, TELY_Renderer *renderer, if (TELY_Platform_InputScanCodeIsPressed(input, TELY_PlatformInputScanCode_F9)) player->flags ^= FP_GameEntityFlag_NoClip; + if (TELY_Platform_InputScanCodeIsPressed(input, TELY_PlatformInputScanCode_F10)) { + player->inventory.clubs += 1; + player->inventory.airports += 1; + player->inventory.churchs += 1; + player->inventory.kennels += 1; + } + + if (TELY_Platform_InputScanCodeIsPressed(input, TELY_PlatformInputScanCode_F11)) { + player->faction = player->faction == FP_GameEntityFaction_Nil + ? FP_GameEntityFaction_Friendly + : FP_GameEntityFaction_Nil; + } } } diff --git a/feely_pona_game.cpp b/feely_pona_game.cpp index 118bc79..afaf496 100644 --- a/feely_pona_game.cpp +++ b/feely_pona_game.cpp @@ -701,7 +701,7 @@ static Dqn_Slice FP_Game_AStarPathFind(FP_Game *game, return result; } -static Dqn_V2 FP_Game_CalcWaypointWorldPos(FP_Game *game, FP_GameEntityHandle src_entity, FP_GameWaypoint const *waypoint) +static Dqn_V2 FP_Game_CalcWaypointWorldPos(FP_Game *game, FP_GameEntityHandle entity_handle, FP_GameWaypoint const *waypoint) { Dqn_V2 result = {}; if (!game || !waypoint) @@ -720,41 +720,47 @@ static Dqn_V2 FP_Game_CalcWaypointWorldPos(FP_Game *game, FP_GameEntityHandle sr case FP_GameWaypointType_ClosestSide: /*FALLTHRU*/ case FP_GameWaypointType_Side: { // NOTE: Sweep entity with half the radius of the source entity - Dqn_Rect src_rect = FP_Game_CalcEntityWorldHitBox(game, src_entity); - Dqn_Rect entity_rect = FP_Game_CalcEntityWorldHitBox(game, waypoint_entity->handle); - entity_rect.pos -= (src_rect.size * .5f); - entity_rect.size += src_rect.size; + Dqn_Rect entity_hit_box = FP_Game_CalcEntityWorldHitBox(game, entity_handle); + Dqn_Rect waypoint_hit_box = FP_Game_CalcEntityWorldHitBox(game, waypoint_entity->handle); + waypoint_hit_box.pos -= (entity_hit_box.size * .5f); + waypoint_hit_box.size += entity_hit_box.size; Dqn_V2 side_pos_list[FP_GameDirection_Count] = {}; - side_pos_list[FP_GameDirection_Up] = Dqn_V2_InitNx2(entity_rect.pos.x + entity_rect.size.w * .5f, entity_rect.pos.y - entity_rect.size.h * .1f); - side_pos_list[FP_GameDirection_Down] = Dqn_V2_InitNx2(entity_rect.pos.x + entity_rect.size.w * .5f, entity_rect.pos.y + entity_rect.size.h + entity_rect.size.h * .1f); - side_pos_list[FP_GameDirection_Left] = Dqn_V2_InitNx2(entity_rect.pos.x - entity_rect.size.w * .1f, entity_rect.pos.y + entity_rect.size.h * .5f); - side_pos_list[FP_GameDirection_Right] = Dqn_V2_InitNx2(entity_rect.pos.x + entity_rect.size.w + entity_rect.size.w * .1f, entity_rect.pos.y + entity_rect.size.h * .5f); + side_pos_list[FP_GameDirection_Up] = Dqn_V2_InitNx2(waypoint_hit_box.pos.x + waypoint_hit_box.size.w * .5f, waypoint_hit_box.pos.y - waypoint_hit_box.size.h * .1f); + side_pos_list[FP_GameDirection_Down] = Dqn_V2_InitNx2(waypoint_hit_box.pos.x + waypoint_hit_box.size.w * .5f, waypoint_hit_box.pos.y + waypoint_hit_box.size.h + waypoint_hit_box.size.h * .1f); + side_pos_list[FP_GameDirection_Left] = Dqn_V2_InitNx2(waypoint_hit_box.pos.x - waypoint_hit_box.size.w * .1f, waypoint_hit_box.pos.y + waypoint_hit_box.size.h * .5f); + side_pos_list[FP_GameDirection_Right] = Dqn_V2_InitNx2(waypoint_hit_box.pos.x + waypoint_hit_box.size.w + waypoint_hit_box.size.w * .1f, waypoint_hit_box.pos.y + waypoint_hit_box.size.h * .5f); if (waypoint->type == FP_GameWaypointType_Side) { result = side_pos_list[waypoint->type_direction]; } else { Dqn_f32 best_dist = DQN_F32_MAX; for (Dqn_V2 target_pos : side_pos_list) { - Dqn_f32 dist_squared = Dqn_V2_LengthSq_V2x2(src_rect.pos, target_pos); + Dqn_f32 dist_squared = Dqn_V2_LengthSq_V2x2(entity_hit_box.pos, target_pos); if (dist_squared < best_dist) { best_dist = dist_squared; result = target_pos; } } - -#if 0 - Dqn_V2 entity_pos = FP_Game_CalcEntityWorldPos(game, waypoint_entity->handle); - Dqn_V2 src_pos = FP_Game_CalcEntityWorldPos(game, src_entity); - Dqn_f32 curr_dist_to_entity = Dqn_V2_LengthSq_V2x2(entity_pos, src_pos); - if (curr_dist_to_entity < best_dist) { - // NOTE: We are already closer to the entity than the closest calculated side, - // we assume we're at the entity already. - result = FP_Game_CalcEntityWorldPos(game, src_entity); - } -#endif } } break; + + case FP_GameWaypointType_Queue: { + Dqn_ArrayFindResult find_result = Dqn_FArray_Find(&waypoint_entity->building_queue, entity_handle); + Dqn_usize index_in_queue = find_result.index; + Dqn_Rect entity_hit_box = FP_Game_CalcEntityWorldHitBox(game, entity_handle); + Dqn_Rect waypoint_hit_box = FP_Game_CalcEntityWorldHitBox(game, waypoint_entity->handle); + + Dqn_V2 queue_starting_p = {}; + if (waypoint_entity->type == FP_EntityType_ClubTerry || waypoint_entity->type == FP_EntityType_ChurchTerry) { + queue_starting_p = Dqn_Rect_InterpolatedPoint(waypoint_hit_box, Dqn_V2_InitNx2(0.5f, 1.1f)); + } else { + queue_starting_p = Dqn_Rect_InterpolatedPoint(waypoint_hit_box, Dqn_V2_InitNx2(1.f, 1.1f)); + } + + Dqn_f32 queue_spacing = FP_Game_MetersToPixelsNx1(game->play, 1.f); + result = Dqn_V2_InitNx2(queue_starting_p.x - (queue_spacing * index_in_queue), queue_starting_p.y); + } break; } result += waypoint->offset; diff --git a/feely_pona_game.h b/feely_pona_game.h index 2493f19..670feae 100644 --- a/feely_pona_game.h +++ b/feely_pona_game.h @@ -76,6 +76,7 @@ enum FP_GameWaypointType FP_GameWaypointType_At, // Move to the specified entity FP_GameWaypointType_Side, // Move to the side of the entity specified by the direction FP_GameWaypointType_ClosestSide, // Move to the side of the entity closest to us + FP_GameWaypointType_Queue, // Queue at the target entity }; enum FP_GameDirection @@ -87,14 +88,8 @@ enum FP_GameDirection FP_GameDirection_Count, }; -enum FP_GameWaypointFlag -{ - FP_GameWaypointFlag_NonInterruptible = 1 << 0, -}; - struct FP_GameWaypoint { - uint32_t flags; FP_GameWaypointType type; FP_GameDirection type_direction; // Used if type is `FP_GameWaypointType_Side` FP_GameEntityHandle entity; // The entity to move to @@ -237,7 +232,10 @@ struct FP_GameEntity uint32_t count_of_entities_targetting_sides[FP_GameDirection_Count]; FP_GameEntityHandle carried_monkey; - Dqn_FArray building_queue; + + Dqn_FArray building_queue; + uint64_t building_queue_next_sort_timestamp_ms; + FP_GameEntityHandle queued_at_building; }; struct FP_GameEntityIterator