diff --git a/External/tely b/External/tely index 595e3c7..8d39ef2 160000 --- a/External/tely +++ b/External/tely @@ -1 +1 @@ -Subproject commit 595e3c7f1e70aab8e51620682291aa2ba6c02b7f +Subproject commit 8d39ef2d8365e223114bbdc2ce2db2edaaf81163 diff --git a/feely_pona.cpp b/feely_pona.cpp index b931b78..c8c68ea 100644 --- a/feely_pona.cpp +++ b/feely_pona.cpp @@ -187,6 +187,12 @@ static void FP_Game_MoveEntity(FP_Game *game, FP_GameEntityHandle entity_handle, #else entity_collides_with_collider = false; #endif + } else if (collider->type == FP_EntityType_Heart || + collider->type == FP_EntityType_MerchantGym || + collider->type == FP_EntityType_MerchantTerry || + collider->type == FP_EntityType_MerchantGraveyard || + collider->type == FP_EntityType_MerchantPhoneCompany) { + entity_collides_with_collider = false; } } break; @@ -464,10 +470,9 @@ static void FP_PlayReset(FP_Game *game, TELY_Platform *platform) Dqn_V2I max_tile = platform->core.window_size / play->tile_size; // NOTE: Heart - FP_Entity_CreateHeart(game, base_mid_p, "Heart"); - - play->camera.world_pos = base_mid_p - Dqn_V2_InitV2I(platform->core.window_size * .5f); - play->camera.scale = Dqn_V2_InitNx1(1); + game->play.heart = FP_Entity_CreateHeart(game, base_mid_p, "Heart"); + play->camera.world_pos = base_mid_p - Dqn_V2_InitV2I(platform->core.window_size * .5f); + play->camera.scale = Dqn_V2_InitNx1(1); } extern "C" __declspec(dllexport) @@ -1145,7 +1150,7 @@ void FP_EntityActionStateMachine(FP_Game *game, TELY_Audio *audio, TELY_Platform if (entity->waypoints.size == 0) { FP_GameEntity *patron = FP_Game_GetEntity(game, entity->building_patron); patron->local_pos = entity->local_pos; - patron->flags &= ~(FP_GameEntityFlag_OccupiedInBuilding | FP_GameEntityFlag_PointOfInterestHeart); + patron->flags &= ~(FP_GameEntityFlag_OccupiedInBuilding); FP_Game_DeleteEntity(game, entity->handle); return; } @@ -1241,7 +1246,7 @@ void FP_EntityActionStateMachine(FP_Game *game, TELY_Audio *audio, TELY_Platform 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 | FP_GameEntityFlag_RespondsToBuildings | FP_GameEntityFlag_PointOfInterestHeart); + patron->flags &= ~(FP_GameEntityFlag_OccupiedInBuilding | FP_GameEntityFlag_RespondsToBuildings); patron->faction = FP_GameEntityFaction_Friendly; patron->converted_faction = true; } @@ -1517,302 +1522,279 @@ void FP_Update(TELY_Platform *platform, FP_Game *game, TELY_PlatformInput *input // NOTE: Determine AI movement ============================================================= Dqn_V2 entity_pos = FP_Game_CalcEntityWorldPos(game, entity->handle); - if (acceleration_meters_per_s.x == 0 && acceleration_meters_per_s.y == 0) { - if (entity->flags & FP_GameEntityFlag_Aggros && entity->faction != FP_GameEntityFaction_Nil) { - FP_GameFindClosestEntityResult closest_defender = {}; - closest_defender.dist_squared = DQN_F32_MAX; - closest_defender.pos = Dqn_V2_InitNx1(DQN_F32_MAX); + if (entity->flags & FP_GameEntityFlag_Aggros && entity->faction != FP_GameEntityFaction_Nil) { + FP_GameFindClosestEntityResult closest_defender = {}; + closest_defender.dist_squared = DQN_F32_MAX; + closest_defender.pos = Dqn_V2_InitNx1(DQN_F32_MAX); - FP_GameEntityFaction enemy_faction = - entity->faction == FP_GameEntityFaction_Friendly - ? FP_GameEntityFaction_Foe - : FP_GameEntityFaction_Friendly; + FP_GameEntityFaction enemy_faction = + entity->faction == FP_GameEntityFaction_Friendly + ? FP_GameEntityFaction_Foe + : FP_GameEntityFaction_Friendly; - for (FP_GameEntityIterator defender_it = {}; FP_Game_DFSPostOrderWalkEntityTree(game, &defender_it, game->play.root_entity); ) { - FP_GameEntity *it_entity = defender_it.entity; - Dqn_V2 pos = FP_Game_CalcEntityWorldPos(game, it_entity->handle); - Dqn_f32 dist = Dqn_V2_LengthSq_V2x2(pos, entity_pos); + for (FP_GameEntityIterator defender_it = {}; FP_Game_DFSPostOrderWalkEntityTree(game, &defender_it, game->play.root_entity); ) { + FP_GameEntity *it_entity = defender_it.entity; + Dqn_V2 pos = FP_Game_CalcEntityWorldPos(game, it_entity->handle); + Dqn_f32 dist = Dqn_V2_LengthSq_V2x2(pos, entity_pos); - if (it_entity->faction != enemy_faction) - continue; + if (it_entity->faction != enemy_faction) + continue; - if (dist < closest_defender.dist_squared) { - closest_defender.pos = pos; - closest_defender.dist_squared = dist; - closest_defender.entity = it_entity->handle; - } - } - - Dqn_f32 aggro_dist_threshold = FP_Game_MetersToPixelsNx1(game->play, 4.f); - if (closest_defender.dist_squared < DQN_SQUARED(aggro_dist_threshold)) { - bool has_waypoint_to_defender = false; - for (FP_SentinelListLink *link = nullptr; - !has_waypoint_to_defender && FP_SentinelList_Iterate(&entity->waypoints, &link); ) { - has_waypoint_to_defender = link->data.entity == closest_defender.entity; - } - - if (!has_waypoint_to_defender) { - FP_GameEntity *defender = FP_Game_GetEntity(game, closest_defender.entity); - FP_SentinelListLink *link = FP_SentinelList_MakeBefore(&entity->waypoints, - FP_SentinelList_Front(&entity->waypoints), - game->play.chunk_pool); - FP_GameWaypoint *waypoint = &link->data; - waypoint->entity = defender->handle; - waypoint->type = FP_GameWaypointType_ClosestSide; - } - } else { - if (closest_defender.dist_squared > DQN_SQUARED(aggro_dist_threshold * 2.f)) { - for (FP_SentinelListLink *link = nullptr; FP_SentinelList_Iterate(&entity->waypoints, &link); ) { - FP_GameEntity *maybe_terry = FP_Game_GetEntity(game, link->data.entity); - if (maybe_terry->type == FP_EntityType_Terry) { - link = FP_SentinelList_Erase(&entity->waypoints, link, game->play.chunk_pool); - } - } - } + if (dist < closest_defender.dist_squared) { + closest_defender.pos = pos; + closest_defender.dist_squared = dist; + closest_defender.entity = it_entity->handle; } } - if (entity->flags & FP_GameEntityFlag_RespondsToBuildings) { - FP_GameFindClosestEntityResult closest_building = {}; - closest_building.dist_squared = DQN_F32_MAX; - closest_building.pos = Dqn_V2_InitNx1(DQN_F32_MAX); - - for (FP_GameEntityIterator building_it = {}; FP_Game_DFSPostOrderWalkEntityTree(game, &building_it, game->play.root_entity); ) { - FP_GameEntity *it_entity = building_it.entity; - if (it_entity->type != FP_EntityType_ClubTerry && - it_entity->type != FP_EntityType_AirportTerry && - it_entity->type != FP_EntityType_ChurchTerry) - continue; - - // NOTE: Already converted, we cannot attend church again - if (entity->converted_faction && it_entity->type == FP_EntityType_ChurchTerry) { - continue; - } - - bool already_visited_building = false; - for (FP_SentinelListLink *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 (already_visited_building) - continue; - - 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; + Dqn_f32 aggro_dist_threshold = FP_Game_MetersToPixelsNx1(game->play, 4.f); + Dqn_f32 dist_to_defender = DQN_SQRTF(closest_defender.dist_squared); + if (dist_to_defender > (aggro_dist_threshold * 1.5f)) { + for (FP_SentinelListLink *link = nullptr; FP_SentinelList_Iterate(&entity->waypoints, &link); ) { + FP_GameEntity *maybe_terry = FP_Game_GetEntity(game, link->data.entity); + if (maybe_terry->type == FP_EntityType_Terry) { + link = FP_SentinelList_Erase(&entity->waypoints, link, game->play.chunk_pool); } } + } else if (dist_to_defender < aggro_dist_threshold) { + bool has_waypoint_to_defender = false; + for (FP_SentinelListLink *link = nullptr; + !has_waypoint_to_defender && FP_SentinelList_Iterate(&entity->waypoints, &link); ) { + has_waypoint_to_defender = link->data.entity == closest_defender.entity; + } - if (!FP_Game_IsNilEntityHandle(game, closest_building.entity) && - closest_building.dist_squared < DQN_SQUARED(FP_Game_MetersToPixelsNx1(game->play, 5.f))) { - - bool has_waypoint_to_building = false; - for (FP_SentinelListLink *link = nullptr; - !has_waypoint_to_building && FP_SentinelList_Iterate(&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 *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; - - - 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)++; - } + if (!has_waypoint_to_defender) { + FP_GameEntity *defender = FP_Game_GetEntity(game, closest_defender.entity); + FP_SentinelListLink *link = FP_SentinelList_MakeBefore(&entity->waypoints, + FP_SentinelList_Front(&entity->waypoints), + game->play.chunk_pool); + FP_GameWaypoint *waypoint = &link->data; + waypoint->entity = defender->handle; + waypoint->type = FP_GameWaypointType_ClosestSide; } } + } - if (entity->flags & FP_GameEntityFlag_PointOfInterestHeart) { - FP_GameFindClosestEntityResult closest_heart = FP_Game_FindClosestEntityWithType(game, entity->handle, FP_EntityType_Heart); - if (closest_heart.dist_squared < DQN_SQUARED(FP_Game_MetersToPixelsNx1(game->play, 4.f))) { + if (entity->flags & FP_GameEntityFlag_RespondsToBuildings) { + FP_GameFindClosestEntityResult closest_building = {}; + closest_building.dist_squared = DQN_F32_MAX; + closest_building.pos = Dqn_V2_InitNx1(DQN_F32_MAX); - bool has_waypoint_to = false; - for (FP_SentinelListLink *link = nullptr; - !has_waypoint_to && FP_SentinelList_Iterate(&entity->waypoints, &link); ) { - has_waypoint_to = link->data.entity == closest_heart.entity; - } + for (FP_GameEntityIterator building_it = {}; FP_Game_DFSPostOrderWalkEntityTree(game, &building_it, game->play.root_entity); ) { + FP_GameEntity *it_entity = building_it.entity; + if (it_entity->type != FP_EntityType_ClubTerry && + it_entity->type != FP_EntityType_AirportTerry && + it_entity->type != FP_EntityType_ChurchTerry) + continue; - if (!has_waypoint_to) { - Dqn_Rect club_hit_box = FP_Game_CalcEntityWorldHitBox(game, closest_heart.entity); - Dqn_V2 club_top_left = Dqn_Rect_TopLeft(club_hit_box); - Dqn_V2 club_top_right = Dqn_Rect_TopRight(club_hit_box); - - 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_heart.entity; - waypoint->type = FP_GameWaypointType_ClosestSide; - } - } - } - - while (entity->waypoints.size) { - FP_SentinelListLink *waypoint_link = entity->waypoints.sentinel->next; - FP_GameWaypoint const *waypoint = &waypoint_link->data; - FP_GameEntity *waypoint_entity = FP_Game_GetEntity(game, waypoint_link->data.entity); - if (FP_Game_IsNilEntity(waypoint_entity)) { - FP_SentinelList_Erase(&entity->waypoints, waypoint_link, game->play.chunk_pool); + // NOTE: Already converted, we cannot attend church again + if (entity->converted_faction && it_entity->type == FP_EntityType_ChurchTerry) { continue; } - // NOTE: We found a waypoint that is valid to move towards - Dqn_V2 target_pos = FP_Game_CalcWaypointWorldPos(game, entity->handle, waypoint); - Dqn_V2 entity_to_waypoint = target_pos - entity_pos; - - // NOTE: Check if we've arrived at the waypoint - Dqn_f32 dist_to_waypoint_sq = Dqn_V2_LengthSq(entity_to_waypoint); - - Dqn_f32 arrival_threshold = {}; - switch (waypoint->arrive) { - case FP_GameWaypointArrive_Default: { - Dqn_Rect waypoint_hit_box = FP_Game_CalcEntityWorldHitBox(game, waypoint_entity->handle); - arrival_threshold = waypoint_hit_box.size.w * .5f; - } break; - - case FP_GameWaypointArrive_WhenWithinEntitySize: { - arrival_threshold = DQN_MAX(waypoint_entity->local_hit_box_size.w, waypoint_entity->local_hit_box_size.h) * waypoint_link->data.value; - } break; + bool already_visited_building = false; + for (FP_SentinelListLink *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; } - // NOTE: We haven't arrived yet, calculate an acceleration vector to the waypoint - if (dist_to_waypoint_sq > DQN_SQUARED(arrival_threshold)) { - Dqn_V2 entity_to_waypoint_norm = Dqn_V2_Normalise(entity_to_waypoint); - acceleration_meters_per_s = entity_to_waypoint_norm * (entity->base_acceleration_per_s.meters * 0.25f); - break; + if (already_visited_building) + continue; + + 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->play, 5.f))) { + + bool has_waypoint_to_building = false; + for (FP_SentinelListLink *link = nullptr; + !has_waypoint_to_building && FP_SentinelList_Iterate(&entity->waypoints, &link); ) { + has_waypoint_to_building = link->data.entity == closest_building.entity; } - // NOTE: We have arrived at the waypoint - bool aggro = false; - if (entity->flags & FP_GameEntityFlag_Aggros) { - aggro |= entity->faction == FP_GameEntityFaction_Friendly && waypoint_entity->faction & FP_GameEntityFaction_Foe; - aggro |= entity->faction == FP_GameEntityFaction_Foe && waypoint_entity->faction & FP_GameEntityFaction_Friendly; + 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 *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; + + + 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)++; + } + } + } + + while (entity->waypoints.size) { + FP_SentinelListLink *waypoint_link = entity->waypoints.sentinel->next; + FP_GameWaypoint const *waypoint = &waypoint_link->data; + FP_GameEntity *waypoint_entity = FP_Game_GetEntity(game, waypoint_link->data.entity); + if (FP_Game_IsNilEntity(waypoint_entity)) { + FP_SentinelList_Erase(&entity->waypoints, waypoint_link, game->play.chunk_pool); + continue; + } + + // NOTE: We found a waypoint that is valid to move towards + Dqn_V2 target_pos = FP_Game_CalcWaypointWorldPos(game, entity->handle, waypoint); + Dqn_V2 entity_to_waypoint = target_pos - entity_pos; + + // NOTE: Check if we've arrived at the waypoint + Dqn_f32 dist_to_waypoint_sq = Dqn_V2_LengthSq(entity_to_waypoint); + + // NOTE: Calculate the approaching direction + FP_GameDirection approach_dir = FP_GameDirection_Up; + { + Dqn_V2 dir_vectors[FP_GameDirection_Count] = {}; + dir_vectors[FP_GameDirection_Up] = Dqn_V2_InitNx2(+0, -1); + dir_vectors[FP_GameDirection_Down] = Dqn_V2_InitNx2(+0, +1); + dir_vectors[FP_GameDirection_Left] = Dqn_V2_InitNx2(-1, +0); + dir_vectors[FP_GameDirection_Right] = Dqn_V2_InitNx2(+1, +0); + + Dqn_V2 target_entity_pos = FP_Game_CalcEntityWorldPos(game, waypoint_entity->handle); + Dqn_V2 entity_to_target = target_entity_pos - entity_pos; + Dqn_V2 entity_to_target_norm = Dqn_V2_Normalise(entity_to_target); + Dqn_f32 approach_dir_scalar_projection_onto_entity_to_waypoint_vector = -1.1f; + DQN_FOR_UINDEX (dir_index, FP_GameDirection_Count) { + Dqn_V2 attack_dir = dir_vectors[dir_index]; + Dqn_f32 scalar_projection = Dqn_V2_Dot(attack_dir, entity_to_target_norm); + if (scalar_projection > approach_dir_scalar_projection_onto_entity_to_waypoint_vector) { + approach_dir = DQN_CAST(FP_GameDirection)dir_index; + approach_dir_scalar_projection_onto_entity_to_waypoint_vector = scalar_projection; + } + } + } + + Dqn_f32 arrival_threshold = {}; + switch (waypoint->arrive) { + case FP_GameWaypointArrive_Default: { + Dqn_Rect waypoint_hit_box = FP_Game_CalcEntityWorldHitBox(game, waypoint_entity->handle); + if (approach_dir == FP_GameDirection_Up || approach_dir == FP_GameDirection_Down) + arrival_threshold = 10.f; + else + arrival_threshold = 10.f; + } break; + + case FP_GameWaypointArrive_WhenWithinEntitySize: { + arrival_threshold = DQN_MAX(waypoint_entity->local_hit_box_size.w, waypoint_entity->local_hit_box_size.h) * waypoint_link->data.value; + } break; + } + + // NOTE: We haven't arrived yet, calculate an acceleration vector to the waypoint + if (dist_to_waypoint_sq > DQN_SQUARED(arrival_threshold)) { + Dqn_V2 entity_to_waypoint_norm = Dqn_V2_Normalise(entity_to_waypoint); + acceleration_meters_per_s = entity_to_waypoint_norm * (entity->base_acceleration_per_s.meters * 0.25f); + break; + } + + // NOTE: We have arrived at the waypoint + bool aggro = false; + if (entity->flags & FP_GameEntityFlag_Aggros) { + aggro |= entity->faction == FP_GameEntityFaction_Friendly && waypoint_entity->faction & FP_GameEntityFaction_Foe; + aggro |= entity->faction == FP_GameEntityFaction_Foe && waypoint_entity->faction & FP_GameEntityFaction_Friendly; + } + + bool building_response = (entity->flags & FP_GameEntityFlag_RespondsToBuildings) && + 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)) { + 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; + if (FP_Game_IsNilEntityHandle(game, building->building_patron)) { + building->building_patron = entity->handle; + + 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; + 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); + } } - bool building_response = (entity->flags & FP_GameEntityFlag_RespondsToBuildings) && - waypoint_entity->type == FP_EntityType_ClubTerry || - waypoint_entity->type == FP_EntityType_AirportTerry || - waypoint_entity->type == FP_EntityType_ChurchTerry; - bool heart_response = (entity->flags & FP_GameEntityFlag_PointOfInterestHeart) && waypoint_entity->type == FP_EntityType_Heart; - - if (((waypoint->flags & FP_GameWaypointFlag_NonInterruptible) == 0) && (aggro || 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 - if (building_response) { - FP_GameEntity *building = waypoint_entity; - if (FP_Game_IsNilEntityHandle(game, building->building_patron)) { - building->building_patron = entity->handle; - - 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 + if (can_attack) { + switch (entity->type) { + case FP_EntityType_Terry: /*FALLTHRU*/ + case FP_EntityType_Smoochie: /*FALLTHRU*/ + case FP_EntityType_Catfish: /*FALLTHRU*/ + case FP_EntityType_Clinger: { + // TODO(doyle): We should check if it's valid to enter this new state + // from the entity's current state + if (entity->type == FP_EntityType_Terry) { + FP_Game_EntityTransitionState(game, entity, FP_EntityTerryState_Attack); + } else if (entity->type == FP_EntityType_Smoochie) { + FP_Game_EntityTransitionState(game, entity, FP_EntitySmoochieState_Attack); + } else if (entity->type == FP_EntityType_Catfish) { + FP_Game_EntityTransitionState(game, entity, FP_EntityCatfishState_Attack); } else { - DQN_ASSERT(building->type == FP_EntityType_ChurchTerry); - FP_Game_EntityTransitionState(game, building, FP_EntityChurchTerryState_ConvertPatron); + DQN_ASSERT(entity->type == FP_EntityType_Clinger); + FP_Game_EntityTransitionState(game, entity, FP_EntityClingerState_Attack); } + entity->direction = approach_dir; + } break; - 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); - } + case FP_EntityType_Nil: break; + case FP_EntityType_ClubTerry: break; + case FP_EntityType_Map: break; + case FP_EntityType_Heart: break; + case FP_EntityType_MerchantTerry: break; + case FP_EntityType_MerchantGraveyard: break; + case FP_EntityType_MerchantGym: break; + case FP_EntityType_MerchantPhoneCompany: break; + case FP_EntityType_AirportTerry: break; + case FP_EntityType_ChurchTerry: break; + case FP_EntityType_KennelTerry: break; + case FP_EntityType_PhoneMessageProjectile: break; + case FP_EntityType_Count: DQN_INVALID_CODE_PATH; break; } - - if (can_attack) { - Dqn_V2 attack_dir_vectors[FP_GameDirection_Count] = {}; - attack_dir_vectors[FP_GameDirection_Up] = Dqn_V2_InitNx2(+0, -1); - attack_dir_vectors[FP_GameDirection_Down] = Dqn_V2_InitNx2(+0, +1); - attack_dir_vectors[FP_GameDirection_Left] = Dqn_V2_InitNx2(-1, +0); - attack_dir_vectors[FP_GameDirection_Right] = Dqn_V2_InitNx2(+1, +0); - - Dqn_V2 defender_entity_pos = FP_Game_CalcEntityWorldPos(game, waypoint_entity->handle); - Dqn_V2 entity_to_defender = defender_entity_pos - entity_pos; - Dqn_V2 entity_to_defender_norm = Dqn_V2_Normalise(entity_to_defender); - - FP_GameDirection best_attack_dir = FP_GameDirection_Up; - Dqn_f32 best_attack_dir_scalar_projection_onto_entity_to_waypoint_vector = -1.1f; - DQN_FOR_UINDEX (dir_index, FP_GameDirection_Count) { - Dqn_V2 attack_dir = attack_dir_vectors[dir_index]; - Dqn_f32 scalar_projection = Dqn_V2_Dot(attack_dir, entity_to_defender_norm); - if (scalar_projection > best_attack_dir_scalar_projection_onto_entity_to_waypoint_vector) { - best_attack_dir = DQN_CAST(FP_GameDirection)dir_index; - best_attack_dir_scalar_projection_onto_entity_to_waypoint_vector = scalar_projection; - } - } - - switch (entity->type) { - case FP_EntityType_Terry: /*FALLTHRU*/ - case FP_EntityType_Smoochie: /*FALLTHRU*/ - case FP_EntityType_Catfish: /*FALLTHRU*/ - case FP_EntityType_Clinger: { - // TODO(doyle): We should check if it's valid to enter this new state - // from the entity's current state - if (entity->type == FP_EntityType_Terry) { - FP_Game_EntityTransitionState(game, entity, FP_EntityTerryState_Attack); - } else if (entity->type == FP_EntityType_Smoochie) { - FP_Game_EntityTransitionState(game, entity, FP_EntitySmoochieState_Attack); - } else if (entity->type == FP_EntityType_Catfish) { - FP_Game_EntityTransitionState(game, entity, FP_EntityCatfishState_Attack); - } else { - DQN_ASSERT(entity->type == FP_EntityType_Clinger); - FP_Game_EntityTransitionState(game, entity, FP_EntityClingerState_Attack); - } - entity->direction = best_attack_dir; - } break; - - case FP_EntityType_Nil: break; - case FP_EntityType_ClubTerry: break; - case FP_EntityType_Map: break; - case FP_EntityType_Heart: break; - case FP_EntityType_MerchantTerry: break; - case FP_EntityType_MerchantGraveyard: break; - case FP_EntityType_MerchantGym: break; - case FP_EntityType_MerchantPhoneCompany: break; - case FP_EntityType_AirportTerry: break; - case FP_EntityType_ChurchTerry: break; - case FP_EntityType_KennelTerry: break; - case FP_EntityType_PhoneMessageProjectile: break; - case FP_EntityType_Count: DQN_INVALID_CODE_PATH; break; - } - } - - // NOTE: Aggro makes the entity attack, we will exit here preserving the waypoint in the entity. - break; - } else { - FP_SentinelList_Erase(&entity->waypoints, waypoint_link, game->play.chunk_pool); } + + // NOTE: Aggro makes the entity attack, we will exit here preserving the waypoint in the entity. + break; + } else { + FP_SentinelList_Erase(&entity->waypoints, waypoint_link, game->play.chunk_pool); } } @@ -1955,10 +1937,14 @@ void FP_Update(TELY_Platform *platform, FP_Game *game, TELY_PlatformInput *input entity->terry_mobile_data_plan_cap); // NOTE: Recover hp & stamina - entity->stamina = DQN_MIN(entity->stamina + 0.25f, entity->stamina_cap); + if (game->play.update_counter % 4 == 0) { + entity->stamina = DQN_MIN(entity->stamina + 1, entity->stamina_cap); + } - if (game->play.update_counter % 12 == 0) { - entity->hp = DQN_MIN(entity->hp + 0.75f, entity->hp_cap); + if (entity->flags & FP_GameEntityFlag_RecoversHP) { + if (game->play.update_counter % entity->hp_recover_every_n_ticks == 0) { + entity->hp = DQN_MIN(entity->hp + 1, entity->hp_cap); + } } // NOTE: Derive dynmamic bounding boxes ==================================================== @@ -2112,6 +2098,15 @@ void FP_Update(TELY_Platform *platform, FP_Game *game, TELY_PlatformInput *input game->play.state = FP_GameState_WinGame; } + { + FP_GameEntity *heart = FP_Game_GetEntity(game, game->play.heart); + FP_GameEntity *player = FP_Game_GetEntity(game, game->play.player); + if (heart->hp <= 0) { + player->hp = 0; + game->play.state = FP_GameState_LoseGame; + } + } + if (!FP_Game_IsNilEntityHandle(game, game->play.clicked_entity)) { TELY_AssetSpriteAnimation *sprite_anim = TELY_Asset_GetSpriteAnimation(&game->atlas_sprite_sheet, g_anim_names.map); Dqn_Rect sprite_rect = game->atlas_sprite_sheet.rects.data[sprite_anim->index]; @@ -2313,7 +2308,7 @@ void FP_Render(FP_Game *game, TELY_Platform *platform, TELY_Renderer *renderer, TELY_COLOUR_WHITE_V4); } - if (entity->handle != game->play.player && entity->hp != entity->hp_cap && entity->hp) { + if (entity->handle != game->play.player && entity->handle != game->play.heart && entity->hp != entity->hp_cap && entity->hp) { Dqn_f32 bar_height = 12.f; TELY_AssetSpriteAnimation *anim = TELY_Asset_GetSpriteAnimation(&game->atlas_sprite_sheet, g_anim_names.icon_health); Dqn_Rect icon_tex_rect = game->atlas_sprite_sheet.rects.data[anim->index]; @@ -2432,7 +2427,7 @@ void FP_Render(FP_Game *game, TELY_Platform *platform, TELY_Renderer *renderer, } } - if (game->play.state == FP_GameState_IntroScreen || game->play.state == FP_GameState_WinGame) { + if (game->play.state == FP_GameState_IntroScreen || game->play.state == FP_GameState_WinGame || game->play.state == FP_GameState_LoseGame) { TELY_Render_PushTransform(renderer, Dqn_M2x3_Identity()); DQN_DEFER { TELY_Render_PopTransform(renderer); }; TELY_Render_RectColourV4( @@ -2459,6 +2454,20 @@ void FP_Render(FP_Game *game, TELY_Platform *platform, TELY_Renderer *renderer, if (TELY_Platform_InputScanCodeIsPressed(input, TELY_PlatformInputScanCode_Return)) game->play.state = FP_GameState_Play; + } else if (game->play.state == FP_GameState_LoseGame) { + TELY_Render_PushFont(renderer, game->inter_regular_font_large); + TELY_Render_TextF(renderer, draw_p, Dqn_V2_Zero, "Terry's heart has been crushed"); draw_p.y += TELY_Render_FontHeight(renderer, assets); + TELY_Render_TextF(renderer, draw_p, Dqn_V2_Zero, "from the hoard of monsters"); draw_p.y += TELY_Render_FontHeight(renderer, assets); + TELY_Render_PopFont(renderer); + + TELY_Render_PushFont(renderer, game->inter_regular_font); + TELY_Render_TextF(renderer, draw_p, Dqn_V2_Zero, "Sayounara amigo"); draw_p.y += TELY_Render_FontHeight(renderer, assets); + TELY_Render_TextF(renderer, draw_p, Dqn_V2_Zero, "Press enter to restart"); draw_p.y += TELY_Render_FontHeight(renderer, assets); + TELY_Render_PopFont(renderer); + TELY_Render_PopColourV4(renderer); + + if (TELY_Platform_InputScanCodeIsPressed(input, TELY_PlatformInputScanCode_Return)) + FP_PlayReset(game, platform); } else { TELY_Render_PushFont(renderer, game->inter_regular_font_large); TELY_Render_TextF(renderer, draw_p, Dqn_V2_Zero, "Terry has been saved"); draw_p.y += TELY_Render_FontHeight(renderer, assets); @@ -3049,6 +3058,53 @@ void FP_Render(FP_Game *game, TELY_Platform *platform, TELY_Renderer *renderer, } } + // NOTE: Render the heart health + { + TELY_Render_PushTransform(renderer, Dqn_M2x3_Identity()); + DQN_DEFER { TELY_Render_PopTransform(renderer); }; + + Dqn_f32 font_height = TELY_Render_FontHeight(renderer, assets); + Dqn_f32 bar_height = font_height * 1.25f; + { + TELY_AssetSpriteAnimation *anim = TELY_Asset_GetSpriteAnimation(&game->atlas_sprite_sheet, g_anim_names.heart); + FP_GameEntity *heart = FP_Game_GetEntity(game, game->play.heart); + + Dqn_f32 max_width = platform->core.window_size.x * .5f; + + Dqn_V2 draw_p = Dqn_V2_InitNx2(platform->core.window_size.x * .25f, player_avatar_rect.pos.y + bar_height); + Dqn_f32 health_t = heart->hp / DQN_CAST(Dqn_f32)heart->hp_cap; + Dqn_Rect health_rect = Dqn_Rect_InitNx4(draw_p.x, draw_p.y, max_width, bar_height); + Dqn_Rect curr_health_rect = Dqn_Rect_InitNx4(draw_p.x, draw_p.y, max_width * health_t, bar_height); + + TELY_Render_RectColourV4(renderer, curr_health_rect, TELY_RenderShapeMode_Fill, TELY_COLOUR_RED_TOMATO_V4); + + TELY_RenderCommandRect *cmd = TELY_Render_RectColourV4( + renderer, + health_rect, + TELY_RenderShapeMode_Line, + TELY_COLOUR_BLACK_V4); + cmd->thickness = 4.f; + + // NOTE: Draw the heart icon + Dqn_Rect icon_tex_rect = game->atlas_sprite_sheet.rects.data[anim->index]; + Dqn_Rect heart_icon_rect = {}; + heart_icon_rect.size = icon_tex_rect.size * .4f; + heart_icon_rect.pos = Dqn_V2_InitNx2(draw_p.x - (heart_icon_rect.size.w * .7f), draw_p.y - (heart_icon_rect.size.y * .4f)); + + TELY_Render_TextureColourV4(renderer, + game->atlas_sprite_sheet.tex_handle, + icon_tex_rect, + heart_icon_rect, + Dqn_V2_Zero /*rotate origin*/, + 0.f /*rotation*/, + TELY_COLOUR_WHITE_V4); + + TELY_Render_PushFont(renderer, game->talkco_font_large); + DQN_DEFER { TELY_Render_PopFont(renderer); }; + TELY_Render_TextF(renderer, Dqn_Rect_InterpolatedPoint(heart_icon_rect, Dqn_V2_InitNx2(1.f, 0.65f)), Dqn_V2_Zero, "Terry's Heart"); + } + } + // NOTE: Add scanlines into the game for A E S T H E T I C S Dqn_V2 screen_size = Dqn_V2_InitNx2(platform->core.window_size.w, platform->core.window_size.h); Dqn_f32 scanline_gap = 4.0f; diff --git a/feely_pona_entity_create.cpp b/feely_pona_entity_create.cpp index 5b2328c..0ae89b5 100644 --- a/feely_pona_entity_create.cpp +++ b/feely_pona_entity_create.cpp @@ -637,7 +637,13 @@ static FP_GameEntityHandle FP_Entity_CreateHeart(FP_Game *game, Dqn_V2 pos, DQN_ FP_Game_EntityActionReset(game, result, duration_ms, render_data.sprite); FP_Entity_AddDebugEditorFlags(game, result); - entity->flags |= FP_GameEntityFlag_NonTraversable; + entity->flags |= FP_GameEntityFlag_NonTraversable; + entity->flags |= FP_GameEntityFlag_Attackable; + entity->flags |= FP_GameEntityFlag_RecoversHP; + entity->hp_cap = FP_DEFAULT_DAMAGE * 16; + entity->hp = entity->hp_cap; + entity->faction = FP_GameEntityFaction_Friendly; + entity->hp_recover_every_n_ticks *= 4; TELY_AssetSpriteAnimation *sprite_anim = TELY_Asset_GetSpriteAnimation(&game->atlas_sprite_sheet, g_anim_names.heart); Dqn_Rect sprite_rect = game->atlas_sprite_sheet.rects.data[sprite_anim->index]; diff --git a/feely_pona_game.cpp b/feely_pona_game.cpp index 9337419..a5d2036 100644 --- a/feely_pona_game.cpp +++ b/feely_pona_game.cpp @@ -234,6 +234,7 @@ static FP_GameEntity *FP_Game_MakeEntityPointerFV(FP_Game *game, DQN_FMT_STRING_ result->inventory.kennels_base_price = 50; result->inventory.clubs_base_price = 50; result->base_attack = FP_DEFAULT_DAMAGE; + result->hp_recover_every_n_ticks = 12; result->inventory.stamina_base_price = 10; result->inventory.health_base_price = 10; @@ -742,6 +743,7 @@ static Dqn_V2 FP_Game_CalcWaypointWorldPos(FP_Game *game, FP_GameEntityHandle sr } } +#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); @@ -750,6 +752,7 @@ static Dqn_V2 FP_Game_CalcWaypointWorldPos(FP_Game *game, FP_GameEntityHandle sr // we assume we're at the entity already. result = FP_Game_CalcEntityWorldPos(game, src_entity); } +#endif } } break; diff --git a/feely_pona_game.h b/feely_pona_game.h index b395b50..efe1fd3 100644 --- a/feely_pona_game.h +++ b/feely_pona_game.h @@ -17,14 +17,13 @@ enum FP_GameEntityFlag FP_GameEntityFlag_Attackable = 1 << 9, FP_GameEntityFlag_RespondsToBuildings = 1 << 10, FP_GameEntityFlag_OccupiedInBuilding = 1 << 11, - FP_GameEntityFlag_PointOfInterestHeart = 1 << 12, - FP_GameEntityFlag_CameraTracking = 1 << 13, - FP_GameEntityFlag_BuildZone = 1 << 14, - FP_GameEntityFlag_TTL = 1 << 15, - FP_GameEntityFlag_Friendly = 1 << 16, - FP_GameEntityFlag_Foe = 1 << 17, - FP_GameEntityFlag_NoClip = 1 << 18, - FP_GameEntityFlag_RecoversHP = 1 << 19, + FP_GameEntityFlag_CameraTracking = 1 << 12, + FP_GameEntityFlag_BuildZone = 1 << 13, + FP_GameEntityFlag_TTL = 1 << 14, + FP_GameEntityFlag_Friendly = 1 << 15, + FP_GameEntityFlag_Foe = 1 << 16, + FP_GameEntityFlag_NoClip = 1 << 17, + FP_GameEntityFlag_RecoversHP = 1 << 18, }; enum FP_GameShapeType @@ -228,6 +227,7 @@ struct FP_GameEntity uint32_t hp_cap; uint32_t stamina; uint32_t stamina_cap; + uint32_t hp_recover_every_n_ticks; uint32_t base_attack; bool converted_faction; @@ -287,6 +287,7 @@ enum FP_GameState FP_GameState_IntroScreen, FP_GameState_Play, FP_GameState_WinGame, + FP_GameState_LoseGame, }; struct FP_GamePlay @@ -306,6 +307,7 @@ struct FP_GamePlay FP_GameRenderSprite player_merchant_menu; uint64_t player_trigger_purchase_upgrade_timestamp; uint64_t player_trigger_purchase_building_timestamp; + FP_GameEntityHandle heart; FP_GameEntityHandle merchant_terry; FP_GameEntityHandle merchant_graveyard;