enemy attacks, defeat, factored movement, unused dash particles

This commit is contained in:
Lephenixnoir 2021-06-30 13:56:58 +02:00
parent 70ecd17834
commit 0838277274
Signed by: Lephenixnoir
GPG Key ID: 1BBA026E13FC0495
15 changed files with 236 additions and 66 deletions

View File

@ -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)

Binary file not shown.

After

Width:  |  Height:  |  Size: 278 B

View File

Before

Width:  |  Height:  |  Size: 276 B

After

Width:  |  Height:  |  Size: 276 B

View File

@ -6,3 +6,6 @@
swing_*.png:
frame_duration: 30, 60, 30, 30, 30, 60, 30
hit.png:
frame_duration: 60, 90, 90, 60, 90

Binary file not shown.

BIN
assets-cg/skills/hit.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 181 B

View File

@ -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];

View File

@ -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);

View File

@ -2,6 +2,7 @@
#include "game.h"
#include "enemies.h"
#include <stdlib.h>
#include <gint/defs/util.h>
//---
// 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;

View File

@ -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[]);

View File

@ -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. */

View File

@ -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);

View File

@ -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 */

View File

@ -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);
}

View File

@ -6,6 +6,7 @@
#include "fixed.h"
#include "geometry.h"
#include "anim.h"
#include <gint/defs/types.h>
@ -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;