fp: Wire up attack chaining

This commit is contained in:
doyle 2023-09-18 23:00:30 +10:00
parent 3e31d0ef69
commit 12e6e5d6f1
4 changed files with 124 additions and 110 deletions

2
External/tely vendored

@ -1 +1 @@
Subproject commit 0b5b08bb635d1aad6832a69d30e2c45f7a59af8b Subproject commit 964aa47a6ed2e2bb1606dd576a57867d09ca8ba6

View File

@ -60,26 +60,26 @@ void TELY_DLL_Init(void *user_data)
sheet->sprite_size = Dqn_V2I_InitNx2(50, 37); sheet->sprite_size = Dqn_V2I_InitNx2(50, 37);
TELY_AssetSpriteAnimation hero_anims[] = { TELY_AssetSpriteAnimation hero_anims[] = {
{DQN_STRING8("Everything"), /*index*/ 0, /*count*/ sheet->sprite_count}, {DQN_STRING8("Everything"), /*index*/ 0, /*count*/ sheet->sprite_count, /*seconds_per_frame*/ 1 / 12.f},
{DQN_STRING8("Idle"), /*index*/ 0, /*count*/ 3}, {DQN_STRING8("Idle"), /*index*/ 0, /*count*/ 3, /*seconds_per_frame*/ 1 / 4.f},
{DQN_STRING8("Run"), /*index*/ 8, /*count*/ 6}, {DQN_STRING8("Run"), /*index*/ 8, /*count*/ 6, /*seconds_per_frame*/ 1 / 8.f},
{DQN_STRING8("Jump"), /*index*/ 14, /*count*/ 10}, {DQN_STRING8("Jump"), /*index*/ 14, /*count*/ 10, /*seconds_per_frame*/ 1 / 12.f},
{DQN_STRING8("Floor slide"), /*index*/ 24, /*count*/ 5}, {DQN_STRING8("Floor slide"), /*index*/ 24, /*count*/ 5, /*seconds_per_frame*/ 1 / 12.f},
{DQN_STRING8("Unknown"), /*index*/ 29, /*count*/ 9}, {DQN_STRING8("Unknown"), /*index*/ 29, /*count*/ 9, /*seconds_per_frame*/ 1 / 12.f},
{DQN_STRING8("Attack A"), /*index*/ 38, /*count*/ 11}, {DQN_STRING8("Attack A"), /*index*/ 42, /*count*/ 7, /*seconds_per_frame*/ 1 / 12.f},
{DQN_STRING8("Attack B"), /*index*/ 49, /*count*/ 4}, {DQN_STRING8("Attack B"), /*index*/ 49, /*count*/ 4, /*seconds_per_frame*/ 1 / 8.f},
{DQN_STRING8("Attack C"), /*index*/ 53, /*count*/ 6}, {DQN_STRING8("Attack C"), /*index*/ 53, /*count*/ 6, /*seconds_per_frame*/ 1 / 12.f},
{DQN_STRING8("Hurt A"), /*index*/ 59, /*count*/ 5}, {DQN_STRING8("Hurt A"), /*index*/ 59, /*count*/ 5, /*seconds_per_frame*/ 1 / 12.f},
{DQN_STRING8("Hurt B"), /*index*/ 64, /*count*/ 5}, {DQN_STRING8("Hurt B"), /*index*/ 64, /*count*/ 5, /*seconds_per_frame*/ 1 / 12.f},
{DQN_STRING8("Unsheath sword"), /*index*/ 69, /*count*/ 4}, {DQN_STRING8("Unsheath sword"), /*index*/ 69, /*count*/ 4, /*seconds_per_frame*/ 1 / 12.f},
{DQN_STRING8("Sheath sword"), /*index*/ 73, /*count*/ 4}, {DQN_STRING8("Sheath sword"), /*index*/ 73, /*count*/ 4, /*seconds_per_frame*/ 1 / 12.f},
{DQN_STRING8("Air drift"), /*index*/ 77, /*count*/ 2}, {DQN_STRING8("Air drift"), /*index*/ 77, /*count*/ 2, /*seconds_per_frame*/ 1 / 12.f},
{DQN_STRING8("Air drop"), /*index*/ 79, /*count*/ 2}, {DQN_STRING8("Air drop"), /*index*/ 79, /*count*/ 2, /*seconds_per_frame*/ 1 / 12.f},
{DQN_STRING8("Ladder climb"), /*index*/ 81, /*count*/ 4}, {DQN_STRING8("Ladder climb"), /*index*/ 81, /*count*/ 4, /*seconds_per_frame*/ 1 / 12.f},
{DQN_STRING8("Chi push"), /*index*/ 85, /*count*/ 8}, {DQN_STRING8("Chi push"), /*index*/ 85, /*count*/ 8, /*seconds_per_frame*/ 1 / 12.f},
{DQN_STRING8("Leap slice A"), /*index*/ 93, /*count*/ 7}, {DQN_STRING8("Leap slice A"), /*index*/ 93, /*count*/ 7, /*seconds_per_frame*/ 1 / 12.f},
{DQN_STRING8("Leap slice B"), /*index*/ 100, /*count*/ 3}, {DQN_STRING8("Leap slice B"), /*index*/ 100, /*count*/ 3, /*seconds_per_frame*/ 1 / 12.f},
{DQN_STRING8("Leap slice C"), /*index*/ 103, /*count*/ 6}, {DQN_STRING8("Leap slice C"), /*index*/ 103, /*count*/ 6, /*seconds_per_frame*/ 1 / 12.f},
}; };
game->hero_sprite_anims = Dqn_Slice_Alloc<TELY_AssetSpriteAnimation>(&platform->arena, DQN_ARRAY_UCOUNT(hero_anims), Dqn_ZeroMem_No); game->hero_sprite_anims = Dqn_Slice_Alloc<TELY_AssetSpriteAnimation>(&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; FP_GameEntityAction *action = &entity->action;
{ {
bool we_are_clicked_entity = entity->handle == game->clicked_entity; bool we_are_clicked_entity = entity->handle == game->clicked_entity;
if (action->state == FP_GameEntityState_Nil) { bool action_has_finished = action->timer_s != FP_GAME_ENTITY_ACTION_INFINITE_TIMER && action->timer_s >= action->end_at_s;
action->state = FP_GameEntityState_Idle;
action->dirty = true; if (action->state == FP_GameEntityState_Nil)
} FP_Game_EntityActionSetState(action, FP_GameEntityState_Idle);
if (action->state == FP_GameEntityState_Idle) { if (action->state == FP_GameEntityState_Idle) {
if (we_are_clicked_entity && TELY_Platform_InputScanCodeIsDown(input, TELY_PlatformInputScanCode_J)) { if (action->flags & FP_GameEntityActionFlag_StateTransition) {
action->state = FP_GameEntityState_AttackA; TELY_AssetSpriteAnimation *anim = entity->sprite_anims.data + TELY_Asset_GetSpriteAnimation(entity->sprite_anims, DQN_STRING8("Idle")).index;
action->dirty = true; FP_Game_EntityActionReset(action, FP_GAME_ENTITY_ACTION_INFINITE_TIMER, anim);
} else if (we_are_clicked_entity && (dir_vector.x || dir_vector.y)) { } else if (we_are_clicked_entity) {
action->state = FP_GameEntityState_Run; if (TELY_Platform_InputScanCodeIsPressed(input, TELY_PlatformInputScanCode_J)) {
action->dirty = true; FP_Game_EntityActionSetState(action, FP_GameEntityState_AttackA);
} else if (action->dirty) { } else if (dir_vector.x || dir_vector.y) {
action->anim = {}; FP_Game_EntityActionSetState(action, FP_GameEntityState_Run);
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->state == FP_GameEntityState_AttackA) { if (action->state == FP_GameEntityState_AttackA) {
if (action->dirty) { if (action->flags & FP_GameEntityActionFlag_StateTransition) {
action->anim = {}; TELY_AssetSpriteAnimation *anim = entity->sprite_anims.data + TELY_Asset_GetSpriteAnimation(entity->sprite_anims, DQN_STRING8("Attack A")).index;
action->anim.asset = 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);
action->timer_s = action->anim.asset->count * 0.016f; } else if (action_has_finished) {
action->dirty = false; FP_Game_EntityActionSetState(action, FP_GameEntityState_Idle);
} else if (action->timer_s <= 0.f) { } else if (!FP_Game_EntityActionHasFailed(action) && we_are_clicked_entity) {
action->state = FP_GameEntityState_Idle; if (TELY_Platform_InputScanCodeIsPressed(input, TELY_PlatformInputScanCode_J)) {
action->dirty = true; Dqn_f32 t01 = action->timer_s / action->end_at_s;
} else if (we_are_clicked_entity && TELY_Platform_InputScanCodeIsDown(input, TELY_PlatformInputScanCode_J)) { if (t01 > 0.5f)
// NOTE: Trigger AttackB if action is at the correct time FP_Game_EntityActionSetState(action, FP_GameEntityState_AttackB);
action->state = FP_GameEntityState_AttackB; else
action->dirty = true; action->flags |= FP_GameEntityActionFlag_Failed;
}
} }
} }
if (action->state == FP_GameEntityState_AttackB) { if (action->state == FP_GameEntityState_AttackB) {
if (action->dirty) { if (action->flags & FP_GameEntityActionFlag_StateTransition) {
action->anim = {}; TELY_AssetSpriteAnimation *anim = entity->sprite_anims.data + TELY_Asset_GetSpriteAnimation(entity->sprite_anims, DQN_STRING8("Attack B")).index;
action->anim.asset = 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);
action->timer_s = action->anim.asset->count * 0.016f; } else if (action_has_finished) {
action->dirty = false; FP_Game_EntityActionSetState(action, FP_GameEntityState_Idle);
} else if (action->timer_s <= 0.f) { } else if (!FP_Game_EntityActionHasFailed(action) && we_are_clicked_entity) {
action->state = FP_GameEntityState_Idle; if (TELY_Platform_InputScanCodeIsPressed(input, TELY_PlatformInputScanCode_J)) {
action->dirty = true; Dqn_f32 t01 = action->timer_s / action->end_at_s;
} else if (we_are_clicked_entity && TELY_Platform_InputScanCodeIsDown(input, TELY_PlatformInputScanCode_J)) { if (t01 > 0.5f)
// NOTE: Trigger AttackC if action is at the correct time FP_Game_EntityActionSetState(action, FP_GameEntityState_AttackC);
action->state = FP_GameEntityState_AttackC; else
action->dirty = true; action->flags |= FP_GameEntityActionFlag_Failed;
}
} }
} }
if (action->state == FP_GameEntityState_AttackC) { if (action->state == FP_GameEntityState_AttackC) {
if (action->dirty) { if (action->flags & FP_GameEntityActionFlag_StateTransition) {
action->anim = {}; TELY_AssetSpriteAnimation *anim = entity->sprite_anims.data + TELY_Asset_GetSpriteAnimation(entity->sprite_anims, DQN_STRING8("Attack C")).index;
action->anim.asset = 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);
action->timer_s = action->anim.asset->count * 0.016f; } else if (action_has_finished) {
action->dirty = false; FP_Game_EntityActionSetState(action, FP_GameEntityState_Idle);
} else if (action->timer_s <= 0.f) {
action->state = FP_GameEntityState_Idle;
action->dirty = true;
} }
} }
if (action->state == FP_GameEntityState_Run) { if (action->state == FP_GameEntityState_Run) {
if (action->dirty) { if (action->flags & FP_GameEntityActionFlag_StateTransition) {
action->anim = {}; TELY_AssetSpriteAnimation *anim = entity->sprite_anims.data + TELY_Asset_GetSpriteAnimation(entity->sprite_anims, DQN_STRING8("Run")).index;
action->anim.asset = 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);
action->timer_s = FP_GAME_ENTITY_ACTION_INFINITE_TIMER; } else if (we_are_clicked_entity) {
action->dirty = false; 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);
} }
if (we_are_clicked_entity && (dir_vector.x == 0.f && dir_vector.y == 0.f)) {
action->state = FP_GameEntityState_Idle;
action->dirty = true;
} }
} }
// NOTE: Tick entity action ================================================================ // NOTE: Tick entity action ================================================================
TELY_AssetSpriteAnimation const *anim = action->anim.asset; action->timer_s += DQN_CAST(Dqn_f32)input->delta_s;
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);
}
} }
// NOTE: Calculate entity attack box ======================================================= // 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; entity->attack_box_size = entity->local_hit_box_size;
TELY_AssetSpriteSheet const *sprite_sheet = entity->sprite_sheet; TELY_AssetSpriteSheet const *sprite_sheet = entity->sprite_sheet;
if (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 ============================================================= // NOTE: Render entity sprites =============================================================
if (entity->sprite_sheet && entity->action.anim.asset) { if (entity->sprite_sheet && entity->action.anim) {
TELY_AssetSpriteSheet const *sprite_sheet = entity->sprite_sheet;
TELY_AssetSpriteAnimation const *sprite_anim = entity->action.anim.asset;
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_row = sprite_index / sprite_sheet->sprites_per_row;
Dqn_usize sprite_sheet_column = sprite_index % sprite_sheet->sprites_per_row; Dqn_usize sprite_sheet_column = sprite_index % sprite_sheet->sprites_per_row;

View File

@ -52,14 +52,6 @@ enum FP_GameEntityState
FP_GameEntityState_Run, FP_GameEntityState_Run,
}; };
struct FP_GameEntityAnimation
{
TELY_AssetSpriteAnimation *asset;
uint16_t frame;
uint16_t ticks;
uint16_t ticks_per_frame;
};
struct FP_GameWaypoint struct FP_GameWaypoint
{ {
Dqn_V2I pos; Dqn_V2I pos;
@ -67,13 +59,20 @@ struct FP_GameWaypoint
FP_GameWaypoint *prev; 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; Dqn_f32 const FP_GAME_ENTITY_ACTION_INFINITE_TIMER = -1.f;
struct FP_GameEntityAction struct FP_GameEntityAction
{ {
FP_GameEntityState state; FP_GameEntityState state;
bool dirty; uint32_t flags; // Bit flags corresponding with `FP_GameEntityActionFlag`
FP_GameEntityAnimation anim; TELY_AssetSpriteAnimation *anim;
Dqn_f32 timer_s; Dqn_f32 timer_s;
Dqn_f32 end_at_s;
}; };
struct FP_GameEntity struct FP_GameEntity

View File

@ -421,3 +421,31 @@ static Dqn_Rect FP_Game_CalcEntityWorldBoundingBox(FP_Game *game, FP_GameEntityH
Dqn_Rect result = FP_Game_CalcEntityArrayWorldBoundingBox(game, &handle, 1); Dqn_Rect result = FP_Game_CalcEntityArrayWorldBoundingBox(game, &handle, 1);
return result; 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;
}