basic gameplay for cooperative

This commit is contained in:
2025-12-21 15:33:37 +01:00
parent 5b9eb5f0e3
commit afd7fdf18d
20 changed files with 1534 additions and 263 deletions

View File

@ -25,6 +25,7 @@
#include "../../graphics/effects/Starfield.h"
#include "../../graphics/renderers/GameRenderer.h"
#include "../../gameplay/core/Game.h"
#include "../../gameplay/coop/CoopGame.h"
#include "../../gameplay/effects/LineEffect.h"
#include <SDL3/SDL.h>
#include <SDL3_image/SDL_image.h>
@ -561,6 +562,7 @@ bool ApplicationManager::initializeGame() {
m_lineEffect->init(m_renderManager->getSDLRenderer());
}
m_game = std::make_unique<Game>(m_startLevelSelection);
m_coopGame = std::make_unique<CoopGame>(m_startLevelSelection);
// Wire up sound callbacks as main.cpp did
if (m_game) {
// Apply global gravity speed multiplier from config
@ -580,6 +582,18 @@ bool ApplicationManager::initializeGame() {
});
}
if (m_coopGame) {
// TODO: tune gravity with Config and shared level scaling once coop rules are finalized
m_coopGame->reset(m_startLevelSelection);
// Wire coop sound callback to reuse same clear-line VO/SFX behavior
m_coopGame->setSoundCallback([&](int linesCleared){
SoundEffectManager::instance().playSound("clear_line", 1.0f);
if (linesCleared == 2) SoundEffectManager::instance().playRandomSound({"nice_combo"}, 1.0f);
else if (linesCleared == 3) SoundEffectManager::instance().playRandomSound({"great_move"}, 1.0f);
else if (linesCleared == 4) SoundEffectManager::instance().playRandomSound({"amazing"}, 1.0f);
});
}
// Prepare a StateContext-like struct by setting up handlers that capture
// pointers and flags. State objects in this refactor expect these to be
// available via StateManager event/update/render hooks, so we'll store them
@ -621,6 +635,7 @@ bool ApplicationManager::initializeGame() {
{
m_stateContext.stateManager = m_stateManager.get();
m_stateContext.game = m_game.get();
m_stateContext.coopGame = m_coopGame.get();
m_stateContext.scores = m_scoreManager.get();
m_stateContext.starfield = m_starfield.get();
m_stateContext.starfield3D = m_starfield3D.get();
@ -1237,74 +1252,144 @@ void ApplicationManager::setupStateHandlers() {
m_stateManager->registerUpdateHandler(AppState::Playing,
[this](double frameMs) {
if (!m_stateContext.game) return;
const bool coopActive = m_stateContext.game->getMode() == GameMode::Cooperate && m_stateContext.coopGame;
// Get current keyboard state
const bool *ks = SDL_GetKeyboardState(nullptr);
bool left = ks[SDL_SCANCODE_LEFT] || ks[SDL_SCANCODE_A];
bool right = ks[SDL_SCANCODE_RIGHT] || ks[SDL_SCANCODE_D];
bool down = ks[SDL_SCANCODE_DOWN] || ks[SDL_SCANCODE_S];
// Handle soft drop
m_stateContext.game->setSoftDropping(down && !m_stateContext.game->isPaused());
// Handle DAS/ARR movement timing (from original main.cpp)
int moveDir = 0;
if (left && !right)
moveDir = -1;
else if (right && !left)
moveDir = +1;
if (moveDir != 0 && !m_stateContext.game->isPaused()) {
if ((moveDir == -1 && !m_leftHeld) || (moveDir == +1 && !m_rightHeld)) {
// First press - immediate movement
m_stateContext.game->move(moveDir);
m_moveTimerMs = DAS; // Set initial delay
} else {
// Key held - handle repeat timing
m_moveTimerMs -= frameMs;
if (m_moveTimerMs <= 0) {
m_stateContext.game->move(moveDir);
m_moveTimerMs += ARR; // Set repeat rate
}
}
} else {
m_moveTimerMs = 0; // Reset timer when no movement
}
// Update held state for next frame
m_leftHeld = left;
m_rightHeld = right;
// Handle soft drop boost
if (down && !m_stateContext.game->isPaused()) {
m_stateContext.game->softDropBoost(frameMs);
}
// Delegate to PlayingState for other updates (gravity, line effects)
if (m_playingState) {
m_playingState->update(frameMs);
}
// Update background fade progression (match main.cpp semantics approx)
// Duration 1200ms fade (same as LEVEL_FADE_DURATION used in main.cpp snippets)
const float LEVEL_FADE_DURATION = 1200.0f;
if (m_nextLevelBackgroundTex) {
m_levelFadeElapsed += (float)frameMs;
m_levelFadeAlpha = std::min(1.0f, m_levelFadeElapsed / LEVEL_FADE_DURATION);
}
// Check for game over and transition to GameOver state
if (m_stateContext.game->isGameOver()) {
// Submit score before transitioning
if (m_stateContext.scores) {
m_stateContext.scores->submit(
m_stateContext.game->score(),
m_stateContext.game->lines(),
m_stateContext.game->level(),
m_stateContext.game->elapsed()
);
if (coopActive) {
auto handleSide = [&](CoopGame::PlayerSide side,
bool leftHeld,
bool rightHeld,
double& timer,
SDL_Scancode leftKey,
SDL_Scancode rightKey,
SDL_Scancode downKey) {
bool left = ks[leftKey];
bool right = ks[rightKey];
bool down = ks[downKey];
// Soft drop flag
m_stateContext.coopGame->setSoftDropping(side, down);
int moveDir = 0;
if (left && !right) moveDir = -1;
else if (right && !left) moveDir = +1;
if (moveDir != 0) {
if ((moveDir == -1 && !leftHeld) || (moveDir == +1 && !rightHeld)) {
// First press - immediate movement
m_stateContext.coopGame->move(side, moveDir);
timer = DAS;
} else {
timer -= frameMs;
if (timer <= 0) {
m_stateContext.coopGame->move(side, moveDir);
timer += ARR;
}
}
} else {
timer = 0.0;
}
// Soft drop boost: coop uses same gravity path; fall acceleration handled inside tickGravity
};
// Left player (WASD): A/D horizontal, S soft drop
handleSide(CoopGame::PlayerSide::Left, m_p1LeftHeld, m_p1RightHeld, m_p1MoveTimerMs,
SDL_SCANCODE_A, SDL_SCANCODE_D, SDL_SCANCODE_S);
// Right player (arrows): Left/Right horizontal, Down soft drop
handleSide(CoopGame::PlayerSide::Right, m_p2LeftHeld, m_p2RightHeld, m_p2MoveTimerMs,
SDL_SCANCODE_LEFT, SDL_SCANCODE_RIGHT, SDL_SCANCODE_DOWN);
// Update held flags for next frame
m_p1LeftHeld = ks[SDL_SCANCODE_A];
m_p1RightHeld = ks[SDL_SCANCODE_D];
m_p2LeftHeld = ks[SDL_SCANCODE_LEFT];
m_p2RightHeld = ks[SDL_SCANCODE_RIGHT];
// Gravity / effects
m_stateContext.coopGame->tickGravity(frameMs);
m_stateContext.coopGame->updateVisualEffects(frameMs);
// Delegate to PlayingState for any ancillary updates (renderer transport bookkeeping)
if (m_playingState) {
m_playingState->update(frameMs);
}
// Game over transition for coop
if (m_stateContext.coopGame->isGameOver()) {
m_stateManager->setState(AppState::GameOver);
}
} else {
bool left = ks[SDL_SCANCODE_LEFT] || ks[SDL_SCANCODE_A];
bool right = ks[SDL_SCANCODE_RIGHT] || ks[SDL_SCANCODE_D];
bool down = ks[SDL_SCANCODE_DOWN] || ks[SDL_SCANCODE_S];
// Handle soft drop
m_stateContext.game->setSoftDropping(down && !m_stateContext.game->isPaused());
// Handle DAS/ARR movement timing (from original main.cpp)
int moveDir = 0;
if (left && !right)
moveDir = -1;
else if (right && !left)
moveDir = +1;
if (moveDir != 0 && !m_stateContext.game->isPaused()) {
if ((moveDir == -1 && !m_leftHeld) || (moveDir == +1 && !m_rightHeld)) {
// First press - immediate movement
m_stateContext.game->move(moveDir);
m_moveTimerMs = DAS; // Set initial delay
} else {
// Key held - handle repeat timing
m_moveTimerMs -= frameMs;
if (m_moveTimerMs <= 0) {
m_stateContext.game->move(moveDir);
m_moveTimerMs += ARR; // Set repeat rate
}
}
} else {
m_moveTimerMs = 0; // Reset timer when no movement
}
// Update held state for next frame
m_leftHeld = left;
m_rightHeld = right;
// Handle soft drop boost
if (down && !m_stateContext.game->isPaused()) {
m_stateContext.game->softDropBoost(frameMs);
}
// Delegate to PlayingState for other updates (gravity, line effects)
if (m_playingState) {
m_playingState->update(frameMs);
}
// Update background fade progression (match main.cpp semantics approx)
// Duration 1200ms fade (same as LEVEL_FADE_DURATION used in main.cpp snippets)
const float LEVEL_FADE_DURATION = 1200.0f;
if (m_nextLevelBackgroundTex) {
m_levelFadeElapsed += (float)frameMs;
m_levelFadeAlpha = std::min(1.0f, m_levelFadeElapsed / LEVEL_FADE_DURATION);
}
// Check for game over and transition to GameOver state
if (m_stateContext.game->isGameOver()) {
// Submit score before transitioning
if (m_stateContext.scores) {
m_stateContext.scores->submit(
m_stateContext.game->score(),
m_stateContext.game->lines(),
m_stateContext.game->level(),
m_stateContext.game->elapsed()
);
}
m_stateManager->setState(AppState::GameOver);
}
m_stateManager->setState(AppState::GameOver);
}
});
// Debug overlay: show current window and logical sizes on the right side of the screen

View File

@ -17,6 +17,7 @@ class Starfield;
class Starfield3D;
class FontAtlas;
class LineEffect;
class CoopGame;
// Forward declare state classes (top-level, defined under src/states)
class LoadingState;
@ -109,6 +110,7 @@ private:
std::unique_ptr<ScoreManager> m_scoreManager;
// Gameplay pieces
std::unique_ptr<Game> m_game;
std::unique_ptr<CoopGame> m_coopGame;
std::unique_ptr<LineEffect> m_lineEffect;
// DAS/ARR movement timing (from original main.cpp)
@ -118,6 +120,14 @@ private:
static constexpr double DAS = 170.0; // Delayed Auto Shift
static constexpr double ARR = 40.0; // Auto Repeat Rate
// Coop DAS/ARR per player
bool m_p1LeftHeld = false;
bool m_p1RightHeld = false;
bool m_p2LeftHeld = false;
bool m_p2RightHeld = false;
double m_p1MoveTimerMs = 0.0;
double m_p2MoveTimerMs = 0.0;
// State context (must be a member to ensure lifetime)
StateContext m_stateContext;