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
Push — master ( 9c91a5...05804d )
by Dan
33s queued 16s
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