fixed for cooperate mode

This commit is contained in:
2025-12-22 13:48:54 +01:00
parent 694243ac89
commit 18463774e9
4 changed files with 311 additions and 1 deletions

293
Spacetris-COOPERATE Mode.md Normal file
View File

@ -0,0 +1,293 @@
# Spacetris — COOPERATE Mode
## Middle SYNC Line Effect (SDL3)
### VS Code Copilot AI Agent Integration Guide
> Goal: Implement a **visual SYNC divider line** in COOPERATE mode that communicates cooperation state between two players.
> This effect must be lightweight, performant, and decoupled from gameplay logic.
---
## 1. Concept Overview
The **SYNC Line** is the vertical divider between the two halves of the COOPERATE grid.
It must:
- Always be visible
- React when one side is ready
- Pulse when both sides are ready
- Flash on line clear
- Provide instant, text-free feedback
No shaders. No textures. SDL3 only.
---
## 2. Visual States
Define these states:
```cpp
enum class SyncState {
Idle, // no side ready
LeftReady, // left half complete
RightReady, // right half complete
Synced, // both halves complete
ClearFlash // row cleared
};
````
---
## 3. Color Language
| State | Color | Meaning |
| ------------------ | --------------- | ------------------- |
| Idle | Blue | Neutral |
| Left / Right Ready | Yellow | Waiting for partner |
| Synced | Green (pulsing) | Perfect cooperation |
| ClearFlash | White | Successful clear |
---
## 4. Geometry
The SYNC line is a simple rectangle:
```cpp
SDL_FRect syncLine;
syncLine.x = gridCenterX - 2;
syncLine.y = gridTopY;
syncLine.w = 4;
syncLine.h = gridHeight;
```
---
## 5. Rendering Rules
* Always render every frame
* Use alpha blending
* Pulse alpha when synced
* Flash briefly on clear
---
## 6. SyncLineRenderer Class (Required)
### Header
```cpp
#pragma once
#include <SDL3/SDL.h>
enum class SyncState {
Idle,
LeftReady,
RightReady,
Synced,
ClearFlash
};
class SyncLineRenderer {
public:
SyncLineRenderer();
void SetRect(const SDL_FRect& rect);
void SetState(SyncState state);
void TriggerClearFlash();
void Update(float deltaTime);
void Render(SDL_Renderer* renderer);
private:
SDL_FRect m_rect;
SyncState m_state;
float m_flashTimer;
float m_time;
static constexpr float FLASH_DURATION = 0.15f;
SDL_Color GetBaseColor() const;
};
```
---
### Implementation
```cpp
#include "SyncLineRenderer.h"
#include <cmath>
SyncLineRenderer::SyncLineRenderer()
: m_state(SyncState::Idle),
m_flashTimer(0.0f),
m_time(0.0f) {}
void SyncLineRenderer::SetRect(const SDL_FRect& rect) {
m_rect = rect;
}
void SyncLineRenderer::SetState(SyncState state) {
if (state != SyncState::ClearFlash)
m_state = state;
}
void SyncLineRenderer::TriggerClearFlash() {
m_state = SyncState::ClearFlash;
m_flashTimer = FLASH_DURATION;
}
void SyncLineRenderer::Update(float deltaTime) {
m_time += deltaTime;
if (m_state == SyncState::ClearFlash) {
m_flashTimer -= deltaTime;
if (m_flashTimer <= 0.0f) {
m_state = SyncState::Idle;
m_flashTimer = 0.0f;
}
}
}
SDL_Color SyncLineRenderer::GetBaseColor() const {
switch (m_state) {
case SyncState::LeftReady:
case SyncState::RightReady:
return {255, 220, 100, 200}; // yellow
case SyncState::Synced:
return {100, 255, 120, 220}; // green
case SyncState::ClearFlash:
return {255, 255, 255, 255}; // white
default:
return {80, 180, 255, 180}; // idle blue
}
}
void SyncLineRenderer::Render(SDL_Renderer* renderer) {
SDL_SetRenderDrawBlendMode(renderer, SDL_BLENDMODE_BLEND);
SDL_Color color = GetBaseColor();
if (m_state == SyncState::Synced) {
float pulse = 0.5f + 0.5f * std::sinf(m_time * 6.0f);
color.a = static_cast<Uint8>(160 + pulse * 80);
}
SDL_SetRenderDrawColor(renderer, color.r, color.g, color.b, color.a);
SDL_RenderFillRect(renderer, &m_rect);
if (m_state == SyncState::ClearFlash) {
SDL_FRect glow = m_rect;
glow.x -= 3;
glow.w += 6;
SDL_SetRenderDrawColor(renderer, 255, 255, 255, 180);
SDL_RenderFillRect(renderer, &glow);
}
}
```
---
## 7. Integration Flow
### Initialization
```cpp
SyncLineRenderer syncLine;
syncLine.SetRect({
gridCenterX - 2.0f,
gridTopY,
4.0f,
gridHeight
});
```
---
### Update Loop
```cpp
syncLine.Update(deltaTime);
```
---
### Game Logic → Sync State Mapping
```cpp
if (leftRowReady && rightRowReady)
syncLine.SetState(SyncState::Synced);
else if (leftRowReady)
syncLine.SetState(SyncState::LeftReady);
else if (rightRowReady)
syncLine.SetState(SyncState::RightReady);
else
syncLine.SetState(SyncState::Idle);
```
---
### On Line Clear Event
```cpp
syncLine.TriggerClearFlash();
```
---
### Render Loop
```cpp
syncLine.Render(renderer);
```
---
## 8. Performance Requirements
* ≤ 2 draw calls per frame
* No textures
* No dynamic allocations
* No blocking logic
* Suitable for 60240 FPS
---
## 9. UX Rationale
* Color communicates state without text
* Pulse indicates readiness
* Flash provides satisfaction on success
* Reinforces cooperation visually
---
## 10. Acceptance Criteria
* Divider is always visible
* State changes are instantly readable
* Flash triggers exactly on cooperative line clear
* No performance impact
* Clean separation from gameplay logic
---
## 11. Optional Future Enhancements (Not Required)
* Vertical energy particles
* Sound hooks on SYNC / CLEAR
* Combo-based intensity
* Color-blind palette
* Gradient or glow variants
---
## Summary for Copilot
Implement a `SyncLineRenderer` class in SDL3 to render the cooperative divider line. The line reflects cooperation state through color, pulse, and flash effects. The renderer must be lightweight, stateless with gameplay, and fully driven by external state updates.

View File

@ -494,6 +494,17 @@ int TetrisApp::Impl::init()
suppressLineVoiceForLevelUp = true;
});
// Mirror single-player level-up audio/visual behavior for Coop sessions
coopGame->setLevelUpCallback([this](int /*newLevel*/) {
if (skipNextLevelUpJingle) {
skipNextLevelUpJingle = false;
} else {
SoundEffectManager::instance().playSound("new_level", 1.0f);
SoundEffectManager::instance().playSound("lets_go", 1.0f);
}
suppressLineVoiceForLevelUp = true;
});
game->setAsteroidDestroyedCallback([](AsteroidType /*type*/) {
SoundEffectManager::instance().playSound("asteroid_destroy", 0.9f);
});

View File

@ -416,7 +416,9 @@ void CoopGame::applyLineClearRewards(PlayerState& creditPlayer, int cleared) {
}
_lines += cleared;
creditPlayer.lines += cleared;
// Credit both players with the cleared lines so cooperative play counts for both
left.lines += cleared;
right.lines += cleared;
_currentCombo += 1;
if (_currentCombo > _maxCombo) _maxCombo = _currentCombo;
@ -445,6 +447,7 @@ void CoopGame::applyLineClearRewards(PlayerState& creditPlayer, int cleared) {
if (targetLevel > _level) {
_level = targetLevel;
gravityMs = gravityMsForLevel(_level);
if (levelUpCallback) levelUpCallback(_level);
}
// Per-player level progression mirrors the shared rules but is driven by

View File

@ -57,7 +57,9 @@ public:
explicit CoopGame(int startLevel = 0);
using SoundCallback = std::function<void(int)>;
using LevelUpCallback = std::function<void(int)>;
void setSoundCallback(SoundCallback cb) { soundCallback = cb; }
void setLevelUpCallback(LevelUpCallback cb) { levelUpCallback = cb; }
void reset(int startLevel = 0);
void tickGravity(double frameMs);
@ -137,6 +139,7 @@ private:
uint32_t hardDropFxId{0};
uint64_t pieceSequence{0};
SoundCallback soundCallback;
LevelUpCallback levelUpCallback;
// Helpers ---------------------------------------------------------------
PlayerState& player(PlayerSide s) { return s == PlayerSide::Left ? left : right; }