fp: Use animated sprites, use ms for animation duration

This commit is contained in:
doyle 2023-09-24 23:08:30 +10:00
parent 858a66786a
commit 677486e094
4 changed files with 161 additions and 176 deletions

2
External/tely vendored

@ -1 +1 @@
Subproject commit 92ae772b6fa8f79bb0d5a93b643305fe482a6ed3 Subproject commit 96ddc4d93441f1fdccf04d9f7faed96de7971c52

View File

@ -83,7 +83,8 @@ TELY_AssetSpriteSheet FP_LoadSpriteSheetFromSpec(TELY_Platform *platform, TELY_A
anim->label = Dqn_String8_Copy(allocator, anim_name); anim->label = Dqn_String8_Copy(allocator, anim_name);
anim->index = DQN_CAST(uint16_t)sprite_rect_index; anim->index = DQN_CAST(uint16_t)sprite_rect_index;
anim->count = DQN_CAST(uint16_t)frame_count.value; anim->count = DQN_CAST(uint16_t)frame_count.value;
anim->seconds_per_frame = 1.f / frames_per_second.value; anim->ms_per_frame = DQN_CAST(uint32_t)(1000.f / frames_per_second.value);
DQN_ASSERT(anim->ms_per_frame != 0);
} else { } else {
DQN_ASSERTF(line_splits.size == 4, "Expected 4 splits for sprite frame lines"); DQN_ASSERTF(line_splits.size == 4, "Expected 4 splits for sprite frame lines");
Dqn_String8ToU64Result x = Dqn_String8_ToU64(line_splits.data[0], 0); Dqn_String8ToU64Result x = Dqn_String8_ToU64(line_splits.data[0], 0);
@ -140,26 +141,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, /*seconds_per_frame*/ 1 / 12.f}, {DQN_STRING8("Everything"), /*index*/ 0, /*count*/ sheet->sprite_count, /*ms_per_frame*/ DQN_CAST(uint32_t)(1000.f / 12.f)},
{DQN_STRING8("Idle"), /*index*/ 0, /*count*/ 3, /*seconds_per_frame*/ 1 / 4.f}, {DQN_STRING8("Idle"), /*index*/ 0, /*count*/ 3, /*ms_per_frame*/ DQN_CAST(uint32_t)(1000.f / 4.f)},
{DQN_STRING8("Run"), /*index*/ 8, /*count*/ 6, /*seconds_per_frame*/ 1 / 8.f}, {DQN_STRING8("Run"), /*index*/ 8, /*count*/ 6, /*ms_per_frame*/ DQN_CAST(uint32_t)(1000.f / 8.f)},
{DQN_STRING8("Jump"), /*index*/ 14, /*count*/ 10, /*seconds_per_frame*/ 1 / 12.f}, {DQN_STRING8("Jump"), /*index*/ 14, /*count*/ 10, /*ms_per_frame*/ DQN_CAST(uint32_t)(1000.f / 12.f)},
{DQN_STRING8("Floor slide"), /*index*/ 24, /*count*/ 5, /*seconds_per_frame*/ 1 / 12.f}, {DQN_STRING8("Floor slide"), /*index*/ 24, /*count*/ 5, /*ms_per_frame*/ DQN_CAST(uint32_t)(1000.f / 12.f)},
{DQN_STRING8("Unknown"), /*index*/ 29, /*count*/ 9, /*seconds_per_frame*/ 1 / 12.f}, {DQN_STRING8("Unknown"), /*index*/ 29, /*count*/ 9, /*ms_per_frame*/ DQN_CAST(uint32_t)(1000.f / 12.f)},
{DQN_STRING8("Attack A"), /*index*/ 42, /*count*/ 7, /*seconds_per_frame*/ 1 / 12.f}, {DQN_STRING8("Attack A"), /*index*/ 42, /*count*/ 7, /*ms_per_frame*/ DQN_CAST(uint32_t)(1000.f / 12.f)},
{DQN_STRING8("Attack B"), /*index*/ 49, /*count*/ 4, /*seconds_per_frame*/ 1 / 8.f}, {DQN_STRING8("Attack B"), /*index*/ 49, /*count*/ 4, /*ms_per_frame*/ DQN_CAST(uint32_t)(1000.f / 8.f)},
{DQN_STRING8("Attack C"), /*index*/ 53, /*count*/ 6, /*seconds_per_frame*/ 1 / 12.f}, {DQN_STRING8("Attack C"), /*index*/ 53, /*count*/ 6, /*ms_per_frame*/ DQN_CAST(uint32_t)(1000.f / 12.f)},
{DQN_STRING8("Hurt A"), /*index*/ 59, /*count*/ 5, /*seconds_per_frame*/ 1 / 12.f}, {DQN_STRING8("Hurt A"), /*index*/ 59, /*count*/ 5, /*ms_per_frame*/ DQN_CAST(uint32_t)(1000.f / 12.f)},
{DQN_STRING8("Hurt B"), /*index*/ 64, /*count*/ 5, /*seconds_per_frame*/ 1 / 12.f}, {DQN_STRING8("Hurt B"), /*index*/ 64, /*count*/ 5, /*ms_per_frame*/ DQN_CAST(uint32_t)(1000.f / 12.f)},
{DQN_STRING8("Unsheath sword"), /*index*/ 69, /*count*/ 4, /*seconds_per_frame*/ 1 / 12.f}, {DQN_STRING8("Unsheath sword"), /*index*/ 69, /*count*/ 4, /*ms_per_frame*/ DQN_CAST(uint32_t)(1000.f / 12.f)},
{DQN_STRING8("Sheath sword"), /*index*/ 73, /*count*/ 4, /*seconds_per_frame*/ 1 / 12.f}, {DQN_STRING8("Sheath sword"), /*index*/ 73, /*count*/ 4, /*ms_per_frame*/ DQN_CAST(uint32_t)(1000.f / 12.f)},
{DQN_STRING8("Air drift"), /*index*/ 77, /*count*/ 2, /*seconds_per_frame*/ 1 / 12.f}, {DQN_STRING8("Air drift"), /*index*/ 77, /*count*/ 2, /*ms_per_frame*/ DQN_CAST(uint32_t)(1000.f / 12.f)},
{DQN_STRING8("Air drop"), /*index*/ 79, /*count*/ 2, /*seconds_per_frame*/ 1 / 12.f}, {DQN_STRING8("Air drop"), /*index*/ 79, /*count*/ 2, /*ms_per_frame*/ DQN_CAST(uint32_t)(1000.f / 12.f)},
{DQN_STRING8("Ladder climb"), /*index*/ 81, /*count*/ 4, /*seconds_per_frame*/ 1 / 12.f}, {DQN_STRING8("Ladder climb"), /*index*/ 81, /*count*/ 4, /*ms_per_frame*/ DQN_CAST(uint32_t)(1000.f / 12.f)},
{DQN_STRING8("Chi push"), /*index*/ 85, /*count*/ 8, /*seconds_per_frame*/ 1 / 12.f}, {DQN_STRING8("Chi push"), /*index*/ 85, /*count*/ 8, /*ms_per_frame*/ DQN_CAST(uint32_t)(1000.f / 12.f)},
{DQN_STRING8("Leap slice A"), /*index*/ 93, /*count*/ 7, /*seconds_per_frame*/ 1 / 12.f}, {DQN_STRING8("Leap slice A"), /*index*/ 93, /*count*/ 7, /*ms_per_frame*/ DQN_CAST(uint32_t)(1000.f / 12.f)},
{DQN_STRING8("Leap slice B"), /*index*/ 100, /*count*/ 3, /*seconds_per_frame*/ 1 / 12.f}, {DQN_STRING8("Leap slice B"), /*index*/ 100, /*count*/ 3, /*ms_per_frame*/ DQN_CAST(uint32_t)(1000.f / 12.f)},
{DQN_STRING8("Leap slice C"), /*index*/ 103, /*count*/ 6, /*seconds_per_frame*/ 1 / 12.f}, {DQN_STRING8("Leap slice C"), /*index*/ 103, /*count*/ 6, /*ms_per_frame*/ DQN_CAST(uint32_t)(1000.f / 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);
@ -291,13 +292,14 @@ void TELY_DLL_Init(void *user_data)
game->test_audio = platform->func_load_audio(assets, DQN_STRING8("Test Audio"), DQN_STRING8("Data/Audio/Purrple Cat - Moonwinds.qoa")); game->test_audio = platform->func_load_audio(assets, DQN_STRING8("Test Audio"), DQN_STRING8("Data/Audio/Purrple Cat - Moonwinds.qoa"));
} }
FP_GameEntityActionSprite FP_EntityActionStateMachine(FP_Game *game, TELY_PlatformInput *input, FP_GameEntity *entity, Dqn_V2 dir_vector) void FP_EntityActionStateMachine(FP_Game *game, TELY_PlatformInput *input, FP_GameEntity *entity, Dqn_V2 dir_vector)
{ {
FP_GameEntityAction *action = &entity->action; FP_GameEntityAction *action = &entity->action;
bool we_are_clicked_entity = entity->handle == game->clicked_entity; bool const we_are_clicked_entity = entity->handle == game->clicked_entity;
bool action_has_finished = action->timer_s != FP_GAME_ENTITY_ACTION_INFINITE_TIMER && action->timer_s >= action->end_at_s; bool const entity_has_velocity = entity->velocity.x || entity->velocity.y;
bool entity_has_velocity = entity->velocity.x || entity->velocity.y; bool const entering_new_state = action->state != action->next_state;
FP_GameEntityActionSprite result = {}; bool const action_has_finished = !entering_new_state && game->clock_ms >= action->end_at_clock_ms;
action->state = action->next_state;
switch (entity->type) { switch (entity->type) {
case FP_EntityType_Nil: { case FP_EntityType_Nil: {
@ -306,84 +308,78 @@ FP_GameEntityActionSprite FP_EntityActionStateMachine(FP_Game *game, TELY_Platfo
case FP_EntityType_Terry: { case FP_EntityType_Terry: {
FP_EntityTerryState *state = DQN_CAST(FP_EntityTerryState *) & action->state; FP_EntityTerryState *state = DQN_CAST(FP_EntityTerryState *) & action->state;
TELY_AssetSpriteSheet *sheet = &game->terry_sprite_sheet; TELY_AssetSpriteSheet *sheet = &game->terry_sprite_sheet;
result.sheet = sheet;
switch (*state) { switch (*state) {
case FP_EntityTerryState_Nil: { case FP_EntityTerryState_Nil: {
FP_Game_EntityActionSetState(action, FP_EntityTerryState_Idle); action->next_state = FP_EntityTerryState_Idle;
} break; } break;
case FP_EntityTerryState_Idle: { case FP_EntityTerryState_Idle: {
result.anim = TELY_Asset_GetSpriteAnimation(sheet, g_anim_names.terry_walk_idle); if (entering_new_state) {
if (action->flags & FP_GameEntityActionFlag_StateTransition) { TELY_AssetAnimatedSprite sprite = TELY_Asset_MakeAnimatedSprite(sheet, g_anim_names.terry_walk_idle, TELY_AssetFlip_No);
FP_Game_EntityActionReset(action, FP_GAME_ENTITY_ACTION_INFINITE_TIMER, result); uint64_t duration_ms = FP_GAME_ENTITY_ACTION_INFINITE_TIMER;
} else if (we_are_clicked_entity) { FP_Game_EntityActionReset(game, entity->handle, duration_ms, sprite);
}
if (we_are_clicked_entity) {
if (TELY_Platform_InputScanCodeIsPressed(input, TELY_PlatformInputScanCode_J) || if (TELY_Platform_InputScanCodeIsPressed(input, TELY_PlatformInputScanCode_J) ||
TELY_Platform_InputGamepadButtonCodeIsPressed(input, 0, TELY_PlatformInputGamepadButtonCode_X)) { TELY_Platform_InputGamepadButtonCodeIsPressed(input, 0, TELY_PlatformInputGamepadButtonCode_X)) {
switch (entity->direction) { switch (entity->direction) {
case FP_GameDirection_Up: { case FP_GameDirection_Up: {
FP_Game_EntityActionSetState(action, FP_EntityTerryState_AttackUp); action->next_state = FP_EntityTerryState_AttackUp;
} break; } break;
case FP_GameDirection_Left: { case FP_GameDirection_Left: {
FP_Game_EntityActionSetState(action, FP_EntityTerryState_AttackSide); action->next_state = FP_EntityTerryState_AttackSide;
} break; } break;
case FP_GameDirection_Right: { case FP_GameDirection_Right: {
FP_Game_EntityActionSetState(action, FP_EntityTerryState_AttackSide); action->next_state = FP_EntityTerryState_AttackSide;
} break; } break;
case FP_GameDirection_Down: { case FP_GameDirection_Down: {
FP_Game_EntityActionSetState(action, FP_EntityTerryState_AttackDown); action->next_state = FP_EntityTerryState_AttackDown;
} break; } break;
} }
} else if (dir_vector.x || dir_vector.y) { } else if (dir_vector.x || dir_vector.y) {
FP_Game_EntityActionSetState(action, FP_EntityTerryState_Run); action->next_state = FP_EntityTerryState_Run;
} }
} }
} break; } break;
case FP_EntityTerryState_AttackUp: { case FP_EntityTerryState_AttackUp: {
result.anim = TELY_Asset_GetSpriteAnimation(sheet, g_anim_names.terry_attack_up); if (entering_new_state) {
if (action->flags & FP_GameEntityActionFlag_StateTransition) { TELY_AssetAnimatedSprite sprite = TELY_Asset_MakeAnimatedSprite(sheet, g_anim_names.terry_attack_up, TELY_AssetFlip_No);
FP_Game_EntityActionReset(action, result.anim->count * result.anim->seconds_per_frame, result); uint64_t duration_ms = sprite.anim->count * sprite.anim->ms_per_frame;
} else if (action_has_finished) { FP_Game_EntityActionReset(game, entity->handle, duration_ms, sprite);
FP_Game_EntityActionSetState(action, FP_EntityTerryState_Idle); }
if (action_has_finished) {
action->next_state = FP_EntityTerryState_Idle;
} }
} break; } break;
case FP_EntityTerryState_AttackDown: { case FP_EntityTerryState_AttackDown: {
result.anim = TELY_Asset_GetSpriteAnimation(sheet, g_anim_names.terry_walk_down); if (entering_new_state) {
if (action->flags & FP_GameEntityActionFlag_StateTransition) { TELY_AssetAnimatedSprite sprite = TELY_Asset_MakeAnimatedSprite(sheet, g_anim_names.terry_attack_down, TELY_AssetFlip_No);
FP_Game_EntityActionReset(action, result.anim->count * result.anim->seconds_per_frame, result); uint64_t duration_ms = sprite.anim->count * sprite.anim->ms_per_frame;
} else if (action_has_finished) { FP_Game_EntityActionReset(game, entity->handle, duration_ms, sprite);
FP_Game_EntityActionSetState(action, FP_EntityTerryState_Idle);
} }
if (action_has_finished)
action->next_state = FP_EntityTerryState_Idle;
} break; } break;
case FP_EntityTerryState_AttackSide: { case FP_EntityTerryState_AttackSide: {
result.anim = TELY_Asset_GetSpriteAnimation(sheet, g_anim_names.terry_attack_side); if (entering_new_state) {
action->flip_on_x = entity->direction == FP_GameDirection_Right; TELY_AssetFlip flip = entity->direction == FP_GameDirection_Right ? TELY_AssetFlip_X : TELY_AssetFlip_No;
TELY_AssetAnimatedSprite sprite = TELY_Asset_MakeAnimatedSprite(sheet, g_anim_names.terry_attack_side, flip);
if (action->flags & FP_GameEntityActionFlag_StateTransition) { uint64_t duration_ms = sprite.anim->count * sprite.anim->ms_per_frame;
FP_Game_EntityActionReset(action, result.anim->count * result.anim->seconds_per_frame, result); FP_Game_EntityActionReset(game, entity->handle, duration_ms, sprite);
} else if (action_has_finished) {
FP_Game_EntityActionSetState(action, FP_EntityTerryState_Idle);
action->flip_on_x = false;
} }
#if 0 if (action_has_finished)
else if (!FP_Game_EntityActionHasFailed(action) && we_are_clicked_entity) { action->next_state = FP_EntityTerryState_Idle;
if (TELY_Platform_InputScanCodeIsPressed(input, TELY_PlatformInputScanCode_J) ||
TELY_Platform_InputGamepadButtonCodeIsPressed(input, 0, TELY_PlatformInputGamepadButtonCode_X)) {
Dqn_f32 t01 = action->timer_s / action->end_at_s;
if (t01 > 0.5f)
FP_Game_EntityActionSetState(action, FP_EntityTerryState_AttackB);
else
action->flags |= FP_GameEntityActionFlag_Failed;
}
}
#endif
} break; } break;
case FP_EntityTerryState_Run: { case FP_EntityTerryState_Run: {
@ -394,49 +390,50 @@ FP_GameEntityActionSprite FP_EntityActionStateMachine(FP_Game *game, TELY_Platfo
case FP_GameDirection_Left: desired_action_name = g_anim_names.terry_walk_left; break; case FP_GameDirection_Left: desired_action_name = g_anim_names.terry_walk_left; break;
case FP_GameDirection_Right: desired_action_name = g_anim_names.terry_walk_right; break; case FP_GameDirection_Right: desired_action_name = g_anim_names.terry_walk_right; break;
} }
result.anim = TELY_Asset_GetSpriteAnimation(sheet, desired_action_name);
if (entering_new_state || action->sprite.anim->label != desired_action_name) {
TELY_AssetAnimatedSprite sprite = TELY_Asset_MakeAnimatedSprite(sheet, desired_action_name, TELY_AssetFlip_No);
uint64_t duration_ms = FP_GAME_ENTITY_ACTION_INFINITE_TIMER;
FP_Game_EntityActionReset(game, entity->handle, duration_ms, sprite);
}
if (we_are_clicked_entity) { if (we_are_clicked_entity) {
if (TELY_Platform_InputScanCodeIsPressed(input, TELY_PlatformInputScanCode_J) || if (TELY_Platform_InputScanCodeIsPressed(input, TELY_PlatformInputScanCode_J) ||
TELY_Platform_InputGamepadButtonCodeIsPressed(input, 0, TELY_PlatformInputGamepadButtonCode_X)) { TELY_Platform_InputGamepadButtonCodeIsPressed(input, 0, TELY_PlatformInputGamepadButtonCode_X)) {
switch (entity->direction) { switch (entity->direction) {
case FP_GameDirection_Up: { case FP_GameDirection_Up: {
FP_Game_EntityActionSetState(action, FP_EntityTerryState_AttackUp); action->next_state = FP_EntityTerryState_AttackUp;
} break; } break;
case FP_GameDirection_Left: { case FP_GameDirection_Left: {
FP_Game_EntityActionSetState(action, FP_EntityTerryState_AttackSide); action->next_state = FP_EntityTerryState_AttackSide;
} break; } break;
case FP_GameDirection_Right: { case FP_GameDirection_Right: {
FP_Game_EntityActionSetState(action, FP_EntityTerryState_AttackSide); action->next_state = FP_EntityTerryState_AttackSide;
} break; } break;
case FP_GameDirection_Down: { case FP_GameDirection_Down: {
FP_Game_EntityActionSetState(action, FP_EntityTerryState_AttackDown); action->next_state = FP_EntityTerryState_AttackDown;
} break; } break;
} }
} else if (TELY_Platform_InputScanCodeIsPressed(input, TELY_PlatformInputScanCode_LeftShift) || } else if (TELY_Platform_InputScanCodeIsPressed(input, TELY_PlatformInputScanCode_LeftShift) ||
TELY_Platform_InputGamepadButtonCodeIsPressed(input, 0, TELY_PlatformInputGamepadButtonCode_A)) { TELY_Platform_InputGamepadButtonCodeIsPressed(input, 0, TELY_PlatformInputGamepadButtonCode_A)) {
FP_Game_EntityActionSetState(action, FP_EntityTerryState_Dash); action->next_state = FP_EntityTerryState_Dash;
} }
} }
// NOTE: Also handles state transition
if (action->sprite.anim->label != result.anim->label) {
FP_Game_EntityActionReset(action, FP_GAME_ENTITY_ACTION_INFINITE_TIMER, result);
}
if (!entity_has_velocity /*&& !has_collision*/) { if (!entity_has_velocity /*&& !has_collision*/) {
FP_Game_EntityActionSetState(action, FP_EntityTerryState_Idle); action->next_state = FP_EntityTerryState_Idle;
} }
} break; } break;
case FP_EntityTerryState_Dash: { case FP_EntityTerryState_Dash: {
result.anim = TELY_Asset_GetSpriteAnimation(sheet, g_anim_names.terry_walk_right);
if (action->flags & FP_GameEntityActionFlag_StateTransition) { if (entering_new_state) {
FP_Game_EntityActionReset(action, result.anim->count * result.anim->seconds_per_frame, result); TELY_AssetAnimatedSprite sprite = TELY_Asset_MakeAnimatedSprite(sheet, g_anim_names.terry_walk_right, TELY_AssetFlip_No);
uint64_t duration_ms = sprite.anim->count * sprite.anim->ms_per_frame;
FP_Game_EntityActionReset(game, entity->handle, duration_ms, sprite);
Dqn_V2 dash_dir = {}; Dqn_V2 dash_dir = {};
switch (entity->direction) { switch (entity->direction) {
@ -449,13 +446,14 @@ FP_GameEntityActionSprite FP_EntityActionStateMachine(FP_Game *game, TELY_Platfo
Dqn_V2 dash_acceleration = dash_dir * 400'000'000.f; Dqn_V2 dash_acceleration = dash_dir * 400'000'000.f;
Dqn_f32 t = DQN_CAST(Dqn_f32)DQN_SQUARED(input->delta_s); Dqn_f32 t = DQN_CAST(Dqn_f32)DQN_SQUARED(input->delta_s);
entity->velocity = (dash_acceleration * t) + entity->velocity * 2.0f; entity->velocity = (dash_acceleration * t) + entity->velocity * 2.0f;
}
} else if (action_has_finished) { if (action_has_finished) {
if (entity_has_velocity) { if (entity_has_velocity) {
// TODO(doyle): Not sure if this branch triggers properly. // TODO(doyle): Not sure if this branch triggers properly.
FP_Game_EntityActionSetState(action, FP_EntityTerryState_Run); action->next_state = FP_EntityTerryState_Run;
} else { } else {
FP_Game_EntityActionSetState(action, FP_EntityTerryState_Idle); action->next_state = FP_EntityTerryState_Idle;
} }
} }
} break; } break;
@ -482,19 +480,21 @@ FP_GameEntityActionSprite FP_EntityActionStateMachine(FP_Game *game, TELY_Platfo
case FP_EntityType_Smoochie: { case FP_EntityType_Smoochie: {
FP_EntitySmoochieState *state = DQN_CAST(FP_EntitySmoochieState *) & action->state; FP_EntitySmoochieState *state = DQN_CAST(FP_EntitySmoochieState *) & action->state;
TELY_AssetSpriteSheet *sheet = &game->smoochie_sprite_sheet; TELY_AssetSpriteSheet *sheet = &game->smoochie_sprite_sheet;
result.sheet = sheet;
switch (*state) { switch (*state) {
case FP_EntitySmoochieState_Nil: { case FP_EntitySmoochieState_Nil: {
FP_Game_EntityActionSetState(action, FP_EntitySmoochieState_Idle); action->next_state = FP_EntitySmoochieState_Idle;
} break; } break;
case FP_EntitySmoochieState_Idle: { case FP_EntitySmoochieState_Idle: {
result.anim = TELY_Asset_GetSpriteAnimation(sheet, g_anim_names.smoochie_walk_down);
if (action->flags & FP_GameEntityActionFlag_StateTransition) { if (entering_new_state) {
FP_Game_EntityActionReset(action, FP_GAME_ENTITY_ACTION_INFINITE_TIMER, result); TELY_AssetAnimatedSprite sprite = TELY_Asset_MakeAnimatedSprite(sheet, g_anim_names.smoochie_walk_down, TELY_AssetFlip_No);
} else if (we_are_clicked_entity) { uint64_t duration_ms = FP_GAME_ENTITY_ACTION_INFINITE_TIMER;
FP_Game_EntityActionReset(game, entity->handle, duration_ms, sprite);
}
if (we_are_clicked_entity) {
if (TELY_Platform_InputScanCodeIsPressed(input, TELY_PlatformInputScanCode_J) || if (TELY_Platform_InputScanCodeIsPressed(input, TELY_PlatformInputScanCode_J) ||
TELY_Platform_InputGamepadButtonCodeIsPressed(input, 0, TELY_PlatformInputGamepadButtonCode_X)) { TELY_Platform_InputGamepadButtonCodeIsPressed(input, 0, TELY_PlatformInputGamepadButtonCode_X)) {
@ -502,37 +502,41 @@ FP_GameEntityActionSprite FP_EntityActionStateMachine(FP_Game *game, TELY_Platfo
case FP_GameDirection_Up: /*FALLTHRU*/ case FP_GameDirection_Up: /*FALLTHRU*/
case FP_GameDirection_Right: /*FALLTHRU*/ case FP_GameDirection_Right: /*FALLTHRU*/
case FP_GameDirection_Left: { case FP_GameDirection_Left: {
FP_Game_EntityActionSetState(action, FP_EntitySmoochieState_AttackSide); action->next_state = FP_EntitySmoochieState_AttackSide;
} break; } break;
case FP_GameDirection_Down: { case FP_GameDirection_Down: {
FP_Game_EntityActionSetState(action, FP_EntitySmoochieState_AttackDown); action->next_state = FP_EntitySmoochieState_AttackDown;
} break; } break;
} }
} else if (dir_vector.x || dir_vector.y) { } else if (dir_vector.x || dir_vector.y) {
FP_Game_EntityActionSetState(action, FP_EntitySmoochieState_Run); action->next_state = FP_EntitySmoochieState_Run;
} }
} }
} break; } break;
case FP_EntitySmoochieState_AttackDown: { case FP_EntitySmoochieState_AttackDown: {
result.anim = TELY_Asset_GetSpriteAnimation(sheet, g_anim_names.smoochie_attack_down); if (entering_new_state) {
if (action->flags & FP_GameEntityActionFlag_StateTransition) { TELY_AssetAnimatedSprite sprite = TELY_Asset_MakeAnimatedSprite(sheet, g_anim_names.smoochie_attack_down, TELY_AssetFlip_No);
FP_Game_EntityActionReset(action, result.anim->count * result.anim->seconds_per_frame, result); uint64_t duration_ms = sprite.anim->count * sprite.anim->ms_per_frame;
} else if (action_has_finished) { FP_Game_EntityActionReset(game, entity->handle, duration_ms, sprite);
FP_Game_EntityActionSetState(action, FP_EntitySmoochieState_Idle); }
if (action_has_finished) {
action->next_state = FP_EntitySmoochieState_Idle;
} }
} break; } break;
case FP_EntitySmoochieState_AttackSide: { case FP_EntitySmoochieState_AttackSide: {
result.anim = TELY_Asset_GetSpriteAnimation(sheet, g_anim_names.smoochie_attack_side); if (entering_new_state) {
action->flip_on_x = entity->direction == FP_GameDirection_Right; TELY_AssetFlip flip = entity->direction == FP_GameDirection_Right ? TELY_AssetFlip_X : TELY_AssetFlip_No;
if (action->flags & FP_GameEntityActionFlag_StateTransition) { TELY_AssetAnimatedSprite sprite = TELY_Asset_MakeAnimatedSprite(sheet, g_anim_names.smoochie_attack_side, flip);
FP_Game_EntityActionReset(action, result.anim->count * result.anim->seconds_per_frame, result); uint64_t duration_ms = sprite.anim->count * sprite.anim->ms_per_frame;
} else if (action_has_finished) { FP_Game_EntityActionReset(game, entity->handle, duration_ms, sprite);
FP_Game_EntityActionSetState(action, FP_EntitySmoochieState_Idle);
action->flip_on_x = false;
} }
if (action_has_finished)
action->next_state = FP_EntitySmoochieState_Idle;
} break; } break;
case FP_EntitySmoochieState_AttackHeart: { case FP_EntitySmoochieState_AttackHeart: {
@ -546,7 +550,12 @@ FP_GameEntityActionSprite FP_EntityActionStateMachine(FP_Game *game, TELY_Platfo
case FP_GameDirection_Left: desired_action_name = g_anim_names.smoochie_walk_left; break; case FP_GameDirection_Left: desired_action_name = g_anim_names.smoochie_walk_left; break;
case FP_GameDirection_Right: desired_action_name = g_anim_names.smoochie_walk_right; break; case FP_GameDirection_Right: desired_action_name = g_anim_names.smoochie_walk_right; break;
} }
result.anim = TELY_Asset_GetSpriteAnimation(sheet, desired_action_name);
if (entering_new_state || action->sprite.anim->label != desired_action_name) {
TELY_AssetAnimatedSprite sprite = TELY_Asset_MakeAnimatedSprite(sheet, desired_action_name, TELY_AssetFlip_No);
uint64_t duration_ms = FP_GAME_ENTITY_ACTION_INFINITE_TIMER;
FP_Game_EntityActionReset(game, entity->handle, duration_ms, sprite);
}
if (we_are_clicked_entity) { if (we_are_clicked_entity) {
if (TELY_Platform_InputScanCodeIsPressed(input, TELY_PlatformInputScanCode_J) || if (TELY_Platform_InputScanCodeIsPressed(input, TELY_PlatformInputScanCode_J) ||
@ -556,23 +565,18 @@ FP_GameEntityActionSprite FP_EntityActionStateMachine(FP_Game *game, TELY_Platfo
case FP_GameDirection_Up: /*FALLTHRU*/ case FP_GameDirection_Up: /*FALLTHRU*/
case FP_GameDirection_Right: /*FALLTHRU*/ case FP_GameDirection_Right: /*FALLTHRU*/
case FP_GameDirection_Left: { case FP_GameDirection_Left: {
FP_Game_EntityActionSetState(action, FP_EntitySmoochieState_AttackSide); action->next_state = FP_EntitySmoochieState_AttackSide;
} break; } break;
case FP_GameDirection_Down: { case FP_GameDirection_Down: {
FP_Game_EntityActionSetState(action, FP_EntitySmoochieState_AttackDown); action->next_state = FP_EntitySmoochieState_AttackDown;
} break; } break;
} }
} }
} }
// NOTE: Also handles state transition
if (action->sprite.anim->label != result.anim->label) {
FP_Game_EntityActionReset(action, FP_GAME_ENTITY_ACTION_INFINITE_TIMER, result);
}
if (!entity_has_velocity /*&& !has_collision*/) { if (!entity_has_velocity /*&& !has_collision*/) {
FP_Game_EntityActionSetState(action, FP_EntitySmoochieState_Idle); action->next_state = FP_EntitySmoochieState_Idle;
} }
} break; } break;
} }
@ -595,26 +599,23 @@ FP_GameEntityActionSprite FP_EntityActionStateMachine(FP_Game *game, TELY_Platfo
case FP_EntityType_Merchant: { case FP_EntityType_Merchant: {
FP_EntityTerryMerchantState *state = DQN_CAST(FP_EntityTerryMerchantState *)&action->state; FP_EntityTerryMerchantState *state = DQN_CAST(FP_EntityTerryMerchantState *)&action->state;
TELY_AssetSpriteSheet *sheet = &game->terry_merchant_sprite_sheet; TELY_AssetSpriteSheet *sheet = &game->terry_merchant_sprite_sheet;
result.sheet = sheet;
switch (*state) { switch (*state) {
case FP_EntityTerryMerchantState_Nil: { case FP_EntityTerryMerchantState_Nil: {
FP_Game_EntityActionSetState(action, FP_EntityTerryMerchantState_Idle); action->next_state = FP_EntityTerryMerchantState_Idle;
} break; } break;
case FP_EntityTerryMerchantState_Idle: { case FP_EntityTerryMerchantState_Idle: {
result.anim = TELY_Asset_GetSpriteAnimation(sheet, g_anim_names.terry_merchant); if (entering_new_state) {
if (action->flags & FP_GameEntityActionFlag_StateTransition) { TELY_AssetAnimatedSprite sprite = TELY_Asset_MakeAnimatedSprite(sheet, g_anim_names.terry_merchant, TELY_AssetFlip_No);
FP_Game_EntityActionReset(action, FP_GAME_ENTITY_ACTION_INFINITE_TIMER, result); uint64_t duration_ms = FP_GAME_ENTITY_ACTION_INFINITE_TIMER;
FP_Game_EntityActionReset(game, entity->handle, duration_ms, sprite);
} }
} break; } break;
} }
} break; } break;
} }
// NOTE: Tick entity action ================================================================
action->timer_s += DQN_CAST(Dqn_f32)input->delta_s;
return result;
} }
void FP_Update(TELY_Platform *platform, FP_Game *game, TELY_Renderer *renderer, TELY_PlatformInput *input) void FP_Update(TELY_Platform *platform, FP_Game *game, TELY_Renderer *renderer, TELY_PlatformInput *input)
@ -624,6 +625,7 @@ void FP_Update(TELY_Platform *platform, FP_Game *game, TELY_Renderer *renderer,
if (TELY_Platform_InputKeyIsReleased(input->mouse_left)) if (TELY_Platform_InputKeyIsReleased(input->mouse_left))
game->clicked_entity = game->prev_active_entity; game->clicked_entity = game->prev_active_entity;
game->clock_ms = DQN_CAST(uint64_t)(platform->input.timer_s * 1000.f);
Dqn_V2 dir_vector = {}; Dqn_V2 dir_vector = {};
// NOTE: Keyboard movement input // NOTE: Keyboard movement input
@ -1019,26 +1021,27 @@ void FP_Render(FP_Game *game, TELY_Platform *platform, TELY_Renderer *renderer)
// NOTE: Render entity sprites ============================================================= // NOTE: Render entity sprites =============================================================
if (entity->action.sprite.anim) { if (entity->action.sprite.anim) {
FP_GameEntityAction const *action = &entity->action; FP_GameEntityAction const *action = &entity->action;
TELY_AssetSpriteSheet const *sprite_sheet = action->sprite.sheet; TELY_AssetAnimatedSprite const sprite = action->sprite;
TELY_AssetSpriteAnimation const *sprite_anim = action->sprite.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; uint64_t elapsed_ms = game->clock_ms - action->started_at_clock_ms;
uint16_t anim_frame = DQN_CAST(uint16_t)(elapsed_ms / sprite.anim->ms_per_frame) % sprite.anim->count;
Dqn_usize sprite_index = sprite.anim->index + anim_frame;
Dqn_Rect src_rect = {}; Dqn_Rect src_rect = {};
switch (sprite_sheet->type) { switch (sprite.sheet->type) {
case TELY_AssetSpriteSheetType_Uniform: { case TELY_AssetSpriteSheetType_Uniform: {
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;
src_rect.pos.x = DQN_CAST(Dqn_f32)(sprite_sheet_column * sprite_sheet->sprite_size.w); src_rect.pos.x = DQN_CAST(Dqn_f32)(sprite_sheet_column * sprite.sheet->sprite_size.w);
src_rect.pos.y = DQN_CAST(Dqn_f32)(sprite_sheet_row * sprite_sheet->sprite_size.y); src_rect.pos.y = DQN_CAST(Dqn_f32)(sprite_sheet_row * sprite.sheet->sprite_size.y);
src_rect.size.w = DQN_CAST(Dqn_f32)sprite_sheet->sprite_size.w; src_rect.size.w = DQN_CAST(Dqn_f32)sprite.sheet->sprite_size.w;
src_rect.size.h = DQN_CAST(Dqn_f32)sprite_sheet->sprite_size.h; src_rect.size.h = DQN_CAST(Dqn_f32)sprite.sheet->sprite_size.h;
} break; } break;
case TELY_AssetSpriteSheetType_Rects: { case TELY_AssetSpriteSheetType_Rects: {
DQN_ASSERT(sprite_index < sprite_sheet->rects.size); DQN_ASSERT(sprite_index < sprite.sheet->rects.size);
src_rect = sprite_sheet->rects.data[sprite_index]; src_rect = sprite.sheet->rects.data[sprite_index];
} break; } break;
} }
@ -1046,10 +1049,13 @@ void FP_Render(FP_Game *game, TELY_Platform *platform, TELY_Renderer *renderer)
dest_rect.size = src_rect.size * entity->size_scale; dest_rect.size = src_rect.size * entity->size_scale;
dest_rect.pos = world_pos - (dest_rect.size * .5f); dest_rect.pos = world_pos - (dest_rect.size * .5f);
if (action->flip_on_x) if (sprite.flip & TELY_AssetFlip_X)
dest_rect.size.w *= -1.f; // NOTE: Flip the texture horizontally dest_rect.size.w *= -1.f; // NOTE: Flip the texture horizontally
TELY_Render_TextureColourV4(renderer, sprite_sheet->tex_handle, src_rect, dest_rect, TELY_COLOUR_WHITE_V4); if (sprite.flip & TELY_AssetFlip_Y)
dest_rect.size.h *= -1.f; // NOTE: Flip the texture vertically
TELY_Render_TextureColourV4(renderer, sprite.sheet->tex_handle, src_rect, dest_rect, TELY_COLOUR_WHITE_V4);
} }
// NOTE: Render attack box ================================================================= // NOTE: Render attack box =================================================================

View File

@ -425,32 +425,16 @@ static Dqn_Rect FP_Game_CalcEntityWorldBoundingBox(FP_Game *game, FP_GameEntityH
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, uint32_t 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 // Reset the timers and animation for the current action and set the duration
// for the new action. // for the new action.
static void FP_Game_EntityActionReset(FP_GameEntityAction *action, Dqn_f32 new_action_duration, FP_GameEntityActionSprite sprite) static void FP_Game_EntityActionReset(FP_Game *game, FP_GameEntityHandle entity_handle, uint64_t duration_ms, TELY_AssetAnimatedSprite sprite)
{ {
if (!action) FP_GameEntity *entity = FP_Game_GetEntity(game, entity_handle);
if (!entity)
return; return;
action->sprite = sprite; entity->action.sprite = sprite;
action->timer_s = 0.f; entity->action.started_at_clock_ms = game->clock_ms;
action->end_at_s = new_action_duration; entity->action.end_at_clock_ms = DQN_MAX(duration_ms, game->clock_ms + duration_ms);
action->flags = 0;
}
static bool FP_Game_EntityActionHasFailed(FP_GameEntityAction const *action)
{
bool result = action ? action->flags & FP_GameEntityActionFlag_Failed : true;
return result;
} }
static Dqn_V2I FP_Game_WorldPosToTilePos(FP_Game *game, Dqn_V2 world_pos) static Dqn_V2I FP_Game_WorldPosToTilePos(FP_Game *game, Dqn_V2 world_pos)

View File

@ -60,27 +60,20 @@ struct FP_GameEntitySpawnList
FP_GameEntitySpawnList *prev; FP_GameEntitySpawnList *prev;
}; };
enum FP_GameEntityActionFlag
{
FP_GameEntityActionFlag_StateTransition = 1 << 0,
FP_GameEntityActionFlag_Failed = 1 << 1,
};
struct FP_GameEntityActionSprite struct FP_GameEntityActionSprite
{ {
TELY_AssetSpriteSheet *sheet; TELY_AssetSpriteSheet *sheet;
TELY_AssetSpriteAnimation *anim; TELY_AssetSpriteAnimation *anim;
}; };
Dqn_f32 const FP_GAME_ENTITY_ACTION_INFINITE_TIMER = -1.f; uint64_t const FP_GAME_ENTITY_ACTION_INFINITE_TIMER = UINT64_MAX;
struct FP_GameEntityAction struct FP_GameEntityAction
{ {
bool flip_on_x; uint32_t state;
uint32_t state; uint32_t next_state;
uint32_t flags; // Bit flags corresponding with `FP_GameEntityActionFlag` TELY_AssetAnimatedSprite sprite;
FP_GameEntityActionSprite sprite; uint64_t started_at_clock_ms;
Dqn_f32 timer_s; uint64_t end_at_clock_ms;
Dqn_f32 end_at_s;
}; };
enum FP_GameDirection enum FP_GameDirection
@ -183,6 +176,8 @@ struct FP_Game
FP_GameCamera camera; FP_GameCamera camera;
TELY_RFui rfui; TELY_RFui rfui;
uint64_t clock_ms;
}; };
struct FP_GameAStarNode struct FP_GameAStarNode