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

Failed Conditions
Pull Request — master (#1072)
by Dan
05:03
created

AbstractSmrPort::getTradeRestriction()   A

Complexity

Conditions 5
Paths 5

Size

Total Lines 14
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 5
eloc 9
c 0
b 0
f 0
nc 5
nop 1
dl 0
loc 14
rs 9.6111
1
<?php declare(strict_types=1);
2
class AbstractSmrPort {
3
	use Traits\RaceID;
4
5
	protected static array $CACHE_PORTS = [];
6
	protected static array $CACHE_CACHED_PORTS = [];
7
8
	const DAMAGE_NEEDED_FOR_ALIGNMENT_CHANGE = 300; // single player
9
	const DAMAGE_NEEDED_FOR_DOWNGRADE_CHANCE = 325; // all attackers
10
	const CHANCE_TO_DOWNGRADE = 1;
11
	const TIME_FEDS_STAY = 1800;
12
	const MAX_FEDS_BONUS = 4000;
13
	const BASE_CDS = 725;
14
	const CDS_PER_LEVEL = 100;
15
	const CDS_PER_TEN_MIL_CREDITS = 25;
16
	const BASE_DEFENCES = 500;
17
	const DEFENCES_PER_LEVEL = 700;
18
	const DEFENCES_PER_TEN_MIL_CREDITS = 250;
19
	const MAX_LEVEL = 9;
20
	const BASE_REFRESH_PER_HOUR = array(
21
		'1' => 150,
22
		'2' => 110,
23
		'3' => 70
24
	);
25
	const REFRESH_PER_GOOD = .9;
26
	const TIME_TO_CREDIT_RAID = 10800; // 3 hours
27
	const GOODS_TRADED_MONEY_MULTIPLIER = 50;
28
	const BASE_PAYOUT = 0.85; // fraction of credits for looting
29
	const RAZE_PAYOUT = 0.75; // fraction of base payout for razing
30
31
	protected Smr\Database $db;
32
33
	protected int $gameID;
34
	protected int $sectorID;
35
	protected int $shields;
36
	protected int $combatDrones;
37
	protected int $armour;
38
	protected int $reinforceTime;
39
	protected int $attackStarted;
40
	protected int $level;
41
	protected int $credits;
42
	protected int $upgrade;
43
	protected int $experience;
44
45
	protected array $goodIDs = array('All' => [], TRADER_SELLS => [], TRADER_BUYS => []);
46
	protected array $goodAmounts;
47
	protected array $goodAmountsChanged = [];
48
	protected array $goodDistances;
49
50
	protected bool $cachedVersion = false;
51
	protected int $cachedTime;
52
	protected bool $cacheIsValid = true;
53
54
	protected string $SQL;
55
56
	protected bool $hasChanged = false;
57
	protected bool $isNew = false;
58
59
	public static function refreshCache() : void {
60
		foreach (self::$CACHE_PORTS as $gameID => &$gamePorts) {
61
			foreach ($gamePorts as $sectorID => &$port) {
62
				$port = self::getPort($gameID, $sectorID, true);
63
			}
64
		}
65
	}
66
67
	public static function clearCache() : void {
68
		self::$CACHE_PORTS = array();
69
		self::$CACHE_CACHED_PORTS = array();
70
	}
71
72
	public static function getGalaxyPorts(int $gameID, int $galaxyID, bool $forceUpdate = false) : array {
73
		$db = Smr\Database::getInstance();
74
		// Use a left join so that we populate the cache for every sector
75
		$db->query('SELECT port.*, sector_id FROM sector LEFT JOIN port USING(game_id, sector_id) WHERE game_id = ' . $db->escapeNumber($gameID) . ' AND galaxy_id = ' . $db->escapeNumber($galaxyID));
76
		$galaxyPorts = [];
77
		while ($db->nextRecord()) {
78
			$sectorID = $db->getInt('sector_id');
79
			$port = self::getPort($gameID, $sectorID, $forceUpdate, $db);
80
			// Only return those ports that exist
81
			if ($port->exists()) {
82
				$galaxyPorts[$sectorID] = $port;
83
			}
84
		}
85
		return $galaxyPorts;
86
	}
87
88
	public static function getPort(int $gameID, int $sectorID, bool $forceUpdate = false, Smr\Database $db = null) : self {
89
		if ($forceUpdate || !isset(self::$CACHE_PORTS[$gameID][$sectorID])) {
90
			self::$CACHE_PORTS[$gameID][$sectorID] = new SmrPort($gameID, $sectorID, $db);
91
		}
92
		return self::$CACHE_PORTS[$gameID][$sectorID];
93
	}
94
95
	public static function removePort(int $gameID, int $sectorID) : void {
96
		$db = Smr\Database::getInstance();
97
		$SQL = 'game_id = ' . $db->escapeNumber($gameID) . '
98
		        AND sector_id = ' . $db->escapeNumber($sectorID);
99
		$db->query('DELETE FROM port WHERE ' . $SQL);
100
		$db->query('DELETE FROM port_has_goods WHERE ' . $SQL);
101
		$db->query('DELETE FROM player_visited_port WHERE ' . $SQL);
102
		$db->query('DELETE FROM player_attacks_port WHERE ' . $SQL);
103
		$db->query('DELETE FROM port_info_cache WHERE ' . $SQL);
104
		self::$CACHE_PORTS[$gameID][$sectorID] = null;
105
		unset(self::$CACHE_PORTS[$gameID][$sectorID]);
106
	}
107
108
	public static function createPort(int $gameID, int $sectorID) : self {
109
		if (!isset(self::$CACHE_PORTS[$gameID][$sectorID])) {
110
			$p = new SmrPort($gameID, $sectorID);
111
			self::$CACHE_PORTS[$gameID][$sectorID] = $p;
112
		}
113
		return self::$CACHE_PORTS[$gameID][$sectorID];
114
	}
115
116
	public static function savePorts() : void {
117
		foreach (self::$CACHE_PORTS as $gamePorts) {
118
			foreach ($gamePorts as $port) {
119
				$port->update();
120
			}
121
		}
122
	}
123
124
	public static function getBaseExperience(int $cargo, int $distance) : float {
125
		return ($cargo / 13) * $distance;
126
	}
127
128
	protected function __construct(int $gameID, int $sectorID, Smr\Database $db = null) {
129
		$this->cachedTime = Smr\Epoch::time();
130
		$this->db = Smr\Database::getInstance();
131
		$this->SQL = 'sector_id = ' . $this->db->escapeNumber($sectorID) . ' AND game_id = ' . $this->db->escapeNumber($gameID);
132
133
		if (isset($db)) {
134
			$this->isNew = !$db->hasField('game_id');
135
		} else {
136
			$db = $this->db;
137
			$db->query('SELECT * FROM port WHERE ' . $this->SQL . ' LIMIT 1');
138
			$this->isNew = !$db->nextRecord();
139
		}
140
141
		$this->gameID = (int)$gameID;
142
		$this->sectorID = (int)$sectorID;
143
		if (!$this->isNew) {
144
			$this->shields = $db->getInt('shields');
145
			$this->combatDrones = $db->getInt('combat_drones');
146
			$this->armour = $db->getInt('armour');
147
			$this->reinforceTime = $db->getInt('reinforce_time');
148
			$this->attackStarted = $db->getInt('attack_started');
149
			$this->raceID = $db->getInt('race_id');
150
			$this->level = $db->getInt('level');
151
			$this->credits = $db->getInt('credits');
152
			$this->upgrade = $db->getInt('upgrade');
153
			$this->experience = $db->getInt('experience');
154
155
			$this->checkDefenses();
156
			$this->getGoods();
157
			$this->checkForUpgrade();
158
		} else {
159
			$this->shields = 0;
160
			$this->combatDrones = 0;
161
			$this->armour = 0;
162
			$this->reinforceTime = 0;
163
			$this->attackStarted = 0;
164
			$this->raceID = 1;
165
			$this->level = 0;
166
			$this->credits = 0;
167
			$this->upgrade = 0;
168
			$this->experience = 0;
169
		}
170
	}
171
172
	public function checkDefenses() : void {
173
		if (!$this->isUnderAttack()) {
174
			$defences = self::BASE_DEFENCES + $this->getLevel() * self::DEFENCES_PER_LEVEL;
175
			$cds = self::BASE_CDS + $this->getLevel() * self::CDS_PER_LEVEL;
176
			// Upgrade modifier
177
			$defences += max(0, IRound(self::DEFENCES_PER_LEVEL * $this->getUpgrade() / $this->getUpgradeRequirement()));
178
			$cds += max(0, IRound(self::CDS_PER_LEVEL * $this->getUpgrade() / $this->getUpgradeRequirement()));
179
			// Credits modifier
180
			$defences += max(0, IRound(self::DEFENCES_PER_TEN_MIL_CREDITS * $this->getCredits() / 10000000));
181
			$cds += max(0, IRound(self::CDS_PER_TEN_MIL_CREDITS * $this->getCredits() / 10000000));
182
			// Defences restock (check for fed arrival)
183
			if (Smr\Epoch::time() < $this->getReinforceTime() + self::TIME_FEDS_STAY) {
184
				$federalMod = (self::TIME_FEDS_STAY - (Smr\Epoch::time() - $this->getReinforceTime())) / self::TIME_FEDS_STAY;
185
				$federalMod = max(0, IRound($federalMod * self::MAX_FEDS_BONUS));
186
				$defences += $federalMod;
187
				$cds += IRound($federalMod / 10);
188
			}
189
			$this->setShields($defences);
190
			$this->setArmour($defences);
191
			$this->setCDs($cds);
192
			if ($this->getCredits() == 0) {
193
				$this->setCreditsToDefault();
194
			}
195
			$this->db->query('DELETE FROM player_attacks_port WHERE ' . $this->SQL);
196
		}
197
	}
198
199
	/**
200
	 * Used for the automatic resupplying of all goods over time
201
	 */
202
	private function restockGood(int $goodID, int $secondsSinceLastUpdate) : void {
203
		if ($secondsSinceLastUpdate <= 0) {
204
			return;
205
		}
206
207
		$goodClass = Globals::getGood($goodID)['Class'];
208
		$refreshPerHour = self::BASE_REFRESH_PER_HOUR[$goodClass] * $this->getGame()->getGameSpeed();
209
		$refreshPerSec = $refreshPerHour / 3600;
210
		$amountToAdd = IFloor($secondsSinceLastUpdate * $refreshPerSec);
211
212
		// We will not save automatic resupplying in the database,
213
		// because the stock can be correctly recalculated based on the
214
		// last_update time. We will only do the update for player actions
215
		// that affect the stock. This avoids many unnecessary db queries.
216
		$doUpdateDB = false;
217
		$amount = $this->getGoodAmount($goodID);
218
		$this->setGoodAmount($goodID, $amount + $amountToAdd, $doUpdateDB);
219
	}
220
221
	// Sets the class members that identify port trade goods
222
	private function getGoods() : void {
223
		if ($this->isCachedVersion()) {
224
			throw new Exception('Cannot call getGoods on cached port');
225
		}
226
		if (empty($this->goodIDs['All'])) {
227
			$this->db->query('SELECT * FROM port_has_goods WHERE ' . $this->SQL . ' ORDER BY good_id ASC');
228
			while ($this->db->nextRecord()) {
229
				$goodID = $this->db->getInt('good_id');
230
				$transactionType = $this->db->getField('transaction_type');
231
				$this->goodAmounts[$goodID] = $this->db->getInt('amount');
232
				$this->goodIDs[$transactionType][] = $goodID;
233
				$this->goodIDs['All'][] = $goodID;
234
235
				$secondsSinceLastUpdate = Smr\Epoch::time() - $this->db->getInt('last_update');
236
				$this->restockGood($goodID, $secondsSinceLastUpdate);
237
			}
238
		}
239
	}
240
241
	private function getVisibleGoods(string $transaction, AbstractSmrPlayer $player = null) : array {
242
		$goodIDs = $this->goodIDs[$transaction];
243
		if ($player == null) {
244
			return $goodIDs;
245
		} else {
246
			return array_filter($goodIDs, function($goodID) use ($player) {
247
				$good = Globals::getGood($goodID);
248
				return $player->meetsAlignmentRestriction($good['AlignRestriction']);
249
			});
250
		}
251
	}
252
253
	/**
254
	 * Get IDs of goods that can be sold by $player to the port
255
	 */
256
	public function getVisibleGoodsSold(AbstractSmrPlayer $player = null) : array {
257
		return $this->getVisibleGoods(TRADER_SELLS, $player);
258
	}
259
260
	/**
261
	 * Get IDs of goods that can be bought by $player from the port
262
	 */
263
	public function getVisibleGoodsBought(AbstractSmrPlayer $player = null) : array {
264
		return $this->getVisibleGoods(TRADER_BUYS, $player);
265
	}
266
267
	public function getAllGoodIDs() : array {
268
		return $this->goodIDs['All'];
269
	}
270
271
	/**
272
	 * Get IDs of goods that can be sold to the port
273
	 */
274
	public function getSoldGoodIDs() : array {
275
		return $this->goodIDs[TRADER_SELLS];
276
	}
277
278
	/**
279
	 * Get IDs of goods that can be bought from the port
280
	 */
281
	public function getBoughtGoodIDs() : array {
282
		return $this->goodIDs[TRADER_BUYS];
283
	}
284
285
	public function getGood(int $goodID) : array|false {
286
		if (!$this->hasGood($goodID)) {
287
			return false;
288
		}
289
		return Globals::getGood($goodID);
290
	}
291
292
	public function getGoodDistance(int $goodID) : int {
293
		if (!isset($this->goodDistances[$goodID])) {
294
			$x = $this->getGood($goodID);
295
			if ($x === false) {
296
				throw new Exception('This port does not have this good!');
297
			}
298
			if ($this->hasGood($goodID, TRADER_BUYS)) {
299
				$x['TransactionType'] = TRADER_SELLS;
300
			} else {
301
				$x['TransactionType'] = TRADER_BUYS;
302
			}
303
			$di = Plotter::findDistanceToX($x, $this->getSector(), true);
304
			if (is_object($di)) {
0 ignored issues
show
introduced by
The condition is_object($di) is always false.
Loading history...
305
				$di = $di->getRelativeDistance();
306
			}
307
			$this->goodDistances[$goodID] = max(1, $di);
308
		}
309
		return $this->goodDistances[$goodID];
310
	}
311
312
	/**
313
	 * Returns the transaction type for this good (Buy or Sell).
314
	 * Note: this is the player's transaction, not the port's.
315
	 */
316
	public function getGoodTransaction(int $goodID) : ?string {
317
		foreach ([TRADER_BUYS, TRADER_SELLS] as $transaction) {
318
			if ($this->hasGood($goodID, $transaction)) {
319
				return $transaction;
320
			}
321
		}
322
		return null; // port does not have this good
323
	}
324
325
	public function hasGood(int $goodID, ?string $type = null) : bool {
326
		if ($type === null) {
327
			$type = 'All';
328
		}
329
		return in_array($goodID, $this->goodIDs[$type]);
330
	}
331
332
	private function setGoodAmount(int $goodID, int $amount, bool $doUpdate = true) : void {
333
		if ($this->isCachedVersion()) {
334
			throw new Exception('Cannot update a cached port!');
335
		}
336
		// The new amount must be between 0 and the max for this good
337
		$amount = max(0, min($amount, $this->getGood($goodID)['Max']));
338
		if ($this->getGoodAmount($goodID) == $amount) {
339
			return;
340
		}
341
		$this->goodAmounts[$goodID] = $amount;
342
343
		if ($doUpdate) {
344
			// This goodID will be changed in the db during `update()`
345
			$this->goodAmountsChanged[$goodID] = true;
346
		}
347
	}
348
349
	public function getGoodAmount(int $goodID) : int {
350
		return $this->goodAmounts[$goodID];
351
	}
352
353
	public function decreaseGood(array $good, int $amount, bool $doRefresh) : void {
354
		$this->setGoodAmount($good['ID'], $this->getGoodAmount($good['ID']) - $amount);
355
		if ($doRefresh === true) {
356
			//get id of goods to replenish
357
			$this->refreshGoods($good['Class'], $amount);
358
		}
359
	}
360
361
	public function increaseGoodAmount(int $goodID, int $amount) : void {
362
		$this->setGoodAmount($goodID, $this->getGoodAmount($goodID) + $amount);
363
	}
364
365
	public function decreaseGoodAmount(int $goodID, int $amount) : void {
366
		$this->setGoodAmount($goodID, $this->getGoodAmount($goodID) - $amount);
367
	}
368
369
	/**
370
	 * Adds extra stock to goods in the tier above a good that was traded
371
	 */
372
	protected function refreshGoods(int $classTraded, int $amountTraded) : void {
373
		$refreshAmount = IRound($amountTraded * self::REFRESH_PER_GOOD);
374
		//refresh goods that need it
375
		$refreshClass = $classTraded + 1;
376
		foreach ($this->getAllGoodIDs() as $goodID) {
377
			$goodClass = Globals::getGood($goodID)['Class'];
378
			if ($goodClass == $refreshClass) {
379
				$this->increaseGoodAmount($goodID, $refreshAmount);
380
			}
381
		}
382
	}
383
384
	protected function tradeGoods(array $good, int $goodsTraded, int $exp) : void {
385
		$goodsTradedMoney = $goodsTraded * self::GOODS_TRADED_MONEY_MULTIPLIER;
386
		$this->increaseUpgrade($goodsTradedMoney);
387
		$this->increaseCredits($goodsTradedMoney);
388
		$this->increaseExperience($exp);
389
		$this->decreaseGood($good, $goodsTraded, true);
390
	}
391
392
	public function buyGoods(array $good, int $goodsTraded, int $idealPrice, int $bargainPrice, int $exp) : void {
393
		$this->tradeGoods($good, $goodsTraded, $exp);
394
		// Limit upgrade/credits to prevent massive increases in a single trade
395
		$cappedBargainPrice = min(max($idealPrice, $goodsTraded * 1000), $bargainPrice);
396
		$this->increaseUpgrade($cappedBargainPrice);
397
		$this->increaseCredits($cappedBargainPrice);
398
	}
399
400
	public function sellGoods(array $good, int $goodsTraded, int $exp) : void {
401
		$this->tradeGoods($good, $goodsTraded, $exp);
402
	}
403
404
	public function stealGoods(array $good, int $goodsTraded) : void {
405
		$this->decreaseGood($good, $goodsTraded, false);
406
	}
407
408
	public function checkForUpgrade() : int {
409
		if ($this->isCachedVersion()) {
410
			throw new Exception('Cannot upgrade a cached port!');
411
		}
412
		$upgrades = 0;
413
		while ($this->upgrade >= $this->getUpgradeRequirement() && $this->level < 9) {
414
			++$upgrades;
415
			$this->decreaseUpgrade($this->getUpgradeRequirement());
416
			$this->decreaseCredits($this->getUpgradeRequirement());
417
			$this->doUpgrade();
418
		}
419
		return $upgrades;
420
	}
421
422
	/**
423
	 * This function should only be used in universe creation to set
424
	 * ports to a specific level.
425
	 */
426
	public function upgradeToLevel(int $level) : void {
427
		if ($this->isCachedVersion()) {
428
			throw new Exception('Cannot upgrade a cached port!');
429
		}
430
		while ($this->getLevel() < $level) {
431
			$this->doUpgrade();
432
		}
433
		while ($this->getLevel() > $level) {
434
			$this->doDowngrade();
435
		}
436
	}
437
438
	/**
439
	 * Returns the good class associated with the given level.
440
	 * If no level specified, will use the current port level.
441
	 * This is useful for determining what trade goods to add/remove.
442
	 */
443
	protected function getGoodClassAtLevel(int $level = null) : int {
444
		if ($level === null) {
445
			$level = $this->getLevel();
446
		}
447
		if ($level <= 2) {
448
			return 1;
449
		} elseif ($level <= 6) {
450
			return 2;
451
		} else {
452
			return 3;
453
		}
454
	}
455
456
	protected function selectAndAddGood(int $goodClass) : array {
457
		$GOODS = Globals::getGoods();
458
		shuffle($GOODS);
459
		foreach ($GOODS as $good) {
460
			if (!$this->hasGood($good['ID']) && $good['Class'] == $goodClass) {
461
				$transactionType = rand(1, 2) == 1 ? TRADER_BUYS : TRADER_SELLS;
462
				$this->addPortGood($good['ID'], $transactionType);
463
				return $good;
464
			}
465
		}
466
		throw new Exception('Failed to add a good!');
467
	}
468
469
	protected function doUpgrade() : void {
470
		if ($this->isCachedVersion()) {
471
			throw new Exception('Cannot upgrade a cached port!');
472
		}
473
474
		$this->increaseLevel(1);
475
		$goodClass = $this->getGoodClassAtLevel();
476
		$this->selectAndAddGood($goodClass);
477
478
		if ($this->getLevel() == 1) {
479
			// Add 2 extra goods when upgrading to level 1 (i.e. in Uni Gen)
480
			$this->selectAndAddGood($goodClass);
481
			$this->selectAndAddGood($goodClass);
482
		}
483
	}
484
485
	public function getUpgradeRequirement() : int {
486
//		return round(exp($this->getLevel()/1.7)+3)*1000000;
487
		return $this->getLevel() * 1000000;
488
	}
489
490
	/**
491
	 * Manually set port goods.
492
	 * Input must be an array of good_id => transaction.
493
	 * Only modifies goods that need to change.
494
	 * Returns false on invalid input.
495
	 */
496
	public function setPortGoods(array $goods) : bool {
497
		// Validate the input list of goods to make sure we have the correct
498
		// number of each good class for this port level.
499
		$givenClasses = [];
500
		foreach (array_keys($goods) as $goodID) {
501
			$givenClasses[] = Globals::getGood($goodID)['Class'];
502
		}
503
		$expectedClasses = [1, 1]; // Level 1 has 2 extra Class 1 goods
504
		foreach (range(1, $this->getLevel()) as $level) {
505
			$expectedClasses[] = $this->getGoodClassAtLevel($level);
506
		}
507
		if ($givenClasses != $expectedClasses) {
508
			return false;
509
		}
510
511
		// Remove goods not specified or that have the wrong transaction
512
		foreach ($this->getAllGoodIDs() as $goodID) {
513
			if (!isset($goods[$goodID]) || !$this->hasGood($goodID, $goods[$goodID])) {
514
				$this->removePortGood($goodID);
515
			}
516
		}
517
		// Add goods
518
		foreach ($goods as $goodID => $trans) {
519
			$this->addPortGood($goodID, $trans);
520
		}
521
		return true;
522
	}
523
524
	/**
525
	 * Add good with given ID to the port, with transaction $type
526
	 * as either "Buy" or "Sell", meaning the player buys or sells.
527
	 * If the port already has this transaction, do nothing.
528
	 *
529
	 * NOTE: make sure to adjust the port level appropriately if
530
	 * calling this function directly.
531
	 */
532
	public function addPortGood(int $goodID, string $type) : void {
533
		if ($this->isCachedVersion()) {
534
			throw new Exception('Cannot update a cached port!');
535
		}
536
		if ($this->hasGood($goodID, $type)) {
537
			return;
538
		}
539
540
		$this->goodIDs['All'][] = $goodID;
541
		$this->goodIDs[$type][] = $goodID;
542
		// sort ID arrays, since the good ID might not be the largest
543
		sort($this->goodIDs['All']);
544
		sort($this->goodIDs[$type]);
545
546
		$this->goodAmounts[$goodID] = Globals::getGood($goodID)['Max'];
547
		$this->cacheIsValid = false;
548
		$this->db->query('REPLACE INTO port_has_goods (game_id, sector_id, good_id, transaction_type, amount, last_update) VALUES (' . $this->db->escapeNumber($this->getGameID()) . ',' . $this->db->escapeNumber($this->getSectorID()) . ',' . $this->db->escapeNumber($goodID) . ',' . $this->db->escapeString($type) . ',' . $this->db->escapeNumber($this->getGoodAmount($goodID)) . ',' . $this->db->escapeNumber(Smr\Epoch::time()) . ')');
549
		$this->db->query('DELETE FROM route_cache WHERE game_id=' . $this->db->escapeNumber($this->getGameID()));
550
	}
551
552
	/**
553
	 * Remove good with given ID from the port.
554
	 * If the port does not have this good, do nothing.
555
	 *
556
	 * NOTE: make sure to adjust the port level appropriately if
557
	 * calling this function directly.
558
	 */
559
	public function removePortGood(int $goodID) : void {
560
		if ($this->isCachedVersion()) {
561
			throw new Exception('Cannot update a cached port!');
562
		}
563
		if (!$this->hasGood($goodID)) {
564
			return;
565
		}
566
		if (($key = array_search($goodID, $this->goodIDs['All'])) !== false) {
567
			array_splice($this->goodIDs['All'], $key, 1);
0 ignored issues
show
Bug introduced by
It seems like $key can also be of type string; however, parameter $offset of array_splice() does only seem to accept integer, maybe add an additional type check? ( Ignorable by Annotation )

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

567
			array_splice($this->goodIDs['All'], /** @scrutinizer ignore-type */ $key, 1);
Loading history...
568
		}
569
		if (($key = array_search($goodID, $this->goodIDs[TRADER_BUYS])) !== false) {
570
			array_splice($this->goodIDs[TRADER_BUYS], $key, 1);
571
		} elseif (($key = array_search($goodID, $this->goodIDs[TRADER_SELLS])) !== false) {
572
			array_splice($this->goodIDs[TRADER_SELLS], $key, 1);
573
		}
574
575
		$this->cacheIsValid = false;
576
		$this->db->query('DELETE FROM port_has_goods WHERE ' . $this->SQL . ' AND good_id=' . $this->db->escapeNumber($goodID) . ';');
577
		$this->db->query('DELETE FROM route_cache WHERE game_id=' . $this->db->escapeNumber($this->getGameID()));
578
	}
579
580
	/**
581
	 * Returns the number of port level downgrades due to damage taken.
582
	 */
583
	public function checkForDowngrade(int $damage) : int {
584
		$numDowngrades = 0;
585
		$numChances = floor($damage / self::DAMAGE_NEEDED_FOR_DOWNGRADE_CHANCE);
586
		for ($i = 0; $i < $numChances; $i++) {
587
			if (rand(1, 100) <= self::CHANCE_TO_DOWNGRADE && $this->level > 1) {
588
				++$numDowngrades;
589
				$this->doDowngrade();
590
			}
591
		}
592
		return $numDowngrades;
593
	}
594
595
	protected function selectAndRemoveGood(int $goodClass) : void {
596
		// Pick good to remove from the list of goods the port currently has
597
		$goodIDs = $this->getAllGoodIDs();
598
		shuffle($goodIDs);
599
600
		foreach ($goodIDs as $goodID) {
601
			$good = Globals::getGood($goodID);
602
			if ($good['Class'] == $goodClass) {
603
				$this->removePortGood($good['ID']);
604
				return;
605
			}
606
		}
607
		throw new Exception('Failed to remove a good!');
608
	}
609
610
	protected function doDowngrade() : void {
611
		if ($this->isCachedVersion()) {
612
			throw new Exception('Cannot downgrade a cached port!');
613
		}
614
615
		$goodClass = $this->getGoodClassAtLevel();
616
		$this->selectAndRemoveGood($goodClass);
617
618
		if ($this->getLevel() == 1) {
619
			// For level 1 ports, we don't want to have fewer goods
620
			$newGood = $this->selectAndAddGood($goodClass);
621
			// Set new good to 0 supply
622
			// (since other goods are set to 0 when port is destroyed)
623
			$this->setGoodAmount($newGood['ID'], 0);
624
		} else {
625
			// Don't make the port level 0
626
			$this->decreaseLevel(1);
627
		}
628
		$this->setUpgrade(0);
629
	}
630
631
	public function attackedBy(AbstractSmrPlayer $trigger, array $attackers) : void {
632
		if ($this->isCachedVersion()) {
633
			throw new Exception('Cannot attack a cached port!');
634
		}
635
636
		$trigger->increaseHOF(1, array('Combat', 'Port', 'Number Of Triggers'), HOF_PUBLIC);
637
		foreach ($attackers as $attacker) {
638
			$attacker->increaseHOF(1, array('Combat', 'Port', 'Number Of Attacks'), HOF_PUBLIC);
639
			$this->db->query('REPLACE INTO player_attacks_port (game_id, account_id, sector_id, time, level) VALUES
640
							(' . $this->db->escapeNumber($this->getGameID()) . ', ' . $this->db->escapeNumber($attacker->getAccountID()) . ', ' . $this->db->escapeNumber($this->getSectorID()) . ', ' . $this->db->escapeNumber(Smr\Epoch::time()) . ', ' . $this->db->escapeNumber($this->getLevel()) . ')');
641
		}
642
		if (!$this->isUnderAttack()) {
643
644
			//5 mins per port level
645
			$nextReinforce = Smr\Epoch::time() + $this->getLevel() * 300;
646
647
			$this->setReinforceTime($nextReinforce);
648
			$this->updateAttackStarted();
649
			//add news
650
			$newsMessage = '<span class="red bold">*MAYDAY* *MAYDAY*</span> A distress beacon has been activated by the port in sector ' . Globals::getSectorBBLink($this->getSectorID()) . '. It is under attack by ';
651
			if ($trigger->hasAlliance()) {
652
				$newsMessage .= 'members of ' . $trigger->getAllianceBBLink();
653
			} else {
654
				$newsMessage .= $trigger->getBBLink();
655
			}
656
657
			$newsMessage .= '. The Federal Government is offering ';
658
			$bounty = number_format(floor($trigger->getLevelID() * DEFEND_PORT_BOUNTY_PER_LEVEL));
659
660
			if ($trigger->hasAlliance()) {
661
				$newsMessage .= 'bounties of <span class="creds">' . $bounty . '</span> credits for the deaths of any raiding members of ' . $trigger->getAllianceBBLink();
662
			} else {
663
				$newsMessage .= 'a bounty of <span class="creds">' . $bounty . '</span> credits for the death of ' . $trigger->getBBLink();
664
			}
665
			$newsMessage .= ' prior to the destruction of the port, or until federal forces arrive to defend the port.';
666
//			$irc_message = '[k00,01]The port in sector [k11]'.$this->sectorID.'[k00] is under attack![/k]';
667
			$this->db->query('INSERT INTO news (game_id, time, news_message, type,killer_id,killer_alliance,dead_id) VALUES (' . $this->db->escapeNumber($this->getGameID()) . ',' . $this->db->escapeNumber(Smr\Epoch::time()) . ',' . $this->db->escapeString($newsMessage) . ',\'REGULAR\',' . $this->db->escapeNumber($trigger->getAccountID()) . ',' . $this->db->escapeNumber($trigger->getAllianceID()) . ',' . $this->db->escapeNumber(ACCOUNT_ID_PORT) . ')');
668
		}
669
	}
670
671
	public function getDisplayName() : string {
672
		return '<span style="color:yellow;font-variant:small-caps">Port ' . $this->getSectorID() . '</span>';
673
	}
674
675
	public function setShields(int $shields) : void {
676
		if ($this->isCachedVersion()) {
677
			throw new Exception('Cannot update a cached port!');
678
		}
679
		if ($shields < 0) {
680
			$shields = 0;
681
		}
682
		if ($this->shields == $shields) {
683
			return;
684
		}
685
		$this->shields = $shields;
686
		$this->hasChanged = true;
687
	}
688
689
	public function setArmour(int $armour) : void {
690
		if ($this->isCachedVersion()) {
691
			throw new Exception('Cannot update a cached port!');
692
		}
693
		if ($armour < 0) {
694
			$armour = 0;
695
		}
696
		if ($this->armour == $armour) {
697
			return;
698
		}
699
		$this->armour = $armour;
700
		$this->hasChanged = true;
701
	}
702
703
	public function setCDs(int $combatDrones) : void {
704
		if ($this->isCachedVersion()) {
705
			throw new Exception('Cannot update a cached port!');
706
		}
707
		if ($combatDrones < 0) {
708
			$combatDrones = 0;
709
		}
710
		if ($this->combatDrones == $combatDrones) {
711
			return;
712
		}
713
		$this->combatDrones = $combatDrones;
714
		$this->hasChanged = true;
715
	}
716
717
	public function setCreditsToDefault() : void {
718
		$this->setCredits(2700000 + $this->getLevel() * 1500000 + pow($this->getLevel(), 2) * 300000);
719
	}
720
721
	public function setCredits($credits) {
722
		if ($this->isCachedVersion()) {
723
			throw new Exception('Cannot update a cached port!');
724
		}
725
		if ($this->credits == $credits) {
726
			return;
727
		}
728
		$this->credits = $credits;
729
		$this->hasChanged = true;
730
	}
731
732
	public function decreaseCredits(int $credits) : void {
733
		if ($credits < 0) {
734
			throw new Exception('Cannot decrease negative credits.');
735
		}
736
		$this->setCredits($this->getCredits() - $credits);
737
	}
738
739
	public function increaseCredits(int $credits) : void {
740
		if ($credits < 0) {
741
			throw new Exception('Cannot increase negative credits.');
742
		}
743
		$this->setCredits($this->getCredits() + $credits);
744
	}
745
746
	public function setUpgrade(int $upgrade) : void {
747
		if ($this->isCachedVersion()) {
748
			throw new Exception('Cannot update a cached port!');
749
		}
750
		if ($this->getLevel() == $this->getMaxLevel()) {
751
			$upgrade = 0;
752
		}
753
		if ($this->upgrade == $upgrade) {
754
			return;
755
		}
756
		$this->upgrade = $upgrade;
757
		$this->hasChanged = true;
758
		$this->checkForUpgrade();
759
	}
760
761
	public function decreaseUpgrade(int $upgrade) : void {
762
		if ($upgrade < 0) {
763
			throw new Exception('Cannot decrease negative upgrade.');
764
		}
765
		$this->setUpgrade($this->getUpgrade() - $upgrade);
766
	}
767
768
	public function increaseUpgrade(int $upgrade) : void {
769
		if ($upgrade < 0) {
770
			throw new Exception('Cannot increase negative upgrade.');
771
		}
772
		$this->setUpgrade($this->getUpgrade() + $upgrade);
773
	}
774
775
	public function setLevel(int $level) : void {
776
		if ($this->isCachedVersion()) {
777
			throw new Exception('Cannot update a cached port!');
778
		}
779
		if ($this->level == $level) {
780
			return;
781
		}
782
		$this->level = $level;
783
		$this->hasChanged = true;
784
	}
785
786
	public function increaseLevel(int $level) : void {
787
		if ($level < 0) {
788
			throw new Exception('Cannot increase negative level.');
789
		}
790
		$this->setLevel($this->getLevel() + $level);
791
	}
792
793
	public function decreaseLevel(int $level) : void {
794
		if ($level < 0) {
795
			throw new Exception('Cannot increase negative level.');
796
		}
797
		$this->setLevel($this->getLevel() - $level);
798
	}
799
800
	public function setExperience(int $experience) : void {
801
		if ($this->isCachedVersion()) {
802
			throw new Exception('Cannot update a cached port!');
803
		}
804
		if ($this->experience == $experience) {
805
			return;
806
		}
807
		$this->experience = $experience;
808
		$this->hasChanged = true;
809
	}
810
811
	public function increaseExperience(int $experience) : void {
812
		if ($experience < 0) {
813
			throw new Exception('Cannot increase negative experience.');
814
		}
815
		$this->setExperience($this->getExperience() + $experience);
816
	}
817
818
	public function getGameID() : int {
819
		return $this->gameID;
820
	}
821
822
	public function getGame() : SmrGame {
823
		return SmrGame::getGame($this->gameID);
824
	}
825
826
	public function getSectorID() : int {
827
		return $this->sectorID;
828
	}
829
830
	public function getSector() : SmrSector {
831
		return SmrSector::getSector($this->getGameID(), $this->getSectorID());
832
	}
833
834
	public function setRaceID(int $raceID) : void {
835
		if ($this->raceID == $raceID) {
836
			return;
837
		}
838
		$this->raceID = $raceID;
839
		$this->hasChanged = true;
840
		$this->cacheIsValid = false;
841
		// route_cache tells NPC's where they can trade
842
		$this->db->query('DELETE FROM route_cache WHERE game_id=' . $this->db->escapeNumber($this->getGameID()));
843
	}
844
845
	public function getLevel() : int {
846
		return $this->level;
847
	}
848
849
	public function getMaxLevel() : int {
850
		// Hunter Wars redefines this, so use lazy static binding
851
		return static::MAX_LEVEL;
852
	}
853
854
	public function getShields() : int {
855
		return $this->shields;
856
	}
857
858
	public function hasShields() : bool {
859
		return ($this->getShields() > 0);
860
	}
861
862
	public function getCDs() : int {
863
		return $this->combatDrones;
864
	}
865
866
	public function hasCDs() : bool {
867
		return ($this->getCDs() > 0);
868
	}
869
870
	public function getArmour() : int {
871
		return $this->armour;
872
	}
873
874
	public function hasArmour() : bool {
875
		return ($this->getArmour() > 0);
876
	}
877
878
	public function getExperience() : int {
879
		return $this->experience;
880
	}
881
882
	public function getCredits() : int {
883
		return $this->credits;
884
	}
885
886
	public function getUpgrade() : int {
887
		return $this->upgrade;
888
	}
889
890
	public function getNumWeapons() : int {
891
		return $this->getLevel() + 3;
892
	}
893
894
	public function getWeapons() : array {
895
		$weapons = array();
896
		for ($i = 0; $i < $this->getNumWeapons(); ++$i) {
897
			$weapons[$i] = SmrWeapon::getWeapon(WEAPON_PORT_TURRET);
898
		}
899
		return $weapons;
900
	}
901
902
	public function getUpgradePercent() : float {
903
		return min(1, max(0, $this->upgrade / $this->getUpgradeRequirement()));
904
	}
905
906
	public function getCreditsPercent() : float {
907
		return min(1, max(0, $this->credits / 32000000));
908
	}
909
910
	public function getReinforcePercent() : float {
911
		if (!$this->isUnderAttack()) {
912
			return 0;
913
		}
914
		return min(1, max(0, ($this->getReinforceTime() - Smr\Epoch::time()) / ($this->getReinforceTime() - $this->getAttackStarted())));
915
	}
916
917
	public function getReinforceTime() : int {
918
		return $this->reinforceTime;
919
	}
920
921
	public function setReinforceTime(int $reinforceTime) : void {
922
		if ($this->reinforceTime == $reinforceTime) {
923
			return;
924
		}
925
		$this->reinforceTime = $reinforceTime;
926
		$this->hasChanged = true;
927
	}
928
929
	public function getAttackStarted() : int {
930
		return $this->attackStarted;
931
	}
932
933
	private function updateAttackStarted() : void {
934
		$this->setAttackStarted(Smr\Epoch::time());
935
	}
936
937
	private function setAttackStarted(int $time) : void {
938
		if ($this->attackStarted == $time) {
939
			return;
940
		}
941
		$this->attackStarted = $time;
942
		$this->hasChanged = true;
943
	}
944
945
	public function isUnderAttack() : bool {
946
		return ($this->getReinforceTime() >= Smr\Epoch::time());
947
	}
948
949
	public function isDestroyed() : bool {
950
		return ($this->getArmour() < 1 && $this->isUnderAttack());
951
	}
952
953
	public function exists() : bool {
954
		return $this->isNew === false || $this->hasChanged === true;
955
	}
956
957
	public function decreaseShields(int $number) : void {
958
		$this->setShields($this->getShields() - $number);
959
	}
960
961
	public function decreaseCDs(int $number) : void {
962
		$this->setCDs($this->getCDs() - $number);
963
	}
964
965
	public function decreaseArmour(int $number) : void {
966
		$this->setArmour($this->getArmour() - $number);
967
	}
968
969
	public function getTradeRestriction(SmrPlayer $player) : string|false {
970
		if (!$this->exists()) {
971
			return 'There is no port in this sector!';
972
		}
973
		if ($this->getSectorID() != $player->getSectorID()) {
974
			return 'That port is not in this sector!';
975
		}
976
		if ($player->getRelation($this->getRaceID()) <= RELATIONS_WAR) {
977
			return 'We will not trade with our enemies!';
978
		}
979
		if ($this->isUnderAttack()) {
980
			return 'We are still repairing damage caused during the last raid.';
981
		}
982
		return false;
983
	}
984
985
	public function getIdealPrice(int $goodID, string $transactionType, int $numGoods, int $relations) : int {
986
		$supply = $this->getGoodAmount($goodID);
987
		$dist = $this->getGoodDistance($goodID);
988
		return self::idealPrice($goodID, $transactionType, $numGoods, $relations, $supply, $dist);
989
}
990
991
	/**
992
	 * Generic ideal price calculation, given all parameters as input.
993
	 */
994
	public static function idealPrice(int $goodID, string $transactionType, int $numGoods, int $relations, int $supply, int $dist) : int {
995
		$relations = min(1000, $relations); // no effect for higher relations
996
		$good = Globals::getGood($goodID);
997
		$base = $good['BasePrice'] * $numGoods;
998
		$maxSupply = $good['Max'];
999
1000
		$distFactor = pow($dist, 1.3);
1001
		if ($transactionType === TRADER_SELLS) {
1002
			$supplyFactor = 1 + ($supply / $maxSupply);
1003
			$relationsFactor = 1.2 + 1.8 * ($relations / 1000); // [0.75-3]
1004
			$scale = 0.088;
1005
		} elseif ($transactionType === TRADER_BUYS) {
1006
			$supplyFactor = 2 - ($supply / $maxSupply);
1007
			$relationsFactor = 3 - 2 * ($relations / 1000);
1008
			$scale = 0.03;
1009
		} else {
1010
			throw new Exception('Unknown transaction type');
1011
		}
1012
		return IRound($base * $scale * $distFactor * $supplyFactor * $relationsFactor);
1013
	}
1014
1015
	public function getOfferPrice(int $idealPrice, int $relations, string $transactionType) : int {
1016
		$relations = min(1000, $relations); // no effect for higher relations
1017
		$relationsEffect = (2 * $relations + 8000) / 10000; // [0.75-1]
1018
1019
		if ($transactionType === TRADER_BUYS) {
1020
			$relationsEffect = 2 - $relationsEffect;
1021
			return max($idealPrice, IFloor($idealPrice * $relationsEffect));
1022
		} else {
1023
			return min($idealPrice, ICeil($idealPrice * $relationsEffect));
1024
		}
1025
	}
1026
1027
	/**
1028
	 * Return the fraction of max exp earned.
1029
	 */
1030
	public function calculateExperiencePercent(int $idealPrice, int $bargainPrice, string $transactionType) : float {
1031
		if ($bargainPrice == $idealPrice || $transactionType === TRADER_STEALS) {
1032
			// Stealing always gives full exp
1033
			return 1;
1034
		}
1035
1036
		$offerPriceNoRelations = $this->getOfferPrice($idealPrice, 0, $transactionType);
1037
1038
		// Avoid division by 0 in the case where the ideal price is so small
1039
		// that relations have no impact on the offered price.
1040
		$denom = max(1, abs($idealPrice - $offerPriceNoRelations));
1041
1042
		$expPercent = 1 - abs(($idealPrice - $bargainPrice) / $denom);
1043
		return max(0, min(1, $expPercent));
1044
	}
1045
1046
	public function getRaidWarningHREF() : string {
1047
		return Page::create('skeleton.php', 'port_attack_warning.php')->href();
1048
	}
1049
1050
	public function getAttackHREF() : string {
1051
		$container = Page::create('port_attack_processing.php');
1052
		$container['port_id'] = $this->getSectorID();
1053
		return $container->href();
1054
	}
1055
1056
	public function getClaimHREF() : string {
1057
		$container = Page::create('port_claim_processing.php');
1058
		$container['port_id'] = $this->getSectorID();
1059
		return $container->href();
1060
	}
1061
1062
	public function getRazeHREF(bool $justContainer = false) : string|Page {
1063
		$container = Page::create('port_payout_processing.php');
1064
		$container['PayoutType'] = 'Raze';
1065
		return $justContainer === false ? $container->href() : $container;
1066
	}
1067
1068
	public function getLootHREF(bool $justContainer = false) : string|Page {
1069
		if ($this->getCredits() > 0) {
1070
			$container = Page::create('port_payout_processing.php');
1071
			$container['PayoutType'] = 'Loot';
1072
		} else {
1073
			$container = Page::create('skeleton.php', 'current_sector.php');
1074
			$container['msg'] = 'This port has already been looted.';
1075
		}
1076
		return $justContainer === false ? $container->href() : $container;
1077
	}
1078
1079
	public function getLootGoodHREF(int $boughtGoodID) : string {
1080
		$container = Page::create('port_loot_processing.php');
1081
		$container['GoodID'] = $boughtGoodID;
1082
		return $container->href();
1083
	}
1084
1085
	public function isCachedVersion() : bool {
1086
		return $this->cachedVersion;
1087
	}
1088
1089
	public function getCachedTime() : int {
1090
		return $this->cachedTime;
1091
	}
1092
1093
	protected function setCachedTime(int $cachedTime) : void {
1094
		$this->cachedTime = $cachedTime;
1095
	}
1096
1097
	public function updateSectorPlayersCache() : void {
1098
		$accountIDs = array();
1099
		$sectorPlayers = $this->getSector()->getPlayers();
1100
		foreach ($sectorPlayers as $sectorPlayer) {
1101
			$accountIDs[] = $sectorPlayer->getAccountID();
1102
		}
1103
		$this->addCachePorts($accountIDs);
1104
	}
1105
1106
	public function addCachePort(int $accountID) : void {
1107
		$this->addCachePorts(array($accountID));
1108
	}
1109
1110
	public function addCachePorts(array $accountIDs) : bool {
1111
		if (count($accountIDs) > 0 && $this->exists()) {
1112
			$cache = $this->db->escapeObject($this, true);
1113
			$cacheHash = $this->db->escapeString(md5($cache));
1114
			//give them the port info
1115
			$query = 'INSERT IGNORE INTO player_visited_port ' .
1116
						'(account_id, game_id, sector_id, visited, port_info_hash) ' .
1117
						'VALUES ';
1118
			foreach ($accountIDs as $accountID) {
1119
				$query .= '(' . $accountID . ', ' . $this->getGameID() . ', ' . $this->getSectorID() . ', 0, \'\'),';
1120
			}
1121
			$query = substr($query, 0, -1);
1122
			$this->db->query($query);
1123
1124
			$this->db->query('INSERT IGNORE INTO port_info_cache
1125
						(game_id, sector_id, port_info_hash, port_info)
1126
						VALUES (' . $this->db->escapeNumber($this->getGameID()) . ', ' . $this->db->escapeNumber($this->getSectorID()) . ', ' . $cacheHash . ', ' . $cache . ')');
1127
1128
			// We can't use the SQL member here because CachePorts don't have it
1129
			$this->db->query('UPDATE player_visited_port SET visited=' . $this->db->escapeNumber($this->getCachedTime()) . ', port_info_hash=' . $cacheHash . ' WHERE visited<=' . $this->db->escapeNumber($this->getCachedTime()) . ' AND account_id IN (' . $this->db->escapeArray($accountIDs) . ') AND sector_id=' . $this->db->escapeNumber($this->getSectorID()) . ' AND game_id=' . $this->db->escapeNumber($this->getGameID()) . ' LIMIT ' . count($accountIDs));
1130
1131
			unset($cache);
1132
			return true;
1133
		}
1134
		return false;
1135
	}
1136
1137
	public static function getCachedPort(int $gameID, int $sectorID, int $accountID, bool $forceUpdate = false) : self|false {
1138
		if ($forceUpdate || !isset(self::$CACHE_CACHED_PORTS[$gameID][$sectorID][$accountID])) {
1139
			$db = Smr\Database::getInstance();
1140
			$db->query('SELECT visited, port_info
1141
						FROM player_visited_port
1142
						JOIN port_info_cache USING (game_id,sector_id,port_info_hash)
1143
						WHERE account_id = ' . $db->escapeNumber($accountID) . '
1144
							AND game_id = ' . $db->escapeNumber($gameID) . '
1145
							AND sector_id = ' . $db->escapeNumber($sectorID) . ' LIMIT 1');
1146
1147
			if ($db->nextRecord()) {
1148
				self::$CACHE_CACHED_PORTS[$gameID][$sectorID][$accountID] = $db->getObject('port_info', true);
1149
				self::$CACHE_CACHED_PORTS[$gameID][$sectorID][$accountID]->setCachedTime($db->getInt('visited'));
1150
			} else {
1151
				self::$CACHE_CACHED_PORTS[$gameID][$sectorID][$accountID] = false;
1152
			}
1153
		}
1154
		return self::$CACHE_CACHED_PORTS[$gameID][$sectorID][$accountID];
1155
	}
1156
1157
	// This is a magic method used when serializing an SmrPort instance.
1158
	// It designates which members should be included in the serialization.
1159
	public function __sleep() {
1160
		// We omit `goodAmounts` and `goodDistances` so that the hash of the
1161
		// serialized object is the same for all players. This greatly improves
1162
		// cache efficiency.
1163
		return array('gameID', 'sectorID', 'raceID', 'level', 'goodIDs');
1164
	}
1165
1166
	public function __wakeup() {
1167
		$this->cachedVersion = true;
1168
		$this->db = Smr\Database::getInstance();
1169
	}
1170
1171
	public function update() : void {
1172
		if ($this->isCachedVersion()) {
1173
			throw new Exception('Cannot update a cached port!');
1174
		}
1175
		if (!$this->exists()) {
1176
			return;
1177
		}
1178
1179
		// If any cached members (see `__sleep`) changed, update the cached port
1180
		if (!$this->cacheIsValid) {
1181
			$this->updateSectorPlayersCache();
1182
		}
1183
1184
		// If any fields in the `port` table have changed, update table
1185
		if ($this->hasChanged) {
1186
			if ($this->isNew === false) {
1187
				$this->db->query('UPDATE port SET experience = ' . $this->db->escapeNumber($this->getExperience()) .
1188
								', shields = ' . $this->db->escapeNumber($this->getShields()) .
1189
								', armour = ' . $this->db->escapeNumber($this->getArmour()) .
1190
								', combat_drones = ' . $this->db->escapeNumber($this->getCDs()) .
1191
								', level = ' . $this->db->escapeNumber($this->getLevel()) .
1192
								', credits = ' . $this->db->escapeNumber($this->getCredits()) .
1193
								', upgrade = ' . $this->db->escapeNumber($this->getUpgrade()) .
1194
								', reinforce_time = ' . $this->db->escapeNumber($this->getReinforceTime()) .
1195
								', attack_started = ' . $this->db->escapeNumber($this->getAttackStarted()) .
1196
								', race_id = ' . $this->db->escapeNumber($this->getRaceID()) . '
1197
								WHERE ' . $this->SQL . ' LIMIT 1');
1198
			} else {
1199
				$this->db->query('INSERT INTO port (game_id,sector_id,experience,shields,armour,combat_drones,level,credits,upgrade,reinforce_time,attack_started,race_id)
1200
								values
1201
								(' . $this->db->escapeNumber($this->getGameID()) .
1202
								',' . $this->db->escapeNumber($this->getSectorID()) .
1203
								',' . $this->db->escapeNumber($this->getExperience()) .
1204
								',' . $this->db->escapeNumber($this->getShields()) .
1205
								',' . $this->db->escapeNumber($this->getArmour()) .
1206
								',' . $this->db->escapeNumber($this->getCDs()) .
1207
								',' . $this->db->escapeNumber($this->getLevel()) .
1208
								',' . $this->db->escapeNumber($this->getCredits()) .
1209
								',' . $this->db->escapeNumber($this->getUpgrade()) .
1210
								',' . $this->db->escapeNumber($this->getReinforceTime()) .
1211
								',' . $this->db->escapeNumber($this->getAttackStarted()) .
1212
								',' . $this->db->escapeNumber($this->getRaceID()) . ')');
1213
				$this->isNew = false;
1214
			}
1215
			$this->hasChanged = false;
1216
		}
1217
1218
		// Update the port good amounts if they have been changed
1219
		// (Note: `restockGoods` alone does not trigger this)
1220
		foreach ($this->goodAmountsChanged as $goodID => $doUpdate) {
1221
			if (!$doUpdate) { continue; }
1222
			$amount = $this->getGoodAmount($goodID);
1223
			$this->db->query('UPDATE port_has_goods SET amount = ' . $this->db->escapeNumber($amount) . ', last_update = ' . $this->db->escapeNumber(Smr\Epoch::time()) . ' WHERE ' . $this->SQL . ' AND good_id = ' . $this->db->escapeNumber($goodID) . ' LIMIT 1');
1224
		}
1225
	}
1226
1227
	public function shootPlayers(array $targetPlayers) : array {
1228
		$results = array('Port' => $this, 'TotalDamage' => 0, 'TotalDamagePerTargetPlayer' => array());
1229
		foreach ($targetPlayers as $targetPlayer) {
1230
			$results['TotalDamagePerTargetPlayer'][$targetPlayer->getAccountID()] = 0;
1231
			$results['TotalShotsPerTargetPlayer'][$targetPlayer->getAccountID()] = 0;
1232
		}
1233
		if ($this->isDestroyed()) {
1234
			$results['DeadBeforeShot'] = true;
1235
			return $results;
1236
		}
1237
		$results['DeadBeforeShot'] = false;
1238
		$weapons = $this->getWeapons();
1239
		foreach ($weapons as $orderID => $weapon) {
1240
			do {
1241
				$targetPlayer = array_rand_value($targetPlayers);
1242
			} while ($results['TotalShotsPerTargetPlayer'][$targetPlayer->getAccountID()] > min($results['TotalShotsPerTargetPlayer']));
1243
			$results['Weapons'][$orderID] = $weapon->shootPlayerAsPort($this, $targetPlayer);
1244
			$results['TotalShotsPerTargetPlayer'][$targetPlayer->getAccountID()]++;
1245
			if ($results['Weapons'][$orderID]['Hit']) {
1246
				$results['TotalDamage'] += $results['Weapons'][$orderID]['ActualDamage']['TotalDamage'];
1247
				$results['TotalDamagePerTargetPlayer'][$targetPlayer->getAccountID()] += $results['Weapons'][$orderID]['ActualDamage']['TotalDamage'];
1248
			}
1249
		}
1250
		if ($this->hasCDs()) {
1251
			$thisCDs = new SmrCombatDrones($this->getCDs(), true);
1252
			$results['Drones'] = $thisCDs->shootPlayerAsPort($this, array_rand_value($targetPlayers));
1253
			$results['TotalDamage'] += $results['Drones']['ActualDamage']['TotalDamage'];
1254
			$results['TotalDamagePerTargetPlayer'][$results['Drones']['TargetPlayer']->getAccountID()] += $results['Drones']['ActualDamage']['TotalDamage'];
1255
		}
1256
		return $results;
1257
	}
1258
1259
	public function doWeaponDamage(array $damage) : array {
1260
		$alreadyDead = $this->isDestroyed();
1261
		$shieldDamage = 0;
1262
		$cdDamage = 0;
1263
		$armourDamage = 0;
1264
		if (!$alreadyDead) {
1265
			if ($damage['Shield'] || !$this->hasShields()) {
1266
				$shieldDamage = $this->doShieldDamage(min($damage['MaxDamage'], $damage['Shield']));
1267
				$damage['MaxDamage'] -= $shieldDamage;
1268
				if (!$this->hasShields() && ($shieldDamage == 0 || $damage['Rollover'])) {
1269
					$cdDamage = $this->doCDDamage(min($damage['MaxDamage'], $damage['Armour']));
1270
					$damage['Armour'] -= $cdDamage;
1271
					$damage['MaxDamage'] -= $cdDamage;
1272
					if (!$this->hasCDs() && ($cdDamage == 0 || $damage['Rollover'])) {
1273
						$armourDamage = $this->doArmourDamage(min($damage['MaxDamage'], $damage['Armour']));
1274
					}
1275
				}
1276
			} else { //hit drones behind shields
1277
				$cdDamage = $this->doCDDamage(IFloor(min($damage['MaxDamage'], $damage['Armour']) * DRONES_BEHIND_SHIELDS_DAMAGE_PERCENT));
1278
			}
1279
		}
1280
1281
		$return = array(
1282
						'KillingShot' => !$alreadyDead && $this->isDestroyed(),
1283
						'TargetAlreadyDead' => $alreadyDead,
1284
						'Shield' => $shieldDamage,
1285
						'HasShields' => $this->hasShields(),
1286
						'CDs' => $cdDamage,
1287
						'NumCDs' => $cdDamage / CD_ARMOUR,
1288
						'HasCDs' => $this->hasCDs(),
1289
						'Armour' => $armourDamage,
1290
						'TotalDamage' => $shieldDamage + $cdDamage + $armourDamage
1291
		);
1292
		return $return;
1293
	}
1294
1295
	protected function doShieldDamage(int $damage) : int {
1296
		$actualDamage = min($this->getShields(), $damage);
1297
		$this->decreaseShields($actualDamage);
1298
		return $actualDamage;
1299
	}
1300
1301
	protected function doCDDamage(int $damage) : int {
1302
		$actualDamage = min($this->getCDs(), IFloor($damage / CD_ARMOUR));
1303
		$this->decreaseCDs($actualDamage);
1304
		return $actualDamage * CD_ARMOUR;
1305
	}
1306
1307
	protected function doArmourDamage(int $damage) : int {
1308
		$actualDamage = min($this->getArmour(), IFloor($damage));
1309
		$this->decreaseArmour($actualDamage);
1310
		return $actualDamage;
1311
	}
1312
1313
	protected function getAttackersToCredit() : array {
1314
		//get all players involved for HoF
1315
		$attackers = array();
1316
		$this->db->query('SELECT account_id FROM player_attacks_port WHERE ' . $this->SQL . ' AND time > ' . $this->db->escapeNumber(Smr\Epoch::time() - self::TIME_TO_CREDIT_RAID));
1317
		while ($this->db->nextRecord()) {
1318
			$attackers[] = SmrPlayer::getPlayer($this->db->getInt('account_id'), $this->getGameID());
1319
		}
1320
		return $attackers;
1321
	}
1322
1323
	protected function creditCurrentAttackersForKill() : void {
1324
		//get all players involved for HoF
1325
		$attackers = $this->getAttackersToCredit();
1326
		foreach ($attackers as $attacker) {
1327
			$attacker->increaseHOF($this->level, array('Combat', 'Port', 'Levels Raided'), HOF_PUBLIC);
1328
			$attacker->increaseHOF(1, array('Combat', 'Port', 'Total Raided'), HOF_PUBLIC);
1329
		}
1330
	}
1331
1332
	protected function payout(AbstractSmrPlayer $killer, int $credits, string $payoutType) : bool {
1333
		if ($this->getCredits() == 0) {
1334
			return false;
1335
		}
1336
		$killer->increaseCredits($credits);
1337
		$killer->increaseHOF($credits, array('Combat', 'Port', 'Money', 'Gained'), HOF_PUBLIC);
1338
		$attackers = $this->getAttackersToCredit();
1339
		foreach ($attackers as $attacker) {
1340
			$attacker->increaseHOF(1, array('Combat', 'Port', $payoutType), HOF_PUBLIC);
1341
		}
1342
		$this->setCredits(0);
1343
		return true;
1344
	}
1345
1346
	/**
1347
	 * Get a reduced fraction of the credits stored in the port for razing
1348
	 * after a successful port raid.
1349
	 */
1350
	public function razePort(AbstractSmrPlayer $killer) : int {
1351
		$credits = IFloor($this->getCredits() * self::BASE_PAYOUT * self::RAZE_PAYOUT);
1352
		if ($this->payout($killer, $credits, 'Razed')) {
1353
			$this->doDowngrade();
1354
		}
1355
		return $credits;
1356
	}
1357
1358
	/**
1359
	 * Get a fraction of the credits stored in the port for looting after a
1360
	 * successful port raid.
1361
	 */
1362
	public function lootPort(AbstractSmrPlayer $killer) : int {
1363
		$credits = IFloor($this->getCredits() * self::BASE_PAYOUT);
1364
		$this->payout($killer, $credits, 'Looted');
1365
		return $credits;
1366
	}
1367
1368
	public function killPortByPlayer(AbstractSmrPlayer $killer) : array {
1369
		$return = array();
1370
1371
		// Port is destroyed, so empty the port of all trade goods
1372
		foreach ($this->getAllGoodIDs() as $goodID) {
1373
			$this->setGoodAmount($goodID, 0);
1374
		}
1375
1376
		$this->creditCurrentAttackersForKill();
1377
1378
		// News Entry
1379
		$news = $this->getDisplayName() . ' has been successfully raided by ';
1380
		if ($killer->hasAlliance()) {
1381
			$news .= 'the members of <span class="yellow">' . $killer->getAllianceBBLink() . '</span>';
1382
		} else {
1383
			$news .= $killer->getBBLink();
1384
		}
1385
		$this->db->query('INSERT INTO news (game_id, time, news_message, type,killer_id,killer_alliance,dead_id) VALUES (' . $this->db->escapeNumber($this->getGameID()) . ', ' . $this->db->escapeNumber(Smr\Epoch::time()) . ', ' . $this->db->escapeString($news) . ', \'REGULAR\',' . $this->db->escapeNumber($killer->getAccountID()) . ',' . $this->db->escapeNumber($killer->getAllianceID()) . ',' . $this->db->escapeNumber(ACCOUNT_ID_PORT) . ')');
1386
		// Killer gets a relations change and a bounty if port is taken
1387
		$return['KillerBounty'] = $killer->getExperience() * $this->getLevel();
1388
		$killer->increaseCurrentBountyAmount('HQ', $return['KillerBounty']);
1389
		$killer->increaseHOF($return['KillerBounty'], array('Combat', 'Port', 'Bounties', 'Gained'), HOF_PUBLIC);
1390
1391
		$return['KillerRelations'] = 45;
1392
		$killer->decreaseRelations($return['KillerRelations'], $this->getRaceID());
1393
		$killer->increaseHOF($return['KillerRelations'], array('Combat', 'Port', 'Relation', 'Loss'), HOF_PUBLIC);
1394
1395
		return $return;
1396
	}
1397
1398
	public function hasX(mixed $x) : bool {
1399
		if (is_array($x) && $x['Type'] == 'Good') { // instanceof Good) - No Good class yet, so array is the best we can do
1400
			if (isset($x['ID'])) {
1401
				return $this->hasGood($x['ID'], $x['TransactionType'] ?? null);
1402
			}
1403
		}
1404
		return false;
1405
	}
1406
}
1407