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

Passed
Push — live ( bd440a...b758da )
by Dan
04:19
created

AbstractSmrPort::getGood()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 5
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 3
c 0
b 0
f 0
nc 2
nop 1
dl 0
loc 5
rs 10
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
		$dbResult = $db->read('SELECT port.* FROM port LEFT JOIN sector USING(game_id, sector_id) WHERE game_id = ' . $db->escapeNumber($gameID) . ' AND galaxy_id = ' . $db->escapeNumber($galaxyID));
76
		$galaxyPorts = [];
77
		foreach ($dbResult->records() as $dbRecord) {
78
			$sectorID = $dbRecord->getInt('sector_id');
79
			$port = self::getPort($gameID, $sectorID, $forceUpdate, $dbRecord);
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\DatabaseRecord $dbRecord = null) : self {
89
		if ($forceUpdate || !isset(self::$CACHE_PORTS[$gameID][$sectorID])) {
90
			self::$CACHE_PORTS[$gameID][$sectorID] = new SmrPort($gameID, $sectorID, $dbRecord);
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->write('DELETE FROM port WHERE ' . $SQL);
100
		$db->write('DELETE FROM port_has_goods WHERE ' . $SQL);
101
		$db->write('DELETE FROM player_visited_port WHERE ' . $SQL);
102
		$db->write('DELETE FROM player_attacks_port WHERE ' . $SQL);
103
		$db->write('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\DatabaseRecord $dbRecord = 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 ($dbRecord === null) {
134
			$dbResult = $this->db->read('SELECT * FROM port WHERE ' . $this->SQL . ' LIMIT 1');
135
			if ($dbResult->hasRecord()) {
136
				$dbRecord = $dbResult->record();
137
			}
138
		}
139
		$this->isNew = $dbRecord === null;
140
141
		$this->gameID = $gameID;
142
		$this->sectorID = $sectorID;
143
		if (!$this->isNew) {
144
			$this->shields = $dbRecord->getInt('shields');
145
			$this->combatDrones = $dbRecord->getInt('combat_drones');
146
			$this->armour = $dbRecord->getInt('armour');
147
			$this->reinforceTime = $dbRecord->getInt('reinforce_time');
148
			$this->attackStarted = $dbRecord->getInt('attack_started');
149
			$this->raceID = $dbRecord->getInt('race_id');
150
			$this->level = $dbRecord->getInt('level');
151
			$this->credits = $dbRecord->getInt('credits');
152
			$this->upgrade = $dbRecord->getInt('upgrade');
153
			$this->experience = $dbRecord->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->write('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
			$dbResult = $this->db->read('SELECT * FROM port_has_goods WHERE ' . $this->SQL . ' ORDER BY good_id ASC');
228
			foreach ($dbResult->records() as $dbRecord) {
229
				$goodID = $dbRecord->getInt('good_id');
230
				$transactionType = $dbRecord->getField('transaction_type');
231
				$this->goodAmounts[$goodID] = $dbRecord->getInt('amount');
232
				$this->goodIDs[$transactionType][] = $goodID;
233
				$this->goodIDs['All'][] = $goodID;
234
235
				$secondsSinceLastUpdate = Smr\Epoch::time() - $dbRecord->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 getGoodDistance(int $goodID) : int {
286
		if (!isset($this->goodDistances[$goodID])) {
287
			$x = Globals::getGood($goodID);
288
			$x['TransactionType'] = $this->getGoodTransaction($goodID);
289
			$di = Plotter::findDistanceToX($x, $this->getSector(), true);
290
			if (is_object($di)) {
0 ignored issues
show
introduced by
The condition is_object($di) is always false.
Loading history...
291
				$di = $di->getRelativeDistance();
292
			}
293
			$this->goodDistances[$goodID] = max(1, $di);
294
		}
295
		return $this->goodDistances[$goodID];
296
	}
297
298
	/**
299
	 * Returns the transaction type for this good (Buy or Sell).
300
	 * Note: this is the player's transaction, not the port's.
301
	 */
302
	public function getGoodTransaction(int $goodID) : string {
303
		foreach ([TRADER_BUYS, TRADER_SELLS] as $transaction) {
304
			if ($this->hasGood($goodID, $transaction)) {
305
				return $transaction;
306
			}
307
		}
308
		throw new Exception('Port does not trade goodID ' . $goodID);
309
	}
310
311
	public function hasGood(int $goodID, ?string $type = null) : bool {
312
		if ($type === null) {
313
			$type = 'All';
314
		}
315
		return in_array($goodID, $this->goodIDs[$type]);
316
	}
317
318
	private function setGoodAmount(int $goodID, int $amount, bool $doUpdate = true) : void {
319
		if ($this->isCachedVersion()) {
320
			throw new Exception('Cannot update a cached port!');
321
		}
322
		// The new amount must be between 0 and the max for this good
323
		$amount = max(0, min($amount, Globals::getGood($goodID)['Max']));
324
		if ($this->getGoodAmount($goodID) == $amount) {
325
			return;
326
		}
327
		$this->goodAmounts[$goodID] = $amount;
328
329
		if ($doUpdate) {
330
			// This goodID will be changed in the db during `update()`
331
			$this->goodAmountsChanged[$goodID] = true;
332
		}
333
	}
334
335
	public function getGoodAmount(int $goodID) : int {
336
		return $this->goodAmounts[$goodID];
337
	}
338
339
	public function decreaseGood(array $good, int $amount, bool $doRefresh) : void {
340
		$this->setGoodAmount($good['ID'], $this->getGoodAmount($good['ID']) - $amount);
341
		if ($doRefresh === true) {
342
			//get id of goods to replenish
343
			$this->refreshGoods($good['Class'], $amount);
344
		}
345
	}
346
347
	public function increaseGoodAmount(int $goodID, int $amount) : void {
348
		$this->setGoodAmount($goodID, $this->getGoodAmount($goodID) + $amount);
349
	}
350
351
	public function decreaseGoodAmount(int $goodID, int $amount) : void {
352
		$this->setGoodAmount($goodID, $this->getGoodAmount($goodID) - $amount);
353
	}
354
355
	/**
356
	 * Adds extra stock to goods in the tier above a good that was traded
357
	 */
358
	protected function refreshGoods(int $classTraded, int $amountTraded) : void {
359
		$refreshAmount = IRound($amountTraded * self::REFRESH_PER_GOOD);
360
		//refresh goods that need it
361
		$refreshClass = $classTraded + 1;
362
		foreach ($this->getAllGoodIDs() as $goodID) {
363
			$goodClass = Globals::getGood($goodID)['Class'];
364
			if ($goodClass == $refreshClass) {
365
				$this->increaseGoodAmount($goodID, $refreshAmount);
366
			}
367
		}
368
	}
369
370
	protected function tradeGoods(array $good, int $goodsTraded, int $exp) : void {
371
		$goodsTradedMoney = $goodsTraded * self::GOODS_TRADED_MONEY_MULTIPLIER;
372
		$this->increaseUpgrade($goodsTradedMoney);
373
		$this->increaseCredits($goodsTradedMoney);
374
		$this->increaseExperience($exp);
375
		$this->decreaseGood($good, $goodsTraded, true);
376
	}
377
378
	public function buyGoods(array $good, int $goodsTraded, int $idealPrice, int $bargainPrice, int $exp) : void {
379
		$this->tradeGoods($good, $goodsTraded, $exp);
380
		// Limit upgrade/credits to prevent massive increases in a single trade
381
		$cappedBargainPrice = min(max($idealPrice, $goodsTraded * 1000), $bargainPrice);
382
		$this->increaseUpgrade($cappedBargainPrice);
383
		$this->increaseCredits($cappedBargainPrice);
384
	}
385
386
	public function sellGoods(array $good, int $goodsTraded, int $exp) : void {
387
		$this->tradeGoods($good, $goodsTraded, $exp);
388
	}
389
390
	public function stealGoods(array $good, int $goodsTraded) : void {
391
		$this->decreaseGood($good, $goodsTraded, false);
392
	}
393
394
	public function checkForUpgrade() : int {
395
		if ($this->isCachedVersion()) {
396
			throw new Exception('Cannot upgrade a cached port!');
397
		}
398
		$upgrades = 0;
399
		while ($this->upgrade >= $this->getUpgradeRequirement() && $this->level < 9) {
400
			++$upgrades;
401
			$this->decreaseUpgrade($this->getUpgradeRequirement());
402
			$this->decreaseCredits($this->getUpgradeRequirement());
403
			$this->doUpgrade();
404
		}
405
		return $upgrades;
406
	}
407
408
	/**
409
	 * This function should only be used in universe creation to set
410
	 * ports to a specific level.
411
	 */
412
	public function upgradeToLevel(int $level) : void {
413
		if ($this->isCachedVersion()) {
414
			throw new Exception('Cannot upgrade a cached port!');
415
		}
416
		while ($this->getLevel() < $level) {
417
			$this->doUpgrade();
418
		}
419
		while ($this->getLevel() > $level) {
420
			$this->doDowngrade();
421
		}
422
	}
423
424
	/**
425
	 * Returns the good class associated with the given level.
426
	 * If no level specified, will use the current port level.
427
	 * This is useful for determining what trade goods to add/remove.
428
	 */
429
	protected function getGoodClassAtLevel(int $level = null) : int {
430
		if ($level === null) {
431
			$level = $this->getLevel();
432
		}
433
		if ($level <= 2) {
434
			return 1;
435
		} elseif ($level <= 6) {
436
			return 2;
437
		} else {
438
			return 3;
439
		}
440
	}
441
442
	protected function selectAndAddGood(int $goodClass) : array {
443
		$GOODS = Globals::getGoods();
444
		shuffle($GOODS);
445
		foreach ($GOODS as $good) {
446
			if (!$this->hasGood($good['ID']) && $good['Class'] == $goodClass) {
447
				$transactionType = rand(1, 2) == 1 ? TRADER_BUYS : TRADER_SELLS;
448
				$this->addPortGood($good['ID'], $transactionType);
449
				return $good;
450
			}
451
		}
452
		throw new Exception('Failed to add a good!');
453
	}
454
455
	protected function doUpgrade() : void {
456
		if ($this->isCachedVersion()) {
457
			throw new Exception('Cannot upgrade a cached port!');
458
		}
459
460
		$this->increaseLevel(1);
461
		$goodClass = $this->getGoodClassAtLevel();
462
		$this->selectAndAddGood($goodClass);
463
464
		if ($this->getLevel() == 1) {
465
			// Add 2 extra goods when upgrading to level 1 (i.e. in Uni Gen)
466
			$this->selectAndAddGood($goodClass);
467
			$this->selectAndAddGood($goodClass);
468
		}
469
	}
470
471
	public function getUpgradeRequirement() : int {
472
//		return round(exp($this->getLevel()/1.7)+3)*1000000;
473
		return $this->getLevel() * 1000000;
474
	}
475
476
	/**
477
	 * Manually set port goods.
478
	 * Input must be an array of good_id => transaction.
479
	 * Only modifies goods that need to change.
480
	 * Returns false on invalid input.
481
	 */
482
	public function setPortGoods(array $goods) : bool {
483
		// Validate the input list of goods to make sure we have the correct
484
		// number of each good class for this port level.
485
		$givenClasses = [];
486
		foreach (array_keys($goods) as $goodID) {
487
			$givenClasses[] = Globals::getGood($goodID)['Class'];
488
		}
489
		$expectedClasses = [1, 1]; // Level 1 has 2 extra Class 1 goods
490
		foreach (range(1, $this->getLevel()) as $level) {
491
			$expectedClasses[] = $this->getGoodClassAtLevel($level);
492
		}
493
		if ($givenClasses != $expectedClasses) {
494
			return false;
495
		}
496
497
		// Remove goods not specified or that have the wrong transaction
498
		foreach ($this->getAllGoodIDs() as $goodID) {
499
			if (!isset($goods[$goodID]) || !$this->hasGood($goodID, $goods[$goodID])) {
500
				$this->removePortGood($goodID);
501
			}
502
		}
503
		// Add goods
504
		foreach ($goods as $goodID => $trans) {
505
			$this->addPortGood($goodID, $trans);
506
		}
507
		return true;
508
	}
509
510
	/**
511
	 * Add good with given ID to the port, with transaction $type
512
	 * as either "Buy" or "Sell", meaning the player buys or sells.
513
	 * If the port already has this transaction, do nothing.
514
	 *
515
	 * NOTE: make sure to adjust the port level appropriately if
516
	 * calling this function directly.
517
	 */
518
	public function addPortGood(int $goodID, string $type) : void {
519
		if ($this->isCachedVersion()) {
520
			throw new Exception('Cannot update a cached port!');
521
		}
522
		if ($this->hasGood($goodID, $type)) {
523
			return;
524
		}
525
526
		$this->goodIDs['All'][] = $goodID;
527
		$this->goodIDs[$type][] = $goodID;
528
		// sort ID arrays, since the good ID might not be the largest
529
		sort($this->goodIDs['All']);
530
		sort($this->goodIDs[$type]);
531
532
		$this->goodAmounts[$goodID] = Globals::getGood($goodID)['Max'];
533
		$this->cacheIsValid = false;
534
		$this->db->write('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()) . ')');
535
		$this->db->write('DELETE FROM route_cache WHERE game_id=' . $this->db->escapeNumber($this->getGameID()));
536
	}
537
538
	/**
539
	 * Remove good with given ID from the port.
540
	 * If the port does not have this good, do nothing.
541
	 *
542
	 * NOTE: make sure to adjust the port level appropriately if
543
	 * calling this function directly.
544
	 */
545
	public function removePortGood(int $goodID) : void {
546
		if ($this->isCachedVersion()) {
547
			throw new Exception('Cannot update a cached port!');
548
		}
549
		if (!$this->hasGood($goodID)) {
550
			return;
551
		}
552
		if (($key = array_search($goodID, $this->goodIDs['All'])) !== false) {
553
			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

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