Dengine/src/WorldTraveller.c
Doyle Thai a43102ee26 Rotate targetting reticule to mob. Add pivot point
Notion of pivot point now for rotating, i.e. the anchor point at which
rotations are applied. Rotating the reticule is done by using inverse
trigonometry, namely atan2f.
2016-07-23 21:26:15 +10:00

1112 lines
33 KiB
C

#include "WorldTraveller/WorldTraveller.h"
#include "Dengine/Debug.h"
#include "Dengine/Platform.h"
enum State
{
state_active,
state_menu,
state_win,
};
INTERNAL Entity *getHeroEntity(World *world)
{
Entity *result = &world->entities[world->heroIndex];
return result;
}
INTERNAL Entity *addEntity(MemoryArena *arena, World *world, v2 pos, v2 size,
enum EntityType type, enum Direction direction,
Texture *tex, b32 collides)
{
#ifdef DENGINE_DEBUG
ASSERT(tex && world);
ASSERT(world->freeEntityIndex < world->maxEntities);
ASSERT(type < entitytype_count);
#endif
Entity entity = {0};
entity.id = world->uniqueIdAccumulator++;
entity.pos = pos;
entity.hitboxSize = size;
entity.renderSize = size;
entity.type = type;
entity.direction = direction;
entity.tex = tex;
entity.collides = collides;
switch(type)
{
case entitytype_hero:
entity.stats = PLATFORM_MEM_ALLOC(arena, 1, EntityStats);
entity.stats->maxHealth = 100;
entity.stats->health = entity.stats->maxHealth;
entity.stats->actionRate = 100;
entity.stats->actionTimer = entity.stats->actionRate;
entity.stats->actionSpdMul = 100;
entity.stats->entityIdToAttack = -1;
entity.stats->queuedAttack = entityattack_invalid;
entity.state = entitystate_idle;
break;
case entitytype_mob:
{
entity.stats = PLATFORM_MEM_ALLOC(arena, 1, EntityStats);
entity.stats->maxHealth = 100;
entity.stats->health = entity.stats->maxHealth;
entity.stats->actionRate = 100;
entity.stats->actionTimer = entity.stats->actionRate;
entity.stats->actionSpdMul = 100;
entity.stats->entityIdToAttack = -1;
entity.stats->queuedAttack = entityattack_invalid;
entity.state = entitystate_idle;
break;
}
default:
break;
}
world->entities[world->freeEntityIndex++] = entity;
Entity *result = &world->entities[world->freeEntityIndex-1];
return result;
}
INTERNAL void deleteEntity(MemoryArena *arena, World *world, i32 entityIndex)
{
Entity *entity = &world->entities[entityIndex];
PLATFORM_MEM_FREE(arena, entity->stats, sizeof(EntityStats));
// TODO(doyle): Inefficient shuffle down all elements
for (i32 i = entityIndex; i < world->freeEntityIndex-1; i++)
world->entities[i] = world->entities[i+1];
world->freeEntityIndex--;
}
INTERNAL void rendererInit(GameState *state, v2 windowSize)
{
AssetManager *assetManager = &state->assetManager;
Renderer *renderer = &state->renderer;
renderer->size = windowSize;
// NOTE(doyle): Value to map a screen coordinate to NDC coordinate
renderer->vertexNdcFactor =
V2(1.0f / renderer->size.w, 1.0f / renderer->size.h);
renderer->shader = asset_getShader(assetManager, shaderlist_sprite);
shader_use(renderer->shader);
const mat4 projection =
mat4_ortho(0.0f, renderer->size.w, 0.0f, renderer->size.h, 0.0f, 1.0f);
shader_uniformSetMat4fv(renderer->shader, "projection", projection);
glCheckError();
/* Create buffers */
glGenVertexArrays(1, &renderer->vao);
glGenBuffers(1, &renderer->vbo);
glCheckError();
/* Bind buffers */
glBindBuffer(GL_ARRAY_BUFFER, renderer->vbo);
glBindVertexArray(renderer->vao);
/* Configure VAO */
const GLuint numVertexElements = 4;
const GLuint vertexSize = sizeof(v4);
glEnableVertexAttribArray(0);
glVertexAttribPointer(0, numVertexElements, GL_FLOAT, GL_FALSE, vertexSize,
(GLvoid *)0);
glCheckError();
/* Unbind */
glBindBuffer(GL_ARRAY_BUFFER, 0);
glBindVertexArray(0);
glCheckError();
}
INTERNAL void addAnim(AssetManager *assetManager, i32 animId, Entity *entity)
{
Animation *anim = asset_getAnim(assetManager, animId);
entity->anim[animId].anim = anim;
entity->anim[animId].currFrame = 0;
entity->anim[animId].currDuration = anim->frameDuration;
}
INTERNAL void addGenericMob(MemoryArena *arena, AssetManager *assetManager,
World *world, v2 pos)
{
#ifdef DENGINE_DEBUG
DEBUG_LOG("Mob entity spawned");
#endif
Entity *hero = &world->entities[world->heroIndex];
v2 size = V2(58.0f, 98.0f);
enum EntityType type = entitytype_mob;
enum Direction dir = direction_west;
Texture *tex = asset_getTexture(assetManager, texlist_hero);
b32 collides = TRUE;
Entity *mob = addEntity(arena, world, pos, size, type, dir, tex, collides);
/* Populate mob animation references */
addAnim(assetManager, animlist_hero_idle, mob);
addAnim(assetManager, animlist_hero_walk, mob);
addAnim(assetManager, animlist_hero_wave, mob);
addAnim(assetManager, animlist_hero_battlePose, mob);
addAnim(assetManager, animlist_hero_tackle, mob);
mob->currAnimId = animlist_hero_idle;
}
// TODO(doyle): Remove and implement own random generator!
#include <time.h>
#include <stdlib.h>
void worldTraveller_gameInit(GameState *state, v2 windowSize)
{
AssetManager *assetManager = &state->assetManager;
MemoryArena *arena = &state->arena;
/* Initialise assets */
/* Create empty 1x1 4bpp black texture */
u32 bitmap = (0xFF << 24) | (0xFF << 16) | (0xFF << 8) | (0xFF << 0);
Texture emptyTex = texture_gen(1, 1, 4, CAST(u8 *)(&bitmap));
assetManager->textures[texlist_empty] = emptyTex;
/* Load textures */
asset_loadTextureImage(assetManager,
"data/textures/WorldTraveller/TerraSprite1024.png",
texlist_hero);
TexAtlas *heroAtlas = asset_getTextureAtlas(assetManager, texlist_hero);
heroAtlas->texRect[herorects_idle] = V4(746, 1018, 804, 920);
heroAtlas->texRect[herorects_walkA] = V4(641, 1018, 699, 920);
heroAtlas->texRect[herorects_walkB] = V4(849, 1018, 904, 920);
heroAtlas->texRect[herorects_head] = V4(108, 1024, 159, 975);
heroAtlas->texRect[herorects_waveA] = V4(944, 918, 1010, 816);
heroAtlas->texRect[herorects_waveB] = V4(944, 812, 1010, 710);
heroAtlas->texRect[herorects_battlePose] = V4(8, 910, 71, 814);
heroAtlas->texRect[herorects_castA] = V4(428, 910, 493, 814);
heroAtlas->texRect[herorects_castB] = V4(525, 919, 590, 816);
heroAtlas->texRect[herorects_castC] = V4(640, 916, 698, 816);
asset_loadTextureImage(assetManager,
"data/textures/WorldTraveller/Terrain.png",
texlist_terrain);
TexAtlas *terrainAtlas =
asset_getTextureAtlas(assetManager, texlist_terrain);
f32 atlasTileSize = 128.0f;
const i32 texSize = 1024;
v2 texOrigin = V2(0, CAST(f32)(texSize - 128));
terrainAtlas->texRect[terrainrects_ground] =
V4(texOrigin.x, texOrigin.y, texOrigin.x + atlasTileSize,
texOrigin.y - atlasTileSize);
/* Load shaders */
asset_loadShaderFiles(assetManager, arena, "data/shaders/sprite.vert.glsl",
"data/shaders/sprite.frag.glsl",
shaderlist_sprite);
asset_loadTTFont(assetManager, arena, "C:/Windows/Fonts/Arialbd.ttf");
glCheckError();
#ifdef DENGINE_DEBUG
DEBUG_LOG("Assets loaded");
#endif
/* Load animations */
f32 duration = 1.0f;
i32 numRects = 1;
v4 *animRects = PLATFORM_MEM_ALLOC(arena, numRects, v4);
i32 terrainAnimAtlasIndexes[1] = {terrainrects_ground};
// TODO(doyle): Optimise animation storage, we waste space having 1:1 with
// animlist when some textures don't have certain animations
asset_addAnimation(assetManager, arena, texlist_terrain, animlist_terrain,
terrainAnimAtlasIndexes, numRects, duration);
// Idle animation
duration = 1.0f;
numRects = 1;
i32 idleAnimAtlasIndexes[1] = {herorects_idle};
asset_addAnimation(assetManager, arena, texlist_hero, animlist_hero_idle,
idleAnimAtlasIndexes, numRects, duration);
// Walk animation
duration = 0.10f;
numRects = 3;
i32 walkAnimAtlasIndexes[3] = {herorects_walkA, herorects_idle,
herorects_walkB};
asset_addAnimation(assetManager, arena, texlist_hero, animlist_hero_walk,
walkAnimAtlasIndexes, numRects, duration);
// Wave animation
duration = 0.30f;
numRects = 2;
i32 waveAnimAtlasIndexes[2] = {herorects_waveA, herorects_waveB};
asset_addAnimation(assetManager, arena, texlist_hero, animlist_hero_wave,
waveAnimAtlasIndexes, numRects, duration);
// Battle Stance animation
duration = 1.0f;
numRects = 1;
i32 battleStanceAnimAtlasIndexes[1] = {herorects_battlePose};
asset_addAnimation(assetManager, arena, texlist_hero, animlist_hero_battlePose,
battleStanceAnimAtlasIndexes, numRects, duration);
// Battle tackle animation
duration = 0.15f;
numRects = 3;
i32 tackleAnimAtlasIndexes[3] = {herorects_castA, herorects_castB,
herorects_castC};
asset_addAnimation(assetManager, arena, texlist_hero, animlist_hero_tackle,
tackleAnimAtlasIndexes, numRects, duration);
#ifdef DENGINE_DEBUG
DEBUG_LOG("Animations created");
#endif
state->state = state_active;
state->currWorldIndex = 0;
state->tileSize = 64;
/* Init renderer */
rendererInit(state, windowSize);
#ifdef DENGINE_DEBUG
DEBUG_LOG("Renderer initialised");
#endif
/* Init world */
const i32 targetWorldWidth = 100 * METERS_TO_PIXEL;
const i32 targetWorldHeight = 10 * METERS_TO_PIXEL;
v2 worldDimensionInTiles = V2i(targetWorldWidth / state->tileSize,
targetWorldHeight / state->tileSize);
for (i32 i = 0; i < ARRAY_COUNT(state->world); i++)
{
World *const world = &state->world[i];
world->maxEntities = 16384;
world->entities = PLATFORM_MEM_ALLOC(arena, world->maxEntities, Entity);
world->entityIdInBattle =
PLATFORM_MEM_ALLOC(arena, world->maxEntities, i32);
world->numEntitiesInBattle = 0;
world->texType = texlist_terrain;
world->bounds =
math_getRect(V2(0, 0), v2_scale(worldDimensionInTiles,
CAST(f32) state->tileSize));
world->uniqueIdAccumulator = 0;
TexAtlas *const atlas =
asset_getTextureAtlas(assetManager, world->texType);
for (i32 y = 0; y < worldDimensionInTiles.y; y++)
{
for (i32 x = 0; x < worldDimensionInTiles.x; x++)
{
#ifdef DENGINE_DEBUG
ASSERT(worldDimensionInTiles.x * worldDimensionInTiles.y <
world->maxEntities);
#endif
v2 pos = V2(CAST(f32) x * state->tileSize,
CAST(f32) y * state->tileSize);
v2 size =
V2(CAST(f32) state->tileSize, CAST(f32) state->tileSize);
enum EntityType type = entitytype_tile;
enum Direction dir = direction_null;
Texture *tex = asset_getTexture(assetManager, world->texType);
b32 collides = FALSE;
Entity *tile = addEntity(arena, world, pos, size, type, dir,
tex, collides);
addAnim(assetManager, animlist_terrain, tile);
tile->currAnimId = animlist_terrain;
}
}
}
World *const world = &state->world[state->currWorldIndex];
world->cameraPos = V2(0.0f, 0.0f);
/* Init hero entity */
world->heroIndex = world->freeEntityIndex;
Renderer *renderer = &state->renderer;
v2 size = V2(58.0f, 98.0f);
v2 pos = V2(size.x, CAST(f32) state->tileSize);
enum EntityType type = entitytype_hero;
enum Direction dir = direction_east;
Texture *tex = asset_getTexture(assetManager, texlist_hero);
b32 collides = TRUE;
Entity *hero = addEntity(arena, world, pos, size, type, dir, tex, collides);
/* Populate hero animation references */
addAnim(assetManager, animlist_hero_idle, hero);
addAnim(assetManager, animlist_hero_walk, hero);
addAnim(assetManager, animlist_hero_wave, hero);
addAnim(assetManager, animlist_hero_battlePose, hero);
addAnim(assetManager, animlist_hero_tackle, hero);
hero->currAnimId = animlist_hero_idle;
/* Create a NPC */
pos = V2(hero->pos.x * 3, CAST(f32) state->tileSize);
size = hero->hitboxSize;
type = entitytype_npc;
dir = direction_null;
tex = hero->tex;
collides = FALSE;
Entity *npc = addEntity(arena, world, pos, size, type, dir, tex, collides);
/* Populate npc animation references */
addAnim(assetManager, animlist_hero_wave, npc);
npc->currAnimId = animlist_hero_wave;
/* Create a Mob */
pos = V2(renderer->size.w - (renderer->size.w / 3.0f),
CAST(f32) state->tileSize);
addGenericMob(arena, assetManager, world, pos);
#ifdef DENGINE_DEBUG
DEBUG_LOG("World populated");
#endif
srand(CAST(u32)(time(NULL)));
}
INTERNAL inline void setActiveEntityAnim(Entity *entity,
enum AnimList animId)
{
#ifdef DENGINE_DEBUG
ASSERT(animId < animlist_count);
ASSERT(entity->anim[animId].anim);
#endif
/* Reset current anim data */
EntityAnim_ *currAnim = &entity->anim[entity->currAnimId];
currAnim->currDuration = currAnim->anim->frameDuration;
currAnim->currFrame = 0;
/* Set entity active animation */
entity->currAnimId = animId;
}
INTERNAL inline v4 getEntityScreenRect(Entity entity)
{
v4 result = math_getRect(entity.pos, entity.hitboxSize);
return result;
}
INTERNAL void parseInput(GameState *state, const f32 dt)
{
/*
Equations of Motion
f(t) = position m
f'(t) = velocity m/s
f"(t) = acceleration m/s^2
The user supplies an acceleration, a, and by integrating
f"(t) = a, where a is a constant, acceleration
f'(t) = a*t + v, where v is a constant, old velocity
f (t) = (a/2)*t^2 + v*t + p, where p is a constant, old position
*/
World *const world = &state->world[state->currWorldIndex];
Entity *hero = &world->entities[world->heroIndex];
v2 ddPos = V2(0, 0);
if (hero->stats->busyDuration <= 0)
{
// TODO(doyle): As we need to handle more key spam input, we want to
// track
// if a button ended down
LOCAL_PERSIST b32 spaceBarWasDown = FALSE;
if (state->keys[GLFW_KEY_RIGHT])
{
ddPos.x = 1.0f;
hero->direction = direction_east;
}
if (state->keys[GLFW_KEY_LEFT])
{
ddPos.x = -1.0f;
hero->direction = direction_west;
}
if (state->keys[GLFW_KEY_UP])
{
ddPos.y = 1.0f;
}
if (state->keys[GLFW_KEY_DOWN])
{
ddPos.y = -1.0f;
}
if (ddPos.x != 0.0f && ddPos.y != 0.0f)
{
// NOTE(doyle): Cheese it and pre-compute the vector for diagonal
// using
// pythagoras theorem on a unit triangle
// 1^2 + 1^2 = c^2
ddPos = v2_scale(ddPos, 0.70710678118f);
}
// TODO(doyle): Revisit key input with state checking for last ended down
if (state->keys[GLFW_KEY_SPACE])
{
Renderer *renderer = &state->renderer;
f32 yPos = CAST(f32)(rand() % CAST(i32)renderer->size.h);
f32 xModifier = 5.0f - CAST(f32)(rand() % 3);
v2 pos = V2(renderer->size.w - (renderer->size.w / xModifier), yPos);
addGenericMob(&state->arena, &state->assetManager, world, pos);
spaceBarWasDown = TRUE;
}
}
// NOTE(doyle): Clipping threshold for snapping velocity to 0
f32 epsilon = 0.5f;
v2 epsilonDpos = v2_sub(V2(epsilon, epsilon),
V2(ABS(hero->dPos.x), ABS(hero->dPos.y)));
if (epsilonDpos.x >= 0.0f && epsilonDpos.y >= 0.0f)
{
hero->dPos = V2(0.0f, 0.0f);
if (hero->currAnimId == animlist_hero_walk)
{
setActiveEntityAnim(hero, animlist_hero_idle);
}
}
else if (hero->currAnimId == animlist_hero_idle)
{
setActiveEntityAnim(hero, animlist_hero_walk);
}
f32 heroSpeed = 6.2f * METERS_TO_PIXEL;
if (state->keys[GLFW_KEY_LEFT_SHIFT])
heroSpeed = CAST(f32)(22.0f * 10.0f * METERS_TO_PIXEL);
ddPos = v2_scale(ddPos, heroSpeed);
// TODO(doyle): Counteracting force on player's acceleration is arbitrary
ddPos = v2_sub(ddPos, v2_scale(hero->dPos, 5.5f));
/*
NOTE(doyle): Calculate new position from acceleration with old velocity
new Position = (a/2) * (t^2) + (v*t) + p,
acceleration = (a/2) * (t^2)
old velocity = (v*t)
*/
v2 ddPosNew = v2_scale(v2_scale(ddPos, 0.5f), SQUARED(dt));
v2 dPos = v2_scale(hero->dPos, dt);
v2 newHeroP = v2_add(v2_add(ddPosNew, dPos), hero->pos);
// TODO(doyle): Only check collision for entities within small bounding box
// of the hero
b32 heroCollided = FALSE;
if (hero->collides == TRUE)
{
for (i32 i = 0; i < world->maxEntities; i++)
{
if (i == world->heroIndex) continue;
Entity entity = world->entities[i];
if (entity.state == entitystate_dead) continue;
if (entity.collides)
{
v4 heroRect =
V4(newHeroP.x, newHeroP.y, (newHeroP.x + hero->hitboxSize.x),
(newHeroP.y + hero->hitboxSize.y));
v4 entityRect = getEntityScreenRect(entity);
if (((heroRect.z >= entityRect.x && heroRect.z <= entityRect.z) ||
(heroRect.x >= entityRect.x && heroRect.x <= entityRect.z)) &&
((heroRect.w <= entityRect.y && heroRect.w >= entityRect.w) ||
(heroRect.y <= entityRect.y && heroRect.y >= entityRect.w)))
{
heroCollided = TRUE;
break;
}
}
}
}
if (heroCollided)
{
hero->dPos = V2(0.0f, 0.0f);
}
else
{
// f'(t) = curr velocity = a*t + v, where v is old velocity
hero->dPos = v2_add(hero->dPos, v2_scale(ddPos, dt));
hero->pos = newHeroP;
v2 offsetFromHeroToOrigin =
V2((hero->pos.x - (0.5f * state->renderer.size.w)), (0.0f));
// NOTE(doyle): Hero position is offset to the center so -recenter it
offsetFromHeroToOrigin.x += (hero->hitboxSize.x * 0.5f);
world->cameraPos = offsetFromHeroToOrigin;
}
}
INTERNAL void updateEntityAnim(Entity *entity, f32 dt)
{
// TODO(doyle): Recheck why we have this twice
EntityAnim_ *entityAnim = &entity->anim[entity->currAnimId];
Animation anim = *entityAnim->anim;
i32 frameIndex = anim.frameIndex[entityAnim->currFrame];
v4 texRect = anim.atlas->texRect[frameIndex];
entityAnim->currDuration -= dt;
if (entityAnim->currDuration <= 0.0f)
{
entityAnim->currFrame++;
entityAnim->currFrame = entityAnim->currFrame % anim.numFrames;
frameIndex = entityAnim->anim->frameIndex[entityAnim->currFrame];
texRect = anim.atlas->texRect[frameIndex];
entityAnim->currDuration = anim.frameDuration;
}
// NOTE(doyle): If humanoid entity, let animation dictate render size which
// may exceed the hitbox size of the entity
switch (entity->type)
{
case entitytype_hero:
case entitytype_mob:
case entitytype_npc:
entity->renderSize = math_getRectSize(texRect);
default:
break;
}
}
// TODO(doyle): Exposed because of debug .. rework debug system so it we don't
// need to expose any game API to it.
INTERNAL v4 createCameraBounds(World *world, v2 size)
{
v4 result = math_getRect(world->cameraPos, size);
// NOTE(doyle): Lock camera if it passes the bounds of the world
if (result.x <= world->bounds.x)
{
result.x = world->bounds.x;
result.z = result.x + size.w;
}
// TODO(doyle): Do the Y component when we need it
if (result.y >= world->bounds.y) result.y = world->bounds.y;
if (result.z >= world->bounds.z)
{
result.z = world->bounds.z;
result.x = result.z - size.w;
}
if (result.w <= world->bounds.w) result.w = world->bounds.w;
return result;
}
#define ENTITY_IN_BATTLE TRUE
#define ENTITY_NOT_IN_BATTLE FALSE
#define ENTITY_NULL_ID -1
INTERNAL i32 findBestEntityToAttack(World *world, Entity attacker)
{
#ifdef DENGINE_DEBUG
ASSERT(world);
ASSERT(attacker.type == entitytype_hero || attacker.type == entitytype_mob);
#endif
i32 result = 0;
// TODO(doyle): If attacker is mob- retrieve hero entity id directly, change
// when we have party members!
if (attacker.type == entitytype_mob)
{
Entity hero = world->entities[world->heroIndex];
if (hero.state == entitystate_dead) result = ENTITY_NULL_ID;
else result = hero.id;
return result;
}
/* Attacker is hero */
Entity hero = attacker;
for (i32 i = 0; i < world->maxEntities; i++)
{
Entity targetEntity = world->entities[i];
if (hero.id == targetEntity.id) continue;
if (world->entityIdInBattle[targetEntity.id] == ENTITY_IN_BATTLE)
{
result = targetEntity.id;
return result;
}
}
// NOTE(doyle): Not all "battling" entities have been enumerated yet in the
// update loop, guard against when using function
return ENTITY_NULL_ID;
}
INTERNAL inline void updateWorldBattleEntities(World *world, Entity *entity,
b32 isInBattle)
{
#ifdef DENGINE_DEBUG
ASSERT(isInBattle == ENTITY_IN_BATTLE ||
isInBattle == ENTITY_NOT_IN_BATTLE);
ASSERT(world && entity);
#endif
world->entityIdInBattle[entity->id] = isInBattle;
if (isInBattle)
{
world->numEntitiesInBattle++;
}
else
{
world->numEntitiesInBattle--;
}
#ifdef DENGINE_DEBUG
ASSERT(world->numEntitiesInBattle >= 0);
#endif
}
// TODO(doyle): Function too vague
INTERNAL inline void resetEntityState(World *world, Entity *entity)
{
updateWorldBattleEntities(world, entity, ENTITY_NOT_IN_BATTLE);
setActiveEntityAnim(entity, animlist_hero_idle);
entity->stats->busyDuration = 0;
entity->stats->actionTimer = entity->stats->actionRate;
entity->stats->queuedAttack = entityattack_invalid;
entity->stats->entityIdToAttack = ENTITY_NULL_ID;
}
INTERNAL void entityStateSwitch(World *world, Entity *entity,
enum EntityState newState)
{
#ifdef DENGINE_DEBUG
ASSERT(world && entity)
ASSERT(entity->type == entitytype_mob || entity->type == entitytype_hero)
#endif
if (entity->state == newState) return;
switch(entity->state)
{
case entitystate_idle:
switch (newState)
{
case entitystate_battle:
updateWorldBattleEntities(world, entity, ENTITY_IN_BATTLE);
entity->stats->entityIdToAttack =
findBestEntityToAttack(world, *entity);
break;
// TODO(doyle): Corner case- if move out of range and entity has
// switched to idle mode, we reach the attacker entity and they continue
// attacking it since there's no check before attack if entity is idle
// or not (i.e. has moved out of frame last frame).
case entitystate_dead:
setActiveEntityAnim(entity, animlist_hero_idle);
entity->stats->busyDuration = 0;
entity->stats->actionTimer = entity->stats->actionRate;
entity->stats->queuedAttack = entityattack_invalid;
entity->stats->entityIdToAttack = ENTITY_NULL_ID;
break;
case entitystate_attack:
default:
#ifdef DENGINE_DEBUG
ASSERT(INVALID_CODE_PATH);
#endif
}
break;
case entitystate_battle:
switch (newState)
{
case entitystate_attack:
{
break;
}
case entitystate_idle:
case entitystate_dead:
resetEntityState(world, entity);
break;
default:
#ifdef DENGINE_DEBUG
ASSERT(INVALID_CODE_PATH);
#endif
}
break;
case entitystate_attack:
switch (newState)
{
case entitystate_battle:
setActiveEntityAnim(entity, animlist_hero_battlePose);
entity->stats->actionTimer = entity->stats->actionRate;
entity->stats->busyDuration = 0;
break;
// NOTE(doyle): Entity has been forced out of an attack (out of range)
case entitystate_idle:
case entitystate_dead:
resetEntityState(world, entity);
break;
default:
#ifdef DENGINE_DEBUG
ASSERT(INVALID_CODE_PATH);
#endif
}
break;
case entitystate_dead:
switch (newState)
{
case entitystate_idle:
case entitystate_battle:
case entitystate_attack:
default:
#ifdef DENGINE_DEBUG
ASSERT(INVALID_CODE_PATH);
#endif
}
break;
default:
#ifdef DENGINE_DEBUG
ASSERT(INVALID_CODE_PATH);
#endif
}
entity->state = newState;
}
INTERNAL void beginAttack(World *world, Entity *attacker)
{
#ifdef DENGINE_DEBUG
ASSERT(attacker->stats->entityIdToAttack != ENTITY_NULL_ID);
ASSERT(attacker->state == entitystate_battle);
#endif
entityStateSwitch(world, attacker, entitystate_attack);
switch (attacker->stats->queuedAttack)
{
case entityattack_tackle:
EntityAnim_ attackAnim = attacker->anim[animlist_hero_tackle];
f32 busyDuration = attackAnim.anim->frameDuration *
CAST(f32) attackAnim.anim->numFrames;
attacker->stats->busyDuration = busyDuration;
setActiveEntityAnim(attacker, animlist_hero_tackle);
if (attacker->direction == direction_east)
attacker->dPos.x += (1.0f * METERS_TO_PIXEL);
else
attacker->dPos.x -= (1.0f * METERS_TO_PIXEL);
break;
default:
#ifdef DENGINE_DEBUG
ASSERT(INVALID_CODE_PATH);
#endif
}
}
// TODO(doyle): Calculate the battle damage, transition back into battle pose ..
// etc
INTERNAL void endAttack(World *world, Entity *attacker)
{
#ifdef DENGINE_DEBUG
ASSERT(attacker->stats->entityIdToAttack != ENTITY_NULL_ID);
#endif
switch (attacker->stats->queuedAttack)
{
case entityattack_tackle:
// TODO(doyle): Move animation offsets out and into animation type
if (attacker->direction == direction_east)
attacker->dPos.x -= (1.0f * METERS_TO_PIXEL);
else
attacker->dPos.x += (1.0f * METERS_TO_PIXEL);
break;
default:
#ifdef DENGINE_DEBUG
ASSERT(INVALID_CODE_PATH);
#endif
}
/* Compute attack damage */
Entity *defender = NULL;
// TODO(doyle): Implement faster lookup for entity id in entity table
b32 noMoreValidTargets = FALSE;
do
{
/* Get target entity to attack */
for (i32 i = 0; i < world->maxEntities; i++)
{
i32 entityIdToAttack = attacker->stats->entityIdToAttack;
if (world->entities[i].id == entityIdToAttack)
{
defender = &world->entities[i];
#ifdef DENGINE_DEBUG
ASSERT(defender->type == entitytype_mob ||
defender->type == entitytype_hero);
#endif
break;
}
}
/* If no longer exists, find next best */
if (!defender)
{
i32 entityIdToAttack = findBestEntityToAttack(world, *attacker);
if (entityIdToAttack == ENTITY_NULL_ID)
{
noMoreValidTargets = TRUE;
}
else
{
attacker->stats->entityIdToAttack =
findBestEntityToAttack(world, *attacker);
}
}
} while (!defender && noMoreValidTargets == TRUE);
enum EntityState newAttackerState = entitystate_invalid;
if (!noMoreValidTargets)
{
// TODO(doyle): Use attacker stats in battle equations
if (attacker->type == entitytype_hero)
defender->stats->health -= 1;
else
{
// defender->stats->health--;
}
if (defender->stats->health <= 0)
{
#ifdef DENGINE_DEBUG
DEBUG_LOG("Entity has died");
#endif
entityStateSwitch(world, defender, entitystate_dead);
attacker->stats->entityIdToAttack =
findBestEntityToAttack(world, *attacker);
}
newAttackerState = entitystate_battle;
}
else
{
newAttackerState = entitystate_idle;
}
/* Return attacker back to non-attacking state */
entityStateSwitch(world, attacker, newAttackerState);
}
void worldTraveller_gameUpdateAndRender(GameState *state, f32 dt)
{
if (dt >= 1.0f) dt = 1.0f;
/* Update */
parseInput(state, dt);
glCheckError();
AssetManager *assetManager = &state->assetManager;
Renderer *renderer = &state->renderer;
World *const world = &state->world[state->currWorldIndex];
Font *font = &assetManager->font;
/*
******************************
* Update entities and render
******************************
*/
Entity *hero = getHeroEntity(world);
v4 cameraBounds = createCameraBounds(world, renderer->size);
ASSERT(world->freeEntityIndex < world->maxEntities);
for (i32 i = 0; i < world->freeEntityIndex; i++)
{
Entity *const entity = &world->entities[i];
if (entity->state == entitystate_dead)
{
#ifdef DENGINE_DEBUG
switch(entity->type)
{
case entitytype_hero:
case entitytype_mob:
break;
default:
// TODO(doyle): Implement variable expansion in DEBUG_LOG
DEBUG_LOG("Unexpected entity has been deleted");
}
#endif
// TODO(doyle): Accumulate all dead entities and delete at the
// end. Hence resort/organise entity array once, not every time
// an entity dies
#if 1
i32 entityIndexInArray = i;
deleteEntity(&state->arena, world, entityIndexInArray);
// TODO(doyle): DeleteEntity moves elements down 1, so account for i
i--;
#endif
continue;
}
/*
*****************************************************
* Set mob to battle mode if within distance from hero
*****************************************************
*/
if (entity->type == entitytype_mob)
{
// TODO(doyle): Currently calculated in pixels, how about meaningful
// game units?
f32 battleThreshold = 500.0f;
f32 distance = v2_magnitude(hero->pos, entity->pos);
enum EntityState newState = entitystate_invalid;
if (distance <= battleThreshold)
{
// NOTE(doyle): If in range but in battle, no state change
if (entity->state == entitystate_battle ||
entity->state == entitystate_attack)
{
newState = entity->state;
}
else
{
newState = entitystate_battle;
}
}
else
{
newState = entitystate_idle;
}
entityStateSwitch(world, entity, newState);
}
/*
**************************************************
* Conduct battle for humanoid entities if in range
**************************************************
*/
if (entity->type == entitytype_mob || entity->type == entitytype_hero)
{
EntityStats *stats = entity->stats;
if (entity->state == entitystate_battle)
{
if (stats->actionTimer > 0)
stats->actionTimer -= dt * stats->actionSpdMul;
if (stats->actionTimer <= 0)
{
stats->actionTimer = 0;
if (stats->queuedAttack == entityattack_invalid)
stats->queuedAttack = entityattack_tackle;
beginAttack(world, entity);
}
}
else if (entity->state == entitystate_attack)
{
// TODO(doyle): Untested if the attacker and the defender same
Entity *attacker = entity;
stats->busyDuration -= dt;
if (stats->busyDuration <= 0) endAttack(world, attacker);
}
}
/*
**************************************************
* Update animations and render entity
**************************************************
*/
updateEntityAnim(entity, dt);
/* Calculate region to render */
renderer_entity(renderer, cameraBounds, entity, V2(0, 0), 0, V4(1, 1, 1, 1));
}
// TODO(doyle): Dead hero not accounted for here
if (world->numEntitiesInBattle > 0)
{
// NOTE(doyle): If battle entities is 1 then only the hero left
if (hero->state == entitystate_battle &&
world->numEntitiesInBattle == 1)
entityStateSwitch(world, hero, entitystate_idle);
else if (hero->state != entitystate_attack)
{
entityStateSwitch(world, hero, entitystate_battle);
}
}
else
{
if (hero->state == entitystate_battle)
{
hero->state = entitystate_idle;
world->entityIdInBattle[hero->id] = FALSE;
setActiveEntityAnim(hero, animlist_hero_idle);
}
hero->stats->entityIdToAttack = -1;
hero->stats->actionTimer = hero->stats->actionRate;
hero->stats->busyDuration = 0;
}
/* Draw ui */
/* Draw hero avatar */
TexAtlas *heroAtlas = asset_getTextureAtlas(assetManager, texlist_hero);
v4 heroAvatarTexRect = heroAtlas->texRect[herorects_head];
v2 heroAvatarSize = math_getRectSize(heroAvatarTexRect);
v2 heroAvatarP =
V2(10.0f, (renderer->size.h * 0.5f) - (0.5f * heroAvatarSize.h));
RenderTex heroRenderTex = {hero->tex, heroAvatarTexRect};
renderer_staticRect(renderer, heroAvatarP, heroAvatarSize, V2(0, 0), 0,
heroRenderTex, V4(1, 1, 1, 1));
char heroAvatarStr[20];
snprintf(heroAvatarStr, ARRAY_COUNT(heroAvatarStr), "HP: %3.0f/%3.0f",
hero->stats->health, hero->stats->maxHealth);
f32 strLenInPixels =
CAST(f32)(font->maxSize.w * common_strlen(heroAvatarStr));
v2 strPos = V2(heroAvatarP.x, heroAvatarP.y - (0.5f * heroAvatarSize.h));
renderer_staticString(&state->renderer, &state->arena, font, heroAvatarStr,
strPos, V2(0, 0), 0, V4(0, 0, 1, 1));
for (i32 i = 0; i < world->maxEntities; i++)
{
Entity *entity = &world->entities[i];
if (entity->id == hero->id)
continue;
if (entity->state == entitystate_attack ||
entity->state == entitystate_battle)
{
v2 difference = v2_sub(entity->pos, hero->pos);
f32 angle = math_atan2f(difference.y, difference.x);
f32 angleDegrees = RADIANS_TO_DEGREES(angle);
Texture *emptyTex = asset_getTexture(assetManager, texlist_empty);
v2 heroCenter = v2_add(hero->pos, v2_scale(hero->hitboxSize, 0.5f));
RenderTex renderTex = {emptyTex, V4(0, 1, 1, 0)};
f32 distance = v2_magnitude(hero->pos, entity->pos);
renderer_rect(&state->renderer, cameraBounds, heroCenter,
V2(distance, 2.0f), V2(0, 0), angle, renderTex,
V4(1, 0, 0, 1.0f));
}
}
#ifdef DENGINE_DEBUG
renderer_rect(&state->renderer, cameraBounds, V2(500, 500), V2(100, 2.0f),
V2(0, 0), DEGREES_TO_RADIANS(0.0f), renderTex,
V4(0, 0, 1, 1.0f));
renderer_rect(&state->renderer, cameraBounds, V2(500, 500), V2(100, 2.0f),
V2(0, 0), DEGREES_TO_RADIANS(5.0f), renderTex,
V4(0, 0, 1, 1.0f));
renderer_rect(&state->renderer, cameraBounds, V2(500, 500), V2(100, 2.0f),
V2(0, 0), DEGREES_TO_RADIANS(90.0f), renderTex,
V4(0, 0, 1, 1.0f));
debug_drawUi(state, dt);
#endif
}