11 Commits

17 changed files with 524 additions and 156 deletions
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
Binary file not shown.
+1 -1
+1 -21
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
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
+4 -4
View File
@@ -5,7 +5,7 @@ set script_dir_backslash=%~dp0
set script_dir=%script_dir_backslash:~0,-1%
set build_dir=%script_dir%\Build
scp -P 8110 %build_dir%\Terry_Cherry_Emscripten\Terry_Cherry.html doylet@doylet.dev:/selfhost/TerryCherry/index.html
scp -P 8110 %build_dir%\Terry_Cherry_Emscripten\Terry_Cherry.data doylet@doylet.dev:/selfhost/TerryCherry/Terry_Cherry.data
scp -P 8110 %build_dir%\Terry_Cherry_Emscripten\Terry_Cherry.js doylet@doylet.dev:/selfhost/TerryCherry/Terry_Cherry.js
scp -P 8110 %build_dir%\Terry_Cherry_Emscripten\Terry_Cherry.wasm doylet@doylet.dev:/selfhost/TerryCherry/Terry_Cherry.wasm
scp -P 8110 %build_dir%\Terry_Cherry\Terry_Cherry.html doylet@doylet.dev:/selfhost/TerryCherry/index.html
scp -P 8110 %build_dir%\Terry_Cherry\Terry_Cherry.data doylet@doylet.dev:/selfhost/TerryCherry/Terry_Cherry.data
scp -P 8110 %build_dir%\Terry_Cherry\Terry_Cherry.js doylet@doylet.dev:/selfhost/TerryCherry/Terry_Cherry.js
scp -P 8110 %build_dir%\Terry_Cherry\Terry_Cherry.wasm doylet@doylet.dev:/selfhost/TerryCherry/Terry_Cherry.wasm
+344 -117
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);
@@ -305,9 +330,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 +355,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,30 +363,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 ============================================================
{
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);
}
{
@@ -385,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 = {};
@@ -413,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);
@@ -437,6 +449,12 @@ void TELY_OS_DLLInit(TELY_OS *os)
game->audio[FP_GameAudio_PortalDestroy] = os->funcs.load_audio(assets, DQN_STR8("Portal Destroy"), DQN_STR8("Data/Audio/portal_destroy.ogg"));
game->audio[FP_GameAudio_Smooch] = os->funcs.load_audio(assets, DQN_STR8("Smooch"), DQN_STR8("Data/Audio/smooch.ogg"));
game->audio[FP_GameAudio_Woosh] = os->funcs.load_audio(assets, DQN_STR8("Woosh"), DQN_STR8("Data/Audio/woosh.ogg"));
game->audio[FP_GameAudio_GameStart] = os->funcs.load_audio(assets, DQN_STR8("Game Start"), DQN_STR8("Data/Audio/game_start.ogg"));
game->audio[FP_GameAudio_PerryStart] = os->funcs.load_audio(assets, DQN_STR8("Perry Start"), DQN_STR8("Data/Audio/perry_start.ogg"));
game->audio[FP_GameAudio_Ambience1] = os->funcs.load_audio(assets, DQN_STR8("Ambience one"), DQN_STR8("Data/Audio/ambience_1.ogg"));
game->audio[FP_GameAudio_Ambience2] = os->funcs.load_audio(assets, DQN_STR8("Ambience two"), DQN_STR8("Data/Audio/ambience_2.ogg"));
game->audio[FP_GameAudio_Music1] = os->funcs.load_audio(assets, DQN_STR8("Music one"), DQN_STR8("Data/Audio/music_1.ogg"));
game->audio[FP_GameAudio_Music2] = os->funcs.load_audio(assets, DQN_STR8("Music two"), DQN_STR8("Data/Audio/music_2.ogg"));
// NOTE: Load sprite sheets ====================================================================
os->user_data = game;
@@ -522,7 +540,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;
}
@@ -652,23 +670,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);
@@ -1298,19 +1299,24 @@ 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.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) {
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'
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 +1982,69 @@ 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 % 1 == 0;
bool monkey_spawn = 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);
// NOTE: Count all the mobs spawned that are a foe
// (e.g. churches that convert mobs don't count).
for (FP_SentinelListLink<FP_GameEntityHandle> *link = nullptr; FP_SentinelList_Iterate<FP_GameEntityHandle>(&spawner->spawn_list, &link); ) {
FP_GameEntity *mob = FP_Game_GetEntity(game, link->data);
if (mob->faction == FP_GameEntityFaction_Foe)
enemy_count++;
}
}
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;
TELY_Audio_Stop(audio, game->audio[FP_GameAudio_Music1]);
TELY_Audio_Play(audio, game->audio[FP_GameAudio_Music2], 1.f);
} 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
TELY_Audio_Stop(audio, game->audio[FP_GameAudio_Music2]);
TELY_Audio_Play(audio, game->audio[FP_GameAudio_Music1], 1.f);
if (monkey_spawn && 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++;
//TELY_Audio_Stop(audio, game->audio[FP_GameAudio_Music2]);
}
}
}
}
@@ -2049,7 +2112,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);
@@ -2059,11 +2122,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);
@@ -2088,13 +2151,11 @@ static void FP_Update(TELY_OS *os, FP_Game *game, TELY_OSInput *input, TELY_Audi
mob->waypoints = FP_SentinelList_Init<FP_GameWaypoint>(game->play.chunk_pool);
mob->flags |= FP_GameEntityFlag_Aggros;
mob->flags |= FP_GameEntityFlag_RespondsToBuildings;
mob->hp_cap *= hp_adjustment;
mob->hp_cap *= hp_adjustment*2;
mob->hp = mob->hp_cap;
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;
}
}
}
@@ -2284,22 +2345,76 @@ 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_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 (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;
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_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;
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 +2428,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);
}
@@ -2585,8 +2700,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;
@@ -2643,13 +2758,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,
@@ -2915,7 +3030,8 @@ 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;
TELY_Audio_Play(audio, game->audio[FP_GameAudio_PerryStart], 1.f);
}
}
@@ -2930,7 +3046,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;
@@ -2942,7 +3058,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)
@@ -2961,7 +3077,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;
@@ -3006,7 +3122,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);
@@ -3025,7 +3141,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 ============================
@@ -3120,7 +3236,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,
@@ -3180,7 +3296,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));
@@ -3189,7 +3305,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);
}
@@ -3295,7 +3411,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));
@@ -3320,7 +3436,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);
}
@@ -3764,17 +3880,129 @@ 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;
};
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_bounding_size.x = DQN_MAX(text_bounding_size.x, line_size->size.x);
}
// 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 = 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 = dest_rect;
// NOTE: Draw text
{
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));
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_TextureColourV4(renderer,
game->atlas_sprite_sheet.tex_handle,
tex_rect,
dest_rect,
Dqn_V2_Zero /*rotate origin*/,
0.f /*rotation*/,
TELY_COLOUR_WHITE_V4);
}
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) {
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) {
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); };
@@ -3784,7 +4012,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),
@@ -3889,28 +4117,30 @@ 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);
if (game->play.state == FP_GameState_LoseGame)
FP_PlayReset(game, os);
if (FP_ListenForNewPlayer(input, game))
game->play.state = FP_GameState_Play;
} 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;
TELY_Audio_Play(audio, game->audio[FP_GameAudio_GameStart], 1.f);
} else {
game->play.state = FP_GameState_Play;
TELY_Audio_Play(audio, game->audio[FP_GameAudio_GameStart], 1.f);
}
}
} else {
DQN_ASSERT(game->play.state == FP_GameState_WinGame);
TELY_Render_PushFontSize(renderer, game->inter_regular_font, game->large_font_size);
@@ -4214,7 +4444,6 @@ TELY_OS_DLL_FUNCTION
void TELY_OS_DLLFrameUpdate(TELY_OS *os)
{
TELY_OSInput *input = &os->input;
TELY_Assets *assets = &os->assets;
TELY_Renderer *renderer = &os->renderer;
FP_Game *game = DQN_CAST(FP_Game *) os->user_data;
@@ -4232,15 +4461,15 @@ void TELY_OS_DLLFrameUpdate(TELY_OS *os)
TELY_Audio *audio = &os->audio;
#if 0
#if 1
if (audio->playback_size == 0) {
TELY_Audio_Play(audio, game->audio[FP_GameAudio_TestAudio], 1.f /*volume*/);
//TELY_Audio_Play(audio, game->audio[FP_GameAudio_Music2], 1.f /*volume*/);
}
#endif
// =============================================================================================
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;
@@ -4276,6 +4505,4 @@ void TELY_OS_DLLFrameUpdate(TELY_OS *os)
}
FP_Render(game, os, renderer, audio);
TELY_Audio_MixPlaybackSamples(audio, assets);
}
+6
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
+77 -10
View File
@@ -229,6 +229,38 @@ int main(int argc, char const **argv)
}
}
// NOTE: sokol_audio ================================================================================
Dqn_Str8 sokol_audio_source_code_file = Dqn_FsPath_ConvertF(scratch.arena, "%.*s/External/sokol/sokol_audio.c", DQN_STR_FMT(tely_dir));
uint64_t sokol_audio_pc_timings[2] = {};
Dqn_List<Dqn_Str8> sokol_audio_pc_output_files = Dqn_List_Init<Dqn_Str8>(scratch.arena, 16);
{
sokol_audio_pc_timings[0] = Dqn_OS_PerfCounterNow();
DQN_DEFER { sokol_audio_pc_timings[1] = Dqn_OS_PerfCounterNow(); };
Dqn_CPPBuildContext build_context = {};
build_context.compiler = Dqn_CPPBuildCompiler_MSVC;
build_context.compile_files = Dqn_Slice_InitCArrayCopy(scratch.arena, {
Dqn_CPPBuildCompileFile{
{}, // Args
sokol_audio_source_code_file,
},
});
Dqn_List_Add(&sokol_audio_pc_output_files, DQN_STR8("sokol_audio.obj"));
Dqn_List<Dqn_Str8> compile_flags = Dqn_List_InitSliceCopy(scratch.arena, 16, common_compile_flags);
Dqn_List_Add(&compile_flags, DQN_STR8("/c"));
build_context.compile_flags = Dqn_List_ToSliceCopy(&compile_flags, scratch.arena);
build_context.build_dir = build_dir;
if (dry_run) {
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_CPPBuild_ExecOrAbort(build_context, Dqn_CPPBuildMode_CacheBuild);
}
}
// NOTE: QOI Converter =========================================================================
uint64_t qoi_converter_timings[2] = {};
{
@@ -286,6 +318,8 @@ int main(int argc, char const **argv)
{
for (Dqn_ListIterator<Dqn_Str8> it = {}; Dqn_List_Iterate(&raylib_pc_output_files, &it, 0); )
Dqn_List_Add(&link_flags, *it.data);
for (Dqn_ListIterator<Dqn_Str8> it = {}; Dqn_List_Iterate(&sokol_audio_pc_output_files, &it, 0); )
Dqn_List_Add(&link_flags, *it.data);
Dqn_List_Add(&link_flags, DQN_STR8("gdi32.lib"));
Dqn_List_Add(&link_flags, DQN_STR8("opengl32.lib"));
Dqn_List_Add(&link_flags, DQN_STR8("winmm.lib"));
@@ -392,7 +426,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);
@@ -434,9 +468,9 @@ int main(int argc, char const **argv)
DQN_DEFER { raylib_emscripten_timings[1] = Dqn_OS_PerfCounterNow(); };
// NOTE: Setup build context ===========================================================
Dqn_List<Dqn_Str8> raylib_emscripten_output_files = Dqn_List_Init<Dqn_Str8>(scratch.arena, 16);
Dqn_CPPBuildContext raylib_emscripten_build_context = {};
raylib_emscripten_build_context.compiler = Dqn_CPPBuildCompiler_GCC;
Dqn_List<Dqn_Str8> emscripten_obj_files = Dqn_List_Init<Dqn_Str8>(scratch.arena, 16);
Dqn_CPPBuildContext raylib_emscripten_build_context = {};
raylib_emscripten_build_context.compiler = Dqn_CPPBuildCompiler_GCC;
for (Dqn_Str8 base_file : raylib_base_files) {
Dqn_Str8 file_stem = Dqn_Str8_FileNameNoExtension(base_file);
@@ -461,7 +495,7 @@ int main(int argc, char const **argv)
raylib_emscripten_build_context.compile_files = Dqn_Slice_InitCArrayCopy(scratch.arena, {build_file});
raylib_emscripten_build_context.compile_flags = Dqn_List_ToSliceCopy(&compile_flags, scratch.arena);
raylib_emscripten_build_context.build_dir = build_dir;
Dqn_List_Add(&raylib_emscripten_output_files, build_file.output_file_path);
Dqn_List_Add(&emscripten_obj_files, build_file.output_file_path);
if (dry_run) {
Dqn_Str8 cmd = Dqn_CPPBuild_ToCommandLine(raylib_emscripten_build_context, Dqn_CPPBuildMode_AlwaysRebuild, scratch.allocator);
@@ -471,13 +505,45 @@ int main(int argc, char const **argv)
}
}
// NOTE: Build the wasm raylib library =================================================
{
Dqn_Str8 base_file = sokol_audio_source_code_file;
Dqn_Str8 file_stem = Dqn_Str8_FileNameNoExtension(base_file);
// NOTE: Append "emscripten" suffix to the object files
Dqn_CPPBuildCompileFile build_file = {};
build_file.input_file_path = base_file;
build_file.output_file_path = Dqn_Str8_InitF(scratch.allocator, "sokol_audio_emscripten.o");
Dqn_List<Dqn_Str8> compile_flags = Dqn_List_InitCArrayCopy(scratch.arena, 32, {
DQN_STR8("cmd"),
DQN_STR8("/C"),
DQN_STR8("emcc.bat"),
DQN_STR8("-c"), // Compile and assemble, but do not link
DQN_STR8("-Wall"),
DQN_STR8("-Os"), // Optimize for size
});
Dqn_List_AddList(&compile_flags, build_specific_compile_flags);
raylib_emscripten_build_context.compile_files = Dqn_Slice_InitCArrayCopy(scratch.arena, {build_file});
raylib_emscripten_build_context.compile_flags = Dqn_List_ToSliceCopy(&compile_flags, scratch.arena);
raylib_emscripten_build_context.build_dir = build_dir;
Dqn_List_Add(&emscripten_obj_files, build_file.output_file_path);
if (dry_run) {
Dqn_Str8 cmd = Dqn_CPPBuild_ToCommandLine(raylib_emscripten_build_context, Dqn_CPPBuildMode_AlwaysRebuild, scratch.allocator);
Dqn_Print_StdLnF(Dqn_PrintStd_Out, "%.*s\n", DQN_STR_FMT(cmd));
} else {
Dqn_CPPBuild_ExecOrAbort(raylib_emscripten_build_context, Dqn_CPPBuildMode_CacheBuild);
}
}
// NOTE: Build the wasm raylib+sokol_audio library =====================================
{
Dqn_Str8Builder builder = {};
builder.allocator = scratch.allocator;
Dqn_Str8Builder_AppendF(&builder, "cmd /C emar.bat rcs %.*s", DQN_STR_FMT(raylib_emscripten_lib_name));
for (Dqn_ListIterator<Dqn_Str8> it = {}; Dqn_List_Iterate(&raylib_emscripten_output_files, &it, 0); )
for (Dqn_ListIterator<Dqn_Str8> it = {}; Dqn_List_Iterate(&emscripten_obj_files, &it, 0); )
Dqn_Str8Builder_AppendF(&builder, " %.*s", DQN_STR_FMT(*it.data));
Dqn_Str8 cmd = Dqn_Str8Builder_Build(&builder, scratch.allocator);
@@ -543,7 +609,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 +654,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));
@@ -602,7 +668,8 @@ int main(int argc, char const **argv)
build_timings[1] = Dqn_OS_PerfCounterNow();
Dqn_Print_StdLnF(Dqn_PrintStd_Out, "\n-- Dqn_CPPBuild Timings (%.2fms)", Dqn_OS_PerfCounterMs(build_timings[0], build_timings[1]));
Dqn_Print_StdLnF(Dqn_PrintStd_Out, " robocopy: %.2fms", Dqn_OS_PerfCounterMs(robocopy_timings[0], robocopy_timings[1]));
Dqn_Print_StdLnF(Dqn_PrintStd_Out, " raylib: %.2fms", Dqn_OS_PerfCounterMs(raylib_pc_timings[0], raylib_pc_timings[1]));
Dqn_Print_StdLnF(Dqn_PrintStd_Out, " raylib: %.2fms", Dqn_OS_PerfCounterMs(raylib_pc_timings[0], raylib_pc_timings[1]));
Dqn_Print_StdLnF(Dqn_PrintStd_Out, " sokol_audio: %.2fms", Dqn_OS_PerfCounterMs(sokol_audio_pc_timings[0], sokol_audio_pc_timings[1]));
Dqn_Print_StdLnF(Dqn_PrintStd_Out, " qoi_converter: %.2fms", Dqn_OS_PerfCounterMs(qoi_converter_timings[0], qoi_converter_timings[1]));
Dqn_Print_StdLnF(Dqn_PrintStd_Out, " feely pona sprite packer: %.2fms", Dqn_OS_PerfCounterMs(feely_pona_sprite_packer_timings[0], feely_pona_sprite_packer_timings[1]));
Dqn_Print_StdLnF(Dqn_PrintStd_Out, " feely pona (no dll): %.2fms", Dqn_OS_PerfCounterMs(feely_pona_no_dll_timings[0], feely_pona_no_dll_timings[1]));
+1
View File
@@ -185,6 +185,7 @@ static FP_GameEntityHandle FP_Entity_CreateTerryInternal(FP_Game *game, Dqn_V2 p
entity->local_hit_box_size = FP_Game_MetersToPixelsNx2(game->play, 0.5f, entity->sprite_height.meters * .6f);
entity->hp_cap = FP_DEFAULT_DAMAGE * 3;
entity->hp = entity->hp_cap;
entity->coins = 10;
FP_Entity_AddDebugEditorFlags(game, result);
entity->flags |= FP_GameEntityFlag_NonTraversable;
entity->flags |= FP_GameEntityFlag_Attackable;
+1
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;
+44 -1
View File
@@ -334,12 +334,19 @@ enum FP_GameAudio
FP_GameAudio_Message,
FP_GameAudio_Monkey,
FP_GameAudio_PortalDestroy,
FP_GameAudio_GameStart,
FP_GameAudio_PerryStart,
FP_GameAudio_Ambience1,
FP_GameAudio_Ambience2,
FP_GameAudio_Music1,
FP_GameAudio_Music2,
FP_GameAudio_Count,
};
enum FP_GameState
{
FP_GameState_IntroScreen,
FP_GameState_Tutorial,
FP_GameState_Play,
FP_GameState_Pause,
FP_GameState_WinGame,
@@ -365,6 +372,35 @@ 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_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;
@@ -388,6 +424,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;
@@ -429,6 +466,12 @@ struct FP_GamePlay
FP_Particle particles[256];
uint32_t particle_next_index;
FP_GameStateTutorial tutorial_state;
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
@@ -475,7 +518,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
+2 -2
View File
@@ -25,7 +25,7 @@ forwards them to the command line).
You must ensure you have MSVC's `cl.exe` on your path to build. This game was
tested on MSVC 2023 (v14.34.31933).
For most intents and purposes you may simply execute `build.bat` on Windows
For most intents and purposes you may simply execute `build_all.bat` on Windows
which bootstraps the build program and automatically calls the meta-build
program. With fast computers nowadays our default is to build all targets by
default (except web due to extra toolchain requirements).
@@ -57,7 +57,7 @@ then be done by
Tools\emsdk\emsdk.bat activate latest
# Build the game
build --web
build_all --web
```
This will produce at `Build\Terry_Cherry_Emscripten` a HTML and WASM file ready