235 PoolWindow(
int width,
int height,
bool fullscreen,
bool enable_vsync, std::string asset_root) :
mxvk::
VK_Window(
"3D Pool / MXVK", width, height, fullscreen,
MXVK_VALIDATION, enable_vsync), assetRoot(std::move(asset_root)), highScores((std::filesystem::path(assetRoot) /
"pool_scores.dat").string()), fallbackWidth(width), fallbackHeight(height) {
236 setFont(assetRoot +
"/font.ttf", 24);
240 tryOpenFirstGamepad();
244 if (
device != VK_NULL_HANDLE) {
253 const int renderW = (extent.width > 0U) ?
static_cast<int>(extent.width) : fallbackWidth;
254 const int renderH = (extent.height > 0U) ?
static_cast<int>(extent.height) : fallbackHeight;
255 fallbackWidth = renderW;
256 fallbackHeight = renderH;
260 procIntro(renderW, renderH);
263 procStart(renderW, renderH);
266 procGame(renderW, renderH);
269 procScores(renderW, renderH);
275 if (e.type == SDL_EVENT_QUIT) {
280 if (e.type == SDL_EVENT_GAMEPAD_ADDED) {
281 openGamepad(e.gdevice.which);
285 if (e.type == SDL_EVENT_GAMEPAD_REMOVED) {
286 if (gamepad !=
nullptr && e.gdevice.which == gamepadId) {
288 tryOpenFirstGamepad();
293 if (e.type == SDL_EVENT_KEY_DOWN) {
294 if (e.key.key == SDLK_ESCAPE) {
296 setMouseCapture(
false);
309 if (e.key.key == SDLK_RETURN && !playerName.empty()) {
311 }
else if (e.key.key == SDLK_BACKSPACE && !playerName.empty()) {
312 playerName.pop_back();
314 }
else if (e.key.key == SDLK_RETURN) {
321 if (e.key.key == SDLK_RETURN) {
324 }
else if (e.key.key == SDLK_SPACE) {
331 if (e.key.key == SDLK_R) {
336 }
else if (e.key.key == SDLK_RETURN && phase ==
GamePhase::Placing && canPlaceCueBall()) {
349 if (playerName.size() < 15U) {
350 playerName += e.text.text;
356 const float dy = (e.wheel.y != 0.0f) ? e.wheel.y : e.wheel.integer_y;
357 camZoom -= dy * 1.2f;
358 camZoom = glm::clamp(camZoom, 5.0f, 25.0f);
362 if (e.type == SDL_EVENT_MOUSE_BUTTON_DOWN && e.button.button == SDL_BUTTON_RIGHT && screen ==
GameScreen::Game) {
363 if (!mouseCaptured) {
364 setMouseCapture(
true);
366 mouseCamDragging =
true;
367 mouseCamLastX =
static_cast<int>(e.button.x);
368 mouseCamLastY =
static_cast<int>(e.button.y);
372 if (e.type == SDL_EVENT_MOUSE_BUTTON_DOWN && e.button.button == SDL_BUTTON_LEFT) {
374 setMouseCapture(
true);
375 consumeNextMouseLeftUp =
true;
378 onPointerDown(
static_cast<int>(e.button.x),
static_cast<int>(e.button.y), -1);
382 if (e.type == SDL_EVENT_MOUSE_BUTTON_UP && e.button.button == SDL_BUTTON_RIGHT) {
383 mouseCamDragging =
false;
387 if (e.type == SDL_EVENT_MOUSE_BUTTON_UP && e.button.button == SDL_BUTTON_LEFT) {
388 if (consumeNextMouseLeftUp) {
389 consumeNextMouseLeftUp =
false;
396 onPointerUp(
static_cast<int>(e.button.x),
static_cast<int>(e.button.y), -1);
400 if (e.type == SDL_EVENT_MOUSE_MOTION) {
402 if (mouseCamDragging) {
403 camAngle +=
static_cast<float>(e.motion.xrel) * 0.01f;
404 camPitch -=
static_cast<float>(e.motion.yrel) * 0.006f;
405 camPitch = glm::clamp(camPitch, 0.30f, 1.25f);
408 onMouseRelativeMove(e.motion.xrel, e.motion.yrel);
412 const int nx =
static_cast<int>(e.motion.x);
413 const int ny =
static_cast<int>(e.motion.y);
414 const int dx = nx - mouseCamLastX;
415 const int dy = ny - mouseCamLastY;
416 camAngle +=
static_cast<float>(dx) * 0.01f;
417 camPitch -=
static_cast<float>(dy) * 0.006f;
418 camPitch = glm::clamp(camPitch, 0.30f, 1.25f);
423 onPointerMove(
static_cast<int>(e.motion.x),
static_cast<int>(e.motion.y), -1);
427 if (e.type == SDL_EVENT_FINGER_DOWN) {
428 const int px =
static_cast<int>(e.tfinger.x *
static_cast<float>(fallbackWidth));
429 const int py =
static_cast<int>(e.tfinger.y *
static_cast<float>(fallbackHeight));
430 touchPoints[
static_cast<int64_t
>(e.tfinger.fingerID)] = SDL_FPoint{
static_cast<float>(px),
static_cast<float>(py)};
432 auto it = touchPoints.begin();
433 const SDL_FPoint a = it->second;
435 const SDL_FPoint b = it->second;
436 const float dx = a.x - b.x;
437 const float dy = a.y - b.y;
438 touchPinchDistance = std::sqrt(dx * dx + dy * dy);
439 touchPinchActive =
true;
444 pointerCharging =
false;
446 activePointerId = std::numeric_limits<int64_t>::min();
449 onPointerDown(px, py,
static_cast<int64_t
>(e.tfinger.fingerID));
453 if (e.type == SDL_EVENT_FINGER_MOTION) {
454 const int px =
static_cast<int>(e.tfinger.x *
static_cast<float>(fallbackWidth));
455 const int py =
static_cast<int>(e.tfinger.y *
static_cast<float>(fallbackHeight));
456 touchPoints[
static_cast<int64_t
>(e.tfinger.fingerID)] = SDL_FPoint{
static_cast<float>(px),
static_cast<float>(py)};
457 if (screen ==
GameScreen::Game && touchPinchActive && touchPoints.size() >= 2U) {
458 auto it = touchPoints.begin();
459 const SDL_FPoint a = it->second;
461 const SDL_FPoint b = it->second;
462 const float dx = a.x - b.x;
463 const float dy = a.y - b.y;
464 const float dist = std::sqrt(dx * dx + dy * dy);
465 const float delta = dist - touchPinchDistance;
466 touchPinchDistance = dist;
467 camZoom -= delta * 0.02f;
468 camZoom = glm::clamp(camZoom, 5.0f, 25.0f);
471 onPointerMove(px, py,
static_cast<int64_t
>(e.tfinger.fingerID));
475 if (e.type == SDL_EVENT_FINGER_UP) {
476 const int px =
static_cast<int>(e.tfinger.x *
static_cast<float>(fallbackWidth));
477 const int py =
static_cast<int>(e.tfinger.y *
static_cast<float>(fallbackHeight));
478 const bool wasPinching = touchPinchActive;
479 touchPoints.erase(
static_cast<int64_t
>(e.tfinger.fingerID));
480 if (touchPinchActive && touchPoints.size() < 2U) {
481 touchPinchActive =
false;
482 touchPinchDistance = 0.0f;
485 pointerCharging =
false;
487 activePointerId = std::numeric_limits<int64_t>::min();
490 onPointerUp(px, py,
static_cast<int64_t
>(e.tfinger.fingerID));
494 if (e.type == SDL_EVENT_GAMEPAD_BUTTON_DOWN) {
495 handleGamepadButtonDown(e.gbutton.button);
499 if (e.type == SDL_EVENT_GAMEPAD_BUTTON_UP) {
500 handleGamepadButtonUp(e.gbutton.button);
506 feltModel.resize(
this);
507 woodModel.resize(
this);
508 pocketModel.resize(
this);
509 for (
auto &ballModel : ballModels) {
510 ballModel.resize(
this);
512 cueStickModel.resize(
this);
513 cueAimModel.resize(
this);
522 if (extent.width == 0U || extent.height == 0U) {
526 if (backgroundSprite !=
nullptr) {
527 backgroundSprite->setShaderParams(1.0f, 1.0f, 1.0f, 1.0f);
528 backgroundSprite->drawSpriteRect(0, 0,
static_cast<int>(extent.width),
static_cast<int>(extent.height));
529 backgroundSprite->renderSprites(cmd, backgroundSprite->getPipelineLayout(), extent.width, extent.height);
530 backgroundSprite->clearQueue();
533 const float aspect =
static_cast<float>(extent.width) /
static_cast<float>(extent.height);
534 const float time =
static_cast<float>(SDL_GetTicks()) / 1000.0f;
536 const glm::vec3 camPos = getCameraPosition();
537 const glm::mat4 view = glm::lookAt(camPos, glm::vec3{0.0f, 0.0f, 0.0f}, glm::vec3{0.0f, 1.0f, 0.0f});
538 glm::mat4 proj = glm::perspective(glm::radians(45.0f), aspect, 0.1f, 100.0f);
541 drawModel(feltModel, imageIndex, cmd, view, proj, composeTransform(glm::vec3{0.0f}, glm::vec3{1.0f}), glm::vec4{1.0f, 1.0f, 1.0f, 1.0f}, time);
542 drawModel(woodModel, imageIndex, cmd, view, proj, composeTransform(glm::vec3{0.0f}, glm::vec3{1.0f}), glm::vec4{0.4f, 0.22f, 0.05f, 1.0f}, time);
543 drawModel(pocketModel, imageIndex, cmd, view, proj, composeTransform(glm::vec3{0.0f}, glm::vec3{1.0f}), glm::vec4{0.08f, 0.08f, 0.08f, 1.0f}, time);
545 for (
int i = 0; i < NUM_BALLS; ++i) {
546 if (!balls[i].
active || balls[i].pocketed) {
550 m = glm::translate(m, glm::vec3{balls[i].pos.x, BALL_RADIUS, balls[i].pos.y});
551 m = glm::rotate(m, balls[i].spinAngle, glm::vec3{0.0f, 1.0f, 0.0f});
552 m = glm::scale(m, glm::vec3{BALL_RADIUS});
553 drawModel(ballModels[
static_cast<std::size_t
>(i)], imageIndex, cmd, view, proj, m, glm::vec4{BALL_COLORS[
static_cast<std::size_t
>(i)], 1.0f}, time);
556 for (
const auto &anim : sinkAnims) {
557 const float t = anim.timer / SINK_DURATION;
558 const float y = glm::mix(BALL_RADIUS, -BALL_RADIUS * 3.5f, t);
559 const float sc = BALL_RADIUS * (1.0f - t * 0.75f);
561 m = glm::translate(m, glm::vec3{anim.pocketPos.x, y, anim.pocketPos.y});
562 m = glm::rotate(m, anim.spinAngle + t * 6.0f, glm::vec3{0.0f, 1.0f, 0.0f});
563 m = glm::scale(m, glm::vec3{sc});
564 const std::size_t sinkIndex =
static_cast<std::size_t
>(glm::clamp(anim.ballIndex, 0, NUM_BALLS - 1));
565 drawModel(ballModels[sinkIndex], imageIndex, cmd, view, proj, m, glm::vec4{anim.color, 1.0f}, time);
570 const float stickDist = BALL_RADIUS + 0.3f + offset;
571 const float worldCueAngle = cueAngle + camAngle;
572 const glm::vec2 dir{std::cos(worldCueAngle), std::sin(worldCueAngle)};
574 const glm::vec2 stickCenter = balls[0].pos - dir * (stickDist + CUE_LENGTH * 0.5f);
575 glm::mat4 cueTransform{1.0f};
576 cueTransform = glm::translate(cueTransform, glm::vec3{stickCenter.x, BALL_RADIUS + 0.05f, stickCenter.y});
577 cueTransform = glm::rotate(cueTransform, -worldCueAngle, glm::vec3{0.0f, 1.0f, 0.0f});
578 cueTransform = glm::scale(cueTransform, glm::vec3{CUE_LENGTH * 0.5f, CUE_THICKNESS, CUE_THICKNESS});
580 const float pct = chargeAmount / MAX_POWER;
581 const glm::vec4 cueColor = (phase ==
GamePhase::Charging) ? glm::vec4{0.55f + pct * 0.45f, 0.27f * (1.0f - pct), 0.07f * (1.0f - pct), 1.0f} : glm::vec4{0.55f, 0.27f, 0.07f, 1.0f};
582 drawModel(cueStickModel, imageIndex, cmd, view, proj, cueTransform, cueColor, time);
584 const glm::vec2 aimCenter = balls[0].pos + dir * (BALL_RADIUS + AIM_LENGTH * 0.5f);
585 glm::mat4 aimTransform{1.0f};
586 aimTransform = glm::translate(aimTransform, glm::vec3{aimCenter.x, BALL_RADIUS + 0.06f, aimCenter.y});
587 aimTransform = glm::rotate(aimTransform, -worldCueAngle, glm::vec3{0.0f, 1.0f, 0.0f});
588 aimTransform = glm::scale(aimTransform, glm::vec3{AIM_LENGTH * 0.5f, AIM_THICKNESS_Y, AIM_THICKNESS_Z});
589 drawModel(cueAimModel, imageIndex, cmd, view, proj, aimTransform, glm::vec4{1.0f, 1.0f, 0.0f, 1.0f}, time);
594 static glm::mat4 composeTransform(
const glm::vec3 &pos,
const glm::vec3 &scale) {
596 m = glm::translate(m, pos);
597 m = glm::scale(m, scale);
602 backgroundSprite =
createSprite(assetRoot +
"/data/background.png", assetRoot +
"/data/sprite_vert.spv", assetRoot +
"/data/sprite_frag.spv");
603 startSprite =
createSprite(assetRoot +
"/data/start.png", assetRoot +
"/data/sprite_vert.spv", assetRoot +
"/data/sprite_frag.spv");
604 scoresSprite =
createSprite(assetRoot +
"/data/scores.png", assetRoot +
"/data/sprite_vert.spv", assetRoot +
"/data/sprite_frag.spv");
605 introSprite =
createSprite(assetRoot +
"/data/logo.png", assetRoot +
"/data/sprite_vert.spv", assetRoot +
"/data/bend_dir.spv");
609 loadModel(feltModel, assetRoot +
"/data/pooltable_felt.mxmod.z");
610 loadModel(woodModel, assetRoot +
"/data/pooltable_wood.mxmod.z");
611 loadModel(pocketModel, assetRoot +
"/data/pooltable_pocket.mxmod.z");
613 for (
auto &ballModel : ballModels) {
614 loadModel(ballModel, assetRoot +
"/data/geosphere.mxmod.z");
616 loadModel(cueStickModel, assetRoot +
"/data/cube.mxmod.z", 2.0f);
617 loadModel(cueAimModel, assetRoot +
"/data/cube.mxmod.z", 2.0f);
619 applyPrimaryTexture(feltModel, assetRoot +
"/data/table.png");
622 void cleanupModels() {
623 feltModel.cleanup(
this);
624 woodModel.cleanup(
this);
625 pocketModel.cleanup(
this);
626 for (
auto &ballModel : ballModels) {
627 ballModel.cleanup(
this);
629 cueStickModel.cleanup(
this);
630 cueAimModel.cleanup(
this);
633 void loadModel(mxvk::VKAbstractModel &model,
const std::string &path,
float scale = 1.0f) {
634 model.
load(
this, path,
"", assetRoot +
"/data", scale);
635 model.
setShaders(
this, assetRoot +
"/data/model.vert.spv", assetRoot +
"/data/model.frag.spv");
638 void drawModel(mxvk::VKAbstractModel &model, uint32_t imageIndex, VkCommandBuffer cmd,
const glm::mat4 &view,
const glm::mat4 &proj,
const glm::mat4 &transform,
const glm::vec4 &fx,
float time) {
639 mxvk::UniformBufferObject ubo{};
641 ubo.
model = transform;
644 ubo.
fx = glm::vec4(fx.r, fx.g, fx.b, time);
646 model.
render(cmd, imageIndex,
false);
649 void applyPrimaryTexture(mxvk::VKAbstractModel &model,
const std::string &texturePath) {
655 SDL_Surface *rgba = SDL_ConvertSurface(
surface, SDL_PIXELFORMAT_RGBA32);
657 if (rgba ==
nullptr) {
661 const int pitch =
static_cast<int>(rgba->pitch);
662 [[maybe_unused]]
const bool texture_updated = model.
updatePrimaryTexture(rgba->pixels, rgba->w, rgba->h, pitch);
663 SDL_DestroySurface(rgba);
666 void setScreen(GameScreen next) {
668 SDL_StopTextInput(
window.get());
669 setFont(assetRoot +
"/font.ttf", 24);
673 SDL_StartTextInput(
window.get());
677 setMouseCapture(
true);
679 setMouseCapture(
false);
683 void setMouseCapture(
bool enabled) {
684 if (mouseCaptured == enabled) {
687 mouseCaptured = enabled;
688 [[maybe_unused]]
const bool mouse_grabbed = SDL_SetWindowMouseGrab(
window.get(), enabled);
689 [[maybe_unused]]
const bool relative_mouse_mode = SDL_SetWindowRelativeMouseMode(
window.get(), enabled);
695 mouseCamDragging =
false;
698 void procIntro(
int width,
int height) {
699 if (startTicks == 0U) {
700 startTicks = SDL_GetTicks();
703 const Uint64 elapsed = SDL_GetTicks() - startTicks;
704 constexpr Uint64 TOTAL = 5000;
705 constexpr Uint64 FADE_DUR = 1500;
707 float fadeOut = 1.0f;
708 if (elapsed > (TOTAL - FADE_DUR)) {
709 fadeOut = 1.0f -
static_cast<float>(elapsed - (TOTAL - FADE_DUR)) /
static_cast<float>(FADE_DUR);
712 if (startSprite !=
nullptr) {
713 startSprite->setShaderParams(1.0f, 1.0f, 1.0f, 1.0f);
714 startSprite->drawSpriteRect(0, 0, width, height);
716 if (introSprite !=
nullptr) {
717 introSprite->setShaderParams(fadeOut, 1.0f, 1.0f,
static_cast<float>(SDL_GetTicks()) / 1000.0f);
718 introSprite->drawSpriteRect(0, 0, width, height);
721 if (elapsed >= TOTAL) {
727 void procStart(
int width,
int height) {
728 if (startSprite !=
nullptr) {
729 startSprite->setShaderParams(1.0f, 1.0f, 1.0f, 1.0f);
730 startSprite->drawSpriteRect(0, 0, width, height);
733 const char *hint =
"ENTER / A - Play";
737 const int x = width / 2 - tw / 2;
738 const int y = height - (th * 3) + 20;
739 printText(hint, x, y, SDL_Color{220, 220, 100, 255});
740 updateStartClickTargets();
743 void procScores(
int width,
int height) {
744 if (scoresSprite !=
nullptr) {
745 scoresSprite->setShaderParams(1.0f, 1.0f, 1.0f, 1.0f);
746 scoresSprite->drawSpriteRect(0, 0, width, height);
749 const int feltL =
static_cast<int>(width * 0.125f);
750 const int feltT =
static_cast<int>(height * 0.19f);
751 const int feltB =
static_cast<int>(height * 0.87f);
752 const int feltH = feltB - feltT;
753 const int feltCX =
static_cast<int>(width * 0.48f);
754 const int fs = std::max(10, feltH / 15);
756 if (fs != scoreFontSize) {
757 setFont(assetRoot +
"/font.ttf", fs);
761 const int lineH = fs + fs / 3;
762 const auto &list = highScores.list();
763 for (std::size_t i = 0; i < list.size() && i < 10U; ++i) {
764 std::ostringstream ss;
765 ss << (i + 1U) <<
". " << list[i].name <<
" " << list[i].shots <<
" shots";
766 SDL_Color color{255, 255, 255, 255};
767 printText(ss.str(), feltL + fs / 2, feltT +
static_cast<int>(i) * lineH, color);
771 const int entryY = feltT + 10 * lineH + lineH / 2;
772 std::ostringstream ys;
773 ys <<
"Your score: " << finalScore <<
" shots";
778 printText(ys.str(), feltCX - tw / 2, entryY, SDL_Color{255, 255, 0, 255});
780 const std::string dn = playerName +
"_";
782 printText(dn, feltCX - tw / 2, entryY + lineH, SDL_Color{0, 255, 255, 255});
784 const std::string confirm =
"ENTER to confirm";
785 const std::string del =
"BACKSPACE to delete";
789 printText(confirm, feltCX - cw - fs / 3, entryY + lineH * 2, SDL_Color{200, 200, 200, 255});
793 printText(del, feltCX + fs / 3, entryY + lineH * 2, SDL_Color{200, 200, 200, 255});
795 scoresConfirmRect = makeTextRect(confirm, feltCX - cw - fs / 3, entryY + lineH * 2, 10);
796 scoresDeleteRect = makeTextRect(del, feltCX + fs / 3, entryY + lineH * 2, 10);
797 scoresReturnRect = SDL_Rect{0, 0, 0, 0};
799 const std::string ret =
"Press ENTER to return to intro";
803 const int x = feltCX - tw / 2;
804 const int y = feltB - lineH;
805 printText(ret, x, y, SDL_Color{255, 255, 0, 255});
806 scoresReturnRect = makeTextRect(ret, x, y, 12);
807 scoresConfirmRect = SDL_Rect{0, 0, 0, 0};
808 scoresDeleteRect = SDL_Rect{0, 0, 0, 0};
812 void procGame(
int width,
int height) {
813 const float now =
static_cast<float>(SDL_GetTicks()) / 1000.0f;
814 float dt = now - lastTime;
821 handleCameraAndInput(dt);
823 printText(
"Shots: " + std::to_string(shotCount), 15, 45, SDL_Color{255, 255, 0, 255});
826 if (balls[i].
active && !balls[i].pocketed) {
830 printText(
"Balls: " + std::to_string(rem), 15, 75, SDL_Color{200, 200, 200, 255});
833 printText(
"Mouse: move aim + hold/release | Right-drag: rotate table | Wheel/Pinch: zoom", 15, height - 40, SDL_Color{180, 180, 180, 255});
835 const int pct =
static_cast<int>(chargeAmount /
MAX_POWER * 100.0f);
836 printText(
"Power: " + std::to_string(pct) +
"%", 15, 105, SDL_Color{255,
static_cast<Uint8
>(std::max(0, 255 - pct * 2)), 0, 255});
838 printText(
"Mouse move: place cue by camera direction | Click/Enter/A/B: place", 15, height - 40, SDL_Color{255, 100, 100, 255});
842 void handleGameState(
float dt) {
852 if (!anyBallMoving()) {
853 if (balls[0].pocketed) {
855 balls[0].active =
true;
856 balls[0].pocketed =
false;
858 balls[0].vel = glm::vec2{0.0f};
872 for (
auto &ball : balls) {
873 if (ball.active && ball.isMoving()) {
874 ball.spinAngle += glm::length(ball.vel) * 5.0f * dt;
878 for (
auto &anim : sinkAnims) {
881 sinkAnims.erase(std::remove_if(sinkAnims.begin(), sinkAnims.end(), [](
const SinkAnim &anim) { return anim.timer >= SINK_DURATION; }), sinkAnims.end());
884 void handleCameraAndInput(
float dt) {
885 const bool *keys = SDL_GetKeyboardState(
nullptr);
886 if (keys[SDL_SCANCODE_A]) {
887 camAngle -= 1.2f * dt;
889 if (keys[SDL_SCANCODE_S]) {
890 camAngle += 1.2f * dt;
892 if (keys[SDL_SCANCODE_W]) {
893 camZoom -= 5.0f * dt;
895 if (keys[SDL_SCANCODE_E]) {
896 camZoom += 5.0f * dt;
898 camZoom = glm::clamp(camZoom, 5.0f, 25.0f);
900 if (gamepad ==
nullptr) {
904 constexpr float dead = 0.15f;
905 const auto readAxis = [
this](SDL_GamepadAxis axis) {
return static_cast<float>(SDL_GetGamepadAxis(gamepad, axis)) / 32767.0f; };
907 const float rx = readAxis(SDL_GAMEPAD_AXIS_RIGHTX);
908 const float ry = readAxis(SDL_GAMEPAD_AXIS_RIGHTY);
909 if (std::fabs(rx) > dead) {
910 camAngle += rx * 1.5f * dt;
912 if (std::fabs(ry) > dead) {
913 camZoom += ry * 5.0f * dt;
915 camZoom = glm::clamp(camZoom, 5.0f, 25.0f);
917 const float lx = readAxis(SDL_GAMEPAD_AXIS_LEFTX);
918 const float ly = readAxis(SDL_GAMEPAD_AXIS_LEFTY);
920 if (std::fabs(lx) > dead) {
921 cueAngle += lx * 1.5f * dt;
924 const float s = 3.0f * dt;
925 const glm::vec2 camRight{std::cos(camAngle), -std::sin(camAngle)};
926 const glm::vec2 camFwd{-std::sin(camAngle), -std::cos(camAngle)};
927 if (std::fabs(lx) > dead) {
928 balls[0].pos += camRight * (lx * s);
930 if (std::fabs(ly) > dead) {
931 balls[0].pos -= camFwd * (ly * s);
937 void handleGamepadButtonDown(Uint8 button) {
939 if (button == SDL_GAMEPAD_BUTTON_SOUTH || button == SDL_GAMEPAD_BUTTON_START) {
946 if (!enteringName && (button == SDL_GAMEPAD_BUTTON_SOUTH || button == SDL_GAMEPAD_BUTTON_EAST || button == SDL_GAMEPAD_BUTTON_START)) {
953 if (button == SDL_GAMEPAD_BUTTON_SOUTH || button == SDL_GAMEPAD_BUTTON_START) {
956 }
else if (button == SDL_GAMEPAD_BUTTON_NORTH) {
958 }
else if (button == SDL_GAMEPAD_BUTTON_BACK) {
968 if (button == SDL_GAMEPAD_BUTTON_BACK) {
973 if ((button == SDL_GAMEPAD_BUTTON_SOUTH || button == SDL_GAMEPAD_BUTTON_EAST) && phase ==
GamePhase::Aiming) {
976 ctrlChargeButton =
static_cast<int>(button);
977 }
else if ((button == SDL_GAMEPAD_BUTTON_SOUTH || button == SDL_GAMEPAD_BUTTON_EAST) && phase ==
GamePhase::Placing && canPlaceCueBall()) {
982 void handleGamepadButtonUp(Uint8 button) {
985 ctrlChargeButton = -1;
989 bool openGamepad(SDL_JoystickID
id) {
990 if (gamepad !=
nullptr && gamepadId ==
id) {
995 gamepad = SDL_OpenGamepad(
id);
996 if (gamepad ==
nullptr) {
1003 void tryOpenFirstGamepad() {
1004 if (gamepad !=
nullptr) {
1008 SDL_JoystickID *ids = SDL_GetGamepads(&count);
1009 if (ids ==
nullptr || count <= 0) {
1010 if (ids !=
nullptr) {
1015 openGamepad(ids[0]);
1019 void closeGamepad() {
1020 if (gamepad !=
nullptr) {
1021 SDL_CloseGamepad(gamepad);
1032 chargeAmount = 0.0f;
1034 pointerDown =
false;
1035 pointerCharging =
false;
1036 activePointerId = std::numeric_limits<int64_t>::min();
1037 mouseCamDragging =
false;
1038 touchPinchActive =
false;
1039 touchPinchDistance = 0.0f;
1040 touchPoints.clear();
1041 lastTime =
static_cast<float>(SDL_GetTicks()) / 1000.0f;
1047 balls[0].active =
true;
1048 balls[0].number = 0;
1050 const int order[15] = {1, 2, 3, 8, 4, 5, 6, 7, 9, 10, 11, 12, 13, 14, 15};
1055 for (
int row = 0; row < 5; ++row) {
1056 for (
int col = 0; col <= row; ++col) {
1060 const int bn = order[idx++];
1062 balls[bn].pos = glm::vec2{sx + row * sp * 0.866f, (col - row * 0.5f) * sp};
1063 balls[bn].active =
true;
1064 balls[bn].number = bn;
1065 balls[bn].spinAngle =
randFloat(0.0f, 2.0f * PI);
1070 void handleAiming(
float dt) {
1071 const bool *keys = SDL_GetKeyboardState(
nullptr);
1072 if (keys[SDL_SCANCODE_LEFT]) {
1073 cueAngle -= 1.5f * dt;
1075 if (keys[SDL_SCANCODE_RIGHT]) {
1076 cueAngle += 1.5f * dt;
1080 void handleCharging(
float dt) {
1081 const bool *keys = SDL_GetKeyboardState(
nullptr);
1082 if (keys[SDL_SCANCODE_LEFT]) {
1083 cueAngle -= 1.5f * dt;
1085 if (keys[SDL_SCANCODE_RIGHT]) {
1086 cueAngle += 1.5f * dt;
1089 if (chargeAmount > MAX_POWER) {
1094 void handlePlacing(
float dt) {
1095 const bool *keys = SDL_GetKeyboardState(
nullptr);
1096 const float s = 3.0f * dt;
1097 const glm::vec2 camRight{std::cos(camAngle), -std::sin(camAngle)};
1098 const glm::vec2 camFwd{-std::sin(camAngle), -std::cos(camAngle)};
1100 if (keys[SDL_SCANCODE_LEFT]) {
1101 balls[0].pos -= camRight * s;
1103 if (keys[SDL_SCANCODE_RIGHT]) {
1104 balls[0].pos += camRight * s;
1106 if (keys[SDL_SCANCODE_UP]) {
1107 balls[0].pos += camFwd * s;
1109 if (keys[SDL_SCANCODE_DOWN]) {
1110 balls[0].pos -= camFwd * s;
1116 void clampCueBall() {
1118 balls[0].pos.x = glm::clamp(balls[0].pos.x, -TABLE_HALF_W + m, TABLE_HALF_W - m);
1119 balls[0].pos.y = glm::clamp(balls[0].pos.y, -TABLE_HALF_H + m, TABLE_HALF_H - m);
1122 void updatePhysics(
float dt) {
1123 float maxSpeed = 0.0f;
1124 for (
const auto &ball : balls) {
1125 if (ball.active && !ball.pocketed) {
1126 maxSpeed = std::max(maxSpeed, glm::length(ball.vel));
1131 const int steps = std::max(8,
static_cast<int>(std::ceil(maxSpeed * dt * 60.0f / maxMovePerStep)));
1132 const float sub = dt /
static_cast<float>(steps);
1133 const float stepFriction = std::pow(FRICTION, 4.0f /
static_cast<float>(steps));
1135 for (
int s = 0; s < steps; ++s) {
1136 for (
auto &ball : balls) {
1137 if (!ball.active || ball.pocketed) {
1140 ball.pos += ball.vel * sub * 60.0f;
1144 if (!balls[i].
active || balls[i].pocketed) {
1147 for (
int j = i + 1; j <
NUM_BALLS; ++j) {
1148 if (!balls[j].
active || balls[j].pocketed) {
1151 resolveBall(balls[i], balls[j]);
1155 for (
auto &ball : balls) {
1156 if (!ball.active || ball.pocketed) {
1162 for (
auto &ball : balls) {
1163 if (!ball.active || ball.pocketed) {
1169 for (
auto &ball : balls) {
1170 if (!ball.active || ball.pocketed) {
1173 ball.vel *= stepFriction;
1174 if (glm::length(ball.vel) < MIN_SPEED) {
1175 ball.vel = glm::vec2{0.0f};
1181 void resolveBall(PoolBall &a, PoolBall &b) {
1182 const glm::vec2 d = b.pos - a.pos;
1183 const float dist = glm::length(d);
1185 if (dist >= minD || dist <= 0.0001f) {
1189 const glm::vec2 n = d / dist;
1190 const float overlap = minD - dist;
1191 a.pos -= n * (overlap * 0.5f);
1192 b.pos += n * (overlap * 0.5f);
1194 const float rv = glm::dot(a.vel - b.vel, n);
1201 void resolveWall(PoolBall &ball) {
1207 if (ball.pos.x < l) {
1209 ball.vel.x = -ball.vel.x * 0.8f;
1211 if (ball.pos.x > r) {
1213 ball.vel.x = -ball.vel.x * 0.8f;
1215 if (ball.pos.y < t) {
1217 ball.vel.y = -ball.vel.y * 0.8f;
1219 if (ball.pos.y > b) {
1221 ball.vel.y = -ball.vel.y * 0.8f;
1225 void checkPocket(PoolBall &ball) {
1226 for (
const auto &pocket : POCKETS) {
1227 if (glm::length(ball.pos - pocket) < POCKET_R) {
1228 const int idx =
static_cast<int>(&ball - &balls[0]);
1229 sinkAnims.push_back(SinkAnim{pocket,
BALL_COLORS[
static_cast<std::size_t
>(idx)], ball.spinAngle, 0.0f, idx});
1230 ball.pocketed =
true;
1231 ball.vel = glm::vec2{0.0f};
1237 [[nodiscard]]
bool anyBallMoving()
const {
1238 for (
const auto &ball : balls) {
1239 if (ball.active && !ball.pocketed && ball.isMoving()) {
1243 return !sinkAnims.empty();
1246 [[nodiscard]]
bool allObjectBallsPocketed()
const {
1248 if (balls[i].
active && !balls[i].pocketed) {
1255 void checkGameOver() {
1256 if (!allObjectBallsPocketed()) {
1259 finalScore = shotCount;
1261 enteringName = highScores.qualifies(finalScore);
1263 SDL_StartTextInput(
window.get());
1268 void commitScoreEntry() {
1269 if (playerName.empty()) {
1272 highScores.addScore(playerName, finalScore);
1274 enteringName =
false;
1275 SDL_StopTextInput(
window.get());
1278 [[nodiscard]] glm::vec3 getCameraPosition()
const {
1280 const float horiz = orbitDist * std::cos(camPitch);
1281 const float y = orbitDist * std::sin(camPitch);
1282 return glm::vec3{std::sin(camAngle) * horiz, y, std::cos(camAngle) * horiz};
1285 bool screenPointToTable(
int px,
int py, glm::vec2 &out)
const {
1286 if (fallbackWidth <= 0 || fallbackHeight <= 0) {
1290 const float aspect =
static_cast<float>(fallbackWidth) /
static_cast<float>(fallbackHeight);
1291 const glm::vec3 camPos = getCameraPosition();
1292 const glm::mat4 view = glm::lookAt(camPos, glm::vec3{0.0f}, glm::vec3{0.0f, 1.0f, 0.0f});
1293 glm::mat4 proj = glm::perspective(glm::radians(45.0f), aspect, 0.1f, 100.0f);
1294 proj[1][1] *= -1.0f;
1296 const float nx = (2.0f * (
static_cast<float>(px) + 0.5f) /
static_cast<float>(fallbackWidth)) - 1.0f;
1297 const float ny = 1.0f - (2.0f * (
static_cast<float>(py) + 0.5f) /
static_cast<float>(fallbackHeight));
1298 const glm::vec4 nearClip{nx, ny, 0.0f, 1.0f};
1299 const glm::vec4 farClip{nx, ny, 1.0f, 1.0f};
1300 const glm::mat4 invVP = glm::inverse(proj * view);
1302 glm::vec4 nearWorld = invVP * nearClip;
1303 glm::vec4 farWorld = invVP * farClip;
1304 if (std::fabs(nearWorld.w) < 1e-6f || std::fabs(farWorld.w) < 1e-6f) {
1308 nearWorld /= nearWorld.w;
1309 farWorld /= farWorld.w;
1311 const glm::vec3 ro{nearWorld.x, nearWorld.y, nearWorld.z};
1312 const glm::vec3 rf{farWorld.x, farWorld.y, farWorld.z};
1313 const glm::vec3 rd = glm::normalize(rf - ro);
1314 if (std::fabs(rd.y) < 1e-6f) {
1318 const float t = -ro.y / rd.y;
1323 const glm::vec3 hit = ro + rd * t;
1324 out = glm::vec2{hit.x, hit.z};
1328 void updateCueFromPointer(
int px,
int py) {
1333 glm::vec2 tablePos{0.0f};
1334 if (!screenPointToTable(px, py, tablePos)) {
1338 const glm::vec2 d = tablePos - balls[0].pos;
1339 if (glm::dot(d, d) < 0.00001f) {
1343 const float worldAngle = std::atan2(d.y, d.x);
1344 cueAngle = worldAngle - camAngle;
1347 void onMouseRelativeMove(
int dx,
int dy) {
1352 cueAngle +=
static_cast<float>(dx) * 0.012f;
1356 constexpr float MOVE_SCALE = 0.02f;
1357 const glm::vec2 camRight{std::cos(camAngle), -std::sin(camAngle)};
1358 const glm::vec2 camFwd{-std::sin(camAngle), -std::cos(camAngle)};
1359 balls[0].pos += camRight * (
static_cast<float>(dx) * MOVE_SCALE);
1360 balls[0].pos -= camFwd * (
static_cast<float>(dy) * MOVE_SCALE);
1365 void placeCueBallFromPointer(
int px,
int py) {
1370 glm::vec2 tablePos{0.0f};
1371 if (!screenPointToTable(px, py, tablePos)) {
1375 balls[0].pos = tablePos;
1379 [[nodiscard]]
bool canPlaceCueBall()
const {
1381 if (!balls[i].
active || balls[i].pocketed) {
1384 if (glm::length(balls[0].pos - balls[i].pos) < BALL_RADIUS * 2.5f) {
1391 void shootCueBall() {
1392 const float worldCueAngle = cueAngle + camAngle;
1393 const glm::vec2 dir{std::cos(worldCueAngle), std::sin(worldCueAngle)};
1394 balls[0].vel = dir * chargeAmount;
1397 chargeAmount = 0.0f;
1398 pointerCharging =
false;
1399 pointerDown =
false;
1400 activePointerId = std::numeric_limits<int64_t>::min();
1403 SDL_Rect makeTextRect(
const std::string &txt,
int x,
int y,
int pad) {
1415 SDL_Rect makeNormRect(
float cxN,
float cyN,
float wN,
float hN)
const {
1417 r.w = std::max(1,
static_cast<int>(fallbackWidth * wN));
1418 r.h = std::max(1,
static_cast<int>(fallbackHeight * hN));
1419 const int cx =
static_cast<int>(fallbackWidth * cxN);
1420 const int cy =
static_cast<int>(fallbackHeight * cyN);
1426 void updateStartClickTargets() { startPlayRect = makeNormRect(0.50f, 0.78f, 0.28f, 0.11f); }
1428 static bool pointInRect(
int x,
int y,
const SDL_Rect &r) {
return x >= r.x && x <= (r.x + r.w) && y >= r.y && y <= (r.y + r.h); }
1430 void onPointerDown(
int px,
int py, int64_t pointerId) {
1437 updateStartClickTargets();
1438 if (pointInRect(px, py, startPlayRect)) {
1446 if (!enteringName) {
1447 if (pointInRect(px, py, scoresReturnRect)) {
1452 if (pointInRect(px, py, scoresConfirmRect) && !playerName.empty()) {
1456 if (pointInRect(px, py, scoresDeleteRect) && !playerName.empty()) {
1457 playerName.pop_back();
1465 if (pointerId == -1 && !mouseCaptured) {
1468 if (activePointerId != std::numeric_limits<int64_t>::min() && activePointerId != pointerId) {
1473 activePointerId = pointerId;
1474 const bool isCapturedMousePointer = (pointerId == -1 && mouseCaptured);
1478 if (!isCapturedMousePointer) {
1479 updateCueFromPointer(px, py);
1482 chargeAmount = 0.0f;
1483 pointerCharging =
true;
1485 if (!isCapturedMousePointer) {
1486 updateCueFromPointer(px, py);
1488 pointerCharging =
true;
1490 if (!(pointerId == -1 && mouseCaptured)) {
1491 placeCueBallFromPointer(px, py);
1496 void onPointerMove(
int px,
int py, int64_t pointerId) {
1500 if (activePointerId != std::numeric_limits<int64_t>::min() && activePointerId != pointerId) {
1505 updateCueFromPointer(px, py);
1507 placeCueBallFromPointer(px, py);
1511 void onPointerUp(
int px,
int py, int64_t pointerId) {
1513 onPointerDown(px, py, pointerId);
1520 if (activePointerId != pointerId) {
1524 pointerDown =
false;
1531 if (!(pointerId == -1 && mouseCaptured)) {
1532 placeCueBallFromPointer(px, py);
1534 if (canPlaceCueBall()) {
1539 pointerCharging =
false;
1540 activePointerId = std::numeric_limits<int64_t>::min();
1543 std::string assetRoot;
1546 mxvk::VK_Sprite *backgroundSprite =
nullptr;
1547 mxvk::VK_Sprite *startSprite =
nullptr;
1548 mxvk::VK_Sprite *introSprite =
nullptr;
1549 mxvk::VK_Sprite *scoresSprite =
nullptr;
1551 mxvk::VKAbstractModel feltModel{};
1552 mxvk::VKAbstractModel woodModel{};
1553 mxvk::VKAbstractModel pocketModel{};
1554 std::array<mxvk::VKAbstractModel, NUM_BALLS> ballModels{};
1555 mxvk::VKAbstractModel cueStickModel{};
1556 mxvk::VKAbstractModel cueAimModel{};
1558 std::array<PoolBall, NUM_BALLS> balls{};
1559 std::vector<SinkAnim> sinkAnims{};
1564 float cueAngle = 0.0f;
1565 float chargeAmount = 0.0f;
1567 float lastTime = 0.0f;
1568 float camAngle = 0.4f;
1569 float camPitch = 0.744604f;
1570 float camZoom = 13.0f;
1572 SDL_Gamepad *gamepad =
nullptr;
1573 SDL_JoystickID gamepadId = 0;
1574 int ctrlChargeButton = -1;
1576 bool enteringName =
false;
1577 std::string playerName;
1579 int scoreFontSize = 0;
1581 bool pointerDown =
false;
1582 bool pointerCharging =
false;
1583 int64_t activePointerId = std::numeric_limits<int64_t>::min();
1584 bool consumeNextMouseLeftUp =
false;
1585 bool mouseCaptured =
false;
1586 bool mouseCamDragging =
false;
1587 int mouseCamLastX = 0;
1588 int mouseCamLastY = 0;
1589 bool touchPinchActive =
false;
1590 float touchPinchDistance = 0.0f;
1591 std::unordered_map<int64_t, SDL_FPoint> touchPoints;
1593 Uint64 startTicks = 0U;
1595 int fallbackWidth = 1280;
1596 int fallbackHeight = 720;
1598 SDL_Rect startPlayRect{0, 0, 0, 0};
1599 SDL_Rect scoresReturnRect{0, 0, 0, 0};
1600 SDL_Rect scoresConfirmRect{0, 0, 0, 0};
1601 SDL_Rect scoresDeleteRect{0, 0, 0, 0};