fp: Add quick tutorial at beginning of game

This commit is contained in:
doyle 2023-10-29 17:30:08 +11:00
parent b4438c1510
commit 285cc9b5ad
6 changed files with 200 additions and 57 deletions

View File

@ -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

25
build_all.bat Normal file
View File

@ -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

View File

@ -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<FP_GameEntityHandle> *link = nullptr; FP_SentinelList_Iterate<FP_GameEntityHandle>(&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;

View File

@ -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);

View File

@ -250,6 +250,7 @@ static FP_GameEntity *FP_Game_MakeEntityPointerFV(FP_Game *game, DQN_FMT_ATTRIB
result->buildings_visited = FP_SentinelList_Init<FP_GameEntityHandle>(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;

View File

@ -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