#include #include #include #include #include "systems/systems.h" #include "components.h" #include "game_state.h" #include "game_tileset.h" #include "input.h" #include "map_init.h" #include "map_layers.h" #include "buildings.h" #include "ui_widgets.h" #include "utils.h" #include "pathfinding.h" #include "sounds.h" #include "entity_factory.h" // Constants const char *GAME_DATA_SAVE_PATH = "game_data.ini"; // Constants end ECS_COMPONENT_DECLARE(Game); ECS_COMPONENT_DECLARE(InputState); ECS_COMPONENT_DECLARE(SoundState); BzUI *UI = NULL; ecs_world_t *ECS = NULL; bool init(void *userData); void deinit(void *userData); void update(float dt, void *userData); void render(float dt, void *userData); void imguiRender(float dt, void *userData); static bool loopPaused = true; extern void loopPause(void) { loopPaused = true; } extern void loopResume(void) { loopPaused = false; } #ifdef EMSCRIPTEN #include #endif bool bzMain(BzAppDesc *appDesc, int argc, const char **argv) { appDesc->width = 1280; appDesc->height = 720; appDesc->title = "PixelDefense"; appDesc->fps = 60; appDesc->init = (BzAppInitFunc) init; appDesc->deinit = (BzAppDeinitFunc) deinit; appDesc->update = (BzAppUpdateFunc) update; appDesc->render = (BzAppRenderFunc) render; appDesc->imguiRender = (BzAppRenderFunc) imguiRender; appDesc->userData = NULL; //SetConfigFlags(FLAG_WINDOW_RESIZABLE); #ifdef EMSCRIPTEN EM_ASM( FS.mkdir('/game'); FS.mount(IDBFS, {}, '/game'); FS.syncfs(true, function (err) { console.log("resumed"); Module.ccall('loopResume', 'void', ['void']); }); console.log('/game mounted'); /* * Note: FS.syncfs is async so we need to pause the * main loop until it finishes :/ */ Module.ccall('loopPause', 'void', ['void']); ); loopPaused = true; #else loopPaused = false; #endif return true; } int cmpDrawData(const void *a, const void *b) { const DrawData *lhs = (DrawData *) a; const DrawData *rhs = (DrawData *) b; f32 dif = (rhs->dst.y) - (lhs->dst.y); int cmpVal = 0; if (dif < 0) cmpVal = 1; else if (dif > 0) cmpVal = -1; return cmpVal; } bool serializeGameData(const char *path, const GameData *gameData) { #ifdef EMSCRIPTEN char buf[4096]; snprintf(buf, sizeof(buf), "%s%s", "/game/", path); path = buf; #endif bzLogInfo("write path: %s", path); FILE *f = fopen(path, "w"); if (!f) return false; size_t numWritten = fwrite(gameData, sizeof(*gameData), 1, f); fclose(f); #ifdef EMSCRIPTEN // Sync FS EM_ASM( FS.syncfs(false, function (err) { }); ); #endif return numWritten == 1; } bool deserializeGameData(const char *path, GameData *gameData) { #ifdef EMSCRIPTEN char buf[4096]; snprintf(buf, sizeof(buf), "%s%s", "/game/", path); path = buf; #endif bzLogInfo("read path: %s", path); FILE *f = fopen(path, "r"); bzLogInfo("start reading"); if (!f) return false; bzLogInfo("reading"); GameData data; size_t numRead = fread(&data, sizeof(data), 1, f); fclose(f); if (numRead == 1) { *gameData = data; return true; } return false; } void applyOptions(const Options *options) { SoundState *sounds = ecs_singleton_get_mut(ECS, SoundState); soundsApplyVolume(sounds, options->master, options->music, options->sound); bool isFullscreen = IsWindowFullscreen(); bool wantFullscreen = options->fullscreen; if (isFullscreen != wantFullscreen) ToggleFullscreen(); if (options->limitFps) SetTargetFPS(60); else SetTargetFPS(0); } bool init(void *userData) { // Center window int monitor = GetCurrentMonitor(); int monitorWidth = GetMonitorWidth(monitor); int monitorHeight = GetMonitorHeight(monitor); int windowWidth = GetScreenWidth(); int windowHeight = GetScreenHeight(); SetWindowPosition((int)(monitorWidth / 2) - (int)(windowWidth / 2), (int)(monitorHeight / 2) - (int)(windowHeight / 2)); BZ_UNUSED(userData); SetExitKey(0); UI = bzUICreate(); ECS = ecs_init(); initComponentIDs(ECS); // For ecs explorer //ecs_singleton_set(ECS, EcsRest, {0}); //ECS_IMPORT(ECS, FlecsMonitor); ECS_COMPONENT_DEFINE(ECS, Game); ecs_singleton_set(ECS, Game, {}); Game *game = ecs_singleton_get_mut(ECS, Game); game->font = LoadFontEx("assets/fonts/CompassPro.ttf", 92, NULL, 0); game->drawQuery = ecs_query(ECS, { .filter.terms = { { ecs_id(Position) }, { ecs_id(Size) }, { ecs_id(Rotation) }, { ecs_id(TextureRegion) } }, }); game->particles0Query = ecs_query(ECS, { .filter.terms = { { ecs_id(ParticleLayer0) } } }); game->particles1Query = ecs_query(ECS, { .filter.terms = { { ecs_id(ParticleLayer1) } } }); { ECS_COMPONENT_DEFINE(ECS, InputState); ecs_singleton_set(ECS, InputState, {}); InputState *input = ecs_singleton_get_mut(ECS, InputState); input->mapping = inputDefaultMapping(); // Create queries input->queries.selected = ecs_query(ECS, { .filter.terms = { {ecs_id(Position)}, {ecs_id(HitBox)}, {ecs_id(Selected)} } }); input->building = BUILDING_NONE; } { InitAudioDevice(); ECS_COMPONENT_DEFINE(ECS, SoundState); ecs_singleton_set(ECS, SoundState, {}); SoundState *sounds = ecs_singleton_get_mut(ECS, SoundState); sounds->masterVolume = 0.5f; sounds->musicVolume = 0.5f; sounds->soundVolume = 0.5f; soundsLoad(sounds, SOUND_WOOD_PUNCH, 0.1f, "assets/sounds/wood hit 17.wav"); } setScreen(game, SCREEN_MAIN_MENU); game->stackAlloc = bzStackAllocCreate(1 * 1000 * 1000); // 1 MB // init pools game->pools.pathData = bzObjectPoolCreate(&(BzObjectPoolDesc) { .objectSize = sizeof(PathData), .objectsPerPage = 512 }); game->pools.btNode = bzObjectPoolCreate(&(BzObjectPoolDesc) { .objectSize = bzBTGetNodeSize(), .objectsPerPage = 64 }); game->pools.btNodeState = bzObjectPoolCreate(&(BzObjectPoolDesc) { .objectSize = bzBTGetNodeStateSize(), .objectsPerPage = 1024, }); BzObjectPool *nodePool = game->pools.btNode; // moveTo { BzBTNode *root = NULL; BzBTNode *node = NULL; root = bzBTMakeRoot(nodePool); game->BTs.moveTo = root; // Just a single action for now node = bzBTAction(nodePool, root, (BzBTActionFn) aiMoveTo); bzBTNodeSetName(node, "moveTo"); } // evade BzBTNode *evade = NULL; { BzBTNode *node = NULL; evade = bzBTMakeRoot(nodePool); BzBTNode *evadeSeq = bzBTCompSequence(nodePool, evade); { node = bzBTAction(nodePool, evadeSeq, (BzBTActionFn) aiIsEnemyNearby); bzBTNodeSetName(node, "enemyNearby"); node = bzBTAction(nodePool, evadeSeq, (BzBTActionFn) aiEvadeTarget); bzBTNodeSetName(node, "evadeTarget"); } } // worker harvest { BzBTNode *root = NULL; BzBTNode *node = NULL; root = bzBTMakeRoot(nodePool); game->BTs.workerHarvest = root; BzBTNode *pSel = bzBTCompPSelector(nodePool, root); bzBTSubTree(evade, pSel); BzBTNode *collectSeq = bzBTCompSequence(nodePool, pSel); BzBTNode *untilFail = bzBTDecorUntilFail(nodePool, collectSeq); { BzBTNode *untilSeq = bzBTCompSequence(nodePool, untilFail); node = bzBTAction(nodePool, untilSeq, (BzBTActionFn) aiFindNextHarvestable); bzBTNodeSetName(node, "findNextHarvestable"); node = bzBTAction(nodePool, untilSeq, (BzBTActionFn) aiMoveTo); bzBTNodeSetName(node, "moveTo"); node = bzBTAction(nodePool, untilSeq, (BzBTActionFn) aiResetElapsed); bzBTNodeSetName(node, "resetElapsed"); node = bzBTAction(nodePool, untilSeq, (BzBTActionFn) aiHarvestRes); bzBTNodeSetName(node, "harvestRes"); node = bzBTDecorInvert(nodePool, untilSeq); node = bzBTAction(nodePool, node, (BzBTActionFn) aiCarryCapacityFull); bzBTNodeSetName(node, "carryCapacityFull"); } node = bzBTDecorInvert(nodePool, collectSeq); node = bzBTAction(nodePool, node, (BzBTActionFn) aiCarryCapacityEmpty); bzBTNodeSetName(node, "carryCapacityEmpty"); node = bzBTAction(nodePool, collectSeq, (BzBTActionFn) aiFindNearestStorage); bzBTNodeSetName(node, "findNearestStorage"); node = bzBTAction(nodePool, collectSeq, (BzBTActionFn) aiMoveTo); bzBTNodeSetName(node, "moveTo"); node = bzBTAction(nodePool, collectSeq, (BzBTActionFn) aiResetElapsed); bzBTNodeSetName(node, "resetElapsed"); node = bzBTAction(nodePool, collectSeq, (BzBTActionFn) aiDepositRes); bzBTNodeSetName(node, "depositRes"); node = bzBTDecorDelay(nodePool, collectSeq, 1.0f); //node = bzBTAction(nodePool, collectSeq, NULL); //bzBTNodeSetName(node, "harvest"); //node = bzBTAction(nodePool, collectSeq, NULL); //bzBTNodeSetName(node, "moveTo"); //node = bzBTAction(nodePool, collectSeq, NULL); //bzBTNodeSetName(node, "deposit"); } game->frameDuration = 0.16f; game->tileset = bzTilesetCreate( &(BzTilesetDesc) { .path="assets/game.tsj", .texturePath="assets/game.png" }); // Fixes texture bleeding GenTextureMipmaps(&game->tileset.tiles); SetTextureWrap(game->tileset.tiles, TEXTURE_WRAP_CLAMP); SetTextureFilter(game->tileset.tiles, TEXTURE_FILTER_POINT); setupSystems(); loadMap(game, "assets/maps/main_menu_01.tmj"); game->debug.drawMapColliders = false; game->debug.drawSpatialGrid = false; game->debug.drawPath = false; game->debug.inspecting = bzArrayCreate(ecs_entity_t, 10); return true; } void deinit(void *userData) { BZ_UNUSED(userData); Game *game = ecs_singleton_get_mut(ECS, Game); InputState *input = ecs_singleton_get_mut(ECS, InputState); SoundState *sounds = ecs_singleton_get_mut(ECS, SoundState); serializeGameData(GAME_DATA_SAVE_PATH, &game->gameData); unloadMap(game); // Destroy queries ecs_query_fini(input->queries.selected); ecs_query_fini(game->drawQuery); ecs_query_fini(game->particles0Query); ecs_query_fini(game->particles1Query); Game gameCopy = *game; InputState inputCopy = *input; SoundState soundsCopy = *sounds; // Destroy ECS ecs_fini(ECS); ECS = NULL; game = &gameCopy; input = &inputCopy; sounds = &soundsCopy; bzArrayDestroy(game->debug.inspecting); bzTilesetDestroy(&game->tileset); bzStackAllocDestroy(&game->stackAlloc); bzObjectPoolDestroy(game->pools.pathData); bzObjectPoolDestroy(game->pools.btNode); bzObjectPoolDestroy(game->pools.btNodeState); soundsUnloadAll(sounds); soundsUnloadMusicStream(sounds); bzUIDestroy(UI); UI = NULL; UnloadFont(game->font); } void update(float dt, void *userData) { if (loopPaused) return; BZ_UNUSED(userData); char titleBuf[32]; snprintf(titleBuf, sizeof(titleBuf), "FPS: %d | %.2f ms", GetFPS(), GetFrameTime() * 1000); SetWindowTitle(titleBuf); Game *game = ecs_singleton_get_mut(ECS, Game); InputState *input = ecs_singleton_get_mut(ECS, InputState); { static bool gameDataLoaded = false; if (!gameDataLoaded) { // Load settings const char *path = GAME_DATA_SAVE_PATH; GameData gameData = getDefaultGameData(); if (deserializeGameData(path, &gameData)) { bzLogWarning("Failed to read game data: %s.", path); } game->gameData = gameData; applyOptions(&gameData.options); gameDataLoaded = true; } } game->screenPrevFrame = game->screen; if (game->screen != game->nextScreen) game->screen = game->nextScreen; bzStackAllocReset(&game->stackAlloc); ImGuiIO *io = igGetIO(); bzUISetCaptureMouse(UI, !io->WantCaptureMouse); bzUISetCaptureKeyboard(UI, !io->WantCaptureKeyboard); input->canUseMouse = !io->WantCaptureMouse && !bzUICapturedMouse(UI); input->canUseKeyboard = !io->WantCaptureKeyboard && !bzUICapturedKeyboard(UI); updateInputState(input, game->camera, dt); switch (game->screen) { case SCREEN_GAME: updatePlayerInput(); break; case SCREEN_PAUSE_MENU: if (IsKeyReleased(input->mapping.backBtn)) { setScreen(game, SCREEN_GAME); } break; case SCREEN_MAIN_MENU: break; case SCREEN_SETTINGS: break; } SoundState *soundState = ecs_singleton_get_mut(ECS, SoundState); soundsUpdate(soundState); } static void drawOverScreen(Color c) { i32 width = GetScreenWidth(); i32 height = GetScreenHeight(); DrawRectangle(0, 0, width, height, c); } static void renderGame(Game *game, float dt) { ClearBackground(RAYWHITE); BeginMode2D(game->camera); // Map BzTileMap *map = &game->map; bzTileMapDraw(map); // Ground UI drawPlayerInputUIGround(); // Entities i32 numDraws = ecs_query_entity_count(game->drawQuery); DrawData *drawData = bzStackAlloc(&game->stackAlloc, numDraws * sizeof(*drawData)); ecs_iter_t it = ecs_query_iter(ECS, game->drawQuery); i32 drawIdx = 0; Camera2D camera = game->camera; Rectangle camBounds = getCameraBounds(camera); while (ecs_iter_next(&it)) { Position *p = ecs_field(&it, Position, 1); Size *s = ecs_field(&it, Size , 2); Rotation *r = ecs_field(&it, Rotation, 3); TextureRegion *t = ecs_field(&it, TextureRegion, 4); for (i32 i = 0; i < it.count; i++) { Rectangle dst = {p[i].x, p[i].y - s[i].y, s[i].x, s[i].y}; DrawCircleV(p[i], 1.0f, BLUE); Vector2 origin = {dst.width * 0.5f, dst.height}; dst.x += origin.x; dst.y += origin.y; if (!CheckCollisionRecs(camBounds, dst)) continue; Rectangle src = t[i].rec; // Fixes texture bleeding issue src.x += 0.01f; src.y += 0.01f; src.width -= 0.02f; src.height -= 0.02f; if (t[i].flipX) src.width *= -1.0f; if (t[i].flipY) src.height *= -1.0f; DrawData draw = (DrawData) { .src = src, .dst = dst, .origin = origin, .rotation = r[i], .canHaveAlpha = true, }; if (ecs_has_id(ECS, it.entities[i], ecs_id(Unit)) || ecs_has_id(ECS, it.entities[i], ecs_id(Arm))) { Vector2 pos = {dst.x, dst.y}; Vec2i cellPos = bzTileMapPosToTile(map, pos); bzTileMapSetCollisions(map, true, COLL_LAYER_TRANSPARENCY, cellPos.x, cellPos.y, 1, 1); draw.canHaveAlpha = false; } drawData[drawIdx++] = draw; } } numDraws = drawIdx; qsort(drawData, numDraws, sizeof(*drawData), cmpDrawData); Texture2D tex = game->tileset.tiles; for (i32 i = 0; i < numDraws; i++) { DrawData draw = drawData[i]; Color c = WHITE; if (draw.canHaveAlpha) { Vector2 center = { draw.dst.x, draw.dst.y - draw.dst.height * 0.5f }; Vec2i mapPos = bzTileMapPosToTile(map, center); if (bzTileMapHasCollision(map, COLL_LAYER_TRANSPARENCY, mapPos.x, mapPos.y)) { c.a = 180; } } DrawTexturePro(tex, draw.src, draw.dst, draw.origin, draw.rotation, c); } bzTileMapClearCollisionLayer(&game->map, COLL_LAYER_TRANSPARENCY); // Draw particle layer 1 it = ecs_query_iter(ECS, game->particles1Query); ecs_defer_begin(ECS); while (ecs_iter_next(&it)) { Particle *particle = ecs_field(&it, Particle, 1); for (i32 i = 0; i < it.count; i++) { DrawCircleV(particle[i].pos, 2.0f, RED); if (updateParticle(tex, &particle[i], dt)) ecs_delete(ECS, it.entities[i]); } } ecs_defer_end(ECS); #if 0 Vector2 target = GetMousePosition(); target = GetScreenToWorld2D(target, game->camera); static f32 elapsed = 0; static bool attack = false; static Vector2 lockedTarget; if (!attack && IsMouseButtonPressed(0)) { attack = true; lockedTarget = target; elapsed = 0; } elapsed += dt * 2; elapsed = Clamp(elapsed, 0, 1.0f); attack = false; if (worker && false) { Position *pos = ecs_get_mut(ECS, worker, Position); DrawCircle(pos->x, pos->y, 2.0f, BLUE); Vector2 attackVector = Vector2Subtract(lockedTarget, *pos); attackVector = Vector2Normalize(attackVector); attackVector = Vector2Scale(attackVector, 2.0f); DrawLine(pos->x, pos->y, pos->x + attackVector.x, pos->y + attackVector.y, RED); Rotation *rot = ecs_get_mut(ECS, worker, Rotation); f32 targetRot = Vector2Angle(*pos, lockedTarget); targetRot += 25 * DEG2RAD; *rot = targetRot * bzEase(BZ_EASE_IN_BACK, elapsed); bzLogInfo("%.2f", Vector2Angle(*pos, lockedTarget) * RAD2DEG); } #endif ecs_progress(ECS, dt); ecs_enable(ECS, renderDebugPathSystem, game->debug.drawPath); ecs_enable(ECS, renderCollidersSystem, game->debug.drawEntityColliders); if (game->debug.drawMapColliders) bzTileMapDrawCollisions(&game->map); if (game->debug.drawSpatialGrid) bzSpatialGridDrawDebugGrid(game->entityGrid); drawPlayerInputUI(); EndMode2D(); } void render(float dt, void *userData) { if (loopPaused) return; BZ_UNUSED(userData); Game *game = ecs_singleton_get_mut(ECS, Game); Color shadow = BLACK; shadow.a = 35; switch (game->screen) { case SCREEN_GAME: renderGame(game, dt); drawGameUI(game, dt); break; case SCREEN_PAUSE_MENU: renderGame(game, dt); drawOverScreen(shadow); drawPauseUI(game, dt); break; case SCREEN_MAIN_MENU: renderGame(game, dt); drawOverScreen(shadow); drawMainMenuUI(game, dt); break; case SCREEN_SETTINGS: renderGame(game, dt); drawOverScreen(shadow); drawSettingsUI(game, dt); break; } } void igInspectComp(const char *label, ecs_entity_t entity, ecs_entity_t comp, ImGuiCompFn fn) { igPushID_Int(comp); if (igTreeNode_Str(label)) { bool isAttached = ecs_has_id(ECS, entity, comp); igCheckbox("Attached", &isAttached); if (isAttached) fn(ECS, entity, comp); if (isAttached != ecs_has_id(ECS, entity, comp)) { if (!isAttached) { ecs_remove_id(ECS, entity, comp); } else { ecs_set_id(ECS, entity, comp, 0, NULL); } } igTreePop(); } igPopID(); } void igInspectWindow(ecs_entity_t entity, bool *open) { if (!ecs_is_alive(ECS, entity)) { *open = false; return; } igSetNextWindowSize((ImVec2) {300, 440}, ImGuiCond_FirstUseEver); char buf[64]; snprintf(buf, sizeof(buf), "Entity: %ld", entity); if (igBegin(buf, open, 0)) { if (igSmallButton("Destroy")) { ecs_delete(ECS, entity); igEnd(); *open = false; return; } if (igCollapsingHeader_TreeNodeFlags("Tags", 0)) { //igTagCheckbox("GameEntity", ECS, entity, GameEntity); igTagCheckbox("Selectable", ECS, entity, Selectable); igTagCheckbox("Selected", ECS, entity, Selected); igTagCheckbox("Storage", ECS, entity, Storage); igTagCheckbox("Harvestable", ECS, entity, Harvestable); igTagCheckbox("Attackable", ECS, entity, Attackable); } if (ecs_has(ECS, entity, BzBTState) && igCollapsingHeader_TreeNodeFlags("BehaviourTree", 0)) { const BzBTState *state = ecs_get(ECS, entity, BzBTState); if (state->root) igVisualizeBTState(state->root, state->_first); else igTextColored((ImVec4) {1, 0, 0, 1}, "NONE"); } if (igCollapsingHeader_TreeNodeFlags("Components", 0)) { igInspectComp("Resource", entity, ecs_id(Resource), igResource); igInspectComp("Owner", entity, ecs_id(Owner), igOwner); igInspectComp("SpatialGridID", entity, ecs_id(SpatialGridID), igSpatialGridID); igInspectComp("Position", entity, ecs_id(Position), igVec2Comp); //igInspectComp("Size", entity, ecs_id(Size), igVec2Comp); igInspectComp("Velocity", entity, ecs_id(Velocity), igVec2Comp); igInspectComp("TargetPosition", entity, ecs_id(TargetPosition), igVec2Comp); igInspectComp("Steering", entity, ecs_id(Steering), igVec2Comp); igInspectComp("Rotation", entity, ecs_id(Rotation), igFloat); igInspectComp("Path", entity, ecs_id(Path), igPath); igInspectComp("TextureRegion", entity, ecs_id(TextureRegion), igTextureRegion); igInspectComp("Animation", entity, ecs_id(Animation), igAnimation); igInspectComp("Easing", entity, ecs_id(Easing), igEasing); igInspectComp("Arms", entity, ecs_id(Arms), igArms); igInspectComp("Arm", entity, ecs_id(Arm), igArm); igInspectComp("BzBTState", entity, ecs_id(BzBTState), igBzBTState); igInspectComp("AIBlackboard", entity, ecs_id(AIBlackboard), igAIBlackboard); igInspectComp("Worker", entity, ecs_id(Worker), igWorker); igInspectComp("Unit", entity, ecs_id(Unit), igUnit); } } igEnd(); } void imguiRender(float dt, void *userData) { BZ_UNUSED(userData); Game *game = ecs_singleton_get_mut(ECS, Game); InputState *input = ecs_singleton_get_mut(ECS, InputState); //igShowDemoWindow(NULL); igSetNextWindowSize((ImVec2){300, 400}, ImGuiCond_FirstUseEver); igBegin("Debug Menu", NULL, 0); igText("PathData pool available: %lu", bzObjectPoolGetNumFree(game->pools.pathData)); igText("BTNode pool available: %lu", bzObjectPoolGetNumFree(game->pools.btNode)); igText("BTNodeState pool available: %lu", bzObjectPoolGetNumFree(game->pools.btNodeState)); const char *inputState = "NONE"; switch (input->state) { case INPUT_NONE: inputState = "NONE"; break; case INPUT_BUILDING: inputState = "BUILDING"; break; case INPUT_SELECTED_UNITS: inputState = "SELECTED_UNITS"; break; case INPUT_SELECTED_OBJECT: inputState = "SELECTED_OBJECT"; break; case INPUT_SELECTED_BUILDING: inputState = "SELECTED_BUILDING"; break; } igText("Input state: %s", inputState); if (igCollapsingHeader_TreeNodeFlags("ParticleEditor", 0)) { static ParticleEmitter emitter = {0.0f}; emitter.data.tileID = getParticleTypeTile(PARTICLE_CIRCLE); emitter.data.blend = BLEND_ADDITIVE; struct ParticleEmitterData *data = &emitter.data; igSliderFloat2("Pos", &emitter.pos.x, 0.0f, 1000.0f, "%.2f", 0); igSliderFloat2("minOffset", &data->minOffset.x, -50.0f, 50.0f, "%.2f", 0); igSliderFloat2("maxOffset", &data->maxOffset.x, -50.0f, 50.0f, "%.2f", 0); igSliderFloat2("minStartVel", &data->minStartVel.x, -50.0f, 50.0f, "%.2f", 0); igSliderFloat2("maxStartVel", &data->maxStartVel.x, -50.0f, 50.0f, "%.2f", 0); igSliderFloat2("minEndVel", &data->minEndVel.x, -50.0f, 50.0f, "%.2f", 0); igSliderFloat2("maxEndVel", &data->maxEndVel.x, -50.0f, 50.0f, "%.2f", 0); igSliderFloat("minStartSize", &data->minStartSize, -50.0f, 50.0f, "%.2f", 0); igSliderFloat("maxStartSize", &data->maxStartSize, -50.0f, 50.0f, "%.2f", 0); igSliderFloat("minEndSize", &data->minEndSize, -50.0f, 50.0f, "%.2f", 0); igSliderFloat("maxEndSize", &data->maxEndSize, -50.0f, 50.0f, "%.2f", 0); igSliderFloat("minStartRotSpeed", &data->minStartRotSpeed, -50.0f, 50.0f, "%.2f", 0); igSliderFloat("maxStartRotSpeed", &data->maxStartRotSpeed, -50.0f, 50.0f, "%.2f", 0); igSliderFloat("minEndRotSpeed", &data->minEndRotSpeed, -50.0f, 50.0f, "%.2f", 0); igSliderFloat("maxEndRotSpeed", &data->maxEndRotSpeed, -50.0f, 50.0f, "%.2f", 0); #define igColor(label, color) \ do { \ \ float cols[4] = { color.r / 255.0f, color.g / 255.0f, \ color.b / 255.0f, color.a / 255.0f }; \ igColorEdit4(label, cols, 0); \ color.r = cols[0] * 255.0f; \ color.g = cols[1] * 255.0f; \ color.b = cols[2] * 255.0f; \ color.a = cols[3] * 255.0f; \ } while (0) igColor("minStartColor", data->minStartColor); igColor("maxStartColor", data->maxStartColor); igColor("minEndColor", data->minEndColor); igColor("maxEndColor", data->maxEndColor); #undef igColor igSliderFloat("minLifetime", &data->minLifetime, 0.0f, 6.0f, "%.2f", 0); igSliderFloat("maxLifetime", &data->maxLifetime, 0.0f, 6.0f, "%.2f", 0); Particle particle = spawnParticle(&emitter); ecs_entity_t e = entityCreateEmpty(); ecs_set_ptr(ECS, e, ParticleLayer1, &particle); } if (igCollapsingHeader_TreeNodeFlags("Selection", 0)) { switch (input->state) { case INPUT_SELECTED_UNITS: case INPUT_SELECTED_OBJECT: case INPUT_SELECTED_BUILDING: { ecs_iter_t it = ecs_query_iter(ECS, input->queries.selected); while (ecs_iter_next(&it)) { for (i32 i = 0; i < it.count; i++) { ecs_entity_t entity = it.entities[i]; igText("Entity: %ld", entity); } } break; } default: igText("NONE"); break; } } if (igCollapsingHeader_TreeNodeFlags("Resources", 0)) { PlayerResources resources = game->playerResources[game->player]; i32 wood = resources.wood; i32 food = resources.food; i32 gold = resources.gold; igSliderInt("Wood: ", &wood, 0, 10000, "%d", 0); igSliderInt("Food: ", &food, 0, 10000, "%d", 0); igSliderInt("Gold: ", &gold, 0, 10000, "%d", 0); resources.wood = wood; resources.food = food; resources.gold = gold; game->playerResources[game->player] = resources; igText("Pop: %lld", resources.pop); igText("Pop Capacity: %lld", resources.pop); } if (igCollapsingHeader_TreeNodeFlags("BuildMenu", 0)) { for (int i = BUILDING_NONE; i < BUILDING_COUNT; i++) { const char *buildingStr = getBuildingStr(i); if (!buildingStr) buildingStr = "NONE"; if (igSelectable_Bool(buildingStr, input->building == i, 0, (ImVec2){0, 0})) input->building = i; } if (input->building > BUILDING_NONE && input->building < BUILDING_COUNT) input->state = INPUT_BUILDING; } if (igCollapsingHeader_TreeNodeFlags("DebugDraw", 0)) { igCheckbox("map colliders", &game->debug.drawMapColliders); igCheckbox("entity colliders", &game->debug.drawEntityColliders); igCheckbox("spatial grid", &game->debug.drawSpatialGrid); igCheckbox("path", &game->debug.drawPath); } if (igCollapsingHeader_TreeNodeFlags("Entities", 0)) { igSliderFloat("Frame duration", &game->frameDuration, 0.0f, 1.0f, NULL, 0); } if (igSmallButton("Quite game")) { bzGameExit(); } igEnd(); ecs_defer_begin(ECS); i32 inspectLen = bzArraySize(game->debug.inspecting); for (i32 i = inspectLen - 1; i >= 0; i--) { bool open = true; igInspectWindow(game->debug.inspecting[i], &open); if (!open) { bzArrayDelSwap(game->debug.inspecting, i); } } ecs_defer_end(ECS); }