From ca7f7c7b64df61262fa0445bfb4dc744d5aecb9a Mon Sep 17 00:00:00 2001 From: Zyki Date: Tue, 27 May 2025 23:14:49 +0200 Subject: [PATCH 01/13] feat: prepare gym defenders backend --- packages/types/lib/scanner.d.ts | 13 +++++++++++++ server/src/graphql/typeDefs/scanner.graphql | 1 + server/src/models/Gym.js | 4 ++++ src/services/queries/gym.js | 1 + 4 files changed, 19 insertions(+) diff --git a/packages/types/lib/scanner.d.ts b/packages/types/lib/scanner.d.ts index 125b8a75f..a8ded4b95 100644 --- a/packages/types/lib/scanner.d.ts +++ b/packages/types/lib/scanner.d.ts @@ -40,6 +40,18 @@ export interface PokemonDisplay { location_card: number } +export interface Defender extends PokemonDisplay { + pokemon_id: number + deployed_ms: number + deployed_time: number + battles_won: number + battles_lost: number + times_fed: number + motivation_now: number + cp_when_deployed: number + cp_now: number +} + export interface Gym { id: string lat: number @@ -54,6 +66,7 @@ export interface Gym { updated: number guarding_pokemon_id: number guarding_pokemon_display: PokemonDisplay + defenders: Defender[] available_slots: number team_id: number raid_level: number diff --git a/server/src/graphql/typeDefs/scanner.graphql b/server/src/graphql/typeDefs/scanner.graphql index fbd60c854..d2e0e607a 100644 --- a/server/src/graphql/typeDefs/scanner.graphql +++ b/server/src/graphql/typeDefs/scanner.graphql @@ -23,6 +23,7 @@ type Gym { updated: Int guarding_pokemon_id: Int guarding_pokemon_display: JSON + defenders: JSON available_slots: Int team_id: Int raid_level: Int diff --git a/server/src/models/Gym.js b/server/src/models/Gym.js index 233d3cb67..0707ebe3a 100644 --- a/server/src/models/Gym.js +++ b/server/src/models/Gym.js @@ -27,6 +27,7 @@ const gymFields = [ 'in_battle', 'guarding_pokemon_id', 'guarding_pokemon_display', + 'defenders', 'total_cp', 'power_up_points', 'power_up_level', @@ -309,6 +310,9 @@ class Gym extends Model { gym.guarding_pokemon_display, ) } + if (typeof gym.defenders === 'string' && gym.defenders) { + newGym.defenders = JSON.parse(gym.defenders) + } } if ( onlyRaids && diff --git a/src/services/queries/gym.js b/src/services/queries/gym.js index 2a553068b..34835458a 100644 --- a/src/services/queries/gym.js +++ b/src/services/queries/gym.js @@ -23,6 +23,7 @@ const gym = gql` in_battle guarding_pokemon_id guarding_pokemon_display + defenders total_cp badge power_up_level From f3c9511aac1b6c5d7bc7a2851248d4ec56cd2868 Mon Sep 17 00:00:00 2001 From: Zyki Date: Sun, 1 Jun 2025 12:57:43 +0200 Subject: [PATCH 02/13] feat: front-end display gym defenders --- src/features/gym/GymPopup.jsx | 253 ++++++++++++++++++++++++++++++++++ 1 file changed, 253 insertions(+) diff --git a/src/features/gym/GymPopup.jsx b/src/features/gym/GymPopup.jsx index 6ecf6f19c..e715b5012 100644 --- a/src/features/gym/GymPopup.jsx +++ b/src/features/gym/GymPopup.jsx @@ -9,6 +9,8 @@ import MenuItem from '@mui/material/MenuItem' import Divider from '@mui/material/Divider' import Collapse from '@mui/material/Collapse' import Typography from '@mui/material/Typography' +import ArrowBackIosNewIcon from '@mui/icons-material/ArrowBackIosNew' +import FavoriteIcon from '@mui/icons-material/Favorite' import { useTranslation } from 'react-i18next' import { useSyncData } from '@features/webhooks' @@ -44,9 +46,27 @@ export function GymPopup({ hasRaid, hasHatched, raidIconUrl, ...gym }) { const { perms } = useMemory((s) => s.auth) const popups = useStorage((s) => s.popups) const ts = Math.floor(Date.now() / 1000) + const [showDefenders, setShowDefenders] = React.useState(false) useAnalytics('Popup', `Team ID: ${gym.team_id} Has Raid: ${hasRaid}`, 'Gym') + // If defenders modal is toggled, show only that + if (showDefenders) { + return ( + + + setShowDefenders(false)} /> + + + ) + } + return ( {gym.name} + {gym.defenders?.length > 0 && ( + + + + )} {perms.gyms && ( void }} param0 + */ +function DefendersModal({ gym, onClose }) { + const { t } = useTranslation() + const Icons = useMemory((s) => s.Icons) + const defenders = gym.defenders || [] + + return ( + + + + { + e.stopPropagation() + onClose() + }} + size="small" + > + + + + + {gym.name} + + + + {defenders.map((def) => { + const fullCP = def.cp_when_deployed + const currentCP = def.cp_now + const percent = Math.max(0, Math.min(1, currentCP / fullCP)) + + return ( +
+
+ {t(`poke_${def.pokemon_id}`)} +
+
+ + {t(`poke_${def.pokemon_id}`)} + + + CP: {currentCP} / {fullCP} + +
+
+ {/* Heart outline */} + + {/* Heart background */} + + {/* Heart fill */} + + {/* Heart cracks for rounds */} + + {/* Crack at 1/3 height (top) */} + + {/* Crack at 2/3 height (bottom, improved to fit heart) */} + + +
+
+ ) + })} +
+ + {t('last_updated')}:{' '} + {gym.updated + ? new Date(gym.updated * 1000).toLocaleString() + : t('unknown')} + +
+ ) +} + /** * * @param {{ From 14f8f97701a37eeb657e965bd90b18b12425def9 Mon Sep 17 00:00:00 2001 From: lenisko <10072920+lenisko@users.noreply.github.com> Date: Tue, 3 Jun 2025 02:29:38 +0200 Subject: [PATCH 03/13] feat(client): fixes on gym defenders popup --- src/features/gym/GymPopup.jsx | 109 +++++++++++++++++----------------- 1 file changed, 54 insertions(+), 55 deletions(-) diff --git a/src/features/gym/GymPopup.jsx b/src/features/gym/GymPopup.jsx index e715b5012..e3f9f4aaf 100644 --- a/src/features/gym/GymPopup.jsx +++ b/src/features/gym/GymPopup.jsx @@ -10,6 +10,7 @@ import Divider from '@mui/material/Divider' import Collapse from '@mui/material/Collapse' import Typography from '@mui/material/Typography' import ArrowBackIosNewIcon from '@mui/icons-material/ArrowBackIosNew' +import ShieldIcon from '@mui/icons-material/Shield' import FavoriteIcon from '@mui/icons-material/Favorite' import { useTranslation } from 'react-i18next' @@ -50,7 +51,6 @@ export function GymPopup({ hasRaid, hasHatched, raidIconUrl, ...gym }) { useAnalytics('Popup', `Team ID: ${gym.team_id} Has Raid: ${hasRaid}`, 'Gym') - // If defenders modal is toggled, show only that if (showDefenders) { return ( @@ -80,28 +80,6 @@ export function GymPopup({ hasRaid, hasHatched, raidIconUrl, ...gym }) { {gym.name}
- {gym.defenders?.length > 0 && ( - - - - )} {perms.gyms && ( )} - + {perms.gyms && ( @@ -187,7 +171,7 @@ function DefendersModal({ gym, onClose }) {
{t(`poke_${def.pokemon_id}`)}
{/* Heart outline */} @@ -285,8 +272,6 @@ function DefendersModal({ gym, onClose }) { style={{ color: 'transparent', position: 'absolute', - top: 0, - right: 0, width: 28, height: 28, stroke: 'white', @@ -301,8 +286,6 @@ function DefendersModal({ gym, onClose }) { color: 'white', opacity: 0.18, position: 'absolute', - top: 0, - right: 0, width: 28, height: 28, }} @@ -312,27 +295,22 @@ function DefendersModal({ gym, onClose }) { style={{ color: '#ff69b4', position: 'absolute', - top: 0, - right: 0, width: 28, height: 28, clipPath: `inset(${100 - percent * 100}% 0 0 0)`, transition: 'clip-path 0.3s', }} /> - {/* Heart cracks for rounds */} + {/* Heart cracks */} - {/* Crack at 1/3 height (top) */} - {/* Crack at 2/3 height (bottom, improved to fit heart) */} { +const GymFooter = ({ lat, lon, hasRaid, gym, setShowDefenders }) => { const darkMode = useStorage((s) => s.darkMode) const popups = useStorage((s) => s.popups) const perms = useMemory((s) => s.auth.perms) @@ -841,9 +820,15 @@ const GymFooter = ({ lat, lon, hasRaid }) => { } return ( - <> + {hasRaid && perms.raids && perms.gyms && ( - + handleExpandClick('raids')} size="large"> { .Icons.getMisc(popups.raids ? 'gyms' : 'raids')} alt={popups.raids ? 'gyms' : 'raids'} className={darkMode ? '' : 'darken-image'} - height={20} - width="auto" + height={24} + width={24} + style={{ objectFit: 'contain' }} /> )} - + + {gym.defenders?.length > 0 && ( + + { + e.stopPropagation() + setShowDefenders(true) + }} + size="large" + > + + + + )} {perms.gyms && ( - + handleExpandClick('extras')} @@ -871,7 +870,7 @@ const GymFooter = ({ lat, lon, hasRaid }) => { )} - + ) } From 7f240042fd3190099bbfce6284a4e92ec7ee757b Mon Sep 17 00:00:00 2001 From: lenisko <10072920+lenisko@users.noreply.github.com> Date: Tue, 3 Jun 2025 02:32:43 +0200 Subject: [PATCH 04/13] feat(client): restore comments on defenders code --- src/features/gym/GymPopup.jsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/features/gym/GymPopup.jsx b/src/features/gym/GymPopup.jsx index e3f9f4aaf..c7214cab0 100644 --- a/src/features/gym/GymPopup.jsx +++ b/src/features/gym/GymPopup.jsx @@ -51,6 +51,7 @@ export function GymPopup({ hasRaid, hasHatched, raidIconUrl, ...gym }) { useAnalytics('Popup', `Team ID: ${gym.team_id} Has Raid: ${hasRaid}`, 'Gym') + // If defenders modal is toggled, show only that if (showDefenders) { return ( @@ -301,7 +302,7 @@ function DefendersModal({ gym, onClose }) { transition: 'clip-path 0.3s', }} /> - {/* Heart cracks */} + {/* Heart cracks for rounds */} + {/* Crack at 1/3 height (top) */} + {/* Crack at 2/3 height (bottom, improved to fit heart) */} Date: Tue, 3 Jun 2025 02:48:39 +0200 Subject: [PATCH 05/13] feat(client): make deffenders popup more compact --- src/features/gym/GymPopup.jsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/features/gym/GymPopup.jsx b/src/features/gym/GymPopup.jsx index c7214cab0..ac2b12255 100644 --- a/src/features/gym/GymPopup.jsx +++ b/src/features/gym/GymPopup.jsx @@ -157,6 +157,7 @@ function DefendersModal({ gym, onClose }) { container direction="column" alignItems="stretch" + mt={-1} style={{ minWidth: 250, maxWidth: 350, padding: 8 }} > @@ -337,7 +338,8 @@ function DefendersModal({ gym, onClose }) { {t('last_updated')}:{' '} From fd0ee09687baf1bd9eb6c3e7a44202db090f0136 Mon Sep 17 00:00:00 2001 From: lenisko <10072920+lenisko@users.noreply.github.com> Date: Tue, 3 Jun 2025 20:23:22 +0200 Subject: [PATCH 06/13] feat(client): better footer on gym popup --- src/features/gym/GymPopup.jsx | 116 ++++++++++++++++++++-------------- 1 file changed, 68 insertions(+), 48 deletions(-) diff --git a/src/features/gym/GymPopup.jsx b/src/features/gym/GymPopup.jsx index ac2b12255..29846eef7 100644 --- a/src/features/gym/GymPopup.jsx +++ b/src/features/gym/GymPopup.jsx @@ -824,57 +824,77 @@ const GymFooter = ({ lat, lon, hasRaid, gym, setShowDefenders }) => { })) } + const buttons = [] + + if (hasRaid && perms.raids && perms.gyms) { + buttons.push({ + key: 'raids', + element: ( + handleExpandClick('raids')} size="large"> + {popups.raids + + ), + }) + } + + if (gym.defenders?.length > 0) { + buttons.push({ + key: 'defenders', + element: ( + { + e.stopPropagation() + setShowDefenders(true) + }} + size="large" + > + + + ), + }) + } + + buttons.push({ + key: 'nav', + element: , + }) + + if (perms.gyms) { + buttons.push({ + key: 'extras', + element: ( + handleExpandClick('extras')} + size="large" + > + + + ), + }) + } + return ( - {hasRaid && perms.raids && perms.gyms && ( - - handleExpandClick('raids')} size="large"> - {popups.raids - - - )} - - - - {gym.defenders?.length > 0 && ( - - { - e.stopPropagation() - setShowDefenders(true) - }} - size="large" - > - - - - )} - {perms.gyms && ( - - handleExpandClick('extras')} - size="large" - > - - - - )} + {buttons.map(({ key, element }) => ( + {element} + ))} ) } From b187b46eb26331414e803ee2c7eaff53f5927d36 Mon Sep 17 00:00:00 2001 From: lenisko <10072920+lenisko@users.noreply.github.com> Date: Tue, 3 Jun 2025 20:37:35 +0200 Subject: [PATCH 07/13] feat(client): better spacing on gym popup --- src/features/gym/GymPopup.jsx | 114 ++++++++++++++++++++++------------ 1 file changed, 76 insertions(+), 38 deletions(-) diff --git a/src/features/gym/GymPopup.jsx b/src/features/gym/GymPopup.jsx index 29846eef7..0be17ea35 100644 --- a/src/features/gym/GymPopup.jsx +++ b/src/features/gym/GymPopup.jsx @@ -5,6 +5,7 @@ import MoreVert from '@mui/icons-material/MoreVert' import Grid from '@mui/material/Unstable_Grid2' import IconButton from '@mui/material/IconButton' import Menu from '@mui/material/Menu' +import Box from '@mui/material/Box' import MenuItem from '@mui/material/MenuItem' import Divider from '@mui/material/Divider' import Collapse from '@mui/material/Collapse' @@ -694,14 +695,22 @@ const RaidInfo = ({ } return ( - - - + + + {getRaidName(raid_level, raid_pokemon_id)} - - + + + {getRaidForm( raid_pokemon_id, raid_pokemon_form, @@ -709,44 +718,74 @@ const RaidInfo = ({ )} + + {/* Move 1 */} {raid_pokemon_move_1 && raid_pokemon_move_1 !== 1 && ( + container + alignItems="center" + justifyContent="center" + sx={{ maxWidth: '100%', flexWrap: 'nowrap' }} + spacing={1} + > + + + + + + {t(`move_${raid_pokemon_move_1}`)} + + + )} - - - {t(`move_${raid_pokemon_move_1}`)} - - + + {/* Move 2 */} {raid_pokemon_move_2 && raid_pokemon_move_2 !== 2 && ( + container + alignItems="center" + justifyContent="center" + sx={{ maxWidth: '100%', flexWrap: 'nowrap' }} + spacing={1} + > + + + + + + {t(`move_${raid_pokemon_move_2}`)} + + + )} - - - {t(`move_${raid_pokemon_move_2}`)} - - ) } @@ -887,7 +926,6 @@ const GymFooter = ({ lat, lon, hasRaid, gym, setShowDefenders }) => { Date: Tue, 3 Jun 2025 22:30:36 +0200 Subject: [PATCH 08/13] feat(client): conditional gym defender heart lines & translate CP --- src/features/gym/GymPopup.jsx | 39 ++++++++++++++++++++--------------- 1 file changed, 22 insertions(+), 17 deletions(-) diff --git a/src/features/gym/GymPopup.jsx b/src/features/gym/GymPopup.jsx index 0be17ea35..c6010bb26 100644 --- a/src/features/gym/GymPopup.jsx +++ b/src/features/gym/GymPopup.jsx @@ -254,7 +254,7 @@ function DefendersModal({ gym, onClose }) { {t(`poke_${def.pokemon_id}`)} - CP: {currentCP} / {fullCP} + {t('cp')}: {currentCP} / {fullCP}
- {/* Crack at 1/3 height (top) */} - - {/* Crack at 2/3 height (bottom, improved to fit heart) */} - + {/* Show cracks based on health: */} + {percent <= 2 / 3 && ( + // Always show top crack if percent <= 2/3 + + )} + {percent <= 1 / 3 && ( + // Show bottom crack only if percent <= 1/3 + + )}
From 6604029d6058b48d0b951b3d55f4ca9749cccc95 Mon Sep 17 00:00:00 2001 From: lenisko <10072920+lenisko@users.noreply.github.com> Date: Wed, 4 Jun 2025 17:29:15 +0200 Subject: [PATCH 09/13] feat(client): drop defenders from extra info, fix defenders hearth calculation --- src/features/gym/GymPopup.jsx | 52 ++++------------------------------- 1 file changed, 6 insertions(+), 46 deletions(-) diff --git a/src/features/gym/GymPopup.jsx b/src/features/gym/GymPopup.jsx index c6010bb26..00bb3bad0 100644 --- a/src/features/gym/GymPopup.jsx +++ b/src/features/gym/GymPopup.jsx @@ -20,14 +20,13 @@ import { useMemory } from '@store/useMemory' import { useLayoutStore } from '@store/useLayoutStore' import { setDeepStore, useStorage } from '@store/useStorage' import { ErrorBoundary } from '@components/ErrorBoundary' -import { Img, TextWithIcon } from '@components/Img' +import { Img } from '@components/Img' import { Title } from '@components/popups/Title' import { PowerUp } from '@components/popups/PowerUp' import { GenderIcon } from '@components/popups/GenderIcon' import { Navigation } from '@components/popups/Navigation' import { Coords } from '@components/popups/Coords' import { TimeStamp } from '@components/popups/TimeStamps' -import { ExtraInfo } from '@components/popups/ExtraInfo' import { useAnalytics } from '@hooks/useAnalytics' import { getTimeUntil } from '@utils/getTimeUntil' import { formatInterval } from '@utils/formatInterval' @@ -191,7 +190,10 @@ function DefendersModal({ gym, onClose }) { {defenders.map((def) => { const fullCP = def.cp_when_deployed const currentCP = def.cp_now - const percent = Math.max(0, Math.min(1, currentCP / fullCP)) + const percent = Math.max( + 0, + Math.min(1, (currentCP / fullCP) * 1.25 - 0.25), + ) return (
{ * @param {import('@rm/types').Gym} props * @returns */ -const ExtraGymInfo = ({ - last_modified_timestamp, - lat, - lon, - updated, - total_cp, - guarding_pokemon_id, - guarding_pokemon_display, -}) => { - const { t, i18n } = useTranslation() - const Icons = useMemory((s) => s.Icons) - const gymValidDataLimit = useMemory((s) => s.gymValidDataLimit) +const ExtraGymInfo = ({ last_modified_timestamp, lat, lon, updated }) => { const enableGymPopupCoords = useStorage( (s) => s.userSettings.gyms.enableGymPopupCoords, ) - const numFormatter = new Intl.NumberFormat(i18n.language) - /** @type {Partial} */ - const gpd = guarding_pokemon_display || {} - return ( - {!!guarding_pokemon_id && updated > gymValidDataLimit && ( - - - {gpd.badge === 1 && ( - <> - {t('best_buddy')} -   - - )} - {t(`poke_${guarding_pokemon_id}`)} - - - )} - {!!total_cp && updated > gymValidDataLimit && ( - {numFormatter.format(total_cp)} - )} - last_seen last_modified {enableGymPopupCoords && ( From cf9e4129daa8de175e7321fcdf41dca23c96de36 Mon Sep 17 00:00:00 2001 From: Mygod Date: Sun, 6 Jul 2025 22:54:58 -0700 Subject: [PATCH 10/13] fix: support light mode for motivation display Thanks Claude Sonnet 4! --- src/features/gym/GymPopup.jsx | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/features/gym/GymPopup.jsx b/src/features/gym/GymPopup.jsx index 00bb3bad0..7ac28ee37 100644 --- a/src/features/gym/GymPopup.jsx +++ b/src/features/gym/GymPopup.jsx @@ -13,6 +13,7 @@ import Typography from '@mui/material/Typography' import ArrowBackIosNewIcon from '@mui/icons-material/ArrowBackIosNew' import ShieldIcon from '@mui/icons-material/Shield' import FavoriteIcon from '@mui/icons-material/Favorite' +import { useTheme } from '@mui/material/styles' import { useTranslation } from 'react-i18next' import { useSyncData } from '@features/webhooks' @@ -149,6 +150,7 @@ export function GymPopup({ hasRaid, hasHatched, raidIconUrl, ...gym }) { */ function DefendersModal({ gym, onClose }) { const { t } = useTranslation() + const theme = useTheme() const Icons = useMemory((s) => s.Icons) const defenders = gym.defenders || [] @@ -279,7 +281,7 @@ function DefendersModal({ gym, onClose }) { position: 'absolute', width: 28, height: 28, - stroke: 'white', + stroke: theme.palette.text.primary, strokeWidth: 1, filter: 'drop-shadow(0 0 1px #0008)', }} @@ -288,7 +290,7 @@ function DefendersModal({ gym, onClose }) { {/* Heart background */} Date: Sun, 6 Jul 2025 23:02:38 -0700 Subject: [PATCH 11/13] fix: defender info updated time --- src/features/gym/GymPopup.jsx | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/features/gym/GymPopup.jsx b/src/features/gym/GymPopup.jsx index 7ac28ee37..172954787 100644 --- a/src/features/gym/GymPopup.jsx +++ b/src/features/gym/GymPopup.jsx @@ -153,6 +153,9 @@ function DefendersModal({ gym, onClose }) { const theme = useTheme() const Icons = useMemory((s) => s.Icons) const defenders = gym.defenders || [] + const updatedMs = + defenders.length && + defenders[0].deployed_ms + defenders[0].deployed_time * 1000 return ( {t('last_updated')}:{' '} - {gym.updated - ? new Date(gym.updated * 1000).toLocaleString() - : t('unknown')} + {defenders.length ? new Date(updatedMs).toLocaleString() : t('unknown')} ) From 644c01ecb2e24bae89791a69cd7d756d920e50a8 Mon Sep 17 00:00:00 2001 From: Mygod Date: Sun, 6 Jul 2025 23:13:39 -0700 Subject: [PATCH 12/13] feat: predict motivation now --- src/features/gym/GymPopup.jsx | 24 ++++++++++++++++-------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/src/features/gym/GymPopup.jsx b/src/features/gym/GymPopup.jsx index 172954787..8476031ec 100644 --- a/src/features/gym/GymPopup.jsx +++ b/src/features/gym/GymPopup.jsx @@ -156,6 +156,7 @@ function DefendersModal({ gym, onClose }) { const updatedMs = defenders.length && defenders[0].deployed_ms + defenders[0].deployed_time * 1000 + const now = Date.now() return ( {defenders.map((def) => { const fullCP = def.cp_when_deployed - const currentCP = def.cp_now - const percent = Math.max( + const decayTime = + 60 * + 60 * + 1000 * + Math.min(Math.max(Math.log10(3000 / fullCP), 8), 72) + const predictedMotivation = Math.max( 0, - Math.min(1, (currentCP / fullCP) * 1.25 - 0.25), + def.motivation_now - Math.max(0, now - updatedMs) / decayTime, + ) + const currentCP = Math.round( + fullCP * (0.2 + 0.8 * predictedMotivation), ) return ( @@ -307,7 +315,7 @@ function DefendersModal({ gym, onClose }) { position: 'absolute', width: 28, height: 28, - clipPath: `inset(${100 - percent * 100}% 0 0 0)`, + clipPath: `inset(${100 - predictedMotivation * 100}% 0 0 0)`, transition: 'clip-path 0.3s', }} /> @@ -322,8 +330,8 @@ function DefendersModal({ gym, onClose }) { }} > {/* Show cracks based on health: */} - {percent <= 2 / 3 && ( - // Always show top crack if percent <= 2/3 + {predictedMotivation <= 2 / 3 && ( + // Always show top crack if predictedMotivation <= 2/3 )} - {percent <= 1 / 3 && ( - // Show bottom crack only if percent <= 1/3 + {predictedMotivation <= 1 / 3 && ( + // Show bottom crack only if predictedMotivation <= 1/3 Date: Sun, 6 Jul 2025 23:42:59 -0700 Subject: [PATCH 13/13] feat: display MOAR info --- src/features/gym/GymPopup.jsx | 105 ++++++++++++++++++++++++++++------ 1 file changed, 89 insertions(+), 16 deletions(-) diff --git a/src/features/gym/GymPopup.jsx b/src/features/gym/GymPopup.jsx index 8476031ec..d820d4a18 100644 --- a/src/features/gym/GymPopup.jsx +++ b/src/features/gym/GymPopup.jsx @@ -13,6 +13,9 @@ import Typography from '@mui/material/Typography' import ArrowBackIosNewIcon from '@mui/icons-material/ArrowBackIosNew' import ShieldIcon from '@mui/icons-material/Shield' import FavoriteIcon from '@mui/icons-material/Favorite' +import EmojiEventsIcon from '@mui/icons-material/EmojiEvents' +import SentimentVeryDissatisfiedIcon from '@mui/icons-material/SentimentVeryDissatisfied' +import RestaurantIcon from '@mui/icons-material/Restaurant' import { useTheme } from '@mui/material/styles' import { useTranslation } from 'react-i18next' @@ -34,6 +37,26 @@ import { formatInterval } from '@utils/formatInterval' import { useWebhook } from './useWebhook' +/** + * Format deployed time as either "Xd Xh Xm" or "X:X:X" format + * @param {number} intervalMs - Time interval in milliseconds + * @returns {string} Formatted time string + */ +function formatDeployedTime(intervalMs) { + const totalSeconds = Math.floor(intervalMs / 1000) + const days = Math.floor(totalSeconds / 86400) + const hours = Math.floor((totalSeconds % 86400) / 3600) + const minutes = Math.floor((totalSeconds % 3600) / 60) + + if (days > 0) { + // Format as "Xd Xh Xm" + return `${days}d ${hours}h ${minutes}m` + } + // Format as "X:X:X" (HH:MM:SS) + const seconds = totalSeconds % 60 + return `${hours}:${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}` +} + /** * * @param {{ @@ -214,20 +237,21 @@ function DefendersModal({ gym, onClose }) { style={{ display: 'flex', alignItems: 'center', - minHeight: 60, + minHeight: 80, width: '100%', - padding: '4px 0', + padding: '8px 0', + borderBottom: `1px solid ${theme.palette.divider}`, }} >
@@ -235,8 +259,8 @@ function DefendersModal({ gym, onClose }) { src={Icons.getPokemonByDisplay(def.pokemon_id, def)} alt={t(`poke_${def.pokemon_id}`)} style={{ - maxHeight: 44, - maxWidth: 44, + maxHeight: 50, + maxWidth: 50, objectFit: 'contain', }} /> @@ -252,25 +276,74 @@ function DefendersModal({ gym, onClose }) { textAlign: 'left', overflow: 'hidden', marginLeft: 4, + gap: '4px', }} > - {t(`poke_${def.pokemon_id}`)} - - - {t('cp')}: {currentCP} / {fullCP} - +
+ +
+
+ + {def.battles_won || 0} +
+
+ + {def.battles_lost || 0} +
+
+ + {def.times_fed || 0} +
+
+ +
+ CP{currentCP}/{fullCP}{' '} + {formatDeployedTime(def.deployed_ms + now - updatedMs)} +