fencer/game/src/Player.c
Sara 05451c6ca3 feat: implemented level functions
and refactored Enemy and Player to spawn using level files
2024-01-25 00:07:34 +01:00

272 lines
9.2 KiB
C

#include "Player.h"
#include "debug.h"
#include "dictionary.h"
#include "input_axis.h"
#include "physics_world.h"
#include "PlayerStates.h"
#include "Layers.h"
#include "program.h"
#include "variant.h"
const Vector PLAYER_SPEED = { 1.0f, 0.50f };
static const float PLAYER_INPUT_RATE = 1.f/15.f;
START_REFLECT(Player)
REFLECT_TYPECLASS(Player, Drop)
REFLECT_TYPECLASS(Player, PhysicsEntity)
REFLECT_TYPECLASS(Player, BehaviourEntity)
REFLECT_TYPECLASS(Player, Transformable)
END_REFLECT(Player)
impl_Drop_for(Player,
DestroyPlayer
)
impl_BehaviourEntity_for(Player,
PlayerStart,
PlayerUpdate,
PlayerDraw,
PlayerGetDepth
)
impl_Transformable_for(Player,
PlayerGetTransform
)
impl_PhysicsEntity_for(Player,
PlayerGetRigidBody,
PlayerOnCollision,
PlayerOnOverlap
)
static inline
void Internal_PlayerInitInput(Player* self) {
playerinput_add(self->playerInput,
CompositeAxis1D_as_InputAxis(compositeaxis1d_from_keys(SDL_SCANCODE_A, SDL_SCANCODE_D)),
(InputDelegateFn)PlayerHorizontalInput);
playerinput_add(self->playerInput,
CompositeAxis1D_as_InputAxis(compositeaxis1d_from_keys(SDL_SCANCODE_S, SDL_SCANCODE_W)),
(InputDelegateFn)PlayerVerticalInput);
playerinput_add(self->playerInput,
KeyBind_as_InputAxis(keybind_new(SDL_SCANCODE_SPACE)),
(InputDelegateFn)PlayerJumpInput);
playerinput_add(self->playerInput,
KeyBind_as_InputAxis(keybind_new(SDL_SCANCODE_J)),
(InputDelegateFn)PlayerLightAttackInput);
playerinput_add(self->playerInput,
KeyBind_as_InputAxis(keybind_new(SDL_SCANCODE_K)),
(InputDelegateFn)PlayerHeavyAttackInput);
}
Player* MakePlayer() {
Player* self = malloc(sizeof(Player));
ASSERT_RETURN(self != NULL, NULL, "Failed to allocate space for new Player instance");
*self = (Player) {
.transform = IdentityTransform,
.height = 0.f,
.rigidbody = NULL,
.physicsCollider = NULL,
.hitbox = NULL,
.verticalVelocity = 0.f,
.playerInput = playerinput_new(self, -1),
.moveInput = ZeroVector,
.attackInput = 0,
.animationTriggers = 0,
.facing = 1,
.sprite = sprite_new_empty(),
.idle = NULL,
.walk = NULL,
.jump = NULL,
.jab_a = NULL,
.jab_b = NULL,
.kick_a = NULL,
.slash = NULL,
.air_heavy = NULL,
.slide = NULL,
.animationStateMachine = NULL,
.pushInputTimer = 0.f,
.inputLog = list_from_type(PlayerInputFrame),
};
self->rigidbody = rigidbody_make(Player_as_PhysicsEntity(self));
// physics collider used for movement
self->physicsCollider = collider_new(Player_as_PhysicsEntity(self), shape_new((Vector[]){
MakeVector(-0.2f, -0.065f),
MakeVector( 0.2f, -0.065f),
MakeVector( 0.2f, 0.065f),
MakeVector(-0.2f, 0.065f)
}, 4), 0, PHYSICS_LAYER_CHARACTERS, PHYSICS_LAYER_DEFAULT);
// hitbox is used for combat only
self->hitbox = collider_new(Player_as_PhysicsEntity(self), shape_new((Vector[]){
MakeVector(-0.1f, -0.9f),
MakeVector( 0.1f, -0.9f),
MakeVector( 0.1f, 0.00f),
MakeVector(-0.1f, 0.00f)
}, 4), 1, PHYSICS_LAYER_COMBAT, 0x0); // empty mask and overlap means this does not detect collisions itself
sprite_set_origin(self->sprite, MakeVector(0.45f, 0.925f));
// load and configure animations
self->idle = animation_sprite_new(self->sprite, spritesheet_load("assets/Player_Idle.png", IVectorFrom(512)), 1.5f, LoopMode_Loop);
self->walk = animation_sprite_new(self->sprite, spritesheet_load("assets/Player_Walk.png", IVectorFrom(512)), 5.f, LoopMode_Loop);
self->jump = animation_sprite_new(self->sprite, spritesheet_load("assets/Player_Jumping.png", IVectorFrom(512)), 1.f, LoopMode_Stop);
self->jab_a = animation_sprite_new(self->sprite, spritesheet_load("assets/Player_Jab_A.png", IVectorFrom(512)), 10.f, LoopMode_Stop);
self->jab_b = animation_sprite_new(self->sprite, spritesheet_load("assets/Player_Jab_B.png", IVectorFrom(512)), 10.f, LoopMode_Stop);
self->kick_a = animation_sprite_new(self->sprite, spritesheet_load("assets/Player_Kick_A.png", IVectorFrom(512)), 12.f, LoopMode_Stop);
self->slash = animation_sprite_new(self->sprite, spritesheet_load("assets/Player_Slash.png", IVectorFrom(512)), 12.f, LoopMode_Stop);
self->air_heavy = animation_sprite_new(self->sprite, spritesheet_load("assets/Player_Air_Heavy.png", IVectorFrom(512)), 10.f, LoopMode_Stop);
self->slide = animation_sprite_new(self->sprite, spritesheet_load("assets/Player_Slide.png", IVectorFrom(512)), 1.f, LoopMode_Loop);
self->animationStateMachine = state_machine_init(self, PlayerIdle());
return self;
}
BehaviourEntity SpawnPlayer(Dictionary* args) {
Variant arg;
Player* self = MakePlayer();
Internal_PlayerInitInput(self);
if(dictionary_try_get(args, "position", &arg) && arg.type == Variant_Vector)
self->transform.position = arg.as_vector;
return Player_as_BehaviourEntity(self);
}
void DestroyPlayer(Player* self) {
// deregister and free physics components
physics_world_remove_entity(Player_as_PhysicsEntity(self));
collider_destroy(self->physicsCollider);
rigidbody_destroy(self->rigidbody);
playerinput_drop(self->playerInput);
// erase animations
animation_sprite_destroy(self->idle);
animation_sprite_destroy(self->walk);
animation_sprite_destroy(self->jump);
animation_sprite_destroy(self->jab_a);
animation_sprite_destroy(self->jab_b);
animation_sprite_destroy(self->kick_a);
animation_sprite_destroy(self->air_heavy);
animation_sprite_destroy(self->slide);
sprite_destroy(self->sprite);
state_machine_destroy(self->animationStateMachine);
}
void PlayerStart(Player* self) {
animation_sprite_play_from(self->currentAnimation, 0.0f);
}
static
void PlayerPushInput(Player* self) {
// the current input state
PlayerInputFrame state = {
.time = game_time(),
.direction = self->moveInput,
.light = self->attackInput == 1,
.heavy = self->attackInput == 2,
.jump = self->jumpInput,
.facing = self->facing,
};
// push onto the end of the log
list_add(&self->inputLog, &state);
// erase oldest input from the log
if(self->inputLog.len >= 5)
list_erase(&self->inputLog, 0);
// log current input state
LOG_INFO("%f %f, L:%x, H:%x, J:%x",
state.direction.x, state.direction.y,
state.light, state.heavy, state.jump);
}
void PlayerUpdate(Player* self, float deltaTime) {
// update state machine
state_machine_update(self->animationStateMachine, deltaTime);
// update gravity
self->height += self->verticalVelocity * deltaTime;
if(self->height <= 0.f)
self->height = 0.f;
}
void PlayerDraw(Player* self) {
// create a new transform that adjusts the "ground" transform to reflect distance from the ground as well
Transform trans = self->transform;
trans.position.y -= self->height;
animation_sprite_draw(self->currentAnimation, &trans);
}
void PlayerHorizontalInput(Player* self, InputEvent value) {
self->moveInput.x = value.as_float;
PlayerPushInput(self);
}
void PlayerVerticalInput(Player* self, InputEvent value) {
self->moveInput.y = -value.as_float;
PlayerPushInput(self);
}
void PlayerJumpInput(Player* self, InputEvent value) {
if(value.as_bool) {
self->jumpInput = value.as_bool;
PlayerPushInput(self);
}
}
void PlayerLightAttackInput(Player* self, InputEvent value) {
if(value.as_bool) {
self->attackInput = 1;
PlayerPushInput(self);
}
}
void PlayerHeavyAttackInput(Player* self, InputEvent value) {
if(value.as_bool) {
self->attackInput = 2;
PlayerPushInput(self);
}
}
void PlayerOnCollision(Player* self, Collision collision) {}
void PlayerOnOverlap(Player* self, Collider* other) {}
Transform* PlayerGetTransform(Player* self) {
return &self->transform;
}
RigidBody* PlayerGetRigidBody(Player* self) {
return self->rigidbody;
}
PlayerInputFrame* PlayerInputHistory(Player* self) {
return (PlayerInputFrame*)self->inputLog.data;
}
int PlayerInputIsQuarterCircleForward(Player* self) {
// at least four inputs have to be logged for a valid quartercircle
// (three directional, one button, in that order)
if(self->inputLog.len < 4)
return 0;
PlayerInputFrame* history = PlayerInputHistory(self);
// the most recent input (assumed to be a button input)
const size_t last = self->inputLog.len-1;
// the forward direction at the assumed start of the action
const float forward = history[last-3].facing;
if(game_time() - history[last-2].time > 0.225f)
return 0;
// check if the three inputs before the most recent input are a quartercircle towards the facing direction at last-3 (three before current)
// current (history[last]) is assumed to be a button input
return veqf(history[last-3].direction, MakeVector(0.f, 1.f))
&& veqf(history[last-2].direction, MakeVector(forward, 1.f))
&& veqf(history[last-1].direction, MakeVector(forward, 0.f));
}