Scrutinizer GitHub App not installed

We could not synchronize checks via GitHub's checks API since Scrutinizer's GitHub App is not installed for this repository.

Install GitHub App

AbstractSmrShip::getShields()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 2
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 1
dl 0
loc 2
rs 10
c 0
b 0
f 0
cc 1
nc 1
nop 0
1
<?php declare(strict_types=1);
2
3
use Smr\HardwareType;
4
use Smr\ShipClass;
5
use Smr\ShipIllusion;
6
7
/**
8
 * Properties and methods for a ship instance.
9
 * Does not include the database layer (see SmrShip).
10
 */
11
class AbstractSmrShip {
12
13
	// Player exp gained for each point of damage done
14
	protected const EXP_PER_DAMAGE_PLAYER = 0.375;
15
	protected const EXP_PER_DAMAGE_PLANET = 1.0; // note that planet damage is reduced
16
	protected const EXP_PER_DAMAGE_PORT = 0.15;
17
	protected const EXP_PER_DAMAGE_FORCE = 0.075;
18
19
	protected const STARTER_SHIPS = [
20
		RACE_NEUTRAL => SHIP_TYPE_GALACTIC_SEMI,
21
		RACE_ALSKANT => SHIP_TYPE_SMALL_TIMER,
22
		RACE_CREONTI => SHIP_TYPE_MEDIUM_CARGO_HULK,
23
		RACE_HUMAN => SHIP_TYPE_LIGHT_FREIGHTER,
24
		RACE_IKTHORNE => SHIP_TYPE_TINY_DELIGHT,
25
		RACE_SALVENE => SHIP_TYPE_HATCHLINGS_DUE,
26
		RACE_THEVIAN => SHIP_TYPE_SWIFT_VENTURE,
27
		RACE_WQHUMAN => SHIP_TYPE_SLIP_FREIGHTER,
28
		RACE_NIJARIN => SHIP_TYPE_REDEEMER,
29
	];
30
31
	protected AbstractSmrPlayer $player;
32
33
	protected int $gameID;
34
	protected SmrShipType $shipType;
35
36
	/** @var array<int, SmrWeapon> */
37
	protected array $weapons = [];
38
	/** @var array<int, int> */
39
	protected array $cargo = [];
40
	/** @var array<int, int> */
41
	protected array $hardware = [];
42
	protected bool $isCloaked = false;
43
	protected ShipIllusion|false $illusionShip = false;
44
45
	protected bool $hasChangedWeapons = false;
46
	protected bool $hasChangedCargo = false;
47
	/** @var array<int, bool> */
48
	protected array $hasChangedHardware = [];
49
	protected bool $hasChangedCloak = false;
50
	protected bool $hasChangedIllusion = false;
51
52
	public function __construct(AbstractSmrPlayer $player) {
53
		$this->player = $player;
54
		$this->gameID = $player->getGameID();
55
		$this->regenerateShipType();
56
	}
57
58
	protected function regenerateShipType(): void {
59
		$this->shipType = SmrShipType::get($this->player->getShipTypeID());
60
	}
61
62
	public function checkForExcess(): void {
63
		$this->checkForExcessHardware();
64
		$this->checkForExcessWeapons();
65
		$this->checkForExcessCargo();
66
	}
67
68
	public function checkForExcessWeapons(): void {
69
		while ($this->hasWeapons() && ($this->getPowerUsed() > $this->getType()->getMaxPower() || $this->getNumWeapons() > $this->getHardpoints())) {
70
			//erase the first weapon 1 at a time until we are okay
71
			$this->removeLastWeapon();
72
		}
73
	}
74
75
	public function checkForExcessCargo(): void {
76
		if ($this->hasCargo()) {
77
			$excess = array_sum($this->getCargo()) - $this->getCargoHolds();
78
			foreach ($this->getCargo() as $goodID => $amount) {
79
				if ($excess > 0) {
80
					$decreaseAmount = min($amount, $excess);
81
					$this->decreaseCargo($goodID, $decreaseAmount);
82
					$excess -= $decreaseAmount;
83
				} else {
84
					// No more excess cargo
85
					break;
86
				}
87
			}
88
		}
89
	}
90
91
	public function checkForExcessHardware(): void {
92
		//check hardware to see if anything needs to be removed
93
		foreach ($this->getHardware() as $hardwareTypeID => $amount) {
94
			$max = $this->shipType->getMaxHardware($hardwareTypeID);
95
			$this->setHardware($hardwareTypeID, min($amount, $max));
96
		}
97
	}
98
99
	/**
100
	 * Set all hardware to its maximum value for this ship.
101
	 */
102
	public function setHardwareToMax(): void {
103
		foreach ($this->shipType->getAllMaxHardware() as $hardwareTypeID => $max) {
104
			$this->setHardware($hardwareTypeID, $max);
105
		}
106
	}
107
108
	public function getPowerUsed(): int {
109
		$power = 0;
110
		foreach ($this->weapons as $weapon) {
111
			$power += $weapon->getPowerLevel();
112
		}
113
		return $power;
114
	}
115
116
	public function getRemainingPower(): int {
117
		return $this->getType()->getMaxPower() - $this->getPowerUsed();
118
	}
119
120
	/**
121
	 * given power level of new weapon, return whether there is enough power available to install it on this ship
122
	 */
123
	public function checkPowerAvailable(int $powerLevel): bool {
124
		return $this->getRemainingPower() >= $powerLevel;
125
	}
126
127
	public function hasIllegalGoods(): bool {
128
		return $this->hasCargo(GOODS_SLAVES) || $this->hasCargo(GOODS_WEAPONS) || $this->hasCargo(GOODS_NARCOTICS);
129
	}
130
131
	public function getDisplayAttackRating(): int {
132
		if ($this->hasActiveIllusion()) {
133
			return $this->getIllusion()->attackRating;
134
		}
135
		return $this->getAttackRating();
136
	}
137
138
	public function getDisplayDefenseRating(): int {
139
		if ($this->hasActiveIllusion()) {
140
			return $this->getIllusion()->defenseRating;
141
		}
142
		return $this->getDefenseRating();
143
	}
144
145
	public function getDisplayName(): string {
146
		if ($this->hasActiveIllusion()) {
147
			return $this->getIllusion()->getName();
148
		}
149
		return $this->getName();
150
	}
151
152
	public function getAttackRating(): int {
153
		return IRound(($this->getTotalShieldDamage() + $this->getTotalArmourDamage() + $this->getCDs() * 2) / 40);
0 ignored issues
show
Bug introduced by
The function IRound was not found. Maybe you did not declare it correctly or list all dependencies? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

153
		return /** @scrutinizer ignore-call */ IRound(($this->getTotalShieldDamage() + $this->getTotalArmourDamage() + $this->getCDs() * 2) / 40);
Loading history...
154
	}
155
156
	public function getAttackRatingWithMaxCDs(): int {
157
		return IRound(($this->getTotalShieldDamage() + $this->getTotalArmourDamage() + $this->getMaxCDs() * 2) / 40);
0 ignored issues
show
Bug introduced by
The function IRound was not found. Maybe you did not declare it correctly or list all dependencies? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

157
		return /** @scrutinizer ignore-call */ IRound(($this->getTotalShieldDamage() + $this->getTotalArmourDamage() + $this->getMaxCDs() * 2) / 40);
Loading history...
158
	}
159
160
	public function getDefenseRating(): int {
161
		return IRound(($this->getShields() + $this->getArmour() + $this->getCDs() * CD_ARMOUR) / 100);
0 ignored issues
show
Bug introduced by
The function IRound was not found. Maybe you did not declare it correctly or list all dependencies? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

161
		return /** @scrutinizer ignore-call */ IRound(($this->getShields() + $this->getArmour() + $this->getCDs() * CD_ARMOUR) / 100);
Loading history...
162
	}
163
164
	public function getMaxDefenseRating(): int {
165
		return IRound(($this->getMaxShields() + $this->getMaxArmour() + $this->getMaxCDs() * CD_ARMOUR) / 100);
0 ignored issues
show
Bug introduced by
The function IRound was not found. Maybe you did not declare it correctly or list all dependencies? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

165
		return /** @scrutinizer ignore-call */ IRound(($this->getMaxShields() + $this->getMaxArmour() + $this->getMaxCDs() * CD_ARMOUR) / 100);
Loading history...
166
	}
167
168
	public function getShieldLow(): int { return IFloor($this->getShields() / 100) * 100; }
0 ignored issues
show
Bug introduced by
The function IFloor was not found. Maybe you did not declare it correctly or list all dependencies? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

168
	public function getShieldLow(): int { return /** @scrutinizer ignore-call */ IFloor($this->getShields() / 100) * 100; }
Loading history...
169
	public function getShieldHigh(): int { return $this->getShieldLow() + 100; }
170
	public function getArmourLow(): int { return IFloor($this->getArmour() / 100) * 100; }
0 ignored issues
show
Bug introduced by
The function IFloor was not found. Maybe you did not declare it correctly or list all dependencies? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

170
	public function getArmourLow(): int { return /** @scrutinizer ignore-call */ IFloor($this->getArmour() / 100) * 100; }
Loading history...
171
	public function getArmourHigh(): int { return $this->getArmourLow() + 100; }
172
	public function getCDsLow(): int { return IFloor($this->getCDs() / 100) * 100; }
0 ignored issues
show
Bug introduced by
The function IFloor was not found. Maybe you did not declare it correctly or list all dependencies? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

172
	public function getCDsLow(): int { return /** @scrutinizer ignore-call */ IFloor($this->getCDs() / 100) * 100; }
Loading history...
173
	public function getCDsHigh(): int { return $this->getCDsLow() + 100; }
174
175
176
177
	public function addWeapon(SmrWeapon $weapon): SmrWeapon|false {
178
		if ($this->hasOpenWeaponSlots() && $this->checkPowerAvailable($weapon->getPowerLevel())) {
179
			$this->weapons[] = $weapon;
180
			$this->hasChangedWeapons = true;
181
			return $weapon;
182
		}
183
		return false;
184
	}
185
186
	public function moveWeaponUp(int $orderID): void {
187
		$replacement = $orderID - 1;
188
		if ($replacement < 0) {
189
			// Shift everything up by one and put the selected weapon at the bottom
190
			$this->weapons[] = array_shift($this->weapons);
191
		} else {
192
			// Swap the selected weapon with the one above it
193
			$temp = $this->weapons[$replacement];
194
			$this->weapons[$replacement] = $this->weapons[$orderID];
195
			$this->weapons[$orderID] = $temp;
196
		}
197
		$this->hasChangedWeapons = true;
198
	}
199
200
	public function moveWeaponDown(int $orderID): void {
201
		$replacement = $orderID + 1;
202
		if ($replacement >= count($this->weapons)) {
203
			// Shift everything down by one and put the selected weapon at the top
204
			array_unshift($this->weapons, array_pop($this->weapons));
205
		} else {
206
			// Swap the selected weapon with the one below it
207
			$temp = $this->weapons[$replacement];
208
			$this->weapons[$replacement] = $this->weapons[$orderID];
209
			$this->weapons[$orderID] = $temp;
210
		}
211
		$this->hasChangedWeapons = true;
212
	}
213
214
	/**
215
	 * @param array<int, int> $orderArray
216
	 */
217
	public function setWeaponLocations(array $orderArray): void {
218
		$weapons = $this->weapons;
219
		foreach ($orderArray as $newOrder => $oldOrder) {
220
			$this->weapons[$newOrder] = $weapons[$oldOrder];
221
		}
222
		$this->hasChangedWeapons = true;
223
	}
224
225
	public function removeLastWeapon(): void {
226
		$this->removeWeapon($this->getNumWeapons() - 1);
227
	}
228
229
	public function removeWeapon(int $orderID): void {
230
		// Remove the specified weapon, then reindex the array
231
		unset($this->weapons[$orderID]);
232
		$this->weapons = array_values($this->weapons);
233
		$this->hasChangedWeapons = true;
234
	}
235
236
	public function removeAllWeapons(): void {
237
		$this->weapons = [];
238
		$this->hasChangedWeapons = true;
239
	}
240
241
	public function removeAllCargo(): void {
242
		foreach ($this->cargo as $goodID => $amount) {
243
			$this->setCargo($goodID, 0);
244
		}
245
	}
246
247
	public function removeAllHardware(): void {
248
		foreach (array_keys($this->hardware) as $hardwareTypeID) {
249
			$this->hasChangedHardware[$hardwareTypeID] = true;
250
		}
251
		$this->hardware = [];
252
		$this->decloak();
253
		$this->disableIllusion();
254
	}
255
256
	public function getPod(bool $isNewbie = false): void {
257
		$this->removeAllWeapons();
258
		$this->removeAllCargo();
259
		$this->removeAllHardware();
260
261
		if ($isNewbie) {
262
			$this->setShields(75);
263
			$this->setArmour(150);
264
			$this->setCargoHolds(40);
265
			$this->setTypeID(SHIP_TYPE_NEWBIE_MERCHANT_VESSEL);
266
		} else {
267
			$this->setShields(50);
268
			$this->setArmour(50);
269
			$this->setCargoHolds(5);
270
			$this->setTypeID(SHIP_TYPE_ESCAPE_POD);
271
		}
272
	}
273
274
	public function giveStarterShip(): void {
275
		if ($this->player->hasNewbieStatus()) {
276
			$shipID = SHIP_TYPE_NEWBIE_MERCHANT_VESSEL;
277
			$amount_shields = 75;
278
			$amount_armour = 150;
279
		} else {
280
			$shipID = self::STARTER_SHIPS[$this->player->getRaceID()];
281
			$amount_shields = 50;
282
			$amount_armour = 50;
283
		}
284
		$this->setTypeID($shipID);
285
		$this->setShields($amount_shields);
286
		$this->setArmour($amount_armour);
287
		$this->setCargoHolds(40);
288
		$this->addWeapon(SmrWeapon::getWeapon(WEAPON_TYPE_LASER));
289
	}
290
291
	public function hasJump(): bool {
292
		return $this->getHardware(HARDWARE_JUMP) > 0;
293
	}
294
295
	public function hasDCS(): bool {
296
		return $this->getHardware(HARDWARE_DCS) > 0;
297
	}
298
299
	public function hasScanner(): bool {
300
		return $this->getHardware(HARDWARE_SCANNER) > 0;
301
	}
302
303
	public function hasCloak(): bool {
304
		return $this->getHardware(HARDWARE_CLOAK) > 0;
305
	}
306
307
	public function isCloaked(): bool {
308
		return $this->isCloaked;
309
	}
310
311
	public function decloak(): void {
312
		if ($this->isCloaked === false) {
313
			return;
314
		}
315
		$this->isCloaked = false;
316
		$this->hasChangedCloak = true;
317
	}
318
319
	public function enableCloak(): void {
320
		if ($this->hasCloak() === false) {
321
			throw new Exception('Ship does not have the supported hardware!');
322
		}
323
		if ($this->isCloaked === true) {
324
			return;
325
		}
326
		$this->isCloaked = true;
327
		$this->hasChangedCloak = true;
328
	}
329
330
	public function hasIllusion(): bool {
331
		return $this->getHardware(HARDWARE_ILLUSION) > 0;
332
	}
333
334
	/**
335
	 * @phpstan-assert-if-true !false $this->getIllusion()
336
	 */
337
	public function hasActiveIllusion(): bool {
338
		return $this->getIllusion() !== false;
339
	}
340
341
	/**
342
	 * @return \Smr\ShipIllusion|false
343
	 */
344
	public function getIllusion(): ShipIllusion|false {
345
		return $this->illusionShip;
346
	}
347
348
	public function setIllusion(int $shipTypeID, int $attack, int $defense): void {
349
		if ($this->hasIllusion() === false) {
350
			throw new Exception('Ship does not have the supported hardware!');
351
		}
352
		$newIllusion = new ShipIllusion(
353
			shipTypeID: $shipTypeID,
354
			attackRating: $attack,
355
			defenseRating: $defense,
356
		);
357
		if ($this->getIllusion() === $newIllusion) {
358
			return;
359
		}
360
		$this->illusionShip = $newIllusion;
361
		$this->hasChangedIllusion = true;
362
	}
363
364
	public function disableIllusion(): void {
365
		if ($this->getIllusion() === false) {
366
			return;
367
		}
368
		$this->illusionShip = false;
369
		$this->hasChangedIllusion = true;
370
	}
371
372
	public function getPlayer(): AbstractSmrPlayer {
373
		return $this->player;
374
	}
375
376
	public function getAccountID(): int {
377
		return $this->getPlayer()->getAccountID();
378
	}
379
380
	public function getGameID(): int {
381
		return $this->gameID;
382
	}
383
384
	public function getGame(): SmrGame {
385
		return SmrGame::getGame($this->gameID);
386
	}
387
388
	/**
389
	 * Switch to a new ship, updating player turns accordingly.
390
	 */
391
	public function setTypeID(int $shipTypeID): void {
392
		$oldSpeed = $this->shipType->getSpeed();
393
		$this->getPlayer()->setShipTypeID($shipTypeID);
394
		$this->regenerateShipType();
395
		$this->checkForExcess();
396
397
		// Update the player's turns to account for the speed change
398
		$newSpeed = $this->shipType->getSpeed();
399
		$oldTurns = $this->getPlayer()->getTurns();
400
		$this->getPlayer()->setTurns(IRound($oldTurns * $newSpeed / $oldSpeed));
0 ignored issues
show
Bug introduced by
The function IRound was not found. Maybe you did not declare it correctly or list all dependencies? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

400
		$this->getPlayer()->setTurns(/** @scrutinizer ignore-call */ IRound($oldTurns * $newSpeed / $oldSpeed));
Loading history...
401
	}
402
403
	public function getType(): SmrShipType {
404
		return $this->shipType;
405
	}
406
407
	public function getTypeID(): int {
408
		return $this->shipType->getTypeID();
409
	}
410
411
	public function getClass(): ShipClass {
412
		return $this->shipType->getClass();
413
	}
414
415
	public function getName(): string {
416
		return $this->shipType->getName();
417
	}
418
419
	public function getCost(): int {
420
		return $this->shipType->getCost();
421
	}
422
423
	public function getHardpoints(): int {
424
		return $this->shipType->getHardpoints();
425
	}
426
427
	/**
428
	 * Trade-in value of the ship
429
	 */
430
	public function getRefundValue(): int {
431
		return IFloor($this->getCost() * SHIP_REFUND_PERCENT);
0 ignored issues
show
Bug introduced by
The function IFloor was not found. Maybe you did not declare it correctly or list all dependencies? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

431
		return /** @scrutinizer ignore-call */ IFloor($this->getCost() * SHIP_REFUND_PERCENT);
Loading history...
432
	}
433
434
	public function getCostToUpgrade(int $otherShipTypeID): int {
435
		$otherShipType = SmrShipType::get($otherShipTypeID);
436
		return $otherShipType->getCost() - $this->getRefundValue();
437
	}
438
439
	public function getCostToUpgradeAndUNO(int $otherShipTypeID): int {
440
		return $this->getCostToUpgrade($otherShipTypeID) + $this->getCostToUNOAgainstShip($otherShipTypeID);
441
	}
442
443
	protected function getCostToUNOAgainstShip(int $otherShipTypeID): int {
444
		$otherShipType = SmrShipType::get($otherShipTypeID);
445
		$cost = 0;
446
		$hardwareTypes = [HARDWARE_SHIELDS, HARDWARE_ARMOUR, HARDWARE_CARGO];
447
		foreach ($hardwareTypes as $hardwareTypeID) {
448
			$cost += max(0, $otherShipType->getMaxHardware($hardwareTypeID) - $this->getHardware($hardwareTypeID)) * HardwareType::get($hardwareTypeID)->cost;
449
		}
450
		return $cost;
451
	}
452
453
	public function getCostToUNO(): int {
454
		return $this->getCostToUNOAgainstShip($this->getTypeID());
455
	}
456
457
	/**
458
	 * Returns the ship speed modified by the game speed.
459
	 */
460
	public function getRealSpeed(): float {
461
		return $this->shipType->getSpeed() * $this->getGame()->getGameSpeed();
462
	}
463
464
	/**
465
	 * @return ($hardwareTypeID is null ? array<int, int> : int)
0 ignored issues
show
Documentation Bug introduced by
The doc comment ($hardwareTypeID at position 1 could not be parsed: Unknown type name '$hardwareTypeID' at position 1 in ($hardwareTypeID.
Loading history...
466
	 */
467
	public function getHardware(int $hardwareTypeID = null): array|int {
468
		if ($hardwareTypeID === null) {
469
			return $this->hardware;
470
		}
471
		return $this->hardware[$hardwareTypeID] ?? 0;
472
	}
473
474
	public function setHardware(int $hardwareTypeID, int $amount): void {
475
		if ($this->getHardware($hardwareTypeID) === $amount) {
476
			return;
477
		}
478
		$this->hardware[$hardwareTypeID] = $amount;
479
		$this->hasChangedHardware[$hardwareTypeID] = true;
480
	}
481
482
	public function increaseHardware(int $hardwareTypeID, int $amount): void {
483
		$this->setHardware($hardwareTypeID, $this->getHardware($hardwareTypeID) + $amount);
484
	}
485
486
	public function hasMaxHardware(int $hardwareTypeID): bool {
487
		return $this->getHardware($hardwareTypeID) == $this->shipType->getMaxHardware($hardwareTypeID);
488
	}
489
490
	public function getShields(): int {
491
		return $this->getHardware(HARDWARE_SHIELDS);
492
	}
493
494
	public function setShields(int $amount): void {
495
		$this->setHardware(HARDWARE_SHIELDS, $amount);
496
	}
497
498
	public function decreaseShields(int $amount): void {
499
		$this->setShields($this->getShields() - $amount);
500
	}
501
502
	public function increaseShields(int $amount): void {
503
		$this->setShields($this->getShields() + $amount);
504
	}
505
506
	public function hasShields(): bool {
507
		return $this->getShields() > 0;
508
	}
509
510
	public function hasMaxShields(): bool {
511
		return $this->hasMaxHardware(HARDWARE_SHIELDS);
512
	}
513
514
	public function getMaxShields(): int {
515
		return $this->shipType->getMaxHardware(HARDWARE_SHIELDS);
516
	}
517
518
	public function getArmour(): int {
519
		return $this->getHardware(HARDWARE_ARMOUR);
520
	}
521
522
	public function setArmour(int $amount): void {
523
		$this->setHardware(HARDWARE_ARMOUR, $amount);
524
	}
525
526
	public function decreaseArmour(int $amount): void {
527
		$this->setArmour($this->getArmour() - $amount);
528
	}
529
530
	public function increaseArmour(int $amount): void {
531
		$this->setArmour($this->getArmour() + $amount);
532
	}
533
534
	public function hasArmour(): bool {
535
		return $this->getArmour() > 0;
536
	}
537
538
	public function hasMaxArmour(): bool {
539
		return $this->hasMaxHardware(HARDWARE_ARMOUR);
540
	}
541
542
	public function getMaxArmour(): int {
543
		return $this->shipType->getMaxHardware(HARDWARE_ARMOUR);
544
	}
545
546
	public function isDead(): bool {
547
		return !$this->hasArmour() && !$this->hasShields();
548
	}
549
550
	public function hasMaxCDs(): bool {
551
		return $this->hasMaxHardware(HARDWARE_COMBAT);
552
	}
553
554
	public function hasMaxSDs(): bool {
555
		return $this->hasMaxHardware(HARDWARE_SCOUT);
556
	}
557
558
	public function hasMaxMines(): bool {
559
		return $this->hasMaxHardware(HARDWARE_MINE);
560
	}
561
562
	public function hasCDs(): bool {
563
		return $this->getCDs() > 0;
564
	}
565
566
	public function hasSDs(): bool {
567
		return $this->getSDs() > 0;
568
	}
569
570
	public function hasMines(): bool {
571
		return $this->getMines() > 0;
572
	}
573
574
	public function getCDs(): int {
575
		return $this->getHardware(HARDWARE_COMBAT);
576
	}
577
578
	public function setCDs(int $amount): void {
579
		$this->setHardware(HARDWARE_COMBAT, $amount);
580
	}
581
582
	public function decreaseCDs(int $amount): void {
583
		$this->setCDs($this->getCDs() - $amount);
584
	}
585
586
	public function increaseCDs(int $amount): void {
587
		$this->setCDs($this->getCDs() + $amount);
588
	}
589
590
	public function getMaxCDs(): int {
591
		return $this->shipType->getMaxHardware(HARDWARE_COMBAT);
592
	}
593
594
	public function getSDs(): int {
595
		return $this->getHardware(HARDWARE_SCOUT);
596
	}
597
598
	public function setSDs(int $amount): void {
599
		$this->setHardware(HARDWARE_SCOUT, $amount);
600
	}
601
602
	public function decreaseSDs(int $amount): void {
603
		$this->setSDs($this->getSDs() - $amount);
604
	}
605
606
	public function increaseSDs(int $amount): void {
607
		$this->setSDs($this->getSDs() + $amount);
608
	}
609
610
	public function getMaxSDs(): int {
611
		return $this->shipType->getMaxHardware(HARDWARE_SCOUT);
612
	}
613
614
	public function getMines(): int {
615
		return $this->getHardware(HARDWARE_MINE);
616
	}
617
618
	public function setMines(int $amount): void {
619
		$this->setHardware(HARDWARE_MINE, $amount);
620
	}
621
622
	public function decreaseMines(int $amount): void {
623
		$this->setMines($this->getMines() - $amount);
624
	}
625
626
	public function increaseMines(int $amount): void {
627
		$this->setMines($this->getMines() + $amount);
628
	}
629
630
	public function getMaxMines(): int {
631
		return $this->shipType->getMaxHardware(HARDWARE_MINE);
632
	}
633
634
	public function getCargoHolds(): int {
635
		return $this->getHardware(HARDWARE_CARGO);
636
	}
637
638
	public function setCargoHolds(int $amount): void {
639
		$this->setHardware(HARDWARE_CARGO, $amount);
640
	}
641
642
	/**
643
	 * @return ($goodID is null ? array<int, int> : int)
0 ignored issues
show
Documentation Bug introduced by
The doc comment ($goodID at position 1 could not be parsed: Unknown type name '$goodID' at position 1 in ($goodID.
Loading history...
644
	 */
645
	public function getCargo(int $goodID = null): int|array {
646
		if ($goodID === null) {
647
			return $this->cargo;
648
		}
649
		return $this->cargo[$goodID] ?? 0;
650
	}
651
652
	public function hasCargo(int $goodID = null): bool {
653
		if ($goodID === null) {
654
			return $this->getUsedHolds() > 0;
655
		}
656
		return $this->getCargo($goodID) > 0;
657
	}
658
659
	public function setCargo(int $goodID, int $amount): void {
660
		if ($this->getCargo($goodID) === $amount) {
661
			return;
662
		}
663
		$this->cargo[$goodID] = $amount;
664
		$this->hasChangedCargo = true;
665
		// Sort cargo by goodID to make sure it shows up in the correct order
666
		// before the next page is loaded.
667
		ksort($this->cargo);
668
	}
669
670
	public function decreaseCargo(int $goodID, int $amount): void {
671
		if ($amount < 0) {
672
			throw new Exception('Trying to decrease negative cargo.');
673
		}
674
		$this->setCargo($goodID, $this->getCargo($goodID) - $amount);
675
	}
676
677
	public function increaseCargo(int $goodID, int $amount): void {
678
		if ($amount < 0) {
679
			throw new Exception('Trying to increase negative cargo.');
680
		}
681
		$this->setCargo($goodID, $this->getCargo($goodID) + $amount);
682
	}
683
684
	public function getEmptyHolds(): int {
685
		return $this->getCargoHolds() - $this->getUsedHolds();
686
	}
687
688
	public function getUsedHolds(): int {
689
		return array_sum($this->getCargo());
690
	}
691
692
	public function hasMaxCargoHolds(): bool {
693
		return $this->hasMaxHardware(HARDWARE_CARGO);
694
	}
695
696
	public function getMaxCargoHolds(): int {
697
		return $this->shipType->getMaxHardware(HARDWARE_CARGO);
698
	}
699
700
	public function hasWeapons(): bool {
701
		return $this->getNumWeapons() > 0;
702
	}
703
704
	/**
705
	 * @return array<int, SmrWeapon>
706
	 */
707
	public function getWeapons(): array {
708
		return $this->weapons;
709
	}
710
711
	public function canAttack(): bool {
712
		return $this->hasWeapons() || $this->hasCDs();
713
	}
714
715
	public function getNumWeapons(): int {
716
		return count($this->getWeapons());
717
	}
718
719
	public function getOpenWeaponSlots(): int {
720
		return $this->getHardpoints() - $this->getNumWeapons();
721
	}
722
723
	public function hasOpenWeaponSlots(): bool {
724
		return $this->getOpenWeaponSlots() > 0;
725
	}
726
727
	public function getTotalShieldDamage(): int {
728
		$shieldDamage = 0;
729
		foreach ($this->getWeapons() as $weapon) {
730
			$shieldDamage += $weapon->getShieldDamage();
731
		}
732
		return $shieldDamage;
733
	}
734
735
	public function getTotalArmourDamage(): int {
736
		$armourDamage = 0;
737
		foreach ($this->getWeapons() as $weapon) {
738
			$armourDamage += $weapon->getArmourDamage();
739
		}
740
		return $armourDamage;
741
	}
742
743
	public function isFederal(): bool {
744
		return $this->getTypeID() === SHIP_TYPE_FEDERAL_DISCOVERY ||
745
		       $this->getTypeID() === SHIP_TYPE_FEDERAL_WARRANT ||
746
		       $this->getTypeID() === SHIP_TYPE_FEDERAL_ULTIMATUM;
747
	}
748
749
	public function isUnderground(): bool {
750
		return $this->getTypeID() === SHIP_TYPE_THIEF ||
751
		       $this->getTypeID() === SHIP_TYPE_ASSASSIN ||
752
		       $this->getTypeID() === SHIP_TYPE_DEATH_CRUISER;
753
	}
754
755
	/**
756
	 * @param array<AbstractSmrPlayer> $targetPlayers
757
	 * @return array<string, mixed>
758
	 */
759
	public function shootPlayers(array $targetPlayers): array {
760
		$thisPlayer = $this->getPlayer();
761
		$results = ['Player' => $thisPlayer, 'TotalDamage' => 0, 'Weapons' => []];
762
		foreach ($targetPlayers as $targetPlayer) {
763
			$results['TotalDamagePerTargetPlayer'][$targetPlayer->getAccountID()] = 0;
764
		}
765
		if ($thisPlayer->isDead()) {
766
			$results['DeadBeforeShot'] = true;
767
			return $results;
768
		}
769
		$results['DeadBeforeShot'] = false;
770
		foreach ($this->weapons as $orderID => $weapon) {
771
			$results['Weapons'][$orderID] = $weapon->shootPlayer($thisPlayer, array_rand_value($targetPlayers));
0 ignored issues
show
Bug introduced by
The function array_rand_value was not found. Maybe you did not declare it correctly or list all dependencies? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

771
			$results['Weapons'][$orderID] = $weapon->shootPlayer($thisPlayer, /** @scrutinizer ignore-call */ array_rand_value($targetPlayers));
Loading history...
772
			if ($results['Weapons'][$orderID]['Hit']) {
773
				$results['TotalDamage'] += $results['Weapons'][$orderID]['ActualDamage']['TotalDamage'];
774
				$results['TotalDamagePerTargetPlayer'][$results['Weapons'][$orderID]['TargetPlayer']->getAccountID()] += $results['Weapons'][$orderID]['ActualDamage']['TotalDamage'];
775
			}
776
		}
777
		if ($this->hasCDs()) {
778
			$thisCDs = new SmrCombatDrones($this->getCDs());
779
			$results['Drones'] = $thisCDs->shootPlayer($thisPlayer, array_rand_value($targetPlayers));
780
			$results['TotalDamage'] += $results['Drones']['ActualDamage']['TotalDamage'];
781
			$results['TotalDamagePerTargetPlayer'][$results['Drones']['TargetPlayer']->getAccountID()] += $results['Drones']['ActualDamage']['TotalDamage'];
782
		}
783
		$thisPlayer->increaseExperience(IRound($results['TotalDamage'] * self::EXP_PER_DAMAGE_PLAYER));
0 ignored issues
show
Bug introduced by
The function IRound was not found. Maybe you did not declare it correctly or list all dependencies? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

783
		$thisPlayer->increaseExperience(/** @scrutinizer ignore-call */ IRound($results['TotalDamage'] * self::EXP_PER_DAMAGE_PLAYER));
Loading history...
784
		$thisPlayer->increaseHOF($results['TotalDamage'], ['Combat', 'Player', 'Damage Done'], HOF_PUBLIC);
785
		$thisPlayer->increaseHOF(1, ['Combat', 'Player', 'Shots'], HOF_PUBLIC);
786
		return $results;
787
	}
788
789
	/**
790
	 * @return array<string, mixed>
791
	 */
792
	public function shootForces(SmrForce $forces): array {
793
		$thisPlayer = $this->getPlayer();
794
		$results = ['Player' => $thisPlayer, 'TotalDamage' => 0, 'Weapons' => []];
795
		if ($thisPlayer->isDead()) {
796
			$results['DeadBeforeShot'] = true;
797
			return $results;
798
		}
799
		$results['DeadBeforeShot'] = false;
800
		foreach ($this->weapons as $orderID => $weapon) {
801
			$results['Weapons'][$orderID] = $weapon->shootForces($thisPlayer, $forces);
802
			if ($results['Weapons'][$orderID]['Hit']) {
803
				$results['TotalDamage'] += $results['Weapons'][$orderID]['ActualDamage']['TotalDamage'];
804
				$thisPlayer->increaseHOF($results['Weapons'][$orderID]['ActualDamage']['NumMines'], ['Combat', 'Forces', 'Mines', 'Killed'], HOF_PUBLIC);
805
				$thisPlayer->increaseHOF($results['Weapons'][$orderID]['ActualDamage']['Mines'], ['Combat', 'Forces', 'Mines', 'Damage Done'], HOF_PUBLIC);
806
				$thisPlayer->increaseHOF($results['Weapons'][$orderID]['ActualDamage']['NumCDs'], ['Combat', 'Forces', 'Combat Drones', 'Killed'], HOF_PUBLIC);
807
				$thisPlayer->increaseHOF($results['Weapons'][$orderID]['ActualDamage']['CDs'], ['Combat', 'Forces', 'Combat Drones', 'Damage Done'], HOF_PUBLIC);
808
				$thisPlayer->increaseHOF($results['Weapons'][$orderID]['ActualDamage']['NumSDs'], ['Combat', 'Forces', 'Scout Drones', 'Killed'], HOF_PUBLIC);
809
				$thisPlayer->increaseHOF($results['Weapons'][$orderID]['ActualDamage']['SDs'], ['Combat', 'Forces', 'Scout Drones', 'Damage Done'], HOF_PUBLIC);
810
				$thisPlayer->increaseHOF($results['Weapons'][$orderID]['ActualDamage']['NumMines'] + $results['Weapons'][$orderID]['ActualDamage']['NumCDs'] + $results['Weapons'][$orderID]['ActualDamage']['NumSDs'], ['Combat', 'Forces', 'Killed'], HOF_PUBLIC);
811
			}
812
		}
813
		if ($this->hasCDs()) {
814
			$thisCDs = new SmrCombatDrones($this->getCDs());
815
			$results['Drones'] = $thisCDs->shootForces($thisPlayer, $forces);
816
			$results['TotalDamage'] += $results['Drones']['ActualDamage']['TotalDamage'];
817
			$thisPlayer->increaseHOF($results['Drones']['ActualDamage']['NumMines'], ['Combat', 'Forces', 'Mines', 'Killed'], HOF_PUBLIC);
818
			$thisPlayer->increaseHOF($results['Drones']['ActualDamage']['Mines'], ['Combat', 'Forces', 'Mines', 'Damage Done'], HOF_PUBLIC);
819
			$thisPlayer->increaseHOF($results['Drones']['ActualDamage']['NumCDs'], ['Combat', 'Forces', 'Combat Drones', 'Killed'], HOF_PUBLIC);
820
			$thisPlayer->increaseHOF($results['Drones']['ActualDamage']['CDs'], ['Combat', 'Forces', 'Combat Drones', 'Damage Done'], HOF_PUBLIC);
821
			$thisPlayer->increaseHOF($results['Drones']['ActualDamage']['NumSDs'], ['Combat', 'Forces', 'Scout Drones', 'Killed'], HOF_PUBLIC);
822
			$thisPlayer->increaseHOF($results['Drones']['ActualDamage']['SDs'], ['Combat', 'Forces', 'Scout Drones', 'Damage Done'], HOF_PUBLIC);
823
			$thisPlayer->increaseHOF($results['Drones']['ActualDamage']['NumMines'] + $results['Drones']['ActualDamage']['NumCDs'] + $results['Drones']['ActualDamage']['NumSDs'], ['Combat', 'Forces', 'Killed'], HOF_PUBLIC);
824
		}
825
		$thisPlayer->increaseExperience(IRound($results['TotalDamage'] * self::EXP_PER_DAMAGE_FORCE));
0 ignored issues
show
Bug introduced by
The function IRound was not found. Maybe you did not declare it correctly or list all dependencies? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

825
		$thisPlayer->increaseExperience(/** @scrutinizer ignore-call */ IRound($results['TotalDamage'] * self::EXP_PER_DAMAGE_FORCE));
Loading history...
826
		$thisPlayer->increaseHOF($results['TotalDamage'], ['Combat', 'Forces', 'Damage Done'], HOF_PUBLIC);
827
		$thisPlayer->increaseHOF(1, ['Combat', 'Forces', 'Shots'], HOF_PUBLIC);
828
		return $results;
829
	}
830
831
	/**
832
	 * @return array<string, mixed>
833
	 */
834
	public function shootPort(SmrPort $port): array {
835
		$thisPlayer = $this->getPlayer();
836
		$results = ['Player' => $thisPlayer, 'TotalDamage' => 0, 'Weapons' => []];
837
		if ($thisPlayer->isDead()) {
838
			$results['DeadBeforeShot'] = true;
839
			return $results;
840
		}
841
		$results['DeadBeforeShot'] = false;
842
		foreach ($this->weapons as $orderID => $weapon) {
843
			$results['Weapons'][$orderID] = $weapon->shootPort($thisPlayer, $port);
844
			if ($results['Weapons'][$orderID]['Hit']) {
845
				$results['TotalDamage'] += $results['Weapons'][$orderID]['ActualDamage']['TotalDamage'];
846
			}
847
		}
848
		if ($this->hasCDs()) {
849
			$thisCDs = new SmrCombatDrones($this->getCDs());
850
			$results['Drones'] = $thisCDs->shootPort($thisPlayer, $port);
851
			$results['TotalDamage'] += $results['Drones']['ActualDamage']['TotalDamage'];
852
		}
853
		$thisPlayer->increaseExperience(IRound($results['TotalDamage'] * self::EXP_PER_DAMAGE_PORT));
0 ignored issues
show
Bug introduced by
The function IRound was not found. Maybe you did not declare it correctly or list all dependencies? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

853
		$thisPlayer->increaseExperience(/** @scrutinizer ignore-call */ IRound($results['TotalDamage'] * self::EXP_PER_DAMAGE_PORT));
Loading history...
854
		$thisPlayer->increaseHOF($results['TotalDamage'], ['Combat', 'Port', 'Damage Done'], HOF_PUBLIC);
855
		//$thisPlayer->increaseHOF(1,array('Combat','Port','Shots')); //in SmrPort::attackedBy()
856
857
		// Change alignment if we reach a damage threshold.
858
		// Increase if player and port races are at war; decrease otherwise.
859
		if ($results['TotalDamage'] >= SmrPort::DAMAGE_NEEDED_FOR_ALIGNMENT_CHANGE) {
860
			$relations = Globals::getRaceRelations($thisPlayer->getGameID(), $thisPlayer->getRaceID());
861
			if ($relations[$port->getRaceID()] <= RELATIONS_WAR) {
862
				$thisPlayer->increaseAlignment(1);
863
				$thisPlayer->increaseHOF(1, ['Combat', 'Port', 'Alignment', 'Gain'], HOF_PUBLIC);
864
			} else {
865
				$thisPlayer->decreaseAlignment(1);
866
				$thisPlayer->increaseHOF(1, ['Combat', 'Port', 'Alignment', 'Loss'], HOF_PUBLIC);
867
			}
868
		}
869
		return $results;
870
	}
871
872
	/**
873
	 * @return array<string, mixed>
874
	 */
875
	public function shootPlanet(SmrPlanet $planet): array {
876
		$thisPlayer = $this->getPlayer();
877
		$results = ['Player' => $thisPlayer, 'TotalDamage' => 0, 'Weapons' => []];
878
		if ($thisPlayer->isDead()) {
879
			$results['DeadBeforeShot'] = true;
880
			return $results;
881
		}
882
		$results['DeadBeforeShot'] = false;
883
		foreach ($this->weapons as $orderID => $weapon) {
884
			$results['Weapons'][$orderID] = $weapon->shootPlanet($thisPlayer, $planet);
885
			if ($results['Weapons'][$orderID]['Hit']) {
886
				$results['TotalDamage'] += $results['Weapons'][$orderID]['ActualDamage']['TotalDamage'];
887
			}
888
		}
889
		if ($this->hasCDs()) {
890
			$thisCDs = new SmrCombatDrones($this->getCDs());
891
			$results['Drones'] = $thisCDs->shootPlanet($thisPlayer, $planet);
892
			$results['TotalDamage'] += $results['Drones']['ActualDamage']['TotalDamage'];
893
		}
894
		$thisPlayer->increaseExperience(IRound($results['TotalDamage'] * self::EXP_PER_DAMAGE_PLANET));
0 ignored issues
show
Bug introduced by
The function IRound was not found. Maybe you did not declare it correctly or list all dependencies? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

894
		$thisPlayer->increaseExperience(/** @scrutinizer ignore-call */ IRound($results['TotalDamage'] * self::EXP_PER_DAMAGE_PLANET));
Loading history...
895
		$thisPlayer->increaseHOF($results['TotalDamage'], ['Combat', 'Planet', 'Damage Done'], HOF_PUBLIC);
896
		//$thisPlayer->increaseHOF(1,array('Combat','Planet','Shots')); //in SmrPlanet::attackedBy()
897
		return $results;
898
	}
899
900
	/**
901
	 * @param WeaponDamageData $damage
0 ignored issues
show
Bug introduced by
The type WeaponDamageData was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
902
	 * @return array<string, int|bool>
903
	 */
904
	public function takeDamage(array $damage): array {
905
		$alreadyDead = $this->getPlayer()->isDead();
906
		$armourDamage = 0;
907
		$cdDamage = 0;
908
		$shieldDamage = 0;
909
		if (!$alreadyDead) {
910
			// Even if the weapon doesn't do any damage, it was fired at the
911
			// player, so alert them that they're under attack.
912
			$this->getPlayer()->setUnderAttack(true);
913
914
			$shieldDamage = $this->takeDamageToShields($damage['Shield']);
915
			if (!$this->hasShields() && ($shieldDamage == 0 || $damage['Rollover'])) {
916
				$cdMaxDamage = $damage['Armour'] - $shieldDamage;
917
				$cdDamage = $this->takeDamageToCDs($cdMaxDamage);
918
				if (!$this->hasCDs() && ($cdDamage == 0 || $damage['Rollover'])) {
919
					$armourMaxDamage = $damage['Armour'] - $shieldDamage - $cdDamage;
920
					$armourDamage = $this->takeDamageToArmour($armourMaxDamage);
921
				}
922
			}
923
		}
924
		return [
925
						'KillingShot' => !$alreadyDead && $this->isDead(),
926
						'TargetAlreadyDead' => $alreadyDead,
927
						'Shield' => $shieldDamage,
928
						'CDs' => $cdDamage,
929
						'NumCDs' => $cdDamage / CD_ARMOUR,
930
						'HasCDs' => $this->hasCDs(),
931
						'Armour' => $armourDamage,
932
						'TotalDamage' => $shieldDamage + $cdDamage + $armourDamage,
933
		];
934
	}
935
936
	/**
937
	 * @param WeaponDamageData $damage
938
	 * @return array<string, int|bool>
939
	 */
940
	public function takeDamageFromMines(array $damage): array {
941
		$alreadyDead = $this->getPlayer()->isDead();
942
		$armourDamage = 0;
943
		$cdDamage = 0;
944
		$shieldDamage = 0;
945
		if (!$alreadyDead) {
946
			$shieldDamage = $this->takeDamageToShields($damage['Shield']);
947
			if (!$this->hasShields() && ($shieldDamage == 0 || $damage['Rollover'])) { //skip CDs if it's mines
948
				$armourMaxDamage = $damage['Armour'] - $shieldDamage;
949
				$armourDamage = $this->takeDamageToArmour($armourMaxDamage);
950
			}
951
		}
952
		return [
953
						'KillingShot' => !$alreadyDead && $this->isDead(),
954
						'TargetAlreadyDead' => $alreadyDead,
955
						'Shield' => $shieldDamage,
956
						'CDs' => $cdDamage,
957
						'NumCDs' => $cdDamage / CD_ARMOUR,
958
						'HasCDs' => $this->hasCDs(),
959
						'Armour' => $armourDamage,
960
						'TotalDamage' => $shieldDamage + $cdDamage + $armourDamage,
961
		];
962
	}
963
964
	protected function takeDamageToShields(int $damage): int {
965
		$actualDamage = min($this->getShields(), $damage);
966
		$this->decreaseShields($actualDamage);
967
		return $actualDamage;
968
	}
969
970
	protected function takeDamageToCDs(int $damage): int {
971
		$actualDamage = min($this->getCDs(), IFloor($damage / CD_ARMOUR));
0 ignored issues
show
Bug introduced by
The function IFloor was not found. Maybe you did not declare it correctly or list all dependencies? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

971
		$actualDamage = min($this->getCDs(), /** @scrutinizer ignore-call */ IFloor($damage / CD_ARMOUR));
Loading history...
972
		$this->decreaseCDs($actualDamage);
973
		return $actualDamage * CD_ARMOUR;
974
	}
975
976
	protected function takeDamageToArmour(int $damage): int {
977
		$actualDamage = min($this->getArmour(), $damage);
978
		$this->decreaseArmour($actualDamage);
979
		return $actualDamage;
980
	}
981
982
	/**
983
	 * Returns the maneuverability rating for this ship.
984
	 */
985
	public function getMR(): int {
986
		return $this->shipType->getBaseManeuverability();
987
	}
988
989
	public function update(): void {
990
		throw new Exception('Can only call update on SmrShip objects');
991
	}
992
993
}
994