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

DaniSomoza / galactic-commander / 12444891208

21 Dec 2024 11:46AM UTC coverage: 52.036% (-13.6%) from 65.587%
12444891208

Pull #11

github

web-flow
Merge 9b5ea0e56 into 4f9f087f0
Pull Request #11: Build units

206 of 789 branches covered (26.11%)

Branch coverage included in aggregate %.

366 of 898 new or added lines in 85 files covered. (40.76%)

10 existing lines in 7 files now uncovered.

1417 of 2330 relevant lines covered (60.82%)

3.82 hits per line

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

1.44
/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 computedBonus from 'game-engine/src/engine/bonus/computedBonus'
26
import getAmountOfPlayerUnitsInThePlanet from 'game-engine/src/engine/units/getAmountOfPlayerUnitsInThePlanet'
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 Image from '../image/Image'
41
import UnitStats from '../unit-stats/UnitStats'
42
import { usePlayerResources } from '../../store/PlayerResourcesContext'
43
import formatCoordinatesLabel from '../../utils/formatPlanetCoordinates'
44
import { useTheme } from '../../store/ThemeContext'
45
import UnitRequirements from '../unit-requirements/UnitRequirements'
46
import UnitBonus from '../unit-bonus/UnitBonus'
47
import getImage from '../../utils/getImage'
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) {
NEW
64
  const [isLoading, setIsLoading] = useState<boolean>(false)
×
NEW
65
  const [amount, setAmount] = useState<number>(unitToBuild.isHero ? 1 : 0)
×
66

NEW
67
  const { theme } = useTheme()
×
68

NEW
69
  const { translate } = useTranslations()
×
70

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

NEW
73
  const { resources } = usePlayerResources()
×
74

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

NEW
83
  const UnitIconComponent = unitIcon[unitToBuild.type]
×
84

NEW
85
  const resourceCost = amount * unitToBuild.resourceCost
×
NEW
86
  const currentPopulation = calculateCurrentPlayerPopulation(player!)
×
NEW
87
  const predictedPopulation = currentPopulation + amount
×
NEW
88
  const maxPlayerPopulation = calculateMaxPlayerPopulation(player!)
×
NEW
89
  const currentEnergy = calculateCurrentPlayerEnergy(player!)
×
NEW
90
  const predictedEnergy = currentEnergy + unitToBuild.energyCost * amount
×
NEW
91
  const maxPlayerEnergy = calculateMaxPlayerEnergy(player!)
×
92

NEW
93
  const selectedPlanetLabel = formatCoordinatesLabel(selectedPlanet!.coordinates)
×
NEW
94
  const planetResources = resources[selectedPlanetLabel]
×
95

NEW
96
  const isValidAmount = unitToBuild.isHero ? amount === 1 : amount >= 1
×
NEW
97
  const hasEnoughResources = planetResources >= resourceCost
×
98
  const isValidPopulation =
NEW
99
    unitToBuild.type !== 'TROOP' || maxPlayerPopulation >= predictedPopulation
×
NEW
100
  const isValidEnergy = maxPlayerEnergy >= predictedEnergy
×
101

NEW
102
  const unitRequirements = checkUnitRequirements(unitToBuild, player!)
×
103

104
  async function performUpdateBuildUnitsQueue() {
NEW
105
    setIsLoading(true)
×
NEW
106
    await updateBuildUnitsQueue(unitToBuild.name, amount, unitToBuild.type)
×
NEW
107
    setIsLoading(false)
×
NEW
108
    setAmount(0)
×
NEW
109
    handleClose()
×
110
  }
111

112
  async function performStartBuildUnits() {
NEW
113
    setIsLoading(true)
×
NEW
114
    await starBuildUnits(unitToBuild.name, amount)
×
NEW
115
    setIsLoading(false)
×
NEW
116
    setAmount(0)
×
NEW
117
    handleClose()
×
118
  }
119

120
  function handleClose() {
NEW
121
    setUnitToBuild(undefined)
×
122
  }
123

NEW
124
  const buildUnitsPerk: Record<UnitTypes, keyof IBonus> = {
×
125
    TROOP: 'TROOPS_TRAINING_BONUS',
126
    SPACESHIP: 'TROOPS_TRAINING_BONUS',
127
    DEFENSE: 'TROOPS_TRAINING_BONUS'
128
  }
129

NEW
130
  const buildUnitBonus = computedBonus(player!.perks, buildUnitsPerk[unitToBuild.type])
×
NEW
131
  const buildUnitDuration = millisToSeconds(unitToBuild.buildBaseTime * (100 / buildUnitBonus))
×
132

NEW
133
  const troopsInThisPlanet = getAmountOfPlayerUnitsInThePlanet(
×
134
    player!,
135
    selectedPlanet!,
136
    unitToBuild
137
  )
138

NEW
139
  const error = getErrorLabel({
×
140
    isValidAmount,
141
    hasEnoughResources,
142
    isValidPopulation,
143
    isValidEnergy
144
  })
145

NEW
146
  const showErrorLabel = !!amount && !!error
×
147
  const showQueueButton =
NEW
148
    (unitToBuild.type === 'TROOP' && activeBuildTroops) ||
×
149
    (unitToBuild.type === 'SPACESHIP' && activeBuildSpaceships) ||
150
    (unitToBuild.type === 'DEFENSE' && activeBuildDefenses)
151

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

170
      <DialogContent dividers>
171
        <Paper sx={{ padding: 1 }}>
172
          <Stack direction={'row'} justifyContent={'center'}>
173
            <Box sx={{ position: 'relative' }}>
174
              <Paper variant="outlined">
175
                <Stack justifyContent="center" alignItems="center">
176
                  <Image
177
                    src={getImage(unitToBuild.name)}
178
                    alt={translate(unitToBuild.name)}
179
                    height={'230px'}
180
                    width={'230px'}
181
                    border
182
                  />
183

184
                  {/* Unit name */}
185
                  <Box
186
                    position={'absolute'}
187
                    top={20}
188
                    padding={1}
189
                    maxWidth={'230px'}
190
                    sx={{ transform: 'translate(0, -50%)' }}
191
                  >
192
                    <Paper variant="outlined">
193
                      <Paper variant="outlined">
194
                        <Stack
195
                          direction={'row'}
196
                          gap={0.5}
197
                          padding={0.4}
198
                          paddingLeft={0.6}
199
                          paddingRight={0.8}
200
                          alignItems={'center'}
201
                        >
202
                          {unitToBuild.isHero && <StarsIcon fontSize="small" color="info" />}
×
203
                          <Typography variant="body1" fontSize={13}>
204
                            {translate(unitToBuild.name)}
205
                          </Typography>
206
                        </Stack>
207
                      </Paper>
208
                    </Paper>
209
                  </Box>
210

211
                  {/* Amount of units in this planet */}
212
                  <Box position={'absolute'} right={0} bottom={0} padding={1}>
213
                    <Paper variant="outlined">
214
                      <Tooltip
215
                        title={translate(
216
                          'GAME_BUILD_UNITS_PAGE_AMOUNT_OF_UNITS_IN_PLANET_TOOLTIP',
217
                          formatNumber(troopsInThisPlanet, true)
218
                        )}
219
                        arrow
220
                      >
221
                        <Stack
222
                          direction={'row'}
223
                          gap={0.5}
224
                          padding={0.4}
225
                          paddingLeft={0.6}
226
                          paddingRight={0.8}
227
                          alignItems={'center'}
228
                        >
229
                          <GroupIcon fontSize="small" />
230
                          <Typography fontSize={12}> {formatNumber(troopsInThisPlanet)}</Typography>
231
                        </Stack>
232
                      </Tooltip>
233
                    </Paper>
234
                  </Box>
235
                </Stack>
236
              </Paper>
237
            </Box>
238
          </Stack>
239
        </Paper>
240

241
        <Paper sx={{ marginTop: 1, padding: 1 }}>
242
          <Typography fontSize={14}>{translate(unitToBuild.description)}</Typography>
243
        </Paper>
244

245
        <Paper sx={{ marginTop: 1 }}>
246
          <Stack direction={'row'} gap={1} padding={1} justifyContent={'center'}>
247
            <Box flexBasis={'50%'}>
248
              <Stack gap={1}>
249
                {/* Requirements Part */}
250
                <UnitRequirements unitRequirements={unitRequirements} unitName={unitToBuild.name} />
251

252
                {/* Unit bonus */}
253
                <UnitBonus bonus={unitToBuild.bonus} />
254
              </Stack>
255
            </Box>
256

257
            {/* Stats Part */}
258
            <Box flexBasis={'50%'}>
259
              <UnitStats unit={unitToBuild} player={player!} />
260
            </Box>
261
          </Stack>
262
        </Paper>
263

264
        {/* TODO: ADD TOOLTIP */}
265

266
        <Box minHeight={120} marginTop={1}>
267
          <Paper sx={{ padding: 1 }}>
268
            <Stack direction={'row'} justifyContent={'space-between'} gap={1}>
269
              <Box flexBasis={'50%'}>
270
                <Box paddingBottom={0} paddingTop={2}>
271
                  <TextField
272
                    label={'Amount of units to build'}
273
                    helperText={showErrorLabel ? error : ''}
×
274
                    disabled={unitToBuild.isHero}
275
                    fullWidth
276
                    placeholder="type the amount"
277
                    error={showErrorLabel}
278
                    value={amount || ''}
×
279
                    onChange={(event) => {
NEW
280
                      const value = Number(event.target.value)
×
NEW
281
                      const isNaN = Number.isNaN(value)
×
282

NEW
283
                      if (!isNaN) {
×
NEW
284
                        const isMax = value >= MAX_INPUT_AMOUNT
×
285

NEW
286
                        setAmount(isMax ? MAX_INPUT_AMOUNT : value)
×
287
                      }
288
                    }}
289
                    slotProps={{
290
                      input: {
291
                        startAdornment: (
292
                          <InputAdornment position="start">
293
                            <Stack direction={'row'} gap={0.5} alignItems={'center'}>
294
                              {unitToBuild.isHero && <StarsIcon color="info" />}
×
295
                              <UnitIconComponent fontSize="small" />
296
                            </Stack>
297
                          </InputAdornment>
298
                        ),
299
                        endAdornment: (
300
                          <InputAdornment position="end">
301
                            <Tooltip title={'max amount of units'} arrow placement="top">
302
                              <Button
303
                                aria-label={'max amount of units'}
304
                                variant="outlined"
305
                                disabled={unitToBuild.isHero}
306
                                onClick={() =>
NEW
307
                                  setAmount(
×
308
                                    getMaxAmountOfUnits({
309
                                      unit: unitToBuild,
310
                                      currentPopulation,
311
                                      currentEnergy,
312
                                      maxPlayerPopulation,
313
                                      maxPlayerEnergy,
314
                                      planetResources
315
                                    })
316
                                  )
317
                                }
318
                                size="small"
319
                              >
320
                                MAX
321
                              </Button>
322
                            </Tooltip>
323
                          </InputAdornment>
324
                        )
325
                      }
326
                    }}
327
                  />
328
                </Box>
329
              </Box>
330

331
              <Box flexBasis={'50%'}>
332
                <Stack direction={'row'} justifyContent={'center'} gap={0.5}>
333
                  <Stack flexBasis={'50%'} gap={0.5}>
334
                    <Paper
335
                      variant="outlined"
336
                      sx={{ borderColor: isValidPopulation ? undefined : theme.palette.error.main }}
×
337
                    >
338
                      <Stack direction={'row'} padding={0.5} alignItems={'center'}>
339
                        <GroupIcon
340
                          fontSize="small"
341
                          color={isValidPopulation ? 'inherit' : 'error'}
×
342
                        />
343

344
                        <Typography
345
                          variant="body1"
346
                          fontSize={12}
347
                          padding={0.4}
348
                          overflow={'hidden'}
349
                          textOverflow="ellipsis"
350
                          textAlign="center"
351
                          color={isValidPopulation ? 'textPrimary' : 'error'}
×
352
                        >
353
                          {calculateCurrentPlayerPopulation(player!) +
354
                            (unitToBuild.type === 'TROOP' ? amount : 0)}{' '}
×
355
                          / {formatNumber(calculateMaxPlayerPopulation(player!))}
356
                        </Typography>
357
                      </Stack>
358
                    </Paper>
359

360
                    <Paper
361
                      variant="outlined"
362
                      sx={{ borderColor: isValidEnergy ? undefined : theme.palette.error.main }}
×
363
                    >
364
                      <Stack direction={'row'} padding={0.5} alignItems={'center'}>
365
                        <BoltRoundedIcon
366
                          fontSize="small"
367
                          color={isValidEnergy ? 'inherit' : 'error'}
×
368
                        />
369

370
                        <Typography
371
                          variant="body1"
372
                          fontSize={12}
373
                          padding={0.4}
374
                          overflow={'hidden'}
375
                          textOverflow="ellipsis"
376
                          textAlign="center"
377
                          color={isValidEnergy ? 'textPrimary' : 'error'}
×
378
                        >
379
                          {calculateCurrentPlayerEnergy(player!) + amount * unitToBuild.energyCost}{' '}
380
                          /{formatNumber(calculateMaxPlayerEnergy(player!))}
381
                        </Typography>
382
                      </Stack>
383
                    </Paper>
384
                  </Stack>
385

386
                  <Stack flexBasis={'50%'} gap={0.5}>
387
                    <Paper variant="outlined">
388
                      <Tooltip
389
                        title={translate(
390
                          'GAME_BUILD_UNITS_PAGE_BUILD_DURATION',
391
                          formatTimer(buildUnitDuration * amount)
392
                        )}
393
                        arrow
394
                      >
395
                        <Stack direction={'row'} padding={0.5} alignItems={'center'}>
396
                          <AlarmIcon fontSize="small" />
397

398
                          <Typography
399
                            variant="body1"
400
                            fontSize={12}
401
                            padding={0.4}
402
                            overflow={'hidden'}
403
                            textOverflow="ellipsis"
404
                            textAlign="center"
405
                          >
406
                            {formatTimer(buildUnitDuration * amount)}
407
                          </Typography>
408
                        </Stack>
409
                      </Tooltip>
410
                    </Paper>
411

412
                    <Tooltip
413
                      title={translate(
414
                        'UNIT_RESOURCE_COST',
415
                        formatNumber(unitToBuild.resourceCost * amount, true)
416
                      )}
417
                      arrow
418
                    >
419
                      <Paper
420
                        variant="outlined"
421
                        sx={{
422
                          borderColor: hasEnoughResources ? undefined : theme.palette.error.main
×
423
                        }}
424
                      >
425
                        <Stack direction={'row'} padding={0.5} alignItems={'center'}>
426
                          <DiamondIcon
427
                            fontSize="small"
428
                            color={hasEnoughResources ? 'inherit' : 'error'}
×
429
                          />
430

431
                          <Typography
432
                            variant="body1"
433
                            fontSize={12}
434
                            padding={0.4}
435
                            overflow={'hidden'}
436
                            textOverflow="ellipsis"
437
                            textAlign="center"
438
                            color={hasEnoughResources ? 'textPrimary' : 'error'}
×
439
                          >
440
                            {formatNumber(unitToBuild.resourceCost * amount, true)}
441
                          </Typography>
442
                        </Stack>
443
                      </Paper>
444
                    </Tooltip>
445
                  </Stack>
446
                </Stack>
447
              </Box>
448
            </Stack>
449
          </Paper>
450
        </Box>
451
      </DialogContent>
452

453
      <DialogActions>
454
        <Tooltip title={'Add units to planet queue'} arrow>
455
          <Button disabled={isLoading || !!error} autoFocus>
×
456
            Schedule
457
          </Button>
458
        </Tooltip>
459

460
        {showQueueButton ? (
×
461
          <Tooltip title={'Add units to planet queue'} arrow>
462
            <Button
463
              disabled={isLoading || !amount}
×
464
              autoFocus
465
              onClick={performUpdateBuildUnitsQueue}
466
            >
467
              Queue units
468
            </Button>
469
          </Tooltip>
470
        ) : (
471
          <Tooltip title={'Add units to planet queue'} arrow>
472
            <Button disabled={isLoading || !!error} autoFocus onClick={performStartBuildUnits}>
×
473
              Build Units
474
            </Button>
475
          </Tooltip>
476
        )}
477
      </DialogActions>
478
    </Dialog>
479
  )
480
}
481

482
export default BuildUnitsDialog
483

484
function getErrorLabel({
485
  isValidAmount,
486
  hasEnoughResources,
487
  isValidPopulation,
488
  isValidEnergy
489
}: {
490
  isValidAmount: boolean
491
  hasEnoughResources: boolean
492
  isValidPopulation: boolean
493
  isValidEnergy: boolean
494
}): string {
NEW
495
  if (!isValidAmount) {
×
NEW
496
    return 'invalid amount'
×
497
  }
498

NEW
499
  if (!hasEnoughResources) {
×
NEW
500
    return 'no enough resources'
×
501
  }
502

NEW
503
  if (!isValidPopulation) {
×
NEW
504
    return 'no enough population'
×
505
  }
506

NEW
507
  if (!isValidEnergy) {
×
NEW
508
    return 'no enough energy'
×
509
  }
510

NEW
511
  return ''
×
512
}
513

514
function getMaxAmountOfUnits({
515
  unit,
516
  currentPopulation,
517
  currentEnergy,
518
  maxPlayerPopulation,
519
  maxPlayerEnergy,
520
  planetResources
521
}: {
522
  unit: UnitType
523
  currentPopulation: number
524
  currentEnergy: number
525
  maxPlayerPopulation: number
526
  maxPlayerEnergy: number
527
  planetResources: number
528
}): number {
NEW
529
  const { resourceCost, type, isHero, energyCost } = unit
×
530

NEW
531
  if (isHero) {
×
NEW
532
    return 1
×
533
  }
534

NEW
535
  const maxAmountOfUnitsBasedOnResources = planetResources / resourceCost
×
536

NEW
537
  const hasEnergy = energyCost !== 0
×
NEW
538
  const availableEnergy = maxPlayerEnergy - currentEnergy
×
NEW
539
  const maxAmountOfUnitsBasedOnEnergy = hasEnergy ? availableEnergy / energyCost : MAX_INPUT_AMOUNT
×
540

NEW
541
  const isTroop = type === 'TROOP'
×
542

NEW
543
  if (isTroop) {
×
NEW
544
    const maxAmountOfUnitsBasedOnPopulation = maxPlayerPopulation - currentPopulation
×
545

NEW
546
    return Math.floor(
×
547
      Math.min(
548
        maxAmountOfUnitsBasedOnPopulation,
549
        maxAmountOfUnitsBasedOnEnergy,
550
        maxAmountOfUnitsBasedOnResources
551
      )
552
    )
553
  }
554

NEW
555
  return Math.floor(Math.min(maxAmountOfUnitsBasedOnEnergy, maxAmountOfUnitsBasedOnResources))
×
556
}
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