fp: Improve tutorial, add a wave break every 3 waves

This commit is contained in:
doyle 2023-10-29 23:45:45 +11:00
parent 285cc9b5ad
commit 11d5f34522
4 changed files with 222 additions and 108 deletions

View File

@ -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<FP_GameEntityHandle> *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 <BTN> 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<LineSize, 8> 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 <B> or <Gamepad: Start> 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 <B> or <Gamepad: Start> 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 <T> or <Gamepad: Select> 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);

View File

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

View File

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

View File

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