From 285cc9b5adfd747399e4ba4367eb019a0bb81a56 Mon Sep 17 00:00:00 2001 From: doyle Date: Sun, 29 Oct 2023 17:30:08 +1100 Subject: [PATCH] fp: Add quick tutorial at beginning of game --- build.bat | 22 +---- build_all.bat | 25 ++++++ feely_pona.cpp | 190 +++++++++++++++++++++++++++++++++++-------- feely_pona_build.cpp | 2 +- feely_pona_game.cpp | 1 + feely_pona_game.h | 17 ++++ 6 files changed, 200 insertions(+), 57 deletions(-) create mode 100644 build_all.bat diff --git a/build.bat b/build.bat index f395b9e..7716f54 100644 --- a/build.bat +++ b/build.bat @@ -1,25 +1,5 @@ @echo off setlocal -set script_dir_backslash=%~dp0 -set script_dir=%script_dir_backslash:~0,-1% -set build_dir=%script_dir%\Build -set code_dir=%script_dir% +call build_all.bat --fast-dev-build || exit /b 1 -REM Bootstrap a version -git show -s --date=format:%%Y-%%m-%%d --format=%%cd HEAD> feely_pona_version.txt -git rev-parse --short=8 HEAD>> feely_pona_version.txt -git rev-list --count HEAD>> feely_pona_version.txt - -REM Bootstrap the build program -mkdir %build_dir% 2>nul -pushd %build_dir% -cl /nologo /Z7 /W4 %code_dir%\feely_pona_build.cpp || exit /B 1 -copy feely_pona_build.exe %code_dir% 1>nul -popd - -REM Run the build program -%code_dir%\feely_pona_build.exe %* || exit /B 1 - -popd -exit /B 1 diff --git a/build_all.bat b/build_all.bat new file mode 100644 index 0000000..f395b9e --- /dev/null +++ b/build_all.bat @@ -0,0 +1,25 @@ +@echo off +setlocal + +set script_dir_backslash=%~dp0 +set script_dir=%script_dir_backslash:~0,-1% +set build_dir=%script_dir%\Build +set code_dir=%script_dir% + +REM Bootstrap a version +git show -s --date=format:%%Y-%%m-%%d --format=%%cd HEAD> feely_pona_version.txt +git rev-parse --short=8 HEAD>> feely_pona_version.txt +git rev-list --count HEAD>> feely_pona_version.txt + +REM Bootstrap the build program +mkdir %build_dir% 2>nul +pushd %build_dir% +cl /nologo /Z7 /W4 %code_dir%\feely_pona_build.cpp || exit /B 1 +copy feely_pona_build.exe %code_dir% 1>nul +popd + +REM Run the build program +%code_dir%\feely_pona_build.exe %* || exit /B 1 + +popd +exit /B 1 diff --git a/feely_pona.cpp b/feely_pona.cpp index 311d490..5ddf208 100644 --- a/feely_pona.cpp +++ b/feely_pona.cpp @@ -305,9 +305,22 @@ static void FP_PlayReset(FP_Game *game, TELY_OS *os) Dqn_V2 base_mid_p = Dqn_V2_InitNx2(1580, 0.f); { + Dqn_V2 mid_lane_mob_spawner_pos = Dqn_V2_InitNx2(play->map->local_hit_box_size.w * -0.5f + 128.f, 0.f); + Dqn_V2 bottom_lane_mob_spawner_pos = Dqn_V2_InitNx2(mid_lane_mob_spawner_pos.x, mid_lane_mob_spawner_pos.y + 932.f); + Dqn_V2 top_lane_mob_spawner_pos = Dqn_V2_InitNx2(mid_lane_mob_spawner_pos.x, mid_lane_mob_spawner_pos.y - 915.f); + Dqn_usize spawn_cap = 16; + + // NOTE: Top lane spawner =================================================================== + { + FP_GameEntityHandle mob_spawner = FP_Entity_CreateMobSpawner(game, top_lane_mob_spawner_pos, spawn_cap, "Mob spawner"); + FP_Game_PushParentEntity(game, mob_spawner); + FP_Entity_CreateWaypointF(game, Dqn_V2_InitNx2(-top_lane_mob_spawner_pos.x + base_mid_p.x, 0.f), "Waypoint"); + FP_Entity_CreateWaypointF(game, Dqn_V2_InitNx2(-top_lane_mob_spawner_pos.x + base_mid_p.x, +915.f), "Waypoint"); + FP_Game_PopParentEntity(game); + Dqn_FArray_Add(&play->mob_spawners, mob_spawner); + } + // NOTE: Mid lane mob spawner ================================================================== - Dqn_V2 mid_lane_mob_spawner_pos = Dqn_V2_InitNx2(play->map->local_hit_box_size.w * -0.5f + 128.f, 0.f); - Dqn_usize spawn_cap = 16; { FP_GameEntityHandle mob_spawner = FP_Entity_CreateMobSpawner(game, mid_lane_mob_spawner_pos, spawn_cap, "Mob spawner"); FP_Game_PushParentEntity(game, mob_spawner); @@ -317,8 +330,6 @@ static void FP_PlayReset(FP_Game *game, TELY_OS *os) } // NOTE: Bottom lane spawner =================================================================== - #if 1 - 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"); FP_Game_PushParentEntity(game, mob_spawner); @@ -327,18 +338,6 @@ static void FP_PlayReset(FP_Game *game, TELY_OS *os) FP_Game_PopParentEntity(game); Dqn_FArray_Add(&play->mob_spawners, mob_spawner); } - - // NOTE: Top lane spawner =================================================================== - Dqn_V2 top_lane_mob_spawner_pos = Dqn_V2_InitNx2(mid_lane_mob_spawner_pos.x, mid_lane_mob_spawner_pos.y - 915.f); - { - FP_GameEntityHandle mob_spawner = FP_Entity_CreateMobSpawner(game, top_lane_mob_spawner_pos, spawn_cap, "Mob spawner"); - FP_Game_PushParentEntity(game, mob_spawner); - FP_Entity_CreateWaypointF(game, Dqn_V2_InitNx2(-top_lane_mob_spawner_pos.x + base_mid_p.x, 0.f), "Waypoint"); - FP_Entity_CreateWaypointF(game, Dqn_V2_InitNx2(-top_lane_mob_spawner_pos.x + base_mid_p.x, +915.f), "Waypoint"); - FP_Game_PopParentEntity(game); - Dqn_FArray_Add(&play->mob_spawners, mob_spawner); - } - #endif } // NOTE: Monkey ============================================================ @@ -1300,9 +1299,10 @@ static void FP_Update(TELY_OS *os, FP_Game *game, TELY_OSInput *input, TELY_Audi Dqn_Profiler_ZoneScopeWithIndex("FP_Update", FP_ProfileZone_FPUpdate); game->play.update_counter++; + game->play.clock_ms = DQN_CAST(uint64_t)(os->input.timer_s * 1000.f); + 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) { - game->play.clock_ms = DQN_CAST(uint64_t)(os->input.timer_s * 1000.f); DQN_MSVC_WARNING_PUSH DQN_MSVC_WARNING_DISABLE(4127) // Conditional expression is constant 'FP_DEVELOPER_MODE' @@ -2049,7 +2049,7 @@ static void FP_Update(TELY_OS *os, FP_Game *game, TELY_OSInput *input, TELY_Audi } // NOTE: Mob spawner ======================================================================= - if (entity->type == FP_EntityType_MobSpawner && !game->play.debug_disable_mobs) { + if (entity->type == FP_EntityType_MobSpawner && !game->play.debug_disable_mobs && game->play.state != FP_GameState_Tutorial) { // 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); @@ -2284,22 +2284,67 @@ static void FP_Update(TELY_OS *os, FP_Game *game, TELY_OSInput *input, TELY_Audi } // NOTE: 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 (game->play.state == FP_GameState_Tutorial) { + Dqn_f32 arrival_dist = Dqn_V2_LengthSq(camera->world_pos_target - camera->world_pos); + bool camera_arrived = arrival_dist < DQN_SQUARED(5.f); + + switch (game->play.tutorial_state) { + 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; + } + } break; + + case FP_GameStateTutorial_ShowPortalOneWait: /*FALLTHRU*/ + case FP_GameStateTutorial_ShowPortalTwoWait: /*FALLTHRU*/ + case FP_GameStateTutorial_ShowPortalThreeWait: /*FALLTHRU*/ + case FP_GameStateTutorial_ShowPlayerWait: { + if (input->timer_s > game->play.tutorial_wait_end_time_s) { + game->play.tutorial_state = DQN_CAST(FP_GameStateTutorial)(DQN_CAST(uint32_t)game->play.tutorial_state + 1); + } + } break; + + case FP_GameStateTutorial_ShowPortalOne: /*FALLTHRU*/ + case FP_GameStateTutorial_ShowPortalTwo: /*FALLTHRU*/ + case FP_GameStateTutorial_ShowPortalThree: { + if (game->play.tutorial_state == FP_GameStateTutorial_ShowPortalOne) { + camera->world_pos_target = FP_Game_CalcEntityWorldPos(game, game->play.mob_spawners.data[0]) * game->play.camera.scale; + } else if (game->play.tutorial_state == FP_GameStateTutorial_ShowPortalTwo) { + camera->world_pos_target = FP_Game_CalcEntityWorldPos(game, game->play.mob_spawners.data[1]) * game->play.camera.scale; + } else { + DQN_ASSERT(game->play.tutorial_state == FP_GameStateTutorial_ShowPortalThree); + camera->world_pos_target = FP_Game_CalcEntityWorldPos(game, game->play.mob_spawners.data[2]) * 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; + } + } break; + + case FP_GameStateTutorial_Count: { + game->play.state = FP_GameState_Play; + } break; + } + } else { + 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; + } // NOTE: Clamp camera to map bounds ============================================================ { - Dqn_V2 window_size = Dqn_V2_InitV2I(os->core.window_size); - + Dqn_V2 window_size = Dqn_V2_InitV2I(os->core.window_size); camera->scale = window_size / camera->size; Dqn_V2 camera_size_screen = camera->size * camera->scale; @@ -2313,7 +2358,7 @@ static void FP_Update(TELY_OS *os, FP_Game *game, TELY_OSInput *input, TELY_Audi camera->world_pos_target.y = DQN_MIN(camera->world_pos_target.y, half_map_screen_size.h - (camera_size_screen.h * .5f)); } - camera->world_pos += (camera->world_pos_target - camera->world_pos) * (5.f * DQN_CAST(Dqn_f32)input->delta_s); + camera->world_pos += (camera->world_pos_target - camera->world_pos) * DQN_MIN(1.f, (5.f * DQN_CAST(Dqn_f32)input->delta_s)); Dqn_Profiler_EndZone(update_zone); } @@ -2961,7 +3006,7 @@ static void FP_Render(FP_Game *game, TELY_OS *os, TELY_Renderer *renderer, TELY_ } // NOTE: Render overlay UI ===================================================================== - if (!game->play.debug_hide_hud && (game->play.state == FP_GameState_Pause || game->play.state == FP_GameState_Play)) { + if (!game->play.debug_hide_hud && (game->play.state == FP_GameState_Pause || game->play.state == FP_GameState_Play || game->play.state == FP_GameState_Tutorial)) { // NOTE: Render the merchant menus for each player ========================================= FP_GamePlay *play = &game->play; @@ -3764,15 +3809,88 @@ static void FP_Render(FP_Game *game, TELY_OS *os, TELY_Renderer *renderer, TELY_ #endif + // NOTE: Render the other game state modes ===================================================== + Dqn_V4 const maroon_colour = TELY_Colour_V4InitRGBAU32(0x301010FF); // NOTE: Maroon + if (game->play.state == FP_GameState_Tutorial) { + TELY_Render_PushTransform(renderer, Dqn_M2x3_Identity()); + TELY_Render_PushFontSize(renderer, game->talkco_font, game->large_talkco_font_size); + TELY_Render_PushColourV4(renderer, TELY_COLOUR_WHITE_V4); + DQN_DEFER { + TELY_Render_PopFont(renderer); + TELY_Render_PopTransform(renderer); + }; + + // NOTE: Calculate text bounds + 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); + 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 + } + + // NOTE: Calculate terry avatar variables + TELY_AssetSpriteAnimation *anim = TELY_Asset_GetSpriteAnimation(&game->atlas_sprite_sheet, g_anim_names.intro_screen_terry); + Dqn_Rect tex_rect = game->atlas_sprite_sheet.rects.data[anim->index]; + Dqn_f32 desired_width = os->core.window_size.x * .1f; + Dqn_f32 tex_scalar = desired_width / tex_rect.size.w; + + 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); + + Dqn_Rect terry_bounding_rect = Dqn_Rect_ExpandV2(dest_rect, Dqn_V2_InitNx2(scaled_font_size * .5f, scaled_font_size * .25f)); + + // 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_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)); + + for (LineSize &line_size_ : lines) { + LineSize *line_size = &line_size_; + TELY_Render_TextF(renderer, draw_p, Dqn_V2_Zero, "%.*s", DQN_STR_FMT(line_size->line)); + draw_p.y += TELY_Render_FontSize(renderer) * os->core.dpi_scale; + } + } + + // 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, + dest_rect, + Dqn_V2_Zero /*rotate origin*/, + 0.f /*rotation*/, + TELY_COLOUR_WHITE_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) { + 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); Dqn_f32 scanline_gap = 4.0f; Dqn_f32 scanline_thickness = 3.0f; FP_GameRenderScanlines(renderer, scanline_gap, scanline_thickness, screen_size); } - // NOTE: Render the other game state modes ===================================================== 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) { TELY_Render_PushTransform(renderer, Dqn_M2x3_Identity()); @@ -3784,7 +3902,7 @@ static void FP_Render(FP_Game *game, TELY_OS *os, TELY_Renderer *renderer, TELY_ Dqn_V2 draw_p = Dqn_V2_InitNx2(min_inset, min_inset); TELY_Render_PushColourV4(renderer, TELY_COLOUR_WHITE_V4); - Dqn_V4 bg_colour = TELY_Colour_V4InitRGBAU32(0x301010FF); // NOTE: Maroon + Dqn_V4 bg_colour = maroon_colour; 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), @@ -3896,8 +4014,9 @@ 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_Play; + 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); @@ -3911,6 +4030,7 @@ static void FP_Render(FP_Game *game, TELY_OS *os, TELY_Renderer *renderer, TELY_ if (TELY_OSInput_ScanKeyIsPressed(input, TELY_OSInputScanKey_Return)) 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); @@ -4240,7 +4360,7 @@ void TELY_OS_DLLFrameUpdate(TELY_OS *os) // ============================================================================================= - if (game->play.state == FP_GameState_Play) { + if (game->play.state == FP_GameState_Play || game->play.state == FP_GameState_Tutorial) { if (TELY_OSInput_KeyWasDown(input->mouse_left) && TELY_OSInput_KeyIsDown(input->mouse_left)) { if (game->play.prev_active_entity.id) game->play.active_entity = game->play.prev_active_entity; diff --git a/feely_pona_build.cpp b/feely_pona_build.cpp index 5851573..fdda7bd 100644 --- a/feely_pona_build.cpp +++ b/feely_pona_build.cpp @@ -392,7 +392,7 @@ int main(int argc, char const **argv) Dqn_Str8 cmd = Dqn_CPPBuild_ToCommandLine(build_context, Dqn_CPPBuildMode_AlwaysRebuild, scratch.allocator); Dqn_Print_StdLnF(Dqn_PrintStd_Out, "%.*s\n", DQN_STR_FMT(cmd)); } else { - Dqn_Str8 exe_path = Dqn_FsPath_ConvertF(scratch.arena, "%.*s/terry_cherry_dev_msvc.exe", DQN_STR_FMT(build_dir)); + Dqn_Str8 exe_path = Dqn_FsPath_ConvertF(scratch.arena, "%.*s/terry_cherry_dev.exe", DQN_STR_FMT(build_dir)); bool exe_is_locked = false; if (Dqn_Fs_Exists(exe_path)) { Dqn_FsFile exe_file = Dqn_Fs_OpenFile(exe_path, Dqn_FsFileOpen_OpenIfExist, Dqn_FsFileAccess_Read | Dqn_FsFileAccess_Write); diff --git a/feely_pona_game.cpp b/feely_pona_game.cpp index d5bea2b..bf9b0e3 100644 --- a/feely_pona_game.cpp +++ b/feely_pona_game.cpp @@ -250,6 +250,7 @@ static FP_GameEntity *FP_Game_MakeEntityPointerFV(FP_Game *game, DQN_FMT_ATTRIB result->buildings_visited = FP_SentinelList_Init(game->play.chunk_pool); result->action.sprite_alpha = 1.f; result->stamina_cap = 93; + result->stamina = result->stamina_cap; result->hp_cap = DQN_CAST(uint32_t)(FP_DEFAULT_DAMAGE * .8f); result->hp = result->hp_cap; diff --git a/feely_pona_game.h b/feely_pona_game.h index cd638f6..df1c327 100644 --- a/feely_pona_game.h +++ b/feely_pona_game.h @@ -340,6 +340,7 @@ enum FP_GameAudio enum FP_GameState { FP_GameState_IntroScreen, + FP_GameState_Tutorial, FP_GameState_Play, FP_GameState_Pause, FP_GameState_WinGame, @@ -365,6 +366,19 @@ struct FP_Particle Dqn_usize end_ms; }; +enum FP_GameStateTutorial +{ + FP_GameStateTutorial_ShowPlayer, + FP_GameStateTutorial_ShowPlayerWait, + FP_GameStateTutorial_ShowPortalOne, + FP_GameStateTutorial_ShowPortalOneWait, + FP_GameStateTutorial_ShowPortalTwo, + FP_GameStateTutorial_ShowPortalTwoWait, + FP_GameStateTutorial_ShowPortalThree, + FP_GameStateTutorial_ShowPortalThreeWait, + FP_GameStateTutorial_Count, +}; + struct FP_GamePlay { TELY_ChunkPool *chunk_pool; @@ -429,6 +443,9 @@ struct FP_GamePlay FP_Particle particles[256]; uint32_t particle_next_index; + + FP_GameStateTutorial tutorial_state; + Dqn_f64 tutorial_wait_end_time_s; }; struct FP_Game