diff --git a/feely_pona.cpp b/feely_pona.cpp index 5ddf208..ca974fa 100644 --- a/feely_pona.cpp +++ b/feely_pona.cpp @@ -138,9 +138,9 @@ static void FP_SetDefaultGamepadBindings(FP_GameControls *controls) controls->move_building_ui_cursor_right.gamepad_key = TELY_OSInputGamepadKey_DRight; } -static bool FP_ListenForNewPlayer(TELY_OSInput *input, FP_Game *game) +static FP_ListenForNewPlayerResult FP_ListenForNewPlayer(TELY_OSInput *input, FP_Game *game, bool tutorial_is_allowed) { - bool result = false; + FP_ListenForNewPlayerResult result = {}; if (game->play.players.size == 2) return result; @@ -162,6 +162,18 @@ static bool FP_ListenForNewPlayer(TELY_OSInput *input, FP_Game *game) bool keyboard_pressed = !keyboard_already_allocated && TELY_OSInput_ScanKeyIsPressed(input, TELY_OSInputScanKey_B); bool gamepad_pressed = TELY_OSInput_GamepadKeyIsPressed(input, gamepad_index, TELY_OSInputGamepadKey_Start); + if (tutorial_is_allowed) { + if (!keyboard_already_allocated && TELY_OSInput_ScanKeyIsPressed(input, TELY_OSInputScanKey_T)) { + keyboard_pressed = true; + result.tutorial_requested = true; + } + + if (TELY_OSInput_GamepadKeyIsPressed(input, gamepad_index, TELY_OSInputGamepadKey_Select)) { + gamepad_pressed = true; + result.tutorial_requested = true; + } + } + if (keyboard_pressed || gamepad_pressed) { FP_GameEntityHandle terry_handle = {}; if (play->players.size) { @@ -197,11 +209,13 @@ static bool FP_ListenForNewPlayer(TELY_OSInput *input, FP_Game *game) FP_SetDefaultGamepadBindings(controls); } - result = true; + result.yes = true; } + return result; } +uint64_t const FP_COOLDOWN_WAVE_TIME_MS = 30'000; static void FP_PlayReset(FP_Game *game, TELY_OS *os) { FP_GamePlay *play = &game->play; @@ -225,6 +239,17 @@ static void FP_PlayReset(FP_Game *game, TELY_OS *os) Dqn_FArray_Add(&play->parent_entity_stack, play->root_entity->handle); Dqn_PCG32_Seed(&play->rng, os->core.epoch_time); + // NOTE: Seed the shuffled list with indexes + DQN_FOR_UINDEX (index, DQN_ARRAY_UCOUNT(play->monkey_spawn_shuffled_list)) { + play->monkey_spawn_shuffled_list[index] = DQN_CAST(uint8_t)index; + } + + // NOTE: Fisher yates shuffle the list + for (Dqn_usize index = DQN_ARRAY_UCOUNT(play->monkey_spawn_shuffled_list) - 1; index < DQN_ARRAY_UCOUNT(play->monkey_spawn_shuffled_list); index--) { + uint32_t swap_index = Dqn_PCG32_Range(&play->rng, 0, DQN_CAST(uint32_t)index + 1); + DQN_SWAP(play->monkey_spawn_shuffled_list[swap_index], play->monkey_spawn_shuffled_list[index]); + } + // NOTE: Map =================================================================================== { TELY_AssetSpriteAnimation *sprite_anim = TELY_Asset_GetSpriteAnimation(&game->atlas_sprite_sheet, g_anim_names.map); @@ -340,18 +365,6 @@ static void FP_PlayReset(FP_Game *game, TELY_OS *os) } } - // NOTE: Monkey ============================================================ - { - Dqn_V2 monkey_base_p = Dqn_V2_InitNx2(base_mid_p.x + 400.f, base_mid_p.y + 16.f); - FP_GameEntityHandle monkey_a = FP_Entity_CreatePortalMonkey(game, Dqn_V2_InitNx2(monkey_base_p.x, monkey_base_p.y), "Monkey A"); - FP_GameEntityHandle monkey_b = FP_Entity_CreatePortalMonkey(game, Dqn_V2_InitNx2(monkey_base_p.x, monkey_base_p.y + 100.f), "Monkey B"); - FP_GameEntityHandle monkey_c = FP_Entity_CreatePortalMonkey(game, Dqn_V2_InitNx2(monkey_base_p.x, monkey_base_p.y - 100.f), "Monkey C"); - - Dqn_FArray_Add(&play->portal_monkeys, monkey_a); - Dqn_FArray_Add(&play->portal_monkeys, monkey_b); - Dqn_FArray_Add(&play->portal_monkeys, monkey_c); - } - { Dqn_V2 base_top_left_pos = Dqn_V2_InitNx2(1018, -335); Dqn_V2 base_bottom_right_pos = Dqn_V2_InitNx2(2050, +351); @@ -384,7 +397,7 @@ static void FP_PlayReset(FP_Game *game, TELY_OS *os) FP_Entity_CreateBillboard(game, Dqn_V2_InitNx2(2047, -720), FP_EntityBillboardState_RangeAttack, "Range Attack Billboard"); FP_Entity_CreateBillboard(game, Dqn_V2_InitNx2(-936, -500), FP_EntityBillboardState_Monkey, "Monkey Billboard"); FP_Entity_CreateBillboard(game, Dqn_V2_InitNx2(1898, 771), FP_EntityBillboardState_Strafe, "Strafe Billboard"); - FP_Entity_CreateBillboard(game, Dqn_V2_InitNx2(1068, 619), FP_EntityBillboardState_Build, "Build Billboard"); + play->billboard_build = FP_Entity_CreateBillboard(game, Dqn_V2_InitNx2(1068, 619), FP_EntityBillboardState_Build, "Build Billboard"); // NOTE: Camera ================================================================================ play->camera.world_pos = {}; @@ -412,7 +425,7 @@ void TELY_OS_DLLInit(TELY_OS *os) Dqn_f32 font_scalar = FP_TARGET_VIEWPORT_SIZE.w / DQN_CAST(Dqn_f32)os->core.window_size.x; game->font_size = DQN_CAST(uint16_t)(18 * font_scalar); - game->large_font_size = DQN_CAST(uint16_t)(game->font_size * .5f); + game->large_font_size = DQN_CAST(uint16_t)(game->font_size * 5.f); game->large_talkco_font_size = DQN_CAST(uint16_t)(game->font_size * 1.5f); game->xlarge_talkco_font_size = DQN_CAST(uint16_t)(game->font_size * 2.f); @@ -521,7 +534,7 @@ static void FP_EntityActionStateMachine(FP_Game *game, TELY_Audio *audio, TELY_O FP_GameEntity *portal_monkey = FP_Game_GetEntity(game, entity->carried_monkey); if (!FP_Game_IsNilEntity(portal_monkey)) { Dqn_Rect hit_box = FP_Game_CalcEntityWorldHitBox(game, entity->handle); - Dqn_f32 trig_t = DQN_CAST(Dqn_f32)input->timer_s * 2.f; + Dqn_f32 trig_t = DQN_CAST(Dqn_f32)game->play.clock_ms / 1000.f * 2.f; Dqn_V2 offset = Dqn_V2_InitNx2(DQN_COSF(trig_t), DQN_SINF(trig_t)) * 18.f; portal_monkey->local_pos = Dqn_Rect_InterpolatedPoint(hit_box, Dqn_V2_InitNx2(0.5f, -0.75f)) + offset; } @@ -651,23 +664,6 @@ static void FP_EntityActionStateMachine(FP_Game *game, TELY_Audio *audio, TELY_O entity->stamina -= FP_TERRY_DASH_STAMINA_COST; TELY_Audio_Play(audio, game->audio[FP_GameAudio_Woosh], 1.f); - - #if 0 - FP_GameRenderSprite *cosmetic_sprite = Dqn_FArray_Make(&entity->extra_cosmetic_anims, Dqn_ZeroMem_Yes); - if (cosmetic_sprite) { - cosmetic_sprite->asset = TELY_Asset_MakeAnimatedSprite(sheet, g_anim_names.terry_ghost, TELY_AssetFlip_No); - cosmetic_sprite->started_at_clock_ms = game->play.clock_ms; - cosmetic_sprite->height.meters = entity->sprite_height.meters; - - uint32_t max_rng_dist_x = DQN_CAST(uint32_t)(FP_Game_MetersToPixelsNx1(game, entity->sprite_height.meters) * 1.f); - uint32_t rng_x = Dqn_PCG32_Range(&game->play.rng, DQN_CAST(uint32_t)0, max_rng_dist_x); - cosmetic_sprite->offset.x = rng_x - (max_rng_dist_x * .5f); - - uint32_t max_rng_dist_y = DQN_CAST(uint32_t)(FP_Game_MetersToPixelsNx1(game, entity->sprite_height.meters) * .25f); - uint32_t rng_y = Dqn_PCG32_Range(&game->play.rng, DQN_CAST(uint32_t)0, max_rng_dist_y); - cosmetic_sprite->offset.y = -DQN_CAST(Dqn_f32)rng_y; - } - #endif } entity->action.sprite_alpha = Dqn_PCG32_NextF32(&game->play.rng); @@ -1297,9 +1293,11 @@ static void FP_EntityActionStateMachine(FP_Game *game, TELY_Audio *audio, TELY_O static void FP_Update(TELY_OS *os, FP_Game *game, TELY_OSInput *input, TELY_Audio *audio) { Dqn_Profiler_ZoneScopeWithIndex("FP_Update", FP_ProfileZone_FPUpdate); + if (game->play.state == FP_GameState_Pause) + return; game->play.update_counter++; - game->play.clock_ms = DQN_CAST(uint64_t)(os->input.timer_s * 1000.f); + game->play.clock_ms += DQN_CAST(uint64_t)(os->input.delta_ms); Dqn_ProfilerZone update_zone = Dqn_Profiler_BeginZoneWithIndex(DQN_STR8("FP_Update: Entity loop"), FP_ProfileZone_FPUpdate_EntityLoop); if (game->play.state == FP_GameState_Play && game->play.perry_joined != FP_GamePerryJoins_Enters) { @@ -1309,8 +1307,10 @@ static void FP_Update(TELY_OS *os, FP_Game *game, TELY_OSInput *input, TELY_Audi if (FP_DEVELOPER_MODE && TELY_OSInput_KeyIsReleased(input->mouse_left)) game->play.clicked_entity = game->play.prev_active_entity; - if (TELY_OSInput_ScanKeyIsPressed(input, TELY_OSInputScanKey_Escape)) + if (TELY_OSInput_ScanKeyIsPressed(input, TELY_OSInputScanKey_Escape)) { game->play.state = FP_GameState_Pause; + return; + } if (FP_DEVELOPER_MODE && game->play.clicked_entity.id) { if (TELY_OSInput_ScanKeyIsPressed(input, TELY_OSInputScanKey_Delete)) @@ -1976,12 +1976,56 @@ static void FP_Update(TELY_OS *os, FP_Game *game, TELY_OSInput *input, TELY_Audi } } - // NOTE: If all enemies for the current wave have been spawned and the cooldown has elapsed - // start the next wave. - if (game->play.enemies_spawned_this_wave >= game->play.enemies_per_wave && game->play.clock_ms >= game->play.wave_cooldown_timestamp_ms) { - game->play.enemies_per_wave = DQN_MAX(5 * DQN_CAST(uint32_t)game->play.mob_spawners.size, DQN_CAST(uint32_t)(game->play.enemies_per_wave * 1.5)); - game->play.enemies_spawned_this_wave = 0; - game->play.current_wave++; + // NOTE: Typically, the game starts the cooldown for the next wave after + // all enemies are spawned which makes the game fast paced. However, to + // give the player a break to reorganise, every 3 waves- we don't start + // the wave countdown until all enemies are killed. + bool all_enemies_spawned = game->play.enemies_spawned_this_wave >= game->play.enemies_per_wave; + bool advance_to_next_wave = false; + bool current_wave_is_break_for_player = game->play.current_wave % 3 == 0; + if (current_wave_is_break_for_player) { + Dqn_usize enemy_count = 0; + for (FP_GameEntityHandle spawner_handle : game->play.mob_spawners) { + FP_GameEntity *spawner = FP_Game_GetEntity(game, spawner_handle); + enemy_count += spawner->spawn_list.size; + } + + bool all_enemies_killed = enemy_count == 0; + advance_to_next_wave = all_enemies_spawned && all_enemies_killed; + } else { + advance_to_next_wave = all_enemies_spawned; + } + + if (advance_to_next_wave) { + // NOTE: If the cooldown timestamp is 0, the wave is complete and we + // haven't given the player cooldown yet, so we assign a timestamp for that + if (game->play.wave_cooldown_timestamp_ms == 0) { + game->play.wave_cooldown_timestamp_ms = game->play.clock_ms + FP_COOLDOWN_WAVE_TIME_MS; + } else { + // NOTE: Check if cooldown has elapsed, the next wave can start if so + if (game->play.clock_ms > game->play.wave_cooldown_timestamp_ms) { + game->play.enemies_per_wave = DQN_MAX(5 * DQN_CAST(uint32_t)game->play.mob_spawners.size, DQN_CAST(uint32_t)(game->play.enemies_per_wave * 1.5)); + game->play.enemies_spawned_this_wave = 0; // Important! Reset the spawn count + game->play.wave_cooldown_timestamp_ms = 0; // Important! We reset the timestamp for the next wave + + if (current_wave_is_break_for_player && game->play.current_wave != 0) { + // NOTE: We spawn a monkey at these wave intervals; + if (game->play.monkey_spawn_count < 3) { + DQN_ASSERT(game->play.monkey_spawn_count < DQN_ARRAY_UCOUNT(game->play.monkey_spawn_shuffled_list)); + uint8_t spawn_pos_index = game->play.monkey_spawn_shuffled_list[game->play.monkey_spawn_count++]; + DQN_ASSERT(spawn_pos_index < DQN_ARRAY_UCOUNT(FP_MONKEY_SPAWN_LOCATIONS)); + + Dqn_V2 spawn_pos = FP_MONKEY_SPAWN_LOCATIONS[spawn_pos_index]; + FP_GameEntityHandle portal_monkey = FP_Entity_CreatePortalMonkey(game, spawn_pos, "Portal Monkey"); + Dqn_FArray_Add(&game->play.portal_monkeys, portal_monkey); + TELY_Audio_Play(audio, game->audio[FP_GameAudio_Monkey], 1.f); + } + } + + game->play.current_wave++; + + } + } } } @@ -2059,11 +2103,11 @@ static void FP_Update(TELY_OS *os, FP_Game *game, TELY_OSInput *input, TELY_Audi if (entity->action.state != FP_EntityMobSpawnerState_Shutdown && game->play.enemies_spawned_this_wave < game->play.enemies_per_wave && entity->spawn_list.size < entity->spawn_cap) { // NOTE: Spawn new entities - if (input->timer_s >= entity->next_spawn_timestamp_s) { + if (game->play.clock_ms >= entity->next_spawn_timestamp_s) { Dqn_usize spawn_count = DQN_MIN(game->play.current_wave + 1, 8); for (Dqn_usize spawn_index = 0; spawn_index < spawn_count; spawn_index++) { uint16_t hp_adjustment = DQN_CAST(uint16_t)game->play.current_wave; - entity->next_spawn_timestamp_s = DQN_CAST(uint64_t)(input->timer_s + 2.5f); + entity->next_spawn_timestamp_s = DQN_CAST(uint64_t)(game->play.clock_ms + 2.5f); FP_SentinelListLink *link = FP_SentinelList_Make(&entity->spawn_list, game->play.chunk_pool); Dqn_V2 entity_world_pos = FP_Game_CalcEntityWorldPos(game, entity->handle); @@ -2093,8 +2137,6 @@ static void FP_Update(TELY_OS *os, FP_Game *game, TELY_OSInput *input, TELY_Audi FP_AppendMobSpawnerWaypoints(game, entity->handle, mob->handle); game->play.enemies_spawned_this_wave++; - if (game->play.enemies_spawned_this_wave >= game->play.enemies_per_wave) - game->play.wave_cooldown_timestamp_ms = game->play.clock_ms + 30'000; } } } @@ -2295,16 +2337,17 @@ static void FP_Update(TELY_OS *os, FP_Game *game, TELY_OSInput *input, TELY_Audi case FP_GameStateTutorial_ShowPlayer: { camera->world_pos_target = FP_Game_CalcEntityWorldPos(game, game->play.players.data[0]) * game->play.camera.scale; if (camera_arrived) { - game->play.tutorial_state = DQN_CAST(FP_GameStateTutorial)(DQN_CAST(uint32_t)game->play.tutorial_state + 1); - game->play.tutorial_wait_end_time_s = input->timer_s + 2.0; + game->play.tutorial_state = DQN_CAST(FP_GameStateTutorial)(DQN_CAST(uint32_t)game->play.tutorial_state + 1); + game->play.tutorial_wait_end_time_ms = game->play.clock_ms + 3000; } } break; case FP_GameStateTutorial_ShowPortalOneWait: /*FALLTHRU*/ case FP_GameStateTutorial_ShowPortalTwoWait: /*FALLTHRU*/ case FP_GameStateTutorial_ShowPortalThreeWait: /*FALLTHRU*/ + case FP_GameStateTutorial_ShowBillboardBuildWait: /*FALLTHRU*/ case FP_GameStateTutorial_ShowPlayerWait: { - if (input->timer_s > game->play.tutorial_wait_end_time_s) { + if (game->play.clock_ms > game->play.tutorial_wait_end_time_ms) { game->play.tutorial_state = DQN_CAST(FP_GameStateTutorial)(DQN_CAST(uint32_t)game->play.tutorial_state + 1); } } break; @@ -2323,7 +2366,15 @@ static void FP_Update(TELY_OS *os, FP_Game *game, TELY_OSInput *input, TELY_Audi if (camera_arrived) { game->play.tutorial_state = DQN_CAST(FP_GameStateTutorial)(DQN_CAST(uint32_t)game->play.tutorial_state + 1); - game->play.tutorial_wait_end_time_s = input->timer_s + 2.0; + game->play.tutorial_wait_end_time_ms = game->play.clock_ms + 2000; + } + } break; + + case FP_GameStateTutorial_ShowBillboardBuild: { + camera->world_pos_target = FP_Game_CalcEntityWorldPos(game, game->play.billboard_build) * game->play.camera.scale; + if (camera_arrived) { + game->play.tutorial_state = DQN_CAST(FP_GameStateTutorial)(DQN_CAST(uint32_t)game->play.tutorial_state + 1); + game->play.tutorial_wait_end_time_ms = game->play.clock_ms + 4000; } } break; @@ -2630,8 +2681,8 @@ static void FP_Render(FP_Game *game, TELY_OS *os, TELY_Renderer *renderer, TELY_ DQN_FOR_UINDEX(anim_index, entity->extra_cosmetic_anims.size) { FP_GameRenderSprite *sprite = entity->extra_cosmetic_anims.data + anim_index; - 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); + 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); if (raw_anim_frame > sprite->asset.anim->count && !sprite->loop) { anim_index = Dqn_FArray_EraseRange(&entity->extra_cosmetic_anims, anim_index, 1, Dqn_ArrayErase_Unstable).it_index; continue; @@ -2688,13 +2739,13 @@ static void FP_Render(FP_Game *game, TELY_OS *os, TELY_Renderer *renderer, TELY_ dest_rect.size *= 1.5f; dest_rect.pos = Dqn_Rect_InterpolatedPoint(world_hit_box, Dqn_V2_InitNx2(0.0f, 0.0f)) - (Dqn_V2_InitNx2(dest_rect.size.x * .6f, dest_rect.size.y * .7f)); - dest_rect.pos.y += (DQN_SINF(DQN_CAST(Dqn_f32)input->timer_s * 2.f) + 1.f / 2.f) * dest_rect.size.y * .005f; + dest_rect.pos.y += (DQN_SINF(DQN_CAST(Dqn_f32)game->play.clock_ms / 1000.f * 2.f) + 1.f / 2.f) * dest_rect.size.y * .005f; } else { dest_rect.pos = Dqn_Rect_InterpolatedPoint(world_hit_box, Dqn_V2_InitNx2(0.0f, 0.0f)) - (Dqn_V2_InitNx2(dest_rect.size.x * .7f, dest_rect.size.y * .9f)); } - dest_rect.pos.y += (DQN_SINF(DQN_CAST(Dqn_f32)input->timer_s * 2.f) + 1.f / 2.f) * dest_rect.size.y * .1f; + dest_rect.pos.y += (DQN_SINF(DQN_CAST(Dqn_f32)game->play.clock_ms / 1000.f * 2.f) + 1.f / 2.f) * dest_rect.size.y * .1f; TELY_Render_TextureColourV4(renderer, game->atlas_sprite_sheet.tex_handle, @@ -2960,7 +3011,7 @@ static void FP_Render(FP_Game *game, TELY_OS *os, TELY_Renderer *renderer, TELY_ game->play.global_camera_trauma01 = 1.f; game->play.perry_join_splash_screen_shake_triggered = true; game->play.perry_join_flash_alpha = 1.f; - game->play.perry_join_splash_screen_end_ms = DQN_CAST(uint64_t)((DQN_CAST(Dqn_f32)input->timer_s * 1000.f) + 2000); + game->play.perry_join_splash_screen_end_ms = game->play.clock_ms + 2000; } } @@ -2975,7 +3026,7 @@ static void FP_Render(FP_Game *game, TELY_OS *os, TELY_Renderer *renderer, TELY_ TELY_AssetSpriteAnimation *anim = TELY_Asset_GetSpriteAnimation(&game->atlas_sprite_sheet, g_anim_names.intro_screen_perry_joins_the_fight); Dqn_Rect tex_rect = game->atlas_sprite_sheet.rects.data[anim->index]; - Dqn_f32 sin_t = (DQN_SINF(DQN_CAST(Dqn_f32)input->timer_s * 3.f) + 1) / 2.f; + Dqn_f32 sin_t = (DQN_SINF(DQN_CAST(Dqn_f32)game->play.clock_ms / 1000.f * 3.f) + 1) / 2.f; Dqn_Rect dest_rect = {}; dest_rect.size = tex_rect.size * (tex_scalar * 1.5f) + (tex_rect.size * (0.005f * sin_t)); dest_rect.pos = Dqn_Rect_InterpolatedPoint(window_rect, Dqn_V2_InitNx2(1, 1)) - dest_rect.size; @@ -2987,7 +3038,7 @@ static void FP_Render(FP_Game *game, TELY_OS *os, TELY_Renderer *renderer, TELY_ shake_offset.y = (Dqn_PCG32_NextF32(&game->play.rng) * max_shake_dist - half_shake_dist) * game->play.global_camera_trauma01; dest_rect.pos += shake_offset; - if ((input->timer_s * 1000) > (game->play.perry_join_splash_screen_end_ms)) { + if (game->play.clock_ms > game->play.perry_join_splash_screen_end_ms) { game->play.perry_join_splash_pos_offset.x -= dest_rect.size.x * (12.f * DQN_CAST(Dqn_f32)input->delta_s); dest_rect.pos += game->play.perry_join_splash_pos_offset; if (dest_rect.pos.x < -dest_rect.size.x) @@ -3051,7 +3102,7 @@ static void FP_Render(FP_Game *game, TELY_OS *os, TELY_Renderer *renderer, TELY_ DQN_ASSERT(game->play.players.size <= DQN_ARRAY_UCOUNT(player_avatar_base_pos)); DQN_ASSERTF(game->play.players.size <= 2, "We hardcode 2 player support"); - if (game->play.players.size == 1) { + if (game->play.players.size == 1 && game->play.state != FP_GameState_Tutorial) { // NOTE: We show the Press to join for the remaining 2nd player TELY_Render_PushTransform(renderer, Dqn_M2x3_Identity()); TELY_Render_PushFontSize(renderer, game->talkco_font, game->font_size); @@ -3070,7 +3121,7 @@ static void FP_Render(FP_Game *game, TELY_OS *os, TELY_Renderer *renderer, TELY_ Dqn_f32 font_height = TELY_Render_FontSize(renderer) * os->core.dpi_scale; Dqn_V2 base_p = player_avatar_base_pos[game->play.players.size]; TELY_Render_TextF(renderer, base_p, Dqn_V2_Zero, "Press %.*s", DQN_STR_FMT(join_game_key)); base_p.y += font_height; - FP_ListenForNewPlayer(input, game); + FP_ListenForNewPlayer(input, game, false /*tutorial_is_allowed*/); } // NOTE: Render the player(s) HUD and merchant menu interaction ============================ @@ -3165,7 +3216,7 @@ static void FP_Render(FP_Game *game, TELY_OS *os, TELY_Renderer *renderer, TELY_ 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); + Dqn_f32 sin_t = DQN_SINF(DQN_CAST(Dqn_f32)game->play.clock_ms / 1000.f * 3.f); merchant_menu_rect.pos.y += sin_t * 4.f; TELY_Render_TextureColourV4(renderer, @@ -3225,7 +3276,7 @@ static void FP_Render(FP_Game *game, TELY_OS *os, TELY_Renderer *renderer, TELY_ interact_btn_rect.size = TELY_Asset_MeasureText(assets, font_size, key_bind_label); Dqn_Rect key_bind_rect = interact_btn_rect; - key_bind_rect.pos.y += interact_btn_rect.size.y * .3f; + key_bind_rect.pos.y += interact_btn_rect.size.y * .8f; TELY_Render_RectColourV4(renderer, Dqn_Rect_Expand(key_bind_rect, 2.f), TELY_RenderShapeMode_Fill, TELY_Colour_V4Alpha(keybind_btn_shadow_colour, tex_mod_colour.a)); TELY_Render_PushColourV4(renderer, TELY_Colour_V4Alpha(colour_accent_yellow, tex_mod_colour.a)); @@ -3234,7 +3285,7 @@ static void FP_Render(FP_Game *game, TELY_OS *os, TELY_Renderer *renderer, TELY_ } // NOTE: Render the $ cost - dollar_text_label_pos = Dqn_Rect_InterpolatedPoint(interact_btn_rect, Dqn_V2_InitNx2(0.5f, -0.8f)); + dollar_text_label_pos = Dqn_Rect_InterpolatedPoint(interact_btn_rect, Dqn_V2_InitNx2(0.5f, -1.5f)); TELY_Render_TextF(renderer, dollar_text_label_pos, Dqn_V2_InitNx2(0.5, 0.f), "$%u", *mapping.building_base_price); } @@ -3340,7 +3391,7 @@ static void FP_Render(FP_Game *game, TELY_OS *os, TELY_Renderer *renderer, TELY_ interact_btn_rect.size = TELY_Asset_MeasureText(assets, font_size, key_bind_label); Dqn_Rect key_bind_rect = interact_btn_rect; - key_bind_rect.pos.y += interact_btn_rect.size.y * .3f; + key_bind_rect.pos.y += interact_btn_rect.size.y * .8f; TELY_Render_RectColourV4(renderer, Dqn_Rect_Expand(key_bind_rect, 2.f), TELY_RenderShapeMode_Fill, TELY_Colour_V4Alpha(keybind_btn_shadow_colour, tex_mod_colour.a)); TELY_Render_PushColourV4(renderer, TELY_Colour_V4Alpha(colour_accent_yellow, tex_mod_colour.a)); @@ -3365,7 +3416,7 @@ static void FP_Render(FP_Game *game, TELY_OS *os, TELY_Renderer *renderer, TELY_ } // NOTE: Render the $ cost - dollar_text_label_pos = Dqn_Rect_InterpolatedPoint(interact_btn_rect, Dqn_V2_InitNx2(1.f, -0.8f)); + dollar_text_label_pos = Dqn_Rect_InterpolatedPoint(interact_btn_rect, Dqn_V2_InitNx2(1.f, -1.5f)); TELY_Render_TextF(renderer, dollar_text_label_pos, Dqn_V2_InitNx2(0.5, 0.f), "$%u", *mapping.upgrade_base_price); } @@ -3824,19 +3875,34 @@ static void FP_Render(FP_Game *game, TELY_OS *os, TELY_Renderer *renderer, TELY_ struct LineSize { Dqn_Str8 line; Dqn_V2 size; - } lines[] = { - {DQN_STR8("Defend Terry's heart from the oncoming hordes!"), Dqn_V2_Zero}, }; - Dqn_V2 origin = Dqn_V2_InitV2I(os->core.window_size) * Dqn_V2_InitNx2(.5f, .5f); - Dqn_f32 scaled_font_size = TELY_Render_FontSize(renderer) * os->core.dpi_scale; - Dqn_V2 text_min = Dqn_V2_InitNx2(origin.x, origin.y - scaled_font_size * .5f); - Dqn_V2 text_max = Dqn_V2_InitNx2(text_min.x, text_min.y + DQN_ARRAY_UCOUNT(lines) * scaled_font_size); + Dqn_FArray lines = {}; + if (game->play.tutorial_state == FP_GameStateTutorial_ShowPlayer || game->play.tutorial_state == FP_GameStateTutorial_ShowPlayerWait) { + LineSize *line_size = Dqn_FArray_Make(&lines, Dqn_ZeroMem_Yes); + line_size->line = DQN_STR8("Defend Terry's heart from the oncoming cherries!"); + } else if (game->play.tutorial_state == FP_GameStateTutorial_ShowBillboardBuild || game->play.tutorial_state == FP_GameStateTutorial_ShowBillboardBuildWait) { + { + LineSize *line_size = Dqn_FArray_Make(&lines, Dqn_ZeroMem_Yes); + line_size->line = DQN_STR8("Lookout for billboards for tips!"); + } + { + LineSize *line_size = Dqn_FArray_Make(&lines, Dqn_ZeroMem_Yes); + line_size->line = DQN_STR8("Build buildings to slow cherries down and afford more time"); + } + } else { + LineSize *line_size = Dqn_FArray_Make(&lines, Dqn_ZeroMem_Yes); + line_size->line = DQN_STR8("Defeat the cherries spawning from the portals"); + } + + Dqn_f32 scaled_font_size = TELY_Render_FontSize(renderer) * os->core.dpi_scale; + Dqn_V2 text_bounding_size = {}; + text_bounding_size.y = lines.size * scaled_font_size; + for (LineSize &line_size_ : lines) { - LineSize *line_size = &line_size_; - line_size->size = TELY_Asset_MeasureText(assets, TELY_Render_ActiveFont(renderer), line_size->line); - text_min.x = DQN_MIN(origin.x, origin.x - line_size->size.x * .5f); - text_max.x = DQN_MAX(origin.x, origin.x + line_size->size.x * .58f); // TODO: Wrong calc, but too lazy to figure it out + LineSize *line_size = &line_size_; + line_size->size = TELY_Asset_MeasureText(assets, TELY_Render_ActiveFont(renderer), line_size->line); + text_bounding_size.x = DQN_MAX(text_bounding_size.x, line_size->size.x); } // NOTE: Calculate terry avatar variables @@ -3847,19 +3913,17 @@ static void FP_Render(FP_Game *game, TELY_OS *os, TELY_Renderer *renderer, TELY_ Dqn_Rect dest_rect = {}; dest_rect.size = tex_rect.size * tex_scalar; - dest_rect.pos = text_min - Dqn_V2_InitNx2(dest_rect.size.x * 1.f, dest_rect.size.y * .5f); + dest_rect.pos = Dqn_V2_InitV2I(os->core.window_size) * Dqn_V2_InitNx2(0.5f, 0.8f) - (dest_rect.size * .5f) - Dqn_V2_InitNx2(text_bounding_size.x * .5f, 0.f); - Dqn_Rect terry_bounding_rect = Dqn_Rect_ExpandV2(dest_rect, Dqn_V2_InitNx2(scaled_font_size * .5f, scaled_font_size * .25f)); + Dqn_Rect terry_bounding_rect = dest_rect; // NOTE: Draw text { - Dqn_V2 draw_p = Dqn_Rect_InterpolatedPoint(terry_bounding_rect, Dqn_V2_InitNx2(1.1f, 0.5f)); - Dqn_Rect bounding_rect = Dqn_Rect_InitV2x2(text_min, text_max - text_min); - bounding_rect = Dqn_Rect_ExpandV2(bounding_rect, Dqn_V2_InitNx2(scaled_font_size * .1f, scaled_font_size * .1f)); + Dqn_V2 draw_p = Dqn_Rect_InterpolatedPoint(terry_bounding_rect, Dqn_V2_InitNx2(1.0f, 0.5f)); + + Dqn_Rect bounding_rect = Dqn_Rect_InitV2x2(draw_p, text_bounding_size); + bounding_rect = Dqn_Rect_ExpandV2(bounding_rect, Dqn_V2_InitNx2(scaled_font_size * 1.5f, scaled_font_size * .1f)); - Dqn_RectMinMax min_max_bounds = Dqn_Rect_MinMax(bounding_rect); - min_max_bounds.max.y = DQN_MAX(min_max_bounds.max.y, terry_bounding_rect.pos.y + terry_bounding_rect.size.y); - bounding_rect = Dqn_Rect_InitV2x2(min_max_bounds.min, min_max_bounds.max - min_max_bounds.min); TELY_Render_RectColourV4(renderer, Dqn_Rect_Expand(bounding_rect, 5.f), TELY_RenderShapeMode_Fill, TELY_COLOUR_BLACK_V4); TELY_Render_RectColourV4(renderer, bounding_rect, TELY_RenderShapeMode_Fill, TELY_Colour_V4Alpha(maroon_colour, 1.f)); @@ -3871,8 +3935,6 @@ static void FP_Render(FP_Game *game, TELY_OS *os, TELY_Renderer *renderer, TELY_ } // NOTE: Draw terry - TELY_Render_RectColourV4(renderer, Dqn_Rect_Expand(terry_bounding_rect, 5.f), TELY_RenderShapeMode_Fill, TELY_COLOUR_BLACK_V4); - TELY_Render_RectColourV4(renderer, terry_bounding_rect, TELY_RenderShapeMode_Fill, TELY_Colour_V4Alpha(maroon_colour, 1.f)); TELY_Render_TextureColourV4(renderer, game->atlas_sprite_sheet.tex_handle, tex_rect, @@ -3883,6 +3945,35 @@ static void FP_Render(FP_Game *game, TELY_OS *os, TELY_Renderer *renderer, TELY_ } + if (game->play.state == FP_GameState_Pause) { + TELY_Render_PushTransform(renderer, Dqn_M2x3_Identity()); + TELY_Render_PushColourV4(renderer, TELY_COLOUR_WHITE_V4); + DQN_DEFER { + TELY_Render_PopColourV4(renderer); + TELY_Render_PopTransform(renderer); + }; + + TELY_Render_RectColourV4( + renderer, + Dqn_Rect_InitNx4(0, 0, DQN_CAST(Dqn_f32)os->core.window_size.x, DQN_CAST(Dqn_f32)os->core.window_size.y), + TELY_RenderShapeMode_Fill, + TELY_Colour_V4Alpha(TELY_COLOUR_BLACK_V4, .8f)); + + Dqn_V2 draw_p = Dqn_V2_InitV2I(os->core.window_size) * Dqn_V2_InitNx2(0.5f, 0.5f); + + TELY_Render_PushFontSize(renderer, game->talkco_font, game->xlarge_talkco_font_size); + TELY_Render_TextF(renderer, draw_p, Dqn_V2_InitNx1(0.5f), "Paused"); draw_p.y += TELY_Render_FontSize(renderer) * os->core.dpi_scale; + TELY_Render_PopFont(renderer); + + TELY_Render_PushFontSize(renderer, game->talkco_font, game->large_talkco_font_size); + TELY_Render_TextF(renderer, draw_p, Dqn_V2_InitNx1(0.5f), "Press enter to resume"); draw_p.y += TELY_Render_FontSize(renderer) * os->core.dpi_scale; + TELY_Render_PopFont(renderer); + TELY_Render_PopColourV4(renderer); + + if (TELY_OSInput_ScanKeyIsPressed(input, TELY_OSInputScanKey_Return)) + game->play.state = FP_GameState_Play; + } + // NOTE: Add scanlines into the game for A E S T H E T I C S =================================== if (game->play.state == FP_GameState_Play || game->play.state == FP_GameState_Tutorial) { Dqn_V2 screen_size = Dqn_V2_InitNx2(os->core.window_size.w, os->core.window_size.h); @@ -3891,8 +3982,7 @@ static void FP_Render(FP_Game *game, TELY_OS *os, TELY_Renderer *renderer, TELY_ FP_GameRenderScanlines(renderer, scanline_gap, scanline_thickness, screen_size); } - if (game->play.state == FP_GameState_IntroScreen || game->play.state == FP_GameState_WinGame || game->play.state == FP_GameState_LoseGame || game->play.state == FP_GameState_Pause) { - + if (game->play.state == FP_GameState_IntroScreen || game->play.state == FP_GameState_WinGame || game->play.state == FP_GameState_LoseGame) { TELY_Render_PushTransform(renderer, Dqn_M2x3_Identity()); DQN_DEFER { TELY_Render_PopTransform(renderer); }; @@ -4007,7 +4097,13 @@ static void FP_Render(FP_Game *game, TELY_OS *os, TELY_Renderer *renderer, TELY_ TELY_Render_PushFontSize(renderer, game->inter_regular_font, game->font_size); Dqn_f32 t = (DQN_SINF(DQN_CAST(Dqn_f32)input->timer_s * 5.f) + 1.f) / 2.f; TELY_Render_PushColourV4(renderer, TELY_Colour_V4Alpha(TELY_COLOUR_WHITE_V4, t)); - TELY_Render_TextF(renderer, Dqn_Rect_InterpolatedPoint(window_rect, Dqn_V2_InitNx2(0.5f, 0.925f)), Dqn_V2_InitNx1(0.5f), "Press or to %s", game->play.state == FP_GameState_IntroScreen ? "start" : "restart"); + + Dqn_V2 text_p = Dqn_Rect_InterpolatedPoint(window_rect, Dqn_V2_InitNx2(0.5f, 0.925f)); + TELY_Render_TextF(renderer, text_p, Dqn_V2_InitNx1(0.5f), "Press or to %s", game->play.state == FP_GameState_IntroScreen ? "start" : "restart"); + text_p.y += TELY_Render_ActiveFont(renderer).size * os->core.dpi_scale; + + TELY_Render_TextF(renderer, text_p, Dqn_V2_InitNx1(0.5f), "Press or for the tutorial"); + text_p.y += TELY_Render_ActiveFont(renderer).size * os->core.dpi_scale; TELY_Render_PopColourV4(renderer); TELY_Render_PopFont(renderer); @@ -4015,22 +4111,14 @@ static void FP_Render(FP_Game *game, TELY_OS *os, TELY_Renderer *renderer, TELY_ if (game->play.state == FP_GameState_LoseGame) FP_PlayReset(game, os); - if (FP_ListenForNewPlayer(input, game)) - game->play.state = FP_GameState_Tutorial; - - } else if (game->play.state == FP_GameState_Pause) { - TELY_Render_PushFontSize(renderer, game->inter_regular_font, game->large_font_size); - TELY_Render_TextF(renderer, draw_p, Dqn_V2_Zero, "Paused"); draw_p.y += TELY_Render_FontSize(renderer) * os->core.dpi_scale; - TELY_Render_PopFont(renderer); - - TELY_Render_PushFontSize(renderer, game->inter_regular_font, game->font_size); - TELY_Render_TextF(renderer, draw_p, Dqn_V2_Zero, "Press enter to resume"); draw_p.y += TELY_Render_FontSize(renderer) * os->core.dpi_scale; - TELY_Render_PopFont(renderer); - TELY_Render_PopColourV4(renderer); - - if (TELY_OSInput_ScanKeyIsPressed(input, TELY_OSInputScanKey_Return)) - game->play.state = FP_GameState_Play; - + FP_ListenForNewPlayerResult new_player = FP_ListenForNewPlayer(input, game, true /*tutorial_is_allowed*/); + if (new_player.yes) { + if (new_player.tutorial_requested) { + game->play.state = FP_GameState_Tutorial; + } else { + game->play.state = FP_GameState_Play; + } + } } else { DQN_ASSERT(game->play.state == FP_GameState_WinGame); TELY_Render_PushFontSize(renderer, game->inter_regular_font, game->large_font_size); diff --git a/feely_pona.h b/feely_pona.h index 1524607..25d8d6f 100644 --- a/feely_pona.h +++ b/feely_pona.h @@ -165,6 +165,12 @@ struct FP_ParticleDescriptor Dqn_usize duration_ms; }; +struct FP_ListenForNewPlayerResult +{ + bool yes; + bool tutorial_requested; +}; + #if defined(DQN_PLATFORM_EMSCRIPTEN) #define FP_DEVELOPER_MODE 0 #else diff --git a/feely_pona_build.cpp b/feely_pona_build.cpp index fdda7bd..2180837 100644 --- a/feely_pona_build.cpp +++ b/feely_pona_build.cpp @@ -543,7 +543,7 @@ int main(int argc, char const **argv) Dqn_CPPBuildCompileFile{{}, Dqn_FsPath_ConvertF(scratch.arena, "%.*s/feely_pona_unity.cpp", DQN_STR_FMT(code_dir)) }, }); - Dqn_Str8 output_name = DQN_STR8("Terry_Cherry_Emscripten"); + Dqn_Str8 output_name = DQN_STR8("Terry_Cherry"); Dqn_List compile_flags = Dqn_List_InitCArrayCopy(scratch.arena, 32, { DQN_STR8("cmd"), DQN_STR8("/C"), DQN_STR8("emcc.bat"), DQN_STR8("-o"), Dqn_Str8_InitF(scratch.allocator, "%.*s.html", DQN_STR_FMT(output_name)), @@ -588,7 +588,7 @@ int main(int argc, char const **argv) for (Dqn_Str8 file_ext : generated_file_extension) { Dqn_Str8 src_path = Dqn_FsPath_ConvertF(scratch.arena, "%.*s/%.*s.%.*s", DQN_STR_FMT(build_dir), DQN_STR_FMT(output_name), DQN_STR_FMT(file_ext)); - Dqn_Str8 dest_path = Dqn_FsPath_ConvertF(scratch.arena, "%.*s/Terry_Cherry.%.*s", DQN_STR_FMT(folder_path), DQN_STR_FMT(file_ext)); + Dqn_Str8 dest_path = Dqn_FsPath_ConvertF(scratch.arena, "%.*s/%.*s.%.*s", DQN_STR_FMT(folder_path), DQN_STR_FMT(output_name), DQN_STR_FMT(file_ext)); Dqn_Str8 cmd = Dqn_Str8_InitF(scratch.allocator, "cmd /C move /Y %.*s %.*s", DQN_STR_FMT(src_path), DQN_STR_FMT(dest_path)); if (dry_run) { Dqn_Print_StdLnF(Dqn_PrintStd_Out, "%.*s\n", DQN_STR_FMT(cmd)); diff --git a/feely_pona_game.h b/feely_pona_game.h index df1c327..52deced 100644 --- a/feely_pona_game.h +++ b/feely_pona_game.h @@ -376,9 +376,25 @@ enum FP_GameStateTutorial FP_GameStateTutorial_ShowPortalTwoWait, FP_GameStateTutorial_ShowPortalThree, FP_GameStateTutorial_ShowPortalThreeWait, + FP_GameStateTutorial_ShowBillboardBuild, + FP_GameStateTutorial_ShowBillboardBuildWait, FP_GameStateTutorial_Count, }; +Dqn_V2 const FP_MONKEY_SPAWN_LOCATIONS[] = +{ + Dqn_V2_InitNx2(-592, 538), + Dqn_V2_InitNx2(-1503, -568), + Dqn_V2_InitNx2(1890, 1150), + Dqn_V2_InitNx2(1815, -1192), + Dqn_V2_InitNx2(520, 1230), + Dqn_V2_InitNx2(-934, -238), + Dqn_V2_InitNx2(1915, 15), + Dqn_V2_InitNx2(247, 560), + Dqn_V2_InitNx2(-290, -1120), + Dqn_V2_InitNx2(1126, -646), +}; + struct FP_GamePlay { TELY_ChunkPool *chunk_pool; @@ -402,6 +418,7 @@ struct FP_GamePlay FP_GameEntityHandle merchant_graveyard; FP_GameEntityHandle merchant_gym; FP_GameEntityHandle merchant_phone_company; + FP_GameEntityHandle billboard_build; FP_GameEntityHandle clicked_entity; FP_GameEntityHandle hot_entity; @@ -445,7 +462,10 @@ struct FP_GamePlay uint32_t particle_next_index; FP_GameStateTutorial tutorial_state; - Dqn_f64 tutorial_wait_end_time_s; + uint64_t tutorial_wait_end_time_ms; + + uint8_t monkey_spawn_shuffled_list[DQN_ARRAY_UCOUNT(FP_MONKEY_SPAWN_LOCATIONS)]; + uint8_t monkey_spawn_count; }; struct FP_Game @@ -492,7 +512,7 @@ FP_GamePlaceableBuilding const PLACEABLE_BUILDINGS[] = { {FP_EntityType_AirportTerry, FP_EntityAirportTerryState_Idle}, {FP_EntityType_ChurchTerry, FP_EntityChurchTerryState_Idle}, {FP_EntityType_ClubTerry, FP_EntityClubTerryState_Idle}, - {FP_EntityType_KennelTerry, FP_EntityKennelTerryState_Idle}, + // {FP_EntityType_KennelTerry, FP_EntityKennelTerryState_Idle}, }; struct FP_GameCanMoveToPositionResult