diff --git a/AKO_HRAT.md b/AKO_HRAT.md index c47f795..16ef0fd 100644 --- a/AKO_HRAT.md +++ b/AKO_HRAT.md @@ -1,77 +1,89 @@ -# Ako Hrat PokerRogue (Prototyp) +# Ako hrat PokerRogue (prototyp) -PokerRogue je singleplayer hra, kde sa boj riesi pokerom. -Kazde kolo boja je jedna poker handa. +PokerRogue je singleplayer poker-combat hra. Kazda handa je jeden suboj. ## Ciel - Znizit HP nepriatela na 0 skor, ako nepriatel znizi tvoje HP na 0. -- Boj trva obmedzeny pocet hand (`Hand X/Y` hore na obrazovke). +- Boj ma limit hand (`Hand X/Y` v hornom paneli). -## Zakladny priebeh +## Priebeh handy (aktualna logika) -1. Zobrazi sa tvoja ruka (2 karty), board (5 kariet) a zamer nepriatela. -2. Vyberies akciu (`1`, `2`, `3`). -3. Vyhodnoti sa handa: -- kto ma silnejsiu kombinaciu, vyhrava handu, -- podla Pot/Edge/Shield sa vypocita damage. -4. Na 5 sekund sa ukaze showdown: -- nepriatelove karty, -- zlate obrysy vitaznej 5-kartovej kombinacie. -5. Zacne dalsia handa. +1. Dostanes 2 karty, nepriatel ma 2 karty, na stole je board 5 kariet. +2. Rozhodujes sa po streetoch: +- `Flop` (3 karty), +- `Turn` (4 karty), +- `River` (5 kariet). +3. Na kazdom kroku vyberas akciu (`1` az `7`), okrem `Fold`, ktory handu okamzite ukonci. +4. Po River sa z troch vybranych akcii vypocita finalna bojova akcia. +5. Vyhodnoti sa showdown (najlepsich 5 zo 7 kariet), aplikuje sa damage. +6. Vysledok ostane na obrazovke, pokracujes az po `Space`/`Enter`. ## Ovládanie -- `1` = `Strike` (agresivne) -- `2` = `Guard` (defenzivne) -- `3` = `Dodge` (znizenie rizika) -- `R` = restart boja -- `Esc` = ukoncit hru +- `1` = `Strike` +- `2` = `Guard` +- `3` = `Dodge` +- `4` = `Check` +- `5` = `Probe` +- `6` = `All-In` +- `7` = `Fold` (okamzite skonci handu) +- `Space` / `Enter` = pokracovat po showdowne +- `R` = novy run +- `Esc` = koniec hry ## Co robia akcie -### Strike (`1`) +- `Strike`: `Pot +3`, `PlayerEdge +10` +- `Guard`: `Shield +6`, `PlayerEdge +5` +- `Dodge`: `Pot -2` (minimum 1), `Shield +2` +- `Check`: `Shield +1`, `PlayerEdge +2` +- `Probe`: `Pot +1`, `PlayerEdge +6` +- `All-In`: `Pot +8`, `PlayerEdge +18`, `EnemyEdge +8` +- `Fold`: okamzita prehra handy, ziskavas vacsi `Shield`, damage je znizeny oproti beznej prehre -- zvysi `Pot` -- zvysi tvoj `Edge` -- vhodne, ked chces tlacit na vysoky damage +## Edge a Shield (dolezite) -### Guard (`2`) +### Edge -- prida `Shield` -- mierne zvysi tvoj `Edge` -- vhodne, ked cakas silny utok nepriatela +`Edge` je percent bonus k damage: -### Dodge (`3`) +- ak vyhras handu ty: `EnemyDamage = Pot * (100 + PlayerEdge) / 100` +- ak vyhra nepriatel: `RawPlayerDamage = Pot * (100 + EnemyEdge) / 100` -- znizi `Pot` (mensie riziko aj odmena) -- da mensi `Shield` -- vhodne na stabilizaciu pri nevyhodnej hande +Cim vyssi `Edge`, tym vacsie potencialne zranenie. + +### Shield + +`Shield` znizuje damage, ktory dostanes pri prehratej hande: + +- `PlayerDamage = max(0, RawPlayerDamage - Shield)` + +`Shield` sa pocita len pri damage do hraca v danej hande. ## Ako sa urcuje vitaz handy -Hra vyhodnocuje najlepsiu 5-kartovu kombinaciu zo 7 kariet: +Vyhodnocuje sa najlepsia 5-kartova kombinacia zo 7 kariet: -- 2 hole cards + 5 board cards +- tvoje 2 karty + 5 kariet na stole, +- nepriatelove 2 karty + tych istych 5 kariet na stole. -Porovnanie funguje ako v pokri: +Porovnanie: -1. najprv typ kombinacie (napr. Pair, Two Pair, Straight, Flush...) -2. pri rovnakom type rozhoduju kickery +1. Typ kombinacie (Pair, Two Pair, Straight, Flush, ...) +2. Pri rovnosti rozhoduju kickery (rank vector). -Panel vpravo ukazuje: +## Vizualne pomocky v hre -- nazov kombinacie hraca a nepriatela, -- dovod vyhry/prehry, -- vysledny vypocet damage. +- Hore vpravo su animovane bary: +- `Player Edge`, +- `Enemy Edge`, +- `Shield`. +- Po showdowne su vitazne karty zvyraznene zlatym obrysom. +- Nepriatelove karty sa odkryju pri showdowne. -## Tipy pre zaciatok +## Tipy -- Ak mas dobru kombinaciu, casto sa oplati `Strike`. -- Ak je nepriatel agresivny, `Guard` ti casto zachrani HP. -- `Dodge` pouzi, ked nechces prehrat velky pot. -- Sleduj zlate obrysy po hande, rychlo sa naucis, ktore karty rozhodli. - -## Poznamka k verzii - -Toto je prototyp. Balans, grafika a pravidla sa budu postupne menil. +- Silna handa + vyssi `PlayerEdge` = velky tlak na HP nepriatela. +- Pri riziku prehry pomaha `Guard`, `Check` alebo `Dodge` (kvoli `Shield`). +- `Fold` je takticky ustupok: prehras handu, ale casto prezijes lacnejsie. diff --git a/src/PokerRogue.Game/Game1.cs b/src/PokerRogue.Game/Game1.cs index 4e9f3ce..f467f89 100644 --- a/src/PokerRogue.Game/Game1.cs +++ b/src/PokerRogue.Game/Game1.cs @@ -24,13 +24,14 @@ public class Game1 : Microsoft.Xna.Framework.Game private FightState _fightState = null!; private KeyboardState _previousKeyboardState; - private string _feedbackText = "Press 1/2/3 to act."; + private string _feedbackText = "Press 1..7 to act."; private float _feedbackSecondsRemaining; private float _showdownSecondsRemaining; private bool _awaitingContinue; private const float ShowdownDurationSeconds = 5.0f; private const float TurnRevealAtSeconds = 1.2f; private const float RiverRevealAtSeconds = 2.4f; + private const float EnemyTurnDelaySeconds = 0.55f; private Card[] _showdownPlayerCards = Array.Empty(); private Card[] _showdownEnemyCards = Array.Empty(); @@ -42,11 +43,37 @@ public class Game1 : Microsoft.Xna.Framework.Game private int _runMaxHp = 100; private int _runPlayerHp = 100; private int _runPotBonus; + private int _runPlayerDexterity = 10; private string _encounterText = string.Empty; - private DecisionStage _decisionStage = DecisionStage.Flop; + private DecisionStage _decisionStage = DecisionStage.Preflop; + private TurnActor _activeTurn = TurnActor.Player; + private bool _playerActsSecond; + private bool _playerActedThisStreet; + private bool _enemyActedThisStreet; + private float _enemyTurnSecondsRemaining; + private TurnActor _lastSecondActor = TurnActor.None; + private readonly List _currentHandActionLog = new(); + private int _playerDexterity = 10; + private int _enemyDexterity = 10; + private int _lastPlayerInitiative; + private int _lastEnemyInitiative; + private readonly Random _initiativeRng = new(); + private PlayerAction? _preflopAction; private PlayerAction? _flopAction; private PlayerAction? _turnAction; - private PlayerAction? _riverAction; + private float _displayPlayerEdge; + private float _displayEnemyEdge; + private float _displayShield; + private int _lastPlayerEdge = int.MinValue; + private int _lastEnemyEdge = int.MinValue; + private int _lastShield = int.MinValue; + private float _playerEdgePulse; + private float _enemyEdgePulse; + private float _shieldPulse; + private int _pendingPlayerEdgeDelta; + private int _pendingEnemyEdgeDelta; + private int _pendingShieldDelta; + private readonly List _floatingStatTexts = new(); public Game1() { @@ -79,6 +106,7 @@ public class Game1 : Microsoft.Xna.Framework.Game protected override void Update(GameTime gameTime) { + var dt = (float)gameTime.ElapsedGameTime.TotalSeconds; var keyboardState = Keyboard.GetState(); if (GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed || keyboardState.IsKeyDown(Keys.Escape)) { @@ -127,6 +155,11 @@ public class Game1 : Microsoft.Xna.Framework.Game _runPlayerHp = Math.Min(_runMaxHp, _runPlayerHp + 20); AdvanceToNextNode(); } + else if (Pressed(keyboardState, Keys.D4) || Pressed(keyboardState, Keys.NumPad4)) + { + _runPlayerDexterity += 1; + AdvanceToNextNode(); + } break; case RunScreen.Fight: UpdateFightScreen(keyboardState, gameTime); @@ -140,14 +173,17 @@ public class Game1 : Microsoft.Xna.Framework.Game break; } + UpdateHudAnimations(dt); + UpdateFloatingTexts(dt); + if (_feedbackSecondsRemaining > 0f) { - _feedbackSecondsRemaining -= (float)gameTime.ElapsedGameTime.TotalSeconds; + _feedbackSecondsRemaining -= dt; } if (_showdownSecondsRemaining > 0f) { - _showdownSecondsRemaining -= (float)gameTime.ElapsedGameTime.TotalSeconds; + _showdownSecondsRemaining -= dt; if (_showdownSecondsRemaining <= 0f) { _showdownSecondsRemaining = 0f; @@ -209,12 +245,16 @@ public class Game1 : Microsoft.Xna.Framework.Game _runMaxHp = 100; _runPlayerHp = 100; _runPotBonus = 0; + _runPlayerDexterity = 10; _runScreen = RunScreen.Map; _encounterText = string.Empty; - _decisionStage = DecisionStage.Flop; + _decisionStage = DecisionStage.Preflop; + _activeTurn = TurnActor.Player; + _preflopAction = null; _flopAction = null; _turnAction = null; - _riverAction = null; + _currentHandActionLog.Clear(); + ResetPendingHudDeltas(); ResetShowdownState(); ShowFeedback("New run started."); } @@ -243,7 +283,8 @@ public class Game1 : Microsoft.Xna.Framework.Game } _runPlayerHp = Math.Min(_runMaxHp, _runPlayerHp + 18); - _encounterText = "Shop: Purchased provisions (+18 HP)."; + _runPlayerDexterity += 1; + _encounterText = "Shop: Purchased light gear (+18 HP, +1 DEX)."; _runScreen = RunScreen.Encounter; } @@ -277,12 +318,17 @@ public class Game1 : Microsoft.Xna.Framework.Game MaxHands: maxHands, Enemy: enemy); + _enemyDexterity = node.Type switch + { + NodeType.Boss => 13, + NodeType.Elite => 12, + _ => 10 + }; + _playerDexterity = _runPlayerDexterity; + _fightState = FightEngine.StartFight(config, seed: Environment.TickCount); - _decisionStage = DecisionStage.Flop; - _flopAction = null; - _turnAction = null; - _riverAction = null; ResetShowdownState(); + SetupHandOrder(); _runScreen = RunScreen.Fight; } @@ -291,33 +337,34 @@ public class Game1 : Microsoft.Xna.Framework.Game var showdownActive = IsShowdownVisible; if (_fightState.Phase != HandPhase.End && !showdownActive) { - var pickedAction = ReadActionKey(keyboardState); - if (pickedAction is not null) + if (_activeTurn == TurnActor.Enemy) { - if (_decisionStage == DecisionStage.Flop) + _enemyTurnSecondsRemaining -= (float)gameTime.ElapsedGameTime.TotalSeconds; + if (_enemyTurnSecondsRemaining <= 0f) { - _flopAction = pickedAction.Value; - _decisionStage = DecisionStage.Turn; - ShowFeedback($"Flop action: {_flopAction.Value}. Pick turn action."); + ResolveEnemyStreetAction(); } - else if (_decisionStage == DecisionStage.Turn) + } + else + { + var pickedAction = ReadActionKey(keyboardState); + if (pickedAction is not null) { - _turnAction = pickedAction.Value; - _decisionStage = DecisionStage.River; - ShowFeedback($"Turn action: {_turnAction.Value}. Pick river action."); - } - else - { - _riverAction = pickedAction.Value; - var combined = CombineActions( - _flopAction ?? PlayerAction.Check, - _turnAction ?? PlayerAction.Check, - _riverAction.Value); - ApplyActionAndFeedback(combined, _flopAction, _turnAction, _riverAction); - _flopAction = null; - _turnAction = null; - _riverAction = null; - _decisionStage = DecisionStage.Flop; + if (pickedAction.Value == PlayerAction.Fold) + { + _currentHandActionLog.Add($"{StageLabel(_decisionStage)}: Player Fold"); + SpawnActionFloaters(PlayerAction.Fold); + ApplyActionAndFeedback(PlayerAction.Fold); + ResetStreetState(); + return; + } + + StorePlayerAction(pickedAction.Value); + ApplyPendingDeltaForAction(pickedAction.Value); + SpawnActionFloaters(pickedAction.Value); + _currentHandActionLog.Add($"{StageLabel(_decisionStage)}: Player {pickedAction.Value}"); + _playerActedThisStreet = true; + AdvanceStreetIfReady(); } } } @@ -328,10 +375,11 @@ public class Game1 : Microsoft.Xna.Framework.Game _showdownPlayerCards = Array.Empty(); _showdownEnemyCards = Array.Empty(); _showdownBoardCards = Array.Empty(); - _decisionStage = DecisionStage.Flop; - _flopAction = null; - _turnAction = null; - _riverAction = null; + _currentHandActionLog.Clear(); + if (_fightState.Phase != HandPhase.End) + { + SetupHandOrder(); + } ShowFeedback("Next hand."); } @@ -376,7 +424,7 @@ public class Game1 : Microsoft.Xna.Framework.Game private void DrawMapScreen() { - DrawLine(new Vector2(40, 24), $"Map - Node {_currentNodeIndex + 1}/{_runNodes.Count} | HP {_runPlayerHp}/{_runMaxHp} | Pot bonus +{_runPotBonus}"); + DrawLine(new Vector2(40, 24), $"Map - Node {_currentNodeIndex + 1}/{_runNodes.Count} | HP {_runPlayerHp}/{_runMaxHp} | Pot bonus +{_runPotBonus} | DEX {_runPlayerDexterity}"); for (var i = 0; i < _runNodes.Count; i++) { var node = _runNodes[i]; @@ -391,17 +439,18 @@ public class Game1 : Microsoft.Xna.Framework.Game { DrawLine(new Vector2(50, 60), "Node Result"); DrawLine(new Vector2(50, 110), _encounterText); - DrawLine(new Vector2(50, 160), $"Current HP: {_runPlayerHp}/{_runMaxHp}"); + DrawLine(new Vector2(50, 160), $"Current HP: {_runPlayerHp}/{_runMaxHp} | DEX: {_runPlayerDexterity}"); DrawLine(new Vector2(50, 220), "Press Space to continue."); } private void DrawRewardScreen() { DrawLine(new Vector2(50, 44), "Reward Choice"); - DrawLine(new Vector2(50, 88), $"HP: {_runPlayerHp}/{_runMaxHp} | Pot bonus +{_runPotBonus}"); + DrawLine(new Vector2(50, 88), $"HP: {_runPlayerHp}/{_runMaxHp} | Pot bonus +{_runPotBonus} | DEX {_runPlayerDexterity}"); DrawLine(new Vector2(50, 148), "1 - Max HP +10 and heal +12"); DrawLine(new Vector2(50, 182), "2 - Pot bonus +2 for next fights"); DrawLine(new Vector2(50, 216), "3 - Immediate heal +20"); + DrawLine(new Vector2(50, 250), "4 - Dexterity +1 (better initiative)"); } private void DrawEndScreen(string title, string subtitle) @@ -418,10 +467,7 @@ public class Game1 : Microsoft.Xna.Framework.Game _showdownPlayerCards = Array.Empty(); _showdownEnemyCards = Array.Empty(); _showdownBoardCards = Array.Empty(); - _decisionStage = DecisionStage.Flop; - _flopAction = null; - _turnAction = null; - _riverAction = null; + ResetStreetState(); } private Layout BuildLayout() @@ -483,21 +529,30 @@ public class Game1 : Microsoft.Xna.Framework.Game private void DrawTopHud(Layout layout) { var x = layout.Margin; + var viewportWidth = GraphicsDevice.Viewport.Width; + var statsTotalWidth = 240 + 16 + 240 + 16 + 170; + var statsX = Math.Max(x + 520, viewportWidth - layout.Margin - statsTotalWidth); + const int statsY = 124; DrawLine(new Vector2(x, 16), $"Hand {_fightState.HandNumber}/{_fightState.Config.MaxHands} | Phase: {_fightState.Phase}"); DrawLine(new Vector2(x, 44), $"Player HP: {_fightState.PlayerHp} | Enemy HP: {_fightState.EnemyHp} | Pot: {_fightState.Pot}"); DrawLine(new Vector2(x, 72), $"Edge: Player {_fightState.PlayerEdge} | Enemy {_fightState.EnemyEdge} | Enemy Intent: {_fightState.EnemyIntent}"); - DrawLine(new Vector2(x, 100), $"Decision stage: {BuildDecisionStageText()}"); - DrawLine(new Vector2(x, 124), "Action Tooltips", Color.LightGray); - DrawLine(new Vector2(x, 148), "1 Strike: Pot +3, Edge +10 | 2 Guard: Shield +6, Edge +5", Color.LightGray); - DrawLine(new Vector2(x, 172), "3 Dodge: Pot -2 (min 1), Shield +2 | 4 Check: Shield +1, Edge +2", Color.LightGray); - DrawLine(new Vector2(x, 196), "5 Probe: Pot +1, Edge +6 | 6 All-In: Pot +8, Edge +18, EnemyEdge +8", Color.LightGray); - DrawLine(new Vector2(x, 220), "7 Fold: Lose hand early, reduced damage, keep HP for later", Color.LightGray); - DrawLine(new Vector2(x, 244), "Per hand: pick Flop, Turn, River action", Color.LightGray); - DrawLine(new Vector2(x, 268), "Global: Space Continue, R Restart, Esc Exit", Color.LightGray); + DrawLine(new Vector2(x, 100), $"Decision stage: {BuildDecisionStageText()} | Turn: {_activeTurn}"); + DrawLine(new Vector2(x, 124), $"Initiative P:{_lastPlayerInitiative} E:{_lastEnemyInitiative} | Player position: {(_playerActsSecond ? "Second" : "First")}", Color.LightGray); + DrawStatBar("Player Edge", _displayPlayerEdge, GetTargetPlayerEdge(), 0, 100, new Rectangle(statsX, statsY, 240, 12), new Color(210, 170, 60), _playerEdgePulse); + DrawStatBar("Enemy Edge", _displayEnemyEdge, GetTargetEnemyEdge(), 0, 100, new Rectangle(statsX + 256, statsY, 240, 12), new Color(184, 90, 90), _enemyEdgePulse); + DrawStatBar("Shield", _displayShield, GetTargetShield(), 0, 12, new Rectangle(statsX + 512, statsY, 170, 12), new Color(88, 176, 212), _shieldPulse); + DrawFloatingTexts(); + DrawLine(new Vector2(x, 152), "Action Tooltips", Color.LightGray); + DrawLine(new Vector2(x, 176), "1 Strike: Pot +3, Edge +10 | 2 Guard: Shield +6, Edge +5", Color.LightGray); + DrawLine(new Vector2(x, 200), "3 Dodge: Pot -2 (min 1), Shield +2 | 4 Check: Shield +1, Edge +2", Color.LightGray); + DrawLine(new Vector2(x, 224), "5 Probe: Pot +1, Edge +6 | 6 All-In: Pot +8, Edge +18, EnemyEdge +8", Color.LightGray); + DrawLine(new Vector2(x, 248), "7 Fold: instant hand end, enemy wins this hand, reduced incoming damage", Color.LightGray); + DrawLine(new Vector2(x, 272), "Flow: each street is Player <-> Enemy (position set by initiative)", Color.LightGray); + DrawLine(new Vector2(x, 296), "Global: Space Continue, R Restart, Esc Exit", Color.LightGray); if (_feedbackSecondsRemaining > 0f) { - DrawLine(new Vector2(x, 292), _feedbackText, Color.Gold); + DrawLine(new Vector2(x, 320), _feedbackText, Color.Gold); } } @@ -542,6 +597,24 @@ public class Game1 : Microsoft.Xna.Framework.Game DrawLine(new Vector2(x, y), "Why this result"); y += 30; + DrawLine(new Vector2(x, y), "Action timeline:"); + y += 22; + if (_currentHandActionLog.Count == 0) + { + DrawLine(new Vector2(x, y), "-"); + y += 20; + } + else + { + var start = Math.Max(0, _currentHandActionLog.Count - 6); + for (var i = start; i < _currentHandActionLog.Count; i++) + { + DrawLine(new Vector2(x, y), _currentHandActionLog[i], Color.LightGray); + y += 18; + } + } + y += 12; + DrawLine(new Vector2(x, y), "Poker evaluator (best 5 of 7):"); y += 24; DrawLine(new Vector2(x, y), "1) Detect category (pair/straight/flush...)"); @@ -648,9 +721,9 @@ public class Game1 : Microsoft.Xna.Framework.Game private void ApplyActionAndFeedback( PlayerAction action, - PlayerAction? flopAction = null, - PlayerAction? turnAction = null, - PlayerAction? riverAction = null) + PlayerAction? firstAction = null, + PlayerAction? secondAction = null, + PlayerAction? thirdAction = null) { _showdownPlayerCards = _fightState.PlayerHoleCards.ToArray(); _showdownEnemyCards = _fightState.EnemyHoleCards.ToArray(); @@ -659,9 +732,9 @@ public class Game1 : Microsoft.Xna.Framework.Game _fightState = FightEngine.ApplyPlayerAction(_fightState, action); _showdownSecondsRemaining = ShowdownDurationSeconds; _awaitingContinue = false; - if (flopAction is not null && turnAction is not null && riverAction is not null) + if (firstAction is not null && secondAction is not null && thirdAction is not null) { - ShowFeedback($"Flop {flopAction.Value} + Turn {turnAction.Value} + River {riverAction.Value} => {action} | {_fightState.LastOutcome}"); + ShowFeedback($"Preflop {firstAction.Value} + Flop {secondAction.Value} + Turn {thirdAction.Value} => {action} | {_fightState.LastOutcome}"); } else { @@ -669,6 +742,135 @@ public class Game1 : Microsoft.Xna.Framework.Game } } + private void SetupHandOrder() + { + ResetStreetState(); + _currentHandActionLog.Clear(); + + _lastPlayerInitiative = _playerDexterity + _initiativeRng.Next(0, 6); + _lastEnemyInitiative = _enemyDexterity + _initiativeRng.Next(0, 6); + var secondActor = _lastPlayerInitiative >= _lastEnemyInitiative ? TurnActor.Player : TurnActor.Enemy; + if (_lastSecondActor != TurnActor.None && secondActor == _lastSecondActor) + { + secondActor = secondActor == TurnActor.Player ? TurnActor.Enemy : TurnActor.Player; + } + + _lastSecondActor = secondActor; + _playerActsSecond = secondActor == TurnActor.Player; + _activeTurn = _playerActsSecond ? TurnActor.Enemy : TurnActor.Player; + _enemyTurnSecondsRemaining = _activeTurn == TurnActor.Enemy ? EnemyTurnDelaySeconds : 0f; + + _currentHandActionLog.Add($"Initiative: Player {_lastPlayerInitiative} vs Enemy {_lastEnemyInitiative}"); + _currentHandActionLog.Add($"Position: Player {(_playerActsSecond ? "Second" : "First")} this hand"); + } + + private void ResetStreetState() + { + _decisionStage = DecisionStage.Preflop; + _preflopAction = null; + _flopAction = null; + _turnAction = null; + _playerActedThisStreet = false; + _enemyActedThisStreet = false; + _enemyTurnSecondsRemaining = 0f; + _activeTurn = TurnActor.Player; + ResetPendingHudDeltas(); + } + + private void StorePlayerAction(PlayerAction action) + { + if (_decisionStage == DecisionStage.Preflop) + { + _preflopAction = action; + return; + } + + if (_decisionStage == DecisionStage.Flop) + { + _flopAction = action; + return; + } + + _turnAction = action; + } + + private void ResolveEnemyStreetAction() + { + var enemyAction = GetEnemyActionForStage(_decisionStage, _fightState.EnemyIntent); + _currentHandActionLog.Add($"{StageLabel(_decisionStage)}: Enemy {enemyAction}"); + _enemyActedThisStreet = true; + AdvanceStreetIfReady(); + } + + private void AdvanceStreetIfReady() + { + if (!_playerActedThisStreet || !_enemyActedThisStreet) + { + _activeTurn = _playerActedThisStreet ? TurnActor.Enemy : TurnActor.Player; + if (_activeTurn == TurnActor.Enemy) + { + _enemyTurnSecondsRemaining = EnemyTurnDelaySeconds; + ShowFeedback("Enemy turn..."); + } + else + { + ShowFeedback("Your turn."); + } + return; + } + + if (_decisionStage == DecisionStage.Preflop) + { + _decisionStage = DecisionStage.Flop; + _playerActedThisStreet = false; + _enemyActedThisStreet = false; + _activeTurn = _playerActsSecond ? TurnActor.Enemy : TurnActor.Player; + _enemyTurnSecondsRemaining = _activeTurn == TurnActor.Enemy ? EnemyTurnDelaySeconds : 0f; + ShowFeedback("Flop revealed."); + return; + } + + if (_decisionStage == DecisionStage.Flop) + { + _decisionStage = DecisionStage.Turn; + _playerActedThisStreet = false; + _enemyActedThisStreet = false; + _activeTurn = _playerActsSecond ? TurnActor.Enemy : TurnActor.Player; + _enemyTurnSecondsRemaining = _activeTurn == TurnActor.Enemy ? EnemyTurnDelaySeconds : 0f; + ShowFeedback("Turn revealed."); + return; + } + + var combined = CombineActions( + _preflopAction ?? PlayerAction.Check, + _flopAction ?? PlayerAction.Check, + _turnAction ?? PlayerAction.Check); + ApplyActionAndFeedback(combined, _preflopAction, _flopAction, _turnAction); + ResetStreetState(); + } + + private static string StageLabel(DecisionStage stage) + { + return stage switch + { + DecisionStage.Preflop => "Preflop", + DecisionStage.Flop => "Flop", + DecisionStage.Turn => "Turn", + _ => "Street" + }; + } + + private static PlayerAction GetEnemyActionForStage(DecisionStage stage, EnemyIntent intent) + { + return intent switch + { + EnemyIntent.Strike => stage == DecisionStage.Turn ? PlayerAction.AllIn : PlayerAction.Strike, + EnemyIntent.Guard => stage == DecisionStage.Preflop ? PlayerAction.Check : PlayerAction.Guard, + EnemyIntent.Dodge => stage == DecisionStage.Turn ? PlayerAction.Check : PlayerAction.Dodge, + _ => PlayerAction.Check + }; + } + private void ShowFeedback(string text) { _feedbackText = text; @@ -717,6 +919,189 @@ public class Game1 : Microsoft.Xna.Framework.Game _spriteBatch.Draw(_pixel, rect, color); } + private void DrawStatBar(string label, float displayValue, int actualValue, int min, int max, Rectangle barRect, Color fillColor, float pulse) + { + var pulseAmount = Math.Clamp(pulse / 0.4f, 0f, 1f); + var glow = Color.Lerp(Color.Black, Color.White, pulseAmount * 0.45f); + var expanded = new Rectangle(barRect.X - (int)(2 * pulseAmount), barRect.Y - (int)(2 * pulseAmount), barRect.Width + (int)(4 * pulseAmount), barRect.Height + (int)(4 * pulseAmount)); + + DrawFilled(expanded, new Color(20, 20, 20, 180)); + DrawBorder(expanded, glow); + + var clamped = Math.Clamp(displayValue, min, max); + var normalized = max > min ? (clamped - min) / (float)(max - min) : 0f; + var fillWidth = Math.Max(0, (int)((expanded.Width - 2) * normalized)); + if (fillWidth > 0) + { + var animatedFill = Color.Lerp(fillColor, Color.White, pulseAmount * 0.25f); + DrawFilled(new Rectangle(expanded.X + 1, expanded.Y + 1, fillWidth, expanded.Height - 2), animatedFill); + } + + var labelSize = _debugFont.MeasureString(label); + _spriteBatch.DrawString(_debugFont, label, new Vector2(expanded.X, expanded.Y - labelSize.Y - 2), Color.LightGray); + _spriteBatch.DrawString(_debugFont, actualValue.ToString(), new Vector2(expanded.Right + 6, expanded.Y - 4), Color.White); + } + + private void UpdateHudAnimations(float dt) + { + var targetPlayerEdge = GetTargetPlayerEdge(); + var targetEnemyEdge = GetTargetEnemyEdge(); + var targetShield = GetTargetShield(); + + if (_lastPlayerEdge == int.MinValue) + { + _displayPlayerEdge = targetPlayerEdge; + _displayEnemyEdge = targetEnemyEdge; + _displayShield = targetShield; + _lastPlayerEdge = targetPlayerEdge; + _lastEnemyEdge = targetEnemyEdge; + _lastShield = targetShield; + } + + if (_lastPlayerEdge != targetPlayerEdge) + { + _playerEdgePulse = 0.4f; + _lastPlayerEdge = targetPlayerEdge; + } + + if (_lastEnemyEdge != targetEnemyEdge) + { + _enemyEdgePulse = 0.4f; + _lastEnemyEdge = targetEnemyEdge; + } + + if (_lastShield != targetShield) + { + _shieldPulse = 0.4f; + _lastShield = targetShield; + } + + _displayPlayerEdge = SmoothTowards(_displayPlayerEdge, targetPlayerEdge, dt, 12f); + _displayEnemyEdge = SmoothTowards(_displayEnemyEdge, targetEnemyEdge, dt, 12f); + _displayShield = SmoothTowards(_displayShield, targetShield, dt, 18f); + + _playerEdgePulse = Math.Max(0f, _playerEdgePulse - dt); + _enemyEdgePulse = Math.Max(0f, _enemyEdgePulse - dt); + _shieldPulse = Math.Max(0f, _shieldPulse - dt); + } + + private int GetTargetPlayerEdge() + { + return _fightState.PlayerEdge + _pendingPlayerEdgeDelta; + } + + private int GetTargetEnemyEdge() + { + return _fightState.EnemyEdge + _pendingEnemyEdgeDelta; + } + + private int GetTargetShield() + { + return _fightState.PlayerShield + _pendingShieldDelta; + } + + private void ResetPendingHudDeltas() + { + _pendingPlayerEdgeDelta = 0; + _pendingEnemyEdgeDelta = 0; + _pendingShieldDelta = 0; + } + + private void ApplyPendingDeltaForAction(PlayerAction action) + { + var (edge, enemyEdge, shield) = GetActionDelta(action); + _pendingPlayerEdgeDelta += edge; + _pendingEnemyEdgeDelta += enemyEdge; + _pendingShieldDelta += shield; + } + + private static (int Edge, int EnemyEdge, int Shield) GetActionDelta(PlayerAction action) + { + return action switch + { + PlayerAction.Strike => (10, 0, 0), + PlayerAction.Guard => (5, 0, 6), + PlayerAction.Dodge => (0, 0, 2), + PlayerAction.Check => (2, 0, 1), + PlayerAction.Probe => (6, 0, 0), + PlayerAction.AllIn => (18, 8, 0), + PlayerAction.Fold => (0, 0, 10), + _ => (0, 0, 0) + }; + } + + private void SpawnActionFloaters(PlayerAction action) + { + var viewportWidth = GraphicsDevice.Viewport.Width; + var margin = Math.Max(14, viewportWidth / 90); + var statsTotalWidth = 240 + 16 + 240 + 16 + 170; + var statsX = Math.Max(margin + 520, viewportWidth - margin - statsTotalWidth); + const int statsY = 124; + var (edge, enemyEdge, shield) = GetActionDelta(action); + + if (edge != 0) + { + SpawnFloatingText(FormatDelta(edge), new Vector2(statsX + 112, statsY - 18), edge > 0 ? Color.LawnGreen : Color.OrangeRed); + } + + if (enemyEdge != 0) + { + SpawnFloatingText(FormatDelta(enemyEdge), new Vector2(statsX + 368, statsY - 18), enemyEdge > 0 ? Color.Orange : Color.CornflowerBlue); + } + + if (shield != 0) + { + SpawnFloatingText(FormatDelta(shield), new Vector2(statsX + 564, statsY - 18), shield > 0 ? Color.LightSkyBlue : Color.OrangeRed); + } + } + + private void SpawnFloatingText(string text, Vector2 position, Color color) + { + _floatingStatTexts.Add(new FloatingStatText(text, position, color, 0.9f, 0.9f)); + } + + private void UpdateFloatingTexts(float dt) + { + for (var i = _floatingStatTexts.Count - 1; i >= 0; i--) + { + var item = _floatingStatTexts[i]; + var updated = item with + { + Remaining = item.Remaining - dt, + Position = item.Position + new Vector2(0f, -20f * dt) + }; + + if (updated.Remaining <= 0f) + { + _floatingStatTexts.RemoveAt(i); + continue; + } + + _floatingStatTexts[i] = updated; + } + } + + private void DrawFloatingTexts() + { + for (var i = 0; i < _floatingStatTexts.Count; i++) + { + var item = _floatingStatTexts[i]; + var alpha = Math.Clamp(item.Remaining / item.Lifetime, 0f, 1f); + _spriteBatch.DrawString(_debugFont, item.Text, item.Position, item.Color * alpha); + } + } + + private static string FormatDelta(int value) + { + return value >= 0 ? $"+{value}" : value.ToString(); + } + + private static float SmoothTowards(float current, float target, float dt, float speed) + { + var alpha = 1f - MathF.Exp(-speed * Math.Max(0f, dt)); + return current + ((target - current) * alpha); + } + private void DrawBorder(Rectangle rect, Color color) { const int thickness = 2; @@ -820,9 +1205,9 @@ public class Game1 : Microsoft.Xna.Framework.Game return _decisionStage switch { - DecisionStage.Flop => "Flop action (1st)", - DecisionStage.Turn => "Turn action (2nd)", - DecisionStage.River => "River action (3rd)", + DecisionStage.Preflop => "Preflop action (1st)", + DecisionStage.Flop => "Flop action (2nd)", + DecisionStage.Turn => "Turn action (3rd)", _ => "-" }; } @@ -928,9 +1313,9 @@ public class Game1 : Microsoft.Xna.Framework.Game { return _decisionStage switch { + DecisionStage.Preflop => 0, DecisionStage.Flop => 3, DecisionStage.Turn => 4, - DecisionStage.River => 5, _ => 3 }; } @@ -985,9 +1370,16 @@ public class Game1 : Microsoft.Xna.Framework.Game private enum DecisionStage { + Preflop, Flop, - Turn, - River + Turn + } + + private enum TurnActor + { + None, + Player, + Enemy } private enum RunScreen @@ -1025,4 +1417,6 @@ public class Game1 : Microsoft.Xna.Framework.Game Vector2 BoardCardsPos, Vector2 PlayerLabelPos, Vector2 PlayerCardsPos); + + private readonly record struct FloatingStatText(string Text, Vector2 Position, Color Color, float Remaining, float Lifetime); }