From dc9c5f654592333661eee02551582ec328d37f6e Mon Sep 17 00:00:00 2001 From: Lang <7system7@gmail.com> Date: Sat, 18 Apr 2026 13:44:15 +0200 Subject: [PATCH] chg: usr: add extended data to battle reports and sharing image to make viewable bonus points #5 --- Makefile | 29 ++++- assets/css/homepage/_profile.scss | 98 ++++++++++++++ assets/js/components/BattleDialog.jsx | 143 +++++++++++++++++---- docs/game-mechanics/BONUS_POINTS_SYSTEM.md | 35 +++++ src/Controller/ProfileController.php | 32 +++-- src/Service/BattleCardGenerator.php | 13 +- templates/Game/battle_share.html.twig | 125 +++++++++++++++++- 7 files changed, 431 insertions(+), 44 deletions(-) diff --git a/Makefile b/Makefile index 62d5770..515f36e 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -.PHONY: help start start-build stop build down ps logs prune-everything db-reset mercure-jwt +.PHONY: help start start-build stop build down ps logs prune-everything db-reset mercure-jwt cache-clear og-cache-clear .DEFAULT_GOAL := help @@ -12,6 +12,8 @@ help: @echo " make prune-everything - Prune volumes, networks and images (DANGEROUS!)" @echo " make db-reset - Reset the database (drop, create, migrate) (DANGEROUS!)" @echo " make ccp - Clear the production cache" + @echo " make cache-clear - Clear all caches (Vite, Symfony, node_modules)" + @echo " make og-cache-clear - Clear Open Graph cache only" start: docker compose up -d @@ -55,3 +57,28 @@ db-reset: ccp: bin/console cache:clear --no-warmup --env=prod + +cache-clear: + @echo "Clearing all caches..." + @rm -rf node_modules/.vite + @rm -rf .vite + @rm -rf var/og-cache + @php bin/console cache:clear --no-warmup + @echo "✓ Vite cache cleared" + @echo "✓ OG cache cleared" + @echo "✓ Symfony cache cleared" + @echo "" + @echo "Rebuilding assets..." + @bun run build + @echo "" + @echo "✓ All caches cleared and assets rebuilt!" + @echo " Next step: Refresh browser with Ctrl+Shift+R" + +og-cache-clear: + @echo "Clearing Open Graph cache..." + @rm -rf var/og-cache + @echo "✓ OG cache cleared!" + @echo " Battle card images will be regenerated on next access" + + + diff --git a/assets/css/homepage/_profile.scss b/assets/css/homepage/_profile.scss index e9d8537..2280962 100644 --- a/assets/css/homepage/_profile.scss +++ b/assets/css/homepage/_profile.scss @@ -859,6 +859,104 @@ flex-wrap: wrap; } +.bshare-bonus { + padding: 28px 28px 0; + border-top: 1px solid rgba(255, 255, 255, 0.08); +} + +.bshare-bonus__title { + font: 700 13px 'Rajdhani', sans-serif; + text-transform: uppercase; + letter-spacing: 2px; + color: #ffd700; + margin-bottom: 20px; + display: flex; + align-items: center; + gap: 8px; + + i { font-size: 14px; } +} + +.bshare-bonus__grid { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 24px; + margin-bottom: 28px; +} + +.bshare-bonus__player { + padding: 16px; + border-radius: 8px; + border: 1px solid rgba(255, 255, 255, 0.08); + background: rgba(255, 255, 255, 0.02); + + &--red { + border-color: rgba(246, 125, 82, 0.15); + background: rgba(246, 125, 82, 0.04); + } + + &--blue { + border-color: rgba(149, 207, 245, 0.15); + background: rgba(149, 207, 245, 0.04); + } +} + +.bshare-bonus__header { + display: flex; + align-items: baseline; + gap: 8px; + margin-bottom: 16px; + padding-bottom: 12px; + border-bottom: 1px solid rgba(255, 255, 255, 0.08); +} + +.bshare-bonus__points { + font: 700 24px 'Rajdhani', sans-serif; + background: linear-gradient(135deg, #ffd700, #ffed4e); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; +} + +.bshare-bonus__label { + font: 700 11px 'Rajdhani', sans-serif; + text-transform: uppercase; + letter-spacing: 1px; + color: rgba(255, 215, 0, 0.7); +} + +.bshare-bonus__stats { + display: flex; + flex-direction: column; + gap: 10px; +} + +.bshare-bonus__stat { + display: flex; + justify-content: space-between; + align-items: center; + font-size: 12px; + gap: 8px; +} + +.bshare-bonus__stat-label { + color: rgba(255, 255, 255, 0.6); + font: 500 11px 'Rajdhani', sans-serif; + text-transform: capitalize; +} + +.bshare-bonus__stat-value { + font: 700 13px 'Rajdhani', sans-serif; + color: rgba(255, 215, 0, 0.9); + min-width: 24px; + text-align: right; +} + +.bshare-bonus__stat--empty { + color: rgba(255, 255, 255, 0.4); + font-size: 11px; +} + .bshare-btn { display: inline-flex; align-items: center; diff --git a/assets/js/components/BattleDialog.jsx b/assets/js/components/BattleDialog.jsx index 775d115..0991af1 100644 --- a/assets/js/components/BattleDialog.jsx +++ b/assets/js/components/BattleDialog.jsx @@ -50,7 +50,7 @@ const RESULT_META = { }, }; -function Avatar({ name, color, avatarUrl }) { +function Avatar({ name, color, avatarUrl, bonusPoints = 0 }) { const isRed = 'red' === color; const initials = (name || '?').slice(0, 2).toUpperCase(); @@ -66,31 +66,53 @@ function Avatar({ name, color, avatarUrl }) { const textColor = isRed ? '#f67d52' : '#95cff5'; return ( -
-
- {avatarUrl ? ( - {name} - ) : ( - initials +
+
+
+ {avatarUrl ? ( + {name} + ) : ( + initials + )} +
+ {0 < bonusPoints && ( +
+ +
)}
- + game.blueBonusPoints ? game.redBonusPoints : 0} />
{game.redPoints ?? '—'} : {game.bluePoints ?? '—'}
+
+ + {(game.redBonusPoints ?? 0).toFixed(1)} + + : + + {(game.blueBonusPoints ?? 0).toFixed(1)} + +
VS
{meta.label}
- + game.redBonusPoints ? game.blueBonusPoints : 0} />
@@ -244,6 +275,62 @@ export default function BattleDialog({ games }) { )}
+ + {(0 < game.redBonusPoints + || 0 < game.blueBonusPoints + || game.redBonusStats?.blindHits + || game.blueBonusStats?.blindHits + ) && ( +
+
+ {/* Red Bonus */} +
+ + Red Bonus Statistics + +
+ + {0 < game.redBonusStats?.blindHits && } + {0 < game.redBonusStats?.chainBest && } + {0 < game.redBonusStats?.edgeMines && } + {0 < game.redBonusStats?.lastMineHits && } + {0 < game.redBonusStats?.biggestReveal && } + {!game.redBonusStats?.blindHits && !game.redBonusStats?.chainBest && !game.redBonusStats?.edgeMines && !game.redBonusStats?.lastMineHits && !game.redBonusStats?.biggestReveal + && } +
+
+ + {/* Blue Bonus */} +
+ + Blue Bonus Statistics + +
+ + {0 < game.blueBonusStats?.blindHits && } + {0 < game.blueBonusStats?.chainBest && } + {0 < game.blueBonusStats?.edgeMines && } + {0 < game.blueBonusStats?.lastMineHits && } + {0 < game.blueBonusStats?.biggestReveal && } + {!game.blueBonusStats?.blindHits && !game.blueBonusStats?.chainBest && !game.blueBonusStats?.edgeMines && !game.blueBonusStats?.lastMineHits && !game.blueBonusStats?.biggestReveal + && } +
+
+
+
+ )}
diff --git a/docs/game-mechanics/BONUS_POINTS_SYSTEM.md b/docs/game-mechanics/BONUS_POINTS_SYSTEM.md index bf43b93..edb4c7b 100644 --- a/docs/game-mechanics/BONUS_POINTS_SYSTEM.md +++ b/docs/game-mechanics/BONUS_POINTS_SYSTEM.md @@ -124,10 +124,45 @@ The Mine-Seeker game includes a bonus points system that rewards skilled play. B --- +## Battle Report Display Components + +**IMPORTANT**: The Bonus Statistics display appears in **two places** that must be kept in sync: + +### 1. Public Battle Share Page +**File**: `/templates/Game/battle_share.html.twig` +- Displays via `bshare-bonus` CSS classes +- Backend data passed from `ProfileController::battleShare()` +- Shows bonus stats as HTML table format + +### 2. Profile Dialog (BattleDialog component) +**File**: `/assets/js/components/BattleDialog.jsx` +- React component using Material-UI Dialog +- Displays inside the match details modal on profile page +- Shows bonus stats using `StatRow` components in side-by-side boxes + +### Synchronization Requirements + +When making changes to the bonus statistics display, update **BOTH** files: + +1. **Update logic/data** → Edit both template and component +2. **Change stat names** → Update both BONUS_LABELS and both display files +3. **Modify formatting** → Keep visual consistency between both displays +4. **Add new stats** → Add to both the `.twig` template AND the `.jsx` component + +**Checklist for changes**: +- [ ] Update `/src/Util/TopicManager.php` if bonus calculation changes +- [ ] Update `/templates/Game/battle_share.html.twig` for public display +- [ ] Update `/assets/js/components/BattleDialog.jsx` for profile dialog +- [ ] Update `/assets/js/mine-seeker/utils/constants.jsx` if adding new stats +- [ ] Test both displays show identical data + +--- + ## Quick Checklist for Changes - [ ] Code changes implemented - [ ] This documentation updated - [ ] `/docs/README.md` Quick Reference table updated - [ ] Code comments added/updated - [ ] Examples updated to match new behavior +- [ ] Both battle report displays tested diff --git a/src/Controller/ProfileController.php b/src/Controller/ProfileController.php index d823990..1fc24a0 100644 --- a/src/Controller/ProfileController.php +++ b/src/Controller/ProfileController.php @@ -163,6 +163,10 @@ class ProfileController extends AbstractController 'result' => $result, 'myPoints' => $myPts, 'oppPoints' => $oppPts, + 'redBonusPoints' => $game->getRedBonusPoints() ?? 0, + 'blueBonusPoints' => $game->getBlueBonusPoints() ?? 0, + 'redBonusStats' => $game->getRedBonusStats() ?? [], + 'blueBonusStats' => $game->getBlueBonusStats() ?? [], ]; }, $recent), 'chartData' => [ @@ -197,6 +201,10 @@ class ProfileController extends AbstractController $resign = $game->getResign(); $redAvatar = $game->getRed()?->getAvatarPath(); $blueAvatar = $game->getBlue()?->getAvatarPath(); + $redBonusPoints = $game->getRedBonusPoints() ?? 0; + $blueBonusPoints = $game->getBlueBonusPoints() ?? 0; + $redBonusStats = $game->getRedBonusStats() ?? []; + $blueBonusStats = $game->getBlueBonusStats() ?? []; if ($resign === 'red') { $summary = "$redName resigned — $blueName wins"; @@ -215,16 +223,20 @@ class ProfileController extends AbstractController } return $this->render('Game/battle_share.html.twig', [ - 'game' => $game, - 'redName' => $redName, - 'blueName' => $blueName, - 'redPts' => $redPts, - 'bluePts' => $bluePts, - 'resign' => $resign, - 'redAvatar' => $redAvatar, - 'blueAvatar' => $blueAvatar, - 'ogTitle' => "MineSeeker · $summary", - 'ogDesc' => "Watch the battle replay: $summary — played on MineSeeker, the multiplayer minesweeper.", + 'game' => $game, + 'redName' => $redName, + 'blueName' => $blueName, + 'redPts' => $redPts, + 'bluePts' => $bluePts, + 'resign' => $resign, + 'redAvatar' => $redAvatar, + 'blueAvatar' => $blueAvatar, + 'redBonusPoints' => $redBonusPoints, + 'blueBonusPoints' => $blueBonusPoints, + 'redBonusStats' => $redBonusStats, + 'blueBonusStats' => $blueBonusStats, + 'ogTitle' => "MineSeeker · $summary", + 'ogDesc' => "Watch the battle replay: $summary — played on MineSeeker, the multiplayer minesweeper.", ]); } diff --git a/src/Service/BattleCardGenerator.php b/src/Service/BattleCardGenerator.php index e3f8151..1d90c0d 100644 --- a/src/Service/BattleCardGenerator.php +++ b/src/Service/BattleCardGenerator.php @@ -56,8 +56,9 @@ class BattleCardGenerator { $path = $this->cachePath((int)$game->getId()); + // Always regenerate to ensure bonus points are included if (is_file($path)) { - return $path; + unlink($path); } if (!is_dir($this->cacheDir)) { @@ -154,6 +155,12 @@ class BattleCardGenerator $scoreText = $redPts !== null && $bluePts !== null ? $redPts . ' : ' . $bluePts : 'VS'; $this->centeredText($im, $scoreText, 72, self::WIDTH / 2, 390, $white); + /** Bonus points below score*/ + $redBonusPoints = $game->getRedBonusPoints() ?? 0; + $blueBonusPoints = $game->getBlueBonusPoints() ?? 0; + $bonusText = number_format((float)$redBonusPoints, 1, '.', '') . ' * : * ' . number_format((float)$blueBonusPoints, 1, '.', ''); + $this->centeredText($im, $bonusText, 24, self::WIDTH / 2, 425, $gold); + if ($winner === 'red') { $resultText = $redName . ' wins'; $resultColor = $gold; @@ -169,11 +176,11 @@ class BattleCardGenerator } if ($resultText !== '') { - $this->centeredText($im, $resultText, 30, self::WIDTH / 2, 460, $resultColor); + $this->centeredText($im, $resultText, 30, self::WIDTH / 2, 475, $resultColor); } if ($resign) { - $this->centeredText($im, ucfirst($resign) . ' resigned', 18, self::WIDTH / 2, 498, $muted); + $this->centeredText($im, ucfirst($resign) . ' resigned', 18, self::WIDTH / 2, 508, $muted); } $this->centeredText($im, 'mineseeker.hu', 16, self::WIDTH / 2, self::HEIGHT - 20, $muted); diff --git a/templates/Game/battle_share.html.twig b/templates/Game/battle_share.html.twig index f9fed9d..878d137 100644 --- a/templates/Game/battle_share.html.twig +++ b/templates/Game/battle_share.html.twig @@ -31,7 +31,7 @@
-
+
{% if redAvatar %} {{ redName }} blueBonusPoints %} +
+ +
+ {% endif %}
{{ redName }} Red @@ -53,6 +58,15 @@ {% else %}
— : —
{% endif %} +
+ + {{ (redBonusPoints ?? 0)|number_format(1, '.', '') }} + + : + + {{ (blueBonusPoints ?? 0)|number_format(1, '.', '') }} + +
VS
{% if resign == 'red' %}
@@ -79,7 +93,7 @@ {% endif %}
-
+
{% if blueAvatar %} {{ blueName }} redBonusPoints %} +
+ +
+ {% endif %}
{{ blueName }} Blue @@ -118,6 +137,108 @@
{% endif %}
+ + {# Bonus Stats Section #} + {% set hasRedStats = redBonusStats is not empty and (redBonusStats.blindHits or redBonusStats.chainBest or redBonusStats.edgeMines or redBonusStats.lastMineHits or redBonusStats.biggestReveal) %} + {% set hasBlueStats = blueBonusStats is not empty and (blueBonusStats.blindHits or blueBonusStats.chainBest or blueBonusStats.edgeMines or blueBonusStats.lastMineHits or blueBonusStats.biggestReveal) %} + {% if redBonusPoints > 0 or blueBonusPoints > 0 or hasRedStats or hasBlueStats %} +
+
+ Bonus Statistics +
+
+ {# Red Bonus #} +
+
+ {{ redBonusPoints|number_format(1, '.', '') }} + pts +
+
+ {% if redBonusStats is not empty and redBonusStats.blindHits %} +
+ Blind hits + {{ redBonusStats.blindHits }} +
+ {% endif %} + {% if redBonusStats is not empty and redBonusStats.chainBest %} +
+ Best chain + {{ redBonusStats.chainBest }} +
+ {% endif %} + {% if redBonusStats is not empty and redBonusStats.edgeMines %} +
+ Edge mines + {{ redBonusStats.edgeMines }} +
+ {% endif %} + {% if redBonusStats is not empty and redBonusStats.lastMineHits %} +
+ Endgame mines + {{ redBonusStats.lastMineHits }} +
+ {% endif %} + {% if redBonusStats is not empty and redBonusStats.biggestReveal %} +
+ Biggest reveal + {{ redBonusStats.biggestReveal }} +
+ {% endif %} + {% if not hasRedStats %} +
+ No bonuses earned +
+ {% endif %} +
+
+ + {# Blue Bonus #} +
+
+ {{ blueBonusPoints|number_format(1, '.', '') }} + pts +
+
+ {% if blueBonusStats is not empty and blueBonusStats.blindHits %} +
+ Blind hits + {{ blueBonusStats.blindHits }} +
+ {% endif %} + {% if blueBonusStats is not empty and blueBonusStats.chainBest %} +
+ Best chain + {{ blueBonusStats.chainBest }} +
+ {% endif %} + {% if blueBonusStats is not empty and blueBonusStats.edgeMines %} +
+ Edge mines + {{ blueBonusStats.edgeMines }} +
+ {% endif %} + {% if blueBonusStats is not empty and blueBonusStats.lastMineHits %} +
+ Endgame mines + {{ blueBonusStats.lastMineHits }} +
+ {% endif %} + {% if blueBonusStats is not empty and blueBonusStats.biggestReveal %} +
+ Biggest reveal + {{ blueBonusStats.biggestReveal }} +
+ {% endif %} + {% if not hasBlueStats %} +
+ No bonuses earned +
+ {% endif %} +
+
+
+
+ {% endif %}