diff --git a/gameengine/CMakeLists.txt b/gameengine/CMakeLists.txt index 96c03d659..78a97ccea 100644 --- a/gameengine/CMakeLists.txt +++ b/gameengine/CMakeLists.txt @@ -285,6 +285,7 @@ if(BUILD_SDL3_APP) src/services/impl/workflow/geometry/workflow_geometry_cube_generate_step.cpp src/services/impl/workflow/quake3/workflow_q3_menu_update_step.cpp src/services/impl/workflow/quake3/workflow_q3_overlay_draw_step.cpp + src/services/impl/workflow/quake3/workflow_q3_pickups_draw_step.cpp src/services/impl/workflow/quake3/workflow_q3_weapon_update_step.cpp src/services/impl/workflow/rendering/workflow_bsp_build_collision_step.cpp src/services/impl/workflow/rendering/workflow_bsp_build_geometry_step.cpp diff --git a/gameengine/CMakeUserPresets.json b/gameengine/CMakeUserPresets.json index 73fde6670..45cee691b 100644 --- a/gameengine/CMakeUserPresets.json +++ b/gameengine/CMakeUserPresets.json @@ -6,4 +6,4 @@ "include": [ "build-ninja/build/generators/CMakePresets.json" ] -} \ No newline at end of file +} diff --git a/gameengine/packages/quake3/workflows/q3_frame.json b/gameengine/packages/quake3/workflows/q3_frame.json index acb62aa02..7cd58d798 100644 --- a/gameengine/packages/quake3/workflows/q3_frame.json +++ b/gameengine/packages/quake3/workflows/q3_frame.json @@ -105,6 +105,12 @@ "metallic": 0.0 } }, + { + "id": "draw_pickups", + "type": "q3.pickups.draw", + "typeVersion": 1, + "position": [1100, 0] + }, { "id": "end_scene", "type": "frame.gpu.end_scene", @@ -167,7 +173,8 @@ "render_prepare": { "main": { "0": [{ "node": "portal_view", "type": "main", "index": 0 }] } }, "portal_view": { "main": { "0": [{ "node": "frame_begin", "type": "main", "index": 0 }] } }, "frame_begin": { "main": { "0": [{ "node": "draw_map", "type": "main", "index": 0 }] } }, - "draw_map": { "main": { "0": [{ "node": "end_scene", "type": "main", "index": 0 }] } }, + "draw_map": { "main": { "0": [{ "node": "draw_pickups", "type": "main", "index": 0 }] } }, + "draw_pickups": { "main": { "0": [{ "node": "end_scene", "type": "main", "index": 0 }] } }, "end_scene": { "main": { "0": [{ "node": "overlay_fps", "type": "main", "index": 0 }] } }, "overlay_fps": { "main": { "0": [{ "node": "q3_overlay", "type": "main", "index": 0 }] } }, "q3_overlay": { "main": { "0": [{ "node": "postfx_taa", "type": "main", "index": 0 }] } }, diff --git a/gameengine/src/services/impl/workflow/quake3/workflow_q3_overlay_draw_step.cpp b/gameengine/src/services/impl/workflow/quake3/workflow_q3_overlay_draw_step.cpp index 6a17f9770..12283b2ad 100644 --- a/gameengine/src/services/impl/workflow/quake3/workflow_q3_overlay_draw_step.cpp +++ b/gameengine/src/services/impl/workflow/quake3/workflow_q3_overlay_draw_step.cpp @@ -181,10 +181,45 @@ void WorkflowQ3OverlayDrawStep::DrawSurface(WorkflowContext& context, uint32_t f const std::string weapon = context.Get("q3.current_weapon", "weapon_machinegun"); const int shots = context.Get("q3.shots_fired", 0); - std::string hud = "WEAPON " + weapon.substr(7) + " SHOTS " + std::to_string(shots); + const int damage = context.Get("q3.damage_done", 0); + std::string hud = "WEAPON " + weapon.substr(7) + " SHOTS " + std::to_string(shots) + + " DAMAGE " + std::to_string(damage); Text(renderer_, 20, static_cast(kH - 36), hud.c_str(), SDL_Color{255, 216, 64, 255}); Text(renderer_, static_cast(kW / 2 - 4), static_cast(kH / 2 - 4), "+", SDL_Color{255, 255, 255, 220}); + const uint32_t frame = static_cast(context.GetDouble("loop.iteration", 0.0)); + const bool flashing = frame < context.Get("q3.weapon_flash_until_frame", 0u); + const bool hitMarker = frame < context.Get("q3.hit_marker_until_frame", 0u); + + SDL_SetRenderDrawColor(renderer_, 34, 34, 38, 235); + SDL_FRect gunBody{410, 278, 168, 46}; + SDL_RenderFillRect(renderer_, &gunBody); + SDL_SetRenderDrawColor(renderer_, 92, 96, 110, 255); + SDL_RenderRect(renderer_, &gunBody); + SDL_SetRenderDrawColor(renderer_, 20, 20, 22, 255); + SDL_FRect grip{452, 318, 36, 30}; + SDL_RenderFillRect(renderer_, &grip); + SDL_FRect barrel{568, 291, 54, 18}; + SDL_RenderFillRect(renderer_, &barrel); + SDL_SetRenderDrawColor(renderer_, 255, 210, 70, 255); + SDL_RenderLine(renderer_, 424, 290, 550, 290); + Text(renderer_, 430, 300, weapon.substr(7).c_str(), SDL_Color{220, 235, 255, 255}); + if (flashing) { + SDL_SetRenderDrawColor(renderer_, 255, 190, 50, 230); + SDL_FRect flash{616, 284, 18, 32}; + SDL_RenderFillRect(renderer_, &flash); + SDL_RenderLine(renderer_, 615, 300, 638, 276); + SDL_RenderLine(renderer_, 615, 300, 638, 324); + } + if (hitMarker) { + SDL_SetRenderDrawColor(renderer_, 255, 80, 55, 255); + SDL_RenderLine(renderer_, 308, 172, 320, 160); + SDL_RenderLine(renderer_, 332, 172, 320, 160); + SDL_RenderLine(renderer_, 308, 188, 320, 200); + SDL_RenderLine(renderer_, 332, 188, 320, 200); + Text(renderer_, 300, 204, "HIT", SDL_Color{255, 92, 64, 255}); + } + if (context.GetBool("q3.menu_open", false)) { SDL_FRect panel{120, 42, 400, 250}; SDL_SetRenderDrawColor(renderer_, 0, 0, 0, 210); diff --git a/gameengine/src/services/impl/workflow/quake3/workflow_q3_pickups_draw_step.cpp b/gameengine/src/services/impl/workflow/quake3/workflow_q3_pickups_draw_step.cpp new file mode 100644 index 000000000..1b15477dc --- /dev/null +++ b/gameengine/src/services/impl/workflow/quake3/workflow_q3_pickups_draw_step.cpp @@ -0,0 +1,223 @@ +#include "services/interfaces/workflow/quake3/workflow_q3_pickups_draw_step.hpp" +#include "services/interfaces/workflow/rendering/rendering_types.hpp" + +#include +#include +#include +#include +#include + +#include +#include + +namespace sdl3cpp::services::impl { + +namespace { + +bool HasPrefix(const std::string& value, const std::string& prefix) { + return value.rfind(prefix, 0) == 0; +} + +bool IsPickup(const std::string& classname) { + return HasPrefix(classname, "weapon_") || HasPrefix(classname, "ammo_") || + HasPrefix(classname, "item_") || HasPrefix(classname, "holdable_"); +} + +std::string TextureKeyForClass(const std::string& classname) { + if (HasPrefix(classname, "weapon_")) return "q3_pickup_weapon"; + if (HasPrefix(classname, "ammo_")) return "q3_pickup_ammo"; + if (classname.find("health") != std::string::npos) return "q3_pickup_health"; + if (classname.find("armor") != std::string::npos) return "q3_pickup_armor"; + return "q3_pickup_powerup"; +} + +bool ReadVec3(const nlohmann::json& value, glm::vec3& out) { + if (!value.is_array() || value.size() != 3) return false; + out = glm::vec3(value[0].get(), value[1].get(), value[2].get()); + return true; +} + +} // namespace + +WorkflowQ3PickupsDrawStep::WorkflowQ3PickupsDrawStep(std::shared_ptr logger) + : logger_(std::move(logger)) {} + +WorkflowQ3PickupsDrawStep::~WorkflowQ3PickupsDrawStep() { + if (device_) { + if (quad_vb_) SDL_ReleaseGPUBuffer(device_, quad_vb_); + if (quad_ib_) SDL_ReleaseGPUBuffer(device_, quad_ib_); + if (transfer_) SDL_ReleaseGPUTransferBuffer(device_, transfer_); + } +} + +std::string WorkflowQ3PickupsDrawStep::GetPluginId() const { + return "q3.pickups.draw"; +} + +SDL_GPUTexture* WorkflowQ3PickupsDrawStep::CreateColorTexture(SDL_GPUDevice* device, WorkflowContext& context, + const std::string& key, + uint8_t r, uint8_t g, uint8_t b) { + auto* existing = context.Get(key + "_gpu", nullptr); + if (existing) return existing; + + SDL_GPUTextureCreateInfo ti = {}; + ti.type = SDL_GPU_TEXTURETYPE_2D; + ti.format = SDL_GPU_TEXTUREFORMAT_R8G8B8A8_UNORM; + ti.width = 1; + ti.height = 1; + ti.layer_count_or_depth = 1; + ti.num_levels = 1; + ti.usage = SDL_GPU_TEXTUREUSAGE_SAMPLER; + auto* tex = SDL_CreateGPUTexture(device, &ti); + if (!tex) return nullptr; + + SDL_GPUTransferBufferCreateInfo tbi = {}; + tbi.usage = SDL_GPU_TRANSFERBUFFERUSAGE_UPLOAD; + tbi.size = 4; + auto* tb = SDL_CreateGPUTransferBuffer(device, &tbi); + auto* mapped = static_cast(SDL_MapGPUTransferBuffer(device, tb, false)); + mapped[0] = r; mapped[1] = g; mapped[2] = b; mapped[3] = 230; + SDL_UnmapGPUTransferBuffer(device, tb); + + auto* cmd = SDL_AcquireGPUCommandBuffer(device); + auto* copy = SDL_BeginGPUCopyPass(cmd); + SDL_GPUTextureTransferInfo src = {}; + src.transfer_buffer = tb; + SDL_GPUTextureRegion dst = {}; + dst.texture = tex; dst.w = 1; dst.h = 1; dst.d = 1; + SDL_UploadToGPUTexture(copy, &src, &dst, false); + SDL_EndGPUCopyPass(copy); + SDL_SubmitGPUCommandBuffer(cmd); + SDL_ReleaseGPUTransferBuffer(device, tb); + + SDL_GPUSamplerCreateInfo si = {}; + si.min_filter = SDL_GPU_FILTER_NEAREST; + si.mag_filter = SDL_GPU_FILTER_NEAREST; + si.mipmap_mode = SDL_GPU_SAMPLERMIPMAPMODE_NEAREST; + si.address_mode_u = SDL_GPU_SAMPLERADDRESSMODE_CLAMP_TO_EDGE; + si.address_mode_v = SDL_GPU_SAMPLERADDRESSMODE_CLAMP_TO_EDGE; + auto* sampler = SDL_CreateGPUSampler(device, &si); + context.Set(key + "_gpu", tex); + context.Set(key + "_sampler", sampler); + return tex; +} + +void WorkflowQ3PickupsDrawStep::EnsureBuffers(SDL_GPUDevice* device) { + if (quad_vb_ && quad_ib_) return; + device_ = device; + struct V { float x, y, z, u, v; }; + const V verts[4] = { + {-0.5f, -0.5f, 0.0f, 0.0f, 1.0f}, + { 0.5f, -0.5f, 0.0f, 1.0f, 1.0f}, + { 0.5f, 0.5f, 0.0f, 1.0f, 0.0f}, + {-0.5f, 0.5f, 0.0f, 0.0f, 0.0f}, + }; + const uint16_t indices[6] = {0, 1, 2, 0, 2, 3}; + const uint32_t total = sizeof(verts) + sizeof(indices); + + SDL_GPUBufferCreateInfo vbInfo = {}; + vbInfo.usage = SDL_GPU_BUFFERUSAGE_VERTEX; + vbInfo.size = sizeof(verts); + quad_vb_ = SDL_CreateGPUBuffer(device, &vbInfo); + SDL_GPUBufferCreateInfo ibInfo = {}; + ibInfo.usage = SDL_GPU_BUFFERUSAGE_INDEX; + ibInfo.size = sizeof(indices); + quad_ib_ = SDL_CreateGPUBuffer(device, &ibInfo); + SDL_GPUTransferBufferCreateInfo tbInfo = {}; + tbInfo.usage = SDL_GPU_TRANSFERBUFFERUSAGE_UPLOAD; + tbInfo.size = total; + transfer_ = SDL_CreateGPUTransferBuffer(device, &tbInfo); + auto* mapped = static_cast(SDL_MapGPUTransferBuffer(device, transfer_, false)); + std::memcpy(mapped, verts, sizeof(verts)); + std::memcpy(mapped + sizeof(verts), indices, sizeof(indices)); + SDL_UnmapGPUTransferBuffer(device, transfer_); + + auto* cmd = SDL_AcquireGPUCommandBuffer(device); + auto* copy = SDL_BeginGPUCopyPass(cmd); + SDL_GPUTransferBufferLocation srcV = {transfer_, 0}; + SDL_GPUBufferRegion dstV = {quad_vb_, 0, sizeof(verts)}; + SDL_UploadToGPUBuffer(copy, &srcV, &dstV, false); + SDL_GPUTransferBufferLocation srcI = {transfer_, sizeof(verts)}; + SDL_GPUBufferRegion dstI = {quad_ib_, 0, sizeof(indices)}; + SDL_UploadToGPUBuffer(copy, &srcI, &dstI, false); + SDL_EndGPUCopyPass(copy); + SDL_SubmitGPUCommandBuffer(cmd); +} + +void WorkflowQ3PickupsDrawStep::Execute(const WorkflowStepDefinition& step, WorkflowContext& context) { + if (context.GetBool("frame_skip", false)) return; + auto* pass = context.Get("gpu_render_pass", nullptr); + auto* cmd = context.Get("gpu_command_buffer", nullptr); + auto* device = context.Get("gpu_device", nullptr); + auto* pipeline = context.Get("gpu_pipeline_textured", nullptr); + const auto* entities = context.TryGet("bsp.entities"); + if (!pass || !cmd || !device || !pipeline || !entities || !entities->is_array()) return; + + EnsureBuffers(device); + CreateColorTexture(device, context, "q3_pickup_weapon", 255, 210, 40); + CreateColorTexture(device, context, "q3_pickup_ammo", 255, 120, 45); + CreateColorTexture(device, context, "q3_pickup_health", 40, 235, 85); + CreateColorTexture(device, context, "q3_pickup_armor", 70, 150, 255); + CreateColorTexture(device, context, "q3_pickup_powerup", 190, 90, 255); + + auto collected = context.Get("q3.collected", nlohmann::json::object()); + auto view = context.Get("render.view_matrix", glm::mat4(1.0f)); + auto proj = context.Get("render.proj_matrix", glm::mat4(1.0f)); + auto camPos = context.Get("render.camera_pos", glm::vec3(0.0f)); + auto shadowVP = context.Get("render.shadow_vp", glm::mat4(1.0f)); + glm::vec3 camRight(view[0][0], view[1][0], view[2][0]); + glm::vec3 camUp(view[0][1], view[1][1], view[2][1]); + const float time = static_cast(context.GetDouble("frame.elapsed", 0.0)); + + SDL_BindGPUGraphicsPipeline(pass, pipeline); + SDL_GPUBufferBinding vb = {quad_vb_, 0}; + SDL_BindGPUVertexBuffers(pass, 0, &vb, 1); + SDL_GPUBufferBinding ib = {quad_ib_, 0}; + SDL_BindGPUIndexBuffer(pass, &ib, SDL_GPU_INDEXELEMENTSIZE_16BIT); + + int drawn = 0; + for (const auto& ent : *entities) { + const std::string classname = ent.value("classname", std::string{}); + const std::string id = ent.value("id", std::string{}); + if (!IsPickup(classname) || collected.value(id, false)) continue; + glm::vec3 pos; + if (!ent.contains("position") || !ReadVec3(ent["position"], pos)) continue; + if (++drawn > 96) break; + + const float bob = std::sin(time * 3.0f + static_cast(drawn)) * 0.08f; + const float size = HasPrefix(classname, "weapon_") ? 0.9f : 0.55f; + pos.y += 0.45f + bob; + glm::mat4 model(1.0f); + model[0] = glm::vec4(camRight * size, 0.0f); + model[1] = glm::vec4(camUp * size, 0.0f); + model[2] = glm::vec4(glm::normalize(glm::cross(camRight, camUp)) * size, 0.0f); + model[3] = glm::vec4(pos, 1.0f); + + rendering::VertexUniformData vu = {}; + glm::mat4 mvp = proj * view * model; + std::memcpy(vu.mvp, glm::value_ptr(mvp), sizeof(float) * 16); + std::memcpy(vu.model_mat, glm::value_ptr(model), sizeof(float) * 16); + vu.normal[1] = 1.0f; + vu.uv_scale[0] = 1.0f; vu.uv_scale[1] = 1.0f; + vu.camera_pos[0] = camPos.x; vu.camera_pos[1] = camPos.y; vu.camera_pos[2] = camPos.z; + std::memcpy(vu.shadow_vp, glm::value_ptr(shadowVP), sizeof(float) * 16); + + auto fu = context.Get("render.frag_uniforms", rendering::FragmentUniformData{}); + fu.material[0] = 0.35f; + fu.material[1] = 0.0f; + + const std::string texKey = TextureKeyForClass(classname); + auto* tex = context.Get(texKey + "_gpu", nullptr); + auto* samp = context.Get(texKey + "_sampler", nullptr); + if (!tex || !samp) continue; + SDL_GPUTextureSamplerBinding bindings[2] = {}; + bindings[0].texture = tex; bindings[0].sampler = samp; + bindings[1].texture = tex; bindings[1].sampler = samp; + SDL_BindGPUFragmentSamplers(pass, 0, bindings, 2); + SDL_PushGPUVertexUniformData(cmd, 0, &vu, sizeof(vu)); + SDL_PushGPUFragmentUniformData(cmd, 0, &fu, sizeof(fu)); + SDL_DrawGPUIndexedPrimitives(pass, 6, 1, 0, 0, 0); + } +} + +} // namespace sdl3cpp::services::impl diff --git a/gameengine/src/services/impl/workflow/quake3/workflow_q3_weapon_update_step.cpp b/gameengine/src/services/impl/workflow/quake3/workflow_q3_weapon_update_step.cpp index 21216918e..b7c89c257 100644 --- a/gameengine/src/services/impl/workflow/quake3/workflow_q3_weapon_update_step.cpp +++ b/gameengine/src/services/impl/workflow/quake3/workflow_q3_weapon_update_step.cpp @@ -68,6 +68,7 @@ void WorkflowQ3WeaponUpdateStep::Execute(const WorkflowStepDefinition& step, Wor context.Set("q3.weapon_last_fire_frame", lastFire); context.Set("q3.weapon_flash_until_frame", lastFire + 4u); context.Set("q3.shots_fired", context.Get("q3.shots_fired", 0) + 1); + context.Set("q3.last_shot_hit", false); auto* world = context.Get("physics_world", nullptr); auto cameraState = context.Get("camera.state", nlohmann::json::object()); @@ -81,6 +82,13 @@ void WorkflowQ3WeaponUpdateStep::Execute(const WorkflowStepDefinition& step, Wor world->rayTest(from, to, hit); context.Set("q3.last_shot_hit", hit.hasHit()); if (hit.hasHit()) { + context.Set("q3.hit_marker_until_frame", lastFire + 10u); + context.Set("q3.damage_done", context.Get("q3.damage_done", 0) + ( + current == "weapon_railgun" ? 100 : + current == "weapon_rocketlauncher" ? 100 : + current == "weapon_shotgun" ? 80 : + current == "weapon_plasmagun" ? 20 : + current == "weapon_machinegun" ? 7 : 15)); context.Set("q3.last_shot_position", nlohmann::json::array({ hit.m_hitPointWorld.x(), hit.m_hitPointWorld.y(), hit.m_hitPointWorld.z() })); diff --git a/gameengine/src/services/impl/workflow/workflow_registrar.cpp b/gameengine/src/services/impl/workflow/workflow_registrar.cpp index e22e6bc80..7ab2f509d 100644 --- a/gameengine/src/services/impl/workflow/workflow_registrar.cpp +++ b/gameengine/src/services/impl/workflow/workflow_registrar.cpp @@ -117,6 +117,7 @@ #include "services/interfaces/workflow/quake3/workflow_q3_menu_update_step.hpp" #include "services/interfaces/workflow/quake3/workflow_q3_weapon_update_step.hpp" #include "services/interfaces/workflow/quake3/workflow_q3_overlay_draw_step.hpp" +#include "services/interfaces/workflow/quake3/workflow_q3_pickups_draw_step.hpp" // Audio (service-dependent, registered with nullptr) #include "services/interfaces/workflow/workflow_generic_steps/workflow_audio_pause_step.hpp" @@ -323,6 +324,7 @@ void WorkflowRegistrar::RegisterSteps(std::shared_ptr reg registry->RegisterStep(std::make_shared(logger_)); registry->RegisterStep(std::make_shared(logger_)); registry->RegisterStep(std::make_shared(logger_)); + registry->RegisterStep(std::make_shared(logger_)); registry->RegisterStep(std::make_shared(logger_)); count += 18; diff --git a/gameengine/src/services/interfaces/workflow/quake3/workflow_q3_pickups_draw_step.hpp b/gameengine/src/services/interfaces/workflow/quake3/workflow_q3_pickups_draw_step.hpp new file mode 100644 index 000000000..0de2997c7 --- /dev/null +++ b/gameengine/src/services/interfaces/workflow/quake3/workflow_q3_pickups_draw_step.hpp @@ -0,0 +1,31 @@ +#pragma once + +#include "services/interfaces/i_workflow_step.hpp" +#include "services/interfaces/i_logger.hpp" + +#include +#include + +namespace sdl3cpp::services::impl { + +class WorkflowQ3PickupsDrawStep final : public IWorkflowStep { +public: + explicit WorkflowQ3PickupsDrawStep(std::shared_ptr logger); + ~WorkflowQ3PickupsDrawStep(); + + std::string GetPluginId() const override; + void Execute(const WorkflowStepDefinition& step, WorkflowContext& context) override; + +private: + SDL_GPUTexture* CreateColorTexture(SDL_GPUDevice* device, WorkflowContext& context, + const std::string& key, uint8_t r, uint8_t g, uint8_t b); + void EnsureBuffers(SDL_GPUDevice* device); + + std::shared_ptr logger_; + SDL_GPUDevice* device_ = nullptr; + SDL_GPUBuffer* quad_vb_ = nullptr; + SDL_GPUBuffer* quad_ib_ = nullptr; + SDL_GPUTransferBuffer* transfer_ = nullptr; +}; + +} // namespace sdl3cpp::services::impl