diff --git a/feely_pona.cpp b/feely_pona.cpp index 2647498..dbee739 100644 --- a/feely_pona.cpp +++ b/feely_pona.cpp @@ -447,7 +447,7 @@ void TELY_DLL_Init(void *user_data) // NOTE: Mid lane mob spawner ================================================================== Dqn_V2 base_mid_p = Dqn_V2_InitNx2(1580, 0.f); Dqn_V2 mid_lane_mob_spawner_pos = Dqn_V2_InitNx2(game->map->local_hit_box_size.w * -0.5f, 0.f); - Dqn_usize spawn_cap = 16; + Dqn_usize spawn_cap = 1; { FP_GameEntityHandle mob_spawner = FP_Entity_CreateMobSpawner(game, mid_lane_mob_spawner_pos, spawn_cap, "Mob spawner"); FP_Game_PushParentEntity(game, mob_spawner); @@ -457,6 +457,7 @@ void TELY_DLL_Init(void *user_data) } // NOTE: Bottom lane spawner =================================================================== +#if 0 Dqn_V2 bottom_lane_mob_spawner_pos = Dqn_V2_InitNx2(mid_lane_mob_spawner_pos.x, mid_lane_mob_spawner_pos.y + 932.f); { FP_GameEntityHandle mob_spawner = FP_Entity_CreateMobSpawner(game, bottom_lane_mob_spawner_pos, spawn_cap, "Mob spawner"); @@ -475,6 +476,7 @@ void TELY_DLL_Init(void *user_data) FP_Entity_CreateWaypointF(game, Dqn_V2_InitNx2(-top_lane_mob_spawner_pos.x + base_mid_p.x, +915.f), "Waypoint"); FP_Game_PopParentEntity(game); } +#endif FP_Entity_CreateHeart(game, base_mid_p, "Heart"); @@ -600,6 +602,8 @@ void FP_EntityActionStateMachine(FP_Game *game, TELY_Audio *audio, TELY_Platform FP_Game_EntityActionReset(game, entity->handle, duration_ms, render_data.sprite); *acceleration_meters_per_s *= 35.f; + entity->stamina -= FP_TERRY_DASH_STAMINA_COST; + #if 0 FP_GameRenderSprite *cosmetic_sprite = Dqn_FArray_Make(&entity->extra_cosmetic_anims, Dqn_ZeroMem_Yes); if (cosmetic_sprite) { @@ -626,69 +630,6 @@ void FP_EntityActionStateMachine(FP_Game *game, TELY_Audio *audio, TELY_Platform } } break; } - - if (*state == FP_EntityTerryState_Attack || *state == FP_EntityTerryState_RangeAttack) { - DQN_ASSERT(action->sprite.anim); - - uint64_t duration_ms = action->sprite.anim->count * action->sprite.anim->ms_per_frame; - DQN_ASSERT(duration_ms >= PHYSICS_STEP); - uint64_t midpoint_clock_ms = action->end_at_clock_ms - (duration_ms / 2); - - // NOTE: Adding an attack_processed bool to make sure things only fire once - if (!entity->attack_processed && game->clock_ms >= midpoint_clock_ms) { - - // NOTE: Position the attack box - Dqn_V2 dir_vector = {}; - switch (entity->direction) { - case FP_GameDirection_Left: { - dir_vector.x = -1.f; - entity->attack_box_offset = Dqn_V2_InitNx2(entity->local_hit_box_offset.x - entity->attack_box_size.w, - entity->local_hit_box_offset.y); - } break; - - case FP_GameDirection_Right: { - dir_vector.x = +1.f; - entity->attack_box_offset = Dqn_V2_InitNx2( - entity->local_hit_box_offset.x + entity->attack_box_size.w, - entity->local_hit_box_offset.y); - } break; - - case FP_GameDirection_Up: { - dir_vector.y = -1.f; - entity->attack_box_offset = Dqn_V2_InitNx2(entity->local_hit_box_offset.x, - entity->local_hit_box_offset.y - entity->attack_box_size.h); - } break; - - case FP_GameDirection_Down: { - dir_vector.y = +1.f; - entity->attack_box_offset = Dqn_V2_InitNx2(entity->local_hit_box_offset.x, - entity->local_hit_box_offset.y + entity->attack_box_size.h); - } break; - - case FP_GameDirection_Count: break; - } - - if (*state == FP_EntityTerryState_RangeAttack) { - Dqn_Rect entity_hit_box = FP_Game_CalcEntityWorldHitBox(game, entity->handle); - Dqn_V2 projectile_pos = entity_hit_box.pos + entity->attack_box_offset; - Dqn_V2 projectile_acceleration = FP_Game_MetersToPixelsV2(game, dir_vector * 0.25f); - FP_Entity_CreatePhoneMessageProjectile(game, - entity->handle, - projectile_pos, - projectile_acceleration, - "Phone Message Projectile"); - } else { - entity->attack_box_size = entity->local_hit_box_size; - TELY_Audio_Play(audio, game->audio[FP_GameAudio_TerryHit], 1.f); - } - entity->attack_processed = true; - } else { - entity->attack_box_size = {}; - } - } else { - entity->attack_box_size = {}; - entity->attack_processed = false; - } } break; case FP_EntityType_Smoochie: { @@ -814,36 +755,6 @@ void FP_EntityActionStateMachine(FP_Game *game, TELY_Audio *audio, TELY_Platform if (entity->is_dying && *state != FP_EntitySmoochieState_Death) { FP_Game_EntityTransitionState(game, entity, FP_EntitySmoochieState_Death); } - - if (*state == FP_EntitySmoochieState_Attack) { - entity->attack_box_size = entity->local_hit_box_size; - // NOTE: Position the attack box - switch (entity->direction) { - case FP_GameDirection_Left: { - entity->attack_box_offset = Dqn_V2_InitNx2(entity->local_hit_box_offset.x - entity->attack_box_size.w, - entity->local_hit_box_offset.y); - } break; - - case FP_GameDirection_Right: { - entity->attack_box_offset = Dqn_V2_InitNx2(entity->local_hit_box_offset.x + entity->attack_box_size.w, - entity->local_hit_box_offset.y); - } break; - - case FP_GameDirection_Up: { - entity->attack_box_offset = Dqn_V2_InitNx2(entity->local_hit_box_offset.x, - entity->local_hit_box_offset.y - entity->attack_box_size.h); - } break; - - case FP_GameDirection_Down: { - entity->attack_box_offset = Dqn_V2_InitNx2(entity->local_hit_box_offset.x, - entity->local_hit_box_offset.y + entity->attack_box_size.h); - } break; - - case FP_GameDirection_Count: break; - } - } else { - entity->attack_box_size = {}; - } } break; case FP_EntityType_Clinger: { @@ -918,35 +829,6 @@ void FP_EntityActionStateMachine(FP_Game *game, TELY_Audio *audio, TELY_Platform if (entity->is_dying && *state != FP_EntityClingerState_Death) { FP_Game_EntityTransitionState(game, entity, FP_EntityClingerState_Death); } - - if (*state == FP_EntityClingerState_Attack) { // NOTE: Position the attack box - entity->attack_box_size = entity->local_hit_box_size; - switch (entity->direction) { - case FP_GameDirection_Left: { - entity->attack_box_offset = Dqn_V2_InitNx2(entity->local_hit_box_offset.x - entity->attack_box_size.w, - entity->local_hit_box_offset.y); - } break; - - case FP_GameDirection_Right: { - entity->attack_box_offset = Dqn_V2_InitNx2(entity->local_hit_box_offset.x + entity->attack_box_size.w, - entity->local_hit_box_offset.y); - } break; - - case FP_GameDirection_Up: { - entity->attack_box_offset = Dqn_V2_InitNx2(entity->local_hit_box_offset.x, - entity->local_hit_box_offset.y - entity->attack_box_size.h); - } break; - - case FP_GameDirection_Down: { - entity->attack_box_offset = Dqn_V2_InitNx2(entity->local_hit_box_offset.x, - entity->local_hit_box_offset.y + entity->attack_box_size.h); - } break; - - case FP_GameDirection_Count: DQN_INVALID_CODE_PATH; break; - } - } else { - entity->attack_box_size = {}; - } } break; case FP_EntityType_MerchantTerry: { @@ -1186,35 +1068,6 @@ void FP_EntityActionStateMachine(FP_Game *game, TELY_Audio *audio, TELY_Platform if (entity->is_dying && *state != FP_EntityCatfishState_Death) { FP_Game_EntityTransitionState(game, entity, FP_EntityCatfishState_Death); } - - if (*state == FP_EntityCatfishState_Attack) { // NOTE: Position the attack box - entity->attack_box_size = entity->local_hit_box_size; - switch (entity->direction) { - case FP_GameDirection_Left: { - entity->attack_box_offset = Dqn_V2_InitNx2(entity->local_hit_box_offset.x - entity->attack_box_size.w, - entity->local_hit_box_offset.y); - } break; - - case FP_GameDirection_Right: { - entity->attack_box_offset = Dqn_V2_InitNx2(entity->local_hit_box_offset.x + entity->attack_box_size.w, - entity->local_hit_box_offset.y); - } break; - - case FP_GameDirection_Up: { - entity->attack_box_offset = Dqn_V2_InitNx2(entity->local_hit_box_offset.x, - entity->local_hit_box_offset.y - entity->attack_box_size.h); - } break; - - case FP_GameDirection_Down: { - entity->attack_box_offset = Dqn_V2_InitNx2(entity->local_hit_box_offset.x, - entity->local_hit_box_offset.y + entity->attack_box_size.h); - } break; - - case FP_GameDirection_Count: DQN_INVALID_CODE_PATH; break; - } - } else { - entity->attack_box_size = {}; - } } break; case FP_EntityType_ChurchTerry: { @@ -1271,6 +1124,106 @@ void FP_EntityActionStateMachine(FP_Game *game, TELY_Audio *audio, TELY_Platform case FP_EntityType_Count: DQN_INVALID_CODE_PATH; break; case FP_EntityType_PhoneMessageProjectile: break; } + + switch (entity->type) { + case FP_EntityType_Nil: + case FP_EntityType_AirportTerry: + case FP_EntityType_ChurchTerry: + case FP_EntityType_ClubTerry: + case FP_EntityType_Heart: + case FP_EntityType_KennelTerry: + case FP_EntityType_Map: + case FP_EntityType_MerchantGraveyard: + case FP_EntityType_MerchantGym: + case FP_EntityType_MerchantPhoneCompany: + case FP_EntityType_MerchantTerry: + case FP_EntityType_PhoneMessageProjectile: + case FP_EntityType_Count: break; + + case FP_EntityType_Terry: /*FALLTHRU*/ + case FP_EntityType_Catfish: /*FALLTHRU*/ + case FP_EntityType_Clinger: /*FALLTHRU*/ + case FP_EntityType_Smoochie: { + + bool is_attacking = false; + bool is_range_attack = false; + if (entity->type == FP_EntityType_Catfish) { + is_attacking = entity->action.state == FP_EntityCatfishState_Attack; + } else if (entity->type == FP_EntityType_Clinger) { + is_attacking = entity->action.state == FP_EntityClingerState_Attack; + } else if (entity->type == FP_EntityType_Smoochie) { + is_attacking = entity->action.state == FP_EntitySmoochieState_Attack; + } else { + DQN_ASSERT(entity->type == FP_EntityType_Terry); + is_range_attack = entity->action.state == FP_EntityTerryState_RangeAttack; + is_attacking = is_range_attack || entity->action.state == FP_EntityTerryState_Attack; + } + + if (!is_attacking) { + entity->attack_processed = false; + entity->attack_box_size = {}; + break; + } + + // 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(action->sprite.anim); + + // NOTE: Adding an attack_processed bool to make sure things only fire once + if (!entity->attack_processed && game->clock_ms >= midpoint_clock_ms) { + + // NOTE: Position the attack box + Dqn_V2 dir_vector = {}; + switch (entity->direction) { + case FP_GameDirection_Left: { + dir_vector.x = -1.f; + entity->attack_box_offset = Dqn_V2_InitNx2(entity->local_hit_box_offset.x - entity->attack_box_size.w, + entity->local_hit_box_offset.y); + } break; + + case FP_GameDirection_Right: { + dir_vector.x = +1.f; + entity->attack_box_offset = Dqn_V2_InitNx2( + entity->local_hit_box_offset.x + entity->attack_box_size.w, + entity->local_hit_box_offset.y); + } break; + + case FP_GameDirection_Up: { + dir_vector.y = -1.f; + entity->attack_box_offset = Dqn_V2_InitNx2(entity->local_hit_box_offset.x, + entity->local_hit_box_offset.y - entity->attack_box_size.h); + } break; + + case FP_GameDirection_Down: { + dir_vector.y = +1.f; + entity->attack_box_offset = Dqn_V2_InitNx2(entity->local_hit_box_offset.x, + entity->local_hit_box_offset.y + entity->attack_box_size.h); + } break; + + case FP_GameDirection_Count: break; + } + + if (is_range_attack) { + Dqn_Rect entity_hit_box = FP_Game_CalcEntityWorldHitBox(game, entity->handle); + Dqn_V2 projectile_pos = entity_hit_box.pos + entity->attack_box_offset; + Dqn_V2 projectile_acceleration = FP_Game_MetersToPixelsV2(game, dir_vector * 0.25f); + FP_Entity_CreatePhoneMessageProjectile(game, + entity->handle, + projectile_pos, + projectile_acceleration, + "Phone Message Projectile"); + } else { + entity->attack_box_size = entity->local_hit_box_size; + TELY_Audio_Play(audio, game->audio[FP_GameAudio_TerryHit], 1.f); + } + entity->attack_processed = true; + } else { + entity->attack_box_size = {}; + } + } break; + } } void FP_Update(TELY_Platform *platform, FP_Game *game, TELY_PlatformInput *input) @@ -1280,6 +1233,7 @@ void FP_Update(TELY_Platform *platform, FP_Game *game, TELY_PlatformInput *input if (TELY_Platform_InputKeyIsReleased(input->mouse_left)) game->clicked_entity = game->prev_active_entity; + game->update_counter++; game->clock_ms = DQN_CAST(uint64_t)(platform->input.timer_s * 1000.f); Dqn_V2 dir_vector = {}; @@ -1787,10 +1741,16 @@ void FP_Update(TELY_Platform *platform, FP_Game *game, TELY_PlatformInput *input } } - if (entity->type == FP_EntityType_Terry) { - 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), - entity->terry_mobile_data_plan_cap); + // 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), + entity->terry_mobile_data_plan_cap); + + // NOTE: Recover hp & stamina + entity->stamina = DQN_MIN(entity->stamina + 1, entity->stamina_cap); + + if (game->update_counter % 12 == 0) { + entity->hp = DQN_MIN(entity->hp + 1, entity->hp_cap); } // NOTE: Derive dynmamic bounding boxes ==================================================== @@ -1835,7 +1795,7 @@ void FP_Update(TELY_Platform *platform, FP_Game *game, TELY_PlatformInput *input } } else if (entity->spawn_list.size < entity->spawn_cap) { // NOTE: Spawn new entities if (input->timer_s >= entity->next_spawn_timestamp_s) { - uint64_t hp_adjustment = entity->current_wave - 1; + uint16_t hp_adjustment = DQN_CAST(uint16_t)entity->current_wave - 1; entity->next_spawn_timestamp_s = DQN_CAST(uint64_t)(input->timer_s + 5.f); Dqn_V2 entity_world_pos = FP_Game_CalcEntityWorldPos(game, entity->handle); @@ -1843,17 +1803,19 @@ void FP_Update(TELY_Platform *platform, FP_Game *game, TELY_PlatformInput *input Dqn_f32 mob_choice = Dqn_PCG32_NextF32(&game->rng); if (mob_choice <= 0.33f) - link->data = FP_Entity_CreateClinger(game, entity_world_pos, hp_adjustment, "Clinger"); + link->data = FP_Entity_CreateClinger(game, entity_world_pos, "Clinger"); else if (mob_choice <= 0.66f) - link->data = FP_Entity_CreateSmoochie(game, entity_world_pos, hp_adjustment, "Smoochie"); + link->data = FP_Entity_CreateSmoochie(game, entity_world_pos, "Smoochie"); else - link->data = FP_Entity_CreateCatfish(game, entity_world_pos, hp_adjustment, "Catfish"); + link->data = FP_Entity_CreateCatfish(game, entity_world_pos, "Catfish"); // NOTE: Setup the mob with waypoints FP_GameEntity *mob = FP_Game_GetEntity(game, link->data); mob->waypoints = FP_SentinelList_Init(game->chunk_pool); mob->flags |= FP_GameEntityFlag_AggrosWhenNearTerry; mob->flags |= FP_GameEntityFlag_RespondsToBuildings; + mob->hp_cap += hp_adjustment; + mob->hp = entity->hp_cap; for (FP_GameEntity *waypoint_entity = entity->first_child; waypoint_entity; waypoint_entity = waypoint_entity->next) { if ((waypoint_entity->flags & FP_GameEntityFlag_MobSpawnerWaypoint) == 0) @@ -1896,6 +1858,10 @@ void FP_Update(TELY_Platform *platform, FP_Game *game, TELY_PlatformInput *input if ((defender->flags & FP_GameEntityFlag_Attackable) == 0) continue; + // NOTE: Projectiles can't hurt the owner that spawned it + if (attacker->projectile_owner == defender->handle) + continue; + bool permit_attack = true; switch (attacker->type) { case FP_EntityType_Smoochie: { @@ -1937,7 +1903,7 @@ void FP_Update(TELY_Platform *platform, FP_Game *game, TELY_PlatformInput *input continue; // NOTE: Do HP ========================================================================= - defender->hp -= 1; + defender->hp = defender->hp >= FP_DEFAULT_DAMAGE ? defender->hp - FP_DEFAULT_DAMAGE : 0; if (defender->hp <= 0) { defender->is_dying = true; @@ -2237,18 +2203,20 @@ void FP_Render(FP_Game *game, TELY_Platform *platform, TELY_Renderer *renderer) FP_GameEntity *player = FP_Game_GetEntity(game, game->player); Dqn_V2 player_pos = FP_Game_CalcEntityWorldPos(game, game->player); { + FP_GameInventory *invent = &player->inventory; struct FP_MerchantToMenuMapping { FP_GameEntityHandle merchant; Dqn_String8 menu_anim; Dqn_String8 building; Dqn_V2 building_offset01; uint8_t *inventory_count; - uint32_t *base_price; + uint32_t *building_base_price; + uint32_t *upgrade_base_price; } merchants[] = { - {game->merchant_terry, g_anim_names.merchant_terry_menu, g_anim_names.club_terry_dark, Dqn_V2_InitNx2(0.015f, +0.04f), &player->inventory.clubs, &player->inventory.clubs_base_price}, - {game->merchant_graveyard, g_anim_names.merchant_graveyard_menu, g_anim_names.church_terry_dark, Dqn_V2_InitNx2(0.04f, -0.15f), &player->inventory.churchs, &player->inventory.churchs_base_price}, - {game->merchant_gym, g_anim_names.merchant_gym_menu, g_anim_names.kennel_terry, Dqn_V2_InitNx2(0, +0), &player->inventory.kennels, &player->inventory.kennels_base_price}, - {game->merchant_phone_company, g_anim_names.merchant_phone_company_menu, g_anim_names.airport_terry, Dqn_V2_InitNx2(0, -0.1f), &player->inventory.airports, &player->inventory.airports_base_price}, + {game->merchant_terry, g_anim_names.merchant_terry_menu, g_anim_names.club_terry_dark, Dqn_V2_InitNx2(0.015f, +0.04f), &invent->clubs, &invent->clubs_base_price, &invent->health_base_price}, + {game->merchant_graveyard, g_anim_names.merchant_graveyard_menu, g_anim_names.church_terry_dark, Dqn_V2_InitNx2(0.04f, -0.15f), &invent->churchs, &invent->churchs_base_price, &invent->stamina_base_price}, + {game->merchant_gym, g_anim_names.merchant_gym_menu, g_anim_names.kennel_terry, Dqn_V2_InitNx2(0, +0), &invent->kennels, &invent->kennels_base_price, &invent->attack_base_price}, + {game->merchant_phone_company, g_anim_names.merchant_phone_company_menu, g_anim_names.airport_terry, Dqn_V2_InitNx2(0, -0.1f), &invent->airports, &invent->airports_base_price, &invent->mobile_plan_base_price}, }; bool activated_merchant = false; @@ -2300,8 +2268,8 @@ void FP_Render(FP_Game *game, TELY_Platform *platform, TELY_Renderer *renderer) // NOTE: Render the merchant button for buildings ================== uint64_t const buy_duration_ms = 500; - bool const have_enough_coins = player->coins >= *mapping.base_price; { + bool const have_enough_coins = player->coins >= *mapping.building_base_price; // NOTE: Buy trigger + animation =============================== { TELY_PlatformInputScanCode key = TELY_PlatformInputScanCode_J; @@ -2317,8 +2285,15 @@ void FP_Render(FP_Game *game, TELY_Platform *platform, TELY_Renderer *renderer) } else if (TELY_Platform_InputScanCodeIsReleased(input, key)) { if (game->clock_ms > game->player_trigger_purchase_building_timestamp) { if (mapping.inventory_count) { - player->coins -= *mapping.base_price; - *mapping.base_price *= 2; + player->coins -= *mapping.building_base_price; + *mapping.building_base_price *= 2; + + // NOTE: Raise the prices of everything else + invent->airports_base_price *= 2; + invent->clubs_base_price *= 2; + invent->kennels_base_price *= 2; + invent->churchs_base_price *= 2; + (*mapping.inventory_count)++; } } else { @@ -2358,7 +2333,7 @@ void FP_Render(FP_Game *game, TELY_Platform *platform, TELY_Renderer *renderer) 0.f /*rotation*/, tex_mod_colour); - TELY_Render_TextF(renderer, Dqn_Rect_InterpolatedPoint(dest_rect, Dqn_V2_InitNx2(0.5f, -1)), Dqn_V2_InitNx2(0.5, 0.f), "$%u", *mapping.base_price); + TELY_Render_TextF(renderer, Dqn_Rect_InterpolatedPoint(dest_rect, Dqn_V2_InitNx2(0.5f, -1)), Dqn_V2_InitNx2(0.5, 0.f), "$%u", *mapping.building_base_price); } // NOTE: Render the merchant shop item building ================ @@ -2380,34 +2355,56 @@ void FP_Render(FP_Game *game, TELY_Platform *platform, TELY_Renderer *renderer) // NOTE: Render the merchant button for buildings { + bool const have_enough_coins = player->coins >= *mapping.upgrade_base_price; // NOTE: Buy trigger + animation =============================== { TELY_PlatformInputScanCode key = TELY_PlatformInputScanCode_K; bool trigger_buy_anim = false; - if (TELY_Platform_InputScanCodeIsPressed(input, key)) { - game->player_trigger_purchase_upgrade_timestamp = game->clock_ms + buy_duration_ms; - } else if (TELY_Platform_InputScanCodeIsDown(input, key)) { - trigger_buy_anim = true; - if (game->clock_ms > game->player_trigger_purchase_upgrade_timestamp) { - // TODO(doyle): Do buy logic + + if (have_enough_coins) { + if (TELY_Platform_InputScanCodeIsPressed(input, key)) { + game->player_trigger_purchase_upgrade_timestamp = game->clock_ms + buy_duration_ms; + } else if (TELY_Platform_InputScanCodeIsDown(input, key)) { + trigger_buy_anim = true; + if (game->clock_ms > game->player_trigger_purchase_upgrade_timestamp) + game->player_trigger_purchase_upgrade_timestamp = game->clock_ms; + } else if (TELY_Platform_InputScanCodeIsReleased(input, key)) { + if (game->clock_ms > game->player_trigger_purchase_upgrade_timestamp) { + player->coins -= *mapping.upgrade_base_price; + *mapping.upgrade_base_price *= 2; + + if (mapping.merchant == game->merchant_terry) { + // TODO(doyle): Attack damage? Or increase attack range? + } else if (mapping.merchant == game->merchant_graveyard) { + player->stamina_cap += DQN_CAST(uint16_t)(FP_TERRY_DASH_STAMINA_COST * .5f); + } else if (mapping.merchant == game->merchant_gym) { + player->hp_cap += FP_DEFAULT_DAMAGE; + player->hp = player->hp_cap; + } else if (mapping.merchant == game->merchant_phone_company) { + player->terry_mobile_data_plan_cap += DQN_KILOBYTES(1); + } + } else { + game->player_trigger_purchase_upgrade_timestamp = UINT64_MAX; + } } - } - if (trigger_buy_anim) { - uint64_t start_buy_time = game->player_trigger_purchase_upgrade_timestamp - buy_duration_ms; - uint64_t elapsed_time = game->clock_ms - start_buy_time; + if (trigger_buy_anim) { + uint64_t start_buy_time = game->player_trigger_purchase_upgrade_timestamp - buy_duration_ms; + uint64_t elapsed_time = game->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; + 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)); + 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); { 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]; @@ -2420,9 +2417,9 @@ void FP_Render(FP_Game *game, TELY_Platform *platform, TELY_Renderer *renderer) dest_rect, Dqn_V2_Zero /*rotate origin*/, 0.f /*rotation*/, - TELY_COLOUR_WHITE_V4); + tex_mod_colour); - TELY_Render_TextF(renderer, Dqn_Rect_InterpolatedPoint(dest_rect, Dqn_V2_InitNx2(0.5f, -1)), Dqn_V2_InitNx2(0.5, 0.f), "$2"); + TELY_Render_TextF(renderer, Dqn_Rect_InterpolatedPoint(dest_rect, Dqn_V2_InitNx2(0.5f, -1)), Dqn_V2_InitNx2(0.5, 0.f), "$%u", *mapping.upgrade_base_price); } } } @@ -2469,6 +2466,47 @@ void FP_Render(FP_Game *game, TELY_Platform *platform, TELY_Renderer *renderer) player->terry_mobile_data_plan, player->terry_mobile_data_plan_cap); + // NOTE: Health bar ==================================================== + Dqn_f32 bar_height = font_height * .75f; + { + next_pos.y += font_height; + Dqn_f32 health_t = player->hp / DQN_CAST(Dqn_f32)player->hp_cap; + Dqn_Rect health_rect = Dqn_Rect_InitNx4(next_pos.x, next_pos.y, DQN_CAST(Dqn_f32)player->hp_cap, bar_height); + Dqn_Rect curr_health_rect = Dqn_Rect_InitNx4(next_pos.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: Stamina bar =================================================== + { + next_pos.y += font_height; + Dqn_f32 stamina_t = player->stamina / DQN_CAST(Dqn_f32)player->stamina_cap; + Dqn_Rect stamina_rect = Dqn_Rect_InitNx4(next_pos.x, next_pos.y, DQN_CAST(Dqn_f32)player->stamina_cap, bar_height); + Dqn_Rect curr_stamina_rect = Dqn_Rect_InitNx4(next_pos.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; + } + next_pos.y += font_height; TELY_Render_TextF(renderer, next_pos, Dqn_V2_Zero, "$%I64u", player->coins); diff --git a/feely_pona_entity.h b/feely_pona_entity.h index c35a005..2358474 100644 --- a/feely_pona_entity.h +++ b/feely_pona_entity.h @@ -137,3 +137,5 @@ struct FP_EntityRenderData }; Dqn_usize const FP_TERRY_MOBILE_DATA_PER_RANGE_ATTACK = DQN_KILOBYTES(1); +uint16_t const FP_TERRY_DASH_STAMINA_COST = 33; +uint16_t const FP_DEFAULT_DAMAGE = 30; diff --git a/feely_pona_entity_create.cpp b/feely_pona_entity_create.cpp index 216c636..a0ed33c 100644 --- a/feely_pona_entity_create.cpp +++ b/feely_pona_entity_create.cpp @@ -291,7 +291,7 @@ static FP_GameEntityHandle FP_Entity_CreateWaypointF(FP_Game *game, Dqn_V2 pos, return result; } -static FP_GameEntityHandle FP_Entity_CreateClinger(FP_Game *game, Dqn_V2 pos, uint64_t hp_adjustment, DQN_FMT_STRING_ANNOTATE char const *fmt, ...) +static FP_GameEntityHandle FP_Entity_CreateClinger(FP_Game *game, Dqn_V2 pos, DQN_FMT_STRING_ANNOTATE char const *fmt, ...) { va_list args; va_start(args, fmt); @@ -299,13 +299,12 @@ static FP_GameEntityHandle FP_Entity_CreateClinger(FP_Game *game, Dqn_V2 pos, ui FP_GameEntityHandle result = entity->handle; va_end(args); - entity->type = FP_EntityType_Clinger; - entity->hp = 1 + hp_adjustment; - entity->is_dying = false; - entity->base_acceleration_per_s.meters = 8.f; - entity->local_pos = pos; - entity->sprite_height = FP_Entity_GetRenderData(game, entity->type, 0 /*state*/, FP_GameDirection_Down).height; - entity->attack_cooldown_ms = 1000; + entity->type = FP_EntityType_Clinger; + entity->is_dying = false; + entity->base_acceleration_per_s.meters = 8.f; + entity->local_pos = pos; + entity->sprite_height = FP_Entity_GetRenderData(game, entity->type, 0 /*state*/, FP_GameDirection_Down).height; + entity->attack_cooldown_ms = 1000; entity->local_hit_box_size = FP_Game_MetersToPixelsNx2(game, 0.4f, entity->sprite_height.meters * .5f); FP_Entity_AddDebugEditorFlags(game, entity->handle); @@ -314,7 +313,7 @@ static FP_GameEntityHandle FP_Entity_CreateClinger(FP_Game *game, Dqn_V2 pos, ui return result; } -static FP_GameEntityHandle FP_Entity_CreateSmoochie(FP_Game *game, Dqn_V2 pos, uint64_t hp_adjustment, DQN_FMT_STRING_ANNOTATE char const *fmt, ...) +static FP_GameEntityHandle FP_Entity_CreateSmoochie(FP_Game *game, Dqn_V2 pos, DQN_FMT_STRING_ANNOTATE char const *fmt, ...) { va_list args; va_start(args, fmt); @@ -324,7 +323,6 @@ static FP_GameEntityHandle FP_Entity_CreateSmoochie(FP_Game *game, Dqn_V2 pos, u entity->type = FP_EntityType_Smoochie; entity->base_acceleration_per_s.meters = 8.f; - entity->hp = 1 + hp_adjustment; entity->is_dying = false; entity->local_pos = pos; entity->sprite_height = FP_Entity_GetRenderData(game, entity->type, 0 /*state*/, FP_GameDirection_Down).height; @@ -336,28 +334,6 @@ static FP_GameEntityHandle FP_Entity_CreateSmoochie(FP_Game *game, Dqn_V2 pos, u return result; } -static FP_GameEntityHandle FP_Entity_CreateCatfish(FP_Game *game, Dqn_V2 pos, uint64_t hp_adjustment, DQN_FMT_STRING_ANNOTATE char const *fmt, ...) -{ - va_list args; - va_start(args, fmt); - FP_GameEntity *entity = FP_Game_MakeEntityPointerFV(game, fmt, args); - FP_GameEntityHandle result = entity->handle; - va_end(args); - - entity->type = FP_EntityType_Catfish; - entity->base_acceleration_per_s.meters = 8.f; - entity->hp = 1 + hp_adjustment; - entity->is_dying = false; - entity->local_pos = pos; - entity->sprite_height = FP_Entity_GetRenderData(game, entity->type, 0 /*state*/, FP_GameDirection_Down).height; - entity->attack_cooldown_ms = 1000; - entity->local_hit_box_size = FP_Game_MetersToPixelsNx2(game, 0.4f, entity->sprite_height.meters * .6f); - FP_Entity_AddDebugEditorFlags(game, entity->handle); - entity->flags |= FP_GameEntityFlag_NonTraversable; - entity->flags |= FP_GameEntityFlag_Attackable; - return result; -} - static FP_GameEntityHandle FP_Entity_CreateCatfish(FP_Game *game, Dqn_V2 pos, DQN_FMT_STRING_ANNOTATE char const *fmt, ...) { va_list args; @@ -368,7 +344,6 @@ static FP_GameEntityHandle FP_Entity_CreateCatfish(FP_Game *game, Dqn_V2 pos, DQ entity->type = FP_EntityType_Catfish; entity->base_acceleration_per_s.meters = 8.f; - entity->hp = 1; entity->is_dying = false; entity->local_pos = pos; entity->sprite_height = FP_Entity_GetRenderData(game, entity->type, 0 /*state*/, FP_GameDirection_Down).height; @@ -449,17 +424,15 @@ static FP_GameEntityHandle FP_Entity_CreateTerry(FP_Game *game, Dqn_V2 pos, DQN_ entity->sprite_height = FP_Entity_GetRenderData(game, entity->type, 0 /*state*/, FP_GameDirection_Down).height; entity->attack_cooldown_ms = 500; entity->local_hit_box_size = FP_Game_MetersToPixelsNx2(game, 0.5f, entity->sprite_height.meters * .6f); + entity->hp_cap = FP_DEFAULT_DAMAGE * 3; + entity->hp = entity->hp_cap; + entity->coins = 1'000'000; FP_Entity_AddDebugEditorFlags(game, result); entity->flags |= FP_GameEntityFlag_NonTraversable; entity->flags |= FP_GameEntityFlag_Attackable; entity->flags |= FP_GameEntityFlag_CameraTracking; - entity->terry_mobile_data_plan_cap = DQN_KILOBYTES(16); + entity->terry_mobile_data_plan_cap = DQN_KILOBYTES(6); entity->terry_mobile_data_plan = entity->terry_mobile_data_plan_cap; - - entity->inventory.airports_base_price = 4; - entity->inventory.churchs_base_price = 2; - entity->inventory.kennels_base_price = 2; - entity->inventory.clubs_base_price = 2; return result; } diff --git a/feely_pona_game.cpp b/feely_pona_game.cpp index 395d0cd..f28604a 100644 --- a/feely_pona_game.cpp +++ b/feely_pona_game.cpp @@ -219,11 +219,26 @@ static FP_GameEntity *FP_Game_MakeEntityPointerFV(FP_Game *game, DQN_FMT_STRING_ result->handle.id = (game->entities.size - 1) & FP_GAME_ENTITY_HANDLE_INDEX_MASK; } - result->sprite_height.meters = 1; - result->parent = FP_Game_ActiveParentEntityPointer(game); - result->name = TELY_ChunkPool_AllocFmtFV(game->chunk_pool, fmt, args); - result->buildings_visited = FP_SentinelList_Init(game->chunk_pool); - result->action.sprite_alpha = 1.f; + result->sprite_height.meters = 1; + result->parent = FP_Game_ActiveParentEntityPointer(game); + result->name = TELY_ChunkPool_AllocFmtFV(game->chunk_pool, fmt, args); + result->buildings_visited = FP_SentinelList_Init(game->chunk_pool); + result->action.sprite_alpha = 1.f; + result->stamina_cap = 100; + + result->hp_cap = FP_DEFAULT_DAMAGE; + result->hp = result->hp_cap; + + result->inventory.airports_base_price = 4; + result->inventory.churchs_base_price = 2; + result->inventory.kennels_base_price = 2; + result->inventory.clubs_base_price = 2; + + result->inventory.stamina_base_price = 2; + result->inventory.health_base_price = 2; + result->inventory.mobile_plan_base_price = 2; + result->inventory.attack_base_price = 2; + // NOTE: Attach entity as a child to the parent FP_GameEntity *parent = result->parent; @@ -730,7 +745,6 @@ FP_GameFindClosestEntityResult FP_Game_FindClosestEntityWithType(FP_Game *game, static bool FP_Game_CanEntityAttack(FP_GameEntity *entity, uint64_t current_time_ms) { bool result = (current_time_ms - entity->last_attack_timestamp) >= entity->attack_cooldown_ms; - return result; } @@ -750,6 +764,9 @@ static void FP_Game_EntityTransitionState(FP_Game *game, FP_GameEntity *entity, if (entity->terry_mobile_data_plan < FP_TERRY_MOBILE_DATA_PER_RANGE_ATTACK) return; } + } else if (desired_state == FP_EntityTerryState_Dash) { + if (entity->stamina < FP_TERRY_DASH_STAMINA_COST) + return; } } break; @@ -773,6 +790,17 @@ static void FP_Game_EntityTransitionState(FP_Game *game, FP_GameEntity *entity, } } break; + case FP_EntityType_Catfish: { + if (desired_state == FP_EntityClingerState_Attack) { + if (!FP_Game_CanEntityAttack(entity, game->clock_ms)) { + // NOTE: Cooldown not met do not transition + return; + } + entity->last_attack_timestamp = game->clock_ms; + } + } break; + + case FP_EntityType_AirportTerry: { } break; @@ -783,7 +811,6 @@ static void FP_Game_EntityTransitionState(FP_Game *game, FP_GameEntity *entity, } break; case FP_EntityType_Nil: - case FP_EntityType_Catfish: case FP_EntityType_Heart: case FP_EntityType_KennelTerry: case FP_EntityType_Map: diff --git a/feely_pona_game.h b/feely_pona_game.h index 01683c3..104b063 100644 --- a/feely_pona_game.h +++ b/feely_pona_game.h @@ -141,6 +141,11 @@ struct FP_GameInventory uint32_t kennels_base_price; uint32_t churchs_base_price; + uint32_t stamina_base_price; + uint32_t health_base_price; + uint32_t mobile_plan_base_price; + uint32_t attack_base_price; + uint8_t airports; uint8_t clubs; uint8_t kennels; @@ -199,7 +204,6 @@ struct FP_GameEntity uint64_t wave_cooldown_timestamp_s; uint64_t flags; - uint64_t hp; FP_GameDirection direction; Dqn_V2 local_pos; Dqn_f64 alive_time_s; @@ -213,6 +217,10 @@ struct FP_GameEntity Dqn_usize coins; FP_GameInventory inventory; + uint16_t hp; + uint16_t hp_cap; + uint16_t stamina; + uint16_t stamina_cap; }; struct FP_GameEntityIterator @@ -295,6 +303,7 @@ struct FP_Game Dqn_f32 meters_to_pixels; uint64_t clock_ms; + uint64_t update_counter; Dqn_PCG32 rng; bool debug_ui;