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 — master ( 9eaac4...d9a8b1 )
by Dan
04:15
created

AbstractSmrPort::getRaceName()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 2
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 1
c 0
b 0
f 0
dl 0
loc 2
rs 10
cc 1
nc 1
nop 0
1
<?php declare(strict_types=1);
2
class AbstractSmrPort {
3
	use Traits\RaceID;
4
5
	protected static $CACHE_PORTS = array();
6
	protected static $CACHE_CACHED_PORTS = array();
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.9; // fraction of credits for looting
29
	const RAZE_PAYOUT = 0.75; // fraction of base payout for razing
30
	
31
	protected $db;
32
	
33
	protected $gameID;
34
	protected $sectorID;
35
	protected $shields;
36
	protected $combatDrones;
37
	protected $armour;
38
	protected $reinforceTime;
39
	protected $attackStarted;
40
	protected $level;
41
	protected $credits;
42
	protected $upgrade;
43
	protected $experience;
44
45
	protected $goodIDs = array('All' => array(), 'Sell' => array(), 'Buy' => array());
46
	protected $goodAmounts;
47
	protected $goodAmountsChanged = array();
48
	protected $goodDistances;
49
	
50
	protected $cachedVersion = false;
51
	protected $cachedTime = TIME;
52
	protected $cacheIsValid = true;
53
	
54
	protected $SQL;
55
	
56
	protected $hasChanged = false;
57
	protected $isNew = false;
58
	
59
	public static function refreshCache() {
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() {
68
		self::$CACHE_PORTS = array();
69
		self::$CACHE_CACHED_PORTS = array();
70
	}
71
72
	public static function getGalaxyPorts($gameID, $galaxyID, $forceUpdate = false) {
73
		$db = new SmrMySqlDatabase();
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($gameID, $sectorID, $forceUpdate = false, $db = null) {
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($gameID, $sectorID) {
96
		$db = new SmrMySqlDatabase();
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($gameID, $sectorID) {
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() {
117
		foreach (self::$CACHE_PORTS as $gamePorts) {
118
			foreach ($gamePorts as $port) {
119
				$port->update();
120
			}
121
		}
122
	}
123
124
	public static function getBaseExperience($cargo, $distance) {
125
		return ($cargo / 13) * $distance;
126
	}
127
	
128
	protected function __construct($gameID, $sectorID, $db = null) {
129
		$this->db = new SmrMySqlDatabase();
130
		$this->SQL = 'sector_id = ' . $this->db->escapeNumber($sectorID) . ' AND game_id = ' . $this->db->escapeNumber($gameID);
131
132
		if (isset($db)) {
133
			$this->isNew = !$db->hasField('game_id');
134
		} else {
135
			$db = $this->db;
136
			$db->query('SELECT * FROM port WHERE ' . $this->SQL . ' LIMIT 1');
137
			$this->isNew = !$db->nextRecord();
138
		}
139
140
		$this->gameID = (int)$gameID;
141
		$this->sectorID = (int)$sectorID;
142
		if (!$this->isNew) {
143
			$this->shields = $db->getInt('shields');
144
			$this->combatDrones = $db->getInt('combat_drones');
145
			$this->armour = $db->getInt('armour');
146
			$this->reinforceTime = $db->getInt('reinforce_time');
147
			$this->attackStarted = $db->getInt('attack_started');
148
			$this->raceID = $db->getInt('race_id');
149
			$this->level = $db->getInt('level');
150
			$this->credits = $db->getInt('credits');
151
			$this->upgrade = $db->getInt('upgrade');
152
			$this->experience = $db->getInt('experience');
153
			
154
			$this->checkDefenses();
155
			$this->getGoods();
156
			$this->checkForUpgrade();
157
		} else {
158
			$this->shields = 0;
159
			$this->combatDrones = 0;
160
			$this->armour = 0;
161
			$this->reinforceTime = 0;
162
			$this->attackStarted = 0;
163
			$this->raceID = 1;
164
			$this->level = 0;
165
			$this->credits = 0;
166
			$this->upgrade = 0;
167
			$this->experience = 0;
168
		}
169
	}
170
	
171
	public function checkDefenses() {
172
		if (!$this->isUnderAttack()) {
173
			$defences = self::BASE_DEFENCES + $this->getLevel() * self::DEFENCES_PER_LEVEL;
174
			$cds = self::BASE_CDS + $this->getLevel() * self::CDS_PER_LEVEL;
175
			// Upgrade modifier
176
			$defences += max(0, IRound(self::DEFENCES_PER_LEVEL * $this->getUpgrade() / $this->getUpgradeRequirement()));
177
			$cds += max(0, IRound(self::CDS_PER_LEVEL * $this->getUpgrade() / $this->getUpgradeRequirement()));
178
			// Credits modifier
179
			$defences += max(0, IRound(self::DEFENCES_PER_TEN_MIL_CREDITS * $this->getCredits() / 10000000));
180
			$cds += max(0, IRound(self::CDS_PER_TEN_MIL_CREDITS * $this->getCredits() / 10000000));
181
			// Defences restock (check for fed arrival)
182
			if (TIME < $this->getReinforceTime() + self::TIME_FEDS_STAY) {
183
				$federalMod = (self::TIME_FEDS_STAY - (TIME - $this->getReinforceTime())) / self::TIME_FEDS_STAY;
184
				$federalMod = max(0, IRound($federalMod * self::MAX_FEDS_BONUS));
185
				$defences += $federalMod;
186
				$cds += IRound($federalMod / 10);
187
			}
188
			$this->setShields($defences);
189
			$this->setArmour($defences);
190
			$this->setCDs($cds);
191
			if ($this->getCredits() == 0) {
192
				$this->setCreditsToDefault();
193
			}
194
			$this->db->query('DELETE FROM player_attacks_port WHERE ' . $this->SQL);
195
		}
196
	}
197
	
198
	/**
199
	 * Used for the automatic resupplying of all goods over time
200
	 */
201
	private function restockGood($goodID, $secondsSinceLastUpdate) {
202
		if ($secondsSinceLastUpdate <= 0) {
203
			return;
204
		}
205
206
		$goodClass = Globals::getGood($goodID)['Class'];
207
		$refreshPerHour = self::BASE_REFRESH_PER_HOUR[$goodClass] * $this->getGame()->getGameSpeed();
208
		$refreshPerSec = $refreshPerHour / 3600;
209
		$amountToAdd = IFloor($secondsSinceLastUpdate * $refreshPerSec);
210
211
		// We will not save automatic resupplying in the database,
212
		// because the stock can be correctly recalculated based on the
213
		// last_update time. We will only do the update for player actions
214
		// that affect the stock. This avoids many unnecessary db queries.
215
		$doUpdateDB = false;
216
		$amount = $this->getGoodAmount($goodID);
217
		$this->setGoodAmount($goodID, $amount + $amountToAdd, $doUpdateDB);
218
	}
219
	
220
	// Sets the class members that identify port trade goods
221
	private function getGoods() {
222
		if ($this->isCachedVersion()) {
223
			throw new Exception('Cannot call getGoods on cached port');
224
		}
225
		if (empty($this->goodIDs['All'])) {
226
			$this->db->query('SELECT * FROM port_has_goods WHERE ' . $this->SQL . ' ORDER BY good_id ASC');
227
			while ($this->db->nextRecord()) {
228
				$goodID = $this->db->getInt('good_id');
229
				$transactionType = $this->db->getField('transaction_type');
230
				$this->goodAmounts[$goodID] = $this->db->getInt('amount');
231
				$this->goodIDs[$transactionType][] = $goodID;
232
				$this->goodIDs['All'][] = $goodID;
233
234
				$secondsSinceLastUpdate = TIME - $this->db->getInt('last_update');
235
				$this->restockGood($goodID, $secondsSinceLastUpdate);
236
			}
237
		}
238
	}
239
240
	private function getVisibleGoods($transaction, AbstractSmrPlayer $player = null) {
241
		$goodIDs = $this->goodIDs[$transaction];
242
		if ($player == null) {
243
			return $goodIDs;
244
		} else {
245
			return array_filter($goodIDs, function($goodID) use ($player) {
246
				$good = Globals::getGood($goodID);
247
				return $player->meetsAlignmentRestriction($good['AlignRestriction']);
248
			});
249
		}
250
	}
251
252
	/**
253
	 * Get IDs of goods that can be sold by $player to the port
254
	 */
255
	public function getVisibleGoodsSold(AbstractSmrPlayer $player = null) {
256
		return $this->getVisibleGoods('Sell', $player);
257
	}
258
259
	/**
260
	 * Get IDs of goods that can be bought by $player from the port
261
	 */
262
	public function getVisibleGoodsBought(AbstractSmrPlayer $player = null) {
263
		return $this->getVisibleGoods('Buy', $player);
264
	}
265
	
266
	public function getAllGoodIDs() {
267
		return $this->goodIDs['All'];
268
	}
269
	
270
	/**
271
	 * Get IDs of goods that can be sold to the port
272
	 */
273
	public function getSoldGoodIDs() {
274
		return $this->goodIDs['Sell'];
275
	}
276
	
277
	/**
278
	 * Get IDs of goods that can be bought from the port
279
	 */
280
	public function getBoughtGoodIDs() {
281
		return $this->goodIDs['Buy'];
282
	}
283
	
284
	public function getGood($goodID) {
285
		if ($this->hasGood($goodID)) {
286
			return Globals::getGood($goodID);
287
		} else {
288
			$return = false;
289
			return $return;
290
		}
291
	}
292
	
293
	public function getGoodDistance($goodID) {
294
		if (!isset($this->goodDistances[$goodID])) {
295
			$x = $this->getGood($goodID);
296
			if ($x === false) {
297
				throw new Exception('This port does not have this good!');
298
			}
299
			if ($this->hasGood($goodID, 'Buy')) {
300
				$x['TransactionType'] = 'Sell';
301
			} else {
302
				$x['TransactionType'] = 'Buy';
303
			}
304
			$di = Plotter::findDistanceToX($x, $this->getSector(), true);
305
			if (is_object($di)) {
306
				$di = $di->getRelativeDistance();
307
			}
308
			$this->goodDistances[$goodID] = max(1, $di);
309
		}
310
		return $this->goodDistances[$goodID];
311
	}
312
	
313
	/**
314
	 * Returns the transaction type for this good (Buy or Sell).
315
	 * Note: this is the player's transaction, not the port's.
316
	 */
317
	public function getGoodTransaction($goodID) {
318
		foreach (array('Buy', 'Sell') as $transaction) {
319
			if ($this->hasGood($goodID, $transaction)) {
320
				return $transaction;
321
			}
322
		}
323
	}
324
	
325
	public function hasGood($goodID, $type = false) {
326
		if ($type === false) {
327
			$type = 'All';
328
		}
329
		return in_array($goodID, $this->goodIDs[$type]);
330
	}
331
	
332
	private function setGoodAmount($goodID, $amount, $doUpdate = true) {
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($goodID) {
350
		return $this->goodAmounts[$goodID];
351
	}
352
	
353
	public function decreaseGood(array $good, $amount, $doRefresh) {
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($goodID, $amount) {
362
		$this->setGoodAmount($goodID, $this->getGoodAmount($goodID) + $amount);
363
	}
364
	
365
	public function decreaseGoodAmount($goodID, $amount) {
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($classTraded, $amountTraded) {
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, $goodsTraded, $exp) {
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, $goodsTraded, $idealPrice, $bargainPrice, $exp) {
393
		$this->tradeGoods($good, $goodsTraded, $exp);
394
		$this->increaseUpgrade(min(max($idealPrice, $goodsTraded * 1000), $bargainPrice));
395
		$this->increaseCredits($bargainPrice);
396
	}
397
	
398
	public function sellGoods(array $good, $goodsTraded, $idealPrice, $bargainPrice, $exp) {
0 ignored issues
show
Unused Code introduced by
The parameter $idealPrice is not used and could be removed. ( Ignorable by Annotation )

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

398
	public function sellGoods(array $good, $goodsTraded, /** @scrutinizer ignore-unused */ $idealPrice, $bargainPrice, $exp) {

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
Unused Code introduced by
The parameter $bargainPrice is not used and could be removed. ( Ignorable by Annotation )

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

398
	public function sellGoods(array $good, $goodsTraded, $idealPrice, /** @scrutinizer ignore-unused */ $bargainPrice, $exp) {

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
399
		$this->tradeGoods($good, $goodsTraded, $exp);
400
	}
401
	
402
	public function stealGoods(array $good, $goodsTraded) {
403
		$this->decreaseGood($good, $goodsTraded, false);
404
	}
405
	
406
	public function checkForUpgrade() {
407
		if ($this->isCachedVersion()) {
408
			throw new Exception('Cannot upgrade a cached port!');
409
		}
410
		$upgrades = 0;
411
		while ($this->upgrade >= $this->getUpgradeRequirement() && $this->level < 9) {
412
			++$upgrades;
413
			$this->decreaseUpgrade($this->getUpgradeRequirement());
414
			$this->decreaseCredits($this->getUpgradeRequirement());
415
			$this->doUpgrade();
416
		}
417
		return $upgrades;
418
	}
419
	
420
	/**
421
	 * This function should only be used in universe creation to set
422
	 * ports to a specific level.
423
	 */
424
	public function upgradeToLevel($level) {
425
		if ($this->isCachedVersion()) {
426
			throw new Exception('Cannot upgrade a cached port!');
427
		}
428
		while ($this->getLevel() < $level) {
429
			$this->doUpgrade();
430
		}
431
		while ($this->getLevel() > $level) {
432
			$this->doDowngrade();
433
		}
434
	}
435
436
	/**
437
	 * Returns the good class associated with the given level.
438
	 * If no level specified, will use the current port level.
439
	 * This is useful for determining what trade goods to add/remove.
440
	 */
441
	protected function getGoodClassAtLevel($level = false) {
442
		if ($level === false) {
443
			$level = $this->getLevel();
444
		}
445
		if ($level <= 2) {
446
			return 1;
447
		} elseif ($level <= 6) {
448
			return 2;
449
		} else {
450
			return 3;
451
		}
452
	}
453
454
	protected function selectAndAddGood($goodClass) {
455
		$GOODS = Globals::getGoods();
456
		shuffle($GOODS);
457
		foreach ($GOODS as $good) {
458
			if (!$this->hasGood($good['ID']) && $good['Class'] == $goodClass) {
459
				$transactionType = rand(1, 2) == 1 ? 'Buy' : 'Sell';
460
				$this->addPortGood($good['ID'], $transactionType);
461
				return $good;
462
			}
463
		}
464
		throw new Exception('Failed to add a good!');
465
	}
466
	
467
	protected function doUpgrade() {
468
		if ($this->isCachedVersion()) {
469
			throw new Exception('Cannot upgrade a cached port!');
470
		}
471
472
		$this->increaseLevel(1);
473
		$goodClass = $this->getGoodClassAtLevel();
474
		$this->selectAndAddGood($goodClass);
475
476
		if ($this->getLevel() == 1) {
477
			// Add 2 extra goods when upgrading to level 1 (i.e. in Uni Gen)
478
			$this->selectAndAddGood($goodClass);
479
			$this->selectAndAddGood($goodClass);
480
		}
481
	}
482
	
483
	public function getUpgradeRequirement() {
484
//		return round(exp($this->getLevel()/1.7)+3)*1000000;
485
		return $this->getLevel() * 1000000;
486
	}
487
488
	/**
489
	 * Manually set port goods.
490
	 * Input must be an array of good_id => transaction.
491
	 * Only modifies goods that need to change.
492
	 * Returns false on invalid input.
493
	 */
494
	public function setPortGoods(array $goods) {
495
		// Validate the input list of goods to make sure we have the correct
496
		// number of each good class for this port level.
497
		$givenClasses = [];
498
		foreach (array_keys($goods) as $goodID) {
499
			$givenClasses[] = Globals::getGood($goodID)['Class'];
500
		}
501
		$expectedClasses = [1, 1]; // Level 1 has 2 extra Class 1 goods
502
		foreach (range(1, $this->getLevel()) as $level) {
503
			$expectedClasses[] = $this->getGoodClassAtLevel($level);
504
		}
505
		if ($givenClasses != $expectedClasses) {
506
			return false;
507
		}
508
509
		// Remove goods not specified or that have the wrong transaction
510
		foreach ($this->getAllGoodIDs() as $goodID) {
511
			if (!isset($goods[$goodID]) || !$this->hasGood($goodID, $goods[$goodID])) {
512
				$this->removePortGood($goodID);
513
			}
514
		}
515
		// Add goods
516
		foreach ($goods as $goodID => $trans) {
517
			$this->addPortGood($goodID, $trans);
518
		}
519
		return true;
520
	}
521
522
	/**
523
	 * Add good with given ID to the port, with transaction $type
524
	 * as either "Buy" or "Sell", meaning the player buys or sells.
525
	 * If the port already has this transaction, do nothing.
526
	 *
527
	 * NOTE: make sure to adjust the port level appropriately if
528
	 * calling this function directly.
529
	 */
530
	public function addPortGood($goodID, $type) {
531
		if ($this->isCachedVersion()) {
532
			throw new Exception('Cannot update a cached port!');
533
		}
534
		if ($this->hasGood($goodID, $type)) {
535
			return;
536
		}
537
538
		$this->goodIDs['All'][] = $goodID;
539
		$this->goodIDs[$type][] = $goodID;
540
		// sort ID arrays, since the good ID might not be the largest
541
		sort($this->goodIDs['All']);
542
		sort($this->goodIDs[$type]);
543
544
		$this->goodAmounts[$goodID] = Globals::getGood($goodID)['Max'];
545
		$this->cacheIsValid = false;
546
		$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(TIME) . ')');
547
		$this->db->query('DELETE FROM route_cache WHERE game_id=' . $this->db->escapeNumber($this->getGameID()));
548
	}
549
550
	/**
551
	 * Remove good with given ID from the port.
552
	 * If the port does not have this good, do nothing.
553
	 *
554
	 * NOTE: make sure to adjust the port level appropriately if
555
	 * calling this function directly.
556
	 */
557
	public function removePortGood($goodID) {
558
		if ($this->isCachedVersion()) {
559
			throw new Exception('Cannot update a cached port!');
560
		}
561
		if (!$this->hasGood($goodID)) {
562
			return;
563
		}
564
		if (($key = array_search($goodID, $this->goodIDs['All'])) !== false) {
565
			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

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