2 Commits

Author SHA1 Message Date
doylet 25736461fb fp: Add game over sequence 2023-10-08 19:05:56 +11:00
Jojangles a651bc7e14 Rebase changes 2023-10-08 19:00:13 +11:00
15 changed files with 406 additions and 303 deletions
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+1 -1
+358 -282
View File
@@ -187,6 +187,12 @@ static void FP_Game_MoveEntity(FP_Game *game, FP_GameEntityHandle entity_handle,
#else #else
entity_collides_with_collider = false; entity_collides_with_collider = false;
#endif #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; } break;
@@ -453,19 +459,20 @@ static void FP_PlayReset(FP_Game *game, TELY_Platform *platform)
play->merchant_phone_company = FP_Entity_CreateMerchantPhoneCompany(game, base_top_right, "PhoneCompany"); play->merchant_phone_company = FP_Entity_CreateMerchantPhoneCompany(game, base_top_right, "PhoneCompany");
} }
#if 0
FP_Entity_CreateClubTerry(game, Dqn_V2_InitNx2(+500, -191), "Club Terry"); FP_Entity_CreateClubTerry(game, Dqn_V2_InitNx2(+500, -191), "Club Terry");
FP_Entity_CreateKennelTerry(game, Dqn_V2_InitNx2(-300, -191), "Kennel Terry"); FP_Entity_CreateKennelTerry(game, Dqn_V2_InitNx2(-300, -191), "Kennel Terry");
FP_Entity_CreateChurchTerry(game, Dqn_V2_InitNx2(-800, -191), "Church Terry"); FP_Entity_CreateChurchTerry(game, Dqn_V2_InitNx2(-800, -191), "Church Terry");
FP_Entity_CreateAirportTerry(game, Dqn_V2_InitNx2(-1200, -191), "Airport Terry"); FP_Entity_CreateAirportTerry(game, Dqn_V2_InitNx2(-1200, -191), "Airport Terry");
#endif
play->tile_size = 37; play->tile_size = 37;
Dqn_V2I max_tile = platform->core.window_size / play->tile_size; Dqn_V2I max_tile = platform->core.window_size / play->tile_size;
// NOTE: Heart // NOTE: Heart
FP_Entity_CreateHeart(game, base_mid_p, "Heart"); 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.world_pos = base_mid_p - Dqn_V2_InitV2I(platform->core.window_size * .5f); play->camera.scale = Dqn_V2_InitNx1(1);
play->camera.scale = Dqn_V2_InitNx1(1);
} }
extern "C" __declspec(dllexport) extern "C" __declspec(dllexport)
@@ -501,6 +508,13 @@ void TELY_DLL_Init(void *user_data)
game->audio[FP_GameAudio_Woosh] = platform->func_load_audio(assets, DQN_STRING8("Woosh"), DQN_STRING8("Data/Audio/woosh.ogg")); game->audio[FP_GameAudio_Woosh] = platform->func_load_audio(assets, DQN_STRING8("Woosh"), DQN_STRING8("Data/Audio/woosh.ogg"));
game->audio[FP_GameAudio_Ching] = platform->func_load_audio(assets, DQN_STRING8("Ching"), DQN_STRING8("Data/Audio/ching.ogg")); game->audio[FP_GameAudio_Ching] = platform->func_load_audio(assets, DQN_STRING8("Ching"), DQN_STRING8("Data/Audio/ching.ogg"));
game->audio[FP_GameAudio_Church] = platform->func_load_audio(assets, DQN_STRING8("Church"), DQN_STRING8("Data/Audio/church.ogg")); game->audio[FP_GameAudio_Church] = platform->func_load_audio(assets, DQN_STRING8("Church"), DQN_STRING8("Data/Audio/church.ogg"));
game->audio[FP_GameAudio_Plane] = platform->func_load_audio(assets, DQN_STRING8("Plane"), DQN_STRING8("Data/Audio/airport.ogg"));
game->audio[FP_GameAudio_Club] = platform->func_load_audio(assets, DQN_STRING8("Club"), DQN_STRING8("Data/Audio/club_terry.ogg"));
game->audio[FP_GameAudio_Dog] = platform->func_load_audio(assets, DQN_STRING8("Dog"), DQN_STRING8("Data/Audio/dog.ogg"));
game->audio[FP_GameAudio_MerchantTerry] = platform->func_load_audio(assets, DQN_STRING8("Door"), DQN_STRING8("Data/Audio/merchant_terry.ogg"));
game->audio[FP_GameAudio_MerchantGhost] = platform->func_load_audio(assets, DQN_STRING8("Ghost"), DQN_STRING8("Data/Audio/merchant_ghost.ogg"));
game->audio[FP_GameAudio_MerchantGym] = platform->func_load_audio(assets, DQN_STRING8("Gym"), DQN_STRING8("Data/Audio/merchant_gym.ogg"));
game->audio[FP_GameAudio_MerchantPhone] = platform->func_load_audio(assets, DQN_STRING8("Phone"), DQN_STRING8("Data/Audio/merchant_tech.ogg"));
platform->user_data = game; platform->user_data = game;
{ {
@@ -1136,7 +1150,7 @@ void FP_EntityActionStateMachine(FP_Game *game, TELY_Audio *audio, TELY_Platform
if (entity->waypoints.size == 0) { if (entity->waypoints.size == 0) {
FP_GameEntity *patron = FP_Game_GetEntity(game, entity->building_patron); FP_GameEntity *patron = FP_Game_GetEntity(game, entity->building_patron);
patron->local_pos = entity->local_pos; 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); FP_Game_DeleteEntity(game, entity->handle);
return; return;
} }
@@ -1232,7 +1246,7 @@ void FP_EntityActionStateMachine(FP_Game *game, TELY_Audio *audio, TELY_Platform
if (action_has_finished) { if (action_has_finished) {
if (!FP_Game_IsNilEntityHandle(game, entity->building_patron)) { if (!FP_Game_IsNilEntityHandle(game, entity->building_patron)) {
FP_GameEntity *patron = FP_Game_GetEntity(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->faction = FP_GameEntityFaction_Friendly;
patron->converted_faction = true; patron->converted_faction = true;
} }
@@ -1508,302 +1522,279 @@ void FP_Update(TELY_Platform *platform, FP_Game *game, TELY_PlatformInput *input
// NOTE: Determine AI movement ============================================================= // NOTE: Determine AI movement =============================================================
Dqn_V2 entity_pos = FP_Game_CalcEntityWorldPos(game, entity->handle); 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) {
if (entity->flags & FP_GameEntityFlag_Aggros && entity->faction != FP_GameEntityFaction_Nil) { FP_GameFindClosestEntityResult closest_defender = {};
FP_GameFindClosestEntityResult closest_defender = {}; closest_defender.dist_squared = DQN_F32_MAX;
closest_defender.dist_squared = DQN_F32_MAX; closest_defender.pos = Dqn_V2_InitNx1(DQN_F32_MAX);
closest_defender.pos = Dqn_V2_InitNx1(DQN_F32_MAX);
FP_GameEntityFaction enemy_faction = FP_GameEntityFaction enemy_faction =
entity->faction == FP_GameEntityFaction_Friendly entity->faction == FP_GameEntityFaction_Friendly
? FP_GameEntityFaction_Foe ? FP_GameEntityFaction_Foe
: FP_GameEntityFaction_Friendly; : FP_GameEntityFaction_Friendly;
for (FP_GameEntityIterator defender_it = {}; FP_Game_DFSPostOrderWalkEntityTree(game, &defender_it, game->play.root_entity); ) { for (FP_GameEntityIterator defender_it = {}; FP_Game_DFSPostOrderWalkEntityTree(game, &defender_it, game->play.root_entity); ) {
FP_GameEntity *it_entity = defender_it.entity; FP_GameEntity *it_entity = defender_it.entity;
Dqn_V2 pos = FP_Game_CalcEntityWorldPos(game, it_entity->handle); Dqn_V2 pos = FP_Game_CalcEntityWorldPos(game, it_entity->handle);
Dqn_f32 dist = Dqn_V2_LengthSq_V2x2(pos, entity_pos); Dqn_f32 dist = Dqn_V2_LengthSq_V2x2(pos, entity_pos);
if (it_entity->faction != enemy_faction) if (it_entity->faction != enemy_faction)
continue; continue;
if (dist < closest_defender.dist_squared) { if (dist < closest_defender.dist_squared) {
closest_defender.pos = pos; closest_defender.pos = pos;
closest_defender.dist_squared = dist; closest_defender.dist_squared = dist;
closest_defender.entity = it_entity->handle; 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 (entity->flags & FP_GameEntityFlag_RespondsToBuildings) { Dqn_f32 aggro_dist_threshold = FP_Game_MetersToPixelsNx1(game->play, 4.f);
FP_GameFindClosestEntityResult closest_building = {}; Dqn_f32 dist_to_defender = DQN_SQRTF(closest_defender.dist_squared);
closest_building.dist_squared = DQN_F32_MAX; if (dist_to_defender > (aggro_dist_threshold * 1.5f)) {
closest_building.pos = Dqn_V2_InitNx1(DQN_F32_MAX); 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);
for (FP_GameEntityIterator building_it = {}; FP_Game_DFSPostOrderWalkEntityTree(game, &building_it, game->play.root_entity); ) { if (maybe_terry->type == FP_EntityType_Terry) {
FP_GameEntity *it_entity = building_it.entity; link = FP_SentinelList_Erase(&entity->waypoints, link, game->play.chunk_pool);
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;
} }
} }
} 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) && if (!has_waypoint_to_defender) {
closest_building.dist_squared < DQN_SQUARED(FP_Game_MetersToPixelsNx1(game->play, 5.f))) { FP_GameEntity *defender = FP_Game_GetEntity(game, closest_defender.entity);
FP_SentinelListLink<FP_GameWaypoint> *link = FP_SentinelList_MakeBefore(&entity->waypoints,
bool has_waypoint_to_building = false; FP_SentinelList_Front(&entity->waypoints),
for (FP_SentinelListLink<FP_GameWaypoint> *link = nullptr; game->play.chunk_pool);
!has_waypoint_to_building && FP_SentinelList_Iterate<FP_GameWaypoint>(&entity->waypoints, &link); ) { FP_GameWaypoint *waypoint = &link->data;
has_waypoint_to_building = link->data.entity == closest_building.entity; waypoint->entity = defender->handle;
} waypoint->type = FP_GameWaypointType_ClosestSide;
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 (entity->flags & FP_GameEntityFlag_PointOfInterestHeart) { if (entity->flags & FP_GameEntityFlag_RespondsToBuildings) {
FP_GameFindClosestEntityResult closest_heart = FP_Game_FindClosestEntityWithType(game, entity->handle, FP_EntityType_Heart); FP_GameFindClosestEntityResult closest_building = {};
if (closest_heart.dist_squared < DQN_SQUARED(FP_Game_MetersToPixelsNx1(game->play, 4.f))) { closest_building.dist_squared = DQN_F32_MAX;
closest_building.pos = Dqn_V2_InitNx1(DQN_F32_MAX);
bool has_waypoint_to = false; for (FP_GameEntityIterator building_it = {}; FP_Game_DFSPostOrderWalkEntityTree(game, &building_it, game->play.root_entity); ) {
for (FP_SentinelListLink<FP_GameWaypoint> *link = nullptr; FP_GameEntity *it_entity = building_it.entity;
!has_waypoint_to && FP_SentinelList_Iterate<FP_GameWaypoint>(&entity->waypoints, &link); ) { if (it_entity->type != FP_EntityType_ClubTerry &&
has_waypoint_to = link->data.entity == closest_heart.entity; it_entity->type != FP_EntityType_AirportTerry &&
} it_entity->type != FP_EntityType_ChurchTerry)
continue;
if (!has_waypoint_to) { // NOTE: Already converted, we cannot attend church again
Dqn_Rect club_hit_box = FP_Game_CalcEntityWorldHitBox(game, closest_heart.entity); if (entity->converted_faction && it_entity->type == FP_EntityType_ChurchTerry) {
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);
continue; continue;
} }
// NOTE: We found a waypoint that is valid to move towards bool already_visited_building = false;
Dqn_V2 target_pos = FP_Game_CalcWaypointWorldPos(game, entity->handle, waypoint); for (FP_SentinelListLink<FP_GameEntityHandle> *link_it = {};
Dqn_V2 entity_to_waypoint = target_pos - entity_pos; !already_visited_building && FP_SentinelList_Iterate(&entity->buildings_visited, &link_it);
) {
// NOTE: Check if we've arrived at the waypoint FP_GameEntityHandle visit_item = link_it->data;
Dqn_f32 dist_to_waypoint_sq = Dqn_V2_LengthSq(entity_to_waypoint); already_visited_building = visit_item == it_entity->handle;
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;
} }
// NOTE: We haven't arrived yet, calculate an acceleration vector to the waypoint if (already_visited_building)
if (dist_to_waypoint_sq > DQN_SQUARED(arrival_threshold)) { continue;
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); Dqn_V2 pos = FP_Game_CalcEntityWorldPos(game, it_entity->handle);
break; 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 if (!has_waypoint_to_building) {
bool aggro = false; Dqn_Rect hit_box = FP_Game_CalcEntityWorldHitBox(game, closest_building.entity);
if (entity->flags & FP_GameEntityFlag_Aggros) { Dqn_V2 top_left = Dqn_Rect_TopLeft(hit_box);
aggro |= entity->faction == FP_GameEntityFaction_Friendly && waypoint_entity->faction & FP_GameEntityFaction_Foe; Dqn_V2 top_right = Dqn_Rect_TopRight(hit_box);
aggro |= entity->faction == FP_GameEntityFaction_Foe && waypoint_entity->faction & FP_GameEntityFaction_Friendly;
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) && if (can_attack) {
waypoint_entity->type == FP_EntityType_ClubTerry || switch (entity->type) {
waypoint_entity->type == FP_EntityType_AirportTerry || case FP_EntityType_Terry: /*FALLTHRU*/
waypoint_entity->type == FP_EntityType_ChurchTerry; case FP_EntityType_Smoochie: /*FALLTHRU*/
bool heart_response = (entity->flags & FP_GameEntityFlag_PointOfInterestHeart) && waypoint_entity->type == FP_EntityType_Heart; case FP_EntityType_Catfish: /*FALLTHRU*/
case FP_EntityType_Clinger: {
if (((waypoint->flags & FP_GameWaypointFlag_NonInterruptible) == 0) && (aggro || building_response || heart_response)) { // TODO(doyle): We should check if it's valid to enter this new state
bool can_attack = !entity->is_dying; // TODO(doyle): State transition needs to check if it's valid to move to making this not necessary // from the entity's current state
if (building_response) { if (entity->type == FP_EntityType_Terry) {
FP_GameEntity *building = waypoint_entity; FP_Game_EntityTransitionState(game, entity, FP_EntityTerryState_Attack);
if (FP_Game_IsNilEntityHandle(game, building->building_patron)) { } else if (entity->type == FP_EntityType_Smoochie) {
building->building_patron = entity->handle; FP_Game_EntityTransitionState(game, entity, FP_EntitySmoochieState_Attack);
} else if (entity->type == FP_EntityType_Catfish) {
Dqn_Rect hit_box = FP_Game_CalcEntityWorldHitBox(game, building->handle); FP_Game_EntityTransitionState(game, entity, FP_EntityCatfishState_Attack);
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 { } else {
DQN_ASSERT(building->type == FP_EntityType_ChurchTerry); DQN_ASSERT(entity->type == FP_EntityType_Clinger);
FP_Game_EntityTransitionState(game, building, FP_EntityChurchTerryState_ConvertPatron); FP_Game_EntityTransitionState(game, entity, FP_EntityClingerState_Attack);
} }
entity->direction = approach_dir;
} break;
entity->flags |= FP_GameEntityFlag_OccupiedInBuilding; case FP_EntityType_Nil: break;
can_attack = false; case FP_EntityType_ClubTerry: break;
FP_SentinelList_Erase(&entity->waypoints, waypoint_link, game->play.chunk_pool); case FP_EntityType_Map: break;
FP_SentinelList_Add(&entity->buildings_visited, game->play.chunk_pool, building->handle); 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);
} }
} }
@@ -1873,14 +1864,17 @@ void FP_Update(TELY_Platform *platform, FP_Game *game, TELY_PlatformInput *input
TELY_Platform_InputScanCodeIsPressed(input, TELY_PlatformInputScanCode_J)) { TELY_Platform_InputScanCodeIsPressed(input, TELY_PlatformInputScanCode_J)) {
if (placeable_building.type == FP_EntityType_ClubTerry) { if (placeable_building.type == FP_EntityType_ClubTerry) {
FP_Entity_CreateClubTerry(game, placement_pos, "Club Terry"); FP_Entity_CreateClubTerry(game, placement_pos, "Club Terry");
TELY_Audio_Play(audio, game->audio[FP_GameAudio_Club], 1.f);
} else if (placeable_building.type == FP_EntityType_ChurchTerry) { } else if (placeable_building.type == FP_EntityType_ChurchTerry) {
FP_Entity_CreateChurchTerry(game, placement_pos, "Church Terry"); FP_Entity_CreateChurchTerry(game, placement_pos, "Church Terry");
TELY_Audio_Play(audio, game->audio[FP_GameAudio_Church], 1.f); TELY_Audio_Play(audio, game->audio[FP_GameAudio_Church], 1.f);
} else if (placeable_building.type == FP_EntityType_AirportTerry) { } else if (placeable_building.type == FP_EntityType_AirportTerry) {
FP_Entity_CreateAirportTerry(game, placement_pos, "Airport Terry"); FP_Entity_CreateAirportTerry(game, placement_pos, "Airport Terry");
TELY_Audio_Play(audio, game->audio[FP_GameAudio_Plane], 1.f);
} else { } else {
DQN_ASSERT(placeable_building.type == FP_EntityType_KennelTerry); DQN_ASSERT(placeable_building.type == FP_EntityType_KennelTerry);
FP_Entity_CreateKennelTerry(game, placement_pos, "Kennel Terry"); FP_Entity_CreateKennelTerry(game, placement_pos, "Kennel Terry");
TELY_Audio_Play(audio, game->audio[FP_GameAudio_Dog], 1.f);
} }
(*inventory_count)--; (*inventory_count)--;
@@ -1943,10 +1937,12 @@ void FP_Update(TELY_Platform *platform, FP_Game *game, TELY_PlatformInput *input
entity->terry_mobile_data_plan_cap); entity->terry_mobile_data_plan_cap);
// NOTE: Recover hp & stamina // NOTE: Recover hp & stamina
entity->stamina = DQN_MIN(entity->stamina + 1, entity->stamina_cap); if (game->play.update_counter % 4 == 0) {
entity->stamina = DQN_MIN(entity->stamina + 1, entity->stamina_cap);
}
if (entity->flags & FP_GameEntityFlag_RecoversHP) { if (entity->flags & FP_GameEntityFlag_RecoversHP) {
if (game->play.update_counter % 12 == 0) { if (game->play.update_counter % entity->hp_recover_every_n_ticks == 0) {
entity->hp = DQN_MIN(entity->hp + 1, entity->hp_cap); entity->hp = DQN_MIN(entity->hp + 1, entity->hp_cap);
} }
} }
@@ -2102,6 +2098,15 @@ void FP_Update(TELY_Platform *platform, FP_Game *game, TELY_PlatformInput *input
game->play.state = FP_GameState_WinGame; 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)) { if (!FP_Game_IsNilEntityHandle(game, game->play.clicked_entity)) {
TELY_AssetSpriteAnimation *sprite_anim = TELY_Asset_GetSpriteAnimation(&game->atlas_sprite_sheet, g_anim_names.map); 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]; Dqn_Rect sprite_rect = game->atlas_sprite_sheet.rects.data[sprite_anim->index];
@@ -2303,7 +2308,7 @@ void FP_Render(FP_Game *game, TELY_Platform *platform, TELY_Renderer *renderer,
TELY_COLOUR_WHITE_V4); 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; Dqn_f32 bar_height = 12.f;
TELY_AssetSpriteAnimation *anim = TELY_Asset_GetSpriteAnimation(&game->atlas_sprite_sheet, g_anim_names.icon_health); 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]; Dqn_Rect icon_tex_rect = game->atlas_sprite_sheet.rects.data[anim->index];
@@ -2422,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()); TELY_Render_PushTransform(renderer, Dqn_M2x3_Identity());
DQN_DEFER { TELY_Render_PopTransform(renderer); }; DQN_DEFER { TELY_Render_PopTransform(renderer); };
TELY_Render_RectColourV4( TELY_Render_RectColourV4(
@@ -2449,6 +2454,20 @@ void FP_Render(FP_Game *game, TELY_Platform *platform, TELY_Renderer *renderer,
if (TELY_Platform_InputScanCodeIsPressed(input, TELY_PlatformInputScanCode_Return)) if (TELY_Platform_InputScanCodeIsPressed(input, TELY_PlatformInputScanCode_Return))
game->play.state = FP_GameState_Play; 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 { } else {
TELY_Render_PushFont(renderer, game->inter_regular_font_large); 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); TELY_Render_TextF(renderer, draw_p, Dqn_V2_Zero, "Terry has been saved"); draw_p.y += TELY_Render_FontHeight(renderer, assets);
@@ -2470,6 +2489,7 @@ void FP_Render(FP_Game *game, TELY_Platform *platform, TELY_Renderer *renderer,
FP_GameEntity *player = FP_Game_GetEntity(game, game->play.player); FP_GameEntity *player = FP_Game_GetEntity(game, game->play.player);
Dqn_V2 player_pos = FP_Game_CalcEntityWorldPos(game, game->play.player); Dqn_V2 player_pos = FP_Game_CalcEntityWorldPos(game, game->play.player);
{ {
static bool sound_played_flags[4] = {false, false, false, false};
FP_GameInventory *invent = &player->inventory; FP_GameInventory *invent = &player->inventory;
struct FP_MerchantToMenuMapping { struct FP_MerchantToMenuMapping {
FP_GameEntityHandle merchant; FP_GameEntityHandle merchant;
@@ -2479,11 +2499,13 @@ void FP_Render(FP_Game *game, TELY_Platform *platform, TELY_Renderer *renderer,
uint8_t *inventory_count; uint8_t *inventory_count;
uint32_t *building_base_price; uint32_t *building_base_price;
uint32_t *upgrade_base_price; uint32_t *upgrade_base_price;
FP_GameAudio audio_type;
bool *sound_played;
} merchants[] = { } merchants[] = {
{game->play.merchant_terry, g_anim_names.merchant_terry_menu, g_anim_names.club_terry_dark, Dqn_V2_InitNx2(0.015f, +0.04f), &invent->clubs, &invent->clubs_base_price, &invent->health_base_price}, {game->play.merchant_terry, g_anim_names.merchant_terry_menu, g_anim_names.club_terry_dark, Dqn_V2_InitNx2(0.015f, +0.04f), &invent->clubs, &invent->clubs_base_price, &invent->health_base_price, FP_GameAudio_MerchantTerry, &sound_played_flags[0]},
{game->play.merchant_graveyard, g_anim_names.merchant_graveyard_menu, g_anim_names.church_terry_dark, Dqn_V2_InitNx2(0.04f, -0.15f), &invent->churchs, &invent->churchs_base_price, &invent->stamina_base_price}, {game->play.merchant_graveyard, g_anim_names.merchant_graveyard_menu, g_anim_names.church_terry_dark, Dqn_V2_InitNx2(0.04f, -0.15f), &invent->churchs, &invent->churchs_base_price, &invent->stamina_base_price, FP_GameAudio_MerchantGhost, &sound_played_flags[1]},
{game->play.merchant_gym, g_anim_names.merchant_gym_menu, g_anim_names.kennel_terry, Dqn_V2_InitNx2(0, +0), &invent->kennels, &invent->kennels_base_price, &invent->attack_base_price}, {game->play.merchant_gym, g_anim_names.merchant_gym_menu, g_anim_names.kennel_terry, Dqn_V2_InitNx2(0, +0), &invent->kennels, &invent->kennels_base_price, &invent->attack_base_price, FP_GameAudio_MerchantGym, &sound_played_flags[2]},
{game->play.merchant_phone_company, g_anim_names.merchant_phone_company_menu, g_anim_names.airport_terry, Dqn_V2_InitNx2(0, -0.1f), &invent->airports, &invent->airports_base_price, &invent->mobile_plan_base_price}, {game->play.merchant_phone_company, g_anim_names.merchant_phone_company_menu, g_anim_names.airport_terry, Dqn_V2_InitNx2(0, -0.1f), &invent->airports, &invent->airports_base_price, &invent->mobile_plan_base_price, FP_GameAudio_MerchantPhone, &sound_played_flags[3]},
}; };
bool activated_merchant = false; bool activated_merchant = false;
@@ -2492,8 +2514,10 @@ void FP_Render(FP_Game *game, TELY_Platform *platform, TELY_Renderer *renderer,
Dqn_V2 world_pos = FP_Game_CalcEntityWorldPos(game, merchant_handle); Dqn_V2 world_pos = FP_Game_CalcEntityWorldPos(game, merchant_handle);
Dqn_f32 dist_squared = Dqn_V2_LengthSq_V2x2(world_pos, player_pos); Dqn_f32 dist_squared = Dqn_V2_LengthSq_V2x2(world_pos, player_pos);
if (dist_squared > DQN_SQUARED(FP_Game_MetersToPixelsNx1(game->play, 4))) if (dist_squared > DQN_SQUARED(FP_Game_MetersToPixelsNx1(game->play, 4))) {
*mapping.sound_played = false;
continue; continue;
}
// NOTE: Render animated merchant menu ============================= // NOTE: Render animated merchant menu =============================
activated_merchant = true; activated_merchant = true;
@@ -2524,6 +2548,11 @@ void FP_Render(FP_Game *game, TELY_Platform *platform, TELY_Renderer *renderer,
Dqn_V2_Zero /*rotate origin*/, Dqn_V2_Zero /*rotate origin*/,
0.f /*rotation*/, 0.f /*rotation*/,
TELY_COLOUR_WHITE_V4); TELY_COLOUR_WHITE_V4);
if (activated_merchant && !*mapping.sound_played) {
TELY_Audio_Play(audio, game->audio[mapping.audio_type], 1.f);
*mapping.sound_played = true;
}
} }
TELY_Render_PushColourV4(renderer, TELY_COLOUR_WHITE_V4); TELY_Render_PushColourV4(renderer, TELY_COLOUR_WHITE_V4);
@@ -3029,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 // 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_V2 screen_size = Dqn_V2_InitNx2(platform->core.window_size.w, platform->core.window_size.h);
Dqn_f32 scanline_gap = 4.0f; Dqn_f32 scanline_gap = 4.0f;
+7 -1
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_Game_EntityActionReset(game, result, duration_ms, render_data.sprite);
FP_Entity_AddDebugEditorFlags(game, result); 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); 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]; Dqn_Rect sprite_rect = game->atlas_sprite_sheet.rects.data[sprite_anim->index];
+3
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.kennels_base_price = 50;
result->inventory.clubs_base_price = 50; result->inventory.clubs_base_price = 50;
result->base_attack = FP_DEFAULT_DAMAGE; result->base_attack = FP_DEFAULT_DAMAGE;
result->hp_recover_every_n_ticks = 12;
result->inventory.stamina_base_price = 10; result->inventory.stamina_base_price = 10;
result->inventory.health_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 entity_pos = FP_Game_CalcEntityWorldPos(game, waypoint_entity->handle);
Dqn_V2 src_pos = FP_Game_CalcEntityWorldPos(game, src_entity); Dqn_V2 src_pos = FP_Game_CalcEntityWorldPos(game, src_entity);
Dqn_f32 curr_dist_to_entity = Dqn_V2_LengthSq_V2x2(entity_pos, src_pos); 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. // we assume we're at the entity already.
result = FP_Game_CalcEntityWorldPos(game, src_entity); result = FP_Game_CalcEntityWorldPos(game, src_entity);
} }
#endif
} }
} break; } break;
+17 -8
View File
@@ -17,14 +17,13 @@ enum FP_GameEntityFlag
FP_GameEntityFlag_Attackable = 1 << 9, FP_GameEntityFlag_Attackable = 1 << 9,
FP_GameEntityFlag_RespondsToBuildings = 1 << 10, FP_GameEntityFlag_RespondsToBuildings = 1 << 10,
FP_GameEntityFlag_OccupiedInBuilding = 1 << 11, FP_GameEntityFlag_OccupiedInBuilding = 1 << 11,
FP_GameEntityFlag_PointOfInterestHeart = 1 << 12, FP_GameEntityFlag_CameraTracking = 1 << 12,
FP_GameEntityFlag_CameraTracking = 1 << 13, FP_GameEntityFlag_BuildZone = 1 << 13,
FP_GameEntityFlag_BuildZone = 1 << 14, FP_GameEntityFlag_TTL = 1 << 14,
FP_GameEntityFlag_TTL = 1 << 15, FP_GameEntityFlag_Friendly = 1 << 15,
FP_GameEntityFlag_Friendly = 1 << 16, FP_GameEntityFlag_Foe = 1 << 16,
FP_GameEntityFlag_Foe = 1 << 17, FP_GameEntityFlag_NoClip = 1 << 17,
FP_GameEntityFlag_NoClip = 1 << 18, FP_GameEntityFlag_RecoversHP = 1 << 18,
FP_GameEntityFlag_RecoversHP = 1 << 19,
}; };
enum FP_GameShapeType enum FP_GameShapeType
@@ -228,6 +227,7 @@ struct FP_GameEntity
uint32_t hp_cap; uint32_t hp_cap;
uint32_t stamina; uint32_t stamina;
uint32_t stamina_cap; uint32_t stamina_cap;
uint32_t hp_recover_every_n_ticks;
uint32_t base_attack; uint32_t base_attack;
bool converted_faction; bool converted_faction;
@@ -265,6 +265,13 @@ enum FP_GameAudio
FP_GameAudio_Woosh, FP_GameAudio_Woosh,
FP_GameAudio_Ching, FP_GameAudio_Ching,
FP_GameAudio_Church, FP_GameAudio_Church,
FP_GameAudio_Plane,
FP_GameAudio_Club,
FP_GameAudio_Dog,
FP_GameAudio_MerchantTerry,
FP_GameAudio_MerchantGhost,
FP_GameAudio_MerchantGym,
FP_GameAudio_MerchantPhone,
FP_GameAudio_Count, FP_GameAudio_Count,
}; };
@@ -280,6 +287,7 @@ enum FP_GameState
FP_GameState_IntroScreen, FP_GameState_IntroScreen,
FP_GameState_Play, FP_GameState_Play,
FP_GameState_WinGame, FP_GameState_WinGame,
FP_GameState_LoseGame,
}; };
struct FP_GamePlay struct FP_GamePlay
@@ -299,6 +307,7 @@ struct FP_GamePlay
FP_GameRenderSprite player_merchant_menu; FP_GameRenderSprite player_merchant_menu;
uint64_t player_trigger_purchase_upgrade_timestamp; uint64_t player_trigger_purchase_upgrade_timestamp;
uint64_t player_trigger_purchase_building_timestamp; uint64_t player_trigger_purchase_building_timestamp;
FP_GameEntityHandle heart;
FP_GameEntityHandle merchant_terry; FP_GameEntityHandle merchant_terry;
FP_GameEntityHandle merchant_graveyard; FP_GameEntityHandle merchant_graveyard;