diff --git a/CMakeLists.txt b/CMakeLists.txt index 12477c1..f85c045 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -49,6 +49,7 @@ set(ASSETS assets-cg/skills/swing_right.png assets-cg/skills/swing_down.png assets-cg/skills/swing_left.png + assets-cg/skills/hit.png # Enemies: Slime assets-cg/enemies/slime_idle_down.png assets-cg/enemies/slime_death.png @@ -56,7 +57,8 @@ set(ASSETS assets-cg/enemies/bat_idle_down.png assets-cg/enemies/bat_death.png # Misc - assets-cg/font_damage.png + assets-cg/font_damage_red.png + assets-cg/font_damage_white.png ) fxconv_declare_assets(${ASSETS} ${ASSETS} WITH_METADATA) diff --git a/assets-cg/font_damage_red.png b/assets-cg/font_damage_red.png new file mode 100644 index 0000000..38f0d4c Binary files /dev/null and b/assets-cg/font_damage_red.png differ diff --git a/assets-cg/font_damage.png b/assets-cg/font_damage_white.png similarity index 100% rename from assets-cg/font_damage.png rename to assets-cg/font_damage_white.png diff --git a/assets-cg/skills/fxconv-metadata.txt b/assets-cg/skills/fxconv-metadata.txt index 56cf6f3..579249b 100644 --- a/assets-cg/skills/fxconv-metadata.txt +++ b/assets-cg/skills/fxconv-metadata.txt @@ -6,3 +6,6 @@ swing_*.png: frame_duration: 30, 60, 30, 30, 30, 60, 30 + +hit.png: + frame_duration: 60, 90, 90, 60, 90 diff --git a/assets-cg/skills/hit.aseprite b/assets-cg/skills/hit.aseprite new file mode 100644 index 0000000..b914abe Binary files /dev/null and b/assets-cg/skills/hit.aseprite differ diff --git a/assets-cg/skills/hit.png b/assets-cg/skills/hit.png new file mode 100644 index 0000000..bc22dfd Binary files /dev/null and b/assets-cg/skills/hit.png differ diff --git a/src/anim.h b/src/anim.h index 6455b7f..6e849d4 100644 --- a/src/anim.h +++ b/src/anim.h @@ -52,6 +52,7 @@ extern anim_frame_t anim_slime_idle_down[]; extern anim_frame_t anim_slime_death[]; extern anim_frame_t anim_bat_idle_down[]; extern anim_frame_t anim_bat_death[]; +extern anim_frame_t anim_hit[]; /* Directional animations. */ extern anim_frame_t *anim_player_idle[4]; diff --git a/src/enemies.c b/src/enemies.c index 5117246..6497633 100644 --- a/src/enemies.c +++ b/src/enemies.c @@ -90,6 +90,7 @@ entity_t *enemy_spawn(int enemy_id, int level) e->movement.facing = DOWN; e->movement.dash = 0; e->movement.dash_facing = DOWN; + e->movement.dash_particle_cooldown = fix(0); e->movement_params = &data->movement_params; entity_set_anim(e, data->anim_idle); diff --git a/src/entities.c b/src/entities.c index f165b3c..338609b 100644 --- a/src/entities.c +++ b/src/entities.c @@ -2,6 +2,7 @@ #include "game.h" #include "enemies.h" #include +#include //--- // Entities @@ -36,6 +37,30 @@ void entity_dash(entity_t *e, int direction) move->dash = params->dash_duration; move->dash_facing = direction; + move->dash_particle_cooldown = fix(0); +} + +bool entity_dashing(entity_t const *e) +{ + entity_movement_t const *move = &e->movement; + entity_movement_params_t const *params = e->movement_params; + + /* True only if the direction of the dash movement is preserved */ + if(move->facing != move->dash_facing) + return false; + + /* True during initial propulsion */ + if(move->dash > 0) + return true; + + /* Also true as long as over-speed is maintained */ + fixed_t cur_v2 = fmul(move->x, move->x) + fmul(move->y, move->y); + fixed_t max_v2 = fmul(params->max_speed, params->max_speed); + + if(move->dash < 0 && cur_v2 > max_v2) + return true; + + return false; } entity_movement_t entity_move(entity_t *e, fpoint_t direction, fixed_t dt) @@ -162,15 +187,24 @@ static effect_area_record_t *effect_record(effect_area_t *ea, entity_t *e) return NULL; } -static bool slash_apply(game_t *game, effect_area_t *ea, entity_t *e) +static bool slash_hit_apply(game_t *game, effect_area_t *ea, entity_t *e) { - if(e == ea->origin || e->HP == 0) return false; + /* No friendly fire */ + bool origin_is_monster = (ea->origin->identity != 0); + bool target_is_monster = (e->identity != 0); + if(origin_is_monster == target_is_monster || e->HP == 0) return false; + + /* Dash invincibility */ + if(entity_dashing(e)) return false; /* Inflict damage */ int damage = entity_damage(e, ea->data.slash.strength); /* Knockback */ fixed_t r = (rand() & (fix(1)/4-1)) + fix(7)/8; + /* Half knockback against players */ + if(e->identity == 0) r /= 2; + fpoint_t dir = fdir(ea->data.slash.dir); e->movement.vx += fmul(dir.x, fmul(r, KNOCKBACK_SPEED)); e->movement.vy += fmul(dir.y, fmul(r, KNOCKBACK_SPEED)); @@ -181,6 +215,7 @@ static bool slash_apply(game_t *game, effect_area_t *ea, entity_t *e) p->particle.age = 0; p->particle.pos = (fpoint_t){ e->movement.x, e->movement.y - fix(1)/2 }; p->damage = damage; + p->color = (e->identity == 0) ? C_RED : C_WHITE; game_add_particle(game, &p->particle); return true; @@ -196,9 +231,9 @@ void effect_area_apply(game_t *game, effect_area_t *ea, entity_t *e) if(rec && ea->lifetime > rec->lifetime - ea->repeat_delay) return; if(ea->type == EFFECT_HIT) - was_hit = false; + was_hit = slash_hit_apply(game, ea, e); else if(ea->type == EFFECT_SLASH) - was_hit = slash_apply(game, ea, e); + was_hit = slash_hit_apply(game, ea, e); if(!was_hit) return; diff --git a/src/entities.h b/src/entities.h index 39f5079..597b1fd 100644 --- a/src/entities.h +++ b/src/entities.h @@ -39,6 +39,8 @@ typedef struct { uint8_t facing; /* Dash direction */ uint8_t dash_facing; + /* Cooldown until next dash particle */ + fixed_t dash_particle_cooldown; } entity_movement_t; @@ -86,6 +88,9 @@ entity_movement_t entity_move(entity_t *e, fpoint_t direction, fixed_t dt); /* Start dashing in the set direction */ void entity_dash(entity_t *e, int direction); +/* Check if the entity is currently dashing */ +bool entity_dashing(entity_t const *e); + /* Set entity animation */ void entity_set_normal_anim(entity_t *e, anim_frame_t *frame); void entity_set_directional_anim(entity_t *e, anim_frame_t *frame_dirs[]); diff --git a/src/game.c b/src/game.c index 6dcfd41..5a73e90 100644 --- a/src/game.c +++ b/src/game.c @@ -28,6 +28,7 @@ bool game_load(game_t *g, level_t *level) g->time_total = fix(0); g->time_victory = fix(0); + g->time_defeat = fix(0); g->entities = NULL; g->entity_count = 0; @@ -77,6 +78,12 @@ static void game_remove_entity(game_t *g, int i) { if(i < 0 || i >= g->entity_count) return; + entity_t *e = g->entities[i]; + if(e->current_attack) { + /* Kill the effect area */ + e->current_attack->lifetime = fix(0); + } + free(g->entities[i]); g->entities[i] = g->entities[--g->entity_count]; } @@ -158,7 +165,44 @@ static void game_remove_particle(game_t *g, int i) } //--- -// Update functions +// Interacting with game elements +//--- + +void game_try_move_entity(game_t *g, entity_t *e, + entity_movement_t const * next) +{ + entity_movement_t *m = &e->movement; + frect_t hitbox = rect_translate(e->hitbox, (fpoint_t){ next->x, next->y }); + + if(!map_collides(&g->map, hitbox)) { + /* Movement is allowed */ + fixed_t dx = next->x - m->x; + fixed_t dy = next->y - m->y; + + /* Update attached attack animation */ + if(next->facing != m->facing) { + e->attack_follows_movement = false; + } + if(e->current_attack && e->attack_follows_movement) { + e->current_attack->anchor.x += dx; + e->current_attack->anchor.y += dy; + } + + *m = *next; + } + else { + /* Movement is denied. Halve speed so that high-speed movement doesn't + halt at a large distance from a wall. */ + m->facing = next->facing; + m->vx = next->vx / 2; + m->vy = next->vy / 2; + m->dash = next->dash; + m->dash_particle_cooldown = next->dash_particle_cooldown; + } +} + +//--- +// Per-frame update functions //--- void game_update_animations(game_t *g, fixed_t dt) @@ -201,6 +245,29 @@ void game_update_particles(game_t *g, fixed_t dt) if(remove) game_remove_particle(g, i); else i++; } + + /* Spawn dash particles */ +#if 0 + for(int i = 0; i < g->entity_count; i++) { + entity_t *e = g->entities[i]; + + if(entity_dashing(e)) { + e->movement.dash_particle_cooldown -= dt; + + if(e->movement.dash_particle_cooldown < 0) { + particle_dash_t *p = malloc(sizeof *p); + p->particle.type = PARTICLE_DASH; + p->particle.age = 0; + p->particle.pos = entity_pos(e); + extern anim_frame_t anim_player_dash_trail_right[]; + p->frame = e->anim.frame; + game_add_particle(g, &p->particle); + + e->movement.dash_particle_cooldown = fix(25)/100; + } + } + } +#endif } /* Compare the y values of two entities. */ diff --git a/src/game.h b/src/game.h index 3c2259e..ee374b1 100644 --- a/src/game.h +++ b/src/game.h @@ -19,8 +19,9 @@ typedef struct game { camera_t camera; /* Time played */ fixed_t time_total; - /* Time when victory was reached */ + /* Time when victory was reached or defeat was dealt */ fixed_t time_victory; + fixed_t time_defeat; /* List of entities */ entity_t **entities; int entity_count; @@ -43,6 +44,10 @@ bool game_load(game_t *g, level_t *level); /* Free resources allocated for the level. */ void game_unload(game_t *g); +//--- +// Adding dynamic game elements +//--- + /* Add an entity to the game (takes ownership; e will be freed). */ void game_add_entity(game_t *g, entity_t *e); @@ -52,6 +57,23 @@ void game_add_effect_area(game_t *g, effect_area_t *ea); /* Add a particle to the game (takes ownership; p will be freed) */ void game_add_particle(game_t *g, particle_t *p); +//--- +// Interacting with game elements +//--- + +/* Try to move an entity at the specified next-frame movement data. The data is + applied if valid (no collisions). Otherwise the entity does not move and + only some data is updated. The data is obtained by entity_move() or related + functions. */ +void game_try_move_entity(game_t *g, entity_t *e, + entity_movement_t const *next_movement); + + + +//--- +// Per-frame update functions +//--- + /* Update all entities' and effect areas' animations. */ void game_update_animations(game_t *g, fixed_t dt); diff --git a/src/main.c b/src/main.c index aae0335..6a1dadf 100644 --- a/src/main.c +++ b/src/main.c @@ -93,6 +93,7 @@ int main(void) entity_t *player = game.entities[5]; game.player = player; + player->HP += 100; player->movement_params = &emp_player; player->identity = 0; player->hitbox = (frect_t){ @@ -133,8 +134,10 @@ int main(void) if(game.time_victory != 0) dprint(1, 1, C_WHITE, "Victory in %.1f s!", f2double(game.time_victory)); - else dprint(1, 1, C_WHITE, "HP:%d ATK:%d DEF:%d", - player->HP, player->ATK, player->DEF); + else if(game.time_defeat != 0) dprint(1, 1, C_WHITE, "Defeat! :("); + else dprint(1, 1, C_WHITE, "HP:%d ATK:%d DEF:%d dpc:%.1f", + player->HP, player->ATK, player->DEF, + f2double(player->movement.dash_particle_cooldown)); /* Developer/tweaking menu */ if(debug.show_vars) { @@ -335,53 +338,30 @@ int main(void) camera_move(c, 0, fmul(dt, vy)); /* Player movement */ - int dir = -1; - if(keydown(KEY_UP)) dir = UP; - if(keydown(KEY_DOWN)) dir = DOWN; - if(keydown(KEY_LEFT)) dir = LEFT; - if(keydown(KEY_RIGHT)) dir = RIGHT; + if(player->HP > 0) { + int dir = -1; + if(keydown(KEY_UP)) dir = UP; + if(keydown(KEY_DOWN)) dir = DOWN; + if(keydown(KEY_LEFT)) dir = LEFT; + if(keydown(KEY_RIGHT)) dir = RIGHT; - if(keydown(KEY_F1) && !keydown(KEY_VARS)) { - int dash_dir = (dir >= 0) ? dir : player->movement.facing; - entity_dash(player, dash_dir); - } - entity_movement_t next = entity_move4(player, dir, dt); - - frect_t player_hitbox = rect_translate(player->hitbox, - (fpoint_t){ next.x, next.y }); - - bool set_anim = (player->movement.facing != next.facing); - - if(!map_collides(m, player_hitbox)) { - fixed_t dx = next.x - player->movement.x; - fixed_t dy = next.y - player->movement.y; - - if(next.facing != player->movement.facing) { - player->attack_follows_movement = false; + if(keydown(KEY_F1) && !keydown(KEY_VARS)) { + int dash_dir = (dir >= 0) ? dir : player->movement.facing; + entity_dash(player, dash_dir); } + entity_movement_t next = entity_move4(player, dir, dt); - if(player->current_attack && player->attack_follows_movement) { - player->current_attack->anchor.x += dx; - player->current_attack->anchor.y += dy; - } - - player->movement = next; + bool set_anim = (player->movement.facing != next.facing); + game_try_move_entity(&game, player, &next); + if(set_anim) entity_set_anim(player, anim_player_idle); } - else { - player->movement.facing = next.facing; - player->movement.vx = next.vx / 2; - player->movement.vy = next.vy / 2; - player->movement.dash = next.dash; - } - - if(set_anim) entity_set_anim(player, anim_player_idle); /* Directions to reach the player from anywhere on the grid */ pfg_all2one_free(&game.paths_to_player); game.paths_to_player = pfg_bfs(m, point_f2i(entity_pos(player))); - /* Enemy movement */ - for(int i = 0; i < game.entity_count; i++) { + /* Enemy AI */ + if(player->HP > 0) for(int i = 0; i < game.entity_count; i++) { entity_t *e = game.entities[i]; if(e == player || e->HP == 0) continue; @@ -389,7 +369,9 @@ int main(void) fpoint_t direction = { 0, 0 }; fpoint_t pos = entity_pos(e); - if(dist2(pos, entity_pos(player)) > fix(1)) { + bool in_range = dist2(pos, entity_pos(player)) <= fix(1); + + if(!in_range) { pfg_path_t path = pfg_bfs_inwards(&game.paths_to_player, point_f2i(pos)); if(path.points) { @@ -405,21 +387,39 @@ int main(void) } entity_movement_t next = entity_move(e, direction, dt); - frect_t e_hitbox = rect_translate(e->hitbox, - (fpoint_t){ next.x, next.y }); + game_try_move_entity(&game, e, &next); - if(!map_collides(m, e_hitbox)) { - e->movement = next; - } - else { - e->movement.facing = next.facing; - e->movement.vx = fix(0); - e->movement.vy = fix(0); + if(in_range && !e->current_attack) { + /* Attack */ + frect_t hitbox = { + -fix(4)/16, fix(3)/16, -fix(4)/16, fix(3)/16, + }; + hitbox = rect_rotate(hitbox, UP, e->movement.facing); + fpoint_t dir = fdir(e->movement.facing); + + fpoint_t anchor = rect_center(entity_sprite(e)); + anchor.x += fmul(fix(3)/4, dir.x); + anchor.y += fmul(fix(3)/4, dir.y); + + effect_area_t *area = effect_area_new(EFFECT_HIT); + area->sprite = hitbox; + area->anchor = anchor; + area->lifetime = anim_duration(anim_hit); + area->repeat_delay = 0; + area->origin = e; + area->data.slash.strength = e->ATK; + area->data.slash.dir = e->movement.facing; + effect_area_set_anim(area, anim_hit); + game_add_effect_area(&game, area); + +// entity_set_anim(player, anim_player_attack); + e->current_attack = area; + e->attack_follows_movement = true; } } - /* Attack */ - if(attack && !player->current_attack) { + /* Player attack */ + if(player->HP > 0 && attack && !player->current_attack) { frect_t hitbox = { -fix(5)/8, fix(5)/8, -fix(1)/4, fix(1)/4, }; @@ -446,14 +446,17 @@ int main(void) player->attack_follows_movement = true; } - game_update_effect_areas(&game, dt); + /* Remove dead entities first as it will kill their attack areas */ game_remove_dead_entities(&game); + game_update_effect_areas(&game, dt); game_sort_entities(&game); game_update_particles(&game, dt); game.time_total += dt; - if(game.time_victory == 0 && game.entity_count == 1) + if(game.time_defeat == 0 && player->HP == 0) + game.time_defeat = game.time_total; + if(game.time_victory == 0 && player->HP > 0 && game.entity_count == 1) game.time_victory = game.time_total; /* Visual pathfinding debug */ diff --git a/src/particles.c b/src/particles.c index 70942b0..89cf672 100644 --- a/src/particles.c +++ b/src/particles.c @@ -10,9 +10,14 @@ static bool damage_update(particle_damage_t *p, GUNUSED fixed_t dt) static void damage_render(int x, int y, particle_damage_t *p) { - extern bopti_image_t img_font_damage; - int char_w = img_font_damage.width / 10; - int char_h = img_font_damage.height; + extern bopti_image_t img_font_damage_white; + extern bopti_image_t img_font_damage_red; + + bopti_image_t *img = (p->color == C_RED) ? &img_font_damage_red : + &img_font_damage_white; + + int char_w = img->width / 10; + int char_h = img->height; /* Determine number of characters */ char str[16]; @@ -23,12 +28,22 @@ static void damage_render(int x, int y, particle_damage_t *p) for(int i = 0; i < n; i++) { int offset = (char_w + 1) * (str[i] - '0'); - dsubimage(x, y, &img_font_damage, offset, 0, char_w, char_h, - DIMAGE_NONE); + dsubimage(x, y, img, offset, 0, char_w, char_h, DIMAGE_NONE); x += char_w; } } +static bool dash_update(particle_dash_t *p, GUNUSED fixed_t dt) +{ + return p->particle.age >= 500; +} + +static void dash_render(int x, int y, particle_dash_t *p) +{ + if(!p->frame) return; + anim_frame_render(x, y, p->frame); +} + //--- // Generic functions //--- @@ -39,6 +54,8 @@ bool particle_update(particle_t *p, fixed_t dt) if(p->type == PARTICLE_DAMAGE) return damage_update((void *)p, dt); + if(p->type == PARTICLE_DASH) + return dash_update((void *)p, dt); return true; } @@ -47,4 +64,6 @@ void particle_render(int x, int y, particle_t const *p) { if(p->type == PARTICLE_DAMAGE) return damage_render(x, y, (void *)p); + if(p->type == PARTICLE_DASH) + return dash_render(x, y, (void *)p); } diff --git a/src/particles.h b/src/particles.h index 174ab8b..dbbe107 100644 --- a/src/particles.h +++ b/src/particles.h @@ -6,6 +6,7 @@ #include "fixed.h" #include "geometry.h" +#include "anim.h" #include @@ -23,6 +24,7 @@ typedef struct { /* Particle types */ enum { PARTICLE_DAMAGE = 0, + PARTICLE_DASH = 1, }; /* Update time and layout for a particle (type-generic). */ @@ -40,5 +42,15 @@ typedef struct { particle_t particle; /* Damage value */ int damage; + /* Color (accepts C_RED; everything else is white) */ + uint16_t color; } particle_damage_t; + +/* Dash trail particles */ +typedef struct { + particle_t particle; + /* Source sprite */ + anim_frame_t const *frame; + +} particle_dash_t;