diff --git a/Engine.cpp b/Engine.cpp index 03efb77..cadd991 100644 --- a/Engine.cpp +++ b/Engine.cpp @@ -128,6 +128,7 @@ void Engine::Initiate() // ------------------------------------------------------------------------ Font* arial = new Font("assets/ft/arial.ttf"); Texture* blanktex = new Texture("assets/blanktexture.png"); + Texture* blacktex = new Texture("assets/blacktexture.png"); Texture* magic = new Texture("assets/itsmagicbitch.jpg"); Texture* snowflake = new Texture("assets/snowflake.png"); Texture* shockwavetex = new Texture("assets/shockwave.png"); @@ -193,6 +194,8 @@ void Engine::Initiate() Character* character = nullptr; double lastSeen = 0.0; p* body = nullptr; // host only: lets the host's cubes collide with this player + glm::vec3 targetPos = glm::vec3(0.0f); // latest pose received; the model eases toward it every frame + glm::quat targetRot = glm::quat(1.0f, 0.0f, 0.0f, 0.0f); }; std::map remotes; auto SpawnRemote = [&]() -> Character* { @@ -331,6 +334,23 @@ void Engine::Initiate() grappleText->t2d.size = { 0.0f, 0.0f, 560.0f, 40.0f }; grappleText->BindToRenderSystem(); + // Shot cooldown bar at the bottom of the screen: full and white when ready, empties on a shot + // and refills as the cooldown runs down. + const float shotBarWidth = 300.0f, shotBarHeight = 8.0f; + Box* shotBarBack = new Box(); + shotBarBack->Color = { 0.0f, 0.0f, 0.0f }; + shotBarBack->Opacity = 0.45f; + shotBarBack->t2d.position = { 0.5f, 1.0f, 0.0f, -30.0f }; + shotBarBack->t2d.center = { 0.5f, 1.0f }; + shotBarBack->t2d.size = { 0.0f, 0.0f, shotBarWidth + 4.0f, shotBarHeight + 4.0f }; + shotBarBack->BindToRenderSystem(); + Box* shotBar = new Box(); + shotBar->Color = { 1.0f, 1.0f, 1.0f }; + shotBar->t2d.position = { 0.5f, 1.0f, -shotBarWidth * 0.5f, -32.0f }; + shotBar->t2d.center = { 0.0f, 1.0f }; + shotBar->t2d.size = { 0.0f, 0.0f, shotBarWidth, shotBarHeight }; + shotBar->BindToRenderSystem(); + // ------------------------------------------------------------------------ // World meshes // ------------------------------------------------------------------------ @@ -446,6 +466,17 @@ void Engine::Initiate() float shockedTime = 0.0f; const float grappleRange = 30.0f; // metres from the player const float grappleSpeed = 28.0f; // launch speed toward the hit point + // Visible grapple rope: a thin black box stretched from the hand to the latch point + const float grappleLineShowTime = 0.5f; + float grappleLineTime = 0.0f; + glm::vec3 grappleLineEnd(0.0f); + Mesh* grappleLine = CreateCubeMesh(); + grappleLine->name = "grappleline"; + grappleLine->meshTexture = blacktex; + grappleLine->t.TranslateTo({ 0.0f, -1000.0f, 0.0f }); // hidden until a grapple latches + grappleLine->BindToRenderSystem(); + mainf->AddChild(grappleLine); + const float grappleLift = 10.0f; // extra upward speed added on top of the pull const float grappleCooldownTime = 2.5f; // seconds float grappleCooldown = 0.0f; @@ -817,8 +848,12 @@ void Engine::Initiate() } if (found) { characterBody->linearvelocity = glm::normalize(bestPoint - ray.origin) * grappleSpeed; + // Extra upward fling so the grapple gains height instead of just pulling sideways + characterBody->linearvelocity.y = std::max(characterBody->linearvelocity.y, 0.0f) + grappleLift; characterBody->WakeUp(); grappleCooldown = grappleCooldownTime; + grappleLineTime = grappleLineShowTime; + grappleLineEnd = bestPoint; camera->AddScreenshake(0.1f); } } @@ -984,6 +1019,25 @@ void Engine::Initiate() } fpsCounter->text = std::to_string(measuredFps) + " FPS"; } + { + const float ready = 1.0f - glm::clamp(shootCooldown / shootCooldownTime, 0.0f, 1.0f); + shotBar->t2d.size = { 0.0f, 0.0f, shotBarWidth * ready, shotBarHeight }; + shotBar->Opacity = ready > 0.0f ? 1.0f : 0.0f; + } + // Grapple rope: starts at the hand (follows the camera), ends on the latch point + grappleLineTime = std::max(grappleLineTime - deltatime, 0.0f); + if (grappleLineTime > 0.0f) { + const glm::vec3 handPos = camera->t.GetTranslation() + camera->t.GetFrontVector() * 0.6f + + camera->t.GetRightVector() * 0.3f - camera->t.GetUpVector() * 0.3f; + const glm::vec3 delta = grappleLineEnd - handPos; + const float len = glm::length(delta); + if (len > 0.01f) { + grappleLine->t.TranslateTo(handPos + delta * 0.5f); + grappleLine->t.ScaleTo({ 0.05f, 0.05f, len }); + grappleLine->t.RotateToQuaternion(LookAt(delta)); + } + } + else grappleLine->t.TranslateTo(hiddenPos); hitMarkerTime = std::max(hitMarkerTime - deltatime, 0.0f); for (Box* b : hitMarker) b->Opacity = hitMarkerTime / hitMarkerDuration; // Damage flash: any HP loss restarts a short green fade-out @@ -1150,6 +1204,8 @@ void Engine::Initiate() rp.character = SpawnRemote(); rp.character->body->t.TranslateTo(u.state.position); rp.character->body->t.RotateToQuaternion(u.state.rotation); + rp.targetPos = u.state.position; + rp.targetRot = u.state.rotation; if (lan->GetMode() == Lan::Mode::Host) { // Solid for the host's cubes (kinematic: moved by the client's stream only) rp.body = new p(rp.character->body); @@ -1159,14 +1215,10 @@ void Engine::Initiate() it = remotes.emplace(u.id, rp).first; } else { - // Smooth over the 60 Hz updates - t& rt = it->second.character->body->t; - const glm::vec3 before = rt.GetTranslation(); - rt.TranslateTo(glm::mix(rt.GetTranslation(), u.state.position, 0.5f)); - // Kinematic body: its velocity makes it push cubes like the client's own body does there - if (it->second.body && deltatime > 0.0f) - it->second.body->linearvelocity = (rt.GetTranslation() - before) / deltatime; - rt.RotateToQuaternion(glm::slerp(rt.GetRotationQuaternion(), u.state.rotation, 0.5f)); + // Packets arrive in bursts (several in one frame, then none), so just remember the + // newest pose here; the per-frame smoothing below does the actual moving. + it->second.targetPos = u.state.position; + it->second.targetRot = u.state.rotation; } it->second.lastSeen = now; } @@ -1180,6 +1232,20 @@ void Engine::Initiate() it = remotes.erase(it); } else { + // Frame-rate independent ease toward the latest pose (no matter how packets were batched) + t& rt = it->second.character->body->t; + const glm::vec3 before = rt.GetTranslation(); + if (glm::distance(before, it->second.targetPos) > 6.0f) + rt.TranslateTo(it->second.targetPos); // respawn / big jump: snap + else { + const float k = 1.0f - std::exp(-deltatime * 25.0f); + rt.TranslateTo(glm::mix(before, it->second.targetPos, k)); + } + rt.RotateToQuaternion(glm::slerp(rt.GetRotationQuaternion(), it->second.targetRot, + 1.0f - std::exp(-deltatime * 25.0f))); + // Kinematic body: its velocity makes cubes get pushed like the client's own body does there + if (it->second.body && deltatime > 0.0f) + it->second.body->linearvelocity = (rt.GetTranslation() - before) / deltatime; it->second.character->Step(deltatime); ++it; } diff --git a/assets/blacktexture.png b/assets/blacktexture.png new file mode 100644 index 0000000..8ef45ce Binary files /dev/null and b/assets/blacktexture.png differ