Dengine/src/WorldTraveller.c
Doyle Thai ebe09bd345 Improved render groups, use degenerate triangles
Degenerate triangles allows a single triangle strip call to be called on
multiple independent triangles without "connecting" strips inbetween the
triangles.
2016-09-22 02:16:14 +10:00

2465 lines
72 KiB
C

#include "Dengine/WorldTraveller.h"
#include "Dengine/Audio.h"
#include "Dengine/Debug.h"
#include "Dengine/Entity.h"
#include "Dengine/Platform.h"
#include "Dengine/String.h"
#include "Dengine/UserInterface.h"
enum State
{
state_active,
state_menu,
state_win,
};
INTERNAL Entity *getHeroEntity(World *world)
{
Entity *result = &world->entities[entity_getIndex(world, world->heroId)];
return result;
}
INTERNAL void addGenericMob(EventQueue *eventQueue, MemoryArena *arena,
AssetManager *assetManager, World *world, v2 pos)
{
#ifdef DENGINE_DEBUG
DEBUG_LOG("Mob entity spawned");
#endif
Entity *hero = &world->entities[entity_getIndex(world, world->heroId)];
v2 size = V2(58.0f, 98.0f);
f32 scale = 2;
enum EntityType type = entitytype_mob;
enum Direction dir = direction_west;
Texture *tex = asset_getTex(assetManager, "ClaudeSprite.png");
b32 collides = TRUE;
Entity *mob =
entity_add(arena, world, pos, size, scale, type, dir, tex, collides);
mob->numAudioRenderers = 4;
mob->audioRenderer =
PLATFORM_MEM_ALLOC(arena, mob->numAudioRenderers, AudioRenderer);
for (i32 i = 0; i < mob->numAudioRenderers; i++)
mob->audioRenderer[i].sourceIndex = AUDIO_SOURCE_UNASSIGNED;
/* Populate mob animation references */
entity_addAnim(assetManager, mob, "claudeIdle");
entity_addAnim(assetManager, mob, "claudeRun");
entity_addAnim(assetManager, mob, "claudeBattleIdle");
entity_addAnim(assetManager, mob, "claudeAttack");
entity_addAnim(assetManager, mob, "claudeAttackUp");
entity_addAnim(assetManager, mob, "claudeAttackDown");
entity_addAnim(assetManager, mob, "claudeAirSlash");
entity_addAnim(assetManager, mob, "claudeDragonHowl");
entity_addAnim(assetManager, mob, "claudeEnergySword");
entity_addAnim(assetManager, mob, "claudeRipperBlast");
entity_setActiveAnim(eventQueue, mob, "claudeIdle");
}
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);
GL_CHECK_ERROR();
/* Create buffers */
glGenVertexArrays(1, &renderer->vao);
glGenBuffers(1, &renderer->vbo);
GL_CHECK_ERROR();
/* Bind buffers */
glBindBuffer(GL_ARRAY_BUFFER, renderer->vbo);
glBindVertexArray(renderer->vao);
/* Configure VAO */
const GLuint numVertexElements = 4;
const GLuint vertexSize = sizeof(Vertex);
glEnableVertexAttribArray(0);
glVertexAttribPointer(0, numVertexElements, GL_FLOAT, GL_FALSE, vertexSize,
(GLvoid *)0);
GL_CHECK_ERROR();
/* Unbind */
glBindBuffer(GL_ARRAY_BUFFER, 0);
glBindVertexArray(0);
GL_CHECK_ERROR();
// TODO(doyle): Lazy allocate render group capacity
renderer->groupCapacity = 1024;
for (i32 i = 0; i < ARRAY_COUNT(renderer->groups); i++)
{
renderer->groups[i].vertexList =
PLATFORM_MEM_ALLOC(&state->arena, renderer->groupCapacity, Vertex);
}
#ifdef DENGINE_DEBUG
DEBUG_LOG("Renderer initialised");
#endif
}
INTERNAL void assetInit(GameState *state)
{
AssetManager *assetManager = &state->assetManager;
MemoryArena *arena = &state->arena;
i32 audioEntries = 32;
assetManager->audio.size = audioEntries;
assetManager->audio.entries =
PLATFORM_MEM_ALLOC(arena, audioEntries, HashTableEntry);
i32 texAtlasEntries = 8;
assetManager->texAtlas.size = texAtlasEntries;
assetManager->texAtlas.entries =
PLATFORM_MEM_ALLOC(arena, texAtlasEntries, HashTableEntry);
i32 texEntries = 32;
assetManager->textures.size = texEntries;
assetManager->textures.entries =
PLATFORM_MEM_ALLOC(arena, texEntries, HashTableEntry);
i32 animEntries = 1024;
assetManager->anims.size = animEntries;
assetManager->anims.entries =
PLATFORM_MEM_ALLOC(arena, animEntries, HashTableEntry);
/* Create empty 1x1 4bpp black texture */
u32 bitmap = (0xFF << 24) | (0xFF << 16) | (0xFF << 8) | (0xFF << 0);
Texture *tex = asset_getFreeTexSlot(assetManager, arena, "nullTex");
*tex = texture_gen(1, 1, 4, CAST(u8 *)(&bitmap));
/*
*********************************
* Load terrain texture atlas data
*********************************
*/
PlatformFileRead terrainXml = {0};
i32 result = platform_readFileToBuffer(
arena, "data/textures/WorldTraveller/terrain.xml", &terrainXml);
if (result)
{
DEBUG_LOG("Failed to read sprite sheet xml");
}
else
{
result = asset_loadXmlFile(assetManager, arena, &terrainXml);
if (!result)
{
TexAtlas *terrainAtlas =
asset_getTexAtlas(assetManager, "terrain.png");
i32 numSubTextures = 1;
f32 duration = 1.0f;
char *grassTerrain[1] = {"grass.png"};
asset_addAnimation(assetManager, arena, "terrainGrass",
terrainAtlas, grassTerrain, numSubTextures,
duration);
}
else
{
#ifdef DENGINE_DEBUG
DEBUG_LOG("Failed to load terrain sprite xml data");
#endif
}
platform_closeFileRead(arena, &terrainXml);
}
/*
*********************************
* Load Claude texture atlas data
*********************************
*/
PlatformFileRead claudeXml = {0};
result = platform_readFileToBuffer(
arena, "data/textures/WorldTraveller/ClaudeSprite.xml", &claudeXml);
if (result)
{
DEBUG_LOG("Failed to read sprite sheet xml");
}
else
{
result = asset_loadXmlFile(assetManager, arena, &claudeXml);
if (!result)
{
TexAtlas *claudeAtlas =
asset_getTexAtlas(assetManager, "ClaudeSprite.png");
char *claudeIdle[1] = {"ClaudeSprite_Walk_Left_01"};
f32 duration = 1.0f;
i32 numRects = ARRAY_COUNT(claudeIdle);
asset_addAnimation(assetManager, arena, "claudeIdle", claudeAtlas,
claudeIdle, numRects, duration);
// Run animation
char *claudeRun[6] = {
"ClaudeSprite_Run_Left_01", "ClaudeSprite_Run_Left_02",
"ClaudeSprite_Run_Left_03", "ClaudeSprite_Run_Left_04",
"ClaudeSprite_Run_Left_05", "ClaudeSprite_Run_Left_06"};
duration = 0.1f;
numRects = ARRAY_COUNT(claudeRun);
asset_addAnimation(assetManager, arena, "claudeRun", claudeAtlas,
claudeRun, numRects, duration);
// Battle Idle animation
char *claudeBattleIdle[3] = {"ClaudeSprite_BattleIdle_Left_01",
"ClaudeSprite_BattleIdle_Left_02",
"ClaudeSprite_BattleIdle_Left_03"};
numRects = ARRAY_COUNT(claudeBattleIdle);
duration = 0.2f;
asset_addAnimation(assetManager, arena, "claudeBattleIdle",
claudeAtlas, claudeBattleIdle, numRects,
duration);
// Attack Left animation
char *claudeAttack[6] = {
"ClaudeSprite_Attack_Left_01", "ClaudeSprite_Attack_Left_02",
"ClaudeSprite_Attack_Left_03", "ClaudeSprite_Attack_Left_04",
"ClaudeSprite_Attack_Left_05", "ClaudeSprite_Attack_Left_06"};
numRects = ARRAY_COUNT(claudeAttack);
duration = 0.1f;
asset_addAnimation(assetManager, arena, "claudeAttack", claudeAtlas,
claudeAttack, numRects, duration);
char *claudeAttackDown[7] = {
"ClaudeSprite_Attack_Down_01",
"ClaudeSprite_Attack_Down_02",
"ClaudeSprite_Attack_Down_03",
"ClaudeSprite_Attack_Down_04",
"ClaudeSprite_Attack_Down_05",
"ClaudeSprite_Attack_Down_06",
"ClaudeSprite_Attack_Down_07",
};
numRects = ARRAY_COUNT(claudeAttackDown);
duration = 0.1f;
asset_addAnimation(assetManager, arena, "claudeAttackDown",
claudeAtlas, claudeAttackDown, numRects,
duration);
char *claudeAttackUp[3] = {
"ClaudeSprite_Attack_Up_01",
"ClaudeSprite_Attack_Up_02",
"ClaudeSprite_Attack_Up_03",
};
numRects = ARRAY_COUNT(claudeAttackUp);
duration = 0.2f;
asset_addAnimation(assetManager, arena, "claudeAttackUp",
claudeAtlas, claudeAttackUp, numRects,
duration);
char *claudeDragonHowl[3] = {
"ClaudeSprite_Attack_DragonHowl_01",
"ClaudeSprite_Attack_DragonHowl_02",
"ClaudeSprite_Attack_DragonHowl_03",
};
numRects = ARRAY_COUNT(claudeDragonHowl);
duration = 0.2f;
asset_addAnimation(assetManager, arena, "claudeDragonHowl",
claudeAtlas, claudeDragonHowl, numRects,
duration);
char *claudeDragonHowlVfx[7] = {
"ClaudeSprite_Attack_DragonHowl_Vfx_01",
"ClaudeSprite_Attack_DragonHowl_Vfx_02",
"ClaudeSprite_Attack_DragonHowl_Vfx_03",
"ClaudeSprite_Attack_DragonHowl_Vfx_04",
"ClaudeSprite_Attack_DragonHowl_Vfx_05",
"ClaudeSprite_Attack_DragonHowl_Vfx_06",
"ClaudeSprite_Attack_DragonHowl_Vfx_07",
};
numRects = ARRAY_COUNT(claudeDragonHowlVfx);
duration = 0.1f;
asset_addAnimation(assetManager, arena, "claudeDragonHowlVfx",
claudeAtlas, claudeDragonHowlVfx, numRects,
duration);
char *claudeRipperBlast[6] = {
"ClaudeSprite_Attack_RipperBlast_01",
"ClaudeSprite_Attack_RipperBlast_02",
"ClaudeSprite_Attack_RipperBlast_03",
"ClaudeSprite_Attack_RipperBlast_04",
"ClaudeSprite_Attack_RipperBlast_05",
"ClaudeSprite_Attack_RipperBlast_06",
};
numRects = ARRAY_COUNT(claudeRipperBlast);
duration = 0.1f;
asset_addAnimation(assetManager, arena, "claudeRipperBlast",
claudeAtlas, claudeRipperBlast, numRects,
duration);
char *claudeRipperBlastVfx[12] = {
"ClaudeSprite_Attack_RipperBlast_Vfx_01",
"ClaudeSprite_Attack_RipperBlast_Vfx_02",
"ClaudeSprite_Attack_RipperBlast_Vfx_03",
"ClaudeSprite_Attack_RipperBlast_Vfx_04",
"ClaudeSprite_Attack_RipperBlast_Vfx_05",
"ClaudeSprite_Attack_RipperBlast_Vfx_06",
"ClaudeSprite_Attack_RipperBlast_Vfx_07",
"ClaudeSprite_Attack_RipperBlast_Vfx_08",
"ClaudeSprite_Attack_RipperBlast_Vfx_09",
"ClaudeSprite_Attack_RipperBlast_Vfx_10",
"ClaudeSprite_Attack_RipperBlast_Vfx_11",
"ClaudeSprite_Attack_RipperBlast_Vfx_12",
};
numRects = ARRAY_COUNT(claudeRipperBlastVfx);
duration = 0.1f;
asset_addAnimation(assetManager, arena, "claudeRipperBlastVfx",
claudeAtlas, claudeRipperBlastVfx, numRects,
duration);
// Victory animation
char *claudeVictory[8] = {"ClaudeSprite_Battle_Victory_01",
"ClaudeSprite_Battle_Victory_02",
"ClaudeSprite_Battle_Victory_03",
"ClaudeSprite_Battle_Victory_04",
"ClaudeSprite_Battle_Victory_05",
"ClaudeSprite_Battle_Victory_06",
"ClaudeSprite_Battle_Victory_07",
"ClaudeSprite_Battle_Victory_08"};
numRects = ARRAY_COUNT(claudeVictory);
duration = 0.1f;
asset_addAnimation(assetManager, arena, "claudeVictory",
claudeAtlas, claudeVictory, numRects, duration);
char *claudeEnergySword[6] = {"ClaudeSprite_Attack_EnergySword_01",
"ClaudeSprite_Attack_EnergySword_02",
"ClaudeSprite_Attack_EnergySword_03",
"ClaudeSprite_Attack_EnergySword_04",
"ClaudeSprite_Attack_EnergySword_05",
"ClaudeSprite_Attack_EnergySword_06"};
numRects = ARRAY_COUNT(claudeEnergySword);
duration = 0.1f;
asset_addAnimation(assetManager, arena, "claudeEnergySword",
claudeAtlas, claudeEnergySword, numRects,
duration);
char *claudeAirSlash[7] = {"ClaudeSprite_Attack_AirSlash_01",
"ClaudeSprite_Attack_AirSlash_02",
"ClaudeSprite_Attack_AirSlash_03",
"ClaudeSprite_Attack_AirSlash_04",
"ClaudeSprite_Attack_AirSlash_05",
"ClaudeSprite_Attack_AirSlash_06",
"ClaudeSprite_Attack_AirSlash_07"};
numRects = ARRAY_COUNT(claudeAirSlash);
duration = 0.075f;
asset_addAnimation(assetManager, arena, "claudeAirSlash",
claudeAtlas, claudeAirSlash, numRects,
duration);
char *claudeAirSlashVfx[7] = {"ClaudeSprite_Attack_AirSlash_Vfx_01",
"ClaudeSprite_Attack_AirSlash_Vfx_02",
"ClaudeSprite_Attack_AirSlash_Vfx_03",
"ClaudeSprite_Attack_AirSlash_Vfx_04",
"ClaudeSprite_Attack_AirSlash_Vfx_05",
"ClaudeSprite_Attack_AirSlash_Vfx_06",
"ClaudeSprite_Attack_AirSlash_Vfx_07"};
numRects = ARRAY_COUNT(claudeAirSlashVfx);
duration = 0.075f;
asset_addAnimation(assetManager, arena, "claudeAirSlashVfx",
claudeAtlas, claudeAirSlashVfx, numRects,
duration);
char *claudeSword[1] = {
"ClaudeSprite_Sword_01",
};
numRects = ARRAY_COUNT(claudeSword);
duration = 0.4f;
asset_addAnimation(assetManager, arena, "claudeSword",
claudeAtlas, claudeSword, numRects,
duration);
char *claudeAttackSlashLeft[4] = {
"ClaudeSprite_Attack_Slash_Left_01",
"ClaudeSprite_Attack_Slash_Left_02",
"ClaudeSprite_Attack_Slash_Left_03",
"ClaudeSprite_Attack_Slash_Left_04",
};
numRects = ARRAY_COUNT(claudeAttackSlashLeft);
duration = 0.1f;
asset_addAnimation(assetManager, arena, "claudeAttackSlashLeft",
claudeAtlas, claudeAttackSlashLeft, numRects,
duration);
}
else
{
#ifdef DENGINE_DEBUG
DEBUG_LOG("Failed to load claude sprite xml data");
#endif
}
platform_closeFileRead(arena, &claudeXml);
}
#ifdef DENGINE_DEBUG
DEBUG_LOG("Animations created");
#endif
/* Load shaders */
asset_loadShaderFiles(assetManager, arena, "data/shaders/sprite.vert.glsl",
"data/shaders/sprite.frag.glsl",
shaderlist_sprite);
result =
asset_loadTTFont(assetManager, arena, "C:/Windows/Fonts/Arialbd.ttf");
#ifdef DENGINE_DEBUG
if (result) DEBUG_LOG("Font loading failed");
GL_CHECK_ERROR();
DEBUG_LOG("Assets loaded");
#endif
/* Load sound */
i32 before = arena->bytesAllocated;
char *sfxListPath = "data/audio/sfx/sfx.txt";
PlatformFileRead sfxList = {0};
result = platform_readFileToBuffer(arena, sfxListPath, &sfxList);
char *sfxAudioNames[256];
i32 sfxAudioIndex = 0;
if (!result)
{
char string[256] = {0};
i32 stringIndex = 0;
for (i32 i = 0; i < sfxList.size; i++)
{
char c = (CAST(char *)sfxList.buffer)[i];
switch(c)
{
case 0x0a:
{
i32 actualStrLen = common_strlen(string) + 1;
sfxAudioNames[sfxAudioIndex] =
PLATFORM_MEM_ALLOC(arena, actualStrLen, char);
common_strncpy(sfxAudioNames[sfxAudioIndex++], string,
actualStrLen);
common_memset(string, 0, ARRAY_COUNT(string));
stringIndex = 0;
break;
}
default:
{
if (c >= ' ' && c <= '~')
{
string[stringIndex++] = c;
}
break;
}
}
}
}
char *sfxDir = "data/audio/sfx/";
char *sfxExtension = ".ogg";
i32 sfxDirLen = common_strlen(sfxDir);
i32 sfxExtensionLen = common_strlen(sfxExtension);
for (i32 i = 0; i < sfxAudioIndex; i++)
{
char *sfxName = sfxAudioNames[i];
i32 sfxNameLen = common_strlen(sfxName);
i32 sfxFullPathLen = sfxDirLen + sfxExtensionLen + sfxNameLen + 1;
char *sfxFullPath = PLATFORM_MEM_ALLOC(arena, sfxFullPathLen, char);
common_strncat(sfxFullPath, sfxDir, sfxDirLen);
common_strncat(sfxFullPath, sfxName, sfxNameLen);
common_strncat(sfxFullPath, sfxExtension, sfxExtensionLen);
i32 result = asset_loadVorbis(assetManager, arena, sfxFullPath, sfxName);
if (result) DEBUG_LOG("Failed to load sfx file");
// TODO(doyle): Need better string type to account for null terminating
// character, having to remember to +1 on allocation AND freeing since
// strlen only counts until null char is going to leave memory leaks
// everywhere
PLATFORM_MEM_FREE(arena, sfxName, sfxNameLen * sizeof(char) + 1);
PLATFORM_MEM_FREE(arena, sfxFullPath, sfxFullPathLen * sizeof(char));
}
platform_closeFileRead(arena, &sfxList);
char *audioPath =
"data/audio/Motoi Sakuraba - Stab the sword of justice.ogg";
asset_loadVorbis(assetManager, arena, audioPath, "audio_battle");
audioPath = "data/audio/Motoi Sakuraba - Field of Exper.ogg";
asset_loadVorbis(assetManager, arena, audioPath, "audio_overworld");
audioPath = "data/audio/nuindependent_hit22.ogg";
asset_loadVorbis(assetManager, arena, audioPath, "audio_tackle");
#ifdef DENGINE_DEBUG
DEBUG_LOG("Sound assets initialised");
#endif
}
INTERNAL void entityInit(GameState *state, v2 windowSize)
{
AssetManager *assetManager = &state->assetManager;
MemoryArena *arena = &state->arena;
/* Init world */
const i32 targetWorldWidth = 100 * METERS_TO_PIXEL;
const i32 targetWorldHeight = 10 * METERS_TO_PIXEL;
#if 0
v2 worldDimensionInTiles = V2i(targetWorldWidth / state->tileSize,
targetWorldHeight / state->tileSize);
#else
v2 worldDimensionInTiles =
V2i(CAST(i32)(windowSize.w / state->tileSize) * 2,
CAST(i32) windowSize.h / state->tileSize);
#endif
EventQueue *eventQueue = &state->eventQueue;
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->bounds =
math_getRect(V2(0, 0), v2_scale(worldDimensionInTiles,
CAST(f32) state->tileSize));
world->uniqueIdAccumulator = 0;
#if 1
TexAtlas *const atlas = asset_getTexAtlas(assetManager, "terrain.png");
for (i32 y = 0; y < 1; 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);
f32 scale = 1.0f;
enum EntityType type = entitytype_tile;
enum Direction dir = direction_null;
Texture *tex = asset_getTex(assetManager, "terrain.png");
b32 collides = FALSE;
Entity *tile = entity_add(arena, world, pos, size, scale, type,
dir, tex, collides);
entity_addAnim(assetManager, tile, "terrainGrass");
entity_setActiveAnim(eventQueue, tile, "terrainGrass");
}
}
#endif
}
World *const world = &state->world[state->currWorldIndex];
world->cameraPos = V2(0.0f, 0.0f);
/* Add world soundscape */
Renderer *renderer = &state->renderer;
v2 size = V2(10.0f, 10.0f);
v2 pos = V2(0, 0);
f32 scale = 0.0f;
enum EntityType type = entitytype_soundscape;
enum Direction dir = direction_null;
Texture *tex = NULL;
b32 collides = FALSE;
Entity *soundscape =
entity_add(arena, world, pos, size, scale, type, dir, tex, collides);
world->soundscape = soundscape;
soundscape->numAudioRenderers = 1;
soundscape->audioRenderer =
PLATFORM_MEM_ALLOC(arena, soundscape->numAudioRenderers, AudioRenderer);
for (i32 i = 0; i < soundscape->numAudioRenderers; i++)
soundscape->audioRenderer[i].sourceIndex = AUDIO_SOURCE_UNASSIGNED;
/* Init hero entity */
size = V2(58.0f, 98.0f);
pos = V2(size.x, CAST(f32) state->tileSize);
scale = 2.0f;
type = entitytype_hero;
dir = direction_east;
tex = asset_getTex(assetManager, "ClaudeSprite.png");
collides = TRUE;
Entity *hero =
entity_add(arena, world, pos, size, scale, type, dir, tex, collides);
Entity *heroWeapon =
entity_add(arena, world, pos, V2(20, 20), scale, entitytype_weapon,
dir, tex, FALSE);
heroWeapon->flipX = TRUE;
entity_addAnim(assetManager, heroWeapon, "claudeSword");
hero->stats->weapon = heroWeapon;
hero->numAudioRenderers = 4;
hero->audioRenderer =
PLATFORM_MEM_ALLOC(arena, hero->numAudioRenderers, AudioRenderer);
for (i32 i = 0; i < hero->numAudioRenderers; i++)
hero->audioRenderer[i].sourceIndex = AUDIO_SOURCE_UNASSIGNED;
world->heroId = hero->id;
world->cameraFollowingId = hero->id;
/* Populate hero animation references */
entity_addAnim(assetManager, hero, "claudeIdle");
entity_addAnim(assetManager, hero, "claudeRun");
entity_addAnim(assetManager, hero, "claudeBattleIdle");
entity_addAnim(assetManager, hero, "claudeAttack");
entity_addAnim(assetManager, hero, "claudeAttackUp");
entity_addAnim(assetManager, hero, "claudeAttackDown");
entity_addAnim(assetManager, hero, "claudeDragonHowl");
entity_addAnim(assetManager, hero, "claudeEnergySword");
entity_addAnim(assetManager, hero, "claudeRipperBlast");
entity_addAnim(assetManager, hero, "claudeAirSlash");
entity_setActiveAnim(eventQueue, hero, "claudeIdle");
/* Create a NPC */
pos = V2(hero->pos.x * 3, CAST(f32) state->tileSize);
size = hero->hitbox;
type = entitytype_npc;
dir = direction_null;
tex = hero->tex;
collides = FALSE;
Entity *npc =
entity_add(arena, world, pos, size, scale, type, dir, tex, collides);
/* Populate npc animation references */
entity_addAnim(assetManager, npc, "claudeVictory");
entity_setActiveAnim(eventQueue, npc, "claudeVictory");
/* Create a Mob */
pos = V2(renderer->size.w - (renderer->size.w / 3.0f),
CAST(f32) state->tileSize);
addGenericMob(eventQueue, arena, assetManager, world, pos);
#ifdef DENGINE_DEBUG
DEBUG_LOG("World populated");
#endif
}
INTERNAL v2 getPosRelativeToRect(Rect rect, v2 offset,
enum RectBaseline baseline)
{
#ifdef DENGINE_DEBUG
ASSERT(baseline < rectbaseline_count);
#endif
v2 result = {0};
v2 posToOffsetFrom = rect.pos;
switch (baseline)
{
case rectbaseline_top:
posToOffsetFrom.y += (rect.size.h);
posToOffsetFrom.x += (rect.size.w * 0.5f);
break;
case rectbaseline_topLeft:
posToOffsetFrom.y += (rect.size.h);
break;
case rectbaseline_topRight:
posToOffsetFrom.y += (rect.size.h);
posToOffsetFrom.x += (rect.size.w);
break;
case rectbaseline_bottom:
posToOffsetFrom.x += (rect.size.w * 0.5f);
break;
case rectbaseline_bottomRight:
posToOffsetFrom.x += (rect.size.w);
break;
case rectbaseline_left:
posToOffsetFrom.y += (rect.size.h * 0.5f);
break;
case rectbaseline_right:
posToOffsetFrom.x += (rect.size.w);
posToOffsetFrom.y += (rect.size.h * 0.5f);
break;
case rectbaseline_bottomLeft:
break;
default:
#ifdef DENGINE_DEBUG
DEBUG_LOG(
"getPosRelativeToRect() warning: baseline enum not recognised");
#endif
break;
}
result = v2_add(posToOffsetFrom, offset);
return result;
}
INTERNAL void unitTest(MemoryArena *arena)
{
ASSERT(common_atoi("-2", common_strlen("-2")) == -2);
ASSERT(common_atoi("100", common_strlen("100")) == 100);
ASSERT(common_atoi("1", common_strlen("1")) == 1);
ASSERT(common_atoi("954 32", common_strlen("954 32")) == 954);
ASSERT(common_atoi("", 0) == -1);
ASSERT(common_atoi(" 32", common_strlen(" 32")) == -1);
ASSERT(common_atoi("+32", common_strlen("+32")) == 32);
ASSERT(common_atoi("+ 32", common_strlen("+ 32")) == 0);
asset_unitTest(arena);
i32 memBefore = arena->bytesAllocated;
String *hello = string_make(arena, "hello, ");
String *world = string_make(arena, "world");
ASSERT(string_len(hello) == 7);
ASSERT(string_len(world) == 5);
hello = string_append(arena, hello, world, string_len(world));
ASSERT(string_len(hello) == 12);
string_free(arena, hello);
string_free(arena, world);
hello = string_make(arena, "");
world = string_make(arena, "");
hello = string_append(arena, hello, world, string_len(world));
ASSERT(string_len(hello) == 0);
ASSERT(string_len(world) == 0);
string_free(arena, hello);
string_free(arena, world);
i32 memAfter = arena->bytesAllocated;
ASSERT(memBefore == memAfter);
}
// TODO(doyle): Remove and implement own random generator!
#include <time.h>
#include <stdlib.h>
void worldTraveller_gameInit(GameState *state, v2 windowSize)
{
#ifdef DENGINE_DEBUG
unitTest(&state->arena);
#endif
i32 result = audio_init(&state->audioManager);
if (result)
{
#ifdef DENGINE_DEBUG
ASSERT(INVALID_CODE_PATH);
#endif
}
state->state = state_active;
state->currWorldIndex = 0;
state->tileSize = 70;
state->uiState.uniqueId = 1;
state->uiState.keyEntered = keycode_null;
state->uiState.keyMod = keycode_null;
state->uiState.keyChar = keycode_null;
common_strncpy(state->uiState.statWindow.title, "Stat Menu",
common_strlen("Stat Menu"));
state->uiState.statWindow.id = 99;
state->uiState.statWindow.rect.pos = V2(300, 400);
state->uiState.statWindow.rect.size = V2(300, 400);
state->uiState.statWindow.prevFrameWindowHeld = FALSE;
state->uiState.statWindow.windowHeld = FALSE;
WindowState *debugWindow = &state->uiState.debugWindow;
common_strncpy(debugWindow->title, "Debug Menu",
common_strlen("Debug Menu"));
debugWindow->id = 98;
debugWindow->numChildUiItems = 0;
debugWindow->rect.size = V2(400, 200);
debugWindow->rect.pos = V2(windowSize.w - debugWindow->rect.size.w - 10,
windowSize.h - debugWindow->rect.size.h - 10);
debugWindow->prevFrameWindowHeld = FALSE;
debugWindow->windowHeld = FALSE;
UiItem *audioBtn =
(debugWindow->childUiItems) + debugWindow->numChildUiItems++;
common_strncpy(audioBtn->label, "Toggle Music",
common_strlen("Toggle Music"));
audioBtn->id = userInterface_generateId(&state->uiState);
audioBtn->rect.size = V2(100, 50);
audioBtn->rect.pos = getPosRelativeToRect(debugWindow->rect, V2(10, -65.0f),
rectbaseline_topLeft);
audioBtn->type = uitype_button;
UiItem *debugBtn =
(debugWindow->childUiItems) + debugWindow->numChildUiItems++;
common_strncpy(debugBtn->label, "Toggle Debug",
common_strlen("Toggle Debug"));
debugBtn->id = userInterface_generateId(&state->uiState);
debugBtn->rect.size = V2(100, 50);
debugBtn->rect.pos = getPosRelativeToRect(audioBtn->rect, V2(25, 0),
rectbaseline_bottomRight);
debugBtn->type = uitype_button;
UiItem *scrollbar =
(debugWindow->childUiItems) + debugWindow->numChildUiItems++;
scrollbar->id = userInterface_generateId(&state->uiState);
scrollbar->rect.size = V2(16, debugWindow->rect.size.h);
scrollbar->rect.pos =
getPosRelativeToRect(debugWindow->rect, V2(-scrollbar->rect.size.w, 0),
rectbaseline_bottomRight);
scrollbar->value = 0;
scrollbar->maxValue = 160;
scrollbar->type = uitype_scrollbar;
UiItem *textField =
(debugWindow->childUiItems) + debugWindow->numChildUiItems++;
textField->id = userInterface_generateId(&state->uiState);
textField->rect.size = V2(200, 20);
textField->rect.pos = getPosRelativeToRect(
audioBtn->rect, V2(0, -textField->rect.size.h - 10),
rectbaseline_bottomLeft);
common_strncpy(textField->string, "Hello world",
common_strlen("Hello world"));
textField->type = uitype_textField;
state->config.playWorldAudio = FALSE;
state->config.showDebugDisplay = TRUE;
assetInit(state);
rendererInit(state, windowSize);
entityInit(state, windowSize);
srand(CAST(u32)(time(NULL)));
}
INTERNAL inline v4 getEntityScreenRect(Entity entity)
{
v4 result = math_getRect(entity.pos, entity.hitbox);
return result;
}
enum ReadKeyType
{
readkeytype_oneShot,
readkeytype_delayedRepeat,
readkeytype_repeat,
readkeytype_count,
};
#define KEY_DELAY_NONE 0.0f
INTERNAL b32 getKeyStatus(KeyState *key, enum ReadKeyType readType,
f32 delayInterval, f32 dt)
{
if (!key->endedDown) return FALSE;
switch(readType)
{
case readkeytype_oneShot:
{
if (key->newHalfTransitionCount > key->oldHalfTransitionCount)
return TRUE;
break;
}
case readkeytype_repeat:
case readkeytype_delayedRepeat:
{
if (key->newHalfTransitionCount > key->oldHalfTransitionCount)
{
if (readType == readkeytype_delayedRepeat)
{
// TODO(doyle): Let user set arbitrary delay after initial input
key->delayInterval = 2 * delayInterval;
}
else
{
key->delayInterval = delayInterval;
}
return TRUE;
}
else if (key->delayInterval <= 0.0f)
{
key->delayInterval = delayInterval;
return TRUE;
}
else
{
key->delayInterval -= dt;
}
break;
}
default:
#ifdef DENGINE_DEBUG
DEBUG_LOG("getKeyStatus() error: Invalid ReadKeyType enum");
ASSERT(INVALID_CODE_PATH);
#endif
break;
}
return FALSE;
}
INTERNAL Rect createWorldBoundedCamera(World *world, v2 size)
{
Rect camera = {world->cameraPos, size};
// NOTE(doyle): Lock camera if it passes the bounds of the world
if (camera.pos.x <= world->bounds.x)
camera.pos.x = world->bounds.x;
// TODO(doyle): Allow Y panning when we need it
f32 cameraTopBoundInPixels = camera.pos.y + camera.size.h;
if (cameraTopBoundInPixels >= world->bounds.w)
camera.pos.y = (world->bounds.w - camera.size.h);
f32 cameraRightBoundInPixels = camera.pos.x + camera.size.w;
if (cameraRightBoundInPixels >= world->bounds.z)
camera.pos.x = (world->bounds.z - camera.size.w);
if (camera.pos.y <= world->bounds.y) camera.pos.y = world->bounds.y;
return camera;
}
#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[entity_getIndex(world, world->heroId)];
if (hero.state == entitystate_dead) result = ENTITY_NULL_ID;
else result = hero.id;
return result;
}
/* Attacker is hero */
Entity hero = attacker;
for (i32 i = world->maxEntities; i >= 0; 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(EventQueue *eventQueue, World *world,
Entity *entity)
{
updateWorldBattleEntities(world, entity, ENTITY_NOT_IN_BATTLE);
entity_setActiveAnim(eventQueue, entity, "claudeIdle");
entity->stats->busyDuration = 0;
entity->stats->actionTimer = entity->stats->actionRate;
entity->stats->queuedAttack = entityattack_invalid;
entity->stats->entityIdToAttack = ENTITY_NULL_ID;
}
void worldTraveller_registerEvent(EventQueue *eventQueue, enum EventType type, void *data)
{
#ifdef DENGINE_DEBUG
ASSERT(eventQueue && type < eventtype_count);
ASSERT(eventQueue->numEvents+1 < ARRAY_COUNT(eventQueue->queue));
#endif
i32 currIndex = eventQueue->numEvents++;
eventQueue->queue[currIndex].type = type;
eventQueue->queue[currIndex].data = data;
}
INTERNAL void entityStateSwitch(EventQueue *eventQueue, 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:
worldTraveller_registerEvent(eventQueue, eventtype_entity_died,
CAST(void *) entity);
entity_setActiveAnim(eventQueue, entity, "claudeIdle");
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;
}
break;
case entitystate_battle:
switch (newState)
{
case entitystate_attack:
{
break;
}
case entitystate_idle:
resetEntityState(eventQueue, world, entity);
break;
case entitystate_dead:
worldTraveller_registerEvent(eventQueue, eventtype_entity_died,
CAST(void *) entity);
resetEntityState(eventQueue, world, entity);
break;
default:
#ifdef DENGINE_DEBUG
ASSERT(INVALID_CODE_PATH);
#endif
break;
}
break;
case entitystate_attack:
switch (newState)
{
case entitystate_battle:
entity_setActiveAnim(eventQueue, entity, "claudeBattleIdle");
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:
resetEntityState(eventQueue, world, entity);
break;
case entitystate_dead:
worldTraveller_registerEvent(eventQueue, eventtype_entity_died,
CAST(void *) entity);
resetEntityState(eventQueue, world, entity);
break;
default:
#ifdef DENGINE_DEBUG
ASSERT(INVALID_CODE_PATH);
#endif
break;
}
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;
}
break;
default:
#ifdef DENGINE_DEBUG
ASSERT(INVALID_CODE_PATH);
#endif
break;
}
entity->state = newState;
}
typedef struct AttackSpec
{
Entity *attacker;
Entity *defender;
i32 damage;
} AttackSpec;
INTERNAL void beginAttack(AssetManager *assetManager, MemoryArena *arena,
EventQueue *eventQueue, World *world,
Entity *attacker)
{
#ifdef DENGINE_DEBUG
ASSERT(attacker->stats->entityIdToAttack != ENTITY_NULL_ID);
ASSERT(attacker->state == entitystate_battle);
#endif
entityStateSwitch(eventQueue, world, attacker, entitystate_attack);
switch (attacker->stats->queuedAttack)
{
case entityattack_claudeAttack:
{
entity_setActiveAnim(eventQueue, attacker, "claudeAttack");
if (attacker->direction == direction_east)
attacker->dPos.x += (1.0f * METERS_TO_PIXEL);
else
attacker->dPos.x -= (1.0f * METERS_TO_PIXEL);
break;
}
case entityattack_claudeAttackUp:
{
entity_setActiveAnim(eventQueue, attacker, "claudeAttackUp");
if (attacker->direction == direction_east)
attacker->dPos.x += (1.0f * METERS_TO_PIXEL);
else
attacker->dPos.x -= (1.0f * METERS_TO_PIXEL);
break;
}
case entityattack_claudeAttackDown:
{
entity_setActiveAnim(eventQueue, attacker, "claudeAttackDown");
if (attacker->direction == direction_east)
attacker->dPos.x += (1.0f * METERS_TO_PIXEL);
else
attacker->dPos.x -= (1.0f * METERS_TO_PIXEL);
break;
}
case entityattack_claudeDragonHowl:
{
entity_setActiveAnim(eventQueue, attacker, "claudeDragonHowl");
f32 scale = 1.5f;
v2 size = V2(40, 40);
Entity *projectile = entity_add(
arena, world, attacker->pos, size, scale,
entitytype_projectile, attacker->direction, attacker->tex, TRUE);
projectile->collidesWith[entitytype_hero] = TRUE;
projectile->collidesWith[entitytype_mob] = TRUE;
projectile->collidesWith[attacker->type] = FALSE;
projectile->stats->entityIdToAttack = attacker->stats->entityIdToAttack;
entity_addAnim(assetManager, projectile, "claudeDragonHowlVfx");
entity_setActiveAnim(eventQueue, projectile, "claudeDragonHowlVfx");
break;
}
case entityattack_claudeEnergySword:
{
entity_setActiveAnim(eventQueue, attacker, "claudeEnergySword");
break;
}
case entityattack_claudeRipperBlast:
{
entity_setActiveAnim(eventQueue, attacker, "claudeRipperBlast");
f32 scale = 5.0f;
v2 size = V2(20, 20);
Entity *target = entity_get(world, attacker->stats->entityIdToAttack);
v2 targetPos = v2_add(attacker->pos, V2(100, 0));
if (target)
{
targetPos = target->pos;
}
Entity *projectile = entity_add(
arena, world, targetPos, size, scale,
entitytype_attackObject, attacker->direction, attacker->tex, TRUE);
projectile->collidesWith[entitytype_hero] = TRUE;
projectile->collidesWith[entitytype_mob] = TRUE;
projectile->collidesWith[attacker->type] = FALSE;
projectile->stats->entityIdToAttack = attacker->stats->entityIdToAttack;
entity_addAnim(assetManager, projectile, "claudeRipperBlastVfx");
entity_setActiveAnim(eventQueue, projectile, "claudeRipperBlastVfx");
break;
}
case entityattack_claudeAirSlash:
{
entity_setActiveAnim(eventQueue, attacker, "claudeAirSlash");
f32 scale = 1.5f;
v2 size = V2(20, 20);
Entity *projectile = entity_add(
arena, world, attacker->pos, size, scale,
entitytype_projectile, attacker->direction, attacker->tex, TRUE);
projectile->collidesWith[entitytype_hero] = TRUE;
projectile->collidesWith[entitytype_mob] = TRUE;
projectile->collidesWith[attacker->type] = FALSE;
projectile->stats->entityIdToAttack = attacker->stats->entityIdToAttack;
entity_addAnim(assetManager, projectile, "claudeAirSlashVfx");
entity_setActiveAnim(eventQueue, projectile, "claudeAirSlashVfx");
v2 initialOffset = V2(size.x * 0.5f, 0);
f32 deltaScale = 0.3f;
projectile->numChilds = 3;
for (i32 i = 0; i < projectile->numChilds; i++)
{
v2 childOffset = v2_scale(initialOffset, CAST(f32) i + 1);
scale -= deltaScale;
Entity *child =
entity_add(arena, world, v2_sub(projectile->pos, childOffset),
V2(20, 20), scale, entitytype_projectile,
projectile->direction, projectile->tex, FALSE);
child->collidesWith[entitytype_hero] = FALSE;
child->collidesWith[entitytype_mob] = TRUE;
child->stats->entityIdToAttack =
projectile->stats->entityIdToAttack;
entity_addAnim(assetManager, child, "claudeAirSlashVfx");
entity_setActiveAnim(eventQueue, child, "claudeAirSlashVfx");
projectile->childIds[i] = child->id;
}
break;
}
default:
#ifdef DENGINE_DEBUG
ASSERT(INVALID_CODE_PATH);
#endif
break;
}
EntityAnim attackAnim = attacker->animList[attacker->currAnimId];
f32 busyDuration =
attackAnim.anim->frameDuration * CAST(f32) attackAnim.anim->numFrames;
attacker->stats->busyDuration = busyDuration;
}
// TODO(doyle): MemArena here is temporary until we incorporate AttackSpec to
// battle in general!
INTERNAL void endAttack(MemoryArena *arena, EventQueue *eventQueue,
World *world, Entity *attacker)
{
#ifdef DENGINE_DEBUG
ASSERT(attacker->stats->entityIdToAttack != ENTITY_NULL_ID);
#endif
entityStateSwitch(eventQueue, world, attacker, entitystate_battle);
switch (attacker->stats->queuedAttack)
{
case entityattack_claudeAttack:
case entityattack_claudeAttackUp:
case entityattack_claudeAttackDown:
// 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;
case entityattack_claudeAirSlash:
case entityattack_claudeRipperBlast:
case entityattack_claudeDragonHowl:
break;
case entityattack_claudeEnergySword:
attacker->stats->health += 80;
AttackSpec *attackSpec = PLATFORM_MEM_ALLOC(arena, 1, AttackSpec);
attackSpec->attacker = attacker;
attackSpec->defender = attacker;
attackSpec->damage = 30;
worldTraveller_registerEvent(eventQueue, eventtype_end_attack,
attackSpec);
return;
default:
#ifdef DENGINE_DEBUG
ASSERT(INVALID_CODE_PATH);
#endif
break;
}
/* 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 */
i32 entityIdToAttack = attacker->stats->entityIdToAttack;
i32 entityIndex = entity_getIndex(world, entityIdToAttack);
if (entityIndex != -1)
{
defender = &world->entities[entityIndex];
#ifdef DENGINE_DEBUG
ASSERT(defender->type == entitytype_mob ||
defender->type == entitytype_hero);
#endif
}
else
{
i32 entityIdToAttack = findBestEntityToAttack(world, *attacker);
if (entityIdToAttack == ENTITY_NULL_ID)
{
noMoreValidTargets = TRUE;
}
else
{
attacker->stats->entityIdToAttack =
findBestEntityToAttack(world, *attacker);
}
}
} while (!defender && noMoreValidTargets == TRUE);
if (!noMoreValidTargets)
{
i32 damage = 25;
AttackSpec *attackSpec = PLATFORM_MEM_ALLOC(arena, 1, AttackSpec);
attackSpec->attacker = attacker;
attackSpec->defender = defender;
attackSpec->damage = damage;
worldTraveller_registerEvent(eventQueue, eventtype_end_attack,
attackSpec);
// TODO(doyle): Use attacker stats in battle equations
if (attacker->type == entitytype_hero)
{
defender->stats->health -= damage;
}
else if (attacker->type == entitytype_mob)
{
defender->stats->health -= damage * 0.25f;
}
if (defender->stats->health <= 0.0f) defender->stats->health = 10.0f;
if (defender->stats->health <= 0)
{
entityStateSwitch(eventQueue, world, defender, entitystate_dead);
entityStateSwitch(eventQueue, world, attacker, entitystate_idle);
}
}
}
INTERNAL void sortWorldEntityList(World *world)
{
b32 listHasChanged = TRUE;
i32 numUnsortedEntities = world->freeEntityIndex;
while (listHasChanged)
{
listHasChanged = FALSE;
for (i32 i = 0; i < numUnsortedEntities-1; i++)
{
// TODO(doyle): Better sort algorithm
Entity *entityA = &world->entities[i];
Entity *entityB = &world->entities[i+1];
if (entityA->type == entitytype_null ||
(entityB->type != entitytype_null && entityA->id > entityB->id))
{
Entity tempEntity = world->entities[i];
world->entities[i] = world->entities[i + 1];
world->entities[i + 1] = tempEntity;
listHasChanged = TRUE;
}
}
numUnsortedEntities--;
}
}
INTERNAL enum EntityAttack selectBestAttack(Entity *entity)
{
if (entity->stats->health <= 50.0f && entity->type == entitytype_hero)
{
return entityattack_claudeEnergySword;
}
else
{
enum EntityAttack attack = entityattack_claudeAttack;
i32 choice = rand() % 7;
switch(choice)
{
case 0: attack = entityattack_claudeAttack; break;
case 1: attack = entityattack_claudeAttackUp; break;
case 2: attack = entityattack_claudeAttackDown; break;
case 3: attack = entityattack_claudeDragonHowl; break;
case 4: attack = entityattack_claudeEnergySword; break;
case 5: attack = entityattack_claudeRipperBlast; break;
case 6: attack = entityattack_claudeAirSlash; break;
default: break;
}
return attack;
}
}
INTERNAL i32 entityGetFreeAudioRendererIndex(Entity *entity)
{
i32 result = -1;
for (i32 i = 0; i < entity->numAudioRenderers; i++)
{
if (entity->audioRenderer[i].state == audiostate_stopped)
{
result = i;
break;
}
}
return result;
}
typedef struct DamageDisplay
{
char damageStr[12];
v2 pos;
f32 lifetime;
} DamageDisplay;
typedef struct BattleState
{
DamageDisplay damageDisplay[128];
} BattleState;
GLOBAL_VAR BattleState battleState = {0};
INTERNAL b32 checkCollision(Entity *a, Entity *b)
{
b32 result = FALSE;
if (a->collides && b->collides && a->collidesWith[b->type])
{
Rect aRect = {a->pos, a->hitbox};
v2 aTopLeftP =
getPosRelativeToRect(aRect, V2(0, 0), rectbaseline_topLeft);
v2 aTopRightP =
getPosRelativeToRect(aRect, V2(0, 0), rectbaseline_topRight);
v2 aBottomLeftP =
getPosRelativeToRect(aRect, V2(0, 0), rectbaseline_bottomLeft);
v2 aBottomRightP =
getPosRelativeToRect(aRect, V2(0, 0), rectbaseline_bottomRight);
Rect bRect = {b->pos, b->hitbox};
if (math_pointInRect(bRect, aTopLeftP) ||
math_pointInRect(bRect, aTopRightP) ||
math_pointInRect(bRect, aBottomLeftP) ||
math_pointInRect(bRect, aBottomRightP))
{
result = TRUE;
return result;
}
}
return result;
}
INTERNAL b32 moveEntityAndReturnCollision(World *world, Entity *entity,
v2 ddPos, f32 speedInMs, f32 dt)
{
/*
**************************
* Calculate Hero Speed
**************************
*/
// NOTE(doyle): Clipping threshold for snapping velocity to 0
f32 epsilon = 15.0f;
v2 epsilonDpos = v2_sub(V2(epsilon, epsilon),
V2(ABS(entity->dPos.x), ABS(entity->dPos.y)));
if (epsilonDpos.x >= 0.0f && epsilonDpos.y >= 0.0f)
entity->dPos = V2(0.0f, 0.0f);
f32 speedInPixels = speedInMs * METERS_TO_PIXEL;
ddPos = v2_scale(ddPos, speedInPixels);
// TODO(doyle): Counteracting force on player's acceleration is
// arbitrary
ddPos = v2_sub(ddPos, v2_scale(entity->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(entity->dPos, dt);
v2 oldP = entity->pos;
v2 newP = v2_add(v2_add(ddPosNew, dPos), entity->pos);
entity->pos = newP;
/*
**************************
* Collision Detection
**************************
*/
// TODO(doyle): Only check collision for entities within small
// bounding box of the hero
b32 entityCollided = FALSE;
if (entity->collides)
{
for (i32 i = 0; i < world->maxEntities; i++)
{
Entity *collider = &world->entities[i];
if (collider->state == entitystate_dead) continue;
if (collider->id == entity->id) continue;
entityCollided = checkCollision(entity, collider);
if (entityCollided) break;
}
}
if (entityCollided)
{
entity->dPos = V2(0.0f, 0.0f);
entity->pos = oldP;
}
else
{
// f'(t) = curr velocity = a*t + v, where v is old velocity
entity->dPos = v2_add(entity->dPos, v2_scale(ddPos, dt));
entity->pos = newP;
}
return entityCollided;
}
void worldTraveller_gameUpdateAndRender(GameState *state, f32 dt)
{
// TODO(doyle): use proper dt limiting techniques
if (dt >= 1.0f) dt = 1.0f;
GL_CHECK_ERROR();
AssetManager *assetManager = &state->assetManager;
Renderer *renderer = &state->renderer;
World *const world = &state->world[state->currWorldIndex];
Font *font = &assetManager->font;
MemoryArena *arena = &state->arena;
EventQueue *eventQueue = &state->eventQueue;
/*
**********************
* Parse Input Keys
**********************
*/
{
/*
**********************
* Parse Alphabet Keys
**********************
*/
KeyState *keys = state->input.keys;
for (enum KeyCode code = 0; code < keycode_count; code++)
{
b32 parseKey = FALSE;
KeyState *key = &keys[code];
if (key->newHalfTransitionCount > key->oldHalfTransitionCount)
{
i32 numTransitions =
key->newHalfTransitionCount - key->oldHalfTransitionCount;
if (!IS_EVEN(numTransitions))
key->endedDown = (key->endedDown) ? FALSE : TRUE;
}
if (code >= keycode_a && code <= keycode_z) parseKey = TRUE;
if (code >= keycode_A && code <= keycode_Z) parseKey = TRUE;
if (code >= keycode_0 && code <= keycode_9) parseKey = TRUE;
if (!parseKey) continue;
if (getKeyStatus(key, readkeytype_repeat, 0.15f, dt))
{
// TODO(doyle): Multi press within frame overrides ui parsing
state->uiState.keyChar = code;
}
}
/*
**************************
* Parse Game Control Keys
**************************
*/
if (getKeyStatus(&keys[keycode_enter], readkeytype_oneShot,
KEY_DELAY_NONE, dt))
{
state->uiState.keyEntered = keycode_enter;
}
if (getKeyStatus(&keys[keycode_tab], readkeytype_delayedRepeat, 0.25f,
dt))
{
state->uiState.keyEntered = keycode_tab;
}
if (getKeyStatus(&keys[keycode_space], readkeytype_delayedRepeat, 0.25f,
dt))
{
state->uiState.keyChar = keycode_space;
}
if (getKeyStatus(&keys[keycode_backspace], readkeytype_delayedRepeat,
0.1f, dt))
{
state->uiState.keyEntered = keycode_backspace;
}
if (getKeyStatus(&keys[keycode_left_square_bracket],
readkeytype_delayedRepeat, 0.0f, dt))
{
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(eventQueue, &state->arena, &state->assetManager, world, pos);
}
}
/*
******************************
* Update entities and render
******************************
*/
Rect camera = createWorldBoundedCamera(world, renderer->size);
AudioManager *audioManager = &state->audioManager;
ASSERT(world->freeEntityIndex < world->maxEntities);
for (i32 i = 0; i < world->freeEntityIndex; i++)
{
Entity *const entity = &world->entities[i];
/* Reposition camera if entity is targeted id */
if (world->cameraFollowingId == entity->id)
{
// NOTE(doyle): Set the camera such that the hero is centered
v2 offsetFromEntityToCameraOrigin =
V2((entity->pos.x - (0.5f * state->renderer.size.w)), (0.0f));
// NOTE(doyle): Account for the hero's origin being the bottom left
offsetFromEntityToCameraOrigin.x += (entity->hitbox.w * 0.5f);
world->cameraPos = offsetFromEntityToCameraOrigin;
}
if (entity->state == entitystate_dead) continue;
if (entity->invisible == TRUE) continue;
if (entity->type == entitytype_soundscape)
{
AudioRenderer *audioRenderer = &entity->audioRenderer[0];
if (!state->config.playWorldAudio)
{
// TODO(doyle): Use is playing flag, not just streaming flag
if (audioRenderer->state == audiostate_playing)
{
audio_pauseVorbis(&state->audioManager, audioRenderer);
}
}
else
{
if (world->numEntitiesInBattle > 0)
{
AudioVorbis *battleTheme =
asset_getVorbis(assetManager, "audio_battle");
if (audioRenderer->audio)
{
if (common_strcmp(audioRenderer->audio->key,
"audio_battle") != 0)
{
audio_streamPlayVorbis(arena, &state->audioManager,
audioRenderer, battleTheme,
AUDIO_REPEAT_INFINITE);
}
}
else
{
audio_streamPlayVorbis(arena, &state->audioManager,
audioRenderer, battleTheme,
AUDIO_REPEAT_INFINITE);
}
}
else
{
AudioVorbis *overworldTheme =
asset_getVorbis(assetManager, "audio_overworld");
if (audioRenderer->audio)
{
if (common_strcmp(audioRenderer->audio->key,
"audio_overworld") != 0)
{
audio_streamPlayVorbis(
arena, &state->audioManager, audioRenderer,
overworldTheme, AUDIO_REPEAT_INFINITE);
}
}
else
{
audio_streamPlayVorbis(arena, &state->audioManager,
audioRenderer, overworldTheme,
AUDIO_REPEAT_INFINITE);
}
}
if (audioRenderer->state == audiostate_paused)
{
audio_resumeVorbis(&state->audioManager, audioRenderer);
}
}
}
/*
*****************************************************
* Calculate Mob In Battle Range
*****************************************************
*/
if (entity->type == entitytype_mob)
{
// TODO(doyle): Currently calculated in pixels, how about meaningful
// game units?
f32 battleThreshold = 500.0f;
Entity *hero = getHeroEntity(world);
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;
}
i32 numEntitiesInBattleBefore = world->numEntitiesInBattle;
entityStateSwitch(eventQueue, world, entity, newState);
if (numEntitiesInBattleBefore == 0 &&
world->numEntitiesInBattle > 0)
{
i32 freeAudioIndex = entityGetFreeAudioRendererIndex(hero);
if (freeAudioIndex != -1)
{
char *battleTaunt[11] = {
"Battle_come_on",
"Battle_heh",
"Battle_heres_the_enemy",
"Battle_hey",
"Battle_oh_its_just_them",
"Battle_shouldnt_you_run_away",
"Battle_things_will_work_out_somehow",
"Battle_things_will_work_out",
"Battle_were_gonna_win",
"Battle_we_can_win_this",
"Battle_you_think_you_can_win_over_the_hero_of_light"};
i32 battleTauntSfxIndex = rand() % ARRAY_COUNT(battleTaunt);
audio_playVorbis(
arena, audioManager,
&hero->audioRenderer[freeAudioIndex],
asset_getVorbis(assetManager,
battleTaunt[battleTauntSfxIndex]),
1);
}
}
}
else if (entity->type == entitytype_hero)
{
/*
**************************
* Calculate Hero Movement
**************************
*/
/*
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
*/
Entity *hero = entity;
KeyState *keys = state->input.keys;
v2 ddPos = V2(0, 0);
if (hero->stats->busyDuration <= 0)
{
if (getKeyStatus(&keys[keycode_right], readkeytype_repeat,
KEY_DELAY_NONE, dt))
{
ddPos.x = 1.0f;
hero->direction = direction_east;
}
if (getKeyStatus(&keys[keycode_left], readkeytype_repeat,
KEY_DELAY_NONE, dt))
{
ddPos.x = -1.0f;
hero->direction = direction_west;
}
if (getKeyStatus(&keys[keycode_up], readkeytype_repeat,
KEY_DELAY_NONE, dt))
{
ddPos.y = 1.0f;
}
if (getKeyStatus(&keys[keycode_down], readkeytype_repeat,
KEY_DELAY_NONE, dt))
{
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);
}
}
f32 heroSpeed = 6.2f;
if (getKeyStatus(&state->input.keys[keycode_leftShift],
readkeytype_repeat, KEY_DELAY_NONE, dt))
{
// TODO: Context sensitive command separation
state->uiState.keyMod = keycode_leftShift;
heroSpeed *= 10.0f;
}
else
{
state->uiState.keyMod = keycode_null;
}
moveEntityAndReturnCollision(world, hero, ddPos, heroSpeed, dt);
char *currAnimName = hero->animList[hero->currAnimId].anim->key;
if (ABS(entity->dPos.x) > 0.0f || ABS(entity->dPos.y) > 0.0f)
{
if (common_strcmp(currAnimName, "claudeIdle") == 0)
{
entity_setActiveAnim(eventQueue, hero, "claudeRun");
}
}
else
{
if (common_strcmp(currAnimName, "claudeRun") == 0)
{
entity_setActiveAnim(eventQueue, hero, "claudeIdle");
}
}
}
else if (entity->type == entitytype_projectile)
{
Entity *projectile = entity;
i32 targetIndex =
entity_getIndex(world, projectile->stats->entityIdToAttack);
Entity *target = &world->entities[targetIndex];
v2 ddPos = V2(0, 0);
f32 projectileSpeed = 10.0f;
v2 difference = v2_sub(target->pos, projectile->pos);
f32 longSide = (ABS(difference.x) > ABS(difference.y))
? ABS(difference.x)
: ABS(difference.y);
ddPos.x = (difference.x / longSide);
ddPos.y = (difference.y / longSide);
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);
}
b32 collided = moveEntityAndReturnCollision(
world, projectile, ddPos, projectileSpeed, dt);
if (collided)
{
// TODO(doyle): Unify concept of dead entity for mobs and
// projectiles
projectile->state = entitystate_dead;
// TODO(doyle): register endattack event
worldTraveller_registerEvent(eventQueue, eventtype_entity_died,
projectile);
}
}
else if (entity->type == entitytype_attackObject)
{
if (entity->animList[entity->currAnimId].timesCompleted == 1)
{
entity->state = entitystate_dead;
worldTraveller_registerEvent(eventQueue, eventtype_entity_died,
entity);
}
}
/*
**************************************************
* Conduct Battle For Mobs In Range
**************************************************
*/
if (entity->type == entitytype_mob || entity->type == entitytype_hero)
{
EntityStats *stats = entity->stats;
if (stats->weapon)
{
Entity *weapon = stats->weapon;
weapon->pos = entity->pos;
// TODO(doyle): Add concept of entity origin and make all
// transform start from that origin point
if (entity->direction == direction_east)
{
weapon->pos.x += entity->size.w;
weapon->pos.x -= weapon->size.w;
}
weapon->direction = entity->direction;
SubTexture entitySubTexture =
entity_getActiveSubTexture(entity);
weapon->pos.y += entitySubTexture.offset.y;
if (weapon->direction == direction_west)
{
weapon->pos.x += entitySubTexture.offset.x;
// TODO(doyle): Typedef rotation to degrees for type safety
weapon->rotation = DEGREES_TO_RADIANS(60.0f);
}
else
{
weapon->pos.x -= entitySubTexture.offset.x;
weapon->rotation = DEGREES_TO_RADIANS(-60.0f);
}
}
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)
{
enum EntityAttack attack = selectBestAttack(entity);
stats->queuedAttack = attack;
}
/* Launch up attack animation */
beginAttack(assetManager, &state->arena, eventQueue, 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)
{
/* Apply attack damage */
endAttack(&state->arena, eventQueue, world, entity);
}
}
}
/*
****************
* Update Entity
****************
*/
for (i32 i = 0; i < entity->numAudioRenderers; i++)
{
audio_updateAndPlay(&state->arena, &state->audioManager,
&entity->audioRenderer[i]);
}
if (entity->tex)
{
entity_updateAnim(eventQueue, entity, dt);
/* Calculate region to render */
renderer_entity(renderer, camera, entity,
v2_scale(entity->size, 0.5f), 0, V4(1, 1, 1, 1.0f));
}
}
#if RENDERER_USE_RENDER_GROUPS
renderer_renderGroups(renderer);
#endif
/*
*****************************************
* Process Events From Entity Update Loop
*****************************************
*/
i32 numDeadEntities = 0;
for (i32 i = 0; i < eventQueue->numEvents; i++)
{
Event event = eventQueue->queue[i];
eventQueue->queue[i].type = eventtype_null;
eventQueue->queue[i].data = NULL;
switch(event.type)
{
case eventtype_end_attack:
{
if (!event.data) continue;
AttackSpec *attackSpec = (CAST(AttackSpec *) event.data);
Entity *attacker = attackSpec->attacker;
Entity *defender = attackSpec->defender;
if (attacker->stats->queuedAttack == entityattack_claudeAttack ||
attacker->stats->queuedAttack == entityattack_claudeAttackUp ||
attacker->stats->queuedAttack == entityattack_claudeAttackDown)
{
i32 freeAudioIndex = entityGetFreeAudioRendererIndex(attacker);
if (freeAudioIndex != -1)
{
char *attackSfx[11] = {
"Attack_1", "Attack_2", "Attack_3",
"Attack_4", "Attack_5", "Attack_6",
"Attack_7", "Attack_hit_it", "Attack_take_that",
"Attack_hya", "Attack_burn"};
i32 attackSfxIndex = rand() % ARRAY_COUNT(attackSfx);
audio_playVorbis(arena, audioManager,
&attacker->audioRenderer[freeAudioIndex],
asset_getVorbis(assetManager,
attackSfx[attackSfxIndex]),
1);
}
freeAudioIndex = entityGetFreeAudioRendererIndex(defender);
if (freeAudioIndex != -1)
{
char *hurtSfx[10] = {"Hurt_1", "Hurt_2",
"Hurt_3", "Hurt_hows_this",
"Hurt_battlecry", "Hurt_ow",
"Hurt_uh_oh", "Hurt_ugh",
"Hurt_woah", "Hurt_yearning"};
i32 hurtSfxIndex = rand() % ARRAY_COUNT(hurtSfx);
audio_playVorbis(
arena, audioManager,
&defender->audioRenderer[freeAudioIndex],
asset_getVorbis(assetManager, hurtSfx[hurtSfxIndex]),
1);
}
}
else if (attacker->stats->queuedAttack ==
entityattack_claudeEnergySword)
{
i32 freeAudioIndex = entityGetFreeAudioRendererIndex(attacker);
if (freeAudioIndex != -1)
{
audio_playVorbis(
arena, audioManager,
&attacker->audioRenderer[freeAudioIndex],
asset_getVorbis(assetManager, "Attack_energy_sword"),
1);
}
}
else if (attacker->stats->queuedAttack ==
entityattack_claudeAirSlash)
{
i32 freeAudioIndex = entityGetFreeAudioRendererIndex(attacker);
if (freeAudioIndex != -1)
{
audio_playVorbis(
arena, audioManager,
&attacker->audioRenderer[freeAudioIndex],
asset_getVorbis(assetManager, "Attack_air_slash"),
1);
}
}
else if (attacker->stats->queuedAttack ==
entityattack_claudeDragonHowl)
{
i32 freeAudioIndex = entityGetFreeAudioRendererIndex(attacker);
if (freeAudioIndex != -1)
{
audio_playVorbis(
arena, audioManager,
&attacker->audioRenderer[freeAudioIndex],
asset_getVorbis(assetManager, "Attack_Dragon_howl"),
1);
}
}
else if (attacker->stats->queuedAttack ==
entityattack_claudeRipperBlast)
{
i32 freeAudioIndex = entityGetFreeAudioRendererIndex(attacker);
if (freeAudioIndex != -1)
{
audio_playVorbis(
arena, audioManager,
&attacker->audioRenderer[freeAudioIndex],
asset_getVorbis(assetManager, "Attack_tear_into_pieces"),
1);
}
}
else
{
//ASSERT(INVALID_CODE_PATH);
}
attacker->stats->queuedAttack = entityattack_invalid;
/* Get first free string position and store the damage str data */
for (i32 i = 0; i < ARRAY_COUNT(battleState.damageDisplay); i++)
{
if (battleState.damageDisplay[i].lifetime <= 0.0f)
{
common_memset(
battleState.damageDisplay[i].damageStr, 0,
ARRAY_COUNT(battleState.damageDisplay[i].damageStr));
battleState.damageDisplay[i].lifetime = 1.0f;
battleState.damageDisplay[i].pos = defender->pos;
common_itoa(
attackSpec->damage,
battleState.damageDisplay[i].damageStr,
ARRAY_COUNT(battleState.damageDisplay[i].damageStr));
break;
}
}
PLATFORM_MEM_FREE(&state->arena, attackSpec, sizeof(AttackSpec));
break;
}
// NOTE(doyle): We delete dead entities at the end of the update
// loop incase a later indexed entity deletes an earlier indexed
// entity, the entity will exist for an additional frame
case eventtype_entity_died:
{
#ifdef DEGINE_DEBUG
DEBUG_LOG("Entity died: being deleted");
#endif
if (!event.data) continue;
Entity *entity = (CAST(Entity *) event.data);
for (i32 i = 0; i < entity->numAudioRenderers; i++)
{
audio_stopVorbis(&state->arena, audioManager,
&entity->audioRenderer[i]);
}
entity_clearData(&state->arena, world, entity);
numDeadEntities++;
for (i32 i = 0; i < entity->numChilds; i++)
{
Entity *child = entity_get(world, entity->childIds[i]);
if (child)
{
for (i32 i = 0; i < child->numAudioRenderers; i++)
{
audio_stopVorbis(&state->arena, audioManager,
&child->audioRenderer[i]);
}
entity_clearData(&state->arena, world, child);
numDeadEntities++;
}
else
{
DEBUG_LOG("Entity child expected but not found");
}
}
break;
}
default:
{
break;
}
}
}
eventQueue->numEvents = 0;
/*
****************************
* Update World From Results
****************************
*/
// TODO(doyle): Dead hero not accounted for here
Entity *hero = getHeroEntity(world);
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(eventQueue, world, hero, entitystate_idle);
}
else if (hero->state != entitystate_attack)
{
entityStateSwitch(eventQueue, world, hero, entitystate_battle);
}
}
else
{
if (hero->state == entitystate_battle)
{
hero->state = entitystate_idle;
world->entityIdInBattle[hero->id] = FALSE;
entity_setActiveAnim(eventQueue, hero, "claudeIdle");
}
hero->stats->entityIdToAttack = -1;
hero->stats->actionTimer = hero->stats->actionRate;
hero->stats->busyDuration = 0;
}
sortWorldEntityList(world);
world->freeEntityIndex -= numDeadEntities;
/*
********************
* UI Rendering Code
********************
*/
/* Render all damage strings */
for (i32 i = 0; i < ARRAY_COUNT(battleState.damageDisplay); i++)
{
if (battleState.damageDisplay[i].lifetime > 0.0f)
{
battleState.damageDisplay[i].lifetime -= dt;
char *damageString = battleState.damageDisplay[i].damageStr;
v2 damagePos = battleState.damageDisplay[i].pos;
f32 lifetime = battleState.damageDisplay[i].lifetime;
// TODO(doyle): Incorporate UI into entity list or it's own list
// with a rendering lifetime value
renderer_string(renderer, &state->arena, camera, font,
damageString, damagePos, V2(0, 0), 0,
V4(1, 1, 1, lifetime));
}
}
// INIT IMGUI
state->uiState.hotItem = 0;
/* Draw ui */
if (state->uiState.keyChar == keycode_i)
{
state->config.showStatMenu =
(state->config.showStatMenu == TRUE) ? FALSE : TRUE;
}
if (state->config.showStatMenu)
{
WindowState *statWindow = &state->uiState.statWindow;
userInterface_window(&state->uiState, &state->arena, assetManager,
renderer, font, state->input, statWindow);
}
/* Draw debug window */
WindowState *debugWindow = &state->uiState.debugWindow;
i32 activeId =
userInterface_window(&state->uiState, &state->arena, assetManager,
renderer, font, state->input, debugWindow);
// TODO(doyle): Name lookup to user interface id
if (activeId == 1)
{
state->config.playWorldAudio =
(state->config.playWorldAudio == TRUE) ? FALSE : TRUE;
}
else if (activeId == 2)
{
state->config.showDebugDisplay =
(state->config.showDebugDisplay == TRUE) ? FALSE : TRUE;
}
// RESET IMGUI
if (!state->input.keys[keycode_mouseLeft].endedDown)
state->uiState.activeItem = 0;
else if (state->uiState.activeItem == 0)
state->uiState.activeItem = -1;
if (state->uiState.keyEntered == keycode_tab) state->uiState.kbdItem = 0;
state->uiState.keyEntered = keycode_null;
state->uiState.keyChar = keycode_null;
/* Draw hero avatar */
TexAtlas *heroAtlas =
asset_getTexAtlas(assetManager, "ClaudeSprite.png");
SubTexture heroAvatarRect =
asset_getAtlasSubTex(heroAtlas, "ClaudeSprite_Avatar_01");
v2 heroAvatarP = V2(10.0f, (renderer->size.h * 0.5f) -
(0.5f * heroAvatarRect.rect.size.h));
// TODO(doyle): Use rect in rendering not V4
v4 heroAvatarTexRect = {0};
heroAvatarTexRect.vec2[0] = heroAvatarRect.rect.pos;
heroAvatarTexRect.vec2[1] =
v2_add(heroAvatarRect.rect.pos, heroAvatarRect.rect.size);
RenderTex heroRenderTex = {hero->tex, heroAvatarTexRect};
renderer_staticRect(renderer, heroAvatarP, heroAvatarRect.rect.size, 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 * heroAvatarRect.rect.size.h));
renderer_staticString(&state->renderer, &state->arena, font, heroAvatarStr,
strPos, V2(0, 0), 0, V4(0, 0, 1, 1));
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);
v2 heroCenter = v2_add(hero->pos, v2_scale(hero->hitbox, 0.5f));
RenderTex renderTex = renderer_createNullRenderTex(assetManager);
f32 distance = v2_magnitude(hero->pos, entity->pos);
renderer_rect(&state->renderer, camera, heroCenter,
V2(distance, 2.0f), V2(0, 0), angle, renderTex,
V4(1, 0, 0, 1.0f));
}
}
for (i32 i = 0; i < keycode_count; i++)
{
state->input.keys[i].oldHalfTransitionCount =
state->input.keys[i].newHalfTransitionCount;
}
/*
********************
* DEBUG CODE
********************
*/
#ifdef DENGINE_DEBUG
for (i32 i = 0; i < world->freeEntityIndex-1; i++)
{
ASSERT(world->entities[i].type != entitytype_null);
ASSERT(world->entities[i].id < world->entities[i+1].id);
}
#endif
#ifdef DENGINE_DEBUG
if (state->config.showDebugDisplay)
{
debug_drawUi(state, dt);
}
#endif
}