Dengine/src/WorldTraveller.c
Doyle Thai 23720fae19 Fix batch-render of strings, fix group logic
Incorrect assumption that strings only needed degenerate vertexes at the start
and end of each string instead of characters causing render "wings" between
separate strings.

Fix render-group not switching to next group if current group is full.
2016-09-22 18:46:53 +10:00

2466 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));
}
}
/*
*****************************************
* 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
#if RENDERER_USE_RENDER_GROUPS
renderer_renderGroups(renderer);
#endif
}