• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

DaniSomoza / galactic-commander / 15029782185

14 May 2025 07:52PM UTC coverage: 47.664% (-4.4%) from 52.086%
15029782185

Pull #12

github

web-flow
Merge d58e631f3 into a8e301a23
Pull Request #12: [fleets] Explore planets

216 of 930 branches covered (23.23%)

Branch coverage included in aggregate %.

162 of 529 new or added lines in 56 files covered. (30.62%)

10 existing lines in 9 files now uncovered.

1569 of 2815 relevant lines covered (55.74%)

3.43 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

1.38
/packages/frontend/src/components/dialogs/BuildUnitsDialog.tsx
1
import { useState, Dispatch, SetStateAction } from 'react'
2
import Button from '@mui/material/Button'
3
import Dialog from '@mui/material/Dialog'
4
import DialogTitle from '@mui/material/DialogTitle'
5
import DialogContent from '@mui/material/DialogContent'
6
import DialogActions from '@mui/material/DialogActions'
7
import IconButton from '@mui/material/IconButton'
8
import CloseIcon from '@mui/icons-material/Close'
9
import Typography from '@mui/material/Typography'
10
import Box from '@mui/material/Box'
11
import Stack from '@mui/material/Stack'
12
import Paper from '@mui/material/Paper'
13
import Tooltip from '@mui/material/Tooltip'
14
import TextField from '@mui/material/TextField'
15
import InputAdornment from '@mui/material/InputAdornment'
16
import StarsIcon from '@mui/icons-material/Stars'
17
import BoltRoundedIcon from '@mui/icons-material/BoltRounded'
18
import AlarmIcon from '@mui/icons-material/Alarm'
19
import DiamondIcon from '@mui/icons-material/Diamond'
20
import GroupIcon from '@mui/icons-material/Group'
21
import RocketIcon from '@mui/icons-material/Rocket'
22
import FortIcon from '@mui/icons-material/Fort'
23

24
import { UnitType, UnitTypes } from 'game-api-microservice/src/types/Unit'
25
import isHeroAlreadyBuild from 'game-engine/src/engine/units/isHeroAlreadyBuild'
26
import computedBonus from 'game-engine/src/engine/bonus/computedBonus'
27
import calculateMaxPlayerEnergy from 'game-engine/src/engine/units/calculateMaxPlayerEnergy'
28
import calculateCurrentPlayerEnergy from 'game-engine/src/engine/units/calculateCurrentPlayerEnergy'
29
import calculateCurrentPlayerPopulation from 'game-engine/src/engine/units/calculateCurrentPlayerPopulation'
30
import calculateMaxPlayerPopulation from 'game-engine/src/engine/units/calculateMaxPlayerPopulation'
31
import checkUnitRequirements from 'game-engine/src/engine/units/checkUnitRequirements'
32
import { IBonus } from 'game-engine/src/types/IBonus'
33

34
import { useBuildUnits } from '../../store/buildUnitsContext'
35
import { usePlayer } from '../../store/PlayerContext'
36
import { useTranslations } from '../../store/TranslationContext'
37
import formatTimer from '../../utils/formatTimer'
38
import formatNumber from '../../utils/formatNumber'
39
import millisToSeconds from '../../utils/millisToSeconds'
40
import UnitStats from '../unit-stats/UnitStats'
41
import { usePlayerResources } from '../../store/PlayerResourcesContext'
42
import formatCoordinatesLabel from '../../utils/formatPlanetCoordinates'
43
import { useTheme } from '../../store/ThemeContext'
44
import UnitRequirements from '../unit-requirements/UnitRequirements'
45
import UnitBonus from '../unit-bonus/UnitBonus'
46
import UnitCard from '../unit-card/UnitCard'
47
import { useFleet } from '../../store/FleetContext'
48

49
type BuildUnitDialogProps = {
50
  unitToBuild: UnitType
51
  setUnitToBuild: Dispatch<SetStateAction<UnitType | undefined>>
52
  isOpen: boolean
53
}
54

55
const unitIcon = {
1✔
56
  TROOP: GroupIcon,
57
  SPACESHIP: RocketIcon,
58
  DEFENSE: FortIcon
59
}
60

61
const MAX_INPUT_AMOUNT = 10_000_000
1✔
62

63
function BuildUnitsDialog({ unitToBuild, isOpen, setUnitToBuild }: BuildUnitDialogProps) {
64
  const [isLoading, setIsLoading] = useState<boolean>(false)
×
65
  const [amount, setAmount] = useState<number>(unitToBuild.isHero ? 1 : 0)
×
66

67
  const { theme } = useTheme()
×
68

69
  const { translate } = useTranslations()
×
70

71
  const { selectedPlanet, player } = usePlayer()
×
72

73
  const { resources } = usePlayerResources()
×
74

75
  const {
76
    starBuildUnits,
77
    updateBuildUnitsQueue,
78
    activeBuildTroops,
79
    activeBuildSpaceships,
80
    activeBuildDefenses
81
  } = useBuildUnits()
×
82

NEW
83
  const { unitsInThePlanet } = useFleet()
×
84

UNCOV
85
  const UnitIconComponent = unitIcon[unitToBuild.type]
×
86

87
  const resourceCost = amount * unitToBuild.resourceCost
×
NEW
88
  const currentPopulation = player
×
89
    ? calculateCurrentPlayerPopulation(player, player.units, player.fleets)
90
    : 0
91
  const predictedPopulation = currentPopulation + amount
×
92
  const maxPlayerPopulation = calculateMaxPlayerPopulation(player!)
×
NEW
93
  const currentEnergy = player
×
94
    ? calculateCurrentPlayerEnergy(player, player.units, player.fleets)
95
    : 0
96
  const predictedEnergy = currentEnergy + unitToBuild.energyCost * amount
×
97
  const maxPlayerEnergy = calculateMaxPlayerEnergy(player!)
×
98

99
  const selectedPlanetLabel = formatCoordinatesLabel(selectedPlanet!.coordinates)
×
100
  const planetResources = resources[selectedPlanetLabel]
×
101

102
  const isValidAmount = unitToBuild.isHero ? amount === 1 : amount >= 1
×
103
  const hasEnoughResources = planetResources >= resourceCost
×
104
  const isValidPopulation =
105
    unitToBuild.type !== 'TROOP' || maxPlayerPopulation >= predictedPopulation
×
106
  const isValidEnergy = maxPlayerEnergy >= predictedEnergy
×
107

108
  const unitRequirements = checkUnitRequirements(unitToBuild, player!)
×
109

110
  async function performUpdateBuildUnitsQueue() {
111
    setIsLoading(true)
×
112
    await updateBuildUnitsQueue(unitToBuild.name, amount, unitToBuild.type)
×
113
    setIsLoading(false)
×
114
    setAmount(0)
×
115
    handleClose()
×
116
  }
117

118
  async function performStartBuildUnits() {
119
    setIsLoading(true)
×
120
    await starBuildUnits(unitToBuild.name, amount)
×
121
    setIsLoading(false)
×
122
    setAmount(0)
×
123
    handleClose()
×
124
  }
125

126
  function handleClose() {
127
    setUnitToBuild(undefined)
×
128
  }
129

130
  const buildUnitsPerk: Record<UnitTypes, keyof IBonus> = {
×
131
    TROOP: 'TROOPS_TRAINING_BONUS',
132
    SPACESHIP: 'TROOPS_TRAINING_BONUS',
133
    DEFENSE: 'TROOPS_TRAINING_BONUS'
134
  }
135

136
  const buildUnitBonus = computedBonus(player!.perks, buildUnitsPerk[unitToBuild.type])
×
137
  const buildUnitDuration = millisToSeconds(unitToBuild.buildBaseTime * (100 / buildUnitBonus))
×
138

139
  const amountOfUnitsInThePlanet =
NEW
140
    unitsInThePlanet.find(({ unit }) => unit.name === unit.name)?.amount || 0
×
141

142
  const error = getErrorLabel({
×
143
    isValidAmount,
144
    hasEnoughResources,
145
    isValidPopulation,
146
    isValidEnergy
147
  })
148

149
  const showErrorLabel = !!amount && !!error
×
150
  const showQueueButton =
151
    (unitToBuild.type === 'TROOP' && activeBuildTroops) ||
×
152
    (unitToBuild.type === 'SPACESHIP' && activeBuildSpaceships) ||
153
    (unitToBuild.type === 'DEFENSE' && activeBuildDefenses)
154

155
  return (
×
156
    <Dialog onClose={handleClose} open={isOpen}>
157
      <DialogTitle sx={{ m: 0, p: 2 }} id="customized-dialog-title">
158
        Build {translate(unitToBuild.name)}
159
      </DialogTitle>
160
      <IconButton
161
        aria-label="close"
162
        onClick={handleClose}
163
        sx={(theme) => ({
×
164
          position: 'absolute',
165
          right: 8,
166
          top: 8,
167
          color: theme.palette.grey[500]
168
        })}
169
      >
170
        <CloseIcon />
171
      </IconButton>
172

173
      <DialogContent dividers>
174
        <Paper>
175
          <Stack padding={1} direction={'row'} justifyContent={'center'}>
176
            <UnitCard
177
              height={230}
178
              width={230}
179
              unit={unitToBuild}
180
              amount={amountOfUnitsInThePlanet}
181
              isAvailable
182
            />
183
          </Stack>
184
        </Paper>
185

186
        <Paper sx={{ marginTop: 1, padding: 1 }}>
187
          <Typography fontSize={14}>{translate(unitToBuild.description)}</Typography>
188
        </Paper>
189

190
        <Paper sx={{ marginTop: 1 }}>
191
          <Stack direction={'row'} gap={1} padding={1} justifyContent={'center'}>
192
            <Box flexBasis={'50%'}>
193
              <Stack gap={1}>
194
                {/* Requirements Part */}
195
                <UnitRequirements unitRequirements={unitRequirements} unitName={unitToBuild.name} />
196

197
                {/* Unit bonus */}
198
                <UnitBonus bonus={unitToBuild.bonus} />
199
              </Stack>
200
            </Box>
201

202
            {/* Stats Part */}
203
            <Box flexBasis={'50%'}>
204
              <UnitStats unit={unitToBuild} player={player!} />
205
            </Box>
206
          </Stack>
207
        </Paper>
208

209
        {/* TODO: ADD TOOLTIP */}
210
        {/* TODO: ADD 25% button... */}
211

212
        <Box minHeight={120} marginTop={1}>
213
          <Paper sx={{ padding: 1 }}>
214
            <Stack direction={'row'} justifyContent={'space-between'} gap={1}>
215
              <Box flexBasis={'50%'}>
216
                <Box paddingBottom={0} paddingTop={2}>
217
                  <TextField
218
                    label={'Amount of units to build'}
219
                    helperText={showErrorLabel ? error : ''}
×
220
                    disabled={unitToBuild.isHero}
221
                    fullWidth
222
                    placeholder="type an amount"
223
                    error={showErrorLabel}
224
                    value={amount || ''}
×
225
                    onChange={(event) => {
226
                      const value = Number(event.target.value)
×
227
                      const isNaN = Number.isNaN(value)
×
228

229
                      if (!isNaN) {
×
230
                        const isMax = value >= MAX_INPUT_AMOUNT
×
231

232
                        setAmount(isMax ? MAX_INPUT_AMOUNT : value)
×
233
                      }
234
                    }}
235
                    slotProps={{
236
                      input: {
237
                        startAdornment: (
238
                          <InputAdornment position="start">
239
                            <Stack direction={'row'} gap={0.5} alignItems={'center'}>
240
                              {unitToBuild.isHero && <StarsIcon color="info" />}
×
241
                              <UnitIconComponent fontSize="small" />
242
                            </Stack>
243
                          </InputAdornment>
244
                        ),
245
                        endAdornment: (
246
                          <InputAdornment position="end">
247
                            <Tooltip title={'max amount of units'} arrow placement="top">
248
                              <Button
249
                                aria-label={'max amount of units'}
250
                                variant="outlined"
251
                                disabled={unitToBuild.isHero}
252
                                onClick={() =>
253
                                  setAmount(
×
254
                                    getMaxAmountOfUnits({
255
                                      unit: unitToBuild,
256
                                      currentPopulation,
257
                                      currentEnergy,
258
                                      maxPlayerPopulation,
259
                                      maxPlayerEnergy,
260
                                      planetResources
261
                                    })
262
                                  )
263
                                }
264
                                size="small"
265
                              >
266
                                MAX
267
                              </Button>
268
                            </Tooltip>
269
                          </InputAdornment>
270
                        )
271
                      }
272
                    }}
273
                  />
274
                </Box>
275
              </Box>
276

277
              <Box flexBasis={'50%'}>
278
                <Stack direction={'row'} justifyContent={'center'} gap={0.5}>
279
                  <Stack flexBasis={'50%'} gap={0.5}>
280
                    <Paper
281
                      variant="outlined"
282
                      sx={{ borderColor: isValidPopulation ? undefined : theme.palette.error.main }}
×
283
                    >
284
                      <Stack direction={'row'} padding={0.5} alignItems={'center'}>
285
                        <GroupIcon
286
                          fontSize="small"
287
                          color={isValidPopulation ? 'inherit' : 'error'}
×
288
                        />
289

290
                        <Typography
291
                          variant="body1"
292
                          fontSize={12}
293
                          padding={0.4}
294
                          overflow={'hidden'}
295
                          textOverflow="ellipsis"
296
                          textAlign="center"
297
                          color={isValidPopulation ? 'textPrimary' : 'error'}
×
298
                        >
299
                          {calculateCurrentPlayerPopulation(
300
                            player!,
301
                            player!.units,
302
                            player!.fleets
303
                          ) + (unitToBuild.type === 'TROOP' ? amount : 0)}{' '}
×
304
                          / {formatNumber(calculateMaxPlayerPopulation(player!))}
305
                        </Typography>
306
                      </Stack>
307
                    </Paper>
308

309
                    <Paper
310
                      variant="outlined"
311
                      sx={{ borderColor: isValidEnergy ? undefined : theme.palette.error.main }}
×
312
                    >
313
                      <Stack direction={'row'} padding={0.5} alignItems={'center'}>
314
                        <BoltRoundedIcon
315
                          fontSize="small"
316
                          color={isValidEnergy ? 'inherit' : 'error'}
×
317
                        />
318

319
                        <Typography
320
                          variant="body1"
321
                          fontSize={12}
322
                          padding={0.4}
323
                          overflow={'hidden'}
324
                          textOverflow="ellipsis"
325
                          textAlign="center"
326
                          color={isValidEnergy ? 'textPrimary' : 'error'}
×
327
                        >
328
                          {calculateCurrentPlayerEnergy(player!, player!.units, player!.fleets) +
329
                            amount * unitToBuild.energyCost}{' '}
330
                          /{formatNumber(calculateMaxPlayerEnergy(player!))}
331
                        </Typography>
332
                      </Stack>
333
                    </Paper>
334
                  </Stack>
335

336
                  <Stack flexBasis={'50%'} gap={0.5}>
337
                    <Paper variant="outlined">
338
                      <Tooltip
339
                        title={translate(
340
                          'GAME_BUILD_UNITS_PAGE_BUILD_DURATION',
341
                          formatTimer(buildUnitDuration * amount)
342
                        )}
343
                        arrow
344
                      >
345
                        <Stack direction={'row'} padding={0.5} alignItems={'center'}>
346
                          <AlarmIcon fontSize="small" />
347

348
                          <Typography
349
                            variant="body1"
350
                            fontSize={12}
351
                            padding={0.4}
352
                            overflow={'hidden'}
353
                            textOverflow="ellipsis"
354
                            textAlign="center"
355
                          >
356
                            {formatTimer(buildUnitDuration * amount)}
357
                          </Typography>
358
                        </Stack>
359
                      </Tooltip>
360
                    </Paper>
361

362
                    <Tooltip
363
                      title={translate(
364
                        'UNIT_RESOURCE_COST',
365
                        formatNumber(unitToBuild.resourceCost * amount, true)
366
                      )}
367
                      arrow
368
                    >
369
                      <Paper
370
                        variant="outlined"
371
                        sx={{
372
                          borderColor: hasEnoughResources ? undefined : theme.palette.error.main
×
373
                        }}
374
                      >
375
                        <Stack direction={'row'} padding={0.5} alignItems={'center'}>
376
                          <DiamondIcon
377
                            fontSize="small"
378
                            color={hasEnoughResources ? 'inherit' : 'error'}
×
379
                          />
380

381
                          <Typography
382
                            variant="body1"
383
                            fontSize={12}
384
                            padding={0.4}
385
                            overflow={'hidden'}
386
                            textOverflow="ellipsis"
387
                            textAlign="center"
388
                            color={hasEnoughResources ? 'textPrimary' : 'error'}
×
389
                          >
390
                            {formatNumber(unitToBuild.resourceCost * amount, true)}
391
                          </Typography>
392
                        </Stack>
393
                      </Paper>
394
                    </Tooltip>
395
                  </Stack>
396
                </Stack>
397
              </Box>
398
            </Stack>
399
          </Paper>
400
        </Box>
401
      </DialogContent>
402

403
      <DialogActions>
404
        <Tooltip title={'Add units to planet queue'} arrow>
405
          <Button disabled={isLoading || !!error} autoFocus>
×
406
            Schedule
407
          </Button>
408
        </Tooltip>
409

410
        {showQueueButton ? (
×
411
          <Tooltip title={'Add units to planet queue'} arrow>
412
            <Button
413
              disabled={isLoading || !amount}
×
414
              autoFocus
415
              onClick={performUpdateBuildUnitsQueue}
416
            >
417
              Queue units
418
            </Button>
419
          </Tooltip>
420
        ) : (
421
          <Tooltip title={'Add units to planet queue'} arrow>
422
            <Button
423
              disabled={isLoading || !!error || isHeroAlreadyBuild(unitToBuild, player!.units)}
×
424
              autoFocus
425
              onClick={performStartBuildUnits}
426
            >
427
              Build Units
428
            </Button>
429
          </Tooltip>
430
        )}
431
      </DialogActions>
432
    </Dialog>
433
  )
434
}
435

436
export default BuildUnitsDialog
437

438
function getErrorLabel({
439
  isValidAmount,
440
  hasEnoughResources,
441
  isValidPopulation,
442
  isValidEnergy
443
}: {
444
  isValidAmount: boolean
445
  hasEnoughResources: boolean
446
  isValidPopulation: boolean
447
  isValidEnergy: boolean
448
}): string {
449
  if (!isValidAmount) {
×
450
    return 'invalid amount'
×
451
  }
452

453
  if (!hasEnoughResources) {
×
454
    return 'no enough resources'
×
455
  }
456

457
  if (!isValidPopulation) {
×
458
    return 'no enough population'
×
459
  }
460

461
  if (!isValidEnergy) {
×
462
    return 'no enough energy'
×
463
  }
464

465
  return ''
×
466
}
467

468
function getMaxAmountOfUnits({
469
  unit,
470
  currentPopulation,
471
  currentEnergy,
472
  maxPlayerPopulation,
473
  maxPlayerEnergy,
474
  planetResources
475
}: {
476
  unit: UnitType
477
  currentPopulation: number
478
  currentEnergy: number
479
  maxPlayerPopulation: number
480
  maxPlayerEnergy: number
481
  planetResources: number
482
}): number {
483
  const { resourceCost, type, isHero, energyCost } = unit
×
484

485
  if (isHero) {
×
486
    return 1
×
487
  }
488

489
  const maxAmountOfUnitsBasedOnResources = planetResources / resourceCost
×
490

491
  const hasEnergy = energyCost !== 0
×
492
  const availableEnergy = maxPlayerEnergy - currentEnergy
×
493
  const maxAmountOfUnitsBasedOnEnergy = hasEnergy ? availableEnergy / energyCost : MAX_INPUT_AMOUNT
×
494

495
  const isTroop = type === 'TROOP'
×
496

497
  if (isTroop) {
×
498
    const maxAmountOfUnitsBasedOnPopulation = maxPlayerPopulation - currentPopulation
×
499

500
    return Math.floor(
×
501
      Math.min(
502
        maxAmountOfUnitsBasedOnPopulation,
503
        maxAmountOfUnitsBasedOnEnergy,
504
        maxAmountOfUnitsBasedOnResources
505
      )
506
    )
507
  }
508

509
  return Math.floor(Math.min(maxAmountOfUnitsBasedOnEnergy, maxAmountOfUnitsBasedOnResources))
×
510
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc