260 PoolWindow(
int width,
int height,
bool fullscreen,
bool enable_vsync, std::string asset_root)
262 assetRoot(std::move(asset_root)),
263 highScores((std::filesystem::path(assetRoot) /
"pool_scores.dat").string()),
264 fallbackWidth(width),
265 fallbackHeight(height) {
266 setFont(assetRoot +
"/font.ttf", 24);
270 tryOpenFirstGamepad();
274 if (
device != VK_NULL_HANDLE) {
283 const int renderW = (extent.width > 0U) ?
static_cast<int>(extent.width) : fallbackWidth;
284 const int renderH = (extent.height > 0U) ?
static_cast<int>(extent.height) : fallbackHeight;
285 fallbackWidth = renderW;
286 fallbackHeight = renderH;
290 procIntro(renderW, renderH);
293 procStart(renderW, renderH);
296 procGame(renderW, renderH);
299 procScores(renderW, renderH);
305 if (e.type == SDL_EVENT_QUIT) {
310 if (e.type == SDL_EVENT_GAMEPAD_ADDED) {
311 openGamepad(e.gdevice.which);
315 if (e.type == SDL_EVENT_GAMEPAD_REMOVED) {
316 if (gamepad !=
nullptr && e.gdevice.which == gamepadId) {
318 tryOpenFirstGamepad();
323 if (e.type == SDL_EVENT_KEY_DOWN) {
324 if (e.key.key == SDLK_ESCAPE) {
326 setMouseCapture(
false);
339 if (e.key.key == SDLK_RETURN && !playerName.empty()) {
341 }
else if (e.key.key == SDLK_BACKSPACE && !playerName.empty()) {
342 playerName.pop_back();
344 }
else if (e.key.key == SDLK_RETURN) {
351 if (e.key.key == SDLK_RETURN) {
354 }
else if (e.key.key == SDLK_SPACE) {
361 if (e.key.key == SDLK_R) {
366 }
else if (e.key.key == SDLK_RETURN && phase ==
GamePhase::Placing && canPlaceCueBall()) {
379 if (playerName.size() < 15U) {
380 playerName += e.text.text;
386 const float dy = (e.wheel.y != 0.0f) ? e.wheel.y : e.wheel.integer_y;
387 camZoom -= dy * 1.2f;
388 camZoom = glm::clamp(camZoom, 5.0f, 25.0f);
392 if (e.type == SDL_EVENT_MOUSE_BUTTON_DOWN && e.button.button == SDL_BUTTON_RIGHT && screen ==
GameScreen::Game) {
393 if (!mouseCaptured) {
394 setMouseCapture(
true);
396 mouseCamDragging =
true;
397 mouseCamLastX =
static_cast<int>(e.button.x);
398 mouseCamLastY =
static_cast<int>(e.button.y);
402 if (e.type == SDL_EVENT_MOUSE_BUTTON_DOWN && e.button.button == SDL_BUTTON_LEFT) {
404 setMouseCapture(
true);
405 consumeNextMouseLeftUp =
true;
408 onPointerDown(
static_cast<int>(e.button.x),
static_cast<int>(e.button.y), -1);
412 if (e.type == SDL_EVENT_MOUSE_BUTTON_UP && e.button.button == SDL_BUTTON_RIGHT) {
413 mouseCamDragging =
false;
417 if (e.type == SDL_EVENT_MOUSE_BUTTON_UP && e.button.button == SDL_BUTTON_LEFT) {
418 if (consumeNextMouseLeftUp) {
419 consumeNextMouseLeftUp =
false;
426 onPointerUp(
static_cast<int>(e.button.x),
static_cast<int>(e.button.y), -1);
430 if (e.type == SDL_EVENT_MOUSE_MOTION) {
432 if (mouseCamDragging) {
433 camAngle +=
static_cast<float>(e.motion.xrel) * 0.01f;
434 camPitch -=
static_cast<float>(e.motion.yrel) * 0.006f;
435 camPitch = glm::clamp(camPitch, 0.30f, 1.25f);
438 onMouseRelativeMove(e.motion.xrel, e.motion.yrel);
442 const int nx =
static_cast<int>(e.motion.x);
443 const int ny =
static_cast<int>(e.motion.y);
444 const int dx = nx - mouseCamLastX;
445 const int dy = ny - mouseCamLastY;
446 camAngle +=
static_cast<float>(dx) * 0.01f;
447 camPitch -=
static_cast<float>(dy) * 0.006f;
448 camPitch = glm::clamp(camPitch, 0.30f, 1.25f);
453 onPointerMove(
static_cast<int>(e.motion.x),
static_cast<int>(e.motion.y), -1);
457 if (e.type == SDL_EVENT_FINGER_DOWN) {
458 const int px =
static_cast<int>(e.tfinger.x *
static_cast<float>(fallbackWidth));
459 const int py =
static_cast<int>(e.tfinger.y *
static_cast<float>(fallbackHeight));
460 touchPoints[
static_cast<int64_t
>(e.tfinger.fingerID)] = SDL_FPoint{
static_cast<float>(px),
static_cast<float>(py)};
462 auto it = touchPoints.begin();
463 const SDL_FPoint a = it->second;
465 const SDL_FPoint b = it->second;
466 const float dx = a.x - b.x;
467 const float dy = a.y - b.y;
468 touchPinchDistance = std::sqrt(dx * dx + dy * dy);
469 touchPinchActive =
true;
474 pointerCharging =
false;
476 activePointerId = std::numeric_limits<int64_t>::min();
479 onPointerDown(px, py,
static_cast<int64_t
>(e.tfinger.fingerID));
483 if (e.type == SDL_EVENT_FINGER_MOTION) {
484 const int px =
static_cast<int>(e.tfinger.x *
static_cast<float>(fallbackWidth));
485 const int py =
static_cast<int>(e.tfinger.y *
static_cast<float>(fallbackHeight));
486 touchPoints[
static_cast<int64_t
>(e.tfinger.fingerID)] = SDL_FPoint{
static_cast<float>(px),
static_cast<float>(py)};
487 if (screen ==
GameScreen::Game && touchPinchActive && touchPoints.size() >= 2U) {
488 auto it = touchPoints.begin();
489 const SDL_FPoint a = it->second;
491 const SDL_FPoint b = it->second;
492 const float dx = a.x - b.x;
493 const float dy = a.y - b.y;
494 const float dist = std::sqrt(dx * dx + dy * dy);
495 const float delta = dist - touchPinchDistance;
496 touchPinchDistance = dist;
497 camZoom -= delta * 0.02f;
498 camZoom = glm::clamp(camZoom, 5.0f, 25.0f);
501 onPointerMove(px, py,
static_cast<int64_t
>(e.tfinger.fingerID));
505 if (e.type == SDL_EVENT_FINGER_UP) {
506 const int px =
static_cast<int>(e.tfinger.x *
static_cast<float>(fallbackWidth));
507 const int py =
static_cast<int>(e.tfinger.y *
static_cast<float>(fallbackHeight));
508 const bool wasPinching = touchPinchActive;
509 touchPoints.erase(
static_cast<int64_t
>(e.tfinger.fingerID));
510 if (touchPinchActive && touchPoints.size() < 2U) {
511 touchPinchActive =
false;
512 touchPinchDistance = 0.0f;
515 pointerCharging =
false;
517 activePointerId = std::numeric_limits<int64_t>::min();
520 onPointerUp(px, py,
static_cast<int64_t
>(e.tfinger.fingerID));
524 if (e.type == SDL_EVENT_GAMEPAD_BUTTON_DOWN) {
525 handleGamepadButtonDown(e.gbutton.button);
529 if (e.type == SDL_EVENT_GAMEPAD_BUTTON_UP) {
530 handleGamepadButtonUp(e.gbutton.button);
536 feltModel.resize(
this);
537 woodModel.resize(
this);
538 pocketModel.resize(
this);
539 for (
auto &ballModel : ballModels) {
540 ballModel.resize(
this);
542 cueStickModel.resize(
this);
543 cueAimModel.resize(
this);
552 if (extent.width == 0U || extent.height == 0U) {
556 if (backgroundSprite !=
nullptr) {
557 backgroundSprite->setShaderParams(1.0f, 1.0f, 1.0f, 1.0f);
558 backgroundSprite->drawSpriteRect(0, 0,
static_cast<int>(extent.width),
static_cast<int>(extent.height));
559 backgroundSprite->renderSprites(cmd,
560 backgroundSprite->getPipelineLayout(),
563 backgroundSprite->clearQueue();
566 const float aspect =
static_cast<float>(extent.width) /
static_cast<float>(extent.height);
567 const float time =
static_cast<float>(SDL_GetTicks()) / 1000.0f;
569 const glm::vec3 camPos = getCameraPosition();
570 const glm::mat4 view = glm::lookAt(camPos, glm::vec3{0.0f, 0.0f, 0.0f}, glm::vec3{0.0f, 1.0f, 0.0f});
571 glm::mat4 proj = glm::perspective(glm::radians(45.0f), aspect, 0.1f, 100.0f);
574 drawModel(feltModel, imageIndex, cmd, view, proj,
575 composeTransform(glm::vec3{0.0f}, glm::vec3{1.0f}), glm::vec4{1.0f, 1.0f, 1.0f, 1.0f}, time);
576 drawModel(woodModel, imageIndex, cmd, view, proj,
577 composeTransform(glm::vec3{0.0f}, glm::vec3{1.0f}), glm::vec4{0.4f, 0.22f, 0.05f, 1.0f}, time);
578 drawModel(pocketModel, imageIndex, cmd, view, proj,
579 composeTransform(glm::vec3{0.0f}, glm::vec3{1.0f}), glm::vec4{0.08f, 0.08f, 0.08f, 1.0f}, time);
581 for (
int i = 0; i < NUM_BALLS; ++i) {
582 if (!balls[i].
active || balls[i].pocketed) {
586 m = glm::translate(m, glm::vec3{balls[i].pos.x, BALL_RADIUS, balls[i].pos.y});
587 m = glm::rotate(m, balls[i].spinAngle, glm::vec3{0.0f, 1.0f, 0.0f});
588 m = glm::scale(m, glm::vec3{BALL_RADIUS});
589 drawModel(ballModels[
static_cast<std::size_t
>(i)],
595 glm::vec4{BALL_COLORS[
static_cast<std::size_t
>(i)], 1.0f},
599 for (
const auto &anim : sinkAnims) {
600 const float t = anim.timer / SINK_DURATION;
601 const float y = glm::mix(BALL_RADIUS, -BALL_RADIUS * 3.5f, t);
602 const float sc = BALL_RADIUS * (1.0f - t * 0.75f);
604 m = glm::translate(m, glm::vec3{anim.pocketPos.x, y, anim.pocketPos.y});
605 m = glm::rotate(m, anim.spinAngle + t * 6.0f, glm::vec3{0.0f, 1.0f, 0.0f});
606 m = glm::scale(m, glm::vec3{sc});
607 const std::size_t sinkIndex =
static_cast<std::size_t
>(glm::clamp(anim.ballIndex, 0, NUM_BALLS - 1));
608 drawModel(ballModels[sinkIndex], imageIndex, cmd, view, proj, m, glm::vec4{anim.color, 1.0f}, time);
613 const float stickDist = BALL_RADIUS + 0.3f + offset;
614 const float worldCueAngle = cueAngle + camAngle;
615 const glm::vec2 dir{std::cos(worldCueAngle), std::sin(worldCueAngle)};
617 const glm::vec2 stickCenter = balls[0].pos - dir * (stickDist + CUE_LENGTH * 0.5f);
618 glm::mat4 cueTransform{1.0f};
619 cueTransform = glm::translate(cueTransform, glm::vec3{stickCenter.x, BALL_RADIUS + 0.05f, stickCenter.y});
620 cueTransform = glm::rotate(cueTransform, -worldCueAngle, glm::vec3{0.0f, 1.0f, 0.0f});
621 cueTransform = glm::scale(cueTransform, glm::vec3{CUE_LENGTH * 0.5f, CUE_THICKNESS, CUE_THICKNESS});
623 const float pct = chargeAmount / MAX_POWER;
625 ? glm::vec4{0.55f + pct * 0.45f, 0.27f * (1.0f - pct), 0.07f * (1.0f - pct), 1.0f}
626 : glm::vec4{0.55f, 0.27f, 0.07f, 1.0f};
627 drawModel(cueStickModel, imageIndex, cmd, view, proj, cueTransform, cueColor, time);
629 const glm::vec2 aimCenter = balls[0].pos + dir * (BALL_RADIUS + AIM_LENGTH * 0.5f);
630 glm::mat4 aimTransform{1.0f};
631 aimTransform = glm::translate(aimTransform, glm::vec3{aimCenter.x, BALL_RADIUS + 0.06f, aimCenter.y});
632 aimTransform = glm::rotate(aimTransform, -worldCueAngle, glm::vec3{0.0f, 1.0f, 0.0f});
633 aimTransform = glm::scale(aimTransform, glm::vec3{AIM_LENGTH * 0.5f, AIM_THICKNESS_Y, AIM_THICKNESS_Z});
634 drawModel(cueAimModel, imageIndex, cmd, view, proj, aimTransform, glm::vec4{1.0f, 1.0f, 0.0f, 1.0f}, time);
639 static glm::mat4 composeTransform(
const glm::vec3 &pos,
const glm::vec3 &scale) {
641 m = glm::translate(m, pos);
642 m = glm::scale(m, scale);
647 backgroundSprite =
createSprite(assetRoot +
"/data/background.png", assetRoot +
"/data/sprite_vert.spv", assetRoot +
"/data/sprite_frag.spv");
648 startSprite =
createSprite(assetRoot +
"/data/start.png", assetRoot +
"/data/sprite_vert.spv", assetRoot +
"/data/sprite_frag.spv");
649 scoresSprite =
createSprite(assetRoot +
"/data/scores.png", assetRoot +
"/data/sprite_vert.spv", assetRoot +
"/data/sprite_frag.spv");
650 introSprite =
createSprite(assetRoot +
"/data/logo.png", assetRoot +
"/data/sprite_vert.spv", assetRoot +
"/data/bend_dir.spv");
654 loadModel(feltModel, assetRoot +
"/data/pooltable_felt.mxmod.z");
655 loadModel(woodModel, assetRoot +
"/data/pooltable_wood.mxmod.z");
656 loadModel(pocketModel, assetRoot +
"/data/pooltable_pocket.mxmod.z");
658 for (
auto &ballModel : ballModels) {
659 loadModel(ballModel, assetRoot +
"/data/geosphere.mxmod.z");
661 loadModel(cueStickModel, assetRoot +
"/data/cube.mxmod.z", 2.0f);
662 loadModel(cueAimModel, assetRoot +
"/data/cube.mxmod.z", 2.0f);
664 applyPrimaryTexture(feltModel, assetRoot +
"/data/table.png");
667 void cleanupModels() {
668 feltModel.cleanup(
this);
669 woodModel.cleanup(
this);
670 pocketModel.cleanup(
this);
671 for (
auto &ballModel : ballModels) {
672 ballModel.cleanup(
this);
674 cueStickModel.cleanup(
this);
675 cueAimModel.cleanup(
this);
678 void loadModel(mxvk::VKAbstractModel &model,
const std::string &path,
float scale = 1.0f) {
679 model.
load(
this, path,
"", assetRoot +
"/data", scale);
681 assetRoot +
"/data/model.vert.spv",
682 assetRoot +
"/data/model.frag.spv");
685 void drawModel(mxvk::VKAbstractModel &model,
688 const glm::mat4 &view,
689 const glm::mat4 &proj,
690 const glm::mat4 &transform,
693 mxvk::UniformBufferObject ubo{};
695 ubo.
model = transform;
698 ubo.
fx = glm::vec4(fx.r, fx.g, fx.b, time);
700 model.
render(cmd, imageIndex,
false);
703 void applyPrimaryTexture(mxvk::VKAbstractModel &model,
const std::string &texturePath) {
709 SDL_Surface *rgba = SDL_ConvertSurface(
surface, SDL_PIXELFORMAT_RGBA32);
711 if (rgba ==
nullptr) {
715 const int pitch =
static_cast<int>(rgba->pitch);
716 [[maybe_unused]]
const bool texture_updated = model.
updatePrimaryTexture(rgba->pixels, rgba->w, rgba->h, pitch);
717 SDL_DestroySurface(rgba);
720 void setScreen(GameScreen next) {
722 SDL_StopTextInput(
window.get());
723 setFont(assetRoot +
"/font.ttf", 24);
727 SDL_StartTextInput(
window.get());
731 setMouseCapture(
true);
733 setMouseCapture(
false);
737 void setMouseCapture(
bool enabled) {
738 if (mouseCaptured == enabled) {
741 mouseCaptured = enabled;
742 [[maybe_unused]]
const bool mouse_grabbed = SDL_SetWindowMouseGrab(
window.get(), enabled);
743 [[maybe_unused]]
const bool relative_mouse_mode = SDL_SetWindowRelativeMouseMode(
window.get(), enabled);
749 mouseCamDragging =
false;
752 void procIntro(
int width,
int height) {
753 if (startTicks == 0U) {
754 startTicks = SDL_GetTicks();
757 const Uint64 elapsed = SDL_GetTicks() - startTicks;
758 constexpr Uint64 TOTAL = 5000;
759 constexpr Uint64 FADE_DUR = 1500;
761 float fadeOut = 1.0f;
762 if (elapsed > (TOTAL - FADE_DUR)) {
763 fadeOut = 1.0f -
static_cast<float>(elapsed - (TOTAL - FADE_DUR)) /
static_cast<float>(FADE_DUR);
766 if (startSprite !=
nullptr) {
767 startSprite->setShaderParams(1.0f, 1.0f, 1.0f, 1.0f);
768 startSprite->drawSpriteRect(0, 0, width, height);
770 if (introSprite !=
nullptr) {
771 introSprite->setShaderParams(fadeOut, 1.0f, 1.0f,
static_cast<float>(SDL_GetTicks()) / 1000.0f);
772 introSprite->drawSpriteRect(0, 0, width, height);
775 if (elapsed >= TOTAL) {
781 void procStart(
int width,
int height) {
782 if (startSprite !=
nullptr) {
783 startSprite->setShaderParams(1.0f, 1.0f, 1.0f, 1.0f);
784 startSprite->drawSpriteRect(0, 0, width, height);
787 const char *hint =
"ENTER / A - Play";
791 const int x = width / 2 - tw / 2;
792 const int y = height - (th * 3) + 20;
793 printText(hint, x, y, SDL_Color{220, 220, 100, 255});
794 updateStartClickTargets();
797 void procScores(
int width,
int height) {
798 if (scoresSprite !=
nullptr) {
799 scoresSprite->setShaderParams(1.0f, 1.0f, 1.0f, 1.0f);
800 scoresSprite->drawSpriteRect(0, 0, width, height);
803 const int feltL =
static_cast<int>(width * 0.125f);
804 const int feltT =
static_cast<int>(height * 0.19f);
805 const int feltB =
static_cast<int>(height * 0.87f);
806 const int feltH = feltB - feltT;
807 const int feltCX =
static_cast<int>(width * 0.48f);
808 const int fs = std::max(10, feltH / 15);
810 if (fs != scoreFontSize) {
811 setFont(assetRoot +
"/font.ttf", fs);
815 const int lineH = fs + fs / 3;
816 const auto &list = highScores.list();
817 for (std::size_t i = 0; i < list.size() && i < 10U; ++i) {
818 std::ostringstream ss;
819 ss << (i + 1U) <<
". " << list[i].name <<
" " << list[i].shots <<
" shots";
820 SDL_Color color{255, 255, 255, 255};
821 printText(ss.str(), feltL + fs / 2, feltT +
static_cast<int>(i) * lineH, color);
825 const int entryY = feltT + 10 * lineH + lineH / 2;
826 std::ostringstream ys;
827 ys <<
"Your score: " << finalScore <<
" shots";
832 printText(ys.str(), feltCX - tw / 2, entryY, SDL_Color{255, 255, 0, 255});
834 const std::string dn = playerName +
"_";
836 printText(dn, feltCX - tw / 2, entryY + lineH, SDL_Color{0, 255, 255, 255});
838 const std::string confirm =
"ENTER to confirm";
839 const std::string del =
"BACKSPACE to delete";
843 printText(confirm, feltCX - cw - fs / 3, entryY + lineH * 2, SDL_Color{200, 200, 200, 255});
847 printText(del, feltCX + fs / 3, entryY + lineH * 2, SDL_Color{200, 200, 200, 255});
849 scoresConfirmRect = makeTextRect(confirm, feltCX - cw - fs / 3, entryY + lineH * 2, 10);
850 scoresDeleteRect = makeTextRect(del, feltCX + fs / 3, entryY + lineH * 2, 10);
851 scoresReturnRect = SDL_Rect{0, 0, 0, 0};
853 const std::string ret =
"Press ENTER to return to intro";
857 const int x = feltCX - tw / 2;
858 const int y = feltB - lineH;
859 printText(ret, x, y, SDL_Color{255, 255, 0, 255});
860 scoresReturnRect = makeTextRect(ret, x, y, 12);
861 scoresConfirmRect = SDL_Rect{0, 0, 0, 0};
862 scoresDeleteRect = SDL_Rect{0, 0, 0, 0};
866 void procGame(
int width,
int height) {
867 const float now =
static_cast<float>(SDL_GetTicks()) / 1000.0f;
868 float dt = now - lastTime;
875 handleCameraAndInput(dt);
877 printText(
"Shots: " + std::to_string(shotCount), 15, 45, SDL_Color{255, 255, 0, 255});
880 if (balls[i].
active && !balls[i].pocketed) {
884 printText(
"Balls: " + std::to_string(rem), 15, 75, SDL_Color{200, 200, 200, 255});
887 printText(
"Mouse: move aim + hold/release | Right-drag: rotate table | Wheel/Pinch: zoom", 15, height - 40,
888 SDL_Color{180, 180, 180, 255});
890 const int pct =
static_cast<int>(chargeAmount /
MAX_POWER * 100.0f);
891 printText(
"Power: " + std::to_string(pct) +
"%", 15, 105,
892 SDL_Color{255,
static_cast<Uint8
>(std::max(0, 255 - pct * 2)), 0, 255});
894 printText(
"Mouse move: place cue by camera direction | Click/Enter/A/B: place", 15, height - 40,
895 SDL_Color{255, 100, 100, 255});
899 void handleGameState(
float dt) {
909 if (!anyBallMoving()) {
910 if (balls[0].pocketed) {
912 balls[0].active =
true;
913 balls[0].pocketed =
false;
915 balls[0].vel = glm::vec2{0.0f};
929 for (
auto &ball : balls) {
930 if (ball.active && ball.isMoving()) {
931 ball.spinAngle += glm::length(ball.vel) * 5.0f * dt;
935 for (
auto &anim : sinkAnims) {
939 std::remove_if(sinkAnims.begin(), sinkAnims.end(), [](
const SinkAnim &anim) {
940 return anim.timer >= SINK_DURATION;
945 void handleCameraAndInput(
float dt) {
946 const bool *keys = SDL_GetKeyboardState(
nullptr);
947 if (keys[SDL_SCANCODE_A]) {
948 camAngle -= 1.2f * dt;
950 if (keys[SDL_SCANCODE_S]) {
951 camAngle += 1.2f * dt;
953 if (keys[SDL_SCANCODE_W]) {
954 camZoom -= 5.0f * dt;
956 if (keys[SDL_SCANCODE_E]) {
957 camZoom += 5.0f * dt;
959 camZoom = glm::clamp(camZoom, 5.0f, 25.0f);
961 if (gamepad ==
nullptr) {
965 constexpr float dead = 0.15f;
966 const auto readAxis = [
this](SDL_GamepadAxis axis) {
967 return static_cast<float>(SDL_GetGamepadAxis(gamepad, axis)) / 32767.0f;
970 const float rx = readAxis(SDL_GAMEPAD_AXIS_RIGHTX);
971 const float ry = readAxis(SDL_GAMEPAD_AXIS_RIGHTY);
972 if (std::fabs(rx) > dead) {
973 camAngle += rx * 1.5f * dt;
975 if (std::fabs(ry) > dead) {
976 camZoom += ry * 5.0f * dt;
978 camZoom = glm::clamp(camZoom, 5.0f, 25.0f);
980 const float lx = readAxis(SDL_GAMEPAD_AXIS_LEFTX);
981 const float ly = readAxis(SDL_GAMEPAD_AXIS_LEFTY);
983 if (std::fabs(lx) > dead) {
984 cueAngle += lx * 1.5f * dt;
987 const float s = 3.0f * dt;
988 const glm::vec2 camRight{std::cos(camAngle), -std::sin(camAngle)};
989 const glm::vec2 camFwd{-std::sin(camAngle), -std::cos(camAngle)};
990 if (std::fabs(lx) > dead) {
991 balls[0].pos += camRight * (lx * s);
993 if (std::fabs(ly) > dead) {
994 balls[0].pos -= camFwd * (ly * s);
1000 void handleGamepadButtonDown(Uint8 button) {
1002 if (button == SDL_GAMEPAD_BUTTON_SOUTH || button == SDL_GAMEPAD_BUTTON_START) {
1009 if (!enteringName && (button == SDL_GAMEPAD_BUTTON_SOUTH || button == SDL_GAMEPAD_BUTTON_EAST || button == SDL_GAMEPAD_BUTTON_START)) {
1016 if (button == SDL_GAMEPAD_BUTTON_SOUTH || button == SDL_GAMEPAD_BUTTON_START) {
1019 }
else if (button == SDL_GAMEPAD_BUTTON_NORTH) {
1021 }
else if (button == SDL_GAMEPAD_BUTTON_BACK) {
1031 if (button == SDL_GAMEPAD_BUTTON_BACK) {
1036 if ((button == SDL_GAMEPAD_BUTTON_SOUTH || button == SDL_GAMEPAD_BUTTON_EAST) && phase ==
GamePhase::Aiming) {
1038 chargeAmount = 0.0f;
1039 ctrlChargeButton =
static_cast<int>(button);
1040 }
else if ((button == SDL_GAMEPAD_BUTTON_SOUTH || button == SDL_GAMEPAD_BUTTON_EAST) && phase ==
GamePhase::Placing && canPlaceCueBall()) {
1045 void handleGamepadButtonUp(Uint8 button) {
1048 ctrlChargeButton = -1;
1052 bool openGamepad(SDL_JoystickID
id) {
1053 if (gamepad !=
nullptr && gamepadId ==
id) {
1058 gamepad = SDL_OpenGamepad(
id);
1059 if (gamepad ==
nullptr) {
1066 void tryOpenFirstGamepad() {
1067 if (gamepad !=
nullptr) {
1071 SDL_JoystickID *ids = SDL_GetGamepads(&count);
1072 if (ids ==
nullptr || count <= 0) {
1073 if (ids !=
nullptr) {
1078 openGamepad(ids[0]);
1082 void closeGamepad() {
1083 if (gamepad !=
nullptr) {
1084 SDL_CloseGamepad(gamepad);
1095 chargeAmount = 0.0f;
1097 pointerDown =
false;
1098 pointerCharging =
false;
1099 activePointerId = std::numeric_limits<int64_t>::min();
1100 mouseCamDragging =
false;
1101 touchPinchActive =
false;
1102 touchPinchDistance = 0.0f;
1103 touchPoints.clear();
1104 lastTime =
static_cast<float>(SDL_GetTicks()) / 1000.0f;
1110 balls[0].active =
true;
1111 balls[0].number = 0;
1113 const int order[15] = {1, 2, 3, 8, 4, 5, 6, 7, 9, 10, 11, 12, 13, 14, 15};
1118 for (
int row = 0; row < 5; ++row) {
1119 for (
int col = 0; col <= row; ++col) {
1123 const int bn = order[idx++];
1125 balls[bn].pos = glm::vec2{sx + row * sp * 0.866f, (col - row * 0.5f) * sp};
1126 balls[bn].active =
true;
1127 balls[bn].number = bn;
1128 balls[bn].spinAngle =
randFloat(0.0f, 2.0f * PI);
1133 void handleAiming(
float dt) {
1134 const bool *keys = SDL_GetKeyboardState(
nullptr);
1135 if (keys[SDL_SCANCODE_LEFT]) {
1136 cueAngle -= 1.5f * dt;
1138 if (keys[SDL_SCANCODE_RIGHT]) {
1139 cueAngle += 1.5f * dt;
1143 void handleCharging(
float dt) {
1144 const bool *keys = SDL_GetKeyboardState(
nullptr);
1145 if (keys[SDL_SCANCODE_LEFT]) {
1146 cueAngle -= 1.5f * dt;
1148 if (keys[SDL_SCANCODE_RIGHT]) {
1149 cueAngle += 1.5f * dt;
1152 if (chargeAmount > MAX_POWER) {
1157 void handlePlacing(
float dt) {
1158 const bool *keys = SDL_GetKeyboardState(
nullptr);
1159 const float s = 3.0f * dt;
1160 const glm::vec2 camRight{std::cos(camAngle), -std::sin(camAngle)};
1161 const glm::vec2 camFwd{-std::sin(camAngle), -std::cos(camAngle)};
1163 if (keys[SDL_SCANCODE_LEFT]) {
1164 balls[0].pos -= camRight * s;
1166 if (keys[SDL_SCANCODE_RIGHT]) {
1167 balls[0].pos += camRight * s;
1169 if (keys[SDL_SCANCODE_UP]) {
1170 balls[0].pos += camFwd * s;
1172 if (keys[SDL_SCANCODE_DOWN]) {
1173 balls[0].pos -= camFwd * s;
1179 void clampCueBall() {
1181 balls[0].pos.x = glm::clamp(balls[0].pos.x, -TABLE_HALF_W + m, TABLE_HALF_W - m);
1182 balls[0].pos.y = glm::clamp(balls[0].pos.y, -TABLE_HALF_H + m, TABLE_HALF_H - m);
1185 void updatePhysics(
float dt) {
1186 float maxSpeed = 0.0f;
1187 for (
const auto &ball : balls) {
1188 if (ball.active && !ball.pocketed) {
1189 maxSpeed = std::max(maxSpeed, glm::length(ball.vel));
1194 const int steps = std::max(8,
static_cast<int>(std::ceil(maxSpeed * dt * 60.0f / maxMovePerStep)));
1195 const float sub = dt /
static_cast<float>(steps);
1196 const float stepFriction = std::pow(FRICTION, 4.0f /
static_cast<float>(steps));
1198 for (
int s = 0; s < steps; ++s) {
1199 for (
auto &ball : balls) {
1200 if (!ball.active || ball.pocketed) {
1203 ball.pos += ball.vel * sub * 60.0f;
1207 if (!balls[i].
active || balls[i].pocketed) {
1210 for (
int j = i + 1; j <
NUM_BALLS; ++j) {
1211 if (!balls[j].
active || balls[j].pocketed) {
1214 resolveBall(balls[i], balls[j]);
1218 for (
auto &ball : balls) {
1219 if (!ball.active || ball.pocketed) {
1225 for (
auto &ball : balls) {
1226 if (!ball.active || ball.pocketed) {
1232 for (
auto &ball : balls) {
1233 if (!ball.active || ball.pocketed) {
1236 ball.vel *= stepFriction;
1237 if (glm::length(ball.vel) < MIN_SPEED) {
1238 ball.vel = glm::vec2{0.0f};
1244 void resolveBall(PoolBall &a, PoolBall &b) {
1245 const glm::vec2 d = b.pos - a.pos;
1246 const float dist = glm::length(d);
1248 if (dist >= minD || dist <= 0.0001f) {
1252 const glm::vec2 n = d / dist;
1253 const float overlap = minD - dist;
1254 a.pos -= n * (overlap * 0.5f);
1255 b.pos += n * (overlap * 0.5f);
1257 const float rv = glm::dot(a.vel - b.vel, n);
1264 void resolveWall(PoolBall &ball) {
1270 if (ball.pos.x < l) {
1272 ball.vel.x = -ball.vel.x * 0.8f;
1274 if (ball.pos.x > r) {
1276 ball.vel.x = -ball.vel.x * 0.8f;
1278 if (ball.pos.y < t) {
1280 ball.vel.y = -ball.vel.y * 0.8f;
1282 if (ball.pos.y > b) {
1284 ball.vel.y = -ball.vel.y * 0.8f;
1288 void checkPocket(PoolBall &ball) {
1289 for (
const auto &pocket : POCKETS) {
1290 if (glm::length(ball.pos - pocket) < POCKET_R) {
1291 const int idx =
static_cast<int>(&ball - &balls[0]);
1292 sinkAnims.push_back(SinkAnim{pocket,
1297 ball.pocketed =
true;
1298 ball.vel = glm::vec2{0.0f};
1304 [[nodiscard]]
bool anyBallMoving()
const {
1305 for (
const auto &ball : balls) {
1306 if (ball.active && !ball.pocketed && ball.isMoving()) {
1310 return !sinkAnims.empty();
1313 [[nodiscard]]
bool allObjectBallsPocketed()
const {
1315 if (balls[i].
active && !balls[i].pocketed) {
1322 void checkGameOver() {
1323 if (!allObjectBallsPocketed()) {
1326 finalScore = shotCount;
1328 enteringName = highScores.qualifies(finalScore);
1330 SDL_StartTextInput(
window.get());
1335 void commitScoreEntry() {
1336 if (playerName.empty()) {
1339 highScores.addScore(playerName, finalScore);
1341 enteringName =
false;
1342 SDL_StopTextInput(
window.get());
1345 [[nodiscard]] glm::vec3 getCameraPosition()
const {
1347 const float horiz = orbitDist * std::cos(camPitch);
1348 const float y = orbitDist * std::sin(camPitch);
1349 return glm::vec3{std::sin(camAngle) * horiz, y, std::cos(camAngle) * horiz};
1352 bool screenPointToTable(
int px,
int py, glm::vec2 &out)
const {
1353 if (fallbackWidth <= 0 || fallbackHeight <= 0) {
1357 const float aspect =
static_cast<float>(fallbackWidth) /
static_cast<float>(fallbackHeight);
1358 const glm::vec3 camPos = getCameraPosition();
1359 const glm::mat4 view = glm::lookAt(camPos, glm::vec3{0.0f}, glm::vec3{0.0f, 1.0f, 0.0f});
1360 glm::mat4 proj = glm::perspective(glm::radians(45.0f), aspect, 0.1f, 100.0f);
1361 proj[1][1] *= -1.0f;
1363 const float nx = (2.0f * (
static_cast<float>(px) + 0.5f) /
static_cast<float>(fallbackWidth)) - 1.0f;
1364 const float ny = 1.0f - (2.0f * (
static_cast<float>(py) + 0.5f) /
static_cast<float>(fallbackHeight));
1365 const glm::vec4 nearClip{nx, ny, 0.0f, 1.0f};
1366 const glm::vec4 farClip{nx, ny, 1.0f, 1.0f};
1367 const glm::mat4 invVP = glm::inverse(proj * view);
1369 glm::vec4 nearWorld = invVP * nearClip;
1370 glm::vec4 farWorld = invVP * farClip;
1371 if (std::fabs(nearWorld.w) < 1e-6f || std::fabs(farWorld.w) < 1e-6f) {
1375 nearWorld /= nearWorld.w;
1376 farWorld /= farWorld.w;
1378 const glm::vec3 ro{nearWorld.x, nearWorld.y, nearWorld.z};
1379 const glm::vec3 rf{farWorld.x, farWorld.y, farWorld.z};
1380 const glm::vec3 rd = glm::normalize(rf - ro);
1381 if (std::fabs(rd.y) < 1e-6f) {
1385 const float t = -ro.y / rd.y;
1390 const glm::vec3 hit = ro + rd * t;
1391 out = glm::vec2{hit.x, hit.z};
1395 void updateCueFromPointer(
int px,
int py) {
1400 glm::vec2 tablePos{0.0f};
1401 if (!screenPointToTable(px, py, tablePos)) {
1405 const glm::vec2 d = tablePos - balls[0].pos;
1406 if (glm::dot(d, d) < 0.00001f) {
1410 const float worldAngle = std::atan2(d.y, d.x);
1411 cueAngle = worldAngle - camAngle;
1414 void onMouseRelativeMove(
int dx,
int dy) {
1419 cueAngle +=
static_cast<float>(dx) * 0.012f;
1423 constexpr float MOVE_SCALE = 0.02f;
1424 const glm::vec2 camRight{std::cos(camAngle), -std::sin(camAngle)};
1425 const glm::vec2 camFwd{-std::sin(camAngle), -std::cos(camAngle)};
1426 balls[0].pos += camRight * (
static_cast<float>(dx) * MOVE_SCALE);
1427 balls[0].pos -= camFwd * (
static_cast<float>(dy) * MOVE_SCALE);
1432 void placeCueBallFromPointer(
int px,
int py) {
1437 glm::vec2 tablePos{0.0f};
1438 if (!screenPointToTable(px, py, tablePos)) {
1442 balls[0].pos = tablePos;
1446 [[nodiscard]]
bool canPlaceCueBall()
const {
1448 if (!balls[i].
active || balls[i].pocketed) {
1451 if (glm::length(balls[0].pos - balls[i].pos) < BALL_RADIUS * 2.5f) {
1458 void shootCueBall() {
1459 const float worldCueAngle = cueAngle + camAngle;
1460 const glm::vec2 dir{std::cos(worldCueAngle), std::sin(worldCueAngle)};
1461 balls[0].vel = dir * chargeAmount;
1464 chargeAmount = 0.0f;
1465 pointerCharging =
false;
1466 pointerDown =
false;
1467 activePointerId = std::numeric_limits<int64_t>::min();
1470 SDL_Rect makeTextRect(
const std::string &txt,
int x,
int y,
int pad) {
1482 SDL_Rect makeNormRect(
float cxN,
float cyN,
float wN,
float hN)
const {
1484 r.w = std::max(1,
static_cast<int>(fallbackWidth * wN));
1485 r.h = std::max(1,
static_cast<int>(fallbackHeight * hN));
1486 const int cx =
static_cast<int>(fallbackWidth * cxN);
1487 const int cy =
static_cast<int>(fallbackHeight * cyN);
1493 void updateStartClickTargets() {
1494 startPlayRect = makeNormRect(0.50f, 0.78f, 0.28f, 0.11f);
1497 static bool pointInRect(
int x,
int y,
const SDL_Rect &r) {
1498 return x >= r.x && x <= (r.x + r.w) && y >= r.y && y <= (r.y + r.h);
1501 void onPointerDown(
int px,
int py, int64_t pointerId) {
1508 updateStartClickTargets();
1509 if (pointInRect(px, py, startPlayRect)) {
1517 if (!enteringName) {
1518 if (pointInRect(px, py, scoresReturnRect)) {
1523 if (pointInRect(px, py, scoresConfirmRect) && !playerName.empty()) {
1527 if (pointInRect(px, py, scoresDeleteRect) && !playerName.empty()) {
1528 playerName.pop_back();
1536 if (pointerId == -1 && !mouseCaptured) {
1539 if (activePointerId != std::numeric_limits<int64_t>::min() && activePointerId != pointerId) {
1544 activePointerId = pointerId;
1545 const bool isCapturedMousePointer = (pointerId == -1 && mouseCaptured);
1549 if (!isCapturedMousePointer) {
1550 updateCueFromPointer(px, py);
1553 chargeAmount = 0.0f;
1554 pointerCharging =
true;
1556 if (!isCapturedMousePointer) {
1557 updateCueFromPointer(px, py);
1559 pointerCharging =
true;
1561 if (!(pointerId == -1 && mouseCaptured)) {
1562 placeCueBallFromPointer(px, py);
1567 void onPointerMove(
int px,
int py, int64_t pointerId) {
1571 if (activePointerId != std::numeric_limits<int64_t>::min() && activePointerId != pointerId) {
1576 updateCueFromPointer(px, py);
1578 placeCueBallFromPointer(px, py);
1582 void onPointerUp(
int px,
int py, int64_t pointerId) {
1584 onPointerDown(px, py, pointerId);
1591 if (activePointerId != pointerId) {
1595 pointerDown =
false;
1602 if (!(pointerId == -1 && mouseCaptured)) {
1603 placeCueBallFromPointer(px, py);
1605 if (canPlaceCueBall()) {
1610 pointerCharging =
false;
1611 activePointerId = std::numeric_limits<int64_t>::min();
1614 std::string assetRoot;
1617 mxvk::VK_Sprite *backgroundSprite =
nullptr;
1618 mxvk::VK_Sprite *startSprite =
nullptr;
1619 mxvk::VK_Sprite *introSprite =
nullptr;
1620 mxvk::VK_Sprite *scoresSprite =
nullptr;
1622 mxvk::VKAbstractModel feltModel{};
1623 mxvk::VKAbstractModel woodModel{};
1624 mxvk::VKAbstractModel pocketModel{};
1625 std::array<mxvk::VKAbstractModel, NUM_BALLS> ballModels{};
1626 mxvk::VKAbstractModel cueStickModel{};
1627 mxvk::VKAbstractModel cueAimModel{};
1629 std::array<PoolBall, NUM_BALLS> balls{};
1630 std::vector<SinkAnim> sinkAnims{};
1635 float cueAngle = 0.0f;
1636 float chargeAmount = 0.0f;
1638 float lastTime = 0.0f;
1639 float camAngle = 0.4f;
1640 float camPitch = 0.744604f;
1641 float camZoom = 13.0f;
1643 SDL_Gamepad *gamepad =
nullptr;
1644 SDL_JoystickID gamepadId = 0;
1645 int ctrlChargeButton = -1;
1647 bool enteringName =
false;
1648 std::string playerName;
1650 int scoreFontSize = 0;
1652 bool pointerDown =
false;
1653 bool pointerCharging =
false;
1654 int64_t activePointerId = std::numeric_limits<int64_t>::min();
1655 bool consumeNextMouseLeftUp =
false;
1656 bool mouseCaptured =
false;
1657 bool mouseCamDragging =
false;
1658 int mouseCamLastX = 0;
1659 int mouseCamLastY = 0;
1660 bool touchPinchActive =
false;
1661 float touchPinchDistance = 0.0f;
1662 std::unordered_map<int64_t, SDL_FPoint> touchPoints;
1664 Uint64 startTicks = 0U;
1666 int fallbackWidth = 1280;
1667 int fallbackHeight = 720;
1669 SDL_Rect startPlayRect{0, 0, 0, 0};
1670 SDL_Rect scoresReturnRect{0, 0, 0, 0};
1671 SDL_Rect scoresConfirmRect{0, 0, 0, 0};
1672 SDL_Rect scoresDeleteRect{0, 0, 0, 0};