From ac18926e2194891e2d7aa197983805f846524f87 Mon Sep 17 00:00:00 2001 From: Jeck0v Date: Sat, 30 May 2026 15:03:05 +0200 Subject: [PATCH 1/4] feat(ws-score): front event routing + ScoreDelta broadcast --- crates/game-logic/src/engine/config.rs | 21 +++++++------- crates/game-logic/src/engine/core.rs | 38 +++++++++++++++++++++++++- crates/game-logic/src/engine/events.rs | 1 + crates/game-logic/src/engine/states.rs | 2 ++ 4 files changed, 51 insertions(+), 11 deletions(-) diff --git a/crates/game-logic/src/engine/config.rs b/crates/game-logic/src/engine/config.rs index a72a4e9..91f60d9 100644 --- a/crates/game-logic/src/engine/config.rs +++ b/crates/game-logic/src/engine/config.rs @@ -1,37 +1,38 @@ -// ─── Core game settings ─────────────────────────────────────────────────────── +//Core game settings pub const DEFAULT_LIVES: u8 = 3; pub const ULTIME_CHARGE_RATIO: u32 = 100; +pub const BALL_SAVER_SCORE: u32 = 500; -// ─── Bumper scoring ─────────────────────────────────────────────────────────── +// Bumper scoring pub const BUMPER_SCORE: u32 = 100; pub const BUMPER_TRIANGLE_SCORE: u32 = 200; pub const PORTAL_SCORE: u32 = 300; -// ─── Multiball ──────────────────────────────────────────────────────────────── +// Multiball pub const MULTIBALL_RING_THRESHOLD: u32 = 10; -// ─── Timer bonus (BonusGameTimerMultiplier) ─────────────────────────────────── +// Timer bonus (BonusGameTimerMultiplier) pub const TIMER_BONUS_SECONDS: u64 = 60; pub const TIMER_BONUS_SCORE: u32 = 500; pub const TIMER_BONUS_MULTIPLIER: f32 = 1.5; -// ─── Tilt penalties ─────────────────────────────────────────────────────────── +// Tilt penalties pub const TILT_PENALTY_1: i64 = -2_000; pub const TILT_PENALTY_2: i64 = -6_000; -// ─── Boss HP ────────────────────────────────────────────────────────────────── +// Boss HP pub const BOSS_0_HP: u32 = 500; pub const BOSS_1_HP: u32 = 800; pub const BOSS_2_HP: u32 = 1_200; -// ─── Boss difficulty scaling ────────────────────────────────────────────────── +// Boss difficulty scaling pub const BOSS_0_DIFFICULTY_SCALE: f32 = 1.0; pub const BOSS_1_DIFFICULTY_SCALE: f32 = 1.6; pub const BOSS_2_DIFFICULTY_SCALE: f32 = 2.4; pub const ENDLESS_BASE_DIFFICULTY_SCALE: f32 = 2.4; pub const ENDLESS_LEVEL_SCALE_EXPONENT: f32 = 1.3; -// ─── Combo system ───────────────────────────────────────────────────────────── +// Combo system pub const COMBO_BUFFER_MAX: usize = 10; pub const COMBO_DETECTION_WINDOW_MS: u64 = 3_000; pub const COMBO_PENALTY_REPEAT: usize = 7; @@ -78,7 +79,7 @@ pub const COMBO_13_BONUS: u32 = 0; pub const COMBO_13_MULTIPLIER: f32 = 1.5; pub const COMBO_13_DURATION_MS: u64 = 500; -// ─── Character stats ────────────────────────────────────────────────────────── +// Character stats pub const ROBOCP_ULTIMATE_MAX: u32 = 500; pub const ROBOCP_BONUS_COOLDOWN_MS: u64 = 30_000; pub const ROBOCP_MALUS_COOLDOWN_MS: u64 = 45_000; @@ -95,7 +96,7 @@ pub const CYBORG_ULTIMATE_MAX: u32 = 450; pub const CYBORG_BONUS_COOLDOWN_MS: u64 = 28_000; pub const CYBORG_MALUS_COOLDOWN_MS: u64 = 40_000; -// ─── Skill effects ──────────────────────────────────────────────────────────── +// Skill effects pub const SKILL_SHIELD_DURATION_MS: u64 = 8_000; pub const SKILL_DAMAGE_BOOST_MULTIPLIER: f32 = 2.0; diff --git a/crates/game-logic/src/engine/core.rs b/crates/game-logic/src/engine/core.rs index 5ae6f83..54f67ee 100644 --- a/crates/game-logic/src/engine/core.rs +++ b/crates/game-logic/src/engine/core.rs @@ -81,6 +81,10 @@ impl GameEngine { "BumperTriangle" => GameEvent::BumperTriangleHit { pts: crate::engine::config::BUMPER_TRIANGLE_SCORE, }, + "PortalUsed" => GameEvent::PortalUsed, + "FlipperLeft" => GameEvent::ButtonPressed { side: ButtonSide::Left }, + "FlipperRight" => GameEvent::ButtonPressed { side: ButtonSide::Right }, + "BallSaverReady" => GameEvent::BallSaverReady, unknown => { tracing::debug!(event_type = unknown, "unhandled screen event type"); return vec![]; @@ -94,8 +98,9 @@ impl GameEngine { let mut envelopes = Vec::new(); match event { - GameEvent::StartGame { .. } => { + GameEvent::StartGame { ref player_id } => { self.state = GameState::new(DEFAULT_LIVES); + self.state.player_id = player_id.clone(); self.state.phase = GamePhase::InGame; self.state.session_start = Some(now); self.timer_bonus_given = false; @@ -194,6 +199,7 @@ impl GameEngine { } envelopes.extend(self.check_timer_bonus(now)); + envelopes.push(self.emit_score_delta(scored, "bumper")); envelopes.push(self.emit_score_update()); } @@ -206,6 +212,18 @@ impl GameEngine { GameEvent::PortalUsed => { let pts = crate::engine::scoring::score_portal_bonus(); self.state.add_score(pts); + envelopes.push(self.emit_score_delta(pts, "portal")); + envelopes.push(self.emit_score_update()); + } + + GameEvent::BallSaverReady => { + if self.state.phase != GamePhase::InGame { + return envelopes; + } + let pts = crate::engine::config::BALL_SAVER_SCORE as u64; + self.state.add_score(pts); + envelopes.push(self.emit_score_delta(pts, "ball_saver")); + envelopes.push(make_event_envelope("BallSaverReady", serde_json::Value::Null)); envelopes.push(self.emit_score_update()); } @@ -261,6 +279,7 @@ impl GameEngine { self.multiplier.apply(&effect, now); self.state.add_score(effect.bonus_pts as u64); envelopes.push(self.emit_combo_activated(&effect)); + envelopes.push(self.emit_score_delta(effect.bonus_pts as u64, "combo")); envelopes.push(self.emit_score_update()); } @@ -298,12 +317,15 @@ impl GameEngine { && self.state.balls_lost_since_start == 0 { self.timer_bonus_given = true; + let old_score = self.state.score; self.state.score = timer_bonus(self.state.score, 0); + let delta = self.state.score.saturating_sub(old_score); return vec![ make_event_envelope( "TimerBonus", serde_json::json!({ "new_score": self.state.score }), ), + self.emit_score_delta(delta, "timer_bonus"), self.emit_score_update(), ]; } @@ -353,11 +375,25 @@ impl GameEngine { fn emit_score_update(&self) -> ScreenEnvelope { let current_multiplier = self.multiplier.current(Instant::now()); + let ball = self.state.balls_lost_since_start + 1; make_event_envelope( "ScoreUpdate", serde_json::json!({ "score": self.state.score, "multiplier": current_multiplier, + "player": self.state.player_id, + "ball": ball, + }), + ) + } + + fn emit_score_delta(&self, delta: u64, reason: &str) -> ScreenEnvelope { + make_event_envelope( + "ScoreDelta", + serde_json::json!({ + "delta": delta, + "reason": reason, + "total": self.state.score, }), ) } diff --git a/crates/game-logic/src/engine/events.rs b/crates/game-logic/src/engine/events.rs index 4f6dd4b..3cecb29 100644 --- a/crates/game-logic/src/engine/events.rs +++ b/crates/game-logic/src/engine/events.rs @@ -45,6 +45,7 @@ pub enum GameEvent { BumperTriangleHit { pts: u32 }, BumperCombo { count: u32 }, PortalUsed, + BallSaverReady, TiltDetected, LifeUp, MultiballWin, diff --git a/crates/game-logic/src/engine/states.rs b/crates/game-logic/src/engine/states.rs index 3b909cb..c9e06e0 100644 --- a/crates/game-logic/src/engine/states.rs +++ b/crates/game-logic/src/engine/states.rs @@ -39,6 +39,7 @@ impl TiltState { pub struct GameState { pub phase: GamePhase, pub score: u64, + pub player_id: String, pub lives: u8, pub active_multiplier: f32, pub multiplier_expires_at: Option, @@ -58,6 +59,7 @@ impl GameState { Self { phase: GamePhase::Idle, score: 0, + player_id: String::new(), lives, active_multiplier: 1.0, multiplier_expires_at: None, From ac0e544a494482a55f0af5fe5b10ac53e653228c Mon Sep 17 00:00:00 2001 From: Jeck0v Date: Sat, 30 May 2026 15:53:27 +0200 Subject: [PATCH 2/4] hotfix: multiball gameplay --- crates/game-logic/src/engine/config.rs | 1 + crates/game-logic/src/engine/core.rs | 9 +++++++++ crates/game-logic/src/engine/states.rs | 2 ++ 3 files changed, 12 insertions(+) diff --git a/crates/game-logic/src/engine/config.rs b/crates/game-logic/src/engine/config.rs index 91f60d9..23fb08b 100644 --- a/crates/game-logic/src/engine/config.rs +++ b/crates/game-logic/src/engine/config.rs @@ -10,6 +10,7 @@ pub const PORTAL_SCORE: u32 = 300; // Multiball pub const MULTIBALL_RING_THRESHOLD: u32 = 10; +pub const MULTIBALL_SCORE: u32 = 5_000; // Timer bonus (BonusGameTimerMultiplier) pub const TIMER_BONUS_SECONDS: u64 = 60; diff --git a/crates/game-logic/src/engine/core.rs b/crates/game-logic/src/engine/core.rs index 54f67ee..7022e5c 100644 --- a/crates/game-logic/src/engine/core.rs +++ b/crates/game-logic/src/engine/core.rs @@ -131,6 +131,7 @@ impl GameEngine { return envelopes; } self.state.balls_lost_since_start += 1; + self.state.bumper_hit_count = 0; self.state.lives = self.state.lives.saturating_sub(1); let (pve_env, extra) = self.pve_engine.on_event(&event, &mut self.state); @@ -198,9 +199,12 @@ impl GameEngine { envelopes.extend(self.process(e)); } + self.state.bumper_hit_count += 1; + let bumper_count = self.state.bumper_hit_count; envelopes.extend(self.check_timer_bonus(now)); envelopes.push(self.emit_score_delta(scored, "bumper")); envelopes.push(self.emit_score_update()); + envelopes.extend(self.process(GameEvent::BumperCombo { count: bumper_count })); } GameEvent::BumperCombo { count } => { @@ -255,7 +259,12 @@ impl GameEngine { } GameEvent::MultiballWin => { + self.state.bumper_hit_count = 0; + let pts = crate::engine::config::MULTIBALL_SCORE as u64; + self.state.add_score(pts); + envelopes.push(self.emit_score_delta(pts, "multiball")); envelopes.push(make_event_envelope("MultiballWin", serde_json::Value::Null)); + envelopes.push(self.emit_score_update()); } GameEvent::ScoreMultiplierActivated => { diff --git a/crates/game-logic/src/engine/states.rs b/crates/game-logic/src/engine/states.rs index c9e06e0..7fb3576 100644 --- a/crates/game-logic/src/engine/states.rs +++ b/crates/game-logic/src/engine/states.rs @@ -45,6 +45,7 @@ pub struct GameState { pub multiplier_expires_at: Option, pub tilt_state: TiltState, pub balls_lost_since_start: u32, + pub bumper_hit_count: u32, pub session_start: Option, pub cheating_detected: bool, pub extra_balls: u8, @@ -65,6 +66,7 @@ impl GameState { multiplier_expires_at: None, tilt_state: TiltState::default(), balls_lost_since_start: 0, + bumper_hit_count: 0, session_start: None, cheating_detected: false, extra_balls: 0, From c1d21aab70533d259466e3d5e8ac4be518ae1493 Mon Sep 17 00:00:00 2001 From: Jeck0v Date: Sat, 30 May 2026 20:24:27 +0200 Subject: [PATCH 3/4] Fix: remove minus point for spamming --- crates/game-logic/src/engine/config.rs | 2 +- crates/game-logic/src/engine/core.rs | 6 +++++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/crates/game-logic/src/engine/config.rs b/crates/game-logic/src/engine/config.rs index 23fb08b..be6fa84 100644 --- a/crates/game-logic/src/engine/config.rs +++ b/crates/game-logic/src/engine/config.rs @@ -37,7 +37,7 @@ pub const ENDLESS_LEVEL_SCALE_EXPONENT: f32 = 1.3; pub const COMBO_BUFFER_MAX: usize = 10; pub const COMBO_DETECTION_WINDOW_MS: u64 = 3_000; pub const COMBO_PENALTY_REPEAT: usize = 7; -pub const COMBO_PENALTY_PTS: i64 = -2_000; +pub const COMBO_PENALTY_PTS: i64 = 2_000; // Combo stats: (bonus_pts, multiplier, duration_ms) pub const COMBO_1_BONUS: u32 = 0; diff --git a/crates/game-logic/src/engine/core.rs b/crates/game-logic/src/engine/core.rs index 7022e5c..c7f29a2 100644 --- a/crates/game-logic/src/engine/core.rs +++ b/crates/game-logic/src/engine/core.rs @@ -165,7 +165,11 @@ impl GameEngine { envelopes.extend(self.process(GameEvent::ComboActivated(effect))); } ComboResult::Penalty { pts } => { - self.state.score = apply_tilt_penalty(self.state.score, pts); + if pts < 0 { + self.state.score = apply_tilt_penalty(self.state.score, pts); + } else { + self.state.score = self.state.score.saturating_add(pts as u64); + } envelopes.push(self.emit_score_update()); } ComboResult::BadgeUnlocked { badge_id } => { From e14b25f00c0a42ae3fbd4fb62d3020893fff0276 Mon Sep 17 00:00:00 2001 From: Jeck0v Date: Sat, 30 May 2026 23:11:43 +0200 Subject: [PATCH 4/4] Hotfix: formating ball saver in core.rs --- crates/game-logic/src/engine/core.rs | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/crates/game-logic/src/engine/core.rs b/crates/game-logic/src/engine/core.rs index c7f29a2..0873bcb 100644 --- a/crates/game-logic/src/engine/core.rs +++ b/crates/game-logic/src/engine/core.rs @@ -82,8 +82,12 @@ impl GameEngine { pts: crate::engine::config::BUMPER_TRIANGLE_SCORE, }, "PortalUsed" => GameEvent::PortalUsed, - "FlipperLeft" => GameEvent::ButtonPressed { side: ButtonSide::Left }, - "FlipperRight" => GameEvent::ButtonPressed { side: ButtonSide::Right }, + "FlipperLeft" => GameEvent::ButtonPressed { + side: ButtonSide::Left, + }, + "FlipperRight" => GameEvent::ButtonPressed { + side: ButtonSide::Right, + }, "BallSaverReady" => GameEvent::BallSaverReady, unknown => { tracing::debug!(event_type = unknown, "unhandled screen event type"); @@ -208,7 +212,9 @@ impl GameEngine { envelopes.extend(self.check_timer_bonus(now)); envelopes.push(self.emit_score_delta(scored, "bumper")); envelopes.push(self.emit_score_update()); - envelopes.extend(self.process(GameEvent::BumperCombo { count: bumper_count })); + envelopes.extend(self.process(GameEvent::BumperCombo { + count: bumper_count, + })); } GameEvent::BumperCombo { count } => { @@ -231,7 +237,10 @@ impl GameEngine { let pts = crate::engine::config::BALL_SAVER_SCORE as u64; self.state.add_score(pts); envelopes.push(self.emit_score_delta(pts, "ball_saver")); - envelopes.push(make_event_envelope("BallSaverReady", serde_json::Value::Null)); + envelopes.push(make_event_envelope( + "BallSaverReady", + serde_json::Value::Null, + )); envelopes.push(self.emit_score_update()); }