From 12e6e5d6f1848e70889aa6126965f5015a007b2f Mon Sep 17 00:00:00 2001 From: doyle Date: Mon, 18 Sep 2023 23:00:30 +1000 Subject: [PATCH] fp: Wire up attack chaining --- External/tely | 2 +- feely_pona.cpp | 181 ++++++++++++++++++++------------------------ feely_pona.h | 23 +++--- feely_pona_game.cpp | 28 +++++++ 4 files changed, 124 insertions(+), 110 deletions(-) diff --git a/External/tely b/External/tely index 0b5b08b..964aa47 160000 --- a/External/tely +++ b/External/tely @@ -1 +1 @@ -Subproject commit 0b5b08bb635d1aad6832a69d30e2c45f7a59af8b +Subproject commit 964aa47a6ed2e2bb1606dd576a57867d09ca8ba6 diff --git a/feely_pona.cpp b/feely_pona.cpp index 483cbfe..7f12e59 100644 --- a/feely_pona.cpp +++ b/feely_pona.cpp @@ -60,26 +60,26 @@ void TELY_DLL_Init(void *user_data) sheet->sprite_size = Dqn_V2I_InitNx2(50, 37); TELY_AssetSpriteAnimation hero_anims[] = { - {DQN_STRING8("Everything"), /*index*/ 0, /*count*/ sheet->sprite_count}, - {DQN_STRING8("Idle"), /*index*/ 0, /*count*/ 3}, - {DQN_STRING8("Run"), /*index*/ 8, /*count*/ 6}, - {DQN_STRING8("Jump"), /*index*/ 14, /*count*/ 10}, - {DQN_STRING8("Floor slide"), /*index*/ 24, /*count*/ 5}, - {DQN_STRING8("Unknown"), /*index*/ 29, /*count*/ 9}, - {DQN_STRING8("Attack A"), /*index*/ 38, /*count*/ 11}, - {DQN_STRING8("Attack B"), /*index*/ 49, /*count*/ 4}, - {DQN_STRING8("Attack C"), /*index*/ 53, /*count*/ 6}, - {DQN_STRING8("Hurt A"), /*index*/ 59, /*count*/ 5}, - {DQN_STRING8("Hurt B"), /*index*/ 64, /*count*/ 5}, - {DQN_STRING8("Unsheath sword"), /*index*/ 69, /*count*/ 4}, - {DQN_STRING8("Sheath sword"), /*index*/ 73, /*count*/ 4}, - {DQN_STRING8("Air drift"), /*index*/ 77, /*count*/ 2}, - {DQN_STRING8("Air drop"), /*index*/ 79, /*count*/ 2}, - {DQN_STRING8("Ladder climb"), /*index*/ 81, /*count*/ 4}, - {DQN_STRING8("Chi push"), /*index*/ 85, /*count*/ 8}, - {DQN_STRING8("Leap slice A"), /*index*/ 93, /*count*/ 7}, - {DQN_STRING8("Leap slice B"), /*index*/ 100, /*count*/ 3}, - {DQN_STRING8("Leap slice C"), /*index*/ 103, /*count*/ 6}, + {DQN_STRING8("Everything"), /*index*/ 0, /*count*/ sheet->sprite_count, /*seconds_per_frame*/ 1 / 12.f}, + {DQN_STRING8("Idle"), /*index*/ 0, /*count*/ 3, /*seconds_per_frame*/ 1 / 4.f}, + {DQN_STRING8("Run"), /*index*/ 8, /*count*/ 6, /*seconds_per_frame*/ 1 / 8.f}, + {DQN_STRING8("Jump"), /*index*/ 14, /*count*/ 10, /*seconds_per_frame*/ 1 / 12.f}, + {DQN_STRING8("Floor slide"), /*index*/ 24, /*count*/ 5, /*seconds_per_frame*/ 1 / 12.f}, + {DQN_STRING8("Unknown"), /*index*/ 29, /*count*/ 9, /*seconds_per_frame*/ 1 / 12.f}, + {DQN_STRING8("Attack A"), /*index*/ 42, /*count*/ 7, /*seconds_per_frame*/ 1 / 12.f}, + {DQN_STRING8("Attack B"), /*index*/ 49, /*count*/ 4, /*seconds_per_frame*/ 1 / 8.f}, + {DQN_STRING8("Attack C"), /*index*/ 53, /*count*/ 6, /*seconds_per_frame*/ 1 / 12.f}, + {DQN_STRING8("Hurt A"), /*index*/ 59, /*count*/ 5, /*seconds_per_frame*/ 1 / 12.f}, + {DQN_STRING8("Hurt B"), /*index*/ 64, /*count*/ 5, /*seconds_per_frame*/ 1 / 12.f}, + {DQN_STRING8("Unsheath sword"), /*index*/ 69, /*count*/ 4, /*seconds_per_frame*/ 1 / 12.f}, + {DQN_STRING8("Sheath sword"), /*index*/ 73, /*count*/ 4, /*seconds_per_frame*/ 1 / 12.f}, + {DQN_STRING8("Air drift"), /*index*/ 77, /*count*/ 2, /*seconds_per_frame*/ 1 / 12.f}, + {DQN_STRING8("Air drop"), /*index*/ 79, /*count*/ 2, /*seconds_per_frame*/ 1 / 12.f}, + {DQN_STRING8("Ladder climb"), /*index*/ 81, /*count*/ 4, /*seconds_per_frame*/ 1 / 12.f}, + {DQN_STRING8("Chi push"), /*index*/ 85, /*count*/ 8, /*seconds_per_frame*/ 1 / 12.f}, + {DQN_STRING8("Leap slice A"), /*index*/ 93, /*count*/ 7, /*seconds_per_frame*/ 1 / 12.f}, + {DQN_STRING8("Leap slice B"), /*index*/ 100, /*count*/ 3, /*seconds_per_frame*/ 1 / 12.f}, + {DQN_STRING8("Leap slice C"), /*index*/ 103, /*count*/ 6, /*seconds_per_frame*/ 1 / 12.f}, }; game->hero_sprite_anims = Dqn_Slice_Alloc(&platform->arena, DQN_ARRAY_UCOUNT(hero_anims), Dqn_ZeroMem_No); @@ -521,104 +521,88 @@ void FP_GameUpdate(TELY_Platform *platform, FP_Game *game, TELY_Renderer *render FP_GameEntityAction *action = &entity->action; { bool we_are_clicked_entity = entity->handle == game->clicked_entity; - if (action->state == FP_GameEntityState_Nil) { - action->state = FP_GameEntityState_Idle; - action->dirty = true; - } + bool action_has_finished = action->timer_s != FP_GAME_ENTITY_ACTION_INFINITE_TIMER && action->timer_s >= action->end_at_s; + + if (action->state == FP_GameEntityState_Nil) + FP_Game_EntityActionSetState(action, FP_GameEntityState_Idle); if (action->state == FP_GameEntityState_Idle) { - if (we_are_clicked_entity && TELY_Platform_InputScanCodeIsDown(input, TELY_PlatformInputScanCode_J)) { - action->state = FP_GameEntityState_AttackA; - action->dirty = true; - } else if (we_are_clicked_entity && (dir_vector.x || dir_vector.y)) { - action->state = FP_GameEntityState_Run; - action->dirty = true; - } else if (action->dirty) { - action->anim = {}; - action->anim.asset = entity->sprite_anims.data + TELY_Asset_GetSpriteAnimation(entity->sprite_anims, DQN_STRING8("Idle")).index; - action->timer_s = FP_GAME_ENTITY_ACTION_INFINITE_TIMER; - action->dirty = false; + if (action->flags & FP_GameEntityActionFlag_StateTransition) { + TELY_AssetSpriteAnimation *anim = entity->sprite_anims.data + TELY_Asset_GetSpriteAnimation(entity->sprite_anims, DQN_STRING8("Idle")).index; + FP_Game_EntityActionReset(action, FP_GAME_ENTITY_ACTION_INFINITE_TIMER, anim); + } else if (we_are_clicked_entity) { + if (TELY_Platform_InputScanCodeIsPressed(input, TELY_PlatformInputScanCode_J)) { + FP_Game_EntityActionSetState(action, FP_GameEntityState_AttackA); + } else if (dir_vector.x || dir_vector.y) { + FP_Game_EntityActionSetState(action, FP_GameEntityState_Run); + } } } if (action->state == FP_GameEntityState_AttackA) { - if (action->dirty) { - action->anim = {}; - action->anim.asset = entity->sprite_anims.data + TELY_Asset_GetSpriteAnimation(entity->sprite_anims, DQN_STRING8("Attack A")).index; - action->timer_s = action->anim.asset->count * 0.016f; - action->dirty = false; - } else if (action->timer_s <= 0.f) { - action->state = FP_GameEntityState_Idle; - action->dirty = true; - } else if (we_are_clicked_entity && TELY_Platform_InputScanCodeIsDown(input, TELY_PlatformInputScanCode_J)) { - // NOTE: Trigger AttackB if action is at the correct time - action->state = FP_GameEntityState_AttackB; - action->dirty = true; + if (action->flags & FP_GameEntityActionFlag_StateTransition) { + TELY_AssetSpriteAnimation *anim = entity->sprite_anims.data + TELY_Asset_GetSpriteAnimation(entity->sprite_anims, DQN_STRING8("Attack A")).index; + FP_Game_EntityActionReset(action, anim->count * anim->seconds_per_frame, anim); + } else if (action_has_finished) { + FP_Game_EntityActionSetState(action, FP_GameEntityState_Idle); + } else if (!FP_Game_EntityActionHasFailed(action) && we_are_clicked_entity) { + if (TELY_Platform_InputScanCodeIsPressed(input, TELY_PlatformInputScanCode_J)) { + Dqn_f32 t01 = action->timer_s / action->end_at_s; + if (t01 > 0.5f) + FP_Game_EntityActionSetState(action, FP_GameEntityState_AttackB); + else + action->flags |= FP_GameEntityActionFlag_Failed; + } } } if (action->state == FP_GameEntityState_AttackB) { - if (action->dirty) { - action->anim = {}; - action->anim.asset = entity->sprite_anims.data + TELY_Asset_GetSpriteAnimation(entity->sprite_anims, DQN_STRING8("Attack B")).index; - action->timer_s = action->anim.asset->count * 0.016f; - action->dirty = false; - } else if (action->timer_s <= 0.f) { - action->state = FP_GameEntityState_Idle; - action->dirty = true; - } else if (we_are_clicked_entity && TELY_Platform_InputScanCodeIsDown(input, TELY_PlatformInputScanCode_J)) { - // NOTE: Trigger AttackC if action is at the correct time - action->state = FP_GameEntityState_AttackC; - action->dirty = true; + if (action->flags & FP_GameEntityActionFlag_StateTransition) { + TELY_AssetSpriteAnimation *anim = entity->sprite_anims.data + TELY_Asset_GetSpriteAnimation(entity->sprite_anims, DQN_STRING8("Attack B")).index; + FP_Game_EntityActionReset(action, anim->count * anim->seconds_per_frame, anim); + } else if (action_has_finished) { + FP_Game_EntityActionSetState(action, FP_GameEntityState_Idle); + } else if (!FP_Game_EntityActionHasFailed(action) && we_are_clicked_entity) { + if (TELY_Platform_InputScanCodeIsPressed(input, TELY_PlatformInputScanCode_J)) { + Dqn_f32 t01 = action->timer_s / action->end_at_s; + if (t01 > 0.5f) + FP_Game_EntityActionSetState(action, FP_GameEntityState_AttackC); + else + action->flags |= FP_GameEntityActionFlag_Failed; + } } } if (action->state == FP_GameEntityState_AttackC) { - if (action->dirty) { - action->anim = {}; - action->anim.asset = entity->sprite_anims.data + TELY_Asset_GetSpriteAnimation(entity->sprite_anims, DQN_STRING8("Attack C")).index; - action->timer_s = action->anim.asset->count * 0.016f; - action->dirty = false; - } else if (action->timer_s <= 0.f) { - action->state = FP_GameEntityState_Idle; - action->dirty = true; + if (action->flags & FP_GameEntityActionFlag_StateTransition) { + TELY_AssetSpriteAnimation *anim = entity->sprite_anims.data + TELY_Asset_GetSpriteAnimation(entity->sprite_anims, DQN_STRING8("Attack C")).index; + FP_Game_EntityActionReset(action, anim->count * anim->seconds_per_frame, anim); + } else if (action_has_finished) { + FP_Game_EntityActionSetState(action, FP_GameEntityState_Idle); } } if (action->state == FP_GameEntityState_Run) { - if (action->dirty) { - action->anim = {}; - action->anim.asset = entity->sprite_anims.data + TELY_Asset_GetSpriteAnimation(entity->sprite_anims, DQN_STRING8("Run")).index; - action->timer_s = FP_GAME_ENTITY_ACTION_INFINITE_TIMER; - action->dirty = false; - } - - if (we_are_clicked_entity && (dir_vector.x == 0.f && dir_vector.y == 0.f)) { - action->state = FP_GameEntityState_Idle; - action->dirty = true; + if (action->flags & FP_GameEntityActionFlag_StateTransition) { + TELY_AssetSpriteAnimation *anim = entity->sprite_anims.data + TELY_Asset_GetSpriteAnimation(entity->sprite_anims, DQN_STRING8("Run")).index; + FP_Game_EntityActionReset(action, FP_GAME_ENTITY_ACTION_INFINITE_TIMER, anim); + } else if (we_are_clicked_entity) { + if (TELY_Platform_InputScanCodeIsPressed(input, TELY_PlatformInputScanCode_J)) { + FP_Game_EntityActionSetState(action, FP_GameEntityState_AttackA); + } else if (dir_vector.x == 0.f && dir_vector.y == 0.f) { + FP_Game_EntityActionSetState(action, FP_GameEntityState_Idle); + } } } // NOTE: Tick entity action ================================================================ - TELY_AssetSpriteAnimation const *anim = action->anim.asset; - if (anim) { - if (action->anim.frame >= anim->count) { // NOTE: Animation is finished - action->anim.frame = 0; - action->anim.ticks = 0; - } else if (action->anim.ticks++ > 4 /*ticks_per_anim_frame*/) { - action->anim.frame++; - action->anim.ticks = 0; - } - } - - if (action->timer_s != FP_GAME_ENTITY_ACTION_INFINITE_TIMER) { - action->timer_s = DQN_MAX(action->timer_s - DQN_CAST(Dqn_f32)input->delta_s, 0.f); - } + action->timer_s += DQN_CAST(Dqn_f32)input->delta_s; } // NOTE: Calculate entity attack box ======================================================= - if (action->state == FP_GameEntityState_AttackA) { - + if (action->state == FP_GameEntityState_AttackA || + action->state == FP_GameEntityState_AttackB || + action->state == FP_GameEntityState_AttackC) { entity->attack_box_size = entity->local_hit_box_size; TELY_AssetSpriteSheet const *sprite_sheet = entity->sprite_sheet; if (sprite_sheet) { @@ -730,11 +714,14 @@ void FP_GameRender(FP_Game *game, TELY_Platform *platform, TELY_Renderer *render } // NOTE: Render entity sprites ============================================================= - if (entity->sprite_sheet && entity->action.anim.asset) { - TELY_AssetSpriteSheet const *sprite_sheet = entity->sprite_sheet; - TELY_AssetSpriteAnimation const *sprite_anim = entity->action.anim.asset; + if (entity->sprite_sheet && entity->action.anim) { - Dqn_usize sprite_index = (sprite_anim->index + (entity->action.anim.frame % sprite_anim->count)) % sprite_sheet->sprite_count; + TELY_AssetSpriteSheet const *sprite_sheet = entity->sprite_sheet; + FP_GameEntityAction const *action = &entity->action; + TELY_AssetSpriteAnimation const *sprite_anim = action->anim; + uint16_t anim_frame = DQN_CAST(uint16_t)(action->timer_s / sprite_anim->seconds_per_frame) % sprite_anim->count; + + Dqn_usize sprite_index = sprite_anim->index + anim_frame; Dqn_usize sprite_sheet_row = sprite_index / sprite_sheet->sprites_per_row; Dqn_usize sprite_sheet_column = sprite_index % sprite_sheet->sprites_per_row; diff --git a/feely_pona.h b/feely_pona.h index adef30d..5eb99d3 100644 --- a/feely_pona.h +++ b/feely_pona.h @@ -52,14 +52,6 @@ enum FP_GameEntityState FP_GameEntityState_Run, }; -struct FP_GameEntityAnimation -{ - TELY_AssetSpriteAnimation *asset; - uint16_t frame; - uint16_t ticks; - uint16_t ticks_per_frame; -}; - struct FP_GameWaypoint { Dqn_V2I pos; @@ -67,13 +59,20 @@ struct FP_GameWaypoint FP_GameWaypoint *prev; }; +enum FP_GameEntityActionFlag +{ + FP_GameEntityActionFlag_StateTransition = 1 << 0, + FP_GameEntityActionFlag_Failed = 1 << 1, +}; + Dqn_f32 const FP_GAME_ENTITY_ACTION_INFINITE_TIMER = -1.f; struct FP_GameEntityAction { - FP_GameEntityState state; - bool dirty; - FP_GameEntityAnimation anim; - Dqn_f32 timer_s; + FP_GameEntityState state; + uint32_t flags; // Bit flags corresponding with `FP_GameEntityActionFlag` + TELY_AssetSpriteAnimation *anim; + Dqn_f32 timer_s; + Dqn_f32 end_at_s; }; struct FP_GameEntity diff --git a/feely_pona_game.cpp b/feely_pona_game.cpp index 13af4a6..66e00a1 100644 --- a/feely_pona_game.cpp +++ b/feely_pona_game.cpp @@ -421,3 +421,31 @@ static Dqn_Rect FP_Game_CalcEntityWorldBoundingBox(FP_Game *game, FP_GameEntityH Dqn_Rect result = FP_Game_CalcEntityArrayWorldBoundingBox(game, &handle, 1); return result; } + +// Transition the action into the desire state and set the flag to indicate it +// has just transitioned +static void FP_Game_EntityActionSetState(FP_GameEntityAction *action, FP_GameEntityState state) +{ + if (!action) + return; + action->state = state; + action->flags |= FP_GameEntityActionFlag_StateTransition; +} + +// Reset the timers and animation for the current action and set the duration +// for the new action. +static void FP_Game_EntityActionReset(FP_GameEntityAction *action, Dqn_f32 new_action_duration, TELY_AssetSpriteAnimation *anim) +{ + if (!action) + return; + action->anim = anim; + action->timer_s = 0.f; + action->end_at_s = new_action_duration; + action->flags = 0; +} + +static bool FP_Game_EntityActionHasFailed(FP_GameEntityAction const *action) +{ + bool result = action ? action->flags & FP_GameEntityActionFlag_Failed : true; + return result; +}