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

@ -1,6 +1,7 @@
#include "PlayingState.h"
#include "../core/state/StateManager.h"
#include "../gameplay/core/Game.h"
#include "../gameplay/coop/CoopGame.h"
#include "../gameplay/effects/LineEffect.h"
#include "../persistence/Scores.h"
#include "../audio/Audio.h"
@ -18,12 +19,15 @@ PlayingState::PlayingState(StateContext& ctx) : State(ctx) {}
void PlayingState::onEnter() {
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "[PLAYING] Entering Playing state");
// Initialize the game based on mode: endless uses chosen start level, challenge keeps its run state
// Initialize the game based on mode: endless/cooperate use chosen start level, challenge keeps its run state
if (ctx.game) {
if (ctx.game->getMode() == GameMode::Endless) {
if (ctx.game->getMode() == GameMode::Endless || ctx.game->getMode() == GameMode::Cooperate) {
if (ctx.startLevelSelection) {
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "[PLAYING] Resetting game with level %d", *ctx.startLevelSelection);
ctx.game->reset(*ctx.startLevelSelection);
if (ctx.game->getMode() == GameMode::Cooperate && ctx.coopGame) {
ctx.coopGame->reset(*ctx.startLevelSelection);
}
}
} else {
// Challenge run is prepared before entering; ensure gameplay is unpaused
@ -45,124 +49,164 @@ void PlayingState::onExit() {
}
void PlayingState::handleEvent(const SDL_Event& e) {
if (!ctx.game) return;
// If a transport animation is active, ignore gameplay input entirely.
if (GameRenderer::isTransportActive()) {
return;
}
// We keep short-circuited input here; main still owns mouse UI
if (e.type == SDL_EVENT_KEY_DOWN && !e.key.repeat) {
if (!ctx.game) return;
auto setExitSelection = [&](int value) {
if (ctx.exitPopupSelectedButton) {
*ctx.exitPopupSelectedButton = value;
}
};
const bool coopActive = ctx.game->getMode() == GameMode::Cooperate && ctx.coopGame;
auto getExitSelection = [&]() -> int {
return ctx.exitPopupSelectedButton ? *ctx.exitPopupSelectedButton : 1;
};
auto setExitSelection = [&](int idx) {
if (ctx.exitPopupSelectedButton) {
*ctx.exitPopupSelectedButton = idx;
}
};
auto getExitSelection = [&]() -> int {
return ctx.exitPopupSelectedButton ? *ctx.exitPopupSelectedButton : 1;
};
// Pause toggle (P)
if (e.key.scancode == SDL_SCANCODE_P) {
bool paused = ctx.game->isPaused();
ctx.game->setPaused(!paused);
if (e.type != SDL_EVENT_KEY_DOWN || e.key.repeat) {
return;
}
// If exit-confirm popup is visible, handle shortcuts here
if (ctx.showExitConfirmPopup && *ctx.showExitConfirmPopup) {
// Navigate between YES (0) and NO (1) buttons
if (e.key.scancode == SDL_SCANCODE_LEFT || e.key.scancode == SDL_SCANCODE_UP) {
setExitSelection(0);
return;
}
if (e.key.scancode == SDL_SCANCODE_RIGHT || e.key.scancode == SDL_SCANCODE_DOWN) {
setExitSelection(1);
return;
}
// If exit-confirm popup is visible, handle shortcuts here
if (ctx.showExitConfirmPopup && *ctx.showExitConfirmPopup) {
// Navigate between YES (0) and NO (1) buttons
if (e.key.scancode == SDL_SCANCODE_LEFT || e.key.scancode == SDL_SCANCODE_UP) {
setExitSelection(0);
return;
}
if (e.key.scancode == SDL_SCANCODE_RIGHT || e.key.scancode == SDL_SCANCODE_DOWN) {
setExitSelection(1);
return;
}
// Activate selected button with Enter or Space
if (e.key.scancode == SDL_SCANCODE_RETURN || e.key.scancode == SDL_SCANCODE_KP_ENTER || e.key.scancode == SDL_SCANCODE_SPACE) {
const bool confirmExit = (getExitSelection() == 0);
*ctx.showExitConfirmPopup = false;
if (confirmExit) {
// YES - Reset game and return to menu
if (ctx.startLevelSelection) {
ctx.game->reset(*ctx.startLevelSelection);
} else {
ctx.game->reset(0);
}
ctx.game->setPaused(false);
if (ctx.stateManager) ctx.stateManager->setState(AppState::Menu);
// Activate selected button with Enter or Space
if (e.key.scancode == SDL_SCANCODE_RETURN || e.key.scancode == SDL_SCANCODE_KP_ENTER || e.key.scancode == SDL_SCANCODE_SPACE) {
const bool confirmExit = (getExitSelection() == 0);
*ctx.showExitConfirmPopup = false;
if (confirmExit) {
// YES - Reset game and return to menu
if (ctx.startLevelSelection) {
ctx.game->reset(*ctx.startLevelSelection);
} else {
// NO - Just close popup and resume
ctx.game->setPaused(false);
ctx.game->reset(0);
}
return;
}
// Cancel with Esc (same as NO)
if (e.key.scancode == SDL_SCANCODE_ESCAPE) {
*ctx.showExitConfirmPopup = false;
ctx.game->setPaused(false);
setExitSelection(1);
return;
if (ctx.stateManager) ctx.stateManager->setState(AppState::Menu);
} else {
// NO - Just close popup and resume
ctx.game->setPaused(false);
}
// While modal is open, suppress other gameplay keys
return;
}
// ESC key - open confirmation popup
// Cancel with Esc (same as NO)
if (e.key.scancode == SDL_SCANCODE_ESCAPE) {
if (ctx.showExitConfirmPopup) {
if (ctx.game) ctx.game->setPaused(true);
*ctx.showExitConfirmPopup = true;
setExitSelection(1); // Default to NO for safety
}
return;
}
// Debug: skip to next challenge level (B)
if (e.key.scancode == SDL_SCANCODE_B && ctx.game && ctx.game->getMode() == GameMode::Challenge) {
ctx.game->beginNextChallengeLevel();
// Cancel any countdown so play resumes immediately on the new level
if (ctx.gameplayCountdownActive) *ctx.gameplayCountdownActive = false;
if (ctx.menuPlayCountdownArmed) *ctx.menuPlayCountdownArmed = false;
*ctx.showExitConfirmPopup = false;
ctx.game->setPaused(false);
setExitSelection(1);
return;
}
// While modal is open, suppress other gameplay keys
return;
}
// ESC key - open confirmation popup
if (e.key.scancode == SDL_SCANCODE_ESCAPE) {
if (ctx.showExitConfirmPopup) {
ctx.game->setPaused(true);
*ctx.showExitConfirmPopup = true;
setExitSelection(1); // Default to NO for safety
}
return;
}
// Debug: skip to next challenge level (B)
if (e.key.scancode == SDL_SCANCODE_B && ctx.game->getMode() == GameMode::Challenge) {
ctx.game->beginNextChallengeLevel();
// Cancel any countdown so play resumes immediately on the new level
if (ctx.gameplayCountdownActive) *ctx.gameplayCountdownActive = false;
if (ctx.menuPlayCountdownArmed) *ctx.menuPlayCountdownArmed = false;
ctx.game->setPaused(false);
return;
}
// Tetris controls (only when not paused)
if (ctx.game->isPaused()) {
return;
}
if (coopActive && ctx.coopGame) {
// Player 1 (left): A/D move via DAS in ApplicationManager; here handle rotations/hold/hard-drop
if (e.key.scancode == SDL_SCANCODE_W) {
ctx.coopGame->rotate(CoopGame::PlayerSide::Left, 1);
return;
}
if (e.key.scancode == SDL_SCANCODE_Q) {
ctx.coopGame->rotate(CoopGame::PlayerSide::Left, -1);
return;
}
if (e.key.scancode == SDL_SCANCODE_LSHIFT) {
SoundEffectManager::instance().playSound("hard_drop", 0.7f);
ctx.coopGame->hardDrop(CoopGame::PlayerSide::Left);
return;
}
if (e.key.scancode == SDL_SCANCODE_LCTRL) {
ctx.coopGame->holdCurrent(CoopGame::PlayerSide::Left);
return;
}
// Tetris controls (only when not paused)
if (!ctx.game->isPaused()) {
// Hold / swap current piece (H)
if (e.key.scancode == SDL_SCANCODE_H) {
ctx.game->holdCurrent();
return;
}
// Player 2 (right): arrow keys move via DAS; rotations/hold/hard-drop here
if (e.key.scancode == SDL_SCANCODE_UP) {
bool upIsCW = Settings::instance().isUpRotateClockwise();
ctx.coopGame->rotate(CoopGame::PlayerSide::Right, upIsCW ? 1 : -1);
return;
}
if (e.key.scancode == SDL_SCANCODE_RALT) {
ctx.coopGame->rotate(CoopGame::PlayerSide::Right, -1);
return;
}
if (e.key.scancode == SDL_SCANCODE_RSHIFT) {
SoundEffectManager::instance().playSound("hard_drop", 0.7f);
ctx.coopGame->hardDrop(CoopGame::PlayerSide::Right);
return;
}
if (e.key.scancode == SDL_SCANCODE_RCTRL) {
ctx.coopGame->holdCurrent(CoopGame::PlayerSide::Right);
return;
}
} else {
// Single-player classic controls
// Hold / swap current piece (H)
if (e.key.scancode == SDL_SCANCODE_H) {
ctx.game->holdCurrent();
return;
}
// Rotation (still event-based for precise timing)
if (e.key.scancode == SDL_SCANCODE_UP) {
// Use user setting to determine whether UP rotates clockwise
bool upIsCW = Settings::instance().isUpRotateClockwise();
ctx.game->rotate(upIsCW ? 1 : -1);
return;
}
if (e.key.scancode == SDL_SCANCODE_X) {
// Toggle the mapping so UP will rotate in the opposite direction
bool current = Settings::instance().isUpRotateClockwise();
Settings::instance().setUpRotateClockwise(!current);
Settings::instance().save();
// Play a subtle feedback sound if available
SoundEffectManager::instance().playSound("menu_toggle", 0.6f);
return;
}
// Rotation (still event-based for precise timing)
if (e.key.scancode == SDL_SCANCODE_UP) {
// Use user setting to determine whether UP rotates clockwise
bool upIsCW = Settings::instance().isUpRotateClockwise();
ctx.game->rotate(upIsCW ? 1 : -1);
return;
}
if (e.key.scancode == SDL_SCANCODE_X) {
// Toggle the mapping so UP will rotate in the opposite direction
bool current = Settings::instance().isUpRotateClockwise();
Settings::instance().setUpRotateClockwise(!current);
Settings::instance().save();
// Play a subtle feedback sound if available
SoundEffectManager::instance().playSound("menu_toggle", 0.6f);
return;
}
// Hard drop (space)
if (e.key.scancode == SDL_SCANCODE_SPACE) {
SoundEffectManager::instance().playSound("hard_drop", 0.7f);
ctx.game->hardDrop();
return;
}
// Hard drop (space)
if (e.key.scancode == SDL_SCANCODE_SPACE) {
SoundEffectManager::instance().playSound("hard_drop", 0.7f);
ctx.game->hardDrop();
return;
}
}
@ -172,7 +216,21 @@ void PlayingState::handleEvent(const SDL_Event& e) {
void PlayingState::update(double frameMs) {
if (!ctx.game) return;
const bool coopActive = ctx.game->getMode() == GameMode::Cooperate && ctx.coopGame;
if (coopActive) {
// Visual effects only; gravity and movement handled from ApplicationManager for coop
ctx.coopGame->updateVisualEffects(frameMs);
// Update line clear effect for coop mode as well (renderer starts the effect)
if (ctx.lineEffect && ctx.lineEffect->isActive()) {
if (ctx.lineEffect->update(frameMs / 1000.0f)) {
ctx.coopGame->clearCompletedLines();
}
}
return;
}
ctx.game->updateVisualEffects(frameMs);
// If a transport animation is active, pause gameplay updates and ignore inputs
if (GameRenderer::isTransportActive()) {
@ -204,6 +262,8 @@ void PlayingState::update(double frameMs) {
void PlayingState::render(SDL_Renderer* renderer, float logicalScale, SDL_Rect logicalVP) {
if (!ctx.game) return;
const bool coopActive = ctx.game->getMode() == GameMode::Cooperate && ctx.coopGame;
// Get current window size
int winW = 0, winH = 0;
SDL_GetRenderOutputSize(renderer, &winW, &winH);
@ -244,26 +304,44 @@ void PlayingState::render(SDL_Renderer* renderer, float logicalScale, SDL_Rect l
// Render game content (no overlays)
// If a transport effect was requested due to a recent spawn, start it here so
// the renderer has the correct layout and renderer context to compute coords.
if (s_pendingTransport) {
if (!coopActive && s_pendingTransport) {
GameRenderer::startTransportEffectForGame(ctx.game, ctx.blocksTex, 1200.0f, 1000.0f, logicalScale, (float)winW, (float)winH, 0.4f);
s_pendingTransport = false;
}
GameRenderer::renderPlayingState(
renderer,
ctx.game,
ctx.pixelFont,
ctx.lineEffect,
ctx.blocksTex,
ctx.asteroidsTex,
ctx.statisticsPanelTex,
ctx.scorePanelTex,
ctx.nextPanelTex,
ctx.holdPanelTex,
countdown,
1200.0f, // LOGICAL_W
1000.0f, // LOGICAL_H
logicalScale,
if (coopActive && ctx.coopGame) {
GameRenderer::renderCoopPlayingState(
renderer,
ctx.coopGame,
ctx.pixelFont,
ctx.lineEffect,
ctx.blocksTex,
ctx.statisticsPanelTex,
ctx.scorePanelTex,
ctx.nextPanelTex,
ctx.holdPanelTex,
1200.0f,
1000.0f,
logicalScale,
(float)winW,
(float)winH
);
} else {
GameRenderer::renderPlayingState(
renderer,
ctx.game,
ctx.pixelFont,
ctx.lineEffect,
ctx.blocksTex,
ctx.asteroidsTex,
ctx.statisticsPanelTex,
ctx.scorePanelTex,
ctx.nextPanelTex,
ctx.holdPanelTex,
countdown,
1200.0f, // LOGICAL_W
1000.0f, // LOGICAL_H
logicalScale,
(float)winW,
(float)winH,
challengeClearFx,
@ -272,7 +350,8 @@ void PlayingState::render(SDL_Renderer* renderer, float logicalScale, SDL_Rect l
challengeClearDuration,
countdown ? nullptr : ctx.challengeStoryText,
countdown ? 0.0f : (ctx.challengeStoryAlpha ? *ctx.challengeStoryAlpha : 0.0f)
);
);
}
// Reset to screen
SDL_SetRenderTarget(renderer, nullptr);
@ -341,33 +420,53 @@ void PlayingState::render(SDL_Renderer* renderer, float logicalScale, SDL_Rect l
} else {
// Render normally directly to screen
if (s_pendingTransport) {
if (!coopActive && s_pendingTransport) {
GameRenderer::startTransportEffectForGame(ctx.game, ctx.blocksTex, 1200.0f, 1000.0f, logicalScale, (float)winW, (float)winH, 0.4f);
s_pendingTransport = false;
}
GameRenderer::renderPlayingState(
renderer,
ctx.game,
ctx.pixelFont,
ctx.lineEffect,
ctx.blocksTex,
ctx.asteroidsTex,
ctx.statisticsPanelTex,
ctx.scorePanelTex,
ctx.nextPanelTex,
ctx.holdPanelTex,
countdown,
1200.0f,
1000.0f,
logicalScale,
(float)winW,
(float)winH,
challengeClearFx,
challengeClearOrder,
challengeClearElapsed,
challengeClearDuration,
countdown ? nullptr : ctx.challengeStoryText,
countdown ? 0.0f : (ctx.challengeStoryAlpha ? *ctx.challengeStoryAlpha : 0.0f)
);
if (coopActive && ctx.coopGame) {
GameRenderer::renderCoopPlayingState(
renderer,
ctx.coopGame,
ctx.pixelFont,
ctx.lineEffect,
ctx.blocksTex,
ctx.statisticsPanelTex,
ctx.scorePanelTex,
ctx.nextPanelTex,
ctx.holdPanelTex,
1200.0f,
1000.0f,
logicalScale,
(float)winW,
(float)winH
);
} else {
GameRenderer::renderPlayingState(
renderer,
ctx.game,
ctx.pixelFont,
ctx.lineEffect,
ctx.blocksTex,
ctx.asteroidsTex,
ctx.statisticsPanelTex,
ctx.scorePanelTex,
ctx.nextPanelTex,
ctx.holdPanelTex,
countdown,
1200.0f,
1000.0f,
logicalScale,
(float)winW,
(float)winH,
challengeClearFx,
challengeClearOrder,
challengeClearElapsed,
challengeClearDuration,
countdown ? nullptr : ctx.challengeStoryText,
countdown ? 0.0f : (ctx.challengeStoryAlpha ? *ctx.challengeStoryAlpha : 0.0f)
);
}
}
}