diff --git a/crates/game-logic/src/engine/config.rs b/crates/game-logic/src/engine/config.rs index a72a4e9..be6fa84 100644 --- a/crates/game-logic/src/engine/config.rs +++ b/crates/game-logic/src/engine/config.rs @@ -1,41 +1,43 @@ -// ─── 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; +pub const MULTIBALL_SCORE: u32 = 5_000; -// ─── 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; -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; @@ -78,7 +80,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 +97,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..0873bcb 100644 --- a/crates/game-logic/src/engine/core.rs +++ b/crates/game-logic/src/engine/core.rs @@ -81,6 +81,14 @@ 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 +102,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; @@ -126,6 +135,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); @@ -159,7 +169,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 } => { @@ -193,8 +207,14 @@ 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 } => { @@ -206,6 +226,21 @@ 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()); } @@ -237,7 +272,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 => { @@ -261,6 +301,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 +339,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 +397,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..7fb3576 100644 --- a/crates/game-logic/src/engine/states.rs +++ b/crates/game-logic/src/engine/states.rs @@ -39,11 +39,13 @@ 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, 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, @@ -58,11 +60,13 @@ impl GameState { Self { phase: GamePhase::Idle, score: 0, + player_id: String::new(), lives, active_multiplier: 1.0, 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,