#include "raylib.h" #include "raymath.h" #if defined(PLATFORM_WEB) #include #endif typedef enum { O = 0, // nothing //P = 1, // player t = 2, // tree T = 3, // troll area B = 4, // bandit area T_B = 5, // troll base B_B = 6, // bandit base //G = 7, // gate (exit) } MapSymbol; typedef enum { AA_IDLE = 0, AA_CHARGE = 1, AA_ATTACK = 2, } AttackAnimState; typedef enum { P_IDLE = 0, P_MOVE = 1, P_HIT = 2, } PlayerState; typedef enum { C_IDLE = 0, C_OBSERVE = 1, C_PATROL = 2, C_FOLLOW = 3, C_AGGRO = 4, C_HIT = 5, C_DEAD = 6 } CharState; typedef enum { AI_NONE = 0, AI_IDLE = 1, AI_PATROL = 2, AI_DETECT = 3, AI_ATTACK = 4, } AIState; typedef enum { HA_IDLE = 0, HA_PLAY = 1, } HitAnimState; typedef enum { NONE = 0, PLAYER = 1, TROLL = 2, BANDIT = 3, } CharacterType; typedef enum { PT_NONE = 0, PT_ALLY = 1, PT_ATTACK = 2, } PotionType; typedef enum { PS_NONE = 0, PS_HELD = 1, PS_THROWN = 2, PS_SPREAD = 3, } PotionState; typedef enum { GS_GAME = 0, GS_UI = 1, GS_SUCCESS = 2, GS_OVER = 3, } GameplayState; typedef enum { PATROL_TOP = 0, PATROL_LEFT = 1, PATROL_DOWN = 2, PATROL_RIGHT = 3, PATROL_END = 4, } PatrolDir; typedef struct ScuffString { char *buffer; int len; int capacity; } String; struct Entity { int type; // EntityType int id; // Entity ID }; typedef struct Entity AttackedEntity; typedef struct Entity AggroEntity; typedef struct Entity FriendEntity; struct Potion { int state; int type; float radius; // radius during which potion effects last int damage; // how much damage potion does int healing; // how much healing potion does float speed_multiplier; // how much enemy speed is effected inside potion radius float t_potion_throw_start; float t_potion_spread_start; float t_potion_spread_duration; // how long does the potion spread last Vector2 throw_dir; Vector2 position_start; Vector2 position_curr; Vector2 position_target; Color tint_color_spread; Color tint_color_effect; Texture2D *sprite; } Potion; typedef struct Potion FirePotion; typedef struct Potion FriendPotion; typedef struct Player { int entity_id; int state; // PlayerState int tile_id; // MapSymbol int health; float move_speed; Vector2 position; Vector2 target_position; Vector2 move_dir; Color tint_color; Rectangle render_rect; Texture2D *sprite; // attack state bool is_attack; float throw_dist; Vector2 attack_dir; float potion_throw_speed; float potion_throw_dist; struct Potion *active_potion; FirePotion fire_potion; FriendPotion friend_potion; // // Animation // int anim_hit_state; float anim_hit_speed; double t_anim_hit_start; float t_anim_hit_duration; } Player; struct Character { bool isburning; int entity_type; // TROLL / BANDIT int entity_id; int state; // CharState int tile_id; // MapSymbol int home_tile_id; // MapSymbol int ai_state; int health; float speed_multiplier; float move_speed; float detection_threshold; float attack_threshold; // improved idle behavior PatrolDir patrol_dir; bool to_core_pos; Vector2 core_position; // this is the main position characters will stick to double t_idle_start; // time spent at core position, useful to rotate them out of positions float t_idle_duration; // max allowed duration they should be idle Vector2 position; Vector2 last_enemy_position; Vector2 move_dir; Vector2 target_position; Rectangle target_rect; Rectangle render_rect; Color tint_color_base; Color tint_color_active; Color tint_color; Texture2D* sprite; AggroEntity attackers[2]; FriendEntity friends[2]; // // Animation // // getting hit int anim_hit_state; float anim_hit_speed; double t_anim_hit_start; float t_anim_hit_duration; // getting burned double t_anim_burn_start; // attacking int anim_attack_state; float anim_attack_speed; float t_anim_charge_duration; float t_anim_attack_duration; double t_anim_charge_start; double t_anim_attack_start; }; typedef struct Character Troll; typedef struct Character Bandit; typedef struct State { int gameplay_state; // how much scaling to apply from original asset resolution float render_scale; Vector2 pixels_per_gridbox; Vector2 mouse_position; // @note: normally when we place a texture, the coordinates // are the top left coordinates. This will cause a disconnect // when moving the player with a point and click motion. // To fix that we calculate a new position so the player_coordinates // are in the center. This will make it so when the player clicks on the // ground somewhere, the character sprite will be properly centered on that point. Player player; Troll _troll; Troll trolls[5]; int troll_arr_sz; Bandit _bandit; Bandit bandits[5]; int bandit_arr_sz; Vector2 bandit_position; // player movement // @todo // direction player is being hit from, incorporate that into the gameplay code // For now, just update the player_move_dir to the player_hit_dir // Vector2 player_hit_dir; // float player_move_delta; // amount player will move // target position player will move to // 1. the player can move intentionally, in which case carry forward from player position target // 2. the player can move from feedback of being hit, NOT IMPLEMENTED YET // game sprites Texture2D *grass_sprite; Texture2D *troll_weapon_sprite; Texture2D *bandit_sprite; Texture2D *bandit_weapon_sprite; Texture2D *troll_area_floor_sprite; Texture2D *bandit_area_floor_sprite; Texture2D *troll_base_sprite; Texture2D *bandit_base_sprite; // key sprites and stuff int key_picked_count; bool key_should_render[2]; Texture2D *key_sprite; Vector2 key_positions[2]; Rectangle key_render_rects[2]; // gate stuff bool is_gate_open; Vector2 gate_position; Rectangle gate_render_rect; Texture2D *active_gate_sprite; Texture2D *gate_closed_sprite; Texture2D *gate_open_sprite; String *action_log; } State; //---------------------------------------------------------------------------------- // Global Variables Definition //---------------------------------------------------------------------------------- const int screenWidth = 1280; const int screenHeight = 1024; State mega_state = {}; // layer ground int floor_grid_map[8][8] = {{O, O, O, O, O, O, O, O}, {O, O, O, O, T, T, T, T}, {B, B, B, B, T, T, T, T}, {B, B, B, B, T, T, T, T}, {B, B, B, B, T, T, T, T}, {B, B, B, B, T, T, T, T}, {B, B, B, B, T, T, T, T}, {B, B, B, B, T, T, T, T}}; // vegetation grid int floor_cover_grid_map[8][8] = {{O, O, O, O, O, O, O, O}, {O, O, O, O, O, O, O, O}, {O, O, O, O, O, O, O, O}, {O, O, O, O, O, O, O, O}, {O, O, O, O, O, O, O, T_B}, {O, B_B, O, O, O, O, O, O}, {O, O, O, O, O, O, O, O}, {O, O, O, O, O, O, O, O}}; Vector2 grid_dims = {8, 8}; //---------------------------------------------------------------------------------- // Module functions declaration //---------------------------------------------------------------------------------- void CenterRectAroundTL(Rectangle *to_center, Vector2 position); void UpdateDrawFrame(); // Update and Draw one frame void CenterRectAroundTL(Rectangle *to_center, Vector2 position) { to_center->x = position.x - ((float)to_center->width)/2.0f; to_center->y = position.y - ((float)to_center->height)/2.0f; } int main(void) { InitWindow(screenWidth, screenHeight, "blaidville prototype"); SetTargetFPS(60); SetExitKey(0); Texture2D grass_sprite = LoadTexture("./assets/dungeon/floor/grass/grass_flowers_blue_1_new.png"); Texture2D player_sprite = LoadTexture("./assets/player/base/human_male.png"); Texture2D troll_sprite = LoadTexture("./assets/monster/hill_giant_old.png"); Texture2D bandit_sprite = LoadTexture("./assets/monster/unique/maurice_new.png"); Texture2D troll_area_floor_sprite = LoadTexture("./assets/dungeon/floor/dirt_northeast_new.png"); Texture2D bandit_area_floor_sprite = LoadTexture("./assets/dungeon/floor/dirt_east_new.png"); Texture2D bandit_base_sprite = LoadTexture("./assets/dungeon/floor/hive_3.png"); Texture2D troll_base_sprite = LoadTexture("./assets/dungeon/floor/dirt_2_old.png"); Texture2D troll_weapon_sprite = LoadTexture("./assets/item/weapon/club_old.png"); Texture2D fire_potion_sprite = LoadTexture("./assets/item/potion/ruby_old.png"); Texture2D friend_potion_sprite = LoadTexture("./assets/item/potion/emerald.png"); Texture2D key_sprite = LoadTexture("./assets/item/misc/key.png"); Texture2D gate_closed_sprite = LoadTexture("./assets/dungeon/gateways/bazaar_gone.png"); Texture2D gate_open_sprite = LoadTexture("./assets/dungeon/gateways/bazaar_portal.png"); mega_state.render_scale = 2.0f; mega_state.key_sprite = &key_sprite; mega_state.key_positions[0] = (Vector2){200, 700}; mega_state.key_positions[1] = (Vector2){1200, 600}; mega_state.key_render_rects[0] = (Rectangle){.width=key_sprite.width, .height=key_sprite.height}; mega_state.key_render_rects[1] = (Rectangle){.width=key_sprite.width, .height=key_sprite.height}; CenterRectAroundTL(&mega_state.key_render_rects[0], mega_state.key_positions[0]); CenterRectAroundTL(&mega_state.key_render_rects[1], mega_state.key_positions[1]); mega_state.key_should_render[0] = 1; mega_state.key_should_render[1] = 1; mega_state.grass_sprite = &grass_sprite; mega_state.bandit_sprite = &bandit_sprite; mega_state.gate_open_sprite = &gate_open_sprite; mega_state.gate_closed_sprite = &gate_closed_sprite; mega_state.active_gate_sprite = &gate_closed_sprite; mega_state.troll_area_floor_sprite = &troll_area_floor_sprite; mega_state.bandit_area_floor_sprite = &bandit_area_floor_sprite; mega_state.bandit_base_sprite = &bandit_base_sprite; mega_state.troll_base_sprite = &troll_base_sprite; mega_state.troll_weapon_sprite = &troll_weapon_sprite; mega_state.pixels_per_gridbox = (Vector2){screenWidth/grid_dims.x, screenHeight/grid_dims.y}; // characters // player mega_state.player = (Player){ .health = 45.0f, .position = (Vector2){50.0f, 50.0f}, .move_speed = 2.0f, .sprite = &player_sprite, .tint_color = WHITE, .render_rect = (Rectangle){ .x=0,.y=0, .width=0.5*player_sprite.width*mega_state.render_scale, .height=player_sprite.height*mega_state.render_scale }, .throw_dist = 100.0f, .fire_potion = (FirePotion){ .state=PS_NONE,.type=PT_ATTACK,.radius=40.0f,.damage=5, .speed_multiplier=0.5f, .sprite=&fire_potion_sprite, .t_potion_spread_duration = 5.0f, .tint_color_spread=(Color){200, 50, 50, 100}, .tint_color_effect=(Color){200, 50, 50, 255} }, .friend_potion = (FriendPotion){ .state=PS_HELD,.type=PT_ALLY,.radius=40.0f, .speed_multiplier=1.0f, .sprite=&friend_potion_sprite, .t_potion_spread_duration = 5.0f, .tint_color_spread=(Color){50, 200, 50, 100}, .tint_color_effect=(Color){50, 200, 50, 255} }, .potion_throw_speed = 250.0f, .potion_throw_dist = 200.0f, .anim_hit_speed = 50.0f, .t_anim_hit_duration = 0.2f, }; // troll Troll troll = { .state = C_IDLE, .entity_type = TROLL, .home_tile_id = T, .health = 50, .move_speed = 2.0f, .speed_multiplier = 1.0f, .tint_color_base = WHITE, .tint_color_active = WHITE, .tint_color = WHITE, .render_rect = (Rectangle){.x=0,.y=0,.width=troll_sprite.width*mega_state.render_scale,.height=troll_sprite.height*mega_state.render_scale}, .sprite = &troll_sprite, .t_anim_charge_duration = 1.0f, .t_anim_attack_duration = 0.4, .anim_attack_speed = 100.0f, .anim_hit_speed = 20.0f, .t_anim_hit_duration = 0.2f, .t_idle_duration = 2.0f, .to_core_pos = true, }; mega_state.troll_arr_sz = 5; mega_state.trolls[0] = troll; mega_state.trolls[0].entity_id = 0; mega_state.trolls[0].t_idle_duration = 4.0f; mega_state.trolls[0].position = (Vector2){750, 340}; mega_state.trolls[0].core_position = mega_state.trolls[0].position; mega_state.trolls[1] = troll; mega_state.trolls[1].entity_id = 1; mega_state.trolls[1].position = (Vector2){1200, 300}; mega_state.trolls[1].core_position = mega_state.trolls[1].position; mega_state.trolls[2] = troll; mega_state.trolls[2].entity_id = 2; mega_state.trolls[2].t_idle_duration = 3.5f; mega_state.trolls[2].position = (Vector2){800, 800}; mega_state.trolls[2].core_position = mega_state.trolls[2].position; mega_state.trolls[3] = troll; mega_state.trolls[3].entity_id = 3; mega_state.trolls[3].t_idle_duration = 3.0f; mega_state.trolls[3].position = (Vector2){1130, 640}; mega_state.trolls[3].core_position = mega_state.trolls[0].position; mega_state.trolls[4] = troll; mega_state.trolls[4].entity_id = 4; mega_state.trolls[4].t_idle_duration = 1.5f; mega_state.trolls[4].position = (Vector2){1000, 900}; mega_state.trolls[4].core_position = mega_state.trolls[4].position; // bandit Bandit bandit = (Bandit){ .state = C_IDLE, .ai_state = AI_NONE, .entity_type = BANDIT, .home_tile_id = B, .health = 40, .move_speed = 4.0f, .speed_multiplier = 1.0f, .tint_color_base = WHITE, .tint_color_active = WHITE, .tint_color = WHITE, .render_rect = (Rectangle){.x=0,.y=0,.width=bandit_sprite.width*mega_state.render_scale,.height=bandit_sprite.height*mega_state.render_scale}, .sprite = &bandit_sprite, .t_anim_charge_duration = 0.5f, .t_anim_attack_duration = 0.2f, .anim_attack_speed = 50.0f, .anim_hit_speed = 50.0f, .t_anim_hit_duration = 0.2f, .t_idle_duration = 2.0f, .to_core_pos = true }; mega_state.bandit_arr_sz = 5; mega_state.bandits[0] = bandit; mega_state.bandits[0].entity_id = 0; mega_state.bandits[0].position = (Vector2){264.0f, 900.0f}; mega_state.bandits[0].core_position = mega_state.bandits[0].position; mega_state.bandits[1] = bandit; mega_state.bandits[1].entity_id = 1; mega_state.bandits[1].t_idle_duration = 4.0f; mega_state.bandits[1].position = (Vector2){164.0f, 600.0f}; mega_state.bandits[1].core_position = mega_state.bandits[1].position; mega_state.bandits[2] = bandit; mega_state.bandits[2].entity_id = 2; mega_state.bandits[2].t_idle_duration = 3.0f; mega_state.bandits[2].position = (Vector2){264.0f, 400.0f}; mega_state.bandits[2].core_position = mega_state.bandits[2].position; mega_state.bandits[3] = bandit; mega_state.bandits[3].entity_id = 3; mega_state.bandits[3].t_idle_duration = 2.5f; mega_state.bandits[3].position = (Vector2){400.0f, 800.0f}; mega_state.bandits[3].core_position = mega_state.bandits[3].position; mega_state.bandits[4] = bandit; mega_state.bandits[4].entity_id = 4; mega_state.bandits[4].t_idle_duration = 1.0f; mega_state.bandits[4].position = (Vector2){400.0f, 600.0f}; mega_state.bandits[4].core_position = mega_state.bandits[4].position; // @todo: action log to describe and dictate all the major events happen char *str_buffer = (char*)MemAlloc(512*sizeof(char)); mega_state.player.active_potion = &mega_state.player.friend_potion; CenterRectAroundTL(&mega_state.trolls[0].render_rect, mega_state.trolls[0].position); CenterRectAroundTL(&mega_state.trolls[1].render_rect, mega_state.trolls[1].position); CenterRectAroundTL(&mega_state.bandits[0].render_rect, mega_state.bandits[0].position); CenterRectAroundTL(&mega_state.bandits[0].render_rect, mega_state.bandits[0].position); // no action log right now // mega_state.action_log = &(String){.buffer=str_buffer, .capacity=512, .len=0}; mega_state.gameplay_state = GS_UI; mega_state.gate_position = (Vector2){.x=500.0f, .y=16.0f}; mega_state.gate_render_rect = (Rectangle){.width=mega_state.gate_closed_sprite->width*1.5, .height=mega_state.gate_closed_sprite->height*1.5}; CenterRectAroundTL(&mega_state.gate_render_rect, mega_state.gate_position); #if defined(PLATFORM_WEB) emscripten_set_main_loop(UpdateDrawFrame, 0, 1); #else SetTargetFPS(60); while(!WindowShouldClose()) { UpdateDrawFrame(); } #endif CloseWindow(); return 0; } void DrawMapFloorTiles() { Color BlackTint = {128,128,128,255}; Texture2D grass = *mega_state.grass_sprite; float winc = (float)grass.width * mega_state.render_scale; float hinc = (float)grass.height * mega_state.render_scale; Vector2 grid_pos = {0, 0}; for (int ypos=0; yposstate == PS_THROWN) { DrawTextureEx(*mega_state.player.active_potion->sprite, mega_state.player.active_potion->position_curr, 0, 0.4*mega_state.render_scale, WHITE); } else if (mega_state.player.active_potion->state == PS_SPREAD) { Vector2 pos = mega_state.player.active_potion->position_curr; DrawCircle(pos.x, pos.y, mega_state.player.active_potion->radius, mega_state.player.active_potion->tint_color_spread); } if (mega_state.key_should_render[0] == 1) { DrawTextureEx(*mega_state.key_sprite, (Vector2){mega_state.key_positions[0].x, mega_state.key_positions[0].y}, 0, mega_state.render_scale/2.0f, WHITE); } if (mega_state.key_should_render[1] == 1) { DrawTextureEx(*mega_state.key_sprite, (Vector2){mega_state.key_positions[1].x, mega_state.key_positions[1].y}, 0, mega_state.render_scale/2.0f, WHITE); } Rectangle src_rect, dest_rect; // draw Troll // - troll weapon for (int i=0;iwidth; src_rect = (Rectangle){.x=0, .y=0, .width=-mega_state.troll_weapon_sprite->width, .height=mega_state.troll_weapon_sprite->height}; dest_rect = (Rectangle){.x=troll_weapon_centered.x, .y=troll_weapon_centered.y, .width=0.5f*mega_state.render_scale*mega_state.troll_weapon_sprite->width, .height=0.5f*mega_state.render_scale*mega_state.troll_weapon_sprite->height}; DrawTexturePro(*mega_state.troll_weapon_sprite, src_rect, dest_rect, (Vector2){0,0}, 0, RAYWHITE); // - troll sprite DrawTextureEx(*mega_state.trolls[i].sprite, (Vector2){mega_state.trolls[i].render_rect.x, mega_state.trolls[i].render_rect.y}, 0, mega_state.render_scale, mega_state.trolls[i].tint_color); // draw collision box //DrawRectangleLines(mega_state.troll.render_rect.x, mega_state.troll.render_rect.y, // mega_state.troll.render_rect.width, mega_state.troll.render_rect.height, RED); } // draw Bandit for (int i=0;iwidth*p_render_scale/2.0f) - 2.0f; player_weapon_centered.y += (12.0f); src_rect = (Rectangle){ .x=0, .y=0, .width = mega_state.player.active_potion->sprite->width, .height = mega_state.player.active_potion->sprite->height }; dest_rect = (Rectangle){.x=player_weapon_centered.x, .y=player_weapon_centered.y, .width=0.4f*mega_state.render_scale*mega_state.player.active_potion->sprite->width, .height=0.4f*mega_state.render_scale*mega_state.player.active_potion->sprite->height}; DrawTexturePro(*mega_state.player.active_potion->sprite, src_rect, dest_rect, (Vector2){0,0}, 0, RAYWHITE); Vector2 player_center = GetSpriteCenterPosition(*mega_state.player.sprite, mega_state.player.position, mega_state.render_scale); DrawTextureEx(*mega_state.player.sprite, player_center, 0, mega_state.render_scale, mega_state.player.tint_color); } int GetTileIDFromWorld(Vector2 world_position) { Vector2 grid_pos = Vector2Divide(Vector2Multiply(world_position, grid_dims), (Vector2){screenWidth, screenHeight}); int tile = floor_grid_map[(int)grid_pos.y][(int)grid_pos.x]; return tile; } bool MoveCharacter(struct Character *to_move, Vector2 position) { Vector2 new_pos = Vector2Add(to_move->position, position); int tile_at_new_pos = GetTileIDFromWorld(new_pos); int tile_at_curr_pos = GetTileIDFromWorld(to_move->position); bool char_at_home = to_move->home_tile_id == tile_at_curr_pos ? true: false; // move only if next position in home tile if (tile_at_new_pos == to_move->home_tile_id || to_move->state == C_FOLLOW || char_at_home == false) { to_move->position = Vector2Add(to_move->position, position); return true; } else { to_move->target_position = to_move->position; return false; } } AttackedEntity GetEntityToAttack(struct Character attacker) { AttackedEntity attackee = {}; // check if player can be attacked bool attacker_at_home_tile = GetTileIDFromWorld(attacker.position) == attacker.home_tile_id ? true: false; Vector2 char_position = mega_state.player.position; int char_tile_id = GetTileIDFromWorld(char_position); Rectangle char_rect = mega_state.player.render_rect; float min_dist = screenWidth; if ((attacker.attackers[0].type == PLAYER || char_tile_id == attacker.home_tile_id || attacker_at_home_tile == false) && attacker.friends[0].type != PLAYER) { float char_dist = Vector2Distance(char_position, attacker.position); if (char_dist < 200) { attackee.type = PLAYER; if (attacker.attackers[0].type == PLAYER) return attackee; } } // check if bandit can instead be attacked for (int i=0; istate = C_IDLE; troll->ai_state = AI_NONE; switch (to_attack.type) { case (PLAYER): { troll->last_enemy_position = mega_state.player.position; troll->target_position = mega_state.player.position; troll->target_rect = mega_state.player.render_rect; dist_to_target = Vector2Distance(troll->target_position, troll->position); troll->ai_state = AI_DETECT; troll->state = C_FOLLOW; } break; case (BANDIT): { troll->last_enemy_position = mega_state.bandits[to_attack.id].position; troll->target_position = mega_state.bandits[to_attack.id].position; troll->target_rect = mega_state.bandits[to_attack.id].render_rect; dist_to_target = Vector2Distance(troll->target_position, troll->position); troll->ai_state = AI_DETECT; troll->state = C_FOLLOW; } break; default: break; } if (troll->ai_state == AI_DETECT && dist_to_target < 80) { troll->ai_state = AI_ATTACK; } if (troll->ai_state >= AI_DETECT && troll->anim_attack_state != AA_ATTACK) { troll->move_dir = Vector2Normalize(Vector2Subtract(troll->target_position, troll->position)); if (dist_to_target > 10.0f) { // @note: this actually might not even be needed // move only if both are more than 10pixels apart. // it becomes jarring other wise MoveCharacter(troll, Vector2Scale(troll->move_dir, troll->speed_multiplier*troll->move_speed)); } } CenterRectAroundTL(&troll->render_rect, troll->position); if (troll->anim_attack_state == AA_IDLE && troll->ai_state == AI_ATTACK) { // start attack animation troll->t_anim_charge_start = GetTime(); troll->anim_attack_state = AA_CHARGE; } float charge_time = 0.0f; float charge_color_delta = 0.0f; if (troll->anim_attack_state == AA_CHARGE) { // get color based on time float charge_progress = (GetTime() - troll->t_anim_charge_start) / troll->t_anim_charge_duration; troll->tint_color.r = Clamp(troll->tint_color_active.r - (charge_progress*troll->tint_color_active.r), 0, 255); if (charge_progress >= 1.0f) { troll->anim_attack_state = AA_ATTACK; troll->t_anim_attack_start = GetTime(); } } else if (troll->anim_attack_state == AA_ATTACK) { // maximum distance to attack jump in float anim_length = GetTime() - troll->t_anim_attack_start; float ft = GetFrameTime(); float pixel_per_second_per_ft = ft*troll->anim_attack_speed/troll->t_anim_attack_duration; troll->position = Vector2Add(troll->position, Vector2Scale(troll->move_dir, pixel_per_second_per_ft)); troll->tint_color = (Color){255, 128, 255, 255}; // get troll center position and check for collision with player // now check if colliding with player bool is_colliding = CheckCollisionRecs(troll->target_rect, troll->render_rect); if (anim_length >= troll->t_anim_attack_duration || is_colliding) { troll->state = C_IDLE; troll->ai_state = AI_NONE; troll->anim_attack_state = AA_IDLE; troll->tint_color = troll->tint_color_active; if (is_colliding == true) { return true; // is colliding with target } } } // once troll is done moving, clear state. troll->state = C_IDLE; troll->ai_state = AI_NONE; return false; } bool BanditAttackTarget(Bandit *bandit, AttackedEntity to_attack) { float dist_to_target = 0; bandit->state = C_IDLE; bandit->ai_state = AI_NONE; switch (to_attack.type) { case (PLAYER): { bandit->last_enemy_position = mega_state.player.position; bandit->target_position = mega_state.player.position; bandit->target_rect = mega_state.player.render_rect; dist_to_target = Vector2Distance(bandit->target_position, bandit->position); bandit->ai_state = AI_DETECT; bandit->state = C_FOLLOW; } break; case (TROLL): { bandit->last_enemy_position = mega_state.trolls[to_attack.id].position; bandit->target_position = mega_state.trolls[to_attack.id].position; bandit->target_rect = mega_state.trolls[to_attack.id].render_rect; dist_to_target = Vector2Distance(bandit->target_position, bandit->position); bandit->ai_state = AI_DETECT; bandit->state = C_FOLLOW; } break; default: break; } if (bandit->ai_state == AI_DETECT && dist_to_target < 80) { bandit->ai_state = AI_ATTACK; } if (bandit->ai_state >= AI_DETECT && bandit->anim_attack_state != AA_ATTACK) { bandit->move_dir = Vector2Normalize(Vector2Subtract(bandit->target_position, bandit->position)); if (dist_to_target > 10.0f) { // @note: this actually might not even be needed // move only if both are more than 10pixels apart. // it becomes jarring other wise bool can_move = MoveCharacter(bandit, Vector2Scale(bandit->move_dir, bandit->speed_multiplier*bandit->move_speed)); } } CenterRectAroundTL(&bandit->render_rect, bandit->position); if (bandit->anim_attack_state == AA_IDLE && bandit->ai_state == AI_ATTACK) { // start attack animation bandit->t_anim_charge_start = GetTime(); bandit->anim_attack_state = AA_CHARGE; } float charge_time = 0.0f; float charge_color_delta = 0.0f; if (bandit->anim_attack_state == AA_CHARGE) { // get color based on time float charge_progress = (GetTime() - bandit->t_anim_charge_start) / bandit->t_anim_charge_duration; bandit->tint_color.r = Clamp(bandit->tint_color_active.r - (charge_progress*bandit->tint_color_active.r), 0, 255); if (charge_progress >= 1.0f) { bandit->anim_attack_state = AA_ATTACK; bandit->t_anim_attack_start = GetTime(); } } else if (bandit->anim_attack_state == AA_ATTACK) { // maximum distance to attack jump in float anim_length = GetTime() - bandit->t_anim_attack_start; float ft = GetFrameTime(); float pixel_per_second_per_ft = ft*bandit->anim_attack_speed/bandit->t_anim_attack_duration; bandit->position = Vector2Add(bandit->position, Vector2Scale(bandit->move_dir, pixel_per_second_per_ft)); bandit->tint_color = (Color){255, 128, 255, 255}; // get bandit center position and check for collision with player // now check if colliding with player bool is_colliding = CheckCollisionRecs(bandit->target_rect, bandit->render_rect); if (anim_length >= bandit->t_anim_attack_duration || is_colliding) { bandit->state = C_IDLE; bandit->ai_state = AI_NONE; bandit->anim_attack_state = AA_IDLE; bandit->tint_color = bandit->tint_color_active; if (is_colliding == true) { return true; // is colliding with target } } } bandit->state = C_IDLE; bandit->ai_state = AI_NONE; return false; } void UpdateDrawFrame(void) { // INPUT if (IsKeyPressed(KEY_ESCAPE)) { mega_state.gameplay_state = mega_state.gameplay_state == GS_UI ? GS_GAME : GS_UI; } if (mega_state.gameplay_state == GS_GAME) { mega_state.mouse_position = GetMousePosition(); if (IsKeyPressed(KEY_ONE)) { mega_state.player.active_potion = &mega_state.player.fire_potion; mega_state.player.active_potion->t_potion_throw_start = 0; mega_state.player.active_potion->t_potion_spread_start = 0; mega_state.player.active_potion->state = PS_HELD; } else if (IsKeyPressed(KEY_TWO)) { mega_state.player.active_potion = &mega_state.player.friend_potion; mega_state.player.active_potion->t_potion_throw_start = 0; mega_state.player.active_potion->t_potion_spread_start = 0; mega_state.player.active_potion->state = PS_HELD; } if (IsMouseButtonPressed(MOUSE_BUTTON_LEFT) && mega_state.player.state <= P_MOVE) { mega_state.player.state = P_MOVE; mega_state.player.target_position = mega_state.mouse_position; mega_state.player.move_dir = Vector2Normalize(Vector2Subtract(mega_state.player.target_position, mega_state.player.position)); } if (IsMouseButtonPressed(MOUSE_BUTTON_RIGHT)) { // @todo: abilities mega_state.player.active_potion->throw_dir = Vector2Normalize(Vector2Subtract(mega_state.mouse_position, mega_state.player.position)); mega_state.player.active_potion->position_start = mega_state.player.position; mega_state.player.active_potion->position_curr = mega_state.player.position; mega_state.player.active_potion->position_target = mega_state.mouse_position; mega_state.player.active_potion->state = PS_THROWN; mega_state.player.active_potion->t_potion_throw_start = GetTime(); } // PROCESSING // initialize values for (int i = 0; i < mega_state.troll_arr_sz; i++) { if (mega_state.trolls[i].health <= 0) { mega_state.trolls[i].state = C_DEAD; mega_state.trolls[i].tint_color = (Color){128, 128, 128, 128}; } mega_state.trolls[i].speed_multiplier = 1.0f; } for (int i = 0; i < mega_state.bandit_arr_sz; i++) { if (mega_state.bandits[i].health <= 0) { mega_state.bandits[i].state = C_DEAD; mega_state.bandits[i].tint_color = (Color){128, 128, 128, 128}; } mega_state.bandits[i].speed_multiplier = 1.0f; } if (mega_state.player.health <= 0) { mega_state.gameplay_state = GS_OVER; } switch (mega_state.player.state) { case (P_MOVE): { // MOVE PLAYER Vector2 next_position = Vector2Add(mega_state.player.position, Vector2Scale(mega_state.player.move_dir, mega_state.player.move_speed)); // checke player collisions bool is_colliding_gate = CheckCollisionRecs(mega_state.player.render_rect, mega_state.gate_render_rect); if (is_colliding_gate) { if (mega_state.key_picked_count == 2) { mega_state.is_gate_open = true; mega_state.active_gate_sprite = mega_state.gate_open_sprite; mega_state.gameplay_state = GS_SUCCESS; } } mega_state.player.position = next_position; if (Vector2Distance(mega_state.player.position, mega_state.player.target_position) <= 5.0f) { mega_state.player.state = P_IDLE; } // calculate center positions CenterRectAroundTL(&mega_state.player.render_rect, mega_state.player.position); mega_state.player.tile_id = GetTileIDFromWorld(mega_state.player.position); // check if a key can be picked up for (int i=0;i<2;i++) { bool is_colliding = CheckCollisionRecs(mega_state.player.render_rect, mega_state.key_render_rects[i]); if (is_colliding && mega_state.key_should_render[i] == 1) { mega_state.key_picked_count += 1; mega_state.key_should_render[i] = 0; } } } break; case (P_HIT): { float anim_length = GetTime() - mega_state.player.t_anim_hit_start; float ft = GetFrameTime(); float pixel_per_second_per_ft = ft*mega_state.player.anim_hit_speed/mega_state.player.t_anim_hit_duration; mega_state.player.position = Vector2Add(mega_state.player.position, Vector2Scale(mega_state.player.move_dir, pixel_per_second_per_ft)); mega_state.player.tint_color = (Color){255, 128, 128, 255}; if (anim_length >= mega_state.player.t_anim_hit_duration) { mega_state.player.state = P_IDLE; mega_state.player.anim_hit_state = HA_IDLE; mega_state.player.tint_color = (Color){255, 255, 255, 255}; } // calculate center positions CenterRectAroundTL(&mega_state.player.render_rect, mega_state.player.position); mega_state.player.tile_id = GetTileIDFromWorld(mega_state.player.position); } break; default: {}break; } switch (mega_state.player.active_potion->state) { case (PS_THROWN): { // play potion throw animation float throw_speed = GetFrameTime() * mega_state.player.potion_throw_speed; float dist_thrown = Vector2Distance(mega_state.player.active_potion->position_curr, mega_state.player.active_potion->position_start); mega_state.player.active_potion->position_curr = Vector2Add(mega_state.player.active_potion->position_curr, Vector2Scale(mega_state.player.active_potion->throw_dir, throw_speed)); float dist_to_target = Vector2Distance(mega_state.player.active_potion->position_curr, mega_state.player.active_potion->position_target); if (dist_thrown > mega_state.player.potion_throw_dist || dist_to_target <= 5.0f) { // change potion state to potion explode mega_state.player.active_potion->state = PS_SPREAD; mega_state.player.active_potion->t_potion_spread_start = GetTime(); } } break; case (PS_SPREAD): { float spread_duration = GetTime() - mega_state.player.active_potion->t_potion_spread_start; if (spread_duration >= mega_state.player.active_potion->t_potion_spread_duration) { mega_state.player.active_potion->state = PS_HELD; } // loop through enemies to check if any are being burned // check troll for (int i=0;istate == C_DEAD) continue; float dist_to_enemy = Vector2Distance(troll->position, mega_state.player.active_potion->position_curr); if (dist_to_enemy <= mega_state.player.active_potion->radius) { if (mega_state.player.active_potion->type == PT_ATTACK) { // mark troll state as being burned if (troll->isburning == false) { troll->isburning = true; troll->t_anim_burn_start = GetTime(); troll->attackers[0].type = PLAYER; troll->attackers[0].id = 0; } } else if (mega_state.player.active_potion->type == PT_ALLY) { troll->friends[0] = (FriendEntity){.type=PLAYER, .id=0}; } troll->tint_color_active = mega_state.player.active_potion->tint_color_effect; troll->tint_color = troll->tint_color_active; troll->speed_multiplier = mega_state.player.active_potion->speed_multiplier; } else { troll->speed_multiplier = 1.0f; } if (mega_state.player.active_potion->state != PS_SPREAD) { troll->tint_color_active = troll->tint_color_base; troll->tint_color = troll->tint_color_active; troll->isburning = false; // remove player/entity as friend troll->friends[0].type = 0; troll->friends[0].id = 0; } } // check bandit for (int i=0;istate == C_DEAD) continue; float dist_to_enemy = Vector2Distance(bandit->position, mega_state.player.active_potion->position_curr); if (dist_to_enemy <= mega_state.player.active_potion->radius) { if (mega_state.player.active_potion->type == PT_ATTACK) { // mark bandit state as being burned if (bandit->isburning == false) { bandit->isburning = true; bandit->t_anim_burn_start = GetTime(); bandit->attackers[0].type = PLAYER; bandit->attackers[0].id = 0; } } else if (mega_state.player.active_potion->type == PT_ALLY) { bandit->friends[0] = (FriendEntity){.type=PLAYER, .id=0}; } bandit->tint_color_active = mega_state.player.active_potion->tint_color_effect; bandit->tint_color = bandit->tint_color_active; bandit->speed_multiplier = mega_state.player.active_potion->speed_multiplier; } else { bandit->speed_multiplier = 1.0f; } if (mega_state.player.active_potion->state != PS_SPREAD) { bandit->tint_color_active = bandit->tint_color_base; bandit->tint_color = bandit->tint_color_active; bandit->isburning = false; // remove player/entity as friend bandit->friends[0].type = 0; bandit->friends[0].id = 0; } } } break; default: break; } // ================= TROLL DETECTION LOOP AND STUFF ======================= for (int i=0;istate == C_HIT) { float anim_length = GetTime() - troll->t_anim_hit_start; float ft = GetFrameTime(); float pixel_per_second_per_ft = ft*troll->anim_hit_speed/troll->t_anim_hit_duration; troll->position = Vector2Add(troll->position, Vector2Scale(troll->move_dir, pixel_per_second_per_ft)); troll->tint_color = (Color){255, 128, 128, 255}; if (anim_length >= troll->t_anim_hit_duration) { troll->state = C_IDLE; troll->anim_hit_state = HA_IDLE; troll->tint_color = (Color){255, 255, 255, 255}; } // calculate center positions CenterRectAroundTL(&troll->render_rect, troll->position); troll->tile_id = GetTileIDFromWorld(troll->position); } else if (troll->state != C_DEAD) { if (troll->isburning == true) { int anim_length_ms = 1000*(int)(GetTime() - troll->t_anim_burn_start); if (anim_length_ms%500 == 0 && anim_length_ms > 0) { // decrement health every 500ms troll->health -= 8; troll->t_anim_burn_start = GetTime(); } } troll->speed_multiplier *= troll->anim_attack_state == AA_CHARGE ? 0.2f : 1.0f; AttackedEntity tentity = GetEntityToAttack(*troll); if (tentity.type > NONE || troll->anim_attack_state >= AA_CHARGE) { bool entity_hit = TrollAttackTarget(troll, tentity); if (entity_hit) { switch (tentity.type) { case (PLAYER): { mega_state.player.anim_hit_state = HA_PLAY; mega_state.player.t_anim_hit_start = GetTime(); mega_state.player.move_dir = troll->move_dir; mega_state.player.state = P_HIT; // disable walk check, stuff been thrown off mega_state.player.health -= 8; } break; case (TROLL): { // trigger troll get hit animation } break; case (BANDIT): { // trigger bandit get hit animation AggroEntity attacker = {.type=TROLL, .id=0}; mega_state.bandits[tentity.id].attackers[0] = attacker; mega_state.bandits[tentity.id].anim_hit_state = HA_PLAY; mega_state.bandits[tentity.id].t_anim_hit_start = GetTime(); mega_state.bandits[tentity.id].move_dir = troll->move_dir; mega_state.bandits[tentity.id].state = C_HIT; mega_state.bandits[tentity.id].health -= 10; } break; default: // do nothing } } } else { // get entity to follow FriendEntity follow = {}; if (troll->friends[0].type == PLAYER) { float char_dist = Vector2Distance(mega_state.player.position, troll->position); if (char_dist < 200) { follow.type = PLAYER; } } if (troll->state == C_IDLE && troll->ai_state == AI_NONE) { troll->t_idle_start = GetTime(); troll->move_dir = Vector2Zero(); troll->ai_state = AI_IDLE; troll->to_core_pos = troll->to_core_pos == true ? false: true; } double idle_time = GetTime() - troll->t_idle_start; if (idle_time >= troll->t_idle_duration && troll->ai_state == AI_IDLE) { if (troll->to_core_pos == false) { // calculate target position from patrol direction float max_dist = 200.0f; Vector2 move_dir; switch(troll->patrol_dir) { case (PATROL_TOP): { move_dir = (Vector2){.x=0.0f, .y=-1.0f}; } break; case (PATROL_LEFT): { move_dir = (Vector2){.x=-1.0f, .y=0.0f}; } break; case (PATROL_DOWN): { move_dir = (Vector2){.x=0.0f, .y=1.0f}; } break; case (PATROL_RIGHT): { move_dir = (Vector2){.x=1.0f, .y=0.0f}; } break; }; troll->move_dir = move_dir; troll->target_position = Vector2Add(troll->position, Vector2Scale(troll->move_dir, max_dist)); } else { troll->target_position = troll->core_position; troll->move_dir = Vector2Normalize(Vector2Subtract(troll->target_position, troll->position)); } troll->state = C_PATROL; troll->ai_state = AI_PATROL; } // follow player / friend if (follow.type == PLAYER) { troll->state = C_FOLLOW; troll->ai_state = AI_NONE; troll->target_position = mega_state.player.position; troll->move_dir = Vector2Normalize(Vector2Subtract(troll->target_position, troll->position)); } float dist_to_target = Vector2Distance(troll->target_position, troll->position); if (dist_to_target > 50.0f) { MoveCharacter(troll, Vector2Scale(troll->move_dir, troll->move_speed)); } else if (troll->state == C_PATROL && troll->ai_state == AI_PATROL) { // we have reached target position // in case we were patrolling, increment the direction to move to // @note: maybe do this when first changing state from idle after character has been idle for hours if (troll->to_core_pos == false) { troll->patrol_dir = (troll->patrol_dir + 1)%PATROL_END; } troll->state = C_IDLE; troll->ai_state = AI_NONE; } CenterRectAroundTL(&troll->render_rect, troll->position); } } } // ========== BANDIT DETECTION AND STUFF for (int i=0;istate == C_HIT) { float anim_length = GetTime() - bandit->t_anim_hit_start; float ft = GetFrameTime(); float pixel_per_second_per_ft = ft*bandit->anim_hit_speed/bandit->t_anim_hit_duration; bandit->position = Vector2Add(bandit->position, Vector2Scale(bandit->move_dir, pixel_per_second_per_ft)); bandit->tint_color = (Color){255, 128, 128, 255}; if (anim_length >= bandit->t_anim_hit_duration) { bandit->state = P_IDLE; bandit->anim_hit_state = HA_IDLE; bandit->tint_color = (Color){255, 255, 255, 255}; } // calculate center positions CenterRectAroundTL(&bandit->render_rect, bandit->position); bandit->tile_id = GetTileIDFromWorld(bandit->position); } else if (bandit->state != C_DEAD) { if (bandit->isburning == true) { int anim_length_ms = 1000*(int)(GetTime() - bandit->t_anim_burn_start); if (anim_length_ms%500 == 0 && anim_length_ms > 0) { // decrement health every 500ms bandit->health -= 8; bandit->t_anim_burn_start = GetTime(); } } bandit->speed_multiplier *= bandit->anim_attack_state == AA_CHARGE ? 0.2f : 1.0f; AttackedEntity tentity = GetEntityToAttack(*bandit); if (tentity.type > NONE || bandit->anim_attack_state >= AA_CHARGE) { bool entity_hit = BanditAttackTarget(bandit, tentity); if (tentity.type > NONE) { if (entity_hit) { switch (tentity.type) { case (PLAYER): { mega_state.player.anim_hit_state = HA_PLAY; mega_state.player.t_anim_hit_start = GetTime(); mega_state.player.move_dir = bandit->move_dir; mega_state.player.state = P_HIT; // disable walk check, stuff been thrown off mega_state.player.health -= 5; } break; case (TROLL): { // trigger troll get hit animation AggroEntity attacker = {.type=BANDIT, .id=0}; mega_state.trolls[tentity.id].attackers[0] = attacker; mega_state.trolls[tentity.id].anim_hit_state = HA_PLAY; mega_state.trolls[tentity.id].t_anim_hit_start = GetTime(); mega_state.trolls[tentity.id].move_dir = bandit->move_dir; mega_state.trolls[tentity.id].state = C_HIT; mega_state.trolls[tentity.id].health -= 5; } break; case (BANDIT): { // trigger bandit get hit animation } break; default: // do nothing } } } } else { if (bandit->state == C_IDLE && bandit->ai_state == AI_NONE) { bandit->t_idle_start = GetTime(); bandit->move_dir = Vector2Zero(); bandit->ai_state = AI_IDLE; bandit->to_core_pos = bandit->to_core_pos == true ? false: true; } double idle_time = GetTime() - bandit->t_idle_start; if (idle_time >= bandit->t_idle_duration && bandit->ai_state == AI_IDLE) { if (bandit->to_core_pos == false) { // calculate target position from patrol direction float max_dist = 200.0f; Vector2 move_dir; switch(bandit->patrol_dir) { case (PATROL_TOP): { move_dir = (Vector2){.x=0.0f, .y=-1.0f}; } break; case (PATROL_LEFT): { move_dir = (Vector2){.x=-1.0f, .y=0.0f}; } break; case (PATROL_DOWN): { move_dir = (Vector2){.x=0.0f, .y=1.0f}; } break; case (PATROL_RIGHT): { move_dir = (Vector2){.x=1.0f, .y=0.0f}; } break; }; bandit->move_dir = move_dir; bandit->target_position = Vector2Add(bandit->position, Vector2Scale(bandit->move_dir, max_dist)); } else { bandit->target_position = bandit->core_position; bandit->move_dir = Vector2Normalize(Vector2Subtract(bandit->target_position, bandit->position)); } bandit->state = C_PATROL; bandit->ai_state = AI_PATROL; } // get entity to follow FriendEntity follow = {}; if (bandit->friends[0].type == PLAYER) { float char_dist = Vector2Distance(mega_state.player.position, bandit->position); if (char_dist < 200) { follow.type = PLAYER; } } // follow player / friend if (follow.type == PLAYER) { bandit->target_position = mega_state.player.position; bandit->state = C_FOLLOW; bandit->ai_state = AI_NONE; bandit->move_dir = Vector2Normalize(Vector2Subtract(bandit->target_position, bandit->position)); } float dist_to_target = Vector2Distance(bandit->target_position, bandit->position); if (dist_to_target > 50.0f) { MoveCharacter(bandit, Vector2Scale(bandit->move_dir, bandit->move_speed)); } else if (bandit->state == C_PATROL && bandit->ai_state == AI_PATROL) { if (bandit->to_core_pos == false) { bandit->patrol_dir = (bandit->patrol_dir + 1)%PATROL_END; } bandit->state = C_IDLE; bandit->ai_state = AI_NONE; } CenterRectAroundTL(&bandit->render_rect, bandit->position); } } } // RENDERING BeginDrawing(); ClearBackground(RAYWHITE); DrawMapFloorTiles(); DrawFloorCoverTiles(); DrawCharacters(); // do text rendering at the end // draw mouse position for debugging if (mega_state.player.state == P_HIT) { DrawText("player hit", 500, 20, 20, GREEN); } DrawText(TextFormat("Keys: %d/2", mega_state.key_picked_count), 1100, 20, 20, GREEN); EndDrawing(); } else if (mega_state.gameplay_state == GS_UI) { BeginDrawing(); ClearBackground(BLACK); DrawText("Blaidville", screenWidth/2 - 100, 40, 28, WHITE); DrawText("Instructions", screenWidth/2 - 140, 100, 22, WHITE); DrawText("Find keys to unlock the gate", screenWidth/2 - 140, 140, 18, WHITE); DrawText("Unlock the gate to escape", screenWidth/2 - 140, 160, 18, WHITE); DrawText("Controls", screenWidth/2 - 140, 220, 22, WHITE); DrawText("1 -> Potion 1", screenWidth/2 - 140, 260, 18, WHITE); DrawText("2 -> Potion 2", screenWidth/2 - 140, 280, 18, WHITE); DrawText("right mouse click -> throw potion", screenWidth/2 - 140, 300, 18, WHITE); DrawText("left mouse click -> move", screenWidth/2 - 140, 320, 18, WHITE); DrawText("Escape to play or pause the game", screenWidth/2 - 140, 340, 18, WHITE); DrawText("Restart the game to reset state", screenWidth/2 - 140, 360, 18, WHITE); EndDrawing(); } else if (mega_state.gameplay_state == GS_SUCCESS) { BeginDrawing(); ClearBackground(BLACK); DrawText("Objective Complete", screenWidth/2 - 100, 40, 28, GREEN); EndDrawing(); } else if (mega_state.gameplay_state == GS_OVER) { BeginDrawing(); ClearBackground(BLACK); DrawText("You Died", screenWidth/2 - 100, 40, 28, RED); EndDrawing(); } }