i;nint
This commit is contained in:
+61
-49
@@ -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.
|
||||
|
||||
+460
-66
@@ -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<Card>();
|
||||
private Card[] _showdownEnemyCards = Array.Empty<Card>();
|
||||
@@ -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<string> _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<FloatingStatText> _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<Card>();
|
||||
_showdownEnemyCards = Array.Empty<Card>();
|
||||
_showdownBoardCards = Array.Empty<Card>();
|
||||
_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<Card>();
|
||||
_showdownEnemyCards = Array.Empty<Card>();
|
||||
_showdownBoardCards = Array.Empty<Card>();
|
||||
_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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user