fp: Add game over sequence

This commit is contained in:
doyle 2023-10-08 19:04:11 +11:00
parent a651bc7e14
commit 25736461fb
5 changed files with 355 additions and 288 deletions

2
External/tely vendored

@ -1 +1 @@
Subproject commit 595e3c7f1e70aab8e51620682291aa2ba6c02b7f
Subproject commit 8d39ef2d8365e223114bbdc2ce2db2edaaf81163

View File

@ -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<FP_GameWaypoint> *link = nullptr;
!has_waypoint_to_defender && FP_SentinelList_Iterate<FP_GameWaypoint>(&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<FP_GameWaypoint> *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<FP_GameWaypoint> *link = nullptr; FP_SentinelList_Iterate<FP_GameWaypoint>(&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<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 (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<FP_GameWaypoint> *link = nullptr; FP_SentinelList_Iterate<FP_GameWaypoint>(&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<FP_GameWaypoint> *link = nullptr;
!has_waypoint_to_defender && FP_SentinelList_Iterate<FP_GameWaypoint>(&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<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->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<FP_GameWaypoint> *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<FP_GameWaypoint> *link = nullptr;
!has_waypoint_to && FP_SentinelList_Iterate<FP_GameWaypoint>(&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<FP_GameWaypoint> *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<FP_GameWaypoint> *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<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;
}
// 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<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;
}
// 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<FP_GameWaypoint> *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<FP_GameWaypoint> *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;

View File

@ -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];

View File

@ -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;

View File

@ -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;