switch engine to ECS and rewrite just about everything

This commit is contained in:
Lephenixnoir 2021-12-25 11:47:22 +01:00
parent 228dec623e
commit cb3f363cfe
Signed by: Lephenixnoir
GPG Key ID: 1BBA026E13FC0495
35 changed files with 1548 additions and 1103 deletions

View File

@ -16,8 +16,8 @@ endif()
set(SOURCES
src/anim.c
src/aoe.c
src/enemies.c
src/entities.c
src/game.c
src/geometry.c
src/level.c
@ -27,6 +27,12 @@ set(SOURCES
src/pathfinding.c
src/render.c
src/util.c
src/comp/entity.c
src/comp/fighter.c
src/comp/mechanical.c
src/comp/physical.c
src/comp/visible.c
)
set(ASSETS
# Tilesets
@ -80,6 +86,7 @@ fxconv_declare_converters(assets-cg/converters.py)
add_executable(addin ${SOURCES} ${ASSETS})
target_compile_options(addin PRIVATE -Wall -Wextra -Os)
target_include_directories(addin PRIVATE src)
target_link_libraries(addin LibProf::LibProf Gint::Gint)
target_link_options(addin PRIVATE -Wl,-Map=map)

View File

@ -1,13 +1,13 @@
*.aseprite:
custom-type: aseprite-anim
name_regex: (.*)\.aseprite anims_\1
name_regex: (.*)\.aseprite frames_\1
profile: p8
next: Idle=Idle, Walking=Walking, Hit=Idle
slime_left.aseprite:
center: 12, 17
center: 11, 18
slime_right.aseprite:
center: 13, 17
center: 11, 18
bat_left.aseprite:
center: 10, 12

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@ -2,13 +2,13 @@
__ #_ #b #_ #_ __ __ __ #_ #t #_ __ __ __ __ __ __ __ __ __ __ __ __ __
__ R_ Rs R_ R_ #_ __ #_ Y_ Y_ Y_ #_ #b #t #_ __ __ __ #b #_ #_ __ __ __
__ R_ R_ R_ R_ R_ __ Y_ Y_ Y_ Y_ Y_ Y_ Y_ Y_ #_ #_ #_ G_ G_ G_ __ __ __
__ R_ R_ R_ R_ Y_ #_ Y_ Y_ Y_ Y_ Y_ Y_ Y_ Y_ Y_ Y_ Y_ G_ G_ G_ __ __ __
__ R_ Y_ Y_ Y_ Y_ Y_ Y_ __ __ Y_ Y_ Y_ Y_ Y_ Y_ Y_ Y_ Y_ Y_ __ __ __ __
__ __ __ __ __ __ Y_ Y_ #t #_ Y_ Y_ Y_ Y_ Y_ Y_ __ Y_ Y_ Y_ #_ __ __ __
__ R_ R_ R_ R_ R_ #_ Y_ Y_ Y_ Y_ Y_ Y_ Y_ Y_ #_ #_ #_ G_ G_ G_ __ __ __
__ R_ R_ R_ R_ Y_ Y_ Y_ Y_ R_ R_ Y_ Y_ Y_ Y_ Y_ Y_ Y_ G_ G_ G_ __ __ __
__ R_ Y_ Y_ Y_ Y_ Y_ Y_ R_ R_ R_ Y_ Y_ Y_ Y_ Y_ Y_ Y_ Y_ Y_ __ __ __ __
__ __ __ __ __ __ Y_ Y_ R_ R_ Y_ Y_ Y_ Y_ Y_ Y_ B_ Y_ Y_ Y_ #_ __ __ __
__ __ #_ #t #_ #_ Y_ Y_ Y_ Y_ Y_ Y_ Y_ Y_ Y_ Y_ __ Y_ Y_ Y_ Y_ #b #_ __
__ __ G_ G_ G_ Y_ Y_ Y_ Y_ __ __ __ Y_ #_ #b #_ #_ Y_ B_ B_ B_ B_ Y_ __
__ __ G_ G_ G_ __ Y_ Y_ Y_ __ __ __ Y_ Y_ Y_ B_ B_ B_ B_ Bs B_ B_ B_ __
__ __ G_ G_ G_ G_ Y_ Y_ Y_ Y_ __ __ Y_ #_ #b #_ #_ Y_ B_ B_ B_ B_ Y_ __
__ __ G_ G_ G_ G_ G_ Y_ Y_ __ __ __ Y_ Y_ Y_ B_ B_ B_ B_ Bs B_ B_ B_ __
__ __ G_ G_ G_ __ __ Y_ Y_ __ __ __ __ __ __ __ __ B_ B_ B_ B_ B_ __ __
__ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __

View File

@ -1,6 +1,6 @@
player_*.aseprite:
custom-type: aseprite-anim
name_regex: (.*)\.aseprite anims_\1
center: 12, 17
name_regex: (.*)\.aseprite frames_\1
center: 11, 19
next: Idle=Idle, Walking=Walking
profile: p8

Binary file not shown.

Before

Width:  |  Height:  |  Size: 540 B

After

Width:  |  Height:  |  Size: 553 B

View File

@ -1,6 +1,6 @@
*.aseprite:
custom-type: aseprite-anim
name_regex: (.*)\.aseprite anims_skill_\1
name_regex: (.*)\.aseprite frames_skill_\1
profile: p4
bullet_up.aseprite:

View File

@ -1,46 +1,72 @@
#include "anim.h"
#include <gint/display.h>
/* List of animations (and directional animations). */
/* Definition of simple and direction animations. */
#define DIRECTIONAL_ANIM(name) \
extern anim_frame_t name ## _up[]; \
extern anim_frame_t name ## _right[]; \
extern anim_frame_t name ## _down[]; \
extern anim_frame_t name ## _left[]; \
anim_frame_t *name[4] = { \
name ## _up, name ## _right, name ## _down, name ## _left };
#define ANIM1(name) \
extern anim_frame_t frames_ ## name[]; \
anim_t anims_ ## name = { 1, { frames_ ## name }};
#define ANIM_2DIRECTIONAL(prefix, suffix) \
extern anim_frame_t frames_ ## prefix ## _left ## suffix[]; \
extern anim_frame_t frames_ ## prefix ## _right ## suffix[]; \
anim_t anims_ ## prefix ## suffix = { 2, { \
frames_ ## prefix ## _left ## suffix, \
frames_ ## prefix ## _right ## suffix, \
}}; \
#define ANIM_4DIRECTIONAL(prefix, suffix) \
extern anim_frame_t prefix ## _up_ ## suffix[]; \
extern anim_frame_t prefix ## _right_ ## suffix[]; \
extern anim_frame_t prefix ## _down_ ## suffix[]; \
extern anim_frame_t prefix ## _left_ ## suffix[]; \
anim_frame_t *prefix ## _ ## suffix[4] = { \
prefix ## _up_ ## suffix, \
prefix ## _right_ ## suffix, \
prefix ## _down_ ## suffix, \
prefix ## _left_ ## suffix, \
};
extern anim_frame_t frames_ ## prefix ## _up ## suffix[]; \
extern anim_frame_t frames_ ## prefix ## _right ## suffix[]; \
extern anim_frame_t frames_ ## prefix ## _down ## suffix[]; \
extern anim_frame_t frames_ ## prefix ## _left ## suffix[]; \
anim_t anims_ ## prefix ## suffix = { 4, { \
frames_ ## prefix ## _up ## suffix, \
frames_ ## prefix ## _right ## suffix, \
frames_ ## prefix ## _down ## suffix, \
frames_ ## prefix ## _left ## suffix, \
}};
DIRECTIONAL_ANIM(anims_skill_swing);
DIRECTIONAL_ANIM(anims_skill_impale);
DIRECTIONAL_ANIM(anims_skill_bullet);
/* Make suffix optional */
#define ANIM2(prefix, ...) \
ANIM_2DIRECTIONAL(prefix, __VA_OPT__(_ ## __VA_ARGS__))
#define ANIM4(prefix, ...) \
ANIM_4DIRECTIONAL(prefix, __VA_OPT__(_ ## __VA_ARGS__))
ANIM_4DIRECTIONAL(anims_player, Idle);
ANIM_4DIRECTIONAL(anims_player, Walking);
ANIM_4DIRECTIONAL(anims_player, Attack);
ANIM_4DIRECTIONAL(anims_player, Hit);
/* Basic skills */
ANIM1(skill_hit);
ANIM1(skill_teleport);
ANIM1(skill_shock);
ANIM1(skill_judgement);
/* Directional skills */
ANIM4(skill_swing);
ANIM4(skill_impale);
ANIM4(skill_bullet);
/* Enemies */
ANIM2(slime, Idle);
ANIM2(slime, Walking);
ANIM2(slime, Hit);
ANIM2(slime, Death);
ANIM2(bat, Idle);
ANIM2(bat, Hit);
ANIM2(bat, Death);
/* Player */
ANIM4(player, Idle);
ANIM4(player, Walking);
ANIM4(player, Attack);
ANIM4(player, Hit);
/* Animation functions. */
fixed_t (anim_duration)(anim_frame_t const *first_frame)
fixed_t (anim_duration)(anim_t const *anim)
{
anim_frame_t const *frame = first_frame;
anim_frame_t const *frame = anim->start[0];
int ms = 0;
int i = 0;
while(frame == first_frame + i) {
while(frame == anim->start[0] + i) {
ms += frame->duration;
frame = frame->next;
i++;
@ -49,6 +75,22 @@ fixed_t (anim_duration)(anim_frame_t const *first_frame)
return fix(ms) / 1000;
}
bool anim_in(anim_frame_t const *frame, anim_t const *anim, int index)
{
if(index >= anim->directions)
return false;
anim_frame_t *f = anim->start[index];
for(int i = 0; f == anim->start[index] + i; i++) {
if(f == frame)
return true;
f = f->next;
}
return false;
}
void anim_frame_render(int x, int y, anim_frame_t const *frame)
{
if(!frame) return;

View File

@ -12,7 +12,8 @@
#include <gint/display.h>
typedef struct anim_frame {
typedef struct anim_frame
{
/* Sheet */
bopti_image_t const *sheet;
/* Box for the frame within the sheet */
@ -23,21 +24,35 @@ typedef struct anim_frame {
uint16_t duration;
/* Next frame */
struct anim_frame *next;
} anim_frame_t;
typedef struct {
typedef struct
{
/* Number of directions
1: Basic sprite
2: LEFT and RIGHT (enemies)
4: UP, RIGHT, DOWN and LEFT (player, some attacks) */
int directions;
/* First frame for each direction */
anim_frame_t *start[];
} anim_t;
typedef struct
{
/* Current frame */
anim_frame_t const *frame;
/* Time elapsed */
uint16_t elapsed;
} anim_state_t;
/* Duration of an animation, in seconds (assuming linear order until end). */
fixed_t anim_duration(anim_frame_t const *first_frame);
/* Duration of an animation, in seconds; assumed unique for all directions */
fixed_t anim_duration(anim_t const *anim);
#define anim_duration(ff) _Generic((ff), \
anim_frame_t *: anim_duration((void *)ff), \
anim_frame_t **: anim_duration(*(void **)ff))
/* Check if a frame is in an animation. */
bool anim_in(anim_frame_t const *frame, anim_t const *anim, int index);
/* Render a frame at the center position (x,y). */
void anim_frame_render(int x, int y, anim_frame_t const *frame);
@ -47,35 +62,27 @@ void anim_state_update(anim_state_t *state, fixed_t dt);
/* List of animations. */
/* Basic animations. */
extern anim_frame_t anims_skill_hit[];
extern anim_frame_t anims_skill_teleport[];
extern anim_frame_t anims_skill_shock[];
extern anim_frame_t anims_skill_judgement[];
/* Basic skills */
extern anim_t anims_skill_hit;
extern anim_t anims_skill_teleport;
extern anim_t anims_skill_shock;
extern anim_t anims_skill_judgement;
/* Directional skills */
extern anim_t anims_skill_swing;
extern anim_t anims_skill_impale;
extern anim_t anims_skill_bullet;
/* Enemy animations (bidirectional). */
/* Enemies */
extern anim_t anims_slime_Idle;
extern anim_t anims_slime_Walking;
extern anim_t anims_slime_Hit;
extern anim_t anims_slime_Death;
extern anim_t anims_bat_Idle;
extern anim_t anims_bat_Hit;
extern anim_t anims_bat_Death;
extern anim_frame_t anims_slime_left_Idle[];
extern anim_frame_t anims_slime_right_Idle[];
extern anim_frame_t anims_slime_left_Walking[];
extern anim_frame_t anims_slime_right_Walking[];
extern anim_frame_t anims_slime_left_Hit[];
extern anim_frame_t anims_slime_right_Hit[];
extern anim_frame_t anims_slime_left_Death[];
extern anim_frame_t anims_slime_right_Death[];
extern anim_frame_t anims_bat_left_Idle[];
extern anim_frame_t anims_bat_right_Idle[];
extern anim_frame_t anims_bat_left_Hit[];
extern anim_frame_t anims_bat_right_Hit[];
extern anim_frame_t anims_bat_left_Death[];
extern anim_frame_t anims_bat_right_Death[];
/* Quadri-directional animations. */
extern anim_frame_t *anims_player_Idle[4];
extern anim_frame_t *anims_player_Walking[4];
extern anim_frame_t *anims_player_Attack[4];
extern anim_frame_t *anims_player_Hit[4];
extern anim_frame_t *anims_skill_swing[4];
extern anim_frame_t *anims_skill_impale[4];
extern anim_frame_t *anims_skill_bullet[4];
/* Player */
extern anim_t anims_player_Idle;
extern anim_t anims_player_Walking;
extern anim_t anims_player_Attack;
extern anim_t anims_player_Hit;

256
src/aoe.c Normal file
View File

@ -0,0 +1,256 @@
#include "comp/entity.h"
#include "comp/physical.h"
#include "comp/visible.h"
#include "comp/fighter.h"
#include "aoe.h"
#include "game.h"
#include "enemies.h"
entity_t *aoe_make(uint16_t type, vec2 position, fixed_t lifetime)
{
entity_t *e = entity_make(physical, visible, aoe);
physical_t *p = getcomp(e, physical);
p->x = position.x;
p->y = position.y;
aoe_t *aoe = getcomp(e, aoe);
aoe->type = type;
aoe->lifetime = lifetime;
aoe->hits = NULL;
aoe->hit_count = 0;
return e;
}
void aoe_destroy(entity_t *e)
{
aoe_t *aoe = getcomp(e, aoe);
free(aoe->hits);
}
entity_t *aoe_make_attack(uint16_t type, entity_t *origin, int facing)
{
entity_t *e = aoe_make(type, physical_pos(origin), fix(0));
if(e == NULL)
return NULL;
fighter_t *f = getcomp(origin, fighter);
physical_t *p = getcomp(e, physical);
visible_t *v = getcomp(e, visible);
aoe_t *aoe = getcomp(e, aoe);
rect hitbox = { 0 };
fixed_t distance = fix(0.625);
vec2 dir = fdir(facing);
anim_t const *anim = NULL;
bool rotate = true;
fixed_t lifetime = fix(0);
int plane = VERTICAL;
if(type == AOE_HIT) {
anim = &anims_skill_hit;
hitbox = (rect){ -fix(4)/16, fix(3)/16, -fix(4)/16, fix(3)/16 };
}
else if(type == AOE_SLASH) {
anim = &anims_skill_swing;
hitbox = (rect){ -fix(10)/16, fix(10)/16, -fix(8)/16, 0 };
}
else if(type == AOE_IMPALE) {
anim = &anims_skill_impale;
hitbox = (rect){ -fix(4)/16, fix(4)/16, -fix(10)/16, 0 };
}
else if(type == AOE_SHOCK) {
anim = &anims_skill_shock;
hitbox = (rect){ -fix(17)/16, fix(18)/16, -fix(17)/16, fix(18)/16 };
distance = fix(0);
rotate = false;
plane = HORIZONTAL;
}
else if(type == AOE_JUDGEMENT) {
anim = &anims_skill_judgement;
hitbox = (rect){ -fix(10)/16, fix(11)/16, -fix(6)/16, fix(6)/16 };
distance = fix(1.5);
rotate = false;
}
else if(type == AOE_BULLET) {
anim = &anims_skill_bullet;
hitbox = (rect){ -fix(8)/16, fix(8)/16, -fix(7)/16, fix(7)/16 };
distance = fix(0.375);
lifetime = fix(999.0);
}
p->x += fmul(distance, dir.x);
p->y += fmul(distance, dir.y);
p->facing = facing;
p->hitbox = rotate ? rect_rotate(hitbox, UP, facing) : hitbox;
v->sprite_plane = plane;
aoe->lifetime = lifetime ? lifetime : anim_duration(anim);
aoe->repeat_delay = 0;
aoe->origin = origin;
if(type == AOE_HIT
|| type == AOE_SLASH
|| type == AOE_IMPALE
|| type == AOE_JUDGEMENT) {
aoe->data.generic.strength = f->ATK;
aoe->data.generic.dir = facing;
}
else if(type == AOE_SHOCK) {
aoe->data.shock.strength = f->ATK;
aoe->data.shock.origin = physical_pos(origin);
aoe->repeat_delay = fix(0.1);
}
else if(type == AOE_BULLET) {
aoe->data.bullet.strength = f->ATK;
aoe->data.bullet.dir = facing;
aoe->data.bullet.v = fix(10.0);
aoe->data.bullet.final_v = fix(4.5);
aoe->repeat_delay = fix(0.05);
}
visible_set_anim(e, anim, 1);
return e;
}
/* Existing record of the area hitting this entity */
static aoe_record_t *aoe_record(aoe_t *aoe, entity_t *target)
{
for(int i = 0; i < aoe->hit_count; i++) {
aoe_record_t *r = &aoe->hits[i];
if(r->entity == target) return r;
}
return NULL;
}
static bool attack_apply(game_t *game, aoe_t *aoe, entity_t *target)
{
fighter_t *origin_f = getcomp(aoe->origin, fighter);
fighter_t *target_f = getcomp(target, fighter);
mechanical_t *target_m = getcomp(target, mechanical);
physical_t *target_p = getcomp(target, physical);
if(!target_f)
return false;
/* No friendly fire */
bool origin_is_monster = (origin_f && origin_f->identity != 0);
bool target_is_monster = (target_f->identity != 0);
if(origin_is_monster == target_is_monster || target_f->HP == 0)
return false;
/* Dash invincibility */
if(target_m && mechanical_dashing(target))
return false;
/* Knockback */
fixed_t r = (rand() & (fix(0.125)-1)) + fix(0.375);
/* Half knockback against players */
if(target_f->identity == 0) r /= 2;
vec2 dir = { 0, 0 };
int damage = 0;
if(aoe->type == AOE_HIT
|| aoe->type == AOE_SLASH
|| aoe->type == AOE_IMPALE) {
dir = fdir(aoe->data.generic.dir);
damage = aoe->data.generic.strength;
}
else if(aoe->type == AOE_SHOCK) {
dir.x = target_p->x - aoe->data.shock.origin.x;
dir.y = target_p->y - aoe->data.shock.origin.y;
dir = fnormalize(dir);
damage = aoe->data.shock.strength * 3 / 2;
r /= 2;
}
else if(aoe->type == AOE_JUDGEMENT) {
r = fix(0);
damage = aoe->data.generic.strength * 5;
}
else if(aoe->type == AOE_BULLET) {
/* TODO: Sideways knockback */
damage = aoe->data.bullet.strength * 2;
}
/* Inflict damage */
damage = fighter_damage(target, damage);
/* Apply knockback */
if(target_m) {
target_m->vdx += fmul(dir.x, fmul(r, KNOCKBACK_SPEED));
target_m->vdy += fmul(dir.y, fmul(r, KNOCKBACK_SPEED));
}
/* Spawn damage particle */
particle_damage_t *p = malloc(sizeof *p);
p->particle.type = PARTICLE_DAMAGE;
p->particle.age = 0;
p->particle.pos = (vec2){ target_p->x, target_p->y - fix(0.5) };
p->damage = damage;
p->color = (target_f->identity == 0) ? C_RED : C_WHITE;
game_add_particle(game, &p->particle);
return true;
}
void aoe_apply(game_t *game, entity_t *entity, entity_t *e)
{
bool was_hit = false;
aoe_t *aoe = getcomp(entity, aoe);
aoe_record_t *rec = aoe_record(aoe, e);
/* Don't hit entities that have been recently hit */
if(rec && aoe->repeat_delay == 0) return;
if(rec && aoe->lifetime > rec->lifetime - aoe->repeat_delay) return;
if(aoe->type == AOE_HIT
|| aoe->type == AOE_SLASH
|| aoe->type == AOE_IMPALE
|| aoe->type == AOE_SHOCK
|| aoe->type == AOE_JUDGEMENT
|| aoe->type == AOE_BULLET)
was_hit = attack_apply(game, aoe, e);
if(!was_hit) return;
/* Update record */
if(!rec) {
size_t new_size = (aoe->hit_count + 1) * sizeof *aoe->hits;
aoe_record_t *new_hits = realloc(aoe->hits, new_size);
if(!new_hits) return;
aoe->hits = new_hits;
rec = &new_hits[aoe->hit_count];
rec->entity = e;
aoe->hit_count++;
}
rec->lifetime = aoe->lifetime;
}
void aoe_update(game_t *game, entity_t *entity, fixed_t dt)
{
physical_t *p = getcomp(entity, physical);
aoe_t *aoe = getcomp(entity, aoe);
if(aoe->type == AOE_BULLET) {
vec2 dir = fdir(aoe->data.bullet.dir);
p->x += fmul(fmul(aoe->data.bullet.v, dir.x), dt);
p->y += fmul(fmul(aoe->data.bullet.v, dir.y), dt);
/* Speed update - exponential decline from initial value to final_v */
aoe->data.bullet.v += fmul(
aoe->data.bullet.final_v - aoe->data.bullet.v, fix(0.001));
/* Collision of anchor with map destroys the bullet */
rect small_box = { 0, 0, 0, 0 };
small_box = rect_translate(small_box, physical_pos(entity));
if(map_collides(&game->map, small_box))
aoe->lifetime = 0;
}
}

77
src/aoe.h Normal file
View File

@ -0,0 +1,77 @@
//---
// aoe: Physical entities tracking areas of effect of skills
//---
#pragma once
#include "geometry.h"
#include "anim.h"
enum {
/* Bare-hand fist/close-contact attack */
AOE_HIT,
/* Normal attack by a slashing weapon */
AOE_SLASH,
/* Impaling attack using a slashing weapon */
AOE_IMPALE,
/* Skills */
AOE_SHOCK,
AOE_JUDGEMENT,
AOE_BULLET,
/* Spawn effect */
EFFECT_SPAWN,
};
/* Record of when an area hit an entity */
typedef struct
{
/* Entity hit */
entity_t *entity;
/* Lifetime of the area at the time of the hit */
fixed_t lifetime;
} aoe_record_t;
/* Extra component for areas of effect */
typedef struct
{
/* Lifetime (s) */
fixed_t lifetime;
/* Effect repeat delay (seconds; 0 for no repeat) */
fixed_t repeat_delay;
/* List of entities that were hit */
aoe_record_t *hits;
int hit_count;
/* Entity that caused the area */
entity_t *origin;
/* Type of effect */
uint16_t type;
/* Effect data (type-dependent) */
union {
/* Generic attacks: source and ATK strength */
struct { int strength; int dir; } generic;
/* EFFECT_ATTACK_SHOCK: origin and ATK strength */
struct { int strength; vec2 origin; } shock;
/* EFFECT_ATTACK_BULLET: Magic strengh, speed parameters */
struct { int strength; int dir; fixed_t v; fixed_t final_v; } bullet;
} data;
} aoe_t;
/* Create a new area of the specified type. The animation, repeat delay, origin
and type-specific data should be set manually after creation. */
entity_t *aoe_make(uint16_t type, vec2 position, fixed_t lifetime);
/* Free resources for an area. */
void aoe_destroy(entity_t *aoe);
/* Create an area for a particular attack; all attributes are initialized. */
entity_t *aoe_make_attack(uint16_t type, entity_t *origin, int facing);
/* Apply effect of area on entity */
struct game;
void aoe_apply(struct game *g, entity_t *aoe, entity_t *e);
/* Regular update, for moving areas (eg. bullet) */
void aoe_update(struct game *g, entity_t *aoe, fixed_t dt);

69
src/comp/entity.c Normal file
View File

@ -0,0 +1,69 @@
#include "comp/entity.h"
#include "comp/physical.h"
#include "comp/visible.h"
#include "comp/mechanical.h"
#include "comp/fighter.h"
#include "aoe.h"
/* Align up to native pointer size */
#define ALIGN(expr) ((((expr) - 1) | (sizeof(void *) - 1)) + 1)
entity_t *(entity_make)(uint32_t comps)
{
/* Determine full size of entity; also store offsets of components (we
explicitly 4-align them so we don't want to compute it several times) */
int offsets[ENTITY_COMP_CAPACITY] = { 0 };
size_t size = 0;
int n = 0;
if(comps & ENTITY_COMP_physical) {
offsets[n++] = size;
size = ALIGN(size + sizeof(physical_t));
}
if(comps & ENTITY_COMP_visible) {
offsets[n++] = size;
size = ALIGN(size + sizeof(visible_t));
}
if(comps & ENTITY_COMP_mechanical) {
offsets[n++] = size;
size = ALIGN(size + sizeof(mechanical_t));
}
if(comps & ENTITY_COMP_fighter) {
offsets[n++] = size;
size = ALIGN(size + sizeof(fighter_t));
}
if(comps & ENTITY_COMP_aoe) {
offsets[n++] = size;
size = ALIGN(size + sizeof(aoe_t));
}
/* Allocate all components together */
entity_t *e;
size_t base_size = sizeof(*e) + n * sizeof(e->data[0]);
e = calloc(1, base_size + size);
if(e == NULL)
return NULL;
/* Initialize component addresses */
e->comps = comps;
for(int i = 0; i < n; i++)
e->data[i] = (void *)e + base_size + offsets[i];
return e;
}
void entity_mark_to_delete(entity_t *e)
{
e->deleted = 1;
}
void entity_destroy(entity_t *e)
{
if(e->comps & ENTITY_COMP_aoe) {
aoe_destroy(e);
}
/* Since there's only one allocation for all components, this is easy */
free(e);
}

87
src/comp/entity.h Normal file
View File

@ -0,0 +1,87 @@
//---
// entity: Varied objects in the game, implemented with components
//
// This game runs on an Entity-Component system. Entities uniquely identify a
// variety of game objects, ranging from players and enemies to skills' areas
// of effect, item drops, spawn regions...
//
// Entities themselves are fairly empty, and most of their properties are
// defined through components. For instance, the [physical] component assigns a
// in space and a hitbox; the [visible] component gives sprites and rendering
// properties; the [mechanical] component provides controlled motion.
//---
#pragma once
#include <stdint.h>
#include <stddef.h>
#include <stdbool.h>
typedef struct
{
/* Deletion flag - deletions are delayed until end of frame to ease
management of dangling references */
uint32_t deleted: 1;
/* Bitmask of components in that entity (see ENTITY_COMP_*) */
uint32_t comps: 31;
/* Pointer to components; one pointer per non-zero entry in [comps],
starting from LSB to MSB */
void *data[];
} entity_t;
/* Bit field values for each type of component */
#define ENTITY_COMP_physical 0x00000001
#define ENTITY_COMP_visible 0x00000002
#define ENTITY_COMP_mechanical 0x00000004
#define ENTITY_COMP_fighter 0x00000008
#define ENTITY_COMP_aoe 0x00000010
/* Maximum number of components */
#define ENTITY_COMP_CAPACITY 31
/* Get a component from an entity */
static inline void *getcomp(entity_t const *e, uint32_t comp)
{
if(__builtin_expect(!(e->comps & comp), 0))
return NULL;
int i = 0;
while(comp) i += (e->comps & (comp >>= 1)) != 0;
return e->data[i];
}
/* Write "getcomp(e, physical)" */
#define getcomp(e, comp) ((comp ## _t *)getcomp(e, ENTITY_COMP_ ## comp))
/* Create an entity with a fixed set of components */
entity_t *entity_make(uint32_t comp);
/* Variation of entity_make() with up to 6 variable arguments */
#define ENTITY_MAKE_ARGS1(type, ...) \
ENTITY_COMP_##type __VA_OPT__(| ENTITY_MAKE_ARGS(__VA_ARGS__))
#define ENTITY_MAKE_ARGS2(type, ...) \
ENTITY_COMP_##type __VA_OPT__(| ENTITY_MAKE_ARGS1(__VA_ARGS__))
#define ENTITY_MAKE_ARGS3(type, ...) \
ENTITY_COMP_##type __VA_OPT__(| ENTITY_MAKE_ARGS2(__VA_ARGS__))
#define ENTITY_MAKE_ARGS4(type, ...) \
ENTITY_COMP_##type __VA_OPT__(| ENTITY_MAKE_ARGS3(__VA_ARGS__))
#define ENTITY_MAKE_ARGS5(type, ...) \
ENTITY_COMP_##type __VA_OPT__(| ENTITY_MAKE_ARGS4(__VA_ARGS__))
#define ENTITY_MAKE_ARGS6(type, ...) \
ENTITY_COMP_##type __VA_OPT__(| ENTITY_MAKE_ARGS5(__VA_ARGS__))
#define entity_make(...) \
entity_make(ENTITY_MAKE_ARGS6(__VA_ARGS__))
/* Mark an entity for deletion at the next collection step */
void entity_mark_to_delete(entity_t *e);
/* Destroy an entity */
void entity_destroy(entity_t *e);
// Useful types when finding/filtering/sorting entities
/* Propositional predicate on entities; used for counting and filtering */
typedef bool entity_predicate_t(entity_t const *e);
/* Measure; used for filtering-sorting */
typedef int entity_measure_t(entity_t const *e);

33
src/comp/fighter.c Normal file
View File

@ -0,0 +1,33 @@
#include "comp/entity.h"
#include "comp/fighter.h"
#include "comp/visible.h"
#include "enemies.h"
#include <stdlib.h>
int fighter_damage(entity_t *e, int base_damage)
{
fighter_t *f = getcomp(e, fighter);
if(f->HP == 0) return 0;
base_damage -= f->DEF;
if(base_damage <= 0) return 0;
int variation = (base_damage >= 4) ? (rand() % (base_damage / 4)) : 0;
int damage = (base_damage * 7) / 8 + variation;
if(f->HP < damage) f->HP = 0;
else f->HP -= damage;
if(f->identity > 0) {
if(f->HP == 0)
visible_set_anim(e, enemies[f->identity]->anim_death, 4);
else
visible_set_anim(e, enemies[f->identity]->anim_hit, 3);
}
else {
visible_set_anim(e, &anims_player_Hit, 3);
}
return damage;
}

38
src/comp/fighter.h Normal file
View File

@ -0,0 +1,38 @@
//---
// fighter: Component for entities that participate in fights
//
// This component gives combat statistics and leveling properties to entities.
//---
#pragma once
#include "comp/entity.h"
#include "fixed.h"
typedef struct
{
/* Current attack's effect area */
entity_t *current_attack;
/* Whether attack follows movement */
uint8_t attack_follows_movement;
/* Fighter ID (0 for player) */
uint8_t identity;
/* Combat statistics */
uint16_t HP, ATK, DEF, HP_max;
/* Number of hits in main attack's combo (TODO: Delegate to weapon?) */
uint8_t combo_length;
/* Next hit to be dealt */
uint8_t combo_next;
/* Delay until ideal time to start next hit */
fixed_t combo_delay;
/* Skill and item cooldowns (F2..F5).The "total" field is updated when
switching skills/items. The base field is the dynamic value. */
fixed_t actions_cooldown_total[5];
fixed_t actions_cooldown[5];
} fighter_t;
/* Damage entity for that amount of raw strength. Returns actual damage after
DES is subtracted, and randomization. */
int fighter_damage(entity_t *e, int base_damage);

132
src/comp/mechanical.c Normal file
View File

@ -0,0 +1,132 @@
#include "comp/entity.h"
#include "comp/mechanical.h"
#include "comp/physical.h"
#include "comp/fighter.h"
// Movement functions
void mechanical_move(entity_t *e, vec2 direction, fixed_t dt, map_t const *map)
{
physical_t *p = getcomp(e, physical);
mechanical_t *m = getcomp(e, mechanical);
mechanical_limits_t const *limits = m->limits;
direction = fnormalize(direction);
/* Determine facing */
int facing = -1;
bool horz = abs(direction.x) >= abs(direction.y);
if(direction.x == 0 && direction.y == 0) facing = -1;
else if( horz && direction.x >= 0) facing = RIGHT;
else if( horz && direction.x <= 0) facing = LEFT;
else if(!horz && direction.y <= 0) facing = UP;
else if(!horz && direction.y >= 0) facing = DOWN;
/* Targeted speed, with two components: dash motion and walk motion */
vec2 target = { 0, 0 };
if(m->dash > 0) {
/* The dash speed is set to one direction and cannot changed */
vec2 dir = fdir(m->dash_facing);
target.x += fmul(limits->dash_speed, dir.x);
target.y += fmul(limits->dash_speed, dir.y);
}
if(facing >= 0) {
/* Walking speed can be directed anywhere, anytime */
target.x += fmul(limits->max_speed, direction.x);
target.y += fmul(limits->max_speed, direction.y);
}
/* Friction from environment */
fixed_t friction_x = fmul(fmul(-target.x, limits->friction), dt);
fixed_t friction_y = fmul(fmul(-target.y, limits->friction), dt);
/* Get there exponentially fast */
m->vx = target.x + friction_x + m->vdx;
m->vy = target.y + friction_y + m->vdy;
/* Round very small speeds (smaller than 1/256 tiles/s) to 0 */
if(m->vx >= -256 && m->vx <= 255) m->vx = 0;
if(m->vy >= -256 && m->vy <= 255) m->vy = 0;
/* Decrement dash duration or dash cooldown */
if(m->dash > 0) {
m->dash -= dt;
if(m->dash <= 0) m->dash = -limits->dash_cooldown;
}
else if(m->dash < 0) {
m->dash += dt;
if(m->dash >= 0) m->dash = 0;
}
if(facing >= 0)
p->facing = facing;
fixed_t new_x = p->x + fmul(m->vx, dt);
fixed_t new_y = p->y + fmul(m->vy, dt);
rect new_hitbox = rect_translate(p->hitbox, (vec2){ new_x, new_y });
// TODO ECS: New collision/ejection system based on teleports
if(!map_collides(map, new_hitbox)) {
fighter_t *f = getcomp(e, fighter);
if(f && f->current_attack && f->attack_follows_movement) {
physical_t *attack = getcomp(f->current_attack, physical);
attack->x += (new_x - p->x);
attack->y += (new_y - p->y);
}
p->x = new_x;
p->y = new_y;
}
/* TODO: Without acceleration, the movement model is broken */
m->vdx = fmul(m->vdx, fix(0.8));
m->vdy = fmul(m->vdy, fix(0.8));
}
void mechanical_move4(entity_t *e, int direction, fixed_t dt, map_t const *map)
{
return mechanical_move(e, fdir(direction), dt, map);
}
void mechanical_dash(entity_t *e, int direction)
{
mechanical_t *m = getcomp(e, mechanical);
if(m->dash != 0)
return;
m->dash = m->limits->dash_duration;
m->dash_facing = direction;
}
// Information functions
bool mechanical_moving(entity_t const *e)
{
mechanical_t *m = getcomp(e, mechanical);
return (m->vx != 0) || (m->vy != 0);
}
bool mechanical_dashing(entity_t const *e)
{
physical_t *p = getcomp(e, physical);
mechanical_t *m = getcomp(e, mechanical);
mechanical_limits_t const *limits = m->limits;
/* True only if the direction of the dash movement is preserved */
if(p->facing != m->dash_facing)
return false;
/* True during initial propulsion */
if(m->dash > 0)
return true;
/* Also true as long as 1.5x over-speed is maintained */
fixed_t cur_v2 = fmul(m->vx, m->vx) + fmul(m->vy, m->vy);
fixed_t max_v2 = fmul(limits->max_speed, limits->max_speed);
if(m->dash < 0 && 2 * cur_v2 > 3 * max_v2)
return true;
return false;
}

67
src/comp/mechanical.h Normal file
View File

@ -0,0 +1,67 @@
//---
// mechanical: Component for objects with continuous motion
//
// Mechanical entities (which must also be physical) have kinematic properties
// that carry over from frame to frame, such as speed. The mechanical component
// itself only holds simulation data, and is controlled by another component
// (either player input or an AI).
//---
#pragma once
#include "comp/entity.h"
#include "geometry.h"
#include "map.h"
typedef struct
{
/* Maximum walking speed (m/s) */
fixed_t max_speed;
/* Friction coefficient */
fixed_t friction;
/* Peak dash speed (m/s) */
fixed_t dash_speed;
/* Dash duration (s) */
fixed_t dash_duration;
/* Dash cooldown (s) */
fixed_t dash_cooldown;
} mechanical_limits_t;
typedef struct
{
mechanical_limits_t const *limits;
/* Position of the mechanical entity is specified in physical.x/y! */
/* Current speed */
fixed_t vx, vy;
/* Disruption speed to be integrated next frame (eg. knockback) */
fixed_t vdx, vdy;
/* Dash time remaining (if positive) or cooldown remaining (if negative) */
fixed_t dash;
/* Dash direction */
uint8_t dash_facing;
} mechanical_t;
// Movement functions
/* Update movement for a 4-directional control (stand still if direction = -1).
Both the [mechanical] and [physical] components are affected. */
void mechanical_move4(entity_t *e, int direction, fixed_t dt,map_t const *map);
/* Update movement for a full-directional control (for AIs). */
void mechanical_move(entity_t *e, vec2 direction, fixed_t dt,map_t const *map);
/* Start dashing in the specified direction */
void mechanical_dash(entity_t *e, int direction);
// Information functions
/* Check if the entity is currently moving */
bool mechanical_moving(entity_t const *e);
/* Check if the entity is currently dashing */
bool mechanical_dashing(entity_t const *e);

14
src/comp/physical.c Normal file
View File

@ -0,0 +1,14 @@
#include "comp/entity.h"
#include "comp/physical.h"
vec2 physical_pos(entity_t const *e)
{
physical_t *p = getcomp(e, physical);
return (vec2){ p->x, p->y };
}
rect physical_abs_hitbox(entity_t const *e)
{
physical_t *p = getcomp(e, physical);
return rect_translate(p->hitbox, (vec2){ p->x, p->y });
}

29
src/comp/physical.h Normal file
View File

@ -0,0 +1,29 @@
//---
// physical: Component for entities that exist on the map
//
// This basic component gives a position and size (hitbox) to every object on
// the map. It's a requirement for several others, including [visible] and
// [mechanical], since the position is very commonly used.
//---
#pragma once
#include "comp/entity.h"
#include "geometry.h"
typedef struct
{
/* Current position, in absolute map coordinates */
fixed_t x, y;
/* Hitbox on the floor, relative to (x,y) */
rect hitbox;
/* Direction facing */
uint8_t facing;
} physical_t;
/* Entity position as a vector */
vec2 physical_pos(entity_t const *e);
/* Hitbox offset by position */
rect physical_abs_hitbox(entity_t const *e);

37
src/comp/visible.c Normal file
View File

@ -0,0 +1,37 @@
#include "comp/entity.h"
#include "comp/physical.h"
#include "comp/visible.h"
void visible_set_anim(entity_t *e, anim_t const *anim, int priority)
{
visible_t *v = getcomp(e, visible);
if(priority < v->anim_priority)
return;
int anim_index = 0;
physical_t *p = getcomp(e, physical);
if(p && anim->directions == 2) {
anim_index = (p->facing == RIGHT);
}
else if(p && anim->directions == 4) {
anim_index = p->facing;
}
if(anim_in(v->anim.frame, anim, anim_index))
return;
v->anim.frame = anim->start[anim_index];
v->anim.elapsed = 0;
v->anim_priority = priority;
}
void visible_update(entity_t *e, fixed_t dt)
{
visible_t *v = getcomp(e, visible);
anim_state_update(&v->anim, dt);
if(v->anim.frame == NULL)
v->anim_priority = 0;
}

55
src/comp/visible.h Normal file
View File

@ -0,0 +1,55 @@
//---
// visible: Component for visible entities with a sprite
//
// Visible entities (which must also be physical in order to have a position)
// mostly consist of 2D sprites that either lie flat on the ground or stand
// vertically. They also have a visual elevation (z) which only affects the
// display, and can drop shadows.
//
// Below is a schematic diagram of visible objects' geometry.
//
// |-_
// | `-_ z
// | `. | y
// ^--- :-_ | < Wall |_.-`
// z | : `-_ | sprite `-_
// | : `: x
// v--- :_.-``-:
// _.-``-_ :`-_
// `-_ `x_ :_.-` < Floor sprite
// `-_ _.-``
// `
//
// The "x" marks the entity's anchor, which is the point of the object tied to
// its position (which is used for depth ordering among other things). The
// anchor is aligned with the sprite's anchor, before optionally adding a z
// displacement for elevated visible entities.
//---
#pragma once
#include "comp/entity.h"
#include "geometry.h"
#include "anim.h"
typedef struct
{
/* Display elevation; has no influence on mechanics */
fixed_t z;
/* Current sprite */
anim_state_t anim;
/* Priority of current sprite (used to avoid animation conflicts) */
uint8_t anim_priority;
/* Sprite plane (either VERTICAL or HORIZONTAL) */
uint8_t sprite_plane;
/* Size of shadow cast (0 for no shadow; not all values are valid) */
uint8_t shadow_size;
} visible_t;
/* Set the entity's animation. If the priority is strictly lower than the
current animation's priority, this operation is ignored. */
void visible_set_anim(entity_t *e, anim_t const *anim, int priority);
/* Regular update function */
void visible_update(entity_t *e, fixed_t dt);

View File

@ -1,21 +1,26 @@
#include "comp/entity.h"
#include "comp/physical.h"
#include "comp/visible.h"
#include "comp/mechanical.h"
#include "comp/fighter.h"
#include "enemies.h"
#include <stdlib.h>
#include <string.h>
/* Declare animations for an enemy */
#define ANIMS(name, I, W, H, D) \
.anim_idle = { anims_##name##_left_##I, anims_##name##_right_##I }, \
.anim_walking = { anims_##name##_left_##W, anims_##name##_right_##W }, \
.anim_hit = { anims_##name##_left_##H, anims_##name##_right_##H }, \
.anim_death = { anims_##name##_left_##D, anims_##name##_right_##D }
.anim_idle = &anims_ ## name ## _ ## I, \
.anim_walking = &anims_ ## name ## _ ## W, \
.anim_hit = &anims_ ## name ## _ ## H, \
.anim_death = &anims_ ## name ## _ ## D
static enemy_t const slime = {
.name = "Slime",
ANIMS(slime, Idle, Walking, Hit, Death),
.hitbox = (rect){ -fix(3)/16, fix(4)/16, -fix(2)/16, fix(3)/16 },
.sprite = (rect){ -fix(5)/16, fix(5)/16, -fix(4)/16, fix(3)/16 },
.movement_params = {
.limits = {
.max_speed = fix(1),
.propulsion = fix(12),
.friction = fix(0.6),
},
.HP = {
.base = fix(10),
@ -32,6 +37,8 @@ static enemy_t const slime = {
.growth = fix(1),
.affinity = fix(0.5),
},
.shadow_size = 4,
.z = 0,
};
static enemy_t const bat = {
@ -39,9 +46,9 @@ static enemy_t const bat = {
ANIMS(bat, Idle, Idle, Hit, Death),
.hitbox = (rect){ -fix(3)/16, fix(4)/16, -fix(2)/16, fix(3)/16 },
.sprite = (rect){ -fix(5)/16, fix(5)/16, -fix(4)/16, fix(3)/16 },
.movement_params = {
.limits = {
.max_speed = fix(1.8),
.propulsion = fix(8),
.friction = fix(0.8),
},
.HP = {
.base = fix(8),
@ -58,6 +65,8 @@ static enemy_t const bat = {
.growth = fix(2),
.affinity = fix(1),
},
.shadow_size = 4,
.z = fix(0.75),
};
enemy_t const * const enemies[] = {
@ -76,42 +85,49 @@ static int instantiate_stat(enemy_stat_t const *stat, int level)
return ffloor(value);
}
entity_t *enemy_spawn(int enemy_id, int level)
entity_t *enemy_make(int enemy_id, int level)
{
if(enemy_id < 0 || (size_t)enemy_id >= sizeof enemies / sizeof *enemies)
return NULL;
entity_t *e = malloc(sizeof *e);
if(!e) return NULL;
entity_t *e = entity_make(physical, visible, mechanical, fighter);
if(e == NULL)
return NULL;
enemy_t const *data = enemies[enemy_id];
/* These will probably be overridden by the caller */
e->movement.x = 0;
e->movement.y = 0;
e->movement.vx = 0;
e->movement.vy = 0;
e->movement.facing = LEFT;
e->movement.dash = 0;
e->movement.dash_facing = LEFT;
e->movement_params = &data->movement_params;
e->anim_priority = 0;
entity_set_anim(e, data->anim_idle[0], 1);
physical_t *p = getcomp(e, physical);
p->x = fix(0);
p->y = fix(0);
p->hitbox = data->hitbox;
p->facing = LEFT;
e->hitbox = data->hitbox;
e->sprite = data->sprite;
visible_t *v = getcomp(e, visible);
v->z = data->z;
v->sprite_plane = VERTICAL;
v->shadow_size = data->shadow_size;
v->anim_priority = 0;
visible_set_anim(e, data->anim_idle, 1);
e->current_attack = NULL;
e->attack_follows_movement = false;
mechanical_t *m = getcomp(e, mechanical);
m->limits = &data->limits;
m->vx = fix(0);
m->vy = fix(0);
m->dash = fix(0);
m->dash_facing = LEFT;
e->identity = enemy_id;
/* Instantiate fighter statistics */
fighter_t *f = getcomp(e, fighter);
memset(f, 0, sizeof *f);
e->HP_max = instantiate_stat(&data->HP, level);
e->ATK = instantiate_stat(&data->ATK, level);
e->DEF = instantiate_stat(&data->DEF, level);
e->HP = e->HP_max;
f->identity = enemy_id;
f->HP_max = instantiate_stat(&data->HP, level);
f->ATK = instantiate_stat(&data->ATK, level);
f->DEF = instantiate_stat(&data->DEF, level);
f->HP = f->HP_max;
f->combo_length = 1;
return e;
}

View File

@ -4,12 +4,14 @@
#pragma once
#include "comp/entity.h"
#include "comp/mechanical.h"
#include "geometry.h"
#include "anim.h"
#include "entities.h"
/* enemy_stat_t: Statistic base and growth */
typedef struct {
typedef struct
{
/* Base value at level 1 */
fixed_t base;
/* Initial per-level growth */
@ -20,19 +22,24 @@ typedef struct {
} enemy_stat_t;
/* enemy_t: Static enemy information */
typedef struct {
/* Enemy name (showed in wave information) */
typedef struct
{
/* Enemy name (shown in wave information) */
char const *name;
/* Map hitbox (represents ground size, collides with walls) */
rect hitbox;
/* Sprite hitbox (interacts with attacks and effect areas) */
rect sprite;
/* Idle animation, death animation, damage animation */
anim_frame_t *anim_idle[2], *anim_walking[2], *anim_hit[2], *anim_death[2];
anim_t *anim_idle, *anim_walking, *anim_hit, *anim_death;
/* Movement parameters */
entity_movement_params_t movement_params;
mechanical_limits_t limits;
/* Statistics model */
enemy_stat_t HP, ATK, DEF;
/* Shadow size */
uint8_t shadow_size;
/* Rendering elevation */
fixed_t z;
} enemy_t;
@ -47,4 +54,4 @@ enum {
extern enemy_t const * const enemies[];
/* Create a new enemy of the given type. */
entity_t *enemy_spawn(int enemy_id, int level);
entity_t *enemy_make(int enemy_id, int level);

View File

@ -1,404 +0,0 @@
#include "entities.h"
#include "game.h"
#include "enemies.h"
#include <stdlib.h>
#include <gint/defs/util.h>
//---
// Entities
//---
vec2 entity_pos(entity_t const *e)
{
return (vec2){ e->movement.x, e->movement.y };
}
rect entity_hitbox(entity_t const *e)
{
return rect_translate(e->hitbox, entity_pos(e));
}
rect entity_sprite(entity_t const *e)
{
return rect_translate(e->sprite, entity_pos(e));
}
static void update_pos(entity_movement_t *m, fixed_t dt)
{
m->x += fmul(m->vx, dt);
m->y += fmul(m->vy, dt);
}
void entity_dash(entity_t *e, int direction)
{
entity_movement_params_t const *params = e->movement_params;
entity_movement_t *move = &e->movement;
if(move->dash != 0) return;
move->dash = params->dash_duration;
move->dash_facing = direction;
}
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 1.5x over-speed is maintained */
fixed_t cur_v2 = fmul(move->vx, move->vx) + fmul(move->vy, move->vy);
fixed_t max_v2 = fmul(params->max_speed, params->max_speed);
if(move->dash < 0 && 2 * cur_v2 > 3 * max_v2)
return true;
return false;
}
entity_movement_t entity_move(entity_t *e, vec2 direction, fixed_t dt)
{
entity_movement_params_t const *params = e->movement_params;
entity_movement_t next = e->movement;
direction = fnormalize(direction);
/* Determine facing */
int facing = -1;
bool horz = abs(direction.x) >= abs(direction.y);
if(direction.x == 0 && direction.y == 0) facing = -1;
else if( horz && direction.x >= 0) facing = RIGHT;
else if( horz && direction.x <= 0) facing = LEFT;
else if(!horz && direction.y <= 0) facing = UP;
else if(!horz && direction.y >= 0) facing = DOWN;
/* Targeted speed */
vec2 target = { 0, 0 };
/* The dash speed is set to one direction and cannot changed */
if(next.dash > 0) {
vec2 dir = fdir(next.dash_facing);
target.x += fmul(params->dash_speed, dir.x);
target.y += fmul(params->dash_speed, dir.y);
}
/* Walking speed can be directed anywhere, anytime */
if(facing >= 0) {
next.facing = facing;
target.x += fmul(params->max_speed, direction.x);
target.y += fmul(params->max_speed, direction.y);
}
/* Get there exponentially fast */
next.vx += fmul(target.x - next.vx, fmul(params->propulsion, dt));
next.vy += fmul(target.y - next.vy, fmul(params->propulsion, dt));
/* Round very small speeds (smaller than 1/256 tiles/s) to 0 */
if(next.vx >= -256 && next.vx <= 255) next.vx = 0;
if(next.vy >= -256 && next.vy <= 255) next.vy = 0;
/* Decrement dash duration or dash cooldown */
if(next.dash > 0) {
next.dash -= dt;
if(next.dash <= 0) next.dash = -params->dash_cooldown;
}
else if(next.dash < 0) {
next.dash += dt;
if(next.dash >= 0) next.dash = 0;
}
update_pos(&next, dt);
return next;
}
entity_movement_t entity_move4(entity_t *e, int direction, fixed_t dt)
{
return entity_move(e, fdir(direction), dt);
}
bool entity_moving(entity_t const *e)
{
return (e->movement.vx != 0) || (e->movement.vy != 0);
}
void entity_set_normal_anim(entity_t *e, anim_frame_t *frame, int priority)
{
if(priority < e->anim_priority) return;
e->anim.frame = frame;
e->anim.elapsed = 0;
e->anim_priority = priority;
}
void entity_set_directional_anim(entity_t *e, anim_frame_t *frame_dirs[],
int priority)
{
if(e->movement.facing >= 4) return;
entity_set_normal_anim(e, frame_dirs[e->movement.facing], priority);
}
int entity_damage(entity_t *e, int base_damage)
{
if(e->HP == 0) return 0;
base_damage -= e->DEF;
if(base_damage <= 0) return 0;
int variation = (base_damage >= 4) ? (rand() % (base_damage / 4)) : 0;
int damage = (base_damage * 7) / 8 + variation;
if(e->HP < damage) e->HP = 0;
else e->HP -= damage;
if(e->identity > 0) {
int index = (e->movement.facing == RIGHT);
if(e->HP == 0)
entity_set_anim(e, enemies[e->identity]->anim_death[index], 4);
else
entity_set_anim(e, enemies[e->identity]->anim_hit[index], 3);
}
else {
entity_set_anim(e, anims_player_Hit[e->movement.facing], 3);
}
return damage;
}
//---
// Effect areas
//---
effect_area_t *effect_area_new(uint16_t type)
{
effect_area_t *ea = malloc(sizeof *ea);
if(!ea) return NULL;
ea->type = type;
ea->hits = NULL;
ea->hit_count = 0;
return ea;
}
effect_area_t *effect_area_new_attack(uint16_t type, entity_t *e, int facing)
{
effect_area_t *area = effect_area_new(type);
if(!area) return NULL;
rect hitbox = { 0 };
fixed_t distance = fix(0.625);
vec2 dir = fdir(facing);
vec2 anchor = rect_center(entity_sprite(e));
anim_frame_t *anim = NULL;
bool rotate = true;
fixed_t lifetime = fix(0);
if(type == EFFECT_ATTACK_HIT) {
anim = anims_skill_hit;
hitbox = (rect){ -fix(4)/16, fix(3)/16, -fix(4)/16, fix(3)/16 };
}
else if(type == EFFECT_ATTACK_SLASH) {
anim = anims_skill_swing[facing];
hitbox = (rect){ -fix(10)/16, fix(10)/16, -fix(8)/16, 0 };
if(facing == UP || facing == DOWN)
anchor = entity_pos(e);
}
else if(type == EFFECT_ATTACK_IMPALE) {
anim = anims_skill_impale[facing];
hitbox = (rect){ -fix(4)/16, fix(4)/16, -fix(10)/16, 0 };
if(facing == UP || facing == DOWN)
anchor = entity_pos(e);
}
else if(type == EFFECT_ATTACK_SHOCK) {
anim = anims_skill_shock;
hitbox = (rect){ -fix(17)/16, fix(18)/16, -fix(17)/16, fix(18)/16 };
anchor = entity_pos(e);
distance = fix(0);
rotate = false;
}
else if(type == EFFECT_ATTACK_JUDGEMENT) {
anim = anims_skill_judgement;
hitbox = (rect){ -fix(10)/16, fix(11)/16, -fix(6)/16, fix(6)/16 };
anchor = entity_pos(e);
distance = fix(1.5);
rotate = false;
}
else if(type == EFFECT_ATTACK_BULLET) {
anim = anims_skill_bullet[facing];
hitbox = (rect){ -fix(8)/16, fix(8)/16, -fix(7)/16, fix(7)/16 };
distance = fix(0.375);
anchor = entity_pos(e);
lifetime = fix(999.0);
}
area->sprite = rotate ? rect_rotate(hitbox, UP, facing) : hitbox;
area->anchor = anchor;
area->anchor.x += fmul(distance, dir.x);
area->anchor.y += fmul(distance, dir.y);
area->lifetime = lifetime ? lifetime : anim_duration(anim);
area->repeat_delay = 0;
area->origin = e;
if(type == EFFECT_ATTACK_HIT
|| type == EFFECT_ATTACK_SLASH
|| type == EFFECT_ATTACK_IMPALE
|| type == EFFECT_ATTACK_JUDGEMENT) {
area->data.generic.strength = e->ATK;
area->data.generic.dir = facing;
}
else if(type == EFFECT_ATTACK_SHOCK) {
area->data.shock.strength = e->ATK;
area->data.shock.origin = entity_pos(e);
area->repeat_delay = fix(0.1);
}
else if(type == EFFECT_ATTACK_BULLET) {
area->data.bullet.strength = e->ATK;
area->data.bullet.dir = facing;
area->data.bullet.v = fix(10.0);
area->data.bullet.final_v = fix(4.5);
area->repeat_delay = fix(0.05);
}
effect_area_set_anim(area, anim);
return area;
}
void effect_area_set_anim(effect_area_t *ea, anim_frame_t *frame)
{
ea->anim.frame = frame;
ea->anim.elapsed = 0;
}
/* Existing record of the area hitting this entity */
static effect_area_record_t *effect_record(effect_area_t *ea, entity_t *e)
{
for(int i = 0; i < ea->hit_count; i++) {
effect_area_record_t *rec = &ea->hits[i];
if(rec->entity == e) return rec;
}
return NULL;
}
static bool attack_apply(game_t *game, effect_area_t *ea, entity_t *e)
{
/* 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;
/* Knockback */
fixed_t r = (rand() & (fix(0.25)-1)) + fix(0.875);
/* Half knockback against players */
if(e->identity == 0) r /= 2;
vec2 dir = { 0, 0 };
int damage = 0;
if(ea->type == EFFECT_ATTACK_HIT
|| ea->type == EFFECT_ATTACK_SLASH
|| ea->type == EFFECT_ATTACK_IMPALE) {
dir = fdir(ea->data.generic.dir);
damage = ea->data.generic.strength;
}
else if(ea->type == EFFECT_ATTACK_SHOCK) {
dir.x = e->movement.x - ea->data.shock.origin.x;
dir.y = e->movement.y - ea->data.shock.origin.y;
dir = fnormalize(dir);
damage = ea->data.shock.strength * 3 / 2;
r /= 2;
}
else if(ea->type == EFFECT_ATTACK_JUDGEMENT) {
r = fix(0);
damage = ea->data.generic.strength * 5;
}
else if(ea->type == EFFECT_ATTACK_BULLET) {
/* TODO: Sideways knockback */
damage = ea->data.bullet.strength * 2;
}
/* Inflict damage */
damage = entity_damage(e, damage);
/* Apply knockback */
e->movement.vx += fmul(dir.x, fmul(r, KNOCKBACK_SPEED));
e->movement.vy += fmul(dir.y, fmul(r, KNOCKBACK_SPEED));
/* Spawn damage particle */
particle_damage_t *p = malloc(sizeof *p);
p->particle.type = PARTICLE_DAMAGE;
p->particle.age = 0;
p->particle.pos = (vec2){ e->movement.x, e->movement.y - fix(0.5) };
p->damage = damage;
p->color = (e->identity == 0) ? C_RED : C_WHITE;
game_add_particle(game, &p->particle);
return true;
}
void effect_area_apply(game_t *game, effect_area_t *ea, entity_t *e)
{
bool was_hit = false;
effect_area_record_t *rec = effect_record(ea, e);
/* Don't hit entities that have been recently hit */
if(rec && ea->repeat_delay == 0) return;
if(rec && ea->lifetime > rec->lifetime - ea->repeat_delay) return;
if(ea->type == EFFECT_ATTACK_HIT
|| ea->type == EFFECT_ATTACK_SLASH
|| ea->type == EFFECT_ATTACK_IMPALE
|| ea->type == EFFECT_ATTACK_SHOCK
|| ea->type == EFFECT_ATTACK_JUDGEMENT
|| ea->type == EFFECT_ATTACK_BULLET)
was_hit = attack_apply(game, ea, e);
if(!was_hit) return;
/* Update record */
if(!rec) {
size_t new_size = (ea->hit_count + 1) * sizeof *ea->hits;
effect_area_record_t *new_hits = realloc(ea->hits, new_size);
if(!new_hits) return;
ea->hits = new_hits;
rec = &new_hits[ea->hit_count];
rec->entity = e;
ea->hit_count++;
}
rec->lifetime = ea->lifetime;
}
bool effect_area_is_background(effect_area_t const *ea)
{
return ea->type == EFFECT_ATTACK_SHOCK;
}
void effect_area_update(game_t *game, effect_area_t *ea, fixed_t dt)
{
if(ea->type == EFFECT_ATTACK_BULLET) {
vec2 dir = fdir(ea->data.bullet.dir);
ea->anchor.x += fmul(fmul(ea->data.bullet.v, dir.x), dt);
ea->anchor.y += fmul(fmul(ea->data.bullet.v, dir.y), dt);
/* Speed update - exponential decline from initial value to final_v */
ea->data.bullet.v += fmul(ea->data.bullet.final_v - ea->data.bullet.v,
fix(0.001));
/* Collision of anchor with map destroys the bullet */
rect small_box = { 0, 0, 0, 0 };
small_box = rect_translate(small_box, ea->anchor);
if(map_collides(&game->map, small_box))
ea->lifetime = 0;
}
}

View File

@ -1,204 +0,0 @@
//---
// entities: Objects that move around the map
//---
#pragma once
#include "geometry.h"
#include "anim.h"
struct effect_area;
//---
// Agents
//---
typedef struct {
/* Maximum walking speed (m/s) */
fixed_t max_speed;
/* Rate a which target speed is approached (1/s) */
fixed_t propulsion;
/* Peak dash speed (m/s) */
fixed_t dash_speed;
/* Dash duration (s) */
fixed_t dash_duration;
/* Dash cooldown (s) */
fixed_t dash_cooldown;
} entity_movement_params_t;
typedef struct {
/* Current position */
fixed_t x, y;
/* Current speed */
fixed_t vx, vy;
/* Dash time remaining (if positive) or cooldown remaining (if negative) */
fixed_t dash;
/* Direction currently facing */
uint8_t facing;
/* Dash direction */
uint8_t dash_facing;
} entity_movement_t;
typedef struct {
/* Number of hits in the main combo (TODO: delegate to weapon?) */
uint8_t combo_length;
/* Next hit to be dealt */
uint8_t combo_next;
/* Delay until ideal time to start next hit */
fixed_t combo_delay;
/* Skill and item cooldowns (F2..F5).The "total" field is updated when
switching skills/items. The base field is the dynamic value. */
fixed_t actions_cooldown_total[5];
fixed_t actions_cooldown[5];
} entity_player_t;
/* Alive agents with hitboxes and movement control */
typedef struct {
/* Map hitbox, centered around (movement.x, movement.y) */
rect hitbox;
/* Sprite hitbox, centered similarly */
rect sprite;
/* Current cinematic situation */
entity_movement_t movement;
/* Cinematic parameters */
entity_movement_params_t const *movement_params;
/* Player-only parameters */
entity_player_t const *player;
/* Animated image and state */
anim_state_t anim;
/* Current attack's effect area */
struct effect_area *current_attack;
/* Animation priority (to improve legibility) */
uint8_t anim_priority;
/* Whether attack follows movement */
uint8_t attack_follows_movement;
/* Enemy ID (0 for player) */
uint8_t identity;
/* Combat statistics */
uint16_t HP, ATK, DEF, HP_max;
/* Time left until nexth pathfinding run (ms) */
// uint8_t pathfind_delay;
} entity_t;
/* Entity position */
vec2 entity_pos(entity_t const *e);
/* Entity hitbox, accounting for position */
rect entity_hitbox(entity_t const *e);
/* Entity sprite, accounting for position */
rect entity_sprite(entity_t const *e);
/* Update walk/sprint movement (stand still if direction == -1); this cinematic
state can be submitted for collisions and may or may not be accpeted */
entity_movement_t entity_move4(entity_t *e, int direction, fixed_t dt);
/* Update walk/sprint movement in any direction (for IAs) */
entity_movement_t entity_move(entity_t *e, vec2 direction, fixed_t dt);
/* Check if the entity is currently moving */
bool entity_moving(entity_t const *e);
/* 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, int priority);
void entity_set_directional_anim(entity_t *e, anim_frame_t *frame_dirs[],
int priority);
#define entity_set_anim(e, frame, priority) _Generic((frame), \
anim_frame_t *: entity_set_normal_anim(e, (void *)frame, priority), \
anim_frame_t **: entity_set_directional_anim(e, (void *)frame, priority))
/* Damage entity for that amount of raw strength. Returns actual damage after
DES is subtracted, and randomization. */
int entity_damage(entity_t *e, int base_damage);
//---
// Effect areas
//---
enum {
/* Bare-hand fist/close-contact attack */
EFFECT_ATTACK_HIT,
/* Normal attack by a slashing weapon */
EFFECT_ATTACK_SLASH,
/* Impaling attack using a slashing weapon */
EFFECT_ATTACK_IMPALE,
/* Skills */
EFFECT_ATTACK_SHOCK,
EFFECT_ATTACK_JUDGEMENT,
EFFECT_ATTACK_BULLET,
/* Monster spawning */
EFFECT_SPAWN,
};
/* Record of when an effect area hit an entity */
typedef struct {
/* Entity hit */
entity_t *entity;
/* Lifetime of the area at that time */
fixed_t lifetime;
} effect_area_record_t;
/* Effect area */
typedef struct effect_area {
/* Area sprite (used as a hitbox) */
rect sprite;
/* Position of center */
vec2 anchor;
/* Lifetime (s) */
fixed_t lifetime;
/* Animated image and state */
anim_state_t anim;
/* Effect repeat delay (s; 0 for no repeat) */
fixed_t repeat_delay;
/* List of entities that were hit */
effect_area_record_t *hits;
int hit_count;
/* Entity that caused the area */
entity_t *origin;
/* Type of effect */
uint16_t type;
/* Effect data (type-dependent) */
union {
/* Generic attacks: source and ATK strength */
struct { int strength; int dir; } generic;
/* EFFECT_ATTACK_SHOCK: origin and ATK strength */
struct { int strength; vec2 origin; } shock;
/* EFFECT_ATTACK_BULLET: Magic strengh, speed parameters */
struct { int strength; int dir; fixed_t v; fixed_t final_v; } bullet;
} data;
} effect_area_t;
/* Create a new effect area of the specified type.
* sprite, anchor, lifetime, anim, repeat_delay, and data should be set. */
effect_area_t *effect_area_new(uint16_t type);
/* Create an effect area for a particular attack
All additional parameters are set by this function. */
effect_area_t *effect_area_new_attack(uint16_t type, entity_t *e, int facing);
/* Set area animation */
void effect_area_set_anim(effect_area_t *ea, anim_frame_t *frame);
/* Apply effect of area on entity */
struct game;
void effect_area_apply(struct game *g, effect_area_t *ea, entity_t *e);
/* Whether the area should be rendered in background (floor level) */
bool effect_area_is_background(effect_area_t const *ea);
/* Regular update, for moving areas (eg. bullet) */
void effect_area_update(struct game *g, effect_area_t *ea, fixed_t dt);

View File

@ -2,6 +2,11 @@
#include "util.h"
#include "enemies.h"
#include "comp/fighter.h"
#include "comp/physical.h"
#include "comp/visible.h"
#include "aoe.h"
#include <stdlib.h>
bool game_load(game_t *g, level_t *level)
@ -19,7 +24,7 @@ bool game_load(game_t *g, level_t *level)
for(int y = 0; y < m->height; y++)
for(int x = 0; x < m->width; x++) {
struct tile *t = map_tile(m, x, y);
tile_t *t = map_tile(m, x, y);
t->base = level->map->tiles[level->map->width * y + x].base;
t->decor = level->map->tiles[level->map->width * y + x].decor;
t->solid = (t->base == 0) || (t->base >= 16);
@ -33,8 +38,6 @@ bool game_load(game_t *g, level_t *level)
g->entities = NULL;
g->entity_count = 0;
g->effect_areas = NULL;
g->effect_area_count = 0;
g->particles = NULL;
g->particle_count = 0;
@ -53,16 +56,19 @@ void game_unload(game_t *g)
free(g->map.tiles);
for(int i = 0; i < g->entity_count; i++)
free(g->entities[i]);
entity_destroy(g->entities[i]);
free(g->entities);
for(int i = 0; i < g->effect_area_count; i++)
free(g->effect_areas[i]);
free(g->effect_areas);
g->entities = NULL;
g->entity_count = 0;
// TODo ECS: Remove particles
for(int i = 0; i < g->particle_count; i++)
free(g->particles[i]);
free(g->particles);
g->particles = NULL;
g->particle_count = 0;
}
level_wave_t const *game_current_wave(game_t const *g)
@ -97,97 +103,79 @@ void game_add_entity(game_t *g, entity_t *entity)
g->entity_count++;
}
void game_spawn_entity(game_t *g, entity_t *e, vec2 pos)
void game_spawn_entity(game_t *g, entity_t *e)
{
game_add_entity(g, e);
e->movement.x = pos.x;
e->movement.y = pos.y;
if(e->current_attack) return;
fighter_t *f = getcomp(e, fighter);
if(!f || f->current_attack) return;
/* Teleport hitbox */
rect hitbox = {
-fix(8)/16, fix(7)/16, -fix(20)/16, fix(4)/16,
};
effect_area_t *area = effect_area_new(EFFECT_SPAWN);
area->sprite = hitbox;
area->anchor = entity_pos(e);
area->lifetime = anim_duration(anims_skill_teleport);
area->repeat_delay = 0;
area->origin = e;
effect_area_set_anim(area, anims_skill_teleport);
game_add_effect_area(g, area);
entity_t *spawn = aoe_make(EFFECT_SPAWN, physical_pos(e), fix(0));
e->current_attack = area;
e->attack_follows_movement = true;
getcomp(spawn, physical)->hitbox = hitbox;
aoe_t *aoe = getcomp(spawn, aoe);
aoe->lifetime = anim_duration(&anims_skill_teleport);
aoe->repeat_delay = 0;
aoe->origin = e;
visible_set_anim(spawn, &anims_skill_teleport, 2);
game_add_entity(g, spawn);
f->current_attack = spawn;
f->attack_follows_movement = true;
}
/* Remove an entity and rearrange the array. */
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]);
entity_destroy(g->entities[i]);
g->entities[i] = g->entities[--g->entity_count];
}
void game_remove_dead_entities(game_t *g)
{
int i = 0;
while(i < g->entity_count) {
for(int i = 0; i < g->entity_count; i++) {
entity_t *e = g->entities[i];
if(e->HP == 0 && !e->anim.frame && e->identity != 0)
game_remove_entity(g, i);
else i++;
}
}
/* First remove dead fighters */
fighter_t *f = getcomp(e, fighter);
visible_t *v = getcomp(e, visible);
bool anim_finished = !v || (v->anim.frame == NULL);
void game_add_effect_area(game_t *g, effect_area_t *ea)
{
size_t new_size = (g->effect_area_count + 1) * sizeof *g->effect_areas;
effect_area_t **new_effect_areas = realloc(g->effect_areas, new_size);
if(!new_effect_areas) return;
if(f && f->HP == 0 && f->identity != 0 && anim_finished) {
/* Disown remaining areas of effect */
fighter_t *f = getcomp(e, fighter);
if(f->current_attack) {
aoe_t *aoe = getcomp(f->current_attack, aoe);
aoe->origin = NULL;
}
entity_mark_to_delete(e);
}
g->effect_areas = new_effect_areas;
g->effect_areas[g->effect_area_count] = ea;
g->effect_area_count++;
}
/* Remove an effect area and rearrange the array. */
static void game_remove_effect_area(game_t *g, int i)
{
if(i < 0 || i >= g->effect_area_count) return;
effect_area_t *ea = g->effect_areas[i];
if(ea->origin && ea->origin->current_attack == ea) {
ea->origin->current_attack = NULL;
ea->origin->attack_follows_movement = false;
/* Then remove areas of effect with expired lifetime */
aoe_t *aoe = getcomp(e, aoe);
if(aoe && aoe->lifetime <= 0) {
/* Notify origin of area removal */
if(aoe->origin) {
fighter_t *f = getcomp(aoe->origin, fighter);
f->current_attack = NULL;
f->attack_follows_movement = false;
}
entity_mark_to_delete(e);
}
}
free(g->effect_areas[i]->hits);
free(g->effect_areas[i]);
g->effect_areas[i] = g->effect_areas[--g->effect_area_count];
/* Don't realloc, we'll likely add new areas soon enough and space will be
reclaimed as needed at that time. Don't need to add heap work now. */
}
/* Remove all dead effect areas */
static void game_remove_dead_effect_areas(game_t *g)
{
int i = 0;
while(i < g->effect_area_count) {
effect_area_t *ea = g->effect_areas[i];
if(ea->lifetime <= 0) game_remove_effect_area(g, i);
while(i < g->entity_count) {
if(g->entities[i]->deleted)
game_remove_entity(g, i);
else i++;
}
}
@ -215,9 +203,61 @@ static void game_remove_particle(game_t *g, int i)
}
//---
// Interacting with game elements
// Generic entity functions
//---
int game_count_entities(game_t const *g, entity_predicate_t *predicate)
{
int total = 0;
for(int i = 0; i < g->entity_count; i++)
total += (predicate(g->entities[i]) != 0);
return total;
}
/* Not re-entrant, but we can deal with that */
static entity_measure_t *gse_measure = NULL;
static entity_t **gse_entities = NULL;
static int gse_compare(const void *ptr1, const void *ptr2)
{
int i1 = *(uint16_t *)ptr1;
int i2 = *(uint16_t *)ptr2;
return gse_measure(gse_entities[i1]) - gse_measure(gse_entities[i2]);
}
int game_sort_entities(game_t const *g, entity_measure_t *measure,
uint16_t **result)
{
int count = 0;
*result = NULL;
for(int i = 0; i < g->entity_count; i++)
count += (measure(g->entities[i]) >= 0);
if(count == 0)
return 0;
/* Initialize array with matching entities in storage order */
uint16_t *array = malloc(count * sizeof *array);
*result = array;
if(array == NULL)
return -1;
for(int i=0, j=0; i < g->entity_count; i++) {
if(measure(g->entities[i]) >= 0)
array[j++] = i;
}
/* Sort and return */
gse_measure = measure;
gse_entities = g->entities;
qsort(array, count, sizeof *array, gse_compare);
return count;
}
#if 0
// TODO ECS: New collision/ejection system based on teleports
void game_try_move_entity(game_t *g, entity_t *e,
entity_movement_t const * next)
{
@ -249,6 +289,7 @@ void game_try_move_entity(game_t *g, entity_t *e,
m->dash = next->dash;
}
}
#endif
//---
// Per-frame update functions
@ -263,9 +304,12 @@ void game_spawn_enemies(game_t *g)
level_wave_spawn_t *s = &wave->enemies[i];
if(s->time > g->time_wave) break;
entity_t *e = enemy_spawn(s->identity, s->level);
vec2 pos = { fix(s->x) + fix(0.5), fix(s->y) + fix(0.5) };
game_spawn_entity(g, e, pos);
entity_t *e = enemy_make(s->identity, s->level);
physical_t *p = getcomp(e, physical);
p->x = fix(s->x) + fix(0.5);
p->y = fix(s->y) + fix(0.5);
game_spawn_entity(g, e);
g->wave_spawned++;
}
}
@ -274,37 +318,37 @@ void game_update_animations(game_t *g, fixed_t dt)
{
for(int i = 0; i < g->entity_count; i++) {
entity_t *e = g->entities[i];
anim_state_update(&e->anim, dt);
if(e->anim.frame == NULL)
e->anim_priority = 0;
}
for(int i = 0; i < g->effect_area_count; i++) {
effect_area_t *ea = g->effect_areas[i];
anim_state_update(&ea->anim, dt);
if(getcomp(e, visible))
visible_update(e, dt);
}
}
void game_update_effect_areas(game_t *g, fixed_t dt)
void game_update_aoes(game_t *g, fixed_t dt)
{
for(int i = 0; i < g->effect_area_count; i++) {
effect_area_t *ea = g->effect_areas[i];
for(int i = 0; i < g->entity_count; i++) {
entity_t *e = g->entities[i];
aoe_t *aoe = getcomp(e, aoe);
if(aoe == NULL)
continue;
/* Movement and collisions, when relevant */
effect_area_update(g, ea, dt);
aoe_update(g, e, dt);
rect hitbox = rect_translate(ea->sprite, ea->anchor);
rect hitbox = physical_abs_hitbox(e);
// TODO ECS: Move collisions in a proper system
// TODO ECS: Quadratic collision check is a no-no for high performance
for(int i = 0; i < g->entity_count; i++) {
entity_t *e = g->entities[i];
if(rect_collide(hitbox, entity_sprite(e)))
effect_area_apply(g, ea, e);
entity_t *target = g->entities[i];
physical_t *p = getcomp(target, physical);
if(p && rect_collide(hitbox, rect_translate(p->hitbox,
(vec2){ p->x, p->y })))
aoe_apply(g, e, target);
}
ea->lifetime -= dt;
aoe->lifetime -= dt;
}
game_remove_dead_effect_areas(g);
}
void game_update_particles(game_t *g, fixed_t dt)
@ -320,32 +364,18 @@ void game_update_particles(game_t *g, fixed_t dt)
/* Spawn dash particles */
for(int i = 0; i < g->entity_count; i++) {
entity_t *e = g->entities[i];
mechanical_t *m = getcomp(e, mechanical);
if(entity_dashing(e)) {
if(m && mechanical_dashing(e)) {
particle_dash_t *p = malloc(sizeof *p);
p->particle.type = PARTICLE_DASH;
p->particle.age = 0;
p->particle.pos = entity_pos(e);
p->particle.pos = physical_pos(e);
game_add_particle(g, &p->particle);
}
}
}
/* Compare the y values of two entities. */
static int game_sort_entities_compare(void const *p1, void const *p2)
{
entity_t const *e1 = *(entity_t const **)p1;
entity_t const *e2 = *(entity_t const **)p2;
return e1->movement.y - e2->movement.y;
}
void game_sort_entities(game_t *g)
{
heap_sort(g->entities, g->entity_count, sizeof *g->entities,
game_sort_entities_compare);
}
/* Compare the y values of two particles. */
static int game_sort_particles_compare(void const *v1, void const *v2)
{

View File

@ -5,12 +5,13 @@
#pragma once
#include "map.h"
#include "entities.h"
#include "render.h"
#include "level.h"
#include "pathfinding.h"
#include "particles.h"
#include "comp/entity.h"
typedef struct game {
/* The map's coordinate system is the primary coordinate system in all of
this game's code */
@ -27,16 +28,13 @@ typedef struct game {
int entity_count;
/* Player; this must be one of the entities loaded in the game */
entity_t *player;
/* List of effect areas */
effect_area_t **effect_areas;
int effect_area_count;
/* List of particles */
particle_t **particles;
int particle_count;
/* Field of movement to reach the player (used by most enemy AIs) */
pfg_all2one_t paths_to_player;
/* Level begin played */
/* Level being played */
level_t *level;
/* Current wave, number of enemies spawned in wave, time spent in wave */
int wave;
@ -58,31 +56,34 @@ level_wave_t const *game_current_wave(game_t const *g);
void game_next_wave(game_t *g);
//---
// Adding dynamic game elements
// Managing 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);
/* Add an effect area to the game (takes ownership; ea will be freed). */
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);
/* Like game_add_entity() at a specific position, but with a visual effect */
void game_spawn_entity(game_t *g, entity_t *e, vec2 pos);
/* Like game_add_entity(), but with a visual effect */
void game_spawn_entity(game_t *g, entity_t *e);
/* Remove dead entities. */
void game_remove_dead_entities(game_t *g);
//---
// Interacting with game elements
// Generic entity functions
//---
/* 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);
/* Count entities satisfying the provided predicate. */
int game_count_entities(game_t const *g, entity_predicate_t *predicate);
/* Filter and sort entities. The entities are sorted by increasing measure,
with negative measures filtering entities out. Returns an array of entity
indices (within [g->entities]) to be freed with free() in [*result], while
returning the number of matching entities. If 0, *result is NULL. */
int game_sort_entities(game_t const *g, entity_measure_t *measure,
uint16_t **result);
//---
// Per-frame update functions
@ -95,16 +96,10 @@ void game_spawn_enemies(game_t *g);
void game_update_animations(game_t *g, fixed_t dt);
/* Update all effect areas and apply their effects. */
void game_update_effect_areas(game_t *g, fixed_t dt);
void game_update_aoes(game_t *g, fixed_t dt);
/* Update all particles and remove the oldest ones. */
void game_update_particles(game_t *g, fixed_t dt);
/* Sort entities by increasing y position (which is rendering order). */
void game_sort_entities(game_t *g);
/* Sort particles by increasing y position */
void game_sort_particles(game_t *g);
/* Remove dead entities. */
void game_remove_dead_entities(game_t *g);

View File

@ -1,6 +1,12 @@
#include "comp/entity.h"
#include "comp/physical.h"
#include "comp/visible.h"
#include "comp/mechanical.h"
#include "comp/fighter.h"
#include "anim.h"
#include "aoe.h"
#include "enemies.h"
#include "entities.h"
#include "game.h"
#include "geometry.h"
#include "level.h"
@ -15,6 +21,7 @@
#include <gint/usb-ff-bulk.h>
#include <gint/cpu.h>
#include <gint/timer.h>
#include <gint/kmalloc.h>
#include <gint/drivers/r61524.h>
#include <stdlib.h>
@ -38,7 +45,7 @@ int main(void)
usb_open(interfaces, GINT_CALL_NULL);
game_t game = { 0 };
map_t *m = &game.map;
map_t *map = &game.map;
camera_t *c = &game.camera;
game_load(&game, &lv_1);
@ -65,48 +72,41 @@ int main(void)
// Spawn player
//---
entity_movement_params_t emp_player = {
mechanical_limits_t ml_player = {
.max_speed = fix(4.5),
.propulsion = fix(12),
.dash_speed = fix(55),
.dash_duration = fix(1) / 32,
.friction = fix(0.7),
.dash_speed = fix(20),
.dash_duration = fix(1) / 8,
.dash_cooldown = fix(1),
};
entity_player_t player_data = {
.combo_length = 2,
.combo_next = 0,
.combo_delay = fix(0),
};
entity_t *player = enemy_spawn(ENEMY_BAT, 1);
entity_player_t *playerd = &player_data;
player->player = playerd;
entity_t *player = enemy_make(ENEMY_BAT, 2);
physical_t *player_p = getcomp(player, physical);
visible_t *player_v = getcomp(player, visible);
mechanical_t *player_m = getcomp(player, mechanical);
fighter_t *player_f = getcomp(player, fighter);
int x=1, y=1;
for(int i = 0; i < 1000; i++) {
x = rand() % m->width;
y = rand() % m->height;
player_f->combo_length = 2;
player_f->combo_next = 0;
player_f->combo_delay = fix(0);
player_f->identity = 0;
player_f->HP_max += 100;
player_f->HP += 100;
struct tile *t = map_tile(m, x, y);
if(t && !t->solid) break;
}
player->movement.x = fix(x) + fix(0.5);
player->movement.y = fix(y) + fix(0.5);
player_p->x = fix(9.5);
player_p->y = fix(4.5);
player_p->hitbox = (rect){ -fix(5)/16, fix(5)/16, -fix(2)/16, fix(4)/16 };
player_m->limits = &ml_player;
player_v->z = 0;
player_v->shadow_size = 4;
visible_set_anim(player, &anims_player_Idle, 1);
game_add_entity(&game, player);
game.player = player;
player->HP_max += 100;
player->HP += 100;
player->movement_params = &emp_player;
player->identity = 0;
player->hitbox = (rect){
-fix(5)/16, fix(5)/16, -fix(2)/16, fix(4)/16 };
player->sprite = (rect){
-fix(6)/16, fix(5)/16, -fix(12)/16, fix(4)/16 };
entity_set_anim(player, anims_player_Idle, 1);
//---
// Main loop
//---
@ -137,6 +137,10 @@ int main(void)
render_game(&game, debug.show_hitboxes);
/* kmalloc_arena_t *_uram = kmalloc_get_arena("_uram");
kmalloc_gint_stats_t *_uram_stats = kmalloc_get_gint_stats(_uram);
dprint(1, 1, C_WHITE, "Memory: %d", _uram_stats->used_memory); */
/* Developer/tweaking menu */
if(debug.show_vars) {
uint32_t *vram = (void *)gint_vram;
@ -149,25 +153,23 @@ int main(void)
uint16_t gray = C_RGB(16, 16, 16);
dprint(3, 40, C_WHITE, "Player speed: %g tiles/s",
f2double(emp_player.max_speed));
f2double(ml_player.max_speed));
dprint(15, 55, gray, "[frac] -/+ [X,0,T]");
dprint(3, 70, C_WHITE, "Propulsion: %g s^-1",
f2double(emp_player.propulsion));
dprint(3, 70, C_WHITE, "Friction: %g",
f2double(ml_player.friction));
dprint(15, 85, gray, "[F<>D] -/+ [log]");
dprint(15, 100, C_WHITE, "(Friction: %g)",
f2double(fix(1) - emp_player.propulsion / FRAME_RATE));
dprint(3, 115, C_WHITE, "Dash speed: %g tiles/s",
f2double(emp_player.dash_speed));
f2double(ml_player.dash_speed));
dprint(15, 130, gray, "[(] -/+ [ln]");
dprint(3, 145, C_WHITE, "Dash duration: %g s",
f2double(emp_player.dash_duration));
f2double(ml_player.dash_duration));
dprint(15, 160, gray, "[)] -/+ [sin]");
dprint(3, 175, C_WHITE, "Dash cooldown: %g s",
f2double(emp_player.dash_cooldown));
f2double(ml_player.dash_cooldown));
dprint(15, 190, gray, "[,] -/+ [cos]");
}
@ -189,9 +191,9 @@ int main(void)
dline(j.x, j.y, k.x, k.y, C_RGB(0, 31, 31));
}
vec2 p = entity_pos(player);
vec2 p = physical_pos(player);
vec2 q = vec_i2f_center((ivec2){ 6, 9 });
bool clear = raycast_clear_hitbox(m, p, q, player->hitbox);
bool clear = raycast_clear_hitbox(map, p, q, player_p->hitbox);
ivec2 j = camera_map2screen(c, p);
ivec2 k = camera_map2screen(c, q);
@ -254,10 +256,6 @@ int main(void)
prof_enter(perf_simul);
game_spawn_enemies(&game);
game_update_animations(&game, dt);
game_sort_entities(&game);
game_sort_particles(&game);
key_event_t ev;
@ -282,46 +280,46 @@ int main(void)
debug.show_perf ^= 1;
if(ev.key == KEY_XOT)
emp_player.max_speed += fix(1)/8;
ml_player.max_speed += fix(1)/8;
if(ev.key == KEY_FRAC) {
emp_player.max_speed -= fix(1)/8;
if(emp_player.max_speed < 0)
emp_player.max_speed = 0;
ml_player.max_speed -= fix(1)/8;
if(ml_player.max_speed < 0)
ml_player.max_speed = 0;
}
if(ev.key == KEY_LOG) {
emp_player.propulsion += fix(1) / 8;
if(emp_player.propulsion > fix(FRAME_RATE))
emp_player.propulsion = fix(FRAME_RATE);
ml_player.friction += fix(1) / 32;
if(ml_player.friction > 1)
ml_player.friction = 1;
}
if(ev.key == KEY_FD) {
emp_player.propulsion -= fix(1) / 8;
if(emp_player.propulsion <= 0)
emp_player.propulsion = 0;
ml_player.friction -= fix(1) / 32;
if(ml_player.friction <= 0)
ml_player.friction = 0;
}
if(ev.key == KEY_LN)
emp_player.dash_speed += fix(0.5);
ml_player.dash_speed += fix(0.5);
if(ev.key == KEY_LEFTP) {
emp_player.dash_speed -= fix(0.5);
if(emp_player.dash_speed <= 0)
emp_player.dash_speed = 0;
ml_player.dash_speed -= fix(0.5);
if(ml_player.dash_speed <= 0)
ml_player.dash_speed = 0;
}
if(ev.key == KEY_SIN)
emp_player.dash_duration += fix(1) / 64;
ml_player.dash_duration += fix(1) / 64;
if(ev.key == KEY_RIGHTP) {
emp_player.dash_duration -= fix(1) / 64;
if(emp_player.dash_duration <= 0)
emp_player.dash_duration = 0;
ml_player.dash_duration -= fix(1) / 64;
if(ml_player.dash_duration <= 0)
ml_player.dash_duration = 0;
}
if(ev.key == KEY_COS)
emp_player.dash_cooldown += fix(1) / 8;
ml_player.dash_cooldown += fix(1) / 8;
if(ev.key == KEY_COMMA) {
emp_player.dash_cooldown -= fix(1) / 8;
if(emp_player.dash_cooldown <= 0)
emp_player.dash_cooldown = 0;
ml_player.dash_cooldown -= fix(1) / 8;
if(ml_player.dash_cooldown <= 0)
ml_player.dash_cooldown = 0;
}
if(ev.key == KEY_PLUS)
@ -348,7 +346,7 @@ int main(void)
#endif
/* Player movement */
if(player->HP > 0) {
if(player_f->HP > 0) {
int dir = -1;
if(keydown(KEY_UP)) dir = UP;
if(keydown(KEY_DOWN)) dir = DOWN;
@ -356,43 +354,43 @@ int main(void)
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);
int dash_dir = (dir >= 0) ? dir : player_p->facing;
mechanical_dash(player, dash_dir);
}
entity_movement_t next = entity_move4(player, dir, dt);
mechanical_move4(player, dir, dt, map);
bool set_anim = (player->movement.facing != next.facing) ||
((entity_moving(player) == 0) != (dir == -1)) ||
(player->anim.frame == NULL);
game_try_move_entity(&game, player, &next);
if(set_anim && dir >= 0)
entity_set_anim(player, anims_player_Walking, 1);
else if(set_anim && dir < 0)
entity_set_anim(player, anims_player_Idle, 1);
if(dir >= 0)
visible_set_anim(player, &anims_player_Walking, 1);
else
visible_set_anim(player, &anims_player_Idle, 1);
}
/* Directions to reach the player from anywhere on the grid */
pfg_all2one_free(&game.paths_to_player);
game.paths_to_player = pfg_bfs(m, vec_f2i(entity_pos(player)));
game.paths_to_player = pfg_bfs(map, vec_f2i(physical_pos(player)));
/* Enemy AI */
if(player->HP > 0) for(int i = 0; i < game.entity_count; i++) {
if(player_f->HP > 0)
for(int i = 0; i < game.entity_count; i++) {
entity_t *e = game.entities[i];
if(e == player || e->HP == 0) continue;
fighter_t *f = getcomp(e, fighter);
mechanical_t *m = getcomp(e, mechanical);
physical_t *p = getcomp(e, physical);
if(!f || !m || f->identity == 0 || f->HP == 0)
continue;
/* Go within 1 block of the player */
vec2 direction = { 0, 0 };
vec2 pos = entity_pos(e);
vec2 pos = physical_pos(e);
bool in_range = dist2(pos, entity_pos(player)) <= fix(1);
bool in_range = dist2(pos, physical_pos(player)) <= fix(1);
if(!in_range) {
pfg_path_t path = pfg_bfs_inwards(&game.paths_to_player,
vec_f2i(pos));
if(path.points) {
direction = pfc_shortcut_one(&path, pos,
entity_pos(player), e->hitbox);
physical_pos(player), getcomp(e, physical)->hitbox);
pfg_path_free(&path);
}
@ -402,88 +400,83 @@ int main(void)
}
}
entity_movement_t next = entity_move(e, direction, dt);
if(direction.x > 0) next.facing = RIGHT;
else if(direction.x < 0) next.facing = LEFT;
else if(e->movement.x < player->movement.x) next.facing = RIGHT;
else next.facing = LEFT;
mechanical_move(e, direction, dt, map);
if(direction.x > 0) p->facing = RIGHT;
else if(direction.x < 0) p->facing = LEFT;
else if(p->x < player_p->x) p->facing = RIGHT;
else p->facing = LEFT;
bool will_move = !in_range;
bool set_anim = (e->movement.facing != next.facing) ||
((entity_moving(e) == 0) != (will_move == false)) ||
(e->anim.frame == NULL);
if(mechanical_moving(e))
visible_set_anim(e, enemies[f->identity]->anim_walking, 1);
else
visible_set_anim(e, enemies[f->identity]->anim_idle, 1);
game_try_move_entity(&game, e, &next);
int id = e->movement.facing == RIGHT;
if(set_anim && will_move)
entity_set_anim(e, enemies[e->identity]->anim_walking[id], 1);
else if(set_anim && !will_move)
entity_set_anim(e, enemies[e->identity]->anim_idle[id], 1);
if(in_range && !e->current_attack) {
if(in_range && !f->current_attack) {
/* Enemy attack */
int facing = frdir((vec2){
player->movement.x - e->movement.x,
player->movement.y - e->movement.y });
player_p->x - p->x,
player_p->y - p->y });
effect_area_t *area = effect_area_new_attack(EFFECT_ATTACK_HIT,
e, facing);
game_add_effect_area(&game, area);
entity_t *aoe = aoe_make_attack(AOE_HIT, e, facing);
game_add_entity(&game, aoe);
e->current_attack = area;
e->attack_follows_movement = true;
f->current_attack = aoe;
f->attack_follows_movement = true;
}
}
/* Player attack */
if(player->HP > 0 && attack && !player->current_attack) {
int hit_number=0, effect=EFFECT_ATTACK_SLASH;
if(player_f->HP > 0 && attack && !player_f->current_attack) {
int hit_number=0, effect=AOE_SLASH;
/* If hitting within .25s of the previous hit ending, combo! */
if(abs(playerd->combo_delay) < fix(0.25))
hit_number = playerd->combo_next;
playerd->combo_next = (hit_number + 1) % playerd->combo_length;
if(abs(player_f->combo_delay) < fix(0.25))
hit_number = player_f->combo_next;
player_f->combo_next = (hit_number + 1) % player_f->combo_length;
if(hit_number == 0) effect = EFFECT_ATTACK_SLASH;
if(hit_number == 1) effect = EFFECT_ATTACK_IMPALE;
if(hit_number == 0) effect = AOE_SLASH;
if(hit_number == 1) effect = AOE_IMPALE;
entity_t *aoe = aoe_make_attack(effect, player, player_p->facing);
game_add_entity(&game, aoe);
effect_area_t *area = effect_area_new_attack(effect, player,
player->movement.facing);
game_add_effect_area(&game, area);
entity_set_anim(player, anims_player_Attack, 2);
player->current_attack = area;
player->attack_follows_movement = true;
playerd->combo_delay = area->lifetime;
visible_set_anim(player, &anims_player_Attack, 2);
player_f->current_attack = aoe;
player_f->attack_follows_movement = true;
player_f->combo_delay = getcomp(aoe, aoe)->lifetime;
}
/* Player skills */
if(player->HP > 0 && keydown(KEY_F2) && !player->current_attack) {
effect_area_t *area = effect_area_new_attack(EFFECT_ATTACK_SHOCK,
player, player->movement.facing);
game_add_effect_area(&game, area);
bool can_attack = player_f->HP > 0 && !player_f->current_attack &&
!keydown(KEY_VARS);
entity_set_anim(player, anims_player_Attack, 2);
player->current_attack = area;
player->attack_follows_movement = true;
}
if(player->HP > 0 && keydown(KEY_F3) && !player->current_attack) {
effect_area_t *area = effect_area_new_attack(
EFFECT_ATTACK_JUDGEMENT, player, player->movement.facing);
game_add_effect_area(&game, area);
if(can_attack && keydown(KEY_F2)) {
entity_t *aoe = aoe_make_attack(AOE_SHOCK, player,
player_p->facing);
game_add_entity(&game, aoe);
entity_set_anim(player, anims_player_Attack, 2);
player->current_attack = area;
player->attack_follows_movement = false;
visible_set_anim(player, &anims_player_Attack, 2);
player_f->current_attack = aoe;
player_f->attack_follows_movement = true;
}
if(player->HP > 0 && keydown(KEY_F4) && !player->current_attack) {
effect_area_t *area = effect_area_new_attack(
EFFECT_ATTACK_BULLET, player, player->movement.facing);
game_add_effect_area(&game, area);
if(can_attack && keydown(KEY_F3)) {
entity_t *aoe = aoe_make_attack( AOE_JUDGEMENT, player,
player_p->facing);
game_add_entity(&game, aoe);
entity_set_anim(player, anims_player_Attack, 2);
player->current_attack = area;
player->attack_follows_movement = false;
visible_set_anim(player, &anims_player_Attack, 2);
player_f->current_attack = aoe;
player_f->attack_follows_movement = false;
}
if(can_attack && keydown(KEY_F4)) {
entity_t *aoe = aoe_make_attack(AOE_BULLET, player,
player_p->facing);
game_add_entity(&game, aoe);
visible_set_anim(player, &anims_player_Attack, 2);
player_f->current_attack = aoe;
player_f->attack_follows_movement = false;
}
/* Ideas for additional skills:
- Freeze enemies
- Barrier around player
@ -496,18 +489,22 @@ int main(void)
- XP boosts
- Weaker but longer-lasting buffs */
/* Remove dead entities first as it will kill their attack areas */
game_remove_dead_entities(&game);
game_update_effect_areas(&game, dt);
game_update_animations(&game, dt);
game_update_aoes(&game, dt);
game_update_particles(&game, dt);
playerd->combo_delay -= dt;
game_remove_dead_entities(&game);
player_f->combo_delay -= dt;
/* Reset default anims */
if(!player_v->anim.frame)
visible_set_anim(player, &anims_player_Idle, 1);
game.time_total += dt;
game.time_wave += dt;
if(game.time_defeat == 0 && player->HP == 0)
if(game.time_defeat == 0 && player_f->HP == 0)
game.time_defeat = game.time_total;
if(game.time_victory == 0 && player->HP > 0 && game.entity_count == 1)
if(game.time_victory == 0 && player_f->HP > 0 && game.entity_count==1)
game.time_victory = game.time_total;
/* Next wave */
@ -524,7 +521,7 @@ int main(void)
debug.grid_path = pfg_bfs_outwards(&game.paths_to_player,
vec_f2i(target));
debug.continuous_path = pfc_shortcut_full(&debug.grid_path,
entity_pos(player), target, player->hitbox);
physical_pos(player), target, player_p->hitbox);
}
prof_leave(perf_simul);

View File

@ -1,7 +1,7 @@
#include "map.h"
#include <stdlib.h>
rect tile_shape(struct tile const *tile)
rect tile_shape(tile_t const *tile)
{
if(!tile->solid) return (rect){ 0 };
@ -13,7 +13,7 @@ rect tile_shape(struct tile const *tile)
};
}
struct tile *map_tile(map_t const *m, int x, int y)
tile_t *map_tile(map_t const *m, int x, int y)
{
if((unsigned)x >= (unsigned)m->width || (unsigned)y >= (unsigned)m->height)
return NULL;
@ -31,7 +31,7 @@ bool map_collides(map_t const *m, rect hitbox)
/* Collisions against walls and static objects */
for(int y = y_min; y < y_max; y++)
for(int x = x_min; x < x_max; x++) {
struct tile *t = map_tile(m, x, y);
tile_t *t = map_tile(m, x, y);
if(!t || !t->solid) continue;
vec2 center = { fix(x) + fix(0.5), fix(y) + fix(0.5) };

View File

@ -11,7 +11,6 @@
#include "fixed.h"
#include "map.h"
#include "geometry.h"
#include "entities.h"
#include <stdint.h>
#include <stdbool.h>
@ -21,7 +20,8 @@
// Tiles
//---
struct tile {
typedef struct
{
/* TODO: Layers of objects, stuff, dynamic elements, etc? */
/* TODO: Allow any collision shape for the tile! */
bool solid;
@ -29,27 +29,29 @@ struct tile {
uint8_t base;
/* Decoration layer */
uint8_t decor;
};
} tile_t;
/* Shape for a tile; this lives in a coordinate system whose (0,0) ends up at
the middle of the tile in the map space (which is a point with half-integer
coordinates) */
rect tile_shape(struct tile const *tile);
rect tile_shape(tile_t const *tile);
//---
// Map grid, tiles location in space, and entities
//---
typedef struct {
typedef struct
{
/* Dimensions, columns are 0 to width-1, rows are 0 to height-1 */
int width, height;
/* All tiles */
struct tile *tiles;
tile_t *tiles;
} map_t;
/* Get a pointer to the tile at (x,y) in map; NULL if out of bounds. */
struct tile *map_tile(map_t const *m, int x, int y);
tile_t *map_tile(map_t const *m, int x, int y);
/* Check whether a hitbox collides with the map. */
bool map_collides(map_t const *m, rect hitbox);

View File

View File

@ -66,7 +66,7 @@ pfg_all2one_t pfg_bfs(map_t const *map, ivec2 center)
int dx = dx_array[dir];
int dy = dy_array[dir];
struct tile *tile = map_tile(map, point.x+dx, point.y+dy);
tile_t *tile = map_tile(map, point.x+dx, point.y+dy);
if(!tile || tile->solid) continue;
int next_i = idx(point.x+dx, point.y+dy);
@ -159,7 +159,7 @@ bool raycast_clear(map_t const *map, vec2 start, vec2 end)
perfectly */
int current_x = ffloor(x-(u.x < 0));
int current_y = ffloor(y-(u.y < 0));
struct tile *tile = map_tile(map, current_x, current_y);
tile_t *tile = map_tile(map, current_x, current_y);
if(tile && tile->solid) return false;
if(raycast_clear_points < 64) {
@ -185,7 +185,7 @@ bool raycast_clear(map_t const *map, vec2 start, vec2 end)
int next_x = ffloor(x-(u.x < 0));
int next_y = ffloor(y-(u.y < 0)) + (u.y >= 0 ? 1 : -1);
struct tile *tile = map_tile(map, next_x, next_y);
tile_t *tile = map_tile(map, next_x, next_y);
if(tile && tile->solid) return false;
}
else {
@ -195,7 +195,7 @@ bool raycast_clear(map_t const *map, vec2 start, vec2 end)
int next_x = ffloor(x-(u.x < 0)) + (u.x >= 0 ? 1 : -1);
int next_y = ffloor(y-(u.y < 0));
struct tile *tile = map_tile(map, next_x, next_y);
tile_t *tile = map_tile(map, next_x, next_y);
if(tile && tile->solid) return false;
}
}

View File

@ -1,8 +1,14 @@
#include "comp/entity.h"
#include "comp/physical.h"
#include "comp/visible.h"
#include "comp/mechanical.h"
#include "comp/fighter.h"
#include "render.h"
#include "game.h"
#include "anim.h"
#include <gint/display.h>
#include <gint/defs/util.h>
//---
// Camera management
@ -111,7 +117,7 @@ fixed_t camera_ppu(camera_t const *c)
// Rendering
//---
void render_map(map_t const *m, camera_t const *c)
static void render_map_layer(map_t const *m, camera_t const *c, int layer)
{
extern bopti_image_t img_tileset_base;
extern bopti_image_t img_tileset_decor;
@ -119,7 +125,7 @@ void render_map(map_t const *m, camera_t const *c)
/* Render floor and walls */
for(int row = -2; row < m->height + 2; row++)
for(int col = -1; col < m->width + 1; col++) {
struct tile *t = map_tile(m, col, row);
tile_t *t = map_tile(m, col, row);
vec2 tile_pos = { fix(col), fix(row) };
ivec2 p = camera_map2screen(c, tile_pos);
@ -128,7 +134,12 @@ void render_map(map_t const *m, camera_t const *c)
continue;
}
/* Floor/wall layer */
bool will_draw = false;
if(layer == 0) will_draw = (t->base < 16); // Floor
if(layer == 1) will_draw = (t->base >= 16); // Walls
if(!will_draw)
continue;
dsubimage(p.x, p.y, &img_tileset_base,
TILE_WIDTH * (t->base % 16), TILE_HEIGHT * (t->base / 16),
TILE_WIDTH, TILE_HEIGHT, DIMAGE_NOCLIP);
@ -139,82 +150,128 @@ void render_map(map_t const *m, camera_t const *c)
}
}
static void render_effect_area(effect_area_t const *ea, camera_t const *c,
bool show_hitboxes)
static int depth_measure(entity_t const *e, int direction)
{
ivec2 anchor = camera_map2screen(c, ea->anchor);
visible_t *v = getcomp(e, visible);
if(v == NULL || v->sprite_plane != direction)
return -1;
return getcomp(e, physical)->y;
}
static int floor_depth_measure(entity_t const *e)
{
return depth_measure(e, HORIZONTAL);
}
static int wall_depth_measure(entity_t const *e)
{
return depth_measure(e, VERTICAL);
}
if(ea->anim.frame) {
ivec2 p = camera_map2screen(c, ea->anchor);
anim_frame_render(p.x, p.y, ea->anim.frame);
static void render_shadow(int cx, int cy, int shadow_size)
{
if(shadow_size < 1 || shadow_size > 4)
return;
static const int hw_array[] = { 2, 3, 5, 7 };
static const int hh_array[] = { 1, 1, 2, 3 };
/* TODO: render_shadow(): Encode shadow shapes properly x) */
static const uint8_t profile_1[] = { 1, 0, 1 };
static const uint8_t profile_2[] = { 1, 0, 1 };
static const uint8_t profile_3[] = { 3, 1, 0, 1, 3 };
static const uint8_t profile_4[] = { 4, 2, 1, 0, 1, 2, 4 };
static const uint8_t *profile_array[] = {
profile_1, profile_2, profile_3, profile_4,
};
int hw = hw_array[shadow_size - 1];
int hh = hh_array[shadow_size - 1];
uint8_t const *profile = profile_array[shadow_size - 1] + hh;
int xmin = max(-hw, -cx);
int xmax = min(hw, DWIDTH-1 - cx);
int ymin = max(-hh, -cy);
int ymax = min(hh, DHEIGHT-1 - cy);
for(int y = ymin; y <= ymax; y++)
for(int x = xmin; x <= xmax; x++) {
if(hw - abs(x) < profile[y])
continue;
int i = DWIDTH * (cy+y) + (cx+x);
gint_vram[i] = (gint_vram[i] & 0xf7de) >> 1;
}
if(!ea->anim.frame || show_hitboxes) {
rect r = ea->sprite;
r = rect_scale(r, camera_ppu(c));
r = rect_translate(r, vec_i2f(anchor));
rect_draw(r, C_RGB(31, 31, 0));
}
static void render_entities(game_t const *g, camera_t const *camera,
entity_measure_t *measure, bool show_hitboxes)
{
uint16_t *rendering_order;
int count = game_sort_entities(g, measure, &rendering_order);
for(int i = 0; i < count; i++) {
entity_t *e = g->entities[rendering_order[i]];
physical_t *p = getcomp(e, physical);
visible_t *v = getcomp(e, visible);
ivec2 scr = camera_map2screen(camera, physical_pos(e));
int elevated_y = scr.y - fround(16 * v->z);
/* Show shadow */
if(v->shadow_size) {
render_shadow(scr.x, scr.y, v->shadow_size);
}
/* Show entity sprite */
if(v->anim.frame) {
anim_frame_render(scr.x, elevated_y, v->anim.frame);
}
/* Show entity hitbox in the map coordinate system */
if(!v->anim.frame || show_hitboxes) {
rect r = p->hitbox;
r = rect_scale(r, camera_ppu(camera));
r = rect_translate(r, vec_i2f(scr));
rect_draw(r, C_BLUE);
/* Show entity center */
dline(scr.x-1, scr.y, scr.x+1, scr.y, C_BLUE);
dline(scr.x, scr.y-1, scr.x, scr.y+1, C_BLUE);
}
}
free(rendering_order);
}
void render_game(game_t const *g, bool show_hitboxes)
{
camera_t const *c = &g->camera;
camera_t const *camera = &g->camera;
render_map(&g->map, &g->camera);
/* Render map floor */
render_map_layer(&g->map, camera, 0);
/* Render background particles */
/* Render background particles
TODO ECS: Use ECS entities for particles */
for(int i = 0; i < g->particle_count; i++) {
particle_t *p = g->particles[i];
if(!particle_is_background(p)) continue;
ivec2 center = camera_map2screen(c, p->pos);
ivec2 center = camera_map2screen(camera, p->pos);
particle_render(center.x, center.y, p);
}
/* Render background effect areas */
for(int i = 0; i < g->effect_area_count; i++) {
if(!effect_area_is_background(g->effect_areas[i])) continue;
render_effect_area(g->effect_areas[i], c, show_hitboxes);
}
/* Render floor entities */
render_entities(g, camera, floor_depth_measure, show_hitboxes);
/* Render entities */
for(int i = 0; i < g->entity_count; i++) {
entity_t *e = g->entities[i];
/* Render map walls */
render_map_layer(&g->map, camera, 1);
ivec2 p = camera_map2screen(c, entity_pos(e));
/* Render wall entities
TODO ECS: Sort walls along wall entities! */
render_entities(g, camera, wall_depth_measure, show_hitboxes);
/* Show entity sprite */
if(e->anim.frame) {
anim_frame_render(p.x, p.y, e->anim.frame);
}
/* Show entity hitbox in the map coordinate system */
if(!e->anim.frame || show_hitboxes) {
rect r = e->hitbox;
r = rect_scale(r, camera_ppu(c));
r = rect_translate(r, vec_i2f(p));
rect_draw(r, C_BLUE);
r = e->sprite;
r = rect_scale(r, camera_ppu(c));
r = rect_translate(r, vec_i2f(p));
rect_draw(r, C_RGB(12, 12, 12));
/* Show entity center */
dline(p.x-1, p.y, p.x+1, p.y, C_BLUE);
dline(p.x, p.y-1, p.x, p.y+1, C_BLUE);
}
}
/* Render background effect areas */
for(int i = 0; i < g->effect_area_count; i++) {
if(effect_area_is_background(g->effect_areas[i])) continue;
render_effect_area(g->effect_areas[i], c, show_hitboxes);
}
/* Render foreground particles */
/* Render foreground particles
TODO ECS: Use ECS entities for particles */
for(int i = 0; i < g->particle_count; i++) {
particle_t *p = g->particles[i];
if(particle_is_background(p)) continue;
ivec2 center = camera_map2screen(c, p->pos);
ivec2 center = camera_map2screen(camera, p->pos);
particle_render(center.x, center.y, p);
}
@ -223,8 +280,10 @@ void render_game(game_t const *g, bool show_hitboxes)
/* Render wave progress bar */
int enemies_left = 0;
for(int i = 0; i < g->entity_count; i++)
enemies_left += (g->entities[i]->identity != 0);
for(int i = 0; i < g->entity_count; i++) {
fighter_t *f = getcomp(g->entities[i], fighter);
enemies_left += (f && f->identity != 0);
}
if(g->wave_spawned < game_current_wave(g)->enemy_count)
dprint_opt(DWIDTH/2, 2, C_WHITE, C_NONE, DTEXT_CENTER, DTEXT_TOP,
@ -241,10 +300,12 @@ void render_game(game_t const *g, bool show_hitboxes)
extern bopti_image_t img_hud;
dimage(0, DHEIGHT - img_hud.height, &img_hud);
fighter_t *player_f = getcomp(g->player, fighter);
mechanical_t *player_m = getcomp(g->player, mechanical);
/* Render life bar */
extern bopti_image_t img_hud_life;
entity_t const *player = g->player;
int fill_height = (img_hud_life.height * player->HP) / player->HP_max;
int fill_height = (img_hud_life.height * player_f->HP) / player_f->HP_max;
dsubimage(184, DHEIGHT - 5 - fill_height, &img_hud_life,
0, img_hud_life.height - fill_height, img_hud_life.width, fill_height,
DIMAGE_NONE);
@ -254,13 +315,13 @@ void render_game(game_t const *g, bool show_hitboxes)
for(int i = 0; i < 6; i++) {
/* Activity and cooldown */
fixed_t cooldown_total=0, cooldown_remaining=0;
if(i == 0 && player->movement.dash < 0) {
cooldown_total = player->movement_params->dash_cooldown;
cooldown_remaining = -player->movement.dash;
if(i == 0 && player_m->dash < 0) {
cooldown_total = player_m->limits->dash_cooldown;
cooldown_remaining = -player_m->dash;
}
else if(i > 0) {
cooldown_total = player->player->actions_cooldown_total[i-1];
cooldown_remaining = player->player->actions_cooldown[i-1];
cooldown_total = player_f->actions_cooldown_total[i-1];
cooldown_remaining = player_f->actions_cooldown[i-1];
}
int x = 33 + 44*i + 84*(i>=3);
@ -288,7 +349,7 @@ void render_pfg_all2one(pfg_all2one_t const *paths, camera_t const *c)
{
for(int row = 0; row < paths->map->height; row++)
for(int col = 0; col < paths->map->width; col++) {
struct tile *tile = map_tile(paths->map, col, row);
tile_t *tile = map_tile(paths->map, col, row);
if(!tile || tile->solid) continue;
vec2 fp = vec_i2f_center((ivec2){ col, row });

View File

@ -56,11 +56,6 @@ fixed_t camera_ppu(camera_t const *c);
// Rendering
//---
/* Render map for the specified camera.
TODO: render_map: Honor zoom
TODO: render_map: Clip around viewport */
void render_map(map_t const *m, camera_t const *c);
/* Render game full-screen. */
struct game;
void render_game(struct game const *g, bool show_hitboxes);