diff --git a/External/tely b/External/tely index 6fefb31..c105643 160000 --- a/External/tely +++ b/External/tely @@ -1 +1 @@ -Subproject commit 6fefb31c2f4a44f974d6987fc5942857891ae429 +Subproject commit c1056434f07a1f2b12378fee6c5ed33941e2bfcb diff --git a/feely_pona.cpp b/feely_pona.cpp index f6035e2..4af6134 100644 --- a/feely_pona.cpp +++ b/feely_pona.cpp @@ -3,44 +3,7 @@ #include "feely_pona_unity.h" #endif -Dqn_f32 const PHYSICS_STEP = 1 / 60.f; - -Dqn_Rect FP_Game_GetBuildingPlacementRectForEntity(FP_Game *game, FP_GamePlaceableBuilding placeable_building, FP_GameEntityHandle handle) -{ - Dqn_Rect result = {}; - FP_GameEntity *entity = FP_Game_GetEntity(game, handle); - if (FP_Game_IsNilEntity(entity)) - return result; - - FP_EntityRenderData render_data = FP_Entity_GetRenderData(game, placeable_building.type, placeable_building.state, entity->direction); - Dqn_Rect box = FP_Game_CalcEntityWorldHitBox(game, entity->handle); - Dqn_V2 build_p = {}; - switch (entity->direction) { - case FP_GameDirection_Up: { - build_p = Dqn_Rect_InterpolatedPoint(box, Dqn_V2_InitNx2(0.5f, 0.f)) - Dqn_V2_InitNx2(0.f, render_data.render_size.h * .5f + 10.f); - } break; - - case FP_GameDirection_Down: { - build_p = Dqn_Rect_InterpolatedPoint(box, Dqn_V2_InitNx2(0.5f, 1.f)) + Dqn_V2_InitNx2(0.f, render_data.render_size.h * .5f + 10.f); - } break; - - case FP_GameDirection_Left: { - build_p = Dqn_Rect_InterpolatedPoint(box, Dqn_V2_InitNx2(0.0f, 0.5f)) - Dqn_V2_InitNx2(render_data.render_size.w * .5f + 10.f, 0); - } break; - - case FP_GameDirection_Right: { - build_p = Dqn_Rect_InterpolatedPoint(box, Dqn_V2_InitNx2(1.f, 0.5f)) + Dqn_V2_InitNx2(render_data.render_size.w * .5f + 10.f, 0); - } break; - - case FP_GameDirection_Count: DQN_INVALID_CODE_PATH; break; - } - - result.size = render_data.render_size; - result.pos = build_p - (render_data.render_size * .5f); - return result; -} - -TELY_AssetSpriteSheet FP_LoadSpriteSheetFromSpec(TELY_Platform *platform, TELY_Assets *assets, Dqn_Arena *arena, Dqn_String8 sheet_name) +static TELY_AssetSpriteSheet FP_LoadSpriteSheetFromSpec(TELY_Platform *platform, TELY_Assets *assets, Dqn_Arena *arena, Dqn_String8 sheet_name) { TELY_AssetSpriteSheet result = {}; Dqn_ThreadScratch scratch = Dqn_Thread_GetScratch(arena); @@ -126,168 +89,6 @@ TELY_AssetSpriteSheet FP_LoadSpriteSheetFromSpec(TELY_Platform *platform, TELY_A return result; } -static void FP_Game_MoveEntity(FP_Game *game, FP_GameEntityHandle entity_handle, Dqn_V2 acceleration_meters_per_s) -{ - // f"(t) = a - // f'(t) = at + v - // f (t) = 0.5f*a(t^2) + vt + p - - FP_GameEntity *entity = FP_Game_GetEntity(game, entity_handle); - if (FP_Game_IsNilEntity(entity)) - return; - - Dqn_f32 t = DQN_CAST(Dqn_f32)DQN_SQUARED(PHYSICS_STEP); - Dqn_f32 t_squared = DQN_SQUARED(t); - - Dqn_f32 velocity_falloff_coefficient = 0.82f; - Dqn_f32 acceleration_feel_good_factor = 15'000.f; - Dqn_V2 acceleration = FP_Game_MetersToPixelsV2(game->play, acceleration_meters_per_s) * acceleration_feel_good_factor; - entity->velocity = (acceleration * t) + entity->velocity * velocity_falloff_coefficient; - - // NOTE: Zero out velocity with epsilon - if (DQN_ABS(entity->velocity.x) < 5.f) - entity->velocity.x = 0.f; - if (DQN_ABS(entity->velocity.y) < 5.f) - entity->velocity.y = 0.f; - - Dqn_V2 delta_pos = (acceleration * 0.5f * t_squared) + (entity->velocity * t); - Dqn_Rect entity_world_hit_box = FP_Game_CalcEntityWorldHitBox(game, entity->handle); - Dqn_V2 entity_pos = FP_Game_CalcEntityWorldPos(game, entity->handle); - Dqn_V2 entity_new_pos = entity_pos + delta_pos; - - Dqn_f32 const SENTINEL_T = 999.f; - Dqn_f32 global_earliest_t = SENTINEL_T; - Dqn_V2 global_earliest_pos_just_before_collide = {}; - - if ((entity->flags & FP_GameEntityFlag_NoClip) == 0) { - for (FP_GameEntityIterator collider_it = {}; FP_Game_DFSPostOrderWalkEntityTree(game, &collider_it, game->play.root_entity);) { - FP_GameEntity *collider = collider_it.entity; - if (collider->handle == entity->handle) - continue; - - // TODO(doyle): Calculate the list of collidables at the start of the frame - if ((collider->flags & FP_GameEntityFlag_NonTraversable) == 0) - continue; - - bool entity_collides_with_collider = true; - switch (entity->type) { - case FP_EntityType_Catfish: /*FALLTHRU*/ - case FP_EntityType_Smoochie: /*FALLTHRU*/ - case FP_EntityType_Clinger: { - if (collider->type == FP_EntityType_Smoochie || collider->type == FP_EntityType_Clinger || collider->type == FP_EntityType_Catfish) { - entity_collides_with_collider = false; - } else if (FP_Entity_IsBuildingForMobs(collider)) { - #if 0 - // NOTE: We disable collision on buildings we have visited to avoid some - // problems ... - if (FP_SentinelList_Find(&entity->buildings_visited, collider->handle)) { - entity_collides_with_collider = false; - } - #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; - - case FP_EntityType_Terry: { - // NOTE: Don't collide with mobs when dashing (e.g. phase through) - FP_EntityTerryState state = *DQN_CAST(FP_EntityTerryState *)&entity->action.state; - if (state == FP_EntityTerryState_Dash || state == FP_EntityTerryState_DeadGhost) { - if (collider->type == FP_EntityType_Smoochie || collider->type == FP_EntityType_Clinger || collider->type == FP_EntityType_Catfish) - entity_collides_with_collider = false; - } - } break; - - case FP_EntityType_Nil: break; - case FP_EntityType_MerchantTerry: break; - case FP_EntityType_Count: break; - case FP_EntityType_ClubTerry: break; - case FP_EntityType_Map: break; - case FP_EntityType_MerchantGraveyard: break; - case FP_EntityType_MerchantGym: break; - case FP_EntityType_MerchantPhoneCompany: break; - case FP_EntityType_Heart: break; - case FP_EntityType_AirportTerry: break; - case FP_EntityType_ChurchTerry: break; - case FP_EntityType_KennelTerry: break; - case FP_EntityType_PhoneMessageProjectile: break; - case FP_EntityType_AirportTerryPlane: - case FP_EntityType_MobSpawner: - case FP_EntityType_PortalMonkey: break; - case FP_EntityType_Billboard: break; - } - - if (!entity_collides_with_collider) - continue; - - // NOTE: Sweep collider with half the radius of the source entity - Dqn_Rect collider_world_hit_box = FP_Game_CalcEntityWorldHitBox(game, collider->handle); - if (Dqn_V2_Area(collider_world_hit_box.size) <= 0) - continue; - - Dqn_Rect swept_collider_world_hit_box = collider_world_hit_box; - swept_collider_world_hit_box.pos -= (entity_world_hit_box.size * .5f); - swept_collider_world_hit_box.size += entity_world_hit_box.size; - - if (!Dqn_Rect_ContainsPoint(swept_collider_world_hit_box, entity_new_pos)) - continue; - - Dqn_f32 collider_left_wall_x = swept_collider_world_hit_box.pos.x; - Dqn_f32 collider_right_wall_x = swept_collider_world_hit_box.pos.x + swept_collider_world_hit_box.size.w; - Dqn_f32 collider_top_wall_y = swept_collider_world_hit_box.pos.y; - Dqn_f32 collider_bottom_wall_y = swept_collider_world_hit_box.pos.y + swept_collider_world_hit_box.size.h; - - Dqn_V2 o = entity_pos; - Dqn_V2 d = delta_pos; - - // NOTE: Solve collision by determining the 't' value at which - // we hit one of the walls of the collider and move the entity - // at exactly that point. - // O + td = x - // td = x - O - // t = (x - O) / d - - Dqn_f32 earliest_t = SENTINEL_T; - if (d.x != 0.f) { - Dqn_f32 left_t = (collider_left_wall_x - o.x) / d.x; - Dqn_f32 right_t = (collider_right_wall_x - o.x) / d.x; - if (left_t >= 0.f && left_t <= 1.f) - earliest_t = DQN_MIN(earliest_t, left_t); - if (right_t >= 0.f && right_t <= 1.f) - earliest_t = DQN_MIN(earliest_t, right_t); - } - - if (d.y != 0.f) { - Dqn_f32 top_t = (collider_top_wall_y - o.y) / d.y; - Dqn_f32 bottom_t = (collider_bottom_wall_y - o.y) / d.y; - if (top_t >= 0.f && top_t <= 1.f) - earliest_t = DQN_MIN(earliest_t, top_t); - if (bottom_t >= 0.f && bottom_t <= 1.f) - earliest_t = DQN_MIN(earliest_t, bottom_t); - } - - if (earliest_t < global_earliest_t) { - global_earliest_t = earliest_t; - global_earliest_pos_just_before_collide = entity_pos + (d * earliest_t); - } - } - } - - - if (global_earliest_t == SENTINEL_T) { - entity->local_pos += delta_pos; - } else { - Dqn_V2 new_delta_pos = global_earliest_pos_just_before_collide - entity_pos; - entity->local_pos += new_delta_pos; - } -} - static void FP_PlayReset(FP_Game *game, TELY_Platform *platform) { FP_GamePlay *play = &game->play; @@ -441,9 +242,43 @@ static void FP_PlayReset(FP_Game *game, TELY_Platform *platform) // NOTE: Hero ================================================================================== { - FP_GameEntityHandle terry = FP_Entity_CreateTerry(game, Dqn_V2_InitNx2(1434, 11), "Terry"); - play->clicked_entity = terry; - play->player = terry; + FP_GameEntityHandle terry_handle = FP_Entity_CreateTerry(game, Dqn_V2_InitNx2(1434, 11), "Terry"); + Dqn_FArray_Add(&game->play.players, terry_handle); + play->clicked_entity = terry_handle; + + FP_GameEntity *terry = FP_Game_GetEntity(game, terry_handle); + FP_GameControls *controls = &terry->controls; + controls->up.scan_code = TELY_PlatformInputScanCode_W; + controls->down.scan_code = TELY_PlatformInputScanCode_S; + controls->left.scan_code = TELY_PlatformInputScanCode_A; + controls->right.scan_code = TELY_PlatformInputScanCode_D; + controls->attack.scan_code = TELY_PlatformInputScanCode_J; + controls->range_attack.scan_code = TELY_PlatformInputScanCode_K; + controls->build_mode.scan_code = TELY_PlatformInputScanCode_H; + controls->strafe.scan_code = TELY_PlatformInputScanCode_L; + controls->dash.scan_code = TELY_PlatformInputScanCode_N; + controls->buy_building.scan_code = TELY_PlatformInputScanCode_U; + controls->buy_upgrade.scan_code = TELY_PlatformInputScanCode_I; + } + + { + FP_GameEntityHandle terry_handle = FP_Entity_CreateTerry(game, Dqn_V2_InitNx2(1380, 11), "Terry 2"); + Dqn_FArray_Add(&game->play.players, terry_handle); + play->clicked_entity = terry_handle; + + FP_GameEntity *terry = FP_Game_GetEntity(game, terry_handle); + FP_GameControls *controls = &terry->controls; + controls->up.scan_code = TELY_PlatformInputScanCode_Up; + controls->down.scan_code = TELY_PlatformInputScanCode_Down; + controls->left.scan_code = TELY_PlatformInputScanCode_Left; + controls->right.scan_code = TELY_PlatformInputScanCode_Right; + controls->attack.scan_code = TELY_PlatformInputScanCode_J; + controls->range_attack.scan_code = TELY_PlatformInputScanCode_K; + controls->build_mode.scan_code = TELY_PlatformInputScanCode_H; + controls->strafe.scan_code = TELY_PlatformInputScanCode_L; + controls->dash.scan_code = TELY_PlatformInputScanCode_N; + controls->buy_building.scan_code = TELY_PlatformInputScanCode_U; + controls->buy_upgrade.scan_code = TELY_PlatformInputScanCode_I; } { @@ -540,18 +375,6 @@ void TELY_DLL_Init(void *user_data) game->atlas_sprite_sheet = FP_LoadSpriteSheetFromSpec(platform, assets, &platform->arena, DQN_STRING8("atlas")); FP_PlayReset(game, platform); - FP_GameControls *controls = &game->controls; - controls->up.scan_code = TELY_PlatformInputScanCode_W; - controls->down.scan_code = TELY_PlatformInputScanCode_S; - controls->left.scan_code = TELY_PlatformInputScanCode_A; - controls->right.scan_code = TELY_PlatformInputScanCode_D; - controls->attack.scan_code = TELY_PlatformInputScanCode_J; - controls->range_attack.scan_code = TELY_PlatformInputScanCode_K; - controls->build_mode.scan_code = TELY_PlatformInputScanCode_H; - controls->strafe.scan_code = TELY_PlatformInputScanCode_L; - controls->dash.scan_code = TELY_PlatformInputScanCode_N; - controls->buy_building.scan_code = TELY_PlatformInputScanCode_U; - controls->buy_upgrade.scan_code = TELY_PlatformInputScanCode_I; } struct FP_GetClosestPortalMonkeyResult @@ -614,17 +437,16 @@ static void FP_AppendMobSpawnerWaypoints(FP_Game *game, FP_GameEntityHandle src_ void FP_EntityActionStateMachine(FP_Game *game, TELY_Audio *audio, TELY_PlatformInput *input, FP_GameEntity *entity, Dqn_V2 *acceleration_meters_per_s) { - TELY_AssetSpriteSheet *sheet = &game->atlas_sprite_sheet; - FP_GameEntityAction *action = &entity->action; - bool const we_are_clicked_entity = entity->handle == game->play.clicked_entity; - bool const entity_has_velocity = entity->velocity.x || entity->velocity.y; - bool const entering_new_state = entity->alive_time_s == 0.f || action->state != action->next_state; - bool const action_has_finished = !entering_new_state && game->play.clock_ms >= action->end_at_clock_ms; - action->state = action->next_state; + TELY_AssetSpriteSheet *sheet = &game->atlas_sprite_sheet; + FP_GameEntityAction *action = &entity->action; + bool const we_are_clicked_entity = entity->handle == game->play.clicked_entity; + bool const entity_has_velocity = entity->velocity.x || entity->velocity.y; + bool const entering_new_state = entity->alive_time_s == 0.f || action->state != action->next_state; + bool const action_has_finished = !entering_new_state && game->play.clock_ms >= action->end_at_clock_ms; + FP_GameControls const *controls = &entity->controls; + action->state = action->next_state; + FP_EntityRenderData render_data = FP_Entity_GetRenderData(game, entity->type, entity->action.state, entity->direction); - FP_GameControls const *controls = &game->controls; - - FP_EntityRenderData render_data = FP_Entity_GetRenderData(game, entity->type, entity->action.state, entity->direction); switch (entity->type) { case FP_EntityType_Terry: { FP_EntityTerryState *state = DQN_CAST(FP_EntityTerryState *) & action->state; @@ -651,42 +473,40 @@ void FP_EntityActionStateMachine(FP_Game *game, TELY_Audio *audio, TELY_Platform FP_Game_EntityActionReset(game, entity->handle, duration_ms, render_data.sprite); } - if (we_are_clicked_entity) { - if (game->play.in_game_menu != FP_GameInGameMenu_Build) { - bool picked_up_monkey_this_frame = false; - if (FP_Game_IsNilEntityHandle(game, entity->carried_monkey)) { - // NOTE: Check if we are nearby a monkey and picking it up - FP_GetClosestPortalMonkeyResult closest_monkey = FP_GetClosestPortalMonkey(game, entity->handle); - if (closest_monkey.dist < DQN_SQUARED(FP_Game_MetersToPixelsNx1(game->play, 2.f))) { - if (TELY_Platform_InputScanCodeIsPressed(input, controls->attack.scan_code)) { - entity->carried_monkey = closest_monkey.entity; - picked_up_monkey_this_frame = true; - TELY_Audio_Play(audio, game->audio[FP_GameAudio_Monkey], 1.f); - } - } - } - - if (FP_Game_IsNilEntityHandle(game, entity->carried_monkey)) { - if (TELY_Platform_InputScanCodeIsPressed(input, controls->attack.scan_code) || - TELY_Platform_InputGamepadKeyIsPressed(input, 0, TELY_PlatformInputGamepadKey_X)) { - FP_Game_EntityTransitionState(game, entity, FP_EntityTerryState_Attack); - } else if (TELY_Platform_InputScanCodeIsPressed(input, controls->range_attack.scan_code) || - TELY_Platform_InputGamepadKeyIsPressed(input, 0, TELY_PlatformInputGamepadKey_Y)) { - FP_Game_EntityTransitionState(game, entity, FP_EntityTerryState_RangeAttack); - } - } else { - if (!picked_up_monkey_this_frame && TELY_Platform_InputScanCodeIsPressed(input, controls->attack.scan_code)) { - FP_GameEntity *portal_monkey = FP_Game_GetEntity(game, entity->carried_monkey); - portal_monkey->local_pos = FP_Game_CalcEntityWorldPos(game, entity->handle); - entity->carried_monkey = {}; + if (entity->in_game_menu != FP_GameInGameMenu_Build) { + bool picked_up_monkey_this_frame = false; + if (FP_Game_IsNilEntityHandle(game, entity->carried_monkey)) { + // NOTE: Check if we are nearby a monkey and picking it up + FP_GetClosestPortalMonkeyResult closest_monkey = FP_GetClosestPortalMonkey(game, entity->handle); + if (closest_monkey.dist < DQN_SQUARED(FP_Game_MetersToPixelsNx1(game->play, 2.f))) { + if (TELY_Platform_InputScanCodeIsPressed(input, controls->attack.scan_code)) { + entity->carried_monkey = closest_monkey.entity; + picked_up_monkey_this_frame = true; + TELY_Audio_Play(audio, game->audio[FP_GameAudio_Monkey], 1.f); } } } - if (action->next_state == action->state && (acceleration_meters_per_s->x || acceleration_meters_per_s->y)) { - FP_Game_EntityTransitionState(game, entity, FP_EntityTerryState_Run); + if (FP_Game_IsNilEntityHandle(game, entity->carried_monkey)) { + if (TELY_Platform_InputScanCodeIsPressed(input, controls->attack.scan_code) || + TELY_Platform_InputGamepadKeyIsPressed(input, 0, TELY_PlatformInputGamepadKey_X)) { + FP_Game_EntityTransitionState(game, entity, FP_EntityTerryState_Attack); + } else if (TELY_Platform_InputScanCodeIsPressed(input, controls->range_attack.scan_code) || + TELY_Platform_InputGamepadKeyIsPressed(input, 0, TELY_PlatformInputGamepadKey_Y)) { + FP_Game_EntityTransitionState(game, entity, FP_EntityTerryState_RangeAttack); + } + } else { + if (!picked_up_monkey_this_frame && TELY_Platform_InputScanCodeIsPressed(input, controls->attack.scan_code)) { + FP_GameEntity *portal_monkey = FP_Game_GetEntity(game, entity->carried_monkey); + portal_monkey->local_pos = FP_Game_CalcEntityWorldPos(game, entity->handle); + entity->carried_monkey = {}; + } } } + + if (action->next_state == action->state && (acceleration_meters_per_s->x || acceleration_meters_per_s->y)) { + FP_Game_EntityTransitionState(game, entity, FP_EntityTerryState_Run); + } } break; case FP_EntityTerryState_Attack: { @@ -718,46 +538,44 @@ void FP_EntityActionStateMachine(FP_Game *game, TELY_Audio *audio, TELY_Platform FP_Game_EntityActionReset(game, entity->handle, duration_ms, render_data.sprite); } - if (we_are_clicked_entity) { - bool picked_up_monkey_this_frame = false; - if (game->play.in_game_menu != FP_GameInGameMenu_Build) { - if (FP_Game_IsNilEntityHandle(game, entity->carried_monkey)) { - // NOTE: Check if we are nearby a monkey and picking it up - FP_GetClosestPortalMonkeyResult closest_monkey = FP_GetClosestPortalMonkey(game, entity->handle); - if (closest_monkey.dist < DQN_SQUARED(FP_Game_MetersToPixelsNx1(game->play, 2.f))) { - if (TELY_Platform_InputScanCodeIsPressed(input, controls->attack.scan_code)) { - entity->carried_monkey = closest_monkey.entity; - picked_up_monkey_this_frame = true; - TELY_Audio_Play(audio, game->audio[FP_GameAudio_Monkey], 1.f); - } - } - } - - if (FP_Game_IsNilEntityHandle(game, entity->carried_monkey)) { - if (TELY_Platform_InputScanCodeIsPressed(input, controls->attack.scan_code) || - TELY_Platform_InputGamepadKeyIsPressed(input, 0, TELY_PlatformInputGamepadKey_X)) { - FP_Game_EntityTransitionState(game, entity, FP_EntityTerryState_Attack); - } else if (TELY_Platform_InputScanCodeIsPressed(input, controls->range_attack.scan_code) || - TELY_Platform_InputGamepadKeyIsPressed(input, 0, TELY_PlatformInputGamepadKey_Y)) { - FP_Game_EntityTransitionState(game, entity, FP_EntityTerryState_RangeAttack); + bool picked_up_monkey_this_frame = false; + if (entity->in_game_menu != FP_GameInGameMenu_Build) { + if (FP_Game_IsNilEntityHandle(game, entity->carried_monkey)) { + // NOTE: Check if we are nearby a monkey and picking it up + FP_GetClosestPortalMonkeyResult closest_monkey = FP_GetClosestPortalMonkey(game, entity->handle); + if (closest_monkey.dist < DQN_SQUARED(FP_Game_MetersToPixelsNx1(game->play, 2.f))) { + if (TELY_Platform_InputScanCodeIsPressed(input, controls->attack.scan_code)) { + entity->carried_monkey = closest_monkey.entity; + picked_up_monkey_this_frame = true; + TELY_Audio_Play(audio, game->audio[FP_GameAudio_Monkey], 1.f); } } } if (FP_Game_IsNilEntityHandle(game, entity->carried_monkey)) { - if (TELY_Platform_InputScanCodeIsPressed(input, controls->dash.scan_code) || - TELY_Platform_InputGamepadKeyIsPressed(input, 0, TELY_PlatformInputGamepadKey_A)) { - FP_Game_EntityTransitionState(game, entity, FP_EntityTerryState_Dash); - } - } else { - if (!picked_up_monkey_this_frame && TELY_Platform_InputScanCodeIsPressed(input, controls->attack.scan_code)) { - FP_GameEntity *portal_monkey = FP_Game_GetEntity(game, entity->carried_monkey); - portal_monkey->local_pos = FP_Game_CalcEntityWorldPos(game, entity->handle); - entity->carried_monkey = {}; + if (TELY_Platform_InputScanCodeIsPressed(input, controls->attack.scan_code) || + TELY_Platform_InputGamepadKeyIsPressed(input, 0, TELY_PlatformInputGamepadKey_X)) { + FP_Game_EntityTransitionState(game, entity, FP_EntityTerryState_Attack); + } else if (TELY_Platform_InputScanCodeIsPressed(input, controls->range_attack.scan_code) || + TELY_Platform_InputGamepadKeyIsPressed(input, 0, TELY_PlatformInputGamepadKey_Y)) { + FP_Game_EntityTransitionState(game, entity, FP_EntityTerryState_RangeAttack); } } } + if (FP_Game_IsNilEntityHandle(game, entity->carried_monkey)) { + if (TELY_Platform_InputScanCodeIsPressed(input, controls->dash.scan_code) || + TELY_Platform_InputGamepadKeyIsPressed(input, 0, TELY_PlatformInputGamepadKey_A)) { + FP_Game_EntityTransitionState(game, entity, FP_EntityTerryState_Dash); + } + } else { + if (!picked_up_monkey_this_frame && TELY_Platform_InputScanCodeIsPressed(input, controls->attack.scan_code)) { + FP_GameEntity *portal_monkey = FP_Game_GetEntity(game, entity->carried_monkey); + portal_monkey->local_pos = FP_Game_CalcEntityWorldPos(game, entity->handle); + entity->carried_monkey = {}; + } + } + if (!entity_has_velocity) { FP_Game_EntityTransitionState(game, entity, FP_EntityTerryState_Idle); } @@ -820,7 +638,6 @@ void FP_EntityActionStateMachine(FP_Game *game, TELY_Audio *audio, TELY_Platform } } break; - case FP_EntityType_Smoochie: { FP_EntitySmoochieState *state = DQN_CAST(FP_EntitySmoochieState *) & action->state; switch (*state) { @@ -1383,7 +1200,7 @@ void FP_EntityActionStateMachine(FP_Game *game, TELY_Audio *audio, TELY_Platform // NOTE: Position the attack box uint64_t duration_ms = action->sprite.anim->count * action->sprite.anim->ms_per_frame; uint64_t midpoint_clock_ms = action->started_at_clock_ms + (duration_ms / 2); - DQN_ASSERT(duration_ms >= PHYSICS_STEP); + DQN_ASSERT(duration_ms >= FP_GAME_PHYSICS_STEP); DQN_ASSERT(action->sprite.anim); // NOTE: Adding an attack_processed bool to make sure things only fire once @@ -1431,137 +1248,111 @@ void FP_Update(TELY_Platform *platform, FP_Game *game, TELY_PlatformInput *input { Dqn_Profiler_ZoneScopeWithIndex("FP_Update", FP_ProfileZone_FPUpdate); - FP_GameControls const *controls = &game->controls; - game->play.update_counter++; - game->play.clock_ms = DQN_CAST(uint64_t)(platform->input.timer_s * 1000.f); + game->play.clock_ms = DQN_CAST(uint64_t)(platform->input.timer_s * 1000.f); Dqn_ProfilerZone update_zone = Dqn_Profiler_BeginZoneWithIndex(DQN_STRING8("FP_Update: Entity loop"), FP_ProfileZone_FPUpdate_EntityLoop); if (game->play.state == FP_GameState_Play) { - Dqn_V2 dir_vector = {}; if (TELY_Platform_InputKeyIsReleased(input->mouse_left)) game->play.clicked_entity = game->play.prev_active_entity; - // NOTE: Keyboard movement input - if (TELY_Platform_InputScanCodeIsDown(input, controls->up.scan_code)) - dir_vector.y = -1.f; - if (TELY_Platform_InputScanCodeIsDown(input, controls->left.scan_code)) - dir_vector.x = -1.f; - if (TELY_Platform_InputScanCodeIsDown(input, controls->down.scan_code)) - dir_vector.y = +1.f; - if (TELY_Platform_InputScanCodeIsDown(input, controls->right.scan_code)) - dir_vector.x = +1.f; - - // NOTE: Gamepad movement input - // NOTE: button_codes 0 should be the first gamepad connected, we can - // get this working with other gamepads later - uint32_t gamepad = 0; - if (input->button_codes[gamepad]) { - dir_vector.x += input->left_stick[gamepad].x; - dir_vector.y += input->left_stick[gamepad].y; - } - - // TODO(doyle): Some bug, diagonal is still faster - if (dir_vector.x && dir_vector.y) { - dir_vector.x *= 0.7071067811865475244f; - dir_vector.y *= 0.7071067811865475244f; - } - if (TELY_Platform_InputScanCodeIsPressed(input, TELY_PlatformInputScanCode_Escape)) game->play.state = FP_GameState_Pause; if (game->play.clicked_entity.id) { if (TELY_Platform_InputScanCodeIsPressed(input, TELY_PlatformInputScanCode_Delete)) FP_Game_DeleteEntity(game, game->play.clicked_entity); - - // NOTE: Building selector - Dqn_usize last_building_index = DQN_ARRAY_UCOUNT(PLACEABLE_BUILDINGS) - 1; - if (TELY_Platform_InputScanCodeIsPressed(input, TELY_PlatformInputScanCode_Q)) { - if (game->play.build_mode_building_index <= 0) { - game->play.build_mode_building_index = last_building_index; - } else { - game->play.build_mode_building_index -= 1; - } - } - - if (TELY_Platform_InputScanCodeIsPressed(input, TELY_PlatformInputScanCode_E)) { - if (game->play.build_mode_building_index >= last_building_index) { - game->play.build_mode_building_index = 0; - } else { - game->play.build_mode_building_index += 1; - } - } - - } else { - Dqn_f32 pan_speed = 5.f; - if (TELY_Platform_InputScanCodeIsDown(input, TELY_PlatformInputScanCode_Space)) - pan_speed *= 2.5f; - game->play.camera.world_pos += dir_vector * pan_speed; } // NOTE: Handle input ========================================================================== for (FP_GameEntityIterator it = {}; FP_Game_DFSPostOrderWalkEntityTree(game, &it, game->play.root_entity); ) { FP_GameEntity *entity = it.entity; - entity->alive_time_s += PHYSICS_STEP; + entity->alive_time_s += FP_GAME_PHYSICS_STEP; if (entity->flags & FP_GameEntityFlag_OccupiedInBuilding) continue; + // NOTE: Calc direction vector from input + FP_GameControls const *controls = &entity->controls; + Dqn_V2 dir_vector = {}; + { + if (TELY_Platform_InputScanCodeIsDown(input, controls->up.scan_code)) + dir_vector.y = -1.f; + if (TELY_Platform_InputScanCodeIsDown(input, controls->left.scan_code)) + dir_vector.x = -1.f; + if (TELY_Platform_InputScanCodeIsDown(input, controls->down.scan_code)) + dir_vector.y = +1.f; + if (TELY_Platform_InputScanCodeIsDown(input, controls->right.scan_code)) + dir_vector.x = +1.f; + + // NOTE: Gamepad movement input + // NOTE: button_codes 0 should be the first gamepad connected, we can + // get this working with other gamepads later + uint32_t gamepad = 0; + if (input->button_codes[gamepad]) { + dir_vector.x += input->left_stick[gamepad].x; + dir_vector.y += input->left_stick[gamepad].y; + } + + // TODO(doyle): Some bug, diagonal is still faster + if (dir_vector.x && dir_vector.y) { + dir_vector.x *= 0.7071067811865475244f; + dir_vector.y *= 0.7071067811865475244f; + } + } + // NOTE: Move entity by keyboard and gamepad =============================================== Dqn_V2 acceleration_meters_per_s = entity->constant_acceleration_per_s; - if (game->play.clicked_entity == entity->handle) { - if (entity->flags & (FP_GameEntityFlag_MoveByKeyboard | FP_GameEntityFlag_MoveByGamepad)) { - bool move_entity = true; - switch (entity->type) { - case FP_EntityType_Terry: { - auto *state = DQN_CAST(FP_EntityTerryState *)&entity->action.state; - move_entity = *state == FP_EntityTerryState_Run || *state == FP_EntityTerryState_Idle; - if (*state == FP_EntityTerryState_DeadGhost) { - FP_GameEntityAction const *action = &entity->action; - uint64_t const elapsed_ms = game->play.clock_ms - action->started_at_clock_ms; - uint16_t const raw_anim_frame = DQN_CAST(uint16_t)(elapsed_ms / action->sprite.anim->ms_per_frame); - if (raw_anim_frame >= action->sprite.anim->count) { - move_entity = true; - } + if (entity->flags & (FP_GameEntityFlag_MoveByKeyboard | FP_GameEntityFlag_MoveByGamepad)) { + bool move_entity = true; + switch (entity->type) { + case FP_EntityType_Terry: { + auto *state = DQN_CAST(FP_EntityTerryState *)&entity->action.state; + move_entity = *state == FP_EntityTerryState_Run || *state == FP_EntityTerryState_Idle; + if (*state == FP_EntityTerryState_DeadGhost) { + FP_GameEntityAction const *action = &entity->action; + uint64_t const elapsed_ms = game->play.clock_ms - action->started_at_clock_ms; + uint16_t const raw_anim_frame = DQN_CAST(uint16_t)(elapsed_ms / action->sprite.anim->ms_per_frame); + if (raw_anim_frame >= action->sprite.anim->count) { + move_entity = true; } - } break; + } + } break; - case FP_EntityType_Smoochie: { - auto *state = DQN_CAST(FP_EntitySmoochieState *)&entity->action.state; - move_entity = *state == FP_EntitySmoochieState_Run || *state == FP_EntitySmoochieState_Idle; - } break; + case FP_EntityType_Smoochie: { + auto *state = DQN_CAST(FP_EntitySmoochieState *)&entity->action.state; + move_entity = *state == FP_EntitySmoochieState_Run || *state == FP_EntitySmoochieState_Idle; + } break; - case FP_EntityType_Clinger: { - auto *state = DQN_CAST(FP_EntityClingerState *)&entity->action.state; - move_entity = *state == FP_EntityClingerState_Run || *state == FP_EntityClingerState_Idle; - } break; + case FP_EntityType_Clinger: { + auto *state = DQN_CAST(FP_EntityClingerState *)&entity->action.state; + move_entity = *state == FP_EntityClingerState_Run || *state == FP_EntityClingerState_Idle; + } break; - case FP_EntityType_Nil: break; - case FP_EntityType_ClubTerry: break; - case FP_EntityType_Count: break; - case FP_EntityType_Map: break; - case FP_EntityType_MerchantTerry: break; - case FP_EntityType_MerchantGraveyard: break; - case FP_EntityType_MerchantPhoneCompany: break; - case FP_EntityType_Heart: break; - case FP_EntityType_MerchantGym: break; - case FP_EntityType_AirportTerry: break; - case FP_EntityType_Catfish: break; - case FP_EntityType_ChurchTerry: break; - case FP_EntityType_KennelTerry: break; - case FP_EntityType_PhoneMessageProjectile: break; - case FP_EntityType_AirportTerryPlane: break; - case FP_EntityType_MobSpawner: - case FP_EntityType_PortalMonkey: break; - case FP_EntityType_Billboard: break; - } - - if (move_entity) { - acceleration_meters_per_s = dir_vector * entity->base_acceleration_per_s.meters; - // if (TELY_Platform_InputScanCodeIsDown(input, TELY_PlatformInputScanCode_Space)) - // acceleration_meters_per_s *= 2.5f; - } + case FP_EntityType_Nil: break; + case FP_EntityType_ClubTerry: break; + case FP_EntityType_Count: break; + case FP_EntityType_Map: break; + case FP_EntityType_MerchantTerry: break; + case FP_EntityType_MerchantGraveyard: break; + case FP_EntityType_MerchantPhoneCompany: break; + case FP_EntityType_Heart: break; + case FP_EntityType_MerchantGym: break; + case FP_EntityType_AirportTerry: break; + case FP_EntityType_Catfish: break; + case FP_EntityType_ChurchTerry: break; + case FP_EntityType_KennelTerry: break; + case FP_EntityType_PhoneMessageProjectile: break; + case FP_EntityType_AirportTerryPlane: break; + case FP_EntityType_MobSpawner: + case FP_EntityType_PortalMonkey: break; + case FP_EntityType_Billboard: break; } - } else { + + if (move_entity) + acceleration_meters_per_s = dir_vector * entity->base_acceleration_per_s.meters; + } + + if (dir_vector == Dqn_V2_Zero) { if (entity->velocity.x) entity->direction = entity->velocity.x > 0.f ? FP_GameDirection_Right : FP_GameDirection_Left; else if (entity->velocity.y) @@ -1736,34 +1527,6 @@ void FP_Update(TELY_Platform *platform, FP_Game *game, TELY_PlatformInput *input } } - #if 0 - // NOTE: Bubble sort entities in the building queue by distance to the building ======== - // For example the closest entity will be assigned the first queue slot to the building - if (entity->building_queue.size && game->play.clock_ms >= entity->building_queue_next_sort_timestamp_ms) { - - // NOTE: We only sort the queue for the building every second. This prevents the queue - // from stagnating incase some entity has locked a position in the queue but had - // some dodgy physics that sent it far away - entity->building_queue_next_sort_timestamp_ms = game->play.clock_ms + 1000; - for (bool swapped = true; swapped; ) { - swapped = false; - for (Dqn_usize index = 0; index < (entity->building_queue.size - 1); index++) { - FP_GameEntityHandle left_handle = entity->building_queue.data[index + 0]; - FP_GameEntityHandle right_handle = entity->building_queue.data[index + 1]; - Dqn_V2 left_world_p = FP_Game_CalcEntityWorldPos(game, left_handle); - Dqn_V2 right_world_p = FP_Game_CalcEntityWorldPos(game, right_handle); - Dqn_f32 left_dist_sq = Dqn_V2_LengthSq_V2x2(entity_pos, left_world_p); - Dqn_f32 right_dist_sq = Dqn_V2_LengthSq_V2x2(entity_pos, right_world_p); - - if (left_dist_sq > (right_dist_sq * 1.1f)) { - DQN_SWAP(entity->building_queue.data[index + 0], entity->building_queue.data[index + 1]); - swapped = true; - } - } - } - } - #endif - // NOTE: Handle waypoints ============================================================== while (entity->waypoints.size) { FP_SentinelListLink *waypoint_link = entity->waypoints.sentinel->next; @@ -1988,19 +1751,43 @@ void FP_Update(TELY_Platform *platform, FP_Game *game, TELY_PlatformInput *input entity->local_pos += mouse_p_delta; } - if (game->play.clicked_entity == entity->handle) { - if (game->play.in_game_menu == FP_GameInGameMenu_Nil || game->play.in_game_menu == FP_GameInGameMenu_Build) { + if (entity->flags & FP_GameEntityFlag_CameraTracking) { + if (!Dqn_FArray_Find(&game->play.camera_tracking_entity, entity->handle).data) + Dqn_FArray_Add(&game->play.camera_tracking_entity, entity->handle); + } + + // NOTE: Building logic ================================================================ + for (FP_GameEntityHandle player : game->play.players) { + if (player != entity->handle) + continue; + + // NOTE: Toggle menu =============================================================== + if (entity->in_game_menu == FP_GameInGameMenu_Nil || entity->in_game_menu == FP_GameInGameMenu_Build) { if (TELY_Platform_InputScanCodeIsPressed(input, controls->build_mode.scan_code)) - game->play.in_game_menu = DQN_CAST(FP_GameInGameMenu)(DQN_CAST(uint32_t)game->play.in_game_menu ^ FP_GameInGameMenu_Build); + entity->in_game_menu = DQN_CAST(FP_GameInGameMenu)(DQN_CAST(uint32_t)entity->in_game_menu ^ FP_GameInGameMenu_Build); } - if (entity->flags & FP_GameEntityFlag_CameraTracking) { - game->play.camera_tracking_entity = entity->handle; - game->play.camera.world_pos_target = FP_Game_CalcEntityWorldPos(game, entity->handle) * game->play.camera.scale; + // NOTE: Building selector ========================================================= + Dqn_usize const last_building_index = DQN_ARRAY_UCOUNT(PLACEABLE_BUILDINGS) - 1; + if (TELY_Platform_InputScanCodeIsPressed(input, TELY_PlatformInputScanCode_Q)) { + if (entity->build_mode_building_index <= 0) { + entity->build_mode_building_index = last_building_index; + } else { + entity->build_mode_building_index -= 1; + } } - FP_GamePlaceableBuilding placeable_building = PLACEABLE_BUILDINGS[game->play.build_mode_building_index]; - game->play.build_mode_can_place_building = false; + if (TELY_Platform_InputScanCodeIsPressed(input, TELY_PlatformInputScanCode_E)) { + if (entity->build_mode_building_index >= last_building_index) { + entity->build_mode_building_index = 0; + } else { + entity->build_mode_building_index += 1; + } + } + + // NOTE: Builder blueprint ========================================================= + FP_GamePlaceableBuilding placeable_building = PLACEABLE_BUILDINGS[entity->build_mode_building_index]; + entity->build_mode_can_place_building = false; uint8_t *inventory_count = nullptr; if (placeable_building.type == FP_EntityType_ChurchTerry) @@ -2013,7 +1800,7 @@ void FP_Update(TELY_Platform *platform, FP_Game *game, TELY_PlatformInput *input inventory_count = &entity->inventory.airports; bool have_building_inventory = inventory_count && (*inventory_count) > 0; - if (have_building_inventory && game->play.in_game_menu == FP_GameInGameMenu_Build) { + if (have_building_inventory && entity->in_game_menu == FP_GameInGameMenu_Build) { Dqn_Rect dest_rect = FP_Game_GetBuildingPlacementRectForEntity(game, placeable_building, entity->handle); Dqn_V2 placement_pos = Dqn_Rect_Center(dest_rect); @@ -2021,16 +1808,23 @@ void FP_Update(TELY_Platform *platform, FP_Game *game, TELY_PlatformInput *input FP_Game_DFSPreOrderWalkEntityTree(game, &zone_it, game->play.root_entity); ) { - FP_GameEntity *zone = zone_it.entity; - bool is_building = zone->type == FP_EntityType_KennelTerry || - zone->type == FP_EntityType_AirportTerry || - zone->type == FP_EntityType_ChurchTerry || - zone->type == FP_EntityType_ClubTerry; + FP_GameEntity *zone = zone_it.entity; + bool cant_overlap_build = zone->type == FP_EntityType_KennelTerry || + zone->type == FP_EntityType_AirportTerry || + zone->type == FP_EntityType_ChurchTerry || + zone->type == FP_EntityType_ClubTerry; + + // NOTE: We also can't overlap with the player incase of 2 players + if (game->play.players.size > 1) { + if (!cant_overlap_build) { + cant_overlap_build = Dqn_FArray_Find(&game->play.players, zone_it.entity->handle).data; + } + } Dqn_Rect zone_hit_box = FP_Game_CalcEntityWorldHitBox(game, zone->handle); - if (is_building) { + if (cant_overlap_build) { if (Dqn_Rect_Intersects(zone_hit_box, dest_rect)) { - game->play.build_mode_can_place_building = false; + entity->build_mode_can_place_building = false; break; } } @@ -2042,10 +1836,10 @@ void FP_Update(TELY_Platform *platform, FP_Game *game, TELY_PlatformInput *input zone_hit_box.size -= dest_rect.size; zone_hit_box.size = Dqn_V2_Max(zone_hit_box.size, Dqn_V2_Zero); - game->play.build_mode_can_place_building |= Dqn_Rect_ContainsPoint(zone_hit_box, placement_pos); + entity->build_mode_can_place_building |= Dqn_Rect_ContainsPoint(zone_hit_box, placement_pos); } - if (game->play.build_mode_can_place_building && + if (entity->build_mode_can_place_building && TELY_Platform_InputScanCodeIsPressed(input, controls->attack.scan_code)) { if (placeable_building.type == FP_EntityType_ClubTerry) { FP_Entity_CreateClubTerry(game, placement_pos, "Club Terry"); @@ -2091,8 +1885,42 @@ void FP_Update(TELY_Platform *platform, FP_Game *game, TELY_PlatformInput *input FP_GameEntityHandle entity_handle = entity->handle; FP_EntityActionStateMachine(game, &platform->audio, input, entity, &acceleration_meters_per_s); - // NOTE: Core equations of motion ========================================================== - FP_Game_MoveEntity(game, entity_handle, acceleration_meters_per_s); + // NOTE: Limit the entity to within bounds of the camera only in multiplayer =========== + bool entity_is_oob_with_camera = false; + Dqn_V2 entity_oob_move = {}; + if (game->play.players.size > 1) { + FP_GameCamera *camera = &game->play.camera; + for (FP_GameEntityHandle player : game->play.players) { + if (entity->handle != player) + continue; + + Dqn_V2 new_entity_pos = entity_pos; + Dqn_Rect camera_view_rect = Dqn_Rect_InitV2x2(camera->world_pos_target - (camera->size * .5f) * camera->scale, camera->size * camera->scale); + + Dqn_Rect entity_hit_box = FP_Game_CalcEntityWorldHitBox(game, entity_handle); + Dqn_Rect playable_bounds = Dqn_Rect_ExpandV2(camera_view_rect, -entity_hit_box.size); + + Dqn_Rect hit_box = FP_Game_CalcEntityWorldHitBox(game, entity_handle); + new_entity_pos.x = DQN_MAX(new_entity_pos.x, playable_bounds.pos.x); + new_entity_pos.y = DQN_MAX(new_entity_pos.y, playable_bounds.pos.y); + new_entity_pos.x = DQN_MIN(new_entity_pos.x, playable_bounds.pos.x + playable_bounds.size.x); + new_entity_pos.y = DQN_MIN(new_entity_pos.y, playable_bounds.pos.y + playable_bounds.size.y); + + if (new_entity_pos != entity_pos) { + entity_is_oob_with_camera = true; + Dqn_V2 delta_pos = new_entity_pos - entity_pos; + entity_oob_move = delta_pos; + } + } + } + + if (entity_is_oob_with_camera) { + if (FP_Game_CanEntityMoveToPosition(game, entity_handle, entity_oob_move).yes) + entity->local_pos += entity_oob_move; + } else { + // NOTE: Core equations of motion ================================================== + FP_Game_MoveEntity(game, entity_handle, acceleration_meters_per_s); + } } // NOTE: If all enemies for the current wave have been spawned and the cooldown has elapsed @@ -2117,7 +1945,7 @@ void FP_Update(TELY_Platform *platform, FP_Game *game, TELY_PlatformInput *input // NOTE: Recover mobile data entity->terry_mobile_data_plan = - DQN_MIN(entity->terry_mobile_data_plan + DQN_CAST(Dqn_usize)(FP_TERRY_MOBILE_DATA_PER_RANGE_ATTACK * .25f * PHYSICS_STEP), + DQN_MIN(entity->terry_mobile_data_plan + DQN_CAST(Dqn_usize)(FP_TERRY_MOBILE_DATA_PER_RANGE_ATTACK * .25f * FP_GAME_PHYSICS_STEP), entity->terry_mobile_data_plan_cap); // NOTE: Recover hp & stamina @@ -2155,7 +1983,7 @@ void FP_Update(TELY_Platform *platform, FP_Game *game, TELY_PlatformInput *input } // NOTE: Mob spawner ======================================================================= - if (entity->type == FP_EntityType_MobSpawner) { + if (entity->type == FP_EntityType_MobSpawner && 0) { // NOTE: Flush any spawn entities that are dead for (FP_SentinelListLink *link = nullptr; FP_SentinelList_Iterate(&entity->spawn_list, &link); ) { FP_GameEntity *spawned_entity = FP_Game_GetEntity(game, link->data); @@ -2241,11 +2069,24 @@ void FP_Update(TELY_Platform *platform, FP_Game *game, TELY_PlatformInput *input continue; // NOTE: Do HP ========================================================================= - if (game->play.player == defender->handle || game->play.heart == defender->handle) { + if (game->play.heart == defender->handle) { if (game->play.god_mode) continue; } + bool god_mode_override = false; + if (game->play.god_mode) { + for (FP_GameEntityHandle player : game->play.players) { + if (player == defender->handle) { + god_mode_override = true; + break; + } + } + } + + if (god_mode_override) + continue; + defender->hp = defender->hp >= attacker->base_attack ? defender->hp - attacker->base_attack : 0; defender->hit_on_clock_ms = game->play.update_counter; defender->trauma01 = 1.f - (defender->hp / DQN_CAST(Dqn_f32)defender->hp_cap); @@ -2286,18 +2127,26 @@ void FP_Update(TELY_Platform *platform, FP_Game *game, TELY_PlatformInput *input game->play.state = FP_GameState_WinGame; } + // NOTE: Game over check ======================================================================= { 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; + if (heart->hp <= 0) game->play.state = FP_GameState_LoseGame; - } } // NOTE: Camera ================================================================================ - FP_GamePlay *play = &game->play; - FP_GameCamera *camera = &play->camera; + + FP_GamePlay *play = &game->play; + FP_GameCamera *camera = &play->camera; + camera->world_pos_target = {}; + for (FP_GameEntityHandle camera_entity : game->play.camera_tracking_entity) { + Dqn_V2 entity_pos = FP_Game_CalcEntityWorldPos(game, camera_entity) * game->play.camera.scale; + camera->world_pos_target += entity_pos; + } + + if (game->play.camera_tracking_entity.size) + camera->world_pos_target /= DQN_CAST(Dqn_f32)game->play.camera_tracking_entity.size; + if (!FP_Game_IsNilEntityHandle(game, game->play.clicked_entity)) { Dqn_V2 window_size = Dqn_V2_InitV2I(platform->core.window_size); @@ -2332,18 +2181,19 @@ void FP_Render(FP_Game *game, TELY_Platform *platform, TELY_Renderer *renderer, TELY_RFui_PushFont(rfui, game->jetbrains_mono_font); TELY_RFui_PushLabelColourV4(rfui, TELY_COLOUR_BLACK_MIDNIGHT_V4); - FP_GameCamera shake_camera = game->play.camera; - FP_GameEntity *camera_entity = FP_Game_GetEntity(game, game->play.camera_tracking_entity); + FP_GameCamera shake_camera = game->play.camera; { - // NOTE: Calculate camera position based on camera shake - Dqn_f32 trauma01 = DQN_SQUARED(camera_entity->trauma01); - - if (camera_entity->type == FP_EntityType_Terry) { - // NOTE: The heart shake is trauma^3 to emphasise the severity of losing heart health - FP_GameEntity *heart = FP_Game_GetEntity(game, game->play.heart); - trauma01 = DQN_MAX(trauma01, DQN_SQUARED(heart->trauma01) * heart->trauma01); + Dqn_f32 trauma01 = 0.f; + for (FP_GameEntityHandle camera_entity_handle : game->play.camera_tracking_entity) { + FP_GameEntity *camera_entity = FP_Game_GetEntity(game, camera_entity_handle); + trauma01 = DQN_MAX(trauma01, DQN_SQUARED(camera_entity->trauma01)); } + // NOTE: The heart shake is trauma^3 to emphasise the severity of losing heart health + FP_GameEntity *heart = FP_Game_GetEntity(game, game->play.heart); + trauma01 = DQN_MAX(trauma01, DQN_SQUARED(heart->trauma01) * heart->trauma01); + + // NOTE: Calculate camera position based on camera shake Dqn_f32 max_shake_dist = 400.f; Dqn_f32 half_shake_dist = max_shake_dist * .5f; Dqn_V2 shake_offset = {}; @@ -2354,6 +2204,7 @@ void FP_Render(FP_Game *game, TELY_Platform *platform, TELY_Renderer *renderer, shake_camera.world_pos += shake_offset * interp_rate; } + FP_GameCameraM2x3 camera_xforms = FP_Game_CameraModelViewM2x3(shake_camera); TELY_Render_PushTransform(renderer, camera_xforms.model_view); Dqn_V2 world_mouse_p = Dqn_M2x3_MulV2(camera_xforms.view_model, input->mouse_p); @@ -2377,6 +2228,14 @@ void FP_Render(FP_Game *game, TELY_Platform *platform, TELY_Renderer *renderer, TELY_Render_PushColourV4(renderer, TELY_COLOUR_BLACK_V4); // NOTE: Draw entities ========================================================================= + bool render_build_mode = false; + for (FP_GameEntityHandle player : game->play.players) { + FP_GameEntity *entity = FP_Game_GetEntity(game, player); + render_build_mode = entity->in_game_menu == FP_GameInGameMenu_Build;; + if (render_build_mode) + break; + } + Dqn_V4 const colour_accent_yellow = TELY_Colour_V4InitRGBU32(0xFFE726); for (FP_GameEntityIterator it = {}; FP_Game_DFSPostOrderWalkEntityTree(game, &it, game->play.root_entity); ) { FP_GameEntity *entity = it.entity; @@ -2538,16 +2397,25 @@ void FP_Render(FP_Game *game, TELY_Platform *platform, TELY_Renderer *renderer, TELY_COLOUR_WHITE_V4); } - 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_V2 draw_p = Dqn_Rect_InterpolatedPoint(world_hit_box, Dqn_V2_InitNx2(0.f, -0.2f)); - Dqn_f32 health_t = entity->hp / DQN_CAST(Dqn_f32)entity->hp_cap; - Dqn_Rect health_rect = Dqn_Rect_InitNx4(draw_p.x, draw_p.y, DQN_CAST(Dqn_f32)entity->hp_cap, bar_height); - Dqn_Rect curr_health_rect = Dqn_Rect_InitNx4(draw_p.x, draw_p.y, DQN_CAST(Dqn_f32)entity->hp_cap * health_t, bar_height); + if (entity->handle != game->play.heart && entity->hp != entity->hp_cap && entity->hp) { + // NOTE: We don't draw health bar for the player either + bool current_entity_is_a_player = false; + for (FP_GameEntityHandle player : game->play.players) { + current_entity_is_a_player |= (entity->handle == player); + } + + if (!current_entity_is_a_player) { + Dqn_f32 bar_height = 12.f; + Dqn_V2 draw_p = Dqn_Rect_InterpolatedPoint(world_hit_box, Dqn_V2_InitNx2(0.f, -0.2f)); + Dqn_f32 health_t = entity->hp / DQN_CAST(Dqn_f32)entity->hp_cap; + Dqn_Rect health_rect = Dqn_Rect_InitNx4(draw_p.x, draw_p.y, DQN_CAST(Dqn_f32)entity->hp_cap, bar_height); + Dqn_Rect curr_health_rect = Dqn_Rect_InitNx4(draw_p.x, draw_p.y, DQN_CAST(Dqn_f32)entity->hp_cap * 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 = 1.f; + } - 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 = 1.f; } if (entity->type == FP_EntityType_ClubTerry || @@ -2569,7 +2437,7 @@ void FP_Render(FP_Game *game, TELY_Platform *platform, TELY_Renderer *renderer, } } - if (game->play.in_game_menu == FP_GameInGameMenu_Build) { + if (render_build_mode) { if (entity->flags & FP_GameEntityFlag_BuildZone) TELY_Render_RectColourV4(renderer, world_hit_box, TELY_RenderShapeMode_Fill, TELY_Colour_V4Alpha(TELY_COLOUR_BLUE_CADET_V4, 0.5f)); } @@ -2655,50 +2523,56 @@ void FP_Render(FP_Game *game, TELY_Platform *platform, TELY_Renderer *renderer, } if (entity->type == FP_EntityType_Billboard) { - FP_EntityBillboardState state = DQN_CAST(FP_EntityBillboardState)entity->action.state; - FP_GameKeyBind const *key_bind = nullptr; - Dqn_V2 draw_p = {}; - Dqn_V4 colour = TELY_COLOUR_BLACK_V4; - switch (state) { - case FP_EntityBillboardState_Attack: { - key_bind = &game->controls.attack; - draw_p = Dqn_Rect_InterpolatedPoint(world_hit_box, Dqn_V2_InitNx2(0.65f, 0.2f)); - colour = colour_accent_yellow; - } break; + for (FP_GameEntityHandle player_handle : game->play.players) { + FP_GameEntity const *player = FP_Game_GetEntity(game, player_handle); + FP_GameControls const *controls = &player->controls; - case FP_EntityBillboardState_Dash: { - key_bind = &game->controls.dash; - draw_p = Dqn_Rect_InterpolatedPoint(world_hit_box, Dqn_V2_InitNx2(0.62f, -0.07f)); - colour = TELY_Colour_V4InitRGBU32(0xFFE726); - } break; + FP_EntityBillboardState state = DQN_CAST(FP_EntityBillboardState) entity->action.state; + FP_GameKeyBind const *key_bind = nullptr; + Dqn_V2 draw_p = {}; + Dqn_V4 colour = TELY_COLOUR_BLACK_V4; + switch (state) { + case FP_EntityBillboardState_Attack: { + key_bind = &controls->attack; + draw_p = Dqn_Rect_InterpolatedPoint(world_hit_box, Dqn_V2_InitNx2(0.65f, 0.2f)); + colour = colour_accent_yellow; + } break; - case FP_EntityBillboardState_Monkey: { - } break; + case FP_EntityBillboardState_Dash: { + key_bind = &controls->dash; + draw_p = Dqn_Rect_InterpolatedPoint(world_hit_box, Dqn_V2_InitNx2(0.62f, -0.07f)); + colour = TELY_Colour_V4InitRGBU32(0xFFE726); + } break; - case FP_EntityBillboardState_RangeAttack: { - key_bind = &game->controls.range_attack; - draw_p = Dqn_Rect_InterpolatedPoint(world_hit_box, Dqn_V2_InitNx2(0.33f, -0.2f)); - colour = TELY_Colour_V4InitRGBU32(0x364659); - } break; + case FP_EntityBillboardState_Monkey: { + } break; - case FP_EntityBillboardState_Strafe: { - key_bind = &game->controls.dash; - draw_p = Dqn_Rect_InterpolatedPoint(world_hit_box, Dqn_V2_InitNx2(0.52f, -0.15f)); - colour = TELY_Colour_V4InitRGBU32(0xFF68A8); - } break; - } + case FP_EntityBillboardState_RangeAttack: { + key_bind = &controls->range_attack; + draw_p = Dqn_Rect_InterpolatedPoint(world_hit_box, Dqn_V2_InitNx2(0.33f, -0.2f)); + colour = TELY_Colour_V4InitRGBU32(0x364659); + } break; - if (key_bind) { - TELY_Render_PushColourV4(renderer, colour); - TELY_Render_PushFont(renderer, game->talkco_font_xlarge); - DQN_DEFER { - TELY_Render_PopFont(renderer); - TELY_Render_PopColourV4(renderer); - }; + case FP_EntityBillboardState_Strafe: { + key_bind = &controls->dash; + draw_p = Dqn_Rect_InterpolatedPoint(world_hit_box, Dqn_V2_InitNx2(0.52f, -0.15f)); + colour = TELY_Colour_V4InitRGBU32(0xFF68A8); + } break; + } - DQN_ASSERT(key_bind->scan_code >= TELY_PlatformInputScanCode_A && key_bind->scan_code <= TELY_PlatformInputScanCode_Z); - char scan_code_ch = DQN_CAST(char)('A' + (key_bind->scan_code - TELY_PlatformInputScanCode_A)); - TELY_Render_TextF(renderer, draw_p, Dqn_V2_InitNx1(0.5f), "[%c]", scan_code_ch); + if (key_bind) { + TELY_Render_PushColourV4(renderer, colour); + TELY_Render_PushFont(renderer, game->talkco_font_xlarge); + DQN_DEFER { + TELY_Render_PopFont(renderer); + TELY_Render_PopColourV4(renderer); + }; + + if (key_bind->scan_code >= TELY_PlatformInputScanCode_A && key_bind->scan_code <= TELY_PlatformInputScanCode_Z) { + char scan_code_ch = DQN_CAST(char)('A' + (key_bind->scan_code - TELY_PlatformInputScanCode_A)); + TELY_Render_TextF(renderer, draw_p, Dqn_V2_InitNx1(0.5f), "[%c]", scan_code_ch); + } + } } } } @@ -2706,602 +2580,607 @@ void FP_Render(FP_Game *game, TELY_Platform *platform, TELY_Renderer *renderer, // NOTE: Render overlay UI ===================================================================== if (!game->play.debug_hide_hud && (game->play.state == FP_GameState_Pause || game->play.state == FP_GameState_Play)) { - // NOTE: Render the merchant menus ========================================================= - FP_GameEntity *player = FP_Game_GetEntity(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}; + // NOTE: Render the merchant menus for each player ========================================= + FP_GamePlay *play = &game->play; + FP_GameEntityHandle merchant_handles[] = { + play->merchant_terry, + play->merchant_graveyard, + play->merchant_gym, + play->merchant_phone_company, + }; - FP_GameInventory *invent = &player->inventory; - FP_GamePlay *play = &game->play; + static bool sound_played_flags[DQN_ARRAY_UCOUNT(merchant_handles)] = {false, false, false, false}; + DQN_FOR_UINDEX (merchant_index, DQN_ARRAY_UCOUNT(merchant_handles)) { + FP_GameEntityHandle merchant = merchant_handles[merchant_index]; + Dqn_V2 merchant_pos = FP_Game_CalcEntityWorldPos(game, merchant); - struct FP_MerchantToMenuMapping { - FP_GameEntityHandle merchant; - Dqn_V2 *menu_pos; - Dqn_String8 upgrade_icon; - Dqn_String8 menu_anim; - Dqn_String8 building; - Dqn_V2 building_offset01; - uint8_t *inventory_count; - uint32_t *building_base_price; - uint32_t *upgrade_base_price; - FP_GameAudio audio_type; - bool *sound_played; - } merchants[] = { - {play->merchant_terry, &play->merchant_terry_menu_pos, g_anim_names.icon_attack, 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]}, - {play->merchant_graveyard, &play->merchant_graveyard_menu_pos, g_anim_names.icon_stamina, 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]}, - {play->merchant_gym, &play->merchant_gym_menu_pos, g_anim_names.icon_health, 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]}, - {play->merchant_phone_company, &play->merchant_phone_company_menu_pos, g_anim_names.icon_phone, 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; - for (FP_MerchantToMenuMapping mapping : merchants) { - FP_GameEntityHandle merchant_handle = mapping.merchant; - Dqn_V2 world_pos = FP_Game_CalcEntityWorldPos(game, merchant_handle); - Dqn_f32 dist_squared = Dqn_V2_LengthSq_V2x2(world_pos, player_pos); - - if (dist_squared > DQN_SQUARED(FP_Game_MetersToPixelsNx1(game->play, 4))) { - *mapping.sound_played = false; - continue; + bool all_players_are_far_away_from_merchant_menu_trigger = true; + DQN_FOR_UINDEX (player_index, game->play.players.size) { + FP_GameEntityHandle player_handle = game->play.players.data[player_index]; + Dqn_V2 player_pos = FP_Game_CalcEntityWorldPos(game, player_handle); + Dqn_f32 dist_squared = Dqn_V2_LengthSq_V2x2(merchant_pos, player_pos); + if (dist_squared < DQN_SQUARED(FP_Game_MetersToPixelsNx1(game->play, 4))) { + all_players_are_far_away_from_merchant_menu_trigger = false; + break; } + } - // NOTE: Render animated merchant menu ============================= - activated_merchant = true; - Dqn_Rect merchant_menu_rect = {}; - { - FP_GameRenderSprite *sprite = &game->play.player_merchant_menu; - if (!sprite->asset.anim || sprite->asset.anim->label != mapping.menu_anim) { - sprite->asset = TELY_Asset_MakeAnimatedSprite(&game->atlas_sprite_sheet, mapping.menu_anim, TELY_AssetFlip_No); - sprite->started_at_clock_ms = game->play.clock_ms; - } + if (all_players_are_far_away_from_merchant_menu_trigger) + sound_played_flags[merchant_index] = false; + } - uint64_t elapsed_ms = game->play.clock_ms - sprite->started_at_clock_ms; - uint16_t raw_anim_frame = DQN_CAST(uint16_t)(elapsed_ms / sprite->asset.anim->ms_per_frame); - uint16_t anim_frame = raw_anim_frame % sprite->asset.anim->count; - - Dqn_usize sprite_index = sprite->asset.anim->index + anim_frame; - Dqn_Rect src_rect = sprite->asset.sheet->rects.data[sprite_index]; - - if (*mapping.menu_pos == Dqn_V2_Zero) - *mapping.menu_pos = world_pos; - - Dqn_Rect top_rect = Dqn_Rect_InitV2x2(world_pos - (src_rect.size * .5f) - Dqn_V2_InitNx2(0.f, src_rect.size.h), src_rect.size); - Dqn_Rect bottom_rect = Dqn_Rect_InitV2x2(world_pos - (src_rect.size * .5f) + Dqn_V2_InitNx2(0.f, src_rect.size.h), src_rect.size); - - // NOTE: Move the merchant menu if we overlap with it so as to not occlude the player - { - Dqn_V2 target_pos = {}; - Dqn_Rect camera_entity_hit_box = FP_Game_CalcEntityWorldHitBox(game, camera_entity->handle); - if (Dqn_Rect_Intersects(top_rect, camera_entity_hit_box)) { - target_pos = bottom_rect.pos; - } else { - target_pos = top_rect.pos; - } - - // NOTE: Interpolate the menu position - *mapping.menu_pos += (target_pos - *mapping.menu_pos) * (24.f * DQN_CAST(Dqn_f32)input->delta_s); - } - - merchant_menu_rect.size = src_rect.size; - merchant_menu_rect.pos = *mapping.menu_pos; - - // NOTE: Bob the merchant menu - Dqn_f32 sin_t = DQN_SINF(DQN_CAST(Dqn_f32)input->timer_s * 3.f); - merchant_menu_rect.pos.y += sin_t * 4.f; - - TELY_Render_TextureColourV4(renderer, - sprite->asset.sheet->tex_handle, - src_rect, - merchant_menu_rect, - Dqn_V2_Zero /*rotate origin*/, - 0.f /*rotation*/, - 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_BLACK_V4); - TELY_Render_PushFont(renderer, game->talkco_font_large); - DQN_DEFER { - TELY_Render_PopFont(renderer); - TELY_Render_PopColourV4(renderer); + Dqn_Rect first_player_avatar_rect = {}; + DQN_FOR_UINDEX (player_index, game->play.players.size) { + FP_GameEntityHandle player_handle = game->play.players.data[player_index]; + FP_GameEntity *player = FP_Game_GetEntity(game, player_handle); + Dqn_V2 player_pos = FP_Game_CalcEntityWorldPos(game, player_handle); + { + FP_GameInventory *invent = &player->inventory; + struct FP_MerchantToMenuMapping { + FP_GameEntityHandle merchant; + Dqn_V2 *menu_pos; + Dqn_String8 upgrade_icon; + Dqn_String8 menu_anim; + Dqn_String8 building; + Dqn_V2 building_offset01; + uint8_t *inventory_count; + uint32_t *building_base_price; + uint32_t *upgrade_base_price; + FP_GameAudio audio_type; + bool *sound_played; + } merchants[] = { + {play->merchant_terry, &play->merchant_terry_menu_pos, g_anim_names.icon_attack, 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]}, + {play->merchant_graveyard, &play->merchant_graveyard_menu_pos, g_anim_names.icon_stamina, 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]}, + {play->merchant_gym, &play->merchant_gym_menu_pos, g_anim_names.icon_health, 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]}, + {play->merchant_phone_company, &play->merchant_phone_company_menu_pos, g_anim_names.icon_phone, 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]}, }; - // NOTE: Render the merchant button for buildings ================== - uint64_t const buy_duration_ms = 500; - Dqn_V4 keybind_btn_shadow_colour = Dqn_V4_InitNx4(0.4f, 0.4f, 0.4f, 1.f); - { - bool const have_enough_coins = player->coins >= *mapping.building_base_price; - FP_GameKeyBind key_bind = game->controls.buy_building; - // NOTE: Buy trigger + animation =============================== - { - bool trigger_buy_anim = false; - - if (have_enough_coins) { - if (TELY_Platform_InputScanCodeIsPressed(input, key_bind.scan_code)) { - game->play.player_trigger_purchase_building_timestamp = game->play.clock_ms + buy_duration_ms; - } else if (TELY_Platform_InputScanCodeIsDown(input, key_bind.scan_code)) { - trigger_buy_anim = true; - if (game->play.clock_ms > game->play.player_trigger_purchase_building_timestamp) - game->play.player_trigger_purchase_building_timestamp = game->play.clock_ms; - } else if (TELY_Platform_InputScanCodeIsReleased(input, key_bind.scan_code)) { - if (game->play.clock_ms > game->play.player_trigger_purchase_building_timestamp) { - if (mapping.inventory_count) { - player->coins -= *mapping.building_base_price; - TELY_Audio_Play(audio, game->audio[FP_GameAudio_Ching], 1.f); - *mapping.building_base_price *= 1; - - // NOTE: Raise the prices of everything else - invent->airports_base_price *= 1; - invent->clubs_base_price *= 1; - invent->kennels_base_price *= 1; - invent->churchs_base_price *= 1; - - (*mapping.inventory_count)++; - } - } else { - game->play.player_trigger_purchase_building_timestamp = UINT64_MAX; - } - } - - if (trigger_buy_anim) { - uint64_t start_buy_time = game->play.player_trigger_purchase_building_timestamp - buy_duration_ms; - uint64_t elapsed_time = game->play.clock_ms - start_buy_time; - - Dqn_f32 buy_t = DQN_MIN(elapsed_time / DQN_CAST(Dqn_f32)buy_duration_ms, 1.f); - Dqn_Rect buy_lerp_rect = {}; - buy_lerp_rect.pos = Dqn_Rect_InterpolatedPoint(merchant_menu_rect, Dqn_V2_InitNx2(0.297f, 0.215f)); - buy_lerp_rect.size.w = (merchant_menu_rect.size.w * 0.38f) * buy_t; - buy_lerp_rect.size.h = merchant_menu_rect.size.h * .611f; - - TELY_Render_RectColourV4(renderer, buy_lerp_rect, TELY_RenderShapeMode_Fill, TELY_Colour_V4Alpha(TELY_COLOUR_BLUE_CADET_V4, 0.5f)); - } - } + bool activated_merchant = false; + for (FP_MerchantToMenuMapping mapping : merchants) { + FP_GameEntityHandle merchant_handle = mapping.merchant; + Dqn_V2 world_pos = FP_Game_CalcEntityWorldPos(game, merchant_handle); + Dqn_f32 dist_squared = Dqn_V2_LengthSq_V2x2(world_pos, player_pos); + if (dist_squared > DQN_SQUARED(FP_Game_MetersToPixelsNx1(game->play, 4))) { + continue; } - // NOTE: Render the (A) button ================================= - Dqn_V4 tex_mod_colour = have_enough_coins ? TELY_COLOUR_WHITE_V4 : TELY_Colour_V4Alpha(TELY_COLOUR_RED_TOMATO_V4, .5f); + // NOTE: Render animated merchant menu ============================= + activated_merchant = true; + Dqn_Rect merchant_menu_rect = {}; { - // NOTE: Render the gamepad button - Dqn_Rect gamepad_btn_rect = {}; - { - TELY_AssetSpriteAnimation *anim = TELY_Asset_GetSpriteAnimation(&game->atlas_sprite_sheet, g_anim_names.merchant_button_a); - Dqn_Rect button_rect = game->atlas_sprite_sheet.rects.data[anim->index]; - gamepad_btn_rect.size = button_rect.size * 1.5f; - gamepad_btn_rect.pos = Dqn_Rect_InterpolatedPoint(merchant_menu_rect, Dqn_V2_InitNx2(0.345f, 0.41f)); - TELY_Render_TextureColourV4(renderer, - game->atlas_sprite_sheet.tex_handle, - button_rect, - gamepad_btn_rect, - Dqn_V2_Zero /*rotate origin*/, - 0.f /*rotation*/, - tex_mod_colour); + FP_GameRenderSprite *sprite = &game->play.player_merchant_menu; + if (!sprite->asset.anim || sprite->asset.anim->label != mapping.menu_anim) { + sprite->asset = TELY_Asset_MakeAnimatedSprite(&game->atlas_sprite_sheet, mapping.menu_anim, TELY_AssetFlip_No); + sprite->started_at_clock_ms = game->play.clock_ms; } - // NOTE: Render the $ cost - TELY_Render_TextF(renderer, Dqn_Rect_InterpolatedPoint(gamepad_btn_rect, Dqn_V2_InitNx2(0.5f, -0.8f)), Dqn_V2_InitNx2(0.5, 0.f), "$%u", *mapping.building_base_price); + uint64_t elapsed_ms = game->play.clock_ms - sprite->started_at_clock_ms; + uint16_t raw_anim_frame = DQN_CAST(uint16_t)(elapsed_ms / sprite->asset.anim->ms_per_frame); + uint16_t anim_frame = raw_anim_frame % sprite->asset.anim->count; - // NOTE: Render the keyboard binding ======================================= + Dqn_usize sprite_index = sprite->asset.anim->index + anim_frame; + Dqn_Rect src_rect = sprite->asset.sheet->rects.data[sprite_index]; + + if (*mapping.menu_pos == Dqn_V2_Zero) + *mapping.menu_pos = world_pos; + + Dqn_Rect top_rect = Dqn_Rect_InitV2x2(world_pos - (src_rect.size * .5f) - Dqn_V2_InitNx2(0.f, src_rect.size.h), src_rect.size); + Dqn_Rect bottom_rect = Dqn_Rect_InitV2x2(world_pos - (src_rect.size * .5f) + Dqn_V2_InitNx2(0.f, src_rect.size.h), src_rect.size); + + // NOTE: Move the merchant menu if we overlap with it so as to not occlude the player { - DQN_ASSERT(key_bind.scan_code >= TELY_PlatformInputScanCode_A && key_bind.scan_code <= TELY_PlatformInputScanCode_Z); - char scan_code_ch = DQN_CAST(char)('A' + (key_bind.scan_code - TELY_PlatformInputScanCode_A)); + Dqn_V2 target_pos = {}; + Dqn_Rect camera_entity_hit_box = FP_Game_CalcEntityWorldHitBox(game, player->handle); + if (Dqn_Rect_Intersects(top_rect, camera_entity_hit_box)) { + target_pos = bottom_rect.pos; + } else { + target_pos = top_rect.pos; + } - Dqn_ThreadScratch scratch = Dqn_Thread_GetScratch(nullptr); - TELY_AssetFont const *font = TELY_Render_Font(renderer, assets); - Dqn_String8 key_bind_label = Dqn_String8_InitF(scratch.allocator, "[%c]", scan_code_ch); - - Dqn_Rect key_bind_rect = {}; - key_bind_rect.pos = Dqn_Rect_InterpolatedPoint(gamepad_btn_rect, Dqn_V2_InitNx2(-0.1f, 1.1f)); - key_bind_rect.size = TELY_Asset_MeasureText(font, key_bind_label); - key_bind_rect = Dqn_Rect_Expand(key_bind_rect, 2.f); - - TELY_Render_PushColourV4(renderer, TELY_Colour_V4Alpha(colour_accent_yellow, tex_mod_colour.a)); - TELY_Render_RectColourV4(renderer, key_bind_rect, TELY_RenderShapeMode_Fill, TELY_Colour_V4Alpha(keybind_btn_shadow_colour, tex_mod_colour.a)); - TELY_Render_Text(renderer, Dqn_Rect_InterpolatedPoint(key_bind_rect, Dqn_V2_InitNx1(0.5f)), Dqn_V2_InitNx1(0.5f), key_bind_label); - TELY_Render_PopColourV4(renderer); + // NOTE: Interpolate the menu position + *mapping.menu_pos += (target_pos - *mapping.menu_pos) * (24.f * DQN_CAST(Dqn_f32)input->delta_s); } - } - // NOTE: Render the merchant shop item building ================ - { - TELY_AssetSpriteAnimation *anim = TELY_Asset_GetSpriteAnimation(&game->atlas_sprite_sheet, mapping.building); - Dqn_Rect tex_rect = game->atlas_sprite_sheet.rects.data[anim->index]; - Dqn_Rect dest_rect = {}; - dest_rect.size = tex_rect.size * 0.35f; - dest_rect.pos = Dqn_Rect_InterpolatedPoint(merchant_menu_rect, Dqn_V2_InitNx2(0.42f, 0.25f) + mapping.building_offset01); + merchant_menu_rect.size = src_rect.size; + merchant_menu_rect.pos = *mapping.menu_pos; + + // NOTE: Bob the merchant menu + Dqn_f32 sin_t = DQN_SINF(DQN_CAST(Dqn_f32)input->timer_s * 3.f); + merchant_menu_rect.pos.y += sin_t * 4.f; + TELY_Render_TextureColourV4(renderer, - game->atlas_sprite_sheet.tex_handle, - tex_rect, - dest_rect, + sprite->asset.sheet->tex_handle, + src_rect, + merchant_menu_rect, Dqn_V2_Zero /*rotate origin*/, 0.f /*rotation*/, - tex_mod_colour); + 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; + } } - } - // NOTE: Render the merchant button for buildings - { - bool const have_enough_coins = player->coins >= *mapping.upgrade_base_price; - FP_GameKeyBind key_bind = game->controls.buy_upgrade; - // NOTE: Buy trigger + animation =============================== + TELY_Render_PushColourV4(renderer, TELY_COLOUR_BLACK_V4); + TELY_Render_PushFont(renderer, game->talkco_font_large); + DQN_DEFER { + TELY_Render_PopFont(renderer); + TELY_Render_PopColourV4(renderer); + }; + + // NOTE: Render the merchant button for buildings ================== + uint64_t const buy_duration_ms = 500; + Dqn_V4 keybind_btn_shadow_colour = Dqn_V4_InitNx4(0.4f, 0.4f, 0.4f, 1.f); { - bool trigger_buy_anim = false; + bool const have_enough_coins = player->coins >= *mapping.building_base_price; + FP_GameKeyBind key_bind = player->controls.buy_building; + // NOTE: Buy trigger + animation =============================== + { + bool trigger_buy_anim = false; - if (have_enough_coins) { - if (TELY_Platform_InputScanCodeIsPressed(input, key_bind.scan_code)) { - game->play.player_trigger_purchase_upgrade_timestamp = game->play.clock_ms + buy_duration_ms; - } else if (TELY_Platform_InputScanCodeIsDown(input, key_bind.scan_code)) { - trigger_buy_anim = true; - if (game->play.clock_ms > game->play.player_trigger_purchase_upgrade_timestamp) - game->play.player_trigger_purchase_upgrade_timestamp = game->play.clock_ms; - } else if (TELY_Platform_InputScanCodeIsReleased(input, key_bind.scan_code)) { - if (game->play.clock_ms > game->play.player_trigger_purchase_upgrade_timestamp) { - player->coins -= *mapping.upgrade_base_price; - *mapping.upgrade_base_price *= 1; - TELY_Audio_Play(audio, game->audio[FP_GameAudio_Ching], 1.f); + if (have_enough_coins) { + if (TELY_Platform_InputScanCodeIsPressed(input, key_bind.scan_code)) { + game->play.player_trigger_purchase_building_timestamp = game->play.clock_ms + buy_duration_ms; + } else if (TELY_Platform_InputScanCodeIsDown(input, key_bind.scan_code)) { + trigger_buy_anim = true; + if (game->play.clock_ms > game->play.player_trigger_purchase_building_timestamp) + game->play.player_trigger_purchase_building_timestamp = game->play.clock_ms; + } else if (TELY_Platform_InputScanCodeIsReleased(input, key_bind.scan_code)) { + if (game->play.clock_ms > game->play.player_trigger_purchase_building_timestamp) { + if (mapping.inventory_count) { + player->coins -= *mapping.building_base_price; + TELY_Audio_Play(audio, game->audio[FP_GameAudio_Ching], 1.f); + *mapping.building_base_price *= 1; - if (mapping.merchant == game->play.merchant_terry) { - player->base_attack += DQN_CAST(uint32_t)(FP_DEFAULT_DAMAGE * 1.2f); - } else if (mapping.merchant == game->play.merchant_graveyard) { - player->stamina_cap += DQN_CAST(uint32_t)(FP_TERRY_DASH_STAMINA_COST * .5f); - } else if (mapping.merchant == game->play.merchant_gym) { - player->hp_cap += FP_DEFAULT_DAMAGE; - player->hp = player->hp_cap; - } else if (mapping.merchant == game->play.merchant_phone_company) { - player->terry_mobile_data_plan_cap += DQN_KILOBYTES(1); + // NOTE: Raise the prices of everything else + invent->airports_base_price *= 1; + invent->clubs_base_price *= 1; + invent->kennels_base_price *= 1; + invent->churchs_base_price *= 1; + + (*mapping.inventory_count)++; + } + } else { + game->play.player_trigger_purchase_building_timestamp = UINT64_MAX; } - } else { - game->play.player_trigger_purchase_upgrade_timestamp = UINT64_MAX; + } + + if (trigger_buy_anim) { + uint64_t start_buy_time = game->play.player_trigger_purchase_building_timestamp - buy_duration_ms; + uint64_t elapsed_time = game->play.clock_ms - start_buy_time; + + Dqn_f32 buy_t = DQN_MIN(elapsed_time / DQN_CAST(Dqn_f32)buy_duration_ms, 1.f); + Dqn_Rect buy_lerp_rect = {}; + buy_lerp_rect.pos = Dqn_Rect_InterpolatedPoint(merchant_menu_rect, Dqn_V2_InitNx2(0.297f, 0.215f)); + buy_lerp_rect.size.w = (merchant_menu_rect.size.w * 0.38f) * buy_t; + buy_lerp_rect.size.h = merchant_menu_rect.size.h * .611f; + + TELY_Render_RectColourV4(renderer, buy_lerp_rect, TELY_RenderShapeMode_Fill, TELY_Colour_V4Alpha(TELY_COLOUR_BLUE_CADET_V4, 0.5f)); } } - if (trigger_buy_anim) { - uint64_t start_buy_time = game->play.player_trigger_purchase_upgrade_timestamp - buy_duration_ms; - uint64_t elapsed_time = game->play.clock_ms - start_buy_time; + } - Dqn_f32 buy_t = DQN_MIN(elapsed_time / DQN_CAST(Dqn_f32)buy_duration_ms, 1.f); - Dqn_Rect buy_lerp_rect = {}; - buy_lerp_rect.pos = Dqn_Rect_InterpolatedPoint(merchant_menu_rect, Dqn_V2_InitNx2(0.68f, 0.215f)); - buy_lerp_rect.size.w = (merchant_menu_rect.size.w * 0.211f) * buy_t; - buy_lerp_rect.size.h = merchant_menu_rect.size.h * .611f; + // NOTE: Render the (A) button ================================= + Dqn_V4 tex_mod_colour = have_enough_coins ? TELY_COLOUR_WHITE_V4 : TELY_Colour_V4Alpha(TELY_COLOUR_RED_TOMATO_V4, .5f); + { + // NOTE: Render the gamepad button + Dqn_Rect gamepad_btn_rect = {}; + { + TELY_AssetSpriteAnimation *anim = TELY_Asset_GetSpriteAnimation(&game->atlas_sprite_sheet, g_anim_names.merchant_button_a); + Dqn_Rect button_rect = game->atlas_sprite_sheet.rects.data[anim->index]; + gamepad_btn_rect.size = button_rect.size * 1.5f; + gamepad_btn_rect.pos = Dqn_Rect_InterpolatedPoint(merchant_menu_rect, Dqn_V2_InitNx2(0.345f, 0.41f)); + TELY_Render_TextureColourV4(renderer, + game->atlas_sprite_sheet.tex_handle, + button_rect, + gamepad_btn_rect, + Dqn_V2_Zero /*rotate origin*/, + 0.f /*rotation*/, + tex_mod_colour); + } - TELY_Render_RectColourV4(renderer, buy_lerp_rect, TELY_RenderShapeMode_Fill, TELY_Colour_V4Alpha(TELY_COLOUR_RED_TOMATO_V4, 0.5f)); + // NOTE: Render the $ cost + TELY_Render_TextF(renderer, Dqn_Rect_InterpolatedPoint(gamepad_btn_rect, Dqn_V2_InitNx2(0.5f, -0.8f)), Dqn_V2_InitNx2(0.5, 0.f), "$%u", *mapping.building_base_price); + + // NOTE: Render the keyboard binding ======================================= + { + DQN_ASSERT(key_bind.scan_code >= TELY_PlatformInputScanCode_A && key_bind.scan_code <= TELY_PlatformInputScanCode_Z); + char scan_code_ch = DQN_CAST(char)('A' + (key_bind.scan_code - TELY_PlatformInputScanCode_A)); + + Dqn_ThreadScratch scratch = Dqn_Thread_GetScratch(nullptr); + TELY_AssetFont const *font = TELY_Render_Font(renderer, assets); + Dqn_String8 key_bind_label = Dqn_String8_InitF(scratch.allocator, "[%c]", scan_code_ch); + + Dqn_Rect key_bind_rect = {}; + key_bind_rect.size = TELY_Asset_MeasureText(font, key_bind_label); + key_bind_rect.pos = Dqn_Rect_InterpolatedPoint(gamepad_btn_rect, Dqn_V2_InitNx2(-0.1f, 1.1f)); + key_bind_rect = Dqn_Rect_Expand(key_bind_rect, 2.f); + + TELY_Render_PushColourV4(renderer, TELY_Colour_V4Alpha(colour_accent_yellow, tex_mod_colour.a)); + TELY_Render_RectColourV4(renderer, key_bind_rect, TELY_RenderShapeMode_Fill, TELY_Colour_V4Alpha(keybind_btn_shadow_colour, tex_mod_colour.a)); + TELY_Render_Text(renderer, Dqn_Rect_InterpolatedPoint(key_bind_rect, Dqn_V2_InitNx1(0.5f)), Dqn_V2_InitNx1(0.5f), key_bind_label); + TELY_Render_PopColourV4(renderer); } } - } - // NOTE: Render the (B) button ================================= - Dqn_V4 tex_mod_colour = have_enough_coins ? TELY_COLOUR_WHITE_V4 : TELY_Colour_V4Alpha(TELY_COLOUR_RED_TOMATO_V4, .5f); - { - Dqn_V2 interp_pos01 = {}; - Dqn_Rect gamepad_btn_rect = {}; - // NOTE: Render the gamepad button + // NOTE: Render the merchant shop item building ================ { - TELY_AssetSpriteAnimation *anim = TELY_Asset_GetSpriteAnimation(&game->atlas_sprite_sheet, g_anim_names.merchant_button_b); - Dqn_Rect button_rect = game->atlas_sprite_sheet.rects.data[anim->index]; - interp_pos01 = Dqn_V2_InitNx2(0.71f, 0.41f); - gamepad_btn_rect.size = button_rect.size * 1.5f; - gamepad_btn_rect.pos = Dqn_Rect_InterpolatedPoint(merchant_menu_rect, interp_pos01); + TELY_AssetSpriteAnimation *anim = TELY_Asset_GetSpriteAnimation(&game->atlas_sprite_sheet, mapping.building); + Dqn_Rect tex_rect = game->atlas_sprite_sheet.rects.data[anim->index]; + Dqn_Rect dest_rect = {}; + dest_rect.size = tex_rect.size * 0.35f; + dest_rect.pos = Dqn_Rect_InterpolatedPoint(merchant_menu_rect, Dqn_V2_InitNx2(0.45f, 0.25f) + mapping.building_offset01); TELY_Render_TextureColourV4(renderer, game->atlas_sprite_sheet.tex_handle, - button_rect, - gamepad_btn_rect, - Dqn_V2_Zero /*rotate origin*/, - 0.f /*rotation*/, - tex_mod_colour); - - } - - // NOTE: Render the building - { - TELY_AssetSpriteAnimation *anim = TELY_Asset_GetSpriteAnimation(&game->atlas_sprite_sheet, mapping.upgrade_icon); - Dqn_Rect button_rect = game->atlas_sprite_sheet.rects.data[anim->index]; - Dqn_Rect dest_rect = {}; - dest_rect.size = button_rect.size * .75f; - dest_rect.pos = Dqn_Rect_InterpolatedPoint(merchant_menu_rect, interp_pos01 + Dqn_V2_InitNx2(0.12f, 0.15f)) - (dest_rect.size * .5f); - TELY_Render_TextureColourV4(renderer, - game->atlas_sprite_sheet.tex_handle, - button_rect, + tex_rect, dest_rect, Dqn_V2_Zero /*rotate origin*/, 0.f /*rotation*/, tex_mod_colour); } + } - // NOTE: Render the $ cost - TELY_Render_TextF(renderer, Dqn_Rect_InterpolatedPoint(gamepad_btn_rect, Dqn_V2_InitNx2(1.f, -0.8f)), Dqn_V2_InitNx2(0.5, 0.f), "$%u", *mapping.upgrade_base_price); - - // NOTE: Render the keyboard binding ======================================= + // NOTE: Render the merchant button for buildings + { + bool const have_enough_coins = player->coins >= *mapping.upgrade_base_price; + FP_GameKeyBind key_bind = player->controls.buy_upgrade; + // NOTE: Buy trigger + animation =============================== { - DQN_ASSERT(key_bind.scan_code >= TELY_PlatformInputScanCode_A && key_bind.scan_code <= TELY_PlatformInputScanCode_Z); - char scan_code_ch = DQN_CAST(char)('A' + (key_bind.scan_code - TELY_PlatformInputScanCode_A)); + bool trigger_buy_anim = false; - Dqn_ThreadScratch scratch = Dqn_Thread_GetScratch(nullptr); - TELY_AssetFont const *font = TELY_Render_Font(renderer, assets); - Dqn_String8 key_bind_label = Dqn_String8_InitF(scratch.allocator, "[%c]", scan_code_ch); + if (have_enough_coins) { + if (TELY_Platform_InputScanCodeIsPressed(input, key_bind.scan_code)) { + game->play.player_trigger_purchase_upgrade_timestamp = game->play.clock_ms + buy_duration_ms; + } else if (TELY_Platform_InputScanCodeIsDown(input, key_bind.scan_code)) { + trigger_buy_anim = true; + if (game->play.clock_ms > game->play.player_trigger_purchase_upgrade_timestamp) + game->play.player_trigger_purchase_upgrade_timestamp = game->play.clock_ms; + } else if (TELY_Platform_InputScanCodeIsReleased(input, key_bind.scan_code)) { + if (game->play.clock_ms > game->play.player_trigger_purchase_upgrade_timestamp) { + player->coins -= *mapping.upgrade_base_price; + *mapping.upgrade_base_price *= 1; + TELY_Audio_Play(audio, game->audio[FP_GameAudio_Ching], 1.f); - Dqn_Rect key_bind_rect = {}; - key_bind_rect.pos = Dqn_Rect_InterpolatedPoint(gamepad_btn_rect, Dqn_V2_InitNx2(-0.1f, 1.1f)); - key_bind_rect.size = TELY_Asset_MeasureText(font, key_bind_label); - key_bind_rect = Dqn_Rect_Expand(key_bind_rect, 2.f); + if (mapping.merchant == game->play.merchant_terry) { + player->base_attack += DQN_CAST(uint32_t)(FP_DEFAULT_DAMAGE * 1.2f); + } else if (mapping.merchant == game->play.merchant_graveyard) { + player->stamina_cap += DQN_CAST(uint32_t)(FP_TERRY_DASH_STAMINA_COST * .5f); + } else if (mapping.merchant == game->play.merchant_gym) { + player->hp_cap += FP_DEFAULT_DAMAGE; + player->hp = player->hp_cap; + } else if (mapping.merchant == game->play.merchant_phone_company) { + player->terry_mobile_data_plan_cap += DQN_KILOBYTES(1); + } + } else { + game->play.player_trigger_purchase_upgrade_timestamp = UINT64_MAX; + } + } - TELY_Render_PushColourV4(renderer, TELY_Colour_V4Alpha(colour_accent_yellow, tex_mod_colour.a)); - TELY_Render_RectColourV4(renderer, key_bind_rect, TELY_RenderShapeMode_Fill, TELY_Colour_V4Alpha(keybind_btn_shadow_colour, tex_mod_colour.a)); - TELY_Render_Text(renderer, Dqn_Rect_InterpolatedPoint(key_bind_rect, Dqn_V2_InitNx1(0.5f)), Dqn_V2_InitNx1(0.5f), key_bind_label); - TELY_Render_PopColourV4(renderer); + if (trigger_buy_anim) { + uint64_t start_buy_time = game->play.player_trigger_purchase_upgrade_timestamp - buy_duration_ms; + uint64_t elapsed_time = game->play.clock_ms - start_buy_time; + + Dqn_f32 buy_t = DQN_MIN(elapsed_time / DQN_CAST(Dqn_f32)buy_duration_ms, 1.f); + Dqn_Rect buy_lerp_rect = {}; + buy_lerp_rect.pos = Dqn_Rect_InterpolatedPoint(merchant_menu_rect, Dqn_V2_InitNx2(0.68f, 0.215f)); + buy_lerp_rect.size.w = (merchant_menu_rect.size.w * 0.211f) * buy_t; + buy_lerp_rect.size.h = merchant_menu_rect.size.h * .611f; + + TELY_Render_RectColourV4(renderer, buy_lerp_rect, TELY_RenderShapeMode_Fill, TELY_Colour_V4Alpha(TELY_COLOUR_RED_TOMATO_V4, 0.5f)); + } + } } + + // NOTE: Render the (B) button ================================= + Dqn_V4 tex_mod_colour = have_enough_coins ? TELY_COLOUR_WHITE_V4 : TELY_Colour_V4Alpha(TELY_COLOUR_RED_TOMATO_V4, .5f); + { + Dqn_V2 interp_pos01 = {}; + Dqn_Rect gamepad_btn_rect = {}; + // NOTE: Render the gamepad button + { + TELY_AssetSpriteAnimation *anim = TELY_Asset_GetSpriteAnimation(&game->atlas_sprite_sheet, g_anim_names.merchant_button_b); + Dqn_Rect button_rect = game->atlas_sprite_sheet.rects.data[anim->index]; + interp_pos01 = Dqn_V2_InitNx2(0.71f, 0.41f); + gamepad_btn_rect.size = button_rect.size * 1.5f; + gamepad_btn_rect.pos = Dqn_Rect_InterpolatedPoint(merchant_menu_rect, interp_pos01); + TELY_Render_TextureColourV4(renderer, + game->atlas_sprite_sheet.tex_handle, + button_rect, + gamepad_btn_rect, + Dqn_V2_Zero /*rotate origin*/, + 0.f /*rotation*/, + tex_mod_colour); + + } + + // NOTE: Render the building + { + TELY_AssetSpriteAnimation *anim = TELY_Asset_GetSpriteAnimation(&game->atlas_sprite_sheet, mapping.upgrade_icon); + Dqn_Rect button_rect = game->atlas_sprite_sheet.rects.data[anim->index]; + Dqn_Rect dest_rect = {}; + dest_rect.size = button_rect.size * .75f; + dest_rect.pos = Dqn_Rect_InterpolatedPoint(merchant_menu_rect, interp_pos01 + Dqn_V2_InitNx2(0.12f, 0.15f)) - (dest_rect.size * .5f); + TELY_Render_TextureColourV4(renderer, + game->atlas_sprite_sheet.tex_handle, + button_rect, + dest_rect, + Dqn_V2_Zero /*rotate origin*/, + 0.f /*rotation*/, + tex_mod_colour); + } + + // NOTE: Render the $ cost + TELY_Render_TextF(renderer, Dqn_Rect_InterpolatedPoint(gamepad_btn_rect, Dqn_V2_InitNx2(1.f, -0.8f)), Dqn_V2_InitNx2(0.5, 0.f), "$%u", *mapping.upgrade_base_price); + + // NOTE: Render the keyboard binding ======================================= + { + DQN_ASSERT(key_bind.scan_code >= TELY_PlatformInputScanCode_A && key_bind.scan_code <= TELY_PlatformInputScanCode_Z); + char scan_code_ch = DQN_CAST(char)('A' + (key_bind.scan_code - TELY_PlatformInputScanCode_A)); + + Dqn_ThreadScratch scratch = Dqn_Thread_GetScratch(nullptr); + TELY_AssetFont const *font = TELY_Render_Font(renderer, assets); + Dqn_String8 key_bind_label = Dqn_String8_InitF(scratch.allocator, "[%c]", scan_code_ch); + + Dqn_Rect key_bind_rect = {}; + key_bind_rect.pos = Dqn_Rect_InterpolatedPoint(gamepad_btn_rect, Dqn_V2_InitNx2(-0.1f, 1.1f)); + key_bind_rect.size = TELY_Asset_MeasureText(font, key_bind_label); + key_bind_rect = Dqn_Rect_Expand(key_bind_rect, 2.f); + + TELY_Render_PushColourV4(renderer, TELY_Colour_V4Alpha(colour_accent_yellow, tex_mod_colour.a)); + TELY_Render_RectColourV4(renderer, key_bind_rect, TELY_RenderShapeMode_Fill, TELY_Colour_V4Alpha(keybind_btn_shadow_colour, tex_mod_colour.a)); + TELY_Render_Text(renderer, Dqn_Rect_InterpolatedPoint(key_bind_rect, Dqn_V2_InitNx1(0.5f)), Dqn_V2_InitNx1(0.5f), key_bind_label); + TELY_Render_PopColourV4(renderer); + } + } + } + } + + if (activated_merchant) { + player->in_game_menu = FP_GameInGameMenu_Merchant; + } else { + if (player->in_game_menu == FP_GameInGameMenu_Merchant) { + player->in_game_menu = FP_GameInGameMenu_Nil; } } } - if (activated_merchant) { - game->play.in_game_menu = FP_GameInGameMenu_Merchant; + // NOTE: Render player avatar HUD ========================================================== + Dqn_Rect player_avatar_rect = {}; + if (player_index) { + player_avatar_rect.pos = Dqn_V2_InitNx2(platform->core.window_size.x - 320.f, 32.f); } else { - if (game->play.in_game_menu == FP_GameInGameMenu_Merchant) { - game->play.in_game_menu = FP_GameInGameMenu_Nil; - } + player_avatar_rect.pos = Dqn_V2_InitNx1(32.f); } - } - // NOTE: Render player avatar HUD ========================================================== - Dqn_Rect player_avatar_rect = {}; - player_avatar_rect.pos = Dqn_V2_InitNx1(32.f); - Dqn_V2 next_pos = {}; - { - TELY_Render_PushTransform(renderer, Dqn_M2x3_Identity()); - DQN_DEFER { TELY_Render_PopTransform(renderer); }; - - FP_EntityRenderData render_data = FP_Entity_GetRenderData(game, FP_EntityType_Terry, FP_EntityTerryState_Idle, FP_GameDirection_Down); - player_avatar_rect.size = render_data.render_size; - - TELY_Render_TextureColourV4(renderer, - render_data.sheet->tex_handle, - render_data.sheet_rect, - player_avatar_rect, - Dqn_V2_Zero, - 0.f, - TELY_COLOUR_WHITE_V4); - - TELY_Render_PushFont(renderer, game->talkco_font); - DQN_DEFER { TELY_Render_PopFont(renderer); }; - - next_pos = Dqn_Rect_InterpolatedPoint(player_avatar_rect, Dqn_V2_InitNx2(1.f, 0)); - Dqn_f32 font_height = TELY_Render_FontHeight(renderer, &platform->assets); - - // NOTE: Health bar ==================================================== - Dqn_f32 bar_height = font_height * .75f; - Dqn_Rect health_icon_rect = {}; + Dqn_V2 next_pos = {}; { - 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]; - { - health_icon_rect.size = icon_tex_rect.size * .4f; - health_icon_rect.pos = Dqn_V2_InitNx2(next_pos.x - health_icon_rect.size.x * .25f, next_pos.y - (health_icon_rect.size.y * .35f)); - } + TELY_Render_PushTransform(renderer, Dqn_M2x3_Identity()); + DQN_DEFER { TELY_Render_PopTransform(renderer); }; - Dqn_f32 bar_x = next_pos.x + (health_icon_rect.size.x * .25f); - Dqn_f32 health_t = player->hp / DQN_CAST(Dqn_f32)player->hp_cap; - Dqn_Rect health_rect = Dqn_Rect_InitNx4(bar_x, next_pos.y, DQN_CAST(Dqn_f32)player->hp_cap, bar_height); - Dqn_Rect curr_health_rect = Dqn_Rect_InitNx4(bar_x, next_pos.y, DQN_CAST(Dqn_f32)player->hp_cap * health_t, bar_height); + FP_EntityRenderData render_data = FP_Entity_GetRenderData(game, FP_EntityType_Terry, FP_EntityTerryState_Idle, FP_GameDirection_Down); + player_avatar_rect.size = render_data.render_size; - 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 shadow TELY_Render_TextureColourV4(renderer, - game->atlas_sprite_sheet.tex_handle, - icon_tex_rect, - Dqn_Rect_InitV2x2(health_icon_rect.pos - (cmd->thickness * 2.f), health_icon_rect.size + (cmd->thickness * 4.f)), - Dqn_V2_Zero /*rotate origin*/, - 0.f /*rotation*/, - TELY_COLOUR_BLACK_V4); - - // NOTE: Draw the heart icon - TELY_Render_TextureColourV4(renderer, - game->atlas_sprite_sheet.tex_handle, - icon_tex_rect, - health_icon_rect, - Dqn_V2_Zero /*rotate origin*/, - 0.f /*rotation*/, + render_data.sheet->tex_handle, + render_data.sheet_rect, + player_avatar_rect, + Dqn_V2_Zero, + 0.f, TELY_COLOUR_WHITE_V4); - } - // NOTE: Stamina bar =================================================== - next_pos.y += health_icon_rect.size.h * .8f; - Dqn_Rect stamina_icon_rect = {}; - { - TELY_AssetSpriteAnimation *anim = TELY_Asset_GetSpriteAnimation(&game->atlas_sprite_sheet, g_anim_names.icon_stamina); - Dqn_Rect icon_tex_rect = game->atlas_sprite_sheet.rects.data[anim->index]; + TELY_Render_PushFont(renderer, game->talkco_font); + DQN_DEFER { TELY_Render_PopFont(renderer); }; + + next_pos = Dqn_Rect_InterpolatedPoint(player_avatar_rect, Dqn_V2_InitNx2(1.f, 0)); + Dqn_f32 font_height = TELY_Render_FontHeight(renderer, &platform->assets); + + // NOTE: Health bar ==================================================== + Dqn_f32 bar_height = font_height * .75f; + Dqn_Rect health_icon_rect = {}; { - stamina_icon_rect.size = icon_tex_rect.size * .35f; - stamina_icon_rect.pos = Dqn_V2_InitNx2(next_pos.x - stamina_icon_rect.size.x * .25f, next_pos.y - (stamina_icon_rect.size.y * .35f)); + 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]; + { + health_icon_rect.size = icon_tex_rect.size * .4f; + health_icon_rect.pos = Dqn_V2_InitNx2(next_pos.x - health_icon_rect.size.x * .25f, next_pos.y - (health_icon_rect.size.y * .35f)); + } + + Dqn_f32 bar_x = next_pos.x + (health_icon_rect.size.x * .25f); + Dqn_f32 health_t = player->hp / DQN_CAST(Dqn_f32)player->hp_cap; + Dqn_Rect health_rect = Dqn_Rect_InitNx4(bar_x, next_pos.y, DQN_CAST(Dqn_f32)player->hp_cap, bar_height); + Dqn_Rect curr_health_rect = Dqn_Rect_InitNx4(bar_x, next_pos.y, DQN_CAST(Dqn_f32)player->hp_cap * 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 shadow + TELY_Render_TextureColourV4(renderer, + game->atlas_sprite_sheet.tex_handle, + icon_tex_rect, + Dqn_Rect_InitV2x2(health_icon_rect.pos - (cmd->thickness * 2.f), health_icon_rect.size + (cmd->thickness * 4.f)), + Dqn_V2_Zero /*rotate origin*/, + 0.f /*rotation*/, + TELY_COLOUR_BLACK_V4); + + // NOTE: Draw the heart icon + TELY_Render_TextureColourV4(renderer, + game->atlas_sprite_sheet.tex_handle, + icon_tex_rect, + health_icon_rect, + Dqn_V2_Zero /*rotate origin*/, + 0.f /*rotation*/, + TELY_COLOUR_WHITE_V4); } - Dqn_f32 bar_x = next_pos.x + (stamina_icon_rect.size.x * .25f); - Dqn_f32 stamina_t = player->stamina / DQN_CAST(Dqn_f32)player->stamina_cap; - Dqn_Rect stamina_rect = Dqn_Rect_InitNx4(bar_x, next_pos.y, DQN_CAST(Dqn_f32)player->stamina_cap, bar_height); - Dqn_Rect curr_stamina_rect = Dqn_Rect_InitNx4(bar_x, next_pos.y, DQN_CAST(Dqn_f32)player->stamina_cap * stamina_t, bar_height); - TELY_Render_RectColourV4( - renderer, - curr_stamina_rect, - TELY_RenderShapeMode_Fill, - TELY_COLOUR_YELLOW_SANDY_V4); - - TELY_RenderCommandRect *cmd = TELY_Render_RectColourV4( - renderer, - stamina_rect, - TELY_RenderShapeMode_Line, - TELY_COLOUR_BLACK_V4); - cmd->thickness = 4.f; - - // NOTE: Draw the icon shadow - TELY_Render_TextureColourV4(renderer, - game->atlas_sprite_sheet.tex_handle, - icon_tex_rect, - Dqn_Rect_InitV2x2(stamina_icon_rect.pos - (cmd->thickness * 2.f), stamina_icon_rect.size + (cmd->thickness * 4.f)), - Dqn_V2_Zero /*rotate origin*/, - 0.f /*rotation*/, - TELY_COLOUR_BLACK_V4); - - // NOTE: Draw the icon - TELY_Render_TextureColourV4(renderer, - game->atlas_sprite_sheet.tex_handle, - icon_tex_rect, - stamina_icon_rect, - Dqn_V2_Zero /*rotate origin*/, - 0.f /*rotation*/, - TELY_COLOUR_WHITE_V4); - } - - // NOTE: Mobile data bar =================================================== - next_pos.y += stamina_icon_rect.size.h * .8f; - Dqn_Rect phone_icon_rect = {}; - { - TELY_AssetSpriteAnimation *anim = TELY_Asset_GetSpriteAnimation(&game->atlas_sprite_sheet, g_anim_names.icon_phone); - Dqn_Rect icon_tex_rect = game->atlas_sprite_sheet.rects.data[anim->index]; + // NOTE: Stamina bar =================================================== + next_pos.y += health_icon_rect.size.h * .8f; + Dqn_Rect stamina_icon_rect = {}; { - phone_icon_rect.size = icon_tex_rect.size * .35f; - phone_icon_rect.pos = Dqn_V2_InitNx2(next_pos.x - phone_icon_rect.size.x * .25f, next_pos.y - (phone_icon_rect.size.y * .35f)); + TELY_AssetSpriteAnimation *anim = TELY_Asset_GetSpriteAnimation(&game->atlas_sprite_sheet, g_anim_names.icon_stamina); + Dqn_Rect icon_tex_rect = game->atlas_sprite_sheet.rects.data[anim->index]; + { + stamina_icon_rect.size = icon_tex_rect.size * .35f; + stamina_icon_rect.pos = Dqn_V2_InitNx2(next_pos.x - stamina_icon_rect.size.x * .25f, next_pos.y - (stamina_icon_rect.size.y * .35f)); + } + + Dqn_f32 bar_x = next_pos.x + (stamina_icon_rect.size.x * .25f); + Dqn_f32 stamina_t = player->stamina / DQN_CAST(Dqn_f32)player->stamina_cap; + Dqn_Rect stamina_rect = Dqn_Rect_InitNx4(bar_x, next_pos.y, DQN_CAST(Dqn_f32)player->stamina_cap, bar_height); + Dqn_Rect curr_stamina_rect = Dqn_Rect_InitNx4(bar_x, next_pos.y, DQN_CAST(Dqn_f32)player->stamina_cap * stamina_t, bar_height); + TELY_Render_RectColourV4( + renderer, + curr_stamina_rect, + TELY_RenderShapeMode_Fill, + TELY_COLOUR_YELLOW_SANDY_V4); + + TELY_RenderCommandRect *cmd = TELY_Render_RectColourV4( + renderer, + stamina_rect, + TELY_RenderShapeMode_Line, + TELY_COLOUR_BLACK_V4); + cmd->thickness = 4.f; + + // NOTE: Draw the icon shadow + TELY_Render_TextureColourV4(renderer, + game->atlas_sprite_sheet.tex_handle, + icon_tex_rect, + Dqn_Rect_InitV2x2(stamina_icon_rect.pos - (cmd->thickness * 2.f), stamina_icon_rect.size + (cmd->thickness * 4.f)), + Dqn_V2_Zero /*rotate origin*/, + 0.f /*rotation*/, + TELY_COLOUR_BLACK_V4); + + // NOTE: Draw the icon + TELY_Render_TextureColourV4(renderer, + game->atlas_sprite_sheet.tex_handle, + icon_tex_rect, + stamina_icon_rect, + Dqn_V2_Zero /*rotate origin*/, + 0.f /*rotation*/, + TELY_COLOUR_WHITE_V4); } - Dqn_f32 pixels_per_kb = 15.5f; - Dqn_f32 bar_width = DQN_CAST(Dqn_f32)player->terry_mobile_data_plan_cap / DQN_KILOBYTES(1) * pixels_per_kb; - - Dqn_f32 bar_x = next_pos.x + (phone_icon_rect.size.x * .25f); - Dqn_f32 data_plan_t = player->terry_mobile_data_plan / DQN_CAST(Dqn_f32)player->terry_mobile_data_plan_cap; - Dqn_Rect data_plan_rect = Dqn_Rect_InitNx4(bar_x, next_pos.y, bar_width, bar_height); - Dqn_Rect curr_data_plan_rect = Dqn_Rect_InitNx4(bar_x, next_pos.y, bar_width * data_plan_t, bar_height); - TELY_Render_RectColourV4( - renderer, - curr_data_plan_rect, - TELY_RenderShapeMode_Fill, - TELY_COLOUR_BLUE_CADET_V4); - - TELY_RenderCommandRect *cmd = TELY_Render_RectColourV4( - renderer, - data_plan_rect, - TELY_RenderShapeMode_Line, - TELY_COLOUR_BLACK_V4); - cmd->thickness = 4.f; - - // NOTE: Draw the icon shadow - TELY_Render_TextureColourV4(renderer, - game->atlas_sprite_sheet.tex_handle, - icon_tex_rect, - Dqn_Rect_InitV2x2(phone_icon_rect.pos - (cmd->thickness * 2.f), phone_icon_rect.size + (cmd->thickness * 4.f)), - Dqn_V2_Zero /*rotate origin*/, - 0.f /*rotation*/, - TELY_COLOUR_BLACK_V4); - - // NOTE: Draw the icon - TELY_Render_TextureColourV4(renderer, - game->atlas_sprite_sheet.tex_handle, - icon_tex_rect, - phone_icon_rect, - Dqn_V2_Zero /*rotate origin*/, - 0.f /*rotation*/, - TELY_COLOUR_WHITE_V4); - } - - next_pos.y += phone_icon_rect.size.h * .8f; - Dqn_Rect money_icon_rect = {}; - { - TELY_AssetSpriteAnimation *anim = TELY_Asset_GetSpriteAnimation(&game->atlas_sprite_sheet, g_anim_names.icon_money); - Dqn_Rect icon_tex_rect = game->atlas_sprite_sheet.rects.data[anim->index]; + // NOTE: Mobile data bar =================================================== + next_pos.y += stamina_icon_rect.size.h * .8f; + Dqn_Rect phone_icon_rect = {}; { - money_icon_rect.size = icon_tex_rect.size * .35f; - money_icon_rect.pos = Dqn_V2_InitNx2(next_pos.x - money_icon_rect.size.x * .25f, next_pos.y - (money_icon_rect.size.y * .15f)); + TELY_AssetSpriteAnimation *anim = TELY_Asset_GetSpriteAnimation(&game->atlas_sprite_sheet, g_anim_names.icon_phone); + Dqn_Rect icon_tex_rect = game->atlas_sprite_sheet.rects.data[anim->index]; + { + phone_icon_rect.size = icon_tex_rect.size * .35f; + phone_icon_rect.pos = Dqn_V2_InitNx2(next_pos.x - phone_icon_rect.size.x * .25f, next_pos.y - (phone_icon_rect.size.y * .35f)); + } + + Dqn_f32 pixels_per_kb = 15.5f; + Dqn_f32 bar_width = DQN_CAST(Dqn_f32)player->terry_mobile_data_plan_cap / DQN_KILOBYTES(1) * pixels_per_kb; + + Dqn_f32 bar_x = next_pos.x + (phone_icon_rect.size.x * .25f); + Dqn_f32 data_plan_t = player->terry_mobile_data_plan / DQN_CAST(Dqn_f32)player->terry_mobile_data_plan_cap; + Dqn_Rect data_plan_rect = Dqn_Rect_InitNx4(bar_x, next_pos.y, bar_width, bar_height); + Dqn_Rect curr_data_plan_rect = Dqn_Rect_InitNx4(bar_x, next_pos.y, bar_width * data_plan_t, bar_height); + TELY_Render_RectColourV4( + renderer, + curr_data_plan_rect, + TELY_RenderShapeMode_Fill, + TELY_COLOUR_BLUE_CADET_V4); + + TELY_RenderCommandRect *cmd = TELY_Render_RectColourV4( + renderer, + data_plan_rect, + TELY_RenderShapeMode_Line, + TELY_COLOUR_BLACK_V4); + cmd->thickness = 4.f; + + // NOTE: Draw the icon shadow + TELY_Render_TextureColourV4(renderer, + game->atlas_sprite_sheet.tex_handle, + icon_tex_rect, + Dqn_Rect_InitV2x2(phone_icon_rect.pos - (cmd->thickness * 2.f), phone_icon_rect.size + (cmd->thickness * 4.f)), + Dqn_V2_Zero /*rotate origin*/, + 0.f /*rotation*/, + TELY_COLOUR_BLACK_V4); + + // NOTE: Draw the icon + TELY_Render_TextureColourV4(renderer, + game->atlas_sprite_sheet.tex_handle, + icon_tex_rect, + phone_icon_rect, + Dqn_V2_Zero /*rotate origin*/, + 0.f /*rotation*/, + TELY_COLOUR_WHITE_V4); } - TELY_Render_TextF(renderer, - Dqn_V2_InitNx2(next_pos.x + money_icon_rect.size.x * .75f, money_icon_rect.pos.y), - Dqn_V2_InitNx2(0, -0.5), - "$%zu", - player->coins); + next_pos.y += phone_icon_rect.size.h * .8f; + Dqn_Rect money_icon_rect = {}; + { + TELY_AssetSpriteAnimation *anim = TELY_Asset_GetSpriteAnimation(&game->atlas_sprite_sheet, g_anim_names.icon_money); + Dqn_Rect icon_tex_rect = game->atlas_sprite_sheet.rects.data[anim->index]; + { + money_icon_rect.size = icon_tex_rect.size * .35f; + money_icon_rect.pos = Dqn_V2_InitNx2(next_pos.x - money_icon_rect.size.x * .25f, next_pos.y - (money_icon_rect.size.y * .15f)); + } - // NOTE: Draw the icon shadow - Dqn_f32 thickness = 4.f; - TELY_Render_TextureColourV4(renderer, - game->atlas_sprite_sheet.tex_handle, - icon_tex_rect, - Dqn_Rect_InitV2x2(money_icon_rect.pos - (thickness * 2.f), money_icon_rect.size + (thickness * 4.f)), - Dqn_V2_Zero /*rotate origin*/, - 0.f /*rotation*/, - TELY_COLOUR_BLACK_V4); + TELY_Render_TextF(renderer, + Dqn_V2_InitNx2(next_pos.x + money_icon_rect.size.x * .75f, money_icon_rect.pos.y), + Dqn_V2_InitNx2(0, -0.5), + "$%zu", + player->coins); - // NOTE: Draw the icon - TELY_Render_TextureColourV4(renderer, - game->atlas_sprite_sheet.tex_handle, - icon_tex_rect, - money_icon_rect, - Dqn_V2_Zero /*rotate origin*/, - 0.f /*rotation*/, - TELY_COLOUR_WHITE_V4); - } + // NOTE: Draw the icon shadow + Dqn_f32 thickness = 4.f; + TELY_Render_TextureColourV4(renderer, + game->atlas_sprite_sheet.tex_handle, + icon_tex_rect, + Dqn_Rect_InitV2x2(money_icon_rect.pos - (thickness * 2.f), money_icon_rect.size + (thickness * 4.f)), + Dqn_V2_Zero /*rotate origin*/, + 0.f /*rotation*/, + TELY_COLOUR_BLACK_V4); - next_pos.y += money_icon_rect.size.h; - - #if 0 - next_pos.y += font_height; - TELY_Render_TextF(renderer, next_pos, Dqn_V2_Zero, "[H] Build Menu"); - - next_pos.y += font_height; - TELY_Render_TextF(renderer, next_pos, Dqn_V2_Zero, "[Shift+WASD] Strafe"); - - next_pos.y += font_height; - TELY_Render_TextF(renderer, next_pos, Dqn_V2_Zero, "[Ctrl+WASD] Dash"); - - next_pos.y += font_height; - TELY_Render_TextF(renderer, next_pos, Dqn_V2_Zero, "[J|K] Melee/Range"); - #endif - } - - // NOTE: Render the wave =================================================================== - { - TELY_Render_PushTransform(renderer, Dqn_M2x3_Identity()); - DQN_DEFER { TELY_Render_PopTransform(renderer); }; - - TELY_Render_PushFont(renderer, game->talkco_font_large); - DQN_DEFER { TELY_Render_PopFont(renderer); }; - - uint64_t time_until_next_wave_ms = 0; - if (game->play.enemies_spawned_this_wave >= game->play.enemies_per_wave) { - if (game->play.wave_cooldown_timestamp_ms > game->play.clock_ms) { - time_until_next_wave_ms = game->play.wave_cooldown_timestamp_ms - game->play.clock_ms; + // NOTE: Draw the icon + TELY_Render_TextureColourV4(renderer, + game->atlas_sprite_sheet.tex_handle, + icon_tex_rect, + money_icon_rect, + Dqn_V2_Zero /*rotate origin*/, + 0.f /*rotation*/, + TELY_COLOUR_WHITE_V4); } + + next_pos.y += money_icon_rect.size.h; + + #if 0 + next_pos.y += font_height; + TELY_Render_TextF(renderer, next_pos, Dqn_V2_Zero, "[H] Build Menu"); + + next_pos.y += font_height; + TELY_Render_TextF(renderer, next_pos, Dqn_V2_Zero, "[Shift+WASD] Strafe"); + + next_pos.y += font_height; + TELY_Render_TextF(renderer, next_pos, Dqn_V2_Zero, "[Ctrl+WASD] Dash"); + + next_pos.y += font_height; + TELY_Render_TextF(renderer, next_pos, Dqn_V2_Zero, "[J|K] Melee/Range"); + #endif + + if (player_index == 0) + first_player_avatar_rect = player_avatar_rect; } - Dqn_f32 mid_x = platform->core.window_size.x * .5f; - if (time_until_next_wave_ms) { - TELY_Render_TextF(renderer, - Dqn_V2_InitNx2(mid_x, player_avatar_rect.pos.y), - Dqn_V2_InitNx1(0.5f), - "%.1fs remaining until Wave %u!", - (time_until_next_wave_ms / 1000.f), game->play.current_wave + 1); - } else { - TELY_Render_TextF(renderer, Dqn_V2_InitNx2(mid_x, player_avatar_rect.pos.y), Dqn_V2_InitNx1(0.5f), "Wave %u", game->play.current_wave); - } - } - - if (!FP_Game_IsNilEntityHandle(game, game->play.clicked_entity)) { // NOTE: Render building blueprint ===================================================== - if (game->play.in_game_menu == FP_GameInGameMenu_Build) { - FP_GameEntity *entity = FP_Game_GetEntity(game, game->play.clicked_entity); - FP_GamePlaceableBuilding placeable_building = PLACEABLE_BUILDINGS[game->play.build_mode_building_index]; - FP_EntityRenderData render_data = FP_Entity_GetRenderData(game, placeable_building.type, placeable_building.state, entity->direction); - Dqn_Rect dest_rect = FP_Game_GetBuildingPlacementRectForEntity(game, placeable_building, entity->handle); + if (player->in_game_menu == FP_GameInGameMenu_Build) { + FP_GamePlaceableBuilding placeable_building = PLACEABLE_BUILDINGS[player->build_mode_building_index]; + FP_EntityRenderData render_data = FP_Entity_GetRenderData(game, placeable_building.type, placeable_building.state, player->direction); + Dqn_Rect dest_rect = FP_Game_GetBuildingPlacementRectForEntity(game, placeable_building, player->handle); - Dqn_V4 colour = game->play.build_mode_can_place_building ? + Dqn_V4 colour = player->build_mode_can_place_building ? TELY_Colour_V4Alpha(TELY_COLOUR_WHITE_V4, 0.5f) : TELY_Colour_V4Alpha(TELY_COLOUR_RED_V4, 0.5f); @@ -3319,7 +3198,7 @@ void FP_Render(FP_Game *game, TELY_Platform *platform, TELY_Renderer *renderer, { TELY_Render_PushTransform(renderer, Dqn_M2x3_Identity()); DQN_DEFER { TELY_Render_PopTransform(renderer); }; - game->play.build_mode_building_index = DQN_CLAMP(game->play.build_mode_building_index, 0, DQN_ARRAY_UCOUNT(PLACEABLE_BUILDINGS) - 1); + player->build_mode_building_index = DQN_CLAMP(player->build_mode_building_index, 0, DQN_ARRAY_UCOUNT(PLACEABLE_BUILDINGS) - 1); Dqn_f32 building_ui_size = 64.f; Dqn_f32 padding = 10.f; @@ -3334,7 +3213,7 @@ void FP_Render(FP_Game *game, TELY_Platform *platform, TELY_Renderer *renderer, Dqn_V4 texture_colour = TELY_Colour_V4Alpha(TELY_COLOUR_WHITE_V4, .5f); Dqn_V4 outline_colour = TELY_COLOUR_WHITE_PALE_GOLDENROD_V4; - if (game->play.build_mode_building_index == building_index) { + if (player->build_mode_building_index == building_index) { outline_colour = TELY_COLOUR_RED_TOMATO_V4; texture_colour.a = 1.f; } @@ -3370,6 +3249,33 @@ void FP_Render(FP_Game *game, TELY_Platform *platform, TELY_Renderer *renderer, } } + // NOTE: Render the wave =================================================================== + { + TELY_Render_PushTransform(renderer, Dqn_M2x3_Identity()); + DQN_DEFER { TELY_Render_PopTransform(renderer); }; + + TELY_Render_PushFont(renderer, game->talkco_font_large); + DQN_DEFER { TELY_Render_PopFont(renderer); }; + + uint64_t time_until_next_wave_ms = 0; + if (game->play.enemies_spawned_this_wave >= game->play.enemies_per_wave) { + if (game->play.wave_cooldown_timestamp_ms > game->play.clock_ms) { + time_until_next_wave_ms = game->play.wave_cooldown_timestamp_ms - game->play.clock_ms; + } + } + + Dqn_f32 mid_x = platform->core.window_size.x * .5f; + if (time_until_next_wave_ms) { + TELY_Render_TextF(renderer, + Dqn_V2_InitNx2(mid_x, first_player_avatar_rect.pos.y), + Dqn_V2_InitNx1(0.5f), + "%.1fs remaining until Wave %u!", + (time_until_next_wave_ms / 1000.f), game->play.current_wave + 1); + } else { + TELY_Render_TextF(renderer, Dqn_V2_InitNx2(mid_x, first_player_avatar_rect.pos.y), Dqn_V2_InitNx1(0.5f), "Wave %u", game->play.current_wave); + } + } + // NOTE: Render the heart health =========================================================== { TELY_Render_PushTransform(renderer, Dqn_M2x3_Identity()); @@ -3381,7 +3287,7 @@ void FP_Render(FP_Game *game, TELY_Platform *platform, TELY_Renderer *renderer, 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_V2 draw_p = Dqn_V2_InitNx2(platform->core.window_size.x * .25f, first_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); @@ -3416,6 +3322,11 @@ void FP_Render(FP_Game *game, TELY_Platform *platform, TELY_Renderer *renderer, } } + FP_GameCamera *camera = &game->play.camera; + Dqn_Rect camera_view_rect = Dqn_Rect_InitV2x2(camera->world_pos - (camera->size * .5f) * camera->scale, camera->size * camera->scale); + TELY_Render_RectColourV4(renderer, Dqn_Rect_Expand(camera_view_rect, -5.f), TELY_RenderShapeMode_Line, TELY_COLOUR_RED_V4); + + // NOTE: Add scanlines into the game for A E S T H E T I C S =================================== if (game->play.state == FP_GameState_Play) { Dqn_V2 screen_size = Dqn_V2_InitNx2(platform->core.window_size.w, platform->core.window_size.h); @@ -3721,7 +3632,6 @@ void FP_Render(FP_Game *game, TELY_Platform *platform, TELY_Renderer *renderer, TELY_Render_PushTransform(renderer, Dqn_M2x3_Identity()); DQN_DEFER { TELY_Render_PopTransform(renderer); }; - FP_GameEntity *player = FP_Game_GetEntity(game, game->play.player); Dqn_V2 draw_p = Dqn_V2_InitNx2(32.f, platform->core.window_size.h * .5f); TELY_Render_PushFont(renderer, game->inter_regular_font); TELY_Render_TextF(renderer, draw_p, Dqn_V2_Zero, "DEBUG MENU"); draw_p.y += TELY_Render_FontHeight(renderer, assets); @@ -3734,10 +3644,8 @@ void FP_Render(FP_Game *game, TELY_Platform *platform, TELY_Renderer *renderer, TELY_Render_TextF(renderer, draw_p, Dqn_V2_Zero, " F7 Increase stamina"); draw_p.y += TELY_Render_FontHeight(renderer, assets); TELY_Render_TextF(renderer, draw_p, Dqn_V2_Zero, " F8 Increase mobile data"); draw_p.y += TELY_Render_FontHeight(renderer, assets); TELY_Render_TextF(renderer, draw_p, Dqn_V2_Zero, " F9 %s god mode", game->play.god_mode ? "Disable" : "Enable"); draw_p.y += TELY_Render_FontHeight(renderer, assets); - TELY_Render_TextF(renderer, draw_p, Dqn_V2_Zero, " F10 %s noclip", player->flags & FP_GameEntityFlag_NoClip ? "Disable" : "Enable"); draw_p.y += TELY_Render_FontHeight(renderer, assets); TELY_Render_TextF(renderer, draw_p, Dqn_V2_Zero, " F11 Building inventory +1"); draw_p.y += TELY_Render_FontHeight(renderer, assets); TELY_Render_TextF(renderer, draw_p, Dqn_V2_Zero, " 1 %s HUD", game->play.debug_hide_hud ? "Show" : "Hide"); draw_p.y += TELY_Render_FontHeight(renderer, assets); - TELY_Render_TextF(renderer, draw_p, Dqn_V2_Zero, " 2 %s by enemies", player->faction == FP_GameEntityFaction_Nil ? "Attacked" : "Ignored"); draw_p.y += TELY_Render_FontHeight(renderer, assets); TELY_Render_PopFont(renderer); TELY_Render_PopColourV4(renderer); @@ -3745,8 +3653,10 @@ void FP_Render(FP_Game *game, TELY_Platform *platform, TELY_Renderer *renderer, game->play.debug_ui = !game->play.debug_ui; if (TELY_Platform_InputScanCodeIsPressed(input, TELY_PlatformInputScanCode_F2)) { - if (!FP_Game_IsNilEntity(player)) + for (FP_GameEntityHandle player_handle : game->play.players) { + FP_GameEntity *player = FP_Game_GetEntity(game, player_handle); player->coins += 10'000; + } } if (TELY_Platform_InputScanCodeIsPressed(input, TELY_PlatformInputScanCode_F3)) @@ -3759,21 +3669,24 @@ void FP_Render(FP_Game *game, TELY_Platform *platform, TELY_Renderer *renderer, FP_PlayReset(game, platform); if (TELY_Platform_InputScanCodeIsPressed(input, TELY_PlatformInputScanCode_F6)) { - if (!FP_Game_IsNilEntity(player)) { + for (FP_GameEntityHandle player_handle : game->play.players) { + FP_GameEntity *player = FP_Game_GetEntity(game, player_handle); player->hp_cap += FP_DEFAULT_DAMAGE; - player->hp = player->hp_cap; + player->hp = player->hp_cap; } } if (TELY_Platform_InputScanCodeIsPressed(input, TELY_PlatformInputScanCode_F7)) { - if (!FP_Game_IsNilEntity(player)) { + for (FP_GameEntityHandle player_handle : game->play.players) { + FP_GameEntity *player = FP_Game_GetEntity(game, player_handle); player->stamina_cap += DQN_CAST(uint16_t)(FP_TERRY_DASH_STAMINA_COST * .5f); player->stamina = player->stamina_cap; } } if (TELY_Platform_InputScanCodeIsPressed(input, TELY_PlatformInputScanCode_F8)) { - if (!FP_Game_IsNilEntity(player)) { + for (FP_GameEntityHandle player_handle : game->play.players) { + FP_GameEntity *player = FP_Game_GetEntity(game, player_handle); player->terry_mobile_data_plan_cap += DQN_KILOBYTES(1); player->terry_mobile_data_plan = player->terry_mobile_data_plan_cap; } @@ -3782,25 +3695,18 @@ void FP_Render(FP_Game *game, TELY_Platform *platform, TELY_Renderer *renderer, if (TELY_Platform_InputScanCodeIsPressed(input, TELY_PlatformInputScanCode_F9)) game->play.god_mode = !game->play.god_mode; - if (TELY_Platform_InputScanCodeIsPressed(input, TELY_PlatformInputScanCode_F10)) - player->flags ^= FP_GameEntityFlag_NoClip; - if (TELY_Platform_InputScanCodeIsPressed(input, TELY_PlatformInputScanCode_F11)) { - player->inventory.clubs += 1; - player->inventory.airports += 1; - player->inventory.churchs += 1; - player->inventory.kennels += 1; + for (FP_GameEntityHandle player_handle : game->play.players) { + FP_GameEntity *player = FP_Game_GetEntity(game, player_handle); + player->inventory.clubs += 1; + player->inventory.airports += 1; + player->inventory.churchs += 1; + player->inventory.kennels += 1; + } } if (TELY_Platform_InputScanCodeIsPressed(input, TELY_PlatformInputScanCode_1)) game->play.debug_hide_hud = !game->play.debug_hide_hud; - - if (TELY_Platform_InputScanCodeIsPressed(input, TELY_PlatformInputScanCode_2)) { - player->faction = player->faction == FP_GameEntityFaction_Nil - ? FP_GameEntityFaction_Friendly - : FP_GameEntityFaction_Nil; - } - } } @@ -3864,8 +3770,8 @@ void TELY_DLL_FrameUpdate(void *user_data) } for (game->play.delta_s_accumulator += DQN_CAST(Dqn_f32)input->delta_s; - game->play.delta_s_accumulator > PHYSICS_STEP; - game->play.delta_s_accumulator -= PHYSICS_STEP) { + game->play.delta_s_accumulator > FP_GAME_PHYSICS_STEP; + game->play.delta_s_accumulator -= FP_GAME_PHYSICS_STEP) { FP_Update(platform, game, input, audio); } diff --git a/feely_pona_entity.cpp b/feely_pona_entity.cpp new file mode 100644 index 0000000..5faca9b --- /dev/null +++ b/feely_pona_entity.cpp @@ -0,0 +1,292 @@ +#if defined(_CLANGD) + #pragma once + #include "feely_pona_unity.h" +#endif + +static bool FP_Entity_IsBuildingForMobs(FP_GameEntity *entity) +{ + bool result = entity->type == FP_EntityType_AirportTerry || + entity->type == FP_EntityType_ClubTerry || + entity->type == FP_EntityType_ChurchTerry || + entity->type == FP_EntityType_KennelTerry; + return result; +} + +static Dqn_f32 FP_Entity_CalcSpriteScaleForDesiredHeight(FP_Game *game, FP_Meters height, Dqn_Rect sprite_rect) +{ + Dqn_f32 sprite_in_meters = FP_Game_PixelsToMetersNx1(game->play, sprite_rect.size.y); + Dqn_f32 result = height.meters / sprite_in_meters; + return result; +} + +static FP_EntityRenderData FP_Entity_GetRenderData(FP_Game *game, FP_EntityType type, uint32_t raw_state, FP_GameDirection direction) +{ + FP_EntityRenderData result = {}; + switch (type) { + case FP_EntityType_Nil: { + } break; + + case FP_EntityType_Map: { + result.height.meters = 41.9f; + result.anim_name = g_anim_names.map; + } break; + + case FP_EntityType_Terry: { + result.height.meters = 1.8f; + FP_EntityTerryState state = DQN_CAST(FP_EntityTerryState)raw_state; + switch (state) { + case FP_EntityTerryState_Idle: result.anim_name = g_anim_names.terry_walk_idle; break; + + case FP_EntityTerryState_Attack: { + switch (direction) { + case FP_GameDirection_Up: result.anim_name = g_anim_names.terry_attack_up; result.height.meters *= 1.5f; break; + case FP_GameDirection_Down: result.anim_name = g_anim_names.terry_attack_down; result.height.meters *= 1.6f; break; + case FP_GameDirection_Left: result.anim_name = g_anim_names.terry_attack_side; result.height.meters *= 1.5f; break; + case FP_GameDirection_Right: result.anim_name = g_anim_names.terry_attack_side; result.height.meters *= 1.5f; result.flip = TELY_AssetFlip_X; break; + case FP_GameDirection_Count: DQN_INVALID_CODE_PATH; break; + } + } break; + + case FP_EntityTerryState_RangeAttack: { + switch (direction) { + case FP_GameDirection_Up: result.anim_name = g_anim_names.terry_attack_phone_up; result.height.meters *= 1.25f; break; + case FP_GameDirection_Down: result.anim_name = g_anim_names.terry_attack_phone_down; result.height.meters *= 1.25f; break; + case FP_GameDirection_Left: result.anim_name = g_anim_names.terry_attack_phone_side; result.height.meters *= 1.25f; result.flip = TELY_AssetFlip_X; break; + case FP_GameDirection_Right: result.anim_name = g_anim_names.terry_attack_phone_side; result.height.meters *= 1.25f; break; + case FP_GameDirection_Count: DQN_INVALID_CODE_PATH; break; + } + } break; + + case FP_EntityTerryState_Dash: { + switch (direction) { + case FP_GameDirection_Up: result.anim_name = g_anim_names.terry_ghost; break; + case FP_GameDirection_Down: result.anim_name = g_anim_names.terry_ghost; break; + case FP_GameDirection_Left: result.anim_name = g_anim_names.terry_ghost; break; + case FP_GameDirection_Right: result.anim_name = g_anim_names.terry_ghost; result.flip = TELY_AssetFlip_X; break; + case FP_GameDirection_Count: DQN_INVALID_CODE_PATH; break; + } + } break; + + case FP_EntityTerryState_Run: { + switch (direction) { + case FP_GameDirection_Up: result.anim_name = g_anim_names.terry_walk_up; break; + case FP_GameDirection_Down: result.anim_name = g_anim_names.terry_walk_down; break; + case FP_GameDirection_Left: result.anim_name = g_anim_names.terry_walk_left; break; + case FP_GameDirection_Right: result.anim_name = g_anim_names.terry_walk_right; break; + case FP_GameDirection_Count: DQN_INVALID_CODE_PATH; break; + } + } break; + + case FP_EntityTerryState_DeadGhost: { + result.anim_name = g_anim_names.terry_death; break; + } break; + } + } break; + + case FP_EntityType_Smoochie: { + result.height.meters = 1.6f; + FP_EntitySmoochieState state = DQN_CAST(FP_EntitySmoochieState)raw_state; + switch (state) { + case FP_EntitySmoochieState_Idle: result.anim_name = g_anim_names.smoochie_walk_down; break; + case FP_EntitySmoochieState_Attack: result.anim_name = g_anim_names.smoochie_attack_down; break; + case FP_EntitySmoochieState_HurtSide: result.anim_name = g_anim_names.smoochie_hurt_side; result.flip = direction == FP_GameDirection_Right ? TELY_AssetFlip_X : TELY_AssetFlip_No; break; + case FP_EntitySmoochieState_Death: result.anim_name = g_anim_names.smoochie_death; break; + case FP_EntitySmoochieState_Run: { + switch (direction) { + case FP_GameDirection_Up: result.anim_name = g_anim_names.smoochie_walk_up; break; + case FP_GameDirection_Down: result.anim_name = g_anim_names.smoochie_walk_down; break; + case FP_GameDirection_Left: result.anim_name = g_anim_names.smoochie_walk_left; break; + case FP_GameDirection_Right: result.anim_name = g_anim_names.smoochie_walk_right; break; + case FP_GameDirection_Count: DQN_INVALID_CODE_PATH; break; + } + } break; + } + } break; + + case FP_EntityType_MerchantTerry: { + result.height.meters = 3.66f; + FP_EntityMerchantTerryState state = DQN_CAST(FP_EntityMerchantTerryState)raw_state; + switch (state) { + case FP_EntityMerchantTerryState_Idle: result.anim_name = g_anim_names.merchant_terry; break; + } + } break; + + case FP_EntityType_MerchantGraveyard: { + result.height.meters = 3.66f; + FP_EntityMerchantGraveyardState state = DQN_CAST(FP_EntityMerchantGraveyardState)raw_state; + switch (state) { + case FP_EntityMerchantGraveyardState_Idle: result.anim_name = g_anim_names.merchant_graveyard; break; + } + } break; + + case FP_EntityType_MerchantGym: { + result.height.meters = 3.66f; + result.anim_name = g_anim_names.merchant_gym; + FP_EntityMerchantGymState state = DQN_CAST(FP_EntityMerchantGymState)raw_state; + switch (state) { + case FP_EntityMerchantGymState_Idle: result.anim_name = g_anim_names.merchant_gym; break; + } + } break; + + case FP_EntityType_MerchantPhoneCompany: { + result.height.meters = 5.f; + FP_EntityMerchantPhoneCompanyState state = DQN_CAST(FP_EntityMerchantPhoneCompanyState)raw_state; + switch (state) { + case FP_EntityMerchantPhoneCompanyState_Idle: result.anim_name = g_anim_names.merchant_phone_company; break; + } + } break; + + case FP_EntityType_ClubTerry: { + result.height.meters = 4.f; + FP_EntityClubTerryState state = DQN_CAST(FP_EntityClubTerryState)raw_state; + switch (state) { + case FP_EntityClubTerryState_Idle: result.anim_name = g_anim_names.club_terry_dark; break; + case FP_EntityClubTerryState_PartyTime: result.anim_name = g_anim_names.club_terry_alive; break; + } + } break; + + case FP_EntityType_Clinger: { + result.height.meters = 1.6f; + FP_EntityClingerState state = DQN_CAST(FP_EntityClingerState)raw_state; + switch (state) { + case FP_EntityClingerState_Idle: result.anim_name = g_anim_names.clinger_walk_down; break; + + case FP_EntityClingerState_Attack: { + switch (direction) { + case FP_GameDirection_Up: result.anim_name = g_anim_names.clinger_attack_up; break; + case FP_GameDirection_Down: result.anim_name = g_anim_names.clinger_attack_down; break; + case FP_GameDirection_Left: result.anim_name = g_anim_names.clinger_attack_side; result.flip = TELY_AssetFlip_X; break; + case FP_GameDirection_Right: result.anim_name = g_anim_names.clinger_attack_side; break; + case FP_GameDirection_Count: DQN_INVALID_CODE_PATH; break; + } + } break; + + case FP_EntityClingerState_Death: result.anim_name = g_anim_names.clinger_death; break; + + case FP_EntityClingerState_Run: { + switch (direction) { + case FP_GameDirection_Up: result.anim_name = g_anim_names.clinger_walk_up; break; + case FP_GameDirection_Down: result.anim_name = g_anim_names.clinger_walk_down; break; + case FP_GameDirection_Left: result.anim_name = g_anim_names.clinger_walk_down; break; + case FP_GameDirection_Right: result.anim_name = g_anim_names.clinger_walk_down; break; + case FP_GameDirection_Count: DQN_INVALID_CODE_PATH; break; + } + } break; + } + } break; + + case FP_EntityType_Heart: { + result.height.meters = 4.f; + FP_EntityHeartState state = DQN_CAST(FP_EntityHeartState)raw_state; + switch (state) { + case FP_EntityHeartState_Idle: result.anim_name = g_anim_names.heart; break; + } + } break; + + case FP_EntityType_AirportTerry: { + result.height.meters = 4.f; + FP_EntityAirportTerryState state = DQN_CAST(FP_EntityAirportTerryState)raw_state; + switch (state) { + case FP_EntityAirportTerryState_Idle: result.anim_name = g_anim_names.airport_terry; break; + case FP_EntityAirportTerryState_FlyPassenger: result.anim_name = g_anim_names.airport_terry; break; + } + } break; + + case FP_EntityType_Catfish: { + result.height.meters = 1.6f; + FP_EntityCatfishState state = DQN_CAST(FP_EntityCatfishState)raw_state; + switch (state) { + case FP_EntityCatfishState_Idle: result.anim_name = g_anim_names.catfish_walk_down; break; + case FP_EntityCatfishState_Attack: { + switch (direction) { + case FP_GameDirection_Up: result.anim_name = g_anim_names.catfish_attack_up; break; + case FP_GameDirection_Down: result.anim_name = g_anim_names.catfish_attack_down; break; + case FP_GameDirection_Left: result.anim_name = g_anim_names.catfish_attack_side; break; + case FP_GameDirection_Right: result.anim_name = g_anim_names.catfish_attack_side; result.flip = TELY_AssetFlip_X; break; + case FP_GameDirection_Count: DQN_INVALID_CODE_PATH; break; + } + } break; + case FP_EntityCatfishState_Death: result.anim_name = g_anim_names.catfish_death; break; + case FP_EntityCatfishState_Run: { + switch (direction) { + case FP_GameDirection_Up: result.anim_name = g_anim_names.catfish_walk_up; break; + case FP_GameDirection_Down: result.anim_name = g_anim_names.catfish_walk_down; break; + case FP_GameDirection_Left: result.anim_name = g_anim_names.catfish_walk_side; break; + case FP_GameDirection_Right: result.anim_name = g_anim_names.catfish_walk_side; result.flip = TELY_AssetFlip_X; break; + case FP_GameDirection_Count: DQN_INVALID_CODE_PATH; break; + } + } break; + } + } break; + + case FP_EntityType_ChurchTerry: { + result.height.meters = 4.f; + FP_EntityChurchTerryState state = DQN_CAST(FP_EntityChurchTerryState)raw_state; + switch (state) { + case FP_EntityChurchTerryState_Idle: result.anim_name = g_anim_names.church_terry_dark; break; + case FP_EntityChurchTerryState_ConvertPatron: result.anim_name = g_anim_names.church_terry_alive; break; + } + } break; + + case FP_EntityType_KennelTerry: { + result.height.meters = 3.f; + FP_EntityKennelTerryState state = DQN_CAST(FP_EntityKennelTerryState)raw_state; + switch (state) { + case FP_EntityKennelTerryState_Idle: result.anim_name = g_anim_names.kennel_terry; break; + } + } break; + + case FP_EntityType_PhoneMessageProjectile: { + result.height.meters = 1.f; + result.anim_name = g_anim_names.terry_attack_phone_message; + } break; + + case FP_EntityType_AirportTerryPlane: { + result.height.meters = 1.5f; + result.anim_name = g_anim_names.airport_terry_plane; + } break; + + case FP_EntityType_MobSpawner: { + result.height.meters = 3.f; + FP_EntityMobSpawnerState state = DQN_CAST(FP_EntityMobSpawnerState)raw_state; + switch (state) { + case FP_EntityMobSpawnerState_Idle: result.anim_name = g_anim_names.portal; break; + case FP_EntityMobSpawnerState_Shutdown: { + result.anim_name = g_anim_names.portal_break; + result.height.meters = 3.5f; + } break; + } + } break; + + case FP_EntityType_PortalMonkey: { + result.height.meters = 1.f; + result.anim_name = g_anim_names.portal_monk; break; + } break; + + case FP_EntityType_Billboard: { + result.height.meters = 7.5f; + FP_EntityBillboardState state = DQN_CAST(FP_EntityBillboardState)raw_state; + switch (state) { + case FP_EntityBillboardState_Attack: result.anim_name = g_anim_names.map_billboard_attack; break; + case FP_EntityBillboardState_Dash: result.anim_name = g_anim_names.map_billboard_dash; break; + case FP_EntityBillboardState_Monkey: result.anim_name = g_anim_names.map_billboard_monkey; break; + case FP_EntityBillboardState_RangeAttack: result.anim_name = g_anim_names.map_billboard_range_attack; break; + case FP_EntityBillboardState_Strafe: result.anim_name = g_anim_names.map_billboard_strafe; break; + } + } break; + + case FP_EntityType_Count: DQN_INVALID_CODE_PATH; break; + } + + result.sheet = &game->atlas_sprite_sheet; + TELY_AssetSpriteAnimation *sprite_anim = TELY_Asset_GetSpriteAnimation(result.sheet, result.anim_name); + if (sprite_anim) { + result.sheet_rect = result.sheet->rects.data[sprite_anim->index]; + Dqn_f32 size_scale = FP_Entity_CalcSpriteScaleForDesiredHeight(game, result.height, result.sheet_rect); + result.render_size = result.sheet_rect.size * size_scale; + result.sprite = TELY_Asset_MakeAnimatedSprite(result.sheet, result.anim_name, result.flip); + } + + return result; +} + diff --git a/feely_pona_entity_create.cpp b/feely_pona_entity_create.cpp index 5dd1b94..f030fc7 100644 --- a/feely_pona_entity_create.cpp +++ b/feely_pona_entity_create.cpp @@ -3,293 +3,6 @@ #include "feely_pona_unity.h" #endif -static bool FP_Entity_IsBuildingForMobs(FP_GameEntity *entity) -{ - bool result = entity->type == FP_EntityType_AirportTerry || - entity->type == FP_EntityType_ClubTerry || - entity->type == FP_EntityType_ChurchTerry || - entity->type == FP_EntityType_KennelTerry; - return result; -} - -static Dqn_f32 FP_Entity_CalcSpriteScaleForDesiredHeight(FP_Game *game, FP_Meters height, Dqn_Rect sprite_rect) -{ - Dqn_f32 sprite_in_meters = FP_Game_PixelsToMetersNx1(game->play, sprite_rect.size.y); - Dqn_f32 result = height.meters / sprite_in_meters; - return result; -} - -FP_EntityRenderData FP_Entity_GetRenderData(FP_Game *game, FP_EntityType type, uint32_t raw_state, FP_GameDirection direction) -{ - FP_EntityRenderData result = {}; - switch (type) { - case FP_EntityType_Nil: { - } break; - - case FP_EntityType_Map: { - result.height.meters = 41.9f; - result.anim_name = g_anim_names.map; - } break; - - case FP_EntityType_Terry: { - result.height.meters = 1.8f; - FP_EntityTerryState state = DQN_CAST(FP_EntityTerryState)raw_state; - switch (state) { - case FP_EntityTerryState_Idle: result.anim_name = g_anim_names.terry_walk_idle; break; - - case FP_EntityTerryState_Attack: { - switch (direction) { - case FP_GameDirection_Up: result.anim_name = g_anim_names.terry_attack_up; result.height.meters *= 1.5f; break; - case FP_GameDirection_Down: result.anim_name = g_anim_names.terry_attack_down; result.height.meters *= 1.6f; break; - case FP_GameDirection_Left: result.anim_name = g_anim_names.terry_attack_side; result.height.meters *= 1.5f; break; - case FP_GameDirection_Right: result.anim_name = g_anim_names.terry_attack_side; result.height.meters *= 1.5f; result.flip = TELY_AssetFlip_X; break; - case FP_GameDirection_Count: DQN_INVALID_CODE_PATH; break; - } - } break; - - case FP_EntityTerryState_RangeAttack: { - switch (direction) { - case FP_GameDirection_Up: result.anim_name = g_anim_names.terry_attack_phone_up; result.height.meters *= 1.25f; break; - case FP_GameDirection_Down: result.anim_name = g_anim_names.terry_attack_phone_down; result.height.meters *= 1.25f; break; - case FP_GameDirection_Left: result.anim_name = g_anim_names.terry_attack_phone_side; result.height.meters *= 1.25f; result.flip = TELY_AssetFlip_X; break; - case FP_GameDirection_Right: result.anim_name = g_anim_names.terry_attack_phone_side; result.height.meters *= 1.25f; break; - case FP_GameDirection_Count: DQN_INVALID_CODE_PATH; break; - } - } break; - - case FP_EntityTerryState_Dash: { - switch (direction) { - case FP_GameDirection_Up: result.anim_name = g_anim_names.terry_ghost; break; - case FP_GameDirection_Down: result.anim_name = g_anim_names.terry_ghost; break; - case FP_GameDirection_Left: result.anim_name = g_anim_names.terry_ghost; break; - case FP_GameDirection_Right: result.anim_name = g_anim_names.terry_ghost; result.flip = TELY_AssetFlip_X; break; - case FP_GameDirection_Count: DQN_INVALID_CODE_PATH; break; - } - } break; - - case FP_EntityTerryState_Run: { - switch (direction) { - case FP_GameDirection_Up: result.anim_name = g_anim_names.terry_walk_up; break; - case FP_GameDirection_Down: result.anim_name = g_anim_names.terry_walk_down; break; - case FP_GameDirection_Left: result.anim_name = g_anim_names.terry_walk_left; break; - case FP_GameDirection_Right: result.anim_name = g_anim_names.terry_walk_right; break; - case FP_GameDirection_Count: DQN_INVALID_CODE_PATH; break; - } - } break; - - case FP_EntityTerryState_DeadGhost: { - result.anim_name = g_anim_names.terry_death; break; - } break; - } - } break; - - case FP_EntityType_Smoochie: { - result.height.meters = 1.6f; - FP_EntitySmoochieState state = DQN_CAST(FP_EntitySmoochieState)raw_state; - switch (state) { - case FP_EntitySmoochieState_Idle: result.anim_name = g_anim_names.smoochie_walk_down; break; - case FP_EntitySmoochieState_Attack: result.anim_name = g_anim_names.smoochie_attack_down; break; - case FP_EntitySmoochieState_HurtSide: result.anim_name = g_anim_names.smoochie_hurt_side; result.flip = direction == FP_GameDirection_Right ? TELY_AssetFlip_X : TELY_AssetFlip_No; break; - case FP_EntitySmoochieState_Death: result.anim_name = g_anim_names.smoochie_death; break; - case FP_EntitySmoochieState_Run: { - switch (direction) { - case FP_GameDirection_Up: result.anim_name = g_anim_names.smoochie_walk_up; break; - case FP_GameDirection_Down: result.anim_name = g_anim_names.smoochie_walk_down; break; - case FP_GameDirection_Left: result.anim_name = g_anim_names.smoochie_walk_left; break; - case FP_GameDirection_Right: result.anim_name = g_anim_names.smoochie_walk_right; break; - case FP_GameDirection_Count: DQN_INVALID_CODE_PATH; break; - } - } break; - } - } break; - - case FP_EntityType_MerchantTerry: { - result.height.meters = 3.66f; - FP_EntityMerchantTerryState state = DQN_CAST(FP_EntityMerchantTerryState)raw_state; - switch (state) { - case FP_EntityMerchantTerryState_Idle: result.anim_name = g_anim_names.merchant_terry; break; - } - } break; - - case FP_EntityType_MerchantGraveyard: { - result.height.meters = 3.66f; - FP_EntityMerchantGraveyardState state = DQN_CAST(FP_EntityMerchantGraveyardState)raw_state; - switch (state) { - case FP_EntityMerchantGraveyardState_Idle: result.anim_name = g_anim_names.merchant_graveyard; break; - } - } break; - - case FP_EntityType_MerchantGym: { - result.height.meters = 3.66f; - result.anim_name = g_anim_names.merchant_gym; - FP_EntityMerchantGymState state = DQN_CAST(FP_EntityMerchantGymState)raw_state; - switch (state) { - case FP_EntityMerchantGymState_Idle: result.anim_name = g_anim_names.merchant_gym; break; - } - } break; - - case FP_EntityType_MerchantPhoneCompany: { - result.height.meters = 5.f; - FP_EntityMerchantPhoneCompanyState state = DQN_CAST(FP_EntityMerchantPhoneCompanyState)raw_state; - switch (state) { - case FP_EntityMerchantPhoneCompanyState_Idle: result.anim_name = g_anim_names.merchant_phone_company; break; - } - } break; - - case FP_EntityType_ClubTerry: { - result.height.meters = 4.f; - FP_EntityClubTerryState state = DQN_CAST(FP_EntityClubTerryState)raw_state; - switch (state) { - case FP_EntityClubTerryState_Idle: result.anim_name = g_anim_names.club_terry_dark; break; - case FP_EntityClubTerryState_PartyTime: result.anim_name = g_anim_names.club_terry_alive; break; - } - } break; - - case FP_EntityType_Clinger: { - result.height.meters = 1.6f; - FP_EntityClingerState state = DQN_CAST(FP_EntityClingerState)raw_state; - switch (state) { - case FP_EntityClingerState_Idle: result.anim_name = g_anim_names.clinger_walk_down; break; - - case FP_EntityClingerState_Attack: { - switch (direction) { - case FP_GameDirection_Up: result.anim_name = g_anim_names.clinger_attack_up; break; - case FP_GameDirection_Down: result.anim_name = g_anim_names.clinger_attack_down; break; - case FP_GameDirection_Left: result.anim_name = g_anim_names.clinger_attack_side; result.flip = TELY_AssetFlip_X; break; - case FP_GameDirection_Right: result.anim_name = g_anim_names.clinger_attack_side; break; - case FP_GameDirection_Count: DQN_INVALID_CODE_PATH; break; - } - } break; - - case FP_EntityClingerState_Death: result.anim_name = g_anim_names.clinger_death; break; - - case FP_EntityClingerState_Run: { - switch (direction) { - case FP_GameDirection_Up: result.anim_name = g_anim_names.clinger_walk_up; break; - case FP_GameDirection_Down: result.anim_name = g_anim_names.clinger_walk_down; break; - case FP_GameDirection_Left: result.anim_name = g_anim_names.clinger_walk_down; break; - case FP_GameDirection_Right: result.anim_name = g_anim_names.clinger_walk_down; break; - case FP_GameDirection_Count: DQN_INVALID_CODE_PATH; break; - } - } break; - } - } break; - - case FP_EntityType_Heart: { - result.height.meters = 4.f; - FP_EntityHeartState state = DQN_CAST(FP_EntityHeartState)raw_state; - switch (state) { - case FP_EntityHeartState_Idle: result.anim_name = g_anim_names.heart; break; - } - } break; - - case FP_EntityType_AirportTerry: { - result.height.meters = 4.f; - FP_EntityAirportTerryState state = DQN_CAST(FP_EntityAirportTerryState)raw_state; - switch (state) { - case FP_EntityAirportTerryState_Idle: result.anim_name = g_anim_names.airport_terry; break; - case FP_EntityAirportTerryState_FlyPassenger: result.anim_name = g_anim_names.airport_terry; break; - } - } break; - - case FP_EntityType_Catfish: { - result.height.meters = 1.6f; - FP_EntityCatfishState state = DQN_CAST(FP_EntityCatfishState)raw_state; - switch (state) { - case FP_EntityCatfishState_Idle: result.anim_name = g_anim_names.catfish_walk_down; break; - case FP_EntityCatfishState_Attack: { - switch (direction) { - case FP_GameDirection_Up: result.anim_name = g_anim_names.catfish_attack_up; break; - case FP_GameDirection_Down: result.anim_name = g_anim_names.catfish_attack_down; break; - case FP_GameDirection_Left: result.anim_name = g_anim_names.catfish_attack_side; break; - case FP_GameDirection_Right: result.anim_name = g_anim_names.catfish_attack_side; result.flip = TELY_AssetFlip_X; break; - case FP_GameDirection_Count: DQN_INVALID_CODE_PATH; break; - } - } break; - case FP_EntityCatfishState_Death: result.anim_name = g_anim_names.catfish_death; break; - case FP_EntityCatfishState_Run: { - switch (direction) { - case FP_GameDirection_Up: result.anim_name = g_anim_names.catfish_walk_up; break; - case FP_GameDirection_Down: result.anim_name = g_anim_names.catfish_walk_down; break; - case FP_GameDirection_Left: result.anim_name = g_anim_names.catfish_walk_side; break; - case FP_GameDirection_Right: result.anim_name = g_anim_names.catfish_walk_side; result.flip = TELY_AssetFlip_X; break; - case FP_GameDirection_Count: DQN_INVALID_CODE_PATH; break; - } - } break; - } - } break; - - case FP_EntityType_ChurchTerry: { - result.height.meters = 4.f; - FP_EntityChurchTerryState state = DQN_CAST(FP_EntityChurchTerryState)raw_state; - switch (state) { - case FP_EntityChurchTerryState_Idle: result.anim_name = g_anim_names.church_terry_dark; break; - case FP_EntityChurchTerryState_ConvertPatron: result.anim_name = g_anim_names.church_terry_alive; break; - } - } break; - - case FP_EntityType_KennelTerry: { - result.height.meters = 3.f; - FP_EntityKennelTerryState state = DQN_CAST(FP_EntityKennelTerryState)raw_state; - switch (state) { - case FP_EntityKennelTerryState_Idle: result.anim_name = g_anim_names.kennel_terry; break; - } - } break; - - case FP_EntityType_PhoneMessageProjectile: { - result.height.meters = 1.f; - result.anim_name = g_anim_names.terry_attack_phone_message; - } break; - - case FP_EntityType_AirportTerryPlane: { - result.height.meters = 1.5f; - result.anim_name = g_anim_names.airport_terry_plane; - } break; - - case FP_EntityType_MobSpawner: { - result.height.meters = 3.f; - FP_EntityMobSpawnerState state = DQN_CAST(FP_EntityMobSpawnerState)raw_state; - switch (state) { - case FP_EntityMobSpawnerState_Idle: result.anim_name = g_anim_names.portal; break; - case FP_EntityMobSpawnerState_Shutdown: { - result.anim_name = g_anim_names.portal_break; - result.height.meters = 3.5f; - } break; - } - } break; - - case FP_EntityType_PortalMonkey: { - result.height.meters = 1.f; - result.anim_name = g_anim_names.portal_monk; break; - } break; - - case FP_EntityType_Billboard: { - result.height.meters = 7.5f; - FP_EntityBillboardState state = DQN_CAST(FP_EntityBillboardState)raw_state; - switch (state) { - case FP_EntityBillboardState_Attack: result.anim_name = g_anim_names.map_billboard_attack; break; - case FP_EntityBillboardState_Dash: result.anim_name = g_anim_names.map_billboard_dash; break; - case FP_EntityBillboardState_Monkey: result.anim_name = g_anim_names.map_billboard_monkey; break; - case FP_EntityBillboardState_RangeAttack: result.anim_name = g_anim_names.map_billboard_range_attack; break; - case FP_EntityBillboardState_Strafe: result.anim_name = g_anim_names.map_billboard_strafe; break; - } - } break; - - case FP_EntityType_Count: DQN_INVALID_CODE_PATH; break; - } - - result.sheet = &game->atlas_sprite_sheet; - TELY_AssetSpriteAnimation *sprite_anim = TELY_Asset_GetSpriteAnimation(result.sheet, result.anim_name); - if (sprite_anim) { - result.sheet_rect = result.sheet->rects.data[sprite_anim->index]; - Dqn_f32 size_scale = FP_Entity_CalcSpriteScaleForDesiredHeight(game, result.height, result.sheet_rect); - result.render_size = result.sheet_rect.size * size_scale; - result.sprite = TELY_Asset_MakeAnimatedSprite(result.sheet, result.anim_name, result.flip); - } - - return result; -} - static void FP_Entity_AddDebugEditorFlags(FP_Game *game, FP_GameEntityHandle handle) { FP_GameEntity *entity = FP_Game_GetEntity(game, handle); diff --git a/feely_pona_game.cpp b/feely_pona_game.cpp index c6be49d..6b04a42 100644 --- a/feely_pona_game.cpp +++ b/feely_pona_game.cpp @@ -3,14 +3,6 @@ #include "feely_pona_unity.h" #endif -#define FP_Game_MetersToPixelsNx1(game, val) ((val) * (game).meters_to_pixels) -#define FP_Game_MetersToPixelsNx2(game, x, y) (Dqn_V2_InitNx2(x, y) * (game).meters_to_pixels) -#define FP_Game_MetersToPixelsV2(game, xy) (xy * (game).meters_to_pixels) - -#define FP_Game_PixelsToMetersNx1(game, val) ((val) * (1.f/(game).meters_to_pixels)) -#define FP_Game_PixelsToMetersNx2(game, x, y) (Dqn_V2_InitNx2(x, y) * (1.f/(game).meters_to_pixels)) -#define FP_Game_PixelsToMetersV2(game, xy) (xy * (1.f/(game).meters_to_pixels)) - static bool operator==(FP_GameEntityHandle const &lhs, FP_GameEntityHandle const &rhs) { bool result = lhs.id == rhs.id; @@ -23,13 +15,6 @@ static bool operator!=(FP_GameEntityHandle const &lhs, FP_GameEntityHandle const return result; } -// TODO(doyle): Use this -struct FP_GameCameraM2x3 -{ - Dqn_M2x3 model_view; - Dqn_M2x3 view_model; -}; - static FP_GameCameraM2x3 FP_Game_CameraModelViewM2x3(FP_GameCamera camera) { FP_GameCameraM2x3 result = {}; @@ -932,3 +917,215 @@ static void FP_GameRenderScanlines(TELY_Renderer *renderer, TELY_Render_LineColourV4(renderer, start, end, TELY_Colour_V4Alpha(TELY_COLOUR_WHITE_V4, 0.1f), scanline_thickness); } } + +static FP_GameCanMoveToPositionResult FP_Game_CanEntityMoveToPosition(FP_Game *game, FP_GameEntityHandle entity_handle, Dqn_V2 delta_pos) +{ + FP_GameCanMoveToPositionResult result = {}; + FP_GameEntity *entity = FP_Game_GetEntity(game, entity_handle); + if (FP_Game_IsNilEntity(entity)) + return result; + + Dqn_Rect const entity_world_hit_box = FP_Game_CalcEntityWorldHitBox(game, entity->handle); + Dqn_V2 const entity_pos = FP_Game_CalcEntityWorldPos(game, entity->handle); + Dqn_f32 const SENTINEL_T = 999.f; + Dqn_f32 global_earliest_t = SENTINEL_T; + Dqn_V2 global_earliest_pos_just_before_collide = {}; + Dqn_V2 const entity_new_pos = entity_pos + delta_pos; + + if ((entity->flags & FP_GameEntityFlag_NoClip) == 0) { + for (FP_GameEntityIterator collider_it = {}; FP_Game_DFSPostOrderWalkEntityTree(game, &collider_it, game->play.root_entity);) { + FP_GameEntity *collider = collider_it.entity; + if (collider->handle == entity->handle) + continue; + + // TODO(doyle): Calculate the list of collidables at the start of the frame + if ((collider->flags & FP_GameEntityFlag_NonTraversable) == 0) + continue; + + bool entity_collides_with_collider = true; + switch (entity->type) { + case FP_EntityType_Catfish: /*FALLTHRU*/ + case FP_EntityType_Smoochie: /*FALLTHRU*/ + case FP_EntityType_Clinger: { + if (collider->type == FP_EntityType_Smoochie || collider->type == FP_EntityType_Clinger || collider->type == FP_EntityType_Catfish) { + entity_collides_with_collider = false; + } else if (FP_Entity_IsBuildingForMobs(collider)) { + #if 0 + // NOTE: We disable collision on buildings we have visited to avoid some + // problems ... + if (FP_SentinelList_Find(&entity->buildings_visited, collider->handle)) { + entity_collides_with_collider = false; + } + #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; + + case FP_EntityType_Terry: { + // NOTE: Don't collide with mobs when dashing (e.g. phase through) + FP_EntityTerryState state = *DQN_CAST(FP_EntityTerryState *)&entity->action.state; + if (state == FP_EntityTerryState_Dash || state == FP_EntityTerryState_DeadGhost) { + if (collider->type == FP_EntityType_Smoochie || collider->type == FP_EntityType_Clinger || collider->type == FP_EntityType_Catfish) + entity_collides_with_collider = false; + } + } break; + + case FP_EntityType_Nil: break; + case FP_EntityType_MerchantTerry: break; + case FP_EntityType_Count: break; + case FP_EntityType_ClubTerry: break; + case FP_EntityType_Map: break; + case FP_EntityType_MerchantGraveyard: break; + case FP_EntityType_MerchantGym: break; + case FP_EntityType_MerchantPhoneCompany: break; + case FP_EntityType_Heart: break; + case FP_EntityType_AirportTerry: break; + case FP_EntityType_ChurchTerry: break; + case FP_EntityType_KennelTerry: break; + case FP_EntityType_PhoneMessageProjectile: break; + case FP_EntityType_AirportTerryPlane: + case FP_EntityType_MobSpawner: + case FP_EntityType_PortalMonkey: break; + case FP_EntityType_Billboard: break; + } + + if (!entity_collides_with_collider) + continue; + + // NOTE: Sweep collider with half the radius of the source entity + Dqn_Rect collider_world_hit_box = FP_Game_CalcEntityWorldHitBox(game, collider->handle); + if (Dqn_V2_Area(collider_world_hit_box.size) <= 0) + continue; + + Dqn_Rect swept_collider_world_hit_box = collider_world_hit_box; + swept_collider_world_hit_box.pos -= (entity_world_hit_box.size * .5f); + swept_collider_world_hit_box.size += entity_world_hit_box.size; + + if (!Dqn_Rect_ContainsPoint(swept_collider_world_hit_box, entity_new_pos)) + continue; + + Dqn_f32 collider_left_wall_x = swept_collider_world_hit_box.pos.x; + Dqn_f32 collider_right_wall_x = swept_collider_world_hit_box.pos.x + swept_collider_world_hit_box.size.w; + Dqn_f32 collider_top_wall_y = swept_collider_world_hit_box.pos.y; + Dqn_f32 collider_bottom_wall_y = swept_collider_world_hit_box.pos.y + swept_collider_world_hit_box.size.h; + + Dqn_V2 o = entity_pos; + Dqn_V2 d = delta_pos; + + // NOTE: Solve collision by determining the 't' value at which + // we hit one of the walls of the collider and move the entity + // at exactly that point. + // O + td = x + // td = x - O + // t = (x - O) / d + + Dqn_f32 earliest_t = SENTINEL_T; + if (d.x != 0.f) { + Dqn_f32 left_t = (collider_left_wall_x - o.x) / d.x; + Dqn_f32 right_t = (collider_right_wall_x - o.x) / d.x; + if (left_t >= 0.f && left_t <= 1.f) + earliest_t = DQN_MIN(earliest_t, left_t); + if (right_t >= 0.f && right_t <= 1.f) + earliest_t = DQN_MIN(earliest_t, right_t); + } + + if (d.y != 0.f) { + Dqn_f32 top_t = (collider_top_wall_y - o.y) / d.y; + Dqn_f32 bottom_t = (collider_bottom_wall_y - o.y) / d.y; + if (top_t >= 0.f && top_t <= 1.f) + earliest_t = DQN_MIN(earliest_t, top_t); + if (bottom_t >= 0.f && bottom_t <= 1.f) + earliest_t = DQN_MIN(earliest_t, bottom_t); + } + + if (earliest_t < global_earliest_t) { + global_earliest_t = earliest_t; + global_earliest_pos_just_before_collide = entity_pos + (d * earliest_t); + } + } + } + + result.yes = global_earliest_t == SENTINEL_T; + if (!result.yes) + result.next_closest_valid_move = delta_pos * global_earliest_t; + return result; +} + +static void FP_Game_MoveEntity(FP_Game *game, FP_GameEntityHandle entity_handle, Dqn_V2 acceleration_meters_per_s) +{ + // f"(t) = a + // f'(t) = at + v + // f (t) = 0.5f*a(t^2) + vt + p + + FP_GameEntity *entity = FP_Game_GetEntity(game, entity_handle); + if (FP_Game_IsNilEntity(entity)) + return; + + Dqn_f32 t = DQN_CAST(Dqn_f32)DQN_SQUARED(FP_GAME_PHYSICS_STEP); + Dqn_f32 t_squared = DQN_SQUARED(t); + + Dqn_f32 velocity_falloff_coefficient = 0.82f; + Dqn_f32 acceleration_feel_good_factor = 15'000.f; + Dqn_V2 acceleration = FP_Game_MetersToPixelsV2(game->play, acceleration_meters_per_s) * acceleration_feel_good_factor; + entity->velocity = (acceleration * t) + entity->velocity * velocity_falloff_coefficient; + + // NOTE: Zero out velocity with epsilon + if (DQN_ABS(entity->velocity.x) < 5.f) + entity->velocity.x = 0.f; + if (DQN_ABS(entity->velocity.y) < 5.f) + entity->velocity.y = 0.f; + + Dqn_V2 const delta_pos = (acceleration * 0.5f * t_squared) + (entity->velocity * t); + Dqn_Rect const entity_world_hit_box = FP_Game_CalcEntityWorldHitBox(game, entity->handle); + Dqn_V2 const entity_pos = FP_Game_CalcEntityWorldPos(game, entity->handle); + Dqn_V2 const entity_new_pos = entity_pos + delta_pos; + + FP_GameCanMoveToPositionResult move_to_result = FP_Game_CanEntityMoveToPosition(game, entity->handle, delta_pos); + if (move_to_result.yes) { + entity->local_pos += delta_pos; + } else { + entity->local_pos += move_to_result.next_closest_valid_move; + } +} + +static Dqn_Rect FP_Game_GetBuildingPlacementRectForEntity(FP_Game *game, FP_GamePlaceableBuilding placeable_building, FP_GameEntityHandle handle) +{ + Dqn_Rect result = {}; + FP_GameEntity *entity = FP_Game_GetEntity(game, handle); + if (FP_Game_IsNilEntity(entity)) + return result; + + FP_EntityRenderData render_data = FP_Entity_GetRenderData(game, placeable_building.type, placeable_building.state, entity->direction); + Dqn_Rect box = FP_Game_CalcEntityWorldHitBox(game, entity->handle); + Dqn_V2 build_p = {}; + switch (entity->direction) { + case FP_GameDirection_Up: { + build_p = Dqn_Rect_InterpolatedPoint(box, Dqn_V2_InitNx2(0.5f, 0.f)) - Dqn_V2_InitNx2(0.f, render_data.render_size.h * .5f + 10.f); + } break; + + case FP_GameDirection_Down: { + build_p = Dqn_Rect_InterpolatedPoint(box, Dqn_V2_InitNx2(0.5f, 1.f)) + Dqn_V2_InitNx2(0.f, render_data.render_size.h * .5f + 10.f); + } break; + + case FP_GameDirection_Left: { + build_p = Dqn_Rect_InterpolatedPoint(box, Dqn_V2_InitNx2(0.0f, 0.5f)) - Dqn_V2_InitNx2(render_data.render_size.w * .5f + 10.f, 0); + } break; + + case FP_GameDirection_Right: { + build_p = Dqn_Rect_InterpolatedPoint(box, Dqn_V2_InitNx2(1.f, 0.5f)) + Dqn_V2_InitNx2(render_data.render_size.w * .5f + 10.f, 0); + } break; + + case FP_GameDirection_Count: DQN_INVALID_CODE_PATH; break; + } + + result.size = render_data.render_size; + result.pos = build_p - (render_data.render_size * .5f); + return result; +} diff --git a/feely_pona_game.h b/feely_pona_game.h index 5d6d975..0e58bd2 100644 --- a/feely_pona_game.h +++ b/feely_pona_game.h @@ -158,6 +158,34 @@ enum FP_GameEntityFaction FP_GameEntityFaction_Foe, }; +enum FP_GameInGameMenu +{ + FP_GameInGameMenu_Nil, + FP_GameInGameMenu_Build, + FP_GameInGameMenu_Merchant, +}; + +struct FP_GameKeyBind +{ + TELY_PlatformInputScanCode scan_code; + TELY_PlatformInputGamepadKey gamepad_key; +}; + +struct FP_GameControls +{ + FP_GameKeyBind up; + FP_GameKeyBind down; + FP_GameKeyBind left; + FP_GameKeyBind right; + FP_GameKeyBind attack; + FP_GameKeyBind buy_building; + FP_GameKeyBind buy_upgrade; + FP_GameKeyBind range_attack; + FP_GameKeyBind build_mode; + FP_GameKeyBind strafe; + FP_GameKeyBind dash; +}; + struct FP_GameEntity { FP_GameEntity *next; @@ -240,12 +268,18 @@ struct FP_GameEntity Dqn_FArray building_queue; uint64_t building_queue_next_sort_timestamp_ms; FP_GameEntityHandle queued_at_building; + + FP_GameInGameMenu in_game_menu; + bool build_mode_can_place_building; + Dqn_usize build_mode_building_index; + + FP_GameControls controls; }; struct FP_GameEntityIterator { - bool init; - Dqn_usize iteration_count; + bool init; + Dqn_usize iteration_count; FP_GameEntity *entity; FP_GameEntity *last_visited; @@ -284,13 +318,6 @@ enum FP_GameAudio FP_GameAudio_Count, }; -enum FP_GameInGameMenu -{ - FP_GameInGameMenu_Nil, - FP_GameInGameMenu_Build, - FP_GameInGameMenu_Merchant, -}; - enum FP_GameState { FP_GameState_IntroScreen, @@ -313,7 +340,7 @@ struct FP_GamePlay FP_GameEntity *entity_free_list; FP_GameEntity *map; - FP_GameEntityHandle player; + Dqn_FArray players; FP_GameRenderSprite player_merchant_menu; uint64_t player_trigger_purchase_upgrade_timestamp; uint64_t player_trigger_purchase_building_timestamp; @@ -336,7 +363,7 @@ struct FP_GamePlay FP_GameEntityHandle prev_hot_entity; FP_GameEntityHandle prev_active_entity; - FP_GameEntityHandle camera_tracking_entity; + Dqn_FArray camera_tracking_entity; FP_GameCamera camera; Dqn_f32 meters_to_pixels; uint64_t clock_ms; @@ -346,9 +373,6 @@ struct FP_GamePlay bool debug_ui; bool debug_hide_hud; bool god_mode; - FP_GameInGameMenu in_game_menu; - bool build_mode_can_place_building; - Dqn_usize build_mode_building_index; Dqn_FArray mob_spawners; Dqn_FArray portal_monkeys; @@ -361,27 +385,6 @@ struct FP_GamePlay FP_GameState state; }; -struct FP_GameKeyBind -{ - TELY_PlatformInputScanCode scan_code; - TELY_PlatformInputGamepadKey gamepad_key; -}; - -struct FP_GameControls -{ - FP_GameKeyBind up; - FP_GameKeyBind down; - FP_GameKeyBind left; - FP_GameKeyBind right; - FP_GameKeyBind attack; - FP_GameKeyBind buy_building; - FP_GameKeyBind buy_upgrade; - FP_GameKeyBind range_attack; - FP_GameKeyBind build_mode; - FP_GameKeyBind strafe; - FP_GameKeyBind dash; -}; - struct FP_Game { TELY_AssetFontHandle inter_regular_font_large; @@ -395,7 +398,6 @@ struct FP_Game TELY_AssetSpriteSheet atlas_sprite_sheet; TELY_RFui rfui; FP_GamePlay play; - FP_GameControls controls; }; struct FP_GameAStarNode @@ -426,3 +428,27 @@ FP_GamePlaceableBuilding const PLACEABLE_BUILDINGS[] = { {FP_EntityType_ClubTerry, FP_EntityClubTerryState_Idle}, {FP_EntityType_KennelTerry, FP_EntityKennelTerryState_Idle}, }; + +struct FP_GameCanMoveToPositionResult +{ + bool yes; + Dqn_V2 next_closest_valid_move; +}; + +struct FP_GameCameraM2x3 +{ + Dqn_M2x3 model_view; + Dqn_M2x3 view_model; +}; + + +Dqn_f32 const FP_GAME_PHYSICS_STEP = 1 / 60.f; + +#define FP_Game_MetersToPixelsNx1(game, val) ((val) * (game).meters_to_pixels) +#define FP_Game_MetersToPixelsNx2(game, x, y) (Dqn_V2_InitNx2(x, y) * (game).meters_to_pixels) +#define FP_Game_MetersToPixelsV2(game, xy) (xy * (game).meters_to_pixels) + +#define FP_Game_PixelsToMetersNx1(game, val) ((val) * (1.f/(game).meters_to_pixels)) +#define FP_Game_PixelsToMetersNx2(game, x, y) (Dqn_V2_InitNx2(x, y) * (1.f/(game).meters_to_pixels)) +#define FP_Game_PixelsToMetersV2(game, xy) (xy * (1.f/(game).meters_to_pixels)) + diff --git a/feely_pona_unity.h b/feely_pona_unity.h index 3bdfc98..8f612cd 100644 --- a/feely_pona_unity.h +++ b/feely_pona_unity.h @@ -67,6 +67,7 @@ DQN_MSVC_WARNING_DISABLE(4505) // warning C4505: unreferenced function with inte #include "feely_pona_entity.h" #include "feely_pona_game.h" +#include "feely_pona_entity.cpp" #include "feely_pona_game.cpp" #include "feely_pona_entity_create.cpp" #include "feely_pona_misc.cpp" diff --git a/feely_pona_unity_nodll.h b/feely_pona_unity_nodll.h index e0402aa..e046395 100644 --- a/feely_pona_unity_nodll.h +++ b/feely_pona_unity_nodll.h @@ -84,6 +84,7 @@ DQN_GCC_WARNING_DISABLE(-Wunused-function) #include "feely_pona_entity.h" #include "feely_pona_game.h" +#include "feely_pona_entity.cpp" #include "feely_pona_game.cpp" #include "feely_pona_entity_create.cpp" #include "feely_pona_misc.cpp" diff --git a/project.rdbg b/project.rdbg index 371468c..ed1868c 100644 Binary files a/project.rdbg and b/project.rdbg differ