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.
2466 lines
72 KiB
C
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
|
|
|
|
}
|